IO多路转接二
一、poll函数:IO多路复用的方式之一
1、接口:
第一个参数为一个结构体指针,也可以理解为一个结构体数组,fds表示结构体的起始位置;第二个参数为描述结构体的元素个数;第三个参数为一个超时时间。
结构体里面的内容:
2、基于poll实现一个监控标准输入就绪的代码:将输入的内容打印出来,等待的过程交给poll来等待,read直接进行读写。
poll与select的对比:
a、select每次使用前都得重新设置监控描述符,poll也得设置
b、poll每次调用都要将内容拷贝到内核之中,拷贝的频繁程度相同,拷贝的数据量更大啦!poll一次拷贝一个结构体,开销反而更大了。
c、select每次调用时都得遍历文件描述符表,了解哪个文件描述符就绪……poll也得遍历,但是遍历的不是位图而是数组
d、poll唯一解决了select中的上限问题,select文件描述符上限是有限制的,对于poll,只要数组足够大,就可以放更多的文件描述符。poll把输入与输出用两个字段分开,使用时,更方便。
二、epoll(windows上没有epoll,Linux上才有)【相当于一个改进版本的poll】【高效的处理大量的文件】
1、epoll的系统调用(epoll有三个函数)
(1)创建一个epoll的句柄【文件描述符】:.
参数中的size没有任何意义,可以传任何值,主要是为了保证向前兼容
(2)epoll的事件注册函数:把文件描述符添加到epoll中,或者把某个文件描述符从epoll中删除
第一个参数epfd:对哪个epoll对象进行操作
第二个参数int op存在三个取值
第三个参数int fd
对哪个文件描述符进行修改
第四个参数struct epoll_event *event 结构体指针
epoll_data_t中://根据用户的需要决定放指针;文件描述符;uint32还是uint64
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered) 来说的. EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的 话, 需要再次把这个socket加入到EPOLL队列里.
(3)epoll_wait等待(注册的动作和等待的动作分开,不同于poll和select):会一直等待直到某些文件描述符关闭
第一个参数:对哪个epoll对象进行等待
第二个参数:epoll_event这样的结构体指针,可以理解为一个数组,表示数组的首地址
第三个 参数:数组的元素个数
第四个参数:超时的时间,若为0则为非阻塞,立即返回;若为-1,则会永久阻塞
2、epoll在内核中的表现形式(工作原理):创建一个epoll之后,每次调用一个Add就是把epoll加到集合之中。这个集合是一颗红黑树。通过一棵红黑树描述文件描述符。创建epoll相当于创建一个结构体,该结构体(a、红黑树:管理所有文件描述符的集合,包括每个文件描述符包含的事件的状态 b、就绪队列:一旦有某些文件描述符就绪,就会通过一个回调函数,把我们对应的节点的文件描述符放到就绪队列里。)就绪队列用来放置所有已经就绪的文件描述符,有了就绪队列后,上层代码里调用epoll_wait直接返回的结果,相当于直接把就绪队列取出来、返回。通过就绪队列可以快速知道哪些文件描述符是就绪的。回调函数是指先把函数注册到系统中,在适当的时机,由系统自动调用,这个函数什么时候调用由系统或框架自动调用,不是程序员干涉得了的。epoll函数内部存在内置的回调函数机制,只要发现某个红黑树的文件描述符就绪,就会自动触发回调函数,进而把文件描述符,以及相关信息放到就绪队列。如果没有回调函数,无法得知谁就绪,就得遍历,遍历麻烦。文件描述符就绪,先把就绪信息给网卡,网卡驱动告诉内核数据进入,内核根据数据调用对应节点的回调函数,从而把内容放到红黑树里面。若是没有回调函数,反复遍历这颗树,由于树比较大,整体的效率就会比较低。
实现epoll版本的TCP服务器:1、调用epoll_create创建一个epoll句柄; 2、调用epoll_ctl, 将要监控的文件描述符进行注册; 3、调用epoll_wait, 等待文件描述符就绪;
进程中默认打开了3个文件描述符,默认为0,1,2;创建一个listen_sock文件描述符为3,client却显示为5。4是epoll_create创建的,epoll_create创建的句柄也是文件描述符,也回占据文件描述符表的一个位置,所以后续客户端连上之后,都是从5开始排序的。发送一个字符串,服务器端可以返回一个字符串。重新打开一个客户端,文件描述符会增加(client后跟的数字),显示建立连接,关闭客户端,显示断开连接。
epoll的优缺点(与select横向对比):
a、文件描述符数目无上限: 通过epoll_ctl()来注册一个文件描述符, 内核中使用红黑树的数据结构来管 理所有需要监控的文件描述符.select中由于文件大小的限制,最多只能有固定数目的文件描述符,epoll底层是一颗红黑树,没加一个文件描述符即加一个节点,红黑树的大小取决于内存的大小,没有明显的上限。
b、基于事件的就绪通知方式: 一旦被监听的某个文件描述符就绪, 内核会采用类似于callback的回调机制, 迅速激活这个文件描述符. 这样随着文件描述符数量的增加, 也不会影响判定就绪的性能;【回调函数和文件描述符的大小没有关系】
c、维护就绪队列: 当文件描述符就绪, 就会被放到内核中的一个就绪队列中. 这样调用epoll_wait获取就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1)【避免遍历,与上述b配合】
d、从接口使用得角度讲:不必每次都重新设置要监控得文件描述符,使接口使用更方便,也能够避免频繁的用户态和内核态之间大量拷贝文件描述符结构。【有三个函数:有客户端建立链接就添加,有客户端断开连接就减少。就不用每次调用一次epoll_wait就得重新设置一次文件描述符,避免频繁进行数据拷贝和设置。select函数由于每次调用都得设置一次文件描述符,导致代码不方便、频繁得进行用户态和内核态得拷贝。】
5、判断下面这句话说的对么:epoll中使用了内存映射机制。
内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销 。这种说法是不正确的。a、用户态看到的结果可能定义在栈上、堆上……是一个什么样的内存是不确定的。b、拷贝过程是不可避免的,需要把内核中的数据拷贝到用户空间内存中。
6、epoll的工作方式:
(1)水平触发LT(Level Triggered)【默认】
客户端给服务器端发送数据,服务器端按照epoll的方式处理数据,假设客户端给服务器端发送10k数据,由于服务器端采用epoll的方式进行处理,epoll_wait返回,并且返回文件描述符读就绪,然后从文件描述符中读数据,此时只读取1k数据,还剩下9k数据还在缓冲区中。对于水平触发模式,如果只读取了1k数据,再次进入epoll_wait的循环,epoll_wait会立刻返回,并且返回的信息是未读完的文件描述符的就绪状态。epool,select,poll的默认情况下都是LT,水平触发。
(2)边缘触发ET(Edge Triggered)
再次进入epoll_wait的时候,此时epoll_wait就不会返回了。【一次没有把数据读完,下次就不能再读了】。ET更严格,要求程序员每次在ET返回的时候都要所有的数据都一次性处理完。按照LT的方式,代码简单。ET也有效率更高的优点,因为一次处理完毕的效率更高。(高速模式:不要分多次处理数据,一次处理完)
使用ET模式的epoll最好将文件描述符设置为非阻塞,可以在一定程度上避免出错。例如:假设服务器收到一个10k的请求,会向客户端返回一个应答数据,如果客户端收不到应答,不会发送第二个10k请求:
6、epoll的使用模式:
epoll的高性能只是相对于select的。
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll。 只有少数的几个连接(阻塞式IO或者异步思想更合适), 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型.
7、epoll中的惊群问题:
浙公网安备 33010602011771号