Linux 网络IO模型
2025-01-22 08:19:30    3.3k 字   
This post is also available in English and alternative languages.

网络I/O模型是许多东西的基础,不理解的话就很难理解一些框架的核心(精髓),无法理解它们为什么要这么设计;例如:redis为什么快?kafka性能为什么好?tomcat是如何操作web request的?


1. 知识准备


2. 一次I/O操作经历了什么

在正式开始之前看个简单的磁盘IO示例,先对I/O的流程有一个感性的认识。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception {
//fd已经在构造FileInputStream创建
try (FileInputStream in = new FileInputStream("/tmp/file1.txt");
FileOutputStream out = new FileOutputStream("/tmp/file2.txt")) {
byte[] buf = new byte[in.available()];
in.read(buf);
out.write(buf);
} catch (FileNotFoundException e) {
log.error(e.getMessage(), e);
}
}

上面的代码是Java中典型的磁盘IO操作,读取一个文件的内容,输出到另一个文件中;那么这个过程会发生那些事呢?请看下图:

操作文件BIO流程

梳理下整个流程:

  1. in.read(buf);执行时,JVM向kernel(内核)发送read()指令;
  2. 操作系统发生上下文切换,由 userMode(用户态) 切换到 kernelMode(内核态) ,将磁盘上的数据读取到Kernel space buffer(内核空间的buffer)中;
  3. 内核将数据从内核空间copy到用户空间,同时将 kernelMode(内核态) 切换到 userMode(用户态);
  4. JVM执行out.write(buf);JVM向kernel(内核)发送write()指令;
  5. 操作系统发生上下文切换,由 userMode(用户态) 切换到 kernelMode(内核态) ,同时将用户空间数据 copy 到 kernelSpaceBuffer(内核空间的buffer)中。
  6. 然后由内核写入磁盘(文件),并将结果返回应用程序。

注:read()、write()指令即是systemCall(系统调用)。

从上面可以看出一个I/O操作,通常会发生下面的事:

  • 两次上下文切换(userMode 和 kernelMode)

    注意,一个IO操作就会有两次上下文切换;
    像上面代码示例中有 read 和 write,就是四次上下文切换。

  • 数据在 useSpace(用户空间) 与 kernelSpace(内核空间) 之间复制


上面这个简单的磁盘IO示例只是为了对IO流有个认识,在讨论网络IO前,还是要提醒下:不要用操作磁盘文件IO的经验去看待网络IO
因为在磁盘IO中,等待阶段是不存在的,磁盘文件并不像网络IO那样需要等待远程传输数据;


3. 网络I/O模型种类

种类关联函数
blocking I/O - 阻塞式I/O
nonblocking I/O - 非阻塞式I/O
I/O multiplexing - I/O多路复用select、poll、epoll
signal driven I/O - 信号驱动式I/OSIGIO
asynchronous I/O - 异步I/OPOSIX的aio_系列函数

4. blocking I/O - 阻塞式I/O

注:recvfrom()函数即是systemCall(系统调用)。

blocking I/O - 阻塞式I/O

blocking I/O(阻塞式I/O) 调用recvfrom()时,用户进程将一直阻塞等待另一端Socket的数据到来;

值得注意的是,这里的阻塞点有两个:「内核数据准备好」和「数据从内核态拷贝到用户态」。

在这种I/O模型下,如果希望提高性能(吞吐、处理量)就不得不为每一个Socket都分配一个线程,这会造成很大的资源浪费。

blocking I/O(阻塞式I/O) 优缺点都非常明显:

  • 优点:简单易用,对于本地I/O而言性能很高。
  • 缺点:处理网络I/O时,会造成进程阻塞空等,浪费资源。

5. nonblocking I/O - 非阻塞式I/O

相对于 blocking I/O(阻塞式I/O) 在那傻傻的等待,nonblocking I/O(非阻塞I/O) 隔一段时间就执行 systemCall(系统调用) 查看数据是否就绪(ready),这种方式称之为轮训(polling);

