有关于epollout事件触发的实验&epoll坑

epollout事件触发机制 & epoll坑


在网上看了很多关于epollout事件触发的文章,说的有点千奇百怪。一头雾水。之前自己做的实验也无法验证,一直没有思路,昨天晚上看epoll原理图的时候突然想到了解决方案。

感觉很多文章都是在说自己转的别人的,或者听别人说的,也一直没有说明白,所以给出实验代码,也想讲明白。

epoll机制

epoll底层机制是红黑树,以fd为key的key-value数据结构。红黑树有俩种类型,一种可以插入多个key,一种不可以插入多个key。经过验证后,我认为epoll底层的红黑树是不可以插入多个key的。

结论

先给出结论
LT模式下EPOLLOUT只要在写缓冲区有空间下,就会就绪
ET模式下EPOLLOUT只要由不可写变为可写,就会就绪,注意特殊的一点是EPOLLOUT在注册时的第一次会就绪。即以前不关注OUT事件,那么就可以认为其不可写,现在关注了,发现是可写,就有一次不可写变为可写脉冲。

ET模式下EPOLLIN和EPOLLOUT不可以同时注册

实验

实验基本文件,后序均是在此基础上实验

服务器端

#server.cc
#include <iostream>
#include <vector>

#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>

#define PORT "8080"
#define IP  // 待补充
#define BUFFER_SIZE 4096 /*读缓冲区大小*/

#define my_error(x, str) \
    do{\
        if((x) < 0)\
        {   perror(str);printf("%s\n",str); }\
    }while(0)

#define ERR_EXIT(str) \
    do{\
        perror(str);\
        exit(EXIT_FAILURE);\
    }while(0)

using namespace std;
using EventVec = vector<struct epoll_event> ;

int main(int argc, char**argv)
{
    // 服务器地址和端口设置
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    // inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(atoi(PORT));

    int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
    int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // 高内核版本直接可以使用
    // CLOEXEC,当fork后,会关闭该文件描述符

    my_error(listenfd, "socket error");

    // 设置了端口立即重启, 即取消time_wait状态
    int reuse = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    my_error(ret, "bind error");
    ret = listen(listenfd, 5);
    my_error(ret, "listen error");

    EventVec events(16);

    int epollfd = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event event;
    event.data.fd = listenfd;
    event.events = EPOLLIN;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);

    vector<int> clients;
    int nready = 0;
    int connfd = 0;
    while(1)
    {
        nready = epoll_wait(epollfd, events.data(), events.size(), -1);  
        if(nready == -1)
        {
            if(errno == EINTR)
                continue;
            perror("epoll");
            exit(-1);
        }
        else if(nready == 0) // timeout到了,但没有人就绪,目前这种情况下不可能
            continue;
        else if(nready > 0)
        {
            if(nready == static_cast<int>(events.size()))
                events.resize(2*events.size());
            for(int i = 0; i < nready; ++i)
            {
                if(events[i].data.fd == listenfd && events[i].events & EPOLLIN)
                {
                    struct sockaddr_in client_address;
                    socklen_t client_addrlength = sizeof(client_address);
                    connfd = accept4(listenfd, (struct sockaddr*)&client_address, &client_addrlength, SOCK_NONBLOCK|SOCK_CLOEXEC);
                    if(-1 == connfd)
                    {
                        /*if(EMFILE == errno) // 这里是用来处理EMFILE错误的,在实验内可以忽略
                        {
                            close(idlefd);
                            idlefd = accept(listenfd, nullptr, nullptr);
                            close(idlefd);
                            idlefd = open("/dev/null", O_RDONLY|O_CLOEXEC);
                            continue;
                        }
                        else{*/
                            ERR_EXIT("accept");
                        //}
                    }
                    cout << "client_address " 
                        << inet_ntoa(client_address.sin_addr) 
                        << " fd: "
                        << connfd 
                        << " client connected" << endl;
                    event.data.fd = connfd;
                    event.events = EPOLLIN;
                    epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
                    clients.push_back(connfd);
                }
                for(auto it = clients.begin(); it != clients.end(); ++it)
                {
                if(events[i].data.fd == *it)
                {
                    connfd = *it;

                    if(events[i].events & EPOLLIN)
                        cout << " event read " << endl;
                    if(events[i].events & EPOLLOUT)
                    {
                        cout << " event write " << endl;
                    }
                    cout << "one time" << endl;
                    event.data.fd = connfd;
                    event.events = EPOLLOUT;
                    epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);

                    char buffer[1024] = {0};
                    do{
                        ret = recv(connfd, buffer, sizeof(buffer), 0); // 将收到的字节全部读出来。
                    }while(ret >= 0 || errno != EAGAIN);

                    sleep(2);
                    continue;
                }
                }
            }
        }
    }
    close(listenfd);
    return 0;
}

客户端

#include <iostream>

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define PORT "8080"
#define IP "172.16.79.129"

#define my_error(x, str) \
    do{\
        if((x) < 0)\
        printf("%s\n",str);\
    }while(0)

int main()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    my_error(fd, "socket error");

    // 设置连接服务器地址
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port = htons(atoi(PORT));
    inet_pton(AF_INET, IP, &address.sin_addr);

    int ret = connect(fd, (struct sockaddr*) &address, sizeof(address));
    my_error(ret, "connect error");

    char buffer[1024] = {0};
    // 客户端就是从终端收到数据,然后向服务端写,为了触发服务器端读事件
    while(1)
    {
        bzero(buffer, sizeof(buffer));
        int nread = read(STDIN_FILENO, buffer, sizeof(buffer));
        send(fd, buffer, nread, 0);
    }
    close(fd);
}

验证开始

验证一:epoll底层红黑树只允许插入单个key

即一个fd只能add一个事件。后面的add不会覆盖。注意我指的就是EPOLL_CTL_ADD这个操作。
我在accept后EPOLL_CTL_ADD写事件EPOLLOUT。
然后在通信逻辑中EPOLL_CTL_ADD读事件EPOLLIN。

// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD/*EPOLL_CTL_MOD */, connfd, &event);

实验结果

  1. 运行服务器端
  2. 客户端连接后,写事件触发,输出event write
  3. 客户端向服务器端发送数据,服务器端按理来说应该读事件就绪,会输出event read,事实上,我们根本没有让epoll关注读事件成功。
// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);

修改代码后,实验出现我们想要的结果。服务器端定时输出event write。客户端每向服务器端发送一次数据后。会有event read输出。

结论
  1. 实践:epoll只会认准第一次EPOLL_CTL_ADD的事件,比如一开始关注读事件,如果后序想关注这个fd的写事件,不能使用EPOLL_CTL_ADD,请使用EPOLL_CTL_MOD配合EPOLLIN | EPOLLOUT的方式。
  2. 真理:epoll中的红黑树的元素是基于fd的,且只允许有一个fd。所以如果对一个fd试图调用ADD多次,只会添加最初的一次。
    • 我最开始的错误想法就是epoll的红黑树的元素是基于事件类型的,如果想关注读写事件就要ADD,EVENTIN一次,ADD,EVENTOUT一次,事实上,要么使用ADD,EVENTIN|EVENTOUT一部到位。要么就是ADD,EVENTIN,然后MOD,EVENTIN|EVENTOUT。
    • 不知道说明白没有。
  3. 之前看到有个问他这样做为什么服务器端写事件没有就绪。
event.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

其原因一定是因为这是他这是第二次对这个fd进行ADD操作。我猜测他的逻辑是这样。
读事件就绪后,进入处理逻辑,然后向关注写事件,所以用了ADD,这里应该是用MOD

验证二:EPOLLOUT的触发时机

网上说什么连接时会触发一次,EPOLLIN|EPOLLOUT同时注册会触发一次什么的,很乱。
EPOLLOUT触发条件只有一个,内核缓冲区可写。

LT
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

直接注册OUT事件,因为该fd的内核写缓冲区一直为空,所以一直输出event write

// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN|EPOLLOUT; 
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);

实验结果同上,网上说的EPOLLIN|EPOLLOUT同时注册的意思,并不是因为同时注册的原因。而是因为其写缓冲区本身就可写,我OUT事件为什么不触发。

// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

// 在后面的和connfd通信逻辑中
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
event.data.fd = connfd;
event.events = EPOLLERR|EPOLLOUT; 
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

我这里先由读事件就绪,然后将关注的读事件删除,然后再将EPOLLERR|EPOLLOUT添加。依然会触发写事件,这里绕这么多弯弯的原因就是向告诉大家,OUT触发条件很简单。就是该fd的内核写缓冲区为空时,OUT事件就会就绪。

ET

上面仅仅是一些小测试,我们实际关心的还是在ET模式下,配合nonblock r/w来编程。
这里OUT事件触发条件也很简单,就是由不可写变为可写时会触发。

// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT|ET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);

内核写缓冲区由满变为不满时,write会由不可写变为可写这个大家都很理解。主要是在一开始触发一次即所谓的在连接是触发一次的原因可以这么理解。最开始不存在fd的时候,是没有fd的内核写缓冲区这么一个概念的,所以最开始其是不可写状态。最后该fd的内核写缓冲区建立之后,变为可写状态。所以存在这么一个由不可写变为可写的状态,导致在最一开始ET模式下accept后该fd关注写事件后,从无到有这么一个脉冲导致OUT事件会就绪一次。

猜你喜欢

转载自blog.csdn.net/weixin_43468441/article/details/88710514