当前位置: 首页 > news >正文

Linux内存管理深度解析:从首次访问缺页处理到NUMA策略的完整架构

前言

在现代操作系统中,内存管理是连接硬件资源与应用程序的关键桥梁,其设计直接影响着系统性能、资源利用率和可扩展性。Linux作为一款成熟的企业级操作系统,其内存管理子系统经过数十年的演进,形成了一套复杂而精密的架构。本文将深入剖析Linux内存管理的核心机制,从最基础的缺页异常处理,到复杂的反向映射系统,再到NUMA架构下的内存策略优化。

通过逐行分析关键源码,我们将揭示:

  • 缺页处理如何智能区分匿名映射与文件映射,实现按需分配
  • 反向映射机制如何高效维护物理页到虚拟地址的逆向关联
  • 写时复制(COW) 如何平衡内存共享与写入性能
  • NUMA策略如何在多处理器系统中优化内存访问局部性

处理首次访问缺页do_no_page

static int
do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
{struct page * new_page;struct address_space *mapping = NULL;pte_t entry;int sequence = 0;int ret = VM_FAULT_MINOR;int anon = 0;if (!vma->vm_ops || !vma->vm_ops->nopage)return do_anonymous_page(mm, vma, page_table,pmd, write_access, address);pte_unmap(page_table);spin_unlock(&mm->page_table_lock);if (vma->vm_file) {mapping = vma->vm_file->f_mapping;sequence = atomic_read(&mapping->truncate_count);}smp_rmb();  /* Prevent CPU from reordering lock-free ->nopage() */
retry:new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret);/* no page was available -- either SIGBUS or OOM */if (new_page == NOPAGE_SIGBUS)return VM_FAULT_SIGBUS;if (new_page == NOPAGE_OOM)return VM_FAULT_OOM;/** Should we do an early C-O-W break?*/if (write_access && !(vma->vm_flags & VM_SHARED)) {struct page *page;if (unlikely(anon_vma_prepare(vma)))goto oom;page = alloc_page_vma(GFP_HIGHUSER, vma, address);if (!page)goto oom;copy_user_highpage(page, new_page, address);page_cache_release(new_page);new_page = page;anon = 1;}spin_lock(&mm->page_table_lock);if (mapping &&(unlikely(sequence != atomic_read(&mapping->truncate_count)))) {sequence = atomic_read(&mapping->truncate_count);spin_unlock(&mm->page_table_lock);page_cache_release(new_page);goto retry;}page_table = pte_offset_map(pmd, address);if (pte_none(*page_table)) {if (!PageReserved(new_page))++mm->rss;flush_icache_page(vma, new_page);entry = mk_pte(new_page, vma->vm_page_prot);if (write_access)entry = maybe_mkwrite(pte_mkdirty(entry), vma);set_pte(page_table, entry);if (anon) {lru_cache_add_active(new_page);page_add_anon_rmap(new_page, vma, address);} elsepage_add_file_rmap(new_page);pte_unmap(page_table);} else {/* One of our sibling threads was faster, back out. */pte_unmap(page_table);page_cache_release(new_page);spin_unlock(&mm->page_table_lock);goto out;}/* no need to invalidate: a not-present page shouldn't be cached */update_mmu_cache(vma, address, entry);spin_unlock(&mm->page_table_lock);
out:return ret;
oom:page_cache_release(new_page);ret = VM_FAULT_OOM;goto out;
}

函数详细解析

  • 目的:创建新的页面映射,积极尝试共享现有页面
  • 写时复制:如果write_access为真,创建单独副本避免下次页错误
  • 调用上下文:持有MM信号量和页表自旋锁,退出时释放自旋锁
  • 优化:由于页面原先不存在,不需要刷新TLB或虚拟缓存

函数声明和变量定义

static int
do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
{struct page * new_page;struct address_space *mapping = NULL;pte_t entry;int sequence = 0;int ret = VM_FAULT_MINOR;int anon = 0;
  • mm:内存管理结构
  • vma:虚拟内存区域
  • address:故障地址
  • write_access:写访问标志
  • page_table:页表项指针
  • pmd:中间页目录指针

局部变量

  • new_page:新分配的页面
  • mapping:文件地址空间映射
  • sequence:用于检测文件截断的序列号
  • anon:标记是否为匿名页面

匿名页面处理

	if (!vma->vm_ops || !vma->vm_ops->nopage)return do_anonymous_page(mm, vma, page_table,pmd, write_access, address);
  • 检查VMA是否有vm_opsnopage操作
  • 如果没有,说明是匿名映射
  • 调用do_anonymous_page处理匿名页面分配

文件映射准备

	pte_unmap(page_table);spin_unlock(&mm->page_table_lock);if (vma->vm_file) {mapping = vma->vm_file->f_mapping;sequence = atomic_read(&mapping->truncate_count);}smp_rmb();  /* Prevent CPU from reordering lock-free ->nopage() */
  1. 释放锁:在调用文件操作前释放页表锁,避免死锁
  2. 获取映射信息:如果是文件映射,获取文件地址空间和截断计数
  3. 内存屏障smp_rmb()防止CPU重排序无锁的nopage调用

页面分配

retry:new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret);
  • 调用VMA特定的nopage方法获取页面
  • address & PAGE_MASK:对齐到页面边界
  • &ret:传递返回码指针

错误处理

	/* no page was available -- either SIGBUS or OOM */if (new_page == NOPAGE_SIGBUS)return VM_FAULT_SIGBUS;if (new_page == NOPAGE_OOM)return VM_FAULT_OOM;
  • NOPAGE_SIGBUS:文件映射错误(如文件被截断)
  • NOPAGE_OOM:内存不足错误

早期写时复制

	/** Should we do an early C-O-W break?*/if (write_access && !(vma->vm_flags & VM_SHARED)) {struct page *page;if (unlikely(anon_vma_prepare(vma)))goto oom;page = alloc_page_vma(GFP_HIGHUSER, vma, address);if (!page)goto oom;copy_user_highpage(page, new_page, address);page_cache_release(new_page);new_page = page;anon = 1;}
  • write_access:写访问请求
  • !(vma->vm_flags & VM_SHARED):非共享映射

复制过程

  1. anon_vma_prepare(vma):准备匿名映射数据结构
  2. alloc_page_vma():在VMA中分配新页面
  3. copy_user_highpage():复制原页面内容到新页面
  4. page_cache_release(new_page):释放原页面引用
  5. 标记为匿名页面(anon = 1)

文件截断检测

	spin_lock(&mm->page_table_lock);/** For a file-backed vma, someone could have truncated or otherwise* invalidated this page.  If unmap_mapping_range got called,* retry getting the page.*/if (mapping &&(unlikely(sequence != atomic_read(&mapping->truncate_count)))) {sequence = atomic_read(&mapping->truncate_count);spin_unlock(&mm->page_table_lock);page_cache_release(new_page);goto retry;}
  • 比较之前的序列号和当前序列号
  • 如果不同,说明文件被截断,需要重试
  • 释放页面和锁,跳转回retry标签

重新获取页表项

	page_table = pte_offset_map(pmd, address);
  • 重新映射页表项,因为之前释放了

页面表项设置

	/* Only go through if we didn't race with anybody else... */if (pte_none(*page_table)) {

竞争检测

  • 检查PTE是否仍然为空
  • 如果其他线程已经设置了PTE,需要回滚

页面统计和设置

		if (!PageReserved(new_page))++mm->rss;flush_icache_page(vma, new_page);entry = mk_pte(new_page, vma->vm_page_prot);if (write_access)entry = maybe_mkwrite(pte_mkdirty(entry), vma);set_pte(page_table, entry);
  1. RSS统计:增加进程的常驻集大小(如果不是保留页面)
  2. 创建PTEmk_pte创建页表项
  3. 写权限处理:如果需要写访问,标记为可写和脏
  4. 设置PTEset_pte原子性地设置页表项

反向映射设置

		if (anon) {lru_cache_add_active(new_page);page_add_anon_rmap(new_page, vma, address);} elsepage_add_file_rmap(new_page);pte_unmap(page_table);
  • 匿名页面:添加到活跃LRU缓存,建立匿名反向映射
  • 文件页面:建立文件反向映射

竞争失败处理

	} else {/* One of our sibling threads was faster, back out. */pte_unmap(page_table);page_cache_release(new_page);spin_unlock(&mm->page_table_lock);goto out;}
  • 取消页表映射
  • 释放新分配的页面
  • 释放页表锁
  • 跳转到退出路径

缓存更新和清理

	/* no need to invalidate: a not-present page shouldn't be cached */update_mmu_cache(vma, address, entry);spin_unlock(&mm->page_table_lock);
out:return ret;
  • 由于页面原本不存在,不需要无效化缓存
  • 释放页表锁并返回

内存不足处理

oom:page_cache_release(new_page);ret = VM_FAULT_OOM;goto out;
  • 释放已分配的页面
  • 设置返回码为VM_FAULT_OOM
  • 跳转到退出路径

函数功能总结

do_no_page 是处理首次访问缺页的核心函数,负责:

  1. 页面类型识别:区分匿名映射和文件映射
  2. 页面分配:通过VMA特定的nopage方法获取页面
  3. 写时复制:在写访问时提前进行COW处理
  4. 竞态处理:检测文件截断和其他线程竞争
  5. 页表更新:安全地设置页表项并建立反向映射

为匿名页面添加反向映射条目page_add_anon_rmap

/*** page_add_anon_rmap - add pte mapping to an anonymous page* @page:	the page to add the mapping to* @vma:	the vm area in which the mapping is added* @address:	the user virtual address mapped** The caller needs to hold the mm->page_table_lock.*/
void page_add_anon_rmap(struct page *page,struct vm_area_struct *vma, unsigned long address)
{struct anon_vma *anon_vma = vma->anon_vma;pgoff_t index;BUG_ON(PageReserved(page));BUG_ON(!anon_vma);vma->vm_mm->anon_rss++;anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;index = (address - vma->vm_start) >> PAGE_SHIFT;index += vma->vm_pgoff;index >>= PAGE_CACHE_SHIFT - PAGE_SHIFT;if (atomic_inc_and_test(&page->_mapcount)) {page->index = index;page->mapping = (struct address_space *) anon_vma;inc_page_state(nr_mapped);}/* else checking page index and mapping is racy */
}

函数功能分析

page_add_anon_rmap 函数为匿名页面添加反向映射条目,建立从物理页面到虚拟地址空间的逆向关联。

参数验证和初始化

	struct anon_vma *anon_vma = vma->anon_vma;pgoff_t index;BUG_ON(PageReserved(page));BUG_ON(!anon_vma);

严格验证

  • PageReserved(page):确保页面不是保留页面(保留页面不应有反向映射)
  • !anon_vma:确保VMA已经准备好了匿名映射管理结构
  • 使用BUG_ON在开发阶段捕获编程错误,确保调用条件正确

匿名内存统计更新

	vma->vm_mm->anon_rss++;

内存统计

  • 增加内存管理结构的匿名常驻集大小统计
  • anon_rss跟踪进程使用的匿名页面数量
  • 用于内存监控、OOM killer决策和系统统计

匿名映射标识处理

	anon_vma = (void *) anon_vma + PAGE_MAPPING_ANON;

巧妙编码

  • PAGE_MAPPING_ANON是一个特殊标志值(通常是1)
  • 通过指针运算将匿名映射标志编码到指针的最低有效位
  • 后续可以通过检查指针最低位来区分匿名映射和文件映射
  • 这是Linux内核中常见的标志编码技巧

页面索引计算

	index = (address - vma->vm_start) >> PAGE_SHIFT;index += vma->vm_pgoff;index >>= PAGE_CACHE_SHIFT - PAGE_SHIFT;

索引构建

  1. (address - vma->vm_start) >> PAGE_SHIFT:计算在VMA内的页偏移
  2. + vma->vm_pgoff:加上VMA在文件/匿名空间中的起始偏移
  3. 右移调整:转换为页缓存索引
  4. 最终index表示页面在匿名地址空间中的逻辑位置

