第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

写在前面

本文的大多数内容大多来自于UNIX网络编程这本书,我觉得要学好网络编程最好都看一下这本书,因为网络编程离不开Socket,而Socket起源于Unix。
Unix/Linux 基本哲学之一就是“一切皆文件,套接字的本质就是一种特殊的文件,不过这个特殊特殊在他的背后是一个网络连接。所谓写操作就是向远程主机发送数据,所谓读操作就是接收来自远方主机的信息。

本文概述

本文将通过案例介绍同步 异步 阻塞 非阻塞的概念

同时本文将介绍一下五种IO模型

1.阻塞式I/O
2.非阻塞式I/O
3.I/O复用(select 和 poll)
4.信号驱动式I/O
5.异步I/O

正文

我们先明确两点,一个输入操作通常包含两个不同的阶段

1.等待数据准备好
2.从内核向进程复制数据

对于一个套接字上的输入操作,也涉及两步。

  • 第一步通常涉及等待数据从网络中到达。当所等待的分组到达时,他被复制到内核中的某个缓冲区。
  • 第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

I/O概述

第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者
我们小文中的等待数据往往是指从设备空间到内核空间的过程。
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。

Kernel(操作系统内核)是指大多数操作系统的核心部分,它由操作系统中用于管理存储器、文件、外设和系统资源的那些部分组成。操作系统内核通常运行进程,并提供进程间的通信。Kernel的核心功能为:事件的调度和同步、进程间的通信(消息传递)、存储器管理、进程管理。在DOS操作系统,操作系统内核被认为是界于基本输入输出系统(BIOS)和应用软件之间的那部分(应用命令通过操作系统内核传递到BIOS,然后再传送到相关硬件)。

对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
所以,对于一个网络输入操作通常包括两个不同阶段:

  • 等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
  • 从内核缓冲区复制数据到进程空间。

阻塞式I/O模型

介绍:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

**典型应用:**阻塞socket,Java BIO

特点:

  • 进程阻塞挂起操作不消耗CPU资源,及时响应每个操作
  • 实现难度低,开发容易
  • 适用并发量小的网络开发

不适用于并发量大的应用:因为一个请求IO会阻塞一个进程,需要为每个请求分配一个进程(线程)以及时响应,系统开销大。

介绍:进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。在数据未复制到用户空间之前,进程一直处于阻塞状态

最流行的I/O模型式阻塞式I/O模型。默认情况下所有套接字都是阻塞的。以数据包套接字作为例子。我们有下图所示的场景。

第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单,要么整个数据报已经收到,要么还没有。然而对于TCP而言,诸如套接字低水位标记等额外变量开始起作用,导致整个概念变得复杂。

在本节的例子中,我们把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不论它如何实现(在源自Berkeley的内核上是作为系统调用,在System V内核上是作为调用系统调用getmsg的函数),一般都会从在应用进程空间中运行切换到在内核空间中运行,一段时间之后再切换回来。

在图6-1中,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断,如5.9节所述。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。

非阻塞式I/O 模型

**介绍:**进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

**典型应用:**socket非阻塞方式

特点:

  • 进程轮询(重复)调用,重复调用系统函数,消耗CPU资源
  • 实现难度低,开发应用相对阻塞 IO模式较难
  • 适用并发量小,且不需要及时响应的网络应用开发

**

进程把一个套接字设置为非阻塞是在通知内核,当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误,我们将在后面详细介绍非阻塞式I/O。通过一种轮询的操作来判断数据是否已经准备好。
第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

