2023-06-11  阅读(4)
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10946851.html

CFS是内核使用的一种调度器或调度类,它主要负责处理三种调度策略:SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE。调度器的核心在挑选下一个运行的进程时有可能会遍历所有的调度类别。实际上系统大多数进程通常都是CFS调度类负责处理的,因此为了优化下一个进程的挑选调度器核心会先判断当前进程是否采用了CFS调度策略,若是,则直接调用CFS代码来挑选下一个进程,若不是或CFS代码未能挑选到一个合适的进程,则会调用各个调度类的挑选函数来寻找一个合适的进程。若CFS代码寻找到了合适的下一个运行的进程,则直接返回该进程的实例而不会再遍历调度类。本文将主要关注下列的CFS活动和行为:

  • 将一个任务插入运行队列
  • 从运行队列中挑选一个合适的任务
  • 从运行队列中移除一个任务

1 调度器的数据结构

linux内核采用进程描述符(PCB)来描述和抽象一个进程,数据结构task_struct用于描述进程的运行状态和控制状态全部信息,其相关结构信息如下:

    struct task_struct {
    	//当前的运行状态
    	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    	//SMP会用到
    #ifdef CONFIG_SMP
    	int on_cpu;			    /* 在哪个CPU上运行 */
    #ifdef CONFIG_THREAD_INFO_IN_TASK
    	unsigned int cpu;	     /* current CPU */
    #endif
    	unsigned int wakee_flips;   /* 用于wakee affine特性 */
    	unsigned long wakee_flip_decay_ts;  /* 用于记录上一次wakee_flip时间 */
    	struct task_struct *last_wakee;     /* 表示上一次唤醒的是哪个进程 */
    
    	int wake_cpu;						/* 表示进程唤醒在哪个CPU */
    #endif
        /* 用于进程的状态,支持状态如下
        - TASK_ON_RQ_QUEUED:表示进程正在就绪队列中运行
        - TASK_ON_RQ_MIGRATING:表示处于迁移过程中的进程,可能不在就绪队列中 */
    	int on_rq;							
    	//进程的动态优先级,静态优先级,给予动态优先级和调度策略计算出来的优先级 
    	int prio, static_prio, normal_prio;
    	unsigned int rt_priority;          /* 实时进程优先级 */
    	const struct sched_class *sched_class;    /*调度类*/
    	struct sched_entity se;					  /*普通进程调度类实体 */
    	struct sched_rt_entity rt;				  /* 实时进程 */
    #ifdef CONFIG_CGROUP_SCHED
    	struct task_group *sched_task_group;      /*  组调度 */
    #endif
    	struct sched_dl_entity dl;                /* deadline调度类实体 */
    	int nr_cpus_allowed;                      /* 进程允许运行的CPU个数 */
    	cpumask_t cpus_allowed; 				  /* 进程允许运行的CPU位图 */
    #ifdef CONFIG_SCHED_INFO
    	struct sched_info sched_info;			  /* 进程调度相关信息 */
    #endif
    };

static_prio表示进程的静态优先级。静态优先级是进程启动时分配的优先级。它可以用nice和sched_setscheduler系统调用修改,否则在进程运行期间会一直保持恒定
normal_priority表示基于进程的静态优先级和调度策略计算出的优先级。调度器考虑的优先级则保存在prio。由于在某些情况下内核需要暂时提高进程的优先级,因此需要第3个成员来表示

进程调度有一个非常重要的数据结构sched_entity,被称为调度实体,它描述了进程作为一个调度实体参与进程调度所需要的全部信息,其数据结构定义在include/linux/sched.h

    struct sched_entity {
    	struct load_weight	load;				/* 调度实体的权重 */
    	struct rb_node		run_node;			/* 调度实体作为一个节点插入到CFS的红黑树中 */
      	/* 在就绪队列中有一个链表rq->cfs_tasks,调度实体添加到就绪队列后添加到该链表 */
    	struct list_head	group_node;			
      	 /* 进程加入到就绪队列,该位被置1,退出就绪队列,被清0,用于表示是否在运行队列中 */
    	unsigned int		on_rq;             
    	/* 统计时间信息 */
    	u64			exec_start;					/* 调度实体虚拟时间的起始时间 */
    	u64			sum_exec_runtime;			/* 调度实体总的运行时间,实际时间 */
    	u64			vruntime;					/* 调度实体的虚拟时间 */
    	u64			prev_sum_exec_runtime;		/* 上一次统计调度实体运行总时间 */
    
    	u64			nr_migrations;				/* 该调度实体发生迁移的次数 */
    
    #ifdef CONFIG_SCHEDSTATS
    	struct sched_statistics statistics;		/* 统计信息 */
    #endif
    
    #ifdef CONFIG_FAIR_GROUP_SCHED
    	int			depth;				/* 任务组的深度,其中根任务组的深度为0,逐级往下增加 */
    	struct sched_entity	*parent;	/* 指向调度实体的父对象 */
    	/* rq on which this entity is (to be) queued: */
    	struct cfs_rq		*cfs_rq;	/* 指向调度实体归属的CFS队列,也就是需要入列的CFS队列 */
    	/* rq "owned" by this entity/group: */
    	struct cfs_rq		*my_q;		/* 指向归属于当前调度实体的CFS队列,用于包含子任务或子的任务组 */
    #endif
    // 用于调度实体的负载计算(`PELT`)
    #ifdef CONFIG_SMP
    	struct sched_avg	avg ____cacheline_aligned_in_smp;
    #endif
    };

