Linux内核源码分析-进程调度(三)-从进程创建到唤醒的过程去了解CFS调度器

2023-11-10

从do_fork开始

进程的创建是通过do_fork函数。新进程的诞生,我们调度核心层会通知调度类来初始化新进程。
我们一路顺着do_fork函数往下看:do_fork()->_do_fork()->copy_process()->sched_fork()。

long do_fork(unsigned long clone_flags, // 创建进程的标志位集合
	      unsigned long stack_start,  // 用户态栈的起始地址
	      unsigned long stack_size,  // 用户态栈的大小,一般情况下设置位0
	      int __user *parent_tidptr,  //  指向用户空间中地址的指针, 指向父进程的PID
	      int __user *child_tidptr)  //  指向用户空间中地址的指针, 指向子进程的PID 
{
	struct kernel_clone_args args = {
		.flags		= (clone_flags & ~CSIGNAL),
		.pidfd		= parent_tidptr,
		.child_tid	= child_tidptr,
		.parent_tid	= parent_tidptr,
		.exit_signal	= (clone_flags & CSIGNAL),
		.stack		= stack_start,
		.stack_size	= stack_size,
	};

	if (!legacy_clone_args_valid(&args))
		return -EINVAL;

	return _do_fork(&args);
}
long _do_fork(struct kernel_clone_args *args)
{
	u64 clone_flags = args->flags;
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p;
	int trace = 0;
	long nr;

	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if (args->exit_signal != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}

	p = copy_process(NULL, trace, NUMA_NO_NODE, args);  // 创建子进程

    wake_up_new_task(p);  // 唤醒新创建的进程,把进程加入就绪队列里等待调度。
}

创建子进程

static __latent_entropy struct task_struct *copy_process(
					struct pid *pid,
					int trace,
					int node,
					struct kernel_clone_args *args)
{
    p = dup_task_struct(current, node);  // 复制当前进程(父进程)描述符task_struct、创建内核堆栈等
    u64 clone_flags = args->flags;
    /*
     * 初始化新建进程p相关的调度参数、设置进程p状态为TASK_NEW、优先级、调度策略、权重、调度器,并分配CPU资源。
     */
    int retval = sched_fork(clone_flags, p);
}

初始化新建进程p相关的调度参数

针对sched_fork()函数如下:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
	unsigned long flags;
	__sched_fork(clone_flags, p);  // 初始化p中与调度相关的参数。
	/*
	 * 标记进程为Running状态,用于保证实际上并没有运行,信号或许其他外部事件都无法唤醒该进程,同时把它插入运行队列。
	 */
	p->state = TASK_NEW;

	/*
	 * Make sure we do not leak PI boosting priority to the child.
	 */
	p->prio = current->normal_prio;  // 因为父进程的动态优先级可能会被临时提升,因此新进程的优先级需要从父进程的normal_prio中继承,而不能从其动态优先级prio中继承。

	/*
	 * Revert to default priority/policy on fork if requested.
	 */
	if (unlikely(p->sched_reset_on_fork)) {
		if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
			p->policy = SCHED_NORMAL;
			p->static_prio = NICE_TO_PRIO(0);
			p->rt_priority = 0;
		} else if (PRIO_TO_NICE(p->static_prio) < 0)
			p->static_prio = NICE_TO_PRIO(0);

		p->prio = p->normal_prio = __normal_prio(p);
		set_load_weight(p, false);

		/*
		 * We don't need the reset flag anymore after the fork. It has
		 * fulfilled its duty:
		 */
		p->sched_reset_on_fork = 0;
	}

	if (dl_prio(p->prio))
		return -EAGAIN;
	else if (rt_prio(p->prio))
		p->sched_class = &rt_sched_class;  // 为进程p选择实时调度类
	else
		p->sched_class = &fair_sched_class;  // 为进程p选择CFS调度类

	init_entity_runnable_average(&p->se);  // 设置该进程对应调度实体的初始负载
	__set_task_cpu(p, smp_processor_id());  // 为进程p分配相应CPU,这个值在该进程加入就绪队列前可能会根据实际需求更改,故此处只是简单地将其初始化为与父进程相同。
	if (p->sched_class->task_fork)
		p->sched_class->task_fork(p);  // 调用调度类中的task_fork函数。task_fork方法主要做fork相关操作。传递的参数p就是创建的task_struct。
}

