zoukankan      html  css  js  c++  java
  • epoll-1

    前言

    提到网络高并发,windows下避不开的是iocp,linux下是epoll,freebsd下是kqueue。iocp与epoll的区别是,windows一如既往的把所有的东西都做好,iocp通知的时候,数据已经放到了我们提供的buffer中;而linux则是一如既往的给你最大自由,epoll通知的时候,是告诉你数据到了,需要程序自己处理接收;kqueue接触不多,不做解释了。

    iocp相关的文章在Windows网络编程专栏

    参考

    https://www.man7.org/linux/man-pages/man7/epoll.7.html

    介绍

    epoll提供了与poll类似的功能,就是监视多个网络文件描述符,看看哪一个需要操作。epoll提供了ET边界触发和LT水平触发两种模式。对于有着大量监视描述符的场景,性能表现良好。

    epoll核心的内容是epoll instance。这个是内核态的结构体,可以让我们从用户态访问,里面包含两个列表:

    • interest list(epoll set) 这个set里面保存的是注册到监视中的interest。
    • ready list 已经准备好的描述符列表。ready list是interest list中的一个subset,它是由内核动态创建的。当一个文件描述符(也就是socket)被激活,也就是有数据到达或是出现异常等,这个描述符就会被加入到ready list。

    相关api

    • epoll_create 创建一个epoll instance并且返回对应的文件描述符
    • epoll_create1 epoll_create的扩展api
    • epoll_ctl 在epoll中添加修改或是删除一个描述符
    • epoll_wait 等待I/0事件。如果没有事件,会阻塞。可以看做是从ready list中获取一个元素

    Level-triggered Edge-triggered

    当有io事件触发时,epoll提供两种处理方式,一种是level-triggered(水平触发),另一种是edge-triggered(边界触发)。我们用一个示例情况来说明一下这两个模式的区别:

    1. 有一个文件描述符用来接收数据
    2. 有2k的数据到达
    3. epoll_wait 会返回这个文件描述符,表明它有数据要处理
    4. 读取1k的数据
    5. 再次调用epoll_wait

    如果使用EPOLLET(edge_triggered),那么在第5步的时候会阻塞,虽然我们还有1k数据没有读取,并且发送数据的远端,同样会在等待这次数据发送接收完成的响应。因为ET模式,仅仅在状态发生变化的时候触发,所以第5步只会在下次有新的数据到达才会返回。在上面的示例中,第3步已经响应了2k数据到达的事件,虽然没有处理完成,但是这个事件已经触发了,然后就被删除掉了,因为没有新的事件到达,所以第5步会阻塞。

    使用ET模式的时候,建议使用非阻塞的文件描述符,因为事件到达通知是有时效的,如果是阻塞的,在处理多个文件描述符的时候,可能会导致在等待的事件饿死,也就是超时之后丢弃掉,不再通知。

    其次,在调用read和write的时候,需要不断循环,直到返回EAGAIN,然后再调用 epoll_wait 去等待新的事件。

    当使用LT模式的时候(默认情况下epoll就是这个模式),epoll相当于效率更快的poll。只要有内容还没操作完,就会提示,所以再次执行第5步的时候,还会返回。

    在多线程的环境下,如果一个文件描述符收到一个事件唤醒一个线程,在这个线程还没处理的时候又来了一个事件,又唤起一个线程,这时就会出现多个线程操作同一块数据,因为虽然唤起了两个线程,但是数据都没有操作,同时接收数据的时候,会出现莫名其妙的问题。就算是使用ET这种模式,也是会发生的。我们可以设置EPOLLONESHOT flag,告诉epoll,当收到一个事件的时候,就把这个文件描述符与epoll断开关联,这样就不会有多个事件同时触发。当我们操作完成后,在使用 epoll_ctl 把文件描述符设置为EPOLL_CTL_MOD,让它可以继续触发新的事件。

    如果程序是多线程或是多进程(通过fork创建),当调用epoll_wait等待同一个文件描述符的时候,在ET模式下,只会有一个线程或是进程会被唤醒,在某些情况下可以避免惊群现象。

    惊群

    惊群现象,是指,有多个线程同时等待同一个文件描述符,这个时候来了一个事件,结果所有的线程都被唤醒,而只有一个才能成功,其他的返回失败。浪费了资源,增加了处理难度。

    惊群现象,在epoll中并没完全解决,所以需要在开发中额外注意。在最新的版本上,说是解决了,这个还需要确定。但是很多linux的内核版本是远远落后于最新的内核版本的,尤其是做server的linux,所以还是需要额外的手段处理。

    autosleep

    如果系统通过/sys/power/autosleep设置了autosleep模式,当一个事件触发时,会把设备从sleep唤醒,设备的驱动会一直保持唤醒状态,直到这个事件被放入了队列。如果需要设备在事件处理之前一直处于唤醒阶段,那么就要通过epoll_ctl设置EPOLLWAKEUP标志。

    当通过结构体epoll_event,在事件中设置了EPOLLWAKEUP标志,系统会一直保持唤醒,就算事件被放入了队列。从epoll_wait调用返回事件到下次epoll_wait调用这期间,也会一直保持唤醒。如果事件想让系统在除了上面的时间内也保持唤醒,那么应该在第二次调用epoll_wait之前设置wake_lock。

    /proc interfaces

    /proc/sys/fs/epoll/max_user_watche(since Linux 2.6.28)配置可以限制epoll使用内核内存的总量

    这个配置限制了通过epoll向系统注册的文件描述符的最大数量。这个限制是针对每个真实用户的ID。每个注册的文件描述符需要大约90 bytes的空间在32位系统上,大约160 bytes的空间在64位系统上。默认情况下max_user_watches值是让epoll最大占用内存的1/25(4%),比如默认是n,其中n*90(32位系统)或n*160(64位系统)小于等于实际内存的4%。

    示例

    epoll在使用LT模式的时候,与poll基本相似。如果使用ET模式,需要格外注意不要在事件循环中出现阻塞的情况,也就是占用时间比较久的操作。这个例子中监听是一个非阻塞的socket。do_use_fd()函数使用了一个新的已经准备好的文件描述符,里面会调用read或是write,不断循环直到EAGAIN返回。一个事件驱动的程序在收到EAGAIN返回的时候,需要记录当前的状态,也就是接收的数据有多少,记录到哪里了,这样在下次调用read或write的时候就能把数据拼接起来继续操作。

               #define MAX_EVENTS 10
               struct epoll_event ev, events[MAX_EVENTS];
               int listen_sock, conn_sock, nfds, epollfd;
    
               /* Code to set up listening socket, 'listen_sock',
                  (socket(), bind(), listen()) omitted */
    
               epollfd = epoll_create1(0);
               if (epollfd == -1) {
                   perror("epoll_create1");
                   exit(EXIT_FAILURE);
               }
    
               ev.events = EPOLLIN;
               ev.data.fd = listen_sock;
               if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
                   perror("epoll_ctl: listen_sock");
                   exit(EXIT_FAILURE);
               }
    
               for (;;) {
                   nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
                   if (nfds == -1) {
                       perror("epoll_wait");
                       exit(EXIT_FAILURE);
                   }
    
                   for (n = 0; n < nfds; ++n) {
                       if (events[n].data.fd == listen_sock) {
                           conn_sock = accept(listen_sock,
                                              (struct sockaddr *) &addr, &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 {
                           do_use_fd(events[n].data.fd);
                       }
                   }
               }

    如果使用ET模式,为了性能,最好在添加文件描述符到epoll中时设置(EPOLLIN|EPOLLOUT)标志。这样可以避免不断的调用epoll_ctl让文件描述符在EPOLLIN和EPOLLOUT之间切换。

    QA

    0.怎么样才能确定一个文件描述符已经注册到epoll了

    可以通过记录文件描述符组合的数量和已经打开的文件描述符来判断。

    1.如果重复注册同一个文件描述符到epoll中,会有什么问题

    当第二次注册文件描述符的时候,会收到EEXIST的错误。但是如果添加一个通过 dup dup2 或是 fcntl F_DUPFD 复制的文件描述符时,是没有问题的。添加一个复制的文件描述符,可以实现事件的过滤功能。比如指向同一个文件的不同文件描述符,设置不同的事件标识,那么不同的事件到来时,就可以被相应的文件描述符捕获,而做一些特定的工作。

    2.可以两个epoll等待同一个文件描述符吗?如果可以,事件可以通知到两个epoll吗?

    可以两个epoll监听同一个文件描述符,事件也会通知到两个epoll。但是需要特别注意,保证处理没有问题。

    3.epoll的文件描述符就是 poll/epoll/selectable吗

    是的,当一个epoll的文件描述符有事件等待的时候,就表示epoll已经是准备状态了

    4.把epoll的文件描述符加入到自己的set中会怎么样

    epoll_ctl会报错,错误代码是EINVAL。但是你可以把一个epoll的文件描述符添加到另一个epoll set中

    5.可以把一个epoll的文件描述符通过UNIX的域 socket发送到另一个进程吗

    可以,但是这样做没意义。因为接收epoll文件描述符的进程并没有拷贝interest list

    6.如果关闭一个文件描述符,系统会把它从所有的epoll的interest list中删除吗

    会的。但是需要注意几点。一个文件描述符只是一个打开文件的引用。可以通过 dup  dup2  fcntl F_DUPFD 或是fork创建一个新的文件描述符。只有所有的文件描述符都关闭后,这个文件才真正的关闭。

    只有所有的指向系统底层真正文件的文件描述符都关闭了,被加入到epoll中的指向这个文件的文件描述符才会被从interest list中删除。也就是,即使在interest list中的一个文件描述符已经关闭了,另一个指向同一个文件的文件描述符,如果处在interest list中,同样还会收到事件通知。为了避免出现这个问题,在它被复制之前,必须通过 epoll_ctl EPOLL_CTL_DEL 把文件描述符从interest list中删除。不然的话,就要保证所有的文件描述符都被关闭了,这样操作更加困难,因为有时候文件描述符可能被dup或是fork隐式的复制了。

    这里所说的问题是什么呢?就是我不想监听一个socket的消息了,如何把这个socket从epoll中移除。如果这个socket有多个文件描述符(通过 dup dup2  fork 等创建的)被加入到epoll的监听列表中。关闭其中一个文件描述符,仅仅是关闭了一个引用,其他的指向这个socket的文件描述符还会收到消息。关闭一个文件描述符并不能保证把这个socket关闭了。除非所有指向这个socket的文件描述符都被关闭了。

    7.如果在两个epoll_wait中间到达了多个事件,这些数据事件是合并到一起的还是分开回调的

    是合并到一起,统一一次回调的

    8.对于一个文件描述符操作,会不会影响,已经被接收但是还没有通知的事件

    对于已经存在的文件描述符,我们有两种操作:删除-在这种情况下没有意义;修改-会触发I/O重新可读的事件。

    感觉上这里表达的就是这种情况,不用在意,系统会处理好

    9.如果使用ET模式,是不是必须要不断的读或是写,直到收到EAGAIN

    从epoll_wait收到事件,表示这个文件描述符的本次IO请求已经准备好了。你必须认为在下一个(非阻塞的)读/写触发EAGAIN之前都是已经准备好了的。至于你怎么用,完全取决于你。

    对于发送的是包或是token类型的文件(比如,数据报socket,常规模式下的终端),只有一种方式能确定IO空间内的数据是否读/写结束,那就是一直操作到EAGAIN。

    对于流类型的文件(比如,管道,FIFO,流socket),可以通过IO空间被耗尽的时候,来判断这次读写是否结束。比如,调用read读数据,如果这次调用返回获取的数据少于我们给定的buff空间,就表示这次读取完成了。对于调用write写数据也一样。(如果不能确定你监听的文件描述符是不是流式的,那么最好避免使用第二种方式)

    这里的意思也就是,如果我可以读写的时候,是不是必须要每次都不断操作,直到返回EAGAIN事件,也就是所有的数据都操作完了。上面说可以这样做,如果是一个个包类型的socket,也必须这样操作。如果是流类型的,可能数据会不断的过来,也可以通过这次这次读写的数据量少于我们指定的buff大小时结束,也就是当前缓冲区中可能数据读完了或是不多,我们也可以结束。不过最后提了一句,最好避免使用第二种,也就是根据读写获取的数据量来判断是否需要结束,因为我们有时候不清楚socket是流式的还是一个个包类型的。

    陷阱和解决方法

    饿死(ET模式)

    如果IO的缓冲中有很多数据要处理,当我们处理的时候,另一个文件的时间可能得不到处理而饿死(这个问题不是epoll专有的)

    解决方法就是保存准备好的list标识准备好的文件描述符,把他们保存在一个结构体中。程序就可以知道哪些文件需要处理,一直在这些准备好的文件中循环,甚至可以忽略接下来已经在我们记录列表中的文件触发的事件,直到这个文件描述符处理完成,然后把它从这个列表中删除。

    使用事件缓存

    如果使用了缓存或是保存了所有 epoll_wait 返回的文件描述符,那么就要确保提供了标识文件描述符已经关闭的方法(比如被前一个事件处理导致关闭)。假设有100个事件,处理47事件的时候导致第13个事件关闭了,如果我们缓存中还保留第13个事件,下次循环到第13个事件的时候就会继续处理,这样就导致出现了问题。

    一个解决方法就是在处理47事件的时候,调用 epoll_ctl(EPOLL_CTL_DEL) 把第13个事件删除,并且调用close关闭。然后标识对应的结构体已经被删除了,把它放到清除列表中。如果发现缓存中有来自第13个文件描述符的其他事件,我们需要删除它,避免歧义。

    提示

    在epoll中监视的文件描述符,可以通过epoll文件描述符的条目查看,在/proc/[pid]/fdinfo目录中,参考proc的详细功能。

    kcmp KCMP_EPOLL_TFD可以检测一个文件描述符是否在一个epoll中

  • 相关阅读:
    servlet-servletConfig
    servlet-servletContext网站计数器
    servlet-cookie
    Android 无cp命令 mv引起cross-device link
    android使用mount挂载/system/app为读写权限,删除或替换系统应用
    android使用百度地图、定位SDK实现地图和定位功能!(最新、可用+吐槽)
    解决android sdk manager无法下载SDK 的问题
    Android APK反编译详解(附图)
    Android如何防止apk程序被反编译
    不用外部JAR包,自己实现JSP文件上传!
  • 原文地址:https://www.cnblogs.com/studywithallofyou/p/12971209.html
Copyright © 2011-2022 走看看