69、内核同步 - 第二部分

内核同步 - 第二部分

在操作全局整数时,若不加以保护,可能会出现并发访问的问题,这属于临界区,需要进行同步保护。最初,我们使用互斥锁来保护临界区,后来发现使用自旋锁保护非阻塞临界区在性能上更优。不过,内核还提供了一类专门用于原子操作整数的运算符,即引用计数(refcount)和原子整数运算符或接口。

1. 新旧接口对比

在 4.10 及之前的内核版本中,使用 atomic_t 接口来原子操作整数。从 4.11 内核开始,引入了更优的 refcount_t API,用于操作内核空间对象的引用计数器。 refcount_t 极大地提升了内核的安全性,能更好地防止整数溢出(IoF)和释放后使用(UAF)的问题,还提供了内存排序保证,而这些是旧的 atomic_t API 所欠缺的。

目前,旧的 atomic_t 接口在内核核心和驱动中仍广泛使用,但正在逐步向 refcount_t 模型和 API 集转换。 refcount_t 接口可视为 atomic_t 接口的一种变体,专门用于引用计数。

两者的关键区别在于:
- atomic_t 操作有符号整数,可用于任何类型的计数。
- refcount_t 仅用于内核对象的引用计数,且有严格的有效范围。

2. refcount_t 详细介绍
  • 有效范围 refcount_t 仅在严格指定的范围内工作,即 1 到 INT_MAX - 1 ([1 .. 2,147,483,646])。若尝试将 refcount_t 变量设置为 0、负数、 INT_MAX 或更高的值,是不可能的。这有助于预防和捕获整数下溢/溢出问题,从而避免许多 UAF 类的错误。
  • 计数器饱和 :计数器达到 REFCOUNT_SATURATED 时会饱和,不再变化,避免计数器回绕导致的 UAF 错误。若引用计数器溢出或下溢,会触发 REFCOUNT_WARN() 警告,该警告在系统生命周期内仅会发出一次。

refcount_t 变量仅用于内核对象的引用计数。对象的引用计数器通常在对象新实例化时初始化为 1,每当代码获取对象引用时增加,释放引用时减少。开发者需谨慎操作引用计数器,确保其值在合法范围内。

3. 简单的 atomic_t refcount_t 接口

atomic_t 接口主要针对 32 位整数,不过也有 64 位的原子整数运算符( atomic64_t atomic_long_t )。 refcount_t 接口则同时支持 32 位和 64 位整数。

访问 atomic_t refcount_t 变量必须通过特定的访问方法或辅助函数。例如,读取 atomic_t 变量的值需使用 atomic_read() 方法,设置值需使用 atomic_set() 方法。

以下是一些常用的 atomic_t (32 位)和 refcount_t 接口的对比:
| 操作 | 旧的(<= 4.10 内核)32 位 atomic_t 接口 | 新的(>= 4.11;32 位和 64 位) refcount_t 接口 |
| ---- | ---- | ---- |
| 包含头文件 | <linux/atomic.h> | <linux/refcount.h> |
| 声明并初始化为 1 | static atomic_t v = ATOMIC_INIT(1); | static refcount_t v = REFCOUNT_INIT(1); |
| 原子读取当前值 | int atomic_read(atomic_t *v) | unsigned int refcount_read(const refcount_t *v) |
| 原子设置值为 i | void atomic_set(atomic_t *v, int i) | void refcount_set(refcount_t *v, int i) |
| 原子加 1 | void atomic_inc(atomic_t *v) | void refcount_inc(refcount_t *v) |
| 原子减 1 | void atomic_dec(atomic_t *v) | void refcount_dec(refcount_t *v) |
| 原子加 i | void atomic_add(int i, atomic_t *v) | void refcount_add(int i, refcount_t *v) |
| 原子减 i | void atomic_sub(int i, atomic_t *v) | void refcount_sub(int i, refcount_t *v) |
| 原子加 i 并返回结果 | int atomic_add_return(int i, atomic_t *v) | bool refcount_add_not_zero(int i, refcount_t *v) (除非为 0,否则加 i) |
| 原子减 i 并返回结果 | int atomic_sub_return(int i, atomic_t *v) | bool refcount_sub_and_test(int i, refcount_t *r) (减 i 并测试,结果为 0 则返回 true) |

