Linux kernel中任务、CPU相关的结构体介绍,及__schedule()的调度过程(单队列任务调度、跨队列任务调度)

最近对__schedule()调度的过程比较感兴趣,为此我写一篇专门介绍任务调度过程的介绍文章。

这篇文章先快速介绍Linux中的结构体 任务task_struct、任务组task_group、队列rq等,从结构体的内容角度阐述任务与CPU间的关系;然后介绍队列的调度策略和过程,包括单队列和多队列间的调度。在任务的种类上,会重点介绍CFS公平调度类和RT实时任务类。

目录


1 各结构体关键元素介绍

1.1 任务相关的结构体

1.1.1 任务task_struct

1.1.2 任务组task_group

1.1.3 任务实体sched_entity

1.2 队列相关的结构体

1.2.1 队列rq

1.2.2 CFS任务队列cfs_rq

1.2.3 RT任务队列rt_rq

1.3 任务与CPU队列的关系

2 schedule()中的调度

2.1 单队列任务调度

2.2 跨队列任务调度

1 各结构体关键元素介绍

##1.1 任务相关的结构体

1.1.1 任务task_struct

struct task_struct结构体的内容存储在include/linux/sched.h中,以下是该结构体重点介绍的成员:

struct task_struct{
	const struct sched_class	*sched_class; //表明当前任务的调度类型,可以是stop、dl、rt、cfs、idle这五种
	struct sched_entity		se; //调度类型为cfs的任务实体
	struct sched_rt_entity		rt; //调度类型为rt的任务实体
#ifdef CONFIG_CGROUP_SCHED
	struct task_group		*sched_task_group; //表明该任务的父任务组
#endif 
}

1.1.2 任务组task_group

结构体task_group用于表述一个任务组的内容。一个任务组可以同时包含多个任务。以下是该结构体重点介绍的成员:

struct task_group{
    /* schedulable entities of this group on each CPU */
	struct sched_entity	**se; //任务组下的CFS任务实体数组
	/* runqueue "owned" by this group on each CPU */
	struct cfs_rq		**cfs_rq; //任务组下的CFS队列数组
    
    struct sched_rt_entity	**rt_se; //任务组下的RT任务实体数组
	struct rt_rq		**rt_rq; //任务组下的RT队列数组
}

1.1.3 任务实体sched_entity

sched_entity是从task_struct抽象出来的结构,该结构具体表示所属任务的具体运行情况。以下是该结构体重点介绍的成员:

struct sched_entity{
    struct rb_node			run_node; //cfs队列使用红黑树管理队列中的任务,一个任务实体对应一个红黑树节点
    /* rq on which this entity is (to be) queued: */
	struct cfs_rq			*cfs_rq; //表明该任务运行在特定CPU的队列上
	/* rq "owned" by this entity/group: */
	struct cfs_rq			*my_q; //表明该任务属于task_group中的特定cfs_rq队列
}

结构体sched_entity用于表述调度类型为CFS的任务。结构体sched_rt_entity与sched_entity,只是sched_rt_entity用于表述调度类型为RT的任务,此处不再赘述。

1.2 队列相关的结构体

1.2.1 队列rq

每个CPU都有自己唯一的CPU任务队列,CPU任务队列用结构体rq表示。以下是该结构体重点介绍的内容:

struct rq{
	raw_spinlock_t		lock; //spinlock锁,用于保护队列内容
    int			cpu; //当前队列的CPU编号
    struct cfs_rq		cfs; //该CPU下的CFS任务队列
	struct rt_rq		rt; //该CPU下的RT任务队列
}

注意,一个CPU的队列需要同时管理cfs和rt任务,且CFS任务队列和RT任务队列同时且平行存在于CPU的任务队列中。

1.2.2 CFS任务队列cfs_rq

结构体cfs_rq队列既可用于CPU的队列管理,也可以用于任务组的CFS任务管理。该结构体重点介绍的内容如下:

struct cfs_rq{
	struct sched_entity	*curr; //该指针指向当前该队列正在运行的任务实体
	struct sched_entity	*next; //该指针指向该队列下一次运行的任务实体
    struct rq		*rq; //该任务组的CFS队列具体指向的CPU队列
    struct task_group	*tg; //该队列当前所属的任务组
}

1.2.3 RT任务队列rt_rq

结构体rt_rq队列既可用于CPU的队列管理,也可以用于任务组的RT任务管理。该结构体重点介绍的内容如下:

struct rt_rq{
	struct rt_prio_array	active; //长度为优先级数目的数组,用于表示哪些优先级上存在RT任务
#if defined CONFIG_SMP || defined CONFIG_RT_GROUP_SCHED
	struct {
		int		curr; //该队列的任务的最高优先级
#ifdef CONFIG_SMP
		int		next; //该队列的任务的次高优先级
#endif
	} highest_prio;
#endif
    
    struct rq		*rq; //该任务组的RT队列具体指向的CPU队列
	struct task_group	*tg; //该队列当前所属的任务组
}

需要强调的是,CFS队列使用红黑树结构管理队列中任务运行次序,而RT队列使用优先级高低策略的方式管理任务运行。

1.3 任务与CPU队列的关系

下图为任务与CPU队列的关系。该图以4核CPU的系统为例,介绍一个task_group中各成员与各CPU的rq成员关系。在下图中,sched_entity简写为se,sched_rt_entity简写为rt_se。

结构体task_group的初始化过程和任务与CPU队列的关系建立过程,请另行查询代码过程sched_create_group()—>alloc_fair_sched_group()—>init_tg_cfs_entry()和过程sched_create_group()—>alloc_fair_sched_group()—>init_tg_cfs_entry(),代码的详细内容和逻辑不再另行介绍。

在该图中,黑色框线代表结构体的父成员为task_group,红色框线代表结构体的父成员为CPU。蓝色箭头实线代表两成员间的指针指向关系,例如黑色框线的se成员使用蓝色箭头实线指向黑色框线的cfs_rq成员,代表结构体se的成员*my_q指向cfs_rq。蓝色箭头虚线代表包含关系,如结构体task_group使用箭头虚线指向**cfs_rq,**se,**rt_se,**rt_rq,代表结构体task_group含有这四个成员。

结构体关系图

在上图中,**cfs_rq,**se,**rt_se,**rt_rq成员为数组,其数组长度为系统的CPU个数。以**cfs_rq为例,该数组的成员类型为结构体cfs_rq,数组长度为4,每个结构体cfs_rq的*rq成员指针指向相对应的CPU的rq队列。CFS任务实体sched_entity和RT任务实体sched_rt_entity各自指向相对应的CPU的rq队列的cfs_rq成员或rt_rq成员。

由上图可知,一个task_group下最多同时创建N个cfs_rq队列、N个rt_rq队列、N个sched_entity CFS任务实体和N个sched_rt_entity RT任务实体,其中N为当前系统的CPU个数;每个队列或任务实体上不一定真实存在任务。系统上每个CPU都含有结构体rq,同时一个结构体rq同时管理CFS任务队列cfs_rq和RT任务队列rt_rq。

CPU上的CFS任务队列和RT任务队列,均可同时接受多个task_group的CFS任务实体和RT任务实体;但是一个task_group中的一个CFS任务队列和RT任务队列,只能接受一个任务实体,同时也只能指向一个CPU的CFS队列或RT队列。

2 __schedule()中的调度

2.1 单队列任务调度

单队列任务调度的过程大体上分为三阶段,第一阶段主要负责队列信息的统计和加锁工作,第二阶段是判断当前队列执行任务的状态,并根据任务状态决定是否需要进行队列移除工作,第三阶段负责选择队列下一时刻应该运行的工作。请注意,无论第二阶段当前队列是否被移除,第三阶段都会正常进行。

第一阶段的前期工作以及代码简要介绍如下所示:

prev = rq->curr; //获得当前队列执行任务
schedule_debug(prev, preempt); //记录了一些调试用的统计信息,并检查了一些状态是否符合预期。
local_irq_disable(); //local_irq_disable():禁止当前处理器上的所有本地中断发生
rcu_note_context_switch(preempt); //更新RCU状态
rq_lock(rq, &rf); //rq可能会被其它CPU访问,所以需要给该CPU上的runqueue队列加锁
smp_mb__after_spinlock(); //加内存屏障
update_rq_clock(rq);  //更新rq的clock和clock_task时间,前者是当前运行队列总共运行的时间,后者是去除中断时间后,当前运行队列实际执行任务的时间。

第二阶段先判断当前prev任务的状态,根据任务状态决定是否需要进行运行队列移除工作。

​ —>当任务状态为stopped,且__schedule()的触发是在非抢占状态下时,进行以下判断

​ —>若prev任务状态具体为TASK_INTERRUPTIBLE,或当前prev任务有kill信号进行处理,则需要将prev任务状态调整为TASK_RUNNING,且在该运行队列上保留该任务

​ —>若prev任务状态不是以上分支状态,则调用deactivate_task()函数,将该任务从当前CPU的运行队列上移除

第三阶段调用pick_next_task()函数,该函数负责选择CPU下一个时刻运行的进程任务。以下是pick_next_task的代码以及具体解释:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	const struct sched_class *class;
	struct task_struct *p;

	/* sched_class能够比较的原因是用地址进行比较的,实时调度类策略的地址
	* 要比公平调度类策略的地址低*/
    //sched_class从高到低依次为stop、dl、rt、fair、idle
    //idle类进程只有一种,即idle进程
	if (likely(prev->sched_class <= &fair_sched_class &&
		   rq->nr_running == rq->cfs.h_nr_running)) {//nr_running:可运行进程的数量

		//说明当前队列可运行进程全是CFS类进程,使用pick_next_task_fair函数挑选下一个进程
		p = pick_next_task_fair(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))//RETRY_TASK:表示有从属于更高优先级调度类的进程被唤醒------->跳转到步骤restart
			goto restart;

		/* 当前CFS可运行队列没有任务,只能选择idle进程 */
		if (!p) {
			put_prev_task(rq, prev);
			p = pick_next_task_idle(rq);//使用idle调度类策略,下一个进程选作idle进程
		}

		return p;
	}

restart:
	put_prev_task_balance(rq, prev, rf);

	//从stop->dl->rt->fair->idle,一个个调度类去寻找最适合的进程
	for_each_class(class) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
	}

	//选择不到下一个运行的任务,出bug了
	BUG();
}

在多数情况下,单个队列上可运行任务只有公平调度类CFS任务和idle进程,idle进程用于当前CPU无实际任务时运行。在pick_next_task()函数中,函数先比较当前队列的可运行任务数目是否与CFS运行任务数目相等,若相等,则表明当前队列无stop、dl和rt类任务,进入到if函数结构体中。若不相等,则表明有更高优先级级别的任务需要运行。

在restart: 下边的内容中,有一行代码for_each_class(class)。该行代码的作用是依次迭代stop、dl、rt、cfs和idle进程类的pick_next_task函数,选择当前所有可运行任务中,调度类和优先级均为最高的任务。该代码先根据不同类优先级高低寻找大类任务,随后在同一大类的队列内寻找最合适的任务作为当前队列下一时刻的运行任务。rt队列的下一任务为当前队列的最高优先级任务,cfs队列的下一任务为红黑树的最左边节点任务,该红黑树的排布利用所有CFS任务的各种数据。

2.2 跨队列任务调度

__schedule()函数的最后一行为balance_callback(rq),该行代码顾名思义进行负载的调度均衡,而从本质上看负载调度均衡即为跨CPU队列的任务调度过程。