反向映射核心设置

	if (atomic_inc_and_test(&page->_mapcount)) {page->index = index;page->mapping = (struct address_space *) anon_vma;inc_page_state(nr_mapped);}

首次映射处理

  • atomic_inc_and_test(&page->_mapcount):原子增加映射计数,如果从0变为1返回true
  • 只有在页面首次被映射时才设置索引和映射指针
  • page->index = index:设置页面在地址空间中的位置
  • page->mapping = (struct address_space *) anon_vma:设置反向映射指针(包含编码的标志)
  • inc_page_state(nr_mapped):增加系统全局的已映射页面统计

竞态条件说明

	/* else checking page index and mapping is racy */

并发说明

  • 对于已经映射的页面,检查和设置index/mapping可能存在竞态条件
  • 因此只在首次映射时设置这些字段
  • 后续映射只增加计数,不修改关键字段

函数功能总结

page_add_anon_rmap 是匿名内存反向映射系统的核心组件:

  1. 反向映射建立:创建从物理页面到虚拟内存区域的逆向关联
  2. 映射计数管理:跟踪每个页面被映射的次数
  3. 内存统计维护:更新进程和系统的内存使用统计
  4. 标识编码:通过指针编码区分匿名映射和文件映射

对原子变量进行加1操作并测试结果是否为零atomic_inc_and_test

/*** atomic_inc_and_test - increment and test * @v: pointer of type atomic_t* * Atomically increments @v by 1* and returns true if the result is zero, or false for all* other cases.*/ 
static __inline__ int atomic_inc_and_test(atomic_t *v)
{unsigned char c;__asm__ __volatile__(LOCK "incl %0; sete %1":"=m" (v->counter), "=qm" (c):"m" (v->counter) : "memory");return c != 0;
}

函数功能分析

这是一个原子操作函数,用于对原子变量进行加1操作并测试结果是否为零。

函数原型和参数

static __inline__ int atomic_inc_and_test(atomic_t *v)
  • static __inline__:定义为静态内联函数,避免函数调用开销
  • atomic_t *v:指向原子类型变量的指针,通常包含一个整型计数器

内联汇编实现

__asm__ __volatile__(LOCK "incl %0; sete %1":"=m" (v->counter), "=qm" (c):"m" (v->counter) : "memory");

LOCK前缀保证原子性

  • LOCK:汇编指令前缀,确保操作在多处理器环境下的原子性
  • 通过锁总线或缓存一致性协议防止其他CPU同时访问同一内存位置

指令序列执行

  • incl %0:对第一个操作数(v->counter)进行加1操作
  • sete %1:根据上条指令结果设置第二个操作数(c)
  • sete指令在结果为0时将字节寄存器设为1,否则设为0

输入输出约束

:"=m" (v->counter), "=qm" (c)
:"m" (v->counter) : "memory");
  • 输出部分:v->counter为内存操作数,c可使用字节寄存器
  • 输入部分:v->counter作为输入操作数
  • "memory":防止编译器重排序,确保内存一致性

条件判断和返回

unsigned char c;
// ... 汇编指令设置c的值
return c != 0;
  • c存储了sete指令的结果(加1后是否为0)
  • 返回c != 0:当加1后结果为0时返回true,否则返回false

函数功能总结

该函数原子性地将原子变量加1,然后检测结果是否为零。如果加1后结果为零则返回true(1),否则返回false(0)。这种操作在引用计数、资源管理等场景中非常有用,特别是需要知道计数器是否从-1变为0(表示没有引用者)的情况

向文件页添加反向映射page_add_file_rmap

/*** page_add_file_rmap - add pte mapping to a file page* @page: the page to add the mapping to** The caller needs to hold the mm->page_table_lock.*/
void page_add_file_rmap(struct page *page)
{BUG_ON(PageAnon(page));if (!pfn_valid(page_to_pfn(page)) || PageReserved(page))return;if (atomic_inc_and_test(&page->_mapcount))inc_page_state(nr_mapped);
}

函数功能分析

这个函数用于向文件页添加反向映射(reverse mapping),跟踪哪些页表项(PTE)映射了该文件页。

函数参数和调用约束

void page_add_file_rmap(struct page *page)
  • struct page *page:要添加映射的内核页面结构体指针

页面类型安全检查

BUG_ON(PageAnon(page));
  • 使用BUG_ON宏进行断言检查
  • PageAnon(page):检测页面是否为匿名页面(匿名内存 vs 文件内存)
  • 如果是匿名页面,触发内核BUG,因为此函数仅处理文件页的映射

特殊页面过滤检查

if (!pfn_valid(page_to_pfn(page)) || PageReserved(page))return;
  • pfn_valid(page_to_pfn(page)):验证页面帧号(PFN)是否有效
    • 排除不存在或无效的物理内存区域
  • PageReserved(page):检查页面是否为保留页面
    • 保留页面通常用于内核代码、数据结构等特殊用途
  • 任一条件满足则直接返回,不对特殊页面进行映射计数

映射计数原子操作

if (atomic_inc_and_test(&page->_mapcount))inc_page_state(nr_mapped);
  • atomic_inc_and_test(&page->_mapcount):原子性地增加_mapcount字段
    • _mapcount:记录该页面被多少个页表项映射
    • 函数返回true表示加1前值为-1(从无映射变为有映射)
  • inc_page_state(nr_mapped):增加系统全局的已映射页面计数
    • 仅当页面从无映射变为有映射状态时调用
    • 用于内核统计和内存管理决策

函数功能总结

该函数是Linux内存管理中的关键例程,专门用于管理文件页的反向映射计数。它通过原子操作安全地增加页面的映射计数,并在页面首次被映射时更新系统统计信息。这种机制对于内存回收、页面共享检测和文件回写等操作至关重要,确保了内核能够准确跟踪每个文件页的映射状态。

处理匿名内存映射的缺页错误do_anonymous_page

static int
do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,pte_t *page_table, pmd_t *pmd, int write_access,unsigned long addr)
{pte_t entry;struct page * page = ZERO_PAGE(addr);/* Read-only mapping of ZERO_PAGE. */entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));/* ..except if it's a write access */if (write_access) {/* Allocate our own private page. */pte_unmap(page_table);spin_unlock(&mm->page_table_lock);if (unlikely(anon_vma_prepare(vma)))goto no_mem;page = alloc_page_vma(GFP_HIGHUSER, vma, addr);if (!page)goto no_mem;clear_user_highpage(page, addr);spin_lock(&mm->page_table_lock);page_table = pte_offset_map(pmd, addr);if (!pte_none(*page_table)) {pte_unmap(page_table);page_cache_release(page);spin_unlock(&mm->page_table_lock);goto out;}mm->rss++;entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,vma->vm_page_prot)),vma);lru_cache_add_active(page);mark_page_accessed(page);page_add_anon_rmap(page, vma, addr);}set_pte(page_table, entry);pte_unmap(page_table);/* No need to invalidate - it was non-present before */update_mmu_cache(vma, addr, entry);spin_unlock(&mm->page_table_lock);
out:return VM_FAULT_MINOR;
no_mem:return VM_FAULT_OOM;
}

函数详细解析

函数声明和变量定义

static int
do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,pte_t *page_table, pmd_t *pmd, int write_access,unsigned long addr)
{pte_t entry;struct page * page = ZERO_PAGE(addr);
  • mm:内存管理结构
  • vma:虚拟内存区域
  • page_table:页表项指针
  • pmd:中间页目录指针
  • write_access:写访问标志
  • addr:故障地址

局部变量

  • entry:要设置的页表项
  • page:页面指针,初始化为零页面

只读访问处理

零页面映射

	/* Read-only mapping of ZERO_PAGE. */entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot));
  • ZERO_PAGE(addr):全局的零页面(全零内容)

  • mk_pte():创建页表项

  • pte_wrprotect():设置为只读权限

  • 性能优化:所有只读匿名映射共享同一个零页面

  • 内存节省:避免为只读访问分配实际物理页面

  • 写时复制:只有在实际写入时才分配真实页面

写访问处理

锁释放和准备

	/* ..except if it's a write access */if (write_access) {/* Allocate our own private page. */pte_unmap(page_table);spin_unlock(&mm->page_table_lock);
  1. 释放资源:在页面分配前释放页表映射和锁
  2. 避免死锁:页面分配可能触发内存回收,需要睡眠等待
  3. 性能考虑:页面分配是较慢的操作,不应持有自旋锁

匿名映射准备

		if (unlikely(anon_vma_prepare(vma)))goto no_mem;
  • 准备匿名虚拟内存区域数据结构
  • 建立反向映射所需的基础设施
  • 如果失败,跳转到内存不足处理

页面分配和初始化

		page = alloc_page_vma(GFP_HIGHUSER, vma, addr);if (!page)goto no_mem;clear_user_highpage(page, addr);
  1. alloc_page_vma(GFP_HIGHUSER, vma, addr):在VMA中分配页面
    • GFP_HIGHUSER:从高端内存区域分配,可移动到高端内存
  2. 内存不足检查:如果分配失败,跳转到no_mem
  3. clear_user_highpage(page, addr):清空页面内容(填充零)

重新加锁和竞争检查

		spin_lock(&mm->page_table_lock);page_table = pte_offset_map(pmd, addr);
  • 获取页表自旋锁
  • 重新映射页表项指针(因为之前释放了)

竞争条件检查

		if (!pte_none(*page_table)) {pte_unmap(page_table);page_cache_release(page);spin_unlock(&mm->page_table_lock);goto out;}
  • 检查PTE是否仍然为空
  • 如果其他线程已经设置了PTE
    • 释放页表映射
    • 释放刚分配的页面
    • 释放锁
    • 跳转到成功返回

页面设置和映射

		mm->rss++;entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,vma->vm_page_prot)),vma);
  1. mm->rss++:增加进程的常驻集大小统计
  2. mk_pte(page, vma->vm_page_prot):创建页表项
  3. pte_mkdirty():标记页面为脏(因为即将写入)
  4. maybe_mkwrite():根据VMA权限设置写权限

页面管理和反向映射

		lru_cache_add_active(page);mark_page_accessed(page);page_add_anon_rmap(page, vma, addr);}
  1. lru_cache_add_active(page):将页面添加到活跃LRU列表
  2. mark_page_accessed(page):标记页面已被访问
  3. page_add_anon_rmap(page, vma, addr):建立匿名反向映射

页表更新和清理

	set_pte(page_table, entry);pte_unmap(page_table);
  • set_pte(page_table, entry):原子性地设置页表项
  • pte_unmap(page_table):取消页表映射

缓存更新

	/* No need to invalidate - it was non-present before */update_mmu_cache(vma, addr, entry);spin_unlock(&mm->page_table_lock);
  • update_mmu_cache(vma, addr, entry):更新架构特定的MMU缓存
  • 优化:由于页面原本不存在,不需要无效化TLB
  • 释放页表自旋锁

返回处理

out:return VM_FAULT_MINOR;
no_mem:return VM_FAULT_OOM;
  • VM_FAULT_MINOR:次要缺页错误,处理成功
  • VM_FAULT_OOM:内存不足错误

函数功能总结

do_anonymous_page 专门处理匿名内存映射的缺页错误:

  1. 零页面优化:对只读访问映射到全局零页面,节省内存
  2. 按需分配:只有在写访问时才分配实际物理页面
  3. 写时复制:实现COW语义的基础
  4. 竞态处理:正确处理多线程并发访问
  5. 资源管理:准确的内存统计和反向映射

用户空间页面分配alloc_page_vma

struct page *
alloc_page_vma(unsigned gfp, struct vm_area_struct *vma, unsigned long addr)
{struct mempolicy *pol = get_vma_policy(vma, addr);if (unlikely(pol->policy == MPOL_INTERLEAVE)) {unsigned nid;if (vma) {unsigned long off;BUG_ON(addr >= vma->vm_end);BUG_ON(addr < vma->vm_start);off = vma->vm_pgoff;off += (addr - vma->vm_start) >> PAGE_SHIFT;nid = offset_il_node(pol, vma, off);} else {/* fall back to process interleaving */nid = interleave_nodes(pol);}return alloc_page_interleave(gfp, 0, nid);}return __alloc_pages(gfp, 0, zonelist_policy(gfp, pol));
}

函数详细解析

函数声明

struct page *
alloc_page_vma(unsigned gfp, struct vm_area_struct *vma, unsigned long addr)
{struct mempolicy *pol = get_vma_policy(vma, addr);
  • gfp:分配标志(GFP_*系列)
  • vma:虚拟内存区域指针(可为NULL)
  • addr:分配页面的虚拟地址

策略获取

  • get_vma_policy(vma, addr):获取VMA或当前进程的NUMA内存策略
  • 策略层次:VMA策略 → 进程策略 → 系统默认策略

交错策略特殊处理

	if (unlikely(pol->policy == MPOL_INTERLEAVE)) {

MPOL_INTERLEAVE

  • NUMA交错策略:在多个NUMA节点间轮询分配页面
  • 优化目标:提高内存访问的并行性,避免热点

VMA存在时的节点计算

		unsigned nid;if (vma) {unsigned long off;BUG_ON(addr >= vma->vm_end);BUG_ON(addr < vma->vm_start);off = vma->vm_pgoff;off += (addr - vma->vm_start) >> PAGE_SHIFT;nid = offset_il_node(pol, vma, off);}
  • BUG_ON(addr >= vma->vm_end):确保地址不超过VMA结束
  • BUG_ON(addr < vma->vm_start):确保地址不小于VMA开始

偏移计算

  • vma->vm_pgoff:VMA在文件中的页偏移
  • (addr - vma->vm_start) >> PAGE_SHIFT:在VMA内的页偏移
  • off:总页偏移(用于确定交错节点)

节点选择

  • offset_il_node(pol, vma, off):根据偏移计算目标NUMA节点

VMA不存在时的回退处理

		} else {/* fall back to process interleaving */nid = interleave_nodes(pol);}
  • 当没有VMA信息时,使用进程级别的交错策略
  • interleave_nodes(pol):在策略指定的节点间轮询选择

交错分配执行

		return alloc_page_interleave(gfp, 0, nid);}
  • alloc_page_interleave(gfp, 0, nid):在指定NUMA节点上分配页面
  • 参数:分配标志、order(0表示单页)、目标节点

通用页面分配

	return __alloc_pages(gfp, 0, zonelist_policy(gfp, pol));
}
  • __alloc_pages(gfp, 0, zonelist_policy(gfp, pol)):内核页面分配核心函数
  • 参数分解
    • gfp:分配标志
    • 0:order,分配2^0=1个页面
    • zonelist_policy(gfp, pol):根据策略生成zone列表

函数功能总结

alloc_page_vma用户空间页面分配的专用函数:

  1. 策略感知分配:考虑VMA和进程的NUMA内存策略
  2. 交错策略优化:特殊处理MPOL_INTERLEAVE策略
  3. 地址验证:确保分配地址在有效VMA范围内
  4. 统一接口:为不同策略提供一致的分配接口

在指定NUMA节点上执行交错策略的页面分配alloc_page_interleave

/* Allocate a page in interleaved policy.Own path because it needs to do special accounting. */
static struct page *alloc_page_interleave(unsigned gfp, unsigned order, unsigned nid)
{struct zonelist *zl;struct page *page;BUG_ON(!node_online(nid));zl = NODE_DATA(nid)->node_zonelists + (gfp & GFP_ZONEMASK);page = __alloc_pages(gfp, order, zl);if (page && page_zone(page) == zl->zones[0]) {zl->zones[0]->pageset[get_cpu()].interleave_hit++;put_cpu();}return page;
}

函数功能分析

alloc_page_interleave 函数在指定NUMA节点上执行交错策略的页面分配,并进行特殊的统计计数

参数验证和节点检查

	BUG_ON(!node_online(nid));

严格验证

  • 使用BUG_ON确保目标节点nid是在线状态
  • 如果节点不在线,触发内核崩溃,防止在无效节点上分配
  • 这是安全防护,确保后续操作的基础条件

Zone列表准备

	zl = NODE_DATA(nid)->node_zonelists + (gfp & GFP_ZONEMASK);

zone列表构建

  • NODE_DATA(nid):获取目标节点的内存管理数据结构
  • node_zonelists:节点的zone列表数组,包含不同内存区域的优先级顺序
  • gfp & GFP_ZONEMASK:从GFP标志中提取zone类型掩码,确定使用哪个zone列表
  • 结果zl是指向合适zone列表的指针

核心页面分配

	page = __alloc_pages(gfp, order, zl);

实际分配调用

  • __alloc_pages:Linux内核的核心页面分配函数
  • gfp:分配标志,控制分配行为和位置
  • order:分配阶数,0表示单页(2^0=1页)
  • zl:准备好的zone列表,指导分配在指定节点进行
  • 返回分配的页面指针或NULL(分配失败)

交错命中统计

	if (page && page_zone(page) == zl->zones[0]) {zl->zones[0]->pageset[get_cpu()].interleave_hit++;put_cpu();}

统计条件

  • page:分配成功
  • page_zone(page) == zl->zones[0]:页面确实分配在首选zone中

统计操作

  • get_cpu():获取当前CPU ID,禁用抢占
  • zl->zones[0]->pageset[cpu].interleave_hit++:增加当前CPU的交错命中计数
  • put_cpu():释放CPU,重新启用抢占
  • 设计目的:监控交错策略的实际效果,验证页面是否按预期分配

结果返回

	return page;

统一返回

  • 成功:返回分配的page结构指针
  • 失败:返回NULL指针
  • 调用者需要检查返回值处理分配失败

函数功能总结

