深入理解计算机系统_第8章 异常控制流

异常控制流发生在计算机系统的各个层次:
1) 硬件层面, 硬件检测到事件会触发控制,跳转到异常处理程序;
2) 操作系统层面, 内核通过上下文切换,实现进程切换,也是异常控制流;
3) 应用程序层面, 信号的信号处理程序,也是异常控制流;
语言层面的try, catch, throw也是异常控制流;
非本地跳转setjmp, longjmp也是异常控制流;

8.1 异常

异常就是控制流中的突变,用来响应处理器状态的某些变化;
异常事件可能是与当前执行指令相关: 虚拟内存缺页、算术溢出、一条指令试图除以零;
异常事件与当前指令无关:定时器产生的信号、一个IO请求;
8.1.1 异常处理
系统为每种类型的异常分配了唯一一个异常号,有一些异常号由处理器设计者提供, 有一些异常号由操作系统内核的设计者分配;
处理器设计者提供的异常号:零除, 缺页, 内存访问违例,断点, 算术溢出;
操作系统内核提供的异常号: 系统调用, 外部的有关IO设备;
异常表是异常号与异常处理程序的表,通过索引异常号可以得到具体的异常处理程序;异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器中;

异常与过程调用的不同之处:
1) 异常的返回地址要么是当前中断的指令,要么是下一条指令;
2) 处理异常处理程序的时候iu处理器会将一些额外的处理器状态压到栈中。处理程序返回的时候, 重新开始处理被中断的程序可能需要这些状态;
3) 处理异常处理程序的时候,如果控制从用户转换到内核, 处理器的状态被压入到内核栈中,而不是用户栈中;
4) 异常处理程序运行在内核状态下,意味着一场处理程序对所有的系统资源都具有访问权限;

8.1.2 异常的类别
异常卡哇伊分为四类:中断、陷阱、故障、终止;
深入理解计算机系统_第8章 异常控制流_第1张图片
1) 中断
中断是异步发生的,来自处理器外部的IO设备的信号。硬件中断不是由任何一条专门的指令造成的, 所以是异步的。
陷阱、故障、终止都是当前指令执行的结果,所以是同步到;
2) 陷阱和系统调用
陷阱实际上就是系统调用,用户程序经常要向内核请求服务,为了允许用户对内核服务的受控访问, 操作系统提供系统调用接口;
3) 故障由错误引起,肯呢个会被错误处理程序修正。如果故障处理程序能够修正这个故障, 控制就返回到引起故障的指令(当前指令)再次执行;否则故障处理程序就执行内核的abort例程, abort例程会终止引起故障的应用程序;
例如缺页异常;
4) 终止
终止时不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序会直接调用一个abort例程, abort例程会终止这个应用程序;

8.1.3 linux/x86-64系统中的异常

1) Linux/x86-64 故障和终止

  1. 除法错误
    除法错误直接终止程序, linux shell将除法错误报告成浮点异常;(floating execotion)
  2. 一般保护故障
    一个程序引用了未定义的虚拟内存区域, 或者因为程序试图写一个只读的文本段;这些是段故障(segmentation fault);
  3. 缺页异常
    异常处理程序会将适当的磁盘上的一个页面映射到物理内存上,然后返回重新执行引起故障的指令;(实际上就是缓存的机制)
  4. 机器检查
    检测到致命的硬件错误时发生的。

2) Linux/86-64 系统调用
linux有几百个系统调用,每一个系统调用都有唯一一个整数号, 对应于内核中跳转表的偏移量(区别跳转表和异常表);
所有linux系统调用的参数都是通过通用寄存器而不是栈传递的。寄存器%rax 包含系统调用号, 其他6个寄存器包含最多6个参数, %rax 包含返回值;

8.2 进程

每一个进程都有一个上下文, 上下文是程序正确运行时需要的各种状态, 包括内存中的代码和数据,栈、通用目的寄存器, PC, 环境变量, 打开的文件描述符集合;
关于进程的两个抽象:
1) 一个独立的逻辑控制流,好像我们的程序独占处理器资源;
2) 一个私有的地址空间, 好像程序独占内存系统;
8.2.1 逻辑控制流
进程的PC值的序列就是逻辑控制流;

8.2.2 并发流
一个逻辑流动执行时间上与另外一个流重叠,就是并发流;
多个流并发地执行的现象称为并发;
一个进程和其他进程轮流运行的概念称为多任务;
一个进程执行他的控制流的一部分的每一时间段叫做时间片;
并行流是并发流的一个子集,并行流中,不同的逻辑控制流同时运行在不同的处理器上;

