【Linux】Linux的信號

Linux的信號是一種系統或進程發出的通知,它的主要作用是用來激活信號接收者的一段程序,除此之外,也可以攜帶少量信息。從實現方式上來看,它是一種用軟件構建的中斷系統,只不過接收及處理中斷請求的不是處理器而是進程。與外設向處理器的中斷請求一樣,它是一種異步通信方式。

 

基本概念

計算機系統必須創建某種機制,要使發生事件的實體能在事件發生時將這個事件發送出去,同時還要使希望感知這個事件的實體能夠接收到這個事件,並做出下一步的行爲。也就是說,信號就是一個攜帶少量信息的通知。

系統中的很多事件都可以產生一個信號。Linux爲系統中可能產生的信號都進行了命名和編號,併爲接收信號的進程提供了默認服務,即每個信號都附帶一個信號默認服務程序。也就是說,進程在接收到某個信號時會執行另一個服務程序,如果進程沒有提供該服務程序,將會執行一個系統提供的默認服務程序。

系統常用的信號和默認服務有:

  • 當用戶按某些終端鍵時會產生信號。如:DELETE鍵;
  • 硬件異常產生的信號。例如:除數爲0、無效的存儲訪問等等;
  • 進程使用函數kill(2)可將信號發送給另一個進程或進程組。自然,有些限制:接收信號的進程和發送信號的進程的所有者必須相同,或發送信號進程所有者必須爲超級用戶;
  • 用戶可用命令kill(1)將信號發送給其它進程。常用於中止一個失控的後臺進程;
  • 當檢測到某種軟件條件已經發生,並將其通知有關進程時也產生信號。這裏指的不是硬件條件(如被0除),而是軟件條件,例如在網絡連接上傳來非規定波特率的數據。

Linux部分信號的名稱、編號、用途及默認服務見下表:

Linux部分信號名稱、編號、默認服務
信號名 編號 默認服務 用途
SIGHUP 1 進程中止 控制TTY斷開連接
SIGINT 2 進程中止 用戶在鍵盤上按下Ctrl+C
SIGQUIT 3 進程中止,內存轉儲 TTY鍵盤上按下了Ctrl+\
SIGILL 4 進程中止,內存轉儲 非法指令
SIGABRT 6 進程中止,內存轉儲 異常終止abort()
SIGFPE 8 進程中止,內存轉儲 浮點異常
SIGKILL 9 進程中止 中止信號
SIGSEGV 11 進程中止,內存轉儲 非法內存訪問
... ... ... ...

從接收信號進程的角度來看,信號的產生是隨機的,它實質上是向進程發出的中斷請求,即信號的到來意味着進程要中斷現行工作去執行信號服務程序。所以人們也將信號機制叫做“軟中斷”,只不過這不是向處理器而是向進程請求的中斷,如下圖所示:

具體來說,進程接收到某個信號之後,可以有如下三種反應:

  • 忽略信號。大多數信號都可使用這種方式進行處理,但有兩種信號卻不能被忽略:SIGKILL和SIGSTOP,因爲它們爲超級用戶提供了一種使進程中止或停止的可靠方法。另外,如果忽略某些由硬件異常產生的信號(例如非法存儲訪問或除以0),則進程的行爲是未定義的;
  • 捕捉信號。爲了做到這一點,進程必須爲該信號提供一個用戶信號處理程序;
  • 執行默認操作。對於大多數信號而言,系統默認的操作是中止該進程。

 

信號的發送

系統中,中斷、異常服務程序及進程都可以調用發送信號函數來發送信號。用來發送信號的主要函數有kill()、raise()、sigqueue()、alarm()、setitimer()及abort()等。

下面以函數kill()和alarm()爲例簡單介紹發送信號函數的作用。

函數kill()的原型如下:

int kill(pid_t pid, int signo);

其中,參數pid爲進程標識符ID。根據pid的值,函數kill()有如下四種不同的操作:

  • pid>0,將信號發送給進程ID爲pid的進程;
  • pid==0,將信號發送給ID與發送進程所在進程組ID相等的進程組;
  • pid<0,將信號發送給一個進程組,該進程組的ID等於pid絕對值;
  • pid==-1,未定義此等情況。

