socket通信之epoll模型

综合select和poll的一些优缺点,Linux从内核2.6版本开始引入了更高效的epoll模型,本节我们来详细介绍epoll模型。

linux中并没有一个epoll函数,而是提供了三个以epoll开头的函数:

  • epoll_create
  • epoll_ctl
  • epoll_wait

有关linux官方对epoll的解释可以参考man 7 epoll

epoll_create

epoll_create函数原型如下:

#include <sys/epoll.h>

int epoll_create(int size);

函数说明:

  • 参数size:从Linux 2.6.8以后就不再使用,但是必须设置一个大于0的值。
  • 返回值:调用成功返回一个非负值的文件描述符fd(本文称之为epollfd),调用失败返回-1,这个epollfd主要是供下面两个函数使用。

epoll_ctl

有了epollfd之后,我们需要将我们需要检测事件的其他fd绑定到这个epollfd上,或者修改一个已经绑定上去的fd的事件类型,或者在不需要时将fd从epollfd上解绑,这都可以使用epoll_ctl函数。

epoll_ctl函数原型如下:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数说明:

  • 参数epfd:调用epoll_create函数创建的epollfd。
  • 参数op:操作类型,取值有EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL,分别表示向epollfd上添加、修改和移除一个其他fd,当取值是EPOLL_CTL_DEL,第四个参数event忽略不计,可以设置为NULL。
  • 参数fd:即需要被操作的fd。
  • 参数event:是一个epoll_event结构体的地址,epoll_event结构体定义下面会详细介绍。
  • 返回值:调用成功返回0,调用失败返回-1,你可以通过errno错误码获取具体的错误原因。

epoll_event结构体定义如下:

struct epoll_event {
    uint32_t     events; // 需要检测的fd事件,取值与poll函数一样
    epoll_data_t data; // 用户自定义数据
};

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll_event支持的事件宏如下表:

事件宏

描述

EPOLLIN

数据可读(包括普通数据&优先数据)

EPOLLOUT

数据可写(包括普通数据&优先数据)

EPOLLRDHUP

TCP连接被对端关闭,或者关闭了写操作

EPOLLPRI

高优先级数据可读,例如 TCP 带外数据

EPOLLERR

错误

EPOLLHUP

挂起

EPOLLET

边缘触发模式

EPOLLONESHOT

最多触发其上注册的事件一次

epoll_wait

创建了epollfd,设置好某个fd上需要检测事件并将该fd绑定到epollfd上去后,我们就可以调用epoll_wait检测事件了。

epoll_wait函数原型如下:

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数说明:

  • 参数epfd:调用epoll_create函数创建的epollfd。
  • 参数events:是一个epoll_event结构数组的首地址,这是一个输出参数,函数调用成功后,events中存放的是与就绪事件相关epoll_event结构体数组。
  • 参数maxevents:数组元素的个数。
  • 参数timeout:超时时间,单位是毫秒,如果设置为0,epoll_wait会立即返回。
  • 返回值:调用成功会返回有事件的fd数目;如果返回0表示超时;调用失败返回-1。

实例

#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>
#include <sys/epoll.h>

int main() {
    // 创建一个侦听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    // 初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) {
        std::cout << "bind listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    // 启动侦听
    if (listen(listenfd, SOMAXCONN) == -1) {
        std::cout << "listen error." << std::endl;
        close(listenfd);
        return -1;
    }
    
    std::cout << "listen fd." << listenfd << std::endl;
    
    // epoll_create
    int epollfd = epoll_create(1);
    if(-1 == epollfd) {
        std::cout << "epoll_create error." << std::endl;
        close(listenfd);
        return -1;
    }

    epoll_event listen_fd_event;
    listen_fd_event.data.fd = listenfd;
    listen_fd_event.events = EPOLLIN;
    
    if(-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event)) {
        std::cout << "epoll_ctl error." << std::endl;
        close(listenfd);
        return -1;
    }
    
    while (true) {
        epoll_event epoll_events[1024];

        int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
        
        if (n < 0) {
            // 被信号中断
            if (errno == EINTR) 
                continue;
            // 出错,退出
            break;
        } else if (n == 0){
            // 超时,继续
            continue;
        }
        
        for(int i = 0; i < n; i++) {
            std::cout << "events." << epoll_events[i].events << std::endl;
            std::cout << "fd." << epoll_events[i].data.fd << std::endl;
            if(epoll_events[i].events & EPOLLIN) {
                if(epoll_events[i].data.fd == listenfd) {
                    // 侦听socket的可读事件,则表明有新的连接到来
                    struct sockaddr_in clientaddr;
                    socklen_t clientaddrlen = sizeof(clientaddr);
                    // 接受客户端连接
                    int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
                    if (clientfd == -1) {           
                        //接受连接出错,退出程序
                        break;
                    }
                    
                    epoll_event client_fd_event;
                    client_fd_event.data.fd = clientfd;
                    client_fd_event.events = EPOLLIN;
                    
                    if(-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event)) {
                        std::cout << "epoll_ctl error." << std::endl;
                        close(clientfd);
                        return -1;
                    }
                
                    //只接受连接,不调用recv收取任何数据
                    std:: cout << "accept a client connection, fd: " << clientfd << std::endl;
                } else {

                    // 有数据可以读
                    char recvbuf[32] = {0};
                    int clientfd = epoll_events[i].data.fd;
                    std::cout << "before recv: " << n << std::endl;
                    int length = recv(clientfd, recvbuf, 32, 0);
                    std::cout << "after recv: " << n << std::endl;
                    
                    if (length <= 0 && errno != EINTR) {
                        // 收取数据出错了
                        std::cout << "recv data error, clientfd: " << clientfd << std::endl;                            
                        close(clientfd);
                        continue;
                    }
                    std::cout << "clientfd: " << clientfd << ", recv data: " << recvbuf << std::endl;   
                    
                    // send data
                    int ret = send(clientfd, recvbuf, strlen(recvbuf), 0);
                    if (ret != strlen(recvbuf)) {
                        std::cout << "send data error., clientfd: " << clientfd << std::endl;   
                        close(clientfd);
                    }
                }                   
            }
        }
    }
    //关闭侦听socket
    close(listenfd);

    return 0;
}

LT模式和ET模式

select()、poll()模型都是水平触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

  • LT:(Level_triggered,水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你,如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
  • ET:(Edge_triggered,边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你,这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

这两个词汇来自电学术语,你可以将fd上有数据认为是高电平,没有数据认为是低电平,将fd可写认为是高电平,fd不可写认为是低电平。

水平模式的触发条件:

  1. 低电平 => 高电平
  2. 处于高电平状态

边缘模式的触发条件:

  1. 低电平 => 高电平

也就是说,如果对于一个非阻塞socket,如果使用epoll边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把socket上的数据收取干净才行,也就是说一定要循环调用recv函数直到recv出错,错误码EWOULDBLOCK(EAGAIN 一样)(此时表示socket上本次数据已经读完);如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。

select、poll与epoll的区别

select:

  • 优点:目前几乎在所有的平台上支持。
  • 缺点:
  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,当fd很多时开销大;
  2. select返回时又需要把就绪的fd集合从内核态拷贝到用户态,当fd很多时开销也大;
  3. select支持的文件描述符数量太小了,32位机器默认是1024,64位机器默认2048。

poll:调用过程和select类似,时间复杂度:O(n),它没有最大连接数的限制,原因是它是基于链表来存储文件描述符的。

epoll优点:

  1. 监视的描述符数量不受限制
  2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
  3. epoll只需要第一次注册一次,后面的监听无需每次传递fds,epoll是通过内核与用户空间mmap同一块内存,避免了无畏的内存拷贝。

当然,这并不意味着,poll模型的效率不如epoll,一般在fd数量比较多,但某段时间内,就绪事件fd数量较少的情况下,epoll才会体现出它的优势,也就是说socket连接数量较大时而活跃连接较少时epoll模型更高效。