Linux相关概念和易错知识点(27)(认识线程、页表与进程地址空间、线程资源划分)

发布于:2025-02-16 ⋅ 阅读:(25) ⋅ 点赞:(0)

目录

1.认识线程

(1)进程与线程的关系

(2)最小执行流

(3)轻量级进程(LWP)

①对task_struct的理解

②轻量级进程

③LWP和TCB的区别

2.页表与进程地址空间

(1)页表的结构

①虚拟地址和物理地址的作用

②页框和page

③多级页表

a.第一级:页目录

b.第二级:页表

c.第三级:页内偏移量(严格意义上不算一级)

④多级页表的作用

(2)虚拟地址 -> 物理地址的过程

(3)懒加载和缺页中断

(4)MMU和快表(TLB)

(5)高速缓存cache和局部性原理

(6)线程的优缺点

①优点

②缺点

3.线程资源划分

(1)代码块

(2)其它独立资源


1.认识线程

(1)进程与线程的关系

一个进程是为了完成一整个任务,而线程是完成这个任务的最小执行单位。以一个直观的说法,宏观上看这个进程是一个公司,这个公司里面有人力、资源等,就像进程那样掌管着进程地址空间。这个公司存在的使命就是完成一个大型项目。要完成一个大项目,还需要有员工分工,这些员工就是线程。每个员工作为最小执行单元完成分配给自己的子任务,期间公司会给这些员工分配资源(如电脑、办公桌),对应起来就是进程给线程分配资源(如进程地址空间的划分)。

我们前面之所以感受不到线程是因为我们创建的都是单(多)进程单线程,但除此之外还有单(多)进程多线程等待我们学习。

(2)最小执行流

进程是一个执行起来的程序,它是承担分配系统资源的基本实体,它承担完成任务的使命,但并不是进程直接去处理任务,它只负责分配资源,处理任务需要交给线程。线程就是执行流,执行粒度比进程要更细,是进程内部的一个执行分支。

就好比工人和它们小组长的关系,组长需要向上(系统)交代,完成一整个任务,但并不是组长去完成这一整个任务,它不会直接参与,而是会把任务拆开分配给工人,让工人去做。整个过程中组长就是进程,而工人就是线程,相对整个产业来看,工人又是最底层最基础的劳动力,因此线程被称为最小执行流,线程是OS调度的基本单位。

这个时候我们就发现之前我们对进程的理解只停留在对小组长进行交流,很局限,而学习线程我们可以进一步去管理工人。

(3)轻量级进程(LWP)

①对task_struct的理解

如果Linux支持线程,那么线程就必须要被OS管理,这会创造TCB(T指的是Thread,TCB和PCB相对应)这个数据结构,和PCB同时存在于系统之中,对Linux设计者来说这太复杂。

线程的本质是在进程内部运行,且粒度要更细Linux下的线程是用进程模拟实现的,复用了历史代码。

在Linux中,task_struct不再简单代表PCB,同一个进程可以有多个task_struct,我们可以简单认为一个task_struct对应一个线程。一个进程地址空间里面所有的task_struct都属于一个进程。之前我们直接让PCB和task_struct划等号,是因为我们之前遇到的都是单线程,一个进程要有task_struct,但从来没说只有一个。task_struct描述的是执行流,task_struct里面拥有一个线程需要的页表、分配的地址空间的描述。

一个进程的所有线程共享同一进程地址空间,同时代码区也被肢解分给不同线程执行,即线程在进程的地址空间内运行。

②轻量级进程

线程直接进程的资源用,相对而言进程就需要承担开辟资源的责任。我们已经知道task_struct是用来描述线程的了,它在一定程度上相当于TCB(但不等于,这点很重要)。但真正的PCB在哪呢?实际上,Linux里不存在真正意义上的PCB,而是当前进程下所有task_struct的总和描述了一个PCB等效的信息,这也是task_struct和TCB不同的点。

站在系统角度来看这个进程,没有专门管理整个进程的PCB,而是众多task_struct中的其中一个。在Linux中,task_struct既不是TCB,也不是PCB,其对应的线程也被统一称为轻量级进程LWP。