函數alarm()的原型如下:

unsigned int alarm(unsigned seconds);

其中,參數seconds的值是秒數,經過了指定的seconds秒後會產生信號SIGALRM。

函數alarm()的功能是爲一個“鬧鐘”設定一個定時時間,當時間值到達或超過時間設定值時,會產生SIGALRM信號。如果不忽略或不捕捉此信號,則其默認動作時中止該進程。

函數alarm()的返回值爲0或以前設置的鬧鐘時間的剩餘時間數(秒)。

 

信號的安裝

如果進程要處理某一信號,那麼就要在進程中安裝該信號。所謂信號的安裝,就是把信號編號及其對應的處理函數加入到進程控制塊中。

Linux用來安裝信號的函數有兩個:signal()和sigaction()。其中,signal()是在系統調用sys_signal()基礎上實現的庫函數,不支持信號傳遞信息;sigaction()是在系統調用sys_sigaction()實現的庫函數,它與sigqueue()系統調用配合,允許信號傳遞附加信息。

函數sigaction()的原型如下:

int sigaction(int signo, const struct sigaction * act, struct sigaction * oact);

其中,參數signo是信號的編號;參數指針act爲一個包含要安裝的信號處理程序的sigaction結構;參數指針oact,則返回該信號的原有包含了信號處理程序的sigaction結構。

當函數調用成功後,返回0,否則返回-1。

函數中所使用的sigaction結構如下:

struct sigaction {
	__sighandler_t	sa_handler;        //信號處理函數指針
	unsigned long	sa_flags;            //信號處理函數行爲方式標誌
	sigset_t	sa_mask;	/* 屏蔽位圖 */
};

這個結構可以看作是帶有管理信息的信號處理函數指針。

域sa_handler是一個函數指針,它指向的函數就是對應信號的服務程序,如果用戶設計了進程的信號處理函數,那麼就應該把這個處理函數與這個指針關聯起來,這樣當進程響應這個信號時就可以引用而執行信號服務程序了。

至於結構中的域sa_mask則相當於中斷屏蔽控制寄存器。它是一個叫做信號集sigset_t類型的變量。該類型定義如下:

typedef struct {
	unsigned long sig[_NSIG_WORDS];
} sigset_t;

它其實是一個位圖,每一位都對應一個信號。如果位圖中的某一位爲1,就表示在執行當前信號處理程序期間,位圖爲1的位所對應的信號都被“屏蔽”,以防止這些信號中斷當前信號服務程序的執行。

系統定義了下列五個對信號集進行操作的函數:

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);    

結構sigaction中的sa_flags是用來指定信號處理函數操作方式的參數,其可選值如下表:

信號處理的選擇項標誌(sa_flags)
sa_flags 說明
SA_NOCLDSTOP 若signo是SIGCHLD,則當一子進程停止時(作業控制),不產生此信號。當一子進程中止時,仍舊產生此信號。
SA_RESTART 由此信號中斷的系統調用自動再啓動
SA_ONSTACK 若用sigalstack(2)已說明了一替換棧,則此信號傳送給替換棧上的進程
SA_NOCLDWAIT 若signo是SIGCHLD,則當調用進程的子進程中止時,不創建僵死進程。若調用進程在後面調用wait,則阻塞到它所有子進程都中止,此時返回-1,errno設置爲ECHILD。
SA_NODEFER 若捕捉到此信號時,在執行其信號捕捉函數時,系統不自動阻塞此信號。注意,此種類型的操作對應於早期的不可靠信號。
SA_RESETHAND 對此信號的處理方式在此信號捕捉函數的入口處復置爲SIG_DFL。注意,此種類型的信號對應於早期的不可靠信號。
SA_SIGINFO 此選項對信號處理程序提供了附加信息

sigaction action結構如下圖所示:

在文件linux/kernel/signal.c中定義的系統調用sys_signal()的原型如下:

asmlinkage long sys_signal(int sig, __sighandler_t handler);

在文件linux/kernel/signal.c中定義的系統調用sys_rt_sigaction()的原型如下:

asmlinkage long sys_rt_sigaction(
                                 int sig,            //信號編號
				 const struct sigaction __user * act,        //sigaction結構的實例
				 struct sigaction __user * oldact,
				 size_t);

該函數的第一個參數爲信號的編號,可以去除SIGKILL及SIGSTOP外的任何一個特定有效的信號;第二個參數是指向結構sigaction的一個實例的指針;第三個參數oldact指向的對象用來保存原來對相應信號的處理,可指定oldact爲NULL。

 

進程的信號向量表

與處理器使用中斷向量表把中斷源和中斷服務程序關聯起來,並對其進行管理的方法類似,進程在自己的控制塊中也設置了一個信號向量表,前面所述的信號的安裝,其實就是要把sigaction結構安裝到這個向量表中。

進程控制塊中有很多與信號有關的域,其中最重要的就是指向信號向量表的指針sighand:

struct task_struct {
        ...
	struct sighand_struct *sighand;        //信號向量表指針
	struct sigpending pending;            //未決信號對列指針
        ...
};

在文件include/linux/sched.h中,信號向量表結構sighand_struct的定義如下:

struct sighand_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];            //信號數組
	spinlock_t		siglock;
	wait_queue_head_t	signalfd_wqh;
};

其中,信號數組action[_NSIG]的每一個元素都對應一個信號,其下標就是信號的編號。數組元素是一個k_sigaction結構:

struct k_sigaction {
	struct sigaction sa;
	__sigrestore_t ka_restorer;
};

也就是說,進程與信號向量表之間的關係如下圖所示:

 

進程響應信號的時機

既然對進程來說信號是一種中斷請求,那麼進程什麼時候可以響應並處理這種中斷呢?

因爲信號的處理程序都運行在進程空間,所以Linux規定,進程對信號的檢測與響應總是發生在系統調用或中斷服務的末尾處,也就是說當進程由系統空間返回到用戶進程空間之前。即內核在時鐘中斷timer_interrupt處理程序最後會跳轉去的ret_from_sys_call及其他系統調用的後面有對函數do_singal()的調用,如下圖所示:

 

信號的生命期及可靠性

從信號發送到相應的處理函數執行完畢,這是一個信號完整的生命期。因此,一個信號是否能完整地度過其生命期,取決於信號的可靠性。Linux的信號分爲不可靠信號和可靠信號。

信號的生命期

一個信號的生命期可分爲三個階段,這三個階段可由四個重要事件來刻畫:誕生、在進程中註冊、在進程中的註銷以及信號處理程序執行完畢。

當有事件發生時,如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等,即會誕生相應的信號。

在接收信號的進程端維護着一個信號隊列,該隊列是一個sigqueue結構的鏈表。結構sigqueue的定義如下:

struct sigqueue {
	struct sigqueue * next;            //指向下一個
	siginfo_t info;
};

其隊列頭是一個digpending結構:

struct sigpending {
	struct sigqueue * head, ** tail;
	sigset_t signal;
};

在這個結構中,除了用隊列頭尾兩個指針之外還有一個sigset_t類型的位圖signal。前面講過,系統中的每個信號都在位圖中佔有一個固定的位置,可以在某個信號的固定位置以1或0表示這個信號的狀態。所以結構sigpending中的這個位圖就用其中各個位的值來表示對應信號的到來情況:用1表示信號已經到來,但還未被進程處理;用0表示該位對應的信號未到,或已處理完畢。因此,這個位圖也叫做進程的未決信號位圖。

進程對信號管理的結構如下圖所示:

當信號誕生後,該信號及其相關信息會被加入進程的信號向量表(信號的安裝),同時還會將其加入到目標進程未決信號隊列,並記錄在該隊列中的未決信號集中,等待進程處理。這個行爲叫做信號向進程註冊。

每次在目標進程從系統空間返回到用戶空間的過程中,會檢測未決信號位圖,以發現等待處理的信號。如果存在未決信號並且該信號沒有被進程阻塞,則在未決信號鏈中卸掉該信號的結點並立即執行相應的信號處理函數,執行完畢後,信號生命期結束。

不可靠信號

