CFS ordonnancement analyse de code principal un

J'ai appris les principes de l'ordonnancement CFS et la structure principale des données à l'avant. Aujourd'hui, nous allons entrer dans l'analyse du code. Bien entendu, l'analyse de code ne porte que sur le tronc principal et non sur le capillaire, mais en même temps, nous analysons certains codes importants en fonction de la façon dont un processus est planifié.

Avant d'analyser le code, il y a quelques petites fonctions qui doivent être analysées en premier. Comme le dit le proverbe, les grands immeubles s'élèvent au sol, ces petites fonctions sont toujours très importantes.

calc_delta_fair

La fonction calc_delta_fair est une fonction utilisée pour calculer le temps d'exécution du processus. Dans les principes CFS précédents, j'ai appris que la formule de calcul du temps d'exécution d'un processus est la suivante:

                                       

Le temps d'exécution d'un processus est égal au temps d'exécution réel du processus multiplié par le poids correspondant du processus NICE0 divisé par le poids du processus en cours. Afin de s'assurer que les opérations en virgule flottante ne sont pas impliquées dans le calcul de la division, le noyau Linux évite les opérations en virgule flottante en décalant à gauche de 32 bits puis de 32 bits vers la droite. La formule modifiée est:

                                 

Et puis converti dans la formule suivante

                                  

Parmi eux, la valeur de inv_weight a été calculée dans le code du noyau. Lorsque vous l'utilisez, il vous suffit de consulter la table pour obtenir la valeur de Inv_weigth

/*
 * Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated.
 *
 * In cases where the weight does not change often, we can use the
 * precalculated inverse to speed up arithmetics by turning divisions
 * into multiplications:
 */
const u32 sched_prio_to_wmult[40] = {
 /* -20 */     48388,     59856,     76040,     92818,    118348,
 /* -15 */    147320,    184698,    229616,    287308,    360437,
 /* -10 */    449829,    563644,    704093,    875809,   1099582,
 /*  -5 */   1376151,   1717300,   2157191,   2708050,   3363326,
 /*   0 */   4194304,   5237765,   6557202,   8165337,  10153587,
 /*   5 */  12820798,  15790321,  19976592,  24970740,  31350126,
 /*  10 */  39045157,  49367440,  61356676,  76695844,  95443717,
 /*  15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};

De cette façon, le temps d'exécution d'un processus peut être facilement obtenu grâce à la méthode de calcul ci-dessus. Après avoir connu le processus de calcul, regardons le code

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);

	return delta;
}

 Si la valeur de poids de l'entité de planification actuelle est égale à NICE_0_LOAD, le temps d'exécution réel du processus est directement renvoyé. Parce que l'heure virtuelle du processus nice0 est égale à l'heure physique. Sinon, appelez la fonction __calc_delta pour calculer la durée d'exécution du processus

static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
	u64 fact = scale_load_down(weight);
	int shift = WMULT_SHIFT;

	__update_inv_weight(lw);

	if (unlikely(fact >> 32)) {
		while (fact >> 32) {
			fact >>= 1;
			shift--;
		}
	}

	/* hint to use a 32x32->64 mul */
	fact = (u64)(u32)fact * lw->inv_weight;

	while (fact >> 32) {
		fact >>= 1;
		shift--;
	}

	return mul_u64_u32_shr(delta_exec, fact, shift);
}

Enfin, le temps d'exécution virtuel d'un processus peut être calculé par la formule de calcul ci-dessus. Le code détaillé n'est pas poussé, il n'est pas nécessaire. Les personnes intéressées peuvent y jeter un œil.

sched_slice

