type
status
date
slug
summary
tags
category
icon
password
1、网络编程基本概念
在学习网络 IO 模型之前,先学习网络编程的一些基本概念:
- 同步、异步、阻塞和非阻塞
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 IO
1.1 同步、异步、阻塞和非阻塞
这四个概念是编程中处理I/O操作和多任务的核心概念,假设在一次调用里面存在
调用者
、被调用者
和调用结果
这三个概念- 同步(Synchronous):
调用者
发起请求后,必须等待调用结果
返回才能继续执行后续代码。
- 异步(Asynchronous):
调用方
发起请求后,无需等待调用结果
,后续通过回调、事件或通知机制在完成后获取调用结果
。
- 阻塞(Blocking):
调用者
在操作完成前,线程被挂起(无法执行其他任务)。
- 非阻塞(Non-blocking):
调用者
发起请求后,线程立即返回,无需等待操作完成。
举个例子
对于主线程上执行的一系列代码块,当其中的某个代码块需要与相关线程交互,而调用了一个函数时:
- 如果立即去执行此函数,这称为同步。
- 如果没有去执行此函数,而是将执行此函数的时机安排在未来的某个时间,然后马上继续执行刚才的代码块,这称为异步。
- 当执行此函数时,直至获得完整的资源之前,都暂停执行当前的代码块,这称为阻塞。
- 当执行此函数时,立即获得瞬时的结果,然后马上继续执行当前的代码块。如果获得的瞬时资源不是完整的资源,之后周期性发送类似的请求,直至获得完整的资源,这称为非阻塞。
同步和异步的区别
同步和异步是从
调用结果通知机制
这个角度来区分的。- 同步:调用发生后,
调用者
主动去获取调用结果
,这就是同步调用。
- 异步:调用发生后,
调用者
被动等待被调用者
通知调用结果
,这就是异步调用。通常过程是,调用发生后,调用者
收到一个来自被调用者
的回执,这个回执内容通常是是被调用者
告诉调用者
我已经收到这个任务了,但是这个任务具体什么时候执行,什么时候能得到执行结果,调用者
不需要管。等到执行完成后,被调用者
主动把调用结果
通知给调用者
,整个调用过程才算完成。
阻塞和非阻塞的区别
阻塞和非阻塞是从
调用者在获取到调用结果前的行为
这个角度进行区分的。- 阻塞:
调用者
在获取到调用结果
前什么都不干,线程整个挂起一直等待调用结果
,这就是阻塞调用。
- 非阻塞:
调用者
在获取调用结果
前可以做自己的事情,这就是非阻塞调用。
综上所述,同步和异步、阻塞和非阻塞是两套完全不同而且并不矛盾的概念,所以产生以下四种概念组合场景:
- 同步阻塞:
调用者
一直挂起什么都不做,主动去获取调用结果
。这是最常见的场景,例如默认的readFile
。
- 同步非阻塞:
调用者
拿到回执后继续做自己的事情,然后通过轮询机制,每隔一段时间去询问调用结果
,直到最终获取调用结果
,调用才算完成。例如 Linux 的epoll
或 Java NIO,通过轮询检查状态。
异步阻塞:调用者
一直挂起什么都不做,直到被调用者
通知调用结果
。这种场景罕见且通常无意义。
- 异步非阻塞:调用者拿到回执后继续做自己的事情,等到
被调用者
通知调用结果
后,调用才算完成。例如 Node.js 的fs.readFile
非阻塞模式,通过事件循环处理完成通知。
1.2 用户空间与内核空间
为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)两部分
- 用户空间:运行普通应用程序的内存区域,权限受限。特点:
- 无法直接访问硬件或内核内存,只能通过系统调用(System Calls)请求内核服务。
- 每个进程有独立的虚拟地址空间,互相隔离。
- 支持多用户、多任务环境。
- 占用内存里面较低的字节,例如32位系统里面占用较低的 3G 字节(从虚拟地址0x00000000到0xBFFFFFFF)。
- 内核空间:运行操作系统内核的内存区域,拥有最高权限。特点:
- 可直接访问硬件、内存管理、进程调度等核心功能。
- 所有进程共享同一内核空间。
- 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)
用户空间与内核空间通过以下方式进行交互:
- 系统调用(System Calls):用户程序通过API(如
open()
、write()
)请求内核服务。
- 中断和异常:硬件中断或程序异常触发内核处理。
- 共享内存:部分场景下通过映射(如
mmap
)共享数据,但需谨慎同步。
1.3 进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
1.4 进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。
当进程进入阻塞状态,是不占用CPU资源的
。1.5 文件描述符fd
文件描述符(File descriptor)是操作系统中用于访问文件或I/O资源的一个抽象概念,是一个非负整数,用于唯一标识一个打开的文件或 I/O 通道。
在Unix/Linux系统中,每个进程都有一个文件描述符表,由内核维护,fd 就是该表中的索引。fd 的值通常从 0 开始,最大值受系统限制(可通过
ulimit -n
查看)。例如常用的标准描述符:- 0: 标准输入 (stdin)
- 1: 标准输出 (stdout)
- 2: 标准错误 (stderr)
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。
1.6 Linux IO 的过程
大多数文件系统的默认 IO 操作都是缓存 IO(标准 IO)。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,
数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
。对于本地 read IO,分为以下两个阶段:
- 第一阶段:等待数据准备 (Waiting for the data to be ready)。
- 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于网络 IO,
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作
。对于socket流而言,- 第一阶段:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
- 第二阶段:把数据从内核缓冲区复制到应用进程缓冲区。
2、网络IO模型
根据以上内容,我们可以得到这样的网络 IO 过程:
阻塞/非阻塞
概念发生在 网络数据—>内核空间
和 内核空间—>拷贝到用户空间
这两个阶段。- 如果应用程序什么事都不做直到本阶段完成就称作
阻塞
- 如果应用程序在阶段完成前可以做自己的事情就称作
非阻塞
。
同步/异步
概念发生在 应用程序怎么收到结果
这个阶段。- 如果应用程序主动去获取结果就称作
同步
- 如果应用程序被动等待内核通知结果就称作
异步
。
这些概念是 IO 模型的基础。我们常见的有五种 IO 模型:
- 同步阻塞 IO(blocking IO)
- 同步非阻塞 IO(nonblocking IO)
- IO 多路复用( IO multiplexing)
- 异步非阻塞 IO(asynchronous IO)
- 信号驱动式IO(signal-driven IO)
2.1 同步阻塞 IO(blocking IO)
网络模型
同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在这个 IO 模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据。
在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络 IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。流程图如下:

流程描述
- 第一个阶段:准备数据。当用户进程调用了
recv()/recvfrom()
这个系统调用之后,用户进程就开始阻塞等待数据。对于网络 IO 来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的 UDP 包。这个时候 kernel 就要等待足够的数据到来。这个过程也需要等待。也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。
- 第二个阶段:当kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存。然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。
分析
- 特点:在 IO 执行的两个阶段都被阻塞。
- 优点
- 能够及时返回数据,无延迟。
- 对内核开发者来说这是省事了。
- 缺点
- 性能差,不能支持高并发的应用场景。
2.2 同步非阻塞 IO(nonblocking IO)
网络模型
同步非阻塞就是「每隔一会儿瞄一眼进度条」的轮询方式。在这种模型中,非阻塞也会调用
recvform
系统调用,不管数据是否准备好,内核都会马上返回给进程。如果数据还没准备好,就会返回一个错误代码(EAGAIN 或 EWOULDBLOCK),说明这个命令不能立即满足。进程在返回之后,可以干点别的事情,过一段时间再发起recvform
系统调用,重复上面的过程。这个过程通常被称之为轮询。直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。流程如图所示:
流程描述
- 用户进程发出 read 操作时,马上就得到了一个结果。
- kernel中的数据还没有准备好,返回一个 error 信号。
- 用户进程接收到一个 error 信号时,就知道数据还没有准备好,这时候用户进程可以去做自己的事情。
- 过一段时间再进行 read 操作。一直等到 kernel 中的数据准备好,返回一个数据已经准备好的信号。
- 用户进程收到数据准备好的信号后,开始阻塞自己,将 kernel 的数据拷到用户进程空间。
分析
- 特点:nonblocking IO 的特点是用户进程需要不断的主动询问 kernel 数据好了没有。
- 优点:
- 同步非阻塞方式相比同步阻塞方式,能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
- 缺点
- 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次 read 操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
- 由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间
2.3 IO 多路复用( IO multiplexing)
网络模型
IO 多路复用就是用一个单独的非用户态的进程去完成同步非阻塞的轮询工作,本质上也是一种同步非阻塞 IO。和同步非阻塞 IO 的区别是,同步非阻塞 IO使用用户态的进程去完成这个轮询工作,而 IO多路复用使用 UNIX/Linux 下的内核函数
select、poll、epoll
都可以完成这个轮询工作(epoll 比 poll、select 效率高,做的事情是一样的)。我们可以把这类函数的工作称为 select 轮询。select 轮询是内核级别的,可以监听多个 socket,能实现同时对多个IO端口进行监听,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用 I/O 操作函数。然后进程再进行 recvform 系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。流程图如下:

