文章目錄
- 一:信號簡介
- (1)CTRL+C
- (2)注意
- (3)信號串列
- (4)處理信號-signal函式
- 二:產生信號
- (1)通過按鍵產生信號-Core Dump
- (2)呼叫系統函式向行程發送信號
- A:kill
- B:raise
- C:abort
- (3)由軟體條件產生信號
- (4)硬體例外產生信號
- 總結:
- 三:阻塞信號
- (1)信號相關術語
- (2)信號在內核中的表示
- 四:信號集操作函式
- (1)sigset_t
- (2)信號集操作函式
- 五:信號的捕捉程序
- (1)用戶態和內核態
- (2)用戶態和內核態的切換
- (3)內核是如何實作信號的捕捉
- (4)sigaction
- 六:其他概念
- (1)可重入函式
- (2)volatile關鍵字
- A:背景知識
- B:產生的問題
- C:volatile關鍵字
- (3)SIGHLD信號
- A:復習僵尸行程
- B:清理僵尸狀態的新方法-SIGCHLD
一:信號簡介
(1)CTRL+C
我們都知道,按下ctrl+c后將會結束當前運行的一個前臺行程,當我們按下ctrl+c后,會被作業系統獲取,而這個動作或者這個快捷鍵組合已經被賦予了結束行程的含義(就像你從小就被告知看見紅燈就要停下來),它被解釋為信號,發送給目標前臺行程,而行程由于收到了信號所以退出
其中2號信號KILLINT發揮的作用就是ctrl+c,如下可以一個行程不斷地向螢屏列印文字,然后使用2號信號結束

(2)注意
ctrl+c產生的信號只能發送給前臺行程,一個命令后面加入&可以將行程放在后臺運行,這樣shell就不必等待行程結束就可以接受新的命令從而啟動新的行程- shell可以同時運行一個前臺行程和多個后臺行程,但是只有前臺行程才能接收到類似于
ctrl+c這樣的控制鍵產生的信號 - 前臺行程在運行程序中用戶隨時可能按下
ctrl+c而產生一個信號,也就是說該行程的用戶空間代碼執行到任何地方都有可能受到SIGINT信號而終止,所以信號相對于行程的控制流是異步的
(3)信號串列
使用kill -l命令可以查看系統定義的信號串列

每一個信號都有一個編號和一個宏定義名稱,編號34及以上的屬于實時信號,我們在這里只討論的是1-31的非實時信號
(4)處理信號-signal函式
行程獲得一個信號后,有如下三種處理方式
- 忽略此信號(
SIGIGN) - 執行該信號的默認動作(
SIG_DFL) - 提供一個信號處理函式,要求內核在處理該信號時切換到用戶態執行這個處理函式,這種方式稱為捕捉一個信號
signal函式是用來讓行程處理信號的,它的函式原型是(大家先不要管它的回傳值)
sighandler_t signal(int signum,sighandler_t handler)
其中第一個引數就是我們要處理的信號(可以輸入編號或者名稱);第二個引數就是我們處理的方式,處理的方式分別就對應上面的三條
舉例1:ctrl+c對應的是2號信號,如果使用signal函式,第一個引數設定為2,表示處理2號信號,第二個引數設定為SIG_IGN,然后在signal函式后面回圈列印文字
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main()
{
signal(2,SIG_IGN);//忽略此信號
while(1)
{
printf("I'm running now!\n");
sleep(1);
}
return 0;
}
由于我們動作是忽略2號信號,所以大家可以發現即便我瘋狂的按下ctrl+c,也無法終端行程(按下ctrl+\結束行程)