cfs的task_fork操作

CFS调度类fair_sched_class方法如下:

const struct sched_class fair_sched_class = {
	.task_fork		= task_fork_fair,
}

task_fork_fair实现如下:

static void task_fork_fair(struct task_struct *p)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se, *curr;
	struct rq *rq = this_rq();
	struct rq_flags rf;

	rq_lock(rq, &rf);
	update_rq_clock(rq);

	cfs_rq = task_cfs_rq(current);  // 获取当前进程所在的per-cpu rq中的cfs_rq。
	curr = cfs_rq->curr;  // curr指向当前正在cpu上运行的task的调度实体。
	if (curr) {
		update_curr(cfs_rq);  // 更新当前正在运行的调度实体的运行时间信息(上次运行(启动)开始时间、总的实际运行时间、虚拟运行时间等等),并更新cfs_rq的最小虚拟运行时间。
		se->vruntime = curr->vruntime;  // 初始化当前创建的新进程p对应的se的虚拟运行时间。
	}

	/*
	 * place_entity()函数在进程创建以及唤醒的时候都会调用,创建进程的时候传递参数initial=1。
	 * 此处的目的是更新进程p对应的调度实体se的虚拟时间(se->vruntime),使得se->vruntime和cfs_rq->min_vruntime的值保持差别不大,如果非常小的话,就会导致创建的新进程一直占用cpu。
	 */
	place_entity(cfs_rq, se, 1);

	if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
		/*
		 * Upon rescheduling, sched_class::put_prev_task() will place
		 * 'current' within the tree based on its new key value.
		 */
		swap(curr->vruntime, se->vruntime);
		resched_curr(rq);
	}

	/*
	 * 这里为什么要减去cfs_rq->min_vruntime?因为现在计算进程的vruntime是基于当前cpu上的cfs_rq,并且现在还没有加入当前cfs_rq的就绪队列上。
	 * 等到当前进程创建完毕开始唤醒的时候,加入的就绪队列就不一定是现在计算基于的cpu。
	 * 因此非就绪态或者非运行态时虚拟运行时间这个字段保存的其实是一个从生命周期开始的一个虚拟运行时间,是与cpu无关的。
	 * 所以,在加入就绪队列的函数中会根据情况加上当前就绪队列cfs_rq->min_vruntime。为什么要“先加后减”?
	 * 假设cpu0上的cfs就绪队列的最小虚拟运行时间min_vruntime的值是1000000,此时创建进程的时候赋予当前进程虚拟时间是1000500。
	 * 但是唤醒此进程,并让进程加入的就绪队列是CFS就绪队列,cpu1上的cfs就绪队列的最小虚拟运行时间min_vruntime的值如果是9000000。
	 * 如果不采用“先减后加”的方法,那么该进程就在cpu上长时间运行。现在的处理计算得到的调度实体的虚拟运行时间是1000500-1000000+9000000 = 9000500。
	 */
	se->vruntime -= cfs_rq->min_vruntime; 
	rq_unlock(rq, &rf);
}

更新cfs_rq上正在运行的进程的运行时间信息

我们来对上面出现的update_curr函数来深挖一把。

