最近更新: 2006-10-19

多工作業下的資料讀寫處理事項 - read()/write() 被 signal 中斷的處理

當 read() 或 write() 在處理資料時,若剛好產生了一個 signal ,系統為了要處理這個 signal ,便會中斷 read() 或 write() ,將程序狀態切換到 signal 的處理動作中。 而當 signal 的處理動作結束後,再將程序狀態切換到 read() 或 write() 的後續處理動作。

在多工作業環境下, read()/write() 被系統中斷,將作業資源切換到其他工作狀態下的情形,是非常普遍的現象。中斷讀寫動作後再切換回去,想當然爾會關係到資料讀寫的完整性。因此切換回去後的後續處理動作,自然會影嚮資料讀取或寫入的完整性,但是在這一方面,各系統間卻有很大的差異存在,在 POSIX (*1) 中也未採取強制規範。

*1 現代作業系統,主要依循著 POSIX ("Portable Operating System Interface for uniX") 規格建議書實作。 POSIX 係由 IEEE 和 ISO 集合主流作業系統開發廠商制定的作業系統規格建議書。由於主流作業系統主要就是 Unix 和類似 Unix 的作業系統,如 Unix, AIX, HP-UX, SunOS ,故 POSIX 沿用了 Unix-like 家族的內容,其 API 是 C 式風格,並有高達 90% 的部份和 ANSI C 相同。使用 C/C++ 開發工具的 programmer ,很快就會熟悉 POSIX 。
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."
Section 6.4.1.2 of POSIX.1

在 POSIX.1 中,關於 read() 或 write() 被 signal 中斷時,系統所允許採取的兩種實作規格如下列示:

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

由於兩種規格都有系統在使用,因此 POSIX 就採用了折衷之道,兩種規格都允許。 有些系統只採用其中一種,但也許有的系統是兩種規格都用,即當一個 signal 中斷了 read() 或 write() 後,如果已經讀取或寫入了部份資料,就採第二種規格處理,如果還沒有任何資料被處理,就採第一種規格處理。 由於連 BSD (Berkeley Software Distribution)SVR (System V Release) 在這一方面,都沒有明顯而強制的規定,因此對一個程式設計人員來說,最好是照 POSIX 的方式,兩種規格都要兼顧到。

第一種規格的因應方法

如果錯誤是因為被 signal 中斷的話,就再讀一次,如果是其他原因導致的錯誤,則視為致命錯誤,應該中止程式繼續。 不過有些文件則建議應將 EINTR 也視為致命錯誤。我個人並不完全贊同這種看法,因為在 Unix-like 系統中, signal 本來就是最基本的 IPC 技巧,是一種普遍存在的事件,如果因此就要中止程式的進行,未免奇怪。 但是我倒認為,如果系統中提供 SA_RESTART (*2) 這個 sigaction() 的設定旗標,則應該使用此旗標來處理 signal ,要求系統自動繼續未完成的 read() 或 write() 動作,此時將不會回傳 EINTR 。

*2 這是 SVR4 中所定義的選項,不在 POSIX 規格中,因此不具移植性。
1
2
3
4
5
6
while( read(fd, buf, nbytes) < 0 ) {
  if( errno == EINTR )
    continue;
  else
    FATAL;
}

第二種規格的因應方法

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

1
2
3
4
5
6
void* bp;
bp = buf;
while( (rc=read(fd, bp, nbytes)) < nbytes ) {
  bp += rc;
  nbytes -= rc;
}

兼顧兩種規格的安全方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
  }
}

可以再重整如下:

1
2
3
4
5
6
7
8
9
10
void* bp;
bp = buf;
while( (rc=read(fd, bp, nbytes)) < nbytes ) {
  if( rc > 0 ) {
    bp += rc;
    nbytes -=rc;
  }
  else if( errno != EINTR )
    FATAL;
}

仿造 POSIX 對 read()/write() 的原型,定義一個具有安全性的 (可確保資料被完整處理)的 safe read() 和 safe write() 應寫成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// The prototype of POSIX of read() is:

//   ssize_t read(int fd, void *buf, size_t nbyte);

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();
  }
}

// The prototype of POSIX of write() is:

//   ssize_t write(int fd, void *buf, size_t nbyte) {

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.

FIPS of the U.S. Government

我的看法是,這種處理方式比較合理,可以明確知道有多少資料已被處理了,減少資料遺失的情形。 如果是採回傳 EINTR 的方式,那我們無法知道有多少資料已經處理了,如果要再進行一次,就勢必整個資料重送一次。這時已經處理過的資料,我們卻無法得知系統將如何處理 (除非作業系統的手冊白紙黑字說明其實作方式,或是從作業系統的 source 中確認) ,問題就比較多了,這也是為何有的文件會建議將 EINTR 也視為致命錯誤。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/2159383.html

樂多舊回應
cmchao@gmail.com(cmchao) (#comment-19094621)
Mon, 11 May 2009 11:14:23 +0800
很有用的資訊,感謝你的整理