美文网首页
Select_poll_epoll详解

Select_poll_epoll详解

作者: R0lan | 来源:发表于2019-07-26 17:00 被阅读0次

    Select_poll_epoll详解


    参考链接

    1. epoll简介及触发模式(accept、read、send)
    2. epoll内核源码详解+自己总结的流程
    3. linux man page

    epoll函数

    注意: epoll不属于任何namespace。

    #include <sys/epoll.h>
    
    int epoll_create(int size);  // return epollfd, 失败return -1
    
    /*
    op:
    EPOLL_CTL_ADD
    EPOLL_CTL_MOD
    EPOLL_CTL_DEL 如果是delete的话, epoll_ctl的最后一个参数event可以是NULL
    */
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  // 成功return0, 失败return -1
    int epoll_wait(int epfd, struct epoll_event *events,
                    int maxevents, int timeout);  // 成功return nready. 失败return -1
    
    //epoll_event
    /*
    其实这个epoll_data只是给用户自行使用的,epoll不关心里面的内容。 这个dta回随着epoll_data 返回的epoll_event一并返回
    */
    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 */
    };
    

    close

    其实在外面关闭一个fd之后,就可以不用再在epoll list里面删除了,但是为了安全起见,还是用EPOLL_CTL_DEL删掉吧。详情可以看 epoll(7) man page FAQ。

    epoll event

    1. EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    2. EPOLLOUT:表示对应的文件描述符可以写;
    3. EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    4. EPOLLERR:表示对应的文件描述符发生错误;
    5. EPOLLHUP:表示对应的文件描述符被挂断;
    6. EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    7. EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

    EL/LT

    有关ET/LT 阻塞/非阻塞的操作,网络上基本都是错的,只要你安排的好,既可以用阻塞,也可以用非阻塞。(linux man page上也让你用阻塞)

    ET Edge Trigger 边沿触发工作模式

    1. 必须使用非阻塞 工作模式,因为在循环调用epoll_wait的时候,有可能某个句柄已知会ready, 如果用阻塞操作,会导致一个文件句柄的阻塞操作把多个文件描述符饿死。
      1. 基于非阻塞文件句柄
      2. 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待(退出read/write返回epoll_wait)。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf)),就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
      3. 阻塞IO的事件处理原则:
        1. recv() > 0:(并且小于请求的数据长度sizeof(buf)), 表示接收数据完毕,返回值即是接收到的字节数。
        2. recv() == 0: 表示链接已经正常断开,这个时候就可以把fd关掉,从epoll里面移除了
        3. recv() < 0 && errno == EAGAIN: 表示recv操作还未完成
        4. recv() < 0 && errno != EAGAIN: 表示操作遇到系统errno
    2. 边缘触发但是这种模式下在读数据的时候一定要注意,因为如果一次可写事件我们没有把数据读完,如果没有读完,在socket没有新的数据可读时epoll就不回返回了,只有在新的数据到来时,我们才能读取到上次没有读完的数据。最差的情况是client在发送的n个byte之后已经关闭了,但是epoll由于接收缓冲区没有清空,这个fd在服务端并不会关掉。
    3. 使用ET模式,就算接收缓冲区里的数据没有读完,如果再接收到新的数据, epoll_wait 还是会触发可读事件的。
    4. 设置为EPOLLET之后仍然会对同一事件多次触发的原因:
      1. 接收缓冲区过小,无法容纳所有发送过来的数据
      2. 用EPOLL_CTL_MOD更改了epollevent,会重置之前的触发(这个我自己没有复现出来)

    LT Level Trigger 水平触发工作模式

    1. poll(), select() 都是水平触发
    2. 如果我们用水平触发不用担心数据有没有读完因为下次epoll返回时,没有读完的socket依然会被返回
    3. 但是要注意这种模式下的写事件,因为是水平触发,每次socket可写时epoll都会返回,当我们写的数据包过大时,一次写不完,要多次才能写完或者每次socket写都写一个很小的数据包时,每次写都会被epoll检测到,因此长期关注socket写事件会无故cpu消耗过大甚至导致cpu跑满,所以在水平触发模式下我们一般不关注socket可写事件而是通过调用socket write或者send api函数来写socket
    4. 我们可以看到这种模式在效率上是没有边缘触发高的,因为每个socket读或者写可能被返回两次甚至多次

    epoll 源码解析

    https://blog.csdn.net/wangyin159/article/details/48895287

    epoll_wait

    1. 检查MAXEXENT参数
    2. 用access_ok() 检查event指针是否可写,如果这个指针是空指针或者指向内核态的指针,那么会设置errno EFAULT。
      1. Just because a pointer was supplied by userspace doesn't mean that it's definitely a userspace pointer - in many cases "kernel pointer" simply means that it's pointing within a particular region of the virtual address space.https://stackoverflow.com/questions/12357752/what-is-the-point-of-using-the-linux-macro-access-ok
    3. 获取epfd对应的eventpoll文件实例,如果取不到,errno:EBADF
    4. 检查eventpoll文件是不是真的是一个epoll文件, 如果不是说值errno EINVAL
    5. 其实epoll_wait 中如果出错了,那么基本上应该是程序本身的问题,比如陷入死循环之类
    6. 调用ep_epoll函数,这个函数在做一些配置之后就会主动让出处理器,进入睡眠状态,等待文件就绪(回调函数唤醒本进程)或者超时或者信号中断
    • 缺省的工作模式

    一道腾讯后台开发面试题

    Q:使用Linux epoll模型,水平(LT)触发模式,当socket可写时,会不停的触发socket可写的事件,如何处理?

    1. 第一种最普遍的方式:
      • 需要向socket写数据的时候才把socket加入epoll,等待可写事件。接受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll(用EPOLLONESHOT也行)。
      • 这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。
    2. 一种改进的方式:
      • 开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN(缓冲区满了,后面还需要继续发),把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
      • 这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。

    ET/LT 比较

    1. 因为ET要基于非阻塞IO, LT在读写的时候不必等待EAGAIN的出现,可以节省系统调用次数,降低延迟

    epoll 优点

    1. 对应select()的缺点, epoll都有解决的方法

      1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
        • 使用epoll_ctl()函数,只有在注册、修改、删除的时候才会对内核进行操作。
      2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
        • epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)
      3. select支持的文件描述符数量太小了,默认是1024
        • epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,我的1GB内存阿里云ECS是999999,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
    2. poll每次返回整个文件描述符数组, 用户需要遍历数组已找到哪些文件描述符上有IO事件。 而epoll_wait(2)返回的是活动fd的列表,需要遍历的数组通常会小很多,在并发连接数较大而活动连接比例不高时,epoll(4)比epoll(2)更高效。

    epoll 源码解读

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    struct eventpoll{
        ....
        /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
        struct rb_root  rbr;
        /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
        struct list_head rdlist;
        ....
        };
    
    

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    struct epitem{
        struct rb_node  rbn;//红黑树节点
        struct list_head    rdllink;//双向链表节点
        struct epoll_filefd  ffd;  //事件句柄信息
        struct eventpoll *ep;    //指向其所属的eventpoll对象
        struct epoll_event event; //期待发生的事件类型
        }
    

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    image.png

    select()

    select()简介

    1. select()函数是阻塞的, 只有某些端口状态转换了或者达到timeout才会返回
    2. 该函数可以允许进程指示等待多个事件中任何一个的发生
    3. select(), poll() 都是水平触发

    为什么需要select()?

    1. 多路复用io mutiplexing
      1. 如果不采用多路复用,要么使用阻塞IO(会使线程长时间处于阻塞状态,无法执行任何计算或者响应任何网络请求),要么使用非阻塞IO:(要用while循环调用recv函数,大幅占用CPU资源), 复用的优势在于可以同时处理多个连接

    select()函数

    #include <sys/select.h>
    #include <sys/time.h>
    
    int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    

    返回: 若有描述符就绪,则返回就绪描述符的数量,若超时则为0, 若出错则为1

    1. timeout-->timeval

     struct timeval {
       long tv_sec;  // seconds
       log tv_usec;  // microseconds
     }
    
    • 用于指定timeout的秒数和微秒数
    • 如果输入为0,那么select函数会一直等下去一直到某个描述符准备好
    • 如果输入这个参数,那么最长等待时间就确定了
    • 如果输入这个结构,但是其中的两个值为0,那么就不等待-->轮询机制
    延伸 gettimeofday()
    • 用gettimeofday() 可以获得微秒(us)级别的时间。
    • 会把目前的时间tv所指的结构返回,当地时区的信息则放到tz所指的结构中。
    • 1970年1月1日到现在的时间
    • 调用两次gettimeofday(), 前后做减法,从而达到计算时间的目的。
    #include <sys/time.h>
    int gettimeofday(struct timeval *tv,struct timezone *tz);
    

    2. readset, writeset, exceptset

    #include <sys/select.h>
    
    struct fd_set myset;
    //四个相关的宏函数
    
    void FD_ZERO(fd_set *fdset);  // clean all bits at fdset
    void FD_SET(int fd, fd_set *fdset);  // turn on the bit for fd in fdset
    void FD_CLR(int fd, fd_set *fdset);  // turn on the bit for fd in fdset
    void FD_ISSET(int fd, fd_set *fdset);  //is the bit for fd on in fdset? 如果set了,返回1
    
    1. fd_set 每一位表示一个fd, set其中的某一位就表示要监视某个fd.
    2. 指针输入, 输入的时候把我们所关心的fd置为1. 返回时,他将指示哪些描述符已经就绪了。因此,每次重新调用select时,我们都需要再次把所有我们关心的描述符置为1。

    3. maxfdp1

    1. maxfdp1 = 最大描述符+1
    2. 最大描述符系统内是有定义的 FD_SETSIZE

    例子

    select\strcliselect01.c

    void
    str_cli(FILE *fp, int sockfd)
    {
      int maxfdp1;
      fd_set  rset;
      char  sendline[MAXLINE], recvline[MAXLINE];
    
      FD_ZERO(&rset);
      for ( ; ; ) {
        FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;
        Select(maxfdp1, &rset, NULL, NULL, NULL);
    
        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */
          if (Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
          Fputs(recvline, stdout);
        }
    
        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
          if (Fgets(sendline, MAXLINE, fp) == NULL)
            return;     /* all done */
          Writen(sockfd, sendline, strlen(sendline));
        }
      }
    }
    

    select 缺点

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

    select() 文件描述符上限

    这个问题的关键其实要先理解select关于文件描述符上限的原因

    1. linux系统本身就有文件描述符上限,文件描述符的建立会连带建立很多其它表项,具体可以搜索文件描述符的详解,也就是说文件描述符一定会占用资源,那在有限的硬件条件下,文件描述符必定会有上限,我在ubuntu14.04的ECS里通过
    cat /proc/sys/fs/file-max //结果99999
    
    1. 进程文件描述符上限user limit中nofile的soft limit,实际上这是单个用户的文件描述符上限,通过
    ulimit -n //结果65535
    

    soft limit可以修改,但是不能超过hard limit

    ulimit -Hn //结果65535
    
    1. select函数本身限制,主要是头文件中FD_SETSIZE的大小,一般来说是1024,这就限定了select函数中的文件描述符上限,当然可以做修改,但是需要重新编译内核,而且效果由于select的实现机制,会比较差

    poll()

    #include <poll.h>
    #include    <limits.h>      /* for OPEN_MAX */ // 描述了poll的最大数量
    
    int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
    

    返回: 若有描述符就绪,则返回就绪描述符的数量,若超时则为0, 若出错则为1

    fdarray

    指向一个数组结构第一个元素的指针,没一个元素都是一个pollfd结构,使用这个结构,避免了select中使用一个参数既表示我们关心的值,又表示结果。

    struct pollfd {
      int fd;  // 描述符
      short events;  // 我们关心的状态
      short revents;  // 返回的结果
    }
    

    nfds

    第一个参数中的数组元素的个数

    timeout

    timeout 说明
    INFTIM 永远等待
    0 立即返回,不阻塞进程
    > 0 等待指定的毫秒

    poll() 文件描述符上限

    poll虽然不像select一样受到select() 中FD_SETSIZE 的限制,但是仍然受到ulimit中设定的一个进程所能打开的最大文件描述符的限制

    ulimit -n //结果65535
    

    poll()/select()的区别

    1. poll() 解决了select文件描述符最大只有1024的限制
    2. select和poll都需要自己不断轮询所有fd集合,直到设备就绪,(首先把所有的fd挂到对应的等待队列上,然后睡眠,在设备收到一条消息或者填写完文件数据之后,会唤醒设备等待队列上的进程,进程会再次扫描整个注册文件描述符的集合,并返回就绪文件描述符的数目给用户)期间可能要睡眠和唤醒多次交替(存疑),虽然epoll也需要唤醒,但是唤醒之后只需要检测就绪链表是否为空就行了。

    相关文章

      网友评论

          本文标题:Select_poll_epoll详解

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