IO多路复用
在聊IO多路复用之前,先讲点基础知识
-
Socket
socket是一套网络编程规范,用于跨主机通信,服务端和客户端都要各自创建一个socket,后续读取和发送数据都是通过这个socket。在创建socket时,需要指定使用的协议,例如IPv4还是IPv6,tcp还是udp,以下所叙都是基于tcp的socket
-
一切皆文件的linux系统
在内核中,socket也是以文件的形式存在的,有对应的文件描述符。一般来说,进程会持有文件描述符,使用时通过文件描述符找到对应的文件,通过文件的inode找到内核中对应的socket结构。
-
C10K问题
所谓的c10k问题,指的是单机同时处理一万个请求。而怎么处理并发请求呢?最容易想到的是用多线程模型,再进一步就是多线程模型。
多进程模型:我们用一个进程去监听连接,一旦有连接创建成功,就fork()一个子线程去处理这个socket
多线程模型:每有一个连接创建,创建一个新的线程,将文件描述符传过去处理。
不管是多进程模型还是多线程模型,都无法解决c10k问题,服务端是无法提供1万个进程或线程的服务的!
接下来看看IO多路复用技术,c10k到达是怎么解决的:
1、 select
select函数实现IO多路复用的方法是,将已连接的socket都放在一个集合里,拷贝到内核,让内核遍历该集合,对需要读写的socket进行标记,标记后拷贝回用户态,用户态再遍历一遍集合找到那些被标记的socket进行处理。(这个过程发生了两次拷贝,两次遍历)
2、poll
poll的实现原理其实跟select一样,区别只在于select使用集合BitsMap是长度固定的,默认为1024,而poll使用了动态数组,也就是链表来实现。
3、epoll
epoll通过两个方面的升级,解决了select/poll的问题
- epoll在内核里使用了红黑树来跟踪需要检查的socket,用户态可以将需要监控的socket传进去,而不是传整个集合到内核,减少了拷贝和内存的开销。
- epoll在内核维护了一个链表来记录就绪事件,但某个socket有事件发生时,内核会将其加入到链表中。但用户态读取时,只需将该链表返回而不是整个socket集合。
- epoll支持两种事件触发模式,边缘触发和水平触发,边缘触发和水平触发的区别在于,在第一次通知之后,如果进程没有及时读取数据,水平触发会不断的通知,而边缘触发只会通知那一次。
了解了select/poll/epoll后,我们再来了解一下,它们的具体使用Reactor和Proactor
先来看看最简单的Reactor模式:
图中Reactor模式通过select监听socket,但有事件发生时,通过dispatch进行分发。
Acceptor处理的是连接的建立,并且创建对应的Handler来处理后续的响应
Handler处理的是具体的业务流程:read->业务处理->send
以上模式是单Reactor单进程模式,虽然实现简单,但是无法利用多核CPU的性能,也无法高并发的处理请求,只适用于业务处理非常快的场景。一般来说,C语言程序实现的就是该模式,Redis使用的就是这个模式
要解决单Reactor单进程模式的缺点,最容易想到的就是利用多线程来实现它:
我们很容易就想到,不要在handler里处理业务了,而是将数据传给业务线程,让业务线程去处理业务,这样就可以实现并发出来业务了。当然,使用了多线程就不可避免的要使用锁来保证一个线程安全问题了。
我们再来想想单Reactor多线程模式还有什么问题?如果一瞬间有大量的请求同时到达,单Reactor可能撑不住的
那解决的方法其实也很简单,单个不行,那就多个呗
于是多Reactor多线程模式就出现了,它其实跟单Reactor多线程模式差不多,只不过,在主Reactor只负责监听半连接的socket,一旦连接完成,就将其分配给子Reactor监听,而子Reactor不再处理连接创建而只关心业务的分发。Netty和Memcache都是使用的这个模式。
聊完了Reactor,再来讲讲所谓的Proactor
它两的区别就在于Reactor使用的是非阻塞同步网络模式,而Proactor使用的是异步网络模式
区别就在read和write上
非阻塞同步:非阻塞的意思就是,进程会询问内核数据是否准备完毕,如果未准备好,进程就回头先做其它任务, 待会再来问准备好没。阻塞的意思是,但内核数据准备好后,进程要等待内核进行数据拷贝到进程
异步:进程无需主动发起拷贝动作,但内核将数据准备好后,会自己进行数据拷贝,拷贝完成后再通知进程