http://hi.baidu.com/comcat/blog/item/3cff7bddec14a1f176c6388a.html

1. 概述

MIPS 统称异常(同步事件)和中断(异步事件)为例外 (Exception)

引入异常则是为了解决处理器运行过程中的一些意外情形,比如执行流中有非法指令(无法被处理器辨识的指令),访问了没有映射的虚拟地址等等。

中断的引入则是提供一种 IO 设备请求处理器服务的一种通讯机制;往往是由外部设备主动发起(给处理器的中断引脚一个信号),处理器收到信号后跳转到特定入口处执行。

处理器运行在用户态的话,例外发生时,则会进行模式切换(即进入特权级,可以执行管理系统资源的特权指令,比如 mfc0/mtc0),因此中断和异常也可看作是一些事件(主动及意外)请求特权服务。因为现代操作系统都运行在特权级下,拥有并管理系统资源,因此中断和异常实际是请求操作系统服务。

因为例外发生时总会伴随模式切换,即进入特权模式,因此对于程序需要主动进入特权模式的情形(比如用户态程序通过系统调用请求操作系统服务),MIPS 引入一条 'syscall' 指令来触发系统调用例外,一旦该指令被执行,处理器即切换到特权级并进入“系统调用例外”

处理器的一个例外过程,多数情形下(非嵌套)其实就相当于一个伴随模式切换的过程调用。例外发生后,MIPS 在置 CP0 寄存器 Status[EXL] = 1,并将当前 PC 值存入 EPC (Exception PC,即指向发生例外的指令),就从当前指令流跳转到另外一个指令流,MIPS 硬件不负责保存上下文,因此软件先保存上下文,服务完成后恢复上下文,再执行指令 'eret' 例外返回(eret 所作的操作为:将 EPC/ErrorEPC 的值置入 PC,同时清除 CP0_Status 寄存器的 EXL 位)。MIPS 下,只要 Status 寄存器的 EXL (exception) 位为 1 即表示处理器进入例外,处于特权模式下。

首先看一个系统调用异常发生时的现场过程:

用户执行 'syscall' 指令,触发系统调用异常,MIPS 这样响应:

a. 将引起异常的 syscall 指令所在地址压入 EPC

b. 置 STATUS 的  EXL 位为 1(其他位不变)

c. 设置 CAUSE 的 ExcCode 位为 8,指明这是其他例外里的系统调用异常

d. 跳转到其它例外入口

CPU 的 STATUS[EXL] 被置为 1,这个意味着:

I. CPU 自动进入内核模式(忽略 STATUS[KSU] 位),禁止中断(忽略 STATUS[IE] 位)

II. TLB Refill 例外入口使用其他例外入口而非原 TLB Refill 入口

III. 当又出现新例外时,CPU 将不会重新设置EPC 和 CAUSE[BD]

一个中断过程:        特权级迁移  例外向量入口  异常类型、优先级  中断特殊性

由于 MIPS 版本众多,光官方的规范版本就有 MIPS32 R1, MIPS32 R2, MIPS64 R1, MIPS64 R2 四个版本,全覆盖地讨论实在没有必要,下文单核将以 Loongson 2E 为范例(类MIPS64 R1),多核将以目前使用较多、并且 Kernel Mainline 已有支持的 Cavium Octeon CN38XX 系列为例 (MIPS64 R2) 讨论。

2. 高优先级例外类型和入口

所谓高优先级例外,就是需要 MIPS 尽可能快地响应并执行例外处理函数的例外。

MIPS 高优先级例外有 4 类,分别为:

Vec0: 冷启动、热重启、非屏蔽中断 (Reset)

Vec1: TLB Refill

Vec2: Cache Error

Vec3: 其它例外

他们在 MIPS 上,有固定的入口地址,一旦例外发生,即跳转到这个地址,迅速处理。

Reset 类例外容易理解,一旦这类发生,将伴随系统启动,需要即刻响应。固定入口地址处,实际上就是 bootloader 的第一条指令,也是存放 bootloader 的 flash 之所在。

由于 MIPS 采用的是软件 TLB,即需要 OS 读取内存中的页表来填充 TLB。这个对系统的性能影响很大,因此也需要第一时间处理。

Cache 发生错误时,往往内部数据就不再可信,因此需要第一时间处理。

其他例外包括中断,系统调用,地址错误,非法指令等等例外都是

2.1 正常运行模式下例外入口

2.1.1 MIPS64 R1 情形

MIPS 设计时固定了四大类例外的入口,正常运行情形下(非初始化情形,BEV=0):

TLB Refill 时,PC 的值为 0xFFFF FFFF 8000 0000,对应物理地址 0x00 处,访问的数据会进入 Cache  (32 bit address space)

xTLB Refill 时,PC 的值为 0xFFFF FFFF 8000 0080,对应物理地址 0x80 处,访问的数据会进入 Cache (64 bit address space)

Cache Error 时,PC 的值为 0xFFFF FFFF a000 0100,对应物理地址 0x100 处,访问的数据不会进入 Cache

其它例外时,PC 的值为 0xFFFF FFFF 8000 0180,对应物理地址 0x180 处,访问的数据会进入 Cache

MIPS64 通常是用 xTLB Refill 例外入口,为 0xFFFF FFFF 8000 0080,其处理程序的体积最大为 0x80 字节(MIPS32 为 0x8000 0000,容量为 0x80 字节),loongson 2E 比较特殊,他的入口只用了 32 位地址空间的,始终为 0xFFFF FFFF 8000 0000

Cache Error 例外,因为 Cache 已不能相信,因此只能使用不走 Cache 的虚拟地址 0xFFFF FFFF a000 0100 置入 PC 中。其处理程序的体积最大为 0x80 字节

其它例外,入口在 0xFFFF FFFF 8000 0180,MIPS 的其他例外都进入此入口,由软件根据寄存器中的通用例外类型,进入到对应的处理函数。其处理程序的体积一般不超过 0x80 字节

2.1.2Cavium Octeon (MIPS64 R2) 情形

对于像 Cavium Octeon 这类多核处理器,他有 16 个 CPU 核,在一些特殊应用环境下(比如其中 1 个核跑一个 Linux 作为控制面,其他 15 个核,每个核只跑一个应用作为数据面),不同的核就需要不同的例外入口基地址。为了这个原因,MIPS 在 MIPS64 R2 里引入了 CP0_EBase 寄存器,用于存放例外入口的基地址。

MIPS64 R2 规定,CP0_EBase 只在 CP0_STATUS[BEV]=0 时,即在正常运行时有效,BEV=1 的上电启动状态,例外的基地址还是原来的地址。

CP0_EBase 的默认值为 0x8000 0000,32bit 的值在 64bit 下,高位自动扩展: 0xFFFF FFFF 8000 0000,这样就能和 MIPS64 R1 兼容,即使用户无视这个寄存器,原系统也能在 R2 上正常运行。

有了 CP0_EBase 后,不同的核就可以有自己的例外入口,这样就可以各自跑自己的系统,甚至只跑一个应用。

因此在 MIPS64 R2 下,BEV = 0 时只规定四个固定例外入口的偏移:

TLB Refill 时,偏移为 0x00 (32 bit address space)

xTLB Refill 时,偏移为 0x80 (64 bit address space)

Cache Error 时,偏移为 0x100

其它例外时,偏移为 0x180

实际的入口为 0xFFFF FFFF 0000 0000 + (cp0_ebase & 0x3FFF F000)

Cavium Octeon 上 Bootloader 将异常的基地址设为 0xFFFF FFFF 8000 1000

2.2 上电初始化时例外入口

下面这一段与 Linux Kernel 关系不大,但与 Bootloader 关系密切,请酌情选读:

对于 MIPS 刚上电的初始化阶段(CP0_STATUS[BEV]=1),因为 Cache 状态未知,不能使用,所以所有的例外入口虚地址都得使用数据不进入 Cache 的地址段。因此MIPS 规定,BEV=1 时,例外向量入口为:

冷启动、热重启、非屏蔽中断,入口为 0xFFFF FFFF BFC0 0000

TLB Refill,入口为 0xFFFF FFFF BFC0 0200

Cache Error,入口为 0xFFFF FFFF BFC0 0300

其它例外,入口为 0xFFFF FFFF BFC0 0380

特别地,冷启动、热重启、非屏蔽中断例外时,PC 的值都为 0xFFFF FFFF BFC0 0000 (MIPS32 为 0xBFC0 0000),即当发生这类例外时,处理器总是跳转到这个地址继续执行,其固定映射到的物理地址为 0x1FC0 0000 处。这个地址常常关联到放置 Bootloader 的内部flash,每次一 reset,就会触发这个例外,跳转到 0x1FC0 0000,去取 Bootloader 的第一条指令。

可以看到在初始化阶段 (CP0_STATUS[BEV]=1,bootloader 最初执行时),4 类例外处理程序的最大允许长度分别为:0x200, 0x100, 0x80, 通常不超过 0x80 字节

