前言
本篇主要对进程和线程的概念进行分析,kernel中的进程、线程模型,涉及进程、用户态线程、内核态线程、轻量级进程(LWP),在分析之前,需要阅读What are threads?,该文章有便于理解操作系统中线程、用户态线程和内核态线程的对应关系。
不同操作系统对线程和进程的理解及对应关系稍有不同(M:N、N:1、1:1),本篇只针对Linux操作系统。
本篇文章是阅读ULK3及网上相应文章后的个人总结笔记,加上了我的理解,这部分内容会加以标识,如有错误,欢迎指正。
进程
根据ULK3上对进程的定义:“进程是程序执行时的一个实例”、“一个运行程序的执行上下文”、“是系统分配资源的最小单位”。
描述一个进程
当我们谈论进程时,我们在谈论什么?
众所周知,kernel中用进程描述符task_struct来定义一个进程:
关于task_struct即各个成员的含义,属于老生常谈,这里就不讨论了,但是从上图可以看出一个进程的基本组成如下:
- thread info 包含了task_struct和该进程的stack
- mm,进程的内存描述符,即指明了进程的地址空间,通过它可以找到该线程所有可用的线性地址
- tty相关
- fs,该进程可见文件系统集合
- files,该进程打开的文件列表
- signal,该进程的信号表
结合What are threads (user/kernel)?文章中的描述,可总结为:代码段,进程地址空间、栈、文件IO、信号表等。这是对进程的直观认识。
线程
线程是系统调度的最小单位。
操作系统的线程可分为:用户线程、内核线程。Linux为了支持用户线程,提供了轻量级进程(LWP)。
内核线程
本质上,内核线程是进程,但是其特点在于,内核进程只运行在内核态,
进程切换主要有两个步骤:
- 切换全局页目录以及安装一个新的地址空间
- 切换内核态堆栈及硬件上下文
第二步其实比较简单,主要是寄存器的操作,从资料来看,第一步的开销比较大,因为涉及到flush tlb。
而内核线程因为永远运行在内核态(的地址范围内),所以不需要处理用户态上下文(即不存在从用户态到内核态的切换)。又因为所有进程的内核态页表都是相同的,所以内核线程在切换的时候不需要重新建立虚拟地址和物理地址的映射关系,直接stolen切换之前的进程的页表即可。
这样一来,用户态进程的开销就大大减小了(只有切换硬件上下文和栈的开销)。
kernel中将一些后台的、周期性的工作交付给内核线程完成,常见的如keventd、kswapd(内存回收)、ksoftirqd(软中断处理)等。
创建内核线程的API为create_thread,最终调用do_fork,具体创建过程下一篇会详细分析。
用户线程和轻量级进程
早期POSIX库线程的创建、调度和销毁都是在用户空间由POSIX独立完成,基本上没内核什么事儿,kernel也不知道用户空间有哪些进程。但是这种实现方式有其弊端,ULK3中举了一个象棋程序的例子,说明了用户空间并发性的缺陷,解决这种缺陷需要采用比较复杂的非阻塞技术。
(除此以外,POSIX线程库不利于充分利用多处理器,有一种说法是:“在哪个CPU上创建的线程组,其中所有线程只能在该CPU上运行。”对POSIX线程库不了解,有待验证)
kernel使用了轻量级线程,对用户线程提供了比较好的支持,主要体现在以下几点:
- 一组轻量级进程之间更利于资源共享,如地址空间,打开的文件集合。共享资源被修改后,同一组的其他LWP可以立即查看这些修改(这也要求LWP在访问共享资源时要做好同步工作);
- 用户线程和LWP之间采用一对一模型,即一个用户线程对应一个LWP(示意图见附录)。这样一来,相当于内核干预了用户态线程,用户线程的创建、调度、销毁都要经过内核处理(我的理解);
- 用户线程的并发性得到了很大提升,如果一个线程block住,schedule的控制权在内核手中,内核可以调度组内的其他线程运行。
进程和线程的区别、联系
联系
- 我的理解:目前内核的实现中,进程和线程是包含关系,一个进程(可以运行在用户态和内核态)可以理解为只有一个线程或多个线程的线程组,即对应了一个进程或一组LWP。
- 一组LWP共享了进程的部分资源,进程地址空间、文件IO等,但是有自己的PC、寄存器、stack,即有自己的控制流。
区别
- 进程是heavy weight的,需要很多资源;线程(或者说LWP)相反
- 进程切换开销大,线程开销小;
- 多进程并发性差,LWP并发性好;
- 共享资源效率方面,LWP比较好
附录
one to one model