EPOLLONESHOT事件

EPOLLONESHOT事件用于解决多线程环境下,同一个socket可能被多个线程同时处理的问题。它确保操作系统仅触发一次文件描述符的可读、可写或异常事件。在处理完socket数据后,需要及时重置EPOLLONESHOT事件,以允许下次事件触发。此外,监听描述符不应注册EPOLLONESHOT,以免影响新客户连接的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

即使使用ET模式,一个socket上的同一事件还是可能被触发多次(将TCP缓冲区设置的小一点,发送一个较大的数据,接受的数据会多次填满缓冲区,导致会被触发多次),,当某个线程在读取完某个socket的数据后开始处理这些数据,但在处理这些数据的时候这个socket上又有数据来了,此时另外一个线程被唤醒来读取这些新的数据,于是出现了两个线程同时操作一个socket的局面。但我们期望一个socket在任意时刻值被一个socket处理。此时可以用epoll的EPOLLONESHOT事件来解决。

引入

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或者异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上EPOLLONESHOT事件。

使用

注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能触发,进而让其他线程有机会继续处理这个socket.

代码实现

epolloneshot.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <iostream>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 200
struct fds
{
    int epollfd;
    int sockfd;
};
//设置非阻塞
int setnonblocking(int fd)
{
    int old_flag = fcntl(fd,F_GETFL);
    int new_flag = old_flag | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_flag);
    return old_flag;
}

//向epollfd注册文件描述符,并添加EPOLLONESHOT事件
void addfd(int epollfd,int fd,bool oneshot)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN|EPOLLET;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    setnonblocking(fd);
}
//重置EPOLLONESHOT事件
void reset_oneshot(int epollfd,int fd)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN|EPOLLOUT|EPOLLONESHOT;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}

//工作线程
void* worker(void *arg)
{
    int sockfd = ((fds*)arg)->sockfd;
    int epollfd = ((fds*)arg)->epollfd;
    printf("start new thread to recive data on fd:%d\n",sockfd);
    char buf[BUFFER_SIZE];
    memset(buf,'\0',BUFFER_SIZE);
    //循环读取sockfd上的数据,直到遇到EAGAIN错误
    while(1)
    {
        memset(buf,'\0',sizeof(buf));
        int ret = recv(sockfd,buf,BUFFER_SIZE-1,0);
        if(ret == 0)
        {
            close(sockfd);
            printf("client closed the connection\n");
            break;
        }
        else if(ret < 0)
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                reset_oneshot(epollfd,sockfd);
                printf("read later\n");
                break;
            }
        }
        else
        {
            printf("get content:%s\n",buf);
            sleep(5);
        }
        
    }
    printf("end thread reciving data on fd:%d\n",sockfd);
}

int main(int argc,char** argv)
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr.s_addr);
    address.sin_port = htons(port);
    
    int listenfd = socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd >= 0);

    ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret >= 0);

    ret = listen(listenfd,5);
    assert(ret >= 0);

    //复用地址和端口号
    int on = 1;
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(char*)&on,sizeof(on));
    setsockopt(listenfd,SOL_SOCKET,SO_REUSEPORT,(char*)&on,sizeof(on));
    


    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd >= 0);
    //注意,监听socket上时不能注册EPOLLONESHOT事件的,因为后续的客户连接请求将不会触发EPOLLIN
    addfd(epollfd,listenfd,false);
    
    while(1)
    {
        int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
        if(ret < 0)
        {
            printf("epoll failure");
            break;
        }

        for(int i = 0;i < ret;i++)
        {
            int sockfd = events[i].data.fd;
            if(sockfd == listenfd)
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength;
                int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
                //更改缓冲区大小,模拟ET模式下一个事件触发多次
                int in = 200;
                setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,(void*)&in,sizeof(in));
                //注册EPOLLONESHOT事件;
                //addfd(epollfd,connfd,false);
                addfd(epollfd,sockfd,true);
            }
            else if(events[i].events & EPOLLIN)
            {
                pthread_t thread;
                fds new_worker;
                new_worker.epollfd = epollfd;
                new_worker.sockfd = sockfd;
                pthread_create(&thread,NULL,worker,(void*)&new_worker);
            }
            else
            {
                printf("something else happened\n");
            }
            
        }
    }

    close(listenfd);
    return 0;

}

client_tess.cpp

#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;

int main(int argc,char** argv)
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));

    ser_addr.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&ser_addr.sin_addr.s_addr);
    ser_addr.sin_port = htons(port);

    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    assert(sockfd >= 0);
    if(connect(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
    {
        cout<<"error"<<endl;
    }
    else
    {
        string str(2000,'a');
        int ret = send(sockfd,str.c_str(),str.size(),0);
        cout<<"ret = "<<ret<<endl;
    }

    return 0;

}

通过缩小接受缓冲区的大小,发送一个远远大于接受缓冲区的数据来模拟多个线程同时处理一个socket的现象,通过开启EPOLLONESHOT避免这种情况。

要点

  • 处理fd的读事件,一直读一直读,读到没有数据 ( errno==EAGAIN||errno == EWOULDBLOCK) ,这时才重置fd上的事件 。
  • 重置fd上的事件,这样尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次。如果不进行重置,下一次读事件到来时就不会触发EPOLLIN.
  • 监听描述符listenfd不能注册EPOLLONESHOT,否则只能处理一个客户连接
  • 更改缓冲区的大小setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,(void*)&in,sizeof(in));
  • 非阻塞IO下的收发包逻辑

重置代码

void reset_oneshot(int epollfd,int fd)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN|EPOLLOUT|EPOLLONESHOT;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值