CSAPP:第八章——异常控制流

异常控制流

    • 一、异常
      • 1.1 异常处理
      • 1.2 异常的类别
        • 1)中断
        • 2)陷阱和系统调用
        • 3)故障
        • 4)终止
      • 1.3 Linux/x86-64 系统中的异常
        • 1)Linux/x86-64 故障和终止
        • 2)Linux/x86-64 系统调用
    • 二、进程
      • 2.1 逻辑控制流
      • 2.2 并发流
      • 2.3 私有地址空间
      • 2.4 用户模式和内核模式
      • 2.5 上下文切换
    • 三、进程控制
      • 3.1 父进程与子进程
        • 1)父进程
        • 2)子进程
        • 3)父子进程之间的关系
      • 3.2 获取进程 ID
      • 3.3 创建和终止进程
      • 3.4 回收子进程
      • 3.5 让进程休眠
      • 3.6 加载并运行程序
      • 3.7 利用 fork 和 execve 运行程序
    • 四、信号
      • 4.1 信号术语
      • 4.2 发送信号
      • 4.3 接受信号
      • 4.4 阻塞和解除阻塞信号
      • 4.5 编写信号处理程序
      • 4.6 同步流以避免讨厌的并发错误
      • 4.7 显式地等待信号
    • 五、非本地跳转

程序计数器假设有一个值序列 a1,a2, ... , an-1,其中,每个 ak是某个相应指令 Ik的地址,每次从地址 akak+1的过渡称为 控制转移。而这样的控制转移序列叫做处理器的 控制流

最简单的一种控制流是一个平滑的序列,其中每个指令IkIk+1在内存中的位置都是相邻的。当然也有平滑流的突变,即指令IkIk+1在内存中的位置不相邻,通常是由跳转、调用和返回这种程序指令造成的。这些指令都是一些必要机制,使得程序能够对由程序变量表示的内部程序状态的变化做出反应。

同理,系统也必须能够对系统状态的变化做出反应,这些系统状态不能由内部程序变量捕获,而且也不一定要会程序的执行相关,现代系统通过使控制流突变来对系统状态变化做出反应,一般将这种突变称为异常控制流

调试器触发断点的一种方法是,将断点处的目标指令替换为一个不规范指令,并捕获由此引发的异常

一、异常

异常是异常控制流的一种形式,它的一部分由硬件实现,一部分由操作系统实现。异常就是控制流中的突变,用来响应处理器中的一些变化。如下图所示,当处理器状态发生一个重要的变化时,处理器正在执行某个当前指令Icurr。在处理器中,状态被编码为不同的信号。状态变化称为事件(event)。
CSAPP:第八章——异常控制流_第1张图片
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序exception handler),当异常处理完成处理后,根据引起异常的事件类型,会发生以下3种情况中的一种:

  • 处理程序将控制返还给当前指令Icurr,即当事件发生时正在执行的指令;
  • 处理程序将控制返还给Inext,如果没有发生异常将会执行下一条指令;
  • 处理程序终止被中断的程序

这个例子对异常解释的很形象:在我很小的时候,经常和一些小朋友弹弹珠,这时候玩儿的正起劲儿(正在执行当前指令),突然母亲大人在门口一声大喊:“xxx 回家吃饭了”(突发性事件)。我就必须要放下手头上正在玩儿的游戏,一溜小跑回家吃饭(异常处理程序)。吃完饭以后,我可以选择继续玩,玩其他游戏,或者不玩游戏(异常处理程序完成以后)。

1.1 异常处理

系统为每种可能类型的异常分配了一个唯一的非负整数异常号。在系统启动时(通电或者重启),操作系统分配初始化一张称为异常表的跳转表,使得表目k包含异常k的处理程序的地址
CSAPP:第八章——异常控制流_第2张图片
在运行时(系统执行某个程序),处理器检测到一个事件,并确定了相应的异常号k,然后处理器执行间接过程调用,通过异常表的表目k,转到相应的程序。异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器中。

1.2 异常的类别

异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort),下表对这些类别属性做了点总结。
CSAPP:第八章——异常控制流_第3张图片

1)中断

中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的,从这个意义上说它是异步的。硬件中断的异常处理程序常常称为中断处理程序。

