【Linux内核七】进程管理模块:进程调度管理器sched_class

接上篇:【Linux内核六】进程管理模块:task_struct的进程状态------state字段

上篇提到了task_struct结构体中的一个关键字段:state,用于表示操作系统内核调度过程中的状态。

这一篇就进一步的来了解下内核的调度器的一些内容:

c 复制代码
const struct sched_class	*sched_class;
struct sched_entity		se;
struct sched_rt_entity		rt;
struct sched_dl_entity		dl;

进程调度器

这一篇就来说一说sched_class这个指针变量。

sched_class结构体的定义

进程调度器负责管理内核对于进程的调度,也就是struct sched_class结构体。这个结构体定义在kernel/sched/sched.h中。

部分关键成员:

c 复制代码
struct sched_class {
	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
	void (*yield_task)   (struct rq *rq);
	bool (*yield_to_task)(struct rq *rq, struct task_struct *p);
	void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
	struct task_struct *(*pick_next_task)(struct rq *rq);
}

从结构体的定义来看,sched_class的最主要的成员变量就是几个函数指针:

  • enqueue_task,进程入队(就绪态)。当进程从 "非就绪态" 转为 "就绪态"(TASK_RUNNING)时,调度器调用该函数,将进程加入对应调度器类的就绪队列(如 CFS 的红黑树、RT 的优先级队列),并更新队列统计信息。
    在下面的一些场景中会被调用到:
    • 进程被唤醒(如wake_up_process());
    • 进程从其他 CPU 迁移到当前 rq(负载均衡);
    • 进程主动放弃 CPU 后重新就绪(如sched_yield())。
  • dequeue_task:进程出队(退出就绪态)。当进程退出 "就绪态"(如阻塞、退出、迁移到其他 CPU)时,调度器调用该函数,将进程从对应调度器类的就绪队列中移除,并更新队列统计信息。
    在下面的一些场景中会被调用到:
    • 进程调用sleep()/wait()进入阻塞态;
    • 进程执行完毕退出(exit());
    • 进程被迁移到其他 CPU 的 rq(负载均衡)。
  • yield_task:进程主动让出 CPU。当进程调用sched_yield()主动放弃 CPU 时,调度器调用该函数,将进程重新放入就绪队列的 "末尾"(降低其调度优先级),并触发重新调度。
    • 进程执行非紧急任务时,主动让渡 CPU 给其他进程(如后台批处理任务)。
  • pick_next_task:选择下一个要运行的进程。调度器执行schedule()时,调用该函数从当前 rq 的就绪队列中,选出 "最应该运行" 的进程(调度器的核心决策逻辑)。
    • 内核时钟中断触发调度;
    • 进程主动放弃 CPU;
    • 高优先级进程被唤醒触发抢占。

内核中定义的几个默认调度器

基于上面的结构体,内核定义了多个调度器,每个调度器的调度规则不一样(也在kernel/sched/sched.h):

这几个默认调度器都是通过定义自己的函数指针来实现不同的调度策略

c 复制代码
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
  • 相当于在task_struct中的sched_class *sched_class指针变量,指向的就是某个特定的默认调度器类型,比如CFS调度器。
  • 而且,这个变量是const类型的,也就是在task_struct结构体初始化的时候就指定了。具体如何分配调度器类型的,以后再说。

一个宏定义

在这个头文件中还增加了一个宏定义:

c 复制代码
/*
 * Helper to define a sched_class instance; each one is placed in a separate
 * section which is ordered by the linker script:
 *
 *   include/asm-generic/vmlinux.lds.h
 *
 * Also enforce alignment on the instance, not the type, to guarantee layout.
 */
