# 进程管理

## **计算机操作系统 - 进程管理**

## 进程与线程

一个标准的线程由线程ID，当前指令指针PC，寄存器和堆栈组

成。而进程由内存空间(代码，数据，进程空间，打开的文件)和一个或多个线程组成。

### 1. 进程

<https://blog.csdn.net/cwxxiayi/article/details/80288850>

进程是资源分配的基本单位。

进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态，所谓的创建进程和撤销进程，都是指对 PCB 的操作。

下图显示了 4 个程序创建了 4 个进程，这 4 个进程可以并发地执行。

![](/files/-Lq7QSUYRKRGFRa3DMdQ)

### 2. 线程

线程是独立调度的基本单位。

一个进程中可以有多个线程，它们共享进程资源。

QQ 和浏览器是两个进程，浏览器进程里面有很多线程，例如 HTTP 请求线程、事件响应线程、渲染线程等等，线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时，浏览器还可以响应用户的其它事件。

![](/files/-Lq7QSUc9DU8hxtGhTJF)

### 3. 区别

Ⅰ 拥有资源

进程是资源分配的基本单位，但是线程不拥有资源，线程可以访问隶属进程的资源。

Ⅱ 调度

线程是独立调度的基本单位，在同一进程中，线程的切换不会引起进程切换，从一个进程中的线程切换到另一个进程中的线程时，会引起进程切换。

Ⅲ 系统开销

由于创建或撤销进程时，系统都要为之分配或回收资源，如内存空间、I/O 设备等，所付出的开销远大于创建或撤销线程时的开销。类似地，在进行进程切换时，涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置，而线程切换时只需保存和设置少量寄存器内容，开销很小。

Ⅳ 通信方面

线程间可以通过直接读写同一进程中的数据进行通信，但是进程通信需要借助 IPC（Inter-Process Communication，进程间通信）。

## 进程状态的切换

![](/files/-Lq7QSUexrNnUrtx0vQ3)

* 就绪状态（ready）：等待被调度
* 运行状态（running）
* 阻塞状态（waiting）：等待资源

应该注意以下内容：

* **只有就绪态和运行态可以相互转换，其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间，转为运行状态；而运行状态的进程，在分配给它的 CPU 时间片用完之后就会转为就绪状态，等待下一次调度**。
* **阻塞状态是缺少需要的资源从而由运行状态转换而来，但是该资源不包括 CPU 时间，缺少 CPU 时间会从运行态转换为就绪态**。

## 进程调度算法

不同环境的调度算法目标不同，因此需要针对不同环境来讨论调度算法。

### 1. 批处理系统

批处理系统没有太多的用户操作，在该系统中，调度算法目标是保证吞吐量和周转时间（从提交到终止的时间）。

**1.1 先来先服务 first-come first-serverd（FCFS）**

非抢占式的调度算法，按照请求的顺序进行调度。

有利于长作业，但不利于短作业，因为短作业必须一直等待前面的长作业执行完毕才能执行，而长作业又需要执行很长时间，造成了短作业等待时间过长。

**1.2 短作业优先 shortest job first（SJF）**

非抢占式的调度算法，按估计运行时间最短的顺序进行调度。

长作业有可能会饿死，处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来，那么长作业永远得不到调度。

**1.3 最短剩余时间优先 shortest remaining time next（SRTN）**

最短作业优先的抢占式版本，按剩余运行时间的顺序进行调度。 当一个新的作业到达时，其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少，则挂起当前进程，运行新的进程。否则新的进程等待。

### 2. 交互式系统

交互式系统有大量的用户交互操作，在该系统中调度算法的目标是快速地进行响应。

**2.1 时间片轮转**

将所有就绪进程按 FCFS 的原则排成一个队列，每次调度时，把 CPU 时间分配给队首进程，该进程可以执行一个时间片。当时间片用完时，由计时器发出时钟中断，调度程序便停止该进程的执行，并将它送往就绪队列的末尾，同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系：

* 因为进程切换都要保存进程的信息并且载入新进程的信息，如果时间片太小，会导致进程切换得太频繁，在进程切换上就会花过多时间。
* 而如果时间片过长，那么实时性就不能得到保证。

![](/files/-Lq7QSUgJ7Q0f3IcZP7w)

**2.2 优先级调度**

为每个进程分配一个优先级，按优先级进行调度。