alloc_page_interleave 是NUMA交错分配策略的专用实现函数,主要功能包括:

  1. 目标节点分配:在指定的NUMA节点上执行页面分配
  2. zone列表管理:根据GFP标志构建合适的zone优先级列表
  3. 统计监控:跟踪交错策略的实际命中情况,用于性能分析
  4. 安全验证:确保目标节点在线状态,防止无效分配

将NUMA内存策略转换为具体的zone列表zonelist_policy

/* Return a zonelist representing a mempolicy */
static struct zonelist *zonelist_policy(unsigned gfp, struct mempolicy *policy)
{int nd;switch (policy->policy) {case MPOL_PREFERRED:nd = policy->v.preferred_node;if (nd < 0)nd = numa_node_id();break;case MPOL_BIND:/* Lower zones don't get a policy applied */if (gfp >= policy_zone)return policy->v.zonelist;/*FALL THROUGH*/case MPOL_INTERLEAVE: /* should not happen */case MPOL_DEFAULT:nd = numa_node_id();break;default:nd = 0;BUG();}return NODE_DATA(nd)->node_zonelists + (gfp & GFP_ZONEMASK);
}

函数代码分析

函数声明和变量定义

static struct zonelist *zonelist_policy(unsigned gfp, struct mempolicy *policy)
{int nd;
  • 输入:GFP分配标志和内存策略
  • 输出:对应的zone列表指针
  • nd:目标NUMA节点ID

MPOL_PREFERRED策略处理

	switch (policy->policy) {case MPOL_PREFERRED:nd = policy->v.preferred_node;if (nd < 0)nd = numa_node_id();break;
  • 使用策略中指定的首选节点policy->v.preferred_node
  • 如果首选节点为负值(未指定),回退到当前节点numa_node_id()

MPOL_BIND策略处理

	case MPOL_BIND:/* Lower zones don't get a policy applied */if (gfp >= policy_zone)return policy->v.zonelist;/*FALL THROUGH*/
  • gfp >= policy_zone:检查GFP标志是否满足策略zone要求
  • 如果满足,直接返回策略预计算的zone列表policy->v.zonelist
  • /*FALL THROUGH*/注释表示故意穿透到默认case

设计原理

  • 低zone(如DMA zone)不应用绑定策略,确保关键内存可用
  • 只有高zone分配时才严格执行绑定策略

默认和回退处理

	case MPOL_INTERLEAVE: /* should not happen */case MPOL_DEFAULT:nd = numa_node_id();break;
  • MPOL_INTERLEAVE:不应该到达这里,因为交错策略有专门处理
  • MPOL_DEFAULT:使用当前节点numa_node_id()
  • 提供策略无法应用时的安全回退

错误处理

	default:nd = 0;BUG();}
  • 设置默认节点0
  • 触发BUG()内核错误,因为未知策略是编程错误
  • 确保代码健壮性

Zone列表构建

	return NODE_DATA(nd)->node_zonelists + (gfp & GFP_ZONEMASK);
}
  • NODE_DATA(nd):获取目标节点的内存数据
  • node_zonelists:节点的zone列表数组
  • gfp & GFP_ZONEMASK:从GFP标志提取zone类型索引

函数功能总结

zonelist_policyNUMA策略到zone列表的转换器

  1. 策略解析:将抽象的内存策略转换为具体的分配目标
  2. zone选择:根据GFP标志选择合适的memory zone
  3. 边界处理:处理策略不适用时的回退情况
  4. 错误防护:检测并报告未知策略错误

策略转换逻辑

// 策略映射关系:
MPOL_PREFERRED → 首选节点zone列表
MPOL_BIND      → 绑定zone列表(条件性)
MPOL_DEFAULT   → 当前节点zone列表  
MPOL_INTERLEAVE → 不应到达此处(有专门路径)

基于进程状态的动态NUMA节点轮询分配策略interleave_nodes

/* Do dynamic interleaving for a process */
static unsigned interleave_nodes(struct mempolicy *policy)
{unsigned nid, next;struct task_struct *me = current;nid = me->il_next;BUG_ON(nid >= MAX_NUMNODES);next = find_next_bit(policy->v.nodes, MAX_NUMNODES, 1+nid);if (next >= MAX_NUMNODES)next = find_first_bit(policy->v.nodes, MAX_NUMNODES);me->il_next = next;return nid;
}

函数代码分析

函数声明和变量定义

static unsigned interleave_nodes(struct mempolicy *policy)
{unsigned nid, next;struct task_struct *me = current;
  • 这是一个静态函数,专用于进程级别的交错分配
  • 参数policy包含允许的NUMA节点位图
  • 使用current获取当前进程的任务结构,用于维护轮询状态

当前节点获取和验证

	nid = me->il_next;BUG_ON(nid >= MAX_NUMNODES);
  • me->il_next:从当前进程结构中获取上次分配的节点ID
  • il_next字段在进程结构体中维护轮询状态
  • BUG_ON确保节点ID在有效范围内,防止越界访问

查找下一个可用节点

	next = find_next_bit(policy->v.nodes, MAX_NUMNODES, 1+nid);
  • find_next_bit在位图中从1+nid位置开始查找下一个设置的节点
  • 1+nid:从当前节点的下一个位置开始搜索,实现轮询效果
  • 只在策略允许的节点集合(policy->v.nodes)中查找

循环回绕处理

	if (next >= MAX_NUMNODES)next = find_first_bit(policy->v.nodes, MAX_NUMNODES);
  • 如果从1+nid开始找不到更多节点(到达位图末尾)
  • 使用find_first_bit从位图开头重新开始搜索
  • 确保轮询在允许的节点集合内循环进行

状态更新和返回

	me->il_next = next;return nid;
}
  • 更新me->il_next为下一次分配准备的节点ID
  • 返回当前要使用的节点ID(nid)
  • 注意:返回的是旧的il_next值,更新的是新的

函数功能总结

interleave_nodes 实现进程级别的动态NUMA交错分配

  1. 状态维护:在进程结构体中维护轮询状态(il_next)
  2. 轮询分配:在策略允许的节点间循环选择
  3. 边界处理:正确处理节点集合的循环回绕
  4. 动态性:基于进程运行状态而非固定偏移量

