可重入函数与不可重入函数

简介: 可重入函数与不可重入函数

引言

      在早期的编程中,不可重入性对程序员并不构成威胁;函数不会有并发访问,也没有中断。在很多较老的 C 语言实现中,函数被认为是在单线程进程的环境中运行。

不过,现在,并发编程已普遍使用,您需要意识到这个缺陷。本文描述了在并行和并发程序设计中函数的不可重入性导致的一些潜在问题。信号的生成和处理尤其增加了额外的复杂性。由于信号在本质上是异步的,所以难以找出当信号处理函数 触发某个不可重入函数时导致的 bug。

 

可重入函数与不可重入函数的概念

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

 

常用的可重入函数的方法有

1.不要使用全局变量,防止别的代码覆盖这些变量的值。

2.调用这类函数之前先关掉中断,调用完之后马上打开中断。防止函数执行期间被中断进入别的任务执行。

3.使用信号量(互斥条件)。

总而言之:要保证中断是安全的。

 

不可重入函数

那么不可重入函数显而易见,就是在函数执行期间,被中断,从头执行这个函数,执行完毕后再返回刚才的中断点继续执行,此时由于刚才的中断导致了现在从新在中断点执行时发生了不可预料的错误。那么这类函数就是不可重入函数。

 

我们知道,在中断函数之后会将中断点的一些数据保存起来,上下文数据等等,那么这些数据不是被保存了吗?怎么还会被影响?其实我们在保存上下文数据的时候,仅仅保存了一小部分,而且那一小部分也只是地址,而对全局变量、静态变量这些并没有保存。所以一旦中断之后,返回到中断点,此时就是不可预料的了。

 

常见的不可重入函数

  1. 使用了静态数据结构
  2. 调用了malloc和free等
  3. 调用了标准I/O函数
  4. 进行了浮点运算

malloc与free是不可重入的,它们使用了全局变量来指向堆区。标准I/O大多都使用了全局数据结构。浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现)。

确保可重入的经验

经验一

返回指向静态数据的指针可能会导致函数不可重入。例如,将字符串转换为大写的 strToUpper 函数可能被实现如下:

strToUpper 的不可重入版本

1. char *strToUpper(char *str)
2. 
3. {
4. 
5.         /*Returning pointer to static data makes it non-reentrant */
6. 
7.        static char buffer[STRING_SIZE_LIMIT];
8. 
9.        int index;
10. 
11.        for (index = 0; str[index]; index++)
12. 
13.                 buffer[index] = toupper(str[index]);
14. 
15.        buffer[index] = '\0';
16. 
17.        return buffer;
18. 
19. }

通过修改函数的原型,您可以实现这个函数的可重入版本。下面的清单为输出准备了存储空间:

strToUpper 的可重入版本

1. char *strToUpper_r(char *in_str, char *out_str)
2. 
3. {
4. 
5.         int index;
6. 
7.         for (index = 0; in_str[index] != '\0'; index++)
8. 
9.         out_str[index] = toupper(in_str[index]);
10. 
11.         out_str[index] = '\0';
12. 
13.         return out_str;
14. 
15. }

经验 2

记忆数据的状态会使函数不可重入。不同的线程可能会先后调用那个函数,并且修改那些数据时不会通知其他 正在使用此数据的线程。如果函数需要在一系列调用期间维持某些数据的状态,比如工作缓存或指针,那么 调用者应该提供此数据。

 

在下面的例子中,函数返回某个字符串的连续小写字母。字符串只是在第一次调用时给出,如 strtok 子例程。当搜索到字符串末尾时,函数返回 \0。函数可能如下实现:

1. 
2. getLowercaseChar 的不可重入版本
3. 
4. char getLowercaseChar(char *str)
5. 
6. {
7. 
8.         static char *buffer;
9. 
10.         static int index;
11. 
12.         char c = '\0';
13. 
14.         /* stores the working string on first call only */
15. 
16.         if (string != NULL) {
17. 
18.                 buffer = str;
19. 
20.                 index = 0;
21. 
22.         }
23. 
24.         /* searches a lowercase character */
25. 
26.         while(c=buff[index]){
27. 
28.          if(islower(c))
29. 
30.          {
31. 
32.              index++;
33. 
34.              break;
35. 
36.          }
37. 
38.         index++;
39. 
40.        }
41. 
42.       return c;
43. 
44. }

这个函数是不可重入的,因为它存储变量的状态。为了让它可重入,静态数据,即 index, 需要由调用者来维护。此函数的可重入版本可能类似如下实现:

1. getLowercaseChar 的可重入版本
2. 
3. char getLowercaseChar_r(char *str, int *pIndex)
4. 
5. {
6. 
7.         char c = '\0';
8. 
9.         /* no initialization - the caller should have done it */
10. 
11.         /* searches a lowercase character */
12. 
13.        while(c=buff[*pIndex]){
14. 
15.           if(islower(c))
16. 
17.           {
18. 
19.              (*pIndex)++; break;
20. 
21.           }
22. 
23.        (*pIndex)++;
24. 
25.        }
26. 
27.          return c;
28. 
29. }

经验 3