rq数据结构是描述CPU通用就绪队列,rq数据结构记录了一个就绪队列所需要的全部信息,包括一个CFS就绪队列数据结构,实时进程调度就绪队列等,其定义在kernel/sched/sched.h,重点的数据成员为

    struct rq {
    	raw_spinlock_t lock;						/* 用于保护通用就绪队列的自旋锁 */
    	unsigned int nr_running;					/* 就绪队列中可运行的进程数量 */
    	#define CPU_LOAD_IDX_MAX 5
    	unsigned long cpu_load[CPU_LOAD_IDX_MAX];	/* 每个就绪队列维护一个cpu_load,在scheduler tick中重新计算,负载更均衡 */
    	struct load_weight load;					/* 就绪队列的权重 */
    	unsigned long nr_load_updates;				/* 记录cpu_load的更新次数 */
    	u64 nr_switches;							/* 记录进程切换的次数 */
    
    	struct cfs_rq cfs;							/* 指向CFS的就绪队列 */
    	struct rt_rq rt;							/* 指向实时进程的就绪队列 */
    	struct dl_rq dl;							/* 指向deadline进程的就绪队列 */
    	unsigned long nr_uninterruptible;			/* 统计不可中断的状态进程进入就绪队列的数量 */
    
    	struct task_struct *curr, *idle, *stop;		/* 指向正在运行的进程、idle、系统的stop进程 */
    	unsigned long next_balance;					/* 下一次做负载均衡的时间 */
    	struct mm_struct *prev_mm;					/* 进程切换用于指向前任进程的内存结构描述符mm */
    
    	unsigned int clock_skip_update;				/* 用于更新就绪队列的时钟的标志位 */
    	u64 clock;									/* 每次时钟节拍到来时候会更新这个时钟 */
    	u64 clock_task;								/* 计算进程vruntime时使用的该时钟 */
    	
    #ifdef CONFIG_SMP
    	struct root_domain *rd;						/* 调度域的根 */
    	struct sched_domain *sd;					/* 指向CPU对应的最低等级的调度域 */
    
    	unsigned long cpu_capacity;					/* CPU对应普通进程的量化计算能力 */
    	unsigned long cpu_capacity_orig;
    	int cpu;									/* 用于表示就绪队列运行在哪个CPU上 */
    	int online;									/* 用于表示CPU处于active状态或online状态 */
    
    	struct list_head cfs_tasks;					/* 可运行的调度实体会添加到该链表 */
    };

系统中每个CPU都有一个就绪队列,它是一个pre-cpu的变量,即每个CPU都有一个rq的数据结构,可以通过this_rq()可获取当前CPU的数据结构rq。

    DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
    
    #define cpu_rq(cpu)		(&per_cpu(runqueues, (cpu)))
    #define this_rq()		this_cpu_ptr(&runqueues)
    #define task_rq(p)		cpu_rq(task_cpu(p))
    #define cpu_curr(cpu)		(cpu_rq(cpu)->curr)
    #define raw_rq()		raw_cpu_ptr(&runqueues)

cfs_rq是表示CFS就绪队列的数据结构,其主要定义如下:

    struct cfs_rq {
    	struct load_weight load;							/* 就绪队列的总权重 */
    	unsigned int nr_running, h_nr_running;				/* 可运行状态的进程数量 和组调度机制下可运行状态的进程数量 */
    
    	u64 exec_clock;										/* 统计就绪队列总的运行时间 */
    	u64 min_vruntime;									/* 用于跟踪CFS就绪队列中红黑树最小vruntime */
    #ifndef CONFIG_64BIT
    	u64 min_vruntime_copy;								
    #endif
    
    	struct rb_root tasks_timeline;						/* 红黑树的根 */
    	struct rb_node *rb_leftmost;						/* 指向红黑树中最左边的调度实体 */
    	struct sched_entity *curr, *next, *last, *skip;		/* 正在运行的进程,切换的下一个进程 */
    #ifdef CONFIG_FAIR_GROUP_SCHED
    	struct rq *rq;	/* cpu runqueue to which this cfs_rq is attached */
    	int on_list;
    	struct list_head leaf_cfs_rq_list;
    	struct task_group *tg;	/* group that "owns" this runqueue */
    #endif /* CONFIG_FAIR_GROUP_SCHED */
    };

