基于mykernel 2.0编写一个操作系统内核——Linux操作系统分析第一次作业

实验环境:

Ubuntu 16.0.4

1. 搭建基于mykernel虚拟⼀个x86-64的CPU硬件平台

    包括下载mykernel补丁、linux5.4.34内核代码、依赖库文件和qemu模拟器,给linux内核打上mykernel的补丁等等,ppt上已经有详细的命令,逐条运行即可,这里不再赘述。值得一提的是,从github上clone速度还是不太稳定,因此我将这一项目搬运到了码云上,可以:

git clone https://gitee.com/dexttter/mykernel.git

获取整个仓库。总而言之,经过以上的步骤后,我们可以使用qemu加载我们的mykernel的启动镜像:

qemu-system-x86_64 -kernel arch/x86/boot/bzImage

      运行结果:

   

      从qemu的窗口我们可以看到:my_start_kernel在持续打印,同时my_timer_handler也在周期性打印。

  

2. 分析源码

    接着上面,我们来关注下my_timer_handler和my_timer_handler这两条打印信息的源头。

    进入mykernel文件中,我们看到主要就是两个C代码文件:mymain.c、myinterrupt.c。

    mymain.c

void __init my_start_kernel(void)

 {

     int i = 0;

     while(1)

     {

         i++;

         if(i%100000 == 0)

             pr_notice("my_start_kernel here  %d \n",i); 

    }  

}

     这是我们mykernel的系统入口,一个死循环,每当计数器等于整100000时打印一条信息。

    myinterrupt.c

void my_timer_handler(void)

 {

        pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");

 }

    更为简洁,只要它被调用就打印my_timer_handler的函数。获取时钟中断、进入时钟中断linux的内核已经帮我们完成,相当于屏蔽了这些细节。因此我们只需要在这里完成我们在中断时想处理的工作即可——也就是我们要完成的进程切换。

3. 完成进程切换代码

    参照老师的范本,我们完成了以下工作:

    3.1   完成进程控制块这一数据结构的定义和基本操作——mypcb.h

struct Thread {

    unsigned long          ip;

    unsigned long          sp;

};

 

typedef struct PCB{

    int pid;

    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

    unsigned long stack[KERNEL_STACK_SIZE];

    struct Thread thread;

    unsigned long  task_entry;

    struct PCB *next;

}tPCB;
void my_schedule(void);

   ip和sp就是eip寄存器和esp寄存器,我们要完成进程的切换,也即进程栈帧的切换,实际上就是把rsp指向即将运行进程栈帧的栈顶.rbp指向栈底。

   此外,在pcb这一结构体中,我们还有pid——进程id,state——进程的三状态、task_entry——入口以及next——指向进程链表中的下一进程的指针。

    3.2   完成mymain.c

my_start_kermel函数:

int pid = 0;

    int i;

    /* Initialize process 0*/

    task[pid].pid = pid;

    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */

    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;

    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];

    task[pid].next = &task[pid];

    /*fork more process */

    for(i=1;i<MAX_TASK_NUM;i++)

    {

        memcpy(&task[i],&task[0],sizeof(tPCB));

        task[i].pid = i;

      task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]);

        task[i].next = task[i-1].next;

        task[i-1].next = &task[i];

    }

    /* start process 0 by task[0] */

    pid = 0;

    my_current_task = &task[pid];

          asm volatile(

        "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */

        "pushq %1\n\t"         /* push rbp */

        "pushq %0\n\t"         /* push task[pid].thread.ip */

        "ret\n\t"               /* pop task[pid].thread.ip to rip */

        :

        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/

          );

    它完成了:初始化0号进程:包括初始化pid,状态,入口,ip和sp等等。然后,我们fork了更多的进程,并加入进程管理链表,这是实现进程切换的前提。然后我们运行0号进程,它主要是由这段嵌入式的汇编代码实现的,我们重点关注下:

首先明确:%0指代task[pid].thread.ip,%1指代task[pid].thread.sp。

首先将sp的值赋给rsp,然后将rbp压栈(这里0号进程的栈帧为空,rsp和rbp相等),然后将0号程的ip压栈,return操作完成对eip的pop操作,使得rip = ip=myprocess,开始执行myprocess。

    my_process函数:

 当计数器my_need_sched=1时,我们运行my_schedule函数(在myinterrupt.c中,下面会提到)完成进程的切换

    3.3   完成myinterrupt.c

my_timer_handler:

void my_timer_handler(void)

{

    if(time_count%1000 == 0 && my_need_sched != 1)

    {

        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");

        my_need_sched = 1;

    }

    time_count ++ ; 

    return;   

}

    当tim_count这个变量为1000,且my_need_sched!=1时我们把my_need_sched置为1,然后会调用my_schedule,这是进程切换的核心,它也是由一段汇编代码实现的:

asm volatile(   

                "pushq %%rbp\n\t"         /* save rbp of prev */

                "movq %%rsp,%0\n\t" /* save rsp of prev */

                "movq %2,%%rsp\n\t"     /* restore  rsp of next */

                "movq $1f,%1\n\t"       /* save rip of prev */   

                "pushq %3\n\t"

                "ret\n\t"               /* restore  rip of next */

                "1:\t"                  /* next process start here */

                "popq %%rbp\n\t"

                : "=m" (prev->thread.sp),"=m" (prev->thread.ip)

                : "m" (next->thread.sp),"m" (next->thread.ip)
);

 

它完成的工作可以用上面这张图来解释:

  1. 保存push rbp,将prev进程的rbp的值压栈
  2. 把rsp的值保存到它自己的sp变量中
  3. 之后将next进程的sp变量中的值赋给rsp寄存器(完成栈帧的转换)
  4. 并保存prev进程的rip
  5. 随后压栈next进程的ip,也即$1(这里已经是在next的栈帧中操作了)
  6. 这之后return, 也就是pop next进程的rip,执行next进程也就是$1后面的语句
  7. pop rbp,将rbp指向next进程的栈底

至此,进程切换彻底完成,rbp和rsp都分别指向了next进程的栈底和栈顶

4. make后重新运行mykernel

 

 

 

  完成了进程的切换

 

posted on 2020-05-09 22:27  dextttter  阅读(304)  评论(0编辑  收藏  举报

导航