Linux实验——基于mykernel完成多进程的简单内核

Linux实验——基于mykernel完成多进程的简单内核

学号后三位为342
原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

实验工具

  • Linux内核代码
  • VMware Workstation Pro
  • Ubuntu 17

实验目的

  • 操作系统是如何工作
  • 分析进程的启动和进程的切换机制

实验内容

  • 部署环境

sudo apt-get install qemu                          
//下载QEMU

sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu  
//建立软连接,继续使用qemu作为指令,非必要

wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz  
//下载Linux 3.9.4内核,速度较慢

wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
//下载mykernel补丁

xz -d linux-3.9.4.tar.xz 

tar -xvf linux-3.9.4.tar 

cd linux-3.9.4 

patch -p1 < ../mykernel_for_linux3.9.4sc.patch 
//打补丁

make allnoconfig 
//最小化运行,不需要多的模块

make
//编译
  • 需要注意的是,由于使用的内核版本较老,可能会出现缺少相应的gcc文件的情况无法编译成功;如在我的实验环境下,提示缺少gcc7.h。经过查找一些解决方案,将linux-3.9.4/include/linux文件夹下compiler-gcc4.h重命名为compiler-gcc7.h即可编译通过,如下图。

Linux实验——基于mykernel完成多进程的简单内核_第1张图片

qemu -kernel arch/x86/boot/bzImage
//启动内核

Linux实验——基于mykernel完成多进程的简单内核_第2张图片Linux实验——基于mykernel完成多进程的简单内核_第3张图片
内核在运行时,会检查是否有中断需要处理;如果有中断请求,那么执行中断处理;否则,继续执行当前程序。

内核正常运行时,会循环打印

“my_start_kernel here i”

而当中断触发时则会打印

“>>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<”

实验要求我们完成一个基于mykernel完成多进程的简单内核。
将老师提供的代码(mymain.c;myinterrupt.c;mypcb.h)复制到mykernel文件夹中,重新进行编译。

而在重新编译时出现了如下的错误。
Linux实验——基于mykernel完成多进程的简单内核_第4张图片
经过查找,发现是mypcb.h文件中的#unsigned long多余,删掉后重新编译,继续用qemu运行,
实验结果如下。
Linux实验——基于mykernel完成多进程的简单内核_第5张图片
Linux实验——基于mykernel完成多进程的简单内核_第6张图片
Linux实验——基于mykernel完成多进程的简单内核_第7张图片

代码分析

我们通过分析代码看为何会得到上面的结果。
首先先看头文件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 //# unsigned long
/* 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);

这个头文件里定义了Thread结构体和PCB结构体。其中Thread结构体中包括sp和lp两个变量;PCB结构体包括id(任务运行的ID号),任务运行的状态state,任务入口task_entry,以及next指针。

然后是mymain.c文件

/*
*  linux/mykernel/mymain.c
*
*  Kernel internal my_start_kernel
*
*  Copyright (C) 2013  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 */

这一部分的主要目的是初始化,定义了0号进程,将进程的运行状态变为可运行状态,同时进程入口为my_process;同时sp指向栈顶元素。

   for(i=1;i<MAX_TASK_NUM;i++)
   {
       memcpy(&task[i],&task[0],sizeof(tPCB));
       task[i].pid = i;
   //*(&task[i].stack[KERNEL_STACK_SIZE-1] - 1) = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];
   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(
   	"movl %1,%%esp\n\t" 	/* set task[pid].thread.sp to esp */
   	"pushl %1\n\t" 	        /* push ebp */
   	"pushl %0\n\t" 	        /* push task[pid].thread.ip */
   	"ret\n\t" 	            /* pop task[pid].thread.ip to eip */
   	: 
   	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
   );
} 

这一部分将进程的sp指针压入esp寄存器中,然后再进行压栈操作,之后将指令指针ip的内容也压栈,下一条指令是ret,它是将当前栈中esp所指的内容出栈到eip中,当前esp所指的内容就是前一条指令压栈进去的ip的值,然后利用ret弹栈到eip,从而使得进程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)
            {
                my_need_sched = 0;
        	    my_schedule();
        	}
        	printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

这段代码主要是通过条件来改变输出内容,再通过变量my_need_sched来判断是否要进行调度。

最后我们对myinterrupt.c文件进行分析。

 /*
 *  linux/mykernel/myinterrupt.c
 *
 *  Kernel internal my_timer_handler
 *
 *  Copyright (C) 2013  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 1
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
#endif
    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(	
        	"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)
    	); 
    }  
    return;	
}

my_timer_handler函数就是用来实现上面的变量my_need_sched。
而这段代码最关键的就是函数void my_schedule()

"pushl %%ebp\n\t" 	    /* save ebp */

这段代码用来保存当前进程的edp。

"movl %%esp,%0\n\t" 	/* save esp */

将esp保存到A的sp中,从而保证调用my_schedule,prev指针始终指向A进程。

"movl %2,%%esp\n\t"     /* restore  esp */

取出之前的esp。

"movl $1f,%1\n\t"       /* save eip */

他表明当下一次A进程被my_shcedule再次调度回来的时候,会开始新的进程。

"pushl %3\n\t"

这里要分两种情况进行讨论,如果B进程在之前被调度过了,那么这里的next->thread.ip指向的就是1f的标号地址,如果进程B在之前没有被调度过的话,那么这里存的就是Myprocess这个函数地址。

"ret\n\t"               /* restore  eip */

如果next->thread.ip指向的是1f的标号地址那么程序将继续向下运行,如果next->thread.ip指向的是my_process这个函数的话,将直接跳转到my_process函数中。

"1:\t"                  /* next process start here */
"popl %%ebp\n\t"

进程再次执行时,会因为这条命令,将之前存的ebp弹出至ebp寄存器,实现ebp寄存器值的切换

总结

  • 操作系统是如何工作的 ?

Linux操作系统的正常工作可以说有三个非常重要的部分,简言之就是:

  • 存储程序原理
  • 堆栈
  • 中断机制

Linux操作系统是多进程的操作系统,通过对进程的管理与调度,同时,操作系统以一种中断的机制实现与用户的交互,函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能。

你可能感兴趣的:(Linux实验——基于mykernel完成多进程的简单内核)