zoukankan      html  css  js  c++  java
  • epoll函数与参数总结学习 & errno的线程安全

    select/poll被监视的文件描述符数目非常大时要O(n)效率很低;epoll与旧的 select 和 poll 系统调用完成操作所需 O(n) 不同, epoll能在O(1)时间内完成操作,所以性能相当高。

    epoll不用每次把注册的fd在用户态和内核态反复拷贝。

    epoll不同与之前的轮询方式,用了类似事件触发的方式,能够精确得获得实际需要操作的fd.

    今天看到一个说法是 epoll_wait 里面 maxevents 这个参数,不能大于epoll_create的size参数。而之前我的程序,epoll_wait用的都是1024,而epoll_create用的都是5. 看来以后epoll_create的参数要谢大一点了。

    但是实际上,epoll_create的参数不使用了。

     Since Linux 2.6.8, the size argument is unused.  (The kernel dynamically sizes
           the required data structures without needing this initial hint.)

    然后epoll_ctl很重要,我一般都是单独写一个wrapper函数,如下:

    void addfd(int epollfd, int fd, bool enable_et) {
      epoll_event event;
      event.data.fd = fd;
      event.events = EPOLLIN;
      if (enable_et) {
        event.events |= EPOLLET;
      }
      epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
      setnonblocking(fd);
    }

    上面epoll_ctl的第二个参数,可以有如下选择:

    EPOLL_CTL_ADD    //注册新的fd到epfd中;
    EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL    //从epfd中删除一个fd;

    第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct 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 */
    };

    上面,我一般都会把epoll_event.data里面的fd也写成正确的fd.

    epoll_event里面的events可以是下面的宏的集合:

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

    注意上面的EPOLLONESHOT,在读取完一整个事件之后,要重置EPOLLONESHOT让其他的线程能够接收到事件,通过如下方式来重置:

    void reset_oneshot(int epollfd, int fd) {
      epoll_event event;
      event.data.fd = fd;
      event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
      epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
    }

    注意以上的方式,是确知原来events内容的情况下;如果稳妥起见,最好把原来的events信息拿过来(更正:下面有讲到,其实对于EPOLLONESHOT,不是恢复事件,而是重新注册事件,所以也不一定要拿原来的events信息了)。

    当对方关闭连接(FIN), EPOLLERR,都可以认为是一种EPOLLIN事件,在read的时候分别有0,-1两个返回值。

    另注意:

    read返回0,不管阻塞还是非阻塞,一概是对方关闭连接;阻塞的话读不到数据不会返回,返回0说明对方关闭;非阻塞的话读不到数据会返回-1同时errno是EAGIN,返回0也说明对方关闭。

    read返回-1,对于阻塞,是有错误返回,需检查错误码处理;对于非阻塞,有可能是需要重试,也需要检查错误码,如果是EAGAIN,那么正常重试获取数据就可以了。

    ERRNO及线程安全性

    上面提到了errno,那么如果errno不是线程安全的,多个线程同时读取的时候,岂不是会出现大问题?还好!errno是线程安全的!

    从字面上看,errno是全局变量,但是实际上,errno其实是线程局部变量!这是GCC中保证的。他保证了线程之间的错误原因不会互相串改,当你在一个线程中串行执行一系列过程,那么得到的errno仍然是正确的。

    看下,bits/errno.h的定义:

    # ifndef __ASSEMBLER__
    /* Function to get address of global `errno' variable.  */
    extern int *__errno_location (void) __THROW __attribute__ ((__const__));
     #  if !defined _LIBC || defined _LIBC_REENTRANT
    /* When using threads, errno is a per-thread value.  */
    #   define errno (*__errno_location ())
    #  endif
    # endif /* !__ASSEMBLER__ */

    注意其中,飘红的那一句。是一个线程局部变量

    另外还有个errno.h中是这样定义的:

    /* Declare the `errno' variable, unless it's defined as a macro by
       bits/errno.h.  This is the case in GNU, where it is a per-thread
       variable.  This redeclaration using the macro still works, but it
       will be a function declaration without a prototype and may trigger
       a -Wstrict-prototypes warning.  */
    #ifndef errno
    extern int errno;
    #endif

    从上面可以看出,errno首先是在bits/errno.h中定义的,没定义的话,才会在errno.h中定义。而且errno实际上是一个整型指针(见bits/errno.h),并不是我们通常认为的是个整型数值,而是通过整型指针来获取值的。这个整型就是线程安全的。

    如果想看下编译选项里面有没有加上_LIBC_REENTRANT,可以用下面的代码:

    #include <stdio.h>
    #include <errno.h>
    
    int main() {
    
    #ifndef __ASSEMBLER__
            printf( "Undefine __ASSEMBLER__
    " );
    #else
            printf( "define __ASSEMBLER__
    " );
    #endif
    
    #ifndef __LIBC
            printf( "Undefine __LIBC
    " );
    #else
            printf( "define __LIBC
    " );
    #endif
    
    
    #ifndef _LIBC_REENTRANT
            printf( "Undefine _LIBC_REENTRANT
    " );
    #else
            printf( "define _LIBC_REENTRANT
    " );
    #endif
    
            return 0;
    }

    编译运行:

    $ g++ -o errno_demo errno_demo.cpp 
    
    $ ./errno_demo 
    

    Undefine __ASSEMBLER__
    Undefine __LIBC
    Undefine _LIBC_REENTRANT

    注意,__ASSEMBLER__没有定义,所以进入了bits/errno.h的代码块,然后__LIBC没有定义,errno就会用线程安全的定义,不需要再看_LIBC_REENTRANT是不是定义。也就是说默认的编译选项,errno就已经是线程安全的!!!安全的!!!

    errno的实现可以参考如下:

    static pthread_key_t key;
    static pthread_once_t key_once = PTHREAD_ONCE_INIT;
    static void make_key()
    {
        (void) pthread_key_create(&key, NULL);
    }
    int *_errno()
    {
        int *ptr ;
        (void) pthread_once(&key_once, make_key);
        if ((ptr = pthread_getspecific(key)) == NULL) 
        {
            ptr = malloc(sizeof(int));        
            (void) pthread_setspecific(key, ptr);
        }
        return ptr ;
    }

    其中有pthread_key_t 和 pthread_once_t。在另外的文章里面详细说吧。

    epoll_wait的原型是这样的:

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

    第四个参数timeout为0的时候表示不阻塞立即返回,为-1表示一直阻塞。

    返回值是等待处理的事件数量,如果是0可能是因为超时或者非阻塞。

    LT vs. ET

    EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):

    LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。

    ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。

    要注意的是,如果设置了EPOLL_ONESHOT模式,那么在每次获取一个fd上的事件之后,这个fd上的这个事件会被清除(主要是为了避免多个线程读数据时候相互干扰),直到读完数据需要手动地使用epoll_ctl的EPOLL_CTL_MOD再对这个fd加上这个event事件才行。

    EPOLL_ONESHOT的更多内容,可以参考我的另一篇文章:http://www.cnblogs.com/charlesblc/p/5538363.html

    从man手册中,得到ET和LT的具体描述如下
    EPOLL事件有两种模型:
    Edge Triggered(ET)       //高速工作方式,错误率比较大,只支持no_block socket (非阻塞socket)
    LevelTriggered(LT)       //缺省工作方式,即默认的工作方式,支持blocksocket和no_blocksocket,错误率比较小。

    注意,ET这种方式对于accept也是一样的,如果是listen的句柄,那么ET模式下收到事件,必须循环确保都处理完,因为多个accept同时发生也只会触发一次事件。

    EPOLLOUT

    另外,EPOLLOUT这种监听方式,平时不太用的到。在网上搜到如下的解释和用法,觉得很好

    对于LT 模式,如果注册了EPOLLOUT,只要该socket可写(发送缓冲区)未满,那么就会触发EPOLLOUT
    对于ET模式,如果注册了EPOLLOUT,只有当socket从不可写变为可写的时候,会触发EPOLLOUT

    如果需要,一种用法:自己在应用层加个发送缓冲区,需要发送数据的时候,如果应用层的发送缓冲区为空,则直接写到socket中。否则就写到应用层的发送缓冲区,并注册OUT时间(LT模式)

    反正我是没用过EPOLLOUT,直接写就行了,哈哈哈。

    负责listen的socket上同时注册EPOLLIN | EPOLLOUT,收到connet请求时,只看到EPOLLIN事件。
    
    在accectp后的socket上同时注册EPOLLIN | EPOLLOUT,这时候客户端还没有操作,这时只发生了EPOLLOUT事件。
    
    客户端send后,服务端收到了EPOLLIN事件,然后改为关注EPOLLOUT事件,立即就又收到了EPOLLOUT事件。
    
    跟上面的分析一致。另外从实验中发现貌似listen的fd只有EPOLLIN会生效

    EAGAIN 

    最后,还是要再说一下EAGAIN,仔细领悟下面这句话:

    /* If errno == EAGAIN, that means we have read all
    data. So go back to the main loop. */

    也就是说,对于ET模式循环读取数据的情况,如果read函数返回-1并且errno等于EAGAIN,是要跳出循环的,但是不需要close socket,因为不是真的有错误;其他的errno才是有错误,才需要关闭socket(为了兼容其他系统,有时候会把EWOULDBLOCK和EAGAIN放在一起处理,其实是等价的);只有read返回>0的时候,才需要继续在循环里面读取;read返回0表示对方关闭了,直接跳出循环,并且关闭socket.

    以上基本就是ET模式对于read函数返回几种情况的处理方式。对于LT模式,基本也是相同的处理,只不过不需要放在循环里读取,也就是说read函数返回>0的时候,不回到循环继续读取也是可以的,因为对于这种还有数据没有读完的情况,LT模式会再次触发EPOLLIN事件的。

  • 相关阅读:
    Android图片缩放方法
    网站建设底层知识Socket与Http解析
    802.11成帧封装实现(五)
    802.11成帧封装实现(四)
    802.11成帧封装实现(三)
    802.11成帧封装实现(二)
    802.11成帧封装实现(一)
    802.11n协议解析(二)
    802.11n协议解析(一)
    早期主流的wlan技术(二)
  • 原文地址:https://www.cnblogs.com/charlesblc/p/6202795.html
Copyright © 2011-2022 走看看