淺談Linux中的信號處理機制(三)


       一晃眼,已經到9月底了,都來不及去感慨時間匆匆。最近常常會想明年的今天我將會在那里干着什么樣的工作?對未來又是憧憬又是擔憂,壓力山大。無論如何現在還是踏踏實實的學習吧,能這樣安安靜靜學習的日子也不多了。不扯了,還是接着前面的寫吧。

SA_RESTART語義

       在上篇提到過,SA_RESTART標志的作用是重啟系統調用。其作用是建立在這樣的基礎上的:在Linux系統上,如果進程正在執行一個低速系統調用期間捕捉到一個信號,那么該系統調用會被中斷,在處理完信號之后,這個系統調用將不會繼續執行。隨后返回錯誤,errno被設置為EINTR。所謂的慢速系統調用包括但不局限於以下:

  1. 對慢速設備(pipe、terminal、FIFO、socket)的讀取操作,當其上不存在數據時,可能會阻塞當前的系統調用
  2. 如果數據不能被相同的類型文件立即接受,寫操作可能會使調用者永遠阻塞
  3. 在某種條件發生之間打開某些類型的文件,可能會發生阻塞(是的open()也會阻塞,打開FIFO的時候就有可能)
  4. pause()、wait()系列函數
  5. 某些ioctl()操作
  6. 某些IPC操作(mq_receive()等)
  7. 設置文件鎖的函數flock()、fcntl()等
  8. epoll_wait() epoll_pwait()
  9. select() poll()

以我現在的功力總結全面是不可能的,平時當我們遇上進程要處理會阻塞的系統調用時,就需要留個心眼兒,要考慮一下被信號中斷的情況。在不使用SA_RESTART的時候,我們要重啟系統調用時,可以這樣組織代碼:

int cnt;
while((cnt = read(fd,buf,BUFSIZE))==-1 && errno== EINTR)               //read()如果被中斷返回錯誤,就會自動重啟
    continue;
...
if(cnt == -1)
    exit(-1);                                                          //其他使read()出錯的情況

  我反正是不喜歡的這樣的代碼風格的,有了SA_RESTART這個標志,我們本可以把代碼寫得更加優雅:

#include <errno.h>
#include <signal.h>
#include <unistd.h>
#define BUFSIZE 1024

void handler(int sig)
{
}

int main()
{
    struct sigaction act;
    act.sa_flags = SA_RESTART;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;
    if(sigaction(SIGINT,&act,0) == -1)
        exit(-1);    
    char buf[BUFSIZE] = {0};
    read(0,buf,BUFSIZE-1);
    write(1,buf,BUFSIZE);
}

  在之前的一篇博客上,曾使用過這個標志,應該說這個標志位還是比較常用的一個,特別是在socket編程中。

可重入函數與不可重入函數

      在《c++11 Thread庫之原子操作》中提到了多線程程序中多個線程之間數據共享所引起的問題。其實在有信號處理的程序中也存在着這樣的問題,因為信號可能會在程序執行的某一時間點異步中斷程序,轉而去執行信號處理函數。和多線程程序一樣,這時候程序就有了兩個執行的線程,雖然不是並發的。如果一個進程的多條線程可以同時安全地(能產生預期的效果)執行某一函數,那么我們稱這個函數是可重入函數,反之則為不可重入函數。

      我做了一個gif圖來表示不可重入函數,就拿我們最熟悉的printf()函數來舉例,我們已經知道printf()函數是行緩沖的IO函數,而這個緩沖區是一個全局的buffer。當主線程中正在執行printf()的時候,一個信號過來了,那么進程會把這個當前線程暫停,轉而去執行信號處理函數,恰巧這個信號處理函數中,也調用了printf()函數,於是buf就被修改了(圖中用變了顏色來表示),當信號處理函數返回以后,主線程恢復執行,而此時它正在使用的buf已經不是之前的那個buf了。於是可能會出現一些意料之外的輸出。

  一般來講,更新全局數據結構的函數,是不可重入的函數。通常有這幾類:

  1. 使用靜態數據結構保存返回信息的函數,有 getlogin() gethostbyname() crypt()...
  2. malloc() free()因為他們在內部維護了一個全局的鏈表來記錄分配和釋放的內存的相關信息
  3. 標准IO函數,他們大都是行緩沖的,而所使用的緩沖區是一個全局的buffer

 當我們所編寫的函數要更新全局變量該怎么辦呢?sig_atomic_t這種數據類型是C語言標准所規定的一種原子操作的數據類型,關於原子操作的內容可移步:《c++11 Thread庫之原子操作》。具體用法和c++11中的std::atomic類型類似,不再贅述。值得一提的是,使用這個數據類型時,應當使用volatile關鍵字聲明,以防止編譯器把其優化到寄存器之中。

GDB調試與信號

      在使用gdb調試程序時,缺省情況下信號會被gdb截獲,導致要調試的程序無法接收到信號,我們可以使用info handle來查看信號的缺省處理方式,同樣info signals可以查看接受到的信號。要想在調試的程序中使用信號,我們需要使用gdb中的handle這個命令,具體用法如這個形式   :handle  signal keywords。keywords的取值如下:

keywords 說明 keywords 說明
stop 當GDB收到signal,停止被調試程序的執行 nostop GDB收到指定的信號,不會應用停止程序的執行,只會打印出一條收到信號的消息
print 如果收到signal,打印出一條信息 noprint 不會打印信息
pass 如果收到signal,把該信號通知給被調試程序 nopass 不會告知被調試程序收到signal
ignore 同nopass noignore 同pass

    handle命令還是比較簡單的,設置完以后,可以像普通的程序那樣調試了。

    關於信號暫時先總結這么多吧,以后用到了什么再慢慢往里邊塞吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM