重新载入函数(Reentrant Functions)

重新载入函数(Reentrant Functions)

当一个信号被捕获后,被一个进程处理,该进程的正常指令队列被信号处理函数临时中断。进程会继续处理,不过执行的是信号处理函数的指令。如果信号处理函数返回(不是调用exitlongjmp),会继续捕获信号之前指令继续执行。但是当信号被捕获后,在信号处理函数中,我们不能告诉进程处理到哪里了。当进程使用malloc向堆上分配额外内存时会发生什么?还有我们在信号处理函数中调用malloc会发生什么?或者进程正在调一个函数的过程中,如:getpwnam,它把结果存储在一个静态区域中,并且我们会在信号处理函数中调用相同的函数,这时会发生什么?在malloc的例子中,破坏会影响到进程,因为malloc一般维护着一个链表,该链表包含所有它分配的区域,并且它可能已经改变了链表中的数据。在getpwnam例子中,正常调用者的数据可能被信号处理函数覆盖。

Single UNIX Specification指定了一些函数,可以保证它们重新载入(reentrant)。

Figure 10.4. Reentrant functions that may be called from a signal handler

accept

fchmod

lseek

sendto

stat

access

fchown

lstat

setgid

symlink

aio_error

fcntl

mkdir

setpgid

sysconf

aio_return

fdatasync

mkfifo

setsid

tcdrain

aio_suspend

fork

open

setsockopt

tcflow

alarm

fpathconf

pathconf

setuid

tcflush

bind

fstat

pause

shutdown

tcgetattr

cfgetispeed

fsync

pipe

sigaction

tcgetpgrp

cfgetospeed

ftruncate

poll

sigaddset

tcsendbreak

cfsetispeed

getegid

posix_trace_event

sigdelset

tcsetattr

cfsetospeed

geteuid

pselect

sigemptyset

tcsetpgrp

chdir

getgid

raise

sigfillset

time

chmod

getgroups

read

sigismember

timer_getoverrun

chown

getpeername

readlink

signal

timer_gettime

clock_gettime

getpgrp

recv

sigpause

timer_settime

close

getpid

recvfrom

sigpending

times

connect

getppid

recvmsg

sigprocmask

umask

creat

getsockname

rename

sigqueue

uname

dup

getsockopt

rmdir

sigset

unlink

dup2

getuid

select

sigsuspend

utime

execle

kill

sem_post

sleep

wait

execve

link

send

socket

waitpid

_Exit & _exit

listen

sendmsg

socketpair

write

还有很多函数不在Figure 10.4中,因为(a)它们使用静态数据结构(b)它们调用mallocfree(c)它们是标准I/O库的一部分。多数标准I/O库的实现,以非重新载入(nonreentrant)的方式使用全局数据结构。注意,即便在我们的信号处理函数例子中调用printf,也不保证有预计的结果,因为信号处理函数能中断我们的主函数后调用printf

要知道即使我们在信号处理函数中调用了Figure 10.4中的函数,每个线程也只有一个errno变量(APUE的1.7节谈论了errno和线程),并且我们可以修改这个值。考虑一个信号处理函数,它在main函数刚设置完errno后立即被调用,如果这个信号处理函数调用read,而且这个调用可以改变errno的值,那么这将会擦除之前在main函数中存储于errno中的值。因此,做为一般规则,当在信号处理函数中调用Figure 10.4中的函数时,我们应该保存和恢复errno中的值。要知道通常捕获了SIGCHLD信号后,信号处理函数都会调用wait函数(几个版本中的一个),而所有的wait函数都能改变errno值。

注意longjmp(APUE 7.10节)siglongjmp(APUE 10.15节)不在Figure 10.4中,因为可能信号发生的同时,main例程已经以非重新载入(nonreentrant way)的方式更新了数据结构。如果我们调用siglongjmp代替从信号处理函数返回,这时这个数据结构可能已经更新了一半。如果用类似的方式更新全局数据结构,当捕获信号的同时,会导致sigsetjmp被执行,一个应用须要在更新数据结构的同时阻塞信号。

例子:

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);
    }
}

上面的例子显示了一个程序,它从每秒调用一次的信号处理函数中调用了一个非重新载入函数(nonreentrant function)getpwnam。alarm函数会在APUE 10.10节介绍。在这里我们用它每秒产生一个SIGALRM信号。

当这个程序运行时,结果是随机的。一般情况下,当信号处理函数会在几次迭代后返回,程序可能会被SIGSEGV信号结束。可以通过检查core文件来查看main函数已经调用getpwnam,但是它的一些内部指针在信号处理函数调用相同函数时已经被破坏。偶尔这个程序也能在收到SIGSEGV信号前运行几秒钟。当信号被捕获后main函数正确的运行时,返回值有时是好的,而有时是被破坏了的。在Mac OS X上,malloc库会输出一条警告信息:释放过的内存不会再被malloc分配。

发表回复