说一下epoll的好处

Epoll是Linux下高效的I/O多路复用机制,它在设计上克服了select的多个缺陷,具有以下优点:

  1. 突破文件描述符数量限制。Epoll使用一个文件描述符管理多个socket连接,将用户关心的socket事件通过epoll_ctl维护在内核中,因此不存在描述符数量的限制,一般只与系统资源有关。

  2. O(1)时间复杂度。Epoll使用事件驱动机制,当某个socket有事件发生时,内核会使用回调函数将其加入就绪队列。Epoll_wait只需要从就绪队列中取出事件,无须遍历整个描述符集,因此时间复杂度是O(1)。

  3. 内存拷贝次数少。Epoll使用mmap在内核和用户空间之间建立映射,通过这个映射区域传递事件,减少了内存拷贝的次数。此外,内核还可以通过共享内存直接访问用户态的数据,再一次减少了数据拷贝。

  4. 支持多种事件触发模式。Epoll支持边缘触发(edge-triggered)和水平触发(level-triggered)两种事件模式。边缘触发只在socket状态发生变化时才触发事件,避免了重复触发。而水平触发与select和poll的行为类似,只要socket处于就绪状态就一直触发。

下面是一个使用epoll边缘触发实现高效回显服务器的例子:

#include <iostream>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
using namespace std;

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

    sockaddr_in srvAddr;
    memset(&srvAddr, 0, sizeof(srvAddr));
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    srvAddr.sin_port = htons(9999);
    bind(listenFd, (sockaddr*)&srvAddr, sizeof(srvAddr));

    listen(listenFd, 5);

    int epfd = epoll_create(1);
    epoll_event ev, events[1024];
    ev.data.fd = listenFd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenFd, &ev);

    while (true) {
        int nReady = epoll_wait(epfd, events, 1024, -1);
        for (int i = 0; i < nReady; ++i) {
            if (events[i].data.fd == listenFd) {
                sockaddr_in cliAddr;
                socklen_t len = sizeof(cliAddr);
                int connFd = accept(listenFd, (sockaddr*)&cliAddr, &len);
                ev.data.fd = connFd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connFd, &ev);
            } else {
                int fd = events[i].data.fd;
                char buf[256];
                while (true) {
                    memset(buf, 0, sizeof(buf));
                    int n = read(fd, buf, sizeof(buf));
                    if (n == -1) {
                        if (errno == EAGAIN) {
                            break;
                        }
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        break;
                    } else if (n == 0) {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); 
                        close(fd);
                        break;
                    } else {
                        write(fd, buf, strlen(buf));
                    }
                }
            }
        }
    }

    close(listenFd);
    return 0;
}

该例子中使用了epoll的以下特性:

  1. 使用epoll_create创建一个epoll实例,返回一个表示epoll的文件描述符。

  2. 使用epoll_ctl将需要监听的socket添加到epoll实例中,并设置关心的事件类型。例子中,我们对监听socket关心”可读”事件,对已连接socket关心”可读”和”边缘触发”事件。

  3. 使用epoll_wait等待事件发生。一旦有事件发生,epoll_wait就会返回,并将发生的事件填充到传入的数组中。

  4. 遍历事件数组,根据事件类型进行不同处理。例子中,如果发生事件的是监听socket,则接受新连接;如果是已连接socket,则进行读写。

  5. 由于使用了边缘触发,一次事件到来时需要将socket的数据全部处理完毕。因此,例子中使用了while循环不断读取数据,直到read返回EAGAIN错误,表示数据已被读完。

总的来说,epoll是Linux下高性能网络编程的利器。它在高并发场景下表现优异,已被Nginx、Redis等知名项目广泛使用。对epoll的深入理解和应用,是C++服务端开发者的必备技能。