舉例2: 如果將第二個引數設定為SIG_DFL呢,那么它就表示默認,這個就相當于不寫這個函式一樣,所以按下Ctrl+c就能正常終止,這里就不演示了
舉例3: 除了前兩種處理方式外,我們說過第三種方式就是用戶自定義一個函式,稱這種方式為捕捉到一個信號,當捕捉到信號后,行程就會執行你所定義的函式的內容(所以第二個引數是一個函式指標)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sin : %d\n",sig);
}
int main()
{
signal(2,handler);//一旦捕捉到2號信號,將會執行handler函式內的操作
while(1)
{
printf("I Am runnng now...\n");
sleep(1);
}
return 0;
}
效果如下,大家可以看到,一旦信號被捕捉后,將會執行所指定函式內的內容,所以這里的2號信號被捕捉后也無法結束行程了,其中9號行程無法被捕捉,因為9號行程一旦被捕捉,作業系統就無法終止一些惡意行程了

在這里我們就可以具體說一說這個函式的形參,回傳值了,因為它的真正的原型是這樣子的,是不是感覺很懵?
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
我們首先要明白的就是這里typedef的作用,typedef的作用實則就是定義一個新型別,比如說typedef int Myint,這樣的話我定義整形就可以用Myint a=10了,那么在這里先不要看typedef,先看后面的,void(*sighandler)(int)這個明顯是一個函式指標,sighandler指向一個一個形參為int回傳值為void的函式,當加上typedef后,sighandler_t就是一個新的型別,就可以像int一樣用它,只不過int宣告的是一個整形變數,而sighandler宣告的是一個函式指標,而且這個函式指標指向的函式接收一個整型引數并回傳一個void,
你可能會問,這樣的寫法有什么作用呢?其實如果不這樣寫,那么你看見的signal函式將會是下面的這樣子
void (*signal(int signum,void (*handler)(int)))(int);
是不是感覺更加不懂了,所以現在我們去剖析一下這個函式,想要弄清楚,你必須明白下面的兩個簡單的例子
void (*p)(int):這個肯定很簡單,自然是一個函式指標p,p指向的函式是一個形參為int,且回傳值為void的函式void (*fun())(int):這里,fun是一個函式,所以可以看成是fun這個函式執行完畢之后,它的回傳值是一個函式指標,指向了一個形參為int,回傳值為void的函式
了解完畢后,就可以結合上面的例子解釋一下它的運行程序了

二:產生信號
為了方便后面的講解,我們先要了解一下如何捕捉信號,使用signal函式可以捕捉到發送給這個行程的信號,該函式第一個形參設定要捕捉的信號,第二個形參實則是一個函式指標,指向了一個函式,表明當捕捉到要捕捉的信號時,所要進行的操作
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sin : %d\n",sig);
}
int main()
{
signal(2,handler);//一旦捕捉到2號信號,將會執行handler函式內的操作
while(1)
{
printf("I Am runnng now...\n");
sleep(1);
}
return 0;
}
效果如下,大家可以看到,一旦信號被捕捉后,將會執行所指定函式內的內容,所以這里的2號信號被捕捉后也無法結束行程了,其中9號行程無法被捕捉,因為9號行程一旦被捕捉,作業系統就無法終止一些惡意行程了

(1)通過按鍵產生信號-Core Dump
通過按鍵產生信號,這里就不多強調了,本節重點介紹一下Core Dump
Core Dump是什么?在VS中如果我們寫上這樣的代碼,那肯定會報出相應的錯誤的
int main()
{
int arr[10]={0};
for(int i=0;i<=100;i++)
{
arr[i]=i;
}
printf("運行到了這里\n");
}
但是在Linux中不是圖形化界面,如果要給出這樣的報錯資訊的話,就需要到Core Dump,一旦程式例外終止后,就會在磁盤上轉存一份以core開頭的檔案,后面跟上的數字是此行程的pid,
如下,這個行程運行時出現了發生了段錯誤

但是這里沒有相應的檔案,是因為core file size被設定為了0(使用ulimit -a查看)

可以使用ulimit -c設定core的大小

再次運行后,出現了相應的檔案

這個檔案就記錄了出錯的資訊,有了這樣的檔案,使用gdb除錯的時候,鍵入core-file 【那個檔案名】后,就可以列出十分詳細的錯誤資訊

