10.6 Reentrant Functions可重入函数
(翻译by linxuleio)
当一个进程捕捉到信号,进程执行的正常指令流程被signal handler(自定义的信号处理函数)临时打断。这时进程转而执行signal handler里的指令。当signal handler执行完返回,进程从之前被信号打断的代码处继续执行正常的指令流程。(进程对信号处理流程类似硬件中断处理。)但是在signal handler里,如果进程接收到信号,我们是无法知道进程执行的位置。
当进程正调用malloc分配额外内存空间时接收到信号,同时我们在signal handler里也调用malloc函数时会发生什么?或则,当进程在调用某个函数(例如getpwnam函数会将返回结果存储在内存静态区域)时接收到信号,同时我们在signal handler里也调用同样的函数时会发生什么?以malloc为例子,因为malloc维护了一个所有已分配内存的链表。当进程接收到信号的时候,malloc可能正在修改它的链表,这时在signal handler里又一次修改链表将对进程造成巨大灾难。在getpwnam的例子里,进程调用getpwnam函数返回的结果会把signal handler里调用getpwnam函数返回结果给覆盖掉。
为了避免这些问题,因此UNIX系统定义了一些函数允许在signal handler里重复调用。下边的列表为可重入函数。
UNIX大多数函数不属于可重入函数。这是因为(a)某些函数内使用静态数据结构。(b)某些函数调用了malloc或free。(c)属于标准I/O库的函数。大部分标准I/O库函数使用了全局数据结构,所以不属于可重入函数。
注意尽管之前的一些例子,我们在signal handlers里调用了printf函数。因为会有可能在main中调用printf函数时产生信号中断,最后printf就不会输出我们期望的结果,
signal handler调用可重入函数时,还有一件事要求我们注意。考虑到signal handler调用可重入函数时可能会改变进程原errno变量的值。比如在signal handler中调用read函数,read会改变errno的值,原先进程在main中执行程序所保存的errno的值会在signal handler中被改写。因此通用规则是,当我们在signal handler中调用可重入函数时,最好先保存下errno的值。(比如信号SIGCHLD,它的信号处理函数通常是调用wait函数。所有的wait函数都会更改errrno)
Example
10.5展示了一段在signal handler中每2秒调用一次非重入函数getpwnam的代码。
当这段程序运行的时候,输出结果是任意的。程序接受到SIGSEGV信号,在signal handler返回后应当终止。但实际上当signal handler里调用非重入函数getpwnam时程序里的一些内部指针被打乱。有时候,程序在接受SIGSEGV信号后,崩溃之前会运行一段时间。main函数在接受到信号后之后运行,有时这个返回值是正确的有时又是错误的。
这个例子告诉我们,一旦我们在signal handler里调用一个非重入函数,程序的结果是不可预知的。
Figure 10.5. Call a nonreentrant function from a signal handler
#include "apue.h"
#include <pwd.h>
static void
my_alarm(int signo)
{
struct passwd *rootptr;
printf("in signal handler\n");
if ((rootptr = getpwnam("root")) == NULL)
err_sys("getpwnam(root) error");
alarm(1);
}
int
main(void)
{
struct passwd *ptr;
signal(SIGALRM, my_alarm);
alarm(1);
for ( ; ; ) {
if ((ptr = getpwnam("sar")) == NULL)
err_sys("getpwnam error");
if (strcmp(ptr->pw_name, "sar") != 0)
printf("return value corrupted!, pw_name = %s\n",
ptr->pw_name);
}
}