/*
 * 此函数的作用是更新cfs_rq中正在cpu上运行的调度实体的运行时间信息(上次运行(启动)开始时间、总的实际运行时间、虚拟运行时间等等)。
 * 所有与虚拟时钟有关的计算都在update_curr中执行,该函数在系统中各个不同地方调用,包括周期性调度器在内。
 * update_curr的流程如下:
 *   1、首先计算当前时间与当前正在运行进程的上次启动时间的差值
 *   2、通过负荷权重和当前系统时间计算出进程的虚拟运行时间
 *   3、重新设置cfs的min_vruntime保持其单调性
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;  // 获取就绪队列的当前执行进程

	/*
	 * rq_of:用于确定CFS就绪队列所属的struct rq实例。csf_rq就绪队列中存储了指向就绪队列的实参,而rq_of就返回了这个指向rq的指针。
	 * rq_of => return cfs_rq->rq 返回cfs队列所属的全局就绪队列  
	 */
	u64 now = rq_clock_task(rq_of(cfs_rq)); // 返回了rq的clock_task
	u64 delta_exec;

	if (unlikely(!curr))  //  如果队列上没有进程在执行,即没事可干就返回呗。
		return;

	delta_exec = now - curr->exec_start;  //  计算出当前运行的进程距离上次运行并更新虚拟运行时间的差值,更本质的说法是:内核计算当前运行进程和此进程上一次更新负荷权重时两次的时间的差值。
	if (unlikely((s64)delta_exec <= 0))
		return;

	curr->exec_start = now;  // 重新更新启动时间为now,以备下次计算时使用。

	schedstat_set(curr->statistics.exec_max,
		      max(delta_exec, curr->statistics.exec_max));

	curr->sum_exec_runtime += delta_exec;  // 更新当前进程总共执行的时间,即将计算出来的时间差加到先前的统计时间上。
	schedstat_add(cfs_rq->exec_clock, delta_exec);

    /*
	 * calc_delta_fair的运行依据公式为:delta = delta * (NICE_0_LOAD / curr->vruntime->load.weight)
	 */
	curr->vruntime += calc_delta_fair(delta_exec, curr);  // 更新当前调度实体的虚拟运行时间。

	/*
	 * 更新CFS就绪队列的最小虚拟运行时间min_vruntime。min_vruntime也是不断更新的,主要就是跟踪就绪队列中所有调度实体的最小虚拟运行时间。
	 * 如果min_vruntime一直不更新的话,由于min_vruntime太小,导致后面创建的新进程根据这个值来初始化新进程的虚拟运行时间,这样新进程又会一个个的霸占着cpu。
	 */
	update_min_vruntime(cfs_rq);

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cgroup_account_cputime(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

更新cfs_rq的最小虚拟运行时间

calc_delta_fair函数我们在前面的文章中已经说过不再赘述,我们主要来看一下update_min_vruntime的实现:

/*
 * 既然要更新就绪队列的最小虚拟运行时间min_vruntime,试想一下,拥有最小虚拟运行时间的地方有哪儿?
 *   1、就绪队列本身的cfs_rq->min_vruntime成员。
 *   2、当前正在运行的进程的虚拟运行时间,因为CFS调度器选择最适合运行的进程是选择维护的红黑树中虚拟运行时间最小的进程。
 *   3、如果在当前进程运行的过程中,有进程加入就绪队列,那么红黑树最左边的进程的虚拟运行时间同样也有可能是最小的虚拟运行时间。
 * 因此此函数从这三方面来入手,确保就绪队列的最小虚拟运行时间得到更新。
 */
static void update_min_vruntime(struct cfs_rq *cfs_rq)  // 更新cfs就绪队列的最小虚拟运行时间min_vruntime
{
	struct sched_entity *curr = cfs_rq->curr;
	struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline);

	u64 vruntime = cfs_rq->min_vruntime;  // 首先用队列cfs_rq的虚拟运行时间初始化vruntime变量。

    // 其次当正在运行进程存在时用当前正在运行的进程的虚拟运行时间去更新vruntime变量。
	if (curr) {
		if (curr->on_rq)  // 如果当前调度实体在就绪队列中,则vruntime设置为当前调度实体的虚拟运行时间。
			vruntime = curr->vruntime;
		else
			curr = NULL;
	}

	if (leftmost) { /* non-empty tree */  // 如果在当前进程运行的过程中,有进程加入就绪队列,那么红黑树最左边的进程的虚拟运行时间同样也有可能是最小的虚拟运行时间。
		struct sched_entity *se;
		se = rb_entry(leftmost, struct sched_entity, run_node);  // 取红黑数最左边的进程(调度实体)。

        // 如果当前无运行中进程,则vruntime更新为红黑数最左边进程的虚拟运行时间(即整个红黑树中最小的时间),否则和前面得到的vruntime值相比取较小值。
		if (!curr)  
			vruntime = se->vruntime;
		else
			vruntime = min_vruntime(vruntime, se->vruntime);
	}

	/* ensure we never gain time by being placed backwards. */
	cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);  // 就绪队列本身的最小虚拟运行时间与vruntime相比,取较小值。
