读书笔记之计算机体系结构

知识要点

此文章内容摘抄自计算机体系结构基础(胡伟武著),有些地方可能介绍的不是很详细,如果读者想仔细的了解,请自己阅读原著。

第二章 指令系统

无论是在什么架构上,都有自己的指令系统,如x86采用的是复杂指令集,而ARM/PowerPc等架构均采用的是精简指令集,这里面提到的指令集就是所谓的指令系统。因此不同平台上的要实现同样功能的指令可能会大部相同。指令是介于软件和硬件之间的桥梁,对于一个程序员来说,熟悉机器最底层的指令,那么对编程来说是至关重要的。无论是对上层的应用软件还是操作系统层面的软件,经过编译器编译后,都会翻译成这个架构上对应的指令来运行。

指令的设计原则

  • 兼容性
  • 通用性
  • 高效性
  • 安全性

第三章 指令集结构

地址空间

处理器可以访问的地址空间包括寄存器空间和内存空间,而寄存器空间又包括通用寄存器,特殊寄存器和控制寄存器。寄存器空间通过指令以及编码于指令中的寄存器号来访问,系统内存空间通过访存指令来访问。而广义上的系统内存空间包括IO空间和内存空间,不同指令系统对系统内存空间的定义也各不相同。x86就严格区分IO空间和内存空间,访问IO空间是需要特点的指令来访问的,然后在MIPS和ARM架构中却没有严格的区分,把他们当作同一个系统内存空间进行访问和编址。一般访问这些地址都是使用load/store指令来访问。

指令的类型

  • 堆栈型
  • 累加器型
  • 寄存器-存储器型
  • 寄存器-寄存器型

当今指令系统主要是寄存器型-寄存器型的,主要是因为寄存器的访问速度快,便于编译器的调度优化,并且可以充分利用局部性原理大量的操作可以在寄存器中完成。

访存地址

在指令访存指令的时候,必须烤炉一个问题,那就是访存地址是否对齐,和指令系统是否支持不对齐访问。所谓对齐访问是指对数据访问的起始地址和结束地址都符合其数据长度。例如访问一个4字节数,其访问地址的低两位都应该为0.也就是地址应该是4的倍数。若支持不对齐访问,硬件需要完成数据的才分和合并,不支持不对齐访问的架构相对来说就缺少了一些灵活性。x86是支持不对齐访问的,但是ARM和MIPS上是不支持不对齐访问的,不对齐的访问将产生异常。

尾端

另一个于访问地址相关的问题就是尾端的问题,不同架构采用的尾短不同,不同的尾端也带来了兼容性的问题。X86系统采用的是小段系统,而ARM和MIPS同时支持大端和小端两种模式。

寻址方式

  • 寄存器寻址
  • 立即数寻址
  • 偏移量寻址
  • 寄存器间接寻址
  • 变址寻址
  • 绝对寻址
  • 存储器间接寻址
  • 自增量寻址
  • 自减量寻址
  • 比例变量寻址

偏移量寻址,立即数寻址和寄存器间接寻址是最常用的寻址方式,MIPS指令集主要支持上面三种寻址方式。

指令操作和编码

从功能上看,指令大致可以分为四类:

  • 运算指令(加减乘除,移位,逻辑运算等)
  • 访存指令(load/store)
  • 转移指令(j/jr)等跳转指令
  • 特殊指令 (用于访问特殊的寄存器的指令)

C语言的过程调用

像C语言这种高级语言,都必须经过编译器将高级语言转换为汇编语言,然后再由汇编语言转换为指令码才能在机器上运行。过程调用是高级语言的一个关键特性,他可以让特定程序端的内容和数据分离,过程接受参数输入,并通过参数返回执行结果。过程调用过程中,调用者和被调用这之间必须接受接口约定,包括寄存器使用,栈的使用和参数传递的约定等。在MIPS指令集中负责函数调用的指令是JAL,属于绝对跳转指令,该指令在跳转的同时,将其下一条指令(延迟槽除外)的地址放入31号通用寄存器(RA)中,作为函数的返回地址。负责函数返回的指令是JR RA 属于间接跳转。除了调用和返回指令外,函数调用和执行的过程中,还需要执行一下操作:

  • 调用者将实参放入寄存器或者堆栈中
  • 使用JAL指令调用被调用函数R
  • R在栈中分配自己所需要的局部变量空间
  • 执行R过程
  • R释放局部变量的空间,将栈指针还原
  • R使用JR指令返回调用者

