Linux 内核目录项缓存与内存回收机制总结
一、目录项缓存(dcache
)回收机制
1. shrink_dcache_memory
核心函数
- 功能 :控制目录项缓存(
dcache
)的内存回收 - 关键参数
nr
:目标回收数量gfp_mask
:内存分配标志
- 返回值 :剩余未使用
dentry
的估算值(受vfs_cache_pressure
影响)
2. prune_dcache
实际回收实现
- LRU策略:从链表尾部(最近最少使用)开始回收
- 三级检查
- 引用计数检查(
d_count
) - 最近访问标志(
DCACHE_REFERENCED
) - 实际回收条件
- 引用计数检查(
二、inode
缓存回收机制
1. shrink_icache_memory
核心函数
- 与
dcache
区别- 使用不同的统计结构(
inodes_stat
) - 相同的压力系数计算方式
- 使用不同的统计结构(
2. prune_icache
实际回收实现
- 特殊处理
- 缓冲区清理(
remove_inode_buffers
) - 页面缓存无效化(
invalidate_inode_pages
)
- 缓冲区清理(
- 双重锁机制
- 全局信号量(
iprune_sem
) - 自旋锁(
inode_lock
)
- 全局信号量(
三、内存收缩器框架
1. 收缩器注册
c
struct shrinker *set_shrinker(int seeks, shrinker_t shrinker_func) {
struct shrinker *s = kmalloc(sizeof(*s), GFP_KERNEL);
if (s) {
s->shrinker = shrinker_func; // 设置回调函数
s->seeks = seeks; // 设置查找代价
down_write(&shrinker_rwsem);
list_add(&s->list, &shrinker_list); // 加入全局列表
up_write(&shrinker_rwsem);
}
return s;
}
- 核心字段
shrinker
:实际回收函数seeks
:查找代价参数
- 保护机制 :读写信号量(
shrinker_rwsem
)
五、核心机制对比
机制 | dcache 回收 |
inode 回收 |
收缩器框架 |
---|---|---|---|
数据结构 | dentry + LRU链表 |
inode + LRU链表 |
shrinker结构体 |
锁机制 | dcache_lock + dentry->d_lock |
iprune_sem + inode_lock |
shrinker_rwsem |
回收条件 | 引用计数=0且无REFERENCED标志 | 引用计数=0且无脏数据 | 回调函数决定 |
性能优化 | 预取(prefetch) | 页面向量(pagevec )批处理 |
seeks参数调整优先级 |
错误处理 | 竞争条件重试 | 状态检查后跳过 | 信号量保护 |
六、系统调优参数
vfs_cache_pressure
- 值=100:默认行为
- 值>100:更积极回收(内存紧张时)
- 值<100:更保守回收(高性能需求时)
- 收缩器
seeks
参数- 影响内存回收时的扫描优先级
- 值越小表示回收代价越低,优先级越高
七、典型应用场景
- 内存不足时
kswapd
后台线程调用收缩器- 直接内存回收路径调用
- 文件系统卸载时
- 清理关联的
dentry
和inode
缓存 - 确保资源完全释放
- 清理关联的
- 性能敏感场景
- 调整
vfs_cache_pressure
平衡性能与内存使用 - 优化
seeks
参数减少回收开销
- 调整
八、设计模式总结
- LRU缓存管理
- 最近最少使用优先回收
- 双向链表实现高效插入/删除
- 多级锁机制
- 全局锁保护列表结构
- 细粒度锁保护单个对象
- 信号量序列化关键操作
- 安全回收策略
- 引用计数防止使用中回收
- 状态标志避免竞争条件
- 回调函数实现灵活策略
- 性能优化技术
- 批量处理减少锁持有时间
- 预取指令提高缓存命中率
- 条件调度避免长时间阻塞
目录项缓存的内存回收shrink_dcache_memory
c
static int shrink_dcache_memory(int nr, unsigned int gfp_mask)
{
if (nr) {
if (!(gfp_mask & __GFP_FS))
return -1;
prune_dcache(nr);
}
return (dentry_stat.nr_unused / 100) * sysctl_vfs_cache_pressure;
}
static void prune_dcache(int count)
{
spin_lock(&dcache_lock);
for (; count ; count--) {
struct dentry *dentry;
struct list_head *tmp;
tmp = dentry_unused.prev;
if (tmp == &dentry_unused)
break;
list_del_init(tmp);
prefetch(dentry_unused.prev);
dentry_stat.nr_unused--;
dentry = list_entry(tmp, struct dentry, d_lru);
spin_lock(&dentry->d_lock);
/*
* We found an inuse dentry which was not removed from
* dentry_unused because of laziness during lookup. Do not free
* it - just keep it off the dentry_unused list.
*/
if (atomic_read(&dentry->d_count)) {
spin_unlock(&dentry->d_lock);
continue;
}
/* If the dentry was recently referenced, don't free it. */
if (dentry->d_flags & DCACHE_REFERENCED) {
dentry->d_flags &= ~DCACHE_REFERENCED;
list_add(&dentry->d_lru, &dentry_unused);
dentry_stat.nr_unused++;
spin_unlock(&dentry->d_lock);
continue;
}
prune_one_dentry(dentry);
}
spin_unlock(&dcache_lock);
}
函数功能
shrink_dcache_memory
和prune_dcache
函数共同实现了目录项缓存的内存回收机制,在系统内存不足时释放未使用的dentry
对象来回收内存
shrink_dcache_memory
函数
第1行:函数声明
c
static int shrink_dcache_memory(int nr, unsigned int gfp_mask)
- 参数 :
nr
: 要回收的对象数量目标gfp_mask
: 内存分配标志位,指示回收上下文
第2-7行:回收条件检查
c
if (nr) {
if (!(gfp_mask & __GFP_FS))
return -1;
prune_dcache(nr);
}
if (nr)
: 检查是否有回收目标数量if (!(gfp_mask & __GFP_FS))
: 检查是否允许文件系统操作__GFP_FS
标志表示允许调用到文件系统层- 如果没有此标志,返回-1表示无法回收
prune_dcache(nr)
: 调用实际的回收函数
第8行:返回剩余对象估算
c
return (dentry_stat.nr_unused / 100) * sysctl_vfs_cache_pressure;
- 计算逻辑 : 剩余未使用
dentry
数量除以100,乘以缓存压力系数 dentry_stat.nr_unused
: 全局统计的未使用dentry
数量sysctl_vfs_cache_pressure
: 系统可调参数,控制缓存回收积极性- 最终得出各种缓存应该回收数量所占的比例
prune_dcache
函数
第1行:函数声明
c
static void prune_dcache(int count)
- 参数 :
count
- 要尝试回收的dentry
数量
第2-3行:获取全局锁并开始循环
c
spin_lock(&dcache_lock);
for (; count ; count--) {
spin_lock(&dcache_lock)
: 获取dentry
缓存全局自旋锁for (; count ; count--)
: 循环尝试回收count个dentry
第4-12行:从LRU链表获取dentry
c
struct dentry *dentry;
struct list_head *tmp;
tmp = dentry_unused.prev;
if (tmp == &dentry_unused)
break;
list_del_init(tmp);
prefetch(dentry_unused.prev);
dentry_stat.nr_unused--;
dentry = list_entry(tmp, struct dentry, d_lru);
tmp = dentry_unused.prev
: 从LRU链表尾部获取(最近最少使用)if (tmp == &dentry_unused)
: 检查链表是否为空list_del_init(tmp)
: 从链表中移除该节点prefetch(dentry_unused.prev)
: 预取下一个dentry
,提高性能dentry_stat.nr_unused--
: 减少未使用dentry
统计计数dentry = list_entry(tmp, struct dentry, d_lru)
: 获取dentry
结构体指针
第14-20行:检查引用计数
c
spin_lock(&dentry->d_lock);
/*
* We found an inuse dentry which was not removed from
* dentry_unused because of laziness during lookup. Do not free
* it - just keep it off the dentry_unused list.
*/
if (atomic_read(&dentry->d_count)) {
spin_unlock(&dentry->d_lock);
continue;
}
spin_lock(&dentry->d_lock)
: 获取单个dentry
的锁- 可能找到正在使用的
dentry
atomic_read(&dentry->d_count)
: 检查dentry
引用计数- 如果被引用: 释放锁并继续下一个
第21-29行:检查最近引用标志
c
/* If the dentry was recently referenced, don't free it. */
if (dentry->d_flags & DCACHE_REFERENCED) {
dentry->d_flags &= ~DCACHE_REFERENCED;
list_add(&dentry->d_lru, &dentry_unused);
dentry_stat.nr_unused++;
spin_unlock(&dentry->d_lock);
continue;
}
DCACHE_REFERENCED
: 最近被访问的标志位- 清除标志 :
dentry->d_flags &= ~DCACHE_REFERENCED
- 重新加入链表: 放回LRU链表头部(最近使用)
- 增加统计 :
dentry_stat.nr_unused++
- 继续下一个 : 这个
dentry
暂时保留
第30行:回收符合条件的dentry
c
prune_one_dentry(dentry);
}
spin_unlock(&dcache_lock);
prune_one_dentry(dentry)
: 回收这个dentry
- 循环结束: 完成count次尝试或链表为空
spin_unlock(&dcache_lock)
: 释放全局锁
详细机制分析
LRU链表管理策略
LRU链表dentry_unused 链表头部
最近使用 链表尾部
最近最少使用 新加入或最近访问 回收候选 DCACHE_REFERENCED标志 无标志的dentry被回收
引用计数和状态标志
c
// dentry的三种状态:
1. 正在使用: d_count > 0 → 不能被回收
2. 最近使用: DCACHE_REFERENCED → 暂时保留
3. 可回收: d_count=0 且 无DCACHE_REFERENCED → 可安全回收
// 状态转换:
使用中 → 释放后 → 最近使用 → 长时间未用 → 可回收
系统调优参数
sysctl_vfs_cache_pressure
c
// 缓存压力参数影响:
值 = 100: 默认行为
值 > 100: 更积极回收(适合内存紧张系统)
值 < 100: 更保守回收(适合需要高性能的系统)
// 在shrink_dcache_memory中的影响:
返回值 = (未使用dentry数 / 100) * 压力系数
// 影响内存回收子系统对dcache的回收优先级
inode
缓存的内存回收shrink_icache_memory
c
static int shrink_icache_memory(int nr, unsigned int gfp_mask)
{
if (nr) {
/*
* Nasty deadlock avoidance. We may hold various FS locks,
* and we don't want to recurse into the FS that called us
* in clear_inode() and friends..
*/
if (!(gfp_mask & __GFP_FS))
return -1;
prune_icache(nr);
}
return (inodes_stat.nr_unused / 100) * sysctl_vfs_cache_pressure;
}
static void prune_icache(int nr_to_scan)
{
LIST_HEAD(freeable);
int nr_pruned = 0;
int nr_scanned;
unsigned long reap = 0;
down(&iprune_sem);
spin_lock(&inode_lock);
for (nr_scanned = 0; nr_scanned < nr_to_scan; nr_scanned++) {
struct inode *inode;
if (list_empty(&inode_unused))
break;
inode = list_entry(inode_unused.prev, struct inode, i_list);
if (inode->i_state || atomic_read(&inode->i_count)) {
list_move(&inode->i_list, &inode_unused);
continue;
}
if (inode_has_buffers(inode) || inode->i_data.nrpages) {
__iget(inode);
spin_unlock(&inode_lock);
if (remove_inode_buffers(inode))
reap += invalidate_inode_pages(&inode->i_data);
iput(inode);
spin_lock(&inode_lock);
if (inode != list_entry(inode_unused.next,
struct inode, i_list))
continue; /* wrong inode or list_empty */
if (!can_unuse(inode))
continue;
}
hlist_del_init(&inode->i_hash);
list_move(&inode->i_list, &freeable);
inode->i_state |= I_FREEING;
nr_pruned++;
}
inodes_stat.nr_unused -= nr_pruned;
spin_unlock(&inode_lock);
dispose_list(&freeable);
up(&iprune_sem);
if (current_is_kswapd())
mod_page_state(kswapd_inodesteal, reap);
else
mod_page_state(pginodesteal, reap);
}
函数功能
shrink_icache_memory
和prune_icache
函数共同实现了inode
缓存的内存回收机制,在系统内存不足时释放未使用的inode
对象来回收内存。
shrink_icache_memory
函数
第1行:函数声明
c
static int shrink_icache_memory(int nr, unsigned int gfp_mask)
- 参数 :
nr
: 要回收的inode
数量目标gfp_mask
: 内存分配标志位,指示回收上下文
第2-10行:回收条件检查和注释
c
if (nr) {
/*
* Nasty deadlock avoidance. We may hold various FS locks,
* and we don't want to recurse into the FS that called us
* in clear_inode() and friends..
*/
if (!(gfp_mask & __GFP_FS))
return -1;
prune_icache(nr);
}
if (nr)
: 检查是否有回收目标数量- 这里可能持有各种文件系统锁,不希望递归进入调用我们的文件系统
- 这个注释描述的是Linux内核中一个经典且危险的死锁场景:
- 根本原因: 内存回收操作递归进入正在等待内存的文件系统
- 触发条件: 文件系统持有锁时发生内存分配,而内存回收又需要相同的锁
- 解决方案 : 通过
__GFP_FS
标志识别安全上下文,只在允许文件系统操作时执行inode
回收 - 设计原则: 安全性优先于内存回收效率
- 这个注释描述的是Linux内核中一个经典且危险的死锁场景:
if (!(gfp_mask & __GFP_FS))
: 检查是否允许文件系统操作prune_icache(nr)
: 调用实际的inode
回收函数
第11行:返回剩余对象估算
c
return (inodes_stat.nr_unused / 100) * sysctl_vfs_cache_pressure;
- 计算逻辑 : 剩余未使用
inode
数量除以100,乘以缓存压力系数 inodes_stat.nr_unused
: 全局统计的未使用inode
数量
prune_icache
函数
第1-5行:函数声明和变量定义
c
static void prune_icache(int nr_to_scan)
{
LIST_HEAD(freeable);
int nr_pruned = 0;
int nr_scanned;
unsigned long reap = 0;
- 参数 :
nr_to_scan
- 要扫描的inode
数量 LIST_HEAD(freeable)
: 创建临时链表头,用于存放要释放的inode
nr_pruned
: 实际修剪的inode
计数nr_scanned
: 已扫描的inode
计数reap
: 回收的页面计数
第7-8行:获取锁
c
down(&iprune_sem);
spin_lock(&inode_lock);
down(&iprune_sem)
: 获取inode
修剪信号量(可能睡眠)spin_lock(&inode_lock)
: 获取inode
全局自旋锁
第9-11行:开始扫描循环
c
for (nr_scanned = 0; nr_scanned < nr_to_scan; nr_scanned++) {
struct inode *inode;
if (list_empty(&inode_unused))
break;
- 循环条件 : 扫描nr_to_scan个
inode
或链表为空 list_empty(&inode_unused)
: 检查未使用inode
链表是否为空
第13-14行:获取inode
c
inode = list_entry(inode_unused.prev, struct inode, i_list);
inode_unused.prev
: 从LRU链表尾部获取(最近最少使用)list_entry
: 获取inode
结构体指针
第16-20行:检查inode
状态
c
if (inode->i_state || atomic_read(&inode->i_count)) {
list_move(&inode->i_list, &inode_unused);
continue;
}
inode->i_state
: 检查inode
状态标志(如I_DIRTY等)atomic_read(&inode->i_count)
: 检查引用计数- 如果被使用: 移动到链表头部,继续下一个
第21-36行:处理有缓存页的inode
c
if (inode_has_buffers(inode) || inode->i_data.nrpages) {
__iget(inode);
spin_unlock(&inode_lock);
if (remove_inode_buffers(inode))
reap += invalidate_inode_pages(&inode->i_data);
iput(inode);
spin_lock(&inode_lock);
if (inode != list_entry(inode_unused.next,
struct inode, i_list))
continue; /* wrong inode or list_empty */
if (!can_unuse(inode))
continue;
}
详细步骤:
- 检查是否有缓存 :
inode_has_buffers()
或inode->i_data.nrpages
- 增加引用 :
__iget(inode)
防止在清理过程中被释放 - 释放锁 :
spin_unlock(&inode_lock)
因为后续操作可能睡眠 - 移除缓冲区 :
remove_inode_buffers(inode)
- 无效化页面 :
invalidate_inode_pages()
并统计回收页面 - 减少引用 :
iput(inode)
- 重新获取锁 :
spin_lock(&inode_lock)
- 验证
inode
: 检查inode
是否仍在列表中且可释放
第37-41行:标记要释放的inode
c
hlist_del_init(&inode->i_hash);
list_move(&inode->i_list, &freeable);
inode->i_state |= I_FREEING;
nr_pruned++;
}
hlist_del_init(&inode->i_hash)
: 从哈希表中移除list_move(&inode->i_list, &freeable)
: 移动到释放链表inode->i_state |= I_FREEING
: 标记为正在释放状态nr_pruned++
: 增加修剪计数
第42-46行:更新统计和释放锁
c
inodes_stat.nr_unused -= nr_pruned;
spin_unlock(&inode_lock);
dispose_list(&freeable);
up(&iprune_sem);
- 更新统计 : 减少未使用
inode
计数 - 释放锁 :
spin_unlock(&inode_lock)
- 释放
inode
:dispose_list(&freeable)
实际释放内存 - 释放信号量 :
up(&iprune_sem)
第48-52行:更新页面回收统计
c
if (current_is_kswapd())
mod_page_state(kswapd_inodesteal, reap);
else
mod_page_state(pginodesteal, reap);
- 区分上下文 : 如果是
kswapd
线程还是直接回收 - 更新统计 : 记录通过
inode
回收的页面数量
关键机制详解
锁的使用策略
锁层次结构 iprune_sem 信号量 inode_lock 自旋锁 保护整个修剪过程
可睡眠 保护inode链表操作
不可睡眠 序列化多个修剪操作 保证链表操作原子性 避免并发修剪冲突 防止数据结构损坏
inode
状态检查逻辑
c
// 不能释放的inode条件:
1. i_state != 0 // 有特殊状态标志(如脏数据)
2. i_count > 1 // 被其他组件引用
3. 有缓存页面 // 需要先清理页面缓存
4. 有缓冲区 // 需要先清理缓冲区
5. can_unuse()失败 // 文件系统特定检查
// 可释放的inode:
- 在inode_unused链表中
- i_count = 1(只有缓存引用)
- 无特殊状态标志
- 无缓存页面和缓冲区
页面缓存清理过程
c
// 清理有缓存页的inode:
1. __iget(inode): 增加引用,防止在清理时被释放
2. remove_inode_buffers: 移除缓冲区头
3. invalidate_inode_pages: 使页面缓存无效,回收内存
4. iput(inode): 减少引用,可能触发实际释放
// reap统计:记录通过inode回收的页面数量
性能优化特性
临时链表的使用
c
// 为什么使用freeable链表?
1. 在持有锁时只进行链表操作
2. 释放锁后再进行实际的内存释放
3. 减少锁持有时间,提高并发性
4. 避免在持有锁时调用可能睡眠的函数
区分回收上下文
c
// kswapd vs 直接回收:
kswapd_inodesteal: 后台回收线程的统计
pginodesteal: 直接内存回收的统计
// 意义:
- 监控不同回收路径的效果
- 调优系统内存回收策略
- 诊断性能问题
错误处理和边界情况
竞争条件处理
c
// 可能的竞争场景:
1. 其他线程在修剪过程中增加i_count
2. 其他线程将inode重新加入使用
3. 其他修剪操作同时进行
// 防护措施:
- iprune_sem信号量序列化修剪操作
- inode_lock保护链表操作
- 引用计数防止意外释放
- 状态标志确保一致性
安全释放保证
c
// 确保inode安全释放的步骤:
1. 检查引用计数和状态
2. 清理关联的缓存页面
3. 从所有链表中移除
4. 设置I_FREEING状态
5. 实际释放内存
总结
这两个函数共同实现了Linux inode
缓存的高效内存回收:
- 安全第一 : 通过
__GFP_FS
检查防止死锁,确保只在安全上下文中回收 - 精确选择 : 基于LRU算法和
inode
状态智能选择回收候选 - 完整清理 : 不仅释放
inode
本身,还清理关联的页面缓存和缓冲区 - 并发安全: 通过多级锁机制防止竞争条件
- 性能监控: 区分不同回收上下文并统计回收效果
检查inode
是否有关联的缓冲区inode_has_buffers
c
int inode_has_buffers(struct inode *inode)
{
return !list_empty(&inode->i_data.private_list);
}
函数功能
inode_has_buffers
函数用于检查一个inode
是否有关联的缓冲区(buffer heads)。它是文件系统缓冲区管理的基础函数,用于确定inode
是否需要特殊的缓冲区清理操作
代码逐段解释
第1行:函数声明
c
int inode_has_buffers(struct inode *inode)
- 返回类型 :
int
- 返回1表示有缓冲区,0表示没有缓冲区 - 参数 :
struct inode *inode
- 要检查的inode
指针 - 功能 : 检查指定
inode
是否包含缓冲区
第2行:函数实现
c
return !list_empty(&inode->i_data.private_list);
这是函数的唯一操作,包含几个关键步骤:
步骤分解:
-
&inode->i_data.private_list
:inode->i_data
是inode
的地址空间结构(struct address_space)private_list
是地址空间中的一个链表头,用于链接该inode
的所有缓冲区头
-
list_empty(&inode->i_data.private_list)
:list_empty()
函数检查链表是否为空- 如果链表为空,返回1(真)
- 如果链表不为空,返回0(假)
-
!
(逻辑非操作):- 对
list_empty()
的结果取反 - 如果链表为空:
list_empty()
返回1 →!1
= 0 → 函数返回0(没有缓冲区) - 如果链表不为空:
list_empty()
返回0 →!0
= 1 → 函数返回1(有缓冲区)
- 对
详细数据结构分析
inode
和 address_space 的关系
struct inode struct address_space i_data struct list_head private_list 缓冲区头链表 struct buffer_head struct buffer_head ... 块设备缓冲区1 块设备缓冲区2 块设备缓冲区N
private_list 链表的实际内容
c
// private_list中存储的是buffer_head结构体:
struct buffer_head {
struct list_head b_assoc_buffers; // 用于链接到private_list
struct buffer_head *b_this_page; // 同一页面的其他缓冲区
sector_t b_blocknr; // 块号
// ... 其他字段
};
// 链表连接关系:
inode->i_data.private_list → buffer_head->b_assoc_buffers → buffer_head->b_assoc_buffers → ...
移除与inode
关联的所有缓冲区remove_inode_buffers
c
int remove_inode_buffers(struct inode *inode)
{
int ret = 1;
if (inode_has_buffers(inode)) {
struct address_space *mapping = &inode->i_data;
struct list_head *list = &mapping->private_list;
struct address_space *buffer_mapping = mapping->assoc_mapping;
spin_lock(&buffer_mapping->private_lock);
while (!list_empty(list)) {
struct buffer_head *bh = BH_ENTRY(list->next);
if (buffer_dirty(bh)) {
ret = 0;
break;
}
__remove_assoc_queue(bh);
}
spin_unlock(&buffer_mapping->private_lock);
}
return ret;
}
static inline void __remove_assoc_queue(struct buffer_head *bh)
{
list_del_init(&bh->b_assoc_buffers);
}
函数功能
remove_inode_buffers
函数用于移除与inode
关联的所有缓冲区。它会遍历inode
的缓冲区链表,移除所有干净的缓冲区,但如果遇到脏缓冲区则停止并返回失败。
代码逐段解释
remove_inode_buffers
函数
第1-2行:函数声明和变量初始化
c
int remove_inode_buffers(struct inode *inode)
{
int ret = 1;
- 参数 :
struct inode *inode
- 要清理缓冲区的inode
指针 - 返回值 :
int
- 1表示成功移除所有缓冲区,0表示遇到脏缓冲区而中止 ret = 1
: 初始化返回值为成功状态
第3-7行:条件检查和变量准备
c
if (inode_has_buffers(inode)) {
struct address_space *mapping = &inode->i_data;
struct list_head *list = &mapping->private_list;
struct address_space *buffer_mapping = mapping->assoc_mapping;
if (inode_has_buffers(inode))
: 检查inode
是否有缓冲区,没有则直接返回mapping = &inode->i_data
: 获取inode
的地址空间结构list = &mapping->private_list
: 获取缓冲区链表头指针buffer_mapping = mapping->assoc_mapping
: 获取关联的缓冲区映射地址空间
第9行:获取缓冲区映射锁
c
spin_lock(&buffer_mapping->private_lock);
spin_lock(&buffer_mapping->private_lock)
: 获取缓冲区映射的私有锁- 目的: 保护缓冲区链表操作的原子性,防止竞争条件
第10-17行:遍历缓冲区链表
c
while (!list_empty(list)) {
struct buffer_head *bh = BH_ENTRY(list->next);
if (buffer_dirty(bh)) {
ret = 0;
break;
}
__remove_assoc_queue(bh);
}
详细步骤:
while (!list_empty(list))
: 循环直到缓冲区链表为空bh = BH_ENTRY(list->next)
:- 从链表头部获取下一个缓冲区头
BH_ENTRY
宏将list_head转换为buffer_head
if (buffer_dirty(bh))
: 检查缓冲区是否脏(有未写回的数据)ret = 0; break;
: 如果缓冲区脏,设置返回值为失败并跳出循环__remove_assoc_queue(bh)
: 如果缓冲区干净,从链表中移除
第18行:释放锁
c
spin_unlock(&buffer_mapping->private_lock);
- 释放之前获取的缓冲区映射锁
第20行:返回结果
c
}
return ret;
}
- 返回操作结果:1=成功,0=遇到脏缓冲区
__remove_assoc_queue
函数
第1-4行:函数实现
c
static inline void __remove_assoc_queue(struct buffer_head *bh)
{
list_del_init(&bh->b_assoc_buffers);
}
- 参数 :
struct buffer_head *bh
- 要移除的缓冲区头 list_del_init(&bh->b_assoc_buffers)
:- 从链表中删除缓冲区节点
- 并重新初始化节点指针(
prev/next
指向自己)
实际使用场景
在内存回收中的角色
c
// 完整的inode回收流程:
1. 找到可回收的inode
2. remove_inode_buffers() 清理缓冲区
3. invalidate_inode_pages() 清理页面缓存
4. 实际释放inode内存
// 如果remove_inode_buffers返回0:
- 这个inode暂时不能回收
- 需要等待脏数据写回
- 或者由其他机制处理
页面缓存无效化函数invalidate_inode_pages
c
unsigned long invalidate_inode_pages(struct address_space *mapping)
{
return invalidate_mapping_pages(mapping, 0, ~0UL);
}
unsigned long invalidate_mapping_pages(struct address_space *mapping,
pgoff_t start, pgoff_t end)
{
struct pagevec pvec;
pgoff_t next = start;
unsigned long ret = 0;
int i;
pagevec_init(&pvec, 0);
while (next <= end &&
pagevec_lookup(&pvec, mapping, next, PAGEVEC_SIZE)) {
for (i = 0; i < pagevec_count(&pvec); i++) {
struct page *page = pvec.pages[i];
if (TestSetPageLocked(page)) {
next++;
continue;
}
if (page->index > next)
next = page->index;
next++;
if (PageDirty(page) || PageWriteback(page))
goto unlock;
if (page_mapped(page))
goto unlock;
ret += invalidate_complete_page(mapping, page);
unlock:
unlock_page(page);
if (next > end)
break;
}
pagevec_release(&pvec);
cond_resched();
}
return ret;
}
函数功能
invalidate_inode_pages
和invalidate_mapping_pages
函数用于使地址空间中的页面缓存无效,从内存中移除文件的缓存页面,通常在inode
回收或文件截断时调用
代码逐段解释
invalidate_inode_pages
函数
第1-3行:包装函数
c
unsigned long invalidate_inode_pages(struct address_space *mapping)
{
return invalidate_mapping_pages(mapping, 0, ~0UL);
}
- 参数 :
struct address_space *mapping
-inode
的地址空间 - 返回值 :
unsigned long
- 成功无效化的页面数量 invalidate_mapping_pages(mapping, 0, ~0UL)
:- 调用实际实现函数
0
: 起始页面索引~0UL
: 结束页面索引(最大值,表示所有页面)
invalidate_mapping_pages 函数
第1-5行:函数声明和变量定义
c
unsigned long invalidate_mapping_pages(struct address_space *mapping,
pgoff_t start, pgoff_t end)
{
struct pagevec pvec;
pgoff_t next = start;
unsigned long ret = 0;
int i;
- 参数 :
mapping
: 地址空间指针start
: 起始页面索引end
: 结束页面索引
struct pagevec pvec
: 页面向量,用于批量处理页面pgoff_t next = start
: 下一个要处理的页面索引,初始化为startunsigned long ret = 0
: 成功无效化的页面计数int i
: 循环计数器
第7行:初始化页面向量
c
pagevec_init(&pvec, 0);
pagevec_init(&pvec, 0)
: 初始化页面向量- 参数0: 冷页面(cold page),表示这些页面不太可能被再次访问
第8-9行:批量查找页面循环
c
while (next <= end &&
pagevec_lookup(&pvec, mapping, next, PAGEVEC_SIZE)) {
next <= end
: 检查是否还有要处理的页面范围pagevec_lookup(&pvec, mapping, next, PAGEVEC_SIZE)
:- 从地址空间中批量查找页面
- 从
next
索引开始,最多查找PAGEVEC_SIZE
个页面 - 返回true表示找到了页面
第10-11行:遍历页面向量
c
for (i = 0; i < pagevec_count(&pvec); i++) {
struct page *page = pvec.pages[i];
pagevec_count(&pvec)
: 获取页面向量中实际包含的页面数量page = pvec.pages[i]
: 获取第i个页面指针
第13-16行:尝试锁定页面
c
if (TestSetPageLocked(page)) {
next++;
continue;
}
TestSetPageLocked(page)
: 原子性地测试并设置页面锁定标志- 返回值: 如果页面已被锁定,返回true(非零)
- 处理: 如果页面已被锁定,跳过这个页面,继续下一个
第17-20行:更新下一个页面索引
c
if (page->index > next)
next = page->index;
next++;
page->index
: 页面在文件中的索引位置- 逻辑: 如果页面索引大于当前next,更新next以避免重复处理
next++
: 准备处理下一个页面
第21-23行:检查页面状态
c
if (PageDirty(page) || PageWriteback(page))
goto unlock;
PageDirty(page)
: 检查页面是否脏(有未写回的数据)PageWriteback(page)
: 检查页面是否正在写回磁盘- 如果页面脏或正在写回: 跳转到unlock标签,跳过这个页面
第24-25行:检查页面映射
c
if (page_mapped(page))
goto unlock;
page_mapped(page)
: 检查页面是否被映射到进程地址空间- 如果页面被映射: 跳转到unlock标签,跳过这个页面
第26行:执行页面无效化
c
ret += invalidate_complete_page(mapping, page);
invalidate_complete_page(mapping, page)
: 实际执行页面无效化- 返回值: 成功无效化返回1,失败返回0
- 累加到ret: 统计成功无效化的页面数量
第27-31行:解锁页面和循环控制
c
unlock:
unlock_page(page);
if (next > end)
break;
}
unlock:
: 标签,用于各种跳过情况的统一处理unlock_page(page)
: 释放页面锁if (next > end)
: 检查是否已超出处理范围,如果是则跳出内层循环
第33-35行:释放页面向量和调度
c
pagevec_release(&pvec);
cond_resched();
}
pagevec_release(&pvec)
: 释放页面向量,减少页面的引用计数cond_resched()
: 条件调度,如果需要则让出CPU,避免长时间占用
第36行:返回结果
c
return ret;
}
- 返回成功无效化的页面总数
详细机制分析
页面向量(Pagevec
)批处理
Pagevec批处理 一次性查找多个页面 减少锁竞争 提高缓存局部性 PAGEVEC_SIZE通常=14 批量操作减少全局锁持有时间 连续页面可能在同一缓存行 平衡内存使用和性能 更好的多核扩展性 减少缓存失效
页面状态检查逻辑
c
// 不能无效化的页面条件:
1. 页面已被锁定: TestSetPageLocked()失败
2. 页面脏: PageDirty(page)为真
3. 正在写回: PageWriteback(page)为真
4. 被进程映射: page_mapped(page)为真
// 可以无效化的页面:
- 未被锁定
- 干净(非脏)
- 不在写回过程中
- 未被进程映射
向内核注册一个内存收缩器set_shrinker
c
struct shrinker *set_shrinker(int seeks, shrinker_t theshrinker)
{
struct shrinker *shrinker;
shrinker = kmalloc(sizeof(*shrinker), GFP_KERNEL);
if (shrinker) {
shrinker->shrinker = theshrinker;
shrinker->seeks = seeks;
shrinker->nr = 0;
down_write(&shrinker_rwsem);
list_add(&shrinker->list, &shrinker_list);
up_write(&shrinker_rwsem);
}
return shrinker;
}
函数功能
set_shrinker
函数用于(shrinker),使得该收缩器能够参与系统的内存回收过程。它是Linux内核内存管理子系统的基础设施函数
代码逐段解释
第1行:函数声明
c
struct shrinker *set_shrinker(int seeks, shrinker_t theshrinker)
- 返回类型 :
struct shrinker *
- 返回注册的收缩器指针,失败返回NULL - 参数 :
int seeks
- 查找代价参数,影响回收优先级shrinker_t theshrinker
- 实际的收缩函数指针
第2-3行:变量声明
c
struct shrinker *shrinker;
struct shrinker *shrinker
: 声明收缩器结构体指针
第5-6行:内存分配
c
shrinker = kmalloc(sizeof(*shrinker), GFP_KERNEL);
if (shrinker) {
kmalloc(sizeof(*shrinker), GFP_KERNEL)
:- 分配收缩器结构体大小的内存
sizeof(*shrinker)
: 使用指针解引用获取正确大小GFP_KERNEL
: 标准内核内存分配标志
if (shrinker)
: 检查内存分配是否成功
第7-9行:初始化收缩器字段
c
shrinker->shrinker = theshrinker;
shrinker->seeks = seeks;
shrinker->nr = 0;
shrinker->shrinker = theshrinker
: 设置收缩函数指针- 这是实际执行内存回收的回调函数
shrinker->seeks = seeks
: 设置查找代价参数- 影响内存回收时的优先级
- 值越小表示回收代价越低,优先级越高
shrinker->nr = 0
: 初始化统计字段为0- 用于记录收缩器回收的对象数量
第10-12行:注册到全局收缩器列表
c
down_write(&shrinker_rwsem);
list_add(&shrinker->list, &shrinker_list);
up_write(&shrinker_rwsem);
-
down_write(&shrinker_rwsem)
: 获取收缩器读写信号量的写锁- 保护全局收缩器列表的并发访问
- 写锁确保在修改列表时没有其他读者或写者
-
list_add(&shrinker->list, &shrinker_list)
:- 将新收缩器添加到全局收缩器链表
&shrinker->list
: 收缩器中的链表节点&shrinker_list
: 全局收缩器链表头
-
up_write(&shrinker_rwsem)
: 释放写锁
第13-14行:返回结果
c
}
return shrinker;
}
- 返回分配和初始化好的收缩器指针
- 如果内存分配失败,返回NULL