本文将简要介绍 select 、epoll 接口,并从接口的设计、调用方式分析两者的差异,最后总结两者功能的差异。当然,为什么么会有这些差异还得去研究相关接口的内核实现细节。
1. Select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
2. Epoll
epoll 使用起来也很清晰,
- 首先调用
epoll_create
建立一个epoll 对象,返回文件 fd epoll_ctl
可以操作上面建立的 epoll 对象进行,增添需要被监听新建立的 socket ,或从监听列表中删除某个 socket- 向
epoll_wait
函数传递一个struct epoll_event
结构参数,当在监控的所有句柄中有事件发生时,就返回数据已经准备就绪的 socket 文件描述符的数量 - 利用返回得到的准备就绪socket 的
num
(数量)轮询上面得到的struct epoll_event
, 根据描述符的类型和事件类型分别进行处理
从上面的调用方式就可以看到epoll比select/poll的优越之处:epoll 只对活跃的 socket 进行事件处理,并且通过 add、delete 的方式来增减 socket,省去了不必要的重复拷贝。
epoll_wait范围之后应该是一个循环,遍利所有的事件。
几乎所有的epoll程序都使用下面的框架:
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}
3. Different
从 select 的调用方式就可以看出,每次调用都要传递需要监控的所有 FD_SET ,这意味着需要将用户态的描述符集 copy 到内核。而且 select 只是在有事件发生时才被唤醒,并没有给出哪些文件描述符是准备就绪的,必须得通过一次 O(n) 的线性扫描。
所以 select 有如下几点缺陷:
- 每次调用 select 都需要把fd集合从用户态拷贝到内核态
- 拷贝结束后 select 都需要在内核遍历传递进来的所有fd
- select支持的文件描述符数量太小了,默认是1024
epoll 能解决上面三点缺陷
- 省去不必要的重复拷贝:epoll 通过内核与用户空间mmap同一块内存,保证了每个fd在整个过程中只会拷贝一次
- 效率:epoll 只会对”活跃”的socket进行操作—这是因为在内核中 epoll 是根据每个fd上面的 callback 函数实现的。只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会
- epoll 没有最大文件符限制,它所支持的FD上限是最大可以打开文件的数目