重学计算机(十七、linux调度器和调度器类)

没想到上一篇只写了一个优先级,这一篇尽量把linux调度器整体架构缕清楚,下一篇正式开始CFS完全公平调度器。

17.1 整体框架

我感觉还是喜欢从整体到细节,虽然现在介绍整体比较懵逼,不过有一个整体的概念,然后在慢慢的细化分析,等分析完了,再回来看整体框架,就感觉很清晰。

在这里插入图片描述

这是从《深入linux内核架构》里面抄出来的图,我第一次见这个图就比较懵逼,为什么调度类上面还有主调度器和周期性调度器,调度类和主调器和周期性调度器究竟是什么关系。

调度器类和进程的关系,都是比较明显,因为linux系统只有有多个调度器类,比如有CFS完全公平调度器类,还有一个实时调度器类,在无事好做时调度空闲进程。普通进程都属于CFS完全公平调度器的,要求实时进程当然就属于实时调度器类。

17.2 调度器

接下来我们来分析一下调度器的实现,调度器的实现基于上面两个函数:周期行调度器函数和主调度器函数。我们接下里看看:

17.2.1 周期性调度器

周期性调度器相对来说简单一点,周期性调度器是在scheduler_tick中实现,如果系统正在工作,内核会按照频率Hz自动调用该函数。具体的我们后面有缘再介绍。(很有可能不会介绍,因为太底层的东西好处也不是很大)

我们直接来看看源码:

// kernel/sched/core.c
// 虽然好多细节我也不知道,就是因为当年老是分析内核细节,一下子就绕进去了,这次避免分析细节,等到整体抓的差不多再分析细节。
/*
 * This function gets called by the timer code, with HZ frequency.
 * We call it with interrupts disabled.
 */
void scheduler_tick(void)
{
	int cpu = smp_processor_id();   // 多核cpu
	struct rq *rq = cpu_rq(cpu);	// 应该是获取当前CPU运行的队列
	struct task_struct *curr = rq->curr;	// 通过运行队列获取到当前运行进程的控制块

	sched_clock_tick();		// 可能是调整什么时钟吧,这个忽略

	raw_spin_lock(&rq->lock);    // 内核的锁,忽略,以后再分析
	update_rq_clock(rq);		// 这个重要一点,更新就绪队列时钟的更新,这个就绪队列我们以后还会接触,就是就绪的进程都在这里
	curr->sched_class->task_tick(rq, curr, 0);		// 这个才是最重点的,是当前进程的控制块中有调度器类的指针,这个调度器类的task_tick,意思就是如果这个进程是CFS调度的,这个task_tick就是CFS调度类的,不同调度器类对task_tick处理不一样,所以需要这样来调用
	update_cpu_load_active(rq);   // 负责更新就绪队列的cpu_load[]数组(说实话我也没看懂,哈哈)
	calc_global_load_tick(rq);
	raw_spin_unlock(&rq->lock);   // 解锁

	perf_event_task_tick();

    // 这个是多和CPU的时候,如果有一个核比较空闲,就会做一下负载均衡
#ifdef CONFIG_SMP
	rq->idle_balance = idle_cpu(cpu);
	trigger_load_balance(rq);
#endif
	rq_last_tick_reset(rq);		//更新运行队列的时钟
}

虽然这个代码分析的,有一大半还看懂,但是我们还是看出了核心,就是

curr->sched_class->task_tick(rq, curr, 0);

通过进程去控制调度器类,然后调度器类再做相应的处理,等我们分析到CFS的时候,在来看看这个调度器类是做什么的。

17.2.2 主调度器

我们刚刚分析周期性调度器的时候,是不是很开心,感觉很简单,不过别高兴太早,这个主调度器不会太简单的,压力山大。。。。

周期性调度器是通过定时触发的,那是因为周期性调度器主要是判断进程的运行的时间,是否已经达到了自己的时间片,如果达到了,就会做相应的处理(这里没有说直接抢占,等到分析CFS的时候就会明白)。

主调度器被调用的地方就比较多了,比如:当前进程主动让出CPU,还有判断重调度标记等。我刚刚想在内核代码中搜索一下啥时候调用主调度器,结果搜出了一大推,然后就放弃了,也等到CFS的时候,看看有什么发现吧。

吹水吹完了,接下来上代码:

//#define __sched		__attribute__((__section__(".sched.text")))
// 这个有看前面的章节就知道,gcc自定义的一个段,把调度程序全部集中在.sched.text段中,这种做法是在显示堆栈信息时,忽略与调度相关的部分。所以我们在函数调用的时候,是看不到这些部分的。
asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;  
    //current 这个全局变量有点意思,并且在内核中,使用的次数也是比较多的,只想当前进程控制块,这个变量的存储,我们到进程控制块那章再介绍,有兴趣的可以先看,为了加快访问速度,是存储在寄存器中的。

	sched_submit_work(tsk);   // 不知道这个干啥的
	do {
		preempt_disable();		// 进程控制块中有一个计数器preempt_count,当数值为0的时候,表示可以抢占,不为0不能抢占,这个函数会把计数器perrmpt_count+1,
		__schedule(false);		// 这个是主要的调度函数,下面详解分析
		sched_preempt_enable_no_resched();   // 这个就是把preempt_count-1
	} while (need_resched());	
}
EXPORT_SYMBOL(schedule);

集中精力分析__schedule,看能不能看懂,看不懂也要看一个大概就可以了。

/*
 * __schedule() is the main scheduler function.
   __schedule()是主要的调度函数
 *
 * The main means of driving the scheduler and thus entering this function are:
 	驱动调度器进入这个函数的主要方法是:
 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.
 	1. 阻塞:互斥锁,信号量,等待队列
 *
 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
 *      paths. For example, see arch/x86/entry_64.S.
	 在中断和用户空间返回路径上检查TIF_NEED_RESCHED标志
 *		
 *      To drive preemption between tasks, the scheduler sets the flag in timer
 *      interrupt handler scheduler_tick().
 		任务之间的抢占,调度器在定时器中断处理程序scheduler_tick()中设置TIF_NEED_RESCHED标志
 *
 *   3. Wakeups don't really cause entry into schedule(). They add a
 *      task to the run-queue and that's it.
 	唤醒并不会真正导致进入schedule(),他们将一个任务添加到运行队列中,仅此而已
 *
 *      Now, if the new task added to the run-queue preempts the current
 *      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
 *      called on the nearest possible occasion:
 	现在,如果添加到运行队列中的新任务抢占当前任务,然后唤醒设置TIF_NEED_RESCHED,并在最近的可能场合调用schedule()
 *
 *       - If the kernel is preemptible (CONFIG_PREEMPT=y):
 		如果内核可抢占,linux内核是抢占式的
 *
 *         - in syscall or exception context, at the next outmost
 *           preempt_enable(). (this might be as soon as the wake_up()'s
 *           spin_unlock()!)
 			在系统调用或异常上下文中,下一个调用preempt_enable()。这可能需要wake_up()的spin_unlock()。
 *
 *         - in IRQ context, return from interrupt-handler to
 *           preemptible context
 			在IRQ上下文中,从中断处理程序返回到抢占上下文
 *
 *       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
 *         then at the next:
 *
 *          - cond_resched() call     cond_resched()调用
 *          - explicit schedule() call
 *          - return from syscall or exception to user-space
 			从系统调用或异常返回到用户空间
 *          - return from interrupt-handler to user-space
 			从中断处理程序返回到用户空间
 *
 * WARNING: must be called with preemption disabled!
 	警告:必须在禁用抢占的情况下调用! (刚开始的时候就设置了标志)
 */
