從一個精簡Linux內核分析操作系統的基本運行過程

鄭德倫 原創作品轉載請註明出處 《Linux內核分析》MOOC課程
http://mooc.study.163.com/course/USTC-1000029000

STEP1:搭建實驗環境
首先在自己的Linux系統中配置好實驗的環境,依次輸入以下的命令:

•   sudo apt-get install qemu # install QEMU
•   sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
•   wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code
•   wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch
•   xz -d linux-3.9.4.tar.xz
•   tar -xvf linux-3.9.4.tar
•   cd linux-3.9.4
•   patch -p1 < ../mykernel_for_linux3.9.4sc.patch
•   make allnoconfig
•   make

最後執行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一個進程切換的內核
經過環境的配置,Linux內核經過我們修改,在qemu窗口中我們會看到,一個只有時鐘中斷的系統。如圖所示

這裏寫圖片描述
因爲在mymian.c裏面,只有一個函數在執行,my_start_kernel(void),循環輸出一句話。

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文件裏面只有一個處理時鐘中斷的函數

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

如果我們要完成可以進行進程調度的操作系統內核,需要將github中mykernel的myinterrupt.c mymain.c mypcb.h三個文件複製到linux內核文件夾下面,然後再進行make。
首先在github上面下載3個文件,在終端中輸入如下命令:

wget https://raw.github.com/mengning/mykernel/master/mypcb.h
wget https://raw.github.com/mengning/mykernel/master/myinterrupt.c
wget https://raw.github.com/mengning/mykernel/master/mymain.c

然後將三個文件拷貝到linux內核文件夾下面的mykernel文件夾下面,在終端中輸入命令

cp myinterrupt.c linux-3.9.4/mykernel/
cp mymain.c linux-3.9.4/mykernel/
cp mypch.h linux-3.9.4/mykernel/

最後執行make重新編譯內核
然後終端中輸入qemu -kernel arch/x86/boot/bzImage就可以看到一個進程切換的內核運行了。
這裏寫圖片描述
這裏寫圖片描述
STEP3:分析代碼,理解執行過程
首先打開mypcb.h文件,有兩個結構體。
第一Thread結構體,保存一個任務的eip還有esp的信息。

struct Thread {
    unsigned long       ip;
    unsigned long       sp;
};

第二個結構體PCB,保存進程的一些信息。
pid保存一個進程唯一的一個ID號
state表示一個進程的運行狀態
stack數組表示一個進程的堆棧空間
Thread結構體記錄eip和esp堆棧棧頂的信息
task_entry表示進程的執行入口地址
struct PCB* next則指向下一個進程,用鏈式儲存結構將進程連在一起

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;

然後打開mymain.c文件
volatile int my_need_sched = 0;
my_need_sched變量表示一個任務,是否可以進行調度。
然後分析kernel開始執行的函數。

void __init my_start_kernel(void)
{
int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;/*將進程號初始化爲0*/
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
/*把進程的入口和eip都初始化爲my_process函數的入口地址*/
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
/*將堆棧的棧頂地址保存爲棧的最後一個元素,即最高的地址*/
task[pid].next = &task[pid];
/*next指針指向自己*/
/*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
/*將task[0]的信息全部複製到各個新進程中*/
        task[i].pid = i;/*將新進程的pid設置爲變量i*/
        task[i].state = -1;/*新進程的狀態爲unrunnable*/
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];/*將每個新進程的棧頂指針都指向堆棧數組的最後一個元素*/
/*使用尾插法創建鏈表形成一個任務鏈0->1->2->3->0這樣的循環*/
        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];//將當前進程設置爲0號進程
    asm volatile(
/*把0號進程的esp放入esp寄存器*/
        "movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */
/*push0號進程的esp,因爲此時棧爲空,esp==ebp,所以此處等同於push ebp*/
        "pushl %1\n\t"          /* push ebp */
/*將0號進程的eip,即process函數的入口地址push到當前堆棧*/
        "pushl %0\n\t"          /* push task[pid].thread.ip */
/*將棧頂元素pop到eip然後跳轉*/
        "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*/
    );
}

