一个简单的时间片轮转多道程序内核代码分析

 郑斌  原创作品转载请注明出处

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

第二周的实验内容分析

 

1.实验环境:实验楼中执行。

2.程序代码

整个程序主要由3个文件构成:

1)mypcb.h。 主要定义了进程结构体 

2)mymain.c。 主要定义了my_start_kernel初始化第一个进程的函数,和my_process实现进程运行输出和调度。

3)  myinterrupt.c。  主要定义了my_time_handler函数通过计时器设置进程是否需要调度,实现分时,my_schedule函数实现进程调度,完成上一个进程的现场保存和转到下一个进程的功能。

 

具体代码如下:

 

 

 

mypcb.h

 

/*
 *  linux/mykernel/mypcb.h
 *
 *  Kernel internal PCB types
 *
 *  Copyright (C) 2013  Mengning
 *
 */

#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*8

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

 

mymain.c

/*
 *  linux/mykernel/mymain.c
 *
 *  Kernel internal my_start_kernel
 *
 *  Copyright (C) 2013  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>


#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 */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
        task[i].state = -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 */
        "popl %%ebp\n\t"
        : 
        : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)    /* input c or d mean %ecx/%edx*/
    );
}   
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);
        }     
    }
}

 

myinterrupt.c

#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#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 */
    {
        /* 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; 
        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 */    
            "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;    
}

 

 3.实验过程。

进入实验楼,在对应文件目录下,把3个文件编辑到mykernel文件夹下,然后make编译

 

4.程序代码分析

为了方便说明,本文中把代码中的进程根据下标叫为进程(pid),如进程(0)即为刚开始的0号进程。

整个代码部分,CPUmy_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];

 

先是初始化了进程(0)的信息。这里主要关注两点:

1.task_entry=thread.ip=my_process

我的理解是进程的eip,即执行内容为my_process函数前面加(unsigned long)表示该函数执行的入口地址。

2.thread.sp 的赋值表达式是(unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]

我们回顾在mypcb.h中定义PCB部分

 

所以进程(0)sp是从stack字符串的末尾开始的,符合第一周课程中的栈地址从高地址向低地址

然后是复制另外几个进程的信息。

 

state = -1 表示开始未执行。

这里有个小细节挺有意思,开始乍一看我以为这样写没有进程的下一个进程是进程(0)

然后分析task[i].next = task[i-1].next;

        task[i-1].next = &task[i];

这两条语句,手写模拟后发现其实不是那样的,最后的数组next情况是

task.next[] task[0] task[1] task[2] task[3]

对应内容     task[1] task[2] task[3]  task[0]

 

 

然后执行

 

关键字说明:

asm表示嵌入式汇编,其中%0%1等表示下面的参数,可以简单理解为函数参数,如此处%0表示task[pid].thread.ip,其他情况类似。

volatile表示编译器不需要优化的代码。

 

我们来观察其中的汇编代码部分。  注意当前的堆栈是进程(0)的堆栈空间

 

"movl %1,%%esp\n\t" 	/* set task[pid].thread.sp to esp,此时esp是进程(0)的thread.sp*/
"pushl %1\n\t" 	        /* push ebp ,把进程(0)的thread.sp压栈,因为当前进程(0)的栈为空,ebp跟esp指向相同,所以相当于把ebp压栈*/
"pushl %0\n\t" 	        /* push task[pid].thread.ip ,把进程(0)的thread.ip压栈*/
"ret\n\t" 	        /* pop task[pid].thread.ip to eip ,eip为进程(0)的thread.ip,接下来开始执行my_process函数。*/
"popl %%ebp\n\t"	//我觉得这句代码并没有被执行
: 
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	

下面来看my_process函数。

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

 

这段代码中定义了变量i初始值为0,然后一个循环体while(1)i每次加1,当i10000000才倍数时,输出“this is process +进程标号”

