【Linux内核十一】进程管理模块:stop调度器(一)

接上篇:【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

原因在于:

  1. 调试友好:保留 "修改前的值",便于问题定位

    这是最核心的原因。nr_running是调度器的核心计数字段,一旦出错(比如计数异常导致调度器卡死),需要快速定位问题:

    用prev_nr保存修改前的值后,可随时打印 / 断言:

    c 复制代码
    unsigned 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,修改前的值会被覆盖,无法追溯 "原本是多少",调试时只能靠猜测。

  2. 可读性优化:明确 "基于旧值计算新值"

    内核代码追求 "一眼能看懂逻辑",临时变量prev_nr的命名本身就是 "注释":

    prev_nr = previous number(修改前的数量),看到这个变量就知道 "接下来要基于旧值计算新值";

    而rq->nr_running = rq->nr_running + count是 "重复引用字段",阅读时需要多一步确认 "前后两个rq->nr_running是不是同一个值"(虽然是,但视觉上更繁琐)。

  3. 便于扩展:后续逻辑可复用旧值

    如果后续代码需要用到 "修改前的数量"(比如计算负载变化、统计差值),prev_nr可以直接复用,无需再次读取rq->nr_running

相关推荐
刘叨叨趣味运维2 小时前
Linux发行版选择指南:找到你的最佳拍档
linux
txinyu的博客2 小时前
解析muduo源码之 TimeZone.h & TimeZone.cc
linux·服务器·网络·c++
爱吃生蚝的于勒2 小时前
【Linux】零基础学习命名管道-共享内存
android·linux·运维·服务器·c语言·c++·学习
txinyu的博客2 小时前
解析muduo源码之 atomic.h
服务器·c++
数智工坊2 小时前
【操作系统-处理器调度】
java·linux·服务器·windows·ubuntu
济6172 小时前
Linux内核---vmlinux、zImage、uImage区别
linux·运维·服务器
阿拉伯柠檬2 小时前
网络层协议IP(二)
linux·网络·网络协议·tcp/ip·面试
静谧空间2 小时前
Linux自动备份Mysql数据
linux·运维·mysql
技术摆渡人2 小时前
专题一:【BSP 核心实战】Linux 系统死机与 DDR 稳定性“法医级”排查全书
linux·驱动开发·车载系统