异常控制流 - CS:APP 第8章

异常控制流 - CS:APP 第8章

这一章更多的是实践,理论的部分实在不多

本文将介绍:

  1. 常见的异常
  2. (Linux)部分并发编程

一系列的指令组成的流叫做控制流,现在的操作系统通过使控制流突变来对异常情况作出反应,这种突变就叫做异常控制流。

异常及其分类

  1. 中断 (interrupt) : 异步发生,是来自处理器外部I/O设备的信号的结果

  2. 陷阱 (trap) : 同步发生,是故意造成的异常,最重要的用途是实现用户程序的系统调用。用户程序通过syscall 指令,造成一个到异常处理程序的陷阱。

    系统调用和普通函数调用的区别:普通函数调用运行在用户模式,系统调用发生在内核模式

  3. 故障 (fault):可能被处理程序修复的异常,比如内存不命中导致的缺页

  4. 终止 (abort):不可恢复的错误。

Linux 进程

进程的定义:一个执行中的程序实例

进程提供给程序两个抽象的概念:

  1. 一个独立的逻辑控制流,它提供一个假象,使我们觉得我们的程序独占地使用处理器。
  2. 一个私有的地址空间,它提供一个假象,使我们觉得我们的程序独占地使用存储器系统。

区分并发和并行:

并发流(concurrency flow):一个逻辑流的执行在时间是与另一个逻辑流重合

并发(concurrency):多个逻辑流并发地执行,它是一种思想,包括的范围更大

并行流(parallel flow):两个逻辑流并发地运行在两个处理器或计算机上

获取进程id

1
2
3
4
5
6
#include <sys/types.h>
#include <unistd.h>

// 在linux上,pid_t 被定义为 int
pid_t getpid(void); // 获取当前进程pid
pid_t getppid(void); // 获得父进程pid

进程只有三种状态:

  1. 运行。进程要么在CPU上执行,要么在等待被执行且最终会被调度。
  2. 暂停。进程的执行被挂起(suspended),且不会被调度。当收到SIGSTOP、SIGTSTP、SIDTTIN或者SIGTTOU信号时,进程就暂停,并且保持暂停直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。(信号是一种软件中断的形式,将在8.5节中给予描述。)
  3. 终止。进程永远地停止了。进程会因为三种原因终止:
    1. 收到一个信号,该信号的默认行为是终止进程
    2. 从主程序返回
    3. 调用exit函数。

创建进程:

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
/*
该函数创建一个进程,子进程和父进程的进程组相同(如果不可以改变)
子进程与父进程的虚拟地址空间相同,文件描述符相同
该函数返回两次:
在父进程中返回创建子进程的pid
在子进程中返回0
可以依此区分两个进程
*/

回收僵死进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options);
/*
pid 等待集合
pid > 0 一个单独的子进程
pid = -1 父进程的所有子进程

statusp 导致返回的状态信息
options 选项

return :
成功回收:回收进程的pid
WNOHANG 返回0
其他错误 -1 (例如没有子进程):
如果没有子进程,该函数返回-1,并设置全局变量errno为ECHILD
如果挂起的进程被信号中断(经过我测试,当信号到达,转到处理程序时,waitpid并不会返回):返回-1, 设置errno为EINTR

典型用法:waitpid(-1, NULL, 0);
挂起该进程,等待该进程的子进程终止,回收该僵死进程
*/

pid_t wait(int *statusp); // 等价于waitpid(-1, &statusp, 0);

让进程休眠:

1
2
3
4
#include <unistd.h>

unsigned int sleep(unsigned int secs); // 挂起进程secs秒,如果因为信号中断则会提前返回
int pause(void); // 挂起进程,因为进程中断会提前返回

加载并运行程序

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>

// filename 可执行文件路径,argv参数列表,envp环境变量
int execve(const char *filename, const char *argv[], const char *envp[]);
/*
execve调用一次,从不返回,除非找不到可执行文件文件
argv/envp 指向以NULL结尾的指针数组

特别强调:
通过fork创建的子进程将会继承父进程处置(disposition)的副本,在执行execve时,所有信号的处置将会重置为默认值,但是对于是否阻塞信号不会修改,因为在执行execve时将保留信号掩码
*/

信号

信号是一种软件层次的异常

