目錄
一、概念引入
1、生活中的信號
2、Linux中的信號
二、信號處理常見方式
三、信號的產生
1、鍵盤產生信號
2、系統調用接口產生信號
3、軟件條件產生信號
4、硬件異常產生信號
四、信號的保存
相關概念
信號保存——三個數據結構
信號集——sigset_t
信號集操作函數
五、信號的處理
內核態與用戶態
六、信號的捕捉
內核如何進行信號捕捉
sigaction
七、可重入函數
八、關鍵字volatile
九、SIGCHLD信號
一、概念引入
1、生活中的信號
在生活中,我們很容易能夠想到常見的一些信號。比如,紅綠燈,手機鬧鐘,上下課鈴聲,轉向燈等等。我們人不僅能夠識別它們,還能夠知道不同的信號對應的下一步動作應該怎么做。比如,紅燈停綠燈行;上課鈴響就上課,下課鈴響就下課;轉向燈告訴別人我要轉的方向。
那么,我們是怎么識別并知道這些信號,并且知道信號發出后,接下來的動作應該是怎么樣的呢?首先,這當然是規定過的,交通部門規定了紅燈停綠燈行,而如果交通部門規定紅燈行,綠燈停,那么我們也就只能照做。其次,我們從出生開始,大人們就不斷告訴我們,要紅燈停,綠燈行,久而久之,我們就記住了特定場景下的信號,以及后續我們需要做到動作,并且終身不忘。
而且,即使我們沒有在過馬路,而是在吃飯,我們也能夠知道應該如何處理紅綠燈信號。
再比如,如果,我的9點的鬧鐘響了,但是我沒有立即起床,而是30分鐘后再起床。這就說明,當信號產生的時候,我們不一定會立即執行后續動作,但是我記住了鬧鐘響過了,我該起床了,后面我再執行起床的動作。
上面就是一些生活中的信號,以及我們對待信號的方式。下面我們就來看看Linux中的信號。
2、Linux中的信號
什么是Linux信號?
Linux信號本質是一種通知機制,是用戶或者操作系統,通過發送一定的信號,來通知進程某件事已經發生,你可以后續對其進行處理。
Linux信號的特點
結合上面生活中的信號的特點,Linux信號有如下特點:
a. 進程能夠識別信號,即能夠看到信號發送給了自己,并且知道后續的處理動作。
b. 進程能夠識別信號,已經由Linux設計者提前設計好了,并且規定了各種信號的后續處理動作。
c. 信號的產生是隨機的,信號產生時,進程可能正在做自己的事,所以,進程不一定會立即對信號進行處理。
d. 因為進程不一定立即處理信號,所以進程一定能夠將信號記住,后續再進行處理。
e. 進程會在合適的時候處理信號(什么時候合適?后面會講)。
g. 一般而言,信號的產生相對于進程是異步的。
信號查看:我們可以通過 kill -l 命令查看Linux中有哪些信號
其中,1~31號信號,是普通信號,34~64是實時信號。我們在平時使用中使用的最多的是普通信號。
二、信號處理常見方式
為了方便后面的講解,我們首先了解一下信號處理的常見方式:
1、執行該信號的默認處理動作(進程自帶的,Linux設計者寫好的邏輯)。
2、用戶自己提供一個信號處理函數,要求在進行信號處理時,使用用戶自己定義的方式處理信號,這種方式稱為捕捉(Catch)一個信號。
3、忽略該信號。
我們可以通過?man 7 signal 查看信號的默認處理動作:
value:信號編號? action:默認處理動作。?
三、信號的產生
簡單理解信號的保存和發送
為了下面我們講解信號的產生,這里我們先簡單地理解一下信號的保存。
前面講到過,信號產生后,進程不一定會立即處理信號,而是在之后的某個合適的時間對信號進行處理。所以在這中間的一段時間里,我們必須對信號進行保存。
對于保存,進程只需要知道是否有這個信號,就可以對信號進行處理,所以我們可以使用位圖來對信號進行保存。0就代表該比特位對應的信號沒有產生,1就代表產生了該信號。這樣,在之后進程只需要遍歷一遍位圖,就可以知道產生了哪些信號,然后進行處理。
該位圖在進程的PCB中,屬于內核數據,只有操作系統能夠修改,所以信號的發送就是os把信號對應的比特位的數字由0改成1。
當然,關于信號的保存和發送我們會在下面的內容中,進行詳細的講解,這里只是有一個概念。
1、鍵盤產生信號
在之前講進程等待時,我們知道使用 Ctrl + c 的組合鍵能夠終止一個進程,而且我們也講了,其本質就是通過向進程發送2號信號,來終止進程的。下面我們就來證明一下:
我們使用自定義函數,將信號進行捕捉:signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error. In the event of an error, errno is set to indicate the cause.
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void catchsig(int signum)
{cout << "進程捕捉到了一個信號:" << signum << " "<< "pid"<< " " << getpid() << endl;
}int main()
{signal(SIGINT, catchsig);while (true){cout << "我是一個進程,我正在運行"<< " "<< "pid"<< " " << getpid() << endl;sleep(1);}return 0;
}
?
通過對比上面兩張圖,我們發現Ctrl + c 和發送2號命令,都調用了我們自定義的處理動作。所以?Ctrl + c的本質就是發送2號命令。
~ 核心轉儲
上面的一張圖,在信號的默認動作action中,term表示只終止進程,而還有的信號的動作是core,這個動作不僅會終止進程,還可以發生核心轉儲。這個與我們前面的進程等待的內容又有些關聯了。
上圖是進程等待中,父進程獲取子進程信息的status位圖結構。低7位保存信號,之前有一個core dump標志,該比特位表示是否發生了核心轉儲。
核心轉儲:當進程出現某種異常時,是否由os將當前進程在內存中的相關核心數據,轉存到磁盤中。
一般來說,云服務器上的核心轉儲功能是被關閉了的。而我們可以使用ulimit -a 命令查看core文件,ulimit -c 大小 命令打開云服務器的核心轉儲功能。
那么核心轉儲有什么作用呢?我們使用下面的代碼來看看:
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void catchsig(int signum)
{cout << "進程捕捉到了一個信號:" << signum << " "<< "pid"<< " " << getpid() << endl;
}int main()
{signal(SIGQUIT, catchsig);while (true){cout << "我是一個進程,我正在運行"<< " "<< "pid"<< " " << getpid() << endl;sleep(1);int a = 100;a /= 0;}return 0;
}
我們這次使用的是3號信號,它具有core的功能。
運行代碼后生成了core文件,且以進程pid為后綴。
我們知道程序出錯了,而有了core文件后,我們不用去一行一行找出錯位置,使用core文件在gdb下可以直接定位出錯位置,如下:
2、系統調用接口產生信號
1、kill函數
NAMEkill - send signal to a processSYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):kill(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
其實,我們常常使用的kill命令的底層所調用的就是該函數,下面我們可以模擬實現一下 kill命令的實現。?
#include <iostream>
#include <cassert>
#include <sys/types.h>
#include <signal.h>using namespace std;static void Usage(const string &proc)
{cout << "\nUsage:" << proc << " pid signo\n"<< endl;
}// ./mykill 2 pid
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}int signo = atoi(argv[1]);int sigpid = atoi(argv[2]);int n = kill(sigpid, signo);assert(n == 0);return 0;
}
2、raise
作用:進程讓os給自己發送某一個信號。
NAMEraise - send a signal to the callerSYNOPSIS#include <signal.h>int raise(int sig);DESCRIPTIONThe raise() function sends a signal to the calling process or thread. In a single-threaded program it is equivalent tokill(getpid(), sig);
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main()
{cout << "我開始運行了" << endl;sleep(2);raise(2);return 0;
}
3、abort?
作用:讓os給自己發一個6號信號。其實abort的底層也是去調用 raise(6)去實現的。
NAMEabort - cause abnormal process terminationSYNOPSIS#include <stdlib.h>void abort(void);
#include <iostream>
#include <cassert>
#include <unistd.h>
#include<stdlib.h>
#include <sys/types.h>
#include <signal.h>using namespace std;int main()
{cout << "我開始運行了" << endl;sleep(2);abort();return 0;
}
所以,總的來說,系統調用接口產生信號的具體過程就是: 用戶調用系統接口——os執行對應的代碼——os向目標進程寫入信號——修改信號對應的比特位——進程后續對信號進行處理。
3、軟件條件產生信號
~ 管道
在前面的進程間通信的管道中,我們討論了一個問題:對于正在通信的兩個進程,當管道的讀端不讀了,而且讀端關閉了,但是寫端一直在寫。這時,寫就沒有任何意義了。我們驗證了,在這個時候,os會通過發送13號信號的方式終止進程。因為管道是一個通過文件在內存級的實現,所以管道是一個軟件,所以這種情況就是軟件條件不滿足而產生信號的一種情況。
~ 設置鬧鐘 alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
調用alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之后給當前進程發SIGALRM信號,該信號的默認處理動作是終止當前進程。這個函數的返回值是0或者是以前設定的鬧鐘時間還余下的秒數。鬧鐘一旦觸發了,將會自動移除。
我們可以使用該函數寫一個能夠測試自己的電腦CPU的計算能力的代碼:
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>using namespace std;int count = 0;void sigcath(int sig)
{cout << "final count: "<< " " << count << endl;
}int main()
{alarm(1);signal(SIGALRM, sigcath);while (true)count++;return 0;
}
我們也可以寫一個代碼來讓os幫助我們每隔1秒就可以顯示cout最新的計算結果:?
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>using namespace std;uint64_t count = 0;void sigcath(int sig)
{cout << "final count: "<< " " << count << endl;alarm(1);
}int main()
{alarm(1);signal(SIGALRM, sigcath);while (true)count++;return 0;
}
4、硬件異常產生信號
~ 除0錯誤
我們來看一看下面的代碼:
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>using namespace std;void hander(int sig)
{cout << "我捕捉了一個信號:"<< " " << sig << endl;sleep(1);
}int main()
{signal(SIGFPE, hander);int a = 100;a /= 0;return 0;
}
運行結果如下:?
我們知道了如果代碼中出現了除0錯誤,os會給進程發送8號信號,那么是怎么產生并發送的呢?
首先,計算以及各種信息的處理是由CPU這個硬件進行的。CPU中有一個寄存器,叫做狀態寄存器,它含有一個位圖,該位圖上有溢出標記位。?CPU在進行計算時,發現代碼中出現了除0錯誤,因此將溢出標記位由0改為1,進程異常,CPU將該進程切出。os會自動進行計算完成后,檢測狀態寄存器,當檢查到溢出標記位為1時,os就會提取當前正在運行的進程的PID,給其發送8號信號。
那么為什么會是死循環打印呢?
上面講到,溢出標記位由0改為1后,CPU就會將該進程切出,因為寄存器里面的數據是該進程的上下文,所以位圖也會跟隨進程一起切出。但是,我們雖然將信號進行了捕捉,但是并沒有讓進程退出,所以這個進程只是被切出去了,當CPU正常進行調度時,再次調度該進程,上下文恢復上去,os立馬識別到了溢出標記位還是1,再次打印,如此反復。
所以,為了解決這個問題,我們要在捕捉函數最后加上 exit,讓進程退出。
~ 野指針和越界訪問
我們知道,指針變量必須通過地址才能找到目標位置。而我們語言上的地址是虛擬地址,所以我們前面講了通過頁表將物理地址和虛擬地址建立聯系。但是事實上,我們是通過頁表+MMU(memory manger unit,一個硬件)的方式將物理地址和虛擬地址建立聯系的,所以當代碼中出現了野指針或者越界訪問時,因為這是一個非法地址,那么MMU一定會報錯,它會將自己內部的寄存器進行標識,os就能夠檢測到,且知道是哪個進程的地址轉化出錯了。
四、信號的保存
相關概念
a. 信號遞達:進程對信號的處理動作稱為信號遞達。
b. 信號未決:信號從產生到遞達之間的這個狀態稱為信號未決。
c. 信號阻塞:被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。
阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
信號保存——三個數據結構
前面我們講到,在進程的PCB中,存在一種位圖是用來保存信號的,但是事實上有3種數據結構與信號是相關的。他們分別是pending位圖,block位圖,typedef void(*handler_t)(int signo),handler_t handler[32]={0}結構。
pending位圖:該位圖就是我們常說的用來保存信號的位圖。
block位圖:該位圖比特位的位置與信號標號一一對應,比特位的內容代表該信號是否阻塞。
typedef void(*handler_t)(int signo),handler_t handler[32]={0}:這個是一個函數指針數組,這個數組在內核中有指針指向它,這個數組稱為當前進程所匹配的信號遞達的所有方法,數組下標代表信號的編號,數組的每一個元素都是一個函數指針(存函數地址),指向信號對應的處理方法。
信號集——sigset_t
上面講到的三個結構都是屬于進程PCB,是內核數據結構。所以os必定不會讓用戶直接訪問這三個結構,更不能夠讓用戶直接進行位移操作。那么如果用戶想要得到pending和block位圖該怎么辦呢?于是,Linux就提供了一種數據類型信號集——sigset_t,用戶可以直接使用。
信號集操作函數
既然Linux提供了信號集,那么必定也通過了與之相關的各種方法,讓用戶能夠去操作,這樣用戶根本就不需要關系在內核中這些結構到底是怎么樣的。下面的5個函數就是對信號集進行操作的函數。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
sigpending:獲取當前進程的 pending 信號集。信號發送的本質就是對pending位圖進行修改。
NAMEsigpending - examine pending signalsSYNOPSIS#include <signal.h>int sigpending(sigset_t *set);讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1。
sigprocmask :讀取或更改進程的信號屏蔽字(阻塞信號集)?(block)
NAMEsigprocmask - examine and change blocked signalsSYNOPSIS#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
下表說明了how參數的可選值及其作用:
選項 | 作用 |
SIG_BLOCK | set包含了我們希望添加到當前信號屏蔽字的信號 |
SIG_UNBLOCK | set包含了我們希望從當前信號屏蔽字中解除阻塞的信號 |
SIG_SETMASK | 設置當前信號屏蔽字為set所指向的信號 |
注:9號信號是不能被捕捉或阻塞的。?
五、信號的處理
有了上面信號保存相關概念,進程對應信號的處理的一般步驟就是:先去遍歷pending位圖,找到比特位為1的位置對應的信號,然后再去檢測block位圖對應位置的比特位是否為1。若不為1,就hander表的對應位置去調用信號的處理動作函數,若為1,不做任何事。
好了,既然我們已經知道了信號處理的一般步驟了,那么進程是在什么時候進行那幾個步驟的呢?前面我們說了,是在合適的時候,那什么時候是合適的呢?其實,信號處理發生在由內核態返回用戶態的時候。
內核態與用戶態
CPU在執行我們自己寫的代碼的時候,我們就稱為用戶態,但是在自己的代碼中我們會使用系統調用接口(write,getpid...),這樣我們必然就會訪問os的內核數據或硬件資源,此時我們就稱為內核態。用戶不能以用戶態的身份執行系統調用,必須讓自己的身份變成內核態。
那么CPU怎么知道進程是處于用戶態還是內核態呢?
在CPU中,存在大量的寄存器,進程在執行的時候,會將自己的上下文數據加載到寄存器中。CPU中的寄存器分為可見寄存器和不可見寄存器。在不可見寄存器中有一個寄存器叫做CR3,它的作用是表示CPU運行級別,0表示內核態,3表示用戶態,這就能夠辨別是用戶態還是內核態。
那么如何理解代碼在操作系統上運行呢?
在進程地址空間中,0—3G的部分我們稱為用戶空間,是用戶自己寫的代碼,這些數據通過用戶級頁表映射到物理內存中。3—4G的部分我們稱為內核空間,是操作系統的相關數據,這些數據通過內核級頁表映射到物理內存中。開機時OS加載到內存中,OS在物理內存中只會存在一份,因為OS只有一份,所以OS的代碼和數據在內存中只有獨一份。
內核級頁表只有一份,不同的進程通過同一個內核級頁表就可以訪問同一個操作系統。
所以,進程進行系統調用的步驟就是:用戶空間中的代碼調用了系統調用——進程由用戶態轉成內核態——跳轉到內核空間該接口的位置——通過內核級頁表——訪問物理內存中的os代碼
內核態和用戶態之間是怎么切換的呢?
從用戶態切換為內核態通常有如下幾種情況:1、需要進行系統調用時。2、當前進程的時間片到了,導致進程切換(進程切換由os執行自己的調度算法完成)。3、產生異常、中斷、陷阱等。其中,由用戶態切換為內核態我們稱之為陷入內核。
所以,如果系統調用完成時,進程切換完畢或者異常、中斷、陷阱等處理完畢,進程將由內核態轉變成用戶態,此時就會對信號進行處理。
六、信號的捕捉
內核如何進行信號捕捉
當一個執行流正在執行我們的代碼時,可能會因為某些原因,陷入內核,去執行操作系統的代碼。當操作系統的代碼執行完畢準備返回到用戶態時,os會檢查pending表(此時仍處于內核態,有權力查看當前進程的pending位圖),如果某個信號處于未決狀態,那就再去檢測block位圖,看該信號是否被阻塞。如果阻塞,就直接返回,接著執行用戶的代碼。
如果未決信號沒有被阻塞,那么此時就需要對該信號進行處理。
如果待處理信號的處理動作是默認或者忽略,而這兩種處理動作已經由os寫好,可以直接在內核態下進行處理。執行該信號的處理動作后清除對應的pending標志位,如果沒有新的信號要遞達,就直接返回用戶態,接著上次被中斷的地方繼續向下執行。
但是,如果未決信號的處理動作是被自定義捕捉了的,那么我們就需要返回用戶態,去執行用戶自定義的處理動作的代碼,執行完后再通過特殊的系統調用sigreturn再次陷入內核并清除對應的pending標志位,如果沒有新的信號要遞達,就直接返回用戶態,接著上次被中斷的地方繼續向下執行。
為什么不能在內核態下直接執行自定義捕捉動作的代碼呢??
從理論上來說,是可以的,因為內核具有最高的執行權限。
但是,我們不能這樣做。因為如果在用戶自定義的捕捉函數里面有非法操作,比如清空數據,如果在內核態執行這樣的代碼,后果將不堪設想。所以,不能讓操作系統直接去執行用戶的代碼。
sigaction
信號捕捉除了前面用過的signal函數之外,我們還可以使用sigaction函數對信號進行捕捉。該函數可以讀取和修改與指定信號相關聯的處理動作,該函數調用成功返回0,出錯返回-1。
NAMEsigaction - examine and change a signal actionSYNOPSIS#include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
參數act和oldact都是結構體指針變量,該結構體的定義如下:
struct sigaction
{void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void(*sa_restorer)(void);
};
說明:
sa_handler:該結構體變量就是信號的處理方法。我們可以給其賦值:SIG_IGN 或者 SIG_DFL 或者 自定義函數。
sa_flags:直接將sa_flags設置為0即可。
我們寫一段代碼來使用一下sigaction:
#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>using namespace std;void hander(int signum)
{cout << "pid: " << getpid() << " "<< "獲取了一個信號: " << signum << endl;
}int main()
{struct sigaction sig;struct sigaction osig;sigemptyset(&sig.sa_mask);sigemptyset(&osig.sa_mask);sig.sa_flags = 0;sig.sa_handler = hander;sigaction(2, &sig, &osig);while (true)sleep(1);return 0;
}
sa_mask:當某個信號的處理函數被調用,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字。這樣就保證了在處理某個信號時,如果這種信號再次產生,那么它會被阻塞,直到處理結束,該信號會在下次合適的時候被處理。
如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時,自動恢復原來的信號屏蔽字。
#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>using namespace std;static void showpending(sigset_t &set)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&set, sig))cout << "1";elsecout << "0";}cout << endl;
}void hander(int signum)
{cout << "獲取了一個信號: " << signum << endl;cout << "獲取了一個信號: " << signum << endl;cout << "獲取了一個信號: " << signum << endl;cout << "獲取了一個信號: " << signum << endl;cout << "獲取了一個信號: " << signum << endl;sigset_t pending;int c = 20;while (true){sigpending(&pending);showpending(pending);c--;if (!c)break;sleep(1);}
}int main()
{cout << "pid: " << getpid() << " " << endl;struct sigaction sig;struct sigaction osig;sigemptyset(&sig.sa_mask);sigemptyset(&osig.sa_mask);sig.sa_flags = 0;sig.sa_handler = hander;sigaddset(&sig.sa_mask, 3);sigaddset(&sig.sa_mask, 4);sigaddset(&sig.sa_mask, 5);sigaction(2, &sig, &osig);while (true)sleep(1);return 0;
}
七、可重入函數
有下面這樣的一個鏈表。
對于該鏈表,我們有下面的頭插函數:
void insert(Node* p)
{p->next = head;head = p;
}
main函數中我們調用了它
int main()
{
...Node p1;insert(&p1)
...
}
?信號捕捉函數中也調用了它:
void hander(int signum)
{
...insert(&p2);
...
}
~ 首先,main函數中調用了insert函數,想將結點p1插入鏈表,但插入操作分為兩步,剛做完第一步的時候,因為硬件中斷使進程切換到內核,再次回到用戶態之前檢查到有信號待處理,于是切換到handler函數。
~ 在hander函數中,我們需要插入p2,將p2插入后,返回用戶態。此時鏈表結構如下:
~ 返回用戶態后,繼續執行插入p1的insert的第二步。此時鏈表結構如下:
最終結果是,main函數和handler函數先后向鏈表中插入了兩個結點,但最后只有p1結點真正插入到了鏈表中,而p2結點就再也找不到了,造成了內存泄漏。?
像上例這樣,insert函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱為重入。
insert函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為不可重入函數。反之,如果一個函數只訪問自己的局部變量或參數,則稱為可重入函數。
如果一個函數符合以下條件之一則是不可重入的:
1、調用了malloc或free,因為malloc也是用全局鏈表來管理堆的。
2、調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。
八、關鍵字volatile
volatile是C語言的一個關鍵字,該關鍵字的作用是保持內存的可見性。
我們來看一看下面的代碼:
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;int flag = 0;void hander(int signum)
{(void)signum;cout << "change flag: " << flag;flag = 1;cout << "->" << flag << endl;
}int main()
{signal(2, hander);while (!flag);cout << "進程退出后flag: " << flag << endl;return 0;
}
該程序的運行結果好像都在我們的意料之中,但是,如果我們使用的編譯器優化程度太高,就會出現一些問題。
代碼中的main函數和handler函數是兩個獨立的執行流,而while循環是在main函數當中的,在編譯器編譯時只能檢測到在main函數中對flag變量的使用,而且main函數中只是對變量flag進行了檢測,?并沒有對其值進行修改。所以在編譯器優化級別較高的時候,會直接將flag的值保存到CPU的寄存器中。
在編譯器優化程度高的情況下,當進程運行起來,flag初始值0,就會被保存到CPU的寄存器里面,每次while循環檢測的時候,CPU會直接到寄存器里面檢測flag的值(CPU無法看到內存了),但是這個值一直是0。雖然我們對flag的值進行了修改,但是也只是將內存里面flag的值修改成了1,CPU寄存器里的值任然為0。while循環永遠不會自動結束。
在編譯代碼時攜帶-O3選項使得編譯器的優化級別最高,此時再運行該代碼,就算向進程發生2號信號,該進程也不會終止。
為了解決這個問題,我們就可以使用volatile關鍵字對flag變量進行修飾,告知編譯器,對flag變量的任何操作都必須真實的在內存中進行,即保持了內存的可見性。?
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;volatile int flag = 0;void hander(int signum)
{(void)signum;cout << "change flag: " << flag;flag = 1;cout << "->" << flag << endl;
}int main()
{signal(2, hander);while (!flag);cout << "進程退出后flag: " << flag << endl;return 0;
}
進程正常退出。?
九、SIGCHLD信號
在進程等待的文章中,我們講到,為了避免出現僵尸進程,父進程需要使用wait或waitpid函數等待子進程結束,父進程可以阻塞等待子進程結束,但是父進程阻塞就不能處理自己的工作了;當然也可以非阻塞地查詢的是否有子進程結束等待清理,即輪詢的方式,這樣父進程在處理自己的工作的同時還要記得時不時詢問一下子進程是否退出以及子進程的情況,程序實現復雜且效率低。
其實,子進程在退出時會給父進程發生SIGCHLD信號,該信號的默認處理動作是忽略。
于是,由于Linux的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調用signal或者sigaction將SIGCHLD的處理動作置為SIG_IGN,這樣fork出來的子進程在退出時會自動清理掉,不會產生僵尸進程,也不會通知父進程。
下面的代碼中父進程就沒有等待子進程:
??#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;int main()
{signal(SIGCHLD, SIG_IGN);pid_t id = fork();if (id == 0){// 子進程cout << "我是子進程: pid: " << getpid() << endl;sleep(10);exit(0);}while (true){cout << "我是父進程: pid: " << getpid() << endl;sleep(1);}return 0;
}
????????
還有一種方法就是:父進程可以自定義SIGCHLD信號的處理動作,這樣父進程就只需專心處理自己的工作,不必關心子進程了,子進程退出時會通知父進程,父進程在自定義信號處理函數中調用wait或waitpid函數回收子進程即可。這樣,子進程退出后向父進程發送17號信號,父進程就會去調用自定義的處理動作,回收子進程。