可重入函數 volatile關鍵字 以及SIGCHLD信號
- 一、可重入函數
- 1、引入
- 2、可重入函數的判斷
- 二、volatile關鍵字
- 1、引入
- 2、關于編譯器的優化的簡單討論
- 三、SIGCHLD信號
一、可重入函數
1、引入
我們來先看一個例子來幫助我們理解什么是可重入函數:
假設我們現在要對一個鏈表進行頭插,在執行到第10行代碼時,突然進程的時間片到了,進程被切換了,一會等進程再度切換回來時,當前進程要處理信號,而信號處理函數是sighandler
,而sighandler
里面也進行了頭插,等進程從內核態返回到用戶態時,繼續執行第11行的代碼,這時我們再觀察鏈表的結構會發現鏈表中出現了節點丟失的問題,而造成這種問題的根源是我們的insert
函數同時被兩個執行流給進入了。
node_t node1, node2, *head;
int main()
{...insert(&node1);...
}void insert(node_t*p)
{p->next = head;head = p;
}void sighandler(int signo)
{insert(&node2);
}
由這個問題衍生出了一種函數分類的方式:
- 如果一個函數同時被多個執行流進入所產生的結果沒有問題,該函數被稱為可重入函數
- 如果一個函數同時被多個執行流進入所產生的結果有問題,該函數被稱為不可重入函數
- 可重入函數主要用于多任務環境中,一個可重入的函數通常來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS調度下去執行另外一段代碼,而返回控制時不會出現什么錯誤;
- 不可重入的函數由于使用了一些系統資源,比如全局變量區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。
2、可重入函數的判斷
如果一個函數符合以下條件之一則是不可重入的:
- 函數體內使用了靜態(
static
)的數據結構或者變量; - 調用了
malloc
或free
,因為malloc
也是用全局鏈表來管理堆的。 - 調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。
二、volatile關鍵字
1、引入
volatile
是C語言的一個關鍵字,該關鍵字的作用是保證內存數據的可見性。
我們來先來看一段代碼,這里我們不加入volatile
關鍵字并開啟編譯器優化選項,優化級別是-O2
。
這段代碼的意思是:我們讓進程一直運行,直到我們給進程發送2
號信號以后,進程再退出。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>int flag = 0;void handler(int signo)
{printf("捕捉到了%d號信號\n", signo);// 將flag置為1flag = 1;printf("已經將flag置為%d\n", flag);
}int main()
{signal(2, handler);printf("進程正在運行...\n");while (!flag); // 當flag == 1時,進程退出。printf("運行結束!\n");return 0;
}
運行結果:
可以看到,我們明明都已經讓flag = 1
了但是進程中的循環依然沒有結束,這時為什么呢?下面我們一起來分析這個過程:
代碼中的main
函數和handler
函數在觸發時是兩個獨立的執行流,而while循環是在main
函數當中的,而且main
執行流里面并沒有使用過handler
函數(signal
函數只是對2
號信號進行了捕捉,沒有調用過handler
函數),所以在編譯器編譯時檢測到在main
函數中對flag變量沒有做過修改操作,而且由于while循環運行時需要頻繁使用flag變量,所以編譯器可以將flag變量的值用一個寄存器進行保存,以后每次使用flag變量直接去寄存器里面取數據,不必每次都要將內存中的flag搬運到寄存器里面然后讓CPU去計算。
可是不巧的是我們給當前進程發送了2
號信號,讓另外一個執行流更改了內存中的flag變量,而由于編譯器的優化,認為flag變量不會改變導致內存中的flag變量改變以后也沒有將寄存器中的數據同步修改,而CPU運算使用的數據又是寄存器中的數據,這就導致了內存數據的不可見,于是while循環就會一直運行,導致了上面的問題。
為了讓編譯器每次都要去內存取數據來進行計算,我們可以在flag變量前面加上volatile
關鍵字。
#include <stdio.h>
...
volatile int flag = 0;void handler(int signo)
{...
}
int main()
{...
}
再次運行程序,發現運行結果符合預期!
2、關于編譯器的優化的簡單討論
上面的代碼如果我們不開啟優化,就算不加上volatile
關鍵字也是能正常運行的,可見編譯器的優化不是越高越好。
如何理解編譯器的優化?
編譯器的本質是將代碼翻譯成01的二進制序列,所以編譯器的優化是在你編寫的代碼上動手腳,也就是說編譯器的優化其實改變了一些最終翻譯成01
二進制以后的執行邏輯。
三、SIGCHLD信號
在一前我們講過用wait
和waitpid
函數清理僵尸進程,父進程可以阻塞等待子進程結束,也可以非阻塞地查詢是否有子進程結束等待清理(也就是輪詢的方式)。采用第一種方式,父進程阻塞了就不能處理自己的工作了;采用第二種方式,父進程在處理自己的工作的同時還要記得時不時地輪詢一 下,也很麻煩。
《wait與waitpid的使用介紹》
上面使用wait
與waitpid
其實都是父進程主動檢查子進程是否處于僵尸狀態,那么有沒有一種方法能夠讓子進程主動告訴父進程自己處于僵尸狀態呢?
其實,子進程在終止時會給父進程發SIGCHLD
信號,該信號的默認處理動作是忽略,父進程可以自定義SIGCHLD
信號的處理函數,這樣父進程只需專心處理自己的工作,不必關心子進程了,子進程終止時會通知父進程,父進程在信號處理函數中調用wait
或waitpid
清理子進程即可。
下面就是一個對SIGCHLD
信號的一個使用:
在父進程中我們創建了10個子進程,這10個子進程退出時都會給父進程發送SIGCHLD
信號,由于父進程回收其中一個子進程時,其他子進程也有可能同時給父進程發送SIGCHLD
信號,而pending
表又沒有辦法同時存儲多個信號,所以我們就要進行循環回收子進程,而為了不影響父進程的執行流程我們可以選擇非阻塞等待。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>pid_t id = 0;void WaitProcess(int signo)
{printf("捕捉到了%d號信號,正在處理...\n", signo);while (1){pid_t ret = waitpid(-1, NULL, WNOHANG);if (ret > 0){printf("等待子進程%d成功,父進程%d\n", ret, id);}else{break;}}printf("WaitProcess, done\n");
}int main()
{signal(SIGCHLD, WaitProcess);int i = 0;// 創建10個子進程for (i = 0; i < 10; i++){id = fork();// 子進程if (id == 0){int cnt = 5;//睡眠cnt秒以后退出while (cnt--){printf("我是子進程,我的pid是:%d,ppid是:%d\n", getpid(), getppid());sleep(1);}exit(0);}}// 父進程一直休眠while (1){sleep(1);}return 0;
}
事實上,由于UNIX 的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調用signal
將SIGCHLD
的處理動作置為SIG_IGN
,這樣fork
出來的子進程在終止時會自動清理掉,不會產生僵尸進程,也不會通知父進程。系統默認的忽略動作和用戶用signal
函數自定義的忽略 通常是沒有區別的,但這是一個特例。此方法對于Linux
可用,但不保證在其它UNIX系統上都可用。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>pid_t id = 0;int main()
{// 對SIGCHLD設置為忽略,這樣產生的子進程退出時不會形成僵尸狀態。signal(SIGCHLD, SIG_IGN);int i = 0;// 創建10個子進程for (i = 0; i < 10; i++){id = fork();// 子進程if (id == 0){int cnt = 5;//睡眠cnt秒以后退出while (cnt--){printf("我是子進程,我的pid是:%d,ppid是:%d\n", getpid(), getppid());sleep(1);}exit(0);}}// 父進程一直休眠while (1){sleep(1);}return 0;
}