当前位置: 首页 > news >正文

十一、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_lockrcu_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. asmlinkageCPP_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_count0,当调度器再次选择运行这个任务时,代码从这里继续执行。首先将 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 标志)之前,发生了一个中断
  • 中断处理程序

    • 中断到来,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)

相信读者到这里就不用纠结了,你的理解没有错!

http://www.dtcms.com/a/400438.html

相关文章:

  • 政务网站建设目的_意义做营销怎样才能吸引客户
  • 安徽宿州住房与建设网站济南市建设工程招标投标协会网站
  • 做外贸网络推广网站河南seo推广平台
  • 怀化网站优化哪个好php做网站示例
  • 用tornado做网站qq空间网站是多少
  • mvc电子商务网站开发微信app
  • 网站设计师主要做什么wordpress积分查看
  • 厘清大小端:分清数值轴、地址轴与书写轴
  • 保定做网站电话北京市住房建设投资中心网站
  • 如何优化唯品会库存API性能?
  • 四川住房和城乡建设部官方网站WordPress文章不置顶
  • 建设图片展示网站网站服务器指的是什么
  • 卖东西的网站怎么建设app开发需要哪些软件
  • 分析可口可乐网站建设的目的男女做爰视频网站在线
  • 网站模板平台资源电子商务网站功能模块
  • 外贸网站响应式伊春住房和城乡建设局网站
  • 网站制作费用需要多少钱网站主机服务器
  • 河南工会考试真题分享
  • 有需要做网站的吗网站建设设置分享功能
  • 社交网站开发项目计划报告最好最值得做的调查网站
  • 处理视频抽帧并转换成json
  • 东大桥做网站的公司中国建设银行 网站登录
  • 成都科技网站建设联免费书画网站怎么做的
  • [SQL] 给定起止日期,计算出日期行
  • 育苗盘补苗路径规划研究_2.5
  • 建设网站的网站叫什么男网站案例展示分类
  • 什么是网络营销广东宣布即时优化调整
  • 杭州国外网站推广公司企业微信app下载安装不了
  • java-Collection集合-Set集合
  • 明亮的夜晚