ubuntu下signal()函数的行为

写此文的目的是最近遇到了与signal()函数相关的问题。虽然一开始就知道signal()是一个机制不完善、语意混乱、已经不被推荐使用的函数,但我遇到的问题出乎了我的意料,所以上网搜了搜,结果基本都是一个口径:signal()安装的信号处理函数会被自动重置。对于这样的结果我非常不满意,所以只好自己去查阅了man,再在ubuntu13.04上动手试验才弄明白了整个问题。


先来摘录一段man里的表述(有点长):

在最开始的UNIX系统中,用signal()安装的信号处理函数响应一次就会失效。因为在信号处理函数被调用的同时,这个处理函数就会被自动重置为默认信号处理函数(SIG_DFL)。所以一般的做法都是在信号处理函数里面再调用signal()重新安装一次。但这样做有个缺陷就是从调用信号处理函数到重新安装信号处理函数这段极短的时间是个“真空区”。这时候的信号处理函数已经被重置为了默认的,而且还没来得及重新安装,如果正好有信号在这个“缝隙”中间到达了,那么它将不能被正确响应。

这个版本的signal()还有一个问题就是信号处理函数运行期间不会屏蔽相同的信号,如果这时候相同的信号到来,就会导致信号处理函数被递归调用。

System V也提供了语意相同的signal()函数。而BSD提供了一个改进的signal(),它安装的处理函数不会自动重置,处理函数运行期间会阻塞相同的信号,处理函数运行返回以后,被阻塞的信号会再次触发信号处理函数。我试验测得如果处理函数运行期间有多个相同的信号到达并被阻塞,那么处理函数运行返回以后不会被再次触发多次,只会被触发一次,也就是说系统不会对阻塞的信号进行排队。BSD的signal()还有一个好处就是能够自动重启被信号处理函数中断的慢系统调用。

BSD的signal()虽然很好,但是这样一来signal()函数就具有了多种语意,显得很混乱,所以POSIX设计了语意清晰、机制完善、功能强大的sigaction()用于取代signal()。

System V的signal()相当于以如下参数调用sigaction():

sa.sa_flags = SA_RESETHAND | SA_NODEFER;

BSD的signal()相当于以如下参数调用sigaction():

sa.sa_flags  =  SA_RESTART;

现在Linux系统中的情况有点复杂:

  • 内核的signal()系统调用是跟System V的类似的。
  • 默认情况下,glibc 2及其后来版本中的signal()包装函数并不调用内核的signal()系统调用。如果定义了_BSD_SOURCE宏或者通过定义_GNU_SOURCE宏隐式定义了_BSD_SOURCE宏,那么glibc库将会用sigaction()模拟BSD的语意,默认情况下_BSD_SOURCE已经定义了。否则glibc库将会用sigaction()模拟System V的语意。
  • 在Linux 的libc4库或libc5库中,如果编程时包含的头文件是<signal.h>,那么signal()将会是System V的版本。如果包含的头文件是<bsd/signal.h>,那么signal()显然会是BSD的版本。


看完这一段man,基本上已经能确定ubuntu13.04里面的signal()是用sigaction()模拟的BSD版本,因为ubuntu13.04的glibc版本是2.7。验证程序很简单:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int uid_gen=0;
void handler_alrm(int sig) {
	int uid=uid_gen++;
	printf("%d:收到信号,休眠5秒...\n", uid);
	sleep(5);
	printf("%d:信号函数返回\n", uid);
}
int main() {
	signal(SIGALRM, handler_alrm);
	printf("进程%d\n", getpid());
	getchar();
	puts("正常退出");
	return 0;
}

main()函数里先安装了一个SIGALRM的信号处理函数,然后输出当前进程号并阻塞在getchar()处等待退出。信号处理函数依靠uid来区分每一次的信号触发,它会休眠5秒以便验证处理函数返回之前相同信号再次到来会发生什么。

验证程序启动运行以后,会输出自己的进程号,然后程序阻塞等待:


此时另开一个终端,使用kill -s SIGALRM 2765命令向验证程序的进程发送一次SIGALRM信号,验证程序的信号处理函数响应了,等待5秒后,信号处理函数返回:


接着在5秒内连续发送三次SIGALRM信号,信号处理函数立即响应,等待5秒后,再次响应并等待5秒,然后就没有响应了:

ubuntu下signal()函数的行为_第1张图片

这说明了三个问题:

  • 信号处理函数没有被自动重置,还可以反复触发。
  • 信号处理函数运行期间会阻塞所有相同的信号。
  • 信号处理函数返回后,运行期间阻塞的信号会被重新递交且仅递交一次,再次触发处理函数。

上面这个验证程序太过简单,还有一些情况没有验证,而下面这个稍微复杂一些的程序将会进一步验证:

  • 处理函数返回之前,只有触发它的信号会被阻塞,其它信号不会被阻塞。
  • 不同的信号阻塞以后,都会各自被递交一次。
  • 信号处理函数所中断的慢系统调用会自动重启。

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

char buf[4];
int uid_gen=0;

void handler(int sig) {
	int uid=uid_gen++;

	printf("%d:收到信号%d,等待回车...\n", uid, sig);

	read(0, buf, sizeof(buf));

	printf("%d:信号函数返回\n", uid);
}

int main() {
	signal(SIGALRM, handler);
	signal(SIGCHLD, handler);

	printf("进程%d,等待回车...\n", getpid());

	read(0, buf, sizeof(buf));

	puts("正常退出");
	return 0;
}
验证程序启动以后,输出自己的的进程号并等待回车:


此时另开一个终端,用kill命令发送一个SIGALRM(14),处理函数立即响应,并等待回车:


发送一个SIGCHLD信号,SIGALRM的处理函数尚未返回,但SIGCHLD(17)的处理函数仍然被触发了,即便它们是同一个函数:


再次发送两个SIGALRM,没有响应,因为SIGARLM的处理函数被SIGCHLD的处理函数中断了,尚未返回,所以SIGALRM被阻塞。再次发送两个SIGCHLD,同样没有响应,被阻塞了。按一下回车,SIGCHLD触发的处理函数返回,于是阻塞的SIGCHLD(17)被递交一次:

ubuntu下signal()函数的行为_第2张图片

再按一下回车,SIGCHLD的处理函数返回,此时回到了最先的由SIGALRM触发的函数中,而且慢系统调用read()没有返回,说明它被中断以后在内部自动重启了。继续按回车,SIGALRM的处理函数返回,阻塞的SIGALRM(被递交一次,再次触发处理函数:

ubuntu下signal()函数的行为_第3张图片

按回车,处理函数返回,再按一次,主函数退出:

ubuntu下signal()函数的行为_第4张图片

上面两个验证在ubuntu 13.04上很成功,但是就像大多数资料上所说的那样,在实际应用中非常不推荐使用signal()函数,因为它并不是在所有平台上都靠谱的,这会带来移植问题。最好还是使用sigaction(),如果觉得sigaction()参数比较麻烦,可以像glibc那样把sigaction()包装成signal()。

关于signal()/sigaction()这种阻塞递交特性的应用,最经典的例子之一就是多进程编程中父进程对僵死子进程的清理。通过给父进程安装一个如下的SIGCHLD处理函数,可以一个不漏地清理所有的僵死子进程,防止僵尸进程的产生。

void handler_chld(int sig) {
	while(waitpid(-1, NULL, WNOHAND)>0);
}

你可能感兴趣的:(c,linux,ubuntu,Signal,posix)