优秀的门户网站,房地产销售新手必学知识,做教育集团的网站,网站与网页的区别.✨个人主页#xff1a; 熬夜学编程的小林
#x1f497;系列专栏#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【Linux网络编程】
目录 1、I/O 多路转接之 epoll
1.1、epoll 初识
1.2、epoll 的相关系统调用
1.2.1、epoll_create
1.2.2、epol…✨个人主页 熬夜学编程的小林
系列专栏 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【Linux网络编程】
目录 1、I/O 多路转接之 epoll
1.1、epoll 初识
1.2、epoll 的相关系统调用
1.2.1、epoll_create
1.2.2、epoll_ctl
1.2.3、epoll_wait
1.3、epoll 工作原理
1.3.1、理解数据到达主机
1.3.2、epoll原理
1.4、代码演示一(框架实现)
1.4.1、主函数
1.4.2、EpollServer类
1.4、代码演示二(通信实现)
1.4.1、Loop()
1.4.2、HandlerEvent()
1.5、epoll 的优点
1.6、epoll 工作方式
1.7、对比 LT 和 ET
1.8、理解 ET 模式和非阻塞文件描述符
1.9、epoll 的使用场景 1、I/O 多路转接之 epoll
1.1、epoll 初识
按照 man 手册的说法: 是为处理大批量句柄而作了改进的 poll.它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)它几乎具备了之前所说的一切优点被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法. 作用为了等待多个fd等待fd上面的新事件就绪通知程序员事件已经就绪可以进行IO拷贝了 定位只负责进行等等就绪事件派发 1.2、epoll 的相关系统调用
epoll 有 3 个相关的系统调用.
1.2.1、epoll_create
epoll_create()
epoll_create - 创建一个 epoll 的句柄.#include sys/epoll.hint epoll_create(int size);
参数
size 在早期版本的 Linux 中指定了监听的文件描述符数量上限自从 linux2.6.8 之后这个参数被忽略因为 epoll 自动调整以处理最大数量的文件描述符。因此传递任何大于 0 的值都是可以的通常使用 1 作为默认值。
返回值
成功时epoll_create() 返回一个非负的文件描述符该描述符用于后续的 epoll 操作如 epoll_ctl() 和 epoll_wait()。失败时返回 -1 并设置 errno 以指示错误类型。
注意用完之后, 必须调用 close()关闭.
1.2.2、epoll_ctl
epoll_ctl()
epoll_ctl - 允许你向一个 epoll 实例中添加、删除或修改监听的文件描述符及其相关的事件。#include sys/epoll.hint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数
epfd由 epoll_create() 或 epoll_create1() 返回的 epoll 文件描述符。op要执行的操作可以是以下三个值之一 EPOLL_CTL_ADD向 epoll 实例中添加一个新的文件描述符。EPOLL_CTL_DEL从 epoll 实例中删除一个文件描述符。EPOLL_CTL_MOD修改一个已经存在于 epoll 实例中的文件描述符的监听事件。fd要添加、删除或修改的文件描述符。event指向一个 epoll_event 结构体的指针该结构体指定了要监听的事件类型和数据。对于 EPOLL_CTL_DEL 操作这个参数可以是 nullptr因为删除操作不需要知道事件类型。
返回值
成功时epoll_ctl() 返回 0。失败时返回 -1 并设置 errno 以指示错误类型。
struct epoll_event 结构如下:
typedef union epoll_data
{void *ptr;int fd;__uint32_t u32;__uint64_t u64;struct sockaddr sockaddr; // 仅在特定情况下使用
} epoll_data_t;struct epoll_event
{__uint32_t events; // 事件类型可以是多个事件的按位或组合epoll_data_t data; // 与事件相关的用户数据可以是文件描述符、指针或 sockaddr 结构
};
events 可以是以下几个宏的集合
EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);EPOLLOUT : 表示对应的文件描述符可以写;EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);EPOLLERR : 表示对应的文件描述符发生错误;EPOLLHUP : 表示对应的文件描述符被挂断;EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.EPOLLONESHOT只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.
1.2.3、epoll_wait
epoll_wait()
epoll_wait - 阻塞调用线程直到有至少一个文件描述符上的事件变得就绪或者超时发生。#include sys/epoll.hint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数
epfd由 epoll_create() 或 epoll_create1() 返回的 epoll 文件描述符。events指向一个 epoll_event 结构体数组的指针该数组用于存储返回的事件信息。maxeventsevents 数组的大小即最多可以返回的事件数量。timeout等待事件的超时时间毫秒。 如果为 -1则 epoll_wait() 将无限期地阻塞直到有事件发生。如果为 0则 epoll_wait() 将立即返回即使没有任何事件发生这可以用于非阻塞模式。
返回值
成功时epoll_wait() 返回就绪事件的数量这些事件被存储在 events 数组中。失败时返回 -1 并设置 errno 以指示错误类型。 1.3、epoll 工作原理
1.3.1、理解数据到达主机 数据到达主机的原理涉及多个层级和协议的协同工作。通过逐层封装、转发和接收处理数据能够准确地从源主机传输到目的主机。 硬件中断是由硬件设备发出的信号用于通知计算机系统发生了某个事件需要系统进行处理。这些硬件设备可以是磁盘、网卡、键盘、时钟等。 1.3.2、epoll原理 1.4、代码演示一(框架实现)
1.4.1、主函数 老规矩根据主函数反向实现类和成员函数 // ./epoll_server 8888
int main(int argc, char *argv[])
{if(argc ! 2){std::cerr Usage: argv[0] locak-port std::endl;exit(0);}uint16_t port std::stoi(argv[1]);EnableScreen(); // 开启日志std::unique_ptrEpollServer svr std::make_uniqueEpollServer(port);svr-InitServer();svr-Loop();return 0;
}
1.4.2、EpollServer类 EpollServer类的成员变量包括端口号listen套接字epfd(epoll_create()函数的返回值)接收事件的数组成员函数与PollServer类基本一致 基本结构
class EpollServer
{const static int size 128;const static int num 128;public:EpollServer(uint16_t port);void InitServer();void Loop();~EpollServer();private:uint16_t _port;std::unique_ptrSocket _listensock;int _epfd;struct epoll_event revs[num];
};
构造析构函数 构造函数初始化端口号根据端口号创建监听套接字对象以及创建epoll句柄析构函数关闭epfd(合法的前提下)和listenfd EpollServer(uint16_t port) : _port(port), _listensock(std::make_uniqueTcpSocket())
{_listensock-BuildListenSocket(port);_epfd ::epoll_create(size);if (_epfd 0){LOG(FATAL, epoll_create error\n);exit(1);}LOG(INFO, epoll create success,epfd: %d\n, _epfd); // 4
}~EpollServer()
{if (_epfd 0)::close(_epfd);_listensock-Close();
}
InitServer() InitServer()函数使用系统调用(epoll_ctl)listensock添加到epoll void InitServer()
{// 新链接到来我们认为是读事件就绪struct epoll_event ev;ev.events EPOLLIN;ev.data.fd _listensock-Sockfd(); //???// 必须先把listensock添加到epollint n epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock-Sockfd(), ev);if (n 0){LOG(FATAL, epoll_ctl error\n);exit(2);}LOG(INFO, epoll_ctl success,add new sockfd: %d\n, _listensock-Sockfd());
}
Loop() Loop()函数调用epoll_wait系统调用进行等待根据返回值执行对应的操作 1、返回值为0 打印超时日志并退出循环 2、返回值为-1 打印出错日志并退出循环 3、返回值大于0 打印事件发生日志暂时先退出循环 void Loop()
{int timeout 1000;while (true){// _listensock-Accepter();int n ::epoll_wait(_epfd, revs, num, timeout);switch (n){case 0:LOG(INFO, epoll time out...\n);break;case -1:LOG(ERROR, epoll error\n);break;default:LOG(INFO,haved event happend!,n : %d\n,n);break;}}
}
运行结果 1.4、代码演示二(通信实现) 代码演示二只需要修改EpollServer类中主函数调用的成员函数即可 1.4.1、Loop() Loop()函数调用epoll_wait系统调用进行等待根据返回值执行对应的操作 1、返回值为0 打印超时日志并退出循环 2、返回值为-1 打印出错日志并退出循环 3、返回值大于0 打印事件发生日志并处理合法事件 void Loop()
{int timeout 1000;while (true){// 事件通知事件派发// _listensock-Accepter();int n ::epoll_wait(_epfd, revs, num, timeout);switch (n){case 0:LOG(INFO, epoll time out...\n);break;case -1:LOG(ERROR, epoll error\n);break;default:LOG(INFO, haved event happend!,n : %d\n, n);HandlerEvent(n);break;}}
}
1.4.2、HandlerEvent() HandlerEvent()函数处理就绪事件主要分为以下两步 1、从事件数组中读取合法fd和events2、判断读事件是否就绪 2.1、listensock就绪2.2、normal sockfd就绪 void HandlerEvent(int n)
{for (int i 0; i n; i){// 1.从事件数组中读取合法fd和eventsint fd revs[i].data.fd;uint32_t revents revs[i].events;LOG(INFO, %d 上面有事件就绪了具体事件是: %s\n, fd, EventsToString(revents).c_str());// 2.判断读事件是否就绪if (revents EPOLLIN){// listensock 读事件就绪新链接到来了if (fd _listensock-Sockfd())Accepter();elseHandlerIO(fd);}}
}
Accepter() Accepter()函数处理新链接主要分为以下两步 1、获取链接2、获取链接成功将新的 fd 和 读事件添加到epoll中(使用epoll_ctl系统调用) void Accepter()
{InetAddr addr;int sockfd _listensock-Accepter(addr); // 肯定不会出错if (sockfd 0){LOG(ERROR, 获取链接失败\n);return;}LOG(INFO, 得到一个新的链接: %d, 客户端信息: %s:%d\n, sockfd, addr.Ip().c_str(), addr.Port());// 得到了一个新的sockfd我们能不呢个进行read,recv? 不能不清楚有没有数据没数据会阻塞// 等底层有数据(读事件就绪)read/recv才不会被阻塞// 底层有数据谁最清楚epoll// 将新的sockfd添加到epoll中怎么做呢struct epoll_event ev;ev.data.fd sockfd;ev.events EPOLLIN;::epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, ev);LOG(INFO, epoll_ctl success,add new sockfd: %d\n, sockfd);
}
HandlerIO() HandlerIO()函数处理普通fd情况直接读取文件描述符中的数据根据recv()函数的返回值做出不一样的决策主要分为以下三种情况 1、返回值大于0读取文件描述符中的数据并使用send()函数做出回应 2、返回值等于0读到文件结尾打印客户端退出的日志将epfd从epoll中移除并关闭fd 3、返回值小于0读取文件错误打印接受失败的日志然后同上 void HandlerIO(int fd)
{char buffer[4096];int n ::recv(fd, buffer, sizeof(buffer) - 1, 0); // 会阻塞不会if (n 0){buffer[n] 0;std::cout buffer;std::string response HTTP/1.0 200 OK\r\n;std::string content htmlbodyh1hello linux,hello world/h1/body/html;response Content-Type: text/html\r\n;response Content-Length: std::to_string(content.size()) \r\n;response \r\n;response content;::send(fd, response.c_str(), response.size(), 0);}else if (n 0){LOG(INFO, client quit,close fd: %d\n, fd);// 1.从epoll中移除从epoll中移除fd这个必须是健康合法的fd否则会移除出错::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr); // 进一步验证红黑树以fd作为键值// 2.关闭fd::close(fd);}else{LOG(ERROR, recv error,close fd: %d\n, fd);// 1.从epoll中移除从epoll中移除fd这个必须是健康合法的fd否则会移除出错::epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);// 2.关闭fd::close(fd);}
}
运行结果 1.5、epoll 的优点
接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度 O(1). 即使文件描述符数目很多, 效率也不会受到影响.没有数量限制: 文件描述符数目无上限.
注意 网上有些博客说, epoll 中使用了内存映射机制
内存映射机制: 内核直接将就绪队列通过 mmap 的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.
这种说法是不准确的. 我们定义的 struct epoll_event 是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的. 请uu们对比总结 select, poll, epoll 之间的优点和缺点(重要, 面试中常见).
1.6、epoll 工作方式
你妈喊你吃饭的例子 你正在吃鸡, 眼看进入了决赛圈, 你妈饭做好了, 喊你吃饭的时候有两种方式: 1. 如果你妈喊你一次, 你没动, 那么你妈会继续喊你第二次, 第三次...(亲妈,水平触发) 2. 如果你妈喊你一次, 你没动, 你妈就不管你了(后妈, 边缘触发) epoll 有 2 种工作方式-水平触发(LT)和边缘触发(ET) 假如有这样一个例子:
我们已经把一个 tcp socket 添加到 epoll 描述符 这个时候 socket 的另一端被写入了 2KB 的数据调用 epoll_wait并且它会返回. 说明它已经准备好读取操作然后调用 read, 只读取了 1KB 的数据继续调用 epoll_wait......
水平触发 Level Triggered 工作模式epoll 默认状态下就是 LT 工作模式.
当 epoll 检测到 socket 上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.如上面的例子, 由于只读了 1K 数据, 缓冲区中还剩 1K 数据, 在第二次调用epoll_wait 时, epoll_wait 仍然会立刻返回并通知 socket 读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.支持阻塞读写和非阻塞读写
边缘触发 Edge Triggered 工作模式 如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志, epoll 进入 ET 工作模式.
当 epoll 检测到 socket 上事件就绪时, 必须立刻处理.如上面的例子, 虽然只读了 1K 的数据, 缓冲区还剩 1K 的数据, 在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了.也就是说, ET 模式下, 文件描述符上的事件就绪后, 只有一次处理机会.ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多). Nginx 默认采用ET 模式使用 epoll.只支持非阻塞的读写
select 和 poll 其实也是工作在 LT 模式下. epoll 既可以支持 LT, 也可以支持 ET.
1.7、对比 LT 和 ET 1、LT 是 epoll 的默认行为.2、使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完. 相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.3、另一方面, ET 的代码复杂程度更高了.4、ET的通知效率更高5、ET可能给对方一个更大的接受窗口增加IO效率 -- 即ET的IO效率更高 1.8、理解 ET 模式和非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 工程实践 上的要求逻辑如下 ET模式下只通知一次本轮数据没读完epoll不在通知 - ET模式一旦就绪就必须把数据全部读完 - 你怎么知道你把数据全部读完了 - 循环读取直到读取不到 - 循环读取是不是可能出现阻塞问题 - 因此需将fd设置为非阻塞 有一个问题LT也可以设置非阻塞LT我也可以循环读取完毕啊为什么要有ET呢 最简单的理解ET是被强制要求非阻塞的但是LT可以是阻塞也可以是非阻塞 假设这样的场景: 服务器接收到一个 10k 的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个 10k 请求. 如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话(read 不能保证一次就把所有的数据都读出来, 参考 man 手册的说明, 可能被信号打断), 剩下的 9k 数据就会待在缓冲区中. 此时由于 epoll 是 ET 模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据.epoll_wait 才能返回 但是问题来了.
服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据.客户端要读到服务器的响应, 才会发送下一个请求客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据. 所以, 为了解决上述问题(阻塞 read 不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区, 保证一定能把完整的请求都读出来.
而如果是 LT 没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.
1.9、epoll 的使用场景
epoll 的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll 的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用 epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网 APP 的入口服务器,这样的服务器就很适合 epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll 就并不合适. 具体要根据需求和场景特点来决定使用哪种 IO 模型.