• 网络编程:epoll


    原理

    select 的几个缺点:
    1)每次调用select,都需要把fd集合从用户空间拷贝到内核空间,这个开销在fd很多时会很大
    2)每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也会很大
    3)select支持的文件描述符数量太小了,默认是1024
    

    在调用接口上,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄,epoll_ctl是注册要监听的事件类型,epoll_wait是等待事件的产生。
    对于第一个缺点,epoll的解决方案在epoll_ctl函数中。 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
    对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍,并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者,就会调用这个回调函数,而这个 回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(就绪链表是否为空)。
    对于第三个缺点,epoll没有这个限制,它所 支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,具体可以cat /proc/sys/fs/file-max查看,在1GB内存的机器上大约是10万左右。

    epoll的回调机制:

    /* 
     * This is the callback that is used to add our wait queue to the 
     * target file wakeup lists. 
     */  
    static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,  
                     poll_table *pt)  
    {  
        struct epitem *epi = ep_item_from_epqueue(pt);  
        struct eppoll_entry *pwq;  
      
        if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {  
            init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);  
            pwq->whead = whead;  
            pwq->base = epi;  
            add_wait_queue(whead, &pwq->wait);  
            list_add_tail(&pwq->llink, &epi->pwqlist);  
            epi->nwait++;  
        } else {  
            /* We have to signal that an error occurred */  
            epi->nwait = -1;  
        }  
    }  
    

    其中init_waitqueue_func_entry的实现如下:

    static inline void init_waitqueue_func_entry(wait_queue_t *q,  
                        wait_queue_func_t func)  
    {  
        q->flags = 0;  
        q->private = NULL;  
        q->func = func;  
    }  
    

    可以看到,总体上和select实现是类似的,只不过它是创建了一个epoll_entry结构pwq,pwq->wait的func成员被设置成了回调函数ep_poll_callback(而不是default_wake_function,所以这里并不会有唤醒操作而只是执行回调函数),private成员被设置成了NULL。最后把pwq->wait链入到whead中(也就是设备等待队列中)。这样,当设备等待队列中的进程被唤醒时,就会调用ep_poll_callback了。

    epoll的流程:
    当epoll_wait时,它会判断就绪链表中有没有就绪的fd,如果没有,则把current进程加入到一个等待队列(file->private_data->wq)中,并在一个while(1)循环中判断就绪队列是否为空,并结合schedule_timeout实现睡一会。如果current进程在睡眠中,设备就绪了,就会调用回调函数。在回调函数中,会把就绪的fd放到就绪链表,并唤醒等待队列(file->private_data->wq)中的current进程,这样epoll_wait又能继续执行下去了。

    API

    epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的 edge-triggered(边缘触发)机制
    使用 epoll 进行网络程序的编写,需要三个步骤,分别是 epoll_createepoll_ctl epoll_wait

    • epoll_create:用于创建一个epoll实例
    int epoll_create(int size);
    int epoll_create1(int flags); 
    返回值: 若成功返回一个大于0的值,表示epoll实例;若返回-1表示出错
    

    size参数:用来告知内核期望监控的文件描述字大小,然后内核使用这部分的信息来初始化内核数据结构。现在,对size设置为一个大于0的整数就 可以
    flags参数:输入flags为0,则和epoll_create一样,内核自动忽略

    • epoll_ctl :往这个epoll实例中添加删除监控的事件
     int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
            返回值: 若成功返回0;若返回-1表示出错
    

    第一个参数epfd:调用epoll_create创建的epoll实例描述字,可简单理解为epoll的句柄
    第二个参数op:表示增加/删除一个监控事件,有三个选项可供选择:

    • EPOLL_CTL_ADD: 向 epoll 实例注册文件描述符对应的事件;
    • EPOLL_CTL_DEL:向 epoll 实例删除文件描述符对应的事件;
    • EPOLL_CTL_MOD: 修改文件描述符对应的事件。
      第三个参数fd:注册的事件的文字描述符,比如一个监听套接字
      第四个参数event:表示注册的事件类型,并且可以在这个结构体里设置用户需要的数据,其中最为常见的是使用联合结构里的fd字段,表示事件所对应的文件描述符
    typedef union epoll_data {
         void        *ptr;
         int          fd;
         uint32_t     u32;
         uint64_t     u64;
     } epoll_data_t;
    
     struct epoll_event {
         uint32_t     events;      /* Epoll events */
         epoll_data_t data;        /* User data variable */
     };
    

    重点看一下这几种事件类型:

    • EPOLLIN:表示对应的文件描述字可以读;
    • EPOLLOUT:表示对应的文件描述字可以写;
    • EPOLLRDHUP:表示套接字的一端已经关闭,或者半关闭;
    • EPOLLHUP:表示对应的文件描述字被挂起;
    • EPOLLET:设置为 edge-triggered,默认为 level-triggered。
    • epoll_wait:调用者进程被挂起,在等待内核I/O事件的分发
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
      返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1.
    
    • 第一个参数epfd是 epoll 实例描述字,也就是 epoll 句柄。
    • 第二个参数events返回给用户空间需要处理的 I/O 事件,这是一个数组,数组的大小由 epoll_wait 的返回值决定,这个数组的每个元素都是一个需要待处理的 I/O 事件,其中 events 表示具体的事件类型,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据。
    • 第三个参数maxevents是一个大于 0 的整数,表示 epoll_wait 可以返回的最大事件值。
    • 第四个参数timeout是 epoll_wait 阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生

    实践

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <netinet/in.h>
    
    #define SERV_PORT 43211
    #define LISTENQ 1024
    #define MAXEVENTS 128
    
    char rot13_char(char c) {
        if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
            return c + 13;
        else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
            return c - 13;
        else
            return c;
    }
    
    void make_nonblocking(int fd)
    {
        fcntl(fd, F_SETFL, O_NONBLOCK);
    }
    
    
    int tcp_nonblocking_server_listen(int port)
    {
        int listen_fd;
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        make_nonblocking(listen_fd);
    
        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int on = 1;
        setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if(rt1 < 0)
        {
            perror("bind error\n");
            return -1;
        }
    
        int rt2 = listen(listen_fd, LISTENQ);
        if(rt2 < 0)
        {
            perror("listen failed");
            return -1;
        }
    
        signal(SIGPIPE, SIG_IGN);
        return listen_fd;
    
    }
    
    
    int main(int argc, char *argv[])
    {
        int listen_fd, socket_fd;
        int n, i;
        int efd;
        struct epoll_event event;
        struct epoll_event *events;
    
    
        listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
    
        //为epoll创建实例
        efd = epoll_create1(0);
        if(efd == -1)
        {
            perror("epoll create failed");
            return -1;
        }
    
        event.data.fd = listen_fd;
        event.events = EPOLLIN | EPOLLET;
        // 调用epoll_ctl将监听字对应的I/O事件进行注册,有新的连接建立,就可以感知,采用edge-triggered边缘触发
        if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
        {
            perror("epoll_ctl add listen fd failed");
            return -1;
        }
    
        // Buffer where events are returned
        events = calloc(MAXEVENTS, sizeof(event));
    
        while(1)
        {
            // 调用epoll_wait函数分发I/O事件,当epoll_wait成功返回后,通过遍历返回的event数组,就可知道发生的I/O事件
            n = epoll_wait(efd, events, MAXEVENTS, -1);
            printf("epoll_waite wakeup\n");
            for(i = 0; i < n; i++)
            {
                if((events[i].events & EPOLLERR) ||
                    (events[i].events & EPOLLHUP) ||
                    (!events[i].events & EPOLLIN))
                    {
                        fprintf(stderr, "epoll error\n");
                        close(events[i].data.fd);
                        continue;
                    }
                    else if(listen_fd == events[i].data.fd)
                    {
                        struct sockaddr_storage ss;
                        socklen_t slen = sizeof(ss);
                        int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                        if(fd < 0)
                        {
                            perror("accept failed");
                            return -1;
                        }
                         else
                        {
                            // accept建立连接,并将该连接设置为非阻塞,在调用epoll_ctl把已连接套接字对应的可读事件
                            // 注册到epoll实例中,这里使用了event_data里面的fd字段,将连接套接字存储器中
                            make_nonblocking(fd);
                            event.data.fd = fd;
                            event.events = EPOLLIN | EPOLLET;// edge-triggered
                            if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                            {
                                perror("epoll_ctl add connection fd failed");
                                return -1;
                            }
                        }
                        continue;
                    }
                    else
                    {
                        socket_fd = events[i].data.fd;
                        printf("get event on socket fd == %d\n",socket_fd);
                        while(1)
                        {
                            char buf[512];
                            if((n = read(socket_fd, buf, sizeof(buf))) < 0)
                            {
                                if(errno != EAGAIN)
                                {
                                    perror("read error");
                                    close(socket_fd);
                                }
                                break;
                            }
                            else if(n == 0)
                            {
                                close(socket_fd);
                                break;
                            }
                            else 
                            {
                                for(i = 0;i < n; ++i)
                                {
                                    buf[i] = rot13_char(buf[i]);
                                }
                                if(write(socket_fd, buf, n) < 0)
                                {
                                    perror("write error");
                                    return -1;
                                }
                            }
                        }
                    }
                   
            }
        }
        free(events);
        close(listen_fd);
        return 0;
    }
    

    运行结果

    边缘触发vs水平触发

    条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
    一般认为,边缘触发的效率比条件触发的效率要高
    边缘触发:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <netinet/in.h>
    
    #define SERV_PORT 43211
    #define LISTENQ 1024
    #define MAXEVENTS 128
    
    char rot13_char(char c) {
        if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
            return c + 13;
        else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
            return c - 13;
        else
            return c;
    }
    
    void make_nonblocking(int fd)
    {
        fcntl(fd, F_SETFL, O_NONBLOCK);
    }
    
    
    int tcp_nonblocking_server_listen(int port)
    {
        int listen_fd;
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        make_nonblocking(listen_fd);
    
        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int on = 1;
        setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if(rt1 < 0)
        {
            perror("bind error\n");
            return -1;
        }
    
        int rt2 = listen(listen_fd, LISTENQ);
        if(rt2 < 0)
        {
            perror("listen failed");
            return -1;
        }
    
        signal(SIGPIPE, SIG_IGN);
        return listen_fd;
    
    }
    
    
    int main(int argc, char *argv[])
    {
        int listen_fd, socket_fd;
        int n, i;
        int efd;
        struct epoll_event event;
        struct epoll_event *events;
    
    
        listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
    
        efd = epoll_create1(0);
        if(efd == -1)
        {
            perror("epoll create failed");
            return -1;
        }
    
        event.data.fd = listen_fd;
        event.events = EPOLLIN | EPOLLET;
        if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
        {
            perror("epoll_ctl add listen fd failed");
            return -1;
        }
    
        // Buffer where events are returned
        events = calloc(MAXEVENTS, sizeof(event));
    
        while(1)
        {
            n = epoll_wait(efd, events, MAXEVENTS, -1);
            printf("epoll_waite wakeup\n");
            for(i = 0; i < n; i++)
            {
                if((events[i].events & EPOLLERR) ||
                    (events[i].events & EPOLLHUP) ||
                    (!events[i].events & EPOLLIN))
                    {
                        fprintf(stderr, "epoll error\n");
                        close(events[i].data.fd);
                        continue;
                    }
                    else if(listen_fd == events[i].data.fd)
                    {
                        struct sockaddr_storage ss;
                        socklen_t slen = sizeof(ss);
                        int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                        if(fd < 0)
                        {
                            perror("accept failed");
                            return -1;
                        }
                         else
                        {
                            make_nonblocking(fd);
                            event.data.fd = fd;
                            event.events = EPOLLIN | EPOLLET;// edge-triggered
                            if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                            {
                                perror("epoll_ctl add connection fd failed");
                                return -1;
                            }
                        }
                        continue;
                    }
                    else
                    {
                        socket_fd = events[i].data.fd;
                        printf("get event on socket fd == %d\n",socket_fd);
                    }
                   
            }
        }
        free(events);
        close(listen_fd);
        return 0;
    }
    
    

    执行效果:

    可发现,边缘触发情况下,开启这个服务器程序,用 telnet 连接上,输入一些字符,我们看到,服务器端只从 epoll_wait 中苏醒过一次,就是第一次有数据可读的时候。
    水平触发:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <netinet/in.h>
    
    #define SERV_PORT 43211
    #define LISTENQ 1024
    #define MAXEVENTS 128
    
    char rot13_char(char c) {
        if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
            return c + 13;
        else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
            return c - 13;
        else
            return c;
    }
    
    void make_nonblocking(int fd)
    {
        fcntl(fd, F_SETFL, O_NONBLOCK);
    }
    
    
    int tcp_nonblocking_server_listen(int port)
    {
        int listen_fd;
        listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    
        make_nonblocking(listen_fd);
    
        struct sockaddr_in servaddr;
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int on = 1;
        setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        int rt1 = bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if(rt1 < 0)
        {
            perror("bind error\n");
            return -1;
        }
    
        int rt2 = listen(listen_fd, LISTENQ);
        if(rt2 < 0)
        {
            perror("listen failed");
            return -1;
        }
    
        signal(SIGPIPE, SIG_IGN);
        return listen_fd;
    
    }
    
    
    int main(int argc, char *argv[])
    {
        int listen_fd, socket_fd;
        int n, i;
        int efd;
        struct epoll_event event;
        struct epoll_event *events;
    
    
        listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
    
        efd = epoll_create1(0);
        if(efd == -1)
        {
            perror("epoll create failed");
            return -1;
        }
    
        event.data.fd = listen_fd;
        event.events = EPOLLIN | EPOLLET;
        if(epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1)
        {
            perror("epoll_ctl add listen fd failed");
            return -1;
        }
    
        // Buffer where events are returned
        events = calloc(MAXEVENTS, sizeof(event));
    
        while(1)
        {
            n = epoll_wait(efd, events, MAXEVENTS, -1);
            printf("epoll_waite wakeup\n");
            for(i = 0; i < n; i++)
            {
                if((events[i].events & EPOLLERR) ||
                    (events[i].events & EPOLLHUP) ||
                    (!events[i].events & EPOLLIN))
                    {
                        fprintf(stderr, "epoll error\n");
                        close(events[i].data.fd);
                        continue;
                    }
                    else if(listen_fd == events[i].data.fd)
                    {
                        struct sockaddr_storage ss;
                        socklen_t slen = sizeof(ss);
                        int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
                        if(fd < 0)
                        {
                            perror("accept failed");
                            return -1;
                        }
                         else
                        {
                            make_nonblocking(fd);
                            event.data.fd = fd;
                            event.events = EPOLLIN;// level-triggered
                            if(epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1)
                            {
                                perror("epoll_ctl add connection fd failed");
                                return -1;
                            }
                        }
                        continue;
                    }
                    else
                    {
                        socket_fd = events[i].data.fd;
                        printf("get event on socket fd == %d\n",socket_fd);
                       
                    }
                   
            }
        }
        free(events);
        close(listen_fd);
        return 0;
    }
    

    效果:

    然后按照同样的步骤来一次,观察服务器端,可看到,服务器端不断地从 epoll_wait 中苏醒,告诉我们有数据需要读取。

    小结

    epoll 通过改进的接口设计,避免了用户态 - 内核态频繁的数据拷贝,大大提高了系统性能。在使用 epoll 的时候,我们一定要理解条件触发和边缘触发两种模式。条件触发的意思是只要满足事件的条件,比如有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

  • 相关阅读:
    备忘录模式
    观察者模式
    状态模式
    模板方法模式
    策略模式
    装饰者模式
    访问者模式
    工作那些事(二十七)项目经理在项目中是什么角色?
    工作那些事(二十六)个人和团队
    工作那些事(二十五)项目经理与产品经理
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16029403.html
Copyright © 2020-2023  润新知