【IO多路转接】Epoll

当文件描述符数量过多时,Select和Poll都会表现出性能偏低的问题,因此Epoll在Poll的基础上做出了改进
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

Epoll相关系统调用

epoll_create

#include <sys/epoll.h>

int epoll_create(int size);

创建一个epoll句柄

  • size参数可被忽略
  • 必须调用close进行关闭

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 不同于select在监听时告诉内核事件类型,而是先要注册要监听的事件类型,即用户要告诉内核需要关心哪个文件描述符上的哪个事件
  • epfd为epoll_create的返回值(epoll句柄)
  • op为对fd的操作方法
  • fd为要监听的文件描述符
  • event为要监听fd的事件类型

关于op参数

  • EPOLL_CTL_ADD:注册新的fd到epfd中
  • EPOLL_CTL_MOD:修改已注册fd中的监听事件
  • EPOLL_CTL_DEL:从epfd中删除fd

关于epoll_event

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

struct epoll_event
{
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};
  • epoll_event中包含了events和data,其中events即为要监听事件的类型,data为用户数据,内核不会对用户数据做修改
  • events可以是以下几个宏的集合

EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

epoll_wait

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epoll_wait用来收集epoll中已就绪的事件
  • epoll将已就绪的事件发送到events数组中,这个events需要用户自己管理,内核只负责把数据复制到数组中,而不对数组大小做管理,因此events不能为空指针
  • maxevents为events的大小
  • timeout为超时时间,单位为毫秒(ms)
  • epoll_wait成功返回已就绪文件描述符的个数,返回0表示等待超时,返回小于0表示函数等待失败

Epoll工作原理

Epoll会管理一棵红黑树,红黑树的每一个节点包含了fd,events以及相关链接属性等字段,在当底层通知OS事件就绪时,该“就绪节点”会调用自己的回调机制,将自己链接到就绪队列中,最后内核再将就绪队列中的数据拷贝给用户。

在这里插入图片描述

整个Epoll过程

  1. epoll_create()创建epoll
  2. epoll_ctl()注册事件
  3. epoll_wait()等待事件就绪

Epoll常见程序片段

int epfd = epoll_create(size);

struct epoll_event ev;
ev.data.fd = sock;
ev.events = EPOLLIN;//以读事件为例

epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);//注册该事件

struct epoll_event revs[num];
int n = epoll_wait(epfd, revs, num, timeout);

for (int i = 0; i < n; ++i)
{
	int fd = revs[i].data.fd;
	uint32_t events = revs[i].events;
	//TODO
}
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
close(epfd);

Epoll的优点

  • 接口方便,不需要循环设置文件描述符,管理事件
  • 不需要反复拷贝,在epoll_ctl中的EPOLL_CTL_ADD操作并不频繁,只需要在最开始时将文件描述符拷贝到内核,最后通过EPOLL_CTL_DEL删除即可,而select和poll在每次使用时都需要循环拷贝文件描述符,增大了开销
  • 底层红黑树插入效率高(lgN),而select和poll插入效率都为O(N)
  • 事件回调机制,能够在事件就绪时直接将节点插入到就绪序列,也能避免重复拷贝,即使文件描述符数量过多也不会影响效率
  • 没有文件描述符数量的限制

Epoll的两种工作方式

为了介绍两种工作方式我就举个栗子
一天你正在打游戏,这个时候你麻麻喊你吃饭
一种情况是你麻麻喊了一遍,你没有反应,于是你麻麻喊了第二遍,第三遍…直到你去吃饭,这就是水平触发
另一种情况是你麻麻喊了一遍,你没动,至此之后,你麻麻不管你了再也没喊过你,这就是边缘触发

水平触发(Level Triggered)

epoll默认状态下就是水平触发模式

  • 当epoll检测到有事件就绪时,可以进行处理也可以不处理,又或者处理一部分
  • 如果不处理或处理一部分,下次调用epoll_wait时epoll会继续通知该事件处于就绪状态
  • 直到缓冲区中的数据被读完,epoll_wait不返回
  • 读写支持阻塞读写和非阻塞读写

边缘触发(Edge Triggered)

如果将事件中添加EPOLLET属性,那么epoll的模式会变为ET工作模式

  • 当epoll检测到事件就绪时,必须立刻处理
  • 如果只处理一部分或者不处理,下次调用epoll_wait时则不会通知该事件处于就绪状态
  • 也就是说在ET模式下,处理事件的机会只有一次
  • ET的性能要比LT高,因为epoll_wait的返回次数更少,也可以理解为同样的事件,只通知一次就完成了,效率也就更高了
  • 只支持非阻塞读写

为什么ET模式只支持非阻塞读写?
因为阻塞式读写可能是一次性把数据读完,但也有可能是循环读,如果是循环读就会造成数据一次不能读完,也就破坏了ET模式工作的平衡性。

select和poll的工作模式是LT,而epoll可以支持LT或者ET,ET模式下读写必须采用非阻塞轮询的方式进行。

将fd设置为非阻塞IO

一个文件描述符默认状态下都是阻塞式IO

fcntl函数原型

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

传入的cmd不同,后面追加的参数也不同
fcntl函数有五种功能

  • 复制一个现有的描述符(cmd=F_DUPFD)
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

此处用第三种功能就可以将fd设置为非阻塞式IO

实现SetNonBlock

void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) 
	return;
else
	fcntl(fd, F_SETFL, fl | O_NONBLOCK)}
  • 使用F_GETFL将该fd的属性提取出来(是一个位图)
  • 然后再使用F_SETFL将fd中的属性追加O_NONBLOCK

对比LT和ET

  • LT模式为默认行为,ET模式能减少响应次数,这也就倒逼程序员必须一次响应就绪就将数据取完
  • 但如果在LT模式下也能做到响应一次就绪就将数据取完,二者的性能其实是一样的

实现简易版Epoll服务器(LT/ET)

写之前先封装一下socket

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>

static const int gbacklog = 32;
const int defaultfd = -1;

class Sock
{
public:
    Sock() : _sock(defaultfd)
    {
    }
    void Socket()
    {
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            exit(-1);
        }
        // 设置地址是复用的
        int opt = 1;
        setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }
    void Bind(const uint16_t &port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(-1);
        }
    }
    void Listen()
    {
        if (listen(_sock, gbacklog) < 0)
        {
            exit(-1);
        }
    }
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int sock = accept(_sock, (struct sockaddr *)&temp, &len);
        if (sock < 0)
        {
        }
        else
        {
            *clientip = inet_ntoa(temp.sin_addr);
            *clientport = ntohs(temp.sin_port);
        }
        return sock;
    }
    int Connect(const std::string &serverip, const uint16_t &serverport)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());

        return connect(_sock, (struct sockaddr *)&server, sizeof(server));
    }
    int Fd()
    {
        return _sock;
    }
    void Close()
    {
        if (_sock != defaultfd)
            close(_sock);
    }
    ~Sock()
    {
    }

private:
    int _sock;
};

LT模式

///
// 这里只考虑读的情况,并做本地测试
///
#include <iostream>
#include <sys/epoll.h>
#include <functional>
#include <memory>
#include "Sock.hpp"

static const int defaultepfd = -1;
static const int gsize = 128;
using func_t = std::function<std::string(std::string)>;

class Epoller
{
public:
    Epoller() : epfd_(defaultepfd)
    {
    }
    void Create()
    {
        epfd_ = epoll_create(gsize);
        if (epfd_ < 0)
        {
            exit(-1);
        }
    }
    // 用户 -> 内核
    bool AddModEvent(int fd, uint32_t events, int op) //
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd; // 用户数据, epoll底层不对该数据做任何修改,就是为了给未来就绪返回的!
        int n = epoll_ctl(epfd_, op, fd, &ev);
        if (n < 0)
        {
            return false;
        }
        return true;
    }
    bool DelEvent(int fd)
    {
        // epoll在操作fd的时候,有一个要求,fd必须合法!
        return epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }
    int Wait(struct epoll_event *revs, int num, int timeout)
    {
        //
        return epoll_wait(epfd_, revs, num, timeout);
    }
    int Fd()
    {
        return epfd_;
    }
    void Close()
    {
        if (epfd_ != defaultepfd)
            close(epfd_);
    }
    ~Epoller()
    {
    }

private:
    int epfd_;
};

class EpollServer
{
    const static int gnum = 64;
    const static int gport = 8888;

public:
    EpollServer(func_t func, uint16_t port = gport) : func_(func), port_(port) {}
    void InitServer()
    {
        // 创建、绑定、监听
        listensock_.Socket();
        listensock_.Bind(gport);
        listensock_.Listen();
        epoller_.Create(); // 创建epoll
        // 将listensock添加到epoll中负责监听新连接
        epoller_.AddModEvent(listensock_.Fd(), EPOLLIN, EPOLL_CTL_ADD);
    }
    void Start()
    {
        int timeout = -1;
        while (true)
        {
            int n = epoller_.Wait(revs_, gnum, timeout); // 等待事件就绪
            switch (n)
            {
            case 0:
                std::cout << "timeout..." << std::endl;
                break;
            case -1:
                std::cerr << "epoll_wait failed" << std::endl;
                break;
            default:
                HandlerEvent(n);
                break;
            }
        }
    }
    void HandlerEvent(int num)
    {
        for (int i = 0; i < num; ++i)
        {
            int fd = revs_[i].data.fd;
            uint32_t events = revs_[i].events;
            if (events & EPOLLIN) // 读
            {
                if (fd == listensock_.Fd()) // 如果是监听成功
                {
                    std::string clientip;
                    uint16_t clientport;
                    int sock = listensock_.Accept(&clientip, &clientport); // 获取连接
                    if (sock < 0)
                    {
                        // 获取失败则继续获取
                        continue;
                    }
                    else
                    {
                        printf("%s:%d 已经连上了服务器了", clientip.c_str(), clientport);
                        fflush(stdout);
                        // 获取成功则将fd添加到epoll中
                        epoller_.AddModEvent(sock, EPOLLIN, EPOLL_CTL_ADD);
                    }
                }
                else // 其他读事件
                {
                    char request[1024];
                    ssize_t s = recv(fd, request, sizeof(request) - 1, 0);
                    if (s > 0) // 读取成功
                    {
                        request[s - 1] = 0; // \r\n
                        request[s - 2] = 0; // \r\n

                        std::string response = func_(request); // 执行业务处理,以telnet回显为例

                        send(fd, response.c_str(), response.size(), 0);
                    }
                    else // 读取异常
                    {
                        // 在处理异常的时候,先从epoll中移除,然后再关闭
                        epoller_.DelEvent(fd);
                        close(fd);
                    }
                }
            }
        }
    }
    ~EpollServer()
    {
        // 关闭fd
        listensock_.Close();
        epoller_.Close();
    }

private:
    uint16_t port_;
    Sock listensock_;
    Epoller epoller_;
    func_t func_;
    struct epoll_event revs_[gnum];
};

std::string echoServer(std::string r)
{
    std::string resp = r;
    resp += "[echo]\r\n";

    return resp;
}

int main()
{
    std::unique_ptr<EpollServer> svr(new EpollServer(echoServer));
    svr->InitServer();
    svr->Start();

    return 0;
}

ET模式

ET模式与LT模式类似,不过要在添加fd之前,增加非阻塞属性以及将epoll改为ET模式

#include <iostream>
#include <sys/epoll.h>
#include <functional>
#include <fcntl.h>
#include <memory>
#include "Sock.hpp"

static const int defaultepfd = -1;
static const int gsize = 128;
using func_t = std::function<std::string(std::string)>;

// 设置非阻塞IO
void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        std::cerr << "error string: " << strerror(errno)
                  << "error code:" << errno << std::endl;
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

