此文仅用于MOOC
Linux内核分析
作业张依依+原创作品转载请注明出处 + 《Linux内核分析》**
MOOC课程**http://mooc.study.163.com/course/USTC-1000029000
进程调度
Linux的调度程序是一个叫schedule()
的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。
Linux调度时机主要有:
- 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
- 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
- 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
代码分析
关键函数的调用关系:
schedule() --> context_switch() --> switch_to --> __switch_to()
- schedule()
这里调用__schedule(),tsk为当前进程.
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
__schedule();
}
- __schedule();
该函数包含了一些:
- 针对抢占的处理
- 自旋锁(
raw_spin_lock_irq(&rq->lock);
) - 检查prev的状态,并且重设state的状态
- 进程调度算法(
next = pick_next_task(rq, prev);
) - 更新就绪队列的时钟
- 进程上下文切换(
context_switch(rq, prev, next);
)
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
...
//调度算法
next = pick_next_task(rq, prev);
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
rq->skip_clock_update = 0;
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;
//进程上下文切换
context_switch(rq, prev, next);
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}
- context_switch
在挑选得到了下一个即将被调度进来的进程之后,如果被选中的进程不是当前正在运行的进程,那么需要进行上下文切换以执行被选中的进程即context_switch
.
context_switch中包含了:
- 判断是否为内核线程,即是否需要上下文切换(
mm
)- 如果next是一个普通进程,schedule( )函数用next的地址空间替换prev的地址空间
- 如果prev是内核线程或正在退出的进程,context_switch()函数就把指向prev内存描述符的指针保存到运行队列的prev_mm字段中,然后重新设置prev->active_mm
- 切换堆栈和寄存器(
switch_to(prev, next, prev);
)
ps:宏switch_to用来进行关键上下文切换
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
arch_start_context_switch(prev);
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
context_tracking_task_switch(prev, next);
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
finish_task_switch(this_rq(), prev);
}
宏switch_to
#define switch_to(prev, next, last)
do {
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" /* save flags */
"pushl %%ebp\n\t" /* save EBP */
"movl %%esp,%[prev_sp]\n\t" /* save ESP */
"movl %[next_sp],%%esp\n\t" /* restore ESP */
"movl $1f,%[prev_ip]\n\t" /* save EIP */
"pushl %[next_ip]\n\t" /* restore EIP */
__switch_canary
"jmp __switch_to\n" /* regparm call */
"1:\t"
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
/* output parameters */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip),
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* input parameters: */
: [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),
/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
: /* reloaded segment registers */
"memory");
} while (0)
这个宏实现了进程之间的真正切换:
- 首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
- 然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
- 把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
- 将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
- 通过jmp指令(而不是call指令)转入一个函数
__switch_to()
- 恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。
内核堆栈情况:
__switch_to函数
在宏switch_to中,用jmp
跳转到该函数运行.
该函数主要进行一些针对TSS的操作,不再赘述
__visible __notrace_funcgraph struct task_struct *
__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();
struct tss_struct *tss = &per_cpu(init_tss, cpu);
fpu_switch_t fpu;
fpu = switch_fpu_prepare(prev_p, next_p, cpu);
load_sp0(tss, next);
lazy_save_gs(prev->gs);
load_TLS(next, cpu);
if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
set_iopl_mask(next->iopl);
task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);
if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
__switch_to_xtra(prev_p, next_p, tss);
arch_end_context_switch(next_p);
this_cpu_write(kernel_stack,
(unsigned long)task_stack_page(next_p) +
THREAD_SIZE - KERNEL_STACK_OFFSET);
if (prev->gs | next->gs)
lazy_load_gs(next->gs);
switch_fpu_finish(next_p, fpu);
this_cpu_write(current_task, next_p);
return prev_p;
}
GDB调试
使用MenuOS进行调试,并设置合适的断点.
- 首先在schedule处停下来:
- 查看当前进程
tsk
,观察到该进程pid=1,stack=0xC7858000
- 继续执行,到__schedule中的关键函数pick_next_task停下
- 查看队列
rq
- context_switch
- switch_to宏&__switch_to函数
- 在这里查看切换的进程prev&next,prev就是最开始tsk
总结
- Linux的调度程序是一个叫schedule()的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。
- Linux系统的一般执行过程主要在进程X切换到进程Y
- 正在运行的用户态进程X
- 发生中断
- SAVE_ALL
- 中断处理过程中或中断返回前调用了schedule()
- 开始运行用户态进程Y
- restore_all
- iret
- 继续运行用户态进程Y
- 内核线程主动调用
schedule()
,只有进程上下文的切换 - 宏
switch_to
实现了进程之间的真正切换