Linux 可重入-异步信号安全和线程安全(架构师篇)

1、可重入函数

基本定义:安全

  • 重入:同一个函数被不一样的执行流调用,当前一个流程尚未执行完,就有其余的进程已经再次调用(执行流之间的相互嵌套执行);
  • 可重入:多个执行流反复执行一个代码,其结果不会发生改变,一般访问的都是各自的私有栈资源;
  • 不可重入:多个执行流反复执行一段代码时,其结果会发生改变;
  • 可重入函数:当一个执行流由于异常或者被内核切换而中断正在执行的函数而转为另一个执行流时,当后者的执行流对同一个函数的操做并不影响前一个执行流恢复后执行函数产生的结果;

当一个被捕获的信号被一个进程处理时,进程执行的普通的指令序列会被一个信号处理器暂时地中断。它首先执行该信号处理程序中的指令。若是从信号处理程序返回(例如没有调用exit或longjmp),则继续执行在捕获到信号时进程正在执行的正常指令序列(这和当一个硬件中断发生时所发生的事情类似)。可是在信号处理器里,咱们并不知道当信号被捕获时进程正在执行哪里的代码。
若是进程正使用malloc在它的堆上分配额外的内存,而此时因为捕捉到信号而插入执行该信号处理程序,其中又调用了malloc,这会发生什么呢?或者,若是进程正调用一个把结果存储在一个静态区域里的函数到一半,好比 getpwnam,而咱们在信号处理器里调用相同的函数,又会发生什么呢?在malloc的例子里,进程可能会遭到严重破坏,由于malloc一般维护它 全部分配过的区域的链表,而插入执行信号处理程序时,进程可能正在更改此连接表。数据结构

在getpwnam的例子里,返回给普通调用者的信息可能被返回给信号处理器的信息覆盖。
SUS规定了必须保证是能够再入的函数。
下表列出了这些再入函数:
在这里插入图片描述
一个可重入的函数简单来讲就是能够被中断的函数,也就是说,能够在这个函数执行的任什么时候刻中断它,转入OS 调度下去执行另一段代码,而返回控制时不会出现什么错误。可重入(reentrant)函数能够由多于一个任务并发使用,而没必要担忧数据错误。相反, 不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥 (或者使用信号量,或者在代码的关键部分禁用中断)。多线程

可重入函数能够在任意时刻被中断, 稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时 保护本身的数据。
信号安全,其实也就是异步信号安全,是说线程在信号处理函数当中,无论以任何方式调用你的这个函数若是不死锁不修改数据,那就是信号安全的。所以,我认为可重入与异步信号安全是一个概念 。并发

可重入函数知足条件:less

  • (1)不使用全局变量或静态变量;
  • (2)不使用用malloc或者new开辟出的空间;
  • (3)不调用不可重入函数;
  • (4)不返回静态或全局数据,全部数据都有函数的调用者提供;
  • (5)使用本地数据,或者经过制做全局数据的本地拷贝来保护全局数据;

不可重入函数符合如下条件之一异步

  • (1)调用了malloc/free函数,由于malloc函数是用全局链表来管理堆的。
  • (2)调用了标准I/O库函数,标准I/O库的不少实现都以不可重入的方式使用全局数据结构。
  • (3)可重入体内使用了静态的数据结构。

可重入函数分类async

(1)显式可重入函数:若是全部函数的参数都是传值传递的(没有指针),而且全部的数据引用都是本地的自动栈变量(也就是说没有引用静态或全局变量),那么函数就是显示可重入的,也就是说无论如何调用,咱们均可断言它是可重入的。函数

(2)隐式可重入函数:可重入函数中的一些参数是引用传递(使用了指针),也就是说,在调用线程当心地传递指向非共享数据的指针时,它才是可重入的。学习

可重入函数能够有多余一个任务并发使用,而没必要担忧数据错误,相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在 代码的关键部分禁用中断)。可重入函数能够在任意时刻被中断,稍后再继续运行,不会丢失数据,可重入函数要么使用本地变量,要么在使用全局变量时保护本身 的数据。ui

代码演示:

#include<stdio.h>
#include<signal.h>
 
int value=0;
 
void fun(){
        int i=0;
        while(i++<5){
                value++;
                printf("value is %dn",value);
                sleep(1);
        }
}
int main()
{
        signal(2,fun);
        fun();
        printf("the value is %dn",value);
        return 0;
}

在这里插入图片描述

2、线程安全

基本定义:

  • 线程安全:简单来讲线程安全就是多个线程并发同一段代码时,不会出现不一样的结果,咱们就能够说该线程是安全的;
  • 线程不安全:说完了线程安全,线程不安全的问题就很好解释,若是多线程并发执行时会产生不一样的结果,则该线程就是不安全的。
  • 线程安全产生的缘由:大可能是由于对全局变量和静态变量的操做。
  • 线程安全:一个函数被称为线程安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
    有一类重要的线程安全函数,叫作可重入函数,其特色在于它们具备一种属性:当它们被多个线程调用时,不会引用任何共享的数据。

尽管线程安全和可重入有时会( 不正确的 )被用作同义词,可是它们之间仍是有清晰的技术差异的。可重入函数是线程安全函数的一个真子集。

常见的线程不安全的函数:

  • (1)不保护共享变量的函数
  • (2)函数状态随着被调用,状态发生变化的函数
  • (3)返回指向静态变量指针的函数
  • (4)调用线程不安全函数的函数

常见的线程安全的状况

  • (1)每一个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,通常来讲这些线程是安全的;
  • (2)类或者接口对于线程来讲都是原子操做;
  • (3)多个线程之间的切换不会致使该接口的执行结果存在二义性;

代码演示:

#include<stdio.h>
#include<pthread.h>
 
int value=0;
 
void* func(void* arg){
        int i=0;
        while(i<10000){
                int tmp=value;
                value=i;
                printf("value is %dn",value);
                value=tmp+1;
                i++;
        }
}
int main()
{
        pthread_t id1,id2;
        pthread_create(&id1,NULL,func,NULL);
        pthread_create(&id2,NULL,func,NULL);
        pthread_join(id1,NULL);
        pthread_join(id2,NULL);
        printf("value is %dn",value);
        return 0;
}

3、可重入与线程安全的区别及联系

可重入函数:重入即表示重复进入,首先它意味着这个函数能够被中断,其次意味着它除了使用本身栈上的变量之外不依赖于任何环境(包括static ),这样的函数就是purecode (纯代码)可重入,能够容许有该函数的多个副本在运行,因为它们使用的是分离的栈,因此不会互相干扰。

可重入函数是线程安全函数,可是反过来,线程安全函数未必是可重入函数。
实际上,可重入函数不多,APUE 10.6 节中描述了Single UNIX Specification 说明的可重入的函数,只有115 个;APUE 12.5 节中描述了POSIX.1 中不能保证线程安全的函数,只有89 个。

信号就像硬件中断同样,会打断正在执行的指令序列。信号处理函数没法判断捕获到信号的时候,进程在何处运行。若是信号处理函数中的操做与打断的函数的操做相同,并且这个操做中有静态数据结构等,当信号处理函数返回的时候(固然这里讨论的是信号处理函数能够返回),恢复原先的执行序列,可能会致使信号处理函数中的操做覆盖了以前正常操做中的数据。

区别:

  • (1)可重入函数是线程安全函数的一种,其特色在于它们被多个线程调用时,不会引用任何共享数据。
  • (2)线程安全是在多个线程状况下引起的,而可重入函数能够在只有一个线程的状况下来讲。
  • (3)线程安全不必定是可重入的,而可重入函数则必定是线程安全的。
  • (4)若是一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  • (5)若是将对临界资源的访问加上锁,则这个函数是线程安全的,但若是这个重入函数若锁还未释放则会产生死锁,所以是不可重入的。
  • (6)线程安全函数可以使不一样的线程访问同一块地址空间,而可重入函数要求不一样的执行流对数据的操做互不影响使结果是相同的。

