Linux中ro_after_init修饰的变量,是否所有成员都不可修改

在Linux内核中,使用ro_after_init宏修饰的变量会在内核初始化完成后被标记为只读内存区域。其行为如下:

1. 对变量本身的直接修改

  • 所有直接存储在该变量内存中的成员均不可修改(无论是结构体成员、数组元素,还是基本类型的变量)。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    struct example {
    int a;
    char b;
    };
    static struct example my_struct __ro_after_init;

    // 以下操作在内核初始化完成后会触发页错误(Page Fault):
    my_struct.a = 42; // 失败,内存区域已设为只读
  • 数组元素同样不可修改

    1
    2
    static int my_array[10] __ro_after_init;
    my_array[0] = 100; // 失败,内存区域只读

2. 对指针成员的间接修改

如果变量包含指针成员,则:

  • 指针本身(存储的地址值)不可修改

    1
    2
    3
    4
    5
    6
    struct 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
    2
    my_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
2
3
4
5
6
7
8
# 1. 编译程序时添加调试符号(-g)
gcc -g -o my_program my_program.c

# 2. 使用 readelf 查看符号表
readelf -s my_program | grep "变量名"

# 3. 查看段头信息(确认段的起始地址和属性)
readelf -S my_program

示例输出:

1
2
3
Symbol table '.symtab' contains 72 entries:
Num: Value Size Type Bind Vis Ndx Name
55: 0000000000004010 4 OBJECT GLOBAL DEFAULT 25 global_var

其中 Ndx 列表示段索引,对应 readelf -S 输出的段列表(如索引25对应 .data 段)。


2. 使用 objdump 工具

1
2
# 查看符号及其段信息
objdump -t my_program | grep "变量名"

输出示例:

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
2
3
Virtual kernel memory layout:
rodata : 0xffffffffa2000000 - 0xffffffffa2200000 ( 2 MB)
data : 0xffffffffa2200000 - 0xffffffffa2400000 ( 2 MB)

若变量地址在 rodata 段范围内,则说明变量是只读的。


3. 使用 nm 工具分析内核镜像

1
2
# 查看内核镜像中的符号信息
nm vmlinux | grep "变量名"

输出示例:

1
ffffffffa2345678 r my_var

符号类型 r 表示只读段。


三、内核模块中的变量

对于内核模块(.ko 文件),可以使用以下方法:

1. 查看模块的段信息

1
2
3
4
5
# 使用 modinfo 查看模块信息
modinfo my_module.ko

# 使用 objdump 查看模块的段和符号
objdump -t my_module.ko | grep "变量名"

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. 查看符号类型

    1
    2
    sudo grep my_var /proc/kallsyms
    # 输出示例:ffffffffa2345678 r my_var
    • r 表示只读属性。
  2. 确认段地址范围

    1
    2
    dmesg | grep "rodata"
    # 输出示例:rodata : 0xffffffffa2000000 - 0xffffffffa2200000

    若变量地址 0xffffffffa2345678 在此范围内,则位于只读段。


五、总结

场景 工具/方法 关键输出
用户空间程序变量 readelf -s / objdump -t 符号的段索引(如 .data
内核变量 /proc/kallsyms + dmesg 符号地址与段范围的匹配
内核模块变量 objdump -t / readelf -S 符号所在的段(如 .data..ro_after_init

注意事项

  1. 内核符号可能需要 CONFIG_KALLSYMS 配置开启。
  2. __ro_after_init 变量仅在初始化后变为只读,需结合内核版本确认行为。
  3. 用户空间程序需编译时保留调试符号(-g)。

Linux hlist_add_tail_rcu的用法,给一个示例

hlist_add_tail_rcu 是 Linux 内核中用于在 哈希链表(hlist) 的尾部添加节点的 RCU(Read-Copy-Update)安全函数。它通过原子操作保证并发访问时的安全性,适用于需要无锁读写的场景。以下是一个完整的使用示例:


1. 定义哈希链表和节点结构

1
2
3
4
5
6
7
8
9
10
11
#include <linux/types.h>
#include <linux/rculist.h>

// 定义节点结构体
struct my_node {
int data;
struct hlist_node node; // 哈希链表节点
};

// 定义哈希链表头
static HLIST_HEAD(my_hashlist);

2. 初始化节点并添加到链表尾部

1
2
3
4
5
6
7
8
9
10
11
// 初始化节点并添加到哈希链表尾部(RCU安全)
void add_node_to_tail_rcu(int value) {
struct my_node *new_node = kmalloc(sizeof(struct my_node), GFP_KERNEL);
if (!new_node) {
return; // 错误处理
}
new_node->data = value;

// 使用 RCU 安全的方式添加到链表尾部
hlist_add_tail_rcu(&new_node->node, &my_hashlist);
}

3. 遍历哈希链表(RCU读侧保护)

1
2
3
4
5
6
7
8
9
10
// 遍历哈希链表(RCU读保护)
void print_hashlist(void) {
struct my_node *entry;

rcu_read_lock(); // 进入 RCU 读临界区
hlist_for_each_entry_rcu(entry, &my_hashlist, node) {
pr_info("Data: %d\n", entry->data);
}
rcu_read_unlock(); // 退出 RCU 读临界区
}

4. 删除节点(RCU安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 删除指定数据的节点(RCU安全)
void delete_node_rcu(int value) {
struct my_node *entry;

rcu_read_lock();
hlist_for_each_entry_rcu(entry, &my_hashlist, node) {
if (entry->data == value) {
hlist_del_rcu(&entry->node); // RCU 安全删除
kfree_rcu(entry, rcu); // 延迟释放内存
break;
}
}
rcu_read_unlock();
}

5. 完整示例模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>

// 定义节点和链表头
struct my_node {
int data;
struct hlist_node node;
};
static HLIST_HEAD(my_hashlist);

// 初始化模块
static int __init my_module_init(void) {
add_node_to_tail_rcu(10);
add_node_to_tail_rcu(20);
print_hashlist(); // 输出: Data: 10, Data: 20
return 0;
}

// 清理模块
static void __exit my_module_exit(void) {
delete_node_rcu(10);
delete_node_rcu(20);
}

module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");

关键说明

  1. RCU 保护机制:

    • 写操作hlist_add_tail_rcuhlist_del_rcu 确保对链表的修改是原子的。
    • 读操作hlist_for_each_entry_rcu 必须包裹在 rcu_read_lock()rcu_read_unlock() 之间。
    • 内存释放:删除节点后使用 kfree_rcu 延迟释放内存,确保所有读侧临界区退出后才释放。
  2. 适用场景:

    • 高并发场景(如网络协议栈、文件系统)。
    • 读操作远多于写操作的情况。
  3. 注意事项:

    • 内存序:RCU 通过内存屏障保证操作的顺序性。
    • 写者同步:如果有多个写者,需额外加锁(如 spinlock)协调。
    • 调试:开启 CONFIG_DEBUG_ATOMIC_SLEEP 检查潜在的 RCU 误用。

典型输出

1
2
3
# dmesg 输出
[ 1234.567890] Data: 10
[ 1234.567891] Data: 20

通过这种方式,你可以安全地在多核环境中操作哈希链表。