网上有一张画的很好的组织关系图

202306111306502661.png

  • struct task_struct:任务的描述符,包含了进程的所有信息,该结构中的struct sched_entity,用于参与CFS的调度
  • struct sched_entity:每一个调度类并不是直接管理task_struct,而是引入调度实体的概念,这个也是CFS调度管理的对象了,描述了进程作为调度实体参与调度的所需要的所有信息,调度实体会作为一个节点插入到CFS的红黑树中
  • struct rq:每个CPU都有一个对应的运行队列,rq数据结构记录了一个就绪队列所需要的全部信息,包括一个CFS就绪队列数据结构,实时进程调度就绪队列以及就绪队列的负载权重等信息
  • struct cfs_rq:CFS运行队列,该结构中包含了struct rb_root_cached红黑树,用于链接调度实体struct sched_entity。rq运行队列中对应了一个CFS运行队列,此外,在task_group结构中也会为每个CPU再维护一个CFS运行队列,所以CFS调度器使用cfs_rq跟踪就绪队列信息以及管理就绪态调度实体,并维护一棵按照虚拟时间排序的红黑树
  • struct task_group:组调度,Linux支持将任务分组来对CPU资源进行分配管理,该结构中为系统中的每个CPU都分配了struct sched_entity调度实体和struct cfs_rq运行队列,其中struct sched_entity用于参与CFS的调度

2 调度器类

对于CPU资源,本身数量是有限的,而在系统中运行状态的进程数量有很多,就设计到资源的调度,此时就需要设计一种方法,尽可能的保证这种资源被公平的分配到进程中,就涉及到以下的概念

  • 核心调度器:对外提供周期性调度(定时触发)以及主调度器
  • 就绪队列:当所有当前运行的进程都在这个队列中维护,需要选择下一个执行的进程
  • 调度优先级:给不同的进程不同的优先级,这样分配到的实际运行时间片不一样
  • 调度算法:不同类型的进程使用不同的调度算法来选择执行的jinchen

进程调度,就是来管理系统中所有的进程,调度器的工作方式与这些结构的设计密切相关,几个组件在许多方面彼此交互,其如下图所示

202306111306512002.png

  • **激活进程调度(Scheduler Core)层:**可以用两种方式激活调度,一种是直接的,比如进程打算睡眠或者其他原因放弃CPU;另外一种是通过周期性的机制,以固定的频率运行,不时检测是否有必要进行进程切换,这个在**进程管理(二十)----进程调度器有介绍
  • **上下文切换:**当进程A切换到进程B的时候,如何能正常切换回去,需要保存当时的现场,包含用户空间的页表、用户空间的栈和硬件上下文信息,[ 进程管理(十九)----linux内核进程上下文(二) ]((1条消息) 进程管理(十九)----linux内核进程上下文(二)_奇小葩-CSDN博客)这个有介绍
  • **调度器类:**当调度器被选用时,它会查询调度器类,从中获取接下来运行哪个进程,同时在选中将要运行的进程之后,必须执行底层的任务切换,就需要跟CPU的紧密交互,这里面会包含很多的调度器类,例如我们熟悉的CFS,实时调度类等

调度器类提供了通用调度方法和各个调度方法之间的关联,调度器类由特定的数据结构中汇集的几个函数指针表示,如下图所示

202306111306519463.png

每次调用调度器时,它会挑选具有最高优先级的进程,把CPU提供给该进程,调度器通过将进程在红黑树中排序,跟踪进程的等待时间。

202306111306531344.png

所有可运行的进程按照时间在一个红黑树中排序,所谓时间为的等待时间,等待CPU时间最长的进程是最左侧的项,调度器下一次调度会考虑该进程。

  • **就绪队列:**用于管理活动进程的主要数据结构,各个CPU都有自身的就绪队列,各个活动进程只出现在一个就绪队列中,在各个CPU上同上运行一个进程是不可能的。就绪队列是全局调度器操作的起点,进程不是直接由就绪队列的成员直接管理,还是由CPU的调度类负责,因此各个就绪队列嵌入了特定于调度类的子就绪队列。
  • **调度类实体:**每个task_struct都嵌入到sched_entity的一个实体中

对于这个软件实现框图如下:

202306111306537065.png