#ifndef CONFIG_64BIT
	smp_wmb();
	cfs_rq->min_vruntime_copy = cfs_rq->min_vruntime;
#endif
}

更新进程p对应的调度实体se的虚拟时间

update_curr函数我们基本已经了解清楚,接下来看看place_entity函数:

/*
 * 在进程创建以及唤醒的时候都会调用,创建进程的时候传递参数initial=1,唤醒进程的时候传递参数initial=0
 * 目的是更新进程调度实体se的虚拟时间(se->vruntime),使得se->vruntime和cfs_rq->min_vruntime的值保持差别不大,如果非常小的话,就会导致创建的新进程一直占用cpu。
 */
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
	u64 vruntime = cfs_rq->min_vruntime;

	/*
	 * The 'current' period is already promised to the current tasks,
	 * however the extra weight of the new task will slow them down a
	 * little, place the new task so that it fits in the slot that
	 * stays open at the end.
	 */	
	if (initial && sched_feat(START_DEBIT))
		vruntime += sched_vslice(cfs_rq, se);  // 针对刚创建的进程会进行一定的惩罚,将虚拟运行时间加上一个值就是惩罚,毕竟虚拟运行时间越小越容易得到调度执行。惩罚的时间由sched_vslice()计算。

	/* sleeps up to a single latency don't count. */
	if (!initial) {
		unsigned long thresh = sysctl_sched_latency;

		/*
		 * Halve their sleep time's effect, to allow
		 * for a gentler effect of sleepers:
		 */
		if (sched_feat(GENTLE_FAIR_SLEEPERS))
			thresh >>= 1;

		vruntime -= thresh;  // 这里主要是针对唤醒的进程,针对睡眠很久的进程,我们总是期望它很快得到调度执行,所以会将虚拟运行时间减去一个值。
	}

	/* ensure we never gain time by being placed backwards. */
	/*
	 * 我们保证调度实体的虚拟运行时间不能倒退。为何呢?
	 * 可以想一下,如果一个进程刚睡眠了1ms,然后醒来你却要奖励3ms(虚拟运行时间减去3ms),然后他竟然赚了2ms。
	 * 作为一个公平的调度器,不做这种傻逼事情。你睡眠100ms,奖励你3ms没问题。
	 */
	se->vruntime = max_vruntime(se->vruntime, vruntime);
}

新创建的进程惩罚的时间是多少

在update_curr函数中,我们可以看到惩罚的时间计算函数是sched_vslice()函数,即将sched_slice(cfs_rq, se)算出来的实际运行时间转换为虚拟运行时间。

/*
 * We calculate the vruntime slice of a to-be-inserted task.
 *
 * vs = s/w
 */
static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	return calc_delta_fair(sched_slice(cfs_rq, se), se);
}

calc_delta_fair函数我们之前介绍过,运行依据公式为:delta = delta * (NICE_0_LOAD / se->load.weight),此处就不过多赘述。

实际运行时间的计算

delta值由sched_slice函数计算得到:

/*
 * 计算se的实际运行时间(wall-time)。
 */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);  // 根据就绪队列中调度实体个数计算调度周期。

	for_each_sched_entity(se) {  // 根据se->paren往上计算权重比例。
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;  // 得到当前se所在的就绪队列的权重,也就是就绪队列上所有调度实体权重之和。

		if (unlikely(!se->on_rq)) {
			lw = cfs_rq->load;

			update_load_add(&lw, se->load.weight);
			load = &lw;
		}

		/* 
		 * 计算调度时间,slice = period * se->load.weight / cfs_rq->load.weight
		 */
		slice = __calc_delta(slice, se->load.weight, load);
	}
	return slice;
}

__calc_delta的详细介绍见前文。

唤醒新创建的子进程

经过copy_process创建了子进程之后,我们就可以唤醒新进程准备运行。也就是将新进程加入就绪队列准备调度。
wake_up_new_task函数负责唤醒新创建的进程,并将进程加入就绪队列里等待调度。

/*
 * wake_up_new_task - wake up a newly created task for the first time.
 *
 * This function will do some initial scheduler statistics housekeeping
 * that must be done for every newly created context, then puts the task
 * on the runqueue and wakes it.
 */
void wake_up_new_task(struct task_struct *p)
{
	struct rq_flags rf;
	struct rq *rq;

	raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
	p->state = TASK_RUNNING;
#ifdef CONFIG_SMP
	p->recent_used_cpu = task_cpu(p);
	__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));  // 选择调度类中最空闲的CPU
#endif
	rq = __task_rq_lock(p, &rf);
	update_rq_clock(rq);
	post_init_entity_util_avg(p);

	activate_task(rq, p, ENQUEUE_NOCLOCK);  // 把进程加入到相应的就绪队列
	trace_sched_wakeup_new(p);
	check_preempt_curr(rq, p, WF_FORK);  // 既然新进程已经准备就绪,那么此时需要检查新进程是否满足抢占当前正在运行进程的条件,如果满足则设置TIF_NEED_RESCHED标志位。
}

将新进程加入到对应的就绪队列

activate_task(rq, p, ENQUEUE_NOCLOCK)这一步将进程p加入到就绪队列rq中:

void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
	enqueue_task(rq, p, flags);
}
static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
	p->sched_class->enqueue_task(rq, p, flags);
}

CFS的入队函数如下:

const struct sched_class fair_sched_class = {
	.enqueue_task		= enqueue_task_fair,  // 当任务进入可运行状态时,用此函数将调度实体存放在红黑树中。(linux对于可运行状态定义比较模糊,可运行状态包括就绪态和运行态)
}
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;

	for_each_sched_entity(se) {
		if (se->on_rq)  // 判断调度实体是否已经在就绪队列中。值为1代表在就绪队列中,当然就不需要添加到就绪队列了。
			break;
		cfs_rq = cfs_rq_of(se);  
		enqueue_entity(cfs_rq, se, flags);  // 将调度实体加入到就绪队列。
	}
}

我们暂时不关注组调度,来看一下将调度实体加入到就绪队列的具体逻辑:

static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se;
	
	for_each_sched_entity(se) {
		if (se->on_rq)  // 判断调度实体是否已经在就绪队列中。值为1代表在就绪队列中,当然就不需要添加到就绪队列了。
			break;
		cfs_rq = cfs_rq_of(se);
		enqueue_entity(cfs_rq, se, flags);  // 将调度实体加入到就绪队列。
	}
}

顺着继续往下看enqueue_entity函数:

// 被调度实体入队
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
	bool curr = cfs_rq->curr == se;

	update_curr(cfs_rq);  // 更新cfs_rq中正在cpu上运行的调度实体的运行时间信息(上次运行(启动)开始时间、总的实际运行时间、虚拟运行时间等等)

	if (renorm && !curr)
		se->vruntime += cfs_rq->min_vruntime;  // 调度实体在进入某个cpu中具体的就绪队列时,调度实体的虚拟运行时间需要加上当前就绪队列的虚拟运行时间。这样才能和其余调度实体一起去调度,要不然当前调度实体的虚拟运行时间可就太低了,上去就会把cpu霸占着不放。

	/*
	 * When enqueuing a sched_entity, we must:
	 *   - Update loads to have both entity and cfs_rq synced with now.
	 *   - Add its load to cfs_rq->runnable_avg
	 *   - For group_entity, update its weight to reflect the new share of
	 *     its group cfs_rq
	 *   - Add its new weight to cfs_rq->load.weight
	 */
	update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);  // load_avg成员更新信息,传递flag包含DO_ATTACH。当进程创建第一次调用update_load_avg()函数时,这个flag会用上。
	update_cfs_group(se);
	enqueue_runnable_load_avg(cfs_rq, se);  // 更新runnable_load_avg信息。
	account_entity_enqueue(cfs_rq, se);  // 更新就绪队列的相关信息,比如nr_running加1,就绪队列权重加上当前调度实体的权重。

	if (flags & ENQUEUE_WAKEUP)  // 针对被唤醒进程,新进程第一次加入就绪队列就不需要做下面的place_entity操作。
		place_entity(cfs_rq, se, 0);  // 针对唤醒进程,进行一定的补偿(即加上一定的虚拟运行时间),要不然你睡醒之后虚拟运行时间较小,可能会长时间霸占cpu。

	check_schedstat_required();
	update_stats_enqueue(cfs_rq, se, flags);
	check_spread(cfs_rq, se);
	if (!curr)
		__enqueue_entity(cfs_rq, se);  // 当前调度实体不是se,将se加入到就绪队列维护的红黑树中,所有的se以vruntim为key。
	se->on_rq = 1;  // se加入就绪队列完成,on_rq设置为1,代表调度实体se已经在就绪队列中了。
}

检测新进程是否可以抢占正在运行的进程

当唤醒一个新进程的时候,此时也是一个检测抢占的机会。因为唤醒的进程有可能具有更高的优先级或者更小的虚拟运行时间。

/*
 * 用于在待运行任务插入rq后,检查是否应该抢占正在CPU上运行的当前任务。
 * wakeup preemption(唤醒抢占)的实现逻辑主要在这里。对CFS调度器而言,主要是在“是否能满足延迟调度时延”和“是否保证足够任务运行时间”之间来取舍。
*/
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
	const struct sched_class *class;

	if (p->sched_class == rq->curr->sched_class) {  // 如果p的调度类和cpu上正在运行的进程属于同一个调度类
		rq->curr->sched_class->check_preempt_curr(rq, p, flags);  // 则调用该调度类的check_preempt_curr方法检查抢占条件,来决定是否抢占。
	} else {
		for_each_class(class) {  // 从高优先级调度类开始轮循
			if (class == rq->curr->sched_class)
				break;
			if (class == p->sched_class) {  // 因为是从高优先级调度类开始往低优先级调度类轮循,
			                                // 所以当进程p的优先级先满足的时候,即p的优先级高于当前正在cpu上运行的进程的优先级。即可发生抢占动作。
				resched_curr(rq);  // 
				break;
			}
		}
	}
}

CFS调度类的抢占

我们现在来看一下同为CFS调度类的情况下,如何进行抢占条件检查。

const struct sched_class fair_sched_class = {
	.check_preempt_curr	= check_preempt_wakeup,  // 检测是否可以抢占当前正在cpu上运行的进程。
}

