CPU 随着时间顺序执行不同的指令, from startup to shutdown.
两个可以在 program state 修改 control flow
但是,对于计算机而言,需要system state的 control flow 的切换: Exceptional Control Flow
用户程序在运行中,抛出异常,程序的控制流转移到 Kernel Code,内核中的 Exception Handler 可以确定下一步的状态:

在内核代码中,具体的异常处理指令的跳转指令是固定的,具体由一张 Exception Tables 进行查询。
上述的异常机制叫做同步异常,程序在运行 user code 的过程中遇到异常,将程序的控制流直接转移到内核代码。而异步异常是由外部硬件事件随机触发、与当前执行的指令无关的中断信号,CPU 必须在”任意时刻”暂停当前程序去处理它。
例如:
在汇编代码中,系统会直接使用 syscall 代表进行 system calls,%rax 存储了 sys-call 的编号(每一个编号对应不同 sys-call 的操作,例如读文件,写文件没打开关闭文件,创建/终止/杀死一个进程,运行一个程序等等)
在操作系统的视角,sys_call 被特定的函数封装。
movl 之后抛出了 Page Fault 的异常错误,并在 Kernel 将 Page 复制到主存中,并实现 return and re-execute movl.Def: A process is an instance of a running program.
Process provides each program with 2 abstractions:
进程是资源分配的最小单位(内存、文件、FD 等),线程是CPU 调度的最小单位(执行流)。
实际计算机的运行过程是多进程的,并行的运行很多进程:
在多核 CPU 上,可能存在多个 CPU + registers,context switching 的情况仍然也会发生,每一个核都可以执行一个单独的进程。
并发的定义: Flows overlap in time.
在并发的过程中,CPU 可能会中断执行 Process A 的运行,在中途运行 Process B, 则两个进程之间是并发的,因为从时间上两个进程之间产生了 Overlap.
Otherwise: sequential
在并行执行的进程之间,不同的 Process 之间如何切换?通过内核代码实现 Context Switch.

System Call 利用返回值来判断执行的状态是否成功。
On error, Linux system-level func&ons typically return ‐1 and set global variable errno to indicate cause.
例如,在 C 代码中 fork 一个新的进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <errno.h>
int main() {
pid_t pid;
if ((pid = fork()) < 0) {
// fork 失败
fprintf(stderr, "fork error: %s\n", strerror(errno));
exit(1);
}
else if (pid == 0) {
printf("Hello from CHILD process!\n");
printf("Child PID: %d\n", getpid());
printf("Parent PID: %d\n", getppid());
exit(0);
}
else {
printf("Hello from PARENT process!\n");
printf("Parent PID: %d\n", getpid());
printf("Child PID: %d\n", pid);
wait(NULL);
printf("Child process finished.\n");
}
return 0;
}
系统级编程和用户级编程的理念存在很大的差异,系统级编程中,内核中任何微小的 bug 都可能会被快速放大,造成严重的错误,因此,对于那些内核函数,一旦检测到返回值为异常返回值,就会立刻中断程序。而对于用户级编程而言,存在更加高级易用的异常处理机制。
当然,操作系统封装了对应的操作函数,也自带了 Error Handling 的机制。
exit functions: returning void创建进程的方式可以调用 fork 函数:
通过 Fork 创建的子进程在初始化时会复制父进程的大部分上下文:
写时复制(COW) 是一种资源管理优化技术:多个进程共享同一块内存,只有当某个进程试图“修改”这块内存时,系统才会真正复制一份新的给它。是一种延迟复制来减少资源占用的机制。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0) {
fprintf(stderr, "Fork error: %s\n", strerror(errno));
exit(0);
}
return pid;
}
int main(){
pid_t pid;
int x = 1;
pid = Fork();
if (pid == 0){
// child
printf("Child : x=%d\n", ++x);
exit(0);
}
// parent
printf("Parent : x=%d\n", --x);
exit(0);
}
Parent : x=0
Child : x=2
stdout is the same in both parent and child
我们使用一个树来建模不同进程之间的依赖关系:
对上述的进程树进行拓扑排序,可以得到一个启动进程的序列。
void fork_2(){
printf("L0\n");
fork();
printf("L1\n");
fork();
printf("L2\n");
fork();
printf("L3\n");
}
例如,该程序的运行结果如下:
L0
L1
L1
L2
L2
L2
L2
L3
L3
L3
L3
L3
L3
L3
L3
每一次 fork,父进程都会创建一个新的子进程,同时父进程本身也持续执行代码,因此,在 3 此 fork 之后,就会存在 8 个正在执行的进程。
void fork_4() {
printf("L0\n");
if (fork() != 0) {
// * 只对父进程进行
printf("L1\n");
if (fork() != 0) {
// 只对父进程执行
printf("L2\n");
}
}
printf("L3\n");
}
L0
L1
L3
L2
L3
L3
无论进程何时终止,内核会在回收阶段回收已经终止的进程。当子进程结束时,操作系统不会立刻把它的所有信息都删掉。它会保留一点点信息,直到它的父进程来查看。
Reap 的方式: 父进程调用 wait() 函数
init process) 的进程接管并释放因此,总结一下:
wait: Synchronizing with Childrenwait 函数是一个同步函数,父进程在调用之后,父进程的运行状态被阻塞,并等待任意一个子进程结束,在结束后父进程恢复执行,并且子进程的资源被内核释放。
void fork_9_nonwait() {
if (fork() == 0) {
printf("L0\n");
exit(0);
} else {
printf("L1\n");
printf("L2\n");
}
printf("L3\n");
}
void fork_9_wait() {
int child_status;
if (fork() == 0) {
printf("L0\n");
exit(0);
} else {
printf("L1\n");
wait(&child_status);
printf("L2\n");
}
printf("L3\n");
}
=== fork_9_nonwait
L1
L2
L3
L0
=== fork_9_wait
L1
L0
* L2 和 L3 被阻塞,优先等待子进程运行完成
L2
L3
waitpid 函数会等待一个指定的子进程终止。execev: Loading and Running Programs这个函数可以在当前进程中读取并运行一个程序:
int execve(char *filename, char *argv[], char *envp[])
三个参数:
execve 不会创建新进程,而是替换当前进程的代码、数据和栈。
// 可以是二进制可执行文件
execve("/bin/ls", ...);
// 也可以是脚本文件(以 #! 开头)
execve("./script.sh", ...); // 文件首行:#!/bin/bash
// 例如执行:ls -l /home
char *argv[] = { "ls", "-l", "/home", NULL };
execve("/bin/ls", argv, envp);
// 约定:argv[0] 是程序名本身
execve 只会被调用一次,并且不会有返回值。

在调用 execve 之后,当前的 PID 的数据段和代码会被替换:
libc_start_main 的栈帧main 函数的栈帧在实际运行时,通常会选择新建一个 child,在一个 child process 中运行 execev 函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main() {
pid_t pid;
int status;
// 1. 创建子进程
pid = fork();
if (pid < 0) {
// fork 失败
fprintf(stderr, "Fork failed: %s\n", strerror(errno));
exit(1);
}
else if (pid == 0) {
// ============================================
// 子进程代码
// ============================================
printf("[Child %d] Starting...\n", getpid());
printf("[Child %d] Current directory: ", getpid());
// 获取并打印当前工作目录
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s\n", cwd);
}
// 准备 execve 的参数
// 相当于执行:ls -l
char *filename = "/bin/ls";
char *argv[] = { "ls", "-l", NULL }; // argv[0] 约定为程序名
char *envp[] = { NULL };
printf("[Child %d] Executing: %s\n", getpid(), filename);
execve(filename, argv, envp);
fprintf(stderr, "[Child %d] execve failed: %s\n", getpid(), strerror(errno));
exit(127);
}
else {
printf("[Parent %d] Child created with PID %d\n", getpid(), pid);
printf("[Parent %d] Waiting for child to complete...\n", getpid());
// 4. 等待子进程结束并回收资源
pid_t wpid = wait(&status);
// 5. 检查子进程退出状态
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("[Parent %d] Child %d exited normally with status %d\n",
getpid(), wpid, exit_code);
if (exit_code == 0) {
printf("[Parent %d] ✅ Child completed successfully!\n", getpid());
} else {
printf("[Parent %d] ⚠️ Child completed with error code %d\n",
getpid(), exit_code);
}
}
else if (WIFSIGNALED(status)) {
printf("[Parent %d] Child %d was killed by signal %d\n",
getpid(), wpid, WTERMSIG(status));
}
printf("[Parent %d] All done, exiting.\n", getpid());
}
return 0;
}
[Parent 32680] Child created with PID 32682
[Parent 32680] Waiting for child to complete...
[Child 32682] Starting...
[Child 32682] Current directory: /Users/xiyuanyang/Desktop/Dev/CSAPP
[Child 32682] Executing: /bin/ls
total 40
-rw-r--r--@ 1 xiyuanyang staff 97 Feb 18 21:25 Dockerfile
-rw-r--r--@ 1 xiyuanyang staff 4223 Mar 17 00:02 README.md
drwxr-xr-x@ 67 xiyuanyang staff 2144 Mar 24 12:13 build
drwxr-xr-x@ 8 xiyuanyang staff 256 Mar 24 08:29 docs
-rw-r--r--@ 1 xiyuanyang staff 975 Jan 9 17:05 makefile
-rw-r--r--@ 1 xiyuanyang staff 32 Jan 9 14:04 run.sh
drwxr-xr-x@ 4 xiyuanyang staff 128 Feb 18 21:29 scripts
drwxr-xr-x@ 28 xiyuanyang staff 896 Mar 21 16:56 slides
drwxr-xr-x@ 17 xiyuanyang staff 544 Mar 24 10:31 src
[Parent 32680] Child 32682 exited normally with status 0
[Parent 32680] ✅ Child completed successfully!
[Parent 32680] All done, exiting.
Shell: an application program that runs programs on behalf of the user:
shtcsh/cshbash/zshint main() {
char cmdline[MAXLINE];
while (1) {
// read
fgets(cmdline, MAXLINE, stdin);
if (feof(stdin)) {
exit(0);
}
// evaluate
eval(cmdline);
}
}
Shell 执行的功能是和用户的字符串输入进行交互,并将对应的指令解析、发送到操作系统内核。
cmdline 进行解析:
argv 是根据空格解析出的数组argv[0] 是程序的名称,剩下的都是后缀和参数
execve(argv[0], argv, environ)&,这样子进程就会放入后台运行
&,但是父进程的后续进行需要等待子进程的完成(这也是一般的 shell 交互逻辑)
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLINE 100
#define MAXARGS 100
extern char **environ;
void unix_error(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0) {
fprintf(stderr, "Fork error: %s\n", strerror(errno));
exit(0);
}
return pid;
}
int parseline(char *buf, char **argv) {
char *delim; /* Points to first space delimiter */
int argc; /* Number of args */
int bg; /* Background job? */
buf[strlen(buf) - 1] = ' '; /* Replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* Ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
while ((delim = strchr(buf, ' '))) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* Ignore spaces */
buf++;
}
argv[argc] = NULL;
if (argc == 0) /* Ignore blank line */
return 1;
/* Should the job run in the background? */
if ((bg = (*argv[argc - 1] == '&')) != 0)
argv[--argc] = NULL;
return bg;
}
int builtin_command(char **argv) {
if (!strcmp(argv[0], "quit")) /* quit command */
exit(0);
if (!strcmp(argv[0], "&")) /* Ignore singleton & */
return 1;
return 0; /* Not a builtin command */
}
void eval(char *cmdline) {
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return; /* Ignore empty lines */
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) { /* Child runs user job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminate */
if (!bg) {
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
} else
printf("%d %s", pid, cmdline);
}
return;
}
int main() {
char cmdline[MAXLINE];
while (1) {
// read
fgets(cmdline, MAXLINE, stdin);
if (feof(stdin)) {
exit(0);
}
// evaluate
eval(cmdline);
}
}
bash run.sh src/Lecture14/shellex.c
/bin/ls
build docs README.md scripts src
Dockerfile makefile run.sh slides
/bin/pwd
/Users/xiyuanyang/Desktop/Dev/CSAPP
/usr/bin/whoami
xiyuanyang
quit
可以看到,我们写了一个非常简单的 eval 函数,可以实现 shell 的最基本的一些功能。
Signal: a small message that nofifies a process that an event of some type has occured in the systems:
Kernel sends (delivers) a signal to a destination process by updating some state in the context of the destination process. Linux 系统没有额外设计一套复杂的内核和进程之间的通信机制来进行 signals 的传输和接受,内核并没有向进程的内存空间写入任何数据,也没有打断进程当前的指令流。内核只是在该进程对应的 进程控制块(PCB,Linux 中是 task_struct) 中,将某个比特位(bit)从 0 改为 1。