内核临时页表

本文详细解析Linux内核启动时如何创建临时页表,包括映射关系、代码流程及关键注释,深入理解MMU启动前的实地址模式运行与后续的虚拟地址空间映射。
建立临时页表:内核启动前期、段式页表映射(1M大小)
主要完成的映射关系:
1.将虚拟地址空间按照“1:1”对等映射到内核映像的第一个1M处
  作用:在启动MMU之前只能使用实地址模式运行
2.将整个内核地址空间直接映射区的代码那部分映射到SDRAM上

3.将内核地址空间的开始的1M映射到SDRAM开始的第一个1M空间,因为那里存放了内核启动参数

看图意会时间:


ARM920T内置的MMU地址转换方式:这个建议去看看ARM920T的使用手册,毕竟那才是原汁原味的


一级描述符的格式:这里主要涉及的是一级段式页表描述符


具体使用一级段式页表将MVA转换成PA的过程


代码时间到了

第一段注释:
临时页表采用的是段式页表

分析linux-2.6.30.4/arch/arm/kernel/head.S的__create_page_tables汇编代码:

/*
 * Setup the initial page tables.  We only setup the barest
 * amount which are required to get the kernel running, which
 * generally means mapping in the kernel code.
 *
 * r8  = machinfo
 * r9  = cpuid
 * r10 = procinfo
 *
 * Returns:
 *  r0, r3, r6, r7 corrupted
 *  r4 = physical page table address:r4=页表的基地址,r4的值还是在内核自解压工作的时候赋给的:内核解压后映像的起始地址0x30008000
 */