Linux信號機制基本上是從Unix系統中繼承過來的。早期的Unix系統中的信號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,那些建立在早期機制上的信號就叫做“不可靠信號”。

不可靠信號的主要問題是:進程在處理一個信號後,信號會自動將該信號的響應設置爲默認服務。因此,進程在下次接收到這個信號後,信號執行的是默認服務,而在某些情況下,這個默認服務並不是進程所希望的服務。所以,常常需要在信號處理程序中,重新安裝該信號,但這種做法很不靠譜,極易造成信號的丟失。

因爲這種信號採用的是位圖來進行註冊,而沒有采用隊列,所以當進程中已有先到的同樣信號處在未決狀態時,後到的這個信號將會被丟棄,從而造成信號丟失。因此,這種信號叫做“不可靠信號”。

可靠信號

通過信號排隊的方法實現了“可靠信號”,即在多個信號集中到達時,內核用信號隊列記住這些信號,這樣就從根本上解決了信號丟失的問題。

即當一個信號發送給一個進程時,不管該信號是否已經在進程中註冊,都會被再註冊一次,並加入到未決信號隊列,這意味着同一個信號可以在同一個進程的未決信號隊列中佔有多個sigqueue結構。因此,信號不會丟失,是“可靠信號”。

也就是說,可靠信號與不可靠信號的區別在於:

可靠性是指信號是否會丟失,即該信號是否支持排隊; 如果支持排隊就是可靠的,不支持排隊就是不可靠的。

但由於已經很多的實際應用已經採用了原有的不可靠信號,所以爲了兼容,Linux仍然保留了原本的那些不可靠信號,同時新增了一些可靠信號。通常:

  • SIGHUP(1號) 至 SIGSYS(31號)之間的信號都是繼承自UNIX系統,是不可靠信號,也稱爲非實時信號;
  • SIGRTMIN(33號) 與 SIGRTMAX(64號)之間的信號,它們都是可靠信號,也稱爲實時信號。

 

總結

匿名管道、命名管道和共享內存都是以文件形式出現的通信方式,三者的共同特點都是以文件作爲通信雙方的中介,也都是屬於特殊文件。但匿名管道是虛文件,它並不存在於外部存儲器。又由於它是通過繼承方式來實現共享的,所以它只能用來在具有親緣關係的進程之間進行通信。另外,它是依靠單向通信來維護進程間的同步的。

消息隊列和管道基本上都是4次拷貝,而共享內存(mmap, shmget)只有兩次。

  • 4次:1,由用戶空間的buf中將數據拷貝到內核中。2,內核將數據拷貝到內存中。3,內存到內核。4,內核到用戶空間的buf;
  • 2次: 1,用戶空間到內存。 2,內存到用戶空間。

消息隊列和管道都是內核對象,所執行的操作也都是系統調用,而這些數據最終是要存儲在內存中執行的。因此不可避免的要經過4次數據的拷貝。但是共享內存不同,當執行mmap或者shmget時,會在內存中開闢空間,然後再將這塊空間映射到用戶進程的虛擬地址空間中,即返回值爲一個指向一個內存地址的指針。當用戶使用這個指針時,例如賦值操作,會引起一個從虛擬地址到物理地址的轉化,會將數據直接寫入對應的物理內存中,省去了拷貝到內核中的過程。當讀取數據時,也是類似的過程,因此總共有兩次數據拷貝。

命名管道就是一個文件,它與普通文件的最大區別有兩點:第一,它是嚴格按照FIFO方式工作的;第二,它只能單向操作。而共享內存則是一種只存在於內存的虛文件,由於它既可讀又可寫,因此它需要使用另外的同步措施。

消息隊列與匿名管道以及命名管道相比,具有更大的靈活性。因爲,它提供有格式的字節流,有利於減少開發人員的工作量。同樣,消息隊列可以在幾個進程間複用,而不管這幾個進程是否具有親緣關係,這一點與命名管道很類似。

消息類似於一種中斷,因此它有類似於中斷號的信號編號,也有類似於中斷服務程序的信號服務程序。它是一種異步通信方式,是事件與進程通信的手段。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章