确保可重入性的经验理解这五条最好的经验将帮助您保持程序的可重入性。
经验 1返回指向静态数据的指针可能会导致函数不可重入。例如,将字符串转换为大写的 strToUpper 函数可能被实现如下:
清单 3. strToUpper 的不可重入版本1
2
3
4
5
6
7
8
9
10
| char *strToUpper(char *str)
{
/*Returning pointer to static data makes it non-reentrant */
static char buffer[STRING_SIZE_LIMIT];
int index;
for (index = 0; str[index]; index++)
buffer[index] = toupper(str[index]);
buffer[index] = '\0';
return buffer;
}
|
通过修改函数的原型,您可以实现这个函数的可重入版本。下面的清单为输出准备了存储空间:
清单 4. strToUpper 的可重入版本1
2
3
4
5
6
7
8
| char *strToUpper_r(char *in_str, char *out_str)
{
int index;
for (index = 0; in_str[index] != '\0'; index++)
out_str[index] = toupper(in_str[index]);
out_str[index] = '\0';
return out_str;
}
|
由进行调用的函数准备输出存储空间确保了函数的可重入性。注意,这里遵循了标准惯例,通过向函数名添加“_r”后缀来命名可重入函数。
经验 2记忆数据的状态会使函数不可重入。不同的线程可能会先后调用那个函数,并且修改那些数据时不会通知其他正在使用此数据的线程。如果函数需要在一系列调用期间维持某些数据的状态,比如工作缓存或指针,那么调用者应该提供此数据。
在下面的例子中,函数返回某个字符串的连续小写字母。字符串只是在第一次调用时给出,如 strtok 子例程。当搜索到字符串末尾时,函数返回 \0。函数可能如下实现:
清单 5. getLowercaseChar 的不可重入版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| char getLowercaseChar(char *str)
{
static char *buffer;
static int index;
char c = '\0';
/* stores the working string on first call only */
if (string != NULL) {
buffer = str;
index = 0;
}
/* searches a lowercase character */
while(c=buff[index]){
if(islower(c))
{
index++;
break;
}
index++;
}
return c;
}
|
这个函数是不可重入的,因为它存储变量的状态。为了让它可重入,静态数据,即 index,需要由调用者来维护。此函数的可重入版本可能类似如下实现:
清单 6. getLowercaseChar 的可重入版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
| char getLowercaseChar_r(char *str, int *pIndex)
{
char c = '\0';
/* no initialization - the caller should have done it */
/* searches a lowercase character */
while(c=buff[*pIndex]){
if(islower(c))
{
(*pIndex)++; break;
}
(*pIndex)++;
}
return c;
}
|
经验 3在大部分系统中,malloc 和 free 都不是可重入的,因为它们使用静态数据结构来记录哪些内存块是空闲的。实际上,任何分配或释放内存的库函数都是不可重入的。这也包括分配空间存储结果的函数。
避免在处理器分配内存的最好方法是,为信号处理器预先分配要使用的内存。避免在处理器中释放内存的最好方法是,标记或记录将要释放的对象,让程序不间断地检查是否有等待被释放的内存。不过这必须要小心进行,因为将一个对象添加到一个链并不是原子操作,如果它被另一个做同样动作的信号处理器打断,那么就会“丢失”一个对象。不过,如果您知道当信号可能到达时,程序不可能使用处理器那个时刻所使用的流,那么就是安全的。如果程序使用的是某些其他流,那么也不会有任何问题。
经验 4为了编写没有 bug 的代码,要特别小心处理进程范围内的全局变量,如 errno 和 h_errno。考虑下面的代码:
清单 7. errno 的危险用法1
2
3
4
| if (close(fd) < 0) {
fprintf(stderr, "Error in close, errno: %d", errno);
exit(1);
}
|
假定信号在 close 系统调用设置 errno 变量到其返回之前这一极小的时间片段内生成。这个生成的信号可能会改变 errno 的值,程序的行为会无法预计。
如下,在信号处理器内保存和恢复 errno 的值,可以解决这一问题:
清单 8. 保存和恢复 errno 的值1
2
3
4
5
6
7
8
9
10
| void signalHandler(int signo){
int errno_saved;
/* Save the error no. */
errno_saved = errno;
/* Let the signal handler complete its job */
...
...
/* Restore the errno*/
errno = errno_saved;
}
|
经验 5如果底层的函数处于关键部分,并且生成并处理信号,那么这可能会导致函数不可重入。通过使用信号设置和信号掩码,代码的关键区域可以被保护起来不受一组特定信号的影响,如下:
- 保存当前信号设置。
- 用不必要的信号屏蔽信号设置。
- 使代码的关键部分完成其工作。
- 最后,重置信号设置。
下面是此方法的概述:
清单 9. 使用信号设置和信号掩码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| sigset_t newmask, oldmask, zeromask;
...
/* Register the signal handler */
signal(SIGALRM, sig_handler);
/* Initialize the signal sets */
sigemtyset(&newmask); sigemtyset(&zeromask);
/* Add the signal to the set */
sigaddset(&newmask, SIGALRM);
/* Block SIGALRM and save current signal mask in set variable 'oldmask'
*/
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
/* The protected code goes here
...
...
*/
/* Now allow all signals and pause */
sigsuspend(&zeromask);
/* Resume to the original signal mask */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
/* Continue with other parts of the code */
|
忽略 sigsuspend(&zeromask); 可能会引发问题。从消除信号阻塞到进程执行下一个指令之间,必然会有时钟周期间隙,任何在此时间窗口发生的信号都会丢掉。函数调用 sigsuspend 通过重置信号掩码并使进程休眠一个单一的原子操作来解决这一问题。如果您能确保在此时间窗口中生成的信号不会有任何负面影响,那么您可以忽略 sigsuspend 并直接重新设置信号。
在编译器层次处理可重用性我将提出一个在编译器层次处理可重入函数的模型。可以为高级语言引入一个新的关键字:reentrant,函数可以被指定一个 reentrant 标识符,以此确保函数可重入,比如:
此指示符告知编译器要专门处理那个特殊的函数。编译器可以将这个指示符存储在它的符号表中,并在中间代码生成阶段使用这个指示符。为达到此目的,编译器的前端设计需要有一些改变。此可重入指示符遵循这些准则:
- 不为连续的调用持有静态数据。
- 通过制作全局数据的本地拷贝来保护全局数据。
- 绝对不调用不可重入的函数。
- 不返回对静态数据的引用,所有数据都由函数的调用者提供。
准则 1 可以通过类型检查得到保证,如果在函数中有任何静态存储声明,则抛出错误消息。这可以在编译的语法分析阶段完成。
准则 2,全局数据的保护可以通过两种方式得到保证。基本的方法是,如果函数修改全局数据,则抛出一个错误消息。一种更为复杂的技术是以全局数据不被破坏的方式生成中间代码。可以在编译器层实现类似于前面经验 4 的方法。在进入函数时,编译器可以使用编译器生成的临时名称存储将要被操作的全局数据,然后在退出函数时恢复那些数据。使用编译器生成的临时名称存储数据对编译器来说是常用的方法。
确保准则 3 得到满足,要求编译器预先知道所有可重入函数,包括应用程序所使用的程序库。这些关于函数的附加信息可以存储在符号表中。
最后,准则 4 已经得到了准则 2 的保证。如果函数没有静态数据,那么也就不存在返回静态数据的引用的问题。
提出的这个模型将简化程序员遵循可重入函数准则的工作,而且使用此模型可以预防代码出现无意的可重入性 bug。 |