接上篇:【Linux内核十】进程管理模块,CPU在执行进程时的状态:current指针
刚开始提到调度器的时候还是:【Linux内核七】进程管理模块:进程调度管理器sched_class
中间学习了调度的其他概念,从这一篇开始,把这几个调度器都先了解一下。
调度器原型
在之前已经提到了sched_class的结构体,这个结构体中有几个函数指针,作为调度用的回调函数,所以在调度器中,首先就是定义了一个宏来初始化这些函数指针,在stop_task.c文件中:
c
/*
* Simple, special scheduling class for the per-CPU stop tasks:
*/
DEFINE_SCHED_CLASS(stop) = {
.enqueue_task = enqueue_task_stop, // 入rq,等待队列时的回调函数
.dequeue_task = dequeue_task_stop, // 出rq,等待队列时的回调函数
.yield_task = yield_task_stop, // 让当前运行的进程主动 "让出 CPU",放弃剩余的运行时间片,请求调度器重新选择下一个进程执行
.check_preempt_curr = check_preempt_curr_stop, // 判断是否可以抢占CPU的回调函数
.pick_next_task = pick_next_task_stop, // 挑选下一个进程
.put_prev_task = put_prev_task_stop, // 完成调度层面的所有收尾工作(统计、记账、状态清理)
.set_next_task = set_next_task_stop,
#ifdef CONFIG_SMP
.balance = balance_stop,
.pick_task = pick_task_stop,
.select_task_rq = select_task_rq_stop,
.set_cpus_allowed = set_cpus_allowed_common,
#endif
.task_tick = task_tick_stop, // 在每个 CPU 时钟滴答(tick)到来时,检查当前运行进程的调度状态(如时间片是否到期),并决定是否需要触发重新调度------ 它是调度器实现 "时间片管理、抢占触发、公平调度" 的关键环节。
.prio_changed = prio_changed_stop, // Linux 内核stop调度类专属的优先级变更回调函数
.switched_to = switched_to_stop, // 当进程的调度类从其他类型(如 CFS/RT)切换到当前调度类(如 stop/RT/CFS)时,完成该调度类专属的初始化工作
.update_curr = update_curr_stop, // 用于实时更新当前运行进程调度统计信息.在调度器的关键节点(如时钟 tick、进程切换),更新当前 CPU 上运行进程的核心统计数据(如 CPU 使用时长、虚拟运行时间、负载等)
};
时间滴答:tick
CPU 时钟滴答(tick)的本质是硬件定时器周期性触发的中断信号,是整个进程调度时间上的基石,所有的调度的时间概念都是以这个tick为基本计数单位。
-
CPU 内置的可编程定时器(如 x86 的 PIT、TSC,ARM 的通用定时器)会按照预设频率,周期性地向 CPU 发送时钟中断请求。每一次中断触发,就称为一个tick。也就是说,一个tick相当于一次中断信号(操作系统的中断概念)。
-
内核通过中断处理函数捕获 tick 信号,以此为时间基准,完成进程调度、时间统计、延时计算等核心工作。
-
在内核中和tick相关的代码:
include/linux/jiffies.h
c# define HZ CONFIG_HZ /* Internal kernel timer frequency */ # define USER_HZ 100 /* some user interfaces are */ // 实际编译时,该值会被 .config 中的 CONFIG_HZ 覆盖。kernel/time.c
c/* * Convert jiffies to milliseconds and back. * * Avoid unnecessary multiplications/divisions in the * two most common HZ cases: */ unsigned int jiffies_to_msecs(const unsigned long j) { #if HZ <= MSEC_PER_SEC && !(MSEC_PER_SEC % HZ) return (MSEC_PER_SEC / HZ) * j; #elif HZ > MSEC_PER_SEC && !(HZ % MSEC_PER_SEC) return (j + (HZ / MSEC_PER_SEC) - 1)/(HZ / MSEC_PER_SEC); #else # if BITS_PER_LONG == 32 return (HZ_TO_MSEC_MUL32 * j + (1ULL << HZ_TO_MSEC_SHR32) - 1) >> HZ_TO_MSEC_SHR32; # else return DIV_ROUND_UP(j * HZ_TO_MSEC_NUM, HZ_TO_MSEC_DEN); # endif #endif }可以计算得到一个tick是多少具体的时间单位。
stop调度器
stop调度器是 Linux 内核中优先级最高的调度实体,其核心作用是:让指定 CPU 核心进入 "完全独占" 状态(暂停所有其他进程 / 调度活动),执行高优先级、不可中断的核心操作------ 简单说,它是内核给 CPU "强行清场" 的工具,保证关键操作不受任何干扰。
stop调度器在极少的场景上会被调度:
- CPU 热插拔(在线 / 离线)
- 内核热补丁 / 模块升级
- per-CPU 变量重分配(罕见)
作为程序员来说,一般不会接触到,但是因为其结构简单,我觉得是入门学习调度器的一个比较好的切入点,毕竟stop_task.c总共才不到200行代码。麻雀虽小五脏俱全,该有的那些回调函数都有,而且很简单。
单队列
因为rq对应的所谓stop调度器对应的队列就是一个task_struct(在调度器和struct rq的模块已经提到过)
c
struct task_struct *stop;
也就是说stop调度器的队列就是一个task,所以叫做单队列,就是这个队列只有一个元素。
回调函数
set_next_task_stop
记录执行起始时间:exec_start是进程调度实体(sched_entity)的核心字段,记录进程开始占用 CPU 的时间戳(来自rq_clock_task,CPU 级别的高精度时钟);
c
static void set_next_task_stop(struct rq *rq, struct task_struct *stop, bool first)
{
stop->se.exec_start = rq_clock_task(rq);
}
调用了:
c
// kernel/sched/clock.c(5.15)
static inline u64 rq_clock_task(struct rq *rq)
{
// 断言1:当前必须持有rq的锁(避免多核竞争)
lockdep_assert_rq_held(rq);
// 断言2:rq->clock_task已经被更新到最新
assert_clock_updated(rq);
// 直接返回缓存值(因为前置更新保证了准确性)
return rq->clock_task;
}
对应了rq队列中的:
c
u64 clock_task ____cacheline_aligned;
从代码上来看,就是最终返回的是rq队列中的clock_task字段的内容。
pick_next_task_stop,如何挑选一个stop进程
没得挑,就那一个,直接判断是否在rq队列里即可。
stop_task.c
c
static inline bool sched_stop_runnable(struct rq *rq)
{
return rq->stop && task_on_rq_queued(rq->stop);
}
static struct task_struct *pick_task_stop(struct rq *rq)
{
if (!sched_stop_runnable(rq))
return NULL;
return rq->stop;
}
static struct task_struct *pick_next_task_stop(struct rq *rq)
{
struct task_struct *p = pick_task_stop(rq);
if (p)
set_next_task_stop(rq, p, true);
return p;
}
kernel/sched/sched.h
c
/* task_struct::on_rq states: */
#define TASK_ON_RQ_QUEUED 1
#define TASK_ON_RQ_MIGRATING 2
static inline int task_on_rq_queued(struct task_struct *p)
{
return p->on_rq == TASK_ON_RQ_QUEUED;
}
最终就是判断stop进程的on_rq状态是否为TASK_ON_RQ_QUEUED。
enqueue_task_stop && dequeue_task_stop
这两个函数是在一起:
c
static void
enqueue_task_stop(struct rq *rq, struct task_struct *p, int flags)
{
add_nr_running(rq, 1);
}
static void
dequeue_task_stop(struct rq *rq, struct task_struct *p, int flags)
{
sub_nr_running(rq, 1);
}
这两个函数就是更新了 rq队列中的nr_running,统计当前 CPU 运行队列中处于 "可运行状态(TASK_RUNNING)" 的进程总数
c
static inline void add_nr_running(struct rq *rq, unsigned count)
{
unsigned prev_nr = rq->nr_running;
rq->nr_running = prev_nr + count;
...
}
static inline void sub_nr_running(struct rq *rq, unsigned count)
{
rq->nr_running -= count;
....
}
我看到这个问题是:为什么不写成:
c
rq->nr_running = rq->nr_running + count;
或者
c
rq->nr_running += count
原因在于:
-
调试友好:保留 "修改前的值",便于问题定位
这是最核心的原因。nr_running是调度器的核心计数字段,一旦出错(比如计数异常导致调度器卡死),需要快速定位问题:
用prev_nr保存修改前的值后,可随时打印 / 断言:
cunsigned prev_nr = rq->nr_running; rq->nr_running = prev_nr + count; // 调试:打印修改前后的值,快速发现异常(比如count为负数、prev_nr溢出) pr_debug("nr_running: prev=%u, count=%d, new=%u\n", prev_nr, count, rq->nr_running); // 断言:防御非法修改(比如count导致nr_running溢出) WARN_ON(rq->nr_running < prev_nr && count > 0);如果直接写rq->nr_running += count,修改前的值会被覆盖,无法追溯 "原本是多少",调试时只能靠猜测。
-
可读性优化:明确 "基于旧值计算新值"
内核代码追求 "一眼能看懂逻辑",临时变量prev_nr的命名本身就是 "注释":
prev_nr = previous number(修改前的数量),看到这个变量就知道 "接下来要基于旧值计算新值";
而rq->nr_running = rq->nr_running + count是 "重复引用字段",阅读时需要多一步确认 "前后两个rq->nr_running是不是同一个值"(虽然是,但视觉上更繁琐)。
-
便于扩展:后续逻辑可复用旧值
如果后续代码需要用到 "修改前的数量"(比如计算负载变化、统计差值),prev_nr可以直接复用,无需再次读取rq->nr_running