文章目录
- 异常控制流
- 进程
-
- 多任务处理
- 进程并发
- 上下文切换
- 系统调用错误处理
- 创建与终止进程
- [fork 函数的基本行为](#fork 函数的基本行为)
- [fork 的概念视图](#fork 的概念视图)
- [fork 函数再探](#fork 函数再探)
- [fork 示例分析](#fork 示例分析)
- [fork 示例:连续两次 fork](#fork 示例:连续两次 fork)
- 僵尸进程与回收机制
- wait:与子进程同步
- execve:加载并运行程序
- 信号机制
异常控制流
- 控制流:CPU核心从启动到关闭持续读取并执行指令序列,指令执行顺序构成CPU的控制流(或控制转移流程)。
- 改变控制流的机制分为两种:跳转与分支、调用与返回。但是以上的控制流机制难以响应系统级事件,典型的系统事件包括:磁盘或网络数据到达、除零操作、用户输入Ctrl-C,系统定时器超时。因此系统需要异常控制流机制处理部分系统事件。
- 异常控制流(Exceptional Control Flow,ECF)存在于计算机系统的各个层级。
- 底层机制:异常------硬件与操作系统协同实现,响应系统事件。
- 高层机制:上下文进程切换------有操作系统和硬件定时器实现;信号------由操作系统软件实现;非局部跳转------由C运行时库实现。
异常
- 异常是处理器因某种事件触发而将控制权转移到操作系统内核的过程。
- 内核是操作系统驻留内存的部分。
- 触发事件示例:除以0、算术溢出、页面错误、I/O请求完成、键盘输入Ctrl-C。

异常表和分类

- 异常控制流按照同步与异步可以划分为如下内容:
- 同步异常:Traps(陷阱)、Faults(故障)、Aborts(中止)
- 异步异常:Interrupts(中断)

中断:异步异常
- 成因:来自处理器外部的事件,通过设置处理器中断引脚触发。
- 返回行为:处理完成后返回"下一条"指令。
- 定时器中断:每隔几毫秒触发一次,用于内核夺回用户程序控制权。
- I/O设备中断:键盘按下
Ctrl-C、网络包到达、磁盘数据就绪。
同步异常
- 同步异常成因:执行某条指令直接导致的事件。
- Traps(陷阱):故意引发,用于设定特定功能,返回至"下一条"指令。示例:系统调用、gdb断点。
- Faults(故障):非故意但可能恢复。示例:页面错误(可恢复)、保护性错误(不可恢复)、浮点异常。处理方式:重试当前指令或终止程序。
- Aborts(中止):非故意且不可恢复,直接终止当前程序。示例:非法指令、奇偶校验错误、机器检查错误。
系统调用
- x86-64系统调用编号:每个系统调用有唯一ID。

- 系统调用示例:打开文件
c
00000000000e5d70 <__open>:
...
e5d79: b8 02 00 00 00 mov $0x2,%eax # open is syscall #2
e5d7e: 0f 05 syscall # Return value in %rax
e5d80: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax
...
e5dfa: c3 retq

- 用户调用open(filename, options)。调用__open函数,触发syscall指令。
参数传递约定
- %rax:存放系统调用号
- %rdi, %rsi, %rdx, %r10, %r8, %r9:依次存放参数
- 返回值存于%rax
- 负值表示错误,对应负的errno值
故障示例:缺页
-
用户写入内存位置,对应页面当前位于磁盘上。
c80483b7: c7 05 10 9d 04 08 0d movl $0xd,0x8049d10 -
代码示例
cint a[1000]; main() { a[500] = 13; }
执行流程:用户代码执行movl指令,触发页面错误异常,内核处理程序将页面从磁盘加载到内存,返回并重新执行原movl指令。

进程
- 进程是正在运行的程序的一个实例。进程为每个程序提供两个关键抽象:逻辑控制流和私有地址空间。
- 逻辑控制流:每个程序看似独占CPU,由内核的上下文切换机制提供支持。
- 私有地址空间:每个程序看似独占主存,由虚拟内存机制提供支持。
多任务处理
- 逻辑情况:多个进程看似同时运行,独立使用栈、堆、CPU、寄存器等相关部件。

- 真实情况1:单个CPU(单核情况)并发执行多个进程,进程执行交错进行(多任务)。
- 内存管理:地址空间由虚拟内存系统管理。
- 寄存器保存:非运行进程的寄存器值保存在内存中。

上下文切换步骤(单核情况)
- 保存现场:将当前进程的寄存器状态保存到内存。
- 调度:内核选择下一个要执行的进程。
- 恢复现场:加载目标进程的寄存器状态,切换地址空间,完成上下文切换。
- 真实情况2:单个CPU(单核情况)并发执行多个进程,进程执行交错进行(多任务)。

进程并发
- 进程并发:若两个进程的控制流在时间上有重叠,则称其为并发,否则为顺序执行。
- A、B、C进程在时间轴上的执行区间有交集即为并发

- 用户视角下的并发进程:并发进程的实际执行在时间上是分离的,但在用户的视角中,可视为多个进程并行运行,符合直觉上的"同时运行"理解。

上下文切换
- 进程由一段驻留在内存中的共享操作系统代码(即内核)进行管理。
- 内核不是独立进程,而是嵌入在现有进程中运行。
- 控制流转:进程间控制流通过上下文切换实现。
- 执行流程:用户代码 ↔ 内核代码交替出现,上下文切换发生在内核态中。

系统调用错误处理
- 错误返回规则:Linux系统级函数出错时通常返回-1,同时设置全局变量errno说明错误原因。
- 编程准则:必须检查每个系统级函数的返回状态,唯一例外是返回类型为void的少数函数。
c
if ((pid = fork()) < 0) {
fprintf(stderr, "fork error: %s\n", strerror(errno));
exit(-1);
}
创建与终止进程
进程的三种状态
- 运行:进程正在执行或等待被内核调度执行。
- 停止:进程执行被暂停,不会被调度(将在信号章节详细讨论)。
- 终止:进程永久停止,包括正常退出、收到终止信号或异常崩溃。
进程终止的原因
- 收到默认动作为终止的信号(后续讲解)
- 从主函数
main返回 - 显式调用
exit函数
exit 函数
void exit(int status)以指定状态码终止进程,惯例:正常返回为 0,出错为非零值。- 从
main函数返回等价于调用exit(return_value),exit被调用后不再返回
- 从
fork 函数的基本行为
int fork(void)创建子进程:调用一次,返回两次,在子进程中返回 0,在父进程中返回子进程 PID。
子进程与父进程的关系
- 子进程获得父进程虚拟地址空间的独立副本
- 子进程继承父进程的打开文件描述符
- 子进程拥有不同于父进程的 PID
fork因"一次调用,两次返回"而常令人困惑
fork 的概念视图

- 进程复制机制:完全复制父进程的执行状态(内存、寄存器等),内核将两个进程分别标记为父进程和子进程,恢复其中一个进程的执行(通常是父进程先继续)。
fork 函数再探
- 虚拟内存机制支持 fork:利用 VM 和写时复制(COW)机制高效实现 fork
实现步骤
- 复制当前进程的
mm_struct、vm_area_struct和页表 - 将所有页面标记为只读
- 将
vm_area_struct标记为私有 COW 区域 - 返回后,两进程拥有相同的虚拟内存视图
- 后续写操作触发页面复制(COW 机制)
fork 示例分析
c
int main(int argc, char** argv)
{
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x);
return 0;
}
/* Parent */
printf("parent: x=%d\n", --x);
return 0;
}
变量独立性与并发执行:示例程序中变量 x 在父子进程中独立变化
fork返回后,父子进程各自拥有x=1- 子进程执行
++x输出x=2;父进程执行--x输出x=0 - 输出顺序不可预测,体现并发特性
- 共享标准输出文件描述符,输出可能交错

- 进程图:用于表示并发程序中语句的偏序关系。每个顶点代表一条语句的执行,边
a -> b表示 a 发生在 b 之前,可标注变量值或输出内容,图起点为无入边的顶点,任意拓扑排序对应一个可行的总执行顺序。
- 主函数开始执行为起始顶点,
fork调用产生两条分支路径(父、子)。 - 子进程路径包含
printf("child : x=%d\n", ++x); exit;,父进程路径包含printf("parent: x=%d\n", --x); exit;,两个exit结束各自路径。 - 多种拓扑排序对应不同输出顺序。


- 原始图包含 main → fork → printf_child / printf_parent → exit
- 正确的总顺序必须满足所有边方向一致(左到右)
- 示例:
a b e c f d是可行顺序 a b e c f d中若出现反向依赖则为不可行
fork 示例:连续两次 fork

- 多进程生成模式:函数
fork2()包含两个连续fork()调用。 - 第一个
fork生成两个进程 - 第二个
fork使每个已有进程再生成一个子进程,共四个进程 - 输出序列中 "L0" 仅一次,"L1" 两次,"Bye" 四次
- 可行输出:
L0 L1 Bye Bye L1 Bye Bye - 不可行输出:
L0 Bye L1 Bye L1 Bye Bye(违反执行顺序)
僵尸进程与回收机制
- 进程终止后仍占用资源(如退出状态),称为"僵尸"(zombie)。必须由父进程通过
wait或waitpid回收
回收过程
- 父进程获取子进程退出状态,内核删除僵尸进程。
- 若父进程未回收即终止,其子进程由 init 进程(PID=1)接管并回收。
- 特殊情况:若父进程已是 init,则无法回收,需重启解决。
- 长期运行的进程(如 shell、服务器)必须显式回收子进程。
wait:与子进程同步
wait 函数功能
int wait(int *child_status):挂起调用进程,直到任一子进程终止,通过系统调用实现,返回终止子进程的 PID

状态信息获取
- 若
child_status非空,保存退出原因和状态 - 使用宏解析状态:
WIFEXITED,WEXITSTATUS,WIFSIGNALED,WTERMSIG,WIFSTOPPED,WSTOPSIG,WIFCONTINUED
- 示例
fork9()中父进程调用wait等待子进程结束。

- 子进程打印 "HC: hello from child" 后退出
- 父进程打印 "HP: hello from parent" 后阻塞于
wait - 子进程结束后,父进程恢复并打印 "CT: child has terminated"
- 最后打印 "Bye"
- 可行输出:
HC HP CT Bye或HP HC CT Bye - 不可行输出:
HP CT Bye HC(HC 不能在 CT 之后)
execve:加载并运行程序
- execve 函数原型:
int execve(char *filename, char *argv[], char *envp[]) - 功能:在当前进程上下文中加载并运行新程序,可执行文件可以是目标文件或脚本(以
#!interpreter开头)。argv[]:命令行参数数组,惯例argv[0]为程序名envp[]:环境变量数组,格式为"name=value"- 如
"USER=droh",可通过getenv,putenv,printenv操作

- 覆盖原有代码段、数据段和栈,保留 PID、打开文件和信号上下文,调用一次且永不返回(除非出错)。
在子进程中执行 ls 命令
- 构造参数数组
myargv:myargv[0] = "/bin/ls"myargv[1] = "-lt"myargv[2] = "/usr/include"myargv[3] = NULL
- 使用全局变量
environ传递当前环境 - 子进程中调用
execve(myargv[0], myargv, environ) - 若失败则打印错误并退出
新程序启动时的栈结构
argc(传入%rdi)argv指针数组(传入%rsi),末尾为NULLenvp指针数组(传入%rdx),末尾为NULL- 空终止的命令行参数字符串
- 空终止的环境变量字符串

栈帧结构
- 顶部:未来
main函数的栈帧 - 中间:
libc_start_main的栈帧 - 底部:实际存储参数和环境字符串
信号机制
-
信号定义与用途:内核发送的小消息,通知进程某事件已发生,类似异常和中断,但面向进程而非指令。
-
信号来源:内核自动检测系统事件(如除零、子进程结束),其他进程调用
kill()系统调用显式请求。 -
信号特性:仅包含 ID 和到达事实,小整数 ID 标识类型(1--30)。
-
常见信号列表
| ID | 名称 | 默认动作 | 对应事件 |
|---|---|---|---|
| 2 | SIGINT | 终止 | 用户输入 Ctrl-C |
| 9 | SIGKILL | 终止 | 强制杀死进程(不可忽略/覆盖) |
| 11 | SIGSEGV | 终止 | 段错误(非法内存访问) |
| 14 | SIGALRM | 终止 | 定时器信号 |
| 17 | SIGCHLD | 忽略 | 子进程停止或终止 |
发送信号
- 内核操作:更新目标进程上下文中的状态字段,表示信号已发送但尚未处理。
- 发送原因:内核检测到系统事件(如 SIGFPE、SIGCHLD),其他进程调用
kill()请求发送信号。 - 系统模型:多个用户级进程(A、B、C),内核维护每个进程的信号状态。
- 信号状态分类:待决信号(Pending)和阻塞信号(Blocked)。
- 待决信号(Pending):已发送但未处理的信号,如进程 C 收到一个待决信号。
- 阻塞信号(Blocked):进程暂时屏蔽某些信号,即使信号到达也不立即处理,保持在待决状态直到解除阻塞。

接收信号
接收信号的行为:当内核强制进程对信号做出反应时,该进程即接收到信号。
- 可能的反应方式包括:忽略信号(不做任何操作)、终止进程(可选生成核心转储)、通过执行用户级函数(信号处理程序)来捕获信号
控制流转移过程:类似于硬件异常处理程序响应异步中断。
- 进程接收到信号
- 控制转移到信号处理程序
- 信号处理程序运行
- 处理完成后返回原指令流的下一条指令

待处理与阻塞信号
待处理信号:信号在已发送但未被接收时为"待处理"状态,每种类型最多只能有一个待处理信号。
- 信号不会排队,若某类型k的信号已处于待处理状态,则后续同类型的信号将被丢弃。
- 阻塞信号机制:进程可以阻止某些信号的接收,被阻塞的信号可以被发送,但不会被接收,直到解除阻塞,每个待处理信号最多只被接收一次。
- 某些信号无法被阻塞(如SIGKILL、SIGSTOP),或仅在来自其他进程时才可被阻塞(如SIGSEGV、SIGILL)
待处理/阻塞位图
- 内核为每个进程的上下文中维护两个位图:pending 和 blocked
- pending 位图:表示当前待处理的信号集合,发送某类型k信号时,内核设置 pending 中第k位,接收该信号后,清除 pending 中第k位。
- blocked 位图(信号掩码):表示被阻塞的信号集合,可通过 sigprocmask 函数设置或清除。


- 每个进程属于且仅属于一个进程组
- getpgrp():获取当前进程所属的进程组ID
- setpgid():更改进程的进程组
- kill:向指定进程或进程组发送任意信号。
/bin/kill -9 24818:向进程24818发送 SIGKILL 信号。/bin/kill -9 -24817:向进程组24817中的所有进程发送 SIGKILL。- 启动 forks 程序生成多个子进程(同一进程组)。
- 使用 kill 终止整个进程组后,ps 显示这些进程均已消失。
c
linux> ./forks 16
Child1: pid=24818 pgrp=24817
Child2: pid=24819 pgrp=24817
linux> ps
PID TTY TIME CMD
24788 pts/2 00:00:00 tcsh
24818 pts/2 00:00:02 forks
24819 pts/2 00:00:02 forks
24820 pts/2 00:00:00 ps
linux> /bin/kill -9 -24817
linux> ps
PID TTY TIME CMD
24788 pts/2 00:00:00 tcsh
24823 pts/2 00:00:00 ps
linux>
从键盘发送信号
- 按下 Ctrl-C:内核向前台进程组的所有作业发送 SIGINT 信号(默认行为:终止进程)。
- 按下 Ctrl-Z:发送 SIGTSTP 信号(默认行为:暂停/挂起进程)。
- 前台进程组(20)中的进程会响应键盘输入,后台进程组不受影响。

接收信号机制
- 假设内核正从异常处理程序返回,并准备将控制权交还给进程 p p p。

信号处理程序作为并发流
- 信号处理程序是一个独立的逻辑流(非独立进程),与主程序并发执行,但生命周期仅限于处理程序执行期间。


嵌套信号处理程序
- 多层中断场景:主程序先接收到信号 s,跳转至 Handler S,在 Handler S 执行期间又接收到信号 t,跳转至 Handler T,Handler T 返回后回到 Handler S,Handler S 完成后返回主程序。

阻塞与解除阻塞信号
- 隐式阻塞机制:内核自动阻塞正在处理的信号类型,例如:SIGINT 处理程序执行期间不会再被另一个 SIGINT 中断。
- 显式阻塞机制,使用 sigprocmask 函数进行显式控制。
支持函数
- sigemptyset:创建空信号集
- sigfillset:将所有信号加入集合
- sigaddset:添加特定信号到集合
- sigdelset:从集合中删除特定信号