Skip to the content.

task rcu

function tracer 会自动的打开 CONFIG_TASKS_RUDE_RCU

config FUNCTION_TRACER
	bool "Kernel Function Tracer"
	depends on HAVE_FUNCTION_TRACER
	select KALLSYMS
	select GENERIC_TRACER
	select CONTEXT_SWITCH_TRACER
	select GLOB
	select TASKS_RCU if PREEMPTION
	select TASKS_RUDE_RCU
	help
	  Enable the kernel to trace every kernel function. This is done
	  by using a compiler feature to insert a small, 5-byte No-Operation
	  instruction at the beginning of every kernel function, which NOP
	  sequence is then dynamically patched into a tracer call when
	  tracing is enabled by the administrator. If it's runtime disabled
	  (the bootup default), then the overhead of the instructions is very
	  small and not measurable even in micro-benchmarks (at least on
	  x86, but may have impact on other architectures).

https://docs.kernel.org/RCU/Design/Requirements/Requirements.html#tasks-rcu

trace 机制和 preemption 通知打开的时候,tracepoint 里面的 trampolines 的内存的释放。

所以,我们观察到,这一组函数的调用者都是 call_rcu_tasks() 之类的

为什么 task rcu 引入一堆的 thread 来支持

这些线程可以粗分成四类:

在一台开启 PREEMPT_RCUTASKS_RCUTASKS_TRACE_RCU 之类配置的机器上,常见线程名可以这样理解:

可以用名字快速猜它的职责:

详细的讲解一下 rcu tasks 如用于解决 trace 问题的

可以把 RCU Tasks 理解成:

普通 RCU 等“旧 reader 消失”;RCU Tasks 等“旧 task 不可能还停留在某段旧代码里”。

它主要不是为了保护普通数据结构,而是为了 ftrace/BPF/kprobe 等动态修改代码、trampoline 这种场景。 Linux 文档也是从“如何安全释放旧 trampoline”这个问题引出 Tasks RCU 的。

1. trampoline 引入了一个奇怪的问题

考虑 ftrace/BPF 修改函数入口:

foo:
    ...
    call trampoline_A
    ...

现在我们想把 trampoline_A 替换成 trampoline_B

old:

    code ---> trampoline_A

new:

    code ---> trampoline_B

修改入口本身可能很容易。

真正麻烦的是:

什么时候可以 free trampoline_A?

例如 CPU0:

CPU0

trampoline_A:
    instruction 1
    instruction 2
    instruction 3
         ^
         |
     此时被抢占

CPU0 上的 task T 被抢占了。

注意它保存的 RIP 是:

T->saved_rip = trampoline_A + offset

这时候 CPU1:

CPU1:

把入口修改成 trampoline_B

free(trampoline_A);

那就炸了。

等 CPU0 上的 T 再调度回来:

resume T
   |
   v
trampoline_A + offset

而这块代码已经被释放。

3. 为什么 synchronize_rcu() 不够?

你可能马上想到:

patch_to_B();

synchronize_rcu();

free(trampoline_A);

问题是这里:

trampoline_A

里面没有:

rcu_read_lock();
...
rcu_read_unlock();

而且也不容易添加进去,因为无法保证 rcu_read_lock 和 rcu_read_unlock 是 trampoline 的第一条指令和最后一条指令

所以普通 RCU 根本不知道:

task T 正停在 trampoline_A 里面

4. Tasks RCU 的核心思路

Tasks RCU 干脆换一个观察对象:

普通 RCU:

        看 RCU reader
             ↓
rcu_read_lock() ----- rcu_read_unlock()


Tasks RCU:

        看整个 task
             ↓
   一个 quiescent state
             |
             | 任意 kernel execution
             |
   下一个 quiescent state

也就是说,Tasks RCU 可以近似认为:

一个 task 从上一个 Tasks-RCU quiescent state 到下一个 quiescent state 之间,都处于一个隐式的 read-side critical section。

因此它没有普通 Tasks RCU 的:

rcu_read_lock_tasks();
rcu_read_unlock_tasks();

这也是官方 API 表里面 RCU-Tasks 的 read-side critical section 写成 N/A 的原因。([Linux Kernel Archives][3])

主要 API 就是:

synchronize_rcu_tasks();

call_rcu_tasks(...);

rcu_barrier_tasks();

5. Tasks RCU 的 quiescent state 计算

最重要的是 voluntary context switch

