什么是IO复用

一种在单个线程中管理多个输入/输出通道的技术。它允许一个线程同时监听多个输入流(例如网络套接字、文件描述符等),并在有数据可读或可写时进行相应的处理,而不需要为每个通道创建一个独立的线程

使用多线程搭建服务端会造成大量的执行上下文切换开销,所以出现了单线程的IO复用技术

1

1

select

介绍

1

1

1

  • 设置文件描述符

    1

    1

    2

  • 设置检查(监视)范围及超时

    maxfd一般是最大的文件描述符加上1

  • 调用select函数后查看结果

    1

    传递给select函数的文件描述符集会发生变化,所以一般是用一个临时的文件描述符集,先复制然后传递给select函数,避免直接覆写原始的文件描述符集

使用

// 收集客户端信息(准备fds[5], maxfd)
for (int i = 0; i < 5; i++) {
    ...
    fds[i] = accept(sockfd, (struct sockaddr*)&client, &addrlen);
    if (fds[i] > max) max = fds[i]
}

while (1) {
    // 准备临时的文件描述符集,避免覆写原始的fds[5]
    FD_ZERO(&rset);
    for (int i = 0; i < 5; i++) {
        FD_SET(fds[i], &rset);
    }

    // 调用select
    select(max+1, &rset, NULL, NULL, NULL);

    // 查看select调用结果,不断的轮询
    for (int i = 0; i < 5; i++) {
        if (FD_ISSET(fds[i], &rset)) {
            read(fds[i], buffer, MAXBUF);
        }
    }
}

有事件发生时,FD置位,select返回

优缺点

  • 优点

    • 有一个整体的用户态到内核态的切换,由内核来监听事件,避免了频繁切换的开销
  • 缺点

    • 监听的文件描述符大小有限制,1024 bitmap

    • fdset不可重用,每次监听都需要准备一个临时的文件描述符集

    • 每次调用都需要将fdset由用户态复制到内核态

    • 无法知道发生事件的具体文件描述符,需要进行轮询处理 O(n)

poll函数

介绍

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数

    • fds: pollfd 类型的结构体数组,包含需要被监视的文件描述符及其相关事件信息

      struct pollfd {
          int fd;         // 文件描述符
          short events;   // 要监听的事件
          short revents;  // 发生的事件
      };
      
      • fd: 需要监视的文件描述符,可以是任何类型的文件描述符,如套接字、管道等

      • events: 要监听的事件,多个事件可以通过按位“或”组合

      • revents: 返回的实际事件,用于指示文件描述符上发生了哪些事件。poll 调用返回后,revents 中将包含触发的事件

    • nfds: 该值是 fds 数组中元素的数量,即 要监听的文件描述符的数量

    • timeout: 指定 poll 调用 阻塞的时间,单位为毫秒。

      • timeout == 0: poll 会 立即返回,不会阻塞(非阻塞模式)

      • timeout == -1: poll 会 无限期阻塞,直到有文件描述符准备好

      • timeout > 0: 最大阻塞时间(单位为毫秒),在该时间内返回

  • 返回值

    • > 0: 表示文件描述符数组中有 多少个 文件描述符准备好进行 I/O 操作

    • 0: 表示 没有 文件描述符在指定的 timeout 时间内准备好

    • -1: 出现 错误,错误原因可以通过 errno 获取

  • 事件常量

    在 events 和 revents 字段中使用以下常量来指定感兴趣的事件或返回的事件:

    • POLLIN: 数据可读

    • POLLOUT: 数据可写

    • POLLERR: 错误发生

    • POLLHUP: 文件描述符被挂起

    • POLLNVAL: 无效的文件描述符

使用

struct pollfd {
    int fd;
    short events;
    short revents;
};

// 准备pollfds
for (int i = 0; i < 5; i++) {
    pollfds[i].fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
    pollfds[i].events = POLLIN;
}

while (1) {
    // 调用poll
    poll(pollfds, 5, 50000);

    // 轮询处理
    for (int i = 0; i < 5; i++) {
        if (pollfds[i].revents & POLLIN) {
            // 复位revents
            pollfds[i].revents = 0;
            read(pollfds[i].fd, buffer, MAXBUF);
        }
    }
}

有事件发生时,POLLFD的revents字段保存事件,poll返回

优缺点

  • 优点:解决了select的前两点,大小不限制,pollfds可以重用

    • 可以重用的原因是,内核将发生的事件存放在revents字段中,而传入poll的参数并不需要使用该字段,所以只需将其清零即可复用
  • 缺点:select最后两点未解决,需要从用户态复制pollfd数组到内核态,需要进行轮询处理

epoll函数

介绍

1

1

1

1

epoll将发生事件的文件描述符填充到以下结构体中

1

1

使用

struct epoll_event events[5];
int epfd = epoll_create(10);

for (int i = 0; i < 5; i++) {
    static struct epoll_event ev;
    ...
    ev.data.fd = accept(sockfd, (struct sockadddr*)&client, &addrlen);
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
}

while (1) {
    int nfds = epoll_wait(epfd, events, 5, 10000);

    for (int i = 0; i < nfds; i++) {
        read(events[i].data.fd, buffer, MAXBUF);
    }
}

有事件发生时,epoll将有事件触发的fd重排到event数组的前面,epoll_wait返回值为发生事件的文件描述符个数

优缺点

  • 优点: 解决了select的缺点

    • 无大小限制

    • epoll_event数组可重用(根据epoll函数的返回值每次只需使用数组的前n个即可,内核会修改前n个元素的数据以确保他们可使用)

    • 用户态和内核态共享epollfd创建的空间

    • 会将发生事件的文件描述符重排到数组前端,无需轮询,O(1)

  • 缺点:

    • 平台兼容性差

条件触发和边缘触发

条件触发和边缘触发的区别在于发生事件的时间点

条件触发(Level-triggered)

  • 在条件触发模式下,事件会在每次检查时触发,只要文件描述符 仍然处于“就绪”状态

  • 它的行为更像是 “持续触发”,即只要输入缓冲区中有数据,它就会一直通知你

边缘触发(Edge-triggered)

  • 边缘触发模式下,只有在文件描述符状态 从“非就绪”变为“就绪”时,系统才会触发事件。即,如果缓冲区中有数据,事件会触发一次,但如果数据还没有被处理完,系统不会再次通知你,除非状态发生变化(比如数据被清空或添加新的数据)

  • 这种模式要求你在 单次通知中处理完所有的数据(或者至少尽可能多的数据),否则下次状态改变时可能会错过事件通知

补充

边缘触发相较于条件触发,需要开发者更加小心地处理每一次事件通知,以确保在事件触发时能尽量读取和处理完所有的数据。边缘触发适合处理高性能、低延迟的场景,因为它减少了不必要的事件通知和重复检查

总结

  • 条件触发:每次检查时,只要文件描述符就绪,就会继续通知。适合简单的处理场景,适合不关心处理效率的应用

  • 边缘触发只在文件描述符的状态发生变化时通知一次,适合高效、非阻塞的 I/O 操作,但要求应用程序能在一次通知中处理所有的数据,避免遗漏。常用于 将接收数据与处理数据分离的场景,因为它允许应用程序主动去处理数据

 posted on 2025-06-17 18:20  Dylaris  阅读(16)  评论(0)    收藏  举报