信号处理是操作系统中的一个重要机制,它允许进程在运行期间响应外部事件,并作出相应的处理。为了处理信号,程序员需要理解如何设置信号处理器,如何管理信号的屏蔽与阻塞,以及信号的递送机制。本文将结合操作系统中的信号处理、可重入函数、以及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()
等系统调用来收集子进程的退出状态。