基于mykernel2.0 编写一个操作系统内核

一、实验要求

  1. 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;

  2. 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码

  3. 简要分析操作系统内核核心功能及运行工作机制

二、实验环境

发行版本:Deepin 15.11

内核版本:Linux 4.15.0-30deepin-generic x86_64

三、实验步骤

1. 运行mykernel2.0

首先下载Linux源码及mykernel patch文件,并通过patch命令在Linux源码之上修改生成mykernel。

接下来使用默认选项完成内核编译构建,并在qemu虚拟环境中启动内核。

可以看到内核成功启动,并在终端上周期性打印信息。下面从源码角度对该行为进行分析。

从前述执行patch命令的输出信息不难看出,核心逻辑代码位于mymain.c和myinterrupt.c两个源文件中。

mymain.c中定义了一个内核启动函数,该函数会循环打印"my_start_kernel here"

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

那么函数my_start_kernel何时被调用的呢?在Linux内核的启动代码源文件main.c中,可以看到它在内核启动函数start_kernel被调用。

asmlinkage __visible void __init start_kernel(void)
{
    // ...
​
    acpi_subsystem_init();
    arch_post_acpi_subsys_init();
    sfi_init_late();
​
        my_start_kernel();
    /* Do the rest non-__init'ed, we're now alive */
    arch_call_rest_init();
}

myinterrupt.c中定义了一个定时器句柄函数,其在定时器中断时执行,并打印信息。

/*
 * called by timer interrupt.
 */
void my_timer_handler(void)
{
    pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}

该句柄函数是如何被注册的呢?在time.c源文件中,可以看到my_timer_handler是在Linux定时器中断函数timer_interrupt中被调用的。

/*
 * Default timer interrupt handler for PIT/HPET
 */
static irqreturn_t timer_interrupt(int irq, void *dev_id)
{
    global_clock_event->event_handler(global_clock_event);
        my_timer_handler();
    return IRQ_HANDLED;
}

至此,我们明白了mykernel在启动后打印的两种控制台信息的由来。

2. 实现进程调度

在上述mykernel框架的基础上,我们来为其添加进程调度的功能。首先,我们需要定义表示进程的数据结构,用于存储进程的信息和资源;其次,我们需要实现调度函数,这是进程调度的核心逻辑;除此之外我们还需要在内核启动时完成进程调度的初始化。这分别涉及到三个源文件:mypcb.h,myinterrupt.c以及mymain.c。下面依次分析。

首先是数据结构的定义,对于一个进程所涉及的资源和控制信息,我们将其统一置于结构体PCB中。主要字段为进程id、进程状态、函数调用栈、代码入口以及ip、sp等。我们还可以把ip、sp进一步抽象为Thread结构体。每一个PCB都是链表中的一个节点,故还需要一个next字段,这样多个进程可以链接成为进程队列。

/* CPU-specific state of this task */
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];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long   task_entry;
    struct PCB *next;
}tPCB;

my_schedule函数是实现进程调度及上下文切换的核心。

对于进程的调度,我们可以简单地选择进程链表中的下一个就绪进程。

我们重点关注如何实现两个进程间上下文的切换。上下文切换的核心是代码执行流的切换,二与代码执行流的控制相关的主要有三个寄存器:ip、sp以及bp,它们分别指示执行流的下一条指令、栈顶以及栈底的位置,后两者构成指令执行时的栈帧上下文。当然在切换过程中,不仅要载入新进程的执行流,还需要保存原进程执行流的快照。

以进程A切换到进程B为例,大致流程如下:

  1. 保存进程A当前的bp值

  2. 保存进程A当前的sp值

  3. 更新寄存器sp为进程B的sp值

  4. 保存进程A的ip值

  5. 更新寄存器ip为进程B的ip值

至此控制流完成跳变,指令继续向下执行。有几处关键点需要稍加说明:

  1. 进程A的bp保存在自身的栈中;而其sp和ip都保存在PCB中

  2. 保存进程A的ip值时,保存的是下一条指令的位置,即进程A下次被调度时执行的第一条指令。而这条指令所做的就是从自身的栈中还原bp值。对于进程B来说亦是如此。

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;
​
    if(my_current_task == NULL 
        || my_current_task->next == NULL)
    {
        return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<\n");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {        
        my_current_task = next; 
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
        /* switch to next process */
        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)
        ); 
    }  
    return; 
}

最后,我们需要在内核启动时进行初始化一些进程。在原有的my_start_kernel内添加相关代码:

对于每个进程,分别设置pid、状态、各自的栈空间,将代码入口置为函数my_process地址。各进程PCB之间构成环形链表。从pid为0的进程开始执行,设置ip、sp,bp,其中bp = sp表示当前执行栈为空。

而函数my_process则在一个循环中周期性地调用my_schedule。

void __init my_start_kernel(void)
{
    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*/
    );
} 
​
int i = 0;
​
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);
        }     
    }
}

将上述三个源文件移动到Linux源码下的mykernel目录下替换原来的文件。再次编译并在虚拟机中启动,可以看到进程调度成功运行。

四、实验总结

本次实验通过一个简单的进程调度程序的实现,让我们了解到操作系统如何通过软硬件通力合作完成多任务的并发执行,从而一窥多任务操作系统的核心实现原理,为后续学习打下良好基础。

posted @ 2020-05-10 18:54  smarxdray  阅读(151)  评论(0编辑  收藏  举报