注意 BEV=1 的时间很短,一旦 bootloader 完成初始化,其就将 BEV 置为 0,进入系统正常运行模式

3. 其它例外的子类型

当发生的例外不是 Reset, TLB Refill, Cache Error 时,MIPS 进入其它例外入口,对这个其它例外的区分,即 MIPS 怎么告诉 OS, 系统调用是进系统调用的处理函数,地址错误是进另外的处理函数。MIPS 如何做的呢?

他是在 CP0 的 STATUS 寄存器里,保留了 5 位 (STATUS[6:2]) 作为 ExcCode,即例外 Code。不同的其它例外有不用的 ExcCode:

0        Int        中断

1        Mod       TLB 修改异常

2        TLBL       TLB 读异常

3        TLBS       TLB 写异常

4        AdEL       读地址错误异常

5        AdES       写地址错误异常

......

8        Sys         系统调用异常

......

31       保留

这样 OS 就可以用 ExcCode 作为一个索引值,去索引一个函数数组,啥样的例外进啥样的处理函数。至于他们怎么传递参数和返回值,后面详细讨论。

MIPS Linux 下,中断和系统调用的处理函数为 handle_int 和 handle_sys

4. 中断的特殊结构

MIPS CPU 收到硬件中断请求后,会跳到其他例外入口,此时 CP0_STATUS 的 ExcCode 的值为 0,此只能描述“有了中断”,尚不能确定中断来自哪个中断控制器亦或就是 CPU 内部的硬件中断,比如 Timer 中断或者 Performace Counter 溢出中断

因此 MIPS 把 CP0_CAUSE 的 15:8 位留给了 8 个中断,可以描述 8 个中断的到来 (Pending),对应位为 1,则表示相应中断到来:

Bit | Name | Meaning
15 | IP7    | Hardware interrupt 5
14 | IP6    | Hardware interrupt 4
13 | IP5    | Hardware interrupt 3
12 | IP4    | Hardware interrupt 2
11 | IP3    | Hardware interrupt 1
10 | IP2    | Hardware interrupt 0
09 | IP1    | Request software interrupt 1
08 | IP0    | Request software interrupt 0

相对应的,CP0_STATUS[15:8] 为 IM7 ~ IM0,是为 IP7 ~ IP0 的中断 Mask 位

IP1 ~ IP0 对应 CP0_CAUSE[9:8],留给软中断

IP2 ~ IP7 则给硬件中断

在 MIPS64 R1 里,IP7 留给 Timer 或者 Performace Counter,至于是哪个,实现时自己取舍。Loongson 2E 实现时,把 IP7 给了 Timer;Performace Counter 用 IP6。

而在 MIPS64 R2 里,IP7 可以同时给 Timer 和 Performace Counter。当 IP7 被置位时,OS 可以检查 CP0_CAUSE[30] 和 CP0_CAUSE[26],前者为 1,表示 TI (Timer Interrupt);位 26 为 1,则表示是 PCI (Performace Counter Interrupt)

通常来说, IP7 ~ IP2 会对应 6 个电气信号引脚。在 2E 上对应 Int#[5..0],当然 Int#[1:0] 保留未用

各外围设备都可以直接把中断请求线接到 IP2 ~ IP6 上,但现代系统上外围设备五花八门,种类繁多,几根线根本不够用,因此一般都是外围设备的中断先汇总到中断控制器,中断控制器再单独向 CPU 请求中断。

比如 Loongson 2E 上,一个类 8259A 的中断控制器(负责键盘、声卡、IDE 等设备的中断),链接到 IP5

北桥内的中断控制器(负责 PCI 上的中断,如网卡、USB 等)则链接到 IP2

Cavium Octeon 上 CIU0(负责 UART0/1, MailBox0/1, USB0, MII0, WorkQueue 等)链接到 IP2;CIU1(负责 UART2, USB1, MII1, NAND 等)连接到 IP3。16 个核,CIU0 都连接到每个核的 IP2,CIU1 也都连接到每个核的 IP3

不管 IP2, IP3 直接挂设备还是挂中断控制器,OS 都可以区分到底是哪个具体设备的中断,对中断控制器,只要访问一下中断控制器的内部状态寄存器,就能确定是哪个具体设备的中断

也可以多个设备/中断控制器接到同一个 IP2,如果同时发生,这时候 OS 处理时就要负责判断优先级了,哪个先执行哪个后执行。当然有人要说了,这样不就把中断控制器省略了吗?理论上是可以的,但当设备繁多到上百后,原来控制器硬件做的事都由软件来做对性能是一个不小的影响,而且可扩展性也是一个问题。微型系统上用用可以,大一点的系统就不合适了。

在 2E 上,一个用户敲击键盘的行为,中断流程是这样的:

1. 用户击键后,键盘控制器8042产生中断,这个中断链接到 (route) I8259A,并由 8259A 在CPU的中断引脚上发起中断请求

2. CPU 自动设置 CAUSE 的 ExcCode 位为0,IP5 为1,并跳转到其他例外入口 0xFFFFFFFF 80000180

3. 位于其他例外入口处的简单异常处理程序,根据 ExcCode 的值索引例外处理表(exception_handlers),获取到0号例外的处理程序是 handle_int,并跳转过去

4. handle_int 根据 CAUSE 之 IP 位的值跳转到中断控制器 8259A 相关的中断处理函数  i8259_irqdispatch()

5.  i8259_irqdispatch() 读取 8259A 之 In-Service Register (ISR, 注意与x86的差异,x86是由8259A主动将中断号送上数据总线的),通过简单的计算得到中断号,进而调用 do_IRQ 进入相应的键盘中断处理函数

类似 Cavium Octeon 的多核处理器,往往都在 CPU 内部实现一个中央中断控制器 (CIU),来负责所有中断的路由,哪一个设备中断路由到哪一个核,都是由中央中断控制器控制,并且是可配置的,一般 OS 也是能更改的,即 OS 可以根据当前核的中断负载情形,对中断路由进行动态调整。Cavium Octeon 的详细情形,后文讨论。

5. 摘要

四个高优先级例外,对应四个固定入口。前三个 Reset,TLB Refill,Cache Error 需要系统尽可能快地进入处理函数,因此他们的处理函数直接位于固定入口处。

所有其他例外共享第四个入口,统称一般例外 (General Exception)。他的处理函数区分具体哪个例外是通过 CP0_STATUS 的 ExcCode

中断例外的 ExcCode 为 0,他的处理函数区分具体的中断是通过 CP0_STATUS 的 IP7 ~ IP0,外围的中断控制器固定链接到其中的一位 IPn (n = 6 ~ 2),中断控制器收到中断请求时,他会向上传递这个请求,IPn 会被置 1。这样就能进入到具体的中断控制器的处理函数中,他再访问一下中断控制器内部的 ISR 就能获取到是哪个具体的设备,从而进一步进入设备中断处理函数。

1. 高优先级例外入口初始化

本文不涉及 Bootloader,因此 Reset 例外入口处的 Bootloader 我们就不讨论了。

多核环境下,所有核默认指向的例外入口基地址都是一样的,尽管可以修改各自核内部的 EBase 寄存器来改变例外入口基地址,但我们此处只讨论所有核运行在对称多处理模式(SMP) 下的情形,即所有核都使用同样的例外入口

1.1 Cache Error

先看看 Cache Error 时,Linux Kernel 都做了什么


1.1.1 Loongson 2E Cache Error

Cache Error 例外入口初始化,位于:

[arch/mips/mm/c-r4k.c]

void __init r4k_cache_init()

{

......

set_uncached_handler(0x100, &except_vec2_generic, 0x80);

......

}

因为 cache 错误时可以 cache 的 KSEG0 段不能用了,则 cache 错误例外处理函数位于 KSEG1 之0xFFFF FFFF A000 0000 + 0x100 处,长度最大为128 Bytes(32 条指令),处理函数为 except_vec2_generic,定义于:

[arch/mips/mm/cex-gen.S]
/*
 * Game over.  Go to the button.  Press gently.  Swear where allowed by
 * legislation.
 */
    LEAF(except_vec2_generic)
    .set    noreorder
    .set    noat
    .set    mips0
    /*
     * This is a very bad place to be.  Our cache error
     * detection has triggered.  If we have write-back data
     * in the cache, we may not be able to recover.  As a
     * first-order desperate measure, turn off KSEG0 cacheing.
     */
    mfc0    k0,CP0_CONFIG
    li  k1,~CONF_CM_CMASK
    and k0,k0,k1
    ori k0,k0,CONF_CM_UNCACHED      # 改变 KSEG0 为 Uncached
    mtc0    k0,CP0_CONFIG
    /* Give it a few cycles to sink in... */
    nop
    nop
    nop

j   cache_parity_error
    nop
    END(except_vec2_generic)