前三次调用recvfrom时没有数据可返回,因此内核转而立即返回-个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。我们接着处理数据。
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回-个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。我们接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询( polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。

I/O复用模型

介绍:

  • 多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;
  • 如果select监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;
  • 而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
  • 可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。

**典型应用:**select,poll。epoll三种方案,nginx都可以选择使用这三种方案,java     NIO

特点:

  • 专一进程解决多个进程IO的阻塞问题,性能好,Reactor模式
  • 实现,开发应用难度较大
  • 适用高并发服务应用开发,一个进程(线程)响应多个请求

select,poll,epoll:
Linux中IO复用的实现方式主要有select,poll,epoll
select:注册IO,阻塞扫描,监听的IO最大连接数不能多于FD_SIZE
Poll:原理与select相似,但IO数量大扫描线性性能下降
Epoll:事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Liunx2.6后不阻塞

有了IO复用(IO multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的IO系统调用上。图6-3概括展示了IO复用模型。

第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

比较图6-3和图6-1,IO复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,IO复用还稍有劣势。不过我们将在本章稍后看到,使用select的优势在于我们可以等待多个描述符就绪。

与IO复用密切相关的另一种IO模型是在多线程中使用阻塞式IO.这种模型与上述模型极为相似,但它没有使用select阻塞在多个文件描述符上,而是使用多个线程(每个文件描述符一个线程),这样每个线程都可以*地调用诸如recvfrom之类的阻塞式I/O系统调用了。

信号驱动型I/O模型

**介绍:**当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。

特点:
回调机制,实现,开发应用难度大

我们也可以用信号,让内核在描述符就绪时发送sGIo信号通知我们。我们称这种模型为信号驱动式IO ( signal-driven IO),图6-4是它的概要展示。

第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

我们首先开启套接字的信号驱动式IO功能(我们将在25.2节讲解这个过程),并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIo信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理(这正是我们将在25.3节中所要做的事情),也可以立即通知主循环,让它读取数据报。

无论如何处理sIGIo信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

异步I/O模型

**介绍:**当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

**典型应用:**JAVA7,AIO,高性能服务器应用

特点:

  • 不阻塞,数据一步到位;Proactor模式;
  • 需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性;
  • 实现、开发应用难度大;
  • 非常适合高性能高并发应用;

异步IO (asynchronous IO)由POSIX规范定义。演变成当前POSIX规范的各种早期标准所定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们I/O操作何时完成。图6-5给出了一个例子。

第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞

我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待IO完成期间,我们的进程不被阻塞。本例子中我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式IO模型。

本书编写至此的时候,支持POSIX异步IO模型的系统仍较罕见。我们不能确定这样的系统是否支持套接字上的这种模型。这儿我们只是用它作为–个与信号驱动式I/O模型相比照的例子。

各种I/O模型的比较

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

同步I/O和异步I/O模型对比

同步 I/O操作:导致请求进程阻塞,直到I/O操作完成

异步I/O操作:不导致进程阻塞
第3节 从UNIX底层出发一文带你领会 同步 异步 阻塞 非阻塞
从上面对比图片来说,阻塞IO模型是一个阻塞IO调用,而非阻塞IO模型是多个非阻塞IO调用+一个阻塞IO调用,因为多个IO检查会立即返回错误,不会阻塞进程。
而上面也说过了,非阻塞IO模型对于阻塞IO模型来说区别就是,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。

根据上述定义,我们的前4种模型-——阻塞式IO模型、非阻塞式IO模型、I/O复用模型和信号驱动式IO模型都是同步IO模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步IO模型与POSIX定义的异步IO相匹配。

同步异步拓展解释

同步IO:导致请求进程阻塞,直到I/O操作完成

异步IO:不导致IO进程阻塞,因为所有的IO操作都已经由内核完成

上面两个定义是《UNIX网络编程 卷1:套接字联网API》给出的。这不是很好理解,我们来扩展一下,先说说同步和异步,同步和异步关注的是双方的消息通信机制:

  • 同步:双方的动作是经过双方协调的,步调一致的。
  • 异步:双方并不需要协调,都可以随意进行各自的操作。

这里我们的双方是指,用户进程和IO设备;明确同步和异步之后,我们在上面网络输入操作例子的基础上,进行扩展定义:

  • 同步IO:用户进程发出IO调用,去获取IO设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到I/O操作完成。
  • 异步IO:用户进程发出IO调用,去获取IO设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。

附录

Linux 系统中一切都是文件并有相应的文件类型
在 Unix 和它衍生的比如 Linux 系统中,一切都可以看做文件。虽然它仅仅只是一个泛泛的概念,但这是事实。如果有不是文件的,那它一定是正运行的进程。
要理解这点,可以举个例子,您的根目录(/)的空间充斥着不同类型的 Linux 文件。当您创建一个文件或向系统传输一个文件时,它会在物理磁盘上占据的一些空间,而且是一个特定的格式(文件类型)。
虽然 Linux 系统中文件和目录没有什么不同,但目录还有一个重要的功能,那就是有结构性的分组存储其它文件,以方便查找访问。所有的硬件组件都表示为文件,系统使用这些文件来与硬件通信。
这些思想是对 Linux 中的各种事物的重要阐述,因此像文档、目录(Mac OS X 和 Windows 系统下称之为文件夹)、键盘、监视器、硬盘、可移动媒体设备、打印机、调制解调器、虚拟终端,还有进程间通信(IPC)和网络通信等输入/输出资源都是定义在文件系统空间下的字节流。
一切都可看作是文件,其最显著的好处是对于上面所列出的输入/输出资源,只需要相同的一套 Linux 工具、实用程序和 API。
虽然在 Linux 中一切都可看作是文件,但也有一些特殊的文件,比如套接字和命令管道

参考文章与数据

1.理解一下5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO:https://cloud.tencent.com/developer/article/1684951

2.<<UNIX网络编程卷1套接字联网API(第3版)>>

上一篇:java中文乱码unix,值得一看


下一篇:Linux概述+面试