进程&线程模型

一.进程基本概念

以多道程序设计技术为切点。多道程序设计技术是操作系统最早引入的软件技术。它的基本思想是允许多个程序同时进入内存并运行。主要是为了提高 CPU的利用率,进而提高整个系统的效率。

来看一个例子:

​ 在A图中, 内存里有四个程序,因为只有一个物理的程序计数器,所以这四个程序呢是串形执行的。

​ 有了多道程序设计技术之后,每个程序变换成了一个独立的控制流,占用一个逻辑的程序计数器。这也是操作系统虚拟性的一个体现把一个物理的程序计数器,给它变换成多个逻辑的程序计数器,实际上每个程序都有自己的程序计数器,由于物理上只有一个程序计数器,所以每个程序真正的上 CPU 就把逻辑程序计数器的内容,推送到物理程序计数器里头。通过这种变换,达到了在内存中同时有多个程序,达到并发执行的效果。

​ C图表示出在一个时间间隔内,每一个程序 A B C D 都执行过了。由于只有一个物理 CPU ,所以这些程序是轮流在 CPU 上执行。但是从宏观上讲它们都在并发执行。

同时又产生了一个问题:在这样一个计算环境下,多个程序并发执行,如何管理在并发环境下同时执行的这些程序?首先来看下并发环境与并发程序。

1.并发环境与并发程序

并发环境:一段时间间隔内,单处理器上有两个或两个以上的程序同时处于开始运行但尚未结束的状态, 并且次序不是事先确定的。

并发程序:在并发环境中执行的程序。

在一个并发环境下执行的并发程序,怎么样来刻画这样的程序呢?于是进程的定义就应运而生了。

2.进程的定义

定义:进程是具有独立功能的程序,关于某个数据集合上的一次运行活动,是资源分配的单位,也是CPU调度的单位。又称任务。

特点:

  • 进程是程序的一次执行过程。(一个程序执行了两次,三次, 那就是不同的进程)

  • 进程是运行程序的一个抽象。(它代表了所运行的那个环境,代表了一个 CPU,因此有时候说进程是对 CPU 的一个抽象)

  • 将一个 CPU 把它变换成多个虚拟的 CPU 。(虚拟化技术)

  • 操作系统的资源是以进程为单位来分配的。比如说内存,文件等等。 最重要的一个资源就是地址空间。(操作系统为每一个进程分配了一个独立的地址空间)

  • 操作系统把CPU的控制权,交给了某一个进程,让这个进程上去运行,这称之为一个调度。

在操作系统执行过程中,会有很多的程序向操作系统提出申请来运行,那么操作系统怎么知道这些进程是存在,还是不存在呢? 这里就介绍操作系统为了管理进程所设计的一个非常重要的数据结构,进程控制块 PCB 。

3.进程控制块PCB

(1).定义

PCB:Process Control Block.又称进程描述符、进程属性。

​ 操作系统为了管理进程而,设计的一个非常重要的数据结构,这就是进程控制块PCB。这个数据结构是专门用于控制和管理进程的,它保存控制和管理进程所需要的所有的信息。主要是记录了进程的各种属性,并且描述出进程的运动变化过程,进程的发展程度进程控制块PCB这个数据结构是操作系统感知进程存在的一个标志。它们是一一对应的。

进程表:由于操作系统管理了很多的进程,为了便于管理,就把所有进程的每个进程的PCB集中在一起,放在了内存的固定区域,这就形成了进程表。

进程表是所有进程的 PCB 的一个集合。而且就是进程表的大小往往是固定的,确定了在一个操作系统中最多支持多少个进程 。

(2).PCB包含的信息内容

进程描述信息

  • 进程标识符(process ID),唯一的,通常是一个整数。
  • 进程名,通常基于可执行文件名,不唯一。
  • 用户标识符(user ID)
  • 进程组关系

进程控制信息

  • 当前状态
  • 优先级(priority)
  • 代码执行入口地址
  • 程序的磁盘地址
  • 运行统计信息(执行时间、页面调度)
  • 进程间同步和通信
  • 进程的队列指针
  • 进程的消息队列指针

