当前位置: 首页 > news >正文

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使用权的交换。

显示简
http://www.dtcms.com/a/389313.html

相关文章:

  • 使用BeanUtils返回前端为空值?
  • Windows Server数据库服务器安全加固
  • Linux TCP/IP调优实战,性能提升200%
  • Amazon ElastiCache:提升应用性能的云端缓存解决方案
  • 查找并替换 Excel 中的数据:Java 指南
  • 多线服务器具体是指什么?
  • Golang语言基础篇001_常量变量与数据类型
  • pytest文档1-环境准备与入门
  • MySQL 专题(四):MVCC(多版本并发控制)原理深度解析
  • 【开发者导航】在终端中运行任意图形应用:term.everything
  • [Python]pytest是什么?执行逻辑是什么?为什么要用它测试?
  • Nginx set指令不能使用在http块里,可以使用map指令
  • LeetCode 1759.统计同质子字符串的数目
  • 揭秘Linux文件管理与I/O重定向核心
  • 【PyTorch】DGL 报错FileNotFoundError: Cannot find DGL C++ graphbolt library
  • Autoware不同版本之间的区别
  • 多轮对话-上下文管理
  • 在阿里云私网服务器(无公网IP)上安装 Docker 环境的完整指南
  • opencv DNN模块及利用实现风格迁移
  • 多层感知机:从感知机到深度神经网络的演进
  • centos7 docker compose 安装redis
  • ⸢ 肆-Ⅱ⸥ ⤳ 风险发现体系的演进(下):实践与演进
  • 18兆欧超纯水抛光树脂
  • 第三篇:C++的进化之旅:从C with Class到C++20
  • 机器视觉的手机FPC丝印应用
  • 在Windows上使用Claude Code并集成到PyCharm IDE的完整指南
  • MoPKL与SPAR的思考
  • Ubuntu 启动分配不到 ip 地址问题
  • iOS 推送证书配置 - p12
  • Qt QVPieModelMapper详解