Linux网络I/O模型

随着微服务和云原生时代的到来,网络通信成为了我们日常开发都要面对的问题。不管是服务间通信、网关、负载均衡还是种类繁多的分布式基础设施,都需要进行网络通信,虽然这些组件的封装很大程度的减轻了我们的认知负荷,让我们不需要了解太多网络底层的知识,就可以很方便的实现我们的功能。但是我们仍然有理由要去弄清楚这些底层知识,因为如果你想成为一名架构师,在日常技术选型中,这些知识能帮助我们快速准确地做出决策。

今天,我们就来总结下Linux网络I/O模型。

《UNIX网络编程》中,将网络I/O模型分为了五种:

  • 阻塞式I/O(blocking I/O)
  • 非阻塞式I/O(nonblocking I/O)
  • I/O多路复用(I/O multiplexing)
  • 信号驱动I/O(signal driven I/O)
  • 异步I/O(asynchronous I/O)

下面我们来对每种模型进行讲解。

在这之前,我们首先要明白,对于一次I/O访问(以read举例),通常包含两个阶段:

(1)等待数据准备好;

(2)将数据从内核拷贝到进程中。

第一步通常涉及等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。

第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

阻塞式I/O

日常我们接触到的最常用的I/O模型就是阻塞式I/O(bloking I/O)模型。一个典型的读操作流程大概是这样:

当用户进程调用了 recvfrom 这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于网络I/O来说,很多时候数据在一开始还没有到达,比如,还没有一个完整的UDP包。这个时候kernel就要等待足够的数据到来。这个过程中,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个等待过程的,从用户进程的角度看,这个过程是被阻塞住了。

当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户进程,然后kernel返回结果,用户进程才解除block状态,继续往下运行。

从图中也可以看出,阻塞I/O的特点就是在I/O执行的两个阶段都被block了

非阻塞I/O

通常,也可以通过设置socket使其变为非阻塞的。当对一个非阻塞socket执行读操作时,流程大概是这样的:

当用户进程调用了 recvfrom 这个系统调用,如果kernel发现没有数据可返回,它并不会阻塞住用户进程,而是立即返回一个 EWOULDBLOCK 错误。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就能得到一个结果。

当用户进程判断结果是 EWOULDBLOCK 时,它就知道数据还没有准备好,于是它可以再次发起 read 操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的 recvfrom 系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。

当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom 时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这样的做法涉及到多次的用户态和内核态的互相切换,往往会耗费大量的CPU时间,所以目前并不常用。

从图中可以看出,非阻塞I/O的第一阶段(数据准备)是非阻塞的,但第二阶段(数据从内核缓冲区复制到用户进程缓冲区)是阻塞的

I/O多路复用

I/O多路复用就是我们常说的 selectpollepoll,有些地方也称为事件驱动I/O(Event Driven I/O)。selectepoll 的好处就在于单个线程就可以同时处理多个网络连接的I/O。它的基本原理就是 selectpollepoll 这个function 会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。流程大概是这样的:

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

这个图和阻塞I/O的图其实没有太大的差别,事实上,从图中看感觉效率还更差些。这是因为这里需要使用两个系统调用(selectrecvfrom),而阻塞I/O只有一个系统调用(recvfrom)。但是,select 的优势在于它可以同时处理多个连接。

所以,如果处理的连接数不是很高的话,使用多路复用I/O模型不一定比使用多线程+阻塞I/O模型性能更好,可能延迟还更大,因为多路复用I/O的优势并不是对单个连接能处理得快,而是在于能处理更多的连接。多路复用I/O是目前高并发网络应用的主流

从图中可以看出,多路复用I/O本质上是阻塞I/O的一种,在I/O执行的两个阶段都被block了

信号驱动I/O

我们也可以用信号,让kernel在描述符就绪时发送 SIGIO 信号通知我们。我们称这种模型为信号驱动式I/O(signal-driven I/O)。流程大概是这样的:

我们首先开启socket的信号驱动式I/O功能,并通过 sigaction 系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据准备好读取时,kernel就会为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 recvfrom 读取数据,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据。

从图中可以看出,信号驱动I/O的第一阶段(数据准备)是非阻塞的,但第二阶段(数据从内核缓冲区复制到用户进程缓冲区)是阻塞的

信号驱动I/O相比前三种I/O模型,实现了等待数据就绪时,进程不被阻塞,主循环可以继续工作,所以性能更佳。但对于TCP来说,信号驱动I/O模型几乎没有被使用,这是因为 SIGIO 信号是一种 Unix 信号,信号没有附加信息,如果一个信号源有多种产生信号的原因,信号接受者就无法确定究竟发生了什么。而TCP socket生产的信号事件有七种之多,这样应用程序收到 SIGIO,根本无从区分处理。

异步I/O

信号驱动I/O虽然在等待数据就绪时,没有阻塞进程,但在被通知后进行的I/O操作还是阻塞的,进程会等待数据从内核空间复制到用户空间中。而异步I/O模型则是实现了真正的非阻塞I/O。

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

所以,异步 I/O 受限于操作系统,Windows NT 内核早在 3.5 以后,就通过IOCP实现了真正的异步 I/O 模型。而 Linux 系统下,是在 Linux Kernel 2.6 才首次引入,目前也还并不完善,因此在 Linux 下实现高并发网络编程时,仍然是以多路复用 I/O 模型模式为主。

从图中可以看出,异步I/O模型在I/O执行的两个阶段都是非阻塞的

I/O模型总结

对比上面5种不同的I/O模型,可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到用户进程缓冲区期间,进程阻塞于 recvfrom 调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。

  • 同步I/O操作(synchronous I/O opetation)导致请求进程阻塞,直到I/O操作完成;
  • 异步I/O操作(asynchronous I/O opetation)不导致请求进程阻塞。

前4中模型——阻塞式I/O模型、非阻塞式I/O模型、I/O多路复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型完全不会阻塞。

欢迎关注我的公众号 【架构小菜

posted @ 2021-07-30 23:03  晋好林  阅读(88)  评论(0编辑  收藏  举报
作者:jinhaolin
出处:http://www.cnblogs.com/jinhaolin/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件咨询.