APUE學習:信號

信號機制是一個經典的進程異步機制。
Linux信號機制的基本流程:

  1. 用戶程序爲一個信號註冊一個信號處理函數,例如SIGQUIT註冊了一個sig_hander函數
  2. 因爲某些原因,進程從用戶態切換到內核態
  3. 從內核態要返回到用戶態時,內核會去檢測有沒有給該進程傳遞一個SIGQUIT信號,如果有會在用戶態下面去執行對應的信號處理函數sig_hander
  4. sig_hander執行完畢之後會自動執行特殊的系統調用sigreturn再次進入內核態。
  5. 如果沒有新的信號傳遞過來,這次在返回用戶態就恢復到上次中斷的地方之後繼續執行。

信號傳遞的過程
這裏寫圖片描述

一些概念

當產生一個信號之後,注意信號是跟進程相關聯的。所以信號產生之後,內核需要把這個信號交給進程去處理。

進程處理信號基本上有三種方式:
1.忽略該信號。但是注意有兩種信號不可以忽略。例如SIGKILL與SIGSTOP。
2.捕捉該信號。捕捉該信號之後,激活進程準備好的信號處理函數,把這個信號交給這個信號處理函數去處理。
3.捕捉該信號但是讓默認的信號處理函數去處理。

注意點:
1)另外需要注意的是,進程有一個信號阻塞隊列。阻塞不同於忽略。阻塞表示的是我接受該信號,但是目前卻不想處理該信號。
對於處於阻塞狀態的信號一般有兩種方式去處理。
1)忽略這個處於阻塞狀態的信號
2)解除阻塞狀態後,調用處理該信號的信號處理函數去處理。

2)未決信號
與阻塞不同,阻塞指的是阻止這個信號被處理,即不讓改信號的信號處理函數去處理它。未決指的是信號產生到被處理之前的這段時間。

signal函數

#include <signal.h>
void (*signal(int signo, void(*func)(int)))(int);

說明:
signal函數會返回之前的信號處理函數,如果成功的話。
失敗就設置error爲SIG_ERR
裏面的func有三種情況:
1.SIG_IGN:忽略該信號
2.SIG_DFL:使用默認的信號處理函數
3.我們自定義的信號處理函數

注意點:

  • signal函數的一個弱點是,如果我們想要知道以前的信號處理函數,那麼只有通過改變當前的信號處理函數。
  • 進程創建:當一個進程調用fork()生成子進程之後,子進程會保留父進程的信號處理方式。
  • 程序啓動:一個程序啓動之後,所有的信號狀態都是默認或者忽略的。通常程序啓動之後都是默認。但是如果一個進程設置了一個信號爲忽略的,那麼在這個進程調用exec之後,該信號就是忽略的了。但是如果進程把該信號設置爲捕捉,那麼在調用exec之後,該信號的處理方式就是系統默認的了。

對於程序啓動的解釋:
1.如果父進程中對於一個信號註冊了一個自己的信號處理函數,那麼父進程fork之後,子進程也會保留這個信號處理函數。
2.但是如果在子進程中調用exec後,除非在父進程中對於信號的處理方式是默認或者忽略,否則子進程會把該信號的處理方式修改爲默認方式處理。

不可靠信號

  • 早期版本的一個問題是:對於一個信號,我們調用了信號處理函數之後,對於這個信號的信號處理函數我們會進行復位。即將該信號的信號處理函數設置爲默認的動作。
  • 早期版本的另一問題是:我們不可以如果我們不想一個信號發生,我們不可以關閉該信號。

注意點:
還有一點要記住的是:從信號產生到調用信號處理函數來處理該信號是需要一個時間的。

中斷的系統調用

即系統調用是可以被信號中斷的,比如慢速系統調用。系統調用會在被信號打斷之後返回,並且設置error變量爲EINTR

  • 對於被中斷的系統調用,一般有3中處理方式:

    • 人爲重啓被中斷的系統調用
    • 安裝信號時設置SA_RESTART屬性
    • 忽略信號
  • 低速系統調用: read , write, open, pause, ioctl, interprocess communication

ioctl, read, readv, write, writev, wait, waitpid這些系統調用都會在被打斷之後重啓。但是如果我們不想他們重啓,那麼就可以對於相應的信號設置對應的處理過程。

信號中斷與慢系統調用

可重入函數

UNIX定義的可重入函數:
這裏寫圖片描述
這些可重入函數會阻塞那些可能造成不連貫的信號。