(2)呼叫系統函式向行程發送信號
A:kill
我們使用到的最多也就是kill 命令了,kill命令是呼叫kill函式實作的,kill函式可以給一個指定的行程發送指定的信號
#include <signal.h>
int kill(pid_t pid,int signo);
B:raise
#include <signal.h>
int raise(int signo);
raise函式可以給當前行程發送指定的信號
int main(int argc,char* argv[])
{
if(argc==2)//保證傳入引數正確
{
raise(atoi(argv[1]));//將信號值傳入
}
while(1)
{
printf("I Am runnng now...\n");
sleep(1);
}
return 0;
}

C:abort
abort函式使行程接收到信號而例外終止,和exit函式一樣,abort函式總能被呼叫成功
#include <stdlib.h>
void abort(void);
abort對應的信號就是SIGABRT,編號是6
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int sig)
{
printf("catch a sin : %d\n",sig);
}
int main(int argc,char* argv[])
{
signal(6,handler);//捕捉6號信號
abort();//例外終止
while(1)
{
printf("I Am runnng now...\n");
sleep(1);
}
return 0;
}

(3)由軟體條件產生信號
前面咋們在講管道的特性四時說到過:如果讀端關閉檔案描述符,那么寫端就會被作業系統終結掉,因為作業系統發現此時的寫端是一個無用,浪費資源的行程,并且發送的型號是SIGPIPE,這其實就是一種由軟體條件產生的信號,這種型別信號不同于之前的硬體,系統呼叫介面那種方式,而是一種滿足了一定條件就發出信號,就像水滿了就溢位的感覺
今天主要介紹alarm函式和SIGALRM信號,它的作用是設定一個時間進行倒計時,當倒計時完成之后結束行程
比如下面的死回圈中,首先設定alarm(1),這表示行程進行1s就終止,然后再while回圈中不斷列印
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int cout=0;
void handler(int sig)
{
printf("catch a sin : %d\n",sig);
printf("cout:%d\n",cout);
exit(1);
}
int main()
{
alarm(1);//1s后結束
while(1)
{
cout++;
printf("cout:%d\n",cout);
}
return 0;
}
可以發現1s后,被SIGALRM信號給終止

(4)硬體例外產生信號
除0,空指標,野指標這些操作是不被允許的,所以作業系統一旦監測這樣的行程,就會發送回應的信號終止
比如說空指標
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int sig)
{
printf("catch a sin : %d\n",sig);
exit(1);
}
int main()
{
signal(11,handler);
sleep(1);
int* p=NULL;
*p=10;//錯誤
return 0;
}

其實在C++中使用到的捕獲例外,在系統層面,就是在處理信號
總結:
1:從上面的描述中大家可以看到,不管是哪一種產生信號的方式,背后總有一個角色在默默支撐著,信號的發送依靠的是作業系統,因為作業系統是行程的管理者
2:以core dump為例,大家可能也看到了,即便發生了段錯誤,但是后面的陳述句也被執行了,這表明行程在接受到信號時并不一定會立即處理,中間可能會存在一定的空檔期

3:作業系統給行程發送信號,其中發送二字顯得尤為專業,總讓感覺這一定是一個很復雜的程序,實則不然,與其說發送,倒不如用寫入二字更為貼切,每個行程都有其task_struct,所以可以在它里面搞一個位圖,32位(就像檔案系統中的inode和bitmap),一旦作業系統發送某個信號,就把某一位置為1,比如發送8號信號,就把它的第八位的0置1
三:阻塞信號
前面說過,作業系統發出信號之后,對于行程有可能不是立馬就處理的,所以如果不是立即處理,那么在這個空檔期間行程究竟對信號做了怎樣的處理呢?
(1)信號相關術語
為了表示清楚,這里總結關于信號的一些術語
- 遞達(Delivery):行程執行信號的處理動作
- 信號未決(Pending):信號從產生到遞達之間的狀態
- 阻塞(Block):行程可以選擇對信號進行阻塞,被阻塞的信號產生時將保持在未決狀態,知道行程解除對此信號的阻塞,才會執行遞達動作
需要注意區分阻塞和忽略,遞達有三種可選動作——忽略,執行默認,自定義捕捉,所以忽略是也就是遞達了,但是阻塞是保持在了未決
(2)信號在內核中的表示
我們一切的敘述都是圍繞行程來展開的,管理行程對應的資料結構式task_struct,而task_struct中又會涉及到各種各樣的結構(比如之前的files struct),
前面說過作業系統對行程發送信號的動作實際應該表述為寫入,其實每個信號都有兩個標志位分別表示阻塞和未決,還有一個函式指標來表示處理的動作,如下

