理解进程调度时机跟踪分析进程调度与进程切换的过程

1.Linux系统中进程调度的时机,schedule()函数

进程调度的时机主要有以下几种情况:

1).进程状态的转换,如运行态进入终止,睡眠态,进程通过调用函数exit(),sleep()等函数,再通过进程调度程序schedule()完成进程状态的转换。

2).当前进程的时间片结束(current->counter=0)

3).设备驱动程序,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。

4).进程从中断、异常及系统调用返回到用户态时,不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测。

schedule():进程调度函数,由它来完成进程的调度。通过检查检测 need_resched 标志,如果此标志为非0,那么就转到reschedule处调用调度程序schedule()进行进程的选择。调度程序schedule()会根据具体的标准在运行队列中选择下一个应该运行的进程。当从调度程序返回时,如果发现又有调度标志被设置,则又调用调度程序,直到调度标志为0,这时,从调度程序返回时由RESTORE_ALL恢复被选定进程的环境,返回到被选定进程的用户空间,使之得到运行。schedule()函数被定义在usr/include/linux/sched.h中,它的实现代码如下,

5asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;  //获取当前进程的状态
	sched_submit_work(tsk);  //用以提交记录当前进程的状态。
	__schedule(); //开始调度
}
EXPORT_SYMBOL(schedule);
调度算法在__shedule()函数中,通过检查need_resched标志位,判断是否继续执行。此时会进行大量的工作,比如获取进程的优先级等。其中 调用context_switch进行上下文的切换是关键步骤,context_switch通过调用宏switch_to进行上下文切换工作。 进程调度算法封装函数如下:

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:
	 */
	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 */
		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);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}
	BUG(); /* the idle class will always have a runnable task */
}

处理流程next = pick_next_task(rq, prev);->context_switch(rq, prev, next)->switch_to

2.分析switch_to中的汇编代码,进程上下文的切换机制

进程上下文切换:context_switch(rq, prev, next),它通过switch_to用了prev和next两个参数:prev指向当前进程,next指向被调度的进程。
#define switch_to(prev, next, last)					\
32do {									\
33	/*								\
34	 * Context-switching clobbers all registers, so we clobber	\
35	 * them explicitly, via unused output variables.		\
36	 * (EAX and EBP is not listed because EBP is saved/restored	\
37	 * explicitly for wchan access and EAX is the return value of	\
38	 * __switch_to())						\
39	 */								\
40	unsigned long ebx, ecx, edx, esi, edi;				\
41									\
42	asm volatile("pushfl\n\t"		/* save    flags */	\  //保护标志位
43		     "pushl %%ebp\n\t"		/* save    EBP   */	\  //栈基址
44		     "movl %%esp,%[prev_sp]\n\t"	/* save    ESP   */ \
45		     "movl %[next_sp],%%esp\n\t"	/* restore ESP   */ \
46		     "movl $1f,%[prev_ip]\n\t"	/* save    EIP   */	\
47		     "pushl %[next_ip]\n\t"	/* restore EIP   */	\
48		     __switch_canary					\
49		     "jmp __switch_to\n"	/* regparm call  */	\
50		     "1:\t"						\
51		     "popl %%ebp\n\t"		/* restore EBP   */	\
52		     "popfl\n"			/* restore flags */	\
53									\
54		     /* output parameters */				\
55		     : [prev_sp] "=m" (prev->thread.sp),		\
56		       [prev_ip] "=m" (prev->thread.ip),		\
57		       "=a" (last),					\
58									\
59		       /* clobbered output registers: */		\  //各个寄存器的值
60		       "=b" (ebx), "=c" (ecx), "=d" (edx),		\
61		       "=S" (esi), "=D" (edi)				\
62		       							\
63		       __switch_canary_oparam				\
64									\
65		       /* input parameters: */				\
66		     : [next_sp]  "m" (next->thread.sp),		\
67		       [next_ip]  "m" (next->thread.ip),		\
68		       							\
69		       /* regparm parameters for __switch_to(): */	\
70		       [prev]     "a" (prev),				\  //当前进程
71		       [next]     "d" (next)				\  //下一条进程
72									\
73		       __switch_canary_iparam				\
74									\
75		     : /* reloaded segment registers */			\
76			"memory");					\
77} while (0)
78
79#else /* CONFIG_X86_32 */
宏switch_to()内部做了很多压栈的工作,目的是保护现场,以待后续进程恢复现场restore_all。 context_switch()调用switch_to()执行prev和next之间的进程切换了:switch_to(prev,next,prev); return prev;

 进程切换后schedule()所执行的操作

schedule( ) 函数中在switch_to 宏之后紧接着的指令并不由next 进程立即执行,而是稍后当调度程序选择prev 又执行时由prev 执行。

最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程


  1. 正在运行的用户态进程X

  2. 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

  3. SAVE_ALL //保存现场

  4. 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

  5. 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

  6. restore_all //恢复现场
  7. iret - pop cs:eip/ss:esp/eflags from kernel stack

  8. 继续运行用户态进程Y

我们把需要调度的进程统称为Y进程。

3.使用gdb跟踪分析一个schedule()函数

1).重新克隆一份menu文件,打开调试窗口,如下图:


2)..打开gdb调试窗口,在schedule处设置断点:


3.)在pick_next_task处设置断点



由于switch_to是宏而不是函数,因此在gdb中不能追踪到,但是它的执行过程是清晰地。

张伟+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值