如果数据就绪,内核将数据就从 kernelSpace(内核空间) 复制到 useSpace(用户空间) 然后再返回到用户程序;如果内核中数据还没就绪, kernel(内核) 会立即返回EWOULDBLOCK这个错误;具体流程如下图:

非阻塞式IO

当 kernel(内核) 数据准备就绪,用户程序调用 recvfrom() 执行IO时,用户程序依旧是会阻塞的;也就是上图中用户程序调用的最后一个 recvfrom() 函数,直到return ok,这段时间是会阻塞的。

值得注意的是,这里的阻塞点有一个:「数据从内核态拷贝到用户态」。

上图中的 recvfrom() 函数和 blocking I/O(阻塞式I/O) 中的是同一个函数;
recvfrom() 函数中有一个flags参数,设置flags参数为非阻塞就可以让 kernel(内核) 在数据未就绪时直接返回EWOULDBLOCK


6. I/O multiplexing - I/O多路复用

什么是I/O多路复用?
上面介绍的I/O模型,都只能检测一个fileDescriptor(文件描述符),而I/O多路复用一次可以检测多个fileDescriptor(文件描述符),一旦某个*fileDescriptor(文件描述符)*就绪(读就绪或者写就绪),能够通知用户程序进行相应的读写操作。

I/O multiplexing 中的 multiplexing 指的其实是:通过单个线程记录跟踪每一个Socket(I/O流)的状态来同时管理多个I/O流。尽量多的提高服务器的吞吐能力。

首先向 kernel(内核) 发起 systemCall(系统调用),传入多个 fileDescriptor(文件描述符) 和感兴趣的事件(readable、writable等)让 kernel(内核) 监测;

  • 如果没有事件发生,用户程序(线程)只需阻塞在这个系统调用上,无需像非阻塞I/O模型那样,通过轮训操作来判断是否有数据。

  • 如果有事件发生,内核会返回产生了事件的连接,用户程序(线程)就会从阻塞状态返回,用户程序再发起真正的I/O操作recvfrom读取数据。

注意,这里的阻塞点有一个:「数据从内核态拷贝到用户态」。

IO多路复用

在linux中,有3种 systemCall(系统调用) 可以让内核监测 fileDescriptor(文件描述符),分别是selectpollepoll

epoll是一种性能很高的同步I/O方案;现在linux中的高性能网络框架(tomcat、netty等)都有epoll的实现,缺点是只有linux支持epoll,BSD内核的kqueue类似于epoll


7. signal driven I/O - 信号驱动式I/O

这种信号驱动的I/O并不常见,本文忽略。


8. asynchronous I/O - 异步I/O

异步IO

asynchronous I/O(异步I/O) ,即I/O操作不会引起进程阻塞;

如上图,发起aio_read请求后, 数据没有准备好,kernel(内核) 会直接返回;等数据就绪(数据已经从内核空间copy到用户空间), kernel(内核) 发送一个signal到process处理数据。

需要注意的是,用户程序不需要再次发起读取数据的 systemCall(系统调用) ,因为 kernel(内核) 会把数据复制到用户空间再通知用户程序进行处理,整个过程不会存在任何阻塞。

在这里,用户程序不用在「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程中等待,是真正的异步I/O。

aio_read函数用于对一个有效的 fileDescriptor(文件描述符) 发送异步读请求,这个 fileDescriptor(文件描述符) 可以是一个文件、套接字或者管道;
当读请求被插入到队列之后aio_read函数会立即返回,成功时返回值为0,失败时返回值为-1,并且会设置errno全局变量。


9. IO模型比较

5种IO模型比较

POSIX中定义了同步I/O与异步I/O:

  • 同步I/O:导致请求进程阻塞,知道I/O操作完成。
  • 异步I/O:不导致请求进程阻塞

