Linux中动态修改页面映射属性函数change_page_attr的实现

核心函数功能总结

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页。
    • oldstandard 用于保存旧的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);
}
  • 恢复默认PTEset_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))
  1. #define page_address(page) lowmem_page_address(page)

    • page_address宏定义为lowmem_page_address函数
    • 这是一个内联函数的宏封装
  2. #define PAGE_SHIFT 12

    • 定义页面大小为4KB(2^12 = 4096字节)
    • 这是x86架构的标准页面大小
  3. #define page_to_pfn(page) ((unsigned long)((page) - mem_map))

    • 页帧号转换:将page结构体指针转换为页帧号(Page Frame Number)
    • (page) - mem_map:计算page在全局mem_map数组中的偏移
    • mem_map是全局page结构体数组的起始地址
    • 偏移量就是页帧号(PFN)
  4. #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);
}
  1. static inline:静态内联函数,减少函数调用开销

  2. void *lowmem_page_address(struct page *page):低内存页面地址转换函数

  3. page_to_pfn(page) << PAGE_SHIFT

    • page_to_pfn(page):获取页帧号(PFN)
    • << PAGE_SHIFT:左移12位,将页帧号转换为物理地址
  4. __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);

初始化和检查

  1. 变量声明

    • unsigned long flags:保存中断状态的变量
    • void *ret:返回值
    • struct page_address_slot *pas:页面地址槽指针
  2. 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);

高内存页面处理开始

  1. pas = page_slot(page);

    • 获取页面对应的地址槽(address slot)
    • 通过哈希函数计算:page_slot(page) = &page_address_htable[hash_ptr(page, PA_HASH_ORDER)]
  2. ret = NULL;:初始化返回值为NULL

  3. 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;
			}
		}
	}

哈希查找过程

  1. if (!list_empty(&pas->lh)):检查哈希槽中的链表是否非空

  2. struct page_address_map *pam;:页面地址映射结构指针

    • page_address_map结构通常包含:

      c 复制代码
      struct page_address_map {
          struct page *page;      // 对应的物理页面
          void *virtual;          // 映射的虚拟地址  
          struct list_head list;  // 链表节点
      };
  3. list_for_each_entry(pam, &pas->lh, list):遍历哈希槽链表

    • 宏展开为链表遍历循环
    • 对链表中的每个page_address_map进行检查
  4. if (pam->page == page):找到目标页面

    • 比较映射结构中的page指针与传入的page指针
  5. ret = pam->virtual;:获取映射的虚拟地址

  6. goto done;:跳转到解锁和返回部分

c 复制代码
done:
	spin_unlock_irqrestore(&pas->lock, flags);
	return ret;
}

清理和返回

  1. spin_unlock_irqrestore(&pas->lock, flags);:释放自旋锁并恢复中断状态

  2. 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地址

函数功能总结

核心功能

  1. 统一页面地址转换:为所有物理页面提供虚拟地址查找接口
  2. 分层处理
    • 低内存:直接线性映射,快速高效
    • 高内存:哈希查找,动态映射管理
  3. 并发安全:通过自旋锁保护高内存映射数据结构

高内存页面地址哈希管理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];

哈希表结构定义

  1. struct page_address_slot:定义每个哈希桶的结构

    • struct list_head lh:链表头,用于链接该桶中的所有page_address_map结构
    • spinlock_t lock:自旋锁,保护这个桶的链表并发访问
  2. ____cacheline_aligned_in_smp缓存行对齐修饰符

    • 在SMP(对称多处理)系统中,确保每个结构体对齐到缓存行
    • 防止假共享(False Sharing):不同CPU访问不同桶时不会互相干扰
    • 提高多核环境下的性能
  3. 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)];
}

函数功能:根据页面指针找到对应的哈希槽

  1. static struct page_address_slot *page_slot(struct page *page)

    • 静态函数,输入page指针,返回对应的page_address_slot指针
  2. hash_ptr(page, PA_HASH_ORDER):调用哈希函数计算索引

    • page:要哈希的页面指针
    • PA_HASH_ORDER:7,指定需要7位哈希值(0-127)
  3. &page_address_htable[...]:返回哈希表中对应槽的地址

第三部分:指针哈希函数

