網(wǎng)站建設(shè)管理及維護百度中心人工電話號碼
目錄
一、信號被處理的時機:
1、理解:
2、內(nèi)核態(tài)與用戶態(tài):
1、概念:
2、重談地址空間:
3、處理時機:
補充知識:
1、sigaction:
2、函數(shù)重入:
3、volatile:
4、SIGCHLD:
前言:
信號在保存后,當(dāng)OS準(zhǔn)備對信號進行處理的時候,還需要到合適的時機才能進行處理,那么什么是合適的時機?
一、信號被處理的時機:
1、理解:
信號的產(chǎn)生是異步的
首先,要將信號進行處理之前就需要讓OS知道某個進程收到了信號了,所以進程就需要在合適的時機查查其對應(yīng)的block,pending,handler表,但是這三個表都屬于內(nèi)核級別的,我們用戶級別的進程是不允許訪問的,所以這里就自然涉及了進程的用戶態(tài)和內(nèi)核態(tài)之間的轉(zhuǎn)化
合適的時機:在進程從內(nèi)核態(tài)轉(zhuǎn)化到用戶態(tài)的時候?qū)π盘栠M行檢測,如果有并未被屏蔽就進行處理
怎么進行處理:默認(rèn),忽略,用戶自定義
2、內(nèi)核態(tài)與用戶態(tài):
1、概念:
進程在執(zhí)行代碼的時候不僅僅是只執(zhí)行用戶的代碼,還有操作系統(tǒng)的代碼,想要訪問操作系統(tǒng)就需要變成內(nèi)核態(tài),執(zhí)行用戶的代碼就要變成用戶態(tài):
用戶態(tài):進程只能訪問用戶自己的代碼和數(shù)據(jù)
內(nèi)核態(tài):進程允許訪問操作系統(tǒng)的代碼和數(shù)據(jù)
用戶態(tài)-->內(nèi)核態(tài)
進程時間片到了,進行進程切換的時候
調(diào)用系統(tǒng)調(diào)用接口:open,read等等
產(chǎn)生異常,中斷的時候等等
內(nèi)核態(tài)-->用戶態(tài)
進程切換完畢
系統(tǒng)調(diào)用結(jié)束后
異常,中斷處理完后
OS是不會收到信號就立即執(zhí)行的,比如當(dāng)前我正在進行系統(tǒng)調(diào)用或者正在切換進程等等,在從內(nèi)核態(tài)轉(zhuǎn)化為用戶態(tài)的時候,就進行信號的查詢?nèi)砗吞幚硇盘?#xff0c;所以要等到OS將更重要的事情忙完后,在進程從內(nèi)核態(tài)到用戶態(tài)的時候就進行信號的檢測和處理
2、重談地址空間:
如下,我們回顧下我們地址空間的知識:
每個進程都有其獨有的虛擬地址空間,進程具有獨立性
通過頁表的映射+MMU機制進行虛擬到物理地址之間的轉(zhuǎn)化
進程都具有其對應(yīng)的虛擬地址空間,能夠讓進程以統(tǒng)一的時間看待我們的代碼和數(shù)據(jù)
?
虛擬地址空間又可分為兩個空間0~3GB的空間為用戶空間,3~4GB的空間為內(nèi)核空間
為什么要區(qū)分內(nèi)核空間和用戶空間?
因為內(nèi)核空間中存放的是OS的代碼和數(shù)據(jù),是不允許進程隨便訪問的,需要進程切換到內(nèi)核態(tài)才能進行訪問,并且規(guī)劃也能夠OS進行更好的管理
其中:用戶空間就是我們的代碼和數(shù)據(jù),當(dāng)進程在用戶態(tài)的時候就能夠訪問這段空間的代碼和數(shù)據(jù)
內(nèi)核空間中存放的就是OS的代碼和數(shù)據(jù),這里的虛擬地址通過特有的內(nèi)核級頁表從虛擬地址空間映射到物理內(nèi)存中,由于OS是最先被加載的程序,所以其映射應(yīng)該在較為底部的位置
我們知道每一個進程都有其對應(yīng)的進程地址空間, 那么是不是每一個進程都有其獨特的內(nèi)核空間和內(nèi)核級頁表呢?
答案是只有一份
對應(yīng)用戶空間和用戶級頁表有很多份的,因為進程具有獨立性
但是內(nèi)核級頁表只有一份,內(nèi)核空間比較特殊,所有進程最終映射到物理內(nèi)存都是同一塊區(qū)域,進程只是將操作系統(tǒng)代碼和數(shù)據(jù)映射入自己的進程地址空間而已,其中內(nèi)核級頁表只需將虛擬地址空間映射到物理內(nèi)存,所有進程都是如此
所以,每一個進程看到的內(nèi)核空間中的內(nèi)容,看到的內(nèi)核級頁表資源,最后映射到的物理內(nèi)存中的代碼和數(shù)據(jù)都是一樣的
所以在用戶空間的代碼區(qū)中執(zhí)行對應(yīng)的系統(tǒng)調(diào)用,
首先將進程從用戶態(tài)轉(zhuǎn)化到內(nèi)核態(tài),
然后在自己的內(nèi)核空間中找到對應(yīng)的系統(tǒng)調(diào)用方法,
然后通過內(nèi)核級頁表映射到物理內(nèi)存找到對應(yīng)的代碼和數(shù)據(jù),
然后在返回(在返回的時候進程從內(nèi)核態(tài)轉(zhuǎn)化為用戶態(tài)),
這樣就相當(dāng)與在進程自己的進程地址空間中進行系統(tǒng)調(diào)用
那么上述的切換是如何進行切換的呢?
首先,我們知道,CR3 寄存器存儲當(dāng)前進程的頁目錄表物理地址,用于分頁機制下的內(nèi)存管理,
在CPU中同樣還有一個叫做ESC寄存器的東西,這個寄存器的作用就是用來表示當(dāng)前進程的狀態(tài):是用戶態(tài)還是內(nèi)核態(tài)
那么這個寄存器是怎樣實現(xiàn)的呢?
在這個寄存器中,有著最后兩個比特位,兩個比特位有4種表示方式:00 01 10 11,其中只使用兩種方式,00和11也就是對應(yīng)十進制的0和3,
當(dāng)這個寄存器最后兩個比特位為0的時候,表示當(dāng)前進程處于內(nèi)核態(tài)
當(dāng)這個寄存器最后兩個比特位為1的時候,表示當(dāng)前進程處于用戶態(tài)
所以切換進程的狀態(tài)就需要將ESC寄存器中的最后兩個比特位修改為對應(yīng)的值,CPU為我們提供了一種方法來修改自己的工作狀態(tài):int 0x80指令
小總結(jié):
1、每一個進程中的0~3GB中的內(nèi)容是不一樣的,因為進程具有獨立性
2、每一個進程中的3~4GB中的內(nèi)容是一樣的,在整個系統(tǒng)中,無論進程再怎么切換,? ? ? ? ? ? ? ? 3~4GB中的內(nèi)核空間內(nèi)容是不變的
3、在進程視角:我們進行系統(tǒng)調(diào)用,就是在我自己的地址空間中進行執(zhí)行的
4、操作系統(tǒng)視角:任何一個時刻,都有進程執(zhí)行,想執(zhí)行OS的代碼可以隨時執(zhí)行
5、操作系統(tǒng)本質(zhì)是一個基于時鐘中斷的一個死循環(huán)
怎么理解操作系統(tǒng)的執(zhí)行邏輯呢或者說操作系統(tǒng)的本質(zhì)是什么?
其可以理解為一種基于中斷驅(qū)動的“死循環(huán)”模型,核心在于通過時鐘中斷等機制實現(xiàn)任務(wù)調(diào)度,資源管理和實時響應(yīng)
主框架循環(huán):
操作系統(tǒng)的核心代碼是一個死循環(huán)(for(; ;) pause();?),這個循環(huán)并非是空轉(zhuǎn)的,而是通過中斷機制被動喚醒的,當(dāng)沒有外部事件(用戶輸入,I/O完成)或內(nèi)部事件(如時間片耗盡)時,操作系統(tǒng)會進入低功耗狀態(tài)或執(zhí)行空閑任務(wù)
其中操作系統(tǒng)本會一直卡在pause()這個行代碼暫停,等到發(fā)生中斷機制,被進程“推著走”才能夠讓代碼得以運行
那有什么中斷機制呢?
時鐘中斷:每過一定很短的時間產(chǎn)生一次,用于更新系統(tǒng)時間、檢查進程時間片、觸發(fā)調(diào)度?
硬件中斷:如鍵盤輸入,由外設(shè)通過中斷控制器通知CPU?
軟件中斷:如系統(tǒng)調(diào)用,允許用戶程序訪問內(nèi)核空間
進程是如何被操作系統(tǒng)調(diào)用的呢?
進程被調(diào)度,就意味著它的時間片到了,操作系統(tǒng)會通過時鐘中斷,檢測到是哪一個進程的時間片到了,然后通過系統(tǒng)調(diào)用函數(shù)schedule()保存進程的上下文數(shù)據(jù),然后選擇合適的進程去運行
3、處理時機:
有了上述鋪墊的知識,接下來可以理解:
我們前面了解到,在進程從內(nèi)核態(tài)轉(zhuǎn)化到用戶態(tài)的時候?qū)π盘栠M行檢測,處理方式如下圖:
對于上述的執(zhí)行用戶自定義有兩個問題:
為什么要切回用戶態(tài)再執(zhí)行對應(yīng)的方法:
因為如果在內(nèi)核態(tài)中執(zhí)行用戶自定義的方法,可能自定義方法中存在危害操作系統(tǒng)的代碼,這是不安全的
為什么在用戶態(tài)執(zhí)行完畢后還要返回內(nèi)核態(tài)再到用戶態(tài)
需要回到內(nèi)核態(tài)找到進程的上下文,將上下文帶出到用戶態(tài)才行,并且自定義的動作和待返回的進程是屬于不同的堆棧,不能夠直接返回
對于信號捕捉的理解可以看看下面這張圖
如上,這是一個橫著的8,然后用一橫貫穿,上面是用戶態(tài),下面是內(nèi)核態(tài),注意有4個交點,并且8的交點是在橫線下方的也就是在內(nèi)核態(tài)中
接下來解釋上圖:
四個綠色的圈圈就表示兩態(tài)之間的切換了四次,當(dāng)進程時間片到了,進行進程切換,產(chǎn)生異常,中斷的時候等等就進行用戶態(tài)到內(nèi)核態(tài)之間的轉(zhuǎn)化
進程切換完畢,系統(tǒng)調(diào)用結(jié)束后,異常,中斷處理完后進行內(nèi)核態(tài)到用戶態(tài)之間的轉(zhuǎn)化
此時就進行信號的檢測:首先看pending表,如果其為1并且該信號沒有被阻塞就執(zhí)行對應(yīng)動作,如果被屏蔽或者為0就繼續(xù)從pending表向下找下一個,以此類推
二、補充知識:
1、sigaction:
其中有個新的結(jié)構(gòu)體:sigaction,其內(nèi)部成員如下
我們只關(guān)心第一個和第三個成員:
成功返回0,失敗返回-1,錯誤碼被設(shè)置
第一個參數(shù)signum:信號編號
第二個參數(shù)act: 傳入該類結(jié)構(gòu)體,設(shè)置屏蔽信號什么的在之前就要設(shè)置好
第三個參數(shù)oldact:保存修改前的結(jié)構(gòu)體
其中sigaction中的第一個成員變量:就是signal的第二個參數(shù),需要自己設(shè)置自定義
第三個成員變量就是屏蔽的信號集,怎么理解呢?
首先我們知道,比如當(dāng)在處理2號信號的時候,如果sa_mask默認(rèn),那么OS就只會屏蔽2號信號,如果還想要屏蔽更多信號,就需要sigaddset(&act.sa_mask,1);這樣在待傳入結(jié)構(gòu)體中的sa_mask進行更多的設(shè)置來屏蔽
void Printpending()
{sigset_t set;sigpending(&set);for(int signo = 31; signo>=1; signo--){if(sigismember(&set,signo)) cout <<"1";else cout<<"0";}cout << endl;
}void myhandler(int signo)
{cout << "get a signo : " << signo << endl;while(1){Printpending();sleep(1);}
}int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));//清空信號集sigemptyset(&act.sa_mask);//添加屏蔽信號sigaddset(&act.sa_mask,1);sigaddset(&act.sa_mask,3);sigaddset(&act.sa_mask,4);sigaddset(&act.sa_mask,9);//設(shè)置自定義方法act.sa_handler = myhandler;sigaction(2,&act,&oact); while(1){cout << "i am a process mypid : " << getpid() << endl;sleep(1);}return 0;
}
如上,這樣就將1?3 4號都屏蔽了,9號和19號信號可以經(jīng)過試驗發(fā)現(xiàn)不可屏蔽,
2、函數(shù)重入:
可以被重復(fù)進入的函數(shù)稱為可重入函數(shù)
如下是一個場景:
如上,當(dāng)在insert函數(shù)中捕捉到了信號,并且在信號的自定義動作中又調(diào)用了insert,這樣函數(shù)就會重入,這樣就會導(dǎo)致函數(shù)節(jié)點丟失,在釋放的時候無法釋放node2,就會導(dǎo)致內(nèi)存泄漏
這就是函數(shù)重入導(dǎo)致的內(nèi)存泄漏問題
我們把這種函數(shù)稱為可重入函數(shù)(注意:這個是特性,不具有褒貶含義)
3、volatile:
int flag;void myhandler(int signo)
{cout << "get a signo : " << signo << endl;flag = 1;
}int main()
{signal(2,myhandler);while(!flag); //flag = 0 為假 !flag 為真cout << "a process quit ! " << endl;return 0;
}
如上,上述本來是一個死循環(huán),但是當(dāng)給當(dāng)前進程發(fā)送2號信號的時候就會進入我們的自定義函數(shù)調(diào)用,這個時候在里面將flag修改為1,這樣就能夠退出while死循環(huán)了,并且我們的代碼運行起來也是達到預(yù)期了
但是在編譯中,有個-O1/O2/O3這種的優(yōu)化,如下,我們把這種優(yōu)化帶著,在運行代碼試試
如上,此時發(fā)現(xiàn)盡管我們給當(dāng)前進程發(fā)送2號信號,并且也看到了自定義代碼中打印的字符串,但是進程卻沒有退出while這個死循環(huán)
這是為什么呢?
這是因為在編譯的時候進行了O1的優(yōu)化
如上,正常情況下,CPU在每次進行邏輯檢測的時候,每次都從內(nèi)存中進行讀入,這種IO比較費事,當(dāng)進行O1的優(yōu)化之后,就會對整個代碼進行檢測,此時沒有信號就檢查不到flag被修改了,此時就會將flag放入邏輯檢測的寄存器中,這樣,當(dāng)在自定義中修改了flag,這只是把內(nèi)存中的flag修改了,但是CPU寄存器中的flag并沒有被修改,所以就會一直繼續(xù)死循環(huán)
為了防止這種過度優(yōu)化,保存內(nèi)存的可見性,可以在全局變量前面加上volatile關(guān)鍵字修飾,這樣就無法將flag放入到寄存器中,老老實實地每次都從內(nèi)存中進行讀入
4、SIGCHLD:
在前面實現(xiàn)進程等待的時候,每次當(dāng)子進程退出的時候,父進程都需要等待,回收子進程,防止其成為僵尸進程造成內(nèi)存泄漏問題
父進程有兩種方式等待子進程:設(shè)置0為阻塞等待或者設(shè)置WNOHANG為非阻塞輪詢
但是上述兩種方式都有缺陷,要么父進程阻塞,不能做其自己的事情,要么每次工作的時候都還要關(guān)心關(guān)心子進程的狀態(tài)
那么有沒有一種方式能夠讓父進程不在關(guān)心子進程,當(dāng)子進程退出的時候自動回收呢?
有的有的:
首先要了解:子進程在退出的時候會向父進程發(fā)送17號信號,所以我們可以在父進程中將17好信號捕捉,然后在自定義函數(shù)中等待,這里設(shè)置第一個參數(shù)為-1為等待任意進程
void myhandler(int signo)
{pid_t rid = waitpid(-1,nullptr,0);cout << "a signo get : " << signo << " mypid " << getpid() << " rid : " << rid << endl;
}int main()
{signal(17,myhandler);pid_t id = fork();if(id == 0){//childcout << "child process " << getpid() << " myppid : " << getppid() << endl;sleep(5);exit(0);}//fatherwhile(1){cout << "father process " << getpid() << endl;sleep(1);}return 0;
}
如上,這樣父進程就可以不用管子進程了,能夠完成子進程的自動回收
但是這樣還不夠,如果有多個子進程呢?能夠全部回收嗎
這顯然是不會的,SIGCHLD這是一個信號,當(dāng)父進程收到了第一個信號的時候,會將block表中的17號信號置為1使其屏蔽,這樣,其他子進程的信號就丟失了,就會導(dǎo)致僵尸進程
那么如何解決呢?
我們在自定義中采取while循環(huán)的方式回收即可
當(dāng)然,還有一種更方便的,但是只能在Linux中有效:
將SIGCHLD這個信號的默認(rèn)動作設(shè)置為忽略,這樣父進程不會對其處理,但是當(dāng)子進程退出之后,OS會對其負(fù)責(zé),這樣的話就會自動清理資源并回收,不會產(chǎn)生僵尸進程引起內(nèi)存泄漏