__create_page_tables:
	pgtbl	r4				@ page table address
	//pgtbl是一个汇编宏定义 .macro	pgtbl, rd,通过这个宏将r4(0x30004000)设置成页表的物理基地址,往后r4值一直没有变

	/*
	 * Clear the 16K level 1 swapper page table  清零一级交换页表16K
	 * 页表将4GB的地址空间分成若干个1MB的段(section),因此页表包含4096个页表项(section entry)。每个页表项是32bits(4 bytes),因而页表占用4096*4=16k的内存空间。
	 */
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #0x4000
1:	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6
	bne	1b

	ldr	r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags 获得proc_info_list的__cpu_mm_mmu_flags的值,并存储到r7中,看第二段注释
	//r7 = __cpu_mm_mmu_flags = 0x00000C1E
	/*
	 * Create identity mapping for first MB of kernel to
	 * cater for the MMU enable.  This identity mapping
	 * will be removed by paging_init().  We use our current program
	 * counter to determine corresponding section base address.
	 * 接下来是为内核空间的直接映射区第一个1MB建立页表,这都是为之后MMU的启动,之后会被paging_init()函数移除销毁
	 * 我们通过当前的程序计数寄存器PC值来获得段基址
	 */
	mov	r6, pc, lsr #20				@ start of kernel section    r6 = 0x300
	orr	r3, r7, r6, lsl #20			@ flags + kernel base(PA)    r3 = (r7) | ((r6)<<20)   r3里边放的内容就是一级描述符
	str	r3, [r4, r6, lsl #2]		@ identity mapping           [0x30004000+0x300<<2] = r3   将一级描述符放到页目录中
                                                                 @不过这里为什么要左移两位(乘于4)???为什么又是存放到这个地址处
                                                                 @想到了:r4里边是ttb基址(启动MMU时会被写到cp15的c2寄存器),r6里边的是ttb表中对应1M的索引值,每个描述符是4字节
                                                                 @实际上只是将pc的值右移了18位
	@上面这一段代码实现了VA=PA(1:1)的映射:根据ARM920T的MMU一级地址转换,4G的虚拟地址空间被分成4096个条目(描述符,每个4字节),因此每一个条目对应1M=4G/4096的地址空间映射。
	@到这里第一个映射关系建立完成
	
	/*
	 * Now setup the pagetables for our kernel direct
	 * mapped region.
	 * 现在为了内核直接映射区来设置页表
	 * 即为kernel镜像所占有空间,即KERNL_START到KERNEL_END建立内存映射
	 * 由于这块内核虚拟空间要映射到SDRAM中内核映像0x30008000开始处,所以第一个1M的描述符(保存到r3寄存器中)和上面的是样的
	 * 这里是将整个内核空间的直接映射区全部映射完毕--以段的方式(1M)
	 */
	add	r0, r4,  #(KERNEL_START & 0xff000000) >> 18      @取内核空间地址(VA)高8位,arm的立即数只能是8位
	str	r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!	 @再取内核空间地址(VA)高8位往后4位,和上一句加起来共12位,“!”将地址写到r0
	ldr	r6, =(KERNEL_END - 1)                            @r4=ttb基址  r0=ttb中kernel第1M描述符检索值
	add	r0, r0, #4
	add	r6, r4, r6, lsr #18                              @r6= 用来标识ttb中内核地址空间结束的检索地址
1:	cmp	r0, r6
	add	r3, r3, #1 << 20   ;将描述符的段偏移地址加1,即物理段基地址加1
	strls	r3, [r0], #4   ;表示小于等于,即只要r0<=r6,strls就会执行
	bls	1b

#ifdef CONFIG_XIP_KERNEL  ;没有定义CONFIG_XIP_KERNEL,注释掉这个条件分支,XIP技术就是内核代码可以不用拷贝到SDRAM而立地执行
	
.......
	
#endif

	/*
	 * Then map first 1MB of ram in case it contains our boot params.
	 * 接下来建立内核临时页表的最后一个页表描述符,即SDRAM开始1M的地址空间,那里保存了uboot传递给内核的启动参数
	 */
	add	r0, r4, #PAGE_OFFSET >> 18   ;PAGE_OFFSET被定义为SDRAM起始地址0x30000000
	orr	r6, r7, #(PHYS_OFFSET & 0xff000000)    ;r6 = PA section base addr + flags
	.if	(PHYS_OFFSET & 0x00f00000)
	orr	r6, r6, #(PHYS_OFFSET & 0x00f00000)
	.endif
	str	r6, [r0]  ;将描述符写入到对应的页表位置,第三个页表描述符建立完成
	
	;........中间省略一段代码,因为条件编译不成立,相当于注释掉...............
	
	mov	pc, lr  ; 从__create_page_tables返回
ENDPROC(__create_page_tables)

#include "head-common.S"

第二段注释:
在arch/arm/include/asm/procinfo.h头文件定义处理器相关信息数据结构

struct proc_info_list {
	unsigned int		cpu_val;
	unsigned int		cpu_mask;
	unsigned long		__cpu_mm_mmu_flags;	/* used by head.S */这个值在哪里被赋值?往下看
	unsigned long		__cpu_io_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_flush;		/* used by head.S */
	const char		*arch_name;
	const char		*elf_name;
	unsigned int		elf_hwcap;
	const char		*cpu_name;
	struct processor	*proc;
	struct cpu_tlb_fns	*tlb;
	struct cpu_user_fns	*user;
	struct cpu_cache_fns	*cache;
};
在文件arch/arm/kernel/vmlinux.lds.S中定义了一个“.proc.info.init”段属性
      __proc_info_begin = .;
         *(.proc.info.init)
      __proc_info_end = .;
因此,以关键字“.proc.info.init”在工程里面搜索,找到arch/arm/mm/proc-arm920.S文件,第三行的定义就是对__cpu_mm_mmu_flags“映射属性标识”的赋值
__arm920_proc_info:
	.long	0x41009200
	.long	0xff00fff0
	.long   PMD_TYPE_SECT | \
		PMD_SECT_BUFFERABLE | \
		PMD_SECT_CACHEABLE | \
		PMD_BIT4 | \
		PMD_SECT_AP_WRITE | \
		PMD_SECT_AP_READ
	.long   PMD_TYPE_SECT | \
		PMD_BIT4 | \
		PMD_SECT_AP_WRITE | \
		PMD_SECT_AP_READ
	b	__arm920_setup
	.long	cpu_arch_name
	.long	cpu_elf_name
	.long	HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
	.long	cpu_arm920_name
	.long	arm920_processor_functions
	.long	v4wbi_tlb_fns
	.long	v4wb_user_fns
#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
	.long	arm920_cache_fns
#else
	.long	v4wt_cache_fns
#endif
	.size	__arm920_proc_info, . - __arm920_proc_info
在arch/arm/include/asm/pgtable-hwdef.h找到上面用到的宏的定义:

#define PMD_TYPE_SECT		(2 << 0) 采用段式地址映射
#define PMD_SECT_BUFFERABLE	(1 << 2)
#define PMD_SECT_CACHEABLE	(1 << 3)
#define PMD_BIT4			(1 << 4)
#define PMD_SECT_AP_WRITE	(1 << 10)
#define PMD_SECT_AP_READ	(1 << 11)
.long是分配长整型长度的空间,在arm9处理器占4个字节(32位)
按位或之后:__cpu_mm_mmu_flags = 0x00000C1E


从临时页表创建原理到具体的代码实现过程可以说到这里我已经消化了。突然又想到一个问题:三张页表描述符创建好了,MMU也启动了,cpu执行过程中究竟是怎么知道什么时候使用对等映射描述表符,什么时候使用内核空间与内核映像的那个描述符,又是什么时候要知道切换使用启动参数那块的映射描述符???

仔细想想就可以知道了,我们知道ttb的“选择子”就是MVA(这里就相当于VA)的高12位,这12位的变化就是cpu自动选择ttb中哪一个描述符的根本原因。arm9从cpu上电开始工作到在SDRAM取指执行时(MMU未开启)PC指针都是0x3打头的,在开始真正运行内核那段head.S代码并且C语言运行环境未设置好时,必须使用位置无关码来实现汇编函数跳转,也就是说这就是为了保持PC是0x3打头的,这样一来,即使之后启动了MMU,我的PC保持原有作风就可以让我的“选择子”等于“0x300”选中1:1的对等映射描述符表项。同理只要往后pc的值不再是0x3打头就会选中0xC打头的描述符表项,具体选中哪一个就看0xCxx了。


内核临时页表的创建过程分析完毕微笑,linux之路又前进了一小步,加油!

<think>好的,我现在要解释内核页表管理机制,以及进程地址空间内核地址空间的区别。首先,我需要回忆一下操作系统中的内存管理知识,特别是关于分页机制的部分。 用户的问题是要求用中文回答,所以我要确保用中文准确表达概念。可能需要先定义页表管理机制,然后分点详细说明。接着比较进程地址空间内核地址空间的不同,可能包括权限、地址范围、映射内容等方面。 首先,内核页表管理机制。页表的作用是将虚拟地址转换为物理地址。内核需要管理自己的页表,同时也需要管理用户进程的页表。每个进程有自己的页表,但内核页表可能部分是共享的。可能需要提到多级页表结构,如PGD、PUD、PMD、PTE等层级。内核如何维护这些结构,例如在进程切换时如何切换页表,或者内核线程是否使用独立的页表。 然后,进程地址空间内核地址空间的区别。进程地址空间是用户态的,通常分为代码段、数据段、堆、栈等,而内核地址空间是所有进程共享的,包含内核代码、数据、设备映射等。内核空间在虚拟地址的高端,比如在32位系统中可能是0xC0000000以上的地址。权限方面,用户进程不能直接访问内核空间,除非通过系统调用陷入内核态。另外,内核地址空间可能包括直接映射区、vmalloc区、固定映射区等不同区域,而进程地址空间则由mm_struct管理,包含vma区域链表。 需要检查是否有混淆的概念,比如是否每个进程都有自己的内核栈,但内核地址空间是共享的。可能还要提到内核页全局目录(PGD)在进程切换时如何加载,比如在x86架构中,CR3寄存器指向当前进程的页表基址,而内核部分可能在所有进程的页表中都有相同的映射,因此切换进程时只需切换用户空间部分,而内核部分保持不变。 另外,可能要考虑不同架构的实现差异,比如ARMx86在页表管理上的不同,但用户的问题可能更关注通用机制,而不是特定架构的细节。 可能还需要解释为什么内核地址空间需要被所有进程共享,例如系统调用时不需要切换页表,可以直接访问内核数据。而进程地址空间是独立的,每个进程有自己的用户态虚拟内存布局。 在页表管理机制中,可能涉及的内核函数如vmalloc、kmalloc的区别,它们如何影响页表。比如vmalloc分配虚拟连续但物理不连续的内存,需要修改页表;而kmalloc使用slab分配器,物理地址连续,位于直接映射区,不需要频繁修改页表。 另外,内核使用“高端内存”来处理32位系统中物理内存超过内核直接映射区的情况,这时需要通过临时映射来访问高端内存,这也涉及到页表的管理。 在比较进程内核地址空间时,可能还要提到内存保护。用户进程的页表项有用户/超级visor权限位,防止用户程序访问内核空间。而内核空间页表通常具有更高的权限,如可读写执行(根据配置)。 最后,需要总结两者的主要区别,确保信息准确,没有遗漏关键点,比如地址范围、共享性、权限、内容等。同时,可能需要举例说明,比如用户进程访问0x08048000(典型的ELF加载地址)内核访问0xC0000000以上的地址。 现在要组织这些思路,分点回答,确保结构清晰,技术细节正确,同时语言通俗易懂。可能还需要检查术语的准确性,比如页表项、TLB、上下文切换时的处理流程等是否正确。</think>### **内核页表管理机制** --- #### **1. 基本概念** **页表(Page Table)**是操作系统将**虚拟地址**映射到**物理地址**的核心数据结构。内核通过多级页表(如PGD、PUD、PMD、PTE)管理内存,以支持虚拟内存机制。 --- #### **2. 关键机制** - **分层结构**(以x86_64为例): ```bash 虚拟地址 → PGD(Page Global Directory) → PUD(Page Upper Directory) → PMD(Page Middle Directory) → PTE(Page Table Entry) → 物理页帧(Page Frame) ``` - **内核页表初始化**: - 内核启动时构建**恒等映射**(直接物理地址=虚拟地址) - 随后建立完整虚拟地址空间(如`__va()``__pa()`转换) - **进程页表管理**: ```c // 进程描述符中的页表指针(mm_struct) struct mm_struct { pgd_t *pgd; // 指向页全局目录 ... }; ``` - 进程切换时通过`cr3`寄存器加载新进程的`pgd` - **内核线程特殊性**: - 内核线程共享内核地址空间,使用当前CPU的`init_mm.pgd` - 无用户空间映射(`mm`字段为NULL) --- ### **进程地址空间 vs 内核地址空间** --- #### **1. 地址范围** | 类型 | 典型32位系统范围 | 典型64位系统范围 | |--------------------|----------------------|-------------------------------| | **进程地址空间** | 0x00000000-0xBFFFFFFF | 0x0000000000000000-0x00007FFFFFFFFFFF | | **内核地址空间** | 0xC0000000-0xFFFFFFFF | 0xFFFF800000000000-0xFFFFFFFFFFFFFFFF | --- #### **2. 核心差异** | 特征 | 进程地址空间 | 内核地址空间 | |---------------------|-------------------------------|----------------------------------| | **隔离性** | 每个进程独立 | 所有进程共享 | | **访问权限** | 用户态可访问(R/W/X) | 仅内核态可访问 | | **内存类型** | 用户代码/堆/栈 | 内核代码/数据/设备映射/缓存 | | **页表切换** | 进程切换时`cr3`寄存器更新 | 内核空间映射固定,不随进程切换改变| | **分配方式** | brk/mmap等系统调用 | vmalloc/kmalloc/page allocator | | **典型地址示例** | 0x08048000(ELF入口地址) | 0xffffffff81000000(内核代码段) | --- #### **3. 映射内容对比** **进程地址空间**: ```bash # 通过/proc/<pid>/maps查看 00400000-00401000 r-xp 00000000 08:01 123456 /bin/app # 代码段 7ffeefbff000-7ffef0000000 rw-p 00000000 00:00 0 # 栈 ``` **内核地址空间**: ```bash # 通过内核符号表查看 ffffffff81000000 T _text # 内核代码段起始 ffffffffc0000000 - ffffffffc0a00000 # 模块映射区域 ``` --- #### **4. 地址转换示例** - **用户空间访问**: ```c char *ptr = malloc(4096); // 用户虚拟地址0x7f2a3bc000 // 转换为物理地址需遍历进程页表 ``` - **内核空间访问**: ```c void *kptr = kmalloc(4096, GFP_KERNEL); // 内核虚拟地址0xffff888007d12000 // 直接映射区地址,物理地址=0x7d12000(假设偏移0xffff888000000000) ``` --- #### **5. 权限保护机制** - **用户→内核防护**: - 页表项中设置`User/Supervisor`位 - 用户态访问内核地址触发**缺页异常**(Page Fault) - **内核自我保护**: ```c // 关键区域设置只读权限(如内核代码段) set_memory_ro(text_start, text_size >> PAGE_SHIFT); ``` --- #### **6. 性能优化** - **TLB管理**: - 进程切换时`INVLPG`指令刷新TLB - 内核使用`global`页避免TLB刷新 - **大页支持**: ```c // 配置2MB/1GB大页减少页表层级 setup_hugetlb_page(...); ``` --- ### **关键区别总结** | **维度** | **进程地址空间** | **内核地址空间** | |------------------|----------------------------------|---------------------------------| | 生命周期 | 随进程创建/销毁 | 系统运行期间持续存在 | | 修改权限 | 用户程序可通过mmap修改 | 仅内核代码可修改 | | DMA访问 | 需要`get_user_pages()`获取物理页 | 直接使用`virt_to_phys()`转换 | | 调试支持 | 用户态调试器(gdb) | Crash工具分析vmcore | | 内存错误处理 | 触发SIGSEGV信号 | 导致内核oops/panic | 掌握这些机制对理解Linux内存管理、调试段错误(Segmentation Fault)及开发内核模块至关重要。实际开发中可通过`/proc/kallsyms`查看内核符号地址,使用`ptrace`跟踪进程内存访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值