关于linux:Linux-可重入异步信号安全和线程安全架构师篇

39次阅读

共计 6888 个字符,预计需要花费 18 分钟才能阅读完成。

一、可重入函数

根本定义:

  • 重入:同一个函数被不同的执行流调用,以后一个流程还没有执行完,就有其余的过程曾经再次调用(执行流之间的互相嵌套执行);
  • 可重入:多个执行流重复执行一个代码,其后果不会产生扭转,通常拜访的都是各自的公有栈资源;
  • 不可重入:多个执行流重复执行一段代码时,其后果会产生扭转;
  • 可重入函数:当一个执行流因为异样或者被内核切换而中断正在执行的函数而转为另外一个执行流时,当后者的执行流对同一个函数的操作并不影响前一个执行流复原后执行函数产生的后果;

当一个被捕捉的信号被一个过程解决时,过程执行的一般的指令序列会被一个信号处理器临时地中断。它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用 exit 或 longjmp),则继续执行在捕捉到信号时过程正在执行的失常指令序列(这和当一个硬件中断产生时所产生的事件类似)。然而在信号处理器里,咱们并不知道当信号被捕捉时过程正在执行哪里的代码。
如果过程正应用 malloc 在它的堆上调配额定的内存,而此时因为捕捉到信号而插入执行该信号处理程序,其中又调用了 malloc,这会产生什么呢?或者,如果过程正调用一个把后果存储在一个动态区域里的函数到一半,比方 getpwnam,而咱们在信号处理器里调用雷同的函数,又会产生什么呢?在 malloc 的例子里,过程可能会受到严重破坏,因为 malloc 通常保护它 所有调配过的区域的链表,而插入执行信号处理程序时,过程可能正在更改此链接表。

在 getpwnam 的例子里,返回给一般调用者的信息可能被返回给信号处理器的信息笼罩。
SUS 规定了必须保障是能够再入的函数。
下表列出了这些再入函数:

一个可重入的函数简略来说就是能够被中断的函数,也就是说,能够在这个函数执行的任何时刻中断它,转入 OS 调度上来执行另外一段代码,而返回管制时不会呈现什么谬误。可重入(reentrant)函数能够由多于一个工作并发应用,而不用放心数据谬误。相同,不可重入(non-reentrant)函数不能由超过一个工作所共享,除非能确保函数的互斥(或者应用信号量,或者在代码的要害局部禁用中断)。

可重入函数能够在任意时刻被中断,稍后再持续运行,不会失落数据。可重入函数要么应用本地变量,要么在应用全局变量时 爱护本人的数据。
信号平安,其实也就是异步信号平安,是说线程在信号处理函数当中,不论以任何形式调用你的这个函数如果不死锁不批改数据,那就是信号平安的。因而,我认为可重入与异步信号平安是一个概念。

可重入函数满足条件:

  • (1)不应用全局变量或动态变量;
  • (2)不应用用 malloc 或者 new 开拓出的空间;
  • (3)不调用不可重入函数;
  • (4)不返回动态或全局数据,所有数据都有函数的调用者提供;
  • (5)应用本地数据,或者通过制作全局数据的本地拷贝来爱护全局数据;

不可重入函数合乎以下条件之一

  • (1)调用了 malloc/free 函数,因为 malloc 函数是用全局链表来治理堆的。
  • (2)调用了规范 I / O 库函数,规范 I / O 库的很多实现都以不可重入的形式应用全局数据结构。
  • (3)可重入体内应用了动态的数据结构。

可重入函数分类

(1)显式可重入函数:如果所有函数的参数都是传值传递的(没有指针),并且所有的数据援用都是本地的主动栈变量(也就是说没有援用动态或全局变量),那么函数就是显示可重入的,也就是说不论如何调用,咱们都可断言它是可重入的。

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

可重入函数能够有多余一个工作并发应用,而不用放心数据谬误,相同,不可重入函数不能由超过一个工作所共享,除非能确保函数的互斥(或者应用信号量,或者在 代码的要害局部禁用中断)。可重入函数能够在任意时刻被中断,稍后再持续运行,不会失落数据,可重入函数要么应用本地变量,要么在应用全局变量时爱护本人 的数据。

代码演示:

#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;
} 

二、线程平安

根本定义:

  • 线程平安:简略来说线程平安就是多个线程并发同一段代码时,不会呈现不同的后果,咱们就能够说该线程是平安的;
  • 线程不平安:说完了线程平安,线程不平安的问题就很好解释,如果多线程并发执行时会产生不同的后果,则该线程就是不平安的。
  • 线程平安产生的起因:大多是因为对全局变量和动态变量的操作。
  • 线程平安:一个函数被称为线程平安的,当且仅当被多个并发线程重复的调用时,它会始终产生正确的后果。
    有一类重要的线程平安函数,叫做可重入函数,其特点在于它们具备一种属性:当它们被多个线程调用时,不会援用任何共享的数据。

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

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

  • (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;
} 

三、可重入与线程平安的区别及分割

可重入函数:重入即示意反复进入,首先它意味着这个函数能够被中断,其次意味着它除了应用本人栈上的变量以外不依赖于任何环境(包含 static),这样的函数就是 purecode(纯代码)可重入,能够容许有该函数的多个正本在运行,因为它们应用的是拆散的栈,所以不会相互烦扰。

可重入函数是线程平安函数,然而反过来,线程平安函数未必是可重入函数。
实际上,可重入函数很少,APUE 10.6 节中形容了 Single UNIX Specification 阐明的可重入的函数,只有 115 个;APUE 12.5 节中形容了 POSIX.1 中不能保障线程平安的函数,只有 89 个。

信号就像硬件中断一样,会打断正在执行的指令序列。信号处理函数无奈判断捕捉到信号的时候,过程在何处运行。如果信号处理函数中的操作与打断的函数的操作雷同,而且这个操作中有静态数据构造等,当信号处理函数返回的时候(当然这里探讨的是信号处理函数能够返回),复原原先的执行序列,可能会导致信号处理函数中的操作笼罩了之前失常操作中的数据。

区别:

  • (1)可重入函数是线程平安函数的一种,其特点在于它们被多个线程调用时,不会援用任何共享数据。
  • (2)线程平安是在多个线程状况下引发的,而可重入函数能够在只有一个线程的状况下来说。
  • (3)线程平安不肯定是可重入的,而可重入函数则肯定是线程平安的。
  • (4)如果一个函数中有全局变量,那么这个函数既不是线程平安也不是可重入的。
  • (5)如果将对临界资源的拜访加上锁,则这个函数是线程平安的,但如果这个重入函数若锁还未开释则会产生死锁,因而是不可重入的。
  • (6)线程平安函数可能使不同的线程拜访同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使后果是雷同的。

四、不可重入的几种状况

应用静态数据构造,比方 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() 的调用中返回后无奈从新评估条件。


学习材料视频收费分享看这里,收费学习。

五、总结

  • 判断一个函数是不是可重入函数,在于判断其是否能够被打断,打断后复原运行可能失去正确的后果。(打断执行的指令序列并不扭转函数的数据)。
  • 判断一个函数是不是线程平安的,在于判断其是否在多个线程同时执行其指令序列的时候,保障每个线程都可能失去正确的后果。
  • 如果一个函数对多个线程来说是可重入的,则说这个函数是线程平安的,但这并不能阐明对信号处理程序来说该函数也是可重入的。
  • 如果函数对异步信号处理程序的重入是平安的,那 么就能够说函数是”异步 - 信号平安”的。

可重入与线程平安是两个独立的概念,都与函数解决资源的形式无关。

首先,可重入和线程平安是两个并不等同的概念,一个函数能够是可重入的,也能够是线程平安的,能够两者均满足,能够两者皆不满足 (该形容严格的说存在破绽,参见第二条)。
其次,从汇合和逻辑的角度看,可重入是线程平安的子集,可重入是线程平安的充沛非必要条件。可重入的函数肯定是线程平安的,然过去则不成立。
第三,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() 只管通过加锁的形式能保障线程平安,然而因为函数体对共享资源的拜访,因而是非可重入。

正文完
 0