注意點:

  • 對於可重入函數,我們需要注意的是: 系統調用肯能影響errno的值,所以我們在調用信號處理函數時,最好先保存errno的值。
  • 對於longjmp以及siglongjmp來說,如果主例程在以非可重入方式更新數據時,這時產生了信號,並且在信號中我們調用了longjmp與siglongjmp,這兩個函數會造成我們中斷更新數據這個步驟。所以在信號處理函數中不要使用longjmp與siglongjmp

可靠信號術語與語義

幾個術語

  • 信號產生:當信號產生時,內核通常把進程表中相應字段的標識設置好。
  • 信號delivered:指的是信號被信號處理函數接受了。
  • 信號pending:指的是信號產生到delivered這一段時間。
  • 信號blocking:指的是進程有權阻塞信號。阻塞的過程是:進程有block mask,這個mask記錄了要阻塞的進程。在信號產生後,如果進程對於該信號不是忽略的,那麼就接受這個信號,然後進程根據自己的mask決定這個信號是不是去信號阻塞隊列中去。對於處於阻塞隊列的信號,我們可以決定解除阻塞或者忽略它。
  • 信號集:表示的是信號的集合
  • 信號屏蔽字:表示的是進程想要屏蔽的信號的集合,一般用位操作。

注意點

  • 如果一個信號已經被進程阻塞了,那麼如果繼續傳遞該信號,那麼進程可能把產生的信號丟棄掉,除非它是實時信號,會把它排隊。
  • 如果有多個signal產生並且deliver給進程的話,POSIX建議將最新的deliver給進程

信號集處理函數

因爲信號的個數可能超過int類型的位數,爲了跨平臺,所以建議使用下面的函數去處理信號集。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int segdelset(sigset_t *set, int signo);

int sigismember(const sigset_t *set, int signo);

alarm與pause 函數

alarm

unsigned int
alarm(unsigned int seconds);

說明:
alarm函數在用來判斷低速系統調用時,要注意他與系統調用之間的競爭關係。也就是說注意alarm的計時到時,但是系統調用卻沒有使用完的問題。
alarm接受一個sec,返回以前的alarm還剩餘的時間或者返回0.
注意一個進程只有一個alarm。alarm(0)會註銷掉進程設置的鬧鐘。
記住現代操作系統都是多任務的。注意線程安全等。

注意點:
見書上的3個編程例子
主要問題是
1.我們記得要保留以前留下的時間,因爲一個進程只會有一個計時。
2.alarm與其他系統調用結合使用時,可能會有alarm已經返回,而其他系統調用還沒開始執行的問題。
3.另外不要在信號處理函數中使用longjmp,而是要用siglongjmp代替,因爲longjmp沒有定義跳出信號處理函數時,信號屏蔽字的處理方法。

信號集

信號集是用來表示多個信號的一個集合。因爲系統中信號的數量會比一個int類型大,所以定義一個sigset_t類型,來定義信號集。本質上還是用位來表示。

sigprocmask函數

int sigprocmask(int how,
                const sigset_t *restrict set,
                sigset_t *restrict oset);

根據how的值的不同,新的mask的值也不同。
通過oset,來存儲舊的mask。
具體定義見書上。

注意點

sigprocmask只能用於單線程環境。
在call sigprocmask之後,如果有處於unblocked狀態的信號在排隊,那麼在sigprocmask返回之前,至少有一個信號會被傳遞給進程,即被信號處理函數處理。
注意點:
需要說明的是,在sigprocmask的函數體內應該就會解除信號的阻塞,這樣信號就是unblocked and pending狀態了,對於有處於unblocked and pending狀態的信號,對於這樣的信號,至少有一個在sigprocmask返回之前就要去調用信號處理函數去處理它。
這就是Figure 10.15中爲什麼在第二次調用sigprocmask之後,先輸出QUIT的信號處理的printf,再去輸出後面的printf(“SIGQUIT unblocked”),因爲在sigprocmask函數體內就把QUIT變成了unbloced狀態,使得QUIT是一個unblocked and pending的信號,所以在sigprocmask函數返回之前就要調用QUIT的信號處理函數。

sigpending函數

#include <signal.h>
int sigpending(sigset_t *set); //成功返回0,否則返回-1

獲取正在處於blocked並且在排隊的signal,將他們放在set中。

sigaction

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

struct sigaction
{
    union
    {
        void (*sa_handler)(int);
        void (*sa_sigaction)(int, siginfo_t *, void *);
    }
    sigset_t sa_mask;
    int sa_flags;
};

