第二周 操作系统是如何工作的

第一节 函数调用堆栈

存储程序计算机:是所有计算机基础的框架

堆栈:计算机中基础的部分,在计算机只有机器语言、汇编语言时,就有了堆栈。堆栈机制是高级语言可以运行的基础。

计算机“三宝”:存储程序计算机、函数调用堆栈和中断机制。

  • 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间
  • 函数调用框架(eg:enter、leave )
  • 传递参数(32位,通过堆栈来传递参数)
  • 保存返回地址(用eax)
  • 提供局部变量空间
  • 等等

  

♦ C语言编译器对堆栈的使用有一套的规则,不同的系统中,编译C代码会有不同的汇编代码。

♦ 了解堆栈存在的目的和编译器对堆栈使用的规则是理解操作系统一些关键性代码的基础。

一、堆栈寄存器和堆栈操作

pop:从高地址向低地址

push:从低地址向高地址

二、利用堆栈实现函数的调用和返回

1.其他关键寄存器

  • cs : eip:总是指向下一条的指令地址
• 顺序执行:总是指向地址连续的下一条指令
• 跳转/分支:执行这样的指令的时候,cs : eip的值会
根据程序需要被修改
• call:将当前cs : eip的值压入栈顶,cs : eip指向被
调用函数的入口地址
• ret:从栈顶弹出原来保存在这里的cs : eip的值,放
入cs : eip中

  中断机制是如何工作的?

call指令:
)将eip中下一条指令的地址A保存在栈顶
)设置eip指向被调用程序代码开始处

三、函数堆栈框架的形成

  1. call xxx
  • 执行call之前
  • 执行call时,cs : eip原来的值指向call下一条指令,该值被保存到栈顶,然后cs : eip的值指向xxx的入口地址

  2. 进入xxx

  • 第一条指令: pushl %ebp
  • 第二条指令: movl %esp, %ebp
  • 函数体中的常规操作,可能会压栈、出栈

  3. 退出xxx

  • movl %ebp,%esp
  • popl %ebp
  • ret

第二节 借助Linux内核部分源代码模拟存储程序计算机工作模型及时钟中断

当一个中断信号发生时,CPU把当前的eip,esp,ebp压到内核堆栈中去,并把eip指向中断处理程序的入口。

C代码中嵌入汇编代码

第三节 在mykernel基础上构造一个简单的操作系统内核

执行:

mymain.c

myinterrupt.c

实验:完成一个简单的时间片轮转多道程序内核代码

mypcb.h

/*
* linux/mykernel/mypcb.h
*
* Kernel internal PCB types
*
* Copyright (C) 2013 Mengning
*
*/ #define MAX_TASK_NUM 4
#define KERNEL_STACK_SIZE 1024*8 //定义进程控制块,实际内存叫TASK_STRUCT /* CPU-specific state of this task */
struct Thread { //用来存储eip,esp
unsigned long ip; //保存eip
unsigned long sp; //保存esp
}; 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; //定义程序入口,一般是main函数,这里指定了
struct PCB *next; //进程用链表链起来
}tPCB; void my_schedule(void); //调度器

mymain.c

/*
* linux/mykernel/mymain.c
*
* Kernel internal my_start_kernel
*
* Copyright (C) 2013 Mengning
*
*/
#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 * my_current_task = NULL; //声明当前TASK的指针
volatile int my_need_sched = ; //是否需要调度的标志 void my_process(void); void __init my_start_kernel(void)
{
int pid = ;
int i;
/* Initialize process 0*/
task[pid].pid = pid; //初始化当前的0号进程
task[pid].state = ;/* -1 unrunnable, 0 runnable, >0 stopped */ //状态:正在运行
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-]; //栈顶为stack
task[pid].next = &task[pid]; //指向自己本身
/*fork more process */ //初始化更多的进程
for(i=;i<MAX_TASK_NUM;i++)
{
memcpy(&task[i],&task[],sizeof(tPCB));
task[i].pid = i; //复制0号进程的状态
task[i].state = -;
task[i].thread.sp = (unsigned long)&task[i].stack[KERNEL_STACK_SIZE-]; //每个进程有自己的堆栈
task[i].next = task[i-].next; //指向下一个进程
task[i-].next = &task[i]; //新fork的进程放在进程列表的尾部
}
/* start process 0 by task[0] */
pid = ; //0号进程开始执行
my_current_task = &task[pid];
asm volatile(
"movl %1,%%esp\n\t" /* set task[pid].thread.sp to esp */
"pushl %1\n\t" /* push ebp */
"pushl %0\n\t" /* push task[pid].thread.ip */
"ret\n\t" /* pop task[pid].thread.ip to eip */
"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 = ;
while()
{
i++;
if(i% == ) //执行1000万次
{
printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid); //输出,主动调度
if(my_need_sched == ) //执行1000万次调度1次
{
my_need_sched = ;
my_schedule();
}
printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
}
}
}

mymain.c是一个死循环,一直不断地打印my_start_kernel here。中间加上if判断只是因为,cpu处理一次循环的速度太快,它只每100000次循环打印一次,以便我们观察。

嵌入式汇编

myinterrupt.c

/*
* linux/mykernel/myinterrupt.c
*
* Kernel internal my_timer_handler
*
* Copyright (C) 2013 Mengning
*
*/
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h> #include "mypcb.h" extern tPCB task[MAX_TASK_NUM]; //extern一些全局的东西
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = ; //时间计数 /*
* Called by timer interrupt.
* it runs in the name of current running process,
* so it use kernel stack of current running process
*/
void my_timer_handler(void)
{
#if 1
if(time_count% == && my_need_sched != ) //时间中断一千次,并且my_need_sched不等于1时,把my_need_sched赋为1
{
printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
my_need_sched = ;
}
time_count ++ ;
#endif
return;
} void my_schedule(void)
{
tPCB * next;
tPCB * prev; //当前进程 if(my_current_task == NULL
|| my_current_task->next == NULL)
{
return;
}
printk(KERN_NOTICE ">>>my_schedule<<<\n");
/* schedule */
next = my_current_task->next; //把当前进程的下一个进程赋给prev
prev = my_current_task;
if(next->state == )/* -1 unrunnable, 0 runnable, >0 stopped */
{
/* switch to next process */ //两个正在运行的进程之间做进程上下文切换
asm volatile(
"pushl %%ebp\n\t" /* save ebp */ //保存当前进程的ebp
"movl %%esp,%0\n\t" /* save esp */ //当前进程的esp赋到0,即thresd.sp
"movl %2,%%esp\n\t" /* restore esp */ //把下一个进程的sp放入esp中
"movl $1f,%1\n\t" /* save eip */ //保存eip
"pushl %3\n\t" //把下一个进程的eip push到栈里
"ret\n\t" /* restore eip */
"1:\t" /* next process start here */
"popl %%ebp\n\t"
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
}
else
{
next->state = ; //进程设置为运行时状态
my_current_task = next; //作为当前正在执行的进程
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
/* switch to new process */
asm volatile(
"pushl %%ebp\n\t" /* save ebp */ //保存当前ebp
"movl %%esp,%0\n\t" /* save esp */ //保存当前esp
"movl %2,%%esp\n\t" /* restore esp */ //重新记录要跳转进程的esp,%2为next->thread.sp
"movl %2,%%ebp\n\t" /* restore ebp */ //重新记录要跳转进程的ebp,%2为next->thread.sp
"movl $1f,%1\n\t" /* save eip */ //保存当前eip,%1为next->thread.ip,%1f指标号:1的代码在内存中存储的地址
"pushl %3\n\t"
"ret\n\t" /* restore eip */ //重新记录要跳转进程的eip,%3为next->thread.ip
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
}
return;
}

myinterrupt.c是一个被时间周期调用的函数,也可以看成是一个死循环的功能,一直打印>>>>>my_timer_handler here <<<<<。但与一般的死循环不同是,一般的死循环只是在做循环,而这个是周期性的调用函数。因此,mymain.c中的my_start_kernel只调用了一次,而myinterrupt.c中的my_timer_handler一直被调用。相当于,进程的初始化与进程的调度。

操作系统的两个法宝:

  • 保存现场与恢复现场:中断上下文处理程序
  • 进程上下文的切换

总结:操作系统是如何工作的

工作机制:存储程序计算机、函数调用堆栈和中断机制。

存储程序计算机最早是由著名数学家冯·诺伊曼等人在1946年总结并明确提出来的。任何一个程序通常都包括代码段和数据段,程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。引入中断机制使内核可以处理硬件外设I/O。中断来源有I/O请求、时钟以及系统调用。

在单处理系统中,任何时候只有一个进程在运行,它要么处于用户态,要么处于内核态。然而在linux内核是可重入的,这意味着若干个进程可以同时在内核态下执行。用户态切换到内核态有3种方式,系统调用、异常、外围设备的中断。系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本上也是一致的。

进程是动态执行的实体,内核是进程的管理者。进程不但包括程序的指令和数据,而且包括程序计数器和CPU的所有寄存器以及存储临时数据的进程堆栈。所以,正在执行的进程包括处理器当前的一切活动。进程既可以在用户态下运行,也能在内核下运行,只是内核提供了一些用户态没有的核心服务,因此进程在访问这些服务时会产生中断,必须进行用户态与内核态的切换。

Linux内核分析——第二周学习笔记20135308的更多相关文章

  1. Linux内核分析第二周学习笔记

    linux内核分析第二周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  2. Linux内核分析——第一周学习笔记20135308

    第一周 计算机是如何工作的 第一节 存储程序计算机工作模型 1.冯·诺依曼结构模型:冯·诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构.程序指令存储地址和数据存储 ...

  3. 三20135320赵瀚青LINUX内核分析第二周学习笔记

    赵瀚青原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.计算机的三个法宝 存储程 ...

  4. Linux内核分析——第二周学习笔记

    20135313吴子怡.北京电子科技学院 chapter 1 知识点梳理 (一)计算机是如何工作的?(总结)——三个法宝 ①存储程序计算机工作模型,计算机系统最最基础性的逻辑结构: ②函数调用堆栈,高 ...

  5. LINUX内核分析第二周学习总结——操作系统是如何工作的

    LINUX内核分析第二周学习总结——操作系统是如何工作的 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/course ...

  6. linux内核分析第一周学习笔记

    linux内核分析第一周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  7. Linux内核分析第二周学习博客——完成一个简单的时间片轮转多道程序内核代码

    Linux内核分析第二周学习博客 本周,通过实现一个简单的操作系统内核,我大致了解了操作系统运行的过程. 实验主要步骤如下: 代码分析: void my_process(void) { int i = ...

  8. 20135320赵瀚青LINUX内核分析第一周学习笔记

    赵瀚青原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.概述 第一周的学习内容主 ...

  9. Linux内核分析第二周学习总结:操作系统是如何工作的?

    韩玉琪 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.函数调用堆栈 ...

随机推荐

  1. Alpha冲刺报告(3/12)(麻瓜制造者)

    今日已完成情况: 肖小强: 解决了之前的部署demo问题,学习了基本需要的api 江郑: 进行发布需求数据库的增删改查 邓弘立: 完成了轮播图的设计 刘双玉: 编码发布商品的接口,数据库表的修改 汪志 ...

  2. Django商城项目笔记No.5用户部分-注册接口-短信验证码

    Django商城项目笔记No.4用户部分-注册接口-短信验证码 短信验证码也保存在redis里(sms_code_15101234567) 在views中新增SMSCodeView类视图,并且写出步骤 ...

  3. DFS的运用(二分图判定、无向图的割顶和桥,双连通分量,有向图的强连通分量)

    一.dfs框架: vector<int>G[maxn]; //存图 int vis[maxn]; //节点访问标记 void dfs(int u) { vis[u] = ; PREVISI ...

  4. SDOI 2018 R2 游记

    一篇真正的“游记”. DAY -3 下午: 今天老师批准了我去省选划水的请假要求. 批准之后感觉学习非常有动力,顺便忽悠别的同学和我一起去,然而wzx是唯一一个表示可以考虑一下的同学,其他同学直接一口 ...

  5. ES6标准入门之数值的拓展解说

    ES6提供了二进制和八进制数值的新写法,分别用前缀0b(或0B)和0o(或0O)表示. 0b111110111 === 503                    // true 0o767 === ...

  6. Docker学习4-Containers - 容器

    用Docker方式构建应用程序,从这个应用程序层次结构的底层容器开始.高于此级别的是一项服务,它定义了容器在生产中的行为方式.在顶层是堆栈,它定义了所有服务的交互. Stack  堆栈 Service ...

  7. odoo方法

    一. 类似于前面有个_ 的方法,格式是如下def _getdlvqty(self, cr, uid, ids, field_names, args, context=None): def _get_p ...

  8. jQuery上传文件

    1.引入资源 <script src="/yami/backend/backres/js/jquery.min.js"></script> <scri ...

  9. Docker:一个装应用的容器

    一:简介:你是否经历过“我本地运行没问题啊!““哪个哥们有写死循环了““完了,服务器撑不住了“等等问题,docker就是这么帮你解决问题的工具,它可以帮你把web应用自动化打包和发布,在服务型环境下进 ...

  10. 如何优雅的使用mybatis

    原文:https://blog.csdn.net/gebitan505/article/details/54929287 这两天启动了一个新项目因为项目组成员一直都使用的是mybatis,虽然个人比较 ...