Linux中页表缓存初始化pgtable_cache_init函数的实现

为PGD和PMD创建slab分配器pgtable_cache_init

c 复制代码
void __init pgtable_cache_init(void)
{
        if (PTRS_PER_PMD > 1) {
                pmd_cache = kmem_cache_create("pmd",
                                        PTRS_PER_PMD*sizeof(pmd_t),
                                        PTRS_PER_PMD*sizeof(pmd_t),
                                        0,
                                        pmd_ctor,
                                        NULL);
                if (!pmd_cache)
                        panic("pgtable_cache_init(): cannot create pmd cache");
        }
        pgd_cache = kmem_cache_create("pgd",
                                PTRS_PER_PGD*sizeof(pgd_t),
                                PTRS_PER_PGD*sizeof(pgd_t),
                                0,
                                pgd_ctor,
                                PTRS_PER_PMD == 1 ? pgd_dtor : NULL);
        if (!pgd_cache)
                panic("pgtable_cache_init(): Cannot create pgd cache");
}

函数功能概述

这个函数初始化内核的页表缓存系统,为页全局目录(PGD)和页中间目录(PMD)创建专用的slab分配器缓存。这是虚拟内存管理系统的核心基础设施

代码逐行详细解释

第一部分:函数声明和PMD缓存条件检查

c 复制代码
void __init pgtable_cache_init(void)
{
        if (PTRS_PER_PMD > 1) {
  • void __init pgtable_cache_init(void)

    • __init:这是一个宏,表示该函数只在系统初始化阶段使用。初始化完成后,这个函数占用的内存会被释放,不会一直留在内核中
    • 函数名 pgtable_cache_init 明确表示这是页表缓存的初始化函数
    • 无参数,无返回值
  • if (PTRS_PER_PMD > 1)

    • PTRS_PER_PMD 是一个架构相关的常量,表示每个PMD(Page Middle Directory)中包含的指针数量
    • 这个条件检查系统是否使用三级页表架构:
      • 如果 PTRS_PER_PMD > 1:表示使用三级页表(PGD → PMD → PTE)
      • 如果 PTRS_PER_PMD == 1:表示使用两级页表(PGD → PTE),PMD层被省略

第二部分:PMD slab缓存创建

c 复制代码
                pmd_cache = kmem_cache_create("pmd",
                                        PTRS_PER_PMD*sizeof(pmd_t),
                                        PTRS_PER_PMD*sizeof(pmd_t),
                                        0,
                                        pmd_ctor,
                                        NULL);
  • pmd_cache = kmem_cache_create(...)

    • pmd_cache 是一个全局变量,用于存储PMD slab缓存的指针。
    • kmem_cache_create 是内核slab分配器的API,用于创建专用的对象缓存。
  • 参数详细解析

    • "pmd" :缓存的名称字符串,用于调试和/proc/slabinfo中的显示。

    • PTRS_PER_PMD*sizeof(pmd_t) :第一个大小参数是对象大小

      • pmd_t 是PMD项的数据类型
      • PTRS_PER_PMD 是每个PMD中的项数
      • 计算结果是分配一个完整PMD表所需的总内存大小
    • PTRS_PER_PMD*sizeof(pmd_t) :第二个大小参数是对齐大小

      • 这里与对象大小相同,确保每个PMD对象正确对齐
      • 对齐对于CPU访问效率和缓存性能很重要
    • 0:标志位,控制缓存的行为。

      • 0表示使用默认标志,不设置特殊行为
    • pmd_ctor构造函数,在从缓存分配对象时自动调用。

      • 用于初始化PMD内存,将其所有项设置为空或无效状态
      • 确保新分配的PMD处于已知的清洁状态
    • NULL析构函数,在对象返回到缓存时调用。

      • 这里设为NULL,表示PMD对象不需要特殊的清理操作
c 复制代码
                if (!pmd_cache)
                        panic("pgtable_cache_init(): cannot create pmd cache");
        }
  • if (!pmd_cache)

    • 检查 kmem_cache_create 的返回值,如果返回NULL表示缓存创建失败。
  • panic("pgtable_cache_init(): cannot create pmd cache")

    • panic 是内核的紧急处理函数,会使系统停止运行。
    • 如果PMD缓存创建失败,系统无法正常进行内存管理,必须立即停止。
    • 错误信息明确指出了失败的原因和位置。

第三部分:PGD slab缓存创建

c 复制代码
        pgd_cache = kmem_cache_create("pgd",
                                PTRS_PER_PGD*sizeof(pgd_t),
                                PTRS_PER_PGD*sizeof(pgd_t),
                                0,
                                pgd_ctor,
                                PTRS_PER_PMD == 1 ? pgd_dtor : NULL);
  • PGD缓存创建,与PMD类似但有一些重要区别:

  • 参数解析

    • "pgd":缓存名称,标识这是PGD缓存

    • PTRS_PER_PGD*sizeof(pgd_t):对象大小,计算一个完整PGD表所需内存

      • pgd_t 是PGD项的数据类型
      • PTRS_PER_PGD 是每个PGD中的项数
    • PTRS_PER_PGD*sizeof(pgd_t):对齐大小

    • 0:标志位。

    • pgd_ctor:PGD的构造函数,用于初始化新分配的PGD。

    • PTRS_PER_PMD == 1 ? pgd_dtor : NULL条件析构函数

      • 这是一个三元条件表达式:
        • 如果 PTRS_PER_PMD == 1(两级页表),使用 pgd_dtor 析构函数
        • 否则(三级页表),使用 NULL(无析构函数)
      • 这是因为在两级页表架构中,PGD需要特殊的清理操作
c 复制代码
        if (!pgd_cache)
                panic("pgtable_cache_init(): Cannot create pgd cache");
}
  • if (!pgd_cache)

    • 检查PGD缓存是否创建成功。
  • panic("pgtable_cache_init(): Cannot create pgd cache")

    • 如果PGD缓存创建失败,触发内核恐慌。
    • PGD是页表的根目录,没有它系统完全无法进行虚拟内存管理。

