(1)基本功能介绍
如题,本文将介绍如何构造一个简单的操作系统内核(基于内核版本3.9.4 )。它有以下功能:
1:进程的管理
2:进程的初始化
3 : 进程基于时间片的调度
(2)实操步骤
1 安装qemu, 以ubuntu为例:
sudo apt-get install qemu
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
2 下载linux内核源代码
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz
3 解压源码
xz -d linux-3.9.4.tar.xz
tar -xvf linux-3.9.4.tar
4 下载制作好的patch_for_mykernel
链接:https://pan.baidu.com/s/164qQa4PfyQZjoP8F7eT-cw
提取码:7387
5 打上patch
cd linux-3.9.4
patch -p1 < patch_for_mykernel
6 编译运行
make allnoconfig
make
qemu -kernel arch/x86/boot/bzImage
运行效果视频:
<iframe allowfullscreen="true" data-mediaembed="youku" id="J7jQfXWx-1639234727816" src="https://player.youku.com/embed/XNTgyNjI4MjA4MA=="></iframe>操作系统进程调度
(3) 代码概述:
1 my_start_kernel: my kernel的入口, 初始化了10个进程,并且启动0号进程。
2 用tPCB去保存进程的信息,包括id, state, 调用栈起始地址,入口函数,ip (instruct point, 指令指针), sp (stack point, 栈顶指针)。所有的进程通过tPCB的链表链在一起。
3 my_timer_handler: 时钟中断处理函数, 每触发2000次, 将my_need_sched 设置成 1, 表 示要进行一次调度。
4 my_process:进程的入口函数。每执行10000000次,看一下是否需要调度,如果是,则调用my_schedule进行调度。
5 my_schedule:调度函数的实现。保存当前进程的上下文现场,并切换到下一个进程。这里下一个进程的选择用的是简单的方法:tPCB struct 的 next指向的进程。
(4)关键代码详解:
void __init my_start_kernel(void)
{
int pid = 0;
/* Initialize process 0*/
task[pid].pid = pid;
task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
// set task 0 execute entry address to my_process
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(pid=1;pid<MAX_TASK_NUM;pid++)
{
memcpy(&task[pid],&task[0],sizeof(tPCB));
task[pid].pid = pid;
task[pid].state = -1;
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
task[pid].priority=get_rand(PRIORITY_MAX);//each time all tasks get a random priority
task[pid].next = task[pid-1].next;
task[pid-1].next = &task[pid]; //所有的线程用链表链接起来
}
//task[MAX_TASK_NUM-1].next=&task[0];
printk(KERN_NOTICE "\n\n\n\n\n\n system begin :>>>process 0 running!!!<<<\n\n");
/* start process 0 by task[0] */
pid = 0;
my_current_task = &task[pid];
asm volatile( //嵌入式汇编
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ //esp=task[pid].thread.sp
"pushl %0\n\t" /* push task[pid].thread.ip */ //task[pid].thread.ip
"ret\n\t" /* pop task[pid].thread.ip to eip */ // eip = task[pid].thread.ip
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
void my_schedule(void)
{
tPCB * next;
tPCB * prev;
// if there no task running or only a task ,it shouldn't need schedule
if(my_current_task == NULL
|| my_current_task->next == NULL)
{
printk(KERN_NOTICE " time out!!!,but no more than 2 task,need not schedule\n");
return;
}
/* schedule */
//next = get_next();
next = my_current_task->next;
prev = my_current_task;
if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ //下一个线程是运行过的
{//save current scene
/* switch to next process */
asm volatile(
"pushl %%ebp\n\t" /* save ebp */
"movl %%esp,%0\n\t" /* save esp */
"movl %2,%%esp\n\t" /* restore esp */
"movl $1f,%1\n\t" /* save eip */
"pushl %3\n\t"
"ret\n\t" /* restore eip */
"1:\t" /* next process start here */
"popl %%ebp\n\t"
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
my_current_task = next;//switch to the next task
printk(KERN_NOTICE " switch from %d process to %d process\n >>>process %d running!!!<<<\n\n",prev->pid,next->pid,next->pid);
}
else //下一个进程是从来没有运行过的
{
next->state = 0;
my_current_task = next;
printk(KERN_NOTICE " switch from %d process to %d process\n >>>process %d running!!!<<<\n\n\n",prev->pid,next->pid,next->pid);
/* switch to new process */
asm volatile(
"pushl %%ebp\n\t" /* save ebp */
"movl %%esp,%0\n\t" /* save esp */
"movl %2,%%esp\n\t" /* restore esp */
"movl %2,%%ebp\n\t" /* restore ebp */
"movl $1f,%1\n\t" /* save eip */
"pushl %3\n\t"
"ret\n\t" /* restore eip */ //eip = next->thread.ip
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}//end of my_schedule
1 0号进程的启动代码:
1.1 “movl %1 %%esp\n\t” ===》esp = task[0].thread.sp
将0号进程栈顶的地址存入ESP寄存器,因为一开始栈是空的,EBP等于ESP,所以EBP也等于task[0].thread.sp
1.2 ’pushl %0\n\t' ===> push task[0].thread.ip
将0号进程的入口(ip)压栈
1.3 'ret\n\t'
pop栈顶entry到eip, 此时栈顶entry即是之前一步刚压栈的task[0.thread.ip, 所以EIP = taks[0].thread.ip
到此0号进程初始化完毕: esp = eip = task[0].thread.sp, eip = taks[0].thread.ip
接下来分析进程切换部份,为了简便,假设系统只有两个进程,分别是0号进程处1号进程。进程0由内核启动时初始化执行,然后需要进行进程调度,开始执行1号进程。由于1号进程是一个从未执行过的进程,那么进入代码的else部分。
2.由0号进程切换到1号进程
2.1 pushl %%ebp\n\t
保存当前进程EBP到堆栈
2.2 ’movl %%esp, %0\n\t‘===>prev->thread.sp = esp
保存当前进程ESP到内存(prev->threads.sp)
2.3 'movl %2,%%esp\n\t' ===>esp = next->thread.sp
载入next进程的栈顶地址到ESP寄存器,此时EPS切换到process 1的栈顶
2.4 ’movl %2,%%ebp\n\t‘ ===>ebp = next->thread.sp
载入next进程的堆栈基地址到EBP寄存器
2.5 ’movl $1f, %1\n\t‘
保存当前EIP寄存器值到内存(pre->thread.ip), $1f是一种特殊的语法,后面会介绍
2.6 pushl %3\n\t===>push next->thread.ip
把next进程的代码入口(ip)入栈
2.7 ’ret \n\t‘ ===> pop to eip
pop栈顶entry到eip,即在上一步push到栈的next->thread.ip. 所以EIP = next ->thread.ip
到此0号进程切换到1号进程完毕!
经过一段时间之后,1号进程会切换回0号进程,由于0号进程是已经执行过的进程,那么走的是if这个分支
3.由1号进程切换回0号进程
3.1 'pushl %%ebp \n\t'
保存当前EBP到栈中
3.2 move %%esp,%0\n\t===> pre->thread.sp = esp
保存当前ESP到内存中
3.3 ‘move %2,%%esp\n\t’===> esp = next->thread.sp
将next进程的堆栈栈顶保存到ESP寄存器,此时已经切换回0号进程的调用栈
3.4 'pushl %3'===>push ext->thread.ip
将next进行继续执行的代码位置($1f)压栈
3.5 'ret\n\t' ===>pop to EIP
将栈顶entry pop(next 进程继续执行的代码位置)到EIP
3.6 '1:\t'
定义标号1的位置,即next进程开使执行的位置
3.7 ‘popl %%ebp\n\t’
恢复EBP寄存器的值
到此1号进程切换回0号进程完毕!
关于文中$1f的使用再补充一点说明:
$1f的含义是前的标号1(forwarding label 1), 在这个例子中指的是
"ret\n\t" /* restore eip */
"1:\t" /* next process start here */
"popl %%ebp\n\t"
即next开始执行的入口。if中有标号1,else中没有标号1。这里可能会有疑问。else中$1f只是将其存入prev-thread.ip.并没有使用$1ff, 但当进程被重新调度执行时,prev->thread.ip变成了next->thread.ip.此时进入了if代码中会将next->thread.ip压栈,并由ret出栈到EIP寄存器存中,这时才实际使用了$1F,因此将执行if代码块中的标号1处的代码,所有else中没有标号1也就不奇怪了。
4总结:
0号进程初始化包括两个步
--ESP和EBP的赋值(进程调用栈地址)
--EIP的赋值(进程入口)
进程切换
--保存当前线程的ESP和EBP以及EIP
--load下一个进程的sp,bp以及ip. 如果下一个进程从未执行过,那么其运行的调用栈是空的,那么不用恢复EBP. (此时EBP=ESP)。它的入口直接是代码。
--如果下一个进程已经执行过了,那么它的入口是恢复ebp, 然后再执行真正的代码。
嵌入式汇编语法可以参考: