MST

星途 面试题库

面试题:C语言epoll机制在非阻塞I/O中的应用

请详细阐述在C语言中,epoll相较于select和poll在实现非阻塞I/O方面的优势。编写一个C语言程序,使用epoll_create、epoll_ctl和epoll_wait函数实现对多个套接字的非阻塞I/O监控,并处理不同类型的I/O事件(如可读、可写),同时说明epoll的两种工作模式(LT和ET)的区别以及应用场景。
22.9万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

epoll相较于select和poll在实现非阻塞I/O方面的优势

  1. 可扩展性
    • select:支持的文件描述符数量受限于FD_SETSIZE,通常为1024,在处理大量文件描述符时可扩展性差。
    • poll:理论上没有文件描述符数量限制,但由于其采用线性遍历文件描述符集合的方式,随着文件描述符数量增多,性能会急剧下降。
    • epoll:基于事件驱动,通过红黑树管理文件描述符,在处理大量文件描述符时性能稳定,可扩展性强。
  2. 事件通知机制
    • select:需要遍历整个文件描述符集合来检查哪些描述符有事件发生,效率较低。
    • poll:同样需要遍历整个文件描述符链表来获取事件,性能也不理想。
    • epoll:使用回调机制,当有事件发生时,内核将事件添加到就绪列表中,epoll_wait函数直接从就绪列表中获取事件,大大提高了效率。
  3. 内存拷贝
    • selectpoll:每次调用都需要将用户态的文件描述符集合拷贝到内核态,返回时又要将内核态的结果拷贝回用户态,开销较大。
    • epoll:通过epoll_ctl将文件描述符注册到内核后,后续操作只需要在内核态进行事件管理,不需要每次都进行内存拷贝,提高了效率。

C语言程序使用epoll实现对多个套接字的非阻塞I/O监控

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10
#define BUF_SIZE 1024

void setnonblocking(int sockfd) {
    int flags;
    if ((flags = fcntl(sockfd, F_GETFL, 0)) == -1) {
        perror("fcntl F_GETFL");
        exit(EXIT_FAILURE);
    }
    if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL");
        exit(EXIT_FAILURE);
    }
}

int main() {
    int listen_sock, conn_sock;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen = sizeof(cliaddr);

    listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);

    if (bind(listen_sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("bind");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }

    if (listen(listen_sock, 5) == -1) {
        perror("listen");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }

    int epollfd = epoll_create1(0);
    if (epollfd == -1) {
        perror("epoll_create1");
        close(listen_sock);
        exit(EXIT_FAILURE);
    }

    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listen_sock;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
        perror("epoll_ctl add listen_sock");
        close(listen_sock);
        close(epollfd);
        exit(EXIT_FAILURE);
    }

    setnonblocking(listen_sock);

    while (1) {
        int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == listen_sock) {
                conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &clilen);
                if (conn_sock == -1) {
                    perror("accept");
                    continue;
                }
                setnonblocking(conn_sock);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = conn_sock;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
                    perror("epoll_ctl add conn_sock");
                    close(conn_sock);
                }
            } else {
                int sockfd = events[i].data.fd;
                char buf[BUF_SIZE];
                ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
                if (n == -1) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        continue;
                    } else {
                        perror("recv");
                        epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
                        close(sockfd);
                    }
                } else if (n == 0) {
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
                    close(sockfd);
                } else {
                    buf[n] = '\0';
                    printf("Received: %s\n", buf);
                    // 简单回显
                    if (send(sockfd, buf, n, 0) == -1) {
                        perror("send");
                        epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd, NULL);
                        close(sockfd);
                    }
                }
            }
        }
    }

    close(listen_sock);
    close(epollfd);
    return 0;
}

epoll的两种工作模式(LT和ET)的区别以及应用场景

  1. 水平触发(LT - Level Triggered)
    • 区别:只要文件描述符对应的缓冲区还有未处理的数据,或者还有空间可以写入数据,epoll_wait就会不断通知该文件描述符有事件发生。也就是说,对于读事件,只要缓冲区有数据,就会一直触发可读事件;对于写事件,只要缓冲区有空间,就会一直触发可写事件。
    • 应用场景:适用于数据处理速度较慢的场景,因为即使一次没有处理完所有数据,下次epoll_wait依然会通知,保证数据不会丢失。在传统的网络服务器编程中,LT模式使用较为普遍,因为它更符合人们对事件处理的常规理解。
  2. 边缘触发(ET - Edge Triggered)
    • 区别:只有当文件描述符对应的缓冲区状态发生变化时(例如有新数据到达、缓冲区从不可写变为可写等),epoll_wait才会通知该文件描述符有事件发生。对于读事件,只有当新数据到达缓冲区时才会触发可读事件,并且需要一次性将缓冲区数据读完,否则下次不会再触发;对于写事件,只有当缓冲区从不可写变为可写时才会触发可写事件。
    • 应用场景:适用于数据处理速度较快的场景,因为它可以减少不必要的事件通知,提高效率。在一些对性能要求极高的网络应用中,如高性能的Web服务器、网络爬虫等,ET模式能够充分发挥其优势,减少系统开销。