原创作品转载请注明出处,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、准备实验环境
准备步骤基本按照Github上面mykernel项目主页里的教程。
1 2 3 4 5 6 7 8 9 10 11 |
sudo apt-get 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 wget https://raw.github.com/mengning/mykernel/master/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 |
图1.mykernel运行截图
二、运行过程简析
1、基本数据结构--头文件mypcb.h。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* file : mypcb.h*/ 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; |
最主要的就是上面的两个结构体,定义了线程以及进程控制块两个结构体。结构体里面定义了ip、sp、pid、state等进程必须的基本属性。
2、开始干活咯--内核初始化入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/* file: mymain.c */ 25 void __init my_start_kernel(void) 26 { 27 int pid = 0; 28 int i; 29 /* Initialize process 0*/ 30 task[pid].pid = pid; 31 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 32 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 33 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 34 task[pid].next = &task[pid]; 35 /*fork more process */ 36 for(i=1;i<MAX_TASK_NUM;i++) 37 { 38 memcpy(&task[i],&task[0],sizeof(tPCB)); 39 task[i].pid = i; 40 task[i].state = -1; 41 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 42 task[i].next = task[i-1].next; 43 task[i-1].next = &task[i]; 44 } /* * 第27~34行,这里对0号进程进行初始化,设置了一些基本属性。 * 第32~33行,先将该进程的ip设为了函数my_process()的地址,然后根据改进程的stack大小设置了栈顶地址sp。 * 第34行,下一个进程控制块的地址同样是0号进程,算是只有一个元素的循环链表吧,也就是说当前进程链表中只有一个进程。 * 第36~44行,这里复制了一堆tPCB。 * 第38行,将0号进程的内存数据复制为第i号进程的内存段,这样做和linux最初的设计相符,算是一种偷懒的手段吧。但是这样在实际的OS中是种浪费资源的行为。所以后来的linux有了COW(copy-on-write,写时复制)机制。 * 第42~43行,涉及进程链表的相关操作,保证循环链表不会中断 */ 45 /* start process 0 by task[0] */ 46 pid = 0; 47 my_current_task = &task[pid]; 48 asm volatile( 49 "movl %1,%%espnt" /* set task[pid].thread.sp to esp */ 50 "pushl %1nt" /* push ebp */ 51 "pushl %0nt" /* push task[pid].thread.ip */ 52 "retnt" /* pop task[pid].thread.ip to eip */ 53 "popl %%ebpnt" 54 : 55 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 56 ); 57 } /* * 第48~56行,这段内联汇编为0号进程的执行完成了关键寄存器的准备操作. * 第49行,将esp值设为了0号进程的栈顶地址; * 第50行,ebp入栈,也就是保存之前的ebp寄存器的值,保存现场; * 第51行,将0号进程的ip压入栈顶; * 第52行,利用ret指令,即pop eip,改写eip(因为不能直接操作eip) * 然后就进入了0号进程的执行上下文。 */ |
3、闹钟响了!定时器处理代码部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/*file: myinterrupt.c */ 27 void my_timer_handler(void) 28 { 29 #if 1 30 if(time_count%1000 == 0 && my_need_sched != 1) 31 { 32 printk(KERN_NOTICE ">>>my_timer_handler here<<<n"); 33 my_need_sched = 1; 34 } 35 time_count ++ ; 36 #endif 37 return; 38 } |
- 该函数被调用每1000次之后会进行一次是否需要进程调度的判断。如果需要,就将my_need_sched设为1,正在运行的进程my_process()会根据该值进行相关调度操作。
4、孪生的兄弟进程my_process()们都干了些什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* file: mymain.c */ 58 void my_process(void) 59 { 60 int i = 0; 61 while(1) 62 { 63 i++; 64 if(i%10000000 == 0) 65 { 66 printk(KERN_NOTICE "this is process %d -n",my_current_task->pid); 67 if(my_need_sched == 1) 68 { 69 my_need_sched = 0; 70 my_schedule(); 71 } 72 printk(KERN_NOTICE "this is process %d +n",my_current_task->pid); 73 } 74 } 75 } |
- 很明显,该函数很自恋啊,每运行10,000,000次就向屏幕上打印一行“我是进程 xxx”。然后会根据my_need_sched的值决定是否让出CPU给下一个小伙伴,一旦决定将资源让给下一个伙伴,他先将my_need_sched的值设为0,毕竟不能让下一个小伙伴刚开始执行就再一次地让位嘛。
5、换位那些事儿--进程上下文切换的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
/* file: myinterrupt.c */ 40 void my_schedule(void) 41 { 42 tPCB * next; 43 tPCB * prev; 44 45 if(my_current_task == NULL 46 || my_current_task->next == NULL) 47 { 48 return; 49 } 50 printk(KERN_NOTICE ">>>my_schedule<<<n"); 51 52 next = my_current_task->next; 53 prev = my_current_task; /* * 楼上发生了很多事,但是和楼下的比起来简直弱爆了有木有! */ 54 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 55 { 56 /* switch to next process */ 57 asm volatile( 58 "pushl %%ebpnt" /* save ebp */ 59 "movl %%esp,%0nt" /* save esp */ 60 "movl %2,%%espnt" /* restore esp */ 61 "movl $1f,%1nt" /* save eip */ 62 "pushl %3nt" 63 "retnt" /* restore eip */ 64 "1:t" /* next process start here */ 65 "popl %%ebpnt" 66 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 67 : "m" (next->thread.sp),"m" (next->thread.ip) 68 ); 69 my_current_task = next; 70 printk(KERN_NOTICE ">>>switch %d to %d<<<n",prev->pid,next->pid); 71 } /* * 第54行,判断下一个进程的是否已经执行过,结果为真就执行if分支 * 第57-68行,关键的内联汇编代码,实现了进程上下文的切换操作。 * 第58~59行,ebp入栈,保存现场。然后将esp保存至当前进程的tPCB中 * 第60-61行,将下一个进程的sp的值恢复至esp寄存器中。并保存当前eip。 * 第62-63行,先将新的ip压入栈,然后利用ret指令改写eip寄存器的值。 * 第64行,开始执行下一个进程 * 第65行,pop %ebp,将该进程上次让出CPU时压入栈的ebp弹出,恢复现场 */ 72 else 73 { 74 next->state = 0; 75 my_current_task = next; 76 printk(KERN_NOTICE ">>>switch %d to %d<<<n",prev->pid,next->pid); 77 /* switch to new process */ 78 asm volatile( 79 "pushl %%ebpnt" /* save ebp */ 80 "movl %%esp,%0nt" /* save esp */ 81 "movl %2,%%espnt" /* restore esp */ 82 "movl %2,%%ebpnt" /* restore ebp */ 83 "movl $1f,%1nt" /* save eip */ 84 "pushl %3nt" 85 "retnt" /* restore eip */ 86 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 87 : "m" (next->thread.sp),"m" (next->thread.ip) 88 ); 89 } 90 return; 91 } /* * 该分支处理的是下一个进程从未被执行过的情形。 * 第74行,首先将下一个进程的状态设为了0,runnable。 * 第78-88行,内联汇编代码,同样是关键的进程上下文切换。 * 第79-80行,ebp入栈,并将esp保存至原进程的tPCB中,保存了现场。 * 第81-82行,将该进程的esp、ebp存至相应寄存器中。 * 第83行,保存当前进程的eip。 * 第84-85行,新进程的ip入栈,然后利用ret指令改写eip寄存器的值,实现跳转。 */ |
三、作业小结
(对“操作系统是如何工作的”理解)
计算机硬件,比如CPU,提供了各种存储器、运算器以及一系列的控制器,组成了 "输入-存储-计算-输出"的基本结构。操作系统统筹了这些硬件的协同工作,提供了应用软件所必须的基本运行环境,其重要性不言而喻。
因为硬件平台的不同,所以操作系统是针对具体硬件平台的。操作系统根据具体硬件的功能特性搭建出一套统一的运行环境提供给上层应用运行。
除了提供基本的运行环境外,操作系统也负责硬件资源在不同应用程序之间的合理分配。操作系统根据一系列规则将有限的硬件资源分配给各个应用程序,这个也就是传说中的"进程调度程序"。这个特性使多任务操作系统的实现成为了可能,多道程序同时运行并合理切换成为一个成熟操作系统的必备特性。