内核默认提供了5个调度器,Linux内核使用struct sched_class来对调度器进行抽象:

  1. Stop调度器, stop_sched_class:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占;
  2. Deadline调度器, dl_sched_class:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行;
  3. RT调度器, rt_sched_class:实时调度器,为每个优先级维护一个队列;
  4. CFS调度器, cfs_sched_class:完全公平调度器,采用完全公平调度算法,引入虚拟运行时间概念;
  5. IDLE-Task调度器, idle_sched_class:空闲调度器,每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程;

他们之间的关系如下表

调度器类 描述 调度策略 调度实体 优先级 说明
stop_sched_class 最高优先级,比deadline的优先级更高 1在每个CPU上实现一个名为”migration/N”内核线程,该内核线程优先级最高,可以抢占任何进程
2一般用于负载均衡机制中的进程迁移,softlockup检测,CPU热插拔,RCU等
dl_sched_class deadline调度器 SCHED_DEADLINE sched_dl_entity 最高优先级的实时进程,优先级为-1 用于调度有严格要求的实时进程,如视频编解码等
rt_sched_class 实时调度器 SCHED_FIFO
SCHED_RR sched_rt_entity 普通实时进程,优先级为0~99 用于普通的实时进程,如IRQ线程化
fair_sched_class 完全公平调度器 SCHED_NORMAL
SCHED_BATCH sched_entity 普通进程,优先级为100~139 CFS调度
idle_sched_class idle调度器 SCHED_IDLE 最低优先级 当就绪队列中没有其他进程时进入idle调度类,idle调度类会让CPU进入低功耗模式

不同的调度器算法,无论内部如何实现,其最终都是从就绪队列中选择下一个可执行的进程来运行。 在这个版本的内核中一共实现了如下几种调度器算法,它们统一由结构体sched_class来表示

202306111306542966.png

Linux内核提供了一些调度策略供用户程序来选择调度器,其中Stop调度器IDLE调度器,仅由内核使用,用户无法进行选择:

  • SCHED_DEADLINE:限期进程调度策略,使task选择Deadline调度器来调度运行;
  • SCHED_RR:实时进程调度策略,时间片轮转,进程用完时间片后加入优先级对应运行队列的尾部,把CPU让给同优先级的其他进程;
  • SCHED_FIFO:实时进程调度策略,先进先出调度没有时间片,没有更高优先级的情况下,只能等待主动让出CPU;
  • SCHED_NORMAL:普通进程调度策略,使task选择CFS调度器来调度运行;
  • SCHED_BATCH:普通进程调度策略,批量处理,使task选择CFS调度器来调度运行;
  • SCHED_IDLE:普通进程调度策略,使task以最低优先级选择CFS调度器来调度运行;

3 核心调度器

核心调度器指的是内核的进程调度框架,由内核来触发调度进程的时机,而如何选择进程的工作,交予调度器类来实现,主要分为周期性和主调度器,我们主要来看概况内容

周期性调度器

周期性调度器的入口函数是scheduler_tick,内核会按照系统频率HZ来自动调用该函数,其主要内容如下:

    // kernel/sched/core.c
    void scheduler_tick(void)
    {
    	int cpu = smp_processor_id();
    	struct rq *rq = cpu_rq(cpu);
    	struct task_struct *curr = rq->curr;
    	// 调用调度类对应的task_tick方法,针对CFS调度类该函数是task_tick_fair。
    	curr->sched_class->task_tick(rq, curr, 0);
    }

可以看到,周期性调度器通过调度器算法的task_tick函数来完成调度工作,后面学习CFS算法时详细分析

主调度器

主调度器的入口函数是schedule,在内核中,当需要将CPU分配给与当前进程不同的另一个进程时,就会调用schedule函数来选择下一个可执行进程。schedule函数最终调用的是__schedule

    // kernel/sched/core.c
    static void __sched notrace __schedule(bool preempt)
    {
    	struct task_struct *prev, *next;
    	unsigned long *switch_count;
    	struct rq_flags rf;
    	struct rq *rq;
    	int cpu;
    	cpu = smp_processor_id();
    	rq = cpu_rq(cpu);
    	prev = rq->curr;
    	switch_count = &prev->nivcsw;
    	if (!preempt && prev->state) {
    		//1. 如果当前进程处于可中断的睡眠状态,同时现在接收到了信号,那么将再次被提升为可运行进程
    		if (unlikely(signal_pending_state(prev->state, prev))) {
    			prev->state = TASK_RUNNING;	/* 1 */
    		} else {
    			//2. 调用deactivate_task函数将当前进程变成不活跃状态
    			deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);	/* 2 */
    			prev->on_rq = 0;
    	}
    	//3. 调用pick_next_task函数选择下一个执行的进程
    	next = pick_next_task(rq, prev, &rf);	
    	//4. 清除当前进程的TIF_NEED_RESCHED标志位,意味着不需要重调度
    	clear_tsk_need_resched(prev);	
    	clear_preempt_need_resched();
    	if (likely(prev != next)) {
    		//5. 如果下一个被调度执行进程不是当前进程,调用context_switch函数进行进程上下文切换
    		rq = context_switch(rq, prev, next, &rf);	
    	}
    }

