信马由缰操作系统-闲话线程模型2
1 线程模型
目前的操作系统不再仅实现用户线程或者内核线程,而是通过组合的方式实现。由于链接方式的不同,所以形成了3种不同的多线程模型:多对一、一对一和多对多。
讨论线程模型还有一个前提是讨论的是同一个进程中的多个用户线程,不涉及跨进程的多个线程。
1.1 多对一
多对一模型指的是多个用户线程映射到一个内核线程,这里的一个内核线程相当于一个内核进程,所以也可以说是多个用户线程(常称M个)对应一个内核进程,即 M:1。
- 这些用户线程一般属于同一个进程,且运行在该进程的用户空间,多个线程也就不能并行运行在多个处理器上。
- 对这些线程的管理和调度也在该进程的用户空间中进行的。
- 只有当用户线程需要访问内核时,才会将其分配给一个内核线程,但每次仅允许一个线程进行映射。
这是一种用户线程的实现模型,用于不支持内核线程的系统,常见例子有: - Solaris Green Threads
- GNU Protable
主要优点是因为不用经过内核切换,线程间切换管理开销小,效率高。
主要缺点有:
- 一个线程访问内核时阻塞会导致该线程所在进程阻塞
- 同一时间只能有一个线程能运行,多个线程不能同时在多个处理器上执行。
由于其无法利用多个处理器,现在几乎没有系统继续使用这种模型。
1.2 一对一
一对一模型指的是为每一个用户线程都映射一个内核进程,即1:1。是现代操作系统广泛采用的模型,常见例子有:
- Windows 95/98/xp/7/NT/2000等常用版本
- Linux
- Solaris 9 及之后
- OS/2
主要优点是
- 一个线程阻塞后,允许调度另一个线程运行,所以会比多对一模型有更好的并发性。
- 在多处理器系统中,多个线程可以在多个处理器中并行。
主要缺点:每创建一个用户线程 ULT 都要创建一个对应的内核线程 KST,开销较大,效率较差,需要考虑限制整个系统中的线程数。
1.3 多对多
多对多模型指的是将多个用户线程(常称M个)映射到较少个内核线程中(常称N个,小于等于 M 个),即 M:N。对应关系比较复杂,执行起来也比较复杂,常见例子有:
- Solaris 9 以前的版本
- 带有 ThreadFiber 开发包的 Windows NT/200
内核线程数量是可以变化的,可以与用户线程数相同,也可以少于用户线程数。因此这种模型结合了上述两种模型的优点,可以像一对一模型使一个进程的多个线程并行执行,也可以像多对一模型中减少线程管理开销并提高效率。
这种模型的缺点也很明显就是该模型实现起来非常复杂。Linux曾经想用过,但由于过于复杂,最终选择了一对一模型。
多对多模型有一种变种既可以多对多,也允许绑定某个用户线程到一个内核线程,这个变种有时候也成为双层模型(tow-level model)。Solaris 操作系统在第九版以前持有这种双层模型,但从第9版后,开始使用一对一模型。
2 线程具体实现
上面介绍的模型属于理论,只有将理论实现才能成为我们使用线程。
2.1 内核线程 KST 的具体实现
我们在上篇文章简单说过,内核线程通常使用系统调用实现。一种可能的控制方式是,系统在创建一个新进程的时候,变为它分配了一个任务数据区( per task data area, PTDA ),其中包括若干个 TCB 空间,在每个 TCB 空间中都包含着线程的标识符等信息,虽然与 ULT 中的 TCB 信息相同,但它们是保存在内核空间的。
KST 的调度和切换与进程的调度和切换类似,但花费的开销更小。
2.2 用户线程 ULT 的具体实现
用户线程 ULT 基于中间系统实现,当前常见的中间系统有两种:运行时系统和核心线程(即 轻量级进程,LWP)。
2.2.1 运行时系统
运行时系统( runtime system)实质上是用于管理和控制线程的函数(过程)集合。其中包含创建和撤销线程的函数、用于控制线程同步和通信的函数以及用于实现线程调度的函数等。运行时系统中的所有函数均驻留在用户空间,并且作为 ULT 与内核之间的接口。
从上篇文章我们知道在传统 OS 中,进程的切换时必须先由用户态转为内核态,再由内核线程来执行切换任务。但使用 运行时系统 实现的 ULT 不需要进入内核态,只需要基于 运行时系统 的线程切换函数来执行切换任务,而且操作简单,因此切换过程很快。
关于系统资源分配,也不像传统OS中由内核管理,因为 ULT 本身不能执行系统调用,而是需要将需求传递给运行时系统,由后者通过相关系统调用来获得系统资源。
2.2.1 核心线程 LWP
核心线程又称为轻量级进程( Light Weight Process,LWP )。
- 每个进程可以有多个 LWP ,同用户线程一样,每个 LWP 都有自己数据结构(如TCB),其中包含线程标识符、优先级、状态等信息,另外还有栈和局部存储区等。
- LWP 可以共享进程拥有的资源,也可以通过系统调用获得内核提供的服务。当一个用户级线程运行时,只要将它连接到一个 LWP 上,就能具有 KST 的所有属性。这种线程实现方式是组合方式。
一个系统中可以有众多 ULT,但 LWP 不能无限多,一般通过设置一个缓冲区(即线程池)来解决。同时可以使多个 ULT 多路复用一个 LWP,但只有当前连接到 LWP 的 ULT 才能与 内核通信,其余的 ULT 或阻塞、或等待 LWP,而每个 LWP 都要与一个 KST 连接,这样通过 LWP 即可把 ULT 与 KST 连接起来。内核看到的是多个 LWP 而非 ULT,这样就把 ULT 与 KST 隔离了,从而使 ULT 与内核无关。当 ULT 不需要与内核通信时是不需要 LWP 的。
关于阻塞,有以下几种情况:
- KST 执行操作时如果发生阻塞,则与之相连的多个 LWP 均会阻塞,与这些 LWP 相连的 ULT 也会陷入阻塞。
- 对于 LWP 发生阻塞时,又可以细分为如下几种情况:
- 如果一个进程中只有一个 LWP,此时进程会被阻塞。这与传统 OS 中一样,进程执行系统调用时,该进程实际上是阻塞的。
- 如果一个进程中有多个 LWP,其中一个阻塞时,进程中的另一个 LWP 可以继续执行。
- 即使进程中所有 LWP 都发生阻塞,进程中的线程(即 ULT)仍可以继续执行,只是不能再去访问内核了。
3 线程库
线程库( thread library )是为程序员提供创建和管理线程的 API 。实现方案通常有两种:
- 第一种方法是在用户空间中提供一个没有内核支持的库,这种实现所有代码和数据结构均在用户空间,当程序员调用库中函数时其实只是用户空间中的一个本地函数的调用,而不是系统调用。
- 第二种方法是实现一个由操作系统直接支持的内核级别的库。这种实现库内所有的的代码和数据结构均在内核中,当调用库中的 API 函数时通常会导致对内核的系统调用。
目前有三种主要使用的线程库:POSIX Pthreads、Windows、Java。 - Pthreads 作为 POSIX 标准( IEEE 1003.1c )的扩展,可以提供用户级别或内核级别的库。
- 定义了线程创建和同步API,这个本身是线程行为的规范( specification )而不是实现( implementation)。操作系统设计人员可以根据自己的意愿选择如何实现。
- 大多数 UNIX 类系统如 Linux、Mac OS X 和 Solaris 都实现了这个线程规范。
- Windows 本身不支持 Pthreads,但有些第三方为其提供了实现。
- Windows 线程库是用于 Windows 操作系统的内核级线程库。
- Java 线程 API 允许线程在 Java 程序中直接创建和管理。大多数 JVM 运行在宿主操作系统上,所以 Java 线程 API 通常采用宿主操作系统的线程库来实现。即在 windows 系统中调用 WindowsAPI 来实现,在 UNIX 类系统中采用 Pthreads 来实现。
4 线程池
由于系统本身资源有限,所以线程不是创建越多越好,线程创建需要花费时间,而且创建过多可能导致 OOM 等问题,解决这些问题的一种方案就是使用线程池( thread pool )。通过创建一批线程并复用的方式,解决了无限创建等问题。其具有的优点如下:
- 用现有线程服务请求比等待创建一个线程更快。
- 线程池限制了任何时候可用线程的数量。这对那些不支持大量并发的系统非常重要。
- 将要执行的任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务。例如任务可以被安排在某个时间延迟后执行,或定期执行。
更多内容我们在后面介绍 Java 线程时具体介绍一下。
除了线程池,Open MP 和 Grand Central Dispatch (大中央调度)也是用于管理多线程应用程序的众多新技术之一,这两个在这里就不展开说了,有兴趣的可以自己看下。除此之外,其他商用方法包括并行和并行库,如:
- Intel 的线程构建模块 ( Threading Building Block,TBB)和一些微软的产品。
- Java 语言和 API 也对并发编程提供了重要支持。比如
java.util.concurrent
包,它支持隐式线程创建和管理。
5 参考资料
这里仅列出文章中未实时标注的参考资料。
- 《计算机操作系统(慕课版)》汤子瀛等
- 《操作系统概念(原书第9版)》
- 《深入理解计算机系统(原书第3版)》
除特别注明外,本站所有文章均为 windcoder 原创,转载请注明出处来自: xinmayoujiangcaozuoxitong-xianhuaxianchengmoxing2

暂无数据