Cette fonction est utilisée pour calculer le temps d'exécution qu'une entité de planification peut allouer au cours d'une période de planification

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) {
		struct load_weight *load;
		struct load_weight lw;

		cfs_rq = cfs_rq_of(se);
		load = &cfs_rq->load;

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

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

La fonction __sched_period est une fonction permettant de calculer la période de planification. Lorsque le nombre de processus est inférieur à 8, la période de planification est égale au délai de planification égal à 6 ms. Sinon, la période de planification est égale au nombre de processus multiplié par 0,75 ms, ce qui indique qu'un processus peut s'exécuter au moins 0,75 ms pour empêcher le changement de contexte de se produire trop rapidement.

L'étape suivante consiste à parcourir l'entité de planification actuelle. Si l'entité de planification n'a pas de relation de groupe de planification, elle ne s'exécute qu'une seule fois. Obtenez la file d'attente d'exécution CFS actuelle cfs_rq, obtenez le poids de la file d'attente d'exécution cfs_rq-> rq représente le poids de cette file d'attente d'exécution. Enfin, le temps d'exécution réel de ce processus est calculé par __calc_delta.

__calc_delta Cette fonction a été introduite lors du calcul de la fonction virtuelle auparavant, elle peut non seulement calculer le temps virtuel d'un processus, elle est ici pour calculer le temps d'exécution d'un processus dans le cycle de planification total, la formule est:

进程的运行时间 = (调度周期时间 * 进程的weight) / CFS运行队列的总weigth

place_entity

Cette fonction est utilisée pour punir une entité de programmation, l'essentiel est de modifier sa valeur vruntime

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);

	/* 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. */
	se->vruntime = max_vruntime(se->vruntime, vruntime);
}
  • Obtenez la valeur min_vruntime de la file d'attente d'exécution CFS actuelle
  • Lorsque le paramètre initial est égal à true, il représente un processus nouvellement créé et le processus nouvellement créé ajoute de la valeur à son vruntime, ce qui signifie qu'il le punit. Ceci est une punition pour le processus nouvellement créé, car le temps d'exécution du processus nouvellement créé est trop petit pour l'empêcher d'occuper le CPU
  • Si l'initiale n'est pas vraie, elle représente le processus d'éveil. Vous devez prendre soin du processus d'éveil. Le plus grand soin est la moitié du délai de planification.
  • Assurez-vous que le vruntime de l'entité de planification ne peut pas revenir en arrière et obtenez le vruntime maximum via max_vruntime.

update_curr

La fonction update_curr est utilisée pour mettre à jour les informations de durée d'exécution du processus en cours

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_clock_task(rq_of(cfs_rq));
	u64 delta_exec;

	if (unlikely(!curr))
		return;

	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;

	curr->exec_start = 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);

	curr->vruntime += calc_delta_fair(delta_exec, curr);
	update_min_vruntime(cfs_rq);


	account_cfs_rq_runtime(cfs_rq, delta_exec);
}
  • delta_exec = now-curr-> exec_start; Calcule la différence entre le processus de file d'attente d'exécution CFS actuel et la dernière heure virtuelle mise à jour
  • curr-> exec_start = now; mettre à jour la valeur de exec_start
  • curr-> sum_exec_runtime + = delta_exec; mettre à jour la durée totale d'exécution du processus en cours
  • Calculer le temps virtuel du processus en cours par calc_delta_fair
  • Mettez à jour la valeur minimale de vruntime dans la file d'attente d'exécution CFS via la fonction update_min_vruntime

Création de nouveaux processus

À travers le nouveau processus de création de processus, pour analyser comment le planificateur CFS configure le processus nouvellement créé. Lorsque nous avons créé un nouveau processus dans fork, nous l'avons introduit dans le module sched. Ici, nous nous concentrons sur l'analyse.

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
	unsigned long flags;

	__sched_fork(clone_flags, p);
	/*
	 * We mark the process as NEW here. This guarantees that
	 * nobody will actually run it, and a signal or other external
	 * event cannot wake it up and insert it on the runqueue either.
	 */
	p->state = TASK_NEW;

	/*
	 * Make sure we do not leak PI boosting priority to the child.
	 */
	p->prio = current->normal_prio;


	if (dl_prio(p->prio))
		return -EAGAIN;
	else if (rt_prio(p->prio))
		p->sched_class = &rt_sched_class;
	else
		p->sched_class = &fair_sched_class;

	init_entity_runnable_average(&p->se);

	raw_spin_lock_irqsave(&p->pi_lock, flags);
	/*
	 * We're setting the CPU for the first time, we don't migrate,
	 * so use __set_task_cpu().
	 */
	__set_task_cpu(p, smp_processor_id());
	if (p->sched_class->task_fork)
		p->sched_class->task_fork(p);
	raw_spin_unlock_irqrestore(&p->pi_lock, flags);

	init_task_preempt_count(p);
	return 0;
}
  • __sched_fork consiste principalement à initialiser l'entité de planification, ici n'a pas besoin d'hériter du processus parent, car le processus enfant se réexécutera, ces valeurs seront le processus de recopie
  • Définissez le statut du processus sur TASK_NEW, ce qui signifie qu'il s'agit d'un nouveau processus
  • Définissez la priorité du processus actuel en cours sur le processus nouvellement créé, la priorité dynamique du processus nouvellement créé p-> prio = current-> normal_prio
  • Définissez la classe de programmation du processus en fonction de la priorité du processus, s'il s'agit d'un processus RT, définissez la classe de programmation sur rt_sched_class, s'il s'agit d'un processus normal, définissez la classe de programmation sur fair_sched_class
  • Définissez le processeur sur lequel le processus actuel s'exécute, voici simplement un paramètre simple. Il sera également défini une fois lors de la connexion à la file d'attente d'exécution du planificateur.
  • Enfin, appelez le pointeur de la fonction task_fork dans la classe de planification, et enfin appelez le pointeur de la fonction task_fork dans la classe fair_sched_class
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);
	curr = cfs_rq->curr;
	if (curr) {
		update_curr(cfs_rq);
		se->vruntime = curr->vruntime;
	}
	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);
	}

	se->vruntime -= cfs_rq->min_vruntime;
	rq_unlock(rq, &rf);
}
  • Obtenez la file d'attente en cours d'exécution CFS via current, obtenez l'entité de planification actuelle via le pointeur curr de la file d'attente en cours d'exécution, puis mettez à jour le temps d'exécution de l'entité de planification actuelle via update_curr, et affectez en même temps la valeur virtuelle vruntime de l'entité de planification actuelle à la vruntime du processus nouvellement créé.
  • Pénaliser le processus nouvellement créé via la fonction place_entity, en pensant que le troisième paramètre est 1, le processus nouvellement créé sera puni
  • se-> vruntime- = cfs_rq-> min_vruntime; soustraire min_vruntime du temps d'exécution virtuel de l'entité de planification actuelle. Cette phrase peut être comprise car il y a encore un certain temps avant que l'entité de planification soit ajoutée à la file d'attente en cours d'exécution. Pendant cette période, min_vruntime La valeur va changer. Lorsqu'il est ajouté à la file d'attente d'exécution, cela semble juste.

