文章目录
- 1.Linux中线程该如何理解
- 2.重新定义线程 和 进程
- 3.重谈地址空间 --- 第四讲
- 4.Linux线程周边的概念
线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细
很多教材喜欢这么说,这只是一个线程的特征之一,来解释线程。
1.Linux中线程该如何理解
地址空间是进程的资源窗口!
你的进程想访问用户空间和访问OS都必须通过地址空间去查看,加上页表转化去物理内存中去找
我们以前创建子进程,父子进程在数据结构上相互独立,需要给子进程开辟新的pcb,地址空间,页表,虽然大部分都是从父进程来的。
如果我今天再创建一个“进程” 不再给它创建新的地址空间,新的页表,我只创建pcb,让其指向父进程的地址空间,也指向父进程的页表(我们不管)
他们两个pcb把代码区分一部分,全局数据区分一部分,堆区分一部分给这个新的“进程”,或者两个Pcb直接共享某个区域
此时新的子进程就在父进程的地址空间内运行,同时把页表中对应分出来区域的映射给新的子进程就可以了
既然能创建一个,就能创建多个,只创建pcb,把一个进程的地址空间分成若干份分给子进程部分代码。
此时线程的执行粒度,要比进程更细,为什么这么说?
以前主进程执行全部代码,现在只需要执行全部代码一部分。
它只执行其中一部分,所以把它称为进程内的一个执行分支。
为了区分和fork创建子进程的区别,把这种形式的进程起个名字叫做 线程
Linux实现方案 – 结论
1.在Linux中,线程在进程“内部”执行,更详细的说是线程在进程的地址空间内运行(为什么? )
任何执行流要执行,都要有资源! 地址空间是进程的资源窗口
你进程或线程你要执行你要不要代码,没有代码和数据你就跑不起来,你要的话就得在地址空间内要。
2.在Linux中,线程的执行粒度要比进程要更细? 线程执行进程代码的一部分
站在CPU角度它知不知道哪个task_struct是进程,哪个是线程?或者他需不需要知道?
cpu不需要关心它执行的是进程还是线程,它只有调度执行流的概念
cpu要代码和数据你就给他,让他找到代码和数据执行就行了
2.重新定义线程 和 进程
什么叫做线程呢?我们给个定义
我们认为,线程操作系统调度的基本单位!
(我们以前可根本没说进程是OS基本调度单位,没说过。)
以前不都是拿着个进程调度来调度去,那什么叫进程呢?
所有的执行流都叫进程执行流,地址空间都叫做进程所占有的资源,页表和进程在物理内存中代码和数据,把这一整套称之为进程
重新理解进程? 内核观点: 进程是承担分配系统资源的基本实体
所以显而易见地址空间页表代码和数据都是要占资源的,那pcb执行流是资源吗?
是的
不要认为一个进程能被调度它就是进程的所有,它只是进程内部的一个对应的执行流资源被cpu执行了。
线程只是进程概念中的基本调度单元,所以进程和线程的关系是进程内部包含线程,因为进程是承担分配系统资源的基本实体,而你线程是我进程内部的执行流资源。
以前给的进程概念:进程= 内核数据结构(task_struct) +代码和数据— v1
当然也是对的,指的是所有的内核数据结构,所有的PCB
创建进程OS会给他分配很多资源,如果你要创建线程,在进程内部创建pcb,然后把进程的资源分出一部分给你线程你去调度吧,你去执行吧。
可是如何理解我们以前的进程???
操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只不过当前进程只有一个执行流 !
复用进程数据结构和管理算法
struct task_strut ----模拟线程
如果你的资源PCB只有一个那你就是进程,如果内部有多个PCB就是线程
甚至linux中我不区分PCB是进程还是线程,我都把他叫执行流,承担分配系统资源的基本实体才是进程
(那么如果我们真正的一个进程内部要有对应的线程,所以进程和线程它对应的比例一定是1:n的至少是1:1,也就是一个进程里面应该有多个线程
所以你这个线程执行的时候,那么当前的状态是什么?那么你这个线程当前执行到什么位置了?当前需要访问哪些资源?即将访问哪些资源?你这个线程是属于哪个进程的?你这个线程呢在调度的时候啊,那么什么时候被切换了?需要被切换吗?时间片有没有到等等等等啊,
第二线程可不是一创建就退出,一创建就完成,是创建才是开始,操作系统要能够调度这个线程,那要运行这个线程,切换这个线程,所以线程又多,比进程还多,你还要来对他做调度,一个问题,操作系统要不要管理线程?
那我当然要管理啊,你不管理我,我这个线程我应该属于哪个进程?我的地址空间在哪里?我的代码在哪里?我调度到哪里了,我状态是什么?
必须得管怎么管理,先描述再组织
先描述再组织,你想一想吧,曾经光光这么多的PCB就把你搞得头昏脑胀的,那么再来给你搞一大堆的tcb啊,你先描述tcb再组织,你组织一下试试,,这个线程出问题还要影响整个进程,等那个复杂的关系维护会特别特别特别特别特别复杂
Windows操作系统他就这么干了,他就给线程创建的tcp,然后再把进程和线程之间还有关联起来
那么我们Linux呢,他们是这么认为的你这个线程不也被调度吗?你线程要的代码和进程的代码,无非线程的代码少了一点儿,你也要切换,也要调度,无非就是线程的资源少一点嘛,好,那么Linux的设计者来说,我们当然要遵守人家的设计哲学,对线程要管理,先描述,再组织,可是谁规定描述必须得用新的方法来描述,谁规定描述都必须得用组织都必须得用新的,组织方式用新的来组织。
其实你的进程和线程高度类似可以复用tast_struct结构体来模拟线程,那么进程我们已经描述了,他们都有状态,有优先级,要有自己的上下文要被切换。)
我们把LInux当中的执行流,叫做轻量级进程
因为 执行流 <= 进程 你执行流要是进程 那就相等 ,执行流要是线程那就是粒度<进程
3.重谈地址空间 — 第四讲
问题:如何理解把资源分配给每个线程(执行流)?
CPU有寄存器保存当前调度进程的PCB,PCB找到地址空间就找到了,而地址空间其实也有字段找到它的页表
CR3寄存器能找到页表
物理内存分成了一个一个页框,每个页框4KB 按照字节换算 是 2^12 byte
下面重点谈页表
虚拟地址是如何转换到物理地址的?? ?
从物理内存页框内容当中读取到CPU的地址是虚拟地址,然后在CPU内部做转化找到物理地址
3位虚拟地址为例
虚 拟地址是多少位的? 32位
如何理解页表呢?
第一 32位虚拟地址 不是一个整体 而是转化成了 10 + 10 + 12 = 32
第二 页表也不是一整块的,如果他是一整块,每一个行中有虚拟地址,物理地址,权限位假设有10字节,页表被写满,有2^32个地址,也就是2 ^32行,再乘以10结果是字节进行换算大概是40G
这样整个物理内存放不下这一张页表,更别提所有进程都有页表了
所以页表不可能是我们以前画的一张大表
页表是拆成了两级的
第一级页表有1024个条目,二级页表也有1024个条目
你将来在CPU寻址读到的虚拟地址有32位 ,假如是 0000 0000 0000 0001 0000 0000 0000 0101
会从左往右按照10+10+12被拆成 0000000000 0000010000 000000000101
10,10,12每个区域都有自己的十进制数,范围:从全0到全1
用第一个十个比特位转化成十进制数 充当第一级页表的数组下标。
一级页表存放的是二级页表对应的地址,接着找到二级页表,
拿着第二个十进制数索引二级页表中的下标,
二级页表中保存的是物理内存中页框的起始地址(低地址),所以就能找到物理内存中的页框了。
一级页表一般叫 页目录
页目录里面的内容 叫 页目录表项
二级页表里面 的内容 叫 页表表项
其实只需要通过虚拟地址前20位查一级查二级页表其实已经找到对应的页框了。
接下来还有剩下的12位范围[0,2^12-1]一共2 ^12个,刚好是页框的大小,12位相当于你要访问物理内存在页框中的偏移量,用页框地址 + 虚拟地址的最后12位 = 物理地址
下面我们来算算账
先算一个页表有多大?
二级页表一行中保存了页框的起始地址 ,按4字节算,不算二级页表中的权限位
一共有1024行,也就是 4 x 1024 = 4096byte = 4kb
则一个页表是可以放进一个页框里面的
一级页表中一共有 1024个二级页表,所以所有页表大小: 1024 x 4kb = 4096 kb = 4MB
一级页表就一个4KB 没算进去
所以说这1024 个二级页表 4MB 和之前40G的大表相比 少了很多
一个进程会把整个地址空间全部用完吗?一部分地址空间根本不需要给每个进程都维护的,内核级页表只需要维护一份就行了。
每个进程只需要维护0-3G
每个进程不一定把整个地址空间全用完,二级页表不一定全部存在。
二级页表在大部分情况下都是不全的!
这样算就比4MB还小,即便是进程把页表用全了,还有页面置换算法来维护
所以极端情况是4MB,但是大部分情况进程只会用到其中很少一部分,所以进程的页表就大大减少了
页表不会很大架不住进程个数多,所以我们说创建一个进程依旧是一个很 “重” 的工作,所以才有线程存在的意义和背景。
二级页表保存的页框地址个数 和 物理内存的页框个数对着呢吗?
对着呢
这是物理内存一共有1048576 = 2 ^ 20个页框,从下往上你就算吧
一共有1024个二级页表,每个二级页表保存了1024个地址,也就是1024 X 1024 = 1048576 个地址
虚拟地址整个10 10 12划分 ,它为什么这么划分呢? 这个12为什么要有呢?
答:
根本原因就在于配合内存管理
今天页框大小是4KB,有的OS把页框干成4MB,最终页表还会更小
因为物理内存分的页框个数少了,要保存的地址也就少了,页表也就小了
它为什么是4KB呢,因为它和12是相对应的!
所以要访问一个虚拟地址时,OS怎么知道这个虚拟地址有没有加载到内存呢?
答:
1.可能你查一级页表时二级页表不存在,那就没有被加载到内存,所以缺页中断
2.可能你访问二级页表里和对应的页框并没有建立映射关系,此时也没有加载到内存,二级页表里面有标记位确认映射关系有没有。
因为二级页表只能索引到页框,所以内存管理的基本单位是4KB
你现在虚拟转物理只能找到一个字节的地址,那我们的int, double
float,各种自定义类型 怎么说?
int a = 10;
整形有4个字节,&a只拿到了一个地址,用它的低地址做代表
C/C++中任何变量只有一个地址就是内存中开辟的多字节起始地址
找到这个起始地址,根据类型连续读取多个字节就把数据读取上来了。
计算机他怎么知道我要读取几字节?
类型被CPU转化成偏移量,类型是给cpu看的,
汇编中内置了命令读取1,2字节的命令,dword ,word字
CPU和内存连着呢(冯诺依曼),软件定位到了内存中起始物理地址,CPU拷贝时它就知道拷贝几字节了
你说的是内置类型,我要是结构体,类呢?
结构体,类不都是由内置类型的集合描述的。
就算结构体很大,CPU很小,我就能读多少读多少
C/C++中任何变量只有一个地址就是内存中开辟的多字节起始地址
说白了我们访问任何一个变量都叫做起始地址+类型 本质就是 起始地址 + 偏移量
所以最终一句话
虚拟地址到物理地址的转化 它只需要查10 10 12 找到页框再在页框内索引
至此就完成了虚拟到物理的转化
CPU内部的CR3寄存器直接指向一级页表,任何进程二级页表可以没有或残缺,但任何进程必须要有一级页表存在!后面随着运行过程页表缺页中断会被填充越来越完善。
当物理地址访问时,物理地址不存在 or 越界了
CPU内部还有一个寄存器叫做CR2 保存 引起缺页中断 or 异常的虚拟地址
相当于你进行访问二级页表,而二级页表不存在,不存在你说要缺页中断 把页面调换进来在内存里申请构建映射关系,你把这些做完了我怎么知道上次访问的虚拟地址是谁呢?
当它把这个工作做完就会把CR2保存的虚拟地址拿出来重新访问。
下面在回答一下最开始的问题
如何理解资源分配?
上面所讲的这个线程,它所对应的所有的资源分配全部都是通过地址空间来的,而你所有的代码和数据都是通过我们的地址空间页表然后映射过来的,
那么现在我的问题,我们线程分配资源在地址空间角度
线程目前分配资源 , 本质就是分配地址空间范围 ,页表你就别动了。
页表,物理内存都给我分配好了,一个线程它 要分配资源,目前站在地址空间角度,因为地址空间本身就是资源窗口,线程分配资源本质就是把地址空间划分一部分,就是你用你的,我用我的,比如说把代码分成几部分给几个线程,凡是不划分的比如堆区,全局数据区所有线程全部共享
所以线程资源分配本质就是空间范围的分配,因为所有的线程也共同属于同一个进程,大家使用同一个页表映射,查同一个页表,换句话说,
你把哪一部分资源给这个线程,其实就是把对应代码范围给它就可以了
那这个划分工作难不难呢?
以代码为例,全局数据不需要划分,就是要被所有线程共享
剩下的大部分区域线程都能共享
最重要的是代码,你怎么让每一个线程执行不同的代码?
代码有地址吗?
有,所以才有函数指针的概念。代码的地址也是虚拟地址
我们可以定义上10个函数,每个函数地址都不一样,所以把一个函数交给一个线程运行它天然就在代码层面上已经做好了地址空间划分上的分开。
10个函数每一个函数内部所有地址都是互相独立的,所以把每个函数给每个线程去跑,天然在函数上就划分好了。这就叫线程之间代码就分离了
你做了什么呢?
你只需要在代码中编译,编译好让所有线程执行不同函数就行了
所以线程就跑起来了