[TOC]

异常控制流

现代系统通过使控制流发生突变来对系统状态的变化做出反应 。一般而言 ,我们把这些突变称为异常控制流 (Exceptional Control Flow, ECF) 。

异常

异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。

image-20240210180910491

异常 (exception) 就是控制流中的突变,用来响应处理器状态中的某些变化。

当处理器检测到有事件发生时,它就会通过一张叫做异常表 (exception table)的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序 (exception handler)) 。

异常处理

image-20240212222613370

异常处理程序运行在内核模式下,这意味它们对所有的系统资源都有完全的访问权限。

异常的类别

image-20240210192934372

同步异步表示的结果的获取方式是主动获取还是被动接收;阻塞非阻塞表示的是获取这个动作是否可以立即返回不用等待。

中断

中断是异步发生的, 是来自处理器外部的 I/0 设备的信号的结果。例如定时器计时结束后会向处理器发送一个中断。

image-20240215162205029

陷阱和系统调用

陷阱是有意的异常,是执行一条指令的结果 。如读文件(read),创建一个新的进程(fork)。

image-20240215163333455

故障

故障由错误情况引起,它可能能够被故障处理程序修正。

一个经典的故障示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。

image-20240215163341288

终止

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM 或者SRAM 位被损坏时发生的奇偶错误。

Linux/86-64 系统调用

image-20240215164416422

进程

  • 异常是允许操作系统内核提供进程 (process) 概念的基本构造块。

  • 进程的经典定义就是一个执行中程序的实例。

  • 系统中的每个程序都运行在某个进程的上下文 (context) 中。

  • 上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的通用目的寄存器的内容程序计数器环境变量以及打开文件描述符的集合.

  • 每次用户通过向 shell 输入 个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。

进程提供给应用程序的关键抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
  • 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

逻辑控制流

进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占 (preempted) (暂时挂起),然后轮到其他进程。

image-20240215165111954

并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流 (concurrent flow), 这两个流被称为并发地运行。

多个流并发地执行的一般现象被称为并发 (concurrency)

一 个进程和其他进程轮流运行的概念称为多任务(multitasking)

一个进程执行它的控制流的一部分的每一时间段叫做时间片 (time slice)

因此,多任务也叫做时间分片 (time slicing)

私有地址空间

x86-64:

image-20240215170927300

用户模式和内核模式

处理器将某个控制寄存器中的一个模式为(mode bit)来区别进程运行在用户模式还是内核模式(超级用户模式)。

/proc文件系统它允许用户模式进程访问内核数据结构的内容。

/sys文件系统,它输出关于系统总线和设备的额外的低层信息。

上下文切换

操作系统内核使用一种称为上下文切换 (context switch) 的较高层形式的异常控制流来实现多任务。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度 (scheduling), 是由内核中称为调度器 (scheduler) 的代码处理的。

  1. 保存当前进程的上下文
  2. 恢复某个先前被抢占的进程被保存的上下文
  3. 将控制传递给这个新恢复的进程

由于从磁盘中读取文件需要几十毫秒,当进程A阻塞(需要的文件未读取完成),cpu会去执行其他进程,当读取完成时,磁盘发送中断异常到cpu,继续执行指令。

image-20240215172337812

系统调用错误处理

Unix 系统级函数遇到错误时,它们通常会返回-1, 并设詈全局整数变量errno示什么出错了。

1
2
3
4
5
void unix_error(char *msg) /* 错误处理包装函数 */
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(0);
}

Fork

1
2
3
4
5
6
7
8
pid_t Fork(void)
{
pid_t pid;

if ((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}

进程控制

获取进程ID

每个进程都有一个唯一的正数进程 ID(PID)getpid()通过函数返回调用进程的 PID。getppid()函数返回它的父进程的 PID( 创建调用进程的进程)。

1
2
3
4
#include <sys/types.h> 
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

getppid ()函数返回一个类型为 pid_t 的整数值,在 Linux 系统上它在types.h中被定义为int

创建和终止进程

  • 进程总是处于运行、停止和终止三种状态。

fork函数

子进程返回0,父进程返回子进程的PID,如果出错则返回-1。

  • 调用一次,返回两次
  • 并发执行
  • 相同但是独立的地址空间
  • 共享文件

可以通过进程图来理解带有嵌套的fork函数

image-20240215183725431

回收子进程

  • 当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收 (reaped) 。当父进程回收己终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃己终止的进程,从此时开始,该进程就不存在了。

  • 一个终止了但还未被回收的进程称为僵死进程 (zombie)

  • 如果一个父进程终止了,内核会安排 init进程成为它的孤儿进程的养父。
  • init进程PID=1, 是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。

waitpid

1
2
3
#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);

当等待集合中的进程终止后返回终止进程pid。

  1. 判定等待集合的成员

    如果pid>0, 那么等待集合就是一个单独的子进程,它的进程 ID 等于 pid。
    如果pid= -1, 那么等待集合就是由父进程所有的子进程组成的。

  2. 修改默认行为

image-20240215201704669

  1. 检查已回收子进程的退出状态

image-20240215201738218

  1. 错误条件

如果调用进程没有子进程,那么 waitpid 返回-1, 并且设置errnoECHILD。如果waitpid 函数被一个信号中断,那么它返回-1, 并设置 errnoEINTR

  1. wait函数
1
2
3
#include <sys/types.h> 
#include <sys/wait.h>
pid_t wait(int *statusp);

waitpid的简化版。等价于waitpid(-1,&status,0)

  1. 使用waitpid的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* $begin waitpid2 */
#include "csapp.h"
#define N 2

int main()
{
int status, i;
pid_t pid[N], retpid;

/* Parent creates N children */
for (i = 0; i < N; i++)
if ((pid[i] = Fork()) == 0) /* Child */ //fork进程并存储对应pid
exit(100+i);

/* Parent reaps N children in order */
i = 0;
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);
}
/* $end waitpid2 */

让进程休眠

sleep函数将一个进程挂起一个指定时间。

1
2
3
#include <unistd.h> 
unsigned int sleep(unsigned int secs);
int pause(void);

时间到了,返回0;时间未到,返回剩余时间。

pause函数使调用函数休眠,直到该进程收到一个信号。

加载并运行程序

1
2
#include <unistd.h> 
int execve(const char *filename, const char *argv[], const char *envp[]);

execve调用一次从不返回,execve 函数在当前进程的上下文中加载并运行一个新的程序 它会覆盖当前进程的地址空间,但并没有创建一个新进程 。

argv 变量指向一个以 null 结尾的指针数组,其中每个指针都指向一个参数字符串。按照惯例, argv [0] 是可执行目标文件的名字。

envp 变量指向一个以null 结尾的指针数组,其中每个指针指向一个环境变量字符串,每个串都是形如”name=value” 的名字-值对。

image-20240215204034862

main 函数有3个参数:

  1. argc, 它给出 argv[ ]数组中非空指针的数量
  2. argv, 指向argv[ ]数组中的第一个条目。
  3. envp, 指向 envp[]数组中的第一个条目。

image-20240215204911421

linux可以通过getenvsetenv来修改环境变量。

程序与进程

进程是执行中程序的一个具体的实例;程序总是运行在某个进程的上下文中。

利用fork和execve运行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/* $begin shellmain */
#include "csapp.h"
#define MAXARGS 128

/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main()
{
char cmdline[MAXLINE]; /* Command line */

while (1) {
/* Read */
printf("> ");
Fgets(cmdline, MAXLINE, stdin); //反复读取参数
if (feof(stdin))//C 库函数 int feof(FILE *stream) 测试给定流 stream 的文件结束标识符。
exit(0); //当设置了与流关联的文件结束标识符时,该函数返回一个非零值,否则返回零。

/* Evaluate */
eval(cmdline);
}
}
/* $end shellmain */

/* $begin eval */
/* eval - Evaluate a command line */
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;
}

/* If first arg is a builtin command, run it and return true */
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 */
}
/* $end eval */

/* $begin parseline */
/* parseline - Parse the command line and build the argv array */
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;
}
/* $end parseline */

信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。

软件形式的异常,称为 Linux 信号,它允许进程和内核中断其他进程。

1
man signal   //信号手册

image-20240216134615426

信号术语

  • 发送信号
  • 接受信号
  • 待处理信号,在任何时刻,一种类型至多只会有一个待处理信号,多余的会被丢弃。
  • 一个待处理信号最多只能被接收一次。内核为每个进程在pending 位向量中维护着待处理信号的集合,而在 blocked 位向量(也称为信号掩码signal mask)中维护被阻塞的信号集合

image-20240216135351733

