理解这五条最好的经验将帮助您保持程序的可重入性。
经验 1
返回指向静态数据的指针可能会导致函数不可重入。例如,将字符串转换为大写的 strToUpper 函数可能被实现如下:
清单 3. strToUpper 的不可重入版本
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 的可重入版本
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 的不可重入版本
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 的可重入版本
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 的危险用法
if (close(fd) < 0) {
fprintf(stderr, "Error in close, errno: %d", errno);
exit(1);
}
|
假定信号在
close
系统调用设置
errno
变量到其返回之前这一极小的时间片段内生成。这个生成的信号可能会改变
errno
的值,程序的行为会无法预计。
如下,在信号处理器内保存和恢复 errno 的值,可以解决这一问题:
清单 8. 保存和恢复 errno 的值
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. 使用信号设置和信号掩码
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
并直接重新设置信号。
(skw0rm) |