#define DEFINE_SCHED_CLASS(name) \
const struct sched_class name##_sched_class \
	__aligned(__alignof__(struct sched_class)) \
	__section("__" #name "_sched_class")

在对应的每个调度器的.c文件中就使用了这个宏定义,相当于实现了c++代码中的类成员的定义:

DEFINE_SCHED_CLASS(fair) = {

复制代码
.enqueue_task		= enqueue_task_fair,
.dequeue_task		= dequeue_task_fair,
.yield_task		= yield_task_fair,
.yield_to_task		= yield_to_task_fair,

.check_preempt_curr	= check_preempt_wakeup,

.pick_next_task		= __pick_next_task_fair,
.put_prev_task		= put_prev_task_fair,
.set_next_task          = set_next_task_fair,

}

几个调度器的优先级与对应的作用场景

调度器类按优先级链表遍历:stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → idle_sched_class。

搜了一下这几个调度器主要对那些进程进行调度:

  1. stop_sched_class:停机调度器(最高优先级)

    (1)核心定位

    处理内核最紧急的 CPU 专属任务------ 这类任务必须 "独占 CPU、不被任何进程抢占",甚至能抢占实时进程。

    适用场景:CPU 热插拔(如关闭某个 CPU)、核间中断(IPI)处理、调度器调试等内核核心操作;

    任务特征:每个 CPU 最多只有 1 个stop类任务,且是 "CPU 绑定" 的(只能在指定 CPU 上运行)。

  2. dl_sched_class:截止时间调度器(次高优先级)

    (1)核心定位

    处理硬实时任务(Deadline Scheduling,EDF 算法)------ 这类任务有 "截止时间" 要求(如必须在 10ms 内完成),内核需保证任务在截止时间前执行完毕。

    适用场景:工业控制、机器人、自动驾驶等 "硬实时" 场景(如传感器数据处理、执行器控制);

    启用条件:内核编译时需开启CONFIG_SCHED_DEADLINE(5.15 默认可能未开启)。

  3. rt_sched_class:实时调度器(第三优先级)

    (1)核心定位

    处理软实时任务(SCHED_FIFO/SCHED_RR 策略)------ 这类任务有固定优先级(0~99),优先级高于普通进程,但低于dl/stop类任务。

    适用场景:音频播放、视频编码、服务器高优先级任务等 "软实时" 场景(允许少量延迟,但需优先执行);

    两种策略:

    SCHED_FIFO:先进先出,高优先级 FIFO 任务一直运行,直到主动放弃或被更高优先级任务抢占;

    SCHED_RR:时间片轮转,同优先级 RR 任务轮流执行(避免同优先级任务饥饿)。

  4. fair_sched_class:

    (1)核心定位

    处理普通非实时任务(SCHED_NORMAL/SCHED_OTHER/SCHED_IDLE/SCHED_BATCH 策略)------ 这类任务无固定实时优先级,通过 "虚拟运行时间(vruntime)" 实现 CPU 时间的公平分配,优先级低于 stop/dl/rt 类任务,是 Linux 系统中最核心、最常用的调度器类(绝大多数用户进程 / 后台进程都由它管理)。

    (2)适用场景

    日常所有非实时业务场景:如终端命令执行、Web 服务(Nginx/Redis)、数据库(MySQL)、桌面应用、后台批处理任务等 ------ 这类场景不要求 "毫秒级响应",但需要保证多个进程公平占用 CPU,避免单个进程长期独占资源导致其他进程饥饿。

  5. idle_sched_class:空闲调度器(第五优先级 / 最低优先级)

    (1)核心定位

    处理CPU 空闲线程(swapper 线程) ------ 无专属调度策略,是调度器的 "兜底逻辑",仅当 CPU 上 stop/dl/rt/fair 类均无任何可运行进程时才会被触发,优先级低于所有其他调度器类。

    (2)适用场景

    CPU 完全空闲的场景:当系统中没有任何需要执行的任务(包括内核紧急任务、实时任务、普通进程)时,执行 idle 线程,核心目标是让 CPU 进入低功耗状态,避免 "空转" 浪费能耗,同时保证任何进程就绪时能立刻让出 CPU。

后面估计要花蛮多篇幅来记录这些调度器的内容。今天先试着输出操作系统中一些进程的调度器类型吧。

内核模块输出进程类型

下面代码有几个需要注意的地方

  • 里面的一个宏定义:for_each_process是在linux/sched/signal.h头文件中。
  • sched_class stop_sched_class,这些默认的调度器,一般在内核中是不对外导出的,所以就算增加了extern 符号也没用。需要修改内核代码,重新导出才行。这个这次没有尝试。
  • 可以使用task_struct结构体的另外一个字段:policy来间接的展示每个进程的调度情况。

policy字段说明

  • policy是进程 "调度策略的直接标识",内核通过它:
  • 区分进程是普通进程、实时进程还是空闲进程;
  • 决定进程归属于哪个调度器类(fair/rt/dl);
  • 确定调度器类的具体调度逻辑(如 RT 类的 FIFO/RR)。

policy与调度器之间的对应关系,这些宏定义在linux/sched.h中,所以可以通过这样一个对应关系来展示每个进程的调度器情况。

上代码和结果

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/rculist.h>
#include <linux/sched/signal.h>

// 关键:手动声明5.15内核中各调度器类的外部引用(匹配内核定义)
// 注:struct sched_class的定义在<linux/sched.h>中已包含,只需声明全局变量
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;

// 模块参数:指定要查询的PID,默认-1(遍历进程)
static int pid = -1;
module_param(pid, int, 0644);
MODULE_PARM_DESC(pid, "PID of the process to check (default: -1 for traverse)");

// 核心函数:将sched_class转换为可读字符串(包含stop_sched_class)
// 需要重新编译内核,后续再尝试,因为之前修改内核之后重启不好使。
/*
static const char *sched_class_to_name(const struct sched_class *cls) {
    if (cls == &stop_sched_class)  return "STOP (内核紧急任务)";
    if (cls == &dl_sched_class)    return "DL (硬实时)";
    if (cls == &rt_sched_class)    return "RT (软实时)";
    if (cls == &fair_sched_class)  return "FAIR (普通公平)";
    if (cls == &idle_sched_class)  return "IDLE (空闲兜底)";
    return "UNKNOWN (未知)";
}
*/

// 核心函数:通过公开字段间接判断调度器类型(无符号依赖)
static const char *get_sched_class_name(struct task_struct *p) {
    // 1. IDLE调度器类:PID=0 或 comm以swapper开头(5.15内核固定特征)
    if (task_pid_nr(p) == 0 || strstr(p->comm, "swapper") != NULL) {
        return "IDLE (空闲兜底)";
    }
    // 2. STOP调度器类:内核migration线程(comm以migration开头)
    if (strstr(p->comm, "migration") != NULL) {
        return "STOP (内核紧急任务)";
    }
    // 3. DL调度器类:SCHED_DEADLINE策略(policy=6)
    if (p->policy == SCHED_DEADLINE) {
        return "DL (硬实时)";
    }
    // 4. RT调度器类:SCHED_FIFO(1)/SCHED_RR(2)策略
    if (p->policy == SCHED_FIFO || p->policy == SCHED_RR) {
        return "RT (软实时)";
    }
    // 5. FAIR调度器类:剩余所有情况(普通进程)
    return "FAIR (普通公平)";
}

// 极简遍历:输出前100个进程的调度器类型
static void traverse_process_sched_class(void) {
    struct task_struct *p;
    int count = 0;
    
    printk(KERN_INFO "===== 系统进程调度器类型(前20个)=====\n");
    rcu_read_lock();
    
    // 遍历系统进程(内核通用宏)
    for_each_process(p) {
        if (count++ >= 20) break; // 仅输出前100个,避免刷屏
        printk(KERN_INFO "[PID %d] %s → %s\n",
               task_pid_nr(p), p->comm, get_sched_class_name(p));
    }
    
    rcu_read_unlock();
    printk(KERN_INFO "===== 遍历结束 =====\n");
}

// 模块初始化函数
static int __init sched_class_check_init(void) {
    if (pid > 0) {
        print_process_sched_class(pid);
    } else {
        traverse_process_sched_class();
    }
    return 0;
}

// 模块退出函数
static void __exit sched_class_check_exit(void) {
    printk(KERN_INFO "模块卸载完成\n");
}

module_init(sched_class_check_init);
module_exit(sched_class_check_exit);

MODULE_LICENSE("GPL"); // 必须声明GPL,否则无法访问EXPORT_SYMBOL_GPL的符号
MODULE_DESCRIPTION("极简输出进程调度器类型(含stop_sched_class)");
MODULE_AUTHOR("Your Name");

输出结果:

bash 复制代码
40696.540310] ===== 系统进程调度器类型(前20个)=====
[40696.540311] [PID 1] systemd → FAIR (普通公平)
[40696.540313] [PID 2] kthreadd → FAIR (普通公平)
[40696.540314] [PID 3] rcu_gp → FAIR (普通公平)
[40696.540346] [PID 4] rcu_par_gp → FAIR (普通公平)
[40696.540348] [PID 5] slub_flushwq → FAIR (普通公平)
[40696.540349] [PID 6] netns → FAIR (普通公平)
[40696.540350] [PID 8] kworker/0:0H → FAIR (普通公平)
[40696.540351] [PID 10] mm_percpu_wq → FAIR (普通公平)
[40696.540353] [PID 11] rcu_tasks_rude_ → FAIR (普通公平)
[40696.540354] [PID 12] rcu_tasks_trace → FAIR (普通公平)
[40696.540356] [PID 13] ksoftirqd/0 → FAIR (普通公平)
[40696.540357] [PID 14] rcu_sched → FAIR (普通公平)
[40696.540358] [PID 15] migration/0 → STOP (内核紧急任务)
[40696.540360] [PID 16] idle_inject/0 → RT (软实时)
[40696.540361] [PID 18] cpuhp/0 → FAIR (普通公平)
[40696.540362] [PID 19] cpuhp/1 → FAIR (普通公平)
[40696.540363] [PID 20] idle_inject/1 → RT (软实时)
[40696.540365] [PID 21] migration/1 → STOP (内核紧急任务)
[40696.540366] [PID 22] ksoftirqd/1 → FAIR (普通公平)
[40696.540367] [PID 24] kworker/1:0H → FAIR (普通公平)
[40696.540368] ===== 遍历结束 =====

从结果可以看到,大部分普通进程都是通过CFS的FAIR调度器来调度的

相关推荐
@YDWLCloud2 小时前
华为云国际版 vs 阿里云国际版:东南亚市场选型指南
大数据·服务器·阿里云·华为云·云计算
快乐的划水a2 小时前
上下文简析
linux·运维·服务器
HABuo2 小时前
【linux进程控制(一)】进程创建&退出-->fork&退出码详谈
linux·运维·服务器·c语言·c++·ubuntu·centos
EndingCoder2 小时前
高级类型:联合类型和类型别名
linux·服务器·前端·ubuntu·typescript
2301_765715142 小时前
Linux中组合使用多个命令的技巧与实现
linux·运维·chrome
想唱rap2 小时前
MySQL内置函数
linux·运维·服务器·数据库·c++·mysql
Jet_582 小时前
Ubuntu 桌面版 Wireshark 抓包权限不足问题解决指南
linux·ubuntu·wireshark
wit_yuan2 小时前
openbmc 支持mctp over pcie(三)(支持作为endpoint)
linux·服务器·嵌入式硬件
wait_luky2 小时前
NFS服务器
linux·服务器·网络