Linux 线程浅析

进程和线程的区别与联系

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。

为了让进程完成一定的工作,进程必须至少包含一个线程。

进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位。

线程存在与进程当中,是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。

线程就是个无产阶级,但无产阶级干活,总得有自己的劳动工具吧,这个劳动工具就是栈,线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的。

进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler,等;

线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集,等;

然而,一直以来,Linux 内核并没有线程的概念。每一个执行实体都是一个 task_struct 结构,通常称之为进程。

进程是一个执行单元,维护着执行相关的动态资源。同时,它又引用着程序所需的静态资源。通过系统调用 clone 创建子进程时,可以有选择性地让子进程共享父进程所引用的资源,这样的子进程通常称为轻量级进程。

linux 上的线程就是基于轻量级进程,由用户态的 pthread 库实现的。使用 pthread 以后,在用户看来,每一个 task_struct 就对应一个线程,而一组线程以及它们所共同引用的一组资源就是一个进程。

但是,一组线程并不仅仅是引用同一组资源就够了,它们还必须被视为一个整体。

对此,POSIX 标准提出了如下要求:

1)查看进程列表的时候,相关的一组 task_struct 应当被展现为列表中的一个节点;

2)发送给这个“进程”的信号(对应 kill 系统调用), 将被对应的这一组 task_struct 所共享,并且被其中的任意一个“线程”处理;

3)发送给某个“线程”的信号(对应 pthread_kill), 将只被对应的一个task_struct接收,并且由它自己来处理;

4)当“进程”被停止或继续时(对应 SIGSTOP/SIGCONT 信号), 对应的这一组 task_struct 状态将改变;

5)当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出;

6)等等(以上可能不够全)

LinuxThreads

在 linux 2.6 以前,pthread 线程库对应的实现是一个名叫 LinuxThreads 的 lib。

LinuxThreads 利用前面提到的轻量级进程来实现线程,但是对于 POSIX 提出的那些要求,LinuxThreads 除了第 5 点以外(当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出),都没有实现(实际上是无能为力):

1)如果运行了 A 程序,A 程序创建了 10 个线程,那么在 shell 下执行 ps 命令时将看到 11 个 A 进程,而不是 1 个(注意, 也不是10个,下面会解释);

2)不管是 kill 还是 pthread_kill,信号只能被一个对应的线程所接收;

3)SIGSTOP/SIGCONT 信号只对一个线程起作用;

还好 LinuxThreads 实现了第 5 点,我认为这一点是最重要的。如果某个线程“挂”了,整个进程还在若无其事地运行着,可能会出现很多的不一致状态。进程将不是一个整体,而线程也不能称为线程。

或许这也是为什么 LinuxThreads 虽然与 POSIX 的要求差距甚远,却能够存在,并且还被使用了好几年的原因吧~

但是,LinuxThreads 为了实现这个“第 5 点”, 还是付出了很多代价,并且创造了 LinuxThreads 本身的一大性能瓶颈。

接下来要说说,为什么 A 程序创建了 10 个线程,但是 ps 时却会出现 11 个 A 进程了。 因为 LinuxThreads 自动创建了一个管理线程。上面提到的“第5点”就是靠管理线程来实现的。

当程序开始运行时, 并没有管理线程存在(因为尽管程序已经链接了 pthread 库, 但是未必会使用多线程)。 程序第一次调用 pthread_create 时,LinuxThreads 发现管理线程不存在,于是创建这个管理线程。这个管理线程是进程中的第一个线程(主线程)的儿子。然后在 pthread_create 中,会通过 pipe 向管理线程发送一个命令,告诉它创建线程。即是说,除主线程外,所有的线程都是由管理线程来创建的,管理线程是它们的父亲

于是,当任何一个子线程退出时,管理线程将收到 SIGUSER1 信号(这是在通过 clone 创建子线程时指定的)。管理线程在对应的 sig_handler 中会判断子线程是否正常退出,如果不是,则杀死所有线程,然后自杀。

那么,主线程怎么办呢? 主线程是管理线程的父亲,其退出时并不会给管理线程发信号。 于是,在管理线程的主循环中通过 getppid 检查父进程的 ID 号,如果 ID 号是 1,说明父亲已经退出,并把自己托管给了 init 进程(1 号进程)。这时候,管理线程也会杀掉所有子线程,然后自杀。

那么,如果主线程是调用 pthread_exit 主动退出的呢? 按照 posix 的标准,这种情况下其他子线程是应该继续运行的。于是,在 LinuxThreads 中,主线程调用 pthread_exit 以后并不会真正退出,而是会在 pthread_exit 函数中阻塞等待所有子线程都退出了, pthread_exit 才会让主线程退出。(在这个等等过程中,主线程一直处于睡眠状态。)

可见,线程的创建与销毁都是通过管理线程来完成的,于是管理线程就成了 LinuxThreads 的一个性能瓶颈。线程的创建与销毁需要一次进程间通信,一次上下文切换之后才能被管理线程执行,并且多个请求会被管理线程串行地执行

