此文章内容摘抄自计算机体系结构基础(胡伟武著),有些地方可能介绍的不是很详细,如果读者想仔细的了解,请自己阅读原著。
无论是在什么架构上,都有自己的指令系统,如x86采用的是复杂指令集,而ARM/PowerPc等架构均采用的是精简指令集,这里面提到的指令集就是所谓的指令系统。因此不同平台上的要实现同样功能的指令可能会大部相同。指令是介于软件和硬件之间的桥梁,对于一个程序员来说,熟悉机器最底层的指令,那么对编程来说是至关重要的。无论是对上层的应用软件还是操作系统层面的软件,经过编译器编译后,都会翻译成这个架构上对应的指令来运行。
处理器可以访问的地址空间包括寄存器空间和内存空间,而寄存器空间又包括通用寄存器,特殊寄存器和控制寄存器。寄存器空间通过指令以及编码于指令中的寄存器号来访问,系统内存空间通过访存指令来访问。而广义上的系统内存空间包括IO空间和内存空间,不同指令系统对系统内存空间的定义也各不相同。x86就严格区分IO空间和内存空间,访问IO空间是需要特点的指令来访问的,然后在MIPS和ARM架构中却没有严格的区分,把他们当作同一个系统内存空间进行访问和编址。一般访问这些地址都是使用load/store指令来访问。
当今指令系统主要是寄存器型-寄存器型的,主要是因为寄存器的访问速度快,便于编译器的调度优化,并且可以充分利用局部性原理大量的操作可以在寄存器中完成。
在指令访存指令的时候,必须烤炉一个问题,那就是访存地址是否对齐,和指令系统是否支持不对齐访问。所谓对齐访问是指对数据访问的起始地址和结束地址都符合其数据长度。例如访问一个4字节数,其访问地址的低两位都应该为0.也就是地址应该是4的倍数。若支持不对齐访问,硬件需要完成数据的才分和合并,不支持不对齐访问的架构相对来说就缺少了一些灵活性。x86是支持不对齐访问的,但是ARM和MIPS上是不支持不对齐访问的,不对齐的访问将产生异常。
另一个于访问地址相关的问题就是尾端的问题,不同架构采用的尾短不同,不同的尾端也带来了兼容性的问题。X86系统采用的是小段系统,而ARM和MIPS同时支持大端和小端两种模式。
偏移量寻址,立即数寻址和寄存器间接寻址是最常用的寻址方式,MIPS指令集主要支持上面三种寻址方式。
从功能上看,指令大致可以分为四类:
像C语言这种高级语言,都必须经过编译器将高级语言转换为汇编语言,然后再由汇编语言转换为指令码才能在机器上运行。过程调用是高级语言的一个关键特性,他可以让特定程序端的内容和数据分离,过程接受参数输入,并通过参数返回执行结果。过程调用过程中,调用者和被调用这之间必须接受接口约定,包括寄存器使用,栈的使用和参数传递的约定等。在MIPS指令集中负责函数调用的指令是JAL,属于绝对跳转指令,该指令在跳转的同时,将其下一条指令(延迟槽除外)的地址放入31号通用寄存器(RA)中,作为函数的返回地址。负责函数返回的指令是JR RA 属于间接跳转。除了调用和返回指令外,函数调用和执行的过程中,还需要执行一下操作:
从异常的来源来分,异常分为以下几种:
中断在外部事件想要获取CPU时产生,由于外部事件的不可控性,中断处理程序所使用的事件就比较关键。在嵌入式系统中,CPU的主要作用之一就是处理外设相关的事物,因此中断发生的数量很多也非常重要。与X86类似,MIPS处理器也包含两类中断输入,一类是可屏蔽的中断(INT),另一类就是不可屏蔽的中断(NMI)。可屏蔽中断可通过协处理器寄存器SR来进行屏蔽,而不可屏蔽中断无法屏蔽。MIPS处理器定义了8个可屏蔽中断输入,通常5-6个来用于CPU外部,其他用于CPU内部的时钟中断和软件中断。注意,在MIPS处理器中NMI的中断入口地址是和重启是一致的,也就是说没有特定的处理,出发NMI中断就会导致系统重启。NMI的触发通常来源于硬件预设的致命的错误。MIPS在内的许多指令系统都将中断甚至异常都一视同仁,但是不同具体来源的中断是有优先级区分的,在使用的时候可以通过软件来实现中断优先级的方案。
mft0 t0,SR
1:
or t0,
and t0,
2:
mtc0 t0,SR
这段程序本身也可能被打断,若在标号1和2之间被中断,且中断处理程序修改了SR寄存器的值,则在返回时该中断处理程序对SR的写就会被这段程序覆盖。若不想让这种情况发生,就需要保证在整个对SR寄存器读写的过程中,保证其原子性。保证原子性的方法有很多种,例如使用专门的原子指令,在程序执行时禁用中断,不允许中断处理程序修改SR,或者使用通用的方法保证程序段的原子性,即将SR作为临界区来考虑。
要保证上面的原子性,通常有两种方法来实现,一种是信号量的方法,一种是“测试并设置的方法”,这种方法就是在没有原子保障的情况下运行程序,但是只在原子的运行的情况下的“设置”才会生效,软件只需要知道是否设置成功,如果不成功可以进行重新设置。
MIPS就是采用的这种第二种的方法,使用LL(Load Link)和SC(Store Conditional)指令来完成原子操作。由硬件维护一个LLbit,在LL指令访问某一个内存地址的时候,若该地址被改写则修改LL bit,执行SC指令时检查LL bit来确认原子性。
传统的MIPS指令系统中,中断处理程序的入口是一个固定的地址,也就是通常所说的异常入口地址。在异常入口的中断处理程序中再进一步对中断源进行划分和处理。一般来说集中式的处理方式已经足够简介而有效,而在MIPS32R2规范中,新增了向量化中断和EIC模式中断,可以减少某些常用中断处理过程中区分中断源的代价。打开向量化中断功能的时候,8个可屏蔽中断将拥有各自独立的入口地址。InCtl(VS)域可定义这些入口地址之间的间隔,如果间隔设为0,就使用同一个入口地址。
X86指令系统则原生支持更为完备的向量化中断方案,x86规定在地址空间的特定位置存放中断向量表(IVT 实模式下默认为0的地址)或中断描述符(IDT 保护模式),中断向量表中存放中断入口地址和偏移量,中断描述符还包含特权等级和描述符类别信息,x86的向量化中断机制最多可支持256个中断和异常,0-19为系统预设的异常和NMI,20-31是Intel保留的编号,32号开始是可用于外部的中断编号。
中断中系统中的中断源传送到处理器的机制主要有两种:
中断额生命周期从一个真正的外部时间开始,比如你在键盘上按下空格,键盘将空格编码发送到PS2控制器,PS2控制器收到并发送一个中断到CPU(这中间可能经过多级中断控制器),CPU接收到中断输入,将中断状态附在流水线的某一条指令上,当这条指令提交后,将触发异常处理过程。CPU设置控制器SR的EXL位并从异常处理入口开始取指令。此后的中断被屏蔽并进入核心态,PC跳转至0x80000180开始进行异常处理。首先异常处理程序检查cause寄存器,当发现ExcCode为0时,检查中断状态位IP7-2,再依据处理器的定义查询可能的各级中断控制器,最终知道中断的来源是来自键盘,查表得到键盘对应的中断号。在进行更多的程序调用前,要将寄存器的值进行保存从而支持被中断程序运行状态的恢复。需要保存的寄存器除了通用寄存器外,还需要保存SR,EPC,Cause等控制寄存器。完成保存后,清楚CPU的中断使能并进行更通用的中断处理过程。首先是do_IRQ(),这是一个处理相关的C语言程序,具有标准的结构流程。do_IRQ()传递一个全部由寄存器值组成的结构体作为参数,其任务是选择性的禁用对应的中断,并清除各层级的中断控制寄存器的中断状态,告知硬件系统已经开始处理相应的中断。若有中断服务程序注册在对应的中断号上,do_IRQ()调用handle_IRQ_event()。不同的中断处理程序所需要的时间是不同的,当中断处理时间较长的时候,可能在handle_IRQ_event()中重新使能一些优先级高的中断源。完成中断处理后,handle_IRQ_event()再次禁用中断,并返回do_IRQ(),在进行处理相关的操作后,调用ret_from_intr()返回。在执行ERET返回前,操作系统检查是否需要借此进行进程调度。若不需要进行进程调度。则回复寄存器的值并返回。若中断服务程序所需要的处理时间较长,即便使能了异常嵌套。操作系统仍然无法进行进程调度,长期停留在进程的上下文中是不对的。解决这个问题的方案是将中断过程分为上下两个部分。即我们所知道的中断上半部和中断下半部。中断上半部主要处理清楚中断状态等紧急的事物,并将后续的处理方案通知响应中断的进程。中断的下半部有三种实现方法:分别为软中断,tasklet和工作队列。软中断和tasklet紧接着上半部执行,无法祖塞和睡眠。工作队列由专门的工作队列进程进行处理,可以进行休眠和阻塞。
系统调用是操作系统内核为用户态程序实现的子程序,Linux操作系统中部分系统调用只返回一些内核知道但是用户不知道的信息。系统调用要满足安全性和兼容性两方面的要求。安全性方面在面对错误设置恶意的应用时,内核应该是健壮的,应该能保证自身的安全,兼容性方面操作系统内核应该能够运行兼容指令系统的应用程序。也就要求系统调用是兼容的,轻易移除一个系统调用是无法接受的。
系统调用运行在内核态,具有很高的权限。因此调用的入口应该被严格控制以保证安全性。在MIPS指令系统中,系统调用作为一种异常处理,由SYSCALL指令触发,在协处理器寄存器Cause的ExcCode域反应为特定的值。不同系统调用号不同,但是调用好的定义与具体指令系统有关,x86和MIPS对同一函数的调用号,可能不同。
一般的系统调用也是需要传递参数的,在参数传递过程中要尽量使用寄存器来传递,这样可以避免在用户态和内核态之间避免一些不必要的复制。系统调用参数有以下约定:
多任务是现代操作系统的关键特点之一,在系统中存在多个进程,每个进程又包含多个执行的线程。在linux系统中某一个线程正在操作的数据很可能也在被另一个线程访问,并发访问的线程可能有以下来源:
处理器的存储管理单元,简称MMU,支持虚实地址的转换,多进程空间等功能,是处理器关键的单元之一,也是处理器和操作系统联系最紧密的部分之一。本章主要介绍MIPS架构上存储管理单元的工作原理,在其他的架构上其实原理都大同小异。
存储管理构建虚拟内存地址,并通过MMU进行虚拟地址到物理地址的转换。其意义主要有:
隐藏和保护
用户态程序只能访问kuseg段的内存内的数据,其他区域的数据只能由内核态访问。引入存储管理后,不同程序仿佛在独立的使用kuseg段的地址,互相之间并不影响。此外,分页的管理办法对每个页都有单独的写保护,内核态的操作系统可以防止用户态的程序修改内核态的代码和数据。
为程序分配连续的内存空间
MMU可以由分散的物理页构建连续的虚拟地址空间,以页为单元管理物理内存。
扩展地址空间
在MIPS中kseg0和kseg1都映射到物理内存额低256M,在32位体系中,若要访问所有4G物理内存空间,则必须使用MMU来进行转换。
节约物理内存,减小内存碎片。
MIPS指令中的TLB异常有三种类型,分别为:
上面的三种异常中,重填异常有专门的程序入口地址,是因为他发生的异常最为频繁,最异常处理程序的性能要求较高。发生TLB无效异常的时候,需要从外存取物理页到内存,发生TLB修改异常的时候,操作系统会杀掉进行非法访问的进程。
程序在某一个指令系统上运行的时候,必须遵循特定的约定,这个约定就被称作为ABI(Application Binary Interface,应用程序二进制接口)。ABI通常包含寄存器的使用,函数调用规范,数据表示格式等。
MIPS指令系统流行的ABI主要有一下三种:
在函数调用规范方面,N32与N64与O32的区别不大,主要的变化在数据宽度和寄存器使用方面,下面就罗列介绍MIPS N32 ABI在函数调用方面的规定。
首先是参数类型方面:
所有整数参数都会经过扩展,字节,半字节等会以符号位(对有符号数)或者0(对无符号数)进行扩展,并放在一个单独的寄存器中。O32 扩展到32位,N32与N64扩招到64位。
O32和N32中所有指针和地址都是32位对象,而N64是64位的。
浮点参数使用单精度或双精度取决于ANSI C的定义规则。
参数槽的大小为64位双字,即使参数类型小于64位。
详细分析函数调用过程,具有一下特性:
栈区域都是4字节对齐,O32中则是双字对齐。这意味着栈指针和寄存器SP的值也有同样的要求。
最多8个整数寄存器(4-11)可以用作参数的传递使用,在O32中只是用4个(4–7)。
最多8个浮点寄存器(f12–f19)可作为参数传递使用。在O32中也是只使用4个(f12-f15),并且奇数号只能用作双精度参数的半部分。
若将左右参数视为一个大的结构体(每个参数域都是64位的倍数),参数寄存器可以视为这个结构体的前8个双字的影像,而且整数和浮点都是独立的影像。对应槽位是否有效,取决于参数的类型。
32位参数总是按符号扩展。
在64位的参数槽中,更小的标量参数右对齐(大尾端的时候放在最高位地址),浮点参数左对齐。
四精度浮点参数总是16字节对齐,传递时需要一堆浮点寄存器。
复数类型,参数传递通过浮点寄存器来进行传递。
struct 和union 或者其他的混合数据类型视为双字的序列。通过整数或者浮点寄存器传递。具体来说,struct视为64位数据块的序列。union无论是单独传递还是作为struct域中的一部分,都视为整数双字序列。
只要可能浮点参数都通过浮点寄存器来传递。
当参数类型不定的时候,不定参数用作浮点时,只能通过整数寄存器传递。
超过8个双字的参数通过内存的栈进行传递。
函数返回值通过v0/v1寄存器或者f0/f1寄存器传递。
位置无关的代码(Postion-Independent Code 简称PIC)直接由编译器生成,不需要后期转化。
IO设备大部分都是具有特定功能的部件,不能当作简单的存储整列来处理。由于IO设备的底层控制比较复杂,一般由设备控制器控制。设备控制器会提供一组寄存器接口,寄存器的内容变化会引起设备控制器执行一系列复杂的动作。设备控制器的接口寄存器也被称为IO寄存器。处理器通过读写IO寄存器来访问设备,写入这些寄存器的数据,会被设备控制器解析成命令,因此有些情况下将处理器对IO寄存器的访问成为命令字。
为了访问IO寄存器,处理器必须能够寻址这些寄存器。IO从寄存器的寻址方式有两种,内存映射IO和特殊IO指令。内存映射IO是把IO寄存器的地址映射到内存地址空间中,这些寄存器就可以像访问正常的内存地址一样来访问。读写IO寄存器的指令就可以使用访存指令来完成。处理器需要通过它所处的状态来控制应用程序可以访问的地址空间,使其不能直接访问IO地址空间,从而保证应用程序不能直接操作IO设备。MIPS指令系统并没有特殊的指令来访问IO设备,因此采用这种方式来访问IO设备。但是在x86指令系统中,是有专门用于访问IO设备的指令来访问设备寄存器空间的。
处理器和IO设备间需要协同工作,这就需要同步。他们之间的同步方式有两种:查询和中断。
目前广泛使用的是中断的方式。
存储器和IO设备间需要进行大量的数据传输,例如在启动过程中需要将操作系统的代码从硬盘搬运到内存中,或者显卡的现实数据需要将数据从内存搬运到显示器的控制器中。那么存储器和IO设备间是如何进行数据传输的呢?
早期的传输方式主要是通过CPU来完成的,由于存储器和IO设备间没有直接的数据通路,当需要进行数据搬运的时候,处理器就负责之间的通信工作。这种方式被称为PIO(Programming Input Output)模式。PIO方式的特点是数据要经过处理器内部的通用寄存器来进行中转,中转不仅影像处理器的执行,也降低了数据传输的效率。因此就想在IO设备和存储器之间开辟一条数据通路来,专门用于传输数据。这样CPU就可以从中解放出来去做别的事。开辟的这条同路就是现在的DMA通道,这就是现在广泛使用的DMA(Direct Memory Access 直接存储访问)。DMA的确切意义在于:在存储器和外设之间建立了直接的数据传输通道,数据传送由专门的硬件来控制。DMA是由DMA控制器来控制的。
中断本质上是IO设备对处理器发出的一个信号,让处理器知道此时有数据传输或者已经发生数据传输。中断的一般过程为: