精华内容
下载资源
问答
  • linux内核上下文切换解析

    千次阅读 2018-09-22 00:41:53
    linux的上下文切换就是进程线程的切换,也就是切换struct task_struct结构体,一个任务的上下文包括cpu的寄存器,内核栈等,由于1个cpu上的所有任务共享一套寄存器,所以在任务挂起的时候需要保存寄存器,当任务重新...

        linux的上下文切换就是进程线程的切换,也就是切换struct task_struct结构体,一个任务的上下文包括cpu的寄存器,内核栈等,由于1个cpu上的所有任务共享一套寄存器,所以在任务挂起的时候需要保存寄存器,当任务重新被调度执行的时候需要恢复寄存器。每种处理器都提供了硬件级别的上下文切换,比如x86架构下的TSS段,TSS段包括了一个任务执行的所需要的所有上下文,主要有:1.通用寄存器和段寄存器。2.标志寄存器EFLAGS,程序指针EIP,页表基地址寄存器CR3,任务寄存器和LDTR寄存器。3.I/O映射位图基地址和I/O位图信息。4.特权级0,1,2堆栈指针。5.链接到前一任务的链指针。所以上下文切换也很简单,直接用call或者jmp指令调度任务。同样ARM架构也有快速上下文切换技术。但是Linux为了适用更多的cpu架构没使用处理器相关的上下文切换技术,而是大部分通过软件实现。linux上下文切换就在schedule()函数里,很多地方都会调用这个函数。scchedule函数前面大部分代码是和调度算法相关的,比如实时任务调度算法,O(1)调度算法(2.6.22版本被CFS调度算法取代),CFS调度算法等。经过前面的代码计算后找出下一个要执行的任务,然后开始执行上下文切换。先看一段linux2.6.18版本还使用O(1)调度算法的schedule函数代码:

    /*
     * schedule() is the main scheduler function.
     */
    asmlinkage void __sched schedule(void)
    {
    	struct task_struct *prev, *next;//当前任务和将要执行的任务
    	struct prio_array *array;    //优先队列
    	struct list_head *queue;     //队列
    	unsigned long long now;      //系统当前时间
    	unsigned long run_time;     //运行时间
    	int cpu, idx, new_prio;  
    	long *switch_count;        //切换次数
    	struct rq *rq;            //当前cpu运行队列
    
    	/*
    	 * Test if we are atomic.  Since do_exit() needs to call into
    	 * schedule() atomically, we ignore that path for now.
    	 * Otherwise, whine if we are scheduling when we should not be.
    	 */
    	if (unlikely(in_atomic() && !current->exit_state)) {
    		printk(KERN_ERR "BUG: scheduling while atomic: "
    			"%s/0x%08x/%d\n",
    			current->comm, preempt_count(), current->pid);
    		dump_stack();
    	}
    	profile_hit(SCHED_PROFILING, __builtin_return_address(0));
    
    need_resched:
    	preempt_disable();   //关闭抢占
    	prev = current;      //prev等于当前进程
    	release_kernel_lock(prev); //释放当前进程的内核锁
    need_resched_nonpreemptible:
    	rq = this_rq();     //获取当前cpu的运行队列
    
    	/*
    	 * 如果当前进程是idel进程,并且idel进程不是运行态则是bug,因为
    	 * 是idel进程说明运行队列为空,而此cpu还在运行,所以进程不可能是非运行态
    	 */
    	if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) {
    		printk(KERN_ERR "bad: scheduling from the idle thread!\n");
    		dump_stack();
    	}
    	//调度统计
    	schedstat_inc(rq, sched_cnt);
    	//读取系统时间,也就是cpu启动以来的时钟周期数
    	now = sched_clock();
    	//如果当前任务的运行时间大于等于最大平均睡眠时,则运行时间为最大平均睡眠时间
    	if (likely((long long)(now - prev->timestamp) < NS_MAX_SLEEP_AVG)) {
    		run_time = now - prev->timestamp;
    		if (unlikely((long long)(now - prev->timestamp) < 0))
    			run_time = 0;
    	} else
    		run_time = NS_MAX_SLEEP_AVG;
    
    	/*
    	 * Tasks charged proportionately less run_time at high sleep_avg to
    	 * delay them losing their interactive status
    	 */
    	run_time /= (CURRENT_BONUS(prev) ? : 1);
    
    	spin_lock_irq(&rq->lock);
    
    	if (unlikely(prev->flags & PF_DEAD))
    		prev->state = EXIT_DEAD;
    
    	switch_count = &prev->nivcsw;
    	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
    		switch_count = &prev->nvcsw;
    		if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
    				unlikely(signal_pending(prev))))
    			prev->state = TASK_RUNNING;
    		else {
    			if (prev->state == TASK_UNINTERRUPTIBLE)
    				rq->nr_uninterruptible++;
    			deactivate_task(prev, rq);
    		}
    	}
    	//获取当前cpuid
    	cpu = smp_processor_id();
    	//如果当前cpu运行队列没有任务则进行负载均衡,从其它cpu的运行队列获取任务去执行
    	if (unlikely(!rq->nr_running)) {
    		//负载均衡调度
    		idle_balance(cpu, rq);
    		//如果负载均衡完还是没有任务,则还继续执行idel任务
    		if (!rq->nr_running) {
    			next = rq->idle;
    			rq->expired_timestamp = 0;
    			wake_sleeping_dependent(cpu);
    			goto switch_tasks;
    		}
    	}
    
    	//获取运行队列中的活动队列
    	array = rq->active;
    	if (unlikely(!array->nr_active)) {
    		/*
    		 * 如果活动队列没有任务了,则交换活动队列和过期队列的指针,避免进程饥饿
    		 */
    		schedstat_inc(rq, sched_switch);
    		rq->active = rq->expired;
    		rq->expired = array;
    		array = rq->active;
    		rq->expired_timestamp = 0;
    		rq->best_expired_prio = MAX_PRIO;
    	}
    
    	//从活动队列中根据优先级找出第一个任务,复杂度为O(1)
    	idx = sched_find_first_bit(array->bitmap);
    	queue = array->queue + idx;
    	next = list_entry(queue->next, struct task_struct, run_list);
    	//优先级计算
    	if (!rt_task(next) && interactive_sleep(next->sleep_type)) {
    		unsigned long long delta = now - next->timestamp;
    		if (unlikely((long long)(now - next->timestamp) < 0))
    			delta = 0;
    
    		if (next->sleep_type == SLEEP_INTERACTIVE)
    			delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
    
    		array = next->array;
    		new_prio = recalc_task_prio(next, next->timestamp + delta);
    
    		if (unlikely(next->prio != new_prio)) {
    			dequeue_task(next, array);
    			next->prio = new_prio;
    			enqueue_task(next, array);
    		}
    	}
    	next->sleep_type = SLEEP_NORMAL;
    	if (dependent_sleeper(cpu, rq, next))
    		next = rq->idle;
    switch_tasks:
    	if (next == rq->idle)
    		schedstat_inc(rq, sched_goidle);
    	prefetch(next);
    	prefetch_stack(next);
    	clear_tsk_need_resched(prev);
    	rcu_qsctr_inc(task_cpu(prev));
    
    	update_cpu_clock(prev, rq, now);
    
    	prev->sleep_avg -= run_time;
    	if ((long)prev->sleep_avg <= 0)
    		prev->sleep_avg = 0;
    	prev->timestamp = prev->last_ran = now;
    	//一些切换信息的计算
    	sched_info_switch(prev, next);
    	//如果将要运行的任务和当前任务不是同一个任务,则执行上下文切换
    	if (likely(prev != next)) {
    		next->timestamp = now; //下一个任务开始执行时间
    		rq->nr_switches++;   //当前运行队列切换次数加一
    		rq->curr = next;     //当前运行队列当前执行任务为将要执行的任务
    		++*switch_count;     //切换次数加一
    
    		//切换前的准备,和体系结构相关,很多cpu为空实现
    		prepare_task_switch(rq, next);
    		//进入真正的切换
    		prev = context_switch(rq, prev, next);
    		barrier();
    		/*
    		 * this_rq must be evaluated again because prev may have moved
    		 * CPUs since it called schedule(), thus the 'rq' on its stack
    		 * frame will be invalid.
    		 */
    		finish_task_switch(this_rq(), prev);
    	} else
    		spin_unlock_irq(&rq->lock);
    
    	prev = current;
    	if (unlikely(reacquire_kernel_lock(prev) < 0))
    		goto need_resched_nonpreemptible;
    	preempt_enable_no_resched();
    	if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
    		goto need_resched;
    }

    可以看到前面大部分代码都是和调度算法相关的计算,最终的上下文切换函数是context_switch

    /*
     * 上下文切换,切换mm,寄存器和内核栈
     */
    static inline struct task_struct *
    context_switch(struct rq *rq, struct task_struct *prev,
    	       struct task_struct *next)
    {
    	struct mm_struct *mm = next->mm; //将要运行进程的mm内存描述符
    	//当前进程的active_mm描述符,此处用active_mm是因为,如果当前进程是内核线程则mm为null
    	//如过是用户态进程或线程则active_mm和mm相等,所以使用active_mm
    	struct mm_struct *oldmm = prev->active_mm; 
    
    	 //如果将要运行进程的mm描述符为null,说明此进程是内核线程,则直接复用前一个进程的mm
    	 //描述符
    	if (unlikely(!mm)) {  
    		//将要运行的内核线程的active_mm值
    		next->active_mm = oldmm;
    		//oldmm引用加一
    		atomic_inc(&oldmm->mm_count);
    		//在多核系统下进入tlb懒惰模式,避免刷新tlb,这样速度更快,因为内核线程只需要使用
    		//页表中的内核态部分,所以没必要刷新tlb,刷新tlb会导致缓存丢失,需要重新加载
    		enter_lazy_tlb(oldmm, next);
    	} else
    		//如果是用户进程或线程则切换mm,也就是切换页表,在x86下就是重新装载CR3寄存器
    		switch_mm(oldmm, mm, next);
    
    	//如果当前进程是内核线程,则将activemm置为null
    	if (unlikely(!prev->mm)) {
    		prev->active_mm = NULL;
    		WARN_ON(rq->prev_mm);
    		rq->prev_mm = oldmm;
    	}
    	/*
    	 * Since the runqueue lock will be released by the next
    	 * task (which is an invalid locking op but in the case
    	 * of the scheduler it's an obvious special-case), so we
    	 * do an early lockdep release here:
    	 */
    #ifndef __ARCH_WANT_UNLOCKED_CTXSW
    	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
    #endif
    
    	/* 开始切换寄存器和内核栈 */
    	switch_to(prev, next, prev);
    
    	return prev;
    }

    context_switch首先切换进程的地址空间,这里面会对内核线程和普通进程线程做区别对待,在将要运行的进程是内核线程时,cpu的tlb会进入懒惰模式,因为内核线程只需要在内核空间运行,它可以借用前一个进程的mm,因为每个页表内核态部分都是一样的,所以tlb进入懒惰模式可以避免页表cache刷新造成大量的cache miss,可以提高速度。如果是用户态进程线程,则调用switch_mm进行地址空间切换。

    /*
     *   切换mm,重新装载cr3页表指针
     */
    static inline void switch_mm(struct mm_struct *prev,
    			     struct mm_struct *next,
    			     struct task_struct *tsk)
    {
    	int cpu = smp_processor_id();//获取当前cpu
    
    	//如果当前mm和将要运行的进程的mm不相等,说明是进程的切换
    	//单核cpu下,如果是同一个进程下的线程切换则不需要做任何处理
    	if (likely(prev != next)) {  
    		/* stop flush ipis for the previous mm */
    		cpu_clear(cpu, prev->cpu_vm_mask); //清除cpuvm标记位
    #ifdef CONFIG_SMP
    		per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK; //当前cpu的tlb状态
    		per_cpu(cpu_tlbstate, cpu).active_mm = next; //当前cpu的活动mm是将要运行进程的mm
    #endif
    		cpu_set(cpu, next->cpu_vm_mask); //设置新的cpuvm标志
    
    		/* Re-load page tables */
    		load_cr3(next->pgd);   //装载新的页表地址,到此cpu就开始在next进程下的地址空间执行了
    
    		/*
    		 * 如果局部描述符表不同,则装载新的局部描述符表
    		 */
    		if (unlikely(prev->context.ldt != next->context.ldt))
    			load_LDT_nolock(&next->context, cpu);
    	}
    #ifdef CONFIG_SMP
    	//在多核cpu下如果相等,则说明是同一个地址空间,说明是同一个进程里的线程切换
    	else {
    		per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;
    		BUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);
    		//如果next的cpuvm标志位没有置位,说明处于tlb懒惰模式,则需要重新装载
    		//cr3
    		if (!cpu_test_and_set(cpu, next->cpu_vm_mask)) {
    			/* We were in lazy tlb mode and leave_mm disabled 
    			 * tlb flush IPI delivery. We must reload %cr3.
    			 */
    			load_cr3(next->pgd);
    			load_LDT_nolock(&next->context, cpu);
    		}
    	}
    #endif
    }

    地址空间交换完了,下面就应该是寄存器和内核栈了。由于是寄存器和内核栈的交换所以主要得用汇编实现了。看switch_to宏。

    #define switch_to(prev,next,last) do {					\
    	unsigned long esi,edi;						\
    	asm volatile("pushfl\n\t"		/* 保存prev的EFLAGS */	\
    		     "pushl %%ebp\n\t"		/*保存prev 的ebp*/			\
    		     "movl %%esp,%0\n\t"	/* 保存prev内核栈指针,保存到当前进程的thread结构 */ \
    		     "movl %5,%%esp\n\t"	/* 将next进程的内核栈指针加载到esp,完成内核栈切换 */\
    		     "movl $1f,%1\n\t"		/* 将prev下一次被调度运行的地址保存到thered的eip */\
    		     "pushl %6\n\t"		/* 将next将要运行的地址压栈*/	\
    		     "jmp __switch_to\n"/*短调用__switch_to,从__switch_to返回的时候直接返回到%6*/\
    		     "1:\t"						\
    		     "popl %%ebp\n\t"					\
    		     "popfl"						\
    		     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),	\
    		      "=a" (last),"=S" (esi),"=D" (edi)			\
    		     :"m" (next->thread.esp),"m" (next->thread.eip),	\
    		      "2" (prev), "d" (next));				\
    } while (0)

    核心就是prev保存它的eflags,ebp,eip,esp。然后切换到next内核栈,至此prev进程就被挂起来了,把next上次被挂起的地址压栈,调用__switch_to函数,__switch_to函数返回的时候会直接跳到标号1或者ret_from_fork。这里有两种情况1、当一个进程是新创建的,首次运行,则会跳到ret_from_fork汇编例程。2、当一个进程是被挂起,又被重新调度的,则跳转到标号1。

    再看__switch_to函数

    //采用fastcall调用,则不使用栈传参,使用通用寄存器传递参数
    struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
    {
    	struct thread_struct *prev = &prev_p->thread,
    				 *next = &next_p->thread;
    	int cpu = smp_processor_id();//获取当前cpu
    	//获取tss段
    	struct tss_struct *tss = &per_cpu(init_tss, cpu);
    
    	/* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
    	//浮点寄存器相关
    	__unlazy_fpu(prev_p);
    
    	/*
    	 * 重新装载esp0
    	 */
    	load_esp0(tss, next);
    
    	//保存prev段寄存器
    	savesegment(fs, prev->fs);
    	savesegment(gs, prev->gs);
    
    	/*
    	 * 装载每线程的线程本地描述符
    	 */
    	load_TLS(next, cpu);
    
    	/*
    	 *装载新的fs gs
    	 */
    	if (unlikely(prev->fs | next->fs))
    		loadsegment(fs, next->fs);
    
    	if (prev->gs | next->gs)
    		loadsegment(gs, next->gs);
    
    	/*
    	 * 如果prev和next的IO特权级不一样则需要重新装载
    	 */
    	if (unlikely(prev->iopl != next->iopl))
    		set_iopl_mask(next->iopl);
    
    	/*
    	 * 处理debug寄存器和IO位图
    	 */
    	if (unlikely((task_thread_info(next_p)->flags & _TIF_WORK_CTXSW)
    	    || test_tsk_thread_flag(prev_p, TIF_IO_BITMAP)))
    		__switch_to_xtra(next_p, tss);
    
    	disable_tsc(prev_p, next_p);
    
    	//返回prev_p,这个prev_p是用eax返回的,如果next进程是新进程从未运行过,则返回到
    	//ret_from_fork,如果不是,则返回到switch_to的标号1
    	return prev_p;
    }

    这个函数主要是保存和恢复一些寄存器。其中看返回的prev_p是用eax返回的,shcedule函数中调用context_switch函数的语句是prev = context_switch(rq, prev, next);这个prev就是prev_p,比如进程A切换到进程B时,此时context_switch未返回前prev为A,再由进程B切换到进程C,再由进程C切换到A时,context_switch返回,此时prev为C,所以context_switch返回的值就是当前进程前一个进程的task_strcuct。

    从整体看流程

    由schedule函数到context_switch,context_switch主要切换mm(cr3页表寄存器),寄存器,内核栈。切换完之后,如果next是用户态进程或线程也会返回到用户态陷入内核态时被打断的地址继续执行。

    展开全文
  • 什么是:CPU寄存器 CPU上下文切换? 进程上下文切换 线程上下文切换 中断上下文切换 Go程序内核线程能有多少个? Linux创建的线程是用户级还是内核级线程? 什么是:CPU寄存器 CPU上下文切换? CPU寄存器,是...

    最近在看Go语言的goroutine调度,看到一篇理论文章,对一些关于Linux多线程的知识进行进一步的了解并且记录。

    目录

    什么是:CPU寄存器 CPU上下文切换?

    进程上下文切换

    线程上下文切换

    中断上下文切换

    Go程序内核线程能有多少个?

    Linux创建的线程是用户级还是内核级线程?


    什么是:CPU寄存器 CPU上下文切换?

    CPU寄存器,是CPU内置的容量小、但速度极快的内存

    在说上下文切换的前,需要知道

    程序计数器(Program Counter,PC),则是用来存储CPU正在执行的指令的位置,或者即将执行的下一条指令的位置。他们(CPU寄存器和程序计数器)都是CPU在运行任何任务前,必须依赖的环境,因此也被叫做CPU上下文。

    CPU上下文切换,就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文,到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而保存下来的上下文,会存储在系统内核中,并在任务重新调度执行的时候再加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

    CPU上下文切换分为三种:

    1. 进程上下文切换
    2. 线程上下文切换
    3. 中断上下文切换

    进程上下文切换

    Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中。CPU特权等级的Ring0 和 Ring3(也就是说Ring0和Ring3程序可以在CPU上运行)。

    1. 内核空间(Ring 0)具有最高权限,可以直接访问所有资源。
    2. 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

    换个角度看,也就是说,进程即可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行是,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

    从用户态到内核态的转变,需要通过系统调用来完成,比如当我们查看文件内容时,就需要多次系统调用来完成:首先调用open()打开文件,然后调用read()读取文件内容,并调用write()将内容写到标准输出,最后再调用close()关闭文件。

    • 那么,系统调用的过程有没有发生CPU上下文切换呢?答案是肯定的。

    CPU寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。而系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程,所以一次系统调用的过程,其实是发生了两次CPU上下文切换。

    不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的。进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。

    所以,系统调用过程通常称为特权模式切换而不是上下文切换。但实际上系统调用过程中,CPU的上下文切换还是无法避免的。

    • 那么,进程上下文切换跟系统调用又有什么区别呢?

    首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以进程的上下文不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包含了内核堆栈、寄存器等内核空间状态。

    因此,进程的上下文切换就比系统调用多了一步:在保存当前进程的内核状态和CPU寄存器之前,需要先把该进程的虚拟内存、栈等用户态资源保存下来;而加载了下一进程的内核态后,还需要刷新新进程的虚拟内存和用户栈。如下图所示,保存上下文和恢复上下文的过程,并不是免费的,需要内核在CPU上允许才能完成。

    根据Tsuna的测试报告,每次上下文切换都需要几十纳秒到数微妙的CPU时间。这个时间还是相当可观的,特别是进程上下文切换次数较多的情况下,很容易导致CPU将大量的时间耗费在寄存器、内核栈、以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。

    另外,我们知道,Linux通过TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。

    • 知道了进程上下文切换潜在的性能问题后,我们再来看,究竟什么时候会切换进程上下文?

    显然,进程切换时才需要切换上下文,换句话说,只有在进程调度的时候,才需要切换上下文。Linux为每个CPU都维护了一个就绪队列,将获取进程(即正在运行和等待CPU的进程)按照优先级和等待CPU的时间排序,然后选择最需要CPU的进程,也就是优先级最高和等待CPU时间最长的进程来运行。

    • 那么,进程在什么时候才会被调度到CPU上运行呢?

    最容易想到的一个时机,就是进程执行完,终止了,它之前使用的CPU会释放出来,这个时候再从就绪队列里,拿一个新的进程过来运行。其实还有很多其他场景,也会触发进程调度,这里逐个梳理下。

    1. 为了保证所有进程可以得到公平调度,CPU时间片被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其他正在等待CPU的进程运行。
    2. 进程在系统资源不足(比如内存不足)时,需要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
    3. 当进程通过睡眠函数sleep这样的方法将自己主动挂起时,自然也会重新调度。
    4. 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级的进程来运行。
    5. 当发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中中断服务程序。

    线程上下文切换

    线程与进程最大的区别在与,线程是调度的基本单位,而进程则是资源拥有的基本单位。所谓内核中的任务调用,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解:

    • 当进程只有一个线程时,可以认为进程就等于线程。
    • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
    • 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

    这么一来,线程的上下文切换其实就可以分为两种情况:

    1. 前后两个线程属于不同进程,此时,由于资源不共享,所以切换过程就跟进程上下文切换是一样的。
    2. 前后两个线程属于同一个进程,此时,应为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据,寄存器等不共享的数据。

    所以同为上下文切换,但同进程内的线程切换,要比多进程间切换消耗更少的资源,这也正是多线程代替多进程的一个优势。

    中断上下文切换

    为了快速响应硬件的时间,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备时间。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。

    因为中断这块一般都不会涉及,所以不做过多介绍。

     

    Go程序内核线程能有多少个?

    其实这个是简单的问题(上面已经间接回答了),在没有看goroutine的MPG调度模型前,我肯定知道,理论上会有N个内核线程;但是看完后,总是模模糊糊的,以为会和CPU个数有关系,其实没有关系,M(即内核线程)是按需创建的,但是如果只有一个cpu(1核)同一时刻只能执行一个内核线程。(这也算是个误区,很多人认为runtime.GOMAXPROCS可以限制系统线程的数量)

    虽然M理论上可以有N(但是N<maxcount)个,但是很多以后会有一个问题就是,频繁CPU上下文切换,倒是大多时间都消耗在CPU上下文切换。

     

    Linux创建的线程是用户级还是内核级线程

    你可能知道:线程分为内核态线程和用户态线程,用户态线程需要绑定内核态线程,CPU并不能感知用户态线程的存在,它只知道它在运行1个线程,这个线程实际是内核态线程。

    线程的实现曾有3种模型:

    1. 多对一(M:1)的用户级线程模型
    2. 一对一(1:1)的内核级线程模型
    3. 多对多(M:N)的两级线程模型

    上面的x对y(x:y)即x个用户态线程对应y个内核调度实体(Kernel Scheduling Entity,这个是内核分配CPU的对象单位)。

    多对一用户线级程模型  

    多对一线程模型中,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。用户态线程的很多操作对内核来说都是透明的,因为不需要内核来接管,这意味不需要内核态和用户态频繁切换。线程的创建、调度、同步处理速度非常快。当然线程的一些其他操作还是要经过内核,如IO读写。这样导致了一个问题:当多线程并发执行时,如果其中一个线程执行IO操作时,内核接管这个操作,如果IO阻塞,用户态的其他线程都会被阻塞,因为这些线程都对应同一个内核调度实体。在多处理器机器上,内核不知道用户态有这些线程,无法把它们调度到其他处理器,也无法通过优先级来调度。这对线程的使用是没有意义的!

    一对一内核极线程模型 

    一对一模型中,每个用户线程都对应各自的内核调度实体。内核会对每个线程进行调度,可以调度到其他处理器上面。当然由内核来调度的结果就是:线程的每次操作会在用户态和内核态切换。另外,内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。但该模型的实用性还是高于多对一的线程模型。

    多对多两极线程模型   

    多对多模型中,结合了1:1和M:1的优点,避免了它们的缺点。每个线程可以拥有多个调度实体,也可以多个线程对应一个调度实体。听起来好像非常完美,但线程的调度需要由内核态和用户态一起来实现。可想而知,多个对象操作一个东西时,肯定要一些其他的同步机制。用户态和内核态的分工合作导致实现该模型非常复杂。linux多线程模型曾经也想使用该模型,但它太复杂,要对内核进行大范围改动,所以还是采用了一对一的模型!!!

     

    那么,Linux下C语言thread_create函数创建的线程属于用户级还是内核级。其实创建的还是用户级线程,只不过对应了一个内核级的内核调度线程(这个线程实际的内核调度器分配的),即一对一(1:1)的内核级线程模型。

    Go语言中goroutine使用就是一种特殊的两级线程模型(MPG模型)。得益于这种模型,所以Go语言并发性比其他语言都要好。


    参考原文:

    CPU上下文切换是什么

    Linux历史上线程的3种实现模型 

    Go并发原理

    展开全文
  • 本文主要关注进程管理的一个切入点,那就是进程的上下文切换,来理解linux内核是如何进程进程上下文切换的,从而揭开上下文切换的神秘面纱。 (注意:本文以linux-5.0内核源码讲解,采用arm64架构)

    我都知道操作系统的一个重要功能就是进行进程管理,而进程管理就是在合适的时机选择合适的进程来执行,在单个cpu运行队列上各个进程宏观并行微观串行执行,多个cpu运行队列上的各个进程之间完全的并行执行。进程管理是个复杂的过程,例如进程的描述、创建和销毁、生命周期管理、进程切换、进程抢占、调度策略、负载均衡等等。本文主要关注进程管理的一个切入点,那就是进程的上下文切换,来理解linux内核是如何进程进程上下文切换的,从而揭开上下文切换的神秘面纱。
    (注意:本文以linux-5.0内核源码讲解,采用arm64架构)

    本文目录:
    1.进程上下文的概念
    2.上下文切换详细过程
     2.1 进程地址空间切换
     2.2 处理器状态(硬件上下文)切换
    3.ASID机制
    4.普通用户进程、普通用户线程、内核线程切换的差别
    5.进程切换全景视图
    6.总结

    1.进程上下文的概念

    进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

    实际上linux内核中,进程上下文包括进程的虚拟地址空间和硬件上下文。

    进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用task_struct结构的thread成员的cpu_context成员来描述,包括x19-x28,sp, pc等。

    如下为硬件上下文存放示例图:

    2.上下文切换详细过程

    进程上下文切换主要涉及到两部分主要过程:进程地址空间切换和处理器状态切换。地址空间切换主要是针对用户进程而言,而处理器状态切换对应于所有的调度单位。下面我们分别看下这两个过程:

    __schedule   // kernel/sched/core.c->context_switch  ->switch_mm_irqs_off   //进程地址空间切换  ->switch_to //处理器状态切换

    2.1 进程地址空间切换

    进程地址空间指的是进程所拥有的虚拟地址空间,而这个地址空间是假的,是linux内核通过数据结构来描述出来的,从而使得每一个进程都感觉到自己拥有整个内存的假象,cpu访问的指令和数据最终会落实到实际的物理地址,对用进程而言通过缺页异常来分配和建立页表映射。进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。
    实际上,进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到进程描述符(我们通常所说的进程控制块PCB)task_struct中,mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。

    pgd中保存的是进程的页全局目录的虚拟地址(本文会涉及到页表相关的一些概念,在此不是重点,不清楚的可以查阅相关资料,后期有机会会讲解进程页表),记住保存的是虚拟地址,那么pgd的值是何时被设置的呢?答案是fork的时候,如果是创建进程,需要分配设置mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给pgd。

    我们来看看进程地址空间究竟是如何切换的,结果会让你大吃一惊(这里暂且不考虑asid机制,后面有机会会在其他文章中讲解):
    代码路径如下:

    context_switch  // kernel/sched/core.c->switch_mm_irqs_off  ->switch_mm  ->__switch_mm    ->check_and_switch_context    ->cpu_switch_mm      ->cpu_do_switch_mm(virt_to_phys(pgd),mm) //arch/arm64/include/asm/mmu_context.h    arch/arm64/mm/proc.S158 /*159  *      cpu_do_switch_mm(pgd_phys, tsk)160  *161  *      Set the translation table base pointer to be pgd_phys.162  *163  *      - pgd_phys - physical address of new TTB164  */165 ENTRY(cpu_do_switch_mm)166         mrs     x2, ttbr1_el1167         mmid    x1, x1                          // get mm->context.id168         phys_to_ttbr x3, x0169170 alternative_if ARM64_HAS_CNP171         cbz     x1, 1f                          // skip CNP for reserved ASID172         orr     x3, x3, #TTBR_CNP_BIT173 1:174 alternative_else_nop_endif175 #ifdef CONFIG_ARM64_SW_TTBR0_PAN176         bfi     x3, x1, #48, #16                // set the ASID field in TTBR0177 #endif178         bfi     x2, x1, #48, #16                // set the ASID179         msr     ttbr1_el1, x2                   // in TTBR1 (since TCR.A1 is set)180         isb181         msr     ttbr0_el1, x3                   // now update TTBR0182         isb183         b       post_ttbr_update_workaround     // Back to C code...184 ENDPROC(cpu_do_switch_mm)

    代码中最核心的为181行,最终将进程的pgd虚拟地址转化为物理地址存放在ttbr0_el1中,这是用户空间的页表基址寄存器,当访问用户空间地址的时候mmu会通过这个寄存器来做遍历页表获得物理地址(ttbr1_el1是内核空间的页表基址寄存器,访问内核空间地址时使用,所有进程共享,不需要切换)。完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换。

    内核处理的是不是很简单,很优雅,别看只是设置了页表基址寄存器,也就是将即将执行的进程的页全局目录的物理地址设置到页表基址寄存器,他却完成了地址空间切换的壮举,有的小伙伴可能不明白为啥这就完成了地址空间切换?试想如果进程想要访问一个用户空间虚拟地址,cpu的mmu所做的工作,就是从页表基址寄存器拿到页全局目录的物理基地址,然后和虚拟地址配合来查查找页表,最终找到物理地址进行访问(当然如果tlb命中就不需要遍历页表),每次用户虚拟地址访问的时候(内核空间共享不考虑),由于页表基地址寄存器内存放的是当前执行进程的页全局目录的物理地址,所以访问自己的一套页表,拿到的是属于自己的物理地址(实际上,进程是访问虚拟地址空间的指令数据的时候不断发生缺页异常,然后缺页异常处理程序为进程分配实际的物理页,然后将页帧号和页表属性填入自己的页表条目中),就不会访问其他进程的指令和数据,这也是为何多个进程可以访问相同的虚拟地址而不会出现差错的原因,而且做到的各个地址空间的隔离互不影响(共享内存除外)。

    其实,地址空间切换过程中,还会清空tlb,防止当前进程虚拟地址转化过程中命中上一个进程的tlb表项,一般会将所有的tlb无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的tlb,造成很大概率的tlb miss,需要重新遍历多级页表,所以arm64在tlb表项中增加了非全局(nG)位区分内核和进程的页表项,使用ASID区分不同进程的页表项,来保证可以在切换地址空间的时候可以不刷tlb,后面会主要讲解ASID技术。

    还需要注意的是仅仅切换用户地址空间,内核地址空间由于是共享的不需要切换,也就是为何切换到内核线程不需要也没有地址空间的原因。

    如下为进程地址空间切换示例图:

    2.2 处理器状态(硬件上下文)切换

    前面进行了地址空间切换,只是保证了进程访问指令数据时访问的是自己地址空间(当然上下文切换的时候处于内核空间,执行的是内核地址数据,当返回用户空间的时候才有机会执行用户空间指令数据**,地址空间切换为进程访问自己用户空间做好了准备**),但是进程执行的内核栈还是前一个进程的,当前执行流也还是前一个进程的,需要做切换。

    arm64中切换代码如下:

    switch_to->__switch_to  ... //浮点寄存器等的切换  ->cpu_switch_to(prev, next)
    arch/arm64/kernel/entry.S:1032 /*1033  * Register switch for AArch64. The callee-saved registers need to be saved1034  * and restored. On entry:1035  *   x0 = previous task_struct (must be preserved across the switch)1036  *   x1 = next task_struct1037  * Previous and next are guaranteed not to be the same.1038  *1039  */1040 ENTRY(cpu_switch_to)1041         mov     x10, #THREAD_CPU_CONTEXT1042         add     x8, x0, x101043         mov     x9, sp1044         stp     x19, x20, [x8], #16             // store callee-saved registers1045         stp     x21, x22, [x8], #161046         stp     x23, x24, [x8], #161047         stp     x25, x26, [x8], #161048         stp     x27, x28, [x8], #161049         stp     x29, x9, [x8], #161050         str     lr, [x8]1051         add     x8, x1, x101052         ldp     x19, x20, [x8], #16             // restore callee-saved registers1053         ldp     x21, x22, [x8], #161054         ldp     x23, x24, [x8], #161055         ldp     x25, x26, [x8], #161056         ldp     x27, x28, [x8], #161057         ldp     x29, x9, [x8], #161058         ldr     lr, [x8]1059         mov     sp, x91060         msr     sp_el0, x11061         ret1062 ENDPROC(cpu_switch_to)

    其中x19-x28是arm64 架构规定需要调用保存的寄存器,可以看到处理器状态切换的时候将前一个进程(prev)的x19-x28,fp,sp,pc保存到了进程描述符的cpu_contex中,然后将即将执行的进程(next)描述符的cpu_contex的x19-x28,fp,sp,pc恢复到相应寄存器中,而且将next进程的进程描述符task_struct地址存放在sp_el0中,用于通过current找到当前进程,这样就完成了处理器的状态切换。

    实际上,处理器状态切换就是将前一个进程的sp,pc等寄存器的值保存到一块内存上,然后将即将执行的进程的sp,pc等寄存器的值从另一块内存中恢复到相应寄存器中,恢复sp完成了进程内核栈的切换,恢复pc完成了指令执行流的切换。其中保存/恢复所用到的那块内存需要被进程所标识,这块内存这就是cpu_contex这个结构的位置(进程切换都是在内核空间完成)。

    由于用户空间通过异常/中断进入内核空间的时候都需要保存现场,也就是保存发生异常/中断时的所有通用寄存器的值,内核会把“现场”保存到每个进程特有的进程内核栈中,并用pt_regs结构来描述,当异常/中断处理完成之后会返回用户空间,返回之前会恢复之前保存的“现场”,用户程序继续执行。
    所以当进程切换的时候,当前进程被时钟中断打断,将发生中断时的现场保存到进程内核栈(如:sp, lr等),然后会切换到下一个进程,当再次回切换回来的时候,返回用户空间的时候会恢复之前的现场,进程就可以继续执行(执行之前被中断打断的下一条指令,继续使用自己用户态sp),这对于用户进程来说是透明的。

    如下为硬件上下文切换示例图:

     

    3.ASID机制

    前面讲过,进程切换的时候,由于tlb中存放的可能是其他进程的tlb表项,所有才需要在进程切换的时候进行tlb的清空工作(清空即是使得所有的tlb表项无效,地址转换需要遍历多级页表,找到页表项,然后重新加载页表项到tlb),有了ASID机制之后,命中tlb表项,由虚拟地址和ASID共同决定(当然还有nG位),可以减小进程切换中tlb被清空的机会。

    下面我们讲解ASID机制,ASID(Address Space Identifer 地址空间标识符),用于区别不同进程的页表项,arm64中,可以选择两种ASID长度8位或者16位,这里以8位来讲解。

    如果ASID长度为8位,那么ASID有256个值,但是由于0是保留的,所有可以分配的ASID范围就为1-255,那么可以标识255个进程,当超出255个进程的时候,会出现两个进程的ASID相同的情况,因此内核使用了ASID版本号。
    内核中处理如下(参考arch/arm64/mm/context.c):

    1)内核为每个进程分配一个64位的软件ASID,其中低8位为硬件ASID,高56位为ASID版本号,这个软件ASID存放放在进程的mm_struct结构的context结构的id中,进程创建的时候会初始化为0。
    2)内核中有一个64位的全局变量asid_generation,同样它的高56位为ASID版本号,用于标识当前ASID分配的批次。
    3)当进程调度,由prev进程切换到next进程的时候,如果不是内核线程则进行地址空间切换调用check_and_switch_context,此函数会判断next进程的ASID版本号是否和全局的ASID版本号相同(是否处于同一批次),如果相同则不需要为next进程分配ASID,不相同则需要分配ASID。
    4)内核使用asid_map位图来管理硬件ASID的分配,asid_bits记录使用的ASID的长度,每处理器变量active_asids保存当前分配的硬件ASID,每处理器变量reserved_asids存放保留的ASID,tlb_flush_pending位图记录需要清空tlb的cpu集合。
    硬件ASID分配策略如下:
    (1)如果进程的ASID版本号和当前全局的ASID版本号相同(同批次情况),则不需要重新分配ASID。
    (2)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),且进程原本的硬件ASID已经被分配,则重新分配新的硬件ASID,并将当前全局的ASID版本号组合新分配的硬件ASID写到进程的软件ASID中。
    (3)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),且进程原本的硬件ASID还没有被分配,则不需要重新分配新的硬件ASID,只需要更新进程软件ASID版本号,并将当前全局的ASID版本号组合进程原来的硬件ASID写到进程的软件ASID中。
    (4)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),需要分配硬件ASID时,发现硬件ASID已经被其他进程分配完(asid_map位图中查找,发现位图全1),则这个时候需要递增全局的ASID版本号, 清空所有cpu的tlb, 清空asid_map位图,然后分配硬件ASID,并将当前全局的ASID版本号组合新分配的硬件ASID写到进程的软件ASID中。

    下面我们以实例来看ASID的分配过程:
    如下图:

    我们假设图中从A进程到D进程,有255个进程,刚好分配完了asid, ,从A到D的切换过程中使用的都是同一批次的asid版本号。

    则这个过程中,有进程会创建的时候被切换到,假设不超出255个进程,在切换过程中会为新进程分配硬件的ASID,分配完后下次切换到他时由于他的ASID版本号和当前的全局的ASID版本号相同,所以不需要再次分配ASID,当然也不需要清空tlb。

    注:这里说的ASID即为硬件ASID区别于ASID版本号。

    情况1-ASID版本号不变 属于策略(1):从C进程到D进程切换,内核判断D进程的ASID版本号和当前的全局的ASID版本号相同,所以不需要为他分配ASID(执行快速路径switch_mm_fastpath去设置ttbrx_el1))。

    情况2 -硬件ASID全部分配完 属于策略(4):假设到达D进程时,asid已经全部分配完(系统中有255个进程都分配到了硬件asid号),这个时候新创建的进程E被调度器选中,切换到E,由于新创建的进程的软件ASID被初始化为0,所以和当前的全局的ASID版本号不同(不在同一批次),则这个时候会执行new_context为进程分配ASID,但是由于没有可以分配的ASID,所以会将全局的ASID版本号加1(发生ASID回绕),这个时候全局的ASID为801,然后清空asid_map,置位tlb_flush_pending所有bit用于清空所有cpu的tlb,然后再次去分配硬件ASID给E进程,这个时候分配到了1给他(将ASID版本号)。

    情况3-ASID版本号发生变化,进程的硬件ASID可以再次使用 属于策略(3):假设从E切换到了B进程,而B进程之前已经在全局的ASID版本号为800的批次上分配了编号为5的硬件ASID,但是B进程的ASID版本号800和现在全局的ASID版本号801不相同,所有需要new_context为进程分配ASID,分配的时候发现asid_map中编号为5没有被置位,也就是没有其他进程分配了5这个ASID,所有可以继续使用原来分配的硬件ASID 5。

    情况4 - ASID版本号发生变化,有其他进程已经分配了相同的硬件ASID 属于策略(2): 假设从B进程切换到A进程,而B进程之前已经在全局的ASID版本号为800的批次上分配了编号为1的硬件ASID,但是B进程的ASID版本号800和现在全局的ASID版本号801不相同,所有需要new_context为进程分配ASID,分配的时候发现asid_map中编号为1已经被置位,也就是其他进程已经分配了1这个ASID,需要从asid_map寻找下一个空闲的ASID,则分配了新的ASID为6。

    假设从A到E,由于E的ASID版本号和全局的ASID版本号(同一批次),和情况1相同,不需要分配ASID。但是之前原来处于800这个ASID版本号批次的进程都需要重新分配ASID,有的可以使用原来的硬件ASID,有的重新分配硬件ASID,但是都将ASID版本号修改为了现在全局的ASID版本号801。但是,随着硬件ASID的不断分配,最终处于801这一批次的硬件ASID也会分配完,这个时候就是上面的情况2,要情况所有cpu的tlb。

    我可以看到有了ASID机制之后,由于只有当硬件ASID被分配完了(如被255个进程使用),发生回绕的时候才会清空所有cpu的tlb,大大提高了系统的性能(没有ASID机制的情况下每次进程切换需要地址空间切换的时候都需要清空tlb)。

    4. 普通用户进程、普通用户线程、内核线程切换的差别

    内核地址空间切换的时候有一下原则:看的是进程描述符的mm_struct结构,即是成员mm:
    1)如果mm为NULL,则表示即将切换的是内核线程,不需要切换地址空间(所有任务共享内核地址空间)。
    2)内核线程会借用前一个用户进程的mm,赋值到自己的active_mm(本身的mm为空),进程切换的时候就会比较前一个进程的active_mm和当前进程的mm。
    3)如果前一个任务的和即将切换的任务,具有相同的mm成员,也就是共享地址空间的线程则也不需要切换地址空间。

    ->所有的进程线程之间进行切换都需要切换处理器状态。
    ->对于普通的用户进程之间进行切换需要切换地址空间。
    ->同一个线程组中的线程之间切换不需要切换地址空间,因为他们共享相同的地址空间。
    -> 内核线程在上下文切换的时候不需要切换地址空间,仅仅是借用上一个进程mm_struct结构。

    有一下场景:
    约定:我们将进程/线程统称为任务,其中U表示用户任务(进程/线程),K表示内核线程,带有数字表示同一个线程组中的线程。
    有以下任务:Ua1 Ua2 Ub Uc Ka Kb (eg:Ua1为用户进程, Ua2为和Ua1在同一线程组的用户进程,Ub普通的用户进程,Ka普通的内核线程 )。

    如果调度顺序如下:

    Uc -> Ua1 -> Ua2 -> Ub -> Ka -> Kb -> Ub

    从Uc -> Ua1 由于是不同的进程,需要切换地址空间。
    从 Ua1 -> Ua2 由于是相同线程组中的不同线程,共享地址空间,在切换到Ua1的时候已经切换了地址空间,所有不需要切换地址空间。
    从 Ua2 -> Ub 由于是不同的进程,需要切换地址空间。
    从 Ub -> Ka 由于切换到内核线程,所以不需要切换地址空间。
    从Ka -> Kb 俩内核线程之前切换,不需要切换地址空间。
    从Kb -> Ub 从内核线程切换到用户进程,由于Ka和Kb都是借用Ub的active_mm,而Ub的active_mm 等于Ub的mm,所以这个时候 Kb的active_mm和 Ub的mm相同,所有也不会切换地址空间。

    如下为多任务地址空间切换示例图:

     

    5. 进程切换全景视图

    我们以下场景为例:
    A,B两个进程都是普通的用户进程,从进程A切换到进程B,简单起见我们在这里不考虑其他的抢占时机,我们假设A,B进程只是循环进行一些基本的运算操作,从来不调用任何系统调用,只考虑被时钟中断,返回用户空间之前被抢占的情况。

    下面给出进程切换的全景视图:

    视图中已经讲解很清楚,需要强调3个关键点:

    1.发生中断时的保存现场,将发生中断时的所有通用寄存器保存到进程的内核栈,使用struct pt_regs结构。

    2.地址空间切换将进程自己的页全局目录的基地址pgd保存在ttbr0_le1中,用于mmu的页表遍历的起始点。

    3.硬件上下文切换的时候,将此时的调用保存寄存器和pc, sp保存到struct cpu_context结构中。做好了这几个保存工作,当进程再次被调度回来的时候,通过cpu_context中保存的pc回到了cpu_switch_to的下一条指令继续执行,而由于cpu_context中保存的sp导致当前进程回到自己的内核栈,经过一系列的内核栈的出栈处理,最后将原来保存在pt_regs中的通用寄存器的值恢复到了通用寄存器,这样进程回到用户空间就可以继续沿着被中断打断的下一条指令开始执行,用户栈也回到了被打断之前的位置,而进程访问的指令数据做地址转化(VA到PA)也都是从自己的pgd开始进行,一切对用户来说就好像没有发生一样,简直天衣无缝。

    6. 总结

    进程管理中最重要的一步要进行进程上下文切换,其中主要有两大步骤:地址空间切换和处理器状态切换(硬件上下文切换),前者保证了进程回到用户空间之后能够访问到自己的指令和数据(其中包括减小tlb清空的ASID机制),后者保证了进程内核栈和执行流的切换,会将当前进程的硬件上下文保存在进程所管理的一块内存,然后将即将执行的进程的硬件上下文从内存中恢复到寄存器,有了这两步的切换过程保证了进程运行的有条不紊,当然切换的过程是在内核空间完成,这对于进程来说是透明的。

     

     

    展开全文
  • 都知道操作系统的一个重要功能就是...本文主要关注进程管理的一个切入点,那就是进程的上下文切换,来理解linux内核是如何进程进程上下文切换的,从而揭开上下文切换的神秘面纱。进程地址空间切换和处理器状态切换。

    目录

    1.进程上下文的概念

    2.上下文切换详细过程

    2.1 进程地址空间切换

    2.2 处理器状态(硬件上下文)切换

    3.ASID机制

    4. 普通用户进程、普通用户线程、内核线程切换的差别

    5. 进程切换全景视图

    6. 总结


     

    都知道操作系统的一个重要功能就是进行进程管理,而进程管理就是在合适的时机选择合适的进程来执行,在单个cpu运行队列上各个进程宏观并行微观串行执行,多个cpu运行队列上的各个进程之间完全的并行执行。进程管理是个复杂的过程,例如进程的描述、创建和销毁、生命周期管理、进程切换、进程抢占、调度策略、负载均衡等等。本文主要关注进程管理的一个切入点,那就是进程的上下文切换,来理解linux内核是如何进程进程上下文切换的,从而揭开上下文切换的神秘面纱。
    (注意:本文以linux-5.0内核源码讲解,采用arm64架构

     

    1.进程上下文的概念


    进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

    实际上linux内核中,进程上下文包括进程的虚拟地址空间和硬件上下文。

    进程硬件上下文包含了当前cpu的一组寄存器的集合,arm64中使用task_struct结构的thread成员的cpu_context成员来描述,包括x19-x28,sp, pc等。

    如下为硬件上下文存放示例图:

     

    2.上下文切换详细过程


    进程上下文切换主要涉及到两部分主要过程:进程地址空间切换处理器状态切换。地址空间切换主要是针对用户进程而言,而处理器状态切换对应于所有的调度单位。

    下面我们分别看下这两个过程:

    __schedule   // kernel/sched/core.c
    ->context_switch
      ->switch_mm_irqs_off   //进程地址空间切换
      ->switch_to //处理器状态切换

    2.1 进程地址空间切换


    进程地址空间指的是进程所拥有的虚拟地址空间,而这个地址空间是假的,是linux内核通过数据结构来描述出来的,从而使得每一个进程都感觉到自己拥有整个内存的假象,cpu访问的指令和数据最终会落实到实际的物理地址,对用进程而言通过缺页异常来分配和建立页表映射。进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。

    实际上,进程地址空间使用mm_struct结构体来描述,这个结构体被嵌入到进程描述符(我们通常所说的进程控制块PCB)task_struct中,mm_struct结构体将各个vma组织起来进行管理,其中有一个成员pgd至关重要,地址空间切换中最重要的是pgd的设置。

    pgd中保存的是进程的页全局目录的虚拟地址(本文会涉及到页表相关的一些概念,在此不是重点,不清楚的可以查阅相关资料,后期有机会会讲解进程页表),记住保存的是虚拟地址,那么pgd的值是何时被设置的呢?答案是fork的时候,如果是创建进程,需要分配设置mm_struct,其中会分配进程页全局目录所在的页,然后将首地址赋值给pgd。

    我们来看看进程地址空间究竟是如何切换的,结果会让你大吃一惊(这里暂且不考虑asid机制,后面有机会会在其他文章中讲解):
    代码路径如下:

    context_switch  // kernel/sched/core.c
    ->switch_mm_irqs_off
      ->switch_mm
      ->__switch_mm
        ->check_and_switch_context
        ->cpu_switch_mm
          ->cpu_do_switch_mm(virt_to_phys(pgd),mm) //arch/arm64/include/asm/mmu_context.h
        
    arch/arm64/mm/proc.S
    158 /*
    159  *      cpu_do_switch_mm(pgd_phys, tsk)
    160  *
    161  *      Set the translation table base pointer to be pgd_phys.
    162  *
    163  *      - pgd_phys - physical address of new TTB
    164  */
    165 ENTRY(cpu_do_switch_mm)
    166         mrs     x2, ttbr1_el1
    167         mmid    x1, x1                          // get mm->context.id
    168         phys_to_ttbr x3, x0
    169
    170 alternative_if ARM64_HAS_CNP
    171         cbz     x1, 1f                          // skip CNP for reserved ASID
    172         orr     x3, x3, #TTBR_CNP_BIT
    173 1:
    174 alternative_else_nop_endif
    175 #ifdef CONFIG_ARM64_SW_TTBR0_PAN
    176         bfi     x3, x1, #48, #16                // set the ASID field in TTBR0
    177 #endif
    178         bfi     x2, x1, #48, #16                // set the ASID
    179         msr     ttbr1_el1, x2                   // in TTBR1 (since TCR.A1 is set)
    180         isb
    181         msr     ttbr0_el1, x3                   // now update TTBR0
    182         isb
    183         b       post_ttbr_update_workaround     // Back to C code...
    184 ENDPROC(cpu_do_switch_mm)

    代码中最核心的为181行,最终将进程的pgd虚拟地址转化为物理地址存放在ttbr0_el1中,这是用户空间的页表基址寄存器,当访问用户空间地址的时候mmu会通过这个寄存器来做遍历页表获得物理地址(ttbr1_el1是内核空间的页表基址寄存器,访问内核空间地址时使用,所有进程共享,不需要切换)。完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换

    内核处理的是不是很简单,很优雅,别看只是设置了页表基址寄存器,也就是将即将执行的进程的页全局目录的物理地址设置到页表基址寄存器,他却完成了地址空间切换的壮举,有的小伙伴可能不明白为啥这就完成了地址空间切换?

    试想如果进程想要访问一个用户空间虚拟地址,cpu的mmu所做的工作,就是从页表基址寄存器拿到页全局目录的物理基地址,然后和虚拟地址配合来查查找页表,最终找到物理地址进行访问(当然如果tlb命中就不需要遍历页表),每次用户虚拟地址访问的时候(内核空间共享不考虑),由于页表基地址寄存器内存放的是当前执行进程的页全局目录的物理地址,所以访问自己的一套页表,拿到的是属于自己的物理地址(实际上,进程是访问虚拟地址空间的指令数据的时候不断发生缺页异常,然后缺页异常处理程序为进程分配实际的物理页,然后将页帧号和页表属性填入自己的页表条目中),就不会访问其他进程的指令和数据,这也是为何多个进程可以访问相同的虚拟地址而不会出现差错的原因,而且做到的各个地址空间的隔离互不影响(共享内存除外)。

    其实,地址空间切换过程中,还会清空tlb,防止当前进程虚拟地址转化过程中命中上一个进程的tlb表项,一般会将所有的tlb无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的tlb,造成很大概率的tlb miss,需要重新遍历多级页表,所以arm64在tlb表项中增加了非全局(nG)位区分内核和进程的页表项,使用ASID区分不同进程的页表项,来保证可以在切换地址空间的时候可以不刷tlb,后面会主要讲解ASID技术。

    还需要注意的是仅仅切换用户地址空间,内核地址空间由于是共享的不需要切换,也就是为何切换到内核线程不需要也没有地址空间的原因。

    如下为进程地址空间切换示例图:

     

    2.2 处理器状态(硬件上下文)切换


    前面进行了地址空间切换,只是保证了进程访问指令数据时访问的是自己地址空间(当然上下文切换的时候处于内核空间,执行的是内核地址数据,当返回用户空间的时候才有机会执行用户空间指令数据**,地址空间切换为进程访问自己用户空间做好了准备**),但是进程执行的内核栈还是前一个进程的当前执行流也还是前一个进程的,需要做切换

    arm64中切换代码如下:

    switch_to
    ->__switch_to
      ... //浮点寄存器等的切换
      ->cpu_switch_to(prev, next)
    
    arch/arm64/kernel/entry.S:
    1032 /*
    1033  * Register switch for AArch64. The callee-saved registers need to be saved
    1034  * and restored. On entry:
    1035  *   x0 = previous task_struct (must be preserved across the switch)
    1036  *   x1 = next task_struct
    1037  * Previous and next are guaranteed not to be the same.
    1038  *
    1039  */
    1040 ENTRY(cpu_switch_to)
    1041         mov     x10, #THREAD_CPU_CONTEXT
    1042         add     x8, x0, x10
    1043         mov     x9, sp
    1044         stp     x19, x20, [x8], #16             // store callee-saved registers
    1045         stp     x21, x22, [x8], #16
    1046         stp     x23, x24, [x8], #16
    1047         stp     x25, x26, [x8], #16
    1048         stp     x27, x28, [x8], #16
    1049         stp     x29, x9, [x8], #16
    1050         str     lr, [x8]
    1051         add     x8, x1, x10
    1052         ldp     x19, x20, [x8], #16             // restore callee-saved registers
    1053         ldp     x21, x22, [x8], #16
    1054         ldp     x23, x24, [x8], #16
    1055         ldp     x25, x26, [x8], #16
    1056         ldp     x27, x28, [x8], #16
    1057         ldp     x29, x9, [x8], #16
    1058         ldr     lr, [x8]
    1059         mov     sp, x9
    1060         msr     sp_el0, x1
    1061         ret
    1062 ENDPROC(cpu_switch_to)

    其中x19-x28是arm64 架构规定需要调用保存的寄存器,可以看到处理器状态切换的时候将前一个进程(prev)的x19-x28,fp,sp,pc保存到了进程描述符的cpu_contex中,然后将即将执行的进程(next)描述符的cpu_contex的x19-x28,fp,sp,pc恢复到相应寄存器中,而且将next进程的进程描述符task_struct地址存放在sp_el0中,用于通过current找到当前进程,这样就完成了处理器的状态切换。

    实际上,处理器状态切换就是将前一个进程的sp,pc等寄存器的值保存到一块内存上,然后将即将执行的进程的sp,pc等寄存器的值从另一块内存中恢复到相应寄存器中,恢复sp完成了进程内核栈的切换,恢复pc完成了指令执行流的切换。其中保存/恢复所用到的那块内存需要被进程所标识,这块内存这就是cpu_contex这个结构的位置(进程切换都是在内核空间完成)。

    由于用户空间通过异常/中断进入内核空间的时候都需要保存现场,也就是保存发生异常/中断时的所有通用寄存器的值,内核会把“现场”保存到每个进程特有的进程内核栈中,并用pt_regs结构来描述,当异常/中断处理完成之后会返回用户空间,返回之前会恢复之前保存的“现场”,用户程序继续执行。

    所以当进程切换的时候,当前进程被时钟中断打断,将发生中断时的现场保存到进程内核栈(如:sp, lr等),然后会切换到下一个进程,当再次回切换回来的时候,返回用户空间的时候会恢复之前的现场,进程就可以继续执行(执行之前被中断打断的下一条指令,继续使用自己用户态sp),这对于用户进程来说是透明的。

    如下为硬件上下文切换示例图:

     

    3.ASID机制


    前面讲过,进程切换的时候,由于tlb中存放的可能是其他进程的tlb表项,所有才需要在进程切换的时候进行tlb的清空工作(清空即是使得所有的tlb表项无效,地址转换需要遍历多级页表,找到页表项,然后重新加载页表项到tlb),有了ASID机制之后,命中tlb表项,由虚拟地址和ASID共同决定(当然还有nG位),可以减小进程切换中tlb被清空的机会

    下面我们讲解ASID机制,ASID(Address Space Identifer 地址空间标识符),用于区别不同进程的页表项,arm64中,可以选择两种ASID长度8位或者16位,这里以8位来讲解。

    如果ASID长度为8位,那么ASID有256个值,但是由于0是保留的,所有可以分配的ASID范围就为1-255,那么可以标识255个进程,当超出255个进程的时候,会出现两个进程的ASID相同的情况,因此内核使用了ASID版本号。

    内核中处理如下(参考arch/arm64/mm/context.c):

    • 1)内核为每个进程分配一个64位的软件ASID,其中低8位为硬件ASID,高56位为ASID版本号,这个软件ASID存放放在进程的mm_struct结构的context结构的id中,进程创建的时候会初始化为0。
    • 2)内核中有一个64位的全局变量asid_generation,同样它的高56位为ASID版本号,用于标识当前ASID分配的批次。
    • 3)当进程调度,由prev进程切换到next进程的时候,如果不是内核线程则进行地址空间切换调用check_and_switch_context,此函数会判断next进程的ASID版本号是否和全局的ASID版本号相同(是否处于同一批次),如果相同则不需要为next进程分配ASID,不相同则需要分配ASID。
    • 4)内核使用asid_map位图来管理硬件ASID的分配,asid_bits记录使用的ASID的长度,每处理器变量active_asids保存当前分配的硬件ASID,每处理器变量reserved_asids存放保留的ASID,tlb_flush_pending位图记录需要清空tlb的cpu集合。

    硬件ASID分配策略如下:

    • (1)如果进程的ASID版本号和当前全局的ASID版本号相同(同批次情况),则不需要重新分配ASID。
    • (2)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),且进程原本的硬件ASID已经被分配,则重新分配新的硬件ASID,并将当前全局的ASID版本号组合新分配的硬件ASID写到进程的软件ASID中。
    • (3)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),且进程原本的硬件ASID还没有被分配,则不需要重新分配新的硬件ASID,只需要更新进程软件ASID版本号,并将当前全局的ASID版本号组合进程原来的硬件ASID写到进程的软件ASID中。
    • (4)如果进程的ASID版本号和当前全局的ASID版本号不相同(不同批次情况),需要分配硬件ASID时,发现硬件ASID已经被其他进程分配完(asid_map位图中查找,发现位图全1),则这个时候需要递增全局的ASID版本号, 清空所有cpu的tlb, 清空asid_map位图,然后分配硬件ASID,并将当前全局的ASID版本号组合新分配的硬件ASID写到进程的软件ASID中。

    下面我们以实例来看ASID的分配过程:
    如下图:

    我们假设图中从A进程到D进程,有255个进程,刚好分配完了asid, ,从A到D的切换过程中使用的都是同一批次的asid版本号。

    则这个过程中,有进程会创建的时候被切换到,假设不超出255个进程,在切换过程中会为新进程分配硬件的ASID,分配完后下次切换到他时由于他的ASID版本号和当前的全局的ASID版本号相同,所以不需要再次分配ASID,当然也不需要清空tlb。

    注:这里说的ASID即为硬件ASID区别于ASID版本号。

    情况1-ASID版本号不变 属于策略(1):从C进程到D进程切换,内核判断D进程的ASID版本号和当前的全局的ASID版本号相同,所以不需要为他分配ASID(执行快速路径switch_mm_fastpath去设置ttbrx_el1))。

    情况2 -硬件ASID全部分配完 属于策略(4):假设到达D进程时,asid已经全部分配完(系统中有255个进程都分配到了硬件asid号),这个时候新创建的进程E被调度器选中,切换到E,由于新创建的进程的软件ASID被初始化为0,所以和当前的全局的ASID版本号不同(不在同一批次),则这个时候会执行new_context为进程分配ASID,但是由于没有可以分配的ASID,所以会将全局的ASID版本号加1(发生ASID回绕),这个时候全局的ASID为801,然后清空asid_map,置位tlb_flush_pending所有bit用于清空所有cpu的tlb,然后再次去分配硬件ASID给E进程,这个时候分配到了1给他(将ASID版本号)。

    情况3-ASID版本号发生变化,进程的硬件ASID可以再次使用 属于策略(3):假设从E切换到了B进程,而B进程之前已经在全局的ASID版本号为800的批次上分配了编号为5的硬件ASID,但是B进程的ASID版本号800和现在全局的ASID版本号801不相同,所有需要new_context为进程分配ASID,分配的时候发现asid_map中编号为5没有被置位,也就是没有其他进程分配了5这个ASID,所有可以继续使用原来分配的硬件ASID 5。

    情况4 - ASID版本号发生变化,有其他进程已经分配了相同的硬件ASID 属于策略(2): 假设从B进程切换到A进程,而B进程之前已经在全局的ASID版本号为800的批次上分配了编号为1的硬件ASID,但是B进程的ASID版本号800和现在全局的ASID版本号801不相同,所有需要new_context为进程分配ASID,分配的时候发现asid_map中编号为1已经被置位,也就是其他进程已经分配了1这个ASID,需要从asid_map寻找下一个空闲的ASID,则分配了新的ASID为6。

    假设从A到E,由于E的ASID版本号和全局的ASID版本号(同一批次),和情况1相同,不需要分配ASID。但是之前原来处于800这个ASID版本号批次的进程都需要重新分配ASID,有的可以使用原来的硬件ASID,有的重新分配硬件ASID,但是都将ASID版本号修改为了现在全局的ASID版本号801。但是,随着硬件ASID的不断分配,最终处于801这一批次的硬件ASID也会分配完,这个时候就是上面的情况2,要情况所有cpu的tlb。

    我可以看到有了ASID机制之后,由于只有当硬件ASID被分配完了(如被255个进程使用),发生回绕的时候才会清空所有cpu的tlb,大大提高了系统的性能(没有ASID机制的情况下每次进程切换需要地址空间切换的时候都需要清空tlb)。

     

    4. 普通用户进程、普通用户线程、内核线程切换的差别


    内核地址空间切换的时候有一下原则:看的是进程描述符的mm_struct结构,即是成员mm:

    • 1)如果mm为NULL,则表示即将切换的是内核线程,不需要切换地址空间(所有任务共享内核地址空间)。
    • 2)内核线程会借用前一个用户进程的mm,赋值到自己的active_mm(本身的mm为空),进程切换的时候就会比较前一个进程的active_mm和当前进程的mm。
    • 3)如果前一个任务的和即将切换的任务,具有相同的mm成员,也就是共享地址空间的线程则也不需要切换地址空间。

    1. ->所有的进程线程之间进行切换都需要切换处理器状态。
    2. ->对于普通的用户进程之间进行切换需要切换地址空间。
    3. ->同一个线程组中的线程之间切换不需要切换地址空间,因为他们共享相同的地址空间。
    4. -> 内核线程在上下文切换的时候不需要切换地址空间,仅仅是借用上一个进程mm_struct结构。

    有以下场景:

    • 约定:我们将进程/线程统称为任务,其中U表示用户任务(进程/线程),K表示内核线程,带有数字表示同一个线程组中的线程。
    • 有以下任务:Ua1 Ua2 Ub Uc Ka Kb (eg:Ua1为用户进程, Ua2为和Ua1在同一线程组的用户进程,Ub普通的用户进程,Ka普通的内核线程 )。

    如果调度顺序如下:

    Uc -> Ua1 -> Ua2 -> Ub -> Ka -> Kb -> Ub
    1. 从Uc -> Ua1 由于是不同的进程,需要切换地址空间。
    2. 从 Ua1 -> Ua2 由于是相同线程组中的不同线程,共享地址空间,在切换到Ua1的时候已经切换了地址空间,所有不需要切换地址空间。
    3. 从 Ua2 -> Ub 由于是不同的进程,需要切换地址空间。
    4. 从 Ub -> Ka 由于切换到内核线程,所以不需要切换地址空间。
    5. 从Ka -> Kb 俩内核线程之前切换,不需要切换地址空间。
    6. 从Kb -> Ub 从内核线程切换到用户进程,由于Ka和Kb都是借用Ub的active_mm,而Ub的active_mm 等于Ub的mm,所以这个时候 Kb的active_mm和 Ub的mm相同,所以也不会切换地址空间。

    如下为多任务地址空间切换示例图:

     

    5. 进程切换全景视图


    我们以下场景为例:

    A,B两个进程都是普通的用户进程,从进程A切换到进程B,简单起见我们在这里不考虑其他的抢占时机,我们假设A,B进程只是循环进行一些基本的运算操作,从来不调用任何系统调用,只考虑被时钟中断,返回用户空间之前被抢占的情况。

    下面给出进程切换的全景视图:

     

    视图中已经讲解很清楚,需要强调3个关键点:

    • 1.发生中断时的保存现场,将发生中断时的所有通用寄存器保存到进程的内核栈,使用struct pt_regs结构。
    • 2.地址空间切换将进程自己的页全局目录的基地址pgd保存在ttbr0_le1中,用于mmu的页表遍历的起始点。
    • 3.硬件上下文切换的时候,将此时的调用保存寄存器和pc, sp保存到struct cpu_context结构中。做好了这几个保存工作,当进程再次被调度回来的时候,通过cpu_context中保存的pc回到了cpu_switch_to的下一条指令继续执行,而由于cpu_context中保存的sp导致当前进程回到自己的内核栈,经过一系列的内核栈的出栈处理,最后将原来保存在pt_regs中的通用寄存器的值恢复到了通用寄存器,这样进程回到用户空间就可以继续沿着被中断打断的下一条指令开始执行,用户栈也回到了被打断之前的位置,而进程访问的指令数据做地址转化(VA到PA)也都是从自己的pgd开始进行,一切对用户来说就好像没有发生一样,简直天衣无缝。

     

    6. 总结


    进程管理中最重要的一步要进行进程上下文切换,其中主要有两大步骤:地址空间切换和处理器状态切换(硬件上下文切换),前者保证了进程回到用户空间之后能够访问到自己的指令和数据(其中包括减小tlb清空的ASID机制),后者保证了进程内核栈和执行流的切换,会将当前进程的硬件上下文保存在进程所管理的一块内存,然后将即将执行的进程的硬件上下文从内存中恢复到寄存器,有了这两步的切换过程保证了进程运行的有条不紊,当然切换的过程是在内核空间完成,这对于进程来说是透明的。

     

    韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。

     

    展开全文
  • 深入理解Linux内核进程上下文切换

    千次阅读 多人点赞 2020-09-28 08:39:19
    作者简介韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。我都知道...
  • 进程上下文切换 进程上下文包含了进程执行所需要的所有信息。 用户地址空间:包括程序代码,数据,用户堆栈等; 控制信息:进程描述符,内核栈等; 硬件上下文:进程恢复前,必须装入寄存器的数据统称为硬件上下文...
  • 线程上下文切换与进程上下文切换 进程切换分两步 ...这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。 另外一个隐藏的损耗是上下
  • 进程切换分两步 1.切换页目录以使用新的地址空间 2.切换内核栈和硬件上下文。 对于linux来说,线程和进程的最大区别就在于地址空间。...对于线程切换,第1步是不需要做的,第2是进程...这两种上下文切换的处理都是通
  • 文章目录1 进程上下文与中断上下文2 进程上下文切换2.1 概念2.2 步骤3 处理器模式切换3.1 概念3.2 步骤4系统任务与用户进程 1 进程上下文与中断上下文 进程上下文 所谓的“进程上下文”,就是一个进程在执行...
  • 内核控制的一些功能需要移植层提供,为了方便移植,这些API函数用宏来实现,比如上下文切换、进入和退出临界区、禁止和使能可屏蔽中断。内核控制函数还包括启动和停止调度器、挂起和恢复调度器以及用于低功耗模式的...
  • 一、进程上下文切换? 对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是...
  • Linux内核中的上下文切换

    万次阅读 2012-09-29 10:51:03
    在调度器选择新的可用的进程之后,不是马上可以执行运行这个进程,而是必须处理与多任务相关的一些环节,所有这些环节就组成了上下文切换。 在调度函数schedule中,有这样的一个片段,这个函数主要用于在就绪队列上...
  • CPU 上下文切换 多任务操作系统中,多于CPU个数的任务同时运行就需要进行任务调度,从而多个任务轮流使用CPU。 从用户角度看好像所有的任务同时在运行,实际上是多个任务你运行一会,我运行一会,任务切换的速度很快...
  • PS:上下文切换是什么意思? 上下文切换(Context Switch)有时也称做进程切换或任务切换,是指CPU 从一个进程或线程切换到另一个进程或线程。 上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。...
  • 什么是上下文切换

    千次阅读 多人点赞 2019-04-29 13:46:56
    文章目录什么是上下文切换上下文上下文切换系统调用进程上下文切换线程上下文切换中断上下文切换感谢 上下文 首先,需要讲清楚什么是上下文。 每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,这...
  • CPU上下文切换(上)

    2020-08-06 08:10:05
    CPU上下文切换:指将前一个任务的CPU上下文(CPU寄存器和程序计数器)保存起来(保存在系统内核中), 然后加载新任务的上下文到这些寄存器和程序计数器, 最后再跳转到程序计数器所值的位置,运行新任务。 什么是...
  • 线程上下文切换

    千次阅读 2018-08-29 10:05:04
    一、为什么要减少线程上下文切换  当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为...
  • 进程调度时,当被选中的next进程不是current进程时,需要进行上下文切换。 进行上下文切换时,有一些问题不太容易理解,比如: 1、进程上下文切换必然发生在内核态吗? 2、上下文切换后原来的进程(prev)如果恢复...
  • CPU上下文切换详解

    2019-11-01 10:29:29
    上下文切换
  • 中断是在同一个进程的上下文中(在中断函数中可能产生进程上下文切换) 而进程上下文切换时在两个进程之间进行切换 中断的执行过程 1.内核把当前eip esp等压到内核堆栈里面去 然后把eip的内容放到中断函数开始处...
  • CPU上下文切换

    2018-12-02 15:34:00
     CPU上下文切换包括进程上下文切换、线程上下文切换及中断上下文切换,当任务进行io或发生时间片事件及发生中断(如硬件读取完成)时,就会进入内核态,发生CPU上下文切换。 进程上下文切换,进程的上下文信息包括,...
  • 上下文切换技术

    千次阅读 2016-10-10 18:50:46
    简述 在进一步之前,让我们先回顾一下各种上下文切换技术。...当我们说“上下文切换”的时候,表达的是一种从一个上下文切换到另一个上下文执行的技术。而“调度”指的是决定哪个上下文可以获得接下去的CPU时
  • 1.首先,需要搞明白什么是上下文切换上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态。 2. 然后,需要明白进程与线程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 67,518
精华内容 27,007
关键字:

内核上下文切换