Linux C 中epoll函数用法详细介绍及注意事项:
阻塞IO:一次IO操作后一直等待成功或失败才返回,期间程序不能做其它的事情。阻塞IO操作只能对单个文件描述符进行操作。
非阻塞IO:轮询,耗费cpu资源。只能对单个文件描述符进行操作。
IO多路复用:select, poll, epoll。
poll,英文单词意思是轮询的意思。总的来说,epoll就是应用在多路复用I/O的场景中,突破描述符过多导致耗时过多的限制,其可以使用一个文件描述符来管理多个描述符。相比于select和poll每次线性扫描所有的socket,epoll只会对“活跃”的socket操作。
epoll机制相关的三个函数
分别是:epoll_create、epoll_ctl和epoll_wait。
一、epfd = int epoll_create(int size);
函数作用:创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_create() 返回一个epoll实例的文件描述符。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close()关闭epoll_create()返回的文件描述符epfd,否则可能导致fd被耗尽。在所有文件描述符引用fd全都关闭的情况下,针对该epoll实例,内核将销毁该实例并释放关联的资源以供重用。
返回值:
----1)成功:非负描述符; -----2)失败:出错,则返回-1,并且将errno设置为指示错误。
EINVAL:入参size大小不为正,size<0。
ENFILE:已达到打开文件总数的系统限制。
ENOMEM:没有足够的内存来创建内核对象。
1)在最初的epoll_create()实现中,size参数:将调用者希望添加进来的文件描述符的数量告知内核。epoll实例(epfd):内核使用该信息作为内部数据结构初始空间分配的依据。(如果有必要,如果调用方的使用超出了设置大小,内核将分配更多空间。)如今,此提示不再必须(内核无需提示即可动态调整所需数据结构的大小),但是大小必须仍大于零,以便适配新的epoll应用程序在较旧的内核上运行。从Linux 2.6.8开始,maxsize参数将被忽略,但必须大于零。
2)我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdlist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
3)创建一个epoll对象,返回该对象的描述符(文件描述符)epfd,这个描述符就代表这个epoll对象,这个epoll对象最终是要close()的,因为文件描述符始终是要关闭的!
struct event* ep = (struct eventpoll*)calloc(1, sizeof(struct eventpoll));
rbr结构成员:代表一个红黑树的根节点(刚开始指向空),把ebe理解成红黑树的根节点的指针;红黑树用来保存键值对,键【数字】值【结构】,能够快速的通过key取出值。
rdlist结构成员为双向链表的表头指针;从头访问每个元素很快。
epoll对象的结构(如下图 eventpoll, 红黑树+双向链表):
epitem结构(epoll节点)(在epoll中对于每一个事件都会建立一个epitem结构体):
节点中的链表结构:
红黑树中的每个节点结构:
select, poll, epoll的对比:
二、ret = int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数作用:epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值,第二个参数表示动作类型,用三个宏来表示:
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构体的定义如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//联合体:多种类型是为了考虑后期的拓展
typedef union epoll_data {
void ptr;
int fd; //存放文件描述符
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
其中,这个void ptr 指针其实主要是为了自定义的数据结构,用处很大,但一般用不到。
其中 events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
返回值:1) 成功时,epoll_ctl()返回零。 2) 发生错误时,epoll_ctl()返回 -1,并适当设置 errno。
EBADF:epfd 或 fd 不是有效的文件描述符。
EINVAL:epfd 不是 epoll 文件描述符,或者 fd与epfd 相同 ,或者 该接口不支持请求的操作 op 。
ENOMEM:内存不足,无法处理请求的 操作控制操作。
ENOENT:op 是 EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,并且 fd 不在epfd中。
三、ret = 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表示已超时。
阻塞一小段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知;说白了就是遍历这个双向链表,把这个双向链表里边的结点数据拷贝出去,拷贝完毕的就从双向链表里面移除;双向链表里存储的是所有 有数据/有事件 的socket(tcp连接)。
epfd:由epoll_create 生成并返回的epoll对象专用的文件描述符;
epoll_event* events:用来从内核得到事件的集合,用于回传待处理事件的数组;是内存,也是数组,长度是maxevents,表示此次epoll_wait()调用可以收集到的maxevents个事件/socket连接(已经准备好的读写事件)!说白了,就是返回的实际发生事件的tcp连接的数目!
maxevents:每次能处理的事件数;通知内核events大小,maxevents的值不能大于创建epoll_create()时的size。
timeout:等待I/O事件发生的超时时间的值大小(单位 ms 毫秒)。-1相当于(永久)阻塞,0相当于非阻塞。一般使用-1;
返回值:有(可读、可写或异常等)事件发生的数目,返回需要处理的事件数目;如返回0表示已超时,即没有任何事件发生,不需要进行事件处理。
内核向双向链表增加结点,一般有四种情况,会使操作系统把结点插入到双向链表中:
1)客户端完成三次握手了服务器要accept();
2)当客户端关闭连接,服务器也要调用close()关闭
3)客户端发数据来;服务器要调用read recv函数来收数据
4)当可以发送数据时,服务器可以调用send write函数。
四、epoll_event_callback();
当客户端发生上述情况后,操作系统会调用这个函数,用来往双向链表中增加一个结点!
添加以及返回事件
通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就是把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。
五、epoll的工作模式
epoll对文件描述符有两种工作模式(水平触发、边缘触发),分别是LT(level trigger 缺省工作方式) 和ET(edge triggered 高速工作方式)。
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。且必须使用非阻塞套接口。
相比之下,ET模式在一定程度上减少了epoll时间被触发的次数,因此效率较LT模式高。
两种工作模式对比图:
六、epoll实现机制(归纳)
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,存放被监控的socket。这些socket以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立并维护一个list链表,用于存储准备就绪的事件。当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
即使监控数以百万计的socket句柄,准备就绪句柄只是少数,epoll_wait仅需要从内核态copy少量的句柄到用户态即可,更加高效。
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。
执行epoll_create()时,创建了红黑树和就绪链表;
执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
执行epoll_wait()时立刻返回准备就绪链表里的数据即可。
七、最基础的网络编程代码
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。
八、epoll代码实现简版
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...)
listen(s, ...)
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
while(1){
int n = epoll_wait(...)
for(接收到数据的socket){
//处理
}
}
功能分离,使得epoll有了优化的可能。
九、select、poll、epoll区别与对比
纸质书籍资料三者对比
epoll的优点:
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被监听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
1.支持一个进程打开大数目的socket描述符
2.IO效率不随FD数目增加而线性下降
3.使用mmap加速内核与用户空间的消息传递
无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
4.内核微调
回调,回调的目的是提高处理性能和节省资源
epoll的缺点:
1.水平触发模式的 epoll 的扩展性很差。
2.边缘触发的问题:数据乱序
3.epoll_ctl(EPOLL_CTL_ADD) 实际上并不是注册一个 file descriptor (fd),而是将 fd 和 一个指向内核 file description 的指针的对 (tuple) 一块注册给了 epoll,导致问题的根源在于,epoll 里管理的 fd 的生命周期,并不是 fd 本身的,而是内核中相应的 file description 的。
当使用 close(2) 这个系统调用关掉一个 fd 时,如果这个 fd 是内核中 file description 的唯一引用时,内核中的 file description 也会跟着一并被删除,这样是 OK 的;但是当内核中的 file description 还有其他引用时,close 并不会删除这个 file descrption。这样会导致当这个 fd 还没有从 epoll 中挪出就被直接 close 时,epoll() 还会在这个已经 close() 掉了的 fd 上上报事件。
因此,存在 close 掉了一个 fd,却还一直从这个 fd 上收到 epoll 事件的可能性。并且这种情况一旦发生,不管你做什么都无法恢复了。
4.因此,并不能完全依赖于 close() 来做清理工作,一旦调用了 close(),而正好内核里面的 file description 还有引用,这个 epoll fd 就再也修不好了,唯一的做法是把 epoll fd 给干掉,然后创建一个新的,并将之前那些 fd 全部再加到这个新的 epoll fd 上。
所以记住这条忠告:
永远记着先在调用 close() 之前,显示的调用 epoll_ctl(EPOLL_CTL_DEL)
十、整理描述感觉不错的相关博文链接
https://blog.csdn.net/haogenmin/article/details/118527213https://blog.csdn.net/haogenmin/article/details/118527213
1.进程阻塞为什么不占用cpu资源?
2.内核接收网络数据全过程
网卡接收数据的过程。在①阶段,网卡收到网线传来的数据;经过②阶段的硬件电路的传输;最终将数据写入到内存中的某个地址上(③阶段)。这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。
网卡接收数据的过程
通过硬件传输,网卡接收的数据存放到内存中。操作系统就可以去读取它们。
这一步,贯穿网卡、中断、进程调度的知识,叙述阻塞recv下,内核接收数据全过程。
进程在recv阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应socket的接收缓冲区里面(步骤④),再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。
中断程序调用
以键盘为例,当用户按下键盘某个按键时,键盘会给cpu的中断引脚发出一个高电平。cpu能够捕获这个信号,然后执行键盘中断程序。下图展示了各种硬件通过中断与cpu交互。
一般而言,由硬件产生的信号需要cpu立马做出回应(不然数据可能就丢失),所以它的优先级很高。cpu理应中断掉正在执行的程序,去做出响应;当cpu完成对硬件的响应后,再重新执行用户程序。中断的过程如下图,和函数调用差不多。只不过函数调用是事先定好位置,而中断的位置由“信号”决定。
十一、man手册linux函数详情介绍参考文档链接
http://www.hechaku.com/Unix_Linux/epoll_create.htmlhttp://www.hechaku.com/Unix_Linux/epoll_create.html