1.首先是配置mykernel 2.0,并熟悉Linux内核的编译
本机环境是VirtualBoxVM6.0.20+Ubuntu 16.04.2,首先是按照孟老师在README.md上面提供的指导,依次配置好linux5.4.34内核代码、mykernel补丁、相对应的依赖库文件和qemu仿真系统,以下是课程PPT里面的命令,逐条执行即可:
1 wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch 2 sudo apt install axel 3 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz 4 xz -d linux-5.4.34.tar.xz 5 tar -xvf linux-5.4.34.tar 6 cd linux-5.4.34 7 patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch 8 sudo apt install build-essential gcc-multilib 9 sudo apt install qemu # install QEMU 10 sudo apt install libncurses5-dev bison flflex libssl-dev libelf-dev 11 make defconfifig # Default confifiguration is based on 'x86_64_defconfifig' 12 make -j$(nproc) 13 qemu-system-x86_64 -kernel arch/x86/boot/bzImage
在本机环境下,linux-5.4.34内核源代码位于目录/home/hesetone/mylinux/linux-5.4.34位置。
在上面的命令中,axel是一个多线程高速下载命令,支持多种文件传输协议,可以从多个地址或者与一个地址建立多个连接来下载同一个文件。第10行的install目录中,README.md里面的libncurses-dev似乎是不起作用的,执行sudo apt install libncurses-dev之后,qemu启动的时候,会莫名其妙卡住崩溃,无法正常模拟出结果,但是换成libncurses5-dev之后就可以正常复现。
其次,make defconfig命令的作用是按照默认的配置文件.arch/arm64/configs/defconfig对内核进行配置,在当前目录下生成.config用作初始化配置。用ls -a命令可以在编译完成配置文件之后查看到该文件。最后,make -j$(nproc)命令作用是编译内核,这里nproc是一个内核参数,指系统上的最大进程数。$(nproc)是获取安装系统的该内核参数,指定该参数,完成内核编译。
执行qemu-system-x86_64 -kernel arch/x86/boot/bzImage正常复现的结果如下:
2.基于mykernel 2.0编写一个操作系统内核,实现进程的切换。
第一步是在目录./mylinux/linux-5.4.34/mykernel/目录下增加一个头文件mypcb.h,并且在mymain.c和myinterrupt.c中都include进去,它用于定义进程控制块的结构,后面会有各个进程控制块内容的初始化步骤,在Linux内核中,实际上是struct task_struct结构体。在struct PCB中,包含进程标识符pid、进程状态state(-1表示阻塞状态,0表示正在运行中,大于0则表示停止执行)、栈空间stack、指示CPU特征状态(sp和ip)的线程结构体thread、进程入口task_entry以及指向下一个进程的指针*next,最后的指针,说明进程是以链表的形式组织起来的,mypcb.h内容如下。
1 #define MAX_TASK_NUM 16 2 #define KERNEL_STACK_SIZE 1024 3 4 /* CPU-specific state of this task */ 5 struct Thread 6 { 7 unsigned long ip; 8 unsigned long sp; 9 }; 10 11 typedef struct PCB 12 { 13 int pid; 14 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 15 char stack[KERNEL_STACK_SIZE]; 16 /* CPU-specific state of this task */ 17 struct Thread thread; 18 unsigned long task_entry; 19 struct PCB *next; 20 } tPCB; 21 22 void my_schedule(void);
第二步是对mymain.c进⾏修改,mymain.c是mykernel内核代码的⼊⼝,我们在这里对内核的各个组成部分进行初始化操作,在函数my_start_kernel中,首先是初始化第0个进程,然后开始对1~MAX_TASK_NUM中的进程依次进行初始化,其中进程状态初始化为0,表示所有进程都处在就绪状态等待调用,且所有的进程初始化之后成为一个循环链表。在mymain.c中添加了my_process函数,用于模拟进程执行的过程。此外,my_need_sched变量初始化为0,但是因为my_time_handler函数会定期更新其为1,表示需要调度新的进程,调度完毕之后,重新切换为0,执行1个时间片,依次循环,mymain.c内容如下:
1 #include "mypcb.h" 2 3 tPCB task[MAX_TASK_NUM]; /*task数组*/ 4 tPCB *my_current_task = NULL; /*指向当前进程的指针8*/ 5 volatile int my_need_sched = 0; /*是否需要调度,1表示需要,0表示不需要*/ 6 7 void __init my_start_kernel(void) 8 { 9 int pid = 0; 10 int i; 11 /* Initialize process 0*/ 12 task[pid].pid = pid; 13 task[pid].state = 0; /* -1 unrunnable, 0 runnable, >0 stopped */ 14 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 15 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE - 1]; 16 task[pid].next = &task[pid]; 17 /*fork more process */ 18 for (i = 1; i < MAX_TASK_NUM; i++) 19 { 20 memcpy(&task[i], &task[0], sizeof(tPCB)); 21 task[i].pid = i; 22 task[i].state = 0; 23 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE - 1]; 24 task[i].next = task[i - 1].next; 25 task[i - 1].next = &task[i]; 26 } 27 /* start process 0 by task[0] */ 28 pid = 0; 29 my_current_task = &task[pid]; 30 asm volatile( 31 "movq %1,%%rsp\n\t" /* set task[pid].thread.sp to rsp */ 32 "pushq %1\n\t" /* push rbp */ 33 "pushq %0\n\t" /* push task[pid].thread.ip */ 34 "ret\n\t" /* pop task[pid].thread.ip to rip */ 35 : 36 : "c"(task[pid].thread.ip), "d"(task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 37 ); 38 } 39 40 void my_process(void) 41 { 42 int i = 0; 43 while (1) 44 { 45 i++; 46 if (i % 10000000 == 0) 47 { 48 printk(KERN_NOTICE "this is process %d -\n", my_current_task->pid); 49 if (my_need_sched == 1) /*表示需要从当前进程切换到下一个进程*/ 50 { 51 my_need_sched = 0; /*重新置0,执行一个时间片*/ 52 my_schedule(); /*切换进程*/ 53 } 54 printk(KERN_NOTICE "this is process %d +\n", my_current_task->pid); 55 } 56 } 57 }
第三步是myinterrupt.c的内容,进程运⾏过程使用时钟中断处理过程记录时间⽚,每次time_count % 1000 == 0的时候,就置my_need_sched为1,表示当前进程时间片用完,需要调度下一个进程。当next->state为0的时候,表示下一个进程可以被调度,只要先将指向当前进程的指针指向下一个进程,然后保存上一个进程的rsp和rip,重新设置rsp和rip指向当前进程的栈空间,就完成了进程的切换。对myinterrupt.c中修改如下:
1 extern tPCB task[MAX_TASK_NUM]; 2 extern tPCB *my_current_task; 3 extern volatile int my_need_sched; 4 volatile int time_count = 0; 5 6 /* 7 * Called by timer interrupt. 8 */ 9 void my_timer_handler(void) 10 { 11 if (time_count % 1000 == 0 && my_need_sched != 1) 12 { 13 printk(KERN_NOTICE ">>>my_timer_handler here<<<\n"); 14 my_need_sched = 1; /*执行一个时间片之后,重新置my_need_sched为1,因为当前进程时间片用完,需要调度下一个进程*/ 15 } 16 time_count++; 17 return; 18 } 19 20 void my_schedule(void) 21 { 22 tPCB *next; 23 tPCB *prev; 24 printk(KERN_NOTICE ">>>my_schedule<<<\n"); 25 /* schedule */ 26 next = my_current_task->next; 27 prev = my_current_task; 28 if (next->state == 0) /* -1 unrunnable, 0 runnable, >0 stopped */ 29 { 30 my_current_task = next; /*将当前进程指针指向下一个进程,表示调度下一个进程*/ 31 printk(KERN_NOTICE ">>>switch %d to %d<<<\n", prev->pid, next->pid); 32 /* switch to next process */ 33 asm volatile( 34 "pushq %%rbp\n\t" /* save rbp of prev */ 35 "movq %%rsp,%0\n\t" /* save rsp of prev */ 36 "movq %2,%%rsp\n\t" /* restore rsp of next */ 37 "movq $1f,%1\n\t" /* save rip of prev */ 38 "pushq %3\n\t" 39 "ret\n\t" /* restore rip of next */ 40 "1:\t" /* next process start here */ 41 "popq %%rbp\n\t" 42 : "=m"(prev->thread.sp), "=m"(prev->thread.ip) 43 : "m"(next->thread.sp), "m"(next->thread.ip) 44 ); 45 } 46 return; 47 }
最后,修改完毕,重新执行make命令,编译内核,得到进程切换成功的复现结果如下:
3.简要分析操作系统内核核心功能及运行工作机制
操作系统内核一个核心的功能是进程管理,包括进程的创建与销毁、处理与外部设备的联系、进程间通信等等,最主要的进程管理操作主要有实现进程间的切换执行、对各种设备的调度使用以及CPU的共享,以下以进程调度为例阐述进程切换机制:
首先是第一个进程启动执行的步骤,右侧是该过程的栈空间以及对应寄存器值变化图:
1 asm volatile( 2 "movq %1,%%rsp\n\t" /* 将进程原堆栈栈顶的地址存⼊RSP寄存器 */ 3 "pushq %1\n\t" /* 将当前RBP寄存器值压栈 */ 4 "pushq %0\n\t" /* 将当前进程的RIP压栈 */ 5 "ret\n\t" /* ret命令正好可以让压栈的进程RIP保存到RIP寄存器中 */ 6 : 7 : "c"(task[pid].thread.ip), "d"(task[pid].thread.sp) 8 );
|
|
其次是进程的切换,核心代码如下:
1 asm volatile( 2 "pushq %%rbp\n\t" /* save rbp of prev */ 3 "movq %%rsp,%0\n\t" /* save rsp of prev */ 4 "movq %2,%%rsp\n\t" /* restore rsp of next */ 5 "movq $1f,%1\n\t" /* save rip of prev */ 6 "pushq %3\n\t" 7 "ret\n\t" /* restore rip of next */ 8 "1:\t" /* next process start here */ 9 "popq %%rbp\n\t" 10 : "=m"(prev->thread.sp), "=m"(prev->thread.ip) 11 : "m"(next->thread.sp), "m"(next->thread.ip) 12 );