为了防止低优先级的进程永远等不到调度，可以随着时间的推移增加等待进程的优先级。

**2.3 多级反馈队列**

一个进程需要执行 100 个时间片，如果采用时间片轮转调度算法，那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑，它设置了多个队列，每个队列时间片大小都不同，例如 1,2,4,8,..。进程在第一个队列没执行完，就会被移到下一个队列。这种方式下，之前的进程只需要交换 7 次。

每个队列优先权也不同，最上面的优先权最高。因此只有上一个队列没有进程在排队，才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

![](/files/-Lq7QSUiFaEb5fZuoVHW)

### 3. 实时系统

实时系统要求一个请求在一个确定时间内得到响应。

分为硬实时和软实时，前者必须满足绝对的截止时间，后者可以容忍一定的超时。

## 进程同步

### 1. 临界区

对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源，每个进程在进入临界区之前，需要先进行检查。

```
// entry section
// critical section;
// exit section
```

### 2. 同步与互斥

* 同步：多个进程因为合作产生的直接制约关系，使得进程有一定的先后执行关系。
* 互斥：多个进程在同一时刻只有一个进程能进入临界区。

### 3. 信号量

信号量（Semaphore）是一个整型变量，可以对其执行 down 和 up 操作，也就是常见的 P 和 V 操作。

* **down** : 如果信号量大于 0 ，执行 -1 操作；如果信号量等于 0，进程睡眠，等待信号量大于 0；
* **up** ：对信号量执行 +1 操作，唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语，不可分割，通常的做法是在执行这些操作的时候屏蔽中断。

如果信号量的取值只能为 0 或者 1，那么就成为了 **互斥量（Mutex）** ，0 表示临界区已经加锁，1 表示临界区解锁。

使用最广泛的信号量为二元信号量，它控制单个资源，对于这种信号量而言，它只有两种合法值： 0 和 1 ，对应一个可用的资源。若当前有资源可用，则与之对应的二值信号量的值为 1 ；若资源已被占用，则与之对应的二值信号量的值为 0 。当进程申请资源时，如果当前信号量的值为 0 ，那么进程会陷入阻塞，直到有其他进程释放资源，将信号量的值加 1 才能被唤醒。

**使用信号量实现生产者-消费者问题**

问题描述：使用一个缓冲区来保存物品，只有缓冲区没有满，生产者才可以放入物品；只有缓冲区不为空，消费者才可以拿走物品。

因为缓冲区属于临界资源，因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为，需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计，这里需要使用两个信号量：empty 记录空缓冲区的数量，full 记录满缓冲区的数量。其中，empty 信号量是在生产者进程中使用，当 empty 不为 0 时，生产者才可以放入物品；full 信号量是在消费者进程中使用，当 full 信号量不为 0 时，消费者才可以取走物品。

注意，**不能先对缓冲区进行加锁，再测试信号量**。也就是说，不能先执行 down(mutex) 再执行 down(empty)。如果这么做了，那么可能会出现这种情况：生产者对缓冲区加锁后，执行 down(empty) 操作，发现 empty = 0，此时生产者睡眠。消费者不能进入临界区，因为生产者对缓冲区加锁了，消费者就无法执行 up(empty) 操作，empty 永远都为 0，导致生产者永远等待下，不会释放锁，消费者因此也会永远等待下去。

### 4. 管程

使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制，而管程把控制的代码独立出来，不仅不容易出错，也使得客户端代码调用更容易。

c 语言不支持管程，下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法，客户端代码通过调用这两个方法来解决生产者-消费者问题。

管程有一个重要特性：在**一个时刻只能有一个进程使用管程**。进程在无法继续执行的时候不能一直占用管程，否则其它进程永远不能使用管程。

管程引入了 **条件变量** 以及相关的操作：**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 **wait() 操作会导致调用进程阻塞，把管程让出来给另一个进程持有**。**signal() 操作用于唤醒被阻塞的进程**。

## 经典同步问题

生产者和消费者问题前面已经讨论过了。

### 1. 读者-写者问题

允许多个进程同时对数据进行读操作，但是不允许读和写以及写和写操作同时发生。

一个整型变量 count 记录在对数据进行读操作的进程数量，一个互斥量 count\_mutex 用于对 count 加锁，一个互斥量 data\_mutex 用于对读写的数据加锁。

