【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

相关推荐
A小辣椒10 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒13 小时前
TShark:基础知识
linux
AlfredZhao16 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux