io多路复用:epoll水平触发(LT)和边沿触发(ET)的区别和优缺点

news/2024/9/25 18:37:28/

在进行ET模式的正式分析之前,我们来举个例子简单地了解下ET和LT:

假设我们通过fork函数创建了父子两个进程,并通过匿名管道来通信,在子进程中,我们一次向管道写入10个字符数据,为"aaaa\nbbbb\n";每隔5s写入10个字符数据;在父进程中,我们从管道中一次读取5个字符数据:

若我们采用的是LT模式,则在5s的周期内,会先读取5个字符数据,读完之后,因为文件描述符中仍然有数据,epoll_wait会立即返回,会继续读取接下来的5个数据,之后在5s周期以内的剩余时间内,管道中的数据都为空,如下图1。

若我们采用的是ET模式,则当父进程先读完管道中的5个字符后,由于子进程没有立即向管道中写入字符(间隔5s后才会第二次写入),所以此时父进程会先读到5个字符"aaaa\n",隔5s之后,再读到5个字符"bbbb\n",如下图2,可以看到ET模式下,随时间推移,管道中数据会越来越多。

图1

图2

epoll工作流程

想要了解ET模式和LT模式的区别,首先需要熟悉epoll的工作流程:

【注:】上图的poll不要理解成和select相似那个poll,这是通过epoll_ctl调用的,进行回调函数注册。

下面简要分析一下epoll的工作过程:

1)epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。

2)****文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。

ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。

3)ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。

4)ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对应的poll方法(图中蓝线)。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。

5)之后如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist(图中蓝线),否则(ET模式)不在加入rdlist。

ET模式 vs LT模式

区别

通过上图epoll的工作流程描述,可以看出ET模式和LT模式的区别主要发生在第5步:是否将文件描述符fd放回rdlist中,而rdlist的是否为空决定了epoll_wait函数的阻塞和非阻塞。因此,总结下二者的区别:

ET

如果是ET, epitem是不会再进入到readly list;除非fd再次发生了状态改变, ep_poll_callback被调用。【因此,ET模式可减少epoll_wait函数的调用,从而减少系统开销,提高效率。】

LT

如果是非ET, 不管你还有没有有效的事件或者数据,都会被重新插入到ready list, 再下一次epoll_wait时, 会立即返回, 并通知给用户空间。当然如果这个被监听的fds确实没事件也没数据了, epoll_wait会返回一个0,空转一次。

加入rdlist途径分析

红线:fd状态改变才会触发,那什么情况会导致fd状态的改变呢?

对于读取操作:

1)当buffer由不可读状态变为可读的时候,即由空变为不空的时候。

2)当有新数据到达时,即buffer中的待读内容变多的时候。

对于写操作:

1)当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。

2)当有旧数据被发送走时,即buffer中待写的内容变少得时候。

蓝线:fd的events中有相应的时间(位置1)即会触发。那什么情况下会改变events的相应位呢?

对于读操作:

1)buffer中有数据可读的时候,即buffer不空的时候,fd的events的可读为就置1。

对于写操作:

2) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。

【说明:】

  • 红线是时间驱动被动触发(fd状态改变);

  • 蓝线是函数查询主动触发(LT和ET判断是否加入rdlist)。

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

代码实践

基础代码

以下为一个epoll触发模式测试的基础代码,也不算太长,直接拿来就可以测试:

