什么是线程
线程本质上是进程的一个执行分支,用于处理进程中的代码和数据;
每个线程都可以执行独立不同的代码片段,这意味着在一个进程中可以同时执行多个任务;
同一个进程中的所有线程共享相同的内存地址空间和资源(如全局变量,文件句柄等);
使得线程之间的通信和数据共享十分搞笑,因为它们不需要像进程通信那样复杂的机制;
-
在Linux中,线程在进程"内部"执行,线程在进程的地址空间内运行
任何执行流要执行的前提是具有资源;
进程地址空间是进程的资源窗口,线程在进程"内部"执行意味着其将执行进程代码的一部分;
-
在Linux中,线程的执行粒度要比进程更细
线程执行进程代码的一部分,所以其执行粒度要比进程更细;
每个进程在创建时会默认包含一个线程,这个线程通常称为主线程;
主线程负责执行进程的主要代码逻辑;
本质上CPU
无法区分执行的是线程还是进程;
对于CPU
而言只存在执行流的概念;
在计算机组成和操作系统实现中,线程必须被管理和调度,这表明线程需要有对应的数据结构来保存其状态和相关信息;
-
线程控制块(TCB结构体)
线程控制块是用于存储线程的状态信息的数据结构,其一般包含以下内容:
-
线程
ID
唯一标识线程的
ID
; -
程序计数器
PC
保存线程当前执行的指令地址;
-
寄存器集合
保存线程运行时的
CPU
寄存器状态; -
堆栈指针
指向线程的堆栈;
-
线程状态
如就绪,运行,阻塞等状态;
-
线程优先级
线程的优先级信息;
-
调度信息
用于线程的调度的相关信息;
-
在Linux中,线程的实现采用的是复用进程的task_struct
结构体;
每个进程和线程在内核中都用一个task_struct
结构体实例来表示;
执行流
执行流是指CPU
按照指令顺序逐条执行的过程,它可以来源于进程或线程;
CPU
在运行时不区分其执行的指令是属于进程还是线程,其只进行逐条执行指令,并根据指令执行结果更新程序计数器和相关寄存器;
-
调度器
操作系统调度器负责决定哪个执行流在某个时刻被分配到
CPU
上执行;调度器管理进程控制块(PCB)和线程控制块(TCB),在Linux中统一使用
task_struct
; -
上下文切换
当调度器决定和切换执行流时将会保存当前执行流的上下文,如寄存器程序计数器等,并恢复下一个执行流的上文;
线程是更细粒度的执行单元,多个线程共享统一进程的地址空间和资源但其个字拥有独立的执行流;
在CPU
的视角看,不管是进程还是线程都只是一个需要被执行的指令序列,即执行流;
CPU
至按照调度器的指令执行下一个指令序列而不关心其所属的进程或线程;
-
线程<=执行流<=进程
当执行流是线程时,其
<=
进程;当执行流是进程时,其
>=
线程;所以可以称为 线程<=执行流<=进程 ;
故在Linux中的执行流可以被称为 “轻量级进程” ;
线程与进程的关系
一个进程中可以包含多个线程,这表明一个进程中线程的数量必须>=1
;
本质上进程为其对应的代码和数据的结合;
-
进程
进程是操作系统中资源分配和管理的基本单位;
这些资源包括内存空间,进程控制块(
PCB
结构体),打开的文件描述符,信号处理机制等;每个进程都有自己独立的内存空间,打开的文件,信号处理方式等;
进程之间是相互独立的,一个进程的崩溃不会影响到其他进程;
-
线程
线程是进程中的一个执行单元,也称为轻量型进程;
线程是操作系统中能够进行运算调度的最小单位;
同一个进程中的所有线程共享该进程的资源,如进程地址空间,全局变量,文件句柄等;
对应的线程也需要独立的属性和数据使其能够被操作系统管理,包括其具有独立的程序计数器,寄存器集合和栈;
一个进程至少包含一个线程,即主线程;
主线程负责执行代码的主要代码逻辑;
一个进程可以创建多个线程,这些线程共享进程的资源和地址空间;
线程本质上是进程内部的执行流资源;
页表构造及线程资源分配
一个进程存在着对应的PCB
结构体(task_struct
);
task_struct
中包含着指向进程地址空间的指针,即间接指向进程地址空间;
进程地址空间通过页表映射至物理内存,当在处理一个进程的上下文时对应的CR3
寄存器将会指向这个页表,即存放该页表的物理地址;
当需要访问一个物理地址时MMU
寄存器将通过页表将虚拟地址转化为具体物理地址;
页表本质上是以分页的形式进行存放的从而大大减少了占用空间的开销;
以32
位虚拟地址为例,其将32
位虚拟地址分别以10 + 10 + 12
的形式进行存储,其分别为页目录(10
位),页表(10
位)和页内偏移量(12
位);
其每十位或是十二位都可以由全0
到全1
进行存储,当需要转化时将会将其转化为对应的十进制从而进行索引;
-
页目录
页目录中存在
2^10
个页目录表项,即1024
个页目录表项;其中每个页目录表项包含着每个页目录的页表的索引;
32
位虚拟地址的前十位可索引到对应的页目录表项从而索引页表; -
页表
也被称为二级页表,存在
2^10
个页表表项,即1024
个页表表项;每个页表表项包含着物理内存的页号;
32
位虚拟地址的中间十位可索引到对应页号的页初始地址; -
物理内存
物理内存中将以 页 作为最小访问单位,一般情况下一个页的大小为
4kb
即2^12
字节;其
12
次方可对应着32
位虚拟内存中的最后12
位;
当一个执行流需要访问物理内存时本质上是从CR3
中访问到对应的页目录,通过前10
位的十进制转换找到对应的页目录表项并索引到对应的页表;
再利用中间10
位转换为十进制访问该页表找到对应的页表表项,并根据对应的页号找到物理内存的页;
最后通过32
位虚拟地址的最后12
位索引到页中的具体位置从而进行访问;
-
页表的大小
一般情况下,页目录表项与页表表项的大小都为
4byte
;一个页目录存在
1024
个页目录表项,即4 * 1024byte
为4kb
;一个页表存在
1024
个页表表项,且一个页目录存在1024
个页表,即4 * 1024 * 1024byte
为4mb
;最终大小为
4kb + 4mb
(一个进程);
而实际上一个进程不是所有页表(二级页表)的页表表项都会被使用,故一个进程的页表(进程页表)大小为<= 4kb + 4mb
;
当对一个int
类型的地址进行取地址时获取到的地址本质上是段基地址,即该数据块的起始地址;
在X86
计算机中本质上 起始地址 + 类型 就是 起始地址 + 偏移量 ;
同时在计算机中,无论使用何种复杂的数据结构类型,这些数据最终都会被转换为由内置类型(int
,float
,char
等)组成的数据块;
这些基本数据类型定义了数据在内存中的具体布局和操作方式,编译器通过这些基本数据类型生成底层的机器代码;
-
CR2
寄存器该寄存器是一个在
X86
计算机上的控制寄存器;专门用于存储导致最近一次的缺页异常的线性地址(虚拟地址);
当
CPU
访问一个未映射的虚拟地址或没有适当权限访问的地址时,会触发缺页中断;处理器将导致缺页中断的虚拟地址存储在
CR2
寄存器中,然后将控制权转移到缺页中断处理程序;操作系统的缺页中断处理程序可以使用
CR2
寄存器中的地址来确定哪个页导致了缺页中断,并采取相应的措施,如加载缺失的页到内存中或终止进程;
本质上线程的资源分配就是分配地址空间范围;
操作系统为每个线程分配一个独立的虚拟地址空间,保证每个线程可以独立运行而互不干扰;
-
共享的虚拟地址空间
-
代码段
线程共享进程的代码段,即可执行指令;
-
数据段
线程共享进程的数据段,包括已初始化和未初始化的全局变量和静态变量;
-
堆
线程共享进程的堆区,用于动态内存分配;
多个线程可以同时分配和实施方内存(需要同步互斥机制避免竞争);
-
-
独立的栈空间
每个进程有自己独立的栈,用于函数调用和局部变量的存储;
-
栈分配
操作系统在创建线程时会为其分配独立的栈空间;
栈空间大小通常是固定的(某些操作系统可配置线程的栈的大小);
-
栈增长
栈通常从高地址向低地址增长;
-
线程的轻量化
线程较进程更加轻量化的具体表现为以下几点:
-
创建
-
进程
在创建一个进程时操作系统需要进新的虚拟地址空间分配,包括代码段,数据段,堆栈等;
创建进程控制块包含进程的所有信息;
初始化文件描述符表,复制父进程的文件描述符;
进行资源分配和权限设置;
一般情况下可用
fork()
创建进程,将会复制整个进程的上下文包括进程地址空间; -
线程
创建一个新线程时操作系统将分配一个新的栈空间;
创建线程控制块(Linux依旧为
task_struct
)用于包含线程信息;对于进程地址空间而言线程共享进程的进程地址空间;
一般情况下线程的创建只需要分配栈和对应的
TCB
结构体;
-
-
切换(上下文切换)
-
进程切换
在进行进程切换的过程中需要进行上下文保存,包括
CPU
寄存器,程序计数器,栈指针,虚拟地址空间等;更改
CR3
寄存器的页表指针以切换虚拟地址空间;为保证内存访问的一致性,由于页表切换导致的地址空间变化需要刷新
TLB
和部分缓存(cache
),该操作表明数据需要再次进行一次由冷到热的过程;进程切换设计大量状态信息的保存与恢复,以及页表切换,开销较大;
-
线程切换
保存当前运行线程的寄存器状态和栈指针;
由于线程共享进程的虚拟地址空间,所以无需切换页表;
同时线程切换不涉及虚拟地址空间的改变,其
TLB
和cache
缓存不需要刷新,意味着不需要使数据再次经过一个由冷到热的过程以保持了缓存的一致性和效率;恢复即将运行线程的寄存器和栈指针;
线程切换仅涉及寄存器状态和栈指针的保存和恢复,总体开销较小;
-
-
销毁
-
进程销毁
进程在销毁时需要回收进程占用的所有资源,包括内存,文件描述符等,需要全面对进程的资源进行清理;
-
线程销毁
线程在销毁时只需要回收线程的栈空间与
TCB
,仅需清理线程相关资源;
-
线程的特点
线程的优点为以下几点:
-
创建代价低
创建一个新的线程的代价比创建一个新进程要小;
线程共享大部分的进程资源,创建时只需要分配新的栈空间和对应的线程控制块即可;
-
切换开销小
与进程切换想必,线程之间的切换需要操作系统做的工作要少;
切换避免了大量数据的重新预加载;
-
占用资源少
线程的占用资源比进程少,其共享进程的虚拟地址空间和其他资源从而减少了内存和资源的占用;
-
利用多处理器并行
线程能充分利用多处理器的并行能力;
通过将计算任务分解到多个线程中从而在多处理器系统上执行多个线程而提高计算效率;
-
提高
I/O
性能在等待慢速
I/O
操作结束的同时,线程可以执行其他计算任务;提高程序的并行性和响应速度;
-
计算密集型应用
在计算密集型应用中可将计算任务分解为多个线程;
可在多处理器系统上并行执行以提高整体计算能力;
-
I/O
密集型应用在
I/O
密集型应用中,通过将I/O
操作重叠执行,不同的线程可以同时等待不同的I/O
操作从而提高I/O
性能;
其缺点为以下几点:
-
性能损失
对于很少被外部事件阻塞的计算密集型线程无法与其他线程共享同一个处理器;
如果计算密集型线程的数量超过可用的处理器则会增加额外的同步和调度开销导致性能损失;
-
健壮性降低
线程之间的共享数据可能会导致同步问题和竞态条件;
增加了程序的不确定性和复杂性从而出现难以调试的错误;
-
缺乏访问控制
线程共享进程的所有资源,意味着一个线程的错误可能会影响整个进程的稳定性;
-
编程难度提高
在进行多线程编程的开发中需要处理线程同步,死锁和竞态条件等问题;
对应的编程难度提高;
同时在多线程程序中,由于线程是进程的一部分,若是该进程中的一个线程崩溃(除零错误,野指针等问题)将影响到整个进程;
线程异常通常会触发进程的信号机制从而使得整个进程退出,相应的所有线程也会随之退出;
线程通常用来进行 CPU
密集型程序 与 I/O
密集型程序 的开发;
-
CPU
密集型程序多线程可以提高
CPU
密集型程序的效率;通过并行处理,可以利用多核处理器的能力加快任务完成的速度从而提升效率;
-
I/O
密集型程序在
I/O
密集型程序中使用多线程,可以在等待I/O
操作时执行其他任务从而提高程序的相应速度和用户体验;