Nginx基礎. eventfd, 異步IO 與epoll的相互協作

關於eventfd.
對於eventfd, 這裏只是簡單的講它的功能. 看manpage就足夠了, 其中的例子也很容易看懂
eventfd函數可以創建一個efd描述符, 該描述符在內核中維護着一個計數器counter.
在調用eventfd時, 可以傳入參數指定內核中維護着的計數器的值.
如果這樣調用:
      int efd = eventfd(0, 0);
那麼計數器值爲0.
如果此時調用:
      read(efd, &ret, sizeof(ret)); 
那麼會休眠. 直到有人調用:
      int add = 1;
      write(efd, &add, sizeof(add))
這時候上面的read纔會停止休眠, 得到的ret值爲1. 如果write寫入的add值不是1, 而是5
那麼read函數得到的ret值也會是5. read之後, efd對應的內核中的計數器值就回到了0, 此時再次調用:
      read(efd, &ret, sizeof(ret)); 
那麼進程又會阻塞. 除非, efd被你設置成了非阻塞... 會返回EAGAIN錯誤.



關於異步IO
這裏將認識異步IO, 以及epoll是如何與異步IO相互協作的.
在linux下, 有兩種異步IO. 一種是由glibc實現的aio, 它是直接在用戶空間用pthread進行模擬的僞異步IO; 還有一種是內核實現的aio. 相關的系統調用是以 io_xxx 形式出現的.
在nginx中, 採用的是內核實現的aio, 只有在內核中成功完成了磁盤操作, 內核纔會通知進程, 進而使磁盤文件的處理與網絡事件的處理同樣高效.
內核提供的aio的優點是, 能夠同時提交多個io請求給內核, 當大量事件堆積到IO設備的隊列中時, 內核將發揮出io調度算法的優勢, 對到來的事件處理進行優化, 合併等.
這種內核級別的aio缺點也是很明顯的, 它是不支持緩存操作的, 即使需要操作的文件塊在linux文件緩存中已經存在, 也不會通過讀取緩存來代替實際對磁盤的操作, 所以儘管相對於阻塞進程來說有了很大的好轉, 但對於單個請求來說, 還是有可能降低實際的處理效率. 因爲本可以從緩存中讀的數據在使用異步IO後一定會從磁盤讀取.
所以異步IO並不是完美的. 如果大部分用戶請求對文件的操作都會落到文件緩存中, 那麼放棄異步IO可能是更好的選擇.
目前, Nginx僅支持讀文件的異步IO, 因爲正常寫入文件往往是寫入內存就返回, 相比於異步IO效率明顯提高了.


異步IO方法
在理解異步IO方法時, 爲了更好的理解這些方法, 與epoll相關方法做比較則更容易理解.
int io_setup(unsigned nr_events, aio_context_t *ctxp);
        初始化異步IO上下文. 與epoll相比, 類似epoll_create. nr_events表示異步IO上下文可以處理事件的最小個數
        通過ctxp獲得的上下文描述符與epoll_create返回的描述符類似, 貫穿始終.
int io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);
        提交文件異步IO操作. iocbpp是提交的事件數組的首個元素地址, nr是提交的事件個數
        返回值表示成功提交的事件個數. 類似epoll中的 epoll_ctl + EPOLL_CTL_ADD
int io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result);
        取消某個IO操作
        類似epoll中的epoll_ctl + EPOLL_CTL_DEL
int io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);
        從已完成的文件異步IO操作隊列中讀取操作
        其中, min_nr表示至少獲取min_nr個事件; nr表示至多獲取nr個事件; events是IO操作完成的事件數組
        類似epoll中的epoll_wait
int io_destroy(aio_context_t ctx);
        銷燬異步IO上下文.

對於一個進程來說, 通常一個異步IO就可以了, 因爲一個異步IO可以同時包含很多異步IO操作.
在調用io_setup初始化完成異步IO上下文後, 將會使用io_submit提交異步IO操作.
其中的struct iocb結構體如下:
struct iocb {
        //存儲業務需要的指針. 在Nginx中, 這個字段通常存儲對應ngx_event_t對象的指針.
        //它實際上與io_getevents中返回的io_event結構體的data成員是完全一致的.
      uint64_t aio_data;
        //操作碼.  比如IO_CMD_PREAD指定異步讀操作;  IO_CMD_PWRITE異步寫操作;  IO_CMD_FSYNC強制同步等...
      uint16_t aio_lio_opcode;
        //請求的優先級
      int16_t aio_reqprio; 
        //文件描述符
      uint32_t aio_fields;
        //讀寫操作對應的用戶態緩衝區.
      uint64_t aio_buf;
        //讀寫操作的字節長度
      uint64_t aio_nbytes;
      uint64_t aio_offset;
        //表示可以設置爲IOCB_FLAG_RESFD, 它會告訴內核當有異步IO請求處理完成時使用eventfd進行通知
        //可與epoll配合使用. 下面會涉及到
      uint32_t aio_flags;
        //表示當使用IOCB_FLAG_RESFD標誌時, 用於進行事件通知的句柄
      uint64_t aio_resfd;

        ...
};
在設置好iocb結構體後, 就可以向異步IO提交事件了. 也可以使用io_cancel將已經提交的事件取消.
那麼在異步IO操作完成後, 如何使用io_getevents獲取已完成的事件呢?
其中io_event結構體如下:
struct io_event {
        //與io_submit中的iocb結構體的aio_data是一致的
      uint64_t data;
        //指向提交事件時候帶入的iocb結構體
      uint64_t obj;
        //大於或等於0表示成功, 表示讀取到的字節數; 否則失敗
      int64_t res;
        ...
};
根據獲取到的io_evnet結構體, 就可以得到已完成的異步IO事件了.
要重點注意的是io_event的data和iocb中的aio_data可以用於傳遞指針, 所以業務中的數據結構,完成後的回調方法都放在這裏.
到這裏, 對於異步IO使用的數據結構以及大致使用流程有了初步瞭解, 下面就配合nginx中的epoll模塊來詳細分析.


如何使用異步IO
接下來, 將會與Nginx中的epoll模塊來分析異步IO的使用實例.
(下面的代碼部分均取自 xxx/nginx/src/event/module/ngx_epoll_module.c)
之前, 在分析epoll模塊的初始化方法ngx_epoll_init中, 有這麼一段代碼:
#if (NGX_HAVE_FILE_AIO)
        ngx_epoll_aio_init(cycle, epcf);
#endif
所以, 在epoll啓動前, 對於異步IO的初始化工作已經在ngx_epoll_aio_init方法中完成了.
在分析異步IO初始化方法前, 我們可以對其初始化工作做一個猜測, 調用io_setup是必須的, 其他還有什麼工作呢?
既然是與epoll模塊相互協作的, 那麼可以想象, 異步IO與epoll模塊之間的聯繫必須在這裏處理好. 這裏存在的問題就是epoll與異步IO是如何協作的呢? 異步IO事件完成後如何通知epoll呢? 以及epoll在得到處理完成的異步IO事件後, 如何將其放回業務邏輯中呢?

如果對libevent中的信號集中處理機制有所瞭解的話, 那麼這裏第二個問題似乎就明朗了.
libevent中的信號集中處理是什麼呢? 我們知道, 信號總是來的很突然, 以及因爲信號的特殊, 信號處理函數的內容不宜過多, 所以在libevent中, 每個信號真正要做的事情就不被放在信號處理函數中完成. 那麼如何完成信號要真正處理的事件呢? 既然libevent是事件驅動框架, 那麼就將每個信號的到來看作一個事件, 將信號與epoll利用管道相聯繫. 每個需要處理的信號在發生後, 其信號處理函數都只是簡單的向管道發送數據(數據往往是每個信號的整型值), 這樣, epoll在檢查管道中的數據時就會得知某信號發生了, 之後就調用該信號對應的真正的處理函數進行處理. 所以, 管道一端描述符在epoll中註冊的事件處理函數的主要工作就是, 讀取管道中的內容, 根據不同的信號調用不同的處理函數.

接下來回到Nginx中的異步IO與epoll, 先看代碼:
static void
ngx_epoll_aio_init(ngx_cycle_t *cycle, ngx_epoll_conf_t *epcf)
{
    int                 n;
    struct epoll_event  ee;
     //這裏設置evendfd中的counter初始值爲0
    ngx_eventfd = eventfd(0, 0);

    n = 1;
     //設置eventfd爲非阻塞
    if (ioctl(ngx_eventfd, FIONBIO, &n) == -1) {
    }
     //調用io_setup初始化異步IO事件的上下文
    if (io_setup(epcf->aio_requests, &ngx_aio_ctx) == -1) {
    }

     //可以看到, 這裏的eventfd對應於libevent中的信號處理機制的管道
     //在被觸發後的任務主要是 1. 得到已完成的異步IO事件   2. 根據不同的事件調用不同的處理函數
     //所以在分析完初始化函數後, 我們會立即去分析ngx_epoll_eventfd_handler方法來驗證我們的猜想.
     //初始化eventfd的事件, 以及其對應的連接結構體
    ngx_eventfd_event.data = &ngx_eventfd_conn;
    ngx_eventfd_event.handler = ngx_epoll_eventfd_handler;
    ngx_eventfd_event.log = cycle->log;
    ngx_eventfd_event.active = 1;
    ngx_eventfd_conn.fd = ngx_eventfd;
    ngx_eventfd_conn.read = &ngx_eventfd_event;
    ngx_eventfd_conn.log = cycle->log;

    ee.events = EPOLLIN|EPOLLET;
    ee.data.ptr = &ngx_eventfd_conn;
     //將eventfd註冊到epoll中, 作爲異步IO完成的通知描述符
    if (epoll_ctl(ep, EPOLL_CTL_ADD, ngx_eventfd, &ee) != -1) {
        return;
    }
     ...   //以下爲調用失敗後的處理
}
執行完了初始化函數後, 以後在添加異步IO操作同時, 我們會在iocb結構體中聲明eventfd作爲通知描述符. 這樣一來, 當某個異步IO事件完成後, ngx_eventfd句柄就處於可用狀態了. 接下來就會調用該句柄對應的ngx_epoll_eventfd_handler方法處理這些完成的異步IO操作.
下面就分析ngx_epoll_eventfd_handler方法的主要內容:
static void
ngx_epoll_eventfd_handler(ngx_event_t *ev)
{
    int               n, events;
    long              i;
    uint64_t          ready;
    ngx_err_t         err;
    ngx_event_t      *e;
    ngx_event_aio_t  *aio;
    struct io_event   event[64];
    struct timespec   ts;
     //通過ready得到已經完成的異步IO事件個數. 
     //注意的是ready的大小不受限制, 有多少完成了就會有多少返回出來
    n = read(ngx_eventfd, &ready, 8);

     ...

    ts.tv_sec = 0;
    ts.tv_nsec = 0;

     //因爲io_getevents函數會指定取出事件的個數, 所以可能不能一次全取出
     //如果還有沒有取出的, 則循環取出
    while (ready) {

        events = io_getevents(ngx_aio_ctx, 1, 64, event, &ts);

        if (events > 0) {
                //如果ready等於0了, 表示全部取完了
            ready -= events;
                //處理每一個已完成的IO異步事件
            for (i = 0; i < events; i++) {
                    //在瞭解結構體的時候, 已經知道data成員其實就是ngx_event_t結構體
                e = (ngx_event_t *) (uintptr_t) event[i].data;

                e->complete = 1;
                e->active = 0;
                e->ready = 1;
                     //可以看到, 在異步IO事件中, ngx_event_t結構體的data成員存儲的是aio結構體
                aio = e->data;
                aio->res = event[i].res;
                    //將事件取出後放到post_events中延後處理
                ngx_post_event(e, &ngx_posted_events);
            }

            continue;
        }
        ...
    }
}
可以看到, 之前我們的猜測是正確的, eventfd的處理函數主要就是對不同的事件調用其不同的處理函數.
只是這裏將處理函數調用的時機延後了.
所以, 整個網絡事件的驅動機制就是這樣通過ngx_evnetfd通知描述符和ngx_epoll_eventfd_handler回調方法, 並與文件異步IO事件結合起來的
當我們在分析eventfd的處理函數的同時, 我們可能對其中ngx_event_t對象的data成員存儲ngx_event_aio_t結構體對象感到不解, 所以下面對異步IO事件提交進行分析:
(對於其中的file結構體尚不做解釋, 暫時可以不理解)
ssize_t
ngx_file_aio_read(ngx_file_t *file, u_char *buf, size_t size, off_t offset,
    ngx_pool_t *pool)
{
    ngx_err_t         err;
    struct iocb      *piocb[1];
    ngx_event_t      *ev;
    ngx_event_aio_t  *aio;

    aio = file->aio;
     //得到當前要操作的事件對象
    ev = &aio->event;

     ......
     //想要提交異步IO事件, 首先就需要初始化iocb結構體
    ngx_memzero(&aio->aiocb, sizeof(struct iocb));
     //之前說過, iocb結構體中的aio_data對象與使用io_getevents得到的io_event結構體中的data是一致的.
     //所以這個指針指向的事件對象會被傳遞
     //所以在獲取完成的異步事件後通過io_event結構體的data成員得到的指針也是指向這個ev的
    aio->aiocb.aio_data = (uint64_t) (uintptr_t) ev;
    aio->aiocb.aio_lio_opcode = IOCB_CMD_PREAD;
    aio->aiocb.aio_fildes = file->fd;
    aio->aiocb.aio_buf = (uint64_t) (uintptr_t) buf;
    aio->aiocb.aio_nbytes = size;
    aio->aiocb.aio_offset = offset;
     //解釋結構體期間就對這個標誌說明過, 這是異步事件與eventd和epoll相關聯的實現方式
    aio->aiocb.aio_flags = IOCB_FLAG_RESFD;
     //對應的eventfd就是在epoll中註冊過的eventfd
    aio->aiocb.aio_resfd = ngx_eventfd;
     //異步事件完成後, 此事件會被調用下面這個回調方法
    ev->handler = ngx_file_aio_event_handler;

    piocb[0] = &aio->aiocb;

    if (io_submit(ngx_aio_ctx, 1, piocb) == 1) {
        ev->active = 1;
        ev->ready = 0;
        ev->complete = 0;

        return NGX_AGAIN;
    }

    ...
}
雖然我們不瞭解ngx_event_aio_t結構體, 但從上面函數來看, 它提供了對iocb結構體的包裝以及其他一些必要的信息.
看上面這個函數, 唯一的疑惑點在於ngx_file_aio_event_handler方法, 它幹了什麼呢? 按照我們的理解, 此函數應該是異步事件完成後, 真正處理邏輯的方法
但它的名字看上去卻不像, 仔細看其源碼:
static void
ngx_file_aio_event_handler(ngx_event_t *ev)
{
    ngx_event_aio_t  *aio;

    aio = ev->data;

    aio->handler(ev);
}
原來, 真正在異步IO事件結束後進行邏輯處理的函數被放在了ngx_event_aio_t結構體中.
那麼索性看一下ngx_event_aio_t結構體內容:
struct ngx_event_aio_s {
    void                      *data;
     //異步IO結束後真正用來處理邏輯的回調函數
    ngx_event_handler_pt       handler;
 
     ...

    ngx_aiocb_t                aiocb;
    ngx_event_t                event;
};
可以想象的是, 如果某個消費者模塊需要處理異步IO事件, 那麼其應該會在ngx_event_aio_s結構體中設置一個回調函數來處理自己的業務邏輯
好了, 到這裏, 無論是添加異步IO事件, 將異步IO事件與epoll相結合還是異步IO事件完成後的回調方法, 這裏都有詳細的介紹了.



自己實現:
在自己進行異步IO編程的時候, 發現Nginx中的struct iocb成員可能已經被Nginx修改過別名了, 所以下面纔是真正的struct iocb結構體
struct iocb {
        void *data; /* Return in the io completion event */
        unsigned key; /*r use in identifying io requests */
        short aio_lio_opcode;
        short aio_reqprio;
        int aio_fildes;

        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;
};

在對iocb結構體進行初始化時, 系統提供了一些函數
//設置讀寫類型的iocb
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
//下面兩個暫時不知道..
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset);
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset);
//設置異步IO事件完成後的函數句柄, 注意, 這裏函數是被掛在iocb結構體的data成員中的, 所以如果有數據要放在data上, 那就把回調函數放到自己的結構體掛在data上
void io_set_callback(struct iocb *iocb, io_callback_t cb);
對於設置的函數, linux給出的原型如下:
      void callback_function(io_context_t ctx, struct iocb *iocb, long res, long res2);
        參數: 上下文.  iocb結構體.  讀取到的字節數.  不清楚..

將異步IO與epoll結合的具體流程如下:

1. 創建一個eventfd
        efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
2. 將eventfd設置到iocb中
        io_set_eventfd(iocb, efd);
3. 交接AIO請求
        io_submit(ctx, NUM_EVENTS, iocb);
4. 創建一個epollfd,並將eventfd加到epoll中
        epfd = epoll_create(1);
        epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &epevent);
        epoll_wait(epfd, &epevent, 1, -1);
5. 當eventfd可讀時,從eventfd讀出完成IO請求的數量,並調用io_getevents獲取這些IO
        read(efd, &finished_aio, sizeof(finished_aio);
        r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms);

實例:
以下是我個人編寫的例子, 這裏過程很直白, 而不是將每個部分用函數分割開來, 是爲了理解起來更順暢 :
(此文件名爲 as.c)
#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>

