作者杨沙洲
内容
·基础知识线程和进程
·Linux 内核中的轻量进程实现
·LinuxThread的线程机制
·其他的线程实现机制
·参考资料
·关于作者
自从多线程编程的概念出现在 Linux 中以来Linux 多线应用的发展总是与两个问题脱不开干系兼容性效率本文从线程模型入手通过分析目前 Linux 平台上最流行的 LinuxThreads 线程库的实现及其不足描述了 Linux 社区是如何看待和解决兼容性和效率这两个问题的
一基础知识线程和进程
按照教科书上的定义进程是资源管理的最小单位线程是程序执行的最小单位在操作系统设计上从进程演化出线程最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销
无论按照怎样的分法一个进程至少需要一个线程作为它的指令执行体进程管理着资源(比如cpu内存文件等等)而将线程分配到某个cpu上执行一个进程当然可以拥有多个线程此时如果进程运行在SMP机器上它就可以同时使用多个cpu来执行各个线程达到最大程度的并行以提高效率同时即使是在单cpu的机器上采用多线程模型来设计程序正如当年采用多进程模型代替单进程模型一样使设计更简洁功能更完备程序的执行效率也更高例如采用多个线程响应多个输入而此时多线程模型所实现的功能实际上也可以用多进程模型来实现而与后者相比线程的上下文切换开销就比进程要小多了从语义上来说同时响应多个输入这样的功能实际上就是共享了除cpu以外的所有资源的
针对线程模型的两大意义分别开发出了核心级线程和用户级线程两种线程模型分类的标准主要是线程的调度者在核内还是在核外前者更利于并发使用多处理器的资源而后者则更多考虑的是上下文切换开销在目前的商用系统中通常都将两者结合起来使用既提供核心线程以满足smp系统的需要也支持用线程库的方式在用户态实现另一套线程机制此时一个核心线程同时成为多个用户态线程的调度者正如很多技术一样混合通常都能带来更高的效率但同时也带来更大的实现难度出于简单的设计思路Linux从一开始就没有实现混合模型的计划但它在实现上采用了另一种思路的混合
在线程机制的具体实现上可以在操作系统内核上实现线程也可以在核外实现后者显然要求核内至少实现了进程而前者则一般要求在核内同时也支持进程核心级线程模型显然要求前者的支持而用户级线程模型则不一定基于后者实现这种差异正如前所述是两种分类方式的标准不同带来的
当核内既支持进程也支持线程时就可以实现线程进程的多对多模型即一个进程的某个线程由核内调度而同时它也可以作为用户级线程池的调度者选择合适的用户级线程在其空间中运行这就是前面提到的混合线程模型既可满足多处理机系统的需要也可以最大限度的减小调度开销绝大多数商业操作系统(如Digital UnixSolarisIrix)都采用的这种能够完全实现POSIXc标准的线程模型在核外实现的线程又可以分为一对一多对一两种模型前者用一个核心进程(也许是轻量进程)对应一个线程将线程调度等同于进程调度交给核心完成而后者则完全在核外实现多线程调度也在用户态完成后者就是前面提到的单纯的用户级线程模型的实现方式显然这种核外的线程调度器实际上只需要完成线程运行栈的切换调度开销非常小但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的因而无法定位到线程所以这种实现方式不能用于多处理器系统而这个需求正变得越来越大因此在现实中纯用户级线程的实现除算法研究目的以外几乎已经消失了
Linux内核只提供了轻量进程的支持限制了更高效的线程模型的实现但Linux着重优化了进程的调度开销一定程度上也弥补了这一缺陷目前最流行的线程机制LinuxThreads所采用的就是线程进程一对一模型调度交给核心而在用户级实现一个包括信号处理在内的线程管理机制LinuxLinuxThreads的运行机制正是本文的描述重点
二Linux 内核中的轻量进程实现
最初的进程定义都包含程序资源及其执行三部分其中程序通常指代码资源在操作系统层面上通常包括内存资源IO资源信号处理等部分而程序的执行通常理解为执行上下文包括对cpu的占用后来发展为线程在线程概念出现以前为了减小进程切换的开销操作系统设计者逐渐修正进程的概念逐渐允许将进程所占有的资源从其主体剥离出来允许某些进程共享一部分资源例如文件信号数据内存甚至代码这就发展出轻量进程的概念Linux内核在x版本就已经实现了轻量进程应用程序可以通过一个统一的clone()系统调用接口用不同的参数指定创建轻量进程还是普通进程在内核中clone()调用经过参数传递和解释后会调用do_fork()这个核内函数同时也是fork()vfork()系统调用的最终实现
int do_fork(unsigned long clone_flags unsigned long stack_start
struct pt_regs *regs unsigned long stack_size)
其中的clone_flags取自以下宏的或值
#define CSIGNALxff
/* signal mask to be sent at exit */
#define CLONE_VMx
/* set if VM shared between processes */
#define CLONE_FSx
/* set if fs info shared between processes */
#define CLONE_FILES x
/* set if open files shared between processes */
#define CLONE_SIGHANDx
/* set if signal handlers and blocked signals shared */
#define CLONE_PIDx
/* set if pid shared */
#define CLONE_PTRACEx
/* set if we want to let tracing continue on the child too */
#define CLONE_VFORKx
/* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENTx
/* set if we want to have the same parent as the cloner */
#define CLONE_THREADx
/* Same thread group? */
#define CLONE_NEWNSx
/* New namespace group? */
#define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
在do_fork()中不同的clone_flags将导致不同的行为对于LinuxThreads它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建线程表示共享内存共享文件系统访问计数共享文件描述符表以及共享信号处理方式本节就针对这几个参数看看Linux内核是如何实现这些资源的共享的
CLONE_VM
do_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项这两个mm_struct数据与进程所关联的内存空间相对应如果do_fork()时指定了CLONE_VM开关copy_mm()将把新的task_struct中的mm和active_mm设置成与current的相同同时提高该mm_struct的使用者数目(mm_struct::mm_users)也就是说轻量级进程与父进程共享内存地址空间由下图示意可以看出mm_struct在进程中的地位
CLONE_FS
task_struct中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息do_fork()时调用copy_fs()复制了这个结构而对于轻量级进程则仅增加fs>count计数与父进程共享相同的fs_struct也就是说轻量级进程没有独立的文件系统相关的信息进程中任何一个线程改变当前目录根目录等信息都将直接影响到其他线程
CLONE_FILES
一个进程可能打开了一些文件在进程结构task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息do_fork()中调用了copy_files()来处理这个进程属性轻量级进程与父进程是共享该结构的copy_files()时仅增加files>count计数这一共享使得任何线程都能访问进程所维护的打开文件对它们的操作会直接反映到进程中的其他线程
CLONE_SIGHAND
每一个Linux进程都可以自行定义对信号的处理方式在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息do_fork()中的copy_sighand()负责复制该信息轻量级进程不进行复制而仅仅增加signal_struct::count计数与父进程共享该结构也就是说子进程与父进程的信号处理方式完全相同而且可以相互更改
do_fork()中所做的工作很多在此不详细描述对于SMP系统所有的进程fork出来后都被分配到与父进程相同的cpu上一直到该进程被调度时才会进行cpu选择
尽管Linux支持轻量级进程但并不能说它就支持核心级线程因为Linux的线程和进程实际上处于一个调度层次共享一个进程标识符空间这种限制使得不可能在Linux上实现完全意义上的POSIX线程机制因此众多的Linux线程库实现尝