如果一个函数仅操作局部变量,那么该函数可以说是个可重入函数,因为每次调用函数时,都会有一个独立的函数帧,即拥有各自的栈,而这些局部变量都是存在栈中的,因此即使多线程,并发的调用这些函数以及这些函数在调用过程中被信号所中断都不会发生什么危险的情况。
那么非可重入函数具有什么样的特征呢?
一般的,非可重入函数大多会有操作全局变量或静态数据结构的动作。如同时两次调用这样的函数,它们都试图去更新同一个全局变量或者数据结构,那么此时容易发生错误 -- 例如,假设线程 A 正在增加一个新的节点到一个链表结构中,与此同时另一个线程B也要试图更新这个链表结构。由于增加一个新的节点涉及对多个指针的更新,因此如果另一个线程中途打断了这些动作,那势必引起混乱。
上述情况在标准 C 库中是普遍存在的。比如,在主函数中使用 malloc() 分配内存时突然被信号中断,而在信号处理函数中同样使用 malloc() 时,那么这个链表就会被破坏。因此,像 malloc() 及其族中其它的函数,抑或是使用了函数的其它库函数,都是非可重入函数。
此外,还有其它的库函数,如crypt() , getpwnam() ,gethostbyname() 以及 getservbyname() 等函数的返回信息都使用了静态内存分配技术。如果在信号处理函数中也使用这些函数,那么就会其返回值就会覆盖之前在主函数或其它地方调用这些函数的返回值。
如果函数使用了静态数据结构那么它们也是非可重入函数,最为常见的是 stdio 库中的 printf(), scanf() 这些函数,它们会对缓存了的 I/O 进行内部数据更新。因此,如果在主函数执行 printf() 或其它类似函数的过程中发生信号中断,而在信号处理函数中同样使用 printf() 时,有时会看到一些奇怪的输出,或是数据被破坏,甚至是程序的崩溃。
尽管我们可能不直接使用不可重入函数,但这并不表示可不会发生可重入造成的事件。比如,在主函数中我们在更新一个自定义的数据结构,与此同时被信号中断,而在信号处理函数中同样对该结构进行更新,此时也可以说信号处理函数是相对主函数是不可重入的。
下面的程序代码将主要使用 crypt() 这个函数演示在使用非可重入函数时将带来的问题,代码如下:
[C++] 纯文本查看 复制代码 #define _XOPEN_SOURCE 600
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
static char *str2;
static int handled = 0; //计算信号处理函数的调用次数
static void handler(int sig)
{
crypt(str2, "xx");
handled++;
}
int main(int argc, char **argv)
{
char *cr1;
int call_num, mis_match;
struct sigaction sa;
if (argc != 3) {
printf ("Usage: %s str1 str2\n", argv[0]);
exit (EXIT_FAILURE);
}
str2 = argv[2];
cr1 = strdup(crypt(argv[1], "xx"));
if (cr1 == NULL) {
perror("strdup");
exit (EXIT_FAILURE);
}
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = handler;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit (EXIT_FAILURE);
}
for (call_num = 1, mis_match = 0; ; call_num++) {
if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
mis_match++;
printf ("Mismatch on call %d (mismatch=%d handled=%d)\n", call_num, mis_match, handled);
}
}
return 0;
}
注意,在较新的 Linux 发行版中(使用了较新版的 glibc )可能需要使用 -lcrypt 选项来编译该程序,如:$ gcc -lcrypt nonreentrant.c -o nonreentrant
运行该程序,然后不断的按下 Ctrl + c 组合键(想退出程序时可以按下 Ctrl + \ 组合键,这样会发送一个 SIGQUIT 信号,该信号默认结束程序并产生 coredump 文件),那么会看到:[beyes@beyes signal]$ ./nonreentrant 1234 5678
^C^C^C^C^C^C^CMismatch on call 3825071 (mismatch=1 handled=7)
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^CMismatch on call 7479454 (mismatch=2 handled=34)
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^CMismatch on call 8498596 (mismatch=3 handled=64)
^C^C^C^C^C^CMismatch on call 8531204 (mismatch=4 handled=70)
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^CMismatch on call 8945749 (mismatch=5 handled=146)
^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^CMismatch on call 9179311 (mismatch=6 handled=189)
^C^C^C^C^\退出(吐核) 在上面程序中,cr1 用来指向对命令行第 1 个参数加密后的内容。在 for 循环中,不断的将 crypt() 对第一个命令行参数加密后的内容和之前存储的内容相比较,查看是否相等。如果不发生 SIGINT 信号的话(按下 Ctrl + c 组合键),那么不会发生什么事,也就是比较结果永远相等。但是,像我们上面,不断按下 Ctrl + c 时,如果在执行 crypt() 时(strcmp()函数中执行)送来了信号,那么就会去执行信号处理程序,而在其中,刚好又使用了 crypt() 函数,它加密的是命令行的第 2 个参数。正如上面提到的,crypt() 函数的返回值使用的是静态内存分配,即返回值都放在同一块内存区域。因此,如果信号正好在执行完主函数中的 crypt() 后而在比较之前,那么在执行信号处理函数后,主函数中的 crypt() 的用以存放返回值的内存区域将被信号处理函数中的返回值所覆盖,因此最后的比较结果势必不一样。
上面就是在信号处理函数中使用非可重入函数带来的问题。 |