說明
1.在現代系統中,使用sigaction,因爲sa_flags爲0時,會自動保存對於該信號的信號處理函數,而早期系統中調用signal之後,該信號的處理方式就變爲了默認。並且使用sigaction之後,在信號處理函數中我們會屏蔽掉該信號,即自動把該信號加入信號屏蔽字,而在我的系統上,signal並不會自動把該信號加入信號屏蔽字。
2.sa_flags的取值及說明:
這裏寫圖片描述
這裏寫圖片描述
3.如果sa_flags使用了SA_SIGINFO,那麼信號處理函數使用的是sa_sigaction,而不是sa_handler。

注意點

sigaction的運行過程:
先使用signal_hander註冊信號處理函數,然後在使用信號處理函數之前,將進程的mask設置爲sa_mask。接着再信號處理函數返回之後,把mask設置爲原先的值。這樣在調用一個signal_handler前,我們可以阻塞任意的信號。
需要特別注意的是:如果一個信號已經在被delivered,那麼這個信號會被放到該進程的信號屏蔽字中去。所以對於非實時信號的多次產生,我們可能只會響應一次。

需要注意的是:當sa_flags設置爲SA_SIGINFO時,信號註冊信號處理函數時,使用的是那個alternate handler,也就是說,當有一個信號被捕捉到了,我們調用的信號處理函數由sa_sigaction來指示,而不是 sa_handler來指示。

sigsetjmp和siglongjmp函數

longjump的問題

對於在signal handler中使用longjmp與setjmp的問題:
這個問題在於longjmp使用之後會跳出當前的棧空間,返回到主例程中去。這樣可能會造成在一個signal handler中設置的信號屏蔽字依然在主例程中有效(我們前面就說了,對於一個delivered的信號,操作系統會把該信號屏蔽掉,這樣我們直接跳出棧空間就會出現一些不可預見的事情)
總結:
信號處理的一個關鍵問題:就是信號屏蔽字在什麼時候去設置,什麼時候釋放,防止出現不可預料的信號屏蔽字的產生。

ing sigsetjmp(sigjmp_buf env, int savemask);

void siglongjmp(sigjmp_buf env, int val);

說明:
sigsetjmp會保存當前的堆棧信息到env中,如果savemask不爲0,那麼當前環境的signal mask也會被保存到env中去。這樣當siglongjmp被調用時,他會檢測他的env,如果他的env是被一個sigsetjmp設置,且這個sigsetjmp的savemask不是0,那麼siglongjmp就會從env中取到以前的堆棧信息,包括以前的signal mask,並且還原他們。
而setjmp與longjmp並沒有說明signal mask的還原問題。
所以對於信號處理問題要使用sigsetjmp 與 siglongjmp

sigsuspend函數

爲什麼引入sigsuspend

因爲sigsuspend是一個原子操作,在我們需要解除信號的屏蔽,並且等待一個信號處理函數的返回時,我們就需要它。沒有它,解除操作與等待操作就會有一個時間窗口,在這個窗口可能會出現信號的丟失。

例子:
這裏寫圖片描述
紅框中的代碼的問題:
這裏出現的問題就是:
如果有一個信號在pause()調用之前就delivered,而且以後也不會出現了,那麼就不會獲得這個信號了。並且pause就會獲得別的信號。
造成這個問題的原因就是sigprocmask與pause連在一起不是一個整體操作。所以我想要在block一個信號之後,在unblock他並且去處理他就要調用sigsuspend(),這相當於將sigprocmask與pause組合成了一個原語操作

sigsuspend

#include <signal.h>
int sigsuspend(const sigset_t *sigmask); //返回-1,並將errno設置爲EINTR

sigsuspend函數:
調用它時,它將當前的信號屏蔽在該爲sigmask,並且阻塞進程,直到有一個信號被捕捉到了,或者有一個終止該進程的信號發生了。如果一個信號被捕捉並且信號處理函數返回了,sigsuspend才返回,並且會把信號屏蔽字設置爲調用sigsuspend之前的值。
注意sigsuspend總是返回-1,並且把errno設置爲ENTR。表示一個被中斷的系統調用。

這個函數就比較有效的解決了上面信號丟失的問題。比如我先用sigprocmask來設置信號屏蔽字,並且保持舊的信號屏蔽字,然後執行critical section,執行完畢之後我想把在block and pending 的信號取出來給unblock掉,那麼就可以調用sigsuspend來重新設定信號屏蔽字,從而unblock我們要unblock的信號,這個時候sigsuspend會等待直到有一個信號處理函數返回,從而防止了上面程序中的丟失信號問題。在sigsuspend返回之後,我們就可以調用sigprocmask來將信號屏蔽字設定爲以前的值。

