林光伟
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、准备工作
1、安装qemu,
我有一份qeum-2.0.0.rc0的源码,以来是用来调试arm的,现在将i386平台的也编译下
./configure --target-list='arm-softmmu arm-linux-user armeb-linux-user i386-softmmu'
make -j4
sudo make install
2、下载kernel
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.9.4.tar.xz
3、下载补丁
wget https://raw.github.com/mengning/mykernel/master/mykernel_for_linux3.9.4sc.patch
4、解压linux内核源码
tar -xvf linux-3.9.4.tar.xz
5、给linux打上本实验所修改的补丁
cd linux-3.9.4 patch -p1 < ../mykernel_for_linux3.9.4sc.patch
6、编译内核
make allnoconfig
make -j4
二、单任务内核测试
1、采用qeum模拟器运行内核
qemu-system-i386 -kernel arch/x86/boot/bzImage
运行结果

https://github.com/mengning/mykernel主页上的说明只是给出了该实验的基本结构,单任务外加一个定时中断外理,涉及到的两个主要文件就是myinterrupt.c与mymain.c,
观查mykernel_for_linux3.9.4sc.patch这个补丁文件,可以看修改了下面这几个文件(当然还有Makefile,这里不去观注)
arch/x86/kernel/time.c linux-3.9.4.new/include/linux/start_kernel.h include/linux/timer.h init/main.c
将arch/x86/kernel/time.c的定时中断入口处指引到myinterrupt.c的void my_timer_handler(void),将init/main.c的start_kernel函数指引到mymain.c,这样就可以将原来复杂的linux kernel代码隐藏,后面我们只需要关注修改myinterrupt.c与mymain.c就行了。
关键代码 (arch/x86/kernel/time.c)
static irqreturn_t timer_interrupt(int irq, void *dev_id) { global_clock_event->event_handler(global_clock_event); my_timer_handler();//从这里调用myinterrupt.c return IRQ_HANDLED; }
关键代码 (init/main.c)
asmlinkage void __init start_kernel(void) { ///////////////这里省略几十行///////////////////////
ftrace_init();
my_start_kernel();//调用mymain.c /* Do the rest non-__init'ed, we're now alive */ rest_init(); }
三、改成时间片轮转多任务内核
1、进入到mykernel目录,更新程序
cd mykernel
rm *.h *.c
wget https://raw.githubusercontent.com/mengning/mykernel/master/mymain.c wget https://raw.githubusercontent.com/mengning/mykernel/master/myinterrupt.c wget https://raw.githubusercontent.com/mengning/mykernel/master/mypcb.h
2、为了便于观察,需要减少任务轮转的时间粒度
将myinterrupt.c的“if(time_count%1000 == 0 && my_need_sched != 1)”改成 if(time_count%50 == 0 && my_need_sched != 1)
3、再次编译内核
cd ..
make -j4
4、再次通过qeum进行模拟运行
qemu-system-i386 -kernel arch/x86/boot/bzImage
这里就可以看到多任务的运行结果了

四、代码分析
这个简内的时间片多任务调度内核的核心是mpcb.h的PCB结构体的定义,该结构体决定了运行算法。
typedef struct PCB{ int pid; //任务进程id号 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ //任务进程的状态,我个人更喜欢用enum{PCP_UNRUNNABLE = -1, PCB_RUNNABLE, PCB_STOPPED}state;这种方式表示 char stack[KERNEL_STACK_SIZE]; //预分配堆栈内存,大小为KERNEL_STACK_SIZE /* CPU-specific state of this task */ struct Thread thread; //任务进程的入口函数与堆栈指针(见struct Thread的定义) unsigned long task_entry; //任务进程的入口函数 struct PCB *next; //指向下一个入PCB结构体 }tPCB;
在内核启动后会进入 void __init my_start_kernel(void)这个函数,运行过程是

这个函数很好理解,最后PCB链表会形成一个环型结构,在调度0号进程时,用到了汇编
asm volatile( "movl %1,%%esp\n\t" //令esp指向指定位置task[pid].thread.sp 作为堆栈初始地址 "pushl %1\n\t" //令堆栈地址本身入栈 "pushl %0\n\t" //task[pid].thread.ip入栈,即进程函数的地址入栈 "ret\n\t" //ret 相当于popl eip,eip的值变为进程函数的地址,cpu就跳到该地址去执行 "popl %%ebp\n\t" //还原ebp,实际上不会被执行到。 : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ );
上述汇编运行完后,cpu就跳到 void my_process(void)上去了,而且esp的值为 task[pid].thread.sp - 4,也就是(unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1-4];
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_need_sched ,当定时器中断时,my_need_sched会置1时,表示分配给该进程的时间结束了,需要调度下一个进程,my_need_sched ,于是就进入了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; 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" //指定跳转的位置为next->thread.ip "ret\n\t" /* restore eip */ //跳到 next->thread.ip "1:\t" /* next process start here *///(位置1) "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 */ // (位置2)将prev->thread.ip指向上一个汇编代码的1:\t的那个位置 "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()用到的两段汇编代码有所区别,最下面那一段(就是被 else包含的那段)汇编是处理从来都没有运行过的进程,(位置2)处的代码将prev->thread.ip指向位(置1)处,这样再次调用prev进程时,将从(位置1)处开始。
五、总结:多任务进程之间的切换实际上就是对eip、esp 、ebp之间的操作与保存,每道进程都有自已独立的stack,相对于C语言,有像是在玩杂技表演。
浙公网安备 33010602011771号