操作系统(哈工大李治军老师)----- 学习笔记

操作系统哈工大

启动

图灵机一条带(取指执行) ==>> 冯诺依曼包装起来,一整个程序通过PC交由CPU计算。加载到内存才能取指执行

打开电源执行的第一条指令 PC = ???内存中有什么 ???

X86的PC ==>> CS << 4 + IP

PC由硬件决定

开机后取指执行,检查硬件情况,把磁盘0磁道0扇区(引导扇区)读入0x7c00处,设置PC,执行bootsect.s汇编代码(把引导扇区的代码移动,让出位置),代码继续执行,通过中断(int 0x13)载入setup模块, setup模块记录硬件信息,通过中断向屏幕打印字符。控制权交由setup模块,bootsect.s退出舞台。

setup初始化,获取内存大小(管理内存得先获取内存大小才能用数据结构管理),获取硬件参数信息,并把操作系统移动到0的位置,之前让出的位置就是以免这里的移动覆盖了setup,因为setup还没工作完。最后一个关键是进入保护模式改变寻址方式扩大寻址范围。之前都是16位寻址,15位最大寻址为1M所以得扩大。

跳转到head.s操作系统模块的第一个汇编,初始化GDT和IDT表项,跳转到main.c(第一个c程序)启动后初始化各项管理硬件的数据结构,一直在跑。操作系统代码放在了内存的零地址,应用程序在上端,通过系统调用的中断进入操作系统。

bootsect.s

中断读入setup模块(4个扇区)再读入操作系统模块

setup模块

加载和打印字符logo

初始化完成后,进入保护模式

间接寻址,查找中间表

head.s

初始化gdt和idt表

启动中用到了3种汇编

main.c

清空内存非系统使用的地方变为零

进入操作系统

通过系统调用中的中断代码进入到内核

操作系统在后台跑着,并开启shell等待用户的输入执行进程

消息队列 + 消息处理程序

统一系统调用接口标准,可以实现夸平台

为了安全需要区分用户态和内核态

通过硬件设计,目标端寄存器和源寄存器,进入的时候区分内核端还是用户段

中断把目的寄存器权限位改成3,进入到系统后把源寄存器权限位改成0

中断是进入系统的唯一方式,系统调用通过宏展开包涵里面的int0x80 (进入到操作系统)

进入中断的关键代码(在call表,找到对应的系统调用)

历史

简单的取指执行cpu利用率会大大降低,遇到IO操作需要等很久

出现了多道程序,交替执行提高利用率

多进程的视图

系统管理多个进程向前推荐,操作系统的重要部分

进程运行

进程切换

进程的状态队列,(就绪、等待、执行),切换的策略

出现的问题

内存地址相同时有问题

出现了对内存的管理MMU,虚拟内存映射,每个PCB有一个映射表

进程的合作会出现竞争(生产者,消费者模型)

当还没执行完就切换的时候导致两个进程读取的数据不一致,需要加锁

引入用户级线程

进程切换需要保存上下文,消耗时间久。拆成多个指令序列。线程比进程的切换少切了内存的映射表。

例如一个服务器可以拆开多个指令做不同任务,并且他们共享同一资源

通过yield切换让出cpu,切换也得保存上下文,所以得需要栈保存切出去的时候的返回地址。

如果这个栈共用的话,切回来的时候就找不到对应线程的返回地址,所以抽出一个和进程一样的数据结构(PCB, TCB)

create的时候创建数据结构,把数据结构初始化,切出去的时候保存上下文

引入内核级线程

进程的切换是在内核中的(因为要分配资源,访问内存文件等),进程的切换就是内核级线程切换加上一些资源表的切换,不像用户级线程一样可以随意切换。

用户级线程,不用管操作系统,是内核级的一个子部分(因为了解了指令序列怎么切换)

用户级线程,操作系统感知不到,当其中一个线程需要IO时,操作系统因为时间问题会切换另一个进程,那么刚刚那个进程里面的多个线程就没作用了

因为内核级线程,多核才有了意义。

TCB切换内核和用户态都要切

工作原理

内核级线程和进程基本一样,除了进程多了个资源映射表
只要有中断就会进入内核栈,内核栈和用户栈有一条拉链关联,当内核栈返回时调用iret退回到用户栈,那么一套栈就建立起来了

当下沉到内核栈时发生阻塞就会进行线程切换,把切换前的状态保存在TCB,找到要切换的TCB盖到CPU执行

用户级线程通过中断进入内核,切换内核级线程时(先保存快照TCB)再找到要切换的线程的TCB进行内核栈切换,再回到另一个用户级线程

ThreadCreate这个系统调用要构造TCB

举个例子 fork

中断进入后,发生阻塞或时间片没了就切换

切换就是把另一个TCB盖到CPU去执行

fork的时候,进入内核TCB保存一些寄存器的值(用来给子进程的内核栈赋值),fork后共用用户栈(返回的时候才会再同一个地方)
父进程有自己的内核栈,子进程也有自己的内核栈,途中父进程阻塞了切换到子进程的内核栈执行。(通过寄存器eax的值判断父子进程)


fork之后,子进程如何exec, exec中断进入后 (中间的过程读文件修改TCB) 通过iret返回到要执行的文件入口(entry),执行路径就和父进程不一样了


脑海里想一想这个过程

单核,多进程切换

进程的调度


第一种是短作业优先 (周转时间小,响应时间不能保障)

第二种是时间片轮转 (响应时间有保障,周转时间有问题)

两种结合,需要不断的学习来区分前台和后台进程

schedule算法

折中的调度算法,counter做时间片,等待中的程序counter加大,并且收敛与2p(就是一个学习的过程)

在时钟中断中需要对时间片--,时间片为零是切换

保证了响应时间,同时IO型进程优先级有加大

进程同步

同步:关键在于控制进程的“走停”, 关键在于什么时候该“停”

引入“信号”这个概念去控制,但是“信号”能保存的信息有限(例如有多少个进程睡眠了)
生产者有多个,消费者在唤醒的时候单靠信号(缓冲区的个数)是不够的, 还应该记录一些信息例如有多少个生产者

资源个数定义为信号量,没资源了就变负数了就阻塞在队列中,队列保存进程的信息(PCB)

思考这个走停过程,如果mutex往上提会出现死锁 资源为缓冲区,缓冲区满了生产者睡眠资源数--(信号量--)
申请三个信号量,一个是缓冲区资源,一个是生产者生产的资源(提供给消费者),一个是锁资源(互斥信号量)

目前的信号量相当于全局变量,也会发生竞争
所以要用临界区对信号量进行保护, 只允许一个进程进入


临界区的实现

用临界区去保护信号量,用信号量实现同步推进多进程的合作
第一种值日轮换法, 可以满足互斥,但是因为轮回turn改变之后就不会进入了得该别人了

第二种标记法, 可以满足互斥,但是会发生两个人都不进入

第三种非对称标记,有一个人更勤劳, Peterson算法

第四种遇到多进程,面包店算法

但是这些都太麻烦了,所以用硬件进行处理 (可以关中断,让当前进程不被调度一定会执行完,但是只合适单核) (用硬件实现原子操作)
用临界区去保护信号量,用信号量实现同步推进多进程的合作

Linux0.11对信号量的使用

为什么用while循环,因为要把队列的睡眠进程全部唤醒 需要再听一遍
全部唤醒,优先级高的先执行,有schedule决定,而不是普通的信号量一个一个排队

死锁问题



死锁解决





内存

编译分段,找一段空闲内存(内存分区分页),磁盘写入并把LDT(页表)填写好
CPU取指执行,指令得存在内存中,通过偏移找到指令位置
编译时写死物理地址,载入的时候写死。空间有限进程需要置换到磁盘,再次出来的时候物理地址会变,所以在运行时找到合适的空闲物理地址保存起来(PCB)



程序分段

如果把整个文件当成一个整体去执行不灵活

可以区分出不同的段, 分成不同的段,执行时分别找出几段空闲的内存,并且把每个段的基地址记录在LDT这张表里
分段去执行,把每个段的基地址记下来赋值给PCB, 通过地址翻译,段内偏移找到指令位置
每个段的基地址存在LDT表中,就是之前说的映射表


内存分区和分页

分区


用两个表来记录剩余空间和已经使用的空间


分页

用户程序需要分段,物理内存需要分页

分区会产生内存碎片,把内存分成一片片有单位,这样浪费最多也是4k


代码分成多个段,段又打散成多个页写入(一个段可能会分配好几个页),MMU找到页号,记录页表信息给PCB

多级页表

页多了,页表的放置就有问题

第一种尝试把没用到的页表项删除
知道逻辑地址找物理地址,如果去掉没有用的页表项就会不连续了导致遇到查找内存 (得需要连续)

第二种尝试章加上节去找 (分级)
把内存当成一本书(把它划分了),分为章节通过章去找节,保证了他连续,又不用什么内存。
只是增加一层就会多一次访问内存 (为了解决这个加一层缓存, 因为程序具有局部性,所以缓存好使)


虚拟内存 (段页结合)



逻辑地址先找LDT表(段表)通过虚拟内存地址翻译在页表中找到页号和物理地址

举个例子

虚拟内存每个进程分64M大小


第一步建立段表,nr就是第几个进程,算出虚拟基地址

第二步找出空闲页

第三步建立页表

因为是fork子进程和父进程共享了一级页表(目录),写时分离。

实现虚拟内存需要换入换出

内存换入,内存相当于门店,磁盘相当于仓库,换入换出实现大内存


当发生缺页的时候,发出缺页中断把需要的页从内存找出放入内存


因为内存是有限的,当找到需要的页时需要把一些页置换到内存中

第一种置换出去的策略,先入先出

第二种时MIN ,每次需要置换的时候把未来使用的页的最后一个页置换出去

第三种,因为第二种要预测无法做到,所以用过去来预测未来。 LRU看似可以当是实际实现中因为要维护变量或者栈,开销大


第四种clock算法,类似LRU思想,二次机会最近被用过的页置为1,指针转过时置为0,没被用过的为了0,指针遇到了就置换出去
只有一个指针旋转不够,缺页的状况很少所以页都会置为1,就会退化成FIFO

分配多少个页框给进程,进程有局部性,可以通过工作集求出局部

文件视图

IO与显示器

cpu给外设(显卡网卡)发出命令写到外设的寄存器中,外设驱动执行
用外设的三件事(提供文件视图统一使用,发out指令,处理中断)




建立联系,通过PCB找到inode



IO与键盘



磁盘


最原始的通过柱面,磁头,扇区找到对应的物理位置

第一层抽象。移动磁头去寻道很浪费时间,所以block相邻的可以快速读出。


多个进程使用磁盘需要等待,调度的算法(电梯算法)
先进先出磁头移动厉害

找近距离的,如果来的请求都是近距离的容易饥饿

先把需要上去的人先到顶楼,在慢慢送下来

多进程使用磁盘


从盘块号变成字符流,第二层抽象

不适合动态增长

多次查询,消耗内存


通过文件使用磁盘



通过inode信息区分不同的文件设备, 文件系统为用户提供了一切皆文件

目录 --- 抽象整个磁盘





open函数怎么去获取到的inode,从根节点往下找,fork继承根目录
inode数组和数据区读入内存找找到对应文件的inode



读写磁盘的次数

posted @ 2022-01-04 16:20  小润_c++  阅读(784)  评论(0)    收藏  举报