操作系统基础
-
什么是操作系统?
- 是管理计算机硬件和软件的资源的程序,是计算机系统的内核与基石
- 本质上是计算机软件程序
- 为用户提供了与系统交互的操作界面
- 操作系统分为内核和外壳,外壳主要是运行在系统上的其他的应用程序,内核则包括管理与配置内存,决定系统资源的供需的优先次序,控制输入与输出设备,操作网络与管理文件系统等基本事务。总结就是进程管理,内存管理,文件管理,设备管理。
-
什么系统调用?能不能详细介绍一下。
先来介绍一下用户态和系统态。
- 用户态:用户态运行的进程或可以直接读取用户程序中的数据。
- 核心态:可以简单的理解系统态运行的进程可以访问计算机的任何资源不受限制,进程控制,文件操作,设备操作等
系统调用
- 我们运行的程序都是在用户态中的,但是我们想要调用操作系统提供的系统态级别的子功能,那就需要系统调用
- 系统调用按功能可分为以下几类:设备管理,文件管理,进程控制,进程通信,内存管理等
-
操作系统的基本特征
-
并发
并发是指宏观上在一段时间内能运行多个程序,但是微观上是对cpu的时间片进行串行的占用。并行是同一时刻内能运行多个指令。并行需要硬件支持。线程和进程的引入,使得程序能够并发的运行。
-
共享
系统的资源可以被多个并发进程共同使用,包括互斥的共享和同时共享。互斥共享的资源称为临界区资源。
-
虚拟
虚拟是将一个物理上的实体转化为多个逻辑实体。包括时分复用和空分复用。多个进程的并发就是使用的了时分复用,让每个进程轮流占用处理器。虚拟内存则使用了空分复用,它将物理内存抽象为地址空间,每个进程都有各自的地址空间,地址空间的页被映射到了物理内存,地址空间上页并不需要全部在物理内存中,当使用一个没有在地址空间上的页时则需要使用页面置换算法,将页面置换到内存中。
-
异步
异步指进程不是一次性持续完毕额度,而是走走停停,以不可知的速度向前推进的。
-
-
并发,并行,同步,异步的区别?
- 并发:在一个时间段中同时有多个程序运行,但是实际上只有一个程序在CPU上运行,宏观上的并发是通过不断的切换实现。
- 并行:需要多cpu等硬件支持,在同一个时刻,多个程序无论是宏观上还是微观上都是并行的。
- 异步:同步是顺序执行,异步是在等待某个资源的时候做自己的事,走走停停,以不可知的速度向前推进。
-
中断的分类
-
外中断
例如i/o中断,时钟中断等
-
异常
由cpu执行指令的内部事件引起的,如地址越界,算术溢出
-
陷入
在用户程序中使用系统调用
-
进程和线程
-
进程的概念和作用
进程是具有独立功能的程序在一个数据集合上运行的过程,进程可以更好的描述和控制程序的并发执行。从而实现操作的并发性和共享性。进程是通过进程控制块PCB来进行创建和销毁的。
进程是对运行时的程序的封装,是系统进行资源分配和调度的基本单位,实现了系统的并发,优点:内存隔离,单个进程的崩溃不会引起整个系统的崩溃。缺点:创建和销毁比较麻烦,进程间数据的共享比较麻烦。
-
线程的概念
可以理解为轻量级进程,是一个基本的cpu执行单元,也是程序执行单元,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,线程只有就绪,阻塞,运行三种基本状态。引入线程后,进程只作为除cpu以外系统资源的分配单元。
是进程的子任务,是cpu进行调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发。一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在
-
进程和线程之间的区别
- 资源:进程是资源分配和调度的基本单位,但是线程不拥有资源,线程可以访问所属进程的资源。
- 调度:线程是cpu调度和分派的基本单位,在同一个进程的线程切换会不会引起进程的切换,不同进程中的线程切换会引起进程的切换。
- 系统开销:由于创建和销毁进程时,系统都要为之分配或回收资源,如内存空间,I/O设备等,所付出的开销大于创建或撤销线程所付出的开销。类似的,在进行线程的切换时,涉及当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需要保存和设置少量的寄存器内存,开销很小
- 通信方面:线程间可以通过直接读写同一个进程中的数据进行通信,进程的话则需要借助IPC。
-
同一个进程下的线程可以共享哪些数据
- 进程的代码段和进程的公有数据(全局变量,静态变量)
- 进程打开的文件描述符,进程的当前目录以及信号处理器/信号处理函数,进程ID和进程组ID
-
进程可以独占哪些资源
- 线程ID,一组寄存器的值,线程自身的栈。
-
进程间的通信方式
本质上:每个进程拥有自己独立的地址空间,任何一个进程的变量其他进程都无法接触,所以进程进行数据交换必须通过内核,在内存中开辟缓存区,然后通信的进程轮流去缓存区把数据放入和取走。内核提供的这种机制称为进程间通信(IPC)
- 管道/匿名管道:半双工的,数据只能向一个方向流动,需要双方通信时,建立二个管道,只能父子或兄弟进程之间,管道的实质是一个内存缓冲区,结构类似于队列。局限:只支持单项数据流,只能用于具有亲缘关系的进程之间,管道的缓冲区是有限的。
- 有名管道(FIFO):克服匿名管道没有名称,只能在亲属进程之间进行通信的缺点。是以有名管道的文件形式存在于文件系统中。
- 信号(Signal):常用于Linux,信号可以在任何时候发给某一个进程,无需知道该进程的状态,
- 消息队列(Message):是消息的链表,具有特定的格式,存放在内存中并由消息队列标示符标识。消息队列存放在内核中,可以实现消息的随机查询,比FIFO有优势。克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺陷。
- 信号量:是一个计数器,用于多个进程对共享数据的访问,意图在于进程间同步,主要解决与同步相关的问题并避免竞争条件。
- 共享内存:多个进程访问同一块内存空间。但是需要依靠某种同步操作,如互斥锁和信号量等,可以说这是最有用的进程间通信方式。
- 套接字(Socket):主要用于客户端和服务器之间通过网络进行通信,是支持TCP/IP的网络通信的基本操作单元。可以看着不同主机进程进行双向通信的端点,简单的说就是通信的双方的一种约定,用套接字中的相关函数来完成通信过程。
最快的方式是共享内存,采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。
-
进程哪几种状态?
- 就绪:进程已经获得了除了cpu以外的所有资源,等待分配处理器资源
- 运行:占用处理器进行运行的资源。
- 阻塞:进程等待某种条件,在条件满足之前无法执行
-
线程有哪几种状态?
在java虚拟机中,线程从最初的创建到最终的消亡,要经历若干个状态,创建(new),就绪(runnable/start),运行(running),阻塞(blocked),等待(waiting),时间等待(time waiting)和消亡(dead)。每个线程只能处于一种状态。
9. 多线程和多进程的使用场景
- 需要频繁创建和销毁的场景下使用多线程,进程的创建和销毁代价太大,
- 需要大量计算,频繁切换时,还有耗时的操作使用线程可以提高性能,提高程序的响应。线程切换速度比较快
- 因为对cpu的使用效率更高。多机分布用进程,多核分布用线程。
- 需要更安全稳定时,适合选择进程,需要速度时,选择线程更好。
-
进程同步
进程的同步是目的,进程间通信是实现同步的手段
-
临界区:对临界资源进行访问的那段代码称为临界区,为了互斥的访问资源,每个进程在进入临界区之前,需要先进行检查。
-
同步与互斥:多个进程因为合作关系产生的直接制约关系,是的进程有一定的先后持续关系叫同步。互斥,指同一个时刻只能有一个进程能进入临界区
-
信号量:整型变量,常见的P和V操作。P:信号量大于0,持续P操作-1。等于0,持续P则进入睡眠,等待信号量大于0.V:对信号量+1,唤醒睡眠的进程可以进行P操作。
P和V是原语,不可分割,持续原语的时候屏蔽中断,如果信号量只能取0和1,则成为了互斥量(Mutex),0表示临界区加锁。1表示临界区解锁。
生产者消费者问题
int mutex = 1; int empty = N; int full = 0; void producer(){ while(True){ P(empty) P(mutex)//使用互斥量控制对缓冲区的互斥访问 produce V(mutex) V(full) } } void consumer(){ while(True){ P(full) P(mutex) consumer V(mutex) V(empty) } }
-
管程Monitor
在一个时刻只能有一个进程使用管程,进程在无法继续执行的时候不能一直占用管程。否则其它进程永远不能使用管程。
-
-
线程间的同步的方式
线程同步主要指多个共享关键资源的线程的并发执行,应该同步线程以避免关键的资源使用冲突。
- 互斥量(Mutex):采用互斥对象的机制来保证拥有共享资源的线程只能有一个,只有拥有互斥对象才能对资源进行访问,比如Java中的Synchronized关键字和各种Lock都是这种机制
- 信号量(Semphares):它允许同一个时刻多个线程访问资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 事件(Event):Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
-
什么是协程,以及与线程的区别
协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存在其他位置,等到切回来时,恢复先前保存的寄存器上下文和栈,直接操作栈基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
- 一个进程可以拥有多个协程,一个线程也可以拥有多个协程,这样python中则能使用多核CPU
- 线程进程都是同步机制,协程是异步机制
- 协程能保存上一次调用的状态,每次过程重入时,就相当于进入了上一次调用的状态
-
什么是僵尸进程,什么是孤儿进程
一个子进程结束以后,父进程没有等待它,也就是父进程没有对它进行善后处理导致其变为一个僵尸进程。僵尸是已经死亡的进程,但是并没有真正的被销毁。它没有内存空间,没有可执行代码,也不能被调度。如果不进行处理,可能会一直留在系统中直到系统重启。危害:占用进程号,系统所能用到的进程号有限。
父进程结束了,子进程还存在,那么这些子进程将成为孤儿进程,被init接管,但这些孤儿进程结束了由init完成收尾工作。
-
进程调度策略有哪些?
批处理系统
-
想来先服务(FCFS)
按照进程请求的顺序进行调度,非抢占式,开销小,无饥饿问题,响应时间不确定(可能很慢),对短进程不利,对IO密集型不利。
-
最短作业优先(SJF)
按照估计运行时间最短的顺序进行调度,非抢占式,吞吐量大,开销可能较大,会导致饥饿问题,对于短进程有利,对于长进程不利
-
最短剩余时间优先(SRTN)
按剩余运行时间的顺序进行调度。最短作业时间进程抢占式版本,吞吐量大,会导致饥饿问题,对长进程不利。
-
最高响应比优先(HRRN)
同时考虑等待时间和执行时间长短,很好的平衡了长短进程,非抢占,吞吐量高,开销可能较大。好的响应时间无饥饿问题。
交互式系统
-
时间片轮转
将就绪的进程按照FCFS排成队列,用完时间片的进程放到队列的末尾,抢占式(时间片用完的),开销小,无饥饿。时间片小,切换频繁,时间片大,实时性得不到保障。
-
优先级调度算法
按照进程的优先级进行调度,为了防止低优先级的进程永远等不到调度,可以随时间的推移增加等待进程的优先级。
-
死锁
-
死锁产生的必要条件
- 互斥:每个资源要么已经分配给了一个进程,要么是可用的。
- 保持和请求:每个进程在保持一定资源的同时,可以申请其他新的资源。
- 不可剥夺:已经分配的资源不能被强制的剥夺,只能由这个进程自己显示的释放。
- 环路等待:有二个或者二个以上的进程围成一个环路,每个进程都在等待环路中下一个进程所占有的资源。
-
处理方法
- 鸵鸟政策:因为处理死锁的系统花销很大,且死锁发生的概率很低,所以很多系统在发生死锁时通常不去处理它而是忽略它。
- 死锁检测和死锁恢复:通常指不去阻碍死锁的发生,而是在死锁发生时检测到它,并采取措施进行恢复。
- 死锁预防:在程序运行之前预防死锁的发生。
- 死锁避免:在程序运行时不让程序进入不安全状态从而避免死锁。
-
死锁预防
破坏死锁发生的四大必要条件
- 破坏互斥条件:例如将打印这种互斥资源变为共享资源,这显然是无法实现和不合理的。
- 破坏保持和请求:一种实现方式是一个进程在请求资源时一次性将所以资源获取到。这个进程保持的一部分资源要很久才能用的到,降低了系统的性能。
- 破坏不能剥夺条件:这显然也是不合理的。
- 破坏环路等待条件:给资源统一编号,进程只能按编号顺序来请求资源。
-
死锁避免:
在进程请求资源时,计算这种分配方式是否会导致系统进入不安全状态,如果是的话就不去分配这个资源。
- 安全状态:如果当前没有死锁放生,并且如果所以进程突然对资源的最大需求,仍然存在某种调度次序能使每一个进程运行完毕,则称这种状态为安全状态。
- 通常会使用银行家算法检测是否会进入不安全状态。如果一个状态不安全则拒绝进入。
-
死锁检测和死锁解除
在死锁发生时检测到并对死锁进行解除
- 死锁解除的常用方法是进程终止和资源抢占。进程终止的方式可以是对所以进程进行终止或者逐一终止至不产生死锁为止。资源抢夺则是从一个多个进程中进行资源抢夺,选择一个牺牲品或者回滚到安全状态。
- 死锁检测通过检测有向图是否为环等。
内存管理
-
操作系统的内存管理主要是用来做什么
主要负责内存的分配和回收,另外还负责地址转换也就是将逻辑地址转换为物理地址
-
常见的内存管理机制
- 连续分配管理方式:指一个用户程序分配在连续的内存空间,常见有块式管理
- 块式管理:将内存分为几个固定大小的块,每个程序分配一个块,程序小则块内碎片大。
- 非连续分配管理方式:允许程序使用内存分布在离散或者说不相邻的内存中,常见的有页式管理和段式管理。
- 页式管理:内存分为大小相等的页的形式,减少了页内碎片,页式管理通过页表对应逻辑地址和物理地址。
- 段式管理:内存根据程序的逻辑信息进行分段,
- 段页式管理:内存先分段再在段内分页。
- 连续分配管理方式:指一个用户程序分配在连续的内存空间,常见有块式管理
-
快表和多级页表
- 解决的问题:虚拟地址到物理地址的转换要快。解决虚拟地址过大导致页表也很大的问题。
- 快表
- 使用页表作地址转换时,cpu需要访问二次内存。有了快表,快表指将页表的一部分或者全部内容放入Cache,那么在做地址映射时只需要访问一次内存,一次Cache即可,速度更快,快表的使用和我们的缓存很像,操作系统中的很多算法在日常开发中都会遇到。
- 多级页表
- 将一次查表转化为多次查表,时间换空间的做法。
-
虚拟内存,逻辑地址以及物理地址
- 虚拟内存的目的是为了让物理内存扩产成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存允许程序不用将地址空间的每一页都映射到物理内存中,当程序引用到不在物理内存的页时,使用页面置换算法将缺失的页调换到物理内存中。虚拟内存就是一种时间换空间的策略,用CPU的计算时间和页的调入和调出的花费时间,来换取更大的虚拟内存来支持程序的运行。程序的世界中不是时间换空间就是空间换时间。
- 虚拟内存的局部性原理:时间局部性指最近使用的指令或数据不久以后可能被再次访问,因为程序中存在这大量循环。空间局部性指最近访问过的储存单元周围的页面在将来可能会被访问,主要是因为指令和数据都是聚簇储存的。
- 时间局部性和空间局部性可以通过使用缓存来实现,虚拟内存技术实际上建立了“内存-外存”的二级储存器的结构。利于局部性原理的实现。
- 我们程序地址空间中的地址都是逻辑地址,逻辑地址由操作系统决定,物理地址指的物理内存中的地址。通过内存管理单元管理将逻辑地址转化为物理地址。例如分页式的页表储存着页(程序的地址空间)和页框(物理内存空间)的映射表。
-
页面置换算法
在程序运行中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中,如果内存中无空闲空间,则需要将内存中的某一个页面调出到磁盘对换区中来腾出空间。把选择哪个页面作为淘汰页面的算法叫做页面置换算法。
页面置换算法和缓存淘汰策略类似,主要目的都是页面的置换频率低,也就是缺页率低。不好的页面置换算法的就会导致颠簸,也叫抖动,它置换一个页立刻再需要这个页。
- 最佳页面置换算法(OPT):理想情况下,不可能实现,指的是被换出的页面是最长时间内不再被访问的,但是我们一般无法预测未来的事情,所以这是一种理想情况。
- 先进先出页面置换算法(FIFO):总是淘汰最先进入内存的,也就是在内存中时间最久的页面。
- 最近最久没使用页面置换算法(LRU):无法知道未来页面的使用情况,但是可以知道过去使用页面的情况,LRU将最近最久未使用的页面置换出来,算法赋予每个页面一个字段T,T记录了一个页面自上次被访问以来所经历的时间T,当淘汰页面时,选择现有页面中T值最大的。即最近最久未使用的。使用的场景最多。
- 最近未使用页面置换算法(NRU):每个页面都维护二个状态位:R和M,当页面被访问时页面设R=1,当页面被修改时设置M=1,R定时清零。淘汰页面时优先换出已经被修改的脏页面(R=0,M=1),而不是频繁访问的干净页面(R=1,M=0)
-
分页和分段的区别?
- 分页式储存管理:用户空间划分大小相等的部分称为页(page),内存空间划分为大小相等的区域称为页框,分配时以页为单位,按进程需要的页数进行分配,逻辑上相邻的页物理上不一定相邻。
- 分段式储存管理:用户空间按照自身的逻辑关系划分为若干段(如代码段,数据段),内存中被动态划分为长度不同的区域,分配时以段为单位,每段在内存中占据连续空间,各个段在内存中不连续。
- 段页式储存管理:用户空间中先按段划分,段内再按页划分。内存空间中按照页进行划分和分配。
- 请求分页式储存管理和分页式储存管理的区别:请求是不将页面一次性放入内存,可以提供虚拟内存。分页式是将页面全部放入内存中。
区别
- 目的不同:分页的目的是管理内存,用于虚拟内存以获得更大的地址空间;分段的目的是满足用户的需求,使得程序和数据可以被划分为逻辑上独立的地址空间;
- 大小不同:段的大小固定,由程序的决定,所以对于程序员是可见的。页的大小固定,是由系统决定的,对于程序员是透明的。
- 分段利于信息的保护和共享,分页的共享受到限制。
- 碎片:分段没有内碎片,但会产生外碎片,分页没有外碎片,但会产生内碎片。
设备
-
读写一个磁盘块的时间的影响因素
- 旋转时间(主轴转动盘面,使磁头移动到响应的扇区)
- 寻道时间(使磁头移动响应的磁道上)
- 实际的数据传输时间
寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。
-
磁盘调度算法
- 先来先服务:按照磁盘的请求顺序进行调度。平均寻道时间可能过长。
- 最短寻道时间优先:优先调度与当前磁头最近的磁道。平均寻道时间比较低,但是不公平,有的请求等待时间过长。
- 电梯算法:保持一个方向运行,直到这个方向没有请求为止,然后改变方向。
编译原理相关
-
编译过程
以一个hello.c程序为例子,在unix系统上,由编译器把源文件转换为目标文件。
- 预处理阶段:处理以#开头的预处理命令
- 编译阶段:翻译成汇编文件
- 汇编阶段:将汇编文件翻译成可重定向目标文件
- 链接阶段:将可重定向目标文件和printf.o等单独预编译好的目标文件进行合并,得到最终的可执行的目标文件。
-
目标文件
- 可执行目标文件:可以直接在内存中执行的文件
- 可重定位目标文件:可于其他可重定位目标文件在链接阶段合并,创建一个可执行文件
- 共享目标文件:这是一种特殊的可重定向目标文件,可以在运行时被动态加载进内存并链接。
-
静态链接
以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出
-
动态链接
- 静态库更新时需要重新链接,对于printf这种标准函数库,如果每个程序都要有代码,这会是种浪费。
- 共享库为了解决静态库的这二个问题而设计的。Linux系统中常常为.so后缀。所有引用该库的可执行文件都共享这个文件,他不会被复制到引用它的可执行文件中。
IO多路复用
-
常见的五种IO模型
- 阻塞I/O模型,老李去火车站买票,排队三天买到了票。耗时:在火车等待三天
- 非阻塞I/O模型,老李买票,没有在火车站等,而是每隔12个小时去问一次,直到买到票。耗时:往返车站6次,路上6次。
- I/O复用模型:
- select/poll:老李买票,委托黄牛,然后每个6个小时打电话询问黄牛,黄牛三天买到票,然后老李去火车站交钱领票。耗时:打电话
- epoll: 老李去火车站买票,委托黄牛。黄牛买到了通知老李去领。耗时:无需打电话
- 信号驱动I/O模型:老李去买票,给售票员留下电话,有票了,售票员电话通知老李去取,耗时:无需打电话
- 异步I/O模型:老李去买票,给售票员留下电话,有票后,售票员送货到家。
-
I/O多路复用
- 一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。
- I/O多路复用的实现:用户将想要监视的文件描述符添加到select/poll/epoll函数中,由内核监视,函数阻塞,一旦描述符就绪(读就绪或写就绪),或者超时(设置timeout),函数就会返回,然后该进程可以进行相应的读写操作。
-
select/poll/epoll三者的区别
- selelct:将文件描述符放入一个集合中,调用select时,将这个集合从用户空间拷贝到内核空间中(缺点1:每次都要复制,开销大),由内核根据就绪状态修改该集合的内容,(缺点2)集合大小有限制,32位机默认时1024;采用水平触发机制,select函数返回后,需要通过,遍历这个集合,找到就绪的文件描述符(缺点3:轮询的方式效率太低),当文件描述符的数量增加时,效率会线性下降。
- poll:和select几乎没什么区别,区别在于文件描述符的储存方式不同,poll采用链表的方式储存,没有最大储存数量的限制
- epoll:通过内核和用户空间共享内存,避免了不断复制的问题,支持的同时连接上限很高;文件描述符就绪时,采用了回调机制,避免了轮询(回调函数将就绪的描述符添加到一个链表中,执行epoll_wait,返回这个链表),支持水平触发和边缘触发,采用了边缘触发机制,只有活跃的描述符才会触发回调函数。
区别主要在
- 一个线程/进程所能打开的最大连接数
- 文件描述符传递方式(是否复制)
- 水平触发or边缘触发
- 查询就绪的描述符时的效率(是否轮询)
-
什么时候用epoll,什么时间用select/poll
当连接数较多并且有很多的不活跃连接时,epoll的效率比其他二者高很多,但是当连接数比较少并且都十分不活跃的情况下,由于epoll需要很多回调,因此性能可能低于其他二者。
参考
- https://cyc2018.github.io/CS-Notes/#/
- https://github.com/wolverinn/Waking-Up
- https://github.com/Snailclimb/JavaGuide