在开始讲网络IO模式之前,我们先来熟悉一下几个概念:
-
用户空间和内核空间
-
进程切换
-
文件描述符
-
buffer IO
用户空间和内核空间
在Linux中不管是内核空间还是用户空间都是使用虚拟地址,在32位平台而言,它的寻址范围是4GB,如下图所示:
所以从较低3GB(0x00000000到0xBFFFFFFF)称之为用户空间,较高1GB(0xC0000000到0xFFFFFFFF)称之为内核空间。在我的《Linux内存管理》文章有更加详细的说明,可自行前往参考。
进程切换
在Linux中存在多线程的情况下,肯定需要进程的切换,也就是说需要挂起正在运行的进程,然后运行正在就绪状态的进程。这种行为我们称之为进程切换。进程切换有哪些变化呢?
-
首先需要保存寄存器现场,保存处理机上下文
-
更新PCB信息
-
把进程的PCB加入到某些队列中(例如就绪队列)
-
选择需要运行的进程,并且执行,然后更新其PCB
-
更新内存管理的结构
-
恢复处理机上下文
虽然从这六个步骤中,无法感觉,但是真的挺繁琐的,非常消耗CPU资源。
文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
buffer IO
buffer IO也就是标准IO,是默认的IO模式。在用户态write或者read等IO操作时,内核中数据是存储在文件系统的页缓存中。它有个缺点:许多多次拷贝的消耗,带来CPU和内存的消耗。在本人之前的《Linux直接IO原理》中有详细比较两种IO的优缺点。
IO模型
先直接给出五种网络模型:
-
阻塞I/O
-
非阻塞I/O
-
I/O多路复用
-
信号驱动I/O
-
异步I/O
在Linux中,socket的I/O默认都是阻塞的,流程图,如下:
当应用程序调用recvfrom系统调用,内核进入第一个阶段:等待数据。所以在用户态这边整个进程都会阻塞。当内核准备数据之后,还需要将数据拷贝到用户态内存,然后才会return,之后用户进程才会接触阻塞状态。
非阻塞I/O
设置I/O属性可以修改socket为非阻塞模式,当进行读操作时候,流程图如下图:
当用户进程read操作时候,如果内核还没数据,就会返回无数据状态。在用户进程的角度上,如果是无数据,继续recvfrom等待。直到内核数据准备好,拷贝到用户空间内存中。所以非阻塞I/O需要用户进程一直轮询IO。
I/O多路复用(I/O multiplexing)
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。epoll的方式有所不一样,这里不展开。
从图中我们可以看出,当调用select,整个进程都会阻塞,select可以同时监听多个socket,一旦其中任意一个socket准备就绪状态,select就会立即返回,用户进程就可以read操作了。
上图和阻塞IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而阻塞 IO只调用了一个系统调用(recvfrom)。但是,select的真正优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing 模型中,实际中,对于所有的socket,一般都设置成为非阻塞的。但是,如上图所示,整个用户进程其实是一直被阻塞的。只不过进程是被select这个函数阻塞的,而不是被socket IO给阻塞。
异步I/O
Linux中异步I/O其实用的很少。下图流程:
用户进程发起read操作之后,这个时候就可以去干其他事情了。而从kernel的角度,当它受到一个异步 read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它read操作完成了。
总结
看下图各I/O模型比较图:
从图中我们可以发现,非阻塞和异步的区别还是很明显的,在非阻塞中进程大部分不会阻塞的,但是需要主动check I/O。而异步I/O就不需要,内核将数据拷贝完成后,发送信号通知进程就行了。
觉得不错,记得关注,转发,点赞!