4、不可重入的几种状况

使用静态数据结构,好比getpwnam,getpwuid:若是信号发生时正在执行getpwnam,信号处理程序中执行getpwnam可能覆盖原来getpwnam获取的旧值:

调用malloc或free:若是信号发生时正在malloc(修改堆上存储空间的连接表),信号处理程序又调用malloc,会破坏内核的数据结构使用标准IO函数,由于好多标准IO的实现都使用全局数据结构,好比printf(文件偏移是全局的)。
函数中调用longjmp或siglongjmp:信号发生时程序正在修改一个数据结构,处理程序返回到另一处,致使数据被部分更新。
即便对于可重入函数,在信号处理函数中使用也须要注意一个问题就是errno 。一个线程中只有一个errno 变量,信号处理函数中使用的可重入函数也有可能 会修改errno 。例如,read 函数是可重入的,可是它也有可能会修改errno 。所以,正确的作法是在信号处理函数开始,先保存errno ;在信号处 理函数退出的时候,再恢复errno 。
例如,程序正在调用printf 输出,可是在调用printf 时,出现了信号,对应的信号处理函数也有printf 语句,就会致使两个printf 的输出混杂在一块儿。
若是是给printf 加锁的话,一样是上面的状况就会致使死锁。对于这种状况,采用的方法通常是在特定的区域屏蔽必定的信号。

屏蔽信号的方法:

signal(SIGPIPE, SIG_IGN); // 忽略一些信号
sigprocmask();// sigprocmask 只为单线程定义的
pthread_sigmask(); // pthread_sigmasks 能够在多线程中使用

如今看来信号异步安全和可重入的限制彷佛是同样的,因此这里把它们等同看待;
线程安全:若是一个函数在同一时刻能够被多个线程安全的调用,就称该函数是线程安全的。Malloc 函数是线程安全的。
不须要共享时,请为每一个线程提供一个专用的数据副本。若是共享很是重要,则提供显式同步,以确保程序以肯定的方式操做。经过将过程包含在语句中来锁定和解除锁定互斥,可使不安全过程变成线程安全过程,并且能够进行串行化。
不少函数并非线程安全的,由于他们返回的数据是存放在静态的内存缓冲区中的。经过修改接口,由调用者自行提供缓冲区就可使这些函数变为线程安全的。
操做系统实现支持线程安全函数的时候,会对POSIX.1 中的一些非线程安全的函数提供一些可替换的线程安全版本。
例如,gethostbyname() 是线程不安全的,在Linux 中提供了gethostbyname_r() 的线程安全实现。
函数名字后面加上 _r ,以代表这个版本是可重入的(对于线程可重入,也就是说是线程安全的,但并非说对于信号处理函数也是可重入的,或者是异步信号安全的)。

多线程程序中常见的疏忽性问题:

  • 将指针做为新线程的参数传递给调用方栈。
  • 在没有同步机制保护的状况下访问全局内存的共享可更改状态。
  • 两个线程尝试轮流获取对同一对全局资源的权限时致使死锁。其中一个线程控制第一种资源,另外一个线程控制第二种资源。其中一个线程放弃以前,任何一个线程都没法继续操做。
  • 尝试从新获取已持有的锁(递归死锁)。
  • 在同步保护中建立隐藏的间隔。若是受保护的代码段包含的函数释放了同步机制,而又在返回调用方以前从新获取了该同步机制,则将在保护中出现此间隔。结果具备误导性。对于调用方,表面上看全局数据已受到保护,而实际上未受到保护。
  • 将UNIX 信号与线程混合时,使用sigwait(2) 模型来处理异步信号。
  • 调用setjmp(3C) 和longjmp(3C) ,而后长时间跳跃,而不释放互斥锁。
  • 从对_cond_wait() 或 _cond_timedwait() 的调用中返回后没法从新评估条件。

在这里插入图片描述
学习资料视频免费分享看这里,免费学习

5、总结

  • 判断一个函数是否是可重入函数,在于判断其可否能够被打断,打断后恢复运行可以获得正确的结果。(打断执行的指令序列并不改变函数的数据)。
  • 判断一个函数是否是线程安全的,在于判断其可否在多个线程同时执行其指令序列的时候,保证每一个线程都可以获得正确的结果。
  • 若是一个函数对多个线程来讲是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来讲该函数也是可重入的。
  • 若是函数对异步信号处理程序的重入是安全的,那 么就能够说函数是” 异步-信号安全 ” 的。

可重入与线程安全是两个独立的概念, 都与函数处理资源的方式有关。

首先,可重入和线程安全是两个并不等同的概念,一个函数能够是可重入的,也能够是线程安全的,能够二者均知足,能够二者皆不知足( 该描述严格的说存在漏洞,参见第二条) 。
其次,从集合和逻辑的角度看,可重入是线程安全的子集,可重入是线程安全的充分非必要条件。可重入的函数必定是线程安全的,然过来则不成立。
第三,POSIX 中对可重入和线程安全这两个概念的定义:

Reentrant Function :A function whose effect, when called by two or
more threads,is guaranteed to be as if the threads each executed
thefunction one after another in an undefined order, even ifthe
actual execution is interleaved.

Thread-Safe Function :A function that may be safely invoked
concurrently by multiple threads.

Async-Signal-Safe Function :A function that may be invoked, without
restriction fromsignal-catching functions. No function is
async-signal -safe unless explicitly described as such

以上三者的关系为:可重入函数 必然 是 线程安全函数 和 异步信号安全函数;线程安全函数不必定是可重入函数。
可重入与线程安全的区别体如今可否在signal 处理函数中被调用的问题上, 可重入函数在signal 处理函数中能够被安全调用,所以同时也是 Async-Signal-Safe Function ;而线程安全函数不保证能够在signal 处理函数中被安全调用,若是经过设置信号阻塞集合等方法保证一个非可重入函数不被信号中断,那么它也是Async-Signal-Safe Function。

值得一提的是POSIX 1003.1 的 System Interface 缺省是 Thread-Safe 的,但不是Async-Signal-Safe 的。Async-Signal-Safe 的须要明确表示,好比fork () 和signal() 。

一个非可重入函数一般( 尽管不是全部状况下) 由它的外部接口和使用方法便可进行判断。例如:strtok() 是非可重入的,由于它在内部存储了被标记分割的字符串;ctime() 函数也是非可重入的,它返回一个指向静态数据的指针,而该静态数据在每次调用中都被覆盖重写。

一个线程安全的函数经过加锁的方式来实现多线程对共享数据的安全访问。线程安全这个概念,只与函数的内部实现有关,而不影响函数的外部接口。在 C 语言中,局部变量是在栈上分配的。所以,任何未使用静态数据或其余共享资源的函数都是线程安全的。
目前的 AIX 版本中,如下函数库是线程安全的:

  • C 标准函数库
  • 与BSD 兼容的函数库

使用全局变量( 的函数) 是非线程安全的。这样的信息应该以线程为单位进行存储,这样对数据的访问就能够串行化。一个线程可能会读取由另一个线程生成的错误代码。在AIX 中,每一个线程有独立的errno 变量。

最后让咱们来构想一个线程安全但不可重入的函数:
假设函数func() 在执行过程当中须要访问某个共享资源,所以为了实现线程安全,在使用该资源前加锁,在不须要资源解锁。

假设该函数在某次执行过程当中,在已经得到资源锁以后,有异步信号发生,程序的执行流转交给对应的信号处理函数;再假设在该信号处理函数中也须要调用函数 func() ,那么func() 在此次执行中仍会在访问共享资源前试图得到资源锁,然而咱们知道前一个func() 实例已然得到该锁,所以信号处理函数阻塞——另外一方面,信号处理函数结束前被信号中断的线程是没法恢复执行的,固然也没有释放资源的机会,这样就出现了线程和信号处理函数之间的死锁局面。

所以,func() 尽管经过加锁的方式能保证线程安全,可是因为函数体对共享资源的访问,所以是非可重入。