Linux IO 多路复用 epoll 机制

2023年 10月 13日 100.9k 0

本文摘自写给应用开发的 Android Framework 教程,完整教程请查阅 yuandaimaahao.github.io/AndroidFram… 更为详细的视频教程与答疑服务,请联系微信 zzh0838

什么是 IO 多路复用

在 Linux 中:

  • IO 就是对文件的读写操作
  • 多路是指同时读写多个文件
  • 复用是指使用一个程序处理多个文件的同时读写

问题来了,为什么需要多路复用,为了快,要给每一个 fd 通道最快的感受,要让每一个 fd 觉得,你只在给他一个人跑腿。

为了更快的处理多路 IO,大体有两种方案:

  • 一种方案是:一个 IO 请求(比如 write )对应一个线程来处理,但是线程数多了,性能反倒会差。
  • 另外一种方案是: IO 多路复用

接下来,我们就来看看 IO 多路复用:

我不用任何其他系统调用,能否实现 IO 多路复用?

可以的,写个 for 循环,每次都尝试 IO 一下,读/写到了就处理,读/写不到就 sleep 下。

while(true) 
{
    foreach fd数组 
    {
        read/write(fd, /* 参数 */)
    }

    sleep(1s)
}

默认情况下,我们没有加任何参数 create 出的 fd 是阻塞类型的。我们读数据的时候,如果数据还没准备好,是会需要等待的,当我们写数据的时候,如果还没准备好,默认也会卡住等待。所以,在上面伪代码中的 read/write 是可能被直接卡死,而导致整个线程都得到不到运行。只需要把 fd 都设置成非阻塞模式。这样 read/write 的时候,如果数据没准备好,返回 EAGIN 的错误即可,不会卡住线程,从而整个系统就运转起来了。

这个实现只是为了帮助我们理解 IO 多路复用,实际上,上面的实现在性能上有很大的缺陷。for 循环每次要定期 sleep 1s,这个会导致吞吐能力极差,因为很可能在刚好要 sleep 的时候,所有的 fd 都准备好 IO 数据,而这个时候却要硬生生的等待 1s。

IO 多路复用就是 1 个线程处理 多个 fd 的模式。我们的要求是:这个 “1” 就要尽可能的快,避免一切无效工作,要把所有的时间都用在处理句柄的 IO 上,不能有任何空转,sleep 的时间浪费。

为了实现上诉的功能,内核提供了 3 种系统调用 select,poll,epoll。

这 3 种系统调用都能够管理 fd 的可读可写事件,在所有 fd 不可读不可写无所事事的时候,可以阻塞线程,切走 cpu 。fd 可读写的时候,对应线程会被唤醒。

三者的差异主要是在性能上,epoll 的性能是强于 select 和 poll 的,我们接下来就来看看 epoll 的具体使用。

epoll 的使用

使用 epoll 需要以下三个系统调用:

//头文件
#include 

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
  • epollctl 负责管理这个池子里的 fd 增、删、改;
  • epollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;

接下来我们看个示例程序:

使用 epoll_create 创建一个管理 fd 的池子

epollfd = epoll_create(1024);
if (epollfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
}

这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。

然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

我们就把 fd 放到这个池子里了,EPOLL_CTL_ADD 表明操作是增加 fd,最后一个参数是 epoll_event 结构体:

struct epoll_event {
  __uint32_t events;  /* Epoll 事件 */
  epoll_data_t data;  /* 用户数据 */
};

epoll_event 的第一个成员 events 用于指定我们监听的 fd 事件类型,常见的值有:

  • EPOLLIN:可读事件
  • EPOLLOUT:可写事件

多个值可以通过或操作同时生效:

epoll_event event;
// 同时监听可读可写事件
event.events = EPOLLIN | EPOLLOUT;

最后,我们需要调用 epoll_wait 进入休眠状态,可读或可写事件到来时,醒休眠中的程序从 epoll_wait 处被唤醒。

其使用方法,通常如下:

while (true)
{   
    // epollfd 是 epoll_create 的返回值
    // events 是一个 epoll_event 的数组,用于存储收到的多个事件
    // EPOLL_SIZE 用于设定最多监听多少个事件
    // 最后一个参数 -1 用于指定阻塞时间上限,-1:表示调用将一直阻塞
    int count = epoll_wait(epollfd, events, EPOLL_SIZE, -1);
    if (count < 0)
    {
        perror("epoll failed");
        break;
    }
    for (int i=0;i < count;i++)
    {
        //处理可读或可写事件
    }
}

参考资料

  • 深入理解 Linux 的 epoll 机制
  • Linux下的I/O复用技术 之 epoll为什么更高效 ?
  • epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析
  • epoll LT 模式和 ET 模式详解

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论