高级I/O中多路转接之epoll

11 篇文章 0 订阅

在介绍epoll之前,先说说poll。我们都知道,select通过固定的参数位置加输入输出型参数来进行数据的传递。这样做就有一个很大的缺陷,操作麻烦。用户自己还需要创建一个新的数组,将进行监听的源数据保留下来。同时还有一个硬伤,就是select监听的fd是有上限的,这个上限只能通过修改内核的属性来实现增强。如果我们的服务器业务很大的话,就会发现select不够用。

所以有后来出现了poll,poll针对select进行了改进,他将输入的源和输出的数据分离开来,这样就不用新创建一个数组保留源数据。同时因为是使用结构体,不再使用位图的方法,所以poll没有了数量的限制。

poll

函数原型和参数
#include <sys/poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

//pollfd结构体
struct pollfd{
    int fd;
    short events;
    short revents;
}

参数说明:

fds:是一个pollfd* 结构体数组,每一个结构体中包括了三部分:监听的文件描述符、关心的事件集、返回的事件集。

nfds:是数组的大小

timeout:表示poll愿意等待的时间,如果为NULL,表示阻塞等待;如果为0,表示非阻塞;如果大于0,表示周期等待设定时间,超时了返回。

poll中events和revents的标志位:

poll

返回值:如果小于0,表示出错;如果等于0,表示超时返回;如果大于0,表示返回已经就绪的描述符数目。

poll的优点和缺点

优点:

poll很好的解决了之前select的数量上限问题,同时接口也更加的好使用。不再需要考虑源数据的保留,每次进行select之前都需要重新赋值的问题。

缺点:

poll由于是使用了结构体数组作为参数保存结果的。那么我们依然需要遍历这个数组,才知道我们所关心的哪一个fd中哪一个事件就绪。这样就会导致当监听数量增大的时候,性能大大减小。如果当监听的数量很大,但是一段时间内活跃的用户很少的话,就会导致效率及其低下,因为需要遍历寻找,这是一个O(n)的时间复杂度。其次,poll使用的也是输出型参数,每次调用都需要将数据从用户态调入到内核态,再将数据取出来,这样会有大量的消耗。

epoll

正是因为poll还有着这些问题,后来出现了epoll。epoll跟之前的select和poll使用了不一样的思路,可以称为当前linux下性能最好的多路I/O就绪通知方法。man手册上说,他是为了处理大批量句柄而做了改进的poll。

epoll的相关函数调用
#include <sys/epoll.h>
//创建一个epoll句柄
int epoll_create(int size);
//成功返回socket fd,失败返回-1

参数size指的是可以创建的文件描述符的最大值。这个函数在内存中申请一个空间,该空间是一个epoll专用的。该size就是socket fd的最大值。最后需要使用close()函数将fd释放。

#include <sys/epoll.h>
//添加、删除、删除一个epoll事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//成功返回0,失败返回-1

epfd:epoll_create返回的socket fd

op:操作选项。有三个选项,用宏来定义的。

  • EPOLL_CTL_ADD:添加一个新的fd到epfd中。
  • EPOLL_CTL_MOD:修改一个fd的监听事件。
  • EPOLL_CTL_DEL:删除epfd中的一个fd。

fd:需要监听的fd。

event:期望监听的事件。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是一个32整型的宏的集合,data是一个联合。events可以使用的值如下:

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

events:传入型参数,用来保存epoll_wait返回的就绪事件。该参数不能是一个空指针,内核只会讲数据拷贝到我们制定的events中,并不会创建新的空间。

maxevents:是events的大小,该大小不能超过epoll_create指定的大小。

timeout:超时时间。如果指定为-1,表示阻塞等待;如果指定为0,表示非阻塞;如果大于0,指定愿意等待的时间。

返回值:如果返回0,表示超时返回;如果返回大于0,是的是对应I/O已经就绪的fd数目;小于0,表示失败。

一般调用epoll,就只需要使用三个epoll函数,同时最后加上close关闭就好。

epoll工作原理

当我们调用了epoll_create的时候,在内核中就会创建一个eventpoll结构体,这个结构体的内容很多,其中有两个成员与epoll的使用密切相关。eventpoll.rdllist是一个双向链表,eventpoll.rbr是一个红黑树。这两个结构的成员都是一个epitem结构体。epitem结构体如下:

struct epitem{
    struct rb_node rbn; //红黑树节点
    struct list_head rdllink; //双向链表节点
    struct epoll_filefd ffd; //事件句柄
    struct eventpo;; *ep; //指向所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

当我们用epoll_create函数的时候,创建了一个epoll句柄,每个句柄都有一个自己的eventpoll结构体,用来存放epoll中添加的事件。这些事件会被挂在红黑树中,这个红黑树使用关心的fd作为关键字,用关心的事件作为值。这样就做到了插入、查询、删除、修改的时间复杂度是O(log₂n)。同时如果出现重复的写入也不会有什么大影响。

在调用epoll_creat的时候,内核还会创建一个回调函数(ep_epoll_\callback)。当内核发现有关心的事件就绪的时候,该回调函数就会将在红黑树中对应的节点加入到双向链表中。epoll_wait函数就可以直接从双向链表中直接获得。如果双向链表不为空就将事件复制给用户空间,然后返回数量。这样获得就绪事件的时间复杂度就是O(1)。

epoll工作方式

epoll有两种工作方式,分别是水平触发(LT)和边缘触发(ET)。

设置一个场景:有一个tcp socket添加入epoll描述符,对端发送了3K的数据,此时调用了epoll_wait函数,因为有数据写入,此时epoll_wait函数返回。此时调用read读取,read每次只读取1K的数据。接着继续调用epoll_wait函数。

epoll默认的工作方式是水平触发方式(LT):当epoll_wait函数返回的时候,LT模式的epoll会读取数据,如果一次没有读完,epoll_wait下次调用还会返回,然后让epoll再次读取,直到当前的数据被读取完。当前场景下,第一次read了1K,还剩下2K没有读取,再次调用epoll_wait的时候,依然会返回,通知epoll再次读取。

可以在创建句柄的时候,添加选项EPOLLIN | EPOLLET,让LT模式修改为ET模式:ET模式下,当epoll_wait返回的时候,epoll只有一次机会将数据获取完。如果没有获取完,下次调用epoll_wait的时候,只要缓冲区中的数据没有发生变化(增加)epoll_wait不会再次通知epoll获取数据。这样就可能导致数据的丢失。为了防止这种情况产生,在ET模式的时候,必须一次性将数据获取完。但是就如例子所说,缓冲区有3K的数据,read每次只获取1K,我们就可以通过循环读取的方式读取完缓冲区的数据。很不巧的是,read只有读取到阻塞才表示将数据读取完全,所以我们还需要将fd设置为非阻塞的。这样当read返回错误同时errno为EAGAIN的时候,表示数据被读取完全。可以进行下一次的epoll_wait。

两种方式的对比,LT模式可以使用阻塞和非阻塞式的读写。ET只能使用非阻塞式的读写。同时select和poll只有LT模式。

epoll的优点
  • fd的上限很大,而且可以同ulimit进行修改,所以可以说fd没有上限。对比于select中有着数组fd_set大小的限制。
  • epoll利用红黑树来保存监听事件,进行事件的查询、删除、修改、添加的时候,复杂度为O(log₂n)。
  • epoll不再需要使用轮询的方式查找就绪事件,只要在双向链表中获取,其中每一个元素都是已经就绪的。
  • epoll的接口使用很方便。
  • epoll使用了回调函数机制,这就大大减少了操作系统的负担。
epoll的使用场景

都说epoll的高性能,但是是具有一定场景的。并不是适用于所有的场景。epoll适用于多连接中只有一部分连接活跃的情况,这样情况中使用select和poll需要使用轮询的方式访问全部监听的事件,但是epoll速度就很快。如果多连接中连接数量少,我们可以直接使用select或poll处理即可。

同时epoll还有惊群问题,epoll的惊群问题指的是,监听同一个socket的进程会被挂在等待队列中,如果当这个socket到来的时候,这里所以子进程都会被唤醒。但是最后只有一个子进程可以成功获得资源。此时就导致了大量的无用功,浪费了资源。解决这个问题可以在accept阻塞函数加上锁,竞争到锁的才可以进行获取socket。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值