Labs 导读
标准服务器技术是网络功能虚拟化(NFV)实现的一个关键因素,了解一些x86架构的基础知识对大家后续了解电信云关键技术,尤其是掌握虚拟化技术原理和关键优化方案是必须具备的。本文接着上篇从x86架构的中断和异常、IO架构等部分进行阐述讲解。
1、中断与异常
程序的执行往往不只是按顺序执行那么简单,一些异常和中断会打断顺序执行的程序流,转而进入一条完全不同的执行路径。中断提供给外部设备一种“打断CPU当前执行任务,并响应自身服务”的手段。中断(interrupt)是异步的事件,典型的比如由I/O设备触发;异常(exception)是同步的事件,典型的比如处理器执行某条指令时发现出错了等等,其实异常的本质就是同步中断。
中断通常被定义为一个打断CPU芯片指令执行的事件,该事件对应到CPU芯片内部或者外部的电路产生的电子信号。
中断信号可以被划分为同步中断和异步中断:
- 同步中断,该类型中断由CPU的控制单元在执行指令的时候产生,并且是在当前指令执行完毕下一个指令执行之前产生。
- 异步中断,该类型中断由其他硬件设备在任意的时间产生,并且遵循CPU的时钟信号传递给CPU。
对于Intel的CPU而言,它将同步中断称作异常,而将异步中断称作中断。
通常中断(即异步中断)由时钟定时器或者其他I/O设备产生,如键盘接收到敲击某个按键的信号后产生的中断信号。而异常(即同步中断)则通常由于编程错误或者由CPU检测到异常条件需要内核进行处理而产生,如上面讲到的Page Fault Exception(缺页异常),异常可以由程序通过int或者sysenter指令主动产生。
对于Intel x86 CPU而言,它将中断和异常进行了如下归类:
中断,即异步中断,中断信息随着CPU的时钟信号传递到CPU内部。中断分为可屏蔽中断和不可屏蔽中断两类。
- 可屏蔽中断,所有由I/O设备产生的IRQ请求都被归为可屏蔽中断。一个可屏蔽中断可以有两种状态,屏蔽或者不屏蔽,当一个中断被屏蔽时,该中断信号将被对应的控制单元所忽略。
- 不可屏蔽中断,即控制单元无法忽略该类型的中断信号,CPU肯定会接收到该类型的中断,一般对应到一些紧要的事件,比如硬件错误。
异常,即同步中断,中断信号在CPU执行完某个指令后产生并接收到。处理器检测到的异常,即当CPU执行指令的时候检测到硬件上存在一些异常条件的时候就会产生该信号。这种类型的异常根据产生时在内核堆栈中保存的EIP寄存器的值(即异常恢复后CPU重新执行的位置)进行细分:
- Faults,该异常可以被内核正确纠正,并且纠正后重新执行引起该异常的指令时不会造成程序的中断或者功能的异常。这时候保存到EIP寄存器的值是引起异常的指令的地址,故异常恢复的时候会重新执行该指令,如Page Fault Exception(缺页异常),当访问的内存地址没有被映射到物理内存时,产生异常,内核分配新的物理内存页并建立映射关系,然后异常处理完毕后,CPU重新访问该地址,即可访问到正确的物理内存。
- Traps,该异常发生时,内核堆栈EIP寄存器保存的地址指向引起该异常的指令的下一条指令,即当该异常处理返回后会继续程序的执行,而不是重新执行引起异常的指令。x86 CPU的硬件虚拟化功能就是利用陷入(Traps)再模拟的方法,当CPU执行虚拟机指令的时候,如果执行的是敏感指令,就会触发Traps类型的异常,让VMM(Virtual Machine Monitor)对该敏感指令进行模拟,然后继续恢复虚拟机的运行。
- Aborts,当发生严重的错误时,CPU已经无法保证内核堆栈中EIP寄存器存放的值是引起该异常的指令的地址。该异常用于汇报严重的错误,如硬件错误或者是内存的不一致性。该异常信号让CPU切换到相应的abort exception handler,该处理函数由于无法确认错误,只能结束当前进程。
我们在写程序时,经常会在容易产生错误的地方进行异常抛出,然后针对抛出的异常定义执行策略。这类编程产生的异常,由程序主动执行int或者int3之类的指令产生。CPU像处理Traps一样处理这些程序主动产生的异常,该类异常通常被称为软件中断(software interrupt)。这类异常主要有两种用途:实现系统调用和通知某个debugger特定的事件发生。
这些异常或中断由0~255的数字唯一标识,也就是经常说的中断信号量。对于不可屏蔽中断和异常来说,相应的中断信号量是固定的,而可屏蔽中断对应的中断信号量则可以通过设置中断控制器来更改。
2、x86系统的I/O架构
计算机所处理的任务其实只有两种:CPU运算和I/O操作。这部分内容是后续学习计算虚拟化中I/O虚拟化的基础。I/O(输入/输出)是CPU访问外部设备的方法。设备通常通过寄存器和设备RAM将自身功能展现给CPU,CPU通过读/写这些寄存器和RAM完成对设备的访问及其他操作。按访问方式的不同,x86架构的I/O分为如下两类:
2.1 端口I/O(后文简称为Port I/O)
即通过I/O端口访问设备寄存器。x86有65536个8位的I/O端口,编号为0x0~0xFFFF。CPU将端口号作为设备端口的地址,进而对设备进行访问。这65536个端口构成了64KB的I/O端口地址空间。I/O端口地址空间是独立的,不是线性地址空间或物理地址空间的一部分。需要使用特定的操作命令IN/OUT对端口进行访问,此时CPU通过一个特殊的芯片管脚标识这是一次I/O端口访问,于是芯片组知道地址线上的地址是I/O端口号并相应地完成操作。此外,2个或4个连续的8位I/O端口可以组成16位或32位的I/O端口。
2.2 内存映射I/O(Memory Map I/O,后文简称为MMIO)
即通过内存访问的形式访问设备寄存器或设备RAM。MMIO要占用CPU的物理地址空间,它将设备寄存器或设备RAM映射到物理地址空间的某段地址,然后使用MOV等访存指令访问此段地址,即可访问到映射的设备。MMIO方式访问设备也需要进行线性地址到物理地址的转换,但是这个转换过程中的MMIO地址不可缓存到TLB中。MMIO是一种更普遍、更先进的I/O访问方式,很多CPU 架构都没有Port I/O,采用统一的MMIO方式。
3、DMA技术
直接内存访问(Direct Memory Access,后文简称为DMA)是所有现代计算机的重要特色。DMA允许设备绕开CPU直接向内存中复制或读取数据。如果设备向内存复制数据都经过CPU,则CPU会有大量中断负载,中断过程中,CPU对其他任务来讲无法使用,不利于系统性能的提高。通过DMA,CPU只负责初始化这个传输动作,而传输动作本身由DMA 控制器(简称为DMAC)来实行和完成。在实现DMA传输时,由DMAC直接控制总线,在DMA传输前,CPU要把总线控制权交给DMAC,结束DMA传输后,DMAC立即把总线控制权交回给CPU。
一个完整的DMA 传输过程的基本流程如下:
- DMA请求:CPU对DMAC进行初始化,并向I/O端口发出操作命令,I/O端口提出DMA请求。
- DMA响应:DMAC对DMA请求进行优先级判别和屏蔽判别,然后向总线控制芯片提出总线请。CPU执行完当前总线周期后释放总线控制权。此时,总线控制芯片发出总线应答,表示DMA请求已被响应,并通过DMAC通知I/O端口开始DMA传输。
- DMA传输:DMAC获得总线控制权后,CPU即可挂起或只执行内部操作,由DMAC发出读/写命令,直接控制RAM与I/O端口进行DMA传输。
- DMA结束:当完成规定的成批数据传送后,DMAC释放总线控制权,并向I/O端口发出结束信号。当I/O端口接收到结束信号后,停止I/O设备的工作并向CPU提出中断请求,使CPU执行一段检查本次DMA传输操作正确性判断的代码,并从不介入的状态退出。
由此可见,DMA无须CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件(DMAC)为RAM与I/O设备开辟了一条直接传送数据的通路,极大地提高了CPU效率。需要注意的是,DMA操作访问的必须是连续的物理内存。DMA 传输的过程如下图所示。
4、进程、线程和协程
4.1 什么是进程和线程
进程是什么呢?大白话讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。进程拥有代码和打开的文件资源、数据资源、独立的内存空间。
线程又是什么呢?线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。无论进程还是线程,都是由操作系统所管理的。线程一般具有五种状态:初始化>>>可运行>>>运行中>>>阻塞>>>销毁。线程不同状态之间的转化均需要CPU开销来完成。
4.2 什么是协程
协程英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
在Python语言中有个生成器的概念,里面有个关键字yield,当程序执行到yield关键字时,会暂停在那一行,等到主线程调用send方法发送了数据,协程才会接到数据继续执行。但是,yield让程序暂停,和线程的阻塞是有本质区别的。通过yield关键字的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。大家可以在Python脚本中写入如下代码并执行体验下:
- def consume():
- while True:
- # consume等待接收数据
- number = yield
- print("我要执行啦。。。。开始计数:",number)
- consumer = consume()
- next(consumer)
- for num in range(0,100):
- print("开始执行:",num)
- consumer.send(num)
协程Python代码
【本文为51CTO专栏作者“移动Labs”原创稿件,转载请联系原作者】