李俊锋 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一.实验目的
1、熟悉、理解Linux内核工作方式
2、尝试编写自己的内核
3、理解多进程时间片轮转的工作方式
二.实验步骤
1.编写时间片轮转程序。
2.运行程序
1 cd LinuxKernel/linux-3.9.4 2 qemu -kernel arch/x86/boot/bzImage
三.代码分析
3.1 mypcb.h
1 #define MAX_TASK_NUM 4 2 #define KERNEL_STACK_SIZE 1024*8 3 4 /* CPU-specific state of this task */ 5 //用于保存EIP,ESP 6 struct Thread { 7 unsigned long ip; 8 unsigned long sp; 9 }; 10 //定义进程管理相关的数据结构 11 typedef struct PCB{ 12 int pid;//进程ID 13 volatile long state; //进程状态 /* -1 unrunnable, 0 runnable, >0 stopped */ 14 char stack[KERNEL_STACK_SIZE]; //当前进程的堆栈 15 /* CPU-specific state of this task */ 16 struct Thread thread; 17 unsigned long task_entry;//进程入口 18 struct PCB *next; //构造链表 19 }tPCB; 20 //声明调度器函数 21 void my_schedule(void);
(1)Thread结构体中的ip,sp用于保存EIP和ESP寄存器的值。
(2)PCB结构体定义了进程管理相关的数据结构。
(3) my_schedule是调度函数,此处声明调度函数。
3.2 mymain.c
1 #include <linux/tty.h> 2 #include <linux/vmalloc.h> 3 4 5 #include "mypcb.h" 6 7 tPCB task[MAX_TASK_NUM];//进程结构数组 8 tPCB * my_current_task = NULL; //当前进程的指针 9 volatile int my_need_sched = 0;//是否需要调度 10 11 void my_process(void);
(1)上面的代码中定义了进程的结构数组,当前运行的进程结构的指针以及调度标志位。
(2)声明my_process函数,该函数可以看成在初始化结束后程序的主函数。
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;//入口是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]; //ret之后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*/ ); }
(1)上面的程序首先初始化第一个进行即task[0]。
(2)之后创建更多的进程,扩充循环链表,使用memcpy将task[0]初始状态复制到task[i]。至此形成一个有4个PCB的循环链表
(3)后边的汇编代码是用来初始化堆栈esp、ebp、eip,并调用my_progress函数运行第一个进程。汇编详细的分析如下:
1 asm volatile( 2 "movl %1,%%esp\n\t" /* 将esp指向task[0]的堆栈stack顶端。 */ 3 "pushl %1\n\t" /* 将task[0].thread.sp即堆栈顶端地址压栈,为以后的%ebp的复位使用。 */ 4 "pushl %0\n\t" /* 将task[0].thread.ip即程序入口my_process压栈。 */ 5 "ret\n\t" /*将task[0].thread.ip即程序入口my_process放到eip。 */ 6 "popl %%ebp\n\t" /*popl %%ebp:引文eip被更改,所以实际没有运行。 */ 7 : 8 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 9 );
1 void my_process(void) 2 { 3 int i = 0; 4 while(1) 5 { 6 i++; 7 if(i%10000000 == 0) 8 { 9 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 10 if(my_need_sched == 1) 11 { 12 my_need_sched = 0; 13 my_schedule(); 14 } 15 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 16 } 17 } 18 }
(1)循环执行因为my_need_sched初始为0,只用当时钟中断my_timer_handler将my_need_sched置为1时,
才会调用my_schedule()进行进程的调度。
3.3 myinterrupt.c
1 #include <linux/ctype.h> 2 #include <linux/tty.h> 3 #include <linux/vmalloc.h> 4 5 #include "mypcb.h" 6 7 extern tPCB task[MAX_TASK_NUM]; 8 extern tPCB * my_current_task; 9 extern volatile int my_need_sched; 10 volatile int time_count = 0;
(1)文件头定义了一些外部变量,以及记录时间中断次数的变量time_count
1 void my_timer_handler(void) 2 { 3 #if 1 4 if(time_count%1000 == 0 && my_need_sched != 1) 5 { 6 printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 7 my_need_sched = 1; 8 } 9 time_count ++ ; 10 #endif 11 return; 12 }
(1)本函数作用是当时间中断的次数每隔1000次将my_need_sched置1,调用进程的调度函数。
1 void my_schedule(void) 2 { 3 tPCB * next; 4 tPCB * prev; 5 //出错处理 6 if(my_current_task == NULL 7 || my_current_task->next == NULL) 8 { 9 return; 10 } 11 printk(KERN_NOTICE ">>>my_schedule<<<\n"); 12 /* schedule */ 13 next = my_current_task->next; 14 prev = my_current_task; 15 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 16 { 17 /* switch to next process */ 18 asm volatile( 19 "pushl %%ebp\n\t" /* save ebp */ 20 "movl %%esp,%0\n\t" /* save esp */ 21 "movl %2,%%esp\n\t" /* restore esp */ 22 "movl $1f,%1\n\t" /* save eip */ 23 "pushl %3\n\t" 24 "ret\n\t" /* restore eip */ 25 "1:\t" /* next process start here */ 26 "popl %%ebp\n\t" 27 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 28 : "m" (next->thread.sp),"m" (next->thread.ip) 29 ); 30 my_current_task = next; 31 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 32 } 33 else 34 { 35 next->state = 0; 36 my_current_task = next; 37 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 38 /* switch to new process */ 39 asm volatile( 40 "pushl %%ebp\n\t" /* save ebp */ 41 "movl %%esp,%0\n\t" /* save esp */ 42 "movl %2,%%esp\n\t" /* restore esp */ 43 "movl %2,%%ebp\n\t" /* restore ebp */ 44 "movl $1f,%1\n\t" /* save eip */ 45 "pushl %3\n\t" 46 "ret\n\t" /* restore eip */ 47 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 48 : "m" (next->thread.sp),"m" (next->thread.ip) 49 ); 50 } 51 return; 52 } 53
(1)上面的函数是进程的调度函数,开始时首先进行错误处理,如果进程运行出错就返回。
(2)之后判断在进程链表中,目前正在运行的进程的下一个进程是否已经被开启。
(3)如果之前运行过,对于一个state = 0的task进行了堆栈的复原并对原task的ebp、esp进行保存。其中原ebp保存在
原堆栈顶端,原esp保存在prev->thread.sp中,而新的ebp指向之前压入栈中的ebp即next.task的堆栈底端即最高地址,
而新的ebp指向之前保存在next->thread.sp即next.task的堆栈顶端即最低地址。
(4)如果state-1,对于一个state = -1的task进行了堆栈的初始化并对原task的ebp、esp进行保存。其中原ebp保存在原堆
栈顶端,原esp保存在prev->thread.sp中,而新的ebp、esp指向next.task的堆栈底端即最高地址。
四.实验运行图片
五.实验总结
操作系统是如何工作的:
首先操作系统介于硬件和上层软件之间,操作系统不仅负责管理计算机上的硬件资源也要负责管理计算机上软件的
运行。由于硬件条件的限制,操作系统不可能让所有的程序都同时运行,虽然可以有分时运行的策略,但是当面对
进程数量,线程数量都比较多的时候,就必须采用中断机制和上下文切换机制对所有进程进行控制。在进程切换时,
操作系统会将此时寄存器中的值保存起来,当切换回时,再保存回去。
本实验难度非常大,虽然我能在老师教学视频的帮助下完成实验报告,但仍有一些问题没弄懂,比如时间中断的设置
等。希望在下一次的实验中,我能更好的理解实验的原理。