目錄
一、信號的引入
1、信號概念
2、signal函數
普通標準信號詳解表
3、前臺/后臺進程
3.1 概念
3.2?查看后臺進程
3.3?后臺進程拉回前臺
3.4 終止后臺進程
3.5 暫停前臺進程
3.6 回復運行后臺進程
4、發信號的本質
二、信號的產生
1、終端按鍵
2、系統調用
2.1 kill
2.2 raise
2.3 abort
總結
3、硬件異常
3.1 除0錯誤
3.2 野指針錯誤
底層原理
4、軟件條件
4.1 SIGPIPE
4.2 SIGALRM
4.3 SIGCHLD
總結
理解系統鬧鐘
一、信號的引入
1、信號概念
#?在Linux
系統中,信號(Signal)是一種軟件中斷機制,用于通知進程發生了特定的事件。信號可以由系統內核、其他進程或者進程自身發送。
#?我們可以通過指令kill -l查看
所有信號:
#?信號的本質就是一個define定義的宏,其中1 - 31號信號是普通信號,34 - 64號信號是實時信號,普通信號和實時信號各自都有31個。每一個信號與一個數字相對應,每個信號也都有特定的含義和默認的處理動作。例如,信號SIGINT(通常由用戶按下ctrl + c產生)表示中斷信號,默認情況下會導致進程終止。
# 注意:在Linux中,前臺進程只能有一個,而后臺進程可以為多個。一般而言,我們的bash進程作為我們的前臺進程,而一旦我們執行一個可執行程序,這個可執行程序就會成為前臺進程,而bash進程就會轉為后臺進程。但是我們如果在執行一個可執行程序時,在之后加一個&,此時的可執行程序就會由前臺進程轉換為后臺進程。而前臺進程與后臺進程本質間區別就是前臺進程可以從鍵盤獲取數據,后臺進程則不能。
# 比如我們運行一個后臺進程,就無法通過ctrl + c
終止進程,因為其無法從鍵盤讀取數據。此時就只能通過kill
指令直接殺死對應的進程。
2、signal函數
# 1 - 31號信號是普通信號,可以不用立即處理。普通信號的特點是:不支持排隊。如果同一個信號在進程處理它之前多次產生,它只會被記錄一次,這可能會導致信號丟失。34 - 64號信號是實時信號,收到就要立即處理!
#?當一個實時信號被發送給一個進程時,進程可以采取以下幾種方式來處理信號:
- 忽略信號:進程可以選擇忽略某些信號,即不對信號做出任何反應。但并不是所有信號都可以被忽略,例如 SIGKILL 和 SIGSTOP 信號不能被忽略。
- 捕獲信號:進程可以注冊一個信號處理函數,當接收到特定信號時,就會執行這個函數。通過這種方式,進程可以在接收到信號時執行自定義的處理邏輯。
- 執行默認動作:如果進程沒有顯式地忽略或捕獲信號,那么它將執行信號的默認動作。默認動作通常是終止進程、停止進程、繼續進程等。
# 我們可以通過指令?man 7 signal 查看信號的默認處理動作:
普通標準信號詳解表
編號 | 信號名稱 | 默認行為 | 觸發場景說明 | 可否捕獲或忽略 | 重要說明 |
---|---|---|---|---|---|
1 | SIGHUP | Term | 掛起。終端連接斷開(如網絡斷開、關閉終端窗口)、控制進程終止。 | Yes | 常被用于通知守護進程重新讀取配置文件(如?nginx -s reload )。 |
2 | SIGINT | Term | 中斷。來自鍵盤的中斷,通常是用戶按下?Ctrl+C 。 | Yes | 請求優雅地終止前臺進程。 |
3 | SIGQUIT | Core | 退出。來自鍵盤的退出,通常是用戶按下?Ctrl+\ 。 | Yes | 不僅終止進程,還會生成?core dump?文件用于調試。表示用戶希望進程終止并留下調試信息。 |
4 | SIGILL | Core | 非法指令。進程嘗試執行一條非法、錯誤或特權的指令。 | Yes | 通常由程序 bug?引起,例如執行了損壞的二進制文件、棧溢出等。 |
5 | SIGTRAP | Core | 跟蹤/斷點陷阱。由調試器使用,用于在斷點處中斷進程的執行。 | Yes | 這是調試器(如 gdb)實現斷點功能的機制。 |
6 | SIGABRT | Core | 中止。通常由?abort() ?函數調用產生。 | Yes | 進程自己調用?abort() ?來終止自己,通常表示檢測到了嚴重的內部錯誤(如?assert ?斷言失敗)。也會生成 core dump。 |
7 | SIGBUS | Core | 總線錯誤。無效的內存訪問,即訪問的內存地址不存在或違反了內存對齊要求。 | Yes | 硬件級別的錯誤。例如,在支持對齊要求的架構上訪問未對齊的地址。與?SIGSEGV ?類似但原因更底層。 |
8 | SIGFPE | Core | 浮點異常。錯誤的算術運算,如除以零、溢出等。 | Yes | 不僅是浮點數,整數除以零也會觸發此信號。 |
9 | SIGKILL | Term | 殺死。無條件立即終止進程。 | No | 無法被捕獲、阻塞或忽略。是終止進程的最終極、最強制的手段。kill -9 ?的由來。 |
10 | SIGUSR1 | Term | 用戶自定義信號 1。 | Yes | 沒有預定義的含義,完全留給用戶程序自定義其行為。常用于應用程序內部通信(如通知進程切換日志文件、重新加載特定數據等)。 |
11 | SIGSEGV | Core | 段錯誤。無效的內存引用,即訪問了未分配或沒有權限訪問的內存(如向只讀內存寫入)。 | Yes | C/C++ 程序中最常見的崩潰原因之一(解空指針、訪問已釋放內存、棧溢出、緩沖區溢出等)。 |
12 | SIGUSR2 | Term | 用戶自定義信號 2。 | Yes | 同上,另一個用戶可自定義用途的信號。 |
13 | SIGPIPE | Term | 管道破裂。向一個沒有讀者的管道(或 socket)進行寫入操作。 | Yes | 常見場景:一個管道中,讀端進程已關閉或終止,寫端進程還在寫入。如果忽略此信號,write ?操作會返回錯誤并設置?errno ?為?EPIPE 。 |
14 | SIGALRM | Term | 定時器信號。由?alarm() ?或?setitimer() ?設置的定時器超時后產生。 | Yes | 常用于實現超時機制或周期性任務。 |
15 | SIGTERM | Term | 終止。這是一個友好的終止進程的請求。 | Yes | kill ?命令的默認信號。進程收到此信號后,應該執行清理工作(關閉文件、釋放資源等)然后退出。是優雅關閉服務的首選方式。 |
16 | SIGSTKFLT | Term | 協處理器棧錯誤。極少使用。 | Yes | 與早期的數學協處理器有關,現代 Linux 系統上基本不會見到。 |
17 | SIGCHLD | Ign | 子進程狀態改變。一個子進程停止或終止時,內核會向父進程發送此信號。 | Yes | 非常重要!父進程可以通過捕獲此信號來調用?wait() ?或?waitpid() ?回收子進程資源,防止出現僵尸進程。默認行為是 Ignore,但最好顯式處理。 |
18 | SIGCONT | Cont | 繼續。讓一個停止的進程繼續運行。 | Yes | 無法被停止的進程忽略。常用于作業控制(fg ?/?bg ?命令的底層實現)。 |
19 | SIGSTOP | Stop | 停止。暫停進程的執行(進入 Stopped 狀態)。 | No | 無法被捕獲、阻塞或忽略。是?Ctrl+Z ?(SIGTSTP ) 的強制版本。 |
20 | SIGTSTP | Stop | 終端停止。來自終端的停止信號,通常是用戶按下?Ctrl+Z 。 | Yes | 請求優雅地暫停前臺進程。進程可以被捕獲,在捕獲函數中它可以做一些準備工作后再決定是否暫停自己。 |
21 | SIGTTIN | Stop | 后臺進程讀終端。一個后臺進程嘗試從控制終端讀取輸入。 | Yes | 為了防止后臺進程干擾前臺,內核會自動停止該后臺進程。 |
22 | SIGTTOU | Stop | 后臺進程寫終端。一個后臺進程嘗試向控制終端寫入輸出。 | Yes | 類似于?SIGTTIN ,但用于寫入操作。是否停止取決于終端配置(stty tostop )。 |
... | ... | ... | ... | ... | ... |
# 其中,Term是終止進程,Ign是忽略信號,Stop是暫停進程,Cont是繼續進程,Core也是終止進程并生成 core dump 文件,但是和Term有區別。
#?SIGKILL
?(9) 和?SIGSTOP
?(19) 是兩個特殊的信號,無法被捕獲、阻塞或忽略。這是為了給系統管理員一個最終能控制任何進程的手段。
#?接下來我們介紹一個函數signal
,其可以設置進程對某個信號的自定義捕捉方法:即當進程收到?signum
?信號的時候,去執行?handler
?方法。
- 函數原型:
- typedef void (*sighandler_t)(int);
- sighandler_t signal(int signum, sighandler_t handler);
? ? ?2. ? 參數:
signum
:是一個整數,表示要處理的信號編號。handler
:是一個函數指針,指向一個信號處理函數。這個信號處理函數接受一個整數參數(即接收到的信號編號),并且沒有返回值(void
)。可以是以下幾種值:
SIG_DFL
:表示默認的信號處理動作。SIG_IGN
:表示忽略該信號。- 自定義的信號處理函數指針,用于處理特定信號。
# 我們知道?ctrl + c?的本質是向前臺進程發送 SIGINT(即 2 號信號)。為了驗證這一點,我們需要使用系統調用signal函數來進行演示。
#include<iostream>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{signal(SIGINT, handlerSig); // 收到2號信號時,調用handlerSig函數,執行函數里的動作,將SIGINT作為參數傳過去int cnt = 0;while(true){std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;sleep(1);}return 0;
}
#?其中前臺進程在運行過程中,用戶隨時可能按下?ctrl + c?而產生一個信號,也就是說該進程的代碼執行到任何地方都可能收到SIGINT
信號而終止,所以信號相對于進程的控制流程來說是異步的。
#?我們可以看到,按下 ctrl + c?后打印消息而非終止進程。所以,ctrl + c?觸發了?SIGINT
?信號,此時會執行我們寫的自定義邏輯。這個時候我們?ctrl + c 終止不了這個進程,但是我們還可以使用 ctrl + \ 來結束這個進程。因為?ctrl + \ ?會發送另一個不同的信號:SIGQUIT
?(信號編號3)。
3、前臺/后臺進程
3.1 概念
3.2?查看后臺進程
#?使用 Shell 內置命令?jobs
,可以查看所有的后臺任務。
$ ./testsig &
[1] 263501 # [job_number] pid
$ jobs
[1]+ Running ./testsig &
[1]
:任務編號(Job Number),由 Shell 分配管理,在當前 Shell 會話內有效。
263501
:進程PID,由操作系統分配,在整個系統內有效。
3.3?后臺進程拉回前臺
#?使用命令?fg %<job_number>
,這樣然后就能再用?Ctrl+C
?來終止它了。
$ fg %1 # 將作業編號為1的后臺作業變為前臺作業
# 此時終端再次被 testsig 霸占,可以用 Ctrl+C 終止
3.4 終止后臺進程
#?既然?Ctrl+C
無效,就必須使用?kill
?命令通過發送信號來終止。
方法一:通過作業編號(推薦,在當前終端內操作最方便)
$ kill %1 # 向作業1發送默認的 TERM 信號,請求終止
$ jobs
[1]+ Terminated ./testsig # 確認它已終止
方法二:通過進程PID
$ kill 263501 # 發送 SIGTERM (15),友好地請求終止
$ kill -9 263501 # 發送 SIGKILL (9),強制殺死,無法被捕獲或忽略
3.5 暫停前臺進程
# 我們可以使用 ctrl + z 來暫停前臺進程,但是由于前臺進程要一直運行著,所以暫停的進程自動變為后臺進程。
-
Ctrl+Z
?的效果:向進程發送?SIGTSTP
?(Terminal?Stop) 信號 -
進程狀態變化:從?
Running
?變為?Stopped
?(停止)
3.6 回復運行后臺進程
#?我們可以使用 bg 任務號?來回復運行后臺進程。
bg
?命令的效果:向進程發送?SIGCONT
?(Continue) 信號,但不將其帶回前臺
關鍵點:雖然?bg
?讓進程繼續執行,但 Shell 和內核對其處理方式與直接用?&
?啟動的進程有細微差別。
# 總結:
4、發信號的本質
二、信號的產生
#?至此,我們對信號有了一個基本認識,那么接下來我們就先從信號的產生介紹。
# 在我們操作系統中,信號的產生方式有許多,總體歸納來說有四種。
1、終端按鍵
#?其中我們通過鍵盤快捷鍵直接向我們的進程發出信號的方式非常常見,其中較為我們常用的有:
組合鍵 | 功能 |
---|---|
Ctrl+C | 向進程發出SIGINT 信號,終止進程。 |
Ctrl+\ | 向進程發出SIGQUIT 信號,終止進程。 |
Ctrl+Z | 向進程發送SIGTSTP 信號,暫停進程的執行。 |
2、系統調用
#?我們也可以通過操作系統為我們提供的接口對進程發送對應的信號。
2.1 kill
- 頭文件:#include <sys/types.h> ? ? ?#include <signal.h>
- 函數原型:int kill(pid_t pid, int sig);
- 參數:
pid
對應要發送信號進程的pid
,sig
表示發送的信號種類。- 返回值:如果成功,返回值為 0。否則,返回值為 -1
#?這里我們使用kill系統調用,來實現一個我們自己的kill命令:
// kill.cc#include<iostream>
#include<sys/types.h>
#include<signal.h>// ./mykill signumber pid
int main(int argc, char *argv[])
{if(argc != 3){std::cout << "./mykill signumber pid" << std::endl;return 1;}int signumber = std::stoi(argv[1]); // 字符串轉成整數pid_t target = std::stoi(argv[2]);int n = kill(target, signumber);if(n == 0){std::cout << "send " << signumber << " to " << target << " success.";}return 0;
}
#?所以這里我們使用kill系統調用,就可以給另一個進程傳遞信號了,這里在命令參數傳入信號編號和目標進程,我們傳入2號信號,進程就收到2號信號。
#?萬一我們的進程是惡意的病毒呢?不就無法殺掉了嗎?我們操作系統的設計者也考慮到了這點,所以我們kill發送9號信號時,可以殺掉進程,因為9號信號進禁止自定義捕捉,防止病毒程序屏蔽信號。
#??SIGKILL
?(9) 和?SIGSTOP
?(19) 是兩個特殊的信號,無法被捕獲、阻塞或忽略。這是為了給系統管理員一個最終能控制任何進程的手段。
2.2 raise
#?raise
的目標只有一個,就是調用者進程自身,自己給自己的進程發信號。
- 頭文件:#include <signal.h>
- 函數原型:int raise(int sig);
- 返回值:如果成功,返回值為 0。否則,返回值為非0
#?這里我們把普通信號都捕獲了,方便我們使用raise系統調用給當前進程發送信號時,都能捕獲到然后打印出來我們查看。
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{// signal(SIGINT, handlerSig); // 收到2號信號時,調用handlerSig函數,執行函數里的動作,將SIGINT作為參數傳過去for (int i = 1; i < 32; i++)signal(i, handlerSig); // 將1 - 32所有信號都自定義捕捉 for (int i = 1; i < 32; i++) // 每隔一秒自己給自己發一個信號{sleep(1);if(i == 9 || i == 19) // 跳過兩個無法被捕捉的信號continue;raise(i);}int cnt = 0;while(true){std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;sleep(1);}return 0;
}
hzy@tata:/home/tata/lesson14$ ./testsig
獲得了一個信號: 1
獲得了一個信號: 2
獲得了一個信號: 3
獲得了一個信號: 4
獲得了一個信號: 5
獲得了一個信號: 6
獲得了一個信號: 7
獲得了一個信號: 8
Killed
#?在前文中我們有說過,有兩個信號:9號和19號信號不能被捕獲,所以當我們進程在準備捕獲9號信號時,由于9號信號不能被捕獲,所以當前進程被終止。
2.3 abort
#?abort
函數的功能非常明確和強硬:立即異常終止當前進程,并生成一個 core dump 文件(如果系統配置允許)。
- 頭文件:#include <stdlib.h>
- 函數原型:void abort(void);
注意:該函數無參數,且無返回值,因為它永遠不會返回到調用者。
#?raise
函數用于給當前進程發送sig
號信號,而abort
函數相當于給當前進程發送SIGABRT
信號(6號),使當前進程異常終止。
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{for (int i = 1; i < 32; i++)signal(i, handlerSig); // 將1 - 32所有信號都自定義捕捉 int cnt = 0;while(true){std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;abort();sleep(1);}return 0;
}
# abort
與exit
函數同樣是終止進程,它們之間有什么區別嗎?
# 首先明確
abort
函數和exit
函數的不同作用。abort
函數的作用是異常終止進程,它本質上是通過向當前進程發送SIGABRT信號來實現這一目的。而exit
函數的作用是正常終止進程。
需要注意的是,使用exit
函數終止進程可能會失敗,因為在某些復雜的程序運行環境中,可能存在一些因素干擾正常的進程終止流程。然而,使用abort
函數終止進程通常被認為總是成功的,這是由于其通過發送特定信號強制終止進程,一般情況下進程很難忽略該信號而繼續運行。
總結
3、硬件異常
#?當程序出現除 0、野指針、越界等錯誤時,程序會崩潰,本質是進程在運行中收到操作系統發來的信號而被終止。 這些發送的信號都是由硬件異常產生的。
#?比如下面這段代碼,進行了對一個數的除0和空指針的解引用,那么其到底是如何被操作系統識別的呢?
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;exit(13);
}int main()
{int cnt = 0;while(true){std::cout << "hello tata, " << cnt++ << ",PID: " << getpid() << std::endl;int a = 10;// a /= 0; // 除0錯誤int *p = nullptr;*p = 100; // 野指針sleep(1);}return 0;
}
#?發現他們分別收到了8號SIGFPE和11號SIGSEGV信號,8號信號就是浮點數異常,11號信號就是段錯誤,所以我們的程序會崩潰是因為進程收到了信號,此時進程就會執行默認處理動作,進而終止進程。
# 所以異常也是會產生信號的。那么這個信號是誰發的?我們說過發信號本質就是修改PCB里面的位圖,只有OS才能修改,所以是OS發的。那么問題是操作系統如何知道進程出錯?
#?首先我們知道,當我們要訪問一個變量時,進程控制塊task_struct
一定要會經過頁表的映射,將虛擬地址轉換成物理地址,然后才能進行相應的訪問操作。
3.1 除0錯誤
#?程序都是運行在硬件CPU之上的,CPU有各種寄存器,如EIP、EBP,還有一個狀態寄存器,它里面有運算的標記位,如是否溢出、是否為0等,所以CPU硬件出錯后,操作系統作為軟硬件的管理者就會第一時間知道,然后CPU寄存器里保存有進程上下文,所以操作系統就知道是哪一個進程出錯,然后發現是計算溢出,就給進程發送8號信號進程就終止了。
3.2 野指針錯誤
# 野指針拿到一個虛擬地址,同時CPU的CR3寄存器記錄了頁表的虛擬地址,同時CPU里面集成了一個MMU硬件單元,此時MMU拿著頁表地址和虛擬地址就可以做虛擬地址到物理地址的轉換了。所以MMU有沒有可能轉換失敗?并且發現指針想要去0號地址寫入,所以MMU硬件報錯,操作系統立馬知道,根據CPU的進程上下文向對應的進程發送11號段錯誤信號,進程就直接終止了。
底層原理
#?我們都知道,當我們要訪問一個變量時,進程控制塊task_struct
一定要會經過頁表的映射,將虛擬地址轉換成物理地址,然后才能進行相應的訪問操作。
# 而頁表屬于一種軟件映射關系,在從虛擬地址到物理地址映射過程中,有一個硬件單元叫做 MMU(內存管理單元),它是負責處理 CPU 的內存訪問請求的計算機硬件。如今,MMU 已集成到 CPU 當中。雖然映射工作原本不是由 CPU 做而是由 MMU做,但現在其與 CPU 的緊密結合使得整個內存訪問過程更加高效。
# 當進行虛擬地址到物理地址的映射時,先將頁表左側的虛擬地址提供給?MMU,MMU會計算出對應的物理地址,隨后通過這個物理地址進行相應的訪問。
# 由于?MMU?是硬件單元,所以它有相應的狀態信息。當要訪問不屬于我們的虛擬地址時,MMU?在進行虛擬地址到物理地址的轉換時會出現錯誤,并將對應的錯誤寫入到自己的狀態信息當中。此時,硬件異常,硬件上的信息會立馬被操作系統識別到,進而向對應進程發送?SIGSEGV
信號。
# 現代 CPU 并不是傻傻地執行指令,它的內部有一套復雜的監控電路。當這些電路在執行指令的過程中檢測到某些特定條件時,會立即中斷當前控制流,并強制 CPU 去執行一段預設好的、屬于操作系統的代碼。這個過程是硬件自動完成的。我們可以用以下流程圖來概括這個硬協同的過程:
# 下面,我們以最常見的?SIGSEGV
?(段錯誤)?和?SIGFPE
?(除零錯誤)?為例,拆解圖中的每一步。
4、軟件條件
4.1 SIGPIPE
# 軟件條件也可以產生信號,這類信號的特點是:它們并非由外部進程或用戶通過?kill
?發送,也非由硬件錯誤觸發,而是由操作系統內核在檢測到某種特定的、預先定義的“軟件條件”滿足時,自動向進程發送的。
# 在我們前面學習管道通信時,就知道如果進程將讀端關閉,而寫端進程還一直向管道寫入數據,那么此時寫端進程就會收到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){perror("pipe:");return 1;}pid_t id = fork();if(id ==0 ){//child -> writeclose(fd[0]);char*msg = "hello father, i am child...";while(1){write(fd[1],msg,strlen(msg));sleep(1);}close(fd[1]);exit(0);}// father -> readclose(fd[1]);close(fd[0]);int status = 0;waitpid(id,&status,0);printf("child get a signal :%d\n",status&0x7f);return 0;
}
4.2 SIGALRM
#?我們能夠通過alarm函數,設定一個鬧鐘,倒計時完畢向我們的進程發送SLGALRM
信號,其具體用法如下:
- 頭文件:#include<stdio.h>
- 函數原型:unsigned int alarm(unsigned int seconds);
- 參數:seconds表示倒計時的秒數。如果?
seconds
?為?0
,則表示取消之前設置的所有尚未觸發的?alarm
?定時器。- 返回值:如果調用alarm函數前,進程已經設置了鬧鐘,則返回上一個鬧鐘時間的剩余時間,并且本次鬧鐘的設置會覆蓋上一次鬧鐘的設置。如果調用alarm函數前,進程沒有設置鬧鐘,則返回值為0。
#?例如下面這段代碼,我們首先對SLGALRM
信號進行捕捉,并給出我們的自定義方法,然后1秒后調用alarm
函數。
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;exit(13);
}int main()
{signal(SIGALRM, handlerSig);alarm(1); // 設定1s鬧鐘,1s后,當前進程會收到信號SIGALRMwhile (true){int cnt = 0;std::cout << "count: " << cnt++ << std::endl; }return 0;
}
#?此時cnt的值為才18萬多?問題:我們的計算機不是運算次數都是上億次的嗎?
# 因為我們這里一直在cout打印,cout本質是向顯示器文件寫入,所以本質是?IO,并且我們用的是云服務器,通過網絡把云服務器上跑的代碼結果返回給顯示器,所以他的效率就比較低。
#?所以下面直接定義全局的?cnt?循環,不要?IO,直接cnt++,然后收到信號后先打印cnt,再終止進程。
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>int cnt = 0;void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << "count: " << cnt << std::endl;exit(13);
}int main()
{signal(SIGALRM, handlerSig);alarm(1); // 設定1s鬧鐘,1s后,當前進程會收到信號SIGALRMwhile (true){// cout本質是向顯示器文件寫入,所以是IO// vscode -> ./testSig -> 云服務器 -> 網絡 -> 顯示器// std::cout << "count: " << cnt++ << std::endl; // 打印效率不高!cnt++;}return 0;
}
#?然后我們就發現 cnt 就是5億多了,所以 IO 和純計算相差好幾個數量級,因為我們CPU進行 IO 時需要訪問外設,外設的速度就是比較慢的。
# 現在我們想設定一個鬧鐘,然后進程收到信號后不退出循環,一直打印,所以我們就可以看到一秒后鬧鐘發送信號打印語句,然后再也收不到信號了,說明我們的鬧鐘是一次性的。
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>void handlerSig(int sig)
{std::cout << "獲得了一個信號: " << sig << std::endl;
}int main()
{signal(SIGALRM, handlerSig);alarm(1);while (true){std::cout << "." << std::endl;sleep(1);}return 0;
}
#?問題:今天我們就是想讓鬧鐘每隔疫苗發送一次信號該怎么辦呢?此時可以在自定義捕捉信號方法里面再設置鬧鐘,然后發送一次信號后就會重新設置鬧鐘。所以這里我們每個一秒就接收到了一個信號,并且還是同一個進程在接收,因為pid一直都是一樣的。
# 我們可以讓進程一直pause暫停,然后每隔一秒發送信號,由信號驅動進程執行我們注冊的任務。
#include<iostream>
#include<vector>
#include<functional>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>////////// func //////////void Sched()
{std::cout << "我是進程調度" << std::endl;
}void MemManger()
{std::cout << "我是周期性的內存管理,正在檢查有沒有內存問題" << std::endl;
}void Fflush()
{std::cout << "我是刷新程序,我在定期刷新內存數據到磁盤" << std::endl;
}//////////////////////////using func_t = std::function<void()>;std::vector<func_t> funcs;// 每隔一秒,完成一些任務
void handlerSig(int sig)
{std::cout << "##############################" << std::endl;for(auto f : funcs)f();std::cout << "##############################" << std::endl;alarm(1);
}int main()
{funcs.push_back(Sched);funcs.push_back(MemManger);funcs.push_back(Fflush);signal(SIGALRM, handlerSig);alarm(1);while (true){// 讓進程什么都不做,就讓進程暫停,一旦來一個信號,就喚醒一次執行方法pause();}return 0;
}
# 而這就是操作系統的原理,操作系統也是一個死循環,在別人發送的信號的驅動下運行,它把鬧鐘時間設置地很小,此時操作系統就會非常高頻地執行任務。所以今天我們可以把進程的PCB鏈入一個鏈表中,然后調度時讓操作系統根據信號驅動遍歷鏈表,找到時間片消耗最少的進程來調度。