操作系统热点
文章目录
参加过几场面试后发现,什么狗屁的流行技术;基础才是王道;是能伴随程序员终生的财富;所以有必要对热门的操作系统进行整理,方便理解和复习
进程与线程
区别
背下这句话:进程是操作系统资源分配的最小单位,线程是执行任务调度的基本单位;一个是分配;一个是执行;
每个进程都最起码有一个主线程,由这个主线程去执行代码段或者是创建子线程去执行其他代码段;但是在他们执行的时候需要分配变量或者获取变量,这些东西在哪呢?这些交给进程管理,线程共享这些信息;
通信
首先就是:为什么需要通信?—
通信的目的:
- 数据传输:一个 进程需要将它的数据 发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程 发送消息,通知它(它们)发生了 某种事件( 如进程终止时要通知父进程)。(kill - 9 pid)
- 资源共享:多个进程之间 共享同样的资源 。为了做到这一点,需要内核提供互斥和同步机制。
- 进程控制:有些进程 希望完全控制另一个进程的执行 (如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
通信的方式:
调度
无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。
-
先进先出
-
短进程
-
轮转法
-
多级反馈队列
死锁
什么是死锁就没要介绍了,主要讲一下造成死锁的几个条件
死锁的四个条件:
- 互斥:争抢的资源是互斥访问的,只允许一个线程或进程进行访问,如(打印机);
- 占有且等待:这个也比较好理解,占用一份互斥资源,等待另一份互斥资源;
- 不可抢占:不可以抢别人的,自己的也不会被别人抢!陷入僵局
- 循环等待:你等我释放,我等你释放;你不放我也没办法放;
如何预防
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
- 破坏占有且等待:也就是不需要等待,你直接把我需要的都给我不就行了;在一开始的时候就分配资源;饿汉式!
- 破坏不可抢占:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。 - 破坏循环等待:一开始的时候就将所有资源进行编号;进行升序排列起来;线程申请资源i的时候;下次申请只能申请大于i的;那就所有的申请都往一个方向走;必然不会造成循环等待! 但是为了避免产生循环等待,某些申请会被拒绝,这样就降低了资源的利用率
如何避免
银行家算法: 银行家算法通过对进程需求、占有和系统拥有资源的实时统计,确保系统在分配给进程资源不会造成死锁才会给与分配。
内存管理相关
物理内存:
字面意思:内存条;
内存管理的几种方式:
- 块式存储:远古时期;将内存划分为几个固定的块,用于放程序代码段去执行,一但进程数量上来了;就会导致分配不均问题,出现很多内存碎片;无法利用,资源浪费极大!
- 页式存储:不划分为块了,而是缩小管理,划分为更小的页:一个页通常4KB;这样就能避免产生大块的内存碎片,内存利用率很高
- 段式存储:将程序按照逻辑划分地址空间;比方说主函数放在一段;funcA 放在一段;形成互不干扰的程序段;执行的时候更加连续;也能达到复用的效果,多道程序都可以运行同一段代码而不需要重复分配空间!
- 段页式存储:中和了两种方式的优点,先分段,再分页;能避免内存碎片的同时,也能实现共享内存!
以上就是内存管理方面的知识点,当然这些都避不开一个很重要的技术:虚拟内存
首先,我们写的程序会写入到文件中保存,文件保存在磁盘;经过我们编译之后就会成为一个可执行文件;当我们想要运行这个可执行文件的时候;就会将它加载(load)到内存;CPU找到程序执行入口后便可愉快的执行了(进程);在远古时代的时候;进程直接使用实际物理地址;同时也可以随意修改物理地址;还可以该别的进程的地址信息;有很大的隐患!
为了防止这种情况,就需要一个中间人入场,来管理程序的内存空间!
虚拟内存
为每一个进程分配一个虚拟内存,虚拟内存存放的都是线性地址,
虚拟内存到物理内存之间需要映射;如何映射呢? 当然是通过页表;通过更改页表就能实现实际地址的更改,
页目录存放哪些信息呢?
由此可见,两级页表即可寻址4GB地址空间,完全够用!
我们能不能用一个地址就能表示三个页之间的信息呢?当然是可以的;虽然现代操作系统一般都是64位,但是好多程序都是运行在32位上,我们要表示一个地址的话,需要使用32位来表示;那我们就可以使用前10为来表示页目录位置,中间10位表示页表位置;后面12为可以表示数据在页中偏移量以及该位置的权限
这样就完成了用户程序虚拟内存到物理内存之间的映射了;
每个进程拥有自己的虚拟内存,以为着每个进程的线性地址可以相同,具体映射时候将会映射到不同的物理地址;
页表的功能:
一个页表4KB;一行4字节;也就是32位;这三十二位存储内容是什么呢?是不是需要存储:物理内存页号的起始;然而,物理内存都是按4kb每页已经划分好了,所以其实地址是:0、4 * 1024byte…,这就意味着,最低的12位永远是000000000000;我们是不是就可以充分利用这八位信息;
虚拟内存分配的过程
当一个进程想要一些空间来运行自己的程序时;就会向OS发出申请;说我想要【200,400】这个区间的地址,你给我分配一下;OS就会去查一下这个进程对应的VMA链表;看看是否已经分配过了;没有的话就分配并加入到链表中;除了链表也有可能是红黑树;
VMA( virtual memory areas ): 一个vma就是一块连续的线性地址空间的抽象,它拥有自身的权限(可读,可写,可执行等等) ,每一个虚拟内存区域都由一个相关的struct vm_area_struct结构来描述 0
分配的过程是懒加载的,进程靠OS分配到了虚拟内存之后,并不会马上进行物理地址的映射,因为开销很大;而是在使用的时候发现页表记录的也还没有完成映射时,就会发生page fault;会回到Vma链表中查一下该进程是否有这个页空间;有就进行物理地址的映射;没有就会发生内存访问异常!
CPU使用虚拟内存:
程序的执行时靠CPU来完成的,所以说CPU使用的也是进程的虚拟地址;虚拟地址是不可以直接使用的,所以需要一个中间人开解析一下,将线性地址转换为物理地址;具体过程如下:
CPU拿到线性地址—交给MMU进行解析-----MMU从进程信息中拿到进程对应的目录---------通过页目录找到页表-----通过页表找到物理页------将对应关系写入块表(TLB)缓存起来;下次先查缓存
因为有缓存,程序在执行一段时间后就会稳定;因为寻址变快了!
但是!如果进程切换了;TLB也就清空了;再回来的话又得重建缓存;这就是为什么进程切换代价比价高的原因之一
用户态、内核态
概述
看完上面的内容相信你已经明白:每个进程都有自己的虚拟地址空间;
但是!为了系统安全,不得不将这个虚拟空间划分为用户空间和内核空间!操作系统运行在内核空间;用户程序运行在用户空间;
内核空间也得映射到物理内存;值得注意的是:这个地址是线程共享的一块物理地址;也就是说;不同的进程内核空间映射到同一块物理地址空间;
内核空间的进程同样保存了进程控制信息;
用户在用户空间调用用户空间的代码段是完全没问题不受阻的;但是如果调用了操作系统的api,是不能够直接被调用的;需要触发用户态—内核态的切换过程;切换之前先保留现场到线程对应的PCB,方便切换回来的时候进行恢复现场;
切换到内核空间后,内核也需要地址空间来存放内核运行的变量信息之类的;所以一个进程在用户空间的地址空间对应在内核空间也有一份;用来存放线程切换到内核运行时的一些重要信息;
为什么内核空间需要对应的地址空间?
有人可能会想,处于内核态的线程权限是很高的,可以调用任何代码;为什么不直接调用用户空间的代码不就行了;为什么还要在内核空间划分一块来运行呢?----主要是如果线程使用用户空间的地址的话,数据是会被用户空间的其他线程访问并修改的;这样就不安全了;所以需要在内核空间进行程序操作!
线程的分配过程:
我们都知道,线程从属于进程;什么意思呢?操作系统分配资源的时候是分配给进程的;所谓的资源就是虚拟地址;进程创建的线程都只能使用这个虚拟地址;这就是为什么说线程共享进程的资源的原因!
那线程是如何被创建并分配空间的呢?
一般来说,程序的执行是靠具体的线程来执行的;也就是说一个进程最起码有一个线程;我们称之为主线程;一般是有父进程或者操作系统来创建的;那么如果CPU要执行一个线程是不是需要程序入口;是不是需要栈空间;用来保存线程运行时的上下文信息;函数调用的过程就是不断压栈出栈的过程;在为线程分配栈空间的时候,同时会在内核空间分配一段栈空间;处于什么空间就使用什么空间的栈空间,这样就能做到隔离效果与安全性!这就是所谓的用户栈和内核栈;
进程有控制信息;那线程呢?
当然也有啦;操作系统会记录每一个线程的控制信息:TCB;然后进程控制信息里面会有一个指针指向TCB,再次验证了进程从属于进程;通过进程就可以查询出该进程下的所有线程信息!那么TCB作用是啥呢?当然是保存线程执行的信息了:执行入口、用户栈、内核栈;同一个进程内的线程共享进程的地址空间和句柄表信息!
线程的执行
如图,在CPU执行线程的时候,指令指针指向线程的执行入口,栈基和栈指针执行用户栈;如果当前是内核态,就会指向内核栈;
系统调用
我们都知道,一台裸机是跑不起来的,需要装系统才能运行;这是为什么呢?
很明显,一台裸机都是硬件设备;我们需要一个中间人来管理这些硬件设备;比如CPU、显卡、打印机等等;这就是操作系统;操作系统通过管理统一这些硬件然后再将这些硬件的使用接口暴露出去供我们高级语言调用;如果我们想要使用操作系统提供的接口的话,我们就不得不了解一个概念:系统调用
顾名思义嘛,系统调用;只有系统才能调用;系统运行在哪?----内核空间;所以我们如果要使用到系统的接口;就必须得切换到内核态;才能进行调用!那么问题来了;程序怎么知道我要系统调用呢?这就不得不引入一个新的概念:中断
中断:字面意思;中断当前程序从而执行中断程序;中断分为两种;
- 硬中断:由硬件发起的中断;CPU有一个引脚专门处理中断信号;当有硬件设备发送中断信号是就会触发硬件中断;
- 软中断:指在程序中通过指令来执行的中断;如大名鼎鼎的80中断!
所以;当我们程序设计到系统掉用的时候;都会加入一行中断指令;指示CPU需要调用系统函数来 满足当前程序的使用;这时候我们内核空间分配的内核栈就派上用场了!
过程:
- 指令指针执行到中断指令;
- 将当前线程的信息保存到线程的线程控制块中;
- 栈基和栈指针指向内核栈;
- 开始执行;调用系统函数;
- 执行完毕,切换回用户栈;
- 利用线程控制块中的信息恢复现场,继续执行!
其中还有一个重要问题:CPU怎么知道执行哪个函数?
首先,中断指令对应的处理程序的对应关系会被操作系统作为一张表维护在内存中;所以当触发指令中断时,根据指令信息就可以查询出来是那个中断处理程序;也就是中断向量表
但是!操作系统有那么多个系统函数;难倒要为每一个函数都分配一个中断编号嘛?这显然是不符合设计思想的;如何解决呢;
其实很简单;只用一个中断编号来表示;但是这个中断编号所对应的处理程序是一个派发程序;由它来决定派发给哪个编号的函数来执行!
这里的函数编号将会被存放在cpu中的一个函数调用寄存器中!这样就能完美解决以上问题!
关系如下图所示!
以上截图资源来自