Linux中ro_after_init变量只读性解析
Linux中ro_after_init修饰的变量,是否所有成员都不可修改
在Linux内核中,使用ro_after_init
宏修饰的变量会在内核初始化完成后被标记为只读内存区域。其行为如下:
1. 对变量本身的直接修改
所有直接存储在该变量内存中的成员均不可修改(无论是结构体成员、数组元素,还是基本类型的变量)。例如:
1
2
3
4
5
6
7
8struct example {
int a;
char b;
};
static struct example my_struct __ro_after_init;
// 以下操作在内核初始化完成后会触发页错误(Page Fault):
my_struct.a = 42; // 失败,内存区域已设为只读数组元素同样不可修改:
1
2static int my_array[10] __ro_after_init;
my_array[0] = 100; // 失败,内存区域只读
2. 对指针成员的间接修改
如果变量包含指针成员,则:
指针本身(存储的地址值)不可修改:
1
2
3
4
5
6struct example {
char *ptr;
};
static struct example my_struct __ro_after_init;
my_struct.ptr = new_address; // 失败,指针本身所在内存只读指针指向的内存内容是否可修改,取决于目标内存自身的属性:
1
2
3
4
5
6
7// 假设指针指向动态分配的可写内存:
char *dynamic_str = kmalloc(100, GFP_KERNEL);
strcpy(dynamic_str, "writable");
my_struct.ptr = dynamic_str; // 初始化阶段允许设置指针值
// 初始化完成后:
my_struct.ptr[0] = 'X'; // 允许修改,因为dynamic_str所在内存可写如果指针指向只读内存(如字符串常量),则修改会失败:
1
2my_struct.ptr = "readonly"; // 初始化阶段允许设置指针值
my_struct.ptr[0] = 'X'; // 触发页错误(目标内存只读)
3. 核心机制
__ro_after_init
通过将变量放置在.data..ro_after_init
段,并在kernel_init
阶段调用mark_rodata_ro()
,修改其内存页属性为只读(通过页表标志位)。- 此操作作用于变量自身的内存区域,不影响其指针成员指向的其他内存区域。
总结
场景 | 是否可修改 | 原因 |
---|---|---|
直接修改变量成员(非指针) | 否 | 变量所在内存被标记为只读 |
修改指针成员的值 | 否 | 指针变量本身所在内存只读 |
修改指针指向的内容 | 取决于目标内存属性 | ro_after_init 仅保护指针变量本身,不保护其指向的内存 |
因此,ro_after_init
仅确保变量本身(包括所有直接成员)的不可写性,但与其指针成员指向的外部内存无关。
Linux下如何查看变量在哪个段
在Linux系统中,查看变量所在的段(Section)可以通过以下几种方法实现,具体取决于变量的类型(用户空间程序、内核空间变量或内核模块中的变量)。
一、用户空间程序中的变量
对于用户空间的程序(如ELF可执行文件或动态库),可以通过以下工具查看变量所在的段:
1. 使用 readelf
工具
readelf
可以查看ELF文件的详细段信息。
1 | # 1. 编译程序时添加调试符号(-g) |
示例输出:
1 | Symbol table '.symtab' contains 72 entries: |
其中 Ndx
列表示段索引,对应 readelf -S
输出的段列表(如索引25对应 .data
段)。
2. 使用 objdump
工具
1 | # 查看符号及其段信息 |
输出示例:
1 | 0000000000004010 g O .data 0000000000000004 global_var |
此处 .data
表示变量位于数据段。
二、内核空间中的变量
对于内核或内核模块中的变量,需要通过内核符号表和调试工具分析。
1. 查看内核符号表 /proc/kallsyms
1 | sudo grep "变量名" /proc/kallsyms |
输出示例:
1 | ffffffffa2345678 r __ro_after_init my_var |
- 列含义:
地址 类型 符号名
。 - 类型标识:
r
:只读(.rodata
或.data..ro_after_init
)。d
:数据段(.data
)。b
:BSS段(.bss
)。
2. 结合内核内存布局
通过内核启动日志确认各段的地址范围:
1 | dmesg | grep "Virtual kernel memory layout" |
输出示例:
1 | Virtual kernel memory layout: |
若变量地址在 rodata
段范围内,则说明变量是只读的。
3. 使用 nm
工具分析内核镜像
1 | # 查看内核镜像中的符号信息 |
输出示例:
1 | ffffffffa2345678 r my_var |
符号类型 r
表示只读段。
三、内核模块中的变量
对于内核模块(.ko
文件),可以使用以下方法:
1. 查看模块的段信息
1 | # 使用 modinfo 查看模块信息 |
2. 结合 __ro_after_init
宏
如果变量使用 __ro_after_init
宏修饰,它会被放置在 .data..ro_after_init
段。通过以下命令验证:
1 | readelf -S my_module.ko | grep "ro_after_init" |
四、示例:验证 __ro_after_init
变量
假设内核中有如下变量:
1 | static int my_var __ro_after_init; |
查看符号类型:
1
2sudo grep my_var /proc/kallsyms
# 输出示例:ffffffffa2345678 r my_varr
表示只读属性。
确认段地址范围:
1
2dmesg | grep "rodata"
# 输出示例:rodata : 0xffffffffa2000000 - 0xffffffffa2200000若变量地址
0xffffffffa2345678
在此范围内,则位于只读段。
五、总结
场景 | 工具/方法 | 关键输出 |
---|---|---|
用户空间程序变量 | readelf -s / objdump -t |
符号的段索引(如 .data ) |
内核变量 | /proc/kallsyms + dmesg |
符号地址与段范围的匹配 |
内核模块变量 | objdump -t / readelf -S |
符号所在的段(如 .data..ro_after_init ) |
注意事项:
- 内核符号可能需要
CONFIG_KALLSYMS
配置开启。 __ro_after_init
变量仅在初始化后变为只读,需结合内核版本确认行为。- 用户空间程序需编译时保留调试符号(
-g
)。
Linux hlist_add_tail_rcu的用法,给一个示例
hlist_add_tail_rcu
是 Linux 内核中用于在 哈希链表(hlist) 的尾部添加节点的 RCU(Read-Copy-Update)安全函数。它通过原子操作保证并发访问时的安全性,适用于需要无锁读写的场景。以下是一个完整的使用示例:
1. 定义哈希链表和节点结构
1 |
|
2. 初始化节点并添加到链表尾部
1 | // 初始化节点并添加到哈希链表尾部(RCU安全) |
3. 遍历哈希链表(RCU读侧保护)
1 | // 遍历哈希链表(RCU读保护) |
4. 删除节点(RCU安全)
1 | // 删除指定数据的节点(RCU安全) |
5. 完整示例模块
1 |
|
关键说明
RCU 保护机制:
- 写操作:
hlist_add_tail_rcu
和hlist_del_rcu
确保对链表的修改是原子的。 - 读操作:
hlist_for_each_entry_rcu
必须包裹在rcu_read_lock()
和rcu_read_unlock()
之间。 - 内存释放:删除节点后使用
kfree_rcu
延迟释放内存,确保所有读侧临界区退出后才释放。
- 写操作:
适用场景:
- 高并发场景(如网络协议栈、文件系统)。
- 读操作远多于写操作的情况。
注意事项:
- 内存序:RCU 通过内存屏障保证操作的顺序性。
- 写者同步:如果有多个写者,需额外加锁(如
spinlock
)协调。 - 调试:开启
CONFIG_DEBUG_ATOMIC_SLEEP
检查潜在的 RCU 误用。
典型输出
1 | # dmesg 输出 |
通过这种方式,你可以安全地在多核环境中操作哈希链表。