从上面代码可以看出,CFS调度类的抢占条件检测为check_preempt_wakeup函数。

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
	struct task_struct *curr = rq->curr;
	struct sched_entity *se = &curr->se, *pse = &p->se;
	struct cfs_rq *cfs_rq = task_cfs_rq(curr);
	int scale = cfs_rq->nr_running >= sched_nr_latency;
	int next_buddy_marked = 0;

	if (unlikely(se == pse))  // 如果正在运行的调度实体和想要去抢占的进程p的调度实体是同一个,则是大水冲了龙王庙,直接返回。
		return;

	if (test_tsk_need_resched(curr))  // 当前正在运行的进程已经被设置了重新调度标志,则直接返回不进行抢占处理。
		return;

	/* Idle tasks are by definition preempted by non-idle tasks. */
	if (unlikely(task_has_idle_policy(curr)) &&  // 正在运行的进程是idle进程,而唤醒的进程不是idle进程,进行抢占。
	    likely(!task_has_idle_policy(p)))
		goto preempt;

	find_matching_se(&se, &pse);
	update_curr(cfs_rq_of(se));  // 更新当前进程的时间统计
	BUG_ON(!pse);
	if (wakeup_preempt_entity(se, pse) == 1) {  // 检查唤醒的进程是否满足抢占当前进程的条件,如果满足就触发调度。
		/*
		 * Bias pick_next to pick the sched entity that is
		 * triggering this preemption.
		 */
		if (!next_buddy_marked)
			set_next_buddy(pse);
		goto preempt;
	}
	return;
preempt:
	resched_curr(rq);  // 抢占处理
}

在抢占条件检测函数中,wakeup_preempt_entity负责真正地根据虚拟运行时间计算是否满足抢占条件。