void cf(io_context_t ctx, struct iocb *iocb, long res, long res2);

int main(int ac, char *av[])
{
    int epfd;                //epoll fd
    int evfd;                //eventfd fd
    int fd;                    //file fd
    io_context_t ctx;             //異步IO fd
    char filename[20];            //要異步IO處理的文件
    void *buf;                //異步IO讀取出來的內容存放地
    struct epoll_event ev_event;        //evfd 存入 epoll 的 epoll_event結構
    struct epoll_event *events_list;    //epoll_wait用
    struct iocb cb;                //一個異步IO讀取文件事件
    struct iocb *pcbs[1];            //io_submit參數, 包含上面的cb
    struct timespec ts;            //io_getevents定時器
    int n;
    uint64_t ready;                //已完成的異步IO事件
    struct io_event *events_ret;        //io_getevents參數

    if(ac != 2){
        fprintf(stderr, "%s [pathname]\n", av[0]);
        exit(2);
    }

    //初始化epoll
    epfd = epoll_create(10);
    if(epfd < 0){
        perror("epoll_create");
        exit(4);
    }

    //初始化eventfd, 並將其描述符加入epoll
    //eventfd對應事件直接在此main中寫出
    evfd = eventfd(0, 0);
    if(evfd < 0){
        perror("eventfd");
        exit(5);
    }
    ev_event.events = EPOLLIN | EPOLLET;
    ev_event.data.ptr = NULL;
    if(epoll_ctl(epfd, EPOLL_CTL_ADD, evfd, &ev_event) != 0){
        perror("epoll_ctl");
        exit(6);
    }

    memset(filename, '\0', 20);
    strncpy(filename, av[1], 19);
    //初始化異步IO上下文
    ctx = 0;
    if(io_setup(1024, &ctx) != 0){
        perror("io_setup");
        exit(3);
    }

    //添加異步IO事件
    fd = open(filename, O_RDONLY | O_CREAT, 0644);
    if(fd < 0){
        perror("open");
        exit(7);
    }
    posix_memalign(&buf, 512, 1024);
    memset(&cb, '\0', sizeof(struct iocb));
    io_prep_pread(&cb, fd, buf, 1024, 0);
    io_set_eventfd(&cb, evfd);
    io_set_callback(&cb, cf);
    pcbs[0] = &cb;
    if(io_submit(ctx, 1, pcbs) != 1){
        perror("io_submit");
        exit(8);
    }

    //調用epoll_wait等待異步IO事件完成
    events_list = (struct epoll_event *)malloc(sizeof(struct epoll_event) * 32);
    while(1){
        n = epoll_wait(epfd, events_list, 32, -1);
        if(n <= 0){
            if(errno != EINTR){
                perror("epoll_wait");
                exit(9);
            }
        }
        else
            break;
    }

    //讀取已完成的異步IO事件數量
    n = read(evfd, &ready, sizeof(ready));
    if(n != 8){
        perror("read error");
        exit(10);
    }

    //取出完成的異步IO事件並處理
    ts.tv_sec = 0;
    ts.tv_nsec = 0;
    events_ret = (struct io_event *)malloc(sizeof(struct io_event) * 32);
    n = io_getevents(ctx, 1, 32, events_ret, &ts);

    printf("log: %d events are ready;  get %d events\n",ready, n);

    ((io_callback_t)(events_ret[0].data))(ctx, events_ret[0].obj, events_ret[0].res, events_ret[0].res2);

    //收尾
    io_destroy(ctx);
    free(buf);
    close(epfd);
    close(fd);
    close(evfd);
    return 0;
}

void cf(io_context_t ctx, struct iocb *iocb, long res, long res2){
    printf("can read %d bytes, and in fact has read %d bytes\n", iocb->u.c.nbytes, res);
    printf("the content is :\n%s", iocb->u.c.buf);
}


結果:
$ gcc -o as as.c
$ ./a3 test.txt 
log: 1 events are ready;  get 1 events
can read 1024 bytes, and in fact has read 61 bytes
the content is :
qqqqq
qqqqq
qqqqq
qqqqq
fewfqqqqq
fwegw
gwegmewrgjewgnwgnnwj


其他具體的代碼, 參考他人博客...

        http://blog.sina.com.cn/s/blog_6b19f21d0100znza.html
        http://blog.csdn.net/heyutao007/article/details/7065166

主要參考:
      http://www.kuqin.com/linux/20120908/330333.html
        深入理解Nginx
     
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章