Linux的进程调度实现

        经常被问到进程的调度算法有哪些,什么先进先出、短进程优先、时间片轮转、多级反馈多列等等算法能说一大堆?那具体的,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实现了三种不同的调度器类:实时调度器、完全公平调度器、空闲调度器,分别对应实时进程、普通进程、空闲进程。实际环境中大多数进程都是普通进程,即采用完全公平调度算法。完全公平调度算法调度程序取决于运行程序消耗了多少处理器使用比,如果消耗的使用比比当前进程小,那么新进程会立刻投入运行,抢占当前进程。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LLM Drift(漂移), Prompt Drift Cascading(级联)

原文地址:LLM Drift, Prompt Drift & Cascading 提示链接可以手动或自动执行;手动需要通过 GUI 链构建工具手工制作链。自治代理在执行时利用可用的工具动态创建链。这两种方法都容易受到级联、LLM 和即时漂移的影响。 2024 年 2 月 23 日 在讨论大型…

STM32_3-1点亮LED灯与蜂鸣器发声

STM32之GPIO GPIO在输出模式时可以控制端口输出高低电平,用以驱动Led蜂鸣器等外设,以及模拟通信协议输出时序等。 输入模式时可以读取端口的高低电平或电压,用于读取按键输入,外接模块电平信号输入,ADC电压采集灯 GP…

记一次项目所学(中间件等)-动态提醒功能(RocketMQ)

记一次项目所学(中间件等)–动态提醒功能(RocketMQ) 订阅发布模式与观察者模式 RocketMQ:纯java编写的开源消息中间件 高性能低延迟分布式事务 Redis : 高性能缓存工具,数据存储在内存中,读写速度非常快 …

读算法的陷阱:超级平台、算法垄断与场景欺骗笔记05_共谋(中)

1. 默许共谋 1.1. 又称寡头价格协调(Oligopolistic Price Coordination)或有意识的平行行为(Conscious Parallelism) 1.1.1. 在条件允许的情况下,它会发生在市场集中度较高的行业当中 1.1.…

AI智能应用百科立即落地实操课

该课程旨在教授学员如何将AI智能应用于实际场景。通过深入的案例研究和实操练习,学员将学会应用机器学习、自然语言处理等技术,快速解决现实问题。课程强调实际操作,帮助学员快速运用AI技术解决工作中的挑战。 课程大小:3.3G 课…

(关键点检测)YOLOv8实现多类人体姿态估计的输出格式分析

(关键点检测)YOLOv8实现多类人体姿态估计的输出格式分析 任务分析 所使用的数据配置文件 网络结构 导出模型 用 netron 可视化 输出格式分析 参考链接 1. 任务分析 判断人体关键点时一并给出关键点所属的类别,比如男人,女…

Vue3 状态管理 - Pinia

Vue3 状态管理 - Pinia 1. 什么是Pinia Pinia 是 Vue 的专属的最新状态管理库 ,是 Vuex 状态管理工具的替代品 2. 手动添加Pinia到Vue项目 后面在实际开发项目的时候,Pinia可以在项目创建时自动添加,现在我们初次学习,从零开…

EasyPoi 教程

文章目录 EasyPoi教程文档1. 前传1.1 前言 这个服务即将关闭,文档迁移到 http://www.wupaas.com/ 请大家访问最新网站1.2 Easypoi介绍1.3 使用1.4 测试项目1.5 可能存在的小坑 2. Excel 注解版2.1 Excel导入导出2.2 注解注解介绍ExcelTargetExcelEntityExcelCollectionExcelIgn…

[LeetCode][LCR151]彩灯装饰记录 III——队列

题目 LCR 151. 彩灯装饰记录 III 一棵圣诞树记作根节点为 root 的二叉树,节点值为该位置装饰彩灯的颜色编号。请按照如下规则记录彩灯装饰结果: 第一层按照从左到右的顺序记录除第一层外每一层的记录顺序均与上一层相反。即第一层为从左到右&#xff0c…

transformer--使用transformer构建语言模型

什么是语言模型? 以一个符合语言规律的序列为输入,模型将利用序列间关系等特征,输出一个在所有词汇上的概率分布.这样的模型称为语言模型. # 语言模型的训练语料一般来自于文章,对应的源文本和目标文本形如: src1"Ican do",tgt1…

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2

KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记10 - STM32的SDIO学习2 一、问题回顾二、本次的任务三、 需要注意的问题3.1 Card Identification Mode时的时钟频率3.2 CMD0指令的疑似问题3.3 发送带参数的ACMD41时要注意时间时序和时效3.4 CPSM的指令发送问题3.5 调试过程中的SD卡的…

分布式解决方案

目录 1. 分布式ID1-1. 传统方案1-2. 分布式ID特点1-3. 实现方案1-4. 开源组件 1. 分布式ID 1-1. 传统方案 时间戳UUID 1-2. 分布式ID特点 全局唯一高并发高可用 1-3. 实现方案 方案总结: 号段模式 有两台服务器,给第一台服务器分配0-100&#xff0…

嵌入式Linux串口和 poll() 函数的使用

一、poll() 函数的介绍 poll() 函数用于监控多个文件描述符的变化的函数。它可以用来检查一个或多个文件描述符的状态是否改变,比如是否可读、可写或有错误发生。它常用于处理 I/O 多路复用,这在需要同时处理多个网络连接或文件操作时非常有用。 头文件…

Linux高级IO之select

(。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~https://blog.csdn.net/ky233?typeblog 点个关注不迷路⌯▾⌯ 目录 一、五种IO模型 1.IO效率的问题 2.阻塞IO是…

蓝桥杯C/C++实用知识总结

蓝桥杯C/C 文章目录 蓝桥杯C/C头文件实用函数及运算符求幂次移位运算符STL排序sort()函数依次读取数据STL全排列函数next_permutation()求数组最大/最小值初始化函数memset()GCD(最大公约数)和LCM(最小公倍数)C字符串函数 实用数据结构模板vector链表lis…

未来城市:探索数字孪生在智慧城市中的实际应用与价值

目录 一、引言 二、数字孪生与智慧城市的融合 三、数字孪生在智慧城市中的实际应用 1、智慧交通管理 2、智慧能源管理 3、智慧建筑管理 4、智慧城市管理 四、数字孪生在智慧城市中的价值 五、挑战与展望 六、结论 一、引言 随着科技的飞速发展,智慧城市已…

鸿蒙OpenHarmony HDF 驱动开发

目录 序一、概述二、HDF驱动框架三、驱动程序四、驱动配置坚持就有收获 序 最近忙于适配OpenHarmonyOS LiteOS-M 平台,已经成功实践适配平台GD32F407、STM32F407、STM32G474板卡,LiteOS适配已经算是有实际经验了。 但是,鸿蒙代码学习进度慢下…

超网、IP 聚合、IP 汇总分别是什么?三者有啥区别和联系?

一、超网 超网(Supernet)是一种网络地址聚合技术,它可以将多个连续的网络地址合并成一个更大的网络地址,从而减少路由表的数量和大小。超网技术可以将多个相邻的网络地址归并成一个更大的网络地址,这个更大的网络地址…

Lesson 6 Convolutional Neural Network(CNN)

听课(李宏毅老师的)笔记,方便梳理框架,以作复习之用。本节课主要讲了CNN的适用范围,整体架构与工作流程,CNN的应用,CNN的缺点以及解决方法。 1. CNN的输入与输出 CNN是专门为了图像而设计的一…

2.4_3 死锁的处理策略——避免死锁

文章目录 2.4_3 死锁的处理策略——避免死锁(一)什么是安全序列(二)安全序列、不安全状态、死锁的联系(三)银行家算法 总结 2.4_3 死锁的处理策略——避免死锁 银行家算法是“避免死锁”策略的最著名的一个…