這一篇,大家可以輕松下了。本篇的壓力相對來說比較小。如果你編寫過多線程程式,相信你聽說過線程安全的函數,線程安全的函數是一種可重入函數。如果你從未接觸過這個概念,也沒什麼關系。
本文我們不講解線程安全函數,而是講異步信号安全函數。《可重入函數(二)》 中對比了線程安全函數與異步信号安全函數的異同點。
1. 何為可重入
不妨看下面的一個函數。
int a = 0; // 全局變量
int fun() {
++a;
return
試想一下,當你在執行
fun()
函數的
return a
的時候(假設這時候 a 的值已經為 1),你的代碼突然由于信号的打斷而跳轉到另一段代碼運作。然而十分不巧的是,那段代碼把
fun
函數執行了一遍(此時 a 的值已經變成了 2),當重新回到你的代碼時,你的
fun
函數的傳回值已經不再是你期望的 1,而是 2.
産生這種現象的本質在于,該函數引用了全局變量 a。
除此之外,使用靜态局部變量也會出現這種問題。是以,我們把所有引用了全局變量或靜态變量的函數,稱為不可重入函數,不可重入函數都不是信号安全的,也不是線程安全的。(有關線程,後面會慢慢涉及)。
反過來說,如果一個函數對于信号處理來說是可重入的,則稱其為異步信号安全函數。
注意:線程安全的函數,不一定是異步信号安全的。
有一點需要注意的是,如果一個函數使用了不可重入函數,那麼該函數也會變成不可重入的。這意味着,你不能在信号處理函數中使用不可重入函數。
有很多 C 庫函數和 linux 系統調用都是不可重入的,比如 malloc、getpwdnam。很多标準庫的 IO 函數都是不可重入的,因為這些函數使用了緩沖區。
2. 不可重入導緻的 bug
下面以執行個體說明在信号處理函數中使用不可重入函數帶來的危害。
該段程式使用 getpwdnam 根據使用者名擷取使用者 uid。在 main 函數中,getpwdnam 執行完後即進入 sleep 狀态。另外該段程式注冊了信号 SIGINT,待會我們在鍵盤鍵入
Ctrl + C
指令以及什麼都不做的情況下,看看螢幕列印的 uid 是多少。
- 代碼
// reenterable.c
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdio.h>
void handler(int sig) {
getpwnam("root");
}
int main() {
if (SIG_ERR == signal(SIGINT, handler)) {
perror("signal");
return 1;
}
printf("I'm %d\n", getpid());
struct passwd *pwd = getpwnam("allen");
sleep(10);
printf("allen's uid = %d\n", pwd->pw_uid);
return 0;
}
- 編譯
$ gcc reenterable.c -o reenterable
- 運作
$ ./reenterable
2.1 結果分析
- 當你運作程式後,什麼也不做,等待 10 秒後,結果顯示:
I'm 8055
allen's uid = 1000
- 當程式在列印 uid 前,如果你按下
,螢幕會列印:Ctrl + C
I'm 8049
^Callen's uid = 0
3. 總結
- 了解可重入、不可重入的含義
- 知道信号處理函數要求是可重入的