基于mykernel2.0编写一个操作系统内核

1. 实验环境

  • 虚拟机VMware Workstation 15 Player

  • Linux

  • 所需文件linux-5.4.34.tar.xzmykernel-2.0_for_linux-5.4.34.patch

  • 配置命令

    xz -d linux-5.4.34.tar.xz
    tar -xvf linux-5.4.34.tar
    cd linux-5.4.34
    patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
    sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
    make defconfig 
    make -j4
    sudo apt install qemu 
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage
    

    主要操作为 1)解压得到的linux内核文件 2)打事先准备好的补丁 3)安装依赖库 4)编译内核 5)启动内核

    成功后QEMU运行结果如下,可以看出CPU在不停地执行,同时时钟中断也在不停的产生。


2.编写内核

  • 分析现有代码:

    通过查看mykernel-2.0_for_linux-5.4.34.patch的代码,很容易发现该补丁对

    linux-5.4.34/arch/x86/kernel/time.c

    linux-5.4.34/include/linux/start_kernel.h

    linux-5.4.34/include/linux/timer.h

    linux-5.4.34/init/main.c

    linux-5.4.34/Makefile

    等文件进行了修改,

    新建了linux-5.4.34/mykernel/myinterrupt.c

    linux-5.4.34/mykernel/mymain.c

    linux-5.4.34/mykernel/README.md等文件。

    其中,最主要的即为myinterrupt.cmymain.c,前者定义了my_timer_handler(),用于在发生时钟中断时进行处理;后者定义了my_start_kernel(),在内核启动后一直运行。

    /* myinterrupt.c */
    void my_timer_handler(void)
    {
    	pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
    }
    
    /* mymain.c */
    void __init my_start_kernel(void)
    {
        int i = 0;
        while(1)
        {
            i++;
            if(i%100000 == 0)
                pr_notice("my_start_kernel here  %d \n",i);
                
        }
    }
    

    基于此,我们才可以在安装完成实验环境并启动内核的时候,观察到不停打印的信息。

    这里还需要关注一个点:周期性产生的时钟中断信号,是由硬件平台自主产生。

  • 基于mykernel2.0编写内核

    1. 在mykernel文件夹下新建mypcb.h来定义进程控制块(PCB):

      /* mypcb.h */
      #define MAX_TASK_NUM        8
      #define KERNEL_STACK_SIZE   1024*2
      /* CPU-specific state of this task */
      struct Thread {
          unsigned long		ip;
          unsigned long		sp;
      };
      
      typedef struct PCB{
          int pid;
          long priority;
          unsigned long need_time;
          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;
          struct PCB *pre;
      }tPCB;
      
      void my_schedule(void);
      

      与孟老师的PCB结构相比,我新增了用于描述进程动态优先级的变量long priority,以及用于表明该进程需要运行多少次时间片的变量unsigned long need_time,以及一个前向指针pre

      其中进程优先级priority设定为每运行一个时间片,就减少1,当优先级降低到不是所有进程中最高时(或者进程执行完成时),才进行进程的切换。

    2. mymain.c中修改my_start_kernel()以实现内核启动时进程的正常运行,新增my_process()以实现进程优先级变化或者进程执行完成时可以正确调用进程切换函数:

      /* mymain.c */
      #include <linux/types.h>
      #include <linux/string.h>
      #include <linux/ctype.h>
      #include <linux/tty.h>
      #include <linux/vmalloc.h>
      
      
      #include "mypcb.h"
      tPCB HEAD;
      tPCB * head = &HEAD;
      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];
          task[pid].priority = MAX_TASK_NUM - pid;
          task[pid].need_time = MAX_TASK_NUM;
          head->next = &task[pid];
          task[pid].pre = head;
          /*fork more process */
          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].pre = &task[i-1];
              task[i-1].next = &task[i];
              task[i].priority = MAX_TASK_NUM - i;
              task[i].need_time = MAX_TASK_NUM;
          }
          task[MAX_TASK_NUM - 1].next = NULL;
          /* start process 0 by task[0] */
          pid = 0;
          my_current_task = &task[pid];
      	asm volatile(
          	"movq %1,%%rsp\n\t" 	/* set task[pid].thread.sp to rsp */
          	"pushq %1\n\t" 	        /* push rbp */
          	"pushq %0\n\t" 	        /* push task[pid].thread.ip */
          	"ret\n\t" 	            /* pop task[pid].thread.ip to rip */
          	: 
          	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
      	);  
      } 
      
      int i = 0;
      struct PCB *tpos = NULL;
      void my_process(void)
      {    
          while(1)
          {   
              if(my_need_sched == 1 && my_current_task != NULL && (my_current_task->need_time == 1 || (my_current_task->next != NULL && my_current_task->priority <= my_current_task->next->priority)))
              {
                  if(--my_current_task->need_time == 0){
                      my_current_task->state = 1;
                      if(my_current_task->next != NULL){
                          my_current_task->pre->next = my_current_task->next;
                          my_current_task->next->pre = my_current_task->pre;
                      }else{
                          head->next = NULL;
                      }
                  }else {                
                      struct PCB *pos = my_current_task;
                      --my_current_task->priority;
                      while(pos->next != NULL && my_current_task->priority <= pos->next->priority){
                          pos = pos->next;
                      }
                      if(pos->next == NULL){
                          my_current_task->pre->next = my_current_task->next;
                          my_current_task->next->pre = my_current_task->pre;
                          pos->next = my_current_task;
                          my_current_task->pre = pos;
                          my_current_task->next = NULL;
                      }else{
                          my_current_task->pre->next = my_current_task->next;
                          my_current_task->next->pre = my_current_task->pre;
                          pos->next->pre = my_current_task;
                          my_current_task->next = pos->next;
                          my_current_task->pre = pos;
                          pos->next = my_current_task;
                      }
                  }
                  my_need_sched = 0;
                  tpos = head;
                    
                  //while(tpos->next != NULL){
                  //    pr_notice("pid:%d need_time:%d priority:%d",tpos->next->pid,(int)tpos->next->need_time,(int)tpos->next->priority);
                  //    tpos = tpos->next;
                  //}
                  my_schedule();          
              } 
              else if(my_need_sched == 1 && my_current_task != NULL && my_current_task->need_time > 1 && my_current_task->next != NULL && my_current_task->priority > my_current_task->next->priority){
                  --my_current_task->need_time;
                  --my_current_task->priority;
                  my_need_sched = 0;
              }
              else if(my_need_sched == 1 && my_current_task == NULL){
                  pr_notice("\n\n>>>>>>>>>>>>>>>>> waiting for process <<<<<<<<<<<<<<<<<<\n\n");
                  my_need_sched = 0;
              }
          }
          
      }
      
      

      在孟老师的基础上,我在my_start_kernel()新增了进程优先级、进程运行所需时间片、前向指针的初始化。

      my_process()新增了四种发生时钟中断时的判断(前两种合在第一个if语句中):

      ①该进程在用完该次时间片后就执行完毕,此时将该进程从进程队列中移走,调用进程切换程序。

      ②该进程在用完该次时间片后就不再满足优先级最高,此时将该进程挪到进程队列中适当的位置,调用进程切换程序。

      ③该进程在用完该次时间片后人仍然满足优先级最高且尚未执行完毕,此时不进行进程切换,仅将该进程的优先级与运行所需时间片数量做自减。

      ④当前没有进程在运行(即当前所有进程都执行完毕),则输出信息waiting for process

    3. myinterrupt.c中修改my_timer_handler()以实现设定定期时钟中断的间隔,新增my_schedule()以实现进程间的切换:

      /* myinterrupt.c */
      #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 tPCB * head;
      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)
      {
          time_count ++;
          if(time_count%2000 == 0 && my_need_sched != 1)
          {
              //printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
              my_need_sched = 1;
          } 
          
          return;  	
      }
      
      void my_schedule(void)
      {
          tPCB * next;
          tPCB * prev;
      
          if(head->next == NULL)
          {
              my_current_task = NULL;
          	return;
          }
          printk(KERN_NOTICE ">>>my_schedule<<<\n");
          /* schedule */
          next = head->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(	
              	"pushq %%rbp\n\t" 	    /* save rbp of prev */
              	"movq %%rsp,%0\n\t" 	/* save rsp of prev */
              	"movq %2,%%rsp\n\t"     /* restore  rsp of next */
              	"movq $1f,%1\n\t"       /* save rip of prev */	
              	"pushq %3\n\t" 
              	"ret\n\t" 	            /* restore  rip of next */
              	"1:\t"                  /* next process start here */
              	"popq %%rbp\n\t"
              	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
              	: "m" (next->thread.sp),"m" (next->thread.ip)
          	); 
          }  
          return;	
      }
      
      

      仅在孟老师的基础上,对进程切换过程中的nextprev做了适当的修改,以适应进程按照动态优先级进行切换。


