传统文件传输过程
文件传输主要就是文件读写过程
linux传统文件传输过程
文件拷贝过程中发生了四次用户态与内核态的切换
四次拷贝(从盘读取文件并发送到网卡)
四次上下文切换
每次系统调用的时候都会从用户态切换到内核,内核任务完成之后再切换回用户态
用户空间:最外侧,只能访问到受限的资源
内核空间:进程调度 内存分配 连接硬件资源
对应不同的内核等级,数字越小权限越大,内核是最高的等级 可以访问所有的资源
两个模式的切换过程叫做 :特权模式切换
进程可以在俩个空间中运行
CPU上下切换
调用read的时候进行了一次切换,首先会保存原来的cpu寄存器里的用户态的指令位,为了执行内核态代码,CPU寄存器需要更新为内核指令的新位置,然后跳转到内核态运行内核任务。系统调用结束后 CPU寄存器需要回复原来保存的用户态,然后再切换回用户空间 继续之前的进程(几十纳秒-几微秒) 但是在高并发的场景下时间很容易被累计和放大 从而影响系统的整体性能
之前的操作系统没有区分用户态和核心态,应用程序都是在内核中运行的,这样很容易导致整个操作系统崩溃,如果代码中有些bug 可能会导致运行在这个内核操作系统上的其他程序也随之崩溃 难以保证操作系统的稳定性 所以需要进行用户态和核心态的区分
CPU拷贝
用户态的数据是不可信的 所以内核在访问操作之前都需要做必要的检查不同的系统调用 可能要做的检查流程是不同的 但是这个检查如果不拷贝直接在原地操作的话可能会导致在检查和使用之间有小段时间窗口 如果用户在这个时间窗口内对内存进行修改的话会导致之前的检查是无效的 可能会导致内核出现崩溃等其他问题 所以需要先拷贝数据到内核态防止这种变化
CPU数据拷贝方式
CPU访问速度远远高于磁盘准备数据的速度
I/O轮询
CPU大量时间都在等待 效率太低
IO中断
CPU在等待的时间内先去完成其他任务 可以解放CPU
处理中断也是有一定的开销的
DMA传输 直接内存访问
DMA是主板上独立的一块芯片 就是一个协处理器(本身也是个处理器芯片)和CPU一样可以访问内存和外设
DMA是被设计用来单独处理io数据传输的 成本比CPU低
CPU和DMA进行交互 首先指定需要读写的数据块大小和需要进行io数据目的地内存地址之后 CPU异步处理别的指令 只有当DMA完成了整体数据块io后才会发起中断通知CPU 相当于做了一个缓存
异步非阻塞
内核和用户态的数据拷贝还是需要CPU执行 因为DMA只能支持硬件数据拷贝
可以把内核和磁盘中两次拷贝替换为DMA拷贝了
零拷贝的实现方式
mmap + write
进程虚拟内存映射的方法 将文件或者一段物理内存映射到进程的虚拟内存地址空间 然后进程可以用指针来读取这一段内存 系统会自动会写脏页到对应的文件磁盘上(完成文件操作 同时也不用调用read操作)
还是有CPU拷贝 但是不需要从用户态拷贝而是直接从内核中拷贝
mmap代替了read方法 减少一次用户空间和内核空间的CPU拷贝(不会真正把数据拷贝到用户空间 不占用真实的物理内存 适合大文件传输) 需要磁盘代替内存的时候都可以用mmap
sendfile
有写程序不要用到磁盘中的数据 所以可以不用这两次拷贝
调用sendfile接口将内核缓存区拷贝到socket的缓存区中,仅仅发生在内核
只用一次CPU拷贝
sendfile + gather
将数据直接从内核缓存区通过DMA拷贝到网卡
CPU会吧内核缓存区中的内存地址和偏移量发送到socket中 然后由DMA根据这些信息直接把数据从内核缓存区拷贝到网卡 最后发生上下文切换 sendfile函数调用返回
两次上下文切换 + 两次DMA拷贝
需要硬件支持
splice
硬件不支持的话
主要通过管道进行传输数据
go语言中的实现
返回的是内存地址
sendfile
read
io.Copy是调用了copyBuffer方法 主要是readFrom
优先级调用
零拷贝的应用
消息产生:网络数据持久化到磁盘中
消费:磁盘通过网络发送给consumer进行消费
磁盘读写性能比较差
完成io请求:寻到时间 旋转延迟 数据传输(物理操作 耗时高)