目錄
信號(signal)入門
技術應用角度的信號
注意
用kill -l命令可以察看系統定義的信號列表
信號處理常見方式概覽
產生信號
1.通過終端(鍵盤)按鍵產生信號
signal函數
2. 調用系統函數向進程發信號
kill 函數
raise 函數
3.由軟件條件產生的信號
alarm 函數
4.硬件異常產生信號
核心轉儲文件(core dump)
ulimit指令
總結:
阻塞(保存)信號
1.信號其他相關常見概念
2. 在內核中的表示
3.sigset_t類型
4.信號集操作函數
1. sigemptyset:
2. sigfillset:
3. sigaddset:
4. sigdelset:
5. sigismember:
sigprocmask
sigpending
捕捉信號(信號的處理)
1.內核如何實現信號的捕捉
2.sigaction
可重入函數
volatile關鍵字
gcc/g++在進行編譯時,是有一些優化級別的選項:
信號(signal)入門
在計算機系統中,信號是一種用于通知進程發生了某種事件的軟件中斷。它是一種進程間通信的方式,通常用于在異步事件發生時通知進程,例如用戶輸入、硬件錯誤、或者其他進程的狀態變化。
信號的特點包括:
1.異步性: 信號的產生和處理是異步的,即信號可以在任何時間點發生,而進程必須隨時準備好處理信號。
2.瞬時性: 信號是一種瞬時事件,通常是由硬件或其他進程生成,被發送到目標進程后立即執行相應的處理函數。
3.中斷性: 信號是一種中斷處理流程的機制,進程在接收到信號時會中斷當前的執行,執行與該信號相關聯的處理函數,然后繼續執行原來的流程。
信號的產生:
信號可以由多種事件觸發,其中包括:
4.硬件事件: 例如,除零錯誤、段錯誤等硬件異常可以觸發相應的信號。
5.軟件事件: 進程可以使用系統調用 kill 主動發送信號給其他進程,或者使用 raise 或 kill 自己產生信號。
6.用戶操作: 例如,按下 Ctrl+C 鍵盤組合會發送一個 SIGINT 信號給前臺進程。
7.其他進程的狀態變化: 當子進程終止時,父進程會收到 SIGCHLD 信號。
在Linux系統中,可以使用 kill -l命令顯示所有的信號或系統調用 kill 來向進程發送信號。每個信號都有一個唯一的編號,例如,SIGINT 的編號是2,SIGTERM 的編號是15。除了標準的信號,還有一些特殊的信號,如 SIGKILL 用于強制終止進程。
進程可以注冊信號處理函數,用于在接收到信號時執行特定的操作。這可以通過 signal 函數或 sigaction 函數來完成。處理函數可以是系統提供的默認處理函數,也可以是用戶自定義的函數。
總的來說,信號是一種重要的進程間通信機制,用于處理各種事件和異常情況,使得進程能夠響應外部環境的變化。
技術應用角度的信號
比如用戶輸入命令,在Shell下啟動一個前臺進程。用戶按下Ctrl-C ,這個鍵盤輸入產生一個硬件中斷,被OS獲取,解釋成信號,發送給目標前臺進程前臺進程因為收到信號,進而引起進程退出,如下:
[root@erciyuan Day11]# ll
總用量 20
-rw-r--r-- 1 root root 82 11月 28 01:15 Makefile
-rwxr-xr-x 1 root root 9184 11月 28 01:20 mysignal
-rw-r--r-- 1 root root 221 11月 28 01:20 mysignal.cc
[root@erciyuan Day11]#
[root@erciyuan Day11]# cat mysignal.cc
#include <iostream>
#include<unistd.h>using namespace std;int main()
{while(true){cout << "我是一個進程,正在運行...,pid: " << getpid() << endl;sleep(1);}return 0;
}
[root@erciyuan Day11]# ./mysignal
我是一個進程,正在運行...,pid: 19927
我是一個進程,正在運行...,pid: 19927
我是一個進程,正在運行...,pid: 19927
我是一個進程,正在運行...,pid: 19927
^C
[root@erciyuan Day11]#
硬件中斷:
硬件中斷是計算機體系結構中的一種機制,用于處理和響應外部設備發出的信號或事件。當外部設備需要與計算機進行通信或發出某種請求時,它會通過硬件中斷發送一個信號給計算機的中央處理器(CPU)。
硬件中斷可以是由各種外部設備觸發的,例如鍵盤、鼠標、網絡適配器、磁盤控制器等。當外部設備發生相關事件或需要處理時,它會發出一個硬件中斷信號,這個信號會被CPU的中斷控制器接收。
硬件中斷的處理過程如下:
- 外部設備發出中斷請求信號。
- CPU的中斷控制器接收到中斷請求信號,并將其轉發給中央處理器。
- 中央處理器暫停當前正在執行的任務,保存當前的執行狀態,并跳轉到預定義的中斷處理程序。
- 中斷處理程序會執行特定的操作來響應中斷請求,根據中斷源的不同進行相應的處理。處理完后,中斷處理程序會恢復之前保存的執行狀態,并返回到中斷發生的地方繼續執行。
硬件中斷的主要作用是允許外部設備與計算機進行異步通信,而不需要不斷地輪詢設備的狀態。它使得計算機能夠響應外部設備的事件,并及時進行處理,提高了系統的效率和響應性能。
在操作系統中,中斷處理程序通常由設備驅動程序編寫,用于處理特定設備發出的中斷請求。操作系統負責管理和分發中斷請求,將其分派給合適的中斷處理程序進行處理。
進程是如何記錄保存對應的信號:
進程該如何記錄對應產生的信號?記錄在哪里?先描述,在組織,怎么描述一個信號?用0和1來描述一個信號。用什么數據結構管理這個信號?通過位圖來管理產生的信號。
task _struct內部必定要存在一個位圖結構,用int表示:
uint32_t signals;
0000 0000 0000 0000 0000 0001 0000 0000 (比特位的位置,信號的編號,比特位的內容,是否收到該信號)
所謂的發送信號,本質其實寫入信號,直接修改特定進程的信號位圖中的特定的比特位,0->1
task_struct數據內核結構,只能由OS進行修改--無論后面我們有多少種信號產生的方式,最終都必須讓OS來完成最后的發送過程!
信號產生之后,不是立即處理的。是在合適的時候進行處理。
注意
1. Ctrl-C 產生的信號只能發給前臺進程。一個命令后面加個&可以放到后臺運行,這樣Shell不必等待進程結束就可以接受新的命令,啟動新的進程。
2. Shell可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像 Ctrl-C 這種控制鍵產生的信號
3. 前臺進程在運行過程中用戶隨時可能按下 Ctrl-C 而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能收到 SIGINT 信號而終止,所以信號相對于進程的控制流程來說是異步(Asynchronous)的。
前臺進程(Foreground Process)是指在終端(或控制臺)中正在直接運行的進程,前臺進程在運行時我們無法輸入指令。前臺進程通常是用戶當前正在交互的進程,接收用戶的輸入并將輸出顯示在終端上。與之相對的是后臺進程(Background Process),后臺進程在終端不接受用戶輸入,但仍然可以在系統中運行。
在Linux或類Unix系統中,可以使用一些命令和操作符來控制前臺和后臺進程:
- 啟動前臺進程:
-
- 在終端中運行一個程序,該程序將成為前臺進程。例如:
bash./my_program
- 啟動后臺進程:
-
- 在命令末尾加上 & 符號可以將一個進程放到后臺運行,使終端立即返回可輸入狀態,例如:
bash./my_program &
- 查看前臺和后臺進程:
-
- 使用 jobs 命令可以列出當前終端中運行的所有作業(包括前臺和后臺),以及它們的狀態。
- 將后臺進程切換到前臺:
-
- 使用 fg 命令可以將一個后臺進程切換到前臺運行。例如,fg %1 將編號為1的后臺進程切換到前臺。
- 將前臺進程放到后臺:
-
- 使用 Ctrl+Z 可以將當前正在前臺運行的進程暫停,并將其放到后臺。然后,可以使用 bg 命令將其繼續在后臺運行。
- 終止進程:
-
- 使用 Ctrl+C 可以發送 SIGINT 信號,終止當前前臺進程。使用 kill 命令可以發送其他信號,例如 kill -9 <PID> 可以強制終止一個進程。
前臺進程的交互性使得它們適合用戶直接操作,而后臺進程則可以在不阻塞終端的情況下在后臺執行任務。控制前臺和后臺進程的方法可以提供更靈活的進程管理。
用kill -l命令可以察看系統定義的信號列表
普通信號和實時信號是兩種不同類型的信號,它們在處理機制和特性上有一些區別。下面是它們的主要區別:
- 實時信號的引入:
-
- 普通信號: 普通信號是早期UNIX系統中引入的,其處理機制并沒有特別強調對實時性的支持。
- 實時信號: 實時信號是為了滿足對實時性和精確性要求更高的應用而引入的。它們在POSIX標準中定義,并且相對于普通信號,提供了更可靠的信號傳遞機制。
- 排隊特性:
-
- 普通信號: 普通信號在接收端排隊的能力有限,同一種類型的信號在排隊時可能會被合并成一個。
- 實時信號: 實時信號具有排隊特性,即同一種類型的信號可以被排隊,不會丟失。
- 信號編號范圍:
-
- 普通信號: 普通信號的編號范圍通常比較有限,取值在1到31之間,且不包括0。
- 實時信號: 實時信號的編號范圍相對較大,可以是任意正整數,不受限于1到31的范圍。
- 實時信號的優先級:
-
- 普通信號: 普通信號沒有定義優先級的概念,它們在信號隊列中按照到達的順序被處理。
- 實時信號: 實時信號可以具有優先級,低編號的實時信號比高編號的實時信號具有更高的優先級。
- 實時信號的可靠性:
-
- 普通信號: 普通信號在傳遞和處理過程中可能會出現一些不可靠的情況,例如丟失信號。
- 實時信號: 實時信號提供了更可靠的信號傳遞機制,確保信號在傳遞和處理時的可靠性。
在使用信號時,選擇使用普通信號還是實時信號通常取決于應用程序的實際需求。如果應用程序對信號的實時性和可靠性有較高的要求,那么使用實時信號可能更為適合。否則,普通信號可能足夠滿足一般的信號通知需求。
信號處理常見方式概覽
可選的處理動作有以下三種:
1. 忽略此信號。
2. 執行該信號的默認處理動作。
3. 提供一個信號處理函數(signal、sigaction函數),要求內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱為捕捉(Catch)一個信號
產生信號
1.通過終端(鍵盤)按鍵產生信號
signal函數
在Linux中,signal 函數用于注冊信號處理函數,以便在程序接收到指定信號時執行相應的操作(簡單來說signal的作用就是捕捉發送的信號,并執行相應的自定義函數)。signal 函數的原型如下:
#include <signal.h>typedef void (*sighandler_t) (int)//函數指針
sighandler_t signal(int signum, sighandler_t handler);
typedef void (*sighandler_t)(int); 這行代碼定義了一個類型別名 sighandler_t,它是一個函數指針類型,指向一個函數,該函數接受一個整數參數(代表信號編號),返回 void。
然后,signal 函數的原型是 sighandler_t signal(int signum, sighandler_t handler);,這表示 signal 函數接受兩個參數:
- signum:表示要處理的信號的編號。可以是預定義的信號常量(如 SIGINT 表示中斷信號)或自定義的信號編號。
- handler:是一個函數指針,指向用戶定義的信號處理函數。當程序接收到指定信號時,系統將調用這個函數執行相應的操作。如果 handler 的值是 SIG_IGN,表示忽略該信號;如果是 SIG_DFL,表示使用系統默認的處理方式。
函數返回之前與指定信號相關聯的信號處理函數的值。如果發生錯誤,返回 SIG_ERR。
所以,typedef void (*sighandler_t)(int); 定義了一個函數指針類型,用于表示信號處理函數的類型,而 signal 函數則用于注冊信號處理函數。
下面是一個簡單的例子,演示了如何使用 signal 函數注冊一個信號處理函數:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{cout << "get a singal: " << signo << endl;
}int main()
{// 注冊 SIGINT 信號的處理函數為 sigint_handlerif (signal(SIGINT, sigint_handler) == SIG_ERR) {perror("Unable to register SIGINT handler");return 1;}// 進入一個無限循環while(true){cout << "我是一個進程,正在運行...,pid: " << getpid() << endl;sleep(1);}return 0;
}
在這個例子中,程序在 main 函數中使用 signal 函數注冊了 SIGINT 信號的處理函數為 sigint_handler。當用戶按下Ctrl+C時,程序將收到 SIGINT 信號,然后調用 sigint_handler 函數來處理這個信號。
需要注意的是,signal 函數在一些平臺上被認為是不可靠的,因為它對信號處理的具體實現可能有所不同。在現代的程序中,更推薦使用 sigaction 函數,因為它提供了更多的控制選項和可移植性。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;//自定義方法
//signal作用:特定信號被發送給當前進程的時候,執行handler方法的時候,要自動填充對應的信號給handler方法
//我們甚至可以給所以信號設置同一個處理函數
void handler(int signo)
{cout << "get a singal: " << signo << endl;exit(2);
}int main()
{signal(2, handler);//ctrl + 'c'signal(3, handler);//ctrl + '\'//signal(9, handler);// 9號 信號不可被捕捉,因為9號只會執行默認動作。while(true){cout << "我是一個進程,正在運行...,pid: " << getpid() << endl;sleep(1);}return 0;
}
捕捉鍵盤發送的2號和3號 信號
這里只介紹1 - 31號 信號(普通信號),這些信號各自在什么條件下產生,默認的處理動作是什么,在signal(7)手冊中都有詳細說明: man 7 signal。
2. 調用系統函數向進程發信號
kill 函數
在Linux中,kill函數用于向指定的進程發送信號。具體語法如下:
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
該函數的參數解釋如下:
- pid:要發送信號的進程ID。可以使用進程ID(pid)或進程組ID(-pid)發送信號。特殊值0表示發送給當前進程所屬的進程組,特殊值-1表示發送給所有具有權限的進程。其他特殊值如-2、-3和-4有特殊的含義,用于特定情況下的信號發送。
- sig:要發送的信號編號,可以使用<signal.h>中定義的宏常量,例如SIGINT表示中斷信號(也可以改為使用這些宏常量的編號)。
該函數的返回值為成功發送信號的數量,如果出錯則返回-1,并設置errno變量來指示錯誤類型。
以下是kill函數的一些常見用法:
- 給指定進程發送信號:
kill(pid, SIGTERM); // 發送SIGTERM信號給pid進程
- 發送終止信號給進程組:
kill(-pid, SIGKILL); // 向進程組ID為pid的進程組發送SIGKILL信號
- 發送信號給當前進程所屬的進程組:
kill(0, SIGINT); // 向當前進程所屬的進程組發送SIGINT信號
需要注意的是,只有具有足夠權限的進程才能向其他進程發送信號。進程接收到信號后,可以通過注冊信號處理函數來處理信號。
使用kill函數封裝實現一個kill指令:
//mykill.cc
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <cassert>
#include <string>
#include <signal.h>
#include <sys/types.h>using namespace std;int count = 0;void Usage(std::string proc)
{//指令用法提示cout << "\tUsage: \n\t";cout << proc << " 信號編號 目標進程\n"<< endl;
}int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}int signo = atoi(argv[1]);int target_id = atoi(argv[2]);int n = kill(target_id, signo);if(n != 0){cerr << errno << " : " << strerror(errno) << endl;}return 0;
}
raise 函數
在Linux中,raise 函數通常用于向當前進程發送信號(意思是誰調用我,我就給誰發送信號)。這個函數的聲明如下:
#include <signal.h>int raise(int sig);
這個函數的目的是向當前進程發送信號 sig。如果成功,返回0;否則,返回非零值。
使用 raise 函數,你可以在程序中發送信號,觸發信號處理函數或默認的信號處理行為。例如,如果你想向當前進程發送信號,你可以這樣做:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>void myhandler(int signo)
{cout << "get a signal: " << signo << endl;
}int main(int argc, char *argv[])
{signal(SIGINT, myhandler);while(true){sleep(1);raise(2);//自動發送信號}return 0;
}
需要注意的是,使用信號處理函數時要謹慎,因為它們在異步環境中執行,可能會導致一些不可預測的行為。
在C和C++中也有一個類似raise系統函數的函數,abort 函數用于終止程序的運行,并生成一個程序終止信號。其聲明如下:
#include <stdlib.h>void abort(void);
調用 abort 函數會導致程序異常終止,同時產生一個 SIGABRT 信號(6號信號)。默認情況下,如果程序收到 SIGABRT 信號,會產生一個核心轉儲文件(core dump),該文件包含程序在崩潰時的內存映像,有助于調試。但是,你可以通過設置環境變量 COREDUMP_DISABLE 來禁用核心轉儲文件的生成。
注:abort發送的信號可以被捕捉,就算是被捕捉了當前進程也會退出。
3.由軟件條件產生的信號
SIGPIPE是一種由軟件條件產生的信號,在“管道”中已經介紹過了。在操作系統中,信號是用于在進程之間或由操作系統向進程發送通知的一種機制。信號可以由不同的條件產生,包括硬件條件和軟件條件。
軟件條件產生的信號是由軟件或操作系統內部的事件或條件引發的。這些信號用于與進程通信,傳遞某些特定的事件或請求。
alarm 函數
在Linux系統中,alarm 函數用于設置一個定時器,以在指定的時間間隔后發送 SIGALRM 信號給正在運行的進程。這個函數的聲明如下:
#include <unistd.h>unsigned int alarm(unsigned int seconds);
alarm 函數接受一個正整數參數 seconds,也就是指定了定時器的時間間隔(單位為秒)。函數返回的是上一次設置的定時器剩余的時間,如果之前沒有設置定時器,則返回0。
使用 alarm 函數可以在程序中創建一個簡單的定時器。當指定的時間間隔過去后,進程將收到 SIGALRM 信號。可以通過注冊 SIGALRM 的信號處理函數來處理該信號,并執行相應的操作。
注:把alarm的參數設置為0就是取消鬧鐘。
下面是一個簡單的示例,演示了如何使用 alarm 函數創建一個定時器:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void alarm_handler(int signum) {printf("Alarm received\n");// 執行需要在定時器觸發時執行的操作
}int main()
{signal(SIGALRM, alarm_handler); // 注冊 SIGALRM 的信號處理函數unsigned int seconds = 5;printf("Setting alarm for %u seconds\n", seconds);alarm(seconds); // 設置定時器// 其他的程序邏輯// ...while (1) {// 進程的其他工作// ...}return 0;
}
在上面的例子中,alarm_handler 函數是注冊給 SIGALRM 信號的處理函數。當定時器觸發時,進程將收到 SIGALRM 信號,并調用該處理函數,在該函數中執行需要在定時器觸發時執行的操作。
請注意,alarm 函數只能設置一個全局定時器,并且在調用 alarm 函數時,之前設置的定時器將被新的定時器替換。如果你需要多個定時器,可以考慮使用 timer_create 和 timer_settime 函數,它們提供了更靈活和精確的定時器功能。
實驗例子1:
實驗例子2:
4.硬件異常產生信號
硬件異常被硬件以某種方式被硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋 (8號)為SIGFPE信號發送給進程。再比如當前進程訪問了非法內存地址,,MMU會產生異常,內核將這個異常解釋為(11號)SIGSEGV信號發送給進程。
以下是一些常見的硬件異常及其相關的軟件信號:
- SIGSEGV(段錯誤):由于試圖訪問未分配的內存或對只讀內存執行寫操作等引起的內存訪問錯誤。硬件檢測到這種錯誤時,會發送SIGSEGV信號。
- SIGILL(非法指令):當進程執行了不合法或未定義的指令時產生。這可能是由于程序錯誤、二進制文件損壞等原因引起的。
- SIGFPE(浮點異常):由于進行了不合法的浮點運算(如除以零)而產生的信號。
- SIGBUS(總線錯誤):由于對計算機硬件總線上的地址執行了不合法的內存訪問而產生。
這些信號是在進程運行時由硬件檢測到的,表明了發生了某些嚴重的錯誤。當進程收到這些信號時,通常會執行相應的信號處理函數,以進行清理操作、記錄錯誤信息或終止進程。
要注意的是,硬件異常通常表示程序中存在錯誤,因此在正常情況下應該避免它們的發生。合理的錯誤處理和調試手段是確保程序健壯性的關鍵。
MMU:
MMU 是內存管理單元(Memory Management Unit)的縮寫。它是計算機系統中的一個硬件組件,負責執行虛擬內存到物理內存的地址映射,以及訪問內存時的權限控制。
主要功能包括:
- 地址映射: 將程序中使用的虛擬地址映射到物理內存中的實際地址。這樣,程序可以使用虛擬地址,而不需要知道實際物理地址。
- 內存保護: 控制對內存的訪問權限,包括讀、寫、執行等。通過在頁表中設置相應的權限位,MMU 可以確保程序只能訪問它被授權的內存區域。
- 地址轉換: 將虛擬地址轉換為物理地址。當程序訪問某個虛擬地址時,MMU 負責將其轉換為實際的物理地址。
- 緩存控制: 管理虛擬內存與物理內存之間的數據緩存,以提高訪問速度。
- TLB(Translation Lookaside Buffer): 一種用于加速地址轉換的高速緩存,存儲了最近使用的虛擬地址到物理地址的映射。這有助于避免每次地址訪問都要完全查詢頁表的開銷。
MMU 的引入使得操作系統能夠實現虛擬內存的概念,從而提供了一種抽象層,使得程序可以使用比實際物理內存更大的虛擬地址空間。這對于多任務處理、內存保護和地址空間隔離等方面都有很大的好處。
總的來說,MMU 在計算機體系結構中發揮著至關重要的作用,為操作系統提供了有效管理內存的手段,同時提高了系統的可靠性和安全性。
核心轉儲文件(core dump)
ulimit指令
ulimit 是一個用于設置或顯示用戶級資源限制的命令。這個命令通常在命令行終端中使用,它允許用戶限制特定的資源,以防止單個用戶或進程占用過多的系統資源。
以下是一些常見的用法和選項:
- ulimit -a 或 ulimit -all:顯示所有的資源限制。這將列出當前 shell 的所有資源限制,包括軟限制和硬限制。
bashulimit -a
- ulimit -c [限制]:設置或顯示核心轉儲文件的大小限制。如果沒有給定限制,它將顯示當前限制。
bashulimit -c unlimited
- ulimit -n [限制]:設置或顯示文件描述符的數量限制。
bashulimit -n 1024
- ulimit -u [限制]:設置或顯示用戶進程數限制。
bashulimit -u 500
- ulimit -q [限制]:設置或顯示隊列的大小限制。
bashulimit -q 1000
- ulimit -f [限制]:設置或顯示文件的大小限制。
bashulimit -f unlimited
- ulimit -l [限制]:設置或顯示鎖定內存的大小限制。
bashulimit -l 64
- ulimit -s [限制]:設置或顯示堆棧的大小限制。
bashulimit -s 8192
- ulimit -v [限制]:設置或顯示虛擬內存的大小限制。
bashulimit -v 1048576
這些是 ulimit 命令的一些常見用法。請注意,ulimit 命令設置的資源限制通常只對當前的 shell 會話有效,并且這些限制可能會被子進程繼承。如果你希望更改全局系統范圍內的資源限制,通常需要在系統啟動時或者使用特定配置文件中進行設置。
總結:
1.上面所說的所有信號產生,最終都要有OS來進行執行,為什么?
因為OS是進程的管理者,只有OS有權利修改進程PCB當中的數據。
2.信號的處理是否是立即處理的?
在合適的時候進行處理。
3.信號如果不是被立即處理,那么信號是否需要暫時被進程記錄下來?記錄在哪里最合適呢?
是的,記錄在PCB當中。
4.一個進程在沒有收到信號的時候,能否能知道,自己應該對合法信號作何處理呢?
知道,因為他已經被默認設置進了編碼進程的處理邏輯當中。
5.如何理解OS向進程發送信號?能否描述一下完整的發送處理過程?
不管是用戶通過鍵盤,系統調用、還是軟件條件,或者硬件異常,無論什么方式操作系統都一定能識別到,識別到之后,向目標進程寫信號,這就是發信號的過程。
阻塞(保存)信號
1.信號其他相關常見概念
實際執行信號的處理動作稱為信號遞達(Delivery)。
信號從產生到遞達之間的狀態,稱為信號未決(Pending)。
進程可以選擇阻塞 (Block )某個信號。
被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。
注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
2. 在內核中的表示
信號在內核中的表示示意圖:
1、每個信號都有兩個標志位分別表示阻塞(block:也稱為,信號屏蔽集)和未決(pending:也稱為pending信號集),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。在上圖的例子中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
2、SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。
3、SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可以依次放在一個隊列里。本章不討論實時信號
3.sigset_t類型
從上圖來看,每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的。因此,未決和阻塞標志可以用相同的數據類型sigset_t(sigset_t類型是一個位圖結構)來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。我們將詳細介紹信號集的各種操作。 阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應該理解為阻塞而不是忽略。(直白點就是sigset_t類型里面包含了兩張表,分別是block表和pending表)
4.信號集操作函數
sigset_t類型對于每種信號用一個bit表示“有效”或“無效”狀態,至于這個類型內部如何存儲這些bit則依賴于系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作sigset_ t類型變量,而不應該對它的內部數據做任何解釋,比如用printf直接打印sigset_t變量是沒有意義的。
以下是與 sigset_t 相關的一些常見函數:
1. sigemptyset:
-
- 函數原型:int sigemptyset(sigset_t *set);
- 功能:清空信號集合,將所有信號從集合中移除。
- 示例:
sigset_t my_set;
sigemptyset(&my_set);
2. sigfillset:
-
- 函數原型:int sigfillset(sigset_t *set);
- 功能:將所有信號添加到信號集合中,即將信號集合設置為包含所有信號。
- 示例:
sigset_t my_set;
sigfillset(&my_set);
3. sigaddset:
-
- 函數原型:int sigaddset(sigset_t *set, int signum);
- 功能:將指定的信號添加到信號集合中。
- 示例:
sigset_t my_set;
sigemptyset(&my_set);
sigaddset(&my_set, SIGINT);
4. sigdelset:
-
- 函數原型:int sigdelset(sigset_t *set, int signum);
- 功能:從信號集合中刪除指定的信號。
- 示例:
sigset_t my_set;
sigfillset(&my_set);
sigdelset(&my_set, SIGTERM);
5. sigismember:
-
- 函數原型:int sigismember(const sigset_t *set, int signum);
- 功能:檢查指定的信號是否包含在信號集合中。
- 示例:
sigset_t my_set;
sigfillset(&my_set);
if (sigismember(&my_set, SIGUSR1)) {// SIGUSR1 在信號集合中
}
這些函數通常用于在信號處理中設置和管理信號集合。例如,在使用 sigprocmask 函數時,你可能會使用 sigset_t 來指定哪些信號需要被阻塞。這些函數提供了對信號集合進行操作的便利方式。
前四個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則返回1,不包含則返回0,出錯返回-1。
sigprocmask
在Linux中,sigprocmask 函數用于檢查或修改進程的信號屏蔽集(signal mask)。信號屏蔽集(信號屏蔽集指的是block表)是一個集合,用于指定哪些信號在調用時應該被阻塞,即不被傳遞給進程。
以下是 sigprocmask 函數的基本信息:
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//返回值:若成功則為0,若出錯則為-1
- how:表示要執行的操作,可以是以下值之一:
-
- SIG_BLOCK(添加):將指定的信號集合添加到當前信號屏蔽集中,阻塞這些信號。
- SIG_UNBLOCK(刪除):從當前信號屏蔽集中移除指定的信號集合,解除對這些信號的阻塞。
- SIG_SETMASK(覆蓋):更改當前進程信號屏蔽集,將當前參數信號屏蔽集(set)設置為指定的信號集合(how)。
- set:對應于 how 操作的信號集合。
- oldset:如果不為 NULL,則在函數調用結束時,存儲之前的信號屏蔽集。
如果oset是非空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是非空指針,則 更改進程的信號屏蔽字,參數how指示如何更改。如果oset和set都是非空指針,則先將原來進程的信號 屏蔽字備份到oset里,然后根據set和how參數更改信號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值
下面是一些示例用法:
using namespace std;void showBlock(sigset_t *oset)
{int signo = 1;//從1開始,因為沒有0號信號for(; signo <= 31; signo++)//遍歷所有的比特位{if(sigismember(oset, signo))cout << "1";elsecout << "0";}cout << endl;
}int main()
{//1.只是在用戶層面上進行設置。設置的什么?設置的信號!//說直白點sigaddset(&set, 2);只是把信號設置到了set變量里//通過第2步,調用sigprocmask(SIG_SETMASK, &set, &oset);才是設置到進程里sigset_t set, oset;sigemptyset(&set);//清空(初始化)sigemptyset(&oset);sigaddset(&set, 2);//SIGIN//2.設置進入進程,誰調用,設置給誰int cnt = 0;sigprocmask(SIG_SETMASK, &set, &oset);while(true){showBlock(&oset);//輸出舊的信號集的所有信號sleep(1);cnt++;if(cnt == 10){cout << "recover block" << endl;sigprocmask(SIG_SETMASK, &oset, &set);//恢復舊的信號集showBlock(&set);//輸出舊的信號集的所有信號showBlock(&oset);//輸出舊的信號集的所有信號sleep(10);}}return 0;
}
這個例子演示了如何使用 sigprocmask 函數來設置和修改信號屏蔽集。
sigpending
sigpending 函數用于獲取當前進程被阻塞但是已經產生的待處理信號集。這個函數允許程序查詢在信號阻塞狀態下已經產生但尚未處理的信號。以下是 sigpending 函數的基本信息:
#include <signal.h>int sigpending(sigset_t *set);
- set:用于存儲待處理信號的信號集。函數成功調用后,set 將被設置為包含了當前被阻塞的、但已經產生的信號。
函數返回值:
- 如果成功,返回0。
- 如果失敗,返回-1,并設置 errno 表示錯誤的原因。
下面是一個簡單的示例,演示如何使用 sigpending 函數:
#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>using namespace std;void handler(int signo)
{cout << "對特定信號:" << signo << "執行捕捉" << endl;
}void PrintPemding(const sigset_t &pending)
{cout << "當前進程的pending位圖:";for(int signo = 1; signo <= 31; signo++){if(sigismember(&pending, signo))cout << "1";elsecout << "0";}cout << endl;
}int main()
{//2.0 設置對2號信號的自定義捕捉,以防止解除對2號信號的屏蔽之后退出進程signal(2, handler);sigset_t set, oset;//1.1 初始化sigemptyset(&set);sigemptyset(&oset);//1.2 將2號信號添加到set中sigaddset(&set, 2);//1.3 將新的信號屏蔽字設置到進程sigprocmask(SIG_BLOCK, &set, &oset);int cnt = 0;while(true){//2.1 先獲取pending信號集sigset_t pending;//用來存儲被阻塞的信號sigemptyset(&pending);int n = sigpending(&pending);//獲取被阻塞的信號assert(n == 0);(void)n;//2.2 打印,方便進行查看PrintPemding(pending);//2.3 休眠時間sleep(1);//2.4 10s之后,恢復所以信號的屏蔽(block)動作if(cnt++ == 10){cout << "解除對2號信號的屏蔽" << endl;sigprocmask(SIG_SETMASK, &oset, nullptr);}}return 0;
}
捕捉信號(信號的處理)
我們之前說過,信號的處理(信號的遞達),可以不是立即執行,而是"合適"的時候,那么這個"合適"指的又是什么時候?
信號可以立即被處理嗎?如果一個信號之前被block了,當他解除block的時候,對應的信號會被立即遞達!因為信號的產生是異步的,當前進程可能正在做更重要的事情!
什么時候是"合適"的時候?當進程從內核態 切換回 用戶態的時候,進程會在OS的指導下,進行信號的檢測與處理!
用戶態:執行你寫的代碼的時候,進程所處的狀態。
內核態:執行OS的代碼的時候,進程所處的狀態
所以什么時候從用戶態進入內核態呢?
1.進程時間片到了,需要切換,就要執行進程切換邏輯。2.系統調用
1.內核如何實現信號的捕捉
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下: 用戶程序注冊(設置捕捉)了SIGQUIT信號的處理函數sighandler。 當前正在執行main函數,這時發生中斷或異常切換到內核態。 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。 sighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
2.sigaction
在Linux中,sigaction 函數用于設置對信號的處理方式,調用成功則返回0,出錯則返回- 1 。以下是 sigaction 函數的一般形式:
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//sigaction函數和signal函數類似,都是用來捕捉當前進程發送的信號
- signum:表示要處理(捕捉)的信號的編號,比如 SIGINT 代表中斷信號。
- act:是一個指向 struct sigaction 結構體類型的指針,該結構包含了對信號的新的處理方式(處理函數、標志等)。
- oldact:是一個指向 struct sigaction 結構的指針,如果不為 NULL,則 oldact 將用于存儲之前對信號的處理方式。
struct sigaction 這個結構類型定義在 signal.h 頭文件中。以下是 struct sigaction 結構的簡化原型:
cstruct sigaction {void (*sa_handler)(int); // 處理函數的地址,或者是 SIG_IGN、SIG_DFLvoid (*sa_sigaction)(int, siginfo_t *, void *); // 用于三參數信號的處理函數的地址sigset_t sa_mask; // 指定在信號處理期間需要被屏蔽的信號集int sa_flags; // 指定信號處理的一些標志void (*sa_restorer)(void); // 恢復函數的地址
};
- sa_handler:用于設置信號處理函數的地址,或者可以指定為 SIG_IGN(忽略信號)或 SIG_DFL(使用默認處理方式)。
- sa_sigaction:用于設置三參數信號的處理函數的地址。如果 sa_handler 被使用,這個字段將被忽略。
- sa_mask:指定在信號處理期間需要被屏蔽的信號集。
- sa_flags:指定信號處理的一些標志,例如 SA_RESTART 表示在系統調用中自動重啟被信號中斷的系統調用。
- sa_restorer:用于設置恢復函數的地址。在一些舊的系統中可能使用,一般置為 NULL。
在使用 sigaction 函數時,你可以通過設置 act 參數為指向一個 struct sigaction 結構的指針,從而定義對特定信號的處理方式。
注:
當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字(說白了就是操作系統正在執行某一個信號的處理函數時,哪怕是這個信號曾經沒有被設置為block狀態(信號屏蔽字),即block表對應的比特位由0置為1,操作系統也會自動將這個信號設置為block狀態。簡單來說就是,后來的信號要排隊,直到當前信號的處理函數被執行完,才會輪到下一個信號),當信號處理完函數返回時也會自動恢復原來的信號屏蔽字(即block表對應的比特位由1置為0),這樣就保證了在處理某個信號時,如果這種信號再次產生,那么它會被阻塞到當前處理結束為止。 如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號(比如說再屏蔽3號和4號信號),則用sa_mask字段說明這些需要額外屏蔽的信號(也就是說把3號和4號添加到sa_mask里),當信號處理函數返回時自動恢復原來的信號屏蔽字。 sa_flags字段包含一些選項,本章的代碼都把sa_flags設為0,sa_sigaction是實時信號的處理函數,本章不詳細解釋這兩個字段,有興趣的可以自己在了解一下。
下面是一個示例,演示如何使用 sigaction 函數:
#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>using namespace std;void PrintPemding(const sigset_t &pending)
{cout << "當前進程的pending位圖:";for(int signo = 1; signo <= 31; signo++){if(sigismember(&pending, signo))cout << "1";elsecout << "0";}cout << endl;
}void handler(int signo)
{cout << "對特定信號:" << signo << "執行捕捉" << endl;int cnt = 10;while(cnt){cnt--;sigset_t pending;sigemptyset(&pending);sigpending(&pending);//獲取被阻塞的信號PrintPemding(pending);//輸出pending表的位圖sleep(1);}
}int main()
{struct sigaction act, oldact;memset(&act, 0, sizeof(act));memset(&act, 0, sizeof(oldact));act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaction(2, &act, &oldact);while(true){cout << getpid() << endl;sleep(1);}return 0;
}
可重入函數
可重入函數(reentrant function)是指一個函數在被多個任務(線程)同時調用時,能夠正確地處理多個調用而不會出現沖突或錯誤。這通常需要確保函數內部使用的數據是線程安全的,不依賴于全局狀態,而是依賴于函數的參數和局部變量。
main函數調用insert函數向一個鏈表head中插入節點node1,插入操作分為兩步,剛做完第一步的 時候,因為硬件中斷(時間片到了)使進程切換到內核,再次回用戶態之前檢查到有信號待處理,于是切換 到sighandler函
數,sighandler也調用insert函數向同一個鏈表head中插入節點node2,插入操作的 兩步都做完之后從
sighandler返回內核態,再次回到用戶態就從main函數調用的insert函數中繼續 往下執行,先前做第一步
之后被打斷,現在繼續做完第二步。結果是,main函數和sighandler先后 向鏈表中插入兩個節點,而最后只有一個節點真正插入鏈表中了。
像上例這樣,insert函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱
為重入,insert函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為 不可重入函數,反之,
如果一個函數只訪問自己的局部變量或參數,則稱為可重入(Reentrant) 函數。想一下,為什么兩個不同的
控制流程調用同一個函數,訪問它的同一個局部變量或參數就不會造成錯亂?
以下是一些確保函數可重入性的常見做法:
- 使用本地變量: 避免使用全局變量,因為全局變量是共享的,可能導致不同線程之間的競態條件。使用函數的參數和局部變量,這樣每個線程都有自己的副本。
- 避免使用靜態變量: 靜態變量在多線程環境中可能引發問題。如果確實需要使用靜態變量,要確保它們在函數內部是線程私有的,可以通過關鍵字 static 和函數作用域來實現。
- 避免使用不可重入的庫函數: 有些庫函數是不可重入的,因為它們使用了全局變量或其他共享資源。在多線程環境中,應該選擇可重入的庫函數或者使用線程安全的版本。
- 使用互斥鎖: 在必要的情況下,可以使用互斥鎖來保護共享資源,確保同一時刻只有一個線程能夠訪問這些資源。
- 注意信號處理: 在信號處理函數中,要謹慎使用那些不是異步信號安全(async-signal-safe)的函數,因為信號處理是在中斷上下文中執行的,可能會中斷正在執行的函數。
- 避免遞歸調用: 在一些情況下,遞歸調用可能導致函數不可重入。確保函數能夠正確處理遞歸調用,或者避免使用遞歸。
可重入函數的設計考慮到了并發執行的需求,因此在多線程環境中更為安全。在使用現代編程語言和庫時,通常會提供一些線程安全的工具和函數,但程序員仍然需要注意函數的可重入性。
volatile關鍵字
volatile 是一個在C和C++中使用的關鍵字,它主要用于告訴編譯器不要對被聲明為 volatile 的變量進行優化,因為這些變量的值可以在程序的執行流中被意外地改變。
主要作用:
- 禁止編譯器優化: 當一個變量被聲明為 volatile 時,編譯器會避免對該變量的操作進行優化。這是因為該變量的值可以被意外地改變,例如在中斷服務例程中。
- 告知編譯器不要緩存: 對于一些對硬件寄存器進行讀寫的情況,使用 volatile 可以告訴編譯器不要將這些寄存器的值緩存在寄存器中,而是要每次都從內存中讀取。這是因為這些寄存器的值可能會被硬件或者其他并發的代碼改變。
示例:
cvolatile int flag = 0; // 定義一個 volatile 變量void interruptServiceRoutine() {// 在中斷服務例程中改變 flag 的值flag = 1;
}int main() {while (flag == 0) {// 在循環中檢查 flag 的值// 由于 flag 是 volatile,編譯器不會進行優化,確保每次都從內存中讀取 flag 的值}// 執行其他操作...return 0;
}
注意事項:
- 不解決并發問題:volatile 并不能解決并發訪問的問題。它僅僅告訴編譯器不要對這個變量進行某些優化,但并不提供同步機制。在多線程環境下,仍需要使用互斥鎖等機制來確保對變量的原子操作。
- 適用于特定場景:volatile 通常用于與硬件相關的編程,比如在嵌入式系統中對寄存器的訪問。
- 不同編譯器的實現可能有差異: 標準中對 volatile 的語義定義相對寬泛,因此不同編譯器可能有不同的實現方式,特別是在多線程環境下。在需要跨平臺的代碼中,需要注意這一點。
總的來說,volatile 是一種告知編譯器的工具,用于處理一些特定的、容易被優化掉的場景,以確保程序的行為符合預期。
gcc/g++在進行編譯時,是有一些優化級別的選項:
這些選項是用來設置編譯器的優化級別的,通常用于控制生成可執行程序時的優化程度。這些選項的含義可能略有不同,具體取決于所使用的編譯器,但一般來說,它們包含以下幾個級別:
- -O0: 不進行優化。編譯器將生成易于調試的代碼,包括完整的調試信息,以便于在調試器中進行源代碼級別的調試。這會導致生成的可執行文件較大,執行速度較慢,但對于調試目的非常有用。
- -O1: 低級別的優化。編譯器會執行一些基本的優化,如刪除不可達代碼和一些局部優化,但不會進行過多的優化,以確保編譯速度較快。
- -O2: 中級別的優化。在-O1的基礎上,編譯器會執行更多的優化,包括一些可能會增加編譯時間的優化。這通常會產生更高效的代碼,但也可能增加生成可執行文件的時間。
- -O3: 高級別的優化。這一級別會進行更多、更激進的優化,包括一些可能會導致編譯時間顯著增加的優化。生成的代碼可能更加緊湊和高效,但這也可能導致一些編譯器可能無法處理的問題,或者增加代碼的復雜性。
- -Os: 以盡可能減小目標文件的大小為目標進行優化。這個選項更注重代碼大小而非執行速度,適用于一些嵌入式系統或者需要優化可執行文件大小的場景。
- -Ofast: 啟用除了標準不允許的一些優化,例如允許忽略 IEEE 浮點數規范,可能會導致數學計算結果的不確定性。這個選項通常用于對執行速度要求非常高、而對精確性要求相對較低的場景。
- -Og: 優化以保留調試信息的方式。這個選項在-O1級別的基礎上進行優化,但同時保留了對調試的支持,用于在開發階段進行調試。
- -On: 一些編譯器可能提供其他的優化級別,如 -O4、-O5 等,具體含義取決于編譯器的實現。
選擇優化級別通常是一個權衡,需要考慮編譯時間和生成代碼的效率。在開發和調試階段,通常會選擇較低的優化級別以獲得更好的調試支持和更快的編譯速度。在最終發布版本時,可以選擇較高的優化級別以獲得更好的執行性能。
請注意,具體的優化選項和級別可能因編譯器而異,建議查閱編譯器的文檔以獲取詳細信息。在實際應用中,選擇適當的優化級別需要根據具體情況進行權衡,考慮編譯時間、可執行文件大小和執行性能。
SIGCHLD(17號信號)
進程一章講過用wait和waitpid函數清理僵尸進程,父進程可以阻塞等待子進程結束,也可以非阻 塞地查詢是否有子進程結束等待清理(也就是輪詢的方式)。采用第一種方式,父進程阻塞了就不 能處理自己的工作了;采用第二種方式,父進程在處理自己的工作的同時還要記得時不時地輪詢一 下,程序實現復雜。
其實,子進程在終止時會給父進程發SIGCHLD信號,該信號的默認處理動作是忽略,父進程可以自 定義SIGCHLD信號的處理函數,這樣父進程只需專心處理自己的工作,不必關心子進程了,子進程 終止時會通知父進程,父進程在信號處理函數中調用wait清理子進程即可。
編寫一個程序完成以下功能:父進程fork出子進程,子進程調用exit(2)終止,父進程自定義SIGCHLD信號的處理函數,在其中調用wait獲得子進程的退出狀態并打印。
方法一:
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/wait.h>using namespace std;pid_t id;//因為信號和當前的main是兩個執行流,所以定義為全局的。void handler(int signo)
{cout << "捕捉到一個信號:" << signo << ", who:" << getpid() << endl;sleep(3);//等待3秒,期間可以查看子進程是否處于僵尸狀態while(1){//0:阻塞式等待,但是我們這里絕對不會阻塞!為什么呢?因為我已經收到了信號,所以當前子進程//肯定要退出了,所以wait只要調用就會立馬回收子進程并返回//-1:等待任意一個子進程退出,只要有死掉的子進程就會一直回收pid_t res = waitpid(-1, NULL, WNOHANG);//返回成功res就是等待的子進程的pidif(res > 0){printf("wait success, res: %d, id: %d\n", res, id);}elsebreak;}cout << "handler done..." << endl;
}int main()
{signal(SIGCHLD, handler);//如果一次性創建多個子進程呢?int i = 1;for(; i <= 10; i++){id = fork();if(id == 0){//childint cnt = 5;while(cnt){cout << "我是子進程,我的pid:" << getpid() << ", ppid:" << getppid() << endl;sleep(1);cnt--;}exit(1);}}//如果你的父進程沒有事干,你還是用以前的方法//如果你的父進程很忙,而且不退出,可以選擇信號的方法while(1){sleep(1);}return 0;
}
事實上,由于UNIX 的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調 用 signal/sigaction 將SIGCHLD的處理動作置為SIG_IGN (SIG_IGN,表示忽略該信號),這樣fork出來的子進程在終止時會自動清理掉,不 會產生僵尸進程,也不會通知父進程。系統默認的忽略動作和用戶用sigaction函數自定義的忽略 通常是沒有區別的,但這是一個特例。此方法對于Linux可用,但不保證在其它UNIX系統上都可 用。請編寫程序驗證這樣做不會產生僵尸進程
方法二:
nt main()
{signal(SIGCHLD, SIG_IGN);//將SIGCHLD的參數設置為SIG_IGN即可自動回收子進程//如果一次性創建多個子進程呢?int i = 1;for(; i <= 10; i++){id = fork();if(id == 0){//childint cnt = 5;while(cnt){cout << "我是子進程,我的pid:" << getpid() << ", ppid:" << getppid() << endl;sleep(1);cnt--;}exit(1);}}//如果你的父進程沒有事干,你還是用以前的方法//如果你的父進程很忙,而且不退出,可以選擇信號的方法while(1){sleep(1);}return 0;
}
因為子進程在終止時會給父進程發SIGCHLD信號,該信號的默認處理動作是忽略,所以父進程調 用 signal/sigaction 將它們參數的SIGCHLD的處理動作置為SIG_IGN (SIG_IGN,表示忽略該信號),這樣fork出來的子進程在終止時會自動清理掉,不會產生僵尸進程,也不會通知父進程。此方法對于Linux可用,但不保證在其它UNIX系統上都可 用。