从零开始探究网络IO模型

前言

对于一个应用程序即一个操作系统进程来说,它既有内核空间(与其他进程共享),也有用户空间(进程私有),它们都是处于虚拟地址空间中。用户进程是无法访问内核空间的,它只能访问用户空间,通过用户空间去内核空间复制数据,然后进行处理,于是在读写通信的过程中,产生了各种IO模型。
本文主要以unix中的网络IO为背景,介绍Unix下可用的5种I/O模型,大多内容参考了UNIX网络编程书籍及朝闻道博主。
以输入操作为例,一个输入操作通常包括两个不同的阶段:
(1) 等待数据准备好
(2) 从内核向进程复制数据
对于一个套接字的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

1. 阻塞式I/O模型

阻塞式I/O模型是最常见的I/O模型,默认情形下,所有套接字都是阻塞的,以数据报套接字作为例子,流程如下图
在这里插入图片描述
当用户进程进行了recvfrom的调用,内核开始IO第一个阶段,等待数据准备好,但是大多数情况下,数据不会很快全部到达,此时内核进入等待,而在用户进程这也将处于阻塞状态。当数据都到达以后,数据将从内核拷贝到用户内存,内核返回结果,用户进程阻塞消除继续运行。
由此可见在阻塞式I/O模型中,等待数据和复制数据两个过程,用户进程都处于阻塞状态。
显然阻塞式I/O模型有着很大的弊端,一个进程必须结束第一个任务的阻塞才能开始下一个任务,这会大大降低网络通信的速率。那么怎么解决这个问题呢?
一个最简单的办法就是使用多进程或多线程,多进程或多线程可以让每个套接字有自己独立的进程或线程,不受其他阻塞套接字的影响。
但这只能解决连接请求较少的情况,当有上万个请求同时到达时,多线程或多进程会大大占用系统资源,以至于服务器卡死。
此时线程池或连接池的概念被提出,“线程池”旨在线程池中保持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。但是池能解决的问题也是有上限的。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。

2. 非阻塞式I/O模型

还是以数据报套接字为例,流程如下图
在这里插入图片描述
当用户进程进行了recvfrom调用,内核查看数据状态,虽然数据报没准备好,进程等待片刻立即得到Error反馈,进程未阻塞。接着进程继续进行recvfrom调用,一旦数据报准备好,内核将数据拷贝到用户内存。
由此可见非阻塞式I/O模型是一种用户进程主动查询内核数据状态的过程。相比阻塞式I/O模型,当数据未准备好,用户进程不会阻塞,但是如果不断地查询内核数据状态,也会极大地占用CPU,而多路复用I/O模型可以解决此问题。

3. 多路复用I/O模型

有了I/O复用,我们就可以调用SELECT或POLL,阻塞在这两个系统调用中地某一个之上,而不是阻塞在真正的I/O系统调用上。下图展示了I/O复用模型

在这里插入图片描述
当用户进程调用了select,那么整个进程都会阻塞,同时内核会监视每一个套接字,当任何一个套接字中的数据准备好了,select就会返回,此时,用户进程进行recvfrom调用。
有些人会有疑问复用I/O模型与阻塞式I/O模型相比,不仅还存在着阻塞,而且多了一个系统调用,完全没有优势。当处理的连接数很少的时候,复用I/O相比阻塞I/O确实没有优势,但是当连接数很多时,优势就体现出来了。
在复用I/O模型中,每个socket一般是非阻塞的,但是用户进程是阻塞的,只不过进程阻塞的是Select这个函数,而不是阻塞的I/O,所以对系统影响很小。
相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。
但这个模型依旧有着很多问题。首先select()接口本身需要消耗大量时间去轮询各个连接。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。其中epoll目前看来效果组好。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口跨平台不容易实现。
还好现在有事件驱动库如libevent库,还有作为libevent替代者的libev库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal) 等技术以支持异步响应。
实际上,Linux内核从2.6开始,也引入了支持异步响应的IO操作,如aio_read, aio_write,这就是异步IO。

4. 异步I/O模型

异步I/O模型大致如下图:
在这里插入图片描述
用户进程发起read操作之后,立刻就可以开始去做其它的事。从kernel的角度,当它受到一个异步 read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

写在最后

到目前为止,已经将四个IO模型都介绍完了。总结一下阻塞和非阻塞的区别,同步I/O与异步I/O的区别。
阻塞IO会一直block住对应的进程直到操作完成,而非阻塞IO在内核还在准备数据的情况下会立刻返回。
同步 IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的阻塞IO,非阻塞 IO,复用IO都属于同步IO。有些人会有疑问非阻塞 IO并没有被阻塞啊,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个系统调用。非阻塞 IO在执行recvfrom这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中数据准备好的时候,recvfrom会将数据从内核拷贝到用户内存中,这个时候进程是被阻塞了,在这段时间内进程是被阻塞的。而异步IO则不一样,当进程发起IO操作之后,就直接返回了,直到内核发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被阻塞。

发布了6 篇原创文章 · 获赞 6 · 访问量 101

猜你喜欢

转载自blog.csdn.net/sdnyqfyqf/article/details/105563699
今日推荐