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. 直接返回
// 不需要锁和链表操作
相关推荐
风为你而吹3 小时前
【玩泰山派】8、泰山派安装armbian,玩armbian
linux
qiuiuiu4133 小时前
正点原子RK3568学习日志12-注册字符设备
linux·开发语言·单片机·学习·ubuntu
christine-rr4 小时前
linux常用命令——其他
linux·服务器·网络·数据库·redis·ubuntu
“αβ”4 小时前
了解“网络协议”
linux·服务器·网络·c++·网络协议·tcp/ip·tcp
Doro再努力4 小时前
Linux01:基础指令与相关知识铺垫(一)
linux·运维·服务器
Cyan_RA94 小时前
Linux 远程Ubuntu服务器扩展硬盘后,将/home目录移动到新的硬盘空间上(把新硬盘的分区挂载到/home目录) 教程
linux·运维·ubuntu
_dindong4 小时前
Linux网络编程:Socket编程TCP
linux·服务器·网络·笔记·学习·tcp/ip
tianyuanwo5 小时前
tar打包过滤指定目录指南
linux·tar·过滤式打包
DARLING Zero two♡5 小时前
【Linux操作系统】简学深悟启示录:进程间通信
linux·运维·服务器