架构差异的深层理解

三级页表架构(如x86 with PAE)

复制代码
虚拟地址: [PGD索引][PMD索引][PTE索引][页内偏移]
页表结构: PGD → PMD → PTE → 物理页
  • PTRS_PER_PMD > 1:需要独立的PMD缓存
  • PGD不需要特殊析构

两级页表架构(如早期x86)

复制代码
虚拟地址: [PGD索引][PTE索引][页内偏移]  
页表结构: PGD → PTE → 物理页
  • PTRS_PER_PMD == 1:PMD被优化掉,不需要PMD缓存
  • PGD可能需要特殊清理,因此需要析构函数

PGD缓存析构过程pgd_dtor

c 复制代码
static inline void pgd_list_del(pgd_t *pgd)
{
        struct page *next, **pprev, *page = virt_to_page(pgd);
        next = (struct page *)page->index;
        pprev = (struct page **)page->private;
        *pprev = next;
        if (next)
                next->private = (unsigned long)pprev;
}
/* never called when PTRS_PER_PMD > 1 */
void pgd_dtor(void *pgd, kmem_cache_t *cache, unsigned long unused)
{
        unsigned long flags; /* can be called from interrupt context */

        spin_lock_irqsave(&pgd_lock, flags);
        pgd_list_del(pgd);
        spin_unlock_irqrestore(&pgd_lock, flags);
}

函数功能概述

这两个函数协同工作,用于从全局PGD链表中安全地删除一个PGD页表。这是两级页表架构中PGD缓存析构过程的核心部分

代码逐行详细解释

第一部分:pgd_list_del - 链表删除核心逻辑

