您的位置:首页 > 运维架构 > Linux

linux 线程 进程(ZZ)

2010-05-25 10:12 267 查看
一.基础知识:线程和进程  
按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。  
无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。  
针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足smp系统的需要,也支持用线程库的方式在用户态实现另一套线程机制,此时一个核心线程同时成为多个用户态线程的调度者。正如很多技术一样, "混合 "通常都能带来更高的效率,但同时也带来更大的实现难度,出于 "简单 "的设计思路,Linux从一开始就没有实现混合模型的计划,但它在实现上采用了另一种思路的 "混合 "。  
在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。  
当核内既支持进程也支持线程时,就可以实现线程-进程的 "多对多 "模型,即一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这就是前面提到的 "混合 "线程模型,既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝大多数商业操作系统(如Digital   Unix、Solaris、Irix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为 "一对一 "、 "多对一 "两种模型,前者用一个核心进程(也许是轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,而这个需求正变得越来越大,因此,在现实中,纯用户级线程的实现,除算法研究目的以外,几乎已经消失了。  
Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程 "一对一 "模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。Linux-LinuxThreads的运行机制正是本文的描述重点。  
二.Linux   2.4内核中的轻量进程实现  
最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部分,而程序的执行通常理解为执行上下文,包括对cpu的占用,后来发展为线程。在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码,这就发展出轻量进程的概念。Linux内核在2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实现:  
<linux-2.4.20/kernel/fork.c>  
int   do_fork(unsigned   long   clone_flags,   unsigned   long   stack_start,    
struct   pt_regs   *regs,   unsigned   long   stack_size)  
其中的clone_flags取自以下宏的 "或 "值:  
<linux-2.4.20/include/linux/sched.h>  
#define   CSIGNAL   0x000000ff   /*   signal   mask   to   be   sent   at   exit   */  
#define   CLONE_VM   0x00000100   /*   set   if   VM   shared   between   processes   */  
#define   CLONE_FS                 0x00000200   /*   set   if   fs   info   shared   between   processes   */  
#define   CLONE_FILES           0x00000400   /*   set   if   open   files   shared   between   processes   */  
#define   CLONE_SIGHAND   0x00000800   /*   set   if   signal   handlers   and   blocked   signals   shared   */  
#define   CLONE_PID   0x00001000   /*   set   if   pid   shared   */  
#define   CLONE_PTRACE   0x00002000   /*   set   if   we   want   to   let   tracing   continue   on   the   child   too   */  
#define   CLONE_VFORK   0x00004000   /*   set   if   the   parent   wants   the   child   to   wake   it   up   on   mm_release   */  
#define   CLONE_PARENT   0x00008000   /*   set   if   we   want   to   have   the   same   parent   as   the   cloner   */  
#define   CLONE_THREAD   0x00010000   /*   Same   thread   group?   */  
#define   CLONE_NEWNS   0x00020000   /*   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内核是如何实现这些资源的共享的。  
1.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在进程中的地位:  
2.CLONE_FS  
task_struct中利用fs(struct   fs_struct   *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了这个结构;而对于轻量级进程则仅增加fs-> count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。  
3.CLONE_FILES  
一个进程可能打开了一些文件,在进程结构task_struct中利用files(struct   files_struct   *)来保存进程打开的文件结构(struct   file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files()时仅增加files-> count计数。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。  
4.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线程库实现尝试都只能尽可能实现POSIX的绝大部分语义,并在功能上尽可能逼近。  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: