基於mykernel的一個簡單的時間片輪轉多道程序內核代碼的分析
學號178原創作品,轉載請註明出處,
《Linux內核分析》MOOC課程:http://mooc.study.163.com/course/USTC-1000029000
本次實驗資源來自:https://github.com/mengning/linuxkernel/
一、mykernel簡介
這是一個由中科大軟件學院孟寧老師建立的一個用於開發屬於你自己的操作系統內核的平臺,基於Linux Kernel 3.9.4 source code。mykernel的源代碼地址:https://github.com/mengning/mykernel
你可以根據這上面的指南將其部署到自己的系統上。此外,你也可以使用實驗樓上提供的虛擬機。http://www.shiyanlou.com/courses/195。根據“實驗2”的步驟即可找到並運行這個平臺框架。
打開終端,通過如下指令便可打開如圖所示的QEMU虛擬機了。
cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
make allnoconfig
make
qemu -kernel arch/x86/boot/bzImage
我們會發現,>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<和>>>>>>>>>>>>>>>my_start_kernel here<<<<<<<<<<<<<<<<會無限循環輸出。(此時一個操作系統已經在運行了,功能就是無限輸出上述的兩個字符串。)
如上圖所示,我們依此查看myinterrupt.c和mymain.c文件
這便是實現QEMU中輸出字符串功能的文件。
我們發現,mykernel系統在啟動後,會一直調用這兩個文件中的函數,每當i%00000為0時,周期輸出my_start_kernel_here。
二、實現一個簡單的時間片輪轉多道程序
將孟老師GitHub上實驗用的源代碼https://github.com/mengning/mykernel 通過git clone指令下載到mykernel平臺上。如下圖所示。
之後會得到 mypcb.h、myinterrupt.c和mymain.c三個文件。
回到linux-3.9.4文件夾中,依次輸入make allnoconfig、make、qemu -kernel arch/x86/boot/bzImage三條指令,得到下圖:
源代碼分析:
1、mypcb.h:定義進程控制塊PCB的結構體。
pid:進程號
state:進程狀態,在模擬系統中,所有進程控制塊信息都會被創建出來,其初始化值就是-1,如果被調度運行起來,其值就會變成0
stack:進程使用的堆棧
thread:當前正在執行的線程信息
task_entry:進程入口函數
next:指向下一個PCB,模擬系統中所有的PCB是以鏈表的形式組織起來的。
這裏還有一個函數的聲明 my_schedule,它的實現在my_interrupt.c中,在mymain.c中的各個進程函數會根據一個全局變量的狀態來決定是否調用它,從而實現主動調度。
2、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; | |
prev = my_current_task; | |
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" | |
: "=m" (prev->thread.sp),"=m" (prev->thread.ip) | |
: "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; | |
} |
這裏 my_timer_handler 函數會被內核周期性的調用,每調用1000次,就去將全局變量my_need_sched的值修改為1,通知正在執行的進程執行調度程序my_schedule。在my_schedule函數中,完成進程的切換。進程的切換分兩種情況,一種情況是下一個進程沒有被調度過,另外一種情況是下一個進程被調度過,可以通過下一個進程的state知道其狀態。進程切換依然是通過內聯匯編代碼實現,無非是保存舊進程的eip和堆棧,將新進程的eip和堆棧的值存入對應的寄存器中,詳見代碼中的註釋。
3、mymain.c:初始化各進程,並啟動0號進程。
/* | |
* 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[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ | |
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; | |
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++) | |
{ | |
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] */ | |
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*/ | |
); | |
} | |
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); | |
} | |
} | |
} |
正如前文所述,這裏的函數 my_start_kernel 是系統啟動後,最先調用的函數,在這個函數裏完成了0號進程的初始化和啟動,並創建了其它的進程PCB,以方便後面的調度。在模擬系統裏,每個進程的函數代碼都是一樣的,即 my_process 函數,my_process 在執行的時候,會打印出當前進程的 id,從而使得我們能夠看到當前哪個進程正在執行。
另外,在 my_process 也會檢查一個全局標誌變量 my_need_sched,一旦發現其值為 1 ,就調用 my_schedule 完成進程的調度。
0號線程的啟動,采用了內聯匯編代碼完成,詳細參見源碼中的註釋。
再來看看最後一個文件,myinterrupt.c
三、總結
操作系統內核有一個起始位置,從這個起始位置開始執行。在執行了一些初始化操作,比如進程的狀態設置,各個進程的棧的空間的分配後,將CPU分配給第一個進程,開始執行第一個進程,然後通過一定的調度算法度,比如時間片輪轉,在一個時間片後,發生中斷,第一個進程被阻塞,在完成保存現場後將CPU分配給下一個進程,執行下一個進程。這樣,操作系統就完成了基本的進程調度的功能。
基於mykernel的一個簡單的時間片輪轉多道程序內核代碼的分析