自学内容网 自学内容网

【后端面试总结】select、poll、epoll三种IO多路复用方法对比

一、引言

IO多路复用是一种在网络编程中广泛使用的技术,它允许单个线程或进程同时处理多个IO操作,显著提高了系统的并发处理能力。本文将详细介绍三种常见的IO多路复用方法:select、poll和epoll,并探讨它们的实现原理、劣势及源码示例。

二、select的实现原理及劣势

实现原理

select方法通过调用一个系统调用来实现IO多路复用。它要求用户进程传入三个文件描述符集合:读集合、写集合和异常集合。内核会监控这些集合中的文件描述符,当任何一个文件描述符变为就绪状态时,select会返回。用户进程随后通过遍历这些集合来确定哪些文件描述符已经就绪,并进行相应处理。

源码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int lfd, cfd;
    struct sockaddr_in seraddr, cliaddr;
    fd_set rset, aset;
    int nfds, ret;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(8000);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
    listen(lfd, 128);

    FD_ZERO(&aset);
    FD_SET(lfd, &aset);
    nfds = lfd + 1;

    while (1) {
        rset = aset;
        ret = select(nfds, &rset, NULL, NULL, NULL);
        if (ret == -1) {
            perror("select error");
            exit(1);
        }

        if (FD_ISSET(lfd, &rset)) {
            cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
            FD_SET(cfd, &aset);
            if (nfds - 1 < cfd) {
                nfds = cfd + 1;
            }
        }

        for (int i = lfd + 1; i < nfds; i++) {
            if (FD_ISSET(i, &rset)) {
                char buf[1024];
                int rr = read(i, buf, sizeof(buf));
                if (rr < 0) {
                    perror("read error");
                    exit(1);
                } else if (rr == 0) {
                    FD_CLR(i, &aset);
                    close(i);
                } else {
                    write(i, buf, rr);
                }
            }
        }
    }

    return 0;
}

优势

  • 跨平台性好:select几乎在所有操作系统上都得到了支持,具有良好的跨平台性。

  • 使用简单:select的接口相对简单,易于理解和使用。

劣势

  • 文件描述符数量限制:select使用固定大小的位图来存储文件描述符,导致它能够监控的文件描述符数量有限,通常为1024个

  • 效率低下:每次调用select时,内核和用户空间之间需要复制文件描述符集合,这在大规模并发场景下会导致显著的性能开销。

  • 轮询机制:select使用轮询机制来检查文件描述符的就绪状态,这在高并发场景下会导致效率低下。

三、poll的实现原理及劣势

实现原理

poll方法是对select的改进,它使用链表来存储文件描述符,从而消除了文件描述符数量的限制。poll方法通过调用一个系统调用来监控多个文件描述符,并返回就绪的文件描述符集合。

源码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXFD 1024

int main() {
    int lfd, cfd;
    struct sockaddr_in seraddr, cliaddr;
    struct pollfd fds[MAXFD];
    int nfds, ret;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(8000);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
    listen(lfd, 128);

    nfds = 1;
    fds[0].fd = lfd;
    fds[0].events = POLLIN;

    while (1) {
        ret = poll(fds, nfds, -1);
        if (ret < 0) {
            perror("poll error");
            exit(1);
        }

        if (fds[0].revents & POLLIN) {
            cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
            for (int i = 1; i < MAXFD; i++) {
                if (fds[i].fd < 0) {
                    fds[i].fd = cfd;
                    fds[i].events = POLLIN;
                    break;
                }
            }
            if (i > nfds - 1) {
                nfds = i + 1;
            }
        }

        for (int i = 1; i < nfds; i++) {
            if (fds[i].fd == -1) {
                continue;
            }
            if (fds[i].revents & POLLIN) {
                char buf[1024];
                int rr = read(fds[i].fd, buf, sizeof(buf));
                if (rr < 0) {
                    perror("read error");
                    exit(1);
                } else if (rr == 0) {
                    close(fds[i].fd);
                    fds[i].fd = -1;
                } else {
                    write(fds[i].fd, buf, rr);
                }
            }
        }
    }

    return 0;
}

优势

  • 无文件描述符数量限制:poll使用链表来存储文件描述符,因此没有数量上的限制,可以监控更多的文件描述符。

  • 功能分离:poll将等待队列和阻塞进程分开,使得管理更加灵活。

劣势

  • 效率问题:尽管poll解决了文件描述符数量的限制,但它仍然需要每次调用时复制整个文件描述符集合,这在大规模并发场景下会导致性能问题。

  • 轮询机制:与select类似,poll也使用轮询机制来检查文件描述符的就绪状态,这在高并发场景下效率较低。

四、epoll的实现原理及劣势

实现原理

epoll是Linux特有的IO多路复用机制,它通过红黑树和就绪链表来高效管理文件描述符。epoll提供了三个系统调用:epoll_create用于创建epoll实例,epoll_ctl用于添加、删除或修改监控的文件描述符,epoll_wait用于等待事件并返回就绪的文件描述符集合。

epoll的核心优势在于其事件驱动机制。当文件描述符变为就绪状态时,内核会通过回调函数将其添加到就绪链表中。因此,epoll_wait只需要返回就绪的文件描述符集合,而无需遍历整个文件描述符集合。

源码示例

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

#define MAXEVENTS 10

int main() {
    int lfd, cfd, epfd;
    struct sockaddr_in seraddr, cliaddr;
    struct epoll_event ev, events[MAXEVENTS];
    int nfds, ret;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(8000);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
    listen(lfd, 128);

    epfd = epoll_create(10);
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    while (1) {
        nfds = epoll_wait(epfd, events, MAXEVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait error");
            exit(1);
        }

        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == lfd) {
                cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
            } else {
                char buf[1024];
                int rr = read(events[n].data.fd, buf, sizeof(buf));
                if (rr < 0) {
                    perror("read error");
                    exit(1);
                } else if (rr == 0) {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[n].data.fd, NULL);
                    close(events[n].data.fd);
                } else {
                    write(events[n].data.fd, buf, rr);
                }
            }
        }
    }

    return 0;
}

优势:

  • 高效性:epoll通过红黑树和就绪链表高效地管理文件描述符,避免了大量的数据拷贝和遍历开销,显著提高了性能。

  • 无文件描述符数量限制:epoll支持的文件描述符数量受限于系统最大文件描述符数,远超过select和poll的限制。

  • 事件驱动机制:epoll采用事件驱动机制,只在有事件发生时通知用户进程,减少了不必要的系统调用和上下文切换。

劣势

  • 平台依赖性:epoll是Linux特有的机制,不具备跨平台性,这限制了其在非Linux系统上的使用。

  • 复杂度较高:与select和poll相比,epoll的接口更加底层,需要更多的编程技巧和经验。

  • 频繁系统调用:在大量描述符同时变更状态的情况下,epoll_ctl的系统调用开销可能较高。

应用:

由于epoll的高效性和灵活性,它在多个高性能中间件中得到了广泛应用,如Nginx、Redis、Memcached等。这些中间件需要处理大量的并发连接,epoll能够显著提高它们的性能和稳定性。

五、总结

select、poll和epoll是三种常见的IO多路复用方法,它们各有优缺点。select和poll适用于中小规模的并发场景,但在处理大规模并发连接时效率较低。epoll则专为大规模并发场景设计,通过高效的数据结构和事件驱动机制提供了显著的性能优势。然而,epoll的平台依赖性也限制了其在非Linux系统上的使用。在实际应用中,应根据具体需求选择合适的IO多路复用方法,并不是epoll就一定比select和poll好,虽然epoll却是应该是目前应用最广的IO多路复用方法。


原文地址:https://blog.csdn.net/u013135921/article/details/144385980

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!