文章目錄
- 信號入門
- 生活角度的信號
- 技術應用角度的信號
- 用kill -l命令可以察看系統定義的信號列表
- 信號處理常見方式概述
- 產生信號
- 通過鍵盤進行信號的產生,```ctrl+c```向前臺發送2號信號
- 通過系統調用
- 異常
- 軟件條件
信號入門
生活角度的信號
- 你在網上買了很多件商品,再等待不同商品快遞的到來。但即便快遞沒有到來,你也知道快遞來臨時,你該怎么處理快遞。也就是你能“識別快遞”
- 當快遞員到了你樓下,你也收到快遞到來的通知,但是你正在打游戲,需5min之后才能去取快遞。那么在在這5min之內,你并沒有下去去取快遞,但是你是知道有快遞到來了。也就是取快遞的行為并不是一定要立即執行,可以理解成“在合適的時候去取”。
- 在收到通知,再到你拿到快遞期間,是有一個時間窗口的,在這段時間,你并沒有拿到快遞,但是你知道有一個快遞已經來了。本質上是你“記住了有一個快遞要去取”
- 當你時間合適,順利拿到快遞之后,就要開始處理快遞了。而處理快遞一般方式有三種:1. 執行默認動作(幸福的打開快遞,使用商品)2. 執行自定義動作(快遞是零食,你要送給你你的女朋友)3. 忽略快遞(快遞拿上來之后,扔掉床頭,繼續開一把游戲)
- 快遞到來的整個過程,對你來講是異步的,你不能準確斷定快遞員什么時候給你打電話
技術應用角度的信號
#include <stdio.h>
#include <unistd.h>int main()
{while (1){printf("I am a process, I am waiting signal!\n");sleep(1);}
}
我們知道上述代碼是死循環的,我們使用
ctrl+c
來終止掉這個進程,本質是鍵盤向CPU發送了一個中斷被操作系統獲取并解釋成信號(ctrl+c
被解釋成2號信號),最后操作系統將2號信號發送給目標前臺進程,當前臺進程收到2號信號后就會退出。
按照文章開頭所談的話,進程就是我,操作系統就是快遞員,信號就是快遞
注意:
ctrl+c
產生的信號只能發給前臺進程。shell
可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像 ```ctrl+c``這種控制鍵產生的信號- 前臺進程在運行過程中用戶隨時可能按下
ctrl+c
而產生一個信號,也就是說該進程的用戶空間代碼執行到任何地方都有可能收到SIGINT
信號而終止,所以信號相對于進程的控制流程來說是異步(Asynchronous)的。
- 我們可以使用
jobs
來查看正在執行的任務fg number
將后臺中的命令調至前臺繼續運行bg number
將一個在后臺暫停的命令變成繼續執行&
加在一個命令的最后,可以把這個命令放到后臺執行;
(./xxx &)這樣就可以將進程放在后臺
ctrl+z
可以將一個正在前臺執行的命令放到后臺,并且處于暫停狀態,不可執行
用kill -l命令可以察看系統定義的信號列表
kil -l
其中1 - 31號信號是普通信號,34 - 64號信號是實時信號
我們看到上面這一堆的大寫字母 + 數字,不難想到它們是使用了宏
信號是如何記錄的?
使用位圖,如果該位置為1
即該信號被收到
所以信號產生的本質上就是操作系統直接去修改目標進程的task_struct中的信號位圖。
信號處理常見方式概述
- 執行該信號的默認處理動作。
- 忽略此信號。
- 提供一個信號處理函數,內核在處理該信號時切換到用戶態執行這個處理函數,這種方式稱為捕捉(Catch)一個信號;自定義的——信號的捕捉
我們可以使用man 7 signal
來查看信號的默認處理動作。
產生信號
通過鍵盤進行信號的產生,ctrl+c
向前臺發送2號信號
#include <stdio.h>
#include <unistd.h>int main()
{while (1){printf("hello signal!\n");sleep(1);}return 0;
}
除此之外,我們還可以使用ctrl+\
來終止進程
那么它們兩個有什么區別呢???
我們發現
ctrl+c
對應的行為是Term
,而ctrl+\
對應的行為是Core
,二者都代表著終止進程,但是Core
會多進行一個動作,那就是核心轉儲
何為核心轉儲???
首先, 在云服務器中,核心轉儲是默認被關掉的,我們可以通過使用ulimit -a
命令查看當前資源限制的設定。
第一行顯示core文件的大小為0,即表示核心轉儲是被關閉的。
我們可以通過ulimit -c size
命令來設置core文件的大小。
core文件的大小設置完畢后,就將核心轉儲功能打開了。此時如果我們再使用
ctrl+\
對進程進行終止,就會發現終止進程后會顯示core dumped
并且在該路徑下生成了一個
core.pid
文件
其中pid
,是一串數字,而這一串數字就是發生這一次核心轉儲的進程的PID。
核心轉儲有什么用呢???
當我們的代碼報錯以后,我們總需要找到報錯原因;
當我們的程序在運行過程中崩潰了,我們會通過調試來進行逐步查找程序崩潰的原因;
當我們的程序在運行結束了,那么我們可以通過退出碼來判斷代碼出錯的原因;
而在某些特殊情況下,我們會使用核心轉儲,核心轉儲是操作系統在進程收到某些信號而終止運行時,將該進程地址空間的內容以及有關進程狀態的其他信息轉而存儲到一個磁盤文件當中,這個磁盤文件也叫做核心轉儲文件,一般命名為core.pid
。
#include <stdio.h>int main()
{printf("I am Div\n");int a = 10;a /= 0;return 0;
}
顯然,上述代碼出現了除零錯誤
那ctrl+其他
有什么效果呢???
我們可以通過以下代碼,將1~31號信號全部進行捕捉,將收到信號后的默認處理動作改為打印收到信號的編號。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void handler(int signal)
{printf("get a signal:%d\n", signal);
}
int main()
{int signo;for (signo = 1; signo <= 31; signo++){signal(signo, handler);}while (1){sleep(1);}return 0;
}
但我們發送9號進程,它并不會收到,而是執行收到9號信號后的默認處理動作,即被殺死。
所以,對于某些信號是不可以被自定義處理的。比如9號信,因為如果所有信號都能被捕捉的話,那么進程就可以將所有信號全部進行捕捉并將動作設置為忽略,此時該進程將無法被殺死,即便是操作系統也做不到。
通過系統調用
我們可以以kill -信號編號 進程ID
的形式進行發送。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>int main()
{while (true){printf("I am a running process..., mypid = %d\n", getpid());sleep(1);}return 0;
}
kill函數
int kill(pid_t pid, int sig);
實際上,
kill
指令的底層就是kill
函數
kill函數
用于向進程ID
為pid
的進程發送sig
號信號,如果信號發送成功,則返回0,否則返回-1。
raise函數
raise函數
可以給當前進程發送指定信號,即自己給自己發送信號
int raise(int sig);
用raise函數每隔一秒向自己發送一個2號信號。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void handler(int signo)
{printf("get a signal:%d\n", signo);
}
int main()
{signal(2, handler);while (1){sleep(1);raise(2);}return 0;
}
abort函數
raise函數
可以給當前進程發送SIGABRT
信號,使得當前進程異常終止
void abort(void);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void handler(int signo)
{printf("get a signal:%d\n", signo);
}
int main()
{signal(6, handler);while (1){sleep(1);abort();}return 0;
}
abort函數的作用是異常終止進程
exit 函數的作用是正常終止進程
異常
當我們程序當中出現類似于除0、野指針、越界之類的錯誤時,為什么程序會崩潰?本質上是因為進程在運行過程中收到了操作系統發來的信號進而被終止,那操作系統是如何識別到一個進程觸發了某種問題的呢?
我們知道,CPU當中有一堆的寄存器,當我們需要對兩個數進行算術運算時,我們是先將這兩個操作數分別放到兩個寄存器當中,然后進行算術運算并把結果寫回寄存器當中。此外,CPU當中還有一組寄存器叫做狀態寄存器,它可以用來標記當前指令執行結果的各種狀態信息,如有無進位、有無溢出等等。而操作系統是軟硬件資源的管理者,在程序運行過程中,若操作系統發現CPU內的某個狀態標志位被置位,而這次置位就是因為出現了某種除0錯誤而導致的,那么此時操作系統就會馬上識別到當前是哪個進程導致的該錯誤,并將所識別到的硬件錯誤包裝成信號發送給目標進程,本質就是操作系統去直接找到這個進程的task_struct,并向該進程的位圖中寫入8信號,寫入8號信號后這個進程就會在合適的時候被終止。
那對于下面的野指針問題,或者越界訪問的問題時,操作系統又是如何識別到的呢?
當我們要訪問一個變量時,一定要先經過頁表的映射,將虛擬地址轉換成物理地址,然后才能進行相應的訪問操作。
其中頁表屬于一種軟件映射關系,而實際上在從虛擬地址到物理地址映射的時候還有一個硬件叫做MMU,它負責處理CPU的內存訪問請求的計算機硬件,因此映射工作不是由CPU做的,而是MMU做的,但現在MMU已經集成到CPU當中了。
當需要進行虛擬地址到物理地址的映射時,我們先將頁表的左側的虛擬地址導給MMU,然后MMU會計算出對應的物理地址,我們再通過這個物理地址進行相應的訪問。
而MMU既然是硬件單元,那么它當然也有相應的狀態信息,當我們要訪問不屬于我們的虛擬地址時,MMU在進行虛擬地址到物理地址的轉換時就會出現錯誤,然后將對應的錯誤寫入到自己的狀態信息當中,這時硬件上面的信息也會立馬被操作系統識別到,進而將對應進程發送SIGSEGV信號。
C/C++程序會崩潰,是因為程序當中出現的各種錯誤最終一定會在硬件層面上有所表現,進而會被操作系統識別到,然后操作系統就會發送相應的信號將當前的進程終止。
軟件條件
SIGPIPE信號
SIGPIPE信號實際上就是一種由軟件條件產生的信號,當進程在使用管道進行通信時,讀端進程將讀端關閉,而寫端進程還在一直向管道寫入數據,那么此時寫端進程就會收到SIGPIPE信號進而被操作系統終止。
下面代碼當中,創建匿名管道進行父子進程之間的通信,其中父進程是讀端進程,子進程是寫端進程,但是一開始通信父進程就將讀端關閉了,那么此時子進程在向管道寫入數據時就會收到SIGPIPE信號,進而被終止。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe創建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork創建子進程if (id == 0){//childclose(fd[0]); //子進程關閉讀端//子進程向管道寫入數據const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子進程寫入完畢,關閉文件exit(0);}//fatherclose(fd[1]); //父進程關閉寫端close(fd[0]); //父進程直接關閉讀端(導致子進程被操作系統殺掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); //打印子進程收到的信號return 0;
}
alarm函數
unsigned int alarm(unsigned int seconds);
alarm函數可以設定一個鬧鐘,也就是告訴操作系統在若干時間后發送SIGALRM信號
給當前進程
alarm函數的返回值:
- 若調用alarm函數前,進程已經設置了鬧鐘,則返回上一個鬧鐘時間的剩余時間,并且本次鬧鐘的設置會覆蓋上一次鬧鐘的設置。
- 如果調用alarm函數前,進程沒有設置鬧鐘,則返回值為0。
用下面的代碼,測試自己的云服務器一秒時間內可以將一個變量累加到多大。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main()
{int count = 0;alarm(1);while (1){count++;printf("count: %d\n", count);}return 0;
}
我們發現加的變量好像有點小,原有有兩點:
- 由于我們每進行一次累加就進行了一次打印操作,而與外設之間的IO操作所需的時間要比累加操作的時間更長
- 我當前使用的是云服務器,因此在累加操作后還需要將累加結果通過網絡傳輸將服務器上的數據發送過來,因此最終顯示的結果要比實際一秒內可累加的次數小得多。
為了盡可能避免上述問題,我們可以先讓count變量一直執行累加操作,直到一秒后進程收到SIGALRM信號后再打印累加后的數據。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int count = 0;
void handler(int signo)
{printf("get a signal: %d\n", signo);printf("count: %d\n", count);exit(1);
}
int main()
{signal(SIGALRM, handler);alarm(1);while (1){count++;}return 0;
}
count變量在一秒內被累加的次數變成了四億多,所以,我們得出一個結論與計算機單純的計算相比較,計算機與外設進行IO時的速度是非常慢的。