网络 IO 模型 | 计算机基础

为了解决网络 IO 中的问题,学者们提出了 4 种网络 IO 模型: 阻塞 IO 模型、非阻塞 IO 模型、多路 IO 复用模型和异步 IO 模型。

阻塞 IO 模型

大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用时(一般是 IO 接口)却不返回调用结果,并让当前线程一直处于阻塞状态只有当该系统调用获得结果或者超时出错时才返回结果。 实际上,除非特别指定,几乎所有的 IO 接口(包括 socket 接口)都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send() 的同时,线程处于阻塞状态,则在此期间,线程将无法执行任何运算或响应任何网络请求。

一个简单的改进方案是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户端提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,例如需要进行大规模或长时间的数据运算或文件访问,则推荐使用较为安全的进程。通常,使用 pthread_create() 创建新线程,使用 fork() 创建新进程。

现实生活所面临的可能是同时出现的上千甚至上万次的客户端请求, “线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。 总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞模型来尝试解决这个问题。

非阻塞 IO 模型

在 Linux 下,可以通过设置 socket 使 IO 变为非阻塞状态 。当对一个非阻塞的 socket 执行 read 操作时,流程如图所示。

可以看到服务器线程可以通过循环调用 recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐,因为循环调用 recv() 将大幅度占用 CPU 使用率;此外,在这个方案中 recv() 更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成”作用的接口,例如 select() 多路复用模式,可以一次检测多个连接是存活跃。

多路复用 IO 模型

多路 IO 复用, 有时也称为事件驱动 IO。 它的基本原理就是有个函数(如 select)会不断 地轮询所负责的所有 socket ,当某个 socket 有数据到达了, 就通知用户进程, 多路 IO 复用 模型的流程如图所示。

当用户进程调用了 select,那么整个进程会被阻塞,而同时内核会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。

这个模型和阻塞 IO 的模型其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而阻塞 IO 只调用了一个系统调用(recvfrom) 。 但是,用 select 的优势在于它可以同时处理多个连接。所以如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用多线程的阻塞 IO 的 web server 性能更好,可能延迟还更大;select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应,这里可以将这种模型归类为“事件驱动模型” 。

使用 select() 的事件驱动模型只用单线程(进程) 执行,一旦某个事件响应的执行体过于庞大,将直接导致后续事件的执行体迟迟得不到执行。

异步 IO 复用模型

用户进程发起 read 操作之后,立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个异步的 read 请求操作之后,首先会立刻返回,所以不会对用户进程产生任何阻塞。然后, 内核会等待数据准备完成,然后将数据拷贝到用户内存中,当这一切都完成之后,内核会给用户进程发送一个信号,返回 read 操作已完成的信息。

对比

实际上,真实的 IO 操作,就是例子中的 recvfrom 这个系统调用。非阻塞 IO 在执行 recvfrom 这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中数据准备好时,recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞。而异步 IO 则不一样,当进程发起 IO 操作之后,就直接返回, 直到内核发送一个信号,告诉进程 IO 已完成,则在这整个过程中,进程完全没有被阻塞。

在非阻塞 IO 中,虽然进程大部分时间都不会被阻塞,但是它仍然 要求进程去主动检查,并且当数据准备完成以后,也需要进程主动地再次调用 recvfrom 来将数据拷贝到用户内存中。而异步 IO 则完全不同,它就像是用户进程将整个 IO 操作交给了他人(内核)完成,然后内核做完后发信号通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动地拷贝数据。

参考

《后台开发:核心技术与应用实践》

网络 IO 模型 | 计算机基础

http://www.zh0ngtian.tech/posts/90bad40a.html

作者

zhongtian

发布于

2020-03-01

更新于

2023-12-16

许可协议

评论