详细介绍Linux 内存管理struct page数据结构中的_count和_mapcount有什么区别?
在Linux内核的struct page中,_count(或_refcount)和_mapcount是两个关键的引用计数成员,它们各自承担不同的职责。以下是深度解析和代码案例:
1. _count vs _mapcount 区别详解
_count(或_refcount)
特性 说明
全称 atomic_t _refcount(新内核)或atomic_t _count(旧内核)
核心作用 页面生命周期计数 - 表示内核持有该物理页的"所有者"数量
触发释放条件 当计数值降为0时,页面返回伙伴系统
操作接口 get_page(), put_page(), page_ref_count()
计数值含义 每增加1表示新增一个所有者(如页缓存、匿名页、设备映射等)
数值范围 ≥0
作用:记录物理页的引用总数,用于判断页是否可以被释放。
递增场景:
页被分配给进程(如malloc、fork)。
页被内核临时使用(如I/O缓冲)。
递减场景:
进程释放内存(如free、exit)。
内核不再需要该页。
临界值:
_count == 0:页可以被回收。
_count > 0:页正在被使用。
_mapcount
特性 说明
全称 atomic_t _mapcount 或 int _mapcount
核心作用 页表映射计数 - 表示页面被映射到用户空间页表的次数
特殊值 -1: 未被用户进程映射
0: 被1个进程映射
N: 被N+1个进程映射
操作接口 page_mapcount(), page_add_file_rmap(), page_remove_rmap()
计数范围 -1 ~ 未限定上限
作用:记录页表映射的数量(即有多少个进程的页表指向该物理页)。
递增场景:
页被映射到进程的地址空间(如mmap、写时复制)。
递减场景:
进程取消映射(如munmap、exit)。
特殊值:
_mapcount == -1:页未被任何进程映射(如仅内核使用)。
_mapcount == 0:页被1个进程映射。
_mapcount > 0:页被多个进程共享(如共享内存)。
2. 核心差异总结
维度 _refcount _mapcount
保护对象 物理页面的生命周期 页面在虚拟地址空间的映射关系
计数值=0 页面可被回收 无实际意义(正常值为-1,0或正数)
增加场景 加入页缓存、设备映射等 创建新页表映射(mmap/mprotect)
减少场景 put_page()调用、文件删除等 解除页表映射(munmap)
用户态影响 间接(决定物理页存在) 直接(决定虚拟映射是否有效)
3.代码案例:模拟写时复制(COW)
以下是一个简化版的内核模块代码,演示_count和_mapcount在写时复制中的变化:
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/sched.h>
static void print_page_counts(struct page *page) {
pr_info("_count = %d, _mapcount = %d\n",
page_ref_count(page),
page_mapcount(page));
}
static int __init cow_demo_init(void) {
struct page *page;
struct vm_area_struct *vma;
unsigned long addr = current->mm->mmap->vm_start;
// 1. 获取用户态地址对应的页
page = follow_page(current->mm->mmap, addr, FOLL_GET);
if (!page) {
pr_err("Failed to get page\n");
return -EFAULT;
}
pr_info("Original page:\n");
print_page_counts(page); // 初始状态:_count=1, _mapcount=0
// 2. 模拟fork():写时复制前(共享映射)
get_page(page); // _count++
atomic_inc(&page->_mapcount); // _mapcount++
pr_info("After fork (shared):\n");
print_page_counts(page); // _count=2, _mapcount=1
// 3. 模拟写入触发COW
put_page(page); // 原进程释放引用 _count--
atomic_dec(&page->_mapcount); // 原进程取消映射 _mapcount--
// 新分配COW页
struct page *new_page = alloc_page(GFP_KERNEL);
copy_user_highpage(new_page, page, addr, current->mm->mmap);
pr_info("After COW (new page):\n");
print_page_counts(new_page); // _count=1, _mapcount=0
// 清理
__free_page(page);
__free_page(new_page);
return 0;
}
static void __exit cow_demo_exit(void) {
pr_info("Module exited\n");
}
module_init(cow_demo_init);
module_exit(cow_demo_exit);
MODULE_LICENSE("GPL");
5. 代码流程解析
初始状态:
进程A映射一个页:_count=1(A持有),_mapcount=0(未共享)。
fork()后:
进程B共享该页:_count=2(A+B持有),_mapcount=1(1次映射)。
写入触发COW:
进程B复制新页:原页_count=1(A持有),_mapcount=0(B取消映射)。
新页_count=1(B持有),_mapcount=0(新映射)。
6. 关键函数说明
page_ref_count(page):获取_count值。
page_mapcount(page):获取_mapcount值。
follow_page():通过虚拟地址获取struct page。
copy_user_highpage():复制页内容(COW核心操作)。
7. 实际运行输出示例
Original page:
_count = 1, _mapcount = 0
After fork (shared):
_count = 2, _mapcount = 1
After COW (new page):
_count = 1, _mapcount = 0
8. 总结
_count是物理页的全局引用计数,决定页是否可回收。
_mapcount是映射计数,反映共享状态(如COW、共享内存)。
两者协同工作:即使_mapcount=0(无映射),若_count>0(内核仍在使用),页也不能释放。
9.性能优化与注意事项
原子操作开销
_refcount使用atomic_t确保原子性
高并发场景下可能成为瓶颈,需尽量减少频繁操作
引用循环问题
// 错误示例:导致永久引用
void set_page_private(struct page *page, void *data)
{
get_page(page); // 增加额外引用
page->private = data;
}
多类型页面处理
// 处理复合页(Compound Pages)
if (PageCompound(page)) {
// 整个复合页共享相同_refcount
// 但每个子页有独立_mapcount
}
页面迁移保护
// 迁移前检查
if (page_mapcount(page) > 0 || page_ref_count(page) > 1) {
return -EBUSY; // 页面被使用中
}
10.调试工具
CONFIG_DEBUG_VM:检测无效的计数操作
page_owner:追踪页面生命周期所有者
/proc/pagetypeinfo:查看页面计数统计
通过正确理解_refcount和_mapcount的差异及其协同工作方式,开发者可以有效优化内存管理逻辑,避免常见错误如页面泄露或过早释放。