一、实验要求
- 按照https://github.com/mengning/mykernel 的说明配置mykernel 2.0,熟悉Linux内核的编译;
- 基于mykernel 2.0编写一个操作系统内核,参照https://github.com/mengning/mykernel 提供的范例代码
- 简要分析操作系统内核核心功能及运行工作机制
二、实验环境与原始代码
2.1 实验环境
VMware+虚拟机Ubuntu 18.04.1 LTS amd64
2.2 原始代码
原始代码来自孟宁老师的github:https://github.com/mengning/mykernel ,然后根据老师写的步骤,一步步完成就可以。
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
执行完上述指令后,可以看到如下结果
三、代码分析
3.1 原始 代码分析
我们首先可以看到原始代码的执行逻辑,在**mymain中,我们可以看到如下的代码。我们只要稍微有点计算机基础,应该就可以看明白,这是无限循环的代码,i 每增加10000000就会将i的值输出一次。
void my_process(void)
{
while(1)
{
i++;
if(i%100000 == 0){
pr_notice("my_start_kernel here %d \n",i);
}
}
}
接下来,如果我们想要多个进程来执行这个代码,我们如何实现进程调度呢?
3.2 修改代码,实现进程调度
从孟宁老师的github上获取实现进程切换最为重要的三个代码:mymain.c,myinterrupt.c和mypcb.h。随后将原先代码替换为上述三个代码,可以从下面两个图可以明显看出进程的切换。
当然,因为有老师的代码作为指导,运行起来是很简单的事情,我们当然也要了解这其中是如何实现的,实现的细节是什么。所以接下来对那三个代码一一分析。
3.3 源码分析
3.3.1 mypcb.h
首先是mypcb.h,我们要实现一个功能,最先想到的应该就是为这个动能定义数据结构。那么PCB是什么呢:进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息,它使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或一个能与其他进程并发执行的进程。
而对于这个代码的详细介绍,我将写在下面的代码中
/*定义最大进程数*/
#define MAX_TASK_NUM 4
/*定义了堆栈空间大小 */
#define KERNEL_STACK_SIZE 1024*2
/* CPU-specific state of this task
定义了一个结构体用来保存当前ip和sp */
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); /* 声明进程调度函数 */
而这个代码主要就是做了三件事
- 定义Thread结构体用来指示堆栈指针和指令指针。
- 定义PCB,用来标识进程的相关信息,例如进程id, 进程可以申请堆栈大小,程序的入口,以及下一个进程。
- 声明调度函数,这个调度函数会在下面讲解。
3.3.2 myinterrupt.c
同样相关的信息写在代码中
#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;
/* 这里就用到了我们在PCB中的定义的状态,只有正确的状态才能进行进程切换 */
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;
}
总体来说,这个代码就是实现进程调度的核心代码,在asm volatile中,通过对,当前进程的ip,sp,以及下一个线程的ip, sp ,以及对寄存器的相关操作,实现对进程调度的控制。
3.3.3 mymain.c
这段代码主要就是我们用来定义执行逻辑的代码,我们可以看到熟悉的对i输出的代码。
#include
#include
#include
#include
#include
#include "mypcb.h"
/* 定义存储PCB的队列 */
tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL; /* 初始化一个PCB */
volatile int my_need_sched = 0;
void my_process(void);
/* 初始化一个0号进程,并进行相关处理 */
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);
}
}
}
这段代码总结起来就是,首先创建一个0号的进程,并进行初始化(修改相应的状态,初始化ip, sp, 初始化程序入口关等),并且在此之后,用一个for循环又创建了3个进程,并用链表讲这四个进程串联起来,这也是我们在最终实现中能够看到有四个进程在调度的原因。
随后这段代码又对相关的寄存器EIP,EBP,ESP进行初始化,先将堆栈指针sp赋给了ESP寄存器,然后将堆栈指针sp内容压栈,之后将指令指针ip的内容也压栈,下一条指令是ret,它是将当前栈中ESP所指的内容出栈到EIP中,当前ESP所指的内容就是前一条指令压栈进去的ip的值,现在使得EIP寄存器的内容就是进程0的入口地址(ip内容),从而使得进程0能够被执行。
最后就是我们可见的相关的代码的实现,同样i 没经过10000000输出一次,不过,这里通过上述讲的 myinterrupt.c实现了对进程的调度。
四、总结
本来对Linux系统,或者说计算机底层的实现知之甚少,通过本次实验,我学习到计算机三大法宝:1. 函数调用堆栈:记录调用的路径和参数的空间。 2.存储程序计算机:冯诺依曼结构。3. 中断机制:由CPU和内核代码共同实现了保存现场和恢复现场,把ebp,esp,eip寄存器的数据push到内核堆栈中。再把eip指向中断程序的入口,保存现场。另外还学习到进程调度相关的细节,像如何对相关寄存器操作可以实现进程调度等。