发生 Cache Error 后,Cache 已不再可信,因此原 KSEG0 不能再使用 Cache,首先改变其为 Uncached,然后进入真正的处理函数 cache_parity_error:

[arch/mips/kernel/traps.c]

asmlinkage void cache_parity_error(void)
{
    const int field = 2 * sizeof(unsigned long);
    unsigned int reg_val;

/* For the moment, report the problem and hang. */
    printk("Cache error exception:\n");
    printk("cp0_errorepc == %0*lx\n", field, read_c0_errorepc());
    reg_val = read_c0_cacheerr();
    printk("c0_cacheerr == %08x\n", reg_val);

printk("Decoded c0_cacheerr: %s cache fault in %s reference.\n",
           reg_val & (1<<30) ? "secondary" : "primary",
           reg_val & (1<<31) ? "data" : "insn");
    printk("Error bits: %s%s%s%s%s%s%s\n",
           reg_val & (1<<29) ? "ED " : "",
           reg_val & (1<<28) ? "ET " : "",
           reg_val & (1<<26) ? "EE " : "",
           reg_val & (1<<25) ? "EB " : "",
           reg_val & (1<<24) ? "EI " : "",
           reg_val & (1<<23) ? "E1 " : "",
           reg_val & (1<<22) ? "E0 " : "");
    printk("IDX: 0x%08x\n", reg_val & ((1<<22)-1));

#if defined(CONFIG_CPU_MIPS32) || defined(CONFIG_CPU_MIPS64)
    if (reg_val & (1<<22))
        printk("DErrAddr0: 0x%0*lx\n", field, read_c0_derraddr0());

if (reg_val & (1<<23))
        printk("DErrAddr1: 0x%0*lx\n", field, read_c0_derraddr1());
#endif
    panic("Can't handle the cache error!");
}