大家可以看到,從1號信號(SIGHUP)開始,每個信號都對應block位圖,pending位圖和handler陣列(它是一個函式指標陣列,每個函式指標指向一個函式,表示處理的動作)的一位或下標,一旦信號產生,作業系統會在task_struct中設定該信號的未決狀態,也就是說如果作業系統給某個行程發送了3號信號,那么就會把這個行程的task_struct中的pending位圖中的第三個位置為1,此時信號處于未決狀態還沒有遞達,一旦信號遞達,作業系統就會將該標志位置為0,而block位圖中,一旦把第n位設定為了1,表示n號信號被阻塞,需要注意的是無論這個信號是否產生,它都可以被阻塞,
SIGHUP信號產生過,也未被阻塞,所以當此信號遞達時將會執行默認動作SIGINT信號產生過,但是正在被阻塞,所以暫時無法遞達,它的處理動作是忽略,但是在沒有接觸阻塞之前是不能忽略這個信號的,因為行程仍然有機會改變處理動作再接觸阻塞SIGQUIT信號沒有產生過,一旦產生就會被阻塞,它的處理動作是用戶自定義的一個函式,
四:信號集操作函式
(1)sigset_t
前面說過,未決和阻塞分別用位圖來表示,于是我們把保存位圖這樣的資料型別稱為sigset_t,sigset_t稱為信號集,于是他們分別稱為阻塞信號集和未決信號集
sigset_t這種型別可以表示每個信號的有效和無效的狀態(阻塞信號集的有效和無效的含義是該信號是否被阻塞,未決信號集則是該信號是否處于未決狀態),其中阻塞信號集也叫做當前行程的信號屏蔽字(SignaL Mask)
(2)信號集操作函式
sigset既然是一個保存位圖的資料型別,那么是否直接修改它對應資料的位元位就能達到屏蔽信號,產生信號的目的呢?答案是可以的,但是由于這個型別內部如何存盤這些位圖要依賴于系統實作,簡單來說不同平臺的存盤方式是不一樣的,所以我們不能直接操作位元位,我們只能呼叫一下函式來操作sigset_t變數
(注意以下函式僅在操作變數,它并沒有深入到內核中改變對應的位圖,就像ftok函式生成key的作用一樣)
#include <signal.h>
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
//注意,使用sigset_t型別的變數前,一定使用他們其中的一個做初始化,
//讓信號集處于一種確定的狀態
int sigaddset(sigset_t* set,int signo)
int sigdelset(sigset_t* set,int signo)
int sigismember(const sigset_t* set,int signo)
int sigpending(sigset_t* set);
sigemptyset是初始化set所指向的信號集,所有位元位清零sigfillset是初始化set所指向的信號集,所有位元位置為1sigaddset是對set所指信號集添加信號signosigdelset是從set所指信號集中洗掉信號signosigismember用于判斷一個信號集的有效信號是否包含signo這個信號sigpending用于讀取當前行程的未決信號集
所以結合上述例子,我們就可以寫下面這樣一段小程式,查看一下未決信號集,
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void print_pending(sigset_t* pending)
{
int i=1;
for(i=1;i<=31;i++)
{
if(sigismember(pending,i))
{
printf("1");//只要i信號存在,就列印1
}
else
{
printf("0");//不存在這個信號就列印0
}
}
printf("\n");
}
int main()
{
sigset_t pending;//定義信號集變數
while(1)
{
sigemptyset(&pending);//初始化信號集
sigpending(&pending);//讀取未決信號集,傳入pending
print_pending(&pending);//定義一個函式,列印未決信號集
sleep(1);
}
}
運行效果如下,由于沒有傳入信號,所以未決信號集全部為0

