可重入函数这一概念早有接触,但一直未有系统的理解,最近阅读《APUE》信号一章时,其中讲解很到位,故总结如下。
信号作为一种软中断,能够被进程给捕获,因而也就中断进程的正常执行,转而去执行信号处理程序,最后再返回到原进程继续正常执行。然而,当进程正在执行malloc()动态内存分配时,信号产生从而转入到信号处理程序,但当信号处理程序中也用到了malloc()函数时,问题就出来了?因为malloc()通常维护一个所有已分配内存链表,当信号发生时,进程可能正在修改链表指针,这时在信号处理程序中将又一次修改链表。当然类似的情况还有不少,下文中将会谈到。
因此,在进行上层应用程序设计过程中我们就必须明确哪些函数是可重入性函数(reentrant functions)。可重入性函数通常也一定能够在信号处理程序(signal handler)中被调用。
图1 能够在信号处理程序中调用的可重入性函数(节自《APUE》)
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 |
纵观上表,我们可以看出,有不少系统调用函数并没有出现,换言之也就是非可重入性函数。函数不可重入的原因主要如下:
<!--[if !supportLists]-->(1) <!--[endif]-->函数使用了static静态数据结构
如:struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
struct passwd *getpwent(void);
以上3个函数都是返回一个指向passwd结构的指针,而该passwd结构通常都是函数中static变量,其内容在每次调用以上函数时都会被重写。因此,当进程主程序与信号处理程序中均调用了以上函数时,冲突就产生了。
<!--[if !supportLists]-->(2) <!--[endif]-->函数调用了malloc和free函数,正如文章最开始所提到的;
<!--[if !supportLists]-->(3) <!--[endif]-->函数为标准I/O的库函数,因为大多数的标准I/O库函数的实现都使用了global全局数据结构;
因此,若要写可重入性函数的做法通常是我们在函数中只修改局部变量,而不改变全局变量,或尽量不使用全局变量、静态static变量。
事实上,与可重入性函数(reentrant function)对应的还有可重入内核(reentrant kernel),其区别和联系在《深入理解Linux内核》上有较详细的讲解。