IO多路转接

机制 监听方式 是否遍历所有 fd 最大监听数
select fd_set 位图 每次遍历所有 fd,效率低 1024(受 FD_SETSIZE 限制)
epoll 事件驱动 只遍历发生事件的 fd,效率高 无固定限制

while((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)

​ if(write(STDOUT_FILENO, buf, n) != n)

​ err_sys(“wrtite error”);

需求:从两个描述符读, 可能会阻塞在一个读IO上,导致另外的描述符也不能读。也不知道到底哪个输入会得到数据

IO多路转接:构造一个感兴趣的描述符表,调用一个函数,直到其中一个描述符已经准备好,该函数才返回。 poll , select, pselect , 返回后 进程会得知哪些描述符已经准备好。 然后就可以正确调用IO read , write

Select

函数原型

1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数 说明
nfds 表示三个描述符集最大值加1.
readfds, writefds 表示感兴趣的 读写。 每个描述符为一位。
exceptfds 监听异常事件(如 OOB 数据)
timeout 超时控制,决定 select 阻塞时间,NULL 表示无限等待
返回值 -1表示出错,0表示描述符都没有准备好(读集合中读不会阻塞,写集合写不会阻塞),大于0为准备好的描述符个数

Fd_set操作

文件描述符集合,存储需要监听的文件描述符。

作用
FD_ZERO(&set) 清空 fd_set 集合
FD_SET(fd, &set) 将 fd 添加到 fd_set
FD_CLR(fd, &set) 从 fd_set 中移除 fd
FD_ISSET(fd, &set) 检查 fd 是否在 fd_set 中(即是否准备好)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>

int main() {
fd_set read_fds; // 可读事件监听集合
struct timeval timeout;
int fd = STDIN_FILENO; // 监听标准输入(键盘输入)

FD_ZERO(&read_fds); // 清空集合
FD_SET(fd, &read_fds); // 将标准输入加入监听集合

timeout.tv_sec = 5; // 设置超时 5 秒
timeout.tv_usec = 0;

printf("等待输入,5 秒后超时...\n");

int ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);

if (ret == -1) {
perror("select 出错");
return 1;
} else if (ret == 0) {
printf("超时,无输入\n");
} else {
if (FD_ISSET(fd, &read_fds)) {
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("输入内容:%s\n", buffer);
}
}

return 0;
}

要点

  1. select 会修改 fd_set,需要每次重新设置。(因为会清除未发生变化的描述符)
  2. timeout每次也要重新设置
  3. 最多能监听102个描述符

epoll

高效的特点:

  1. 事件驱动:注册的fd,不会每次遍历所有fd。而是只关心发生变化的fd,epoll_wait也只返回有事件的fd,而不是select需要遍历
  2. 支持边缘触发ETL模式:减少重复通知

用法:

  1. 创建epoll实例:epoll_create1

    1
    2
    3
    4
    5
    6
    7
    8
    #include <sys/epoll.h>

    int epfd = epoll_create1(0); // 推荐使用 epoll_create1
    if (epfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
    }

  2. 添加、修改、删除 fd:epoll_ctl()

    1
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • epfd: epoll实例
    • op:操作
      • EPOLL_CTL_ADD → 添加 fd
      • EPOLL_CTL_MOD → 修改 fd监听事件
      • EPOLL_CTL_DEL → 删除fd
    • fd:要监听的文件描述符
    • event:要监听的epoll事件

    epoll_event:表示一个fd上的事件

    存储监听的事件类型和与之关联的fd

    1
    2
    3
    4
    5
    6
    struct epoll_event {
    uint32_t events; // 事件类型(EPOLLIN、EPOLLOUT、EPOLLERR、EPOLLET)
    epoll_data_t data; // 存储用户数据(通常是文件描述符)
    };


    事件类型:读、写、错误、ET模式

    ET模式:只在fd发生状态改变时候通知触发。默认是LT模式

    • LT:只要fd可读/可写,每次wait都会返回该fd
    • ET:只在状态改变时候通知。
    1
    2
    3
    4
    5
    6
    7

    typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
    } epoll_data_t;
    • ptr常指向一个数据结构体,当fd不够满足需求适合使用
    • fd存储描述符

    示例:给fd注册监听可读事件。

    1
    2
    3
    4
    5
    6
    7
    8
    struct epoll_event event;
    event.events = EPOLLIN; // 监听可读事件
    event.data.fd = socket_fd;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event) == -1) {
    perror("epoll_ctl: EPOLL_CTL_ADD");
    exit(EXIT_FAILURE);
    }
  3. 等待事件

    1
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    参数 作用
    epfd epoll 实例的文件描述符。
    events 用于存储触发的事件。
    maxevents 最多返回的事件数量。
    timeout: -1 → 阻塞等待 0 → 立即返回 >0 → 等待超时时间(毫秒)
    返回值 > 0:触发的事件数量,events 数组里存有 n 个就绪的 fd0:超时,无事件发生; -1:错误,errno 设定错误类型。

    示例:wait返回有事件的fd的event

    比如监听了 fd 1,3,4,5 。 其中1,4有事件,那么nfds返回2. events[0].data.fd=1, events[1].data.fd=2。即返回有事件fd的event,填充进events数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct epoll_event events[10];  // 存放触发的事件

    int nfds = epoll_wait(epfd, events, 10, -1);
    if (nfds == -1) {
    perror("epoll_wait");
    exit(EXIT_FAILURE);
    }

    for (int i = 0; i < nfds; i++) {
    if (events[i].events & EPOLLIN) {
    printf("fd %d 可读\n", events[i].data.fd);
    }
    }