Linux內核分析:完成一個簡單的時間片輪轉多道程序內核代碼

PS.賀邦   原創做品轉載請註明出處  《Linux內核分析》MOOC課程    http://mooc.study.163.com/course/USTC-1000029000 linux

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

使用實驗樓虛擬機打開shell輸入下列代碼git

  • 1 cd LinuxKernel/linux-3.9.4
  • 2 qemu -kernel arch/x86/boot/bzImage

能夠看到初始的內核運行狀況以下:github

 

內核不停的執行my_start_kernel(),每隔一段時間被my_timer_handler()中斷,而後執行一條打印語句:printk(KERN_NOTICE 「\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n」);算法

後,又回到my_start_kernel()繼續執行。shell

打開mymain.c文件,能夠看到其中只有以下這一個函數。它做爲內核啓動的起始位置,從這個函數開始運行,並沒有限循環。數組

打開myinterrupt.c文件,裏面也只有一個my_timer_handler(),它被Linux內核週期性調用,從而產生了一個週期性的中斷機制。函數

2.修改內核代碼,使之成爲一個簡單的時間片輪轉多道程序內核,而後從新編譯運行。

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也放在這裏。 this

而後執行make,從新編譯內核。效果以下: spa

而後再次輸入:操作系統

  • qemu -kernel arch/x86/boot/bzImage 命令

啓動內核。 能夠發現跳轉和時間片都有了明顯的改變。

實驗結果與預期結果相符,實驗成功。

3.重點代碼理解

mymain.c

void__init my_start_kernel(void) {

 Int pid = 0; 

 Int i; 

/* Initialize process 0*/

task[pid].pid = pid;//task[0].pid=0;

ask[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */

task[pid].task_entry = task[pid].thread.ip = (unsignedlong)my_process;//0號進程的入口地址爲my_process();

task[pid].thread.sp = (unsignedlong)&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 = (unsignedlong)&task[i].stack[KERNEL_STACK_SIZE-1]; task[i].next = task[i-1].next;//新建立的進程的next指向0號進程的首地址

task[i-1].next = &task[i];//前一個進程的next指向最新建立的進程的首地址,從而成爲一個循環鏈表。

 

mypcb.h

#define MAX_TASK_NUM 4 //最大進程數,這裏設置爲了4個。

#define KERNEL_STACK_SIZE 1024*8 //每一個進程的內核棧的大小。

/* CPU-specific state of this task */

structThread { unsignedlongip;//用於保存進程的eip

unsignedlongsp;//用戶保存進程的esp}; 

typedefstructPCB{ intpid;//進程的id

volatilelongstate; /* 進程的狀態:-1 unrunnable, 0 runnable, >0 stopped */

charstack[KERNEL_STACK_SIZE];//進程的棧,只有一個核心棧。/* CPU-specific state of this task */

structThread thread;//每一個進程只有一個線程。

unsignedlongtask_entry;//進程的起始入口地址。

myinterrupt.c

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

 

4.分析進程的啓動和進程的切換機制。

 

首先,內核啓動__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號進程的切換。此後相似。

5.對「操做系統是如何工做的」理解。

操做系統的內核有一個起始位置,從這個起始位置開始執行。開始工做時,CPU分配給第一個進程,開始執行第一個進程,而後經過必定的調度算法,好比時間片輪轉,在一個時間片後,發生中斷,

第一個進程被阻塞,在完成保存現場後將CPU分配給下一個進程,執行下一個進程。這樣,操做系統就完成了基本的進程調度的功能。

相關文章
相關標籤/搜索