面试篇——epoll/select多路复用

一、I/O多路复用

在网络编程中,多路复用是指同时监控多个文件描述符(如套接字)是否有事件(如数据可读、可写或异常)发生。常见的多路复用机制有 selectpollepoll。这些机制用于开发高并发的网络服务器,能够高效处理大量客户端连接。

二、select

select 是最早引入的 I/O 多路复用机制之一,支持监控多个文件描述符(套接字、管道等)。使用 select 监控文件描述符集,并在其中一个或多个文件描述符发生 I/O 事件时返回。

1.优点:

  • 简单且广泛

2.缺点:

  • 文件描述符集大小受限(默认最大 1024 个)。
  • 每次调用时需要重新初始化描述符集(开销较大)。
  • select 需要遍历全部文件描述符集,效率较低。

3.函数原型:

#include <sys/select.h>
#include <sys/time.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

4.使用步骤:

  • 始化文件描述符集 fd_set
  • 调用 select() 监听可读、可写或异常的文件描述符。
  • 遍历文件描述符集,处理准备就绪的文件描述符。

5.select 示例代码:

#include <iostream>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8080);

    bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(listen_fd, 5);

    fd_set readfds;
    int max_fd = listen_fd;
    while (true) {
        FD_ZERO(&readfds);          // 清空描述符集
        FD_SET(listen_fd, &readfds);  // 将监听 socket 添加到集合中

        int activity = select(max_fd + 1, &readfds, nullptr, nullptr, nullptr);
        if (activity < 0) {
            std::cerr << "select error\n";
            break;
        }

        if (FD_ISSET(listen_fd, &readfds)) {
            int new_socket = accept(listen_fd, nullptr, nullptr);
            std::cout << "New connection accepted\n";
            close(new_socket);
        }
    }
    close(listen_fd);
    return 0;
}

三、poll

pollselect 的改进版,克服了 select 的文件描述符集大小限制,可以监控任意数量的文件描述符。此外,poll 通过数组结构传递文件描述符及其对应的事件,避免了 select 需要每次重置文件描述符集的问题。

1.优点:

  • 没有文件描述符数量的限制。
  • 可以同时监控多个不同类型的 I/O 事件。

2.缺点:

  • 对所有文件描述符进行线性扫描,效率不高。
  • 每次调用时仍然需要重新初始化文件描述符数组。

3.函数原型:

#include <poll.h>

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

4.使用步骤:

  • 初始化 pollfd 数组,指定文件描述符和感兴趣的事件。
  • 调用 poll() 监控事件的变化。
  • 遍历 pollfd 数组,处理就绪的文件描述符。

5.示例代码:

#include <iostream>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8080);

    bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(listen_fd, 5);

    pollfd fds[1];
    fds[0].fd = listen_fd;
    fds[0].events = POLLIN;

    while (true) {
        int activity = poll(fds, 1, -1); // 无限等待
        if (activity < 0) {
            std::cerr << "poll error\n";
            break;
        }

        if (fds[0].revents & POLLIN) {
            int new_socket = accept(listen_fd, nullptr, nullptr);
            std::cout << "New connection accepted\n";
            close(new_socket);
        }
    }
    close(listen_fd);
    return 0;
}

四、epoll

poll 是 Linux 内核中提供的高效的 I/O 多路复用机制,专门针对大量并发连接优化。与 selectpoll 相比,epoll 只关注活跃的文件描述符,避免了对所有文件描述符的轮询,极大提高了效率,特别是在大量文件描述符的情况下。

1.优点:

  • 高效,适用于大规模并发连接。
  • epoll 可以以水平触发(LT)或边缘触发(ET)的模式工作,ET 模式下效率更高。
  • 无需每次重新初始化监控集,动态添加和删除文件描述符。

2.缺点:

  • 仅适用于 Linux 系统。

3.函数原型:

#include <sys/epoll.h>

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);

4.使用步骤:

  • 创建 epoll 实例。
  • 使用 epoll_ctl() 添加、修改或删除需要监控的文件描述符。
  • 使用 epoll_wait() 监听事件变化。

5.示例代码:

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>

#define MAX_EVENTS 10

int main() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(8080);

    bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    listen(listen_fd, 5);

    // 创建 epoll 实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "Failed to create epoll instance\n";
        return -1;
    }

    // 添加监听套接字到 epoll 监视列表
    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = listen_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);

    epoll_event events[MAX_EVENTS];

    while (true) {
        int num_fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < num_fds; ++i) {
            if (events[i].data.fd == listen_fd) {
                int new_socket = accept(listen_fd, nullptr, nullptr);
                std::cout << "New connection accepted\n";

                // 将新的连接添加到 epoll 监视列表
                epoll_event ev;
                ev.events = EPOLLIN;
                ev.data.fd = new_socket;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev);
            } else if (events[i].events & EPOLLIN) {
                char buffer[1024] = {0};
                int bytes_read = read(events[i].data.fd, buffer, sizeof(buffer));
                if (bytes_read > 0) {
                    std::cout << "Received: " << buffer << std::endl;
                } else {
                    // 关闭并从 epoll 列表中删除
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
                    close(events[i].data.fd);
                }
            }
        }
    }

    close(listen_fd);
    close(epoll_fd);
    return 0;
}

五、对比与选择

  • select:适合较小规模的并发连接,具有广泛的兼容性。
  • poll:改善了 select 的文件描述符限制问题,但效率仍然较低,适合中等规模的并发连接。
  • epoll:在大规模并发连接中效率最高,适用于高性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值