内核同步 - 第二部分
在操作全局整数时,若不加以保护,可能会出现并发访问的问题,这属于临界区,需要进行同步保护。最初,我们使用互斥锁来保护临界区,后来发现使用自旋锁保护非阻塞临界区在性能上更优。不过,内核还提供了一类专门用于原子操作整数的运算符,即引用计数(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
| 内核对象引用计数 | 较高 | 更明确 |
通过合理使用这些接口和方法,可以提高内核代码的质量和可靠性,避免常见的并发访问问题和安全漏洞。在实际开发中,要根据具体的需求和场景选择合适的接口,并严格遵循相关的规范和建议。
超级会员免费看
6196

被折叠的 条评论
为什么被折叠?



