基于mykernel 2.0编写一个操作系统内核
实验环境
本次的实验环境是 VMware Workstation+虚拟机Ubuntu 18.04.1 LTS amd64
MyKernel 2.0实验环境搭建
进入虚拟机,打开ubuntu终端,输入以下命令。
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启动基于mykernel 2.0实现的简易程序,程序运行截图如下所示。

从图中可以看出,内核不断输出my_time_handler here的打印信息,这是因为在mymain.c代码中不断循环的调用了
myinterrupt.c 当中产生的时钟中断,而myinterrupt.c的的中断处理函数中会打印出该信息。
实现一个操作系统内核
PCB进程控制块的定义
我们通过基于MyKernel 2.0实现一个拥有进程调度和切换功能的简易Linux OS,我们首先需要对内核中的进程控制块PCB
进行定义。
/*
* linux/mykernel/mypcb.h
*
* Kernel internal PCB types
*
* Copyright (C) 2013 Mengning
*
*/
#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;
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);
Thread结构体中定义了CPU在进行进程调度时所需要的重要数据成员,分别中ip进程指令指针和sp进程堆栈指针。其中
ip进程指令指针对程序员透明,只向CPU调度进程所要执行的指令位置,sp指向当前进程的栈顶。
PCB结构体中定义了一个进程的基本信息,下面对其中的数据结构成员进行简单的介绍。
- pid为进程的id,它可以唯一的标识当前操作系统环境下的进程。
- state标识进程的当前状态,其中-1为阻塞态,0为运行态,>0为就绪态。
- stack字节数组定义了当前进程的堆栈空间。
- thread定义了进程中用于CPU调度的基本成员,ip和sp。
- task_entry定义了程序的入口。
- next为指向下一个PCB进程控制块的指针,主要用于进程切换。
进程执行函数及进程创建
在main函数中对进程进行初始化。
/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
* Change IA32 to x86-64 arch, 2020/4/26
*
* Copyright (C) 2013, 2020 Mengning
*
*/
#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"
"pushq %1\n\t"
"pushq %0\n\t"
"ret\n\t"
:
: "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)
{m
my_need_sched = 0;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
main函数中主要实现了两个功能:1. 定义了多个进程,并确定执行顺序。 2. 确定了进程的执行函数。
第一个功能的实现主要是创建多个PCB结构体,并通过next指针进行连接,如此变实现了多个进程的创建。
并通过一段汇编语言,为进程分配CPU执行资源,下面对汇编语言进行逐行解析:
- movq 将待执行的进程的栈顶指针赋值给RSP寄存器,此操作后操作系统可以访问该进程的堆栈空间。
- pushq 将待执行的进程的栈顶指针压栈,此步骤主要是用来保存栈顶指针,用来后续恢复。
- pushq 将待执行进程的指令指针压栈,主要是用来实现指令跳转。
- ret 将堆栈空间中的指令指针赋值给RIP寄存器,此时CPU下一条指令为进程的执行函数my_process的入口。
第二个功能主要是my_process函数的实现,该函数通过计时器来调用my_schedule函数对进程进行切换调度。
进程切换调度的实现
下面我们看以下my_schedule函数是如何实现进程切换的。
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;
}
进程切换通过一段汇编语言来实现,下面我们对这段汇编语言进行分析。
- pushq 该步骤是把RBP此存器中的内容压栈,该寄存器中存储了前一个进程的栈帧的栈低地址。
- movq 该步骤是把RSP寄存器中的值赋值给前一个进程的栈顶指针thread.sp,该步骤主要是将前一个
进程的堆栈状态进行保存。 - movq 该步骤是把下一个进程的thread.sp保存到RSP寄存器中,此时操作系统可以对下一个进程的堆栈空间进行访问控制。
- movq 该步骤是把RIP寄存器中的值保存到前一个进程的ip中,此步骤的作用是保存CPU执行的指令位置。
- pushq 该步骤是把下一个进程的ip指令指针压栈。
- ret 该步骤的作用是将下一个进程的ip指令指针出栈,并赋值给RIP寄存器,此时下一个进程获得CPU资源。
- 1:该步骤开始执行下一个进程的进程程序。
- popq 下一个进程执行结束后,将RBP寄存器恢复到下一个进程的栈帧基地址。
执行结果

实验总结
经过此次实验,对Linux内核中的进程切换关系以及原理有了更深入的了解。

浙公网安备 33010602011771号