JuanA1的专栏

金字塔最底层之IT民工的技术点滴

基于PowerPC的Linux内核之旅:第2站-__secondary_start(start_here)-下

    上篇中介绍了基于PowerPC的Linux第二阶段启动过程的一部分,由于MMU的初始化涉及的内容较多,而且代码量很大,所以这部分分为上下两部。本部分继续阐述MMU硬件的初始化和最终真正使能MMU的过程。

    在开始之前,先指出前面文章出现的一个错误,在对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代码初始化工作。

阅读更多
个人分类: PowerPC Linux学习
想对作者说点什么? 我来说一句

Linux内核设计的艺术(第2版)

2017年11月10日 85.36MB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