前言

这是第二周的报告。本周的实验是:完成一个简单的时间片轮转多道程序内核代码,代码见视频中或从mykernel找。

老师已经为我们搭好了实验的环境——linux3.9.4下一个极其迷你的系统。我们不用去关心别的东西,只需要知道这个迷你系统从my_start_kernel函数开始,系统时钟中断会执行my_timer_handler函数。剩下的留给我们自己发挥。同时,实验要写的代码已经给出,所以完成这个实验的难度不大。实验的关键是理解所给的代码为什么要这么写,也就是理解程序如何切换。

代码与分析

mypcb.h定义进程的属性和信息。

#define MAX_TASK_NUM 10                //这个系统最多十个进程
#define KERNEL_STACK_SIZE 1024*8    //每个进程的栈的大小

//进程的各种状态
#define MY_RUNNING 1     
#define MY_SLEEP 2
#define MY_DEAD 3

//用于进程调度时,保存它栈地址和代码地址
struct Thread
{
    unsigned long ip;    
    unsigned long sp;
};

typedef struct PCB
{
    int pid;            //进程总有个编号吧?
    volatile long state;
    char stack[KERNEL_STACK_SIZE];
    struct Thread thread;
    unsigned long entry;    //进程第一次执行开始的地方
    struct PCB *next;        //用于构造进程链表
    unsigned long priority;    //暂时没用到
    
}tPCB;


void my_schedule(void);

总的来说my_start_kernel就是创建各个进程,并且进入0号进程。

#include "mypcb.h"    //PCB结构信息

tPCB my_task[MAX_TASK_NUM];    //创建若干个PCB。也就是创建若干个任务
tPCB *my_current = NULL;        //用来表示当前任务的指针。

//是否要进行程序切换的标记。1为需要,0相反。
//这个变量由my_timer_handler和my_process修改
extern volatile int my_need_sched;   

//创建的进程都执行这个函数
void my_process(void)
{
    unsigned count = 0;
    unsigned slice = sizeof(count);
    while (1)
    {
        count += 1;
        
        //程序执行一定时间后检查是否要进行程序切换,不需要则输出信息
        if (!(count<<slice))
        {
            if (my_need_sched == 1)
            {
                my_need_sched = 0;
                my_schedule();
            }
            else
            {
                printk(KERN_NOTICE "process %d is running\n", my_current->pid);
            }
        } 
    }
}

//迷你系统从这个函数开始执行。
void __init my_start_kernel(void)
{
    int i;

    /* init task 0 */
    //初始化第一个进程,0号
    my_task[0].pid = 0;
    my_task[0].state = MY_RUNNING;
    my_task[0].thread.sp = (unsigned long)&my_task[0].stac[KERNEL_STACK_SIZE-1];
    //设置程序的入口,也就是my_process的地址
    my_task[0].entry = my_task[0].thread.ip = (unsigned long)my_process; 
    //环形链表
    my_task[0].next = &my_task[0];

    /* then init other "processes" */
    //初始化其他进程。
    for (i = 1; i < MAX_TASK_NUM; i++)
    {
        memcpy(&my_task[i], &my_task[0], sizeof(tPCB));
        my_task[i].pid = i;
        my_task[i].state = MY_SLEEP;    //只有0号醒着,其它都睡着了
        my_task[i].thread.sp += (unsigned long)sizeof(tPCB);
        /* to make the list a big loop */
        //环形,链表最后一个元素指向第一个元素
        my_task[i].next = my_task[i-1].next;
        my_task[i-1].next = &my_task[i];

    }
    
    /* going to switch to task 0! */
    printk(KERN_NOTICE "main going to switch to task 0\n");
    //好紧张,要开始切换了。
    my_current = &my_task[0];
    asm volatile(
        "movl %1, %%esp\n\t"    //将esp和ebp设置为任务0的栈
        "movl %1, %%ebp\n\t"
        "pushl %0\n\t"        //不能直接修改eip,所以先将任务0的地址入栈
        "ret\n\t"                //再通过ret赋值给eip
        :
        :"c"(my_task[0].thread.ip), "d"(my_task[0].thread.sp)
    );
}

myinterupt.c实现了时钟中断处理和程序调度。

//记录时钟中断了多少次
volatile unsigned long time_count = 0;
volatile int my_need_sched = 0;
//当前进程的PCB
extern tPCB *my_current;


/*
 * Called by timer interrupt.
 */
//每次时钟中断都会执行它
void my_timer_handler(void)
{
    time_count += 1;
    //如果若干次中断后需要程序调度,修改my_need_sched。
    if ((!(time_count<<COUNT_SLICE)) && my_need_sched != 1)
    {
        my_need_sched = 1;    //my_process会检查它是否为1,若是,就执行下面的调度程序(程序主动调度)
    }
}

//
void my_schedule(void)
{
    tPCB *next = my_current->next;    //将要执行的进程
    tPCB *pre = my_current;

    //错误检查
    if (!next)
    {
        printk(KERN_NOTICE "switch to NULL\n");
        while (1);
    }

    printk(KERN_NOTICE "task %d is going to task %d\n", my_current->pid, next->pid);
    my_current = next;

    if (next->state == MY_RUNNING)
    {
        //如果要切换执行的程序已经醒了
       asm volatile (
           "pushl %%ebp\n\t"        //保存当前ebp,esp,eip
           "movl %%esp, %0\n\t"
           "movl $1f, %1\n\t"
           "movl %2, %%esp\n\t"    //切换到将要运行的栈
           "pushl %3\n\t"          //不能直接修改eip,所以通过给压栈再ret方式赋值
           "ret\n\t"                //切换eip。现在已经是另外一个进程了
           "1:\n\t"                 //切换后从这里开始
           "popl %%ebp\n\t"        //恢复这个进程被切换之前的ebp
           :"=m"(pre->thread.sp), "=m"(pre->thread.ip)
           :"m"(next->thread.sp), "m"(next->thread.ip)
       ); 
        
        
    }
    else if (next->state == MY_SLEEP)
    {
        //如果要切换执行的程序还没有运行过。过程和上面的切换差不多
        next->state = MY_RUNNING;    //先叫醒它
        asm volatile (
            "pushl %%ebp\n\t"
            "movl %%esp, %0\n\t"
            "movl $1f, %1\n\t"
            "movl %2, %%esp\n\t"
            "movl %2, %%ebp\n\t       //新进程的栈是空的,所以要设置ebp
            "pushl %3\n\t"
            "ret\n\t"
            "1:\n\t"            //被切换的进程下次执行从这里开始,而刚被叫醒的进程从my_process开始。
            "popl %%ebp\n\t"     //这个被切换的进程需要恢复ebp
            :"=m"(pre->thread.sp), "=m"(pre->thread.ip)
            : "m"(next->thread.sp), "m"(next->thread.ip) 
        );
    }
}

结果

进程从0到9,又从9到0循环执行。

图片描述


总结

了解了进程如何进行初始化和切换:实质就是寄存器和栈的切换。
问题:切换不需要保存当前的eax等通用寄存器吗?

xxtsmooc
原创作品转载请注明出处
《Linux内核分析》MOOC课程
http://mooc.study.163.com/course/USTC-1000029000


shanyin
129 声望3 粉丝

写代码