read 及 write 被 signal 中斷的處理 來源:石頭閒語
當 read() 或 write() 在處理資料時,若剛好產生了一個 signal ,系統為了要
處理這個 signal ,便會中斷 read() 或 write() ,將程序狀態切換到 signal 
的處理動作中。

而當 signal 的處理動作結束後,再將程序狀態切換到 read() 或 write() 的後
續處理動作。

這個後續動作的處理,會影嚮資料讀取或寫入的完整性,但是在這一方面,各系
統間卻有很大的差異存在,看看強調移植性的 POSIX 標準的說明即可。

在 POSIX.1 中是這樣說的:

Section 6.4.1.2 of POSIX states, "If a read() is interrupted by a signal
after it has successfully read some data, either it shall return -1 with
errno set to EINTR, or it shall return the number of bytes read."

在 POSIX.1 中,關於 read() 或 write() 被 signal 中斷時,系統所允許採取
的兩種方法如下:

1.回傳 -1 並設定變數 errno 的值為 EINTR 。
2.回傳已處理的 bytes 數目。

由於兩種方法都有系統在使用,因此 POSIX 採用了折衷的方法,兩種方法都允許。

有些系統只採用其中一種,但也許有的系統是兩種方法都用,當一個 signal 中
斷了 read() 或 write() 後,如果已經讀取或寫入了部份資料,就採第二種方法
處理,如果還沒有任何資料被處理,就採第一種方法處理。

由於連 BSD 或 SVR 在這一方面,都沒有明顯而強制的規定,因此對一個程式設
計人員來說,最好是照 POSIX 的方式,兩種情形都要兼顧到。

針對第一種處理方法,應採用如下的程式碼:

while( read(fd, buf, nbytes) < 0 ) {
  if( errno == EINTR )
    continue;
  else
    FATAL;
}

如果錯誤是因為被 signal 中斷的話,就再讀一次,如果是其他原因導致的錯誤
,則視為致命錯誤,應該中止程式繼續。

不過在有些文件中,則建議應將 EINTR 也視為致命錯誤,我個人並不完全贊同這
種看法,因為在 unix 系統中, signal 本來就是最基本的 IPC 技巧,是一種普
遍存在的事件,如果因此就要中止程式的進行,未免奇怪。

但是我倒認為,如果系統中支援 SA_RESTART 這個 sigaction() 的設定旗標,則
應該使用此旗標來處理 signal ,要求系統自動繼續未完成的 read() 或 write()
動作,此時將不會回傳 EINTR 。

SA_RESTART 是 SVR4 中所定義的,不在 POSIX 文件中,因此不具移植性。

針對第二種方法,則應採用如下的程式碼:

void* bp;
bp = buf;
while( (rc=read(fd, bp, nbytes)) < nbytes ) {
  bp += rc;
  nbytes -= rc;
}

當發現資料並未全部讀取時,則扣掉已讀取的資料數,再要求 read() 繼續讀取
尚未讀取的部份。

將兩種寫法結合起來後,得到的安全寫法如下:

void* bp;
bp = buf;
while( (rc=read(fd, bp, nbytes)) < nbytes ) {
  if( rc > 0 ) {
    bp += rc;
    nbytes -= rc;
  }
  else {
    if( errno == EINTR )
      continue;
    else
      FATAL;
  }
}

可以簡化為:

void* bp;
bp = buf;
while( (rc=read(fd, bp, nbytes)) < nbytes ) {
  if( rc > 0 ) {
    bp += rc;
    nbytes -=rc;
  }
  else if( errno != EINTR )
    FATAL;
}


因此,一個具有安全性(可確保資料被完整處理)的 safe read() 應寫成:

ssize_t saferead(int fd, void *buf, size_t nbyte) {
  size_t nbr;	/* number of bytes readed */
  ssize_t rc;	/* return code of read() */
  void* bp;
  bp = buf;
  nbr = nbyte;
  while( (rc=read(fd, bp, nbr)) < nbr ) {
    if( rc > 0 ) {
      bp += rc;
      nbr -= rc;
    }
    else if( errno != EINTR )
      abort();
  }
}

ps. 函數原型仿造 POSIX 對 read() 的定義。
  ssize_t read(int fd, void *buf, size_t nbyte);

同樣地,一個具有安全性的 safe write() 應寫成:

ssize_t safewrite(int fd, void *buf, size_t nbyte) {
  size_t nbw;	/* number of bytes written */
  ssize_t rc;	/* return code of write() */
  void* bp;
  bp = buf;
  nbw = nbyte;
  while( (rc=write(fd, bp, nbw)) < nbw ) {
    if( rc > 0 ) {
      bp += rc;
      nbw -= rc;
    }
    else if( errno != EINTR )
      abort();
  }
}


關於上面的討論,是針對 read() 及 write() 對正規檔案的資料處理方式,所應
採取的應對措施,如果是對非正規檔案,如 pipe , FIFOs, socket 的話,則上
面的應對措施就不適用。

例如對 socket 進行 read() 時,系統是對方一次輸出多少資料,我方就讀取多
少資料,而不會等到讀滿指定的資料量時才回傳,因此 read() 的回傳值少於指
定的資料量是正常情形。

而 POSIX 文件有說明,對 pipe 進行 write() 時, write() 從不會回傳
EINTR,因此在寫入 pipe 時,可不考慮被 signal 中斷的處理。

在美國聯邦政府的政府採構規格 (FIPS) 中,則是明確地要求 read() 或
write() 必須採取第二種方法處理。

The U.S. Government (in FIPS 151-1) requires that read() return the
number of bytes read. Since the Federal Government is the world's
largest buyer of POSIX systems, it is a good bet that most POSIX 
systems will return the number of bytes read.

我的看法是,這種處理方式,是比較合理的,可以明確的知道有多少資料已被處
理了,減少資料遺失的情形。

如果是採回傳 EINTR 的方式,那我們無法知道有多少資料已經處理了,如果要再
進行一次,就勢必整個資料重送一次,這時候已經處理過的資料,我們卻無法得
知系統將如何處理,問題就比較多了,這也是為何有的文件會建議將 EINTR 也視
為致命錯誤。

小恐龍工作坊 提供