没有合适的资源?快使用搜索试试~ 我知道了~
linux多线程编程基础,进程间的通信,进程信号量处理,互斥锁
资源推荐
资源详情
资源评论
线程浅析
对于理解 Linux 进程与线程关系,以及所谓的"Linux 没有线程只有轻量级进程的概念"很好的一篇
文章。
在许多经典的操作系统教科书中, 总是把进程定义为程序的执行实例, 它并不执行什么, 只是维护
应用程序所需的各种资源. 而线程则是真正的执行实体.
为了让进程完成一定的工作, 进程必须至少包含一个线程. 如图 1.
进程所维护的是程序所包含的资源(静态资源), 如: 地址空间, 打开的文件句柄集, 文件系统状态
, 信号处理 handler, 等;
线程所维护的运行相关的资源(动态资源), 如: 运行栈, 调度相关的控制信息, 待处理的信号集,
等;
然而, 一直以来, linux 内核并没有线程的概念. 每一个执行实体都是一个 task_struct 结构,
通常称之为进程. 如图 2.
进程是一个执行单元, 维护着执行相关的动态资源. 同时, 它又引用着程序所需的静态资源.
通过系统调用 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 点以外, 都没有实现(实际上是无能为力):
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 号进程). 这时候, 管理线程也会杀掉所有子线程, 然后自杀.
可见, 线程的创建与销毁都是通过管理线程来完成的, 于是管理线程就成了 linuxthreads 的一个
性能瓶颈.
创建与销毁需要一次进程间通信, 一次上下文切换之后才能被管理线程执行, 并且多个请求会被管理
线程串行地执行.
NPTL
到了 linux 2.6, glibc 中有了一种新的 pthread 线程库--NPTL(Native POSIX Threading
Library).
NPTL 实现了前面提到的 POSIX 的全部 5 点要求. 但是, 实际上, 与其说是 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 线程背后的故事,肯定会觉得遇到灵异事件了。
为了应付"发送给进程的信号"和"发送给线程的信号", 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 的
线程还是有着一定的优势.;
一.基础知识:线程和进程
按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上
,从进程演化出线程,最主要的目的就是更好的支持 SMP 以及减小(进程/线程)上下文切换开销。
无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如 cpu
、内存、文件等等),而将线程分配到某个 cpu 上执行。一个进程当然可以拥有多个线程,此时,如
果进程运行在 SMP 机器上,它就可以同时使用多个 cpu 来执行各个线程,达到最大程度的并行,以
提高效率;同时,即使是在单 cpu 的机器上,采用多线程模型来设计程序,正如当年采用多进程模型
代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应
多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程
的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共
享了除 cpu 以外的所有资源的。
针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是
线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下
文切换开销。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足 smp 系统的
需要,也支持用线程库的方式在用户态实现另一套线程机制,此时一个核心线程同时成为多个用户态
线程的调度者。正如很多技术一样,"混合"通常都能带来更高的效率,但同时也带来更大的实现难度
,出于"简单"的设计思路,Linux 从一开始就没有实现混合模型的计划,但它在实现上采用了另一种
思路的"混合"。
在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内
至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,
而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来
的。
当核内既支持进程也支持线程时,就可以实现线程-进程的"多对多"模型,即一个进程的某个线程由
核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这
就是前面提到的"混合"线程模型,既可满足多处理机系统的需要,也可以最大限度的减小调度开销。
绝大多数商业操作系统(如 Digital Unix、Solaris、Irix)都采用的这种能够完全实现
POSIX1003.1c 标准的线程模型。在核外实现的线程又可以分为"一对一"、"多对一"两种模型,前
者用一个核心进程(也许是轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,
而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型
的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,
但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以
剩余20页未读,继续阅读
资源评论
no_明日复明日
- 粉丝: 4
- 资源: 1
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功