静态交错NUMA分配策略offset_il_node

/* Do static interleaving for a VMA with known offset. */
static unsigned offset_il_node(struct mempolicy *pol,struct vm_area_struct *vma, unsigned long off)
{unsigned nnodes = bitmap_weight(pol->v.nodes, MAX_NUMNODES);unsigned target = (unsigned)off % nnodes;int c;int nid = -1;c = 0;do {nid = find_next_bit(pol->v.nodes, MAX_NUMNODES, nid+1);c++;} while (c <= target);BUG_ON(nid >= MAX_NUMNODES);BUG_ON(!test_bit(nid, pol->v.nodes));return nid;
}

函数详细解析

  • 目的:为已知偏移量的VMA执行静态交错分配
  • 静态交错:基于固定偏移量计算目标NUMA节点
  • 确定性:相同偏移量总是映射到相同节点

函数声明

static unsigned offset_il_node(struct mempolicy *pol,struct vm_area_struct *vma, unsigned long off)
{unsigned nnodes = bitmap_weight(pol->v.nodes, MAX_NUMNODES);
  • pol:内存策略,包含允许的节点位图
  • vma:虚拟内存区域(实际未使用)
  • off:页面偏移量

初始化

  • bitmap_weight(pol->v.nodes, MAX_NUMNODES):计算策略中允许的节点数量
  • 作用:确定交错轮询的节点总数

目标节点计算

	unsigned target = (unsigned)off % nnodes;
  • (unsigned)off % nnodes:将页面偏移量映射到节点索引
  • 模运算:确保结果在[0, nnodes-1]范围内
  • 确定性:相同偏移量总是得到相同的目标索引

变量初始化

	int c;int nid = -1;c = 0;
  • nid = -1:起始节点ID,find_next_bitnid+1=0开始
  • c = 0:计数器,记录当前找到的第几个节点

节点查找循环

	do {nid = find_next_bit(pol->v.nodes, MAX_NUMNODES, nid+1);c++;} while (c <= target);
  • pol->v.nodes位图中从nid+1开始查找下一个设置的位

  • MAX_NUMNODES:最大节点数,搜索范围

  • 返回找到的节点ID

  • 遍历策略允许的节点位图

  • 找到第target个设置的节点

  • 示例:如果target=2,找到位图中第二个设置的节点

完整性检查

	BUG_ON(nid >= MAX_NUMNODES);BUG_ON(!test_bit(nid, pol->v.nodes));
  1. nid >= MAX_NUMNODES:确保节点ID在有效范围内
  2. !test_bit(nid, pol->v.nodes):确保找到的节点确实在策略允许集合中

返回结果

	return nid;
}

最终结果:返回计算得到的NUMA节点ID

函数功能总结

offset_il_node 实现静态交错NUMA分配策略

  1. 节点映射:将页面偏移量映射到具体的NUMA节点
  2. 策略遵循:只在策略允许的节点集合中选择
  3. 确定性分配:保证相同输入总是相同输出
  4. 均匀分布:通过模运算实现页面在节点间的循环分布

generic_hweight32/generic_hweight64

