内容一:实验报告相关说明。
所学课程:《Linux内核分析》MOOC课程
链接:http://mooc.study.163.com/course/USTC-1000029000
代码来源于孟宁老师的课件
虚拟实验室实验截图
内容二:进程的启动
1:首先要启动的程序是pid=0的任务,其初始化的c代码如下所示。
1 int pid = 0; 2 int i; 3 /* Initialize process 0*/ 4 task[pid].pid = pid; 5 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 6 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 7 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 8 task[pid].next = &task[pid];
初始化后:
2:通过以下代码初始化了其他的任务,用于后续的进程切换实验。
其中有如下宏定义:
#define MAX_TASK_NUM 4 //说明总共有4个任务
#define KERNEL_STACK_SIZE 1024*8 //说明每个任务的私有堆栈大小为1KB
1 for(i=1;i<MAX_TASK_NUM;i++) 2 { 3 memcpy(&task[i],&task[0],sizeof(tPCB)); 4 task[i].pid = i; 5 task[i].state = -1; 6 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 7 task[i].next = task[i-1].next; 8 task[i-1].next = &task[i]; 9 }
第7、8行代码,将各个任务链接成链表。
3:启动代码是用汇编写的,如下所示。
1 asm volatile(
2 "movl %1,%%esp
" /* set task[pid].thread.sp to esp */
3 "pushl %1
" /* push ebp */
4 "pushl %0
" /* push task[pid].thread.ip */
5 "ret
" /* pop task[pid].thread.ip to eip */
6 "popl %%ebp
"
7 :
8 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
9 );
代码的主要功能是将任务0的入口传到寄存器EIP中,将寄存器ESP和EBP指向任务0的私有堆栈。
%0代表task[0].thread.ip,%1代表task[0].thread.sp
启动机制的分析主要就是对ESP,EBP,EIP三个寄存器进行分析。
执行
2 "movl %1,%%esp " 后:ESP指向任务0堆栈的栈顶 task[0].stack[KERNEL_STACK_SIZE-1]
执行
3 "pushl %1 " /* push ebp */ 4 "pushl %0 " /* push task[pid].thread.ip */
主要就是将 thread.sp 和 thread.ip 的值压入堆栈,目的是为了通过出栈指令间接的改变ESP和SBP寄存器的值。
执行
5 "ret " /* pop task[pid].thread.ip to eip */ 6 "popl %%ebp " 其效果:
EBP和ESP指向了任务0的私有堆栈,EIP指向了任务0代码(my_process)的入口。从而实现了任务的启动。
内容三:进程的切换
1:进程的切换主要是通过调用函数 void my_schedule(void)来进行的。
进程切换的情况有两种:
一 将要被切换的进程在之前已经执行过了,则主要任务则是恢复该任务上一次被切换前的现场。
二 将要被切换的进程从未执行过。
2:情况一对应以下代码
1 asm volatile( 2 "pushl %%ebp " /* save ebp */ 3 "movl %%esp,%0 " /* save esp */ 4 "movl %2,%%esp " /* restore esp */ 5 "movl $1f,%1 " /* save eip */ 6 "pushl %3 " 7 "ret " /* restore eip */ 8 "1: " /* next process start here */ 9 "popl %%ebp " 10 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 11 : "m" (next->thread.sp),"m" (next->thread.ip) 12 ); 13 my_current_task = next; 14 printk(KERN_NOTICE ">>>switch %d to %d<<< ",prev->pid,next->pid);
2 "pushl %%ebp " /* save ebp */ 3 "movl %%esp,%0 " /* save esp */
实现的功能是将保存当前的堆栈。将ESP的值存入到 prev->thread.sp 代码具体分析在上一次的作业已经涉及到了。
4 "movl %2,%%esp " /* restore esp */ 5 "movl $1f,%1 " /* save eip */ 6 "pushl %3 "
语句4将ESP的内容改为带切换任务的私有栈顶。
语句5就是将EIP的值拷贝到 prev->thread.ip 中。$1f就是指标号1:的代码在内存中存储的地址。
语句6和语句7将使得EIP执行待切换的任务。
由于待切换的任务next是之前执行过的,其私有堆栈并不是空的如下图所示。
语句9就是将old ebp 弹入到寄存器 EBP中,从而达到恢复现场堆栈的目的。
3:情况二对应以下代码
1 next->state = 0; 2 my_current_task = next; 3 printk(KERN_NOTICE ">>>switch %d to %d<<< ",prev->pid,next->pid); 4 /* switch to new process */ 5 asm volatile( 6 "pushl %%ebp " /* save ebp */ 7 "movl %%esp,%0 " /* save esp */ 8 "movl %2,%%esp " /* restore esp */ 9 "movl %2,%%ebp " /* restore ebp */ 10 "movl $1f,%1 " /* save eip */ 11 "pushl %3 " 12 "ret " /* restore eip */ 13 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 14 : "m" (next->thread.sp),"m" (next->thread.ip) 15 );
情况一与情况二的区别在于,后者待切换的任务next未曾执行过,所以直接通过语句9修改EBP的值,使得EBP,ESP均指向任务next私有堆栈初始位置即可(即该堆栈是空的)
其他的与情况一类似,就不再赘述。
内容四:小结
1:分析这个时间片轮转多道程序的内核代码让我初步了解了任务调度和任务启动的机制。
2:任务启动和调度的实现主要是通过内嵌的汇编代码实现的,在上课的过程中,自己也学会了基本的AT&T汇编语言,能够用它进行一下简单的分析。而启动和切换做的主要工作都是对EBP,ESP,EIP寄存的修改,达到保护当前任务的状态,恢复下一个任务在上一次切换前的状态。
3:任务的基本信息都是存储在PCB的结构体中,而这都是一个任务所必须具备的元素。这与UCOSii任务控制块TCB相似。只要理解了结构体的定义,才能掌握任务的初始化,这些都是理解任务启动和任务调度的基本条件。