文章目錄
- 信號基礎
- 信號的產生
- OS中的時間
- 信號的保存
- sigset_t
- sigprocmask
- sigpending
- 信號的捕捉
- 用戶態和內核態
- sigaction
- volatile
- SIGCHLD
信號基礎
生活中的信號
你在網上買了很多件商品,再等待不同商品快遞的到來。但即便快遞沒有到來,你也知道快遞來臨時,你該怎么處理快遞。也就是你能“識別快遞”當快遞員到了你樓下,你也收到快遞到來的通知,但是你正在打游戲,需5min之后才能去取快遞。那么在在這5min之內,你并沒有下去去取快遞,但是你是知道有快遞到來了。也就是取快遞的行為并不是一定要立即執行,可以理解成“在合適的時候去取”。在收到通知,再到你拿到快遞期間,是有一個時間窗口的,在這段時間,你并沒有拿到快遞,但是你知道有一個快遞已經來了。本質上是你“記住了有一個快遞要去取”當你時間合適,順利拿到快遞之后,就要開始處理快遞了。而處理快遞一般方式有三種:1. 執行默認動作(幸福的打開快遞,使用商品)2. 執行自定義動作(快遞是零食,你要送給你你的女朋友)3. 忽略快遞(快遞拿上來之后,扔掉床頭,繼續開一把游戲)快遞到來的整個過程,對你來講是異步的,你不能準確斷定快遞員什么時候給你打電話。
總而言之
- 信號沒有產生的時候我們已經知道怎么處理這個信號了
- 信號的到來,我們并不清楚具體是什么時候,信號對于我現在正在左的工作是異步產生的。
- 信號產生了我們不一定要立即處理它,而是在合適的時候去處理
- 因為我們不一定會要立即處理它,所以我們要有對信號的保存能力
信號:信號是一種向目標進程發送通知消息的一種機制。
所以進程在收到信號之前已經知道了有哪些信號并且知道對應信號的處理方法。
在Linux中可以通過kill -l 查看所有的信號。
并且在進程能夠通過自己的PCB找到一張函數指針數組,數組的下標對應的就是各個信號的編號,數組的內容就是對應信號的處理方法。這么多的信號中1 - 34 號信號為普通信號,剩下的為實時信號,我們只說普通信號。
一個信號的處理方法分為三種:
- 默認行為
- 忽略
- 自定義
我們是可以通過signal修改對于信號的執行方法。 其中9號信號為管理員信號,默認方法不能被修改。
第二個參數設置為SIG_DFL就是默認行為,設置為SIGIGN就是忽略。
假設我們現在修改二號信號的默認行為
#include <iostream>
#include <unistd.h>
#include <signal.h>
void sigcb(int signal)
{std::cout << "get a singal :" << signal << std::endl;exit(0);
}
int main()
{signal(2,sigcb);while(true){std::cout << "run.." << std::endl;sleep(1);}return 0;
}
信號的產生
在命令行shell中,前臺命令(./xxx)只能有一個,后臺命令(./xxx &)可以有多個,前臺進程是不能被暫停(ctrl + z),如果被暫停,該前臺進程要立即被放到后臺。OS會自動的把shell自動的提到前臺或者后臺。ctrl + c一般情況下可以終止一個前臺進程。判斷是不是前臺進程可以看有沒有接受用戶輸入的能力,有就是前臺進程。
LInux中可以通過jobs命令查看后臺進程,fg + num 可以把一個后臺進程提到前臺,bg + num 可以啟動一個被暫停的后臺任務。
OS是怎么知道鍵盤有數據準備就緒了呢?
CPU其實和外設也是相連的,CPU上有很多針腳,硬件中有一個8269,作為針腳和硬件的中間設備,因為外設很多,CPU的針腳有限,所以可以通過這個設備把多的外設和CPU連接起來,然后當鍵盤有數據了,會通過針腳產生硬件中斷,OS中會有一張中斷向量表(函數指針數組),然后每個硬件都有自己的編號,CPU有一個寄存器專門存儲硬件的中斷號,數組的下標就是對于硬件的編號,數組的內容就是硬件的讀取方法,所以CPU接收到了硬件中斷,然后直接通過數組下標找到對于的方法,然后把內容加載到內存。
信號產生的方式:
-
可以通過鍵盤產生
ctrl + c (發送2號信號終止進程)
ctrl + z (暫停進程,發送19號信號)
ctrl + \ (終止進程,發送3號信號) -
通過系統調用
kill命令是調用kill函數實現的。kill函數可以給一個指定的進程發送指定的信號。raise函數可以給當前進程發送指定的信號(自己給自己發信號)。
abort函數使當前進程接收到信號而異常終止。 并且abort就算被signal重定義,就算最后我們沒有終止進程,它自己最后也會終止進程。
-
異常
硬件異常被硬件以某種方式被硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋為SIGFPE(8)信號發送給進程。再比如當前進程訪問了非法內存地址,MMU會產生異常,內核將這個異常解釋為SIGSEGV(11)信號發送給進程。
#include <iostream>
#include <signal.h>void handler(int signo)
{std::cout << "run.." << std::endl;
}int main()
{signal(8,handler);int a = 6;a /= 0;return 0;
}
這段代碼會出現死循環的情況 ,原因就是因為出現除0錯誤,然后CPU硬件報錯,然后處理方法就是讓OS給目標進程發信號并且把該進程剝離CPU,但是我們對8號信號進行自定義,沒有退出進程,然后當CPU再一次調度這個進程時,接著出錯,重復之前的動作。
- 軟件條件
管道的一種特性當讀端退出,寫端無意義,OS就會寫端發送SIGPIPE信號,SIGPIPE是一種由軟件條件產生的信號。除了這個以外還有alarm函數 和SIGALRM信號。
這個函數的返回值是0或者是以前設定的鬧鐘時間還余下的秒數。如果seconds值為0,表示取消以前設定的鬧鐘,函數的返回值仍然是以前設定的鬧鐘時間還余下的秒數。
總而言之信號產生的方式多種多樣,但是信號發送都是由OS來發送的。
OS中的時間
- 所有的用戶行為都是以進程的形式在OS中表現的。
- OS只要把進程管理號就能完成所有的用戶任務。
- CMOS會周期性高頻的像CPU發送時鐘中斷。
我們知道我們自己寫的代碼是由OS來調度執行的,但是OS的代碼是誰來調度的呢?
CMOS向CPU發送時鐘中斷就是讓CPU來執行OS的代碼的,他會給CPU一個操作數,然后OS通過這個操作數在中斷向量表中索引下標,數組的內容就是OS的調度方法,所以OS的執行是基于硬件中斷的。。
所以對OS樸素的理解就是OS在電腦開機時完成各種的初始化工作后,開始進入死循環執行自己的調度方法。
信號的保存
信號的其他概念
- 實際執行信號的處理動作稱為信號遞達(Delivery)
- 信號從產生到遞達之間的狀態,稱為信號未決(Pending)。
- 進程可以選擇阻塞 (Block )某個信號。一旦被阻塞,就不能遞達,直到對該信號解除屏蔽。
- 被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作.注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
信號在內核中其實是很好表示的,因為我們只需要表示是否收到了某某信號,所以用位圖這個數據結構就剛剛好。被阻塞也可以這樣表示,都是用位圖就可以表示。
每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。
所以當收到一個信號是,先看block是否被阻塞,如果沒有阻塞就會遞達,如果阻塞了,就需要等解除阻塞之后再遞達。
sigset_t
OS為了我們對信號集進行操作,設置了sigset_t的數據類型,它本質就是一個位圖,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。
為了對信號集更好的操作,OS也為我們提供了對信號集的操作函數。
- 函數sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含任何有效信號。
- 函數sigfillset初始化set所指向的信號集,使其中所有信號的對應bit置位,表示該信號集的有效信號包括系統支持的所有信號。
注意,在使用sigset_ t類型的變量之前,一定要調 用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態。初始化sigset_t變量之后就可以在調用sigaddset和sigdelset在該信號集中添加或刪除某種有效信號。這幾個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則返回1,不包含則返回0,出錯返回-1。
sigprocmask
調用函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
如果oset是非空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是非空指針,則 更改進程的信號屏蔽字,參數how指示如何更改。如果oset和set都是非空指針,則先將原來的信號 屏蔽字備份到oset里,然后根據set和how參數更改信號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。
sigpending
可以通過這個函數獲取當前進程的pending表。通過set參數傳出。
信號的捕捉
發送信號后,信號不會立即遞達,而是在合適的時候遞達,那什么才算合適的時候呢?
進程從內核態返回用戶態時,進行信號的檢測和處理。
用戶態和內核態
用戶態:只能訪問自己的0 - 3GB,是一種受控的狀態,能訪問的資源是有限的。
內核態:可以讓用戶以OS的身份訪問3 - 4GB,是一種OS的工作狀態,可以訪問大部分資源。
我們之前說的所有的地址空間都是用戶空間,里面都是對我們用戶自己的代碼,對應的還有一張用戶級頁表,而內核的進程地址空間都是OS的代碼數據和數據結結構,其中對應的還有一張內核級頁表,因為所有的進程都有自己的進程機地址空間,雖然用戶空間的使用情況可能千奇百怪,但是OS只有一個,所以他們所有的內核空間中的數據都是一樣的,并且在內存中也只會存在一張內存級頁表,所有的進程的內核空間的內容一樣,所以都指向同一張內核級頁表就可以了,我們平時調用函數實在自己的進程地址空間調用,系統調用也是代碼,是OS的代碼,他映射在內核級頁表中,所以我們普通用戶需要進行系統調用一定要發生身份的切換,因為普通用戶是不允許訪問內核級空間的,CPU中有一個CS寄存器,可以標識當前進程是用戶態還是內核態。所以不管是系統調用還是庫函數還是自己寫的函數都可以在自己的進程地址空間進行跳轉和返回,并且無論進程怎么切換,CPU都可以直接找到OS的代碼。
在調用自己的方法時,進程是要切換回用戶態的,因為如果不切換的用戶就可以在自定義方法中利用內核身份做不好的事情了。
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下: 用戶程序注冊了SIGQUIT信號的處理函數sighandler。 當前正在執行main函數,這時發生中斷或異常切換到內核態。 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。 sighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。信號的捕捉過程中,是要進行4次的身份切換的。
sigaction
sigaction函數可以讀取和修改與指定信號相關聯的處理動作。調用成功則返回0,出錯則返回- 1。signum是指定信號的編號。若act指針非空,則根據act修改該信號的處理動作。若oact指針非空,則通過oact傳出該信號原來的處理動作。act和oact指向sigaction結構體.
將sa_handler賦值為常數SIG_IGN傳給sigaction表示忽略信號,賦值為常數SIG_DFL表示執行系統默認動作,賦值為一個函數指針表示用自定義函數捕捉信號,或者說向內核注冊了一個信號處理函數,該函數返回值為void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。顯然,這也是一個回調函數,不是被main函數調用,而是被系統所調用。
當某個信號的處理函數被調用時,內核自動將當前信號加入進程的信號屏蔽字,當信號處理函數返回時自動恢復原來的信號屏蔽字,這樣就保證了在處理某個信號時,如果這種信號再次產生,那么 它會被阻塞到當前處理結束為止。 如果在調用信號處理函數時,除了當前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用sa_mask字段說明這些需要額外屏蔽的信號,當信號處理函數返回時自動恢復原來的信號屏蔽字。 sa_flags字段包含一些選項,把sa_flags設為0就行,sa_sigaction是實時信號的處理函數。
volatile
保持內存的可見性,告知編譯器,被該關鍵字修飾的變量,不允許被優化,對該變量的任何操作,都必須在真實的內存中進行操作
SIGCHLD
現在我們已經會創建子進程了,子進程在退出的時候什么都沒說嗎?
答案肯定是不是的,子進程在退出是是會給父進程發送SIGCHLD信號的。
我們會用wait和waitpid函數清理僵尸進程,父進程可以阻塞等待子進程結束,也可以非阻塞地查詢是否有子進
程結束等待清理(也就是輪詢的方式)。采用第一種方式,父進程阻塞了就不 能處理自己的工作了;
第二種方式,父進程在處理自己的工作的同時還要記得時不時地輪詢一 下,程序實現復雜。其實,子進程在終止時會給父進程發SIGCHLD信號,該信號的默認處理動作是忽略,父進程可以自定義SIGCHLD信號的處理函數,這樣父進程只需專心處理自己的工作,不必關心子進程了,子進程 終止時會通知父進程,父進程在信號處理函數中調用wait清理子進程即可。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void handler(int sig)
{pid_t id;while ((id = waitpid(-1, NULL, WNOHANG)) > 0){printf("wait child success: %d\n", id);}printf("child is quit! %d\n", getpid());
}
int main()
{signal(SIGCHLD, handler);pid_t cid;if ((cid = fork()) == 0){ // childprintf("child : %d\n", getpid());sleep(3);exit(1);}while (1){printf("doing some thing!\n");sleep(1);}return 0;
}
事實上,由于UNIX 的歷史原因,要想不產生僵尸進程還有另外一種辦法:父進程調 用sigaction將SIGCHLD的處理動作置為SIG_IGN,這樣fork出來的子進程在終止時會自動清理掉,不 會產生僵尸進程,也不會通知父進程。系統默認的忽略動作和用戶用sigaction函數自定義的忽略 通常是沒有區別的,但這是一個特例。此方法對于Linux可用,但不保證在其它UNIX系統上都可用。