发送信号

  1. 进程组

    1
    2
    3
    4
    5
    6
    7
    8
    #include <unistd.h> 
    pid_t getpgrp(void); //返回:调用进程的进程组 ID

    #include <unistd.h>
    int setpgid(pid_t pid, pid_t pgid); //返回:若成功则为 0, 若错误则为-1


    setpgid(0, 0); //会创建一个新的进程组,若进程名为15213,那么其进程组 ID 15213, 并且把进程 15213 加入到这个新的进程组中。

    如果 pid是0, 那么就使用当前进程PID 如果 pgid是0, 那么就用pid指定的进程的 PID 作为进程组 ID 。

1
2
ps -ef //常用
ps -ajx //查看父进程ID,进程ID,进程组ID,会话ID
  1. 使用 /bin/kill 程序发送信号
1
2
3
4
linux> /bin/kill -9 15213  
//发送信号 9(SIGKILL) 给进程 15213 。
linux> /bin/kill -9 -15213
//发送 SIGKILL 信号给进程组 15213 中的每个进程。
  1. 从键盘发送信号

Unix shell 使用作业 (job) 这个抽象概念来表示为对一条命令行求值而创建的进程,任何时刻,至多只有一个前台作业和 0个或多个后台作业

image-20240216142444242

Ctrl+C 内核发送一个 SIGINT 信号到前台进程组中的每个进程。默认情况下,结果是终止前台作业。
Ctrl+Z 发送一个 SIGTSTP信号到前台进程组中的每个进程。默认情况下,结果是停止(挂起)前台作业。
  1. 用kill函数发送信号
1
2
3
#include <sys/types.h> 
#include <signal.h>
int kill(pid_t pid, int sig); //返回:若成功则为0, 若错误则为- 1,
  • pid>0,发送给进程pid。
  • pid=0,发送给调用进程所在进程组的每个进程。
  • pid<0,发送给进程组|pid|中的每个进程

父进程用kill函数发送 SIGKILL 信号给它的子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "csapp.h"

int main()
{
pid_t pid;

/* Child sleeps until SIGKILL signal received, then dies */
if ((pid = Fork()) == 0) {
Pause(); /* Wait for a signal to arrive */
printf("control should never reach here!\n");
exit(0);
}

/* Parent sends a SIGKILL signal to a child */
Kill(pid, SIGKILL);
wait(pid);
printf("SIGKILL!@!"); //确认子进程终止
exit(0);
}
  1. 用alarm函数发送信号

进程可以通过调用 alarm 函数向它自己发送 SIGALRM 信号

1
2
3
#include <unistd.h> 
unsigned int alarm(unsigned int secs);
//返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0

接收信号

当内核把进程 从内核模式切换到用户模式时,它会检查进程的未被阻塞的待处理信号的 (pending & ~blocked)

  • 如果这个集合为空(通常情况下),那么内核将控制传递到 的逻辑控制流中的下一条指令
  • 如果集合是非空的,那么内核选择集合中的某个信号,并且且强制进程接收信号,收到这个信号会触发进程采取某种行为, 一旦进程完成了这个行为,那么控制就传递回进程的逻辑控制流中的下一条指令。

每个信号类型都有一个预定义的默认行为。

  • 进程终止
  • 进程终止并转储内存。
  • 进程停止(挂起)直到被 SIGCONT 信号重启。
  • 进程忽略该信号。

可以通过使用 signal 函数修改和信号相 联的默认行为,唯一的例外是 SIGSTOPSIGKILL,它们的默认行为是不能修改的。

1
2
3
4
#include <signal.h> 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//返回:若成功则为指向前次处理程序的指针,若出错则为 SIG_ERR(不设置 errno)

signal 函数可以通过设置handeler来改变和信号 signum 相关联的行为:

  • 如果handler SIG_IGN, 那么忽略类型为 signum 的信号。
  • 如果 handler SIG_DFL, 那么类型为 signum 的信号行为恢复为默认行为
  • 否则, handler 就是用户定义的函数的地址,这个函数被称为信号处理程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "csapp.h"

void sigint_handler(int sig) /* SIGINT handler */
{
printf("Caught SIGINT!\n");
exit(0);
}

int main()
{
/* Install the SIGINT handler */
if (signal(SIGINT, sigint_handler) == SIG_ERR)
unix_error("signal error");
pause();
return 0;
}

image-20240216154617280

练习题8.7