下面是一个中断处理程序的过程:外部I/O设备向处理器芯片的一个引脚发个信号,并将异常号放到系统总线上,以此来触发中断。
CSAPP:第八章——异常控制流_第4张图片
剩下的异常类型(陷阱、故障和终止)是同步发生的,是由执行当前指令引起来的,这类引起异常的指令叫做故障指令

2)陷阱和系统调用

陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。

用户程序经常要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)、加载一个新的程序(execve)、或者终止当前进程(exit)。为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的"syscall n"指令,当应用程序想要请求服务n时,可以执行这条指令。执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核参数。下图描述了一个系统的调用过程。
CSAPP:第八章——异常控制流_第5张图片

3)故障

故障由错误引起的,它可能能够被故障处理程序修正。当故障发生,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核的abort例程,abort会终止当前应用程序。下图是故障的处理:

CSAPP:第八章——异常控制流_第6张图片

4)终止

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,终止处理程序从不将控制返还给应用程序。
CSAPP:第八章——异常控制流_第7张图片

1.3 Linux/x86-64 系统中的异常

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

IA32有高达256种不同的异常,0-31的号码是Intel架构师定义的;32-255号是操作系统定义的中断和陷阱。下图是常见的异常示例:

  • 除法错误。不会恢复,会选择终止程序,Linux shell将除法错误报告为“浮点异常(floating exception)”。
  • 一般保护故障。如引用未定义的虚拟内存、写一个只读文本段,不会恢复,该类异常报告称为“段故障(segmentation fault)”。
    CSAPP:第八章——异常控制流_第8张图片

2)Linux/x86-64 系统调用

理论上C程序可以使用syscall函数直接调用任何系统调用,但是标准C提供了一组方便的包装函数,像这种直接调用系统调用的函数和包装用来系统调用的函数,称为系统级函数
CSAPP:第八章——异常控制流_第9张图片

二、进程

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器内容、程序计数器、环境变量以及打开文件描述符的集合。

每次用户向shell输入一个可执行文件的名字运行程序时,shell就会创建一个新的进程,可执行文件在这个新进程的上下文中运行。应用程序也可以创建进程,并在这个新进程的上下文中运行他们自己的代码或其他应用程序。

进程提供给应用程序两个关键抽象。

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

2.1 逻辑控制流

如果我们调试程序单步执行,就会看到一系列程序计数器(PC)的值,这些值唯一的对应于程序的可执行文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流

考虑一个运行着三个进程的系统,处理器的一个物理控制流被分成了三个逻辑流,每个进程一个。每个竖线表示一个进程逻辑流的一部分。在这个例子中,三个逻辑流的执行是交错的。进程A运行一会儿,然后是进程B开始运行到完成。然后是进程C运行了一会儿,进程A接着运行直到完成。最后是进程C可以运行到结束了。
CSAPP:第八章——异常控制流_第10张图片
A、B、C三个进程是轮流使用处理器的,每个进程执行它的流的一部分,然后被抢占(暂时挂起),轮到其他进程。

2.2 并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发运行。更准确的说,流X和流Y互相并发,当且仅当X在Y开始之后和Y结束之前开始,或者Y在X开始之后和X结束之前开始。如图8-12的进程A和进程B、进程A和进程C就是并发。由于进程B执行结束以后才开始进程C,所以B和C不算是并发。

多个流并发地执行的一般现象被称为并发,也就是某进程开始执行以后(并未完成),PC跳转到其他进程执行。一个进程和其他进程轮流的运行的概念称为多任务。一个进程执行它控制流的一部分的每一时间段叫做时间片,如上面进程A由两个时间片组成。因此,多任务也叫做时间分片

【注】并发流的思想与流运行的处理器核数或者计算机数无关。如果两个流在时间上重叠,那么它们就是并发的,即使它们运行在同一个处理器上。如果两个流并发的运行在不同的处理器核或者计算机上,那么我们称它们为并行流,它们并行的运行并且并行的执行,并行流是并发流的一个真子集

2.3 私有地址空间

进程为每个程序提供它自己的私有地址空间,且这些空间具有相同的结构,如图8-13。一般而言,和这个空间中某个地址相关联的那个内存字节是不能被其他的进程读或者写的,从这个意义上说,这个地址空间是私有的。
CSAPP:第八章——异常控制流_第11张图片

2.4 用户模式和内核模式

