欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CFS 调度器数据结构篇

程序员文章站 2024-02-27 19:26:15
...

在上一节我们了解了CFS的设计原理,包括CFS的引入,CFS是如何实现公平,CFS工作原理的。本小节我们重点在分析CFS调度器中涉及到的一些常见的数据结构,对这些数据结构做一个简单的概括,梳理各个数据结构之间的关系图出来。

调度类

CFS调度器是在Linux2.6.23引入的,在当时就提出了调度类概念,调度类就是将调度策略模块化,有种面向对象的感觉。先来看下调度类的数据结构,调度类是通过struct sched_class数据结构表示

struct sched_class {
	const struct sched_class *next;

	void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
	void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);

	void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);

	/*
	 * It is the responsibility of the pick_next_task() method that will
	 * return the next task to call put_prev_task() on the @prev task or
	 * something equivalent.
	 *
	 * May return RETRY_TASK when it finds a higher prio class has runnable
	 * tasks.
	 */
	struct task_struct * (*pick_next_task)(struct rq *rq,
					       struct task_struct *prev,
					       struct rq_flags *rf);
	void (*put_prev_task)(struct rq *rq, struct task_struct *p);
	void (*set_curr_task)(struct rq *rq);
	void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);

由于sched_class成员比较多,我们这里先介绍几个常用的。可以看到调度类中基本都是一些函数指针,这些函数指针分别代表的不同的含义。

  • next 是用来指向下一级的一个调度类,内核中为每个调度策略提供了一个调度类,这些调度类是通过next成员链接到一起
  • enqueue_task: 用来将一个进程添加到就绪队列中,同时会增加它的可运行的进程数
  • dequeue_task: 用来将一个进程从就绪队列移除,同时减少可运行进程的数量
  • check_preempt_curr: 用来检测当一个进程的状态设置为runnable时,检查当前进程是否可以发生抢占
  • pick_next_task: 在运行队列中选择下一个最合适的进程来运行
  • put_prev_task: 获得当前进程之前的那个进程
  • set_curr_task: 用来设置当前进程的调度状态等
  • task_tick: 在每个时钟tick的时候会调度各个调度类中的tick函数

Linux内核都提供了那些调度类

extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;

 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

同时也提供了六种调度策略。下表示调度策略和调度类之间的关系

调度类 调度策略 调度对象
stop_sched_class(停机调度类) 停机的进程
dl_sched_class(限期调度类) SCHED_DEADLINE dl进程
rt_sched_class(实时调度类) SCHED_RR 或者 SCHED_FIFO 实时进程
fair_sched_class(公平调度类) SCHED_NORMAL 或者 SCHED_BATCH 普通进程
idle_sched_class(空闲调度类) SCHED_IDLE idle进程

同时这些调度类之间是有优先级关系的。

#ifdef CONFIG_SMP
#define sched_class_highest (&stop_sched_class)
#else
#define sched_class_highest (&dl_sched_class)
#endif
#define for_each_class(class) \
   for (class = sched_class_highest; class; class = class->next)

如果定义了SMP,则最高优先级的是stop调度类。调度类的优先级关系

stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_shced_class

调度实体

struct sched_entity {
	/* For load-balancing: */
	struct load_weight		load;
	unsigned long			runnable_weight;
	struct rb_node			run_node;
	struct list_head		group_node;
	unsigned int			on_rq;

	u64				exec_start;
	u64				sum_exec_runtime;
	u64				vruntime;
	u64				prev_sum_exec_runtime;

	u64				nr_migrations;

	struct sched_statistics		statistics;

从Linux2.6.23开始引入了调度实体的概念,调度实体封装了进程的一些重要的信息。在之前的O(1)算法中调度的单位都是task_struct,而在Linux2.6.23引入调度模块化后,调度的单位成为调度实体sched_entity

  • load就是此进程的权重
  • run_node:CFS调度是通过红黑树来管理进程的,这个是红黑树的节点
  • on_rq: 此值为1时,代表此进程在运行队列中
  • exec_start: 记录这个进程在CPU上开始执行任务的时间
  • sum_exec_runtime: 记录这个进程总的运行时间
  • vruntime: 代表的是进程的虚拟运行时间
  • prev_sum_exec_runtime: 记录前面一个进程的总的运行时间
  • nr_migrations: 负载均衡时进程的迁移次数
  • statistics:进程的统计信息

红黑树

红黑树大家肯定不陌生了,树左边节点的值永远比树右边接的值小。

在O(n)和O(1)的调度器中运行队列都是通过数组链表来管理的,而在CFS调度中抛弃了之前的数据结构,采用了以时间为键值的一棵红黑树。其中的时间键值就是进程的vruntime。

CFS维护了一课以时间为排序的红黑树,所有的红黑树节点都是通过进程的se.vruntime来作为key来进行排序。CFS每次调度的时候总是选择这棵红黑树最左边的节点,然后来调度它。随着时间的推移,之前在最左边的节点随着运行,进程的vruntime也随之增大,这些进程慢慢的会添加到红黑树的右边。循环往复这个树上的所有进程都会被调度到,从而达到的公平。

同时CFS也会维护这棵树上最小的vruntime的值cfs.min_vruntime,而且这个值是单调递增的。此值用来跟踪运行队列中最小的vruntime的值。

CFS 调度器数据结构篇

运行队列

系统中每个CPU上有有一个运行队列struct rq数据结构,这个struct rq是个PER-CPU的,每个CPU上都要这样的一个运行队列,可以可以防止多个CPU去并发访问一个运行队列。

/*
 * This is the main, per-CPU runqueue data structure.
 *
 * Locking rule: those places that want to lock multiple runqueues
 * (such as the load balancing or the thread migration code), lock
 * acquire operations must be ordered by ascending &runqueue.
 */
struct rq {

        unsigned int		nr_running;

	/* capture load from *all* tasks on this CPU: */
	struct load_weight	load;
	unsigned long		nr_load_updates;
	u64			nr_switches;

	struct cfs_rq		cfs;
	struct rt_rq		rt;
	struct dl_rq		dl;

可以从注释看到struct rq是一个per-cpu的变量。

  • nr_running: 代表这个运行队列上总的运行进程数
  • load: 在这个CPU上所有进程的权重,这个CPU上可能运行的进程有实时进程,普通进程等
  • nr_switches: 进程切换的统计数
  • struct cfs_rq: 就是CFS调度类的一个运行队列
  • struct rt_rq: 代表的是rt调度类的运行队列
  • struct dl_rq: 代表的是dl调度类的运行队列

可以得出的一个结论是,一个struct rq中包括了各种类型的进程,有DL的,有实时的,有普通的。通过将不同进程的挂到不同的运行队列中管理。

/* CFS-related fields in a runqueue */
struct cfs_rq {
	struct load_weight	load;
	unsigned long		runnable_weight;
	unsigned int		nr_running;
	unsigned int		h_nr_running;

	u64			exec_clock;
	u64			min_vruntime;

从注释上看struct cfs_rq代表的是CFS调度策略对应的运行队列

  • load: 是这个CFS_rq的权重,包含着CFS就绪队列中的所有进程
  • nr_running: 代表的是这个CFS运行队列中可运行的进程数
  • min_vruntime: 此值代表的是CFS运行队列中所有进程的最小的vruntime

看下运行队列的关系图

CFS 调度器数据结构篇

每个CPU中都存在一个struct rq运行队列,struct rq中根据进程调度策略分为不同的运行队列,比如普通进程就会挂载到cfs_rq中,在struct cfs_rq中则定义了每一个调度实体,每一个调度实体根据vruntime的值添加到红黑树的节点中。