那么下一個問題就是用戶如何去控制阻塞信號集(信號屏蔽字),為什么不控制未決信號集呢?因為產生信號無非就是咋們上面說過的4種方式之一嘛,
前面說過你不能直接操作位元位,所以我們要使用到一個函式:sigprocmask
#include <signal.h>
int sigprocmask(int how,const sigset_t* set,sigset_t* oset);
引數how指示了如何更改,當選定引數how后,set的意思如下
| how | set |
|---|---|
| SIG_BLOCK | set包含了我們希望添加到當前信號屏蔽字的信號 |
| SIG_UNBLOCK | set包含了我們希望添加到當前信號屏蔽字中解除阻塞的信號 |
| SIG_SETMASK(常用) | 設定當前信號屏蔽字為set所指的值 |
引數oset如果不設定為NULL,由于此函式會更改當前的信號屏蔽字,所以會在更改之前將此時的信號屏蔽字備份一份到oset所指的變數中
所以現在在上面的例子的基礎上,添加block和oblock變數,再使用sigaddset函式添加2號信號11號信號,此時如果發送2號信號和11號信號,就會將信號添加到阻塞信號集也就是block中,然后使用sigprocmask將屏蔽關鍵字設定為block的值,并將原先的屏蔽關鍵字備份到oblock中,
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void print_pending(sigset_t* pending)
{
int i=1;
for(i=1;i<=31;i++)
{
if(sigismember(pending,i))
{
printf("1");//只要i信號存在,就列印1
}
else
{
printf("0");//不存在這個信號就列印0
}
}
printf("\n");
}
int main()
{
sigset_t pending;//定義信號集變數
sigset_t block,oblock;//定義阻塞信號集變數
sigemptyset(&block);
sigemptyset(&oblock);//初始化阻塞信號集
sigaddset(&block,2);//將2號信號添加的信號集
sigaddset(&block,11);
sigprocmask(SIG_SETMASK,&block,&oblock);//設定屏蔽關鍵字
while(1)
{
sigemptyset(&pending);//初始化信號集
sigpending(&pending);//讀取未決信號集,傳入pending
print_pending(&pending);//定義一個函式,列印未決信號集
sleep(1);
}
}
效果如下,大家可以發現當我發送2號信號時(之前發送2號信號行程立即終止,是因為沒有阻塞它)而現在發送2號信號大家可以看到未決信號集對應的第2號位置變為了1,行程也沒有終止,因為此時在阻塞狀態,沒有遞達

可以發現由于信號一直被阻塞,沒有遞達,所以本該結束行程的命令也將“失效”,要使其生效,就必須接觸阻塞狀態,所以再次修改上面的案例,由于之前用oblock備份了屏蔽關鍵字(都是0),所以在10s之后,用該函式再次設定屏蔽關鍵字為oblock,以此接觸阻塞,由于接觸阻塞后,行程會終止,所以這里用signal捕捉信號,以免結束行程,便于觀察
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
printf("獲得信號:%d\n",sig);
}
void print_pending(sigset_t* pending)
{
int i=1;
for(i=1;i<=31;i++)
{
if(sigismember(pending,i))
{
printf("1");//只要i信號存在,就列印1
}
else
{
printf("0");//不存在這個信號就列印0
}
}
printf("\n");
}
int main()
{
signal(2,handler);//捕捉
sigset_t pending;//定義信號集變數
sigset_t block,oblock;//定義阻塞信號集變數
sigemptyset(&block);
sigemptyset(&oblock);//初始化阻塞信號集
sigaddset(&block,2);//將2號信號添加的信號集
sigprocmask(SIG_SETMASK,&block,&oblock);//設定屏蔽關鍵字
int cout=0;
while(1)
{
sigemptyset(&pending);//初始化信號集
sigpending(&pending);//讀取未決信號集,傳入pending
print_pending(&pending);//定義一個函式,列印未決信號集
sleep(1);
cout++;
if(cout==10)//10s后解除阻塞
{
printf("解除阻塞\n");
sigprocmask(SIG_SETMASK,&oblock,NULL);
}
}
}
效果如下

