被愛(ài)情困住的是傻子
一、信號(hào)的預(yù)備知識(shí)
1.通過(guò)生活例子來(lái)理解信號(hào)
1.
關(guān)于信號(hào)這個(gè)話(huà)題我們其實(shí)并不陌生,早在以前的時(shí)候,我們想要?dú)⑺滥硞€(gè)后臺(tái)進(jìn)程的時(shí)候,無(wú)法通過(guò)ctrl+c熱鍵終止進(jìn)程時(shí),我們就會(huì)通過(guò)kill -9的命令來(lái)殺死信號(hào)。
查看信號(hào)也比較簡(jiǎn)單,通過(guò)kill -l命令就可以查看信號(hào)的種類(lèi),雖然最大的信號(hào)編號(hào)是64,但實(shí)際上所有信號(hào)只有62個(gè)信號(hào),1-31是普通信號(hào),34-64是實(shí)時(shí)信號(hào),這篇博文不對(duì)實(shí)時(shí)信號(hào)做討論,只討論普通信號(hào),感興趣的老鐵可以自己下去研究一下。
2.
在生活中不乏關(guān)于信號(hào)的場(chǎng)景,比如紅綠燈,鬧鐘,手機(jī)消息提示音,上課的鈴聲,田徑場(chǎng)起跑的槍聲等等,那么信號(hào)從產(chǎn)生到被處理的具體過(guò)程是怎么樣的呢?
我們拿紅綠燈來(lái)舉例子,首先我們能夠認(rèn)識(shí)紅綠燈其實(shí)是因?yàn)橛腥私逃^(guò)我們,我們的大腦中有紅燈停綠燈行的意識(shí),其次如果我們站在馬路對(duì)面,現(xiàn)在已經(jīng)綠燈亮起了,我們可以選擇忽略這件事,也可以選擇先不管他,我現(xiàn)在正打王者呢,綠燈亮不亮和我沒(méi)關(guān)系,我都被推到高地了,此時(shí)由于我們有著更重要的事情要去做,所以我們先把處理紅綠燈信號(hào)這件事放在腦后了,也就是他的優(yōu)先級(jí)比較低,等會(huì)兒再說(shuō)紅綠燈的事情,但我們的腦海中是有紅綠燈這回事的,這個(gè)信號(hào)被保存在了我們的腦海里,等我們王者打完之后,我們想起來(lái)原來(lái)紅綠燈亮了啊,此時(shí)我們忙完別的事之后,我們要進(jìn)行過(guò)馬路了,也就是處理紅綠燈信號(hào),處理時(shí)我們也可以分為三種處理行為,一般情況下的默認(rèn)行為就是大家都綠燈過(guò)馬路了,那我也跟著過(guò)馬路吧,另一種行為就是忽略,我們處理紅綠燈信號(hào)這件事了嗎?處理了,我們選擇忽略這件事,繼續(xù)開(kāi)下一把排位賽,這也是我們的處理行為。最后一種就是自定義行為,假設(shè)你的媽媽從小告訴你在綠燈亮的時(shí)候,你要先在馬路邊跳一段舞,然后在過(guò)馬路,所以當(dāng)別人綠燈亮的時(shí)候,其他人的默認(rèn)行為就是直接過(guò)馬路,你先來(lái)旁邊跳起來(lái)了,這就是自定義行為。
所以在信號(hào)產(chǎn)生和信號(hào)被遞達(dá)處理之間,還有一個(gè)時(shí)間窗口,這個(gè)時(shí)間窗口其實(shí)就是用來(lái)保存信號(hào),因?yàn)槲耶?dāng)前正在做別的事情呢,處理紅綠燈什么的等會(huì)兒再說(shuō),等我忙完的。忙完之后,在進(jìn)行信號(hào)處理時(shí),我們的行為可以選擇默認(rèn)或忽略或自定義等行為。
從我們暫時(shí)不處理信號(hào),把這件事先排到后面來(lái)看,其實(shí)可以看出,信號(hào)的產(chǎn)生和我們當(dāng)前做的事情是異步的,也就是說(shuō),兩者相當(dāng)于兩個(gè)執(zhí)行流,互相是不影響的,信號(hào)該發(fā)送就發(fā)送,我該做啥事就做啥事,等我做完之后再去處理你這個(gè)信號(hào)。
2.遷移到進(jìn)程上來(lái)理解信號(hào)
1.
如果將這樣的生活例子遷移到進(jìn)程上呢?其實(shí)道理是類(lèi)似的。
進(jìn)程為什么能夠認(rèn)識(shí)信號(hào)呢?其實(shí)是由于編寫(xiě)系統(tǒng)代碼的程序員所規(guī)定的,程序員讓進(jìn)程能夠?qū)Σ煌男盘?hào)產(chǎn)生不同的響應(yīng)。進(jìn)程本質(zhì)上就是程序員所寫(xiě)的屬性和邏輯的集合,信號(hào)的含義都是程序員所賦予的,而且進(jìn)程這樣的數(shù)據(jù)結(jié)構(gòu)也是程序員所建立出來(lái)的,所以進(jìn)程能夠認(rèn)識(shí)信號(hào),本質(zhì)上就是程序員告訴他的。
信號(hào)是發(fā)送給進(jìn)程的,那么進(jìn)程能不能先不處理這個(gè)信號(hào)呢?比如當(dāng)前進(jìn)程正在處理別的信號(hào),或者進(jìn)程此時(shí)被掛起了并未處于運(yùn)行狀態(tài),那么如果這個(gè)時(shí)候操作系統(tǒng)給進(jìn)程發(fā)送信號(hào)呢?進(jìn)程都不運(yùn)行了,還處理啥信號(hào)?。恐T如以上這樣的情況,進(jìn)程都不會(huì)立即處理該信號(hào),那么在到信號(hào)被遞達(dá)處理之前這段時(shí)間窗口,信號(hào)就會(huì)被保存起來(lái),等到進(jìn)程在合適的時(shí)候去遞達(dá)處理該信號(hào)。所以,進(jìn)程當(dāng)前正在做的事情和處理信號(hào)這件事依舊是異步的。
當(dāng)進(jìn)程已經(jīng)到了合適的時(shí)候,進(jìn)程會(huì)去處理這個(gè)信號(hào),處理的行為也是三種,默認(rèn),忽略,自定義,大概有60%的信號(hào)的默認(rèn)處理動(dòng)作都是Term終止進(jìn)程。進(jìn)程處理信號(hào)這個(gè)動(dòng)作,有專(zhuān)業(yè)的名詞叫信號(hào)捕捉或者是信號(hào)遞達(dá)。
2.
在有了上面的認(rèn)識(shí)之后,我們可以用現(xiàn)有的知識(shí)推導(dǎo)出一些結(jié)論。
我們知道信號(hào)是發(fā)送給進(jìn)程的,如果進(jìn)程當(dāng)前并不處理這個(gè)信號(hào),那么信號(hào)就需要被保存,以便于將來(lái)在合適的時(shí)候處理該信號(hào),那么這個(gè)信號(hào)應(yīng)該被保存在哪里呢?其實(shí)應(yīng)該被保存在PCB struct task_struct{}里面,進(jìn)程收到了哪些信號(hào),進(jìn)程要對(duì)信號(hào)做怎樣的處理,這些信息都屬于進(jìn)程的信息,那么這些信息就理應(yīng)被保存在PCB里面。
話(huà)又說(shuō)回來(lái),既然信號(hào)需要被保存,那么信號(hào)應(yīng)該被保存在哪里呢?其實(shí)在PCB里面有對(duì)應(yīng)的信號(hào)位圖,操作系統(tǒng)用信號(hào)位圖來(lái)保存信號(hào)的,31個(gè)普通信號(hào),我們可以選擇用32個(gè)比特位的unsigned int signal整數(shù)來(lái)進(jìn)行保存。比特位的編號(hào)代表信號(hào)的編號(hào),比特位的0或1代表進(jìn)程是否接收到該信號(hào)。
3.
那么信號(hào)發(fā)送其實(shí)就好理解了,所謂的信號(hào)發(fā)送實(shí)質(zhì)上就是修改PCB種對(duì)應(yīng)的信號(hào)位圖結(jié)構(gòu),將對(duì)應(yīng)的比特位編號(hào)由0置1,這樣就完成了進(jìn)程對(duì)于信號(hào)的接收了。
另一方面,PCB是內(nèi)核數(shù)據(jù)結(jié)構(gòu),修改位圖其實(shí)就是修改內(nèi)核數(shù)據(jù)結(jié)構(gòu),想要訪(fǎng)問(wèn)硬件或內(nèi)核系統(tǒng)資源,則一定繞不開(kāi)操作系統(tǒng),因?yàn)椴僮飨到y(tǒng)是軟硬件資源的管理者,那么修改位圖這件事也一定繞不開(kāi)操作系統(tǒng),而操作系統(tǒng)為了保證自身和他管理的成員的安全性,所以他必須提供系統(tǒng)調(diào)用接口,讓用戶(hù)按照操作系統(tǒng)的意愿來(lái)訪(fǎng)問(wèn)內(nèi)核資源或硬件,不能隨意的想怎么訪(fǎng)問(wèn)就怎么訪(fǎng)問(wèn)。所以如果我們作為用戶(hù)想要向進(jìn)程發(fā)送信號(hào),那么就一定得通過(guò)系統(tǒng)調(diào)用接口來(lái)完成這樣的工作,所以我們以前所用到的kill指令,其底層一定需要調(diào)用系統(tǒng)調(diào)用接口。
二、信號(hào)的發(fā)送(修改PCB的信號(hào)位圖)
1.通過(guò)鍵盤(pán)發(fā)送信號(hào)(kill指令 和 熱鍵)
1.
最常用的發(fā)送信號(hào)方式就是一個(gè)熱鍵ctrl+c,這個(gè)組合鍵其實(shí)會(huì)被操作系統(tǒng)解釋成2號(hào)信號(hào)SIGINT,通過(guò)man 7 signal就可以查看到對(duì)應(yīng)的信號(hào)和其默認(rèn)處理行為等等信息。
我們并未對(duì)2號(hào)信號(hào)做任何特殊處理,所以進(jìn)程處理2號(hào)信號(hào)的默認(rèn)動(dòng)作就是Term,也就是終止進(jìn)程。平常在我們終止前臺(tái)進(jìn)程的時(shí)候,大家的第一感受就是只要我們按下組合鍵ctrl+c,進(jìn)程就會(huì)被立馬終止,所以我們感覺(jué)進(jìn)程應(yīng)該是立馬處理了我們發(fā)送的信號(hào)啊,怎么能是待會(huì)兒處理這個(gè)信號(hào)呢?值得注意的是,我們的感官靈敏度和CPU的靈敏度是不在同一個(gè)level的,我們直覺(jué)感受到進(jìn)程是立馬處理該信號(hào)的,但其實(shí)很大可能進(jìn)程等待了幾十毫秒或幾百毫秒,而這個(gè)過(guò)程我們是無(wú)法感受到的,但事實(shí)就是如此,進(jìn)程需要保存信號(hào)等待合適的時(shí)候再去處理信號(hào)。
2.
下面介紹一個(gè)接口叫做signal,它可以用來(lái)捕捉對(duì)應(yīng)的信號(hào),讓進(jìn)程在遞達(dá)處理信號(hào)時(shí)不再遵循默認(rèn)動(dòng)作,而是按照我們所設(shè)定的方法函數(shù)進(jìn)行遞達(dá)處理,這個(gè)自定義的方法函數(shù)就是handler,signal的第二個(gè)參數(shù)其實(shí)就是接收返回值為void參數(shù)為int的函數(shù)的函數(shù)指針,所以在使用handler時(shí)我們需要傳信號(hào)編號(hào)和處理該信號(hào)編號(hào)時(shí)所遵循的自定義方法的函數(shù)名即可。
signal函數(shù)的返回值我們一般不關(guān)注,signal函數(shù)調(diào)用成功時(shí)返回handler方法的函數(shù)指針,調(diào)用失敗則返回SIG_ERR宏。
SIG_ERR宏其實(shí)就是-1整型被強(qiáng)轉(zhuǎn)成函數(shù)指針類(lèi)型,其余的兩個(gè)宏可以作為參數(shù)傳到signal的第二個(gè)參數(shù),分別代表當(dāng)進(jìn)程收到對(duì)應(yīng)的signo信號(hào)時(shí)的處理行為,SIG_DFL是默認(rèn)行為,比如進(jìn)程的默認(rèn)行為是終止進(jìn)程,但我們將其處理行為改為了忽略,但此時(shí)又想將行為改回默認(rèn)行為,此時(shí)就可以用SIG_DFL這個(gè)宏。SIG_IGN是忽略行為,如果此時(shí)進(jìn)程對(duì)于signo的處理行為是終止,那我們可以手動(dòng)將其處理行為改成SIG_IGN忽略,也就是什么都不做,惰性的將對(duì)應(yīng)的信號(hào)位圖中的比特位再由1置為0,然后什么都不干,這就是忽略行為。
3.
通過(guò)代碼運(yùn)行結(jié)果可以看出,當(dāng)我們向進(jìn)程發(fā)送2號(hào)信號(hào)時(shí),進(jìn)程此時(shí)不會(huì)再被終止了,而是打印出了一條信息"進(jìn)程捕捉到了一個(gè)信號(hào)編號(hào)是2的信號(hào)",此時(shí)進(jìn)程處理2號(hào)信號(hào)的行為就變成了自定義行為,去執(zhí)行我們自己設(shè)定的handler方法。
那我們是不是就無(wú)法通過(guò)向進(jìn)程發(fā)送2號(hào)信號(hào)來(lái)殺死進(jìn)程了呢?答案是 是的,但我們還有其他的手段,通過(guò)kill -9指令可以殺死進(jìn)程。假設(shè)我們把所有的信號(hào)都捕捉了,并且捕捉后的處理行為也不終止這個(gè)進(jìn)程,那么是不是這個(gè)進(jìn)程就金剛不壞,哪個(gè)信號(hào)都沒(méi)有辦法殺死他呢?答案并不是這樣的,9號(hào)信號(hào)是管理員信號(hào),是操作系統(tǒng)給自己留的底牌,這個(gè)信號(hào)被規(guī)定為無(wú)法捕捉,所以即使你使用signal捕捉這個(gè)信號(hào)也是沒(méi)有用的,操作系統(tǒng)必須保證自己有能夠殺死終止任意一個(gè)進(jìn)程的能力,這個(gè)能力就是通過(guò)9號(hào)信號(hào)來(lái)達(dá)到的。
4.
實(shí)際上除熱鍵ctrl+c外,還有一個(gè)熱鍵是ctrl+\,這個(gè)組合鍵會(huì)被操作系統(tǒng)解析為3號(hào)信號(hào)SIGQUIT,這個(gè)信號(hào)的默認(rèn)處理行為是Core,除終止進(jìn)程外還會(huì)進(jìn)行核心轉(zhuǎn)儲(chǔ),Core于Term有什么不同?這個(gè)話(huà)題放到信號(hào)的遞達(dá)處理部分進(jìn)行講解。
5.
有很多人誤以為只要顯示寫(xiě)了signal函數(shù),這個(gè)函數(shù)在main執(zhí)行流里面就會(huì)被調(diào)用。這樣的想法完全是錯(cuò)誤的,我們顯示寫(xiě)signal函數(shù)其實(shí)相當(dāng)于注冊(cè)了一個(gè)信號(hào)處理時(shí)的自定義行為,然后這個(gè)自定義行為handler不會(huì)平白無(wú)故被調(diào)用的,只有當(dāng)對(duì)應(yīng)信號(hào)發(fā)送給進(jìn)程時(shí),這個(gè)handler才會(huì)被調(diào)用,否則這個(gè)函數(shù)是永遠(yuǎn)不會(huì)被調(diào)用的。
你可以把main和handler看作兩個(gè)執(zhí)行流,沒(méi)有信號(hào)時(shí),只有main一個(gè)執(zhí)行流在執(zhí)行代碼,接收到對(duì)應(yīng)的信號(hào)時(shí),會(huì)從main執(zhí)行流轉(zhuǎn)移到handler執(zhí)行流,等到handler執(zhí)行流執(zhí)行結(jié)束后,再回到main中剛剛執(zhí)行到的那一行代碼繼續(xù)向下執(zhí)行剩余代碼。
6.
另外補(bǔ)充一個(gè)知識(shí)點(diǎn),linux規(guī)定,當(dāng)用戶(hù)在和shell交互時(shí),默認(rèn)只能有一個(gè)前臺(tái)進(jìn)程,所以當(dāng)我們自己編寫(xiě)的程序運(yùn)行時(shí),bash進(jìn)程就會(huì)自動(dòng)由前臺(tái)進(jìn)程轉(zhuǎn)換為后臺(tái)進(jìn)程。
除上面的情況外,如果某一個(gè)進(jìn)程由于被發(fā)送19號(hào)信號(hào)SIGSTOP停止后,再被發(fā)送18號(hào)信號(hào)SIGCONT重新繼續(xù)運(yùn)行時(shí),這個(gè)進(jìn)程也會(huì)由原來(lái)的前臺(tái)進(jìn)程轉(zhuǎn)換為后臺(tái)進(jìn)程。
對(duì)于前臺(tái)進(jìn)程我們可以用2號(hào)信號(hào)SIGINT進(jìn)行進(jìn)程終止,但后臺(tái)進(jìn)程無(wú)法用SIGINT進(jìn)行進(jìn)程終止,我們可以選擇9號(hào)信號(hào)終止進(jìn)程。
2.通過(guò)系統(tǒng)調(diào)用發(fā)送信號(hào)(kill系統(tǒng)調(diào)用 和 raise、abort庫(kù)函數(shù))
1.
其實(shí)除上面那種用組合鍵或者是手動(dòng)的通過(guò)kill指令加信號(hào)編號(hào)的方式給進(jìn)程發(fā)送信號(hào)外,我們還可以通過(guò)系統(tǒng)調(diào)用的方式給進(jìn)程發(fā)送信號(hào)。操作系統(tǒng)有向進(jìn)程發(fā)送信號(hào)的能力,但是他并沒(méi)有這個(gè)權(quán)力,操作系統(tǒng)的能力是為用戶(hù)提供的,用戶(hù)才有發(fā)送信號(hào)的權(quán)力,操作系統(tǒng)通過(guò)給用戶(hù)提供系統(tǒng)調(diào)用賦予用戶(hù)讓OS向進(jìn)程發(fā)送信號(hào)的權(quán)力。就像我們將來(lái)可能都會(huì)變成程序員,我們有寫(xiě)代碼的能力,我們的能力是服務(wù)于公司或老板的,讓我們寫(xiě)代碼的權(quán)力來(lái)自于老板。
2.
注意你沒(méi)看錯(cuò),kill不僅是指令,他還是一個(gè)系統(tǒng)調(diào)用,這個(gè)接口用起來(lái)非常簡(jiǎn)單,參數(shù)分別為進(jìn)程id和信號(hào)編號(hào),通過(guò)kill系統(tǒng)調(diào)用和命令行參數(shù)的知識(shí),我們也可以實(shí)現(xiàn)一個(gè)kill指令,我們規(guī)定運(yùn)行mysignal時(shí),命令行參數(shù)形式必須為./mysignal pid signo的形式,通過(guò)命令行輸入的信號(hào)編號(hào)和進(jìn)程id,在mysignal可執(zhí)行程序中向id進(jìn)程發(fā)送對(duì)應(yīng)的信號(hào),這樣的功能不就是kill指令的功能嗎?
3.
有一個(gè)庫(kù)函數(shù)是raise,它可以用來(lái)給自己所在進(jìn)程發(fā)送信號(hào),其實(shí)他底層調(diào)用的還是系統(tǒng)調(diào)用kill(pid_t pid, int sig),這接口沒(méi)啥意思,說(shuō)白了就是變相的用kill系統(tǒng)調(diào)用給自己的進(jìn)程pid發(fā)送指定信號(hào)而已,換湯不換藥。
4.
還有一個(gè)接口是abort,這個(gè)接口就是什么參數(shù)都不用傳,它會(huì)自動(dòng)給異常進(jìn)程發(fā)送信號(hào)SIGABRT,默認(rèn)處理動(dòng)作就是終止該進(jìn)程,abort有中止的意思。這個(gè)接口說(shuō)白了也是變相的使用kill系統(tǒng)調(diào)用給自己進(jìn)程發(fā)送6號(hào)SIGABRT信號(hào)而已,換湯不換藥。
5.
我們上面所說(shuō)的raise和abort都在man 3號(hào)手冊(cè),這代表他們都是庫(kù)函數(shù),而kill在2號(hào)手冊(cè),是純正的系統(tǒng)調(diào)用。但3號(hào)手冊(cè)的庫(kù)函數(shù)可以分為兩類(lèi),底層封裝了系統(tǒng)調(diào)用的庫(kù)函數(shù)和沒(méi)有封裝系統(tǒng)調(diào)用的庫(kù)函數(shù),很明顯,raise和abort庫(kù)函數(shù)就是底層封裝了kill系統(tǒng)調(diào)用的庫(kù)函數(shù)。就連kill指令底層其實(shí)也是封裝的kill系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)的。
由此可以看出,想要修改PCB中的信號(hào)位圖,也就是修改內(nèi)核資源,必須通過(guò)操作系統(tǒng)來(lái)完成,而操作系統(tǒng)會(huì)給用戶(hù)提供對(duì)應(yīng)的系統(tǒng)調(diào)用接口,讓用戶(hù)按照內(nèi)核意愿來(lái)修改內(nèi)核資源。
6.
在上面的內(nèi)容中我們已經(jīng)見(jiàn)到了許多的信號(hào),比如SIGINT, SIGQUIT, SIGABRT, SGIKILL等,他們?cè)谶f達(dá)處理時(shí)的默認(rèn)動(dòng)作都是終止進(jìn)程,那搞出來(lái)那么多信號(hào)還有什么意義呢?他們的默認(rèn)處理動(dòng)作都是一樣的呀!
信號(hào)的意義并不在于其進(jìn)程遞達(dá)處理信號(hào)的結(jié)果上,而是在于是由于什么原因而產(chǎn)生的信號(hào),不同的事件會(huì)產(chǎn)生不同的信號(hào),通過(guò)信號(hào)的不同我們能夠定位出進(jìn)程是由于什么異常而退出的,這能幫助我們快速定位代碼錯(cuò)誤所在。
就像C++的異常一樣,那么多的異常種類(lèi),在捕獲異常之后,進(jìn)程不都終止了嗎?那還要那么多的異常干什么???道理不就和信號(hào)類(lèi)似嗎,異常的意義也不在于異常的處理結(jié)果上,而是程序員能夠通過(guò)異常的種類(lèi)代表產(chǎn)生錯(cuò)誤的不同事件來(lái)判定出程序的錯(cuò)誤所在。
3.硬件異常 通知內(nèi)核 向進(jìn)程發(fā)送信號(hào)
3.1 除0錯(cuò)誤(OS怎么會(huì)知道給當(dāng)前進(jìn)程發(fā)8號(hào)信號(hào)?進(jìn)程只除0一次為什么handler瘋狂被調(diào)用遞達(dá)處理8號(hào)信號(hào)呢?)
1.
除我們主動(dòng)調(diào)用系統(tǒng)調(diào)用或通過(guò)鍵盤(pán)發(fā)送指令外,軟件本身其實(shí)也可以自發(fā)的發(fā)送信號(hào),比如這個(gè)部分所講的硬件異常導(dǎo)致軟件自發(fā)的發(fā)送信號(hào)。
從下面代碼運(yùn)行結(jié)果可以看出,當(dāng)發(fā)生除0錯(cuò)誤之后,代碼運(yùn)行之后,打印出了一條錯(cuò)誤信息Floating point exception然后進(jìn)程就退出了,在通過(guò)kill -l指令查找后,不難確定進(jìn)程其實(shí)是收到了8號(hào)信號(hào)SIGFPE而退出的。
如果想要證明確實(shí)是8號(hào)進(jìn)程導(dǎo)致的進(jìn)程退出,我們可以用signal捕捉一下8號(hào)信號(hào),然后進(jìn)行自定義處理,看看進(jìn)程在運(yùn)行時(shí)是否會(huì)調(diào)用我們自定義的handler方法。
2.
可以看到,第一次在死循環(huán)里面我們除0一次,然后當(dāng)程序運(yùn)行的時(shí)候,signal瘋狂捕捉8號(hào)信號(hào)SIGFPE,那我們可以將其理解成是由于除0代碼放在死循環(huán)里面導(dǎo)致的,因?yàn)樵谒姥h(huán)里面,不斷進(jìn)行除0錯(cuò)誤,那么OS就不斷的給進(jìn)程發(fā)送8號(hào)信號(hào),signal就會(huì)不斷的被捕捉,handler方法就會(huì)不斷的被執(zhí)行,從而導(dǎo)致顯示器上瘋狂打印handler里面的輸出信息,進(jìn)程捕捉到了一個(gè)信號(hào),信號(hào)編號(hào)是8號(hào)。
上面確實(shí)可以這么理解,沒(méi)有絲毫問(wèn)題。那我們就趕快把除0代碼放到死循環(huán)外面啊,放到外面8號(hào)信號(hào)SIGFPE就不會(huì)一直發(fā)送了,那signal就只會(huì)捕捉一次8號(hào)信號(hào),handler也就只會(huì)被執(zhí)行一次,打印一行輸出信息即可,但!結(jié)果和我們所想的一樣嗎?當(dāng)然不一樣!程序依舊還是瘋狂捕捉SIGFPE信號(hào),handler中的輸出信息還是像鞭尸一樣瘋狂的輸出,這是怎么回事捏?
3.
經(jīng)過(guò)你上面的兩個(gè)代碼運(yùn)行結(jié)果來(lái)看,此時(shí)我有兩個(gè)問(wèn)題,一個(gè)是操作系統(tǒng)怎么知道要給我這個(gè)進(jìn)程發(fā)送8號(hào)信號(hào)呢?另一個(gè)問(wèn)題,我都已經(jīng)把除0代碼放到死循環(huán)外面了,就除0一次而已啊,你signal怎么還給我瘋狂捕捉8號(hào)信號(hào)呢,這是怎么回事???
問(wèn)題1:CPU中有很多很多的寄存器,這些寄存器就相當(dāng)于CPU的工作臺(tái),其中有些寄存器位狀態(tài)寄存器,用于標(biāo)識(shí)這次CPU的計(jì)算結(jié)果是否正確,狀態(tài)寄存器標(biāo)識(shí)每次CPU的計(jì)算結(jié)果是否正確其實(shí)也是通過(guò)狀態(tài)位圖來(lái)解決的,如果計(jì)算結(jié)果正常那么對(duì)應(yīng)的標(biāo)志位就是0,如果計(jì)算出現(xiàn)錯(cuò)誤對(duì)應(yīng)的比特位就會(huì)由0置1。除0其實(shí)就相當(dāng)于除無(wú)窮小,那么CPU計(jì)算出來(lái)的結(jié)果就會(huì)很大很大,可能已經(jīng)超出INT_MAX了,此時(shí)狀態(tài)寄存器中的溢出標(biāo)志位就會(huì)由0置為1,那么這是不是代表CPU計(jì)算出錯(cuò)了呢?當(dāng)然是?。?br> 那么操作系統(tǒng)要不要知道CPU計(jì)算出錯(cuò)了呢?當(dāng)然要知道!因?yàn)椴僮飨到y(tǒng)是軟硬件資源的管理者,你硬件計(jì)算都出異常了,我操作系統(tǒng)能不知道嗎?所以操作系統(tǒng)就會(huì)知道當(dāng)前在CPU上運(yùn)行的進(jìn)程導(dǎo)致CPU出現(xiàn)計(jì)算錯(cuò)誤了,并且CPU計(jì)算錯(cuò)誤是由于溢出,那么此時(shí)操作系統(tǒng)就會(huì)給對(duì)應(yīng)進(jìn)程發(fā)送8號(hào)信號(hào)SIGFPE,進(jìn)程收到該信號(hào)后,在合適的時(shí)候會(huì)處理這個(gè)信號(hào),處理時(shí)默認(rèn)的行為就是終止該進(jìn)程,這就能解釋為什么操作系統(tǒng)知道要給具體哪個(gè)進(jìn)程發(fā)送8號(hào)信號(hào)了。因?yàn)檫M(jìn)程在CPU上運(yùn)行的時(shí)候,進(jìn)程相關(guān)的上下文數(shù)據(jù)都被臨時(shí)加載到CPU的寄存器上了,操作系統(tǒng)一讀取寄存器內(nèi)容,進(jìn)程的相關(guān)數(shù)據(jù)還不是輕輕松松都拿到了嗎?根據(jù)CPU的計(jì)算異常種類(lèi),向進(jìn)程發(fā)送個(gè)8號(hào)信號(hào)對(duì)于操作系統(tǒng)還不簡(jiǎn)單嗎?
所以總結(jié)成一句話(huà)就是,CPU計(jì)算發(fā)生異常,操作系統(tǒng)知曉CPU發(fā)生的計(jì)算異常種類(lèi)后,向當(dāng)前在CPU上正在運(yùn)行的進(jìn)程發(fā)送對(duì)應(yīng)的8號(hào)信號(hào),進(jìn)程在合適的時(shí)候處理該信號(hào),默認(rèn)處理行為就是終止退出進(jìn)程。
操作系統(tǒng)作為軟硬件資源的管理者,什么都知道!
問(wèn)題2:?jiǎn)栴}1是基于進(jìn)程遞達(dá)處理信號(hào)時(shí)是默認(rèn)處理行為,也就是終止退出進(jìn)程,我們想知道為什么OS會(huì)給進(jìn)程發(fā)送8號(hào)信號(hào)。問(wèn)題2是基于我們通過(guò)signal捕捉8號(hào)信號(hào),自己定義handler方法,想要驗(yàn)證進(jìn)程的確就是由于收到8號(hào)信號(hào)而退出的,但發(fā)現(xiàn)除0即使就除了一次,但handler依舊被瘋狂的調(diào)用,我們想知道這是為什么。所以問(wèn)題1和2基于的場(chǎng)景是不同的,老鐵們注意一下。
進(jìn)程收到信號(hào)后,在合適的時(shí)候進(jìn)行遞達(dá)處理后,一定會(huì)終止退出嗎?這是不一定的!那如果進(jìn)程沒(méi)有退出的話(huà),他是不是還有可能被CPU進(jìn)行調(diào)度呢?當(dāng)然有可能被重新調(diào)度,這也是我們常說(shuō)的進(jìn)程切換。我們知道寄存器中的數(shù)據(jù)是臨時(shí)數(shù)據(jù),當(dāng)進(jìn)程被切換時(shí),CPU中這一套寄存器的內(nèi)容又會(huì)被重新加載為新的在CPU上運(yùn)行的進(jìn)程的數(shù)據(jù)(CPU的寄存器中的內(nèi)容只屬于當(dāng)前正在執(zhí)行的進(jìn)程的上下文數(shù)據(jù),進(jìn)程切換時(shí)會(huì)進(jìn)行進(jìn)程的上下文數(shù)據(jù)保護(hù),下次調(diào)度時(shí)會(huì)進(jìn)行上下文數(shù)據(jù)恢復(fù),下面的圖描繪的很詳細(xì),這里不贅述)所以當(dāng)除0的進(jìn)程被重新調(diào)度到CPU上運(yùn)行的時(shí)候,對(duì)應(yīng)的狀態(tài)寄存器里面的溢出標(biāo)志位又會(huì)由0置為1,此時(shí)CPU又會(huì)出現(xiàn)計(jì)算異常,操作系統(tǒng)知曉后又會(huì)給進(jìn)程發(fā)送8號(hào)信號(hào),那么signal又會(huì)捕捉到8號(hào)信號(hào),handler方法又會(huì)被再一次調(diào)用,所以這就是為什么我們只除0一次,但8號(hào)信號(hào)依舊多次被捕捉,handler依舊被多次調(diào)用的原因,本質(zhì)上就是因?yàn)槲覀冏远x8號(hào)信號(hào)遞達(dá)處理的行為,我們并沒(méi)有讓進(jìn)程退出,那么進(jìn)程就有可能被CPU重新調(diào)度,此時(shí)相同的問(wèn)題就會(huì)重復(fù)多次的發(fā)生,況且CPU的運(yùn)行速度那么快,就算是進(jìn)程切換,我們的除0進(jìn)程可能在1s內(nèi)還是會(huì)被重復(fù)調(diào)度很多很多次,所以CPU的速度很快很快!不要用我們的感知去衡量。
4.
那么對(duì)于這樣的問(wèn)題,我們能否修正這個(gè)錯(cuò)誤呢?比如將狀態(tài)寄存器的溢出標(biāo)志位重新再置為0?答案是不能,因?yàn)闋顟B(tài)寄存器是由CPU自己維護(hù)的,并且CPU也要被操作系統(tǒng)管理,而用戶(hù)是沒(méi)有權(quán)力訪(fǎng)問(wèn)和修改CPU上寄存器的數(shù)據(jù)的。
這一點(diǎn)也不難理解,用戶(hù)能做的工作從權(quán)限角度來(lái)講是比較有限的,當(dāng)程序已經(jīng)在CPU上跑起來(lái)的時(shí)候,此時(shí)用戶(hù)是什么都無(wú)法做的,他只能在一旁看著CPU取程序的指令并執(zhí)行指令,至于用戶(hù)想要修改或維護(hù)此時(shí)CPU計(jì)算異常這樣的事情,是無(wú)法做到的,我們唯一能做的就是看到進(jìn)程的運(yùn)行結(jié)果或中斷運(yùn)行進(jìn)行報(bào)錯(cuò),一旦程序開(kāi)始運(yùn)行,如果出錯(cuò)我們也只能進(jìn)行事后調(diào)試。
5.
從除0錯(cuò)誤這個(gè)例子我們就能夠?qū)φZ(yǔ)言級(jí)別產(chǎn)生的除0錯(cuò)誤有一個(gè)新的認(rèn)識(shí)了,實(shí)際上語(yǔ)言級(jí)別我們進(jìn)行除0時(shí),也是由于硬件CPU計(jì)算溢出導(dǎo)致操作系統(tǒng)給進(jìn)程發(fā)送SIGFPE信號(hào),信號(hào)的默認(rèn)處理動(dòng)作就是終止進(jìn)程,下面代碼就是在VS上跑的,可以看到進(jìn)程退出。
3.2 訪(fǎng)問(wèn)空指針指向的空間(OS怎么會(huì)知道給當(dāng)前進(jìn)程發(fā)送11號(hào)信號(hào)呢?)
1.
另一個(gè)常見(jiàn)的問(wèn)題就是空指針訪(fǎng)問(wèn),這個(gè)問(wèn)題本質(zhì)其實(shí)也是由于硬件異常導(dǎo)致的軟件自發(fā)向進(jìn)程發(fā)送信號(hào)。與除0相同,為什么進(jìn)程會(huì)在報(bào)錯(cuò)一條信息Segmentation fault之后會(huì)退出呢?我們通過(guò)kill -l的命名推測(cè)是由于操作系統(tǒng)給進(jìn)程發(fā)送了11號(hào)信號(hào)SIGSEGV從而導(dǎo)致進(jìn)程退出,從11號(hào)信號(hào)的默認(rèn)處理動(dòng)作我們也知道Core也是會(huì)終止進(jìn)程的。
2.
那問(wèn)題又來(lái)了,操作系統(tǒng)怎么知道要給當(dāng)前這個(gè)進(jìn)程發(fā)送11號(hào)信號(hào)呢?
(首先我們需要了解一下頁(yè)表和MMU)
頁(yè)表是操作系統(tǒng)維護(hù)的一種內(nèi)核數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)虛擬地址到物理地址之間的映射關(guān)系,當(dāng)進(jìn)程運(yùn)行時(shí),他的地址空間mm_struct會(huì)被劃分為許多固定大小(一般是4KB)的塊,這個(gè)塊我們稱(chēng)之為頁(yè)(Page),每個(gè)頁(yè)在虛擬地址和物理地址中都有唯一性的標(biāo)識(shí),頁(yè)表就是用來(lái)維護(hù)兩個(gè)部分標(biāo)識(shí)之間的映射關(guān)系的。頁(yè)表是由操作系統(tǒng)來(lái)維護(hù)和進(jìn)行管理,操作系統(tǒng)會(huì)給每個(gè)進(jìn)程都分配一個(gè)獨(dú)立的完全屬于該進(jìn)程的頁(yè)表,實(shí)際上這個(gè)頁(yè)表就是用戶(hù)級(jí)頁(yè)表(信號(hào)被捕捉的完整流程部分會(huì)講到這個(gè)知識(shí)內(nèi)容)。
而MMU是內(nèi)存管理單元,是集成在CPU內(nèi)部的一個(gè)硬件部件。當(dāng)CPU訪(fǎng)問(wèn)內(nèi)存時(shí),CPU其實(shí)訪(fǎng)問(wèn)的是虛擬地址,MMU此時(shí)就會(huì)通過(guò)查找內(nèi)核數(shù)據(jù)結(jié)構(gòu)頁(yè)表來(lái)完成CPU訪(fǎng)問(wèn)的虛擬地址到物理地址的轉(zhuǎn)換,物理地址就是實(shí)際硬件上的地址,是內(nèi)存芯片或其他物理設(shè)備上的物理位置,最終CPU訪(fǎng)問(wèn)的地址就是經(jīng)過(guò)MMU轉(zhuǎn)換后的物理地址,MMU轉(zhuǎn)換虛擬地址這一步驟是實(shí)現(xiàn)虛擬內(nèi)存機(jī)制的關(guān)鍵所在。而頁(yè)表則負(fù)責(zé)存儲(chǔ)虛擬地址和物理地址之間的映射關(guān)系,方便MMU在進(jìn)行虛擬地址轉(zhuǎn)換時(shí)通過(guò)頁(yè)表來(lái)進(jìn)行快速查找虛擬地址對(duì)應(yīng)的物理地址。
在大多數(shù)操作系統(tǒng)中,內(nèi)核將0號(hào)虛擬地址保留給操作系統(tǒng)本身,而不允許應(yīng)用程序進(jìn)行訪(fǎng)問(wèn),并且頁(yè)表內(nèi)部也沒(méi)有存儲(chǔ)0號(hào)虛擬地址到物理地址之間的映射關(guān)系,操作系統(tǒng)沒(méi)有將0號(hào)虛擬地址映射到物理內(nèi)存的任何一個(gè)頁(yè)幀上,所以在MMU嘗試將0號(hào)虛擬地址轉(zhuǎn)換為物理地址時(shí),查詢(xún)內(nèi)核數(shù)據(jù)結(jié)構(gòu)頁(yè)表時(shí),此時(shí)MMU就會(huì)發(fā)生錯(cuò)誤,無(wú)法將0號(hào)虛擬地址進(jìn)行轉(zhuǎn)換。MMU會(huì)檢測(cè)到這個(gè)錯(cuò)誤并觸發(fā)空指針異常,操作系統(tǒng)作為軟硬件資源的管理者,知曉空指針異常之后,就會(huì)給當(dāng)前正在CPU上運(yùn)行的進(jìn)程發(fā)送11號(hào)信號(hào)SIGSEGV,在進(jìn)程收到信號(hào)之后,合適的時(shí)候會(huì)去處理這個(gè)信號(hào),默認(rèn)處理動(dòng)作就是Core,會(huì)終止當(dāng)前進(jìn)程。
4.由軟件條件產(chǎn)生信號(hào)
4.1 管道:讀端關(guān)閉,寫(xiě)端一直寫(xiě)。
1.
在進(jìn)程間通信IPC部分我們談到過(guò)匿名管道和命名管道的讀寫(xiě)四大特征,其中的一個(gè)特征其實(shí)就隱含了軟件異常所產(chǎn)生的信號(hào),當(dāng)讀端關(guān)閉時(shí),操作系統(tǒng)會(huì)給寫(xiě)端發(fā)送13號(hào)信號(hào)SIGPIPE,13號(hào)信號(hào)的默認(rèn)處理行為就是Term終止當(dāng)前進(jìn)程,也就是終止寫(xiě)端進(jìn)程。
所以讀端關(guān)閉這一軟件條件,觸發(fā)了操作系統(tǒng)向進(jìn)程發(fā)送信號(hào),這就是由軟件條件所產(chǎn)生的信號(hào)。
4.2 alarm定時(shí)器
1.
通過(guò)alarm鬧鐘,我們可以計(jì)算出1s內(nèi)CPU能夠累加數(shù)據(jù)多少次,下面測(cè)試的代碼中其實(shí)分了兩種情況進(jìn)行測(cè)試,一種是每次將累加數(shù)據(jù)之后的結(jié)果打印到顯示器上,一種是在1s內(nèi)只進(jìn)行數(shù)據(jù)的累加,等到1s到了的時(shí)候,我們捕捉信號(hào)在handler里面進(jìn)行累加后數(shù)據(jù)的值的打印。
聲明:cnt是一個(gè)靜態(tài)全局變量,我想讓cnt只具有內(nèi)部鏈接屬性,handler和main當(dāng)中都能用cnt,cnt的初始值為0
2.
當(dāng)我們采用每次將信息輸出到顯示器上時(shí),cnt累加達(dá)到的數(shù)據(jù)僅僅是53820,其實(shí)主要是因?yàn)槲覀兌啻蔚脑L(fǎng)問(wèn)了顯示器硬件,也就是進(jìn)行了IO,向顯示器文件進(jìn)行output,另外由于我用的是云服務(wù)器,所以還需要將數(shù)據(jù)通過(guò)網(wǎng)絡(luò)傳輸?shù)轿业谋镜仉娔X,所以1s內(nèi)的時(shí)間大部分都消耗在等待顯示器就緒和網(wǎng)絡(luò)傳輸資源上了,CPU計(jì)算的時(shí)間卻占比很小。所以打印出來(lái)的cnt大小僅僅為5w多。
當(dāng)我們將1s的時(shí)間全部放到CPU計(jì)算上來(lái),等到1s過(guò)后定時(shí)器alarm響了,會(huì)給進(jìn)程發(fā)送13號(hào)信號(hào)SIGALRM,此時(shí)用signal捕捉信號(hào),在handler方法里面輸出cnt的值,輸出過(guò)后exit退出子進(jìn)程即可。從打印結(jié)果可以看到,如果將時(shí)間全部用來(lái)進(jìn)行CPU的計(jì)算,CPU還是非??斓模?s計(jì)算了大概5億多次,和上面的5w次差了大概1w多倍數(shù),可以看到一旦訪(fǎng)問(wèn)外設(shè)CPU的執(zhí)行速度就會(huì)慢下來(lái),因?yàn)榈却布途w很慢,硬件就緒的時(shí)間和CPU計(jì)算的時(shí)間根本不在一個(gè)量級(jí)。
3.
話(huà)又說(shuō)回來(lái),那為什么alarm鬧鐘是軟件條件異常呢?
鬧鐘實(shí)際就是軟件,他不就是數(shù)據(jù)結(jié)構(gòu)和屬性的集合嗎?所以鬧鐘本身就是軟件,當(dāng)前進(jìn)程可以設(shè)定鬧鐘,那么其他進(jìn)程也可以設(shè)定鬧鐘,所以操作系統(tǒng)內(nèi)部一定會(huì)存在很多的鬧鐘,那么操作系統(tǒng)要不要對(duì)這些鬧鐘進(jìn)行管理呢?當(dāng)然要,管理的方式就是先描述,再組織。所以鬧鐘在操作系統(tǒng)中實(shí)際就是內(nèi)核數(shù)據(jù)結(jié)構(gòu),此內(nèi)核數(shù)據(jù)結(jié)構(gòu)用于描述鬧鐘,組織的最常見(jiàn)方式就是通過(guò)鏈表,但鬧鐘的組織方式也可以通過(guò)堆,也就是優(yōu)先級(jí)隊(duì)列來(lái)實(shí)現(xiàn)。
下面是鬧鐘內(nèi)核數(shù)據(jù)結(jié)構(gòu)的偽代碼,其內(nèi)部有一個(gè)鬧鐘響鈴的時(shí)間,表示在當(dāng)前進(jìn)程的時(shí)間戳下,經(jīng)過(guò)所傳參數(shù)second秒后,鬧鐘就會(huì)響鈴,這個(gè)響鈴時(shí)間即為當(dāng)前進(jìn)程時(shí)間戳+second參數(shù)大小。
另外鬧鐘還需要一個(gè)PCB結(jié)構(gòu)體指針,用于和設(shè)置鬧鐘的進(jìn)程進(jìn)行關(guān)聯(lián),在鬧鐘響了之后,便于操作系統(tǒng)向?qū)?yīng)進(jìn)程發(fā)送14號(hào)信號(hào)SIGALRM,此信號(hào)默認(rèn)處理動(dòng)作也是終止當(dāng)前進(jìn)程。
OS會(huì)周期性的檢查這些鬧鐘,也就是通過(guò)遍歷鏈表的方式,檢查當(dāng)前時(shí)間戳超過(guò)了哪個(gè)鬧鐘數(shù)據(jù)結(jié)構(gòu)中的when時(shí)間,一旦超過(guò),說(shuō)明此鬧鐘到達(dá)設(shè)定時(shí)間,那么這個(gè)時(shí)候操作系統(tǒng)就該給鬧鐘對(duì)應(yīng)的進(jìn)程發(fā)送14號(hào)信號(hào),如何找到這個(gè)進(jìn)程呢?通過(guò)alarm類(lèi)型的結(jié)構(gòu)體指針便可以拿到alarm結(jié)構(gòu)體的內(nèi)容,其結(jié)構(gòu)體中有一個(gè)字段便是PCB指針,通過(guò)PCB指針就可以找到鬧鐘對(duì)應(yīng)的進(jìn)程了。
除鏈表這樣經(jīng)典的組織方式之外,另一種組織方式就是優(yōu)先級(jí)隊(duì)列,priority_queue,實(shí)際就是堆結(jié)構(gòu),按照鬧鐘結(jié)構(gòu)體中的when的大小建大堆,如果堆頂鬧鐘的時(shí)間小于當(dāng)前進(jìn)程時(shí)間戳,則說(shuō)明整個(gè)堆中所有的鬧鐘均為達(dá)到響鈴的條件。如果堆頂鬧鐘的時(shí)間大于當(dāng)前進(jìn)程時(shí)間戳,那就要給堆頂鬧鐘對(duì)應(yīng)進(jìn)程發(fā)送14號(hào)信號(hào)了,檢查過(guò)后再pop堆頂元素,重新看下一個(gè)堆頂鬧鐘是否超時(shí),大概就是這么一個(gè)邏輯。
5.總結(jié)一下
1.
上面我們談到了四種產(chǎn)生信號(hào)的方式,有通過(guò)鍵盤(pán)產(chǎn)生信號(hào),通過(guò)系統(tǒng)調(diào)用產(chǎn)生信號(hào),由于硬件異常導(dǎo)致軟件自發(fā)的產(chǎn)生信號(hào),由于某些軟件條件產(chǎn)生信號(hào)等等,老鐵們不難發(fā)現(xiàn),這四種產(chǎn)生信號(hào)的方式最終都落到了操作系統(tǒng)本身身上,鍵盤(pán)的kill或組合熱鍵不是通過(guò)kill系統(tǒng)調(diào)用嗎?系統(tǒng)調(diào)用不就是操作系統(tǒng)提供的接口嗎?硬件異常不還是操作系統(tǒng)知曉后給進(jìn)程發(fā)送信號(hào)嗎?由于軟件條件而產(chǎn)生的信號(hào),最終不還是通過(guò)操作系統(tǒng)來(lái)向進(jìn)程發(fā)送信號(hào)嗎?
那為什么所有發(fā)送信號(hào)最終都要落到操作系統(tǒng)上呢?因?yàn)檫M(jìn)程接收信號(hào)的本質(zhì)就是修改PCB中的信號(hào)位圖,而修改PCB這樣的能力只有操作系統(tǒng)才具有,所以只要發(fā)送信號(hào)最終都繞不開(kāi)操作系統(tǒng),因?yàn)?strong>操作系統(tǒng)是進(jìn)程的管理者。
2.
只要進(jìn)程收到信號(hào),那么信號(hào)就一定被處理嗎?并不是這樣的,進(jìn)程會(huì)在合適的時(shí)候處理該信號(hào)。那在合適處理和收到信號(hào)之間有一個(gè)時(shí)間窗口,這個(gè)時(shí)間窗口內(nèi)信號(hào)被保存在哪里呢?信號(hào)會(huì)被保存到PCB的信號(hào)位圖里面。
3.
如何理解OS向進(jìn)程發(fā)送信號(hào)呢?發(fā)送信號(hào)的本質(zhì)就是OS修改進(jìn)程PCB結(jié)構(gòu)體中的信號(hào)位圖,將對(duì)應(yīng)比特位由0置1即為進(jìn)程接收到信號(hào)。
一個(gè)進(jìn)程在未收到信號(hào)的時(shí)候,能否知道自己要對(duì)合法信號(hào)做什么處理呢?當(dāng)然可以知道,這個(gè)工作早被編寫(xiě)系統(tǒng)的程序員完成了,他們讓進(jìn)程能夠知道自己對(duì)不同的信號(hào)該做什么樣的處理。
三、信號(hào)的保存(PCB內(nèi)部的兩張位圖和一個(gè)函數(shù)指針數(shù)組)
1.未決 阻塞 遞達(dá)概念的拋出
1.
信號(hào)會(huì)在合適的時(shí)候被進(jìn)程處理,執(zhí)行信號(hào)處理的動(dòng)作,稱(chēng)為信號(hào)遞達(dá),信號(hào)遞達(dá)前的動(dòng)作被稱(chēng)為信號(hào)捕捉,我們一般通過(guò)signal()或sigaction()進(jìn)行信號(hào)的捕捉,然后對(duì)應(yīng)的handler方法會(huì)進(jìn)行信號(hào)的遞達(dá)處理。當(dāng)然如果你不自定義handler方法的話(huà),那遞達(dá)處理的動(dòng)作就不會(huì)由handler執(zhí)行,操作系統(tǒng)自己會(huì)根據(jù)默認(rèn)或忽略行為對(duì)信號(hào)進(jìn)行遞達(dá)處理。
2.
信號(hào)被保存,但并未被遞達(dá)處理叫做信號(hào)未決!意思就是此時(shí)進(jìn)程已經(jīng)收到信號(hào)了,但信號(hào)尚未被進(jìn)程遞達(dá),此時(shí)稱(chēng)之為信號(hào)未決。
3.
還有一種狀態(tài)是信號(hào)阻塞,此狀態(tài)下即使信號(hào)已經(jīng)被收到,但永遠(yuǎn)不會(huì)被遞達(dá),只有信號(hào)解除阻塞之后,該信號(hào)才會(huì)被遞達(dá)。
信號(hào)是否產(chǎn)生和信號(hào)阻塞是無(wú)關(guān)的, 就算一個(gè)信號(hào)沒(méi)有被產(chǎn)生,沒(méi)有被發(fā)送給進(jìn)程,但進(jìn)程依舊可以選擇阻塞該信號(hào),意味著將來(lái)如果進(jìn)程收到了該信號(hào),那該信號(hào)也不會(huì)被遞達(dá),只有解除阻塞之后才可以被遞達(dá)。
4.
注意阻塞和忽略是兩種完全不同的概念,阻塞指的是信號(hào)被阻塞,無(wú)論進(jìn)程是否收到該信號(hào),進(jìn)程永遠(yuǎn)都不會(huì)遞達(dá)這個(gè)信號(hào)。而忽略是進(jìn)程收到該信號(hào)后,對(duì)信號(hào)進(jìn)行遞達(dá)時(shí)的一種處理行為,進(jìn)程在遞達(dá)時(shí)可以選擇忽略該信號(hào),也就是直接將信號(hào)位圖(實(shí)際是pending位圖)中對(duì)應(yīng)的比特位由1置0之后不再做任何處理。
2.通過(guò)內(nèi)核數(shù)據(jù)結(jié)構(gòu)和偽代碼理解概念
1.
在內(nèi)核中操作系統(tǒng)為了維護(hù)信號(hào),為其創(chuàng)建了三個(gè)內(nèi)核數(shù)據(jù)結(jié)構(gòu),也就是三張表,分別為pending表,block表,handler表,前兩個(gè)表有專(zhuān)業(yè)的稱(chēng)呼叫做pending信號(hào)集和block信號(hào)集,當(dāng)進(jìn)程收到信號(hào)時(shí),對(duì)應(yīng)pending位圖中的比特位就會(huì)由0置1,當(dāng)某個(gè)進(jìn)程被阻塞時(shí),對(duì)應(yīng)block位圖中的比特位就會(huì)由0置1。
當(dāng)調(diào)用signal捕捉函數(shù)時(shí),如果處理行為采取自定義,則用戶(hù)層定義的handler函數(shù)的函數(shù)名就會(huì)被加載到對(duì)應(yīng)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)handler表里面,內(nèi)核調(diào)用handler進(jìn)行自定義處理時(shí),就會(huì)去handler表里面進(jìn)行查找。指針數(shù)組的下標(biāo)代表不同的信號(hào)編號(hào),指針數(shù)組的內(nèi)容代表對(duì)應(yīng)信號(hào)被遞達(dá)時(shí)調(diào)用的handler方法。
如果一個(gè)信號(hào)想要被遞達(dá),最多需要進(jìn)行兩次檢測(cè),第一次判斷其是否為阻塞信號(hào),如果是則判斷結(jié)束,該信號(hào)一定不會(huì)被遞達(dá)。如果不是則進(jìn)行第二次判斷,pending信號(hào)集中比特位是否為1 ,如果為1說(shuō)明該進(jìn)程確實(shí)收到了對(duì)應(yīng)的信號(hào),那就進(jìn)行遞達(dá)即可,如果為0說(shuō)明該進(jìn)程沒(méi)有收到對(duì)應(yīng)信號(hào),則不進(jìn)行遞達(dá)。
2.
下面是PCB源碼中的部分字段,正好對(duì)應(yīng)我們所說(shuō)的三個(gè)內(nèi)核數(shù)據(jù)結(jié)構(gòu),我上面所畫(huà)的圖是為了幫助大家理解信號(hào)在內(nèi)核中是怎么被操作系統(tǒng)維護(hù)的,原理和源碼中是相似的,但具體源碼的實(shí)現(xiàn)肯定要比我們上面所畫(huà)的復(fù)雜很多,如果有老鐵感興趣,可以自己下去研究一下源碼是如何實(shí)現(xiàn)的。
3.
a.即使一個(gè)信號(hào)沒(méi)有被產(chǎn)生,但這并不妨礙進(jìn)程阻塞該信號(hào)。
b.由于pending位圖中比特位只能被置1一次,所以如果某一個(gè)進(jìn)程多次收到同一類(lèi)型的普通信號(hào),這就意味著除第一個(gè)普通信號(hào)外,剩余的普通信號(hào)都將被丟失(信號(hào)丟失也不是什么壞事,他是個(gè)中義詞)。實(shí)時(shí)信號(hào)有所不同,實(shí)時(shí)信號(hào)產(chǎn)生多次時(shí)會(huì)被操作系統(tǒng)放到隊(duì)列里面,以這樣的方式來(lái)保存信號(hào),防止信號(hào)丟失。
四、信號(hào)的遞達(dá)處理(捕捉信號(hào):忽略 默認(rèn) 自定義)
1.信號(hào)默認(rèn)處理動(dòng)作Core和Term的區(qū)別(核心轉(zhuǎn)儲(chǔ)話(huà)題 + 越界訪(fǎng)問(wèn)檢查不出來(lái))
1.
這個(gè)問(wèn)題其實(shí)已經(jīng)在上面的文章中產(chǎn)生不少次了,那么多的信號(hào)默認(rèn)處理動(dòng)作都是終止進(jìn)程,那他們有什么區(qū)別呢?實(shí)際上Term的處理動(dòng)作只是單純的終止進(jìn)程,而Core除終止進(jìn)程外,還會(huì)多做一件事,就是核心轉(zhuǎn)儲(chǔ)core dump。
2.
在介紹核心轉(zhuǎn)儲(chǔ)話(huà)題之前,先來(lái)談一下以前在語(yǔ)言階段我們常見(jiàn)到的越界訪(fǎng)問(wèn)問(wèn)題,有時(shí)候越界訪(fǎng)問(wèn)能檢查出來(lái),有時(shí)候卻檢查不出來(lái),其實(shí)是由于訪(fǎng)問(wèn)的位置不同而導(dǎo)致的,當(dāng)訪(fǎng)問(wèn)的位置可能已經(jīng)超過(guò)了數(shù)組的有效空間,但沒(méi)有超出數(shù)組所在函數(shù)棧幀的有效空間,OS對(duì)于正在運(yùn)行的程序是有可能檢查不出來(lái)越界訪(fǎng)問(wèn)的,同時(shí)g++編譯器在編譯階段也沒(méi)有查找出來(lái)越界訪(fǎng)問(wèn)問(wèn)題,這就有可能導(dǎo)致數(shù)據(jù)已經(jīng)被修改,但用戶(hù)還有可能不知情的情況產(chǎn)生。
此時(shí)可以通過(guò)編譯器選項(xiàng)或其他檢查工具或插件進(jìn)行越界訪(fǎng)問(wèn)的檢查,同時(shí)我們?cè)诰帉?xiě)代碼的時(shí)候也要注意一些,不要寫(xiě)出越界訪(fǎng)問(wèn)的代碼。
編譯器負(fù)責(zé)編譯代碼時(shí)進(jìn)行越界訪(fǎng)問(wèn)的檢查,OS負(fù)責(zé)在程序運(yùn)行時(shí)對(duì)越界訪(fǎng)問(wèn)進(jìn)行檢測(cè)。
我自己在測(cè)試的時(shí)候,100,1000的數(shù)組index位置,g++都沒(méi)有檢查出來(lái)越界訪(fǎng)問(wèn),index到10000的時(shí)候檢查出來(lái)了。
2.
云服務(wù)器默認(rèn)關(guān)閉了core file的選項(xiàng),所以當(dāng)發(fā)生越界訪(fǎng)問(wèn)也就是段錯(cuò)誤時(shí),不會(huì)觸發(fā)核心轉(zhuǎn)儲(chǔ),核心轉(zhuǎn)儲(chǔ)實(shí)際上是將出現(xiàn)異常的進(jìn)程的二進(jìn)制數(shù)據(jù)轉(zhuǎn)移存儲(chǔ)到磁盤(pán)上,此時(shí)就會(huì)生成一個(gè)名為core.xxxxx的普通文件,這個(gè)文件的后綴是當(dāng)前異常進(jìn)程的pid。
當(dāng)我們利用ulimit -c選項(xiàng)設(shè)置core file的大小的時(shí)候,就可以產(chǎn)生對(duì)應(yīng)的文件了,否則云服務(wù)器默認(rèn)是關(guān)閉core file選項(xiàng)的,也就是不給用戶(hù)生成對(duì)應(yīng)的核心轉(zhuǎn)儲(chǔ)文件。
3.
那么這個(gè)核心轉(zhuǎn)儲(chǔ)文件有什么用呢?
他主要用來(lái)幫助我們進(jìn)行事后調(diào)試,當(dāng)gdb進(jìn)程之后,我們通過(guò)core-file 指令 再加對(duì)應(yīng)的異常進(jìn)程的核心轉(zhuǎn)儲(chǔ)文件,回車(chē)之后立馬就可以幫助我們快速定位問(wèn)題出錯(cuò)的位置,直接告訴我們是在main函數(shù)的第46行出現(xiàn)了段錯(cuò)誤。
所以通過(guò)核心轉(zhuǎn)儲(chǔ)文件快速定位程序問(wèn)題所在,是一種不錯(cuò)的調(diào)試策略。
2.信號(hào)被捕捉的完整流程(進(jìn)程在 合適的時(shí)候 處理信號(hào))
2.1 內(nèi)核態(tài)和用戶(hù)態(tài)(調(diào)用系統(tǒng)調(diào)用觸發(fā)軟中斷,處理器由用戶(hù)態(tài)切換到內(nèi)核態(tài))
1.
我們上面老是說(shuō)進(jìn)程會(huì)在合適的時(shí)候處理信號(hào),那么什么時(shí)候是合適的時(shí)候呢?答案是,從內(nèi)核態(tài)返回用戶(hù)態(tài)的時(shí)候,進(jìn)程會(huì)在這個(gè)時(shí)候處理信號(hào)。
需要知道的是,我們所寫(xiě)的代碼在編譯后運(yùn)行時(shí),其實(shí)是以用戶(hù)態(tài)的身份去跑的,但用戶(hù)態(tài)的代碼難免會(huì)訪(fǎng)問(wèn)內(nèi)核資源或硬件資源,而這些資源都是由操作系統(tǒng)管理的,所以想要訪(fǎng)問(wèn)這些資源則一定繞不開(kāi)操作系統(tǒng),那么操作系統(tǒng)就需要提供系統(tǒng)調(diào)用接口,讓用戶(hù)以操作系統(tǒng)的意愿去訪(fǎng)問(wèn)內(nèi)核或硬件資源,因?yàn)椴僮飨到y(tǒng)不相信任何用戶(hù),所以操作系統(tǒng)必須自己實(shí)現(xiàn)系統(tǒng)調(diào)用,這個(gè)實(shí)現(xiàn)代碼也就是我們常說(shuō)的內(nèi)核代碼,然后把代碼的接口提供給用戶(hù),用戶(hù)只能通過(guò)這些系統(tǒng)調(diào)用接口來(lái)訪(fǎng)問(wèn),不能自己隨意訪(fǎng)問(wèn)內(nèi)核或硬件資源。
2.
所以例如printf() write() read() getpid() waitpid()等等接口,前部分需要訪(fǎng)問(wèn)顯示器或鍵盤(pán)等硬件,后部分需要訪(fǎng)問(wèn)內(nèi)核資源PCB,這些接口的底層一定是離不開(kāi)系統(tǒng)調(diào)用接口的,因?yàn)樗麄兌贾苯踊蜷g接的訪(fǎng)問(wèn)了內(nèi)核或硬件資源。
再比如stl容器的各個(gè)接口,這些接口中有沒(méi)有某些接口底層一定調(diào)用的也是系統(tǒng)調(diào)用呢?當(dāng)然是有的!所有的stl容器都需要擴(kuò)容,僅憑這一點(diǎn)就可以確定他們底層要調(diào)用系統(tǒng)調(diào)用了,因?yàn)閿U(kuò)容實(shí)際上就是在訪(fǎng)問(wèn)物理內(nèi)存這一硬件資源,實(shí)際是先訪(fǎng)問(wèn)mm_struct,然后再通過(guò)MMU去訪(fǎng)問(wèn)內(nèi)存硬件資源,那這些接口也一定繞不開(kāi)操作系統(tǒng),因?yàn)椴僮飨到y(tǒng)是軟硬件資源的管理者,那這些接口底層也一定封裝了系統(tǒng)調(diào)用。
(實(shí)際上按照我個(gè)人理解來(lái)看,訪(fǎng)問(wèn)硬件資源本質(zhì)還是訪(fǎng)問(wèn)內(nèi)核資源,因?yàn)樗械挠布夹枰还芾恚僮飨到y(tǒng)會(huì)在內(nèi)核里面創(chuàng)建對(duì)應(yīng)硬件的內(nèi)核數(shù)據(jù)結(jié)構(gòu),對(duì)其進(jìn)行描述和組織,所以你訪(fǎng)問(wèn)硬件說(shuō)到底還是訪(fǎng)問(wèn)內(nèi)核資源)
3.
當(dāng)代碼運(yùn)行到系統(tǒng)調(diào)用接口時(shí),要執(zhí)行對(duì)應(yīng)的內(nèi)核代碼了,程序能否以用戶(hù)態(tài)的身份去執(zhí)行系統(tǒng)調(diào)用的內(nèi)核代碼呢?這當(dāng)然是不可以的!因?yàn)樵谟脩?hù)態(tài)下,進(jìn)程只能訪(fǎng)問(wèn)受操作系統(tǒng)授權(quán)的用戶(hù)空間的代碼,用戶(hù)態(tài)的進(jìn)程運(yùn)行級(jí)別太低,內(nèi)核并不相信用戶(hù),所以如果想要執(zhí)行內(nèi)核代碼,則進(jìn)程的運(yùn)行級(jí)別必須由用戶(hù)態(tài)切為內(nèi)核態(tài),內(nèi)核態(tài)下,進(jìn)程可以訪(fǎng)問(wèn)內(nèi)核代碼或其他內(nèi)核資源,等到系統(tǒng)調(diào)用結(jié)束之后,當(dāng)然也不能以?xún)?nèi)核態(tài)的身份去執(zhí)行用戶(hù)態(tài)的代碼,因?yàn)橛脩?hù)態(tài)的代碼有可能被惡意利用去攻擊操作系統(tǒng),而內(nèi)核態(tài)的執(zhí)行權(quán)限大,所以在系統(tǒng)調(diào)用結(jié)束后,為防止發(fā)生意外,進(jìn)程的運(yùn)行級(jí)別還需要由內(nèi)核態(tài)切換為用戶(hù)態(tài),此時(shí)如果某些代碼想要攻擊操作系統(tǒng),用戶(hù)態(tài)的執(zhí)行權(quán)限是不夠的,他無(wú)法訪(fǎng)問(wèn)任何內(nèi)核資源或硬件,自然就保證了系統(tǒng)的安全性。
4.
當(dāng)調(diào)用系統(tǒng)調(diào)用接口,也就是執(zhí)行內(nèi)核代碼時(shí),我們稱(chēng)進(jìn)程陷入了內(nèi)核態(tài),由于執(zhí)行系統(tǒng)調(diào)用時(shí)和執(zhí)行之后各需要進(jìn)行一次身份的切換,所以系統(tǒng)調(diào)用往往要費(fèi)時(shí)間一些,所以應(yīng)盡量避免頻繁調(diào)用系統(tǒng)調(diào)用接口,因?yàn)檫@會(huì)降低程序運(yùn)行的效率。
所以stl的空間配置器在實(shí)際開(kāi)空間的時(shí)候,往往要給用戶(hù)多擴(kuò)容一些,因?yàn)樗履闵晕⑦€需要多用一些空間時(shí)再次調(diào)用系統(tǒng)調(diào)用,而這樣會(huì)降低程序運(yùn)行的效率。
5.
在linux系統(tǒng)中,當(dāng)用戶(hù)進(jìn)程調(diào)用系統(tǒng)調(diào)用時(shí),會(huì)提前執(zhí)行一個(gè)int 0x80匯編指令(也稱(chēng)為中斷指令),此指令會(huì)觸發(fā)一個(gè)軟中斷(也稱(chēng)為陷阱),這個(gè)指令會(huì)讓處理器從用戶(hù)態(tài)切換為內(nèi)核態(tài),便于內(nèi)核能夠訪(fǎng)問(wèn)進(jìn)程的上下文數(shù)據(jù)(這個(gè)上下文數(shù)據(jù)就是內(nèi)核資源),其實(shí)內(nèi)核訪(fǎng)問(wèn)進(jìn)程的上下文數(shù)據(jù)還是通過(guò)處理器來(lái)實(shí)現(xiàn)的,不過(guò)此時(shí)處理器已經(jīng)切換為內(nèi)核態(tài),能夠取到相應(yīng)的進(jìn)程上下文數(shù)據(jù)
2.2 CPU工作原理(與其說(shuō)是進(jìn)程級(jí)別的切換,不如說(shuō)是處理器級(jí)別的切換)
1.
我們知道CPU中有一套寄存器,寄存器中保存的永遠(yuǎn)是臨時(shí)數(shù)據(jù),寄存器就是CPU的工作臺(tái),凡是和當(dāng)前進(jìn)程強(qiáng)相關(guān)的寄存器,寄存器內(nèi)部數(shù)據(jù)稱(chēng)為當(dāng)前進(jìn)程的上下文數(shù)據(jù),在進(jìn)程切換時(shí)要進(jìn)行上下文數(shù)據(jù)的保護(hù),也就是將被輪換下去的進(jìn)程的上下文數(shù)據(jù)暫時(shí)存到操作系統(tǒng)的某一塊特定空間區(qū)域中,便于下次進(jìn)程被輪換上來(lái)的時(shí)候能夠進(jìn)行上下文數(shù)據(jù)的恢復(fù)。
寄存器大致可以分為可見(jiàn)寄存器和不可見(jiàn)寄存器,其中有一個(gè)特殊的寄存器叫做CR3寄存器,他便為不可見(jiàn)寄存器,用戶(hù)是無(wú)法對(duì)其進(jìn)行修改的。還有一些其他的寄存器比如EBX EDI ESI等(我們這里方便敘述用cur寄存器來(lái)替代),保存的是指向當(dāng)前運(yùn)行進(jìn)程的PCB指針。
2.
實(shí)際上這個(gè)CR3寄存器內(nèi)部存儲(chǔ)的是頁(yè)表的地址,當(dāng)進(jìn)程運(yùn)行級(jí)別是用戶(hù)態(tài)時(shí),這個(gè)CR3寄存器內(nèi)部存儲(chǔ)的是用戶(hù)級(jí)頁(yè)表的物理地址,當(dāng)進(jìn)程運(yùn)行級(jí)別是內(nèi)核態(tài)時(shí),這個(gè)CR3寄存器內(nèi)部存儲(chǔ)的是內(nèi)核級(jí)頁(yè)表的物理地址。
通過(guò)這個(gè)CR3寄存器存儲(chǔ)內(nèi)容的變化,就可以實(shí)現(xiàn)進(jìn)程運(yùn)行級(jí)別的切換。
這個(gè)頁(yè)表地址有那么牛嗎?變一變CR3存儲(chǔ)的頁(yè)表地址就能實(shí)現(xiàn)進(jìn)程運(yùn)行級(jí)別的切換?頁(yè)表能有這么厲害呢?沒(méi)毛??!頁(yè)表確實(shí)挺牛的!你想要訪(fǎng)問(wèn)內(nèi)核資源,這些內(nèi)核數(shù)據(jù)結(jié)構(gòu)或代碼可能位于物理地址空間的不同位置上,所以想要找到他們就必須通過(guò)內(nèi)核級(jí)頁(yè)表,那么MMU進(jìn)行地址轉(zhuǎn)換時(shí),會(huì)去CR3寄存器內(nèi)部取內(nèi)核級(jí)頁(yè)表的地址,通過(guò)這個(gè)內(nèi)核級(jí)頁(yè)表才能實(shí)現(xiàn)內(nèi)核資源的訪(fǎng)問(wèn),因?yàn)閮?nèi)核級(jí)頁(yè)表存儲(chǔ)了內(nèi)核資源從虛擬地址到物理地址轉(zhuǎn)換的映射關(guān)系。
在進(jìn)程切換時(shí),操作系統(tǒng)會(huì)將新的進(jìn)程的頁(yè)目錄表的物理地址加載到CR3寄存器中,MMU會(huì)根據(jù)新的頁(yè)目錄表地址進(jìn)行虛擬到物理地址的轉(zhuǎn)換。
3.
實(shí)際上進(jìn)程運(yùn)行級(jí)別的切換,說(shuō)到底還是處理器由用戶(hù)態(tài)切換為內(nèi)核態(tài),或由內(nèi)核態(tài)切換為用戶(hù)態(tài),你可以這么理解,進(jìn)程在CPU上運(yùn)行,如果此時(shí)處理器是用戶(hù)態(tài)級(jí)別,那么處理器的寄存器存儲(chǔ)的內(nèi)容什么的是不包括任何進(jìn)程的內(nèi)核資源的,處理器無(wú)法取到進(jìn)程中PCB,mm_struct,頁(yè)表,文件描述符表,block信號(hào)集……等等信息,只有當(dāng)處理器為內(nèi)核態(tài)級(jí)別的時(shí)候,他就可以取到進(jìn)程的內(nèi)核資源了,并將這些資源加載到寄存器里面,那么內(nèi)核就可以通過(guò)CPU的寄存器讀取到進(jìn)程的內(nèi)核資源,進(jìn)程如果想要執(zhí)行內(nèi)核代碼,CPU也可以通過(guò)進(jìn)程內(nèi)部的內(nèi)核空間找到對(duì)應(yīng)的內(nèi)核代碼并執(zhí)行。
所以與其說(shuō)成是進(jìn)程的運(yùn)行級(jí)別的切換,不如說(shuō)成是處理器級(jí)別的切換,不過(guò)處理器級(jí)別的切換底層還是通過(guò)CR3寄存器存儲(chǔ)內(nèi)容發(fā)生變化來(lái)實(shí)現(xiàn)的。
2.3 再談進(jìn)程地址空間
1.
進(jìn)程該如何找到操作系統(tǒng)的代碼并執(zhí)行呢?其實(shí)是通過(guò)進(jìn)程地址空間中的內(nèi)核空間來(lái)完成的。在內(nèi)核中實(shí)際除了用戶(hù)級(jí)頁(yè)表之外,還有一張內(nèi)核級(jí)頁(yè)表,這個(gè)頁(yè)表可以將物理內(nèi)存中的操作系統(tǒng)代碼映射到每一個(gè)進(jìn)程的地址空間中的內(nèi)核空間,這個(gè)內(nèi)核級(jí)頁(yè)表專(zhuān)門(mén)用于進(jìn)程訪(fǎng)問(wèn)內(nèi)核資源時(shí)進(jìn)行內(nèi)核數(shù)據(jù)結(jié)構(gòu)或代碼的虛擬地址到物理地址之間的轉(zhuǎn)換。
與用戶(hù)級(jí)頁(yè)表不同的是,內(nèi)核級(jí)頁(yè)表只需要存在一份就夠了,因?yàn)樗械倪M(jìn)程訪(fǎng)問(wèn)的內(nèi)核代碼都是同一份的,而每個(gè)進(jìn)程都有自己獨(dú)立的用戶(hù)級(jí)頁(yè)表是因?yàn)槊總€(gè)進(jìn)程的代碼是不同的,需要經(jīng)過(guò)各自獨(dú)立的頁(yè)表進(jìn)行映射才能找到物理內(nèi)存上對(duì)應(yīng)的進(jìn)程的代碼。
那怎么執(zhí)行內(nèi)核代碼呢?也很簡(jiǎn)單,在進(jìn)程地址空間的上下文進(jìn)行跳轉(zhuǎn)即可,進(jìn)程運(yùn)行時(shí)其進(jìn)程的上下文數(shù)據(jù)都會(huì)被加載到CPU的寄存器里面,進(jìn)行地址的跳轉(zhuǎn)即可找到內(nèi)核代碼的虛擬地址,經(jīng)過(guò)MMU映射后便可執(zhí)行內(nèi)核代碼。
2.4 信號(hào)被捕捉遞達(dá)的完整流程(內(nèi)核如何實(shí)現(xiàn)信號(hào)的捕捉?→ vital)
1.
信號(hào)會(huì)在內(nèi)核態(tài)切換到用戶(hù)態(tài)的時(shí)候被進(jìn)程處理,那么進(jìn)程是由于什么原因進(jìn)入的內(nèi)核態(tài)呢?
常見(jiàn)的進(jìn)入內(nèi)核態(tài)有兩種情況。當(dāng)進(jìn)程調(diào)用系統(tǒng)調(diào)用時(shí),由于處理器要執(zhí)行內(nèi)核代碼,則進(jìn)程運(yùn)行級(jí)別一定需要切換為內(nèi)核態(tài),因?yàn)橛脩?hù)態(tài)權(quán)限太低,等到系統(tǒng)調(diào)用執(zhí)行完畢,進(jìn)程又會(huì)由內(nèi)核態(tài)切換為用戶(hù)態(tài)。另一種情況是進(jìn)程切換,這種情況較為常見(jiàn),當(dāng)進(jìn)程被輪換下去的時(shí)候,進(jìn)程的上下文要進(jìn)行保存,內(nèi)核如果想要訪(fǎng)問(wèn)進(jìn)程的上下文數(shù)據(jù),那么進(jìn)程的運(yùn)行級(jí)別也必須切換為內(nèi)核態(tài),否則處理器無(wú)法拿到進(jìn)程的上下文數(shù)據(jù),進(jìn)程的上下文數(shù)據(jù)也就無(wú)法保存。
2.
當(dāng)進(jìn)程陷入內(nèi)核后,執(zhí)行完系統(tǒng)調(diào)用或者某些任務(wù)后,內(nèi)核會(huì)順便檢查進(jìn)程的三張表,只要信號(hào)未被阻塞并且pending信號(hào)集中對(duì)應(yīng)的比特位是1,那么就可以遞達(dá)該信號(hào),遞達(dá)時(shí)的處理行為如果是默認(rèn),則當(dāng)前進(jìn)程運(yùn)行級(jí)別為內(nèi)核態(tài),操作系統(tǒng)正好向進(jìn)程發(fā)送信號(hào),終止殺死該進(jìn)程,如果是忽略直接惰性遞達(dá)即可,將pending位圖的比特位由1置0后什么處理都不做。上面所說(shuō)的這兩種處理行為直接以?xún)?nèi)核態(tài)的身份執(zhí)行即可,遞達(dá)后直接返回用戶(hù)態(tài)執(zhí)行剩余代碼即可。
但自定義行為的遞達(dá)就沒(méi)有那么輕松了,首先進(jìn)程以?xún)?nèi)核態(tài)的身份去執(zhí)行用戶(hù)層的代碼是萬(wàn)萬(wàn)不可以的,因?yàn)檫@不安全,如果用戶(hù)層的代碼惡意攻擊操作系統(tǒng)呢?此時(shí)內(nèi)核態(tài)的身份還正好能執(zhí)行這樣的惡意攻擊訪(fǎng)問(wèn)內(nèi)核資源的代碼,這不完蛋了嗎?所以遞達(dá)想要執(zhí)行自定義行為,則進(jìn)程運(yùn)行級(jí)別必須由內(nèi)核態(tài)切換為用戶(hù)態(tài),通過(guò)iret匯編指令可以切回到用戶(hù)態(tài),在執(zhí)行完handler后,是不能直接回到代碼的下一條運(yùn)行語(yǔ)句處的,因?yàn)樘D(zhuǎn)是需要地址的,這個(gè)過(guò)程必須有內(nèi)核參與才行,你現(xiàn)在處于用戶(hù)態(tài),進(jìn)程的地址空間等內(nèi)核信息的訪(fǎng)問(wèn)都需要內(nèi)核態(tài)運(yùn)行級(jí)別的進(jìn)程,所以此時(shí)還需要再回到內(nèi)核態(tài),在內(nèi)核態(tài)中通過(guò)進(jìn)程虛擬地址的跳轉(zhuǎn)找到用戶(hù)態(tài)運(yùn)行到哪行代碼處了,然后再通過(guò)iret指令將進(jìn)程運(yùn)行級(jí)別由內(nèi)核態(tài)轉(zhuǎn)為用戶(hù)態(tài)。最后進(jìn)程以用戶(hù)態(tài)身份繼續(xù)向下執(zhí)行剩余代碼即可。
3.
在上面敘述的過(guò)程中,進(jìn)程執(zhí)行handler方法后為什么不能直接回到main執(zhí)行流?而是需要先回到內(nèi)核態(tài),然后再通過(guò)某些匯編指令(iret)回到用戶(hù)態(tài),恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行。
我上面的解釋其實(shí)是有問(wèn)題的,我從進(jìn)程地址空間的角度解釋了進(jìn)程執(zhí)行完handler方法后要回到內(nèi)核態(tài),這個(gè)角度是錯(cuò)誤的,因?yàn)檫M(jìn)程地址空間中的0-3G用戶(hù)空間不屬于內(nèi)核資源.
其實(shí)真正的原因是因?yàn)?,handler執(zhí)行流和main執(zhí)行流使用不同的堆??臻g,他們之間不存在調(diào)用和被調(diào)用的關(guān)系,是兩個(gè)獨(dú)立的控制流(就像fork的父進(jìn)程和子進(jìn)程一樣,他們的堆??臻g完全獨(dú)立)。所以進(jìn)程是無(wú)法做到從用戶(hù)層的handler執(zhí)行流直接跳轉(zhuǎn)到main執(zhí)行流的,而是需要通過(guò)sigreturn再次進(jìn)入內(nèi)核態(tài),如果此時(shí)沒(méi)有信號(hào)被遞達(dá),則這次返回用戶(hù)態(tài)就是恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行剩余代碼了。
4.
下面畫(huà)了一張圖,幫助大家理解信號(hào)捕捉遞達(dá)(處理行為是自定義行為)的完整流程,從左上角開(kāi)始 到 再回到左上角的一個(gè)過(guò)程。紅色圈圈代表進(jìn)程的運(yùn)行級(jí)別要發(fā)生切換,中間的綠圈代表信號(hào)檢測(cè)。(如果信號(hào)遞達(dá)的行為是默認(rèn)或忽略,則信號(hào)檢測(cè)過(guò)后直接返回到用戶(hù)態(tài)即可,無(wú)須執(zhí)行handler方法)
5.
最后再總結(jié)一下信號(hào)被捕捉遞達(dá)的完整流程(很詳細(xì))。
遞達(dá)像是一個(gè)過(guò)程,而捕捉更像是一個(gè)動(dòng)作,當(dāng)信號(hào)的處理行為是自定義行為,那么在信號(hào)遞達(dá)的時(shí)候會(huì)調(diào)用對(duì)應(yīng)的handler方法,此時(shí)我們稱(chēng)調(diào)用handler方法為捕捉信號(hào)。
假設(shè)用戶(hù)已經(jīng)了注冊(cè)某個(gè)信號(hào)(9號(hào)信號(hào)除外)的處理函數(shù)sighandler()。當(dāng)前正在執(zhí)行main函數(shù),由于中斷或異常切換到內(nèi)核態(tài)。在中斷或異常處理完畢之后,即將返回到用戶(hù)態(tài)的main函數(shù)之前,內(nèi)核發(fā)現(xiàn)用戶(hù)注冊(cè)的某個(gè)信號(hào)需要被遞達(dá),那此時(shí)內(nèi)核決定:返回用戶(hù)態(tài)不是恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行,而是去執(zhí)行用戶(hù)注冊(cè)的信號(hào)處理函數(shù)sighandler()。sighandler()執(zhí)行完畢之后,由于sighandler()和main()是兩個(gè)獨(dú)立的控制流程,各自使用不同的堆??臻g(具體我后面在多線(xiàn)程部分會(huì)講解),兩者之間并不存在調(diào)用和被調(diào)用的關(guān)系。所以在sighandler()函數(shù)執(zhí)行完畢,進(jìn)行返回時(shí),會(huì)自動(dòng)調(diào)用特殊的系統(tǒng)調(diào)用sigreturn()(sys_sigreturn()是內(nèi)核中該系統(tǒng)調(diào)用的具體實(shí)現(xiàn))再次進(jìn)入內(nèi)核態(tài),如果此時(shí)沒(méi)有新的信號(hào)需要被遞達(dá),那么進(jìn)程將會(huì)返回為用戶(hù)態(tài),內(nèi)核回恢復(fù)main函數(shù)的上下文繼續(xù)執(zhí)行main的剩余代碼。
上面的敘述過(guò)程拋出了中斷和異常,以及堆棧空間等概念。
1.堆??臻g其實(shí)就是棧區(qū),一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),而每個(gè)函數(shù)都會(huì)有自己獨(dú)立的函數(shù)棧幀空間,這些堆??臻g會(huì)被依次壓入堆??臻g中,進(jìn)行后進(jìn)先出的處理。有很多人喜歡把棧叫做堆??臻g,堆??臻g大小是有限制的,如果函數(shù)調(diào)用層數(shù)過(guò)多,比如遞歸,此時(shí)堆棧空間是有可能發(fā)生stack overflow堆??臻g溢出的,所以在調(diào)用函數(shù)時(shí)要注意遞歸的寫(xiě)法,遞歸展開(kāi)太多的話(huà),棧溢出在所難免。
STL中的stack容器和這里的堆??臻g要區(qū)分開(kāi)來(lái),動(dòng)態(tài)分配內(nèi)存的容器空間一般都開(kāi)辟在堆上,容器對(duì)象本身一般都是在堆??臻g上,對(duì)象中含有指向堆空間的指針。所以我們?cè)谑褂胹tack容器時(shí),一般是不用擔(dān)心溢出的,因?yàn)槎芽臻g很大很大有好幾個(gè)G。(強(qiáng)調(diào)這里,是怕大家把堆??臻g和某些數(shù)據(jù)結(jié)構(gòu)stack搞混掉,stack的空間一般都在堆上進(jìn)行開(kāi)辟,和堆??臻g是不同的)
2.時(shí)鐘中斷就是我們常說(shuō)的進(jìn)程時(shí)間片到了,進(jìn)程要被輪轉(zhuǎn)下去,此時(shí)需要執(zhí)行內(nèi)核中的中斷處理程序,進(jìn)程就需要被切換為內(nèi)核態(tài)。軟件中斷,譬如在信號(hào)產(chǎn)生部分談到的管道讀端關(guān)閉,管道寫(xiě)端進(jìn)程被終止。或者是alarm定時(shí)器,時(shí)間戳超過(guò)alarm定時(shí)器設(shè)定時(shí)間時(shí),就會(huì)觸發(fā)軟件中斷,此時(shí)進(jìn)程回切換為內(nèi)核態(tài),執(zhí)行內(nèi)核中對(duì)應(yīng)的中斷處理程序。
下面是一個(gè)硬件中斷的例子,其實(shí)可以看到,進(jìn)程運(yùn)行級(jí)別的切換還是非常頻繁的。因?yàn)槌绦虻倪\(yùn)行難免要訪(fǎng)問(wèn)到內(nèi)核資源,程序不是單蹦的,他也需要和操作系統(tǒng)進(jìn)行交互,訪(fǎng)問(wèn)內(nèi)核資源,所以進(jìn)程運(yùn)行級(jí)別切換非常頻繁我們也能夠理解,畢竟事物的產(chǎn)生和運(yùn)轉(zhuǎn)往往不是獨(dú)立憑空出現(xiàn)的,而是需要配合其他事物來(lái)共同完成。
五、通過(guò)代碼編寫(xiě) 理解 信號(hào)的保存和遞達(dá)
1.信號(hào)集操作的庫(kù)函數(shù)
1.
sigset_t類(lèi)型對(duì)于所有的信號(hào)都用一個(gè)bit位來(lái)表示當(dāng)前進(jìn)程是否受到該信號(hào),至于這個(gè)類(lèi)型內(nèi)部如何存儲(chǔ)這些bit位,用戶(hù)是不需要關(guān)心的,用戶(hù)只能通過(guò)以下的庫(kù)函數(shù)操作接口來(lái)操作sigset_t變量,而不應(yīng)該主觀的對(duì)其內(nèi)部數(shù)據(jù)做任何解釋。(比如你想打印printf輸出sigset_t變量的值,等等操作都是不被允許的。)
前4個(gè)函數(shù)都是成功返回0,出錯(cuò)返回-1.sigismember是布爾函數(shù),信號(hào)集若包含signo則返回1,不包含返回0,出錯(cuò)返回-1。
在使用sigset_t類(lèi)型的變量之前,一定要使用sigemptyset()或sigfillset()函數(shù)對(duì)變量進(jìn)行初始化,使變量?jī)?nèi)部的數(shù)據(jù)處于一個(gè)確定穩(wěn)定的狀態(tài),在初始化sigset_t變量之后就可以調(diào)用剩余三個(gè)函數(shù)進(jìn)行信號(hào)的添加,刪除,判斷是否存在等。
2.
實(shí)際上sigset_t類(lèi)型是一個(gè)結(jié)構(gòu)體類(lèi)型的重定義,這個(gè)結(jié)構(gòu)體中包含了一個(gè)類(lèi)型為unsigned long int的數(shù)組,每個(gè)元素大小是8字節(jié)。至于信號(hào)是如何添加,如何刪除等操作我們不關(guān)心,感興趣的老鐵可以看下源碼。
2.系統(tǒng)調(diào)用: sigprocmask 和 sigpending
1.
我們之前所說(shuō)的block位圖,其實(shí)還有一些其他的稱(chēng)呼:信號(hào)屏蔽字,阻塞信號(hào)集。
sigprocmask是一個(gè)可以讀取或修改進(jìn)程信號(hào)屏蔽字的函數(shù),set和oset均為輸出型參數(shù),函數(shù)內(nèi)部會(huì)對(duì)set和oldset指針指向的sigset_t類(lèi)型變量做修改。如果oset為非空指針,則讀取當(dāng)前進(jìn)程的信號(hào)屏蔽字通過(guò)oset指針變量傳出。如果set為非空指針,則更改當(dāng)前進(jìn)程的信號(hào)屏蔽字,how通過(guò)傳遞宏的方式實(shí)現(xiàn)sigprocmask的不同功能,SIG_BLOCK用于添加某些信號(hào)到信號(hào)屏蔽字當(dāng)中,SIG_UNBLOCK用于移除信號(hào)屏蔽字的某些信號(hào),SIG_SETMASK用于通過(guò)set參數(shù)將函數(shù)外sigset_t類(lèi)型的信號(hào)集 設(shè)置到 內(nèi)核中PCB里面的信號(hào)屏蔽字。如果set和oset同時(shí)為非空指針,則先將原來(lái)的信號(hào)屏蔽字(set指向的信號(hào)集)備份到oset指向的信號(hào)集里面,然后再通過(guò)how和set參數(shù)對(duì)內(nèi)核中PCB的信號(hào)屏蔽字做修改。
下面便是how參數(shù)的選項(xiàng),其實(shí)就是宏。
2.
sigpending用于將內(nèi)核PCB中的pending位圖掩碼返回到set參數(shù),進(jìn)行傳出。
我們可以通過(guò)這個(gè)函數(shù)取到內(nèi)核中pending信號(hào)集的內(nèi)容,將其放到用戶(hù)層set所指向的sigset_t類(lèi)型的變量里面,用戶(hù)層就可以輸出sigset_t信號(hào)集變量的內(nèi)容,進(jìn)行觀察等一系列操作。
3.上面所學(xué)接口的代碼實(shí)現(xiàn)
1.
在了解上面與信號(hào)有關(guān)的庫(kù)函數(shù)接口以及系統(tǒng)調(diào)用接口之后,我們可以來(lái)實(shí)現(xiàn)一段代碼,我們想屏蔽一下2,3號(hào)信號(hào),此時(shí)向進(jìn)程發(fā)送對(duì)應(yīng)信號(hào),信號(hào)一定是不被遞達(dá)的,但是pending位圖中的第2和第3個(gè)比特位一定被置為1了,我也想看看pending位圖的變化。以上現(xiàn)象我們通過(guò)代碼運(yùn)行結(jié)果來(lái)觀察。
2.
這段代碼在理解上有一個(gè)關(guān)鍵點(diǎn)就是用戶(hù)層和內(nèi)核層的分辨,在開(kāi)始屏蔽數(shù)組sigarr內(nèi)部的信號(hào)之前所做的工作,其實(shí)都是在用戶(hù)層準(zhǔn)備的工作,對(duì)內(nèi)核中的block信號(hào)集,pending信號(hào)集未產(chǎn)生任何影響,第一行的signal會(huì)陷入內(nèi)核,因?yàn)樗裮yhandler的函數(shù)地址設(shè)置進(jìn)信號(hào)處理函數(shù)的方法表里面,所以進(jìn)程會(huì)陷入內(nèi)核。而其他我們定義的block oblock pending等sigset_t類(lèi)型的變量實(shí)際都是為使用系統(tǒng)調(diào)用接口做的準(zhǔn)備工作,用一些庫(kù)函數(shù)sigemptyset() sigaddset() 進(jìn)行變量的初始化,做完這些準(zhǔn)備工作之后,我們才調(diào)用系統(tǒng)調(diào)用接口,比如sigprocmask將用戶(hù)層定義的block信號(hào)集設(shè)置進(jìn)內(nèi)核的信號(hào)屏蔽字當(dāng)中,讓進(jìn)程對(duì)2和3信號(hào)進(jìn)行阻塞,我們想看看在阻塞過(guò)程中,如果我們向進(jìn)程發(fā)送信號(hào),進(jìn)程是否會(huì)遞達(dá)呢?并且還想看到pending信號(hào)集的變化,所以需要調(diào)用sigpending系統(tǒng)調(diào)用接口,將內(nèi)核中的pending信號(hào)集不斷的加載到用戶(hù)層的pending對(duì)象里面來(lái),然后我們多次打印這個(gè)pending對(duì)象的內(nèi)容即可。我們當(dāng)然無(wú)法通過(guò)調(diào)用某個(gè)函數(shù)輸出pending對(duì)象內(nèi)容,但可以利用一下sigismember來(lái)判斷所有的信號(hào)是否在pending位圖中,如果是就輸出1,不是就輸出0,這樣打印出的一行結(jié)果正好就相當(dāng)于32個(gè)比特位。在10s之后,我們對(duì)信號(hào)解除阻塞,解除的方式也很簡(jiǎn)單,調(diào)用sigprocmask,將oblock的內(nèi)容設(shè)置到內(nèi)核即可,oblock中的比特位全部都是0,則相當(dāng)于解除對(duì)所有信號(hào)的屏蔽,解除屏蔽之后,此時(shí)進(jìn)程剛好處于內(nèi)核態(tài)(因?yàn)檎{(diào)用了sigprocmask系統(tǒng)調(diào)用),檢測(cè)到有信號(hào)需要被遞達(dá),那么直接遞達(dá)該信號(hào)即可
3.
下面來(lái)看一下代碼的運(yùn)行結(jié)果,在代碼跑起來(lái)的前10s,我利用熱鍵向進(jìn)程發(fā)送2號(hào)和3號(hào)信號(hào),可以看到的現(xiàn)象是pending位圖的第二個(gè)比特位和第三個(gè)比特位都被置為了1,但是在這10s內(nèi)進(jìn)程不會(huì)遞達(dá)信號(hào),等到10s過(guò)后,進(jìn)程解除所有被屏蔽的信號(hào),此時(shí)信號(hào)會(huì)被遞達(dá),pending位圖的所有比特位又全部變成了0.
4.sigaction和signal的區(qū)別(代碼驗(yàn)證)
1.
sigaction和signal的作用很相似,都可以用來(lái)進(jìn)行信號(hào)的捕捉,signal使用起來(lái)較為簡(jiǎn)單,只需要傳信號(hào)編號(hào)和handler函數(shù)指針即可。而sigaction從參數(shù)的命名上來(lái)看,有點(diǎn)像sigprocmask,兩者都有當(dāng)前的 和 原來(lái)的,分別通過(guò)帶old和不帶old進(jìn)行命名。
2.
與signal相同,signum為需要處理的信號(hào)的編號(hào),所以當(dāng)調(diào)用signal或sigaction時(shí)就代表我們不想按照信號(hào)本身的默認(rèn)行為進(jìn)行信號(hào)遞達(dá),而是想要通過(guò)自己定義的處理行為進(jìn)行信號(hào)的遞達(dá)。
若act為非空指針,則根據(jù)act修改對(duì)應(yīng)信號(hào)的處理行為。若oldact為非空指針,則通過(guò)oldact傳出內(nèi)核中對(duì)于該信號(hào)的原本的處理動(dòng)作,這個(gè)就有點(diǎn)像sigprocmask取出內(nèi)核中信號(hào)屏蔽字的過(guò)程。若oldact和act均為非空,則還是將act的處理行為備份到oldact里面,再根據(jù)act修改內(nèi)核中對(duì)應(yīng)信號(hào)的處理行為。
結(jié)構(gòu)體struct sigaction{}的定義如下圖所示(這里有點(diǎn)特殊哈,結(jié)構(gòu)體的命名和系統(tǒng)調(diào)用的命名均為sigaction,老鐵們不要混在一塊兒),其中的三個(gè)結(jié)構(gòu)體成員與普通信號(hào)無(wú)關(guān),我們也就不用這幾個(gè)成員了,只用sa_handler和sa_mask即可,前者代表信號(hào)自定義處理行為的執(zhí)行方法,后者其實(shí)代表進(jìn)程在處理信號(hào)時(shí),順便屏蔽的信號(hào)有哪些,將要屏蔽的信號(hào)添加到sa_mask即可。
sa_handler也可設(shè)置為宏SIG_DFL和SIG_TGN,這兩個(gè)宏其實(shí)就是整型數(shù)字強(qiáng)轉(zhuǎn)為函數(shù)指針類(lèi)型了,設(shè)置后內(nèi)核對(duì)于對(duì)應(yīng)信號(hào)的處理行為則分別為默認(rèn)和忽略。
3.
sigaction實(shí)際上是要比signal更為安全可靠的,signal具有不可靠性,比如當(dāng)前正在執(zhí)行信號(hào)處理函數(shù),如果此時(shí)相同信號(hào)被遞達(dá),則當(dāng)前信號(hào)處理函數(shù)會(huì)被中斷,轉(zhuǎn)而執(zhí)行新的信號(hào)處理函數(shù),此時(shí)會(huì)新創(chuàng)建信號(hào)處理函數(shù)的函數(shù)棧幀,在新的信號(hào)處理函數(shù)執(zhí)行完后,會(huì)恢復(fù)執(zhí)行舊的信號(hào)處理函數(shù),這個(gè)過(guò)程被稱(chēng)為信號(hào)處理函數(shù)的嵌套執(zhí)行。如果多個(gè)相同類(lèi)型信號(hào)被遞達(dá),則他們的處理順序是不一定的,這無(wú)法確定。
而sigaction注冊(cè)信號(hào)處理函數(shù)時(shí),可以通過(guò)設(shè)置SA_RESTART標(biāo)志來(lái)支持信號(hào)處理函數(shù)的可靠性。當(dāng)正在執(zhí)行信號(hào)處理函數(shù)時(shí),如果相同信號(hào)被遞達(dá),系統(tǒng)會(huì)自動(dòng)等待當(dāng)前信號(hào)處理函數(shù)執(zhí)行完畢后再重新調(diào)用該信號(hào)處理函數(shù),而不是選擇重新建立函數(shù)棧幀,這就保證了信號(hào)處理的可靠性。
下面代碼可以幫助我們驗(yàn)證signal信號(hào)處理的不可靠性,但是我們其實(shí)無(wú)法通過(guò)顯示器輸出的數(shù)據(jù)看到這個(gè)信號(hào)處理的不可靠性,因?yàn)榈诙螆?zhí)行handler的時(shí)候,第二個(gè)handler()函數(shù)的執(zhí)行環(huán)境與第一個(gè)handler()函數(shù)的執(zhí)行環(huán)境是不同的,包括函數(shù)的局部變量、參數(shù)、返回地址等信息都是不同的。所以第二個(gè)handler()函數(shù)輸出的消息不會(huì)重復(fù)打印,我們也就無(wú)法通過(guò)輸出信息看到內(nèi)核重新開(kāi)辟函數(shù)棧幀的現(xiàn)象了。
4.
下面代碼中,我們通過(guò)sigaction對(duì)2號(hào)信號(hào)進(jìn)行捕捉,但同時(shí)又向結(jié)構(gòu)體act的sa_mask里面設(shè)置了3號(hào)信號(hào),這意味著在2號(hào)信號(hào)遞達(dá)處理期間,如果向進(jìn)程發(fā)送3號(hào)信號(hào),信號(hào)也是會(huì)被阻塞的,無(wú)法被遞達(dá)。
在信號(hào)被遞達(dá)處理期間,同類(lèi)型的信號(hào)會(huì)被OS自動(dòng)添加到信號(hào)屏蔽字當(dāng)中,當(dāng)信號(hào)完成遞達(dá)后,OS會(huì)自動(dòng)解除對(duì)該信號(hào)的屏蔽。所以進(jìn)程處理同類(lèi)型信號(hào)的原則是串行的處理同類(lèi)型信號(hào),不能遞歸式的進(jìn)行處理。
當(dāng)信號(hào)處理函數(shù)調(diào)用結(jié)束后進(jìn)行返回時(shí),操作系統(tǒng)會(huì)自動(dòng)解除對(duì)sa_mask中所有被阻塞的信號(hào)的阻塞狀態(tài)。
下面是代碼運(yùn)行結(jié)果,在信號(hào)處理期間,我們發(fā)送2號(hào)或3號(hào)信號(hào),他們是不會(huì)被遞達(dá)的,只有遞達(dá)完當(dāng)前信號(hào)后,OS解除對(duì)于3號(hào)的阻塞,此時(shí)3號(hào)被遞達(dá),進(jìn)程執(zhí)行3號(hào)的默認(rèn)行為,終止退出進(jìn)程。
六、補(bǔ)充知識(shí)內(nèi)容
1.可重入函數(shù)
1.
假設(shè)現(xiàn)在有一個(gè)全局鏈表,main函數(shù)調(diào)用了insert頭插函數(shù),但是當(dāng)函數(shù)執(zhí)行一半的時(shí)候,還沒(méi)有執(zhí)行完剩余代碼時(shí),此時(shí)由于硬件中斷使進(jìn)程陷入內(nèi)核,此時(shí)恰好有信號(hào)需要被遞達(dá),進(jìn)程返回用戶(hù)態(tài)執(zhí)行handler方法,結(jié)果handler方法內(nèi)部也調(diào)用了insert頭插函數(shù),恰好鏈表還是全局的,那么在handler內(nèi)部完成了結(jié)點(diǎn)的頭插,此時(shí)再返回內(nèi)核態(tài),若無(wú)信號(hào)遞達(dá),將返回用戶(hù)態(tài)恢復(fù)main函數(shù)的上下文,正好main的上下文執(zhí)行到頭插的第二行代碼,我們又調(diào)整了一下head指針的指向。此時(shí)就會(huì)出現(xiàn)問(wèn)題,我們明明調(diào)用了兩次頭插函數(shù),但鏈表只頭插了一個(gè)結(jié)點(diǎn)。
2.
像上面這樣的例子,insert被不同的執(zhí)行流調(diào)用,有可能在第一次調(diào)用還未結(jié)束時(shí)就被進(jìn)行第二次調(diào)用,我們稱(chēng)這樣的現(xiàn)象為重入。
insert函數(shù)訪(fǎng)問(wèn)全局鏈表,鏈表有可能因?yàn)榘l(fā)生重入而導(dǎo)致結(jié)果出現(xiàn)錯(cuò)誤,我們稱(chēng)這樣的函數(shù)為不可重入函數(shù)。
反之,如果一個(gè)函數(shù)僅僅訪(fǎng)問(wèn)局部的變量或數(shù)據(jù),則此函數(shù)為可重入函數(shù),因?yàn)檫@樣的函數(shù)即使發(fā)生了重入也不會(huì)出現(xiàn)問(wèn)題,所以我們稱(chēng)其為可重入函數(shù)。
其實(shí)訪(fǎng)問(wèn)局部變量不會(huì)產(chǎn)生問(wèn)題的原因還是因?yàn)椋?strong>main和handler兩個(gè)執(zhí)行流各自處于不同的堆棧空間,insert函數(shù)是兩份,你handler內(nèi)部想怎么調(diào)insert就怎么調(diào),對(duì)我main執(zhí)行流又沒(méi)什么影響,你愛(ài)咋調(diào)咋調(diào),反正你訪(fǎng)問(wèn)的是局部數(shù)據(jù)或變量,這都是屬于你的,而全局的數(shù)據(jù)和變量是不太一樣的,因?yàn)槲覀儍蓚€(gè)執(zhí)行流是共享這部分內(nèi)容的。
3.
如果一個(gè)函數(shù)滿(mǎn)足以下條件也是不可重入函數(shù):
a.調(diào)用了malloc或free,因?yàn)閙alloc也是用全局鏈表來(lái)管理堆的。
b.調(diào)用了標(biāo)準(zhǔn)I/O庫(kù)函數(shù)。標(biāo)準(zhǔn)I/O庫(kù)的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
2.volatile關(guān)鍵字(保持內(nèi)存的可見(jiàn)性)
1.
以下代碼中,正常情況下,進(jìn)程收到2號(hào)信號(hào)時(shí)被handler方法捕捉,在handler方法里將quit置為1,當(dāng)handler執(zhí)行完畢返回的時(shí)候,while循環(huán)判斷為假,進(jìn)程代碼執(zhí)行結(jié)束,自動(dòng)退出。以上敘述情況確實(shí)正常,但當(dāng)gcc編譯時(shí)如果開(kāi)了-O3級(jí)別的優(yōu)化,并且quit全局變量沒(méi)有volatile修飾時(shí),此時(shí)進(jìn)程的運(yùn)行結(jié)果就不盡如人意了。
2.
當(dāng)無(wú)volatile修飾quit時(shí),即使quit已經(jīng)被改為1了,但進(jìn)程依舊沒(méi)有退出,執(zhí)行著main控制流里的while死循環(huán)代碼。當(dāng)有volatile修飾quit的時(shí)候,quit被改為1后,main控制流的while判斷為假,代碼執(zhí)行完畢,進(jìn)程退出。
3.
那到底是什么原因呢?其實(shí)本質(zhì)上和CPU的工作原理有關(guān)。
CPU的寄存器存儲(chǔ)的其實(shí)是臨時(shí)數(shù)據(jù),當(dāng)執(zhí)行完handler后,CPU會(huì)將quit=0這一數(shù)據(jù)內(nèi)容寫(xiě)回物理內(nèi)存中,因?yàn)閷⒂?jì)算的結(jié)果寫(xiě)入寄存器是沒(méi)有意義的,寄存器只保存臨時(shí)數(shù)據(jù),所以此時(shí)物理內(nèi)存中的quit為1,寄存器中的quit為0。
當(dāng)編譯器編譯時(shí)不開(kāi)優(yōu)化選項(xiàng),在執(zhí)行while判斷部分代碼時(shí),編譯器就會(huì)去物理內(nèi)存找quit的值,此時(shí)進(jìn)程就會(huì)正常退出。
而當(dāng)開(kāi)優(yōu)化選項(xiàng)時(shí),CPU檢測(cè)到你的while循環(huán)代碼上下文并未對(duì)quit做任何修改,此時(shí)為了編譯代碼的效率,CPU就不會(huì)去物理內(nèi)存中查找quit的數(shù)據(jù),而是直接在當(dāng)前寄存器內(nèi)部查找quit的數(shù)據(jù),而寄存器保存的是當(dāng)進(jìn)程代碼加載進(jìn)來(lái)的時(shí)候的quit的剛開(kāi)始的數(shù)據(jù),這個(gè)quit恰好是0,所以while判斷就一直為真,所以進(jìn)程就無(wú)法退出。
加volatile修飾代表的意思就是,無(wú)論你編譯器開(kāi)多高級(jí)別的優(yōu)化,在取數(shù)據(jù)時(shí)不要去寄存器里面拿數(shù)據(jù),而是去內(nèi)存里面拿,這樣就不會(huì)出現(xiàn)數(shù)據(jù)二義性的問(wèn)題。我們稱(chēng)volatile的這種作用為保持內(nèi)存的可見(jiàn)性。
3.SIGCHLD信號(hào)
1.
以前我們?cè)谔幚碜舆M(jìn)程退出問(wèn)題時(shí),采用的方法就是waitpid,采用waitpid比較麻煩,很影響main控制流,比如你進(jìn)行阻塞式等待,那main控制流就得停下來(lái)等你子進(jìn)程退出,如果你非阻塞式等待,main中還需要進(jìn)行輪詢(xún)的方式一遍一遍的去檢測(cè)你子進(jìn)程是否退出。
但今天在我們學(xué)習(xí)信號(hào)之后,回收子進(jìn)程就不用那么麻煩了!實(shí)際子進(jìn)程在退出時(shí),是會(huì)給父進(jìn)程發(fā)送17號(hào)信號(hào)SIGCHLD的,那父進(jìn)程注冊(cè)一個(gè)SIGCHLD信號(hào)的handler不就可以了嗎?這樣我父進(jìn)程就完全不用主動(dòng)的再去等子進(jìn)程了,而是當(dāng)子進(jìn)程退出的時(shí)候,直接給我父進(jìn)程發(fā)17號(hào)信號(hào),此時(shí)代碼轉(zhuǎn)到handler控制流執(zhí)行方法即可,在handler里面進(jìn)行子進(jìn)程的等待回收。
這樣就不用讓父進(jìn)程去主動(dòng)的等子進(jìn)程,而是我父進(jìn)程該干嘛干嘛,等你子進(jìn)程退出的時(shí)候,給父進(jìn)程打個(gè)電話(huà),告訴父進(jìn)程“我死了”,你趕快來(lái)回收我吧!
2.
在handler里面進(jìn)行子進(jìn)程等待的時(shí)候,其實(shí)要分情況的。
假如父進(jìn)程fork了大量的子進(jìn)程,子進(jìn)程在同一時(shí)刻都退出了,父進(jìn)程收到了大量的17號(hào)信號(hào),然后進(jìn)入handler方法內(nèi)部,此時(shí)單純只進(jìn)行一次的waitpid當(dāng)然是不行的,因?yàn)檫@么多進(jìn)程都退出了,你就回收一個(gè)???其他全變僵尸進(jìn)程了你不管?。克栽趆andler內(nèi)部要進(jìn)行while循環(huán)式的回收子進(jìn)程,我們將waitpid的第一個(gè)參數(shù)設(shè)置為-1,表示等待任意的子進(jìn)程退出。
那如果子進(jìn)程是分批退出的呢?在這種情況下,如果將waitpid設(shè)置為阻塞式等待(第三個(gè)參數(shù)傳0),就會(huì)出問(wèn)題,比如handler此時(shí)正在阻塞式等待某一子進(jìn)程退出,但其他子進(jìn)程過(guò)了一會(huì)兒又退出了,但你父進(jìn)程此時(shí)正在阻塞啊,就無(wú)法回收其他子進(jìn)程,所以waitpid一定要設(shè)置為非阻塞式等待(第三個(gè)參數(shù)傳WNOHANG),這樣的話(huà)如果為等待到子進(jìn)程退出,那么waitpid函數(shù)返回值就為0,此時(shí)handler內(nèi)部的循環(huán)結(jié)束,重新回到main執(zhí)行流,如果再有一批子進(jìn)程想要退出,那就再進(jìn)入handler即可。設(shè)置為非阻塞式等待就可以把任意時(shí)刻退出的任意子進(jìn)程都能夠回收了。
3.
實(shí)際上,除上面間接通過(guò)waitpid的方式回收僵尸進(jìn)程外,還可以通過(guò)父進(jìn)程調(diào)用sigaction()或者是signal()將SIGCHLD的處理動(dòng)作置為SIG_IGN,這樣fork出來(lái)的子進(jìn)程在終止時(shí)會(huì)自動(dòng)清理掉,不會(huì)產(chǎn)生僵尸進(jìn)程,也不會(huì)向父進(jìn)程發(fā)送信號(hào)。
但其實(shí)SIGCHLD的默認(rèn)行為就是忽略,一般情況下,系統(tǒng)默認(rèn)的忽略和我們手動(dòng)設(shè)置的忽略,通常是沒(méi)有區(qū)別的,但這里是一個(gè)例外。操作系統(tǒng)對(duì)我們手動(dòng)設(shè)置的SIG_IGN做了特殊處理,幫我們做了回收子進(jìn)程的工作。
注意:此方法對(duì)于Linux系統(tǒng)可用,但不保證在其他UNIX系統(tǒng)上也可用,比如MAC OS 或 直接本身就是UNIX操作系統(tǒng)。
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-422470.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-422470.html
到了這里,關(guān)于【Linux】進(jìn)程信號(hào) --- 信號(hào)的產(chǎn)生 保存 捕捉遞達(dá)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!