#include <sys/socket.h> //for socket
#include <arpa/inet.h>  //for htonl htons
#include <sys/epoll.h>  //for epoll_ctl
#include <unistd.h>     //for close
#include <fcntl.h>      //for fcntl
#include <errno.h>      //for errno
#include <iostream>     //for coutclass fd_object
{
public:fd_object(int fd) { listen_fd = fd; }~fd_object() { close(listen_fd); }
private:int listen_fd;
};/*
./epoll for lt mode
and
./epoll 1 for et mode
*/
int main(int argc, char* argv[])
{//create a socket fdint listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1){std::cout << "create listen socket fd error." << std::endl;return -1;}fd_object obj(listen_fd);//set socket to non-blockint socket_flag = fcntl(listen_fd, F_GETFL, 0);socket_flag |= O_NONBLOCK;if (fcntl(listen_fd, F_SETFL, socket_flag) == -1){std::cout << "set listen fd to nonblock error." << std::endl;return -1;}//init server bind infoint port = 51741;struct sockaddr_in bind_addr;bind_addr.sin_family = AF_INET;bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);bind_addr.sin_port = htons(port);if (bind(listen_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) == -1){std::cout << "bind listen socket fd error." << std::endl;return -1;}//start listenif (listen(listen_fd, SOMAXCONN) == -1){std::cout << "listen error." << std::endl;return -1;}elsestd::cout << "start server at port [" << port << "] with [" << (argc <= 1 ? "LT" : "ET") << "] mode." << std::endl;//create a epoll fdint epoll_fd = epoll_create(88);if (epoll_fd == -1){std::cout << "create a epoll fd error." << std::endl;return -1;}epoll_event listen_fd_event;listen_fd_event.data.fd = listen_fd;listen_fd_event.events = EPOLLIN;if (argc > 1) listen_fd_event.events |= EPOLLET;//add epoll event for listen fdif (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1){std::cout << "epoll ctl error." << std::endl;return -1;}while (true){epoll_event epoll_events[1024];int n = epoll_wait(epoll_fd, epoll_events, 1024, 1000);if (n < 0)break;else if (n == 0) //timeoutcontinue;for (int i = 0; i < n; ++i){if (epoll_events[i].events & EPOLLIN)//trigger read event{if (epoll_events[i].data.fd == listen_fd){//accept a new connectionstruct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_fd == -1)continue;socket_flag = fcntl(client_fd, F_GETFL, 0);socket_flag |= O_NONBLOCK;if (fcntl(client_fd, F_SETFL, socket_flag) == -1){close(client_fd);std::cout << "set client fd to non-block error." << std::endl;continue;}epoll_event client_fd_event;client_fd_event.data.fd = client_fd;client_fd_event.events = EPOLLIN | EPOLLOUT;if (argc > 1) client_fd_event.events |= EPOLLET;if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1){std::cout << "add client fd to epoll fd error." << std::endl;close(client_fd);continue;}std::cout << "accept a new client fd [" << client_fd << "]." << std::endl;}else{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;}}else if (epoll_events[i].events & EPOLLOUT){if (epoll_events[i].data.fd == listen_fd) //trigger write eventcontinue;std::cout << "EPOLLOUT event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;}}}return 0;
}

简单说下这段代码的测试方法,可以使用g++ testepoll.cpp -o epoll进行编译,编译后通过./epoll运行为LT模式,通过./epoll et模式运行为ET模式,我们用编译好的epoll程序作为服务器,使用nc命令来模拟一个客户端。

测试分类

1.编译后直接./epoll,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 命令模拟一次连接,此时 ./epoll 会产生大量的 EPOLLOUT event triggered for client fd ...,那是因为在LT模式下,EPOLLOUT会被一直触发。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
...

2.注释包含 EPOLLOUT event triggered for client fd 输出内容的第152行代码,编译后 ./epoll运行,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,输入abcd回车,可以看到服务器./epoll输出内容,EPOLLIN被触发多次,每次读取一个字节。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [d].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].

3.还原刚才注释的那行代码,编译后执行 ./epoll et 启动服务器,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接,服务器窗口显示触发了EPOLLOUT事件

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].

在此基础上,从刚刚运行nc命令的窗口中输入回车、输入回车、输出回车,那么epoll服务器窗口看到的是触发了三次EPOLLIN事件,每次收到一个回车:

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].

但是如果在nc模拟的客户端里输出abcd回车,那么在epoll服务器窗口触发一次EPOLLIN事件接收到一个a之后便再也不会触发EPOLLIN了,即使你在nc客户端在此输入也没有用,那是因为在接受的缓冲区中一直还有数据,新数据来时没有出现缓冲区从空到有数据的情况,所以在ET模式下也注意这种情况。

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].

怎么解决ET触发了一次就不再触发了

改代码呗,ET模式在连接后触发一次EPOLLOUT,接收到数据时触发一次EPOLLIN,如果数据没收完,以后这两个事件就再也不会被触发了,要想改变这种情况可以再次注册一下这两个事件,时机可以选择接收到数据的时候,所以可以修改这部分代码:

else
{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}

添加再次注册的逻辑:

else
{std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;char recvbuf[1024] = { 0 };int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggeredif (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR)){if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." <<  std::endl;close(epoll_events[i].data.fd);}epoll_event client_fd_event;client_fd_event.data.fd = epoll_events[i].data.fd;client_fd_event.events = EPOLLIN | EPOLLOUT;if (argc > 1) client_fd_event.events |= EPOLLET;epoll_ctl(epoll_fd, EPOLL_CTL_MOD, epoll_events[i].data.fd, &client_fd_event);std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}

这次以./epoll et方式启动服务器,使用nc -v 127.0.0.1 51741模拟客户端,输入abc回车发现,epoll服务器输出显示触发的事件变了:

albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLOUT event triggered for client fd [5].

为什么使用边缘触发必须使用非阻塞

在设置边缘触发时,因为每次发消息只会触发一次(不管缓存区是否还留有数据),所以必须把数据一次性读取出来,否则会影响下一次消息

下面的代码实现的是监听文件描述符,每次固定读取5个字节

先看下面这段代码

int main(int argc, char *argv[])
{int epfd, nfds;int i;struct epoll_event event, events[10];char buf[5];int flag;epfd = epoll_create(10);event.data.fd = 0; /* 监听标准输入 */event.events = EPOLLIN | EPOLLET; /* 读监听、边缘触发 *///event.events = EPOLLIN; /* 读监听、边缘触发 */epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);#if 0flag = fcntl(0, F_GETFL);fcntl(0, F_SETFL, flag | O_NONBLOCK);
#endif	while (1){int i;int num = 0;char c;nfds = epoll_wait(epfd, events, 5, -1); /* 返回就绪的文件描述符 */for (i = 0; i < nfds; ++i) {if (events[i].data.fd == STDIN_FILENO) {for(i = 0; i < 5; i++){buf[i] = getc(stdin);if(buf[i] == -1)break;}printf("hello world\n");}}}return 0;
}

这段代码的目的是,使用边沿触发,每次触发读取5个字节,此时getc函数是阻塞读取,这就会引起一个问题,当缓存中的数据小于5时,就会在这里阻塞等待,导致无法处理其他IO,这是非常错误的行为,在并发的服务器中,会导致服务器阻塞,无法处理其他客户端

正确的处理方法是将读取的文件描述符设置为非阻塞,循环读取,如果没有数据了就放回错误,这样就不会让服务器阻塞

可以添加下面这两行代码,将标准输入设置为非阻塞IO

flag = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flag | O_NONBLOCK);

这就解释了为什么边沿触发必须使用非阻塞的问题。

水平触发和边沿触发的优缺点

水平触发LT:

优点:程序简单,会完整地读取所有数据。

缺点:重复地事件触发会影响高并发服务器地性能,因为epoll监控事件涉及到系统调用,需要用户态-内核态的转换。LT消耗了大量的系统资源,影响服务器性能;

边沿触发ET:

优点:每次epoll_wait只用触发一次,通过程序逻辑实现读取缓冲区的所有数据,工作效率高,大大提升了服务器性能;

缺点(没归纳出来,随便写一个):不能保证数据的完整。(这个可以通过上面提到的程序逻辑实现完整地读取数据)

边沿触发没什么缺点?那是不是用epoll就用ET边沿触发就好了?

我的理解是,YES。在日常用epoll实现并发处理,可以优先使用“边沿触发(EPOLLET)+非阻塞IO”模式。

在高并发服务器中边沿触发(ET)的效率更高

因为边沿触发只在数据到来的一刻才触发,很多时候服务器在接受大量数据时会先接受数据头部(水平触发在此触发第一次,边沿触发第一次)。

接着服务器通过解析头部决定要不要接这个数据。此时,如果不接受数据,水平触发需要手动清除(水平触发当有数据时,会一直触发,直到没有数据可读),而边沿触发可以将清除工作交给一个定时的清除程序去做(只触发一次,不需要的数据可以不读),自己立刻返回。

