多任务操作系统在并行执行多任务时,实际上是不断地在任务间进行切换的,也就是切换上文。首先要保存前一个进程的上下文,然后调度一个就绪的进程,并载入该进程的上下文,cpu开始执行该进程的代码。在切换上下文时,最重要的就是切换eip寄存器的值和esp寄存器的值,eip寄存器指向的指令即时cpu即将执行的指令,esp寄存器指向栈顶。下面我们通过一段比较简单的代码来演示一下cpu是如何切换进程的。
代码已经由孟宁老师编写好了,下载地址孟宁-mykernel
按照readme文件中的的操作打好补丁。使用mykernel-1.1文件中的mymain.c、mypcb.h、myinterrupt.c替换linux-3.9.4/mykernel中的相应的文件,然后make。
这是修改后的linux内核源码,代码太长,我们从中取重要的部分,首先来看mypcb.h,这是我们自定义的进程pcb。
/*
* 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 */
//该进程的cpu状态
struct Thread {
//eip寄存器的值
unsigned long ip;
//栈顶
unsigned long sp;
};
// 进程控制块
typedef struct PCB{
//进程id
int pid;
//进程状态
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
//进程栈,栈底是数组的最后一个元素位置
char stack[KERNEL_STACK_SIZE];
/* CPU-specific state of this task */
//cpu状态
struct Thread thread;
//进程入口
unsigned long task_entry;
//指向下一个进程的指针
struct PCB *next;
}tPCB;
void my_schedule(void);
下面看一下mymain.c,这里包括我们手工创造的第0号进程,以及其他进程的fork();
/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
*
* Copyright (C) 2013 Mengning
*
*/
#include "mypcb.h"
//存放进程pcb的数组
tPCB task[MAX_TASK_NUM];
//指向当前进程pcb的指针
tPCB * my_current_task = NULL;
//调度标志,为1时表示需要调度了,为0时表示不需要调度
volatile int my_need_sched = 0;
//进程代码入口
void my_process(void);
void __init my_start_kernel(void)
{
//进程的pid即是pcb表的下标
int pid = 0;
int i;
/* Initialize process 0*/
//0号进程是手工造出来的
task[pid].pid = pid;
//0代表就绪态,-1代表睡眠,>0代表暂停
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 */
//把进程表填满,相当于fork()
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;
//把当前进程指针指向0号进程
my_current_task = &task[pid];
asm volatile(
//%1即参数task[pid].thread.sp,赋给esp
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
//进程开始之前栈是空的,所以esp==ebp,task[pid].thread.sp压栈即把栈底压栈
"pushl %1\n\t" /* push ebp */
//把task[pid].thread.ip压栈
"pushl %0\n\t" /* push task[pid].thread.ip */
//把eip弹出,之所以这么做,是因为eip的值是不能通过movl指令设置的,但是可以使用从栈中弹出的方式为其赋值
"ret\n\t" /* pop task[pid].thread.ip to eip */
//同样弹出ebp,原因同上
"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%100000000 == 0)
{
//打印自己的pid
printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
//需要调用进程调度函数了
if(my_need_sched == 1)
{
//复位my_need_sched
my_need_sched = 0;
//调度,当前进程执行到这里就不再执行了,当该函数返回时,说明又调度到该进程了
my_schedule();
}
//执行调度函数后,这个printfk是不会执行的,当执行到这里时表明又调度到这个进程执行了
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}
再来看一下myinterrupt.c
/*
* linux/mykernel/myinterrupt.c
*
* Kernel internal my_timer_handler
*
* 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"
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
//每1000次时钟中断并且没有正在进行进程调度
//其实这里并不一定每1000次时钟中断就会切换一次进程
//有可能正好1000次的时候,my_need_sched为1,还没有被复位呢
if(time_count%1000 == 0 && my_need_sched != 1)
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
//把需要调度标志设置为1表示需要调度了
my_need_sched = 1;
}
//时钟中断产生的次数+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 */
{
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
/* switch to next process */
asm volatile(
//当前进程的ebp入栈
"pushl %%ebp\n\t" /* save ebp */
//保存当前进程的esp
"movl %%esp,%0\n\t" /* save esp */
//把esp恢复为下一个进程pcb内的esp
"movl %2,%%esp\n\t" /* restore esp */
//把标号1处的地址存入pcb中的ip,以便下次该进程恢复时从此处开始执行
"movl $1f,%1\n\t" /* save eip */
//把下一个进程pcb的eip入栈
"pushl %3\n\t"
//弹出下一个进程的eip值到寄存器
"ret\n\t" /* restore eip */
"1:\t" /* next process start here */
//标号1处的地址就下面这一行代码的地址,当前进程执行到ret后就切换到下一个进程了
//所以当前进程恢复时首先从下面这一行代码开始执行,首先把栈基址弹出
"popl %%ebp\n\t"
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
else
{
//新创建的进程状态在mymain.c中我们设置为-1,新进程是未就绪的,所以首先设置为就绪态
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 */
//新进程和已经运行过的进程的唯一区别在于,已经运行过的进程在切换时把栈基址保存到栈中了
//新进程的栈为空,要初始化栈基址,下面这一行代码就是使用pcb中sp初始化栈基址,其他的和上面一模一样
"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;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)