> For the complete documentation index, see [llms.txt](https://mqjyl2012.gitbook.io/backend-development/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mqjyl2012.gitbook.io/backend-development/operating-system/operating-system-basics/process.md).

# 进程

操作系统一个很关键的概念就是进程（ Process）。进程是对正在运行中的程序的一个抽象，可以把进程看作是容纳运行一个程序所有信息的个容器。与每个进程相关的是**地址空间（address space）**，这是从某个最小值的存储位置（通常是零）到某个最大值的存储位置的列表。在这个地址空间中，进程可以进行读写操作。地址空间中存放有可执行程序，程序所需要的数据和它的栈。与每个进程相关的还有资源集，通常包括**寄存器（registers）**（寄存器一般包括**程序计数器（program counter**）和**堆栈指针（stack pointer）**）、打开文件的清单、突发的报警、有关的进程清单和其他需要执行程序的信息。进程是操作系统提供的最古老也是最重要的概念之一。即使可以使用的CPU只有一个，它们也支持（伪）并发操作，它们会将一个单独的CPU抽象为多个虚拟机的CPU。

> 伪并发：伪并行是指单核或多核处理器同时执行多个进程，从而使程序更快。 通过以非常有限的时间间隔在程序之间快速切换CPU，因此会产生并行感。缺点是CPU时间可能分配给下一个进程，也可能不分配给下一个进程。

## :pencil2: 1、进程模型

### :pen\_ballpoint: 1.1、程序

**程序**是一个在时间上按严格次序前后相继的操作序列，这些操作是机器指令或高级语言编写的语句。

#### **特点：**

1. 顺序性：程序所规定的动作在机器上严格地按顺序执行。
2. 封闭性：资源的状态（除了初始状态外）只有程序本身的动作才能改变。
3. 确定性：程序执行结果与它的执行速度无关，也称为程序执行结果与时间无关性。即CPU在执行程序时，任意两个动作之间的停顿对程序的计算结果都不会产生影响。
4. 可再现性：如果程序执行在不同的时间执行，只要输入的初始条件相同，则无论何时重复执行该程序都会得到相同的结果。

#### :pen\_fountain: 1.1.1、程序与进程

* **程序是指令和数据的集合，可以作为目标文件保存在磁盘中，或者作为段存放在内存地址空间中。**
* **进程是程序运行的一个具体的实例，程序总是运行在某个进程的上下文中。**

### :pen\_ballpoint: 1.2、作业

&#x20;作业就是用户一次请求计算机系统为用户完成任务所做工作的总和。从系统的角度，可以将作业视为**程序、数据和作业说明书**构成的整体，即：**作业程序** + **数据（作业体）**+ **作业说明书（作业控制语言编写）。**&#x5176;中，书写作业说明书的语言称为作业控制语言（JCL），是用户用于描述批处理作业处理过程控制意图的一种特殊程序，例如批处理文件。

> 批处理系统中，作业是抢占内存的基本单位，即以作业为单位将程序和数据调入内存

**作业说明书**：作业基本情况、作业控制、作业资源

* 作业基本情况：用户名、作业名、编程语言、最大处理时间等
* 作业控制描述：作业控制方式、作业步的操作顺序、作业执行出错处理
* 作业资源要求描述：处理时间、优先级、内存空间、外设类型和数量等

#### :pen\_fountain: 1.2.1、作业的建立

作业的建立指的是一个作业的全部程序和数据输入到外存且在系统中建立了相应的作业控制块（job control block——JCB），也就是说，作业的建立包括

* 作业的输入
* 作业控制块的建立

系统为每个作业建立了 JCB，登记该作业所要求的资源情况、预计执行时间和执行优先级&#x20;

**包括：**

* 作业名以及状态
* 资源要求
* 作业控制方式

**作用：**

* 作业的唯一标识
* 通过 JCB 对作业进行控制和管理

**JCB 的创建和撤销：**

* 在作业进入后备状态时，由作业注册程序建立
* 当作业执行完毕时，由作业终止程序撤销

#### :pen\_fountain: 1.2.2、作业的状态

* **提交**：作业由输入设备进入外存的过程
* **后备**：提交完成后，系统建立JCB，作为调度作业的依据，并将JCB 加入到后备作业队列
* **执行**：一个后备作业由作业调度程序选中并调入内存中，分配相应的资源后为其建立了相应的进程
* **完成**：当作业正常结束或因发生错误而终止时，作业进入完成状态

#### &#x20;:pen\_fountain: 1.2.3、**作业与进程**

一个进程是一个程序对某个数据集的执行过程，是分配资源的基本单位。作业是用户需要计算机完成的某项任务，是要求计算机所做工作的集合。一个作业的完成要经过作业提交、作业收容、作业执行和作业完成4个阶段。而进程是对已提交完毕的程序所执行过程的描述，是资源分配的基本单位。其主要区别如下：

1. 作业是用户向计算机提交任务的任务实体。在用户向计算机提交作业后，系统将它放入外存中的作业等待队列中等待执行。而进程则是完成用户任务的执行实体，是向系统申请分配资源的基本单位。任一进程，只要它被创建，总有相应的部分存在于内存中。
2. 一个作业可由多个进程组成，且必须至少由一个进程组成，反过来则不成立。
3. 作业的概念主要用在批处理系统中，像UNIX这样的分时系统中就没有作业的概念。而进程的概念则用在几乎所有的多道程序系统中。

注：作业与进程最主要的区别是：前者是由用户提交，后者是由系统自动生成；前者以用户任务为单位，后者是操作系统控制的单位。

### :pen\_ballpoint: 1.3、多道程序设计

为了提高计算机系统中各种资源的利用效率，缩短作业的周转时间，多种硬件资源能并行工作。

* 单CPU：并发程序按给定的时间片交替的在处理机上执行，其执行的时间是重叠的。
* 多CPU：这些并发程序在各自的处理机上运行。

**多道程序设计**就是允许多个程序同时进入内存并运行。**系统吞吐量**衡量系统效率的尺度。**吞吐量**是指单位时间内系统所处理的作业（程序）的道数（数量）。

> 如果系统的资源利用率高，则单位时间内所完成的有效工作多，吞吐量大。 如果系统的资源利用率低，则单位时间内所完成的有效工作少，吞吐量小。

**作用**：提高了设备资源利用率，内存资源利用率和处理机资源利用率，最终提高了系统吞吐量。

**特点：**

1. 独立性：每道程序都是在逻辑上独立的。
2. 随机性：程序和数据的输入与执行开始时间都是随机的。
3. 资源共享性：资源共享将导致对进程执行速度的制约。

#### :pen\_fountain: 1.3.1、程序的并发执行

**程序的并发执行**是指两个或两个以上的程序在计算机系统中同处于已开始执行的且尚未结束的状态。能够参与并发执行的程序称为**并发程序**。

**特性：**

1. **并发程序在执行期间具有相互制约关系**：“执行-暂停-执行”。
2. **程序与计算不再一一对应**：允许多个用户作业调用一个共享程序段。
3. **并发程序执行结果不再可现**：宏观上是同时进行的，在单CPU系统中，他们仍是顺序执行。

#### :pen\_fountain: 1.3.2、多道程序与进程模型

从系统允许多个程序同时进入CPU那一天开始，才有了进程的概念，它对CPU资源的抽象。我们把这种**多个程序同时运行在CPU的情况叫做多道程序**。举个例子，单一程序设计时，好比公交车上每次只能坐一个人，多道以后，就能坐多个人，有上有下。也是基于这样的设计思路，才有现在的各种貌似高端的技术。多道，跟中断，`DMA`，`SPOOLer`一并，被称为计算机操作系统发展史上里程碑一样的创造。

在进程模型中，所有计算机上运行的软件，通常也包括操作系统，被组织为若干顺序进程（sequential processes），简称为进程（process）。一个进程就是一个正在执行的程序的实例，进程也包括程序计数器、寄存器和变量的当前值。从概念上来说，每个进程都有各自的虚拟CPU，但是实际情况是CPU会在各个进程之间进行来回切换。

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG6FKqEoa8yJMlg6_VV%2F-MG6jbomsIf-anTvoeDB%2F10.jpg?alt=media\&token=c982d616-1e6e-4152-b721-2901223afc5b)