4. 内核代码中使用 refcount_t 的示例
  • get_task_struct() put_task_struct() :在创建内核线程时,使用 get_task_struct() 函数标记任务结构正在使用,其内部通过 refcount_inc() 函数增加任务结构的引用计数器。相反, put_task_struct() 函数通过 refcount_dec_and_test() 函数减少引用计数器,若新的引用计数为 0,则释放任务结构。
// include/linux/sched/task.h
static inline struct task_struct *get_task_struct(struct task_struct *t) 
{
    refcount_inc(&t->usage);
    return t;
}

static inline void put_task_struct(struct task_struct *t) 
{
    if (refcount_dec_and_test(&t->usage))
        __put_task_struct(t);
}
  • kernel/user.c 中的使用 :在 kernel/user.c 文件中,也使用了 refcount_t 接口来跟踪用户的进程、文件等数量。通过搜索该文件中包含 refcount 的行,可以看到代码如何对 refcount_t 变量进行初始化、增减和设置操作。
5. 练习

修改之前的 ch12/2_miscdrv_rdwr_spinlock/miscdrv_rdwr_spinlock.c 驱动代码,将 ga gb 改为 refcount 变量,初始化为 42,并使用适当的 refcount_t 方法进行操作。同时,包含一个测试用例,故意使它们超出有效范围([1..INT_MAX - 1])。

操作步骤如下:
1. 将 ga gb 声明为 refcount_t 类型,并初始化为 42。
2. 使用 refcount_t 方法进行操作,如 refcount_inc() refcount_dec() 等。
3. 编写测试用例,故意使 ga gb 超出有效范围。

参考代码如下:

static refcount_t ga = REFCOUNT_INIT(42);
static refcount_t gb = REFCOUNT_INIT(42);

static int open_miscdrv_rdwr(struct inode *inode, struct file *filp)
{
    /* Tweaked the code here to _deliberately_ cause overflow/underflow
     * refcount bugs (resp) if the #if 1 is changed to #if 0 */
#if 1
    refcount_inc(&ga);
#else
    //refcount_add((int i, atomic_t *v); // adds i to v
    // Here we deliberately overflow it, leading to a warning (once)!
    pr_debug("*** Bad case! About to overflow refcount var! ***\n");
    refcount_add(INT_MAX, &ga);
#endif
    // ...
}

若将 #if 1 改为 #if 0 ,会故意使引用计数器溢出,触发内核的一次性警告。

6. 64 位原子整数运算符

随着 64 位系统的普及,内核为 64 位整数提供了一套相同的原子整数运算符。与 32 位的 atomic_t 运算符相比,主要区别如下:
- 声明 64 位原子整数时,使用 atomic64_t (即 atomic_long_t )类型。
- 所有运算符的前缀由 atomic_ 改为 atomic64_

例如:
- ATOMIC_INIT() 改为 ATOMIC64_INIT()
- atomic_read() 改为 atomic64_read()
- atomic_dec_if_positive() 改为 atomic64_dec_if_positive()

需要注意的是,所有对原子整数的操作都必须通过声明为 atomic[64]_t 类型的变量,并使用相应的方法进行,包括初始化和读取操作。

7. 内部实现说明

原子整数运算符 foo() 通常是一个宏,会展开为内联函数,进而调用特定架构的 arch_foo() 函数。例如,x86 架构使用 LOCK 汇编前缀,而 ARM 系列则采用更通用的内存排序语义方法。

现代内核有许多有用的测试基础设施,如 Linux Kernel Dump Test Module(LKDTM),可在 drivers/misc/lkdtm/refcount.c 中找到许多对 refcount 接口的测试用例。

8. 为何使用 refcount API

虽然 refcount_t API 内部是基于 atomic_t API 实现的,但仍有使用它的必要:
- 计数器饱和 :与 atomic_t 运算符不同, refcount_t 的计数器达到 REFCOUNT_SATURATED 时会饱和,避免计数器回绕导致的 UAF 错误,这是一项关键的安全修复。
- 内存排序保证 :一些新的 refcount_t API 提供了内存排序保证,详细信息可参考官方文档。

