java aio 底层用epoll_linux AIO libaio和epoll实现非阻塞模型

epoll是Linux内核为处理大批句柄而作改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著的减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一个原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select\poll那种IO事件的电平触发(Level

Triggered)外,还提供了边沿触发(Edge

Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提供应用程序的效率。

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->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事件循环结合起来:

C代码

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

epoll 编程实例,测试通过:

Java代码

#define _GNU_SOURCE

#define __STDC_FORMAT_MACROS

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#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 

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 

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 

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

}

说明:

1. 在centos 6.2 (libaio-devel 0.3.107-10) 上运行通过

2. struct

io_event中的res字段表示读到的字节数或者一个负数错误码。在后一种情况下,-res表示对应的

errno。res2字段为0表示成功,否则失败

3. iocb在aio请求执行过程中必须是valid的

4. 在上面的程序中,通过扩展iocb结构来保存额外的信息(nth_request),并使用iocb.data

来保存回调函数的地址。如果回调函数是固定的,那么也可以使用iocb.data来保存额外信息。

优点:

支持一个进程打开大数目的socket描述符

select

最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下

a4c26d1e5885305701be709a3d33442f.png使用epoll进行高性能网络编程

降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过

epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat

/proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。

IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用

callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个“伪”AIO,因为这时候推动力在os内核。在一些

benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle

connections模拟WAN环境,epoll的效率就远在select/poll之上了。

使用mmap加速内核与用户空间的消息传递

这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。而如果你像我一样从2.5内核就关注epoll的话,一定不会忘记手工

mmap这一步的。

使用:

令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。

LT(level

triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

ET

(edge-triggered)是高速工作方式,只支持no-block

socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK

错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only

once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。[1]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值