Exceptional Control Flow¶
ECF 是操作系统和硬件用来“强行打断并重定向程序执行”的统一机制。
并发、进程、系统调用、信号,本质上都是 ECF 的不同表现形式。
ECF 是什么?¶
程序的执行顺序偏离了正常的顺序执行(顺序、分支、函数调用/返回),转而由硬件或操作系统强制改变控制流的一种机制。
ECF 发生在各个层面:硬件(中断、异常),OS(进程切换、系统调用),application(信号、fork、longjmp)
四种异常类型:
-
Interrupts - 异步(与当前执行的指令无关),外部 IO
操作系统如何处理硬件中断?
After the current instruction finishes executing, the processor notices that the interrupt pin has gone high, reads the exception number from the system bus, and then calls the appropriate interrupt handler. When the handler returns, it returns control to the next instruction
-
Faults - 同步,比如缺页 Page Fault,Segmentation Fault
-
Aborts - 同步,不可恢复的严重异常
-
Traps - 同步,由当前正在执行的指令主动触发的异常,通常用于系统调用 system call(如 read, write)
进程¶
进程是正在执行的程序 + 执行上下文。
内核用 ECF 给应用提供两个核心抽象:
-
Logical Control Flow(逻辑控制流)
每个进程都有自己的控制流,内核通过 context switch 或信号、异常来打断和切换控制流。这样,即使 CPU 只有一个核,程序也能看起来并发执行。
-
Private Address Space(私有地址空间)
每个进程都有自己的虚拟地址空间(code, data, heap, stack),一个进程访问另一个进程的内存会被内核阻止(Memory Protection),当发生 ECF(比如 page fault)时,内核可以安全地切换到其他进程。
Concurrency 并发¶
并发的本质是控制流在时间上发生重叠,且会在不可预测的点被中断与重调度。
并发的定义:
1.9.2
We use the term concurrency to refer to the general concept of a system with multiple, simultaneous activities, and the term parallelism to refer to the use of concurrency to make a system run faster.
8.2.2
A logical flow whose execution overlaps in time with another flow is called a concurrent flow, and the two flows are said to run concurrently. More precisely, flows X and Y are concurrent with respect to each other if and only if X begins after Y begins and before Y finishes, or Y begins after X begins and before X finishes.
Notice that the idea of concurrent flows is independent of the number of processor cores or computers that the flows are running on. If two flows overlap in time, then they are concurrent, even if they are running on the same processor. However, we will sometimes find it useful to identify a proper subset of concurrent flows known as parallel flows. If two flows are running concurrently on different processor cores or computers, then we say that they are parallel flows, that they are running in parallel, and have parallel execution.
如果两个任务 interval overlap,即 X 在 Y 开始和结束之间开始;或 Y 在 X 的开始和结束之间开始,就称为 concurrent
concurrency 可以发生在一个核,也可以发生在多个核。
单核上的并发来自内核在多任务间快速切换时间片(time-sharing),看起来像多个任务同时进行;多核上的并发即在多个核上的满足并发的定。如果 concurrent flow 发生在多个核上,那么就是 parallel flow
parallelism:多核同时 (the exact same instant) 运行任务,是真正的物理上同时执行。
concurrency 模拟了多个任务在逻辑上同时进行,但物理上不是“同时”。
parallel flow 是 concurrent flow 的子集。并行一定是并发;并发不一定并行。
Context Switch¶
Context Switch 做了什么?
- saves the context of the current process
- restores the saved context of some previously preempted process
- passes control to this newly restored process.
Fork¶
比如启动一个 shell 执行新的命令,操作系统必须提供机制“从现有进程复制出一个新进程”。这时候,我们就需要 fork.
调用 fork 后,系统会创建一个几乎完全一样的新进程,新进程和父进程有独立的地址空间。
Fork:
- Call once, return twice
- concurrent execution. 先哪个是 nondeterministic 的
- The child gets an identical (but separate) copy of the parent’s user-level virtual address space, including the code and data segments, heap, shared libraries, and user stack.
fork() 的返回值:
- if ((pid = Fork()) == 0) /* Child */
- In the child, fork returns a value of 0.
- Parents return the PID of child
zombie process
当一个 process 因为如正常退出、出错等原因结束时,内核不会立刻把它从系统中完全删除,而是会暂时保留这个进程的信息,直到 parent process reap 收割它. Terminated but yet not reaped by parent process is called zombie process.
init process(PID = 1)
是在系统启动时由内核创建的第一个用户态进程,它永远不会结束。 如果一个父 process 自己退出了,它的子 process 就变成了孤儿。那么 init process 就是这些孤儿的 parent init process 可以 reap 这些孤儿 process
waitpid()
pid_t waitpid(pid_t pid, int *statusp, int options);
// Returns: PID of child if OK, 0 (if WNOHANG), or −1 on error
- 第一个参数 pid 如果 > 0 : 只等待指定 PID 的子进程;如果等于 -1,等待任意子进程
- 第二个参数 *statusp 接收子进程的退出状态,可用宏解析
- 比如 WIFEXITED(status)
- 第三个参数 options
Signals¶
在 Linux 中,signal(信号)是一种更高层次的进程上下文切换(process context switch)机制。
在真实系统中,总会发生一些异步事件:
- 子进程突然退出
- 用户按下 Ctrl+C
- 定时器到期
- 进程访问非法内存
这些事件不是进程自己主动发起的,却可能在任何指令之间发生。signal 的作用,正是让内核把这些异步事件“注入”到用户进程的执行流中。
当一个 signal 被递送(delivered)时,内核会:中断当前正在执行的用户态代码,保存当前进程上下文,转而执行一个预先注册的 signal handler。handler 返回后,再恢复原来的执行流。一个常见的例子是,当用户在终端按下 Ctrl+C 时,终端驱动检测到这个按键组合,内核向当前前台进程组(foreground process group)中的所有进程 发送一个 SIGINT 信号。每个进程都会收到 SIGINT。对于没有自定义 handler 的进程:SIGINT 的默认行为是:终止进程。
不过,signal 不是一来就立刻执行 handler,内核在递送信号前,会先检查信号掩码(signal mask):
如果一个信号在被 block 时到,它会被标记为 pending,handler 不会执行。等到信号解除阻塞时,再统一递送。
需要注意的是,signal 是“提醒”,不是“提醒队列”。以 SIGCHLD 为例,子进程退出时,内核发送 SIGCHLD。如果多个子进程在信号被 block 期间退出,内核只记住“发生过 SIGCHLD”,不记录次数。
可能的输出有哪些
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig) {
printf("H\n");
}
int main() {
sigset_t set;
pid_t pid;
signal(SIGCHLD, handler);
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
printf("A\n");
sigprocmask(SIG_BLOCK, &set, NULL);
pid = fork();
if (pid == 0) {
printf("B\n");
_exit(0);
}
printf("C\n");
wait(NULL);
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("D\n");
return 0;
}