其主要内容就是从调度类中选择下一个进程进行调度,主要是pick_next_task,后面CFS调度器中详细学习

与fork的交互

除了以上两种场景,即周期性调度器以及主调度器之外,fork创建出新进程的时候也会出现与调度器类的交互,其入口函数是sched_fork

    // kernel/sched/core.c
    int sched_fork(unsigned long clone_flags, struct task_struct *p)
    { 
    	if (p->sched_class->task_fork)
    		p->sched_class->task_fork(p);
    }

sched_fork函数中会调用到对应调度器类的task_fork成员函数来处理,下面讲到CFS调度器的时候再详细分析对应的函数。

4 CFS调度器

了解了优先级、虚拟运行时间、相关数据结构、内核调度框架之后,下面正式进入CFS调度器的学习,CFS调度器内部维护一颗红黑树,红黑树的键值即为进程的虚拟运行时间,虚拟运行时间越小的进程,被调度执行的优先级越高,获得更多的CPU时间。

202306111306549847.png

对比两个调度实体在红黑树中的先后顺序,也就只需要对比其中的虚拟运行时间即可

    // kernel/sched/fair.c
    static inline int entity_before(struct sched_entity *a,
    				struct sched_entity *b)
    {
    	return (s64)(a->vruntime - b->vruntime) < 0;
    }

一个进程在刚刚加入到CFS就绪队列的红黑树中时,需要有一个基准值,即这个新加入的进程,应该和什么虚拟运行时间进行对比,找到它在红黑树中的合适位置。这个值由就绪队列中的最小虚拟运行时间来维护,对应的成员是min_vruntime

还需要注意的一点是,既然虚拟运行时间是一直累加的,那么在进程一直运行的情况下,就可能发生数据溢出现象,因此在对比两个虚拟运行时间大小的时候,不是直接比较而是判断的两者的差值(包括上面的entity_before函数也是比较的差值):

    // kernel/sched/fair.c
    static inline u64 max_vruntime(u64 max_vruntime, u64 vruntime)
    {
    	s64 delta = (s64)(vruntime - max_vruntime);
    	if (delta > 0)
    		max_vruntime = vruntime;
    	return max_vruntime;
    }

那么最小虚拟运行时间的更新?

由于更新进程调度实体以及就绪队列的虚拟运行时间的操作如此重要,所以内核中有一个专门的update_curr函数完成这个工作:

202306111306555998.png

其中最后一步是更新CFS就绪队列的最小虚拟运行时间值,来看对应的update_min_vruntime函数

202306111306563059.png

2023061113065725010.png

vruntime值越小的节点,说明虚拟运行时间越少,对应的当前被调度的优先级就越往前,会被更快的调度来执行。

  • 在进程运行的时候,vruntime值稳定增加,于是在红黑树中就是向右边移动。
  • 进程在睡眠时,vruntime值保持不变,而每个队列的min_vruntime时间在增加,那么当睡眠进程被唤醒时(比如等待IO事件),其在红黑树中的位置就靠左,因为其键值变小了,于是会被更快的执行。

place_entity函数属于CFS调度器算法内部使用的一个函数,其作用是调整进程调度实体的虚拟运行时间,传入的第三个参数initial为1的情况下表示是新创建的进程,否则是被唤醒的进程。

2023061113065791011.png

4.1 将一个任务插入运行队列

进程的创建是通过do_fork()函数完成。新进程的诞生,我们调度核心层会通知调度类,调用特别的接口函数初始化新生儿。我们一路尾随do_fork()函数。do_fork()---->_do_fork()---->copy_process()---->sched_fork()

2023061113065871912.png

可以看出sched_fork()进行的初始化也比较简单,需要注意的是不同类型的进程会使用不同的调度类,并且也会调用调度类中的初始化函数。在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数,我们具体看看实现:

2023061113065976013.png

task_fork_fair函数所完成计算并更新虚拟时间vruntime,到这里新进程关于调度的初始化已经完成,但是还没有被调度器加入到队列中,其是在do_fork()中的wake_up_new_task§;中加入到队列中的,我们具体看看wake_up_new_task()的实现

2023061113070039614.png

CFS调度类对应的enqueue_task方法函数是enqueue_task_fair()

2023061113070124815.png

