编写可重入和线程安全的代码
在单线程的进程中,有且仅有一个控制流。这种进程执行的代码不必是可重入的,或线程安全的。在多线程的程序中,同一个函数或是同一个资源可能被多个控制流并发地访问。为了保证资源的完整性,为多线程程序写的代码必须是可重入的和线程安全的。
本节提供了一些编写可重入和线程安全的代码的信息。
理解可重入和线程安全
可重入和线程安全都是指函数处理资源的方式。可重入和线程安全是两个相互独立的概念,一个函数可以仅是可重入的,可以仅是线程安全的,可以两者都是,可以两者都不是。
可重入
一个可重入的函数不能为后续的调用保持静态(或全局)数据,也不能返回指向静态(或全局)数据的指针。函数中用到的所有的数据,都应该由该函数的调用者提供(不包括栈上的局部数据)。一个可重入的函数不能调用不可重入的函数。
一个不可重入的函数经常可以(但不总是可以)通过它的外部接口和功能识别出来。例如strtok是不可重入的,因为它保存着将被识别为token的字符串。ctime也不是一个可重入的函数,它会返回一个指向静态数据的指针,每次调用都可能覆盖这些数据。
线程安全
一个线程安全的函数通过“锁”来保护共享的资源不被并发地访问。“线程安全”仅关心函数的实现,而不影响它的外部接口。
在C中,局部变量是在栈上动态分配的,因此,任何一个不使用静态数据和其它共享资源的函数就是最平凡的线程安全的。例如,下面这个函数就是线程安全的:
/* thread-safe function */
int diff(int x,int y)
{
int delta;
delta=y-x;
if(delta<0)
delta=-delta;
return delta;
}
对全局变量的使用是线程“不安全”的。应该为每一个线程维护一份拷贝,或者将其封装起来,使得对它的访问变成串行的。
使一个函数变成可重入的
在大部分情况下,不可重入的函数修改成可重入的函数时,需要修改函数的对外接口。不可重入的函数不能被多线程的程序调用。一个不可重入的函数,基本上不可能是线程安全的。
返回值
很多不可重入的函数返回一个指向静态数据的指针。这个问题可以有两种解决办法:
1、返回从堆中分配的空间的地址。在这种情况下,调用者必须负责释放堆中的空间。这种办法的优点是不必修改函数的外部接口,但是不能向后兼容。现存的单线程的程序使用修改后的函数会导致内存泄露(因为它们没有释放空间)。
2、由调用者提供空间。尽管函数的外部接口需要改变,仍然推荐这种方法。
例如,一个strtoupper函数(个人感觉这个东东写得很儍)一个字符串转换成大写,实现为:
/* non-reentrant function */
char *srttoupper(char *string)
{
static char buffer[MAX_STR_SIZE];
int index;
for(index=0;string[index];index++)
buffer[index]=toupper(string[index]);
buffer[index]=0;
return buffer;
}
该函数既不是可重入的,也不是线程安全的。使用第一种方法将其改写为可重入的:
/* reentrant function (a poor solution)*/
char *strtoupper(char *string)
{
char *buffer;
int index;
buffer=malloc(MAX_STR_SIZE);
/*error check should be checked*/
for(index=0;string[index];index++)
buffer[index]=toupper(string[index]);
buffer[index]=0;
return buffer;
}
使用第二种方法解决:
/* reentrant function (a better solution)*/
char *strtoupper_r(char *in_str,char *out_string)
{
int index;
for(index=0;in_str[index];index++)
out_str[index]=toupper(in_str[index]);
out_str[index]=0;
return out_str;
}
为后继的调用保持数据
一个可重入的函数不应该为后续的调用保持数据(即后继的调用和本次调用无关),因为下一次调用可能是由不同的线程调用的。如果一个函数需要在连续的调用之间维护一些数据,例如一个工作缓冲区或是一个指针,这些数据(资源)应该由调用这个函数的函数提供。
例如:返回指定字符串中的下一个小写字符的函数。字符串只有在第一次调用时被提供,就像strtok一样。到达末尾时返回0(我认为这个函数写得有问题,姑且明白大意就好):
/* non-reentrant function*/
char lowercase_c(char *string)
{
static char *buffer;
static int index;
char c=0;
if(string!=NULL){
buffer=string;
index=0;
}
for(;c=buffer[index];index++){
if(islower(c)){
index++;
break;
}
}
return c;
}
该函数是不可重入的。要使它改写成可重入的,其中的静态数据应该由它的调用者维护。
/* reentrant function*/
char reentrant_lowercase_c(char *string,int *p_index)
{
char c=0;
for(;c=string[*p_index];(*p_index)++){
if(islower(c)){
(*p_index)++;
break;
}
}
return c;
}
函数的对外接口和使用方法均改变了。
使一个函数变成线程安全的
在一个多线程的程序中,所有的被多线程调用的函数多必须是线程安全的(或可重入的)。注意,不可重入的函数一般都是线程“不安全”的,然而,将它们改写成可重入的同时,一般就会将它们变成线程安全的。
“锁”住共享资源
使用静态数据或者其它任何共享资源(如文件、终端等)的函数,必须对这些资源加“锁”以实现对它们的串行访问,这样才能成为线程安全的函数。例如:
/* thread-unsafe function*/
int increament_counter()
{
static int counter=0;
counter++;
return counter;
}
/* pseudo-code thread-safe function*/
int increment_counter()
{
static int counter=0;
static lock_type counter_lock=LOCK_INITIALIZER;
lock(counter_lock);
counter++;
unlock(counter_lock);
return counter;
}
在一个使用线程库的多线程程序中,应该使用信号量来串行化共享资源的访问,或者其它“锁”
后面还有两节:
A Workaround for Thread-Unsafe Functions
Reentrant and Thread-Safe Libraries
与本文主题关系不大...
综上所述:
可重入:多个线程调用是不会互相影响。例如第一个lowercase_c函数是不可重入的,在另一个线程中第二次调用它时,必将覆盖掉第一个线程中的设定的字符串。
线程安全:解决多个线程共享资源的问题。
使用可重入函数进行更安全的信号处理
何时如何利用可重入性避免代码出现 bug
如果要对函数进行并发访问,不管是通过线程还是通过进程,您都可能会遇到函数不可重入所导致的问题。在本文中,通过示例代码了解如果可重入性不能得到保证会产生何种异常,尤其要注意信号。引入了五条可取的编程经验,并对提出的编译器模型进行了讨论,在这个模型中,可重入性由编译器前端处理。
1 评论:
(dipakjha@in.ibm.com), 软件工程师, IBM
Dipak 为分布式文件系统(Distributed File System,DFS)提供 Level 3
支持。他的工作包括,对内存转储和崩溃进行内核级和用户级的调试,以及修复 AIX 和 Solaris 平台上所报告的 bug。通过 dipakjha@in.ibm.com 与 Dipak
联系。
2005 年 2 月 20 日
在 IBM Bluemix 云平台上开发并部署您的下一个应用。
开始您的试用
在早期的编程中,不可重入性对程序员并不构成威胁;函数不会有并发访问,也没有中断。在很多较老的 C
语言实现中,函数被认为是在单线程进程的环境中运行。
不过,现在,并发编程已普遍使用,您需要意识到这个缺陷。本文描述了在并行和并发程序设计中函数的不可重入性导致的一些潜在问题。信号的生成和处理尤其增加了额外的复杂性。由于信号在本质上是异步的,所以难以找出当信号处理函数触发某个不可重入函数时导致的
bug。
本文:
-
定义了可重入性,并包含一个可重入函数的 POSIX 清单。
-
给出了示例,以说明不可重入性所导致的问题。
-
指出了确保底层函数的可重入性的方法。
-
讨论了在编译器层次上对可重入性的处理。
什么是可重入性?
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反, 不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。
可重入函数:
-
不为连续的调用持有静态数据。
-
不返回指向静态数据的指针;所有数据都由函数的调用者提供。
-
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
-
绝不调用任何不可重入函数。
不要混淆可重入与线程安全。在程序员看来,这是两个独立的概念:函数可以是可重入的,是线程安全的,或者二者皆是,或者二者皆非。不可重入的函数不能由多个线程使用。另外,或许不可能让某个不可重入的函数是线程安全的。
IEEE Std 1003.1 列出了 118 个可重入的 UNIX? 函数,在此没有给出副本。参见 参考资料 中指向 unix.org 上此列表的链接。
出于以下任意某个原因,其余函数是不可重入的:
-
它们调用了
malloc
或 free
。
-
众所周知它们使用了静态数据结构体。
-
它们是标准 I/O 程序库的一部分。
回页首
信号和不可重入函数
信号(signal) 是软件中断。它使得程序员可以处理异步事件。为了向进程发送一个信号,内核在进程表条目的信号域中设置一个位,对应于收到的信号的类型。信号函数的 ANSI C
原型是:
void (*signal (int sigNum, void (*sigHandler)(int))) (int);
或者,另一种描述形式:
typedef void sigHandler(int);
SigHandler *signal(int, sigHandler *);
当进程处理所捕获的信号时,正在执行的正常指令序列就会被信号处理器临时中断。然后进程继续执行,但现在执行的是信号处理器中的指令。如果信号处理器返回,则进程继续执行信号被捕获时正在执行的正常的指令序列。
现在,在信号处理器中您并不知道信号被捕获时进程正在执行什么内容。如果当进程正在使用 malloc
在它的堆上分配额外的内存时,您通过信号处理器调用 malloc
,那会怎样?或者,调用了正在处理全局数据结构的某个函数,而在信号处理器中又调用了同一个函数。如果是调用 malloc
,则进程会被严重破坏,因为 malloc
通常会为所有它所分配的区域维持一个链表,而它又可能正在修改那个链表。
甚至可以在需要多个指令的 C
操作符开始和结束之间发送中断。在程序员看来,指令可能似乎是原子的(也就是说,不能被分割为更小的操作),但它可能实际上需要不止一个处理器指令才能完成操作。例如,看这段
C 代码:
在 x86 处理器上,那个语句可能会被编译为:
mov ax,[temp]
inc ax
mov [temp],ax
这显然不是一个原子操作。
这个例子展示了在修改某个变量的过程中运行信号处理器可能会发生什么事情:
清单 1. 在修改某个变量的同时运行信号处理器
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1)
{data = zeros; data = ones;}
}
这个程序向 data
填充 0,1,0,1,一直交替进行。同时,alarm 信号处理器每一秒打印一次当前内容(在处理器中调用 printf
是安全的,当信号发生时它确实没有在处理器外部被调用)。您预期这个程序会有怎样的输出?它应该打印 0,0 或者
1,1。但是实际的输出如下所示:
0, 0
1, 1
(Skipping some output...)
0, 1
1, 1
1, 0
1, 0
...
在大部分机器上,在 data
中存储一个新值都需要若干个指令,每次存储一个字。如果在这些指令期间发出信号,则处理器可能发现 data.a
为 0 而 data.b
为
1,或者反之。另一方面,如果我们运行代码的机器能够在一个不可中断的指令中存储一个对象的值,那么处理器将永远打印 0,0 或 1,1。
使用信号的另一个新增的困难是,只通过运行测试用例不能够确保代码没有信号 bug。这一困难的原因在于信号生成本质上异步的。
回页首
不可重入函数和静态变量
假定信号处理器使用了不可重入的 gethostbyname
。这个函数将它的值返回到一个静态对象中:
static struct hostent host; /* result stored here*/
它每次都重新使用同一个对象。在下面的例子中,如果信号刚好是在 main
中调用 gethostbyname
期间到达,或者甚至在调用之后到达,而程序仍然在使用那个值,则它将破坏程序请求的值。
清单 2. gethostbyname 的危险用法
main(){
struct hostent *hostPtr;
...
signal(SIGALRM, sig_handler);
...
hostPtr = gethostbyname(hostNameOne);
...
}
void sig_handler(){
struct hostent *hostPtr;
...
/* call to gethostbyname may clobber the value stored during the call
inside the main() */
hostPtr = gethostbyname(hostNameTwo);
...
}
不过,如果程序不使用 gethostbyname
或者任何其他在同一对象中返回信息的函数,或者如果它每次使用时都会阻塞信号,那么就是安全的。
很多库函数在固定的对象中返回值,总是使用同一对象,它们全都会导致相同的问题。如果某个函数使用并修改了您提供的某个对象,那它可能就是不可重入的;如果两个调用使用同一对象,那么它们会相互干扰。
当使用流(stream)进行 I/O 时会出现类似的情况。假定信号处理器使用 fprintf
打印一条消息,而当信号发出时程序正在使用同一个流进行 fprintf
调用。信号处理器的消息和程序的数据都会被破坏,因为两个调用操作了同一数据结构:流本身。
如果使用第三方程序库,事情会变得更为复杂,因为您永远不知道哪部分程序库是可重入的,哪部分是不可重入的。对标准程序库而言,有很多程序库函数在固定的对象中返回值,总是重复使用同一对象,这就使得那些函数不可重入。
近来很多提供商已经开始提供标准 C
程序库的可重入版本,这是一个好消息。对于任何给定程序库,您都应该通读它所提供的文档,以了解其原型和标准库函数的用法是否有所变化。
回页首
确保可重入性的经验
理解这五条最好的经验将帮助您保持程序的可重入性。
经验 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
并直接重新设置信号。
回页首
在编译器层次处理可重用性
我将提出一个在编译器层次处理可重入函数的模型。可以为高级语言引入一个新的关键字: reentrant
,函数可以被指定一个 reentrant
标识符,以此确保函数可重入,比如:
此指示符告知编译器要专门处理那个特殊的函数。编译器可以将这个指示符存储在它的符号表中,并在中间代码生成阶段使用这个指示符。为达到此目的,编译器的前端设计需要有一些改变。此可重入指示符遵循这些准则:
-
不为连续的调用持有静态数据。
-
通过制作全局数据的本地拷贝来保护全局数据。
-
绝对不调用不可重入的函数。
-
不返回对静态数据的引用,所有数据都由函数的调用者提供。
准则 1 可以通过类型检查得到保证,如果在函数中有任何静态存储声明,则抛出错误消息。这可以在编译的语法分析阶段完成。
准则
2,全局数据的保护可以通过两种方式得到保证。基本的方法是,如果函数修改全局数据,则抛出一个错误消息。一种更为复杂的技术是以全局数据不被破坏的方式生成中间代码。可以在编译器层实现类似于前面经验
4
的方法。在进入函数时,编译器可以使用编译器生成的临时名称存储将要被操作的全局数据,然后在退出函数时恢复那些数据。使用编译器生成的临时名称存储数据对编译器来说是常用的方法。
确保准则 3 得到满足,要求编译器预先知道所有可重入函数,包括应用程序所使用的程序库。这些关于函数的附加信息可以存储在符号表中。
最后,准则 4 已经得到了准则 2 的保证。如果函数没有静态数据,那么也就不存在返回静态数据的引用的问题。
提出的这个模型将简化程序员遵循可重入函数准则的工作,而且使用此模型可以预防代码出现无意的可重入性 bug。
号处理程序中应当使用可再入(可重入)函数(注:所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错)。因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数。
满足下列条件的函数多数是不可再入的:
(1)使用静态的数据结构,如getlogin(),gmtime(),getgrgid(),getgrnam
(),getpwuid()以及getpwnam()等等;
(2)函数实现时,调用了malloc()或者free()函数;
(3)实现时使用了标准 I/O函数的。
The Open Group视下列函数为可再入的:
_exit()、
access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed
()、chdir()、chmod()、chown()、close()、creat()、dup()、dup2()、
execle()、execve()、fcntl()、fork()、
fpathconf ()、fstat()、fsync()、getegid()、
geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、
kill()、
link()、lseek()、mkdir()、mkfifo()、
open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、
setgid ()、setpgid()、setsid()、setuid()、
sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、
sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、
stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、
tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、
umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。
即使信号处理函数使用的都是"安全函数",同样要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。另外,longjmp()以及siglongjmp()没有被列为可再入函数,因为不能保证紧接着两个函数的其它调用是安全的。
C语言之可重入函数 && 不可重入函数
时间:
2014-08-14 16:41:58 阅读:
673 评论: 收藏:
0 [点我收藏+]
标签:des class style 代码 src 问题 使用 数据
可重入函数
在
实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数不幸被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任
务调用这个函数的数据,从而导致不可预料的后果。那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会
出错。不可重入函数在实时系统设计中被视为不安全函数。
满足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
如何写出可重入的函数?在函数体内不访问那些全局变量,不使用静态局部变量,坚持只使用缺省态(auto)局部变量,写出的函数就将是可重入的。如果必须访问全局变量,记住利用互斥信号量来保护全局变量。或者调用该函数前关中断,调用后再开中断。
可重入函数可以被一个以上的任务调用,而不必担心数据被破坏。可重入函数任何时候都可以被中断,一段时间以后又可以运行,而相应的数据不会丢失。可重入函数或者只使用局部变量,即保存在CPU寄存器中或堆栈中;或者使用全局变量,则要对全局变量予以保护。
说法2:
一个可重入的函数简单来说,就是:可以被中断的函数。就是说,你可以在这个函数执行的任何时候中断他的运行,在任务调度下去执行另外一段代
码而不会出现什么错误。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等等,所以他如果被中断的话,可能出现问题,所以这类函数是
不能运行在多任务环境下的。
基本上下面的函数是不可重入的
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写他。
其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的。
第一,不要使用全局变量。因为别的代码很可能覆盖这些变量值。
第二,在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”或者用OS_ENTER_KERNAL/OS_EXIT_KERNAL来描述。//这是临界区保护
第三,不能调用任何不可重入的函数。
第四,谨慎使用堆栈。最好先在使用前先OS_ENTER_KERNAL。
还有一些规则,都是很好理解的,总之,时刻记住一句话:保证中断是安全的!
相信很多人都看过下面这个面试题
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字
__interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt
double compute_area (double radius)
{
double area = PI * radius *
radius;
printf("\nArea = %f", area);
return
area;
}
这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR
不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2) ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3)
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4)
与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。
-->
如果说中断服务程序有返回值,那么它的值返回给谁呢?
在系统的运行过程中,一定是某种中断源出发了相应的中断,系统上挂接的中断服务程序进行现场的处理,例如告警等操作,然后清中断。
也就是说中断服务程序链接在某一类中断源上,而这些中断源的产生是随机的,所以,中断服务程序并没有一个固定的调用者,也没有固定的返回地址,所以返回值也没有用
我的问题是,这里所说的printf()经常有重入的问题,具体是指什么?有人能给解释一下么?
这个概念在嵌入式操作系统中比较重要,由于存在任务的调度,它实时系统,可剥夺型内核中是危险的,如同一个安静的水雷。可能会被触发,也可能安然无恙。由于它运行结果的不可预期性,会使系统带来隐患。
下面引用一段别人的解释:
这主要在多任务环境中使用,一个可重入的函数简单来说,就是:可以被中断的函数。就是说,你可以在这个函数执行的任何时候中断他的运行,在OS的调度下去执行另外一段代码而不会出现什么错误。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等等,所以他如果被中断的话,可能出现问题,所以这类函数是不能运行在多任务环境下的。
把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写他。
其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的。
第一,不要使用全局变量。因为别的代码很可能覆盖这些变量值。
第二,在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”或者用OS_ENTER_KERNAL/OS_EXIT_KERNAL来描述。
第三,不能调用任何不可重入的函数。
第四,谨慎使用堆栈。最好先在使用前先OS_ENTER_KERNAL。
还有一些规则,都是很好理解的,总之,时刻记住一句话:保证中断是安全的!
通俗的来讲吧:由于中断是可能随时发生的,断点位置也是无法预期的。所以必须保证每个函数都具有不被中断发生,压栈,转向ISR,弹栈后继续执行影响的稳定性。也就是说具有不会被中断影响的能力。既然有这个要求,你提供和编写的每个函数就不能拿公共的资源或者是变量来使用,因为该函数使用的同时,ISR(中断服务程序)也可那会去修改或者是获取这个资源,从而有可能使中断返回之后,这部分公用的资源已经面目全非。
满足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
下面举例加以说明。
可重入函数
void strcpy(char* lpszDest, char*
lpszSrc)
{
while(*lpszDest++ = *lpszSrc++);
*dest=0;
}
非可重入函数1
char cTemp; //
全局变量
void SwapChar1(char* lpcX, char* lpcY)
{
cTemp =
*lpcX;
*lpcX = *lpcY;
lpcY = cTemp; //
访问了全局变量,在分享内存的多个线程中可能造成问题
}
非可重入函数2
void SwapChar2(char*
lpcX, char* lpcY)
{
static char cTemp; // 静态局部变量
cTemp = *lpcX;
*lpcX = *lpcY;
lpcY = cTemp; //
使用了静态局部变量,在分享内存的多个线程中可能造成问题
}
如何写出可重入的函数?在函数体内不访问那些全局变量,不使用静态局部变量,坚持只使用局部变量,写出的函数就将是可重入的。如果必须访问全局变量,记住利用互斥信号量来保护全局变量。
什么是可重入过程?
开发实时占先式的操作系统,可重入函数是非用不可的。可重入函数可以被一个以上的任务调用,而不必担心数据被破坏。可重入函数任何时候都可以被中断,一段时间后又可以运行,而应用数据不会丢失。使得函数具有可重入性必须使得函数能够满足下列三个条件之一:
① 不使用共享资源;
② 在使用共享资源时关中断,使用完毕后再开中断;
③ 在使用共享资源时申请信号量,使用完后释放信号量。
这些条件在标准C中编程很容易实现,但是在Keil C51中就比较麻烦。因为标准C是把局部变量分配到用户堆栈中(动态分配),而Keil C51将局部变量分配到寄存器或内存固定地址(静态分配),并通过变量覆盖分析的方法,使多个函数的局部变量使用相同的内存地址以减少内存占用。在Keil C51中,如果局部变量分配在寄存器中还好些,如果局部变量分配在内存中就比较麻烦。
若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
函数可重入性及编写规范
一、可重入函数
1)什么是可重入性?
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,
不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或
者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,
稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时
保护自己的数据。
2)可重入函数:
不为连续的调用持有静态数据。
不返回指向静态数据的指针;所有数据都由函数的调用者提供。
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
如果必须访问全局变量,记住利用互斥信号量来保护全局变量。
绝不调用任何不可重入函数。
3)不可重入函数:
函数中使用了静态变量,无论是全局静态变量还是局部静态变量。
函数返回静态变量。
函数中调用了不可重入函数。
函数体内使用了静态的数据结构;
函数体内调用了malloc()或者free()函数;
函数体内调用了其他标准I/O函数。
函数是singleton中的成员函数而且使用了不使用线程独立存储的成员变量 。
总的来说,如果一个函数在重入条件下使用了未受保护的共享的资源,那么它是不可重入
的。
4)示例
在多线程条件下,函数应当是线程安全的,进一步,更强的条件是可重入的。可重入函数
保证了在多线程条件下,函数的状态不会出现错误。以下分别是一个不可重入和可重入函
数的示例:
//c code
static int tmp;
void func1(int* x, int* y) {
tmp=*x;
*x=*y;
*y=tmp;
}
void func2(int* x, int* y) {
int tmp;
tmp=*x;
*x=*y;
*y=tmp;
}
func1是不可重入的,func2是可重入的。因为在多线程条件下,操作系统会在func1还没有
执行完的情况下,切换到另一个线程中,那个线程可能再次调用func1,这样状态就错了。
二、函数编写规范
1 :对所调用函数的错误返回码要仔细、全面地处理
2 :明确函数功能,精确(而不是近似)地实现函数设计
3 :编写可重入函数时,应注意局部变量的使用(如编写C/C++ 语言的可重入函数时,应
使用auto 即缺省态局部变量或寄存器变量)
说明:编写C/C++语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理
,才能使函数具有可重入性。
4 :编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P 、V 操作)等
手段对其加以保护
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调
用此函数时,很有可能使有关全局变量变为不可知状态。
示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有
可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另
外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Ex
am赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出
的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
Exam = para; // 给Exam赋值并计算其平方过程中(即正在使用此
temp = Square_Exam( ); // 信号),本进程必须等待其释放信号后,才可继
[释放信号量操作] // 续执行。若申请到信号,则可继续执行,但其
// 它进程必须等待本进程释放信号量后,才能再使
// 用本信号。
return temp;
}
5 :在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由
接口函数本身负责,缺省是由函数调用者负责
说明:对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要
么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处
理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况
虽不会造成问题,但产生了冗余代码,降低了效率。
6 :防止将函数的参数作为工作变量
说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改
变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:如下函数的实现就不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
for (count = 0; count < num; count++)
{
*sum += data[count]; // sum成了工作变量,不太好。
}
}
若改为如下,则更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;
for (count = 0; count < num; count ++)
{
sum_temp += data[count];
}
*sum = sum_temp;
}
7 :函数的规模尽量限制在200 行以内
说明:不包括注释和空格行。
8 :一个函数仅完成一件功能
9 :为简单功能编写函数
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确
化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
value = ( a > b ) ? a : b ;
改为如下就很清晰了。
int max (int a, int b)
{
return ((a > b) ? a : b);
}
value = max (a, b);
或改为如下。
#define MAX (a, b) (((a) > (b)) ? (a) : (b))
value = MAX (a, b);
10:不要设计多用途面面俱到的函数
说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。
11:函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出
说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内
部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语
言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而
,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为
AUTO类,则返回为错针。
示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
12 :尽量不要编写依赖于其他函数内部实现的函数
说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过
具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由
于其灵活性,很可能使函数出现这种情况。
示例:如下是在DOS下TASM的汇编程序例子。过程Print_Msg的实现依赖于Input_Msg的具体
实现,这种程序是非结构化的,难以维护、修改。
... // 程序代码
proc Print_Msg // 过程(函数)Print_Msg
... // 程序代码
jmp LABEL
... // 程序代码
endp
proc Input_Msg // 过程(函数)Input_Msg
... // 程序代码
LABEL:
... // 程序代码
endp
13 :避免设计多参数函数,不使用的参数从接口中去掉
说明:目的减少函数间接口的复杂度。
14 :非调度函数应减少或防止控制参数,尽量只使用数据参数
说明:本建议目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命
令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指
改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制
参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
示例:如下函数构造不太合理。
int add_sub( int a, int b, unsigned char add_sub_flg )
{
if (add_sub_flg == INTEGER_ADD)
{
return (a + b);
}
else
{
return (a b);
}
}
不如分为如下两个函数清晰。
int add( int a, int b )
{
return (a + b);
}
int sub( int a, int b )
{
return (a b);
}
15 :检查函数所有参数输入的有效性
16 :检查函数所有非参数输入的有效性,如数据文件、公共变量等
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,
即非参数输入。函数在使用输入之前,应进行必要的检查。
17 :函数名应准确描述函数的功能
18 :使用动宾词组为执行某操作的函数命名。如果是OOP 方法,可以只有动词(名词是对
象本身)
示例:参照如下方式命名函数。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;
19 :避免使用无意义或含义不清的动词为函数命名
说明:避免用含义不清的动词如process、handle等为函数命名,因为这些动词并没有说明
要具体做什么。
20 :函数的返回值要清楚、明了,让使用者不容易忽视错误情况
说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或
忽视错误返回码。
21 :除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或
强制的转换方式作为返回值返回
22 :让函数在调用点显得易懂、容易理解
23 :在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转
换
说明:因为数据类型转换或多或少存在危险。
24 :避免函数中不必要语句,防止程序中的垃圾代码
说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可
能给程序的测试、维护等造成不必要的麻烦。
25 :防止把没有关联的语句放到一个函数中
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到
同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,
同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需
要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来
,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理
的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
26:如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题
说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段
代码构造成一个新的函数。
27:功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级
函数中,而不必单独存在
说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别
是扇入很低的或功能不明确的函数,不值得单独存在。
28 :设计高扇入、合理扇出(小于7 )的函数
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数
调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,
表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会
对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出
太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数
进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的
功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间
的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入
到公共模块中。
29 :减少函数本身或函数间的递归调用
说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归
调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除
非为某些算法或功能的实现方便,应减少没必要的递归调用。
30 :仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,
据此来进行模块的函数划分与组织
说明:函数的划分与组织是模块的实现过程中很关键的步骤,如何划分出合理的函数结构
,关系到模块的最终效率和可维护性、可测性等。根据模块的功能图或/及数据流图映射出
函数结构是常用方法之一。
31 :改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性
、效率和可维护性
优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
32 :在多任务操作系统的环境下编程,要注意函数可重入性的构造
说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有
可重入性是非常重要的,因为这是多个进程可以共用此函数的必要条件。另外,编译器是
否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才
有可能提供可重入函数库。如DOS下BC和MSC等就不具备可重入函数库,因为DOS是单用户单
任务操作系统。
33 :避免使用BOOL 参数
说明:原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时
很难知道该参数到底传达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一
个无意义的单词。
34 : 对于提供了返回值的函数,在引用时最好使用其返回值
35 :当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个
意义相当的宏代替
说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,
则可以通过以下宏定义来代替:
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr
一个函数如果满足以及下条件之一,那么它是不可重入的:
函数中使用了静态变量,无论是全局静态变量还是局部静态变量。
函数返回静态变量。
函数中调用了不可重入函数。
函数是singleton中的成员函数而且使用了不使用线程独立存储的成员变量
总的来说,如果一个函数在重入条件下使用了未受保护的共享的资源,那么它是不可重入
的
什么是可重入性?
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,
不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或
者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,
稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时
保护自己的数据。
可重入函数:
不为连续的调用持有静态数据。
不返回指向静态数据的指针;所有数据都由函数的调用者提供。
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
绝不调用任何不可重入函数。
1.什么是可重入性
重入一般可以理解为一个函数在同时多次调用,例如操作系统在进程调度过程中,或者单片机、处理器等的中断的时候会发生重入的现象。
可重入的函数必须满足以下三个条件:
(1)可以在执行的过程中可以被打断;
(2)被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。
(3)再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。
可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。
通常,以下几种情况会受到可重入性的制约:
(1)信号处理程序A内外都调用了同一个不可重入函数B;B在执行期间被信号打断,进入A
(A中调用了B),完事之后返回B被中断点继续执行,这时B函数的环境可能改变,其结果就不可预料了。
众所周知,在进程中断期间,系统会保存和恢复进程的上下文,然而恢复的上下文仅限于返回地址,cpu寄存器等之类的少量上下文,而函数内部使用的诸如全局或静态变量,buffer等并不在保护之列,所以如果这些值在函数被中断期间发生了改变,那么当函数回到断点继续执行时,其结果就不可预料了。打个比方,比如malloc,将如一个进程此时正在执行malloc分配堆空间,此时程序捕捉到信号发生中断,执行信号处理程序中恰好也有一个malloc,这样就会对进程的环境造成破坏,因为malloc通常为它所分配的存储区维护一个链接表,插入执行信号处理函数时,进程可能正在对这张表进行操作,而信号处理函数的调用刚好覆盖了进程的操作,造成错误。
(2)多线程共享进程内部的资源,如果两个线程A,B调用同一个不可重入函数F,A线程进入F后,线程调度,切换到B,B也执行了F,那么当再次切换到线程A时,其调用F的结果也是不可预料的。
常见的不可重入函数有:
printf
--------引用全局变量stdout
malloc
--------全局内存分配表
free
--------全局内存分配表
2.可重入与线程安全
可重入的定义源于单线程环境。在单线程环境下,一段代码在执行中可能被硬件中断,并转而调用中断服务程序(ISR)。在本次调用中断处理函数之前,有可能中断处理函数已经在执行。因此,任何中断处理函数都应该是可重入的。
线程安全的概念则源自于多线程环境。可见,他们的起源是不一样的。那么,他们没有什么必然关系呢。可总结如下:
(1)一个线程安全的函数不一定是可重入的;
(2)一个可重入的函数缺也不一定是线程安全的!
3.不可重入的危害
在单线程进程中,只存在一个控制流。因此,这些进程所执行的代码无需重入或是线程安全的。在多线程程序中,相同的功能和资源可以通过多个控制流并发访问。
要保护资源的完整性,编写的多线程程序代码必须能重入并是线程安全的。不可重入对多线程环境的危害是很大的,甚至会造成系统崩溃。
4.不可重入的例子
4.1 不可重入且线程不安全
下面这个swap函数是不可重入的:
-
int t;
-
-
void swap(int *x, int *y)
-
{
-
t = *x;
-
*x = *y;
-
-
-
*y = t;
-
}
-
-
void isr()
-
{
-
int x = 1, y = 2;
-
swap(&x, &y);
-
}
int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!
*y = t;
}
void isr()
{
int x = 1, y = 2;
swap(&x, &y);
}
当然了,跟据前面的总结,既然它不可重入则它一定不是线程安全的。
可以把t改成线程局部变量,使得该函数变成线程安全。然而,这样修改的话,swap函数依然是不可重入的。例如一个线程已经在执行swap函数,这个时候在同样的语境下收到硬件中断,isr()函数会被调用,进而调用swap,swap的不可重入问题就暴露出来了。
4.2 可重入但是线程安全
我们做一定修改,在swap函数里,在交换前,对此时刻的t全局变量做一个本地的缓存,在交换结束的时候,始终使用该缓存。这样的话,swap函数在退出的时候,全局变量的之跟进入的时候是一样的。这样,就可以保证该函数是线程可重入的。代码如下:
-
int t;
-
-
void swap(int *x, int *y)
-
{
-
int s;
-
-
<span style="color:#FF0000;"> s = t;
-
t = *x;
-
*x = *y;
-
-
-
*y = t;
-
<span style="color:#FF0000;"> t = s;
-
}
-
-
void isr()
-
{
-
int x = 1, y = 2;
-
swap(&x, &y);
-
}
int t;
void swap(int *x, int *y)
{
int s;
<span style="color:#FF0000;"> s = t; // save global variable</span>
t = *x;
*x = *y;
// hardware interrupt might invoke isr() here!
*y = t;
<span style="color:#FF0000;"> t = s; // restore global variable</span>
}
void isr()
{
int x = 1, y = 2;
swap(&x, &y);
}
然而,该函数在多线程环境下,依然是线程不安全的,因为它无法保证全局变量t的一致性。
5.预防不可重入的几个原则
原则总结如下:
(1)不要使用static变量和全局变量,坚持只用局部变量;
(2)若必须访问全局变量,利用互斥信号量来保护全局变量;
(3)获取得知哪些系统调用是可重入的,在多任务处理程序中都使用安全的系统调用;
(4)不调用其它任何不可重入的函数;
(5)谨慎使用堆栈malloc/new。
6.优秀实践:如何优化已有代码,使函数成为可重入?
在多数情况下,必须用带有已修改的将要重入的函数来替代非重入函数。非重入函数不能由多个线程使用。此外,可能也无法使非重入函数变为线程安全。
6.1
返回指向静态数据的指针的函数是不可重入的,如何使其变得可重入?
许多非重入函数会返回一个指向静态数据的指针。可以用以下方法来避免这种情况:
-
返回动态分配的数据。在这种情况下,调用程序将负责释放存储量。好处在于无需对接口进行修改。但是,向后兼容性就无法保证了;现有的使用已修改函数的单线程程序在不更改的情况下不会释放存储量,这将导致内存泄漏。
-
使用调用程序提供的存储量。虽然必须修改接口,但是推荐使用这种方法。
例如,将字符串转换为大写的
strtoupper 函数可以用如以下所示的代码段来实现:
/* non-reentrant function */
char *strtoupper(char *string)
{
static char buffer[MAX_STRING_SIZE];
int index;
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}
该函数不是重入函数(也不是线程安全的函数)。要通过返回动态分配的数据来使该函数重入,那么该函数应类似于以下代码段:
/* reentrant function (a poor solution) */
char *strtoupper(char *string)
{
char *buffer;
int index;
/* error-checking should be performed! */
buffer = malloc(MAX_STRING_SIZE);
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}
较好的解决方案是修改接口。调用程序必须为输入和输出字符串提供存储量,如以下代码段所示:
/* reentrant function (a better solution) */
char *strtoupper_r(char *in_str, char *out_str)
{
int index;
for (index = 0; in_str[index]; index++)
out_str[index] = toupper(in_str[index]);
out_str[index] = 0
return out_str;
}
使用调用程序提供的存储量使非重入标准 C 库子例程重入。
6.2
在连续调用中保存数据的函数是不可重入的,如何修改?
在连续调用中将不保存任何数据,因为不同的线程可能连续地调用该函数。如果函数必须在连续调用中保存某些数据,比如工作缓存或指针,那么调用程序应提供该数据。
请考虑以下例子。函数返回了字符串中连续的小写字符。该字符串只在第一次调用时提供,就像
strtok 子例程。函数在到达字符串的结尾处时返回 0。该函数可通过以下代码段来实现:
/* non-reentrant function */
char lowercase_c(char *string)
{
static char *buffer;
static int index;
char c = 0;
/* stores the string on first call */
if (string != NULL) {
buffer = string;
index = 0;
}
/* searches a lowercase character */
for (; c = buffer[index]; index++) {
if (islower(c)) {
index++;
break;
}
}
return c;
}
该函数不是重入函数。要使其变为重入函数,那么调用程序必须保存静态数据和变量
index。该函数的重入版本可通过以下代码段来实现:
/* reentrant function */
char reentrant_lowercase_c(char *string, int *p_index)
{
char c = 0;
/* no initialization - the caller should have done it */
/* searches a lowercase character */
for (; c = string[*p_index]; (*p_index)++) {
if (islower(c)) {
(*p_index)++;
break;
}
}
return c;
}
该函数的接口和用法都发生了改变。调用程序必须向每次调用提供该字符串,且在首次调用前,必须将索引初始化为 0,如以下代码所示:
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
...
}
7.参考文献
1.%2Fcom.ibm.aix.genprogc%2Fdoc%2Fgenprogc%2Fwriting_reentrant_thread_safe_code.htm
2.%28computing%29
1)什么是可重入性?
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,
不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用
2)
不为连续的调用持有静态数据。
3)不可重入函数:
函数体内调用了其他标准I/O函数。
4)示例
static int tmp;
void func1(int* x, int* y)
{
tmp=*x;
*x=*y;
*y=tmp;
}
void func2(int* x, int* y)
{
int tmp;
tmp=*x;
*x=*y;
*y=tmp;
}
func1是不可重入的,func2是可重入的。因为在多线程条件下,操作系统会在func1还没有执行完的情况下,切换到另一个线程中,那个线程可能再次调用func1,这样状态就错了。
2 :明确函数功能,精确(而不是近似)地实现函数设计
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp
= Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para
)
{
unsigned int temp;
temp = Square_Exam( ); //
信号),本进程必须等待其释放信号后,才可继
return temp;
}
void sum_data( unsigned int
num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
for (count = 0; count < num;
count++)
{
*sum += data[count]; //
sum成了工作
}
}
若改为如下,则更好些。
void sum_data( unsigned int
num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;
for (count = 0; count < num;
count ++)
{
sum_temp += data[count];
}
*sum = sum_temp;
}
7 :函数的规模尽量限制在200 行以内
说明:不包括注释和空格行。
8 :一个函数仅完成一件功能
9 :为简单功能编写函数
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
value = ( a > b ) ? a : b
;
改为如下就很清晰了。
int max (int a, int b)
{
return ((a > b) ? a :
b);
}
value = max (a, b);
或改为如下。
#define MAX (a, b) (((a) >
(b)) ? (a) : (b))
value = MAX (a, b);
10:不要设计多用途面面俱到的函数
说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。
11:函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出
unsigned int integer_sum(
unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; //
注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <=
base; index++)
{
sum += index;
}
return sum;
}
12 :尽量不要编写依赖于其他函数内部实现的函数
proc Print_Msg //
过程(函数)Print_Msg
... // 程序代码
jmp LABEL
... // 程序代码
endp
proc Input_Msg //
过程(函数)Input_Msg
... // 程序代码
LABEL:
... // 程序代码
endp
13:避免设计多参数函数,不使用的参数从接口中去掉
说明:目的减少函数间接口的复杂度。
14 :非调度函数应减少或防止控制参数,尽量只使用数据参数
int add_sub( int a, int b,
unsigned char add_sub_flg )
{
if (add_sub_flg ==
INTEGER_ADD)
{
return (a + b);
}
else
{
return (a b);
}
}
不如分为如下两个函数清晰。
int add( int a, int b )
{
return (a + b);
}
int sub( int a, int b )
{
return (a b);
}
15 :检查函数所有参数输入的有效性
18 :使用动宾词组为执行某操作的函数命名。如果是OOP
方法,可以只有动词(名词是对象本身)
示例:参照如下方式命名函数。
void print_record( unsigned int
rec_ind ) ;
int input_record( void )
;
unsigned char
get_current_color( void ) ;
19 :避免使用无意义或含义不清的动词为函数命名
说明:避免用含义不清的动词如process、handle等为函数命名,因为这些动词并没有说明要具体做什么。
20 :函数的返回值要清楚、明了,让使用者不容易忽视错误情况
说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。
说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。
25 :防止把没有关联的语句放到一个函数中
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机
在
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽
*/
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标
*/
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽
*/
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标
*/
}
26:如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题
说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(5)降低函数间接口的复杂度。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
说明:原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一个无意义的单词。
34 : 对于提供了返回值的函数,在引用时最好使用其返回值
说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,