多道程序被抽象为拥有各自控制流程（即每个自己的程序计数器）的进程，并且每个程序都独立的运行。当然，实际上只有一个物理程序计数器，每个程序要运行时，其逻辑程序计数器装载到物理程序计数器中。当程序运行结束后，其物理程序计数器就会是真正的程序计数器，然后再把它放回进程的逻辑计数器中。在观察足够长的一段时间后，所有的进程都运行了，但在任何一个给定的瞬间仅有一个进程真正运行。

## :pencil2: 2、进程状态

### :pen\_ballpoint: 2.1、进程状态转换

&#x20;**在一个进程的活动期间至少具备三种基本状态，即运行状态、就绪状态、阻塞状态。**

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG6yc7d71gWqxt980ce%2F-MG71HYKVZb7btxpBQpd%2F4.jpg?alt=media\&token=96611b02-f326-4360-8473-57ba0af56e7d)

上图中各个状态的意义：

* 运行状态（*Runing*）：该时刻进程占用 CPU；
* 就绪状态（*Ready*）：可运行，但因为其他进程正在运行而暂停停止；
* 阻塞状态（*Blocked*）：该进程正在等待某一事件发生（如等待输入/输出操作的完成）而暂时停止运行，这时，即使给它CPU控制权，它也无法运行；

当然，进程另外两个基本状态：

* 创建状态（*new*）：进程正在被创建时的状态；
* 结束状态（*Exit*）：进程正在从系统中消失时的状态；

于是，一个完整的进程状态的变迁如下图：

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG6yc7d71gWqxt980ce%2F-MG71ZS_VWHFeVS8vpu8%2F5.jpg?alt=media\&token=a5a0a953-4976-4025-b9db-d14e2dc845b3)

进程的状态变迁：

* *NULL -> 创建状态*：一个新进程被创建时的第一个状态；
* *创建状态 -> 就绪状态*：当进程被创建完成并初始化后，一切就绪准备运行时，变为就绪状态，这个过程是很快的；
* *就绪态 -> 运行状态*：处于就绪状态的进程被操作系统的进程调度器选中后，就分配给 CPU 正式运行该进程；
* *运行状态 -> 结束状态*：当进程已经运行完成或出错时，会被操作系统作结束状态处理；
* *运行状态 -> 就绪状态*：处于运行状态的进程在运行过程中，由于分配给它的运行时间片用完，操作系统会把该进程变为就绪态，接着从就绪态选中另外一个进程运行；
* *运行状态 -> 阻塞状态*：当进程请求某个事件且必须等待时，例如请求 I/O 事件；
* *阻塞状态 -> 就绪状态*：当进程要等待的事件完成时，它从阻塞状态变到就绪状态；

### :pen\_ballpoint: 2.2、进程挂起

另外，还有一个状态叫**挂起状态**，它表示进程没有占有物理内存空间。这跟阻塞状态是不一样，阻塞状态是等待某个事件的返回。

由于虚拟内存管理原因，进程的所使用的空间可能并没有映射到物理内存，而是在硬盘上，这时进程就会出现挂起状态，另外调用 sleep 也会被挂起。挂起状态可以分为两种：

* 阻塞挂起状态：进程在外存（硬盘）并等待某个事件的出现；
* 就绪挂起状态：进程在外存（硬盘），但只要进入内存，即刻立刻运行；

这两种挂起状态加上前面的五种状态，就变成了七种状态变迁，见如下图：

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG7HmhT6fovGSlHI_p5%2F-MG8D2GJQ6fYQoGgWJXV%2F6.jpg?alt=media\&token=0fa6a7bf-a94c-4a9c-85de-7444dde82b55)

新加入的几个状态转化的步骤如下：

1. **运行状态->就绪挂起状态**：这里发生在客户在程序正在运行是直接挂起程序。注意这里的箭头是单向的，所以在就绪挂起状态结束以后实际上是执行激活步骤，进入就绪状态，等待处理机调度。&#x20;
2. **阻塞状态->阻塞挂起状态**：当内存空间比较紧缺的时候，如果有存在内存中的，而且是处于阻塞状态的进程，那么就让他更需要内存的程序占用内存，自己进入阻塞挂起状态，PCB等数据存入外存。因为现在这个进程也不能进入就绪状态，这个程序在内存中是没有什么作用的。
3. **阻塞挂起状态->就绪挂起状态**：当阻塞状态等待的IO事件或其他事件到来的时候状态发生改变。&#x20;
4. **就绪挂起状态->就绪状态**：如果内存中没有就绪态进程，操作系统需要调入一个进程继续执行。此外，当处于就绪/挂起状态的进程比处于就绪态的任何进程的优先级都要高时，也可以进行这种转换。这种情况的产生是由于操作系统设计者规定，调入高优先级的进程比减少交换量更重要。&#x20;
5. **就绪状态->就绪挂起状态**：通常，操作系统更倾向于挂起阻塞态进程而不是就绪态进程，因为就绪态进程可以立即执行，而阻塞态进程占用了内存空间但不能执行。但如果释放内存以得到足够空间的唯一方法是挂起一个就绪态进程，那么这种转换也是必需的。并且，如果操作系统确信高优先级的阻塞态进程很快就会就绪，那么它可能选择挂起一个低优先级的就绪态进程，而不是一个高优先级的阻塞态进程。

进程挂起既可以是主动的，也可以是被动的，即操作系统因为某些原因使得进程挂起。总而言之引入挂起状态的原因有以下几种：

1. 用户的请求：可能是在程序运行期间发现了可疑的问题，需要暂停进程。&#x20;
2. 父进程的请求：考察，协调，或修改子进程。&#x20;
3. 操作系统的需要：对运行中资源的使用情况进行检查和记账。&#x20;
4. 负载调节的需要：有一些实时的任务非常重要，需要得到充足的内存空间，这个时候我们需要把非实时的任务进行挂起，优先使得实时任务执行。&#x20;
5. 定时任务：一个进程可能会周期性的执行某个任务，那么在一次执行完毕后挂起而不是阻塞，这样可以节省内存。&#x20;
6. 安全：系统有时可能会出现故障或者某些功能受到破坏，这是就需要将系统中正在进行的进程进行挂起，当系统故障消除以后，对进程的状态进行恢复。&#x20;

### :pen\_ballpoint: 2.3、挂起和阻塞的区别

1. **是否释放CPU：**&#x963B;塞（pend）就是任务释放CPU，其他任务可以运行，一般在等待某种资源或信号量的时候出现。挂起（suspend）不释放CPU，如果任务优先级高就永远轮不到其他任务运行。一般挂起用于程序调试中的条件中断，当出现某个条件的情况下挂起，然后进行单步调试。
2. **是否主动：**&#x663E;然阻塞是一种**被动行为**，其发生在**磁盘，网络IO，wait，lock**等要等待某种事件的发生的操作之后。因为拿不到IO资源，所以阻塞时会放弃 CPU的占用。而挂起是主动的，因为挂起后还要受到CPU的监督（等待着激活），所以挂起不释放CPU，比如sleep函数，占着CPU不使用。
3. **与调度器是否相关：**&#x4EFB;务调度是操作系统来实现的，任务调度时，直接忽略挂起状态的任务，但是会顾及处于pend下的任务，当pend下的任务等待的资源就绪后，就可以转为ready了。ready只需要等待CPU时间，当然，任务调度也占用开销，但是不大，可以忽略。可以这样理解，只要是挂起状态，操作系统就不再管理这个任务了。

