在开始之前,先指出前面文章出现的一个错误,在对mmu_off函数中的RFI指令的介绍时,我简单的认为是中断返回,但后来想想,CPU初始化时,中断还没有使能,所以中断返回的说法是不正确的,查了下资料,原来使用RFI指令也可以做程序跳转使用,使用RFI进行程序跳转的好处是,程序跳转后将自动执行isync指令,以保证指令空间的同步,在Linux的初始化阶段,使用RFI指令进行程序跳转比较常见,这里的RFI指令与中断返回是没有任何关系的。造成误解请见谅。
另外,最近被工作上的事情困扰的有些力不从心了,感觉这篇对MMU的硬件初始化分析写的极其的烂,待以后收拾了心情,我一定会加以改进的,当然,也希望高手能不吝指教,就当交个朋友吧。
先来看一下MMU_init_hw的详细代码(位于mm/ppc_mmu_32.c):
void __init MMU_init_hw(void)
{
unsigned int hmask, mb, mb2;
unsigned int n_hpteg, lg_n_hpteg;
/*定义于hash_low_32.S,填充和清除Hash表*/
extern unsigned int hash_page_patch_A[];
extern unsigned int hash_page_patch_B[], hash_page_patch_C[];
extern unsigned int hash_page[];
extern unsigned int flush_hash_patch_A[], flush_hash_patch_B[];
if (!mmu_has_feature(MMU_FTR_HPTE_TABLE)) {
/*在hash_page的开始处放置blr指令,因为在603处理器中仍能接收到DSI(Data Storage Interrupt)异常*/
hash_page[0] = 0x4e800020;
flush_icache_range((unsigned long) &hash_page[0],
(unsigned long) &hash_page[1]); /*清空指令cache*/
return;
}
if ( ppc_md.progress ) ppc_md.progress("hash:enter", 0x105);
#define LG_HPTEG_SIZE 6 /* 每个PTEG为64个字节 */
#define SDR1_LOW_BITS ((n_hpteg - 1) >> 10)
#define MIN_N_HPTEG 1024 /* min 64kB hash table */
/*允许每页内存都有一个HPTE*/
n_hpteg = total_memory / (PAGE_SIZE * 8);
if (n_hpteg < MIN_N_HPTEG)
n_hpteg = MIN_N_HPTEG;
lg_n_hpteg = __ilog2(n_hpteg);
if (n_hpteg & (n_hpteg - 1)) {
++lg_n_hpteg; /* round up if not power of 2 */
n_hpteg = 1 << lg_n_hpteg;
}
Hash_size = n_hpteg << LG_HPTEG_SIZE;
/*为哈希表申请内存地址,这两步就类似于malloc和memset*/
if ( ppc_md.progress ) ppc_md.progress("hash:find piece", 0x322);
Hash = __va(memblock_alloc_base(Hash_size, Hash_size,
__initial_memory_limit_addr));
cacheable_memzero(Hash, Hash_size);
_SDR1 = __pa(Hash) | SDR1_LOW_BITS;
Hash_end = (struct hash_pte *) ((unsigned long)Hash + Hash_size);
/*Patch up the instructions in hash_low_32.S:create_hpte*/
if ( ppc_md.progress ) ppc_md.progress("hash:patch", 0x345);
Hash_mask = n_hpteg - 1;
hmask = Hash_mask >> (16 - LG_HPTEG_SIZE);
mb2 = mb = 32 - LG_HPTEG_SIZE - lg_n_hpteg;
if (lg_n_hpteg > 16)
mb2 = 16 - LG_HPTEG_SIZE;
hash_page_patch_A[0] = (hash_page_patch_A[0] & ~0xffff)
| ((unsigned int)(Hash) >> 16);
hash_page_patch_A[1] = (hash_page_patch_A[1] & ~0x7c0) | (mb << 6);
hash_page_patch_A[2] = (hash_page_patch_A[2] & ~0x7c0) | (mb2 << 6);
hash_page_patch_B[0] = (hash_page_patch_B[0] & ~0xffff) | hmask;
hash_page_patch_C[0] = (hash_page_patch_C[0] & ~0xffff) | hmask;
/*确保patch过的地方是否从数据cache中保存,并清除指令cache*/
flush_icache_range((unsigned long) &hash_page_patch_A[0],
(unsigned long) &hash_page_patch_C[1]);
/*Patch up the instructions in hash_low_32.S:flush_hash_page*/
flush_hash_patch_A[0] = (flush_hash_patch_A[0] & ~0xffff)
| ((unsigned int)(Hash) >> 16);
flush_hash_patch_A[1] = (flush_hash_patch_A[1] & ~0x7c0) | (mb << 6);
flush_hash_patch_A[2] = (flush_hash_patch_A[2] & ~0x7c0) | (mb2 << 6);
flush_hash_patch_B[0] = (flush_hash_patch_B[0] & ~0xffff) | hmask;
flush_icache_range((unsigned long) &flush_hash_patch_A[0],
(unsigned long) &flush_hash_patch_B[1]);
if ( ppc_md.progress ) ppc_md.progress("hash:done", 0x205);
}
32位的PowerPC的MMU的实现,需要一个包括PTEs和16个段寄存器的集合的hash表,用于定义虚地址到实际地址的映射。这里的hash表用做额外的TLB(Translation Lookaside Buffers,快表),也可以理解成当前可用映射的缓存。而hash_low_32.S文件就是用于从页表树中提取PTE,再将其放在hash表中,然后更新页表树中的更改位。在PowerPC中,一个PTEG包含8个PTE,每个PTE有8个字节,PTEG的地址是进行表查询操作的入口点。对于DSI,Linux将内存访问出错分为进程数据空间访问出错和进程程序空间访问出错,同时,E300内核也提供了两种异常来处理两种内存访问错误,DSI(Data Storage Interrupt数据访问异常)和ISI(指令访问异常)。内核产生DSI异常的主要原因就是:读取一些在MMU中不能进行读取的地址空间,向MMU不能写入的地址空间进行写操作。Linux有时会故意设置MMU的页表,以产生DSI异常,然后进行异常处理,上面程序中的DSI异常指的就是这种。至于异常处理函数,就又是一篇大文章了,先备案,以后有时间再尝试写一下吧。其中的flush_icache_range函数调用的是misc_32.S中的__flush_icache_range,主要作用为将所有修改过的cache块都保存到内存中,然后将对应的块擦除。详细代码如下:
_KPROBE(__flush_icache_range)
BEGIN_FTR_SECTION
blr /* for 601, do nothing */
END_FTR_SECTION_IFSET(CPU_FTR_COHERENT_ICACHE)
li r5,L1_CACHE_BYTES-1 /*e300的L1 Cahce大小为32Kbyte*/
andc r3,r3,r5
subf r4,r3,r4
add r4,r4,r5
srwi. r4,r4,L1_CACHE_SHIFT
beqlr
mtctr r4
mr r6,r3
1: dcbst 0,r3 /*保存数据缓存块,即拷贝到内存*/
addi r3,r3,L1_CACHE_BYTES
bdnz 1b
sync /*等待dcbst命令完成*/
iccci 0, r0 /*清除*/
#endif
sync /* additional sync needed on g4 */
isync
blr
这里的r3保存VSID,r4保存虚拟地址,r5保存Linux的PTE(Page Table Entry),r6保存在设置_PAGE_HASHPTE之前的Linux的PTE,r7保存需要加上的到地址的偏移量(MMU开启时为0,关闭时为-KERNELBASE:0xc0000000)。
再来看一下几个hash_page_patch,涉及到hash_low_32.S中的两个函数,create_hpte比较简单,功能即创建HPTE,汇编代码里就是填充r5,如下:
_GLOBAL(create_hpte)
/*将linux-style PTE (r5)转换到PPC-style PTE (r8)的低字节*/
rlwinm r8,r5,32-10,31,31 /* _PAGE_RW -> PP lsb */
rlwinm r0,r5,32-7,31,31 /* _PAGE_DIRTY -> PP lsb */
and r8,r8,r0 /*写允许writable if _RW & _DIRTY */
rlwimi r5,r5,32-1,30,30 /* _PAGE_USER -> PP msb */
rlwimi r5,r5,32-2,31,31 /* _PAGE_USER -> PP lsb */
ori r8,r8,0xe04 /*清除备用位*/
andc r8,r5,r8 /*PP = user? (rw&dirty? 2: 3): 0 好复杂啊!!*/
BEGIN_FTR_SECTION
rlwinm r8,r8,0,~_PAGE_COHERENT /*清空M,这里不需要*/
END_FTR_SECTION_IFCLR(CPU_FTR_NEED_COHERENT)
/*补全PPC-style PTE (r5)的高字节*/
rlwinm r5,r3,7,1,24 /* put VSID in 0x7fffff80 bits */
rlwimi r5,r4,10,26,31 /* put in API (abbrev page index) */
SET_V(r5) /* set V (valid) bit */
再来看下flush_hash_pages,它的主要作用为从哈希表中清除特定内存页面的入口,一开始关闭中断是为了让_PAGE_HASHPTE位在整个过程中,不会被其他程序更改,进而可以用它来确定HPTE是否存在。而关闭MMU的数据地址转换,则是为了避免接收哈希表的MISS。
mfmsr r10
SYNC /*处理器架构相关的isync(FTR)*/
rlwinm r0,r10,0,17,15 /*关闭外部中断*/
rlwinm r0,r0,0,28,26 /*关闭数据地址转换*/
mtmsr r0
SYNC_601
isync
然后程序开始寻找并清空PTE,这里就以_PAGE_HASHPTE为判断标准,这里还有附加对VSID的相关操作。
/* 在_PAGE_HASHPTE位已设置的集合中寻找PTE*/
#ifndef CONFIG_PTE_64BIT
rlwimi r5,r4,22,20,29
#else
rlwimi r5,r4,23,20,28
#endif
1: lwz r0,PTE_FLAGS_OFFSET(r5) /*此处为0,即r0<-r5*/
cmpwi cr1,r6,1
andi. r0,r0,_PAGE_HASHPTE
bne 2f /*找到即跳转*/
ble cr1,19f
addi r4,r4,0x1000
addi r5,r5,PTE_SIZE
addi r6,r6,-1
b 1b
/*切换VSID中的上下文和值 */
2: mulli r3,r3,897*16 /* multiply context by context skew */
rlwinm r0,r4,4,28,31 /* get ESID (top 4 bits of va) */
mulli r0,r0,0x111 /* multiply by ESID skew */
add r3,r3,r0 /* note code below trims to 24 bits */
/*构建PTE (r11)的高位*/
rlwinm r11,r3,7,1,24 /* put VSID in 0x7fffff80 bits */
rlwimi r11,r4,10,26,31 /* put in API (abbrev page index) */
SET_V(r11) /* set V (valid) bit */
/*检查当前PTE中的_PAGE_HASHPTE位,若已清除,则完成;否则自动清除*/
#if (PTE_FLAGS_OFFSET != 0)
addi r5,r5,PTE_FLAGS_OFFSET
#endif
33: lwarx r8,0,r5 /*获取PTE标识*/
andi. r0,r8,_PAGE_HASHPTE
beq 8f /* 比较,决定是否完成*/
rlwinm r8,r8,0,31,29 /*清空 HASHPTE位*/
stwcx. r8,0,r5 /* 更新pte*/
bne- 33b
这里的代码比较散,而且跳来跳去的,很难一下分析清楚,本人水平实在有限,在摸索了几天之后。。。。悲催了!!
完成了物理内存的检查、修正和整理后,MMU_init将调用mapin_ram函数对Linux内核程序使用的物理地址空间进行虚实映射。该函数首先调用mmu_mapin_ram,使用前两个BAT或3个对Linux内核所使用的物理地址空间进行虚实映射,这其中有一个setbat函数,用于创建一个I/D BAT寄存器对,大小在128K至256M之间。在这之后再调用__mapin_ram_chunk,将物理地址的一页映射到开始的地方。
再来看后半段的代码,相对于前面调用了几个大函数,这部分相对简单一些,由于MMU被关闭,系统回到unmapped的环境,进而可以获得SDR1及段寄存器的值。
lis r4,2f@h
ori r4,r4,2f@l
tophys(r4,r4)
li r3,MSR_KERNEL & ~(MSR_IR|MSR_DR) /*关闭地址转换,即MMU*/
FIX_SRR1(r3,r5) /*又是空?*/
mtspr SPRN_SRR0,r4
mtspr SPRN_SRR1,r3 /*保存处理器状态*/
SYNC
RFI
/*装载内核上下文*/
2: bl load_up_mmu
/*现在才真正开启MMU*/
li r4,MSR_KERNEL /*重新打开MMU*/
FIX_SRR1(r4,r5)
lis r3,start_kernel@h /*init/main.c,启动kernel的C代码,setup_arch就在此调用*/
ori r3,r3,start_kernel@l
mtspr SPRN_SRR0,r3
mtspr SPRN_SRR1,r4
SYNC
RFI
可以看到,程序之后关闭MMU,将内核载入MMU,调用load_up_mmu。这里的SDR1的值是表示用于对内存页面做虚实地址转换时的页表的格式,这里就是哈希表的指针。这里的r3、r4是从r31和r30装载的,在启动最开始时,这几个寄存器被保留的,后查询资料,发现,若Linux不支持E300内核的OF结构,则通用寄存器r3、r4、r5、r6、r7都会被保存(分别到r31、…、r27),这些都是引导程序传递的,所以,这些寄存器将存放以下值:
r3存放bd_info的地址指针,bd_info用于描述当前处理器系统的硬件信息,包括处理器频率、物理内存大小、网卡地址等;r4存放Init Ramdisk(initrd) 的起始地址;r5存放initrd的结束地址;r6存放内核的命令行参数(即bootargs)的起始地址;r7存放内核的命令行参数的起始地址。而此次的Linux已经支持了,所以,一开始只存储了两个寄存器,r3存储的是指向OF Tree结构的物理地址,这个OF Tree结构也被称作dtb(Device Tree Block);r4指向Linux内核所在的物理地址。
关闭MMU之后执行的load_up_mmu函数,目的很简单,就是在MMU打开之后再重新执行一下MMU_init中的HPTE以及BAT的配置工作。
load_up_mmu:
sync /*Force all PTE updates to finish 更新所有的PTE */
isync
tlbia /*清空TLB入口/
sync /*等待完成*/
TLBSYNC /* ... on all CPUs */
/* 装载SDR1寄存器(包括哈希表的基地址和大小)*/
lis r6,_SDR1@ha
tophys(r6,r6)
lwz r6,_SDR1@l(r6)
mtspr SPRN_SDR1,r6
li r0,16 /*装载段寄存器 */
mtctr r0 /* for context 0 */
lis r3,0x2000 /* Ku = 1, VSID = 0 */
li r4,0
3: mtsrin r3,r4
addi r3,r3,0x111 /* increment VSID */
addis r4,r4,0x1000 /* address of next segment */
bdnz 3b
/* 装载BAT,其值在MMU_init的setbat中被配置*/
mfpvr r3
srwi r3,r3,16
cmpwi r3,1
lis r3,BATS@ha
addi r3,r3,BATS@l
tophys(r3,r3)
LOAD_BAT(0,r3,r4,r5)
LOAD_BAT(1,r3,r4,r5)
LOAD_BAT(2,r3,r4,r5)
LOAD_BAT(3,r3,r4,r5)
blr
这一步执行完成后,MMU就算是真正的开启了,内核也可以使用C语言中的malloc来动态申请内存空间了。接着,程序一个长跳转到main.c中的start_kernel函数中,开始内核的C代码初始化工作。