my_start_kernel完成的任務主要是初始化進程鏈表,然後開始執行第一個進程。
執行完ret操作之後將會跳轉到process函數中執行。
然後我們來分析process程序,process程序是一個無限循環,每循環100000000執行判斷生效一次,輸出KERN_NOTICE “this is process pid-,並且判斷my_need_sched是否等於1,如果等於1的話,就把my_need_sched置0並且執行my_schedule()進行調度,最後執行KERN_NOTICE “this is process pid+

void my_process(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000000 == 0)// 沒執行100000000次循環進入一次
        {
            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);
        }     
    }
}

我們再來看看myinterrupt.c文件

void my_timer_handler(void)
{
#if 1
/*函數每執行1000次,並且my_need_sched不等於1的時候執行if判斷,然後把my_need_sched置爲1*/
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
#endif
    return;     
}

分析一下函數my_timer_handler(void),#if 1 到#endif表示一個條件預編譯指令,表示永遠編譯,一般用於測試代碼中,把1改爲0相當於註釋掉代碼段,方便調試。
my_timer_handler函數每執行1000次並且my_need_sched不爲1的時候輸出一段話,並且把my_timer_handler置爲1.
到此我們就清楚了my_need_sched這個變量的變化過程(由process置0,由my_timer_handler置1,交替執行),和mymain.c裏面的process函數結合就可以分析出進程調度的詳細過程。
注:因爲時間中斷處理每隔一段時間就會進行一次,可能在0號進程執行過程中進行了多次時間中斷處理,我們暫時把沒有發生my_need_sched變量改變的時間中斷處理時間忽略掉。
進程的調度過程如下:

0號進程啓動,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行1號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行2號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行3號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行0號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
整個調度過程就如上面所示,process循環執行,等待時鐘中斷來將允許調度的變量置1,然後完成調度。
具體的調度過程我們要分析my_schedule函數:

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;
/*next進程兩種情況,運行和非運行要分情況處理*/
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
        /* 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 */
/*將下面標號1:之後的語句popl ebp的地址放入prev的ip中保存*/
            "movl $1f,%1\n\t"       /* save eip */ 
/*將next的eip push到棧上,然後執行ret的話,就會從next的eip開始執行*/
            "pushl %3\n\t" 
            "ret\n\t"               /* restore  eip */
            "1:\t"                  /* next process start here */
            "popl %%ebp\n\t"
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
        my_current_task = next; 
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);      
    }
    else
{
/*將next的狀態置爲runnable*/
        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; 
}

如果next進程爲非runnable狀態的時候執行else,runnable的狀態執行if,整個過程就是先保存當前進程的ebp和esp,然後將next進程的esp和ebp復原,並且將prev進程的eip儲存爲標號1的位置,最後將next進程的eip push到棧上,再執行ret指令,返回到next的eip所指向的地方執行。
因爲進程是用一個循環鏈表連接起來的,一直從prev向next切換,總會有一次再次切換到自己,也就是prev進程,變成next進程。如果下一次進入的時候會標號1:開始執行完成ebp的復原操作。
注:爲了便於理解我們用0號進程和1號進程表示。
這個schedule函數分兩次執行,第一次是從0號進程跳轉到1號進程,而第二次進入則是執行popl ebp恢復現場,然後return函數,返回到原調用者也就是0號進程的process裏面。
總結:
通過以上代碼的分析,我們可以初步瞭解到了,linux內核是如何完成進程的切換的。我們可以認爲一個進程相當於一個堆棧,每個進程有自己的堆棧空間。如果將ebp和esp修改爲另一個進程的ebp和esp,並且完成一些寄存器的保存,就相當於完成的進程的切換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章