赵建清+原创作品转载请注明出处+《Linux内核分析》MOOC课程
mykernel是中国科学技术大学孟宁老师基于linux系统开发的一个精简操作系统内核,mykernel使用均匀时间片轮转的调度策略。本文以mykernel为例,分析该调度策略的工作机制以及如何进行上下文切换。具体实验步骤及全部源代码请参考孟宁老师的github:
进程是多道程序设计操作系统中的基本概念,通常认为进程是程序执行的一个实例。首先说明mykernel中描述进程的数据结构,然后具体分析在mykernel中如何进行上下文切换。
mykernel中进程控制块
-
/*
-
* 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 */
-
struct Thread {
-
unsigned long ip;
-
unsigned long sp;
-
};
-
-
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;
-
void my_schedule(void);
|
struct PCB是描述进程控制信息的结构。其中预先分配的stack数组用做进程的堆栈,大小为1Kb; thread.ip和thread.sp分别用于保存指令指针寄存器EIP和进程运行过程中栈指针寄存器ESP,next指针用于进程链表中的后继进程的PCB.
mykernel的调度子系统
struct PCB定义了进程控制块,用于保存进程的上下文信息以及分配堆栈。mykernel把所有进程的PCB组织成一个循环链表,使用时间片轮转调度策略依次选中进程运行。调度子系统中各组件的关系如下:
下面分析时间片轮转调度策略的具体实现,先上源代码:
-
/*
-
* linux/mykernel/mymain.c
-
*
-
* Kernel internal my_start_kernel
-
*
-
* Copyright (C) 2013 Mengning
-
*
-
*/
-
#include
-
#include
-
#include
-
#include
-
#include
-
-
-
#include “mypcb.h”
-
-
tPCB task[MAX_TASK_NUM];
-
tPCB * my_current_task = NULL;
-
volatile int my_need_sched = 0;
-
-
void my_process(void);
-
-
-
void __init my_start_kernel(void)
-
{
-
int pid = 0;
-
int i;
-
/* Initialize process 0*/
-
task[pid].pid = pid;
-
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 */
-
for(i=1;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;
-
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 */
-
“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);
-
}
-
}
-
}
系统使用函数my_start_kernel初始化PCB list,依次设置每个进程pid、状态、堆栈空间、下一条指令运行的地址以及后继PCB的指针,最后通过内嵌汇编代码设置系统堆栈,开始运行pid为0的进程。第30行到第34行设置pid为0的进程PCB结构。
-
第31行将进程设置为可运行状态;
-
第32行设置下一条指令运行的地址指向函数my_process;
-
第33行设置进程堆栈地址空间,指向预先分配好的stack数组最后一个字节,然后设置后继PCB为其自身。此时PCB list已经是个循环链表,不过只有1个结点;
-
第36行到第44行依次设置剩下的MAX_TASK_NUM-1个进程的PCB结构,完成设置一个PCB后将该PCB插入PCB list中,增加1个结点,保持循环链表的结构不变。
-
第38行复制pid为0进程的PCB到当前进程PCB,效率并不高,因为每个进程PCB中只有task_entry和thread.ip是相同的,即下一条指令执行地址都函数从my_process开始,其他字段均需重新设置。
-
第46行到第56行开始设置系统堆栈空间。
先简要介绍C语言中的嵌入式汇编,详细说明请参考gcc手册。嵌入汇编的基本格式为:
-
asm(“汇编语句”
-
:输出寄存器
-
:输入寄存器
-
:会被修改的寄存器);
嵌入式汇编程序规定把输出和输入寄存器统一按顺序编号,分别记为%0、%1、… %9。输出和输入寄存器列表中多个寄存器用逗号分隔,每个寄存器项有引号和括号组成,引号内的字母表示所使用的寄存器代码,括号内表示C语言中的变量。
-
第55行表示输入寄存器,分别表示把task[0].thread.ip的值复制到寄存器ECX以及将task[0].thread.sp的值复制到寄存器EDX中,这样ECX中保存了pid为0的进程下一条指令执行的地址,EDX中保存了该进程堆栈空间的高地址。
-
第49行将寄存器EDX的值复制到堆栈指针寄存器ESP,此时系统堆栈指针指向了task[0].stack[KERNEL_STACK_SIZE-1],指向stack数组最后一个字节的原因是堆栈的增长方向是从高地址到低地址;
-
第50行将堆栈基址EBP的值压入堆栈,EBP压入堆栈前的值等于堆栈指针ESP的值;
-
第51行将下一条指令执行的地址压入堆栈; 第52行从堆栈中弹出下一条指令执行地址到指令指针寄存器EIP;
-
第52行执行后因为设置EIP的值为函数my_process的地址,因此开始调用函数my_process;
-
第53行指令不会被调用。
下图分别表示第50行到第52行执行后的堆栈内容:
接下来分析函数my_process的执行过程。函数my_process有个无限循环,每执行1千万次判断my_need_sched是否等于1,如果my_need_sched等于1说明当前运行进程已经耗尽分配的时间片,需要挂起当前进程,开始运行其后继进程。函数my_schedule负责上下文切换,定义在myinterrupt.c中。
执行进程切换
-
/*
-
* linux/mykernel/myinterrupt.c
-
*
-
* Kernel internal my_timer_handler
-
*
-
* Copyright (C) 2013 Mengning
-
*
-
*/
-
#include
-
#include
-
#include
-
#include
-
#include
-
-
#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
-
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(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 */
-
{
-
/* 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 */
-
“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)
-
);
-
my_current_task = next;
-
printk(KERN_NOTICE “>>>switch %d to %d<<<\n”,prev->pid,next->pid);
-
}
-
else
-
{
-
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 */
-
“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;
-
}
进程切换发生在函数my_schedule中。在初始化PCB链表的时,除了pid为0的其他进程PCB中state变量都设置为-1,表示未就绪状态,因此pid大于0的进程第一次被选中运行时,程序逻辑走的是else分支即第74行到第88行;第一次运行之后进程的PCB中state变量被设置为0,表示可运行状态,再次进入函数my_schedule中,程序逻辑走的是if分支即第56行到第70行。
初次切换
进程被第一次选中运行时,首先将PCB中state变量设置为0。
-
第79行将前驱进程prev的ebp压入其堆栈;
-
第80行将prev的esp保存到prev->thread.sp;
-
第81行将next->thread.sp的值复制到堆栈指针寄存器ESP;
-
第82行将next->thread.sp的值复制到基址EBP,此时建立了当前进程next的栈帧,EBP和ESP指向相同的地址即next->stack[KERNEL_STACK_SIZE-1];
-
第83行将标号“1:”的代码在内存中存储的地址复制到prev->thread.ip,当进程被再次选中运行时将该值加载到指令指针寄存器EIP,然后从标号为1的地方即第64行开始运行;
-
第84行将next->thread.ip的值即函数my_process的地址压入next的堆栈;
-
第85行设置指令指针寄存器EIP的值,因为用户无法直接设置EIP,先将需要执行的指令地址压入堆栈,然后使用ret指令从堆栈中弹出该地址并间接设置EIP,接着系统开始调用my_process;
-
第90行从函数my_schedule返回到函数my_process继续执行。
第74行到第88行执行以后prev进程和next进程的stack数组内容以及EIP寄存器内容如下图:
mykernel中pid为0的进程时间片用尽后切换到pid为1的进程的相关日志:
非初次切换
如果进程被已经运行过,再次被调度系统选中运行时PCB中state变量已经为0,因此调用函数my_schedule时走的是if分支即第57行到第70行。
-
第58行将前驱进程prev的ebp压入其堆栈;
-
第59行将prev的esp保存到prev->thread.sp;
-
第60行将next->thread.sp的值复制到堆栈指针寄存器ESP;
-
第61行将标号“1:”的代码在内存中存储的地址复制到prev->thread.ip,当进程被再次选中运行时将该值加载到指令指针寄存器EIP,然后从标号为1的地方即第64行开始运行;
-
第62行将原先保存在prev->thread.ip中的下一条指令地址复制到指令指针寄存器EIP,由初次切换中分析可知,prev->thread.ip中保存的是标号为“1:”的指令地址;
-
第63行使用ret指令从堆栈中恢复EIP的值,由于上次该进程挂起时保存的指令执行地址是标号为”1:”的语句,因此执行ret指令后看似要调转到别的地方,实际上执行的是第64行的指令;
-
第64行为标号无实际操作;
-
第65行从堆栈从弹出上次保存的堆栈基址到EBP中;
-
第69行将my_current_task设置为next,将next设置为当前运行进程;
-
第70行是个输出语句,供用户观察;
-
第90行从函数my_schedule返回到函数my_process继续执行。
第58行到第63行指令执行以后prev进程和next进程的stack数组内容以及EIP寄存器内容如下图:
时钟中断
函数my_timer_handler是发生时钟中断时的中断处理程序,每发生1000次时钟中断就会将my_need_sched的值设为1,表示当前运行的进程已经耗尽时间片将被系统挂起,然后开始运行在PCB循环链表中的后继进程。
时钟中断引起进程切换的相关日志:
总结
本文详细分析了精简内核mykernel的进程调度过程以及进程切换过程中上下文的保存和恢复,从而对linux系统的进程调度建立了比较直观的认识。mykernel的核心代码虽然只有200多行,但是清晰地示范了中断处理和上下文切换在现代多道程序设计计算机系统中的重要作用。
阅读(2765) | 评论(0) | 转发(1) |