1. 程式人生 > >6、阻塞和非阻塞

6、阻塞和非阻塞

失敗 解決 實驗 file iostream pac \n std shell

  讀常規文件是不會阻塞的,不管讀多少字節,read一定會在有限的時間內返回。但是從終端設備網絡讀則不一定,如果從終端輸入的數據沒有換行符,調用read讀終端設備就會阻塞如果網絡上沒有接收到數據包,調用read從網絡讀就會阻塞,至於會阻塞多長時間也是不確定的,如果一直沒有數據到達就一直阻塞在那裏。同樣,寫常規文件是不會阻塞的,而向終端設備或網絡寫則不一定。

  現在先明確一下阻塞(Block)這個概念。當進程調用一個阻塞的系統函數時,該進程被置於睡眠(Sleep)狀態,這時內核調度其它進程運行,直到該進程等待的事件發生了(比如網絡上接收到數據包,或者調用sleep指定的睡眠時間到了)它才有可能繼續運行。與睡眠狀態相對的是運行(Running)狀態,在Linux內核中,處於運行狀態的進程分為兩種情況:

  正在被調度執行。CPU處於該進程的上下文環境中,程序計數器(eip)裏保存著該進程的指令地址,通用寄存器裏保存著該進程運算過程的中間結果,正在執行該進程的指令,正在讀寫該進程的地址空間。
  就緒狀態。該進程不需要等待什麽事件發生,隨時都可以執行,但CPU暫時還在執行另一個進程,所以該進程在一個就緒隊列中等待被內核調度。系統中可能同時有多個就緒的進程,那麽該調度誰執行呢?內核的調度算法是基於優先級和時間片的,而且會根據每個進程的運行情況動態調整它的優先級和時間片,讓每個進程都能比較公平地得到機會執行,同時要兼顧用戶體驗,不能讓和用戶交互的進程響應太慢。
  下面這個小程序從終端讀數據再寫回終端。

 1 #include <unistd.h>
 2 #include <stdlib.h>
 3 #include <iostream>
 4 using namespace std;
 5 
 6 int main(void)
 7 {
 8     char buf[10]; //這個地方要註意長度為10
 9     int n;
10     n = read(STDIN_FILENO, buf, 10);
11     if (n < 0) {
12         cout << "read STDIN_FILENO" << endl;
13 return 1; 14 } 15 write(STDOUT_FILENO, buf, n); 16 return 0; 17 }

執行結果如下:

1 $ ./a.out
2 hello(回車)
3 hello
4 $ ./a.out
5 hello world(回車)
6 hello worl$ d
7 d: command not found

   第一次執行a.out的結果很正常,而第二次執行的過程有點特殊,現在分析一下
  Shell進程創建a.out進程,a.out進程開始執行,而Shell進程睡眠等待a.out進程退出。
  a.out調用read時睡眠等待直到終端設備輸入了換行符才從read返回read只讀走10個字符剩下的字符仍然保存在內核的終端設備輸入緩沖區中
  a.out進程打印並退出,這時Shell進程恢復運行,Shell繼續從終端讀取用戶輸入的命令,於是讀走了終端設備輸入緩沖區中剩下的字符d和換行符,把它當成一條命令解釋執行,結果發現執行不了,沒有d這個命令。
  如果在open一個設備時指定了O_NONBLOCK標誌,read/write就不會阻塞。以read為例,如果設備暫時沒有數據可讀就返回-1,同時置errno為EWOULDBLOCK(或者EAGAIN,這兩個宏定義的值相同),表示本來應該阻塞在這裏(would block,虛擬語氣),事實上並沒有阻塞而是直接返回錯誤,調用者應該試著再讀一次(again)。這種行為方式稱為輪詢(Poll),調用者只是查詢一下,而不是阻塞在這裏死等,這樣可以同時監視多個設備:

1 while(1) {
2     非阻塞read(設備1);
3     if(設備1有數據到達)
4         處理數據;
5     非阻塞read(設備2);
6     if(設備2有數據到達)
7         處理數據;
8 ...
9 }

  上述偽代碼中如果read(設備1)是阻塞的,那麽只要設備1沒有數據到達就會一直阻塞在設備1的read調用上,即使設備2有數據到達也不能處理,使用非阻塞I/O就可以避免設備2得不到及時處理
  但是非阻塞I/O有一個缺點如果所有設備都一直沒有數據到達,調用者需要反復查詢做無用功,如果阻塞在那裏,操作系統可以調度別的進程執行,就不會做無用功了。在使用非阻塞I/O時,通常不會在一個while循環中一直不停地查詢(這稱為Tight Loop),而是每延遲等待一會兒來查詢一下,以免做太多無用功,在延遲等待的時候可以調度其它進程執行。

 1 while(1) {
 2     非阻塞read(設備1);
 3     if(設備1有數據到達)
 4         處理數據;
 5     非阻塞read(設備2);
 6     if(設備2有數據到達)
 7         處理數據;
 8     ...
 9     sleep(n);
10 }

  這樣做的問題是,設備1有數據到達時可能不能及時處理,最長需延遲n秒才能處理,而且反復查詢還是做了很多無用功。後面博客中介紹的select( )函數可以阻塞地同時監視多個設備,還可以設定阻塞等待的超時時間,從而圓滿地解決了這個問題。
   以下是一個非阻塞I/O的例子。目前我們學過的可能引起阻塞的設備只有終端,所以我們用終端來做這個實驗。程序開始執行時在0、1、2文件描述符上自動打開的文件就是終端,但是沒有O_NONBLOCK標誌。所以就會 “阻塞讀終端”,讀標準輸入是阻塞的。我們可以重新打開一遍設備文件/dev/tty(表示當前終端),在打開時指定O_NONBLOCK標誌。

 1 #include <unistd.h>
 2 #include <fcntl.h>
 3 #include <errno.h>
 4 #include <string.h>
 5 #include <stdlib.h>
 6 
 7 #define MSG_TRY "try again\n"
 8 #define MAX 8192
 9 
10 int main(void)
11 {
12     char buf[MAX];
13     int fd, n;
14     //以只讀且非阻塞方式打開/dev/tty
15     //意思就是打開該終端(tty類似於C++中的this)
16     fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
17     if(fd<0) 
18     {//如果打開失敗就打印出錯原因
19         perror("open /dev/tty");
20         exit(1);
21     }
22 
23 tryagain:
24     //從終端中讀數據到buf中
25     n = read(fd, buf, sizeof(buf));
26     if (n < 0)//如果讀失敗 
27     {
28         //如果錯誤原因是EAGAIN
29         if (errno == EAGAIN) 
30         {//errno就是錯誤原因所對應的編號
31          //EAGAIN就是宏定義的一個錯誤原因
32             sleep(1);
33             //標準輸出(輸出到屏幕),MSG_TRY在上面的宏定義
34             write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
35             goto tryagain;
36         }
37         //如果錯誤原因不是EAGAIN
38         perror("read /dev/tty");
39         exit(1);
40     }
41     //如果讀到數據
42     write(STDOUT_FILENO, buf, n);
43     close(fd);
44     return 0;
45 }

6、阻塞和非阻塞