sigsuspend編程模型

1.一般是先用sigprocmask設定好屏蔽字,然後調用sigsuspend解除一些屏蔽字,並且測試什麼信號到了,如果是我們的信號捕捉到了,那麼就可以返回,然後使用sigprocmask來恢復以前的屏蔽字。
sigsuspend一般用於要等待特定信號的捕捉。

abort函數

void abort(void)

說明:

注意abort函數永不返回。

它的作用是:在進程終止之前,由abort來執行所需要的清理操作。
需要注意的一些東西:
1.abort並不理會進程對它這個信號的阻塞與忽略
2.abort會產生SIGABRT信號,對於這個信號我們可以使用信號捕捉函數來處理,當是在信號處理函數處理完畢返回之後,abort也不會返回。
3.但是在信號處理函數中若是調用exit(),_exti(), _Exit(), siglongjmp或longjmp,那麼進程就可以避免掉abort的不會返回。
4.abort並不會理會block與ignore操作。
5.如果abort調用終止進程,則他對所有打開標準I/O流的效果應當與進程終止前對每個流調用fclose相同。

abort函數要做什麼

注意理解abort()
首先我們要知道abort()需要什麼。
1.abort()不能讓進程忽略SIGABRT
2.abort()不能讓進程阻塞SIGABRT
3.如果採用默認方式使用SIGABRT,那麼在abort中要fflush()
4.abort()會給進程發SIGABRT信號,進程是可以使用信號處理函數來處理SIGABRT這個信號的。
5.如果該信號處理函數正常返回了,那麼應該返回到abort()中,因爲是在abort()中發這個信號的,但是注意到abort()函數並不會返回,他的目的是讓進程使用abort()退出,並且在退出之前做一些清理工作,所以從信號處理函數返回後,abort要把SIGABRT這個信號的信號處理函數設置爲默認值,並且去除對SIGABRT這個信號的阻塞(因爲我們可能在信號處理函數中又把他個屏蔽了,然後再去給進程發SIGABRT這個信號。
6.如果該信號處理函數不是正常返回,例如調用了exit(), _exit(), _Exit(), longjmp, setlongjmp()等就會跳出abort(),不受abort影響。
7.最後一句不會執行,因爲ABRT的默認操作就是退出。

需要注意的一些內容就是:
注意多任務的環境下,編程的細節的處理。即這裏的第5點。

system函數

POSIX要求 system 忽略SIGINT與SIGQUIT,並且要阻塞SIGCHLD

1.爲什麼要阻塞SIGCHILD呢
因爲system一般會調用fork() and exec(),也就是說會有一個子進程產生,那麼我們希望子進程結束後,由我們,而不是fork來處理子進程,所以在system中要阻塞SIGCHILD。
2.爲什麼要忽略SIGINT與SIGQUIT
因爲使用fork() and exec()後,這些進程都是在一個進程組中,這樣我們傳遞一個QUIT之後,在前臺的進程組中所有進程都會收到QUIT信號,但是我們只是想讓跟我們交互的那個進程來接受QUIT,所以在system中我們要忽略SIGQUIT與SIGINT

sleep函數

unsigned int sleep(unsigned int seconds);

sleep 會讓進程suspend,直到
1.wall clock time 到時
2.有一個信號被進程捕捉,並且信號處理函數返回。此時返回剩餘的時間。

sigqueue

int sigqueue(pid_t pid, int signo, const union sigval value)

如果想要queue一個信號,那麼要做3件事:
1.給sa_flags加上SA_SIGINFO
2.signal handler設定爲struct sigaction中的sa_sigaction,而不是sa_handler
3.使用sigqueue函數傳遞消息

sigqueue函數與kill函數類似,但是我們只能給一個進程傳送消息,而kill可以一次性給多個進程傳送消息。

總結

1.Linux的信號機制爲的是解決進程異步的問題。
2.理解Linux的信號傳遞過程。
3.Linux信號編程需要注意的問題:
什麼時候阻塞信號,什麼時候解除阻塞,怎樣處理特定的信號,如何去改變信號處理函數,信號屏蔽字是怎麼改變的,可靠與不可靠信號,可重入函數,系統調用如何被信號中斷,如何防止信號的丟失,什麼是競爭條件。以及一些信號編程模型。注意改變之後恢復以前內容。
4.注意Linux是一個多任務多用戶的系統,所以要注意一些全局變量的改變,以及代碼是不是原子性會產生什麼問題,這些都要注意。

[參考]

信號傳遞過程

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