十一、Linux中RCU的实现以及内核抢占控制函数
文章目录
- 一、RCU
- 1. RCU 是什么?
- 2.RCU怎么实现的?
- 3.有什么注意事项?
- 4.关键函数的实现
- 4.1.rcu_assign_pointer
- 4.1.2.`smp_wmb();`
- 4.1.3.`(p) = (v);`
- 4.2.rcu_dereference
- 4.3.`rcu_read_lock`和`rcu_read_unlock`
- 5.`struct rcu_head` 结构体
- 6.简单的使用RCU的例子
- 6.1.模块源代码
- 6.2.`Makefile`文件
- 6.3.加载和卸载模块以及验证
- 二、preempt_disable和preempt_enable的实现
- 1. `asmlinkage` 和 `CPP_ASMLINKAGE` 的定义
- 2. `preempt_schedule()` 的实现(`kernel/sched.c`)
- 2.1.`barrier()` 的作用
- 2.1.1.不加 `barrier()` 会怎么样?
- 2.1.2.`barrier()` 如何解决这个问题?
- 2.1.3.这里为什么不需要CPU内存屏障?
- 3. `preempt.h` 中的抢占控制宏
- **4. `preempt_disable()` 和 `preempt_enable()` 的实现**
- 4.1.`preempt_disable()`
- 4.2. `preempt_enable()`
- 5.当前版本的缺陷
一、RCU
1. RCU 是什么?
RCU (read-copy-update
)的核心思想非常巧妙:通过写时复制(Copy-on-Write)和延迟释放(Grace Period)来实现近乎无锁的读取
可以用一个比喻来理解:
图书馆的图书目录卡
- 共享数据:图书馆的中央图书目录
- 读者:查找图书的读者
- 写者:更新目录的图书管理员
没有RCU(传统锁机制)
管理员要更新目录时,必须把整个目录柜台锁起来(写锁),期间所有读者都不能查阅。更新完成后才解锁。读者多的时候,管理员几乎没机会更新
使用RCU
-
复制(Copy):管理员不直接修改中央目录。他复制一份目录卡副本
-
更新(Update):他在这个副本上进行修改(如添加新书条目)
-
替换(Publish):当副本修改完成后,他原子性地将新的目录卡替换掉旧的目录卡(例如,修改一个指针指向新目录)。这个操作非常快
-
延迟释放(Reclaim):旧的目录卡不会被立即扔掉。管理员会等一等,直到他确信所有在替换前就已经开始的读者都已经离开。之后,他再安全地销毁旧的目录卡
2.RCU怎么实现的?
RCU 的实现围绕以下几点
针对写者(更新者)
-
复制:
kmalloc
分配新内存,将旧数据拷贝过来 -
更新: 修改新副本
-
发布: 使用一个原子性的指针赋值操作(如
rcu_assign_pointer
)将新指针发布出去。这个操作包含了必要的内存屏障(如smp_wmb
),确保新指针指向的数据初始化完成后再让读者看到
针对读者
-
订阅: 使用
rcu_dereference()
来安全地获取指针。它确保在弱内存序的CPU上,读者能读到正确的指针值,并防止编译器进行有害的优化 -
访问: 在 RCU 读侧临界区 内访问数据。这个临界区由
rcu_read_lock()
和rcu_read_unlock()
界定 -
它不阻塞,只是禁用内核抢占
宽限期(Grace Period)与旧数据回收
这是RCU最精妙的部分
-
宽限期(Grace Period):指的是一段时间窗口,它开始于一个更新操作(如指针替换)之后,并持续到所有在该更新操作之前就已经开始的RCU读临界区都结束为止。
-
简单说:宽限期就是确保所有可能看到旧数据的读者都“离开”所需要等待的时间
-
如何等待宽限期结束?
- 写者调用
synchronize_rcu()
或call_rcu()
synchronize_rcu()
:同步接口,会阻塞直到当前宽限期结束。之后就可以安全地kfree
旧数据了call_rcu(callback_func)
:异步接口,注册一个回调函数。内核会在宽限期结束后自动调用这个回调函数,在回调函数中释放旧数据。这是更常用的方式,因为它不会阻塞写者
- 写者调用
宽限期的实现原理(简化)
- 内核会周期性地进行上下文切换(如时钟中断)。当它发现每个CPU都至少经历过一次上下文切换时,就可以推断:所有在指针替换之前就在运行的线程(可能持有旧数据的读者)肯定都已经切换出去了,它们的读临界区必然已经结束。此时,宽限期就结束了
3.有什么注意事项?
-
读侧临界区不能休眠(Cannot Sleep in Read-Side Critical Section)
- 因为休眠可能会导致宽限期的计算出现错误,即便进行了上下文切换,读临界区仍然有可能没有结束,所以
rcu_read_lock
后不能调用schedule()
、kmalloc(GFP_KERNEL)
、等可能引发休眠的函数
- 因为休眠可能会导致宽限期的计算出现错误,即便进行了上下文切换,读临界区仍然有可能没有结束,所以
-
写者更新开销相对较大
- 需要复制数据,并且等待宽限期,延迟了内存回收。因此RCU适用于读多写少的场景。如果写操作非常频繁,RCU可能不是最佳选择
-
保证数据版本的稳定性
- 读者在读侧临界区内获取的指针所指向的数据是稳定的,在临界区结束前不会被释放。但一旦出了临界区,数据可能随时被释放,所以不能再引用
-
复杂的规则
- 有不同种类的RCU,如可睡眠的SRCU,它是通过维护计数器来确定读临界区是否结束的,所以可以在读临界区调用可睡眠函数
4.关键函数的实现
4.1.rcu_assign_pointer
/* 源码文件 include/linux/rcupdate.h */
#define rcu_assign_pointer(p, v) ({ \smp_wmb(); \(p) = (v); \})
定义了一个名为 rcu_assign_pointer
的宏,它接受两个参数:p
(指针)和 v
(要赋予的值)
4.1.2.smp_wmb();
smp_wmb();
-
作用:在屏障之前的所有写操作,必须先于在屏障之后的所有写操作完成,并且对其他CPU核心可见
-
为什么需要它?
在RCU的写者(更新者)一侧,典型的操作流程是:-
准备新数据:分配内存,初始化一个新的数据结构
-
发布新数据:将全局指针指向这个新数据结构
如果没有内存屏障,编译器和CPU可能会对指令进行重排优化。它们可能会认为步骤2(指针赋值)更重要,从而尝试先执行它。这将导致一个极其危险的时间窗口:
错误顺序(没有屏障):
// 1. 指针先指向新数据(但新数据还未初始化完成!) global_ptr = new_data; // 其他读者可能立刻看到这个指针// 2. 然后才初始化新数据 new_data->field1 = 1; new_data->field2 = 2;
如果在指针赋值之后、数据初始化完成之前,另一个CPU上的读者线程通过
global_ptr
读到了新数据,它将会看到一个半初始化的、状态不一致的数据结构,导致程序错误或崩溃正确顺序(有屏障):
// 1. 初始化新数据(所有写操作) new_data->field1 = 1; new_data->field2 = 2;// 2. 写内存屏障:确保上面的写操作全部完成 smp_wmb();// 3. 发布指针(写操作) global_ptr = new_data;
smp_wmb()
就像一堵墙,严格保证了初始化的写操作必须全部完成之后,才能执行发布指针的写操作这里需要确保多CPU之间内存访问顺序,所以必须使用
smp_wmb()
,不能使用barrier()
,barrier()
使用场景看preempt_schedule()
的实现 一节 -
4.1.3.(p) = (v);
(p) = (v);
- 作用:这是一个简单的指针赋值操作。将值
v
赋给指针p
- 为什么需要括号?
(p)
和(v)
两边的括号是宏定义的良好实践。它可以确保如果传入的参数是复杂的表达式,不会因为运算符优先级问题导致意外的解析错误
4.2.rcu_dereference
/* 源码文件 include/linux/rcupdate.h */
#define rcu_dereference(p) ({ \typeof(p) _________p1 = p; \smp_read_barrier_depends(); \(_________p1); \})
第1行:typeof(p) _________p1 = p;
typeof(p)
:获取参数p
的类型_________p1
:创建一个唯一的局部变量名(多个下划线避免命名冲突)- 将指针
p
的值赋给局部变量_________p1
第2行:smp_read_barrier_depends();
- 插入一个数据依赖内存屏障(memory barrier)
- 确保在解引用指针之前,所有依赖的读操作都已完成
- 一般为空操作,大部分CPU默认会确保数据依赖的内存访问顺序,可参考博客 https://blog.csdn.net/weixin_51019352/article/details/151968561 **
smp_read_barrier_depends()
**一节
第3行:(_________p1);
- 返回局部变量
_________p1
的值 - 括号确保这是一个表达式而不是语句
宏结构:({ ... })
- GCC 的语句表达式扩展,允许在表达式中包含多条语句,最后一条语句的值作为整个表达式的结果
这个宏的主要作用是:
-
防止编译器优化:通过创建局部变量,阻止编译器进行不安全的优化
-
RCU安全读取:在RCU读侧临界区内安全地访问可能被并发修改的指针
-
新版本中该宏添加了指针必须从内存加载以及其他一系列检查
4.3.rcu_read_lock
和rcu_read_unlock
/* 源码文件 include/linux/rcupdate.h */
#define rcu_read_lock() preempt_disable()#define rcu_read_unlock() preempt_enable()
参考后面小节 preempt_disable和preempt_enable的实现
5.struct rcu_head
结构体
struct rcu_head {struct rcu_head *next;void (*func)(struct rcu_head *head);
};#define RCU_HEAD_INIT(head) { .next = NULL, .func = NULL }
#define RCU_HEAD(head) struct rcu_head head = RCU_HEAD_INIT(head)
#define INIT_RCU_HEAD(ptr) do { \(ptr)->next = NULL; (ptr)->func = NULL; \
} while (0)
struct rcu_head
结构体
struct rcu_head {struct rcu_head *next; // 指向下一个RCU回调的指针void (*func)(struct rcu_head *head); // 回调函数指针
};
next
:用于将多个 RCU 回调连接成链表func
:回调函数,当 Grace Period 结束后被调用,用于释放内存或执行清理操作
#define RCU_HEAD_INIT(head) { .next = NULL, .func = NULL }
这是一个初始化器宏
- 将
next
指针初始化为NULL
- 将
func
回调函数指针初始化为NULL
#define RCU_HEAD(head) struct rcu_head head = RCU_HEAD_INIT(head)
这是一个声明并初始化宏
- 声明一个
struct rcu_head
变量并立即初始化
#define INIT_RCU_HEAD(ptr) do { \(ptr)->next = NULL; (ptr)->func = NULL; \
} while (0)
这是一个运行时初始化宏
(ptr)->next = NULL
:将next指针设为NULL(ptr)->func = NULL
:将回调函数设为NULL- 用于对已存在的
rcu_head
结构体进行初始化
6.简单的使用RCU的例子
6.1.模块源代码
创建一个名为rcu_example.c
的文件,并复制下面代码,注意粘贴使用粘贴模式,即:set paste
#include <linux/module.h>
#include <linux/init.h>#include <linux/kernel.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>
#include <linux/kthread.h>#include <linux/delay.h>MODULE_LICENSE("GPL");// 示例数据结构
struct data_node {int value;char name[32];struct rcu_head rcu;
};// 全局 RCU 保护的指针
static struct data_node *global_data = NULL;
static struct task_struct *reader1, *reader2, *writer;struct data {int id;int exited;
};
static struct data reader1_data = {1, 0};
static struct data reader2_data = {2, 0};
static struct data writer_data = {3, 0};// 读线程函数 - 读取数据
static int reader_thread(void *data)
{struct data* id_exited = (struct data *)data;int thread_id = id_exited->id;int read_count = 0;while (!kthread_should_stop() && read_count < 10) {struct data_node *local_ptr;// RCU 读侧临界区开始rcu_read_lock();// 安全解引用 RCU 保护的指针local_ptr = rcu_dereference(global_data);if (local_ptr) {printk(KERN_INFO "Reader %d: value=%d, name=%s\n", thread_id, local_ptr->value, local_ptr->name);} else {printk(KERN_INFO "Reader %d: data is NULL\n", thread_id);}// RCU 读侧临界区结束rcu_read_unlock();msleep(100); // 模拟处理时间read_count++;}id_exited->exited = 1;return 0;
}// 修正的清理函数
static void free_data(struct rcu_head * head)
{struct data_node *old_node = container_of(head, struct data_node, rcu);kfree(old_node);printk(KERN_INFO "free_data: old data free!\n");
}// 写线程函数 - 更新数据
static int writer_thread(void *data)
{struct data* id_exited = (struct data *)data;int update_count = 0;struct data_node *new_node, *old_node;while (!kthread_should_stop() && update_count < 3) {// 分配新节点new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);if (!new_node) {printk(KERN_ERR "Failed to allocate memory\n");return -ENOMEM;}// 初始化新数据snprintf(new_node->name, sizeof(new_node->name), "Node_%d", update_count);new_node->value = update_count * 100;// 获取旧指针old_node = rcu_dereference(global_data);// 发布新数据(包含内存屏障)rcu_assign_pointer(global_data, new_node);printk(KERN_INFO "Writer: updated data to value=%d\n", new_node->value);// 如果存在旧数据,延迟释放if (old_node) {// 使用 call_rcu 异步释放旧数据call_rcu(&old_node->rcu, (void (*)(struct rcu_head *))free_data);printk(KERN_INFO "Writer: scheduled free for old data\n");}msleep(500); // 更新间隔update_count++;}id_exited->exited = 1;return 0;
}// 初始化函数
static int __init rcu_example_init(void)
{printk(KERN_INFO "RCU Example Module Loaded\n");// 初始化一些数据global_data = kmalloc(sizeof(*global_data), GFP_KERNEL);if (global_data) {global_data->value = 0;strcpy(global_data->name, "Initial_Node");}// 创建读线程reader1 = kthread_run(reader_thread, &reader1_data, "rcu_reader1");if (IS_ERR(reader1)) {printk(KERN_ERR "Failed to create reader1: %ld\n", PTR_ERR(reader1));reader1 = NULL; // 标记为无效}reader2 = kthread_run(reader_thread, &reader2_data, "rcu_reader2");if (IS_ERR(reader2)) {printk(KERN_ERR "Failed to create reader2: %ld\n", PTR_ERR(reader2));reader2 = NULL; // 标记为无效}// 创建写线程writer = kthread_run(writer_thread, &writer_data, "rcu_writer");if (IS_ERR(writer)) {printk(KERN_ERR "Failed to create writer: %ld\n", PTR_ERR(writer));writer = NULL; // 标记为无效}printk(KERN_INFO "reader1 pid is %d\n", reader1->pid);printk(KERN_INFO "reader2 pid is %d\n", reader2->pid);printk(KERN_INFO "writer pid is %d\n", writer->pid);return 0;
}// 清理函数
static void __exit rcu_example_exit(void)
{struct data_node *old_node;printk(KERN_INFO "RCU Example Module Unloading\n");if (!reader1_data.exited) {kthread_stop(reader1);}if (!reader2_data.exited) {kthread_stop(reader2);}if (!writer_data.exited) {kthread_stop(writer);}// 安全删除数据old_node = rcu_dereference(global_data);rcu_assign_pointer(global_data, NULL);// 如果存在旧数据,延迟释放if (old_node) {// 使用 call_rcu 异步释放旧数据call_rcu(&old_node->rcu, (void (*)(struct rcu_head *))free_data);printk(KERN_INFO "Writer: scheduled free for old data\n");}printk(KERN_INFO "RCU Example Module Unloaded\n");
}module_init(rcu_example_init);
module_exit(rcu_example_exit);
6.2.Makefile
文件
创建一个名为Makefile
的文件,并复制如下内容,注意把Makefile
文件前面的空格替换为tab
ifneq ($(KERNELRELEASE),)obj-m := rcu_example.o
elseKERNELDIR ?= /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)default:$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesendifclean:$(MAKE) -C $(KERNELDIR) M=$(PWD) cleanrm -f *.ko *.mod.c *.mod.o *.o .tmp_versions
6.3.加载和卸载模块以及验证
加载模块
sudo insmod rcu_example.ko
预期输出
RCU Example Module Loaded
Reader 1: value=0, name=Initial_Node
Reader 2: value=0, name=Initial_Node
Writer: updated data to value=0
Writer: scheduled free for old data
reader1 pid is 6934
reader2 pid is 6935
free_data: old data free!
writer pid is 6936
Reader 1: value=0, name=Node_0
Reader 2: value=0, name=Node_0
Reader 1: value=0, name=Node_0
Reader 2: value=0, name=Node_0
Reader 1: value=0, name=Node_0
Reader 2: value=0, name=Node_0
Reader 1: value=0, name=Node_0
Reader 2: value=0, name=Node_0
Writer: updated data to value=100
Writer: scheduled free for old data
free_data: old data free!
Reader 1: value=100, name=Node_1
Reader 2: value=100, name=Node_1
Reader 1: value=100, name=Node_1
Reader 2: value=100, name=Node_1
Reader 1: value=100, name=Node_1
Reader 2: value=100, name=Node_1
Reader 1: value=100, name=Node_1
Reader 2: value=100, name=Node_1
Reader 1: value=100, name=Node_1
Reader 2: value=100, name=Node_1
Writer: updated data to value=200
Writer: scheduled free for old data
free_data: old data free!
RCU Example Module Unloading
Writer: scheduled free for old data
free_data: old data free!
RCU Example Module Unloaded
卸载模块
sudo rmmod rcu_example
二、preempt_disable和preempt_enable的实现
/* 源码文件 include/asm/linkage.h */
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))/* 源码文件 include/linux/linkage.h */
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif/* 源码文件 kernel/sched.c */
asmlinkage void __sched preempt_schedule(void)
{struct thread_info *ti = current_thread_info();/** If there is a non-zero preempt_count or interrupts are disabled,* we do not want to preempt the current task. Just return..*/if (unlikely(ti->preempt_count || irqs_disabled()))return;need_resched:ti->preempt_count = PREEMPT_ACTIVE;schedule();ti->preempt_count = 0;/* we could miss a preemption opportunity between schedule and now */barrier();if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))goto need_resched;
}/* 源码文件 include/linux/preempt.h */
#define preempt_count() (current_thread_info()->preempt_count)#define inc_preempt_count() \
do { \preempt_count()++; \
} while (0)#define dec_preempt_count() \
do { \preempt_count()--; \
} while (0)#ifdef CONFIG_PREEMPTasmlinkage void preempt_schedule(void);#define preempt_disable() \
do { \inc_preempt_count(); \barrier(); \
} while (0)#define preempt_enable_no_resched() \
do { \barrier(); \dec_preempt_count(); \
} while (0)#define preempt_check_resched() \
do { \if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \preempt_schedule(); \
} while (0)#define preempt_enable() \
do { \preempt_enable_no_resched(); \preempt_check_resched(); \
} while (0)
1. asmlinkage
和 CPP_ASMLINKAGE
的定义
/* include/asm/linkage.h */
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))/* include/linux/linkage.h */
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif
asmlinkage
- 强制函数通过栈传递参数(
regparm(0)
) - 在 C++ 环境下,
CPP_ASMLINKAGE
确保函数使用 C 链接(extern "C"
) - 主要用于系统调用和中断处理,确保参数从用户态正确传递到内核态
- 强制函数通过栈传递参数(
2. preempt_schedule()
的实现(kernel/sched.c
)
asmlinkage void __sched preempt_schedule(void)
{struct thread_info *ti = current_thread_info();/** If there is a non-zero preempt_count or interrupts are disabled,* we do not want to preempt the current task. Just return..*/if (unlikely(ti->preempt_count || irqs_disabled()))return;need_resched:ti->preempt_count = PREEMPT_ACTIVE;schedule();ti->preempt_count = 0;/* we could miss a preemption opportunity between schedule and now */barrier();if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))goto need_resched;
}
- 功能:执行内核抢占,切换到更高优先级的任务。
关键逻辑
-
获取当前线程信息
struct thread_info *ti = current_thread_info();
current_thread_info()
返回当前进程的thread_info
结构体(通常存储在内核栈底部),具体实现可以参考博客 https://blog.csdn.net/weixin_51019352/article/details/151835068 模块怎么访问当前进程的 一节
-
检查是否允许抢占
if (unlikely(ti->preempt_count || irqs_disabled()))return;
preempt_count
:如果非零,表示当前正处在不可抢占的临界区内,抢占被禁用irqs_disabled()
:如果中断被禁用,意味着正处于一个关键区域,不适合进行调度unlikely()
提示编译器,条件为假的可能性更大
-
执行抢占
ti->preempt_count = PREEMPT_ACTIVE; schedule(); ti->preempt_count = 0;
-
PREEMPT_ACTIVE
标记抢占正在进行 -
schedule()
调用调度器,选择新任务并切换上下文,当前任务在此处主动放弃CPU -
恢复
preempt_count
为0
,当调度器再次选择运行这个任务时,代码从这里继续执行。首先将preempt_count
清零,表示抢占过程已经结束,任务回到了可正常抢占的状态 -
处理可能的遗漏抢占
barrier(); if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))goto need_resched;
-
barrier()
即__asm__ __volatile__("": : :"memory")
确保编译器不要为了优化而将barrier()
之前的内存访问指令和之后的内存访问指令进行重排 -
如果
TIF_NEED_RESCHED
标志仍被设置,则重新执行抢占
-
2.1.barrier()
的作用
2.1.1.不加 barrier()
会怎么样?
想象一下以下时序,假设没有 barrier()
-
CPU执行流
- 任务A执行
schedule()
,被切换出去 - 一段时间后,调度器再次选择任务A运行
- 任务A从
schedule()
返回,执行ti->preempt_count = 0;
- (关键点) 在CPU执行下一条指令(即检查
TIF_NEED_RESCHED
标志)之前,发生了一个中断
- 任务A执行
-
中断处理程序
- 中断到来,CPU转去执行中断处理程序
- 在中断处理程序的末尾,会进行抢占检查。它发现任务A的
preempt_count
现在是 0(因为刚刚被清零了),并且任务A的优先级可能不够高,或者有更高优先级任务在等待 - 因此,中断处理程序设置了任务A的
TIF_NEED_RESCHED
标志,告诉它“你应该被抢占了”
-
CPU执行流恢复
- 中断返回,任务A的上下文恢复
- 任务A继续执行,检查
TIF_NEED_RESCHED
标志:if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
- 问题来了! 由于编译器为了性能会指令重排,导致CPU有可能在
ti->preempt_count = 0;
执行之后,还没有从内存中加载TIF_NEED_RESCHED
标志的值,就提前对if
条件进行求值 - 它读取到的可能是一个旧的值(即中断设置标志之前的值)。于是,条件判断失败,任务A错过了这次抢占机会,
goto need_resched
不会发生
后果:一个本应被立即抢占的任务继续运行了下去。这会导致调度延迟增加,降低系统的交互性和实时性
2.1.2.barrier()
如何解决这个问题?
barrier()
是一个编译器屏障(Compiler Barrier),在Linux内核中通常定义为 asm volatile("" ::: "memory")
它的作用是:
-
禁止编译器重排:告诉编译器,不要为了优化而将
barrier()
之前的内存访问指令和之后的内存访问指令进行重排。编译器必须保证,在生成的目标代码中,barrier()
之前的所有对内存的写操作(ti->preempt_count = 0;
)都完成后,才能执行之后的内存读操作(test_thread_flag(TIF_NEED_RESCHED)
) -
保证内存访问的时效性:
"memory"
这个破坏子(clobber)告诉编译器,这段代码会以未知方式修改内存。这会强制编译器在barrier()
之后重新从内存中加载那些可能被其他异步事件(如中断)修改的变量值,而不是使用寄存器中可能过时的缓存值
2.1.3.这里为什么不需要CPU内存屏障?
原因在于这个竞争条件的本质:它可以看做是一个单CPU上的竞争,而不是多CPU之间的竞争
在这个场景中,CPU的执行顺序是严格保证的,写preempt_count(0) → 中断处理程序读preempt_count(看到0) → 中断处理程序写TIF标志 → 任务A读TIF标志(看到设置后的值)
那么,为什么还需要 barrier()
?
因为编译器在优化时,不知道中断这种异步事件的存在
编译器可能会进行以下错误但符合C语言规则的优化
- 指令重排:编译器可能为了效率,将条件判断
test_thread_flag(...)
的指令调度到ti->preempt_count = 0;
之前去执行 - 寄存器缓存:编译器可能将
TIF_NEED_RESCHED
标志的值提前读入某个寄存器,然后在if语句中直接使用这个寄存器的值,而不是每次都从内存重新读取
这两种优化都会导致问题:任务A检查的是旧的 TIF_NEED_RESCHED
值,而错过了中断处理程序设置的新值
因此,使用CPU内存屏障是过度杀伤的。它会导致不必要的性能损耗
3. preempt.h
中的抢占控制宏
/* include/linux/preempt.h */
#define preempt_count() (current_thread_info()->preempt_count)#define inc_preempt_count() \
do { \preempt_count()++; \
} while (0)#define dec_preempt_count() \
do { \preempt_count()--; \
} while (0)
-
功能
preempt_count()
:返回当前thread_info->preempt_count
(抢占计数器)
-
inc_preempt_count()
:增加计数器(禁用抢占)dec_preempt_count()
:减少计数器(启用抢占)
4. preempt_disable()
和 preempt_enable()
的实现
#ifdef CONFIG_PREEMPTasmlinkage void preempt_schedule(void);#define preempt_disable() \
do { \inc_preempt_count(); \barrier(); \
} while (0)#define preempt_enable_no_resched() \
do { \barrier(); \dec_preempt_count(); \
} while (0)#define preempt_check_resched() \
do { \if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \preempt_schedule(); \
} while (0)#define preempt_enable() \
do { \preempt_enable_no_resched(); \preempt_check_resched(); \
} while (0)
#endif
4.1.preempt_disable()
#define preempt_disable() \
do { \inc_preempt_count(); \barrier(); \
} while (0)
-
作用:禁用内核抢占
-
步骤
-
增加
preempt_count
-
inc_preempt_count()
使preempt_count++
,表示抢占被禁用 -
barrier()
确保inc_preempt_count()
不会被编译器交换到后面执行,从而保证inc_preempt_count()
的后续指令都是在内核禁用抢占的情况下执行的
-
4.2. preempt_enable()
#define preempt_enable() \
do { \preempt_enable_no_resched(); \preempt_check_resched(); \
} while (0)
-
作用:启用内核抢占,并检查是否需要调度
-
步骤
- 启用抢占
#define preempt_enable_no_resched() \ do { \barrier(); \dec_preempt_count(); \ } while (0)
-
barrier()
避免编译器把dec_preempt_count()
交换到前面去执行,从而保证dec_preempt_count()
之前的指令都是在内核禁用抢占的情况下执行的 -
dec_preempt_count()
使preempt_count--
,表示抢占可重新启用 -
检查是否需要调度
#define preempt_check_resched() \ do { \if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \preempt_schedule(); \ } while (0)
- 如果
TIF_NEED_RESCHED
标志被设置(有更高优先级任务待运行),则调用preempt_schedule()
触发抢占。
5.当前版本的缺陷
如果读者细心阅读了第2小节preempt_schedule()的实现
,那么在理解preempt_enable
宏定义时肯定会有一个疑惑,这里我们先把preempt_enable
宏定义展开
barrier();
dec_preempt_count();
if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))preempt_schedule();
我们会发现barrier()
是放在第一行,而它只能确保它之前的内存访问和它之后的内存访问不会交换,所以dec_preempt_count()
和test_thread_flag()
之间还是有可能被编译器交换顺序的,那么显而易见,会有和preempt_schedule()
函数一样的问题
不过,这个确实是Linux 2.6.10版本的实现,不可否认,这个是明显的缺陷,在后面版本已经解决了这个问题,解决方式如下:
#define preempt_enable() \
do { \preempt_enable_no_resched(); \barrier(); \preempt_check_resched(); \
} while (0)
相信读者到这里就不用纠结了,你的理解没有错!