Linux调度器及CFS调度器

调度器

​ 调度:就算按照某种调度的算法设计,从进程的就绪队列中部选取进程分配CPU,主要是协调对CPU等等相关资管的使用。进程调度的目的:最大限度利用CPU事件。

如果调度器支持就绪状态切换到执行状态,同时支持执行状态切换到就绪状态,就称该调度器为抢时间调度器。

​ Linux内核中用来安排调度进程(一段程序的执行过程)执行的模块称为调度器(Scheduler),它可以切换进程状态(Process Status)。比如:执行、可中断操作、退出、暂停等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wc2XEkn9-1663493633165)(image/3-1.png)]

调度器相当于CPU中央处理器的管理员,主要负责完成做一下两件事:

  • 选择某些就绪程序来执行
  • 打断某些执行的进程让他们变成就绪状态

调度器分配CPU时间的基本依据就是进程的优先级。上下文 切换(context switch):将进程在CPU中切换执行的过程,内核承担
此任务,负责重建和存储被切换掉之前的CPU状态。

调度器类sched_class结构体

​ Linux内核抽象一个调度器类struct sched_class结构体表示调度类,具体内核源码如下:

struct sched_class {
    //当系统中有多个调度类时,按照调度优先级排成一个链表,下一优先级的高类
	const struct sched_class *next;

#ifdef CONFIG_UCLAMP_TASK
	int uclamp_enabled;
#endif

    //将进程加入到执行队列中,即将调度实体放到红黑树中,并对nr_running变量自动加1
	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
    //从执行队列中删除进程,并对nr_running变量自动减1
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
    //放弃CPU执行权,实际上该函数执行先出队后入队,在这种情况下,它直接将调度实体放到红黑树的最右端
	void (*yield_task)   (struct rq *rq);
	bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);

    //用于检测当前进程是否可被新进程抢占,
	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_next_task)(struct rq *rq, struct task_struct *p, bool first);

#ifdef CONFIG_SMP
	int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
    //为进程选择一个合适的CPU
	int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
     //迁移任务到另一个CPU
	void (*migrate_task_rq)(struct task_struct *p, int new_cpu);

    //专门用于唤醒进程
	void (*task_woken)(struct rq *this_rq, struct task_struct *task);
	
    //修改进程在CPU的亲和力
	void (*set_cpus_allowed)(struct task_struct *p,
				 const struct cpumask *newmask);

    //启动运行队列
	void (*rq_online)(struct rq *rq);
    //禁止运行队列
	void (*rq_offline)(struct rq *rq);
#endif
    //调用time_tick函数,它可能引起进程切换,将驱动运行时抢占
	void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
    //当进程创建的时候使用,不同调度策略的进程初始化也不一样
	void (*task_fork)(struct task_struct *p);
    //进程退出时会使用
	void (*task_dead)(struct task_struct *p);

	/*
	 * The switched_from() call is allowed to drop rq->lock, therefore we
	 * cannot assume the switched_from/switched_to pair is serliazed by
	 * rq->lock. They are however serialized by p->pi_lock.
	 */
	void (*switched_from)(struct rq *this_rq, struct task_struct *task);
	void (*switched_to)  (struct rq *this_rq, struct task_struct *task);
    //更改进程的优先级
	void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
			      int oldprio);

	unsigned int (*get_rr_interval)(struct rq *rq,
					struct task_struct *task);

	void (*update_curr)(struct rq *rq);

#define TASK_SET_GROUP		0
#define TASK_MOVE_GROUP		1

成员解析:

  • enqueue task:向就绪队列添加一个进程,某个任务进入可运行状态时,该函数将会调用,它将调度实体放入到红黑树当中。
  • dequeue_ task:将一个进程从就绪队列中进行删除, 当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应调度实体。
  • yield_ task: 在进程想要资源放弃对处理器的控制权时,可使用在sched_ yiled系统调用,会调用内核API去处理操作。
  • check_ preempt_ curr: 检查当前运行的任务是否被抢占。
  • pick_ next_ task: 选择下来要运行的最合适的实体(进程)。
  • put_ prev_ task:用于另一个进程代替当前运行的进程。
  • set_ curr_ task:当任务修改它调用类或修改它的任务组时,将调用这个函数。
  • task_ _tick: 在每次激活周期调度器时,由周期性调度器调用。

调度器类可以分为:stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class及idle_sched_class

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcaTTLnA-1663493633166)(image/3-2.png)]

这5种调度类的优先级丛高到低依次为:停机调度类、限期调度类、实时调度类公平调度类、空闲调度类。

  • 停机调度类:优先级是最高的调度类,停机进程是优先级最高的进程,可以抢占所有其它进程,其他进程不可能抢占停机进程。
  • 限期调度类:最早使用优先算法,使用红黑树把进程按照绝对截止期限丛小到大排序,每次调度时选择绝对截止期限最小的进程。
  • 实时调度类:为每个调度优先级维护一个队列。
  • 公平调度类:使用完全公平调度算法。完全公平调度算法引入虚拟运行时间的相关概念:虚拟运行时间=实际运行时间*nice0对应的权重/进程的权重。
  • 空闲调度类:每个CPU上有一个空闲线程,即0号线程。空闲调度类优先级别最低,仅当没有其他进程可以调度的时候,才会调度空闲线程。

进程的优先级

​ Linux内核优先级源码如下:

#define MAX_USER_RT_PRIO	100
#define MAX_RT_PRIO		MAX_USER_RT_PRIO

#define MAX_PRIO		(MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO		(MAX_RT_PRIO + NICE_WIDTH / 2)

​ task_ struct结构体中采用三个成员表示进程的优先级: prio和normal_prio表示动态优先级, static_ prio表示进程的静态优先级。
​ 内核将任务优先级划分,实时优先级范围是0到MAX_ RT_ PRIO-1 (即99) ,而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX PRIO-1 (即100到139)。

进程分类:
实时进程(Real-Time Process) :优先级高、需要立即被执行的进程。
普通进程(Normal Process) :优先级低、更长执行时间的进程。
进程的优先级是一个0- 139的整数来表示。数字越小,优先级越高。其中优先级0-99留给实时进程,100-139留给普通进程。

调度策略

​ Linux内核提供一些策略共用户应用程序来选择调度器。Linux内核调度策略源码分析如下:

/*
 * Scheduling policies
 */
#define SCHED_NORMAL		0
#define SCHED_FIFO		1
#define SCHED_RR		2
#define SCHED_BATCH		3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE		5
#define SCHED_DEADLINE		6

/* Can be ORed in to make sure the process is reverted back to SCHED_NORMAL on fork */
#define SCHED_RESET_ON_FORK     0x40000000
  • SCHED_NORMAL:普通进程调度策略,使task选择CFS调度器来调度运行:
  • SCHED_FIFO: 实时进程调度策略,先进先出调度没有时间片,没有更高优先级的状态下,只有等待主动让出CPU;
  • SCHED_RR:实时进程调度策略,时间片轮转,进程使用完时间片之后加入优先级对应运行队列当中的尾部,把CPU让给同等优先级的其它进程:
  • SCHED_BATCH: 普通进程调度策略,批量处理,使task选择CFS调度器来调度运行:
  • SCHED_IDLE: 普通进程调度策略,使task以最低优先级选择CFS调度器来调度运行:
  • SCHED_DEADLINE: 限期进程调度策略,使task选择Deadline调度器来调度运行:

其中Stop调度器和IDLE-task调度器仅使用于内核,用户没有办法进行选择。

CFS调度器

CFS是Completely Fair Scheduler 简称,完全公平调度器。在实际当中必须会有进程优先级高或者进程优先级低,CFS调度器引入权重,使用权重代表进
程的优先级,各个进程按照权重比例分配CPU时间。

完全公平调度算法体现在对待每个进程都是公平的,让每个进程都运行一段相同的时间片,这就是基于时间片轮询调度算法。CFS定义一种新调度模型,它给cfs_rq(cfs的run queue)中的每一个进程都设置一个虚拟时钟-virtual runtime(vruntime)。如果一个进程得以执行,随着执行时间的不断增长,其vruntime也将不断增大,没有得到执行的进程vruntime将保持不变。

实际运行时间

假设有2个进程X和Y,X权重为1024,Y权重为2048。

X获得CPU时间比例为: 1024/ (1024+2048)=33%左右,
Y获得CPU时间比例为: 2048/(1024+2048)=66%左右

在引入权重之后分配给进程的时间计算公式如下:实际运行时间=调度曲*进程权重/所有进程权重之和。

虚拟运行时间

​ 运行时间 = 实际运行时间*NICE 0 LOAD/进程权重= (调度周期进程权重/所有进程权重之后) *NICE 0 LOAD/进程权重=调度周期1024/所有进程总权重。
​ 在一个调度周期里面,所有进程的虚拟运行时间是相同的,所以在进程调度时,只需要找到虚拟运行时间最小的进程调度运行即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7Ktdh6i-1663493633167)(image/3-3.png)]

调度器结构分析

