服务器提供文件传输功能,首先从磁盘读取文件,然后通过网络协议发送给客户端。最直接的办法是根据客户端的请求从磁盘上找到文件位置,然后从磁盘把部分文件(一般文件比较大时,需要对文件进行切分)读入到缓冲区,然后再通过网络把数据发送给客户端。
该方案在一次收发过程中涉及到 4 次用户态和内核态的上下文切换,没处理缓冲区大小的数据需要一次 read 调用和一次 write 调用,每次调用都需要从用户态切换到内核态,然后等内核态完成任务后,再切回用户态。因为文件比较大,读取次数比较多,所以上下文切换的成本不容小觑
磁盘 -> PageCache
PageCache -> 用户缓冲区
用户缓冲区 -> Socket 缓冲区
Socket 缓冲区 -> 网卡
因为涉及到多次内存拷贝,消耗过多的 CPU 资源,降低系统并发处理能力
想要优化传输文件的性能,需要从降低上下文切换的频率和内存拷贝次数入手
读取磁盘文件的上下文切换是一定会做的,因为读取磁盘和操作网卡都是由操作系统内核完成。所以我们在执行 read 或 write 这种系统调用时,一定会经过 2 次上下文切换:先从用户态切换到内核态,当内核态任务完成后,再切换回用户态交由进程代码执行
所以,要想降低上下文切换频率的要点就是减少系统调用的次数。解决办法是把 read 和 write 两次系统调用合并为一次(可以通过 sendfile 一次系统调用完成),在内核态中完成磁盘与网卡的数据交换操作
一次收发过程中有两次与物理设备相关的内存拷贝是必不可少的:把磁盘的数据拷贝到内存;把内存的数据拷贝到网卡。而与用户缓冲区相关的内存拷贝不是必须的
综上所述,可以在内核读取文件后,直接把 PageCache 中的数据拷贝到 socket 缓冲区中,这样就只有 2 次上下文切换 和 3 次内存拷贝。如果网卡支持 SG-DMA 技术,还可以把拷贝到 socket 缓冲区的步骤给省略掉
根据时间局部性原理(刚被访问到的数据在短时间被再次访问的概率高),通常将最近访问的数据放到 PageCache 中,当空间不足时通过 LRU 算法或变种算法淘汰最久未被访问的数据
PageCache 还提供预读功能
但 PageChache 不适应传输大文件的场景,大文件容易把 PageCache 占满,而且由于文件太大,文件中某一个部分被再次访问的概率低。这样会导致大文件在 PageCache 中没有享受到缓存的优势,同时也因为 PageCache 被大文件占据,影响其他热点小文件的缓存
异步 IO 可以把读操作分为两部分,前半部分向内核发起读请求,但不用等待数据就位就返回,然后可以继续处理其他任务。当内核把磁盘中的数据拷贝到进程缓冲区后,会通知进程去处理数据。异步 IO 是不会阻塞用户进程的
对于磁盘,异步 IO 只支持直接 IO
直接 IO 是应用程序绕过 PageCache,即不经过内核缓冲区,直接访问磁盘中的数据,从而减少了内核缓存与用户程序之间的数据拷贝
因为直接 IO 不适用 PageCache 缓存,所以享受不到内核针对 PageCache 做的一些优化,比如内核会试图缓存更多的连续 IO 在 PageCache 中,然后合并成一个更大的 IO 后发给磁盘,可以减少磁盘的寻址操作;另外,内核还会进行数据的预读,把数据缓存到 PageCache 中,较少磁盘操作
Kafka 的高性能的原因就包括使用了零拷贝技术和 PageCache 缓存
异步 IO 一定不会阻塞进程吗?如果阻塞了进程,该如何解决?
这个问题我觉得要看怎么定义异步 IO
Posix 对异步 IO 的定义为:异步 IO 操作不引起请求进程阻塞。老师在文中对异步 IO 的定义也类似,当内核把磁盘中的数据拷贝到进程缓冲区后,会通知进程去处理数据,所以按照这样的定义,异步 IO 不会阻塞进程
《UNIX网络编程》这本书把 IO 模型分成了5类
1.阻塞 IO
2.非阻塞 IO
3.IO 复用(select 和 poll)
4.信号驱动
5.异步 IO(Posix.1 的 aio 系统函数)
IO模型
如果把信号驱动的 IO 模型也看成异步 IO,因为用户进程在调用 sigaction 后,会继续执行其他任务,这里是非阻塞的,内核会在数据准备好时通知用户进程,然后由用户进程发起 recvfrom 的系统调用,把数据从内核缓冲区拷贝到用户空间,此时用户进程是阻塞的
所以还是看如何定义异步 IO,至于那些伪异步 IO,怎么解决,我觉得解决办法就一个,就是等内核把数据准备好后,自己把数据从内核缓冲区复制到用户空间,然后通知用户进程进行数据的处理
参考资料
《系统性能调优必知必会》专栏-极客时间