1.信號的概念
生活當中哪些場景算信號呢?比如說你晚上調了個鬧鐘,然后第二天早上你聽到了鬧鐘響了你就知道該起床了,這種機制就叫做信號機制。在生活中我們的信號是非常非常多的,比如說有:紅綠燈,下課鈴聲,狼煙, 防控警報,電話鈴聲。那我們就以紅綠燈為例,你為什么會認識紅綠燈呢?因為在我們的小時候有人告訴過我們紅綠燈的特征,紅燈停,綠燈行這種理解。 如何認識紅綠燈呢?1.識別紅綠燈 2.知道對應的燈亮了,意味著什么,要做什么。信號沒有產生的時候,其實我們已經能夠知道怎么處理這個信號了,而信號產生了,我們不一定要立即處理它,而是我們在合適的時候處理。比如說我們平常點了一個外賣,然后你跟你的室友開了一把游戲,然后你知道外賣小哥會給你打電話,但是你并不清楚什么時候會給你打電話,但是可能你正打到一半游戲,電話就來了,但是你可能接到電話之后外賣小哥要你下去取外賣,但是你對于外賣遲一點取并不會有太大問題,所以你會選擇先把游戲打完再下去取。也就是說信號的到來,我們并不清楚具體什么時候,信號到來相對于我正在做的工作,是異步產生的。而如果要想做到這一點,我們就需要一種能力,將已經到來的信號進行暫時保存。而我們上述談到的我,你都是以進程為單位來看待的,進程在設計的時候一定是能夠識別系統中會出現的各種信號的這種能力的,而信號產生了,進程可以選擇在合適的時候進行處理,要想在合適的時候處理,那么進程又必須要有能夠將信號暫時保存的能力。
根據上面提到的我們可以總結出以下結論:
我們的信號為了能夠支持進程識別,它也是需要帶上編號的:
每一種信號都是通過編號來標識它的唯一性
什么叫做信號:信號是一種向目標進程發送通知消息的一種機制。?
而我們如果用如下代碼:
#include<iostream>
#include<unistd.h>int main()
{while(true){std::cout<<"running ..."<<std::endl;sleep(1);}return 0;
}
然后進行編譯產生可執行運行的時候我們一開始可以通過ctrl+c發送信號終止進程,發現該進程在運行的時候我們輸入指令是不起作用的,而如果通過./process &將該進程變成后臺進程,那么輸入的指令和該進程就不會互相影響了,但是這種后臺進行通過ctrl+c是不能夠殺死的,需要通過發送信號kill -9 進程的pid來殺死該進程。
進程在運行的時候,一種叫前臺進程(命令行操作的時候只能有一個),一種叫后臺進程(./xxx &)(可以有多個)。
而如何判斷一個進程是否是前臺進程可以通過判斷一個進程有沒有能力接受用戶輸入,為什么前臺進程只有一個?很容易理解,因為鍵盤只有一個。
通過jobs命令可以用來查看后臺進程
把后臺進程移到前臺可以用 fg number命令:
由于bash也是進程,所以bash會自動被操作系統移到后臺去,然后當我們用ctrl+c命令后將1這個后臺進程殺死之后,操作系統又會自動的將bash進程換到前臺,所以就出現了以上現象,那就說明我們平常用的ctrl+c一般情況下是用來終止前臺進程的。而操作系統會自動的將bash進程提到前臺來。
而如果我們將后臺2進程提到前臺后通過命令ctrl+z讓它暫停之后會出現很多的問題,因為如果前臺被暫停了,不能通過鍵盤輸入指令,系統就會掛掉,所以說前臺進程不能被暫停(ctrl+z)如果被暫停,該前臺進程必須被立即放到后臺,所以才有我們剛剛看到的前臺進程被shell進程頂替了,才導致系統沒有掛掉。如果我們想繼續讓該被暫停的進程繼續跑起來就需要通過指令bg number,也就是:
我們發現它從stopped變成了running了,說明又跑起來了。
2.信號的產生
下面我們通過用代碼證明一下ctrl+c是通過對進程發送了2號信號,然后調用了相對應的讓進程終止的方法讓該進程終止的。
我們先來認識一個接口:
signal接口通過傳一個信號編號也就是我們前面kill -l打印出來的對應的信號的編號,然后后面是一個函數指針,也就是通過修改信號編號做出對應的動作(函數),我們通過編寫以下代碼,也就是將2號信號做出的響應改成我們的handler函數里面要執行的操作:也就是會打印出一段文字,然后還會直接退出,進程退出碼是1
#include<iostream>
#include<unistd.h>
#include<signal.h>void handler(int signo)
{std::cout<<"獲得一個2號信號"<<std::endl;exit(1);
}int main()
{signal(2,handler);while(true){std::cout<<"running ..."<<std::endl;sleep(1);}return 0;
}
運行結果:
信號的產生方式
2.1終端按鍵產生信號
可以通過鍵盤進行信號的產生,ctrl+c向前臺發送2號信號
我們發現沒有0號信號,其實0號信號就是進程沒有收到信號,正常運行的,至于結果對不對可以看進程的退出碼來進行判斷。這里也沒有32號信號,33號信號。操作系統為了能處理信號,給每個進程都維護了一張函數指針數組這樣的表,這張表數組對應的下標就是我們信號對應的編號。也就是說:每一個進程都有一張自己的函數指針數組,數組的下標就和信號編號強相關。
什么是發送信號呢?
對于普通信號來講,進程收到信號之后,進程要表示自己是否收到了某種信號?所以對于進程來講是要將這些信號進行管理起來的,如何管理呢?先描述,再組織。一定是需要某種數據結構來進行描述,首先需要考慮到的點是 :是否和某種這兩個次如何用數據結構來解決。當聽到是否的時候我們可以選擇用位圖來表示,讓比特位的位置決定信號編號,比特位的內容決定是否收到信號。
那么在struct task_struct里面就可以維護一個信號位圖
struct task_struct?
{
? ? ? ? //信號位圖
? ? ? ? 0000 0010
? ? ? ? uint32_t sigbitmap;
}
操作系統向目標進程發送信號,這句話如何理解呢?其實準確的說應該是操作系統向目標進程寫信號。
而無論信號有多少種產生方式,永遠只能讓操作系統向目標進程進行發送!因為操作系統是進程的管理者。
每個進程對于信號都有兩個東西:
1.函數指針數組
2.信號位圖
通過函數指針數組(解決了我們前面談到的提前知道如何處理它的問題)和信號位圖(識別信號)我們就解決了前面談到的兩個問題。
下面我們再用代碼來驗證以下signal這個接口將能夠修改函數指針數組的內容,使得信號編號對應的方法被用戶自定義實現。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>
void handler(int signo)
{std::cout<<"獲得一個"<<signo<<" 號信號"<<std::endl;//exit(1);
}int main()
{signal(2,handler);signal(19,handler);signal(20,handler);signal(3,handler);while(true){std::cout<<"running ...,pid: "<<getpid()<<std::endl;sleep(1);}return 0;
}
我們通過ctrl+c發送2號信號不退出,還有ctrl+z發送20號信號也讓進程不退出,還有ctrl+\發送3號信號不退出。
運行結果:
我們發現運行之后通過控制中斷發送信號并沒有讓進程退出,說明確實signal接口是通過修改對應信號編號的處理過程來自定義處理的。但是并不是所有的信號編號都可以被自定義函數方法的,只有少量信號是不能被修改的,比如我們常用來殺死進程的9號信號。下面我們可以修改代碼為:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>
void handler(int signo)
{std::cout<<"獲得一個"<<signo<<" 號信號"<<std::endl;//exit(1);
}int main()
{signal(9,handler);while(true){std::cout<<"running ...,pid: "<<getpid()<<std::endl;sleep(1);}return 0;
}
運行結果:
發現對9號信號不可被自定義捕捉。
另外如果我們想要詳細的看我們信號編號對應的默認初始化動作可以通過執行命令:
man 7 signal
查看對信號的詳細處理,該手冊會存在一張表:
表中的signal就是信號編號,Value就是值是多少,Action就是對應的默認動作,Comment就是對默認動作的描述.
2.2系統調用產生信號
下面我們來通過手冊查看這個系統調用的接口:
process.cc
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>static void Usage(const std::string & proc)
{std::cout<<"\nUsage: "<<proc<<" signumber process\n"<<std::endl;
}void handler(int signo)
{std::cout<<"獲得一個"<<signo<<" 號信號"<<std::endl;//exit(1);
}int main(int argc,char* argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}int signumber = std::stoi(argv[1]);int processid = std::stoi(argv[2]);kill(processid,signumber);// signal(9,handler);// while(true)// {// std::cout<<"running ...,pid: "<<getpid()<<std::endl;// sleep(1);// }// return 0;
}
test.c
#include<stdio.h>
#include<unistd.h>int main()
{while(1){printf("I am a process , pid: %d\n",getpid());sleep(1);}return 0;
}
運行結果:
其實還有兩個系統調用接口:
一個是raise,表示給自己這個進程發送一個信號。
另一個是abort,這是個函數,表示讓該進程終止。
2.3硬件異常產生信號
?硬件異常被硬件以某種方式被硬件檢測到并通知內核,然后內核向當前進程發送適當的信號。例如當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋 為SIGFPE信號發送給進程,也就是對應的8號信號。
用以下代碼進行測試:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>int main()
{int a = 10;a/=0;return 0;
}
運行結果:
而我們如果對8號信號進行自定義捕捉的話:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>void handler(int signo)
{std::cout<<"獲得一個"<<signo<<" 號信號"<<std::endl;//exit(1);
}int main()
{signal(8,handler);int a = 10;a/=0;return 0;
}
運行結果:
再比如當前進程訪問了非法內存地址,,MMU會產生異常,內核將這個異常解釋為SIGSEGV信號發送給進程。
將代碼修改成:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>int main()
{int * p = nullptr;*p = 100;return 0;
}
運行結果:
由于是11號信號,所以修改代碼:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>void handler(int signo)
{std::cout<<"獲得一個"<<signo<<" 號信號"<<std::endl;sleep(1);//exit(1);
}int main()
{signal(11,handler);int * p = nullptr;*p = 100;return 0;
}
運行結果:
進程不退出,操作系統檢測到異常依然發送11號信號,操作系統認為自己解決了,而發送的11號信號并沒有讓該進程退出,只是打印一條語句,所以CPU也跟著做了繼續執行下去,就這樣陷入了循環。
2.4由軟件條件產生信號
SIGPIPE是一種由軟件條件產生的信號,在“管道”中已經介紹過了。這里主要介紹alarm函數 和SIGALRM信號。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
調用alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之后給當前進程發SIGALRM信號, 該信號的默認處理動作是終止當前進程。
下面我們對該接口進行測試:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdlib>int cnt = 0;
int n = 0;void handler(int signo)
{n = alarm(0);std::cout<<"result: "<<n<<std::endl;//std::cout<<"get a signo: "<<signo<<"alarm : "<<cnt++<<std::endl;exit(0);
}int main(int argc,char* argv[])
{signal(14,handler);std::cout<<"pid: "<<getpid()<<std::endl;alarm(30);while(true){cnt++;}return 0;
}
運行結果:
我們可以看到,如果上次的鬧鐘還沒有結束,那么再次調用alarm的話,返回值是上次鬧鐘剩余的時間。如果結束了,則返回0。
鬧鐘就是一種軟件條件。
操作系統中的時間:
1.所有用戶的行為,都是以進程的形式在OS種表現的
2.操作系統只要把進程調度好,就能完成所有的用戶任務。
3.CMOS,中期性的,高頻率的向CPU發送時鐘中斷。
樸素的對操作系統進行理解:
操作系統本質,就是一個死循環
操作系統啟動首先做對各種中斷的陷阱的初始化工作。
while(1)
{
? ? ? ? pause();
}
操作系統的執行是基于硬件中斷的。
core dump
這里面的收到信號后的Action里面的Ign,Cont,Stop我們都并不需要太關心,因為這些都不是出現什么太大的問題而發出的信號,而像Term這種要么是確確實實出現問題了,要么就是被手動殺掉了,都還不是特別嚴重,而像Core這種就是相對于問題比較嚴重的情況下做出的動作了,比如說非法內存訪問,浮點數錯誤這是需要用戶取溯源的比較嚴重的問題。然后我們這里談概念的話了解的不是特別清楚,所以我們下面用一小段測試的代碼來進行測試。
test_signal.cc
#include<iostream>
#include<unistd.h>
#include<signal.h>int main()
{int a = 10;a/=0;return 0;
}
運行結果:
這就是我們剛剛所說的Core
同樣會讓一個進程終止,但是會做一個dump core叫做核心轉儲。意思是會在進程運行的當前目錄下形成一個core.pid的一個文件,它在內存中運行時崩潰了,它會把你這個進程運行時崩潰時核心的上下文數據全部轉儲到磁盤上形成一個臨時文件(文件名為core,運行的進程的pid為文件后綴形成的臨時文件)。
我們通過一條命令?ulimit -a?來查看core file size(也就是core文件的大小)
我們發現它的大小是0,也就是說core file默認是被關閉了,無法形成,但是我們可以通過指令來將他打開,然后我們再看形成的core文件。
如果要修改core file size 那么我們就用命令? ulimit -c +需要設置的文件大小。
這樣就把core file size重新設置了大小,也相當于把它打開了。這個設置只是內存級的設置,如果我們把xshell關閉了,那么該設置又會恢復成0,同時這個設置也只對當前的shell窗口設置的,而不是對當前的用戶設置的
我們發現形成了一個core.579的臨時文件,為了方便看到這個文件是通過core.pid的形式形成的我們把代碼改成:
#include<iostream> #include<unistd.h> #include<signal.h>int main() {std::cout<<"pid: "<<getpid()<<std::endl;int a = 10;a/=0;return 0; }
運行結果:
形成了一個core.877的臨時文件,我們發現剛好是這個進程的pid.這個核心轉儲的意義就在于程序員可以通過core.877這樣的文件來查看代碼從哪個位置出錯了,哪里的原因讓操作系統發送信號給進程的。
3.信號的保存
3.1信號其他相關概念
- 實際執行信號的處理動作稱為信號遞達(Delivery)
- 信號從產生到遞達之間的狀態,稱為信號未決(Pending)。
- 進程可以選擇阻塞 (Block )某個信號。
- 被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作.
- 注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作。
信號的處理動作稱為信號遞達(Delivery),信號的遞達有三種方式:信號的忽略,信號的默認,信號的自定義捕捉。
信號從產生到遞達之間的狀態,稱為信號未決(Pending),換句話說就是信號在信號位圖中。
進程可以選擇阻塞 (Block )某個信號。未決之后,暫時不遞達,知道解除對信號的阻塞。
3.2信號在內核中的表示
信號在內核中的表示示意圖
- 每個信號都有兩個標志位分別表示阻塞(block)和未決(pending),還有一個函數指針表示處理動作。信號產生時,內核在進程控制塊中設置該信號的未決標志,直到信號遞達才清除該標志。在上圖的例子中,SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
- SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因為進程仍有機會改變處理動作之后再解除阻塞。SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。
- 如果在進程解除對某信號的阻塞之前這種信號產生過多次,將如何處理?POSIX.1允許系統遞送該信號一次或多次。Linux是這樣實現的:常規信號在遞達之前產生多次只計一次,而實時信號在遞達之前產生多次可以依次放在一個隊列里。這里不討論實時信號。
3.3 sigset_t
從上圖來看,每個信號只有一個bit的未決標志,非0即1,不記錄該信號產生了多少次,阻塞標志也是這樣表示的。因此,未決和阻塞標志可以用相同的數據類型sigset_t來存儲,sigset_t稱為信號集,這個類型可以表示每個信號的“有效”或“無效”狀態,在阻塞信號集中“有效”和“無效”的含義是該信號是否被阻塞,而在未決信號集中“有效”和“無效”的含義是該信號是否處于未決狀態。下一節將詳細介紹信號集的各種操作。 阻塞信號集也叫做當前進程的信號屏蔽字(Signal Mask),這里的“屏蔽”應該理解為阻塞而不是忽略。
3.4. 信號集操作函數
sigset_t類型對于每種信號用一個bit表示“有效”或“無效”狀態,至于這個類型內部如何存儲這些bit則依賴于系統實現,從使用者的角度是不必關心的,使用者只能調用以下函數來操作sigset_ t變量,而不應該對它的內部數據做任何解釋,比如用printf直接打印sigset_t變量是沒有意義的
#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);
- 函數sigemptyset初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含 任何有效信號。
- 函數sigfillset初始化set所指向的信號集,使其中所有信號的對應bit置位,表示 該信號集的有效信號包括系統支持的所有信號。
- 注意,在使用sigset_ t類型的變量之前,一定要調 用sigemptyset或sigfillset做初始化,使信號集處于確定的狀態。初始化sigset_t變量之后就可以在調用sigaddset和sigdelset在該信號集中添加或刪除某種有效信號。
這四個函數都是成功返回0,出錯返回-1。sigismember是一個布爾函數,用于判斷一個信號集的有效信號中是否包含某種 信號,若包含則返回1,不包含則返回0,出錯返回-1。
sigprocmask
調用函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功則為0,若出錯則為-1
如果oset是非空指針,則讀取進程的當前信號屏蔽字通過oset參數傳出。如果set是非空指針,則 更改進程的信號屏蔽字,參數how指示如何更改。如果oset和set都是非空指針,則先將原來的信號 屏蔽字備份到oset里,然后根據set和how參數更改信號屏蔽字。假設當前的信號屏蔽字為mask,下表說明了how參數的可選值。
如果調用sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。
下面我們來看一段測試代碼:
#include<iostream>
#include<unistd.h>
#include<signal.h>void handler(int signo)
{std::cout<<"handler"<<signo<<std::endl;
}int main()
{std::cout<<"getpid: "<<getpid()<<std::endl;signal(2,handler);sigset_t block,oblock;sigemptyset(&block);sigemptyset(&oblock);for(int signo = 1;signo<=31;signo++) sigaddset(&block,signo);sigprocmask(SIG_SETMASK,&block,&oblock);while(true){std::cout<<"我已經屏蔽了所有信號,來打我啊!"<<std::endl;sleep(1);}return 0;
}
運行結果:
sigpending
#include <signal.h>
sigpending
讀取當前進程的未決信號集,通過set參數傳出。調用成功則返回0,出錯則返回-1。 下面用剛學的幾個函數做個實驗。程序如下:
程序運行時,每秒鐘把各信號的未決狀態打印一遍,由于我們阻塞了SIGINT信號,按Ctrl-C將會 使SIGINT信號處于未決狀態,按Ctrl-\仍然可以終止程序,因為SIGQUIT信號沒有阻塞。
#include<iostream>
#include<unistd.h>
#include<signal.h>void PrintPending(const sigset_t &pending)
{for(int signo = 31;signo>0;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}void handler(int signo)
{std::cout<<"handler"<<signo<<std::endl;
}int main()
{signal(2,handler);std::cout<<"getpid: "<<getpid()<<std::endl;//1.屏蔽2號信號sigset_t set,oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set,2);sigprocmask(SIG_BLOCK,&set,&oset);//2.讓進程不斷獲取當前進程的pendingint cnt = 0;sigset_t pending;while(true){sigpending(&pending);PrintPending(pending);sleep(1);cnt++;if(cnt==5){std::cout<<"解除對2號信號的屏蔽,2號信號準備遞達"<<std::endl;sigprocmask(SIG_SETMASK,&oset,nullptr);}}return 0;
}
運行結果:
這就叫做屏蔽信號。
4.信號的處理
信號在合適的時候被處理——那么什么時候被處理呢? 進程從內核態返回到用戶態的時候,進行信號的檢測和信號的處理。
用戶態是一種受控的狀態,能夠訪問的資源是有限的。
內核態是一種操作系統的工作狀態,能夠訪問大部分系統資源。
系統調用背后就包含了身份的變化。
內核如何實現信號的捕捉
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這稱為捕捉信號。由于信號處理函數的代碼是在用戶空間的,處理過程比較復雜,舉例如下: 用戶程序注冊了SIGQUIT信號的處理函數sighandler。 當前正在執行main函數,這時發生中斷或異常切換到內核態。 在中斷處理完畢后要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。 內核決定返回用戶態后不是恢復main函數的上下文繼續執行,而是執行sighandler函 數,sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關系,是 兩個獨立的控制流程。 sighandler函數返回后自動執行特殊的系統調用sigreturn再次進入內核態。 如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
我們下面用一段代碼來看看內核如何實現信號的捕捉,首先我們先認識一個sigaction的接口:
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
man 手冊:
- sigaction函數可以讀取和修改與指定信號相關聯的處理動作。調用成功則返回0,出錯則返回- 1。signo是指定信號的編號。若act指針非空,則根據act修改該信號的處理動作。若oact指針非 空,則通過oact傳出該信號原來的處理動作。act和oact指向sigaction結構體:
- 將sa_handler賦值為常數SIG_IGN傳給sigaction表示忽略信號,賦值為常數SIG_DFL表示執行系統默認動作,賦值為一個函數指針表示用自定義函數捕捉信號,或者說向內核注冊了一個信號處理函 數,該函數返回值為void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。顯然,這也是一個回調函數,不是被main函數調用,而是被系統所調用。
#include<iostream>
#include<unistd.h>
#include<signal.h>void Print(const sigset_t & pending);void handler(int signo)
{std::cout<<"get a sig: "<<signo<<std::endl;while(true){sigset_t pending;sigpending(&pending);Print(pending);sleep(1);}
}void Print(const sigset_t & pending)
{for(int signo = 31;signo>0;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}int main()
{std::cout<<"getpid: "<<getpid()<<std::endl;struct sigaction act,oact;act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask,3);sigaction(2,&act,&oact);while(true) sleep(1);return 0;
}
這段代碼是在信號處理時屏蔽的信號除了屏蔽2號信號還可以屏蔽3號信號。
運行結果:
#include<iostream>
#include<unistd.h>
#include<signal.h>void Print(const sigset_t & pending);void handler(int signo)
{std::cout<<"get a sig: "<<signo<<std::endl;sleep(1);// while(true)// {// sigset_t pending;// sigpending(&pending);// Print(pending);// sleep(1);// }
}void Print(const sigset_t & pending)
{for(int signo = 31;signo>0;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}int main()
{signal(2,handler);signal(3,handler);signal(4,handler);signal(5,handler);sigset_t mask,omask;sigemptyset(&mask);sigemptyset(&omask);sigaddset(&mask,2);sigaddset(&mask,3);sigaddset(&mask,4);sigaddset(&mask,5);sigprocmask(SIG_SETMASK,&mask,&omask);// std::cout<<"getpid: "<<getpid()<<std::endl;// struct sigaction act,oact;// act.sa_handler = handler;// sigemptyset(&act.sa_mask);// sigaddset(&act.sa_mask,3);// sigaction(2,&act,&oact);int cnt = 20;std::cout<<"getpid: "<<getpid()<<std::endl;while(true){sigset_t pending;sigpending(&pending);Print(pending);cnt--;sleep(1);if(cnt==0){sigprocmask(SIG_SETMASK,&omask,nullptr);std::cout<<"cancel 2, 3, 4, 5 block"<<std::endl;}}return 0;
}
我們先把2,3,4,5這一批信號都屏蔽20s,然后在這20s內發送2,3,4,5號信號來觀察系統如何處理這些信號
運行結果:
我們發現會將多個信號一起遞達處理。
5.信號的其他補充問題
可重入函數
- main函數調用insert函數向一個鏈表head中插入節點node1,插入操作分為兩步,剛做完第一步的 時候,因為硬件中斷使進程切換到內核,再次回用戶態之前檢查到有信號待處理,于是切換 到sighandler函數,sighandler也調用insert函數向同一個鏈表head中插入節點node2,插入操作的 兩步都做完之后從sighandler返回內核態,再次回到用戶態就從main函數調用的insert函數中繼續 往下執行,先前做第一步之后被打斷,現在繼續做完第二步。結果是,main函數和sighandler先后 向鏈表中插入兩個節點,而最后只有一個節點真正插入鏈表中了。
- 像上例這樣,insert函數被不同的控制流程調用,有可能在第一次調用還沒返回時就再次進入該函數,這稱為重入,insert函數訪問一個全局鏈表,有可能因為重入而造成錯亂,像這樣的函數稱為 不可重入函數,反之,如果一個函數只訪問自己的局部變量或參數,則稱為可重入(Reentrant) 函數。想一下,為什么兩個不同的控制流程調用同一個函數,訪問它的同一個局部變量或參數就不會造成錯亂?
如果一個函數符合以下條件之一則是不可重入的:
- 調用了malloc或free,因為malloc也是用全局鏈表來管理堆的。
- 調用了標準I/O庫函數。標準I/O庫的很多實現都以不可重入的方式使用全局數據結構。
volatile 作用:保持內存的可見性,告知編譯器,被該關鍵字修飾的變量,不允許被優化,對該變量的任何操作,都必須在真實的內存中進行操作