7-檔案IO-阻塞與非阻塞IO
1. 阻塞 IO
通常來說,從普通檔案讀資料,無論你是採用 fscanf,fgets 也好,read 也好,一定會在有限的時間內返回。但是如果你從裝置,比如終端(標準輸入裝置)讀資料,只要沒有遇到換行符(‘\n’),read 一定會“堵”在那而不返回。還有比如從網路讀資料,如果網路一直沒有資料到來,read 函式也會一直堵在那而不返回。
read 的這種行為,稱之為 block,一旦發生 block,本程序將會被作業系統投入睡眠,直到等待的事件發生了(比如有資料到來),程序才會被喚醒。
系統呼叫 write 同樣有可能被阻塞,比如向網路寫入資料,如果對方一直不接收,本端的緩衝區一旦被寫滿,就會被阻塞。
1.1 阻塞讀終端實驗
- 程式碼
// 檔名:blockdemo.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
char buf[10];
int len;
while(1) {
// STDIN_FILENO 是標準輸入的描述符,它的值是 0. STDOUT_FILENO 是標準輸出的描述符,它的值是 1.
len = read(STDIN_FILENO, buf, 10 );
write(STDOUT_FILENO, buf, len);
}
return 0;
}
- 編譯
$ gcc blockdemo.c -o blockdemo
- 執行
$ ./blockdemo
如果你不向終端鍵入任何字元,程式將永遠阻塞在 read 系統呼叫處。
1.2 阻塞呼叫面臨的問題
假設有這樣一個場景,我要從 2 個不同裝置讀取資料進行資料,分別進行處理。虛擬碼如下。
while(1) {
阻塞 read(裝置1);
處理裝置1資料;
阻塞 read(裝置2);
處理裝置2資料;
}
上面有什麼問題呢?仔細想想,假如裝置1一直沒有資料到來,那麼程式就一直停在 read(裝置1)這一行,即使裝置2有資料到來,也將得不到處理。
經驗很豐富的同學肯定想出了各種方案,比如什麼多程序多執行緒什麼的。抱歉,我們是新手,目前只會單執行緒單程序。
既然如此,可否有一種方案,讓 read 不阻塞?不管有沒有資料到來,read 執行完立即返回,然後再通過某種特殊的變數來判斷本次呼叫到底有沒有資料到來?
實際上,這個方案是可行的。請續讀下文。
2. 非阻塞 IO
- 如何解決從不同裝置讀資料而造成的干擾
現在,把剛剛上面面臨問題的程式碼改成這樣。
while(1) {
非阻塞 read(裝置1);
if (裝置1有資料){
處理裝置1資料;
}
非阻塞 read(裝置2);
if (裝置2有資料) {
處理裝置2資料;
}
}
且不論這樣的程式碼執行效率如何,我們先看看它是否解決了前面的問題。
如果裝置1沒有資料到來,read(裝置1)也會立即返回,有資料就處理資料,沒資料接著執行 read(裝置2),有資料就處理資料,沒有的話緊接著又去 read(裝置1)……如此往復。
我們會發現,裝置1和裝置2之間,不論有沒有資料到來,都不會互相影響,而不像之前阻塞IO那樣,如果裝置1沒有資料,將會影響到裝置2的資料處理。
這種方案非常不錯,總之目前來說是這樣的,先輩們給這種解決方案取了一個很好聽的名字——Poll (輪詢)。
- 效率
現在,是時候把效率搬上來談談了。
如果裝置1和裝置2一直沒有資料到來,這個 while 迴圈將不斷空轉,CPU將面臨高負荷。這是一種極大的浪費。不像阻塞方式,沒有資料,就直接被作業系統投入睡眠。
那麼,我們把上面的程式碼再改改。
- 修改方案
新增 sleep,主動讓出 CPU。
while(1) {
非阻塞 read(裝置1);
if (裝置1有資料){
處理裝置1資料;
}
非阻塞 read(裝置2);
if (裝置2有資料) {
處理裝置2資料;
}
sleep(5); // 加了一行
}
這種方案仍然有問題,雖然可以每次讓出一定時間的CPU,但是也導致了裝置的資料得不到及時處理。可是以目前的知識,我們只能做到這個份上。未來,我們有機會學習更加先進的技術,來完美解決這個問題。提前預告一下,它的大名是——select。
2.1 非阻塞IO實驗
有幾個需要注意的地方:
- 阻塞非阻塞是檔案本身的特性,不是系統呼叫read/write本身可以控制的。
- 終端預設是阻塞的,我們可以重新 open 裝置檔案 /dev/tty(表示當前終端),開啟的時候指定 O_NONBLOCK 標誌就行了。
- 非阻塞 read,如果有資料到到來,返回讀取到的資料的位元組數。如果沒有資料到來,返回 -1,這時候我們沒有辦法判斷到底是因為出錯而返回,還是因為沒有資料返回。所以需要藉助 errno 全域性變數,來判斷是什麼原因。如果 errno 的值為 E_WOULDBLOCK或 E_AGAIN(這兩個巨集的值是一樣的),表示當前沒有資料到達,希望你再嘗試一次。因為 read 返回 -1 前,linux 系統會在 read 返回前給 errno 賦值,來告訴應用層,到底是什麼原因。
- 非阻塞IO讀終端資料
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h> // errno 變數的標頭檔案
#include <stdlib.h>
char MSG_TRY[] = "try again!\n";
int main() {
char buffer[10];
int len;
int fd;
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
while(1) {
len = read(fd, buffer, 10);
if (len < 0) {
if (errno == EAGAIN) {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(1); // 讓出 CPU,避免CPU長時間空轉
}
else {
perror("read");
exit(1);
}
}
else {
break;
}
}
write(STDOUT_FILENO, buffer, len);
return 0;
}
- 非阻塞IO讀終端資料結合等待超時
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
char MSG_TRY[] = "try again!\n";
char MSG_TMOUT[] = "time out!\n";
int main() {
char buffer[10];
int len;
int fd;
int i;
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
// 超過 5 秒後,無論有沒有資料都退出。
for (i = 0; i < 5; ++i) {
len = read(fd, buffer, 10);
if (len < 0) {
if (errno == EAGAIN) {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(1);
}
else {
perror("read");
exit(1);
}
}
else {
break;
}
}
if (i == 5) {
write(STDOUT_FILENO, MSG_TMOUT, strlen(MSG_TMOUT));
}
else {
write(STDOUT_FILENO, buffer, len);
}
return 0;
}
3. 總結
本文簡單介紹了阻塞與非阻塞IO的概念,並給出一個實際生產環境可能遇到的例子,利用單執行緒來解決多裝置資料處理的方法。
因為還沒有學習多程序與多執行緒,我們只能藉助非阻塞IO來完成這個功能。在後面的深入學習中,我們將出給出更加完美的解決方案,解決因為沒有資料到來而使 CPU 空轉的問題。