linux aio + epoll實踐

Linux的io機制

Buffered-IO 和Direct-IO

Linux磁盤I/O分爲Buffered IO和Direct IO,這兩者有何區別呢?

對於Buffered IO:

當應用程序嘗試讀取某塊數據的時候,如果這塊數據已經存放在了頁緩存(page cache)中,那麼這塊數據就可以立即返回給應用程序,而不需要經過實際的物理讀盤操作。當然,如果數據在應用程序讀取之前並未被存放在頁緩存中,那麼就需要先將數據從磁盤讀到頁緩存中去。對於寫操作來說,應用程序也會將數據先寫到頁緩存中去,數據是否被立即寫到磁盤上去取決於應用程序所採用的寫操作機制:如果用戶採用的是同步寫機制( synchronous writes ),那麼數據會立即被寫回到磁盤上,應用程序會一直等到數據被寫完爲止;如果用戶採用的是延遲寫機制( deferred writes ),那麼應用程序就完全不需要等到數據全部被寫回到磁盤,數據只要被寫到頁緩存中去就可以了。在延遲寫機制的情況下,操作系統會定期地將放在頁緩存中的數據刷到磁盤上。與異步寫機制( asynchronous writes )不同的是,延遲寫機制在數據完全寫到磁盤上的時候不會通知應用程序,而異步寫機制在數據完全寫到磁盤上的時候是會返回給應用程序的。所以延遲寫機制本身是存在數據丟失的風險的,而異步寫機制則不會有這方面的擔心。

總結下,Buffered IO的特點是使用了內存緩存,如:

讀操作:硬盤->內核頁緩存->用戶緩衝區
寫操作:用戶緩衝區->內核頁緩存->硬盤
對Buffered IO,數據在傳輸過程中需要在應用程序地址空間和頁緩存之間進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

對於某些特殊的應用程序(如數據庫)來說,避開操作系統內核緩衝區而直接在應用程序地址空間和磁盤之間傳輸數據會比使用操作系統內核緩衝區獲取更好的性能。

Direct-io的目的在於繞過文件系統(ext)的cache,直接對block設備上的文件進行讀寫。但不經內核緩衝區,直接寫磁盤,必然會引起阻塞。所以通常DIRECT-io與AIO(異步IO)會一起出現。

阻塞模式的IO過程如下:

int fd = open(const char *pathname, int flags, mode_t mode);
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
int close(int fd);
因爲整個過程會等待read/write的返回,所以不需要任何額外的數據結構。但異步IO的思想是:應用程序不能阻塞在昂貴的系統調用上讓CPU睡大覺,而是將IO操作抽象成一個個的任務單元提交給內核,內核完成IO任務後將結果放在應用程序可以取到的地方。這樣在底層做I/O的這段時間內,CPU可以去幹其他的計算任務。但異步的IO任務批量的提交和完成,必須有自身可描述的結構,最重要的兩個就是iocb和io_event。

libaio中的structs:

複製代碼

struct iocb {        // 描述IO請求

        void     *data;  /* Return in the io completion event */
        unsigned key;   /*r use in identifying io requests */
        short           aio_lio_opcode;  // 操作的類型:IO_CMD_PWRITE | IO_CMD_PREAD
        short           aio_reqprio;
        int             aio_fildes;    // 操作的文件fd
        union {
                struct io_iocb_common           c;
                struct io_iocb_vector           v;
                struct io_iocb_poll             poll;
                struct io_iocb_sockaddr saddr;
        } u;
};

struct io_iocb_common {  
        void            *buf;  
        unsigned long   nbytes;
        long long       offset;
        unsigned        flags;
        unsigned        resfd;
};

  struct io_event {    // 描述返回結果

    void *data;

    struct iocb *obj;  // 提交的任務

    unsigned long res;  // IO任務完成的狀態

   unsigned long res2;  // 同上
};

libaio提供的API

libaio提供的API有:io_setup, io_submit, io_getevents, io_destroy。

1. 建立IO任務

int io_setup (int maxevents, io_context_t *ctxp);
io_context_t對應內核中一個結構,爲異步IO請求提供上下文環境。注意在setup前必須將io_context_t初始化爲0。

當然,這裏也需要open需要操作的文件,注意設置O_DIRECT標誌。

2.提交IO任務

long io_submit (aio_context_t ctx_id, long nr, struct iocb **iocbpp);

提交任務之前必須先填充iocb結構體,libaio提供的包裝函數說明了需要完成的工作:

void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset)
{
  memset(iocb, 0, sizeof(*iocb));
  iocb->aio_fildes = fd;
  iocb->aio_lio_opcode = IO_CMD_PREAD;
  iocb->aio_reqprio = 0;
  iocb->u.c.buf = buf;
  iocb->u.c.nbytes = count;
  iocb->u.c.offset = offset;
}

void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset)
{
  memset(iocb, 0, sizeof(*iocb));
  iocb->aio_fildes = fd;
  iocb->aio_lio_opcode = IO_CMD_PWRITE;
  iocb->aio_reqprio = 0;
  iocb->u.c.buf = buf;
  iocb->u.c.nbytes = count;
  iocb->u.c.offset = offset;
}

這裏注意讀寫的buf都必須是按扇區對齊的,可以用posix_memalign來分配。

3.獲取完成的IO

long io_getevents (aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);
這裏最重要的就是提供一個io_event數組給內核來copy完成的IO請求到這裏,數組的大小是io_setup時指定的maxevents。

timeout是指等待IO完成的超時時間,設置爲NULL表示一直等待所有到IO的完成。