编写一个叫做 snooze 的程序,它有一个命令行参数,用这个参数调用
练习题 8. 中的 snooze 函数,然后终止。编写程序,使得用户可以通过在键盘上输入Ctrl+C 中断 snooze 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void handler(int sig)
{
return;
}

unsigned int snooze(unsigned int secs) {
unsigned int rc = sleep(secs);

printf("Slept for %d of %d secs.\n", secs-rc, secs);
return rc;
}


int main(int argc, char **argv) {

if (argc != 2) {
fprintf(stderr, "usage: %s <secs>\n", argv[0]);
exit(0);
}

if (signal(SIGINT, handler) == SIG_ERR)
unix_error("signal error\n");
(void)snooze(atoi(argv[1]));
exit(0);
}

阻塞和解除阻塞信号

隐式阻塞机制。内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。

显式阻塞机制。应用程序可以使用 sigprocmask 函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。

sigprocmask 函数改变当前阻塞的信号集合 (blocked 位向量)。

1
2
3
4
5
6
7
8
9
#include <signal.h> 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); //检查或修改进程的信号掩码
int sigemptyset(sigset_t *set); //初始化 set 为空集合。
int sigfillset(sigset_t *set); //把每个信号都添加到set中。
int sigaddset(sigset_t *set, int signum);//把 signum 添加到 set
int sigdelset(sigset_t *set, int signum);//从set中删除signum
//返回:如果成功则为0, 若出错则为-1
int sigismember(const sigset_t *set, int signum);
//返回:若 signum set 的成员则为 1, 如果不是则为0若出错则为—1

具体行为以来how的值

  • SIG_BLOCK: set 中的信号添加到 blocked (blocked=blocked I set)
  • SIG_ UNBLOCK: blocked 中删除 set 中的信号 (blocked=blocked &~set)
  • SIG_SETMASK: block=set

编写信号处理程序

  1. 安全的信号处理
  • 处理程序要尽可能简单。

  • 在处理程序中只调用异步信号安全的函数。(可重入的且不能被信号处理程序中断)。

异步信号安全的函数

image-20240216222621366

信号处理程序中产生输出唯一安全的方法是使用write函数。

  • 保存和恢复errno

在进入处理程序时把 errno 保存在某个局部变最中,在处理程序返回前恢复它。

  • 阻塞所有的信号,保护对共享全局数据结构的访问

  • 用volatile 声明全局变量

volatile限定符强迫编译器每次在代码中引用时,都要从内存中读取值。

  • sig_atomic_t 声明标志。

对sig_atomic_t声明的变量的读和写会是原子的(不可中断的)。

  1. 正确的信号处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

void handler1(int sig)
{
int olderrno = errno; // 保存和恢复 errno

if ((waitpid(-1, NULL, 0)) < 0)//-1表示由父进程创建的所有子进程
sio_error("waitpid error");
Sio_puts("Handler reaped child\n");
Sleep(1);
errno = olderrno;
}

int main()
{
int i, n;
char buf[MAXBUF];

if (signal(SIGCHLD, handler1) == SIG_ERR)//=设置SIGCHLD的信号处理程序
unix_error("signal error");

/* Parent creates children */
for (i = 0; i < 3; i++) {
if (Fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
exit(0);
}
}

/* Parent waits for terminal input and then processes it */
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
unix_error("read");

printf("Parent processing input\n");
while (1)
;

exit(0);
}

由于信号处理函数没有解决信号不会排队等待这样的情况。当接受并捕获第一个信号进入处理程序,第二个信号就传送并添加到了待处理信号集合里。然而,因为 SIGCHLD 信号被 SIGCHLD理程序阻塞了,所以第二个信号就不会被接收。第三个信号到达。有了一个待处理的 SIGCHLD, 第三个信号会被丢弃。

通过修改handler函数来修正这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

void handler2(int sig)
{
int olderrno = errno;

//使得每次 SIGCHLD 处理程序被调用时,回收尽可能多的僵死子进程。
while (waitpid(-1, NULL, 0) > 0) {
Sio_puts("Handler reaped child\n");
}
if (errno != ECHILD)
Sio_error("waitpid error");
Sleep(1);
errno = olderrno;
}

  1. 可移植的信号处理

Posix 标准定义了 sigaction 函数,它允许用户在设置信号处理时,明确指定他们想要的信号处理语义。

同步流以避免讨厌的并发错误

  • 通过设置和解除阻塞的SIGCHLD信号来避免进程在添加进job组前终止导致把不存在的子进程添加到作业列表中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "csapp.h"

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) { /* Reap a zombie child */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); //防止重入
deletejob(pid); /* Delete the child from the job list */
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, mask_one, prev_one;

