推荐译法:
| 英文 | 中文 |
|---|---|
| exception | 异常 |
| interrupt | 中断 |
| fault | 故障 |
| error | 错误 |
| handle, handler | 处置、处置器 |
| reap | 收割 |
| concurrent | 并发的 |
| parallel | 并行的 |
| abort | 终止 |
| exit | 退出 |
| stop, suspend | 停止、暂停 |
| terminate | 结束 |
理解 ECF 有助于
当处理器检测到某个事件发生时,它会将 PC 设为存储在异常表 (exception table) 中的某个地址。 该地址指向用于响应该事件的某个系统子程序,即异常处置器 (exception handler)。 此机制被称为间接过程调用 (indirect procedure call)。
| 异常(间接过程调用) | 函数(普通过程调用) | |
|---|---|---|
| 返回地址 | 可能为当前指令或下一条指令的地址 | 总是下一条指令的地址 |
| 压栈内容 | 可能包括其他处理器状态 | 调用者负责保存的寄存器 |
| 栈所有者 | 系统内核 | 用户程序 |
| 执行模式 | 内核模式(访问无限) | 用户模式(访问受限) |
syscall 指令调用系统子程序。syscall 的下一条指令 $I_\text{next}$。abort 以终止当前程序的执行。| 编号 | 描述 | 分类 |
|---|---|---|
| 0 | 除法溢出 | 故障 |
| 13 | 非法访存 | 故障 |
| 14 | 页面故障 | 故障 |
| 18 | 硬件错误 | 终止 |
| 32~255 | 操作系统定义的异常 | 中断、陷阱 |
syscall 的 x86-64 指令。 rax 存储系统调用编号。rdi, rsi, rdx, r10, r8, r9 依次存储第一到六个实参。| 编号 | 名称 | 描述 |
|---|---|---|
| 0 | read | |
| 1 | write | |
| 2 | open | |
| 3 | close | |
| 4 | stat | 获取文件信息 |
| 9 | mmap | 将内存页面映射到文件 |
| 12 | brk | 重设堆顶 |
| 32 | dup2 | 复制文件描述符 |
| 33 | pause | 暂停进程,等待信号 |
| 37 | alarm | 安排闹钟信号的发送 |
| 39 | getpid | |
| 57 | fork | |
| 59 | execve | |
| 60 | _exit | 终止进程 |
| 61 | wait4 | 等待某个进程终止 |
| 62 | kill | 向某个进程发送信号 |
逻辑控制流 (logical control flow):简称逻辑流,是指由程序计数器的值构成的序列(即指令地址构成的序列)。
该机制使得当前程序看上去像是独占了处理器。
并发流 (concurrent flow):运行时间有重叠的多个逻辑流。
该机制使得当前程序看上去像是独占了存储器。
应用程序的进程,启动时处于用户模式;要变为内核模式,只能通过异常。
Linux 允许用户模式的进程通过 /proc 文件系统访问内核数据结构的内容,如
/proc/cpuinfo 表示处理器信息/proc/PID/maps 表示某个进程的内存映射抢占 (preempt):暂停
调度 (scheduling):操作系统内核决定是否暂停当前进程、恢复之前被抢占的进程。
上下文切换 (context switch):
可能发生于
readsleep系统调用发生错误时,通常返回 -1 并将整型全局变量 errno 设为错误编号。
原则上,系统调用返回时都应检查是否发生了错误:
if ((pid = fork()) < 0) {
fprintf(stderr, "fork error: %s\n", strerror(errno));
exit(0);
}
其中 strerror(errno) 返回 errno 的字符串描述。
利用错误报告函数 (error-reporting function)
void unix_error(char *msg) { /* Unix-style error */
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}
可将上述系统调用及错误检查简化为
if ((pid = fork()) < 0)
unix_error("fork error");
更进一步,本书作者提供了一组错误处置封装 (error-handling wrapper)。 其中每个封装的形参类型与相应的原始函数一致,只不过将函数名的首字母改为大写:
/* csapp.c */
pid_t Fork(void) {
pid_t pid;
if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
使用时只需一行代码:
#include "csapp.h"
pid = Fork();
每个进程都有一个唯一的由正整数表示的进程身份 (process ID, PID)。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); // 当前进程的 PID
pid_t getppid(void); // parent's PID
其中 pid_t 为定义在 sys/types.h 中的整数类型,Linux 将其定义为 int。
进程可能处于运行 (running)、暂停 (stopped)、结束 (terminated) 三种状态之一。
结束进程:
#include <stdlib.h>
void exit(int status);
在亲进程 (parent process) 中创建子进程 (child process):
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
子进程刚被创建时,几乎与亲进程有相同的上下文(用户级虚拟内存空间、已打开文件的描述符)。
函数 fork() 有两个返回值:在子进程中返回 0,在亲进程中返回子进程的 PID。
进程图 (process graph):
#include "csapp.h"
int main() { Fork(); Fork(); printf("hello\n"); exit(0); }
僵尸 (zombie):已结束 (terminated) 但未被收割 (reap) 的进程。
init 的 PID 为 1,是所有进程的祖先。它负责在亲进程结束时,收割其僵尸子进程。
⚠️ shell 等生存期较长的进程,应当主动收割其子进程。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
默认(即 options == 0 时)行为:暂停当前进程,直到等待集 (wait set) 中的某个子进程结束,返回该子进程的 PID。
pid > 0 ,则等待集只含以 pid 为 PID 的子进程。pid == -1 ,则等待集由该亲进程的所有子进程组成。options 可设为以下值或它们的位或 (bitwise OR) 值:
WNOHANG 立即返回(若被等待的子进程未结束,则返回 0)。WUNTRACED 等待某个子进程结束或暂停。WCONTINUED 等待某个子进程结束,或某个暂停的子进程被 SIGCONT 信号恢复。若 statusp != NULL,则会向其写入 status 的值。 status 的值不应直接使用,而应当用以下宏 (macro) 解读:
WIFEXITED(status) 返回:被等待子进程是否正常结束 WEXITSTATUS(status) 返回:被等待子进程的退出状态WIFSIGNALED(status) 返回:被等待子进程是否因信号而结束 WTERMSIG(status) 返回:导致被等待子进程结束的信号WIFSTOPPED(status) 返回:被等待子进程是否因信号而暂停 WSTOPSIG(status) 返回:导致被等待子进程暂停的信号更多宏可用 man waitpid 命令查询。
errno 设为 ECHILD 并返回 -1。errno 设为 EINTR 并返回 -1。wait 函数waitpid(-1, &status, 0) 的简化版本:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statusp);
waitpid 实例乱序版本:
#include "csapp.h"
#define N 2
int main() {
int status, i;
pid_t pid;
/* parent 创建 N 个 children */
for (i = 0; i < N; i++)
if ((pid = Fork()) == 0)
exit(100+i); /* child 立即结束 */
/* parent 乱序收割这 N 个 children */
while ((pid = waitpid(-1, &status, 0)) > 0) {
if (WIFEXITED(status))
printf("child %d terminated normally with exit status=%d\n",
pid, WEXITSTATUS(status));
else
printf("child %d terminated abnormally\n", pid);
}
if (errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
有序版本:
#include "csapp.h"
#define N 2
int main() {
int status, i;
pid_t pid[N], retpid;
for (i = 0; i < N; i++)
if ((/* 存入数组 */pid[i] = Fork()) == 0)
exit(100+i);
while ((retpid = waitpid(pid[i++]/* 遍历数组 */, &status, 0)) > 0) {
if (WIFEXITED(status))
printf("child %d terminated normally with exit status=%d\n",
retpid, WEXITSTATUS(status));
else
printf("child %d terminated abnormally\n", retpid);
}
if (errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
#include <unistd.h>
unsigned int sleep(unsigned int secs);
该函数让当前进程暂停几秒。若暂停时间已到,则返回 0;否则(收到中断信号),返回剩余秒数。
#include <unistd.h>
int pause(void);
该函数让当前进程暂停至收到中断信号,总是返回 -1。
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
该函数将 filename 所表示的程序加载到当前进程的上下文中,再运行之(将 argv 与 envp 转发给该程序的 main() 函数,再移交控制权)。若未出错,则不返回(由被加载的 main() 结束进程);否则,返回 -1。
其中 argv 与 envp 都是以 NULL 结尾的(字符串)指针数组。
argv 为命令行参数列表,argv[0] 为可执行文件的名称(可以含路径)。envp 为环境变量列表,每个元素具有 name=value 的形式。environ 指向 envp[0],又因 envp 紧跟在 argv 后面,故 &argv[argc] + 8 == &envp[0] == environ。环境变量操纵函数:
#include <stdlib.h>
char *getenv(const char *name); // 返回 value
int setenv(const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);
【shell】交互式的命令行终端,代表用户运行其他程序。
sh = (Bourne) SHellcsh = (Berkeley UNIX) C SHellbash = (GNU) Bourne-Again SHellzsh = Z SHellShell 运行其他程序分两步完成:
fork 出一个子进程,再在其中用 execve 运行 argv[0] 所指向的程序。若命令行以 & 结尾,则在后台 (background) 运行(shell 不等其结束);否则,在前台 (foreground) 运行(shell 等待其结束或暂停)。
main#include "csapp.h"
#define MAXARGS 128
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);
int main() {
char cmdline[MAXLINE];
while (1) { /* 读入命令行 */
printf("> "); /* 提示符 */
Fgets(cmdline, MAXLINE, stdin);
if (feof(stdin))
exit(0);
eval(cmdline); /* 解析命令行 */
}
}
evalvoid eval(char *cmdline) {
char *argv[MAXARGS];
char buf[MAXLINE];
int bg; /* 是否在后台运行 */
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv); /* 将 buf 解析为 argv */
if (argv[0] == NULL)
return; /* 忽略空行 */
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) { /* 创建子进程 */
if (execve(argv[0], argv, environ) < 0) { /* 在子进程中运行 */
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
if (!bg) {
int status;
if (waitpid(pid, &status, 0) < 0) /* 收割前台子进程 */
unix_error("waitfg: waitpid error");
}
else
printf("%d %s", pid, cmdline);
}
return;
}
builtin_commandint builtin_command(char **argv) {
if (!strcmp(argv[0], "quit")) /* 支持 quit 命令 */
exit(0);
if (!strcmp(argv[0], "&")) /* 忽略只含 & 的命令行 */
return 1;
return 0; /* 非内置命令 */
}
parselineint parseline(char *buf, char **argv) {
char *delim;
int argc;
int bg;
buf[strlen(buf)-1] = ' '; /* 将换行符替换为空格 */
while (*buf && (*buf == ' ')) /* 忽略行首空格 */
buf++;
/* 构造 argv */
argc = 0;
while ((delim = strchr(buf, ' ')/* 找到第一个空格 */)) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' '))
buf++;
}
argv[argc] = NULL;
if (argc == 0) /* 忽略空行 */
return 1;
/* 是否在后台运行 */
if ((bg = (*argv[argc-1] == '&')) != 0)
argv[--argc] = NULL;
return bg;
}
信号 (signal):
在 Linux 系统下,可以用 man 7 signal 命令查阅完整信号列表,其中最常用的信号如下:
| 编号 | 名称 | 含义 |
|---|---|---|
| 2 | SIGINT | INTerrupt from keyboard |
| 3 | SIGQUIT | QUIT from keyboard |
| 4 | SIGILL | ILLegal instruction |
| 6 | SIGABRT | ABoRT signal from abort() |
| 8 | SIGFPE | Floating-Point Exception |
| 9 | SIGKILL | KILL program |
| 11 | SIGSEGV | SEGmentation fault |
| 14 | SIGALRM | ALaRM |
| 17 | SIGCHLD | CHiLD terminated or stopped |
| 18 | SIGCONT | CONTinue if stopped |
| 19 | SIGSTOP | STOP signal not from terminal |
| 20 | SIGTSTP | SToP signal from Terminal |
⚠️ SIGKILL 既不能被捕获,又不能被忽略,可用于强制结束进程。
kill() 函数。pending 的位向量 (bit vector) 表示。pending 中的同一个位表示,故同类信号至多有一个待决。blocked 的位向量 (bit vector) 表示。每个进程归属于且仅归属于一个进程组 (process group),后者由一个名为组身份 (group ID, GID) 的正整数来标识。
进程被创建时,继承其 parent 的 GID。
#include <unistd.h>
pid_t getpgrp(void); /* 返回:当前进程的 GID */
int setpgid(pid_t pid, pid_t gid/* gid ? gid : getpgrp() */);
/bin/kill 命令kill -signal_name pid ... # e.g. /bin/kill -KILL 15213
kill -signal_number pid ... # e.g. /bin/kill -9 15213
pid > 0,则向 PID 为 pid 的单一进程发送信号。pid < 0,则向 GID 为 -pid 的所有进程发送信号。任务 (job):执行某一行命令所产生的一个或多个进程。
% 作为前缀。组合键
Ctrl + C 向前台任务(进程组)发送 SIGINT 信号,默认使其结束。Ctrl + Z 向前台任务(进程组)发送 SIGTSTP 信号,默认使其暂停。kill 函数委托内核向其他(一个或多个)进程发送信号:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig/* 可以用 SIGKILL 等信号名称 */);
pid > 0,则向 PID 为 pid 的单一进程发送信号。pid < 0,则向 GID 为 -pid 的所有进程发送信号。pid == 0,则向 GID 为 getpgrp() 的所有进程发送信号。alarm 函数委托内核在若干秒后向当前进程发送 SIGALARM 信号。
#include <unistd.h>
unsigned int alarm(unsigned int secs);
若有尚未走完的闹钟,则返回剩余秒数并取消之;否则返回零。
内核在将某进程从内核模式切换为用户模式时,会检查位向量 pending & ~blocked 所表示的信号集。
各种信号都有默认处置器,完成以下行为之一:
SIGCONT 信号到达。某种信号当前使用的处置器,可以被系统自带的(用 SIG_IGN 忽略信号、用 SIG_DFL 恢复默认行为)或用户编写的处置器(函数指针)替换:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
sigprocmask(how, set, oldset) 设置,其中 how 可以是 SIG_BLOCK,效果为 blocked |= set SIG_UNBLOCK,效果为 blocked &= ~set SIG_SETMASK,效果为 blocked = set #include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
信号处置器编写困难的主要原因:
errno(入口处备份、出口处恢复)。volatile 声明可能被改变的全局变量。 sio_atomic_t 声明全局标签(第 0 条)。 同类信号不排成队列,因此可能被遗漏。
void handler1(int sig) {
int olderrno = errno;
if ((waitpid(-1, NULL, 0)) < 0) /* ⚠️ 只收割一个 */
Sio_error("waitpid error");
Sio_puts("Handler reaped child\n");
errno = olderrno;
}
void handler2(int sig) {
int olderrno = errno;
while (waitpid(-1, NULL, 0) > 0) /* ✅ 收割所有 */
Sio_puts("Handler reaped child\n");
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
⚠️ while 中的 waitpid() 不能用本书作者提供的封装 Waitpid() 替换。
#include <signal.h>
#include "csapp.h"
handler_t *Signal(int signum, handler_t *handler) {
struct sigaction action, old_action;
action.sa_handler = handler; /* 安装并固定处置器 */
sigemptyset(&action.sa_mask); /* 只屏蔽同一类信号 */
action.sa_flags = SA_RESTART; /* 尽量重启系统调用 */
if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}
竞争 (race):处置器与主函数读写同一变量的顺序不确定。
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
Sigfillset(&mask_all);
while ((pid = Waitpid(-1, NULL, 0)) > 0) {
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid); /* It may be called BEFORE the corresponding `addjob`. */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv) {
int pid;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Signal(SIGCHLD, handler);
initjobs();
while (1) {
if ((pid = Fork()) == 0) {
Execve("/bin/date", argv, NULL);
}
/* SIGCHLD may arrive here */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
addjob(pid); /* It may be called AFTER the corresponding `deletejob`. */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
exit(0);
}
⚠️ 亲进程执行 Sigprocmask() 前,子进程可能已经结束,从而可能导致 handler() 中的 deletejob(pid) 早于 addjob(pid) 被执行,这将破坏数据结构。
int main(int argc, char **argv) {
int pid;
sigset_t mask_all, mask_one, prev_one;
Sigfillset(&mask_all);
Sigemptyset(&mask_one); Sigaddset(&mask_one, SIGCHLD); /* only SIGCHLD */
Signal(SIGCHLD, handler);
initjobs();
while (1) {
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
if ((pid = Fork()) == 0) {
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD in child */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Block all */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD in parent */
}
exit(0);
}
volatile sig_atomic_t pid;
int chld_handler(int s) {
int olderrno = errno;
pid = Waitpid(-1, NULL, 0);
errno = olderrno;
}
int main () {
sigset_t mask, prev;
Signal(SIGCHLD, sigchld_handler);
Signal(SIGINT, sigint_handler);
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD);
while (1) {
Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
if (Fork() == 0) /* Child */
exit(0);
/* Parent */
pid = 0;
/* Wait for SIGCHLD to be received */
while (!pid)
Sigsuspend(&prev); /* Temporarily unblock SIGCHLD */
Sigprocmask(SIG_SETMASK, &prev, NULL); /* Optinally unblock SIGCHLD */
/* 错误一:消耗资源
Sigprocmask(SIG_SETMASK, &prev, NULL);
while (!pid)
;
*/
/* 错误二:可能在检查 pid 后、运行 Pause 前收到 SIGCHILD
Sigprocmask(SIG_SETMASK, &prev, NULL);
while (!pid)
Pause();
*/
/* 错误三:等待时间太长
Sigprocmask(SIG_SETMASK, &prev, NULL);
while (!pid)
sleep(1);
*/
/* Do some work after receiving SIGCHLD */
printf(".");
}
exit(0);
}
其中 Sigsuspend(&prev) 是对系统调用 sigsuspend(&prev) 的封装:
int Sigsuspend(const sigset_t *set) {
int rc = sigsuspend(set); /* always returns -1 */
if (errno != EINTR)
unix_error("Sigsuspend error");
return rc;
}
其效果相当于以下三条语句的原子化版本:
Sigprocmask(SIG_BLOCK, &prev, &mask);
Pause();
Sigprocmask(SIG_SETMASK, &mask, NULL);
#include <stdio.h>
#include <setjmp.h>
#include <stdnoreturn.h>
jmp_buf buffer;
noreturn void a(int count) {
printf("a(%d) called\n", count);
longjmp(buffer, count+1/* setjmp 的返回值 */);
}
int main(void) {
volatile int count = 0; // 在 setjmp 中被修改的变量必须是 volatile
if (setjmp(buffer) != 5)
a(++count);
}
运行过程:
setjmp(buffer) 返回多次,且返回值不能存储于变量中。setjmp(buffer) 将当前进程的上下文存储于 buffer 中,以 0 为其(第一次)返回值。longjmp(buffer, count+1) 根据 buffer 恢复上下文,以 count+1 为 setjmp 的(第二至五次)返回值。运行结果:
a(1) called
a(2) called
a(3) called
a(4) called
void foo() {
if (...)
throw std::out_of_range("...");
}
void bar() {
try {
...
} catch (std::out_of_range& e) {
...
} catch {
throw;
}
}
strace# options
-e trace=syscall_set
--trace=syscall_set
-e signal=set
--signal=set
# e.g.
strace --trace=read,write --signal=SIGINT,SIGTSTP /bin/ls ~
ps打印当前所有(含僵尸)进程的信息(PID、TTY、TIME、CMD),并返回。
ps -l # 列出与当前 shell 有关的进程
ps aux # 列出系统内所有用户的所有进程
pstree -u PID # 以树的形式列出各用户的进程
top动态打印各进程的资源(CPU、内存)消耗,按 q 返回。
top -o [cpu|mem|pid] # 按 CPU(默认)、内存、PID 排序
pmap打印某进程的内存映射。
/procLinux 系统供用户读取内核信息的虚拟文件系统。