红联Linux门户
Linux帮助

Linux内核分析(二)—mykernel内核部署及简单时间片轮转程序分析

发布时间:2017-03-06 11:12:23来源:study.163.com作者:付何山
一、部署mykernel
系统:Ubuntu 16.04 64位版本
前提条件:Git 以及部分其他原装Ubuntu中未添加的命令(本部署中涉及的命令应均可通过sudo apt-get install + 你想安装的命令)
部署方案及代码地址
按照孟宁老师的部署方案一步步执行即可完成部署。
需要注意的一点是make的时候可能会产生错误,在/include/linux下找不到compiler-gcc5.h导致make失败。这是因为mykernel是基于linux原来的3.9.4内核写的,当时gcc的版本还没有到5,在对应的文件夹下只有compiler-gcc4.h compiler-gcc3.h compiler-gcc.h。因此需要自己下载compiler-gcc5.h并将它放在make目录的文件夹中的/include/linux中,注意不要错放在系统/usr/include下了。
放好compiler-gcc5.h再运行make即可成功make,接下来使用qemu -kernel arch/x86/boot/bzImage即可查看文件的输出结果,如下图。
Linux内核分析(二)—mykernel内核部署及简单时间片轮转程序分析
可以看到如部署文件最后所言,my_start_kernel在执行,同时my_timer_handler时钟中断处理程序周期性执行。
而后下载代码库中的mymain.c myinterrupt.c mypcb.h(注意现在代码库的版本是数天前修改的有bug版本,请使用git版本管理查看旧版本并下载),覆盖mykernel文件夹中的mymain.c myinterrupt.c。返回上级目录,make,然后使用qemu -kernel arch/x86/boot/bzImage即可看到如下图的输出结果。(图中为进程3切换至进程0的情况)
Linux内核分析(二)—mykernel内核部署及简单时间片轮转程序分析
 
二、简单时间片轮转多道程序内核代码分析
如上所述,本程序完成的功能就是一个的可实现简单时间片轮转的多道程序的内核。下面将分析实际实现以上功能的mypcb.h、myinterrupt.c以及mymain.c三个程序。
1、mypcb.h
mypcb.h文件内主要是对进程的PCB进行数据结构的定义,一些常量的定义,以及my_schedule函数的声明。这些定义会包含在下面的两个代码文件中。mypcb.h具体代码如下:
#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*2
struct Thread {
unsigned long       ip;    //eip
unsigned long       sp;    //esp
};
typedef struct PCB{
int pid;
volatile long state;        //-1 means unrunnable, 0 means runnable, >0 means stopped
unsigned long 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);
宏:
MAX_TASK_NUM 代表最大进程数为4
KERNEL_STACK_SIZE 代表每个进程堆栈的大小
PCB:
pid 进程的唯一标识,用于区分进程
state 进程当前的状态,之所以声明为volatile是希望编译器不要对其进行优化,保证每次都能从内存中获取此值。
stack[KERNEL_STACK_SIZE] 进程的堆栈
thread 进程的ip(程序指针)和sp(栈顶指针)
task_entry 进程的入口
next 下一个PCB,将所有PCB连起来,形成环,从而不断循环切换进程。
2、mymain.c
前16行是一些头文件以及mypcb.h文件的包含。
而后定义了一个PCB的数组task,一个指向当前运行进程的my_current_task指针,以及保存进程是否需要调度的my_need_sched,当其值为1时表示进程需要调度,之所以声明为volatile也是因为希望拒绝编译器对其进行不必要的优化,保证该参数的有效性。
接下来__init my_start_kernel进行所有PCB的初始化。具体细节不做解释。主要流程是对每个进程的PCB各个属性进行赋值,并将所有PCB通过next指针链接。初始化完成后,使用asm(内嵌汇编代码)将当前进程,0号进程装入内存中。每个进程的入口地址均是下面定义的my_process函数。关于内嵌asm的具体信息可参见孟宁老师课程GCC相关文档
P.S.此处有一个不理解的地方在于,for循环中对所有task进行初始化时使用了memcpy直接将已经初始化的0号进程信息复制到其他进程上,而后对特异性的信息进行修改。但是后来尝试将memcpy删除,直接对进程的每一个属性按照0号进程信息进行赋值,却无法正常运行。(一般是一轮后,即1号进程到3号进程再到0号进程时出错)这点有些摸不着头脑,希望有了解的人可以留言告知。
my_process是一个死循环,即每循环一千万次输出信息,查看是否有调度需求,若有,进行调度,调度后输出调度后的进程的信息。若无,输出本进程信息。调度函数为my_schedule。
2、myinterrupt.c
代码17-20行的声明中,前三个都是在mymain.c中定义的变量,是外部声明,只有time_count是定义,模拟需要更改进程是否调度的时间片。即下面的my_timer_handler:
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;
}
即当循环一千次,且调度位为0时,将其置1,使得my_main.c中的my_process可以进行主动调度。
my_schedule是整个程序的关键,它实现的是进程的上下文切换,简单的说主要逻辑是将前一个进程的相关信息保存,并读入下一个进程的相关信息,从而完成进程的切换。
这里有两种情况,第一种情况是该进程已经执行过(即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"   /* save ebp */
"movl %%esp,%0\n\t"     /* save esp */
"movl %2,%%esp\n\t"     /* restore  esp */
"movl $1f,%1\n\t"       /* save eip */
"pushl %3\n\t"
"ret\n\t"           /* restore  eip */
"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)
);
可以将进程的切换看作上一节相应的函数调用,即先后保存当前进程的ebp和esp,然后从新进程断点恢复esp,保存当前进程的eip,读入新进程的eip。
第二种情况,进程尚未执行过(即state不为0),则执行下面的代码进行进程切换,与上面的代码不同的是,这里还对新进程的ebp进行了赋值,将新进程的sp赋给了esp和ebp。
next -> state = 0;
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n", prev -> pid, next -> pid);
asm volatile(
"pushl %%ebp\n\t"
"movl %%esp, %0\n\t"
"movl %2, %%esp\n\t"
"movl %2, %%ebp\n\t"
"movl $1f, %1\n\t"
"pushl %3\n\t"
"ret\n\t"
: "=m" (prev -> thread.sp), "=m" (prev -> thread.ip)
: "m" (next -> thread.sp), "m" (next -> thread.ip)
);
这两种情况最令人迷惑的便是movl $1f, %1\n\t这条指令。$1f的意思是向后寻找标号为1的地方,这个地方在第一种情况的语句中,在第二种情况的语句中没有。于是我很好奇这个1是否指向同一个地方,于是使用objdump -s -d myinterrupt.o对生成的myinterrupt.o文件进行查看。结果如下图所示:
Linux内核分析(二)—mykernel内核部署及简单时间片轮转程序分析
可见两者均被汇编为movl $0x12f, 0x2008(%esi),可见两者所指的位置是相同的(因为esi在最上方被定义为0)。奇异的是使用mov $1b, %1时make会报错说无法找到标号1,且在新进程state不为0的情况中,即使不写这一句也会得到正确的程序结果。所以这里其实还是不太清楚。
 
三、实验心得
综上,这次实验的配置不算难,整体的思想也不太难,弄清楚了进程切换后就觉得并不难,但是细节的理解还是有些困难的。大体的理解是清晰而容易的,但是细节上的涉及到一些编程语言细节的东西,使用搜索引擎以及仔细查看老师的课程也无法得到很好的解释。这说明自身水平仍然不够,仍需要加强。希望今后的课程能更加努力。
 
本文永久更新地址:http://www.linuxdiyf.com/linux/28950.html