[计算机软件及应用]Linuxpthread分析文档04.doc
《[计算机软件及应用]Linuxpthread分析文档04.doc》由会员分享,可在线阅读,更多相关《[计算机软件及应用]Linuxpthread分析文档04.doc(40页珍藏版)》请在三一办公上搜索。
1、Linux POSIX Thread分析文档1 综述自从多线程编程的概念出现在 Linux 中以来,Linux 多线程应用的发展总是与两个问题相关联:兼容性、效率。本文从线程模型的基础知识入手,通过分析目前 Linux 平台上最流行的 LinuxThreads以及NPTL线程库的实现及其不足,描述了 Linux 社区是如何看待和解决效率问题的,对于兼容性问题,本文不加以详细描述,对此方面有兴趣的读者可以参考POSIX Threads规范。本文结合Linux Kernel 2.4到2.6中为了提高线程的性能所作的种种努力,描述了LinuxThreads到NPTL两个线程库的设计思想的变化,以及其
2、两者最终的设计结果、它们的设计缺陷以及优化的可能,最后本文对比了这两库的性能差别,并且结合它们的设计说明了性能差距产生的原因。2 基础知识进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而线程将被分配到某个cpu上执行。一个进程可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率。同时,即使是在单cpu的机
3、器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更自由,可以面对更加复杂的程序执行逻辑,通过有效的安排不同线程之间的同步关系,可以提高程序的执行效率,同时相对于采用多进程的解决方法来说,线程之间的切换代价明显要低很多。图表 1 线程模式对比线程分为核心级线程和用户级线程两类,分类的标准主要是线程运行在内核内部还是在外部。前者更利于并发使用多处理器的资源,而后者则更多考虑的是用户态与内核态之间切换开销,其具体的优缺点对比如图表 1所示。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足SMP系统的需要,也支持用线程库的方式在用户态实现另一套线程机制
4、,此时一个核心线程同时成为多个用户态线程的调度者。在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。当核内既支持进程也支持线程时,就可以实现一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行,这种方式既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝大多数商业操作系统(如Digital Unix、Solaris、Ir
5、ix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为一对一、多对一与“多对多”三种模型,第一种用一个核心进程(也许是轻量进程)对应一个用户态线程,将线程调度等同于进程调度,交给核心完成,而后两种则完全在核外实现多线程,调度也在用户态完成。显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,即该进程的全部线程都仅能被内核认为一个(“多对一”)或者几个(“多对多”)进程而调度。Linux内核只提供了轻量进程的支
6、持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。LinuxThreads所采用的就是线程-进程“一对一”模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。而NPTL线程机制则更加利用了2.6内核的优势,同样采用“一对一”模型,使用基于Futex的管理机制替代了原来的LinuxThreads的基于不稳定且低效率的信号处理方式的管理机制,同时利用内核完成了更多的管理工作,去除了管理线程,更好的优化了线程创建、回收以及管理的开销。总体上说NPTL的性能要明显优于LinuxThreads的性能。3 LinuxThreads分析Li
7、nuxThreads采用了“一对一”的线程模型,如图表 2所示。同时其采用了Linux的信号机制(signal)作为线程之间通信和同步的基础。LinuxThreads在设计和开发时正处在2.4内核的时代,当时的用户态进程或者线程的同步的方法仅仅有:信号量,信号,共享内存,管道等较低效率的进程间通信手段。当时的LinuxThreads设计人员选择了信号作为用户态线程的同步的基础。图表 2 一对一线程模型3.1 LinuxThreads架构分析本小节描述LinuxThreads的设计架构,主要包括LinuxThreads依赖的内核功能以及其在用户态中的实现。3.1.1 内核支持在线程概念出现以前,
8、为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,内存,甚至代码,这就发展出轻量级进程的概念。Linux内核在2.0.x版本就已经实现了轻量级进程,应用程序可以通过一个统一的系统调用接口clone(),用不同的参数指定创建轻量级进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个函数同时也是fork()、vfork()系统调用的最终实现:int do_fork(unsigned long clone_flags, unsigned long stack_s
9、tart, struct pt_regs *regs, unsigned long stack_size)其中的clone_flags取自以下宏的或值: #define CSIGNAL0x000000ff/* signal mask to be sent at exit */#define CLONE_VM0x00000100/* set if VM shared between processes */#define CLONE_FS 0x00000200/* set if fs info shared between processes */#define CLONE_FILES 0x00
10、000400/* set if open files shared between processes */#define CLONE_SIGHAND0x00000800/* set if signal handlers and blocked signals shared */#define CLONE_PID0x00001000/* set if pid shared */#define CLONE_PTRACE0x00002000/* set if we want to let tracing continue on the child too */#define CLONE_VFORK
11、0x00004000/* set if the parent wants the child to wake it up on mm_release */#define CLONE_PARENT0x00008000/* set if we want to have the same parent as the cloner */#define CLONE_THREAD0x00010000/* Same thread group? */#define CLONE_NEWNS0x00020000/* New namespace group? */#define CLONE_SIGNAL (CLON
12、E_SIGHAND | CLONE_THREAD)在do_fork()中,不同的clone_flags将导致不同的行为,对于LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建线程,表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。本节就针对这几个参数,看看Linux内核是如何实现这些资源的共享的。1.CLONE_VMdo_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个mm_struct数据与进程所关联
13、的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的task_struct中的mm和active_mm设置成与current的相同,同时提高该mm_struct的使用者数目(mm_struct:mm_users)。也就是说,轻量级进程与父进程共享内存地址空间,内存访问权限等,由图表 3示意可以看出mm_struct在进程中的地位。图表 3 MM-Struct结构示意图2.CLONE_FStask_struct中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了
14、这个结构;而对于轻量级进程则仅增加fs-count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。3.CLONE_FILES 一个进程可能打开了一些文件,在进程结构task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files()时仅增加files-count计数。这一共享使得任何线
15、程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。4.CLONE_SIGHAND 每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅仅增加signal_struct:count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。do_fork()中所做的工作很多,在此不详细描述。对于SM
16、P系统,所有的进程fork出来后,都被分配到与父进程相同的cpu上,一直到该进程被调度时才会进行cpu选择。 3.1.2 LinuxThread的线程机制LinuxThreads是Linux kernel 2.4平台上使用最为广泛的线程库,由Xavier Leroy (Xavier.Leroyinria.fr)负责开发完成,并已绑定在GLIBC中发行,其在kernel 2.6以后伴随GLIBC 2.3版本的发行被NPTL库替代。它所实现的就是基于核心轻量级进程的“一对一”线程模型,一个线程实体对应一个内核轻量级进程,而线程之间的管理在核外函数库中实现。1. 线程描述数据结构及实现限制Linux
17、Threads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量_pthread_handles来描述和引用进程所辖线程。在_pthread_handles中的前两项,LinuxThreads定义了两个全局的系统线程:_pthread_initial_thread和_pthread_manager_thread,并用_pthread_main_thread表征_pthread_manager_thread的父线程(初始为_pthread_initial_thread)。struct _pthread_descr_struct是一个双环链表结
18、构,_pthread_manager_thread所在的链表仅包括它一个元素,实际上,_pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、p_priority等三个域。而_pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列pthread_create()之后形成的_pthread_handles数组将如图表 4所示。图表 4 _pthread_handles新创建的线程将首先在_pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以_pthread_main_
19、thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程最大线程数,线程私有数据区大小等等。在LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,Linux
20、Threads使用PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到1024(PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用PTHREAD_STACK_MIN,16384(字节)。需要注意的是如果启动TLS,则通过硬件负责切换到需要的thread_descr,而如果不启用TLS,则需要软件通过线程栈的位置推算出thread_de
21、scr。TLS(Thread-Local Storage)与TSD(Thread Specific Data)是两种实现线程本地数据的方法,TLS主要基于包括线程寄存器的CPU硬件实现,后文将详细介绍这种模式,TSD是一种采用纯粹软件方法实现线程本地数据的方法,主要通过pthread_create_key等接口创建和访问数据,这种方式并未在本文中介绍,可以参考glibc/linuxthreads/specific.c中的代码实现。TSD实现的线程私有数据的创建和和回收操作的性能明显要差于TLS实现的线程私有数据。2. 管理线程“一对一”模型的好处之一是线程的调度由核心完成了,而其他诸如线程创建
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 计算机软件及应用 计算机软件 应用 Linuxpthread 分析 文档 04

链接地址:https://www.31ppt.com/p-4561571.html