linux通用基础

多路io

1. select

  • 最大监听 1024 个 fd(内核宏限制)
  • 每次调用都要把整个 fd 集合从用户态拷贝到内核态
  • 内核通过遍历所有 fd 找就绪的
  • 水平触发(LT)
  • 可移植性最好(几乎所有系统支持)

返回值:

>0:就绪的 fd 总数

0:超时

-1:出错(errno)

使用步骤:

定义 fd_set

FD_ZERO 清空

FD_SET 把需要监听的 fd 加进去

调用 select

用 FD_ISSET 遍历判断哪个就绪

需知道的套配宏

cpp 复制代码
FD_ZERO(&set);      // 清空集合
FD_SET(fd, &set);   // 把 fd 加入监听
FD_CLR(fd, &set);   // 从集合删除 fd
FD_ISSET(fd, &set); // 判断 fd 是否就绪
cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

FD_ZERO(fd_set *);
FD_SET(int fd, fd_set *);
FD_ISSET(int fd, fd_set *);
cpp 复制代码
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

int main() {
    fd_set rfds;
    struct timeval tv;
    int ret;

    while (1) {
        // 1. 清空集合
        FD_ZERO(&rfds);
        // 2. 添加要监听的 fd
        FD_SET(0, &rfds);

        tv.tv_sec = 1;
        tv.tv_usec = 0;

        // 3. 监听:最大fd+1,读集合,超时
        ret = select(1, &rfds, NULL, NULL, &tv);

        if (ret == -1) {
            perror("select");
            break;
        } else if (ret == 0) {
            printf("timeout...\n");
            continue;
        }

        // 4. 判断是否就绪
        if (FD_ISSET(0, &rfds)) {
            char buf[128];
            read(0, buf, 127);
            printf("select 读到: %s", buf);
        }
    }
    return 0;
}

2. poll

  • 没有 1024 数量限制
  • 原理和 select 几乎一样:仍遍历、仍全量拷贝
  • 只是数据结构换成链表,去掉上限
  • 水平触发(LT)

返回值:

>0:就绪 fd 数量

0:超时

-1:错误

使用步骤:

定义 struct pollfd 数组

填入 fd 和 events

调用 poll

遍历数组,判断 revents & POLLIN 等

cpp 复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int fd;
    short events;  // POLLIN/POLLOUT
    short revents; // 内核返回
};
cpp 复制代码
#include <stdio.h>
#include <poll.h>
#include <unistd.h>

int main() {
    struct pollfd fds[1];
    fds[0].fd = 0;
    fds[0].events = POLLIN; // 监听可读

    while (1) {
        // 监听 1 个 fd,超时 1000ms
        int ret = poll(fds, 1, 1000);

        if (ret == -1) {
            perror("poll");
            break;
        } else if (ret == 0) {
            printf("timeout...\n");
            continue;
        }

        // 判断返回事件
        if (fds[0].revents & POLLIN) {
            char buf[128];
            read(0, buf, 127);
            printf("poll 读到: %s", buf);
        }
    }
    return 0;
}

3. epoll(高性能王者)

  • Linux 特有
  • 三个 API:epoll_create / epoll_ctl / epoll_wait
  • 内核使用红黑树 管理 fd,只在添加 / 删除时拷贝一次
  • 回调机制 ,只返回就绪的 fd,不用遍历
  • 支持 LT(水平) + ET(边缘) 两种触发方式
  • 高并发下性能碾压 select/poll

返回值:

>0:就绪 fd 个数

0:超时

-1:错误

使用步骤:

epoll_create 创建句柄

构建 epoll_event,调用 epoll_ctl(ADD) 加入 fd

循环 epoll_wait 等待事件

直接遍历返回的就绪数组,不用全扫

cpp 复制代码
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);
cpp 复制代码
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>

int main() {
    int epfd = epoll_create(1);

    struct epoll_event ev, events[1];
    ev.events = EPOLLIN;
    ev.data.fd = 0;

    // 添加监听
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ev);

    while (1) {
        // 等待事件,最多返回 1 个,超时 1000ms
        int n = epoll_wait(epfd, events, 1, 1000);

        if (n == -1) {
            perror("epoll_wait");
            break;
        } else if (n == 0) {
            printf("timeout...\n");
            continue;
        }

        if (events[0].data.fd == 0) {
            char buf[128];
            read(0, buf, 127);
            printf("epoll 读到: %s", buf);
        }
    }
    return 0;
}

4、性能对比

特性 select poll epoll
监听上限 1024
遍历方式 遍历全部 fd 遍历全部 fd 只返回就绪 fd
数据拷贝 每次都全量拷贝 每次都全量拷贝 仅增删时拷贝
效率 O(n) O(n) O(1)
平台 全平台 全平台 Linux 专属
触发方式 LT LT LT + ET