本文将介绍「传统文件传输」,传统文件传输涉及到两个 syscall,也就是
read
和
write
,这个效率是很糟糕,为什么?那么怎么去优化呢?
传统文件传输
场景:服务端提供文件传输的功能。最简单实现方式:将磁盘上的文件读取出来,然后通过网络协议发送给客户端即可。
「传统 I/O 的工作方式」
上述过程涉及到两个系统调用:
read(file, tmp_buf, len); |
可以看到,整个过程有4次用户态和内核态的上下文切换,因为有两次系统调用,每次系统调用都需要先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。要知道,上下文切换的开销并不小,频繁的上下文切换很影响系统的性能。
除此之外,整个过程还有4次数据拷贝,两次是DMA拷贝,两次是CPU拷贝。
- 第一次拷贝:磁盘文件 -> 操作系统内核缓冲区,DMA 完成;
- 第二次拷贝:内核缓冲区 -> 用户缓冲区,CPU 完成,此步骤完成后,应用程序可以使用这部分数据了;
- 第三次拷贝:用户缓冲区 -> Socket 缓冲区,CPU 完成;
- 第四次拷贝:Socket 缓冲区 -> 网卡缓冲区,DMA 完成。
回顾整个过程,我们的需求只是搬运一份数据,结果却拷贝了四次。显然,过多的数据拷贝会消耗 CPU 资源,从而降低系统的性能。
因此,先提高文件传输的性能,需要减少「用户态和内核态的上下文切换」和「数据拷贝」的次数。
优化文件传输性能的思路
减少「用户态和内核态的上下文切换」次数
读取磁盘数据的时候,要发生上下文切换的根本原因是因为用户空间没有足够的权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生 2 次上下文切换。因此,要想减少上下文切换到次数,就要减少系统调用的次数。
减少「数据拷贝」的次数
回顾下上述的整个过程,其实「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」这个过程是没有必要存在的,因为在文件传输的场景中,用户空间并不会对数据再加工,所以数据实际上可以不用搬运到用户空间。
因此,需要想办法绕过用户缓冲区。
这就引出了「零拷贝」的思想,下一篇文章,我们再介绍。