《See MIPS Run Linux》 读书笔记

这里面写的是我研读《See MIPS Run Linux》这本书时的一些读书笔记,对书上的一些疑点有比较详细的阐述。还算草稿性质,比较乱,以后有时间再来整理:)

*现在MIPS世界指令集统一标准为MIPS32/64 R1和R2(Release 2),不再沿用以前所谓的R4K, R24K等芯片命名方式和MIPS I, II, III, IV, V这种指令集方式。

*MIPS指令特点:
1。所有指令都是32位编码;
2。有些指令有26位供目标地址编码;有些则只有16位。因此要想加载任何一个32位值,就得用两个加载指令。16位的目标地址意味着,指令的跳转或子函数的位置必须在64K以内(上下32K);
3。所有的动作原理上要求必须在1个时钟周期内完成,一个动作一个阶段;
4。有32个通用寄存器,每个寄存器32位(对32位机)或64位(对64位机);
5。本身没有任何帮助运算判断的标志寄存器,要实现相应的功能时,是通过测试两个寄存器是否相等来完成的;
6。所有的运算都是基于32位的,没有对字节和对半字的运算(MIPS里,字定义为32位,半字定义为16位);
7。没有单独的栈指令,所有对栈的操作都是统一的内存访问方式。因为push和pop指令实际上是一个复合操作,包含对内存的写入和对栈指针的移动;

*MIPS指令的五级流水线:每条指令都包含五个执行阶段。
第一阶段:从指令缓冲区中取指令。占一个时钟周期;
第二阶段:从指令中的源寄存器域(可能有两个)的值(为一个数字,指定$0~$31中的某一个)所代表的寄存器中读出数据。占半个时钟周期;
第三阶段:在一个时钟周期内做一次算术或逻辑运算。占一个时钟周期;
第四阶段:指令从数据缓冲中读取内存变量的阶段。从平均来讲,大约有3/4的指令在这个阶段没做什么事情,但它是指令有序性的保证(为什么是保证,我还没看清楚?)。占一个时钟周期;
第五阶段:存储计算结果到缓冲或内存的阶段。占半个时钟周期;
*所以一条指令要占用四个时钟周期;

*?书上P27, P28画的示意图有点问题,感觉不是很恰当;我自己画了一个。

*由于MIPS固定指令长度,所以造成其编译后的二进制文件和内存占用空间比x86的要大,(x86平均指令长度只有3个字节多一点,而MIPS是4个字节)

*寻址方式:
1。只有一种内存寻址方式。就是基地址加一个16位的地址偏移;

*内存中的数据访问必须严格对齐(至少4字节对齐);

*硬跳转指令只有26位目标地址,再加上2位的对齐位,可寻址28位的空间,即256M。意思即是说,在一个C程序内,goto语句只能跳转到它之前的128M和之后的128M这个地址空间之内;
*条件分支指令只有16位跳转地址,加上2位的对齐位,共18位寻址空间,即256K。意思即是说,在一个C程序内,if语句只能跳转到它之前的128K和之后的128K这个地址空间之内;

*MIPS默认不把子函数的返回地址(就是调用函数的受害指令地址)存放到栈中,而是存放到$31寄存器中;这对那些叶子函数有利。如果遇到嵌套的函数的话,有另外的机制处理。

*流水线效应。由于采用了高度的流水线,结果产生了一些对程序员来说可见的效应,需要注意。最重要的两个效应就是分支延迟效应和载入延迟效应。
1。任何一个分支跳转语句后面的那条语句叫做分支延迟槽。实际上在程序执行到分支语句时,当他刚把要跳转到的地址填充好(到代码计数器里),还没完成本条指令,分支语句后面的那个指令就执行了。这是因为流水线效应,几条指令同时在执行,只是处于不同的阶段。具体看书上说提前半条指令执行,没看懂。分支延迟槽常用被利用起来完成一些参数初始化等相关工作,而不是被浪费了。
2。载入延迟是这样的。当执行一条从内存中载入数据的命令时,是先载入到高速缓冲中,然后再取到寄存器中,这个过程相对来说是比较慢的。在这个过程完成之前,可能已经有几条在流水线上的指令被执行了。这几条在载入指令后被执行的指令就被称作载入延迟槽。现在就有一个问题,如果后面这几条指令要用到载入指令所载入的那个数据怎么办?一个通用的办法是,把内部锁加在数据载入过程上,这样,当后面的指令要用这条指令时,就只有先停止运行(在ALU阶段),等这条数据载入指令完成了后再开始运行。