Ce qui précède est le flux de processus nouvellement créé, résumé avec l'organigramme suivant

       

Ajouter le nouveau processus à la file d'attente prête

Une fois le processus fork terminé, il réveillera un processus via la fonction wake_up_new_task et ajoutera le processus nouvellement créé à la file d'attente prête

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
	/*
	 * Fork balancing, do it here and not earlier because:
	 *  - cpus_allowed can change in the fork path
	 *  - any previously selected CPU might disappear through hotplug
	 *
	 * Use __set_task_cpu() to avoid calling sched_class::migrate_task_rq,
	 * as we're not fully set-up yet.
	 */
	p->recent_used_cpu = task_cpu(p);
	__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
	rq = __task_rq_lock(p, &rf);
	update_rq_clock(rq);
	post_init_entity_util_avg(&p->se);

	activate_task(rq, p, ENQUEUE_NOCLOCK);
	p->on_rq = TASK_ON_RQ_QUEUED;
	trace_sched_wakeup_new(p);
	check_preempt_curr(rq, p, WF_FORK);
	task_rq_unlock(rq, p, &rf);
}
  • Définissez l'état du processus sur TASK_RUNNING, ce qui signifie que le processus est déjà dans un état prêt
  • Si vous ouvrez SMP, il réinitialisera un processeur optimal via __set_task_cpu et laissera le nouveau processus s'exécuter dessus
  • Enfin, activate_task (rq, p, ENQUEUE_NOCLOCK); fonction pour ajouter le processus nouvellement créé à la file d'attente prête
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
	if (task_contributes_to_load(p))
		rq->nr_uninterruptible--;

	enqueue_task(rq, p, flags);
}

static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
	if (!(flags & ENQUEUE_NOCLOCK))
		update_rq_clock(rq);

	if (!(flags & ENQUEUE_RESTORE)) {
		sched_info_queued(rq, p);
		psi_enqueue(p, flags & ENQUEUE_WAKEUP);
	}

	p->sched_class->enqueue_task(rq, p, flags);
}

