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

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

引言

      在早期的编程中,不可重入性对程序员并不构成威胁;函数不会有并发访问,也没有中断。在很多较老的 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

相关文章
|
物联网
低功耗蓝牙(BLE)设备常用的4种角色
对于主从设备的其它说法,大家需要了解一下。对于Central和Peripheral有多种说法,上面我们说的是主从,还有客户端/服务端,中心设备/外围设备,我们这里简单介绍一下,客户端(Client)对应上面的Central,接收数据;服务端(Server)对应上面的额Peripheral,提供数据,这个需要和网站的服务器/客户端区别一下;中心设备(Central)和外围设备(Peripheral),其实上面叫中心设备和外围设备。上面主设备(Master)和从设备(Slave)应该对应主/从。这个根据个人习惯,主/从用的比较多,如果在蓝牙中提到这些知道就行了。
1731 0
|
IDE 编译器 Linux
linux 编译 c或cpp 文件为动态库 so 文件(最简单直观的模板)
linux 编译 c或cpp 文件为动态库 so 文件(最简单直观的模板)
|
1月前
|
存储 弹性计算 关系型数据库
阿里云服务器及其他云产品免费试用活动解析:160多款产品支持试用,先试后用
阿里云推出涵盖160多款云产品的免费试用活动,覆盖计算、数据库、存储、网络与安全、大数据与AI等全场景。活动包含个人/企业版云服务器ECS(最高8核16G配置)、轻量应用服务器(预装宝塔面板等镜像)、RDS数据库(MySQL/PostgreSQL版)、对象存储OSS等核心产品,提供300元-660元试用金、流量包及企业级规格试用。
225 12
|
5月前
|
人工智能 JavaScript 前端开发
Github 2024-10-28 开源项目周报 Top15
本周GitHub热门项目涵盖Svelte、Open Interpreter、PowerShell等,涉及Web开发、AI助手、自动化工具等领域,Python、JavaScript为主流语言,展现开源技术活跃生态。(239字)
604 19
|
存储 安全 Linux
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
1055 0
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
|
传感器 安全 Linux
linux为什么不是实时操作系统
标准Linux内核并不是实时操作系统,因为它在任务调度、中断处理和内核抢占方面无法提供严格的时间确定性。然而,通过使用PREEMPT_RT补丁、Xenomai等实时扩展,可以增强Linux的实时性能,使其适用于某些实时应用场景。在选择操作系统时,需要根据具体应用的实时性要求,综合考虑系统的性能和可靠性。
441 1
|
移动开发 C语言
C语言:&&和&、||和|有什么区别
在C语言中,&&和||是逻辑运算符,分别表示逻辑与(AND)和逻辑或(OR),它们用于连接两个布尔表达式,只有当两边都为真时&&返回真,||在至少一边为真时返回真;&和|是位运算符,对应地进行位级的与、或操作,它们对操作数的二进制位进行逐位处理。&&和||具有短路特性,而&和|没有。
15130 1
|
Windows
windows 11 恢复右键传统菜单
windows 11 恢复右键传统菜单
625 27