基于mykernel 2.0编写一个操作系统内核
1 配置并编译mykernel 2.0
1.1 本机环境
VirtualBox 6.1.6 + Manjaro 20.0.1
Manjaro是一个基于Arch的面向新手的容易上手的发行版,本次实验使用Manjaro主要是因为我已经有了一个预先配置好的Manjaro虚拟机。
1.2 编译过程
执行以下命令:
sudo pacman -Syu # Manjaro是滚动更新的发行版,使用时需要更新到最新版本
wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
sudo pacman -S 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 pacman -S base-devel libncurses-dev bison flex libssl-dev libelf-dev #在pacman中build-essential包被称为base-devel,libncurses-dev被称为ncurses,libssl-dev被称为openssl,libelf-dev被成为libelf
make defconfig
make -j$(nproc)
sudo pacman -S qemu
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
使用pacman安装build-essential、libncurses-dev、libssl-dev、libelf-dev时会发现找不到这些包,发现这些包在pacman中包名与apt中不同。
上述命令执行完成后,mykernel就在QEMU中运行起来了,可以看到 my_start_kernel 在执行,同时 my_timer_handler 时钟中断处理程序周期性执行。
打开linux-5.4.34/mykernel目录中 mymain.c 和 myinterrupt.c文件 :
mymain.c:
/*
* Called by timer interrupt.
*/
void my_timer_handler(void)
{
pr_notice("\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}
myinterrupt.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);
}
}
可以看到mymain.c中的代码在不断执行,并且myinterrupt.c周期性被时钟中断触发。
2 编写一个操作系统内核
编写 mypcb.h 头文件如下:
/*
* linux/mykernel/mypcb.h
*
* Kernel internal PCB types
*
* Copyright (C) 2013 Mengning
*
*/
#define MAX_TASK_NUM 4
#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;
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);
编写 mymain.c 文件如下:
/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
* Change IA32 to x86-64 arch, 2020/4/26
*
* Copyright (C) 2013, 2020 Mengning
*
*/
#include
#include
#include
#include
#include
#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;ipid);
if(my_need_sched == 1)
{
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
编写 myinterrupt.c 文件如下:
/*
* linux/mykernel/myinterrupt.c
*
* Kernel internal my_timer_handler
* Change IA32 to x86-64 arch, 2020/4/26
*
* Copyright (C) 2013, 2020 Mengning
*
*/
#include
#include
#include
#include
#include
#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;
}
重新编译后,运行结果如下:
3 简要分析操作系统内核核心功能及运行工作机制
进程上下文切换的关键代码如下:
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)
);
初始状态下,x1和x2是prev进程的栈底和栈顶,y1和y2是next进程的栈底和栈顶。
pushq %%rbp\n\t
将prev进程当前RBP的值保存到堆栈,即令RSP指向(x2-8),再将RBP=x1保存到RSP指向的地址中movq %%rsp,%0\n\t
将prev进程当前RSP的值(x2-8)赋给prev->thread.spmovq %2,%%rsp\n\t
将next->thread.sp即y2放⼊RSP寄存器,完成了进程prev和进程next的堆栈切换movq $1f,%1\n\t
保存prev进程当前RIP寄存器值到prev->thread.ippushq %3\n\t
将next->thread.ip入栈,即将rsp指向(y2-8),令next->thread.ip=$1f;ret\n\t
将压入栈中的next->thread.ip放入RIP寄存器popq %%rbp\n\t
将next进程堆栈基地址从堆栈中恢复到RBP寄存器中,即令指向x1的RBP指向y1
这时进程切换完毕。
4 参考资料
[1] https://github.com/mengning/mykernel
[2] https://mp.weixin.qq.com/s/SzpN1BNty5aPDZhNdCO5yA