8.2.3 私有地址空间
进程为程序提供一种假象,好像进程独自占有系统的地址空间;
和这个地址空间相关联的物理内存中的字节不能被其他进程访问,从这个意义上讲, 这个地址空间是私有的;
不同进程的私有空间有着相同的通用结构:
深入理解计算机系统_第8章 异常控制流_第2张图片

8.2.4 用户模式和内核模式
处理器必须提供一种机制, 限制一个应用可以执行的指令以及他可以访问的地址空间范围;处理器使用一个控制寄存器中的模式位来提供这种功能,设置了该位, 当前进程就具有特权,运行在内核模式;
用户模式的进程不允许执行特权指令, 比如停止处理器、改变模式位,或者发起一个IO操作。 也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据;
linux提供一种机制, 叫做/proc文件系统,允许用户模式的进程访问内核数据结构的内容。/proc文件系统将许多的内核数据结构的内容输出为一个用户可以读到文本文件的层次结构。例如, 使用/proc文件系统找出一般的系统属性(CPU类型, /proc/cpuinfo); 某个特殊的进程使用的内存段(/proc//maps);

8.2.5 上下文切换
上下文包括对对象:
通用目的寄存器, 浮点寄存器, PC, 用户栈, 状态寄存器, 内核栈, 各种内核数据结构(页表, 进程表, 文件表);
发生上下文切换的情形:
1) 调度器决定切换进程的时候;
2) 当内核代表用户执行系统调用时候,可能会发生上下文切换。如果系统调用因为等待某个事件而阻塞, 内核会让当前进程休眠,切换到另一个进程,从而发生上下文切换;
3) 中断也会引起进程切换, 所有系统都有某种产生周期定时器中断的机制(1ms 或是10ms), 每次发生定时器中断, 内核就能够判定当前进程已经运行了足够长的时间,并切换到一个新的进程;

8.3 系统调用错误处理

unix系统级函数遇到错误时候, 通常会返回-1;
使用包装函数处理错误,例如将fork函数包装成Fork函数,在Fork函数中处理错误情况,这会使得主函数中代码简洁;

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

8.4 进程控制

8.4.1 获取进程ID

#include 
#include 
pid_t getpid(void);
pid_t getppid(void);

8.4.2 创建和终止进程
进程的3个状态:
1) 运行,进程要么被执行, 要么在等待被执行且被内核调度;
2) 停止,进程挂起,不会被调度
3) 终止, 进程永远地停止了。进程会停止的三个原因:
1. 收到一个信号,该信号终止进行
2. 从主函数返回
3. 调用exit

子进程与父进程共享代码、数据、对、共享库、用户栈,文件描述符;父子进程唯一不同的就是进程ID;fork函数调用一次返回两次,兵器父子进程并发地执行;
理解调用Fork()函数会产生什么样的进程拓扑结构;
深入理解计算机系统_第8章 异常控制流_第3张图片
8.4.3 回收子进程
当一个进程终止时候, 内核并不会马上将这个进程从系统中清除,而是保持这个进程为一种已终止的状态,知道父进程回收子进程资源。一个已经终止了但是没有被回收的进程是僵尸进程;
父进程使用waitpid回收子进程资源

#include 
#include 
pid_t waitpid(pid_t pid, int *statusp, int options);

默认情况时(options =0), waitpid挂起当前进程, 阻塞等待子进程中一个子进程终止。 如果等待集合中的一个进程在刚调用的时刻之前就已经终止了, waitpid会立即返回。
1) 判定等待集合的成员
pid > 0, 等待一个单独的子进程;
pid = -1, 等待子进程中的一个进程, 默认情况;
2) 修改默认行为, options参数
WNOHANG:不阻塞,没有终止的子进程时候函数立即返回;
WUNTRACED: 挂起调用进程的执行, 直到等待集合中的一个进程变为已终止或者被停止
WCONTINUED:挂起调用进程的执行, 直到等待集合中的一个正在运行的进程终止或者一个被停止的进程收到SIGCONT信号重新开始;
3)检查一回收子进程的退出状态
statusp返回子进程状态信息
WIFEXITED(status):是否正常终止
WEXITSTATUS(status): 返回一个正常终止的子进程的终止状态;
WIFSIGNALED(status) :如果子进程因为一个没有被捕获的信号终止的,就返回真;
WTERMSIG(status):返回导致子进程终止的信号的编号;
WIFSTOPPED(status):如果返回的子进程状态是停止的,为真;
WIFCONTINUED(status):如果返回的子进程是重新启动的,为真;
wait函数是waitpid函数的默认版本
wait(&status) 等价于waitpid(-1, &status, 0);
8.4.4 让进程休眠

unsigned int sleep(unsigned int secs);
int pause(void);

当一个信号打断sleep函数的时候, sleep函数返回剩余的时间;
pause使得调用进程一直等待,直到接收到一个信号;

8.4.5 加载并运行程序
execve函数在当前的进程上下文中加载并运行一个新程序;

int execve(const char *filename, const char *argv[], const char *envp[]);

深入理解计算机系统_第8章 异常控制流_第4张图片
进程栈顶实际内容如下,栈顶顶部是系统启动函数libc_start_main的栈帧;
深入理解计算机系统_第8章 异常控制流_第5张图片
关于环境变量的三个函数,不建议直接操作环境变量environ;

#include 
char *getenv(const char *name);
int setenv(const char *name, const char *newvalue, int overwrite);
void unsetenv(const char *name);

8.4.6 利用fork和execve运行程序

8.5 信号

linux信号, 允许进程和内核通过发送信号中断其他进程。
低层次的硬件异常是由内核的异常处理程序处理的,正常情况下, 对用户进程是不可见的。信号提供了一种机制,通知用户进程发生了这些硬件异常;
例如:
除零,硬件异常:SIGFPE信号;
一个进程执行非法指令: 内核发出SIGILL给这个进程;
非法内存引用: 内核向进程发出SIGSEGV信号;

8.5.1 信号术语

传送一个信号到目的进程由两个不同的步骤组成:
1) 发送信号, 发送信号肯呢个会有两种原因
1. 内核检测到一个系统事件;
2. 一个进程调用kill函数,显式地发送一个信号给目的进程;
2) 接收信号
进程对信号的处理方式有三种:
1. 接受这个信号,执行默认的动作
2. 忽略这个信号
3. 执行自定义的信号处理程序

一个发出但是还没有被接受的信号称为待处理信号。在任何时刻,一种类型的信号最多只有一个待处理信号。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队,这些信号只是简单地被丢弃。
一个进程可以有选择地阻塞接收某种信号。当一种信号被阻塞的时候, 这个信号依旧可以被发送,但是待处理的信号不会被接受,知道进程取消对这种信号的阻塞;
内核为每一个进程在pending位向量中维护着待处理信号的集合;在blocked向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中的k位;只要进程接收了k信号,内核就会清除pending中的k位;

8.5.2 发送信号
1) 进程组
每个进程都只属于一个进程组,进程组由一个正整数进程组ID来标识。getpgrp()函数返回当前进程的进程组ID;
一个进程可以通过使用setpgid函数改变自己或是一个进程的进程组;

pid_t getpgrp(void);
int setpgid(pid_t pid, pid_t pgid);//将进程pid的进程组改为pgid;

2) 用/bin/kill 程序发送信号
使用kill向一个负的进程ID发生信号,这个信号会被发送到这个进程组中所有的进程中;

3) 从键盘发送信号
Ctrl+C:内核发送一个SIGINT信号到前台进程组中的每个进程。默认情况下会终止前台作业;
Ctrl+Z:发送一个SIGTSTP信号给前台进程组的每一个进程。默认情况下会挂起前台作业;
4) 用户程序中使用kill函数发送信号

int kill(pid_t pid, int sig);
#include "csapp.h"
int main()
{
	pid_t pid;
	if((pid = Fork()) == 0){
		Pause();
		printf("control should be never reach here!\n");
		exit(0);
	}

	Kill(pid, SIGKILL);
	exit(0);
}

5) 用alarm函数发送信号
进程可以通过调用alarm函数向自己发送SIGALRM信号。alarm(secs), 内核会在secs秒之后发送一个SIGALRM信号给进程;一个进程只有一个闹钟,因此对alarm的调用都将取消任何待等待的闹钟,并且返回待处理脑子剩余的时间;

8.5.3 接受信号
当内核把进程p从内核模式切换到用户模式的时候, 会检查进程p的未被阻塞的待处理信号集合(pending & ~blocked),如果这个集合为空, 内核就将控制传递到进程p的逻辑控制流的下一条指令。如果这个集合非空, 内核就选择集合中的某个信号k(通常k为最小), 并且强制p接收信号k。收到这个信号会触发进程采取某种行为。一旦进程完成这个行为,那么控制就会传递到进程p的逻辑控制流的下一个指令;
每一个信号都有一个预定义的默认行为:
1) 进程终止
2) 进程终止并转储内存
3) 进程挂起直到被信号SIGCONT信号重启
4) 进程忽略该信号
信号SIGKILL和SIGSTOP的默认行为不能修改;
使用signal函数更改信号处理动作:

#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

1) handler是SIG_IGN, 忽略该信号
2) handler是SIG_DFL, 恢复为默认的信号处理动作
3) handler是用户自定义的函数的地址
当一个进程捕获了信号k,会调用信号k的处理程序。

8.5.4 阻塞和接触阻塞信号
linux阻塞信号的两种机制:
1) 隐式阻塞机制
如果进程正在处理信号s的信号处理程序,此时再一次接收到同一类型的信号s,内核会隐式地阻塞这个信号;

2) 显式阻塞机制
调用sigprocmask函数显式地阻塞某一个信号;

sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddrset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
......
sigprocmask(SIG_SETMASK, &prev_mask, NULL);

8.5.5 编写信号处理程序
信号处理程序的几个属性:
1) 信号处理程序与主程序并发运行,共享同样的全局变量,因此信号处理程序和主程序可能互相干扰;
2) 如何以及何时接收信号的规则常常有违人的直觉;
3) 不同的系统有不同的信号处理语义;

1) 安全的信号处理
如果信号处理程序和主程序并发访问同样的全局数据结构,会导致致命的错误;
一些原则
G0. 处理程序尽量简单,保持处理程序尽可能小和简单;
G1. 在处理程序中只调用异步信号安全的函数。异步信号安全的函数,要么是可重入的,要么不能被信号处理程序中断(原子性的);
信号处理函数中产生位移安全的方法是使用write函数,调用printf, sprintf函数是不安全的。
安全的函数包,SIO:
G2. 保存和恢复errno, 主函数和信号处理程序都可能设置errno,解决办法是进入信号处理程序之前,将errno保存在一个局部变量中, 完成信号处理程序之后再恢复;
G3. 阻塞所有的信号, 保护对共享全局数据结构的访问。如果信号处理程序与主程序共享一个全局数据结构,那么在访问该数据结构的时候, 你的信号处理程序和主程序应该暂时阻塞所有的信号。原因是从主程序中访问一个数据结构d通常需要一系列的指令序列,如果信号处理程序也访问d,会中断主程序中的指令序列。那么信号处理程序发现d的状态不一样,得不到预期的结果;
G4. 使用volatile声明全局变量,例如主函数和信号处理程序共享一个全局变量g, 处理程序更新g, 主函数读取g。编译器优化会将g的值优化到缓存或是寄存器中,尽管g的值变化了, 寄存器中g的值还是没有变化,主函数读取的g一直没变;将g声明为volatile, 编译器会拒绝优化;
volatile关键字定义一个变量, 告诉编译器不要优化这个变量, 不要缓存这个变量。volatile限定符强迫编译器每次在代码中引用g,都要从内存中读取g的值;
G5. 使用sig_atomic_t 声明标志。一般的信号处理程序, 处理程序会写全局标志来记录收到的信号。主程序周期性地读这个标志,响应信号,再清除该标志。 C 语言提供一种整型数据类型sig_atomic_t, 对他的读和写是原子的。原子性的保证只适用单个读或是写,不适用flag++, 或是flag + 10;
总结:使信号处理程序尽可能简单, 调用安全函数, 保存和恢复errno, 保护对共享数据结构的访问, 使用volatile和sig_atomic_t。

volatile sig_atomic_t flag;
ssize_t sio_puts(char s[])
{
	return write(STDOUT_FILENO, s, sio_strlen(s));
}

ssize_t sio_putl(long v)
{
	char s[128];
	sio_ltoa(v, s, 10);
	return sio_puts(s);
}

void sio_error(char s[])
{
	sop_puts(s);
	_exit(1);
}

一个安全的信号处理程序示例:

#include "csapp.h"
void sigint_handler(int sig)
{
	Sio_puts("Caught SIGINT!\n");
	_exit(0);
}

_exit, _Exit 是可重入函数, exit不是可重入函数;

2) 正确的信号处理
未处理的信号是不排队的, 因为pending位向量中每种类型的信号只对应有一位,所有每种类型最多能有一个未处理的信号。如果两个k信号发送给同一个目的进程, 那么第二个信号就会简单地被丢弃。
学习一个实例, 主程序中创建子进程, 子进程然后终止。子进程会向父进程发生信号SIGCHLD,父进程在信号处理程序中处理子进程资源的回收;

void handler1(int sig)
{
	int olderrno = errno;
	if((waitpid(-1, NULL, 0)) < 0)
		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)
		unix_error("signal error");

	for(o = 0; o < 3; i++){
		if(Fork() == 0){
			printf("Hello form child %d\n", (int)getpid());
			exit(0);
		}
	}
	if((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
		unix_error("read error");
		printf(Parent processing input\n");
		while(1);
		exit(0);
}

运行结果如下
深入理解计算机系统_第8章 异常控制流_第6张图片
为什么有三个进程,只对两个进程进行了子进程资源回收?
当父进程接收到一个信号时候,跳转到信号处理程序。 紧接着第二个信号就传达到并被添加到待处理信号集合中,并且会阻塞第二个信号。之后第三个信号到达了,但是由于待处理信号集已经有一个信号了, 第三个信号就会被丢失。
总结:不可以用信号来对其他进程中发生的事件计数;
信号处理程序handler1中,只对一个子进程进行资源回收;
信号处理程序handler2中, 对所有的子进程进行资源回收,就不会出现之前的问题

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");
	Sleep(1);
	errno = olderrno;
}

3) 可移植的信号处理
unix信号处理的一个缺陷是,在不同的系统中有不同的信号处理语义;

  1. signal函数的语义在不同版本下可能不同;
  2. 系统调用可以被中断,对于慢速系统调用,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将errno设置为EINTR。这个情况下, 程序员必须手动重启被中断的系统调用的代码;
    为了解决可移植性问题, POSXI标准定义了sigaction函数, 允许用户这只信号处理程序的时候, 明确指定用户想要的信号处理语义;
int sigaction(int signum, struct sigaction *act, struct sigaction *oldact);

signal 函数是使用sigaction函数实现的,signal函数的语义是:
1) 只有这个信号处理程序当前正在处理的这个同类型的信号才会被阻塞;
2)和所有信号实现一样, 信号不会排队
3) 只要可能, 被中断的系统调用会自动重启
4) 一旦设置了信号处理程序, 就会一直保持, 直到signal函数重新设置信号处理程序;

8.5.6 同步流以避免讨厌的并发错误(同步并发流)
一个例程, 父进程在一个全局作业列表中记录着当前子进程,每个作业就是一个条目, addjob函数和deletejob函数分别向这个作业列表中添加和删除作业;
下面代码存在一个竞争关系,addjob和delete函数之间有竞争;如果addjob函数先运行, 程序没错;如果deletejob函数先运行, 程序出错;

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){
		soigprocmask(SIG_BLOCK, &mask_all, &prev_all);
		deletejob(pid);
		sigpprocmask(SIG_SETMASK, &prev_all, NULL);
	}
	if(errno != ECHILD)
		Sio_error("waitpid error");
}

int main(int argc, char **argv)
{
	int pid;
	sigset_t mask_all, preev_all;
	sigfillset(&mask_all);
	signal(SIGCHLD, handler);
	initjobs();
	while(1){
		if((pid = fork()) == 0){
			execve("/bin/date",  argv, NULL);
		}
		sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
		addjob(pid);
		sigprocmask(SIG_SETMASK, &prev_all, NULL);
	}
	exit(0);
}

一种消除竞争的方法, 在调用fork之前就阻塞SIGCHLD信号,调用addjob之后取消阻塞,由于deletejob函数在信号处理程序之中, 这样就保证了addjob函数先执行;子进程继承了父进程的被阻塞信号集合,所以我们必须保证在调用execve之前, 小心点解除子进程中所有被阻塞的SIGCHLD信号;

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);
		deletejobs(pid);
		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);
	sigemptyset(&mask_one);
	sigaddset(&mask_one, SIGCHLD);
	signal(SIGCHLD, handler);
	initjobs();
	while(1){
		sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
		if((pid = fork()) == 0){
			sigprocmask(SIG_SETMASK, &prev_one, NULL);
			execve("/bin/date", argv, NULL);
		}
		sigprocmask(SIG_BLOCK, &mask_all, NNULL);
		addjobs(pid);
		sigprocmask(SIG_SETMASK, &prev_one, NULL);
	}
	exit(0);
}

8.5.7 显式地等待信号
有时候主程序需要显式等待某个信号处理程序运行。 例如, 当linuxshell创建一个前台作业时候, 在接收下一条用户命令之前, 需要等待作业终止;

