1、从中断开始
以fork()为例:
fork()中有中断,执行 INT 0x80时,CPU找到对应的内核栈,将 SS SP PC CS 压栈;注意ret=??处写的是INT 的下一句,也就是PC = mov res,%eax 。
切换五段论中的中断入口和出口
刚进入内核,_system_call将用户态信息压栈,这就是中断入口——建立内核栈和用户栈的关联)
movl _current,%eax //取当前任务(进程)数据结构地址%eax。
//接下来查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。
//如果该任务在就绪状态但counter[??]值(时间片)等于0,则也去执行调度程序。
cmpl $0,state(%eax) //state(%eax)= state + %eax,eax = _current,_current = PCB,其实就是判断PCB是否是 0,0 表示就绪或执行,非0表示阻塞
jne reschedule //如果是非0,就会发生调度
cmpl $0,counter(%eax) //判断counter(%eax) 是否是0,eax = _current,counter是时间片
je reschedule // 相等,调度
ret_from_sys_call: // 中断返回,执行中断返回函数,从内核栈,切换到用户栈
reschedule:
pushl $ret_from_sys_call //将ret_from_sys_call 的地址入栈,,reschedule遇到 } 出栈,弹出ret_from_sys_call
jmp _schedule //调用 schedule
这里的ret_from_sys_call就是中断出口
2、切换:schedule()
void schedule (void)
{
//找到下一个线程的TCB next,切换到下一个线程
...
switch_to (next); // 切换到任务号为next 的任务,并运行之
}
switch_to():
实际上switch_to 通过 TSS 实现切换,如下图:
TSS(Task Struct Segment),任务结构段,一个TSS中有所有寄存器。
上图黄色的是现TSS,绿的是新TSS,下边 GDT(全局描述符表Global Descriptor Table)保存的是TSS的描述符粉色的是CPU当前的寄存器段信息,TR是一个类似于CS的选择子。切换就是将 现在CPU的寄存器信息写入当前线程的TSS中,TR指向新的TSS(n) 的段描述符,在GDT表中找到新的TSS,将新的TSS段内容中所有寄存器信息(包括ESP)覆盖进 CPU。
注:之前的说法是,在切换时找到下一个线程的PCB,由PCB找到esp完成栈的切换,而这里直接覆盖更新了esp,所以实际上TSS是PCB的一个子段。同时,可以看到EIP(cpu下一条指令地址)也被更新了,所以不需要再找到esp之后弹栈eip。但是这种做法慢。
3、总结
核心代码:INT ljmp IRET
五段论:
4、sys_fork()详细
_sys_fork:
……
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process # 调用C 函数copy_process()(kernel/fork.c)。
……
作用:根据父进程,创建子进程,copy_press前将参数压栈,这些参数是父进程在用户态的样子
copy_peocess:
参数来自于system_call和sys_fork压入栈的寄存器,保存父进程用户态的样子。
第一句申请内存用作PCB
esp0是内核栈,esp是用户栈
由tss.esp=esp可知,子进程和父进程共享用户栈
此处p->tss.eax=0与下图结合
当INT 0x80结束返回时执行上面这句,产生了子进程和父进程返回值的差别(子进程的res=0,父进程≠0),所以能够让子进程运行自己的程序,如下:
if(!fork())
{
//子进程执行
} else{
//父进程执行
}
举例: shell 输入命令
int main(int argc, char * argv[])
{
while(1)
{
scanf("%s", cmd);
if(!fork())
{
exec(smd); // 执行子进程命令
}
wait(0); // 执行父进程命令,shell等待用户输入
}
}
以这个例子介绍子进程如何执行自己的代码:
exec() 是一个系统调用,会执行 system_call
_system_call:
push %ds ... %fs
pushl %edx...
call sys_execve
_sys_execve:
lea EIP(%esp),%eax
pushl %eax
call _do_execve
EIP = 0x1C是十进制的28,将%esp偏移28,由上图栈的构造可知是将eip的地址,也就是中断返回时要执行的下一句复制给eax,调用do_execve改变原本的父进程带来的eip。当子进程退出内核(通过IRET实现中断返回),回到用户态,就可以执行新的代码。
do_execve代码如下:
int do_execve(* eip, ...)
{
p += change_ldt(...;
eip[0] = ex.a_entry;// ex.a_entry是可执行程序入口地址,产生可执行文件时写入
eip[3] = p; // eip[3]=esp+0x1C+0x0C,因为一个指针4个字节,有了自己的执行代码之后也要有自己的栈
...
}
总结一下system_call的作用:
- 调用sys_fork,调用 copy_process,父进程与子进程 内核栈不同,用户栈相同
- 判断cmpl $0,state(%eax),非0表示阻塞,调用 reschedule 进程调度。reschedule 调用 schedule,schedule调用 switch_to(switch_to中ljmp实现长跳转,子进程将 TSS的内容复制到 CPU上,TSS图中粉色的部分)
- iret 内核栈出栈
子进程回到用户栈,执行的是 中断下边的一句代码:mov res, %eax ,res = %eax = 0
父进程回到用户栈,执行的也是 中断下边的一句代码:mov res, %eax,父进程 eax != 0