实战Linux I/O多路复用:借助epoll,单线程高效管理10,000+并发连接

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 本文介绍了如何使用Linux的I/O多路复用技术`epoll`来高效管理超过10,000个并发连接。`epoll`允许单线程监控大量文件描述符,显著提高了资源利用率。文章详细阐述了`epoll`的几个关键接口,包括`epoll_create`、`epoll_ctl`和`epoll_wait`,以及它们在处理并发连接中的作用。此外,还探讨了`epoll`在高并发TCP服务场景的应用,展示了如何通过`epoll`和线程/协程池来构建服务框架。

实战Linux I/O多路复用:借助epoll,单线程高效管理10,000+并发连接

引言

  在应对高并发连接的传统策略中,普遍采取为每个连接配置单独线程或进程的直接方式,管理其I/O操作。此法虽直观易行,但随业务规模扩张,线程资源需求急剧上升。相反,Linux下的I/O多路复用技术,尤其是epoll,展示了一种高效路径:单一线程即可监控成千上万的文件描述符,极大提升了资源使用效率。

  I/O 多路复用的场景有很多,也比较实用。通常用法epoll线程 + 线程/协程池处理并发场景,这里做一个简单的实例使用,以便后续查阅。

概述

selectpoll同样能够满足多路复用的需求,在特定场景下各有千秋。不过,当面对需监控大量文件句柄的场景时,epoll凭借其高效的设计和更高的性能表现,成为更为优选的解决方案。其不仅在资源管理和事件处理上展现出明显优势,而且编程接口的灵活性也更为优雅。本文主要聚焦于epoll的实践应用,实例学习其高效而精炼的使用方法。

epoll常用接口

epoll的描述man手册已经记录比较详细了,这里列举一下常用的接口:

  1. epoll_create / epoll_create1
  • 原型: int epoll_create(int size) /  int epoll_create1(int flags)    
  • 功能: 创建一个新的epoll实例,返回一个文件描述符,该描述符代表epoll对象。
  • 参数:
  • size: 接受一个参数 size,在Linux 2.6.8以后这个参数被忽略,但仍要求传递一个大于0的值;
  • flags: 接收一个标志。为0作用与epoll_create相同;为EPOLL_CLOEXEC时,会在execve() 调用后自动关闭 epoll 文件描述符,避免子进程继承。
  • 返回值
  • -1:发生错误,设置errno> 0:epoll文件描述符。
  1. epoll_ctl
  • 原型:  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
  • 功能: 用于控制已经创建好的epoll实例中的文件描述符事件集合。
  • 参数:
  • epfd:epoll_create() 返回的文件描述符。
  • op:操作类型,可以是 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
  • fd:要操作的文件描述符。
  • event:一个指向struct epoll_event的指针,定义了关注的事件类型(如 EPOLLIN, EPOLLOUT)及其它数据。
  • 返回值
  • -1:发生错误,设置errno0:成功。
  1. epoll_wait
  • 原型: int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
  • 功能: 阻塞等待直到epoll实例中的一个或多个文件描述符变为就绪状态(可读、可写或出现错误)。
  • 参数:
  • epfd:epoll实例的文件描述符。
  • events:指向struct epoll_event结构体数组的指针,用于存储就绪事件。
  • maxevents:events 数组的最大容量。
  • timeout:等待超时时间,单位为毫秒,-1表示无限等待,0 表示立即返回,正值为等待的最长时间。
  • 返回值:
  • -1:发生错误,设置errno0:超时;>0: 准备好的文件描述符数量。

应用场景

  在高并发TCP服务场景中,服务端通过部署epoll + 线程/协程池机制,构建高效服务框架。epoll作为核心监听器,统一管理并快速响应来自不同客户端的连接请求,其事件驱动特性确保了对socket就绪状态的即时检测。与此同时,这些请求被异步地分发至线程/协程池中,利用任务队列和工作线程(或轻量级协程)并发执行,提升数据处理能力。

类图

EpollEventHandler类图

  • EpollEventHandler (Epoll事件调度器类)
    该类负责注册并管理监听句柄,实时监控Epoll事件,确保对每个就绪连接的快速响应与处理。
  • IEpollEvent (监听接口类)
    此类定义了句柄注册与事件处理的标准操作,使EpollEventHandler能统一管理不同类型的监听对象,实现接口的标准化与句柄处理的灵活性。
  • PSocket (可被监听的Socket实现类)
    继承自IEpollEvent的实现类,封装标准的Socket操作,同时定义针对Epoll事件的响应逻辑,实现Socket交互的统一管理和定制化处理。
  • PUart (可被监听的Uart实现类)
    继承自IEpollEvent的实现类,封装了标准Uart操作,同时定义针对Epoll事件的响应逻辑,实现Uart交互的统一管理和定制化处理。
  • 其他可被监听的实现类
    还可以实现其他可被epoll监听的类型类,通过继承IEpollEvent实现可被EpollEventHandler统一注册,再通过内部EpollEvent实现差异化响应处理。

源码实现

编程环境

① 编译环境: Linux环境

② 语言: C++语言

接口定义

  • EpollEventHandler
class EpollEventHandler
{
public:
    virtual ~EpollEventHandler();
    static EpollEventHandler* GetInstance();
    void AddPoll(IEpollEvent* p);
    void DelPoll(IEpollEvent* p);
    void EpollLoop(bool bRun);
private:
    EpollEventHandler(int size = 0);
private:
    int     mHandle;
    bool    mRun;
    std::map<int, IEpollEvent*> mEpollMap;   // fd, type, IEpollEvent
};

EpollEventHandler主要封装了epoll接口,集中管理并监听所有IEpollEvent实例。在EpollLoop循环中,阻塞等待并处理各类句柄事件,一旦事件触发,即通过多态调用IEpollEvent的虚函数来EpollEvent执行特定的事件处理逻辑,从而实现差异化的处理需求。

void EpollEventHandler::EpollLoop(bool bRun)
{
    struct epoll_event ep[32];
    mRun = bRun;
    do {
        if (!mRun) {
            break;
        }
        // 无事件时, epoll_wait阻塞, 等待
        int count = epoll_wait(mHandle, ep, sizeof(ep)/sizeof(ep[0]), -1);
        if (count <= 0) {
            continue;
        }
        for (int i = 0; i < count; i++) {
            IEpollEvent* p = (IEpollEvent*)ep[i].data.ptr;
            if (p == nullptr) {
                continue;
            }
            // TODO: 丢到线程/协程池响应
            p->EpollEvent(p->GetEpollFd(), p->GetEpollType(), p->GetArgs());
        }
    } while(mRun);
    SPR_LOGD("EpollLoop exit\n");
}
  • IEpollEvent
class IEpollEvent
{
public:
    IEpollEvent(int fd, EpollType eType = EPOLL_TYPE_BEGIN, void* arg = nullptr)
        : mEpollFd(fd), mEpollType(eType), mArgs(arg) {};
    virtual ~IEpollEvent() = default;
    virtual ssize_t Write(int fd, const std::string& bytes);
    virtual ssize_t Read(int fd, std::string& bytes);
    virtual void*   EpollEvent(int fd, EpollType eType, void* arg) = 0;
    int         GetEpollFd()        { return mEpollFd; }
    EpollType   GetEpollType()      { return mEpollType; }
    void*       GetArgs()           { return mArgs; }
protected:
    int         mEpollFd;
    EpollType   mEpollType;
    void*       mArgs;
};

IEpollEvent主要统一句柄注册与事件处理的标准操作,方便EpollEventHandler统一监听,通过EpollEvent实现差异化响应。

  • PSocket
class PSocket : public IEpollEvent
{
public:
    PSocket(int domain, int type, int protocol,
               std::function<void(int, void*)> cb, void* arg = nullptr);
    PSocket(int sock,
               std::function<void(int, void*)> cb, void* arg = nullptr);
    virtual ~PSocket();
    void Close();
    int AsTcpServer(short bindPort, int backlog);
    int AsTcpClient(bool con = false,
                    const std::string& srvAddr = "",
                    short srvPort = 0,
                    int rcvLen = 512 * 1024,
                    int sndLen = 512 * 1024);
    int AsUdpServer(short bindPort, int rcvLen = 512 * 1024);
    int AsUdpClient(const std::string& srvAddr, short srvPort, int sndLen = 512 * 1024);
    int AsUnixStreamServer(const std::string& serverName, int backlog);
    int AsUnixStreamClient(bool con = false,
                           const std::string& serverName = "",
                           const std::string& clientName = "");
    int AsUnixDgramServer(const std::string& serverName);
    int AsUnixDgramClient(const std::string& serverName);
    virtual void*   EpollEvent(int fd, EpollType eType, void* arg) override;
private:
    bool            mEnable;
    PSocketType     mSockType;
    std::function<void(int, void*)> mCb;
};
  • PUart
class PUart : public IEpollEvent
{
public:
    PUart(const std::string& devPath,
            std::function<void(int, char *, long, void*)> cb,
            void*   arg     = nullptr,
            speed_t rate    = B115200,
            int     parity  = 0,
            int     stopbit = 1
            );
    virtual ~PUart();
    void* EpollEvent(int fd, EpollType eType, void* arg) override;
    bool  SetupPort(speed_t rate, int parity, int stopbit);
    void  Close();
private:
    std::function<void(int, char *, long, void*)> mCb;
    std::string mDevFile;
};

测试效果

  • 测试代码这里实现一个TCP server的功能,响应多个客户端请求。
int main(int argc, const char *argv[])
{
    std::mutex epFdMutex;
    EpollEventHandler *pEpoll = EpollEventHandler::GetInstance();
    auto tcpClient = make_shared<PSocket>(AF_INET, SOCK_STREAM, 0, [&](int sock, void *arg) {
        PSocket* pCliObj = (PSocket*)arg;
        if (pCliObj == nullptr) {
            SPR_LOGE("PSocket is nullptr\n");
            return;
        }
        std::string rBuf;
        int rc = pCliObj->Read(sock, rBuf);
        if (rc > 0) {
            SPR_LOGD("# RECV [%d]> %s\n", sock, rBuf.c_str());
        } else {
            pEpoll->DelPoll(pCliObj);
            SPR_LOGD("## CLOSE [%d]\n", sock);
            std::lock_guard<std::mutex> lock(epFdMutex);
            pCliObj->Close();
        }
    });
    tcpClient->AsTcpClient(true, "127.0.0.1", 8080);
    pEpoll->AddPoll(tcpClient.get());
    std::thread wThread([&]{
        while(true) {
            std::lock_guard<std::mutex> lock(epFdMutex);
            tcpClient->Write(tcpClient->GetEpollFd(), "Hello World");
            sleep(1);
        }
    });
    pEpoll->EpollLoop(true);
    wThread.join();
    return 0;
}
  • 测试结果
$ ./sample_tcpserver
  81 EpollEvent D: Add epoll fd 4
  81 EpollEvent D: Add epoll fd 5
  81 EpollEvent D: Add epoll fd 6
  54 TcpServer D: # RECV [6]> I'm Client A
  58 TcpServer D: # SEND [6]> ACK
  54 TcpServer D: # RECV [5]> I'm Client B
  58 TcpServer D: # SEND [5]> ACK
  54 TcpServer D: # RECV [6]> I'm Client A
  58 TcpServer D: # SEND [6]> ACK
  54 TcpServer D: # RECV [5]> I'm Client B
  58 TcpServer D: # SEND [5]> ACK

测试结果上看,sample_tcpserver能够实现一个线程同时监听两个客户端的请求和应答。

总结

  • 本篇主要操练一下epoll的常规使用,简单做一下封装能够实现epoll监听各个类型的句柄事件。其实epoll还可以监听消息队列、串口等其他文件句柄,深入挖掘一下,能够实现很多优雅的操作。
  • 本实践深受先前一位导师兼朋友所分享代码的启发,其创新性地提出了采用epoll结合协程机制来替代传统多线程架构的方法,让我受益匪浅。
  • epoll的妙用远不止于此,后续的代码会不断挖掘,并集成到个人的开源项目中。
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
1天前
|
消息中间件 Linux 数据处理
Linux命令ipcrm详解:轻松管理IPC对象
`ipcrm`是Linux下用于删除IPC(进程间通信)对象的命令,如消息队列、共享内存和信号量。它通过指定对象ID或键值进行操作,如`-m ID`删除共享内存,`-q ID`删除消息队列,`-s ID`删除信号量。使用时需注意确认对象未被使用,以免影响系统运行。结合`ipcs`命令检查对象详情,并可定期清理不再需要的IPC对象以优化系统资源。
|
1天前
|
消息中间件 监控 安全
深入解析Linux命令ipcmk:IPC对象管理新视角
`ipcmk`非标准Linux命令,假设的IPC对象创建工具,用于演示如何管理消息队列、信号量和共享内存。虽无此命令,但理解其概念有助于掌握IPC管理。例如,创建命名消息队列`my_mq`,最大1000消息,可模拟使用`ipcmk -t mq -n my_mq -q 1000`。实际操作中,应根据需求选择合适IPC机制,设置安全权限,监控使用并及时清理。
|
1天前
|
存储 Linux 数据库
【Linux】Linux基础文件与目录管理:成为Linux大师的入门必修课
【Linux】Linux基础文件与目录管理:成为Linux大师的入门必修课
13 3
|
3天前
|
Linux Windows 虚拟化
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
【Linux环境搭建实战手册】:打造高效开发空间的秘籍
|
6天前
|
Linux 数据处理 数据安全/隐私保护
Linux中的groups命令:管理用户组信息的利器
`groups`命令在Linux中用于显示用户所属的用户组,帮助管理员进行权限管理。它读取`/etc/group`和`/etc/passwd`文件获取信息,特点是简单直观,支持多用户组。命令参数如`-a`显示主组,`-g`显示主组ID,`-n`以数字形式显示,`-r`显示实际组。在实际应用中,结合其他命令可进行权限分析和定制输出。注意权限问题及用户组可能随系统变化。
|
10天前
|
关系型数据库 MySQL Linux
Linux 命令 `db_upgrade` 详解与实战
`db_upgrade` 是一个自定义数据库升级命令,用于更新数据库结构和版本。它包括检查当前版本、备份、执行升级、更新版本信息和验证。基本语法是 `db_upgrade [OPTIONS]`,支持 `-b`(备份)、`-f`(强制升级)、`-v`(详细信息)等选项。在实战中,先备份数据库,然后使用 `db_upgrade` 命令升级,并验证结果。注意在生产环境升级前进行测试。虽然不是标准命令,但理解其用法有助于应对数据库升级。
|
12天前
|
监控 Linux
探索 Linux 中的 Chronyc:一个用于配置和管理 Chrony 的实用工具
Chronyc 是一款用于配置和管理 Linux 系统中 Chrony 时间同步工具的命令行实用程序。Chrony 结合了 ntpd 和 ntpdate 的优点,提供高精度和灵活性。要安装 Chrony,可使用包管理器(如 `apt` 或 `yum/dnf`)。常用 `chronyc` 命令包括:查看时间源状态(`sources`)、跟踪信息(`tracking`)、添加或删除服务器、手动同步时间(`makestep`)以及查看其他信息和帮助。`chronyc` 提供了便捷的方式来监控和调整系统时间同步。
|
12天前
|
安全 Linux 数据安全/隐私保护
使用 `chage` 命令管理 Linux 用户密码过期策略
`chage` 命令用于管理Linux用户密码过期策略,包括设置密码最长有效期、警告天数、过期宽限期和账户非活动天数。例如,`chage -M 90 username` 设置密码最长有效期为90天,`chage -W 7 username` 设定到期前7天警告。确保具备足够权限(如root)并理解更改影响。此工具有助于增强系统安全和符合安全策略。
|
Linux
linux下建立循环连接
1.cd /u01/winscp2.ln -s  /u01/winscp aaa3.这样就可以形成一个环。[ /u01/winscp/aaa 62]# ls -ltotal 6324-rw-r--r-- 1 root root       6 Jun  2 10...
805 0
|
1天前
|
存储 监控 安全
深入探索Linux的journalctl命令:系统日志的利器
**journalctl 深入解析:Linux 系统日志的强大工具** journalctl 是 Linux 中用于查询和管理 systemd 日志的命令行工具,与 systemd-journald 配合收集广泛的信息,包括内核消息和服务日志。它提供实时追踪、过滤、导出等功能,如 `-f` 实时监控,`-u` 过滤特定服务日志,`-k` 显示内核消息,`--since` 和 `--until` 选择时间范围。在实际应用中,结合权限管理、日志空间控制和有效过滤,journalctl 成为系统管理员诊断和优化系统的得力助手。