epoll模型、边缘触发和条件触发记录

发布时间 2023-07-09 23:40:06作者: 苏显
  • 参考:https://blog.csdn.net/liu0808/article/details/52980413
  • epoll模型
    • 三大函数:epoll_create,epoll_wait, epoll_ctl ,是Linux独有的函数,因为它需要linux内核支持。
    • 头文件<sys/epoll.h>
    • epoll_create
      • int epoll_create(int size);
        • 成功时返回epoll文件描述符,失败时返回-1。
        • size:epoll实例的大小。
      • 该函数从2.3.2版本的开始加入的,2.6版开始引入内核。Linux最新的内核稳定版本已经到了5.8.14,长期支持版本到了5.4.70。从2.6.8内核开始的Linux,会忽略这个参数,但是必须要大于0。
    • epoll_ctl
        • 成功时返回0,失败时返回-1。
        • epfd --- epoll_create返回的文件描述符。
        • op --- 指定监视对象的操作,如添加、更改、删除等。
          • EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
        • fd --- 注册需要受监视的对象的文件描述符。
        • event 监视对象的事件类型。
            • EPOLLIN:表示对应的socket缓冲区有数据可读(当又收到了对端的一些数据,就会触发;或者作为服务端时有连接连过来)
            • EPOLLOUT:输出缓冲已为空,表示对应socket缓冲区可写(由于EPOLLLET边缘触发方式更加高效,所以一般都使用边缘触发方式)
            • EPOLLPRI:收到OOB数据的情况(优先级的区别,OOB应该是紧急事件)。
            • EPOLLRDHUP:断开连接或半关闭(有其中一边关闭)的情况,这在边缘触发方式下非常有用。
            • EPOLLHUP:表示文件描述符被挂起
            • EPOLLERR:发生错误。
            • EPOLLET:表示将epoll设置为边缘触发模式。
            • EPOLLONESHOT:设置为一次性事件。发生一次事件后,相应文件描述符不再收到事件通知。需要搭配epoll_ctl函数的二参使用
            • EPOLLCTL_MOD:再次设置事件。
          • data字段表示用户数据,它的类型是一个union,可以存放一个指针或文件描述符等数据。它的定义如下:

            • 其中,ptr可以指向任何类型的用户数据,fd表示文件描述符,u32和u64分别表示一个32位和64位的无符号整数。使用时,用户可以将自己需要的数据存放到这个字段中,当事件触发时,epoll系统调用会返回这个数据,以便用户处理事件。
      • ps:要监视谁,就把谁epoll_ctl处理一下。

    • epoll_wait
      • int epoll_wait(int epfd, struct epoll_event*events,int maxevents,int timeout);
        • 成功时返回发生事件的文件描述符的数目,失败时返回-1。
        • epfd 表示事件发生监视范围的epol例程的文件描述符
        • events 保存发生事件的文件描述符集合的结构体地址值。
        • maxevents 第二个参数中可以保存的最大事件数目。
        • Timeout:以毫秒为单位的等待时间,传递-1时,一直等待直到发生事件。比select的timeout精度低,因此select一般也被用为高精度的定时器。
  • 边缘触发和条件触发,还有IO的阻塞/非阻塞模式(https://blog.csdn.net/liu0808/article/details/52980413
    • Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会每次都通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,则它们每次都会返回,这样就会大大降低你检索自己关心的就绪文件描述符的效率!!!
    • Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
    • 阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。当你去写一个阻塞的文件描述符时,如果在该文件描述符上没有空间(通常是缓冲区满了)可写,那么它会一直阻塞,直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据,还包括接收连接accept(),发起连接connect()等操作...
    • 非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动!!!
    • 几种IO模型的触发方式:
      • select(),poll()模型都是水平触发模式;
      • 信号驱动IO是边缘触发模式;
      • epoll()模型既支持水平触发,也支持边缘触发,默认是水平触发。
      • 1> 水平触发的非阻塞sockSrv
        • 因为水平触发在缓冲区中有可读数据时会在每次epoll_wait提示sockSrv去读,因此都会accept成功。
      • 2> 边缘触发的非阻塞sockSrv
        • 因为边缘触发,在高并发的情况下sockSrv来不及及时处理accept,可能在等待的这段时间内发来了多个可读通知,实际上这些通知被冲抵了,等sockSrv腾出手来处理时它实际上只接收到一个(最后一个),因此它也只会做一次accept,因此会错过一些遗留在缓冲区的信息。当然它可以用循环的方式accept,把缓冲区里的东西读干净再忙别的,但这在代码上无意增加了工作量。
      • 3> 水平触发的阻塞sockCli
        • 单次读取(√):每次只读自己buf缓冲区大小的数据,但因为是水平触发epoll_wait每次都会提醒去读,所以不会落下数据。只有缓冲区有数据时epoll_wait才会提醒sockCli去读,因此也不会阻塞在recv。
        • 循环读取(×):如果是阻塞状态,在不知道要读多少数据时不要用循环读取,因为我们不知道何时该停止,如果没数据可读,它就会阻塞在recv,直到有数据可读。如果这个时候,用另一个客户端去连接,服务器不能受理这个新的客户端!!!
        • 对于写(×),只要输出缓冲区还有空间,水平触发会不断提醒你去写,这很烦;如果写的时候输出缓冲区满了,阻塞的sockCli就会使它阻塞在send那等着空间被腾出来。
      • 4> 水平触发的非阻塞sockCli
        • 单次读取(√):同3的单次读取。
        • 循环读取(乄):非阻塞状态,没数据了就会返回,因此对水平触发的非阻塞sockCli,单次、循环读取都ok。
        • 对于写(乄),只要输出缓冲区还有空间,水平触发会不断提醒你去写,这很烦,你会在就绪描述符里看到很多你不想要的执行写操作的描述符,影响搜索效率,但它没错;还好它不会在写满输出缓冲区时阻塞在send那。
      • 5> 边缘触发的阻塞sockCli
        • 单次读取(×):每次只读自己buf缓冲区大小的数据,就算输入缓冲区还有数据也不管,直到epoll_wait下次发来读信号。有遗留数据,所以会干扰下一个事件。
        • 循环读取(×):同3的循环读取。
        • 对于写(×),如果写的时候输出缓冲区满了,阻塞的sockCli就会使它阻塞在send那等着空间被腾出来。
      • 6> 边缘触发的非阻塞sockCli
        • 单次读取(×):同5的单次读取
        • 循环读取(√):因为不会阻塞,可以通过循环读取输入缓冲区的所有数据,挺好地完成任务。同4的循环读取。
        • 对于写(√),它只会提醒你写一次,且输出缓冲区也不会阻塞在那,非常好,就是要注意用循环写把你这次要写的内容写完。
    • 总结
      • 1.对于监听的sock,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
      • 2.对于读写的sock,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。其次水平触发模式下不要用循环读取数据,本身输入缓冲区有数据水平触发就会叫个不停,所以别给自己找无谓的麻烦。
      • 3.对于读写的sock,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据(也就是循环读写)。
  •