信號
什麼是信號?
在生活中,我們可以看到交通信號,火警信號,急救信號,這些都是信號,通知我們做出相應的反映,在操作系統中,我們也有信號這一個機制,來確保進程的合理運行,當一個程序運行起來的時候,我們在鍵盤上按一個ctrl+c,進程就會停下來,這實際上就是給進程發了一個信號,cpu從用戶態切入內核態,來處理這個硬件中斷,把它理解成一個SIGINT的信號記錄在當前pcb下,然後當某個時候需要從內核切回用戶態的時候,會處理當前pcb中的信號,發現有一個SIGINT的信號存在,就會終止當前進程。linux下的各種信號
我們可以用kill -l 命令來查看linux中的信號列表
其中,1-31號爲普通信號,34到64是實時信號。我們這裏只討論普通信號。- ctrl+c (2號SIGINT信號),ctrl +\(3號SIGQUIT信號),ctrl+z(20SIGTSTP信號),kell-9(9號 SIGKILL信號),鬧鐘alarm超時(14號SIGALRM信號),除0錯誤(8號SIGFPE信號),訪問非法內存(11號SIGSEGV信號),向讀端關閉的管道中寫(13號SIGPIPE信號)
信號的處理方式
- 執行默認動作,一般以上都是終止該進程。
- 忽略此信號
- 用戶自定義捕捉該信號,執行捕捉函數。
Core Dump
什麼是core dump,當一個進程異常終止的時候,可以把用戶進程的內存信息全部保存到磁盤上,文件名通常是core,方便我們利用這個文件來調試程序,找出bug。一般默認是不允許產生core dump文件的,因爲我們的程序可能包含一寫重要信息,但是我們在實際的開發當中,還是可以運行操作系統產生core dump文件的,爲了方便我們進行調試。
我們可以用ulimit -a來查看系統的core dump信息,我們可以看出 現在已經禁止生成core dump文件了,文件大小爲0,我們可以用ulimit -c 1024 ,一般設置爲4k,來重置。我們可以用gdb來對這個文件進行調試。
系統接口
kill和abort
- 我們可以kill命令來向指定進程發信號,當然我們也可以利用系統調用函數來發送命令。
int kill(pid_t pid,int signal)向指定進程發信號 int raise(int signal)向自己發信號
alarm
unsigned int alarm(unsigned int seconds)
調用這個函數的作用是,經過seconds秒之後,操作系統向進程發出一個SIGALRM信號,這個信號的默認終止動作是終止進程。這個函數返回0或者以前設定鬧鐘還剩下的秒數,如果second爲0,表示取消掉鬧鐘。
信號的阻塞
- 執行一個信號的處理動作叫做信號的遞達。
- 一個信號從產生到遞達之間的過程叫做未決。
- 當然進程可以選擇阻塞某個信號的遞達。
- 被阻塞的信號一直處於未決狀態,知道進程解除對這個信號的阻塞,纔可以被遞達
- 注意信號的阻塞和忽略是兩個不同的概念,忽略是信號遞達之後執行的另一種操作。
如何理解操作系統向進程發信號
每一個進程都有一個PCB來維護它,而PCB中又有信號的位圖,而操作系統向進程發信號,實際上就是操作系統在修改目標進程的PCB中的信號位圖中的對應比特位,只要將0該爲1,就表示該進程接收到了這個比特位所對應的信號。當然,
- pending表表示信號是否處於未決狀態,block表表示該信號是否阻塞,hander表表示進程的處理動作。我們可以把hander表形象的理解成一個函數指針數組。裏面存放着每一個信號的處理函數。
- 根據上表可以看出,SIGHUP信號沒有處於未決狀態,同時也沒有被阻塞,它執行的處理動作是默認處理動作。SIGINT信號處於未決狀態,這個信號被阻塞着,它執行的操作是忽略,SIGQUIT沒有位於未決狀態,但是它被阻塞着,它執行的操作是用戶自定義的sighanler方法。
信號集操作函數
- sigset_t 系統通過sigset_t這個類型來標記信號的未決和阻塞狀態,我們稱之爲信號集,我們把阻塞信號集稱之爲信號屏蔽字。這個變量只能操縱以下函數來調用,任何嘗試打印它或其他操作都是無意義的。
- 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); 判斷信號是否在這個信號集中
sigprocmask
int sigprocmask(int how,const sigset_t* sig,sigset_t *osig);
how參數
sig如果非空,則表示根據how參數來更改當前進程的信號屏蔽字,osig非空,表示的是以前舊的信號屏蔽字,通過osig帶出。
sigpending
int sigpending (sigsset_t* set)
將當前進程的未決信號集通過set帶出來
下面我們就來寫一個小程序。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void sigprint(sigset_t * old)
{
int i=0;
for(;i<32;i++)
{
if(sigismember(old,i))
{
printf("1");
fflush(stdout);
}
else
{
printf("0");
fflush(stdout);
}
}
printf("\n");
}
int main()
{
sigset_t new, old;
sigemptyset(&new);
sigaddset(&new,SIGINT);
sigprocmask(SIG_BLOCK,&new,NULL);
while(1)
{
sigpending(&old);
sigprint(&old);
sleep(1);
}
return 0;
}
這個小程序就是把我們的2號信號加入當前進程的信號屏蔽字,然後打印進程的信號未決信號集。當我們按ctrl+c的時候無法結束進程,原因是我們把2號信號給阻塞了,但是我們依舊可以用ctrl+\來結束這個進程。
下面我們站在cpu的角度來理解一個信號從產生到遞達的過程。
- 當執行控制流的因爲某條指令或者硬件中斷或者異常從用戶態進入內核
- 當執行完異常的處理準備從內核返回用戶態時,會檢查當前進程的PCB的信號位圖,查看是否有可以遞達的信號,如果有的話就會對這個信號進行處理,如果處理動作是忽略,那麼就返回到用戶態接着往下執行,而如果是終止該進程,那麼進程就停止。
- 如果信號的處理動作是用戶定義的處理動作,則返回到用戶態系統定義的處理函數,此時處理函數和main函數使用不同的棧空間,不存在調用被調用的關係,是兩個不同的執行流。處理完該信號,再次進入內核態,將PCB中的信號位圖中已遞達的信號的未決狀態修改,然後再次返回到上次被中斷的地方接着向下執行。
sigaction
int siaction(int signo,const struct sigaction *act,struct sigaction *oact);
這個函數主要用來進行信號的捕捉,signo表示要捕捉的信號編號,act表如果非空表示act修改該信號的捕捉動作,而oact表示的是信號原來的處理動作。
在這裏,我們通常把sa_handler賦值爲一個指向我們自定義處理函數的函數指針,sa_flags通常設置爲0;如果我們想屏蔽某些信號的話,就用sa_mask字段來說明。
pause
int pause(void)這個函數的作用是掛起當前進程,直到有信號遞達,如果信號的處理動作是終止,則進程中止,pause無法返回,如果是忽略,則繼續掛起等待,只有當信號處理動作是我們自定義的函數的時候,纔回去執行捕捉函數,pause返回-1,errno設置爲EINTER,也就是說只有錯誤的時候纔會返回。
下面我們來用alarm和pause來進行實現一個功能和sleep一樣的程序。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void sig_alarm(int signo)
5 {
6 //
7 }
8 int mysleep(int seconds)
9 {
10
11 int unslept=0;
12 struct sigaction new,old;
13 new.sa_handler=sig_alarm;
14 new.sa_flags=0;
15 sigemptyset(&new.sa_mask);
16 sigaction(SIGALRM,&new,&old);
17 alarm(seconds);
18 pause();
19 unslept=alarm(0);
20 sigaction(SIGALRM,&old,NULL);
21 return unslept;
22
23 }
24 int main()
25 {
26 while(1)
27 {
28 mysleep(5);
29 printf("5 seconds passed\n");
30 }
31 return 0;
32 }
這個程序我們設置了一個鬧鐘,每隔5秒向進程發送一個alarm信號,然後又掛起進程,直到信號遞達纔會去做我們的SIGALRM捕捉函數,雖然捕捉函數什麼也不幹。所以每隔五秒鐘,程序打印一句話。
可重入函數
我們把函數在不同的控制流程下,重複的進入函數裏面,而且修改一些全局屬性的時候出現錯誤的函數就叫做不可重入函數。
相反的,如果一個函數只修改自己的局部變量或者參數,就算重入也不會,造成錯誤,這樣的函數就叫做可重入函數。
符合什麼樣的特性的函數就是不可重入的
- 調用了malloc或者free,因爲malloc也是利用全局鏈表來管理堆的。
- 調用了標準I/O庫函數,因爲標準I/O庫中很多操作都是以不可重入的方式使用全局數據結構。
volatile
volatile關鍵字的作用是保證內存的可見性,即每次都到內存中去取數據。爲了防止系統在編譯時候的優化措施。
競態條件和sigsuspend函數
現在我們重新思考一下我們上文提到的mysleep程序,設想有沒有這樣一種場景。
- 註冊SIGALRM的信號捕捉函數
- 調用alarm設定鬧鐘
- 此時內核中有很多優先級比當前進程高的進程在運行,內核一直在調度它們,而且每一個都需要很長時間。把這個進程放在一邊。
- 時間到了,內核向這個進程發送SIGALRM信號,使其處於未決狀態。
- 內核把這些優先級高的進程調度調度完成了,調用mysleep進程,執行它的信號捕捉函數。
- 返回主控制流程,alarm返回,接着執行pause()掛起等待
- 但是信號的捕捉函數都處理完了,還等待什麼呢?
造成這種原因就是因爲系統運行的時許不像我們寫程序所設想的那樣,雖然alarm的下一句就是pause但是,無法保證pause會在alarm之後的設定秒數內執行,所以異步時間在任何時候都有可能發生,因此我們在寫程序的時候不考慮周全,就會造成因爲時序問題而導致的錯誤,這就叫做競態條件。
解決方法 - 如果我們能在做到先屏蔽掉SIGALRM信號,此時就算收到該信號也不會去執行捕捉函數,然後再在接觸屏蔽的同時掛起當前進程,就能很好的解決這個問題。
int sigsuspend(const sigset_t* sigset)
和pause一樣,sigsuspend沒有成功返回值,只有執行了一個信號處理函數之後sigsuspend才返回,返回值爲-1,errno設置爲EINTR。 調用sigsuspend時,進程的 信號屏蔽字由sigmask參數指定,可以通過指定sigmask來臨時解除對某 個信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復爲原來的值, 如果原來對該信號是屏蔽的,從sigsuspend返回後仍然是屏蔽的。
SIGCHLD
實際上,在子進程退出的時候,會給父進程發送一個SIGCHLD的信號,通知父進程子進程退出,已經子進程的退出信息,該信號的默認處理動作是忽略,我們也可以自定義SIGCHLD的處理函數,這樣父進程在創建子進程之後就可以安心的幹自己的事情了,不必關心子進程的信息,當子進程退出的時候,父進程在SIGCHLD處理函數中就會對子進程進行回收。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<stdlib.h>
4
5 void handler(int sig)
6 {
7 pid_t id;
8 while((id==waitpid(-1,NULL,WNOHANG))>0)
9 {
10 printf("wait child success %d\n",id);
11
12 }
13 printf("child is quie, id is:%d\n",getpid());
14 }
15 int main()
16 {
17 signal(SIGCHLD,handler);
18 pid_t pid;
19 pid=fork();
20 if(pid<0)
21 {
22 perror("fork");
23 return -1;
24 }
25 else if(pid==0)
26 {
27 //child
28 printf("child ID is:%d\n",getpid());
29 sleep(3);
30 exit(2);
31 }
32 while(1)
33 {
34 printf("father is doing something\n");
35 sleep(1);
36 }
37 return 0;
38 }