Appellera éventuellement le pointeur de la fonction enqueue_task dans la classe de programmation CFS.

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)
			break;
		cfs_rq = cfs_rq_of(se);
		enqueue_entity(cfs_rq, se, flags);

		/*
		 * end evaluation on encountering a throttled cfs_rq
		 *
		 * note: in the case of encountering a throttled cfs_rq we will
		 * post the final h_nr_running increment below.
		 */
		if (cfs_rq_throttled(cfs_rq))
			break;
		cfs_rq->h_nr_running++;

		flags = ENQUEUE_WAKEUP;
	}

}
  • Si le on_rq de l'entité de planification a été défini, le représentant est dans la file d'attente prête et saute directement
  • La fonction enqueue_entity mettra en file d'attente l'entité de planification
  • Augmentez le nombre de files d'attente d'exécution CFS pouvant être exécutées h_nr_running
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;

	/*
	 * If we're the current task, we must renormalise before calling
	 * update_curr().
	 */
	if (renorm && curr)
		se->vruntime += cfs_rq->min_vruntime;

	update_curr(cfs_rq);

	/*
	 * Otherwise, renormalise after, such that we're placed at the current
	 * moment in time, instead of some random moment in the past. Being
	 * placed in the past could significantly boost this task to the
	 * fairness detriment of existing tasks.
	 */
	if (renorm && !curr)
		se->vruntime += cfs_rq->min_vruntime;

	/*
	 * 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);
	update_cfs_group(se);
	enqueue_runnable_load_avg(cfs_rq, se);
	account_entity_enqueue(cfs_rq, se);

	if (flags & ENQUEUE_WAKEUP)
		place_entity(cfs_rq, se, 0);

	check_schedstat_required();
	update_stats_enqueue(cfs_rq, se, flags);
	check_spread(cfs_rq, se);
	if (!curr)
		__enqueue_entity(cfs_rq, se);
	se->on_rq = 1;

	if (cfs_rq->nr_running == 1) {
		list_add_leaf_cfs_rq(cfs_rq);
		check_enqueue_throttle(cfs_rq);
	}
}
  • se-> vruntime + = cfs_rq-> min_vruntime; ajoutez le temps virtuel de l'entité de planification en arrière, min_vruntime a été soustrait dans le fork avant, maintenant il doit être ajouté en arrière, maintenant le min_vruntime est plus précis
  • update_curr (cfs_rq); pour mettre à jour le temps d'exécution de l'entité de planification actuelle et le min_vruntime de la file d'attente d'exécution CFS
  • Grâce aux commentaires, lorsqu'une entité de planification est ajoutée à la file d'attente prête, la charge de la file d'attente en cours d'exécution et la charge de l'entité de planification doivent être mises à jour
  • Si ENQUEUE_WAKEUP est défini, cela signifie que le processus en cours est un processus de réveil et qu'une certaine compensation est requise
  • __enqueue_entity ajoute l'entité de planification à l'arbre CFS rouge-noir
  • se-> on_rq = 1; définissez on_rq sur 1, ce qui signifie qu'il a été ajouté à la file d'attente d'exécution

Sélectionnez le prochain processus en cours

Lorsqu'un processus est créé par fork, puis ajouté à l'arborescence rouge et noire de la file d'attente d'exécution CFS, nous devons sélectionner son exécution, nous examinons directement la fonction de planification. Afin de faciliter la lecture de la dorsale, le code est simplifié

static void __sched notrace __schedule(bool preempt)
{

	cpu = smp_processor_id();             //获取当前CPU
	rq = cpu_rq(cpu);                     //获取当前的struct rq, PER_CPU变量
	prev = rq->curr;                      //通过curr指针获取当前运行进程


       next = pick_next_task(rq, prev, &rf);   //通过pick_next回调选择进程

	if (likely(prev != next)) {
		
		rq = context_switch(rq, prev, next, &rf);  //如果当前进程和下一个进程不同,则发生切换
       }
  • Obtenez le prochain processus en cours d'exécution via pick_next
  • Changement de contexte via context_switch
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;

	if (likely((prev->sched_class == &idle_sched_class ||
		    prev->sched_class == &fair_sched_class) &&
		   rq->nr_running == rq->cfs.h_nr_running)) {

		p = fair_sched_class.pick_next_task(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))
			goto again;

		/* Assumes fair_sched_class->next == idle_sched_class */
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev, rf);

		return p;
	}

again:
	for_each_class(class) {
		p = class->pick_next_task(rq, prev, rf);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

}

Les deux étapes principales de pick_next

  • Parce qu'il y a tellement de processus communs dans le système, nous faisons ici une optimisation en jugeant si la classe de planification du processus en cours et le nombre de processus exécutables dans la file d'attente d'exécution sont égaux au nombre de processus exécutables dans la file d'attente d'exécution CFS. Si c'est la même chose, cela signifie que les processus restants sont des processus ordinaires, puis appellent directement le rappel pick_next dans fair_sched_class
  • Sinon, passez à nouveau, traversez honnêtement l'appel au pointeur de la fonction pick_next_task dans l'ordre de la priorité de la classe de planification de haut en bas
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	struct cfs_rq *cfs_rq = &rq->cfs;
	struct sched_entity *se;
	struct task_struct *p;
	int new_tasks;

again:
	if (!cfs_rq->nr_running)
		goto idle;

	put_prev_task(rq, prev);

	do {
		se = pick_next_entity(cfs_rq, NULL);
		set_next_entity(cfs_rq, se);
		cfs_rq = group_cfs_rq(se);
	} while (cfs_rq);
  • S'il n'y a plus de processus dans la file d'attente d'exécution CFS, le processus inactif sera renvoyé
  • pick_next_entry obtiendra une entité de planification à partir du nœud le plus à gauche de l'arbre CFS rouge-noir
  • set_next_entry définit l'entité de planification suivante sur le pointeur de courant de la file d'attente d'exécution CFS
  • Plus tard, context_switch changera, le contenu du commutateur sera présenté dans les chapitres suivants

 

Publié 187 articles originaux · gagné 108 · 370 000 vues

Je suppose que tu aimes

Origine blog.csdn.net/longwang155069/article/details/104578189
conseillé
Classement