1. 程式人生 > >2018-2019-1 20189229《Linux內核原理與分析》第三周作業

2018-2019-1 20189229《Linux內核原理與分析》第三周作業

復制 rup cif har 時鐘 阻塞 內核 保存 進入

一. mykernel實驗指導(操作系統是如何工作的)

運行並分析一個精簡的操作系統內核,理解操作系統是如何工作的
使用實驗樓的虛擬機打開shell

  • 1.cd LinuxKernel/linux-3.9.4
  • 2.qemu -kernel arch/x86/boot/bzImage
    然後cd mykernel 您可以看到qemu窗口輸出的內容的代碼mymain.c和myinterrupt.c
    使用自己的Linux系統環境搭建過程參見mykernel,其中也可以找到一個簡單的時間片輪轉多道程序內核代碼。

二. 開始第一部分實驗:

進入實驗樓虛擬機後,打開mymain.c文件,可以看到其中只有如下這一個函數。它作為內核啟動的起始位置,從這個函數開始運行,並無限循環。

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文件,裏面也只有一個my_timer_handler(),它被Linux內核周期性調用,從而產生了一個周期性的中斷機制。

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

在終端中輸入如下命令:
技術分享圖片
可以看到初始的內核運行情況如下:
技術分享圖片
內核不停的執行my_start_kernel(),每隔一段時間被my_timer_handler()中斷,然後執行一條打印語句:printk(KERN_NOTICE “\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n”);後,又回到my_start_kernel()繼續執行。

三.第二部分實驗:修改內核代碼,使之成為一個簡單的時間片輪轉多道程序內核,然後重新編譯運行。

從https://github.com/mengning/mykernel上下載mypcb.h;mymain.c;myinterrupt.c;然後替換位於home/shiyanlou/LinuxKernel/linux-3.9.4/mykernel/中的mymain.c;myinterrupt.c;將mypcb.h也放在這裏。 接下來在shell中將當前工作目錄退回到home/shiyanlou/LinuxKernel/linux-3.9.4/ 然後執行make,重新編譯內核。效果如下:
技術分享圖片
技術分享圖片
然後再次輸入:qemu -kernel arch/x86/boot/bzImage 啟動內核。
技術分享圖片
可以看到新的內核運行效果如下:
技術分享圖片
從上圖中可以清晰看到進程的切換過程:在0號進程運行過程中,先是my_timer_handler()被執行,然後是myschedule()被執行,myschedule()在運行過程中會打印switch 0(被切換出去的進程號) to 1(切換到的進程號)。然後就跳到新的進程1繼續執行。
技術分享圖片

四.mykernel內核源代碼分析:在關鍵地方都加了註釋。

4.1 mypcb.h

/*
 *  linux/mykernel/mypcb.h
 *
 *  Kernel internal PCB types
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#define MAX_TASK_NUM        4    //最大進程數,這裏設置為了4個。
#define KERNEL_STACK_SIZE   1024*8  //每個進程的內核棧的大小。

/* CPU-specific state of this task */
struct Thread {
    unsigned long       ip;//用於保存進程的eip
    unsigned long       sp;//用戶保存進程的esp
};

typedef struct PCB{
    int pid;//進程的id號
    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;

void my_schedule(void);

4.2 mymain.c

/*
 *  linux/mykernel/mymain.c
 *
 *  Kernel internal my_start_kernel
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>


#include "mypcb.h"

tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;

void my_process(void);


void __init my_start_kernel(void)
{
    int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;//task[0].pid=0;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;//令0號進程的入口地址為my_process();
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];//0號進程的棧頂為stack[]數組的最後一個元素
    task[pid].next = &task[pid];//next指針指向自己
    /*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)//根據0號進程,復制出幾個只是編號不同的進程
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));//void *memcpy(void *dest, const void *src, size_t n);從源src所指的內存地址的起始位置開始拷貝n個字節到目標dest所指的內存地址的起始位置中。
        task[i].pid = i;
        task[i].state = -1;//這些進程的狀態都設置為未運行。
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
        task[i].next = task[i-1].next;//新創建的進程的next指向0號進程的首地址
        task[i-1].next = &task[i];//前一個進程的next指向最新創建的進程的首地址,從而成為一個循環鏈表。
    }
    /* start process 0 by task[0] */
    pid = 0;
    my_current_task = &task[pid];//當前運行的進程設置為0號進程。
    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++;
        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);//打印+號
        }     
    }
}

4.3 myinterrupt.c

/*
 *  linux/mykernel/myinterrupt.c
 *
 *  Kernel internal my_timer_handler
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
#if 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;     
}

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;//將下一個將要運行的進程設置為my_current_task->next指向的下一個進程。
    prev = my_current_task;//將當前進程設置為prev進程。
    if(next->state == 0)/*如果下一個將要運行的進程已經處於運行狀態 -1 unrunnable, 0 runnable, >0 stopped */
    {
        /* switch to next process */
        asm volatile(   
            "pushl %%ebp\n\t"       /* 保存當前進程的ebp到自己的棧中。    save ebp */
            "movl %%esp,%0\n\t"     /* 保存當前進程的esp到自己的棧中。    save esp */
            "movl %2,%%esp\n\t"     /* 從next->thread.sp中彈出下一個進程的esp。與第二句相對應。   restore  esp */
            "movl $1f,%1\n\t"       /* 將下一個進程的eip設置為1f。$1f就是指標號1:的代碼在內存中存儲的地址  save eip */   
            "pushl %3\n\t"          /* 將next->thread.ip壓入當前進程的棧中。*/
            "ret\n\t"               /* 從當前進程的棧中彈出剛剛壓入的next->thread.ip。完成進程切換。  restore  eip */
            "1:\t"                  /* 即$1f指向的位置。next process start here */
            "popl %%ebp\n\t"        /* 切換到的進程把ebp從棧中彈出至ebp寄存器。與第一句相對應。*/
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
        my_current_task = next; //當前進程切換為next
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); //打印切換信息     
    }
    else//如果下一個將要運行的進程還從未運行過。
    {
        next->state = 0;//將其設置為運行狀態。
        my_current_task = next;////當前進程切換為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"       /* 將要被切換出去的進程的ip設置為$1f。這樣等一下它被切換回來時(一定是運行狀態)肯定會進入if判斷分支,可以從if中的標號1處繼續執行。  save eip */    
            "pushl %3\n\t"          /* 將next->thread.ip(因為它還沒有被運行過,所以next->thread.ip現在仍處於初始狀態,即指向my_process(),壓入將要被切換出去的進程的堆棧。*/
            "ret\n\t"               /* 將剛剛壓入的next->thread.ip出棧至eip,完成進程切換。   restore  eip */
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        );          
    }   
    return; 
}

五.分析進程的啟動和進程的切換機制。

首先,內核啟動__init my_start_kernel(void),創建了4個進程,分別是0,1,2,3號,設置0號為運行態,其它3個進程為未運行態。0號進程的入口都被初始化為task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;即指向my_process()
0號進程的棧頂被初始化為 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];之後的進程也都是根據0號進程復制得到,所以它們的起始入口也是my_process(),初始棧頂也是指向了自己的stack[KERNEL_STACK_SIZE-1];
my_current_task = &task[pid];將當前進程設置為0號進程。然後從0號進程開始運行。
“movl %1,%%esp\n\t”
將0號進程的棧頂放入esp寄存器。
“pushl %1\n\t” /* push ebp */
當前esp指向了stack數組的末尾,這裏也是棧頂,因為棧是空的,所以esp==ebp
“pushl %0\n\t " /* push task[pid].thread.ip */
“ret\n\t” /* pop task[pid].thread.ip to eip */
切換到0號進程的入口地址開始執行。
“popl %%ebp\n\t”
這句多余,在ret之後,不會被執行。

:
: “c” (task[pid].thread.ip),”d” (task[pid].thread.sp)

之後0號進程不斷執行my_process()。一段時間後,my_timer_handler()被內核調用,觸發中斷,my_need_sched = 1;將全局變量my_need_sched 設置為了1。此後,當0號進程執行到了if(my_need_sched == 1)時就會進入這個if條件分支中,執行my_schedule();執行進程調度。

0號進程的next指針指向的是1號進程,所以在my_schedule()中的next指針指向了1號進程,prev指針指向了0號進程。
因為1號進程當前還未被運行過,所以會執行else條件分支:next->state = 0;//將1號進程設置為運行狀態
my_current_task = next;//當前進程切換為1號進程printk(KERN_NOTICE “>>>switch %d to %d<<<\n”,prev->pid,next->pid);//打印switch 0 to 1

“pushl %%ebp\n\t”       /* save ebp */ 
“movl %%esp,%0\n\t”     /* save esp */ 

將0號進程的ebp和esp都保存到0號進程的棧上。

“movl %2,%%esp\n\t”     /* restore  esp */ 
“movl %2,%%ebp\n\t”     /* restore  ebp */ 

將1號進程的存在1號進程結構體中next->thread.sp保存的esp的值存入esp寄存器和ebp寄存器,因為1號進程還未被運行過,所以esp仍指向了1號棧的stack KERNEL_STACK_SIZE-1]
“movl $1f, %1\n\t” 將0號進程的eip設置為if。
“pushl %3\n\t”
“ret\n\t”
將1號進程的eip加入0號進程的棧中,然後通過ret指令,將這個eip從0號進程的棧中彈出,存入eip寄存器,完成從0號進程到1號進程的切換。接下來1到2,2到3號進程的切換也是這樣。 然後是3號進程切換到0號進程,因為0號進程最開始已經運行過,所以已經處於運行態,在執行到my_schedule()中的if(next->state == 0)時,會進入此if條件分支。本次具體分析見代碼後的註釋。

asm volatile(   
            "pushl %%ebp\n\t"    /* 保存3號進程的ebp到3號的棧中。*/
            "movl %%esp,%0\n\t"     /* 保存3號進程的esp到3號的棧中。*/
            "movl %2,%%esp\n\t"     /* 從next->thread.sp中彈出0號進程的esp。與第二句相對應。*/
            "movl $1f,%1\n\t"     /* 將0號進程的eip設置為1f。$1f就是指標號1:的代碼在內存中存儲的地址。*/ 
            "pushl %3\n\t"     /* 將next->thread.ip壓入3號進程的棧中。*/
            "ret\n\t"               /* 從3號進程的棧中彈出剛剛壓入的next->thread.ip。完成進程切換。*/
            "1:\t"     /* 即$1f指向的位置。next process start here */
            "popl %%ebp\n\t"        /* 切換到的進程(這一次是0號進程)把ebp從棧中彈出至ebp寄存器。與第一句相對應。*/
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        ); 
        my_current_task = next; //當前進程切換為0號進程。      
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); //打印切換信息 switch 3 to 0 

接下來從0到1,1到2,2到3······都是執行這個if條件分支。

第二周課程的難點分析(來自《Linux內核分析》課程團隊):
理解和運行mykernel,它是提供初始化好的CPU從my_start_kernel開始執行,並提供了時鐘中斷機制周期性性執行my_time_handler中斷處理程序,執行完後中斷返回總是可以回到my_start_kernel中斷的位置繼續執行。當然中斷保存現場恢復現場的細節都處理好了,mykernel就是一個邏輯上的硬件平臺,具體怎麽做到的一般不必深究。

能運行mykernel後就可以寫一個自己的時間片輪轉調度內核了,自己寫還是很難的,只需到mykernel的github版本庫找到代碼復制過來重新編譯Linux3.9.4的源代碼,能按視頻的效果跑起來,這都不難。

難點是理解基於mykernel實現的時間片輪轉調度代碼。

往往系統都有很多進程比較復雜,我們假定當前系統只有兩個進程0和1,第一次調度是從0切換到1,也就是prev=0,next=1,第二次調度正好相反。

這時再看https://github.com/mengning/mykernel/blob/master/myinterrupt.c 中的匯編代碼,保存prev的進程(0)上下文,下次調度是next進程就是0了,反之進程1是next那它肯定之前作為prev被調度出去過。理解進程上下文的保存和恢復極為關鍵。

$1f就是指標號1:的代碼在內存中存儲的地址

再來看特殊一點代碼切換到一個新的進程,也就是next沒有被保存過進程上下文,它從沒有被執行過,這時稍特殊一點即else部分的匯編代碼。

六.自己對“操作系統是如何工作的”理解。

操作系統的內核有一個起始位置,從這個起始位置開始執行。在執行了一些初始化操作,比如進程的狀態設置,各個進程的棧的空間的分配後,將CPU分配給第一個進程,開始執行第一個進程,然後通過一定的調度算法,比如時間片輪轉,在一個時間片後,發生中斷,第一個進程被阻塞,在完成保存現場後將CPU分配給下一個進程,執行下一個進程。這樣,操作系統就完成了基本的進程調度的功能。

2018-2019-1 20189229《Linux內核原理與分析》第三周作業