目錄
前言
本篇重點解析普通信號(非實時信號), 以信號概念及產生 -- 信號發送及保存 -- 信號處理及操作, 三條主線依次進行
有些結論可能會過早的展示, 如有不懂或沒有解釋的地方就跳過繼續向后看
一.信號的概念以及產生 1.什么是信號
信號本質上是一種通信機制, 用戶或者操作系統通過以給指定進程發送信號來通知進程, 某件事情已經發生, 等待進程后續進行處理
所以, 進程對于信號要滿足以下要求
1.進程需要知道對應的信號, 應該如何被處理(不同信號不同的處理方法)
2.信號是隨機產生的, 所以進程對于信號是可以延后處理的(不得不延后的情況下)
3.進程可以存儲信號, 以便如果無法及時處理的話對該信號做存儲再延后處理
2.信號分為兩類
1.普通信號: 可以延后處理 2.實時信號: 不可以延后處理
例如: 如果同時給同一進程發送了兩個相同信號, 這時對于普通信號的處理方式是先處理該信號一次, 處理的同時將同種信號阻塞,然后處理好之后易語言關閉后進程還在, 再處理第二次發送的該信號... , 而實時信號不同, 它則能夠同一時間處理該信號兩次, 本質上這與普通信號與實時信號的實現有關, 在本篇文章的信號存儲部分會詳細解釋, 注: 本篇文章通篇討論的是普通信號
3.查看信號的命令
1.指令: kill -l查看全部信號, 注: 一共62個信號, 1~31, 一共31個信號為普通信號, 后31個信號為實時信號
2.man手冊: man 7
4.信號如何產生 1).通過軟件產生
OS先識別到某種軟件/進程觸發了某種條件或者不滿足某種條件, 此時構建信號再發送給指定進程
例一:
現象: 父子共用一條匿名管道, 當讀端關閉之后, 寫端進程自動退出
本質: 讀端關閉之后, OS向寫端發送信號終止寫端進程
例二:
現象: 父進程fork創建子進程, 子進程結束之后, 如果父進程沒有調用wait()/()等待回收子進程, 子進程進入僵尸狀態, 則通常會在父進程代碼邏輯中, 輪詢等待/阻塞等待回收子進程, 另一種也可以通過信號捕捉的方式來等待回收子進程, 雖然本質上還是調用()
本質: 子進程退出后會給父進程發送信號, 讓父進程自定義捕捉信號, 在自定義方法中去等待回收即可
以上兩點會在補充內容中用代碼驗證
2).通過硬件產生
通過一系列的硬件操作之后, 將操作結果保存或標記在硬件中, 當操作結束OS檢測硬件時就會檢測出標記的錯誤, 然后就會給特定進程發送信號
例一: 除0錯誤
在計算機中進行運算的是cpu, 而cpu是硬件, cpu內部有很多種寄存器, 且很多個寄存器, 當發生除0時, 狀態寄存器(以位圖的方式), 標記溢出標記位, 當OS進行檢測時便會檢測出錯誤
例二: 指針的非法訪問, 訪問空指針
每個進程都有屬于自己的進程地址空間, 我們稱之為虛擬內存地址空間, 通過頁表映射到物理內存, 那么當出現一個空指針或無效指針, 本質上就是這個指針不指向任何內容, 那么對于這個指針而言, 因為它不指向任何內容或有效內容, 所以頁表中就會標記這種指針, 當訪問的時候頁表+MMU操作的時候會有越界問題, 被OS檢測到, 就會給該進程發送信號
3).通過鍵盤組合鍵產生
一般通過鍵盤組合鍵的方式手動產生信號之后就自動的發送給指定進程了
例如: ctrl+c -- 2號信號 --
ctrl+\ -- 3號信號 --
二.信號的發送以及保存 1.信號如何發送
1).OS自動檢查, 自動構建信號, 自動發送信號
一般的, 信號通過軟件/硬件產生, 本質上也是由OS構建信號, 構建好之后也就自動的發送給進程了
2).可以手動發送, 指令: kill -[信號id] 進程pid
3).鍵盤組合鍵: ctrl+c, ctrl+\, ...
2.信號如何保存 1).概念
實際執行信號的處理動作 --- 遞達() --- 函數指針數組
信號從產生到遞達之間的狀態(發送中/待處理) --- 未決() --- 位圖
進程可與選擇阻塞某個信號 --- 阻塞(Block) --- block位圖
2).底層實現結構&&內核中的實現
存儲信號的底層數據結構: 位圖, 一個整數占4byte->個bit->32個bit, 剛好用一個整數表示32個普通信號, 通過0/1的方式
整體實現結構: 兩個位圖+一個函數指針數組
位圖(信號存儲集), block位圖(信號阻塞集), 函數指針數組(信號操作集)
被阻塞的信號產生時并發送到進程后, 會一直保存在(未決)位圖中(對應的那一位bit為1), 只有當阻塞狀態被解除時才會抵達, 也就是執行相應的處理動作
信號默認是非阻塞的, 并且默認的處理動作是默認動作或忽略動作, 即或, 當捕捉到信號時會修改對應信號的函數指針(操作方法)
3).阻塞vs忽略
阻塞與忽略是不同的, 信號一但被阻塞就不會被遞達, 但信號如果被忽略, 仍會被抵達, 只不過執行的動作是忽略, 本質區別就是前者沒有被抵達而后者反之
三.信號的處理以及操作 1.信號被處理的時機(重點)
信號相關的數據是存放在PCB中的, 想要修改PCB數據, 必須經過內核也就是必須由OS操作, 而我們在程序中對PCB中內核數據結構的修改也必須是通過系統調用的方式, 本質上還是由OS去執行的, 處理信號意味著改變PCB內核數據結構中的數據, 所以信號操作一定是在內核態的狀態下進行的, 由于內核態下執行的代碼優先級非常高, 所以OS設計者選擇將信號的處理放在, 進程從內核態轉變為用戶態的前一刻信號被處理
科普一
當進程進行系統調用/出現異常/中斷等操作時會進入內核態, 進入內核態的過程對于我們而言是透明的(系統調用本質是通過int 80匯編使當前進程陷入到內核中), 那如果是一個沒有系統調用, 也不會出現異常的進程, 也是一定會進入內核態的, 因為進程在OS上跑, 而OS對進程采用類似于輪詢檢測的方式發生中斷, 來檢查進程時間片和其他等等, 當切換到用戶態的前一刻, OS就會去處理進程中待遞達的信號
科普二
進程如何去執行操作系統級別的代碼呢
每個進程都有屬于自己的4G虛擬內存地址空間, 其中3~4G是內核地址空間, 每一個進程都是如此, 我們平時說的進程去執行操作系統的代碼了, 就是進程進入了內核態, 去訪問那3~4G的數據, 而3~4G的數據映射的是什么呢, 對于1~3G的用戶空間而言, 為了保證進程的獨立性, 每個進程的虛擬地址空間都有自己的頁表(用戶級頁表)映射到不同的物理地址, 而在3~4G內存空間有對應的內核級頁表, 每個進程中的1G內核空間都通過同一張內核級頁表映射到該機器的操作系統中, 去執行OS級別的代碼(系統調用...), 內核本質上也是在所有進程的地址空間上下文中執行的, 普通進程是否有權利執行內核代碼完全取決于CPU是處于哪一種狀態,內核態or用戶態?
科普三
什么是進入內核態, 計算機如何區分當前CPU是處在用戶態還是內核態
首先明確一個概念, 用戶態還是內核態是針對CPU而言的, 并不是進程, 當一個進程可以執行OS代碼說明當前的CPU處于內核態, CPU中存在很多寄存器, 一套可見, 一套不可見, 其中CR3寄存器表示當前CPU的執行權限例如1表示內核3表示用戶
2.信號的三種處理方式
1 默認 -- -- # (()0)
2 忽略 -- -- # (()1)
-- void(*)(int);
3 自定義捕捉
3.如何對信號自定義捕捉
1.
#
void(*)(int);
(int , );
參數:
: 要捕捉的信號編號
: 函數指針
需要自己定義一個void (int )函數, 該函數是捕捉到指定信號后對應的自定義處理方法
以回調函數的方式, 向傳入函數指針對函數進行回調
返回值:
捕捉成功, 返回舊的函數, 即信號操作方法
捕捉失敗, 返回, 并且錯誤碼被設置
代碼驗證: 使用捕捉信號
#include
#include
#include
using namespace std;
//注冊自定義捕捉信號的處理方式
void handler(int signum)
{
printf("進程[%d],已捕捉到%d信號\n", getpid(), signum);
}
int main()
{
//自定義捕捉SIGINT信號(2號)
//注意:這里只是在注冊捕捉到的遞達信號的處理行為,并不是在這里調用handler,而是注冊!
//handler是在SIGINT信號遞達時調用的
signal(SIGINT, handler);
printf("I am process: %d\n", getpid());
//不讓進程退出, 方便觀察信號遞達后的自定義捕捉行為
while(true) sleep(1);
return 0;
}
2.
#
int (int signo, const *act, *oact);
解釋: 結構體
{
void (*)(int); //關心, 注冊自定義信號處理方法
void (*)(int, *, void *);//不關心,不用管
; //關心易語言關閉后進程還在, 信號屏蔽字
int ; //不關心, 默認為0
void (*)(void); //不關心, 不用管
};
參數:
signo: 信號編號
act: 若為非空, 根據act修改信號處理動作
oact: 若為非空, 通過oact輸出舊的信號處理動作
對于act.重點介紹
當捕捉一次信號之后, 被捕捉的信號在處理完之前默認在信號屏蔽字中將該信號屏蔽, 處理好之后再接觸相應的屏蔽, 即是去設定在處理當前捕捉到的信號時對于屏蔽字的處理(可以添加一些其他屏蔽的信號), 用去覆蓋掉當前屏蔽字, 這也是與方式捕捉信號的一大區別
#include
#include
#include
#include
using namespace std;
//lab3 -- sigaction捕捉信號的同時, 設置處理時屏蔽字
void MyPrint(sigset_t &set)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(&set, sig)) cout << 1;
else cout << 0;
}
cout << endl;
}
//捕捉SIGINT信號
void handler(int signum)
{
cout << "正在處理信號: " << signum << "(15s)" << endl;
//當第二次發送2號信號時, 應該觀察到pending會存有該信號
//如果發送3456號信號, pending信號集也會有該對應的未決信號標識
//因為處理2號信號期間2好信號被自動屏蔽,3456信號被手動屏蔽
int n = 15;
while(n--)
{
sigset_t set;
sigemptyset(&set);
sigpending(&set);
printf("[%d]pending signal: ", getpid());
MyPrint(set);
sleep(1);
}
}
int main()
{
cout << "I am process: " << getpid() << endl;
struct sigaction act, oact;
act.sa_flags = 0;
act.sa_handler = handler;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, 3);
sigaddset(&set, 4);
sigaddset(&set, 5);
sigaddset(&set, 6);
act.sa_mask = set;
sigaction(SIGINT, &act, &oact);
while(1)
{
sleep(1);
}
return 0;
}
4.類型
是系統級別的變量類型, 是linux定義的類型, 類型的變量用來表示block位圖與位圖
稱為信號集, 這個類型可以表示每個信號有效或無效狀態, 即1或0
由定義出來的變量不可以直接修改與打印, 需要通過以下函數來進行操作
常見操作:, , , , , ,
#
int ( *set); // 初始化set所指向的信號集, 將其所有bit清零
int ( *set); //初始化set所指向的信號集, 將其所有bit置為1
int ( *set, int signo); // 添加signo信號到set
int ( *set, int signo); // 刪除signo信號到set
int (const *set, int signo); // 判斷set是否包含signo信號
以上都需要先自定義一個set信號集
5.如何操作信號集
int ( *set); // 將信號集獲取到set中, 這個set是輸出型參數
代碼驗證: 打印信號集
void PrintPending(sigset_t &pending)
{
for(int sig = 1; sig <= 31; ++sig)
{
//判斷sig是否存在于pending信號集中
//存在: 1 不存在: 0
if(sigismember(&pending, sig))
{
cout << 1;
}
else
{
cout << 0;
}
}
cout << endl;
}
int main()
{
sigset_t set;
//初始化
sigemptyset(&set);
//獲取pending信號集
sigpending(&set);
PrintPending(set);
return 0;
}
6.如何操作block阻塞信號集
阻塞信號集也叫做當前進程的信號屏蔽字( Mask), 這里的屏蔽是指阻塞而并不是忽略!
#
int (int how, const *set, *oset);
參數:
how: 傳入標記位, 以下的mask代表信號屏蔽字
: 添加要阻塞的信號到信號屏蔽字, 相當于mask = mask | set
: 從當前信號屏蔽字解除特定阻塞信號, 相當于mask = mask&~set
: 設置當前信號屏蔽字為set所指向的位圖, 相當于mask = set
set與oset:
如果oset是非空指針, 則讀取修改之前的信號屏蔽字到oset
如果set是非空指針, 則根據how的修改方式去修改set信號屏蔽字, 然后覆蓋掉進程的信號屏蔽字
如果調用解除了對當前若干個未決信號的阻塞,則在返回前,至少將其中一個信號遞達。
代碼驗證:逐一屏蔽1~31號信號, 然后不斷向進程發送1~31號信號, 觀察信號集
#include
#include
#include
#include
using namespace std;
//lab1 -- 逐一屏蔽1~31號信號, 然后不斷向進程發送1~31號信號, 觀察pending信號集
//注: 9號與19號信號不會被屏蔽
//打印pending信號集/block屏蔽字
void MyPrint(sigset_t &set)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(&set, sig)) cout << 1;
else cout << 0;
}
cout << endl;
}
//屏蔽signum信號
void MyBlock(int signum)
{
sigset_t bset, obset;
sigemptyset(&bset);//初始化
sigaddset(&bset, signum);//屏蔽signum信號
int n = sigprocmask(SIG_BLOCK, &bset, &obset);//設置信號屏蔽字并且獲取舊的
assert(n == 0);
(void)n;
//打印信號屏蔽字
printf("[%d]block signal: ", getpid());
MyPrint(obset);
}
int main()
{
for(int sig = 1; sig <= 31; ++sig)
{
printf("[%d]屏蔽信號%d\n", getpid(), sig);
MyBlock(sig);
sleep(1);
}
sigset_t set;
while(true)
{
//打印pending信號集
sigemptyset(&set);
sigpending(&set);
printf("[%d]block signal: ", getpid());
MyPrint(set);
sleep(1);
}
return 0;
}
7.特殊的9號與19號信號
9號信號 --- 一定會殺掉指定進程
9號信號屬于管理員信號, 不會被用戶捕捉也不會被用戶屏蔽!
19號信號 --- 不會被用戶屏蔽
8.block存在的意義
如果一個信號在同一時刻被發送了兩次, OS如何處理?
回答: 當一個信號第一次被遞達, OS在處理信號時, 在處理期間會將該信號屏蔽, 當處理完之后解除該信號的屏蔽, 也就是說上述問題, 同一時刻發送兩次, OS也只能一個一個的進行對于信號的處理
為了保證系統安全, 避免信號遞歸式的處理, OS在同一時刻下同一種信號只能操作1次, 就是通過block屏蔽字實現的
注: 非實時信號(1~31普通信號), 位圖只能標記是否有信號未決, 并不能存儲有幾個相同信號的數量
所以如果同時發送三個相同信號, 那么也是先處理1次, 處理期間屏蔽該信號, 處理完再處理1次, 因為后兩次是第一次屏蔽后發送的所以只能標記有該信號未決, 后續也只能處理1次, 所以同時發送了三個相同信號, 最終只會處理兩次
#include
#include
#include
#include
using namespace std;
//lab2 -- block信號屏蔽字的意義
void MyPrint(sigset_t &set)
{
for(int sig = 1; sig <= 31; ++sig)
{
if(sigismember(&set, sig)) cout << 1;
else cout << 0;
}
cout << endl;
}
void handler(int signum)
{
cout << "正在處理信號: " << signum << "(15s)" << endl;
//當第二次發送2號信號時, 應該觀察到pending會存有該信號
//因為處理2號信號期間2好信號被自動屏蔽
int n = 15;
while(n--)
{
sigset_t set;
sigemptyset(&set);
sigpending(&set);
printf("[%d]pending signal: ", getpid());
MyPrint(set);
sleep(1);
}
}
int main()
{
cout << "I am process: " << getpid() << endl;
//用2號信號實驗
signal(SIGINT, handler);
while(1)
{
sleep(1);
}
return 0;
}
四.信號的整體流程(總結)
產生信號(軟件/硬件/鍵盤組合鍵)
---> 發送信號(自動/手動)
---> 保存信號(未決)
---> 內核態到用戶態的前一刻,OS處理可遞達信號
---> 判斷是否阻塞(block屏蔽字)
--->若未阻塞則處理信號(遞達并處理, 方式: 默認/忽略/自定義捕捉)
五.補充內容 1.核心轉儲
關于核心轉儲:
如何觸發核心轉儲:Core Dump
1.-- 默認處理動作為終止進程
2.abort()函數 -- 本質發送 -- 默認處理動作當作異常去終止進程
3.異常 -- 例如: 除0, 非法訪問等等
表現為: 當進程異常終止時(段錯誤, 除零錯誤, 訪問空指針等等), 會在當前目錄下自動生成一個核心轉儲文件, 該文件內容全部都是二進制數據, 很顯然, 這個文件不是用來直接讓用戶讀取的
關于核心轉儲文件的使用: 我們可以使用Linux下的gdb調試工具載入core文件, 此時gdb調試工具會自動給我們顯示出異常具體出現在哪一行, 該行為叫做Post- Debug(事后調試)
注意: 當然, 在Linux默認是將Core Dump關閉的, 也就是默認不會生成core文件, 因為core文件中可能會包含用戶密碼等敏感信息, 不安全, 或者某種場景下進程也許會一直生成core文件, 大量的文件會占用磁盤空間
如何打開核心轉儲: 在開發調試階段, 可以使用命令改變這個現實, 允許產生core文件, 使用命令改變Shell進程的 Limit且允許core文件最大為1024K(自定義)
()函數的第二個參數: 做為輸出型參數, 傳入&; 的倒數第八位用來標記Core Dump, 1 or 0, 即是否發生核心轉儲
總結/本質: 進程出現異常 or 收到信號, 可以選擇把進程的用戶空間數據(內存中的核心數據)全部保存到磁盤上
-a: 查看
-c 1024(自定義): 打開核心轉儲
#include
#include
#include
#include
using namespace std;
int main()
{
sleep(1);
int a = 10;
int b = 0;
cout << a / b << endl;//除零錯誤
return 0;
}
除零錯誤異常-->觸發核心轉儲
發送信號-->觸發核心轉儲
使用od命令查看core文件
使用gdb操作core文件
1.使用gdb調試進程
2.core-file [core文件名]
2.關鍵字(補充)
#include
#include
#include
using namespace std;
int flag = 1;
void handler(int signum)
{
cout << "old flag: " << flag;
flag = 0;
cout << "->new flag: " << flag << endl;
}
int main()
{
signal(SIGINT, handler);
// 驗證volatile關鍵字
while (flag);
cout << "進程退出flag: " << flag << endl;
return 0;
}
g++編譯代碼時不加優化選項, 此時關鍵字使不使用沒有區別
給g++編譯命令添加-O3優化選項, 運行結果:
進程執行起來之后即使將flag從1改為0, 也沒有跳出while循環
當加了-O3優化選項, CPU在讀取flag全局變量時做出了優化, 對于flag而言, CPU不再每次循環都去內存訪問flag而是直接將flag存到寄存器內, 因為在CPU看來flag變量是永遠不會被修改的, 所以本質上-O3這個優化選項, 對于flag而言無法讓CPU在看到內存了
此時, 即將登場, 關鍵字的作用: 被修飾的變量無視編譯器的優化, 保持了變量的可見性(內存對于CPU而言)
接下來使用修飾flag, 再使用-O3優化, 查看執行結果:
flag從1->0, 直接跳出while循環, 進程結束!
#include
#include
#include
using namespace std;
volatile int flag = 1;
void handler(int signum)
{
cout << "old flag: " << flag;
flag = 0;
cout << "->new flag: " << flag << endl;
}
int main()
{
signal(SIGINT, handler);
// 驗證volatile關鍵字
while (flag);
cout << "進程退出flag: " << flag << endl;
return 0;
}
3.kill()/raise()/abort() 1).kill()
kill屬于指令, 可以向指定進程發送信號
kill命令是調用kill函數實現的, kill函數的功能同理: 可以給指定進程發送指定信號
#
int kill(pid_t pid, int signo);
#include
#include
#include
using namespace std;
int main()
{
cout << "2s后進程結束" << endl;
sleep(2);
kill(getpid(), SIGINT);
return 0;
}
2).raise()
raise函數可以給當前進程發送指定信號
#
int raise(int signo);
#include
#include
#include
using namespace std;
int main()
{
cout << "2s后進程結束" << endl;
sleep(2);
raise(SIGINT);
return 0;
}
3).abort()
abort函數 -- 可以使當前進程接收到信號(6號)而異常終止
#
void abort();
#include
#include
#include
using namespace std;
int main()
{
cout << "2s后進程結束" << endl;
sleep(2);
abort();
return 0;
}
4.定時鬧鐘
alarm()函數 -- 鬧鐘函數 -- 本質: 向當前進程發送14號信號 --
#
int alarm( int );
參數:
秒之后, 給當前進程發送信號, 該信號的默認處理動作是終止當前進程(當然你也可以自定義捕捉)
返回值:
0 or 之前設定的鬧鐘剩余的秒數
假設定一個鬧鐘30min, 在第20min時再設定鬧鐘為15min, 此時返回10min即600s
注:
如果為0, 表示取消以前設定的鬧鐘, 函數的返回值仍是以前設定的鬧鐘時間剩余的秒數
代碼驗證:
驗證一: 自定義捕捉
#include
#include
#include
#include
using namespace std;
void handler(int signum)
{
cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(2);
cout << "已設置鬧鐘, 2s后" << endl;
int count = 4;
while(count--)
{
sleep(1);
}
return 0;
}
驗證二: 驗證alarm返回值
#include
#include
#include
#include
using namespace std;
void handler(int signum)
{
cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
exit(1);
}
int main()
{
signal(SIGALRM, handler);
alarm(10);//先設置一個10s鬧鐘
int count = 4;
while(count--)
{
sleep(1);
}
unsigned int ret = alarm(2);//4s后設置一個2s鬧鐘
//ret預期為6
cout << "alarm返回值: " << ret << endl;
while(true);
return 0;
}
實驗: 使用定時鬧鐘達到輪詢效果
#include
#include
#include
#include
using namespace std;
void handler(int signum)
{
cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
alarm(2);
}
int main()
{
signal(SIGALRM, handler);
alarm(2);//設置2s鬧鐘
while(true);
return 0;
}
5.信號信號 1).
信號產生原理: 使用匿名管道讓父子進程進行通信, 讓某一進程做寫端, 某一進程做讀端, 當關閉讀端, 寫端會被OS發送信號(因為讀端關閉了, 寫端就沒有任何意義)
代碼驗證: 父寫子讀
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void handler(int signum)
{
cout << getpid() << " I am father, I receive a signal: " << signum << endl;
cout << "1s后回收子進程,并終止父進程\n";
sleep(1);
waitpid(-1, nullptr, 0);
exit(2);
}
int main()
{
//目標: 驗證SIGPIPE信號
//父進程一開始就注冊好SIGPIPE操作
signal(SIGPIPE, handler);
printf("I am father: %d\n", getpid());
//創建匿名管道
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
(void)ret;
//創建子進程
pid_t id = fork();
if(id == 0)
{
printf("I am child: %d\n", getpid());
//執行子進程邏輯,讓子進程做讀端
close(pipefd[1]);
//進行通信
//...
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
while(true)
{
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[n] = '\0';
printf("子進程已接收到數據: %s\n", buffer);
if(n == 0)
{
cout << "寫端已退出, 我也即將退出\n";
break;
}
if(strcmp(buffer, "quit") == 0)
{
break;
}
}
//讀端退出后, 向父進程發送SIGPIPE信號
printf("子進程(讀端)即將退出\n");
exit(1);
}
//父進程做寫端
close(pipefd[0]);
string msg;
//不停的寫, 即使輸入quit將讀端退出了, 也一直寫
while(true)
{
getline(cin, msg);
write(pipefd[1], msg.c_str(), msg.size());
}
return 0;
}
代碼驗證: 父讀子寫
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 驗證SIGPIPE: 父讀子寫
int main()
{
// 目標: 驗證SIGPIPE信號
printf("I am father: %d\n", getpid());
// 創建匿名管道
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
(void)ret;
// 創建子進程
pid_t id = fork();
if (id == 0)
{
printf("I am child: %d\n", getpid());
// 執行子進程邏輯,讓子進程做寫端
close(pipefd[0]);
// 進行通信
//...
string msg;
while (true)
{
getline(cin, msg);
write(pipefd[1], msg.c_str(), msg.size());
// 這里不需要讓寫端break, 因為讀端關閉了寫端也就關閉了
}
// 讀端關閉后, 向子進程發送SIGPIPE信號
// 繼而子進程被SIGPIPE信號結束,等待父進程回收檢測
exit(1);
}
// 父進程做讀端
close(pipefd[1]);
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
while (true)
{
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[n] = '\0';
printf("父進程已接收到數據: %s\n", buffer);
if (n == 0)
{
cout << "寫端已退出, 我也即將退出\n";
break;
}
if (strcmp(buffer, "quit") == 0)
{
break;
}
}
close(pipefd[0]); // 關閉讀端
// 子進程做為寫端也會SIGPIPE結束,等待回收子進程
int status = 0;
waitpid(-1, &status, 0); // 阻塞回收
cout << "回收子進程成功, 檢測到子進程退出信號: " << (status & 0x7f) << endl;
return 0;
}
2).
信號產生原理: 當父進程創建了子進程, 子進程退出或暫停時, 就會給父進程發送信號
OS的對信號的默認處理行為是Ign(忽略)
這有什么意義呢? 如果子進程退出了, 而這時父進程還需要執行自己的邏輯代碼, 還沒有到達回wait/邏輯時, 此時就可以采用捕捉信號的方式, 來在執行父進程邏輯的過程中, 在合適的時機去處理信號(注意: 信號處理方法代碼與進程代碼在處理過程中是同一執行流), 也就是說父進程不需要主動關心子進程的回收問題了, 而是當信號遞達父進程就處理即可
可以使用()捕捉信號, 然后對子進程進行回收
也可以使用()捕捉信號, 然后執行(忽略), 這與OS默認的Ign有所區別
Ign與的區別
Linux規定, 父進程捕捉處理動作置為這樣子進程在終止退出時OS自動清理回收, 不會產生僵尸進程, 也不會通知父進程, 此方法對于Linux可用, 并不代表在其他UNIX系統上都可用
一般的Term為Ign的信號就是OS直接忽略掉了, 說白了就是什么都不做,系統默認的忽略動作Ign和用戶用()/()函數自定義的忽略通常情況下是相同的, 但這是一個特例!
代碼驗證:
#include
#include
#include
#include
#include
#include
using namespace std;
void handler(int signum)
{
cout << "父進程接收到信號: " << signum << endl;
// 輪詢檢測式回收子進程,WNOHANG非阻塞等待
// 注:
pid_t id = 0;
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "成功回收進程: " << id << endl;
}
cout << "handler處理結束, 已返回父進程邏輯代碼\n";
}
int main()
{
// 處理SIGCHLD方式一: 自定義捕捉并且回收子進程
signal(SIGCHLD, handler);
// 處理SIGCHLD方式二: 忽略
// signal(SIGCHLD, SIG_IGN);
cout << "I am father: " << getpid() << endl;
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
cout << "I am child: " << getpid() << ", 我將在2s后退出" << endl;
sleep(2);
exit(0);
}
while (true)
{
sleep(1);
}
return 0;
}
方式一:
方式二: