《Linux内核分析》MOOC课程
http://mooc.study.163.com/course/USTC-1000029000

STEP1:搭建实验环境
首先在自己的Linux系统中配置好实验的环境,依次输入以下的命令:

•	sudo apt-get install qemu # install QEMU
•	sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
•	wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code
•	wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch
•	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

最后执行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一个进程切换的内核
经过环境的配置,Linux内核经过我们修改,在qemu窗口中我们会看到,一个只有时钟中断的系统。如图所示

这里写图片描述
因为在mymian.c里面,只有一个函数在执行,my_start_kernel(void),循环输出一句话。

void __init my_start_kernel(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000 == 0)
            printk(KERN_NOTICE "my_start_kernel here  %d \n",i);
            
    }
}

在myinterrupt.c文件里面只有一个处理时钟中断的函数

void my_timer_handler(void)
{
	printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}

如果我们要完成可以进行进程调度的操作系统内核,需要将github中mykernel的myinterrupt.c mymain.c mypcb.h三个文件复制到linux内核文件夹下面,然后再进行make。
首先在github上面下载3个文件,在终端中输入如下命令:

wget https://raw.github.com/mengning/mykernel/master/mypcb.h
wget https://raw.github.com/mengning/mykernel/master/myinterrupt.c
wget https://raw.github.com/mengning/mykernel/master/mymain.c

然后将三个文件拷贝到linux内核文件夹下面的mykernel文件夹下面,在终端中输入命令

cp myinterrupt.c linux-3.9.4/mykernel/
cp mymain.c linux-3.9.4/mykernel/
cp mypch.h linux-3.9.4/mykernel/

最后执行make重新编译内核
然后终端中输入qemu -kernel arch/x86/boot/bzImage就可以看到一个进程切换的内核运行了。
这里写图片描述
这里写图片描述
STEP3:分析代码,理解执行过程
首先打开mypcb.h文件,有两个结构体。
第一Thread结构体,保存一个任务的eip还有esp的信息。

struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};

第二个结构体PCB,保存进程的一些信息。
pid保存一个进程唯一的一个ID号
state表示一个进程的运行状态
stack数组表示一个进程的堆栈空间
Thread结构体记录eip和esp堆栈栈顶的信息
task_entry表示进程的执行入口地址
struct PCB* next则指向下一个进程,用链式储存结构将进程连在一起

typedef struct PCB{
    int pid;
    volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    char stack[KERNEL_STACK_SIZE];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long	task_entry;
    struct PCB *next;
}tPCB;

然后打开mymain.c文件
volatile int my_need_sched = 0;
my_need_sched变量表示一个任务,是否可以进行调度。
然后分析kernel开始执行的函数。

void __init my_start_kernel(void)
{
int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;/*将进程号初始化为0*/
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
/*把进程的入口和eip都初始化为my_process函数的入口地址*/
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
/*将堆栈的栈顶地址保存为栈的最后一个元素,即最高的地址*/
task[pid].next = &task[pid];
/*next指针指向自己*/
/*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
/*将task[0]的信息全部复制到各个新进程中*/
        task[i].pid = i;/*将新进程的pid设置为变量i*/
        task[i].state = -1;/*新进程的状态为unrunnable*/
        task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];/*将每个新进程的栈顶指针都指向堆栈数组的最后一个元素*/
/*使用尾插法创建链表形成一个任务链0->1->2->3->0这样的循环*/
        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];//将当前进程设置为0号进程
	asm volatile(
/*把0号进程的esp放入esp寄存器*/
    	"movl %1,%%esp\n\t" 	/* set task[pid].thread.sp to esp */
/*push0号进程的esp,因为此时栈为空,esp==ebp,所以此处等同于push ebp*/
    	"pushl %1\n\t" 	        /* push ebp */
/*将0号进程的eip,即process函数的入口地址push到当前堆栈*/
    	"pushl %0\n\t" 	        /* push task[pid].thread.ip */
/*将栈顶元素pop到eip然后跳转*/
    	"ret\n\t" 	            /* pop task[pid].thread.ip to eip */
    	"popl %%ebp\n\t"
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
	);
}

my_start_kernel完成的任务主要是初始化进程链表,然后开始执行第一个进程。
执行完ret操作之后将会跳转到process函数中执行。
然后我们来分析process程序,process程序是一个无限循环,每循环100000000执行判断生效一次,输出KERN_NOTICE "this is process pid-,并且判断my_need_sched是否等于1,如果等于1的话,就把my_need_sched置0并且执行my_schedule()进行调度,最后执行KERN_NOTICE "this is process pid+

void my_process(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%100000000 == 0)// 没执行100000000次循环进入一次
        {
            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);
        }     
    }
}

我们再来看看myinterrupt.c文件

void my_timer_handler(void)
{
#if 1
/*函数每执行1000次,并且my_need_sched不等于1的时候执行if判断,然后把my_need_sched置为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;  	
}

分析一下函数my_timer_handler(void),#if 1 到#endif表示一个条件预编译指令,表示永远编译,一般用于测试代码中,把1改为0相当于注释掉代码段,方便调试。
my_timer_handler函数每执行1000次并且my_need_sched不为1的时候输出一段话,并且把my_timer_handler置为1.
到此我们就清楚了my_need_sched这个变量的变化过程(由process置0,由my_timer_handler置1,交替执行),和mymain.c里面的process函数结合就可以分析出进程调度的详细过程。
注:因为时间中断处理每隔一段时间就会进行一次,可能在0号进程执行过程中进行了多次时间中断处理,我们暂时把没有发生my_need_sched变量改变的时间中断处理时间忽略掉。
进程的调度过程如下:

0号进程启动,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行1号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行2号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行3号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行0号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
整个调度过程就如上面所示,process循环执行,等待时钟中断来将允许调度的变量置1,然后完成调度。
具体的调度过程我们要分析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;
/*next进程两种情况,运行和非运行要分情况处理*/
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
    	/* 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 */
/*将下面标号1:之后的语句popl ebp的地址放入prev的ip中保存*/
        	"movl $1f,%1\n\t"       /* save eip */	
/*将next的eip push到栈上,然后执行ret的话,就会从next的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; 
    	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);   	
    }
    else
{
/*将next的状态置为runnable*/
        next->state = 0;
        my_current_task = next;
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->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 */
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	);          
    }   
    return;	
}

如果next进程为非runnable状态的时候执行else,runnable的状态执行if,整个过程就是先保存当前进程的ebp和esp,然后将next进程的esp和ebp复原,并且将prev进程的eip储存为标号1的位置,最后将next进程的eip push到栈上,再执行ret指令,返回到next的eip所指向的地方执行。
因为进程是用一个循环链表连接起来的,一直从prev向next切换,总会有一次再次切换到自己,也就是prev进程,变成next进程。如果下一次进入的时候会标号1:开始执行完成ebp的复原操作。
注:为了便于理解我们用0号进程和1号进程表示。
这个schedule函数分两次执行,第一次是从0号进程跳转到1号进程,而第二次进入则是执行popl ebp恢复现场,然后return函数,返回到原调用者也就是0号进程的process里面。
总结:
通过以上代码的分析,我们可以初步了解到了,linux内核是如何完成进程的切换的。我们可以认为一个进程相当于一个堆栈,每个进程有自己的堆栈空间。如果将ebp和esp修改为另一个进程的ebp和esp,并且完成一些寄存器的保存,就相当于完成的进程的切换。

Logo

更多推荐