c 复制代码
static inline unsigned long hash_ptr(void *ptr, unsigned int bits)
{
	return hash_long((unsigned long)ptr, bits);
}

函数功能:通用指针哈希函数

  1. static inline:内联函数,减少调用开销
  2. void *ptr转换为unsigned long类型,然后调用hash_long
  3. 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);
}

最终处理

  1. 注释说明:"高位更具随机性,所以使用它们"

    • 在哈希计算中,高位通常比低位有更好的随机性
    • 因为低位可能受到对齐等因素的影响
  2. hash >> (BITS_PER_LONG - bits):右移提取高位

    • BITS_PER_LONG:32或64
    • BITS_PER_LONG - bits:计算需要右移的位数

系统设计原理总结

核心功能

  1. 高效映射查找:通过哈希表快速找到高内存页面的虚拟地址映射
  2. 并发安全:每个哈希桶有独立的自旋锁,支持多CPU并发访问
  3. 性能优化:缓存行对齐减少假共享,哈希分散减少冲突

哈希算法设计要点

  • 分布均匀性:确保页面指针均匀分布在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);
}
  1. pgd_t *pgd = pgd_offset_k(address);

    • 获取地址对应的页全局目录项(PGD)
    • PGD是页表的第一级(顶级)
  2. if (pgd_none(*pgd)) return NULL;

    • 检查PGD条目是否为空(未建立映射)
    • 如果为空,说明该地址没有映射,返回NULL
  3. pmd = pmd_offset(pgd, address);

    • 通过PGD和地址获取页中间目录项(PMD)
    • PMD是页表的第二级
  4. if (pmd_none(*pmd)) return NULL;

    • 检查PMD条目是否为空
    • 如果为空,返回NULL
  5. if (pmd_large(*pmd)) return (pte_t *)pmd;

    • 关键检查:判断是否为巨页(2MB/4MB大页)
    • 如果是大页,PMD直接指向物理页面,将其转换为PTE类型返回
  6. 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查找过程

  1. #define pgd_offset_k(address) pgd_offset(&init_mm, address)

    • 内核地址空间查找宏
    • init_mm是初始内存描述符,代表内核的地址空间
  2. #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)

页表项值提取和转换

  1. #define pgd_val(x) ((x).pgd)#define pmd_val(x) ((x).pmd)

    • 提取PGD/PMD结构体中的原始值(通常是unsigned long)
  2. #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状态检查

  1. #define pmd_none(x) (!pmd_val(x))

    • 检查PMD是否为空(值为0)
  2. #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查找过程

  1. #define pmd_page_kernel(pmd) ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))

    • 将PMD值转换为页表物理页的虚拟地址
  2. #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直接指向PTE
  • pmd_offset:直接返回PGD指针(PMD与PGD合并)

函数功能总结

核心功能

  1. 页表遍历:通过虚拟地址逐级查找页表项
  2. 大页支持:自动检测和处理2MB/4MB大页映射
  3. 架构抽象:兼容不同页表层级(二级/三级)
  4. 错误处理:检查空条目并返回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;				\
})
  1. struct page *__page = pg;

    • 创建局部变量保存页面指针,避免多次求值
  2. struct zone *__zone = page_zone(__page);

    • 获取页面所属的内存区域(zone)
    • 每个内存区域有自己的页面管理结构
  3. (unsigned long)(__page - __zone->zone_mem_map)

    • 计算页面在zone内的相对偏移
    • __zone->zone_mem_map:该zone的页面数组起始地址
    • 减法得到页面在zone内的索引
  4. + __zone->zone_start_pfn;

    • 加上zone的起始页帧号,得到全局页帧号
    • zone_start_pfn:该zone在全局页帧号空间的起始位置

第四部分:页帧号到页表项转换

c 复制代码
#define pfn_pte(pfn, prot)	__pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

功能:将页帧号和保护属性组合成页表项

  1. (pfn) << PAGE_SHIFT

    • 将页帧号左移12位(PAGE_SHIFT),转换为物理地址
  2. pgprot_val(prot)

    • 提取保护属性
  3. |:按位或操作,将物理地址和保护属性合并

  4. __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内存

函数功能总结

