首页
社区
课程
招聘
schedule()
发表于: 4天前 950

schedule()

4天前
950

以前认真读过linux-2.4.0内核的schedule()代码,几年过去,有些迷糊了。

它主要包含三方面实现:

1. 调度策略:SCHED_FIFO、SCHED_RR、SCHED_OTHER;

2. 切换进程空间:switch_mm(),修改cr3寄存器,指向目标进程目录表;

3. 切换进程执行:switch_to()


1和2都好理解,本文专注switch_to(),它最梦幻。

#define switch_to(prev,next,last) do {					\
	asm volatile("pushl %%esi\n\t"					\
		     "pushl %%edi\n\t"					\
		     "pushl %%ebp\n\t"					\
		     "movl %%esp,%0\n\t"	/* save ESP */		\
		     "movl %3,%%esp\n\t"	/* restore ESP */	\
		     "movl $1f,%1\n\t"		/* save EIP */		\
		     "pushl %4\n\t"		/* restore EIP */	\
		     "jmp __switch_to\n"				\
		     "1:\t"						\
		     "popl %%ebp\n\t"					\
		     "popl %%edi\n\t"					\
		     "popl %%esi\n\t"					\
		     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),	\
		      "=b" (last)					\
		     :"m" (next->thread.esp),"m" (next->thread.eip),	\
		      "a" (prev), "d" (next),				\
		      "b" (prev));					\
} while (0)


假设一段时间之前,进程A执行schedule()让出了CPU,并调度了进程B执行,接着进程B又调度进程C执行,...,当前执行schedule()的是进程P,它根据所有进程的调度策略和相应数据,比较权重,选择了进程A为调度目标。

那么switch_to()的执行过程会是这样:

    需要注意的是,"movl %3, %%esp"之前的指令,只会影响进程P的内核栈和task_struct内容,对应左下方的演示,而右下方跟着动,只是为了说明,进程A当初切换到进程B时,也对自己的内核栈和task_struct,做过相应操作。


1. "movl $1f, %1"指令,"%1"对应输出部"=m" (prev->thread.eip),这里的prev,是从哪个进程(P/A)的栈里取的?

    定格在这条指令刚执行之前,栈内容是这样:

    此时,ESP已经切换到进程A的栈区域了,EBP仍然指向进程P的schedule()执行栈帧。

这样看来,反汇编一下内核文件(vmlinux),看看schedule()函数中的这条指令,是根据EBP还是ESP获取prev的,就能证实了。不过都不是,因为输入部中的"=m" (prev->thread.eip),会让gcc在指令部之前,添加额外的指令,将prev放入某个寄存器,并且访问prev->thread.eip的指令,都被编译成从该寄存器获取事先保存的prev值,而不是到了执行这条指令时,才根据EBP或ESP从栈里读取。

    所以,进程P执行"movl $1f, %1"指令,实际上是将1:标号处的代码地址,保存到进程P自己的thread.eip。相应的,进程A当初执行switch_to()时,也是如此


观察以下test.c程序的编译结果,可以验证上述内容(为了看得更清晰,修剪了switch_to()的部分代码):

#include <stdio.h>


struct task_struct {
    struct {
        unsigned long esp;
        unsigned long eip;
    } thread;
};

void __switch_to(struct task_struct *prev, struct task_struct *next)
{
    (void)prev;
    (void)next;
}

#define switch_to(prev,next,last) do { \
    asm volatile("pushl %%ebp\n\t" \
          "movl $1f,%0\n\t"        \
          "pushl %2\n\t"           \
          "jmp __switch_to\n"      \
          "1:\t"                   \
          "popl %%ebp\n\t"         \
          :"=m" (prev->thread.eip), "=b" (last) \
          :"m" (next->thread.eip), "a" (prev), "d" (next), "b" (prev)); \
} while (0)