class Epoller
{
public:
    Epoller() : epfd_(defaultepfd)
    {
    }
    void Create()
    {
        epfd_ = epoll_create(gsize);
        if (epfd_ < 0)
        {
            exit(-1);
        }
    }
    // 用户 -> 内核
    bool AddModEvent(int fd, uint32_t events, int op) //
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd; // 用户数据, epoll底层不对该数据做任何修改,就是为了给未来就绪返回的!
        int n = epoll_ctl(epfd_, op, fd, &ev);
        if (n < 0)
        {
            return false;
        }
        return true;
    }
    bool DelEvent(int fd)
    {
        // epoll在操作fd的时候,有一个要求,fd必须合法!
        return epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }
    int Wait(struct epoll_event *revs, int num, int timeout)
    {
        //
        return epoll_wait(epfd_, revs, num, timeout);
    }
    int Fd()
    {
        return epfd_;
    }
    void Close()
    {
        if (epfd_ != defaultepfd)
            close(epfd_);
    }
    ~Epoller()
    {
    }

private:
    int epfd_;
};

class EpollServer
{
    const static int gnum = 64;
    const static int gport = 8888;

public:
    EpollServer(func_t func, uint16_t port = gport) : func_(func), port_(port) {}
    void InitServer()
    {
        // 创建、绑定、监听
        listensock_.Socket();
        listensock_.Bind(gport);
        listensock_.Listen();
        epoller_.Create(); // 创建epoll
        // 将listensock添加到epoll中负责监听新连接
        // 这里没有把listensock设置为非阻塞,稍微麻烦一点,此处暂不实现
        epoller_.AddModEvent(listensock_.Fd(), EPOLLIN, EPOLL_CTL_ADD);
    }
    void Start()
    {
        int timeout = -1;
        while (true)
        {
            int n = epoller_.Wait(revs_, gnum, timeout); // 等待事件就绪
            switch (n)
            {
            case 0:
                std::cout << "timeout..." << std::endl;
                break;
            case -1:
                std::cerr << "epoll_wait failed" << std::endl;
                break;
            default:
                HandlerEvent(n);
                break;
            }
        }
    }
    void HandlerEvent(int num)
    {
        for (int i = 0; i < num; ++i)
        {
            int fd = revs_[i].data.fd;
            uint32_t events = revs_[i].events;
            if (events & EPOLLIN) // 读
            {
                if (fd == listensock_.Fd()) // 如果是监听成功
                {
                    std::string clientip;
                    uint16_t clientport;
                    int sock = listensock_.Accept(&clientip, &clientport); // 获取连接
                    if (sock < 0)
                    {
                        // 获取失败则继续获取
                        continue;
                    }
                    else
                    {
                        printf("%s:%d 已经连上了服务器了", clientip.c_str(), clientport);
                        fflush(stdout);
                        SetNonBlock(fd); // 将fd设置为非阻塞
                        // 获取成功则将fd添加到epoll中并设置为ET模式
                        epoller_.AddModEvent(sock, EPOLLIN | EPOLLET, EPOLL_CTL_ADD);
                    }
                }
                else // 其他读事件
                {
                    // 这里写的有一点不严谨
                    do
                    {
                        char request[2048];
                        ssize_t s = recv(fd, request, sizeof(request) - 1, 0);
                        if (s > 0) // 读取成功
                        {
                            request[s - 1] = 0; // \r\n
                            request[s - 2] = 0; // \r\n

                            std::string response = func_(request); // 执行业务处理,以telnet回显为例

                            send(fd, response.c_str(), response.size(), 0);
                        }
                        else if (s == 0)
                        {
                            // 对方读端关闭,因此直接break
                            break;
                        }
                        else
                        {
                            if (errno == EAGAIN || errno == EWOULDBLOCK)
                                // EAGAIN和EWOULDBLOCK表示读缓冲区已空,需要重新尝试读取
                                continue;
                            else if (errno == EINTR)
                                // EINTR表示读取被中断,也需要重新尝试读取
                                continue;
                            else
                                break;
                        }
                    } while (events & EPOLLET);
                }
            }
        }
    }
    ~EpollServer()
    {
        // 关闭fd
        listensock_.Close();
        epoller_.Close();
    }

private:
    uint16_t port_;
    Sock listensock_;
    Epoller epoller_;
    func_t func_;
    struct epoll_event revs_[gnum];
};

std::string echoServer(std::string r)
{
    std::string resp = r;
    resp += "[echo]\r\n";

    return resp;
}

int main()
{
    std::unique_ptr<EpollServer> svr(new EpollServer(echoServer));
    svr->InitServer();
    svr->Start();

    return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值