可重入函数、不可重入函数及线程安全

清泛原创
一、可重入(reentrance)函数:

首先来看下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

那么究竟什么是可重入函数呢?

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。

在多任务系统当中,当任务执行期间捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断。如果从信号处理程序返回,则继续执行进程断点处的正常指令序列,从重新恢复到断点重新执行的过程中,函数所依赖的环境没有发生改变,就说这个函数是可重入的,反之就是不可重入的。

众所周知,在进程中断期间,系统会保存和恢复进程的上下文,然而恢复的上下文仅限于返回地址,cpu寄存器等之类的少量上下文,而函数内部使用的诸如全局或静态变量,buffer等并不在保护之列,所以如果这些值在函数被中断期间发生了改变,那么当函数回到断点继续执行时,其结果就不可预料了。打个比方,比如malloc,将如一个进程此时正在执行malloc分配堆空间,此时程序捕捉到信号发生中断,执行信号处理程序中恰好也有一个malloc,这样就会对进程的环境造成破坏,因为malloc通常为它所分配的存储区维护一个链接表,插入执行信号处理函数时,进程可能正在对这张表进行操作,而信号处理函数的调用刚好覆盖了进程的操作,造成错误。

满足下面条件之一的多数是不可重入函数:

(1) 使用了静态数据结构;

(2) 调用了malloc或free;

(3) 调用了标准I/O函数;标准io库很多实现都以不可重入的方式使用全局数据结构。

(4) 进行了浮点运算.许多的处理器/编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现。


关于信号处理程序中调用不可重入函数的例子:

#include <stdlib.h>
#include <stdio.h>
#include <pwd.h>

static void func(int signo)
{
    struct passwd *rootptr;
    if( ( rootptr = getpwnam( "root" ) ) == NULL )
    {
        err_sys( "getpwnam error" );
    }
    signal(SIGALRM,func);
    alarm(1);
}

int main(int argc, char** argv)
{
    signal(SIGALRM,func);
    alarm(1);
    for(;;)
    {
        if( ( ptr = getpwnam("sar") ) == NULL )
        {
            err_sys( "getpwnam error" );
        }
    }
    return 0;
}

signal了一个SIGALRM,而后设置一个定时器,在for函数运行期间的某个时刻,也许就是在getpwnam函数运行期间,相应信号发生中断,进入信号处理函数func,在运行func期间又收到alarm发出的信号,getpwnam可能再次中断,这样就很容易发生不可预料的问题。

不可重入函数不可以在它还没有返回就再次被调用。例如printf,malloc,free等都是不可重入函数。因为中断可能在任何时候发生,例如在printf执行过程中,因此不能在中断处理函数里调用printf,否则printf将会被重入。 

函数不可重入大多数是因为在函数中引用了全局变量。例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。

个人理解:如果中断发生的时候,当运行到printf的时候,假设发生了中断嵌套,而此时stdout资源被占用,所以第二个中断printf等待第一个中断的stdout资源释放,第一个中断等待第二个中断返回,造成了死锁,不知这样理解对不对。

不可重入函数指的是该函数在被调用还没有结束以前,再次被调用可能会产生错误。可重入函数不存在这样的问题。
不可重入函数在实现时候通常使用了全局的资源,在多线程的环境下,如果没有很好的处理数据保护和互斥访问,就会发生错误。

常见的不可重入函数有:

printf --------引用全局变量stdout

malloc --------全局内存分配表

free    --------全局内存分配表

在unix里面通常都有加上_r后缀的同名可重入函数版本。如果实在没有,不妨在可预见的发生错误的地方尝试加上保护锁同步机制等等。


二,函数线程安全

看看APUE上,描述的非线程安全函数

asctime

ecvt

gethostent

getutxline

putc_unlocked

basename

encrypt

getlogin

gmtime

putchar_unlocked

catgets

endgrent

getnetbyaddr

hcreate

putenv

crypt

endpwent

getnetbyname

hdestroy

pututxline

ctime

endutxent

getnetent

hsearch

rand

dbm_clearerr

fcvt

getopt

inet_ntoa

readdir

dbm_close

ftw

getprotobyname

l64a

setenv

dbm_delete

gcvt

getprotobynumber

lgamma

setgrent

dbm_error

getc_unlocked

getprotoent

lgammaf

setkey

dbm_fetch

getchar_unlocked

getpwent

lgammal

setpwent

dbm_firstkey

getdate

getpwnam

localeconv

setutxent

dbm_nextkey

getenv

getpwuid

localtime

strerror

dbm_open

getgrent

getservbyname

lrand48

strtok

dbm_store

getgrgid

getservbyport

mrand48

ttyname

dirname

getgrnam

getservent

nftw

unsetenv

dlerror

gethostbyaddr

getutxent

nl_langinfo

wcstombs

drand48

gethostbyname

getutxid

ptsname

wctomb

If a function can be safely called by multiple threads at the same time, we say that the function is thread-safe

上面一段话是APUE中的解释,如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们说这个函数是线程安全的。所谓安全,一切可能导致结果不正确的因素都是不安全的调用。

线程安全,是针对多线程而言的。那么和可重入联系起来,我们可以断定,可重入函数必定是线程安全的,但是线程安全的,不一定是可重入的。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制,使之能安全的同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。

线程安全,描述的是函数能同时被多个线程安全的调用,并不要求调用函数的结果具有可再现性。也就是说,多个线程同时调用该函数,允许出现互相影响的情况,这种情况的出现需要某些机制比如互斥锁来支持,使之安全。

reentrance thread-safe 可重入 不可重入 函数

分享到:
评论加载中,请稍后...
创APP如搭积木 - 创意无限,梦想即时!
回到顶部