NPTL

到了 linux 2.6,glibc 中有了一种新的 pthread 线程库 —— NPTL(Native POSIX Threading Library)。

NPTL 实现了前面提到的 POSIX 的全部5点要求:

1)查看进程列表的时候,相关的一组 task_struct 应当被展现为列表中的一个节点;

2)发送给这个“进程”的信号(对应 kill 系统调用), 将被对应的这一组 task_struct 所共享,并且被其中的任意一个“线程”处理;

3)发送给某个“线程”的信号(对应 pthread_kill), 将只被对应的一个task_struct接收,并且由它自己来处理;

4)当“进程”被停止或继续时(对应 SIGSTOP/SIGCONT 信号), 对应的这一组 task_struct 状态将改变;

5)当“进程”收到一个致命信号(比如由于段错误收到 SIGSEGV 信号), 对应的这一组 task_struct 将全部退出;

但是,实际上,与其说是 NPTL 实现了,不如说是linux内核实现了。

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

在 clone 系统调用中,传递 CLONE_THREAD 参数就可以把新进程的 tgid 设置为父进程的 tgid(否则新进程的 tgid 会设为其自身的 pid)。

类似的 XXid 在 task_struct 中还有两个:task->signal->pgid 保存进程组的打头进程的 pid、task->signal->session 保存会话打头进程的 pid 。通过这两个 id 来关联进程组和会话。

有了 tgid,内核或相关的 shell 程序就知道某个 tast_struct 是代表一个进程还是代表一个线程,也就知道在什么时候该展现它们,什么时候不该展现(比如在 ps 的时候,线程就不要展现了)。而 getpid(获取进程 ID)系统调用返回的也是 tast_struct 中的 tgid,而 tast_struct 中的 pid 则由 gettid 系统调用来返回。

在执行 ps 命令的时候不展现子线程,也是有一些问题的。比如程序 a.out 运行时,创建了一个线程。假设主线程的 pid 是 10001、子线程是 10002(它们的 tgid 都是10001)。这时如果你 kill 10002,是可以把 10001 和 10002 这两个线程一起杀死的,尽管执行 ps 命令的时候根本看不到 10002 这个进程。如果你不知道 linux 线程背后的故事,肯定会觉得遇到灵异事件了。

下面我们一起来验证这灵异事件:

[cpp] view plaincopy

  1. #include <unistd.h>
  2. #include <stdlib.h>
  3. #include <stdio.h>
  4. #include <pthread.h>
  5. void *fun(void *arg)
  6. {
  7. printf("thread is created!\n");
  8. pause(); //挂起线程
  9. }
  10. int main(void)
  11. {
  12. pthread_t tid;
  13. pthread_create(&tid, NULL, fun, NULL);
  14. pause();//主线程挂起(否则主线程终止,子线程也就挂了)
  15. return 0;
  16. }

这个程序创建一个线程后挂起,子线程在输出 “thread is created!” 也挂起。运行结果如下图:

我们打开另外一个终端,查看后台运行的进程,发现 demo 的进程号(pid)是 2361,我们使用 kill 终止 pid 为 2362 的进程(注意 ps 中并没有这个进程),如下图:

结果发现,demo 进程也终止了,如下图。其原因就是 2362 就是所创建线程的线程号,线程异常终止了,其对应的进程也就终止了。

为了应付“发送给进程的信号”和“发送给线程的信号”, task_struct 里面维护了两套 signal_pending,一套是线程组共享的,一套是线程独有的。

通过 kill 发送的信号被放在线程组共享的 signal_pending 中,可以由任意一个线程来处理;通过 pthread_kill 发送的信号(pthread_kill 是 pthread 库的接口,对应的系统调用中 tkill)被放在线程独有的 signal_pending 中, 只能由本线程来处理。

当线程停止/继续,或者是收到一个致命信号时,内核会将处理动作施加到整个线程组中。

NGPT

说到这里,也顺便提一下 NGPT(Next Generation POSIX Threads)。
 
上面提到的两种线程库使用的都是内核级线程(每个线程都对应内核中的一个调度实体),这种模型称为 1:1 模型(1 个线程对应 1 个内核级线程);

而 NGPT 则打算实现 M:N 模型(M 个线程对应 N 个内核级线程),也就是说若干个线程可能是在同一个执行实体上实现的。 线程库需要在一个内核提供的执行实体上抽象出若干个执行实体,并实现它们之间的调度。这样被抽象出来的执行实体称为用户级线程。

大体上,这可以通过为每个用户级线程分配一个栈,然后通过 longjmp 的方式进行上下文切换。(百度一下"setjmp,longjmp", 你就知道。)

但是实际上要处理的细节问题非常之多,目前的 NGPT 好像并没有实现所有预期的功能,并且暂时也不准备去实现。

用户级线程的切换显然要比内核级线程的切换快一些,前者可能只是一个简单的长跳转,而后者则需要保存/装载寄存器,进入然后退出内核态。(进程切换则还需要切换地址空间等)

