20135327郭皓——Linux内核分析第二周 操作系统是如何工作的
操作系统是如何工作的
上章重点回顾:
计算机是如何工作的?(总结)——三个法宝
-
存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
-
函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能;
-
enter
-
pushl %ebp
-
movl %esp,%ebp
-
leave
-
movl %ebp,%esp
-
popl %ebp
-
函数参数传递机制和局部变量存储
-
中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
本章操作系统是如何工作的主要针对的是函数调用堆栈。
一、函数调用堆栈
1.堆栈
2.堆栈寄存器和堆栈操作
3.函数的堆栈框架
4.函数堆栈框架的形成
5.关于一个小例子
p2函数的调用详解:
main中的局部变量:
6.C代码中嵌入汇编代码 内嵌式语法
二、一个简单的时间轮转多道程序实验
1.进入实验楼,进入指定文件夹,运行程序
2.打开mymain.c myinterrupt.c
3.对核心代码分析
1 mymain.c 2 3 void __init my_start_kernel(void) //操作系统的入口 启动操作系统 4 { 5 int i = 0; 6 while(1) 7 { 8 i++; 9 if(i%100000 == 0) //每循环10万次打印一次 10 printk(KERN_NOTICE "my_start_kernel here %d \n",i); 11 12 } 13 }
1 myinterrupt.c 2 3 4 5 #include <trace/events/timer.h> 6 7 /* 8 * Called by timer interrupt. 9 */ 10 void my_timer_handler(void)//每一次中断都执行一次 11 { 12 printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); 13 }
时间轮转多道程序就是在这个函数的基础上,加入自己的要求,下面是多进程时间片轮转代码
mymain.c: 内核初始化和进程的启动
#include <linux/types.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/tty.h> #include <linux/vmalloc.h> #include "mypcb.h" tPCB task[MAX_TASK_NUM]; //声明一个task数组,tPCB结构体类型在mypcb.h中有定义 tPCB * my_current_task = NULL; //表示当前task指针 volatile int my_need_sched = 0; //表示是否需要调度 void my_process(void); void __init my_start_kernel(void) { int pid = 0; //初始进程号,修改方便 int i; /* Initialize process 0*/ //0号进程的初始化过程 task[pid].pid = pid; task[pid].state = 0; //0表示可运行 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; //入口是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)); //复制0号进程的状态 task[i].pid = i; //创建的新进程号 task[i].state = -1; //-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] */ //执行0号进程 pid = 0; //初始进程号,修改方便 my_current_task = &task[pid]; asm volatile( //嵌入式汇编代码 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ //将进程的sp赋给esp寄存器 "pushl %1\n\t" /* push ebp */ //当前的栈是空栈,ebp等于esp,所以push的%1是esp也就是ebp "pushl %0\n\t" /* push task[pid].thread.ip */ //ip入栈 "ret\n\t" /* pop task[pid].thread.ip to eip */ //ip弹栈赋给eip,0号进程正式启动 "popl %%ebp\n\t" : : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ ); } //内核初始化完成,0号进程开始执行 void my_process(void) //进程的起点 { int i = 0; while(1) { i++; if(i%10000000 == 0) //执行10 000 000次才判断一次是否需要调度 { printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); if(my_need_sched == 1) //1为需要调度,这是一个主动调度机制 { my_need_sched = 0; my_schedule(); } printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); } } }
- 首先是先创建第一个进程,定义其进程编号为0,状态为可执行,设置进程入口的地址为该进程的eip,设置进程的堆栈为该进程的堆栈,task[0].next=&task[0];因为此时系统之中仅仅只要一个进程,该进程的下一个进程就只能暂且指向自己了。
- 接着是创建更多的进程,步骤是通过一个for循环进程依次创建进程的编号、状态和堆栈。将新创建的进程加入上一个进程的尾部。
- 接下来就是要启动0号进程也就是第一个进程,将该进程的堆栈标记so存入寄存器esp中,紧接着将ebp压入栈中,再将该进程的eip压入栈中,接着将myprocess的eip弹出给eip,也就是启动0号进程。
- 当运行完后则需要运行popl %ebp退回上一状态。
*1f指的是标号1所在的位置。
myinterrupt.c:进程的切换
1 #include <linux/types.h> 2 #include <linux/string.h> 3 #include <linux/ctype.h> 4 #include <linux/tty.h> 5 #include <linux/vmalloc.h> 6 7 #include "mypcb.h" 8 9 tPCB task[MAX_TASK_NUM]; //声明一个task数组 10 tPCB * my_current_task = NULL; //表示当前task指针 11 volatile int my_need_sched = 0; //表示是否需要调度 12 13 void my_process(void); 14 15 void __init my_start_kernel(void) 16 { 17 int pid = 0; //初始进程号,修改方便 18 int i; 19 /* Initialize process 0*/ //0号进程的初始化过程 20 task[pid].pid = pid; 21 task[pid].state = 0; //0表示可运行 22 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; //入口是my_process 23 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; //堆栈的栈顶位置 24 task[pid].next = &task[pid]; //下一个进程next还是指向自己(刚启动时没有其他进程) 25 26 /*fork more process */ //创建更多进程 27 for(i=1;i<MAX_TASK_NUM;i++) 28 { 29 memcpy(&task[i],&task[0],sizeof(tPCB)); //复制0号进程的状态 30 task[i].pid = i; //创建的新进程号 31 task[i].state = -1; //-1等待 32 task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-1]; //新进程的堆栈 33 task[i].next = task[i-1].next; //指向下一个进程 34 task[i-1].next = &task[i]; //新进程放在链表的尾部 35 } 36 37 //进程的启动 38 /* start process 0 by task[0] */ //执行0号进程 39 pid = 0; //初始进程号,修改方便 40 my_current_task = &task[pid]; 41 asm volatile( //嵌入式汇编代码 42 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */ //将进程的sp赋给esp寄存器 43 "pushl %1\n\t" /* push ebp */ //当前的栈是空栈,ebp等于esp,所以push的%1是esp也就是ebp 44 "pushl %0\n\t" /* push task[pid].thread.ip */ //ip入栈 45 "ret\n\t" /* pop task[pid].thread.ip to eip */ //ip弹栈赋给eip,0号进程正式启动 46 "popl %%ebp\n\t" 47 : 48 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/ 49 ); 50 } //内核初始化完成,0号进程开始执行 51 52 void my_process(void) //进程的起点 53 { 54 int i = 0; 55 while(1) 56 { 57 i++; 58 if(i%10000000 == 0) //执行10 000 000次才判断一次是否需要调度 59 { 60 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); 61 if(my_need_sched == 1) //1为需要调度,这是一个主动调度机制 62 { 63 my_need_sched = 0; 64 my_schedule(); 65 } 66 printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid); 67 } 68 } 69 }
- 当前进程的下一个进程赋给next,当前进程赋给prev。如果下一个进程状态为0的话,即正在执行,则需要在两个正在运行的进程之间做进程上下文切换:
- 保存当前进程的ebp
- 将当前进程的esp赋给prev->thread.sp,从而将其保存起来
- 将下一个进程的sp放置esp中
- 保存当前进程的eip至当前进程所属的pcb中
- 将下一个进程的eip保存至当前栈中
- 通过ret,即可调用下一个进程
- 若下一个进程的状态还未执行过,则进行以下步骤:
- 将下一个进程设置为运行时状态,并将其设置为当前进程
- 保存当前进程的ebp,将其压入栈中
- 保存当前进程的esp至当前进程的pcb中
- 由于该程序并未执行过,所以它的堆栈为空栈,空栈的创建则是通过将ebp和esp都赋予同一个值。
- 保存当前进程的eip
- 将当前进程的eip压入栈中
- 通过ret,即可调用下一个进程
总结:
本节讲的是操作系统是如何工作的,首先总结了一下计算机是如何工作的,计算机有三大法宝:存储程序计算机工作模型、函数调用堆栈和中断机制,而操作系统也是有两把利剑:中断上下文和进程上下文的切换。
郭皓原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000