一个基本思路, 父进程设置SIGINT 和 SIGCHLD信号处理程序,然后进入一个无限循环,其阻塞SIGCHLD信号。 创建了子进程之后将pid设置为0,取消阻塞SIGCHLD ,然后以循环的方式等待pid为非零。子进程终止时候,处理程序回收子进程,并将非0的PID赋值给pid。这会终止主程序中的循环等待。

#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(iint s)
{}

int main(nt 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);
		if(fork() == 0)
			exit(0);
		pid = 0;
		sigprocmask(SIG_SETMASK, &prev, NULL);
		while(!pid);

		printf(", ");
	}
	exit(0);
}

while(!pid); 这个循环一直在占用CPU,浪费CPU资源,可以使用下面代码替换,当收到一个信号时候, pause就会被中断,所以依旧需要循环;不过上述代码依旧存在竞争,while测试和pause是两个操作不是一个原子性的操作;

while(!pid)
	pause();

合适的解决办法是使用sigsuspend函数, sigsuspend函数暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号(这个信号要么运行一个处理程序, 要么终止该进程)。如果这个信号的行为是终止进程, 那么该进程不从sigsuspend返回就直接终止。如果他的行为是运行一个处理程序, 那么sigsuspend从处理程序返回, 然后恢复原有的阻塞集合;

int sigsuspend(const sigset_t *mask);
//sigsuspend等价于下面函数
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

使用sigsuspend修改后的程序

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);
		if(fork() == 0)
			exit(0);
		pid = 0;
		while(!pid)
			sigsuspend(&prev);
		sigprocmask(SIG_SETMASK, &prev, NULL);
		printf(", ");
	}
	exit(0);
}

8.6 非本地跳转

非本地跳转, 将控制从一个函数转移到另一个正在执行的函数, setjmp函数和longjmp函数;

#include 
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesings);	//信号处理函数中使用

setjmp 在缓冲区env中保存当前调用环境,以便后面的longjmp使用,并返回0;调用环境包括程序计数器, 栈指针, 通用目的寄存器。setjmp的返回值不能赋值给变量;但是可以在switch中使用;

#include 
void longjmp(jmp_buf env, int retval);
void siglongjmp(sigjmp_buf, int retval);	//信号处理函数使用

longjmp函数从env缓冲区恢复调用环境,然后触发一个最近一次初始化env的setjmp调用的返回,使得setjmp返回,其返回值是retval;
setjmp函数调用一次,但是返回多次;longjmp被调用一次,但是从来不返回;
非本地跳转的一个重要应用就是允许从一个深层次嵌套的函数调用中立即返回。

#include "csapp.h"
jmp_buf buf;
int error1 = 0;
int error2 = 1;
void foo(void), bar(void);

int main()
{
	switch(setjmp(buf)){
	case 0:
		foo();
		break;
	case 1:
		printf("Detected an error1 condition in foo\n");
		break;
	case 2:
		printf("Detected an error2 condition in foo\n");
		break;
	default:
		printf("Unknow error condition in foo\n");
	}
	exit(0);
}
void foo(void){
	if(error1)
		longjmp(buf, 1);
	bar();
}
void bar(void){
	if(error2)
		longjmp(buf, 2);
}

使用longjmp函数跳过所有的中间函数可能有风险。如果中间函数调用中分配了某些数据结构,本来预期在函数结尾处释放这些内存,那么使用过longjmp之后, 这些释放代码会被跳过,产生内存泄漏;

非本地跳转另一个重要应用是,信号处理程序分支到一个特殊的代码位置, 而不是返回到被信号到达中断了指令的位置;

#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);
}

8.7 操作进程的工具

8.8 小结

硬件层面, 异常是由处理器中的事件触发,导致的控制流的突变。控制流传递给一个软件处理程序,该程序对产生硬件事件的原因进行处理,然后返回到控制流中;
有四种不同类型的异常:中断、故障、终止和陷阱。
当一个外部IO设备发出中断, 从而设置了处理器芯片上的中断管脚时,中断会异步地发生。
控制返回到故障指令后面的那条指令, 一条指令的执行可能导致故障和终止同步。 故障处理程序会重新启动故障指令,而终止处理程序从不将控制返回给中断的流。
陷阱是系统调用,从应用程序陷入到内核;
为进程提供两个重要的抽象:
1) 逻辑控制流, 好像进程独占一个处理器;
2) 私有地址空间,好像进程独占物理内存;

你可能感兴趣的:(操作系统)