博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

可重入 线程安全

Posted on 2016-03-23 17:29  bw_0927  阅读(303)  评论(0)    收藏  举报

https://www.cnblogs.com/liaokang/p/6554415.html

 

可不可重入,主要是跟信号紧密关联的

为了增强程式的稳定性,在信号处理函数中应使用可重入函数。 
信号处理程式中应当使用可再入(可重入)函数(注:所谓可重入函数是指一个能够被多个任务调用的过程,任务在调用时不必担心数据是否会出错)。
因为进程在 收到信号后,就将跳转到信号处理函数去接着执行。假如信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样 进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果
 
不可再入函数在信号处理函数中被视为不安全函数。
满足下列条件的函数多数是不可再入的:
(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()没有被列为可再入函数,因为不能确保紧接着两个函数的其他调用是安全的。

 

=======================

 

new是实现是基于malloc,而个人一直没有关注过malloc的可重入性。此处做少许笔记,以便后期查阅。

 以下内容转载自 https://sites.google.com/site/jishutaolun/programming/reaccessablefunction

主要用于多任务环境中:

  • 一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;
    1. 也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。
    2. 如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
  • 而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
    1. 说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

 

示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。

  1: unsigned int example( int para ) { 
  2:   unsigned int temp; 
  3:   Exam = para; // (**) 
  4:   temp = Square_Exam( ); 
  5:   return temp; 
  6: }

此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。

若申请不到“信号量”,说明另外的进程正处于给Exam赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。

若申请到信号,则可继续执行,但其它进程必须等待本进程释放信号量后,才能再使用本信号。

  1: unsigned int example( int para ) { 
  2:   unsigned int temp; 
  3:   // [申请信号量操作] //(1) 
  4:   Exam = para; 
  5:   temp = Square_Exam( ); 
  6:   // [释放信号量操作] 
  7:   return temp; 
  8: }

 

保证函数的可重入性的方法:

    在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量),对于要使用的全局变量要加以保护(如采取关中断、信号量等方法),这样构成的函数就一定是一个可重入的函数。


      VxWorks中采取的可重入的技术有:
        * 动态堆栈变量(各子函数有自己独立的堆栈空间)
        * 受保护的全局变量和静态变量
        * 任务变量
--------------------------------------------------
在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数不幸被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。那么什么是可重入函数呢?所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。不可重入函数在实时系统设计中被视为不安全函数。

满足下列条件的函数多数是不可重入的:

      1) 函数体内使用了静态的数据结构;
      2) 函数体内调用了malloc()或者free()函数;
      3) 函数体内调用了标准I/O函数。

    不可重入函数不可以在它还没有返回就再次被调用。例如printf,malloc,free等都是不可重入函数。因为中断可能在任何时候发生,例如在printf执行过程中,因此不能在中断处理函数里调用printf,否则printf将会被重入。
       函数不可重入大多数是因为在函数中引用了全局变量。例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。

局部的static变量也是分配在全局区域的,多个线程访问时,不加锁的话,其他线程的修改会导致不可重入

 

下面举例加以说明。

A. 可重入函数

  1: void strcpy(char *lpszDest, char *lpszSrc) { 
  2:   while(*lpszDest++=*lpszSrc++); 
  3:   *dest=0; 
  4: }


B. 不可重入函数1

  1: char cTemp;//全局变量
  2: void SwapChar1(char *lpcX, char *lpcY) { 
  3:   cTemp=*lpcX; 
  4:   *lpcX=*lpcY; 
  5:   lpcY=cTemp;//访问了全局变量 
  6: }


C. 不可重入函数2

  1: void SwapChar2(char *lpcX,char *lpcY) { 
  2:   static char cTemp;//静态局部变量 
  3:   cTemp=*lpcX; 
  4:   *lpcX=*lpcY; 
  5:   lpcY=cTemp;//使用了静态局部变量 
  6: }

 

 

问题1,如何编写可重入的函数?

    答:在函数体内不访问那些全局变量,不使用静态局部变量,坚持只使用局部变量,写出的函数就将是可重入的。如果必须访问全局变量,记住利用互斥信号量来保护全局变量。


问题2,如何将一个不可重入的函数改写成可重入的函数?

    答:把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写它。其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的。

  • 不要使用全局变量。因为别的代码很可能覆盖这些变量值。
  • 在和硬件发生交互的时候,切记执行类似disinterrupt()之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/退出核心”。
  • 不能调用其它任何不可重入的函数。
  1. 谨慎使用堆栈。最好先在使用前先OS_ENTER_KERNAL。堆栈操作涉及内存分配,稍不留神就会造成益出导致覆盖其他任务的数据,所以,请谨慎使用堆栈!最好别用!很多黑客程序就利用了这一点以便系统执行非法代码从而轻松获得系统控制权。还有一些规则,总之,时刻记住一句话:保证中断是安全的!

 