c 复制代码
static inline void pgd_list_del(pgd_t *pgd)
{
        struct page *next, **pprev, *page = virt_to_page(pgd);
  • static inline void pgd_list_del(pgd_t *pgd)

    • static:函数只在当前文件内可见
    • inline:建议编译器内联展开,避免函数调用开销
    • pgd_t *pgd:参数是要删除的PGD页表的虚拟地址
  • 变量声明

    • struct page *next:存储链表中的下一个节点
    • struct page **pprev:指向前驱节点的next指针的指针
    • struct page *page = virt_to_page(pgd)关键转换
      • virt_to_page(pgd) 将PGD的虚拟地址转换为对应的struct page结构指针
      • 在内核中,每个物理页对应一个struct page结构
      • 这里利用struct page来存储链表的指针信息
c 复制代码
        next = (struct page *)page->index;
        pprev = (struct page **)page->private;
  • next = (struct page *)page->index

    • page->index 存储着链表中下一个节点的地址
    • 这里进行了类型转换,将unsigned long转换为struct page *
    • 这是Linux内核中常见的"借用"技巧,利用struct page的字段存储自定义数据
  • pprev = (struct page **)page->private

    • page->private 存储着指向前驱节点next指针的地址
    • 转换为struct page **类型,即指向指针的指针
    • 这种设计实现了高效的链表删除操作

第二部分:双向链表删除算法

c 复制代码
        *pprev = next;
        if (next)
                next->private = (unsigned long)pprev;
  • *pprev = next

    • 这是双向链表删除的核心操作
    • 更新前驱节点的next指针,使其指向当前节点的下一个节点
    • 这样就从链表中移除了当前节点
  • if (next) next->private = (unsigned long)pprev

    • 如果存在下一个节点,更新它的private字段
    • 让下一个节点的pprev指向前驱节点的next指针位置
    • 这样就完成了双向链表的完整性维护

第三部分:pgd_dtor - 析构函数实现

c 复制代码
/* never called when PTRS_PER_PMD > 1 */
void pgd_dtor(void *pgd, kmem_cache_t *cache, unsigned long unused)
{
        unsigned long flags; /* can be called from interrupt context */
  • /* never called when PTRS_PER_PMD > 1 */

    • 明确说明这个函数只在两级页表架构中被调用
    • 在三级页表架构中不会使用
  • 函数签名

    • void *pgd:要销毁的PGD对象
    • kmem_cache_t *cache:所属的slab缓存(未使用)
    • unsigned long unused:未使用的参数
  • unsigned long flags

    • 用于保存中断状态的变量
    • 这个函数从中断上下文调用

第四部分:中断安全的锁保护

c 复制代码
        spin_lock_irqsave(&pgd_lock, flags);
        pgd_list_del(pgd);
        spin_unlock_irqrestore(&pgd_lock, flags);
}
  • spin_lock_irqsave(&pgd_lock, flags)

    • 获取自旋锁并保存当前中断状态
    • pgd_lock 是保护PGD链表的全局自旋锁
    • irqsave 变体会在获取锁时禁用中断,防止死锁
  • pgd_list_del(pgd)

    • 在锁保护下调用实际的链表删除函数
  • spin_unlock_irqrestore(&pgd_lock, flags)

    • 释放自旋锁并恢复之前的中断状态
    • 这是关键的安全操作,确保系统稳定性

数据结构关系详解

PGD链表结构

复制代码
全局PGD链表:
pgd_list_head → page1 → page2 → page3 → ... → NULL
    ↑           ↑       ↑       ↑
    |           |       |       |
通过pprev/next指针连接

每个struct page的字段使用:
- page->index:    存储next指针 (struct page *)
- page->private:  存储pprev指针 (struct page **)

PGD缓存初始化过程pgd_ctor

c 复制代码
static inline void pgd_list_add(pgd_t *pgd)
{
        struct page *page = virt_to_page(pgd);
        page->index = (unsigned long)pgd_list;
        if (pgd_list)
                pgd_list->private = (unsigned long)&page->index;
        pgd_list = page;
        page->private = (unsigned long)&pgd_list;
}
void pgd_ctor(void *pgd, kmem_cache_t *cache, unsigned long unused)
{
        unsigned long flags;

        if (PTRS_PER_PMD == 1)
                spin_lock_irqsave(&pgd_lock, flags);

        memcpy((pgd_t *)pgd + USER_PTRS_PER_PGD,
                        swapper_pg_dir + USER_PTRS_PER_PGD,
                        (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));

        if (PTRS_PER_PMD > 1)
                return;

        pgd_list_add(pgd);
        spin_unlock_irqrestore(&pgd_lock, flags);
        memset(pgd, 0, USER_PTRS_PER_PGD*sizeof(pgd_t));
}

函数功能概述

这两个函数协同工作,用于初始化新的PGD页表并将其添加到全局PGD链表中。主要功能包括复制内核空间映射、管理PGD链表以及在两级页表架构中的特殊处理

代码逐行详细解释

第一部分:pgd_list_add - 链表添加核心逻辑

c 复制代码
static inline void pgd_list_add(pgd_t *pgd)
{
        struct page *page = virt_to_page(pgd);
  • static inline void pgd_list_add(pgd_t *pgd)

    • static inline:内联函数,减少函数调用开销
    • pgd_t *pgd:要添加到链表的PGD虚拟地址
  • struct page *page = virt_to_page(pgd)

    • 将PGD虚拟地址转换为对应的struct page结构
    • 这是内核中物理页管理的核心数据结构
c 复制代码
        page->index = (unsigned long)pgd_list;
        if (pgd_list)
                pgd_list->private = (unsigned long)&page->index;
  • page->index = (unsigned long)pgd_list

    • 设置新页面的index字段为当前链表头
    • 新节点指向原来的链表头,实现头部插入
  • if (pgd_list) pgd_list->private = (unsigned long)&page->index

    • 如果链表不为空,更新原链表头节点的private字段
    • 让原链表头指向新节点的index字段位置
    • 维护双向链表的完整性
c 复制代码
        pgd_list = page;
        page->private = (unsigned long)&pgd_list;
}
  • pgd_list = page

    • 更新全局链表头指针,指向新添加的页面
    • 完成头部插入操作
  • page->private = (unsigned long)&pgd_list

    • 设置新页面的private字段指向链表头指针的地址
    • 这样新节点可以直接找到链表头

第二部分:pgd_ctor - 构造函数实现

c 复制代码
void pgd_ctor(void *pgd, kmem_cache_t *cache, unsigned long unused)
{
        unsigned long flags;

        if (PTRS_PER_PMD == 1)
                spin_lock_irqsave(&pgd_lock, flags);
  • 函数参数

    • void *pgd:要初始化的PGD内存
    • kmem_cache_t *cache:所属的slab缓存(未使用)
    • unsigned long unused:未使用的参数
  • if (PTRS_PER_PMD == 1) spin_lock_irqsave(&pgd_lock, flags)

    • 条件加锁:只在两级页表架构中需要锁保护
    • 因为只有在两级页表中才需要操作共享的PGD链表
    • 三级页表架构不需要链表管理
c 复制代码
        memcpy((pgd_t *)pgd + USER_PTRS_PER_PGD,
                        swapper_pg_dir + USER_PTRS_PER_PGD,
                        (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
  • 关键的内存复制操作
    • 源地址swapper_pg_dir + USER_PTRS_PER_PGD

      • swapper_pg_dir 是内核初始页全局目录
      • USER_PTRS_PER_PGD 是用户空间PGD项的数量
      • 所以这是内核空间映射的起始位置
    • 目标地址(pgd_t *)pgd + USER_PTRS_PER_PGD

      • 新PGD的内核空间部分
    • 复制大小(PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t)

      • 只复制内核空间部分,不复制用户空间部分
c 复制代码
        if (PTRS_PER_PMD > 1)
                return;

        pgd_list_add(pgd);
        spin_unlock_irqrestore(&pgd_lock, flags);
  • if (PTRS_PER_PMD > 1) return

    • 如果是三级页表架构,直接返回
    • 不需要后续的链表操作和用户空间初始化
  • pgd_list_add(pgd)

    • 将新PGD添加到全局链表
    • 只在两级页表架构中执行
  • spin_unlock_irqrestore(&pgd_lock, flags)

    • 释放锁并恢复中断状态
c 复制代码
        memset(pgd, 0, USER_PTRS_PER_PGD*sizeof(pgd_t));
}
  • memset(pgd, 0, USER_PTRS_PER_PGD*sizeof(pgd_t))
    • 将用户空间部分的PGD项清零
    • 只清零用户空间部分,内核空间部分已经复制完成
    • 确保新进程的用户空间映射初始为空白

数据结构关系详解

内存布局理解

复制代码
PGD内存布局:
+------------------+ 高地址
|   内核空间映射   | ← 从swapper_pg_dir复制而来
+------------------+ USER_PTRS_PER_PGD边界
|   用户空间映射   | ← 被memset清零
+------------------+ 低地址

总大小:PTRS_PER_PGD * sizeof(pgd_t)
用户空间:USER_PTRS_PER_PGD项
内核空间:PTRS_PER_PGD - USER_PTRS_PER_PGD项

链表结构构建过程

复制代码
初始状态:
pgd_list = NULL

添加第一个PGD后:
pgd_list → page1
page1->index = NULL
page1->private = &pgd_list

添加第二个PGD后:
pgd_list → page2 → page1
page2->index = page1
page2->private = &pgd_list  
page1->private = &page2->index

架构差异处理详解

两级页表架构(PTRS_PER_PMD == 1)

c 复制代码
// 完整执行路径:
1. 获取锁
2. 复制内核映射
3. 添加到PGD链表
4. 释放锁
5. 清零用户映射

三级页表架构(PTRS_PER_PMD > 1)

c 复制代码
// 简化执行路径:
1. 复制内核映射
2. 直接返回
// 不需要锁和链表操作
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言