《Linux内核分析》第二周 操作系统是如何工作的?
【刘蔚然 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000】
WEEK TWO(2.29——3.6)操作系统是如何工作的?
SECTION 1 函数调用堆栈
1.总结上一讲 计算机是如何工作的?
- 存储程序计算机工作模型(冯诺依曼结构),是各种类型计算机的一个框架;
 - 堆栈在早期是没有的,它是高级语言的起点(函数调用堆栈)
 - 中断机制(多道系统的基础,也是计算机效率提升的关键——否则的画计算机能逐个执行进程)
 
因而,本讲将介绍第二个重点:函数调用堆栈
2.堆栈
堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间,包括:
(也就是说CPU内已经集成好了一些功能)
- 函数调用框架
 - 传递参数(在32位的机器中,64位的略有不同)
 - 保存返回地址(%eax中可以保存返回地址或者返回值)
 - 提供局部变量空间
 
3.堆栈相关的寄存器
(上一讲中已经有所涉及,也就是ebp,esp这两个寄存器;还有eip这个寄存器)
【疑惑:关于pushl和popl两条命令对esp的影响?】
- 在一二讲的文档中,都告诉我们地址是向低地址端增长的。那么,该讲中的图示为什么相反呢?
 - 因为不同的机器有大端法和小端法的存储区别。图示中也是这样向低地址端增长的
 
【发生中断的时候,cs:eip会怎么变化呢?(之前的顺序、跳转以及调用都是一条流畅的指令流,并没有发生中断)】见最后一讲的笔记
【函数调用的堆栈框架?】

4.参数传递过程
以如下的代码反汇编程序为例:
#include <stdio.h>
void p1(char c)
{
    printf("%c",c);
}
int p2(int x,int y)
{
    return x+y;
}
int main(void)
{
    char c ='a';
    int x,y;
    x =1;
    y =2;
    p1(c);
    z = p2(x,y);
    printf("%d = %d+%d",z,x,y);
}
- 关于z = p2(x,y)一句
 

关于0xfffffff4、0xfffffff8的意思? 我在网上查了一下,其实这里面的0x虽然是16进制的意思,不过f并不是表示负数,而是一种内存表示方法(也有说0xfffffff4这个地址有一些其他的限制)
前两条语句什么意思?
- 就是把y的值和x的值存放到堆栈中;
 - 为什么不是pushl y而是采用变址寻址的方式呢?因为这两个值是局部变量,已经被保存在堆栈中了,所以就可以用变址寻址的方式找到;
 - 前两句中的pushl对应着第四句的add $0x8,%esp即:有压栈必有出栈
 
- 如何将p2的值返回给调用函数?
- 看mov %eax,0xfffffffc(%ebp)一句;
 - 0xfffffffc(%ebp)的位置上放置的是z,也就是说,这句指令将把返回值赋给z
 
 - push $0x8048510;就是把内存地址的内容压入堆栈
 
5.观察局部变量的存储机制

1.sub $0x18,%esp 的作用?
- 是预留出来了一段堆栈空间(0x18个空间),用来存储局部变量
 - 早期的编译器比较“笨”,需要在函数头部定义出所有变量,然后一次性留出这么多空间;而现在的编译器可以扫描整段代码中一共有多少变量然后进行空间分配,所以不强制在函数头部声明变量
 
6.练习一

- 进入实验楼,对代码进行反汇编;
 - 下载整理反汇编代码;
 
(6,7为编译及反汇编)


(8——11为代码)




SECTION 2 借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
1.利用mykernel实验模拟计算机硬件平台
- 搭建一个虚拟平台(虚拟的X86CPU),使用Linux源代码把这个系统配置好,通过程序执行入口执行程序;
 - 这样的一个简易的“系统”有上述三个“法宝”;
 - 当一个中断信号发生的时候,CPU把当前的esp,ebp,eip压入内核堆栈中,然后让eip指向中断处理程序入口去处理中断;
 - 在该系统中,使用的是时钟中断,即每隔一段时间发生一次中断
 
2. 实验步骤
- 进入实验楼,打开shell之后按照说明输入;
 - 运行QEMU,见截图
 
- (图12)
 - 查看源代码
- 查看mymain.c
- 见截图
 
- (13)
 - 之前的#include...都是硬件初始化用到的语句,而截图部分是是“操作系统”开始执行的入口
 - 从代码可见,每循环10 000次,打印一句话(从之前的屏幕滚动速度可见,现在的CPU执行的速度还是很快的)
 
 - 查看myinterrupt.c
- 见截图
 
- (14)
 - 每执行一次,都会执行一次时钟中断
 
 
 - 查看mymain.c
 
