前面的文章中我們講述了信號的產(chǎn)生與信號的保存這兩個知識點,在本文中我們將繼續(xù)講述與信號處理有關(guān)的信息。
信號處理
之前我們說過在收到一個信號的時候,這個信號不是立即處理的,而是要得到的一定的時間。從信號的保存中我們可以知道如果一個信號之前被block,當(dāng)解除block的時候,對應(yīng)的信號會立即被遞達(dá)。因為信號的產(chǎn)生是異步的,當(dāng)前進(jìn)程可能在做更重要的事情,當(dāng)進(jìn)程從內(nèi)核態(tài)切換回用戶態(tài)的時候,進(jìn)程就會在OS的指導(dǎo)下進(jìn)行信號的檢測與處理。
用戶態(tài)、內(nèi)核態(tài)
首先我們先來講講這兩個狀態(tài),用戶態(tài):執(zhí)行自己寫的代碼的時候,進(jìn)程所處的狀態(tài);內(nèi)核態(tài):執(zhí)行OS的代碼的時候,進(jìn)程所處的狀態(tài)。
- 當(dāng)進(jìn)程的時間片到了需要切換時,就要執(zhí)行進(jìn)程切換邏輯。
- 系統(tǒng)調(diào)用
之前在進(jìn)程地址空間中我們學(xué)習(xí)過進(jìn)程地址空間的相關(guān)知識,我們知道PCB連接到進(jìn)程地址空間,然后通過頁表的映射,映射到物理內(nèi)存中。之前我們只學(xué)習(xí)了用戶空間,里面有堆、棧、代碼等。我們知道操作系統(tǒng)也是一段代碼,而在進(jìn)程地址空間中的內(nèi)核空間就是存儲的OS的代碼與數(shù)據(jù)映射的地方,因此同樣需要一張內(nèi)核級的頁表。以32位的系統(tǒng)為例子,所有的進(jìn)程地址空間中的0-3GB都是不同的存放的是該進(jìn)程自己的代碼與數(shù)據(jù),匹配了自己的用戶級頁表;所有進(jìn)程的3-4GB都是一樣的存放的是OS的代碼與數(shù)據(jù),每一個進(jìn)程都可以看到同樣的一張內(nèi)核級頁表,所有進(jìn)程都可以通過統(tǒng)一的窗口看到同一個OS;OS運行的本質(zhì):其實都是在進(jìn)程的地址空間中運行的;所以所謂的系統(tǒng)調(diào)用,其實就如同調(diào)用.SO中的方法,在自己的地址空間中進(jìn)行函數(shù)跳轉(zhuǎn)并返回即可。
此時就會出現(xiàn)一個問題,正應(yīng)為OS的代碼與數(shù)據(jù)跟用戶的代碼與數(shù)據(jù)在同一個地址空間中,為了防止用戶隨意的訪問OS的數(shù)據(jù)與代碼,因此就有了用戶態(tài)與內(nèi)核態(tài)。當(dāng)執(zhí)行自己的代碼,對應(yīng)的狀態(tài)就是用戶態(tài),要對系統(tǒng)調(diào)用進(jìn)行訪問,OS就會對身份,執(zhí)行級別進(jìn)行檢測,檢測到不是內(nèi)核態(tài)就會終止進(jìn)程。在CPU中存在一種寄存器叫做CR3,里面有對應(yīng)的比特位,比特位為0表征正在運行的進(jìn)程是用戶態(tài),比特位為3表征正在運行的進(jìn)程級別是內(nèi)核態(tài)。由于用戶無法直接對級別進(jìn)行修改,因此OS提供的系統(tǒng)調(diào)用,內(nèi)部在正式執(zhí)行調(diào)用邏輯的時候會去修改執(zhí)行級別。
進(jìn)程是如何被調(diào)度的?
首先我們要講一下OS。OS的本質(zhì)是軟件,本質(zhì)是一個死循環(huán);OS時鐘硬件,每個很短的時間向OS發(fā)送時鐘中斷,然后OS要執(zhí)行對應(yīng)的中斷處理方法。進(jìn)程被調(diào)度就是時間片到了,然后OS將進(jìn)程對應(yīng)的上下文等進(jìn)行保存并切換,選擇合適的進(jìn)程,這通過系統(tǒng)函數(shù)schedule()函數(shù)執(zhí)行上面的保存任務(wù)。
內(nèi)核如何實現(xiàn)信號的捕捉
?如果信號的處理動作是用戶自定義函數(shù),在信號遞達(dá)時就調(diào)用這個函數(shù),這稱為捕捉信號。由于信號處理函數(shù)的代碼是在用戶空間的,處理過程比較復(fù)雜,舉例如下: 用戶程序注冊了SIGQUIT信號的處理函數(shù)sighandler。 當(dāng)前正在執(zhí)行main函數(shù),這時發(fā)生中斷或異常切換到內(nèi)核態(tài)。 在中斷處理完畢后要返回用戶態(tài)的main函數(shù)之前檢查到有信號SIGQUIT遞達(dá)。 內(nèi)核決定返回用戶態(tài)后不是恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行,而是執(zhí)行sighandler函 數(shù),sighandler和main函數(shù)使用不同的堆??臻g,它們之間不存在調(diào)用和被調(diào)用的關(guān)系,是 兩個獨立的控制流程。 sighandler函數(shù)返回后自動執(zhí)行特殊的系統(tǒng)調(diào)用sigreturn再次進(jìn)入內(nèi)核態(tài)。 如果沒有新的信號要遞達(dá),這次再返回用戶態(tài)就是恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行了。
sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
當(dāng)某個信號的處理函數(shù)被調(diào)用時,內(nèi)核自動將當(dāng)前信號加入進(jìn)程的信號屏蔽字,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產(chǎn)生,那么 它會被阻塞到當(dāng)前處理結(jié)束為止。 如果在調(diào)用信號處理函數(shù)時,除了當(dāng)前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字。
下面我們看一個簡單的例子:
static void PrintPending(const sigset_t &pending) {
cout << "當(dāng)期進(jìn)程的pending信號集:";
for (int signo = 1; signo <= 31; ++signo) {
if (sigismember(&pending, signo)) // 用于打印信號集
cout << "1";
else
cout << "0";
}
cout << endl;
}
static void handler(int signo) { // 添加了static之后該函數(shù)只能在本文件中使用
cout << "對特定信號:" << signo << "執(zhí)行捕捉動作" << endl;
int cnt = 10;
while (cnt) {
cnt--;
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
PrintPending(pending);
cout << "打印完成pending信號集" << endl;
sleep(1);
}
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); // 可以添加其他信號的阻塞方式,在自定義捕捉時將其余收到的信號阻塞
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
sigaction(2, &act, &oldact);
while (true) {
cout << getpid() << endl;
sleep(1);
}
}
通過在對2號信號實行自定義捕捉的時候給進(jìn)程發(fā)送3,4,5號信號,就可以通過打印信號集來查看該信號是否被阻塞。?
其余知識點
可重入函數(shù)
我們以鏈表結(jié)點指針的頭插為例子:
一般頭插分為兩步首先將新節(jié)點插入在鏈表之前,然后再將頭指針指向新節(jié)點的地址,如果在第一步的時候進(jìn)行了信號的自定義動作保存了當(dāng)前函數(shù)執(zhí)行的狀態(tài),在自定義動作之中又執(zhí)行了一次鏈表頭插的動作,那么當(dāng)自定義動作處理結(jié)束之后,返回至用戶態(tài)函數(shù)執(zhí)行的地方,就會繼續(xù)原先的插入動作,那么我們在自定義函數(shù)中的插入結(jié)點就會丟失,導(dǎo)致內(nèi)存泄漏。
?main函數(shù)調(diào)用insert函數(shù)向一個鏈表head中插入節(jié)點node1,插入操作分為兩步,剛做完第一步的 時候,因為硬件中斷使進(jìn)程切換到內(nèi)核,再次回用戶態(tài)之前檢查到有信號待處理,于是切換 到sighandler函數(shù),sighandler也調(diào)用insert函數(shù)向同一個鏈表head中插入節(jié)點node2,插入操作的 兩步都做完之后從sighandler返回內(nèi)核態(tài),再次回到用戶態(tài)就從main函數(shù)調(diào)用的insert函數(shù)中繼續(xù) 往下執(zhí)行,先前做第一步之后被打斷,現(xiàn)在繼續(xù)做完第二步。結(jié)果是,main函數(shù)和sighandler先后 向鏈表中插入兩個節(jié)點,而最后只有一個節(jié)點真正插入鏈表中了。
如果一個函數(shù)符合以下條件之一就是不可重入的:
- 調(diào)用了malloc或free,因為malloc也是用全局鏈表來管理堆的。
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù)。標(biāo)準(zhǔn)I/O庫的很多實現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
volatile
下面我們來看一個關(guān)鍵字volatile,首先我們來看一個例子:
int quit = 0; // 保證內(nèi)存可見性
void handler(int signo) {
printf("change quit from 0 to 1\n");
quit = 1;
printf("quit : %d\n", quit);
}
int main() {
signal(2, handler);
while(!quit); //注意這里我們故意沒有攜帶while的代碼塊,故意讓編譯器認(rèn)為在main中,quit只會被檢測
printf("main quit 正常\n");
return 0;
}
運行上述的代碼,就與我們之前學(xué)習(xí)的一樣會讓全局變量quit由0變1,進(jìn)行打印然后退出。
我們在編譯的時候是有優(yōu)化的級別的,可以根據(jù)不同的優(yōu)化級別記性優(yōu)化。我們選擇-O2來對上述的代碼記性優(yōu)化,可以發(fā)現(xiàn)我們雖然可以自定義捕捉信號,變量quit同樣也變成了1,但是卻無法讓程序退出。
下面我們來解釋一下為什么??CPU匹配的運算種類只有兩種,算術(shù)運算與邏輯運算,while循環(huán)的代碼需要在CPU上執(zhí)行,因為只有CPU能夠進(jìn)行計算,因此需要我們先將quit加載到CPU中,然后再進(jìn)行真假的判斷,在CPU中還有記錄當(dāng)前程序位置的指針,當(dāng)判斷條件生效之后,指針就會指向下一句代碼。這就是為什么我們能夠退出的原因。
?while循環(huán)是一種運算,這樣的運算是需要運算源的,每次都需要將數(shù)據(jù)從內(nèi)存加載到CPU中,編譯器發(fā)現(xiàn)在main函數(shù)中quit的值并沒有修改,而只是進(jìn)行判斷,編譯器就會認(rèn)為每次的quit數(shù)據(jù)都是一樣的,那么就會進(jìn)行優(yōu)化將數(shù)據(jù)第一次load進(jìn)CPU中,然后就不再進(jìn)行加載工作,只檢測CPU中寄存器的保存的quit數(shù)據(jù),相當(dāng)于讓CPU中的quit替換掉了內(nèi)存中的quit。這就導(dǎo)致了quit為什么進(jìn)行了修改但是并沒有退出的問題。為了告訴編譯器,保證每次檢測都要從內(nèi)存中進(jìn)行數(shù)據(jù)讀取不要用寄存區(qū)中的數(shù)據(jù)覆蓋,讓內(nèi)存數(shù)據(jù)可見,因此就有了volatile。
volatile 作用:保持內(nèi)存的可見性,告知編譯器,被該關(guān)鍵字修飾的變量,不允許被優(yōu)化,對該變量的任何操作,都必須在真實的內(nèi)存中進(jìn)行操作
SIGCHLD
在進(jìn)程等待的哪里我們學(xué)習(xí)過子進(jìn)程退出之后如果父進(jìn)程不進(jìn)行處理,子進(jìn)程就會變?yōu)榻┦M(jìn)程,然后我們就學(xué)習(xí)了waitpid和wait函數(shù)清理僵尸進(jìn)程。父進(jìn)程可以以非阻塞或者阻塞的方式進(jìn)行主動檢測,由于子進(jìn)程推出了,父進(jìn)程暫時不知道。子進(jìn)程在退出的時候會給父進(jìn)程發(fā)送SIGCHLD信號,該信號的默認(rèn)處理動作是忽略(SIG_DFL)什么都不做。
我們就可以使用自定義捕捉的方法進(jìn)行檢測?:
那么我們就設(shè)想可以在自定義捕捉中進(jìn)行對僵尸進(jìn)行的處理,這樣就可以讓父進(jìn)程做自己的事情,可以自動對子進(jìn)程進(jìn)行回收。
pid_t id ;
void waitProcess(int signo) {
printf("捕捉到一個信號: %d, who: %d\n", signo, getpid());
sleep(5);
while (1) {
// 這里若設(shè)置的為0,那么如果有些子進(jìn)程退出了,有部分子進(jìn)程沒有退出導(dǎo)致自定義捕捉的函數(shù)無法返回會一直阻塞在里面,因此要設(shè)置為非阻塞的等待方式
pid_t res = waitpid(-1, NULL, WNOHANG); // -1表示等待任意一個子進(jìn)程
if (res > 0)
{
printf("wait success, res: %d, id: %d\n", res, id);
}
else break; // 如果沒有子進(jìn)程了?
}
printf("handler done...\n");
}
void handler(int signo) {
printf("捕捉到一個信號: %d, who: %d\n", signo, getpid());
}
int main() {
signal(SIGCHLD, waitProcess);
// signal(SIGCHLD, handler);
int i = 1;
for (; i <= 10; i++) {
id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt)
{
printf("我是子進(jìn)程, 我的pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1);
}
}
// 如果你的父進(jìn)程沒有事干,你還是用以前的方法
// 如果你的父進(jìn)程很忙,而且不退出,可以選擇信號的方法
while (1) {
sleep(1);
}
return 0;
}
文章來源:http://www.zghlxwxcb.cn/news/detail-489132.html
由于UNIX 的歷史原因,要想不產(chǎn)生僵尸進(jìn)程還有另外一種辦法:父進(jìn)程調(diào) 用sigaction將SIGCHLD的處理動作置為SIG_IGN,這樣fork出來的子進(jìn)程在終止時會自動清理掉,不 會產(chǎn)生僵尸進(jìn)程,也不會通知父進(jìn)程。系統(tǒng)默認(rèn)的忽略動作和用戶用sigaction函數(shù)自定義的忽略 通常是沒有區(qū)別的,但這是一個特例。此方法對于Linux可用,但不保證在其它UNIX系統(tǒng)上都可用。文章來源地址http://www.zghlxwxcb.cn/news/detail-489132.html
到了這里,關(guān)于Linux進(jìn)程信號 | 信號處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!