Linux的进程调度及内核实现

本文主要介绍Linux进程调度及内核实现,顺序如下:进程调度的关键数据结构->调度的不同方式->调度触发方式和进程间切换过程

一、进程管理的关键数据结构

1.1 task_struct

    task_struct是内核描述进程的结构体,包括了调度策略、优先级等信息

struct task_struct {
    // 1. 调度策略:决定进程属于哪种调度类(实时/公平/空闲)
    unsigned int policy;          // 如 SCHED_NORMAL(公平)、SCHED_RR(实时)
    // 2. 优先级相关:静态/动态/实时优先级
    int static_prio;              // 静态优先级(用户通过nice设置,100-139)
    int normal_prio;              // 常规优先级(由static_prio和policy计算)
    int rt_prio;                  // 实时优先级(0-99,值越小优先级越高)
    int prio;                     // 当前动态优先级(可能因优先级继承临时提升)
    // 3. 调度类:指向进程所属的调度类(如fair_sched_class)
    const struct sched_class *sched_class;
    // 4. 调度实体:封装进程的调度信息(如vruntime、权重)
    struct sched_entity se;       // 公平调度(CFS)的实体
    struct sched_rt_entity rt;    // 实时调度的实体
    // 5. 运行队列关联:进程当前归属的CPU运行队列
    struct rq *on_rq;             // 非NULL表示进程在就绪队列中
    // 6. 抢占标志:是否需要调度(由内核设置,schedule()检查)
    unsigned int flags;           // 含 TIF_NEED_RESCHED(需调度)
    // ... 其他字段(内存、文件、信号等)
};

优先级规则:实时进程>普通进程

实时进程(rt_prio)值越小优先级越高

普通优先级(static_prio)值越小优先级越高

动态优先级(prio):默认等于normal_prio,但会因先级继承临时提升,避免 “优先级反转”

1.2 sched_class调度类

    Linux通过调度类实现对不同调度策略的封装,本质是一个含函数指针的结构体,定义调度的一些核心操作(入队、出队等)

struct sched_class {
    // 1. 调度类名称(如"fair"、"rt")
    const char *name;
    // 2. 入队:将进程加入就绪队列(如进程唤醒后)
    void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);
    // 3. 出队:将进程从就绪队列移除(如进程阻塞、退出)
    void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);
    // 4. 选择下一个要运行的进程(调度器核心逻辑)
    struct task_struct *(*pick_next_task)(struct rq *rq);
    // 5. 切换前准备:将当前进程放回就绪队列(如时间片用完)
    void (*put_prev_task)(struct rq *rq, struct task_struct *p);
    // 6. 检查是否需要抢占当前进程
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
    // ... 其他函数(如任务创建、优先级修改)
};

调度类按优先级从高到低排列如下

stop_sched_class

用于停止CPU

dl_sched_class

用于调度截止时间策略的任务

rt_sched_class

用于调度实时策略的任务

fair_sched_class

用于调度公平调度策略的任务

idle_sched_class

每个CPU上有一个空闲任务,无其他任务运行时会运行该空闲任务

1.3进程状态

二、核心调度策略与算法实现

2.1公平调度(CFS)

    CFS是linux默认调度器,核心思想是假设有N个进程,每个进程都获得1/N的CPU时间,实际实现是通过vruntime计算进程虚拟运行时间,优先调度vruntime最小的进程,确保每个进程的vruntime增长速率与权重匹配。

计算公式为vruntime+=实际运行时间*N/进程权重(N表示系统CPU数量),

CFS调度器采用动态时间片(每个进程运行的时间),时间片长度由调度延迟和进程权重决定

时间片长度=(进程权重/总权重)*调度延迟

调度延迟指所有就绪进程轮询一次的总时间

例如:

单核CPU上有两个权重(weight)相同的进程,总权重为2weight,调度延迟=6ms。

那么每个进程的时间片=(weight/2weight)*6ms=3ms.

CFS采用红黑树数据结构组织可运行的调度实体,键为调度进程的vruntime,最左节点为整棵树中vruntime值最小的节点,即下一个应该被运行的任务。

