从一个精简Linux内核分析操作系统的基本运行过程
郑德伦 原创作品转载请注明出处《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000STEP1:搭建实验环境首先在自己的Linux系统中配置好实验的环境,依次输入以下的命令:•sudo apt-get install qemu # install QEMU•sudo ln -s /usr/b
《Linux内核分析》MOOC课程
http://mooc.study.163.com/course/USTC-1000029000
STEP1:搭建实验环境
首先在自己的Linux系统中配置好实验的环境,依次输入以下的命令:
• sudo apt-get install qemu # install QEMU
• sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu
• wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz # download Linux Kernel 3.9.4 source code
• wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch # download mykernel_for_linux3.9.4sc.patch
• xz -d linux-3.9.4.tar.xz
• tar -xvf linux-3.9.4.tar
• cd linux-3.9.4
• patch -p1 < ../mykernel_for_linux3.9.4sc.patch
• make allnoconfig
• make
最后执行qemu -kernel arch/x86/boot/bzImage
STEP2:完成一个进程切换的内核
经过环境的配置,Linux内核经过我们修改,在qemu窗口中我们会看到,一个只有时钟中断的系统。如图所示
因为在mymian.c里面,只有一个函数在执行,my_start_kernel(void),循环输出一句话。
void __init my_start_kernel(void)
{
int i = 0;
while(1)
{
i++;
if(i%100000 == 0)
printk(KERN_NOTICE "my_start_kernel here %d \n",i);
}
}
在myinterrupt.c文件里面只有一个处理时钟中断的函数
void my_timer_handler(void)
{
printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n");
}
如果我们要完成可以进行进程调度的操作系统内核,需要将github中mykernel的myinterrupt.c mymain.c mypcb.h三个文件复制到linux内核文件夹下面,然后再进行make。
首先在github上面下载3个文件,在终端中输入如下命令:
wget https://raw.github.com/mengning/mykernel/master/mypcb.h
wget https://raw.github.com/mengning/mykernel/master/myinterrupt.c
wget https://raw.github.com/mengning/mykernel/master/mymain.c
然后将三个文件拷贝到linux内核文件夹下面的mykernel文件夹下面,在终端中输入命令
cp myinterrupt.c linux-3.9.4/mykernel/
cp mymain.c linux-3.9.4/mykernel/
cp mypch.h linux-3.9.4/mykernel/
最后执行make重新编译内核
然后终端中输入qemu -kernel arch/x86/boot/bzImage就可以看到一个进程切换的内核运行了。
STEP3:分析代码,理解执行过程
首先打开mypcb.h文件,有两个结构体。
第一Thread结构体,保存一个任务的eip还有esp的信息。
struct Thread {
unsigned long ip;
unsigned long sp;
};
第二个结构体PCB,保存进程的一些信息。
pid保存一个进程唯一的一个ID号
state表示一个进程的运行状态
stack数组表示一个进程的堆栈空间
Thread结构体记录eip和esp堆栈栈顶的信息
task_entry表示进程的执行入口地址
struct PCB* next则指向下一个进程,用链式储存结构将进程连在一起
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;
然后打开mymain.c文件
volatile int my_need_sched = 0;
my_need_sched变量表示一个任务,是否可以进行调度。
然后分析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 */
task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
/*把进程的入口和eip都初始化为my_process函数的入口地址*/
task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
/*将堆栈的栈顶地址保存为栈的最后一个元素,即最高的地址*/
task[pid].next = &task[pid];
/*next指针指向自己*/
/*fork more process */
for(i=1;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[0],sizeof(tPCB));
/*将task[0]的信息全部复制到各个新进程中*/
task[i].pid = i;/*将新进程的pid设置为变量i*/
task[i].state = -1;/*新进程的状态为unrunnable*/
task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1];/*将每个新进程的栈顶指针都指向堆栈数组的最后一个元素*/
/*使用尾插法创建链表形成一个任务链0->1->2->3->0这样的循环*/
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(
/*把0号进程的esp放入esp寄存器*/
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
/*push0号进程的esp,因为此时栈为空,esp==ebp,所以此处等同于push ebp*/
"pushl %1\n\t" /* push ebp */
/*将0号进程的eip,即process函数的入口地址push到当前堆栈*/
"pushl %0\n\t" /* push task[pid].thread.ip */
/*将栈顶元素pop到eip然后跳转*/
"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*/
);
}
my_start_kernel完成的任务主要是初始化进程链表,然后开始执行第一个进程。
执行完ret操作之后将会跳转到process函数中执行。
然后我们来分析process程序,process程序是一个无限循环,每循环100000000执行判断生效一次,输出KERN_NOTICE "this is process pid-,并且判断my_need_sched是否等于1,如果等于1的话,就把my_need_sched置0并且执行my_schedule()进行调度,最后执行KERN_NOTICE "this is process pid+
void my_process(void)
{
int i = 0;
while(1)
{
i++;
if(i%100000000 == 0)// 没执行100000000次循环进入一次
{
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文件
void my_timer_handler(void)
{
#if 1
/*函数每执行1000次,并且my_need_sched不等于1的时候执行if判断,然后把my_need_sched置为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_timer_handler(void),#if 1 到#endif表示一个条件预编译指令,表示永远编译,一般用于测试代码中,把1改为0相当于注释掉代码段,方便调试。
my_timer_handler函数每执行1000次并且my_need_sched不为1的时候输出一段话,并且把my_timer_handler置为1.
到此我们就清楚了my_need_sched这个变量的变化过程(由process置0,由my_timer_handler置1,交替执行),和mymain.c里面的process函数结合就可以分析出进程调度的详细过程。
注:因为时间中断处理每隔一段时间就会进行一次,可能在0号进程执行过程中进行了多次时间中断处理,我们暂时把没有发生my_need_sched变量改变的时间中断处理时间忽略掉。
进程的调度过程如下:
0号进程启动,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行1号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行2号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行3号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
->执行0号进程,my_need_sched=0,当my_timer_handler将my_need_sched置1时,process执行调度函数my_schedule(),并将my_need_sched置0
整个调度过程就如上面所示,process循环执行,等待时钟中断来将允许调度的变量置1,然后完成调度。
具体的调度过程我们要分析my_schedule函数:
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;
/*next进程两种情况,运行和非运行要分情况处理*/
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 */
/*将下面标号1:之后的语句popl ebp的地址放入prev的ip中保存*/
"movl $1f,%1\n\t" /* save eip */
/*将next的eip push到栈上,然后执行ret的话,就会从next的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的状态置为runnable*/
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;
}
如果next进程为非runnable状态的时候执行else,runnable的状态执行if,整个过程就是先保存当前进程的ebp和esp,然后将next进程的esp和ebp复原,并且将prev进程的eip储存为标号1的位置,最后将next进程的eip push到栈上,再执行ret指令,返回到next的eip所指向的地方执行。
因为进程是用一个循环链表连接起来的,一直从prev向next切换,总会有一次再次切换到自己,也就是prev进程,变成next进程。如果下一次进入的时候会标号1:开始执行完成ebp的复原操作。
注:为了便于理解我们用0号进程和1号进程表示。
这个schedule函数分两次执行,第一次是从0号进程跳转到1号进程,而第二次进入则是执行popl ebp恢复现场,然后return函数,返回到原调用者也就是0号进程的process里面。
总结:
通过以上代码的分析,我们可以初步了解到了,linux内核是如何完成进程的切换的。我们可以认为一个进程相当于一个堆栈,每个进程有自己的堆栈空间。如果将ebp和esp修改为另一个进程的ebp和esp,并且完成一些寄存器的保存,就相当于完成的进程的切换。
更多推荐
所有评论(0)