实例问题:曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的,为什么?

  1: unsigned int sum_int( unsigned int base ) { 
  2:   unsigned int index; 
  3:   static unsigned int sum = 0; // 注意,是static类型 
  4:   for (index = 1; index <= base; index++) 
  5:     sum += index; 
  6:   return sum; 
  7: }

分析:所谓的函数是可重入的(也可以说是可预测的),即只要输入数据相同就应产生相同的输出。这个函数之所以是不可预测的,就是因为函数中使用了static变量,因为static变量的特征,这样的函数被称为:带“内部存储器”功能的的函数。因此如果需要一个可重入的函数,一定要避免函数中使用static变量,这种函数中的static变量,使用原则是,能不用尽量不用。

将上面的函数修改为可重入的函数,只要将声明sum变量中的static关键字去掉,变量sum即变为一个auto类型的变量,函数即变为一个可重入的函数。

 

=======================================

 

http://www.zyfforlinux.cc/2015/01/09/%E5%8F%AF%E9%87%8D%E5%85%A5%E5%92%8C%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%85%A5%E5%87%BD%E6%95%B0/

 

什么是可重入函数?

关于可重入和不可重入这些概念网上可以找到很多,本人表达能力欠佳,所以在此处引用别人的话
对这两个概念做一个简单的介绍:

在多线程或有异常控制流的情况下,当某个函数运行到中途时,控制流(也就是当前指令序列)
就有可能被打断而去执行另一个函数. 如果在这种情况下不会出现问题,
比如说数据或状态不会被破坏,行为确定。那么这个函数就被称做"可重入"的.
函数是可重入(reentrant)的,是指对于相同的(并且合法的)函数参数(包括无参函数的情况),多
次调用此函数产生的行为是可预期的,即函数的行为一致,或者结果相同。不能保证这一点的函数称为
不可重入(non-reentrant)函数。

相信通过上面的简单介绍你或许对可重入和不可重入有了一丁丁了解,常见的不可重入的函数
一般都具备以下特征:

  • 调用malloc或free  【全局内存分配表;不可重入:malloc时被信号中断在中断处理函数中再malloc,中断处理函数返回后先前的地址可能不可用了,或者被占用了,或者分配了新地址;   线程安全:多个线程调用,安全,有全局锁。】
  • 使用了静态数据结构(全局变量或静态变量)
  • 标准I/O程序库的一部分【stdin, stdout是全局的】
  • 信号处理函数中使用不可重入函数

 

函数不可重入大多数是因为在函数中引用了全局变量。例如,printf会引用全局变量stdout,malloc,free会引用全局的内存分配表。

 

malloc函数是一个我们经常使用的函数,如果不对会造成一些潜在的问题。下面就malloc函数的线程安全性和可重入性做一些分析。

 我们知道一个函数要做到线程安全,需要解决多个线程调用函数时访问共享资源的冲突。

而一个函数要做到可重入,需要不在函数内部使用静态或全局数据,不返回静态或全局数据,也不调用不可重入函数。

 malloc函数线程安全但是不可重入的,因为malloc函数在用户空间要自己管理各进程共享的内存链表,由于有共享资源访问,本身会造成线程不安全。为了做到线程安全,需要加锁进行保护。

同时这个锁必须是递归锁,因为如果当程序调用malloc函数时收到信号,在信号处理函数里再调用malloc函数,如果使用一般的锁就会造成死锁(信号处理函数中断了原程序的执行),所以要使用递归锁

 

 虽然使用递归锁能够保证malloc函数的线程安全性,但是不能保证它的可重入性。按上面的场景,程序调用malloc函数时收到信号,在信号处理函数里再调用malloc函数就可能破坏共享的内存链表等资源,因而是不可重入的

 至于malloc函数访问内核的共享数据结构可以正常的加锁保护,因为一个进程程调用malloc函数进入内核时,必须等到返回用户空间前夕才能执行信号处理函数,这时内核数据结构已经访问完成,内核锁已释放,所以不会有问题。

 

 

下面举一个不可重入函数的两个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
struct data
{
int a;
int b;
}da;

void handler(int signum)
{
cout << "data:" << da.a << da.b << endl;
alarm(1);
}

