- 参考: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实例的大小。
- 成功时返回epoll文件描述符,失败时返回-1。
- 该函数从2.3.2版本的开始加入的,2.6版开始引入内核。Linux最新的内核稳定版本已经到了5.8.14,长期支持版本到了5.4.70。从2.6.8内核开始的Linux,会忽略这个参数,但是必须要大于0。
- int epoll_create(int size);
- epoll_ctl
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
- 成功时返回0,失败时返回-1。
- epfd --- epoll_create返回的文件描述符。
- op --- 指定监视对象的操作,如添加、更改、删除等。
- EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
- EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL
- fd --- 注册需要受监视的对象的文件描述符。
- event 监视对象的事件类型。
- 其中events字段表示要监听的事件类型,可以是以下值之一:
- EPOLLIN:表示对应的socket缓冲区有数据可读(当又收到了对端的一些数据,就会触发;或者作为服务端时有连接连过来)
- EPOLLOUT:输出缓冲已为空,表示对应socket缓冲区可写(由于EPOLLLET边缘触发方式更加高效,所以一般都使用边缘触发方式)
- EPOLLPRI:收到OOB数据的情况(优先级的区别,OOB应该是紧急事件)。
- EPOLLRDHUP:断开连接或半关闭(有其中一边关闭)的情况,这在边缘触发方式下非常有用。
- EPOLLHUP:表示文件描述符被挂起
- EPOLLERR:发生错误。
- EPOLLET:表示将epoll设置为边缘触发模式。
- EPOLLONESHOT:设置为一次性事件。发生一次事件后,相应文件描述符不再收到事件通知。需要搭配epoll_ctl函数的二参使用
- EPOLLCTL_MOD:再次设置事件。
- EPOLLIN:表示对应的socket缓冲区有数据可读(当又收到了对端的一些数据,就会触发;或者作为服务端时有连接连过来)
- data字段表示用户数据,它的类型是一个union,可以存放一个指针或文件描述符等数据。它的定义如下:
- 其中,ptr可以指向任何类型的用户数据,fd表示文件描述符,u32和u64分别表示一个32位和64位的无符号整数。使用时,用户可以将自己需要的数据存放到这个字段中,当事件触发时,epoll系统调用会返回这个数据,以便用户处理事件。
- 其中,ptr可以指向任何类型的用户数据,fd表示文件描述符,u32和u64分别表示一个32位和64位的无符号整数。使用时,用户可以将自己需要的数据存放到这个字段中,当事件触发时,epoll系统调用会返回这个数据,以便用户处理事件。
- 其中events字段表示要监听的事件类型,可以是以下值之一:
- 成功时返回0,失败时返回-1。
- ps:要监视谁,就把谁epoll_ctl处理一下。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
- 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一般也被用为高精度的定时器。
- 成功时返回发生事件的文件描述符的数目,失败时返回-1。
- int epoll_wait(int epfd, struct epoll_event*events,int maxevents,int timeout);
- 三大函数:epoll_create,epoll_wait, epoll_ctl ,是Linux独有的函数,因为它需要linux内核支持。
- 边缘触发和条件触发,还有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()模型既支持水平触发,也支持边缘触发,默认是水平触发。
- select(),poll()模型都是水平触发模式;
- 通过实际例子来看看epoll的水平出发和边缘触发,以及阻塞IO和非阻塞IO对它们的影响。下面假设水平触发为LT,边缘触发为ET;服务端用于监听的socket文件描述符为sockSrv,accept返回的用于读写客户端的文件描述符为sockCli。(下面不涉及阻塞的sockSrv,因为epoll_wait()返回必定是已就绪的连接,设不设置阻塞accept()都会立即返回。存在特例:设置阻塞的监听sock,当客户端发起连接请求时服务端繁忙没顾上accept,等回头accept时客户端自己已经断开了,此时当服务器到达accept时会发生阻塞。很好理解,客户端发来了消息然后自己毙了,但是服务端并不知道客户端毙了,所以会卡在accept)
- 1> 水平触发的非阻塞sockSrv
- 因为水平触发在缓冲区中有可读数据时会在每次epoll_wait提示sockSrv去读,因此都会accept成功。
- 因为水平触发在缓冲区中有可读数据时会在每次epoll_wait提示sockSrv去读,因此都会accept成功。
- 2> 边缘触发的非阻塞sockSrv
- 因为边缘触发,在高并发的情况下sockSrv来不及及时处理accept,可能在等待的这段时间内发来了多个可读通知,实际上这些通知被冲抵了,等sockSrv腾出手来处理时它实际上只接收到一个(最后一个),因此它也只会做一次accept,因此会错过一些遗留在缓冲区的信息。当然它可以用循环的方式accept,把缓冲区里的东西读干净再忙别的,但这在代码上无意增加了工作量。
- 因为边缘触发,在高并发的情况下sockSrv来不及及时处理accept,可能在等待的这段时间内发来了多个可读通知,实际上这些通知被冲抵了,等sockSrv腾出手来处理时它实际上只接收到一个(最后一个),因此它也只会做一次accept,因此会错过一些遗留在缓冲区的信息。当然它可以用循环的方式accept,把缓冲区里的东西读干净再忙别的,但这在代码上无意增加了工作量。
- 3> 水平触发的阻塞sockCli
- 单次读取(√):每次只读自己buf缓冲区大小的数据,但因为是水平触发epoll_wait每次都会提醒去读,所以不会落下数据。只有缓冲区有数据时epoll_wait才会提醒sockCli去读,因此也不会阻塞在recv。
- 循环读取(×):如果是阻塞状态,在不知道要读多少数据时不要用循环读取,因为我们不知道何时该停止,如果没数据可读,它就会阻塞在recv,直到有数据可读。如果这个时候,用另一个客户端去连接,服务器不能受理这个新的客户端!!!
- 对于写(×),只要输出缓冲区还有空间,水平触发会不断提醒你去写,这很烦;如果写的时候输出缓冲区满了,阻塞的sockCli就会使它阻塞在send那等着空间被腾出来。
- 单次读取(√):每次只读自己buf缓冲区大小的数据,但因为是水平触发epoll_wait每次都会提醒去读,所以不会落下数据。只有缓冲区有数据时epoll_wait才会提醒sockCli去读,因此也不会阻塞在recv。
- 4> 水平触发的非阻塞sockCli
- 单次读取(√):同3的单次读取。
- 循环读取(乄):非阻塞状态,没数据了就会返回,因此对水平触发的非阻塞sockCli,单次、循环读取都ok。
- 对于写(乄),只要输出缓冲区还有空间,水平触发会不断提醒你去写,这很烦,你会在就绪描述符里看到很多你不想要的执行写操作的描述符,影响搜索效率,但它没错;还好它不会在写满输出缓冲区时阻塞在send那。
- 单次读取(√):同3的单次读取。
- 5> 边缘触发的阻塞sockCli
- 单次读取(×):每次只读自己buf缓冲区大小的数据,就算输入缓冲区还有数据也不管,直到epoll_wait下次发来读信号。有遗留数据,所以会干扰下一个事件。
- 循环读取(×):同3的循环读取。
- 对于写(×),如果写的时候输出缓冲区满了,阻塞的sockCli就会使它阻塞在send那等着空间被腾出来。
- 单次读取(×):每次只读自己buf缓冲区大小的数据,就算输入缓冲区还有数据也不管,直到epoll_wait下次发来读信号。有遗留数据,所以会干扰下一个事件。
- 6> 边缘触发的非阻塞sockCli
- 单次读取(×):同5的单次读取
- 循环读取(√):因为不会阻塞,可以通过循环读取输入缓冲区的所有数据,挺好地完成任务。同4的循环读取。
- 对于写(√),它只会提醒你写一次,且输出缓冲区也不会阻塞在那,非常好,就是要注意用循环写把你这次要写的内容写完。
- 单次读取(×):同5的单次读取
- 1> 水平触发的非阻塞sockSrv
- 总结
- 1.对于监听的sock,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
- 2.对于读写的sock,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。其次水平触发模式下不要用循环读取数据,本身输入缓冲区有数据水平触发就会叫个不停,所以别给自己找无谓的麻烦。
- 3.对于读写的sock,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据(也就是循环读写)。
- 1.对于监听的sock,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
- Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会每次都通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,则它们每次都会返回,这样就会大大降低你检索自己关心的就绪文件描述符的效率!!!