基于mykernel 2.0编写一个操作系统内核

实验基于https://github.com/mengning/mykernel完成

一.配置虚拟机QEMU

安装过程不再阐述,参考上方链接即可

为了使得qemu能够正常进行debug,需要设置相关内核选项

# 打开debug相关选项

# 关闭KASLR,否则会导致打断点失败 Processor type and features ---->

 

之后终端输入make,编译。

接下来配置内存根文件系统,需要用到busybox。

首先,取消busybox的动态链接,然后编译,将编译后_install下的文件,以及dev目录下的文件打包制作根文件系统。

由于默认的内核命令行上有 init=/linuxrc, 因此,在文件系统被挂载后,运行的第一个程序是根目录下的 linuxrc。 这是一个指向/bin/busybox 的链接,也就是说,系统起来后运行的

第一个程序也就是 busybox 本身。

启动qemu,

二,代码具体分析

首先定义进程控制块PCB

PCB主要包含下面几部分的内容:

1. 进程的描述信息,比如进程的名称,pid,

2. 处理机的状态信息,当程序中断是保留此时的信息,以便CPU返回时能从断点执行

3. 进程调度信息,比如进程状态,优先级等等

4. 进程控制和资源占用,同步通信机制,链接指针(指向队列中下一个进程的PCB地址)

 

 

 

 1 typedef struct PCB{
 2     int pid;//进程id
 3     //进程状态
 4     volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
 5     char stack[KERNEL_STACK_SIZE];//每个进程都有自己独立的栈空间
 6     /* CPU-specific state of this task */
 7     struct Thread thread;//线程
 8     unsigned long   task_entry;//函数入口地址
 9     struct PCB *next;//下一个进程控制块
10 }tPCB;

接下来定义线程

struct Thread {
    unsigned long       ip;//指向的是函数地址
    unsigned long       sp;//指向栈底
};

由线程的定义可见,线程自己不拥有自己的地址空间,它使用的是进程的栈,也就是线程和进程共享数据。

 

接下来初始化所有的pcb,每个进程的pcb的入口地址,以及线程的ip,其值都是my_process函数的地址。

int pid = 0;//0号进程
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    //任务入口,即my_process函数的地址
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//把my_process函数的地址赋给了ip
    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].state = -1;
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];//sp指向栈底
        task[i].next = task[i-1].next;//当前的next指向上一个,上一个的next指向当前。最后的pcb的next指向第0个pcb,形成环形
        task[i-1].next = &task[i];
    }

 

接下来,准备进程切换/调用

栈帧调整: 
1.将调用者的ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置; push ebp
2.将当前栈帧切换到新栈帧(将ebp值装入esp,更新栈帧底部), 这时ebp指向栈顶,而此时栈顶就是old esp ,mov esp, ebp
3.之后将my_process的地址入栈,ret执行后rip保存my_process的地址,之后就会进入这个函数
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*/
    );

 

 

 

进程切换的过程与上面的过程类似,都是先保存当前栈底rbp,这是为了之后的返回,

接着调整把当前线程thread的ip,和sp保存到pcb中

然后下一个pcb的ip,即函数地址,压栈,因为rip无法直接操作

然后rsp指向了新的堆栈的栈顶

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)
        ); 

为了清楚知道rsp,rbp的变化情况,根据vscode的调试,方便起见,把stack的size调整到8*8

一开始,4个pcb的stack全都是空,那么在运行上述代码的时候,当rsp指向next的stack栈顶时,stack全空,如图

 

那么当执行popq %%rbp的时候, rsp-8的位置,也就是本该存放rbp的位置,全0,那么在弹栈的时候,rbp的值应该也为全0

为了验证rbp的值到底是多少,修改popq %2,也就是弹栈到next->thread.sp

 

不知道什么原因,therad.sp的值在前后并未发生改变。。。

本来以为popq可能没有执行,但是将popq %%rbp删除后,第一次循环正常运行,当再次循环到0号pcb的时候就发生错误了

 但是可以看到新堆栈的值已经改变

 

后来查询,在vscode调试控制台输入-exec info registers可以直接查询寄存器的值

在进入进程切换前

 

rbp=0xffffffff82b57b00,rsp=0xffffffff82b5bb3f

切成切换后:

 

 

 rsp的值确确实实被改变了,但是,rbp的值并没有变化(疑惑🤔),并不知道为什么会这样。

在一开始,四个pcb中的stack全都是空的,当准备切换到下一个进程时,rsp指向了下一个进程stack的栈顶,rsp+8的位置理应保存的是rbp的值,但是此时栈是空的,如果rbp值变化了,那么他的值应该是全0,然而rbp并没有变。

 

接下来:

中断在cpu中扮演者重要的角色,cpu每隔一定的时间就会自动检查是否又中断信号,例如每次敲击键盘都会产生一次中断

 在qemu初始化时,就产生了一个中断,每次中断就会调用中断处理函数

通过调试,可以看到time.c中调用了自定义的处理函数

 

 

 

在这个中断处理函数中,会把my_need_sched改为1

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;      
}

接着在init函数中的my_process检测到my_need_sched=1时,就会进行进程切换,也就是上面的那段汇编代码

void my_process(void)
{    
    while(1)
    {
        i++;
        if(i%10000000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
            my_schedule();
            }
            printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

 

 四个pcb会循环执行,中断处理定期改变my_need_sched的值为1,进程切换时再改变为0

 

 

 三,总结

通过断点调试kernel,了解了中断的产生以及中断在计算机中扮演的重要角色,最主要的是明白如何通过栈指针的调整(rbp,rip,调整rsp),完成进程之前的切换。

posted @ 2020-05-10 18:21  刹那很好  阅读(240)  评论(0编辑  收藏  举报