Linux中页表缓存初始化pgtable_cache_init函数的实现
为PGD和PMD创建slab分配器pgtable_cache_init
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缓存条件检查
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缓存创建
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对象不需要特殊的清理操作
-
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缓存创建
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需要特殊的清理操作
- 这是一个三元条件表达式:
-
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
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
- 链表删除核心逻辑
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
来存储链表的指针信息
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 **
类型,即指向指针的指针 - 这种设计实现了高效的链表删除操作
第二部分:双向链表删除算法
*pprev = next;if (next)next->private = (unsigned long)pprev;
-
*pprev = next
- 这是双向链表删除的核心操作
- 更新前驱节点的next指针,使其指向当前节点的下一个节点
- 这样就从链表中移除了当前节点
-
if (next) next->private = (unsigned long)pprev
- 如果存在下一个节点,更新它的private字段
- 让下一个节点的
pprev
指向前驱节点的next指针位置 - 这样就完成了双向链表的完整性维护
第三部分:pgd_dtor
- 析构函数实现
/* 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
- 用于保存中断状态的变量
- 这个函数从中断上下文调用
第四部分:中断安全的锁保护
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
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
- 链表添加核心逻辑
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
结构 - 这是内核中物理页管理的核心数据结构
- 将PGD虚拟地址转换为对应的
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字段位置
- 维护双向链表的完整性
pgd_list = page;page->private = (unsigned long)&pgd_list;
}
-
pgd_list = page
- 更新全局链表头指针,指向新添加的页面
- 完成头部插入操作
-
page->private = (unsigned long)&pgd_list
- 设置新页面的private字段指向链表头指针的地址
- 这样新节点可以直接找到链表头
第二部分:pgd_ctor
- 构造函数实现
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链表
- 三级页表架构不需要链表管理
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)
- 只复制内核空间部分,不复制用户空间部分
-
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)
- 释放锁并恢复中断状态
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)
// 完整执行路径:
1. 获取锁
2. 复制内核映射
3. 添加到PGD链表
4. 释放锁
5. 清零用户映射
三级页表架构(PTRS_PER_PMD > 1)
// 简化执行路径:
1. 复制内核映射
2. 直接返回
// 不需要锁和链表操作