也就是 task 主动走到 scheduler

Task T

kernel code
    |
    |
    v
schedule()
    |
    +-------- Tasks-RCU QS

另外,进入 userspace 也是一个很强的证据:

kernel
 |
 | return to userspace
 v
userspace

因为如果 task 已经跑回 userspace:

它显然已经不可能还停留在刚才那个 kernel trampoline 里面。

Linux 因此将 voluntary scheduling、userspace execution 等作为 Tasks RCU 判断 task 已离开旧执行区间的重要依据。

6. 但是被抢占不算

这个非常重要。

假设:

Task A:

        trampoline_old
              |
              | instruction
              X  <---- preempt here

然后:

CPU0:

A
|
| preempt
v
B
|
C
|
D

虽然 CPU 已经 context switch 了很多次,但:

A->RIP

仍然指向:

trampoline_old

因此:

involuntary context switch 不能证明这个 task 离开了旧代码。

Linux 文档对此明确强调:involuntary context switch 不是 Tasks-RCU quiescent state。原因正是 task 可能被抢占在 trampoline 中,恢复后还要继续执行它。

这其实就是 Tasks RCU 最值得记住的一点。

7. 所以 synchronize_rcu_tasks() 在等什么?

可以先用一个高度简化的模型理解:

调用:

synchronize_rcu_tasks();

时,假设系统里有:

T1
T2
T3
T4

RCU Tasks 大致认为:

              synchronize_rcu_tasks()
                        |
                        v

T1 ------old execution------ QS
T2 --old execution------------- QS
T3 ------------ QS
T4 ----old execution--------------------- QS
                                           |
                                           v
                                      GP complete

必须确认调用开始时相关的 task 都到达了一个能够证明:

我已经不可能还停留在之前那段 kernel code 中

的状态。

然后:

synchronize_rcu_tasks();

才能返回。

8. 回到 trampoline 的例子

现在整个事情就非常漂亮了:

remove_trampoline(old);

synchronize_rcu_tasks();

free(old);

假设某个 task:

                  remove old
                      |
                      v

Task A:
    trampoline_old
         |
         X preempt
         .
         .        synchronize_rcu_tasks()
         .
         v
    resume
         |
         v
    trampoline_old remaining code
         |
         v
    normal kernel code
         |
         v
      schedule()       <-- QS

Tasks RCU 不会因为:

X preempt

就放过 A。

它会一直等。

直到 A 恢复:

resume

把 old trampoline 跑完,随后最终到达安全点:

schedule()

于是可以推导:

A 不可能再执行 old trampoline

此时才能安全:

free(old);

9. 为什么名字叫 Tasks RCU?

因为 grace period 的 tracking 单位不同。

普通 Tree RCU 很大程度是在追踪:

CPU
+
被抢占的显式 RCU reader

而 Tasks RCU 的思想是:

for each relevant task:
        你有没有证明自己已经越过旧执行状态?

所以可以粗略记:

RCU
    grace period over RCU readers

Tasks RCU
    grace period over tasks

这不是说:

“Tasks RCU 用来保护 task_struct。”

这是非常容易产生的误解。

不是。

task_struct / task list 本身也大量使用普通 RCU:

rcu_read_lock();
for_each_process(p) {
        ...
}
rcu_read_unlock();

这里的 "Tasks" 指的是 grace-period 的观察对象是 task 的执行状态

11. 再和普通 RCU 对照一下

最推荐记住这个表:

  普通 RCU Tasks RCU
主要保护对象 data lifetime code/execution lifetime
reader 边界 rcu_read_lock/unlock() 隐式
GP 等谁 old RCU readers old tasks execution
preempt reader 显式 reader 会被 RCU 跟踪 仅仅被 preempt 不代表 QS
典型用途 链表、hash、对象生命周期 ftrace/BPF/trampoline
等待 API synchronize_rcu() synchronize_rcu_tasks()

当然,“data vs code”不是 API 的严格定义,但作为心智模型非常准确

12. 还有 Tasks Rude 和 Tasks Trace

现在内核实际上有三兄弟:

                    RCU Tasks
                       |
        +--------------+--------------+
        |                             |
   Tasks Rude                    Tasks Trace
RCU Tasks

也经常称 classic Tasks RCU:

synchronize_rcu_tasks();

特点:

reader 没有显式标记
依赖 task 到达安全执行状态

典型:

trampoline/code lifetime

RCU Tasks Rude
synchronize_rcu_tasks_rude();

它解决更极端的问题:

连普通 RCU 不观察的 CPU 状态,例如某些 preemption-disabled / idle 相关执行区域,我也想确认它过去了。

它会比较“粗暴”地让各 CPU 执行工作,因此叫 Rude

官方文档描述它会迫使各 online CPU 调度 work,因此可能打扰 nohz_full/实时 workload。

可以粗略理解:

Tasks RCU:
    “等 task 自己证明安全”

Tasks Rude:
    “我主动去各 CPU 敲门:
     你们都给我证明一下现在安全”
RCU Tasks Trace

API:

rcu_read_lock_trace();

...

rcu_read_unlock_trace();

synchronize_rcu_tasks_trace();

和 Classic Tasks RCU 不一样:

Tasks Trace 有显式 reader

它是专门针对 tracing 场景设计的,能够覆盖更加复杂的 execution context。 当前内核文档列出的 API 包括 rcu_read_lock_trace()rcu_read_unlock_trace()synchronize_rcu_tasks_trace()

13. 总结

普通 RCU:

我不知道 reader 在干什么。

但只要:

        rcu_read_lock()
              |
              |
              v
        rcu_read_unlock()

全部过去,

旧对象就没人用了。

Tasks RCU:

我甚至无法给 reader 加 lock/unlock。

所以我观察 task:

        old code
           |
           |
           v
    voluntary schedule / userspace
           |
           v

一旦所有旧 task 都跨过这种边界,

它们就不可能还停在 old code 中。

于是 old code 可以释放。

所以一句话概括:

Tasks RCU 是一种用“task 的执行进展”来定义 grace period 的 RCU,核心目的是在没有显式 read-side marker 的情况下,确认所有旧执行流已经离开某段旧代码。

而它最关键的细节就是:

task 被抢占
    ≠
Tasks-RCU quiescent state

因为被抢占的 task 完全可能保存着指向旧 trampoline 的 RIP。这也是理解 Tasks RCU 的钥匙。([Linux Kernel Archives][4])

补充说明

如何区分是自动 schedule 还是 preempt 的?

都会 context switch ,但是

主动阻塞/主动调度

schedule()
  -> __schedule_loop(SM_NONE)
       -> __schedule(SM_NONE)

内核抢占

preempt_schedule()
  -> preempt_schedule_common()
       -> __schedule(SM_PREEMPT)

进入到用户态后,如何通知 QS

场景 1:中断发生时任务本来就在用户态

  userspace ── 时间片到 / 被唤醒抢占
      │ timer interrupt (interrupt lands in user mode)
      v
  irqentry_exit(): user_mode(regs) 为真
      └─ irqentry_exit_to_user_mode()
          └─ exit_to_user_mode_loop()      kernel/entry/common.c:40-47
              └─ TIF_NEED_RESCHED → schedule()   ← 普通 schedule(),SM_NONE!

场景 2:中断发生时任务在内核态(比如 trampoline 里)

  kernel (trampoline 中间) ── 被抢占
      │ timer interrupt (interrupt lands in kernel mode)
      v
  irqentry_exit(): user_mode(regs) 为假
      └─ irqentry_exit_to_kernel_mode()
          └─ raw_irqentry_exit_cond_resched()   kernel/entry/common.c:132-145
              └─ preempt_schedule_irq()  →  __schedule(SM_PREEMPT)

可以继续切换,然后执行其他程序,加入 A 被打断了,最后切换到用户态,就可以通知 GP

  synchronize_rcu_tasks() 调用者
     │ wait_for_completion(&rs->completion)        睡觉…
     │
     ├─ call_rcu_tasks(&rs->head, wakeme_after_rcu) ← 排进回调队列,唤醒 GP kthread
     │
     ▼
  GP kthread: 建 holdout 列表(快照每个任务 nvcsw)
     │
     ▼
  A 跑完内核代码,进入用户态
     │ 路径1/2/3 之一 ──► current->rcu_tasks_holdout = false   (只写一个位)
     ▼
  GP kthread 睡醒,check_holdout_task() 看到标记 ──► 摘除 A
     │
     ▼ holdout 列表空
  GP 完成 ──► rcu_tasks_invoke_cbs() ──► wakeme_after_rcu() ──► complete()
     │
     ▼
  synchronize_rcu_tasks() 返回

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。