3.结果分析

  • 初始化进程

    本实验中,我创建了8个进程,pid、优先级、运行所需时间片数量如下:

    属性 进程0 进程1 进程2 进程3 进程4 进程5 进程6 进程7
    PID 0 1 2 3 4 5 6 7
    priority 8 7 6 5 4 3 2 1
    need_time 8 8 8 8 8 8 8 8
  • 内核启动到执行第一次进程切换

    1. 内核启动,执行PID为0的进程;

    2. 经过一个时间片,PID为0的进程优先级与运行所需时间片均减1:

      属性 进程0 进程1 进程2 进程3 进程4 进程5 进程6 进程7
      PID 0 1 2 3 4 5 6 7
      priority 7 7 6 5 4 3 2 1
      need_time 7 8 8 8 8 8 8 8
    3. 此时PID为0的进程尚未执行完且优先级仍然最高,所以不进行进程切换,下一时间片仍然执行PID为0的进程:

      属性 进程0 进程1 进程2 进程3 进程4 进程5 进程6 进程7
      PID 0 1 2 3 4 5 6 7
      priority 6 7 6 5 4 3 2 1
      need_time 6 8 8 8 8 8 8 8
    4. 到第二个时间片运行完成时,PID为0的进程尚未执行完成,但优先级已经不是队列最高,所以需要调整进程队列,并使得进程切换到此时优先级最高的PID为1的进程:

      属性 进程0 进程1 进程2 进程3 进程4 进程5 进程6 进程7
      PID 1 2 0 3 4 5 6 7
      priority 6 6 6 5 4 3 2 1
      need_time 7 8 6 8 8 8 8 8
  • 输出结果

    即进程切换顺序为(按照PID):从0->1->2->0->3->1->2->4->0->3->1......

    当所有进程均执行完成后,输出如下:

    8个进程,每个进程需要8个时间片,在内核启动后128秒左右,进程全部执行完毕。


