Solaris的线程模型

操作系统精髓与设计原理

Solaris实现了一种不一般的多层的线程模型,这种线程模型为开拓处理器性能提供有效且灵活的方案。

KEMIN: 应用程序功能复杂化使程序具有很好的并发性,而一开始操作系统实现的是多进程模型,但当多进程的模型的实现代价超出并发带来效益时,人们开始改造这种并发模型,线程概念及线程模型出现。

多线程构架

Solaris2.X引入了四个线程相关的概念:

  • 进程(process):这是直接引用原来的UNIX的进程概念,包括用户地址空间、堆栈和进程控制块;
  • 用户级线程(User-level threads)用户级线程是通过在进程地址空间内的线程库实现的,这种线程是内核不可见的。
  • 轻量进程(Lightweight processes)一个轻量进程可以看成是用户级线程到内核线程之间的映射。每个轻量进程可以把一个或多个用户级线程映射为一个内核线程。轻量进程被内核单独调度并且可在多处理器间并行运行。
  • 内核线程(Kernel thread)内核线程是基本被调度实体,并且也是被指派(dispatch)到多处理器之中一个上运行的独立实体。

下图展示了这四个概念之间的关系。注意每个轻量进程都对应一个内核线程的。轻量进程在进程内是可见的,所以轻量进程的数据结构存放相应的进程地址空间内。同时,每一个轻量进程都绑定到单独可指派的(dispatchable)内核线程,而内核线程数据结构是在内核空间内的。

上面图例子中,

一对一

进程一(process1)是单用户级线程绑定到单轻量进程的。这是传统进程的单线程运行情况。

多对一

进程二则是应用用户级线程策略的典型例子。所有的用户级线程由单独的内核线程支持,也就是说,每次只有一个用户级线程可以执行。这种结构适合那些有并发性(concurrency)而不需并行(parallel)运行的计算任务。

多对少

进程三是多个用户线程复用少量轻量进程的情况。Solaris通常允许应用把用户级线程复用到等数目或少一些的轻量进程上去。这样应用可以指定进程在内核中的并行程度(degree)。

N对N

进程四中的线程是以一对一映射的方式永久绑定轻量进程上的。这种结构使用内核的并行性对应用完全可见的,这很适合线程常常遇到阻塞而被挂起的情况。

M对N

进程五是进程三和进程一的混合。

动机

用户级和内核级线程的两层模型让应用程序员能够使用最有效和最适合的线程结构来开拓他们的应用的并发性。

多对一

有一些程序在逻辑结构上有并行性,代码结构化很好,但是不需要硬件并行运行它。比如,一个程序有多个交互的窗口,但是一次只有一个窗口是活动的,这个程序很适合使用多对一的线程模型。多对一模型的优点在于限制没有硬件级并行性的应用不浪费内核资源,因为多个用户线程是在用户空间内被创建、阻塞、激活和消毁的,不会涉及内核。如果用户级线程要在内核,那么内核必须为其分配内核数据结构并对用户级线程进行调度。下面列出三种上下文切换的代价,从中可看到内核级线程切换比用户级切换代价大很多。

少对多

如果应用中有些线程可能会被阻塞,比如进行输入输出操作,可使轻量进程等于或多于用户级线程的数目,这样应用或线程库都不用引入辅助手段解决线程被阻塞,因为如果进程中有一个用户线程被阻塞,其它用户线程还有剩余的轻量进程使用。

N对N

用户级线程到轻量进程的一对一的映射对一些应用是非常有效率的。比如并行数组计算,数组各行分给不同的线程执行,一对一的情况下,计算过程中没有线程切换的。

M对N

有一些复杂的应用适合使用混合模型。比如一些实时应用,这些应用需要有一部分线程任务在系统级优先级和实时调度,一部分线程任务执行后台服务并共享一个或一池的轻量进程。

进程控制块结构

下图大体的对比了Solaris 和传统UNIX系统的进程结构的区别。传统UNIX系统的进程结构包含了处理器ID、用户ID、信号分派表(signal dispatch table)(内核用其来决定在给进程发送信息时做些什么)、文件描述符(descriptors)、内存映射表和一个处理器状态结构(processor state structure)。处理器状态结构里保存着进程的内核堆栈。Solaris则保持这个基本结构,只是把单一的处理器状态结构替换多个数据结构,每个轻量进程一个。

轻量进程数据结构有如下的内容:

  • 一个轻量进程的ID
  • 轻量进程的优先级
  • 一个信号屏蔽(signal mask),用来过滤信号
  • 用户级寄存器的数据
  • 轻量进程的内核堆栈
  • 资源使用和profiling data
  • 内核线程的指针
  • 进程结构的指针

线程执行

下图是用户级线程和轻量进程执行状态转换的精简图。用户级线程的执行由线程库负责管理。让我们先看看非绑定线程(也就是多个用户级线程共用数个轻量进程)。非绑定线程有四种状态:可运行、活动、休眠和停止。处在活动状态的用户线程被赋给一个轻量进程,当对应的内核线程执行时,用户线程被执行。有很多事件能导致用户级线程离开活动状态。我们假设一个活动的用户级线程叫T1。如下的事件可能发生:

同步(synchornization):T1触发了一条并发原语而被排斥进行休眠状态。当同步的条件达到后再被转为可运行状态。

挂起(suspension):任何一个线程(包括T1自己)都可挂起T1(stop信号),把T1转为停止状态。T1必须等待其它线程向它发出一条continue信号才能离开挂起状态进入可运行状态。

抢占(preemption):当一活动线程(T1或其它线程)执行时触发另一个优先更高的线程(T2)进入了可运行状态,T1会被T2抢先,从活动状态转入可运行状态。

交出(yielding):如果T1执行库命令thr_yield()时,库的调度器查找是否有相同优先级的线程可运行,如果有,则调度它进入活动状态并运行;如果没有,则T1继续运行。

在如上的所有情况,如果T1被移离活动状态,线程库将选择另一个可运行的非绑定线程,将其绑定到一个可用的轻量进程进行运行。

图中还展示了轻量进程的状态转换图。我可以看到轻量进程的状态转换图是对用户级线程在活动状态的放大。这是两层模型不言自明的结果。

中断看作线程

大部分操作系统有两种基本的异步活动形式:进程和中断。进程(或线程)间相互协作时必须使用同步机制同步它的任务执行。

参考

http://blog.chinaunix.net/u/15906/showart_264682.html

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.