epoll模型
epoll是linux下多路复用IO接口select/poll的增强版本。
它能显著减少程序在大量并发连接中只有少量活跃的情况下系统cpu利用率,其一:它不会复制文件描述符集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。其二:epoll获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll 那种IO事件的水平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。其中,以有可读数据事件为例,水平触发就是只要缓冲区中有数据,epoll_wait就会响应,就会返回;而边沿触发就是不管缓冲区中是否有数据,只有当有数据传来事件发生时,epoll_wait才会响应返回。
epoll函数
epoll中共有三个函数:
epoll_create
int epoll_create(int size)
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。
需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,
是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值
第二个参数表示动作,用三个宏来表示
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR:表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
需要注意的是,调用epoll_ctl函数时,会发现这里第三个参数和第四个参数都需要设置一个fd,这是为什么呢?按道理来说,如果都表示与监听事件相关联的文件描述符,那么只需设置一个就可以了,为什么还需要设置两个fd呢?
实际上,传入的第三个参数fd是真正意义上的与需要监听的事件相关联的文件描述符,发生的io事件也是在这个文件描述符上的;而第四个参数epoll_event中设置的fd意义则完全不一样了:这里的fd是放在成员联合体data中的,由于是联合体,说明最终epoll_event中fd肯定不是一种“属性”,它实际上是这个epoll_event对应的事件的一个标识,因为这里通过epoll_ctl传入的epoll_event,如果其对应的事件发生了,那么在epoll_wait返回的那个epoll_event就绪链表中肯定也会存在这个epoll_event,这里的fd就可以用来标识epoll_wait返回的epoll_event是不是当初epoll_ctl设置的那个epoll_event。
举个例子,你完全可以在调用epoll_ctl时,把epoll_event中的fd设置为任意一个独一无二的值比如说999,当epoll_wait返回时,你就可以遍历就绪链表,看哪一个epoll_event的fd为999,那么就说明这个就绪的epoll_event就是当初epoll_ctl设置的那个epoll_event。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,
参数timeout是超时时间(毫秒,0会立即返回,-1将永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
select、poll、epoll对比
select | poll | epoll | |
---|---|---|---|
支持最大连接数 | 1024(x86) or 2048(x64)由内核决定 | 无上限,由硬件决定 | 无上限,由硬件决定 |
I/O效率 | 每次调用进行线性遍历,时间复杂度为O(N) | 每次调用进行线性遍历,时间复杂度为O(N) | 使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1) |
fd拷贝 | 每次select都拷贝 | 每次poll都拷贝 | 调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝 |
为什么ET模式下,需要将套接字设置为非阻塞式?
LT模式是指只要缓冲区中有数据,epoll_wait就会返回,而在ET模式下,epoll_wait只有当新事件来临时才会返回。举个例子,对于一般的LT,在阻塞模式下,如果设置了read一次读取长度为1K,当对端发来2K的数据,read一次读取完1K后,由于是水平触发,那么epoll_wait会再次返回,然后read去读剩下的1K数据,这样就将2K数据全部读完;
而对于ET模式,那么在第一次读完1K数据后,epoll_wait是不会返回的,要返回也是需要等到下一次对端数据传来,假设下一次对端数据再传2K过来,此时epoll_wait返回,read读到的实际上是上一次剩下来的1K,这显然是不行的。因此,如果read是阻塞的,为了让read一次性读完数据,那么就需要用循环让read读取,循环两次来把2K读完,但是2K读完之后呢?由于read是阻塞的,当缓冲区中没有数据,read就会一直卡在那等待数据,此时read不返回,也就无法调用epoll_wait,如果这个时候一个客户端发起连接请求,不就没人来响应了吗?因此,在ET模式下,是不应该设置套接字为阻塞的。
另一方面,如果设置为非阻塞,此时让read循环读取,当没有数据可读的时候,read并不会阻塞在那里,此时read就会返回-1,表示数据读完了。
可以见得,阻塞与非阻塞,关键就在于read函数是否在无数据可读的时候会立刻返回,如果能返回,那自然就能继续处理其他请求了,而如果不能,那么read就会一直阻塞在原处,而其他连接请求也就无法及时处理了。
因此,对于ET模式,应当将套接字设置为非阻塞式。