该问题是进程与线程的区别中的一个核心问题。
进程有什么
一个进程需要存放若干数据,以及若干指令,那么首先其会有一片内存,在分页机制下,所有进程都必须有其独立的页表(PT, Page Table),从而完成虚拟地址到物理地址(用户地址空间)的转换。
有页表,就肯定需要一个访问机制。目前操作系统一般都采用二级页表,也就是访页时最先进入页目录(PD, Page Directory),再根据里面的页目录项获取页表物理地址。需要一个寄存器来存储该目录的物理地址,即页目录表寄存器(PDBR, Page Directory Base Register)(x86_64 下使用 CR3)通过向该寄存器赋予页目录基地址(物理),便可随时访问页目录。
以及页表缓存(TLB, Translation Lookaside Buffer),用于加快虚拟地址->物理地址的转换。
此外,进程在执行程序时,必须要有一个指令指针寄存器(IP, Instruction Pointer),来告诉 CPU 即将执行的指令在代码段中的偏移量,和一个代码段寄存器(CS, Code Segment),用来存放内存代码段区域的入口地址。这两个寄存器合在一起为我们指示了 CPU 当前要读取指令的地址(虚拟),最后根据页表对应到实际的物理地址进行取指令操作。
物理地址 = CS << 4 + IP
程序的运行必然是由若干函数组合起来运行的。当一个函数在运行时,需要为它在栈中创建一个栈帧用来记录运行时产生的相关信息,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。栈由高地址向低地址拓展,为了访问栈,就必须要有栈基址(BP, Base Pointer),以及栈顶指针(SP, Stack Pointer)。
线程有什么
线程与进程的关系,就好比函数与程序的关系。同一进程下的所有线程都共享同一地址空间,包括全局变量与代码段。但每个线程有各自的栈空间与 CS:IP,就好比不同函数对应的参数与返回地址都不同,所执行的指令所处位置也不一定相同。
Linux 中进程与线程都被抽象为
task_struct
,各自拥有pid
,但其内存所指mm_struct
是相同的。
进程与线程切换开销
进程的切换,必然伴随着上文提到的种种寄存器的保存与加载。除了这些,最大的开销来源于:从一个进程切换到另一个进程时,对应的地址空间也将切换,那么 TLB 是需要被清空的,这样就导致 TLB miss 以及访页速率降低,当然,Cache miss 率也会提高。
然而,同一进程内的线程切换并不涉及地址空间的切换,仅仅保存其 BP, SP 以及若干寄存器即可。
如果是不同进程之间的线程切换,那依然会涉及进程切换。
这也就是为什么,进程切换的开销比线程切换大得多。