bytefly + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程
对于多任务操作系统而言,操作系统的运行是由时钟中断来驱动的,并通过调度程序来完成多任务间的切换(包括:进程的上下文的切换,进程的状态切换等)。因此多任务操作系统运行的关键是调度程序,那么核心操作也便是在调度程序中完成。
#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); //调度程序
#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);
}
}
}
#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 |
% | 该操作数可以和下一个操作数交换位置 |
# | 部分注释,从该字符到期后的逗号之间所有字母被忽略 |
* | 表示如果选用寄存器,则期后的字母被忽略 |