深入理解linux进程切换

调度时机

进程的调度切换可以分为被动调度和主动调度。
被动调度主要与系统调用和中断相关。

系统调用<>用户空间
中断<>内核空间

主动调用是进程自己放弃cpu,调用调度函数。

调度函数

Linux内核通过schedule函数实现进程调度,schedule函数负责在运行队列中选择一个进程,然后把它切换到CPU上执行。

… set_current_state(TASK_INTERRUPTIBLE); schedule() …

schedule源码如下

void __sched schedule(void)
{
    struct task_struct *tsk = current;
    ...
    do {
        preempt_disable();
        __schedule(false);
        sched_preempt_enable_no_resched();
    } while (need_resched());
}

schedule中调用_schedule进行实际上的调度。
通过检查need_resched()标记来决定是否调用schedule。

_schedule函数如下

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;//现在的将变成过往
 
	schedule_debug(prev, preempt);//此函数检查当前调用schedule是否合适,如不能在原子上下文(即抢占被关闭的情况下)中调用schedule,栈崩溃等.
 
	if (sched_feat(HRTICK))
		hrtick_clear(rq);
 
	local_irq_disable();
	rcu_note_context_switch(preempt);//通知RCU的相关操作
 
	/*
	 * 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().
	 *
	 * The membarrier system call requires a full memory barrier
	 * after coming from user-space, before storing to rq->curr.
	 */
	rq_lock(rq, &rf);
	smp_mb__after_spinlock();
 
	/* Promote REQ to ACT */
	rq->clock_update_flags <<= 1;//为什么要修改clock_update_flags呢?
	update_rq_clock(rq);
 
	switch_count = &prev->nivcsw; //根据上下文,switch_count此时指向的是非自愿切换计数器
	if (!preempt && prev->state) {//如果不是被抢占的,且task处于非running状态
		if (signal_pending_state(prev->state, prev)) {//如果有signal pending,则重新将prev task置为running状态
			prev->state = TASK_RUNNING;
		} else { //否则做dequeue的操作.
			deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
 
			if (prev->in_iowait) { //如果prev task是在iowait状态,则增加当前runqueue iowait计数
				atomic_inc(&rq->nr_iowait);
				delayacct_blkio_start();
			}
		}
		switch_count = &prev->nvcsw;//因为经过判断是自愿切换的,所以,将计数器指针指向自愿切换计数器
	}
 
	next = pick_next_task(rq, prev, &rf);//选择新的task出来
	clear_tsk_need_resched(prev);//因为可能prev task是设置了need_reschedule,所以,重新置位
	clear_preempt_need_resched();
 
	if (likely(prev != next)) {
		rq->nr_switches++;//nr_switches表示自愿与非自愿的总数
		/*
		 * RCU users of rcu_dereference(rq->curr) may not see
		 * changes to task_struct made by pick_next_task().
		 */
		RCU_INIT_POINTER(rq->curr, next);
		/*
		 * The membarrier system call requires each architecture
		 * to have a full memory barrier after updating
		 * rq->curr, before returning to user-space.
		 *
		 * Here are the schemes providing that barrier on the
		 * various architectures:
		 * - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC.
		 *   switch_mm() rely on membarrier_arch_switch_mm() on PowerPC.
		 * - finish_lock_switch() for weakly-ordered
		 *   architectures where spin_unlock is a full barrier,
		 * - switch_to() for arm64 (weakly-ordered, spin_unlock
		 *   is a RELEASE barrier),
		 */
		++*switch_count;
 
		trace_sched_switch(preempt, prev, next);//这个trace吐出来的信息可以反映出一些信息,需要研究一下.
 
		/* Also unlocks the rq: */
		rq = context_switch(rq, prev, next, &rf);
	} else {
		rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
		rq_unlock_irq(rq, &rf);
	}
 
	balance_callback(rq);//做一些回调动作.
}

该函数可以进行如下抽象

static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    ...
	next = pick_next_task(rq, prev, &rf);
    ...
    rq = context_switch(rq, prev, next, &rf);
    ...
}

进程执行环境的切换大致分为两大步,一是从就绪队列中选择一个进程(pick_next_task),也就是由进程调度算法决定选择哪一个进程作为下一个进程(next);二是完成进程上下文切换context_switch,进程上下文包含了进程执行需要的所有信息。

如何选择下一个进程——调度策略

根据进程的不同分类,Linux采用不同的调度策略。早期很多用户共享同一台小型机,调度算法追求吞吐率、利用率、公平性;现在的个人电脑更强调人机交互响应速度;而很多自动控制场合使用的嵌入式系统更强调实时性。当前Linux系统的解决方案是,对于实时进程,Linux采用FIFO(先进先出)或者Round Robin(时间片轮转)的调度策略。对其他进程,当前Linux采用CFS(Completely Fair Scheduler)调度器,核心思想是“完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美支持。

Linux系统中常用的几种调度策略为SCHED_NORMAL、SCHED_FIFO、SCHED_RR、SCHED_BATCH。其中SCHED_NORMAL是用于普通进程的调度类,而SCHED_FIFO和SCHED_RR是用于实时进程的调度类,优先级高于SCHED_NORMAL。内核中根据进程的优先级来区分普通进程与实时进程,Linux内核进程优先级为0~139,数值越高,优先级越低,0为最高优先级。实时进程的优先级取值为0~99;而普通进程只具有nice值,nice值映射到优先级为100~139。子进程会继承父进程的优先级。对于实时进程,Linux系统会尽量使其调度延时在一个时间期限内,但是不能保证总是如此,不过正常情况下已经可以满足比较严格的时间要求了。

实时进程:SCHED_FIFO、SCHED_RR
普通进程:SCHED_NORMAL—>CFS(after 2.6)

CFS即为完全公平调度算法,其基本原理是基于权重的动态优先级调度算法。每个进程使用CPU的顺序由进程已使用的CPU虚拟时间(vruntime)决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的概率也就越高。每个进程每次占用CPU后能够执行的时间(ideal_runtime)由进程的权重决定,并且保证在某个时间周期(__sched_period)内运行队列里的所有进程都能够至少被调度执行一次

进程切换的实质——进程上下文切换

进程上下文切换通过context_switch函数实现

/*
 * context_switch - switch to the new MM and the new thread's register state.
 */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next, struct rq_flags *rf)
{
    prepare_task_switch(rq, prev, next);
    ...
    /*
     * kernel -> kernel   lazy + transfer active
     *   user -> kernel   lazy + mmgrab() active
     *
     * kernel ->   user   switch + mmdrop() active
     *   user ->   user   switch
     */
    if (!next->mm) {                                // to kernel
    	...
    } else {                                        // to user
        ...
    }
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);
    barrier();

	return finish_task_switch(prev);
}

最核心的是几个关键寄存器的保存与变换。

  • 进程页目录表(页表),即地址空间、数据。
  • 内核堆栈栈顶寄存器sp代表进程内核堆栈(保存函数调用历史),进程描述符(最后的成员thread是关键)和内核堆栈存储于连续存取区域中,进程描述符存在内核堆栈的低地址,栈从高地址向低地址增长,因此通过栈顶指针寄存器还可以获取进程描述符的起始地址。

  • 指令指针寄存器代表进程的CPU上下文,即要执行的下条指令地址。

这些寄存器从一个进程的状态切换到另一个进程的状态,进程切换的关键上下文就算完成了。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值