而用户级线程则不能享受多处理器,因为多个用户级线程对应到一个内核级线程上,一个内核级线程在同一时刻只能运行在一个处理器上。

不过,M:N 的线程模型毕竟提供了这样一种手段,可以让不需要并行执行的线程运行在一个内核级线程对应的若干个用户级线程上,可以节省它们的切换开销。

据说一些类 UNIX 系统(如 Solaris)已经实现了比较成熟的 M:N 线程模型,其性能比起 linux 的线程还是有着一定的优势。

时间: 2024-11-03 01:40:03

Linux 线程浅析的相关文章

浅析Linux线程中数据

本文首先概述了线程中有哪些数据私有的,以及进程中哪些数据线程是共享的,然后详细分析了线程在用户空间中的数据,最后通过一个多线程程序来分析线程中的数据分布. 概述 线程包含了表示进程内执行环境必需的信息,其中包括进程中标识的线程ID.一组寄存器值.栈.调度优先级和策略.信号屏蔽字(每个线程有自己的信号屏蔽字,但对某个信号的处理方式是进程中所有线程共享的).errno变量(每个线程都自己的局部errno)以及线程私有数据.进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本.程序的全局内

Linux 进程调度浅析

概述 操作系统要实现多进程,进程调度必不可少.有人说,进程调度是操作系统中最为重要的一个部分.我觉得这种说法说得太绝对了一点,就像很多人动辄就说"某某函数比某某函数效率高XX倍"一样,脱离了实际环境,这些结论是比较片面的. 而进程调度究竟有多重要呢? 首先,我们需要明确一点:进程调度是对 TASK_RUNNING 状态的进程进行调度.如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系.所以,如果你的系统负载非常低,盼星星盼月亮才出现一个可执行状态的进程.那么进程调度也就不会

[转载]Linux 线程实现机制分析

本文转自http://www.ibm.com/developerworks/cn/linux/kernel/l-thread/ 支持原创.尊重原创,分享知识! 自从多线程编程的概念出现在 Linux 中以来,Linux 多线应用的发展总是与两个问题脱不开干系:兼容性.效率.本文从线程模型入手,通过分析目前 Linux 平台上最流行的 LinuxThreads 线程库的实现及其不足,描述了 Linux 社区是如何看待和解决兼容性和效率这两个问题的. 一.基础知识:线程和进程 按照教科书上的定义,进

c# 线程浅析(代理 、Invoke、Lock)

前言:本来想根据自己的经验总结一下c#线程相关的知识点, 写之前看了一些其他人的博客,发现自己也就掌握了不到三分之一....希望通过这次的博客将自己的知识点补充一下,写出更直白的博客和初学者分享. 这是我参考的博客地址:http://www.cnblogs.com/miniwiki/archive/2010/06/18/1760540.html  . 这个是他参考的英文原著地址:http://www.albahari.com/threading/ 原博客介绍的可以说深入浅出,鞭辟入里.不过我想写

Linux 线程与进程,以及通信

http://blog.chinaunix.net/uid-25324849-id-3110075.html 部分转自:http://blog.chinaunix.net/uid-20620288-id-3025213.html 1.首先要明确进程和线程的含义: 进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位.与程序相比,程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体.进程是程序在某个数据集上的执行,

linux线程(二)内存释放

linux线程有两种模式joinable和unjoinable. joinable线程:系统会保存线程资源(栈.ID.退出状态等)直到线程退出并且被其他线程join. unjoinable线程:系统会在线程退出时自动回收线程资源. linux线程创建后默认为joinable模式,因此线程退出时不会释放资源.若程序中大量的创建线程并未处理,则会导致内存泄漏,最终将导致不能继续创建线程. 应用举例: 1. 一般情况我们并不关注线程的状态,只是让其执行一些操作,所以要将线程设为unjoinable.实

linux线程

linux线程私有的部分:每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器 linux进程切换时通过TSS段,TSS中的esp0和ss0都是系统初始化设置的,指向进程创建时候,分配的栈空间.当进程切换的时候,在内核态下才进行进程切换,在0.11版本中,当前进程的寄存器被压入当前进程的TSS中,然后利用长跳转指令来到下一个TSS中,把这个TSS中的寄存器数据全给赋给寄存器(用于恢复寄存器)

Linux 线程模型的比较:LinuxThreads 与 NPTL

Linux 线程模型的比较:LinuxThreads 与 NPTL 本文参照来源:IBM开发者论坛 前奏:关于POSIX 可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX),是IEEE为要在各种UNIX操作系统上运行的软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945.此标准源于一个大约开始于1985年的项目.POSIX这个名称是由理查德·斯托曼应IEE

Linux 线程 条件变量

下面是一个多线程,生产者消费者问题,一个队列放暂存的数据: 1 #include <iostream> 2 #include <queue> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <pthread.h> 6 7 using std::cout; 8 using std::endl; 9 using std::queue; 10 11 #define N 100 12 #def