## :pencil2: 3、进程的控制结构

&#x20;在操作系统中，是用**进程控制块**（*process control block，PCB*）数据结构来描述进程的。 **PCB 是进程存在的唯一标识**，这意味着一个进程的存在，必然会有一个 PCB，如果进程消失了，那么 PCB 也会随之消失。

### :pen\_ballpoint: 3.1、PCB信息

**进程描述信息：**

* 进程标识符：标识各个进程，每个进程都有一个并且唯一的标识符；
* 用户标识符：进程归属的用户，用户标识符主要为共享和保护服务；

**进程控制和管理信息：**

* 进程当前状态，如 new、ready、running、waiting 或 blocked 等；
* 进程优先级：进程抢占 CPU 时的优先级；

**资源分配清单：**

* 有关内存地址空间或虚拟地址空间的信息，所打开文件的列表和所使用的 I/O 设备信息。

**CPU 相关信息：**

* CPU 中各个寄存器的值，当进程被切换时，CPU 的状态信息都会被保存在相应的 PCB 中，以便进程重新执行时，能从断点处继续执行。

### :pen\_ballpoint: 3.2、PCB组织

通常是通过**链表**的方式进行组织，把具有**相同状态的进程链在一起，组成各种队列**。比如：

* 将所有处于就绪状态的进程链在一起，称为**就绪队列**；
* 把所有因等待某事件而处于等待状态的进程链在一起就组成各种**阻塞队列**；
* 另外，对于运行队列在单核 CPU 系统中则只有一个运行指针了，因为单核 CPU 在某个时间，只能运行一个程序。

那么，就绪队列和阻塞队列链表的组织形式如下图：

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG9Ep8P3zm6dXWBUbbl%2F-MG9FgM1KUmuXxHzZQqV%2F14.jpg?alt=media\&token=c3c1ca95-17a1-46bc-8aa5-5946c48e71c5)

除了链接的组织方式，还有索引方式，它的工作原理：将同一状态的进程组织在一个索引表中，索引表项指向相应的 PCB，不同状态对应不同的索引表。

一般会选择链表，因为可能面临进程创建，销毁等调度导致进程状态发生变化，所以链表能够更加灵活的插入和删除。

## :pencil2: 4、进程控制

### :pen\_ballpoint: 4.1、**创建进程**

操作系统允许一个进程创建另一个进程，而且允许子进程继承父进程所拥有的资源，当子进程被终止时，其在父进程处继承的资源应当还给父进程。同时，终止父进程时同时也会终止其所有的子进程。

创建进程的过程如下：

* 为新进程分配一个唯一的进程标识号，并申请一个空白的 PCB，PCB 是有限的，若申请失败则创建失败；
* 为进程分配资源，此处如果资源不足，进程就会进入等待状态，以等待资源；
* 初始化 PCB；
* 如果进程的调度队列能够接纳新进程，那就将进程插入到就绪队列，等待被调度运行；

### :pen\_ballpoint: 4.2、**终止进程**

进程可以有 3 种终止方式：正常结束、异常结束以及外界干预（信号 `kill` 掉）。

终止进程的过程如下：

* 查找需要终止的进程的 PCB；
* 如果处于执行状态，则立即终止该进程的执行，然后将 CPU 资源分配给其他进程；
* 如果其还有子进程，则应将其所有子进程终止；
* 将该进程所拥有的全部资源都归还给父进程或操作系统；
* 将其从 PCB 所在队列中删除；

### :pen\_ballpoint: 4.3、**阻塞进程**

当进程需要等待某一事件完成时，它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待，它只能由另一个进程唤醒。

阻塞进程的过程如下：

* 找到将要被阻塞进程标识号对应的 PCB；
* 如果该进程为运行状态，则保护其现场，将其状态转为阻塞状态，停止运行；
* 将该 PCB 插入的阻塞队列中去；

### :pen\_ballpoint: 4.4、**唤醒进程**

进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成，所以处于阻塞状态的进程是绝对不可能叫醒自己的。

