Linux内核分析

mykernel内核代码深度剖析

2017-02-23  本文已影响82人  athorn

陈松 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

mykernel原生代码的编译过程以及运行截图:



另外,若出现无法关闭qemu的情况,可用下面这条命令:
ps -A | grep qemu | awk '{print $1}' | xargs sudo kill -9

原生代码中mymain.c的核心部分,Linux内核其实也是一个死循环

void __init my_start_kernel(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000 == 0)
            printk(KERN_NOTICE "my_start_kernel here  %d \n",i);
            
    }
}

myinterrupt.c的核心部分,这个应该是时钟中断的中断处理函数

/*
 * Called by timer interrupt.
 */
void my_timer_handler(void)
{
    printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}

可以看到这两段代码都是比较简单的,只是简单的打印几句话而已。

至于内核是如何运行起来的,硬件是如何初始化的,my_start_kernel之前的代码都帮我们处理好了。在接下来的学习中,希望自己能搞明白从启动到内核运行起来的流程。

按照实验的要求,是希望我们在这两个文件的基础上实现一个简单的操作系统内核:

运行mykernel后就可以写一个自己的时间片轮转调度内核了

目前自己还未能完全理解这所有的机制,所以是把实例代码拷贝到自己的实验环境中去,然后重新make,观察这一整套的运行过程,其次的深入理解才是最重要的。

在实验楼的环境里面,我只是把mymain.c、myinterrupt.c更改为git上的代码,然后增加了mypcb.c,make运行之后,等待bzImage的生成。

实验截图如下:

需要学习的是:patch的生成以及如何打一个补丁。
可以参考以下链接:Linux打Patch的方法补丁(patch)的制作与应用

如果之前接触过其他嵌入式系统的内核,会比较好理解这个内核的运行过程。

/* 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 */

    char stack[KERNEL_STACK_SIZE];

    /* CPU-specific state of this task */

    struct Thread thread;

    unsigned long   task_entry;

    struct PCB *next;

}tPCB;

 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;

    //一般是向下增长,故而sp指向最大的地址
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];

    //只有一个任务的时候next指针只能指向自己
    task[pid].next = &task[pid];

当前期的软硬件环境已经准备妥当之后,程序在/init/main.c中运行到my_start_kernel,这时内核开始初始化,pid = 0。

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

        //相当于有一个任务链表(单向循环链表),把所有的任务串起来,从0到MAX_TASK_NUM
        task[i].next = task[i-1].next;

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

    }

根据我们系统的需要,对需要的MAX_TASK_NUM个任务进行初始化,实际上,这就相当于初始化出来一个任务链表(如果是双向链表的话,会不会更好?),只是暂时没有实际分配任务的入口地址。

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

    pid = 0;

    my_current_task = &task[pid];

    asm volatile(

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

        "pushl %1\n\t"          /* push ebp */

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

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

        "popl %%ebp\n\t"

        : 

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

    );
void my_process(void)

{

    int i = 0;

    while(1)

    {

        i++;
        //10000000次循环之后才检查是否有任务需要调度
        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);

        }     

    }
    next = my_current_task->next;

    prev = my_current_task;

至于什么是当前任务的上下文,我的理解就是,任务的堆栈状态以及当前相关的硬件寄存器(esp,eip,eflags等这些),有解释如下:

当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够返回到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。

所以在设计任务tcb的时候,就考虑到这些问题了,为每个任务保存了esp,eip,state,stack空间等等。
由于我们采用的调度算法是:如果下一个可执行,那么调度到下一个。真正执行任务切换的时候,需要将当前任务的esp到堆栈中去(保存ebp似乎没看到必要性?如果没有必要性的话,最后的"popl %%ebp\n\t"似乎也没有必要,这样的话,代码中的"1:\t"只是起到了一个路引的作用,当被调度的任务返回的时候,返回到此处,继续执行被中断的任务),保存eip(当前任务要运行的下一条指令)到tcb中的ip变量中去(似乎没必要直接用到ip变量,可以直接保存在堆栈中),然后恢复要运行任务的上下文(恢复next thread的esp,同样迂回的办法恢复eip指针,并直接运行),任务调度完成,执行被调度的任务。

值得注意的是,之前所有的假设,都值得尝试,配置到自己的代码中,试运行。

经过尝试,果然,不保存ebp是没问题的。

else中代码就不需要分析了,因为在整个内核的逻辑中,除了初始化外,我们就没有维护到state的值,所以else根本执行不到。

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; //prev指针当然指向当前的任务控制块

    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(   

            "pushl %%ebp\n\t"       /* save ebp */

            "movl %%esp,%0\n\t"     /* save esp */

            "movl %2,%%esp\n\t"     /* restore  esp */

            "movl $1f,%1\n\t"       /* save eip */  

            "pushl %3\n\t" 

            "ret\n\t"               /* restore  eip */

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

            "popl %%ebp\n\t"
                //            %0             %1
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
                //            %2             %3
            : "m" (next->thread.sp),"m" (next->thread.ip)

        ); 

    

    }

    else

    {

        next->state = 0;

        my_current_task = next;

        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);

        /* switch to new process */

        asm volatile(   

            "pushl %%ebp\n\t"       /* save ebp */

            "movl %%esp,%0\n\t"     /* save esp */

            "movl %2,%%esp\n\t"     /* restore  esp */

            "movl %2,%%ebp\n\t"     /* restore  ebp */

            "movl $1f,%1\n\t"       /* save eip */  

            "pushl %3\n\t" 

            "ret\n\t"               /* restore  eip */

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

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

        );          

    }   

    return; 

}

总结:在这所有的代码中,其中的三段汇编代码才是理解的核心,这里面的机制,为何要保存这些寄存器,保存寄存器的顺序,如何恢复一个新的任务,这些机制搞清楚的话,对其他的代码理解应该帮助特别大。

上一篇 下一篇

猜你喜欢

热点阅读