x86 的 LOCK 前缀不是指令,而是指令的前缀,能保证指令对缓存行的独占访问,禁止硬件中断,并提供内存排序保证。

综上所述, refcount_t 接口在安全性和功能上有明显优势,在进行内核对象引用计数时,应优先考虑使用。同时,在操作原子整数时,要严格遵循相应的接口和方法,确保代码的正确性和稳定性。

内核同步 - 第二部分

9. 内存排序保证与安全优势

refcount_t 接口相较于旧的 atomic_t 接口,在内存排序保证和安全性方面有显著提升。内存排序保证对于多线程或多核环境下的数据一致性至关重要。在复杂的内核操作中,不正确的内存排序可能导致数据竞争和未定义行为。

以下是 refcount_t 接口在这两方面的优势:
- 内存排序保证 :新的 refcount_t API 提供了更明确的内存排序保证,确保在多线程环境下数据的一致性。例如,在某些操作中, refcount_t 接口能保证数据的写入和读取操作按照预期的顺序执行,避免了数据竞争和不一致的问题。详细的内存排序保证信息可参考官方文档。
- 安全优势 refcount_t 接口通过计数器饱和机制,避免了计数器回绕问题,有效防止了释放后使用(UAF)的安全漏洞。当计数器达到 REFCOUNT_SATURATED 值时,它将不再变化,从而避免了由于计数器溢出或下溢导致的 UAF 错误。

10. 不同架构的实现差异

不同的架构在实现原子整数运算符时会有所不同。例如,x86 架构通常使用 LOCK 汇编前缀来保证原子操作的执行。 LOCK 前缀可以确保指令在执行时对缓存行的独占访问,同时禁止硬件中断,提供了内存排序保证。

而 ARM 系列架构则采用更通用的方法,利用内存排序语义来实现原子操作。这种方法不依赖于特定的汇编前缀,而是通过更灵活的内存访问控制来保证原子性。

以下是一个简单的 mermaid 流程图,展示了原子整数运算符的内部实现流程:

graph TD;
    A[用户调用 atomic_foo()] --> B[宏展开为 inline 函数];
    B --> C[调用 arch_foo() 函数];
    C --> D[架构特定实现];
11. 测试与调试

在开发内核模块时,测试和调试是确保代码正确性的重要环节。Linux 内核提供了一些有用的测试基础设施,如 Linux Kernel Dump Test Module(LKDTM)。

  • LKDTM 测试 :可以在 drivers/misc/lkdtm/refcount.c 中找到许多对 refcount 接口的测试用例。通过运行这些测试用例,可以验证 refcount 接口在不同场景下的正确性。
  • 故障注入 :可以使用 LKDTM 结合内核的故障注入框架,测试内核在异常情况下的反应。例如,可以模拟引用计数器溢出或下溢的情况,观察内核的警告信息和处理方式。

操作步骤如下:
1. 找到 drivers/misc/lkdtm/refcount.c 文件,查看其中的测试用例。
2. 根据需要修改测试用例,模拟不同的异常情况。
3. 使用内核的故障注入框架,触发相应的故障,观察内核的反应。

12. 总结与建议

在进行内核编程时,正确使用同步机制和原子操作是确保代码正确性和稳定性的关键。以下是一些总结和建议:
- 优先使用 refcount_t 接口 :对于内核对象的引用计数,优先使用 refcount_t 接口,因为它在安全性和功能上有明显优势。
- 严格遵循接口规范 :在操作原子整数时,要严格遵循相应的接口和方法,确保代码的正确性和稳定性。
- 进行充分的测试 :使用内核提供的测试基础设施,如 LKDTM,进行充分的测试和调试,确保代码在各种情况下都能正常工作。

以下是一个简单的表格,总结了 atomic_t refcount_t 接口的特点:
| 接口类型 | 适用范围 | 安全性 | 内存排序保证 |
| ---- | ---- | ---- | ---- |
| atomic_t | 通用整数计数 | 较低 | 部分有 |
| refcount_t | 内核对象引用计数 | 较高 | 更明确 |

通过合理使用这些接口和方法,可以提高内核代码的质量和可靠性,避免常见的并发访问问题和安全漏洞。在实际开发中,要根据具体的需求和场景选择合适的接口,并严格遵循相关的规范和建议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值