信号处理是操作系统中的一个重要机制,它允许进程在运行期间响应外部事件,并作出相应的处理。为了处理信号,程序员需要理解如何设置信号处理器,如何管理信号的屏蔽与阻塞,以及信号的递送机制。本文将结合操作系统中的信号处理、可重入函数、以及volatile关键字等概念,进行详细的分析和总结。
一、信号捕捉与 sigaction 机制
 
1. 信号处理简介
在类 Unix 系统中,信号是一种用于通知进程发生特定事件的机制。信号可以由操作系统发送,也可以由进程发送。信号可以用于进程间通信、错误处理以及通知等。
- 信号处理函数:当进程接收到信号时,操作系统会根据设定的信号处理函数(Signal Handler)来响应该信号。通过捕捉并处理信号,程序可以在接收到信号时执行特定的操作。
2. 使用 sigaction 配置信号处理器
 
sigaction 是一种设置信号处理函数的方式,提供了比 signal 更强大的功能。sigaction 是一个结构体,其中包含了设置信号处理函数的各项信息。
struct sigaction 结构体:
struct sigaction {
    void     (*sa_handler)(int);    // 信号处理函数
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 扩展的信号处理函数
    sigset_t sa_mask;                // 屏蔽信号集,在信号处理期间会被阻塞
    int      sa_flags;               // 信号的行为标志
    void     (*sa_restorer)(void);  // 保留字段,不常用
};
- sa_handler:指向信号处理函数的指针。此函数将在信号到达时执行。
- sa_mask:指示在处理该信号时,需要屏蔽哪些信号。
- sa_flags:指定信号处理的行为。例如,- SA_SIGINFO使得信号处理函数接收额外的信号信息。
- sa_restorer:一般不需要关注,是为了兼容旧的接口。
sigaction 与信号阻塞:
- 信号嵌套:操作系统不允许信号处理方法进行嵌套,即一个信号正在处理时,系统会自动屏蔽该信号,直到当前信号处理完成。嵌套信号的屏蔽是由操作系统自动完成的,而无需程序员干预。
- sa_mask的作用:- sa_mask用来指定在信号处理期间需要屏蔽的其他信号。例如,如果你处理- SIGINT信号时不希望收到- SIGTERM,可以在- sa_mask中屏蔽- SIGTERM。
3. pending 信号和清零时机
 
在某些情况下,信号可能会在处理信号时被再次递送到进程。pending 信号是指操作系统在信号处理期间接收到的所有待处理的信号。操作系统会在信号处理函数执行之前清空 pending 信号队列。这样做是为了避免一个信号处理期间再次接收同样的信号。
然而,如果一个信号的处理函数在执行过程中接收到相同的信号,并且该信号没有被阻塞,则在处理完当前信号后,操作系统会继续递送该信号。信号的递送通常会按照先进先出(FIFO)的顺序进行。
二、可重入函数与不可重入函数
1. 可重入与不可重入
一个函数被称为可重入函数(reentrant)是指它可以在多次调用之间并行执行而不会出现冲突或错误。当一个函数在执行期间,如果被中断(例如:在信号处理中被调用),能够正确地重新进入并执行,而不会影响原来的执行状态。
- 不可重入函数:如果一个函数在执行过程中,其状态依赖于共享资源(例如:全局变量或静态变量),当该函数被再次调用时,可能会导致数据冲突或错误,称为不可重入函数。
- 可重入函数:如果一个函数在执行过程中完全依赖于局部资源(例如:局部变量、栈空间等),则在多个执行流之间调用时,不会发生资源冲突,称为可重入函数。
2. 判断函数是否可重入
-  全局资源/共享资源:不可重入 
 如果函数中涉及对全局变量、静态变量或外部资源(如文件、网络连接等)的操作,并且这些资源是共享的,那么该函数就是不可重入的。
-  局部资源:可重入 
 如果函数只依赖于局部变量,且没有使用任何共享资源,函数就是可重入的。
3. 函数名后缀 _r(线程安全)
有些函数的实现可能是不可重入的,为了提供可重入的变体,许多标准库函数提供了以 _r 结尾的版本(例如 strtok_r)。这些带有 _r 后缀的函数通常会通过将共享资源变为局部资源的方式,解决不可重入问题,使其线程安全和可重入。
4. 可重入函数的例子
- 可重入:strncpy()(只要没有改变共享资源)、malloc()(不会改变全局状态)、memcpy()(只使用栈空间)。
- 不可重入:rand()、strtok()(修改全局状态,可能引发冲突)。
三、volatile 关键字
 
1. volatile 的含义
 
与register相反
volatile 是 C 语言中的一个关键字,用于告诉编译器某个变量的值可能会被外部环境或其他程序所修改,通常用于避免编译器优化掉某些变量的读取或写入操作。
-  防止优化:在多线程或信号处理程序中,某些变量的值可能被外部事件改变,如信号处理程序或者硬件中断。因此,我们需要使用 volatile关键字来告诉编译器每次都从内存中读取该变量的值,而不是使用寄存器缓存的值。
-  内存可见性: volatile确保每次访问该变量时都从内存中读取最新的值,而不是使用寄存器的缓存副本。这在并发环境下尤为重要。
优化指令如下
 其中, -o3为三级优化, 从1-3,优化成都加深。-o0为不优化
gcc file -o3
2. 使用场景
- 多线程:在多线程环境下,如果一个线程修改了某个全局变量,其他线程需要能立即看到该变量的变化,这时可以使用 volatile关键字来确保变量不会被优化。
- 信号处理:在信号处理程序中,如果信号处理程序修改了某个变量,而主程序需要监视这个变量,必须使用 volatile来确保该变量的值不会被优化掉。
3. 示例代码
volatile int flag = 0; //volatile使用方法
void signal_handler(int sig) {//信号捕捉执行流
    flag = 1;  // 设置标志,表示信号到达
}
int main() {//主执行流
    signal(SIGINT, signal_handler);
    
    while (!flag) {
    	pause();//等待信号
        // 忙等,等待信号
    }
    printf("Received signal, exiting...\n");
    return 0;
}
4. 为什么 volatile 不会退出
 
如果不使用 volatile,编译器可能会优化掉 flag 的检查,因为它没有在循环中被修改。通过使用 volatile,确保每次都从内存读取 flag,防止编译器优化掉对 flag 的访问。
四、SIGCHLD 信号
SIGCHLD 是操作系统中与子进程相关的信号。当子进程退出或停止时,父进程会收到 SIGCHLD 信号。父进程可以捕捉这个信号并处理子进程退出的情况。
- 默认行为:默认情况下,操作系统会在子进程退出时,将其状态保留在进程表中,直到父进程调用 wait()系统调用以读取子进程的退出状态。
- 捕捉 SIGCHLD:如果父进程捕捉到SIGCHLD信号,通常会通过wait()或waitpid()等系统调用来收集子进程的退出状态。
板书笔记




















