Linux的网络IO模型
网络IO的本质是socket的读写,socket在Linux中被抽象为流,IO可以理解为对流的操作。
IO的分类和范畴
IO本身可以分为内存IO、网络IO和磁盘IO还有缓存IO等,一般讨论IO时更多是指后(网络IO和磁盘IO,因为这两个是最慢的哈哈),此处特别分析和说明网络IO。
操作处理的分类
阻塞/非阻塞
针对函数/方法的实现方式而言,即数据就绪之前是立刻返回还是等待,即发起IO请求后是否会阻塞。
阻塞IO机制
阻塞IO情况下,当用户调用read后,用户线程会被阻塞,等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后read才会返回。可以看到是阻塞的两个部分。
- CPU把数据从磁盘读到内核缓冲区。
- CPU把数据从内核缓冲区拷贝到用户缓冲区。
非阻塞IO机制
- 非阻塞IO发出read请求后发现数据没准备好,会继续往下执行,此时应用程序会不断轮询polling内核询问数据是否准备好,当数据没有准备好时,内核立即返回EWOULDBLOCK错误。
- 直到数据被拷贝到应用程序缓冲区,read请求才获取到结果。并且你要注意!这里最后一次 read 调用获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
同步/异步
IO读操作指数据流经:网络 -> 内核缓冲区 -> 用户内存
- 同步和异步的主要区别在于数据从内核缓冲区 -> 用户内存这个过程需不需要用户进程等待。
- 等待内核态准备数据结束之后,会自动回通知用户态的线程进行读取信息数据,此时之前用户态的线程不需要等待,可以去做其他操作。
对于一个网络IO,会涉及到两个系统对象,一个是调用这个IO的process(or thread)【用户态】,另一个就是系统内核(kernel)【内核态】
当一个用户态发生read操作发生时,它会经历两个阶段:
- 第一阶段:用户态线程等待内核态的数据准备 (Waiting for the data to be ready)。
- 第二阶段:用户态线程,将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
- 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
- 第二步:把数据从内核缓冲区复制到(用户态)应用进程缓冲区。网络应用处理的是两大类问题:网络IO、数据计算。前者给应用带来的性能瓶颈更大。
网络IO的模型大致有如下几种:
- 同步模型(synchronous IO)
- 阻塞IO模型(blocking IO)
- 非阻塞IO模型(non-blocking IO)
- 多路复用IO模型(multiplexing IO)
- 信号驱动IO模型(signal-driven IO)
- 异步IO(asynchronous IO)
阻塞IO模型(blocking IO)
在Linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程如下:
当用户进程调用了recvfrom这个系统调用,如上所述,会有两个阶段
准备数据:
- 很多时候数据在一开始还没有到达,这个时候kernel就要等待足够的数据到来。而用户进程会一直阻塞。
- 当kernel等到数据准备好了,它会将数据从kernel中拷贝到用户内存,然后kernel返回,用户进程结束block状态,重新运行。
Blocking IO的特点就是IO执行的两个阶段都是block了的。
非阻塞IO模型(non-blocking IO)[poll]
在Linux中,可以通过设置socket使其变为non-blocking,其流程如下:
- 当用户进程调用了recvfrom这个系统调用,如果kernel中的数据还没有准备好,那么用户进程不会block而是立刻返回一个error,即从用户的角度而言,不需要等待,马上得到一个结果。
- 从图中可以看出,用户进程在判断结果是一个error后,了解到数据还没有准备好,于是就不断重复上述操作直至kernel中的数据准备好,然后它马上将数据拷贝到了用户内存,然后返回。
多路复用IO模型(multiplexing IO)
select/epoll/evpoll,也被称作是Event-Driven IO。好处是单个process可以同时处理多个网络连接的IO。
- 基本原理可见下面的“IO复用技术”。也叫多路IO就绪通知。
- 这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知用户进程。
- 使得一个进程能在一连串的事件上等待。
- 这个流程和Blocking IO的流程其实并没有太多不同,事实上仅从图中看起来,由于需要进行两次系统调用,可能更差一些。但是,Select的优势在于它可以同时处理多个连接。
- 如果处理的连接数不是很高的话,使用“Select/Epoll 的 Web Server”不一定比使用“多线程 + BIO的Web Server”性能更好,反而延迟会更大。
Select/Epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO多路复用模型中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
异步IO(asynchronous IO)
用户进程发起read操作之后,立刻就可以开始去做其它的事。
- 从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。
- kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
比较
非阻塞和异步的区别
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。
- 在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
- asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
IO复用技术
在IO编程过程中,当需要处理多个请求时,可以使用多线程和IO复的方式进行处理。
IO复用是什么?
把多个IO的阻塞复用到一个select之类的阻塞上,从而使得系统在单线程的情况下同时支持处理多个请求。
IO复用常见的应用场景:
- 服务器需要同时处理多个处于监听状态和多个连接状态的套接字;
- 服务器需要处理多种网络协议的套接字
- IO复用的实现方式目前主要有select、poll和epoll/evpoll。
select和poll的原理基本相同:
- 注册待侦听的fd(这里的fd创建时最好使用非阻塞)
- 每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回
- 返回结果中包括已就绪和未就绪的fd
select和poll与epoll机制的比较
Linux网络编程过程中,相比于select/poll,epoll是有着更明显优势的一种选择。
- 支持一个进程打开的socket描述符不受限制(仅受限于操作系统的最大文件句柄数 unlimit)。
- Select的缺陷:一个进程所打开的FD受限,默认是2048;尽管数值可以更改,但同样可能导致网络效率下降;可以选择多进程的解决方案,但是进程的创建本身代价不小,而且进程间数据同步远比不上线程间同步的高效。
- epoll所支持的FD上限是最大可以打开文件的数目:/proc/sys/fs/file-max
IO效率可能随着文件描述符数目的增加而线性下降。
- epoll扫描系统的机制不同
- select/poll是线性扫描FD的集合;
- epoll是根据FD上面的回调函数实现的,活跃的socket会主动去调用该回调函数,其它socket则不会,相当于市是一个AIO,只不过推动力在OS内核。
- 使用mmap加速内核与用户空间的消息传递,zero-copy的一种。
- epoll的API更加简单。
- IO复用还有一个 水平触发 和 边缘触发 的概念:
- 水平触发:当就绪的fd未被用户进程处理后,下一次查询依旧会返回,这是select和poll的触发方式。
- 边缘触发:无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。
IO模型的总结
最后,再举几个不是很恰当的例子来说明这四个IO Model,有A,B,C,D四个人在钓鱼:
- A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;(同步阻塞)
- B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;(非阻塞)
- C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;(io多路复用机制)
- D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。(异步机制)