Linux的进程调度实现

news2025/2/27 7:29:59

        经常被问到进程的调度算法有哪些,什么先进先出、短进程优先、时间片轮转、多级反馈多列等等算法能说一大堆?那具体的,linux内核使用了什么样的算法,且来探究一下。

        本文所引用源码基于linux内核2.6.34版本。

目录

调度器类

从 schedule() 开始

pick_next_task()

三种调度器类 

完全公平调度算法

时间记账

公平的pick_next_task()

总结


调度器类

        首先要明确的是,linux并不仅仅使用一种调度算法,而是多种。具体的,内核中以调度器类的方式提供多种调度算法。这样做的目的是为了让不同类型的进程有选择性的使用不同的调度算法。

从 schedule() 开始

        schedule() 是内核调度的入口函数。例如当一个进程调用read之类的阻塞方法而数据没有就绪时,内核就会通过 schedule() 函数触发调度,将自己挂到等待队列中,寻找一个新进程去运行。schedule() 的实现在 kernel/sched.c 文件中,它会停止当前正在运行的进程,并找到下一个待运行的进程去调度执行。

void __sched schedule(void)
{
    // 清除当前进程的重调度标识
	clear_tsk_need_resched(prev);

    // 将旧进程从运行队列中移除
	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
		if (unlikely(signal_pending_state(prev->state, prev)))
			prev->state = TASK_RUNNING;
		else
			deactivate_task(rq, prev, 1);
		switch_count = &prev->nvcsw;
	}

    // 调度前的处理钩子
	pre_schedule(rq, prev);

    // 将当前进程放回运行or等待队列,并选择下一个要运行的进程
	put_prev_task(rq, prev);
	next = pick_next_task(rq);

    // 执行上下文切换及更新统计信息
	if (likely(prev != next)) {
		sched_info_switch(prev, next);
		perf_event_task_sched_out(prev, next);

		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		context_switch(rq, prev, next); /* unlocks the rq */
	} else
		raw_spin_unlock_irq(&rq->lock);

    // 调度后的处理钩子
	post_schedule(rq);
}

        schedule() 虽然函数体比较长,但其做的事情比较简单,最重要的事情就是 pick_next_task() 和 context_switch() 啦。我们需要具体看一下它是怎么pick next的~

pick_next_task()

        pick_next_task() 会根据优先级,从高到低依次检查每一个调度器类,并且从最高优先级的一个调度器类中,选择最高优先级的一个进程出来。

/*
 * 选择优先级最高的task
 */
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
	const struct sched_class *class;
	struct task_struct *p;

	/*
	 * 优化:如果所有的运行态的任务都属于cfs调度器的运行队列
     * 那么这里就可以直接调用cfs调度器的方法
	 */
	if (likely(rq->nr_running == rq->cfs.nr_running)) {
        // fair_sched_class:cfs调度器
		p = fair_sched_class.pick_next_task(rq);
		if (likely(p))
			return p;
	}

    // sched_class_highest:取最高优先级的调度器类
	class = sched_class_highest;
	for ( ; ; ) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
		/*
		 * 永远不会走到这里,因为idle调度器总会返回一个task
         */
		returns a non-NULL p:

		class = class->next;
	}
}

        调度器类 sched_class 的定义如下,其中定义了一个指向下一优先级调度器类的一个指针,以及多个函数指针,分别用以执行不同的操作。这也是kernel中使用面向对象思想的体现。

struct sched_class {
    // 指向下一个sched_class的指针,用于支持调度类的层次结构
	const struct sched_class *next;

    // 将任务(进程或线程)添加到运行队列
	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup,
			      bool head);
    // 从运行队列中移除任务
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
    // 使当前任务放弃CPU,让其他任务有机会运行
	void (*yield_task) (struct rq *rq);
    // 检查当前任务是否应该被抢占(即被其他任务中断)
	void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
    
    // 从运行队列中选择下一个要运行的任务
	struct task_struct * (*pick_next_task) (struct rq *rq);
    // 将前一个任务(即刚刚被抢占或完成的任务)放回运行队列
	void (*put_prev_task) (struct rq *rq, struct task_struct *p);

    // 设置当前任务为运行队列的当前任务
	void (*set_curr_task) (struct rq *rq);
    ...
};

三种调度器类 

     根据 next 指针的指向,很容易找到 kernel 支持的几种调度器类:

  1. rt_sched_class:实时调度器类,优先级最高:分为RR算法、FIFO算法。RR为一种基于时间片的FIFO算法,按入队顺序先进先出的执行。FIFO即为简单的先进先出算法。
  2. fair_sched_class:完全公平(cfs)调度器类,完全公平调度算法。
  3. idle_sched_class:空闲调度器类,优先级最低,只有当系统中没有其它优先级更高的任务时,才会调度到。

        根据调度器器的种类,也就能看出,从调度的角度来讲,kernel 将进程分为三种类型,分别为实时进程、普通进程以及空闲进程。通过 ps 命令可以查看 linux 环境中不同进程的优先级及采用的调度算法:

LC0:~$ ps -eo pid,cls,sched,pri,comm | grep "FF\|RR\|IDL"
     16  FF   1 139 migration/0
     17  FF   1  90 idle_inject/0
     39  FF   1  90 watchdogd
   1776 IDL   5   0 tracker-miner-f
LC0:~$ ps -eo pid,cls,sched,pri,comm | grep "TS" | head -n1
      1  TS   0  19 systemd
LC0:~$ 

        其中 FF 为实时调度算法中的FIFO;TS 表示 "Time Sharing",即为时间共享调度策略,也即 CFS 完全公平调度算法,旨在公平地分配CPU时间给所有进程;IDL 即空闲调度策略。由于 IDL 调度器只是用来在无可运行进程调度时使用,所以其只管理一个 idle 进程,并没有使用到运行队列。

完全公平调度算法

        CFS 调度算法的设计基于一个简单的理念而来:进程调度的效果应该如同系统具备一个理想中的完美多任务处理器。在这种系统中,每个task均能公平的获得 (1/可运行进程数) 的处理器时间比。

        理论上,如果能把时间片分隔为无限小,那么就可以做到平均分配给每个进程相同的运行时间,也就能做到完全公平。然而实际上,一方面cpu的时钟周期不是无限小的,并且任务的切换是有代价的,时间片粒度细到一定程度,CPU 只能忙于切换进程的上下文,而无暇执行实际的任务了。

        现实如何?CFS 充分考虑了这种切换带来的开销,允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。这里的运行最少也不是简单的根据时间片来计算了,而是一个时间片的加权值:处理器时间比。nice 值在 CFS 中被作为进程获得处理器运行比的权重:nice值越高,进程可获得的处理器时间比也就越小,反之。

时间记账

        即使 CFS 不是按时间片,而是按时间比来调度进程,但其仍然必需维护每个进程运行的时间记账,并据此确保每个进程只在分配给它的处理器时间内运行。

        在 linux/sched.h 中定义了时间记账的结构体——调度实体:

struct sched_entity {
    // 实体的负载权重,用于调度决策
	struct load_weight	load;
    // 红黑树节点,用于快速查找和插入调度实体
	struct rb_node		run_node;
    // 链表节点,将实体组织成链表
	struct list_head	group_node;
    // 指示实体是否在调度队列上
	unsigned int		on_rq;
    // 记录实体开始执行的时间
	u64			exec_start;
    // 记录实体执行的总时间
	u64			sum_exec_runtime;
    // 虚拟运行时间,即加权后的执行时间
	u64			vruntime;
    // 上一次计算时的 sum_exec_runtime 值
	u64			prev_sum_exec_runtime;
    // 实体最后一次被唤醒的时间
	u64			last_wakeup;
    ...
};

        调度实体 sched_entity 这个结构的指针,存放在 task_strcut 中。sched_entity比较重要的变量则是 vruntime,它表示进程执行的加权后的时间,CFS 用这个 vruntime 帮助实现逼近理想多任务处理器的完美公平调度。

        记账功能通过定时器定时调用 update_curr() 实现:

static void update_curr(struct cfs_rq *cfs_rq)
{
	// 获得最后一次修改load后当前任务所占用的运行总时间
	delta_exec = (unsigned long)(now - curr->exec_start);

	__update_curr(cfs_rq, curr, delta_exec);
	curr->exec_start = now;

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cpuacct_charge(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}
}

        update_curr() 计算出当前进程的运行时间,保存到了 delta_exec 变量中。然后调用了 __update_curr() 。__update_curr() 根据当前可运行进程总数对运行时间进行加权计算,结果累加到 vruntime 中。

static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
	      unsigned long delta_exec)
{
	curr->sum_exec_runtime += delta_exec;
	delta_exec_weighted = calc_delta_fair(delta_exec, curr);
	curr->vruntime += delta_exec_weighted;
    ...
}

static inline unsigned long
calc_delta_fair(unsigned long delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);

	return delta;
}

// delta *= weight / lw
static unsigned long
calc_delta_mine(unsigned long delta_exec, unsigned long weight,
		struct load_weight *lw)
{
    // 近似计算出 lw->weight的倒数 + 1
	if (!lw->inv_weight) {
		if (BITS_PER_LONG > 32 && unlikely(lw->weight >= WMULT_CONST))
			lw->inv_weight = 1;
		else
			lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2)
				/ (lw->weight+1);
	}

	tmp = (u64)delta_exec * weight;
    // SRR为对tmp低32位四舍五入并只保留tmp的高32位
	tmp = SRR(tmp * lw->inv_weight, WMULT_SHIFT);

	return (unsigned long)min(tmp, (u64)(unsigned long)LONG_MAX);
}

公平的pick_next_task()

        CFS 会挑选一个 vruntime 最小的进程来作为下一个运行的进程,它通过红黑树来组织可运行进程队列,并利用其快速查找最小 vruntime 的节点。pick_next_task_fair() 是 CFS 调度器注册到调度器类中的 pick_next_task() 钩子函数。

static struct task_struct *pick_next_task_fair(struct rq *rq)
{
	do {
		se = pick_next_entity(cfs_rq);
		set_next_entity(cfs_rq, se);
		cfs_rq = group_cfs_rq(se);
	} while (cfs_rq);

	p = task_of(se);
	return p;
}

// pick_next_entity中调用__pick_next_entity
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
	struct rb_node *left = cfs_rq->rb_leftmost;

	if (!left)
		return NULL;

	return rb_entry(left, struct sched_entity, run_node);
}

        最小 vruntime 的节点,其实就是对应红黑树中最左侧的那个叶子节点。__pick_next_entity找到这个最左节点并返回了调度实体,pick_next_task_fair() 中通过 task_of() 找到来调度实体所属的 task_struct。

总结

        linux实现了三种不同的调度器类:实时调度器、完全公平调度器、空闲调度器,分别对应实时进程、普通进程、空闲进程。实际环境中大多数进程都是普通进程,即采用完全公平调度算法。完全公平调度算法调度程序取决于运行程序消耗了多少处理器使用比,如果消耗的使用比比当前进程小,那么新进程会立刻投入运行,抢占当前进程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1504655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Java安全入门]三.CC1链

1.前言 Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。Commons Collections触发反序列化漏洞构造的链叫做cc链,构造方式多种,这里先学习cc1链…

C#,数值计算,解微分方程的龙格-库塔二阶方法与源代码

1 微分方程 含有导数或微分的方程称为微分方程,未知函数为一元函数的微分方程称为常微分方程。 微分方程的阶数 微分方程中导数或微分的最高阶数称为微分方程的阶数。 微分方程的解 使得微分方程成立的函数称为微分方程的解。 微分方程的特解 微分方程的不含任意…

SQLite3中的callback回调函数注意的细节

调用 sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)该例程提供了一个执行 SQL 命令的快捷方式, SQL 命令由 sql 参数提供,可以由多个 SQL 命令组成。 在这里, 第一个参数 sqlite3 是打开的数据库对…

Windows下同一电脑配置多个Git公钥访问不同的账号

前言 产生这个问题的原因是我在Gitee码云上有两个账号,为了方便每次不用使用http模式推拉代码,于是我就使用了ssh的模式,起初呢我用两台电脑分别连接两个账号,用起来也相安无事,近段时时间台式机在家里,我在外地出差了,就想着把ssh公钥同时添加到不同的账号里,结果却发现不能用…

芯片顶级盛会Hotchips2019年-AI的崛起之年-未来芯片论坛及资料下载

提示:下载链接在文章最后。 是什么? HOTCHIPS是一个关于计算机体系结构和电子设计的会议,主要探讨芯片设计、存储器、能源效率、机器学习和人工智能等方面的发展。该会议每年都会召开一次,吸引着来自世界各地的专业人士和研究人…

垃圾回收:JavaScript内存管理的利器

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

数据库中 SQL Hint 是什么?

前言 最近在调研业界其他数据库中 SQL Hint 功能的设计和实现,整体上对 Oracle、Mysql、Postgresql、 Apache Calcite 中的 SQL Hint 的设计和功能都进行了解,这里整理一篇文章来对其进行梳理,一是帮助自己未来回顾,加深自己的思…

全国月平均水汽压空间分布数据、月均降水量空间分布数据、月均太阳辐射空间分布数据、月均风速空间分布数据

