CFS 调度器数据结构篇

在上一节我们了解了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的值。

运行队列

系统中每个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

看下运行队列的关系图

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

发布了187 篇原创文章 · 获赞 108 · 访问量 37万+

猜你喜欢

转载自blog.csdn.net/longwang155069/article/details/104551212
CFS