[Linux 高并发服务器] 信号
[Linux 高并發服務器] 信號
文章概述
??途WC++項目課:Linux高并發服務器的個人筆記,記錄了信號相關的知識點。
作者信息
NEFU 2020級 zsl
ID:fishingrod/魚竿釣魚干
Email:851892190@qq.com
歡迎各位引用此博客,引用時在顯眼位置放置原文鏈接和作者基本信息
參考資料
感謝前輩們留下的優秀資料,從中學到很多,冒昧引用,如有冒犯可以私信或者在評論區下方指出
| ??途WC++項目課:Linux高并發服務器 | ??途W | 貫穿全文,作為筆記的基礎 |
| 硬件中斷和軟件中斷的區別 | dela_ | 用于信號概念補充說明 |
| 異步和同步的區別 | 菠蘿 | 用于信號概念的補充 |
| linux下不產生core文件的原因 | Small-K | 處理不產生core文件的問題 |
| 10張圖讓你徹底理解回調函數 | 碼農的荒島求生 | signal信號捕捉函數,對回調函數作補充說明 |
正文部分
信號的概念
信號是 Linux 進程間通信的最古老的方式之一,是事件發生時對進程的通知機制,有時也稱之為軟件中斷,它是在軟件層次上對中斷機制的一種模擬,是一種異步通信的方式。信號可以導致一個正在運行的進程被另一個正在運行的異步進程中斷,轉而處理某一個突發事件。
補充:
硬中斷和軟中斷區別
異步和同步的區別
引發信號的各類事件
發往進程的諸多信號,通常都是源于內核。引發內核為進程產生信號的各類事件如下:
信號的目的
信號的特點
查看系統定義的信號列表
使用kill -l命令查看系統定義的信號列表
archlinux上的比較少
ubuntu上的比較多(下面以此為例子)
前31個為常規信號,后31個為實時信號,沒錯只有62個,沒有32,33兩個信號
下面這些列表來源于??偷恼n程
表格中標紅的需要重點了解,同時我們通過表格也可以知道信號有四個要素:編號,名稱,事件,默認動作
信號相關的更多信息
我們可以通過man 7 signal來了解信號的相關信息
信號的 5 種默認處理動作
默認處理動作即,信號一定會執行下面動作中的某幾個
1.Term 終止進程
2. Ign 當前進程忽略掉這個信號
3. Core 終止進程,并生成一個Core文件,用于記錄終止的原因
4. Stop 暫停當前進程
5. Cont 繼續執行當前被暫停的進程
對于Core文件,我們以下面這個程序為例子
#include <stdio.h> #include <string.h>int main() {char * buf;strcpy(buf, "hello");return 0; }明顯的,程序會發生訪問野內存的錯誤
我對其編譯運行
發現并沒有產生core文件,我們使用ulimit -a進行查看
發現core file size是unlimited狀態,按道理是生成了core文件的,但是為什么沒有呢?
通過這篇博客第三條提示,發現生成的core文件貌似被腳本文件刪除了。重新配置vim /proc/sys/kernel/core_pattern設置core文件生成在程序目錄下
成功生成core文件
接下來使用gdb調試來查看core
可以發現,程序發生了段錯誤,信號是SIGSEGV 查詢上面的表格可以知道進程進行了無效的進程訪問
信號的幾種狀態:
產生
未決:信號產生到遞達之間的狀態
遞達:實際執行信號的處理動作
SIGKILL 和 SIGSTOP 信號不能被捕捉、阻塞或者忽略,只能執行默認動作。
信號相關的函數
kill、raise、abort函數
/* #include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);- 功能:給任何的進程或者進程組pid, 發送任何的信號 sig- 參數:- pid :> 0 : 將信號發送給指定的進程= 0 : 將信號發送給當前的進程組= -1 : 將信號發送給每一個有權限接收這個信號的進程< -1 : 這個pid=某個進程組的ID取反 (-12345)- sig : 需要發送的信號的編號或者是宏值,0表示不發送任何信號kill(getppid(), 9);kill(getpid(), 9);int raise(int sig);- 功能:給當前進程發送信號- 參數:- sig : 要發送的信號- 返回值:- 成功 0- 失敗 非0kill(getpid(), sig); void abort(void);- 功能: 發送SIGABRT信號給當前的進程,殺死當前進程kill(getpid(), SIGABRT); */#include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h>int main() {pid_t pid = fork();if(pid == 0) {// 子進程int i = 0;for(i = 0; i < 5; i++) {printf("child process\n");sleep(1);}} else if(pid > 0) {// 父進程printf("parent process\n");sleep(2);printf("kill child process now\n");kill(pid, SIGINT);}return 0; }tip:發送信號最好使用宏名,因為不同系統架構的信號編號不一定相同。
alarm函數
/*#include <unistd.h>unsigned int alarm(unsigned int seconds);- 功能:設置定時器(鬧鐘)。函數調用,開始倒計時,當倒計時為0的時候,函數會給當前的進程發送一個信號:SIGALARM- 參數:seconds: 倒計時的時長,單位:秒。如果參數為0,定時器無效(不進行倒計時,不發信號)。取消一個定時器,通過alarm(0)。- 返回值:- 之前沒有定時器,返回0- 之前有定時器,返回之前的定時器剩余的時間- SIGALARM :默認終止當前的進程,每一個進程都有且只有唯一的一個定時器。alarm(10); -> 返回0過了1秒alarm(5); -> 返回9alarm(100) -> 該函數是不阻塞的 */#include <stdio.h> #include <unistd.h>int main() {int seconds = alarm(5);printf("seconds = %d\n", seconds); // 0sleep(2);seconds = alarm(2); // 不阻塞printf("seconds = %d\n", seconds); // 3while(1) {}return 0; } // 1秒鐘電腦能數多少個數? #include <stdio.h> #include <unistd.h>/*實際的時間 = 內核時間 + 用戶時間 + 消耗的時間(內核用戶轉換時間)進行文件IO操作的時候比較浪費時間定時器,與進程的狀態無關(自然定時法)。無論進程處于什么狀態,alarm都會計時。 */int main() { alarm(1);int i = 0;while(1) {printf("%i\n", i++);}return 0; }setitimer 定時器函數
相比alarm,setitimer可以實現周期性定時并且時間更精細
/*#include <sys/time.h>int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);- 功能:設置定時器(鬧鐘)??梢蕴娲鷄larm函數。精度微妙us,可以實現周期性定時- 參數:- which : 定時器以什么時間計時ITIMER_REAL: 真實時間,時間到達,發送 SIGALRM 常用ITIMER_VIRTUAL: 用戶時間,時間到達,發送 SIGVTALRMITIMER_PROF: 以該進程在用戶態和內核態下所消耗的時間來計算,時間到達,發送 SIGPROF- new_value: 設置定時器的屬性struct itimerval { // 定時器的結構體struct timeval it_interval; // 每個階段的時間,間隔時間struct timeval it_value; // 延遲多長時間執行定時器};struct timeval { // 時間的結構體time_t tv_sec; // 秒數 suseconds_t tv_usec; // 微秒 };過10秒后,每個2秒定時一次- old_value :記錄上一次的定時的時間參數,一般不使用,指定NULL- 返回值:成功 0失敗 -1 并設置錯誤號 */#include <sys/time.h> #include <stdio.h> #include <stdlib.h>// 過3秒以后,每隔2秒鐘定時一次 int main() {struct itimerval new_value;// 設置間隔的時間new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 設置延遲的時間,3秒之后開始第一次定時new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定時器開始了...\n");if(ret == -1) {perror("setitimer");exit(0);}getchar();return 0; }signal信號捕捉函數
/*#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);- 功能:設置某個信號的捕捉行為- 參數:- signum: 要捕捉的信號- handler: 捕捉到信號要如何處理- SIG_IGN : 忽略信號- SIG_DFL : 使用信號默認的行為- 回調函數 : 這個函數是內核調用,程序員只負責寫,捕捉到信號后如何去處理信號?;卣{函數:- 需要程序員實現,提前準備好的,函數的類型根據實際需求,看函數指針的定義- 不是程序員調用,而是當信號產生,由內核調用- 函數指針是實現回調的手段,函數實現之后,將函數名放到函數指針的位置就可以了。- 返回值:成功,返回上一次注冊的信號處理函數的地址。第一次調用返回NULL失敗,返回SIG_ERR,設置錯誤號SIGKILL SIGSTOP不能被捕捉,不能被忽略。 */#include <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h>void myalarm(int num) {printf("捕捉到了信號的編號是:%d\n", num);printf("xxxxxxx\n"); }// 過3秒以后,每隔2秒鐘定時一次 int main() {// 注冊信號捕捉// signal(SIGALRM, SIG_IGN);// signal(SIGALRM, SIG_DFL);// void (*sighandler_t)(int); 函數指針,int類型的參數表示捕捉到的信號的值。signal(SIGALRM, myalarm);struct itimerval new_value;// 設置間隔的時間new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 設置延遲的時間,3秒之后開始第一次定時new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定時器開始了...\n");if(ret == -1) {perror("setitimer");exit(0);}getchar();return 0; }tip:捕捉信號之前要先把信號捕捉注冊掉
關于回調函數的補充:
10張圖讓你徹底理解回調函數
信號集
信號集概念
參考??驼n程PPT(見參考資料1)
許多信號相關的系統調用都需要能表示一組不同的信號,多個信號可使用一個稱之為信號集的數據結構來表示,其系統數據類型為 sigset_t。
在 PCB 中有兩個非常重要的信號集**。一個稱之為 “阻塞信號集” ,另一個稱之為“未決信號集”** 。這兩個信號集都是內核使用位圖機制來實現的。但操作系統不允許我們直接對這兩個信號集進行位操作。而需自定義另外一個集合,借助信號集操作函數
來對 PCB 中的這兩個信號集進行修改。
信號的 “未決” 是一種狀態,指的是從信號的產生到信號被處理前的這一段時間。
信號的 “阻塞” 是一個開關動作,指的是阻止信號被處理,但不是阻止信號產生。信號的阻塞就讓系統暫時保留信號留待以后發送。由于另外有辦法讓系統忽略信號,所以一般情況下信號的阻塞只是暫時的,只是為了防止信號打斷敏感的操作
下圖修改自??蚉PT1.用戶通過鍵盤 Ctrl + C, 產生2號信號SIGINT (信號被創建)
2.信號產生但是沒有被處理 (未決)
在內核中將所有的沒有被處理的信號存儲在一個集合中 (未決信號集)
SIGINT信號狀態被存儲在第二個標志位上
這個標志位的值為0, 說明信號不是未決狀態
這個標志位的值為1, 說明信號處于未決狀態
3.這個未決狀態的信號,需要被處理,處理之前需要和另一個信號集(阻塞信號集),進行比較
阻塞信號集默認不阻塞任何的信號
如果想要阻塞某些信號需要用戶調用系統的API
4.在處理的時候和阻塞信號集中的標志位進行查詢,看是不是對該信號設置阻塞了
如果沒有阻塞,這個信號就被處理
如果阻塞了,這個信號就繼續處于未決狀態,直到阻塞解除,這個信號就被處理
信號集相關函數
我們不能直接修改內核中的信號集,這樣太危險了,系統已經封裝了一些函數給我們操作。
/*以下信號集相關的函數都是對自定義的信號集進行操作。int sigemptyset(sigset_t *set);- 功能:清空信號集中的數據,將信號集中的所有的標志位置為0- 參數:set,傳出參數,需要操作的信號集- 返回值:成功返回0, 失敗返回-1int sigfillset(sigset_t *set);- 功能:將信號集中的所有的標志位置為1- 參數:set,傳出參數,需要操作的信號集- 返回值:成功返回0, 失敗返回-1int sigaddset(sigset_t *set, int signum);- 功能:設置信號集中的某一個信號對應的標志位為1,表示阻塞這個信號- 參數:- set:傳出參數,需要操作的信號集- signum:需要設置阻塞的那個信號- 返回值:成功返回0, 失敗返回-1int sigdelset(sigset_t *set, int signum);- 功能:設置信號集中的某一個信號對應的標志位為0,表示不阻塞這個信號- 參數:- set:傳出參數,需要操作的信號集- signum:需要設置不阻塞的那個信號- 返回值:成功返回0, 失敗返回-1int sigismember(const sigset_t *set, int signum);- 功能:判斷某個信號是否阻塞- 參數:- set:需要操作的信號集- signum:需要判斷的那個信號- 返回值:1 : signum被阻塞0 : signum不阻塞-1 : 失敗*/#include <signal.h> #include <stdio.h>int main() {// 創建一個信號集sigset_t set;// 清空信號集的內容,沒有初始化清空的話標志位可能是隨機的sigemptyset(&set);// 判斷 SIGINT 是否在信號集 set 里int ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 添加幾個信號到信號集中sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);// 判斷SIGINT是否在信號集中ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 判斷SIGQUIT是否在信號集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}// 從信號集中刪除一個信號sigdelset(&set, SIGQUIT);// 判斷SIGQUIT是否在信號集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}return 0; }sigprocmask和sigpending函數
/*int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);- 功能:將自定義信號集中的數據設置到內核中(設置阻塞,解除阻塞,替換)- 參數:- how : 如何對內核阻塞信號集進行處理SIG_BLOCK: 將用戶設置的阻塞信號集添加到內核中,內核中原來的數據不變假設內核中默認的阻塞信號集是mask, mask | setSIG_UNBLOCK: 根據用戶設置的數據,對內核中的數據進行解除阻塞mask &= ~setSIG_SETMASK:覆蓋內核中原來的值- set :已經初始化好的用戶自定義的信號集- oldset : 保存設置之前的內核中的阻塞信號集的狀態,可以是 NULL- 返回值:成功:0失敗:-1設置錯誤號:EFAULT、EINVALint sigpending(sigset_t *set);- 功能:獲取內核中的未決信號集- 參數:set,傳出參數,保存的是內核中的未決信號集中的信息。 */// 編寫一個程序,把所有的常規信號(1-31)的未決狀態打印到屏幕 // 設置某些信號是阻塞的,通過鍵盤產生這些信號#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h>int main() {// 設置2、3號信號阻塞sigset_t set;sigemptyset(&set);// 將2號和3號信號添加到信號集中sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);// 修改內核中的阻塞信號集sigprocmask(SIG_BLOCK, &set, NULL);int num = 0;while(1) {num++;// 獲取當前的未決信號集的數據sigset_t pendingset;sigemptyset(&pendingset);sigpending(&pendingset);// 遍歷前32位for(int i = 1; i <= 31; i++) {if(sigismember(&pendingset, i) == 1) {printf("1");}else if(sigismember(&pendingset, i) == 0) {printf("0");}else {perror("sigismember");exit(0);}}printf("\n");sleep(1);if(num == 10) {// 解除阻塞sigprocmask(SIG_UNBLOCK, &set, NULL);}}return 0; }sigaction信號捕捉函數
sigaction與signal功能大致相同,但是我們建議使用sigaction函數來做信號捕捉,因為signal是ANSI C標準,不一定都適用。
/*#include <signal.h>int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);- 功能:檢查或者改變信號的處理。信號捕捉- 參數:- signum : 需要捕捉的信號的編號或者宏值(信號的名稱)- act :捕捉到信號之后的處理動作- oldact : 上一次對信號捕捉相關的設置,一般不使用,傳遞NULL- 返回值:成功 0失敗 -1struct sigaction {// 函數指針,指向的函數就是信號捕捉到之后的處理函數void (*sa_handler)(int);// 不常用void (*sa_sigaction)(int, siginfo_t *, void *);// 臨時阻塞信號集,在信號捕捉函數執行過程中,臨時阻塞某些信號。sigset_t sa_mask;// 使用哪一個信號處理對捕捉到的信號進行處理// 這個值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigactionint sa_flags;// 被廢棄掉了void (*sa_restorer)(void);};*/ #include <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h>void myalarm(int num) {printf("捕捉到了信號的編號是:%d\n", num);printf("xxxxxxx\n"); }// 過3秒以后,每隔2秒鐘定時一次 int main() {struct sigaction act;act.sa_flags = 0;act.sa_handler = myalarm;sigemptyset(&act.sa_mask); // 清空臨時阻塞信號集// 注冊信號捕捉sigaction(SIGALRM, &act, NULL);struct itimerval new_value;// 設置間隔的時間new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 設置延遲的時間,3秒之后開始第一次定時new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定時器開始了...\n");if(ret == -1) {perror("setitimer");exit(0);}// getchar();while(1);return 0; }信號捕捉的特點
1.在信號捕捉處理過程中,使用臨時的阻塞信號集,當信號處理完后會回到內核PCB的信號集
2.在執行某個回調函數期間,相同的信號會被默認屏蔽掉,再次發送同一個信號就沒用了,等前一次回調函數執行好了以后采取執行
3.阻塞的信號不能排隊,因為未決信號集只有01來看狀態,不能統計數量(后面實時信號是可以排隊的)
信號捕捉過程
觀察上圖,可以看到信號處理過程中回調函數的路徑,并且它是由內核來自動控制的
SIGCHLD信號
使用SIGCHLD信號解決僵尸進程的問題。
/*SIGCHLD信號產生的3個條件:1.子進程結束2.子進程暫停了3.子進程繼續運行都會給父進程發送該信號,父進程默認忽略該信號。使用SIGCHLD信號解決僵尸進程的問題。 */#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <sys/wait.h>void myFun(int num) {printf("捕捉到的信號 :%d\n", num);// 回收子進程PCB的資源// while(1) {// wait(NULL); 這樣會死循環// }while(1) {int ret = waitpid(-1, NULL, WNOHANG);if(ret > 0) {printf("child die , pid = %d\n", ret);} else if(ret == 0) {// 說明還有子進程或者break;} else if(ret == -1) {// 沒有子進程break;}} }int main() {// 提前設置好阻塞信號集,阻塞SIGCHLD,因為有可能子進程很快結束,父進程還沒有注冊完信號捕捉sigset_t set;sigemptyset(&set);sigaddset(&set, SIGCHLD);sigprocmask(SIG_BLOCK, &set, NULL);// 創建一些子進程pid_t pid;for(int i = 0; i < 20; i++) {pid = fork();if(pid == 0) {break;}}if(pid > 0) {// 父進程// 捕捉子進程死亡時發送的SIGCHLD信號struct sigaction act;act.sa_flags = 0;act.sa_handler = myFun;sigemptyset(&act.sa_mask);sigaction(SIGCHLD, &act, NULL);// 注冊完信號捕捉以后,解除阻塞sigprocmask(SIG_UNBLOCK, &set, NULL);while(1) {printf("parent process pid : %d\n", getpid());sleep(2);}} else if( pid == 0) {// 子進程printf("child process pid : %d\n", getpid());}return 0; }總結
以上是生活随笔為你收集整理的[Linux 高并发服务器] 信号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【发现问题】IDEA设置全局新创建文件默
- 下一篇: linux系统 硬链接和软链接