int main()
{
static data zeros;
zeros.a = 0;
zeros.b = 0;
static data ones;
ones.a = 1;
ones.b = 1;
signal(SIGALRM,handler);
da = zeros;
alarm(1);
while(1)
{
da = zeros;
da = ones;
}
}

程序的预期的结果应该是00 11交替输出,然后在这里结果却不是这样,你会发现会出现01的情况,
在32位系统上这种情况很容易发生,64位机器上可能出现的概率较小。这是因为在结构体赋值的过程中
可能随时被信号打断,导致才赋值了部分数据,所以输出就会出现01 或 10这类情况

下面再看第二个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <netdb.h>
#include <signal.h>
#include <unistd.h>

using namespace std;

void handler(int signum)
{
hostent *hostptr;
hostptr = gethostbyname("www.51cto.com");
cout << hostptr->h_name << endl;
alarm(1);
}

int main()
{
hostent *hostptr;
signal(SIGALRM,handler);
alarm(1);
while(1)
{
hostptr = gethostbyname("www.baidu.com");
sleep(1);
}
}

同样在这个例子中gethostbyname本身就是一个不可重入函数,这个函数的实现机制是将得到的结果保存
在一个静态变量中,那么这就好导致一个问题,假设此时gethostname解析出结果存入静态变量中,
在函数没返回之前被中断,中断处理函数中再次调用gethotsbyname改变了全局变量的值,
信号返回到被中断处,中断处的gethostbyname返还的则是信号处理函数中gethostbyname设置的结果

可重入和线程安全

线程安全的(Thread-Safe):
如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。
往往可重入和线程安全被很多人混为一谈,其实二者是两个不同的概念,可以相互组合使用。
可重入函数一定是线程安全的;线程安全的函数不一定是可重入的
线程不安全的函数一定是不可重入的。

  • 可重入函数要解决的问题是,不在函数内部使用静态或全局数据,不返回静态或全局数据,
    也不调用不可重入函数。

  • 线程安全函数要解决的问题是,多个线程调用函数时访问资源冲突。

函数如果使用静态变量,通过加锁后可以转成线程安全函数,但仍然有可能不是可重入的,加锁后因为中断导致再次加锁造成死锁所以可能是不可重入的。

最佳实践

这里总结出实际工程项目开发中的一些工程实践经验:

  • 返还指定静态数据结构的指针可能会导致函数不可重入
    下面是不可冲入版本的一个字符串转大写的函数:
1
2
3
4
5
6
7
8
9
10
11
char * strtoupper(char *str)
{
static char buf[BUFSIZ];
int index;
for(index = 0; str[index];index++)
{
buf[index] = toupper(str[index]);
}
buf[index] = 0;
return buf;
}

修改为可重入版本:

1
2
3
4
5
6
7
8
9
10
char * strtoupper_r(char *str,char *dst)
{
int index;
for(index = 0; str[index];index++)
{
dst[index] = toupper(str[index]);
}
dst[index] = 0;
return dst
}

函数命名上加了后缀_r来表示是可重入的,函数内部没有使用全局变量和静态数据结构和使用不可重入函数

  • 在函数中记忆(保存)数据的状态导致不可重入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char GetLowerCaseChar(char *str)
{
static index = 0;
char c = 0;
while(c = str[index])
{
if(islower(c))
{
index++;
break;
}
index++
}
return c;
}

这是一个搜索字符串中小写字符的程序,函数保存了搜索到字符串的位置index,导致这个函数是不可重入的
这个index应该由调用者来维护,下面是这个函数的可重入版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char GetLowerCaseChar(char *str,int *index)
{
char c = 0;
while(c = str[*index])
{
if(islower(c))
{
(*index)++;
break;
}
(*index)++
}
return c;
}
  • 任何分配和释放内存的库函数都是不可重入的

  • 小心处理进程范围内的全局变量入(例如 errno 和h_errno)

1
2
3
4
if(open(filename,O_CREAT|O_RDWR) < 0 ){
perror("open file fail:");
exit(-1);
}

上面这个函数就是一个不可重入的函数,当在执行open完,因为某些原因导致open失败结果就是改变了errno
错误码,但此时被信号中断,去执行信号处理函数,信号处理函数中执行了一些系统调用或者库函数导致了
errno全局状态码发生了变化,那么等待信号处理函数执行完毕返还后那么open失败的错误码就被覆盖了。
为了避免上述问题发生应该在信号处理函数中保存errno状态码,执行完毕后进行恢复。

1
2
3
4
5
6
7
8
9
10
void handler(int signo)
{
int errno_saved;
errno_saved = errno;
/*
执行一些业务
*/
//状态码恢复
errno = errno_saved;
}