4.銷燬IO任務

int io_destroy (io_context_t ctx);

libaio和epoll的結合

在異步編程中,任何一個環節的阻塞都會導致整個程序的阻塞,所以一定要避免在io_getevents調用時阻塞式的等待。還記得io_iocb_common中的flags和resfd嗎?看看libaio是如何提供io_getevents和事件循環的結合:

void io_set_eventfd(struct iocb *iocb, int eventfd)

{
    iocb->u.c.flags |= (1 << 0) /* IOCB_FLAG_RESFD */;
    iocb->u.c.resfd = eventfd;
}

這裏的resfd是通過系統調用eventfd生成的。

int eventfd(unsigned int initval, int flags);
eventfd是linux 2.6.22內核之後加進來的syscall,作用是內核用來通知應用程序發生的事件的數量,從而使應用程序不用頻繁地去輪詢內核是否有時間發生,而是有內核將發生事件的數量寫入到該fd,應用程序發現fd可讀後,從fd讀取該數值,並馬上去內核讀取。

有了eventfd,就可以很好地將libaio和epoll事件循環結合起來:

  1. 創建一個eventfd

efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

  1. 將eventfd設置到iocb中

io_set_eventfd(iocb, efd);

  1. 交接AIO請求

io_submit(ctx, NUM_EVENTS, iocb);

  1. 創建一個epollfd,並將eventfd加到epoll中

epfd = epoll_create(1);

epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &epevent);

epoll_wait(epfd, &epevent, 1, -1);

  1. 當eventfd可讀時,從eventfd讀出完成IO請求的數量,並調用io_getevents獲取這些IO

read(efd, &finished_aio, sizeof(finished_aio);

r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms);

一個epoll/aio/eventfd結合使用的簡單例子:

#define _GNU_SOURCE
#define __STDC_FORMAT_MACROS

#include <stdio.h>
#include <errno.h>
#include <libaio.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <inttypes.h>

#define TEST_FILE   "aio_test_file"
#define TEST_FILE_SIZE  (127 * 1024)
#define NUM_EVENTS  128
#define ALIGN_SIZE  512
#define RD_WR_SIZE  1024

struct custom_iocb
{
    struct iocb iocb;
    int nth_request;
};

void aio_callback(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
    struct custom_iocb *iocbp = (struct custom_iocb *)iocb;
    printf("nth_request: %d, request_type: %s, offset: %lld, length: %lu, res: %ld, res2: %ld\n", 
            iocbp->nth_request, (iocb->aio_lio_opcode == IO_CMD_PREAD) ? "READ" : "WRITE",
            iocb->u.c.offset, iocb->u.c.nbytes, res, res2);
}

int main(int argc, char *argv[])
{
    int efd, fd, epfd;
    io_context_t ctx;
    struct timespec tms;
    struct io_event events[NUM_EVENTS];
    struct custom_iocb iocbs[NUM_EVENTS];
    struct iocb *iocbps[NUM_EVENTS];
    struct custom_iocb *iocbp;
    int i, j, r;
    void *buf;
    struct epoll_event epevent;

    efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    if (efd == -1) {
        perror("eventfd");
        return 2;
    }

    fd = open(TEST_FILE, O_RDWR | O_CREAT | O_DIRECT, 0644);
    if (fd == -1) {
        perror("open");
        return 3;
    }
    ftruncate(fd, TEST_FILE_SIZE);
    
    ctx = 0;
    if (io_setup(8192, &ctx)) {
        perror("io_setup");
        return 4;
    }

    if (posix_memalign(&buf, ALIGN_SIZE, RD_WR_SIZE)) {
        perror("posix_memalign");
        return 5;
    }
    printf("buf: %p\n", buf);

    for (i = 0, iocbp = iocbs; i < NUM_EVENTS; ++i, ++iocbp) {
        iocbps[i] = &iocbp->iocb;
        io_prep_pread(&iocbp->iocb, fd, buf, RD_WR_SIZE, i * RD_WR_SIZE);
        io_set_eventfd(&iocbp->iocb, efd);
        io_set_callback(&iocbp->iocb, aio_callback);
        iocbp->nth_request = i + 1;
    }

    if (io_submit(ctx, NUM_EVENTS, iocbps) != NUM_EVENTS) {
        perror("io_submit");
        return 6;
    }

    epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create");
        return 7;
    }

    epevent.events = EPOLLIN | EPOLLET;
    epevent.data.ptr = NULL;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &epevent)) {
        perror("epoll_ctl");
        return 8;
    }

    i = 0;
    while (i < NUM_EVENTS) {
        uint64_t finished_aio;

        if (epoll_wait(epfd, &epevent, 1, -1) != 1) {
            perror("epoll_wait");
            return 9;
        }

        if (read(efd, &finished_aio, sizeof(finished_aio)) != sizeof(finished_aio)) {
            perror("read");
            return 10;
        }

        printf("finished io number: %"PRIu64"\n", finished_aio);
    
        while (finished_aio > 0) {
            tms.tv_sec = 0;
            tms.tv_nsec = 0;
            r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms);
            if (r > 0) {
                for (j = 0; j < r; ++j) {
                    ((io_callback_t)(events[j].data))(ctx, events[j].obj, events[j].res, events[j].res2);
                }
                i += r;
                finished_aio -= r;
            }
        }
    }
    
    close(epfd);
    free(buf);
    io_destroy(ctx);
    close(fd);
    close(efd);
    remove(TEST_FILE);

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