epoll详解及坑

        epoll是select、poll 的改进版。

        使用select、poll的缺点:

        (1)调用select 时,需要将用户空间的所有fd集合拷贝进内核空间。

        (2)调用select 时,需要在内核空间遍历所有fd的状态。

        (3)select 支持的fd 数目有限,不超过1024。

 

        关于epoll的三个系统调用:epoll_create、epoll_ctl、epoll_wait:

epoll_create:

        #include  
        int epoll_create ( int size );

        功能:创建一个文件描述符作为“监听的一大堆fd”的标识。

        返回值:返回文件描述符epollfd。注意:使用完epoll后记得close(epollfd)。

        参数介绍:这里的size是早期设计时产生的,那时所有需监测的文件描述符放入hash表中,而现在时放入红黑树中,所以该参数现在并没有实际用到,但是必须 >0。

 

epoll_ctl:

        #include 
        int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

        功能:将需监听的fd添加到epfd对应的红黑树上,其中形参中指定:(1)对fd的操作类型:是删除还是增加。(2)监听fd的哪些事件:读事件还是写事件。(3)监听事件的方式:是水平触发还是边沿触发。

        返回值:成功返回0,不成功返回-1。

        参数介绍:

        epfd:epoll_create 的返回值。

        op:操作方式。有三种:(1)向事件表中注事件,EPOLL_CTL_ADD。(2)修改fd上事件,EPOLL_CTL_MOD。(3)删除fd上事件,EPOLL_CTL_DEL。

        fd:要操作的文件描述符。

        event:指定事件。介绍epoll_event:

        struct epoll_event

        {
            __unit32_t events;    // epoll事件
            epoll_data_t data;     // 用户数据
        };

        介绍epoll_event 的两个成员:
events:
        EPOLLIN :  表示监听对应文件描述符,读事件(包括对端SOCKET正常关闭);
        EPOLLOUT:表示监听对应文件描述符,写事件;
        EPOLLPRI:  表示监听对应文件描述符,紧急数据可读事件(这里应该表示有带外数据到来);
        EPOLLERR: 表示监听文件描述符,发生错误事件;
        EPOLLHUP:表示监听文件描述符,被挂断事件;
        EPOLLET:   将epoll设为边沿触发(Edge Triggered)模式,该参数缺省状态为水平触发LT。
        EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socketfd的话,需重新把这个socketfd加入到EPOLL队列里。
data:在实际应用中多只会用到fd。
        typedef union epoll_data
        {
            void* ptr;              //fd相关用户数据
            int fd;                   //事件从属的文件描述符
            uint32_t u32;
            uint64_t u64;
        } epoll_data_t;

        epoll的水平触发和边沿触发:

        (1)水平触发LT:接受缓冲区不为空,对应fd一直处于“读就绪”状态;发送缓冲区不满,对应fd一直处于“写就绪”状态关于这里的缓冲区:读多少就少多少,写多少就多多少。所以水平触发,只要数据没读完,那么在epoll_wait中,就一直认为该fd处于就绪状态。

        (2)边沿触发ET:当fd对应的读缓冲区从”有“变为了”无“时,对应fd才处于”读就绪“。当fd对应的写缓冲区从”无“变为了”有“时,对应fd才处于”写就绪“。注意:一定是在”有“和”无“之间变化才能触发。

        上述是自己的总结。想通过代码了解到两种方式不同的,推荐大牛博客:https://www.cnblogs.com/lojunren/p/3856290.html

        epoll中的event什么时候移除:不处于就绪状态的fd,就会被移除,根据 LT/ET 而不同。

epoll_wait:

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

        功能:发现并获得”就绪“状态的fd。

        返回值:成功,返回就绪文件描述符个数。失败返回-1,并设置errno(使用errno需#include )。

        参数介绍:

        (1) epfd:epoll_create创建的epollfd。

        (2) events:一个数组,若检测到某fd处于就绪状态,将事件从内核事件表复制到该数组。

        (3) maxevents:指定最多监听多少事件。

        (4) 坑一:timeout:epoll超时时间,单位毫秒。为-1表示阻塞,为0表示非阻塞,其他值表示超时时间。这里一定要注意,程序涉猎多线程和多进程时,这个timeout的设置初学时也容易出错。我们来说明以下几种设置方式:

1. 设置为-1,程序阻塞在此,后续任务没法执行。

2. 设置为0,程序能继续跑,但即使没事件时,程序也在空转,十分占用cpu时间片,我测试时每个进程都是60+%的cpu占用时间。

3. 综上,我们给出比较好的设置方法:将其设置为1,但还没完,因为即使这样设置,处理其它任务时,
在每次循环都会在这浪费1ms的阻塞时间,多次循环后性能损失就比较明显了。为了避免该现象,我们通常
向epoll再添加一个fd,我们有其它任务要执行时直接向该fd随便写入一个字节,将epoll唤醒从而跳过
阻塞时间。没任务时epoll超过阻塞时间1ms也会自动挂起,不会占用cpu,两全其美。

 

        坑二:在多进程中,epoll的创建和添加,最好放在同一进程中进行。本人就因此遇上了问题:

        坑的创建:父进程创建epoll,子进程有4步操作:(1) socket;(2) 设置reuseport避免“惊群”;(3) 将socket_fd添加进epoll;(4) 子进程通过epoll_wait等待然后accept;这样做是错误的,运行程序会报错:Resource temporarily unavailable。

        产生坑的原因:多个子进程socket产生的socket_fd是相同的数值,但表示不同的套接字(详见“网络编程”),所以实际上加入父进程epoll的,应该只是某一子进程的套接字,如果有客户端来connect,由于设置了reuseport,只有一个子进程来处理该事件,调用accept,至于能否accept成功取决于:添加入epoll的套接字是否是该子进程创建的套接字,那么只有 1/(进程数) 的概率accept 成功。

        综上述:epoll的三个系统调用最好在同一进程内使用,会方便很多。

        注:accept本身已经有效避免“惊群”了,引起“惊群”的是:epoll/select/poll等。

       

 

你可能感兴趣的:(UNIX高级编程,UNIX高级编程)