在本周的课程中,孟老师主要讲解了操作系统是如何工作的,我根据自己的理解写了这篇博客,请各位小伙伴多多指正。
一、知识点总结
1. 三个法宝
存储程序计算机:所有计算机基础性的逻辑框架。
堆栈:高级语言的起点,函数调用需要堆栈机制。
中断机制:多道系统的基础,是计算机效率提升的关键。
2. 函数调用堆栈
堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间,即CPU内已经集成好了很多功能。
堆栈含以下元素:
函数调用框架
传递参数
保存返回地址(%eax)
提供局部变量空间 等等
C语言编译器对堆栈的使用有一套的规则。
堆栈相关的寄存器:
esp,堆栈指针
ebp,基址指针
堆栈操作:
push,栈顶地址减少4个字节(32位)
pop,栈顶地址增加4个字节 注:ebp在C语言中用做记录当前函数调用基址。
其他关键寄存器:
cs:eip:指向地址连续的下一条指令
顺序执行:总是指向地址连续的下一条指令
跳转/分支:执行这样的指令的时候,cs:eip的值会根据程序需要被修改
call:将当前cs:eip的值压入栈顶,cs:eip指向被调用函数的入地址
ret:从栈顶弹出原来保存在这里的cs:eip的值,放入cs:eip中 发生中断时
二、Mykernel精简内核程序实验
1.实验过程
首先用下面两条命令
1 cd LinuxKernel/linux-3.9.4 2 qemu -kernel arch/x86/boot/bzImage
由图可知:每执行my_ start_ kernel函数一次或两次,my_ time_ hander函数就执行一次。
mymain.c ——系统中唯一的进程。mystartkernel之前的都是硬件初始化的工作,之后是整个操作系统的入口,开始执行操作系统。其中代码完成的工作是每循环10000次,打印一句话。
myinterrupt.c ——时间中断处理程序
每执行一次,都会执行一次时钟中断,每次时钟中断都调用printk并输出。
以上的实验环境仅仅是模拟了时钟中断,一直都是一个进程的run。需要加入下面的代码,才能开始进程切换调度。
需要在mykernel下重新更新文件:myinterrupt.c,主要负责中断以及进程切换;mymain.c,初始化系统环境,mypcb.h描述了进程控制块的定义。
再次到Linux3.9.4目录下重新编译,用下面命令:
1 make allnoconfig 2 make 3 qemu -kernel arch/x86/boot/bzImage
2.进程的启动和进程的切换机制
进程的启动:从void__init my_start_kernel(void)开始启动,函数前面是一些初始化工作,包括对于进程的fork。每个进程初始时,除了pid之外,对于所定义的pcb来说,其他都是相同的,以一个链表来组织各进程。嵌入式汇编代码的主要作用就是初始化第0号进程,将0号进程的esp写入esp寄存器,由于进程还未开始执行,因此进程的esp以及ebp都应该是指向栈底的(空栈),另外,0号进程的eip在这里已经指向了my_process代码段地址,ret之后,开始执行0号进程。
1 void __init my_start_kernel(void) 2 { 3 int pid = 0; 4 int i; 5 /* Initialize process 0*/ 6 task[pid].pid = pid; 7 task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */ 8 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; 9 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; 10 task[pid].next = &task[pid]; 11 /*fork more process */ 12 for(i=1;i<MAX_TASK_NUM;i++) 13 { 14 memcpy(&task[i],&task[0],sizeof(tPCB)); 15 task[i].pid = i; 16 task[i].state = -1; 17 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; 18 task[i].next = task[i-1].next; 19 task[i-1].next = &task[i]; 20 } 21 /* start process 0 by task[0] */ 22 pid = 0; 23 my_current_task = &task[pid]; 24 asm volatile( 25 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ 26 "pushl %1\n\t" /* push ebp */ 27 "pushl %0\n\t" /* push task[pid].thread.ip */ 28 "ret\n\t" /* pop task[pid].thread.ip to eip */ 29 "popl %%ebp\n\t" 30 : 31 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 32 ); 33 }
进程的切换:通过schedule()函数进行进程切换,挡发生时钟中断的时候,中断处理程序会将需要调度的flag置为1,而与此同时,每一个当前执行的进程都在轮询,如果一旦flag被置为了1,则开始进行进程切换。case1: 进程开始切换的时候,是切换到一个进程状态为已经在运行的进程。在切换进程之前,将1f存到prev的eip中,1f指的是被标记为1的地址,ret之后,弹栈next的eip,开始执行next进程。
1 if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */ 2 { 3 /* switch to next process */ 4 asm volatile( 5 "pushl %%ebp\n\t" /* save ebp */ 6 "movl %%esp,%0\n\t" /* save esp */ 7 "movl %2,%%esp\n\t" /* restore esp */ 8 "movl $1f,%1\n\t" /* save eip */ 9 "pushl %3\n\t" 10 "ret\n\t" /* restore eip */ 11 "1:\t" /* next process start here */ 12 "popl %%ebp\n\t" 13 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 14 : "m" (next->thread.sp),"m" (next->thread.ip) 15 ); 16 my_current_task = next; 17 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 18 }
case2:进程切换到一个新进程。由于这里所切换的进程是一个新进程,所以这里的state置为0(running状态),这里的movl $1f,%1\n\t中的1f在进程再次调度回来的时候,走的是case1的分支代码块,所以case2中的 中没有标记为1的代码块,也没有pop ebp的操作。
1 else 2 { 3 next->state = 0; 4 my_current_task = next; 5 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid); 6 /* switch to new process */ 7 asm volatile( 8 "pushl %%ebp\n\t" /* save ebp */ 9 "movl %%esp,%0\n\t" /* save esp */ 10 "movl %2,%%esp\n\t" /* restore esp */ 11 "movl %2,%%ebp\n\t" /* restore ebp */ 12 "movl $1f,%1\n\t" /* save eip */ 13 "pushl %3\n\t" 14 "ret\n\t" /* restore eip */ 15 : "=m" (prev->thread.sp),"=m" (prev->thread.ip) 16 : "m" (next->thread.sp),"m" (next->thread.ip) 17 ); 18 }
三、对操作系统如何工作的理解
操作系统是管理计算机系统的全部硬件资源包括软件资源及数据资源;控制程序运行;改善人机界面;为其它应用软件提供支持等,使计算机系统所有资源最大限度地发挥作用,为用户提供方便有效的服务界面。linux内核从一个初始化上下文环境的函数开始执行,即start_kernel函数,创建很多进程或者fork若干进程,当中断发生的时候,如mykernel中就是时钟中断发生之后,接下来OS就会为各进程进行调度,在调度队列中选取出一个适合的进程,由CPU和内核堆栈保存前一个进程的各寄存器信息(进程描述块中的thread结构体保存大部分CPU寄存器,但是诸如eax、ebx等等这些通用寄存器是保存在内和堆栈中的),将eip指向要调度的进程执行的地址,开始执行。在本次课程中,我对于嵌入汇编的内容仍不熟练,所以为了更深入理解Linux需加强对汇编和操作系统的理解。
刘帅
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000