当前位置: 首页 > article >正文

Epoll编程——流程、易错、关键参数

基本概念

epoll能够管理多个文件描述符,方便服务器对多个链接进行管理。一般来说,一个链接就是一个文件描述符。

设定监听描述符,专门负责所有客户端的TCP连接

int main() {
int listen_fd = socket(AF_INET,SOCK_STREAM,0);
std::cout<<listen_fd<<std::endl;
int opt = 1;
std::cout<<setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
struct sockaddr_in sockaddr_d;
sockaddr_d.sin_family = AF_INET;
sockaddr_d.sin_addr.s_addr = INADDR_ANY;
sockaddr_d.sin_port = htons(8888);
std::cout<<bind(listen_fd,(struct sockaddr*)(&sockaddr_d),sizeof(sockaddr_d));
if(listen(listen_fd,1024)==-1)
{
    return -1;
    //设置套接字进入监听状态,告知操作系统内核这个套接字可以接受客户端的连接请求,并且初始化一个连接请求队列
    //当客户端发起连接请求时,操作系统会将这些请求放入这个队列中。        
    //后续一般有一个accept调用,是阻塞的。
    //accept从监听队列中取出一个连接请求,并返回一个新的套接字文件描述符,用于和该客户端进行通信。
}
else std::cout<<"listen";

初始化

int epoll_fd = epoll_create(100);//最多可以管理多少个文件描述符

管理描述符

epoll_event evd;
evd.data.ptr = new temp_struct(listen_fd);
evd.events = EPOLLIN|EPOLLET;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&evd);
//如果是第一次添加这个文件描述符,用ADD,否则用MOD

这里的理解非常关键。
ptr是 void*指针,一般是自定义数据
evens分为两类:设定该文件描述符感兴趣的事件与模式标志
这里的思路是这样的:只有文件符发生了我们感兴趣的事件,epoll_wait才会返回该文件描述符对应的epoll_event结构体。
1、事件类
EPOLLIN:表示对应的文件描述符可以进行读操作。当文件描述符关联的读缓冲区中有数据可读时,会触发该事件。
EPOLLOUT:表示对应的文件描述符可以进行写操作。当文件描述符关联的写缓冲区有足够的空间可以写入数据时,会触发该事件。
EPOLLRDHUP:表示对端关闭连接或者半关闭连接(即对端关闭了写端)。这个标志在处理 TCP 连接时非常有用,可以及时检测到对端的关闭操作。
EPOLLPRI、EPOLLERR、EPOLLHUP,不说了
2、模式标志
EPOLLET:表示采用边缘触发(Edge-Triggered,ET)模式。在边缘触发模式下,只有当文件描述符上的事件状态发生变化时(如从无数据到有数据),才会通知应用程序。与默认的水平触发(Level-Triggered,LT)模式相比,边缘触发模式可以减少不必要的事件通知,提高效率,但要求应用程序必须一次性处理完所有的数据。
EPOLLONESHOT:表示只监听一次事件。当该文件描述符上的事件被触发后,epoll会自动将其从监听列表中移除。除非再次调用epoll_ctrl

处理事件

epoll_event* all_eve = new epoll_event[10];
int i = epoll_wait(epoll_fd,all_eve,10,500); 

获取发生事件的文件描述符。
i是发生事件的数量,all_eve是epoll_event数组,500是超时时间,10是最多事件数量。

问题

场景

1、现在假设有一个客户端,向服务器发起链接请求,TCP已经握手成功。
2、服务端一侧accept创建了文件描述符,并且用epoll_ctl成功将该描述符new_fd添加到epoll_fd的管理中。
3、客户端不发送任何消息,直接close文件描述符,此时TCP的FIN信号被服务端接收,并且触发可读事件。
4、此时服务端read函数返回为0。但是服务端我们并没有close该文件描述符,处理如上述代码所示。

现象

多次执行epoll_wait(),发现每次执行new_fd都会返回可读事件。

代码

#define BUFFER_SIZE 1024
#include<sys/epoll.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<errno.h>
#include<iostream>
struct temp_struct
{
    int fd = -1;
    int d = 1;
    temp_struct(int fd_)
    {
        fd = fd_;
    }
};
epoll_event* all_eve;
int main() 
{
	int listen_fd = socket(AF_INET,SOCK_STREAM,0);
	std::cout<<listen_fd<<std::endl;
	int opt = 1;
	std::cout<<setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR|		SO_REUSEPORT,&opt,sizeof(opt));
	struct sockaddr_in sockaddr_d;
	sockaddr_d.sin_family = AF_INET;
	sockaddr_d.sin_addr.s_addr = INADDR_ANY;
	sockaddr_d.sin_port = htons(8888);
	std::cout<<bind(listen_fd,(struct sockaddr*)	(&sockaddr_d),sizeof(sockaddr_d));
	if(listen(listen_fd,1024)==-1)
	{
    return -1;
	}
	else std::cout<<"listen";
	int epoll_fd = epoll_create(100);
	std::cout<<epoll_fd;
	epoll_event evd;
	evd.data.ptr = new temp_struct(listen_fd);
	evd.events = EPOLLIN|EPOLLET;
	epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&evd);
	all_eve = new epoll_event[10];
	char buff[1024];
	size_t coutt = 0;
	while(1)
	{
    int evt = epoll_wait(epoll_fd,all_eve,10,500);    
    if(evt==0)
    {
        std::cout<<"no even"<<std::endl;
        continue;
    }
    else if(evt<0)
    {
        std::cout<<"error"<<std::endl;
    }
    else
    {
        for(int i=0;i<evt;i++)
        {
            int even_fd = reinterpret_cast<temp_struct*>(all_eve[i].data.ptr)->fd;
            if(even_fd==listen_fd)
            {
                sockaddr_in sock_a;
                socklen_t lent;
                int new_fd = accept(listen_fd,(sockaddr*)(&sock_a),&lent);
                epoll_event evdn;
                evdn.data.ptr = new temp_struct(new_fd);
                evdn.events = EPOLLIN|EPOLLET;
                epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_fd,&evdn);
            }
            else
            {
                coutt++;
                ssize_t t = read(even_fd,buff,1024);
                if(errno==0 && t==0)
                std::cout<<"fd is closed by client "<<coutt<<std::endl;
                epoll_event evdn;
                evdn.data.ptr = all_eve[i].data.ptr;
                evdn.events = EPOLLIN|EPOLLET;
                epoll_ctl(epoll_fd,EPOLL_CTL_MOD,even_fd,&evdn);
            }
        }
    }
}
return 0;
}

原因分析

客户端断开链接后,服务器这个端口会收到TCP的FIN信号。文件描述符会触发可读事件。此时如果读文件描述符new_fd,返回为0,errno=0。但是文件描述符依然是可读状态。
按理说,如果我们设定为ET,仅EPOLL_IN(可读事件出现时)才会触发epoll事件。因此只有第一次执行epoll_wait会返回new_fd对应的epoll_even,不会反复触发。
问题在于:

epoll_ctl(epoll_fd,EPOLL_CTL_MOD,even_fd,&evdn);

当调用 epoll_ctl(EPOLL_CTL_MOD) 重新注册事件时,内核会重新检查套接字的当前状态。
此时:如果套接字仍处于“可读”状态,epoll_ctl会立即触发一次事件,被epoll_wait返回

另外一种情况

同样的情境下,如果我们不采用边缘触发模式,而是采用水平触发模式,并删掉ctl语句

#define BUFFER_SIZE 1024
#include<sys/epoll.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<errno.h>
#include<iostream>
struct temp_struct
{
    int fd = -1;
    int d = 1;
    temp_struct(int fd_)
    {
        fd = fd_;
    }
};
epoll_event* all_eve;
int main() {
int listen_fd = socket(AF_INET,SOCK_STREAM,0);
std::cout<<listen_fd<<std::endl;
int opt = 1;
std::cout<<setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
struct sockaddr_in sockaddr_d;
sockaddr_d.sin_family = AF_INET;
sockaddr_d.sin_addr.s_addr = INADDR_ANY;
sockaddr_d.sin_port = htons(8888);
std::cout<<bind(listen_fd,(struct sockaddr*)(&sockaddr_d),sizeof(sockaddr_d));
if(listen(listen_fd,1024)==-1)
{
    return -1;
    //设置套接字进入监听状态,告知操作系统内核这个套接字可以接受客户端的连接请求,并且初始化一个连接请求队列
    //当客户端发起连接请求时,操作系统会将这些请求放入这个队列中。        
    //后续一般有一个accept调用,是阻塞的。
    //accept从监听队列中取出一个连接请求,并返回一个新的套接字文件描述符,用于和该客户端进行通信。
}
else std::cout<<"listen";

int epoll_fd = epoll_create(100);
std::cout<<epoll_fd;
epoll_event evd;
evd.data.ptr = new temp_struct(listen_fd);
evd.events = EPOLLIN|EPOLLET;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&evd);
all_eve = new epoll_event[10];
char buff[1024];
size_t coutt = 0;
while(1)
{
    int evt = epoll_wait(epoll_fd,all_eve,10,500);    
    if(evt==0)
    {
        std::cout<<"no even"<<std::endl;
        continue;
    }
    else if(evt<0)
    {
        std::cout<<"error"<<std::endl;
    }
    else
    {
        for(int i=0;i<evt;i++)
        {
            int even_fd = reinterpret_cast<temp_struct*>(all_eve[i].data.ptr)->fd;
            if(even_fd==listen_fd)
            {
                sockaddr_in sock_a;
                socklen_t lent;
                int new_fd = accept(listen_fd,(sockaddr*)(&sock_a),&lent);
                epoll_event evdn;
                evdn.data.ptr = new temp_struct(new_fd);
                evdn.events = EPOLLIN;
                epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_fd,&evdn);
            }
            else
            {
                coutt++;
                ssize_t t = read(even_fd,buff,1024);
                if(errno==0 && t==0)
                std::cout<<"fd is closed by client "<<coutt<<std::endl;
            }
        }
    }
}
return 0;
}

依然在每次epoll_wait执行的时候都会返回new_fd对应的epoll_event。这是因为我们将fd注册为水平触发状态(默认)。意味着:只要文件描述符可读状态下,EPOLL_IN事件就不会消失,直到没有数据为止。这里的可读状态不一定是缓冲区中有数据,当收到FIN后,即便没有数据,也会是可读状态。

情况1:水平触发条件:文件描述符fd的读缓冲区中有数据,我们一次没有读取完,那么这个可读事件不会消失。下一次epoll_wait依然还会返回这个文件描述符(因为他有可读事件)。

情况2:客户端close文件描述符,fd在接收到FIN信号后,持续处于可读状态,触发可读事件。此时服务端read返回0,errno=0。如果fd不关闭,那么fd一直有可读事件。

概念精华总结

1、如果文件描述符处于可读状态,这段代码会马上触发一次EPOLL_IN事件。

epoll_event evdn;
evdn.data.ptr = new temp_struct(new_fd);
evdn.events = EPOLLIN|EPOLLET;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,even_fd,&evdn);

2、客户端close后,服务端收到FIN,会触发一次文件描述符的可读事件。如果read=0,errno=0,代表对端关闭fd。
3、ET是当事件发生时,触发可读。例如收到数据时,触发可读事件,收到FIN时,触发可读。默认情况下是LT,就是只要文件描述符处于可读状态(缓冲区有数据,或者收到FIN后fd持续处于可读状态),可读事件就一直存在。

EPOLL_ONESHOT(场景:ET+并发)

即使可以用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该 socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个 socket 的局面。 一个 socket 连接在任一时刻都只被一个线程处理,可以使用 epoll 的 EPOLLONESHOT 实现。
对于注册了 EPOLLONESHOT 的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时候,其他线程是不可能有机会操作该 socket 的。但反过来思考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕,该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket。


http://www.kler.cn/a/529284.html

相关文章:

  • springCload快速入门
  • 小程序设计和开发:什么是竞品分析,如何进行竞品分析
  • 交易股指期货有什么技巧吗?
  • TypeScript 运算符
  • Keepalived高可用集群企业应用实例二
  • 快速提升网站收录:利用网站历史数据
  • 【C++ 区间位运算】3209. 子数组按位与值为 K 的数目|2050
  • 【开源免费】基于Vue和SpringBoot的流浪宠物管理系统(附论文)
  • 新能源算力战争:为什么AI大模型需要绿色数据中心?
  • 【DeepSeek】本地快速搭建DeepSeek
  • 10 Flink CDC
  • 【Java异步编程】CompletableFuture实现:异步任务的串行执行
  • 编程AI深度实战:给vim装上AI
  • java_包装类
  • 边缘检测算法(candy)
  • 高速PCB设计指南6——电源完整性
  • 【学习笔记之coze扣子】智能体创建
  • Mac M1 源码安装FFmpeg,开启enable-gpl 和 lib x264
  • Agentic Automation:基于Agent的企业认知架构重构与数字化转型跃迁---我的AI经典战例
  • vue面试题|[2025-2-1]
  • 只需5步,免费使用Ollama本地运行DeepSeek-R1模型
  • 关于matlab中rotm2eul的注释错误问题
  • Ollama部署指南
  • Autogen_core源码:_agent.py
  • H3CNE-33-BGP
  • 【Rust自学】19.1. 摆脱安全性限制的unsafe Rust