“Linux内核分析”实验二
完成一个简单的时间片轮转多道程序内核代码
作者:何振豪
原创作品转载请注明出处 http://www.cnblogs.com/scoyer/p/6439992.html
《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000
这一节感觉学了很多东西,要好好理理,消化消化。。。
1.实验简介
此次试验的主要目的是运行和分析老师搭建的简单的时间片轮转多道程序内核代码mykernel,并借此理解和分析操作系统是怎样工作的?
2.简单版
运行实验楼初始简单版本,看看试验效果吧!
(1)先打开实验楼已经配好的kernel环境
(2)进去内核所在目录:cd LinuxKernel/linux-3.9.4
(3)进去mykernel文件夹:cd mykernel\
打开mymain.c:vim mymain.c
打开myinterrupt.c:vim myinterrupt.c
只有这两个主要的c代码(够简单吧),运行该内核效果如图:
上述的截图是运行那两份代码文件之后的效果,屏幕中显示的是时间中断后调用的函数my_timer_handler,向屏幕中输出一个简单的字符串:“>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<”,然后主进程调用也是显示字符串,并且打印用于计数的变量i。以上是一个很简单的内核的执行过程。实际上就是my_start_kernel和my_timer_handler here在交替执行。
3.复杂版
下面分析的是课程上一个更为复杂的内核系统,里面涉及了进程切换,也就是不只是一个进程。
(1)打开mykernel
(2)将mykernel文件夹的mymain.c,myinterrupt.c覆盖,新增mypcb.h
(3)修改Makefile:
obj-y = mymain.o myinterrupt.o mymain.o: cc -c mymain.c mypcb.h myinterrupt.o: cc -c myinterrupt.c mypcb.h
(4)回到LinuxKernel/linux-3.9.4文件夹,键入以下命令:
patch -p1 < ../mykernel_for_linux3.9.4sc.patch make allnoconfig make qemu -kernel arch/x86/boot/bzImage
运行效果如图:
详细分析每个文件的代码和执行过程如下:
1)mypcb.h
这是一个头文件,虽然这里命名为pcb(中文名为进程控制块),但是我觉得这里的切换只是简单的线程的切换,线程是轻量级的,只需要切换相应的寄存器,保存一下就可以了,每个线程都有自己的栈,切换代价小。而进程是重量级的,涉及到一系列的资源的切换,代价高,具体在操作系统的实现也异常复杂。由于现在是初学阶段,所以并不需要实现完整的进程切换。
关键代码如下:
1 struct Thread { 2 unsigned long ip; 3 unsigned long sp; 4 }; 5 6 typedef struct PCB{ 7 int pid; 8 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 9 char stack[KERNEL_STACK_SIZE]; 10 /* CPU-specific state of this task */ 11 struct Thread thread; 12 unsigned long task_entry; 13 struct PCB *next; 14 }tPCB;
这里一个进程需要记录的是:
pid:进程的编号,标记每个进程;
state:进程的状态,这里我们实际上标注的是这个进程是否第一次被运行,0表示不是,其他表示是,这有什么区别呢?这里简单说一下,主要是如果是第一次运行的话esp和ebp都是指向栈底,表示栈为空;如果不是第一次执行的话,切换的时候我们是要从栈里面弹出之前保存在栈中ebp的,后面会详细说;
stack:每个进程独特的栈,用来存储数据;
thread:记录进程的esp和eip的值;
task_entry:进程的入口,也就是进程执行的第一条指令的地址在哪,一开始进程的eip和task_entry应该是一样的;
next:指向下一个进程的地址,这里主要采用了循环链表的写法,后面代码有体现。
2)mymain.c
读代码都是从主函数出发,这里main主要存放的就是初始化内核所需要的准备工作,设置每个进程的相应参数和具体的动作。
1 void __init my_start_kernel(void) { 2 int pid = 0; 3 int i; 4 /* Initialize process 0*/ 5 task[pid].pid = pid; 6 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 7 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 8 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 9 task[pid].next = &task[pid]; 10 /*fork more process */ 11 for(i=1; i<MAX_TASK_NUM; i++) { 12 memcpy(&task[i],&task[0],sizeof(tPCB)); 13 task[i].pid = i; 14 task[i].state = -1; 15 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 16 task[i].next = task[i-1].next; 17 task[i-1].next = &task[i]; 18 } 19 /* start process 0 by task[0] */ 20 pid = 0; 21 my_current_task = &task[pid]; 22 asm volatile( 23 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ 24 "pushl %1\n\t" /* push ebp */ 25 "pushl %0\n\t" /* push task[pid].thread.ip */ 26 "ret\n\t" /* pop task[pid].thread.ip to eip */ 27 "popl %%ebp\n\t" 28 : 29 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 30 ); 31 }
想要读懂上述代码,需要一定的内嵌汇编代码的基础,这里可以参考一下博客。这个函数是内核的入口,初始化整个进程列表。首先将进程串成一个循环链表,方便之后直接取next进行调用。下半段时一段内嵌汇编代码。想要cpu跑这个进程,只要设置好eip,esp,ebp和将执行的指令写进内存就可以了。指令应该是已经准备好了的,也许是在编译的过程就已经将所有代码翻译成汇编,然后写进内存。上述注释应该表明了什么地方设置这些寄存器的值,这里就不重复解释了。
1 void my_process(void) { 2 int i = 0; 3 while(1) { 4 i++; 5 if(i%10000000 == 0) { 6 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 7 if(my_need_sched == 1) { 8 my_need_sched = 0; 9 my_schedule(); 10 } 11 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 12 } 13 } 14 }
这个代码就是每个进程要干的事情了,上面的init函数已经将每个进程的入口(也就是task_entry)设置为这个函数的地址。这个函数干的事情很简单,就是循环计数,计数一千次之后打印相应的结果,并且检查时间片,如果时间片到了那么就切换到另外一个进程。下面看一下整个进程切换的过程,这是一个非常重要的地方。
myinterrupt.c
这段代码主要是怎么调度进程,保存需要保存的值,更改需要更改的值。
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 }
这是一个时间片计数代码,当时间中断产生是就调用这个函数一次,做的事情很简单,就是不断将time_count自增,然后自增到了1000次之后并且my_need_sched为0时,就提示进程时间片到时间了。
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 my_current_task = next; 18 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 19 /* switch to next process */ 20 asm volatile( 21 "pushl %%ebp\n\t" /* save ebp */ 22 "movl %%esp,%0\n\t" /* save esp */ 23 "movl %2,%%esp\n\t" /* restore esp */ 24 "movl $1f,%1\n\t" /* save eip */ 25 "pushl %3\n\t" 26 "ret\n\t" /* restore eip */ 27 "1:\t" /* next process start here */ 28 "popl %%ebp\n\t" 29 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 30 : "m" (next->thread.sp),"m" (next->thread.ip) 31 ); 32 33 } 34 else 35 { 36 next->state = 0; 37 my_current_task = next; 38 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 39 /* switch to new process */ 40 asm volatile( 41 "pushl %%ebp\n\t" /* save ebp */ 42 "movl %%esp,%0\n\t" /* save esp */ 43 "movl %2,%%esp\n\t" /* restore esp */ 44 "movl %2,%%ebp\n\t" /* restore ebp */ 45 "movl $1f,%1\n\t" /* save eip */ 46 "pushl %3\n\t" 47 "ret\n\t" /* restore eip */ 48 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 49 : "m" (next->thread.sp),"m" (next->thread.ip) 50 ); 51 } 52 return; 53 }
这是一个切换进程过程的代码,其中最主要的就是里面的一段内嵌汇编代码。
切换进程要做什么事情呢?还是设好相应的寄存器的值就可以了。esp,ebp,eip三个寄存器和pcb结构体的相应变量。
下面逐行解释一下第一段汇编的主要作用:
(1)将ebp入栈,也就是保存当前这个进程的栈底地址;
(2)esp -> prev->thread.sp,将当前的esp保存到当前这个进程的sp变量中;
(3)next->thread.sp -> esp,切换的进程的sp赋值给esp寄存器;
(4)$1f表示段1的所在地址,所以这行代码的意思就是将段1的地址 -> prev->thread.ip,所以当前的这个进程如果要再次执行的话就要调到段1去执行,为什么要这样呢?看完下面的代码再给出具体解释;
(5)(6)这两行合起来就是next->thread.ip->eip;
(7)将栈顶的元素pop出来作为ebp
终结当前进程需要保存好eip(4),esp(2),ebp(?)
切换到下一个进程需要设置好eip(5+6),esp(3),ebp(?)
上面都出现两个?实际上就是第一条指令都干了的事情,具体原因是:先将当前的进程的ebp入栈,然后执行下一个进程,等执行完下一个进程之后栈的esp不变,那么将esp的东西弹出来自然就是ebp,所以要将eip设置为段1的第一条指令,那么确保不会覆盖掉这个ebp吗?我觉得这是一定要保证的事情,不然ebp就不能够顺利保存,所以切换到下一个进程之前先push ebp,然后执行完回来的时候pop出来的正是ebp,所以就不需要保存ebp的值在pcb的结构体中。
以上就是完整的代码以及详细的解释。这是一个时间片轮转运行多个进程的简单的linux内核,操作系统主要完成的就是进程的切换,这需要保存被切换进程的esp,eip和ebp,将新的进程的esp,eip,ebp设置好,然后继续运行,看似复杂实际上条条有理,一步一步地执行,这正是操作系统的美妙之处。