核心级线程实现实例(李治军操作系统课笔记5)

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值