处理器为了安全起见,不至于损坏操作系统,必须限制一个进程可执行指令能访问的地址空间范围。就发明了两种模式内核模式用户模式

  • 内核模式(超级用户模式),有最高的访问权限,可以执行任何指令,访问任何内存位置,甚至可以停止处理器、改变模式位,或者发起一个I/O操作。
  • 用户模式,不允许执行特定权限的指令。处理器使用一个寄存器当作模式位,描述当前进程的特权。进程从用户变为内核的唯一方法是中断、故障或者陷入系统调用时,才会将模式位设置成内核模式,得到内核访问权限,其他情况下都始终在用户权限中,就能够保证系统的绝对安全。

2.5 上下文切换

内核为每个进程维持一个上下文,上下文就是重新启动一个被抢占的进程所需要的状态,包括寄存器、程序计数器、用户栈、内核栈和各种内核数据结构。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种策略叫做调度。内核中有一个专门的调度程序,被称为调度器

当内核选择一个进程时,就说内核调度了该进程,内核调度进程抢占当前进程后,会使用上下文切换机制来进行控制转移,如从进程A切换到进程B:

  1. 保存进程A的上下文
  2. 恢复被保存的进程B的上下文
  3. 将控制传递到新恢复的进程

CSAPP:第八章——异常控制流_第12张图片
当内核代表用户执行系统调用的时候,就会发生上下文切换,如上图所示,当进程A调用read函数的时候,内核代表进程A开始执行系统调用读取磁盘上的文件,这需要耗费相对很长的时间,处理器这时候不会闲着什么都不做,而是开始一种上下文切换机制,切换到进程B开始执行。当B在用户模式下执行了一段时间,磁盘读取完文件以后发送一个中断信号,将执行进程B到进程A的上下文切换,将控制权返回给进程A系统调用read指令后面的那条指令,继续执行进程A(注:在切换的临界时间内核模式其实也执行了B一个小段时间)。

三、进程控制

3.1 父进程与子进程

1)父进程

指已创建一个或多个子进程的进程。在UNIX里,除了进程0以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。

2)子进程

指的是由另一进程(对应称之为父进程)所创建的进程。子进程继承了对应的父进程的大部分属性,如文件描述符。在Unix中,子进程通常为系统调用fork的产物。在此情况下,子进程一开始就是父进程的副本,而在这之后,根据具体需要,子进程可以借助exec调用来链式加载另一程序。

3)父子进程之间的关系

  • 关于资源。子进程得到的是除了代码段是与父进程共享的意外,其他所有的都是得到父进程的一个副本,子进程的所有资源都继承父进程,得到父进程资源的副本。既然为副本,也就是说,二者是单独的进程,都拥有自己的私有地址空间,继承了以后二者就没有什么关联了,子进程单独运行。

  • 关于文件描述符。继承父进程的文件描述符时,相当于调用了dup函数,父子进程共享文件表项,即共同操作同一个文件,一个进程修改了文件,另一个进程也知道此文件被修改了。

3.2 获取进程 ID

每个进程都有一个唯一的正数(非零)进程ID(PID),getpid函数返回调用进程的PID,getppid函数返回它父进程的PID。

#include 
#include 

//pid_t 在Linux系统的types.h中定义为int
pid_t getpid(void);
pid_t getppid(void);

3.3 创建和终止进程

从程序员角度,我们可以认为进程总是处于下面三种状态之一:

  • 运行。进程要么在CPU上执行,要么在等待被执行且最终会被CPU内核调度。
  • 停止。进程的执行被挂起,且不会被调度。当收到SIGSTOPSIGTSTPSIGTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时候,进程再次运行。
  • 终止。进程永远地停止了。进程会因为3种原因终止:
    1)收到一个信号,该信号的默认行为是终止进程;
    2)从主程序返回;
    3)调用exit函数。
    #include 
    //exit函数以status退出状态来终止进程
    void exit(int status);
    

父进程通过调用fork函数创建一个新的运行的子进程(父子进程并发执行)。

#include 
#include 
pid_t fork(void);

子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本。父进程与子进程最大的区别是它们有不同的PID。