五:信號的捕捉程序
(1)用戶態和內核態
我們說過,每個Linux行程有4GB的地址空間

其中0-3G是用戶空間,由用戶頁表負責映射到物理記憶體,剩余的1G存放的是內核及其維護的資料,由內核頁表負責映射,
一個非常簡單的C語言程式如下
#include <stdio.h>
int main()
{
int a=10;
printf("%d\n",a);
return 0;
}
這段程式中有一個printf函式作用是向螢屏列印內容,根據對作業系統理解,我們知道printf底層肯定是呼叫了系統呼叫介面write完成了對應的功能

也就是說這段程式中像int a=10這種陳述句是屬于行程的,也就是屬于用戶的,而執行printf的時候卻需要深入到內核完成,
內核態
當一個行程執行系統呼叫(比如上面的printf,本質是系統呼叫)而陷入內核代碼中運行時,我們就稱為該行程處于內核運行狀態,
用戶態
當行程執行用戶自己的代碼時(比如上面的int a=10),則稱其處于用戶狀態
用戶態核內核態下作業的程式有很多的差別,但是最主要的差別就在于其權限的不同,運行在內核態下的程式可以直接方位用戶態的代碼和資料(但是禁止這樣做)
所以這樣的一個簡單的程式反映的卻是行程在用戶態和內核態中的不斷切換的程序
(2)用戶態和內核態的切換
當在系統中執行一個程式時,大部分時間都是運行在用戶態下的,在需要作業系統幫助完成一些用戶態沒有能力完成的操作時就會切換到內核態(比如printf函式)
用戶態切換到內核態主要有三種方式
- 系統呼叫:除了前面的printf外,我們之前說過的fork()函式也是一個典型的例子
- 例外:當CPU在執行運行在用戶態下的程式時,,發生了一些沒有預知的例外,這時會觸發由當前運行行程切換到處理此例外的內核相關行程中
- 外圍設備的終端:這個其實就是咋們上面說過的信號
(3)內核是如何實作信號的捕捉
信號是什么時候處理,以及什么時候被捕捉的呢?是在從內核態切換到用戶態的時候

- 首先在用戶態的程式由于中斷,系統呼叫或者是例外將會進入內核態
- 進入內核態,處理完需求,準備回傳用戶態
- 在回傳用戶態之前檢測信號,如果沒有信號繼續回傳;如果有信號,但是被阻塞了,由于無法處理,所以也回傳;最后就是有信號,但是沒有被阻塞,那么信號就應該遞達,當遞達后如果是默認動作,那么一般就是終止行程,而此時在內核態它是有權利終止行程的,如果是忽略,那么不要忘記將pending對應位置置為0,最后一種就是自定義的函式了
- 如果是自定義函式,將會再次回傳用戶態,處理完成之后,借助相關系統呼叫再次進入內核
- 最后再通過回傳到上次中斷的地方繼續向下執行
所以上述程序可以用下面的這一張圖記憶

如果信號的處理動作是用戶自定義函式,所以內核決定回傳用戶態時就會去指定自定義函式,而這個自定義函式和原先的main函式使用的是不同的堆疊空間,所以他們之間不存在呼叫和被呼叫的關系,是兩個獨立的控制流程,而一個行程可以有多個控制流程——執行緒
至此我們可以解釋上面Core Dump中為什么下標已經越界了,但是后面的代碼還是可以輸出
int main()
{
int arr[10]={0};
for(int i=0;i<=100;i++)
{
arr[i]=i;
}
printf("運行到了這里\n");
}

這是因為信號是在從內核態切換到用戶態的時候處理的,所以上面的for回圈的代碼始終運行在用戶態,當涉及到printf的時候,將會陷入內核態完成呼叫,然后回傳時檢測信號,此時檢測到了11號信號,所以進行處理
(4)sigaction
sigaction這個函式和signal作用基本一致,但是前者功能要比后者豐富一點
#include <signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction* oact);
其中act和oact分貝是struct sigaction的結構體指標,act表示捕捉后你做的操作,oact是備份一下捕捉前的操作
其中struct sigaction結構體如下
struct sigaction
{
void (*sa_handler)(int);//捕捉后你要做的操作
void (*sa_sigaction)(int,siginfo*,void*);//處理實時信號
sigset_t sa_mask;//設定為0
int sa_flags;//設定為0
void (*sa_restorer)(void)//處理實時信號
}
- sa_mask的作用:大家可以想象這樣一個場景,上節展示的捕捉程序中,如果處理完2號信號的自定義捕捉動作時,再來了一個2號信號怎么辦,這樣就會導致無法從內核態回傳至用戶態,會造成很嚴重的后果,于是sa_mask也是一個信號集,表示當你在處理某個信號時想要把哪些信號加入屏蔽,在默認狀態下,當前處理的信號是默認加入進去的,也就是作業系統可以處理很多信號的自定義捕捉動作,但是每一個信號的自定義捕捉動作一次只能處理一個
六:其他概念
(1)可重入函式
如下是一個不帶頭結點的單鏈表的頭插操作

- 上述程序是這樣的:main函式呼叫insert函式向一個鏈表head中插入節點node1,插入操作分為兩步,剛做完第一步的時候,因為硬體終端使行程切換到內核態,再次回傳用戶態時做信號檢測,由于是自定義用戶操作,所以切換到sighandler函式,而sinhanderl函式也呼叫了insert函式向同一個鏈表中插入了結點node2,插入操作的兩步都做完之后從sighandler回傳至內核態,接著再次回傳用戶態就從main函式呼叫的insert函式中繼續向下執行,完成了之前由于中斷而未完成的第二步,結果就是最終只有一個有效的結點插入鏈表中了,剩余的一個結點由于沒有指標指向,造成了記憶體泄漏
像上面這樣,iinsert函式被不同的控制流程呼叫,有可能在第一次呼叫還沒有回傳的時候就再次進入了該函式,我們稱之為重入,很明顯,insert函式是不可以被重入的,因為會造成混亂,所以像insert這樣的函式稱之為不可重入函式,
(2)volatile關鍵字
A:背景知識
由于記憶體的訪問速度遠不及CPU的處理速度,所以為了提高機器的整體性能,在硬體上引入了高速緩沖Cache,加速對記憶體的訪問,除了硬體級別的優化外,編譯器也通常會進行優化,比如說將記憶體變數緩沖到暫存器或調整指令順序充分利用CPU指令流水線
B:產生的問題
其實這樣的優化會產生一定的問題,因為訪問暫存器要比訪問記憶體單元快的多,所以編譯器一般都會作減少存取記憶體的優化,也就是讀取資料時更加傾向于讀取暫存器中的資料,這就有可能讀取到臟資料
結合前面信號所講過的相關知識可以做一個案例:
在下面的這段代碼中,首先定義一個全域變數flag并初始化為0,接著在main函式中捕捉2號信號,一旦發送2號信號就執行自定義的捕捉動作——將flag設定為1,signal函式下面是一個while回圈,在默認狀態下,while將會不斷檢測flag的值,如果此時編譯器不做任何優化,那么flag默認處于記憶體中,那么while將會不斷讀取記憶體中flag的值做判斷,
#include <stdio.h>
#include <signal.h>
int flag=0;
void handler(int sig)
{
flag=1;
printf("flag被設定為了1\n");
}
int main()
{
signal(2,handler);
while(!flag);
printf("程式運行到了這里\n");
}
效果如下,由于發送了2號信號,于是flag被設定為1,因此while回圈沒有成為死回圈,最后的一條陳述句也被成功列印了,這的確是我們預料的結果