所拥有的资源和使用情况

  • 虚拟地址空间的状况
  • 打开文件列表

CPU现场信息

CPU的现场信息是指当进程不运行的时候,操作系统要把一些重要的信息,硬件执行的状态信息,保存在PCB 里。

  • 寄存器值(通用寄存器、程序计数 器PC、程序状态字PSW、栈指针)

  • 指向该进程页表的指针

(3).SOLARIS的进程控制块与进程表

SOLARIS是基于 Unix 操作系统,它的进程控制块的名字一般叫Proc结构。每一个Proc结构代表一个PCB。把所有的Proc结构组织成一个链,那么这就是一个进程表。

Proc 结构保存的信息:(重点介绍三个)

第一个是可执行文件 p_exec,通过这样一个记录信息,可以找到这个进程所对应的可执行文件在磁盘上的位置。

第二个是进程的地址空间 p_as,进程地址空间放了很多内容,每一项内容都放在一段里头,通过段来把进程地址空间描述清楚,把这些段按照地址大小的顺序,把它建立成一个 AVL 树,便于以后的查找。

第三个是文件表,通过这张表可以把所有打开的文件都能找到。


二.进程状态及状态转换

1.进程的三种状态及状态的转换

(1).进程的三种基本状态

运行态(Running)

进程占有CPU,并在CPU上运行。

就绪态(Ready)

进程已经具备运行条件,但由于没有空闲CPU,而暂时不能运行。

等待态(Waiting/Blocked)

也称阻塞态、封锁态、睡眠态

进程因等待某一事件而暂时不能运行。

(2).三状态模型及状态转换

2.进程的其他状态

(1).创建态

已完成创建一进程所必要的工作,比如分配了PID、填写了PCB。但由于某些原因,操作系统尚未同意执行该进程。

(2).终止态

终止执行后,进程进入该状态。

  • 可完成一些数据统计工作

  • 资源回收

(3).挂起态

在操作系统当中,如果想进行一些负载调节时,可能会把进程送入这个状态。

比如如果现在系统中进程太多,CPU也忙不过来了,此时操作系统会把一部分进程,让它暂时不能运行,但是它又不是等待某个事件发生,所以就把它弄成一个特殊的状态,叫挂起态。

一旦进程进入了挂起态,操作系统会把它的内存空间呢收回来。把这些进程的相关的内容送到磁盘上保存起来,一旦继续让它运行,我们通常称之为激活。进程的内容再从磁盘上读入内存就可以了。

3.五状态模型

4.七状态模型

5.进程队列

操作系统在设计进程模型的时候要确定有什么样的状态,确定状态之间的转换,在什么条件下转换 ,通过什么样的操作来促成这种转换。 而且操作系统当中有很多的进程,它们都处于不同的状态。所以需要按不同的状态把它们管理起来,因此,操作系统设计了一个若干个进程队列。

操作系统设计了一个若干个进程队列,为每一个类进程建立一个或者多个队列也是可以的。 每个队列的元素,实际上就是PCB 状态的改变,其实就是某个进程的PCB从一个队列出队,然后在另一个队列里头入队的过程。也就是伴随着状态的改变,进程的PCB从一个队列进入到另外一个队列。

五状态进程模型的队列模型:


三.进程控制

进程控制操作主要是完成进程之间的各状态之间的转换,进程控制操作实际上就是具有特定功能的程序。 这个程序执行的时候,由于不允许被中断,所以把它称之为原语。

原语: 所谓原语(有时候又称之为原子操作) 是完成某种特定功能的一段程序,比如说完成创建,或者是完成阻塞,它是一段程序,完成了某种特定功能,但是这个程序在执行过程中,是具有不可分割性,或者是不可中断的,它必须持续地执行,不允许被打断,这就是原语。

实现原语需要操作系统通过屏蔽中断的一些措施来达到这样一个结果。进程控制操作最重要的一个就是进程的创建。

1.进程创建

主要完成以下几个工作:

  1. 首先给每一个新的进程分配一个标识ID,再给它找一个空的、 没有用过的进程控制块
  2. 然后要给这个进程分配它所需要的地址空间。 (如果这个地址空间在虚拟存储机制之下,就假设给了它,只是给了一个虚拟地址空间)。

  3. 再初始化这个进程控制块,填写相应的内容

    • 通常都是设定一些默认值,比如说状态,进程的状态设定为New 等等
  4. 创建进程控制块之后,要把它插入到相应的队列当中, 所以要设置相应的队列指针

2.进程撤销

进程的撤销实际上就是结束进程。

结束进程其实主要做两件事情:

  1. 把进程所占有的资源回收

    • 关闭它打开的文件;如果有网络连接就断开;如果分配了一些内存,就把它回收了
  2. 资源回收之后最重要的,是要把分配给它的 PCB 收回。

3.进程阻塞

处于运行状态的进程,在其运行过程中期待某一事件发生,如等待键盘输入、等待 磁盘数据传输完成、等待其它进程发送消息, 当被等待的事件未发生时,由进程自己执行阻塞原语,使自己由运行态变为阻塞态

4.UNIX的几个进程控制操作

  • fork( ):通过复制调用进程来建立新的进程,是 最基本的进程建立过程

  • exec( ):包括一系列系统调用,它们都是通过用 一段新的程序代码覆盖原来的地址空间,实现进程执行代码的转换

  • wait( ):提供初级进程同步操作,能使一个进程等待另外一个进程的结束
  • exit( ):用来终止一个进程的运行

他们都是以系统调用的形式,作为一个接头呈现给用户,由用户程序来调用 。

5.UNIX的FORK()实现

  1. 首先会为子进程分配一个空闲的进程描述符,也就是 PCB。

    PCB在 UNIX 一般叫 proc 结构

  2. 给子进程分配了一个唯一的标识pid

  3. 给子进程分配地址空间

    在 UNIX 里 fork 以一次一页的方式把父进程的地址空间内容完全地拷贝给子进程

  4. 从父进程那里继承各种共享资源

    比如打开的文件,当前工作目录等等

  5. 子进程的状态 设置为就绪态,并且把它插入到了就绪队列

  6. 做完这项工作之后,fork 就为子进程返回一个值 0

  7. 为父进程返回一个值,是子进程的 pid

那么也就是说,fork 执行完后,原来一个进程,父进程就一分为二,变成了两个进程,一个父进程,一个子进程。

在父进程得到的返回值是 pid,在子进程里得到的返回值是 0。

问题:以一次一页的方式来复制父进程的地址空间有什么弊端?

​ 父进程把它所有的内容都拷贝给子进程 ,但是子进程不一定需要,而且通常情况下,父进程创建子进程是让子进程做与父进程所不同的工作。所以把发父进程所有内容拷贝给子进程,实际上,子进程也不需要。因此,子进程会接着执行 exec这样一个函数来把父进程拷贝过来的这些地址空间给覆盖掉。因此之前的这种复制工作,实际上就是无用功了。

Linux 使用了写时复制技术 Copy-On-Write,Linux中父进程把地址空间的指针传递给子进程,再把地址空间设置为只读。那么当子进程要往地址空间里写东西的时候,操作系统会为子进程单独再开辟一块空间,把相应的内容放进去 那么这样的话呢,节省了之前复制父进程地址空间的时间,加快了 fork 的实现速度。

代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
void main(int argc, char *argv[])
{
pid_t pid;
pid = fork();/* 创建一个子进程 */
if (pid < 0) {/* 出错 */
fprintf(stderr, “fork failed”);
exit(-1);
}
else if (pid == 0) { /* 子进程 */
execlp(“/bin/ls”, “ls”, NULL);
}
else { /* 父进程 */
wait(NULL); /* 父进程等待子进程结束 */
printf(“child complete”);
exit(0);
}
}

流程:


四.进程相关概念

1.进程的讨论

(1).进程的分类

第一种分类

  • 系统进程
  • 用户进程

系统进程是操作系统为了管理一些资源而设计的进程,它的主要特点是优先级比较高。相对于用户进程而言,系统进程会优先被调度上CPU 去执行,因为它完成了一些关键的工作。

第二中分类

  • 前台进程
  • 后台进程

前台进程实际上就是和用户直接交互的这样一些进程:用户敲键盘、 动鼠标。

后台进程往往是操作系统在启动了以后创建的一些进程,这些进程为用户来进行服务,比如说打印进程。

有一些应用进程也在系统启动的时候被创建了, 比如说防火墙,还有一些电子邮件的接收。这样的一些进程它们在后台工作,然后发生了一些事件,它们来接收这些事件。对于用户来讲,他所打交道的是前台进程。

第三种分类

  • CPU 密集型进程
  • I/O 密集型程序

有一些进程需要用到很多的 CPU 时间。比如说,画面渲染需要大量的计算,因此把它称之为 CPU 密集型进程

有些经常需要输入、 输出、 读盘这样一些操作,这些进程被称之为 I/O 密集型

那么这两类进程的区分也是为了以后调度程序的选择做一些准备 。

(2).进程层次结构

UNIX进程家族树:init为根

UNIX 进程都是在一个家族里,这个家族树有一个根,这个根是一个 init 进程,是个 1 号进程,是所有进程的一个祖宗。在某些情况下,某一个进程它结束了,那么它的子孙进程,其实也必须全部的结束。

Windows:地位相同

那在 Windows 中也是一个进程创建另一个进程,但是创建完之后,这两个进程的关系比较疏远,也就是它们的地位是相同的。

2.进程与程序的区别

  • 进程更能准确刻画并发,而程序不能
  • 程序是静态的,进程是动态的
  • 进程有生命周期的,有诞生有消亡,是 短暂的 ; 而程序是相对长久的
  • 一个程序可对应多个进程
  • 进程具有创建其他进程的功能(程序没有)

3.进程地址空间

操作系统会给每一个进程都分配了一个地址空间。怎么理解这句话?怎么理解这样一个场景?

以代码为例:

1
2
3
4
5
6
int myval;
int main(int argc, char *argv[]) {
myval = atoi(argv[1]);
while (1)
printf(“myval is %d, loc 0x%lx\n”, myval, (long) &myval);
}

这个程序实际上是从命令行接收了参数,把这个参数赋给一个变量 myval 然后就是循环,来打印这个变量的值,同时把这个变量的位置 打印出来。

现在同时执行两个 myval 程序。 也就是这两个进程在执行,因为用了一个循环,所以这个进程会一直在那运行下去。如果运行这两个进程,分别用参数 7,8 来调用这个程序,输出的结果如下:

当执行 myval 7 的时候,myval 的这个变量值是 7 ,地址是 60104C 。同时 myval 8 也在执行。 发现 myval 的值是 8 ,而myval 的地址呢 也是 60104C。 为何变量 myval 的值是不一样的,地址却是相同的呢?

实际上每个进程有自己相对独立地址空间。 两个进程实际上是两个地址空间。而且它们的地址空间是隔离的。不同的地址空间,它的地址不是实际的物理内存地址。 实际上是一个相对地址,如果支持虚存的系统当中,那么这个地址就是虚拟地址,也可以说是一个相对地址,或者是逻辑地址。

上图是一个进程地址空间的表示图。在这个空间里,操作系统会占一部分内容:

  • 上半部分是操作系统内核的地址空间

  • 下面是用户地址空间

    用户地址空间包括了用户执行的过程中所需要的一些代码数据,一些临时变量。 还有在运行过程中如果进行了过程调用函数调用,需要用栈来传递参数,那么主体有这样一些内容。在进程运行过程中还可能调一些共享库, 因此还有一些共享库放在这个位置;如果打开了文件以文件内存映射文件的方式来使用这个文件的话,那么也用到这些空间。这就是进程用户地址空间的内容。

myvalue 7 和 myvalue 8 这两个进程每个都有这么一个地址空间 , 因此myvalue这个变量实际上是在不同的地址空间里的相同的位置。因此,我们看到了虚拟地址虽然是相同的 那么这个相同指的是对于这个地址空间的位置,而不是指的在物理内存的位置。

