1. 程式人生 > >程序間的訊號 --------詳解(通過訊號去控制程序的執行狀態)

程序間的訊號 --------詳解(通過訊號去控制程序的執行狀態)

(1)概述
  1.訊號是一種軟體中斷,用來處理非同步事件
  2.訊號的本質是一種程序間的通訊,一個程序向另一個程序傳送訊號
  3.執行kill -l可檢視系統所有的訊號
  4.作用:ctl+c時用來做一些收尾工作:
     1.刪除管道.刪除共享記憶體.刪除訊號量.刪除訊息佇列..
     2.程序間通訊

(2)訊號的生命週期
  程序之間約定好:如果發生了某件事情T ( trigger ),就向目標程序( destination   process )
  傳送某特定訊號 X ,而目標程序看到 X ,就意識到 T 事件發生了,目標程序就會執行相應的動作 A ( action )
  1.Linux 核心收到了產生的訊號,然後就在目標程序的程序描述符裡記錄了一筆:收到訊號一枚。
       2.Linux 核心會在適當的時機,將訊號遞送( deliver )給程序。
  3.在核心收到訊號,但是還沒有遞送給目標程序的這一段時間裡,訊號處於掛起狀態,也稱為未決訊號。
  4.核心將訊號遞送給程序,程序就會暫停當前的控制流,轉而去執行訊號處理函式。
  5.實際情況還應該考慮的問題
    1.目標程序正在執行關鍵程式碼,不能被訊號中斷,需要阻塞某些訊號
    2.如何處理重複的訊號,排隊還是丟棄?
    3.已有多個不同的訊號被掛起,應該優先遞送哪個訊號?
    4.對於多執行緒的程序,如果向該程序傳送訊號,應該由哪個執行緒來負責響應?


(3)訊號的產生
  [1] 硬體異常
    1.硬體檢測到了錯誤並通知核心,由核心傳送相應的訊號給相關程序
    2.常見硬體異常的訊號
      SIGBUS: 匯流排異常
      SIGFPE:    算數錯誤
      SIGILL: 非法及其指令
      SIGSEGV: 段錯誤
        程序訪問未初始化的指標或 NULL 指標指向的地址
        程序在使用者態訪問核心部分的地址    
        程序修改只讀的記憶體地址

  [2] 終端相關的訊號
    ·Ctrl+C :產生 SIGINT 訊號。
    ·Ctrl+\ :產生 SIGQUIT 訊號。
    ·Ctrl+Z :產生 SIGTSTP 訊號。
    鍵入這些訊號生成字元,相當於向前臺程序組傳送了對應的訊號。

  [3] 軟體事件相關的訊號
    · 子程序退出,核心可能會向父程序傳送 SIGCHLD 訊號。
    · 父程序退出,核心可能會給子程序傳送訊號。
    · 定時器到期,給程序傳送訊號。

(4)訊號預設處理函式
  [1]訊號的預設操作
    · 顯式地忽略訊號:ignore
    · 終止程序:terminate
    · 生成核心轉儲檔案並終止程序(用於除錯):core
    · 停止程序(暫停程序):stop
    · 恢復程序的執行: continue

(5)訊號的分類
  [1]不可靠訊號    
        1.訊號值在 [1,31] 之間的所有訊號,都被稱為不可靠訊號
        2.不可靠訊號是從傳統的 Unix 繼承而來的
        3.不可靠訊號如果收到某不可靠訊號,核心發現已經存在該訊號處於未決狀態,
          就會簡單地丟棄該訊號。因此傳送不可靠訊號,訊號可能會丟失,

  [2]可靠訊號
    1.在 [SIGRTMIN,SIGRTMAX] 之間的訊號,被稱為可靠訊號
    2.核心內部有佇列來維護,如果多次收到可靠訊號,核心會將訊號掛到相應的佇列中,因此不會丟失。


