??作者:一只大喵咪1201
??專欄:《Linux學(xué)習(xí)》
??格言:你只管努力,剩下的交給時(shí)間!
信號(hào)的產(chǎn)生以及詳細(xì)講解了,有興趣的小伙伴可以去看看,傳送門。接下來介紹信號(hào)的保存和信號(hào)處理。
首先介紹幾個(gè)新的概念:
- 信號(hào)遞達(dá)(Delivery):實(shí)際執(zhí)行信號(hào)的處理動(dòng)作。
- 信號(hào)未決(Pending):信號(hào)從產(chǎn)生到遞達(dá)之間的狀態(tài)。
- 信號(hào)阻塞(Block):被阻塞的信號(hào)產(chǎn)生時(shí)將保持在未決狀態(tài),直達(dá)解除對(duì)該信號(hào)的阻塞,才執(zhí)行遞達(dá)動(dòng)作。
注意: 阻塞和忽略是不同的,只要信號(hào)被阻塞就不會(huì)被遞達(dá),但是忽略是在遞達(dá)之后進(jìn)行的一種處理動(dòng)作。
??信號(hào)保存
我們知道,信號(hào)是保存在內(nèi)核數(shù)據(jù)結(jié)構(gòu)中的,下面來看它具體的儲(chǔ)存模型:
- pending表:用來存放接收到的信號(hào),操作系統(tǒng)向進(jìn)程發(fā)送信號(hào)時(shí),都會(huì)修改pending表中對(duì)應(yīng)編號(hào)處的比特位。
- block表:用來存放被阻塞的信號(hào),當(dāng)指定信號(hào)需要被阻塞時(shí),操作系統(tǒng)會(huì)修改block表中對(duì)應(yīng)編號(hào)處的比特位。
- handler表:這是是一個(gè)數(shù)組,用來存放不同信號(hào)的處理方法,保存的是函數(shù)指針。
當(dāng)我們使用signal注冊(cè)一個(gè)自定義處理方式時(shí),操作系統(tǒng)會(huì)將我們定義的函數(shù)指針放在handler表中,在信號(hào)遞達(dá)后調(diào)用。如果是默認(rèn)處理方式,會(huì)調(diào)用handler默認(rèn)的初始函數(shù)指針?biāo)鶎?duì)應(yīng)的函數(shù)。
- 信號(hào)產(chǎn)生后,操作系統(tǒng)就會(huì)修改pending位圖,使信號(hào)處于未決狀態(tài)。
操作系統(tǒng)會(huì)按照一定的順序來檢查block表和pending表,然后去調(diào)用相應(yīng)信號(hào)編號(hào)的處理方式來完成信號(hào)遞達(dá)。大概邏輯(偽代碼):
if(1<<(signo - 1) & pcb->block)
{
//signo信號(hào)被阻塞,不會(huì)被遞達(dá)
}
else
{
if(1<<(signo - 1) & pcb->pending)
{
//信號(hào)遞達(dá),處理該信號(hào)
handler[signo - 1];
}
}
操作系統(tǒng)在對(duì)信號(hào)進(jìn)行檢測的時(shí)候,先檢測的是信號(hào)的block位圖,如果對(duì)應(yīng)信號(hào)的比特位被置一,說明該信號(hào)被阻塞,就不再去檢測pending位圖。如果沒有被阻塞,才會(huì)去檢測pending位圖,如果相應(yīng)的位被置一,再去調(diào)用handler表中的處理函數(shù)。
結(jié)論: 如果一個(gè)信號(hào)沒有產(chǎn)生,但是并不妨礙它被阻塞。
被阻塞的信號(hào),在產(chǎn)生之后就會(huì)一直處于未決狀態(tài),不會(huì)被遞達(dá),只有當(dāng)阻塞被解除后才會(huì)被遞達(dá)。
- 默認(rèn)情況下,所有信號(hào)都是不被阻塞的,所有信號(hào)都沒有產(chǎn)生,也就是block位圖和pending位圖都是0。
??信號(hào)集操作
pending圖,block圖以及handler表是存放在內(nèi)核數(shù)據(jù)結(jié)構(gòu)中的,所以只能由操作系統(tǒng)來修改,我們用戶如果要修改也能通過操作系統(tǒng)來實(shí)現(xiàn),所以操作系統(tǒng)同樣給我們提供了系統(tǒng)調(diào)用。
- handler表中的函數(shù)指針可以通過系統(tǒng)調(diào)用signal來設(shè)置。
對(duì)于block位圖和pending位圖的修改,操作系統(tǒng)提供了一族系統(tǒng)調(diào)用,稱為信號(hào)集操作函數(shù)。
- sigset_t set:信號(hào)集變量。
- int signum:信號(hào)編號(hào)。
- 返回值:成功返回0,失敗返回-1。
信號(hào)集:
用戶在設(shè)置pending位圖和block位圖的時(shí)候,并不能直接讓系統(tǒng)調(diào)用將內(nèi)核中對(duì)于的比特位置一或清0,而是需要預(yù)先在一個(gè)變量中表達(dá)出我們的意愿,然后將這個(gè)變量通過系統(tǒng)調(diào)用給到操作系統(tǒng),再由操作系統(tǒng)去修改內(nèi)核數(shù)據(jù)結(jié)構(gòu)。
- 操作系統(tǒng)給我們提供了一個(gè)sigset_t的變量類型,用戶只需要對(duì)這個(gè)變量進(jìn)行預(yù)設(shè)置,然后再交給操作系統(tǒng)。
系統(tǒng)提供的信號(hào)集操作函數(shù)操作的也是也是這個(gè)域先處理的變量,之所以也用系統(tǒng)調(diào)用來處理這個(gè)變量,是因?yàn)檫@個(gè)變量不單單是一個(gè)32位的整形變量,它的結(jié)構(gòu)和內(nèi)核是對(duì)應(yīng)的,所以操作也要按照相應(yīng)的規(guī)則。
- 從使用者的角度不必關(guān)心具體是如何操作的,只需要使用信號(hào)集操作函數(shù)來操作sigset_t變量即可。
- sigset_t變量用其他方式是無法操作的,比如用printf去打印,這是沒有意義的。
具體操作:
- sigemptyset:使所有信號(hào)對(duì)應(yīng)的bit清零,表示該信號(hào)集不包含任何有效信號(hào)。
- sigfillset:使所有信號(hào)對(duì)應(yīng)的bit置位,表示該信號(hào)集的有效信號(hào)包括系統(tǒng)支持的所有信號(hào)。
- sigaddset:使指定信號(hào)所對(duì)應(yīng)的bit置位,表示該信號(hào)集中對(duì)應(yīng)信號(hào)有效。
- sigdetset:使指定信號(hào)所對(duì)應(yīng)的bit清零,表示該信號(hào)集中對(duì)應(yīng)信號(hào)無效。
- sigismember:判斷指定信號(hào)所對(duì)應(yīng)的bit是否有效,返回類型是bool類型。
- 在使用sigset_t類型的變量之前,一定要調(diào)用sigemptyset進(jìn)行初始化,使信號(hào)集處于確定狀態(tài)。
此時(shí)我們已經(jīng)對(duì)sigset_t變量預(yù)處理好了,下一步就是把這個(gè)變量交給操作系統(tǒng)了,操作系統(tǒng)同樣提供了對(duì)應(yīng)的系統(tǒng)調(diào)用。
sigprocmask():
該系統(tǒng)調(diào)用是專門用來修改內(nèi)核數(shù)據(jù)結(jié)構(gòu)中的block位圖的。
- int how:修改方式,有三個(gè)選項(xiàng):
SIG_BLOCK:在block原有位圖基礎(chǔ)上添加sigset_t變量中設(shè)置的比特位。
SIG_UNBLICK:在bolck原有位圖解除上刪除sigset_t變量中設(shè)置的比特位。
SIG_SETMASK:用sigset_t變量覆蓋原有的block位圖。一般使用這個(gè)。- set:我們?cè)O(shè)置好的sigset_t變量。
- oldeset:這是一個(gè)輸出型參數(shù),將原本block位圖輸出到這個(gè)sigset_t變量中。
- 返回值:設(shè)置成功返回0,失敗返回-1。
sigpending():
這是專門用來獲取內(nèi)核數(shù)據(jù)結(jié)構(gòu)中的pending位圖的。
- set:這是一個(gè)輸出型參數(shù),用來返回從內(nèi)核中獲取的pending位圖情況。
- 返回值:成功返回0,失敗返回-1。
此時(shí)我們可以利用上面的系統(tǒng)調(diào)用做一個(gè)小的實(shí)驗(yàn),來驗(yàn)證某個(gè)信號(hào)被阻塞后,它的pengding位圖會(huì)被置一,但是不會(huì)被遞達(dá)。
將編號(hào)為2號(hào)和3號(hào)的信號(hào)阻塞,并且用自定義處理方式來處理2號(hào)和3號(hào)信號(hào),一旦遞達(dá)就會(huì)被處理,打印出信號(hào)的編號(hào),但是不退出。
循環(huán)打印內(nèi)核數(shù)據(jù)結(jié)果中的pending位圖,觀察進(jìn)程在接收到2號(hào)和3號(hào)信號(hào)后的位圖變化。
- 在接收到2號(hào)信號(hào)以后,pending位圖的第二個(gè)比特位置一,表明該信號(hào)處于未決狀態(tài)。
- 在接收到3號(hào)信號(hào)以后,pending位圖的第三個(gè)比特位也置一,表明該信號(hào)也處于未決狀態(tài)。
- 無論哪個(gè)信號(hào)產(chǎn)生,都沒有遞達(dá),因?yàn)闆]有執(zhí)行自定義處理函數(shù)。
所以說,被阻塞的信號(hào),即使產(chǎn)生也是處于未決狀態(tài),不會(huì)被遞達(dá)。
在10秒鐘后,解除對(duì)指定信號(hào)的屏蔽。
- 原本2號(hào)和3號(hào)信號(hào)被阻塞,即使產(chǎn)生也處于未決狀態(tài),沒有被遞達(dá)。
- 當(dāng)解除阻塞以后,被阻塞的信號(hào)便遞達(dá)了,自定義處理方式中打印出了信號(hào)編號(hào)。
- 信號(hào)遞達(dá)后,對(duì)應(yīng)pending位圖中的比特位被自動(dòng)清零。
??信號(hào)處理
現(xiàn)在我們知道,進(jìn)程在接收到信號(hào)后并不是立刻處理的,而是在適當(dāng)?shù)臅r(shí)候,那這個(gè)適當(dāng)?shù)臅r(shí)候到底是什么時(shí)候呢?
- 從內(nèi)核態(tài)返回用戶態(tài)的時(shí)候信號(hào)遞達(dá)。
信號(hào)只是處理的話非常簡單,就是在執(zhí)行默認(rèn)的處理方式或者自定義方式,再或者是忽略,最重要的是信號(hào)處理的時(shí)機(jī),也就是信號(hào)的捕獲。
??捕捉信號(hào)
首先來看什么是內(nèi)核態(tài)和用戶態(tài):
- 用戶為了訪問內(nèi)核或者硬件資源,必須通過系統(tǒng)調(diào)用才能完成訪問。
- 用戶態(tài):正在執(zhí)行用戶層的代碼,此時(shí)CPU的狀態(tài)是用戶態(tài)。
- 內(nèi)核態(tài):正在通過系統(tǒng)調(diào)用訪問內(nèi)核或者硬件資源時(shí),此時(shí)CPU的狀態(tài)是內(nèi)核態(tài)。
雖然系統(tǒng)調(diào)用是在我們的代碼中寫的,也就是用戶在使用,但是具體的執(zhí)行者是內(nèi)核,也就是操作系統(tǒng)。
現(xiàn)在是知道了什么是用戶態(tài),什么是內(nèi)核態(tài),但是操作系統(tǒng)是怎么知道當(dāng)前進(jìn)程的身份狀態(tài)的呢?
CPU中的寄存器雖然只有一套,但是有很多,有可見寄存器,如eax,ebx等等,還有很多的不可見寄存器,凡是和當(dāng)前進(jìn)程強(qiáng)相關(guān)的,都屬于當(dāng)前進(jìn)程的上下文數(shù)據(jù)。
如上圖中:
- 有專門用來存放當(dāng)前進(jìn)程PCB指針的寄存器。
- 也有專門存放當(dāng)前進(jìn)程頁表指針的寄存器。
- CR3寄存器:專門用來表征當(dāng)前進(jìn)程的運(yùn)行級(jí)別的。
0:表示內(nèi)核態(tài),此時(shí)訪問的是內(nèi)核資源或者硬件。
3:表示用戶態(tài),此時(shí)執(zhí)行的是用戶層的代碼。
操作系統(tǒng)是一個(gè)進(jìn)行軟硬件資源管理的軟件,它很容易就可以獲取到CPU中CR3寄存器中是0還是3,從而知道當(dāng)前是用戶態(tài)還是內(nèi)核態(tài)。
執(zhí)行系統(tǒng)調(diào)用時(shí),執(zhí)行者是操作系統(tǒng),而不是用戶。那么又存在一個(gè)問題,一個(gè)進(jìn)程是怎么跑到操作系統(tǒng)中執(zhí)行代碼的呢?
對(duì)進(jìn)程地址空間進(jìn)行一個(gè)補(bǔ)充介紹:
-
我們之前一直所說的頁表都是用戶級(jí)頁表,每個(gè)進(jìn)程都有一個(gè)。
進(jìn)程地址空間的大小一共有4GB,我們之前談?wù)摰闹挥?~3GB,這3GB的空間屬于用戶空間,用來存放用戶的代碼,數(shù)據(jù)等。為了保證進(jìn)程的獨(dú)立性,每個(gè)進(jìn)程都有一個(gè)進(jìn)程地址空間,都有一個(gè)用戶級(jí)頁表。 -
還有一共內(nèi)核級(jí)頁表,所有進(jìn)程共用一份。
進(jìn)程地址空間中的3~4GB空間,是不允許用戶訪問的,因?yàn)檫@1GB空間中的數(shù)據(jù)等,通過內(nèi)核級(jí)頁表和內(nèi)存中的操作系統(tǒng)相映射,屬于內(nèi)核級(jí)別的。因?yàn)閮?nèi)存中只存在一份內(nèi)核,所以所有進(jìn)程的虛擬地址空間的這1GB空間都通過同一份內(nèi)核級(jí)頁表和內(nèi)存中的內(nèi)核相映射。
- 每一個(gè)進(jìn)程地址空間中的3~4GB的內(nèi)容都是一樣的,因?yàn)樗鼈兌纪ㄟ^同一個(gè)內(nèi)核級(jí)頁表和內(nèi)存中的內(nèi)核相映射。
還記得動(dòng)態(tài)鏈接嗎?通過代碼段的位置無關(guān)碼跳轉(zhuǎn)到共享區(qū)從內(nèi)存中映射過來的動(dòng)態(tài)庫來執(zhí)行相應(yīng)的方法。系統(tǒng)調(diào)用和它的原理一樣:
- 當(dāng)執(zhí)行到代碼段中的系統(tǒng)調(diào)用時(shí),會(huì)在跳轉(zhuǎn)到當(dāng)前進(jìn)程虛擬地址空間中的內(nèi)核空間中。
- 系統(tǒng)調(diào)用的具體實(shí)現(xiàn)都放在這1GB的內(nèi)核空間中。
- 然后根據(jù)內(nèi)核級(jí)頁表和內(nèi)存中內(nèi)核的映射關(guān)系實(shí)現(xiàn)內(nèi)核的訪問。
此時(shí)又有一個(gè)問題,為什么我們的代碼中不能訪問這3~4GB的空間,而系統(tǒng)調(diào)用就跳轉(zhuǎn)到這1GB的內(nèi)核空間中進(jìn)行訪問了呢?我們都是用戶的代碼啊?
- 因?yàn)閺拇a段跳轉(zhuǎn)到內(nèi)核空間中后,CPU中的CR3寄存器從3變成了0。
- 意味著進(jìn)程運(yùn)行級(jí)別從用戶態(tài)變成了內(nèi)核態(tài),也就是執(zhí)行者從用戶變成了操作系統(tǒng),所以可以對(duì)這1GB的內(nèi)核空間進(jìn)行訪問。
- 系統(tǒng)調(diào)用接口的起始位置,會(huì)將CR3寄存器中的數(shù)據(jù)從3變成0,完成從用戶態(tài)向內(nèi)核態(tài)的轉(zhuǎn)變。
所以說,系統(tǒng)調(diào)用前一部分是由用戶在執(zhí)行,其余部分由操作系執(zhí)行。
此時(shí)再來理解信號(hào)處理的時(shí)機(jī)—從內(nèi)核態(tài)返回到用戶態(tài),這句話的含義:
- 必然曾經(jīng)進(jìn)入到了內(nèi)核態(tài),而進(jìn)入內(nèi)核態(tài)的方式很多,比如進(jìn)程切換,只有操作系統(tǒng)才有權(quán)力將進(jìn)程從CPU上剝離下來換上另一個(gè)進(jìn)程。還有系統(tǒng)調(diào)用,等等方式。
以我們最熟悉的系統(tǒng)調(diào)用為例:
以黑色長線為界,上面是用戶態(tài),下面是內(nèi)核態(tài)。
- 當(dāng)執(zhí)行到用戶代碼段中的系統(tǒng)調(diào)用時(shí),會(huì)跳轉(zhuǎn)掉虛擬地址空間中的內(nèi)核空間去執(zhí)行具體的方法,此時(shí)從用戶態(tài)變成了內(nèi)核態(tài)。
- 當(dāng)系統(tǒng)調(diào)用被操作系統(tǒng)執(zhí)行完畢以后,在返回之前(來一趟挺不容易的),操作系統(tǒng)會(huì)檢測task_struct中block位圖,pending位圖,然后再根據(jù)handler中的處理方式去處理相應(yīng)的信號(hào)。
- 如是自定義處理方式,操作系統(tǒng)會(huì)拿著handler表中的函數(shù)地址,通過特定的系統(tǒng)調(diào)用去執(zhí)行用戶自定義的處理方式,此時(shí)從內(nèi)核態(tài)變成了用戶態(tài)。
- 在執(zhí)行完自定義處理方式以后,再次回到內(nèi)核中取系統(tǒng)調(diào)用得到的數(shù)據(jù),此時(shí)再次從用戶態(tài)變成了內(nèi)核態(tài)。
- 拿上要取的數(shù)據(jù)以后,通過特定的系統(tǒng)調(diào)用返回到用戶代碼中系統(tǒng)調(diào)用的位置,再次從內(nèi)核態(tài)變成了用戶態(tài)。
上面過程的偽代碼形式:
- 涉及到的系統(tǒng)調(diào)用無需詳細(xì)了解,只需要知道是通過系統(tǒng)調(diào)用實(shí)現(xiàn)的即可。
上面過程中存在一個(gè)問題,在執(zhí)行自定義處理方式的時(shí)候,為什么必須從內(nèi)核態(tài)切換成用戶態(tài)去執(zhí)行用戶定義的處理方式呢?不能直接以內(nèi)核態(tài)的身份去執(zhí)行嗎?
- 不可以。理論上是絕對(duì)可以實(shí)現(xiàn)的,因?yàn)閮?nèi)核態(tài)比用戶態(tài)高,高級(jí)別去處理低級(jí)別肯定是可以的。
- 但是操作系統(tǒng)不相信任何人,如果自定義處理方式中有用戶的惡意代碼,而此時(shí)又以操作系統(tǒng)身份去執(zhí)行,那么就會(huì)導(dǎo)致問題。
- 所以必須得切換到用戶身份去執(zhí)行自定義處理方式才能保證系統(tǒng)的安全。
兩個(gè)獨(dú)立的流程:
此時(shí)就存在了兩個(gè)流程,一個(gè)是main函數(shù)所在的執(zhí)行流程,一個(gè)是自定義處理方式的執(zhí)行流程:
- 在執(zhí)行完系統(tǒng)調(diào)用后不是恢復(fù)main函數(shù)的上下文進(jìn)行執(zhí)行,而是執(zhí)行用戶自定義的處理方式。
- 自定義處理方式函數(shù)和main函數(shù)使用不同的堆棧空間,并且不存在調(diào)用和被調(diào)用的關(guān)系,是兩個(gè)獨(dú)立的控制流程。
- 上面整個(gè)過程可以看成一個(gè)無窮大符號(hào)加一條線,線的上邊是用戶態(tài),下邊是內(nèi)核態(tài)。
- 每經(jīng)過一次黑線就會(huì)發(fā)生一次身份狀態(tài)的改變,一共改變了四次。
上面這種自定義處理方式是最復(fù)雜的情況,如果是SIG_DFL(默認(rèn)處理方式)和SIG_IGN(忽略方式),以內(nèi)核態(tài)身份就可以處理,然后就可以直接返回到用戶代碼中系統(tǒng)調(diào)用的位置,少了兩次身份的轉(zhuǎn)變。
- 因?yàn)槟J(rèn)方式和忽略方式是被寫入到操作系統(tǒng)中的,被操作系統(tǒng)所信任的方式。
- 默認(rèn)處理方式:所有信號(hào)的默認(rèn)處理方式都是結(jié)束進(jìn)程,只是不同信號(hào)表示不同的異常。
- 忽略處理方式:忽略和阻塞不一樣,忽略也是一種處理方式,它僅僅是將task_struct中的pending位圖中對(duì)應(yīng)信號(hào)的比特位清空,然后就直接返回到用戶態(tài)了。
??系統(tǒng)調(diào)用sigaction():
- int signum:信號(hào)編號(hào)。
- act:這是一個(gè)結(jié)構(gòu)體變量,結(jié)構(gòu)體中包括多個(gè)屬性,sa_handler賦值自定義處理方式,暫時(shí)將sa_flags都設(shè)為0,其他暫時(shí)不用管。
- oldact:是一個(gè)輸出型的結(jié)構(gòu)體變量,將原本的捕捉方式放入這個(gè)結(jié)構(gòu)體變量中。
- 返回值:成功返回0,失敗返回-1。
- 將自定義處理方式賦值給結(jié)構(gòu)體變量act中的sa_handler。
- 使用系統(tǒng)調(diào)用sigaction注冊(cè)自定義處理方式。
- 在自定義處理函數(shù)中,打印捕捉到的信號(hào)編號(hào),然后進(jìn)行10s種延時(shí)。
在進(jìn)程開始運(yùn)行后,我們?cè)?0s內(nèi)發(fā)送了很多次2號(hào)信號(hào),但是最終只捕獲了兩次。
- 當(dāng)遞達(dá)第一個(gè)2號(hào)信號(hào)的時(shí)候,同類型的信號(hào)無法被遞達(dá)。
- 因當(dāng)前信號(hào)在被捕捉的時(shí)候,系統(tǒng)會(huì)自動(dòng)將當(dāng)前信號(hào)加入到進(jìn)程的信號(hào)屏蔽字,也就是將block對(duì)應(yīng)的比特位置位,然后將pending表對(duì)應(yīng)比特位清空,再去進(jìn)行遞達(dá)。
- 但是第二個(gè)2號(hào)信號(hào)在第一個(gè)信號(hào)被捕捉的時(shí)候會(huì)將對(duì)應(yīng)pending位圖的比特位置位。
- 所以當(dāng)?shù)谝粋€(gè)2號(hào)信號(hào)處理完畢以后,解除對(duì)2號(hào)信號(hào)的屏蔽后,第二個(gè)2號(hào)信號(hào)就會(huì)被遞達(dá)。
- 除了這兩個(gè)2號(hào)信號(hào),其余的2號(hào)信號(hào)都被舍棄了。
注意: 進(jìn)程處理信號(hào)的原則是串行的處理同類型的信號(hào),不允許遞歸,所以同類型的多個(gè)信號(hào)同時(shí)產(chǎn)生,最多可以處理兩個(gè)。
上面內(nèi)容,系統(tǒng)調(diào)用signal也可以實(shí)現(xiàn),那么sigaction相對(duì)于signal有什么優(yōu)勢呢?
剛剛代碼中,由于在2號(hào)信號(hào)的自定義處理中沒有結(jié)束進(jìn)程,所以只能用其他信號(hào)來結(jié)束這個(gè)進(jìn)程,如上圖中使用的是3號(hào)信號(hào)。
- 如果想要在捕獲2號(hào)信號(hào)以后,將3號(hào)信號(hào)也屏蔽了呢?
- 此時(shí)就需要設(shè)置結(jié)構(gòu)體變量act中的sa_mask成員。
還是使用上面的代碼,只是在act結(jié)構(gòu)體變量中sa_mask成員中增加了3號(hào)信號(hào),并且給3號(hào)信號(hào)注冊(cè)了自定義處理方式。
在10s內(nèi),多次發(fā)送2號(hào)和3號(hào)信號(hào)。
- 當(dāng)?shù)谝淮?號(hào)信號(hào)被捕獲后,第二個(gè)2號(hào)信號(hào)雖然被阻塞了,但是它還是讓pending位圖置位了。
- 當(dāng)?shù)谝淮?號(hào)信號(hào)被遞達(dá)完成后,就會(huì)遞達(dá)第二個(gè)2號(hào)信號(hào)。
- 當(dāng)?shù)诙?號(hào)信號(hào)被遞達(dá)完成后,2號(hào)信號(hào)的pending位圖的比特位是0,所以才遞達(dá)3號(hào)信號(hào)。
- 雖然在捕獲2號(hào)信號(hào)的同時(shí)會(huì)阻塞3號(hào)信號(hào),但是3號(hào)的pending位圖的比特位仍然被置位了。
在第一個(gè)2號(hào)信號(hào)被捕獲的時(shí)候,同時(shí)阻塞了第二個(gè)2號(hào)信號(hào)和3號(hào)信號(hào),此時(shí)pending位圖的第二個(gè)和第三個(gè)比特位都是1,但是當(dāng)?shù)谝粋€(gè)2號(hào)信號(hào)遞達(dá)完成后,先處理的是第二個(gè)2號(hào)信號(hào)而不是3號(hào)信號(hào)。
- 一般一個(gè)信號(hào)被解除屏蔽的時(shí)候,會(huì)自動(dòng)遞達(dá)這個(gè)信號(hào),如果該信號(hào)pending位圖的比特位是1的話就會(huì)遞達(dá),是0的話就不做任何處理。
??不可重入函數(shù)
如上圖所示鏈表,在插入節(jié)點(diǎn)的時(shí)候捕獲到了信號(hào),并且該信號(hào)的自定義處理方式中也調(diào)用了插入節(jié)點(diǎn)的函數(shù)。
- 在main函數(shù)中,使用insert向鏈表中插入一個(gè)節(jié)點(diǎn)node1,在執(zhí)行insert的時(shí),剛讓頭節(jié)點(diǎn)指向node1以后(如上圖序號(hào)1),捕獲到了信號(hào),進(jìn)入到了該信號(hào)的自定義處理方式中。
- 在自定義處理方式中,同樣調(diào)用了insert函數(shù)向鏈表中插入一個(gè)節(jié)點(diǎn)node2,此時(shí)完整的執(zhí)行了insert函數(shù),但是在頭節(jié)點(diǎn)和最開始那個(gè)節(jié)點(diǎn)之間同時(shí)有了node1和node2(如上圖序號(hào)2和3)。
- 當(dāng)?shù)诙握{(diào)用insert中讓頭節(jié)點(diǎn)指向node2后(如上圖序號(hào)3),流程返回到信號(hào)的自定義處理函數(shù)中,然后再返回到第一次調(diào)用insert處,頭節(jié)點(diǎn)指向node1(如上圖序號(hào)4)。
- 最后可以看到,該鏈表是丟了一個(gè)節(jié)點(diǎn)的。
- 重入:像insert函數(shù)這樣,在main流程中調(diào)用還沒有返回時(shí)就再次被handler流程調(diào)用再次進(jìn)入該函數(shù)。
insert函數(shù)訪問的是一個(gè)全局鏈表,有可能會(huì)因?yàn)橹厝牒驮斐慑e(cuò)亂,像insert這樣的函數(shù)就稱為不可重入函數(shù)。
如果一個(gè)函數(shù)只訪問自己的局部變量或參數(shù),則不會(huì)造成錯(cuò)亂,此時(shí)這樣的函數(shù)就稱為可重入函數(shù)。
注意: 可/不可重入是函數(shù)的特性,是中性的,并不是問題,所以也不需要被解決。
我們目前使用的大部分結(jié)構(gòu)都是不可以重入函數(shù)?。?!。
符合以下條件之一的就是不可重入函數(shù):
- 調(diào)用了malloc或者free,因?yàn)閙alloc也是用全局鏈表來管理堆的。
- 調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù),標(biāo)準(zhǔn)I/O庫的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)。
??volatile關(guān)鍵字
int quit = 0;
void handler(int signo)
{
printf("pid:%d,捕捉到的信號(hào)編號(hào)是:%d\n",getpid(),signo);
printf("quid:%d",quit);
quit = 1;
printf("->%d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("pid:%d,我是正常退出的\n",getpid());
return 0;
}
定義全局變量quit,當(dāng)quit是0的時(shí)候,一直進(jìn)行while循環(huán),當(dāng)quit變成1的時(shí)候,結(jié)束循環(huán),進(jìn)程正常退出。
信號(hào)2注冊(cè)自定義處理方式,在函數(shù)中將全局變量改成1,讓main函數(shù)控制的流程正常結(jié)束。
在接收到2號(hào)信號(hào)后,quit從0變成1,所以main流程也正常結(jié)束了,不再循環(huán)。
我們的編譯器會(huì)進(jìn)行很多的優(yōu)化,比如debug版本和relase版本中的assert就會(huì)被優(yōu)化。在使用g++編譯器的時(shí)候,可以指定g++的優(yōu)化級(jí)別。
g++ -o $@ $^ -O3
指定使用級(jí)別為3的編譯器優(yōu)化選項(xiàng)。
仍然是上面代碼,運(yùn)行起來后,發(fā)送2號(hào)信號(hào),quit是從0變成了1,但是進(jìn)程并沒有結(jié)束,還是在運(yùn)行,再次發(fā)送2號(hào)信號(hào),quit從1變成1,進(jìn)程還在繼續(xù)。
- 此時(shí)可以肯定quit被改成了1,但是while(!quit)還是在循環(huán),沒有停下來。
上訴現(xiàn)象的原因是什么?肯定是和優(yōu)化有關(guān),因?yàn)槲覀兗恿?O3選項(xiàng)。
- quit在物理內(nèi)存中一定有一塊空間,最開始是0。
- 當(dāng)CPU指向while(!quit);指令的時(shí)候,會(huì)通過虛擬地址和頁表的映射將物理內(nèi)存中的quit數(shù)據(jù)取到CPU的寄存器中。
- 當(dāng)quit被修改后,物理空間中的數(shù)據(jù)就會(huì)從0變成1。
在沒有優(yōu)化前,CPU每次都是從物理內(nèi)存中拿到quit的數(shù)據(jù),再去指向while循環(huán),所以當(dāng)quit從0變成1后,CPU中寄存器的數(shù)據(jù)也會(huì)及時(shí)從0變成1,所以while循環(huán)會(huì)停下來。
但是采用優(yōu)化方案后:
- 在main控制的執(zhí)行流中,quit沒有進(jìn)行修改,也沒有寫入,只是被讀取,所以在第一次將從物理空間讀取到寄存器中便不再讀取了,每次執(zhí)行while時(shí)候都是使用的寄存器中的quit值,所以始終都是0。
- 在handler執(zhí)行流中,對(duì)quit進(jìn)行了修改,所以物理內(nèi)存中的quit從0變成了1。
導(dǎo)致上面現(xiàn)象的原因就是CPU執(zhí)行while時(shí)的quit和物理內(nèi)存中的quit不是一個(gè)值。
- 為了讓CPU的寄存器每次都從物理內(nèi)存中取數(shù)據(jù),使用volatile關(guān)鍵字來修飾這個(gè)quit變量。
可以看到,此時(shí)在handler的執(zhí)行流中修改了quit值,并且CPU中該值也得到了及時(shí)更新,所以程序可以正常結(jié)束。
??SIGCHLD信號(hào)
在學(xué)習(xí)進(jìn)程控制的時(shí)候,使用wait和waitpid系統(tǒng)調(diào)用何以回收僵尸進(jìn)程,父進(jìn)程可以阻塞等待,也可以非阻塞等待,采用輪詢的方式不停查詢子進(jìn)程是否退出。
- 采用阻塞式等待,父進(jìn)程就被阻塞了,什么都干不了,只能等子進(jìn)程退出。
- 采用非阻塞式等待,父進(jìn)程在干自己事的同時(shí)還要時(shí)不時(shí)的輪詢一下,程序?qū)崿F(xiàn)比較復(fù)雜。
實(shí)際上,子進(jìn)程的退出并不是悄無聲息的,在子進(jìn)程退出時(shí),會(huì)發(fā)出SIGCHLD信號(hào)給父進(jìn)程。
給SIGCHLD信號(hào)注冊(cè)自定義處理方式,打印捕捉到的信號(hào)編號(hào)。父進(jìn)程創(chuàng)建處子進(jìn)程后,子進(jìn)程在5次循環(huán)后退出,父進(jìn)程始終循環(huán)。
可以看到,子進(jìn)程在退出時(shí),發(fā)出了編號(hào)為17的SIGCHLD信號(hào),被父進(jìn)程捕捉到了。
- 我們就可以通過在17號(hào)信號(hào)的自定義處理函數(shù)中進(jìn)行進(jìn)程等待來回收子進(jìn)程。
- 采用這種方式時(shí),main執(zhí)行流中的父子進(jìn)程都不會(huì)收到影響,當(dāng)子進(jìn)程退出時(shí),handler執(zhí)行流進(jìn)行進(jìn)程等待,回收子進(jìn)程資源。
waitpid的第一個(gè)參數(shù)可以填-1,此時(shí)只要是該父進(jìn)程的子進(jìn)程退出就會(huì)被回收。
在17號(hào)信號(hào)的自定義處理函數(shù)中,循環(huán)回收所有子進(jìn)程,只要是子進(jìn)程都會(huì)被循環(huán)回收。
可以看到,父進(jìn)程既回收了退出的子進(jìn)程,而且還不影響父進(jìn)程干自己的事,因?yàn)榛厥展ぷ魇窃赟IGCHLD信號(hào)的自定義處理函數(shù)中進(jìn)行的。
SIGCHLD信號(hào)的默認(rèn)處理方式是Ign,也就是忽略的意思。
- 當(dāng)子進(jìn)程退出后,父進(jìn)程什么都沒有干,子進(jìn)程就會(huì)變成僵尸狀態(tài)。
文章來源:http://www.zghlxwxcb.cn/news/detail-413339.html
- 當(dāng)顯式SIGCHLD信號(hào)使用忽略方式(SIG_IGN)時(shí),退出的子進(jìn)程就會(huì)被自動(dòng)回收。
- 雖然SIGCHLD默認(rèn)的處理方式就是忽略,但是默認(rèn)的忽略不會(huì)回收子進(jìn)程,只有顯式注冊(cè)為SIG_IGN(忽略)方式才會(huì)自動(dòng)回收退出的子進(jìn)程。
??總結(jié)
至此,加上上一篇文章,信號(hào)的整個(gè)生命周期都介紹完了,重點(diǎn)在于新的產(chǎn)生,信號(hào)保存,以及信號(hào)捕捉上面,其它衍生的知識(shí)了解即可。文章來源地址http://www.zghlxwxcb.cn/news/detail-413339.html
到了這里,關(guān)于【Linux學(xué)習(xí)】信號(hào)——信號(hào)保存 | 信號(hào)處理 | 不可重入函數(shù),volatile,SIGCHLD信號(hào)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!