基础知识

栈帧的含义

  在高级语言中,当函数被调用时,系统栈会为这个函数开辟一个栈帧,并把它压入到栈里面。新开辟的栈帧中的空间被它所属的函数所独占,当函数返回的时候,系统栈会清理该函数所对应的栈帧以回收栈上的内存空间。

每个函数都拥有自己独占的栈帧空间,有两个特殊的寄存器用于标识栈帧的相关参数:

  1. ESP寄存器,永远指向栈帧的顶端;

  2. EBP寄存器,永远指向栈帧的底部;

  在调用一个函数的时候,函数所需要的参数首先会依次被压入的栈上,其次压入对应的返回地址,最后跳转到被调用的函数执行代码并开辟新的栈帧。新的栈帧的底部保存有EBP寄存器的值,基于EBP寄存器可以获取到被调用函数所需要的参数信息。

Volatile修饰符的意义

  类型修饰符,确保本条指令不会因编译器的优化而忽略系统总是重新从它所在的内存读取数据,编译器对访问该变量的代码不再进行优化,从而可以提供对改地址的访问稳定。 优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
一般来说:volatile用在以下几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

实验代码分析

线程结构定义:

/* CPU-specific state of this task */
struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};  

该结构体只存了eip和esp
进程的结构体定义:

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;  

用task数组来存储进程,并初始化当前进程

tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;   

定义并初始化是否需要调度标志

volatile int my_need_sched = 0;  

1表示需要调度,0表示不需要调度
编译运行mykernel,代码是从my_start_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 *///0号进程初始化为运行态
    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号进程就会一直运行
    /*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;//除0号进程,其它进程初都始化为未运行态
        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];//将当前进程设为0号进程
	asm volatile(
    	"movl %1,%%esp\n\t" 	/* set task[pid].thread.sp to esp */
    	"pushl %1\n\t" 	        /* push ebp *///此时ebp和esp都指向相同的地址
    	"pushl %0\n\t" 	        /* push task[pid].thread.ip */
    	"ret\n\t" 	            /* pop task[pid].thread.ip to eip *///这里去执行my_process函数了
    	"popl %%ebp\n\t"      //这里之前已经执行ret了,所以这里不执行
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/(ecx保存eip,edx保存esp)
	);
}   

此时的堆栈情况为:

进程函数:

void my_process(void)
{
    int i = 0;
    while(1)
    {
        i++;
        if(i%10000000 == 0) //计数1千万次,执行一次
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
			//打印当前进程id
            if(my_need_sched == 1)
            {
                my_need_sched = 0;//一旦发生调度,就置成不需要调度的状态,等待下一次时间中断
        	    my_schedule();  //发生进程调度,发生进程切换
        	}
        	printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}  

但是0号进程的my_need_sched一开始为0,如何才能进入进程调度呢?这里涉及到时间中断函数:

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

产生的时间中断使得my_need_sched的值变成1,触发进程调度。 进程调度函数:(这里为了方便理解代码,我们将进程设为两个,分别为0号进程和1号进程,两个进程互相切换)

我们按函数的执行流程来分析,首先是0号进程切换到1号进程,此时1号进程的状态是unrunnable。
源代码如下:

    else
    {
        next->state = 0;//这里的进程还没有被调度,第一次运行时,这里是将1号进程状态置0
        my_current_task = next;//第一次运行时,此时进程是1号进程
        printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);//此时0号进程切换到1号进程,见图1
    	/* 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;	
}    

此时的堆栈情况如下:

运行效果图(图1):

然后是1号进程切换到0号进程,此时0号进程的状态是runnable。
源代码如下:

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; //第一次运行时,这里是1号进程切换到0号进程
    	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  //运行效果图见图2
    	/* 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 */	//这里是模拟eip,保存之前进程的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)
    	); 
 	
    }  

此时的堆栈情况如下:

运行效果图(图2):

源代码链接如下:

https://github.com/De4dCr0w/mykernel/blob/master/mymain.c

https://github.com/De4dCr0w/mykernel/blob/master/myinterrupt.c

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