在大部分系统中,malloc 和 free 都不是可重入的, 因为它们使用静态数据结构来记录哪些内存块是空闲的。实际上,任何分配或释放内存的库函数都是不可重入的。这也包括分配空间存储结果的函数。

 

避免在处理器分配内存的最好方法是,为信号处理器预先分配要使用的内存。避免在处理器中释放内存的最好方法是, 标记或记录将要释放的对象,让程序不间断地检查是否有等待被释放的内存。不过这必须要小心进行,因为将一个对象 添加到一个链并不是原子操作,如果它被另一个做同样动作的信号处理器打断,那么就会“丢失”一个对象。不过, 如果您知道当信号可能到达时,程序不可能使用处理器那个时刻所使用的流,那么就是安全的。如果程序使用的是某些其他流,那么也不会有任何问题。

 

经验 4

为了编写没有 bug 的代码,要特别小心处理进程范围内的全局变量,如 errno 和 h_errno。 考虑下面的代码:

1. errno 的危险用法
2. 
3. if (close(fd) < 0) {
4. 
5.   fprintf(stderr, "Error in close, errno: %d", errno);
6. 
7.   exit(1);
8. 
9. }

假定信号在 close 系统调用设置 errno 变量 到其返回之前这一极小的时间片段内生成。这个生成的信号可能会改变 errno 的值,程序的行为会无法预计。

 

如下,在信号处理器内保存和恢复 errno 的值,可以解决这一问题:

1. 保存和恢复 errno 的值
2. 
3. void signalHandler(int signo){
4. 
5.   int errno_saved;
6. 
7.   /* Save the error no. */
8. 
9.   errno_saved = errno;
10. 
11.   /* Let the signal handler complete its job */
12. 
13.   ...
14. 
15.   ...
16. 
17.   /* Restore the errno*/
18. 
19.   errno = errno_saved;
20. 
21. }

经验 5

如果底层的函数处于关键部分,并且生成并处理信号,那么这可能会导致函数不可重入。通过使用信号设置和 信号掩码,代码的关键区域可以被保护起来不受一组特定信号的影响,如下:

 

保存当前信号设置。

用不必要的信号屏蔽信号设置。

使代码的关键部分完成其工作。

最后,重置信号设置。

下面是此方法的概述:

使用信号设置和信号掩码

1. sigset_t newmask, oldmask, zeromask;
2. 
3. ...
4. 
5. /* Register the signal handler */
6. 
7. signal(SIGALRM, sig_handler);
8. 
9. /* Initialize the signal sets */
10. 
11. sigemtyset(&newmask); sigemtyset(&zeromask);
12. 
13. /* Add the signal to the set */
14. 
15. sigaddset(&newmask, SIGALRM);
16. 
17. /* Block SIGALRM and save current signal mask in set variable 'oldmask'
18. 
19. */
20. 
21. sigprocmask(SIG_BLOCK, &newmask, &oldmask);
22. 
23. /* The protected code goes here
24. 
25. ...
26. 
27. ...
28. 
29. */
30. 
31. /* Now allow all signals and pause */
32. 
33. sigsuspend(&zeromask);
34. 
35. /* Resume to the original signal mask */
36. 
37. sigprocmask(SIG_SETMASK, &oldmask, NULL);
38. 
39. /* Continue with other parts of the code */

忽略 sigsuspend(&zeromask); 可能会引发问题。从消除信号阻塞到进程执行下一个 指令之间,必然会有时钟周期间隙,任何在此时间窗口发生的信号都会丢掉。函数调用 sigsuspend 通过重置信号掩码并使进程休眠一个单一的原子操作来解决这一问题。如果您能确保在此时间窗口中生成的信号不会有任何 负面影响,那么您可以忽略 sigsuspend 并直接重新设置信号。

 

参考链接:

https://www.ibm.com/developerworks/cn/linux/l-reent.html

百度百科

https://blog.csdn.net/liuchenxia8/article/details/79961851

相关文章
|
2月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。
|
2月前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
6月前
|
存储 安全 Linux
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
410 0
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
|
安全 Java
Java线程安全:同步方法、同步代码块、volatile 变量和原子变量
Java线程安全:同步方法、同步代码块、volatile 变量和原子变量
227 1
|
安全 Linux 调度
【Linux系统编程】可重入和不可重入函数
【Linux系统编程】可重入和不可重入函数
201 0
|
物联网 Linux 开发者
可重入函数|学习笔记
快速学习可重入函数
|
物联网 Linux 开发者
线程函数传参|学习笔记
快速学习线程函数传参
|
Linux
Linux系统编程-(pthread)线程通信(互斥锁)
这篇文章介绍Linux下线程同步与互斥机制--互斥锁,在多线程并发的时候,都会出现多个消费者取数据的情况,这种时候数据都需要进行保护,比如: 火车票售票系统、汽车票售票系统一样,总票数是固定的,但是购票的终端非常多。
450 0
|
安全 编译器 调度
C语言可重入函数和不可重入函数
C语言可重入函数和不可重入函数
294 0
|
Linux 调度
关于可重入函数
关于可重入函数
130 0