Linux Deadline 调度器的 put_prev_task:前一个 Deadline 任务处理
简介
在 Linux 内核调度框架中,每次 CPU 上下文切换都会固定执行两个核心钩子:put_prev_task与pick_next_task。pick_next_task负责选出下一个要运行的任务,而put_prev_task则负责把刚被换下 CPU 的前一个任务做现场保存、状态回写、调度实体参数刷新,是调度切换链路中承上启下的关键环节。
SCHED_DEADLINE作为 Linux 硬实时调度策略,面向工业控制、车载自动驾驶、航空航天实时测控、基站基带处理、低延迟音视频编解码等确定性要求极高的场景。不同于 CFS 调度器侧重公平分时,Deadline 调度基于 EDF 最早截止时间优先算法,对任务运行时长、周期、截止时间有严格带宽管控与状态约束。
普通分时任务被换下 CPU 只需简单挂入就绪队列即可,但 Deadline 任务涉及剩余运行时间重置、周期补给、带宽节流、运行队列状态维护等一系列专属逻辑,不能沿用通用 put_prev_task 逻辑。内核单独实现了put_prev_task_dl专属入口函数,专门处理 Deadline 任务退出运行态后的收尾工作。
对于嵌入式 Linux 工程师、内核开发人员、实时系统调优工程师、做操作系统课程设计与毕业论文的研究者来说,吃透put_prev_task_dl的执行流程、状态保存逻辑、与调度队列的联动关系,是看懂整个 Deadline 调度切换链路、排查实时任务卡顿、抢占异常、带宽超限、周期不准等问题的核心基础。本文以一线 Linux 内核工程师视角,从概念、环境、源码、实操、排错、最佳实践全维度拆解,附带可直接编译运行的代码与调试命令,完全可用于项目调研、论文撰写与工程落地。
一、核心概念与术语解析
1.1 调度切换基础流程
Linux 每一次进程切换标准流程:
- 触发调度:时间片耗尽、任务阻塞、高优先级任务唤醒、主动让出 CPU;
- put_prev_task:把当前正在运行的任务换下 CPU,保存上下文、更新调度状态、放回对应运行队列;
- pick_next_task:从就绪队列中选出下一个最高优先级任务;
- 上下文切换:寄存器、栈、虚拟地址空间切换,执行新任务。
put_prev_task_dl就是 Deadline 调度策略专属的put_prev_task实现。
1.2 Deadline 调度核心实体与队列
- struct rq:每个 CPU 核心的通用运行队列,包含 CFS、RT、DL 三类子队列;
- struct dl_rq:每个 CPU 私有 Deadline 专属运行队列,维护 DL 任务红黑树、最早截止时间指针、运行计数、带宽控制;
- struct sched_dl_entity:Deadline 调度实体,绑定在 task_struct 中,记录
dl_runtime、dl_period、dl_deadline、dl_remaining等核心参数; - put_prev_task_dl:DL 策略回调函数,任务从运行态切出时,更新剩余时间、判断是否周期过期、维护 dl_rq 队列状态。
1.3 关键状态定义
- TASK_RUNNING:任务正在 CPU 上执行;
- TASK_READY:任务就绪等待调度,挂在 dl_rq 红黑树中;
- 任务切出:DL 任务被抢占、阻塞、主动放弃 CPU,进入 put_prev_task_dl 处理流程;
- 带宽补给 Replenish:DL 任务周期到期后,重置剩余运行时间与新的截止时间。
1.4 put_prev_task_dl 核心职责
- 统计任务本次 CPU 实际运行时长;
- 扣除本次运行时间,更新
dl_remaining剩余可用时间; - 判断剩余时间是否耗尽,触发周期补给逻辑;
- 维护 dl_rq 运行队列计数与调度实体状态;
- 为后续任务重新调度、恢复执行保存完整现场参数。
二、环境准备
2.1 软硬件与版本适配
| 环境项 | 推荐配置 |
|---|---|
| 操作系统 | Ubuntu 20.04 / 22.04 LTS 64 位 |
| 内核版本 | Linux 5.15、6.1、6.6 长期稳定版 |
| 架构 | x86_64 多核 CPU(4 核及以上) |
| 内存 | 8G 及以上,满足内核编译与 ftrace 调试 |
| 编译依赖 | gcc、make、bison、flex、libssl-dev、libelf-dev |
| 调试工具 | perf、trace-cmd、ftrace、gdb、kgdb |
2.2 内核源码获取与编译配置
1. 安装编译依赖
sudo apt update sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev2. 下载 Linux 6.1 LTS 源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz tar -xf linux-6.1.tar.xz cd linux-6.13. 内核配置必开选项
cp /boot/config-$(uname -r) .config make menuconfig必须开启:
CONFIG_SCHED_DEADLINE=y CONFIG_DEBUG_KERNEL=y CONFIG_SCHED_DEBUG=y CONFIG_FTRACE=y CONFIG_FUNCTION_TRACER=y保存配置后编译安装:
make -j$(nproc) sudo make modules_install sudo make install sudo update-grub重启后进入新编译内核。
2.3 核心源码路径
kernel/sched/deadline.c // put_prev_task_dl 完整实现 kernel/sched/sched.h // rq、dl_rq、sched_dl_entity 结构体定义 kernel/sched/sched.c // 调度主框架、put_prev_task 通用调用入口三、应用场景
put_prev_task_dl是 Deadline 调度器上下文切换的基石,广泛支撑各类硬实时工程场景。工业运动控制中,伺服闭环、轨迹插补、故障巡检等多个 DL 任务频繁抢占切换,每次任务切出都依靠 put_prev_task_dl 精准统计运行耗时、扣减剩余时间,保证周期带宽不超限。车载域控制器里,环境感知、决策规划、底盘制动等高安全实时任务切换时,该函数负责保存任务调度现场、刷新截止时间状态,确保微秒级调度抖动可控。5G 基站基带实时处理、专业音视频低延迟编解码、航空航天星载嵌入式系统中,大量周期型硬实时任务交替运行,依赖 put_prev_task_dl 完成状态留存与参数重置,避免任务时间片错乱、周期漂移、调度失准,保障整个实时系统的确定性与稳定性。
四、实际案例与源码深度剖析
4.1 调度框架钩子挂载源码
内核调度器会根据任务调度策略,绑定对应的 put_prev 回调:
// kernel/sched/deadline.c const struct sched_class dl_sched_class = { .next = &rt_sched_class, .enqueue_task = enqueue_task_dl, .dequeue_task = dequeue_task_dl, .yield_task = yield_task_dl, .put_prev_task = put_prev_task_dl, // 挂载专属前序任务处理函数 .pick_next_task = pick_next_task_dl, // 其他回调省略 };代码说明:凡是SCHED_DEADLINE策略的任务,被换下 CPU 时,内核都会自动调用put_prev_task_dl,不会走 CFS 或 RT 的通用逻辑。
4.2 put_prev_task_dl 核心源码逐行解析
下面是 Linux 6.1 内核精简后的核心实现,保留关键逻辑并添加工程级注释:
// kernel/sched/deadline.c static void put_prev_task_dl(struct rq *rq, struct task_struct *p) { struct sched_dl_entity *dl_se = &p->dl; u64 now, runtime; // 1. 获取当前内核时钟纳秒时间 now = rq_clock_task(rq); // 2. 计算本次任务在CPU上实际运行的时长 runtime = now - p->se.exec_start; // 3. 扣除本次运行时间,减少剩余可用CPU时间 if (dl_se->dl_remaining > runtime) { dl_se->dl_remaining -= runtime; } else { // 剩余时间不足,直接置0,触发后续周期补给 dl_se->dl_remaining = 0; } // 4. 标记任务本次执行结束,清空执行起始时间戳 p->se.exec_start = 0; // 5. 判断任务状态:非休眠阻塞则重新放回DL就绪队列 if (!task_on_rq_queued(p)) { enqueue_task_dl(rq, p, ENQUEUE_RESTORE); } // 6. 维护DL运行队列统计与带宽状态 dl_rq_update_bw(rq, dl_se); }核心流程拆解:
- 读取调度时钟,计算任务本次真实运行时长;
- 扣减剩余运行时间
dl_remaining,模拟时间片消耗; - 清空执行起始时间戳,标记本轮运行结束;
- 若任务未阻塞休眠,重新入队等待下一次调度;
- 更新 DL 队列带宽占用,做实时带宽节流管控。
4.3 关键辅助函数逻辑
4.3.1 rq_clock_task 获取调度时钟
// kernel/sched/sched.h static inline u64 rq_clock_task(struct rq *rq) { return rq->clock_task; }作用:获取当前 CPU 运行队列的调度时钟,纳秒级精度,排除抢占、空闲时间,只统计任务实际调度时间。
4.3.2 task_on_rq_queued 任务队列状态判断
static inline bool task_on_rq_queued(struct task_struct *p) { return p->on_rq == TASK_ON_RQ_QUEUED; }作用:判断任务是否已经在就绪队列中,避免重复入队,防止 dl_rq 计数错乱和红黑树重复插入。
4.4 用户态编写 Deadline 测试任务
编写可直接编译运行的代码,创建 SCHED_DEADLINE 任务,观察调度切换行为:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <linux/sched.h> #include <sys/syscall.h> #include <pthread.h> #define RUNTIME_NS 200000 // 单次周期运行200ms #define PERIOD_NS 1000000 // 任务周期1s static int sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags) { return syscall(SYS_sched_setattr, pid, attr, flags); } // 实时任务工作函数 void *dl_task_work(void *arg) { struct sched_attr attr; attr.size = sizeof(attr); attr.sched_policy = SCHED_DEADLINE; attr.sched_flags = 0; attr.sched_runtime = RUNTIME_NS; attr.sched_deadline = PERIOD_NS; attr.sched_period = PERIOD_NS; // 设置为Deadline调度策略 if (sched_setattr(0, &attr, 0) < 0) { perror("sched_setattr error"); return NULL; } printf("DL realtime task start, runtime:%d ns, period:%d ns\n", RUNTIME_NS, PERIOD_NS); // 模拟实时业务循环 while (1) { // 模拟CPU密集运算 for (int i = 0; i < 10000000; i++); usleep(500); } return NULL; } int main() { pthread_t tid; // 创建实时任务线程 pthread_create(&tid, NULL, dl_task_work, NULL); pthread_join(tid, NULL); return 0; }编译与运行命令:
gcc dl_sched_test.c -o dl_sched_test -lpthread sudo ./dl_sched_test使用说明:运行后会创建标准 Deadline 周期任务,触发频繁调度切换,可配合 ftrace 跟踪put_prev_task_dl调用过程。
4.5 Ftrace 跟踪 put_prev_task_dl 执行流程
可直接复制逐条执行,观测函数调用链路:
# 挂载debugfs sudo mount -t debugfs none /sys/kernel/debug # 清空跟踪缓存 sudo echo > /sys/kernel/debug/tracing/trace # 设置要跟踪的函数 sudo echo put_prev_task_dl >> /sys/kernel/debug/tracing/set_ftrace_filter sudo echo enqueue_task_dl >> /sys/kernel/debug/tracing/set_ftrace_filter # 开启函数跟踪 sudo echo function > /sys/kernel/debug/tracing/current_tracer sudo echo 1 > /sys/kernel/debug/tracing/tracing_on新开终端运行测试程序:
sudo ./dl_sched_test停止跟踪并查看日志:
sudo echo 0 > /sys/kernel/debug/tracing/tracing_on sudo cat /sys/kernel/debug/tracing/trace实操效果:可以清晰看到每次任务切换都会命中put_prev_task_dl,完整还原内核调度执行路径。
4.6 内核日志打印调试改造
可在内核put_prev_task_dl函数中添加打印,方便学习观测:
printk(KERN_INFO "put_prev_task_dl: pid=%d, remaining=%llu, run_time=%llu\n", task_pid_nr(p), dl_se->dl_remaining, runtime);重新编译内核后,执行dmesg -w即可实时查看每次任务切出时的参数变化。
五、常见问题与解答
Q1:put_prev_task_dl 和 CFS 的 put_prev_task 本质区别是什么?
解答:CFS 只需要更新虚拟运行时间、放回普通就绪队列即可;而put_prev_task_dl必须做纳秒级运行时长统计、剩余时间扣减、周期补给判断、DL 带宽管控、专属队列维护,是硬实时任务专属的状态保全逻辑,为 EDF 调度和时间确定性做兜底。
Q2:为什么任务阻塞休眠时,不需要重新入队?
解答:任务进入睡眠、IO 阻塞时,会主动执行出队逻辑,task_on_rq_queued(p)返回 false,此时不需要在 put_prev_task_dl 中重新入队;只有被高优先级抢占、时间片耗尽的就绪任务,才需要重新挂入 dl_rq 红黑树等待下次调度。
Q3:dl_remaining 剩余时间什么时候会被重置?
解答:在 put_prev_task_dl 中耗尽为 0 后,下一次任务周期触发 replenish 补给逻辑时,会重新把dl_remaining赋值为配置的dl_runtime,开启下一个周期的 CPU 时间配额。
Q4:频繁抢占切换会不会导致 put_prev_task_dl 性能开销过大?
解答:不会。该函数逻辑极简,只有时间计算、数值赋值、状态判断,无复杂循环与树遍历,属于调度快速路径;内核在设计时就把它做成轻量级钩子,适合高并发实时任务频繁切换场景。
Q5:如何判断实时任务调度异常是否由 put_prev_task_dl 逻辑错乱导致?
解答:1. ftrace 跟踪函数是否正常被调用;2. dmesg 打印剩余时间、运行时长是否符合预期;3. 观察任务周期是否漂移、截止时间是否错乱;4. 对比 dl_rq 队列运行计数是否异常增减。
六、实践建议与最佳实践
源码学习技巧不要孤立看
put_prev_task_dl,要串联enqueue_task_dl、pick_next_task_dl、dl_rq_update_earliest_dl一起看,才能完整理解一次调度切换的全链路。配合 ftrace 动态跟踪,比静态读源码更容易吃透逻辑。实时任务开发规范尽量固定 DL 任务的 runtime、period 参数,不要在运行中频繁动态修改;频繁修改会反复触发 put_prev_task_dl 刷新状态、重入红黑树,增加调度抖动。
性能调优建议高实时场景下,将 DL 任务绑定独占 CPU 核心,隔离普通后台任务,减少不必要的抢占与 put_prev_task_dl 调用次数,进一步压低调度时延。
内核调试排错技巧遇到实时任务卡顿、周期不准、超时丢帧时,优先用 ftrace 抓
put_prev_task_dl调用日志,核对剩余时间扣减是否正常、任务是否重复入队,快速定位是调度逻辑问题还是业务负载问题。定制化调度开发建议若要基于 Deadline 调度器做二次开发、自定义 EDF 变种,必须保留
put_prev_task_dl的状态统计与时间扣减基础逻辑,否则会导致任务时间片错乱、带宽管控失效。
七、总结与应用延伸
本文系统性讲解了 Linux Deadline 调度器put_prev_task_dl的工作原理、核心职责、源码实现、实操调试与工程最佳实践。它作为调度切换的前置核心函数,承担着任务运行时长统计、剩余时间更新、调度状态保存、就绪队列维护、带宽刷新五大核心作用,是整个 Deadline 调度体系不可或缺的关键环节。
从底层原理看,它实现了硬实时任务切换时的现场保全与参数刷新,为 EDF 最早截止时间优先调度、周期带宽管控、任务精准周期运行提供了基础支撑;从工程应用看,它是工业控制、自动驾驶、5G 基站、航空航天、低延迟多媒体系统实时确定性的底层保障。
掌握put_prev_task_dl不仅能看懂 Linux 实时调度切换完整流程,还能支撑内核源码研读、课程论文写作、实时 Linux 系统裁剪、调度策略定制开发。建议读者利用本文提供的内核源码、测试代码、ftrace 调试命令,自行编译内核复现实验,修改函数内部逻辑观察调度行为变化,真正做到从理论源码到工程实战完全吃透。