Sigfillset(&mask_all); //把每个信号添加到mask_all
Sigemptyset(&mask_one); //初始化mask_one为空集合
Sigaddset(&mask_one, SIGCHLD); // 把SIGCHLD添加到mask_one
Signal(SIGCHLD, handler); //设置SIGCHLD异常处理函数
initjobs(); /* Initialize the job list */

while (1) {
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
if ((pid = Fork()) == 0) { /* Child process */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
}
exit(0);
}

显示地等待信号

  • linux shell创建一个前台作业时,在接收下一条用户命令之前,它必须等待作业终止,被SIGCHLD处理程序回收。
1
2
3
4
5
6
7
8
9
10
#include <signal.h>
int sigsuspend(const sigset_t *mask);

//sigsuspend 函数暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,其行为要么是运行一个处理程序,要么是终止该进程。如果它的行为是终止,那么该进程不从sigsuspend 返回就直接终止。如果它的行为是运行一个处理程序,那么sigsuspend 从处理程序返回,恢复调用sigsuspend 时原有的阻塞集合。sigsuspend 函数等价于下述代码的原子的(不可中断的)版本:

//等价于下面三条语句的原子版本

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "csapp.h"

volatile sig_atomic_t pid;

void sigchld_handler(int s)
{
int olderrno = errno;
pid = Waitpid(-1, NULL, 0);
errno = olderrno;
}

void sigint_handler(int s)
{
}

int main(int argc, char **argv)
{
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);

/* Wait for SIGCHLD to be received */
pid = 0;
while (!pid)
Sigsuspend(&prev); //设置掩码,然后挂起进程

/* Optionally unblock SIGCHLD */
Sigprocmask(SIG_SETMASK, &prev, NULL);

/* Do some work after receiving SIGCHLD */
printf(".");
}
exit(0);
}

非本地跳转

  • 通过longjmpsetjmp实现。
1
2
3
4
5
6
#include <setjmp.h>
int setjmp(jmp_buf env); //返回0
int sigsetjmp(sigjmp_buf env, int savesigs); //返回非0
void longjmp(jmp_buf env, int retval); //从不返回。
void siglongjmp(sigjmp_buf env, int retval); //从不返回。

  • 类似于 C 语言中的 try-catch 块,在函数调用链中实现了异常的传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "csapp.h"

sigjmp_buf buf;

void handler(int sig)
{
siglongjmp(buf, 1);
}

int main()
{
if (!sigsetjmp(buf, 1)) {
Signal(SIGINT, handler);
Sio_puts("starting\n");
}
else
Sio_puts("restarting\n");

while(1) {
Sleep(1);
Sio_puts("processing...\n");
}
exit(0); /* Control never reaches here */
}
linux> ./restart
starting
processing
processing
Ctrl+C
restarting
processing


操作进程的工具

  • STRACE: 打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的学生而言,这是一个令人着迷的工具。用 -static 编译你的程序,能得到一个更干净的、不带有大量与共享库相关的输出的轨迹。
  • PS: 列出当前系统中的进程(包括僵死进程)。
  • TOP: 打印出关千当前进程资源使用的信息,HTOP更加详细。
  • PMAP: 显示进程的内存映射
  • /proc: 一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容,用户程序可以读取这些内容 比如,输入 “cat /proc/loadavg”, 可以看到你的 Linux 系统上当前的平均负载。

小结

​ 异常控制流 (ECF) 发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制

  • 在硬件层 ,异常是由处理器中的事件触发的控制流中的 突变。控制流 传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
    有四种不同类型的异常:中断、故障、终 止和陷阱。

  • 在操作系统层,内核用 ECF 提供进程的基本概念 进程提供给应用两个重要的抽象: 1) 逻辑控制流,它提供给每个程序一个假象,好像它是在独占地使用处理器, 2) 私有地址空间,它提供给每个程序一个假象,好像它是在独占地使用主存

  • 在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待它们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号。信号处理的语义是微妙的,并且随系统不同而不同。然而, 在与 Posix 兼容的系统上存在 一些机制,允许程序 清楚 地指定期望的信号处理语义。

  • 最后 ,在应用层, 程序可以使用非本地跳转来规避正常的调用 返回栈规则,并且直接从一个函数分支到另一个函数。