若变量my_need_sched1,把my_need_sched值置为0,然后执行my_schedule函数(该函数将在后面分析变量my_need_sched初始值为0,用来标记是否需要进行进程切换,0表示不用,1表示要切换。

 

 

再来看myinterrupt.c文件里的代码内容。

开头的变量定义有

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

extern表示引用了其他地方(这里为mymain.c)定义了的变量。

 

函数my_timer_handler分析:

这里我简单理解为代码执行后,内核的定时器也是自动执行的,即my_timer_handler也是自动开始执行。

 

该函数的功能为每次计时器走一次,即该函数执行一次,time_count变量加1。当time_count1000的倍数并且my_need_sched0时候,输出">>>my_timer_handler here<<<",并把my_need_sched值置为1.表示当前进程需要被调度(这里体现了系统进程调度的分时处理思想)。

 

 

then是其中一个非常重要的调度函数my_schedule()。下面我们来进行代码分析。

 

void my_schedule(void)
{
//申明两个进程指针 prev和next,用来标记当前进程的pid和下一个进程的pid
    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;    //next指向当前进程的下一个进程
    prev = my_current_task;             //prev指向当前进程

/*这里的if将在所有进程(这里是4个)都运行了才执行,前3次都跳转到else的代码部分,所以可以先看下面的else部分的分析后再回到这里*/
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" 
            "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);       
}

/*程序初始状态只有进程(0)的state为0,其他进程都为-1(见初始化部分),所以if判断的前3次都将执行此部分,依次运行进程(1)、进程(2)、进程(3)并state都标记为0*/
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 */    
            "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函数的功能为保存当前进程的现场,并调度运行下一个进程。

由于程序初始状态只有进程(0)state0,其他进程都为-1(见初始化部分),所以当

3次调用 my_schedule函数时,if判断的都将执行else{}部分,依次运行进程(1)、进程(2)、进程(3)state都标记为0

接下来调用 my_schedule函数时,if判断都执行if(next->state == 0){}部分的代码。

 

我们先看来if判断中else{}部分的代码。以程序第一次调用my_schedule函数为例,此时进程(0)正在运行。所以栈空间是进程(0)的堆栈空间。

prevtask[0],即进程(0)nexttask[1],即进程(1)

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,把ebp压栈 */
            "movl %%esp,%0\n\t"   /* save esp,保存进程(0)的thread.sp为esp*/
            "movl %2,%%esp\n\t"   /* restore  esp ,置当前esp为进程(1)的esp*/
            "movl %2,%%ebp\n\t"   /* restore  ebp ,进程(1)未开始执行,ebp和ebp指向相同,置当前ebp为进程(1)的ebp */
            "movl $1f,%1\n\t"   /* save eip ,保存进程(0)的eip为指向标记1:部分的下一条指令,在if的嵌入式汇编代码块中,至此进程(0)的ebp入栈,并保存了esp和eip*/
            "pushl %3\n\t"        /*把进程(1)的thread.ip压栈*/
            "ret\n\t"             /* restore  eip ,eip指向进程(1)的thread.ip,开始执行进程(1)*/
            : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
            : "m" (next->thread.sp),"m" (next->thread.ip)
        );          
    }   

 

 

我们以第四次执行my_schedule函数时为例,将调用if(next->state == 0){}部分的代码,且接下来每次调用my_schedule函数时为例,都将调用if(next->state == 0){}部分的代码。

prevtask[3],即进程(3)nexttask[0],即进程(0)

 

 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {
        /* switch to next process */
        asm volatile(    
            "pushl %%ebp\n\t"          /* save ebp ,保存进程(3)的ebp*/
            "movl  %%esp,%0\n\t"      /* save esp ,保存进程(3)的thread.sp为esp*/
            "movl  %2,%%esp\n\t"     /* restore  esp,置esp为进程(0)的thread.sp,恢复进程(0)现场的esp */
            "movl  $1f,%1\n\t"       /* save eip ,保存进程(3)的eip为指向标记1:部分的下一条指令
                      至此进程(3)的ebp入栈,并保存了进程(3)的esp和eip
*/ "pushl %3\n\t" /*把进程(0)的thread.ip入栈*/ "ret\n\t" /* restore eip ,eip指向进程(0)的thread.ip,开始恢复执行进程(0),查看上面分析可以看到即为从下面的汇编指令部分开始执行*/ "1:\t" /* next process start here */ "popl %%ebp\n\t" /*进程(0)开始恢复执行的第一句,先把ebp出栈,这里提现了不同进程有各种工作的堆栈空间*/ : "=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); }

 

 

至此代码分析结束,接下来为进程的循环调度工作。

 

 

总结:

进程启动运行,会有自己的数据存储空间和堆栈空间。

内核会有定时器自动启动用以实现分时功能。

进程之间的切换要保存现场,包括esp,eip,ebp等。

posted @ 2015-03-12 18:19  夕羊  阅读(212)  评论(0编辑  收藏  举报