一、配置mykernel 2.0
按照 https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
环境:VMware Workstation 15 pro ,Ubuntu 18.04.4 LTS;
1. 配置前先拍快照,保存当前状态备用。
2. 按照 https://github.com/mengning/mykernel 的说明配置mykernel 2.0,执行以下命令。
wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch sudo apt install axel axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz 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 # Default configuration is based on 'x86_64_defconfig' make -j$(nproc) sudo apt install qemu # install QEMU qemu-system-x86_64 -kernel arch/x86/boot/bzImage
执行完上述代码后,可以看到QEMU窗口中my_start_kernel进程周期性的调用my_handler_here方法,如下图所示。
在Linux-5.3.34 内核源代码根目录下进入mykernel目录,可以看到QEMU窗口输出的内容的代码 mymain.c 和 myinterrupt.c ,当前有一个虚拟的CPU执行C代码的上下文环境,可以看到 mymain.c 中的代码在不停地执行。同时有一个中断处理程序的上下文环境,周期性地产生的时钟中断信号,能够触发myinterrupt.c中的代码。这样就通过Linux内核代码模拟了一个具有时钟中断和C代码执行环境的硬件平台。
void __init my_start_kernel(void) { int i = 0; while(1) { i++; if(i%100000 == 0) printk(KERN_NOTICE "my_start_kernel here %d \n",i); } }
观察上述代码可知:每当 i 能够被 100000 整除 就输出 “my_start_kernel here” ,即相当于一个时钟信号。下面的代码就是进行处理时钟中断的。
void my_timer_handler(void) { printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
二、编写一个操作系统内核并简要分析
参照 https://github.com/mengning/mykernel 提供的范例代码,基于mykernel 2.0编写一个操作系统内核。
在 https://github.com/mengning/mykernel 中 下载 mypcb.h,myinterrupt.c和mymain.c文件,并拷贝到本机的 mykernel 目录下,并新增头文件mypcb.h,修改好文件后重新配置编译内核,并使用QEMU加载,结果如下图:
make allnoconfig
make
qemu -kernel arch/x86/boot/bzImage
观察可知上图中正在进行进程的切换:进程2 切换到 进程3,进程切换的关键代码是一段嵌入式汇编,最有技巧性的地方是通过pushq %rip和ret指令来间接修改%rip的值,从而更改代码执行流,再配合%rsp和%rbp的修改切换进程的工作栈,从而达到切换进程的目的。
简单分析:
首先在mykernel目录下增加一个mypcb.h 头文件,用来定义进程控制块PCB(Process Control Block),也就是进程结构体的定义,在Linux内核中是struct tast_struct结构体。
1 /* 2 * linux/mykernel/mypcb.h 3 */ 4 5 #define MAX_TASK_NUM 4 6 #define KERNEL_STACK_SIZE 1024*8 7 8 /* CPU-specific state of this task */ 9 struct Thread { 10 unsigned long ip; 11 unsigned long sp; 12 }; 13 14 typedef struct PCB{ 15 int pid; 16 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 17 char stack[KERNEL_STACK_SIZE]; 18 /* CPU-specific state of this task */ 19 struct Thread thread; 20 unsigned long task_entry; 21 struct PCB *next; 22 }tPCB; 23 24 void my_schedule(void);
其中:pid表示进程号;state表示进程状态,在模拟系统中,所有进程控制块信息都会被创建出来,其初始化值就是-1,如果被调度运行起来,其值就会变成0;stack是进程使用的堆栈,栈大小为1024*8;task_entry为进程入口函数;*next:指向下一个PCB,此模拟系统中的PCB是以链表的形式组织起来的;函数的声明 my_schedule,它的实现在my_interrupt.c中,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。
对mymain.c进行修改,初始化各个进程并启动0号进程,这里是mykernel内核代码的入口,负责初始化内核的各个组成部分。在Linux内核源代码中,实际的内核入口是init/main.c中的start_kernel(void)函数。
1 /* 2 * linux/mykernel/mymain.c 3 */ 4 5 #include "mypcb.h" 6 7 tPCB task[MAX_TASK_NUM]; 8 tPCB * my_current_task = NULL; 9 volatile int my_need_sched = 0; 10 11 void my_process(void); 12 13 void __init my_start_kernel(void) 14 { 15 int pid = 0; 16 int i; 17 /* Initialize process 0*/ 18 task[pid].pid = pid; 19 task[pid].state = 0; /* -1 unrunnable, 0 runnable, >0 stopped */ 20 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 21 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 22 task[pid].next = &task[pid]; 23 /*fork more process */ 24 for(i=1;i) 25 { 26 memcpy(&task[i],&task[0],sizeof(tPCB)); 27 task[i].pid = i; 28 task[i].state = -1; 29 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 30 task[i].next = task[i-1].next; 31 task[i-1].next = &task[i]; 32 } 33 /* start process 0 by task[0] */ 34 pid = 0; 35 my_current_task = &task[pid]; 36 asm volatile( 37 "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ 38 "pushq %1\n\t" /* push rbp */ 39 "pushq %0\n\t" /* push task[pid].thread.ip */ 40 "ret\n\t" /* pop task[pid].thread.ip to rip */ 41 : 42 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 43 ); 44 }
进程调度为一个环形队列,每次创建新的进程时,都将该进程插入到队尾,然后将该进程指向第一个进程。
在mymain.c中添加了my_process函数,用来作为进程的代码模拟一个个进程,只是我们这里采用的是进程运行完一个时间片后主动让出CPU的方式(简单的时间片轮转方式的进程切换),没有采用中断的时机完成进程切换,因为中断机制实现起来较为复杂,等后续部分再逐渐深入。
1 void my_process(void) 2 { 3 int i = 0; 4 while(1) 5 { 6 i++; 7 if(i%10000000 == 0) 8 { 9 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 10 if(my_need_sched == 1) 11 { 12 my_need_sched = 0; 13 my_schedule(); 14 } 15 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 16 } 17 } 18 }
进程运行过程中是怎么知道时间片消耗完了呢?这就需要时钟中断处理过程中记录时间片。对myinterrupt.c中修改my_timer_handler用来记录时间片。
1 /* 2 * linux/mykernel/myinterrupt.c 3 */ 4 #include "mypcb.h" 5 6 extern tPCB task[MAX_TASK_NUM]; 7 extern tPCB * my_current_task; 8 extern volatile int my_need_sched; 9 volatile int time_count = 0; 10 11 /* 12 * Called by timer interrupt. 13 */ 14 void my_timer_handler(void) 15 { 16 if(time_count%1000 == 0 && my_need_sched != 1) 17 { 18 printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 19 my_need_sched = 1; 20 } 21 time_count ++ ; 22 return; 23 }
对myinterrupt.c进行修改,主要是增加了进程切换的代码my_schedule(void)函数,在Linux内核源代码中对应的是schedule(void)函数。
1 void my_schedule(void) 2 { 3 tPCB * next; 4 tPCB * prev; 5 6 7 if(my_current_task == NULL 8 || my_current_task->next == NULL) 9 { 10 return; 11 } 12 printk(KERN_NOTICE ">>>my_schedule<<<\n"); 13 /* schedule */ 14 next = my_current_task->next; 15 prev = my_current_task; 16 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 17 { 18 my_current_task = next; 19 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 20 /* switch to next process */ 21 asm volatile( 22 "pushq %%rbp\n\t" /* save rbp of prev */ 23 "movq %%rsp,%0\n\t" /* save rsp of prev */ 24 "movq %2,%%rsp\n\t" /* restore rsp of next */ 25 "movq $1f,%1\n\t" /* save rip of prev */ 26 "pushq %3\n\t" 27 "ret\n\t" /* restore rip of next */ 28 "1:\t" /* next process start here */ 29 "popq %%rbp\n\t" 30 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 31 : "m" (next->thread.sp),"m" (next->thread.ip) 32 ); 33 } 34 return; 35 }
my_time_handler中断处理程序,该函数每隔1000 判断 my_need_sched 是否不等于1,如果是则将其置为1,使 myprocess 执行 my_schedule() 。my_schedule 函数在进程队列中选择下一个要执行的进程;对于处于不同状态的进程,调度方式也不同,如果即将上CPU的进程之前已经运行过(即state为0),我们需要保存当前进程的上下文信息,然后把下一个进程的信息写入到寄存器中,执行 ret 使下一个进程开始执行。之前没有在运行态的(state不为0),我们先将其设置为运行态,我们这里需要初始化其ebp,因为该进程的堆栈是空栈 。
参考资料:
1. https://github.com/mengning/mykernel/blob/master/README.md
2. 计算机系统的基本工作原理
3. 自己动手写一个操作系统内核