核心功能

  1. 页表项创建:将物理页面和属性封装成CPU可识别的页表项格式
  2. 地址转换:在页面指针、页帧号、物理地址之间进行转换
  3. 大页支持:支持创建2MB/4MB大页映射
  4. 架构抽象:兼容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位值设置方法

  1. __builtin_constant_p(value):GCC内置函数,检查value是否为编译时常量

    • 如果是常量,返回true
    • 如果是变量,返回false
  2. 条件选择

    • 常量情况 :调用__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位值设置

解析

  1. (unsigned int)(value):提取64位值的低32位
  2. (unsigned int)((value)>>32ULL):右移32位后提取高32位
  3. 调用__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指令:

  1. "\n1:\t":标签1,用于循环跳转

  2. "movl (%0), %%eax\n\t"

    • ptr指向的内存低32位加载到EAX寄存器
    • %0对应第一个输入操作数ptr
  3. "movl 4(%0), %%edx\n\t"

    • ptr+4指向的内存高32位加载到EDX寄存器
    • 与EAX组合形成64位的旧值
  4. "lock cmpxchg8b (%0)\n\t"关键原子指令

    • lock:总线锁前缀,确保操作原子性
    • cmpxchg8b:比较并交换8字节指令
    • 操作:比较EDX:EAX与内存中的值,如果相等,将ECX:EBX的值写入内存
  5. "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位

  1. (unsigned int*)&(x):将64位值的地址转换为32位整数指针
  2. +0:指向低32位
  3. +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_lowll_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转换为数值

设计原理总结

核心功能

  1. 原子性保证:确保64位值的设置操作在多核环境中是原子的
  2. 架构适配:针对不同页表架构提供最优实现
  3. 性能优化:根据值是否为常量选择不同的处理路径

为什么需要这么复杂?

  1. x86架构限制:32位x86 CPU没有直接的64位原子写指令
  2. 并发安全:多核系统中,简单的64位赋值可能被中断
  3. 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_tpte_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_tpte_lowpte_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);
}

功能:增加页面的引用计数,支持大页(复合页)

  1. #ifdef CONFIG_HUGETLB_PAGE:如果配置了大页支持
  2. if (unlikely(PageCompound(page))):检查是否为复合页(大页)
    • PageCompound(page):检查page->flags中的复合页标志
    • unlikely():提示编译器这种情况很少发生,优化分支预测
  3. page = (struct page *)page->private;关键操作
    • 对于复合页,page->private指向该复合页的首页(head page)
    • 将当前页面指针转换为首页指针
  4. 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;

分配过程

  1. spin_unlock_irq(&cpa_lock);临时释放锁

    • 因为alloc_pages可能睡眠,不能持有自旋锁
    • cpa_lock是保护页面属性修改的全局自旋锁
  2. base = alloc_pages(GFP_KERNEL, 0);:分配一个物理页面

    • GFP_KERNEL:标准内核内存分配标志
    • 0:order=0,分配单个页面(4KB)
  3. spin_lock_irq(&cpa_lock);:重新获取锁

    • 分配完成后继续受保护的操作
  4. if (!base) return NULL;:检查分配是否成功

    • 如果分配失败,返回NULL

地址计算和初始化

c 复制代码
	address = __pa(address);
	addr = address & LARGE_PAGE_MASK; 
	pbase = (pte_t *)page_address(base);

地址处理

  1. address = __pa(address);:将虚拟地址转换为物理地址

    • __pa():减去PAGE_OFFSET得到物理地址
  2. addr = address & LARGE_PAGE_MASK;:对齐到大页边界

    • 获取大页的起始物理地址
  3. 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;
}

循环创建页表项

  1. for (i = 0; i < PTRS_PER_PTE; i++, addr += PAGE_SIZE)

    • PTRS_PER_PTE:每个页表包含的页表项数量(如512)
    • 遍历大页中的每个4KB小页
    • 每次循环物理地址增加PAGE_SIZE(4096)
  2. pbase[i] = pfn_pte(addr >> PAGE_SHIFT, ...):创建页表项

    • addr >> PAGE_SHIFT:物理地址右移12位得到页帧号
    • pfn_pte():将页帧号和属性组合成页表项
  3. 属性选择逻辑

    • addr == address ? prot : PAGE_KERNEL
    • 关键 :只有目标地址对应的小页使用新属性prot
    • 其他小页保持默认的PAGE_KERNEL属性
  4. return base;:返回新分配的页表页

设计原理总结

