基于mykernel多进程的简单内核代码分析

学号后三位:069

原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

始臣之解牛之时,所见无非全牛者。

三年之后,未尝见全牛也。

0、mykernel简介

mykernel是孟宁老师在2013年,在Linux内核繁杂的CPU初始化工作的基础上完成的一个虚拟、可编程的计算机硬件模拟环境。有了mykernel,稍有编程能力的学生就可以编写一个简单的时间片轮转调度的小型内核,并且能读懂代码,深刻理解如何在CPU的一个指令执行流上实现多个进程。

1、mykernel部署

使用实验楼的虚拟机打开shell
依次输入如下指令:

cd LinuxKernel/linux-3.9.4
rm -rf mykernel
patch -p1 < ../mykernel_for_linux3.9.4sc.patch  
make allnoconfig
make  //编译时间大概10分钟
qemu -kernel arch/x86/boot/bzImage

基于mykernel多进程的简单内核代码分析

经过耐心的等待后,看到编译完成了。⬇️
基于mykernel多进程的简单内核代码分析

在输入qemu -kernel arch/x86/boot/bzImage指令后,跳出如下界面。且有两类字符串不断交替输出。
基于mykernel多进程的简单内核代码分析

实际上,

  • my_timer_handler here字符串是由myinterrupt.cmy_timer_handler函数控制输出的
  • my_start_kernel here字符串是由mymain.cmy_start_kernel函数控制输出的

可见如下截图:
基于mykernel多进程的简单内核代码分析

基于mykernel多进程的简单内核代码分析

由此,我们可以知道,在系统启动后会周期性的调用myinterrupt.c中my_timer_handler函数以及mymain.c中my_start_kernel函数

2、内核代码分析

本部分是要完成一个简单的时间片轮转多道程序内核代码,源代码来自孟宁老师的GitHub。https://github.com/mengning/mykernel

主要包含三部分文件:
基于mykernel多进程的简单内核代码分析

  • myinterrupt.c
    包含模拟中断的函数。
  • mymain.c
    包含模拟多个运行的进程的函数。
  • mypcb.h
    头文件,定义了一些结构体等内容。

将以上文件下载到实验楼虚拟机中。

git clone https://github.com/mengning/mykernel

基于mykernel多进程的简单内核代码分析

将这三个文件拷贝到LinuxKernel/linux-3.9.4下的mykernel中,覆盖原文件并增加新文件:mypcb.h
基于mykernel多进程的简单内核代码分析

基于mykernel多进程的简单内核代码分析

在LinuxKernel/linux-3.9.4下,执行下面指令:

patch -p1 < ../mykernel_for_linux3.9.4sc.patch //一定要打补丁
make allnoconfig //第二次编译之前,一定要make clean,不然会出错
make  

基于mykernel多进程的简单内核代码分析

输入如下指令后:

qemu -kernel arch/x86/boot/bzImage

弹出窗口,显示进程的运行及切换过程:
基于mykernel多进程的简单内核代码分析
在上图中可以看到,进程切换的过程。⬆️

下面具体分析下这三个文件的代码:

  • mypcb.h

源代码:https://github.com/mengning/mykernel/blob/master/mypcb.h

宏定义:
#define MAX_TASK_NUM 4 定义了最大任务数
#define KERNEL_STACK_SIZE 1024*2定义了堆栈内核大小

线程结构体:
struct Thread {
unsigned long ip;ip指令指针
unsigned long sp;sp堆栈指针
};

此文件中最关键的部分是,下面的PCB结构体:

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;

其中,
pid表示进程号,是进程的标识符
state表示进程的状态,-1表示不可运行;0表示可运行等
stack[KERNEL_STACK_SIZE]表示进程的堆栈空间
thread表示进程的线程信息,包含ip、sp指针
task_entry进程入口点
*nextPCB指针,指向下一个进程控制块

mypcb.h中,最后一部分是void my_schedule(void);函数声明。此函数在myinterrput.c中定义,mymain.c中的进程会依据一个全局变量来决定是否调用此函数,来实现进程的调度切换。

  • mymain.c

源代码:https://github.com/mengning/mykernel/blob/master/mymain.c

全局变量:
volatile int my_need_sched = 0;用来指示当前进程是否需要调度(值为1时,需要调度)

接下来是两个关键的函数:
my_start_kernel函数

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].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*/
	);
} 

my_start_kernel是系统启动后,第一个调用的函数。在这个函数中,首先初始化了0号进程:

  • task[pid].pid = pid;置其pid为0
  • task[pid].state = 0设置进程状态为可运行状态
  • task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process
    设置进程的入口地址为my_process函数

接下来的for循环,则创建了3个进程,pid分别为1、2、3。并通过下面的代码,将PCB用指针链接了起来。
task[i].next = task[i-1].next;
task[i-1].next = &task[i];

接下来,启动0号进程。这里最关键的是下面的内嵌汇编代码:

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*/
	);

汇编代码的作用,注释中都有解释。我下面就主要分析下,随着代码的执行过程,堆栈的内容变化。见下图:
基于mykernel多进程的简单内核代码分析

从左到右依次是代码执行过程中,堆栈的内容变化。最后eip中存的就是my_process(void)的函数位置。显而易见,接下来执行的就是my_process(void)的函数。

my_process(void)函数

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,当其值为1时,就调用my_schedule()函数,完成进程调度切换。

  • myinterrupt.c

源代码:https://github.com/mengning/mykernel/blob/master/myinterrupt.c

这里首先定义/声明了一些全局变量:
extern tPCB task[MAX_TASK_NUM];此变量在mymain.c文件中定义,此处为外部声明
extern tPCB * my_current_task;同上
extern volatile int my_need_sched;同上
volatile int time_count = 0;此处定义了一个全局变量,用来模拟时间片

接下来实现了两个函数,my_timer_handler函数和my_schedule函数。下面依次来分析下这两个函数:

my_timer_handler函数

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;  	
}

此函数的功能非常显然,时间片为1000的整数倍时,就将全局变量my_need_sched的值置为1,这样mymain.c中的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(	
        	"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;	
}

首先定义了两个PCB指针:
tPCB * next;指向下一个PCB
tPCB * prev;指向当前PCB

如果下一个进程的state是可运行的,那么就切换到下一个进程。具体实现由相应的内嵌汇编代码实现:

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)
   	); 

简单来讲,以上汇编代码的功能就是在进程堆栈之前切换——保存当前进程的上下文环境、切换到另一进程的上下文环境;当切换回原来的进程时,也能够做到回复之前的上下文执行环境。

3、总结

操作系统是如何工作的?

操作系统的工作主要依赖三大法宝

-存储程序计算机
-函数嗲用堆栈机制
-中断支持

堆栈是C语言程序运行时必须使用的记录函数调用路径和参数存储的空间,堆栈的具体作用有:记录函数调用框架、传递函数参数、保存返回值的地址、提供内部局部变量的存储空间等。

而中断的支持也不容忽视,有了中断才有了多道处理程序,在没有中断机制之前,计算机智能一个程序一个程序的运行,也就是批处理,而无法实现并发执行。有了中断机制之后,当中断信号发生时,CPU把当前正在执行的程序的EIP、ESP寄存器的内容都压到堆栈当中进行保存。之后转而执行其他的程序,等执行过后还能依靠堆栈来恢复现场,恢复EIP、ESP寄存器的值,进而继续执行中断前的程序。