值得一提的是,在此处只有dl和rt类任务才能实际调用balance_callback()函数以进行实时任务的负载调度,但是这不意味着全局系统中CFS类任务不能做负载调度。在系统中,发生任务调度的时间不仅只发生在__schedule(),还可以发生在其它地方函数中,此处不再赘述。

balance_callback()函数需要调用rq->balance_callback()成员函数。该成员函数在pick_next_task()中,根据队列下一时刻运行的任务调度类别被指定。此处我以rt类的balance_callback()函数——push_rt_tasks()为例,介绍RT类任务的跨队列调度过程。

push_rt_tasks循环调度push_rt_task(rq),直到跨队列任务调度完成。

static void push_rt_tasks(struct rq *rq)
{
	/* push_rt_task will return true if it moved an RT */
	while (push_rt_task(rq))
		;
}

以下是push_rt_task()的函数介绍。

① 首先是查询当前CPU的队列的RT任务数目,如果多于1(也考虑进当前正在运行的),则寻找当前不在运行状态的且优先级最高的RT任务。如果没有这样的任务,表明当前CPU有且仅有一个任务,中止查询。

	//先看看当前队列有没有能推出的RT task(next_task),next_task是当前可推出队列中优先级最高的那个
	//这个next_task是要迁移到其它CPU上的
	next_task = pick_next_pushable_task(rq);
	if (!next_task)
		return 0;

retry:
	//考虑到当前队列没有加锁,可能有其它队列迁移过来的RT任务,因此还要做一次检测。
	//如果next_task就是当前的,那就不用做了
	if (WARN_ON(next_task == rq->curr))
		return 0;

	//lower p->prio means higher priority
	//如果被迁移出去的是高优先级别的任务,既然级别高,那就需要当前队列跑它
	//这说明:这个next_task(就是rt_task)的优先级要比当前任务的优先级高,所以这任务肯定是要
	//保留到当前队列的,为此,需要重新整理这个rq队列
	if (unlikely(next_task->prio < rq->curr->prio)) {
		resched_curr(rq);
		return 0;
	}

② 为准备迁移出去的next_task寻找合适的CPU及其队列。CPU及其队列选择有这些步骤:

—> 获得next_task的优先级task_pri(这里的task_pri经过处理,数值越低代表优先级越低)

—> 遍历0至task_pri-1的优先级,使用全局掩码lowest_mask查询在当前优先级上,全局系统是否存在CPU队列的最高优先级即为当前优先级。如有,则返回该CPU,如无,则继续遍历更高的优先级

如果找到这样的CPU和队列,称该队列为lowest_rq,并且返回,同时将当前队列rq和即将接受任务的队列lowest_rq同时加锁;如果没有,则重新再循环一遍。这样的循环机会一共有3次,3是由kernel指定的

lowest_rq = find_lock_lowest_rq(next_task, rq);

③ 如果在步骤②中寻找到接受任务迁移的队列lowest_rq,则j进行相关迁移工作;如果循环3次仍未能寻找到lowest_rq,则又一次运行代码task = pick_next_pushable_task(rq);。这样做的原因,我猜测是在②中的find_lock_lowest_rq()代码曾释放当前队列的锁,当前队列的任务情况可能发生了变化。

如果task不存在,则说明该队列没有更多的RT任务需要重新调度,此时可以退出调度过程;如果task和next_task相等,则说明当前队列不在运行状态的且优先级最高的RT任务不变,但是已经无法主动迁移push至其它CPU队列,需要等待其它CPU将该任务拉走pull。如果task和next_task不同,则表明此时的队列任务情况已经发生变化,需要回到①中的retry重新执行。

具体的迁移过程也可以查询这篇文章的第四部分 PUSH 推任务迁移[linux实时进程的负载均衡详解](linux实时进程的负载均衡详解 - 掘金 (juejin.cn))。

  • 36
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值