林光伟

原创作品转载请注明出处

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

 

一、准备工作

1、安装qemu,

我有一份qeum-2.0.0.rc0的源码,以来是用来调试arm的,现在将i386平台的也编译下

./configure --target-list='arm-softmmu arm-linux-user armeb-linux-user i386-softmmu'
make -j4
sudo make install

2、下载kernel

wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz

3、下载补丁

 wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch

4、解压linux内核源码

tar -xvf linux-3.9.4.tar.xz

5、给linux打上本实验所修改的补丁

cd linux-3.9.4
patch -p1 < ../mykernel_for_linux3.9.4sc.patch

6、编译内核

make allnoconfig
make -j4

二、单任务内核测试

1、采用qeum模拟器运行内核

qemu-system-i386 -kernel arch/x86/boot/bzImage

运行结果

https://github.com/mengning/mykernel主页上的说明只是给出了该实验的基本结构,单任务外加一个定时中断外理,涉及到的两个主要文件就是myinterrupt.c与mymain.c,

观查mykernel_for_linux3.9.4sc.patch这个补丁文件,可以看修改了下面这几个文件(当然还有Makefile,这里不去观注)

arch/x86/kernel/time.c
 linux-3.9.4.new/include/linux/start_kernel.h
include/linux/timer.h
init/main.c

将arch/x86/kernel/time.c的定时中断入口处指引到myinterrupt.c的void my_timer_handler(void),将init/main.c的start_kernel函数指引到mymain.c,这样就可以将原来复杂的linux kernel代码隐藏,后面我们只需要关注修改myinterrupt.c与mymain.c就行了。

关键代码 (arch/x86/kernel/time.c)

static irqreturn_t timer_interrupt(int irq, void *dev_id)
 {
     global_clock_event->event_handler(global_clock_event);
    my_timer_handler();//从这里调用myinterrupt.c
     return IRQ_HANDLED;
 }

关键代码 (init/main.c)

asmlinkage void __init start_kernel(void)
{
  ///////////////这里省略几十行///////////////////////
    ftrace_init();
    my_start_kernel();//调用mymain.c
    /* Do the rest non-__init'ed, we're now alive */
    rest_init();
}

 

三、改成时间片轮转多任务内核

1、进入到mykernel目录,更新程序

cd mykernel
rm *.h *.c
wget https://raw.githubusercontent.com/mengning/mykernel/master/mymain.c wget https://raw.githubusercontent.com/mengning/mykernel/master/myinterrupt.c wget https://raw.githubusercontent.com/mengning/mykernel/master/mypcb.h

2、为了便于观察,需要减少任务轮转的时间粒度

将myinterrupt.c的“if(time_count%1000 == 0 && my_need_sched != 1)”改成 if(time_count%50 == 0 && my_need_sched != 1)

3、再次编译内核

cd ..
make -j4

4、再次通过qeum进行模拟运行

qemu-system-i386 -kernel arch/x86/boot/bzImage

这里就可以看到多任务的运行结果了

 

 四、代码分析

这个简内的时间片多任务调度内核的核心是mpcb.h的PCB结构体的定义,该结构体决定了运行算法。

typedef struct PCB{
    int pid;                                     //任务进程id号                
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */  //任务进程的状态,我个人更喜欢用enum{PCP_UNRUNNABLE = -1, PCB_RUNNABLE, PCB_STOPPED}state;这种方式表示
    char stack[KERNEL_STACK_SIZE];                        //预分配堆栈内存,大小为KERNEL_STACK_SIZE
    /* CPU-specific state of this task */
    struct Thread thread;                             //任务进程的入口函数与堆栈指针(见struct Thread的定义)
    unsigned long    task_entry;                         //任务进程的入口函数
    struct PCB *next;                                //指向下一个入PCB结构体
}tPCB;

在内核启动后会进入 void __init my_start_kernel(void)这个函数,运行过程是

 

 

这个函数很好理解,最后PCB链表会形成一个环型结构,在调度0号进程时,用到了汇编 

    asm volatile(
        "movl %1,%%esp\n\t"     //令esp指向指定位置task[pid].thread.sp 作为堆栈初始地址
        "pushl %1\n\t"          //令堆栈地址本身入栈
        "pushl %0\n\t"          //task[pid].thread.ip入栈,即进程函数的地址入栈
        "ret\n\t"               //ret 相当于popl eip,eip的值变为进程函数的地址,cpu就跳到该地址去执行
        "popl %%ebp\n\t"        //还原ebp,实际上不会被执行到。
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
    );

上述汇编运行完后,cpu就跳到 void my_process(void)上去了,而且esp的值为 task[pid].thread.sp - 4,也就是(unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1-4];

void my_process(void)
{
    int i = 0;
    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 ,当定时器中断时,my_need_sched会置1时,表示分配给该进程的时间结束了,需要调度下一个进程,my_need_sched ,于是就进入了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 */
    {
        /* 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"                               //指定跳转的位置为next->thread.ip
            "ret\n\t"                 /* restore  eip */ //跳到 next->thread.ip
            "1:\t"                  /* next process start here *///(位置1)
            "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->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 */ //     (位置2)将prev->thread.ip指向上一个汇编代码的1:\t的那个位置
            "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;    
}

my_schedule()用到的两段汇编代码有所区别,最下面那一段(就是被 else包含的那段)汇编是处理从来都没有运行过的进程,(位置2)处的代码将prev->thread.ip指向位(置1)处,这样再次调用prev进程时,将从(位置1)处开始。

 

五、总结:多任务进程之间的切换实际上就是对eip、esp 、ebp之间的操作与保存,每道进程都有自已独立的stack,相对于C语言,有像是在玩杂技表演。 

 

 

 

 

 

posted on 2015-03-15 23:14  林德伟  阅读(157)  评论(0)    收藏  举报