【嵌入式Linux学习】GFP_NOIO / GFP_NOFS (Linux 内核 gfp 分配标志)
GFP全称 Get Free Pages (获取空闲页),gfp_mask是内核内存分配时传入的标志位,用来控制内存分配器的行为。 很容易混淆GFP_NOFS、GFP_NOIO,最大误区是以为它们是原子分配不能睡眠,实际上二者都允许睡眠阻塞 ,核心作用是限制内存回收阶段可以执行哪些 IO,解决内核子系统内部的递归死锁问题。
文章目录
- [【嵌入式Linux学习】GFP_NOIO / GFP_NOFS (Linux 内核 gfp 分配标志)](#【嵌入式Linux学习】GFP_NOIO / GFP_NOFS (Linux 内核 gfp 分配标志))
-
- 标志基础对比表
- 为什么会诞生这两个标志?根源:递归死锁
-
- 场景一:GFP_NOFS,文件系统内部死锁案例
- [场景二:GFP_NOIO,底层块 IO 层死锁案例](#场景二:GFP_NOIO,底层块 IO 层死锁案例)
- 通俗生活化类比
- 注意的坑
- 总结
标志基础对比表
| 标志 | 是否可睡眠 | 内存回收限制 | 典型使用场景 |
|---|---|---|---|
| GFP_KERNEL | ✅ 可以睡眠 | 无限制,可以执行 FS IO、块设备 IO | 普通内核代码,没有持有文件系统 / 块设备锁 |
| GFP_NOFS | ✅ 可以睡眠 | 禁止文件系统 IO,允许普通裸块 IO | 文件系统内部,已经持有文件系统锁时 |
| GFP_NOIO | ✅ 可以睡眠 | 全部磁盘 IO 都禁止(块 IO、文件系统 IO 都不行) | 底层块驱动、块 IO 子系统内部 |
| GFP_ATOMIC | ❌ 不可睡眠 | 尽量不做内存回收,不能发起 IO | 中断上下文、持有自旋锁的原子上下文 |
⚠️ 重要提醒:
GFP_NOFS、GFP_NOIO≠GFP_ATOMIC这两个标志是可以睡眠的!只有GFP_ATOMIC才代表原子上下文,不允许睡眠。
简单释义
- GFP_NOFS(No FileSystem) 内存回收的时候,不许进入文件系统代码路径,不能做文件元数据、pagecache 回写这类文件系统操作;但是底层裸块设备 IO 仍然允许执行。
- GFP_NOIO(No IO) 限制更强。内存回收阶段禁止任何磁盘 IO,不管是文件系统 IO 还是底层块 IO 全部不允许。
为什么会诞生这两个标志?根源:递归死锁
死锁发生的核心条件:
当前代码已经持有某子系统的一把非递归锁;调用内存分配,系统内存紧张触发内存回收;内存回收逻辑又重新跑进同一个子系统,再次尝试获取同一把锁,锁拿不到,整个线程卡死。
注意:限制的仅仅是内存分配器自动触发的回收 IO。我们代码自己主动发起的 IO 操作不受标志影响。
场景一:GFP_NOFS,文件系统内部死锁案例
假设文件系统有一把保护元数据的锁 fs_lock,这把锁不是递归锁。
❌ 错误:文件系统内部使用 GFP_KERNEL
1. 文件系统代码拿到 fs_lock(锁被本线程占有)
2. 业务需要内存,调用 kmalloc(buf_size, GFP_KERNEL)
3. 系统内存不足,分配器触发内存回收
4. GFP_KERNEL没有限制,回收逻辑选择回收文件pagecache脏页
5. 回写脏页需要进入文件系统代码,尝试再次获取 fs_lock
6. fs_lock已经被自己占有,无法再次获取
👉 线程死锁!
✅ 修复:使用 GFP_NOFS
1. 文件系统代码拿到 fs_lock
2. kmalloc(buf_size, GFP_NOFS)
3. 内存不足,启动内存回收
4. 因为带GFP_NOFS标志:回收器直接跳过所有文件系统相关回收逻辑,不会进入FS代码
5. 尝试其他回收手段,例如回收slab对象等,不触碰文件系统锁
6. 回收成功就返回内存;内存实在枯竭,返回NULL交给调用者处理
👉 切断递归路径,避免死锁
关键点:
GFP_NOFS只是不让内存回收跑文件系统代码。文件系统代码自己主动调用写盘 IO 完全不受影响。
场景二:GFP_NOIO,底层块 IO 层死锁案例
块 IO 子系统有保护请求队列的锁 block_lock。
❌ 错误:块层内部使用 GFP_KERNEL
1. 块驱动代码拿到 block_lock
2. 需要分配内存,调用 kmalloc(buf_size, GFP_KERNEL)
3. 内存不够,分配器执行内存回收
4. GFP_KERNEL允许磁盘IO,回收触发块IO读写磁盘
5. 重新进入块层代码,想要获取 block_lock
6. 锁已经被当前线程持有
👉 死锁!
✅ 修复:使用 GFP_NOIO
1. 块驱动代码拿到 block_lock
2. kmalloc(buf_size, GFP_NOIO)
3. 内存不足尝试回收内存
4. GFP_NOIO禁止内存回收做任何磁盘IO,不会触发块IO路径
5. 使用不涉及磁盘IO的回收途径
👉 不会再次争抢block_lock,规避死锁
📌 区分:为什么块层不能用 GFP_NOFS?
GFP_NOFS仅仅禁止文件系统 IO,裸块设备 IO 依旧放行 。内存回收依然可以触发块 IO,依旧会死锁。所以块底层需要限制更强的GFP_NOIO。
通俗生活化类比
把 fs_lock 比作图书馆大门钥匙,同一时刻只能一个人持有;申请内存等价于索要一张草稿纸。
GFP_KERNEL:纸不够了,允许管理员去图书馆库房找纸。但是库房也需要大门钥匙,钥匙在你手上,管理员拿不到,直接卡死。GFP_NOFS:告诉管理员,找纸的时候不许进图书馆。管理员只能去别的仓库寻找纸张,不会再来抢你手里的钥匙,就不会卡死。
你 = 文件系统代码;管理员 = 内核内存回收代码。
注意的坑
- 标志不等于分配保证成功 带上
GFP_NOFS/GFP_NOIO只能规避死锁,不能保证内存一定分配成功。当可用内存极度紧张,可回收的资源又被标志限制,kmalloc依然会返回NULL,代码必须做返回值判空与错误处理。 - 普通驱动几乎不要用这两个标志 它们是为文件系统、块 IO 子系统内部设计的。普通外设驱动,没有持有 fs 锁、块锁,直接使用
GFP_KERNEL就可以。乱用这两个标志会人为限制内存回收策略,增大分配失败概率。 - 不要和 GFP_ATOMIC 混淆
GFP_NOFS、GFP_NOIO允许睡眠,不能在自旋锁、中断上下文使用;中断 / 自旋锁上下文内存分配请使用GFP_ATOMIC。
总结
GFP_NOFS 和 GFP_NOIO 都是允许睡眠的内存分配标志,用于解决内存回收带来的递归死锁。
GFP_NOFS:内存回收禁止执行文件系统 IO,但允许裸块 IO,用于文件系统内部持有文件系统锁的场景,防止回收 pagecache 再次进入文件系统抢锁。
GFP_NOIO 限制更强,内存回收禁止全部磁盘 IO,用在底层块 IO 代码,避免回收触发块 IO 递归争抢块锁。 普通驱动开发很少用到,不要和不能睡眠的 GFP_ATOMIC 弄混。