IO多路复用 - epoll
2016-10-15 10:13
381 查看
epoll与poll的区别
每次调用poll,都需要把fd集合从用户态拷贝进内核态,开销大;epoll只需要一次拷贝。poll在内核通过遍历得到就绪文件描述符,epoll通过注册回调函数+就绪链表的形式得到就绪文件描述符,不需要遍历。
epoll 函数
1. int epoll_create(int size); /*创建一个epoll的句柄*/ 2. int epoll_ctl(int epfd, int op, int fd, structepoll_event *event); /* epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。 第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事: EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 */ 3. int epoll_wait(int epfd, struct epoll_event* events, int maxevents. int timeout); /* 收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。 */ 4.工作模式 ①LT模式:Level Triggered水平触发 这个是缺省的工作模式。同时支持block socket和non-block socket。内核会告诉程序员一个文件描述符是否就绪了。如果程序员不作任何操作,内核仍会通知。 ②ET模式:Edge Triggered 边缘触发 是一种高速模式。仅当状态发生变化的时候才获得通知。这种模式假定程序员在收到一次通知后能够完整地处理事件,于是内核不再通知这一事件。注意:缓冲区中还有未处理的数据不算状态变化,所以ET模式下程序员只读取了一部分数据就再也得不到通知了,正确的用法是程序员自己确认读完了所有的字节(一直调用read/write直到出错EAGAIN为止)。
示范例程
int main() { /*存储就绪文件描述符的数量*/ int nfds; ... //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件 struct epoll_event ev, events[20]; //生成用于处理accept的epoll专用的文件描述符 epfd = epoll_create(256); ... //设置与要处理的事件相关的文件描述符 ev.data.fd = listenfd; //设置要处理的事件类型 ev.events = EPOLLIN | EPOLLET; //注册epoll事件 epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); ... for ( ; ; ) { //等待epoll事件的发生 nfds = epoll_wait(epfd, events, 20, 500); //处理所发生的所有事件 for(i = 0; i < nfds; ++i) { if(events[i].data.fd == listenfd) { ... } else if(events[i].events & EPOLLIN) { ...; } else if(events[i].events & EPOLLOUT) { ... } } } }
源码思路
当epoll_wait时,它会判断就绪链表中有没有就绪的fd,如果没有,则把current进程加入一个等待队列(file->private_data->wq)中,并在一个while(1)循环中判断就绪队列是否为空,并结合schedule_timeout实现睡一会,判断一会的效果。如果current进程在睡眠中,设备就绪了,就会调用回调函数。在回调函数中,会把就绪的fd放入就绪链表,并唤醒等待队列(file->private_data->wq)中的current进程,这样epoll_wait又能继续执行下去了。/* * This is the callback that is used to add our wait queue to the * target file wakeup lists. */ static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) { struct epitem *epi = ep_item_from_epqueue(pt); struct eppoll_entry *pwq; if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) { init_waitqueue_func_entry(&pwq->wait, ep_poll_callback); pwq->whead = whead; pwq->base = epi; add_wait_queue(whead, &pwq->wait); list_add_tail(&pwq->llink, &epi->pwqlist); epi->nwait++; } else { /* We have to signal that an error occurred */ epi->nwait = -1; } } static inline void init_waitqueue_func_entry(wait_queue_t *q, wait_queue_func_t func) { q->flags = 0; q->private = NULL; q->func = func; }
总结
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
相关文章推荐
- IO多路复用之epoll(一)讲解
- 多路IO复用模型 select epoll
- 多路IO复用模型 select epoll 等
- 三种多路复用IO实现方式:select,poll,epoll的区别
- python网络编程--IO多路复用之epoll
- 聊聊IO多路复用之select、poll、epoll详解
- Linux下套接字详解(十)---epoll模式下的IO多路复用服务器
- python网络编程——IO多路复用之epoll
- IO多路复用之epoll总结
- IO多路复用之epoll全面总结(必看篇)
- IO多路复用之epoll总结
- 浅谈IO的多路复用技术之一(select和epoll实质)
- IO多路复用select,poll,epoll的区别
- Linux-(C)IO多路复用之epoll学习(转载)
- IO多路复用--select和epoll详解
- Linux下多路复用IO接口epoll/select/poll的区别
- IO多路复用select,poll,epoll的区别
- 多路复用io接口-epoll
- IO多路复用之epoll
- linux网络编程 IO多路复用 select epoll