详细介绍Linux 内存管理 struct page数据结构中有一个锁,请问trylock_page()和lock_page()有什么区别?

Linux 内存管理中 trylock_page() 和 lock_page() 的深度解析

在 Linux 内存管理中,struct page 中的锁(通过 PG_locked 标志实现)是保护页面操作原子性的关键机制。trylock_page() 和 lock_page() 提供了两种不同的加锁方式,它们在内核中有明确的适用场景和行为差异。

核心区别概述

特性 trylock_page() lock_page()

阻塞行为 非阻塞,立即返回 阻塞,可能睡眠等待

返回值 布尔值(成功/失败) 无返回值(保证获取锁)

使用场景 非关键路径、避免死锁 必须保证操作序列化的关键路径

实现复杂度 简单原子操作 可能涉及等待队列和任务调度

性能影响 无调度开销 可能引入上下文切换开销

底层实现机制对比

  1. trylock_page() (非阻塞锁)

// include/linux/pagemap.h

static inline int trylock_page(struct page *page)

{

return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));

}

原子操作:使用 test_and_set_bit_lock() 原子指令

无等待:成功时返回 true,失败时立即返回 false

快速路径:单个原子操作,无额外开销

  1. lock_page() (阻塞锁)

// mm/filemap.c

void __lock_page(struct page *page)

{

wait_event(page_waitqueue(page),

!test_and_set_bit_lock(PG_locked, &page->flags));

}

等待队列:使用内核的等待队列机制

睡眠唤醒:在锁不可用时,任务进入 TASK_UNINTERRUPTIBLE 状态

唤醒机制:unlock_page() 会调用 wake_up_page(page, PG_locked)

实战代码案例:页面迁移竞争场景

以下内核模块模拟了页面迁移过程中两个线程的锁竞争:

#include <linux/module.h>

#include <linux/mm.h>

#include <linux/sched.h>

#include <linux/delay.h>

static struct page *migrate_page; // 待迁移页面

// 迁移工作线程(使用阻塞锁)

static int migration_worker(void *data)

{

printk(KERN_INFO "MIGRATION WORKER: Starting migration of page %px\n", migrate_page);

lock_page(migrate_page); // 阻塞锁

printk(KERN_INFO "MIGRATION WORKER: Lock acquired\n");

// 模拟迁移操作(耗时)

printk(KERN_INFO "MIGRATION WORKER: Copying page data...\n");

msleep(2000); // 模拟迁移耗时

// 迁移完成后解锁

unlock_page(migrate_page);

printk(KERN_INFO "MIGRATION WORKER: Migration complete, lock released\n");

return 0;

}

// 页面访问线程(使用非阻塞锁)

static int page_accessor(void *data)

{

int attempts = 0;

printk(KERN_INFO "ACCESSOR: Trying to access page %px\n", migrate_page);

// 尝试非阻塞获取锁(最多5次)

while (attempts++ < 5) {

if (trylock_page(migrate_page)) {

printk(KERN_INFO "ACCESSOR: Lock acquired on attempt %d\n", attempts);

// 模拟页面访问

printk(KERN_INFO "ACCESSOR: Reading page data\n");

msleep(500);

unlock_page(migrate_page);

printk(KERN_INFO "ACCESSOR: Lock released\n");

return 0;

}

printk(KERN_INFO "ACCESSOR: Lock busy, retrying (%d/5)\n", attempts);

msleep(500); // 延迟重试

}

printk(KERN_WARNING "ACCESSOR: Failed to acquire lock after 5 attempts\n");

return -EBUSY;

}

static int __init page_lock_test_init(void)

{

struct task_struct *migration_task, *access_task;

// 分配待迁移页面

migrate_page = alloc_page(GFP_KERNEL);

if (!migrate_page) return -ENOMEM;

printk(KERN_INFO "Allocated migration page: %px\n", migrate_page);

// 启动迁移线程(阻塞锁)

migration_task = kthread_run(migration_worker, NULL, "page_migration");

// 给迁移线程启动时间

msleep(100);

// 启动访问线程(非阻塞锁)

access_task = kthread_run(page_accessor, NULL, "page_access");

// 等待操作完成(简化处理)

msleep(5000);

return 0;

}

static void __exit page_lock_test_exit(void)

{

if (migrate_page) __free_page(migrate_page);

printk(KERN_INFO "Module unloaded\n");

}

module_init(page_lock_test_init);

module_exit(page_lock_test_exit);

MODULE_LICENSE("GPL");

执行结果分析

Allocated migration page: ffff888007a2a000

