epoll如何使用?
epoll如何使用?
0、为什么选择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一样,它也会有等待队列。- 创建一个代表该
epoll的eventpoll对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为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:是需要监听的fd4、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),其大小受制于maxevents3、maxevents: 设定最多监听多少个事件,必须大于0,一般设定为655354、timeout:在函数调用中阻塞时间上限,单位是mstimeout = -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 )

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

被折叠的 条评论
为什么被折叠?



