天天看點

淺析Linux Native AIO的實作

前段時間在自研的基于iSCSI的SAN 上跑mysql,CPU的iowait很大,後面改用Native AIO,有了非常大的改觀。這裡簡單總結一下Native AIO的實作。對于以IO為最大瓶頸的資料庫,native AIO幾乎不二的選擇,僅僅依靠多線程,顯然無法解決磁盤和網絡的問題。

1 API 與data struct

AIO的主要接口:

System call Description
io_setup( ) Initializes an asynchronous context for the current process
io_submit( ) Submits one or more asynchronous I/O operations
io_getevents( ) Gets the completion status of some outstanding asynchronous I/O operations
io_cancel( ) Cancels an outstanding I/O operation
io_destroy( ) Removes an asynchronous context for the current process

1.1 AIO上下文

使用AIO的第一步就是建立AIO上下文,AIO上下文用于跟蹤程序請求的異步IO的運作情況。AIO上下文在使用者空間對應資料結果aio_context_t:

//linux/aio_abi.h

typedef unsigned long    aio_context_t;

//建立AIO上下文

int io_setup(unsigned nr_events, aio_context_t *ctxp);

Io_setup建立接收nr_events事件的AIO上下文。

kioctx:

AIO上下文在核心空間對應資料結構kioctx,它儲存異步IO的所有資訊:

//AIO環境

struct kioctx {

    atomic_t      users;

    int        dead;

    struct mm_struct  *mm;

    /* This needs improving */

    unsigned long     user_id; //ring_info.mmap_base,AIO環的起始位址

    struct kioctx     *next; //下一個aio環境

    wait_queue_head_t wait; //等待程序隊列

    spinlock_t    ctx_lock;

    int        reqs_active;

    struct list_head  active_reqs;  /* used for cancellation */

    struct list_head  run_list;  /* used for kicked reqs,正在運作的IO請求連結清單 */

    unsigned      max_reqs;//異步IO操作的最大數量

    struct aio_ring_info ring_info; //AIO Ring

    struct work_struct   wq;

};

一個程序可以建立多個AIO上下文,這些AIO上下文構成一個單向連結清單。

struct mm_struct {

...

/* aio bits */

    rwlock_t      ioctx_list_lock;

    struct kioctx     *ioctx_list; //程序的AIO上下文連結清單

    struct kioctx     default_kioctx;

}

AIO Ring

AIO上下文kioctx對象包含一個重要的資料結構AIO Ring:

//aio.h

//AIO環

#define AIO_RING_PAGES   8

struct aio_ring_info {

    unsigned long     mmap_base; //AIO ring使用者态起始位址

    unsigned long     mmap_size; //緩沖區長度

    struct page       **ring_pages;//AIO環頁框指針數組

    spinlock_t    ring_lock;

    long          nr_pages;

    unsigned      nr, tail;

    struct page       *internal_pages[AIO_RING_PAGES];

};

AIO Ring對應使用者态程序位址空間的一段記憶體緩存區,使用者态程序可以通路,核心也可通路。實際上,核心先調用kmalloc函數配置設定一些頁框,然後通過do_mmap映射到使用者态位址空間,詳細請參考aio_setup_ring函數。

AIO Ring是一個環形緩沖區,核心用它來報告異步IO的完成情況,使用者态程序也可以直接檢查異步IO完成情況,進而避免系統調用的開銷。

AIO結構很簡單:aio_ring + io_event數組:

struct aio_ring {

    unsigned   id; /* kernel internal index number */

    unsigned   nr; /* number of io_events */

    unsigned   head;

    unsigned   tail;

    unsigned   magic;

    unsigned   compat_features;

    unsigned   incompat_features;

    unsigned   header_length;    /* size of aio_ring */

    struct io_event      io_events[0];

}; /* 128 bytes + ring size */

系統調用io_setup有2個參數:(1) nr_events确認最大的異步IO請求數,這将确定AIO Ring大小,即io_event數量;(2) ctxp:AIO上下文句柄的指針,實際上也是AIO Ring的起始位址aio_ring_info.mmap_base,參見函數aio_setup_ring。

1.2 送出IO請求

想要進行異步IO,需要通過系統調用io_submit送出異步IO請求。

//送出異步IO請求/aio.c

asmlinkage long sys_io_submit(aio_context_t ctx_id, long nr,

                 struct iocb __user * __user *iocbpp)

參數:

(1)ctx_id:AIO上下文句柄,核心通過它查找對應的kioctx對象;

(2)iocb數組,每個iocb描述一個異步IO請求;

(3)nr:iocb數組的大小。

iocb

//使用者态異步IO請求描述符/aio_abi.h

struct iocb {

    /* these are internal to the kernel/libc. */

    __u64  aio_data;  /* data是留給用來自定義的指針:可以設定為IO完成後的callback函數 */

    __u32  PADDED(aio_key, aio_reserved1);

              /* the kernel sets aio_key to the req # */

    /* common fields */

    __u16  aio_lio_opcode;   /* see IOCB_CMD_ above,操作的類型:IO_CMD_PWRITE | IO_CMD_PREAD */

    __s16  aio_reqprio;

    __u32  aio_fildes; //IO操作的檔案描述符

    __u64  aio_buf; //IO的buffer

    __u64  aio_nbytes; //IO請求位元組數

    __s64  aio_offset;//偏移

    /* extra parameters */

    __u64  aio_reserved2;    /* TODO: use this for a (struct sigevent *) */

    __u64  aio_reserved3;

}; /* 64 bytes */

資料結構iocb用來描述使用者空間的異步IO請求,對應的核心資料結構為kiocb。

io_submit的流程:

淺析Linux Native AIO的實作

函數io_submit_one對每個iocb配置設定一個kiocb對象,加入到AIO上下文kioctx的IO請求隊列run_list;然後調用aio_run_iocb發起IO操作,它實際上調用kiocb的ki_retry方法(aio_pread/aio_pwrite)。

如果ki_retry方法傳回-EIOCBRETRY,表明異步IO請求已經送出,但是還沒全部完成,稍後kiocb的ki_retry方法還會被繼續調用,來繼續完成IO請求;否則,調用aio_complete,在AIO Ring加入一個表示一個IO完成的io_event。

1.3 收集完成的IO請求

asmlinkage long sys_io_getevents(aio_context_t ctx_id,

               long min_nr,

               long nr,

               struct io_event __user *events,

               struct timespec __user *timeout)

參數:

(1)ctx_id:AIO上下文句柄;

(2)min_nr:至少收集min_nr個已經完成的IO請求才傳回;

(3)nr:最多收集nr個已經完成的IO請求;

(4)timeout:等待的時間

(5)events:由應用層配置設定,核心将完成的io_event拷貝到該緩沖區,是以,events數組要保證至少有nr個io_event。

io_event:

//aio_abi.h

struct io_event {

    __u64      data;      /* the data field from the iocb */

    __u64      obj;       /* what iocb this event came from */

    __s64      res;       /* result code for this event */

    __s64      res2;      /* secondary result */

};

io_event是用來描述傳回結果的:

(1)data對應iocb的aio_data,傳回使用者定義的指針;

(2)obj就是之前送出IO任務時的iocb;

(3)res和res2來表示IO任務完成的狀态。

io_getevents的流程:

淺析Linux Native AIO的實作

比較簡單,掃描AIO上下文kiocxt的AIO Ring,檢查是否有完成的io_event。如果至少有min_nr個完成IO事件(或者逾時),則将完成的io_event拷貝到events,并傳回io_event的個數或者錯誤;否則,将程序本身加入到kiocxt的等待隊列,挂起程序。

2 AIO工作隊列

2.1 建立AIO工作隊列

//aio.c

static struct workqueue_struct *aio_wq;//AIO工作隊列

static int __init aio_setup(void)

{

...

    aio_wq = create_workqueue("aio");

...

2.2 建立work_struct

static struct kioctx *ioctx_alloc(unsigned nr_events)

{

...

    INIT_WORK(&ctx->wq, aio_kick_handler, ctx);

函數aio_kick_hanlder由aio核心線程處理aio work時調用:

static void aio_kick_handler(void *data)

{

    requeue =__aio_run_iocbs(ctx);

...

    /*

     * we're in a worker thread already, don't use queue_delayed_work,

     */

    if (requeue)

       queue_work(aio_wq, &ctx->wq);

}

邏輯很簡單,調用__aio_run_iocbs繼續處理kioctx中的待完成異步IO,如果需要,則将aio work繼續加入aio工作隊列,下一次再處理。

2.3 排程工作

淺析Linux Native AIO的實作

函數aio_run_iocbs發起異步IO請求後,如果kioctx的run_list還有未完成的IO,則調用queue_delayed_work将work_struct(kioctx->wq)加入到AIO工作隊列aio_wq,由aio核心線程繼續發起異步IO。

3 AIO與epoll

在使用AIO時,需要通過系統調用io_getevents擷取已經完成的IO事件,而系統調用io_getevents是阻塞的,是以有2種方式:(1)使用多線程,用專門的線程調用io_getevents,參考MySQL5.5及以上版本;(2)對于單線程程式,可以通過epoll來使用AIO;不過,這需要系統調用eventfd的支援,而該系統調用隻在2.6.22之後的核心才支援。

eventfd 是 Linux-native aio 其中的一個 API,用來生成 file descriptors,這些 file descriptors 可為應用程式提供更高效 “等待/通知” 的事件機制。和 pipe 作用相似,但比 pipe 更好,一方面它隻用到一個 file descriptor(pipe 要用兩個),節省了核心資源;另一方面,eventfd 的緩沖區管理要簡單得多,pipe 需要不定長的緩沖區,而 eventfd 全部緩沖隻有定長 8 bytes。

關于AIO與epoll的結合,請參考:

nginx 0.8.x穩定版對linux aio的支援(​​http://www.pagefault.info/?p=76​​)

4 AIO與direct IO

AIO需要與direct IO結合。

關于direct IO的簡單實作,可以參考:

Linux 中直接 I/O 機制的介紹

​​http://www.ibm.com/developerworks/cn/linux/l-cn-directio/index.html​​

5 案例

(1)同步IO

淺析Linux Native AIO的實作