*MIPS的虚拟地址内存映射空间。
0x0000 0000 ~ 0x7fff ffff
用户级空间,2GB,要经MMU(TLB)地址翻译。kuseg。可以控制要不要经过缓冲。

0x8000 0000 ~ 0x9fff ffff
kseg0. 这块区域为操作系统内核所占的区域,共512M。使用时,不经过地址翻译,将最高位去掉就线性映射到内存的低512M(不足的就裁剪掉顶部)。但要经过缓冲区过渡。

0xa000 0000 ~ 0xbfff ffff
kseg1. 这块区域为系统初始化所占区域,共512M。使用时,不经过地址翻译,也不经过缓冲区。将最高3位去掉就线性映射到内存的低512M(不足的就裁剪掉顶部)。

0xc000 0000 ~ 0xffff ffff
kseg2. 这块区域也为内核级区域。要经过地址翻译。可以控制要不要经过缓冲。

*MIPS的协处理器
CP0:这是MIPS芯片的配置单元。必不可少,虽然叫做协处理器,但是通常都是做在一块芯片上。绝大部分MIPS功能的配置,缓冲的控制,异常/中断的控制,内存管理的控制都在这里面。所以是一个完整的系统所必不可少的。

*MIPS的高速缓冲
MIPS一般有两到三级缓冲,其中第一级缓冲数据和指令分开存储。这样的好处是指令和数据可以同时存取,提高效率。但缺点是提高了复杂度。第二级缓冲和第三级缓冲(如果有的话)就不再分开存放啦。

缓冲的单元叫做缓冲行。每一行中,有一个tag,然后后面接的是一些标志位和一些数据。缓冲行按顺序线性排列起来,就组成了整个缓冲。

缓冲行的索引和存取有一套完整的机制,另画图说明。

*MIPS的异常机制
精确异常的概念:在运行流程中没有任何多余效应的异常。即当异常发生时,在受害指令之前的指令被完全执行,而受害指令及后面的指令还没开始执行(注:说受害指令及后面的指令还没做任何事情是不对的,实际上受害指令是处于其指令周期的第三阶段刚完成,即ALU阶段刚完成)。精确异常有有助于保证软件设计上不受硬件实现的影响。

CP0中的EPC寄存器用于指向异常发生时指令跳转前的执行位置,一般是受害指令地址。当异常时,是返回这个地址继续执行。但如果受害指令在分支延迟槽中,则会硬件自动处理使EPC往回指一条指令,即分支指令。在重新执行分支指令时,分支延迟槽中的指令会被再执行一次。

精确异常的实现对流水线的流畅性是有一定的影响的,如果异常太多,系统执行效率就会受到影响。

*异常又分常规异常和中断两类。常规异常一般为软件的异常,而中断一般为硬件异常,中断可以是芯片内部,也可以是芯片外部触发产生。

异常发生时,跳转前最后被执行的指令是其MEM阶段刚好被执行完的那条指令。受害指令是其ALU阶段刚好执行完的那条指令。

异常发生时,会跳到异常向量入口中去执行。MIPS的异常向量有点特殊,它一般只个2个或几个中断向量入口,一个入口给一般的异常使用,一个入口给TLB miss异常使用(这样的话,可以省下计算异常类型的时间。在这种机制帮助下,系统只用13个时钟周期就可以把TLB重填好)。

CP0寄存器中有个模式位,SR(BEV),只要设置了,就会把异常入口点转移到非缓冲内存地址空间中(kseg1)。

MIPS系统把重启看作一个不可回归的异常来处理。
冷启动:CPU硬件完全被重新配置,软件重新加载;
热启动:软件完全重新初始化;

MIPS对异常的处理的哲学是给异常分配一些类型,然后由软件给它们定义一些优先级,然后由同一个入口进入异常分配程序,在分配程序中根据类型及优先级确定该执行哪个对应的函数。这种机制对两个或几个异常同时出现的情况也是适合的。