int main()
{
    struct task_struct t1 = { {0, 0} };
    struct task_struct t2 = { {0, 0} };
    struct task_struct *prev = &t1;
    struct task_struct *next = &t2;

    switch_to(prev, next, prev);
    printf("%p\n", prev);

    return 0;
}

由于我用的是x86_64环境,所以加-m32编译:

gcc -m32 -S -O0 -o test.s test.c

查看test.s文件内容,分析编译结果:


其实,如果直接拿switch_to()的原始代码编译,会报错:

原因:输出和输入部提出的约束,显式和隐式占用的寄存器数量,超过了CPU可提供范围。

解决:使用-O2参数编译(gcc自己灵活安排,不用完全按照原始约束行事,但是保证效果一样),或者改进switch_to(),想办法让约束少占用些寄存器。

以下代码可以验证,随便删除一个约束("b" (last)除外),就可以用-O0编译了:

#define switch_to(prev,next,last) do {                  \
    asm volatile("nop\n\t"                  \
             :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \
              "=b" (last)                   \
             :"m" (next->thread.esp),"m" (next->thread.eip),    \
              "a" (prev), "d" (next), "b" (prev));                  \
} while (0)


2. 传给__schedule_tail()函数的prev,是从哪个进程(P/A)的栈里取的?

asmlinkage void schedule(void)
{
        ...

	switch_to(prev, next, prev);
	__schedule_tail(prev);

	...
}

一方面,__schedule_tail(prev)不在内联汇编里面,会被老老实实编译成从栈上获取prev(见test.c给printf()传递prev参数,编译结果就是"pushl -52(%ebp)");

另一方面,switch_to()执行完毕时,EBP也一定切换到了进程A的内核栈:

    根据以上两方面事实,传给__schedule_tail()函数的prev,就只能是从进程A的栈里取的了,代表进程A自己,但是这不符合__schedule_tail()的内部逻辑,仅通过"prev->has_cpu = 0"就可以推断,prev应该传"已经让出CPU的进程P"才对。

那真相就只有一个了:switch_to()修改了进程A内核栈中的prev。看代码也确实是这样:

    输入部设置ebx=进程P的prev(test.s,65行),输出部设置了进程A的last=ebx(test.s,76~77行),而last是个宏参数,实际传的是prev(switch_to()调用语句),此时已经是进程A的prev。


3. __switch_to()参数怎么传的?

    __switch_to()要求传入prev和next参数:

extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));

    回头看一下switch_to()代码,它并不是"call __switch_to",而是"jmp __switch_to",并且前面执行了一条"pushl %4"指令,用于将进程A的thread.eip,压入栈中作为__switch_to()的返回地址,这样__switch_to()执行完毕,就可以返回到进程A的thread.eip代码,后续再根据进程A的内核栈、task_struct等信息,恢复它的进程上下文,从而最终达到进程切换的目的。但是,再上一条压栈指令是"pushl %%ebp",而ebp保存的显然不是next,这就说明,__switch_to()的参数,不是通过栈传递的。


答案在于FASTCALL:

#ifdef __i386__
#define FASTCALL(x)	x __attribute__((regparm(3)))
#else
#define FASTCALL(x)	x
#endif

展开后发现regparm(3)修饰符,它表示前3个参数,通过EAX、EDX、ECX寄存器传递,不过__switch_to()只有2个参数,并且EAX、EDX都在switch_to()的输入部设置好了(test.s,63~64行)。


4. 既然所有进程执行switch_to()之后,都会让自己的thread.eip,指向1:标号处,为什么还要将"call __switch_to",拆分成"pushl %4"+"jmp __switch_to"两条指令?

    这是考虑目标进程刚创建,首次被调度的情况,这样的进程,之前并没有调用过schedule(),也就没有执行过switch_to(),将自己的thread.eip设置为这里的1:标号处,而是设置为ret_from_fork(详见fork()函数,本文不再展开)。


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

最后于 1天前 被jmpcall编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回