MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

select、poll、epoll在文件描述符管理上的异同

2022-04-191.7k 阅读

select、poll、epoll在文件描述符管理上的异同

一、文件描述符基础概念

在深入探讨 selectpollepoll 对文件描述符的管理之前,我们先来明确文件描述符的基本概念。

文件描述符(File Descriptor,FD)是一个非负整数,它是 Linux 内核为了高效管理已被打开的文件(包括普通文件、套接字、管道等)所创建的索引值。当程序打开一个现有文件或者创建一个新文件时,内核会返回一个文件描述符。在 Unix 类系统中,标准输入、标准输出和标准错误输出对应的文件描述符分别是 0、1 和 2。

文件描述符是进程与内核交互的桥梁,进程通过文件描述符来对相应的文件进行各种操作,如读取、写入、控制等。例如,在使用 read 函数从文件中读取数据时,就需要传入文件描述符来指定要读取的文件。

二、select对文件描述符的管理

  1. select的工作原理 select 是最早出现的多路复用技术,它允许进程监视多个文件描述符的状态变化,比如是否可读、可写或者有异常发生。其函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要检查的最大文件描述符值加 1。
  • readfdswritefdsexceptfds:分别是指向读、写、异常事件对应的文件描述符集合的指针。
  • timeout:设置等待的超时时间,如果为 NULL,则一直阻塞,直到有事件发生。

select 的工作方式是通过轮询的方式遍历所有传入的文件描述符集合,检查每个文件描述符是否有相应的事件发生。如果在指定的时间内有事件发生,则返回发生事件的文件描述符的总数,否则返回 0(超时)或者 -1(错误)。

  1. 文件描述符集合的操作select 中,使用 fd_set 来表示文件描述符集合。fd_set 本质上是一个位图,其每一位对应一个文件描述符。例如,fd_set 的第 3 位为 1,则表示文件描述符 3 在该集合中。

常用的操作宏有:

  • FD_ZERO(fd_set *set):清空文件描述符集合。
  • FD_SET(int fd, fd_set *set):将指定的文件描述符添加到集合中。
  • FD_CLR(int fd, fd_set *set):将指定的文件描述符从集合中移除。
  • FD_ISSET(int fd, fd_set *set):检查指定的文件描述符是否在集合中。

下面是一个简单的 select 示例代码,用于监听标准输入是否有数据可读:

#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

int main() {
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(0, &read_fds);

    struct timeval timeout;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;

    int ret = select(1, &read_fds, NULL, NULL, &timeout);
    if (ret == -1) {
        perror("select error");
        return 1;
    } else if (ret == 0) {
        printf("select timeout\n");
    } else {
        if (FD_ISSET(0, &read_fds)) {
            char buf[1024];
            ssize_t n = read(0, buf, sizeof(buf));
            if (n > 0) {
                buf[n] = '\0';
                printf("Read data: %s", buf);
            }
        }
    }

    return 0;
}
  1. select在文件描述符管理上的特点
  • 可管理文件描述符数量有限:受限于 FD_SETSIZE,通常为 1024,这意味着 select 最多只能同时监听 1024 个文件描述符。
  • 轮询效率低:每次调用 select 都需要遍历所有传入的文件描述符集合,随着文件描述符数量的增加,性能会显著下降。
  • 数据从内核态到用户态的拷贝select 返回后,需要通过 FD_ISSET 宏来逐个检查文件描述符是否有事件发生,这涉及到内核态到用户态的数据拷贝。

三、poll对文件描述符的管理

  1. poll的工作原理 poll 是对 select 的改进,其函数原型为:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:一个 struct pollfd 类型的数组,每个元素表示一个需要监视的文件描述符及其感兴趣的事件。
struct pollfd {
    int fd;         /* 文件描述符 */
    short events;   /* 感兴趣的事件 */
    short revents;  /* 实际发生的事件 */
};
  • nfds:数组中元素的个数。
  • timeout:等待的超时时间,单位为毫秒,-1 表示一直阻塞。

poll 的工作方式同样是轮询,它遍历 pollfd 数组,检查每个文件描述符是否有感兴趣的事件发生。与 select 不同的是,poll 没有最大文件描述符数量的限制(理论上只受限于系统资源)。

  1. 文件描述符事件的表示poll 中,通过 events 字段来表示对文件描述符感兴趣的事件,例如 POLLIN 表示可读事件,POLLOUT 表示可写事件等。revents 字段则由内核填充,用于返回实际发生的事件。

下面是一个简单的 poll 示例代码,同样监听标准输入是否有数据可读:

#include <stdio.h>
#include <poll.h>
#include <unistd.h>

int main() {
    struct pollfd fds[1];
    fds[0].fd = 0;
    fds[0].events = POLLIN;

    int ret = poll(fds, 1, 5000);
    if (ret == -1) {
        perror("poll error");
        return 1;
    } else if (ret == 0) {
        printf("poll timeout\n");
    } else {
        if (fds[0].revents & POLLIN) {
            char buf[1024];
            ssize_t n = read(0, buf, sizeof(buf));
            if (n > 0) {
                buf[n] = '\0';
                printf("Read data: %s", buf);
            }
        }
    }

    return 0;
}
  1. poll在文件描述符管理上的特点
  • 无文件描述符数量限制:相比 selectpoll 理论上可以管理更多的文件描述符,仅受系统资源的限制。
  • 仍然是轮询方式:虽然 poll 解决了文件描述符数量的限制问题,但它仍然采用轮询的方式遍历文件描述符,在文件描述符数量较多时性能不佳。
  • 事件通知方式改进poll 通过 revents 字段直接返回实际发生的事件,相比 select 通过 FD_ISSET 宏逐个检查,减少了内核态到用户态的数据拷贝次数。