上述編譯程序中編譯器其實沒有做優化,而gcc其實可以攜帶一定的優化選項,也就是-O選項,分別是-O0,-O1,-O2,-O3和-Os,所以這一次我們讓編譯器進行優化,因為前面我們說過編譯器一旦進行優化,將會把變數放在暫存器里面
于是效果如下,大家可以發現這里似乎產生了一定的矛盾:flag的值已經被設定為了1,按理說while回圈一旦對flag取反,肯定是不會產生死回圈的,但實際情況是它還在死回圈當中

C:volatile關鍵字
其實上面矛盾的現象反映也正是volatile的關鍵字,volatile將保持記憶體的關鍵字,一個變數一旦被volatile修飾,那么系統總是會從記憶體中讀取資料,而不是從暫存器 ,因此剛才那個例子中,由于編譯器做了一定的優化,將flag放置到了暫存器中,同時因為這是兩個不同的執行流程,而handler函式修改時修改的仍然是記憶體中的那個資料,這樣就導致記憶體中的flag已經為1了,但是while回圈讀取的確實暫存器中的資料,而暫存器里的flag值仍然是0,因此會造成死回圈
所以既然編譯器已經做了優化,而我們又要達到正常的效果,就可以使用volatile修飾flag,這樣while讀取flag時將會被強制從記憶體中讀取
#include <stdio.h>
#include <signal.h>
volatile int flag=0;
void handler(int sig)
{
flag=1;
printf("flag被設定為了1\n");
}
int main()
{
signal(2,handler);
while(!flag);
printf("程式運行到了這里\n");
}

(3)SIGHLD信號
A:復習僵尸行程
在linux行程那一節我們提及了僵尸行程這個概念,僵尸行程就是子行程已經退出了,父行程還在運行當中,由于父行程沒有讀取到子行程的狀態,所以子行程就會進入僵尸狀態
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// printf("還沒執行fork函式時的本行程為:%d\n",getpid());
pid_t ret=fork();//其回傳值型別是pid_t型的
sleep(1);
if(ret>0)//父行程回傳的是子行程ID
{
while(1)
{
printf("----------------------------------------------------\n");
printf("父行程一直在運行\n");
sleep(1);
}
}
else if(ret==0)//子行程fork回傳是0
{
int count=0;
while(count<=10)
{
printf("子行程已經運行了%d秒\n",count+=1);
sleep(1);
}
exit(0);//讓子行程運行10s
}
else
printf("行程創建失敗\n");
sleep(1);
return 0;
}

B:清理僵尸狀態的新方法-SIGCHLD
在Linux行程控制那一節我們講了可以使用wait和waitpid清理僵尸行程,而且父行程可以以阻塞或非阻塞的方式等待子行程結束,但是這兩種方式都有一個很大的缺點就是:父行程除了忙于自己的作業外,還要時不時的關心子行程怎么樣了,尤其在子行程的回傳狀態不是那么重要的情況下,這樣的操作就顯得有點多余了
其實,子行程在終止時會給父行程發送SIGCHLD信號,該信號的默認動作是忽略,當然父行程也可以自定義SIGCHLD信號的處理函式,這樣父行程就只需要關心自己的作業,當子行程終止時,會向父行程發送信號,而父行程則利用信號捕捉自動處理子行程
于是上面的案例中在父行程中加入
signal(SIGCHLD,SIG_IGN);//忽略

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275896.html
標籤:其他
