mykernel内核代码深度剖析

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;

  • 上面这段是一个任务控制块应该有的一些关键变量,如:任务运行时的sp指向,eip所指向的命令的地址,还有任务的id号,任务状态,任务堆栈以及真正的任务入口。
  • 当然,作为一个可调度的内核,肯定要有一个双向任务链表,自然就会有next和prev指针。
    但是目前这个实现的内核中,并没有prev指针,应该还是有改善的余地的。
 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。

  • 首先是维护出进程的各种属性,如pid号,运行状态(个人理解这个变量指示的是该任务是否已经可以运行),以及这个进程的真正的运行代码入口地址(此内核中,0号idle任务的入口函数自然就是my_process)
  • 当然还少不了每个进程都需要的堆栈所需的堆栈指针(这里面有个KERNEL_STACK_SIZE,应该是在配置文件中预先定义好的,当然,具体的等下还要查tPCB结构体的定义)。最开始的时候,只有一个任务,所以task的next指针只能指向自己。
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*/

    );
  • 这一大段的汇编代码,实际上模拟的正是任务栈帧形成的过程,因为%1代表的是第2个参数,即把当前要运行的任务的sp指针((unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];)恢复到CPU的寄存器esp中,然后把sp压栈(一般每个独立的栈都会有自己的栈帧结构,即ebp和esp范围内的),故相当于ebp = esp。在函数调用的时候,最开始时的pushl %ebp; movl %esp, %ebp;也是同样的道理。
  • 把初始化时候的eip(task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;)压栈,之后迅速ret弹栈到eip(因为x86不允许直接修改eip指针,只好采用这种迂回策略),到此pid=0的线程开始运行。
  • 由于任务是不允许退出的,最后一条popl的指令永远不会执行。
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);

        }     

    }
  • myprocess是一段比较简单的代码,纯粹根据myinterrupt中的tick次数打印出消息,以及判断是否需要调度。

  • my_timer_handler则是系统的中断处理函数irqreturn_t timer_interrupt调用的,该函数对系统tick次数,计数次数到了,置位调度标志变量。

  • 调度代码的核心在于my_schedule,之前讲到,my_need_sched == 1的时候,my_schedule会被调用,执行一次调度。调度的算法有很多,但是这个就比较简单,仅仅是在有任务可调度的情况下,调度下一个任务。

    next = my_current_task->next;

    prev = my_current_task;
  • 如果下一个任务控制块可用,且能运行(当然能运行了,因为我们在初始化tcb链表的时候,可以把state 初始化为 0,因为state似乎目前未体现出价值),则开始进行一次上下文切换。
  • 首先保存当前任务的上下文(各种寄存器、堆栈等),然后恢复需要调度的任务到CPU的执行上下文环境中。

至于什么是当前任务的上下文,我的理解就是,任务的堆栈状态以及当前相关的硬件寄存器(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; 

}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容

  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,758评论 0 27
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,077评论 0 23
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,573评论 1 19
  • 我珍惜这来之不易的一切,哪怕是睁眼眨眼之后看到的一片蓝蓝的天,这一地绿油油的草坪。 我很高兴自己在能珍惜的...
    兜兜喜欢夏天喜欢雨天阅读 96评论 0 0
  • 学英语是长盛不衰的话题。经常看那些广告 什么 “21天听懂VOA” “30天练一口流利的英语” 这些都是广告的幌子...
    TomTan阅读 944评论 6 14