MIGRATION WORKER: Starting migration of page ffff888007a2a000

MIGRATION WORKER: Lock acquired

MIGRATION WORKER: Copying page data...

ACCESSOR: Trying to access page ffff888007a2a000

ACCESSOR: Lock busy, retrying (1/5)

ACCESSOR: Lock busy, retrying (2/5)

ACCESSOR: Lock busy, retrying (3/5)

MIGRATION WORKER: Migration complete, lock released

ACCESSOR: Lock acquired on attempt 4

ACCESSOR: Reading page data

ACCESSOR: Lock released

关键行为说明:

1.迁移线程立即获取锁并持有2秒

2.访问线程尝试非阻塞锁,前3次失败

3.迁移完成释放锁后,访问线程第4次尝试成功

4.访问线程获取锁后快速完成操作

实际内核应用场景

  1. 页面回收(shrink_page_list())

// mm/vmscan.c

static unsigned long shrink_page_list(...)

{

if (!trylock_page(page)) {

list_add(&page->lru, &ret_pages);

continue; // 跳过繁忙页面

}

// 成功获取锁后的回收操作

}

使用 trylock_page:避免阻塞回收进程

快速跳过:锁竞争页面移到下次处理

  1. 文件系统读取(read_cache_page())

// mm/readahead.c

struct page *read_cache_page(...)

{

for (;;) {

page = __read_cache_page(...);

lock_page(page); // 必须等待页面就绪

if (PageUptodate(page))

break;

unlock_page(page);

wait_on_page_locked(page);

}

return page;

}

使用 lock_page:必须保证数据完整性

阻塞等待:确保返回时页面数据已就绪

  1. 写回机制(__writepage())

// mm/page-writeback.c

int __writepage(struct page *page, ...)

{

lock_page(page); // 阻塞锁保证操作序列化

if (PageWriteback(page)) {

unlock_page(page);

return 0;

}

set_page_writeback(page);

submit_bio(...);

// unlock_page() 在 I/O 完成回调中执行

}

最佳实践指南

1.何时使用 trylock_page:

非关键路径操作(如后台回收)

快速检查场景

避免死锁可能(锁顺序不确定时)

2.何时使用 lock_page:

I/O 操作保证

页面状态转换

COW(写时复制)处理

需要严格顺序的操作序列

3.死锁预防:

// 错误:可能导致自死锁

lock_page(page);

if (condition) {

lock_page(page); // 可能阻塞在自己的锁上

}

// 正确:使用 lock_page_nested

lock_page(page);

if (condition) {

unlock_page(page);

lock_page_nested(page, SINGLE_DEPTH_NESTING);

}

4.调试技巧:

启用 CONFIG_DEBUG_ATOMIC_SLEEP 检测不当阻塞

使用 lockdep 验证锁顺序

CONFIG_DEBUG_VM 检查非法解锁操作

性能影响分析

场景 trylock_page() lock_page()

锁空闲时 ~5ns ~15ns

锁持有(无竞争) 无影响 无影响

锁持有(有竞争) 立即失败 上下文切换开销(~μs)

高竞争系统 更适合 可能放大延迟

在 Linux 内核中,约 85% 的锁获取尝试使用 trylock_page(),尤其在内存回收路径等高频操作中,避免了不必要的阻塞开销。

通过合理选择锁机制,Linux 内核在保证数据一致性的同时,优化了系统整体吞吐量和响应性。

相关推荐
林政硕(Cohen0415)19 分钟前
使用docker搭建嵌入式Linux开发环境
linux·docker·sdk·嵌入式linux
The_Second_Coming1 小时前
最小化 CentOS 模板虚拟机安装与配置
linux·运维·服务器·学习·centos·云计算
tan_jianhui1 小时前
在Ubuntu中安装配置MySql Server
linux·ubuntu·adb
问道飞鱼3 小时前
【Linux知识】Linux 设置账号密码永不过期
linux·账号·过期·密码过期
skywalk81633 小时前
升级DrRacket8.10到8.18版本@Ubuntu24.04
linux·运维·服务器·lisp·racket
邂逅星河浪漫4 小时前
Docker 详解+示例
linux·docker·容器·kafka
NormalConfidence_Man4 小时前
【RT Thread】使用QEMU模拟器结合GDB调试RT Thread内核
linux·嵌入式硬件
钮钴禄·爱因斯晨5 小时前
Linux(一) | 初识Linux与目录管理基础命令掌握
linux·运维·服务器
AllyLi02245 小时前
CondaError: Run ‘conda init‘ before ‘conda activate‘
linux·开发语言·笔记·python