enqueue_task_fair的执行流程如下:

  • 如果通过struct sched_entity的on_rq成员判断进程已经在就绪队列上, 则无事可做.
  • 否则, 具体的工作委托给enqueue_entity完成,其中内核会借机用update_curr更新统计量

2023061113070229216.png

enqueue_entity完成了进程真正的入队操作, 其具体流程如下所示

  • 更新一些统计统计量, update_curr, update_cfs_shares等
  • 如果进程此前是在睡眠状态, 则调用place_entity中首先会调整进程的虚拟运行时间
  • 最后如果进程最近在运行, 其虚拟运行时间仍然有效, 那么则直接用__enqueue_entity加入到红黑树

2023061113070321917.png

5 选择下一个进程

调度器schedule函数在进程调度抢占时, 会通过__schedule函数调用全局pick_next_task选择一个最优的进程, 在pick_next_task中我们就按照优先级依次调用不同调度器类提供的pick_next_task方法

2023061113070419118.png

每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而我们的CFS调度器类中, 选择下一个将要运行的进程由pick_next_task_fair函数来完成

2023061113070493219.png

    static struct task_struct *
    pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct pin_cookie cookie)
    {
    	struct cfs_rq *cfs_rq = &rq->cfs;
    	struct sched_entity *se;
    	struct task_struct *p;
    	int new_tasks;
    /* 控制循环来读取最优进程 */
    again:
    /* 完成组调度下的pick_next选择,返回被选择的调度时实体的指针 */
    #ifdef CONFIG_FAIR_GROUP_SCHED
    	if (!cfs_rq->nr_running)
    		goto idle;
    	/* 如果当前进程不是cfs调度器,就用simple的通用调度切换方法 */
    	if (prev->sched_class != &fair_sched_class)
    		goto simple;
    
    	/*
    	 * Because of the set_next_buddy() in dequeue_task_fair() it is rather
    	 * likely that a next task is from the same cgroup as the current.
    	 *
    	 * Therefore attempt to avoid putting and setting the entire cgroup
    	 * hierarchy, only change the part that actually changes.
    	 */
    	/* 这个do while主要是找到最应该被调度的entity,即找到下一个task实体, 
    	这里是存在多个cfs_rq的情况 */
    	do {
    		struct sched_entity *curr = cfs_rq->curr;
    
    		/*
    		 * Since we got here without doing put_prev_entity() we also
    		 * have to consider cfs_rq->curr. If it is still a runnable
    		 * entity, update_curr() will update its vruntime, otherwise
    		 * forget we've ever seen it.
    		 */
    		if (curr) {
    			if (curr->on_rq)    /* 如果当前cfs_rq的task在队列上,更新虚拟时钟 */
    				update_curr(cfs_rq);
    			else
    				curr = NULL;
    
    			/*
    			 * This call to check_cfs_rq_runtime() will do the
    			 * throttle and dequeue its entity in the parent(s).
    			 * Therefore the 'simple' nr_running test will indeed
    			 * be correct.
    			 */
    			 /* 返回true,表示当前cfs已经超出运行时间,不能再进行组内调度*/
    			if (unlikely(check_cfs_rq_runtime(cfs_rq)))
    				goto simple;
    		}
    		/* 从cfs_rq获取虚拟时钟最小或者最应该参与调度的task */
    		se = pick_next_entity(cfs_rq, curr);
    		cfs_rq = group_cfs_rq(se); /* 如果是调度组,会继续while,非调度组的group_cfs_rq返回NULL*/
    	} while (cfs_rq);
    	/* 获取到调度实体指代的进程信息 */
    	p = task_of(se);
    
    	/*
    	 * Since we haven't yet done put_prev_entity and if the selected task
    	 * is a different task than we started out with, try and touch the
    	 * least amount of cfs_rqs.
    	 */ /* 这里的se就是上面组调度里面,找到的最合适参与调度的task*/
    	if (prev != p) { /*这个prev != p里面,主要是更新prev和p相关的组(cfs_cq)内成员的虚拟时钟*/
    		struct sched_entity *pse = &prev->se;
    
    		while (!(cfs_rq = is_same_group(se, pse))) {
    			int se_depth = se->depth;
    			int pse_depth = pse->depth;
    			/* 表明pre所在的调度组 深度更深,需要更新prev所在的组的虚拟时钟*/
    			if (se_depth <= pse_depth) {
    				put_prev_entity(cfs_rq_of(pse), pse);
    				pse = parent_entity(pse);
    			}
    			/* 表明curr所在的调度组更深,需要将curr所在调度组一连串设置成对应cfs_rq的curr*/
    			if (se_depth >= pse_depth) {
    				set_next_entity(cfs_rq_of(se), se);
    				se = parent_entity(se);
    			}
    		}
    
    		put_prev_entity(cfs_rq, pse);
    		set_next_entity(cfs_rq, se);
    	}
    
    	if (hrtick_enabled(rq))
    		hrtick_start_fair(rq, p);
    
    	return p;
    /* 最基础的pick_next函数, 返回被选择的调度时实体的指针*/
    simple:
    	cfs_rq = &rq->cfs;
    #endif
    	/* 如果nr_running计数器为0,则当前队列没有可运行的进程,调度idle调度类 */
    	if (!cfs_rq->nr_running)
    		goto idle;
    	/* 将当前进程放入运行队列的合适位置 */
    	put_prev_task(rq, prev);
    	/* 在没有配置组调度选项(CONFIG_FAIR_GROUP_SCHED)的情况下.group_cfs_rq()返回NULL.
       因此,上函数中的循环只会循环一次*/
    	do { /* 选出下一个可执行调度实体(进程) */
    		se = pick_next_entity(cfs_rq, NULL);
    		/* set_next_entity会调用__dequeue_entity完成此工作 */
    		set_next_entity(cfs_rq, se); /*把选中的进程从红黑树移除,更新红黑树*/
    		cfs_rq = group_cfs_rq(se); /* 在非组调度情况下, group_cfs_rq返回了NULL*/
    	} while (cfs_rq);
    	
    	p = task_of(se);
    
    	if (hrtick_enabled(rq))
    		hrtick_start_fair(rq, p);
    
    	return p;
    /* 4. 如果系统中没有可运行的进行, 则需要调度idle进程 */
    idle:
    	/*
    	 * This is OK, because current is on_cpu, which avoids it being picked
    	 * for load-balance and preemption/IRQs are still disabled avoiding
    	 * further scheduler activity on it and we're being very careful to
    	 * re-start the picking loop.
    	 */
    	lockdep_unpin_lock(&rq->lock, cookie);
    	new_tasks = idle_balance(rq);
    	lockdep_repin_lock(&rq->lock, cookie);
    	/*
    	 * Because idle_balance() releases (and re-acquires) rq->lock, it is
    	 * possible for any higher priority task to appear. In that case we
    	 * must re-start the pick_next_entity() loop.
    	 */
    	if (new_tasks < 0)
    		return RETRY_TASK;
    
    	if (new_tasks > 0)
    		goto again;
    
    	return NULL;
    }
  • again标签用于循环的进行pick_next操作
  • CONFIG_FAIR_GROUP_SCHED宏指定了组调度情况下的pick_next操作, 如果不支持组调度, 则pick_next_task_fair将直接从simple开始执行
  • simple标签是CFS中最基础的pick_next操作
  • idle则使得在没有进程被调度时, 调度idle进程