在这里,我们需要严谨的区分一个概念了。由于Linux下的task_struct不是TCB,那么用它来描述的虽然可以被称为线程,但从严谨的角度出发Linux中没有真正意义上的线程,因为线程的概念及其管理的数据结构TCB是用task_struct模拟实现的,task_struct和TCB是有区别的。

③LWP和TCB的区别

接下来我们需要回答两个问题,多个LWP是如何结合起来描述一整个PCB的等效信息呢?还有为什么LWP不能称为TCB呢?

最关键的一点是同一进程的所有task_struct共享一套进程地址空间mm_struct。虽然实例化出了多个task_struct数据结构,但它们的mm_struct指向同一块空间,它们的进程地址空间的划分并不独立,而是耦合的。也就是说,当我们修改其中一个task_struct里面描述进程地址空间的信息时,其它task_struct的相关信息也会更新。同样的,所有task_struct的页表是一样的,映射位置都一致,但由于空间划分的耦合,正常情况下读写内存不会冲突。

因此,task_struct和TCB存在一定的区别,TCB并不侧重于资源的共享,而task_struct之间互相耦合,最终也描述了PCB应该描述的信息,因此不需要专门存储一个PCB了。这样一来就能回答上面两个问题了,这也是为什么Linux中的线程要被叫做轻量级进程,其根源也是task_struct的特殊性。

例如:对于3线程的进程来说,整个进程有3个task_struct,也就是有3个LWP,这三个task_struct共同完成了对整个进程以及进程内线程的全面描述。

注意:LWP是轻量级进程,代表的是一个实体。而task_struct不叫LWP,只是描述和管理LWP的数据结构。同理,task_struct也不完全等同于PCB或者TCB。

2.页表与进程地址空间

(1)页表的结构

①虚拟地址和物理地址的作用

不同进程有不同的生命周期,这会导致物理内存可能有碎片。虚拟地址将不连续的物理地址转换成连续的,并且实现了系统层面和硬件的解耦合。

②页框和page

物理内存和文件都是以4KB的数据块划分的,通过块内的碎片减少块间的碎片,用空间换时间。

物理内存将划分好的4KB的数据块叫做页框,所有内存都被划分多个页框。

哪一个页框要刷新,哪些页框被用了?OS要对内存进行管理,管理方式就是对页框先描述再组织。这个描述页框的结构体就是struct page。

page里面有位图unsigned long flags,1表示该页框被使用。一个约40字节的page描述一个4KB的页框。OS还会维护一个struct page* mem_map[N]用来管理众多的page,用数组增删查改来管理内存。类似于偏移量,物理地址和mem_map之间能相互转换。物理地址 = mem_map下标 * 4KB,下标 = mem_map物理地址 / 4KB。OS只需要知道下标就能找到对应的page,进而获取页框的使用情况。这个转换过程通过位操作就能实现,这里不展开。

4GB内存的管理需要一百多万个page,一个page大概40B,所有page的占用空间不超过40MB,大概千分之一的空间被占用,不算严重。对于现在普遍的16GB、32GB电脑,同样占用很小。因此用page来管理页框的占用比较合理。

③多级页表

page利用了位图大大压缩了空间占用,仅需1bit就可标志1B的使用情况。但页表就没这么好压缩了。页表左侧为虚拟地址,右侧为物理地址,页表一行就算不计入标记位都至少要8B,而这8B仅描述了1B的地址映射,显然这要占很大的空间,比存数据的内存都要大了。因此我们接下来要用多级页表来解决这个问题。

下面以一个指针4字节(32位系统)的标准来介绍

当拿到虚拟地址之后,32bit会被拆为10、10、12三份,经过三级找到对应的物理地址。

a.第一级:页目录

页目录表里面有1024项,虚拟地址的高10位会作为页目录表的下标匹配对应项,而下标对应的某一项存的内容叫页目录表项,是该虚拟地址对应的物理地址的高10bit,同时这个物理地址会指向下一级页表的物理地址的起始位置,这个起始位置的物理地址高位就是页目录表项存的值,低位为0。

例如:

0111010101 0101001010 101011010101会在页目录表中找到下标为0111010101对应的项,这个项里面存的是对应物理地址的高10位,为0010110101,之后会到下一级页表中查找,这个页表的物理地址就是0010110101 0000000000 0000000000。

b.第二级:页表

