%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 什么是进程?
**程序** 是**机器指令和静态数据**的集合,而 "**进程**" 是**一个运行中的程序实例**。
"**运行中的程序**" 即称之为 "**进程**"——存储在硬盘上的**二进制可执行文件**,**被装载到内存中运行**、**由 CPU 执行其中的指令**。
> [!NOTE] Linux 下的进程启动与终止
>
> - 内核 **使一个程序运行** 的 **唯一方法** 是调用一个 **`exec` 函数**;
> - 进程 **主动终止** 的方式是显式或隐式(通过 `exit`)地**调用 `_exit` 或 `_Exit`**,或者调用 `abort()` 表明 "**异常终止**"。
> - 进程 **被动终止** 的方式是收到一个 "**终止进程**" 的信号,例如 `SIGKILL`。
<br>
# 进程状态
进程包括以下状态:
- (1)**新建**(New):进程正在被创建 & 分配资源,尚未加入就绪队列。
- (2)**就绪**(Ready):进程位于 "**就绪队列**",等待被调度**获取 CPU 时间片**后即可执行。
- (3)**运行**(Running):进程 **正占用 CPU 执行其指令**。
- (4)**阻塞/等待**(Blocked/Waiting):进程正在等待某个事件完成,例如等待 I/O 操作结束、等待某个信号量等。
- (5)**终止**(Exit):进程已经完成运行或被终止,等待其父进程或 OS 内核回收其资源。
- (6)**==挂起==**(Suspended):Linux 下,进程收到 `SIGSTOP`、`SIGTSTP`、`SIGTTIN`、`SIGTTOU` 信号时**被挂起**,且不会被调度,**停止直至收到 `SIGCONT` 信号**。
> [!NOTE] 进程状态变迁图[^1]
>
> 下图中,将 "**挂起**" 状态细分为了 "**就绪挂起**" 与 "**阻塞挂起**"。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-E8E2034B5583A9A5EDDAE4CA47F776AF.png|678]]
>
<br><br>
# Linux 中的任务
Linux 内核中,进程与线程都通过**结构体 `task_struct`** 表示,统称为 "**==任务==**" 。
(**进程控制块 PCB**、**线程控制块 TCB** 在实现上都是用的 `task_struct` 表示)
内核中 **调度器的调度对象** 即是 `task_struct`。
> [!NOTE] 线程的 `task_struct` 结构体中**共享进程已创建的资源**(虚拟内存空间,代码段、文件描述符等)
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-DD39F0C9D497332D10784D0BBC362243.png|524]]
## Linux 任务状态
任务状态由 **`task_struct` 结构体中的 `state` 字段**描述,进程与线程都具有独立的任务状态。
例如, `top -H` 或 `ps -eLF` 可查看线程信息,**每个线程都有自己的 TID 和任务状态**。
主状态码:
| 状态标记 | 含义 | 说明 | 备注 |
| ---- | ------------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| R | Runnable | 正在运行或**正在就绪队列中等待调度** | |
| S | Interruptible Sleeping | **可中断**休眠状态(**可被信号中断,唤醒**) | 阻塞 I/O 下,进程阻塞时就处于该状态 |
| D | UnInterruptible Sleeping | **不可中断**休眠状态(无法被任何信号唤醒,直至等待事件完成) | |
| T | Stopped | 收到 **暂停信号(19 or 20)** 而暂停,仅当收到 **SIGCONT(18) 信号** 才会恢复执行 | - 收到 `SIGSTOP(19)` 或 `SIGTSTP(20)` 信号暂停<br>- 收到 `SIGCONT(18)` 信号继续执行(例如 `fg`)<br> |
| t | Traced | **被附加调试**而暂停 | 例如被 `gdb -p <pid>` 附加调试时 |
| Z | Zombie | **僵尸进程**(进程已终止,但未被父进程回收) | |
| X | Dead | **已终止**(用于内核回收时标记进程已销毁,用户空间通常不会看见该状态) | |
| I | Idle | 空闲(内核线程特有) | |
状态修饰符:
| 修饰符 | 含义 |
| ----- | ------------------------------------- |
| s(小写) | **会话首进程**(**session leader**),例如 bash |
| + | 位于**前台进程组** |
| l | 多线程(内核线程组) |
| L | **有页面锁定**(real-time memory locking) |
| < | 高优先级 |
| N | 低优先级(nice 值为正) |
| n | 低优先级任务 |
> [!example] 常见组合状态码示意
>
> 例如,常见组合状态码含义如下:
>
> - `Ss`:休眠中的 bash;
> - `Ss+`:休眠中的前台 bash;
> - `R+`:正在运行中的前台进程;
> - `Sl`:休眠 + 多线程;
> - `Sl+`:休眠 + 多线程 + 前台进程;
>
> ![[_attachment/02-开发笔记/11-Linux/Linux 命令行/Linux-进程相关命令.assets/IMG-Linux-进程相关命令-A49E51736CF27788290E0DE2B9FDD59F.png|965]]
>
>
> [!info] 僵尸进程(zombie)[^2] [^3]
>
> 僵尸进程:该**子进程已终止**,但其**父进程尚未通过 `wait()` 或 `waitpid()` 回收其退出信息**,故其 **任务控制块 `task_struct`** 仍残留于内核中。
>
> 由于进程终止后**内核会释放掉其绝大部分资源**(页表与内存映射、文件描述符),
> 故僵尸进程**几乎不占用资源**,但 `task_struct` 中仍保留有 **退出状态码** `exit_code`、**进程使用的 CPU 时间总量**等信息,且**仍==占用一个 PID==**。
>
> 由于僵尸进程已终止,故**无法被 `kill -9` 再清除**,只能等待其父进程通过 `wait()` 或 `waitpid()` 回收。
>
> [!info] 会话首进程
>
> Linux 中一个 "**会话(Session)**" 是一个或多个进程的集合,其共享同一个 **会话 ID(==SID==)**。
>
> 通常,**一个用户每==登录一次==就创建==一个会话==**(例如通过 `ssh`、终端、shell)。
>
> 一个会话只有**一个==会话首进程==(session leader)**,通常即为:
>
> - **登录 shell,例如 bash**;
> - `sshd` 的子进程
> - 守护进程启动时的初始进程(指尚未脱离 session 时;守护进程通常要脱离原来的 session,从而不受控制终端的信号影响)
>
> 会话首进程可以打开一个 "**控制终端**",即控制着终端设备,例如 `/dev/pts/0`。
> [!NOTE] 任务的 `T` 与 `t` 状态
>
> - T(Stopped):当直接向进程发送 `SIGSTOP(19)` 或 `SIGTSTP(20)` 信号时,**进程默认进入 `TASK_STOPPED` 状态而暂停**。
> - t(Traced):**进程被 `ptrace` 系统调用附加调试而暂停**
>
> 当通过 **`ptrace(PTRACE_ATTACH, pid, ...)` 系统调用**附加调试进程时,该系统调用的操作为:
>
> 1. 为目标进程设置 `ptrace` 标志位(`tast_struct` 中)
> 2. 向目标进程发送 `SIGSTOP(19)` 信号;
>
> 由此,目标进程会**进入 `TASK_STOPPED` 状态**并且**被标记为"==正被 `ptrace` 跟踪=="**,故显示的是 `t` 状态。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-F4EF8F832D4BA8B4E1C377D25BA154F9.png|439]]
>
> >[!example] `ps` 下显示的进程状态示例
> >
> >`gdb -p <pid>` 附加调试时:
> > ![[_attachment/88-求职面试/FAQ-OS.assets/IMG-FAQ-OS-7FB4ED70D1454C7E865FD0F57E04D6F8.png|649]]
> >
> >`ctrl + z` **发送 `SIGTSTP(20)` 信号**暂停时:
>> ![[_attachment/88-求职面试/FAQ-OS.assets/IMG-FAQ-OS-43A4003627162310CBDFEDEEF5917EA5.png|646]]
>
>
>
<br><br><br>
# 进程信息——进程控制块 PCB
> **进程控制块**(Process Control Block, **==PCB==**),也称 "**进程描述符**"。参见 [^4] [^5] [^6] [^7]
OS 内核通过一个称为 "**进程控制块**" 的结构体来 **跟踪/记录每一个进程的相关信息**,该结构体保存在进程的 ==**内核地址空间**== 中 "**进程各异**" 的部分。
PCB 中包含了**内核管理一个进程所需的所有信息**,包括但不限于:
- 进程描述信息:PID、PPID、GID、进程名、进程组等;
- 进程状态:新建、就绪、运行、阻塞等;
- 进程树信息:指向**父进程 PCB** 的指针、指向**子进程 PCB 链表**的指针;
- CPU 调度信息:进程优先级、调度参数等;
- **CPU 寄存器值**:当发生中断(用户态->内核态)、进程上下文切换时,**进程当前的 CPU 寄存器值**会保存在 PCB 中,以便进程重新运行时恢复;
- 信号相关信息:**信号屏蔽字**、**信号处理函数集**
- 记账信息:进程开始时间、占用的 CPU 时间等;
- 资源信息:
- 内存相关:**0 级页表基地址**、**已分配的虚拟地址空间**;
- 文件相关:**打开的文件列表**(文件描述符表)、**当前工作目录**、根目录等;
- .......
> [!info] Linux 中用作进程 PCB 的 **`task_struct` 结构体**在 32 位系统下约 1.7KB [^5],包含上百项成员。
<br>
## task_struct 结构体
Linux 下的**进程/线程控制块** 均为 **`task_struct` 结构体**,内核**以 `task_struct` 作为调度对象**进行调度。
(从内核角度来说,不区分 "进程" 与 "线程" 两个概念,统一视为由 `task_struct` 所表示的 "**任务**")
`task_struct` 中涉及到的几个重要结构体:
| 相关结构体 | |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `struct thread_struct thread` | 用以保存 **内核栈指针**、**用户态寄存器**、**段寄存器等**相关值(例如用户态 PC `%rip`、用户态栈指针); |
| `struct pt_regs` | 保存了 "**用户态->内核态**" 切换时,"**==用户态的寄存器值==**"(按寄存器值在 "**内核栈**" 上的存储顺序保存)<br>(末尾五项由 **CPU 硬件**自动压入 "内核栈";其余项由**内核代码通过汇编指令**压入 "内核栈") |
| `struct mm_struct *mm` | 进程的 "**用户地址空间**"。 `mm_struct` 中有一个**由 `vm_area_struct` 结构体构成的链表**,每一项代表一个用户地址空间下 "**已分配的虚拟地址区域**; |
| `struvt file_struct` | **打开的文件描述符表** |
> [!info] Linux 下的 `task_struct` 、 `thread_struct` 、`pt_regs` 、`mm_struct` 结构体
>
> ![[Excalidraw/Excalidraw-Solution/os-进程基础概念.excalidraw.svg|1022]]
> %% [[Excalidraw/Excalidraw-Solution/os-进程基础概念.excalidraw|🖋 Edit in Excalidraw]] %%
>
![[02-开发笔记/05-操作系统/os-中断机制#^41h6l9]]
<br>
### PCB 的组织方式
参见 [^1]
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-F431057237C7E300CACA7C3BDD8B9BA2.png|613]]
<br><br><br>
# 进程创建
Linux 下进程创建的几种方式
- `fork()` :创建一个独立的 "**子进程**"。
- `clone()` :Linux 特有,支持控制父子进程共享的内存部分。
- `exec`系列函数:将当前进程**完全替换为一个新程序**——从新程序的 main 函数开始执行。
> [!danger] 不应当使用 vfork !
>
> POSIX 相关标准 SUSv4 中已将 `vfork()` 完全移除,尽管 Linux 内核仍然保留了该系统调用,glibc 也支持。
>
> vfork 创建子进程后:
>
> - **父进程将被挂起**,直至子进程调用 `exec` 或 `exit` 后,才恢复父进程;
> - **子进程==直接使用==父进程的地址空间**(无写时拷贝机制),**子进程修改的任何内存页对父进程完全可见**。
>
> [!NOTE] OS 内核创建进程的伪代码
>
> ```cpp
> int process_create(char *path, char *argv[], char *envp[]) {
> // 创建一个新的PCB, 用于管理进程
> struct process *new_proc = alloc_process();
> // 虚拟内存初始化: 初始化虚拟地址空间及页表基地址
> init_vmspace(new_proc->vmspace);
> new_proc->vmspace->pgtbl = alloc_page(); // pgtbl, 页表基地址
>
> // 内核栈初始化
> init_kern_stack(new_proc->stack);
> // 加载可执行文件并映射到虚拟地址空间
> struct file *file = load_elf_file(path);
> for (struct seg loadable_seg : file->segs)
> vmspace_map(new_proc->vmspace, loadable_seg);
>
> // 准备运行环境: 创建并映射用户栈
> void *stack = alloc_stack(STACKSIZE);
> vmspace_map(new_proc->vmspace, stack);
> // 准备运行环境: 将参数和环境变量放到栈上
> prepare_env(stack, agrv, envp);
> // 上下文初始化
> init_process_ctx(new_proc->ctx);
> // 返回
> }
> ```
>
<br>
## fork 函数
> 位于头文件 `<unistd.h>` 中,参见[^8] [^9]
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-CDC526E3B132A5BB0723D0B0FAAC64A8.png|735]]
`fork()` 创建子进程后,父、子进程均从 **调用 `fork` 系统调用** 的代码位置返回,若创建成功则:
- 父进程获得的返回值是其 **创建的子进程的 PID**;
- 子进程获得的**返回值是 0**。
> [!caution] 对于多线程进程而言,**某个线程调用 `fork` 创建子进程后,子进程中只保留 "==调用 fork 的线程=="**
> [!info] Linux 下通过 `clone()` 系统调用实现 `fork()`。该系统调用可通过一系列参数**指明父、子进程需要共享的资源** [^10]。
<br>
### 子进程获得的副本
fork 后,子进程将获得**父进程的以下副本**:
1. **==用户级==虚拟地址空间**——初始时,父子进程 **共享==用户地址空间==对应的 "==所有物理页=="**
- 对于**原本 "只读"** 的页,例如 **文本段/代码段**( `.text` 段),**父子进程==始终共享==相同物理页**,无 "写时拷贝"。
- 对于**原本 "可写"** 的页,例如 "**==数据段、堆、栈、映射段==**",将应用 "**写时拷贝**" 机制。
2. **父进程中已打开的==所有文件描述符==的副本**(拷贝副本)
> [!caution] 父、子进程各自拥有独立的 "**==内核资源==**"
>
> 内核地址空间中,进程相关的内核数据结构例如 "**==PCB、页表、内核栈==**" 等是进程各异的,这部分始终不会共享!
<br>
### 写时拷贝
写时拷贝机制(Copy-On-Write,**==COW==**)[^8] [^11] [^12]:
1. `fork()` 创建的子进程会与父进程 "**==共享用户地址空间的所有物理页帧==**",并将 "**原本可写的页**" 标记为 "**==只读==**"。
2. 当父进程或子进程 **试图向某个 "原本可写的页帧`X`" 写入时**,将触发 **缺页异常**。在异常处理程序中,内核将执行以下操作:
- **为该物理页==分配一页副本== `X'`并拷贝原页数据 `X->X'`**;
- 对触发缺页异常的进程,**更新其页表映射**,修改**该虚拟页访问权限为 "==可读可写=="**。
在写时拷贝机制下,`fork` 的 "**实际开销**" 即为 "**复制父进程的页表**" 以及 "**为子进程创建唯一的进程控制块 PCB**" [^10]。
> [!note] "写时拷贝" 示意图[^11]
>
> 下图中进程 1 与进程 2 是 fork 创建的父子进程关系。
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-DE0315C8C6C67CC25FE2C5FD31C7C6AD.png|347]]
>
> [!faq] ❓ fork 采用写时拷贝的原因
>
> 很多场景下,**`fork` 创建子进程后会紧接着执行 `exec` 变成一个新进程**,因此**没必要拷贝一份父进程的地址空间副本**。
<br><br>
## exec 系列函数
exec 系列函数作用:在当前进程上下文中**加载并运行一个新程序**,即将当前进程 **完全替换为一个==新程序==** [^13]。
> [!info] exec 调用成功时不会返回!将跳转至 **“==新程序的 main 函数==”** 开始执行。
以 `execve` 为例:
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-B80F09C7A3337F4E66B92C8BA0E48350.png|761]]
该函数将**加载并运行指定的可执行文件**,传入**参数列表 `argv`** 以及**环境变量列表 `envp`**。
> [!note] 关于 exec 系列函数接收的 "参数列表" 与 "环境变量列表"
>
> - **参数列表** `argv`:将传递为新程序的 `main(int argc, char** argv)` 中的 `argv`。
> - **环境变量列表** `envp`:需要提供的是 "**==完整的新环境变量列表==**",即 **旧进程的原有环境变量将==全部失效==**。
>
> 对于参数列表,根据惯例,`argv[0]` 应当传入 "**可执行文件的名称**"。
>
> 示例:shell 通过 `execve` 运行 `ls -lt ...`
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-C6984F931CBB6A1085BA82B13F24C686.png|490]]
>
> [!NOTE] Linux 提供的环境变量列表处理 API
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-2F50CD8A1E125F2069B124DF69A02D9D.png|678]]
<br>
### exec 的作用效果
进程执行 exec 函数后,**PID 不变**,而将 **当前进程的执行上下文(代码段、数据段、堆栈等)** 完全替换为新进程的上下文。
其将从**新程序的 `main` 函数**开始执行,同时继承 **==原进程==** 的下列属性:
- **进程 PID 与父进程 PPID**;
- **当前工作目录**
- **实际用户 ID、实际组 ID**
- **进程组 ID、附属组 ID**
- **会话 ID**
- **控制终端**
- **文件模式创建屏蔽字**(`umask`)
- **==文件锁==**
- **==未处理信号==**
- **==进程信号屏蔽==**
- **资源限制**(`ulimit`)
- **环境变量**(调用的并非 `execle` 或 `execve` 或 `fexecve` 设置环境时 )
- **文件描述符**(除非设置了 `FD_CLOEXEC` ,否则默认保留)
> [!faq] ❓ 文件描述符的保留与否
>
> exec 运行新进程后,原进程中每个文件描述符是否被保留&继承取决于 **`FD_CLOEXEC` 标志**,未设置时**默认 "==保留文件描述符=="**。
>
> 文件描述符的 `FD_CLOEXEC` 属性的作用是:**=="close-on-exec==**",通过 `fcntl` 设置该属性后,**在进程执行 `exec` 时该文件描述符将被关闭**。
> `socket`、`timerfd`、`eventfd` 在创建时均可设置该属性(例如 `TFD_CLOEXEC`,`EFD_CLOEXEC`)。
<br>
### exec 函数的七种变体
exec 系列函数共 7 种变体[^14]: `execl()`,`execle()`,`execlp()`,`execv()`,`execve()`, `execvp()`, `fexecve()`
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-C4B092D3EA094AD0D8CF2A9F3B5DCB48.png|820]]
`
> [!NOTE] 不同 `exec` 函数的区别
>
> 区别在于:
> - (1)**新程序** 如何指定:**完整路径名** or **仅文件名** or **文件描述符**
> - (2)**新程序的参数** 如何给出:**逐一列出**(可变参数) or **由一个指针数组给出**
> - (3)**继承调用进程的环境** or **指定新环境**
>
> 具体来说,**不同后缀 l、v、p、e** 对应不同含义:
>
> - 必带 `l` 或 `v`:
> - `l`:预期接收**不定长的参数列表**,参数列表**要求以 ==`NULL` 指针==作为结束项**;
> - `v`:预期接收一个**以 ==`NULL` 指针结尾==的、字符指针数组** 作为参数列表。
> - 可选 `p` 或`e`:
> - `p`:表示可以 **仅接收 "文件名"**,并将**在环境变量 `PATH` 对应的目录下搜索该可执行程序**。
> - `e`:表示函数参数列表中最后一项接收一个**字符指针数组** `envp`,用以接收**环境变量**,而**不使用当前环境**
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-363EA61B0C58CCFEE664D0DE82C43732.png|669]]
>
> [!info] exec 的 7 种接口中,仅 `execve` 是内核调用,其余均为 "库函数" 封装,均会调用前者。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-9E3FB231C80C8263782122004724CBEC.png|695]]
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-9DDBD6B93417241A598D0EBD60341DFA.png|630]]
>
<br><br><br>
# 进程终止
### 进程终止的触发情况
正常终止情况主要包括:
- 从 `main()` 返回
- 调用 `exit()` 或 `_Exit()`——来自 `<stdlib.h>`
- 调用 `_exit()` ——来自 `<unistd.h>`
异常终止情况主要包括:
- 调用 `abort()`;
- 收到**终止信号**——`SIGKILL`、`SIGTERM` 等
> [!info] 一个进程正常或异常终止时,内核会向其父进程发送 "**==SIGCHLD 信号==**"
> [!info] 无论进程以哪种形式终止,内核都会为其 "**==关闭所有已打开的文件描述符==**"、"**==释放其占用的所有内存==**"。
> [!NOTE] "`main` 函数中 `return status;`" 与 "直接调用 `exit(status)`" 等价
<br><br>
## exit 相关调用——正常终止
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-0E9F1462D6DA40EC14928B6929FB480C.png|695]]
三个函数均接收一个 "**==退出状态==**" 参数,该值可被进程的 **父进程** 通过 `wait(&status)` 或 `waitpid(&status)` 获得。
三个函数中, `exit` 会先**执行清理处理**,再**调用 `_Exit` 或 `_exit` 返回到内核**,而后两者直接返回到内核。
`exit(int status)` 调用执行以下工作:
1. **按注册顺序的 "==逆序==" 执行 `atexit()` 注册的所有回调函数**;
2. **逆序** 遍历 **`.fini_array` 中的函数指针**,执行退出清理函数;
3. **调用 `_exit(status)`** 退出进程 → 通知内核终结进程。
> [!example] `exit` 实现的伪代码
>
> ```c
> void exit(int status) {
> __run_exit_handlers(status, ...);
> _exit(status);
> }
>
> void __rund_exit_handlers(, ...) {
> // 1. 执行通过 atexit() 注册的回调函数(按注册顺序的逆序执行)
> while (has_registered_handlers) {
> handler();
> }
> // 2. 执行 `.fini_array` 中的函数
> if (__libc_fini_array) {
> __libc_fini_array(); // 调用 .fini_array 中的函数(逆序执行)
> }
> }
> ```
>
> [!NOTE] 调用 `exit()` / `_Exit()` / `_exit()` 终止进程时的不同行为
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-E342EFC6B8840FC107FFE965E363B50C.png|721]]
>
> ^1v4ilv
![[02-开发笔记/03-计算机基础/编译与链接/目标文件#^1zl2gv]]
<br>
## atexit 函数——注册退出回调
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-8B7D545AAC0F676E4DCD8802B281C093.png|700]]
<br>
## abort 函数——异常终止
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-8D9BB3D08D08B7EB865E276920FE9C78.png|761]]
`abort` 函数用于触发进程 "**异常终止**",其实现本质上是 **令内核==发送 `SIGABRT` 信号==给进程**,并在信号处理程序返回时**终止进程**。
> [!example] abort 的实现伪代码
>
> ```c
> void abort(void) {
> raise(SIGABRT); // 向自己发送 SIGABRT 信号
> // 若没有终止, 则直接调用 _exit(127)
> }
> ```
>
> [!tip] 进程可以**捕捉 `SIGABRT` 信号并自定义处理**——例如完成**清理**操作 or 直接**调用 `exit` 相关函数终止自身**。
<br><br><br>
# 进程回收
进程终止后,必须**由其父进程手动通过 `wait` 或 `waitpid` 函数回收其资源**,否则将作为 "**==僵尸进程==**",尽管占用极少资源但**会占据一个 pid**。
父进程调用 `wait` 或 `waitpid` 函数后,**将==阻塞等待==子进程运行结束,回收子进程资源**。
> [!info] 若一个子进程的父进程崩溃,**子进程会被内核==自动分配==作为 init 进程的子进程**,**子进程运行结束后将由 ==init 进程自动进行回收==**。
<br>
## waitpid 与 wait 函数
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-050524EB07410C0BB68AF697AC363366.png|709]]
`waitpid` 函数:
- pid:
- `pid > 0`:等待 "**指定子进程**" 终止后才返回。
- `pid = -1`:等待 **==任意一个==子进程终止后返回**。
- `pid = 0`: 等待 "**组 ID = 调用进程组 ID**" 的 **任意一个子进程** 终止后返回。
- `pid < -1`:等待 "**组 ID = pid 绝对值**" 的 **任意一个子进程** 终止后返回。
- staloc:
- 用于**接收子进程终止时的状态信息**,可用于判断子进程是否为通过 `exit` 或从 `main` 返回而正常终止,或是获取令子进程终止的信号。
- options:
- 为 0 表示**默认行为**,可通过 `WNOHANG` 等常量设置不同选项,例如 "**不阻塞直接返回**"。
> [!NOTE] `wait(&status)` 等价于 `waitpid(-1, &status, 0)`,即等待 "任意一个子进程" 终止后即返回
> [!caution] `wait()` / `waitpid(-1,...)` 回收子进程的顺序是 "不确定的",除非通过 `waitpid(x,...)` 指定回收特定线程
> [!tip] 一个进程正常或异常终止时,内核会向其父进程发送 "**SIGCHLD 信号**"
>
> 故**可将 `wait()` / `waitpid()` 作为 SIGCHILD 信号的处理函数**。
>
> Linux 中 `init` 进程**保证其所有子进程不会成为 "僵尸进程"** 的做法便是如此,伪代码示意如下:
>
> ```c
> // 伪代码示意:
> signal(SIGCHLD, handler);
>
> void handler(int sig) {
> pid_t pid;
> int status;
> // 不断地回收所有已终止的子进程
> while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
> // 处理状态,比如日志、重启等
> }
> }
> ```
>
<br><br><br>
# 进程加载 ⭐
程序的加载过程:
1. shell 通过 `fork` **创建==子进程==**;
2. 子进程随后 **调用 `execve` 或其它 exec 函数==加载可执行程序==**;
3. exec 函数完成 "**进程加载**",将控制传递给 **新程序的 main 函数**。
> [!NOTE] ELF 文件中,被加载至内存的 "节" 主要包括 `.text`、`.rodata`、`.bss`、`.data` 等。
>
> 下图参见[^15]
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-8E140C6A7BAAB07C346D19B19468971D.png|553]]
>
> [!info] 程序加载器(loader)
>
> "程序加载器" 是指 **exec 系列函数背后的一整套实现逻辑**,并非一个独立进程 or 系统服务,而是 **OS 内核代码的一部分(位于 `.text` 代码段)**
<br>
## exec 加载 ELF 可执行文件的过程
大致过程如下 [^16]:
1. 读取 **ELF 头部**,获取 "**程序头部表**" 的地址;
2. **==分段映射 + 权限设置==**——遍历 ELF 文件中的 "**==程序头部表==**",**加载 ==`PT_LOAD` 类型的段== 到内存**:
- **内核将该段在 ELF 文件中的起始偏移 `p_offset` 起的 `p_filesz` 字节==映射至虚拟内存地址== `p_vaddr`**,并根据 `p_flags` 设置内存页的 **访问权限**。
- 若 `p_memsz > p_filesz`,则将多出来的部分 "**==清零==**"
- (例如 `.bss` 段,在 ELF 文件中不占空间,但加载到内存后占 `p_memsz` 大小)
3. 完成 "**动态链接**"(仅针对 "动态链接的程序")
1. 读取**程序头部表**中 **`PT_INTERP` 段**,获取 "**动态链接器路径**";
2. 内核加载并启动 **动态链接器**(`/lib/ld-linux.so`);
3. 动态链接器**加载程序依赖的所有共享库**,并完成重定位、符号解析等处理。
4. 构建 "**==用户栈==**"
1. 内核映射得到栈区;
2. 压入**命令行参数列表 `argv`**、**环境变量列表 envp** 以及 `auxv` 等辅助信息,构建初始栈帧,设置**栈指针寄存器 `rsp`**。
5. 设置 "**==程序入口地址==**"
1. 读取 ELF 头部中的 **`e_entry` 字段**,获取 **程序入口地址**(通常是 `_start` 函数的地址);
2. 设置 **指令指针 rip**(即程序计数器 PC) 为 **程序入口地址**。
6. 跳转至 "**==程序入口点==**" (`_start()`函数)开始执行
- `_start` 函数将调用 `__libc_start_main()` 函数,而**后者再进一步调用 `main()` 函数**。
7. **执行 `main()` 函数**
> [!important] 除了一些头部信息,整个进程加载过程中,没有任何从磁盘到内存的 "**数据复制**" [^16]
>
> 加载过程,OS 内核进行**内存映射**时,并不涉及 "**磁盘到内存**" 的数据拷贝,
> 例如对 ELF 文件中的 `.text` 与 `.data` 段,内核实际上只是 "**分配了虚拟页并==标记无效==**" [^17],
> **直至 CPU 首次引用一个被映射的虚拟页** 而触发 "**缺页异常**"后,才由缺页异常处理程序 **真正分配物理页**。
>
>
> [!NOTE] Linux 下所有 ELF 可执行程序的 "默认入口点" 为 `_start()` 函数
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-B99FFFCC7BEE3C0ABA48D0BF06649FBC.png|570]]
>
> 进程加载的实际调用链:
>
> ```text
> _start()
> └──libc_start_main()
> ├── 完成初始化——包括注册 atexit 回调,执行 .init_array 中初始化函数
> ├── 调用 main()
> └── 调用 exit()(执行析构函数、清理资源)
> ```
>
>
> [!NOTE] 可手动指定 ELF 文件的 "**==程序入口点函数==**"
>
> ELF 文件的入口点是在 "**==链接阶段==**" 由 **链接器** 指定的,**默认即为 C/C++运行时库 `crt1.o` 中提供的 `_start()` 函数**。
> 在使用 gcc/g++编译时,可通过特定选项设置 "**指定入口点函数**"。
>
> > [!example] 示例:以汇编代码**编写一个最小可执行的 C++程序**,不依赖 libc 运行时库,不走 `_start`,**==手动指定入口点==**
> >
> > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-95327DF7A3A9B96794F9BBD0F51CF1F6.png|404]]
> >
> > 编译指令:`g++ main.cpp -o myprog -nostdlib -nostartfiles -Wl,-e,my_entry`,
> >
> > - `-nostdlib`:指示编译器**不使用 stdlib 运行时库**
> > - `-nostartfiles`:指示编译器**不使用默认的启动文件**(比如 `crt1.o`)
> > - `-Wl,-e,my_entry`:传给链接器 `ld`,**==指定入口点==为源代码中自定义的 `my_entry`函数**
> >
> > 运行结果:
> >
> > ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-F811FD664ED172F360349F59D774EBD7.png|610]]
> >
>
![[02-开发笔记/03-计算机基础/机器视角下的进程#初始函数栈的结构示意|机器视角下的进程]]
<br>
## `_start` 函数与 `__libc_start_main` 函数
> [!info] 关于 `_start()` 函数
>
> `_start()` 函数是由 **glibc 运行时库**提供的**启动代码**,位于 **系统库的目标文件 `ctr1.o`** 中。
> 当用户程序与 glibc 库链接形成**最终的可执行文件**后,该函数即作为 "**==程序入口点==**"(ELF 头中的 Entry Point)。
>
> `_start()` 会**调用 `__lib_start_main()` 函数**,再 **==由后者进一步调用 `main()` 函数==**。
>
> [!NOTE] 关于 `__libc_start_main()` 函数
>
> `__libc_start_main()` 负责**初始化整个 C/C++ 运行时环境**,主要步骤包括:
>
> 1. 初始化**线程局部存储 TLS**;
> - 若 ELF 中 `PT_TLS` 段存在,则根据该段**为主线程分配 ==TLS 区域==**;
> - 配置寄存器指向该 TLS 区(如 x86-64 中的段寄存器 `fs.base`)
> 2. **==注册 `atexit()` 回调==**
> 3. 遍历 **==初始化函数数组==** `.init_array`,**调用各个初始化函数**。
> 4. 调用 main 函数,**传递参数列表 `argv`、环境变量列表 `envp` 等参数**;
> 5. 等待 main 函数返回,**获取其退出状态码**,根据其返回值 **调用`exit()`** ,触发清理。
>
> ```cpp
> // __lib_start_main() 中对`main`的调用示意
> main_retval = main(argc, argv, envp);
> exit(main_retval); // 将main的返回值直接传递给exit(), 执行清理
> ```
>
![[02-开发笔记/03-计算机基础/编译与链接/目标文件#^jxfmj2]]
> [!NOTE] `__attribute__((constructor))` 标记的函数将在 `main()` 函数之前被执行
>
> ```cpp
>
> __attribute__((constructor))
> void my_ctor() {
> printf("Hello from constructor!\n");
> }
>
> int main() {
> printf("Hello from main!\n");
> return 0;
> }
> ```
>
<br><br><br>
# 进程打开文件
> 参见;《CSAPP》P634
OS 内核使用了三个数据结构来共同维护/记录 "**进程打开的文件**"[^18] [^19]:
- **每个进程独立的 "==文件描述符表=="**(File Descriptor Table)——记录了**该进程打开的所有文件**,"**文件描述符**" 值即用作该表的索引。
- **内核中的 "==文件表=="**(File Table)——每个被打开文件对应一个 "**文件表项**"(同一文件被不同进程同时打开时,**对应不同表项**)
- **内核中的 "==v-node 表=="**(v-node Table)——每个打开文件对应一个 **v-node**
> [!info] 内核中的 "**文件表**" 与 "**v-node 表**" 由所有进程==共享==
> [!info] Linux 中没有单独定义 v-node,而是 **统一使用 i-node 数据结构** 来表示 v-node
>
> 区分 `i-node` 数据结构中记录的信息:
>
> - 一种是 **==充当 v-node==**,用以表示 "**打开文件**" 的 i-node;
> - 一种是文件系统相关的,记录文件元数据的 i-node;
>
![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-0072B1E7EF150F996C30A4B50423A0D8.png|798]]
## 三个表中的信息
- **文件描述符表项**
- 记录 "**描述符**" 的状态标志,例如 **==`FD_CLOEXEC` 标志==**;
- 记录指向 "**文件表项**" 的指针。
- **文件表项**
- 记录 "**打开文件**" 的状态标志,例如 **==`r/w/append`==**,**==阻塞与非阻塞==** 等,即 `open()` 或 `fcntl()` 设置的**打开模式与属性**。
- 记录 "**当前文件的偏移量**"
- 记录指向 "**v-node 表项**" 的指针。
- **v-node 表项**
- 记录 "**文件信息**,包含了 **`stat` 结构体** 中的大多数信息,例如**文件类型、文件大小** 等。
- 记录指向文件 "**i-node**" 的指针。
## 文件共享
每个 "**被打开文件**" 只会**存在==唯一的 v-node 表项==**。
当**多个进程打开同一文件**、或是**单个进程多次打开同一文件**时,都将创建一个 "==**独立的文件表项**==",指向 "**同一个 v-node 表项**"。
> [!example] 文件共享:不同进程打开同一文件
>
>
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-552B39CB852A4A35CFF765F80AF131E1.png|606]]
>
> [!example] 文件共享:同一进程多次打开同一文件
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-CDDA9BFE9FF34ACBA6C54FD13965E61D.png|567]]
> [!example] 文件共享:`fork` 创建的子进程继承 "**父进程已打开的文件描述符表**" 的副本
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-39586C3E06D3AE63A8976028663058EC.png|558]]
>
> [!NOTE] `dup(fd)` 的本质
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-CE17F4D09B2E655B9CBC32118F80EAC1.png|713]]
<br><br><br>
# 进程上下文切换 ⭐
> **上下文切换**(Context Switch)
OS 暂停当前执行的进程,转而**切换执行另一个进程**时,需要**保存当前进程的上下文,恢复另一进程的上下文**,该过程即称之为 "**上下文切换**"。
上下文切换是现代 OS **支持==多任务==** 的关键机制,使得单个 CPU 可被多个进程或线程共享,从而**实现并发**——CPU 在进程或线程间切换,从而**表现为在并发地执行多个任务**。
进程上下文切换可以由多种因素触发,包括:
- **任务调度**
- **时间片用尽**:分配给当前进程的时间片耗尽时,触发时钟中断,进而执行进程调度;
- **高优先级任务就绪**:高优先级任务到来时,OS 可能会中断当前正在执行的低优先级任务,先执行高优先级任务。
- **阻塞等待**:
- **等待 I/O**:进程执行 I/O 操作而阻塞等待时,OS 会将其挂起,转而执行其他任务;
- **主动挂起**:进程调用 `sleep()` 主动陷入睡眠时;
- **主动放弃 CPU**:进程调用 `yield()`,主动放弃时间片;
<br>
## 进程的上下文
进程的上下文是指 "**程序正确运行所需的==所有状态信息==**",由**内核为每个进程维护**。包括但不限于:
- **用户上下文**——进程虚拟地址空间中的 "**用户地址空间部分**"
- 进程的栈、堆、代码/文本段、程序段;
- **内核上下文**——进程虚拟地址空间中的 "**内核地址空间中==进程特异==的部分**"
- **内核栈**
- 内核数据结构:**进程页表**、**进程表**(包含当前进程信息)、**文件表**(当前进程已打开文件的信息)
- ....
- **硬件上下文**(CPU 寄存器值)
- **程序计数器 PC `%rip`**
- **栈指针 `%rsp`**
- **通用寄存器值**
- **页表基址寄存器值**(即进程 0 级页表的物理地址)
- ....
<br>
## 进程上下文切换的过程
步骤概述:
1. OS **保存当前进程的上下文**;
2. OS **加载/恢复另一个进程的被保存的上下文**;
3. **将 CPU 控制权传递给被恢复的进程**
#### 详细步骤
> 参见[^20]
- (1)触发**时钟中断**,**由 "用户态" 陷入 "内核态"**。
1. CPU 特权级由 CPL=3 (用户模式)切换到 CPL=0(内核模式);
2. CPU 修改栈指针寄存器值 `%rsp` 为进程的"**内核栈指针**";
3. CPU 将 "**==用户态的寄存器值==**" 压入 "**==内核栈==**" 中:**用户态 PC 值 `%rip`、用户态栈指针 RSP、状态寄存器 RFLAGS**等;
- (2)CPU 查询中断向量表,跳转执行 **中断处理程序**(内核的 **进程调度程序**)
1. 内核保存 "**当前进程的==内核态寄存器值==**" 到进程控制块 PCB 中。例如,**==内核态 PC 值==**、**==内核态栈指针==** 等;
- Linux 下保存到 `task_struct->thread` ,**`thread_struct` 结构体**
2. 内核通过调度器,**获取下一个进程的 `task_struct`**:
- (1)**恢复新进程的==内核栈态寄存器值==**:读取其 `task_struct->thread`,**存入到 CPU 寄存器里**;
- 例如,**新进程的内核态 PC 值、内核态栈指针**
- (2)**切换到新进程的==页表==**:从 `task_struct->mm` 中获取新进程的 **==页表基地址==**,存入到 **页表基址寄存器** `CR3`。
- (3)内核调度程序返回,执行 `sysretq` 或 `iretq` 指令,从 "**内核态**" 切换回 "**用户态**"
1. CPU 从新进程的 **内核栈** 上弹出其 **新进程的==用户态寄存器值==**:**用户态 RIP**、**用户态栈指 RSP** 等,恢复到对应寄存器;
2. CPU 特权级切换回到 CPL=3(用户模式)
```c
// 进程上文切换伪代码:
save_context(current->thread_struct) // 保存"当前CPU寄存器值"
next = pick_next_task(); // 获取下一个task_struct
if (current->mm != next->mm) { // 如果两个task属于不同的地址空间(针对进程间切换)
switch_mm(prev->mm, next->mm, next); // 切换页表基地址寄存器CR3, 同时需要刷新TLB
}
restore_context(next->thread_struct); // 恢复"CPU寄存器值"
current = next;
```
<br>
## 进程上下文切换保存的内容
进程上下文切换时,首先涉及到 "**用户态->内核态**" 的切换,而后才是 "**进程间切换**",因此保存内容分成两部分:
1. "**旧进程的==用户态寄存器值==**" => 保存到 "**旧进程的==内核栈==**"。("用户态->内核态" 切换所要求的)
2. "**旧进程的==内核态寄存器值==**" => 保存到 "**旧进程的 `task_struct->thread`**" 中。("进程间切换" 所要求的)
其他信息不需要保存,各进程 `task_struct` 常驻内核地址空间,当 **`next` 指向调度器返回的下一个`task_struct` 后就可以直接获取其中信息**。
切换到新进程时,需恢复的内容如下:
1. "**新进程的==内核态寄存器值==**" <= 从 "**新进程的 `task_struct->thread` 中读取**",恢复到对应寄存器。例如内核态栈指针 `%rsp`、内核态 `%rip`;
2. "**新进程的==页表==**" <= 从 "**新进程的 `task_struct->mm`**" 中读取,加载到**页表基址寄存器** `CR3` 中。
3. "**新进程的==用户态寄存器值==**" <= 由 "内核态->用户态" 时,从新进程的 "**内核栈**" 上弹出,恢复到对应寄存器。
> [!NOTE] 进程上下文切换过程中,保存到 "**进程控制块 PCB**" 中的是 "**==内核态寄存器值==**"。
>
> 下图参见[^4]
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/进程基础概念.assets/IMG-进程基础概念-A3A119EA5BFE73D40ACE758D1F709AA3.png|466]]
>
> ^6dpkiu
<br>
## 进程上下文切换的开销
来自于两方面,影响最大的是第二点:
1. **时间开销**:不同进程上下文(**寄存器值**) 的保存/恢复操作。
2. **==缓存失效==**:进程运行时**在 CPU Cache、TLB、分支预测器和其他硬件中建立了大量缓存状态**,**切换后会刷新这些缓存**。
<br><br><br>
# ❓ 进程相关 FAQ
#### <font color="#c00000">❓父进程崩溃时,其创建的子进程是否受影响?</font>
**子进程独立于父进程运行**,当其**父进程先正常退出 or 异常崩溃时,==子进程不会崩溃/退出,其会被 OS 重新分配给 `init` 进程==**(Linux 上 `PID=1`),由 `init` 负责接管,此时该子进程称为 "**==孤儿进程==**",其调用 `getppid()` 返回的是 `init`的 1。
若**子进程依赖于父进程的资源**,例如其**需要与父进程通信而共享文件描述符**(如管道、套接字等),若父进程崩溃关闭了文件描述符,则**子进程可能会因为无法继续进行 I/O 操作**而受到影响。
#### ❓<font color="#c00000">一个多线程程序,其中一个线程调用 `fork()` 创建了子进程,则子进程中具有几个线程?</font>
**子进程只会==保留一个线程==——调用 `fork()` 的那个线程**。
`fork()` 创建的子进程会**复制/继承父进程的整个地址**,包括父进程中的**互斥量、读写锁、条件变量**等,但是**子进程中只会保留 "调用 `fork()`" 的这一个线程**。因此,当子进程从 `fokr()` 返回后:
- 若立即调用 `exec()` 的话,则 **==旧的地址空间被丢弃==**,因此无需再考虑处理锁。
- 若没有立即调用 `exec()` 的话,就**需要==清理==这些继承而来的锁**
=> 由父进程调用 `fork()` 前,**调用 `pthread_atfork()` 预先建立 fork 处理程序**。
<br><br>
# 参考资料
# Footnotes
[^1]: [《小林 Coding》](https://xiaolincoding.com/os/4_process/process_base.html#%E8%BF%9B%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81)
[^2]: 《深入理解计算机系统》(P514)
[^3]: 《UNIX 环境高级编程》(P189)
[^4]: 《操作系统概念》(P74)
[^5]: 《Linux 内核设计与实现》(P21)
[^6]: 《现代操作系统》(P53)
[^7]: 《深入理解 Linux 内核》(P86)
[^8]: 《UNIX 环境高级编程》(P182)
[^9]: 《深入理解计算机系统》(P514)
[^10]: 《Linue 内核设计与实现》(P27)
[^11]: 《操作系统概念》(P271)
[^12]: 《深入理解计算机系统》(P584)
[^13]: 《深入理解计算机系统》(P521~523)
[^14]: 《UNIX 环境高级编程》(P202)
[^15]: 《深入理解计算机系统》(P584)
[^16]: 《深入理解计算机系统》(P485,第 7.9 节)
[^17]: 《深入理解计算机系统》(P566)
[^18]: 《UNIX 环境高级编程》P60、P184
[^19]: 《深入理解计算机系统》(P634)
[^20]: 《深入理解 Linux 内核》(P111)