在哪些情况下是需要考虑函数的可重入性的
1. 进程捕捉信号并对其进行处理时
进程捕获到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(即在信号处理函数中没有调用exit或longjump),则继续执行在捕获到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时所做的)。
但是在信号处理程序中,不能够判断捕捉到信号时进程执行到何处,则存在非常多的可能情况,以下列举了两种可能的情形。
情形一:
如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而转去执行该信号的处理程序,在信号处理程序中又调用了malloc,这是会发生什么呢??
在这种情况下,因为malloc函数通常会为它所分配的存储区维护一个链表,而插入转去执行信号处理程序时,进程可能正在更改此链表,这就可能对进程造成破坏。
情形二:
进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,其间有转去执行信号处理程序,在信号处理程序中又调用了这个函数,这时又会发生什么呢?
显然,在这种情况下,返回给正常调用中的信息会被返回给信号处理程序的信息覆盖。
因此,在信号处理程序中,要调用异步信号安全(async-signal safe)的,可重入(reentrant )的函数。Single Unix Specificaction说明了这些函数。
2. 多线程
关于多线程,可以参考下面这篇文章:
简单来说,OS的调度是随机的,因此在线程执行过程中的任意时刻都有可能被阻塞,然后CPU转去执行另外一个函数。因此这就会出现和上述相同的问题。
可重入性的本质
重入即表示重复进入,首先它意味着这个函数可以被中断(正如上面的转去执行信号处理程序,OS调度,CPU转去执行另一个线程的函数),其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。
不可重入函数
基于上面关于可重入性的讨论,我们可以总结出如下规则,满足如下条件之一的函数,不是可重入函数。
1. 它们调用了malloc或free
2. 它们使用标准I/O函数。标准I/O库的很多实现都以不可重入的方式使用了全局数据结构.
3. 函数体内访问全局变量
4. 已知它们使用静态数据结构
函数的返回值是指向静态变量指针
在我的这篇博文中讨论了这个问题:
函数返回指针类型与函数的可重入性函数将其执行结果存储在静态存储单元中
在第一节中描述的getpwnam就属于此类。
线程安全与可重入函数
在第一节中提到过,在多线程环境中是需要考虑函数的可重入性的。现在对这个问题进行详细地描述。这部分内容主要参考了如下博文:
线程安全(thread-safe)
一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的(thread-unsafe)。
根据上面的论述,我们可以确定可重入函数和线程安全函数之间的关系:
可重入函数是线程安全函数的一种,其特点在于它够被多个线程调用时,不会引用任何共享数据。
显然,我们在上面描述的不可重入函数都是线程不安全函数,如果在多线程环境中,不采用如何如何任何措施就使用不可重入函数,将会给程序带来不良的后果,有时甚至会导致程序崩溃,下面就描述可供使用的措施。
方法一:使用同步措施来保护共享变量
将这类线程不安全函数变为线程安全的,相对比较容易:利用像P和V操作这样的同步操作来保护共享变量。这个方法的优点是在调用程序中不需要做任何修改,缺点是同步操作将减慢程序的执行时间。
示例一:保护全局变量
假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para ) {
unsigned int temp;
[申请信号量操作] //(1)
Exam = para;
temp = Square_Exam( );
[释放信号量操作]
return temp;
}
若申请不到“信号量”,说明另外的进程正处于给Exam赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。若申请到信号,则可继续执行,但其它进程必须等待本进程释放信号量后,才能再使用本信号。
保证函数的可重入性的方法:
在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量),对于要使用的全局变量要加以保护(如采取关中断、信号量等方法),这样构成的函数就一定是一个可重入的函数。
VxWorks中采取的可重入的技术有:
* 动态堆栈变量(各子函数有自己独立的堆栈空间)
* 受保护的全局变量和静态变量
* 任务变量
实例二:保护静态变量
某些函数(如gethostbyname,getpwnam)将计算结果放在静态结构中,并返回一个指向这个结构的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另一个线程悄悄地覆盖了。
有两种方法来处理这类线程不安全函数。一种是选择重写函数,使得调用者传递存放结果的地址。这就消除了所有共享数据,但是它要求程序员还要改写调用者的代码。
如果线程不安全函数是难以修改或不可修改的(例如,它是从一个库中链接过来的),那么另外一种选择就是使用lock-and-copy(加锁-拷贝)技术。这个概念将线程不安全函数与互斥锁联系起来。在每个调用线程不安全函数的位置,对函数的返回结果加互斥锁,然后调用线程不安全函数,动态地为结果分配内存空间,拷贝函数返回的结果到这个内存空间,然后对互斥锁解锁。一个吸引人的变化是定义了一个线程安全的封装(wrapper)函数,它执行lock-and-copy,然后调用这个封转函数来取代所有线程不安全的函数。例如下面的gethostbyname的线程安全函数。
struct hostent* gethostbyname_ts(char* host)
{
struct hostent* shared, * unsharedp;
unsharedp = Malloc(sizeof(struct hostent));
P(&mutex)
shared = gethostbyname(hostname);
//*unsharedp = * shared;//不能够使用浅拷贝,必须使用深拷贝
memcpy(unsharedp, shared, sizeof(struct hostent));
V(&mutex);
return unsharedp;
}
memcpy函数的文档:
memcpy
方法二:消除共享变量
不可重入版本:
unsigned int next = 1;
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}
消除共享变量后的可重入版本:
int rand_r(unsigned int* nextp)
{
*nextp = *nextp * 1103515245 + 12345;
return (unsigned int) (*nextp / 65536) % 32768;
}
如下列举了一些可重入函数与不可重入函数:
可重入函数
void strcpy(char *lpszDest, char *lpszSrc)
{
while(*lpszDest++=*lpszSrc++);
*dest=0;
}
不可重入函数
charcTemp;//全局变量
void SwapChar1(char *lpcX, char *lpcY)
{
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//访问了全局变量
}
不可重入函数2
void SwapChar2(char *lpcX,char *lpcY)
{
static char cTemp;//静态局部变量
cTemp=*lpcX;
*lpcX=*lpcY;
lpcY=cTemp;//使用了静态局部变量
}