欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-14 14:37:42
...

在介绍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的标志位:

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

返回值:如果小于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。

相关标签: 多路转接 epoll