美文网首页网络与I/O
epoll,解决C10K问题的关键

epoll,解决C10K问题的关键

作者: 肥兔子爱豆畜子 | 来源:发表于2020-12-16 17:23 被阅读0次

    epoll是event poll的意思。因为涉及到用户态wait获取到内核返回的读写就绪事件之后、去主动到内核缓冲区获取数据、所以本质上是属于同步非阻塞io模型。linux上边真正的异步非阻塞io模型还没提供、windows倒是有了,但是服务器端仍然是linux的天下,所以现在真正事实上的标准高性能网络io模型仍然是epoll。它也是解决c10k问题的关键突破。

    一、epoll在linux中的定义和系统调用
    //用户数据载体、用来用户态和内核态交换数据

    typedef union epoll_data {
        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    

    //fd装载入内核的载体

    struct epoll_event {
        uint32_t events; /* Epoll events */
        epoll_data_t data; /* User data variable */
    };
    

    //三个主要api

    int epoll_create(int size);  
    

    //在内核空间创建epoll句柄epfd,就是新建一个多路复用器(struct eventpoll对象)然后返回它的文件描述符。可以类比java nio里的selector.open()方法。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
    

    //向epfd指向的内核空间里边的多路复用器epoll添加/删除(int op)一个fd以及感兴趣的事件event。这个event是epoll_event类型,里边封装了epoll_data用户态的数据、以及事件类型。可以类比一下java nio里边的register()方法。

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  
    

    //这个就是需要用户态程序去轮询的方法了,可以查到就绪的事件、内核会通过这个方法告知用户态程序就绪的events。一般来说用户态程序拿到一批就绪的事件以后会遍历、然后逐个判断属于哪种事件、比如是connect还是read/write,然后执行不同的逻辑。类比java nio里的selector.selectedKeys()。

    //epoll官方例子

    #define MAX_EVENTS 10
    struct epoll_event ev, events[MAX_EVENTS];
    int listen_sock, conn_sock, nfds, epollfd;
    /* Set up listening socket, 'listen_sock' (socket(),bind(), listen()) */
    epollfd = epoll_create(10); //创建多路复用器epollfd
    if(epollfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    } 
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { //向创建好的多路复用器epollfd添加server socket, 然后指定其event为EPOLLIN即监听客户端连接请求
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    } 
    for(;;) { //自旋
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //从多路复用器epollfd中查询已就绪的events
        if (nfds == -1) {
            perror("epoll_pwait");
            exit(EXIT_FAILURE);
        } 
        for (n = 0; n < nfds; ++n) {
            if (events[n].data.fd == listen_sock) {
                //主监听socket有新连接,connect事件
                conn_sock = accept(listen_sock,(struct sockaddr *) &local, &addrlen);
                if (conn_sock == -1) {
                    perror("accept");
                    exit(EXIT_FAILURE);
                } 
                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: conn_sock");
                    exit(EXIT_FAILURE);
                }
            } else {
                //已建立连接的可读写句柄,read/write事件
                do_use_fd(events[n].data.fd);
            }
        }
    }
    

    二、LT模式和ET模式的简单理解
    level triggered水平触发和edge triggered边缘触发,指的是epoll在socket读写上的两种通知方式。

    1、LT 是默认的模式,支持阻塞和非阻塞socket、epoll_wait获取到内核拷贝来的就绪事件之后、用户态程序如果没有处理完、下次仍然会在调用epoll_wait的时候通知给用户态程序、数据不会丢失因为反复提醒、更加安全。
    这里边read的话只要有数据就通知就绪可读了,但是write的话,一般来说socket空闲了、写缓冲区不满就会提醒写就绪、也就是反复的提醒某个socket可写。但此时用户态程序可能并没有对这个socket的写的需求,大量连接的时候这也是个不小的开销,所以一般是在没有数据要发送的时候,由用户态程序把对应的fd写事件从epoll列表里去掉,需要的时候再加进去。

    2、ET模式是高速模式,只支持非阻塞socket,如果用户态程序对事件没有处理完,那下一次epoll_wait调用就不会继续通知了。对用户态读写处理的逻辑容错提出了更高的要求、但因为没有反复通知、所以性能更高。简单来说ET模式只在socket的读写状态发生变化的时候通知、状态不变则不通知,比如读缓冲区由无数据到有数据通知read事件、写缓冲区由满到未满通知可写write事件。
    通过前面的对比可以看到LT模式比较安全并且代码编写也更清晰,但是ET模式属于高速模式,在处理大高并发场景使用得当效果更好,具体选择什么根据自己实际需要和团队代码能力来选择。

    相关文章

      网友评论

        本文标题:epoll,解决C10K问题的关键

        本文链接:https://www.haomeiwen.com/subject/isgngktx.html