如果某进程正在等待 I/O 事件，需由别的进程发消息给它，则只有当该进程所期待的事件出现时，才由发现者进程用唤醒语句叫醒它。

唤醒进程的过程如下：

* 在该事件的阻塞队列中找到相应进程的 PCB；
* 将其从阻塞队列中移出，并置其状态为就绪状态；
* 把该 PCB 插入到就绪队列中，等待调度程序调度；

进程的阻塞和唤醒是一对功能相反的语句，如果某个进程调用了阻塞语句，则必有一个与之对应的唤醒语句。

## :pencil2: 5、进程的上下文切换

&#x20;各个进程之间是共享 CPU 资源的，在不同的时候进程之间需要切换，让不同的进程可以在 CPU 执行，那么这个**一个进程切换到另一个进程运行，称为进程的上下文切换**。

### :pen\_ballpoint: 5.1、CPU上下文切换

大多数操作系统都是多任务，通常支持大于 CPU 数量的任务同时运行。实际上，这些任务并不是同时运行的，只是因为系统在很短的时间内，让各个任务分别在 CPU 运行，于是就造成同时运行的错觉。

任务是交给 CPU 运行的，那么在每个任务运行前，CPU 需要知道任务从哪里加载，又从哪里开始运行。所以，操作系统需要事先帮 CPU 设置好 **CPU 寄存器和程序计数器**。

CPU 寄存器是 CPU 内部一个容量小，但是速度极快的内存（缓存）。程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。所以说，CPU 寄存器和程序计数是 CPU 在运行任何任务前，所必须依赖的环境，这些环境就叫做 **CPU 上下文**。

既然知道了什么是 CPU 上下文，那理解 CPU 上下文切换就不难了。CPU 上下文切换就是先把前一个任务的 CPU 上下文（CPU 寄存器和程序计数器）保存起来，然后加载新任务的上下文到这些寄存器和程序计数器，最后再跳转到程序计数器所指的新位置，运行新任务。

系统内核会存储保持下来的上下文信息，当此任务再次被分配给 CPU 运行时，CPU 会重新加载这些上下文，这样就能保证任务原来的状态不受影响，让任务看起来还是连续运行。

上面说到所谓的「任务」，主要包含进程、线程和中断。所以，可以根据任务的不同，把 CPU 上下文切换分成：**进程上下文切换、线程上下文切换和中断上下文切换**。

### :pen\_ballpoint: 5.2、进程上下文切换

进程是由内核管理和调度的，所以进程的切换只能发生在内核态。所以，**进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源，还包括了内核堆栈、寄存器等内核空间的资源。**&#x901A;常，会把交换的信息保存在进程的 PCB，当要运行另外一个进程的时候，我们需要从这个进程的 PCB 取出上下文，然后恢复到 CPU 中，这使得这个进程可以继续执行，如下图所示：

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MG9Ep8P3zm6dXWBUbbl%2F-MG9I4ALhawVL4hcQ0iu%2F13.jpg?alt=media\&token=1af3f936-a448-4917-b3b1-3a1ca9cecb29)

需要注意，进程的上下文开销是很关键的，我们希望它的开销越小越好，这样可以使得进程可以把更多时间花费在执行程序上，而不是耗费在上下文切换。

> 发生进程上下文切换有哪些场景？
>
> * 为了保证所有进程可以得到公平调度，CPU 时间被划分为一段段的时间片，这些时间片再被轮流分配给各个进程。这样，当某个进程的时间片耗尽了，就会被系统挂起，切换到其它正在等待 CPU 的进程运行；
> * 进程在系统资源不足（比如内存不足）时，要等到资源满足后才可以运行，这个时候进程也会被挂起，并由系统调度其他进程运行；
> * 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时，自然也会重新调度；
> * 当有优先级更高的进程运行时，为了保证高优先级进程的运行，当前进程会被挂起，由高优先级进程来运行；
> * 发生硬件中断时，CPU 上的进程会被中断挂起，转而执行内核中的中断服务程序；
