鄭德倫 原創作品轉載請註明出處 《Linux內核分析》MOOC課程
http://mooc.study.163.com/course/USTC-1000029000
STEP1:搭建實驗環境
首先在自己的Linux系統中配置好實驗的環境,依次輸入以下的命令:
• sudo apt-get install qemu # install QEMU
• sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
• wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code
• wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch
• xz -d linux-3.9.4.tar.xz
• tar -xvf linux-3.9.4.tar
• cd linux-3.9.4
• patch -p1 < ../mykernel_for_linux3.9.4sc.patch
• make allnoconfig
• make
最後執行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一個進程切換的內核
經過環境的配置,Linux內核經過我們修改,在qemu窗口中我們會看到,一個只有時鐘中斷的系統。如圖所示
因爲在mymian.c裏面,只有一個函數在執行,my_start_kernel(void),循環輸出一句話。
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文件裏面只有一個處理時鐘中斷的函數
void my_timer_handler(void)
{
printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}
如果我們要完成可以進行進程調度的操作系統內核,需要將github中mykernel的myinterrupt.c mymain.c mypcb.h三個文件複製到linux內核文件夾下面,然後再進行make。
首先在github上面下載3個文件,在終端中輸入如下命令:
wget https://raw.github.com/mengning/mykernel/master/mypcb.h
wget https://raw.github.com/mengning/mykernel/master/myinterrupt.c
wget https://raw.github.com/mengning/mykernel/master/mymain.c
然後將三個文件拷貝到linux內核文件夾下面的mykernel文件夾下面,在終端中輸入命令
cp myinterrupt.c linux-3.9.4/mykernel/
cp mymain.c linux-3.9.4/mykernel/
cp mypch.h linux-3.9.4/mykernel/
最後執行make重新編譯內核
然後終端中輸入qemu -kernel arch/x86/boot/bzImage就可以看到一個進程切換的內核運行了。
STEP3:分析代碼,理解執行過程
首先打開mypcb.h文件,有兩個結構體。
第一Thread結構體,保存一個任務的eip還有esp的信息。
struct Thread {
unsigned long ip;
unsigned long sp;
};
第二個結構體PCB,保存進程的一些信息。
pid保存一個進程唯一的一個ID號
state表示一個進程的運行狀態
stack數組表示一個進程的堆棧空間
Thread結構體記錄eip和esp堆棧棧頂的信息
task_entry表示進程的執行入口地址
struct PCB* next則指向下一個進程,用鏈式儲存結構將進程連在一起
typedef struct PCB{
int pid;
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;
然後打開mymain.c文件
volatile int my_need_sched = 0;
my_need_sched變量表示一個任務,是否可以進行調度。
然後分析kernel開始執行的函數。
void __init my_start_kernel(void)
{
int pid = 0;
int i;
/* Initialize process 0*/
task[pid].pid = pid;/*將進程號初始化爲0*/
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
/*把進程的入口和eip都初始化爲my_process函數的入口地址*/
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
/*將堆棧的棧頂地址保存爲棧的最後一個元素,即最高的地址*/
task[pid].next = &task[pid];
/*next指針指向自己*/
/*fork more process */
for(i=1;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[0],sizeof(tPCB));
/*將task[0]的信息全部複製到各個新進程中*/
task[i].pid = i;/*將新進程的pid設置爲變量i*/
task[i].state = -1;/*新進程的狀態爲unrunnable*/
task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];/*將每個新進程的棧頂指針都指向堆棧數組的最後一個元素*/
/*使用尾插法創建鏈表形成一個任務鏈0->1->2->3->0這樣的循環*/
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];//將當前進程設置爲0號進程
asm volatile(
/*把0號進程的esp放入esp寄存器*/
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
/*push0號進程的esp,因爲此時棧爲空,esp==ebp,所以此處等同於push ebp*/
"pushl %1\n\t" /* push ebp */
/*將0號進程的eip,即process函數的入口地址push到當前堆棧*/
"pushl %0\n\t" /* push task[pid].thread.ip */
/*將棧頂元素pop到eip然後跳轉*/
"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*/
);
}
my_start_kernel完成的任務主要是初始化進程鏈表,然後開始執行第一個進程。
執行完ret操作之後將會跳轉到process函數中執行。
然後我們來分析process程序,process程序是一個無限循環,每循環100000000執行判斷生效一次,輸出KERN_NOTICE “this is process pid-,並且判斷my_need_sched是否等於1,如果等於1的話,就把my_need_sched置0並且執行my_schedule()進行調度,最後執行KERN_NOTICE “this is process pid+
void my_process(void)
{
int i = 0;
while(1)
{
i++;
if(i%100000000 == 0)// 沒執行100000000次循環進入一次
{
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);
}
}
}
我們再來看看myinterrupt.c文件
void my_timer_handler(void)
{
#if 1
/*函數每執行1000次,並且my_need_sched不等於1的時候執行if判斷,然後把my_need_sched置爲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;
}
分析一下函數my_timer_handler(void),#if 1 到#endif表示一個條件預編譯指令,表示永遠編譯,一般用於測試代碼中,把1改爲0相當於註釋掉代碼段,方便調試。
my_timer_handler函數每執行1000次並且my_need_sched不爲1的時候輸出一段話,並且把my_timer_handler置爲1.
到此我們就清楚了my_need_sched這個變量的變化過程(由process置0,由my_timer_handler置1,交替執行),和mymain.c裏面的process函數結合就可以分析出進程調度的詳細過程。
注:因爲時間中斷處理每隔一段時間就會進行一次,可能在0號進程執行過程中進行了多次時間中斷處理,我們暫時把沒有發生my_need_sched變量改變的時間中斷處理時間忽略掉。
進程的調度過程如下:
0號進程啓動,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行1號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行2號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行3號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
->執行0號進程,my_need_sched=0,當my_timer_handler將my_need_sched置1時,process執行調度函數my_schedule(),並將my_need_sched置0
整個調度過程就如上面所示,process循環執行,等待時鐘中斷來將允許調度的變量置1,然後完成調度。
具體的調度過程我們要分析my_schedule函數:
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;
/*next進程兩種情況,運行和非運行要分情況處理*/
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 */
/*將下面標號1:之後的語句popl ebp的地址放入prev的ip中保存*/
"movl $1f,%1\n\t" /* save eip */
/*將next的eip push到棧上,然後執行ret的話,就會從next的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的狀態置爲runnable*/
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;
}
如果next進程爲非runnable狀態的時候執行else,runnable的狀態執行if,整個過程就是先保存當前進程的ebp和esp,然後將next進程的esp和ebp復原,並且將prev進程的eip儲存爲標號1的位置,最後將next進程的eip push到棧上,再執行ret指令,返回到next的eip所指向的地方執行。
因爲進程是用一個循環鏈表連接起來的,一直從prev向next切換,總會有一次再次切換到自己,也就是prev進程,變成next進程。如果下一次進入的時候會標號1:開始執行完成ebp的復原操作。
注:爲了便於理解我們用0號進程和1號進程表示。
這個schedule函數分兩次執行,第一次是從0號進程跳轉到1號進程,而第二次進入則是執行popl ebp恢復現場,然後return函數,返回到原調用者也就是0號進程的process裏面。
總結:
通過以上代碼的分析,我們可以初步瞭解到了,linux內核是如何完成進程的切換的。我們可以認爲一個進程相當於一個堆棧,每個進程有自己的堆棧空間。如果將ebp和esp修改爲另一個進程的ebp和esp,並且完成一些寄存器的保存,就相當於完成的進程的切換。