构造一个简单的操作系统内核,详解进程切换细节

(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, 然后再执行真正的代码。

嵌入式汇编语法可以参考:

嵌入式汇编的基本格式_突围-CSDN博客

上一篇:hadoop常用的调优参数


下一篇:C++线程池