四、epoll对文件描述符的管理

  1. epoll的工作原理 epoll 是 Linux 特有的多路复用技术,它在性能上比 selectpoll 有了质的飞跃。epoll 有三个主要的函数:
  • int epoll_create(int size):创建一个 epoll 实例,size 参数在 Linux 2.6.8 之后被忽略,但仍然需要大于 0。返回一个 epoll 文件描述符。
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):对 epoll 实例进行控制操作。
struct epoll_event {
    uint32_t events;      /* 感兴趣的事件 */
    epoll_data_t data;    /* 关联的数据 */
};
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
  • op:操作类型,如 EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符的事件)、EPOLL_CTL_DEL(删除文件描述符)。
  • fd:要操作的文件描述符。
  • event:指定文件描述符感兴趣的事件和关联的数据。
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):等待 epoll 实例上的事件发生。
  • epfdepoll 文件描述符。
  • events:用于存储发生事件的数组。
  • maxeventsevents 数组的大小。
  • timeout:等待的超时时间,单位为毫秒,-1 表示一直阻塞。

epoll 采用事件驱动的方式,内核会在文件描述符状态发生变化时,将该文件描述符放入一个就绪队列中。epoll_wait 函数只需要检查这个就绪队列,而不需要像 selectpoll 那样轮询所有文件描述符。

  1. 文件描述符事件的管理 epoll 通过 epoll_event 结构体来管理文件描述符的事件。events 字段可以设置多种事件,如 EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLERR(错误)等。data 字段可以关联用户自定义的数据,比如可以将文件描述符本身作为数据关联,方便在事件发生时快速获取对应的文件描述符。

下面是一个简单的 epoll 示例代码,监听标准输入是否有数据可读:

#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>

#define MAX_EVENTS 10

int main() {
    int epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create error");
        return 1;
    }

    struct epoll_event event;
    event.data.fd = 0;
    event.events = EPOLLIN;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event) == -1) {
        perror("epoll_ctl error");
        close(epfd);
        return 1;
    }

    struct epoll_event events[MAX_EVENTS];
    int nfds = epoll_wait(epfd, events, MAX_EVENTS, 5000);
    if (nfds == -1) {
        perror("epoll_wait error");
        close(epfd);
        return 1;
    } else if (nfds == 0) {
        printf("epoll_wait timeout\n");
    } else {
        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLIN) {
                char buf[1024];
                ssize_t n = read(0, buf, sizeof(buf));
                if (n > 0) {
                    buf[n] = '\0';
                    printf("Read data: %s", buf);
                }
            }
        }
    }

    close(epfd);
    return 0;
}
  1. epoll在文件描述符管理上的特点
  • 高效的事件通知机制epoll 采用事件驱动的方式,只关注状态发生变化的文件描述符,避免了轮询所有文件描述符带来的性能开销,在高并发场景下性能优势明显。
  • 支持大量文件描述符epoll 没有像 select 那样的文件描述符数量限制,理论上可以管理系统所允许打开的最大文件描述符数量。
  • 内核态与用户态数据拷贝优化epoll_wait 返回时,直接将发生事件的文件描述符及其事件信息拷贝到用户空间,减少了不必要的数据拷贝。

五、select、poll、epoll在文件描述符管理上的异同总结

  1. 相同点
  • 功能上selectpollepoll 都是多路复用技术,都可以实现对多个文件描述符的监视,以判断是否有可读、可写或异常事件发生。
  • 目的:它们的目的都是为了提高应用程序在处理多个文件描述符时的效率,避免阻塞在单个文件描述符的 I/O 操作上,从而实现并发处理多个 I/O 事件。
  1. 不同点
  • 文件描述符数量限制
    • select 受限于 FD_SETSIZE,通常只能管理 1024 个文件描述符。
    • poll 理论上没有文件描述符数量的限制,仅受系统资源约束。
    • epoll 同样没有类似 select 的硬编码限制,能管理大量文件描述符。
  • 工作方式
    • selectpoll 采用轮询方式,遍历所有文件描述符来检查事件,在文件描述符数量较多时性能较差。
    • epoll 采用事件驱动方式,内核将状态变化的文件描述符放入就绪队列,epoll_wait 只需检查该队列,性能更优。
  • 数据结构和事件表示
    • select 使用 fd_set 位图来表示文件描述符集合,通过特定宏操作集合元素,事件通过检查集合来确定。
    • poll 使用 struct pollfd 数组,events 字段表示感兴趣事件,revents 表示实际发生事件。
    • epoll 使用 struct epoll_event 结构体,通过 epoll_ctl 函数注册和管理事件,events 字段指定感兴趣事件,data 字段可关联自定义数据。
  • 内核态与用户态数据拷贝
    • select 返回后需通过 FD_ISSET 宏逐个检查文件描述符,涉及多次内核态到用户态的数据拷贝。
    • poll 通过 revents 字段直接返回实际发生事件,减少了数据拷贝次数。
    • epollepoll_wait 返回时,直接将发生事件的文件描述符及事件信息拷贝到用户空间,优化了数据拷贝。

在实际应用中,对于并发连接数较少且较为固定的场景,selectpoll 可以满足需求;而在高并发场景下,epoll 凭借其高效的事件通知机制和对大量文件描述符的良好支持,成为首选的多路复用技术。了解它们在文件描述符管理上的异同,有助于开发者根据具体应用场景选择最合适的技术方案,提升系统的性能和稳定性。