__schedule()是主调度器的核心函数,其作用是让调度器选择和切换到一个合适进程运行。调度的时机可分为如下3种:
a、阻塞操作:互斥量(mutex)、信号量(semaphore)、等待队列(waitqueue)等
b、在中断返回前和系统调用返回用户空间时,去检查TIF_NEED_RESCHED标志位以判断是否需要调度
1)、内核态触发中断返回前夕的调度时机:
1 __irq_svc: 2 svc_entry 3 irq_handler 4 5 #ifdef CONFIG_PREEMPT 6 get_thread_info tsk 7 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count 8 ldr r0, [tsk, #TI_FLAGS] @ get flags 9 teq r8, #0 @ if preempt count != 0 10 movne r0, #0 @ force flags to 0 11 tst r0, #_TIF_NEED_RESCHED 12 blne svc_preempt //调用关系:svc_preempt->preempt_schedule_irq->__schedule() 13 #endif 14 15 svc_exit r5 @ return from exception 16 UNWIND(.fnend ) 17 ENDPROC(__irq_svc)
2)、用户态触发中断返回用户空间前夕的调度时机:
1 __irq_usr: 2 usr_entry 3 kuser_cmpxchg_check 4 irq_handler 5 get_thread_info tsk 6 mov why, #0 7 b ret_to_user_from_irq //调用关系:ret_to_user_from_irq->work_pending->work_resched->schedule 8 UNWIND(.fnend ) 9 ENDPROC(__irq_usr)
3)、系统调用返回用户空间的时机
用户空间总是将系统调用号放进r7当中,然后内核会通过调用swi指令将系统切换到内核态,然后将r7中的系统编号取出放入scno中
系统调用号在文件arch\arm\include\asm\unistd.h中,此调用号其实就是系统调用表(数组)的下标。所以系统调用号也对应于系统调用表中所在的数字项。特别注意:系统调用号17之类,此系统调用已经弃用,但为了兼容性及不至于日后混乱,所以调用号不能重用,只能空着(跳过)
系统调用表sys_call_table表在文件arch\arm\kernel\calls.S中。特别注意:系统调用号17之类对应的表项,对于已经弃用的系统调用,linux系统统一赋予sys_ni_syscall()系统调用。
系统调用的声明在文件include\linux\syscalls.h中,注意:源代码中不能直接找到sys_***的实现代码,因为64位系统的BUG,所以源代码中的系统调用sys_***,都用SYSCALL_DEFINE(***)封装了一层,以解决BUG。SYSCALL_DEFINE的定义也在文件include\linux\syscalls.h中。
1 ENTRY(vector_swi) 2 sub sp, sp, #S_FRAME_SIZE 3 stmia sp, {r0 - r12} @ Calling r0 - r12 4 ARM( add r8, sp, #S_PC ) 5 ARM( stmdb r8, {sp, lr}^ ) @ Calling sp, lr 6 THUMB( mov r8, sp ) 7 THUMB( store_user_sp_lr r8, r10, S_SP ) @ calling sp, lr 8 mrs r8, spsr @ called from non-FIQ mode, so ok. 9 str lr, [sp, #S_PC] @ Save calling PC 10 str r8, [sp, #S_PSR] @ Save CPSR 11 str r0, [sp, #S_OLD_R0] @ Save OLD_R0 12 zero_fp 13 14 15 ldr ip, __cr_alignment 16 ldr ip, [ip] 17 mcr p15, 0, ip, c1, c0 @ update control register 18 #endif 19 enable_irq 20 21 get_thread_info tsk 22 adr tbl, sys_call_table @ load syscall table pointer //获取系统调用的基指针 23 24 25 ldr r10, [tsk, #TI_FLAGS] @ check for syscall tracing 26 stmdb sp!, {r4, r5} @ push fifth and sixth args 27 28 tst r10, #_TIF_SYSCALL_WORK @ are we tracing syscalls? 29 bne __sys_trace 30 31 cmp scno, #NR_syscalls @ check upper syscall limit 32 adr lr, BSYM(ret_fast_syscall) @ return address //return address系统调用结束后的返回函数,恢复寄存器,把系统调用接收后要调用的函数赋值给寄存器R14 33 ldrcc pc, [tbl, scno, lsl #2] @ call sys_* routine //将调用号逻辑左移2位,意思就是系统调用的基地址+系统调用号相对于基地址的偏移量就是对应的系统调用的函数,执行系统调用 34 35 add r1, sp, #S_OFF 36 2: mov why, #0 @ no longer a real syscall 37 cmp scno, #(__ARM_NR_BASE - __NR_SYSCALL_BASE) 38 eor r0, scno, #__NR_SYSCALL_BASE @ put OS number back 39 bcs arm_syscall 40 b sys_ni_syscall @ not private func //bx R14,系统调用结束后的调度时机:ret_fast_syscall->fast_work_pending->work_pending->work_resched 41 ENDPROC(vector_swi)
增加一个新的系统调用函数sys_mytestcall的方法:
1)、修正/arch/arm/include/asm/unistd.h中的syscall数量
由#define __NR_syscalls(388)修改成#define __NR_syscalls(392),注意不是加1,而是加4,这个主要是因为padding对齐的原因
2)、将要自定义的系统调用函数sys_mytestcall指针填入arch/arm/kernel/calls.S:CALL(sys_mytestcall)
3)、同时在arch/arm/include/asm/unistd.h也增加自定义函数的系统调用号#define __NR_mysyscall (__NR_SYSCALL_BASE+388)(自己验证的时候这个步骤不添加也可以正常运行)
参考链接:https://blog.csdn.net/21cnbao/article/details/51295955 https://www.cnblogs.com/sky-heaven/p/8080885.html
c、将要被唤醒的进程(Wakups)不会马上调用schedule()要求被调度,而是会被添加到CFS就绪队列中,并且设置TIF_NEED_RESCHED标志位。那么唤醒进程什么时候被调度呢?这要根据内核是否具有抢占功能(CONFIG_PREEMPT=y)分两种情况。
如果内核可抢占,则:
1)、如果唤醒动作发生在系统调用或者异常处理上下文中,在下一次调用preempt_enable()时会检查是否需要抢占调度
2)、如果唤醒动作发生在硬中断处理上下文中,硬件中断处理返回前夕会检查是否要抢占当前进程
如果内核不可抢占,则:
1)、当前进程调用cond_resched()时会检查是否要调度
2)、主动调度调用schedule();
3)、系统调用或者异常处理返回用户空间时
4)、中断处理完成返回用户空间时
1 static void __sched __schedule(void) 2 { 3 struct task_struct *prev, *next; 4 unsigned long *switch_count; 5 struct rq *rq; 6 int cpu; 7 8 preempt_disable(); 9 cpu = smp_processor_id(); 10 rq = cpu_rq(cpu); //获取当前cpu的就绪队列,即对应的runqueues全局变量 11 rcu_note_context_switch(); 12 prev = rq->curr; 13 14 schedule_debug(prev); 15 16 if (sched_feat(HRTICK)) 17 hrtick_clear(rq); 18 19 smp_mb_before_spinlock(); 20 raw_spin_lock_irq(&rq->lock); 21 22 rq->clock_skip_update <<= 1; 23 24 switch_count = &prev->nivcsw; 25 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { //prev指当前进程,preempt_count成员用于判断当前进程是否可以被抢占,preempt_count低8位用于存放抢占计数 26 if (unlikely(signal_pending_state(prev->state, prev))) { 27 prev->state = TASK_RUNNING; 28 } else { 29 deactivate_task(rq, prev, DEQUEUE_SLEEP); 30 prev->on_rq = 0; 31 } 32 switch_count = &prev->nvcsw; 33 } 34 35 next = pick_next_task(rq,prev); //让进程从就绪队列中选择一个最合适的进程next,然后context_swithc()切换到next进程进行 36 clear_tsk_need_resched(prev); 37 clear_preempt_need_resched(); 38 rq->clock_skip_update = 0; 39 40 if (likely(prev != next)) { 41 rq->nr_switches++; 42 rq->curr = next; 43 ++*switch_count; 44 45 rq = context_switch(rq, prev, next); /* unlocks the rq */ 46 cpu = cpu_of(rq); 47 } else 48 raw_spin_unlock_irq(&rq->lock); 49 50 post_schedule(rq); 51 52 sched_preempt_enable_no_resched(); 53 }
preempt_count中还有一个比特位用于PREEMPT_ACTIVE,它只有在内核抢占调度中会被置位,详解如下preempt_schedule()函数:
【preempt_scheudle()->preempt_schedule_common()】:
1 static void __sched notrace preempt_schedule_common(void) 2 { 3 do { 4 _preempt_count_add(PREEMPT_ACTIVE); 5 __schedule(); 6 _preempt_count_sub(PREEMPT_ACTIVE); 7 barrier(); 8 } while (need_resched()); 9 }
第25行代码中的判断语句基于以下两种情况来考虑:
1)、把不处于正在运行状态下的当前进程清除出就绪队列。TASK_RUNNING的状态值为0,其他状态值都非0。
2)、中断返回前夕的抢占调度的情况。
如果当前进程在之前发生过抢占调度preempt_schedule(),那么在preempt_schedule()->__schedule()时它不应该被清除出运行队列。至于为何做这样的判断?下面以睡眠等待函数wait_event()为例,当前进程调用wait_event函数,当条件(condition)不满足时,就会把当前进程加入到睡眠等待队列wq中,然后schedule()调度其他进程直到满足condition。
wait_event()函数等价于如下代码片段:
1 #define __wait_event(wq, condition) \ 2 do { \ 3 DEFINE_WAIT(__wait); \ 4 \ 5 for (;;) { \ 6 wait->private = current; \ 7 list_add(&_wait->taks_list,&wq->task_list); \ 8 set_current_state(TASK_UNINTERRUPTIBLE); \ 9 if (condition) \ 10 break; \ 11 schedule(); \ 12 } \ 13 set_current_state(TASK_RUNNING); \ 14 list_del_init(&_wait->task_list); \ 15 } while (0)
有如下两种情况需要考虑:
1)、进程p在for循环中等待condition条件发生,另外一个进程A设置condition条件来唤醒进程p,假设系统中只触发一次condition条件。第8行代码设置当前进程p的状态为TASK_UNINTERRUPTIBLE之后发生了一个中断,并且中断处理返回前夕判断当前进程p是可抢占的。如果当前进程p没有置位PREEMPT_ACTIVE,那么根据__schedule()函数中的判断逻辑,当前进程会被清除出运行队列。如果此后再也没有进程来唤醒进程p,那么进程p再也没有机会被唤醒了。
2)、若进程p在添加到唤醒队列之前发生了中断,即在第6行和第7行代码之间发生了中断,中断处理返回前夕进程p被抢占调度。若preempt_count中没有置位PREEMPT_ACTIVE,那么当前进程会被清除出运行队列,由于还没有添加到唤醒队列中,因此进程p再也回不来了。
下面继续看__schedule()函数中的pick_next_task()函数:
1 static inline struct task_struct * 2 pick_next_task(struct rq *rq,struct task_struct *prev) 3 { 4 const struct sched_class *class = &fair_sched_class; 5 struct task_struct *p; 6 7 /* 8 * Optimization: we know that if all tasks are in 9 * the fair class we can call that function directly: 10 */ 11 if (likely(prev->sched_class == class && 12 rq->nr_running == rq->cfs.h_nr_running)) { 13 p = fair_sched_class.pick_next_task(rq,prev); 14 if (unlikely(p == RETRY_TASK)) 15 goto again; 16 if(unlikely(!p)) 17 p = idle_sched_class.pick_next_task(rq,prev); //如果CFS就绪队列上没有进程,则选择idle进程 18 return p; 19 } 20 again: 21 for_each_class(class) { 22 p = class->pick_next_task(rq,prev); 23 if (p){ 24 if(unlikely(p == RETRY_TASK)) 25 goto again; 26 return p; 27 } 28 } 29 30 BUG(); /* the idle class will always have a runnable task */ 31 }
pick_next_task()调用调度类中的pick_next_task()方法。第11~19行代码中有个小的优化,如果当前进程prev的调度类是CFS,并且该CPU整个就绪队列rq中的进程数量等于CFS就绪队列中进程数量,那么说明该CPU就绪队列中只有普通进程没有其他调度类;否则需要遍历整个调度类。调度类的优先级为stop_sched_class->dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class。stop_sched_class类用于关闭CPU,接下来是dl_sched_class和rt_sched_class类,它们是实时性进程,所以当系统有实时进程时,它们总是优先执行。
next进程选择好之后,下面继续看进程是如何切换的,这部分内容涉及ARM体系结构。
【__schedule()->context_switch()】:
1 static inline struct rq * 2 context_switch(struct rq *rq, struct task_struct *prev, 3 struct task_struct *next) //rq表示进程切换所在的就绪队列,prev指将要被换出的进程,next指将要被换入执行的过程 4 { 5 struct mm_struct *mm, *oldmm; 6 7 prepare_task_switch(rq, prev, next); //设置next进程的task_struct结构中的on_cpu成员为1,表示next进程马上进入执行状态。on_cpu成员会在Mutex和读写信号量的自旋等待机制中作用 8 mm = next->mm; 9 oldmm = prev->active_mm; 10 11 if (!mm) { 12 next->active_mm = oldmm; //mm为NULL表明是个内核线程,需要借用一个进程的地址空间,因此有了active_mm成员,不借用mm的原因是有可能prev进程也有可能是一个内核线程 13 atomic_inc(&oldmm->mm_count); //增加prev->active_mm的mm_count的引用计数,保证“债主”不会释放mm,递减引用计数详见第27行代码 14 enter_lazy_tlb(oldmm, next); //进入lazy tlb模式,对于ARM处理器来说这是一个空函数 15 } else 16 switch_mm(oldmm, mm, next); //对于普通进程,需要调用switch_mm函数来做一些进程地址空间切换的处理 17 18 if (!prev->mm) { //对于prev进程也是一个内核线程的情况,prev进程马上要换出,因此设置prev->active_mm为NULL 19 prev->active_mm = NULL; 20 rq->prev_mm = oldmm; //就绪队列rq的成员prev_mm记录了prev->active_mm的值 21 } 22 23 /* Here we just switch the register state and the stack. */ 24 switch_to(prev, next, prev); //从prev进程切换到next进程运行,该函数执行完成时,CPU执行next进程,prev进程被调度出去,俗称“睡眠” 25 barrier(); 26 27 return finish_task_switch(prev); //递减mm_count的引用计数,同时设置prev进程的task_struct数据结构的on_cpu成员为0,表示prev进程已经退出执行状态,相当于由next进程来收拾prev进程的“残局” 28 }
总而言之,switch_to()函数是新旧进程的切换点。所有进程在受到调度时的切入点都在switch_to()函数中,即完成next进程堆栈切换后开始执行next进程。next进程一直运行,直到下一次执行switch_to()函数,并且把next进程的堆栈保存在硬件上下文为止。特殊情况是新创建的进程,其第一次执行的切入点是在copy_thread()函数中指定的ret_from_fork汇编函数
标签:__,调用,rq,schedule,next,linux,进程,prev From: https://www.cnblogs.com/penglcool/p/9895375.html