发送信号的两个原因:

  1. 内核检测到一个系统事件,如子进程终止,发送SIGCHLD
  2. 一个进程显式地调用了kill函数,向某个进程发送了信号

一个进程接受到的信号,被放在一个位向量里,当收到一个信号,该位被设置为1,如果再收到一个信号,由于该位已经被设置为1,因此,这个信号会被简单地丢弃。因此:

当我们设计信号处理程序时,我们必须假设,再该信号之前,已经有不止一个该类型的信号已经到达,我们必须处理尽可能处理多的信号。

为了能方便向大量进程发送信号,Linux提供了进程组这个概念。

进程组相关:

1
2
3
4
5
6
7
8
9
#include <unistd.h>

pid_t getpgrp(void); // 获取当前进程的进程组
int setpgid(pid_t pid, pid_t pgid); // 设置pid的进程组
/*
如果pid为0,则设置当前的进程
如果pgid为0,则用pid指定的PID作为进程组id
setpgid(0, 0); 即使当前进程独立出原进程组,使用当前的PID作为进程组ID
*/

发送信号

  1. /bin/kill 程序可以手动发送信号

  2. 在键盘上输入ctrl-c,发送SIGINT信号到shell。.shell捕获该信号(参见8.5.3节),然后发送SGT信号到这个前台进程组中的每个进程。在默认情况中,结果是终止前台作业。类似地,输入ctrl-z会发送一个SIGTSTP信号到shell,shell捕获这个信号,并发送SIGTSTP信号给前台进程组中的每个进程。在默认情况下,结果是暂停(挂起)前台作业。

  3. kill函数
  4. alarm函数
1
2
3
4
#include <unistd.h>

unsigned int alarm(unsigned int secs);
// secs秒后,发送一个SIGALRM信号给调用进程

接受处理信号:

1
2
3
4
5
6
7
8
9
10
#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler handler);
/*
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
1. 如果handler是SG_IGN,那么忽略类型为signum的信号。
2. 如果handler是SIG_DF凡L,那么类型为signum的信号行为恢复为默认行为。
3. 否则,handler就是用户定义的函数的地址,称为信号处理程序(signal handler),只要进程接收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。信号处理程序的调用被称为捕捉信号。信号处理程序的执行被称为处理信号。
*/

阻塞 & 解除阻塞信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <signal.h>

int sigprocmask(int how, sigset_t *set, sigset *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int setdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);
/*
对于sigprocmask的how,有三个选项:
SIG_BLOCK 将set中的信号添加到阻塞集合
SIG_UNBLOCK 将set中的信号从阻塞信号中删除
SIG_SETMASK 将block = set
*/

非本地跳转

1
2
3
4
5
6
7
#include <setjmp.h>

int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

void longjmp(jmp_buf env, int retval);
void siglongjmp(sigjmp_buf env, int retrval);

附:Linux 配置CSAPP库

参考:

https://blog.csdn.net/ustc_sse_shenzhang/article/details/105744435

https://zhuanlan.zhihu.com/p/76930507

https://blog.csdn.net/jakejohn/article/details/79825086

第三版CSAPP库 代码下载:http://csapp.cs.cmu.edu/3e/code.html

我们将csapp.c 库编译成动态库

使用gcc -shared -fpic csapp.c -o libcsapp.so -lphread

得到 libcsapp.so 将它移动到/lib

接着将csapp.h 移动到 /usr/local/include

编译问使用CSAPP动态库时,只需要使用 gcc main.c -o prog -lcsapp

其中编译选项-lxxx 代表告诉GCC去/lib等文件夹下寻找 libxxx.so 与其链接

我们在编译csapp.c的时候,用的编译选项,-lphread 就是告诉编译器与libphread.so库链接,这个库存放与线程相关的代码

以后打包静态库时,我们也要记住,动态库的命名规则是libxxx.so

术语索引

抢占(preempted) 暂时挂起

并发流(concurrency flow):一个逻辑流的执行在时间是与另一个逻辑流重合

并发(concurrency):多个逻辑流并发地执行,它是一种思想,包括的范围更大

并行流(parallel flow):两个逻辑流并发地运行在两个处理器或计算机上

挂起(suspended)

阻塞信号:信号被阻塞不等于信号被丢弃,只是没有被处理