Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析

本文详细讲解epoll的工作原理,包括事件触发机制、监控事件的注册与处理,以及水平触发(LT)与边沿触发(ET)的区别。通过具体案例演示epoll在服务器端的应用,帮助读者理解epoll如何提升I/O效率。

epoll如何使用?

0、为什么选择epoll

请参考上一篇博客:

epoll为什么更高效 ? epoll涉及的数据结构?

1、epoll的使用过程

在这里插入图片描述

1.0、相关基础概念(便于理解事件触发过程)

1、事件

  • 可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。
    (可读:内核缓冲区非空,有数据可以读取)

  • 可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。
    (可写:内核缓冲区不满,有空闲空间可以写入)

2、通知机制
通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。

3、socket 和 文件描述符之间的关系

套接字也是文件。具体数据传输流程如下:

  • server端监听到有连接时,应用程序会请求内核创建Socket;

  • Socket创建好后会返回一个文件描述符给应用程序;

  • 当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口等在内核维护的一个ipcb双向链表中找到对应的Socket,并将数据包赋值到该Socket的缓冲区

  • 应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据

注意:
操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。

想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?

4、服务器文件传输过程:server(服务器) ——》 client(客户)
在这里插入图片描述
当然:服务器接收客户的请求,红色箭头方向相反。

5、内核接收网络数据全过程

  • 1、计算机收到了对端传送的数据(步骤①);

  • 2、数据经由网卡传送到内存(步骤②);

  • 3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;

    • 先将网络数据写入到对应socket的接收缓冲区里面(步骤④);
    • 再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

在这里插入图片描述

注:
蓝色区域里面的等待队列:就是用户空间进程调用recv函数(读取数据)请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。

你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket?

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

如何同时监视多个socket的数据?

哈哈哈,下面就该引出epoll了。epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。

1.1、epoll_create:文件描述符的创建

epoll_create的作用

  • epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 这个文件描述符使用如下epoll_create函数来创建;
  • epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
  • 调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点(epoll_create创建的文件描述符),在内核cache里建了个 红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.(概括就是:调用epoll_create方法时,内核会跟着创建一个eventpoll对象)
    在这里插入图片描述
  • eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。
  • 创建一个代表该epolleventpoll对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为eventpoll的成员。
struct eventpoll
{
   
   
    spin_lock_t lock;            //对本数据结构的访问
    struct mutex mtx;            //防止使用时被删除
    wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist;    //事件满足条件的链表
    struct rb_root rbr;          //用于管理所有fd的红黑树
    struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间
}

总结:
epoll_create创建额外的文件描述符,来唯一标识内核中的这个内核事件表(eventpoll对象)

epoll_create函数原型

该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

#include<sys/epoll.h>
int epoll_create(int size);

返回值:

  • 返回文件描述符epollfd

参数说明:

  • size参数现在并不起作用,只是给内核一个提示,告诉内核应该如何为内部数据结构划分初始大小。

需要注意的是:

这个文件描述符也会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到这个fd,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽

1.2、epoll_ctl:注册监控事件

epoll_ctl的作用

创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
在这里插入图片描述
注意:

当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。

epoll_ctl函数原型

操作epoll的内核事件表

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

返回值:

  • 成功返回0,不成功返回-1并设置errno。

参数说明:

  • 1、epfd :epoll_create()的返回值;
  • 2、op : 指定操作类型。操作类型有如下3种:
    • EPOLL_CTL_ADD:注册新的fd到epfd中;
    • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    • EPOLL_CTL_DEL:从epfd中删除一个fd;
  • 3、fd:是需要监听的fd
  • 4、event:告诉内核需要监听什么事,指定事件,它是epoll_event结构指针类型。
    • epoll_event的定义如下:
struct epoll_event {
   
   
  __uint32_t events;  /* Epoll 事件 */
  epoll_data_t data;  /* 用户数据 */
};

其中events成员描述事件类型,具体如下:

事件 描述
EPOLLIN 可读取非高优先级数据(重要,必用)
EPOLLPRI 可读取高优先级数据
EPOLLOUT 普通数据可写 (重要,必用)
EPOLLHUP 本端描述符产生一个挂断事件,默认监测事件
EPOLLET 采用边沿触发事件通知(重要)
EPOLLONESHOT 在完成事件通知后禁用检查
EPOLLRDHUP 套接字对端关闭
EPOLLERR 有错误发生

data成员用于存储用户数据,其类型epoll_data_t的定义如下:

typedef union epoll_data
{
   
   
    void* ptr;              //指定与fd相关的用户数据
    int fd;                 //指定事件所从属的目标文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll_data_t是一个共用体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。

1.3、epoll_wait:事件等待,返回就绪事件

epoll_wait的作用
当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket,而不是像select轮询所有的sock。
在这里插入图片描述
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。

当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程,如下:

  • 1、在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。

在这里插入图片描述

  • 2、当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
    在这里插入图片描述

epoll_wait函数原型
它在一段超时时间内等待一组文件描述符上的事件,阻塞等待注册的事件发生,返回事件的数目,其原型如下:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

返回值:

  • 成功时返回就绪文件描述符个数,失败时返回-1并设置errno。

参数说明:

  • 1、epfd :epoll_create()的返回值;
  • 2、events: 用来记录被触发的events(结构参考epoll_ctl),其大小受制于maxevents
  • 3、maxevents: 设定最多监听多少个事件,必须大于0,一般设定为65535
  • 4、timeout:在函数调用中阻塞时间上限,单位是ms
    • timeout = -1:表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
    • timeout = 0:用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
    • timeout > 0:表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。

timeout参数设计技巧问题?

  • 1、设置为-1,程序阻塞在此,后续任务没法执行。
  • 2、设置为0,程序能继续跑,但即使没事件时,程序也在空转,十分占用cpu时间片,我测试时每个进程都是60+%的cpu占用时间。
  • 综上,我们给出比较好的设置方法:将其设置为1,但还没完,因为即使这样设置,处理其它任务时,在每次循环都会在这浪费1ms的阻塞时间,多次循环后性能损失就比较明显了。
    • 为了避免该现象,我们通常向epoll再添加一个fd,我们有其它任务要执行时直接向该fd随便写入一个字节,将epoll唤醒从而跳过阻塞时间。没任务时epoll超过阻塞时间1ms也会自动挂起,不会占用cpu,两全其美。
    • int eventfd(unsigned int initval, int flags),linux中是一个较新的进程通信方式,可以通过它写入字节。

2、LT/ET 使用过程

2.1 LT水平触发(Level Triggered)

  • Level Triggered (LT) 水平触发
    • socket接收缓冲区不为空 ,说明有数据可读, 读事件一直触发
    • socket发送缓冲区不满 ,说明可以继续写入数据 ,写事件一直触发
    • 符合思维习惯,epoll_wait返回的事件就是socket的状态

LT的处理过程:

  • 1、accept一个连接,添加到epoll中监听EPOLLIN事件 .(注意这里没有关注EPOLLOUT事件
  • 2、当EPOLLIN事件到达时,read fd中的数据并处理 .
  • 3、当需要写出数据时,把数据write到fd中;如果数据较大,无法一次性写出,那么在epoll中监听EPOLLOUT事件 .
  • 4、当EPOLLOUT事件到达时,继续把数据write到fd中;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值