(6)傳統訊號(System V風格)
  [1]在相同的 Linux 平臺上,由於 glibc 版本的差異,提供的 signal 函式的語義也有差異。
        在早期的 libc4 和 libc5 中, signal 函式的語義是 Syetem V 風格的。因此,從可移植的角度來看,不應該使用 signal 函式。

  [2]訊號執行時遮蔽自身的特性
    1.對於傳統的 System V 訊號機制,在訊號處理期間,不會遮蔽對應的訊號,而這就會引起訊號處理函
     數的重入。
    2.System V 風格的訊號,在其訊號處理期間沒有遮蔽任何訊號,換句話說,執行訊號處理函式期間,
     處理流程可以被任意訊號中斷,包括正在處理的訊號。

  [3]訊號中斷系統呼叫的重啟特性
    1.系統呼叫在執行期間,很可能會收到訊號,此時程序可能不得不從系統呼叫中返回,去執行訊號處理函式
    2.對於執行時間比較久的系統呼叫(如 wait 、 read 等)被訊號中斷的可能性會大大增加。
    3.系統呼叫被中斷後,一般會返回失敗,並置錯誤碼為 EINTR
    4.如果程式設計師希望處理完訊號之後,被中斷的系統呼叫能夠重啟,則需要通過判斷 errno 的值來解決,
     即如果發現錯誤碼是 EINTR ,就重新呼叫系統呼叫。

(7)signal---kill.raise.alarm(訊號安裝和傳送)
  [1]signal函式
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    返回值: sighandler_t型別的函式指標
    形參:   int, sighandler_t型別函式指標
      void (*signal(int signum, sighandler_t handler))(int);
    使用例子1:
        int sig_int(int sig) {printf("sig_int");}
        signal(SIGINT, sig_int);
    使用例子2:
          signal(SIGINT, SIG_IGN);    // 忽略SIGINT訊號
         signal(SIGINT, SIG_DFL);    // 對於SIGINT訊號,使用預設處理函式
    ==>寫法二: void (*signal(int signum, void (*handler)(int)))(int);  

  [2]kill函式(和signal搭配)
      int kill(pid_t pid, int sig);
       ·pid > 0 :傳送訊號給程序 ID 等於 pid 的程序。
       ·pid = 0 :傳送訊號給呼叫程序所在的同一個程序組的每一個程序。
       ·pid = -1 :有許可權向呼叫程序傳送訊號的所有程序發出訊號, init 程序和程序自身除外。
       ·pid < -1 :向程序組 -pid 傳送訊號。
       當函式成功時,返回 0 ,失敗時,返回 -1 ,並置 errno
       1. kill 函式不僅可以向特定程序傳送訊號,也可以向特程序組傳送訊號
       2. 所有訊號值都是>0的。 若第二個引數 signo的值為0,這種情況下,來檢測目標程序或程序組是否存在,
        如果 kill 函式返回 -1 且 errno 為 ESRCH ,則可以斷定我們關注的程序或程序組並不存在

    [3]raise函式
        int raise(int sig);
       向程序自身傳送訊號的介面
      int raise(int sig);
      1.單執行緒的程式而言
        相當於:kill(getpid(),sig)
      2.對於多執行緒的程式而言
       相當於:pthread_kill(pthread_self(),sig) // 給當前執行緒發訊號

    [4]alarm函式(鬧鐘)
        unsigned int alarm(unsigned int seconds);
        描述: 每隔5s給當前程序傳送一個SIGALRM訊號。
            alarm也稱為鬧鐘函式,它可以在程序中設定一個定時器,當定時器指定的時間到時,它向程序傳送SIGALRM訊號。
            可以設定忽略或者不捕獲此訊號,預設動作是終止呼叫該alarm函式的程序。
        成功:如果呼叫此alarm()前,程序已經設定了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,否則返回0。
        出錯:-1


