Linux網絡編程 - 異步 I/O 的探討

概念

第一種是阻塞 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 下高性能網絡程序設計。

 

溫故而知新 !

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章