4.进程映像(IMAGE)

进程映像指的是进程执行过程中它的全过程的一个静态描述,可以把它看成是在某一瞬间的进程的快照。

包括的内容:地址空间的内容,硬件寄存器的内容,以及与该进程相关的一些内核数据结构和内核栈。

  • 用户相关:进程地址空间(包括代码段、数据段、 堆和栈、共享库……)

  • 寄存器相关:程序计数器、指令寄存器、程序状态 寄存器、栈指针、通用寄存器等的值

  • 内核相关:

    • 静态部分:PCB及各种资源数据结构
    • 动态部分:内核栈(不同进程在进入内核后使用不同的内核栈)

5.上下文(CONTEXT)切换

将CPU硬件状态从一个进程换到另一个进程的过程称为上下文切换。

  • 进程运行时,其硬件状态保存在CPU上的寄存器中。

    寄存器:程序计数器、程序状态寄存器、栈指针、通用寄存器、其他控制寄存器的值。

  • 进程不运行时,这些寄存器的值保存在进程控制块 PCB中当操作系统要运行一个新的进程时,将 PCB中的相关值送到对应的寄存器中。这个就完成了上下文切换的一个过程。


五.线程的引入

为什么在进程中再派生线程?具体有三个理由:

  • 应用的需要
  • 开销的考虑
  • 性能的考虑

1.Web服务器为例

(1).工作方式

以Web服务器为例,其工作方式是:

  1. 从客户端接收网页请求(http协议)
  2. 从磁盘上检索相关网页,读入内存
  3. 将网页返回给对应的客户端

每次到磁盘上搜索相关的网页,进程就会停在那里,这样性能就比较慢。 怎么样去提高服务器的工作的效率?

通常情况下,是在服务器的内存里头,开辟一个网页缓存 ( Web page Cache ),保存了常用的网页。当Web服务器从客户端接收了网页请求之后会先到网页缓存当中去查找,如果找到就直接把结果返回给客户端,就不用到磁盘上去找。 但是如果没找到就先到磁盘上去搜索相关的网页,得到了之后写入网页缓存,然后再把结果返回给用户。

(2).无线程的情况

此时有两种解决方案:

服务进程

设定一个服务进程,这个服务进程只能是顺序编程 。也就是说,如果它到磁盘上去搜寻网页,那就不能再去接收客户端的请求。因此会造成服务器性能下降。

为什么不能设定多个服务进程?

每个进程有自己独立的地址空间,所以它不能共享信息,所以只能有一个服务进程。

有限状态机

有限状态机的方法实际上是用一个复杂的编程模型来自己模拟一些并发的工作,即进程自己来模拟并发的工作。

比如说接收了一个用户请求之后,如果要到磁盘上搜寻这个网页,那么原本这个进程会被暂停,这个时候就要改造这个搜寻网页的操作,把它改造成一个非阻塞的I/O

到磁盘上去搜寻网页的同时这个进程还可以继续做与这个网页内容无关的一些工作。 所以叫非阻塞 I/O。

在查询网页的同时,磁盘在工作,而且这个进程就可以回来做别的事情,它可以继续去接收用户的客户端请求。 当新的请求被接收到之后,它继续可能在 Web Cache 里找网页,然后返回去,如果没找到网页呢,继续再去调用磁盘但是这就出现了一个问题:磁盘的这个结果返回了,究竟是哪一个客户端的请求呢?

所以这个时候,进程要自己把这些信息记录下来,然后磁盘返回了请求之后,就要判断是哪一个客户端的请求,然后返回给对应的客户 端。因此,它的编程模型呢是比较复杂的。

(3).引进多线程之后

工作方式:

把线程分成两类:

  • 分派线程

    分派线程只需要一个,分派线程的主要工作就是监听客户端,客户端只要有请求就把请求读进来,但是它不完成客户端的请求,它把这个请求分派给其它的线程来完成,而完成工作的线程就是工作线程。

  • 工作线程

    Web 服务器上有一堆工作线程,它们都是用来完成的是服务客户请求的。分派线程获得了客户端请求之后就把它分给某一个工作线程,工作线程呢还跟前面一样先到Web Cache里去查找网页是否存在,如果存在就返回给客户端,如果不存在就要启动磁盘,到磁盘上去搜寻网页。它到磁盘上搜寻网页,这是一个阻塞的I/O,这时它就会被阻塞,然后等待,但是没有关系,因为还有其它的工作线程。

    2.引入线程的优点

(1).从开销的角度

(2).从性能的角度

如果一个进程里头又有多个线程,而这些线程,有的计算,有的去 I/O,当有多个处理器的时候,就可以充分发挥这个优势了。所以当多处理器的情况下,一个进程就可以有很多的任务同时在执行。性能就提高了很多。


六.线程的基本概念

进程有两个基本的属性:

  • 进程是资源的拥有者。

  • 进程是CPU的一个调度单位。

但是有了线程之后,线程就继承了进程中的一个属性,也就是线程成为了 CPU 的调度单位。 而进程依然还是管理资源,然后是资源的一个拥有者。

线程实际上是进程中的一个运行实体。

从运行的角度,它是一个运行的实体,它是一个 CPU 的调度单位。 有的时候把线程称之为轻量级进程。 也就是说,在进程当中又增加了多个执行序列, 让这些执行序列可以并发执行,以提高软件的运行效率。

所以强调的是:在进程中增加了多个执行序列,叫线程。


七.线程的属性

线程是一个运行实体,它有属于线程自己的一些属性:

  • 有标识符ID

    同一个进程的不同线程要区分

  • 有状态及状态转换

    因为线程是上 CPU 的,所以它有状态。它也有状态的转换,也需要提供一些针对线程的操作。

  • 不运行的时需要保存上下文环境

    上下文环境:程序计数器等寄存器(保存在线程的相对的数据结构里)

  • 有自己的栈和栈指针

    不同的线程,这些信息也是不一样的。

  • 共享所在进程的地址空间和其他资源

    同一个进程的不同线程它们是共享所在进程地址空间,内容和这个进程所拥有的资源的,所以这是非常重要的,也就是线程之间的通信,或者其他的一些操作带来了便利的地方,它们是共享同一个进程的地址空间和有关的资源。

  • 可以创建、撤消另一个线程

    当创建进程以后,实际上是只有一个线程,我们称之为一个主线程。 然后由它再创建其他的线程,所以程序开始的时候,我们可以看成是一个单线程的进程在运行。


八.线程机制的实现

在操作系统中, 如何来支持线程机制的实现? 通常有三种方式:用户级线程核心级线程混合方式

由于在线程的概念提出之前,操作系统已经运行了很多年,进程的概念已经用了很长时间。 因此当有一个新的机制提出来的时候, 不同的操作系统对这个机制的支持是不一样的。

1.用户级线程

(1).概念

用户级线程:在用户空间建了一个线程库, 这个线程库里提供了一系列的针对线程的操作。 这些线程的管理是通过一个 Run-time System 运行时系统来管理的。 它完成的就是这些线程的创建和线程数据结构的一些管理工作。 如下图所示:

有一个Run-time System,它是管理这些线程里的数据结构、 线程表。这是用户级线程的一个实现。

对于内核而言, 线程的实现是在用户空间,所以操作系统内核并不知道线程的存在,也就是说,它的管理还是以进程为单位来管理,它没有感知线程的存在。

从图中可以看到线程的数据结构是由 Run-time System 来管理的。 内核只看到了进程的数据结构, 因此线程的切换,从一个线程换到另外一个线程不需要操作系统内核的干预,也不需要进入内核来做这件事情, 所以速度比较快。

UNIX 内的操作系统通常采用这种方式来支持线程。

(2).POSIX线程库——PTHREAD

UNIX内的操作系统通常采用这种方式来支持线程。 它支持线程的时候是遵循 POSIX 规范。也就是 POSIX 规范当中确定了多线程的这种编程的接口。

