Linux:线程的概念

线程:进程内的一个执行分支,他的执行粒度比进程要细

一、通过进程引入线程

以前我们想要一个执行流,我们需要fork一个子进程,然后子进程需要拷贝 take_struct结构体+进程地址空间+页表+文件描述符表......

而当我们只创建一个task_struct,但是我们不开辟其他新的资源,那么这就是线程!

任何执行流执行,都需要有资源, 而进程地址空间是资源的窗口,所以你要么拷贝一份地址空间(进程),要么和别人一起用一份地址空间(线程)

问题:有了线程的概念后,如何理解我们以前的进程??

------>有了线程之后,我们会发现一个进程里可能会存在多个线程(线程是进程内部的执行流资源)!!也就是可以有多个执行流!!因此我们以前学习的是一个进程只有一个执行流,他其实是一种特殊的情况!不要认为一个进程能够被调度,那他就是进程的所有!

二、OS是如何管理线程的

一个进程里可能有多个线程,那么就会有问题就是:我的线程是属于哪个进程的?哪部分地址空间是属于我的?cpu调度这个进程的时候我应该去调度哪个线程?当前线程的状态是什么?当前线程的时间片是什么??......

------>基于以上诸多属性,必然要求我的OS要把他们管理起来!!

------>所以有些OS内部就会设计TCB结构体,然后再创建新的关于线程的创建、终止、等待、控制、释放等概念建立一些新的逻辑和接口 ,还得把进程和线程该关联起来......这样设计的话其实维护难度很大的,并且你会发现很多代码会和进程部分的一样,所以对于程序员来说维护两份十分相似的代码其实他的维护成本是很大的!!且容易出错效率低下!!

------>而Linux的设计者认为,虽然我们尊重OS系统的学科,对于一个新出来的概念按道理是应该进行先描述再组织,但是这并不代表我们一定要用新的方法来描述和组织,因为我们发现PCB结构体的诸多属性和概念都很符合TCB的要求。 那我为什么要单独搞一个TCB呢??你最多就是执行力度细一点(线程执行的是进程代码的一部分),代码少一点,可你不也是一个执行流么??

------>因此我们Linux的设计者采用描述"进程"的PCB结构体来模拟描述"线程",大不了在内部单独设置一些属性来区分就行了,其他很多概念接口都可以复用,大大降低了设计和维护成本

------>无论是Linux方法还是windows的方法,无论选择哪一种,他都必须得尊重OS学科的要求,但Linux的这个设计方案是特别卓越的!!

------>Linux中,虽然在我们的视角里有进程和线程的区别,但是在OS和CPU的眼里,我只有调度执行流的概念!我不管你是进程还是线程,我只会见到一个个的PCB结构体,一个PCB结构体对应着一个执行流(比如说我们的大O1调度算法,我们知道排队的是PCB结构体,但是他是属于进程还是属于线程,cpu压根不关心)

------>因此Linux基于这种设计方案,我们把内核的线程进行高度抽象,叫做轻量级进程,他是建立在内核之上并由内核支持的用户线程。

问题1:理解cpu视角的 线程<=执行流

------>对于我们的cpu来说,如果当前的平台是Linux,那么线程==执行流(因为被统一看做轻量级进程),而如果当前的平台是windows,那么线程<执行流(winows中有单独的tcb结构体,因此我们的执行流可能是PCB也可能是TCB)

问题2: 理解执行流>=进程

------>如果每个进程只有一个执行流,那么就是==,而一但在进程中引入多线程,那么一个进程里面就会存在多个执行流,此时就是>

问题3:如果理解有些教材上说"Linux没有真正意义上的线程,而是用"进程"模拟的线程"?

------> 这两句话不完全对,(1)前半句:不能说我Linux没有单独对TCB做描述,你就说我没有真正意义上的线程!!我Linux也是遵守了OS学科的线程概念的,只不过实现方法不同,如果非得杠的话,应该只能说没有真正意义删搞得TCB结构体!! (2)后半句:应该说是用进程的内核数据结构PCB模拟的线程

------>因此这两句话想表达的核心意思是 Linux相比于其他OS在内核数据结构上设计的差异!而不是对线程概念的差异!

问题4:为什么都是操作系统,但是Linux和windows存在这么大的差异??不同的平台会带来什么影响??

------>操作系统这门学科本质上是规定了操作系统应该是什么样的以及在设计上需要具备哪些概念,其实可以理解为他是一本设计操作系统的指导手册,只要我们遵守了,那么他的设计方案可以是多种多样的!!(就是设计理论和实现方案的区别,前者指导后者,缺一不可!也是理论和实际相结合)

