個人主頁:敲上癮-CSDN博客
個人專欄:Linux學習、游戲、數據結構、c語言基礎、c++學習、算法
目錄
一、認識信號
二、信號的產生
1.鍵盤輸入
2.系統調用
3.系統指令
4.硬件異常
5.軟件條件
三、信號的保存
1.block
2.pending
3.handler
四、信號的捕捉
五、核心轉儲
六、從不可重入函數
七、特殊信號
9號和19號信號
SIGCHIL信號
一、認識信號
????????什么是信號?信號是一種異步事件通知機制,類似于生活中的紅綠燈、鬧鐘、電話鈴等,用于中斷當前任務并提醒處理新事件。
????????注:異步就是「發起一個任務后不用干等著,先做別的事,等結果好了再回來處理」
類比生活中的信號,我們來理解一下進程中信號相關的基本結論如下:
- 進程在信號沒有產生時就知道各個信號該如何處理了。
- 信號產生后不必立即處理,可以稍等一會,合適的時候處理。
- 進程內已經內置了對信號的識別和處理機制。
- 信號種類很多,產生信號的方式也很多。
信號的處理有這三種方式:
- 默認處理方法
- 自定義處理方法
- 忽略處理
二、信號的產生
在命令行中查找信號的相關信息,使用如下指令:
kill -l
我們可以得到這樣一張表:
注意:這里的信號個數并不是64個,如上表中并沒有32和33信號。
其中1~31為普通信號,34~64為實時信號,在這里我們只探討普通信號。?
1.鍵盤輸入
? ? ? ? 在我們運行程序時通常會用Ctrl+c來使程序退出,這其實是向前臺程序發送2號信號。除此之外還有Ctrl+\,表示發送3號信號,同樣是讓程序退出,2號信號與3號信號的區別將在下文核心轉儲部分詳細講解。
? ? ? ? Ctrl+z:發送20號信號,讓程序暫停。
這些就是通過鍵盤發送信號的一種方式,如何驗證呢?
我們可以使用以下函數:
signal函數用于改變信號的處理方法,即自定義信號處理方法。
signal聲明:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
- 參數signum:傳入一個信號編號或信號名稱。
- 參數handler:傳入自定義的信號處理方法,即一個返回類型為void*,參數為int類型的函數。
- 返回值:?返回?舊的信號處理函數(函數指針)
注:signal內部會將signum作為參數傳入handler函數。
測試代碼:?
void handler(int sig)
{cout<<"正在處理"<<sig<<"號信號"<<endl;
}
int main()
{ int cnt=0;signal(SIGINT,handler);while(true){cout<<"Run: "<<cnt++<<endl;sleep(1);}
}
注意:由于2號信號處理方法已被改變,Ctrl+c無法殺死程序,可以使用Ctrl+\。?
????????當有多個程序在運行時,會分為前臺和后臺程序,前臺程序只有一個,后臺程序可以有多個,鍵盤輸入的信息只能被前臺程序讀取。例如,上述代碼生成的可執行程序test,當我們運行test時它默認是前臺程序,ls,cd,mkdir等指令會失效。因為shell命令行程序已經切換到后臺了。
- ./test:放在前臺運行。
- ./test &:放在后臺運行。
切換前后臺程序的方法:
方法一:
- jobs:查看所有后臺任務。
- fg 任務號:特定的進程提到前臺。
方法二:
- Ctrl+z:暫停當前進程,然后自動把后臺提到前臺
- bg 任務號:把剛才暫停的任務恢復運行。
2.系統調用
除了鍵盤產生信號,我們還可以直接用kill、raise、abort這些接口向系統發信號。
使用方法如下(這里我們暫不對返回值進行討論):
kill函數聲明:
int kill(pid_t pid, int sig);
- 參數pid:傳入需要發信號的進程pid。
- 參數sig:傳入需要發送的信號編號。?
- 功能:向任意進程發送信號。
raise函數聲明:
int raise(int sig);
- 參數sig:傳入需要發送的信號編號。
- 功能:向自己發送信號。
abort函數聲明:
void abort(void);
- 功能:向自己發送6號信號。
我們同樣可以使用改變信號處理的方法來驗證。?這里就不展示。
3.系統指令
kill 信號編號 進程pid
使用kill指令向指定的進程發送指定的信號。?
4.硬件異常
????????發送信號方式還有硬件異常,比如引用空指針,除0等等這些非法操作最終是反應到了硬件上,然后產生信號。比如我們可以這樣做測試:
void sig_handle(int sig)
{cout<<"接收到信號:"<<sig<<endl;exit(1);
}
int main()
{for(int i=1;i<32;i++)signal(i,sig_handle);int a = 10;a /= 0;//int* p = nullptr;//*p = 10;return 0;
}
除0觸發8號信號,引用空指針觸發11號信號。
5.軟件條件
????????軟件條件觸發信號,比如alarm,?alarm函數是一個用于設置定時器的系統調用,主要作用是讓內核在指定的時間后向進程發送SIGALRM
信號。它的核心功能是提供一種簡單的超時機制或定時任務調度。
alarm聲明:
unsigned int alarm(unsigned int seconds);
- 參數
seconds:
是定時器倒計時時間(單位:秒)。若為?0
,表示取消之前設置的定時器。- 返回值:之前未完成的定時器剩余時間(秒)。例如:如果之前設置了 5 秒的定時器,3 秒后再次調用?
alarm(2)
,返回值為?2
(剩余時間),新定時器將在 2 秒后觸發。
三、信號的保存
????????在開篇就提到信號并不一定是產生后就馬上被處理的,所以需先將它保存下來。而信號又分為兩種狀態:信號未決,信號遞達。
- 信號未決:信號被保存但沒有被處理。
- 信號遞達:信號被處理。
進程可以阻塞信號,被阻塞的信號產生時會保持在未決狀態,直到解除阻塞才能被遞達。
注:阻塞和忽略是不同的,忽略是在遞達后的一種處理方式。
在程序中信號的相關信息會被保存在block、pending、handler這三張表中。
- block表:記錄的是信號的阻塞狀態。
- pending表:記錄的是未決情況。
- handler表:儲存的是信號的處理方法。
????????從上圖來看,每個信號只有?個bit的未決標志,?0即1,不記錄該信號產?了多少次,阻塞標志也是這樣表?的。因此,未決和阻塞標志可以?相同的數據類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表?每個信號的“有效”或“?效”狀態,在阻塞信號集中“有效”和“?效”的含義是該信號是否被阻塞,?在未決信號集中“有效”和“?效”的含義是該信號是否處于未決狀態。
????????阻塞信號集也叫作當前進程的“信號屏蔽字”。
1.block
關于信號集的處理函數有這些:
- int sigemptyset(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);
功能:
- sigemptyset:相當于初始化,使其中所有信號的對應bit清零,表示該信號集不包含任何無效信號。
- sigaddset:添加無效信號。
- sigdelset:刪除無效信號。
- sigismember:查看一個信號是否有效,返回0表示有效,返回1表示無效。
block表儲存的是信號的阻塞狀態,用的是位圖的原理,1表示阻塞,0表示未阻塞。
????????以上這些函數只是用來設置信號集,接下來使用函數sigprocmask把信號集設置到程序中,使其信號屏蔽字改變。
sigprocmask函數聲明如下:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
參數how:需要我們傳入一個可選參數,表示要做的操作,這個參數可以是:
mask表示的是程序當前的信號屏蔽字,這里我們最常用的是SIG_SETMASK選項。
參數set:把設置好的信號屏蔽字(類型為signal_t*)傳入。
參數oldset:這是一個輸出型參數,獲取到舊的信號屏蔽字。
測試代碼:
void handler(int sig)
{cout<<"正在處理"<<sig<<"號信號"<<endl;
}
int main()
{ signal(2,handler);sigset_t block,oblock;sigemptyset(&block);sigaddset(&block,SIGINT);//屏蔽2號信號sigprocmask(SIG_SETMASK,&block,&oblock);while(true){cout<<"hello linux"<<endl;sleep(1);}return 0;
}
2.pending
????????pending這張表用來標記信號是否處于未決狀態。函數sigpending可以獲取pending表。
聲明如下:
int sigpending(sigset_t *set);
參數set:這是一個輸出型參數,用來獲取到pending表的信息。
然后我們可以借助setismember來打印pending表的信息。?
測試代碼:
int main()
{sigset_t sig;sigpending(&sig);for(int i=31;i>=1;i--){//判斷i號信號是否未決if(sigismember(&sig,i))cout<<1;else cout<<0;}return 0;
}
注:一個信號在即將要被處理前會把pending表對應的bit位改為0,而不是在處理完后修改。
3.handler
????????handler表是一個函數指針數組,儲存了每一個信號的處理方式。SIG_DEL表示默認處理,SIG_IGN表示忽略處理,然后還可以使用函數signal設定自定義處理方法。
其中SIG_DEL,SIG_IGN可作為參數傳入signal函數中。
除了使用signal函數設置自定義處理方法外,還可以使用sigaction。
sigaction聲明如下:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
其中sigact是一個結構體類型,聲明如下:
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:自定義的信號處理函數。
- sa_mask:在處理該信號的過程中需要阻塞的信號。
????????其它成員變量用得很少這里就不再探討。所以與signal接口相比,sigaction并表示簡單的設置自定義處理方法,它還能做更復雜的處理。
關于sigaction的參數:
- 參數signum:需要設置的信號編號
- 參數act:傳入一個自定義的struct sigaction類型的地址
- 參數oldact:一個輸出型參數,獲取到舊的struct sigaction信息。
測試代碼:
void handler(int sig)
{cout << "收到信號" << sig << endl;while (true){sigset_t s;sigpending(&s);for (int i = 31; i >= 1; i--){if (sigismember(&s, i)) cout << '1';else cout << '0';}cout << endl;sleep(1);}
}
int main()
{struct sigaction act, oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 2);sigaddset(&act.sa_mask, 3);sigaction(SIGINT, &act, &oact);while (true){cout << "hello linux! my pid:" << getpid() << endl;sleep(1);}return 0;
}
測試結果:
四、信號的捕捉
????????接下來我們來學習信號的處理,在此之前最好對CPU中斷機制有所了解,可以通過下面這篇文章進行學習:操作系統的心臟節拍:CPU中斷如何驅動內核運轉?-CSDN博客
????????如上圖是系統處理自定義信號的流程圖,而默認處理和忽略處理就比較簡單,只到第3步。
????????注:無論你寫的程序是否有系統調用,是否觸發異常等 都有機會進入內核狀態,因為在CPU中還存在著時鐘中斷能多次使你的程序陷入內核。
我們可以把自定義信號處理中狀態的轉化抽象成這樣一個圖:
????????其中用戶態和內核態之間做了四次轉化,如圖紅圈部分,而pending表的檢查是在內核態內完成的。
五、核心轉儲
我們通過輸入以下指令可以看到信號相關的信息:
man 7 signal
如下Action這一欄的表示默認行為
標識符 | 全稱 | 含義 |
---|---|---|
Term | Terminate | 終止進程。進程會立即終止。 |
Core | Core Dump + Terminate | 生成核心轉儲文件并終止進程。進程終止時生成?core ?文件(用于調試)。 |
Ign | Ignore | 忽略信號。進程不會采取任何動作。 |
Cont | Continue | 恢復進程執行。如果進程被暫停(如?SIGSTOP ),則恢復運行。 |
Stop | Stop | 暫停進程。進程會被掛起,直到收到?SIGCONT ?信號。 |
????????之前我們說過Ctrl+c和Ctrl+\都是使進程退出,但并沒有講它們的區別,其實就是Ctrl+\會比Ctrl+c多產生一個core文件,它是把內核中核心數據轉儲到磁盤上,這個文件可能儲存到當前路徑,也有可能儲存到路徑:/var/lib/systemd/coredump中。
????????但在一般情況下是生成不了這個core文件的,因為云服務器出于?安全性、資源管理?和?合規性?的考慮,默認關閉了核心轉儲(Core Dump)。比如惡意用戶可能故意觸發程序崩潰,生成大量核心轉儲文件,耗盡磁盤空間,導致系統癱瘓。
通過以下指令可以看到關于core dump的信息:
ulimit -a
如下:
????????我們看到code file size為0,表明核心轉儲已經被關閉了,可以通過ulimit -c指令臨時打開,并設置大小。
比如:
ulimit -c 40960
debug:
core文件有什么作用呢?
? ? ? ? 我們讓程序生成core文件通常是用來查找bug的,使用gdb打開出bug的程序,然后輸入指令 core-file core后程序能跳轉到出問題的具體代碼的位置。
core dump標志位:
在使用waitpid回收子進程時,其中有一個輸出型參數,用來獲取?進程退出狀態。如下:
????????這里第8個比特位記錄的就是是否生成core文件,1表示生成core文件,0表示沒有生成。
六、從不可重入函數
????????在我們執行程序過程中,可能任務執行到一半就因接收到信號,而先去處理信號了。那么如果程序和信號處理的是同一個數據呢,會出現什么問題?
?????????像這樣會被兩個及以上的執行流同時調用而發生不可預料的結果的函數被稱為不可重入函數,需要警惕這樣的事情發生。而函數內部只有自己的臨時變量,這樣的函數是可重入的。
七、特殊信號
9號和19號信號
-
SIGKILL(9)?的默認行為是?立即終止進程。
-
SIGSTOP(19)?的默認行為是?強制暫停進程(進入停止狀態,直到收到SIGCONT)。
????????這兩個信號的默認行為是操作系統強制執行的,進程無法干預,也就是無法對它們進行阻塞、忽略、自定義處理方法等。
????????這是出于操作系統的?安全性和穩定性?考慮,試想一下如果所有信號都可以被阻塞、忽略或自定義處理方法。那么我們就可以做這么一個惡意程序,把所有信號都阻塞了,然后寫一個死循環,那么程序不就無法退出了嗎?還可以更狠一點,在循環內不斷申請內存空間。
所以這樣的設計可以防止惡意進程失控,為管理員提供終極控制權。
SIGCHIL信號
17號信號(SIGCHIL)是在子進程退出后向父進程發送的。
????????當我們知道這一點我們就可以自定義17號信號的處理方法,讓父進程對子進程的等待操作在信號處理里面完成,這樣父進程就不用去關心子進程的回收問題,從而實現異步功能。
代碼示例:
void handler(int sig)
{while(true){int n = waitpid(-1,nullptr,WNOHANG);if(n==0) break;else if(n<0){perror("waitpid");exit(1);}elsecout<<"wait success: "<<n<<endl;}
}
int main()
{signal(17,handler);for(int i=0;i<10;i++){sleep(1);int id=fork();if(id==0){cout<<"child process:"<<getpid()<<" exit "<<endl;sleep(1);exit(1);}}return 0;
}
????????我們回想一下操作系統為什么要在子進程退出后設計一個僵尸進程讓用戶主動回收呢?子進程退出后操作系統直接把它回收不好嗎?
? ? ? ? 其實這樣設計是很合理的,我們創建子進程不就是讓子進程異步去幫我們完成任務嘛,那么它完成得怎么樣我們總應該要知道,所以才有了僵尸進程來儲存任務的完成情況。而當我們并不關心子進程的任務完成情況時,那么是不是就用不著僵尸進程這種機制啊?
? ? ? ? 答案是:是的!所以操作系統也為我們設計了一種不生用成僵尸進程的方法。
????????只需要把17號信號的處理方法設置為忽略處理,即SIG_IGN(上文handler部分已講解),這樣操作系統就不會給我們生成僵尸進程。
????????細心的讀者可能會發現,17號信號的默認行為就是Ign(忽略)嗎?在信號信息表的Action這一欄可以找到。
????????要注意用戶不做任何自定義信號處理時,所有信號都是默認處理方式(即SIG_DFL),而17號的默認行為是Ign而已。和忽略處理(即SIG_IGN)是不同的,是否忽略必須讓用戶自己指明。
非常感謝您能耐心讀完這篇文章。倘若您從中有所收獲,還望多多支持呀!