基于mykernel2.0编写操作系统内核
实验要求
- 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译
- 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel提供的范例代码
- 简要分析操作系统内核核心功能及运行工作机制
搭建环境
(1)本机环境
VMware® Workstation 12 Pro + Ubuntu 18.04

(2)下载kernel文件
wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
注意由于下载速度过慢,可以从群上直接下载然后通过vmware tools移动到虚拟机上。
(3)下载安装axel
sudo apt install axel

(4)通过axel实现多线程下载kernel压缩包
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz

注意,这里由于我的虚拟机之前调整过浏览器的证书,导致证书一直报ssl错误,可以将地址由HTTPS改成HTTP即可顺利完成下载
(5)解压xz包
xz -d linux-5.4.34.tar.xz
(6)解压tar包
tar -xvf linux-5.4.34.tar
(7)通过下载的kernel文件进行修补
cd linux-5.4.34
patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
在这一步之前需要提前将patch文件移至linux-5.4.34文件夹中

(8)安装相关库文件
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev

注意,这里我刚开始使用的阿里云的源进行下载,一直下载失败,更改成科大的源即成功。更改源的方法:<a href="https://blog.csdn.net/qq_34355238/article/details/100274046" target="_blank"https://blog.csdn.net/qq_34355238/article/details/100274046。但用这个改原方法,不建议像原博那样进行upgrade软件,否则很可能引发软件不兼容等问题。
(9)生成内核编译
make defconfig

(10)编译kernel
make -j$(nproc)
此处编译时间较长,约为10分钟左右,耐心等待即可。

(11)安装qemu
sudo apt install qemu
开始实验
(1)验证实验环境
为了确保上述实验环境已成功搭建,我们可以通过以下命令查看QEMU是否正常运行。
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
(2)查看内核文件
进入mykernel文件
cd mykernel
查看mymain.c文件

可以看到,mymain执行的是一个永真循环,并且在一定周期内会产生中断信号,调用myinterrupt.c。接下来我们查看myinterrupt.c。

(3)内核代码修改
综上,我们需要增加PCB和进程管理,以使得内核能完成正常的进程切换。以下三个操作均在mykernel文件中完成。
①增加PCB.h文件
PCB是指进程控制块。每个进程均有一个PCB,它是一个既能标识进程的存在、又能刻画执行瞬间特征的数据机构。
//最大的任务数
#define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8
/* 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 */
char 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);
其中:
- PID为进程标识符,每个进程都有唯一的PID。
- stack[]为进程所使用的堆栈空间
- thread为进程当前正在执行的线程
- task_entry为进程入口函数地址(这里指my_process函数)
- next 指向下一个PCB地址的指针
②修改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 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].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(
"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;
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);
}
}
}
首先,我们需要修改初始化函数,完成设置PCB初值、压栈操作。另外需要增加my_process函数,显示打印当前进程编号,以使得可查看进程是否按时间片轮转。
③修改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 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(time_count%1000 == 0 && my_need_sched != 1)
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
my_need_sched = 1;
}
time_count ++ ;
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(
"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;
}
首先,我们需要修改my_timer_handler函数,增加时间计数器,以使得达到时间片后进行进程切换。同时增加my_schedule函数,完成进程切换操作:
- 保存处理及上下文
- 更新PCB信息
- 把进程的PCB移入相应队列,如就绪或阻塞队列
- 选择下一个进程执行,更新其PCB
- 更新内存管理的数据结构
- 恢复处理器上下文
(4)实验结果
在完成上述操作后,我们需要重新编译,使用make指令即可,注意要在linux-5.4.34文件下执行

实验结果如下,可以看到进程成功完成切换操作。

简要分析操作系统内核核心功能及运行工作机制
(1)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*/
);
- movq %1,%%rsp: RSP寄存器指向进程0的堆栈栈底
- pushq %1: 当前RBP寄存器值压栈
- pushq %0: 当前进程的RIP压栈
- ret: 压栈的进程RIP保存到RIP寄存器中,用于返回
(2)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)
);
- pushq %%rbp: 保存前一个进程的rbp值到堆栈
- movq %%rsp,%0: 保存前一个进程rsp值到prev->thread.sp,这时rsp寄存器指向进程的栈顶地址,实际上就是将prev进程的栈顶地址保存
- movq %2,%%rsp: 为完成进程的切换,将下一个进程栈顶地址next->thread.sp放至RSP寄存器
- movq $1f,%1: 保存prev进程当前RIP寄存器值到prev->thread.ip(保存处理及上下文)
- pushq %3: 将下一个进程next->thread.ip压栈
- ret: 将栈顶的next->thread.ip存至rip寄存器,由于程序不能直接使用rip寄存器,需要分开两步处理
- 1: 特殊地址位置
- popq %%rbp: 将rbp寄存器的值修改为下一个进程的栈底(恢复处理器上下文)
实验总结
进程切换会使得运行环境产生实质性的变化。进程切换需要完成以下六个步骤(此处实验有所简略):
- 保存处理及上下文,包括程序计数器和其他寄存器
- 更新PCB信息
- 把进程的PCB移入相应队列,如就绪或阻塞队列
- 选择下一个进程执行,更新其PCB
- 更新内存管理的数据结构
- 恢复处理器上下文
进程切换依赖于中断机制,因此进行进程切换必须从用户态进入内核态(如实验模拟),之后又回到用户态。所以进程切换需花费大量的资源,引入线程显得就十分必要。
浙公网安备 33010602011771号