Linux操作系统分析实验------linux kernel3.9.4环境部署及基于mykernel的时间片轮转多道程序实现与分析
学号:508。原创作品转载请注明出处,中国科学技术大学孟宁老师的Linux操作系统分析 https://github.com/mengning/linuxkernel/
一、环境搭建
-
虚拟机:VMware® Workstation 14 Pro-14.1.3 build-9474260
操作系统:ubuntu-18.04.2-desktop-amd64.iso
搭建过程中遇到的问题,都在第7点中统一解释。 -
创建该作业的文件夹LinuxHW:
-
下载linux kernel3.9.4和孟宁老师的补丁mykernel_for_linux3.9.4sc.patch
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
并用
tar xvf linux-3.9.4.jar
进行解压缩得到linux-3.9.4
文件。
-
开始打补丁包:先进入解压缩后的文件,在利用下面的指令打补丁:
saltwind@ubuntu:~/LinuxHW$ cd linux-3.9.4 saltwind@ubuntu:~/LinuxHW/linux-3.9.4$ patch -p1 < ../mykernel_for_linux3.9.4sc.patch
-
进行编译:
- 先复位:
make allnoconfig
- 再编译:
make
- 先复位:
-
安装qumu:
sudo apt-get install qemu
,安装完成后,就可以测试内核是否能够正确运行了:qemu -kernel arch/x86/boot/bzImage
-
搭建过程问题总结:
-
没有gcc:
sudo apt-get updat sudo apt-get install gcc //若上两步不能正确安装,则再进行下面两步 sudo apt-get update --fix-missing sudo apt-get install gcc
-
make
编译时报错,fatal error: linux/compiler-gcc7.h: No such file or directory
,cd inculde/linux cp compiler-gcc4.h compiler-gcc7.h
-
安装qemu出错,同上面gcc处理方式一样。
-
二、实验操作
这里主要就是实现进程间的时间片轮转。
-
我们利用孟宁老师提供的源码:将mykernel-master重命名为mykernel,并替换掉linux-3.9.4里面的mykernel。
-
然后重新进行编译运行:
cd .. make allnoconfig make qemu -kernel arch/x86/boot/bzImage
然后看到如下运行结果,这里已经从process 1跳转到了precess 2:
-
下面进行源代码的分析,这里主要涉及三个文件:mypcb.h、mymain.c、myinterrupt.c
-
mypcb.h,先上代码
/* * linux/mykernel/mypcb.h * * Kernel internal PCB types * * Copyright (C) 2013 Mengning * */ #define MAX_TASK_NUM 4 #define KERNEL_STACK_SIZE (unsigned long)1024*2 /* 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);
在该文件中,有Thread结构体,主要用于存储当前进程中正在执行的线程的IP和SP。PCB结构体中各个字段含义:
- pid:进程号
- state:进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0
- stack:进程使用的堆栈
- thread:当前正在执行的线程信息
- task_entry:进程入口函数
- next:指向下一个PCB,模拟系统中所有的PCB是以链表的形式组织起来的
-
mymain.c文件
/* * linux/mykernel/mymain.c * * Kernel internal my_start_kernel * * Copyright (C) 2013 Mengning * */ #include <linux/types.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/tty.h> #include <linux/vmalloc.h> #include "mypcb.h" tPCB task[MAX_TASK_NUM]; tPCB * my_current_task = NULL; volatile int my_need_sched = 0; void my_process(void); void __init my_start_kernel(void) { int pid = 0; int i; /* Initialize process 0*/ 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]; /*fork more process */ for(i=1;i<MAX_TASK_NUM;i++) { memcpy(&task[i],&task[0],sizeof(tPCB)); task[i].pid = i; //*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-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] */ 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*/ ); } int i = 0; 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); } } }
正如前文所述,这里的函数 my_start_kernel 是系统启动后,最先调用的函数,在这个函数里完成了0号进程的初始化和启动,并创建了其它的进程PCB,以方便后面的调度。在模拟系统里,每个进程的函数代码都是一样的,即 my_process 函数,my_process 在执行的时候,会打印出当前进程的 id,从而使得我们能够看到当前哪个进程正在执行。
另外,在 my_process 也会检查一个全局标志变量 my_need_sched,一旦发现其值为 1 ,就调用 my_schedule 完成进程的调度 -
myinterrupt.c
/* * linux/mykernel/myinterrupt.c * * Kernel internal my_timer_handler * * Copyright (C) 2013 Mengning * */ #include <linux/types.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/tty.h> #include <linux/vmalloc.h> #include "mypcb.h" 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; 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_timer_handler 函数会被内核周期性的调用,每调用1000次,就去将全局变量my_need_sched的值修改为1,通知正在执行的进程执行调度程序my_schedule。在my_schedule函数中,完成进程的切换。进程的切换分两种情况,一种情况是下一个进程没有被调度过,另外一种情况是下一个进程被调度过,可以通过下一个进程的state知道其状态。进程切换依然是通过内联汇编代码实现,无非是保存旧进程的eip和堆栈,将新进程的eip和堆栈的值存入对应的寄存器中,详见代码中的注释。
-
三、总结
在孟宁老师的指导下,进行本次实验后,我学习到操作系统的kernel主要就是中断机制和进程调度,通过软硬件的相互配合实现多任务处理。