当任务被唤醒或状态改变时,会被插入到红黑树中,当任务被调度运行或因各种事件离开时,会从树中被移除。


为什么要用红黑树管理调度任务,而不直接用二叉搜索树,是因为红黑树在频繁的动态插入和删除过程中,通过自身规则自动保持树的近似平衡,防止退化成链表,从而保证操作的时间复杂度为O(logn),用二叉搜索树的话,在极端情况下退化成链表,导致操作的时间复杂度变为O(n),使性能不稳定。

2.2实时调度器(RT)

    负责调度SCHED_FIFO和SCHED_RR策略的任务

    SCHED_FIFO采用先进先出实时调度,无时间片概念,进程一旦获得CPU会一直占用直到自己主动放弃或被更高优先级抢占。

    SCHED_RR采用时间片轮转调度,运行时间片(默认 100ms)后,若有同优先级的就绪进程,会被放到该优先级链表末尾,让下一个同优先级进程运行,仍支持被更高优先级的实时进程抢占。

实时调度器为每个优先级维护了一个队列数组,还使用了位图来表示对应优先级的队列是否非空(每个比特位bit表示空或非空二元状态),调度器通过查找位图,可以快速地找到最高优先级的非空队列,然后从该队列的头部取出任务运行。

2.3截止时间调度

每个进程需指定三个参数:

    runtime(每次周期内需要的CPU时间,如10ms)、period(进程的运行周期,如100ms表示每100ms需要10msCPU运行时间)、deadline(截止时间,如周期结束前10ms,值就等于100ms-10ms)。

优先调度截止时间最早的进程,确保进程在截止时间前完成。

三、调度触发方式与进程间切换

调度器不会主动运行,需要通过触发事件触发schedule()(调度主函数)。

3.1主动调度-进程主动放弃CPU

进程阻塞

调用sleep()、wait()、poll()等

进入可中断睡眠态或不可中断睡眠态,内核在进程阻塞前调用schedule()

主动让出CPU

调用sched_yield()

进程仍为就绪态,但会重新被插入就绪队列尾部,触发调度

进程退出

调用exit()

内核调用schedule选择新进程

3.2被动调度-内核强制触发调度

(1)时钟中断

    Linux内核每隔1/HZ秒触发一次时钟中断(例如HZ=1000时,每隔1ms触发一次),时钟中断处理函数timer_interrupt会调用scheduler_tick()

TIF_NEED_RESCHED是一个标志表示当前进程需要进行调度。

(2)进程唤醒

    当进程从睡眠态被唤醒,调用wake_up_process():

将进程状态设为TASK_RUNNING->调用enqueue_task()将进程加入就绪队列->调用check_preempt_curr()(若唤醒的进程优先级高于当前运行进程或CFS中vruntime更小,设置TIF_NEED_RESCHED)。

(3)优先级变化

    用户通过nice()、renice()、sched_setscheduler()修改进程优先级时,内核会调用resched_curr(),设置TIF_NEED_RESCHED。

3.3调度主函数_schedule()

    _schedule()是调度器的核心函数,主要逻辑如下:

(1)获取当前CPU的运行队列(rq)和当前正在运行的任务(prev)

(2)根据是主动调度还是被动抢占,更新 上下文切换计数(prev->nvcsw或prev->nivcsw)

(3)调用pick_next_task()函数,按照调度类的优先级顺序(stop>dl>rt>fair>idle)选择下一个要运行的进程

(4)如果选出的next任务和当前运行的prev任务不同,则调用context_switch()执行上下文切换

(5)切换完成后,重新开启抢占

3.4上下文切换(context_switch())

    简而言之上下文切换是把当前运行进程的信息保存好,把下一进程的信息拿出来的过程。

上下切换:

页表切换,切换进程的地址空间(CR3寄存器,指向页全局目录PGD),确保CPU访问的是下一个进程的内存

寄存器切换,保存当前进程的栈指针、程序计数器等寄存器到task_struct,恢复下一进程的寄存器,完成CPU使用权的交换。

显示简
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值