兰州网站优化公司,it运维工程师简历,郑州网站建设知名公司排名,wordpress菜单外观样式目录
前言
一#xff0c;epoll的三个系统调用接口
1.1.epoll_create函数
1.1.1.epoll_create函数干了什么
1.2. epoll_ctl函数
1.2.1.epoll_ctl函数函数干了什么 1.3.epoll_wait函数 1.3.1.epoll_wait到底干了什么
1.4.epoll的工作过程中内核在干什么
二#xff0c;…目录
前言
一epoll的三个系统调用接口
1.1.epoll_create函数
1.1.1.epoll_create函数干了什么
1.2. epoll_ctl函数
1.2.1.epoll_ctl函数函数干了什么 1.3.epoll_wait函数 1.3.1.epoll_wait到底干了什么
1.4.epoll的工作过程中内核在干什么
二epoll高效的原理
2.1.预备知识的储备 2.2.内核接受网络数据的全过程 2.3.一些小问题 三简陋版本epoll版本TCP服务器
3.1.准备工作
3.2.EpollServer.hpp
3.3.源代码 前言
我们得知道epoll是epoll的意思很明显epoll就是poll的升级版本 epoll 是对 select 和 poll 的改进解决了“性能开销大”和“文件描述符数量少”这两个缺点是性能最高的多路复用实现方式能支持的并发量也是最大。 epoll是Linux内核为处理大批量文件描述符而作了改进的poll是Linux下多路复用IO接口select/poll的增强版本它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。 另一点原因就是获取事件的时候它无须遍历整个被侦听的描述符集只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发Level Triggered外还提供了边缘触发Edge Triggered这就使得用户空间程序有可能缓存IO状态减少epoll_wait/epoll_pwait的调用提高应用程序效率。
一epoll的三个系统调用接口
epoll的使用流程如下
创建 epoll 实例通过 epoll_create 创建一个 epoll 文件描述符。添加文件描述符到 epoll 实例使用 epoll_ctl 将需要监视的文件描述符如套接字添加到 epoll 实例中并指定关心的事件类型如可读、可写等。等待事件发生通过 epoll_wait 或 epoll_pwait 等待文件描述符上发生指定的事件。这些函数会阻塞调用线程直到有文件描述符上的事件发生或者超时。处理事件根据 epoll_wait 或 epoll_pwait 返回的就绪事件列表处理相应的文件描述符上的事件。 接下来我们就要好好认识这些接口
1.1.epoll_create函数 epoll_create函数用于创建epoll文件描述符该文件描述符用于后续的epoll操作。
参数
size:目前内核还没有实际使用只要大于0就行
返回值
返回epoll文件描述符
1.1.1.epoll_create函数干了什么
epoll需要使用一个额外的文件描述符epoll文件描述符来唯一标识内核中的这个事件表。 这个文件描述符使用如下epoll_create函数来创建epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。调用epoll_create时内核除了帮我们在epoll文件系统里建了个file结点epoll_create创建的文件描述符在内核cache里建了个 红黑树用于存储以后epoll_ctl传来的socket外还会再建立一个list链表用于存储准备就绪的事件.概括就是调用epoll_create方法时内核会跟着创建一个eventpoll对象 eventpoll对象也是文件系统中的一员和socket一样它也会有等待队列。 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进行链接起来发送至用户空间
} 在 Linux 内核中struct eventpoll 是一个关键的数据结构用于实现 epoll 机制。这个结构体管理了与 epoll 相关的所有资源和状态。 下面是对 struct eventpoll 结构体的详细解释 spin_lock_t lock;这是一个自旋锁用于保护对 eventpoll 结构体的并发访问。自旋锁在持有锁的线程或进程释放锁之前会忙等待即持续检查锁的状态而不是像互斥锁mutex那样将线程阻塞。这使得自旋锁在预期锁持有时间非常短的情况下非常高效。struct mutex mtx;这是一个互斥锁用于防止在 eventpoll 结构体被使用时被删除。与自旋锁不同互斥锁在无法立即获取锁时会将线程阻塞直到锁被释放。这在锁持有时间较长或等待锁释放的线程可能会进行较长时间的睡眠时更为合适。wait_queue_head_t wq;这是一个等待队列的头节点用于 sys_epoll_wait() 系统调用。当没有事件就绪时调用 epoll_wait 的进程会被挂起在这个等待队列上直到有事件发生或超时。wait_queue_head_t poll_wait;这是另一个等待队列的头节点通常用于支持传统的 poll() 或 select() 系统调用。如果文件描述符如套接字同时被添加到 epoll 实例和传统的轮询机制中这个等待队列就会被用到。struct list_head rdllist;这是一个链表头用于存储已经准备好进行 I/O 操作即事件已就绪的文件描述符。这个链表在 epoll_wait 调用返回给用户空间之前被填充。struct rb_root rbr;这是红黑树的根节点用于高效地存储和管理所有添加到 epoll 实例中的文件描述符。红黑树是一种自平衡的二叉搜索树它能够在对数时间内完成插入、删除和查找操作。struct epitem *ovflist;这个成员看起来是用于某种溢出处理的链表头。在某些情况下如果 epoll 事件表满了尽管实际上这种情况很少发生因为 epoll 支持的文件描述符数量通常很大可能需要一种机制来暂存额外的事件。然而标准的 Linux epoll 实现中可能并不直接使用这个成员或者它可能用于特定的内核版本或定制的内核模块中。 请注意随着 Linux 内核的发展struct eventpoll 结构体的具体实现和成员可能会发生变化。 struct list_head rdllist; 和 struct rb_root rbr; 确实是 epoll 机制中常提及的关键组成部分它们分别对应于 epoll 中的“就绪链表”和“红黑树”。 在epoll机制中“就绪链表”和“红黑树”各自扮演着关键角色它们共同协作以实现高效的事件通知和文件描述符管理。 就绪链表是epoll中的一个关键数据结构它主要用于存储那些已经准备好进行I/O操作即事件已就绪的文件描述符。当epoll监控的文件描述符红黑树上面的上有I/O事件发生时相应的epoll_event会被添加到这个链表中。epoll_wait函数在调用时会检查这个就绪链表如果有事件存在则将这些事件复制到用户空间提供的数组中并返回事件的数量。就绪链表的使用大大提高了事件通知的效率因为它避免了不必要的文件描述符扫描。红黑树是一种自平衡的二叉搜索树它在epoll中用于高效地存储和管理所有添加到epoll实例中的文件描述符。每个文件描述符在红黑树中都有一个对应的节点这些节点按照文件描述符的值进行排序。红黑树提供了快速的查找、插入和删除操作这些操作的时间复杂度都是对数的即O(log n)其中n是树中节点的数量。这使得epoll能够在处理大量文件描述符时保持较高的性能。 在epoll中红黑树的主要作用包括 快速查找当需要检查某个文件描述符是否已经被添加到epoll实例中时可以通过红黑树快速定位到该文件描述符对应的节点。高效插入和删除当向epoll实例中添加或删除文件描述符时红黑树能够确保这些操作在对数时间内完成从而保持较高的性能。有序管理红黑树中的节点按照文件描述符的值进行排序这有助于实现有序的文件描述符管理虽然epoll本身并不直接依赖于文件描述符的顺序但在某些情况下有序性可能有助于优化性能或简化处理逻辑。 总结 在epoll机制中“就绪链表”和“红黑树”是两个相辅相成的数据结构。 就绪链表用于存储已经准备好进行I/O操作的文件描述符以便epoll_wait函数能够快速返回这些事件而红黑树则用于高效地存储和管理所有添加到epoll实例中的文件描述符以确保查找、插入和删除操作的高效性。这两个数据结构的结合使得epoll能够在处理大量并发连接时保持较高的性能。 当调用 epoll_create 时内核会执行一系列操作来创建一个新的 epoll 实例并为其分配必要的资源。以下是内核中发生的主要步骤
分配 eventpoll 对象内核首先会分配一个 struct eventpoll 类型的对象。这个对象将用于存储与 epoll 实例相关的所有信息包括锁、等待队列、就绪事件列表、红黑树等。初始化数据结构对 struct eventpoll 对象进行初始化包括设置锁、等待队列、就绪事件列表rdllist和红黑树rbr等成员变量的初始状态。分配文件描述符内核会分配一个未使用的文件描述符fd并将其与新建的 struct eventpoll 对象关联起来。这个文件描述符将作为用户空间与内核中 epoll 实例通信的接口。创建文件对象创建一个 struct file 类型的对象并将其与 struct eventpoll 对象关联。这个 struct file 对象将包含对 eventpoll 对象的引用以及一系列文件操作函数如 file_operations这些函数定义了针对 epoll 文件描述符的各种操作如读、写、控制等。注册文件操作将 eventpoll_fops一个包含 epoll 相关文件操作函数的结构体设置为 struct file 对象的 f_op 成员。这样当用户空间通过文件描述符对 epoll 实例执行操作时内核就会调用这些预定义的函数来处理。将文件描述符添加到进程的文件描述符表将新分配的文件描述符添加到当前进程的文件描述符表中以便用户空间可以通过标准的文件描述符操作如 read、write、close 等来访问 epoll 实例。返回文件描述符最后epoll_create 系统调用将新分配的文件描述符返回给用户空间。用户空间程序可以使用这个文件描述符来调用其他 epoll 相关的系统调用如 epoll_ctl、epoll_wait 等以添加要监视的文件描述符、等待事件发生以及处理就绪事件。
总结来说当调用 epoll_create 时内核会创建一个新的 epoll 实例并为其分配和初始化必要的资源。这个实例通过一个特殊的文件描述符与用户空间进行交互允许用户空间程序高效地监视和处理多个文件描述符上的 I/O 事件。
我们只需要知道这个epoll_create会返回一个epoll文件描述符即可。 这个文件描述符也会占用一个fd值在linux下如果查看/proc/进程id/fd/能够看到这个fd所以在使用完epoll后必须调用close()关闭否则可能导致fd被耗尽。
1.2. epoll_ctl函数 epoll_ctl 函数是 Linux 下 epoll 接口的一个重要组成部分它用于向 epoll 实例注册、修改或删除文件描述符及其关联的事件。
参数解释
int epfd这是由 epoll_create 函数返回的文件描述符用于标识一个 epoll 实例。通过这个文件描述符用户空间程序可以与内核中的 epoll 实例进行通信。int op这个参数指定了要执行的操作类型它是一个宏决定了 epoll_ctl 函数的具体行为。常见的操作类型包括
EPOLL_CTL_ADD向 epoll 实例注册一个新的文件描述符及其事件。EPOLL_CTL_MOD修改已经注册到 epoll 实例中的文件描述符的事件。EPOLL_CTL_DEL从 epoll 实例中删除一个文件描述符及其事件。 int fd这是要操作的目标文件描述符即用户希望注册、修改或删除的文件描述符。这个文件描述符可以是一个已打开的套接字、管道等。struct epoll_event *event这是一个指向 struct epoll_event 结构体的指针用于指定要注册或修改的事件信息。这个结构体包含了事件的类型如可读、可写、错误等和与该事件相关联的数据。如果操作是删除EPOLL_CTL_DEL则这个参数可以为 NULL因为删除操作不需要指定事件信息。 结构体 epoll_event #include sys/epoll.h// 定义epoll_data_t为union类型
typedef union epoll_data { void *ptr; // 可以指向任何类型的数据 int fd; // 套接字文件描述符 uint32_t u32; // 32位无符号整数 uint64_t u64; // 64位无符号整数
} epoll_data_t; // 定义epoll_event结构体
struct epoll_event { uint32_t events; // epoll事件参考事件列表如EPOLLIN, EPOLLOUT等 epoll_data_t data; // 关联的数据可以是文件描述符、指针或其他
}; struct epoll_event 结构体通常包含以下成员 events这是一个位掩码用于指定事件的类型。常见的类型包括 EPOLLIN可读事件、EPOLLOUT可写事件、EPOLLERR错误事件等。多个事件类型可以通过位或操作符|组合在一起。data这是一个联合体可以包含不同类型的数据。在实际使用中它通常用于存储文件描述符或与事件相关联的用户定义数据。当事件被触发时这些信息会被原样返回给用户空间程序。 epoll事件——events成员 头文件sys/epoll.henum EPOLL_EVENTS
{EPOLLIN 0x001, //读事件EPOLLPRI 0x002,EPOLLOUT 0x004, //写事件EPOLLRDNORM 0x040,EPOLLRDBAND 0x080,EPOLLWRNORM 0x100,EPOLLWRBAND 0x200,EPOLLMSG 0x400,EPOLLERR 0x008, //出错事件EPOLLHUP 0x010, //出错事件EPOLLRDHUP 0x2000,EPOLLEXCLUSIVE 1u 28,EPOLLWAKEUP 1u 29,EPOLLONESHOT 1u 30,EPOLLET 1u 31 //边缘触发}; 返回值
如果 epoll_ctl 函数执行成功则返回 0。如果执行失败则返回 -1并设置 errno 以指示错误原因。
1.2.1.epoll_ctl函数函数干了什么 epoll_ctl函数用于增加删除修改epoll事件epoll事件会存储于内核epoll结构体红黑树中。这个也是epoll的事件注册函数epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件返回0表示成功否则返回–1此时需要根据errno错误码判断错误类型。它不同与select()是在监听事件时告诉内核要监听什么类型的事件而是在这里先注册要监听的事件类型。 epoll_ctl 函数是 epoll 接口中用于增加、删除或修改 epoll 监控的事件的系统调用。当你对一个 epoll 实例执行 epoll_ctl 操作时内核会根据操作类型增加、删除或修改来更新内部的数据结构特别是红黑树RB-tree和就绪事件列表rdllist。 在 epoll 的上下文中红黑树主要用于高效地存储和查找所有添加到 epoll 实例中的文件描述符fd及其对应的事件。每个文件描述符在红黑树中都有一个对应的节点通常是 struct epitem 类型的结构体这些节点按照文件描述符的值进行排序以便于快速查找。
当你通过 epoll_ctl 向 epoll 实例添加一个新的文件描述符和事件时内核会做以下几件事
分配并初始化 epitem 结构体为每个新添加的文件描述符分配一个 epitem 结构体并初始化其成员变量包括指向文件描述符的指针、事件类型、回调函数等。将 epitem 添加到红黑树中根据文件描述符的值将新的 epitem 结构体插入到红黑树中。这保证了文件描述符的快速查找和排序。 当你通过 epoll_ctl 删除一个事件时内核会从红黑树中找到对应的 epitem 结构体并将其从树中删除。同时如果该事件已经在就绪事件列表中也需要从列表中删除它。 修改事件通常意味着更改事件的某些属性如事件类型这可能需要从红黑树中找到对应的 epitem 结构体并更新其成员变量。但是修改操作通常不会改变文件描述符在红黑树中的位置。 就绪事件列表rdllist用于存储那些已经满足条件即事件已经发生的文件描述符。当 epoll_wait 被调用时内核会遍历红黑树中的 epitem 结构体检查是否有事件已经就绪并将它们从红黑树中移动到就绪事件列表中。然后epoll_wait 会返回这些就绪事件的列表给用户空间。 需要注意的是虽然红黑树是 epoll 实现中的一个关键数据结构但 epoll 的高效性并不仅仅依赖于红黑树。epoll 还使用了其他技术如内存映射memory-mapped的就绪事件列表、边缘触发edge-triggered和水平触发level-triggered事件模式等来优化事件通知和减少系统调用的开销。 1.3.epoll_wait函数 注意在调用 epoll_wait 之前必须先通过 epoll_ctl 向 epoll 实例注册文件描述符及其事件。 等待事件的产生类似于select()调用。 参数events用来从内核得到事件的集合maxevents告之内核这个events有多大这个 maxevents的值不能大于创建epoll_create()时的size参数timeout是超时时间毫秒0会立即返回-1将不确定也有说法说是永久阻塞。
第1个参数 epfd是 epoll的描述符。也就是epoll_creat返回的文件描述符第2个参数 events则是分配好的 epoll_event结构体数组epoll将会把发生的事件复制到 events数组中events不可以是空指针内核只负责把数据复制到这个 events数组中不会去帮助我们在用户态中分配内存。内核这种做法效率很高。第3个参数指定了 events 数组的最大长度即 epoll_wait 可以告知调用者的最大事件数量。如果同时有多个事件发生时epoll_wait 将尽可能多地填充 events 数组但不会超过 maxevents 指定的数量。通常 maxevents参数与预分配的events数组的大小是相等的。第4个参数 timeout表示在没有检测到事件发生时最多等待的时间单位为毫秒如果 timeout为0则表示 epoll_wait在 rdllist链表中为空立刻返回不会等待。
返回值
已经就绪的fd的个数如返回0表示已超时。如果返回–1则表示出现错误需要检查 errno错误码判断错误类型。 1.3.1.epoll_wait到底干了什么
epoll_wait 在内核中主要执行了以下操作以实现高效的事件通知机制
1. 等待事件发生
当进程调用 epoll_wait 时它会被阻塞除非设置了非阻塞模式或超时时间直到有注册的文件描述符上发生了感兴趣的事件。epoll_wait 依赖于内核中维护的数据结构主要是红黑树和就绪链表来高效地管理这些文件描述符和它们的事件。
2. 检查就绪链表
在内核中epoll 使用了一个就绪链表就是我们上面提到的struct eventpoll里面的struct list_head rdllist通常是一个双向链表来存储那些已经准备好即发生了感兴趣的事件的文件描述符。当 epoll_wait 被调用时它会检查这个就绪链表。如果链表不为空说明有事件已经发生。
3. 复制事件到用户空间
如果就绪链表中有事件epoll_wait 会将这些事件从内核空间复制到用户空间提供的 epoll_event 结构体数组中。这个过程会尽可能多地复制事件但不超过用户指定的 maxevents 数量。
4. 更新内核状态
在复制事件后epoll_wait 会更新内核中的数据结构以反映哪些事件已经被处理。对于边缘触发ET模式如果事件已经被处理并且没有新的数据到来那么相应的文件描述符可能会被从就绪链表中移除。
5. 唤醒进程
一旦有事件被复制到用户空间epoll_wait 会唤醒调用它的进程并返回发生的事件数量。如果在调用 epoll_wait 时设置了超时时间并且在这段时间内没有事件发生那么 epoll_wait 也会超时返回。
6. 高效性实现
epoll 之所以高效主要是因为它避免了像 select 和 poll 那样的重复扫描和文件描述符限制。它使用红黑树来存储和快速查找文件描述符使用就绪链表来高效地管理就绪事件。这些数据结构使得 epoll 能够在 O(1) 的时间复杂度内完成大部分操作从而支持大规模的文件描述符和高效的事件通知。
综上所述epoll_wait 在内核中主要负责等待事件发生、检查就绪链表、复制事件到用户空间、更新内核状态以及唤醒进程等操作从而实现了高效的事件通知机制。
1.4.epoll的工作过程中内核在干什么 当某一进程调用epoll_create方法时Linux内核会创建一个eventpoll结构体这个结构体中有两个成员与epoll的使用密切相关
struct eventpoll {.../*红黑树的根节点这棵树中存储着所有添加到epoll中的事件也就是这个epoll监控的事件*/struct rb_root rbr;/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/struct list_head rdllist;...
};我们在调用epoll_create时内核除了帮我们在epoll文件系统里建了个file结点在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外还会再建立一个rdllist双向链表用于存储准备就绪的事件当epoll_wait调用时仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回没有数据就sleep等到timeout时间到后即使链表没数据也返回。所以epoll_wait非常高效。 所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback它会把这样的事件放到上面的rdllist双向链表中。 在epoll中对于每一个事件都会建立一个epitem结构体如下所示
struct epitem {...//红黑树节点struct rb_node rbn;//双向链表节点struct list_head rdllink;//事件句柄等信息struct epoll_filefd ffd;//指向其所属的eventepoll对象struct eventpoll *ep;//期待的事件类型struct epoll_event event;...
}; // 这里包含每一个事件对应着的信息。当调用epoll_wait检查是否有发生事件的连接时只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已如果rdllist链表不为空则这里的事件复制到用户态内存使用共享内存提高效率中同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时从rbr红黑树中查找事件也非常快也就是说epoll是非常高效的它可以轻易地处理百万级别的并发连接。 【总结】
一颗红黑树一张准备就绪句柄链表少量的内核cache就帮我们解决了大并发下的socket处理问题。执行epoll_create()时创建了红黑树和就绪链表执行epoll_ctl()时如果增加socket句柄则检查在红黑树中是否存在存在立即返回不存在则添加到树干上然后向内核注册回调函数用于当中断事件来临时向准备就绪链表中插入数据执行epoll_wait()时立刻返回准备就绪链表里的数据即可。 二epoll高效的原理
2.1.预备知识的储备 第一步从硬件的角度看计算机怎样接收网络数据
了解epoll本质的第一步要从硬件的角度看计算机怎样接收网络数据。
在①阶段网卡收到网线传来的数据经过②阶段的硬件电路的传输最终将数据写入到内存中的某个地址上③阶段。这个过程涉及到DMA传输、IO通路选择等硬件有关的知识但我们只需知道网卡会把接收到的数据写入内存。
通过硬件传输网卡接收的数据存放到内存中。操作系统就可以去读取它们。 问题二操作系统是怎么知道网卡是有数据了
了解epoll本质的第二步要从CPU的角度来看数据接收。要理解这个问题要先了解一个概念——中断。 计算机执行程序时会有优先级的需求。比如当计算机收到断电信号时电容可以保存少许电量供CPU运行很短的一小段时间它应立即去保存数据保存数据的程序具有较高的优先级。 一般而言由硬件产生的信号需要cpu立马做出回应不然数据可能就丢失所以它的优先级很高。cpu理应中断掉正在执行的程序去做出响应当cpu完成对硬件的响应后再重新执行用户程序。中断的过程如下图和函数调用差不多。只不过函数调用是事先定好位置而中断的位置由“信号”决定。 以键盘为例当用户按下键盘某个按键时键盘会给cpu的中断引脚发出一个高电平。cpu能够捕获这个信号然后执行键盘中断程序。 现在可以回答本节提出的问题了当网卡把数据写入到内存后网卡向cpu发出一个中断信号操作系统便能得知有新数据到来再通过网卡中断程序去处理数据。
问题三——操作系统怎么知道红黑树上的哪些节点就绪了
操作系统怎么知道红黑树上的哪些节点就绪了呢难道操作系统也要遍历整棵红黑树检测每个节点的就绪情况操作系统其实并不会这样做如果这样做的话那epoll还谈论什么高效呢你epoll不也得遍历所有的fd吗和我poll遍历有什么区别呢红黑树是查找的效率高不是遍历的效率高如果遍历所有的节点红黑树其实和链表遍历在效率上是差不多的一点都不高效 那操作系统是怎么知道红黑树上的哪个节点就绪了呢其实是通过底层的回调机制来实现的这也是epoll接口公认非常高效的重要的一个实现环节 当数据到达网卡时我们知道数据会经过硬件中断CPU执行中断向量表等步骤来让数据到达内存中的操作系统内部而所有添加到epoll模型中的事件都会与网卡建立回调关系当事件就绪时调用这个回调方法将就绪的事件链接到就绪队列当中这个回调方法在内核中叫做ep_poll_callback。 2.2.内核接受网络数据的全过程 当数据到达网络设备网卡时会以硬件中断作为发起点将中断信号通过中断设备发送到CPU的针脚接下来CPU会查讯中断向量表找到中断序号对应的驱动回调方法在回调方法内部会将数据从硬件设备网卡拷贝到软件OS里。数据包在OS中会向上贯穿协议栈到达传输层时数据会被拷贝到struct file的内核缓冲区中同时OS会执行一个叫做private_data的回调函数指针字段在该回调函数内部会通过修改红黑树节点中的就绪队列指针的内容将该节点链入到就绪队列内核告知用户哪些fd就绪时只需要将就绪队列中的节点内容拷贝到epoll_wait的输出型参数events即可这就是epoll模型的底层回调机制 2.3.一些小问题
1.为什么说epoll模型是高效的呢 因为大部分的工作操作系统都帮我们做了比如添加节点到红黑树我们只需要调用epoll_ctrl即可返回就绪的fd直接相当于返回就绪队列中的节点即可上层直接就可以拿到就绪的fd检测是否就绪的工作也不用遍历而是当底层数据就绪时会有回调机制自动将红黑树的节点链入到就绪队列中操作系统也无须遍历红黑树进行就绪检测上层在拿到就绪的fd后可以确定范围的遍历输出型参数struct epoll_event数组而不是盲目的遍历整个数组的所有元素。
2.为什么选用红黑树作为epoll模型的底层数据结构 因为红黑树的搜索效率非常的高可以达到logN的时间复杂度所以无论是epoll_ctl的插入删除还是修改这些工作的首要前提是先找到目标节点或目标位置找到之后再进行具体的操作而找到这一步红黑树的效率就非常的高。 有人可能会说红黑树需要旋转调整平衡啊虽然在逻辑上我们感觉红黑树的旋转调平衡很费时间可能会造成红黑树的效率降低但其实并不是这样的所谓的旋转调平衡只是在逻辑上复杂而已在实际操作上仅仅只是修改节点内的指针而已对红黑树的效率影响并不大。 同时红黑树对于平衡的要求并没有AVL高所以在旋转调平衡的次数上红黑树要比AVL树少很多在整体效率上是要比AVL树高的这也是使用红黑树不使用AVL树的原因。
3.epoll_wait有哪些细节
epoll_wait会将所有就绪的fd依次按照顺序放到输出型参数events中用户在遍历数组处理就绪的事件时无须遍历多余的任何一个fd只需要遍历从0到epoll_wait的返回值个fd即可。如果就绪队列的节点数量很多epoll_wait的输出型参数数组一次拿不完也不用担心因为队列是先进先出下一次在调用epoll_wait时再拿就绪的事件也可以。select poll在使用的时候都需要程序员自己维护一个第三方数组来存储用户关心的fd及事件但epoll不需要因为内核为epoll在底层维护了一棵红黑树用户直接通过epoll_ctl来对红黑树的节点进行增删改即可无须自己在应用层维护第三方的数组。 三简陋版本epoll版本TCP服务器
3.1.准备工作 Socket.hpp #pragma once #include iostream
#include string
#include unistd.h
#include cstring
#include sys/types.h
#include sys/stat.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h // 定义一些错误代码
enum
{ SocketErr 2, // 套接字创建错误 BindErr, // 绑定错误 ListenErr, // 监听错误
}; // 监听队列的长度
const int backlog 10; class Sock //服务器专门使用
{
public: Sock() : sockfd_(-1) // 初始化时将sockfd_设为-1表示未初始化的套接字 { } ~Sock() { // 析构函数中可以关闭套接字但这里选择不在析构函数中关闭因为有时需要手动管理资源 } // 创建套接字 void Socket() { sockfd_ socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ 0) { printf(socket error, %s: %d, strerror(errno), errno); //错误 exit(SocketErr); // 发生错误时退出程序 } int opt1;setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt)); //服务器主动关闭后快速重启} // 将套接字绑定到指定的端口上 void Bind(uint16_t port) { //让服务器绑定IP地址与端口号struct sockaddr_in local; memset(local, 0, sizeof(local));//清零 local.sin_family AF_INET; // 网络local.sin_port htons(port); // 我设置为默认绑定任意可用IP地址local.sin_addr.s_addr INADDR_ANY; // 监听所有可用的网络接口 if (bind(sockfd_, (struct sockaddr *)local, sizeof(local)) 0) //让自己绑定别人{ printf(bind error, %s: %d, strerror(errno), errno); exit(BindErr); } } // 监听端口上的连接请求 void Listen() { if (listen(sockfd_, backlog) 0) { printf(listen error, %s: %d, strerror(errno), errno); exit(ListenErr); } } // 接受一个连接请求 int Accept(std::string *clientip, uint16_t *clientport) { struct sockaddr_in peer; socklen_t len sizeof(peer); int newfd accept(sockfd_, (struct sockaddr*)peer, len); if(newfd 0) { printf(accept error, %s: %d, strerror(errno), errno); return -1; } char ipstr[64]; inet_ntop(AF_INET, peer.sin_addr, ipstr, sizeof(ipstr)); *clientip ipstr; *clientport ntohs(peer.sin_port); return newfd; // 返回新的套接字文件描述符 } // 连接到指定的IP和端口——客户端才会用的 bool Connect(const std::string ip, const uint16_t port) { struct sockaddr_in peer;//服务器的信息 memset(peer, 0, sizeof(peer)); peer.sin_family AF_INET; peer.sin_port htons(port);inet_pton(AF_INET, ip.c_str(), (peer.sin_addr)); int n connect(sockfd_, (struct sockaddr*)peer, sizeof(peer)); if(n -1) { std::cerr connect to ip : port error std::endl; return false; } return true; } // 关闭套接字 void Close() { close(sockfd_); } // 获取套接字的文件描述符 int Fd() { return sockfd_; } private: int sockfd_; // 套接字文件描述符
}; main.cc #includeEpollServer.hpp
#includememoryint main()
{std::unique_ptrEpollServer svr(new EpollServer());svr-Init();svr-Start();
} makefile epoll_server:main.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -rf epoll_server EpollServer.hpp #pragma once#includeiostream
#includesys/epoll.h
#includeSocket.hppconst uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IPclass EpollServer
{
public:EpollServer(const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port){}~EpollServer(){listensock_.Close();}void Init(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen();}void Start(){}private:uint16_t port_; // 绑定的端口号Sock listensock_; // 专门用来listen的std::string ip_; // ip地址
};
我们这里先不在EpollServer.hpp直接调用epoll_createepoll_ctleoll_wait等我们先对epoll的各类接口进行封装封装到Epoller.hpp里面。
首先我们需要保证我们的Epoller对象是不能被复制的 nocopy.hpp #pragma once class nocopy
{
public: // 允许使用默认构造函数由编译器自动生成 nocopy() default; // 禁用拷贝构造函数防止通过拷贝来创建类的实例 nocopy(const nocopy) delete; // 禁用赋值运算符防止类的实例之间通过赋值操作进行内容复制 nocopy operator(const nocopy) delete;
};
这个是用来防止epoll被拷贝的 Epoller.hpp #pragma once#includeiostream
#includenocopy.hppclass Epoller : public nocopy //Eopller是nocpy的子类
{
public:
Epoller()
{}
~Epoller()
{
}private:
int epfd;
};
我们可以测试一下 main.cc 很明显有错误了这样子我们的Epoll对象也就不能被复制啦 Epoller.hpp #pragma once#include iostream
#include sys/epoll.h
#include unistd.h
#include cerrno
#include nocopy.hppclass Epoller : public nocopy
{static const int size 128;public:Epoller(){_epfd epoll_create(size);if (_epfd -1){perror(epoll_creat error);}else{printf(epoll_creat successful:%d\n, _epfd);}}~Epoller(){if (_epfd 0){close(_epfd);}}private:int _epfd;
};
我们回到我们的EpollServer.hpp EpollServer.hpp #pragma once#include iostream
#include sys/epoll.h
#include memory
#include Socket.hpp
#include Epoller.hppconst uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IPclass EpollServer
{
public:EpollServer(const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port),listensock_ptr(new Sock()),epoller_ptr(new Epoller()){}~EpollServer(){listensock_ptr-Close();}void Init(){listensock_ptr-Socket();listensock_ptr-Bind(port_);listensock_ptr-Listen();}void Start(){for(;;){}}private:uint16_t port_; // 绑定的端口号std::string ip_; // ip地址std::unique_ptrSock listensock_ptr; // 专门用来listen的std::unique_ptrEpoller epoller_ptr;
};
我们编译运行一下 3.2.EpollServer.hpp
epoll模型可是只负责IO模型里面的等待部分。
为了美观一点我们接着封装我们的Epoller首先我们把我们的epoll_wait函数进行封装一下 Epoller.hpp的EpollWait函数 。。。
class Epoller : public nocopy
{。。。int EpollerWait(struct epoll_event revents[],int num){int nepoll_wait(_epfd,revents,num,3000);return n;}
。。。
private:int _epfd;
}; EpollerServer.hpp class EpollServer
{const static int num 64;
。。。void Start(){struct epoll_event revs[num];for(;;){int nepoller_ptr-EpollerWait(revs,num);if(n0)//有事件就绪{}else if(n0)//超时了{std::couttime out...std::endl;}else//出错了{std::cerrEpollWait errorstd::endl;}}}private:uint16_t port_; // 绑定的端口号std::string ip_; // ip地址std::unique_ptrSock listensock_ptr; // 专门用来listen的std::unique_ptrEpoller epoller_ptr;
};
我们接着写Epoller.hpp的epoll_ctl函数的封装
class Epoller : public nocopy
{static const int size 128;int EpollUpDate(int oper,int sock,uint16_t event){int n;if(operEPOLL_CTL_DEL)//将该事件从epoll红黑树里面删除{nepoll_ctl(_epfd,oper,sock,nullptr);if(n!0){perror(delete epoll_ctl error);}}else{//添加和修改,即EPOLL_CTL_MOD和EPOLL_CTL_ADDstruct epoll_event ev;ev.eventsevent;ev.data.fdsock;nepoll_ctl(_epfd,oper,sock,ev);if(n!0){perror(delete epoll_ctl error);}}return n;}private:int _epfd;int _timeout{3000};
};
接下来我们就可以去写我们的代码了 EpollServer.hpp的Start函数 void Start(){//将listen套接字添加到epoll中-将listensock和他关心的事件添加到内核的epoll模型中的红黑树里面//将listensock添加到红黑树epoller_ptr-EpollUpDate(EPOLL_CTL_ADD,listensock_ptr-Fd(),EPOLLIN);struct epoll_event revs[num];for(;;){int nepoller_ptr-EpollerWait(revs,num);if(n0)//有事件就绪{std::coutevent happened,fd :revs[0].data.fdstd::endl;}else if(n0)//超时了{std::couttime out...std::endl;}else//出错了{std::cerrEpollWait errorstd::endl;}}}
我们运行一下我们的程序然后使用telnet工具测试一下 很好 然后我们很快就能写出下面这些代码 EpollerServer.hpp测试版 #pragma once#include iostream
#include sys/epoll.h
#include memory
#include Socket.hpp
#include Epoller.hppconst uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IPclass EpollServer
{const static int num 64;public:EpollServer(const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), listensock_ptr(new Sock()), epoller_ptr(new Epoller()){}~EpollServer(){listensock_ptr-Close();}void Init(){listensock_ptr-Socket();listensock_ptr-Bind(port_);listensock_ptr-Listen();}void Accepter(){std::string clientip;uint16_t clientport;int sock listensock_ptr-Accept(clientip, clientport);if (sock 0) // 连接成功{// 获取连接成功之后我们应该把这个连接的文件描述符加入到epoll里面让epoll来关心对应事件epoller_ptr-EpollUpDate(EPOLL_CTL_ADD, sock, EPOLLIN);}}void Receiver(int fd){char in_buff[1024];int n read(fd, in_buff, sizeof(in_buff) - 1);if (n 0){in_buff[n] 0;std::cout get message: in_buff std::endl;// 写事件std::string buffin_buff;std::string echo_str server echo: buff;write(fd,echo_str.c_str(),echo_str.size());}else if (n 0) // 客户端关闭连接{// 我们要把这个连接从epoll的红黑树里面移除掉epoller_ptr-EpollUpDate(EPOLL_CTL_DEL, fd, 0);std::cout client close connect,fd: fd std::endl;close(fd); // 我服务器也要关闭连接的文件描述符}else // 出现错误{// 我们要把这个连接从epoll的红黑树里面移除掉epoller_ptr-EpollUpDate(EPOLL_CTL_DEL, fd, 0);std::cout recv reeor,fd: fd std::endl;close(fd); // 我服务器也要关闭连接的文件描述符}}void HandlerEvent(struct epoll_event revs[], int num) // epoll_wait的返回值n代表有n个事件就绪{for (int i 0; i num; i){int fd revs[i].data.fd; // 哪个文件描述符就绪了uint32_t event revs[i].events; // 什么事情就绪了if (event EPOLLIN) // 是读事件就绪了{if (fd listensock_ptr-Fd()) // 获取了新连接{Accepter();}else // 其他fd上的普通读事件就绪{Receiver(fd);}}else if (event EPOLLOUT) // 是写事件就绪了{}else // 其他事件{}}}void Start(){// 将listen套接字添加到epoll中-将listensock和他关心的事件添加到内核的epoll模型中的红黑树里面// 将listensock添加到红黑树epoller_ptr-EpollUpDate(EPOLL_CTL_ADD, listensock_ptr-Fd(), EPOLLIN);struct epoll_event revs[num];for (;;){int n epoller_ptr-EpollerWait(revs, num); // 返回值代表有n个事件就绪if (n 0) // 有事件就绪{std::cout event happened,fd : revs[0].data.fd std::endl;HandlerEvent(revs, n); // 事件就绪的本质就是看他的文件描述符在不在就绪队列里面}else if (n 0) // 超时了{std::cout time out... std::endl;}else // 出错了{std::cerr EpollWait error std::endl;}}}private:uint16_t port_; // 绑定的端口号std::string ip_; // ip地址std::unique_ptrSock listensock_ptr; // 专门用来listen的std::unique_ptrEpoller epoller_ptr;
}; 为了 我们运行一下来看看 很完美啊
我们第一阶段的代码就写到这里更深入的问题我们留到进阶篇来讲解
3.3.源代码 nocopy.hpp #pragma once class nocopy
{
public: // 允许使用默认构造函数由编译器自动生成 nocopy() default; // 禁用拷贝构造函数防止通过拷贝来创建类的实例 nocopy(const nocopy) delete; // 禁用赋值运算符防止类的实例之间通过赋值操作进行内容复制 nocopy operator(const nocopy) delete;
}; Epoller.hpp #pragma once#include iostream
#include sys/epoll.h
#include unistd.h
#include cerrno
#include nocopy.hppclass Epoller : public nocopy
{static const int size 128;public:Epoller(){_epfd epoll_create(size);if (_epfd -1){perror(epoll_creat error);}else{printf(epoll_creat successful:%d\n, _epfd);}}~Epoller(){if (_epfd 0){close(_epfd);}}int EpollerWait(struct epoll_event revents[],int num){int nepoll_wait(_epfd,revents,num,3000);return n;}int EpollUpDate(int oper,int sock,uint16_t event){int n;if(operEPOLL_CTL_DEL)//将该事件从epoll红黑树里面删除{nepoll_ctl(_epfd,oper,sock,nullptr);if(n!0){perror(delete epoll_ctl error);}}else{//添加和修改,即EPOLL_CTL_MOD和EPOLL_CTL_ADDstruct epoll_event ev;ev.eventsevent;ev.data.fdsock;//方便我们知道是哪个fd就绪了nepoll_ctl(_epfd,oper,sock,ev);if(n!0){perror(delete epoll_ctl error);}}return n;}private:int _epfd;
}; EpollServer.hpp #pragma once#include iostream
#include sys/epoll.h
#include memory
#include Socket.hpp
#include Epoller.hppconst uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IPclass EpollServer
{const static int num 64;public:EpollServer(const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), listensock_ptr(new Sock()), epoller_ptr(new Epoller()){}~EpollServer(){listensock_ptr-Close();}void Init(){listensock_ptr-Socket();listensock_ptr-Bind(port_);listensock_ptr-Listen();}void Accepter(){std::string clientip;uint16_t clientport;int sock listensock_ptr-Accept(clientip, clientport);if (sock 0) // 连接成功{// 获取连接成功之后我们应该把这个连接的文件描述符加入到epoll里面让epoll来关心对应事件epoller_ptr-EpollUpDate(EPOLL_CTL_ADD, sock, EPOLLIN);}}void Receiver(int fd){char in_buff[1024];int n read(fd, in_buff, sizeof(in_buff) - 1);if (n 0){in_buff[n] 0;std::cout get message: in_buff std::endl;// 写事件std::string buffin_buff;std::string echo_str server echo: buff;write(fd,echo_str.c_str(),echo_str.size());}else if (n 0) // 客户端关闭连接{// 我们要把这个连接从epoll的红黑树里面移除掉epoller_ptr-EpollUpDate(EPOLL_CTL_DEL, fd, 0);std::cout client close connect,fd: fd std::endl;close(fd); // 我服务器也要关闭连接的文件描述符}else // 出现错误{// 我们要把这个连接从epoll的红黑树里面移除掉epoller_ptr-EpollUpDate(EPOLL_CTL_DEL, fd, 0);std::cout recv reeor,fd: fd std::endl;close(fd); // 我服务器也要关闭连接的文件描述符}}void HandlerEvent(struct epoll_event revs[], int num) // epoll_wait的返回值n代表有n个事件就绪{for (int i 0; i num; i){int fd revs[i].data.fd; // 哪个文件描述符就绪了uint32_t event revs[i].events; // 什么事情就绪了if (event EPOLLIN) // 是读事件就绪了{if (fd listensock_ptr-Fd()) // 获取了新连接{Accepter();}else // 其他fd上的普通读事件就绪{Receiver(fd);}}else if (event EPOLLOUT) // 是写事件就绪了{}else // 其他事件{}}}void Start(){// 将listen套接字添加到epoll中-将listensock和他关心的事件添加到内核的epoll模型中的红黑树里面// 将listensock添加到红黑树epoller_ptr-EpollUpDate(EPOLL_CTL_ADD, listensock_ptr-Fd(), EPOLLIN);struct epoll_event revs[num];for (;;){int n epoller_ptr-EpollerWait(revs, num); // 返回值代表有n个事件就绪if (n 0) // 有事件就绪{std::cout event happened,fd : revs[0].data.fd std::endl;HandlerEvent(revs, n); // 事件就绪的本质就是看他的文件描述符在不在就绪队列里面}else if (n 0) // 超时了{std::cout time out... std::endl;}else // 出错了{std::cerr EpollWait error std::endl;}}}private:uint16_t port_; // 绑定的端口号std::string ip_; // ip地址std::unique_ptrSock listensock_ptr; // 专门用来listen的std::unique_ptrEpoller epoller_ptr;
}; main.cc #includeEpollServer.hpp
#includememoryint main()
{std::unique_ptrEpollServer svr(new EpollServer());svr-Init();svr-Start();
} 到这里我们对Epoll算是有一定的认识了但是这不够我们需要更深入的学习epoll至于epoll的更深入的知识我留到下一篇来介绍