核心功能

  1. 大页分割:将2MB/4MB大页分割为4KB小页,允许单独设置页面属性
  2. 引用计数管理:正确处理复合页的引用计数
  3. 原子性操作:通过自旋锁保护并发访问

关键技术点

  • 锁管理:在可能睡眠的操作前释放自旋锁
  • 复合页处理 :通过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查找

  1. pgd_t *pgd:页全局目录指针

  2. pmd_t *pmd:页中间目录指针

  3. (pgd_t *)page_address(page)

    • page_address(page):获取PGD页面的虚拟地址
    • 转换为pgd_t *类型,指向进程的PGD表
  4. + 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操作

  1. pmd = pmd_offset(pgd, address):获取PMD指针

    • 在二级页表架构中,这通常直接返回PGD指针
    • 因为PMD与PGD合并
  2. 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
  1. __pa(address):将虚拟地址转换为物理地址

    • 通常:物理地址 = 虚拟地址 - PAGE_OFFSET
  2. & LARGE_PAGE_MASK:对齐到大页边界

  3. >> 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 ...其他小页...

函数功能总结

核心功能

  1. 大页恢复:将512个小页映射恢复为单个大页映射
  2. 性能优化:利用大页减少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特性选择最优方法

  1. do { ... } while (0)宏定义的标准技巧

    • 创建一个复合语句块
    • 确保宏在使用时像单个语句一样工作
    • 防止与if/else等控制流语句产生歧义
  2. if (cpu_has_pge):CPU特性检查

    • cpu_has_pge:检查CPU是否支持Page Global Enable功能
    • PGE允许标记全局页面,避免在任务切换时刷新
  3. 分支选择

    • 支持PGE :调用__flush_tlb_global()(需要特殊处理全局页)
    • 不支持PGE :调用__flush_tlb()(简单刷新)

第二部分:全局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)
  1. "movl %1, %%cr4; # turn off PGE \n"

    • 将输入操作数%1(禁用PGE的CR4值)写入CR4控制寄存器
    • 禁用全局页功能,使所有TLB条目都可刷新
  2. "movl %%cr3, %0; \n"

    • 将CR3寄存器(页表基址寄存器)的值读取到tmpreg变量
    • 保存当前的页表基址
  3. "movl %0, %%cr3; # flush TLB \n"

    • tmpreg的值写回CR3寄存器
    • 关键操作:重新加载CR3会刷新整个TLB(除了全局页)
  4. "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)
  1. "movl %%cr3, %0; \n"

    • 读取CR3寄存器到tmpreg变量
  2. "movl %0, %%cr3; # flush TLB \n"

    • 将相同的值写回CR3寄存器
    • 这会刷新整个TLB

刷新TLB时的特殊处理

c 复制代码
// 问题: 当PGE启用时,简单的CR3重写不会刷新全局页
// 解决方案: 四步操作
1. 临时禁用PGE    // 使全局页变为可刷新
2. 保存CR3        // 准备刷新
3. 重写CR3        // 实际刷新TLB(现在包括全局页)
4. 恢复PGE        // 重新启用全局页优化
相关推荐
gfdgd xi4 小时前
GXDE OS 25.2.1 更新了!引入 dtk6,修复系统 bug 若干
linux·运维·ubuntu·操作系统·bug·移植·桌面
qing222222224 小时前
Ubuntu:设置程序开机自启动
linux·运维·ubuntu
Eiceblue4 小时前
使用 Python 向 PDF 添加附件与附件注释
linux·开发语言·vscode·python·pdf
scilwb4 小时前
Ubuntu 22.04 搭建 ROS 2 Humble 环境与创建节点教程
linux
橘子134 小时前
Linux线程同步(四)
linux·c++
Xの哲學5 小时前
Linux Netlink全面解析:从原理到实践
linux·网络·算法·架构·边缘计算
yolo_guo6 小时前
opencv 学习: 04 通过ROI处理图片局部数据,以添加水印为例
linux·c++·opencv
「QT(C++)开发工程师」6 小时前
VTK开源视觉库 | 行业应用第一篇
linux·qt·物联网·计算机视觉·信息可视化·vtk
LCG元6 小时前
记一次线上故障排查:Linux磁盘空间莫名占满,原来是它在"作妖"(附清理脚本)
linux