2018-2019-1 20189229《Linux內核原理與分析》第三周作業
一. 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內核原理與分析》第三周作業