下面是一个创建子进程的实例,子进程和父进程拥有同样的代码片段,注意输出结果:
CSAPP:第八章——异常控制流_第13张图片

  • fork函数被调用一次,却会返回两次:一次调用进程(父进程)中,一次是在新创建的子进程中。在父进程中fork返回子进程的PID,在子进程中fork返回0,所以上面用例的输出结果不一样。
  • 并发执行,父子进程是并发运行独立进程。
  • 相同但是独立的地址空间。从变量x中就可以看出来,起初x在父进程和子进程的值都是1,后面父子进程分别对值做了改变,但是两者的改变互不影响。
  • 共享文件,都指向控制台输出。

进程图可以帮助理解fork函数:
CSAPP:第八章——异常控制流_第14张图片
比较复杂的fork函数应用:
CSAPP:第八章——异常控制流_第15张图片

3.4 回收子进程

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

如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程的PID为1,是在系统启动内核时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵尸子进程就终止了,那么内核会安排init进程取回收它们。

一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。

#include 
#include 
//返回:如果成功,则为子进程的PID,如果WNOHANG,则为0;如果其他错误则为-1.
pid_t waitpid(pid_t pid, int *statusp,int options);

waitpid函数挂起正在执行的调用进程,直到他的等待集合中的一个子进程终止,返回子进程的ID。否则父进程会被阻塞,暂停运行。参数详解如下:

  • pid : 等待集合由该参数确定(pid>0集合为单独的一个子进程,进程ID为pidpid = -1等待集合为所有子进程)。
  • status:检查已回收子进程的退出状态,有了这个信息父进程就可以了解子进程为什么会退出,是正常退出还是出了什么错误。书上有对这个参数详细的解释。
  • options : 修改默认的行为如果不想使用这些选项,则可以把这个参数设为0。书上有对这个参数详细的解释。
  • 返回值:如果成功就返回子进程ID;如果没有失败返回-1(没有子进程的失败设置ECHILD;被中断设置EINTR)

下面是对waitpid函数使用的一个示例:
CSAPP:第八章——异常控制流_第16张图片
上面程序的输出,但是在这个程序程序中那个子进程先退出哪个后退出是不确定的。
CSAPP:第八章——异常控制流_第17张图片
可以改动代码成下面示例,则子进程的创建和退出是以同样的顺序进行的:
CSAPP:第八章——异常控制流_第18张图片

3.5 让进程休眠

sleep函数将一个进程挂起一段指定的时间。如果请求的时间到了,sleep返回0,否则返回还剩下的要休眠的秒数。sleep函数可以被信号中断,从而过早返回。

#include 
unaigned int sleep(unsigned int sec);

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

#include 
int pause(void);

3.6 加载并运行程序

execve函数在当前进程的上下文中加载并运行一个新程序。execve执行可执行目标文件filename,且带参数列表argv和环境变量列表envp

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

下图是参数列表和环境变量列表所使用的的数据结构,均是以NULL结尾的指针数组,每个指针均指向字符串:
CSAPP:第八章——异常控制流_第19张图片
理解进程与程序:进程是对处理器、内存和文件交互的抽象,文件可以是可执行文件,可执行文件也就是程序。所以可以说,进程是操作系统对一个正在运行的程序的一种抽象。
CSAPP:第八章——异常控制流_第20张图片

3.7 利用 fork 和 execve 运行程序

书上这一小节展示了一个简单shellmain程序,相当于对进程这个章节的内容作了一个实例汇总。

四、信号

信号就像你每天早上起床而设置(调用kill函数)的闹钟一样,你接收到这个闹钟以后,被强迫要处理这个信号(一直闹也没办法睡觉啊),这时候你有三种选择:继续睡觉(忽略)、关闭闹钟(终止)、起床(执行闹钟给我的命令:信号处理程序)。

信号是一种更高层次的软件形式的异常,允许进程和内核中断其他进程,它提供了一种机制,通知用户进程发生了那些具体的异常。

  • 进程试图除以0,内核会给它发送信号8
  • 进程执行一条非法指令,内核会发送信号4
  • 进程进行非法内存引用,内核会给它发送信号11
  • 进程在前台运行时,键入Ctrl+C,内核会发送信号2给前台进程组中的每个进程。
  • 一个进程可以向另一个进程发送信号9强制将其终止。
  • 子进程终止或停止,内核会给父进程发送信号17

Linux系统上提供了30中不同类型的信号(Linux 的各种 signal):
CSAPP:第八章——异常控制流_第21张图片

