核心函数功能总结
kernel_map_pages
- 功能:批量修改内核线性映射区页面属性
- 用途:调试时检测内存访问错误(如访问已释放页面)
- 关键特性 :
- 跳过高端内存,只处理直接映射的低端内存
- 自动刷新TLB确保修改生效
change_page_attr / __change_page_attr - 页面属性修改函数
- 功能:逐页修改页表属性,处理大页分割和复合页
- 关键技术 :
- 大页分割:将2MB大页分割为512个4KB小页以单独设置属性
- 引用计数管理:正确处理复合页的引用关系
- 原子操作:确保页表修改的线程安全
lookup_address - 页表遍历
- 功能:通过虚拟地址查找对应的页表项
- 架构适配:自动处理二级/三级页表差异
- 大页检测:识别并处理2MB/4MB大页映射
page_address - 地址转换
- 功能:统一处理低内存和高内存的地址转换
- 智能分层 :
- 低内存:直接线性映射(快速)
- 高内存:哈希表查找(动态映射)
set_pte_atomic - 原子页表操作
- 功能:原子性地设置页表项,确保多核一致性
- 架构优化 :
- 32位系统:单条指令原子操作
- 支持PAE的32位系统 :使用
lock cmpxchg8b指令
set_pmd_pte - 进程页表同步
- 功能:修改所有进程的页表映射(二级页表架构)
- 并发安全:通过自旋锁保护全局页表链表
revert_page - 大页恢复
- 功能:将小页映射重新合并为大页映射
- 性能优化:减少TLB压力,提高内存访问效率
__flush_tlb_all - TLB刷新
- 功能:刷新转换后备缓冲器,确保页表修改生效
- 智能刷新:根据CPU特性选择最优刷新策略
关键技术机制
调试内存访问错误
c
// 设置页面为不可访问
kernel_map_pages(page, numpages, 0); // prot = __pgprot(0)
// 任何访问都会触发页错误,便于调试
大页分割与合并
- 分割时机:需要单独修改大页中某个小页的属性时
- 合并时机:所有小页恢复相同属性且引用计数合适时
- 性能平衡:在灵活性和性能间取得平衡
多进程页表同步
- 问题:内核线性映射在每个进程页表中都有副本
- 解决方案:遍历所有进程页表并统一修改
- 锁机制 :使用
pgd_lock保护全局页表链表
TLB一致性维护
- 立即刷新:页表修改后立即刷新TLB
- 全局页处理:临时禁用PGE确保全局页也被刷新
动态修改内核线性映射区页面的属性kernel_map_pages
c
#ifdef CONFIG_DEBUG_PAGEALLOC
void kernel_map_pages(struct page *page, int numpages, int enable)
{
if (PageHighMem(page))
return;
/* the return value is ignored - the calls cannot fail,
* large pages are disabled at boot time.
*/
change_page_attr(page, numpages, enable ? PAGE_KERNEL : __pgprot(0));
/* we should perform an IPI and flush all tlbs,
* but that can deadlock->flush only current cpu.
*/
__flush_tlb_all();
}
int change_page_attr(struct page *page, int numpages, pgprot_t prot)
{
int err = 0;
int i;
unsigned long flags;
spin_lock_irqsave(&cpa_lock, flags);
for (i = 0; i < numpages; i++, page++) {
err = __change_page_attr(page, prot);
if (err)
break;
}
spin_unlock_irqrestore(&cpa_lock, flags);
return err;
}
static int
__change_page_attr(struct page *page, pgprot_t prot)
{
pte_t *kpte;
unsigned long address;
struct page *kpte_page;
#ifdef CONFIG_HIGHMEM
if (page >= highmem_start_page)
BUG();
#endif
address = (unsigned long)page_address(page);
kpte = lookup_address(address);
if (!kpte)
return -EINVAL;
kpte_page = virt_to_page(kpte);
if (pgprot_val(prot) != pgprot_val(PAGE_KERNEL)) {
if ((pte_val(*kpte) & _PAGE_PSE) == 0) {
pte_t old = *kpte;
pte_t standard = mk_pte(page, PAGE_KERNEL);
set_pte_atomic(kpte, mk_pte(page, prot));
if (pte_same(old,standard))
get_page(kpte_page);
} else {
struct page *split = split_large_page(address, prot);
if (!split)
return -ENOMEM;
get_page(kpte_page);
set_pmd_pte(kpte,address,mk_pte(split, PAGE_KERNEL));
}
} else if ((pte_val(*kpte) & _PAGE_PSE) == 0) {
set_pte_atomic(kpte, mk_pte(page, PAGE_KERNEL));
__put_page(kpte_page);
}
if (cpu_has_pse && (page_count(kpte_page) == 1)) {
list_add(&kpte_page->lru, &df_list);
revert_page(kpte_page, address);
}
return 0;
}
kernel_map_pages 函数
c
#ifdef CONFIG_DEBUG_PAGEALLOC
void kernel_map_pages(struct page *page, int numpages, int enable)
{
if (PageHighMem(page))
return;
change_page_attr(page, numpages, enable ? PAGE_KERNEL : __pgprot(0));
__flush_tlb_all();
}
#ifdef CONFIG_DEBUG_PAGEALLOC:确保代码仅在开启页面分配调试时编译if (PageHighMem(page)) return;:跳过高端内存。高端内存的页不能直接通过线性映射访问,其映射方式不同,所以此函数不处理change_page_attr(...):核心操作,修改页面属性enable ? PAGE_KERNEL : __pgprot(0):如果enable为真,则设置页面属性为PAGE_KERNEL(正常内核映射,具有读/写/执行权限);如果为假,则设置为__pgprot(0)(无权限,不可访问)。这在调试时可用于检测对已释放页面的非法访问
__flush_tlb_all():刷新TLB。页表改变后,必须使TLB(转换后备缓冲器)中旧的缓存项失效,以便CPU使用新的页表项。这里只刷新当前CPU的TLB
change_page_attr 函数
c
int change_page_attr(struct page *page, int numpages, pgprot_t prot)
{
int err = 0;
int i;
unsigned long flags;
spin_lock_irqsave(&cpa_lock, flags);
for (i = 0; i < numpages; i++, page++) {
err = __change_page_attr(page, prot);
if (err)
break;
}
spin_unlock_irqrestore(&cpa_lock, flags);
return err;
}
spin_lock_irqsave(&cpa_lock, flags):获取自旋锁cpa_lock并保存中断状态,然后禁用本地中断。这是为了防止并发修改页表属性- 循环处理每一页 :遍历
numpages指定的页面数量,对每一页调用__change_page_attr。如果某页处理失败(err != 0),则跳出循环 spin_unlock_irqrestore(...):释放锁并恢复中断状态
__change_page_attr 函数 (核心逻辑)
这个函数完成了修改页属性的具体工作
1. 初始检查和地址获取
c
pte_t *kpte;
unsigned long address;
struct page *kpte_page;
#ifdef CONFIG_HIGHMEM
if (page >= highmem_start_page)
BUG();
#endif
address = (unsigned long)page_address(page);
- 变量声明 :
kpte:指向页表项(Page Table Entry, PTE)address:页面的虚拟地址kpte_page:存储kpte所在页框的struct page
#ifdef CONFIG_HIGHMEM ... BUG();:再次检查高端内存,如果传入高端内存页,触发BUG,这是一个安全保证address = (unsigned long)page_address(page);:获取页面的内核虚拟地址
2. 查找页表项
c
kpte = lookup_address(address);
if (!kpte)
return -EINVAL;
kpte_page = virt_to_page(kpte);
lookup_address(address):通过虚拟地址查找对应的页表项(PTE) 。它遍历内核页表(如swapper_pg_dir)来找到该地址的PTE。if (!kpte):如果没找到PTE(地址未映射),返回错误-EINVAL。kpte_page = virt_to_page(kpte):获得PTE本身所在物理页对应的struct page结构。因为PTE也存储在物理页中
3. 修改页表项属性
这里根据是要设置特殊属性还是恢复默认属性,以及当前页面是大页还是普通页,有不同的处理路径
情况A:设置非默认属性 (prot != PAGE_KERNEL)
c
if (pgprot_val(prot) != pgprot_val(PAGE_KERNEL)) {
if ((pte_val(*kpte) & _PAGE_PSE) == 0) {
pte_t old = *kpte;
pte_t standard = mk_pte(page, PAGE_KERNEL);
set_pte_atomic(kpte, mk_pte(page, prot));
if (pte_same(old,standard))
get_page(kpte_page);
} else {
struct page *split = split_large_page(address, prot);
if (!split)
return -ENOMEM;
get_page(kpte_page);
set_pmd_pte(kpte,address,mk_pte(split, PAGE_KERNEL));
}
}
- 判断条件 :
pgprot_val(prot) != pgprot_val(PAGE_KERNEL)比较权限值 - 普通页(非大页) :
(pte_val(*kpte) & _PAGE_PSE) == 0_PAGE_PSE位为0表示这是常规4KB页。old和standard用于保存旧的PTE和标准的PTE。set_pte_atomic(kpte, mk_pte(page, prot)):原子地设置新的PTE ,使用新的权限prot。if (pte_same(old,standard)) get_page(kpte_page):如果旧的PTE和标准PTE相同,说明PTE页之前没有额外引用,现在因为属性特殊化,需要增加PTE页的引用计数。
- 大页(如2MB) :
else分支split_large_page(address, prot):大页无法直接改属性,需要分割大页为多个4KB小页。- 分割失败则返回
-ENOMEM。 - 分割后,增加PTE页的引用计数 (
get_page(kpte_page)),因为分割增加了页表复杂性。 set_pmd_pte(kpte,address,mk_pte(split, PAGE_KERNEL)):设置分割后页面的映射。
情况B:恢复默认属性 (prot == PAGE_KERNEL) 且当前为普通页
c
else if ((pte_val(*kpte) & _PAGE_PSE) == 0) {
set_pte_atomic(kpte, mk_pte(page, PAGE_KERNEL));
__put_page(kpte_page);
}
- 恢复默认PTE :
set_pte_atomic将PTE设为PAGE_KERNEL。 - 减少引用计数 :
__put_page(kpte_page)减少PTE页的引用计数,与情况A中的get_page对应。
4. 尝试恢复大页
c
if (cpu_has_pse && (page_count(kpte_page) == 1)) {
list_add(&kpte_page->lru, &df_list);
revert_page(kpte_page, address);
}
return 0;
- 条件 :CPU支持大页 (
cpu_has_pse),且PTE页的引用计数为1 (page_count(kpte_page) == 1),说明没有其他特殊引用。 - 恢复大页 :
list_add(&kpte_page->lru, &df_list):将PTE页加入到延迟回收列表df_list。revert_page(kpte_page, address):将小页合并回大页,以提升TLB和内存性能。
页面地址转换page_address
c
#define page_address(page) lowmem_page_address(page)
#define PAGE_SHIFT 12
#define page_to_pfn(page) ((unsigned long)((page) - mem_map))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
static inline void *lowmem_page_address(struct page *page)
{
return __va(page_to_pfn(page) << PAGE_SHIFT);
}
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
return lowmem_page_address(page);
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
第一部分:基础宏定义
c
#define page_address(page) lowmem_page_address(page)
#define PAGE_SHIFT 12
#define page_to_pfn(page) ((unsigned long)((page) - mem_map))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
-
#define page_address(page) lowmem_page_address(page)- 将
page_address宏定义为lowmem_page_address函数 - 这是一个内联函数的宏封装
- 将
-
#define PAGE_SHIFT 12- 定义页面大小为4KB(2^12 = 4096字节)
- 这是x86架构的标准页面大小
-
#define page_to_pfn(page) ((unsigned long)((page) - mem_map))- 页帧号转换:将page结构体指针转换为页帧号(Page Frame Number)
(page) - mem_map:计算page在全局mem_map数组中的偏移- mem_map是全局page结构体数组的起始地址
- 偏移量就是页帧号(PFN)
-
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))- 物理地址到虚拟地址转换
PAGE_OFFSET通常是0xC0000000(3GB边界)- 通过简单的加法将物理地址转换为内核线性映射的虚拟地址
第二部分:低内存页面地址转换
c
static inline void *lowmem_page_address(struct page *page)
{
return __va(page_to_pfn(page) << PAGE_SHIFT);
}
-
static inline:静态内联函数,减少函数调用开销 -
void *lowmem_page_address(struct page *page):低内存页面地址转换函数 -
page_to_pfn(page) << PAGE_SHIFT:page_to_pfn(page):获取页帧号(PFN)<< PAGE_SHIFT:左移12位,将页帧号转换为物理地址
-
__va(...):将物理地址转换为内核线性映射的虚拟地址- 例如:物理地址0x5000 → 虚拟地址0xC0005000
第三部分:通用页面地址转换函数
c
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
if (!PageHighMem(page))
return lowmem_page_address(page);
初始化和检查:
-
变量声明:
unsigned long flags:保存中断状态的变量void *ret:返回值struct page_address_slot *pas:页面地址槽指针
-
if (!PageHighMem(page)) return lowmem_page_address(page);- 关键检查:判断页面是否属于低内存
PageHighMem(page):检查page->flags中的高位内存标志- 如果是低内存页面,直接使用简单的线性映射返回地址
- 优化:避免对低内存页面进行复杂的哈希查找
c
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
高内存页面处理开始:
-
pas = page_slot(page);- 获取页面对应的地址槽(address slot)
- 通过哈希函数计算:
page_slot(page) = &page_address_htable[hash_ptr(page, PA_HASH_ORDER)]
-
ret = NULL;:初始化返回值为NULL -
spin_lock_irqsave(&pas->lock, flags);- 获取自旋锁并保存中断状态
- 防止并发访问哈希槽导致的数据竞争
c
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
哈希查找过程:
-
if (!list_empty(&pas->lh)):检查哈希槽中的链表是否非空 -
struct page_address_map *pam;:页面地址映射结构指针-
page_address_map结构通常包含:cstruct page_address_map { struct page *page; // 对应的物理页面 void *virtual; // 映射的虚拟地址 struct list_head list; // 链表节点 };
-
-
list_for_each_entry(pam, &pas->lh, list):遍历哈希槽链表- 宏展开为链表遍历循环
- 对链表中的每个
page_address_map进行检查
-
if (pam->page == page):找到目标页面- 比较映射结构中的page指针与传入的page指针
-
ret = pam->virtual;:获取映射的虚拟地址 -
goto done;:跳转到解锁和返回部分
c
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
清理和返回:
-
spin_unlock_irqrestore(&pas->lock, flags);:释放自旋锁并恢复中断状态 -
return ret;:返回找到的虚拟地址(或NULL如果未找到)
高内存管理机制详解
为什么需要特殊处理高内存?
c
低内存:物理地址 0x00000000 - 0x3FFFFFFF (约1GB)
高内存:物理地址 0x40000000 - ... (超过1GB的部分)
内核虚拟地址空间:0xC0000000 - 0xFFFFFFFF (1GB)
- 问题:1GB的内核虚拟空间无法直接映射所有物理内存
- 解决方案:高内存需要动态映射到内核的"临时映射区"
页面地址映射系统工作原理
否 是 高内存物理页面 通过哈希函数 page_slot 找到对应的page_address_slot 槽中的page_address_map链表 映射结构1 映射结构2 ...更多映射 page指针 virtual虚拟地址 page指针 virtual虚拟地址 page_address函数 高内存? 直接线性映射 哈希查找 遍历链表匹配page 返回virtual地址
函数功能总结
核心功能
- 统一页面地址转换:为所有物理页面提供虚拟地址查找接口
- 分层处理 :
- 低内存:直接线性映射,快速高效
- 高内存:哈希查找,动态映射管理
- 并发安全:通过自旋锁保护高内存映射数据结构
高内存页面地址哈希管理page_slot
c
#define PA_HASH_ORDER 7
/*
* Hash table bucket
*/
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
static struct page_address_slot *page_slot(struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
static inline unsigned long hash_ptr(void *ptr, unsigned int bits)
{
return hash_long((unsigned long)ptr, bits);
}
static inline unsigned long hash_long(unsigned long val, unsigned int bits)
{
unsigned long hash = val;
#if BITS_PER_LONG == 64
/* Sigh, gcc can't optimise this alone like it does for 32 bits. */
unsigned long n = hash;
n <<= 18;
hash -= n;
n <<= 33;
hash -= n;
n <<= 3;
hash += n;
n <<= 3;
hash -= n;
n <<= 4;
hash += n;
n <<= 2;
hash += n;
#else
/* On some cpus multiply is faster, on others gcc will do shifts */
hash *= GOLDEN_RATIO_PRIME;
#endif
/* High bits are more random, so use them. */
return hash >> (BITS_PER_LONG - bits);
}
#if BITS_PER_LONG == 32
/* 2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1 */
#define GOLDEN_RATIO_PRIME 0x9e370001UL
#elif BITS_PER_LONG == 64
/* 2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1 */
#define GOLDEN_RATIO_PRIME 0x9e37fffffffc0001UL
第一部分:哈希表定义和配置
c
#define PA_HASH_ORDER 7
定义:
PA_HASH_ORDER定义为7,表示哈希表有 2^7 = 128 个桶(buckets)- 这个值平衡了内存使用和查找性能,128个桶对于大多数系统足够分散页面地址
c
/*
* Hash table bucket
*/
static struct page_address_slot {
struct list_head lh; /* List of page_address_maps */
spinlock_t lock; /* Protect this bucket's list */
} ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];
哈希表结构定义:
-
struct page_address_slot:定义每个哈希桶的结构struct list_head lh:链表头,用于链接该桶中的所有page_address_map结构spinlock_t lock:自旋锁,保护这个桶的链表并发访问
-
____cacheline_aligned_in_smp:缓存行对齐修饰符- 在SMP(对称多处理)系统中,确保每个结构体对齐到缓存行
- 防止假共享(False Sharing):不同CPU访问不同桶时不会互相干扰
- 提高多核环境下的性能
-
page_address_htable[1<<PA_HASH_ORDER]:定义哈希表数组1<<7= 128个元素的数组- 每个元素是一个
page_address_slot结构体
第二部分:页面到哈希槽的映射
c
static struct page_address_slot *page_slot(struct page *page)
{
return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)];
}
函数功能:根据页面指针找到对应的哈希槽
-
static struct page_address_slot *page_slot(struct page *page)- 静态函数,输入page指针,返回对应的page_address_slot指针
-
hash_ptr(page, PA_HASH_ORDER):调用哈希函数计算索引page:要哈希的页面指针PA_HASH_ORDER:7,指定需要7位哈希值(0-127)
-
&page_address_htable[...]:返回哈希表中对应槽的地址
第三部分:指针哈希函数
c
static inline unsigned long hash_ptr(void *ptr, unsigned int bits)
{
return hash_long((unsigned long)ptr, bits);
}
函数功能:通用指针哈希函数
static inline:内联函数,减少调用开销- 将
void *ptr转换为unsigned long类型,然后调用hash_long bits参数指定需要的哈希值位数
第四部分:长整型哈希函数(核心算法)
c
static inline unsigned long hash_long(unsigned long val, unsigned int bits)
{
unsigned long hash = val;
初始化:
hash = val:用输入值初始化哈希值
64位系统特殊处理
c
#if BITS_PER_LONG == 64
/* Sigh, gcc can't optimise this alone like it does for 32 bits. */
unsigned long n = hash;
n <<= 18;
hash -= n;
n <<= 33;
hash -= n;
n <<= 3;
hash += n;
n <<= 3;
hash -= n;
n <<= 4;
hash += n;
n <<= 2;
hash += n;
64位混合算法 :
这是一个精心设计的位混合操作序列,目的是:
- 打乱输入值的位模式,增加随机性
- 让高位和低位互相影响
- 通过一系列的移位和加减操作实现良好的分布
32位系统处理
c
#else
/* On some cpus multiply is faster, on others gcc will do shifts */
hash *= GOLDEN_RATIO_PRIME;
#endif
32位简化算法:
- 使用黄金比例素数乘法进行哈希
GOLDEN_RATIO_PRIME是一个特殊选择的质数- 乘法操作能够很好地分散哈希值
第五部分:黄金比例素数定义
c
#if BITS_PER_LONG == 32
/* 2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1 */
#define GOLDEN_RATIO_PRIME 0x9e370001UL
#elif BITS_PER_LONG == 64
/* 2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1 */
#define GOLDEN_RATIO_PRIME 0x9e37fffffffc0001UL
黄金比例素数定义:
32位系统:
0x9e370001UL= 2,654,435,841- 数学表达式:2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1
- 这个特殊质数具有良好的哈希分布特性
64位系统:
0x9e37fffffffc0001UL- 数学表达式:2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1
第六部分:哈希值提取
c
/* High bits are more random, so use them. */
return hash >> (BITS_PER_LONG - bits);
}
最终处理:
-
注释说明:"高位更具随机性,所以使用它们"
- 在哈希计算中,高位通常比低位有更好的随机性
- 因为低位可能受到对齐等因素的影响
-
hash >> (BITS_PER_LONG - bits):右移提取高位BITS_PER_LONG:32或64BITS_PER_LONG - bits:计算需要右移的位数
系统设计原理总结
核心功能
- 高效映射查找:通过哈希表快速找到高内存页面的虚拟地址映射
- 并发安全:每个哈希桶有独立的自旋锁,支持多CPU并发访问
- 性能优化:缓存行对齐减少假共享,哈希分散减少冲突
哈希算法设计要点
- 分布均匀性:确保页面指针均匀分布在128个桶中
- 计算效率:在32位系统使用快速乘法,64位系统使用位操作
- 确定性:相同输入总是产生相同输出
- 位利用:使用高位提高随机性
通过虚拟地址查找对应的页表项lookup_address
c
pte_t *lookup_address(unsigned long address)
{
pgd_t *pgd = pgd_offset_k(address);
pmd_t *pmd;
if (pgd_none(*pgd))
return NULL;
pmd = pmd_offset(pgd, address);
if (pmd_none(*pmd))
return NULL;
if (pmd_large(*pmd))
return (pte_t *)pmd;
return pte_offset_kernel(pmd, address);
}
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pmd_index(address) \
(((address) >> PMD_SHIFT) & (PTRS_PER_PMD-1))
#define pgd_page(pgd) \
((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))
#define pmd_val(x) ((x).pmd)
#define pgd_val(x) ((x).pgd)
#define pmd_none(x) (!pmd_val(x))
#define pmd_large(pmd) \
((pmd_val(pmd) & (_PAGE_PSE|_PAGE_PRESENT)) == (_PAGE_PSE|_PAGE_PRESENT))
#define pte_offset_kernel(dir, address) \
((pte_t *) pmd_page_kernel(*(dir)) + pte_index(address))
#define pmd_page_kernel(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define PAGE_MASK (~(PAGE_SIZE-1))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
// 三级页表
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define pmd_offset(dir, address) ((pmd_t *) pgd_page(*(dir)) + \
pmd_index(address))
// 二级页表
#define PMD_SHIFT 22
#define PTRS_PER_PMD 1
static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}
第一部分:主查找函数 lookup_address
c
pte_t *lookup_address(unsigned long address)
{
pgd_t *pgd = pgd_offset_k(address);
pmd_t *pmd;
if (pgd_none(*pgd))
return NULL;
pmd = pmd_offset(pgd, address);
if (pmd_none(*pmd))
return NULL;
if (pmd_large(*pmd))
return (pte_t *)pmd;
return pte_offset_kernel(pmd, address);
}
-
pgd_t *pgd = pgd_offset_k(address);- 获取地址对应的页全局目录项(PGD)
- PGD是页表的第一级(顶级)
-
if (pgd_none(*pgd)) return NULL;- 检查PGD条目是否为空(未建立映射)
- 如果为空,说明该地址没有映射,返回NULL
-
pmd = pmd_offset(pgd, address);- 通过PGD和地址获取页中间目录项(PMD)
- PMD是页表的第二级
-
if (pmd_none(*pmd)) return NULL;- 检查PMD条目是否为空
- 如果为空,返回NULL
-
if (pmd_large(*pmd)) return (pte_t *)pmd;- 关键检查:判断是否为巨页(2MB/4MB大页)
- 如果是大页,PMD直接指向物理页面,将其转换为PTE类型返回
-
return pte_offset_kernel(pmd, address);- 普通页情况:通过PMD和地址获取页表项(PTE)
- PTE是页表的第三级,直接包含物理页帧号
第二部分:PGD相关宏定义
c
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
PGD查找过程:
-
#define pgd_offset_k(address) pgd_offset(&init_mm, address)- 内核地址空间查找宏
init_mm是初始内存描述符,代表内核的地址空间
-
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))- 通用PGD查找宏
(mm)->pgd:内存描述符的PGD基地址+ pgd_index(address):加上地址在PGD中的索引
c
#define pmd_index(address) \
(((address) >> PMD_SHIFT) & (PTRS_PER_PMD-1))
索引计算:
(address) >> PMD_SHIFT:将地址右移得到在PMD中的索引& (PTRS_PER_PMD-1):掩码操作,确保索引在有效范围内
第三部分:页表项到物理页转换
c
#define pgd_page(pgd) \
((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))
#define pmd_val(x) ((x).pmd)
#define pgd_val(x) ((x).pgd)
页表项值提取和转换:
-
#define pgd_val(x) ((x).pgd)和#define pmd_val(x) ((x).pmd)- 提取PGD/PMD结构体中的原始值(通常是unsigned long)
-
#define pgd_page(pgd) ((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))- 将PGD条目转换为对应的物理页的虚拟地址
pgd_val(pgd) & PAGE_MASK:获取物理页地址(清除标志位)__va(...):物理地址转虚拟地址
c
#define PAGE_MASK (~(PAGE_SIZE-1))
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
地址转换宏:
PAGE_MASK:页面大小掩码__va(x):物理地址到内核虚拟地址的转换
第四部分:大页检测宏
c
#define pmd_none(x) (!pmd_val(x))
#define pmd_large(pmd) \
((pmd_val(pmd) & (_PAGE_PSE|_PAGE_PRESENT)) == (_PAGE_PSE|_PAGE_PRESENT))
PMD状态检查:
-
#define pmd_none(x) (!pmd_val(x))- 检查PMD是否为空(值为0)
-
#define pmd_large(pmd):大页检测_PAGE_PSE:Page Size Extension位,表示大页_PAGE_PRESENT:页面存在位- 两个位同时设置表示这是一个有效的大页映射
第五部分:PTE查找宏
c
#define pte_offset_kernel(dir, address) \
((pte_t *) pmd_page_kernel(*(dir)) + pte_index(address))
#define pmd_page_kernel(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
PTE查找过程:
-
#define pmd_page_kernel(pmd) ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))- 将PMD值转换为页表物理页的虚拟地址
-
#define pte_offset_kernel(dir, address):PTE查找pmd_page_kernel(*(dir)):获取PTE表的虚拟地址+ pte_index(address):加上地址在PTE表中的索引
第六部分:不同架构的PMD实现
三级页表架构(如x86 with PAE)
c
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define pmd_offset(dir, address) ((pmd_t *) pgd_page(*(dir)) + \
pmd_index(address))
三级页表特点:
PMD_SHIFT 21:PMD覆盖2^21 = 2MB区域PTRS_PER_PMD 512:每个PGD有512个PMD条目pmd_offset:通过PGD找到PMD表,再加索引
二级页表架构(传统x86)
c
#define PMD_SHIFT 22
#define PTRS_PER_PMD 1
static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}
二级页表特点:
PMD_SHIFT 22:PMD覆盖4MB区域(实际上不存在真正的PMD)PTRS_PER_PMD 1:只有1个PMD,相当于PGD直接指向PTEpmd_offset:直接返回PGD指针(PMD与PGD合并)
函数功能总结
核心功能
- 页表遍历:通过虚拟地址逐级查找页表项
- 大页支持:自动检测和处理2MB/4MB大页映射
- 架构抽象:兼容不同页表层级(二级/三级)
- 错误处理:检查空条目并返回NULL
将物理页面和属性封装成页表项mk_pte
c
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
#define mk_pte_huge(entry) ((entry).pte_low |= _PAGE_PRESENT | _PAGE_PSE)
#define page_to_pfn(pg) \
({ \
struct page *__page = pg; \
struct zone *__zone = page_zone(__page); \
(unsigned long)(__page - __zone->zone_mem_map) \
+ __zone->zone_start_pfn; \
})
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
#define __pte(x) ((pte_t) { (x) } )
#ifdef CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
#define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
#else
typedef struct { unsigned long pte_low; } pte_t;
第一部分:创建页表项的核心宏
c
#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot))
功能:通过页面和页保护属性创建页表项
- 输入:
page(页面结构体指针)和pgprot(页面保护属性) - 输出:
pte_t类型的页表项 - 实现:先通过
page_to_pfn获取页帧号,再通过pfn_pte创建页表项
第二部分:大页表项创建宏
c
#define mk_pte_huge(entry) ((entry).pte_low |= _PAGE_PRESENT | _PAGE_PSE)
功能:将普通页表项转换为大页表项
(entry).pte_low:访问pte_t结构体的低位字段_PAGE_PRESENT:页面存在位(通常为位0)_PAGE_PSE:Page Size Extension位(大页标志)|=:按位或操作,设置这两个标志位
效果:将4KB页表项标记为2MB/4MB大页表项
第三部分:页面到页帧号转换(核心)
c
#define page_to_pfn(pg) \
({ \
struct page *__page = pg; \
struct zone *__zone = page_zone(__page); \
(unsigned long)(__page - __zone->zone_mem_map) \
+ __zone->zone_start_pfn; \
})
-
struct page *__page = pg;- 创建局部变量保存页面指针,避免多次求值
-
struct zone *__zone = page_zone(__page);- 获取页面所属的内存区域(zone)
- 每个内存区域有自己的页面管理结构
-
(unsigned long)(__page - __zone->zone_mem_map)- 计算页面在zone内的相对偏移
__zone->zone_mem_map:该zone的页面数组起始地址- 减法得到页面在zone内的索引
-
+ __zone->zone_start_pfn;- 加上zone的起始页帧号,得到全局页帧号
zone_start_pfn:该zone在全局页帧号空间的起始位置
第四部分:页帧号到页表项转换
c
#define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))
功能:将页帧号和保护属性组合成页表项
-
(pfn) << PAGE_SHIFT- 将页帧号左移12位(PAGE_SHIFT),转换为物理地址
-
pgprot_val(prot)- 提取保护属性
-
|:按位或操作,将物理地址和保护属性合并 -
__pte(...):将数值转换为pte_t类型
第五部分:页表项类型定义
c
#define __pte(x) ((pte_t) { (x) } )
功能 :创建pte_t类型的简单封装
(pte_t) { (x) }:使用C99的复合字面量创建结构体- 将数值x包装成
pte_t结构体
第六部分:不同架构的pte_t定义
物理地址扩展(PAE)模式
c
#ifdef CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
#define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
PAE模式特点:
pte_low:低32位,包含物理地址低位和标志pte_high:高32位,包含物理地址高位- 支持更多物理内存:36位物理地址,支持64GB内存
pte_val宏:
- 将
pte_t结构体转换为64位数值 (x).pte_low:取低32位(unsigned long long)(x).pte_high << 32:高32位左移合并|:合并为64位值
非PAE模式(传统32位)
c
#else
typedef struct { unsigned long pte_low; } pte_t;
传统模式特点:
- 32位页表项:运行在32位CPU上
pte_low:单个32位字段包含所有信息- 限制:20位物理地址,支持4GB内存
函数功能总结
核心功能
- 页表项创建:将物理页面和属性封装成CPU可识别的页表项格式
- 地址转换:在页面指针、页帧号、物理地址之间进行转换
- 大页支持:支持创建2MB/4MB大页映射
- 架构抽象:兼容PAE和非PAE模式
原子设置pte set_pte_atomic
c
#define set_64bit(ptr,value) \
(__builtin_constant_p(value) ? \
__set_64bit_constant(ptr, value) : \
__set_64bit_var(ptr, value) )
static inline void __set_64bit_constant (unsigned long long *ptr,
unsigned long long value)
{
__set_64bit(ptr,(unsigned int)(value), (unsigned int)((value)>>32ULL));
}
static inline void __set_64bit (unsigned long long * ptr,
unsigned int low, unsigned int high)
{
__asm__ __volatile__ (
"\n1:\t"
"movl (%0), %%eax\n\t"
"movl 4(%0), %%edx\n\t"
"lock cmpxchg8b (%0)\n\t"
"jnz 1b"
: /* no outputs */
: "D"(ptr),
"b"(low),
"c"(high)
: "ax","dx","memory");
}
#define ll_low(x) *(((unsigned int*)&(x))+0)
#define ll_high(x) *(((unsigned int*)&(x))+1)
static inline void __set_64bit_var (unsigned long long *ptr,
unsigned long long value)
{
__set_64bit(ptr,ll_low(value), ll_high(value));
}
// 二级页表
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)
// 三级页表
#define set_pte_atomic(pteptr,pteval) \
set_64bit((unsigned long long *)(pteptr),pte_val(pteval))
第一部分:主设置宏
c
#define set_64bit(ptr,value) \
(__builtin_constant_p(value) ? \
__set_64bit_constant(ptr, value) : \
__set_64bit_var(ptr, value) )
功能:智能选择64位值设置方法
-
__builtin_constant_p(value):GCC内置函数,检查value是否为编译时常量- 如果是常量,返回true
- 如果是变量,返回false
-
条件选择:
- 常量情况 :调用
__set_64bit_constant - 变量情况 :调用
__set_64bit_var
- 常量情况 :调用
优化目的:对于常量值,编译器可以生成更优化的代码
第二部分:常量值设置函数
c
static inline void __set_64bit_constant (unsigned long long *ptr,
unsigned long long value)
{
__set_64bit(ptr,(unsigned int)(value), (unsigned int)((value)>>32ULL));
}
功能:处理编译时常量的64位值设置
解析:
(unsigned int)(value):提取64位值的低32位(unsigned int)((value)>>32ULL):右移32位后提取高32位- 调用
__set_64bit函数进行实际的原子设置
第三部分:核心原子设置函数
c
static inline void __set_64bit (unsigned long long * ptr,
unsigned int low, unsigned int high)
{
__asm__ __volatile__ (
"\n1:\t"
"movl (%0), %%eax\n\t"
"movl 4(%0), %%edx\n\t"
"lock cmpxchg8b (%0)\n\t"
"jnz 1b"
: /* no outputs */
: "D"(ptr),
"b"(low),
"c"(high)
: "ax","dx","memory");
}
这是最关键的原子操作实现 ,使用x86的cmpxchg8b指令:
-
"\n1:\t":标签1,用于循环跳转 -
"movl (%0), %%eax\n\t":- 将
ptr指向的内存低32位加载到EAX寄存器 %0对应第一个输入操作数ptr
- 将
-
"movl 4(%0), %%edx\n\t":- 将
ptr+4指向的内存高32位加载到EDX寄存器 - 与EAX组合形成64位的旧值
- 将
-
"lock cmpxchg8b (%0)\n\t":关键原子指令lock:总线锁前缀,确保操作原子性cmpxchg8b:比较并交换8字节指令- 操作:比较EDX:EAX与内存中的值,如果相等,将ECX:EBX的值写入内存
-
"jnz 1b":如果ZF=0(比较不相等),跳回标签1重试
输入操作数:
"D"(ptr):将ptr放入EDI寄存器(%0)"b"(low):将low放入EBX寄存器(%1)"c"(high):将high放入ECX寄存器(%2)
破坏列表:
"ax","dx":声明EAX和EDX寄存器被修改"memory":内存屏障,确保内存访问顺序
第四部分:64位值分解宏
c
#define ll_low(x) *(((unsigned int*)&(x))+0)
#define ll_high(x) *(((unsigned int*)&(x))+1)
功能:将64位值分解为高低32位
(unsigned int*)&(x):将64位值的地址转换为32位整数指针+0:指向低32位+1:指向高32位(指针算术,+1移动4字节)
第五部分:变量值设置函数
c
static inline void __set_64bit_var (unsigned long long *ptr,
unsigned long long value)
{
__set_64bit(ptr,ll_low(value), ll_high(value));
}
功能:处理变量的64位值设置
- 使用
ll_low和ll_high宏提取变量的高低32位 - 调用
__set_64bit进行原子设置
第六部分:页表项设置实现
二级页表架构
c
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
#define set_pte_atomic(pteptr, pteval) set_pte(pteptr,pteval)
- 32位页表项,单条指令即可原子设置
- 直接使用赋值操作
- 不需要复杂的64位原子操作
三级页表架构(PAE模式)
c
#define set_pte_atomic(pteptr,pteval) \
set_64bit((unsigned long long *)(pteptr),pte_val(pteval))
- 64位页表项,需要原子设置
- 使用
set_64bit宏确保原子性 pte_val(pteval):将pte_t转换为数值
设计原理总结
核心功能
- 原子性保证:确保64位值的设置操作在多核环境中是原子的
- 架构适配:针对不同页表架构提供最优实现
- 性能优化:根据值是否为常量选择不同的处理路径
为什么需要这么复杂?
- x86架构限制:32位x86 CPU没有直接的64位原子写指令
- 并发安全:多核系统中,简单的64位赋值可能被中断
- PAE需求:物理地址扩展需要64位页表项
pte_same\get_page\split_large_page
c
#define pte_same(a, b) ((a).pte_low == (b).pte_low)
static inline int pte_same(pte_t a, pte_t b)
{
return a.pte_low == b.pte_low && a.pte_high == b.pte_high;
}
#ifdef CONFIG_HUGETLB_PAGE
static inline void get_page(struct page *page)
{
if (unlikely(PageCompound(page)))
page = (struct page *)page->private;
atomic_inc(&page->_count);
}
#else /* CONFIG_HUGETLB_PAGE */
static inline void get_page(struct page *page)
{
atomic_inc(&page->_count);
}
static struct page *split_large_page(unsigned long address, pgprot_t prot)
{
int i;
unsigned long addr;
struct page *base;
pte_t *pbase;
spin_unlock_irq(&cpa_lock);
base = alloc_pages(GFP_KERNEL, 0);
spin_lock_irq(&cpa_lock);
if (!base)
return NULL;
address = __pa(address);
addr = address & LARGE_PAGE_MASK;
pbase = (pte_t *)page_address(base);
for (i = 0; i < PTRS_PER_PTE; i++, addr += PAGE_SIZE) {
pbase[i] = pfn_pte(addr >> PAGE_SHIFT,
addr == address ? prot : PAGE_KERNEL);
}
return base;
}
第一部分:页表项比较函数
非PAE模式(32位页表项)
c
#define pte_same(a, b) ((a).pte_low == (b).pte_low)
功能:比较两个页表项是否相同
- 在32位系统中,
pte_t只有一个pte_low字段 - 直接比较两个
pte_t的pte_low字段是否相等 - 如果相等,说明两个页表项指向相同的物理页面并有相同的属性
PAE模式(64位页表项)
c
static inline int pte_same(pte_t a, pte_t b)
{
return a.pte_low == b.pte_low && a.pte_high == b.pte_high;
}
功能:比较两个64位页表项是否相同
- PAE模式下,
pte_t有pte_low和pte_high两个字段 - 需要同时比较低32位和高32位
- 只有两部分都相等,页表项才相同
第二部分:页面引用计数增加函数
大页支持情况
c
#ifdef CONFIG_HUGETLB_PAGE
static inline void get_page(struct page *page)
{
if (unlikely(PageCompound(page)))
page = (struct page *)page->private;
atomic_inc(&page->_count);
}
功能:增加页面的引用计数,支持大页(复合页)
#ifdef CONFIG_HUGETLB_PAGE:如果配置了大页支持if (unlikely(PageCompound(page))):检查是否为复合页(大页)PageCompound(page):检查page->flags中的复合页标志unlikely():提示编译器这种情况很少发生,优化分支预测
page = (struct page *)page->private;:关键操作- 对于复合页,
page->private指向该复合页的首页(head page) - 将当前页面指针转换为首页指针
- 对于复合页,
atomic_inc(&page->_count);:原子地增加页面引用计数- 无论是否复合页,最终都增加首页的引用计数
无大页支持情况
c
#else /* CONFIG_HUGETLB_PAGE */
static inline void get_page(struct page *page)
{
atomic_inc(&page->_count);
}
- 没有大页支持时,直接增加页面引用计数
- 不需要处理复合页的特殊情况
第三部分:大页分割函数
c
static struct page *split_large_page(unsigned long address, pgprot_t prot)
{
int i;
unsigned long addr;
struct page *base;
pte_t *pbase;
- 功能:将一个大页(如2MB)分割为多个小页(4KB)
- 参数 :
address:要修改属性的虚拟地址prot:新的页面保护属性
- 返回值:新分配的页表页,包含分割后的小页表项
变量:
i:循环计数器addr:物理地址计算变量base:新分配的页表页面pbase:页表项的基地址
页表页分配
c
spin_unlock_irq(&cpa_lock);
base = alloc_pages(GFP_KERNEL, 0);
spin_lock_irq(&cpa_lock);
if (!base)
return NULL;
分配过程:
-
spin_unlock_irq(&cpa_lock);:临时释放锁- 因为
alloc_pages可能睡眠,不能持有自旋锁 cpa_lock是保护页面属性修改的全局自旋锁
- 因为
-
base = alloc_pages(GFP_KERNEL, 0);:分配一个物理页面GFP_KERNEL:标准内核内存分配标志0:order=0,分配单个页面(4KB)
-
spin_lock_irq(&cpa_lock);:重新获取锁- 分配完成后继续受保护的操作
-
if (!base) return NULL;:检查分配是否成功- 如果分配失败,返回NULL
地址计算和初始化
c
address = __pa(address);
addr = address & LARGE_PAGE_MASK;
pbase = (pte_t *)page_address(base);
地址处理:
-
address = __pa(address);:将虚拟地址转换为物理地址__pa():减去PAGE_OFFSET得到物理地址
-
addr = address & LARGE_PAGE_MASK;:对齐到大页边界- 获取大页的起始物理地址
-
pbase = (pte_t *)page_address(base);:获取新页表页的虚拟地址page_address(base):返回页面基地址的虚拟地址- 转换为
pte_t *类型,用于存储页表项
创建小页表项
c
for (i = 0; i < PTRS_PER_PTE; i++, addr += PAGE_SIZE) {
pbase[i] = pfn_pte(addr >> PAGE_SHIFT,
addr == address ? prot : PAGE_KERNEL);
}
return base;
}
循环创建页表项:
-
for (i = 0; i < PTRS_PER_PTE; i++, addr += PAGE_SIZE):PTRS_PER_PTE:每个页表包含的页表项数量(如512)- 遍历大页中的每个4KB小页
- 每次循环物理地址增加
PAGE_SIZE(4096)
-
pbase[i] = pfn_pte(addr >> PAGE_SHIFT, ...):创建页表项addr >> PAGE_SHIFT:物理地址右移12位得到页帧号pfn_pte():将页帧号和属性组合成页表项
-
属性选择逻辑:
addr == address ? prot : PAGE_KERNEL- 关键 :只有目标地址对应的小页使用新属性
prot - 其他小页保持默认的
PAGE_KERNEL属性
-
return base;:返回新分配的页表页
设计原理总结
核心功能
- 大页分割:将2MB/4MB大页分割为4KB小页,允许单独设置页面属性
- 引用计数管理:正确处理复合页的引用计数
- 原子性操作:通过自旋锁保护并发访问
关键技术点
- 锁管理:在可能睡眠的操作前释放自旋锁
- 复合页处理 :通过
page->private找到首页管理整个大页 - 属性局部化:只修改目标小页属性,保持其他小页不变
- 内存分配:为分割后的页表分配专用页面
修改pmd的页表项set_pmd_pte
c
static void set_pmd_pte(pte_t *kpte, unsigned long address, pte_t pte)
{
struct page *page;
unsigned long flags;
set_pte_atomic(kpte, pte); /* change init_mm */
if (PTRS_PER_PMD > 1)
return;
spin_lock_irqsave(&pgd_lock, flags);
for (page = pgd_list; page; page = (struct page *)page->index) {
pgd_t *pgd;
pmd_t *pmd;
pgd = (pgd_t *)page_address(page) + pgd_index(address);
pmd = pmd_offset(pgd, address);
set_pte_atomic((pte_t *)pmd, pte);
}
spin_unlock_irqrestore(&pgd_lock, flags);
}
第一部分:函数声明和变量定义
c
static void set_pmd_pte(pte_t *kpte, unsigned long address, pte_t pte)
{
struct page *page;
unsigned long flags;
函数功能:设置PMD级别的页表项,并同步更新所有进程的页表
参数解析:
pte_t *kpte:内核页表中找到的页表项指针unsigned long address:要修改的虚拟地址pte_t pte:新的页表项值
变量说明:
struct page *page:用于遍历进程页表的临时指针unsigned long flags:保存中断状态的变量
第二部分:设置初始内存描述符的页表
c
set_pte_atomic(kpte, pte); /* change init_mm */
关键操作:
set_pte_atomic(kpte, pte):原子地设置init_mm的页表项init_mm:初始内存描述符,代表内核的地址空间- 注释说明 :明确这是在修改
init_mm的页表
原子性保证:
set_pte_atomic确保在SMP环境中修改页表项是线程安全的- 防止其他CPU看到不一致的页表状态
第三部分:架构检查提前返回
c
if (PTRS_PER_PMD > 1)
return;
架构适应性检查:
-
PTRS_PER_PMD:每个PMD包含的条目数量 -
三级页表架构(如x86 with PAE):
PTRS_PER_PMD > 1(通常是512)- 有真正的PMD级别,不需要特殊同步
- 直接返回,因为内核线性映射在所有进程中是共享的
-
二级页表架构(传统x86):
PTRS_PER_PMD == 1- PMD与PGD合并,需要特殊处理
- 继续执行同步逻辑
第四部分:同步所有进程页表
c
spin_lock_irqsave(&pgd_lock, flags);
并发保护:
spin_lock_irqsave(&pgd_lock, flags):获取PGD全局锁pgd_lock:保护进程页表列表的自旋锁irqsave:保存中断状态并禁用本地中断,防止死锁
第五部分:遍历所有进程页表
c
for (page = pgd_list; page; page = (struct page *)page->index) {
进程页表遍历循环:
page = pgd_list:从全局pgd_list链表开始page = (struct page *)page->index:通过page->index链接到下一个节点pgd_list:包含系统中所有进程的PGD页面的全局链表
数据结构关系:
pgd_list → page1 → page2 → page3 → ... → NULL
| | |
v v v
PGD1 PGD2 PGD3 (进程页表)
第六部分:计算进程的PGD条目
c
pgd_t *pgd;
pmd_t *pmd;
pgd = (pgd_t *)page_address(page) + pgd_index(address);
进程PGD查找:
-
pgd_t *pgd:页全局目录指针 -
pmd_t *pmd:页中间目录指针 -
(pgd_t *)page_address(page):page_address(page):获取PGD页面的虚拟地址- 转换为
pgd_t *类型,指向进程的PGD表
-
+ pgd_index(address):计算地址在PGD中的索引pgd_index(address):从虚拟地址提取PGD索引位
结果 :pgd指向该进程页表中对应address的PGD条目
第七部分:获取PMD并设置页表项
c
pmd = pmd_offset(pgd, address);
set_pte_atomic((pte_t *)pmd, pte);
PMD操作:
-
pmd = pmd_offset(pgd, address):获取PMD指针- 在二级页表架构中,这通常直接返回PGD指针
- 因为PMD与PGD合并
-
set_pte_atomic((pte_t *)pmd, pte):原子设置页表项(pte_t *)pmd:将PMD指针转换为PTE指针类型- 原子地设置新的页表项值
关键点:对每个进程的页表执行相同的修改操作
第八部分:清理和解锁
c
}
spin_unlock_irqrestore(&pgd_lock, flags);
}
资源释放:
spin_unlock_irqrestore(&pgd_lock, flags):释放PGD锁并恢复中断状态
为什么需要同步所有进程?
在二级页表架构中:
- 每个进程有完整独立的页表,包括内核空间
- 内核线性映射在每个进程的页表中都有副本
- 修改必须同步到所有进程,否则会出现内存视图不一致
这个机制是Linux内核内存管理的基础设施,确保了在多进程环境中内核内存映射的一致性和正确性。
大页恢复revert_page
c
static inline void revert_page(struct page *kpte_page, unsigned long address)
{
pte_t *linear = (pte_t *)
pmd_offset(pgd_offset(&init_mm, address), address);
set_pmd_pte(linear, address,
pfn_pte((__pa(address) & LARGE_PAGE_MASK) >> PAGE_SHIFT,
PAGE_KERNEL_LARGE));
}
第一部分:函数声明和变量定义
c
static inline void revert_page(struct page *kpte_page, unsigned long address)
{
pte_t *linear = (pte_t *)
pmd_offset(pgd_offset(&init_mm, address), address);
函数功能:将小页映射恢复为大页映射
参数解析:
struct page *kpte_page:要恢复的页表页(包含小页PTE的页面)unsigned long address:要恢复的虚拟地址
变量说明:
pte_t *linear:指向线性映射区域中对应地址的PTE指针
第二部分:页表查找过程
c
pte_t *linear = (pte_t *)
pmd_offset(pgd_offset(&init_mm, address), address);
这是一个嵌套的页表查找操作,让我们逐层分解:
第一层:PGD查找
c
pgd_offset(&init_mm, address)
&init_mm:初始内存描述符,代表内核的地址空间pgd_offset(mm, address):计算address在PGD中的偏移- 结果:得到指向对应PGD条目的指针
第二层:PMD查找
c
pmd_offset(pgd, address)
- 从PGD条目获取对应的PMD指针
- 在x86架构中,这可能是:
- 三级页表:真正的PMD指针
- 二级页表:直接返回PGD指针(PMD合并)
第三层:类型转换
c
(pte_t *) pmd_offset(...)
- 将PMD指针转换为
pte_t *类型 - 关键点:实际上这个PMD指针就是原来指向大页的PMD条目
第三部分:创建大页页表项
c
set_pmd_pte(linear, address,
pfn_pte((__pa(address) & LARGE_PAGE_MASK) >> PAGE_SHIFT,
PAGE_KERNEL_LARGE));
1. 物理地址计算
c
(__pa(address) & LARGE_PAGE_MASK) >> PAGE_SHIFT
-
__pa(address):将虚拟地址转换为物理地址- 通常:
物理地址 = 虚拟地址 - PAGE_OFFSET
- 通常:
-
& LARGE_PAGE_MASK:对齐到大页边界 -
>> PAGE_SHIFT:得到页帧号(PFN)
2. 创建大页页表项
c
pfn_pte(pfn, PAGE_KERNEL_LARGE)
pfn_pte():将页帧号和属性组合成页表项PAGE_KERNEL_LARGE:大页的内核映射属性
3. 设置页表项
c
set_pmd_pte(linear, address, ...)
- 将计算好的大页页表项设置到PMD中
- 这会替换原来指向页表页的映射
完整的内存布局变化
恢复后: 大页映射 恢复前: 小页映射 直接指向2MB大页 同一个PMD条目 连续的2MB物理内存 指向页表页 PMD条目 页表页包含512个PTE PTE0: 4KB页0 PTE1: 4KB页1 ...其他小页...
函数功能总结
核心功能
- 大页恢复:将512个小页映射恢复为单个大页映射
- 性能优化:利用大页减少TLB压力,提高内存访问效率
TLB刷新__flush_tlb_all
c
# define __flush_tlb_all() \
do { \
if (cpu_has_pge) \
__flush_tlb_global(); \
else \
__flush_tlb(); \
} while (0)
#define __flush_tlb_global() \
do { \
unsigned int tmpreg; \
\
__asm__ __volatile__( \
"movl %1, %%cr4; # turn off PGE \n" \
"movl %%cr3, %0; \n" \
"movl %0, %%cr3; # flush TLB \n" \
"movl %2, %%cr4; # turn PGE back on \n" \
: "=&r" (tmpreg) \
: "r" (mmu_cr4_features & ~X86_CR4_PGE), \
"r" (mmu_cr4_features) \
: "memory"); \
} while (0)
#define __flush_tlb() \
do { \
unsigned int tmpreg; \
\
__asm__ __volatile__( \
"movl %%cr3, %0; \n" \
"movl %0, %%cr3; # flush TLB \n" \
: "=r" (tmpreg) \
:: "memory"); \
} while (0)
第一部分:主刷新宏
c
# define __flush_tlb_all() \
do { \
if (cpu_has_pge) \
__flush_tlb_global(); \
else \
__flush_tlb(); \
} while (0)
功能:刷新整个TLB,根据CPU特性选择最优方法
-
do { ... } while (0):宏定义的标准技巧- 创建一个复合语句块
- 确保宏在使用时像单个语句一样工作
- 防止与if/else等控制流语句产生歧义
-
if (cpu_has_pge):CPU特性检查cpu_has_pge:检查CPU是否支持Page Global Enable功能- PGE允许标记全局页面,避免在任务切换时刷新
-
分支选择:
- 支持PGE :调用
__flush_tlb_global()(需要特殊处理全局页) - 不支持PGE :调用
__flush_tlb()(简单刷新)
- 支持PGE :调用
第二部分:全局TLB刷新(支持PGE时)
c
#define __flush_tlb_global() \
do { \
unsigned int tmpreg; \
\
__asm__ __volatile__( \
"movl %1, %%cr4; # turn off PGE \n" \
"movl %%cr3, %0; \n" \
"movl %0, %%cr3; # flush TLB \n" \
"movl %2, %%cr4; # turn PGE back on \n" \
: "=&r" (tmpreg) \
: "r" (mmu_cr4_features & ~X86_CR4_PGE), \
"r" (mmu_cr4_features) \
: "memory"); \
} while (0)
-
"movl %1, %%cr4; # turn off PGE \n"- 将输入操作数%1(禁用PGE的CR4值)写入CR4控制寄存器
- 禁用全局页功能,使所有TLB条目都可刷新
-
"movl %%cr3, %0; \n"- 将CR3寄存器(页表基址寄存器)的值读取到
tmpreg变量 - 保存当前的页表基址
- 将CR3寄存器(页表基址寄存器)的值读取到
-
"movl %0, %%cr3; # flush TLB \n"- 将
tmpreg的值写回CR3寄存器 - 关键操作:重新加载CR3会刷新整个TLB(除了全局页)
- 将
-
"movl %2, %%cr4; # turn PGE back on \n"- 将输入操作数%2(原始CR4值)写回CR4
- 重新启用全局页功能
第三部分:简单TLB刷新(不支持PGE时)
c
#define __flush_tlb() \
do { \
unsigned int tmpreg; \
\
__asm__ __volatile__( \
"movl %%cr3, %0; \n" \
"movl %0, %%cr3; # flush TLB \n" \
: "=r" (tmpreg) \
:: "memory"); \
} while (0)
-
"movl %%cr3, %0; \n"- 读取CR3寄存器到
tmpreg变量
- 读取CR3寄存器到
-
"movl %0, %%cr3; # flush TLB \n"- 将相同的值写回CR3寄存器
- 这会刷新整个TLB
刷新TLB时的特殊处理
c
// 问题: 当PGE启用时,简单的CR3重写不会刷新全局页
// 解决方案: 四步操作
1. 临时禁用PGE // 使全局页变为可刷新
2. 保存CR3 // 准备刷新
3. 重写CR3 // 实际刷新TLB(现在包括全局页)
4. 恢复PGE // 重新启用全局页优化