i/o多路复用

来源:互联网 发布:沈惟敬 知乎 编辑:程序博客网 时间:2024/05/06 12:56

  • auth:农夫-Ben
  • email:szjava#126.com(请把#换成@)
  • blog:http://blog.csdn.net/zll_liang

最常见的i/o多路复用就是:select poll epoll了,下面说说他们的一些特点和区别吧。


select

/* 可读、可写、异常三种文件描述符集的申明和初始化。*/fd_set readfds, writefds, exceptionfds;FD_ZERO(&readfds);FD_ZERO(&writefds);FD_ZERO(&exceptionfds);int max_fd;sock = socket(...);bind(sock, ...);listen(sock, ...);FD_SET(&readfds, sock);//添加关心的sockmax_fd = sock;while(1) {int i;fd_set r,w,e;memcpy(&r, &readfds, sizeof(fd_set));memcpy(&w, &writefds, sizeof(fd_set));memcpy(&e, &exceptionfds, sizeof(fd_set));/* 利用临时变量调用select()阻塞等待,等待时间为永远等待直到发生事件。*/select(max_fd + 1, &r, &w, &e, NULL);if(FD_ISSET(&r, sock)){new_sock = accept(sock, ...);FD_SET(&readfds, new_sock);FD_SET(&writefds, new_sock);max_fd = MAX(max_fd, new_sock);}for(i= sock+1; i<max_fd+1; ++i) {if(FD_ISSET(&r, i))doReadAction(i);if(FD_ISSET(&w, i))doWriteAction(i);}}


poll

利用poll多路复用I/O的Web服务应用模型/* 新建并初始化文件描述符集。*/struct pollfd fds[MAX_NUM_FDS];int max_fd;/* socket配置和监听。*/sock = socket(...);bind(sock, ...);listen(sock, ...);/* 对socket描述符上发生关心的事件进行注册。*/fds[0].fd = sock;fds[0].events = POLLIN;max_fd = 1;while(1) {    int i;    /*调用poll()阻塞等待,等待时间为永远等待直到发生事件。*/    poll(fds, max_fd, -1);    /* 测试是否有客户端发起连接请求,如果有则接受并把新建的描述符加入监控。*/    if(fds[0].revents & POLLIN){        new_sock = accept(sock, ...);        fds[max_fd].fd = new_sock;        fds[max_fd].events = POLLIN | POLLOUT;        ++ max_fd;    }    /* 对其它描述符发生的事件进行适当处理。*/    for(i=1; i<max_fd+1; ++i) {        if(fds.revents & POLLIN)                doReadAction(i);        if(fds.revents & POLLOUT)                doWriteAction(i);    }}

epoll
利用epoll多路复用I/O的Web服务应用模型/* 新建并初始化文件描述符集。*/struct epoll_event ev;struct epoll_event events[MAX_EVENTS];/* 创建epoll句柄。*/int epfd = epoll_create(MAX_EVENTS);/* socket配置和监听。*/sock = socket(...);bind(sock, ...);listen(sock, ...);/* 对socket描述符上发生关心的事件进行注册。*/ev.events = EPOLLIN;ev.data.fd = sock;epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);while(1) {    int i;    /*调用epoll_wait()阻塞等待,等待时间为永远等待直到发生事件。*/    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);    for(i=0; i<n; ++i) {    /* 测试是否有客户端发起连接请求,如果有则接受并把新建的描述符加入监控。*/        if(events.data.fd == sock) {        if(events.events & POLLIN){            new_sock = accept(sock, ...);            ev.events = EPOLLIN | POLLOUT;            ev.data.fd = new_sock;            epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);        }        }else{            /* 对其它描述符发生的事件进行适当处理。*/            if(events.events & POLLIN)                doReadAction(i);            if(events.events & POLLOUT)                doWriteAction(i);        }    }}

为什么select是落后的?
首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的2.6.15-25-386内核中,该值是1024,
搜索内核源代码得到:
include/linux/posix_types.h:#define __FD_SETSIZE        1024
也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。
其次,内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即select要检测的句柄数越多就会越费时。

epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
    select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.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之上了。
3.使用mmap加速内核与用户空间的消息传递。
    这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。

原创粉丝点击