但是如果sockfd中发送的数据较小,我一次recv就能全部读完,这样LT也不会重复触发epoll事件,和边沿触发的性能差不多,那我们为什么不用更简单的水平触发呢,当然可以使用。那其实就可以理解水平触发和边沿触发是有一个分界点,就是看sockfd的数据是小数据还是大数据。recv的BUFFER_LENGTH如果一次能接收完recv buffer中的数据,就是小数据,一次接收不完就是大数据。小数据就用水平触发,大数据就用边沿触发

但但但但是LT还在一种场景有使用,就是Nginx服务器中listenfd是用的水平触发 。(网络服务器中一般涉及两类sockfd,一种是用于监听是否有连接请求的socket,一种是用于传输数据的socket(上面讲的sockfd都是用于传输数据的))

有一些解释是listenfd用水平触发,如果多个client同时连接进来,listenfd里面积攒多个连接的话,accept一次只处理一个连接,防止漏掉连接,选择水平触发。


http://www.ppmy.cn/news/1530408.html

相关文章

CNN网络训练WISDM数据集:模型仿真及可视化分析

卷积神经网络&#xff08;CNN&#xff09;因其强大的特征提取能力和深度学习架构而备受推崇&#xff0c;CNN在处理图像数据时展现出的卓越性能&#xff0c;使其成为解决各种视觉识别任务的首选工具。WISDM数据集是一个广泛用于运动估计研究的基准数据集&#xff0c;它包含了多个…

Vue3 中组件传递 + css 变量的组合

文章目录 需求效果如下图所示代码逻辑代码参考 需求 开发一个箭头组件&#xff0c;根据父组件传递的 props 来修改 css 的颜色 效果如下图所示 代码逻辑 代码 父组件&#xff1a; <Arrow color"red" />子组件&#xff1a; <template><div class&…

【Git入门】使用 Git 进行项目管理:Word Count 程序开发与托管

在软件开发过程中&#xff0c;版本控制工具是不可或缺的。Git 作为一款强大的分布式版本控制工具&#xff0c;为开发者提供了高效的代码管理和协作方式。本博客将介绍如何下载安装 Git 版本管理工具&#xff0c;并使用 Git 和 GitHub 平台进行一个名为 Word Count 的项目开发与…

不在同一局域网怎么远程桌面?非局域网环境下,实现远程桌面访问的5个方法分享!

非局域网环境下&#xff0c;怎么远程桌面&#xff1f;还能做到吗&#xff1f; 在企业管理中&#xff0c;远程桌面访问已成为提高工作效率、实现跨地域协同工作的关键工具。 然而&#xff0c;当被控端与控制端不在同一局域网时&#xff0c;如何实现远程桌面连接成为了许多企业…

基于微信小程序的购物系统+php(lw+演示+源码+运行)

基于微信小程序的购物系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了基于微信小程序的购物系统的开发全过程。通过分析基于微信小程序的购物系统管理的不足&#xff0c;创建了一个计算机管理基于微信小…

Linux相关概念和重要知识点(6)(make、makefile、gdb)

1.make、makefile &#xff08;1&#xff09;什么是make、makefile&#xff1f; 在我们写完代码后&#xff0c;要编译运行&#xff0c;如果有多个.c文件就需要每次都自己用gcc -o来处理&#xff0c;这十分麻烦。当我们想要自定义多个文件的处理时&#xff0c;我们会浪费很多时…

Pytorchz学习---基于卷积神经网络的MINIST数据集训练

# 一般情况下&#xff0c;如果模型有可学习的参数&#xff0c;最好用nn.Module&#xff0c;其他情况用nn.function相对简单一些 import torch.nn.functional as F import torch loss_func F.cross_entropy def model(xb):return xb.mm(weights)bias# bs 64 # xb x_trains[0:…

微服务注册中⼼1

1. 微服务的注册中⼼ 注册中⼼可以说是微服务架构中的”通讯录“ &#xff0c;它记录了服务和服务地址的映射关系。在分布式架构中&#xff0c; 服务会注册到这⾥&#xff0c;当服务需要调⽤其它服务时&#xff0c;就这⾥找到服务的地址&#xff0c;进⾏调⽤。 1.1 注册中⼼的…