Linux之【网络I/O】前世今生(一)
在 Linux之【磁盘I/O】前世今生 一文中,我们介绍了文件I/O 的细节。本文将继续介绍网络I/O的内容。
一、 基本概念
介绍网络I/O前,先了解一些基本概念。
1.1、上下文
CPU 寄存器,是CPU内置的小容量、速度极快的内存。程序计数器,是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。二者是 CPU 在运行任何任务时,必须依赖的环境,记为上下文。
1.2、上下文切换
运行新的任务时需把当前任务的上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器中,最后再跳转到程序计数器所指的新位置,运行新任务。
所以执行不同的程序,即进程切换时会发生上下文切换,线程切换也一样。
操作系统和用户程序是两个不同的程序,前者运行在内核态,后者运行在用户态,所以,内核态和用户态的切换一定会发生上下文切换,进程从用户态到内核态的转变,需要通过系统调用(调用操作系统)来完成。系统调用的过程,会发生CPU上下文的切换。
CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
1.3、Linux I/O读写方式
- 轮询
- 基于死循环对 I/O 端口进行不断检测;
- 需要CPU参与。
- I/O 中断
- 当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程;
- 需要CPU参与。
- DMA 传输
DMA
(Direct Memory Access,直接存储器访问) 传输则在 I/O 中断的基础上引入了 DMA 磁盘控制器,由 DMA 磁盘控制器负责数据的传输,降低了 I/O 中断操作对 CPU 资源的大量消耗;- DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与;
- 每个 I/O 设备里面都有自己的 DMA 控制器,如网卡、声卡、显卡、磁盘控制器等;
- CPU 除了在数据传输开始和结束时做一点处理外(开始和结束时候要做中断处理),在传输过程中 CPU 可以继续进行其他的工作。这样在大部分时间里,CPU 计算和 I/O 操作都处于并行操作,使整个计算机系统的效率大大提高。
二、 网络I/O
2.1、传统网络I/O
以网络传输文件为例,需要经历步骤如下图所示:
read
+ write
组合:
- 涉及上下文切换 4 次
- read 调用发起,图示(1);
- read 调用返回,图示(4);
- write 调用发起,图示(5);
- write 调用返回,图示(8);
- 数据拷贝 4 次
- 数据读取:
- 磁盘拷贝到 page Cache,图示(2)DMA拷贝;
- page Cache 拷贝到程序Buffer,图示(3)CPU拷贝;
- 数据写入:
- 程序Buffer 拷贝到Socket缓存区,图示(6)CPU拷贝;
- Socket缓存区拷贝到网卡,图示(7)DMA拷贝;
- 数据读取:
- 注意上图拷贝数据步骤(2)、(3)、(6)、(7)区别:(3)和(6)是CPU拷贝,(2)和(7)是
DMA
拷贝。
2.2、优化传统网络I/O
上文提到传统网络I/O性能最大的瓶颈在于:
- 数据拷贝次数过多;
- 上下文切换次数过多。
所以我们优化的主要方向就是这两个维度:减少数据拷贝次数和上下文切换次数。
2.2.1、减少数据拷贝次数
2.2.1.1、mmap(Memory Map,内存映射)
仔细观察,数据没有必要拷贝到程序 Buffer 中,我们可以采用 mmap(memory map,内存映射) 技术,将程序 Buffer 映射到内核 Page Cache
,从而避免数据在内核态和用户态之间相互拷贝。
用mmap
+ write
组合替代read
+ write
组合后:
- 涉及上下文切换 4 次
mmap 调用和返回、 write 调用和返回。
- 数据拷贝 3 次
- 1次CPU拷贝;
- 2次DMA拷贝。
Java NIO有一个MappedByteBuffer的类,可以用来实现内存映射。它的底层是调用了Linux内核的mmap的API。
2.2.2、减少上下文切换次数
2.2.2.1、sendfile
目前为止,我们已经通过 mmap 技术成功的将数据拷贝次数减少了1次;但还是涉及4次上下文切换,我们可以将之前的两次系统调用(read、write
)合并为一次,记为 sendfile
(Linux 内核版本 2.1开始提供), sendfile
不会拷贝数据到用户态,直接在内核态工作。
用 sendfile
来替代 mmap
+ write
组合后:
- 涉及上下文切换 2 次
sendfile调用和返回;
- 数据拷贝 3 次
- 1次CPU拷贝;
- 2次DMA拷贝。
java 中 FileChannel#transferTo() 底层调用的正是
sendfile
。
2.2.2.2、sendfile + SG-DMA
linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA(需要硬件支持) 技术,其实就是对DMA拷贝加入了分散/收集(scatter/gather)操作,它可以直接从内核空间缓冲区中将数据读取到网卡。
SG-DMA:
CPU 将 Page Cache 中的数据描述信息(内存地址和偏移量)记录到socket缓冲区,由 SG-DMA 根据这些将数据从Page Cache拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程。
sendfile
+ SG-DMA
组合:
- 涉及上下文切换 2 次
sendfile调用和返回;
- 数据拷贝 2 次
2次SG-DMA拷贝。
2.2.2.3、splice
sendfile
只适用于将数据从文件拷贝到 socket 套接字上,且sendfile
+ SG-DMA
组合还需要硬件的支持,这也限定了它的使用范围。
Linux在 2.6.17 版本引入 splice
系统调用,不仅不需要硬件支持,同时还支持实现了两个普通文件之间数据流动;
也就是说 splice
不仅实现了sendfile
+ SG-DMA
组合的功能,还扩大了使用范围。后续 sendfile
经过优化,底层也直接调用了 splice
来实现功能。
splice
系统调用可以在Page Cache
和 Socket 缓存区
之间建立管道(pipeline),从而避免了两者之间的CPU拷贝。
splice
:
- 涉及上下文切换 2 次
splice 调用和返回;
- 数据拷贝 2 次
2次DMA拷贝。
三、零拷贝(Zero-copy)
拷贝方式 | 系统调用 | DMA拷贝次数 | CPU拷贝次数 | 上下文切换次数 |
---|---|---|---|---|
传统方式 | read + write | 2 | 2 | 4 |
内存映射 | mmap + write | 2 | 1 | 4 |
sendfile | sendfile | 2 | 1 | 2 |
sendfile + SG-DMA | sendfile | 2 | 0 | 2 |
splice | splice | 2 | 0 | 2 |
至此,我们将不断的将传统I/O进行改进,将CPU拷贝次数逐步降低为0。我们把改进后的拷贝方式,统称为零拷贝(Zero-copy),官方定义如下:
零拷贝是指计算机执行I/O操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。
从这个定义来看,在 Linux之【磁盘I/O】前世今生 中提到的 直接I/O (从硬盘直接拷贝数据到用户缓存,不需要OS Cache)同样属于零拷贝;此外,还有写时复制COW(copy on write) 同样也属于零拷贝。
附 Linux 前世今生系列文章:
- Linux之【文件系统】前世今生(一)
- Linux之【文件系统】前世今生(二)
- Linux之【内存管理】前世今生(一)
- Linux之【磁盘 IO】前世今生
- Linux之【网络I/O】前世今生(一)