## 进程通信

> 进程间通信（IPC，InterProcess Communication）是指在不同进程之间传播或交换信息。
>
> IPC的方式通常有管道（包括匿名管道和命名管道）、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

<https://blog.csdn.net/weixin_39763552/article/details/80306734>

<https://www.cnblogs.com/joker-wz/p/11000489.html>

进程同步与进程通信很容易混淆，它们的区别在于：

* 进程同步：控制多个进程**按一定顺序执行**；
* s进程通信：进程间传输信息。

进程通信是一种手段，而进程同步是一种目的。也可以说，为了能够达到进程同步的目的，需要让进程进行通信，传输一些进程同步所需要的信息。

### 1. 管道

管道，通常指**匿名管道**，是 UNIX 系统IPC最古老的形式。

> 都知道在Linux下“一切皆文件”，其实这里的管道就是一个文件。
>
> 管道实现进程通信就是让两个进程都能访问该文件。
>
> 通讯的文件是一种不属于文件系统仅存在内存中的“伪文件”。
>
> 管道的通信方式为：写端每次都将数据写入管道缓冲区的 **末尾** ，而读端每次都从管道缓冲区的 **头部** 读出数据。

它具有以下限制：

> 1.只支持半双工通信（**单向交替传输**），也就是说，两个进程都能访问这个文件，假设进程1往文件内写东西，那么进程2 就只能读取文件的内容。
>
> 2.只能用于具有血缘关系的进程间通信，通常用于父子进程建通信
>
> 3.基于字节流来通信

### 2. FIFO

也称为命名管道，去除了管道只能在父子进程中使用的限制。

> 1.与管道的区别：提供了一个路径名与之关联，以FIFO文件的形式存储于文件系统中，能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的，它仅限于在父子进程之间的通信。
>
> 2.FIFO是一个设备文件，在文件系统中以文件名的形式存在，因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信，前提是可以访问该路径
>
> 3.FIFO(first input first output)总是遵循先进先出的原则，即第一个进来的数据会第一个被读走。

FIFO 常用于客户-服务器应用程序中，FIFO 用作汇聚点，在客户进程和服务器进程之间传递数据。

![](/files/-Lq7QSV1A2VeUSTwztnr)

### 3. 消息队列

> 消息队列是存放在内核中的消息链表，每个消息队列由消息队列标识符表示。
>
> **与管道（匿名管道：只存在于内存中的文件；命名管道：存在于实际的磁盘介质或者文件系统）不同的是消息队列存放在内核中，只有在内核重启(即，操作系统重启)或者显示地删除一个消息队列时，该消息队列才会被真正的删除。**
>
> 消息队列在某个进程往一个队列写入消息之前，并不需要另外某个进程在该队列上等待消息的到达

相比于 FIFO，消息队列具有以下优点：

* 消息队列可以独立于读写进程存在，从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难；
* 避免了 FIFO 的同步阻塞问题，不需要进程自己提供同步方法；
* 读进程可以根据消息类型有选择地接收消息，而不像 FIFO 那样只能默认地接收。

消息队列特点总结：

> 1. 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
> 2. 消息队列允许一个或多个进程向它写入与读取消息
> 3. 管道和消息队列的通信数据都是先进先出的原则。
> 4. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
> 5. 消息队列克服了信号承载信息量少，管道只能承载无格式字 节流以及缓冲区大小受限等缺。
> 6. 目前主要有两种类型的消息队列：POSIX消息队列以及System V消息队列，System V消息队列目前被大量使用。System V消息队列是随内核持续的，只有在内核重起或者人工删除时，该消息队列才会被删除。

对于系统中的每个消息队列，内核维护一个定义在`<sys/msg.h>`头文件中的信息结构。

```c
struct msqid_ds {
    struct ipc_perm msg_perm ; 
    struct msg*    msg_first ; //指向队列中的第一个消息
    struct msg*    msg_last ; //指向队列中的最后一个消息
    ……
} ;
```

#### msgget函数

调用的第一个函数通常是`msgget`，其功能是打开一个现存队列或创建一个新队列。

```c
#include <sys/msg.h>
int  msgget (key_t key,  int oflag) ;
```

#### msgsnd函数

使用`msgsnd`打开一个消息队列后，我们使用`msgsnd`往其上放置一个消息。

```c
#include <sys/msg.h>
int  msgsnd (int msqid,  const void *ptr,  size_t length,  int flag) ;
```

其中msqid是由msgget返回的标识符。ptr是一个结构指针，该结构具有如下模板(我们需要按这个模板自己定义结构体)

```c
struct mymesg {
    long  mtype ;     //消息类型(大于0)
    char  mtext[512] ;  //消息数据
} ;
//结构体的名字和其中变量名都由我们自己确定，我们只要按照这个模板定义即可。
```

消息数据mtext中，任何形式的数据都是允许的，无论是二进制数据还是文本，内核根本不解释消息数据的内容。

#### msgrcv函数

使用msgrcv函数从某个消息队列中读出一个消息。

```
#include <sys/msg.h>
ssize_t  msgrcv (int msqid,  void* ptr,  size_t length,  long type,  int flag) ;
```

参数ptr指定所接收消息的存放位置。参数length指定了数据部分大小(只想要多长的数据)

参数type指定希望从队列中读出什么样的消息。

type == 0 返回队列中的第一个消息

type > 0 返回队列中消息类型为type的第一个消息

type < 0 返回队列中消息类型值小于或等于type绝对值的消息，如果这种消息有若干个。则取类型值最小的消息。

### 4.信号量

> 信号是Linux系统中用于进程间互相通信或者操作的一种机制，信号可以在任何时候发给某一进程，而无需知道该进程的状态。
>
> 如果该进程当前并未处于执行状态，则该信号就有内核保存起来，直到该进程回复执行并传递给它为止。

Linux系统中常用信号：

> 1. SIGHUP：用户从终端注销，所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
> 2. SIGINT：程序终止信号。程序运行过程中，按`Ctrl+C`键将产生该信号。
> 3. SIGQUIT：程序退出信号。程序运行过程中，按`Ctrl+\`键将产生该信号。
> 4. SIGBUS和SIGSEGV：进程访问非法地址。
> 5. SIGFPE：运算中出现致命错误，如除零操作、数据溢出等。
> 6. SIGKILL：用户终止进程执行信号。shell下执行`kill -9`发送该信号。
> 7. SIGTERM：结束进程信号。shell下执行`kill 进程pid`发送该信号。
> 8. SIGALRM：定时器信号。
> 9. SIGCLD：子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号，则子进程退出后将形成僵尸进程。
>
>    可以使用 `kill -l` 查看当前系统可用信号有哪些

它是一个计数器，用于为多个进程提供对共享数据对象的访问。

### 5. 共享存储

允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制，所以这是最快的一种 IPC。

需要使用信号量用来同步对共享存储的访问。

多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件，而是使用内存的匿名段。

### 6. 套接字

与其它通信机制不同的是，它可用于不同机器间的进程通信。

### 化简资源分配图

### 方法步骤

* 第一步：先看系统还剩下多少资源没分配，再看有哪些进程是不阻塞（“不阻塞”即：系统有足够的空闲资源分配给它）的
* 第二步：把不阻塞的进程的所有边都去掉，形成一个孤立的点，再把系统分配给这个进程的资源回收回来
* 第三步：看剩下的进程有哪些是不阻塞的，然后又把它们逐个变成孤立的点。
* 第四步：最后，所有的资源和进程都变成孤立的点。这样的图就叫做“可完全简化”。

如果一个图可完全简化，则不会产生死锁；如果一个图不可完全简化（即：图中还有“边”存在），则会产生死锁。这就是“死锁定理”。

![](/files/-Lq7QSV3_P6A3irjuR77)

解析：

R1有两个资源，一个分配给了P1,,一个分配给了P3，此时P2申请R1的资源，因为R1此时没有可用资源，P2堵塞。

R2有三个资源，已经给P1,P2,P3，各自分配了一个资源，而P1此时又再次申请资源R2,P1堵塞

R3有两个资源，已经分配给P2一个，P2申请一个资源，分配给它，所以P3是非阻塞结点

化简的话，看从没有阻塞的结点开始，删去P3周围所有的bian边，使其成为一个孤立的点，然后看剩下的资源按上述步骤再次进行分配，若到最后只剩下一群孤立的点，则说明该资源图是可以化简的。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://im-qianuxn.gitbook.io/pytorch/ji-suan-ji/interview/2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
