| 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 也視
為致命錯誤。