- 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); //添加关心的sock
max_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多路复用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这一步的。