张建帮 原创作品转载请注明出处 《linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
孟宁大法好啊!第二次的课我就听得云里雾里的,看代码看了好久才理解其中的精髓所在,不过也确实对于在操作系统课上、只在理论上接触到的进程切换和时间片轮转部分的知识有了更深入的理解。
这次课的核心是以C语言为例实现了一个逻辑上的硬件平台,这个硬件平台能实现计时器中断和响应以及不同进程之间的切换工作,重点是后者,至于前面的计时器中断响应部分则没有没有做过多的介绍,只是写了一个响应函数,什么时候注册与调用的——引用孟宁老师的一句话——“不必深究“。
先看一下头文件
define MAX_TASK_NUM 4#define KERNEL_STACK_SIZE 1024*8/* CPU-specific state of this task */struct Thread { unsigned long ip; unsigned long sp;};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;void my_schedule(void);上面这部分代码定义了一个结构体PCB,其实就相当于操作系统课的讲到的那个PCB,PRocess Control Block,即进程控制块,用来保存进程切换过程中的上下文(环境),比如:
pid——保存进程号 state保存进程的运行状态——在本实验中,就2个状态,0和-1,0代表已经执行过了,-1则表示进程一次都没有被执行 task_entry不知道干啥的,也就在初始化的过程用到了,其他地方没见到… next指向下一个进程控制块…其实在实验中的进程控制块都是用数组存放在一起的,使用next指针是为了体现其通用性至于文件的其他部分,比如函数声明,宏也就不再多说了。
接下来看第二部分,mymain.c:
//全局变量部分tPCB task[MAX_TASK_NUM]; //所有的进程控制块都在这了tPCB * my_current_task = NULL; //当前进程volatile int my_need_sched = 0; //调度标志,为1才有可能调度void my_process(void); //每个进程控制块里的进程都指向这个这个函数void __init my_start_kernel(void){ int pid = 0; int i; /* 初始化0号进程 */ task[pid].pid = pid; task[pid].state = 0; //注意!!这里是0,代表马上要执行了 //所有进程的entry和thread.ip都指向函数my_process task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; task[pid].next = &task[pid]; /* 初始化其他进程 */ for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB)); task[i].pid = i; task[i].state = -1; //注意!!这里和上面不同,是-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] */ //开始运行进程0 pid = 0; my_current_task = &task[pid]; //嵌入式汇编代码,具体的解析见下文 asm volatile( "movl %1,%%esp/n/t" /* 设置esp寄存器,使其指向进程0的栈顶 */ "pushl %1/n/t" /* 相当于push %ebp,因为刚启动时,esp和ebp相等 */ "pushl %0/n/t" /* 该指令和下一条指令一起将eip设置为进程的ip,这里将eip设置为函数my_process的入口地址 */ "ret/n/t" "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); } }}上面的代码涉及到嵌入式汇编代码,这部分内容其实就相当于在汇编代码的基础上增加了输入和输出的功能。就拿上面的代码举例:
前面的asm volatile
是固定的格式信息 代码中多次出现的 %%
,第一个 %
说明这是一个转义字符,相当于c语言中的/
,2个 %
就相当于汇编中的%
第一个冒号 :
后面跟的是所有要输出的数据,第二个:
后面跟着所有的输入数据,相当于函数中的参数。这些数据都有默认的编号,从第一个输出数据开始编号,最小的编号为0,从左到右编号逐渐变大;若没有输出数据,则从输入数据开始。比如上面汇编码中就没有输出数据,第一个输入数据 "c" (task[pid].thread.ip)
的编号就是0,前面的修饰符c
表示将后面括号中的数据存放到寄存器ecx
中,后面的 "d" (task[pid].thread.sp)
编号为1,在汇编代码中可以通过 %编号
的方式进行参数的引用,比如上面汇编码出现的%1
指的就是task[pid].thread.sp
的值。 介绍完上面的嵌入式汇编代码的知识后,结合代码中的注释,阅读上面的源程序应该不会有太大的困难。整个汇编代码的作用其实就是启动0号进程,0号进程启动之后,就开始执行其中的my_process
函数。这个函数是一个死循环,只有满足特定条件,才会执行调度函数 my_schedule
。调度函数的代码在myinterrupt.c中:
关于调度函数的进程切换过程的分析,上面的注释部分已经写得很清楚了,这里也就不再赘述。进程的上下文的切换,可以总结成下面2个部分:
将前一个进程此时的esp,eip保存到它的pcb中;将它的ebp保存到系统堆栈中 将cpu中的ebp,esp,eip寄存器值设置为下一个进程的ebp,esp,eip至于上面的中断处理函数 my_time_handler
从名字上就可以看出它是时钟中断处理函数,用于每隔一定的时间设置 my_need_sched
标志位,至于它是怎么让内核在时钟中断时执行它自己的,我猜是用一个模块进行了设置…
另外,关于本次的实验,比较重要的一点是要在正确的目录下make
,在将孟宁老师的github
上面的文件拷贝进来时,我们一般都是在mykernel
的目录下,拷贝完成后,一定要返回到上一层的目录再来make
,否者就会出现找不到目标文件
的错误。最后放上实验中从进程2切换到进程3的截图:
写了这么一大堆,可能还是会有点难以理解,正如孟宁老师说的,“基于mykernel实现的时间片轮转调度代码的理解是有挑战的,它本身就是由Linux内核精简而来的,一时看不懂不必慌张,当然视频里我也没有或者说很难讲清楚,需要大家先分析琢磨一下再讲才有可能理解它”,最重要的还是多思考和多琢磨,有条件的而且有兴趣的同行们可以单击上面网易云课堂的链接来做做实验,更加深入地理解这一块的内容。
新闻热点
疑难解答