陳鐵 + 原創作品轉載請註明出處 + 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000 ”。特別說明,所有代碼出自孟寧老師的mykernel,也許出於練習的目的有所修改,也可忽略。
學習的過程其實就是不斷的模仿,重複老師演示的內容,不斷地練習,直到成爲自己所能獨立表述的知識。自己實在很笨了,作業勉強完成,好在也算努力,花時間多些,畢竟是自己的辛苦學習的過程體現。所以擺出來給方家一笑,好歹也是自己學習的收穫。
一、 實驗用的是實驗樓環境,虛擬機環境如下:Linux d0c756f6c18a 3.13.0-30-generic #55-Ubuntu SMP Fri Jul 4 21:40:53 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux。實驗開始使用簡單代碼,可以看見中斷調度演示。
cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage |
二、將老師的代碼mypch.b,mymain.c,myinterrupt.c複製到mykernel目錄中。回到kernel目錄下:
make all qemu -kernel arch/x86/boot/bzImage |
就可以看到進程調度的過程在虛擬機中體現出來。以下截圖:
三、下面來分析一下代碼的執行過程,描述一下現代操作系統的工作機制。
1.在linux核心中爲了實現高效執行,大量使用了內聯彙編,所以在此先介紹一下內聯彙編的相關知識。(1)雖然現代編譯器優化代碼,但仍比不過手寫的彙編代碼;(2)有些平臺相關的指令必須手寫,在C語言中沒有等價的語法,例如x86是端口I/O。
gcc
提供了一種擴展語法可以在C代碼中使用內聯彙編。最簡單的格式是__asm__("assembly code");
,例如__asm__("nop");
就只是執行一條空指令。執行多條彙編指令,則應該用\n\t
將各條指令分隔開。
內聯彙編要和C的變量建立關聯,使用完整的內聯彙編格式:
__asm__(assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
這種格式由四部分組成,第一部分是彙編指令,第二部分和第三部分是約束條件,第二部分指示彙編指令的運算結果
要輸出到哪些C操作數中,C操作數應該是左值表達式,第三部分指示彙編指令需要從哪些C操作數獲得輸入,第四部分是在彙編指令中被修改過的寄存器列表,指示編譯器哪些寄存器的值在執行這條__asm__
語句時會改變。後三個部分都是可選的,如果有就填寫,沒有就空着只寫個:
號。
2.mypcb.h代碼如下:
/* * linux/mykernel/mypcb.h * * Kernel internal PCB types * * Copyright (C) 2013 Mengning * */ #define MAX_TASK_NUM 4 //定義系統執行的最大進程數。 #define KERNEL_STACK_SIZE 1024*8 //內核堆棧大小 /* CPU-specific state of this task */ struct Thread { //定義結構體Thread unsigned long ip; //存儲指令指針和堆棧指針 unsigned long sp; }; typedef struct PCB{ //結構體類型進程控制塊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); //調度函數
3.以下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; //當前進程指針,從0號進程開始 volatile int my_need_sched = 0; //0號進程不需要調度 void my_process(void); void __init my_start_kernel(void) //內核創建進程,從0號進程開始初始化 { 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; //堆棧指向定義的內核Stack task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; task[pid].next = &task[pid]; /*fork more process */ for(i=1;i<MAX_TASK_NUM;i++) //通過fork函數啓動更多的進程,本例0,1,2,3 { //我們是簡單演示,此處直接複製0號進程的狀況作爲新的進程 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]; task[i].next = task[i-1].next; //進程之間形成鏈表 task[i-1].next = &task[i]; } /* start process 0 by task[0] */ //啓動0號進程 pid = 0; my_current_task = &task[pid]; /* 內聯彙編,%0,%1代表輸入輸出部分的變量"c"代表ECX,"d"代表EDX,"=m"表示內存 %%reg表示寄存器。\n\t表示結束。 以下彙編代碼不難理解,就是爲了效率。構建起CPU的運行環境,啓動了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*/ ); } /*以下是我們的簡單進程所執行的代碼,用來讓人類知道CPU執行了哪個進程。實際上很多操作系統進程 只是在後臺執行,並不需要進行人機交互,但我們不要忽略了它們。 */ void my_process(void) { int i = 0; while(1) { i++; if(i%10000000 == 0) //循環一千萬次,輸出一次進程id,主動調度,避免消息機制。 { 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.以下是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 //計數1000次並且沒有切換進程就輸出一行提醒 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; prev = my_current_task; 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 */ "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" : "=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->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; }
四、實驗總結,老師簡化的代碼還不難理解,但要自己編寫還沒有這個本事,所以直接抄下來自己理解一下,執行的過程沒有出現報錯。雖然是簡化代碼,但對於理解操作系統的工作機制還是很有幫助的。首先是內核的自舉,畢竟所有的程序都不過是內存中的代碼,內核不過是認爲指定了特權,0號進程,開始運行,自己建立自己所需要的環境。其次,操作系統畢竟是爲實際的程序服務的,接下來就要負責創建其他進程執行環境、資源分配,採用鏈表機制切換到新進程,並且執行。最後,內核要負責管理進程的狀態,利用中斷機制實現進程切換,控制程序的執行。總之,操作系統所作的就是中斷上下文的處理和進程切換上下文的處理。