事实上,当出现 Cache Error 时,系统已经没法修复,只能打出一些可供判断的信息后 panic :(

另外 set_uncached_handler 定义于:

[arch/mips/kernel/traps.c]
/*  
 * Install uncached CPU exception handler.
 * This is suitable only for the cache error exception which is the only
 * exception handler that is being run uncached.
 */         
void __cpuinit set_uncached_handler(unsigned long offset, void *addr,
    unsigned long size)
{   
    unsigned long uncached_ebase = CKSEG1ADDR(ebase);
 
    if (!addr)
        panic(panic_null_cerr);
    
    memcpy((void *)(uncached_ebase + offset), addr, size);
}
               
其中 CKSEG1ADDR 的宏用于将虚址 ebase 转化为对应的 uncached 段的虚址 0xFFFF FFFF A000 0000。可以看到这个函数负责把例外处理函数except_vec2_generic 复制到 Cache Error 例外的入口 0xFFFF FFFFF A000 0100 处,即物理地址 0x100 处

1.1.2 Cavium Octeon Cache Error

Cavium Octeon 的 Cache 错误处理和 2E 大同小异,只是处理过程多了些鲁棒性

其Cache Error 例外入口初始化,位于:

[arch/mips/mm/c-octeon.c]

void __cpuinit octeon_cache_init(void)
{   
    extern unsigned long ebase;
    extern char except_vec2_octeon;

memcpy((void *)(ebase + 0x100), &except_vec2_octeon, 0x80);

octeon_flush_cache_sigtramp(ebase + 0x100);

......
}

将例外处理函数 except_vec2_octeon 复制到 ebase + 0x100 处,在 Cavium 上 ebase 为:

[arch/mips/cavium-octeon/setup.c]

uint32_t ebase = read_c0_ebase() & 0x3ffff000

实际就是 EBase 寄存器中的值,默认情形下,ebase = 0x8000 0000,ebase + 0x100 就是 0x8000 0180,即物理地址 0x100 处。因为使用 KSEG0 的虚拟地址访问,数据会被缓存,因此为了及时生效到内存,还要 flush 一下 cache。

另外在 64 位的处理器上使用 32 位的地址访问,处理器会自动将其扩展为 64 位,规则就是 32位高位符号扩展。0x8000 0180 最高位为 1,则会被扩展为 0xFFFF FFFF 8000 0180; 0x0000 0180 则会被扩展为 0x0000 0000 0000 0180

其中,Octeon 的 Cache Error 例外处理函数 except_vec2_octeon 定义于:

[arch/mips/mm/cex-oct.S]

/*
 * Handle cache error. Indicate to the second level handler whether
 * the exception is recoverable.
 */
    LEAF(except_vec2_octeon)
   
    /* due to an errata we need to read the COP0 CacheErr (Dcache)
     * before any cache/DRAM access  */

rdhwr   k0, $0        /* get core_id */
    PTR_LA  k1, cache_err_dcache
    sll     k0, k0, 3
    PTR_ADDU k1, k0, k1    /* k1 = &cache_err_dcache[core_id] */

dmfc0   k0, CP0_CACHEERR, 1
    sd      k0, (k1)
    dmtc0   $0, CP0_CACHEERR, 1     # 把 DCache_Error 寄存器的值存入 cache_err_dcache[core_id] 数组

/* check whether this is a nested exception */
    mfc0    k1, CP0_STATUS
    andi    k1, k1, ST0_EXL
    beqz    k1, 1f
    nop
    j   cache_parity_error_octeon_non_recoverable
    nop

/* exception is recoverable */
1:  j   handle_cache_err
     nop

END(except_vec2_octeon)

回顾一下,例外发生时 CPU 会将 STATUS[EXL] 置为 1,但是在 Cache Error 例外出现时,EXL 不会被置为 1,CPU 而是将 STATUS[ERL] 置为 1,这个意味着:

I. CPU 自动进入内核模式(忽略 STATUS[KSU] 位),禁止中断(忽略 STATUS[IE] 位)

II. eret 指令使用 CP0_ErrorEPC 代替 CP0_EPC 作为例外返回地址

III. Kuseg 和 xkuseg 被改为 unmapped, uncached 的区域,以便在 cache 不能用时,内存空间能正常访问

貌似可恢复 Cache Error 的处理过程如下:

访问地址 VA  ---> 第一次发生 Cache Error 例外 ---> 进入 except_vec2_octeon,STATUS[ERL] = 1 --->CP0_STATUS & ST0_EXL 为 0 ---> 进入

handle_cache_err---> SAVE_ALL 保存上下文后,KMODE 清除 ERL 位 ---> 进入貌似 Cache Error 可恢复的处理函数cache_parity_error_octeon_recoverable---> 打出CP0_ICache_Error/CP0_DCache_Error 和 CP0_ErrorEPC  ---> 执行 eret 指令,例外返回到 ErrorEPC 指向的地址,即刚刚访问出错的地址 VA 再试一下

错误不可恢复的情形:

嵌套异常,即在已有的异常处理过程中,EXL 还没有被清除,却发生了 Cache Error,此时进入except_vec2_octeon,STATUS[ERL] = 1 --->CP0_STATUS & ST0_EXL 为 1 ---> 最终进入cache_parity_error_octeon_non_recoverable,打印CP0_ICache_Error/CP0_DCache_Error 和 CP0_ErrorEPC 后,直接 panic

/* We need to jump to handle_cache_err so that the previous handler
  * can fit within 0x80 bytes. We also move from 0xFFFFFFFFAXXXXXXX
  * space (uncached) to the 0xFFFFFFFF8XXXXXXX space (cached).  */
    LEAF(handle_cache_err)
    .set    push
    .set    noreorder
    .set    noat

SAVE_ALL
    KMODE                # clear EXL, ERL, set kernel mode bit
    jal     cache_parity_error_octeon_recoverable
    nop
    j       ret_from_exception
    nop

.set pop
    END(handle_cache_err)

[arch/mips/mm/c-octeon.c]
asmlinkage void cache_parity_error_octeon_recoverable(void)
{
    cache_parity_error_octeon(0);
}

asmlinkage void cache_parity_error_octeon_non_recoverable(void)
{
    cache_parity_error_octeon(1);
}

static void  cache_parity_error_octeon(int non_recoverable)
{
    unsigned long coreid = cvmx_get_core_num();
    uint64_t icache_err = read_octeon_c0_icacheerr();
   
    pr_err("Cache error exception:\n");
    pr_err("cp0_errorepc == %lx\n", read_c0_errorepc());
    if (icache_err & 1) {
        pr_err("CacheErr (Icache) == %llx\n",
               (unsigned long long)icache_err);
        write_octeon_c0_icacheerr(0);
    }
    if (cache_err_dcache[coreid] & 1) {
        pr_err("CacheErr (Dcache) == %llx\n",
               (unsigned long long)cache_err_dcache[coreid]);
        cache_err_dcache[coreid] = 0;
    }
   
    if (non_recoverable)
        panic("Can't handle cache error: nested exception");
}

本节主要分析 Cavium Octeon 上 TLB 异常相关的处理函数

1.2 TLB Refill 例外入口

非启动模式 (BEV=0) 下,MIPS64 R1上,64 位地址空间的 TLB Refill 入口在物理地址 0x80 处,其空间可以放一个 32 条指令的处理函数; 32 位地址空间的 TLB Refill 的 入口在物理地址 0x0 处,大小是 0x80,也可以放一个 32 条指令的处理函数;MIPS64 R2 默认与 R1 的入口一样,但其多了一个 cp0_ebase 寄存器,可以设置异常的基地址。

至于什么时候是 64 地址空间的访问什么时候又是 32 位地址空间访问,MIPS64 使用 cp0_status 的 KX, SX, UX 位来区分,以下情形都是 64 位地址模式,否则就是 32 位地址模式:

KX = 1 时,访问 Kernel Address Space

UX = 1 时,访问 Supervisor Address Space

UX = 1 时,访问 User Address Space

Loongson 2E 没有实现这个 KX/SX/UX 对地址空间的区分,因此其入口始终为 0xFFFF FFFF 8000 0000

Cavium Octeon 实现标准的 MIPS64 R2,但其通过 bootloader 设置 cp0_ebase 使其所有异常的基址变为 0xFFFF FFFF 8000 1000,从而 TLB Refill 用 0xFFFF FFFF 8000 1080 的入口

当访问一个需要经过 TLB 的虚拟地址 (mapped address) 时,如果在 TLB 中没找到相应的映射 (TLB Miss),同时原始 cp0_status[EXL] 为 0,才会进入这个 TLB Refill 入口;如果原来 cp0_status[EXL] 为 1,则表示 CPU 已经处于例外过程中,此时再发生找不到 TLB 项 (TLB Miss) 的情形,应该进入另外一个入口,以免 TLB Refill 嵌套。对于其它 EXL 为 1 的情形,不管在什么例外环境下出现 TLB Miss 的情形,都不会进入 TLB Refill 入口

比如访问 mapped address VA1,原来 EXL 为 0,无对应 TLB 项 (TLB Miss),进入这个入口,执行 TLB Refill 处理函数,注意,在 TLB Refill 处理函数中是不会去清除 EXL 位的,则此时如果再发生 mapped address 无对应 TLB 项 (TLB Miss)的情形,说明 TLB Refill 处理函数已经不能处理 VA1 对应 TLB 项的重填,得由另外功能更全面的处理函数善待之。更精确地,因为此时 EXL 已经为 1,EPC 不会被更新,还是原来访问 VA1 引起异常的地方,于是换个入口,进其他例外入口,最终由 TLB Invalid 异常的处理函数 handle_tlbl/handle_tlbs 来处理。

注意一下,TLB Invalid 异常的只在访问 VA1 时,对应 TLB 项的有效位 V 为 0 时才会触发;而 TLB Refill 是在 VA1 在 TLB 里没有对应项时触发,因此尽管 EXL 为 1 时,发生的 TLB Refill 异常同样进入 TLB Invalid 异常的处理函数,但还是能区分到底是 TLB Refill 还是 TLB Invalid,只要在处理函数里检查一下 TLB 里有没有 VA1 对应的项即可区分。

位于这些入口处 TLB Refill 处理函数,是由位于 arch/mips/mm/tlbex.c 中的 build_r4000_tlb_refill_handler() 在 kernel 初始化阶段动态生成,然后写到这些入口处的。至于为何要采取这种方式,主要是 MIPS 各种平台太多,如果根据用户的静态配置生成相应的 TLB Refill Handler,要照顾的情形太多,使用通常的条件编译方式已经不能满足要求了。关于这个动态生成过程后面的章节详细讨论。为分析这些动态生成的 Handler,我们可以在内核里把他们直接 dump 出来,具体操作过程,可以参考拙文:<dump MIPS TLB refill handler>

1.2.1 Cavium Octeon CN38xx

CN38xx实现 49 bit 虚拟地址,其 segbit 为 49

1.2.2 EXL=0 时 TLB Refill 的处理

这个的处理函数,在 Cavium Octeon 上位于 0xFFFF FFFF 8000 1080 处,但往往这个处理函数都会超过 0x80 字节,所以动态生成过程就使用原来 32 位的入口空间再放一部分代码,在 64 位下,这个 32 位的 TLB Refill 入口空间是不用的:

0xFFFF FFFF 8000 1000:

c:   07410003        bgez    k0,1c <0x1c>  # badvaddr >= 0,即 badvaddr 低 40 位以上有货,最高位不为 1,即在 0x0 ~ 0x7FFF FFFF FFFF FFFF (xuseg or xsseg) 中时则跳转。一句话就是去处理位于 xuseg or xsseg 的异常地址

10:   3c1b817c        lui     k1,0x817c     # 位于延迟槽,总是被执行。0xFFFF FFFF 817c 0000

#badvaddr低 40 位以上不为 0 且不位于 xuseg 或 xsseg 中,又走 TLB,则其应为位于 xkseg,范围为 0xC000 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF。一句话就是去处理位于 xkseg 中的地址

14:   10000024        b       a8 <0xa8>      ----->@@@   k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir, 他是内核使用的除模块以外的 PGD 基地址,内核模块使用

18:   277be000        addiu   k1,k1,-8192       # 位于延迟槽,总是被执行。0xFFFF FFFF 817c 0000 - 8192 = 0xFFFF FFFF 817B E000

#badvaddr 低 40 位以上不为 0 且位于 0x0 ~ 0x7FFF FFFF FFFF FFFF ( xuseg or xsseg)

1c:   3c1b8110        lui     k1,0x8110
  20:   277b4e00        addiu   k1,k1,19968    # k1 = 0xFFFF FFFF 8110 4E00, tlb_do_page_fault_0
  24:   03600008        jr      k1                 # call tlb_do_page_fault_0
  28:   00000000        nop

...

0xFFFF FFFF 8000 1080:

8c:   403a4000        dmfc0   k0,c0_badvaddr   # 取转换失败的虚拟地址 VA,TLB Refill 异常下,cp0_badvaddr (MIPS64 下,64 位长)存放翻译失败的虚拟地址
  90:   001ada3e        dsrl32  k1,k0,0x8          # VA 逻辑右移 (32+8)位,置入 k1

94:   1760ffdd        bnez    k1,c <0xc>          # k1 不为 0 则跳转到 0xc 处。即 badvaddr[63:41] 不为 0,即高于 40 位不为 0,就跳转。

对于这种高于 40 位还有有效位的虚拟地址,要不他位于 xkseg,要不就是 xuseg/xsseg 的地址。对于 xkseg 的地址,有有效的地址这是确定的(MIPS32 兼容地址空间内核是用的,高于 40 位的部分都不为 0)。

MIPS64 R2 下,segbit 默认为 40,但具体实现时,MIPS Core 的 segbit 可以超过 40,即实现的真正有效的虚拟地址可以超过 40 位

对只实现 40 位 segbit,出现位于 xuseg/xsseg 的地址且高于 40 位不为 0 的话,这个地址肯定就是用户瞎访问,其就不是一个有效地址,进 do_page_fault() 然后 segment fault 即可

但对实现超过 40 位的 segbit,像 Cavium Octeon CN38xx 实现 49 位,则需要特别对待,因为 4KB 的页大小下,Linux/MIPS 设计时规定整个虚拟地址的有效位是低 40 位,高于 40 的虚拟地址空间,内核没有为之建立页表,即没有映射!虽然你实现了 49 位虚拟地址,但我 Linux 还是用不了,最终也要进入 do_page_fault() 进行处理

# badvaddr 高于 40 位为 0

98:   403b2000        dmfc0   k1,c0_context     # 取 cp0_context 值入 k1

9c:   7c1bb007        dins    k1,zero,0x0,0x17   # zero[22:0] 替换 k1[22:0],k1 低 23 位置 0;

# 留意:cp0_context[22:4] 是硬件拷贝自 cp0_badvaddr[31:13]

                           # cp0_context[63:23] 是 PGD 的物理地址入口

a0:   377b0540        ori     k1,k1,0x540        # 0x540 置入 k1 的低位

a4:   003bdafa        dror    k1,k1,0xb           # drotr k1, k1, 11,即 k1 的低 11 位,和 k1 的高 53 位交换;k1 = k1[10:0] | k1[63:11]

上两条指令实际上是让 0b101 0100 0000 (0x540)或上 k1 的高 11 位,相当于 0xA800 0000 0000 0000 + Phy_PGD_ADDR,其是 PGD 的基地址

@@@     k1 中是 PGD 的基地址

a8:   001ad6fa        dsrl    k0,k0,0x1b          # k0 右移 27 位

ac:   335a1ff8        andi    k0,k0,0x1ff8         # 再保留 k0[12:3],实际保留的是 cp0_badvaddr[39:30] 正好是 PGD 的索引值,此时 k0 = cp0_badvaddr[39:30] << 3,64 位下 PGD 的项大小为 2^3 = 8 字节,因此其恰是索引 PGD 的指针

b0:   037ad82d        daddu   k1,k1,k0         # 获取 VA 对应的 PGD 项,即 VA 对应 PMD 基址的指针

b4:   403a4000        dmfc0   k0,c0_badvaddr  # 取转换失败的虚拟地址 VA
  b8:   df7b0000        ld      k1,0(k1)            # 取对应 PMD 基址
  bc:   001ad4ba        dsrl    k0,k0,0x12         # 右移 18 位
  c0:   335a0ff8        andi    k0,k0,0xff8         # 保留 k0[12:3],实际是 cp0_badvaddr[29:21] << 3,正好是 PMD 的索引指针,PMD 的项大小也是 2^3 = 8 字节

c4:   037ad82d        daddu   k1,k1,k0         # 索引 PMD,指向 VA 对应之 PT 基址

c8:   403aa000        dmfc0   k0,c0_xcontext  # 取 cp0_xcontext 值,Cavium Octeon 上,BadVPN2 是 xcontext[39:4],置 badvaddr[48:13]
  cc:   df7b0000        ld      k1,0(k1)           # 取 PT 基址

d0:   335a0ff0        andi    k0,k0,0xff0        # 保留 xcontext 的位 11:4,实际为 cp0_badvaddr[20:13] << (3 + 1),这实际为 PT 的索引值,因为 TLB Refill 总是一起填相邻的两项,因此这索引,实际是 VA 对应的偶数项索引

d4:   037ad82d        daddu   k1,k1,k0       # 索引 PT,指向 VA 对应之偶数项 PTE
  d8:   df7a0000        ld      k0,0(k1)           # 取 VA 对应偶数项的 PTE

dc:   df7b0008        ld      k1,8(k1)           # 取 VA 对应奇数项的 PTE

e0:   001ad13a        dsrl    k0,k0,0x4         # 忽略 PTE 的低 4 位,那是 OS 用的,TLB 的项里没有这些位

e4:   001bd93a        dsrl    k1,k1,0x4

e8:   003ad0ba        dror    k0,k0,0x2         # drotr k0, k0, 0x2,k0 低两位和高 62 位交换,这个操作和上面的右移 4 位应该是合起来的忽略 PTE 的低 6 位,因为在 MIPS64 R2 上,写入数据到 entrylo0/1[63:62] 是始终为 0 的。

ec:   40ba1000        dmtc0   k0,c0_entrylo0   # 偶数项 PTE 入 entrylo0

f0:   003bd8ba        dror    k1,k1,0x2         # 同上面偶数项的分析

f4:   40bb1800        dmtc0   k1,c0_entrylo1   # 奇数项PTE 入 entrylo1

f8:   42000006        tlbwr          # 把 entrylo0, entrylo1 随机写入 TLB 的一项
  fc:   dc1a8078        ld      k0,-32648(zero)
 100:   42000018        eret
        ...

1.2.3 EXL=1 时 TLB Refill 的处理

EXL=1 时,TLB Refill 进的是其他例外入口下 TLB Invalid 异常处理函数,TLB Invalid 分两类异常,一个是读数据引发的 (ExcCode=2, 处理函数 handle_tlbl),一个是写数据引发的 (ExcCode=3, 处理函数是 handle_tlbs)。

位于其他例外入口处的处理函数 except_vec3_generic 只是根据 ExcCode 跳转到具体例外的处理函数,TLB Load Invalid 进入 handle_tlbl;TLB Write Invalid 进入 handle_tlbs。这两个函数也是动态生成。

1.2.3.1 Cavium Octeon handle_tlbl 分析

handle_tlbl (位于代码段,不受向量空间限制。从其它例外入口进入):

c:   403a4000        dmfc0   k0,c0_badvaddr
  10:   001ada3e        dsrl32  k1,k0,0x8         # 失败地址右移 40 位
  14:   17600032        bnez    k1,e0 <0xe0>   # 40 位外不为 0 则跳转。不管CPU 具体实现了多少位的虚拟地址,对内核来讲 40 位外不为 0 都是异常地址,需要小心处理
  18:   403b2000        dmfc0   k1,c0_context
  1c:   7c1bb007        dins    k1,zero,0x0,0x17
  20:   377b0540        ori     k1,k1,0x540
  24:   003bdafa        dror    k1,k1,0xb     # 以上四条指令是为:k1 = 0xA800 0000 0000 0000 + PGD_Phy,即使用一个固定映射的虚址访问 PGD

28:   001ad6fa        dsrl    k0,k0,0x1b     # k0 里放的是 c0_badvaddr
  2c:   335a1ff8        andi    k0,k0,0x1ff8   # PGD offset
  30:   037ad82d        daddu   k1,k1,k0    # PMD base addr

34:   403a4000        dmfc0   k0,c0_badvaddr
  38:   df7b0000        ld      k1,0(k1)
  3c:   001ad4ba        dsrl    k0,k0,0x12
  40:   335a0ff8        andi    k0,k0,0xff8       # PMD offset

44:   037ad82d        daddu   k1,k1,k0      #  get PT base addr

48:   403a4000        dmfc0   k0,c0_badvaddr
  4c:   df7b0000        ld      k1,0(k1)
  50:   001ad27a        dsrl    k0,k0,0x9
  54:   335a0ff8        andi    k0,k0,0xff8      # PT offset

58:   037ad82d        daddu   k1,k1,k0      # index PT

5c:   d37a0000        lld     k0,0(k1)           # get PTE
  60:   42000008        tlbp                       # 查找失效地址 VA1 对应之 TLB 项,有则将 index 写入 cp0_index,无则将 cp0_index[31] = 1;注意所有 TLB 相关的异常 (Refill, Invalid, Modified) 出现时,CPU 都会把失效地址的高位置入 EntryHi[VPN2]

64:   335a0001        andi    k0,k0,0x1
  68:   13400020        beqz    k0,ec <0xec>       # 判断 PTE 的最低位 _PAGE_PRESENT,为 0  表示该页未分配或未在内存,则跳转到 do_page_fault() 处理。此应是 EXL=1 TLB Miss 的在此的主要路径,进入这里的 TLB Miss,多数情形是页面尚未分配,在 TLB Miss 的处理中,索引 PT 失败。经此一路,在 do_page_fault() 中,系统会为其分配页面,填充页表项,往下就不会是页面尚未分配的情形

注意,tlbp 失败只能是失效地址对应页表没有分配或不在内存(交换出去);往下 tlbp 不可能是其失败的情形,如果失效地址有页表项(有效无效皆可),其也不会在 TLB Miss 中再次 TLB Miss

另外 tlbp 失败会将 cp0_index[31] = 1,这个值远远大于现代 MIPS 实现的 TLB 项数目,随后来一条 tlbr 的话,其结果未定义!

6c:   d37a0000        lld     k0,0(k1)           # 该页已分配,get PTE again
  70:   335a0080        andi    k0,k0,0x80
  74:   13400008        beqz    k0,98 <0x98>      # 判断 PTE 的 _PAGE_VALID 位,为 0 表示该页无效,跳转。失效地址对应的页表项无效,其就不是 EXL=1 TLB Miss 引起的了,应为 TLB Invalid 异常

78:   00000000        nop                         # 页表项有效
  7c:   42000001        tlbr                          # 读 TLB,cp0_Index 对应 TLB 项存入 EntryHi, EntryLo0/1, PageMask
  80:   337a0008        andi    k0,k1,0x8          # k1 为页表项指针,此为判断奇偶页操作
  84:   13400002        beqz    k0,90 <0x90>     # 奇数项则跳转
  88:   403a1000        dmfc0   k0,c0_entrylo0    # 延迟槽,总是被执行。取奇数页在 TLB 中的值

8c:   403a1800        dmfc0   k0,c0_entrylo1    # 偶数项

90:   335a0002        andi    k0,k0,0x2          # TLB 项之 V 位
  94:   17400015        bnez    k0,ec <0xec>      # 不为 0(有效)则跳转到 do_page_fault() 处理。这是一个奇怪的现象,能找到失效地址对应的TLB项(非 EXL=1 TLB Refill),页表项目有效(非空 PTE),且对应的 TLB 项亦有效(非 TLB Invalid),应由 do_page_fault() 进一步甄别

# 失效地址对应之页表项无效 (V=0);或者对应页表项有效,却 TLB 项无效 (V=0)
  98:   d37a0000        lld     k0,0(k1)            # 再取 PTE
  9c:   375a0084        ori     k0,k0,0x84         # 置 _PAGE_VALID 和 _PAGE_WRITE
  a0:   f37a0000        scd     k0,0(k1)           # 更新页表项
  a4:   1340ffed        beqz    k0,5c <0x5c>       # k0=0 则事务操作失败,重做
  a8:   00000000        nop
  ac:   377b0008        ori     k1,k1,0x8         
  b0:   3b7b0008        xori    k1,k1,0x8         # 重置页表项指针,使其重新指向失效地址对应之奇数项
  b4:   df7a0000        ld      k0,0(k1)           # 以下操作取失效地址的页表项,填充 TLB,分析同 TLB Refill
  b8:   df7b0008        ld      k1,8(k1)
  bc:   001ad13a        dsrl    k0,k0,0x4
  c0:   001bd93a        dsrl    k1,k1,0x4
  c4:   003ad0ba        dror    k0,k0,0x2
  c8:   40ba1000        dmtc0   k0,c0_entrylo0
  cc:   003bd8ba        dror    k1,k1,0x2
  d0:   40bb1800        dmtc0   k1,c0_entrylo1
  d4:   42000002        tlbwi                       # 写入 Index 指定的 TLB 入口中
  d8:   dc1a8078        ld      k0,-32648(zero)
  dc:   42000018        eret

# 40位外有货,地址异常 ,需要小心对待
  e0:   3c1b817c        lui     k1,0x817c
  e4:   1000ffd0        b       28 <0x28>               # k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir
  e8:   277be000        addiu   k1,k1,-8192

ec:   08441380        j       1104e00 <0x1104e00>   # jump to tlb_do_page_fault_0, call do_page_fault(not write)
  f0:   00000000        nop
        ...

1.2.3.2Cavium Octeon handle_tlbs 分析

handle_tlbs (位于代码段,不受向量空间限制):

c:   403a4000        dmfc0   k0,c0_badvaddr
  10:   001ada3e        dsrl32  k1,k0,0x8
  14:   17600028        bnez    k1,b8 <0xb8>

18:   403b2000        dmfc0   k1,c0_context
  1c:   7c1bb007        dins    k1,zero,0x0,0x17
  20:   377b0540        ori     k1,k1,0x540
  24:   003bdafa        dror    k1,k1,0xb

28:   001ad6fa        dsrl    k0,k0,0x1b
  2c:   335a1ff8        andi    k0,k0,0x1ff8
  30:   037ad82d        daddu   k1,k1,k0

34:   403a4000        dmfc0   k0,c0_badvaddr
  38:   df7b0000        ld      k1,0(k1)

3c:   001ad4ba        dsrl    k0,k0,0x12
  40:   335a0ff8        andi    k0,k0,0xff8
  44:   037ad82d        daddu   k1,k1,k0

48:   403a4000        dmfc0   k0,c0_badvaddr
  4c:   df7b0000        ld      k1,0(k1)

50:   001ad27a        dsrl    k0,k0,0x9
  54:   335a0ff8        andi    k0,k0,0xff8
  58:   037ad82d        daddu   k1,k1,k0

5c:   d37a0000        lld     k0,0(k1)
  60:   42000008        tlbp
  64:   335a0003        andi    k0,k0,0x3
  68:   3b5a0003        xori    k0,k0,0x3
  6c:   17400015        bnez    k0,c4 <0xc4>

# 失效地址对应之页表项无效 (V=0);或者对应页表项有效,却 TLB 项无效 (V=0)

70:   d37a0000        lld     k0,0(k1)
  74:   375a018c        ori     k0,k0,0x18c           # 置 _PAGE_VALID, _PAGE_DIRTY, _PAGE_WRITE, _PAGE_ACCESSED
  78:   f37a0000        scd     k0,0(k1)
  7c:   1340fff7        beqz    k0,5c <0x5c>           # 事务失败,重做
  80:   00000000        nop

84:   377b0008        ori     k1,k1,0x8      
  88:   3b7b0008        xori    k1,k1,0x8
  8c:   df7a0000        ld      k0,0(k1)
  90:   df7b0008        ld      k1,8(k1)
  94:   001ad13a        dsrl    k0,k0,0x4
  98:   001bd93a        dsrl    k1,k1,0x4
  9c:   003ad0ba        dror    k0,k0,0x2
  a0:   40ba1000        dmtc0   k0,c0_entrylo0
  a4:   003bd8ba        dror    k1,k1,0x2
  a8:   40bb1800        dmtc0   k1,c0_entrylo1
  ac:   42000002        tlbwi
  b0:   dc1a8078        ld      k0,-32648(zero)
  b4:   42000018        eret

b8:   3c1b817c        lui     k1,0x817c
  bc:   1000ffda        b       28 <0x28>               # k1 = 0xFFFF FFFF 817B E000, it's swapper_pg_dir
  c0:   277be000        addiu   k1,k1,-8192

c4:   084413c8        j       1104f20 <0x1104f20>     # jump to tlb_do_page_fault_1, call do_page_fault(write)
  c8:   00000000        nop
  cc:   00000000        nop

除标出的外,其它与 handle_tlbl 同

RMI XLR732TLB 相关处理函数分析

0. 背景

MIPS64 下虚拟地址空间的划分:

其中内核空间是 xkphys 和 xkseg;其中 xkphys 是固定映射,不经 TLB (unmapped)

用户空间是 xuseg


1. RMI XLR732 TLB Refill handler 分析


@0xFFFF FFFF 8000 0000                       
   c:    07610005     bgez    k1,24 <0x24>  /* if vaddr[61] != 1 (0xc000 0000 0000 0000 ~ 0xDFFF FFFF FFFF FFFF), branch */   
10:          3c1bc000     lui    k1,0xc000        -----> at delay slot, commit new value to k1 after reading k1 (bgez)
                                           
# vaddr is at 0xE000000000000000 ~ 0xFFFF FFFF FFFF FFFF,这个地址段里被 64 bit Linux 内核使用且又要经过 TLB 的就是 kseg2 了,内核的模块默认使用这个地址
14:    035bd02f     dsubu    k0,k0,k1        /* k0 = vaddr - 0xFFFF FFFF C0000000 */
18:    3c1b8396     lui    k1,0x8396
1c:    10000023     b    ac <0xac>        ----> @@@        
20:    277ba000     addiu    k1,k1,-24576    /* 0xFFFF FFFF 8395A000 = module_pg_dir , using the module_pg_dir as the PGD entry */

# vaddr is at 0xC000000000000000 ~ 0xDFFF FFFF FFFF FFFF
24:    001bd83c     dsll32    k1,k1,0x0        /* (0xFFFF FFFF C0000000 << 32) */
28:    035bd02f     dsubu    k0,k0,k1        /* k0 = vaddr - 0xC000000000000000 */
2c:    1000001f     b    ac <0xac>        ----> @@@
30:    3c1b8396     lui    k1,0x8396            /* 0xFFFF FFFF 83960000 = swapper_pg_current, using the swapper_pg_current as the PGD entry */

34:    00000000

38:    00000000 <repeat>

......

@0xFFFF FFFF 8000 0080 (xTLB Refill 入口)

8c:    403a4000     dmfc0    k0,c0_badvaddr

90:    0740001a     bltz    k0,fc <0xfc>    /* if badvaddr >= 0x80000000 00000000 branch */

# vaddr is < 0x8000 0000 0000 000, so it's at kuseg
94:   403b2000    dmfc0   k1,c0_context
98:   001bddfa    dsrl    k1,k1,0x17      /* get (smp_processor_id() << 3) (26-23), see asm/mmu_context.h */
9c:   3c1a8396    lui k0,0x8396           /* swapper_pg_current = 0xFFFF FFFF 83960000 */
a0:   037ad82d    daddu   k1,k1,k0        /* k1 = swapper_pg_current[smp_processor_id()] */
a4:   403a4000    dmfc0   k0,c0_badvaddr
a8:   df7b2000    ld k1,8192(k1)        /*
                                             * pgd = *((void *)(k1 + 8192), 8 bytes per pgd entry, pgd_current = 0xFFFF FFFF 8396 2000,
                                             * 0x2000 = 8192
                                             * actually it's pgd = pgd_current[smp_processor_id()]
                                             */

@@@
ac:    001ad6fa     dsrl    k0,k0,0x1b        # >> 27
b0:    335a1ff8     andi    k0,k0,0x1ff8    /* get (vaddr[39:30] << 3), for indexing pgd */
b4:    037ad82d     daddu    k1,k1,k0        /* index pgd */

b8:    403a4000     dmfc0    k0,c0_badvaddr
bc:    df7b0000     ld    k1,0(k1)            /* get p_pmd */
c0:    001ad4ba     dsrl    k0,k0,0x12
c4:    335a0ff8     andi    k0,k0,0xff8        /* get (vaddr[29:21] << 3), for indexing pmd */
c8:    037ad82d     daddu    k1,k1,k0        /* index pmd */

cc:    403aa000     dmfc0    k0,c0_xcontext   
d0:    df7b0000     ld    k1,0(k1)            /* get p_pt */       
d4:    335a0ff0     andi    k0,k0,0xff0        /* get (va[20:13] << 4), actually use va[20:12] index the pt, va[12]=0, for indexing pt */
d8:    037ad82d     daddu    k1,k1,k0        /* index pt */

dc:    df7a0000     ld    k0,0(k1)            /* get even page addr */    <-------------
e0:    df7b0008     ld    k1,8(k1)            /* get odd page addr */

e4:    001ad1ba     dsrl    k0,k0,0x6        /* ignore the low 6 bits, it's for os */
e8:    409a1000     mtc0    k0,c0_entrylo0    /* tlb even page entry */
ec:    001bd9ba     dsrl    k1,k1,0x6        /* same as above */
f0:    409b1800     mtc0    k1,c0_entrylo1    /* tlb odd page entry */
f4:    42000006     tlbwr                    /* random write tlb */
f8:    42000018     eret

# go here, vaddr is >=0x8000 0000 0000 0000, so it's in xkphys or xkseg; and 0x8000 0000 0000 0000 ~ 0xBFFF FFFF FFFF FFFF is xkphys, unmapped, do not index TLB, so vaddr is at 0xc000 0000 0000 0000
fc:    001ad8b8     dsll    k1,k0,0x2        # vaddr << 2, will be test the vaddr[61]
100:    1000ffc2     b    c <0xc>

104:    00000000     nop
108:    00000000     nop


2. RMI XLR732  handle_tlbl 分析

handle_tlbl:

c:   403a4000    dmfc0   k0,c0_badvaddr          <---- handle_tlbl start
  10:   07400027    bltz    k0,b0 <0xb0>   # if badvaddr >= 0x80000000 00000000 branch

14:   403b2000    dmfc0   k1,c0_context
  18:   001bddfa    dsrl    k1,k1,0x17      /* get (smp_processor_id() << 3) (26-23) */
  1c:   3c1a8396    lui k0,0x8396           /* 0xffffffff83960000 + 0x2000 = pgd_current */
  20:   037ad82d    daddu   k1,k1,k0
  24:   403a4000    dmfc0   k0,c0_badvaddr
  28:   df7b2000    ld  k1,8192(k1)         /* pgd = pgd_current[smp_processor_id()] */

@@
  2c:   001ad6fa    dsrl    k0,k0,0x1b      # >> 27
  30:   335a1ff8    andi    k0,k0,0x1ff8    /* get (vaddr[39:30] << 3), for indexing pgd */
  34:   037ad82d    daddu   k1,k1,k0        /* index pgd */
  38:   403a4000    dmfc0   k0,c0_badvaddr
  3c:   df7b0000    ld  k1,0(k1)            /* get pmd */
  40:   001ad4ba    dsrl    k0,k0,0x12      # >> 18
  44:   335a0ff8    andi    k0,k0,0xff8     # get (vaddr[29:21] << 3)
  48:   037ad82d    daddu   k1,k1,k0        # index pmd
  4c:   403a4000    dmfc0   k0,c0_badvaddr
  50:   df7b0000    ld  k1,0(k1)            # get p_pt
  54:   001ad27a    dsrl    k0,k0,0x9
  58:   335a0ff8    andi    k0,k0,0xff8     # get (vaddr[20:12])
  5c:   037ad82d    daddu   k1,k1,k0        # index pt

60:   d37a0000    lld k0,0(k1)            # get pt entry
  64:   42000008    tlbp        # to distinguish TLB invalid or TLB refill exception

68:   335a0003    andi    k0,k0,0x3
  6c:   3b5a0003    xori    k0,k0,0x3           # _PRESENT and _READ
  70:   1740001a    bnez    k0,dc <0xdc>   # low two bits is not 011, branch @@-->out
  74:   d37a0000    lld k0,0(k1)                # get pt entry again

78:   375a0088    ori k0,k0,0x88
  7c:   f37a0000    scd k0,0(k1)                # set pt entry's pte[7] = 1, pte[3] =1
  80:   1340fff7    beqz    k0,60 <0x60>
  84:   00000000    nop

88:   377b0008    ori k1,k1,0x8
  8c:   3b7b0008    xori    k1,k1,0x8           # for getting even page
  90:   df7a0000    ld  k0,0(k1)                # get even page addr
  94:   df7b0008    ld  k1,8(k1)                # get odd page addr
  98:   001ad1ba    dsrl    k0,k0,0x6
  9c:   409a1000    mtc0    k0,c0_entrylo0
  a0:   001bd9ba    dsrl    k1,k1,0x6
  a4:   409b1800    mtc0    k1,c0_entrylo1
  a8:   42000002    tlbwi
  ac:   42000018    eret

@@
  b0:   001ad8b8    dsll    k1,k0,0x2   # go here, vaddr is in xkphys or xkseg (>0x8000..0000)
                                        # 0x8000..0000 ~ 0xc000..0000 is xkphys, unmapped,
                                        # so vaddr > 0xc000..0000; vaddr << 2
  b4:   07610005    bgez    k1,cc <0xcc>
                                        # vaddr[61] != 1 (0xc000..0000 ~ 0xdfff..ffff), branch
  b8:   3c1bc000    lui k1,0xc000

# vaddr, 0xe000..0000 ~ 0xffff..ffff
  bc:   035bd02f    dsubu   k0,k0,k1    # k0 = vaddr - 0xffffffffc0000000
  c0:   3c1b8396    lui k1,0x8396
  c4:   1000ffd9    b   2c <0x2c>
  c8:   277ba000    addiu   k1,k1,-24576    # k1 = 0xffffffff8395a000, module_pg_dir
                                            # module_pg_dir & swapper_pg_dir don't need the smp

@@
                                        # vaddr, 0xc000..0000 ~ 0xdfff..ffff
  cc:   001bd83c    dsll32  k1,k1,0x0   # 0xc0000000 00000000
  d0:   035bd02f    dsubu   k0,k0,k1    # k0 = vaddr - 0xc0000000 00000000
  d4:   1000ffd5    b   2c <0x2c>
  d8:   3c1b8396    lui k1,0x8396       # only swapper_pg_current

@@-->do_page_fault
  dc:   08d01150    j   3404540 <0x3404540>
  e0:   00000000    nop
    ...

1.3 其它例外入口初始化

其它例外的入口初始化位于:

[arch/mips/kernel/traps.c]

void __init trap_init()

{

......

/*
     * Copy the generic exception handlers to their final destination.
     * This will be overriden later as suitable for a particular
     * configuration.
     */
     set_handler(0x180, &except_vec3_generic, 0x80);

.......

}

set_handler 同样定义该文件中:/* Install CPU exception handler */
void __init set_handler(unsigned long offset, void *addr, unsigned long size)
{
    memcpy((void *)(ebase + offset), addr, size);
    local_flush_icache_range(ebase + offset, ebase + offset + size);
}

其主要的操作就是把其它例外处理函数 except_vec3_generic,复制到对应的入口处,这个入口一般为 ebase + 0x180。主要的不同在于 ebase 的值,这个在 cavium 上为 CKSEG0 + read_c0_ebase(),loongson2 上为 CKSEG0 = 0xFFFF FFFF8000 0180:

void __init trap_init()

{

......

if (cpu_has_veic || cpu_has_vint) {
        unsigned long size = 0x200 + VECTORSPACING*64;
        ebase = (unsigned long)
            __alloc_bootmem(size, 1 << fls(size), 0);
    } else {
        ebase = CKSEG0;
        if (cpu_has_mips_r2)
            ebase += (read_c0_ebase() & 0x3ffff000);
    }

......

}

其它例外处理函数 except_vec3_generic 定义于:

[arch/mips/kernel/genex.S]

/*
 * General exception vector for all other CPUs.
 *
 * Be careful when changing this, it has to be at most 128 bytes
 * to fit into space reserved for the exception handler.
 */
NESTED(except_vec3_generic, 0, sp)
    .set    push
    .set    noat
#if R5432_CP0_INTERRUPT_WAR
    mfc0    k0, CP0_INDEX
#endif
    mfc0    k1, CP0_CAUSE
    andi    k1, k1, 0x7c         # K1 = ExcCode * 4,32bit
#ifdef CONFIG_64BIT
    dsll    k1, k1, 1                        # k1 = ExcCode * 8,64bit 指针为 8 字节
#endif
    PTR_L   k0, exception_handlers(k1)
    jr  k0
    .set    pop
    END(except_vec3_generic)

代码很短,其功能为:取 cp0_cause 之 ExcCode 值,然后跳转到 exception_handlers[ExcCode] 处。这个 exception_handler 实际是一张表,每一项放的是具体异常处理函数的指针。ExcCode 为 cp0_cause[6:2],cp0_cause & 0x7c 就是 ExcCode * 4;64位下,指针长度为 8 字节,因此其还要左移一位,得 ExcCode * 8

这个 exception_handlers 定义于:

[arch/mips/kernel/traps]

unsigned long exception_handlers[32];

在 trap_init() 中填冲:

/*
     * Setup default vectors
     */
    for (i = 0; i <= 31; i++)
        set_except_vector(i, handle_reserved);

......

set_except_vector(0, rollback ? rollback_handle_int : handle_int);
    set_except_vector(1, handle_tlbm);
    set_except_vector(2, handle_tlbl);
    set_except_vector(3, handle_tlbs);

set_except_vector(4, handle_adel);
    set_except_vector(5, handle_ades);

set_except_vector(6, handle_ibe);
    set_except_vector(7, handle_dbe);

set_except_vector(8, handle_sys);
    set_except_vector(9, handle_bp);
    set_except_vector(10, rdhwr_noopt ? handle_ri :
              (cpu_has_vtag_icache ?
               handle_ri_rdhwr_vivt : handle_ri_rdhwr));
    set_except_vector(11, handle_cpu);
    set_except_vector(12, handle_ov);
    set_except_vector(13, handle_tr);

......

这个 set_except_vector() 定义于同一文件中:

void __init *set_except_vector(int n, void *addr)
{
    unsigned long handler = (unsigned long) addr;
    unsigned long old_handler = exception_handlers[n];

exception_handlers[n] = handler;
    if (n == 0 && cpu_has_divec) { // 处理扩展中断向量的情形, MIPS32/64 R2 都带的一个可选 feature,loongson2 和 cavium 都没有实现,因此我们不关心
        unsigned long jump_mask = ~((1 << 28) - 1);
        u32 *buf = (u32 *)(ebase + 0x200);
        unsigned int k0 = 26;
        if ((handler & jump_mask) == ((ebase + 0x200) & jump_mask)) {
            uasm_i_j(&buf, handler & ~jump_mask);
            uasm_i_nop(&buf);
        } else {
            UASM_i_LA(&buf, k0, handler);
            uasm_i_jr(&buf, k0);
            uasm_i_nop(&buf);
        }
        local_flush_icache_range(ebase + 0x200, (unsigned long)buf);
    }
    return (void *)old_handler;
}

其完成的主要操作就是将传来的具体例外处理函数地址赋值给 exception_handlers 的元素 n

可以看到用来索引的 ExcCode 是这样与具体例外处理函数相关的,MIPS 规定 ExcCode 值表示的含义如下:

0             Int           中断

1            Mod            TLB 修改异常

2            TLBL            TLB 读异常

3            TLBS            TLB 写异常

4            AdEL          读地址错误异常

5            AdEs           写地址错误异常

6            IBE           总线错误异常(取指令)

7            DBE           总线错误异常(读写数据)

8            Sys           系统调用异常

......

上述的 handle_int, handle_sys, handle_adel, handle_ades, handle_ibe, handle_dbe ... 由下面的宏生成:

[arch/mips/kernel/genex.S]
    BUILD_HANDLER adel ade ade silent       /* #4  */
    BUILD_HANDLER ades ade ade silent       /* #5  */
    BUILD_HANDLER ibe be cli silent         /* #6  */
    BUILD_HANDLER dbe be cli silent         /* #7  */
    BUILD_HANDLER bp bp sti silent          /* #9  */
    BUILD_HANDLER ri ri sti silent          /* #10 */
    BUILD_HANDLER cpu cpu sti silent        /* #11 */
    BUILD_HANDLER ov ov sti silent          /* #12 */
    BUILD_HANDLER tr tr sti silent          /* #13 */
    BUILD_HANDLER fpe fpe fpe silent        /* #15 */
    BUILD_HANDLER mdmx mdmx sti silent      /* #22 */
#ifdef  CONFIG_HARDWARE_WATCHPOINTS
    /*
     * For watch, interrupts will be enabled after the watch
     * registers are read.
     */
    BUILD_HANDLER watch watch cli silent        /* #23 */
#else
    BUILD_HANDLER watch watch sti verbose       /* #23 */
#endif
    BUILD_HANDLER mcheck mcheck cli verbose     /* #24 */
    BUILD_HANDLER mt mt sti silent          /* #25 */
    BUILD_HANDLER dsp dsp sti silent        /* #26 */
    BUILD_HANDLER reserved reserved sti verbose /* others */

BUILD_HANDLER 定义为:

.macro  BUILD_HANDLER exception handler clear verbose
    __BUILD_HANDLER \exception \handler \clear \verbose _int

.endm

.macro  __BUILD_HANDLER exception handler clear verbose ext
    .align  5
    NESTED(handle_\exception, PT_SIZE, sp)
    .set    noat
    SAVE_ALL
    FEXPORT(handle_\exception\ext)
    __BUILD_clear_\clear
    .set    at
    __BUILD_\verbose \exception
    move    a0, sp
    PTR_LA  ra, ret_from_exception
    j   do_\handler
    END(handle_\exception)
    .endm

64位多核 MIPS 异常和中断内核代码分析相关推荐

  1. Linux内核分析2:一个简单的时间片轮转多道程序内核代码分析

    Lab2:一个简单的时间片轮转多道程序内核代码 席金玉   <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-100002900 ...

  2. Linux开机启动过程(4):切换到64位模式-长模式(直到内核解压缩之前)

    内核引导过程. Part 4. 本文是在原文基础上经过本人的修改. 切换到64位模式 直到内核解压缩之前的所有步骤 这是 内核引导过程 的第四部分,我们将会看到在保护模式中的最初几步,比如确认CPU是 ...

  3. 教你在64位Win7系统下使用ObRegisterCallbacks内核函数来实现进程保护

    我平时工作很忙,也很少有空闲时间上看雪论坛.我在看雪论坛里面文章发表的很少,几只有几篇,我也很少回答别人的问题.我的很多朋友都这样问我问题:我整理了一下,无非就以下几种问题: (1)怎么样在64位的W ...

  4. python封装成exe win7不能用_如何在win7 64位系统下用pyinstaller打包python代码成exe

    添加网络打印机步骤 1.安装.使用PyInstaller需要安装PyWin32. 下载与Python对应的PyInstaller版本 抄 , 解压后就算安装好了 . 2.生成exe文件.Python程 ...

  5. PCIe学习笔记之MSI/MSI-x中断及代码分析

    本文基于linux 5.7.0, 平台是arm64 1. MSI/MSI-X概述 PCIe有三种中断,分别为INTx中断,MSI中断,MSI-X中断,其中INTx是可选的,MSI/MSI-X是必须实现 ...

  6. linux实验:基于mykernel的一个简单的时间片轮转多道程序内核代码分析

    学号后三位:288 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ 1.mykernel mykernel是由中科大孟宁老师建立的用于开发 ...

  7. 输入子系统代码内核代码分析

    本篇博客里将会对输入子系统进行比较深入的分析,第一部分将基于内核2.6.2版本,第二部分将基于内核的3.14版本,同时我会为两个版本都提供一个示例代码 我会尽可能深入的分析代码,如果你看完了这篇博客, ...

  8. 一个简单的时间片轮转多道程序内核代码分析

    郑斌 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 第二周的实验内容分析 1. ...

  9. 64位ubuntu 12.04编译linux内核提示openssl/opensslv.h文件缺失

    今天心血来潮,想编译一下linux内核来玩玩.从git上下载了最新版本的源码,放在我的ubuntu系统下,首先从现有ubuntu系统的boot目录下,拷贝了config-3.13.0-32-gener ...

最新文章

  1. 资源 | 斯坦福最新NLP课程上线,选择PyTorch放弃TensorFlow
  2. 单核工作法12:现在专注一件事(下)
  3. 【HTML学习】——一个网页HTML编程的构成
  4. Precedence Problems of C Operators
  5. CVE-2020-0601漏洞详细分析
  6. 10- monkey日志分析
  7. python模块规定的格式,按照这样写,最规范
  8. Spring源代码地址
  9. java服装销售系统_java服装管理销售系统
  10. 目标检测算法YOLO3论文解读
  11. 移动端H5 QQ在线客服链接代码
  12. 2068个开源的网站模板
  13. 路由器WIFI密码忘记了怎么办
  14. 冯.诺依曼计算机结构要点
  15. [Paper Reading] Preference-Adaptive Meta-Learning for Cold-Start Recommendation
  16. 【OpenGL】笔记二十七、几何着色器
  17. 服务器测评文档,十年磨一剑,腾讯自研TBase数据库有奖测评
  18. csv文档转为tsv文档-csv to tsv
  19. java编写超市收银系统_java编写的超市收银系统
  20. Delphi的编程语言Object Pascal(3)

热门文章

  1. HTML交互式课件制作,交互式课件制作软件
  2. kaggle之泰坦尼克号乘客死亡预测
  3. 【Full text search】检索条件具有完整的关键词布尔逻辑运算AND、OR、NOT能力
  4. 华为鸿蒙龙头股一览表_华为鸿蒙概念股龙头一览?华为鸿蒙概念股有哪些(表)...
  5. linux+gpfs配置文件,centos7 安装gpfs 4.1.1.3
  6. FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.
  7. 自动化运维---ansible常用模块之文件操作(findreplace模块)
  8. PTA 树的同构 思路分析及代码解析
  9. 手机版PDF编辑器支持PDF转Word、文档内容编辑合并与提取
  10. 采用Slwave提取PCB差分走线的S参数