什么是可重入函数
可重入的概念
若一个程序或子程序可以“在任意时刻被中断,然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。
也就是说,当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时,重新进入同一个子程序,仍然是安全的。
可重入函数可以简单地理解为: 函数可以在任意时刻被中断并再次被调用(即重入),稍后再继续执行被中断的那次调用,而不会丢失数据。
不可重入
为便于理解可重入的概念,先来考察一个不可重入的例子:
我们现在有一个链表,insert()
函数可向链表中插入一个结点
- 在正在调用
insert()
函数插入结点node1
时(假设此时刚完成node1
的指针指向,图中1所示), - 被
sighandler()
函数中断,并在sighandler()
函数中再次调用insert()
函数向链表插入node2
, - 完成后,此时
head
指向了node2, - 继续进行被中断的插入动作(即继续插入
node1
),则完成该动作之后head
将指向node1
,这样就导致中间插入的node2
丢失。
因此,函数insert()
是不可重入的。
可重入函数的条件
若一个函数是可重入的,则该函数应当满足下述条件:
- 不能含有静态(全局)非常量数据。
- 不能返回静态(全局)非常量数据的地址。
- 只能处理由调用者提供的数据。
- 不能依赖于单实例模式资源的锁。
- 调用的函数也必需是可重入的。
上述条件就是要求可重入函数使用的所有变量都保存在调用栈的当前函数帧(frame)上。因此,同一执行线程重入执行该函数时 加载了新的函数帧,与前一次执行该函数时使用的函数帧不冲突、不互相覆盖,从而保证了可重入执行安全。
多“用户/对象/进程优先级”以及多进程,一般会使得对可重入代码的控制变得复杂。同时,IO代码通常不是可重入的,因为他们依赖于像磁盘这样共享的、单独的资源(类似编程中的静态Static
、全域Global
资源)。
因此,如果函数体内调用了标准I/O函数,则该函数是不可重入的。
可重入出现的背景
可重入概念是在单线程操作系统的时代提出的。
这也使得可重入函数未必是线程安全的;线程安全函数未必是可重入的。
一个子程序的重入,可能由于自身原因,如执行了jmp或者call,类似于子程序的递归调用;或者由于操作系统的中断响应。UNIX系统的signal的处理,即子程序被中断处理程序或者signal处理程序调用。所以,可重入也可称作“异步信号安全”。这里的异步是指信号中断可发生在任意时刻。 重入的子程序,按照后进先出线性序依次执行。
示例
下面两个函数f1()和f2()都不是可重入函数:
int global_var = 1;
int f1()
{
global_var += 2;
return global_var;
}
int f2()
{
return f1() + 2;
}
原因分析
函数f1()
使用了全局变量global_var
,所以 如果两个或多个线程同时执行它并访问global_var
,则返回的结果取决于执行的时间。因此,f1()
不可重入。而函数f2()
调用了f1()
,所以它也不可重入。
修改为可重入函数
不再使用全局变量,而是改为以参数的形式接收输入,则两个函数都是可重入的:
int f1(int i)
{
return i + 2;
}
int f2(int i)
{
return f1(i) + 2;
}
Linux提供的可重入函数
slot@slot-ubt:~$ man 7 signal
Async-signal-safe functions
A signal handler function must be very careful, since
processing elsewhere may be interrupted at some arbi‐
trary point in the execution of the program. POSIX has
the concept of "safe function". If a signal interrupts
the execution of an unsafe function, and handler calls
an unsafe function, then the behavior of the program is
undefined.
POSIX.1-2004 (also known as POSIX.1-2001 Technical Cor‐
rigendum 2) requires an implementation to guarantee that
the following functions can be safely called inside a
signal handler:
_Exit()
_exit()
abort()
accept()
access()
aio_error()
aio_return()
aio_suspend()
alarm()
bind()
cfgetispeed()
cfgetospeed()
cfsetispeed()
cfsetospeed()
chdir()
chmod()
chown()
clock_gettime()
close()
connect()
creat()
dup()
dup2()
execle()
execve()
fchmod()
fchown()
fcntl()
fdatasync()
fork()
fpathconf()
fstat()
fsync()
ftruncate()
getegid()
geteuid()
getgid()
getgroups()
getpeername()
getpgrp()
getpid()
getppid()
getsockname()
getsockopt()
getuid()
kill()
link()
listen()
lseek()
lstat()
mkdir()
mkfifo()
open()
pathconf()
pause()
pipe()
poll()
posix_trace_event()
pselect()
raise()
read()
readlink()
recv()
recvfrom()
recvmsg()
rename()
rmdir()
select()
sem_post()
send()
sendmsg()
sendto()
setgid()
setpgid()
setsid()
setsockopt()
setuid()
shutdown()
sigaction()
sigaddset()
sigdelset()
sigemptyset()
sigfillset()
sigismember()
signal()
sigpause()
sigpending()
sigprocmask()
sigqueue()
sigset()
sigsuspend()
sleep()
sockatmark()
socket()
socketpair()
stat()
symlink()
sysconf()
tcdrain()
tcflow()
tcflush()
tcgetattr()
tcgetpgrp()
tcsendbreak()
tcsetattr()
tcsetpgrp()
time()
timer_getoverrun()
timer_gettime()
timer_settime()
times()
umask()
uname()
unlink()
utime()
wait()
waitpid()
write()
POSIX.1-2008 removes fpathconf(), pathconf(), and
sysconf() from the above list, and adds the following
functions:
execl()
execv()
faccessat()
fchmodat()
fchownat()
fexecve()
fstatat()
futimens()
linkat()
mkdirat()
mkfifoat()
mknod()
mknodat()
openat()
readlinkat()
renameat()
symlinkat()
unlinkat()
utimensat()
utimes()
POSIX.1-2008 Technical Corrigendum 1 (2013) adds the
following functions:
fchdir()
pthread_kill()
pthread_self()
pthread_sigmask()
声明:本文部分整理自维基百科:可重入,本文所有文字遵从知识共享 署名-相同方式共享 3.0协议