引言
在早期的编程中,不可重入性对程序员并不构成威胁;函数不会有并发访问,也没有中断。在很多较老的 C 语言实现中,函数被认为是在单线程进程的环境中运行。
不过,现在,并发编程已普遍使用,您需要意识到这个缺陷。本文描述了在并行和并发程序设计中函数的不可重入性导致的一些潜在问题。信号的生成和处理尤其增加了额外的复杂性。由于信号在本质上是异步的,所以难以找出当信号处理函数 触发某个不可重入函数时导致的 bug。
可重入函数与不可重入函数的概念
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
常用的可重入函数的方法有
1.不要使用全局变量,防止别的代码覆盖这些变量的值。
2.调用这类函数之前先关掉中断,调用完之后马上打开中断。防止函数执行期间被中断进入别的任务执行。
3.使用信号量(互斥条件)。
总而言之:要保证中断是安全的。
不可重入函数
那么不可重入函数显而易见,就是在函数执行期间,被中断,从头执行这个函数,执行完毕后再返回刚才的中断点继续执行,此时由于刚才的中断导致了现在从新在中断点执行时发生了不可预料的错误。那么这类函数就是不可重入函数。
我们知道,在中断函数之后会将中断点的一些数据保存起来,上下文数据等等,那么这些数据不是被保存了吗?怎么还会被影响?其实我们在保存上下文数据的时候,仅仅保存了一小部分,而且那一小部分也只是地址,而对全局变量、静态变量这些并没有保存。所以一旦中断之后,返回到中断点,此时就是不可预料的了。
常见的不可重入函数
- 使用了静态数据结构
- 调用了malloc和free等
- 调用了标准I/O函数
- 进行了浮点运算
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
百度百科