#define BITMAP_LAST_WORD_MASK(nbits)					\
(									\((nbits) % BITS_PER_LONG) ?					\(1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL		\
)
int __bitmap_weight(const unsigned long *bitmap, int bits)
{int k, w = 0, lim = bits/BITS_PER_LONG;for (k = 0; k < lim; k++)w += hweight32(bitmap[k]);if (bits % BITS_PER_LONG)w += hweight32(bitmap[k] & BITMAP_LAST_WORD_MASK(bits));return w;
}
/** hweightN: returns the hamming weight (i.e. the number* of bits set) of a N-bit word*/static inline unsigned int generic_hweight32(unsigned int w)
{unsigned int res = (w & 0x55555555) + ((w >> 1) & 0x55555555);res = (res & 0x33333333) + ((res >> 2) & 0x33333333);res = (res & 0x0F0F0F0F) + ((res >> 4) & 0x0F0F0F0F);res = (res & 0x00FF00FF) + ((res >> 8) & 0x00FF00FF);return (res & 0x0000FFFF) + ((res >> 16) & 0x0000FFFF);
}

位图权重计算函数详细解析

BITMAP_LAST_WORD_MASK 宏

#define BITMAP_LAST_WORD_MASK(nbits)					\
(									\((nbits) % BITS_PER_LONG) ?					\(1UL<<((nbits) % BITS_PER_LONG))-1 : ~0UL		\
)

目的:生成最后一个长字的掩码,处理不完整的最后一个字

// 假设 BITS_PER_LONG = 32
BITMAP_LAST_WORD_MASK(35):
35 % 32 = 3(1UL << 3) - 1 = 0b111 (7)
// 只取最后3位有效BITMAP_LAST_WORD_MASK(64):
64 % 32 = 0~0UL = 0xFFFFFFFF
// 所有32位都有效

__bitmap_weight 函数

函数声明

int __bitmap_weight(const unsigned long *bitmap, int bits)
{int k, w = 0, lim = bits/BITS_PER_LONG;
  • bitmap:位图指针

  • bits:要计算的位数

  • k:循环计数器

  • w:权重累加器(设置的位数)

  • lim:完整的长字数量

完整长字处理

	for (k = 0; k < lim; k++)w += hweight32(bitmap[k]);
  • 遍历所有完整的长字
  • 对每个长字调用hweight32计算设置的位数
  • 累加到总权重w

不完整最后一个字处理

	if (bits % BITS_PER_LONG)w += hweight32(bitmap[k] & BITMAP_LAST_WORD_MASK(bits));return w;
}
  • 如果有不完整的最后一个字
  • 应用掩码后计算有效位的权重
  • 返回总设置位数

generic_hweight32 函数

整个32位的设置位数 = (前16位设置位数) + (后16位设置位数)= (前8位 + 后8位) + (再前8位 + 再后8位)= ... 一直分解到2位一组

步骤1:每2位一组计算设置位数

unsigned int res = (w & 0x55555555) + ((w >> 1) & 0x55555555);

掩码0x55555555 = 01010101010101010101010101010101
操作

  • w & 0x55555555:获取所有奇数位
  • (w >> 1) & 0x55555555:获取所有偶数位
  • 相加:每2位中设置的位数(0、1或2)

步骤2:每4位一组累加

res = (res & 0x33333333) + ((res >> 2) & 0x33333333);

掩码0x33333333 = 00110011001100110011001100110011
操作:将相邻的2位组相加,得到每4位中的设置位数

步骤3:每8位一组累加

res = (res & 0x0F0F0F0F) + ((res >> 4) & 0x0F0F0F0F);

掩码0x0F0F0F0F = 00001111000011110000111100001111
操作:将相邻的4位组相加,得到每8位中的设置位数

步骤4:每16位一组累加

res = (res & 0x00FF00FF) + ((res >> 8) & 0x00FF00FF);

掩码0x00FF00FF = 00000000111111110000000011111111
操作:将相邻的8位组相加,得到每16位中的设置位数

步骤5:最终合并

return (res & 0x0000FFFF) + ((res >> 16) & 0x0000FFFF);

操作:将高16位和低16位相加,得到总的设置位数

函数功能总结

BITMAP_LAST_WORD_MASK

  • 生成位图最后一个不完整字的掩码
  • 确保只计算有效的位数

__bitmap_weight

  • 计算位图中设置位的总数
  • 正确处理位图边界情况
  • 高效遍历位图数据结构

generic_hweight32/64

  • 使用分治法计算整数的汉明权重
  • 无分支操作,适合硬件优化
  • 时间复杂度O(log n),n为位数

性能特性

// 与传统循环方法对比:
传统方法: for(i=0; i<32; i++) if(w & (1<<i)) count++;
// 32次迭代,32次位测试,32次分支分治法: 5次位操作,无分支
// 更好的流水线性能,避免分支预测失败

解析多层次的内存策略get_vma_policy

/* Return effective policy for a VMA */
static struct mempolicy *
get_vma_policy(struct vm_area_struct *vma, unsigned long addr)
{struct mempolicy *pol = current->mempolicy;if (vma) {if (vma->vm_ops && vma->vm_ops->get_policy)pol = vma->vm_ops->get_policy(vma, addr);else if (vma->vm_policy &&vma->vm_policy->policy != MPOL_DEFAULT)pol = vma->vm_policy;}if (!pol)pol = &default_policy;return pol;
}

函数详细解析

函数声明

static struct mempolicy *
get_vma_policy(struct vm_area_struct *vma, unsigned long addr)
{struct mempolicy *pol = current->mempolicy;
  • vma:虚拟内存区域指针(可为NULL)

  • addr:需要查询策略的虚拟地址

  • pol = current->mempolicy:从当前进程的内存策略开始

  • 默认起点:首先假设使用进程级别的内存策略

VMA特定策略检查

	if (vma) {if (vma->vm_ops && vma->vm_ops->get_policy)pol = vma->vm_ops->get_policy(vma, addr);
  • 检查VMA是否有特定的get_policy操作
  • 使用场景:特殊内存区域(如共享内存)可能有自定义策略
  • 动态策略:可以根据具体地址返回不同的策略

VMA直接策略检查

		else if (vma->vm_policy &&vma->vm_policy->policy != MPOL_DEFAULT)pol = vma->vm_policy;}
  • 检查VMA是否有直接附加的内存策略
  • vma->vm_policy != MPOL_DEFAULT:排除默认策略(实际就是没有策略)

默认策略回退

	if (!pol)pol = &default_policy;return pol;
}
  • 如果所有层次的策略都为空,使用系统默认策略
  • &default_policy:全局的默认内存策略
  • 确保始终有策略:函数永远不会返回NULL

函数功能总结

get_vma_policyNUMA内存策略解析的核心函数:

  1. 策略解析:解析多层次的内存策略体系
  2. 优先级处理:按照正确优先级选择生效策略
  3. 动态策略支持:支持VMA特定的动态策略回调
  4. 默认保障:确保始终返回有效的策略指针
http://www.dtcms.com/a/605777.html

相关文章:

  • 北京网站设计与制作品牌网站建设策划书
  • Java 9+ 模块化系统(Jigsaw)实战:从 Jar 地狱到模块解耦的架构升级
  • Claude Code 深度解析:架构、工作原理与常见误解
  • 珠海市企业网站制作品牌仿简书wordpress博客主题
  • 文化传媒 网站设计成都网站建设:
  • Python实用指南:python + pyqt
  • SSM基于J2EE的山西旅游网站的设计与实现iiqmx(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 如何通过 WebSocket 接入期货实时行情接口
  • 开源 Objective-C IOS 应用开发(六)Objective-C 和 C语言
  • 网站栅格安装网站模版视频
  • PHP While 循环
  • Docker 部署 DeepSeek-OCR 和WebUI
  • 长沙h5网站建设什么软件可以发布广告信息
  • 如何保证数据库与 Redis 的数据一致性
  • redis连接服务
  • Linux systemd闲谈杂话(第一篇:概述)
  • Spring 核心技术解析【纯干货版】- XII:Spring 数据访问模块 Spring-R2dbc 模块精讲
  • 手机什么网站可以设计楼房关于网站建设的调查问卷
  • 零基础网站建设教学申请自己邮箱域名
  • JVM 内存结构、堆细分、对象生命周期、内存模型全解析
  • 网络安全编程——基于Python实现的SSH通信(Windows执行)
  • WAF防护:应用层安全的核心堡垒
  • 【OpenCV图像处理】图像去噪:cv.fastNlMeansDenoising()
  • 基于AI Agent模板:快速生成 SQL 测试数据
  • 无锡网站建设方案企业计划书
  • 做购票系统网站网站开发推广方案策划书
  • JVM GC 垃圾回收体系完整讲解
  • JVM 内存结构的详细介绍
  • Linux命令-egrep命令(文本搜索工具)
  • 《Flutter全栈开发实战指南:从零到高级》- 14 -网络请求与数据解析