第四章 异常和中断

异常的分类

从异常的来源来分,异常分为以下几种:

  • 外部事件(也称中断)
  • 指令执行中的错误
  • 数据完整性问题
  • 地址转换异常
  • 系统调用和陷入
  • 需要软件修正的运算

异常的处理流程

  • 异常处理准备
    当异常发生,CPU转而执行异常处理前,硬件需要执行一些列准备。这里涉及到一个叫做精确异常的概念,指发生任何异常的时,被异常打断的指令(EPTR)前的所有指令都执行完,而EPTR后的指令都像没有执行一样。实现精确异常的处理器中,异常处理程序可忽略因处理流水线带来的异常发生位置问题。EPTR存放的位置因不同指令集不同而不同,MIPC存放在EPC寄存器中,PowerPc存放于SRR0/CSRR0,X86则用栈存放CS和EIP的组合。
  • 确定异常来源
    处理器将异常进行编号,以便于异常处理程序进行区分哥跳转。X86用硬件进行异常和中断好的查询,并根据编号查询预设好的中断描述符得到不同异常处理程序的入口,并将CS/EIP等压入栈中。而MIPS将异常相关状态存放于Cause寄存器,由异常处理程序进一步查询和区分处理。但对存储管理使用的地址转换异常等频繁发生的异常设置了专用的异常处理程序入口地点。
  • 保存执行状态
    在异常处理前,要先将被打断的程序的状态进行保存,通常至少需要将通用寄存器和程序状态字寄存器的值保存到栈中。同时需要关闭或者保留部分中断使能,防止异常处理过程中产生新的中断影响程序执行状态。
  • 处理异常
    跳转到异常处理程序的入口去执行。在异常处理的过程中,又有新的异常产生,这时就会出现异常嵌套的问题,产生异常嵌套时需要保存当前的异常处理程序的状态,这会消耗一定的栈资源,因此无限的嵌套下去是无法容忍的。异常嵌套是基于优先级的,只有优先级更高的中断到来才能打断当前执行的异常处理程序。而系统支持的最大的异常嵌套的最大层数就是系统支持的优先级数。
  • 恢复执行状态并返回

中断

中断在外部事件想要获取CPU时产生,由于外部事件的不可控性,中断处理程序所使用的事件就比较关键。在嵌入式系统中,CPU的主要作用之一就是处理外设相关的事物,因此中断发生的数量很多也非常重要。与X86类似,MIPS处理器也包含两类中断输入,一类是可屏蔽的中断(INT),另一类就是不可屏蔽的中断(NMI)。可屏蔽中断可通过协处理器寄存器SR来进行屏蔽,而不可屏蔽中断无法屏蔽。MIPS处理器定义了8个可屏蔽中断输入,通常5-6个来用于CPU外部,其他用于CPU内部的时钟中断和软件中断。注意,在MIPS处理器中NMI的中断入口地址是和重启是一致的,也就是说没有特定的处理,出发NMI中断就会导致系统重启。NMI的触发通常来源于硬件预设的致命的错误。MIPS在内的许多指令系统都将中断甚至异常都一视同仁,但是不同具体来源的中断是有优先级区分的,在使用的时候可以通过软件来实现中断优先级的方案。

中断的优先级和原子性

  • 软件随时维护一个中断优先级(IPL)每个中断源都被赋予特定的优先级
  • 正常状态下CPU运行在最低的优先级,此时任何中断都可以触发
  • 当处于最高优先级的时候,任何中断源都被禁止
  • 更高优先级的中断发生时,可以抢占当前的低优先级的处理过程。
    MIPS指令系统中,所有中断先关的控制都要通过协处理器SR来进行,因此软件中断优先级方案必然带来对SR的访问和修改。而SR本身无法直接进行位设置或者改写,需要特定的程序来操作。如下面的程序
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号开始是可用于外部的中断编号。

