仅作为本人学习《深入 C 语言和程序运行原理》的学习笔记,原课程链接:极客时间《深入 C 语言和程序运行原理》——于航
我们都知道除法运算分母不能为 0,如果在程序中用一个数除以 0,程序中常常会出现“除零异常”。
平时我们在运行程序或与操作系统、底层硬件进行交互时,你可能会遇到下面这些情况:
- 程序运行时访问了非法内存,导致段错误(Segmentation Fault);
- 程序卡死时或用户想强制关闭程序时,在控制台终端输入 Ctrl+C;
- 计算机底层硬件出现故障,导致无法实现特定功能;
- 。。。
以上这些情况都可以通过“信号”来解决。
什么是信号
软中断信号(signal,简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
——百度百科
信号产生是一个随机的过程。程序不需要轮询某个全局变量来处理事情,而是可以提前设定好当某个信号到来时如何进行处理,这是典型的异步事件处理方式。
信号与软件中断
信号是一种软中断。
通常来说,中断触发分为两种形式,即硬件中断和软件中断,其中硬件中断是指与计算机硬件特定状态相关的中断过程,该过程直接由硬件触发。
软件中断是指由计算机软件,通过机器指令引起的 CPU 执行流程的临时转移过程,系统调用也是一种软中断。
在 C 代码中与信号交互
C 语言自 C90 标准开始,便提供了 signal.h 头文件,下面这份代码出自原文:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void sigHandler(int sig) {
printf("Signal %d catched!\n", sig);
exit(sig);
}
int main(void) {
signal(SIGFPE, sigHandler);
int x = 10;
int y = 0;
printf("%d", x / y);
}
main 函数开头先用 signal()
函数设置了一个信号捕获,当 SIGFPE 信号触发时,便会调用 sigHandler() 函数。
Signal 8 对应的就是 SIGFPE 信号,下面是 C 标准库提供的 6 种不同类型的信号:
除了上面自定义信号处理函数的方式外,C 标准库还为我们提供了两种基本的信号处理方式,分别是 SIG_DEF(使用默认的信号处理函数)和 SIG_IGN(忽略该信号),它们可以直接作为 signal()
函数的第二个参数使用。
但是并非所有类型的信号都可以忽略,有些难以恢复软硬件异常对应的信号不能被忽略,比如 SIGTERM,或者 Linux 上的 SIGKILL 和 SIGSTOP 等。
可重入函数
信号处理可以发生在程序运行的任意时刻,但是有些时候,程序运行到一个函数 A 内部,此时来了一个信号,导致 CPU 转去执行信号处理函数,但信号处理函数内部也有函数 A,这样会不会有影响呢?
答案是如果函数 A 是不可重入函数,上面这种情况就会导致意想不到的后果,很多 C 标准库函数,如 printf、exit、malloc 都是不可重入函数。这也就是我们常说的中断内不能放不可重入函数。
C 标准库为我们提供了一个名为 sig_atomic_t 的整数类型,即使中断可能打断执行流程,对该类型变量的读写也都是原子的,这是一种在异步信号处理场景下的原子性。
在处理信号(signal)的时候,有时对于一些变量的访问希望不会被中断,无论是硬件中断还是软件中断,这就要求访问或改变这些变量需要在计算机的一条指令内完成。通常情况下,int类型的变量通常是原子访问的,也可以认为 sig_atomic_t就是int类型的数据,因为对这些变量要求一条指令完成,所以sig_atomic_t不可能是结构体,只会是数字类型。
——百度百科
这里我不是很理解,反正无论如何,中断函数最好不要使用不可重入函数。
多线程应用的信号处理
C 语言并没有对并发编程中的信号处理做任何约束和建议,所以在不同操作系统上使用 signal()
和 raise()
函数,可能功能上会有差异。
所以如果要考虑程序的移植性,最好不要在多线程应用中使用信号处理。处理方法:可以通过宏判断来区分不同的操作系统,从而在不同系统上运行不同的代码。