韓語淘寶代購網(wǎng)站建設(shè)東莞關(guān)鍵詞自動排名
文章目錄
- 進(jìn)程信號
- 信號的產(chǎn)生方式(信號產(chǎn)生前)
- 1. 硬件產(chǎn)生
- 2.調(diào)用系統(tǒng)函數(shù)向進(jìn)程發(fā)信號
- 3.軟件產(chǎn)生
- 4.定位進(jìn)程崩潰的代碼(進(jìn)程異常退出產(chǎn)生信號)
- 信號保存的方式(信號產(chǎn)生中)
- 獲取pending表&&修改block表
- 信號的處理(信號處理時)
- 修改handler函數(shù)補(bǔ)充—— sigaction 系統(tǒng)調(diào)用
進(jìn)程信號
信號在生活中無處不在,例如鬧鐘、紅綠燈,快遞到達(dá)發(fā)的短信等等
-
信號的例子
例如在網(wǎng)上你買了一個東西就是信號的注冊;
快遞員該你打電話要你拿一下快遞,就是給你發(fā)送了一個信號;
你收到信號之后,你知道怎么去處理這個信號,在這里就是去拿快遞;
但是你也不一定立馬去拿,你可能會等你忙完現(xiàn)在的事在去處理;
從技術(shù)的角度說,平時電腦上按
alt+f4
就是一種信號,它會關(guān)閉當(dāng)前的窗口,Linux中ctrl+c
可以終止進(jìn)程
信號是進(jìn)程之間事件異步通知的一種方式,屬于軟中斷
- 信號的種類
信號的種類可以通過
kill -l
命令來查看 Linux 系統(tǒng)的信號列表:
如圖,其中1-31是普通信號,也是我們在這里要重點(diǎn)學(xué)習(xí)的信號;34-64是實(shí)時信號
信號的產(chǎn)生方式(信號產(chǎn)生前)
1. 硬件產(chǎn)生
? 例如當(dāng)我們的程序發(fā)生死循環(huán)時,按下CTRL+C
,就可以終止進(jìn)程。CTRL+C
的本質(zhì)其實(shí)是向進(jìn)程發(fā)送2號信號SIGINT,而SIGINT的默認(rèn)處理動作是從鍵盤中斷(man 7 signal查看手冊)
? 那我們可以把信號2的默認(rèn)處理動作更換成我們自己的驗(yàn)證一下,這里需要用到
sighandler_t signal(int signum, sighandler_t handler);
這里我讓收到2號信號后先打印一段話再退出
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void mysignal_2(int signo)
{printf("你好signo:%d\n",signo);exit(1);
}int main()
{signal(2,mysignal_2); //相當(dāng)于一個函數(shù)指針,這里填的函數(shù)名相當(dāng)于函數(shù)地址while(1){printf("hello\n");sleep(1);}return 0;
}
可以看到運(yùn)行結(jié)果:按下ctrl+c后,先打印再終止進(jìn)程
例如野指針問題引起的段錯誤,只要運(yùn)行程序就會崩潰,那么下面驗(yàn)證一下
先把1-31的信號處理動作全部換成自定義,之后捕捉信號查看野指針對應(yīng)的信號
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{printf("signo:%d\n",signo);exit(1);
}int main()
{int sig = 1;for(;sig <= 31; sig++){signal(sig,handler);}while(1){int* p = NULL;*p = 100;printf("hello\n");sleep(1);}return 0;
}
運(yùn)行結(jié)果可知:野指針引起的進(jìn)程崩潰是收到了11信號
再例如除0錯誤,把上面的代碼修改一下進(jìn)行驗(yàn)證:
while(1){int i= 0;i /= 0;printf("hello\n");sleep(1);}
可以看到,進(jìn)程也是直接就崩潰了,是因?yàn)槭盏搅?號信號
上述提到的ctrl+c、除0錯誤、野指針的段錯誤為什么會產(chǎn)生退出信號呢?
首先要知道,os是硬件的管理者,負(fù)責(zé)管理好硬件資源監(jiān)視硬件狀態(tài)等等,
- ctrl+c是由鍵盤發(fā)出的,os檢測到鍵盤發(fā)出的信號就會向進(jìn)程發(fā)出2號信號;
- 除0時是在CPU中運(yùn)算的,CPU中有一些寄存器,會記錄運(yùn)行的狀態(tài),除0時的運(yùn)算會產(chǎn)生異常寄存器就會記錄這個狀態(tài),OS檢測到這個異常狀態(tài),就會向進(jìn)程發(fā)送8號信號;
- 當(dāng)我們對空指針進(jìn)行直接賦值時,OS會發(fā)現(xiàn)我們的進(jìn)程地址空間與物理內(nèi)存的映射對不上,就會向進(jìn)程發(fā)送11號信號
? 綜上這些錯誤分別體現(xiàn)在鍵盤、CPU、內(nèi)存中,所以軟件層面上的錯誤。
? 也就是體現(xiàn)在硬件或其他軟件上,而OS是硬件的管理者,發(fā)現(xiàn)硬件出現(xiàn)一些異常,就會反饋回出現(xiàn)異常的代碼所在的進(jìn)程上,進(jìn)程根據(jù)對應(yīng)的信號進(jìn)行處理(上面提到的錯誤都是默認(rèn)終止進(jìn)程)
2.調(diào)用系統(tǒng)函數(shù)向進(jìn)程發(fā)信號
系統(tǒng)提供了一些函數(shù)接口,可以手動發(fā)送信號
int kill(pid_t pid, int signo);
- pid 表示目標(biāo)進(jìn)程的 pid 。
- sig 表示要發(fā)給目標(biāo)進(jìn)程的信號。
- 成功返回0,失敗返回-1
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>static void Usage(const char* proc)
{printf("Usage:\n\t %s signo who \n",proc);
}int main(int argc ,char* argv[])
{if(argc != 3){Usage(argv[0]);return 1;}int signo = atoi(argv[1]);int who = atoi(argv[2]);kill(who,signo);printf("signo:%d who:%d\n",signo,who);return 0;
}
-
int raise(int signo);
給自己發(fā)送信號,signo:要發(fā)送的信號
int main() {sleep(1);raise(3);return 0; }
-
void abort(void);
給自己發(fā)送固定的信號6
3.軟件產(chǎn)生
unsigned int alarm(unsigned int seconds);
- seconds是代表幾秒后執(zhí)行
- 執(zhí)行成功返回0,取消定時則返回剩余秒數(shù)
? 調(diào)用alarm函數(shù)可以設(shè)定一個鬧鐘,也就是告訴內(nèi)核在seconds秒之后給當(dāng)前進(jìn)程發(fā)SIGALRM信號, 該信號的默認(rèn)處理動作是終止當(dāng)前進(jìn)程。
int main()
{alarm(30);sleep(4);int ret = alarm(0); //取消定時printf("%d\n",ret);return 0;
}
還有管道通信時,寫端退出,讀端收到13號信號等等
4.定位進(jìn)程崩潰的代碼(進(jìn)程異常退出產(chǎn)生信號)
進(jìn)程退出一般分為三種:
- 代碼運(yùn)行完畢,結(jié)果正常
- 代碼運(yùn)行完畢,結(jié)果不正常
- 代碼異常直接終止
如圖:退出碼+退出信號+core dump標(biāo)志位共16位,waitpid函數(shù)接口的第二個參數(shù)可以獲取到并查看退出碼和退出信號
(具體用法自行搜索,這里不做介紹)
? 在Linux中是可以定位到具體崩潰到哪一行的代碼的,當(dāng)一個進(jìn)程退出的時候它會根據(jù)退出的情況置退出碼或者退出信號,表明退出的原因,如果必要,OS會設(shè)置退出信息中的core dump標(biāo)志位,并將進(jìn)程在內(nèi)存中的數(shù)據(jù)轉(zhuǎn)存到磁盤上,以便后期調(diào)試
云服務(wù)器上的core dump是默認(rèn)關(guān)閉的,可以輸入 ulimit -c 102400
打開core dump,ulimit -a
可以查看
這里用除0錯誤演示一下:
int main()
{while(1){int i = 0;i /= 0;printf("hello\n");sleep(1);}return 0;
}
? 在我們運(yùn)行程序后,除了顯示退出原因,還會顯示(core dumped),并且多了一個 core.5195文件,之后利用gdp調(diào)試打開我的可執(zhí)行程序(Linux默認(rèn)是release版本的,需要在編譯時加上 -g)
之后輸入core-file core.5195
加載 core.5195文件
也不是所有信號都會core dump,例如死循環(huán)使用ctrl+c的2號信號,或者 kill -9殺掉進(jìn)程就沒有core dump
總結(jié):信號產(chǎn)生的方式有很多,但本質(zhì)都是由OS向目標(biāo)進(jìn)程發(fā)送的
信號保存的方式(信號產(chǎn)生中)
在Linux中,一個進(jìn)程收到信號也不一定馬上處理,待當(dāng)前工作處理完成后尋找合適時機(jī)處理,那就需要PCB有保存信號的能力
先來解釋三個名詞:
實(shí)際執(zhí)行信號的處理動作稱為信號遞達(dá)(delivery)
信號遞達(dá)可以是自定義捕捉、默認(rèn)、忽略
信號從產(chǎn)生到遞達(dá)之間的狀態(tài)稱為信號未決(Pending)
本質(zhì)是這個信號因?yàn)橐恍┰?#xff08;如優(yōu)先級低等)被暫時存在了task_struct中
進(jìn)程可以選擇阻塞某個信號(block)
本質(zhì)是OS允許進(jìn)程暫時屏蔽指定的信號,被阻塞的信號產(chǎn)生時將保持在未決狀態(tài),直到進(jìn)程解除對此信號的阻塞,才執(zhí)行遞達(dá)的動作
注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達(dá),而忽略是在遞達(dá)之后可選的一種處理動作。
在進(jìn)程的task_struct內(nèi)有三張表,是用來保存信號狀態(tài)的
pending 表和 block 表都是位圖,而 handler 表是一個函數(shù)指針數(shù)組,指向的是信號遞達(dá)時對應(yīng)的處理動作(默認(rèn)/忽略/自定義)
pending和block位圖的比特位的位置代表信號的編號
pending位圖的內(nèi)容代表是否收到信號
block位圖內(nèi)容代表是否被阻塞(阻塞位圖也叫信號屏蔽字)
信號處理過程示例:
只要block為1,那么pending不管是1還是0,該信號都是阻塞狀態(tài)
有了這三張表,進(jìn)程就可以識別信號了
因?yàn)閠ask_struct是內(nèi)核的,沒有人能寫入到內(nèi)核,只有OS可以,所以信號的產(chǎn)生方式都是由OS統(tǒng)一向PCB發(fā)送的,
(不是所有信號都會被屏蔽,例如9號信號)
獲取pending表&&修改block表
? pending表和block表的位圖結(jié)構(gòu),就是由這個sigset_t的類型來存儲的。sigset_t是一個位圖結(jié)構(gòu)類型,稱為信號集;這個類型可以表示每個信號的有效或無效狀態(tài),在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決定狀態(tài)。
? 由于每個平臺的sigset_t信號集實(shí)現(xiàn)位圖的方法不一定一樣,所以不推薦用戶對這個信號集直接進(jìn)行修改信號集,需要通過OS提供的信號集操作函數(shù)進(jìn)行修改。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
int sigemptyset(sigset_t *set):
初始化set所指向的信號集,使其中
所有信號的對應(yīng)bit清零
,表示該信號集不包含任何有效信號成功返回0,失敗返回-1
int sigfillset(sigset_t *set):
初始化 set ,將其中所有信號對應(yīng)的比特位置 1 。
成功返回0,失敗返回-1
int sigaddset (sigset_t *set, int signo)
把signo信號添加到 set 集合里,其實(shí)就是把signo信號對應(yīng)的比特位由 0 置 1
成功返回0,失敗返回-1
int sigdelset(sigset_t *set, int signo)
把 signo 信號從 set 集合里刪去,其實(shí)就是把 signo 信號對應(yīng)的比特位由 1 置 0
成功返回0,失敗返回-1
int sigismember(const sigset_t *set, int signo)
判定 signo 信號是否在 set 集合里,其實(shí)就是判定 signo 信號對應(yīng)的比特位是否為 1
包含就返回1,不包含返回0,失敗返回-1
有了上面的信號集操作函數(shù)后,想要修改block表就需要用到sigprocmask 系統(tǒng)調(diào)用
sigprocmask
:獲取或更改當(dāng)前進(jìn)程的信號屏蔽字
how:表示要對信號屏蔽字進(jìn)行的操作類型
SIG_BLOCK:set 包含了我們希望添加到當(dāng)前信號屏蔽字的信號。 (mask = mask | set )
SIG_UNBLOCK:set 包含了我們希望從當(dāng)前信號屏蔽字中解除阻塞的信號。 (mask = mask & ~set)
SIG_SETMASK:設(shè)置當(dāng)前信號屏蔽字為 set 。 (mask = set) 常用set:輸入型參數(shù),就是我們要設(shè)置的信號集的指針
oldset:輸出型參數(shù),用來獲取原信號集(可設(shè)置NULL)
返回值:成功返回0,失敗返回-1
而想要獲取pending表就要用到sigpending 系統(tǒng)調(diào)用
- 參數(shù):傳出型參數(shù),用于存儲進(jìn)程pending表
- 返回值:成功返回0,失敗返回-1
示例:阻塞2號信號
void test1()
{sigset_t iset,oset;//1.先清空sigemptyset(&iset);sigemptyset(&oset);//2.給iset指定信號編號sigaddset(&iset,2);//3.設(shè)置屏蔽字sigprocmask(SIG_SETMASK,&iset,&oset);while(1){printf("hello\n");sleep(1);}}
這時按ctrl+c是無效的,只能通過kill -9來結(jié)束進(jìn)程
示例:先阻塞2號信號,后獲取pending表并打印,20秒后放開2號信號
void test2()
{//設(shè)置信號集sigset_t iset,oset;sigemptyset(&iset);sigemptyset(&oset);//初始化信號集sigaddset(&iset,2);sigprocmask(SIG_SETMASK,&iset,&oset); //阻塞sigset_t pending;//用于獲取打印用int count = 0;while(1){if(count == 20)sigprocmask(SIG_SETMASK,&oset,NULL); //解除2號的阻塞(更改阻塞的信號集為oset,oset為全0)sigemptyset(&pending); //先清空sigpending(&pending); //在獲取sleep(1); //延時1scount++; //計(jì)數(shù)show_pending(&pending); //打印}
}
20s后,2號被取消阻塞,信號遞達(dá),進(jìn)程退出
綜上步驟:
想要獲取pending表或者更改block表的必備步驟是:
- 設(shè)置信號集
- 初始化信號集
信號的處理(信號處理時)
? 每個進(jìn)程都有自己的地址空間和對應(yīng)的頁表,地址空間一般一共4G,3G的用戶空間,而剩下的1G空間是內(nèi)核空間,存儲的是OS的數(shù)據(jù)和代碼,且內(nèi)核空間也有對應(yīng)的內(nèi)核頁表映射到物理內(nèi)存上,但是內(nèi)核頁表不管有多少個進(jìn)程,都只共享一份;保證無論進(jìn)程怎么切換,都能夠找到同一個OS
? 進(jìn)程是可以看到內(nèi)核和用戶的內(nèi)容的,但是不一定能夠訪問,需要有權(quán)限來證明進(jìn)程處于哪種工作模式,在進(jìn)程里面是有對應(yīng)的相關(guān)數(shù)據(jù)來標(biāo)識進(jìn)程的工作模式的(用戶模式 / 內(nèi)核模式)
- 想要訪問內(nèi)核數(shù)據(jù),就需要切換至內(nèi)核態(tài)
- 想要訪問用戶數(shù)據(jù),就需要切換至用戶態(tài)
這個數(shù)據(jù)會被加載到 CPU 的其中一個寄存器當(dāng)中(CR3),用于保存當(dāng)前進(jìn)程是處于用戶態(tài)還是處于內(nèi)核態(tài)。
- 內(nèi)核態(tài):執(zhí)行 OS 的代碼和數(shù)據(jù)時所處的狀態(tài)。OS 的代碼的執(zhí)行全部都是在內(nèi)核態(tài)。
- 用戶態(tài):用戶的代碼和數(shù)據(jù)被訪問或執(zhí)行時所處的狀態(tài)。也就是說我們寫的代碼全部都是在用戶態(tài)執(zhí)行的。
而系統(tǒng)調(diào)用就是:將進(jìn)程切換至內(nèi)核態(tài)去調(diào)用系統(tǒng)函數(shù)
信號是如何處理的?
? 信號是被保存在PCB中的pending位圖里面,處理的工作分為檢測、遞達(dá)(默認(rèn)、忽略、自定義),當(dāng)進(jìn)程從內(nèi)核態(tài)返回到用戶態(tài)的時候,進(jìn)行信號的處理工作
處理的流程大致如下圖所示:
抽象圖:
為什么不能由OS直接執(zhí)行捕捉函數(shù)方法?
? 要知道OS是不相信任何人的,handler方法是用戶定義的,內(nèi)核態(tài)只能執(zhí)行OS的代碼數(shù)據(jù),所以首先再身份上就不合適。其次因?yàn)镺S只相信自己,也就是只相信自己的代碼和數(shù)據(jù)是安全的,假如handler的函數(shù)方法定義一些惡意代碼,那么OS等于以它的權(quán)限去執(zhí)行,造成安全隱患。所以O(shè)S要保護(hù)好自己
什么時候處理信號?從內(nèi)核態(tài)切換回用戶態(tài)的時候進(jìn)行信號檢測并處理
?
? **當(dāng)某個信號的處理函數(shù)被調(diào)用時,內(nèi)核自動將當(dāng)前信號加入進(jìn)程的信號屏蔽字,當(dāng)信號處理函數(shù)返回時自動恢復(fù)原來的信號屏蔽字。**這樣就保證了在處理某個信號時,如果這種信號再次產(chǎn)生,那么它(第二次)會被阻塞到當(dāng)前處理結(jié)束為止
? 在 Linux 中,如果把進(jìn)程的某一個普通信號屏蔽了,然后 OS 給這個進(jìn)程多次發(fā)送該信號,該進(jìn)程只能記住一次,因?yàn)橛涗浶盘柕臉?biāo)記位只有一個比特位。也就是說,在 Linux 中,普通信號是可能會被丟失的。而實(shí)時信號不會被丟失,因?yàn)閮?nèi)核是以鏈表隊(duì)列的形式把所有的實(shí)時信號組織起來的,來一個就鏈一個。(數(shù)據(jù)結(jié)構(gòu)的差別)
修改handler函數(shù)補(bǔ)充—— sigaction 系統(tǒng)調(diào)用
和signal函數(shù)的功能是一樣的,都是修改handler表中的方法
- signum 表示要設(shè)置的信號
- act 輸入型參數(shù),需填充該結(jié)構(gòu)體里面 signum 的對應(yīng)處理動作。
- oldact 輸出型參數(shù),若為非空,則帶回內(nèi)含老的處理動作的結(jié)構(gòu)體;若不關(guān)心,則可設(shè)為 NULL 。
- 成功返回0,失敗返回-1
與信號集sigset_t相關(guān)的操作類似
- sa_handler 代表 signum 的對應(yīng)處理動作
- sa_mask 代表在調(diào)用信號處理函數(shù)時需要額外屏蔽的信號
- sa_flags 代表選項(xiàng),我們在這里設(shè)為 0
- sa_sigaction 和 sa_restorer 通常與實(shí)時信號相關(guān)聯(lián),我們在這里不關(guān)心
? 如果在調(diào)用信號處理函數(shù)時,除了當(dāng)前信號被自動屏蔽之外,還希望自動屏蔽另外一些信號,則用 struct sigaction
結(jié)構(gòu)體中的 sa_mask 字段說明這些需要額外屏蔽的信號(mask是信號集,可以用信號集操作函數(shù))