中断的传送机制

中断中系统中的中断源传送到处理器的机制主要有两种:

  • 中断线(传统的中断方式)
  • 消息中断(MSI)

中断的生命周期

中断额生命周期从一个真正的外部时间开始,比如你在键盘上按下空格,键盘将空格编码发送到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对同一函数的调用号,可能不同。
一般的系统调用也是需要传递参数的,在参数传递过程中要尽量使用寄存器来传递,这样可以避免在用户态和内核态之间避免一些不必要的复制。系统调用参数有以下约定:

  • 调用号放在v0寄存器中
  • 参数传递遵循O32 ABI,通常最多4个参数通过a0-a3寄存器进行传递。
  • 返回值存放在v0寄存器(少数也会使用v1寄存器),失败的系统调用会返回一个状态到a3寄存器(0表示正常)
  • 类似与过程调用,系统调用需要存储部分寄存器如s0-s7的值。
    为了保证内核的安全性,数组索引指针和缓冲区长度等可能导致溢出攻击的参数都必须被检查。在从用户空间复制数据时,应用程序提供的指针可能是无效的,从而导致内核崩溃,因此与用户空间相关的操作都应该使用专用的copy_to_user()和copy_from_user()来完成。此时如果用户程序传递了无效的地址,内核就会触发异常,异常处理程序会进行错误处理,而内核不会发生崩溃。与异常一样,系统调用在返回时使用ERET指令来完成内核态向用户态的跳转。

同步与通信

多任务是现代操作系统的关键特点之一,在系统中存在多个进程,每个进程又包含多个执行的线程。在linux系统中某一个线程正在操作的数据很可能也在被另一个线程访问,并发访问的线程可能有以下来源:

  • 另一个CPU核上的线程,这是真正的多核处理系统。
  • 处于中断上下文的线程,中断处理程序打断当前线程的执行
  • 因调度而抢占的另一个线程,中断处理后调度而来的其他的内核线程。
    当线程之间出现资源访问的冲突时,需要同步和通信来保证并发数据的正确性。
    同步的机制主要分为两类:
  • 基于互斥的同步机制(主要是锁,互斥所,信号量,自旋锁等)
    缺点:
    1)若持有锁的线程死亡,阻塞或者死循环,则其他等待锁的线程将永远等待下去
    2)即使冲突的情况很少,锁机制也有获得锁和释放锁的代价
    3)锁导致的错误与时机有关,很难复现
    4)持有锁的线程因时间片中断或者页错误而被取消调度的时候,其他线程需要等待。
  • 非阻塞的同步机制(主要有事物内存和事件)

第五章存储管理

处理器的存储管理单元,简称MMU,支持虚实地址的转换,多进程空间等功能,是处理器关键的单元之一,也是处理器和操作系统联系最紧密的部分之一。本章主要介绍MIPS架构上存储管理单元的工作原理,在其他的架构上其实原理都大同小异。

存储管理的原理

存储管理构建虚拟内存地址,并通过MMU进行虚拟地址到物理地址的转换。其意义主要有:

  • 隐藏和保护
    用户态程序只能访问kuseg段的内存内的数据,其他区域的数据只能由内核态访问。引入存储管理后,不同程序仿佛在独立的使用kuseg段的地址,互相之间并不影响。此外,分页的管理办法对每个页都有单独的写保护,内核态的操作系统可以防止用户态的程序修改内核态的代码和数据。

  • 为程序分配连续的内存空间
    MMU可以由分散的物理页构建连续的虚拟地址空间,以页为单元管理物理内存。

  • 扩展地址空间
    在MIPS中kseg0和kseg1都映射到物理内存额低256M,在32位体系中,若要访问所有4G物理内存空间,则必须使用MMU来进行转换。

  • 节约物理内存,减小内存碎片。

TLB异常处理

