李治军操作系统 笔记

第一篇:操作系统基础

1.1 操作系统概述

1.2 操作系统引导(总体做的:将操作系统读入内存+初始化)

bootsect.s
 setup.s
      读取硬件参数
      读取system到内存0x00位置
      从16位模式 转到 32位模式,即从实时模式 转到 保护模式, 使得寻址方式发生了改变,跳转到0x00执行system,其实是转到head.s代码
      实时模式: 地址翻译 CS<<4 + IP
      保护模式 :根据CS查表 + IP
 head.s
      初始化GDT,IDT表
      转到main()函数,main()是c函数
 main()
      各种初始化,包括mem_init 标记那些内存块被使用,哪些没有被使用

1.3系统接口

1.3.1 操作系统接口

由系统提供的函数调用

1.3.2 系统调用的实现

硬件上隔离用户段和内核段,DPL CPL来实现, 其实是把内存分段了,进行隔离保护  从用户段访问内核段,必须通过中断0x80才能访问内核段

img

库函数write:

1)系统调用号 => %eax

参数1 => %ebx

参数2 => %ecx

参数3 => %edx

2) int 0x80 中断,进入内核,调用 system_call,  int 0x80 和 system_call函数的关系要先注册好 ,即设置IDT表,改变了DPL,以及段寄存器地址,还有偏移地址,实际上就是设置了DPL和中断函数地址 !!!

system_call:
      call _sys_call_table(,%eax,4)           //eax保存的是系统调用号,有查call_table表格
call_table :
                //查找表,通过系统调用号查找对应的系统调用指针,然后去调用对应的sys_write

疑问

有system_call()的函数声明,但是没有实体,只有在.s文件中找到_system_call的标号
就是是_system_call,
c语言中声明和调用的时候都写成system_call
汇编中声明为_system_call,多一个下划线

汇编中调用汇编,则调用_system_call;
汇编中调用c函数,要在c函数前面加一个下划线,、_function;

img

1.4 操作系统历史

Unix
进程图谱
文件操作

第二篇. 操作系统之 进程与线程

六、 线程引出与实现

L10:用户级线程

进程 和 线程 都是动态概念
进程 = 资源 (包括寄存器值,PCB,内存映射表)+ 指令序列
线程 = 指令序列

线程 的资源是共享的,
进程 间的资源是分隔独立的,内存映射表不同,占用物理内存地址是分隔的

线程 的切换只是切换PC,切换了指令序列
进程 的切换不仅要切换PC,还包括切换资源,即切换内存映射表

用户级线程:调用Yield函数,自己主动让出cpu,内核看不见,内核只能看见所属进程而看不见用户级线程,所以一个用户级线程需要等待,内核会切到别的进程上,不会切到该进程下的其他用户级线程!!!
内核级线程: 内核能看见内核级线程,一个线程需要等待,内核会切到所属进程下的其他内核级线程。

L11:内核级线程

11.1 基本概念

只有内核级线程才能发挥多核性能,因为内核级线程共用一套MMU,即内存映射表,但是有多个CPU,可以一个CPU执行一个内核级线程
进程 无法发挥多核性能,因为进程切换都得切MMU,即切换内存映射表,一个

内核级线程的切换需要两套栈: 用户栈 + 内核栈

11.2 完整的系统调用中断过程:

  1. INT中断自动压栈的有下一条指令,以及用户级线程SS:SP,就是下图五个参数

img

  1. _system_call 把寄存器保护压栈是压到内核栈中,需要手动压栈
  2. 系统调用,(有可能是_sys_fork,其实就是根据标号找到的系统调用),结束之后继续执行,要执行reschedule,先push $ret_from_sys_call,让其在_schedule之后返回到ret_from_sys_call, _schedule为c函数,结束右括号会把ret_from_sys_call pop出来,返回到这里执行,即执行ret_from_sys_call;
    这里注意call 和 jmp的区别!!!
    4)在ret_from_sys_call中pop出_system_call时保护的寄存器内容,然后中断返回!!!
    5)中断返回是在最后,中断返回会把SS:SP 以及用户态的下一条指令 POP出来,即把5个寄存器pop出来!!!这样就会返回到用户栈,运行用户态的下一条指令!!!

代码:

reschedule:
    pushl $ret_from_sys_call
    jmp _schedule                              //如果用call就是先将下一条指令压栈,然后jmp,这样只能顺序往下走,但是用push+jmp则可以改变跳转地址!!!123

11.3 switch_to五段论

img

这里要注意,中断出口这里已经经过了前面的switch_to,中断的iret已经不是原先的中断返回了,是切换后的新中断的执行返回!!!这样返回以后就来到了引发该新中断的用户态代码来执行

11.4 内核级线程切换实例

fork函数经典调用

if (!fork())          //关键在于 INT 80 后面的指令 mov res, %eax;父进程和子进程都会执行这个代码,但是%eax的值不一样
{
     子进程
}
else
{
     父进程
}12345678

11.5 汇编调用c函数

实际上是把参数都压到栈里,然后c程序就可以调用,用call来调用
但是要注意c语言 调用结束后,要把栈里的参数删掉,即addl指令,把指针改一下,忽略那些参数

实际上是
push 参数
push 返回地址
jmp 调用地址

c函数右括号会生成ret指令,会返回到返回地址!
这个时候要把没用的参数从栈里去掉,这就是为什么要调用addl

img

img

img

img

L12 核心级线程实现实例

L13 操作系统的那棵树

只有进入内核才能进行内核调度,进入内核的唯一方法就是中断

七. CPU 调度

L14 CPU调度策略

14.1 指标

周转时间: 从开始申请执行任务,到执行任务完成
响应时间: 从开始申请执行任务到开始执行任务

14.2 三种调度方法

1) 先来先服务 平均周转时间可能会很长
2) 短作业优先(SJF)
周转时间短,但是响应时间长
适用于后台程序,如gcc的编译,快点把整个程序编译完
3) 时间片轮转(RR)
响应时间可以得到保证,nT,n为任务个数,T为时间片长度,
适用于前台程序,IO操作多的
4) 优先级轮转
固定优先级,可能会造成又程序一直没法得到执行,需要动态调整优先级

L15 一个实际的schedule函数

八. 进程同步与死锁

L16 进程同步与信号量

同步的作用: 各个程序走走停停,配合向前推进
同步 = 等待 + 唤醒
依据信号量来执行等待和唤醒

信号量 为负数表示欠
为整数表示富裕

L17 信号量临界区保护

17.1 为什么要保护

同时修改信号量可能造成empty的含义不正确

img

empty正常应该为-3,但是可能因为同时修改变成了-2,含义不对了
必须进入临界区以后才能修改信号量,修改完成后退出临界区,临界区是互斥的,只能有一个进程能够进入各自临界区!!!

验证保护算法是否合理的标准:
互斥进入
有空让进
有限等待

img

17.2 如何实现保护

其实是进入临界区的保护

17.2.1 软件方法

1)轮换法 有空让进效果不好!!!
2)标记法 可能会造成无限制空转等待
3)非对称标记 结合了标记和轮转两种思想
两个进程:Peterson算法
多个进程:面包店算法

17.2.2 硬件方法

关闭中断来关闭调度即可
但是注意,多CPU的时候不好使
这里涉及到多cpu如何schedule

17.2.3 硬件原子指令

其实是用 mutex锁信号量 来保护信号量,为了解决mutex仍然需要保护的问题
使用硬件级原子操作,不能被打断不能切出去进行调度
img

L18 信号量的代码实现

信号量的用法可以有两种:
1)sem 有正有负
-n 表示有n个进程在等待这个资源,欠了n个
+n 表示该资源有n个空余
这种可以用 if来判断是否睡眠
但是这里没说如何唤醒

2)sem 只有0,和1两种状态
1表示锁住态
0表示解锁态
在检查的时候用while处理,当锁住则睡眠,当被唤醒,重新要检查下状态是否被锁住
这个需要外围机制配合,唤醒的时候,是将整个等待此资源的队列里的所有进程都唤醒,然后让其根据优先级去竞争调度,起到优先级搞得优先获得该资源继续执行的效果!!!
img

唤醒等待此资源的队列里的所有进程的原理是,先唤醒队首进程,再让队首进程去唤醒下一个,以此类推,一路唤醒!!!

void sleep_on(struct task_struct **p){ 
    //p是指向队首pcb的指针的指针 
    struct task_struct *tmp; tmp = *p; 
    //tmp指向原来的队首 
    *p = current; 
    current->state = TASK_UNINTERRUPTIBLE;
    schedule(); 
    if (tmp) //用来唤醒下队列中下一个等待资源的进程 
        tmp->state=0;
} 

img
实际上市把自己作为了队首,然后用tmp记录了队列里的下一个pcb,便于在唤醒的时候能够唤醒队列里的下一个进程

L19 死锁

19.1 必要条件

img
形成了资源等待环路!!!

19.2 死锁处理方法

img
死锁预防
死锁避免 :检测每个资源请求,假装分配,看看进程组是否会造成死锁,如果造成死锁就拒绝如果找到了安全序列,就可以这样分配。
银行家算法
但是这样的算法时间复杂度太高,每次请求资源都算一次,效率太低

死锁检测+恢复
等出现问题了,有一些进程因为死锁而停住了,再处理,选择一个进程进行回滚,然后再用银行家算法来算是否能找到安全序列,如果不行,再回滚,直到所有程序都能执行。
但是回滚是个大问题!!!已经写入磁盘,还得退回来,那就很麻烦了。

死锁忽略
windows,linux个人版都不做死锁处理,直接忽略,大不了重启就好了,小概率事件,代价可以接受

第三篇. 操作系统之 内存管理

九. 内存管理

L20 内存使用与分段

20.1 运行时重定位

基地址 + 逻辑偏移地址
基地址以表的形式保存在PCB中

一个程序分成多个段,每个段都需要记录其基地址,存在一个表中,该表称为段表,段表保存到PCB中
img

img
GDT : 操作系统的段表
LDT : 各个用户进程的段表

L21 内存分区与分页

直接按段来分,程序员很喜欢,可以将数据段 和 代码段分开加载,可以设置只读,可读写属性
但是会有一个很大的问题 : 内存碎片,导致后面没有符合要求大小的空闲内存
img

解决办法: 不要对内存进行连续的分配,将内存划分成1页1页,按页分配,1页4kb大小,最多浪费的也就4KB。这样不会有内存碎片,也不会出现没有符合要求大小的内存可以申请的情况,因为可以打散了分散到一页一页中。
连续的变成离散的,需要记录这种映射关系,所以就有了页表
img

img
这里其实是对整个32条地址线的4G空间做了划分,建立了一个页表;
相当于说对可能用到的地址做一个虚拟页到物理页的映射转换
4G空间,每4KB一页,一共有1M页,那么映射表就是这1M页的相互连线映射关系

L22 多级页表和快表

按上一节的页表处理方式,页表的储存空间太大,1M页的映射关系,每条映射关系需要4字节来存储,那么就需要4M空间来存储页表;
每个进程4M,10个进程就要40M,十分的浪费!!!
img

有很多都是没有用到的,一个程序不可能占满了4G内存空间!!!
所以要想办法减低存储。
法一.
删除那些没有用到的表项
虽然存储减低了,但是会造成无法页号编码不连续,查找映射关系的时候非常麻烦,需要从上往下遍历,看是否是该页号,时间效率很低

法二.
用多级页表
(相当于书的目录的作用)
既减轻了存储压力,每个还都连续,时间效率比较高
img
这里16K是这么来的:
用到三个页目录号,每个页目录号下一级的页表存储空间为 3 * 4KB = 12KB,
加上本身页目录表需要的4KB一共为16KB。
关键在于,有些没有用到的页目录号后面根本就没有再存下一级的映射表,节省了大量空间!!!

法三.
多级页表 + 快表
快表相当于一个cache,把频繁使用的地址映射记录下来

L23 段页结合的实际内存管理

程序员喜欢段,物理内存要用到页,为了满足这两个要求,操作系统在当中用虚拟内存做了一层中转
img

fork
copy_mem 这个函数可以好好读一下,配合linux0.11注解
1)子进程的数据段和代码段映射到不同的虚拟内存地址,每个进程分配 64M 虚拟内存地址
2)父进程和子进程共用一套页目录表,因为虚拟地址不同,页目录号号不同,不打架
3)在页目录表中增加对应页框号表,即父进程和子进程中对应的虚拟地址不同,但是不同地址映射到了同一块页框,也即映射到同一块物理内存。
4)然后对子进程设置共享内存段属性为只读,这样在子进程往里写的时候,会发现只读,会修改映射,映射到其他空间!!!
达到对于用户来讲虽然是操作的同一个地址,但其实对应的是物理内存的两块区域,即子进程和父进程的内存区域是分隔的!!!

img

L24 请求调页与内存换入

虚拟内存永远是4G空间, 但是物理内存可能只有1G或者2G
操作系统做了一层封装,对于用户来讲,使用的空间一直都是4G虚拟空间
操作系统配合MMU,把虚拟地址映射到物理内存页上

L25 内存换出

有换入就有换出

25.1 换出哪一页的算法

1)FIFO
2)MIN算法
最优,但是需要知道将来请求的页号,不现实
3)LRU 最近一段时间内,最久不使用的页换出去,即最近最少使用的换出去
时间戳: 时间代价太大
页码栈: 修改指针次数太多,代价还是太大
近似算法:
用是和否来表示,有访问到这块内存,MMU自动把R置1
指针去找R为0的地方替换,碰到R=1的地方先做R=0

但是这个方法有缺点:缺页比较少的时候,所有的R都为1,每次都要转一圈才能找到换出去的页,退化成FIFO,效率不高

改进: 双指针,一个快,一个慢,像时钟一样
快时钟做R的清0定时清0,等到慢指针转到这里的时候R=0,说明在定时时间片内没有备访问,该页可以被替换了

25.2 给一个进程分配多少个页框

工作集算法

posted @ 2018-05-30 14:52  ACSuperChen  阅读(412)  评论(0编辑  收藏  举报