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