Linux switch_to()深入分析

深入分析任务切换与堆栈 by Liu Wanli    下面可以直接链接文章出处:
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=301272&fpart=all
关键字:时间中断、任务切换、堆栈、LINUX0.01

引言:

任务切换与堆栈的关系怎样?很多朋友可能不知道她们之间有什么关系,还有一些朋友可能认为他们之间不会有太大的关系(文献4)。而我认为:任务切换跟堆栈有着密切的关系!下面是我对它们之间关系进行的探讨,这里的任务切换我指的是发生时间中断时进行强制调度发生的任务切换,所以下面考虑堆栈时我是从中断开始探讨的。当然,我在进行这方面分析的时候,也愈感它们的复杂性,错误之处在所难免,望各位朋友多多指正。建议读者水平:* * *


一、时间中断。

假设一个进程在用户空间执行时(这时CPL=3),发生了时间中断。这时的中断处理过程为(文献1:P438):
1、根据中断向量号找到中断门描述符;
2、从描述符中分解出选择子、偏移量、属性字段并进行相应的特权检查;
3、根据描述符类型转入相应中断处理程序中去执行。
好象太肤浅了一些?再看看(文献1:P439图10.20):
1、选择子为空?no继续;
2、取得对应描述符;(描述符中DPL属性应该为0,文献3中断向量初始化部分)
3、存储段描述符?yes继续;
4、非一致代码段且DPL<CPL且段存在?yes继续;根据假设CPL=3,DPL=0,所以到5!
5、切换成内层堆栈!
如何切换??因为一个进程有用户空间堆栈和系统空间(也叫内核空间)堆栈,用户空间堆栈在哪儿我不管,它应该是由该进程的任务状态段TSS中SS2指定,SS0指定系统空间堆栈,它和该进程任务结构task_struct共占一页空间(见文献3:sched.c)。所以这里的切换成内层堆栈应该是将该进程的TSS中SS0的值赋给SS寄存器。
6、使RPL=0;
7、把描述符装入CS;
8、入口偏移越界?no继续;
9、EFLAG、CS、EIP入栈;呵,开始栈的改变了哟!
10、TF=0、NT=0、IF=0;这里考虑的是中断门。
11、转入处理程序。

别急,先看看现在的堆栈情况:

| 外层EIP |
| 外层CS |
| EFLAG |
| 外层ESP |
| 外层SS |
-----------
这个栈在什么地方呢?这相当重要!这是在当初切换至内层堆栈时进行的,即已经到了当前进程的系统空间堆栈,也就是跟task_struct共占一页的那个堆栈。而这里保存的就是该进程在用户空间的堆栈和代码信息,以便中断完成后恢复进程执行。

二、中断处理程序。
这里指的是时间中断。(文献3:system_call.c: timer_interrupt:)

timer_interrupt:
1. push %ds
2. push %es
3. push %fs
4. pushl %edx
5. pushl %ecx
6. pushl %ebx
7. pushl %eax
8. movl $0x10,%eax
9. mov %ax,%ds
10. mov %ax,%es
11. movl $0x17,%eax
12. mov %ax,%fs
13. incl jiffies
14. movb $0x20,%al
15. outb %al,$0x20
16. movl CS(%esp),%eax
17. andl $3,%eax
18. pushl %eax
19. call do_timer
20. andl $4,%esp
21. jmp ret_from_sys_call

1-7行为压栈操作,这是我们所关心的!16-18即是将CPL(CPL=CS&3)压栈,目的是用于do_tiemr(long cpl)函数。那么在执行到do_timer里面时的堆栈怎么样呢?看看:


|返回地址 |
-----------
| CPL |
| eax |
| ebx |
| ecx |
| edx |
| fs |
| es |
| ds |
-----------
| 外层EIP |
| 外层CS |
| EFLAG |
| 外层ESP |
| 外层SS |
-----------

上面的返回地址当然就是调用do_timer后的那条语句,即20行的andl $4,%esp语句。那么是不是do_timer函数执行完就返回到这儿呢,也是,当然要复杂得多,因为在do_timer()函数中调用了schedule()并且发生了任务切换!哎,好麻烦,也不知道什么时候才能返回到这儿来呢,还是一步一步来看吧。

三、do_timer()(文献3:sched.c: do_timer())

void do_timer(long cpl)
{
...
if ((--current->counter)>0) return;
current->counter=0;
if(!cpl)return;
schedule();
}

省略号为无关紧要的两条语句,进行进程的计时。如果时间片没有用完(counter>0)或CPL为0,不发生调度直接返回,当然这里也不是就直接返回到以前执行的进程空间,而是返回到do_timer()中,注意开始的返回地址,然后再通过iret指令从中断处理返回到进程中去。当然,根据我们的假设,这儿CPL应该为3,因为是在用户空间发生中断的。我们要从最复杂的情况来讨论这个问题。好了,就让我们进入到中心点吧,请进schedule()。

四、schedule()。 (文献3:sched.c: schedule())

void schedule(void )
{
int next;
...
switch_to(next);
}

呵,这里我又省略了几句代码,它执行的是调度算法,即从所有状态为‘运行’的进程中找出下一个要执行的进程,然后将编号赋给next。进行切换!

switch_to()是一个宏,它在(文献3: sched.h)中定义:

#define switch_to(n) { /
struct (long a,b;} __tmp; /
__asm__("cmpl %%ecx,current /n/t" /
"je 1f/n/t" /
"xchgl %%ecx, current/n/t" /
"movw %%dx, %1/n/t" /
"ljmp *%0/n/t" /
"cmpl %%ecx, %2/n/t" /
"jne 1f/n/t" /
"clts/n" /
"1:" /
::"m" (*&__tmp.a), "m" (*&__tmp.b), /
"m" (last_task_used_math),"d" _TSS(n), "c" ((long) task[n])); /
}

这是任务切换的关键代码,原理是直接通过TSS来进行任务的切换(文献1:P420)。那我就将这段关键代码逐行解说一下吧。cmpl %%ecx, current,比较任务n是不是当前进程,如果是当然就不用切换了,直接结束schedule()。xchgl %%ecx,current,current指针指向任务n的任务结构,ecx寄存器保存当前进程的任务结构指针。movw %%dx, %1, 使__tmp.b=‘GDT中第n个任务的TSS选择子’,注意_TSS(n)是求选择子的宏!ljmp *%0,这句代码就是真正的任务切换罗, AT&T语法的ljmp相当于INTEL的jmp far SECTION:OFFSET指令格式,它的绝对地址前加*号。这里引用(文献1:P420)一段话:当段间转移指令JMP所含指针的选择子指示一个可用任务状态段TSS描述符时,正常情况下就发生从当前任务到由该可用任务的切换。目标任务的入口点由目标任务TSS内的CS和EIP字段所规定的指针确定,这样的JMP指令内的偏移被丢弃。再具体的任务切换你也许得翻翻(文献1:P421),这里我只讲有关堆栈的处理,那就是把寄存器现场保存到当前任务的TSS。把通用寄存器、段寄存器、EIP及EFLAGS的当前值保存到当前的TSS中。保存的EIP的值是返回地址,指向引起任务切换指令的下一条指令;恢复目标任务的寄存器现场,根据保存在TSS中的内容恢复各通用寄存器、段寄存器、EFLAG、EIP。好了,基本概念就引用这么多,那么,刚才提到的进程马上要被切换出去了,它保存TSS中EIP是什么呢?显然,根据刚才的分析应该是cmpl %%ecx, %2这条指令。这意味着什么呢?这就是说,如果下次这个任务要被切换成运行状态时,它将从cmpl %%ecx, %2这条指令开始执行!那么,由彼任务推到此任务,也就是说我们切换至任务next时,它也是从这条指令开始执行的!于是我们进入到任务next的堆栈空间,并开始执行,但由于任务next和当前的任务有着相同的堆栈路径(这和LINUX中的内核控制路径是不是一回事呢?),所以我们还是引用当前的堆栈来继续分析。
哦,有点糊涂了,好象是。休息一下,再参考一下(文献2:上册P373)。专家也是这样说的;)
要不,我们这么理解,刚才被中断的进程发生了强制调度,且也发生了任务切换,只不过是切换到它自己,实际上不是哟。好吧,JMP成功,开始执行。

五、转折点,从schedule()返回。

cmpl %%ecx, %2;jne 1f; clts;1: 这几句是与协处理器有关,还有TS标志,我们就直接到1:吧,开始从schedule()返回,注意switch_to()是宏,它在schedule()末端。返回到哪儿去了呢?跟踪一下,看看上面的堆栈示意图,返回地址就是调用do_timer后的那条语句,
addl $4, %esp
jmp ret_from_sys_call
这儿esp加4就是把堆栈中的CPL去掉,因为我们不用了,跳转到ret_from_sys_call。哦,剩下的处理与系统调用返回共用代码。

六、ret_from_sys_call (文献3,kernel/system_call.s)
先看看我们的焦点,堆栈怎么样了呢?

| eax |
| ebx |
| ecx |
| edx |
| fs |
| es |
| ds |
-----------
| 外层EIP |
| 外层CS |
| EFLAG |
| 外层ESP |
| 外层SS |
-----------

ret_from_sys_call:
movel current, %eax
cmpl task, %eax
je 3f
movl CS(%esp), %ebx
testl $3, %ebx
je 3f
cmpw $0x17, OLDSS(%esp)
jne 3f
2:
....
3:
popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret

2标号处我省略了一些有关信号及其它一些处理。让我们分析一下,如果当前任务是0号进程,或是任务先前的CPL为3(即用户态),或是任务先前的堆栈段为LDT中指定的堆栈,JMP到3标号处。由先前的假设可知,此任务的CPL为3,那就跳吧。把eax, ebc, ecx, edx, fs, es, ds寄存器从堆栈中恢复出来。
现在堆栈如下:

| 外层EIP |
| 外层CS |
| EFLAG |
| 外层ESP |
| 外层SS |
-----------
记得我们还有最后一条语句哟,iret。这条指令大家想必已经很熟悉了,它恢复EIP、CS、EFLAG、ESP、SS。记得不,这是不是已经恢复到了最初的时间中断时进程被中断的那一刻?恭喜!你终于可以继续做你需要做的事情了!小心,还有下一个时间中断,哦,你不怕?因为它不会影响你的连贯性。

你可能感兴趣的:(Linux switch_to()深入分析)