4.1 信号术语

传送一个信号到目的进程有两个不同的步骤:

  • 发送信号。内核通过更新目的进程上下文的某个状态,发送一个信号给目的进程,进程也可以发送信号给自己。发送信号有两个原因:
    1)内核检测到一个系统事件。比如被零除错误,或者子进程终止。
    2)一个进程调用了kill函数。显示要求进程发送信号给目的进程。
  • 接受信号。当目的进程 被内核强迫以某种方式对信号的发送做出反应。目的进程就接收了信号。进程可以忽略这个信号,终止或者通过一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。
    CSAPP:第八章——异常控制流_第22张图片

一个只发出而没有被接收的信号叫做待处理信号,一种类型至多只有一个待处理信号,如果一个进程有一个类型为k的待处理信号,那么接下来发送到这个进程类型为k的信号都会被简单的丢弃。一个进程可以有选择性地阻塞接收某种信号,它任然可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。

一个待处理信号最多只能被接收一次。内核为每个进程在pending位向量维护着待处理信号的集合,而在blocked位向量维护着被阻塞的信号集合。
只要发送一个类型为k的信号,内核就会设置pending中的第k位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。

4.2 发送信号

Unix系统提供大量向进程发送信号的机制,所有这些机制都是基于进程组(process group)这个概念的。

1)进程组

每个进程都属于一个进程组,进程组是由一个正整数进程组ID来标示,getpgrp()函数返回当前进程的进程组ID。

  #include
  pid_t getpgrp(void);

默认,一个子进程和它的父进程同属于一个进程组,一个进程可以通过setpgid()来改变自己或者其他进程的进程组。

 #include
   /**
   *如果pid是0 ,那么使用当前进程的pid。
   *如果pgid是0,那么使用指定的pid作为pgid(即pgid=pid)。
   *例如:进程15213调用setpgid(0,0)
   *那么进程15213会 创建/加入进程组15213.
   */
  int setpgid(pid_t pid,pid_t pgid);

2)用/bin/kill程序发送信号

  • /bin/kill可以向另外的进程发送任意的信号。比如unix>/bin/kill -9 15213,发送信号9(SIGKILL)给进程15213
  • 一个为负的PID会导致信号被发送到进程组PID中的每个进程。unix>/bin/kill -9 -15213,发送信号9(SIGKILL)给进程组15213中的每个进程。

/bin/kill的原因是,有些Unix shell 有自己的kill命令。

3)从键盘发送信号

对一个命令行求值而创建的进程,称为作业(jop)。在任何时刻,至多有一个前台作业和0个或多个后台作业。

Linux> ls | sort

上面命令会创建一个由两个进程组成的前台作业,两个进程通过Unix管道连接起来,一个进程运行ls,另一个进程运行sort。shell为每个作业创建了一个独立的进程组,进程组ID取自作业中父进程中的一个。下图是有一个前台作业和两个后台作业的shell(前台作业和后台作业):
CSAPP:第八章——异常控制流_第23张图片
在键盘输入ctrl-c ,内核会发送一个SIGINT信号到前台进程组的每个进程。在默认情况下,结果是终止前台作业。类似,输入ctrl-z内核会发送一个SIGTSTP信号给前台进程组的每个进程,在默认情况,结果是停止(挂起)前台作业。

4)用kill函数发送信号

进程通过调用kill函数发送信号给其他进程,包括他们自己。

int kill(pid_t pid, int sig);
  • pid > 0,发送信号sig给进程pid
  • pid = 0,发送信号sig给进程所在进程组的每个进程
  • pid < 0,发送信号sig给进程组abs(pid)

5)用alarm函数发送信号

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

 #include
 unsigned int alarm(unsigned int secs);
 //返回:前一次alarm剩余的秒数,若前一次没有设置alarm,则返回 0。

alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。如果secs=0 那么不会调度alarm,当然不会发送SIGALRM信号。在任何情况,对alarm的调用会取消待处理(pending)的alarm,并且会返回被取消的alarm还剩余多少秒结束,如果没有pending的话,返回0

4.3 接受信号

信号的处理时机是在从内核态切换到用户态时,会执行do_signal()函数来处理信号。

当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号集合

  • 如果这个集合为空,内核将控制传递到p的逻辑控制流的下一条指令。
  • 如果非空,内核选择集合中某个信号k(通常是最小的k),并且强制p接收k。收到这个信号会触发进程某些行为,一旦进程p完成行为,则控制就传递到p的逻辑控制流的下一条指令。

每个信号都有预定义的默认行为,是下面的一种:

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

进程可以通过使用signal函数修改和信号相关联的默认行为,但是SIGSTOP(19)SIGKILL(9)是不能被修改的。

#include
typedef void (*sighandler_t)(int);

// 返回:成功为指向前次处理程序的指针,出错SIG_ERR.
sighandler_t signal(int signum,sighandler_t handler);

signal函数通过下列三种方式之一改变和信号signum相关联的行为:

  • 如果handlerSIG_IGN,那么忽略类型为signum的信号。
  • 如果handlerSIG_DFL,那么类型为signum的信号恢复为默认行为。
  • 否则,handler就是用户定义的函数地址,这个函数称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用handler
    CSAPP:第八章——异常控制流_第24张图片

当信号处理程序执行它的return语句后,控制通常传递回控制流中进程被信号接收中断位置处的指令。
CSAPP:第八章——异常控制流_第25张图片

4.4 阻塞和解除阻塞信号

Linux提供了信号阻塞的隐式和显式机制:

  • 隐式阻塞机制:内核默认阻塞任何与当前处理程序正在处理的信号类型相同的待处理的信号。
    如图8-31中,程序捕获了信号s,然后运行处理程序S,此时如果再发送另一个相同类型的信号s给该进程,在处理程序S执行完后,第二个信号s会变成待处理而没有被接收。
  • 显式阻塞机制:程序可以使用sigprocmask函数和它的辅助函数,明确的阻塞和解除阻塞选定的信号。
    #include
    
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    

sigprocmask函数改变当前已阻塞信号的集合(4.1节描述的blocked位向量),具体行为依赖how值,如果oldset非空,block位向量以前的值会保存到oldset中:

  • SIG_BLOCK:添加set中的信号到blocked中(blocked = blocked | set)。
  • SIG_UNBLOCK: 从blocked删除set中的信号(blocked = blocked &~set)。
  • SIG_SETMASK: blocked = set

还有以下函数操作set集合:

#include
// 初始化set为空集合
int sigemptyset(sigset_t *set);
// 把每个信号全部填入到set中
int sigfillset(sigset_t *set);
// 把signum添加到set
int sigaddset(sigset_t *set,int signum);
// 从set中删除signum
int sigdelset(sigset_t *set,int signum);
				/*以上函数,返回:成功0,出错-1*/

//判断:若signum是set的成员,返回1,不是返回0,出错返回-1。
int sigismember(const sigset_t *set,int signum);

【链接】信号的阻塞与未决、解除信号阻塞需注意的问题

4.5 编写信号处理程序

后面如果用到这一块的内容再做补充!也可以参考:https://www.cnblogs.com/zy691357966/p/5480537.html

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

后面如果用到这一块的内容再做补充!

4.7 显式地等待信号

后面如果用到这一块的内容再做补充!

五、非本地跳转

C语言提供一种用户级异常控制流形式,称为非本地跳转(nonlocal jump),它将控制直接从一个函数转移到另一个当前正在执行的函数,不需要经过正常的调用-返回序列。非本地跳转是通过setjmplongjmp函数来提供:

  • setjmp函数在env缓冲区保存当前调用环境,以供后面longjmp使用,并返回0

    #include
    
    int setjmp(jmp_buf env);
    
    //参数savesigs若为非0则代表搁置的信号集合也会一块保存 
    int sigsetjmp(sigjmp_buf env,int savesigs);//信号处理程序使用
    
  • longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化envsetjmp调用的返回,然后从setjmp返回(longjmp不返回),并带有非零的返回值retval

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

setjmp函数只被调用一次,但是返回多次,一次是第一次调用setjmp保存当前调用函数,返回0;一次是为每个相应的longjmp调用,返回retval。而longjmp不返回。

C++和Java提供的异常机制是较高层次的,是C语言setjmplongjmp函数的更加结构化的版本。你可以把try语句中的catch字句看作setjmp函数,相似地,throw语句就类似与longjmp函数。

你可能感兴趣的:(计算机系统原理,异常)