《Linux内核分析》第二周笔记 操作系统是如何工作的
操作系统是如何工作的
一、函数调用堆栈
1、三个法宝 计算机是如何工作的?(总结)——三个法宝(存储程序计算机、函数调用堆栈、中断机制)
1)存储程序计算机工作模型,计算机系统最最基础性的逻辑结构;
2)函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算机来说并不那么重要,但有了高级语言及函数,堆栈成为了计算机的基础功能,是高级语言的起点;
•enter
•pushl %ebp
• movl %esp,%ebp
•leave
•movl %ebp,%esp
•popl %ebp
函数参数传递机制和局部变量存储共同
3)中断,多道程序操作系统的基点,没有中断机制程序只能从头一直运行结束才有可能开始运行其他程序。
早期的计算机没有中断只能在执行完一个程序后执行另一个程序,有了中断有了多道程序设计(在系统中同时执行多个程序),一个程序有自己的执行流,在执行流的过程中,如何切换到另一个程序?CPU可以在当一个中断信号发生的时候,cpu把当前的eip,esb,ebp都压到一个堆栈里面,eip指向中断处理程序的入口,执行中断处理程序(由cpu和内核代码共同实现了保存现场和恢复现场)。
1.1什么是堆栈
1)是C语言程序运行时必须的一个记录调用路径和参数的空间,实际上在CPU内部集成好了功能。
- 函数调用框架
- 传递参数(64位略有不同)
- 保存返回地址(利用eax)
- 提供局部变量空间
2)C语言编译器对堆栈的使用有一套的规则(不同的指令序列可以实现完全相同的功能)
3)了解堆栈存在的目的和编译器对堆栈使用的规则是理解操作系统一些关键性代码的基础
2、深入理解函数调用堆栈
2.1堆栈寄存器和堆栈操作
1)堆栈相关的寄存器
– esp,堆栈指针(stack pointer)
– ebp,基址指针(base pointer)
2)堆栈操作
– push 栈顶地址减少4个字节,并将push的内容放进去(32位)
– pop 高地址栈顶地址增加4个字节 (从高地址向低地址增加)
3)ebp在C语言中用作记录当前函数调用基址
当函数嵌套比较深的话,每一个函数的ebp是不一样的
4)其他关键寄存器
– cs(代码段寄存器) : eip:总是指向下一条的指令地址
• 顺序执行:总是指向地址连续的下一条指令
• 跳转/分支:执行这样的指令的时候,cs : eip的值会根据程序需要被修改
• call:将当前cs : eip的值压入栈顶,cs : eip指向被调用函数的入口地址
• ret:从栈顶弹出原来保存在这里的cs : eip的值,放入cs : eip中
2.2深入理解函数调用堆栈的工作机制
call指令:
1)将eip中下一条指令的地址A保存在栈顶
2)设置eip指向被调用程序代码开始处
2.3函数调用时堆栈的变化
对应cs:eip被压栈
——第二条指令:movl %esp,%ebp
在原来堆栈的存储空间里,创建了一个新的函数调用堆栈,一个新的空的堆栈,可继续向低地址增长
——函数体中的常规操作可能会出现压栈、出栈
退出XXX
——movl%ebp,%esp(清空堆栈)
——popl%ebp(ebp指向原来的基地址)
——ret(cs:eip出栈)
3.参数传递与局部变量
3.1举例分析函数调用堆栈(二级调用)
源文件:test.c
main函数中调用了函数p1和p2
首先使用gcc —g生成test.c的可执行文件test
然后使用objdump —s获得test的反汇编文件
3.1.1观察p2的堆栈框架
3.1.2如何传递参数给p2
1)先将y的值push进来,不是push y而是y的变址寻址(局部变量),是因为在建立当前的框架的时候,把局部变量都保存在堆栈里面,就可以用变址寻址找到y的值。
下图就是将两个局部变量压栈的图示:
2)call:
3)函数调用结束后返回:
4)add esp+8:
5)mov保存返回值。函数的返回值通过eax寄存器传递。
p2的返回值是如何返回给z的?
eax放到当前函数调用堆栈Z的位置。eax赋值给z
6)先push y x z再push字符串
3.1.3观察局部变量的存储机制
2)sub $0x18,%esp(预留出0x18个字节,此空间用来存储局部变量)
以前:写c代码的时候必须把变量的声明放在代码的头部,是因为好把声明的变量在堆栈中预留空间。
现在:编译器智能可以先扫描整个函数,里面一共声明多少变量,可以提前准备好,所以编译器不强制要求局部变量在函数头部。
char c='a'(声明时把字符a的ascll码放在变址寻址中)
3.1.4整个程序运行时堆栈的变化
1)从main函数开始执行,由系统建立堆栈。
2)从main开始向下执行,调用到p1的时候,将c,eip,压栈,p1堆栈
3)将p1 return,回到原来的状态
4)执行p2的时候,将x,y压栈,eip堆栈
5)回到main的堆栈
3.2三级函数调用程序
在这个程序中main函数中调用了函数p2,而在p2的执行过程中,又调用了函数p1
具体过程如下:
二、借助linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
1、利用mykernel实验模拟计算机硬件平台
周期性的每过一个时间发生一次时钟中断,时钟中断会调用一个程序。
在实验楼环境下运行mykernel
mykernel相关的源代码:
操作
系统的入口(开始启动操作系统):
查看myinterrupt.c的代码:
每调用一次时钟中断都打印一个:
三、在mykernel基础上构造一个简单的操作系统内核
1.c代码中嵌入汇编代码的写法
1)内嵌汇编语法
_asm_(
汇编语句模板:
输出部分: (函数调用的参数)
输入部分:
破坏描述部分);
举例:使用嵌入式汇编代码使val1+val2=val3
asm volatile(
"movl $0,%%eax\n\t" /* clear %eax to 0*/ 将eax为0 %%:转义字符
"addl %1,%%eax\n\t" /* %eax += val1 */将val1+0放到eax里 %1指的是下面第五行的val1
"addl %2,%%eax\n\t" /* %eax += val2 */
"movl %%eax,%0\n\t" /* val2 = %eax*/将val1+val2的值放到%0中
: "=m" (val3) /* output =m mean only write output memory variable*/写到内存变量里面去,不使用寄存器
: "c" (val1),"d" (val2) /* input c or d mean %ecx/%edx*/ 用ecx寄存器存储val1的值,'c'指的是ecx
);
printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
嵌入式汇编的时候,每一个输出和输入的部分前面都可以加一个限定符。
=m:只写
r:将输入变量放入通用寄存器
eax:破坏描述部分
把0赋给temp(%1)
%2是input,input是1,把1赋给eax
输出remp=0,output=1
2、一个简单的操作系统源代码
mypcb.h
2 * linux/mykernel/mypcb.h
3 *
4 * Kernel internal PCB types
5 *
6 * Copyright (C) 2013 Mengning
7 *
8 */
9
10 #define MAX_TASK_NUM 4
11 #define KERNEL_STACK_SIZE 1024*8
12
13 /* CPU-specific state of this task */
14 struct Thread {
15 unsigned long ip;
16 unsigned long sp;
17 };
18
19 typedef struct PCB{ /*定义进程管理相关的数据结构*/
20 int pid;
21 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
22 char stack[KERNEL_STACK_SIZE]; /*内核堆栈*/
23 /* CPU-specific state of this task */
24 struct Thread thread;
25 unsigned long task_entry; /*入口一般是main函数*/
26 struct PCB *next; /*把进程用链表练起来*/
27 }tPCB;
28
29 void my_schedule(void);
mymain.c
/*
2 * linux/mykernel/mymain.c /*在mykernnel基础上构造一个简单的操作系统内核*/
3 *
4 * Kernel internal my_start_kernel
5 *
6 * Copyright (C) 2013 Mengning
7 *
8 */
9 #include <linux/types.h>
10 #include <linux/string.h>
11 #include <linux/ctype.h>
12 #include <linux/tty.h>
13 #include <linux/vmalloc.h>
14
15
16 #include "mypcb.h"
17
18 tPCB task[MAX_TASK_NUM];
19 tPCB * my_current_task = NULL;
20 volatile int my_need_sched = 0; /*内核初始化和0号进程启动*/
21
22 void my_process(void);
23
24
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 }
45 /* start process 0 by task[0] */ /*启动0号进程*/
46 pid = 0;
47 my_current_task = &task[pid];
48 asm volatile(
49 "movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
50 "pushl %1\n\t" /* push ebp */ /*将ebp压栈*/
51 "pushl %0\n\t" /* push task[pid].thread.ip */
52 "ret\n\t" /* pop task[pid].thread.ip to eip */ /*ret之后0号进程启动*/
53 "popl %%ebp\n\t"
54 :
55 : "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
56 );
57 }
58 void my_process(void)
59 {
60 int i = 0;
61 while(1)
62 {
63 i++;
64 if(i%10000000 == 0) /*循环1000万次才有一次机会需要判断是否需要调度*/
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 }
myinterrupt.c
/*
2 * linux/mykernel/myinterrupt.c
3 *
4 * Kernel internal my_timer_handler
5 *
6 * Copyright (C) 2013 Mengning
7 *
8 */
9 #include <linux/types.h>
10 #include <linux/string.h>
11 #include <linux/ctype.h>
12 #include <linux/tty.h>
13 #include <linux/vmalloc.h>
14
15 #include "mypcb.h"
16
17 extern tPCB task[MAX_TASK_NUM];
18 extern tPCB * my_current_task;
19 extern volatile int my_need_sched;
20 volatile int time_count = 0;
21
22 /*
23 * Called by timer interrupt.
24 * it runs in the name of current running process,
25 * so it use kernel stack of current running process
26 */
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 }
39
40 void my_schedule(void) /*my schedule:把当前进程(prev)的下一进程赋给nest,当下一个进程是0(正在执行)的话,用下面方法来切换进程。*/
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 /* schedule */
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 %%ebp\n\t" /* save ebp */
59 "movl %%esp,%0\n\t" /* save esp */
60 "movl %2,%%esp\n\t" /* restore esp */
61 "movl $1f,%1\n\t" /* save eip */
62 "pushl %3\n\t"
63 "ret\n\t" /* restore eip */
64 "1:\t" /* next process start here */
65 "popl %%ebp\n\t"
66 : "=m" (prev->thread.sp),"=m" (prev->thread.ip)
67 : "m" (next->thread.sp),"m" (next->thread.ip) /*prev sp就是%1 next sp就是%2*/
68 );
69 my_current_task = next;
70 printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
71 }
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 %%ebp\n\t" /* 将ebp压栈 */
80 "movl %%esp,%0\n\t" /* save esp */
81 "movl %2,%%esp\n\t" /* restore esp */
82 "movl %2,%%ebp\n\t" /* restore ebp */
83 "movl $1f,%1\n\t" /* save eip */ /*$1f是指接下来的标号1:的位置*/
84 "pushl %3\n\t"
85 "ret\n\t" /* 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 }
操作系统的两把剑:中断上下文和进程上下文的切换
3、运行这个精简的操作系统内核
可修改时间片缩小,使调度更加频繁。
四、总结
第二周的课程明显比第一周的课程难许多,重点还是要搞明白函数在调用过程中栈的变化,以及在构造简单的操作系统内核的过程中了解进程的调用,进程的切换,还有在进行嵌入式汇编过程中的限定符的应用。还有要记住计算机工作的三个法宝(存储程序计算机、函数调用堆栈、中断机制),还有函数的返回值通过eax寄存器传递,以及操作系统的两把剑:中断上下文和进程上下文的切换。但是对基于mykernel实现的时间片轮转调度代码我还是没有完全理解,希望通过今后的学习和课堂上老师的讲解可以更加清楚的理解。