static int
wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
{
	s64 gran, vdiff = curr->vruntime - se->vruntime;

	if (vdiff <= 0)  // 当前正在调度的实体虚拟运行时间比抢占实体的虚拟运行时间小,则不发生抢占。
		return -1;

	gran = wakeup_gran(se);  // 根据sysctl_sched_wakeup_granularity与se的权重计算出抢占成功需要的最小虚拟运行时间差值。目的是为了避免频繁抢占。
	if (vdiff > gran)
		return 1;

	return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux内核源码分析-进程调度(三)-从进程创建到唤醒的过程去了解CFS调度器 的相关文章

随机推荐

  • PS故障风海报制作技术分享

    1 首先找一张看起来很酷的图 也可以选择自己喜欢的图片 2 复制图层 点击添加图层样式 选择混合选项 在高级混合里面的通道选项 有R G B三个通道选项 默认是全部勾选的状态 选择其中一个勾掉 关于通道应该选择哪一个或者哪几个 可以挨个尝试
  • GMM高斯混合模型聚类的EM估计过程matlab仿真

    目录 1 算法概述 2 仿真效果 3 MATLAB源码 1 算法概述 高斯混合模型 Gaussian Mixed Model 指的是多个高斯分布函数的线性组合 理论上GMM可以拟合出任意类型的分布 通常用于解决同一集合下的数据包含多个不同的
  • 算法,C技能树测评

    产品功能 技能树是一个帮助 IT 领域学习者进行职业成长的一站式学习工具 UI界面 产品交互 算法技能树 左边是对应算法技能树的目录 中间是内容 每个小知识点里面都会有对应的题目 完成题目这里就会显示绿色 没完成的则为白色 在中间每一题都有
  • 【TCP/IP】广播 - 定义、原理及编程实现

    本文共计2974字 预计阅读时间4分钟 目录 广播 广播的原理及形式 广播的编程与实现 套接字选项设置 发送者 接收者 拓展资料 广播 广播 Broadcast 是指封包在计算机网络中传输时 目的地址为网络中所有设备的一种传输方式 这里所说
  • PMBOK(第六版) PMP笔记——《第四章 项目整合管理》

    第 4 章 项目整合管理 从第四章开始 进入49个过程的学习 49个过程被划分为十大知识领域 分为十个章节 本章节是项目整合管理知识领域 主要讲述项目整合管理的7个过程 1 需要对什么进行整合管理 干系人需求 约束条件 项目管理各个过程 项
  • 创建图片外链——“极简图床”

    开发微信小程序项目 上传代码时要求项目文件大小不能超过2M 那么 当小程序页面里有很多图片时 如果全部放在项目文件中 就很容易使得代码包超过2M 为了压缩代码 这时就需要将图片放在其它平台上 然后在小程序项目代码中引用需要的图片的外链地址
  • Android控制界面布局的两种方式

    概念一 View Android所有UI组件都继承自View类 View类是一个抽象类 不能直接创建View类的对象 即不能直接实例化 通常是实例化View类的子类 即具体的UI组件或布局管理器 View类还有一个重要的子类 ViewGro
  • 播放器 potplayer rtsp播放器

    目录 potplayer potplayer 播放万播放下一曲 播放比例设置 html5 rtsp player GitHub Streamedian html5 rtsp player Play RTSP stream from IP c
  • 常用的正则表达式验证

    public abstract class RegexPatterns 手机号正则 public static final String PHONE REGEX 1 38 0 9 4 579 5 0 3 5 9 6 6 7 0135678
  • 彻底删除mac上的Win10分区

    背景 之前在优胜美地上用boot camp装的win10 后来升级到ei capitan了 问题 今天试着用boot camp移除windows 点继续就报错 说不能什么什么 然后用disk工具直接擦除了win10的分区并改为hfs分区 结
  • One stage&Two stage

    One stage Two stage 双阶段 单阶段 双阶段 单阶段 引文
  • 最热门的9个超级SEX问题

    无从选择 女人的身体被造物设计成传宗接代的载体 而SEX就是当初那只引诱我们上钩的苹果 不会忘记新生命诞生时 那场撕裂心肺的痛 那是女人完成自己使命的另类礼赞 那个曾经代表忠贞 圣洁的标签 被男人心怀窃喜地撕下 并据为己有 女人从此无法原价
  • Spring Boot中内置Tomcat最大连接数、线程数与等待数 实践调优

    在 Spring Boot 框架中 我们使用最多的是Tomcat 这是 Spring Boot 默认的容器技术 而且是内嵌式的 Tomcat Tomcat 是 Apache 基金下的一个轻量级的Servlet 容 器 支 持 Servlet
  • 离散型随机变量及其分布律(五)

    有些随机变量值是有限 它全部可能取到的个或可列无限多个 这种随机变量称为离散型随机变量 三种重要的离散型随机变量
  • [转载].一直不怎么明白PID的运算输出结果怎么换算成执行机构的控制量

    http bbs 21ic com icview 50831 1 1 html 小明接到这样一个任务 有一个水缸点漏水 而且漏水的速度还不一定固定不变 要求水面高度维持在某个位置 一旦发现水面高度低于要求位置 就要往水缸里加水 小明接到任务
  • 计算机系统概述

    操作系统的概念 功能 操作系统 Operating System OS 是指控制和管理整个计算机系统的硬件和软件资源 并合理地组织调度计算机的工作和资源的分配 以提供用户和其他软件方便的接口和环境 它说计算机系统中最基本的系统软件 操作系统
  • anaconda添加python虚拟环境

    先将anaconda的源更换为国内源 否则下载很慢 conda config add channels https mirrors tuna tsinghua edu cn anaconda pkgs free conda config a
  • feigin应用

    feigin发送post请求 1 服务方一定加上 RequestBody RequestMapping value queryPerson ResponseBody public Person queryPerson RequestBody
  • 如何快速上手一款新的嵌入式CPU芯片(记录CC2540开发经历)

    新换了工作 需要熟悉新公司的产品开发项目 更新博客就懈怠了 不过环境的不同 也让我对嵌入式开发有了更深刻的理解 在原公司我主要负责在STM32F207芯片平台上 利用UCOS LWIP进行嵌入式服务器开发 工作涉及底层硬件 RTOS 协议栈
  • Linux内核源码分析-进程调度(三)-从进程创建到唤醒的过程去了解CFS调度器

    从进程创建到唤醒的过程去了解CFS调度器 从do fork开始 创建子进程 初始化新建进程p相关的调度参数 cfs的task fork操作 更新cfs rq上正在运行的进程的运行时间信息 更新cfs rq的最小虚拟运行时间 更新进程p对应的