进程调度任务:合理分配CPU赶时间给运行的进程。

调度器目标:有效地分配CPU时间片。

调度器通过各个组件模块及一系列数据结构,来排序和管理系统中的进程。他们之间关系图下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELQMhOHI-1663493633167)(image/3-4.png)]

//Linux内核源码如下
const struct sched_class fair_sched_class = {
	.next			= &idle_sched_class,		//空闲调度类
	.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,

#ifdef CONFIG_SMP
	.balance		= balance_fair,
	.select_task_rq		= select_task_rq_fair,
	.migrate_task_rq	= migrate_task_rq_fair,

	.rq_online		= rq_online_fair,
	.rq_offline		= rq_offline_fair,

	.task_dead		= task_dead_fair,
	.set_cpus_allowed	= set_cpus_allowed_common,
#endif

	.task_tick		= task_tick_fair,
	.task_fork		= task_fork_fair,

	.prio_changed		= prio_changed_fair,
	.switched_from		= switched_from_fair,
	.switched_to		= switched_to_fair,

	.get_rr_interval	= get_rr_interval_fair,

	.update_curr		= update_curr_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED
	.task_change_group	= task_change_group_fair,
#endif

#ifdef CONFIG_UCLAMP_TASK
	.uclamp_enabled		= 1,
#endif
};

调度器管理是各个调度器的职责,CFS的顶级调度队列struct cts_rq。具体Linu内核结构源码如下:

struct cfs_rq {
	struct load_weight	load;
	unsigned long		runnable_weight;
	unsigned int		nr_running;
	unsigned int		h_nr_running;      /* SCHED_{NORMAL,BATCH,IDLE} */
	unsigned int		idle_h_nr_running; /* SCHED_IDLE */

	u64			exec_clock;
	u64			min_vruntime;
#ifndef CONFIG_64BIT
	u64			min_vruntime_copy;
#endif

	struct rb_root_cached	tasks_timeline;
    	/*
	 * 'curr' points to currently running entity on this cfs_rq.
	 * It is set to NULL otherwise (i.e when none are currently running).
	 */
	struct sched_entity	*curr;
	struct sched_entity	*next;
	struct sched_entity	*last;
	struct sched_entity	*skip;

#ifdef	CONFIG_SCHED_DEBUG
	unsigned int		nr_spread_over;
#endif

#ifdef CONFIG_SMP
	/*
	 * CFS load tracking
	 */
	struct sched_avg	avg;
#ifndef CONFIG_64BIT
	u64			load_last_update_time_copy;
#endif
	struct {
		raw_spinlock_t	lock ____cacheline_aligned;
		int		nr;
		unsigned long	load_avg;
		unsigned long	util_avg;
		unsigned long	runnable_sum;
	} removed;

#ifdef CONFIG_FAIR_GROUP_SCHED
	unsigned long		tg_load_avg_contrib;
	long			propagate;
	long			prop_runnable_sum;

	/*
	 *   h_load = weight * f(tg)
	 *
	 * Where f(tg) is the recursive weight fraction assigned to
	 * this group.
	 */
	unsigned long		h_load;
	u64			last_h_load_update;
	struct sched_entity	*h_load_next;
#endif /* CONFIG_FAIR_GROUP_SCHED */
#endif /* CONFIG_SMP */

#ifdef CONFIG_FAIR_GROUP_SCHED
	struct rq		*rq;	/* CPU runqueue to which this cfs_rq is attached */

	/*
	 * leaf cfs_rqs are those that hold tasks (lowest schedulable entity in
	 * a hierarchy). Non-leaf lrqs hold other higher schedulable entities
	 * (like users, containers etc.)
	 *
	 * leaf_cfs_rq_list ties together list of leaf cfs_rq's in a CPU.
	 * This list is used during load balance.
	 */
	int			on_list;
	struct list_head	leaf_cfs_rq_list;
	struct task_group	*tg;	/* group that "owns" this runqueue */

#ifdef CONFIG_CFS_BANDWIDTH
	int			runtime_enabled;
	s64			runtime_remaining;

	u64			throttled_clock;
	u64			throttled_clock_task;
	u64			throttled_clock_task_time;
	int			throttled;
	int			throttle_count;
	struct list_head	throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

cfs_rq:跟踪就绪队列信息以及管理就绪态调度实体,并维护找查按照虚拟时间排序的红黑树。tasks_timeline->rbroot是红黑树的根,tasks_ timeline->rb_leftmost指向红黑树中最左边的调度实体,即虚拟赶时间最小的调度实体。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值