一 IO多路复用
1.1 IO多路复用*
1.1.1 IO多路复用
I/O: 网络I/O;
多路:多个客户端端连接(连接就是套接字描述符,即socket或channel),指的是多条TCP连接。
复用:用一个进程来处理多条的连接,使用单进程就能够实现同时处理多个客户端的连接。
Redis利用epoll函数来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分给事件处理器。
在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。
如下图:
1.1.2 IO多路复用流程(了解)
Redis服务采用Reactor的方式实现文件事件处理器。
所谓I/O多路复用机制,就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作。这种机制的使用需要select、poll、epoll函数来配合。多个连接共同用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需要阻塞等待所有连接,当某条连接有新的数据可以处理时,操作系统通知应用程序、线程,从阻塞状态返回,开始进行业务处理。
如下图:
1.2 同步&异步和阻塞&非阻塞
1.2.1 同步&异步
同步:调用者要一直等待调用结果的通知后,才能进行后续的执行;
异步:被调用方先返回应答,让调用者先回去,然后再计算调用结果,计算完成最终结果后再通知并返回给调用方。
重点在于:同步&异步讨论对象是被调用者(服务提供者),重点在于获得服务提供者给予提供消息的通知方式上。
1.2.2 阻塞&非阻塞
阻塞:调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干。
非阻塞:调用在发出去后,调用方向去忙别的事情,不会阻塞当前进/线程,而会立即返回。
重点在于:阻塞、非阻塞的讨论对象是调用者(服务请求者);重点强调等消息时候的行为,调用者是否能干其他事情。
1.2.3 场景案例
场景说明:
1)同步阻塞:服务员说快到你了,先别离开,我后台看一眼马上通知你。客户在海底捞火锅前台干等着,啥都不干。
2)同步非阻塞:服务员说快到你了,先别离开,客户在海底捞火锅前台边刷抖音等着叫号。
3)异步阻塞:服务员说还要在等等,你先去逛逛,一会通知你,客户怕过号,在海底捞火锅前台拿着排号小票,啥都不干,等着电源通知。
4)异步非阻塞:服务员说还要再等等,你先去逛逛,一会通知你,拿着排号小票+刷着抖音,等着电源通知。
二 NIO
2.1 NIO的特性
在NIO模式中,一切都是非阻塞的。
accept()方法是非阻塞的,如果没有客户端连接,就返回无连接标识。
read()方法是非阻塞的,如果read()方法读取不到数据或返回空闲中标识,如果读取到数据时,只阻塞read()方法读取数据的时间。
在NIO模式中,只有一个线程;
当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间听一次。看这个socket的read()方法能否读取到数据,这样一个线程就能处理多个客户端的连接和读取了。
三 IO多路复用例子
3.1 IO多路复用例子
模拟一个tcp服务端处理30个客户socket,一个监考老师考试多个学生:
第1种选择:按顺序逐个验收;如ABCD;
第2种选择:每一个用户对应一个线程/进程进行处理。
第3种选择:采用IO多路复用,在使用select、poll函数、epoll函数时阻塞,收发客户消息是不会阻塞的。如老师站在讲台上,这时C,D举手,表示他们答题完毕,老师依次检查C,D的答案。然后老师回讲台上进行等,然后去处理E和A.....
3.2 Reactor模式*
Reactor模式有2个关键组成:
1.reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。例如公司接线员。
2.Handlers:处理程序执行I/O事件要完成的实际事件。例如公司实际干活的人。
四 多路复用底层涉及的函数
4.1 select函数
4.1.1 select函数特点
Select函数其实就是把NIO中用户态要遍历的fd数组拷贝到内核态;让内核态来遍历,因为用户态判断socket是否有数据,最后还是要调用内核态的。拷贝到内核态,不用一直在用户态和内核态频繁切换了。
从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需要进行简短的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率。
Select方式,既做到了一个线程处理多个客户端连接,又减少了系统调用的开销(多个文件描述符只有一次select的系统调用+N此就绪状态的文件描述符的read系统调用)
4.1.2 select函数缺点
1.bitmap最大1024位,一个进程最多只能处理1024个客户端
2.&rset不可重用,每次socket有数据就相应的位会被置位
3.文件描述符被拷贝到内核态,仍然又开销,select调用需要传入fd数组,需要拷贝一份到内核,高并发场景下,这种拷贝方式也是耗费资源惊人的。
4.select并没有通知用户态哪一个socket有数据,仍然需要0(n)的遍历,select仅仅返回可读文件描述符的个数,具体那个可读还是要用户自己遍历。
4.2 poll函数
4.2.1 poll函数流程
Poll的执行流程:
1.将5个fd从用户态拷贝到内核态。
2.poll为阻塞的方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
3.poll方法返回
4.循环遍历,查找哪个fd被置位为POLLIN了
5.将revents重置为0,便于复用
6.对置位的fd进行读取和处理
4.2.2 poll函数缺点
解决问题:
1.解决了bitmap的大小限制
2.解决了reset不可重用的情况
Poll函数解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题。
1.pollfds数组拷贝到了内核态,仍然有开销。
2.poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。
4.3 epoll函数
在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。
4.4 三个函数的区别
select |
poll |
epoll |
|
操作方式 |
遍历 |
遍历 |
遍历 |
数据结构 |
Bitmap |
数组 |
红黑树 |
最大连接数 |
1024 |
无上限 |
无上限 |
最大支持文件描述符号 |
一般有最大值限制 |
65535 |
65535 |
Fd拷贝 |
每次调用select,都需要把fd集合从用户态拷贝到内核态 |
每次调用poll,都需要把fd集合从用户态拷贝到内核态 |
Fd首次调用epoll_ctl拷贝,每次调用epoll_wait不靠边 |
时间复杂度 |
O(n) |
o(n) |
o(1) |