(8)sigaction---sigqueue(訊號的傳送和安裝)
    [1]sigqueue(傳送訊號)
        int sigqueue(pid_t pid, int sig, const union sigval value);
        引數: pid:     要傳送訊號的程序ID
              sig:      要傳送的訊號
              value: 傳送的伴隨資料,該引數的資料型別是聯合體
                union sigval {
                    int sival_int;
                    void *sival_ptr;    // 幾乎不用(每個程序都有獨立的地址空間)
                };
            //考慮到不同的程序有各自獨立的地址空間,傳遞指標到另一個程序幾乎沒有任何意義。因此 sigqueue 函式很少傳遞指標( sival_ptr ),大多是傳遞整型( sival_int )。    
        1.傳統的訊號多用 signal/kill 這兩個函式搭配
        2.signal函式的表達力有限,控制不夠精準;所以引入了sigqueue函式來完成實時訊號的傳送
        3.sigqueue函式也可以傳送空訊號(訊號0)來檢查程序是否存在。
        4.和 kill 函式不同的地方在於,它不能通過將pid指定為負值而向整個程序組傳送訊號。


    [2]    sigaction(安裝訊號)
        int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
        1.引數: signum: 訊號編號
            1.struct sigaction {    // 第二個引數
                union {
                    void (*sa_handler)(int);//sa_flags裡沒有設定SA_SIGINFO標記的訊號處理函式
                    //sa_flags裡設定了SA_SIGINFO標誌, 的訊號處理函式
                    void handle(int signum, siginfo_t *info, void *ucontext);
                    // 收到的額外資料儲存在info->si_value中的si_int和si_ptr中
                }
                sigset_t sa_mask;            // 阻塞訊號集
                int sa_flags;                // 標誌
                void (*sa_restorer)(void);    // 恢復處理程式
            };        
            2.siginfo_t {     // handle訊號處理函式的第二個引數    
                int si_signo; // 訊號的值
                int si_code;  // 訊號來源:SI_USER.SI_TKILL.SI_QUEUE.. 
                pid_t si_pid;    // 訊號傳送程序的程序 ID 。
                uid_t si_uid;   //訊號傳送程序的真實使用者 ID 。
                union sigval si_value; //sigqueue 函式傳送訊號時所帶的伴隨資料。
                ...
            }
            3.ucontext是 void* 型別的,其實它是一個 ucontext_t 型別的變數。
                這個結構體提供了程序上下文的資訊,用於描述程序執行訊號處理函式之前程序所處的狀態。
                通常情況下訊號處理函式很少會用到這個變數
            4. sa_flags的含義
                1.SA_NOCLDSTOP
                    一旦父程序為SIGCHLD訊號設定了這個標誌位,那麼子程序停止和子程序恢復這兩件事情,就不會向父程序傳送SIGCHLD訊號了
                        但是子程序切換為SIGCONT時還是會給父程序傳送SIGCHLD訊號。
                2.SA_NOCLDWAIT
                    如果父程序為SIGCHLD設定了SA_NOCLDWAIT 標誌位,那麼子程序退出時,就不會進入殭屍狀態,而是直接自行了斷。
                    對於Linux而言,子程序轉換切換為SIGSTOP.SIGCONT.SIGKILL時都會給父程序傳送SIGCHLD訊號。這點和上面的 SA_NOCLDSTOP 略有不同。
                3.SA_ONESHOT 和 SA_RESETHAND
                    這兩個標誌位的本質是一樣的,表示訊號處理函式是一次性的,訊號遞送出去之後,訊號處理函式便恢復成預設值 SIG_DFL 。
                4.SA_NODEFER 和 SA_NOMASK
                    這兩個標誌位的作用是一樣的,在訊號處理函式執行期間,不阻塞當前訊號。
                5.SA_RESTART
                    這個標誌位表示,如果系統呼叫被訊號中斷,則不返回錯誤,而是自動重啟系統呼叫
                6.SA_SIGINFO
                    沒有設定SA_SIGINFO:
                        跟signal使用方法相同, 使用一個引數的訊號處理函式
                        void (*sa_handler)(int);
                    設定了SA_SIGINFO:
                        1.這個標誌位表示訊號傳送者會提供伴隨資料。這時使用帶3個引數的訊號處理函式
                            void handle(int, siginfo_t *info, void *ucontext);    
                        2.能獲取到傳送程序的PID、UID.訊號來源.及傳送的額外資訊...
        2.注意
            1.對SIGKILL 和 SIGSTOP,不可以為它們安裝訊號處理函式,也不能遮蔽掉這些訊號。
                若通過 sigaction 強行給 SIGKILL 或 SIGSTOP 註冊訊號處理函式,則會返回-1,並置errno為EINVAL。 

  (9)等待訊號
    [1]pause函式
      int pause(void);
      作用:使呼叫程序(執行緒)進入休眠狀態(就是掛起);直到接收到訊號且訊號函式成功返回
        pause函式才會返回
      返回值:始終返回-1