我国幅员辽阔,地形复杂,位于亚欧大陆东部,太平洋西岸。气候特征为:季风气候明显,大陆性气候强,气候类型复杂多样, 水热 同期。我国降水量在空间上,东多西少,南多北少;从东…

鸿蒙培训开发:就业市场的新热点~

金三银四在即,随着春节假期结束,各行各业纷纷复工复产,2024年的春季招聘市场也迎来了火爆的局面。最近发布的《2024年春招市场行情周报(第一期)》显示,尽管整体就业市场仍处于人才饱和状态,但华…

软考高级:信息系统开发方法1(原型法、结构法等)概念和例题

作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

JavaWeb基础入门——(二)MySQL数据库基础(2-SQL 结构化查询语言)

四、MySQL逻辑结构 4.1 逻辑结构 4.1 记录 五、SQL 结构化查询语言 5.1 SQL概述 SQL(Structural Query Language)结构化查询语言,用于存取、查询、更新数据以及管理关系型数据库系统 5.1.1 SQL发展 SQL是在1981年由IBM公司推出,…

Qt 拖动事件

文章目录 1 自定义控件 TextEdit2 实现打开文件功能3 实现鼠标滚轮放大字体 QEvent::DragEnter 当拖动文件进入到窗口/控件中时,触发该事件,它对应的子类是QDragEnterEvent QEvent::DragLeave 当拖动文件离开窗口/控件时,触发该事件&#xff…

20、电源管理入门之Hypervisor中的电源管理

目录 1. Hypervisor概念介绍 2. 汽车软件中的Hypervisor应用 3. QNX Hypervisor 4. Hypervisor中的多OS通信技术 5. 电源管理相关 参考: 很多时候听说Hypervisor,但是对底层软件技术不了解的人感觉挺神秘。本篇文章简单介绍下Hypervisor的基本概念,另外介绍下电影管理…

多数码管(Arduino)

图示 /*该程序的作用是演示动态扫描原理。在线文档:https://docs.geeksman.com/esp32/Arduino/08.esp32-arduino-4-digits-7segment.html */// 定义位选线引脚 int seg_1 5; int seg_2 18; int seg_3 19; int seg_4 21;// 定义位选线数组 int seg_array[4] {se…

ABAP接口部分-C#调用RFC

目录 ABAP接口部分-C#调用RFC创建表结构创建RFC函数创建C#项目引用SAP .Net Connector包绘制窗口的控件最终布局代码 项目配置报错SAP.Middleware.Connector.RfcDestinationManager报错SAP.Middleware.Connector.RfcLoginexception报错SAP.Middleware.Connector.RfcInvalidStat…

LLM 推理优化探微 (3) :如何有效控制 KV 缓存的内存占用,优化推理速度?

编者按: 随着 LLM 赋能越来越多需要实时决策和响应的应用场景,以及用户体验不佳、成本过高、资源受限等问题的出现,大模型高效推理已成为一个重要的研究课题。为此,Baihai IDP 推出 Pierre Lienhart 的系列文章,从多个…

WPF 窗口添加投影效果Effect

BlurRadius&#xff1a;阴影半径 Color&#xff1a;颜色 Direction&#xff1a;投影方向 ShadowDepth&#xff1a;投影的深度 <Window.Effect><DropShadowEffect BlurRadius"10" Color"#FF858484" Direction"300" ShadowDepth&quo…

应对高并发的软件架构之道

在去年年终总结的时候&#xff0c;我提出了这样的困惑&#xff0c;究竟什么是真正的技术能力&#xff0c;是对于各种底层技术的钻研吗&#xff1f;钻研是好事&#xff0c;但实践下来&#xff0c;深入钻研并不在实际工作中有用&#xff0c;且钻研的越深&#xff0c;忘得越快&…

深入分析Java线程池——ThreadPoolExecutor

文章目录 Java 线程池概述ThreadPoolExecutor 构造方法线程池拒绝策略工作流程并发库中的线程池CachedThreadPoolFixedThreadPoolSingleThreadExecutorScheduledThreadPool ThreadPoolExecutor 源码分析线程池状态表示获取 runState获取 workerCount生成 ctl 提交任务 execute(…

图像处理与图像分析—图像的读入(C语言)

学习将会依据教材图像处理与图像分析基础&#xff08;C/C&#xff09;版内容展开 什么是数字图像处理 一副图像可以定义为一个二维函数 f(x&#xff0c;y) &#xff0c;其中 x 和 y 是空间&#xff08;平面&#xff09;坐标&#xff0c;任意一对空间坐标 (x,y) 处的幅度值 &am…