1. 程式人生 > >Linux(高階程式設計)9————程序間通訊6(訊號1)

Linux(高階程式設計)9————程序間通訊6(訊號1)

訊號是什麼?

  • 訊號實質是一種軟中斷,用於通知程序發生了某些事件,實際上訊號也可以算作程序間通訊的一種方式,因為我們可在程序通過另一個程序傳送訊號,來告訴另一個程序發生什麼事。
    這樣來講我們聽起來可能還會比較暈。

深入理解訊號:

  • 在我們生活中其實就有訊號的例子,例如:當我們經過十字路口時會看到紅綠燈,紅綠燈其實就可以類比訊號。它來告知程序1(行人)此時應該停下還是通過該路口;同時它也告知程序2(車輛)是否需要停下還是繼續經過;這是訊號對行人和車輛的作用。

訊號的生命週期又是怎樣的呢?

  • 訊號的一生經歷了這樣的過程:
    產生->註冊

    ->登出->處理
    類比於我們生活中的紅綠燈同樣也經過了這樣的過程:

    產生(紅綠燈的每個顏色的出現即訊號的產生)->註冊(紅綠燈最終都是要被行人和車輛注意的,當人們和車輛在腦海中形成了這個意識的時候,也就是訊號註冊成功)->登出(當我們腦海中有了這個意識的時候,我們就不需要在關注紅綠燈了,此時我們就已經做出了處理,停止或通過路口不過在訊號中和人的思維有一些不同它們是先將訊號從“腦”中踢了出去,然後在處理的。)->處理(當我們看到紅綠燈時,我們的一種動作即是對訊號的處理,停止或者繼續通過)。

  • 其實在上述過程中還有一個過程或者狀態:阻塞/遮蔽
    還是紅綠燈例子:當我們看到了紅燈亮了的時候,我們是立即停下了嗎?其實我們並沒有,當我們腦海有了這個意識(即訊號的註冊

    )時,我們可能還低頭看手機或者思索一些問題,突然當我們多向前走了幾步,然後突然又想起是紅燈,然後才停下來的;其實程序也是這樣的它們並不會看到訊號立即就會處理,而是先放在“腦海”,在處理完某些事之後處理訊號的。
    那麼訊號的生命週期應該是這樣的:
    產生->註冊->“阻塞/遮蔽”->登出->處理
    而阻塞/遮蔽並不是訊號所硬性要求和必須的所以對它打起了引號。

  • 說了這麼多讓我們先來看看Linux下面都有那些訊號
    我們可以通過kill命令-l選項檢視linux下所有的訊號:
    在這裡插入圖片描述
    紅色方框裡面的 1—31的31個訊號為非可靠訊號,其餘的34—64的 31個訊號為可靠訊號由此可知Linux下一共有62個訊號。關於可靠訊號和非可靠訊號留在下面講。關於這31個非可靠訊號是從unix下繼承過來的,後來由於訊號的不足,Linux有添加了後面的34到64這31個可靠訊號。
    對訊號各個階段理解:

  • 1.訊號的產生:

    • 訊號的產生有三中方式:
      1.硬體中斷 如:當我們在Linux下執行某一個程式時按下ctrl+c時終止了該程式(程序)SIGINT訊號。
      2.硬體異常 如:程式發生某些錯誤如我麼常見的(segment fault(core dumped))。
      3.軟體中斷:如kill、raise、alarm、sigqueue(在後面一一介紹)。
      下面瞭解一下一些關於訊號的介面:
      1.kill函式:
       int kill(pid_t pid, int sig);
      
      功能:向指定程序(pid)傳送指定訊號(sig)。
      返回值:成功返回0;失敗返回-1。
      sig:可以為上述62個訊號中的任意一個。
      2.raise函式:
      int raise(int sig);
      
      功能:等同於kill(getpid(),sig),給程序/執行緒自身傳送。
      返回值:成功返回0;失敗返回一個非0值。
      3.sigqueue函式:
       int sigqueue(pid_t pid, int sig, const union sigval value);
      
      功能:給指定程序傳送指定的訊號,同時可以通過value攜帶 一個引數過去。
      返回值:成功返回0;失敗返回-1並設定errno值
      4.alarm函式:
       unsigned int alarm(unsigned int seconds);
      
      功能:指定在seconds秒後傳送一個SIGALRM時鐘訊號給程序。
      返回值:返回seconds當前剩餘的秒數,返回0表示沒有時鐘。
      注意: 當seconds傳為0時,取消前面所設定的鬧鐘。
      5.abort函式:
      void abort(void);
      
      功能:類似於kill(getpid(),SIGABRT);向呼叫程序傳送一個SIGABRT訊號。
      SIGABRT為6號訊號,當我們對正在執行的程序傳送這個訊號時,我們的程序會結束掉,並報錯(core dumped),core dump會產生一個核心轉儲檔案,但我們的Linux通常是預設關閉的,但我們可以通過ulimit -c [重新所要設定儲存核心轉儲檔案的大小];即可開啟核心轉儲檔案的生成,此時我們給程序傳送這個訊號的時候,就會在當前目錄下看到例如:core.3954這樣的一個檔案3954告知我們是哪個程序產生的。說到這裡,我們就有必要知道這個核心轉儲檔案是幹嘛的了:當程式異常的時候,會記錄一個核心轉儲檔案,該檔案中記錄的是程式的執行資料,當我們的程式異常崩潰時,而這個錯誤出現的時間可能間隔很長,此時我們的除錯就很難做,錯誤很難定位時,此時核心轉儲檔案就可以幫助我們使用gdb除錯檢視錯誤資訊,定位錯誤。
      gdb 除錯:file 載入可執行程式
      core-file core.pidjia載入core檔案就可以看到錯誤的定位。
      由於核心轉儲檔案可能含有系統安全資訊,以及檔案增多帶來的開銷,linux預設情況是關閉的。
  • 2.訊號的註冊:

    • 訊號的註冊即將這個訊號傳遞給程序,將這個訊號記錄到程序中,讓程序知道有這麼個訊號。
      訊號是記錄在程序PCB中。訊號集合sigset_t結構體中,可以在/usr/include/bits目錄中看到sigset.h,在這個標頭檔案中即可看到該結構體。其實程序記錄一個訊號是通過這個結構體點陣圖記錄的,這個點陣圖就是指定訊號的儲存位置strcut sigpending pending的結構體。可以在/usr/src/kernels/3.10.0-327.el7.x86_64/include/linux這個目錄下看到sched.h標頭檔案,在這裡面就有PCB資訊(注意:核心版本我們·可能是不一樣的哦!!)。

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

      訊號註冊過程:
      1.將訊號對應點陣圖signal置1
      2.加入未決訊號資訊連結串列中
      在這裡插入圖片描述

  • 3.“訊號的阻塞與遮蔽”:
    在PCB中還有一個sigset_t blocked的點陣圖,用來標識那些訊號要被阻塞。在程序看到pending集合中收到了那些訊號,然後就開始處理這些訊號,但是在處理這些訊號之前,程序會先對比一下訊號有沒有在blocked位圖裡面,如果在裡面意味著這個訊號將不被處理,直到解除阻塞。
    其過程是這樣的:
    在這裡插入圖片描述
    訊號阻塞先關介面:
    在此之前得有一些設定:sigset_t mask

    • sigemptyset函式:
      int sigemptyset(sigset_t *set);
      功能:清空集合set中的訊號,防止已經存在在set中其它訊號造成未知錯誤。
      返回值:成功返回0,失敗返回-1;

    • sigfillset函式:
      int sigismember(const sigset_t *set, int signum);
      功能:向集合中新增所有訊號。
      返回值:成功返回0,失敗返回-1;

    • sigaddset函式:
      int sigaddset(sigset_t *set, int signum);
      功能:向集合中新增指定訊號。
      返回值:成功返回0,失敗返回-1;

    • sigprocmask函式:
      int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
      引數:how:對集合中訊號所要做的操作:SIG_BLOCK(將set集合中的訊號設定為新的訊號遮蔽字)、SIG_UNBLOCK(解除set集合中的訊號遮蔽字)、SIG_SETMASK(將set集合中的訊號設定到blocked集合中)。
      oldset:儲存原有的訊號集合。
      功能:一個程序的訊號遮蔽字規定了當前阻塞而給該程序的訊號集。呼叫函式sigprocmask可以檢測或更改其訊號遮蔽字,或者在一個步驟中同時執行這兩個操作。
      返回值:成功返回0,失敗返回-1。

    • sigismember函式:
      int sigismember(const sigset_t *set, int signum);
      引數:signum 表示所要檢測的訊號值(或訊號的巨集)。
      功能:檢測一個訊號是否在set中。
      返回值:成功返回0,失敗返回-1。

    • sigpending函式:

      int sigpending(sigset_t *set);
      

      功能:將當前(訊號註冊集合)中的訊號取出放入set中。

  • 4.訊號的登出:
    從pending集合中將要處理的訊號移除,該過程就是訊號的登出。
    注意:訊號實現將要處理的訊號移除,然後才處理的。
    移除分為兩種情況:
    在這裡插入圖片描述

  • 5.訊號處理:
    訊號處理方式分為:預設處理忽略處理自定義處理
    預設處理:作業系統原有定義好的對於一個訊號的處理方式。
    忽略處理:切記忽略和阻塞不是一回事,一個訊號如果被忽略了就等於直接被丟棄,不會修改點陣圖,不會被註冊。
    自定義處理:我們使用者自己定義一個訊號的處理方式,然後告訴作業系統該訊號來了就按自定義方式處理。
    訊號是在程序從核心態切換到使用者態的時候去檢測一下有沒有訊號需要被處理,然後才處理的。

    • 關於訊號處理的介面:
      signal函式:
      	typedef void (*sighandler_t)(int);
         	sighandler_t signal(int signum, sighandler_t handler);
      
      引數:signum:要處理的訊號(巨集/訊號值)。
      handler:SIG_IGN 忽略處理;SIG_DFL 預設處理;函式指標,該訊號的自定處理方式。
      sigaction函式:
      int sigaction(int signum, const struct sigaction *act,
      struct sigaction *oldact);
      
      引數:act處理方式只不過是封裝在這個結構體中,下面詳細說明。
      oldact:用於儲存原先對該訊號的處理方式。
       struct sigaction {
             void     (*sa_handler)(int);
             void     (*sa_sigaction)(int, siginfo_t *, void *);
             sigset_t   sa_mask;
             int        sa_flags;
             void     (*sa_restorer)(void);
         };
      
      引數:sa_flags:0 時使用sa_handler ;1 時使用sa_sigaction,這種處理方式會結收訊號攜帶的個引數。
      sa_mask:在處理訊號的時候,希望這個處理不會被其它訊號的到來而打擾,因此sa_mask就是用於在處理訊號時要阻塞,就把其他訊號新增到這個集合中暫時阻塞。
      有兩個比較特殊的訊號: SIGSTOP、SIGKILL這兩個訊號無法被忽略,也無法被自定義處理。
  • 本片部落格講述訊號的一生和各個階段的處理方式以及部分簡單原理,由於時間和篇幅問題,很多問題還未能解決,會在下一篇部落格中繼續更新。