概念
第一種是阻塞 I/O。阻塞 I/O 發起的 read 請求,線程會被掛起,一直等到內核數據準備好,並把數據從內核區域拷貝到應用程序的緩衝區中,當拷貝過程完成,read 請求調用才返回。接下來,應用程序就可以對緩衝區的數據進行數據解析。
第二種是非阻塞 I/O。非阻塞的 read 請求在數據未準備好的情況下立即返回,應用程序可以不斷輪詢內核,直到數據準備好,內核將數據拷貝到應用程序緩衝,並完成這次 read 調用。注意,這裏最後一次 read 調用,獲取數據的過程,是一個同步的過程。這裏的同步指的是內核區域的數據拷貝到緩存區這個過程。
每次讓應用程序去輪詢內核的 I/O 是否準備好,是一個不經濟的做法,因爲在輪詢的過程中應用進程啥也不能幹。於是,像 select、poll 這樣的 I/O 多路複用技術就隆重登場了。通過 I/O 事件分發,當內核數據準備好時,再通知應用程序進行操作。這個做法大大改善了應用進程對 CPU 的利用率,在沒有被通知的情況下,應用進程可以使用 CPU 做其他的事情。
注意,這裏 read 調用,獲取數據的過程,也是一個同步的過程。
第一種阻塞 I/O ,應用程序會被掛起,直到獲取數據。第二種非阻塞 I/O 和第三種基於非阻塞 I/O 的多路複用技術,獲取數據的操作不會被阻塞。這裏,它們都是同步調用技術。爲什麼這麼說呢?因爲同步調用、異步調用的說法,是對於獲取數據的過程而言的,前面幾種最後獲取數據的 read 操作調用,都是同步的,在 read 調用時,內核將數據從內核空間拷貝到應用程序空間,這個過程是在 read 函數中同步進行的,如果內核實現的拷貝效率很差,read 調用就會在這個同步過程中消耗比較長的時間。
而真正的異步調用則不用擔心這個問題,我們接下來就來介紹第四種 I/O 技術,當我們發起 aio_read 之後,就立即返回,內核自動將數據從內核空間拷貝到應用程序空間,這個拷貝過程是異步的,內核自動完成的,和前面的同步操作不一樣,應用程序並不需要主動發起拷貝動作。
這裏放置了一張表格,總結了以上幾種 I/O 模型。
aio_read 和 aio_write 的用法
先上一個程序事例:
const int BUF_SIZE = 512;
int main() {
int err;
int result_size;
// 創建一個臨時文件
char tmpname[256];
snprintf(tmpname, sizeof(tmpname), "/tmp/aio_test_%d", getpid());
unlink(tmpname);
int fd = open(tmpname, O_CREAT | O_RDWR | O_EXCL, S_IRUSR | S_IWUSR);
if (fd == -1) {
error(1, errno, "open file failed ");
}
char buf[BUF_SIZE];
struct aiocb aiocb;
//初始化buf緩衝,寫入的數據應該爲0xfafa這樣的,
memset(buf, 0xfa, BUF_SIZE);
memset(&aiocb, 0, sizeof(struct aiocb));
aiocb.aio_fildes = fd;
aiocb.aio_buf = buf;
aiocb.aio_nbytes = BUF_SIZE;
//開始寫
if (aio_write(&aiocb) == -1) {
printf(" Error at aio_write(): %s\n", strerror(errno));
close(fd);
exit(1);
}
//因爲是異步的,需要判斷什麼時候寫完
while (aio_error(&aiocb) == EINPROGRESS) {
printf("writing... \n");
}
//判斷寫入的是否正確
err = aio_error(&aiocb);
result_size = aio_return(&aiocb);
if (err != 0 || result_size != BUF_SIZE) {
printf(" aio_write failed() : %s\n", strerror(err));
close(fd);
exit(1);
}
//下面準備開始讀數據
char buffer[BUF_SIZE];
struct aiocb cb;
cb.aio_nbytes = BUF_SIZE;
cb.aio_fildes = fd;
cb.aio_offset = 0;
cb.aio_buf = buffer;
// 開始讀數據
if (aio_read(&cb) == -1) {
printf(" air_read failed() : %s\n", strerror(err));
close(fd);
}
//因爲是異步的,需要判斷什麼時候讀完
while (aio_error(&cb) == EINPROGRESS) {
printf("Reading... \n");
}
// 判斷讀是否成功
int numBytes = aio_return(&cb);
if (numBytes != -1) {
printf("Success.\n");
} else {
printf("Error.\n");
}
// 清理文件句柄
close(fd);
return 0;
}
這裏,主要用到的函數有:
- aio_write:用來向內核提交異步寫操作;
- aio_read:用來向內核提交異步讀操作;
- aio_error:獲取當前異步操作的狀態;
- aio_return:獲取異步操作讀、寫的字節數。
這個程序一開始使用 aio_write 方法向內核提交了一個異步寫文件的操作。結構體 aiocb 是應用程序向操作系統內核傳遞的異步申請數據結構,這裏我們使用了文件描述符、緩衝區指針 aio_buf 以及需要寫入的字節數 aio_nbytes。
struct aiocb {
int aio_fildes; /* File descriptor */
off_t aio_offset; /* File offset */
volatile void *aio_buf; /* Location of buffer */
size_t aio_nbytes; /* Length of transfer */
int aio_reqprio; /* Request priority offset */
struct sigevent aio_sigevent; /* Signal number and value */
int aio_lio_opcode; /* Operation to be performed */
};
緊接着,我們使用了 aio_read 從文件中讀取這些數據。爲此,我們準備了一個新的 aiocb 結構體,告訴內核需要把數據拷貝到 buffer 這個緩衝區中,和異步寫一樣,發起異步讀之後一直查詢異步讀動作的結果。
接下來運行這個程序,我們看到屏幕上打印出一系列的字符,顯示了這個操作是有內核在後臺幫我們完成的。
./aio01
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
writing...
Reading...
Reading...
Reading...
Reading...
Reading...
Reading...
Reading...
Reading...
Reading...
Success.
打開 /tmp 目錄下的 aio_test_xxxx 文件,可以看到,這個文件成功寫入了我們期望的數據。
Linux 下 socket 套接字的異步支持
aio 系列函數是由 POSIX 定義的異步操作接口,可惜的是,Linux 下的 aio 操作,不是真正的操作系統級別支持的,它只是由 GNU libc 庫函數在用戶空間藉由 pthread 方式實現的,而且僅僅針對磁盤類 I/O,套接字 I/O 不支持。
也有很多 Linux 的開發者嘗試在操作系統內核中直接支持 aio,例如一個叫做 Ben LaHaise 的人,就將 aio 實現成功 merge 到 2.5.32 中,這部分能力是作爲 patch 存在的,但是,它依舊不支持套接字。
Solaris 倒是有真正的系統系別的 aio,不過還不是很確定它在套接字上的性能表現,特別是和磁盤 I/O 相比效果如何。
綜合以上結論就是,Linux 下對異步操作的支持非常有限,這也是爲什麼使用 epoll 等多路分發技術加上非阻塞 I/O 來解決 Linux 下高併發高性能網絡 I/O 問題的根本原因。
和 Linux 不同,Windows 下實現了一套完整的支持套接字的異步編程接口,這套接口一般被叫做 IOCompletetionPort(IOCP)。這樣,就產生了基於 IOCP 的所謂 Proactor 模式。無論是 Reactor 模式,還是 Proactor 模式,都是一種基於事件分發的網絡編程模式。Reactor 模式是基於待完成的 I/O 事件,而 Proactor 模式則是基於已完成的 I/O 事件,兩者的本質,都是藉由事件分發的思想,設計出可兼容、可擴展、接口友好的一套程序框架。
總之, 異步 I/O 的讀寫動作由內核自動完成,不過,在 Linux 下目前僅僅支持簡單的基於本地文件的 aio 異步操作,這也使得我們在編寫高性能網絡程序時,首選 Reactor 模式,藉助 epoll 這樣的 I/O 分發技術完成開發;而 Windows 下的 IOCP 則是一種異步 I/O 的技術,並由此產生了和 Reactor 齊名的 Proactor 模式,藉助這種模式,可以完成 Windows 下高性能網絡程序設計。
溫故而知新 !