MIPS指令中的TLB异常有三种类型,分别为:

  • TLB异常重填异常
    当TLB中没有相对应的表象的时候,就会触发TLB重填异常
  • TLB无效异常
    当相对应的物理页不存在的时候,就会触发这个异常
  • TLB修改异常
    是非法修改了只读的页所产生的异常

上面的三种异常中,重填异常有专门的程序入口地址,是因为他发生的异常最为频繁,最异常处理程序的性能要求较高。发生TLB无效异常的时候,需要从外存取物理页到内存,发生TLB修改异常的时候,操作系统会杀掉进行非法访问的进程。

第六章 软硬件协同

函数调用规范

程序在某一个指令系统上运行的时候,必须遵循特定的约定,这个约定就被称作为ABI(Application Binary Interface,应用程序二进制接口)。ABI通常包含寄存器的使用,函数调用规范,数据表示格式等。
MIPS指令系统流行的ABI主要有一下三种:

  • O32 来自传统的MIPS约定,广泛应用在嵌入式的工具链和32位的Linux中
  • N64 在64位处理器中广泛使用的新的正式的ABI,改变了指针和long型整数的宽度,并改变了寄存器使用的约定和参数传递的方式。
  • N32 在64位机器上执行的32位的程序,于N64的区别在于指针和long型整数的宽度为32位。

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地址空间,从而保证应用程序不能直接操作IO设备。MIPS指令系统并没有特殊的指令来访问IO设备,因此采用这种方式来访问IO设备。但是在x86指令系统中,是有专门用于访问IO设备的指令来访问设备寄存器空间的。

处理器和IO设备间的同步

处理器和IO设备间需要协同工作,这就需要同步。他们之间的同步方式有两种:查询和中断。
目前广泛使用的是中断的方式。

存储器和IO设备间的数据传输

存储器和IO设备间需要进行大量的数据传输,例如在启动过程中需要将操作系统的代码从硬盘搬运到内存中,或者显卡的现实数据需要将数据从内存搬运到显示器的控制器中。那么存储器和IO设备间是如何进行数据传输的呢?
早期的传输方式主要是通过CPU来完成的,由于存储器和IO设备间没有直接的数据通路,当需要进行数据搬运的时候,处理器就负责之间的通信工作。这种方式被称为PIO(Programming Input Output)模式。PIO方式的特点是数据要经过处理器内部的通用寄存器来进行中转,中转不仅影像处理器的执行,也降低了数据传输的效率。因此就想在IO设备和存储器之间开辟一条数据通路来,专门用于传输数据。这样CPU就可以从中解放出来去做别的事。开辟的这条同路就是现在的DMA通道,这就是现在广泛使用的DMA(Direct Memory Access 直接存储访问)。DMA的确切意义在于:在存储器和外设之间建立了直接的数据传输通道,数据传送由专门的硬件来控制。DMA是由DMA控制器来控制的。

DMA传输的过程

  • 处理器事先为DMA分配一定的地址空间,用来存放要传输的数据
  • 设置DMA控制器参数,这些参数包含设备标识,数据传输方向,内存中用于数据传输的源地址和目的地址,传输的字节数。
  • DMA控制器进行数据传输。DMA控制器发起对内存或者设备的读写操作。
  • DMA控制器向处理器发送一个中断,通知数据传输的结果。
  • 处理器完成本次DMA请求,可以开始新的DMA请求。
    DMA方式对于对于大量数据传输的高数设备是一个很好的选择。例如硬盘,网络,显卡,USB等设备都采用DMA。一个计算机系统中通常包含多个DMA控制器,比如有特定设备专用的SATA接口DMA控制器,USB接口的DMA控制器。也有通用的DMA控制器,可用于编程的源地址和目标地址之间进行数据的传输。

IO中断控制器

中断本质上是IO设备对处理器发出的一个信号,让处理器知道此时有数据传输或者已经发生数据传输。中断的一般过程为:

  • 中断源发出中断信号到中断控制器
  • 中断控制器产生中断请求给CPU
  • CPU发出中断响应,并读取中断类型码
  • CPU根据中断类型码来执行特定的中断服务。
  • CPU从中断服务程序返回,中断结束。

你可能感兴趣的:(读书笔记)