张建帮 原创作品转载请注明出处 《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中:
extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;
/*
* Called by timer interrupt.
* it runs in the name of current running process,
* so it use kernel stack of current running process
*/
void my_timer_handler(void)
{
#if 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;
}
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;
//如果下一个进程的状态为0,则说明它之前执行过,因此也就不用设置
if(next->state == 0)
{
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
/* switch to next process */
asm volatile(
"pushl %%ebp\n\t" /* 保存前一个进程的ebp到系统堆栈中 */
"movl %%esp,%0\n\t" /* 保存前一个进程的esp到该进程的pcb */
"movl %2,%%esp\n\t" /* 重新设置esp,使其指向下一个进程的esp */
"movl $1f,%1\n\t" /* save eip $1f就是指标号1:的代码在内存中存储的地址,保存下一个指令的地址到pcb中 */
"pushl %3\n\t" /* 和下面的指令一起将eip设置为下一个进程的ip,即下一条指令的地址 */
"ret\n\t"
"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)
);
}
else //如果进程还从来没有被执行过
{
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"
"movl %%esp,%0\n\t"
"movl %2,%%esp\n\t"
"movl %2,%%ebp\n\t" /* 和上面的汇编代码相比,唯一多出来的代码,用于设置本进程的ebp */
"movl $1f,%1\n\t"
"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;
}
关于调度函数的进程切换过程的分析,上面的注释部分已经写得很清楚了,这里也就不再赘述。进程的上下文的切换,可以总结成下面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内核精简而来的,一时看不懂不必慌张,当然视频里我也没有或者说很难讲清楚,需要大家先分析琢磨一下再讲才有可能理解它”,最重要的还是多思考和多琢磨,有条件的而且有兴趣的同行们可以单击上面网易云课堂的链接来做做实验,更加深入地理解这一块的内容。