------>OS是计算机世界的哲学,就跟马原一样,虽然不能帮助你赚钱,但是可以知道你多很多事情,教会你看清问题的本质,能够帮助你更好地做出改变!

三、重新定义线程和进程

引入一个生活的问题:承担分配社会资源的基本实体是什么??

------>应该是家庭,因为我们是以家庭为单位生活的,我们基于一些社会资源和亲情关系形成一个集体。

------>但是其实我们每个人都在这个家庭中会有自己的任务,比如说你会去上学,你的父母会去工作,你的爷爷奶奶会去跳广场舞......

------>但是不管我们领的是什么任务,我们都有一个共同的任务:过好日子!!

------>但是你为什么能够去做这些任务呢?你之所以能够上学,你的爷爷奶奶之所以能够去跳广场舞......这些和家庭所拥有的社会资源是有关系的(资源是进行任务的进本保证

------>所以想毁掉一个家庭,那就剥夺所有资源就行了

------>那为什么要去做这么任务呢??也是为了创建更多的资源。

------>所以我们的家庭就相当于是进程,而家庭成员就相当于是线程,每个小任务就是不同的执行流,共同任务就是我们最终想达到的目的,而所有线程共用进程地址空间的资源,如果想创建资源就是像OS申请,想释放资源就是OS回收......

基于以上的引入,我们得到两个结论:

1、线程是OS调度的基本单位

2、进程是承担分配资源的基本实体(而线程就是进程内部的执行流资源!)

四、页表虚拟地址和物理地址的转化原理

我们之前所理解的页表,只知道其中有虚拟地址、物理地址、当前是否加载到内存中的标记位、权限位...... 但是页表真的如下图所示吗??

假设是32位 那么地址的可能性就是2^32个 而虚拟地址+物理地址 一共8个字节(先不管其他的)那么就是2^35字节 (大约是35GB!) 那这不就扯淡了吗,内存才多大呢???所以具体的实现方案肯定不是这样的!!

问题1:所以OS底层究竟是如何做虚拟地址到物理地址的转换呢??

------>以32位为例,其实是 10(页目录)+10(二级页表)+12(页框偏移量)

(1)CPU取得虚拟地址保存在CR3寄存器中

(2)MMU在CR3寄存器查到页目录的地址

(3)MMU根据虚拟地址的前10位,标识页目录中的偏移量,该偏移量对应的地址保存的是二级页表的地址

(4)MMU得到二级页表的地址,根据中间10位标识页表偏移量,在得到页框的起始地址(定位4kb的内存块)以及一些特殊数据(比如权限位)

(5)MMU最后在加上虚拟地址的后12位(表示页框偏移量),得到具体的物理地址并告知CPU!

问题2:这样设计的优势在哪里呢??

------>先对比极端情况

虚拟地址的前10位(转成10进制就是数组下标) ,一共有2^10次方组合,然后每个都保存着二级页表的地址(4字节),所以一共需要 2^12字节

而找到二级页表的地址之后,我们通过虚拟地址的中间10位(转成10进制就是数组下标),也是2^10的组合,而每一个位置都要存储页框的地址,但是由于页框固定是4KB大小的,所以只需要保持20个bit就行 然后再算上可能有一些位置保存权限位或者其他之类的 姑且也算4个字节,所以一共需要2^12字节

接下来找到页框后就好办了 直接加上虚拟地址的后12位就是偏移量了!! 因此我们会发现一共需要2^24字节左右(大概16MB) 比之前2^35字节少了特别多!!

------>并且其实大多数情况下,我们的页目录必须是一开始就创建好的!(2^12字节必须有)!但是二级页表大部分情况下是不全的,而且我们可以通过查看二级页表对应的位置是否是空的,就可以判断数据是否已经被加载到内存中了!!

问题3:如果出现越界等页错误的情况导致虚拟地址转化失败了,cpu是怎么处理的?

------> 一旦发生转化失败,那么cr2寄存器会将转化失败的虚拟地址保存起来,方便告知上层

问题5:为什么4个字节明明有4个地址,但是我们取地址的时候只拿到了一个地址呢??

c、cpp中的变量无论多大,都只需要有起始地址,因为空间是连续的,所以我们通过起始地址+类型大小就可以确定这个变量的范围!

问题6: 你cpu是怎么知道类型的呢??

------>CPU并没有类型的概念!!因为编译器在编译的时候已经根据不同的类型生成不同的机器码了!所以cpu读取汇编指令的时候本身就知道要读几个字节,所以我们只需要通过软件定位到了起始地址,后面具体读多少偏移量CPU是能通过汇编知道的!

问题7:编译器在编译的时候肯定只知道内置类型的机器码啊,那我们的自定义类型编译器是怎么知道的??

------>在编译器的眼中,无论你这个类有多大,我都会把你单纯地看成很多内置类型的集合!!而cpu在被设计的时候读取压实按照1/2/4/8的大小去读的,他只会一点一点地去读!因为你用的时候也是一点点去用的,不会一下子就把整个类给丢进去!在他眼里没有类型的概念

问题8:那么我们是如何给线程分配资源的呢???

------> 我们定义的每个函数在进程空间上的地址都是独立的,所以假设我们将某个函数专门交给一个函数去运行,那么他就天然地将地址空给给他做了划分!

五、线程VS进程

线程比进程更加轻量化,为什么??

1、创建和释放更加轻量化(整个生命周期的生死)

这个应该很好理解,特别是我们通过页表从虚拟地址到物理地址的转化也可以发现,创建一个进程是一个很"重"的事情

2、切换更加轻量化(运行)

切换线程的话,因为一个进程内资源共享,所以页表和进程地址空间不需要切换! 而切换进程则需要切换新的页表和地址空间!!

问题1:可是,这不就是寄存器做了点小工作么??凭什么可以说切换会快很多呢??

------>因为CPU重存储着一个硬件级别缓存cache

cpu觉得和内存交互数据还是太慢了,所以虽然他只能按照很小的字节数去读取,但是他在访问的时候一般会直接把一整个大块数据都放到cache(缓存的热数据)里,这样根据局部性原理一般来说一般接下去要访问的内容都是跟当前内容相邻的,所以这样的话就有一部分资源是在cpu内部读取的,读取会变快!

并且一般来说,我们的cache全称叫做LRU cache,他的设计就是会将最近最少使用的数据替换掉!!所以越往后读取我们的速度会越来越快,因为缓存的命中率在变高(随着时间的推移里面放的都是高频的被访问的内容)

所以切换线程的时候,虽然上下文发生了变化,但是缓存却不会有很大的变化,而如果是切换进程,则会让整个缓存的热数据都需要被丢弃,然后新的进程由冷变热还会消耗很多时间!(因为缓存空间很大!!)

问题2:我cpu眼里都是轻量级进程,我怎么知道我切换的是进程还是线程呢??

------>PCB结构体里是有身份标识的,我们把刚创建的父进程一般叫做主线程,然后其他次线程虽然都跟主线程一样的pid 但是他们也都有自己的独立身份标识tid,所以我们可以通过标识 来判断当前做的是进程切换还是线程切换

3、时间片需要重新分配

如果一个进程想要创建多线程,他的时间片必须要重新被所有线程给分配,这样才能保证不会因为线程的增到导致进程的时间片变长,这样会影响到其他的进程。

六、线程的优缺点

优点:

1、创建一个新线程的代价要比创建一个新进程小得多

2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

3、线程占用的资源要比进程少很多

4、能充分利用多处理器的可并行数量

5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现(一般来说几个cpu就几个进程,这样就省去了切换时间)

7、I/O密集型应用,为了提高性能,将I/O操作重叠。(线程比cpu多一点 因为io是可以重叠的)

8、线程可以同时等待不同的I/O操作
缺点

1、性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。

2、健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

3、缺乏访问控制 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

4、编程难度提高 编写与调试一个多线程程序比单线程程序困难得多

七、线程的异常

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

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

八、线程的用途

合理的使用多线程,能提高CPU密集型程序的执行效率

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是 多线程运行的一种表现)

相关推荐
风无雨5 分钟前
GO 启动 简单服务
开发语言·后端·golang
斯普信专业组11 分钟前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
我是苏苏2 小时前
C#基础:Winform桌面开发中窗体之间的数据传递
开发语言·c#
古希腊数通小白(ip在学)2 小时前
stp拓扑变化分类
运维·服务器·网络·智能路由器
斐波娜娜2 小时前
Maven详解
java·开发语言·maven
小码氓2 小时前
Java填充Word模板
java·开发语言·spring·word
暮鹤筠3 小时前
[C语言初阶]操作符
c语言·开发语言
l1x1n04 小时前
Vim 编辑器常用操作详解(新手快速上手指南)
linux·编辑器·vim
12点一刻4 小时前
搭建自动化工作流:探寻解放双手的有效方案(2)
运维·人工智能·自动化·deepseek
未来之窗软件服务4 小时前
东方仙盟AI数据中间件使用教程:开启数据交互与自动化应用新时代——仙盟创梦IDE
运维·人工智能·自动化·仙盟创梦ide·东方仙盟·阿雪技术观