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_ops和nopage操作 - 如果没有,说明是匿名映射
- 调用
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() */
- 释放锁:在调用文件操作前释放页表锁,避免死锁
- 获取映射信息:如果是文件映射,获取文件地址空间和截断计数
- 内存屏障:
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):非共享映射
复制过程:
anon_vma_prepare(vma):准备匿名映射数据结构alloc_page_vma():在VMA中分配新页面copy_user_highpage():复制原页面内容到新页面page_cache_release(new_page):释放原页面引用- 标记为匿名页面(
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);
- RSS统计:增加进程的常驻集大小(如果不是保留页面)
- 创建PTE:
mk_pte创建页表项 - 写权限处理:如果需要写访问,标记为可写和脏
- 设置PTE:
set_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 是处理首次访问缺页的核心函数,负责:
- 页面类型识别:区分匿名映射和文件映射
- 页面分配:通过VMA特定的
nopage方法获取页面 - 写时复制:在写访问时提前进行COW处理
- 竞态处理:检测文件截断和其他线程竞争
- 页表更新:安全地设置页表项并建立反向映射
为匿名页面添加反向映射条目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;
索引构建:
(address - vma->vm_start) >> PAGE_SHIFT:计算在VMA内的页偏移+ vma->vm_pgoff:加上VMA在文件/匿名空间中的起始偏移- 右移调整:转换为页缓存索引
- 最终
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操作并测试结果是否为零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);
- 释放资源:在页面分配前释放页表映射和锁
- 避免死锁:页面分配可能触发内存回收,需要睡眠等待
- 性能考虑:页面分配是较慢的操作,不应持有自旋锁
匿名映射准备
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);
alloc_page_vma(GFP_HIGHUSER, vma, addr):在VMA中分配页面GFP_HIGHUSER:从高端内存区域分配,可移动到高端内存
- 内存不足检查:如果分配失败,跳转到
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;}
- 检查PTE是否仍然为空
- 如果其他线程已经设置了PTE:
- 释放页表映射
- 释放刚分配的页面
- 释放锁
- 跳转到成功返回
页面设置和映射
mm->rss++;entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,vma->vm_page_prot)),vma);
mm->rss++:增加进程的常驻集大小统计mk_pte(page, vma->vm_page_prot):创建页表项pte_mkdirty():标记页面为脏(因为即将写入)maybe_mkwrite():根据VMA权限设置写权限
页面管理和反向映射
lru_cache_add_active(page);mark_page_accessed(page);page_add_anon_rmap(page, vma, addr);}
lru_cache_add_active(page):将页面添加到活跃LRU列表mark_page_accessed(page):标记页面已被访问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 专门处理匿名内存映射的缺页错误:
- 零页面优化:对只读访问映射到全局零页面,节省内存
- 按需分配:只有在写访问时才分配实际物理页面
- 写时复制:实现COW语义的基础
- 竞态处理:正确处理多线程并发访问
- 资源管理:准确的内存统计和反向映射
用户空间页面分配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 是用户空间页面分配的专用函数:
- 策略感知分配:考虑VMA和进程的NUMA内存策略
- 交错策略优化:特殊处理MPOL_INTERLEAVE策略
- 地址验证:确保分配地址在有效VMA范围内
- 统一接口:为不同策略提供一致的分配接口
在指定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交错分配策略的专用实现函数,主要功能包括:
- 目标节点分配:在指定的NUMA节点上执行页面分配
- zone列表管理:根据GFP标志构建合适的zone优先级列表
- 统计监控:跟踪交错策略的实际命中情况,用于性能分析
- 安全验证:确保目标节点在线状态,防止无效分配
将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_policy 是NUMA策略到zone列表的转换器:
- 策略解析:将抽象的内存策略转换为具体的分配目标
- zone选择:根据GFP标志选择合适的memory zone
- 边界处理:处理策略不适用时的回退情况
- 错误防护:检测并报告未知策略错误
策略转换逻辑
// 策略映射关系:
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:从当前进程结构中获取上次分配的节点IDil_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交错分配:
- 状态维护:在进程结构体中维护轮询状态(
il_next) - 轮询分配:在策略允许的节点间循环选择
- 边界处理:正确处理节点集合的循环回绕
- 动态性:基于进程运行状态而非固定偏移量
静态交错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_bit从nid+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));
nid >= MAX_NUMNODES:确保节点ID在有效范围内!test_bit(nid, pol->v.nodes):确保找到的节点确实在策略允许集合中
返回结果
return nid;
}
最终结果:返回计算得到的NUMA节点ID
函数功能总结
offset_il_node 实现静态交错NUMA分配策略:
- 节点映射:将页面偏移量映射到具体的NUMA节点
- 策略遵循:只在策略允许的节点集合中选择
- 确定性分配:保证相同输入总是相同输出
- 均匀分布:通过模运算实现页面在节点间的循环分布
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_policy 是NUMA内存策略解析的核心函数:
- 策略解析:解析多层次的内存策略体系
- 优先级处理:按照正确优先级选择生效策略
- 动态策略支持:支持VMA特定的动态策略回调
- 默认保障:确保始终返回有效的策略指针
