Linux posix线程库总结

   由于历史原因,2.5.x以前的linux对pthreads没有提供内核级的支持,所以在linux上的pthreads实现只能采用n:1的方式,也称为库实现。

线程的实现,经历了如下发展阶段:

  • LinuxThreads : Linux2.6之前
  • NPTL (Native Posix Thread Library) : RedHat负责,Linux2.6之后
  • NGPT (Next Generation Posix Thread): IBM负责,同NPTL同时开始研究的,但是最后被抛弃了(IBM......)

目前,pthreads的实现有3种方式:

(1)第一种,多对一,linux Threads,也就是库实现。

  linux Threads,这是linux标准的的线程库,但是与IEEE的POSIX不兼容. 在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建并启动管理线程。然后管理线程再来创建用户请求的线程。也就是说,用户在调用pthread_create后,先是创建了管理线程,再由管理线程创建了用户的线程。

(2)第二种,1:1模式。 NPTL

 

  1:1模式适合CPU密集型的机器。我们知道,进程(线程)在运行中会由于等待某种资源而阻塞,可能是I/O资源,也可能是CPU。在多CPU机器上1:1模式是个很不错的选择。因此1:1的优点就是,能够充分发挥SMP的优势。
这种模式也有它的缺点。由于OS为每个线程建立一个内核线程,导致内核级的内存空间(IA32机器的4G虚存空间的最高1G)的大开销。第二个缺点在于,在传统意义上,在mutex互斥锁和条件变量上的操作要求进入内核态,这是因为OS负责调度,它要为线程的转态转换负全责。后面我们会看到,Linux的NPTL库避免了这个缺点,它的互斥锁与条件变量的操作在用户态完成。

(3)第三种,M:N模式。

  这种模式试图兼有上面2种模式的优点。它要求一个分层的模型。比方说,某个进程内有4个线程,其中每2个线程对应一个内核线程。这样,OS知道2个实体的存在,负责它们的调度。而在一个实体内有2个线程,这2个线程间的调度就是OS不加干涉、也不知道的了。
简单的说,M:N模型的OS级调度上,跟1:1模型相似;线程间调度上,跟n:1模型相似。
优点是,既可以在进程内利用SMP的优势,又可以节省系统空间内存的消耗,而且环境切换大多在用户态完成。
缺点是显然的:复杂。

 

  Linux线程是通过进程来实现。Linux kernel为进程创建提供一个clone()系统调用,clone的参数包括如 CLONE_VM, CLONE_FILES, CLONE_SIGHAND 等。通过clone()的参数,新创建的进程,也称为LWP(Lightweight process)与父进程共享内存空间,文件句柄,信号处理等,从而达到创建线程相同的目的。

 

普通进程和LWP在实现上的不同点是:

  • 普通进程,fork会调用clone,第三个参数flags不会包含CLONE_THREAD
  • LWP,pthread_create会调用clone,第三个参数flags会包含CLONE_THREAD(可能还有其他几个标志),标示子进程和父进程同属一个线程组(相同TGID)

线程和LWP是同一个东西,只是在用户态,我们管进程中每一个执行序列为“线程”,但是内核中它被称为LWP。因为内核上没有线程的概念,CPU的调度是以进程为单位的。

 

  在Linux 2.6之前,Linux kernel并没有真正的thread支持,一些thread library都是在clone()基础上的一些基于user space的封装,因此通常在信号处理、进程调度(每个进程需要一个额外的调度线程)及多线程之间同步共享资源等方面存在一定问题。Linux 2.6的线程库叫NPTL(Native POSIX Thread Library)。POSIX thread(pthread)是一个编程规范,通过此规范开发的多线程程序具有良好的跨平台特性。尽管是基于进程的实现,但新版的NPTL创建线程的效率非常高。一些测试显示,基于NPTL的内核创建10万个线程只需要2秒,而没有NPTL支持的内核则需要长达15分钟。

  在Linux中,每一个线程都有一个task_struct。线程和进程可以使用同一调度其调度。内核角度上来将LWP和Process没有区别,有的仅仅是资源的共享。如果独享资源则是HWP,共享资源则是LWP。而在真正内核实现的NPTL的实现是在kernel增加了futex(fast userspace mutex)支持用于处理线程之间的sleep与wake。futex是一种高效的对共享资源互斥访问的算法。kernel在里面起仲裁作用,但通常都由进程自行完成。NPTL是一个1×1的线程模型,即一个线程对于一个操作系统的调度进程,优点是非常简单。而其他一些操作系统比如Solaris则是MxN的,M对应创建的线程数,N对应操作系统可以运行的实体。(N<M),优点是线程切换快,但实现稍复杂。

 

注:

(1)pthread线程库--NPTL(Native POSIX Threading Library)

  在1:1核心线程模型中,应用程序创建的每一个线程(也有书称为LWP)都由一个核心线程直接管理。OS内核将每一个核心线程都调到系统CPU上,

因此,所有线程都工作在“系统竞争范围”(system contention scope):线程直接和“系统范围”内的其他线程竞争。

(2)NGPT(Next Generation POSIX Threads)

  N:M混合线程模型提供了两级控制,将用户线程映射为系统的可调度体以实现并行,这个可调度体称为轻量级进程(LWP:light weight process),LWP

再一一映射到核心线程。如下图所示。OS内核将每一个核心线程都调到系统CPU上,因此,所有线程都工作在“系统竞争范围”。

 

添加:各阶段线程的具体实现

LinuxThreads

在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建并启动管理线程。然后管理线程再来创建用户请求的线程。也就是说,用户在调用pthread_create后,先是创建了管理线程,再由管理线程创建了用户的线程。

为了遵循POSIX对线程的一个规定:当"进程"收到一个致命信号(比如由于段错误收到SIGSEGV信号), 进程内的线程全部退出,LinuxThreads的实现方法:

  1. 程序第一次调用pthread_create时, linuxthreads发现管理线程不存在, 于是创建这个管理线程. 这个管理线程是进程中的第一个线程(主线程)的儿子.
  2. 然后在pthread_create中, 会通过pipe向管理线程发送一个命令, 告诉它创建线程. 即是说, 除主线程外, 所有的线程都是由管理线程来创建的, 管理线程是它们的父亲.
  3. 于是, 当任何一个子线程退出时, 管理线程将收到SIGUSER1信号(这是在通过clone创建子线程时指定的). 管理线程在对应的sig_handler中会判断子线程是否正常退出, 如果不是, 则杀死所有线程, 然后自杀.
  4. 主线程是管理线程的父亲, 其退出时并不会给管理线程发信号. 于是, 在管理线程的主循环中通过getppid检查父进程的ID号, 如果ID号是1, 说明父亲已经退出, 并把自己托管给了init进程(1号进程). 这时候, 管理线程也会杀掉所有子线程, 然后自杀.

容易发现,管理线程可能成为多线程系统的瓶颈,线程创建和销毁的开销很大(需要IPC)。
更为重要的是,LinuxThreads无法满足Posix对线程的绝大多数规定,比如:

  • 查看进程列表的时候, 相关的一组task_struct应当被展现为列表中的一个节点;
  • 发送给这个"进程"的信号(对应kill系统调用), 将被对应的这一组task_struct所共享, 并且被其中的任意一个"线程"处理;
  • 发送给某个"线程"的信号(对应pthread_kill), 将只被对应的一个task_struct接收, 并且由它自己来处理;
  • 当"进程"被停止或继续时(对应SIGSTOP/SIGCONT信号), 对应的这一组task_struct状态将改变;

NPTL

在linux 2.6中, 内核有了线程组的概念, task_struct结构中增加了一个tgid(thread group id)字段.
如果这个task是一个"主线程", 则它的tgid等于pid, 否则tgid等于进程的pid(即主线程的pid).

通过如下方式,解决了LinuxThreads不能兼容POSIX的问题:

  • 有了tgid, 内核或相关的shell程序就知道某个tast_struct是代表一个进程还是代表一个线程, 也就知道在什么时候该展现它们, 什么时候不该展现(比如在ps的时候, 线程就不要展现了).
  • 为了应付"发送给进程的信号"和"发送给线程的信号", task_struct里面维护了两套signal_pending, 一套是线程组共享的, 一套是线程独有的。通过kill发送的信号被放在线程组共享的signal_pending中, 可以由任意一个线程来处理; 通过pthread_kill发送的信号(pthread_kill是pthread库的接口, 对应的系统调用中tkill)被放在线程独有的signal_pending中, 只能由本线程来处理.
  • 当线程停止/继续, 或者是收到一个致命信号时, 内核会将处理动作施加到整个线程组中.

NGPT

上面提到的两种线程库使用的都是内核级线程(每个线程都对应内核中的一个调度实体), 这种模型称为1:1模型(1个线程对应1个内核级线程);
而NGPT则打算实现M:N模型(M个线程对应N个内核级线程), 也就是说若干个线程可能是在同一个执行实体上实现的.

因为模型太复杂,貌似没有实现出来所有预定功能,所以被放弃了。

 

参考链接

linux中的线程的实质和实现  http://particle128.com/posts/2014/05/linux-thread.html

Linux进程、线程模型,LWP,pthread_self() http://blog.csdn.net/tianyue168/article/details/7403693

posted @ 2016-06-09 10:38  轻舟已过万重山  阅读(1092)  评论(0编辑  收藏  举报