Linux内存管理深度剖析:从处理文件映射页面的缺页异常到页面缓存的完整架构
前言
在现代操作系统的核心机制中,内存管理扮演着连接硬件资源与应用程序的关键角色,其设计质量直接影响着系统性能、资源利用率和可扩展性。Linux作为一款成熟的企业级操作系统,其内存管理子系统经过数十年的演进与发展,形成了一套复杂而精密的架构体系。本文将深入剖析Linux内核内存管理的核心机制,从最基础的缺页异常处理,到复杂的文件映射管理,再到高效的页面缓存系统。
通过逐行分析关键源码,我们将系统性地揭示:
- 缺页异常处理如何智能区分匿名映射与文件映射,实现内存的按需分配
- 文件映射机制如何通过特殊的页表项设计支持非线性映射和延迟加载
- 页面缓存系统如何通过预读算法和缓存管理优化I/O性能
- 并发控制机制如何通过精细的锁设计和原子操作保证多处理器环境下的数据一致性
- 资源管理策略如何通过引用计数和LRU算法实现内存的高效利用
从do_file_page的缺页处理到filemap_getpage的智能缓存查找,从install_page的物理映射建立到install_file_pte的特殊映射处理,从lock_page的睡眠锁机制到radix_tree_preload的预加载优化——我们将构建一个从微观操作到宏观架构的完整知识体系。
处理文件映射页面的缺页异常do_file_page
/** Fault of a previously existing named mapping. Repopulate the pte* from the encoded file_pte if possible. This enables swappable* nonlinear vmas.*/
static int do_file_page(struct mm_struct * mm, struct vm_area_struct * vma,unsigned long address, int write_access, pte_t *pte, pmd_t *pmd)
{unsigned long pgoff;int err;BUG_ON(!vma->vm_ops || !vma->vm_ops->nopage);/** Fall back to the linear mapping if the fs does not support* ->populate:*/if (!vma->vm_ops || !vma->vm_ops->populate || (write_access && !(vma->vm_flags & VM_SHARED))) {pte_clear(pte);return do_no_page(mm, vma, address, write_access, pte, pmd);}pgoff = pte_to_pgoff(*pte);pte_unmap(pte);spin_unlock(&mm->page_table_lock);err = vma->vm_ops->populate(vma, address & PAGE_MASK, PAGE_SIZE, vma->vm_page_prot, pgoff, 0);if (err == -ENOMEM)return VM_FAULT_OOM;if (err)return VM_FAULT_SIGBUS;return VM_FAULT_MAJOR;
}
函数功能分析
这个函数处理文件映射页面的缺页异常,尝试从编码的文件页表项中重新填充页面,特别支持可交换的非线性虚拟内存区域(VMA)。
函数参数和基本检查
static int do_file_page(struct mm_struct * mm, struct vm_area_struct * vma,unsigned long address, int write_access, pte_t *pte, pmd_t *pmd)
{unsigned long pgoff;int err;BUG_ON(!vma->vm_ops || !vma->vm_ops->nopage);
- 参数说明:接收内存结构、虚拟内存区域、故障地址、写访问标志和页表项指针
- 严格验证:使用
BUG_ON确保VMA具有必要的文件操作函数,特别是nopage方法
回退条件检查
/** Fall back to the linear mapping if the fs does not support* ->populate:*/if (!vma->vm_ops || !vma->vm_ops->populate || (write_access && !(vma->vm_flags & VM_SHARED))) {pte_clear(pte);return do_no_page(mm, vma, address, write_access, pte, pmd);}
- 回退条件1:文件系统不支持
populate操作 - 回退条件2:写访问且非共享映射(需要写时复制)
- 回退操作:清空当前PTE,调用
do_no_page进行标准的缺页处理
文件偏移提取和锁释放
pgoff = pte_to_pgoff(*pte);pte_unmap(pte);spin_unlock(&mm->page_table_lock);
- 偏移计算:
pte_to_pgoff从页表项中提取文件偏移量,用于非线性映射 - 资源释放:解除页表映射并释放页表锁,为可能阻塞的文件操作做准备
文件页面填充操作
err = vma->vm_ops->populate(vma, address & PAGE_MASK, PAGE_SIZE, vma->vm_page_prot, pgoff, 0);if (err == -ENOMEM)return VM_FAULT_OOM;if (err)return VM_FAULT_SIGBUS;return VM_FAULT_MAJOR;
- 页面填充:调用文件系统的
populate方法重新建立页面映射 - 地址对齐:
address & PAGE_MASK确保页面边界对齐 - 错误处理:
-ENOMEM→ 内存不足错误- 其他错误 → 总线错误信号
- 成功 → 主要缺页错误(涉及磁盘I/O)
函数功能总结
do_file_page是处理文件映射缺页的核心函数,专门针对支持非线性映射的文件系统。它通过文件系统的populate操作高效重建页面映射,特别优化了可交换非线性VMA的场景。当文件系统不支持高级特性或需要写时复制时,会优雅地回退到标准的do_no_page处理路径,确保了系统的兼容性和健壮性。
填充文件映射的虚拟内存区域filemap_populate
static int filemap_populate(struct vm_area_struct *vma,unsigned long addr,unsigned long len,pgprot_t prot,unsigned long pgoff,int nonblock)
{struct file *file = vma->vm_file;struct address_space *mapping = file->f_mapping;struct inode *inode = mapping->host;unsigned long size;struct mm_struct *mm = vma->vm_mm;struct page *page;int err;if (!nonblock)force_page_cache_readahead(mapping, vma->vm_file,pgoff, len >> PAGE_CACHE_SHIFT);repeat:size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;if (pgoff + (len >> PAGE_CACHE_SHIFT) > size)return -EINVAL;page = filemap_getpage(file, pgoff, nonblock);if (!page && !nonblock)return -ENOMEM;if (page) {err = install_page(mm, vma, addr, page, prot);if (err) {page_cache_release(page);return err;}} else {err = install_file_pte(mm, vma, addr, pgoff, prot);if (err)return err;}len -= PAGE_SIZE;addr += PAGE_SIZE;pgoff++;if (len)goto repeat;return 0;
}
函数功能分析
这个函数负责填充文件映射的虚拟内存区域,通过获取文件页面并建立页表映射,支持非线性文件映射的按需加载。
函数参数和变量声明
static int filemap_populate(struct vm_area_struct *vma,unsigned long addr,unsigned long len,pgprot_t prot,unsigned long pgoff,int nonblock)
{struct file *file = vma->vm_file;struct address_space *mapping = file->f_mapping;struct inode *inode = mapping->host;unsigned long size;struct mm_struct *mm = vma->vm_mm;struct page *page;int err;
- 参数说明:接收VMA、地址范围、保护权限、文件偏移和非阻塞标志
- 关键变量:获取文件、地址空间、
inode等核心数据结构,准备页面获取和安装
预读优化处理
if (!nonblock)force_page_cache_readahead(mapping, vma->vm_file,pgoff, len >> PAGE_CACHE_SHIFT);
- 条件预读:仅在阻塞模式下触发强制页面缓存预读
- 性能优化:预先读取后续可能访问的文件页面到缓存
- 参数计算:
len >> PAGE_CACHE_SHIFT将字节长度转换为页面数量
文件边界检查
repeat:size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;if (pgoff + (len >> PAGE_CACHE_SHIFT) > size)return -EINVAL;
- 文件大小计算:
i_size_read(inode)获取文件大小,向上取整到页面边界 - 越界检查:确保请求的文件偏移范围不超过文件实际大小
- 安全防护:防止访问文件范围外的数据,返回
-EINVAL错误
页面获取处理
page = filemap_getpage(file, pgoff, nonblock);if (!page && !nonblock)return -ENOMEM;
- 页面获取:调用
filemap_getpage尝试从页面缓存获取或创建页面 - 阻塞语义:非阻塞模式下允许页面不存在,后续通过设置PTE为文件偏移即可
页面安装逻辑
if (page) {err = install_page(mm, vma, addr, page, prot);if (err) {page_cache_release(page);return err;}} else {err = install_file_pte(mm, vma, addr, pgoff, prot);if (err)return err;}
成功获取页面:
install_page:将物理页面映射到页表- 错误处理:释放页面引用计数并返回错误
页面获取失败:
install_file_pte:安装特殊的文件PTE,编码文件偏移信息- 为后续缺页处理保留映射信息,支持按需加载
循环处理多个页面
len -= PAGE_SIZE;addr += PAGE_SIZE;pgoff++;if (len)goto repeat;return 0;
- 进度更新:减少剩余长度,递增地址和文件偏移
- 循环控制:如果还有剩余页面,跳回
repeat标签继续处理 - 成功返回:所有页面处理完成返回0
函数功能总结
filemap_populate是文件映射内存填充的核心函数,实现了智能的页面加载策略。它通过结合页面缓存预读、安全的边界检查、灵活的页面获取机制,以及物理页面与文件PTE的混合安装方式,为非线性文件映射提供了高效的按需加载支持。这种设计既保证了内存使用的效率(避免立即分配所有物理页面),又提供了完整的虚拟地址映射,是现代操作系统虚拟内存管理的精妙体现。
将文件页面映射到进程虚拟地址空间install_page
/** Install a file page to a given virtual memory address, release any* previously existing mapping.*/
int install_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, struct page *page, pgprot_t prot)
{struct inode *inode;pgoff_t size;int err = -ENOMEM;pte_t *pte;pgd_t *pgd;pmd_t *pmd;pte_t pte_val;pgd = pgd_offset(mm, addr);spin_lock(&mm->page_table_lock);pmd = pmd_alloc(mm, pgd, addr);if (!pmd)goto err_unlock;pte = pte_alloc_map(mm, pmd, addr);if (!pte)goto err_unlock;/** This page may have been truncated. Tell the* caller about it.*/err = -EINVAL;inode = vma->vm_file->f_mapping->host;size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;if (!page->mapping || page->index >= size)goto err_unlock;zap_pte(mm, vma, addr, pte);mm->rss++;flush_icache_page(vma, page);set_pte(pte, mk_pte(page, prot));page_add_file_rmap(page);pte_val = *pte;pte_unmap(pte);update_mmu_cache(vma, addr, pte_val);err = 0;
err_unlock:spin_unlock(&mm->page_table_lock);return err;
}
函数功能分析
install_page函数实现了将文件页面映射到进程虚拟地址空间的核心操作,包括页表设置、反向映射建立和缓存维护。
函数参数和变量声明
int install_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, struct page *page, pgprot_t prot)
{struct inode *inode;pgoff_t size;int err = -ENOMEM;pte_t *pte;pgd_t *pgd;pmd_t *pmd;pte_t pte_val;
- 参数说明:接收内存结构、VMA区域、目标地址、物理页面和页面保护权限
- 关键变量:准备多级页表操作所需的指针和错误码
页表锁获取和全局目录查找
pgd = pgd_offset(mm, addr);spin_lock(&mm->page_table_lock);
- PGD查找:
pgd_offset根据虚拟地址找到页全局目录项 - 内存锁获取:获取内存的页表自旋锁,保护并发页表操作
中间页目录分配
pmd = pmd_alloc(mm, pgd, addr);if (!pmd)goto err_unlock;
- PMD分配:分配中间页目录项,如果需要会创建新的页中间目录
- 错误处理:分配失败跳转到解锁并返回错误
页表项分配和映射
pte = pte_alloc_map(mm, pmd, addr);if (!pte)goto err_unlock;
- PTE分配:分配页表项并将其映射到内核虚拟地址空间
- 资源检查:确保页表项分配成功
页面有效性验证
err = -EINVAL;inode = vma->vm_file->f_mapping->host;size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;if (!page->mapping || page->index >= size)goto err_unlock;
- 文件信息获取:从VMA获取对应的
inode和文件大小 - 大小计算:将文件字节大小转换为页面数量(向上取整)
- 截断检查:验证页面仍有有效映射且索引未超出文件范围
- 安全防护:防止安装已被截断或无效的页面
现有映射清理
zap_pte(mm, vma, addr, pte);
- 旧映射清除:移除该地址可能存在的旧页表映射
- TLB维护:确保过时的转换后备缓冲区条目被无效化
新映射建立
mm->rss++;flush_icache_page(vma, page);set_pte(pte, mk_pte(page, prot));
- 内存统计:增加进程的常驻集大小计数
- 页表项设置:创建新的页表项并原子性地设置
反向映射和缓存更新
page_add_file_rmap(page);pte_val = *pte;pte_unmap(pte);update_mmu_cache(vma, addr, pte_val);err = 0;
- 反向映射:建立页面到VMA的反向关联,用于后续操作如换出
- PTE保存和解映射:保存页表项值后解除内核映射
- MMU缓存更新:更新架构特定的MMU缓存
- 成功标记:设置操作成功返回码
错误处理和资源清理
err_unlock:spin_unlock(&mm->page_table_lock);return err;
}
- 统一错误出口:所有错误路径都汇集到这里
- 锁释放:确保页表锁被正确释放
- 结果返回:返回操作状态(0成功,负数为错误码)
函数功能总结
install_page函数是Linux内存管理中文件页面映射的核心例程,它完整实现了从物理页面到虚拟地址空间的映射建立过程。通过精细的锁管理、多级页表操作、安全性检查和性能优化,该函数确保了:
- 原子性操作:通过页表锁保护并发访问
- 资源管理:正确分配和释放页表资源
- 安全性验证:防止安装无效或被截断的页面
- 性能优化:维护缓存一致性和反向映射信息
- 错误恢复:全面的错误检测和资源清理机制
移除PTE映射zap_pte
static inline void zap_pte(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, pte_t *ptep)
{pte_t pte = *ptep;if (pte_none(pte))return;if (pte_present(pte)) {unsigned long pfn = pte_pfn(pte);flush_cache_page(vma, addr);pte = ptep_clear_flush(vma, addr, ptep);if (pfn_valid(pfn)) {struct page *page = pfn_to_page(pfn);if (!PageReserved(page)) {if (pte_dirty(pte))set_page_dirty(page);page_remove_rmap(page);page_cache_release(page);mm->rss--;}}} else {if (!pte_file(pte))free_swap_and_cache(pte_to_swp_entry(pte));pte_clear(ptep);}
}
函数功能分析
zap_pte函数是页表项清理的核心例程,负责安全地移除PTE映射并管理相关的物理内存和交换空间资源。
函数参数和PTE读取
static inline void zap_pte(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, pte_t *ptep)
{pte_t pte = *ptep;if (pte_none(pte))return;
- 参数说明:接收内存结构、VMA区域、虚拟地址和页表项指针
- PTE读取:先读取当前页表项的值,避免后续操作中的竞态条件
- 空项检查:如果PTE为空(无映射),直接返回,无需进一步处理
物理页面映射处理
if (pte_present(pte)) {unsigned long pfn = pte_pfn(pte);flush_cache_page(vma, addr);pte = ptep_clear_flush(vma, addr, ptep);
- 存在性检查:
pte_present检查PTE是否映射到物理内存 - PFN提取:从PTE中提取物理帧号
- 原子清除:
ptep_clear_flush原子性地清除PTE并刷新TLB
物理页面资源管理
if (pfn_valid(pfn)) {struct page *page = pfn_to_page(pfn);if (!PageReserved(page)) {if (pte_dirty(pte))set_page_dirty(page);page_remove_rmap(page);page_cache_release(page);mm->rss--;}}}
- PFN有效性验证:
pfn_valid确保物理帧号有效 - 页面结构获取:
pfn_to_page将PFN转换为page结构 - 保留页面检查:跳过内核保留的特殊页面
- 脏页处理:如果PTE标记为脏,同步设置页面脏标志
- 反向映射移除:
page_remove_rmap解除页面到VMA的映射关系 - 引用计数释放:
page_cache_release减少页面引用计数 - 内存统计更新:减少进程的常驻集大小计数
交换条目处理
else {if (!pte_file(pte))free_swap_and_cache(pte_to_swp_entry(pte));pte_clear(ptep);}
}
- 文件PTE检查:
pte_file检查是否为特殊的文件映射PTE - 交换空间释放:对于交换PTE,释放对应的交换条目和缓存页面
- PTE清除:
pte_clear简单地清除页表项内容
关键设计要点
-
先读取后操作:先保存PTE值,避免在操作过程中PTE被其他CPU修改
-
原子性操作:使用
ptep_clear_flush确保PTE清除和TLB刷新的原子性 -
引用计数:正确管理页面的引用计数,确保页面在无引用时被释放
-
交换空间:及时释放不再需要的交换条目,避免交换空间泄漏
-
TLB维护:立即刷新TLB,防止陈旧的地址转换
函数功能总结
zap_pte函数是Linux内存管理系统中页表操作的基础构建块,它提供了安全、高效的页表项清理机制。通过精细的状态检查、资源管理和并发控制,该函数确保了:
- 完整性:正确处理各种类型的PTE(物理映射、交换条目、文件映射)
- 安全性:防止资源泄漏,确保所有相关资源都被正确释放
- 并发安全:使用原子操作避免多处理器环境下的竞态条件
建立文件映射的特殊页表项install_file_pte
/** Install a file pte to a given virtual memory address, release any* previously existing mapping.*/
int install_file_pte(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, unsigned long pgoff, pgprot_t prot)
{int err = -ENOMEM;pte_t *pte;pgd_t *pgd;pmd_t *pmd;pte_t pte_val;pgd = pgd_offset(mm, addr);spin_lock(&mm->page_table_lock);pmd = pmd_alloc(mm, pgd, addr);if (!pmd)goto err_unlock;pte = pte_alloc_map(mm, pmd, addr);if (!pte)goto err_unlock;zap_pte(mm, vma, addr, pte);set_pte(pte, pgoff_to_pte(pgoff));pte_val = *pte;pte_unmap(pte);update_mmu_cache(vma, addr, pte_val);spin_unlock(&mm->page_table_lock);return 0;err_unlock:spin_unlock(&mm->page_table_lock);return err;
}
函数功能分析
install_file_pte函数专门用于建立文件映射的特殊页表项,这种PTE不直接指向物理页面,而是编码了文件偏移信息,用于支持非线性文件映射。
函数参数和变量声明
int install_file_pte(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long addr, unsigned long pgoff, pgprot_t prot)
{int err = -ENOMEM;pte_t *pte;pgd_t *pgd;pmd_t *pmd;pte_t pte_val;
- 参数说明:接收内存结构、VMA区域、虚拟地址、文件页面偏移和保护权限
- 错误码预设:初始化为
-ENOMEM,假设内存分配可能失败 - 页表指针:准备多级页表操作所需的变量
页表锁获取和全局目录查找
pgd = pgd_offset(mm, addr);spin_lock(&mm->page_table_lock);
- PGD定位:
pgd_offset根据虚拟地址找到页全局目录项 - 并发控制:获取内存的页表自旋锁,保护页表操作的原子性
中间页目录分配
pmd = pmd_alloc(mm, pgd, addr);if (!pmd)goto err_unlock;
- PMD分配:分配或查找中间页目录项
- 错误处理:如果分配失败,跳转到错误处理路径
页表项分配和映射
pte = pte_alloc_map(mm, pmd, addr);if (!pte)goto err_unlock;
- PTE分配:分配页表项并将其映射到内核地址空间
- 资源检查:确保页表项分配成功,否则跳转到错误处理
现有映射清理
zap_pte(mm, vma, addr, pte);
- 旧映射清除:调用
zap_pte移除该地址可能存在的任何现有映射 - 资源释放:确保相关的物理页面或交换条目被正确释放
文件PTE设置
set_pte(pte, pgoff_to_pte(pgoff));
- 文件偏移编码:
pgoff_to_pte将文件页面偏移编码到PTE中 - 特殊PTE设置:设置的不是常规的物理页面映射,而是包含文件偏移的特殊PTE
- 标志设置:这种PTE通常设置
_PAGE_FILE标志,表示是文件映射而非物理映射
缓存更新和资源清理
pte_val = *pte;pte_unmap(pte);update_mmu_cache(vma, addr, pte_val);spin_unlock(&mm->page_table_lock);return 0;
- PTE值保存:保存设置后的PTE值用于缓存更新
- 解映射:
pte_unmap解除页表项的内核映射 - MMU缓存更新:
update_mmu_cache更新架构特定的MMU缓存 - 锁释放:释放页表锁
- 成功返回:返回0表示操作成功
错误处理路径
err_unlock:spin_unlock(&mm->page_table_lock);return err;
}
- 统一错误出口:所有错误情况都跳转到这里
- 锁释放保证:确保在错误路径上也释放页表锁
- 错误码返回:返回相应的错误代码
函数功能总结
install_file_pte函数是Linux非线性文件映射机制的核心组件,它通过创建特殊的文件页表项来实现虚拟地址与文件偏移的灵活映射。与install_page函数不同,它不建立到物理页面的直接映射,而是创建一种"占位符"映射,在后续的页面错误处理中才会实际加载物理页面。
这种设计的主要优势包括:
- 延迟加载:避免立即分配所有物理内存,按需加载文件内容
- 映射灵活性:支持任意的文件页面到虚拟地址的映射关系
- 内存效率:多个进程可以共享相同的文件映射,减少内存占用
- 交换支持:文件PTE可以轻松处理页面换入换出
智能的页面缓存查找和读取策略filemap_getpage
static struct page * filemap_getpage(struct file *file, unsigned long pgoff,int nonblock)
{struct address_space *mapping = file->f_mapping;struct page *page;int error;/** Do we have something in the page cache already?*/
retry_find:page = find_get_page(mapping, pgoff);if (!page) {if (nonblock)return NULL;goto no_cached_page;}/** Ok, found a page in the page cache, now we need to check* that it's up-to-date.*/if (!PageUptodate(page))goto page_not_uptodate;success:/** Found the page and have a reference on it.*/mark_page_accessed(page);return page;no_cached_page:error = page_cache_read(file, pgoff);/** The page we want has now been added to the page cache.* In the unlikely event that someone removed it in the* meantime, we'll just come back here and read it again.*/if (error >= 0)goto retry_find;/** An error return from page_cache_read can result if the* system is low on memory, or a problem occurs while trying* to schedule I/O.*/return NULL;page_not_uptodate:lock_page(page);/* Did it get unhashed while we waited for it? */if (!page->mapping) {unlock_page(page);goto err;}/* Did somebody else get it up-to-date? */if (PageUptodate(page)) {unlock_page(page);goto success;}if (!mapping->a_ops->readpage(file, page)) {wait_on_page_locked(page);if (PageUptodate(page))goto success;}/** Umm, take care of errors if the page isn't up-to-date.* Try to re-read it _once_. We do this synchronously,* because there really aren't any performance issues here* and we need to check for errors.*/lock_page(page);/* Somebody truncated the page on us? */if (!page->mapping) {unlock_page(page);goto err;}/* Somebody else successfully read it in? */if (PageUptodate(page)) {unlock_page(page);goto success;}ClearPageError(page);if (!mapping->a_ops->readpage(file, page)) {wait_on_page_locked(page);if (PageUptodate(page))goto success;}/** Things didn't work out. Return zero to tell the* mm layer so, possibly freeing the page cache page first.*/
err:page_cache_release(page);return NULL;
}
函数功能分析
filemap_getpage函数实现了智能的页面缓存查找和读取策略,通过多级回退机制确保页面数据的有效性和及时性。
页面缓存查找阶段
static struct page * filemap_getpage(struct file *file, unsigned long pgoff,int nonblock)
{struct address_space *mapping = file->f_mapping;struct page *page;int error;retry_find:page = find_get_page(mapping, pgoff);if (!page) {if (nonblock)return NULL;goto no_cached_page;}
- 参数说明:接收文件对象、页面偏移和非阻塞标志
- 缓存查找:在地址空间的基数树中查找指定偏移的页面
- 非阻塞处理:如果页面不存在且是非阻塞模式,直接返回NULL
- 缓存缺失:页面不存在时跳转到缓存缺失处理路径
页面状态验证
if (!PageUptodate(page))goto page_not_uptodate;success:mark_page_accessed(page);return page;
- 数据有效性检查:验证页面数据是否为最新(未被修改或失效)
- 成功路径:页面存在且数据最新时,标记页面被访问并返回
- 访问标记:
mark_page_accessed更新页面的访问历史,影响LRU算法
缓存缺失处理
no_cached_page:error = page_cache_read(file, pgoff);if (error >= 0)goto retry_find;return NULL;
- 页面缓存读取:调用
page_cache_read从磁盘读取页面到缓存 - 重试机制:读取成功后重新尝试查找页面
- 错误处理:读取失败(内存不足或I/O调度问题)返回NULL
数据过时处理
page_not_uptodate:lock_page(page);if (!page->mapping) {unlock_page(page);goto err;}if (PageUptodate(page)) {unlock_page(page);goto success;}
- 页面加锁:防止并发修改页面状态
- 映射有效性检查:确保页面仍然属于当前地址空间(未被截断)
- 竞态条件处理:检查其他线程是否已更新页面数据
首次读取尝试
if (!mapping->a_ops->readpage(file, page)) {wait_on_page_locked(page);if (PageUptodate(page))goto success;}
- 文件系统读取:调用文件系统特定的
readpage方法读取数据 - 等待完成:等待页面I/O操作完成
- 成功检查:验证读取后页面数据是否变为最新
同步重试机制
lock_page(page);if (!page->mapping) {unlock_page(page);goto err;}if (PageUptodate(page)) {unlock_page(page);goto success;}ClearPageError(page);if (!mapping->a_ops->readpage(file, page)) {wait_on_page_locked(page);if (PageUptodate(page))goto success;}
- 二次加锁:重新获取页面锁进行同步重试
- 状态复查:再次检查页面映射和数据状态
- 错误清除:清除可能的先前错误状态
- 最终读取尝试:同步方式进行最后一次读取尝试
错误处理和资源清理
err:page_cache_release(page);return NULL;
- 错误路径:所有失败情况最终汇集到这里
- 引用释放:释放之前获取的页面引用计数
- 失败返回:返回NULL表示页面获取失败
函数功能总结
filemap_getpage函数体现了Linux内核页面缓存管理的复杂性和健壮性设计。它通过多级回退机制、竞态条件处理、同步重试策略,确保了在并发环境下页面数据的一致性和可靠性。函数的核心价值在于:
- 性能优化:优先使用缓存数据,减少不必要的磁盘I/O
- 并发安全:通过精细的锁管理和状态检查处理各种竞态条件
- 错误恢复:提供多次重试机会,提高在临时I/O故障下的成功率
- 资源管理:确保在失败路径上正确释放所有获取的资源
这种设计使得文件系统能够在高并发环境下稳定运行,同时保持良好的I/O性能。
页面锁定机制lock_page
static inline void lock_page(struct page *page)
{might_sleep();if (TestSetPageLocked(page))__lock_page(page);
}
/** Get a lock on the page, assuming we need to sleep to get it.** Ugly: running sync_page() in state TASK_UNINTERRUPTIBLE is scary. If some* random driver's requestfn sets TASK_RUNNING, we could busywait. However* chances are that on the second loop, the block layer's plug list is empty,* so sync_page() will then return in state TASK_UNINTERRUPTIBLE.*/
void fastcall __lock_page(struct page *page)
{DEFINE_WAIT_BIT(wait, &page->flags, PG_locked);__wait_on_bit_lock(page_waitqueue(page), &wait, sync_page,TASK_UNINTERRUPTIBLE);
}
static int sync_page(void *word)
{struct address_space *mapping;struct page *page;page = container_of((page_flags_t *)word, struct page, flags);/** FIXME, fercrissake. What is this barrier here for?*/smp_mb();mapping = page_mapping(page);if (mapping && mapping->a_ops && mapping->a_ops->sync_page)mapping->a_ops->sync_page(page);io_schedule();return 0;
}
int __sched fastcall
__wait_on_bit_lock(wait_queue_head_t *wq, struct wait_bit_queue *q,int (*action)(void *), unsigned mode)
{int ret = 0;do {prepare_to_wait_exclusive(wq, &q->wait, mode);if (test_bit(q->key.bit_nr, q->key.flags)) {if ((ret = (*action)(q->key.flags)))break;}} while (test_and_set_bit(q->key.bit_nr, q->key.flags));finish_wait(wq, &q->wait);return ret;
}
函数功能分析
lock_page函数族实现了页面的睡眠锁机制,当页面已被锁定时,当前任务会睡眠等待直到获得锁。
页面锁定入口函数
static inline void lock_page(struct page *page)
{might_sleep();if (TestSetPageLocked(page))__lock_page(page);
}
- 睡眠警告:
might_sleep()表明函数可能睡眠,用于调试和检测不当的原子上下文调用 - 原子测试并设置:
TestSetPageLocked(page)原子性地测试并设置PG_locked标志 - 快速路径:如果设置成功(页面未被锁定),立即返回
- 慢速路径:如果页面已被锁定,调用
__lock_page进入睡眠等待
睡眠等待实现
void fastcall __lock_page(struct page *page)
{DEFINE_WAIT_BIT(wait, &page->flags, PG_locked);__wait_on_bit_lock(page_waitqueue(page), &wait, sync_page,TASK_UNINTERRUPTIBLE);
}
- 等待位定义:
DEFINE_WAIT_BIT创建位等待队列项,关联页面标志和PG_locked位 - 等待队列:
page_waitqueue(page)获取页面对应的等待队列哈希桶 - 不可中断睡眠:使用
TASK_UNINTERRUPTIBLE模式,确保等待过程不被信号打断
同步回调函数
static int sync_page(void *word)
{struct address_space *mapping;struct page *page;page = container_of((page_flags_t *)word, struct page, flags);smp_mb();mapping = page_mapping(page);if (mapping && mapping->a_ops && mapping->a_ops->sync_page)mapping->a_ops->sync_page(page);io_schedule();return 0;
}
- 容器转换:
container_of从标志指针反推回page结构体 - 内存屏障:
smp_mb()确保内存访问顺序,防止指令重排 - 文件系统同步:如果文件系统支持
sync_page操作,调用它来刷新页面 - I/O调度:
io_schedule()让出CPU,等待I/O完成
通用位等待锁机制
int __sched fastcall
__wait_on_bit_lock(wait_queue_head_t *wq, struct wait_bit_queue *q,int (*action)(void *), unsigned mode)
{int ret = 0;do {prepare_to_wait_exclusive(wq, &q->wait, mode);if (test_bit(q->key.bit_nr, q->key.flags)) {if ((ret = (*action)(q->key.flags)))break;}} while (test_and_set_bit(q->key.bit_nr, q->key.flags));finish_wait(wq, &q->wait);return ret;
- 准备等待:
prepare_to_wait_exclusive将当前任务加入等待队列 - 位测试:检查目标位是否仍然被设置(页面是否仍被锁定)
- 动作执行:如果位被设置,调用
action回调函数(这里是sync_page) - 原子测试并设置循环:在循环中不断尝试获取锁,直到成功
- 清理等待状态:
finish_wait将任务从等待队列移除
函数功能总结
lock_page机制实现了高效的页面级睡眠锁,通过位等待队列避免了忙等待,充分利用了内核的调度器。这种设计特别适合文件系统操作,因为:
- 避免竞争:通过原子操作确保锁的获取是互斥的
- 资源友好:在等待时让出CPU,提高系统整体吞吐量
- I/O集成:在等待期间有机会执行页面同步操作
- 可扩展性:使用哈希的等待队列减少锁竞争
将文件数据从磁盘读取到页面缓存page_cache_read
static int fastcall page_cache_read(struct file * file, unsigned long offset)
{struct address_space *mapping = file->f_mapping;struct page *page; int error;page = page_cache_alloc_cold(mapping);if (!page)return -ENOMEM;error = add_to_page_cache_lru(page, mapping, offset, GFP_KERNEL);if (!error) {error = mapping->a_ops->readpage(file, page);page_cache_release(page);return error;}/** We arrive here in the unlikely event that someone * raced with us and added our page to the cache first* or we are out of memory for radix-tree nodes.*/page_cache_release(page);return error == -EEXIST ? 0 : error;
}
函数功能分析
page_cache_read函数实现了将文件数据从磁盘读取到页面缓存的完整流程,包括页面分配、缓存管理和错误处理。
函数声明和变量初始化
static int fastcall page_cache_read(struct file * file, unsigned long offset)
{struct address_space *mapping = file->f_mapping;struct page *page; int error;
- fastcall修饰符:使用快速调用约定优化函数调用性能
- 参数说明:接收文件对象和页面偏移量
- 关键变量:
mapping:从文件获取地址空间映射page:要分配的页面指针error:操作结果状态码
冷页面分配
page = page_cache_alloc_cold(mapping);if (!page)return -ENOMEM;
- 冷页面分配:
page_cache_alloc_cold分配标记为"冷"的页面 - 内存不足处理:如果分配失败,立即返回
-ENOMEM错误码 - 性能优化:冷页面适合预读场景,不太可能很快被再次访问
页面缓存添加和LRU管理
error = add_to_page_cache_lru(page, mapping, offset, GFP_KERNEL);if (!error) {error = mapping->a_ops->readpage(file, page);page_cache_release(page);return error;}
- 缓存添加:
add_to_page_cache_lru将页面添加到基数树缓存和LRU列表 - 成功路径处理:
- 调用文件系统的
readpage方法从磁盘读取数据 - 释放页面的临时引用计数(缓存已持有引用)
- 返回读取操作的结果
- 调用文件系统的
竞态条件和错误处理
/** We arrive here in the unlikely event that someone * raced with us and added our page to the cache first* or we are out of memory for radix-tree nodes.*/page_cache_release(page);return error == -EEXIST ? 0 : error;
- 处理两种罕见情况:
- 其他线程抢先添加了相同页面到缓存(竞态条件)
- 基数树节点内存不足
- 资源清理:释放之前分配的页面引用
- 智能错误转换:如果错误是
-EEXIST(页面已存在),返回0表示成功;否则返回原始错误
函数功能总结
page_cache_read函数体现了Linux内核页面缓存管理的精细设计和健壮性。它通过冷页面分配优化内存使用模式,通过add_to_page_cache_lru完成缓存添加和LRU管理。函数的错误处理机制特别值得注意:它能够智能区分真正的错误和良性的竞态条件,将页面已存在的情况视为成功,这在高并发环境下非常重要。整个函数的设计确保了在内存压力、并发竞争和I/O错误等多种异常情况下都能保持正确的行为,为文件系统的页面缓存提供了可靠的基础设施支持。
强制页面缓存预读force_page_cache_readahead
/** Chunk the readahead into 2 megabyte units, so that we don't pin too much* memory at once.*/
int force_page_cache_readahead(struct address_space *mapping, struct file *filp,unsigned long offset, unsigned long nr_to_read)
{int ret = 0;if (unlikely(!mapping->a_ops->readpage && !mapping->a_ops->readpages))return -EINVAL;while (nr_to_read) {int err;unsigned long this_chunk = (2 * 1024 * 1024) / PAGE_CACHE_SIZE;if (this_chunk > nr_to_read)this_chunk = nr_to_read;err = __do_page_cache_readahead(mapping, filp,offset, this_chunk);if (err < 0) {ret = err;break;}ret += err;offset += this_chunk;nr_to_read -= this_chunk;}return ret;
}
函数功能分析
这个函数实现强制页面缓存预读功能,通过将大块预读请求分割成2MB的块来避免一次性占用过多内存。
函数参数和初始检查
int force_page_cache_readahead(struct address_space *mapping, struct file *filp,unsigned long offset, unsigned long nr_to_read)
{int ret = 0;if (unlikely(!mapping->a_ops->readpage && !mapping->a_ops->readpages))return -EINVAL;
- 参数说明:接收地址空间、文件指针、起始偏移和要预读的页面数量
- 操作检查:验证地址空间是否支持读取操作(
readpage或readpages方法) - 错误返回:如果文件系统不支持读取操作,返回
-EINVAL无效参数错误
预读分块处理循环
while (nr_to_read) {int err;unsigned long this_chunk = (2 * 1024 * 1024) / PAGE_CACHE_SIZE;if (this_chunk > nr_to_read)this_chunk = nr_to_read;
- 循环条件:当还有页面需要预读时继续循环
- 分块计算:将预读请求分成2MB大小的块(转换为页面数量)
- 边界处理:如果剩余页面数小于当前块大小,使用剩余页面数
核心预读执行
err = __do_page_cache_readahead(mapping, filp,offset, this_chunk);if (err < 0) {ret = err;break;}
- 执行预读:调用底层预读函数处理当前块
- 错误处理:如果预读失败,保存错误码并跳出循环
- 提前终止:遇到错误时立即停止后续预读操作
进度更新和结果统计
ret += err;offset += this_chunk;nr_to_read -= this_chunk;}return ret;
- 结果累加:累加成功预读的页面数量到总返回值
- 偏移更新:前进文件偏移量,准备处理下一个块
- 计数减少:减少剩余要预读的页面数量
- 最终返回:返回总共成功预读的页面数量或错误码
函数功能总结
force_page_cache_readahead是一个智能的预读分块管理器,通过将大的预读请求分解成2MB的块来平衡内存使用效率和I/O性能。这种设计既避免了单次预读占用过多内存的风险,又通过顺序预读充分利用了磁盘的顺序访问特性。函数的错误处理机制确保了在部分失败的情况下能够及时停止并返回错误信息,而结果统计功能则让调用方能够准确了解实际完成的预读工作量。
页面缓存预读的核心实现__do_page_cache_readahead
static inline int
__do_page_cache_readahead(struct address_space *mapping, struct file *filp,unsigned long offset, unsigned long nr_to_read)
{struct inode *inode = mapping->host;struct page *page;unsigned long end_index; /* The last page we want to read */LIST_HEAD(page_pool);int page_idx;int ret = 0;loff_t isize = i_size_read(inode);if (isize == 0)goto out;end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);/** Preallocate as many pages as we will need.*/spin_lock_irq(&mapping->tree_lock);for (page_idx = 0; page_idx < nr_to_read; page_idx++) {unsigned long page_offset = offset + page_idx;if (page_offset > end_index)break;page = radix_tree_lookup(&mapping->page_tree, page_offset);if (page)continue;spin_unlock_irq(&mapping->tree_lock);page = page_cache_alloc_cold(mapping);spin_lock_irq(&mapping->tree_lock);if (!page)break;page->index = page_offset;list_add(&page->lru, &page_pool);ret++;}spin_unlock_irq(&mapping->tree_lock);/** Now start the IO. We ignore I/O errors - if the page is not* uptodate then the caller will launch readpage again, and* will then handle the error.*/if (ret)read_pages(mapping, filp, &page_pool, ret);BUG_ON(!list_empty(&page_pool));
out:return ret;
}
函数功能分析
这个函数是页面缓存预读的核心实现,负责预分配页面并批量提交I/O读取,采用"先分配后I/O"的策略避免读写操作混合导致的性能问题。
函数参数和变量初始化
static inline int
__do_page_cache_readahead(struct address_space *mapping, struct file *filp,unsigned long offset, unsigned long nr_to_read)
{struct inode *inode = mapping->host;struct page *page;unsigned long end_index; /* The last page we want to read */LIST_HEAD(page_pool);int page_idx;int ret = 0;loff_t isize = i_size_read(inode);
- 参数说明:接收地址空间映射、文件对象、起始偏移量和预读页面数量
- 关键数据结构:初始化页面池链表,准备收集预分配页面
- 文件信息获取:从地址空间获取
inode并读取文件大小
文件有效性检查和边界计算
if (isize == 0)goto out;end_index = ((isize - 1) >> PAGE_CACHE_SHIFT);
- 空文件检查:如果文件大小为0,直接跳转到返回,避免无效操作
- 结束索引计算:将文件字节大小转换为页面索引,
isize-1确保正确计算最后一个有效页面
页面预分配循环处理
spin_lock_irq(&mapping->tree_lock);for (page_idx = 0; page_idx < nr_to_read; page_idx++) {unsigned long page_offset = offset + page_idx;if (page_offset > end_index)break;
- 加锁保护:获取地址空间的树锁,保护radix树操作的原子性
- 循环遍历:逐个处理请求预读的每个页面
- 边界保护:检查页面偏移是否超出文件范围,防止越界访问
页面存在性检查和内存分配
page = radix_tree_lookup(&mapping->page_tree, page_offset);if (page)continue;spin_unlock_irq(&mapping->tree_lock);page = page_cache_alloc_cold(mapping);spin_lock_irq(&mapping->tree_lock);if (!page)break;
- 缓存检查:在radix树中查找页面是否已缓存,避免重复工作
- 锁管理优化:页面分配期间临时释放锁,防止分配阻塞时持有锁
- 冷页面分配:分配标记为"冷"的页面,符合预读页面的预期使用模式
- 分配失败处理:内存不足时提前终止循环
页面初始化和资源管理
page->index = page_offset;list_add(&page->lru, &page_pool);ret++;}spin_unlock_irq(&mapping->tree_lock);
- 索引设置:记录页面在文件中的逻辑位置
- 链表管理:将预分配页面添加到待处理池中
- 计数统计:跟踪成功预分配的页面数量
- 锁释放:完成所有页面操作后安全释放锁
批量I/O提交和清理
if (ret)read_pages(mapping, filp, &page_pool, ret);BUG_ON(!list_empty(&page_pool));
out:return ret;
- 条件提交:仅在成功预分配页面时提交批量I/O请求
- 完整性验证:使用BUG_ON确保所有页面都被正确处理
- 结果返回:返回实际启动I/O操作的页面数量
函数功能总结
__do_page_cache_readahead实现了高效的文件预读机制,其核心设计哲学是严格的"分配优先于I/O"策略。通过预先批量分配所有需要的页面内存,然后再统一提交I/O请求,有效避免了在I/O进行过程中因页面分配可能触发的VM回写操作,从而消除了读写操作相互阻塞的性能瓶颈。这种设计特别优化了顺序读取大文件场景,通过减少锁竞争、批量处理I/O和智能的缓存存在性检查,在保持代码安全性的同时显著提升了文件读取吞吐量。整个实现体现了Linux内核在性能优化和资源管理方面的精细考量。
执行实际的页面读取操作read_pages
static int read_pages(struct address_space *mapping, struct file *filp,struct list_head *pages, unsigned nr_pages)
{unsigned page_idx;struct pagevec lru_pvec;int ret = 0;if (mapping->a_ops->readpages) {ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages);goto out;}pagevec_init(&lru_pvec, 0);for (page_idx = 0; page_idx < nr_pages; page_idx++) {struct page *page = list_to_page(pages);list_del(&page->lru);if (!add_to_page_cache(page, mapping,page->index, GFP_KERNEL)) {mapping->a_ops->readpage(filp, page);if (!pagevec_add(&lru_pvec, page))__pagevec_lru_add(&lru_pvec);} else {page_cache_release(page);}}pagevec_lru_add(&lru_pvec);
out:return ret;
}
函数功能分析
这个函数负责执行实际的页面读取操作,支持批量读取和单页读取两种模式,并处理页面的LRU缓存管理。
函数参数和变量初始化
static int read_pages(struct address_space *mapping, struct file *filp,struct list_head *pages, unsigned nr_pages)
{unsigned page_idx;struct pagevec lru_pvec;int ret = 0;
- 参数说明:接收地址空间映射、文件对象、页面链表和页面数量
- 局部变量:
page_idx:页面循环索引lru_pvec:页面向量,用于批量LRU管理ret:返回值,初始化为0
批量读取优化路径
if (mapping->a_ops->readpages) {ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages);goto out;}
- 批量操作检查:优先检查文件系统是否支持
readpages批量操作 - 性能优化:如果支持批量读取,直接调用文件系统的批量接口
- 提前返回:批量操作完成后直接跳转到函数结束
单页读取回退路径
pagevec_init(&lru_pvec, 0);for (page_idx = 0; page_idx < nr_pages; page_idx++) {struct page *page = list_to_page(pages);list_del(&page->lru);
- LRU向量初始化:初始化页面向量,冷热参数为0(冷页面)
- 循环处理:遍历所有待读取页面
- 页面提取:从链表中获取页面并移除,准备单独处理
页面缓存添加和读取
if (!add_to_page_cache(page, mapping,page->index, GFP_KERNEL)) {mapping->a_ops->readpage(filp, page);
- 缓存添加:尝试将页面添加到页面缓存中
- 成功处理:如果添加成功(页面之前不在缓存中),调用
readpage读取页面内容 - GFP标志:使用
GFP_KERNEL分配标志,允许睡眠等待内存
LRU管理和错误处理
if (!pagevec_add(&lru_pvec, page))__pagevec_lru_add(&lru_pvec);} else {page_cache_release(page);}}
- LRU批量添加:将页面添加到LRU向量,如果向量满了就批量提交到LRU列表
- 缓存添加失败处理:如果页面已存在于缓存中,释放页面引用计数
- 资源清理:确保所有页面都被正确处理
最终清理和返回
pagevec_lru_add(&lru_pvec);
out:return ret;
}
- 剩余LRU处理:将向量中剩余的页面添加到LRU列表
- 结果返回:返回操作结果,批量读取时返回文件系统的返回值,单页读取时返回0
函数功能总结
read_pages函数实现了灵活的页面读取策略,通过优先使用文件系统提供的批量读取接口来最大化I/O效率,在批量接口不可用时回退到单页读取的兼容模式。该函数不仅处理数据的实际读取,还负责页面的缓存管理和LRU列表维护,确保了读取的页面能够被系统正确管理并参与后续的内存回收决策。这种分层设计既提供了性能优化的空间,又保证了与各种文件系统的兼容性。
将新分配的页面添加到页面缓存的基础数据结构add_to_page_cache
int add_to_page_cache(struct page *page, struct address_space *mapping,pgoff_t offset, int gfp_mask)
{int error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);if (error == 0) {spin_lock_irq(&mapping->tree_lock);error = radix_tree_insert(&mapping->page_tree, offset, page);if (!error) {page_cache_get(page);SetPageLocked(page);page->mapping = mapping;page->index = offset;mapping->nrpages++;pagecache_acct(1);}spin_unlock_irq(&mapping->tree_lock);radix_tree_preload_end();}return error;
}int radix_tree_preload(int gfp_mask)
{struct radix_tree_preload *rtp;struct radix_tree_node *node;int ret = -ENOMEM;preempt_disable();rtp = &__get_cpu_var(radix_tree_preloads);while (rtp->nr < ARRAY_SIZE(rtp->nodes)) {preempt_enable();node = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask);if (node == NULL)goto out;preempt_disable();rtp = &__get_cpu_var(radix_tree_preloads);if (rtp->nr < ARRAY_SIZE(rtp->nodes))rtp->nodes[rtp->nr++] = node;elsekmem_cache_free(radix_tree_node_cachep, node);}ret = 0;
out:return ret;
}
static inline void radix_tree_preload_end(void)
{preempt_enable();
}
函数功能分析
add_to_page_cache函数负责将新分配的页面添加到页面缓存的基础数据结构中,通过基数树管理和预加载机制确保操作的原子性和可靠性。
基数树预加载准备
int add_to_page_cache(struct page *page, struct address_space *mapping,pgoff_t offset, int gfp_mask)
{int error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);
- 预加载调用:调用
radix_tree_preload预分配基数树节点 - 内存标志处理:过滤掉
__GFP_HIGHMEM标志,确保在低端内存分配 - 错误检查:预加载失败时直接返回错误码
基数树预加载实现细节
int radix_tree_preload(int gfp_mask)
{struct radix_tree_preload *rtp;struct radix_tree_node *node;int ret = -ENOMEM;preempt_disable();rtp = &__get_cpu_var(radix_tree_preloads);
- 抢占禁用:防止在预加载过程中被抢占,保证操作的原子性
- 每CPU变量:获取当前CPU的预加载结构,每个CPU有独立的节点池
- 初始化返回值:预设为内存不足错误
节点预分配循环
while (rtp->nr < ARRAY_SIZE(rtp->nodes)) {preempt_enable();node = kmem_cache_alloc(radix_tree_node_cachep, gfp_mask);if (node == NULL)goto out;preempt_disable();rtp = &__get_cpu_var(radix_tree_preloads);
- 条件循环:直到预加载池填满或分配失败
- 临时启用抢占:在内存分配期间允许抢占,避免长时间禁用抢占
- slab分配:从专门的缓存中分配基数树节点
- 重新获取CPU变量:分配后重新获取,防止CPU迁移
节点管理和结果返回
if (rtp->nr < ARRAY_SIZE(rtp->nodes))rtp->nodes[rtp->nr++] = node;elsekmem_cache_free(radix_tree_node_cachep, node);}ret = 0;
out:return ret;
}
- 节点存储:如果池未满,将节点存入预加载数组
- 多余节点释放:如果池已满(竞态条件),释放刚分配的节点
- 成功返回:预加载成功返回0,保持抢占禁用状态
基数树插入和页面设置
if (error == 0) {spin_lock_irq(&mapping->tree_lock);error = radix_tree_insert(&mapping->page_tree, offset, page);if (!error) {page_cache_get(page);SetPageLocked(page);page->mapping = mapping;page->index = offset;mapping->nrpages++;pagecache_acct(1);}spin_unlock_irq(&mapping->tree_lock);radix_tree_preload_end();}
- 加锁保护:获取地址空间树锁,保护基数树操作
- 基数树插入:将页面插入到指定偏移位置
- 页面状态设置:
- 增加页面引用计数
- 设置页面锁定状态
- 建立页面与地址空间的映射关系
- 记录页面在文件中的偏移量
- 统计更新:增加地址空间的页面计数和系统页面缓存统计
- 资源清理:释放树锁并结束预加载(重新启用抢占)
预加载结束处理
static inline void radix_tree_preload_end(void)
{preempt_enable();
}
- 抢占恢复:重新启用内核抢占,平衡预加载时的
preempt_disable()
函数功能总结
add_to_page_cache与radix_tree_preload共同构成了Linux内核页面缓存管理的核心机制。通过巧妙的预加载设计和精细的锁管理,确保了在并发环境下页面插入操作的高效性和可靠性。预加载机制通过预先分配基数树节点避免了在插入过程中可能发生的内存分配失败,而精细的抢占控制则平衡了系统响应性和操作原子性的需求。这种设计体现了Linux内核在性能优化和资源管理方面的深度考量,为文件系统的页面缓存提供了坚实的基础设施支持。