上面五种IO模型,如上图所示,前4种I/O模型都是阻塞式,是因为它们在发起真正I/O操作时都会阻塞用户程序,至少会让用户程序等待在「内核数据准备好」和「数据从内核态拷贝到用户态」这两个点中的一个。

而只有最后的异步I/O不会阻塞用户程序,kernel(内核) 会将数据主动copy到用户态中,然后通知用户程序进行处理。


10. 同步&异步 和 阻塞&非阻塞

  • 同步:调用者要一直等待调用结果的通知后才能进行后续的执行,现在就要,我可以等,等出结果为止

  • 异步:指被调用方先返回应答让调用者先回去,然后再计算调用结果,计算完最终结果后再回调通知并返回给调用方

同步和异步的理解:讨论对象是被调用者(服务提供者),重点在于获得调用结果的消息通知方式上。


  • 阻塞:调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干;

  • 非阻塞:调用在发出去后,调用方先去忙别的事情,不会阻塞当前进/线程,而会立即返回;

阻塞非阻塞理解:讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其它事。


11. 疑问解答

11.1. 阻塞I/O和非阻塞I/O

blocking I/O(阻塞式I/O) 发起真正的 I/O操作时(如read、recvfrom等)会阻塞等待 kernel(内核) 中数据就绪;
nonblocking I/O(非阻塞I/O) 则会不断发起systemCall(系统调用),直到 kernel(内核) 中数据就绪。

brpc-io.md中有一段对两种IO模型的对比,觉得说的很好:

linux一般使用non-blocking IO提高IO并发度。

当IO并发度很低时,non-blocking IO不一定比blocking IO更高效,因为后者完全由内核负责,而read/write这类系统调用已高度优化,效率显然高于一般得多个线程协作的non-blocking IO。

但当IO并发度愈发提高时,blocking IO阻塞一个线程的弊端便显露出来:内核得不停地在线程间切换才能完成有效的工作,一个cpu core上可能只做了一点点事情,就马上又换成了另一个线程,cpu cache没得到充分利用,另外大量的线程会使得依赖thread-local加速的代码性能明显下降,如tcmalloc,一旦malloc变慢,程序整体性能往往也会随之下降。

而non-blocking IO一般由少量event dispatching线程和一些运行用户逻辑的worker线程组成,这些线程往往会被复用(换句话说调度工作转移到了用户态),event dispatching和worker可以同时在不同的核运行(流水线化),内核不用频繁的切换就能完成有效的工作。线程总量也不用很多,所以对thread-local的使用也比较充分。这时候non-blocking IO就往往比blocking IO快了。

不过non-blocking IO也有自己的问题,它需要调用更多系统调用,比如epoll_ctl,由于epoll实现为一棵红黑树,epoll_ctl并不是一个很快的操作,特别在多核环境下,依赖epoll_ctl的实现往往会面临棘手的扩展性问题。non-blocking需要更大的缓冲,否则就会触发更多的事件而影响效率。non-blocking还得解决不少多线程问题,代码比blocking复杂很多。


11.2. I/O Multiplexing(I/O多路复用)是非阻塞IO嘛

它不是非阻塞IO ;

selectpollepoll都会导致用户进程阻塞;发起真正的IO操作(如recvfrom)时,用户进程也会阻塞。

I/O Multiplexing(I/O多路复用) 优点在于一次性可以监控大量的 fileDescriptor(文件描述符)


11.3. 同步IO 和 异步IO

POSIX对这两个术语定义如下:

  • 同步I/O操作将会造成请求进程阻塞,直到I/O操作完成
  • 异步I/O操作不会造成进程阻塞

根据上面的定义可以看出,前面4种I/O模型都是同步I/O,因为它们的I/O操作(recvfrom)都会造成进程阻塞。只有最后一个I/O模型匹配异步I/O的定义。

POSIX defines these two terms as follows:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  • An asynchronous I/O operation does not cause the requesting process to be blocked.
    Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

特别注意: select、poll、epoll并不是I/O操作,read、recvfrom这些才是。


12. Reference


13. 推荐阅读