異步AIO

direct-io與非direct-io性能對比:https://blog.csdn.net/wangzhiqing3/article/details/8608780

異步IO的思想

1.應用程序不能阻塞在昂貴的系統調用上讓CPU睡大覺,而是將IO操作抽象成一個個的任務單元提交給內核
2.內核完成IO任務後將結果放在應用程序可以取到的地方
3.這樣在底層做 I/O 的這段時間內,CPU可以去幹其他的計算任務


數據結構 (iocb/io_event)

    異步的IO任務批量的提交和完成,必須有自身可描述的結構,最重要的兩個就是iocb和io_event

1、iocb結構體:提交IO任務時用到的,可以完整的描述一個IO請求(任務單元)
(1) 結構體聲明

struct iocb {
    /*存儲業務指針,與io_getevents方法返回的io_event結構體data成員一致*/
    // 一般存放用戶自定義數據,用戶數據、回調函數
    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;    // 請求的優先級,一般設爲0
    int             aio_fildes;     // 異步IO操作的文件描述符
    union {
        struct io_iocb_common           c;    // IO請求buffer相關,見下
        struct io_iocb_vector           v;
        struct io_iocb_poll             poll;
        struct io_iocb_sockaddr saddr;
    } u;
};

// struct iocb中的c成員
struct io_iocb_common {
    void            *buf;    // 讀寫對應的用戶態緩衝區
    unsigned long   nbytes;  // 讀寫操作的字節長度
    long long       offset;  // 讀寫操作對應的文件中的偏移量
    unsigned        flags;   // 
    unsigned        resfd;
};

(2) iocb結構體的初始化,可以用下面兩個函數
說明:iocb->data 沒有被初始化,需要手動初始化

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

2、io_event結構體:用來描述返回結果 (io_getevents返回結果存放在io_events數組中)

struct io_event {
    void *data;        // 與提交請求事件時iocb結構體中的data一致(二者指向同一個東西,完全相同!)
    struct iocb *obj;  // 指向提交請求事件時對應的iocb結構體
    unsigned long res; // 成功返回讀寫的字節數
    unsigned long res2;// 0,表示成功;內核大部分是爲0,少數情況說明有問題
};

AIO僞代碼步驟

1. 初始化AIO ctx
	io_context_t myctx;
    memset(&myctx, 0, sizeof(myctx));  // 必須清0
    io_setup(AIO_MAXIO, &myctx);       // io_queue_init(AIO_MAXIO, &myctx);

2. 初始化AIO請求數組 iocb_arr[MAX_EVENTS]
	posix_memalign(&buffer, ... ,...)
	io_prep_pwrite/io_prep_pread(&iocb_arr[i], fd, buffer, AIO_BLKSIZE, offset * i)
	設置回調函數 (2種方法)
	   (1) iocb_arr[i].data = aio_done_cbk;
	   (2) io_set_callback(&iocb_arr[i], aio_done_cbk);

3. 將上面AIO請求數組,提交到AIO ctx中
	io_submit(myctx, MAX_EVENTS, iocb_arr);

4. 等待&捕獲所有AIO事件的到來(結果存放在event_res_arr中)
	io_event event_res_arr[NUM_EVENTS];
	aio_cnt = io_getevents(myctx, 1, NUM_EVENTS, event_res_arr, &tms);

5. AIO完成後,隨便執行自定義的操作
	for(i = 0; i < aio_cnt; i++)
	{
		printf("events[%d]:  res = %d, res2 = %d\n", i, event_res_arr[i].res, event_res_arr[i].res2);
		// 回調
		((io_callback_t)(event_res_arr[i].data))(ctx, event_res_arr[i].obj, event_res_arr[i].res, events[i].res2);
	}

6. 在回調函數中,打印讀寫到的數據
	/**
	 * 
	 * @param [in]  ctx     ctx
	 * @param [in]  iocb    event_res_arr[i].obj    保存着buf/nbytes/offset
	 * @param [in]  res     event_res_arr[i].res    實際IO的數據量
	 * @param [in]  res2    events[i].res2          0,成功;非0,錯誤
	 */ 
	static void aio_done_cbk(io_context_t ctx, struct iocb *iocb, long res, long res2)
	{
	   if(res2 != 0)
	   {
	      fprintf(stderr, "AIO occured error\n");
	      return;
	   }
	
	   // 實際IO的數據
	   printf("buf = %s, real_buf_len = %d\n", (char *)iocb->u.c.buf, res);
	
	   // AIO後,fd的偏移位置
	   printf("offset = %d\n", iocb->u.c.offset);
	
	   // 期待讀取到的數據
	   printf("expect len = %d\n", iocb->u.c.nbytes);
	
	   // ... ...
	}

異步IO使用場景 (與eventfd/epoll結合)

Linux native AIO與eventfd、epoll的結合使用 https://blog.csdn.net/cjsycyl/article/details/9332175

我們知道:
    io_getevents函數(與epoll_wait有相似之處),它會等待&直到有已完成的io請求到來才返回。(如果當前沒有或少於指定數目的io請求完成,那麼就會等待直到timeout)

    除io_getevents之外,io_submit()函數也會造成一定的阻塞,使得程序無法繼續向下執行。==> 如果程序還有其它阻塞點,那麼有必要想辦法把這多處等待合而爲一同時進行,從而提高並行性,也就是通常所說的select/epoll等這類多路複用技術。

    本文就以epoll爲例,介紹一下在linux下,如何把aio結合並應用到epoll機制裏。==> 我們知道,epoll機制的最大好處就是它能夠在同一時刻對多個文件描述符(通常是由衆多套接字形成的描述符集合)進行監聽,並將其上發生的讀/寫(或錯誤等)事件通知給應用程序,也就是做到時間上的複用。如果能夠把aio也放到epoll機制裏,即把aio當作epoll機制裏的“一路io”,那麼就能使得aio與其它可能的等待操作(比如:讀/寫套接字)共同工作,從而達到時間複用的目的。

    作爲epoll機制裏的“一路io”,需要一個文件描述符來反饋對應的發生事件,而對於純aio而言,是沒有文件描述符作爲代表的,因此linux系統上多出了一個eventfd()的系統調用:

int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

    對該描述符efd進行read(),如果讀取成功,那麼將返回8-byte的整型數據,而該數據也就是表示已經完成的aio請求個數
    充當中間橋樑的eventfd有了,將eventfd()函數返回的描述符efd添加到epoll機制內,因此剩下需要做的就是把eventfd與aio聯繫起來,而目前aio當然已經有了這個支持。

若想對文件描述符fd,使用異步IO操作,應該怎麼做呢?
道理很簡單:將之前的阻塞read/write函數,更換成異步的IO處理技巧 !
步驟(以read爲例):
1.read(fd, buf, size) --> aio_read(fd, buf, size);
2.aio_read(fd, buf, size);的實現細節見下
(0) 首先要初始化環境:構造eventfd、epoll樹,將eventfd添加到epoll樹中
(1) 構造異步讀請求

io_prep_pread(&iocb, fd, buff, RD_WR_SIZE, 0);
io_set_callback(&iocb, aio_callback);

/* 設置IO完成後,採用事件通知 */
io_set_eventfd(&iocb, efd);   // 等價於write(efd, 1, sizeof(uint64_t)), 即向eventfd中寫入數據

(2) 提交異步讀請求

io_submit(ctx, 1, iocb);

(3) epoll_wait等待efd事件的到來,read讀取AIO異步事件的個數

epoll_wait(efd, &efd_event,1,-1); // 當efd上發生了可讀事件,epoll_wait返回
read(efd, &aio_finish_cnt, 8);

(4) 等待aio_finish_cnt個異步讀事件的完成,逐個處理每個AIO事件

while(aio_finish_cnt > 0)
	{
		// 等待AIO事件的到來,返回值r = 異步事件的個數
		// 將AIO事件執行結果,存放到events中
		r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms); // 與epoll_wait類比

		// 處理每個AIO事件
		for(i = 0; i < r; i++)
		{
			// 調用回調函數events[i].data
			(io_callback_t)(events[i].data)(ctx, ... ,... ,...);
		}
	}

綜上所述過程,僞代碼見下

{
	int efd = eventfd();        // 創建事件通知fd
	int epfd = epoll_create()   // 創建epoll樹
	epoll_clt(epfd, ADD, efd); 	// 將efd添加到epoll樹中,關心讀事件
	
	while(1)
	{
			epoll_wait(efd, &efd_event,1,-1);
			// 當efd上發生了可讀事件,epoll_wait返回
			
			// 讀取efd中存放的數值aio_finish_cnt,表示AIO事件完成的個數
			read(efd, &aio_finish_cnt, 8);
		
			while(aio_finish_cnt > 0)
			{
					// 等待AIO事件的到來,返回值r = 異步事件的個數
					// 將AIO事件執行結果,存放到events中
					r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms); // 與epoll_wait類比
			
					// 處理每個AIO事件
					for(i = 0; i < r; i++)
					{
						// 調用回調函數events[i].data
						(io_callback_t)(events[i].data)(ctx, ... ,... ,...);
					}
			}
			
	} // while(1)
	

示例代碼

#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  5

#define RD_WR_SIZE  1024
  
struct custom_iocb
{
    struct iocb iocb;
    int request_num;
};


void aio_callback(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
    struct custom_iocb *iocbp = (struct custom_iocb *)iocb;

// 自定義結構體:額外的數據
    printf("request_num[%d]:\n", iocbp->request_num);
    
// ioevent.obj, 即iocb
    if(res2 != 0)
    {
        fprintf(stderr, "AIO occured error\n");
        return;
    }

    // 實際IO的數據
    ((char *)iocb->u.c.buf)[res - 1] = '\0';
    printf("    op = %s, buf = %s, real_buf_len = %d \n", 
            (iocb->aio_lio_opcode == IO_CMD_PREAD) ? "READ" : "WRITE",
            (char *)iocb->u.c.buf, res
    );
    // AIO後,fd的偏移位置
    printf("    offset = %d\n", iocb->u.c.offset);
    // 期待讀取到的數據
    printf("    expect len = %d\n", iocb->u.c.nbytes);
}
  
int main(int argc, char *argv[])
{
    int fd;
    
    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;
    
  
// io_load()  
    io_context_t ctx;
    memset(&ctx, 0, sizeof(ctx));
    io_setup(8192, &ctx);  // 初始化ctx
    
    int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

    int epfd = epoll_create(1);  
    struct epoll_event epevent;
    epevent.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &epevent);    

// io_open()
    fd = open(TEST_FILE, O_RDWR /*| O_CREAT*/ | O_DIRECT, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    // ftruncate(fd, TEST_FILE_SIZE);

    char* buff = NULL;
    // 構造多個AIO請求
    for (i = 0, iocbp = iocbs; i < NUM_EVENTS; ++i, ++iocbp) {
        posix_memalign((void **)&buff, getpagesize(), RD_WR_SIZE);

        iocbps[i] = &iocbp->iocb;
        io_prep_pread(&iocbp->iocb, fd, buff, RD_WR_SIZE, 0);
        
        // 等價於write(efd, 1, sizeof(uint64_t)), 即向eventfd中寫入數據
        /* 設置IO完成後,採用事件通知 */
        io_set_eventfd(&iocbp->iocb, efd);  

        io_set_callback(&iocbp->iocb, aio_callback);
        iocbp->request_num = i + 1;
    }

    // 提交請求
    if (io_submit(ctx, NUM_EVENTS, iocbps) != NUM_EVENTS) {
        perror("io_submit");
        return -1;
    }
  
    i = 0;
    while (i < NUM_EVENTS) {
        uint64_t finished_aio;

        printf("epoll_wait before ... \n");
        // 等待efd事件通知的到來
        // 立即返回,因爲上面執行了io_set_eventfd
        epoll_wait(epfd, &epevent, 1, -1);  
        printf("epoll_wait after ... \n");

        // on_notify:處理eventfd讀事件
        {
            read(efd, &finished_aio, sizeof(finished_aio)); 
            printf("finished_aio = %lu\n", finished_aio);
    
            // 處理finished_aio個異步IO事件
            while (finished_aio > 0) {
                tms.tv_sec = 0;
                tms.tv_nsec = 0;

                // 等待IO異步事件的到來
                r = io_getevents(ctx, 1, NUM_EVENTS, events, &tms);
                for (j = 0; j < r; ++j)
                {
                    // 完成單個AIO事件的處理
                    ((io_callback_t)(events[j].data))(ctx, events[j].obj, events[j].res, events[j].res2);
                }
                
                i += r;
                finished_aio -= r;
            }
        }

    }
  
// io_unload()
    close(epfd);
    // free(buff);
    io_destroy(ctx);
    close(fd);
    close(efd);
    // remove(TEST_FILE);
  
    return 0;
}

執行結果

[g ~/test]# touch aio_test_file           # 新建測試文件
[g ~/test]# echo "ABCDE" > aio_test_file  # 向測試文件中寫入"ABCDE"字符串
[g ~/test]# ./laio             # 執行程序
epoll_wait before ...
epoll_wait after ...
finished_aio = 5
request_num[1]:
    op = READ, buf = ABCDE, real_buf_len = 6
    offset = 0
    expect len = 1024
request_num[2]:
    op = READ, buf = ABCDE, real_buf_len = 6
    offset = 0
    expect len = 1024
request_num[3]:
    op = READ, buf = ABCDE, real_buf_len = 6
    offset = 0
    expect len = 1024
request_num[4]:
    op = READ, buf = ABCDE, real_buf_len = 6
    offset = 0
    expect len = 1024
request_num[5]:
    op = READ, buf = ABCDE, real_buf_len = 6
    offset = 0
    expect len = 1024

參考鏈接

https://blog.csdn.net/cjsycyl/article/details/9332175

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