MST

星途 面试题库

面试题:C++ 异步多路复用之中等难度:epoll机制在C++中的应用

请简述epoll在C++网络编程中的原理,并且说明在使用epoll时,C++代码中如何进行事件注册与事件处理,假设你正在开发一个简单的TCP服务器程序,给出关键代码片段并注释说明。
14.1万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

epoll原理

  1. 简介:epoll是Linux下高性能I/O事件通知机制,是多路复用I/O(I/O multiplexing)的一种实现方式。它能高效处理大量并发连接。
  2. 工作方式
    • 内核数据结构:epoll维护一个红黑树结构来管理用户感兴趣的文件描述符(fd),以及一个就绪链表用于存放发生事件的fd。
    • epoll_ctl操作:通过epoll_ctl系统调用,用户可以向epoll内核对象中添加、修改或删除感兴趣的fd及其对应的事件类型(如读、写、异常等)。
    • epoll_wait等待epoll_wait函数会阻塞等待,直到注册的fd中有事件发生。当有事件发生时,内核将这些fd从红黑树中取出,放入就绪链表,并将就绪链表传递给用户空间,epoll_wait返回。

事件注册与处理

  1. 事件注册:在C++中使用epoll_ctl函数来注册事件。首先创建一个epoll实例(epoll_createepoll_create1),然后通过epoll_ctl向该实例中添加需要监控的文件描述符及对应的事件。
  2. 事件处理:在epoll_wait返回后,处理返回的就绪事件。根据事件类型(如读事件、写事件等)对相应的文件描述符进行操作。

TCP服务器关键代码片段

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>

#define MAX_EVENTS 10
#define PORT 8888

int main() {
    // 创建socket
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        return 1;
    }

    // 设置socket为非阻塞
    int flags = fcntl(listen_fd, F_GETFL, 0);
    fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK);

    // 绑定端口
    sockaddr_in server_addr{};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(listen_fd);
        return 1;
    }

    // 监听
    if (listen(listen_fd, 5) == -1) {
        perror("listen");
        close(listen_fd);
        return 1;
    }

    // 创建epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        close(listen_fd);
        return 1;
    }

    // 注册监听socket到epoll
    epoll_event event{};
    event.data.fd = listen_fd;
    event.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
        perror("epoll_ctl: listen_fd");
        close(listen_fd);
        close(epoll_fd);
        return 1;
    }

    epoll_event events[MAX_EVENTS];
    while (true) {
        // 等待事件发生
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < num_events; ++i) {
            if (events[i].data.fd == listen_fd) {
                // 处理新连接
                sockaddr_in client_addr{};
                socklen_t client_addr_len = sizeof(client_addr);
                int client_fd = accept(listen_fd, (sockaddr*)&client_addr, &client_addr_len);
                if (client_fd == -1) {
                    perror("accept");
                    continue;
                }

                // 设置客户端socket为非阻塞
                flags = fcntl(client_fd, F_GETFL, 0);
                fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);

                // 注册客户端socket到epoll
                event.data.fd = client_fd;
                event.events = EPOLLIN | EPOLLET; // 边缘触发模式
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                    perror("epoll_ctl: client_fd");
                    close(client_fd);
                }
            } else {
                // 处理客户端数据
                int client_fd = events[i].data.fd;
                char buffer[1024];
                ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
                if (bytes_read <= 0) {
                    if (bytes_read == 0) {
                        // 对端关闭连接
                        std::cout << "Client disconnected" << std::endl;
                    } else {
                        perror("recv");
                    }
                    // 从epoll中删除并关闭客户端socket
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr);
                    close(client_fd);
                } else {
                    buffer[bytes_read] = '\0';
                    std::cout << "Received: " << buffer << std::endl;
                    // 简单回显数据
                    send(client_fd, buffer, bytes_read, 0);
                }
            }
        }
    }

    close(listen_fd);
    close(epoll_fd);
    return 0;
}
  • 代码说明
    • 初始化部分:创建监听socket,设置为非阻塞,绑定端口并开始监听。
    • epoll部分:创建epoll实例,并将监听socket注册到epoll中,监听读事件。
    • 事件循环部分epoll_wait等待事件发生,当有事件时,区分是新连接事件(监听socket可读)还是客户端数据事件(客户端socket可读)。对于新连接,设置为非阻塞并注册到epoll;对于客户端数据,读取并回显数据,若连接关闭则从epoll中删除并关闭socket。