pick_next_entity的函数为选择最佳task的关键算法,实现如下

    static struct sched_entity *
    pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
    {
    	struct sched_entity *left = __pick_first_entity(cfs_rq);
    	struct sched_entity *se;
    
    	/*
    	 * If curr is set we have to see if its left of the leftmost entity
    	 * still in the tree, provided there was anything in the tree at all.
    	 */
      /* left是红黑树里面,拥有最小虚拟时钟的task 
    		如果left为NULL或者curr的虚拟时钟比当前最小的left还要小
    		那么left就设置成curr,即curr还可以继续获得CPU,不进行任务切换
    	*/
    	if (!left || (curr && entity_before(curr, left)))
    		left = curr;
    	/* 这个se不是指向curr进程,就是指向红黑树的最小虚拟时钟进程 */
    	se = left; /* ideally we run the leftmost entity */
    
    	/*
    	 * Avoid running the skip buddy, if running something else can
    	 * be done without getting too unfair.
    	 */
       /* 发现se指向的task被设置成不参与调度*/
    	if (cfs_rq->skip == se) {
    		struct sched_entity *second;
    	 /* 如果这个不参与调度的task就是当前进程,那么就取红黑树的第一个实体 */
    		if (se == curr) {
    			second = __pick_first_entity(cfs_rq);
    		} else {
    		/* 个不参与调度的task不是curr进程,那说明这个task是红黑树的最小虚拟时钟task,因此,
    		需要从红黑树中找到一个次小的虚拟时钟task*/
    			second = __pick_next_entity(se);
    			/* 判断这个次小task是否能抢占curr,如果不能,second就还是变为curr*/
    			if (!second || (curr && entity_before(curr, second)))
    				second = curr;
    		}
    		/* 判读这个新的task是否能抢占left,如果能,se就为它,
    这里的对比条件并没有最开始的严格,这里允许一个最小调度差值*/
    		if (second && wakeup_preempt_entity(second, left) < 1)
    			se = second;
    	}
    
    	/*
    	 * Prefer last buddy, try to return the CPU to a preempted task.
    	 */
    	/* 发现设置了last值,则优先调度last指向的task*/
    	if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
    		se = cfs_rq->last;
    
    	/*
    	 * Someone really wants this to run. If it's not unfair, run it.
    	 */
    	/* 发现设置了last值,则优先调度last指向的task */
    	if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
    		se = cfs_rq->next;
    	/* 清除task的next或last值,以免下次调度的时候,
    	又把last和next加入判断,导致不公平*/
    	clear_buddies(cfs_rq, se);
    
    	return se;
    }

pick_next_entity则从CFS的红黑树中摘取一个最优的进程, 这个进程往往在红黑树的最左端, 即vruntime最小, 但是也有例外, 但是不外乎这几个进程

  1. 调用__pick_first_entity函数,取红黑树最左子节点的调度实体,根据我们前面的分析,这是目前虚拟运行时间最小的进程。
  2. 如果left为NULL,或者当前进程curr的虚拟运行时间比left节点更小,说明curr进程是主动放弃了执行权力,且其优先级比最左子节点的进程更优,此时将left指向curr。
  3. 此时left存储的是目前最优的调度实体指针,se保存下来。
  4. cfs_rq->skip存储了需要调过不参与调度的进程调度实体,如果我们挑选出来的最优调度实体se正好是skip,就需要重新作出选择。
  5. 如果前面选择的se指针,正好是当前进程,这样就重新__pick_first_entity拿到当前红黑树的最左子节点。
  6. 否则,skip = se = left的情况,调用__pick_next_entity选择se的下一个子节点。
  7. 如果second == NULL,说明没有次优的进程,或者curr不为NULL的情况下,且curr进程比second进程更优,就将second指向curr,即curr是最优的进程。
  8. 在second不为NULL,且left和second的vruntime的差距是否小于sysctl_sched_wakeup_granularity的情况下,那么second进程能抢占left的执行权。
  9. 判断上一个执行的进程能否抢占left的执行权。
  10. 判断next的执行权能否抢占left的执行权。

set_next_entity函数用于将调度实体存放的进程做为下一个可执行进程的信息保存下来

    static void
    set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
    	/* 针对即将运行的进程,我们都会从红黑树中删除当前进程*/
    	if (se->on_rq) {   /* 如果se尚在rq队列上 */
    		/*
    		 * Any task has to be enqueued before it get to execute on
    		 * a CPU. So account for the time it spent waiting on the
    		 * runqueue.
    		 */
    		update_stats_wait_end(cfs_rq, se);
    		__dequeue_entity(cfs_rq, se);   /*将se从cfs_rq的红黑树中删除*/
    		update_load_avg(se, 1);  /* 更新进程的负载信息。负载均衡会使用*/
    	}
       /* 更新调度实体exec_start成员,为update_curr()函数统计时间做准备*/
    	update_stats_curr_start(cfs_rq, se); /* 更新就绪队列curr成员 */
    	cfs_rq->curr = se;
    
    	/*
    	 * Track our maximum slice length, if the CPU's load is at
    	 * least twice that of our own weight (i.e. dont track it
    	 * when there are only lesser-weight tasks around):
    	 */
    	if (schedstat_enabled() && rq_of(cfs_rq)->load.weight >= 2*se->load.weight) {
    		schedstat_set(se->statistics.slice_max,
    			max((u64)schedstat_val(se->statistics.slice_max),
    			    se->sum_exec_runtime - se->prev_sum_exec_runtime));
    	}
    	/* 更新task上一次投入运行的从时间 */
    	se->prev_sum_exec_runtime = se->sum_exec_runtime;
    }

6 从运行队列中移除一个任务

2023061113070557020.png

7 总结

  • CFS给每个进程安排一个 虚拟运行时间vruntime ,正在运行的进程vruntime随tick不断增大,没有运行的进程vruntime不变, vruntime小的会被优先运行

  • 对于不同优先级的进程,换算vruntime时 优先级高的算少,优先级低的算多 ,这样优先级高的进程实际运行时间就变多了

  • 调度队列使用红黑树,红黑树的节点是调度实体

    2023061113070622721.png

  • CFS的队列是一棵红黑树,红黑树的节点是调度实体,每个调度实体都属于一个task_struct,task_struct里面有指针指向这个进程属于哪个调度类

  • CPU需要找下一个任务执行时,会按照优先级依次调用调度类,不同的调度类操作不同的队列。当然rt_sched_class先被调用,它会在rt_rq上找下一个任务,只有找不到的时候,才轮到fair_sched_class被调用,它会在cfs_rq上找下一个任务。这样保证了实时任务的优先级永远大于普通任务


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文