Socket编程权威指南(四)彻底解密 Epoll 原理
在上一篇文章中,我们优化了基于 Socket 的网络服务器,从最初的 select/poll 模型进化到了高效的 epoll。很多读者对 epoll 的惊人性能表示极大的兴趣,对它的工作原理也充满了好奇。今天,就让我们一起揭开 epoll 神秘的面纱,深入剖析其内部运作机制,进一步提升你的 Linux 网络编程技能。
一、Epoll 基本概念
1、 IO 复用模型回顾
我们都知道,传统的 IO 编程模型在处理高并发场景时存在严重的性能瓶颈。select/poll 虽然提供了 IO 复用功能,但由于底层采用轮询机制,当监视的文件描述符数量较多时,开销仍然很大。
想详细了解IO 复用模型知识的同学,请前往查阅:Socket编程权威指南(三)读写无阻塞-完美掌握I/O复用。
2、 epoll 的优势
epoll 是一种高效的事件驱动模型,允许服务器在多个非阻塞的 socket 描述符上等待可读或可写事件。在实际应用中,这可以显著提升系统处理大量 TCP 连接的效率。
假设服务器需要管理 10 万个 TCP 连接,但并非所有连接都是活跃的,可能只有 5000 个或更少的连接在任何给定时间点上有数据可读或可写。这是因为用户并不总是实时在线或活跃。
如果服务器能够直接识别并只处理这 5000 个活跃的连接,而不是盲目地轮询所有 10 万个连接,那么它将能够更高效地利用系统资源。epoll 的核心优势就在于此:它能够快速识别并响应那些真正需要处理的连接。
通过使用 epoll,服务器可以专注于那些有实际 I/O 活动的连接,而不是浪费资源在那些当前没有数据交换的连接上。这种智能的事件通知机制使得 epoll 成为处理大规模并发连接的理想选择,特别是在需要维护大量长连接的高性能网络应用程序中。
Epoll 的出现正是为了解决上述问题。另外,epoll 还提供了以下几个独特的特性:
-
支持水平触发和边缘触发:可以选择监视文件描述符的状态或状态变化。
-
支持一次性监视:采用 EPOLLONESHOT 标志可以避免同一个文件描述符被重复监视。
-
支持指示在内核与用户空间之间拷贝数据:提供了 EPOLLWAKEUP 标志来确保在某些事件下,数据就绪时能被立即拷贝。
二、Epoll 关键数据结构
在深入了解 epoll 的工作原理之前,我们需要先介绍几个关键的数据结构:
-
struct eventpoll
是 epoll 实例的内核数据结构。 -
struct rdllist
是一种双向链表,用于存储就绪文件描述符列表。
1、struct eventpoll 结构剖析
struct eventpoll
是 Linux 内核中用于 epoll 机制的一个内部数据结构,它不是用户空间可以直接访问的,因此其确切的定义通常在 Linux 内核源代码中。
由于 struct eventpoll
的定义可能会随着不同版本的 Linux 内核而有所变化,这里提供一个通用的概述,以及如何在内核源代码中找到它。
在 Linux 内核源代码中,struct eventpoll
通常定义在 include/linux/epoll.h
或者与之相关的文件中。
这个结构体包含了用于管理 epoll 实例的所有必需字段,例如红黑树的根节点、就绪链表、等待队列等。
以下是一个示例性的简化版本,用于说明 struct eventpoll
可能包含的成员:
struct eventpoll {struct rb_root rbroot; // 红黑树的根节点,用于管理注册的事件struct list_head rdllist; // 就绪链表,存储准备好的事件wait_queue_head_t wait; // 等待队列,用于等待事件的进程可以在这里等待// 可能还有其他成员,具体取决于内核版本
};
请注意,上述代码不是内核中实际的 struct eventpoll
定义,而只是一个示例,用于说明这个结构可能包含的类型。实际的 struct eventpoll
定义会更复杂,包含更多成员和嵌套结构。
要查看特定 Linux 内核版本的 struct eventpoll
的确切原型,你需要访问该版本的内核源代码。通常,你可以在以下路径找到它:
<kernel-source>/include/linux/epoll.h
在这里,<kernel-source>
是你的 Linux 内核源代码目录。由于 struct eventpoll
是一个内部结构,它可能没有在任何头文件中公开,这意味着它可能只在内核源代码的某些 .c
文件中定义和使用。
以下是对 struct eventpoll
结构的详解以及它的使用方式。
(1)、结构体定义
虽然 struct eventpoll
的确切定义是 Linux 内核特定的,并且可能会根据不同版本的内核而变化,但通常它包含以下关键组件:
-
一个红黑树(Red-Black Tree),用于存储所有注册的事件和对应的文件描述符。
-
一个就绪链表(Ready List),用于存储那些已经准备好可以进行 I/O 操作的事件。
-
一个等待队列(Wait Queue),用于放置正在等待 I/O 事件的进程或线程。
(2)、主要成员变量
rbroot
:指向红黑树的根节点。红黑树用于高效地插入、删除和查找文件描述符及其关联的事件。rdllist
:就绪链表的头节点。当某个文件描述符上的事件发生时,相关的事件会被添加到这个链表中。wait_list
:等待队列,通常是一个互斥锁(mutex)或自旋锁(spinlock),保护着对就绪链表的访问。ep_events
:存储与文件描述符关联的事件的数组或链表。user_data
:用户自定义数据,可以是任何类型的指针,用于在事件发生时传递额外信息。
(3)、使用方式
-
创建 epoll 实例:使用
epoll_create()
系统调用创建一个新的 epoll 实例,内核会分配一个struct eventpoll
结构。 -
注册事件:通过
epoll_ctl()
系统调用,将感兴趣的文件描述符和事件注册到 epoll 实例中。这会在红黑树中添加一个条目。 -
等待事件:使用
epoll_wait()
系统调用等待感兴趣的事件发生。当事件发生时,它们会被添加到就绪链表中。 -
处理事件:
epoll_wait()
返回后,应用程序可以遍历返回的事件数组,处理每个事件。 -
删除事件:使用
epoll_ctl()
与EPOLL_CTL_DEL
操作可以删除红黑树中的条目,停止监视特定的文件描述符。 -
关闭 epoll 实例:当不再需要 epoll 实例时,使用
close()
系统调用关闭 epoll 实例的文件描述符,内核随后会释放struct eventpoll
结构。
(4)、注意事项
struct eventpoll
结构是内核内部使用的,用户空间程序不会直接操作这个结构。- epoll 的使用需要对 Linux 内核的 epoll 机制有深入的理解,特别是在并发环境下对锁和同步的处理。
- 在使用 epoll 时,应当注意文件描述符的生命周期管理,确保在不再需要时正确地从 epoll 实例中删除并关闭它们。
2、struct rdllist
结构剖析
在 Linux 内核中,struct rdllist
并不是一个独立的数据结构,而是通常用来指代一个双向链表(doubly-linked list)的头节点。在 epoll 的上下文中,struct list_head
被用来实现双向链表,而 rdllist
可能是某个特定内核源码中对这种链表的一个引用或别名。
(1)、结构体定义
在 Linux 内核中,双向链表的节点通常由 struct list_head
定义:
struct list_head {struct list_head *next, *prev;
};
-
next
:指向链表中下一个节点的指针。 -
prev
:指向链表中上一个节点的指针。
(2)、使用方式
双向链表通常用于需要从任意位置快速添加或删除节点的场景。在 epoll 中,一个 struct list_head
类型的成员可能被用作链表的头节点,用于管理一组事件或对象。
(3)、epoll 中的使用
在 epoll 的实现中,struct list_head
可能被用于以下场景:
- 就绪列表(Ready List):epoll 使用一个双向链表来维护那些已经准备好可以进行 I/O 操作的文件描述符。当一个文件描述符上的事件发生时,它会被添加到这个链表中。
- 等待队列:epoll 可能使用链表来管理等待特定事件发生的进程或线程。
(4)、示例代码
以下是如何在 C 语言中使用 struct list_head
来管理一个简单的双向链表的示例:
#include <stddef.h>struct list_head {struct list_head *next, *prev;
};// 初始化链表头节点
#define INIT_LIST_HEAD(ptr) do { (ptr)->next = (ptr); (ptr)->prev = (ptr); } while (0)// 添加新节点到链表末尾
void list_add(struct list_head *new, struct list_head *head) {new->next = head->next;new->prev = head;head->next->prev = new;head->next = new;
}// 从链表中删除节点
void list_del(struct list_head *entry) {entry->next->prev = entry->prev;entry->prev->next = entry->next;
}// 遍历链表
void list_for_each(struct list_head *head, struct list_head *pos) {pos = head->next;while (pos != head) {// 处理 pos 指向的节点pos = pos->next;}
}int main() {struct list_head list, node1, node2;INIT_LIST_HEAD(&list);list_add(&node1, &list);list_add(&node2, &list);// 遍历链表并打印节点地址struct list_head *pos;list_for_each(&list, pos) {printf("Node address: %p\n", pos);}// 删除特定节点list_del(&node1);return 0;
}
请注意,上述代码是一个简化的示例,用于说明如何在用户空间程序中使用 struct list_head
类似的双向链表。在 Linux 内核中,双向链表的使用可能会涉及到更多的内核特定的宏和辅助函数,例如 list_add()
, list_del()
, 和 list_for_each()
等。
三、Epoll 三个核心 API
1、epoll_create()函数详细剖析
epoll_create
函数用于创建一个新的 epoll 实例,用于初始化 epoll 机制,它是实现高性能网络 I/O 多路复用的关键步骤。
以下是对 epoll_create
函数的详细说明:
(1)、函数原型
#include <sys/epoll.h>int epoll_create(int size);
(2)、参数
-
size
:这个参数在 Linux 2.6.8 版本之前的内核实现中,表示 epoll 实例可以同时处理的最大文件描述符数量的提示。从 Linux 2.6.8 版本开始,size
参数被忽略,但调用者仍需传入一个大于零的值以确保向后兼容性32。
(3)、返回值
-
成功时,
epoll_create
返回一个新的文件描述符(fd),该文件描述符引用了新创建的 epoll 实例。 -
出错时,返回
-1
并设置errno
以指示错误类型。
(4)、内核中的对象
-
在内核中,每个 epoll 实例对应一个
struct eventpoll
类型的对象,它是 epoll 机制的核心。
(5)、创建过程
-
epoll_create
调用ep_alloc
函数来分配并初始化struct eventpoll
对象。 -
分配一个未使用的文件描述符,并创建一个
struct file
对象,将file_operations
指向全局变量eventpoll_fops
,并将private_data
设置为指向新创建的eventpoll
对象。 -
最后将文件描述符添加到当前进程的文件描述符表中,并返回给用户。
(6)、epoll 对象结构
-
epoll 对象包含两个核心的数据结构:红黑树和双向链表。
-
epoll 对象会存储红黑树的根节点和双向链表的头节点,而在初始化时,这两个节点都设置为
NULL
。这种结构设计使得 epoll 能够高效地管理大量事件,同时快速响应那些已经准备好的 I/O 请求。 -
红黑树用于存储和管理所有注册的事件,它允许快速地插入、删除和查找操作。 RBTree 的节点就表示一个一个的事件。
-
双向链表则用于管理那些准备就绪的事件,即那些已经发生且等待处理的 I/O 事件。
(7)、使用后的处理
-
使用完 epoll 实例后,必须调用
close()
函数关闭返回的文件描述符,以释放资源。
(8)、错误处理
- 如果
size
不是正数,将返回EINVAL
错误。 - 如果达到每个用户或系统范围内打开文件描述符的数量限制,将返回
EMFILE
或ENFILE
错误。 - 如果内存不足,将返回
ENOMEM
错误3。
2、epoll_ctl()
函数详细剖析
epoll_ctl()
函数是 epoll API 的一部分,用于控制 epoll 实例的操作。它允许用户向 epoll 实例中添加、修改或删除感兴趣的文件描述符(fd)以及它们关联的事件。
以下是对 epoll_ctl()
函数的详细剖析:
(1)、函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
(2)、参数说明
-
epfd:由
epoll_create()
创建的 epoll 实例的文件描述符。 -
op:操作类型,可以是以下宏之一
-
EPOLL_CTL_ADD
:将新的文件描述符fd
注册到 epoll 实例中。-
将文件描述符(fd)添加到 epoll 实例中,在大多数情况下涉及的都是 socket 描述符。这一过程本质上是在红黑树(RBTree)中创建一个新的节点。在这个节点中,键(key)是我们指定的文件描述符 fd,而值(value)是一个指向名为
epitem
的对象的指针。 -
epitem
对象通常包含了有关文件描述符的事件信息,如要监控的事件类型(如可读、可写等)以及用户定义的数据。 -
epoll 的核心特性之一是在将文件描述符添加到红黑树的同时,还会在 socket 的等待队列中注册一个等待事件,并设置一个名为
ep_poll_callback
的回调函数。当 socket 上发生了相应的事件时,操作系统将触发这个回调函数。这个回调函数的主要作用是将红黑树中对应的epitem
节点移动到就绪链表(rdllist
)中。这种移动表示关联的事件已经准备就绪,可以被应用程序处理。 -
由此可见,epoll 的工作机制本质上是基于回调的。由于 epoll 机制在 socket 的实现代码中加入了特定的处理逻辑,这导致了 epoll 的跨平台移植性相对较低。这是因为 epoll 是 Linux 特有的机制,其他操作系统可能需要不同的实现方式来达到类似的效果。
-
-
EPOLL_CTL_MOD
:修改已经在 epoll 实例中的文件描述符fd
的事件。 -
EPOLL_CTL_DEL
:从 epoll 实例中删除文件描述符fd
。
-
-
fd:需要被操作的文件描述符。
-
在 Linux 系统中,epoll 可以管理多种类型的文件描述符,包括但不限于 socket 描述符、POSIX 消息队列、inotify 实例、管道(pipes)或 FIFO(先进先出队列)。
-
然而,epoll 不适用于普通文件或目录的文件描述符。原因在于,与 socket 或命名管道等相比,文件 I/O 操作在 Linux 中被视为“快速 I/O”操作。这意味着文件 I/O 操作通常会立即完成,结果是成功或失败,而不会进入长时间的阻塞状态。
-
简而言之,epoll 设计用于处理那些可能需要长时间等待 I/O 操作完成的描述符,例如网络通信或进程间通信的 socket。对于普通文件操作,由于其快速的特性,通常不需要使用 epoll 这样的多路复用机制。
-
-
event:指向
epoll_event
结构的指针,该结构定义了要注册或修改的事件类型和用户自定义数据。结构体定义如下:
struct epoll_event {uint32_t events; // 事件掩码,可以是多个事件类型的组合epoll_data_t data; // 用户自定义数据,可以是任何类型的指针
};
-
事件类型:epoll_event
结构中的
events字段是一个事件掩码,可以是以下事件类型的组合:
-
EPOLLIN
:表示文件描述符可读(包括对端关闭)。 -
EPOLLOUT
:表示文件描述符可写。 -
EPOLLPRI
:表示有紧急数据可读。 -
EPOLLERR
:表示文件描述符发生错误。 -
EPOLLHUP
:表示对端关闭了连接。 -
EPOLLET
:表示使用边缘触发模式,而不是默认的级别触发模式。
-
(3)、返回值
-
成功时,
epoll_ctl()
返回 0。 -
出错时,返回 -1 并设置
errno
以指示错误类型。
(4)、错误处理:
-
EBADF
:epfd
或fd
不是一个有效的文件描述符。 -
ENOENT
:使用EPOLL_CTL_DEL
时,指定的fd
不存在于 epoll 实例中。 -
ENOMEM
:内核内存不足,无法完成操作。 -
EEXIST
:使用EPOLL_CTL_ADD
时,指定的fd
已经存在于 epoll 实例中。
(5)、工作原理:
- 注册事件:当使用
EPOLL_CTL_ADD
时,epoll_ctl()
将文件描述符fd
和关联的事件添加到 epoll 实例中。 - 修改事件:使用
EPOLL_CTL_MOD
可以更新已经注册的文件描述符fd
的事件类型。 - 删除事件:使用
EPOLL_CTL_DEL
从 epoll 实例中移除文件描述符fd
。
epoll_ctl()
是 epoll 机制中非常关键的函数,它使得应用程序能够灵活地管理感兴趣的 I/O 事件,而无需轮询检查每个文件描述符的状态。这种机制特别适合于需要同时监视大量文件描述符的高性能网络应用。
3、epoll_wait()
函数详细剖析
epoll_wait()
函数是 epoll API 的核心部分,用于等待在 epoll 实例中注册的文件描述符上的 I/O 事件。
系统调用 epoll_wait()
的作用是检索处于就绪状态的文件描述符信息,并将这些信息返回给调用者。这个调用能够一次性返回多个准备好的文件描述符,它们会被存储在用户指定的 evlist
数组中。
在 epoll 实例中,rdllist
(就绪链表)是用来存放那些已经准备好进行 I/O 操作的事件或文件描述符的。当 epoll_wait()
被调用时,其执行的流程如下:
- 从就绪链表
rdllist
中检索出所有已经就绪的事件。 - 将这些事件的信息复制到用户空间提供的
evlist
数组中。这个数组应该足够大,能够容纳所有待返回的事件。 - 一旦事件信息被复制到
evlist
中,对应的节点就会从就绪链表中移除,表示这些事件已经被处理。
因此,epoll_wait()
实际上执行了一个从内核空间到用户空间的数据传输,并将已就绪的 I/O 事件通知给应用程序。应用程序随后可以遍历 evlist
数组,对每个返回的文件描述符进行相应的处理。这个过程是 epoll 机制中处理 I/O 事件的关键步骤,确保了高效的事件通知和响应。
如上图:即使就绪链表 rdllist
中存在 5 个已经准备好的事件节点,但如果用户提供的 evlist
数组仅能容纳 4 个事件,那么 epoll_wait()
调用将只能将 4 个节点的信息复制到 evlist
中。剩余的一个节点将继续保持在 rdllist
上,等待下一次调用 epoll_wait()
时再被处理。
内核负责维护 rdllist
中的内容,确保当 epoll_wait()
被触发时,能够正确地将就绪的事件节点复制到用户空间提供的数组中,并且在复制完成后更新链表,移除已经被处理的节点。这个过程展示了 epoll 机制的高效性,它允许应用程序按需获取事件,同时保持内核管理的就绪事件列表的准确性和最新状态。
以下是对 epoll_wait()
函数的详细剖析:
(1)、函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
(2)、参数说明
-
epfd:由
epoll_create()
创建的 epoll 实例的文件描述符。 -
events:指向
epoll_event
结构数组的指针,该数组用于从内核接收发生的事件。 -
maxevents:
events
数组的最大容量,即最多可以接收的事件数量。 -
**timeout:**等待时间,单位为毫秒。这个参数决定了epoll_wait()
调用的阻塞行为:
- 如果
timeout
为-1
,函数将无限期地阻塞,直到至少有一个事件被触发。 - 如果
timeout
为0
,函数不会阻塞,立即返回当前已经触发的事件(如果有的话)。 - 如果
timeout
大于0
,函数将阻塞直到超时或至少有一个事件被触发。
- 如果
(3)、返回值
- 成功时,返回数组
events
中填充的事件数量。 - 出错时,返回
-1
并设置errno
以指示错误类型。
(4)、错误处理
EBADF
:epfd
不是一个有效的文件描述符。EINTR
:等待被中断,例如通过信号。EFAULT
:events
指向的内存区域不可访问。
(5)、工作原理
-
等待事件:
epoll_wait()
调用会阻塞当前进程,直到以下任一情况发生:- 至少有一个注册的文件描述符上的 I/O 事件被触发。
- 超时时间到达。
-
事件通知:当 I/O 事件发生时,内核会将这些事件的信息填充到
events
数组中。每个epoll_event
结构包含了事件类型和与事件关联的文件描述符。 -
非阻塞和超时:
epoll_wait()
支持非阻塞调用和超时机制,这使得应用程序可以根据需要灵活地控制等待行为。 -
事件处理:应用程序需要遍历
events
数组,检查每个事件,并根据事件类型执行相应的处理逻辑。
(6)、使用示例
#include <sys/epoll.h>
#include <stdio.h>int main() {int epfd = epoll_create(1); // 假设已经创建并设置好 epoll 实例if (epfd == -1) {perror("epoll_create failed");return 1;}struct epoll_event events[10];int nfds = epoll_wait(epfd, events, 10, -1); // 无限期等待for (int i = 0; i < nfds; ++i) {if (events[i].events & EPOLLIN) {printf("EPOLLIN event on fd %d\n", events[i].data.fd);}// 处理其他事件...}close(epfd);return 0;
}
在这个示例中,epoll_wait()
被用来等待最多 10 个事件,无限期地阻塞直到至少有一个事件发生。当事件发生时,程序会打印出触发 EPOLLIN
事件的文件描述符。
epoll_wait()
是 epoll 机制中用于事件通知的关键函数,它使得应用程序能够以事件驱动的方式高效地处理 I/O 操作。
四、节点详细介绍
在 epoll 实例中,确实存在一棵红黑树用于存储所有注册的事件,同时还有一个双向链表用于管理那些已经就绪的事件。虽然在概念上我们可能会将它们分开来理解,但实际上,这两个数据结构是共享节点的。
这意味着,对于某个特定的节点 epi
,它可能同时存在于红黑树中,表示它是一个注册的事件,并且也可能位于双向链表 rdllist
中,表示它是一个已经就绪的事件。这种设计允许内核高效地在两个列表之间移动事件节点:当事件发生并准备就绪时,节点从红黑树移动到双向链表;当应用程序处理完事件后,节点可能再次回到红黑树中等待下一次就绪。
因此,尽管在画图或描述时可能会将红黑树和双向链表分开展示,但在 epoll 的实际实现中,节点 epi
是多面性的,它们在不同的上下文中扮演不同的角色,但物理上是同一个实体。这种设计优化了内存的使用,并且减少了在数据结构之间复制或同步数据的需要。
也就是说,epitem
作为 epoll 实例中的基本数据单元,既充当了红黑树的节点,也充当了双向链表的节点。这种设计使得我们能够利用红黑树的高效查找特性,在平均时间复杂度为 O(log n) 的情况下,通过文件描述符(fd)快速定位到具体的事件。
同时,当事件变得可操作(即处于就绪状态)时,epitem
节点会被移动到双向链表中,这允许我们在 O(K) 的时间复杂度内遍历和检索所有已就绪的事件,其中 K 是就绪事件的数量。这种方法的优点在于,我们不需要为已就绪的事件分配额外的存储空间,因为相同的 epitem
节点在两个数据结构中被重用,既维护了事件的注册信息,也管理了就绪状态。
这种高效的数据结构设计,使得 epoll 在处理大量并发 I/O 事件时,能够保持高性能,同时优化内存使用,是 epoll 成为高效 I/O 事件通知机制的关键因素之一。
五 、工作流程
当使用 epoll 时,内核会为每个监视的文件描述符创建一个 struct epitem
对象并挂载到一颗红黑树上,以实现快速检索。
文件描述符就绪时,内核会将对应的 epitem 添加到一个就绪链表中。
当调用 epoll_wait()
时,只需要遍历就绪链表即可获取到所有就绪事件,避免了 select/poll 中的大量无谓遍历。
整体来看,epoll 的工作流程分为以下几个步骤:
1、调用 epoll_create()
创建一个 eventpoll 对象。此时内部的
RBTree、双向链表均为空。
2、调用 epoll_ctl()
将要监视的文件描述符添加到红黑树中。并将
EPOLL_CTL_ADD 传入,将
socketfd、事件等信息注册至
epoll 中。
-
首先,为 epitem 分配空间
-
添加等待事件到 socket 的等待队列中,这个等待队列是 Linux TCP/IP 实现的一部分,并添加回调函数 ep_poll_callback
-
将 epitem 插入至红黑树中,并且以 socketfd 为 key,使得我 们能够在 O(logn) 的时间复杂度查找到 socketfd 对应的节点
3、当文件描述符就绪时,内核会将其对应的 epitem 添加到就绪链表。
- 当 socketfd 上有可读、可写事件发生时,内核将调用先前注册的回调函数,也就是 ep_poll_callback。
- 该函数做的事 情就是将 epitem 节点添加至 rdllist 双向链表中,表示事件已就绪。
4、调用 epoll_wait()
时,直接从就绪链表中获取事件。
-
当我们调用 epoll_wait() 时,该函数会将 rdllist 中的数据拷贝至我们传入的 evlist 中,并从双向链表中移除该节点。
-
若此时 rdllist 为空,那么 epoll_wait() 调用将一直阻塞,直到所管理的 scoketfd 上有事件发生为止。
通过这种设计,epoll 避免了传统模型中对整个文件描述符集合的遍历,从而提高了效率。同时,红黑树和链表数据结构也保证了事件添加和获取的高效性。
六、示例演示
为了更直观地理解 epoll 的工作流程,我们来编写一个简单的 Demo 程序。该程序启动一个服务器端,监听客户端连接和数据发送事件。
服务器端代码如下:
#include <sys/epoll.h>
#include <iostream>
#include <vector>
#include <unistd.h>
#include <arpa/inet.h>const int MAX_EVENTS = 10;
const int PORT = 8888;int main() {// 创建 epoll 实例int epollfd = epoll_create1(0);if (epollfd == -1) {std::cerr << "Failed to create epoll instance" << std::endl;return 1;}// 创建并绑定套接字int listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd == -1) {std::cerr << "Failed to create socket" << std::endl;return 1;}sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(PORT);if (bind(listenfd, (sockaddr*)&addr, sizeof(addr)) == -1) {std::cerr << "Failed to bind socket" << std::endl;return 1;}// 监听套接字if (listen(listenfd, SOMAXCONN) == -1) {std::cerr << "Failed to listen socket" << std::endl;return 1;}// 将监听套接字添加到 epoll 实例中epoll_event ev;ev.events = EPOLLIN;ev.data.fd = listenfd;if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {std::cerr << "Failed to add listen socket to epoll instance" << std::endl;return 1;}std::vector<epoll_event> events(MAX_EVENTS);while (true) {// 等待事件发生int nfds = epoll_wait(epollfd, events.data(), MAX_EVENTS, -1);if (nfds == -1) {std::cerr << "Failed in epoll_wait" << std::endl;break;}// 处理就绪事件for (int i = 0; i < nfds; ++i) {if (events[i].data.fd == listenfd) {// 新连接事件sockaddr_in clientaddr;socklen_t addrlen = sizeof(clientaddr);int connfd = accept(listenfd, (sockaddr*)&clientaddr, &addrlen);if (connfd == -1) {std::cerr << "Failed to accept connection" << std::endl;continue;}std::cout << "New connection from " << inet_ntoa(clientaddr.sin_addr) << ":"<< ntohs(clientaddr.sin_port) << std::endl;// 将新连接// 将新连接添加到 epoll 实例中ev.events = EPOLLIN | EPOLLONESHOT;ev.data.fd = connfd;if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1) {std::cerr << "Failed to add new connection socket to epoll instance" << std::endl;close(connfd);continue;}} else {// 数据事件char buffer[1024];ssize_t bytesRead = read(events[i].data.fd, buffer, sizeof(buffer));if (bytesRead > 0) {std::cout << "Received data: " << buffer << std::endl;// 回显数据ssize_t bytesWritten = write(events[i].data.fd, buffer, bytesRead);if (bytesWritten != bytesRead) {std::cerr << "Failed to write data" << std::endl;}} else if (bytesRead == 0) {// 客户端断开连接std::cout << "Client disconnected" << std::endl;close(events[i].data.fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);} else {std::cerr << "Failed to read data" << std::endl;close(events[i].data.fd);epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);}}}}close(listenfd);return 0;
}
在上面的代码中,我们首先使用 epoll_create1()
创建了一个 epoll 实例,然后创建并绑定了一个监听套接字。接着,我们使用 epoll_ctl()
将监听套接字添加到 epoll 实例中,监视 EPOLLIN 事件(可读事件)。
进入主循环后,我们调用 epoll_wait()
等待就绪事件的发生。当有新的连接到来时,我们使用 accept()
接受该连接,然后再次调用 epoll_ctl()
将新的连接套接字添加到 epoll 实例中。这里,我们使用了 EPOLLONESHOT 标志,确保每个连接套接字只被监视一次。
当有数据到达时,我们使用 read()
读取数据,并将数据回显给客户端。如果客户端断开连接,我们使用 epoll_ctl()
将该连接从 epoll 实例中移除。
通过这个示例,我们可以看到 epoll 的使用方式以及它在处理高并发网络事件时的高效性。与传统的 select/poll 模型相比,epoll 避免了对整个文件描述符集合的遍历,极大地提高了性能。
七、Epoll 的未来展望
虽然 epoll 已经是目前 Linux 下最高效的 IO 复用模型,但它仍有一些需要进一步改进的地方:
- 更高效的数据结构:尽管红黑树和双向链表已经足够高效,但是在某些极端情况下,它们的性能可能会受到影响。更高效的数据结构(如无锁队列、无锁散列表等)可以进一步提升 epoll 的性能。
- 自适应扩展:目前 epoll 实例中的红黑树和就绪链表是固定大小的,在某些场景下可能会导致内存浪费或者性能下降。实现自适应扩展机制可以动态调整这些数据结构的大小,提高资源利用率。
- 更优秀的通知机制:尽管 epoll 已经采用了事件通知机制,但在某些情况下(如网络中断等),通知可能会被延迟或者丢失,导致性能下降。改进通知机制可以确保事件得到及时、可靠的通知。
- 硬件辅助支持:随着硬件技术的不断进步,未来可能会出现专门为epoll等高性能IO模型设计的硬件加速器,进一步提升系统性能。
总之,epoll 虽然已经非常优秀,但仍有进一步改进的空间。相信在未来,随着操作系统内核和硬件技术的发展,epoll 及其后继者一定能为我们带来更加高效、强大的Linux网络编程体验!
这是我对 epoll 原理的一点点剖析,当然还有很多细节需要我们继续探索。对于热爱编程、追求卓越性能的码农们来说,这无疑是一条充满乐趣与挑战的修行之路。保持好奇心,持续学习,定能在高性能编程的道路上越走越远!
相关文章:
Socket编程权威指南(四)彻底解密 Epoll 原理
在上一篇文章中,我们优化了基于 Socket 的网络服务器,从最初的 select/poll 模型进化到了高效的 epoll。很多读者对 epoll 的惊人性能表示极大的兴趣,对它的工作原理也充满了好奇。今天,就让我们一起揭开 epoll 神秘的面纱&#x…...
Windows开始ssh服务+密钥登录+默认启用powershell
文章内所有的命令都在power shell内执行,使用右键单击Windows徽标,选择终端管理员即可打开 Windows下OpenSSH的安装 打开Windows power shell,检查SSH服务的安装状态。会返回SSH客户端和服务器的安装状态,一下是两个都安装成功的…...
实体商铺私域流量打造策略:从引流到转化的全链路解析
在数字化时代,实体商铺面临着前所未有的挑战与机遇。随着线上购物的兴起,传统商铺如何吸引并留住顾客,成为了每个实体店家必须面对的问题。私域流量的打造,正是解决这一问题的关键所在。本文将从引流、留存、转化三个方面…...
实战 | 通过微调SegFormer改进车道检测效果(数据集 + 源码)
背景介绍 SegFormer:实例分割在自动驾驶汽车技术的快速发展中发挥了关键作用。对于任何在道路上行驶的车辆来说,车道检测都是必不可少的。车道是道路上的标记,有助于区分道路上可行驶区域和不可行驶区域。车道检测算法有很多种,每…...
翻译《The Old New Thing》- Why do messages posted by PostThreadMessage disappear?
Why do messages posted by PostThreadMessage disappear? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20090930-00/?p16553 Raymond Chen 2008年09月30日 为什么 PostThreadMessage 发布的信息会消失? 在显示用户界面的线…...
【深度学习】—— 神经网络介绍
神经网络介绍 本系列主要是吴恩达深度学习系列视频的笔记,传送门:https://www.coursera.org/deeplearning-ai 目录 神经网络介绍神经网络的应用深度学习兴起的原因 神经网络,全称人工神经网络(Artificial Neural Network…...
python-数字黑洞
[题目描述] 给定一个三位数,要求各位不能相同。例如,352是符合要求的,112是不符合要求的。将这个三位数的三个数字重新排列,得到的最大的数,减去得到的最小的数,形成一个新的三位数。对这个新的三位数可以重…...
SpringCloud 负载均衡 spring-cloud-starter-loadbalancer
简述 spring-cloud-starter-loadbalancer 是 Spring Cloud 中的一个组件,它提供了客户端负载均衡的功能。在 Spring Cloud 的早期版本中,Netflix Ribbon 被广泛用作客户端负载均衡器,但随着时间推移和 Netflix Ribbon 进入维护模式ÿ…...
牛客周赛-46
牛客周赛-46 a乐奈吃冰b素世喝茶c爱音开灯d小灯做题 a乐奈吃冰 ac code #include<iostream> using namespace std; int main(){long long a,b;cin>>a>>b;int tmpmin(b,a/2);long long resatmp;cout<<res;return 0; }b素世喝茶 #include<iostream…...
多模态vlm综述:An Introduction to Vision-Language Modeling 论文解读
目录 1、基于对比学习的VLMs 1.1 CLIP 2、基于mask的VLMs 2.1 FLAVA 2.2 MaskVLM 2.3 关于VLM目标的信息理论视角 3、基于生成的VLM 3.1 学习文本生成器的例子: 3.2 多模态生成模型的示例: 3.3 使用生成的文本到图像模型进行下游视觉语言任务 4、 基于预训练主干网…...
28.找零
上海市计算机学会竞赛平台 | YACSYACS 是由上海市计算机学会于2019年发起的活动,旨在激发青少年对学习人工智能与算法设计的热情与兴趣,提升青少年科学素养,引导青少年投身创新发现和科研实践活动。https://www.iai.sh.cn/problem/744 题目描述 有一台自动售票机,每张票卖 …...
[方法] 《鸣潮》/《原神》呼出与锁定光标的功能细节
本方法适用于Cinemachine - FreeLook。 1. 锁定与呼出光标的功能实现 // 锁定光标 private void LockMouse() {// 将光标锁定在屏幕中间Cursor.lockState CursorLockMode.Locked;// 隐藏光标Cursor.visible false; }// 呼出光标 private void UnLockMouse() {// 释放光标Cu…...
计算机网络-NAT配置与ACL
目录 一、ACL 1、ACL概述 2、ACL的作用 3、ACL的分类 4、ACL的配置格式 二、NAT 1、NAT概述 2、NAT分类 2.1 、 静态NAT 2.2 、 动态NAT 3、NAT的功能 4、NAT的工作原理 三、NAT配置 1、静态NAT配置 2、动态NAT配置 四、总结 一、ACL 1、ACL概述 ACLÿ…...
哈尔滨三级等保测评需要测哪些设备?
哈尔滨三级等保测评需要测的设备,主要包括物理安全设备、网络安全设备和应用安全设备三大类别。这些设备在保障哈尔滨地区信息系统安全方面发挥着至关重要的作用。 首先,物理安全设备是确保信息系统实体安全的基础。在哈尔滨三级等保测评中,物…...
大学体育(二)(华中科技大学) 中国大学MOOC答案2024版100分完整版
大学体育(二)(华中科技大学) 中国大学MOOC答案2024版100分完整版 有氧运动 有氧运动单元测验 1、 世界卫生组织对18-64岁年龄组成年人的运动建议是:每周至少( )分钟的中等强度有氧身体活动,或者每周至少&a…...
Web前端策划:从理念到实现的全方位解析
Web前端策划:从理念到实现的全方位解析 在数字化时代的浪潮中,Web前端策划作为连接技术与用户界面的桥梁,扮演着至关重要的角色。它涉及从用户需求分析、设计构思到技术实现的全方位过程,要求策划者具备深厚的技术功底和敏锐的市…...
经济与安全兼顾:茶饮店购买可燃气体报警器的价格考量
可燃气体报警器在如今的社会中扮演着至关重要的角色。它们用于检测环境中的可燃气体浓度,及早发现潜在的火灾隐患,保护人们的生命和财产安全。 在这篇文章中,佰德将介绍可燃气体报警器的安装、检定以及价格,通过实际案例和数据&a…...
鞠小云张霖浩闪耀北京广播电视台春晚发布会,豪门姐弟感爆棚
昨日,2025年北京广播电视台“追梦春晚”全国海选发布会在杭州举行,中国内地青年女演员鞠小云同人气幕后张霖浩,受主办方盛情邀请出席本次活动。从现场流露出的照片中可以看出,鞠小云一袭白色长裙灵动温婉素雅,而张霖浩…...
java Function 用法
**Function 接口是 Java 8 引入的一个核心函数式接口,用于表示一个接受单一输入参数并产生结果的函数**。Function 接口主要用在数据处理和转换操作中,如集合处理、流处理等场景。下面将深入探讨 Function 接口的用法: 1. **基本概念**&…...
LabVIEW与Python的比较及联合开发
LabVIEW和Python在工业自动化和数据处理领域各具优势,联合开发可以充分发挥两者的优点。本文将从语言特性、开发效率、应用场景等多个角度进行比较,并详细介绍如何实现LabVIEW与Python的联合开发。 语言特性 LabVIEW 图形化编程:LabVIEW使用…...
RAG技术在教育领域的应用
一、引言 点击可以查看最新资源 随着人工智能技术的飞速发展,教育领域正迎来一场深刻的变革。大型语言模型(LLM)和检索增强生成(Retrieval-Augmented Generation,RAG)技术的结合,为教育领域注入…...
玉米粒计数检测数据集VOC+YOLO格式107张1类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):107 标注数量(xml文件个数):107 标注数量(txt文件个数):107 标注类别…...
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0.
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0. 🌈 欢迎莅临我的个人主页👈这里是我深耕Python编程、机器学习和自然语言处理(NLP)领域,并乐于分享知识与经验的小天地!Ἰ…...
stm32之USMART调试组件的使用
一、什么是USMART? USMART是正点原子团队为其STM32开发平台开发的一种类似linux的shell的调试工具。具体工作过程是通过串口发送命令给单片机,然后单片机收到命令之后调用单片机里面对应的相关函数,并执行,同时支持返回结果。 二、USMART调…...
【Python】成功解决TypeError: ‘int’ object is not iterable
【Python】成功解决TypeError: ‘int’ object is not iterable 🌈 欢迎莅临我的个人主页👈这里是我深耕Python编程、机器学习和自然语言处理(NLP)领域,并乐于分享知识与经验的小天地!🎇 &#…...
【日常记录】【JS】中文转拼音的库 pinyin-pro
文章目录 1、介绍2、pinyin-pro 基本使用3、参考链接 1、介绍 pinyin-pro 是一个专业的 JavaScript 中文转拼音的库,具备多音字识别准确、体积轻量、性能优异、功能丰富等特点。 常用的案例 搜索功能增强:在输入框输入汉字时,可以转化为拼音输…...
CopyOnWriteArrayList详解
目录 CopyOnWriteArrayList详解1、CopyOnWriteArrayList简介2、如何理解"写时复制"3、CopyOnWriteArrayList的继承体系4、CopyOnWriteArrayList的构造函数5、CopyOnWriteArrayList的使用示例6、CopyOnWriteArrayList 的 add方法7、CopyOnWriteArrayList弱一致性的体现…...
CUDA 编程(1):使用Grid 和 Block分配线程
1 介绍 1.1 Grid 和 Block 概念 核函数以线程为单位进行计算的函数,cuda编程会涉及到大量的线程(thread),几千个到几万个thread同时并行计算,所有的thread其实都是在执行同一个核函数。 对于核函数(Kernel),一个核函数一般会分配1个Grid, 1个Grid又有很多个Block,1个Bloc…...
ArcGIS for js 4.x FeatureLayer 加载、点选、高亮
安装arcgis for js 4.x 依赖: npm install arcgis/core 一、FeatureLayer 加载 代码如下: <template><view id"mapView"></view></template><script setup>import "arcgis/core/assets/esri/themes/li…...
倩女幽魂手游攻略:云手机自动搬砖辅助教程!
《倩女幽魂》手游自问世以来一直备受玩家喜爱,其精美画面和丰富的游戏内容让人沉迷其中。而如今,借助VMOS云手机,玩家可以更轻松地进行搬砖,提升游戏体验。 一、准备工作 下载VMOS云手机: 在PC端或移动端下载并安装VM…...
坑梓网站建设咨询/新疆头条今日头条新闻
方法一:删除注册表选项,变为评估模式 1.windowR打开管理 2.输入regedit后回车,则就会打开注册表编辑器 3.里面有一个cacaheId然后删掉,果断的删掉,然后重新打开即可 方法二:删除动态库文件 4.找到Beyo…...
做app推广上哪些网站吗/2345浏览器网站进入
1001:枚举保留题目的集合,然后模拟(二进制状态压缩 暴力所选的问题k 0~1<<m-1。再把n个人的状态进行按位&) 1002:直接在输入的时候扫一遍就行了,顺便用前缀和统计出每个字母的出现次数 ÿ…...
专业手机移动网站建设/关键词上首页的有效方法
由OpenDigg 出品的安卓开源项目周报第十五期来啦。我们的安卓开源周报集合了OpenDigg一周来新收录的优质的安卓开源项目,方便安卓开发人员便捷的找到自己需要的项目工具。 spruce-android 在屏幕上设计动画 AdaptiveTableLayout 读取和写入CSV文件 ShadowImageView …...
个人做流量大的网站/成都最新动态
业务逻辑:表myupload做了增加、修改、删除时,在表myuploaddaybook新增一条用来记录myupload表变化的行。myupload表CREATE TABLE myupload (uploadid int(11) NOT NULL AUTO_INCREMENT,bookid int(11) DEFAULT NULL,numb int(11) DEFAULT NULL,saleuseri…...
鳌江哪里有做网站/深圳竞价托管
参考视频:慕课网 代理的概念在生活中经常出现,比如火车票代售点和黄牛,他们都充当了代理卖票的角色,只是他们还有其他功能上的区别。 代理分为静态代理和动态代理。动态代理又分为JDK动态代理和cglib代理。 所谓的代理模式是指…...
宁波电器网站制作/社交网络推广方法有哪些
mybatis3中增加了使用注解来配置Mapper的新特性,本篇文章主要介绍其中几个Provider的使用方式,他们是:SelectProvider、UpdateProvider、InsertProvider和DeleteProvider。MyBatis 3 User Guide中的最后一章描述了注解的简单用法,…...