操作系统学习
0.系统调用,用户态,内核态
cpu用户态下只能执行一些指令访问有限空间的内存地址,因此需要切换到内核态,内核态可以执行所有指令,访问任意的内存地址,拥有最高的权限
用户态到内核态的唯一方法:中断(系统调用就是一种中断)。
中断又分内中断和外中断,内中断是指中断信号由CPU发出与当前执行的指令有关,外中断是指中断信号不由CPU发出与当前执行的指令无关(比如IO设备完成后,人工结束进程)
通俗的说系统调用就是提升了我们CPU此刻的等级,能够执行内核函数
1.什么是进程?
进程就是正在执行的程序,进程由PCB进行记录当前进程的状态(进程内的各个变量,当前进程执行的位置等)
2.多进程之间是如何组织的?
操作系统内有一个就绪队列存储已经就绪等待执行的进程,一个磁盘等待对立存储等待比如磁盘IO的进程,多进程之间的组织其实就是进程状态间的切换
3.比如就绪对列中哪个进程应该被执行?
跟调度算法有关
4.多进程之间的相互影响
考虑到多进程如果共用一个内存空间的话,由于每种原因比如可能写错内存地址,导致我们A进程修改了B进程里的数据。
因此我们可以使用内存映射表(LDT,详见21.分段存储),不同的进程拥有各自独立的内存映射表,即使在进程中是相同的内存地址也会被映射到不同的物理内存中
5.什么是线程
由于内存映射表的存在,使得进程切换花费很大,因此我们可以在进程中划分出线程的概念即一个进程有多个线程,线程实际就是只进行PC(程序计数器)的切换 不进行内存映射表的切换,因此切换代价小于进程。也就是说同一进程的多个线程共享内存映射表
进程是资源分配的最小单位,线程是CPU调度的最小单位
6.TCB
线程有自己的TCB,thread control block, 只负责这条流程的信息,包括PC程序计数器,SP堆栈,State状态,和寄存器。有不同的控制流,需要不同的寄存器来表示控制流的执行状态,每个线程有独立的这些信息,但共享一个资源。
7.用户级线程和内核级线程
内核级线程,顾名思义,它的调度是依赖于操作系统的,即操作系统控制着内核级线程的切换,比如有A和B两个内核级线程,我们用户是不知道先执行哪个线程的代码和不知道什么时候切换到另一个线程执行代码的,这件事只有操作系统知道,我们无法干预。
用户级线程,顾名思义,它的调度是依赖于用户的想法的,比如有C和D两个用户级线程,我们用户可以先让A执行一段代码后,然后手动控制让其跳到B去执行一段代码,我们是清楚知道线程间的切换的。
简单一句话来说就是:内核级线程是由操作系统进行调度的,用户级线程是由用户来控制调度的。
8.用户级线程切换
https://blog.csdn.net/qq_31601743/article/details/97514081
我们举例子,来进一步说明用户级线程切换的底层原理,还是记住那句话:用户级线程的切换是由我们用户来主动控制的。
现在我们假设有线程1和线程2两个线程(图中红色的数字为内存的地址)
可以看出,线程1中有A()和B()两个函数,执行流程为A()函数调用B()函数,B()函数执行完毕后返回到地址为104的语句继续往下执行;线程2中有C()和D()两个函数,执行流程为C()函数调用D()函数,D()函数执行完毕后返回到地址为304的语句继续往下执行。那么图中还有一个Yield()函数到底是什么东西呢,简单来说它就是我们用户主动来控制线程切换的一个函数,在线程1中调用Yield()函数,此时会切换到线程2,在线程2中调用了Yield()函数,此时又会回到线程1继续执行。因此,执行流程为下图所示。
现在我们更加深入地去剖析整个切换过程到底发生了什么有趣的事。
线程1运行:B() 为函数调用,此时将函数调用的下一条指令地址入栈,即104入栈(因为要记录函数调用结束后返回到哪里继续执行)。
随即进入B()内执行 ——> 线程切换(用户级线程)函数Yield()也为函数调用(特殊函数调用),即将要执行Yield(),则204入栈,随即执行Yield() ——> 跳至内存中地址为300的指令执行,此时即进行了线程切换。
类似,执行C() ——> 调用D(),304入栈,即将执行Yield(),404入栈。
随即执行线程2的Yield() ——> 切换至线程1执行204 ,之后B() 结束,弹栈,为404 ,即执行完后跳去地址为404处执行。而实际上我们开始说过,执行完B()后要按轨迹5返回线程1的地址为104处执行,而此时调用执行完进入线程2的404,出错。
某线程中的普通函数调用(Yield是特殊函数调用)只能在本线程来回折腾,所以上述情况的普通函数调用会返回到另外的线程执行,这是错误的。
出错原因是因为两个线程共用了一个栈,导致线程之间切换和内部运行出现错乱。
因此,可以用两个栈解决这个问题,即分别为每一个线程分配一个独立的栈!
还是上面的那个例子
此时的栈不在是上面的那仅有的一个栈,而是线程1有一个栈,线程2也有一个栈,其中它们各自的栈地址存放于各自线程的TCB中,还有一个全局的栈指示变量,它的作用是指示当前用的是哪一个线程的栈。
那么,线程1运行:B() 为函数调用,此时将函数调用的下一条指令地址入栈,即104入线程1中的栈,B()函数中执行Yield()函数,204入线程1中的栈,然后切换到线程2执行。
线程2执行,同理,304入线程2的栈,404入线程2的栈。
此时,两个栈的状态如下图
线程2执行Yield()函数后,全局栈指示变量指向线程1的栈,此时执行出栈操作,出的是线程1的栈,弹出204,转到204执行语句,接着B()函数调用完后,return,继续在线程1的栈中(因为全局栈指示变量依然指向线程1的栈)执行出栈操作,弹出104,转回A()函数中的104语句,此时,就完美地解决了使用一个栈会导致乱跳转的问题!
实际上就是找到线程的TCB,再由TCB找到对应的栈,就可以找到接下来要执行的指令地址!
9.并发与并行
并行是真实意义上的同时执行,并发实际只是交替执行宏观上表现为同时执行
10.内核级线程的切换
内核级线程拥有一套栈包括用户栈和内核栈,相互关联。
首先由用户栈进入内核栈,找到下一个线程的TCB,由TCB进入该线程的内核栈,再由内核栈找到用户栈
11.CPU调度算法需要考虑什么
响应时间(响应时间短意味着同一个任务直观上执行是连续的没有切换到其他任务),周转时间(任务从进入到完成的时间,包括等待CPU的时间),吞吐量(完成的任务量)
12.常见的CPU调度算法
FCFS(先来先服务),SJF(短作业优先),SRTN(最短剩余时间优先)(SJF抢占式版本,即当一个新就绪的进程比当前运行进程具有更短完成时间时,系统抢占当前进程,选择新就绪的进程执行。) RR(时间片轮转),最高优先级调度(选择优先级最高的进程投入运行,优先级可以是动态或者静态,按照优先级组织就绪队列), 多级反馈队列调度
13.如何缩短周转时间?
让短作业(需要时间短的)先完成(实际上就是让长作业等待CPU时间变短了)
13.如何缩短响应时间?
按时间片来轮转调度
14.多级反馈队列调度算法
- 设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,依次递减优先级。
- 对于各个队列进程执行时间片的大小也不同,优先级越高的队列,分配到的时间片越少。
- 当第一级队列为空时,再第二级队列进行调度,依次类推,各级队列按照时间片轮转方式进行调度。
- 当一个新进程创建后,首先把它放入第一队列的末尾。按照FCFS原则排队等待调度。当轮到该进程执行时,如它在该时间片完成,便可准备撤离系统,如果它在一个时间片结束时尚未完成,则调度程序便将该进程转入第二队列的末尾,再同样地按照FCFS原则等待调度执行。依次类推。
15.进程同步
进程同步就是指通过控制进程的运行和停止 来使进程按照指定顺序执行
16.信号量 semaphore
信号量就是一个整型变量,用来记录某种"资源" (比如有多少线程可以进入...)的剩余数目(跟信号不同,信号只能记录资源有无)(如果为负数表示有进程在等待)
17.临界区
一次只允许一个进程进入的那一段代码
18.临界区原则:
互斥进入,有空让进(当临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入临界区。),有限等待(进程在等待进入临界区访问临界资源时,应该限制这个等待的时间。)
19.临界区算法:
软件:面包店算法
硬件:我们只需要阻止CPU调度即可,CPU调度必须进入内核态,也就是阻止中断。使用cli(),sti()可以使CPU忽略中断标志位。(但是只能适用于单核CPU)
锁:实际是就是个信号量,但是这个信号量的修改是个原子指令
死锁:
死锁就是两个进程相互拥有对方需要的资源,又同时相互等待对方释放资源
死锁的条件:1.互斥 2.不可剥夺 3.请求和保持 4.循环等待
20.内存重定位:
进程需要放到内存中才能被cpu读取和执行,那么进程要放到内存的哪里才是合适的呢?
进程代码中会有具体语句表明代码的入口地址,但这个地址之只能是相对地址,因为任何时刻内存的状态都是不确定的,不能保证哪个物理地址一定是空闲的并且可以让用户使用的,但cpu只能根据指令找到对应的物理地址来执行程序,那么这个相对地址应该在什么时候变成物理地址呢?
1.编译时:程序在编译时就将相对地址转变为物理地址,这种情况比较少见,由于程序在运行时内存状态是不确定的,所以在编译时就重定位会容易出现问题
2.载入时:程序在载入内存时重定向,但是仍然会存在问题,多线程存在于内存中,由于内存空间有限,有时需要借助磁盘,swap行为就有可能出现(进程被长期阻塞时会从内存空间被移出):进程从磁盘重新写回内存时,原来的物理地址已经被新的进程占用,此时程序的物理地址还是原来的物理地址,就会指向这个“鸠占鹊巢”的新进程,而产生错误。
3.运行时:进程的物理地址是根据pcb中的基地址和程序的相对地址(理解成每条指令都有一个相对基地址的偏移地址)换算而成的,每一次执行一条指令,都要从逻辑地址算出物理地址,这样就不会出错啦~
21.分段存储
需要注意的是 我们不是将整个程序放入内存,而是分段处理(变量段,代码段,函数段,栈等等)(因为不同段的特点不一样,比如代码段可读,变量段可写,栈可以增长,分段方便我们更好的利用内存)。每个段有各自的基地址,上面我们讲的偏移,也是相对于段基地址的偏移。PCB中存储的也是该进程所有段的基地址(LDT表)
22.内存分区
采用可变分区还是固定分区? 可变分区,固定分区太不灵活了
如何管理可变分区? 思想就是记录内存已分配情况,以及空闲情况
如何分配空闲内存? 首先适配(匹配第一个 o(1)),最佳适配(优先选剩余空间和需要内存最接近的分区),最差适配(优先选剩余空间最大的分区)
实际上这种分段的方法是针对虚拟内存的,针对物理内存采用的方法是分页
23.为什么需要分页?
如果采用分区的方法我们会发现存在可能存在大量的内存碎片,造成内存的浪费。能不能将我们请求的内存从连续变成离散呢?
24.内存分页
分页其实就是把物理内存分成更小的单位页。
与段表类似,每个进程PCB也都拥有自己的页表,我们由逻辑地址根据页表找到真实物理地址(逻辑地址的前20位代表页地址,后12位是页内偏移量(因为页的大小是4K,所以用12位表示))
25.多级页表与快表
为了提高空间利用率,页应该尽可能小,但是页小的话页表就会变大。该怎么解决?
这就是多级页表的由来。实际就是参考书的目录,分为页目录表(章)和页表(节) (逻辑地址的前10位代表 页目录,10-20位代表页号)。对于没有使用的页目录,我们就不需要存储他的页表
页表节省了空间但是牺牲了时间!
这就引出了快表(TLB)。快表是个寄存器,可以快速查找(O(1))最近使用的页的物理地址
多级页表+快表! (先查快表,再查多级页表)
26.段页结合的内存管理
程序员希望用段,物理内存希望用页。
操作系统实现了将段叶结合的内存管理!
加入了一层虚拟内存,站在用户的角度,内存是分段的,站在硬件的角度,内存是分页的!
27.内存的换入
加入我们只有1G的物理内存,如何拥有4G的虚拟内存?
在用户眼中0-4G就是特别大的内存,相当于一个仓库,在仓库里有各种货,而物理内存相当于店面,店面小只能摆一些货,当有人需要货A,就从仓库里取(虚拟内存)放到柜台上(物理内存),有人需要货B,货A放回仓库,在从仓库里取货B放到柜台上,只要速度足够的快,柜台上就一直都有你想要的货。
即只有在访问的时候才建立虚拟内存和物理内存的映射
这就是请求调页!
1.当程序访问地址,看页表发现缺页,说明一段代码没有在页表中,不能取指执行,所以一缺页程序就不能执行下去,需要调页,此时进行中断,先执行其他的进程
2.产生了中断,就要执行中断处理程序,也就是页错误处理程序
3.页错误处理程序需要到磁盘中找到这一页程序,使用某个算法
4.找到之后需要在物理内存中找一个空闲页,需要把这一页从磁盘上读进来,这就是请求调页,换入,已经从磁盘上换入了
5.接下来把这个映射做好,对应的页表中,此时中断处理结束,
6.继续执行中断那个时候发生的指令,从用户的角度来看,好像什么事都没有发生过
内存换出
换出就是将物理地址重新映射到别的虚拟地址上
缺页:虚拟内存没有配物理内存
换出算法:
LRU(选最近一段时间最少使用页淘汰)
CLOCK(LRU的近似版本,不记录具体的使用时间,而是记录最近是否使用过)
生磁盘结构
磁盘I/O过程: 控制器 -> 寻道 (找磁道)-> 旋转(找扇区) -> 传输
盘块
盘块就是多个连续的磁盘分区,因为磁盘的读写速度的瓶颈在于寻道和旋转,因此我们希望能够使用连续的扇区。
盘块就是用空间来换时间。只不过现在的磁盘空间都很大,因此读写时间更重要些
磁盘调度算法
FCFS(先来先服务)
SSTF(最短寻道优先),优先让离当前磁道最近的请求访问者启动磁盘驱动器
SCAN(电梯算法),先沿着一个方向的SSTF,到头后再换个方向SSTF
C-SCAN,先沿着一个方向的SSTF,到头后直接移动到另一端,再沿该方向SSTF
从文件到生磁盘
从用户的角度,文件就是一串字符,我们需要将字符对应到磁盘的盘块中
连续结构实现文件:每个文件都有一个FCB记录了文件的起始块(知道起始和每个盘块的大小,我们就可以计算文件内任意字符所在的盘块)
链式结构实现文件:适用于不连续的文件,比如word,FCB记录了链表的开始块,顺着链查找即可
索引结构实现文件:有一个表存储了逻辑块与物理盘块的映射关系
多层索引结构实现文件:类似于多级页表