内核临时页表建立

本文详细介绍了在保护模式下,内核如何构建临时页表来实现对内存的有效寻址,包括页全局目录和页表的初始化过程。
背景:为什么要建立内核临时页表:当内核被解压到线性地址0x100000后,为了继续启动内核,即启动内核的第一进程即swapper进程,内核需要建立一张临时页表供其使用。
 
当内核从16位的实模式进入到保护模式(通过在汇编代码中的setup函数中设置linux的cr0寄存器的PE位),内核要创建一个有限的地址空 间,容纳内核的代码段、数据段、初始页表和用于存放动态数据结构的共128KB大小的空间,此时通常的程序设计者假定,内核使用的段、临时页表和 128KB的内存范围可以全部放在RAM前8MB的空间内。于是我们需要做的工作是建立一个页表可以对内存的前8MB的物理地址进行寻址。
 
由于进程的线性地址空间分成两部分:
0x00000000到0xbfffffff(0-3G)的线性地址,无论进程运行在用户态还是内核态都可以寻址。
0xc0000000到0xffffffff(3G-4GB)的线性地址,只有内核态进程才可以寻址。
 
为了保证在实模式和保护模式下都可以很容易的对这8MB寻址。因此,内核必须建立两个映射。把从0x00000000到0x007fffff的线性 地址和从0xc0000000到0xc07fffff的线性地址都映射到从0x00000000到0x007fffff的物理地址中。
此时可以通过与物理地址相同的线性地址或者通过从0xc0000000开始的8MB线性地址对RAM的前8MB进行寻址。
 
开始建立内核临时页表
内核的临时页表是通过arch/i386/kernel/head.s汇编代码中的startup_32()函数实现的。
建立一个完整的二级页表,需要建立一个页全局目录和一个页表。由于只需要映射8MB的地址空间,一个页表可以容纳1024项,每个页大小为4k,8MB=2*1024*4k;所以只需要2个页全局目录项和一张页表即可。
 
页全局目录项的建立:
页全局目录项有1024项,但是我们每次寻址8MB的空间只需要2个页全局目录项即可,由于我们要同时考虑用户态和内核态的寻址所以我们需要4个页全局目录项分别寻址8MB的用户空间和8MB的内核空间。
要建立页全局目录我们首先要知道页全局目录存放的物理地址,而变量swapper_pg_dir 存放了页全局目录的线性地址
(swapper_pg_dir的线性地址可以在/boot/System.map文件中找到)通过执行swapper_pg_dir - __PAGE_OFFSET计算可以获得swapper_pg_dir 的物理地址(其中__PAGE_OFFSET为0xc0000000是内核线性空间的起始地址)。
知道了临时页全局目录的地址之后,要做的便是初始化临时页全局目录:
页全局目录的初步初始化:
417 ENTRY(swapper_pg_dir)
418     .fill 1024,4,0
这两行汇编代码执行了页全局目录的初步初始化,它的意思是从swapper_pg_dir开始,填充1024项,每项为4字节,值为0,正好是4K一个页面。
页全局目录进一步初始化:
为映射8MB地址空间,内核通过填充swapper_pg_dir中第0项,1项,768项和769项实现(768和769是通过计算内核线性地址 空间对应的页目录偏移量获得的)。前两项是给用户线性地址映射,后两项给内核线性地址映射。内核会将swapper_gp_dir的0项和768项字段设 置为pg0的物理地址(pg0中存放第一张页表的地址),而1项和769项设置为紧随pg0后的页框的物理地址(一般是pg0+4k)。


页表的建立
由于物理地址8MB=2*1024*4k,可以对应2048个页框,建立两个页表的话只需从0x00000000开始,以此将每隔0x1000的地址填入页表中的每一项中即可:0x0000,0x1000,0x2000,…0x3ff00
0x40000,0x41000,…0x7ff000
 
 
内核中有变量pg0,表示对应的页表。建立页表的过程如下:
091 page_pde_offset = (__PAGE_OFFSET >> 20);
092
093  movl $(pg0 - __PAGE_OFFSET), %edi
094  movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
095  movl $0x007, %eax
096 10:
097 leal 0x007(%edi),%ecx
098 movl %ecx,(%edx)
099 movl %ecx,page_pde_offset(%edx)
100 addl $4,%edx
101 movl $1024, %ecx
102 11:
103 stosl
104         addl $0x1000,%eax
105         loop 11b
106        
107        
108         leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
109         cmpl %ebp,%eax
110         jb 10b
111         movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
大致意思是从0开始,把连续的线性地址映射到物理地址。0x007正好表示PRESENT+RW+USER(在内存中,可读写,用户页面,这样在用 户态和内核 态都可读写)。由于每个页表项有32位,但其实只需保存物理地址的高20位 就够了,所以剩下的低12位可以用来表示页的属性。
结束条件:从代码中可知,当映射到当前所操作的页表项往下INIT_MAP_BEYOND_END(128K)处映射结束。

建成的页表示意图如下

 

这样总的页表建立之后的示意图如下


开启页面映射之后就可以引用内核的变量,但是还不能启动start_kernel,要启动swapper进程还需要设置内核堆栈
193        
194         lss stack_start,%esp
然后设置中断向量表
215         call setup_idt
检查CPU类型
载入gdt(原来的gdt是临时的)和ldt
302         lgdt cpu_gdt_descr
303         lidt idt_descr
最后,调用start_kernel
327         call start_kernel
至此启动start_kernel函数
<think>好的,我现在要解释内核页表管理机制,以及进程地址空间与内核地址空间的区别。首先,我需要回忆一下操作系统中的内存管理知识,特别是关于分页机制的部分。 用户的问题是要求用中文回答,所以我要确保用中文准确表达概念。可能需要先定义页表管理机制,然后分点详细说明。接着比较进程地址空间和内核地址空间的不同,可能包括权限、地址范围、映射内容等方面。 首先,内核页表管理机制。页表的作用是将虚拟地址转换为物理地址。内核需要管理自己的页表,同时也需要管理用户进程的页表。每个进程有自己的页表,但内核页表可能部分是共享的。可能需要提到多级页表结构,如PGD、PUD、PMD、PTE等层级。内核如何维护这些结构,例如在进程切换时如何切换页表,或者内核线程是否使用独立的页表。 然后,进程地址空间和内核地址空间的区别。进程地址空间是用户态的,通常分为代码段、数据段、堆、栈等,而内核地址空间是所有进程共享的,包含内核代码、数据、设备映射等。内核空间在虚拟地址的高端,比如在32位系统中可能是0xC0000000以上的地址。权限方面,用户进程不能直接访问内核空间,除非通过系统调用陷入内核态。另外,内核地址空间可能包括直接映射区、vmalloc区、固定映射区等不同区域,而进程地址空间则由mm_struct管理,包含vma区域链表。 需要检查是否有混淆的概念,比如是否每个进程都有自己的内核栈,但内核地址空间是共享的。可能还要提到内核页全局目录(PGD)在进程切换时如何加载,比如在x86架构中,CR3寄存器指向当前进程的页表基址,而内核部分可能在所有进程的页表中都有相同的映射,因此切换进程时只需切换用户空间部分,而内核部分保持不变。 另外,可能要考虑不同架构的实现差异,比如ARM和x86在页表管理上的不同,但用户的问题可能更关注通用机制,而不是特定架构的细节。 可能还需要解释为什么内核地址空间需要被所有进程共享,例如系统调用时不需要切换页表,可以直接访问内核数据。而进程地址空间是独立的,每个进程有自己的用户态虚拟内存布局。 在页表管理机制中,可能涉及的内核函数如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`跟踪进程内存访问。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值