线程+地址空间4


线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细
很多教材喜欢这么说,这只是一个线程的特征之一,来解释线程。

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个函数每一个函数内部所有地址都是互相独立的,所以把每个函数给每个线程去跑,天然在函数上就划分好了。这就叫线程之间代码就分离了
你做了什么呢?
你只需要在代码中编译,编译好让所有线程执行不同函数就行了
所以线程就跑起来了

4.Linux线程周边的概念

线程 VS 进程 主要讲切换

线程比进程要更轻量化,为什么?
a.创建 和 释放 更加轻量化(生死)
创建线程只需要创建PCB就可以了,进程申请资源的所有工作比线程多很多
释放 那就释放PCB就可以了,资源不用管,只有最后一个PCB释放才会释放进程所有资源
b.切换更加轻量化(运行)
运行执行代码,你以前要执行一大批代码,现在你只需要执行一部分。
结论:a,b一结合 线程整个生命周期都比进程更加轻量化

切换
线程在切换调度时,肯定要有自己对应的CPU上下文。
线程在切换时,页表不用切换,地址空间不用切换,它只是局部切换
所以它切换的效率会更高
但是有人说一个线程执行时,它对应的CPU上下文切换了更少的寄存器,这效率能有多大提升?
CPU中除了有寄存器,CPU还会以进程为载体还会缓存最高频的数据或代码
线程在执行,本质上就是进程在执行,因为线程是进程的执行分支
在这里插入图片描述
我们 把cache称为进程运行时缓存的热数据,相当于这部分数据高频被访问。
所以调度时,一个进程内的多个线程在CPU上进行调度
CPU切换时,切换一个进程内的多个线程
虽然上下文一直变化,但是缓存内数据一直不变,或者做少量更新
不需要进行cache的保存。

如果整个进程所有线程的时间片都用完了,整个进程也要被切换
CPU调度的基本单位是线程,但进程也需要被调度
要不然怎么进行进程间切换呢,如果进程不切换怎么让其他进程运行呢
所以整个进程所有线程的时间片都用完了,整个进程也要被切换,可是进程切换时上下文要保存,更重要的是热缓冲数据要被直接丢弃,另一个进程放上去要重新缓冲新数据,
所以又是花费一段时间

所以线程间切换效率更高不仅仅是个别寄存器不需要,更重要的是同一个进程内的多个线程切换时对应的cache数据不需要重新缓存。
这才是线程切换为什么效率更高的原因。


在这里插入图片描述

一个线程被创建时它也要有自己的时间片,它的时间片来自于它对应进程的
时间片也是资源,所以创建每一个线程不能给每个线程重新申请时间片,
必须把整个进程整体时间片,平均划分给每一个线程,因为如果你直接给线程重新分配
时间片,就会变相导致整个进程的时间片变多了。
那我怎么知道是进程该切换还是线程该切换呢?
所以PCB其实有身份标识的,所以把第一个PCB叫做主线程,其他新创建的线程叫做新线程
OS能识别出你当前线程内切换,还是进程时间片跑完了
例如切换时,主线程标识了进程整体时间片 4和 自己分得的时间片1,每个新线程标识了自己分得到的时间片1

线程的缺点

因为线程缺乏访问控制,导致健壮性降低

一个进程内整个地址空间内线程能看到大部分资源除了栈大部分都被线程共享这就叫缺乏访问控制,那么一个线程运行时就可能影响另一个线程导致健壮性降低

一个线程如果除0 or 野指针 导致整个进程挂掉,因为线程做执行动作就是你的进程在执行

你的线程除0 野指针本质是你进程收到信号了,每个线程都要执行信号的默认处理方法

由此带来的问题就是编程难度提高

线程异常

线程代表的就是进程,单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
进程内的所有线程也就随即退出

进程和线程对比-哪些资源共享哪些资源独立

线程共享大部分进程数据,但也拥有自己的一部分私有数据

线程ID 因为线程要独立调度

一组寄存器 线程对应CPU上下文数据


线程要有自己独立的栈结构,小到函数里定义变量,大到函数间调转形成栈帧
栈上保存了程序运行时形成的各种临时变量所以栈是运行时数据,每个线程必须有自己独立的栈结构

线程独立资源中最重要的两个字段,线程的上下文,线程需要独立的栈结构
为什么呢?
独立的上下文体现出线程是被独立被调度的,独立的栈结构体现出线程之间运行是互相不会出现执行流错乱的问题,前提是你代码写的对

errno

信号屏蔽字

调度优先级

进程的多个线程共享 同一地址空间,因此代码、数据都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
如果一个线程打开了一个文件,其他线程也能看到这个文件,因为所有线程共享同一个文件描述符表,每个线程的PCB指向同一张fd表
在这里插入图片描述

每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)

当前工作目录

用户id和组id

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值