由上级页目录表项指向,每个页表1024项,由虚拟地址的中间10bit作为下标匹配,对应项存的内容叫页表项,同样标记了转换后的物理地址的中间10bit。同理,这个物理地址会继续指向下一级的物理地址的起始位置,高20位已确定,低12位自动补0。

同样是刚才的例子:

在物理地址为0010110101 0000000000 0000000000的页表中,虚拟地址的中间10位0101001010作为下标查找该表,得到的项的内容为0011010111。至此,得到的物理地址为0010110101 0011010111,接下来要继续向下一级查找,这一级的起始物理地址为0010110101 0011010111 0000000000。

c.第三级:页内偏移量(严格意义上不算一级)

转换的物理地址的高20位我们已经得到了,还剩下12bit,12bit的地址能够标识2 ^ 12bit数据(即4KB)。也就是说,一个页表项里存的值结合之前获得的地址可以标识内存中的一个具体的页框的起始地址,剩下的12bit就是以页框为起始地址的偏移量。也就是说,32位系统下最后12bit的虚拟地址就是物理地址的最后12bit,这里所谓的第三级其实并不存在,只需要两级页表即可转换虚拟地址为物理地址。

接着刚才的例子:

找到0010110101 0011010111 0000000000对应地址,这就是某一个页框的起始地址。虚拟地址101011010101会作为该页框的偏移量。至此,虚拟地址能够转为物理地址0010110101 0011010111 101011010101。

④多级页表的作用

当系统拿到虚拟地址要转换为物理地址时,它必须采用多级页表,否则页表占用空间会非常大。在32位系统下,多级页表其实只有两级,上面也说了页内偏移量不需要访问任何空间,不算做一级。每一级都开辟1024块空间,按每项4字节,页表总大小在1024 * 1024 * 4 = 4MB左右,完全处于可用状态。另外一个进程不可能使用全部地址,意味着页表一定是不完整的,所以一个页表远远小于4MB。

(2)虚拟地址 -> 物理地址的过程

在了解多级页表的转换之后,我们再次从头到尾梳理一次系统是如何进行地址转换的。

我们程序的每一条指令都有其虚拟地址,在执行某一指令前,要先从虚拟 -> 物理地址,获取到内容后才能交给CPU处理。在CPU中,寄存器CR3存的是页目录表的物理起始地址,当要开始进行虚拟 -> 物理地址的转换时,CR3会直接找到页目录表的物理地址;寄存器EIP里面存的下一条要执行的指令的虚拟地址。CPU中的硬件MMU就拿着这两个地址,按照多级页表的查找方式转化成物理地址。这是硬件自动完成的,是用硬件电路来完成页表查找的过程。

MMU得到物理地址后就通过系统总线来物理寻址,操作是in / out(根据EIP指令决定)。MMU将其内容交给IR,这就是该指令的内容,之后将由CPU来进一步处理。之后EIP + MMU读到的指令的长度就可以得到下一条指令的地址,循环往复。也就是说,整个进程地址空间的存在,是靠软硬结合实现的。

为什么虚拟 -> 物理地址的转换一定要靠硬件?速度是非常重要的因素,这个工作太高频了,CPU不是外设,不需要IO,集成到硬件就是提升效率最好的办法。

(3)懒加载和缺页中断

我们怎么知道指定页框的使用情况?page可以解决这个问题。我们已经知道mem_map[N]存储了所有物理内存的页框使用情况,且访问效率很高,这就意味着OS可以随时知道物理内存页框的使用情况以及相关属性。另外虚拟 -> 物理地址的过程也很高效,所以OS可以先建立映射关系,即先创建好多级页表,内容均填充,但实际物理内存对应位置不一定要有数据。当访问对应物理内存时可以配合page、标志位即时加载数据到对应页框中。

举个例子,一万个虚拟地址 -> 物理地址映射的页表只加载了一千个页框的数据,当访问的地方没有加载时,就会触发错误软中断去进一步加载内容,这就叫缺页中断。

有个问题,如何区分缺页中断和越界访问呢?只需页号合法性检查即可,页表中有映射地址就是缺页中断,物理地址在不在映射范围内就是越界。

理解之后,我们还能进一步理解new和malloc的操作。

