作为一名长期从事Linux内核开发的工程师,我经常需要处理实时性要求极高的任务调度问题。今天我想和大家深入探讨Linux内核中的截止时间调度器(SCHED_DEADLINE,简称DL),这是Linux内核中最强大的实时调度机制之一。
DL调度器是Linux内核3.14版本引入的一种高精度实时调度策略,它基于最早截止时间优先(Earliest Deadline First,EDF)算法实现。与传统的实时调度策略(如SCHED_FIFO和SCHED_RR)不同,DL调度器允许任务以"每周期最多运行多久"的方式使用CPU资源,从而确保任务在截止时间前获得可预测的执行资源。
这种调度方式特别适合以下场景:
在使用DL调度策略时,我们需要为每个任务指定三个关键参数:
这三个参数共同定义了任务的实时行为和资源需求,是DL实现确定性调度的基础。
在用户空间,我们可以通过sched_setattr()系统调用为任务设置DL调度参数。下面是一个完整的示例代码:
c复制// 用户空间兼容定义(若未包含 linux/sched/types.h)
struct sched_attr {
unsigned int size;
unsigned int sched_policy;
unsigned long long sched_flags;
int sched_nice;
unsigned int sched_priority;
unsigned long long sched_runtime;
unsigned long long sched_deadline;
unsigned long long sched_period;
};
int main() {
struct sched_attr attr = {
.size = sizeof(struct sched_attr),
.sched_policy = SCHED_DEADLINE, //指定使用 deadline 调度策略
.sched_runtime = 2000000ULL, // 2 ms
.sched_deadline= 8000000ULL, // 8 ms
.sched_period = 10000000ULL // 10 ms
};
if (syscall(__NR_sched_setattr, 0, &attr, 0) == -1) {
perror("sched_setattr");
return 1;
}
// ... 执行实时工作负载 ...
}
在这个例子中,我们配置了一个DL任务,其参数为:
这意味着该任务每10ms获得一个执行周期,在每个周期内最多可以运行2ms,并且必须在8ms内完成工作。如果任务提前完成,剩余时间会立即释放;如果用尽2ms仍未完成,则会被节流,暂停执行直到下一周期。
内核中,DL调度相关的核心数据结构是sched_dl_entity,它存储了任务的调度参数和状态信息:
c复制struct sched_dl_entity {
struct rb_node rb_node; // 红黑树节点
u64 dl_runtime; // 运行时间预算
u64 dl_deadline; // 当前周期的绝对截止时间
u64 dl_period; // 周期长度
u64 runtime; // 当前剩余运行时间
// ... 其他字段 ...
};
我们可以通过内核调试工具查看这些字段的实际值。例如,在NanoCode中执行:
code复制dt lk!sched_dl_entity 0xffff00010df551c8
会显示类似如下的输出:
code复制dl_runtime = 0x1e8480 ns (2ms)
dl_deadline = 0x7a1200 ns (8ms)
dl_period = 0x989680 ns (10ms)
runtime = 763721 ns (≈0.76ms)
deadline = 0x2f06777fd1 (≈199秒,自系统启动起)
其中runtime表示当前周期中剩余的运行时间,deadline是当前周期的绝对截止时间戳。
DL调度器通过调度类(sched_class)机制与内核调度框架集成。其核心回调函数定义如下(简化版):
c复制DEFINE_SCHED_CLASS(dl) = {
.enqueue_task = enqueue_task_dl, // 任务入队
.dequeue_task = dequeue_task_dl, // 任务出队
.pick_next_task = pick_next_task_dl, // 选择下一个任务(EDF核心)
.put_prev_task = put_prev_task_dl, // 切出当前任务
.task_tick = task_tick_dl, // 时钟滴答:更新执行时间
.update_curr = update_curr_dl, // 更新当前任务CPU时间
.task_fork = task_fork_dl, // 子任务初始化
#ifdef CONFIG_SMP
.select_task_rq = select_task_rq_dl, // 选择目标CPU
#endif
};
这些回调函数共同实现了DL调度器的核心功能。
当DL任务被唤醒时,内核会调用enqueue_task_dl将其加入运行队列。关键步骤如下:
c复制static inline void replenish_dl_new_period(struct sched_dl_entity *dl_se,
struct rq *rq)
{
/* deadline = 当前系统时间 + 用户指定的相对截止期 */
dl_se->deadline = rq_clock(rq) + pi_of(dl_se)->dl_deadline;
/* runtime = 用户配置的运行预算 */
dl_se->runtime = pi_of(dl_se)->dl_runtime;
}
c复制static __always_inline struct rb_node *
rb_add_cached(struct rb_node *node, struct rb_root_cached *tree,
bool (*less)(struct rb_node *, const struct rb_node *))
{
struct rb_node **link = &tree->rb_root.rb_node;
struct rb_node *parent = NULL;
bool leftmost = true;
while (*link) {
parent = *link;
if (less(node, parent)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(node, parent, link);
rb_insert_color_cached(node, tree, leftmost);
return leftmost ? node : NULL;
}
选择下一个任务时,DL调度器总是选择deadline最早的任务,这是EDF算法的核心:
c复制static struct sched_dl_entity *pick_next_dl_entity(struct dl_rq *dl_rq)
{
struct rb_node *left = rb_first_cached(&dl_rq->root);
if (!left)
return NULL;
return __node_2_dle(left);
}
DL调度器通过update_curr_dl函数更新任务的运行时信息:
c复制static void update_curr_dl(struct rq *rq)
{
struct task_struct *curr = rq->curr;
struct sched_dl_entity *dl_se = &curr->dl;
u64 delta_exec, scaled_delta_exec;
u64 now;
if (!dl_task(curr) || !on_dl_rq(dl_se))
return;
now = rq_clock_task(rq);
delta_exec = now - curr->se.exec_start;
if (unlikely((s64)delta_exec <= 0)) {
if (unlikely(dl_se->dl_yielded))
goto throttle;
return;
}
/* 考虑CPU频率和算力缩放 */
unsigned long scale_freq = arch_scale_freq_capacity(cpu_of(rq));
unsigned long scale_cpu = arch_scale_cpu_capacity(cpu_of(rq));
scaled_delta_exec = cap_scale(delta_exec, scale_freq);
scaled_delta_exec = cap_scale(scaled_delta_exec, scale_cpu);
/* 扣减runtime预算 */
dl_se->runtime -= scaled_delta_exec;
当runtime耗尽时,任务会被限流:
c复制throttle:
if (dl_runtime_exceeded(dl_se) || dl_se->dl_yielded) {
dl_se->dl_throttled = 1;
if (dl_runtime_exceeded(dl_se) &&
(dl_se->flags & SCHED_FLAG_DL_OVERRUN))
dl_se->dl_overrun = 1;
__dequeue_task_dl(rq, curr, 0);
/* 启动replenishment定时器 */
if (unlikely(is_dl_boosted(dl_se) || !start_dl_timer(curr)))
enqueue_task_dl(rq, curr, ENQUEUE_REPLENISH);
if (!is_leftmost(curr, &rq->dl))
resched_curr(rq);
}
定时器回调会恢复任务的runtime并推进deadline:
c复制static void replenish_dl_entity(struct sched_dl_entity *dl_se)
{
struct rq *rq = rq_of_dl_rq(dl_rq_of_se(dl_se));
/* 循环推进周期,直到获得正的runtime */
while (dl_se->runtime <= 0) {
dl_se->deadline += pi_of(dl_se)->dl_period;
dl_se->runtime += pi_of(dl_se)->dl_runtime;
}
/* 清除节流标志 */
dl_se->dl_yielded = 0;
dl_se->dl_throttled = 0;
}
DL调度器最适合以下类型的应用:
配置DL参数时,建议遵循以下原则:
在使用DL调度器时,可能会遇到以下问题:
任务被频繁限流:
截止时间错过(deadline miss):
调度延迟过大:
在多核系统上使用DL调度器时,可以考虑以下优化:
c复制cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
sched_setaffinity(0, sizeof(cpuset), &cpuset);
code复制isolcpus=1,2,3
在移动设备上使用DL调度器时,需要注意:
调试DL调度器问题时,可以使用以下工具:
bash复制echo 1 > /sys/kernel/debug/tracing/events/sched/enable
cat /sys/kernel/debug/tracing/trace_pipe
bash复制perf stat -e sched:sched_switch task
bash复制rtla timerlat -c 0-3
经过多年的内核开发和性能调优工作,我发现DL调度器是Linux实时系统中最为强大的工具之一,但也需要谨慎使用。以下是我在实际项目中的一些经验:
参数验证很重要:在实际部署前,务必通过压力测试验证参数设置的合理性。我曾经遇到过一个案例,由于低估了最坏情况执行时间,导致生产环境中频繁出现deadline miss。
系统余量很关键:即使理论计算表明系统利用率可行,也建议保留至少5-10%的余量,以应对不可预知的中断和延迟。
监控不可少:建立完善的监控机制,及时发现和处理deadline miss。我们开发了一个内核模块,专门用于统计和报告DL任务的执行情况。
组合使用其他特性:DL调度器可以与其他Linux特性(如cgroup、IRQ affinity等)配合使用,获得更好的效果。例如,通过cgroup限制后台任务的干扰,可以显著提高DL任务的确定性。
文档和培训:确保团队成员都理解DL调度器的工作原理和限制。我们曾经因为一个开发人员错误地认为DL任务可以无限使用CPU而导致系统不稳定。
Linux调度系统是一个复杂而精妙的工程杰作,DL调度器作为其中的重要组成部分,为实时应用提供了强大的支持。希望通过本文的分享,能够帮助大家更好地理解和使用这一重要特性。