那以什么样的方式呈现给用户呢?它对线程库进行了相应的规范。 这个规范就是 PTHREAD 线程库。 这个线程库按照规范要提供若干个函数来支持线程、 创建线程、撤销线程,等待某个线程的结束。

在这堆操作当中,重点介绍一下 yield 函数,这个函数表示这个线程自愿让出 CPU。

我们知道一个进程的若干线程实际上是相互配合来完成一项任务的。 所以这线程之间是可以协商由谁上 CPU,所以一个线程如果占 CPU 时间太长,那么别的线程得不到机会,就需要这个线程高尚一点,让出 CPU,它就调用 yield 让出 CPU。 如果它不让出 CPU,其实其它线程是没法上 CPU 的,因为对于线程而言,它感知不到时钟中断,因为整个时钟段是对进程而言的。

(3).总结

优点:

  • 线程切换快
  • 调度算法是应用程序特定的
  • 用户级线程可运行在任何操作系统上(只需要实现线程库)

    缺点:

  • 内核只将处理器分配给进程,同一进程中的两 个线程不能同时运行于两个处理器上

  • 大多数系统调用是阻塞的,因此,由于内核阻塞进程,故进程中所有线程也被阻塞

    改变:

    • 把系统调用,阻塞系统调用改成一个非阻塞的
    • 用 Jacketing/ wrapper 的这种技术在系统调用之外封装一层。 在调用系统调用之前,先判断一下调用这个系统调用会不会导致线程阻塞,如果导致线程阻塞,那么就赶紧地换其它线程,这样的话,就不会因为某个线程调用了一个阻塞的系统调用使得整个进程被阻塞。

2.核心级线程

第二类实现线程机制的方法是核心级线程。这个方案就是彻底地改造了操作系统。

  • 内核管理所有的线程。 通过 API 的接口向用户提供一些 API 的函数,由用户可以创建线程。

  • 所以内核既维护了进程的数据结构,也维护了进程里头的各个线程的数据结构。

    从下图中可以看到内核里头既管了线程表, 也管了进程表。

  • 线程的切换需要内核干预,因此要进入内核来完成切换的过程。 调度也是以线程为单位来进行的。

实现核心级线程机制的典型的操作系统就是 Windows。

3.混合模型

混合模型就是线程的创建是在用户空间用线程库来完成的。 但是内核也要管理线程,也就是说调度是由内核来完成的。

这个采用这种混合模型实现线程机制的是 Solaris 操作系统。

用户空间的线程和内核的这个关系是什么?

用户线程通过了一个多路复用来复用多个内核级线程,也就是核外的用户空间的线程通过一个机制和核内的一个内核线程对应起来。调度内核这个线程上 CPU 其实就是调度这个核外的这个线程上 CPU。这是 Solaris 的一种实现。


九.总结

1.进程

  • 并发性:任何进程都可以与其他进程一起向前推进

  • 动态性:进程是正在执行程序的实例

    • 进程是动态产生,动态消亡的
    • 进程在其生命周期内,在三种基本状态之间转换
  • 独立性:进程是资源分配的一个独立单位

    例如:各进程的地址空间相互独立

  • 交互性:指进程在执行过程中可能与其他进程产生直 接或间接的关系

  • 异步性:每个进程都以其相对独立的、不可预知的速 度向前推进

  • 进程映像:程序 + 数据 + 栈(用户栈、内核栈) + PCB

2.线程

  • 多线程应用场景

    知道什么情况下去应用多线程

  • 线程基本概念、属性

    程作为一个进程中运行的一个实体,有哪些是属于它自己的信息?有哪些是共享同一个进程的其他的一些资源?

  • 线程实现机制

    操作系统中如何来实现线程?如何来支持线程?

看一个非常重要的概念:可再入程序(可重入程序)。

所谓可再入程序指的是可以被多个进程同时调用的程序,因此对这个程序有限制。 也就是它必须具有的性质是它是纯代码的, 在执行过程中这个代码不会改变。 如果有改变,就需要调用它的进程提供不同的数据区。 这些改变可以放在数据区,因为代码部分是不再改变的。 实际上是大部分进程和线程都必须是可再入程序才能去运行。

0%