new只需要申请虚拟地址空间,维护好页表的映射关系,但是命中标志位设置为0。当使用该空间时OS才会中断,申请空间,就像懒加载那样。程序中申请内存的本质就是申请虚拟内存 + 修改页表映射,而不会真正开辟空间,这实现了进程管理和内存管理解耦

(4)MMU和快表(TLB)

MMU用于虚拟地址 -> 物理地址转换,若MMU转换时发现对应位置的权限不允许,就会报错,这会存储到寄存器中并作为进程的上下文数据当进程调度时会检查该寄存器,并发送信号终止程序,这也是权限越界导致程序崩溃的原因。

快表也叫转译后备缓冲器,MMU转换地址后会将已访问的物理地址历史记录存到TLB中,后续所有转换操作都会先到TLB找,如果表中没有再到多级页表中查找,并记录到TLB中,快表的出现可以加速寻址,提高转换效率。

(5)高速缓存cache和局部性原理

CPU有自己的高速缓存cache,cache缓存的的是代码块和数据块,区别于TLB缓存的是虚拟地址到物理地址的映射关系。当要取得虚拟地址对应的数据时,MMU会先到cache中找,找到了就放到IR中。如果没找到就会根据多级页表或者TLB访问物理内存,并一次性将大量代码和数据缓存。

例如我们需要10bit数据,OS就有可能缓存20B到cache中,这取决于CPU硬件设计。

TLB和cache都是虚拟地址 -> 物理地址并读取数据过程的产物,TLB用于地址的转换,cache用于快速读取数据。cache缓存有意义吗?这离不开局部性原理。

cache是基于概率的,当我们访问一块代码时,我们就很有可能访问附近的代码,这就叫局部性原理。cache的本质是预加载,效率的提高离不开预加载(局部性原理)。在更大的角度上讲,局部性原理适用于所有缓存,例如内存就是CPU和外设的缓存。

对于多进程来说,进程间切换时cache也会同时失效,而线程切换不会失效,因为同一进程下所有线程共享同一块代码段,cache存的数据都是有意义的。因此,线程的效率相对进程而言更高。同样,线程切换时页表、TLB也不会切换,这也会增加虚拟地址到物理地址的转换效率,只要切换进程,页表、TLB、cache全部都无效了。

(6)线程的优缺点

①优点

创建线程的代价比创建进程小,占用的资源少。

切换线程的成本消耗少,页表、TLB和cache都能延用,而不像进程那样需要重新加载。

等待慢速IO时可以执行其它计算任务,在IO密集型应用中提高性能。除此之外还能将IO操作重叠,不同线程可以等待不同IO,这能充分利用多处理器的可并行数量。

在多处理器系统上运行计算密集型应用,可以将计算分给多个线程实现,提高效率注意线程个数不是越多越好,当线程个数过多,计算问题就会转为线程的调度问题,导致切换成本高。推荐的线程个数是CPU的物理个数 * 核数。

②缺点

性能损失(调度消耗),健壮性降低,缺乏访问控制(某些OS调用会对进程造成影响),编程难度提高。

更重要的一个特性是,一个线程崩溃会导致整个进程崩溃。线程是进程的执行分支,线程干的就是进程干的。在OS看来触发错误并中断程序是直接作用于整个进程的,线程不过是进程下面打工的。只要CPU对应寄存器有标志了,OS会直接杀掉所有线程。

3.线程资源划分

(1)代码块

每一个线程执行自己的任务都是以函数为入口的,每一个函数都有一个入口地址,函数代码紧接着入口地址且函数的代码块是连续编址的。这就是说不同线程划分的资源本来就是分离的,不需要进一步处理。可以理解为,线程瓜分函数代码的途中就将进程地址空间的代码段分成了不同的独立的部分。

(2)其它独立资源

进程是资源分配基本实体,线程是资源调度的基本单位。在同一个进程中,线程共享进程数据,也就是说数据都能被所有线程访问到(代码段和数据段),这和进程的独立性有很大的区别。但相对而言线程也有自己的独立的数据:线程ID、一组寄存器(用于存储线程的上下文数据)、栈(独立栈,局部性的互不影响)、errno、信号屏蔽字、调度优先级。其实进程地址空间中有自己独立的空间,但这空间不是私有的,虽然进程将不同空间分块,但其它线程也可以互相访问,只要拿到相应的地址即可,这也带来了一定的风险。