多工作業下的資料讀寫處理事項 - read()/write() 被 signal 中斷的處理
當 read() 或 write() 在處理資料時,若剛好產生了一個 signal ,系統為了要處理這個 signal ,便會中斷 read() 或 write() ,將程序狀態切換到 signal 的處理動作中。 而當 signal 的處理動作結束後,再將程序狀態切換到 read() 或 write() 的後續處理動作。
在多工作業環境下, read()/write() 被系統中斷,將作業資源切換到其他工作狀態下的情形,是非常普遍的現象。中斷讀寫動作後再切換回去,想當然爾會關係到資料讀寫的完整性。因此切換回去後的後續處理動作,自然會影嚮資料讀取或寫入的完整性,但是在這一方面,各系統間卻有很大的差異存在,在 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."
Section 6.4.1.2 of POSIX.1
在 POSIX.1 中,關於 read() 或 write() 被 signal 中斷時,系統所允許採取的兩種實作規格如下列示:
- 回傳 -1 並設定變數 errno 的值為 EINTR 。
- 回傳已處理的 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 。
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 也視為致命錯誤。
樂多舊回應