收发信号思想是 Linux 程序设计特性之一,一个信号可以认为是一种软中断,通过用来向进程通知异步事件。
本文讲述的 信号处理内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解信号编程。
遵循 C11,POSIX.1 - 2008
标准 c 库,libc, -lc
#include
int raise(int sig);
raise() 函数向调用进程或者调用线程发送一个信号,在单线程程序中它相当于:
kill(getpid(), sig);
在多线程程序中,它相当于:
pthread_kill(pthread_self(), sig);
如果信号导致信号处理函数调用了,那么 raise() 函数只有在信号处理函数返回后才会返回。
raise() 在成功时返回 0,失败时会返回非 0 值。
5.属性
接口 | 属性 | 数值 |
raise() | 线程安全 | MT-Safe |
接着这个机会,我们顺便介绍下这里提到的属性,具体参考 attributes(7)
属性,即 POSIX 安全概念。根据 GNU C 库手册中的 POSIX 安全概念章节描述,其实在很多函数的手册页面都包含了 ATTRIBUTES 章节来描述在不同环境下调用函数的安全性,章节会使用以下安全标记来注释函数的安全性:
MT-Safe
MT-Safe 或者 Thread-Safe 函数是在多线程环境下可以安全调用的,MT 表示的是多线程。
多线程安全的函数不代表它是原子的,也不代表它会使用任何 POSIX 暴漏给用户的任何内存同步机制。甚至两个 MT-Safe 的函数顺序调用并不一定能获得一个 MT-Safe 的组合。比如,在一个线程中顺序调用两个 MT-Safe 函数并不能保证是两个函数合并起来的原子操作,因为其他线程里仍然而可能会有一些破坏性的干扰方式。
在整体程序优化时,可能会导致库函数接口函数内联的不安全重新排序,所以这种 C 库内联并不推荐。MT-Safety 状态并不能在整体优化下得到保证,然而用户可见的头文件中的函数都是内联安全的。
MT-Unsafe
MT-Unsafe 函数在多线程环境下的是不安全的。
条件安全特性
对于那些会导致特定环境下函数调用不安全的的特性,除了不一起调用这些函数避免问题外,还有其他方式来解决安全问题。下面的各个关键字指定了这些特性,每个定义表示了如果从程序整体上做限制来消除关键字执行的安全问题。只有所有导致函数不安全的原因都被发现并解决了,函数在可以在特定环境下安全调用。
init
标注了 init MT-Unsafe 特性的函数会它们第一次调用时执行 MT-Unsafe 初始化单线程模式下至少调用一次这个函数来消除该函数被视为 MT-Unsafe 原因,这样就可以在其他线程启动后安全的调用这个函数了。
race
标注了 race 的函数在操作数据对象时可能会有数据竞争或者影响一致性执行等安全问题,这些对象可能是用户传递给函数的,也可能是函数返回给用户的,还有一些是没有暴漏给用户的。
const
标注为 const 的函数表明它会非原子修改内部对象,最好将它视为常量,因为 GNU C 库后续部分可能会不使用任何同步机制访问它们。和 race 不同,内部对象的读写方都被视为 MT-Unsafe,const 标注只用于写的一方。函数写被视为 MT-Unsafe 的,但是强制将这些对象视为常量可以使得函数读被视为 MT-Safe(只要没有其他原因导致它们仍然不安全),因为即使没有同步机制,这些视为常量也是没有问题的。
sig
标注为 sig 的函数可以临时为了内部目的安装一个信号处理函数,这个可能会影响冒号后标记的其他信号其他使用。这个安全问题可以通过不在这个期间让其他地方使用该信号来缓解。在所有使用相同临时信号的函数调用时持有一个非递归互斥锁。
term
标注为 term 的函数使用推荐的方式修改终端设置,比如调用 tcgetattr(3)修改一些标
记,然后调用 tcsetattr(3) 创建一个其他线程修改丢失的窗口。因此,标注为 term 的函数也是 MT-Unsafe 的。非常建议应用不要在信号处理函数中或者屏蔽可能使用它的信号被屏蔽这种情况下使用终端来避免并发、重入影响,并且在调用这个函数和终端交互时持有一把锁这把锁应该和race:tattr(fd) 标记排他使用,fd 是一个控制终端的描述符。调用者可以简单起见使用一把锁,也可以每个终端一把锁,即使是使用不同的文件描述符。
其他安全标记
locale
标注为 locale 的函数会在不适用任何同步机制的情况下读取本地对象,这样函数的并发调用可能会导致非预期的效果。我们并不会将这些函数标记为 MT-Unsafe,因为修改本地对象的函数会被标记为 const:locale,标记为不安全(unsafe),unsafe 的函数是不会在多线程或者异步信号开启的环境下调用的,这就相当于 locale 实际上相当于在环境下为 const,保证了前者的安全。
C89,POSIX.1-2001
glibc 2.3.3 以来,raise() 在支持tgkill(2) 的内核上使用该系统调用实现,老版本的 glibc 使用 kill(2) 实现 raise()。
signal() 在进程的多线程场景下的副作用是未定义的。
根据 POSIX 定义,进程忽略非 kill(2)/raise(3) 产生的 SIGFPE/SIGILL/SIGSEGV 信号后的行为是未定义的。整数除以 0 是未定义的结果,在一些架构上它会产生 SIGFPE 信号(同样使用 -1 除最大负整数也可能产生 SIGFPE。)忽略这些信号可能会导致无限循环。
参考 sigaction(2) 获取更多关于将 SIGCHLD 信号的处置设置为 SIGIGN 的信息。
参考 signal-safety(7) 来查看一些可以在信号处理函数内部调用的异步信号安全的函数。
下面是一个捕捉 CTRL + C 信号的程序。
#include
#include
#include
#include
void sighandler(int);
int main()
{
signal(SIGINT, sighandler);
while(1)
{
printf("开始休眠一秒钟...\n");
sleep(1);
}
return(0);
}
void sighandler(int signum)
{
printf("捕获信号 %d,跳出...\n", signum);
exit(1);
}
下一篇 【计算机网络】信号处理接口 Signal API(2)