前言
拷贝也就是把磁盘或网络中的A文件中拷到B文件中。那么是谁来执行从磁盘中读取操作和写入操作的呢,并且他们读取完以后是直接就能拷到B文件中,还是需要进行一些复杂的处理?在这里将会对比下传统的数据传输和零拷贝方式的传输,这两者有什么区别。
用户态、内核态和CPU上下文切换
在这之前先了解用户态和内核态这2个概念:
- 用户态:是非特权执行状态,该状态下运行的程序被操作系统禁止进行一些危险操作,例如写入系统配置文件,杀死其他用户进程,重启系统,不可直接访问硬件设备。
- 内核态:是高级别特权执行状态,运行在该状态下的程序通常为操作系统程序,具有高的特权级别,拥有访问设备的所有权限。
再然后让我们了解下什么是上下文切换以及为什么上下文切换产生的开销大会影响性能?
CPU上下文切换的具体过程
在每个任务运行前,CPU都需要知道任务从哪里加载,又是从哪里开始运行,也就是说,需要系统事先帮他设置好CPU寄存器和程序计数器(Program Counter,PC)
CPU寄存器,是CPU内置的容量小、但速度极快的内存。程序计数器,则是用来存储CPU正在执行的指令的位置,或者即将执行的下一条指令的位置。他们都是CPU在运行任何任务前,必须依赖的环境,因此也被叫做CPU上下文。
CPU上下文切换,就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文,到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
其实CPU上下文切换分为三种,进程上下文切换、线程上下文切换、中断上下文切换。
在这里我们主要说的是进程上下文切换中的系统调用。进程的运行空间分为内核空间和用户空间。
因为用户空间只能访问有限资源,不能直接访问内存等硬件设备,必须通过系统调用深入内核才能访问这些资源。
进程可以在这内核空间和用户空间运行,分别称为进程的用户态和进程的内核态。
进程是由内核来管理和调度的,进程的切换只能发生在内核态。进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
从用户态到内核态的转变需要通过系统调用来完成,需要进行CPU上下文切换,保存用户态的CPU上下文,加载内核态的CPU上下文,结束后相反,其中发生了两次CPU上下文切换。这一切都在同一个进程中进行,会消耗系统资源,但并不是进程的CPU上下文切换。
- 进程上下文切换,是指从一个进程切换到另一个进程运行。
- 而系统调用过程中一直是同一个进程在运行。
进程的上下文切换比系统调用多一步,在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存和栈等保存下来,而加载了下一个进程的内核状态和CPU寄存器之后,还需要刷新下一个进程的虚拟内存和栈等。
保存CPU上下文和恢复CPU上下文是需要系统内核在CPU上运行才能完成的。所以进程上下文切换次数较多,很容易导致CPU将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,大大缩短了真正运行进程的时间。
传统的数据传输方式
当进程使用read()、write()去读取和写入磁盘中的文件时会分为以下几个步骤:
- 将进程的用户态转变为内核态(属于系统调用,也是一次上下文切换,需要花费CPU时间)
- 内核态进程读取磁盘文件内容放到内核空间中,然后把内核空间中的文件拷贝到用户空间中。(第一次内存拷贝)
- 然后把内核态装变为用户态(第二次上下文切换)
- 进程将数据输入到用户空间中,然后将进程的用户态转变为内核态(第三次上下文切换)
- 内核态下进程读取用户空间下的数据拷贝到内核空间中(第二次内存拷贝)
- 内核空间中数据写入到磁盘空间中,然后内核态转变为用户态(第四次上下文切换)
可见传统的传输过程中会发生4次上下文切换,2次的内存拷贝。很消耗CPU时间和内存空间。到此为止,我们就了解为什么传统IO是那么的消耗性能且效率又低的原因了。