基于内核栈切换的进程切换
tss:
linux0.11中主要是基于TSS方式的切换,通过switch_to函数来实现,在shcdule()函数中,给switch_to函数传递一个next参数,这个参数为下一个进程在进程队列中的索引值
while (1) {
c = -1;
next = 0;l
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
这里的p是一个二级指针,也就是一个队列的开头,while循环主要是进行队列中的counter最大值的查找,也就是将counter为最大的进程进行执行,当找到counter了就退出,当没有找到就进行其他的判断标准,所以next的产生就是在这里进行的。
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
"je 1f\n\t" \
"movw %%dx,%1\n\t" \
"xchgl %%ecx,current\n\t" \
"ljmp *%0\n\t" \
"cmpl %%ecx,last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
这是在sched.h中对switch_to的宏替换,主要功能就是当传入的参数对应的进程和current不是同一个就进行tss段和ldt段的切换
栈切换:
我们需要将tss方式转化为栈切换方式,需要下一个进程的pcb表项,也就是*p的值,同时因为传输的不是next了,但是switch_to中还需要LDT的切换,所以我们需要进行next的LDT的计算和传输 * p给switch_to函数。
struct task_struct *pnext;
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i,pnext=*p;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(pnext,5 * 8 + next * 16);
}
pnext指向的就是下一个进程pcb的地址,switch_to的第二个参数就是下一个进程的ldt的位置
上图为一个进程的tss和ldt位置对应关系
在tss模式下,使用了宏定义的方式在sched.h中定义了TSS(int next),用来获得next的tss的位置。我们现在需要注释sched.h中的switch_to,在sched.c中重写写一个switch_to函数
实验给出了switch_to函数的大概模板
变量说明:
- struct tss_struct *tss = &(init_task.task.tss)
- kernel_stack,在pcb中增加定义的一个long变量,存放的是内核栈的位置,在sched.h中,因为增加了一个变量,所以当我们定义init_task时,我们知道代码使用的时一个宏定义来赋值,所以我们在INIT_TASK这个宏定义中添加kernel_stack的值
#define INIT_TASK \
/* state etc */ { 0,15,15, /*添加的kernel_stack值 也就是init_tack的pcb页最后一个字节*/PAGE_SIZE+(long)&init_task\
/* signals */ 0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
switch_to:
pushl %ebp
movl %esp,%ebp
pushl %ecx
pushl %ebx
pushl %eax
movl 8(%ebp),%ebx
cmpl %ebx,current
je 1f //取出一个参数,放入ebx中,和current比较,当相同就结束switch_to函数
! 切换PCB
mov %ebx,%eax //ebx为next的地址
xchgl %eax,current //交换将current写入到eax,eax写入到current
! TSS中的内核栈指针的重写
movl tss,%ecx
addl $4096,%ebx
movl %ebx,ESP0(%ecx)
//tss+4得到的就是esp0的地址,将ebx+4096表示的是pcb+4096表示一个页的页尾。ESP0被宏定义为4
//next的tss变为0长度
//这样的作用,每次开始运行一个进程之前,将内核栈长度置为0,
//当下一个这个进程进行了系统调用int之后,就直接在内核栈为0的位置开始第一轮的压栈ss,esp,eflag,cs,ip,
//但是这对于内核栈中的数据并不影响,因为真正的内核栈地址在kernel_stack中,我们在后面只需要将esp换为kernel_stack就可以了。
//可以知道的是,0号进程的tss属于当前正在运行的进程,只有当这个进程被调度离开,
//这个结构再交给下一个进程,而这个tss中的esp0的值也就是当前进程被中断时,栈开始的位置,当压完栈后,
//将tss交给下一个线程,然后使用kernel_stack来存储当前线程的栈顶位置。
! 切换内核栈
movl %esp,KERNEL_STACK(%eax)
! 再取一下 ebx,因为前面修改过 ebx 的值
//当前栈顶存放到current的kernel_stack中
//KERNEL_STACK为宏定义的kernel_stack在pcb中的位置。
movl 8(%ebp),%ebx
//next的开始地址
movl KERNEL_STACK(%ebx),%esp
//esp换为next的esp
! 切换LDT
movl $0x17,%ecx
mov %cx,%fs
! 和后面的 clts 配合来处理协处理器,由于和主题关系不大,此处不做论述
cmpl %eax,last_task_used_math
jne 1f
clts
1: popl %eax
popl %ebx
popl %ecx
popl %ebp
ret
上述代码中主要实现的就是
- 切换PCB
- TSS中的内核栈指针的重写
- 切换内核栈
- 切换LDT
这四个工作
tss为一个定义的全局变量,struct tss_struct *tss = &(init_task.task.tss),指向的是init_task.task.tss的地址,当计算机开机,第一次执行到这里,创建了第一个进程,tss就执行了这个进程的task.tss的位置,之后便不再变化了,tss一直指向这个位置,用于当current进程被中断了,就将值保存
全局变量tss指向的就是当前进程的tss信息,虽然我们不使用tss方式来切换进程了,但是我们还是需要tss的这种机制来保存当前进程的信息,因为intel的机制,int指令会去tr中寻找tss的esp0来作为这个进程的内核栈的栈顶,从这个地方开始存放数据,我们在这个将current的tss的esp0位置置为这个pcb的最后一个字节,也就是栈顶为最后一个字节,那么当这个current下一次被中断,tss中存放的就仍然是栈顶的位置。
栈结构:
高地址--------------------------------------------------------------------------------->低地址
SS,ESP,EFLAGS,CS,EIP,DS,ES,FS,EDX,ECX,EBX,EAX,ret_from_sys_call
高地址--------------------------------------------------------------------------------->低地址
SS-EIP的值就是int指令将值push到栈中的,DS-EBX就是system_call函数开始时push进去的,EAX是执行完sys_call_table之后得到的返回值
当程序执行到这里,我们进行判断,当当前运行进程状态为阻塞或者时钟不足,就调用reschedule函数,push一个ret_from_sys_call,这是一个地址,执行这个函数就会退出system_call函数,这个地址的被pop的位置在当我们执行了schdule(),schdule的"}"会作为ret指令,将eip置为ret_from_sys_call在这里插入代码片
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
我们可以看到ret_from_system_call中的后面部分,进行push和iret,但执行完就正好达到了栈平衡
fork():
当我们进行创建进程的时候,执行fork函数,执行的主要函数是copy_process():
long *krnstack;
p = (struct task_struct *) get_free_page(); //申请一个页
krnstack = (long)(PAGE_SIZE +(long)p); //内核栈位置
*(--krnstack) = ss & 0xffff;
*(--krnstack) = esp;
*(--krnstack) = eflags;
*(--krnstack) = cs & 0xffff;
*(--krnstack) = eip;
*(--krnstack) = ds & 0xffff;
*(--krnstack) = es & 0xffff;
*(--krnstack) = fs & 0xffff;
*(--krnstack) = gs & 0xffff;
*(--krnstack) = esi;
*(--krnstack) = edi;
*(--krnstack) = edx;
*(--krnstack) = (long)first_return_from_kernel;
*(--krnstack) = ebp;
*(--krnstack) = ecx;
*(--krnstack) = ebx;
*(--krnstack) = 0;
p->kernelstack = krnstack;
......
我们需要在这个函数中为新增的进程完成内核栈的初始化,也就是按照父进程的内核栈,仿造一个新的内核栈。之后加入到就绪队列中。当下一次轮到这个进程执行,在switch_to函数中弹出0,ebx,ecx,ebp后schdule的"}"弹到first_return_from_kernel执行。
然后使用一个fisrst_return_from_kernel
popl %edx
popl %edi
popl %esi
pop %gs
pop %fs
pop %es
pop %ds
iret