static void __sched notrace __schedule(bool preempt)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

	cpu = smp_processor_id();    // 终于看到操作多核CPU了
	rq = cpu_rq(cpu);
	rcu_note_context_switch();
	prev = rq->curr;

	/*
	 * do_exit() calls schedule() with preemption disabled as an exception;
	 * however we must fix that up, otherwise the next task will see an
	 * inconsistent (higher) preempt count.
	 do_exit()调用schedule()时异常禁用抢占;但是我们必须解决这个问题,否则下一个任务将看到一个不一致的(更高的)抢占计数
	 *
	 * It also avoids the below schedule_debug() test from complaining
	 * about this.
	 它还避免了下面的schedule_debug()测试对此进行抱怨。(有道翻译的哈哈)
	 */
    // 就是上面的翻译,如果进程挂了,要把计数器给减1
	if (unlikely(prev->state == TASK_DEAD))
		preempt_enable_no_resched_notrace();

    // 各种schedule()时间调试检查和统计信息:(注释是这么说的)
	schedule_debug(prev);

	if (sched_feat(HRTICK))
		hrtick_clear(rq);

	/*
	 * Make sure that signal_pending_state()->signal_pending() below
	 * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
	 * done by the caller to avoid the race with signal_wake_up().
	确保下面的signal_pending_state()->signal_pending()不能被调用方的__set_current_state(TASK_INTERRUPTIBLE)重新排序,以避免与signal_wake_up()的竞争。
	 */
	smp_mb__before_spinlock();
	raw_spin_lock_irq(&rq->lock);
	lockdep_pin_lock(&rq->lock);

	rq->clock_skip_update <<= 1; /* promote REQ to ACT */

	switch_count = &prev->nivcsw;
	if (!preempt && prev->state) {   // 这个判断有点意思,preempt是传参过来的,这次传的是false,prev->state是进程状态,>0不是运行态,也就是说,抢占是不走这个分支?
		if (unlikely(signal_pending_state(prev->state, prev))) {
			prev->state = TASK_RUNNING;
		} else {
            // 先前的进程不再处于可执行状态,需要将其从运行队列中移除出去。
			deactivate_task(rq, prev, DEQUEUE_SLEEP);
			prev->on_rq = 0;

			/*
			 * If a worker went to sleep, notify and ask workqueue
			 * whether it wants to wake up a task to maintain
			 * concurrency.
			 如果一个worker进入睡眠状态,通知并询问workqueue是否需要唤醒一个task来保持并发
			 */
			if (prev->flags & PF_WQ_WORKER) {
				struct task_struct *to_wakeup;

				to_wakeup = wq_worker_sleeping(prev, cpu);
				if (to_wakeup)
					try_to_wake_up_local(to_wakeup);
			}
		}
		switch_count = &prev->nvcsw;
	}

	if (task_on_rq_queued(prev))
		update_rq_clock(rq);		// 在更新运行队列的东西

    // 这个就直接选择下一进程了,太尼玛快了
	next = pick_next_task(rq, prev);
	clear_tsk_need_resched(prev);		// 清除标记
	clear_preempt_need_resched();
	rq->clock_skip_update = 0;			// 把上面的标记清除

    // 如果选中的进程不是之前的进程,需要上下文切换
	if (likely(prev != next)) {
		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		trace_sched_switch(preempt, prev, next);	// 不知道是啥
        // 传说的上下文切换?
		rq = context_switch(rq, prev, next); /* unlocks the rq */
		cpu = cpu_of(rq);
	} else {
		lockdep_unpin_lock(&rq->lock);
		raw_spin_unlock_irq(&rq->lock);
	}

    // 多CPU做负载均衡
	balance_callback(rq);
}

感觉吧,虽然也能抓住核心,但是总是缺少了点啥,应该是缺少了细节,不过目前实力不够,就不沉迷细节了,如果去纠细节的话,会沉迷进去的,接下来我们看pick_next_task选择下一进程的函数。

/*
 * Pick up the highest-prio task:
 选择优先级最高的任务:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
	const struct sched_class *class = &fair_sched_class;
	struct task_struct *p;

	/*
	 * Optimization: we know that if all tasks are in
	 * the fair class we can call that function directly:
	 优化:我们知道如果所有任务都在公平类中,我们可以直接调用该函数
	 */
    // 如果是CFS完全公平调度器的话,就执行下面的
	if (likely(prev->sched_class == class &&
		   rq->nr_running == rq->cfs.h_nr_running)) {
		p = fair_sched_class.pick_next_task(rq, prev);
		if (unlikely(p == RETRY_TASK))
			goto again;

		/* assumes fair_sched_class->next == idle_sched_class */
        // CFS完全公平调度类都没有选择出来,那就调用空闲调度类
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev);

		return p;
	}

again:
    // 这个是遍历调度器类,可以往后看
	for_each_class(class) {
		p = class->pick_next_task(rq, prev);  // 直接找到一个进程,pick_next_task函数是调度器类的,等下一节再详细分析
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

	BUG(); /* the idle class will always have a runnable task
    空闲类将始终有一个可运行的任务*/
}

我们直接看看for_each_class的代码:

#define sched_class_highest (&stop_sched_class)
#define for_each_class(class) \
for (class = sched_class_highest; class; class = class->next)

for_each_class是按照linux调度类的优先级遍历的,找到各自的优先级中有没有就绪的进程,然后进行调度。下面是我按linux调度类的优先级排列的:

stop_sched_class(停止类)
dl_sched_class(最终期限调度类)
rt_sched_class(实时调度类)
fair_sched_class(CFS完全公平调度类)
idle_sched_class(空闲调度类)    

讲到这里,主调度器基本讲完了,虽然很多细节不管,但基本核心还是抓住了,就是按照调度器类的优先级来查找合适的进程,进行进程切换,具体的等看具体的调度类,我们再分析。

17.3 调度器类

本来之前是有安排fork和上下文切换,但是想想fork留着讲完CFS的时候再讲,上下文切换等到以后功力深厚了再分析把,上下文切换的大概就是保存寄存器,保存内存,堆栈里的值,这些以后再分析,希望还有分析的机会。

17.3.1 调度器类的抽象

接下来我们看看调度器类的抽象,虽说c语言不是面向对象的语言,但是在linux内核中,基本都是面向对象的思想,这个调度器类也是这种思想,下面就来看看这个抽象:

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);
    // 进程想要资源放弃对CPU的控制权
	void (*yield_task) (struct rq *rq);
    // 
	bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);

    // 用一个新唤醒的进程来抢占当前进程
	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);
    // 进程切换上下文之前的准备工作
	void (*put_prev_task) (struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP  // CONFIG_SMP 这是多核CPU
	int  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
	void (*migrate_task_rq)(struct task_struct *p);

	void (*task_waking) (struct task_struct *task);
	void (*task_woken) (struct rq *this_rq, struct task_struct *task);

	void (*set_cpus_allowed)(struct task_struct *p,
				 const struct cpumask *newmask);

	void (*rq_online)(struct rq *rq);
	void (*rq_offline)(struct rq *rq);
#endif
	
	void (*set_curr_task) (struct rq *rq);
    // 这个就是周期性调度器调用的
	void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
	void (*task_fork) (struct task_struct *p);
	void (*task_dead) (struct task_struct *p);

	/*
	 * The switched_from() call is allowed to drop rq->lock, therefore we
	 * cannot assume the switched_from/switched_to pair is serliazed by
	 * rq->lock. They are however serialized by p->pi_lock.
	 */
	void (*switched_from) (struct rq *this_rq, struct task_struct *task);
	void (*switched_to) (struct rq *this_rq, struct task_struct *task);
	void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
			     int oldprio);

	unsigned int (*get_rr_interval) (struct rq *rq,
					 struct task_struct *task);

	void (*update_curr) (struct rq *rq);

#ifdef CONFIG_FAIR_GROUP_SCHED
	void (*task_move_group) (struct task_struct *p);
#endif
};

每一个调度器类,都要实现这些方法,通过周期性调度器或者主调度器来调用这些方法,就行进程调度。

上面我们也分析了目前有5种调度器类,每一个都有自己的实现,我们下一节就分析CFS完全调度器。

17.3.2 就绪队列

主调度器用于管理活动进程的主要数据结构称为就绪队列。各个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 {
	/* runqueue lock: */
	raw_spinlock_t lock;

	/*
	 * nr_running and cpu_load should be in the same cacheline because
	 * remote CPUs use both these fields when doing load calculation.
	 nr_running和cpu_load应该在同一个cacheline中,因为远程cpu在进行负载计算时使用这两个字段
	 */
	unsigned int nr_running;  // 指定了队列上可运行进程的数目

	#define CPU_LOAD_IDX_MAX 5
	unsigned long cpu_load[CPU_LOAD_IDX_MAX];	// 用于跟踪此前的负荷状态
	unsigned long last_load_update_tick;

	/* 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;


	/*
	 * This is part of a global counter where only the total sum
	 * over all CPUs matters. A task can increase this counter on
	 * one CPU and if it got migrated afterwards it may decrease
	 * it on another CPU. Always updated under the runqueue lock:
	 */
	unsigned long nr_uninterruptible;

	struct task_struct *curr, *idle, *stop;
	unsigned long next_balance;
	struct mm_struct *prev_mm;

	unsigned int clock_skip_update;
	u64 clock;
	u64 clock_task;

	atomic_t nr_iowait;
};

这个结构体省了一大半,结果这个结构体还是没看的懂,那只能等到后面分析了,这里就有一个印象了。

17.3.3 调度实体

linux内核调度的实体:

struct sched_entity {
    // 这个就是我们上一篇算的优先级保存的结构,一个负荷权重
	struct load_weight	load;		/* for load-balancing */
	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;

#ifdef CONFIG_SCHEDSTATS
	struct sched_statistics statistics;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
	int			depth;
	struct sched_entity	*parent;
	/* rq on which this entity is (to be) queued: */
	struct cfs_rq		*cfs_rq;
	/* rq "owned" by this entity/group: */
	struct cfs_rq		*my_q;
#endif

#ifdef CONFIG_SMP
	/* Per entity load average tracking */
	struct sched_avg	avg;
#endif
};

由于每个进程的控制块中都嵌入了sched_entity实例,所以进程是可调度实体。但可调度实体不一定是进程。

那还有其他什么是可调度实体么?这个我还真不知道,未来的路还很长,下一篇我们就来分析一个CFS完全公平调度器。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值