下面是当异常发生时MIPS CPU所做的事情:
1。设置EPC指向回归的位置;
2。设置SR(EXL)强迫CPU进入kernel态,并禁止所有中断响应。
3。设置Cause寄存器,以使软件可以得到异常的类型信息;还有其它一些寄存器在某些异常时会被设置;
4。CPU开始从异常入口取指令,然后以后的所有事情都交由软件处理了。

k0和k1寄存器用于保存异常处理函数的地址。
异常处理函数执行完成后,会回到异常分配函数那去,在异常分配函数里,有一个eret指令,用于回归原来被中断的程序继续执行;eret指令会原子性地把中断响应打开(置SR(EXL)),并把状态级由kernel转到user级,并返回原地址继续执行。

*中断
MIPS CPU有8个独立的中断位(在Cause寄存器中),其中,6个为外部中断,2个为内部中断(可由软件访问)。一般来说,片上的时钟计数/定时器,会连接到一个硬件位上去。

SR(IE)位控制全局中断响应,为0的话,就禁止所有中断;
SR(EXL)和SR(ERL)位(任何一个)如果置1的话,会禁止中断;
SR(IM)有8位,对应8个中断源,要产生中断,还得把这8位中相应的位置1才行;

中断处理程序也是用通用异常入口。但有些新的CPU有变化。

*在软件中实现中断优先级的方案
1。给各种中断定优先级;
2。CPU在运行时总是处于某个优先级(即定义一个全局变量);
3。中断发生时,只有等于高于CPU优先级的中断优先级才能执行;(如果CPU优先级处于最低,那么所有的中断都可以执行);
4。同时有多个中断发生时,优先执行优先级最高的那个中断程序;

*大小端问题
见专图。

硬件上也有大端小端问题,比如串口通讯,一个字节一个字节的发,首先是低位先发出去。
还有显卡的显示,比如显示黑白图像,在屏幕上一个点对应显存中的一位,这时,这个位对应关系就是屏幕右上角那个点对应显存第一个字节的7号位,即最高位。第一排第8位点对应第一个字节的0号位。

*MIPS上的Linux运行情况

用户态和核心态:在用户态,不能随意访问内核代码和数据存放区,只能访问用户态空间和内核允许访问(通过某种机制)的内核页面。也不能执行CP0相关的指令。用户态要执行内核的某些服务,就得用系统调用(system_call),在系统调用的最后,是一个eret指令。

任何时候Linux都有至少一个线程在跑,Linux一般不禁止中断。发生中断时,其环境是从被中断线程借来的。

中断服务程序(ISR)应该短小。

MIPS Linux系统上半地址空间只能用内核特权级访问。内核不通过TLB地址翻译。

所有线程都共用相同的内核地址空间,但只有同一组线程才用同一个用户地址空间(指向同一个mm_struct结构)。

如果物理内存高于512M,那么不能用kseg0和kseg1来映射高于512M的内存部分。只能用kseg2来映射。kseg2要经过TLB。

从某个方面说,内核就是一组供异常处理函数调用的子程序。内核中,线程调度器就是这样一个小的子程序。由各个线程(异常处理程序也可以算作一个特殊的线程,换他书上的说法)调用。

MIPS Linux有异常模式,而x86上没有这个概念。

异常要小心操作。不是仅用软件锁就能解决的。

*原子操作
MIPS为支持操作系统的原子操作,特地加了一组指令 ll/sc。它们这样来使用:

先写一句
atomic_block:
ll XX1, XXX2
….
sc XX1, XXX2
beq XX1, zero, automic_block
….

在ll/sc中间写上你要执行的代码体,这样就能保证写入的代码体是原子执行的(不会被抢占的)。

其实,ll/sc两语句自身并不保证原子执行,但他耍了个花招:
用一个临时寄存器XX1,执行ll后,把XXX2中的值载入XX1中,然后会在CPU内部置一个标志位,我们不可见,并保存XXX2的地址,CPU会监视它。在中间的代码体执行的过程中,如果发现XXX2的内容变了(即是别的线程执行了,或是某个中断发生了),就自动把CPU内部那个标志位清0。执行sc时,把XX1的内容(可能已经是新值了)存入XXX2中,并返回一个值存入XX1中,如果标志位还为1,那么这个返回的值就为1;如果标志位为0,那么这个返回值就为0。为1的话,就表明这对指令中间的代码是一次性执行完成的,而不是中间受到了某些中断,那么原子操作就成功了;为0的话,就表明原子操作没成功,执行后面beq指令时,就会跳转到ll指令重新执行,直到原子操作成功为止。

