推荐译法:
英文 | 中文 |
---|---|
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):
可能发生于
read
sleep
系统调用发生错误时,通常返回 -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); /* 解析命令行 */
}
}
eval
void 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_command
int builtin_command(char **argv) {
if (!strcmp(argv[0], "quit")) /* 支持 quit 命令 */
exit(0);
if (!strcmp(argv[0], "&")) /* 忽略只含 & 的命令行 */
return 1;
return 0; /* 非内置命令 */
}
parseline
int 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
打印某进程的内存映射。
/proc
Linux 系统供用户读取内核信息的虚拟文件系统。