SECTION 3 在mykernel基础上构造一个简单地操作系统内核
1.C代码中嵌入汇编代码的写法
格式:(图15)

例子:
- C代码:

- (图16)
 
 - 汇编代码:

- (图17)
 - 解释
- movl $0,%%eax\n\t:第一个%表示转义字符
 - addl %1,%%eax\n\t:%1也是特殊符号,表示output与input(即下方:之后的部分)变量依次编号后,按照0,1,2,...的号码取变量
 - :"c"(val1),"d"(val2):也就是将val1的值放到ecx这个寄存器中,系统就会在编译之前自动完成这些工作。这样addl %1,%%eax\n\t 就会把ecx的值放到eax中
 - addl %%eax,%0\n\t:把eax的值放到m(内存变量)中
 
 
 
- C代码:
 - 练习

- (图18)
 - 难点解析:
- 最后的:eax是破坏描述部分,也就是说这段代码可能会破坏eax;
 - 最后输出的是两个内存变量temp和output
 
 - 结果:
- 0,1
 
 
 
2.一个简单的操作系统内核源代码【分析见注释】
1.mypcb.h//头文件,在其他的.c文件中引用;主要是数据结构的定义
1   /*
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; //即eip和esp
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; //进程入口
26 struct PCB *next; //进程链表
27 }tPCB;
28 
29 void my_schedule(void); //调度器
2.mymain.c 内核初始化和0号进程启动
1   /*
2  *  linux/mykernel/mymain.c
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]; //声明一个task数组,tPCB结构体类型在mypcb.h中有定义
19 tPCB * my_current_task = NULL;
20 volatile int my_need_sched = 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*/ //以下是0号进程数据结构的初始化
30 task[pid].pid = pid;
31 task[pid].state = 0;/* -1 不可运行, 0 可运行, >0 停止*/
32 task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process; //见第58行开始的my_process函数;起点是my_process
33 task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1]; //堆栈的栈顶位置,这个在mypcb.h中也有定义
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)); //copy一下0号进程的状态,然后需要修改的地方再修改(好机智的做法)
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; //42,43两句是很常见的链表加入新项的方法
43 task[i-1].next = &task[i];
44 } 
45 /* start process 0 by task[0] */ //为什么这里会是两句pushl和两句popl(ret相当于popl eip)?我的理解是:这样使得新的堆栈中的ebp和eip都与task[i]相匹配(esp的位置已经由第49句决定)
46 pid = 0; //作用同第27行
47 my_current_task = &task[pid];
48  asm volatile(
49  "movl %1,%%esp\n\t"     /* set task[pid].thread.sp to esp */ //确定esp的位置
50  "pushl %1\n\t"  /* push ebp */ //当前的栈是空的,ebp就等于esp
51  "pushl %0\n\t"  /* push task[pid].thread.ip */ //IP压栈
52  "ret\n\t"   /* pop task[pid].thread.ip to eip */ //弹出来eip,这之后0号进程正式启动
53  "popl %%ebp\n\t" //弹出来ebp,内核初始化工作完成
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)
65 {
66 printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
67 if(my_need_sched == 1) //执行10 000 000次才判断一次是否需要调度
68 {
69 my_need_sched = 0; //在第20行初始化为0
70  my_schedule();
71  }
72  printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
73 }
74 }
75 }
3myinterrupt.c 调度机制
1  /*
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) //设置时间片的大小,时间片用完时设置一下调度标志。只要满足上述两个条件,就将my_need_sched设为1,这样mymain.c循环到这个进程的时候就可以对这个进程进行调度
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)
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 */ //$1f就是指标号1:的代码在内存中存储的地址
62  "pushl %3\n\t"
63  "ret\n\t"   /* restore  eip */ //这两句使得下一个进程的ip作为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)
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"   /* save 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 */ //新进程的esp和ebp都是相同位置
83  "movl $1f,%1\n\t"   /* save eip */
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 }
这样,就完成了一个简单地操作系统内核程序并能够在虚拟机中启动。实现的效果就是0,1,2,3号进程依次循环切换。
总结
如何理解“操作系统是如何工作的?”
- 操作系统是建立在硬件基础上的,基于CPU对整个机器的资源进行管理和调度;
 - 操作系统本质上是一种软件,它需要有N多个函数代码来实现“智能”地管理(即根据不同的情境进行不同操作);
 - 就像课程中提到的,操作系统的“两把剑”即中断上下文和进程切换。因为这是操作系统能够有条不紊地处理多个计算机的进程的关键(也是操作系统的意义所在);
 - 操作系统内核会通过程序(就如同上面的几个小程序)判断何时进行进程切换、什么条件下进行切换,以及如何保存进程上下文,再到进程如何中断和开始等等;
 - 操作系统不纠缠于硬件的细节,只关注动作和调度的可实现性。
 
《Linux内核分析》第二周 操作系统是如何工作的?的更多相关文章
- Linux内核分析第二周--操作系统是如何工作的
		
Linux内核分析第二周--操作系统是如何工作的 李雪琦 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course ...
 - linux内核分析 第二周 操作系统是如何工作的
		
银雪纯 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.计算机是如何工作的 ...
 - 20135327郭皓——Linux内核分析第二周 操作系统是如何工作的
		
操作系统是如何工作的 上章重点回顾: 计算机是如何工作的?(总结)——三个法宝 存储程序计算机工作模型,计算机系统最最基础性的逻辑结构: 函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的 ...
 - LINUX内核分析第二周学习总结——操作系统是如何工作的
		
LINUX内核分析第二周学习总结——操作系统是如何工作的 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/course ...
 - Linux内核设计第二周——操作系统工作原理
		
Linux内核设计第二周 ——操作系统工作原理 作者:宋宸宁(20135315) 一.实验过程 图1 执行效果 从图中可以看出,每执行my_ start_ kernel函数两次或一次,my_ time ...
 - Linux内核分析第二周学习博客——完成一个简单的时间片轮转多道程序内核代码
		
Linux内核分析第二周学习博客 本周,通过实现一个简单的操作系统内核,我大致了解了操作系统运行的过程. 实验主要步骤如下: 代码分析: void my_process(void) { int i = ...
 - linux内核分析第二周
		
网易云课堂linux内核分析第二周 20135103 王海宁 <Linux内核分析>MOOC课程http://mooc.study.163.com/cours ...
 - Linux内核分析第二周学习笔记
		
linux内核分析第二周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...
 - Linux内核分析——第二周学习笔记20135308
		
第二周 操作系统是如何工作的 第一节 函数调用堆栈 存储程序计算机:是所有计算机基础的框架 堆栈:计算机中基础的部分,在计算机只有机器语言.汇编语言时,就有了堆栈.堆栈机制是高级语言可以运行的基础. ...
 - Linux内核分析第二周学习总结:操作系统是如何工作的?
		
韩玉琪 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.函数调用堆栈 ...
 
随机推荐
- iOS之07-三大特性之多态 + NSString类
			
多态 1.没有继承就没有多态 2.代码体现:父类类型的指针指向子类对象 类的创建: #import <Foundation/Foundation.h> // 动物 @interface A ...
 - ccc this 指针
			
cc.Class({ extends: cc.Component, properties: { musicPlayer: { default: null, type: cc.AudioSource } ...
 - python 代码片段25
			
#coding=utf-8 # 虽然python是面向对象的语言,但是没有显式的构造函数概念. # python没有new关键词 class MyClass(object): pass m=MyCla ...
 - Storm与Spark:谁才是我们的实时处理利器
			
Storm与Spark:谁才是我们的实时处理利器 ——实时商务智能目前已经逐步迈入主流,而Storm与Spark开源项目的支持无疑在其中起到了显著的推动作用.那么问题来了:实时处理到底哪家强? 实时商 ...
 - hdu 2160  母猪的故事
			
Problem Description 话说现在猪肉价格这么贵,著名的ACBoy 0068 也开始了养猪生活.说来也奇怪,他养的猪一出生第二天开始就能每天中午生一只小猪,而且生下来的竟然都是母猪.不过 ...
 - ACM 懒省事的小明
			
懒省事的小明 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 小明很想吃果子,正好果园果子熟了.在果园里,小明已经将所有的果子打了下来,而且按果子的不同种 ...
 - 蒟蒻修养之cf橙名计划2
			
29 由于第一篇没写题意导致大部分神题题解已经完全不知道在说啥了...所以还是重开一篇保平安... [303A]题意:求三个长度为$n(n<=10^5)$的排列$a,b,c$使得$a_i+b_i ...
 - 【noiOJ】p8211 (PS:二分浮点数的精度问题)
			
05:派 查看 提交 统计 提问 总时间限制: 1000ms 内存限制: 65536kB 描述 我的生日要到了!根据习俗,我需要将一些派分给大家.我有N个不同口味.不同大小的派.有F个朋友会来参加 ...
 - Codevs  p1004  四子连棋
			
四子连棋 题目描述 Description 在一个4*4的棋盘上摆放了14颗棋子,其中有7颗白色棋子,7颗黑色棋子,有两个空白地带,任何一颗黑白棋子都可以向 ...
 - 【JavaService】部署Java jar为Windows后台服务
			
将Java jar文件部署为Windows后台服务有多种方法:Service Installer.Java service Wrapper.JavaService.exe等等.这里介绍下使用JavaS ...