Linux的I/O機制經曆了一下幾個階段的演化:
(1)同步阻塞I/O: 使用者進程進行I/O操作,一直阻塞到I/O操作完成為止。
(2)同步非阻塞I/O: 使用者程式可以通過設定檔案描述符的屬性O_NONBLOCK,I/O操作可以立即返回,但是並不保證I/O操作成功。
(3)非同步阻塞I/O: 使用者進程可以對I/O事件進行阻塞,但是I/O操作並不阻塞。通過select/poll/epoll等函數調用來達到此目的。
(4)非同步非阻塞I/O: 也叫做非同步I/O(AIO),使用者程式可以通過向核心發出I/O請求命令,不用等帶I/O事件真正發生,可以繼續做另外的事情,等I/O操作完成,核心會通過函數回調或者訊號機制通知使用者進程。這樣很大程度提高了系統輸送量。
1、 一般典型的I/O(同步阻塞I/O)
它的典型流程如下:
範例程式碼:while ( (n=read(STDIN_FILENO, buf, BUFSIZ) ) > 0)
if (write (STDOUT_FILENO, buf, n) != n)
err_sys (write error ”) ;
從應用程式的角度來說,read 調用可能會延續很長時間。實際上,在核心執行讀操作和其他工作時,應用程式的確會被阻塞,也就是說應用程式不能做其它事情了。
2、 同步 非阻塞I/O
它的典型流程如下:
對於一個給定的描述符有兩種方法對其指定非阻塞I / O:
(1) 如果是調用o p e n以獲得該描述符,則可指定O _ N O N B L O C K標誌。
(2) 對於已經開啟的一個描述符,則可調用f c n t l開啟O _ N O N B L O C K檔案狀態標誌。
對於非阻塞I/O,read發現沒有資料可讀,則簡單的返回-EAGAIN("try it agin"),而不是阻塞當前進程。來看一個非阻塞I/O的例子://nbtest.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
char buffer[4096];
int main(int argc, char **argv)
{
int delay = 1, n, m = 0;
if (argc > 1)
delay=atoi(argv[1]);
fcntl(0, F_SETFL, fcntl(0,F_GETFL) | O_NONBLOCK); /* stdin */
fcntl(1, F_SETFL, fcntl(1,F_GETFL) | O_NONBLOCK); /* stdout */
while (1) {
n = read(0, buffer, 4096);
if (n >= 0)
m = write(1, buffer, n);
if ((n < 0 || m < 0) && (errno != EAGAIN))
break;
sleep(delay);
}
perror(n < 0 ? "stdin" : "stdout");
exit(1);
}
我們用strace來跟蹤一下程式執行的結果:
out.txt的內容如下:
可以清楚的看到read讀取失敗的情況。實際上,該方式需要應用程式以一種輪詢的方式來實現資料讀取,多次無謂的系統調用會加大系統開銷,影響應整個系統的輸送量。
3、,非同步阻塞I/O
即UNIX環境下的I/O多路轉接(I/O multiplexing),典型流程如下:
Linux中,poll、epoll和select這三個函數可以用來實現 I/O多路轉接。它們的本質上是相同的:每個允許一個進程來決定它是否可讀或者寫一個或多個檔案而不阻塞. 這些調用也可阻塞進程直到任何一個給定集合的檔案描述符可用來讀或寫. 因此, 它們常常用在必須使用多輸入輸出資料流的應用程式。
3.1、poll函數#include <stropts.h>
#include <poll.h>
int poll(struct pollfd fdarray[],unsigned long nfds,int timeout) ;
返回:準備就緒的描述符數,若逾時則為 0,若出錯則為- 1
struct pollfd {
int fd ; /* file descriptor to check, or < 0 to ignore */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
} ;
fdarray數組中的元素數由nfds說明。
應將events成員設定為如下所示值的一個或幾個。通過這些值告訴核心我們對該描述符關心的是什麼。返回時,核心設定revents成員,以說明對該描述符發生了什麼事件。 (注意,poll沒有更改events成員)。events和revents的取值:
頭四行測試可讀性,接著三行測試可寫性,最後三行則是異常條件。最後三行是
由核心在返回時設定的。即使在 events欄位中沒有指定這三個值,如果相應條件發生,則在revents中也返回它們。當一個描述符被掛斷後(POLLUP) ,就不能再寫向該描述符。但是仍可能從該描述符讀取到資料。
poll的最後一個參數說明我們想要等待多少時間。有三種不同的情形:
• timeout == -1永遠等待。常數INFTIM定義在<stropts.h>,其值通常是-1。當所指定
的描述符中的一個已準備好,或捕捉到一個訊號則返回。如果捕捉到一個訊號,則p o l l返回-1,errno設定為EINTR。
• timeout == 0 不等待。測試所有描述符並立即返回。這是得到很多個描述符的狀態而不阻塞p o l l函數的輪詢方法。
• timeout > 0 等待timeout毫秒。當指定的描述符之一已準備好,或指定的時間值已超過時立即返回。如果已逾時但是還沒有一個描述符準備好,則傳回值是 0。 (如果系統不提供毫秒解析度,則timeout值取整到最近的支援值)。
3.2、例子#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/poll.h>
#include <fcntl.h>
char buffer[4096];
int main(int argc, char **argv)
{
struct pollfd pfd;
int n;
fcntl(0, F_SETFL, fcntl(0,F_GETFL) | O_NONBLOCK); /* stdin */
pfd.fd = 0; /* stdin */
pfd.events = POLLIN;
while (1) {
n=read(0, buffer, 4096);
if (n >= 0)
write(1, buffer, n);
n = poll(&pfd, 1, -1);
if (n < 0)
break;
}
perror( n<0 ? "stdin" : "stdout");
exit(1);
}
我們用strace來跟蹤一下程式執行的結果:
out.txt檔案:
該方式中,select(或poll)的調用仍然會阻塞進程,與一般典型的I/O不一樣的它是等待事件通知。但是它引入了逾時機制,可以讓應用程式有權力避免過長時間等待;另一方面,如果應用程式需要讀寫多個檔案,該方式可以一顯身手。典型的應用就是telnet命令(詳細見《UNIX環境進階編程》)。
3、 非同步I/O
Linux 非同步 I/O (AIO),即非同步非阻塞I/O,是 Linux 核心中提供的一個相當新的增強。它是 2.6 版本核心的一個標準特性,但是我們在 2.4 版本核心的補丁中也可以找到它。AIO 背後的基本思想是允許進程發起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍後或在接收到 I/O 操作完成的通知時,進程就可以檢索 I/O 操作的結果。
它的流程如下:
非同步I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會立即返回,說明 read 請求已經成功發起了。在後台完成讀操作時,應用程式然後會執行其他處理操作。當 read 的響應到達時,就會產生一個訊號或執行一個基於線程的回呼函數來完成這次 I/O 處理過程。
在一個進程中為了執行多個 I/O 請求而對計算操作和 I/O 處理進行重疊處理的能力利用了處理速度與 I/O 速度之間的差異。當一個或多個 I/O 請求掛起時,CPU 可以執行其他任務;或者更為常見的是,在發起其他 I/O 的同時對已經完成的 I/O 進行操作。該方式的詳細介紹見參考文獻。
主要參考:
《UNIX環境進階編程》
http://www.ibm.com/developerworks/cn/linux/l-async/