4.代码分析

/* mymain.c */
asm volatile(
    	"movq %1,%%rsp\n\t" 	/* set task[pid].thread.sp to rsp */
    	"pushq %1\n\t" 	        /* push rbp */
    	"pushq %0\n\t" 	        /* push task[pid].thread.ip */
    	"ret\n\t" 	            /* pop task[pid].thread.ip to rip */
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
	); 

这一段汇编代码是嵌入在my_start_kernel()中执行对PID为0的进程的初始化。

movq %1,%%rsp

是将存储在edx寄存器中的PID为0的进程的sp值传到rsp寄存器。

pushq %1

是将该进程sp的值压栈。

pushq %0

是将该进程ip的值压栈。

ret

此处需要稍微理解一下。因为程序员不能擅自改变rip寄存器内的值,所以不管是初始化第一个进程还是后续的进程切换,都需要用到这个技巧,即先将所需要改变的rip值压栈,再使用ret使得ip值出栈并被放入rip寄存器。

/* myinterrupt.c */
asm volatile(	
        	"pushq %%rbp\n\t" 	    /* save rbp of prev */
        	"movq %%rsp,%0\n\t" 	/* save rsp of prev */
        	"movq %2,%%rsp\n\t"     /* restore  rsp of next */
        	"movq $1f,%1\n\t"       /* save rip of prev */	
        	"pushq %3\n\t" 
        	"ret\n\t" 	            /* restore  rip of next */
        	"1:\t"                  /* next process start here */
        	"popq %%rbp\n\t"
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	); 

这一段汇编代码是嵌入在my_schedule()中,用于实现进程间切换。

pushq %rbp
movq %rsp,0
movq $1f,1

是将前一个进程的rbp、rsp、rip值保存到内存中。

movq 2,rsp
pushq 3
ret

是把后一个进程的rsp、rip保存到相应的寄存器中。

1:
popq %rbp

是为了被中断的前一个进程,可以在下一次被调度到时准确恢复环境所作的准备。其中1:所在的地址就是已经被保存在prev->thread.ip中的$1f


5.总结

  • 收获

    对于linux内核有了初步的认识,对于汇编也有了一定的了解,之前学过的操作系统知识得到了温故。

  • 反思

    虽然在实验过程中将原先的按序切换进程,改成了按照动态优先级切换进程,但是改的太粗糙,只是在C代码的层次上做了一点改变,没有做到如某一进程结束后就释放相应的内存资源与堆栈资源等操作,也无法做到在CPU空闲时创建新的进程。

posted @ 2020-05-11 02:40  winkkkk  阅读(115)  评论(0)    收藏  举报