关于一个精简进程调度的过程分析

bytefly + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程

概述

对于多任务操作系统而言,操作系统的运行是由时钟中断来驱动的,并通过调度程序来完成多任务间的切换(包括:进程的上下文的切换,进程的状态切换等)。因此多任务操作系统运行的关键是调度程序,那么核心操作也便是在调度程序中完成。

进程PCB的定义

#define MAX_TASK_NUM        4           //定义最大任务数
#define KERNEL_STACK_SIZE   1024*8   //定义一个进程的栈的大小为4K

/* CPU-specific state of this task */
//定义Thread结构体用于保存进程的上下文,
//主要是进程将要执行的指令地址以及栈顶指针的位置
struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};
/*
 * 下面定义进程控制块,包含一个进程的五方面信息
 * 分别是:资源信息 - 进程的栈大小  和  
 * 调度信息 - 进程的状态、上下文、进程的入口函数 、进程ID
 */
typedef struct PCB{
    int pid;                                //定义了进程的ID
    volatile long state;	                //定义了进程的运行状态:-1 unrunnable, 0 runnable, >0 stopped
    char stack[KERNEL_STACK_SIZE];          //定义了进程的栈大小为8K
    struct Thread thread;                        //指定当前进程的上下文
    unsigned long	task_entry;               //指定一个进程入口
    struct PCB *next;
}tPCB;

void my_schedule(void);                        //调度程序

mymain.c

#include "mypcb.h"

tPCB task[MAX_TASK_NUM];                   //定义MAX_TASK_NUM个进程
tPCB * my_current_task = NULL;              //定义指向当前进程的指针
volatile int my_need_sched = 0;              //标明是否需要调度

void my_process(void);
//my_start_kernel 是整个精简系统的入口函数,
//由此开始进程的初始化和进程的执行
//完成所有进程状态的设置,并将0号进程启动
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; //my_process作为0号进程的入口函数
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; //初始化栈顶指针
    task[pid].next = &task[pid];     //将各个进程链到一个链表中
   
    //初始化其他的进程
    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;
//下面的代码用于设定执行进程的一些准备信息
//设定esp和ebp(进程的栈),设定eip(进程的入口)
    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 */,此时ebp 与 esp的值相同(因为是空栈)
    	"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 "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile unsigned 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
 */
//函数my_timer_handler由定时器中断调用,它使用当前执行进程的堆栈来运行
void my_timer_handler(void)
{
#if 1
 /* 如果时间计数达到1000次(发生了1000次时钟中断),
  * 则进行调度
  */
    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(L my_current_task == NUL
        || 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 */
//保存进程上下文主要有:esp,ebp,eip(还应该有flags)
    	asm volatile(	
        	"pushl %%ebp\n\t" 	    /* save ebp */
//下面两条汇编指令实现了进程的栈顶指针的切换
        	"movl %%esp,%0\n\t"    /* save esp */
        	"movl %2,%%esp\n\t"     /* restore  esp */
//下面三条汇编代码实现了ip的切换
        	"movl $1f,%1\n\t"         //$1f指1:处的代码(pop1 %ebp)在内存中的存储地址,也就是将此条指令的地址保存到内存中(此内存单元将在切换进程的上下文时将内存单元中的数据(指令的地址)放到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)        //m表示内存变量
    	); 
    	my_current_task = next; 
    	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);   	
    }
    else
    {
        next->state = 0; //如果next进程没有运行,则改变进程的状态并切换到next进程
        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, ret <=> popl %eip */
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	);          
    }   
    return;	
}

附:C语言中内嵌汇编

格式

asm(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分);

一个简单的程序分析如下:

#include <stdio.h>
int main()
{
    //求解val1+val2=val3
    unsigned int val1 = 1;
    unsigned int val2 = 2;
    unsigned int val3 = 0;
    pirntf("val1:%d, val2:%d, val3:%d\n", val1, val2, val3);
    asm volatile(
            "movl $0, %%eax\n\t" //clear %eax to zero
//说明:由于输入部分指定了val1放到ecx中,而val1由对应%1,故下面就是将val1加到eax中
            "addl %1, %%eax\n\t" //*eax+=val1
            "addl %2, %%eax\n\t" //%eax+=val2
            "movl %%eax, %0\n\t" //val2=%eax
            :"=m"(val3) //output=m mean only write output memory
//说明c-ecx d-edx, 说明将val1的值放到ecx中,将val2放到edx中
            :"c"(val1),"d"(val2) //input C or D mean %ecx / %edx  
            );
}

说明:

再输入输出部分,所有的值按照出现的顺序分别于指令操作数%0, %1, %2对应
操作数最多有10个,分别用%0 ... %9进行对应

注意:val1编号为%1,val2编号为2

内嵌汇编的常用限定词

限定词 描述
"a" 将输入标量放到eax中
"b" 将输入标量放到ebx中
"c" 将输入标量放到ecx中
"d" 将输入标量放到edx中
"s" 将输入标量放到esi中
"D" 将输入标量放到edi中
"q" 将输入变量放到eax,ebx,ecx,edx中的其中一个
"r" 将输入变量放入通用寄存器中,也就是eax,ebx,ecx,edx,esi,edi中的一个
"A" 放到eax和edx,将eax和edx合成一个6位的寄存器
"m" 内存变量
"o" 操作数为内存变量,但是其寻址方式是偏移量类型
"I" 0-31之间的立即数(用于32为移位指令)
"J" 0-63之间的立即数(用于64位移位指令)
"N" 0-255之间的立即数(用于out指令)
"i" 立即数
"n" 立即数,有些系统不支持除字以外的立即数,这些系统应该使用"n"
"=" 操作数在指令中是只写的(输出操作数)
"+" 操作数在指令中是读写类型的(输入输出操作数)
"f" 浮点数
"t" 第一个浮点寄存器
"u" 第二个浮点寄存器
"G" 标准的80387
% 该操作数可以和下一个操作数交换位置
# 部分注释,从该字符到期后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则期后的字母被忽略

你可能感兴趣的:(进程)