epoll

个人主页:Lei宝啊 

愿所有美好如期而遇


目录

epoll简介

epoll接口

epoll工作原理

epoll工作方式

水平触发LT模式

边缘触发ET模式


epoll简介

Linux2.6下公认性能最好的多路转接方法。

epoll接口

epoll_create() creates a new epoll(7) instance.  Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;

这句话的意思是,Linux2.6.8后,这个函数的参数size就被忽略了,但是他的大小必须大于0。这个系统调用是用来创建epoll模型的,至于什么是epoll模型,下面的工作原理那里我们说。他的返回值是一个文件描述符。

我们先不说为什么,只先说用法,为什么下面说。

第一个参数,就是epoll_create的返回值,第二个参数可以有如下取值:

  • EPOLL_CTL_ADD,添加,将使OS监管fd下的event事件
  • EPOLL_CTL_DEL,删除,使OS不再监管fd下的event事件,此时第三个参数可以为nullptr
  • EPOLL_CTL_MOD,修改。

第三个参数,事件集合,将你需要让OS监管的事件填入,有如下值:

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.

这是struct epoll_event的数据结构,第一个是32bit位的变量,也就是我们想要填入的事件,第二个变量就是用户的数据,并且我们需要注意到他是一个联合体,同一时间只能使用一个变量,我们后面写代码时,会将fd填入,这样在获取就绪事件时,用户也能知道是哪个fd就绪了。

这个系统调用也就是从用户->OS,目的是让OS知道用户想让他监管哪个fd下的哪些事件。

最后一个系统调用,这个系统调用就是用来告知用户哪些文件描述符上哪些事件就绪了。

第一个参数,仍然epoll_create的返回值,第二个参数可以理解为一个数组,元素的类型为struct epoll_event,并且用户需要提供这样的一块空间,第三个参数为数组的最大元素个数,第四个参数类似于poll,以毫秒为单位,这个参数大于0就是在这个时间里等待就绪事件,等于0就是非阻塞,负数就是阻塞。返回值为就绪事件数量,如果等于0,就是等待超时,小于0,表示发生错误。

这个函数整体是这样的:当有fd对应事件就绪时,这个函数就会将他们获取上来,依次顺序填入events数组中,返回值返回就绪的文件描述符数量。

具体来说,当epoll_wait函数被调用时,它会监听在epfd(epoll实例的文件描述符)上注册的所有文件描述符,等待这些文件描述符上的事件发生(如可读、可写、异常等)。一旦有事件发生,epoll_wait会将就绪的文件描述符及其相关信息保存到传入的events数组中,并返回就绪的文件描述符的数量。

epoll工作原理

当进程调用epoll_create时,OS会在内核中创建一个struct eventepoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,需要我们格外关注。

一个就是红黑树,另一个是就绪队列。

红黑树:节点结构体是这样的,struct  epitem,通过epoll_ctl,构建这样一个节点挂靠在红黑树上,如同select中我们手动构建的数组,不同的是这里的操作都是由OS完成的,并且在搜索时,select的数组需要全部遍历,而这里的红黑树以fd为键值索引,搜索效率明显就会增加。

就绪队列:当某个fd对应的事件就绪时,OS有一个回调函数,在传输层会调用他将红黑树中的这个节点通过结构体中的双链表链入到就绪队列中。(这句话包含的信息我们画图来理解)

首先,OS为什么要有这个回调函数,因为当事件就绪时,OS是知道的,他为什么知道?因为网卡也是硬件,是硬件,就会有硬件中断,当数据传到网卡会向CPU的针脚发送高电频,CPU将对应的中断号填入寄存器,OS就执行他所对应的系统调用,将数据从硬件拷贝到内存中,在数据从网络协议栈向上传递时,到达传输层后,在接收缓冲区有数据时,此时OS是知道的,所以此时将数据向上继续交付后,就会看有没有传递这个回调函数,有就调用他将红黑树中的对应节点链入到就绪队列中,那么怎么链入呢?我们说是双指针,就像这样:

&list1 - &(((struct node*)0)->prev)是什么意思?就是说将0地址处的地址强转成struct node*,然后去取prev的地址,也就是list1相对于整个eventpoll的偏移量,用list1的地址减去偏移量,就是evenpoll的地址。

通过这种方式,我们可以将eventpoll链入其他结构中,只要其他结构中也有struct node这样的结构体变量。

这个cb就是回调函数。

接下来就是epoll_wait,会获取就绪队列中的这些就绪事件,对于用户而言,select检测就绪事件时的时间复杂度是O(N),他需要遍历整个数组,而epoll这里,对于检测就绪事件,事件复杂度为O(1),因为只需要检查就绪队列是否为空,不为空就获取,就是这样。

我们上述说的细节,都是由OS自主完成的,我们只需要调用这三个系统调用,现在,我们给出结论,什么是epoll模型呢?epoll模型就是由红黑树,就绪队列和回调函数等组成的就叫epoll模型。

epoll工作方式

epoll默认的工作模式是水平模式。

水平触发LT模式

epoll检测到socket上的事件就绪后,我们假设是读事件,但是上层一次性没有读完,那么这个事件仍然处于就绪状态,他不会从就绪队列中移除,仍然会通知上层,上层下次继续从这里读取,直到缓冲区中的数据被读取完毕,这个事件才会从就绪队列中移除。

边缘触发ET模式

当epoll检测到socket上的事件就绪后,我们仍然假设是读事件,这个事件,如果上层一次性没有读完,那么没有下次读的机会,因为这个模式下,只会通知上层一次事件就绪,然后该事件从就绪队列中移除。

这两种模式,区别就在于,LT模式,事件没有处理完毕,不会从就绪队列中移除,而是会不断通知上层,而ET模式,一个事件就绪只会通知一次,然后从就绪队列中移除。所以,ET模式就要求上层,在通知上层事件就绪后,必须一次性将事件处理完,对于读事件而言,就必须一次性将缓冲区中的数据读取完,但是如何一次性读完呢?就必须循环读取,但是如果循环读取,最后一次将数据读取就会阻塞,而我们epoll就是不想让上层去等待,所以我们需要将socket设置为非阻塞,非阻塞循环读取!

那么如何设置socket为非阻塞呢?我们可以这样做:

void SetNoBlcock(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

使用fcntl系统调用,F_GETFL获取与文件描述符相关联的当前状态标志,这些状态标志描述了文件的打开方式(如只读、只写、读写)、文件的状态(如是否设置了非阻塞模式、是否设置了追加模式等)。

返回值就是这些标志按位或在一起的值,我们再通过F_SETFL将 O_NONBLOCK非阻塞标志设置进去,并且保留以前fd的设置。

那么现在有一个问题,LT模式和ET模式哪个效率更高呢?也许你毫不犹豫得回答ET模式,那么为什么是ET模式呢?仅仅是因为在数据全部保证被读完的情况下,ET模式通知上层次数更少吗?这只是几次IO的消耗,更深层次的原因在于,ET模式有非阻塞要求,而且一个就绪事件只通知一次,也就减少了系统调用次数,以更快的速度将内核缓冲区中的数据读取,那么在Tcp层,因为数据被以更快的速度读取,那么在应答时,就可以给对方一个更大的接收窗口,那么在发送方,不考虑拥塞窗口,滑动窗口的大小 = 接收端发回的窗口大小(即接收缓冲区的剩余大小),也就是根据接收端的接收能力来确定滑动窗口的大小,这样,接收方的接收能力提高,发送方的发送能力提高,也就提高了通信效率,这也就是为什么ET模式的效率更高。

也许你有一个问题,我LT模式照样也可以设置非阻塞,照样也可以循环读取,凭什么说ET模式效率更高?所有人写的LT模式的Epoll代码都会写非阻塞,都会写循环读取吗?是强制的吗?所以他的效率比ET高吗?这就说不准了,但是ET模式是强制非阻塞循环读取的,因为不这样读,数据是可能度不完整的,一但读不完整,可能会引发更多的问题,所以也就逼着程序员这样做。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lei宝啊

觉得博主写的有用就鼓励一下吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值