linux实验:基于mykernel的一个简单的时间片轮转多道程序内核代码分析
学号 029
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/
实验准备
对实验开始进行的准备,主要为安装qemu 与Kernel
个人环境为 vm+ubantu 16.04虚拟机
- 安装 qemu
sudo apt-get install qemu
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu #连接目录
- 安装Kernel
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # 下载Linux Kernel 3.9.4
wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # 下载孟老师提供的补丁
- 解压并且打补丁
xz -d linux-3.9.4.tar.xz
tar -xvf linux-3.9.4.tar
cd linux-3.9.4
patch -p1 < ../mykernel_for_linux3.9.4sc.patch
- 编译内核代码
make allnoconfig
make
在这里可能会出现错误(emmmm,我出现了o(╥﹏╥)o)
这里主要是内核版本与编译器 版本的不匹配,修改方法就是将已有的gcc文件拷贝进去,如下
5. 使用 qemu观看运行
qemu -kernel arch/x86/boot/bzImage
运行结果如下
观看myKernel中的mymain.c文件
与myinterrupt.c文件
可以看出这个QEMU显示的内容主要是由 myinterrupt.c与mymain.c循环执行显示,即不断执行my_start_kernel与间接的调用my_timer_handler函数。
时间片轮转多道程序实现
- 修改mykernel中的文件
这一步主要感谢孟宁老师,已经提供了mymain.c、myinterrupt.c、mypcb.h三个文件来供我们使用。
去https://github.com/mengning/mykernel来进行下载替换原文件夹中的内容。 - 重新进行内核编译
make clean #清除原来编译内容
make allnoconfig
make
- 使用qemu观看运行情况
代码分析
- mypcb.h
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*2 # unsigned long
/* 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 */
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);
增加了一个mypcb.h头文件,这个头文件是用来定义进程控制块
state :进程状态,初始值为-1,运行时为0,停止为大于0。
PCB *next 表示指向下一个PCB
2. mymain.c
tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;
这里面 my_current_task 表明当前进程的指针
my_need_sched是一个进程状态转换的信号,当其为1时,表明进程状态转换。
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].next = &task[pid];
这一段主要是对pid=0的进程0初始化,紧接着后面的一段代码
for(i=1;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[0],sizeof(tPCB));
task[i].pid = i;
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];
}
pid = 0;
my_current_task = &task[pid];
asm volatile(
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
"pushl %1\n\t" /* push ebp */
"pushl %0\n\t" /* push task[pid].thread.ip */
"ret\n\t" /* pop task[pid].thread.ip to eip */
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
是按照初始化0号进程的方式陆续初始化更多进程。
这里使用了内联汇编代码,表明进程0的堆栈与相关寄存器的变化过程,1.将原进程堆栈栈顶的地址存入esp寄存器, 2.然后将当前ebp寄存器值入栈, 3.将当前进程的eip入栈,4.ret命令让入栈进程eip保存到eip寄存器中
void my_process(void)
{
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);
}
}
}
这个函数的作用是每过10000000次进行一次检查,如果my_need_sched 为1 执行myinterrupt中的 my_schedule()函数,并且将my_need_sched回复1,不然的话就一直输出当前进程的pid(即进程号)
3. myinterrupt.c
extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;
使用了mymain.c中定义的当前工作进程指针,工作进程大小,状态转换指针以及自己执行的计数指针,
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;
}
这里面是一个进程中断函数,使用了开头的time_count计数器,每次加1,当到1000是,将全局变量my_need_sched改为1,其中 mymain.c中的my_process函数执行就会检测到,从而执行myinterrupt.c中的my_schedule函数
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;
prev = my_current_task;
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
{
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)
);
}
return;
}
my_schedule()主要是实现了时间片的轮转,定义了:next 当前进程的下一个进程,prev,当前进程
内联汇编代码主要是写两个进程的调度工作
1.保存当前EBP到堆栈中,
2.保存当前esp到当前进程PCB中,
3.将next进程中的堆栈栈顶的值存入esp寄存器中,切换进程
4.保存当前的EIP值,下次回复进程后将在标号1开始执行,
5.将next进程继续执行的代码位置压栈,即将下一个进程的函数地址入栈,切换进程,
6.出栈标号1到EIP寄存器,说明接下来执行next进程
7.标号1,为next进程开始执行的位置打上标签,为之后切换进程做准备 8.回复EBP寄存器的值,然后准备开始next进程。
总结
对这次实验的总结,这次试验对我来说是一次很好的认识系统内部进程的调度的机会,汇编代码接触的很少,确实需要不断的看书来学习才能弄懂,这次的实验重点是理解时间轮转终端与进程调度,即进程在执行的过程中,当时间片用完,进行进程切换时,需要保存当前进程执行环境,然后在进程下一次调用时恢复,最终实现操作系统进程的切换与多道程序的并发处理。