流程描述
- 当用户进程调用了 select,整个进程会被 block,而同时,kernel 会监视所有 select 负责的 socket
- 当任何一个 socket 中的数据准备好了,select 就会返回。
- 用户进程开始阻塞,将数据从 kernel 拷贝到用户进程。
上面的图和 blocking IO 的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个 system call (
select
和 recvfrom
),而 blocking IO 只调用了一个system call (recvfrom
)。用 select 的优势在于它可以同时处理多个 connection。分析
如果处理的连接数不是很高的话,使用 select/epoll 不一定比使用 multi-threading + blocking IO 性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。I/O 多路复用技术通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,大大降低系统开销小,节省了系统资源。
I/O多路复用的主要应用场景如下:
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
- 服务器需要同时处理多种网络协议的套接字。
注意:前面三种IO模式,在用户进程进行系统调用的两个阶段:
- 第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
- 第二个阶段可以阻塞又可以不阻塞。
从整个IO过程来看,他们都是同步模型,因为都属于应用程序主动等待且向内核检查状态。
2.4 异步非阻塞 IO(asynchronous IO)
网络模型
相对于同步IO,异步IO不是顺序执行。用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
Linux 提供了 AIO 库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如 libevent、libev、libuv。异步过程如下图所示:

流程描述
- 用户进程发起 aio_read 操作之后,立刻就可以开始去做其它的事。从 kernel 的角度,当它受到一个asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。
- kernel 会等待数据准备完成,然后将数据拷贝到用户内存。
- kernel 会给用户进程发送一个 signal 或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它 read 操作完成了。如果这个进程正在处理别的事情,就看情况进行处理。
- 如果用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
- 如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
- 如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
Linux 的异步 IO(AIO)支持是 2.6.22 才引入的,还有很多系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的,因此通过异步 IO 的读写操作不会被缓存或缓冲,这就无法利用操作系统的缓存与缓冲机制。
分析
IO 多路复用和 AIO 都适合高并发的应用场景,两者的区别是:
- IO多路复用:IO 多路复用是同步非阻塞模式。select 函数所提供的功能(异步阻塞 IO)与 AIO 类似。不过,它是对通知事件进行阻塞,而不是对 IO 调用进行阻塞。当内核数据在准备过程中,select 函数一直被阻塞,等到数据准备完成后会通知应用程序数据准备好了,然后 select 函数的任务就完成了,至于后面把数据从内核空间复制到用户空间(通过阻塞/非阻塞形式都可以)这部分工作由应用程序本身完成。
- AIO:AIO 是异步模式。内核准备数据和把数据从内核空间复制到用户空间这两部分工作都由系统完成,也就是 IO 过程全部由系统控制,当应用程序收到通知的时候用户空间已经准备好数据了。
2.5 信号驱动式IO(signal-driven IO)
信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。过程如下图所示:

信号驱动IO实际使用得不多,所以在这里不做深入介绍。
3、总结
本章介绍了同步/异步、阻塞/非阻塞的概念、linux IO的基础概念和五种常见的 IO 模型:
- 同步和异步是从
调用结果通知机制
这个角度来区分的。同步调用是指调用发生后,调用者
主动去获取调用结果
;异步调用是指调用发生后,调用者
被动等待被调用者
通知调用结果
。
- 阻塞和非阻塞是从
调用者在获取到调用结果前的行为
这个角度进行区分的。阻塞调用是指调用者在获取到
调用结果前什么都不干,线程整个挂起一直等待调用结果
;非阻塞调用是指调用者
在获取调用结果
前可以做自己的事情。
- 系统空间分为用户空间和内核空间,内核空间供系统内核使用,用户空间供各个进程使用。
- 进程被挂起,新的进程或者以前被挂起的某个进程恢复执行,这种过程被称作进程切换。进程从运行态转换成阻塞态,这种过程被称作进程阻塞,进程阻塞后不占用cpu资源。
- 文件描述符fd是指向文件的引用的抽象化概念。
- linux IO的过程:数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
- 五种常见的 IO 模型:
- 同步阻塞 IO(blocking IO)
- 同步非阻塞 IO(nonblocking IO)
- IO 多路复用( IO multiplexing)
- 异步非阻塞 IO(asynchronous IO)
- 信号驱动式IO(signal-driven IO)
其中 IO 多路复用模型是 netty IO 模型的基础,必须要掌握,五种 IO 模型流程简单总结如下:

- Author:mcbilla
- URL:http://mcbilla.com/article/f82b640b-93c0-4a44-a208-a399136ce67d
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts