注明出处:http://blog.csdn.net/jiange_zh/article/details/50811553
这个问题在面试跟网络编程相关的岗位的时候基本都会被问到,刚刚看到一个很好的比喻:
就像收本子的班长,以前得一个个学生地去问有没有本子,如果没有,它还得等待一段时间而后又继续问,现在好了,只走一次,如果没有本子,班长就告诉大家去那里交本子,当班长想起要取本子,就去那里看看或者等待一定时间后离开,有本子到了就叫醒他,然后取走。
也许在细节方面不是特别恰当,但是总的来说,比较形象地说出了select和epoll的区别。
下面我将简单明了地概述下两者的原理,并概况两者的优缺点。
所需头文件:
#include
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:
监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符。
参数:
nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024。
readfd: 监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这。
writefds: 监视的可写描述符集合。
exceptfds: 监视的错误异常描述符集合
中间的三个参数 readfds、writefds 和 exceptfds 指定我们要让内核监测读、写和异常条件的描述字。如果不需要使用某一个的条件,就可以把它设为空指针( NULL )。集合fd_set 中存放的是文件描述符,可通过以下四个宏进行设置:
//清空集合
void FD_ZERO(fd_set *fdset);
//将一个给定的文件描述符加入集合之中
void FD_SET(int fd, fd_set *fdset);
//将一个给定的文件描述符从集合中删除
void FD_CLR(int fd, fd_set *fdset);
// 检查集合中指定的文件描述符是否可以读写
int FD_ISSET(int fd, fd_set *fdset);
timeout: 超时时间,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其 timeval 结构用于指定这段时间的秒数和微秒数。
struct timeval
{
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* 微秒 */
这个参数有三种可能:};
1)永远等待下去:仅在有一个描述字准备好 I/O 时才返回。为此,把该参数设置为空指针 NULL。
2)等待固定时间:在指定的固定时间( timeval 结构中指定的秒数和微秒数)内,在有一个描述字准备好 I/O 时返回,如果时间到了,就算没有文件描述符发生变化,这个函数会返回 0。
3)根本不等待(不阻塞):检查描述字后立即返回,这称为轮询。为此,struct timeval变量的时间值指定为 0 秒 0 微秒,文件描述符属性无变化返回 0,有变化返回准备好的描述符数量。
返回值:
成功:就绪描述符的数目,超时返回 0,
出错:-1
int epoll_create(int size);
功能:
该函数生成一个 epoll 专用的文件描述符(创建一个 epoll 的句柄)。
参数:
size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
返回值:
成功:epoll 专用的文件描述符
失败:-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
参数:
epfd: epoll 专用的文件描述符,epoll_create()的返回值
op: 表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
fd: 需要监听的文件描述符
event: 告诉内核要监听什么事件,struct epoll_event 结构如下:
events 可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
返回值:
成功:0
失败:-1
int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );
功能:
等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。
参数:
epfd: epoll 专用的文件描述符,epoll_create()的返回值
events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
maxevents: maxevents 告之内核这个 events 有多大 。
timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞
返回值:
成功:返回需要处理的事件数目,如返回 0 表示已超时。
失败:-1
调用select时,会发生以下事情:
调用epoll_create时,做了以下事情:
调用epoll_ctl时,做了以下事情:
调用epoll_wait时,做了以下事情:
观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
总结如下:
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。
执行epoll_create时,创建了红黑树和就绪链表;
执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
执行epoll_wait时立刻返回准备就绪链表里的数据即可。
两种模式的区别:
LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时重复返回这个句柄,而ET模式仅在第一次返回。
两种模式的实现:
当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。
select缺点:
epoll的提升:
当然,以上的优缺点仅仅是特定场景下的情况:高并发,且任一时间只有少数socket是活跃的。
如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。