(10)執行緒的阻塞訊號集      
    [1]概述
        1.每個執行緒都擁有獨立的阻塞訊號掩碼。
        2.開會時關閉手機是一種比較極端的例子。更合理的做法是暫時遮蔽部分人的電話。對於某些重要的電話,比如兒子老師的電話、父母的電
            話或老闆的電話,是不希望被遮蔽的。訊號也是如此。程序在執行某些操作的時候,可能只需要遮蔽一部分訊號,而不是所有訊號。
        3.訊號集: 資料型別為 sigset_t,sigset_t 的型別是位掩碼,每一個位元代表一個訊號。    
        4.SIGKILL 訊號和 SIGSTOP 訊號不能被阻塞。(設定訊號集時會被核心剔除)(避免出現神仙程序)
        5.對於多執行緒的程序而言,每一個執行緒都有自己的阻塞訊號集。

    [2]常用API
        int sigemptyset(sigset_t *set);    // 初始化訊號集set中的訊號為空
        int sigfillset(sigset_t *set);  // 將所有訊號新增進訊號集set
        int sigaddset(sigset_t *set, int signum); // 在訊號集中新增signum訊號
        int sigdelset(sigset_t *set, int signum); // 在訊號集中刪除signum訊號
        int sigismember(const sigset_t *set, int signum); // 判斷signum是否存在於set訊號集中
        int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 設定訊號集
            oldset: 如果oldset為非NULL,則訊號掩碼的先前值儲存在oldset中,故一般設定為NULL。
            how的選項:
                SIG_BLOCK:   在當前阻塞訊號集中增加set訊號集中的訊號
                SIG_UNBLOCK:在當前阻塞訊號集中刪除set訊號集中的訊號
                SIG_SETMASK:阻塞訊號集被設定為set訊號集。

    [3]pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
        1.為了更顯式地設定執行緒的阻塞訊號掩碼,執行緒庫提供了 pthread_sigmask 函式來設定執行緒的阻塞訊號掩碼。
        2.事實上 pthread_sigmask 函式和 sigprocmask 函式的行為是一樣的。
            pthread_sigmask函式將呼叫sigprocmask函式(核心原始碼)

    [4]注意
        1.如果阻塞了某個訊號A, 然後呼叫pause。在程式的執行過程中如果一直給程序傳送
            訊號A,pause函式將不會返回, 因為傳送的訊號都被阻塞。
        2.對於訊號集中阻塞的不可靠訊號a, 在阻塞過程中, 傳送多個訊號a時,之前掛起的訊號a會被拋棄;
           解除阻塞後, 最終傳送到目標程序的訊號a只有一個。
        3.對於訊號集中阻塞的可靠訊號b,在阻塞過程中, 傳送多個訊號a時,會建立一個佇列來管理阻塞的訊號;
           解除阻塞後, 最終傳送到目標程序的訊號b = 訊號b的傳送次數。
        4.SIGKILL 訊號和 SIGSTOP 訊號不能被阻塞。(設定訊號集時會被核心剔除)

(11)SIGCHID訊號
    1.父程序可以監測子程序的以下三種事件; 每次狀態改變,子程序會發SIGCHID給父程序
        · 子程序終止(即子程序死亡)
        · 子程序停止(即子程序暫停)
        · 子程序恢復(即子程序從暫停中恢復執行)

    2.若使用sigaction---sigqueue(訊號的傳送和安裝)
        1.sigaction使用了巨集SA_NOCLDSTOP:
            一旦父程序為SIGCHLD訊號設定了這個標誌位,那麼子程序停止和子程序恢復這兩件事情,就不會向父程序傳送SIGCHLD訊號了。
                但是子程序切換為SIGCONT時還是會給父程序傳送SIGCHLD訊號。
        2.sigaction使用了巨集SA_NOCLDWAIT:
            如果父程序為SIGCHLD設定了SA_NOCLDWAIT 標誌位,那麼子程序退出時,就不會進入殭屍狀態,而是直接自行了斷。對於Linux而言,子程序轉換切換為SIGSTOP.SIGCONT.SIGKILL時都會給父程序傳送SIGCHLD訊號。這點和上面的 SA_NOCLDSTOP 略有不同。

(12)訊號的練習
    訊號示例: (通過訊號模擬 <司機--售票員>)
    1.平常司機在車上休息,售票員在觀察上車人數
    2.售票員發現車上人滿了就提醒司機發車
    3.中途停兩個站: 9km. 15km處, 這時售票員要提醒司機
    6.總里程20公里
    7.到終點站後司機提醒售票員讓所有乘客下車
    8.售票員退出後, 司機才能退出