所以,我们要注意,插在ll/sc指令中间的代码必须短小。

据经验,一般原子操作的循环不会超过3次:)

*系统调用 syscall
系统调用也通过异常入口进入系统内核,选择8号异常代码处理函数进行处理,进入系统调用分配函数后,还要根据传进来的参数再一次分配到具体的功能函数上去。系统调用传递参数是在寄存器中进行的。

系统调用号存放在v0中,参数存放在a0-a3。如果参数过多,会有另一套机制来处理。系统调用的返回值通常放在v0中。如果系统调用出错,则会在a3中返回一个错误号。

*异常入口点位于kseg0的底部,是硬件规定的。

*注:地址空间的0x0000 0000是不能用的,从0开始的一个或多个页不会被映射。

*内存分页映射有以下优点:
1。隐藏和保护数据;
2。分配连续的地址给程序;
3。扩展地址空间;
4。按需求载入代码和数据(通过异常方式);
5。便于重定位;
6。代码和数据在线程中共享,便于交换数据;

所有的线程是平等的,所有的线程都有自己的内存管理结构体;运行于同一地址空间的线程组,共享有大部分这种数据结构。在线程中,保存有本地址空间已经使用的页面的一个页表,用来记录每个已用的虚页与实际物理页的映射关系;

*ASID是与虚拟页高位配合使用。用于描述在TLB和Cache中的不同的线程,只有8位,所以最多只能同时运行256个线程。这个数字一般来说是够的。如果超过这个数目了,就要把Cache刷新了重新装入。所以,在这点上,与x86是不同的。

*MIPS Linux的内存驻留页表结构
用的是两级页表,一个页表目录,一个页表,页表中的每一项是一个 EntryLo0-1。
(这与x86方式类似)。而没有用MIPS原生设计的方案。

*TLB的refill过程-硬件部分
1。CPU先产生一个虚拟地址,要到这个地址所对应的物理地址上取数据(或指令)或写数据(或指令)。
低13位被分开来。然后高19位成为VPN2,和当前线程的ASID(从EntryHi(ASID)取)一起配合与TLB表中的项进行比较。(在比较过程中,会受到PageMask和G标志位的影响)
2。如果有匹配的项,就选择那个。虚拟地址中的第12位用于选取是用左边的物理地址项还是用右边的物理地址项。
然后就会考察V和D标志位,V标志位表示本页是否有效,D表示本页是否已经脏了(被写过)。
如果V=0,或D=1,就会引发翻译异常,BadVAddr会保存现在处理的这个虚拟地址,EntryHi会填入这个虚拟地址的高位,还有Context中的内容会被重填。
然后就会考察C标志位,如果C=1,就会用缓冲作中转,如果C=0,就不使用缓冲。
这几级考察都通过了之后,就正确地找到了那个对应的物理地址。
3。如果没有匹配的项,就会触发一个TLB refill异常,然后后面就是软件的工作了;

*TLB的refill过程-软件部分
1。计算这个虚拟地址是不是一个正确的虚拟地址,在内存页表中有没有与它对应的物理地址;如果没有,则调用地址错误处理函数;
2。如果在内存页表中找到了对应的物理地址,就将其载入寄存器;
3。如果TLB已经满了,就用random选取一个项丢弃;
4。复制新的项进TLB。

*MIPS Linux中标志内存页已经脏了的方式与x86不同。它要耍个把戏:
1。当一个可写的页第一次载入内存中时(从磁盘载入?载入的时候就分配一个物理页,同时就分配个对应的虚拟页,并在内存页表中添一个Entry),将其Entry的D标志位清0;
2。然后,当后面有指令要写这个页时,就会触发一个异常(先载入TLB中判断),我们在这个异常处理函数中把内存页表项中的标志位D置1。这样后面的就可以写了。并且,由于这个异常把标志位改了,我们认为这个物理页是脏的了。
3。至于TLB中已经有的那个Entry拷贝还要修改它的D标志位,这样这次写入操作才能继续入下进行。

*MIPS中的C语言参数传递机制?

*MIPS中的堆栈结构及在内存中的分布?

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License