• Linux c编程:I/O多路复用之epoll


    前面介绍了select处理,这一章继续介绍另外一种I/O多路服用的机制:epoll。来比较下两种机制的不同点。
    select: 调用过程如下:

    1)使用copy_from_user从用户空间拷贝fd_set到内核空间

    2)注册回调函数__pollwait

    3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_pollsock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll

    4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

    5__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

    6poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

    7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd

    8)把fd_set从内核空间拷贝到用户空间

     

    总结:

    select的几大缺点:

    1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

    2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

    3select支持的文件描述符数量太小了,默认是1024

     

    对于select的几个缺点。epoll的改进机制如下:

    对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

      对于第二个缺点,epoll的解决方案不像selectpoll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

      对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

    总结:

    1selectpoll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是selectpoll醒着的时候要遍历整个fd集合,而epoll醒着的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

    2selectpoll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销

     

    epoll的接口函数很简单,只有3个函数

    1. int epoll_create(int size); 

    创建一个 epoll 的句柄, size 用来告诉内核这个监听的数目一共有多大。这个参数不同于 select() 中的第一个参数,给出最大监听的 fd+1 的值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/ ,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。 

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

    epoll 的事件注册函数,它不同与 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。 

    第一个 参数是 epoll_create() 的返回值, 

    第二个 参数表示动作,用三个宏来表示: 

    EPOLL_CTL_ADD :注册新的 fd 到 epfd 中; 

    EPOLL_CTL_MOD :修改已经注册的 fd 的监听事件; 

    EPOLL_CTL_DEL :从 epfd 中删除一个 fd ; 

    第三个 参数是需要监听的 fd , 

    第四个 参数是告诉内核需要监听什么事, struct epoll_event 结构如下: 

    struct epoll_event { 

      __uint32_t events;  /* Epoll events */ 

      epoll_data_t data;  /* User data variable */ 

    }; 

      

    events 可以是以下几个宏的集合: 

    EPOLLIN :      表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭); 

    EPOLLOUT :     表示对应的文件描述符可以写; 

    EPOLLPRI :       表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 

    EPOLLERR :      表示对应的文件描述符发生错误; 

    EPOLLHUP :      表示对应的文件描述符被挂断; 

    EPOLLET :       将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的。 

    EPOLLONESHOT : 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里 

      

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

    等待事件的产生,类似于 select() 调用。参数 events 用来从内核得到事件的集合, maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size ,参数 timeout 是超时时间(毫秒, 会立即返回, -1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 表示已超时。 epoll有两种工作方式:

    LT level triggered 水平触发模式,

     同时支持阻塞和非阻塞的socket。在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行I/O操作,如果你不做任何操作,还是会继续通知你。(没处理这个流还是一直通知你)

     ET edge triggered 边缘触发模式

    只支持非阻塞的socket。效率比LT高。这种工作模式下,当从epoll_wait调用获取到事件后,如果没有把这次事件对应的套接字处理完,那么在这个套接字中没有心的时间再次到来时,ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT只要有数据就总可以获取。

    参考下面这个图:

    实现代码如下:

    //网络编程服务端

    #include <stdio.h>

    #include <stdlib.h>

    #include <string.h>

    #include <unistd.h>

    #include <errno.h>

    #include <sys/types.h>

    #include <sys/socket.h>

    #include <arpa/inet.h>//htons()函数头文件

    #include <netinet/in.h>//inet_addr()头文件

    #include <fcntl.h>

    #include <sys/epoll.h>

    #include "pub.h"

    #define MAXSOCKET 20

    int main(int arg, char *args[])

    {

        if (arg < 2)

        {

            printf("please print one param! ");

            return -1;

        }

        //create server socket

        int listen_st = server_socket(atoi(args[1]));

        if (listen_st < 0)

        {

            return -1;

        }

        /*

         * 声明epoll_event结构体变量ev,变量ev用于注册事件,

         * 数组events用于回传需要处理的事件

         */

        struct epoll_event ev, events[100];

        //生成用于处理accept的epoll专用文件描述符

        int epfd = epoll_create(MAXSOCKET);

        //把socket设置成非阻塞方式    setnonblock(listen_st);

        //设置需要放到epoll池里的文件描述符

        ev.data.fd = listen_st;

        //设置这个文件描述符需要epoll监控的事件

        /*

         * EPOLLIN代表文件描述符读事件

         *accept,recv都是读事件

         */

        ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

        /*

         * 注册epoll事件

         * 函数epoll_ctl中&ev参数表示需要epoll监视的listen_st这个socket中的一些事件

         */

        epoll_ctl(epfd, EPOLL_CTL_ADD, listen_st, &ev);

     

        while (1)

        {

            /*

             * 等待epoll池中的socket发生事件,这里一般设置为阻塞的

             * events这个参数的类型是epoll_event类型的数组

             * 如果epoll池中的一个或者多个socket发生事件,

             * epoll_wait就会返回,参数events中存放了发生事件的socket和这个socket所发生的事件

             * 这里强调一点,epoll池存放的是一个个socket,不是一个个socket事件

             * 一个socket可能有多个事件,epoll_wait返回的是有消息的socket的数目

             * 如果epoll_wait返回事件数组后,下面的程序代码却没有处理当前socket发生的事件

             * 那么epoll_wait将不会再次阻塞,而是直接返回,参数events里面的就是刚才那个socket没有被处理的事件

             */

            int nfds = epoll_wait(epfd, events, MAXSOCKET, -1);

            if (nfds == -1)

            {

                printf("epoll_wait failed ! error message :%s  ", strerror(errno));

                break;

            }

            int i = 0;

            for (; i < nfds; i++)

            {

                if (events[i].data.fd < 0)

                    continue;

                if (events[i].data.fd == listen_st)

                {

                    //接收客户端socket

                    int client_st = server_accept(listen_st);

                    /*

                     * 监测到一个用户的socket连接到服务器listen_st绑定的端口

                     *

                     */

                    if (client_st < 0)

                    {

                        continue;

                    }

                    //设置客户端socket非阻塞                setnonblock(client_st);

                    //将客户端socket加入到epoll池中

                    struct epoll_event client_ev;

                    client_ev.data.fd = client_st;

                    client_ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_st, &client_ev);

                    /*

                     * 注释:当epoll池中listen_st这个服务器socket有消息的时候

                     * 只可能是来自客户端的连接消息

                     * recv,send使用的都是客户端的socket,不会向listen_st发送消息的

                     */

                    continue;

                }

                //客户端有事件到达

                if (events[i].events & EPOLLIN)

                {

                    //表示服务器这边的client_st接收到消息

                    if (socket_recv(events[i].data.fd) < 0)

                    {

                        close_socket(events[i].data.fd);

                        //接收数据出错或者客户端已经关闭

                        events[i].data.fd = -1;

                        /*这里continue是因为客户端socket已经被关闭了,

                         * 但是这个socket可能还有其他的事件,会继续执行其他的事件,

                         * 但是这个socket已经被设置成-1

                         * 所以后面的close_socket()函数都会报错

                         */

                        continue;

                    }

                    /*

                     * 此处不能continue,因为每个socket都可能有多个事件同时发送到服务器端

                     * 这也是下面语句用if而不是if-else的原因,

                     */

     

                }

                //客户端有事件到达

                if (events[i].events & EPOLLERR)

                {

                    printf("EPOLLERR ");

                    //返回出错事件,关闭socket,清理epoll池,当关闭socket并且events[i].data.fd=-1,epoll会自动将该socket从池中清除                close_socket(events[i].data.fd);

                    events[i].data.fd = -1;

                    continue;

                }

                //客户端有事件到达

                if (events[i].events & EPOLLHUP)

                {

                    printf("EPOLLHUP ");

                    //返回挂起事件,关闭socket,清理epoll池                close_socket(events[i].data.fd);

                    events[i].data.fd = -1;

                    continue;

                }

            }

        }

        //close epoll    close(epfd);

        //close server socket    close_socket(listen_st);

        return 0;

    }

  • 相关阅读:
    12、闭包函数、装饰器、迭代器
    11、函数对象、函数的嵌套、名称空间与作用域
    10、初识函数
    9、文件操作
    8、字符编码-Python(转)
    7、str字符串、int整数、list列表、dict字典、set集合、tuple元祖功能详解
    python day11 学习整理
    python day9 学习整理
    python day8 学习整理
    python day7 学习整理
  • 原文地址:https://www.cnblogs.com/zhanghongfeng/p/9656245.html
Copyright © 2020-2023  润新知