10.6 可重入函数
当一个信号捕获到并开始被进程处理的时候,进程正常执行的指令序列将被信号处理函数临时中断,进程立即转到信号处理函数中开始执行,如果信号处理函数返回(而不是调用exit或者是longjmp等),然后在进入信号处理函数之前进程正在执行的指令序列将会接着执行,但是在信号处理函数中,我们无法获知在信号被捕获的时候进程正在执行那一段代码,如果进程正在使用函数malloc在其堆上分配额外的内存的过程中会发生什么呢?或者是进程正在调用一个函数的过程中,比如说getpwnam将会发生什么呢?函数getpwnam会将其结果存储在一个静态区域,这可能会导致一个混乱,因为malloc通常会保存期已经分配的区域到一个链表中,并且信号被捕获的时候,进程可能正在对这一列表进行修改,在函数getpwnam函数的情况下,返回给正常调用进程的结果可能会被信号处理函数重写。
The Single UNIX Specification详细列出了在信号处理函数中调用保证安全的函数,这些函数是可重入的,并且在Single UNIX Specification中被称为async-signal safe,除了可重入函数以外的函数,如果在调用过程中被检测出可能造成不一致性情况的出现的话,那么信号处理函数中对这些函数的调用就会被阻塞,图10.4列出了这些async-signal safe函数,许多函数不是可重入的原因是:
- 函数使用了静态的数据结构;
- 函数调用了malloc或者free函数;
- 它们是标准IO函数库的一部分。
许多标准IO库的实现都以一种不可重入的方式使用了全局数据结构,注意,在我们的许多例子中,虽然我们在信号处理函数中调用了printf函数,但是我们却不能保证这能够产生我们期望的效果,因为信号处理函数可能会中断主程序中的printf函数的调用。
注意即是是图10.4中列出的信号处理函数,对于每一个线程而言都只有一个errno变量,因此我们可能会不经意之间对其进行了不期望的修改。假设main中刚刚设置完成errno的值,一个中断处理函数就被调用了,如果中断处理函数调用了read函数,该调用可能会改变errno的数值,从而覆盖掉刚刚在main函数中所赋的数值,因此,一个通用的规则是:当我们在信号处理函数中调用图10.4中列出的函数的时候,我们应该存储和恢复errno,注意到一个经常被捕获到的信号是SIGCHLD,其信号处理函数通常会调用一个wait函数,而所有的wait函数都将会改变errno的数值。
注意,函数longjmp以及siglongjmp函数并没有出现在图10.4中,因为当主程序正在以一种不可重入的方式更新数据结构的时候,信号可能出现,如果我们在信号处理函数中调用siglongjmp而不是返回的话,就可能导致数据结构只更新了一半,剩下的部分永远不能再正确地更新了。因此如果函数正在处理一些像更新数据结构之类的事情的时候,同时信号处理函数可能造成sigsetjmp函数的调用的话,应用程序就应该在更新数据结构的时候阻塞信号。
Example
图10.5的例子中,在信号处理函数中每秒钟调用一次不可重入函数getpwnam:
#include "apue.h"
#include <pwd.h>
static void my_alarm(int signo)
{
struct passwd *rootptr;
printf("in signal handler\n");
#if 1
if((rootptr = getpwnam("root")) == NULL)
{
err_sys("getpwnam(root) error");
}
#endif
printf("getpwnam function execute finish!\n");
alarm(1);
}
int main(void)
{
struct passwd *ptr;
signal(SIGALRM, my_alarm);
alarm(1);
while(1)
{
#if 1
if((ptr = getpwnam("os")) == NULL)
{
err_sys("getpwnam error");
}
if(strcmp(ptr->pw_name, "os") != 0)
{
printf("return value correpted!, pw_name = %s\n", ptr->pw_name);
}
#endif
}
}
"10_5.c" 76 lines, 633 characters
编译上述程序并在Debian上进行测试,当上述代码中的连个都是#if 1的时候,运行效果如下所示:
os@debian:~/UnixProgram/Chapter10$ ./10_5.exe
in signal handler
^C
os@debian:~/UnixProgram/Chapter10$
可见程序在信号处理函数内调用函数getpwnam的时候被阻塞了,程序运行很长时间都没有看到输出getpwnam function execute finish!。
对上述程序略作修改以后可以看到,只要将其中一个或者两个#if 1
修改为#if 0
,程序就可以按照与其的运行了:
os@debian:~/UnixProgram/Chapter10$ ./10_5.exe
in signal handler
getpwnam function execute finish!
in signal handler
getpwnam function execute finish!
in signal handler
getpwnam function execute finish!
in signal handler
getpwnam function execute finish!
in signal handler
getpwnam function execute finish!
^C
os@debian:~/UnixProgram/Chapter10$
APUE.3E对于上述程序的解释如下所述:
当上述程序运行的时候,结果数随机的,通常情况下,程序可能被信号SIGSEGV(无效内存引用)中断,对于core文件的检查显示main函数已经调用了函数getpwnam,但是当getpwnam调用函数free的时候,定时信号处理函数将其中断,然后在信号处理函数中调用了getpwnam,该函数会再次调用函数free,通常,该程序可以在被信号ISGSEGV错误中断之前运行几秒钟。
可见在一个信号处理函数中调用一个不可重入的函数,其结果将是不可预测的.