自己总结了下操作系统面试常考题,建议背诵
进程和线程和协程区别
主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程造成影响;线程只是一个进程的不同执行路径,线程有自己的堆栈和局部变量,但是没有独立的地址空间,一个线程死掉就是整个进程死掉。所以多进程的程序比多线程的程序健壮,但是进程切换时,耗费资源较大,效率差一些。对于一些需要同时进行又要共享某些变量的并发操作,只能用线程不能用进程。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
线程分类
名称 | 描述 |
---|---|
用户级线程(User-LevelThread, ULT) | 由应用程序所支持的线程实现, 内核意识不到用户级线程的实现 |
内核级线程(Kemel-LevelThread, KLT) | 内核级线程又称为内核支持的线程 |
内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用多核cpu。windows线程就是这样的。
用户级线程(轻量级):内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu,目前Linux pthread大体是这么做的。
区别:
- 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
- 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
- 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
- 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
用户态和核心态
内核态(Kernel Mode):运行操作系统程序
用户态(User Mode):运行用户程序
区别
内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态
当程序运行在0级特权级上时,就可以称之为运行在内核态。
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
这两种状态的主要差别是
处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的。
而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
用户态切换到核心态
系统调用(主动)
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。比如fork()实际上就是执行了一个创建新进程的系统调用。
异常(被动)
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
外围设备的中断(被动)
内存池、线程池、进程池
起因
由于在实际应用当中,分配内存、创建进程、线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作。
因此,当程序中需要频繁的进行内存申请释放,进程、线程创建销毁等操作时,通常会使用内存池、进程池、线程池技术来提升程序的性能。
内存池
内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取。
同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的真正内存释放。
进程池和线程池
先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作。
当工作完成后,线程又处于睡眠状态,而不是将线程销毁。
CPU调度
抢占及非抢占
(1)当一个进程从运行切换到等待状态(如:I/O请求,或者调用wait等待一个子进程的终止)
(2)当一个进程从运行状态切换到就绪状态(如:出现中断)
(3)当一个进程从等待状态切换到就绪状态(如:I/O完成)
(4)当一个进程终止时
调度只能发生在1,4为非抢占,否则为抢占。采用非抢占,一旦CPU分配给进程,其会一直运行到结束或者进入等待状态
FCFS 先到先服务算法
非抢占、平均等待时间长
SJF 最短作业优先算法
抢占/非抢占、平均等待时间最短
priority scheduling algorithm 优先级调度算法
抢占/非抢占,存在无限阻塞和饥饿问题
轮转法调度(round-robin,RR)
定义一个较小的时间单元,称为时间片(time quantum,or time slice)。将就绪队列作为循环队列
平均等待时间通常较长
进程通信
管道
普通管道PIPE
有限制,半双工:只能单向传输;只能在具有亲缘关系的进程间通信
有名管道
半双工,允许无亲缘关系进程间通信
流管道
可以双向传输,去除了第一种限制
系统IPC
信号量
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
消息队列
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
套接字
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
线程同步
临界区
在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
互斥量
只有拥有互斥对象的线程才有访问公共资源的权限,因此互斥对象只有一个,所以能保证公共资源不被多个线程访问。
信号量
允许多个线程在同一时刻访问同一资源,但是需要西安至同一时刻访问此资源的最大线程数目。
事件
通过通知操作的方式来保持线程的同步。
总结:
- 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
- 后三个都可以跨进程使用来同步数据操作
临界区
每个进程中访问临界资源的那段程序称为临界区,每次只准许一个进程进入临界区,进入后不允许其他进程进入。
互斥
如果进程Pi在临界区执行,那么其他进程都不能在临界区执行
前进
如果没有进程在其临界区内执行且有进程需要进入临界区,那么只有那些不在剩余区内执行的进程可以参与选择,以确定谁能下一个进入临界区,且这种选择不能无限推迟。
有限等待
从一个进程做出进入临界区的请求,知道该请求被允许,其他进程允许进入临界区的次数有上限
生产者消费者问题
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
semaphore mutex=1; //临界区互斥信号量
semaphore empty=n; //空闲缓冲区
semaphore full=0; //缓冲区初始化为空
producer () { //生产者进程
while(1){
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区.
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
}
}
consumer () { //消费者进程
while(1){
P(full); //获取满缓冲区单元
P(mutex); // 进入临界区
remove an item from buffer; //从缓冲区中取出数据
V (mutex); //离开临界区,释放互斥信号量
V (empty) ; //空缓冲区数加1
consume the item; //消费数据
}
}
死锁
发生条件
互斥
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。请求和保持
占有并等待
进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
非抢占
进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
循环等待
指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
死锁处理
死锁预防
死锁避免
- 银行家算法
1 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
2 顾客可以分期贷款,但贷款的总数不能超过最大需求量;
3 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
4 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.
缺点
1 这个算法要求客户数保持固定不变,这在多道程序系统中是难以做到的。
2 这个算法保证所有客户在有限的时间内得到满足,但实时客户要求快速响应,所以要考虑这个因素。
3 由于要寻找一个安全序列,实际上增加了系统的开销。
死锁检测和恢复
进程的特征和状态
特征
动态性、并发性、独立性、异步性
状态
- 就绪状态
当进程已经分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态为就绪状态。
- 执行状态
当进程已经获得处理机,其程序正在处理机上执行,此时的进程状态为执行状态。
- 阻塞状态
正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引发进程阻塞的事件有很多,例如:等待I/O,等待信号。
状态切换
- 就绪→执行
处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转为执行状态。
- 执行→就绪
处于执行状态的进程在其执行过程中,因分配给它的一个时间片用完而不得不让出处理机,于是进程从执行状态转为就绪状态。
- 执行→阻塞
正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态转为阻塞状态。
- 阻塞→就绪
处于阻塞状态的进程,若其等待的事件已经发生,便从阻塞状态转为就绪状态。
分页和分段和段页式
两者都采用离散分配方式,且都要通过地址映射机构来实现地址变换。
不同点:
页是信息的物理单位,用户透明,长度固定常见512B~8KB。分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率。可以说分页仅仅是由于系统管理的需要而不是用户的需要。
段是信息的逻辑单位,用户可见,长度可变。它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要。
页的大小固定且由系统确定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而一个系统只能有一种大小的页面。
段的长度却不固定,决定于用户所编写的程序,通常由编译程序根据信息的性质来划分。
分页的作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示一个地址;
分段的作业地址空间则是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。
连续内存分配
单一连续分配
内存在此方式下分为系统区和用户区,系统区仅提供给操作系统使用,通常在低地址部分;用户区是为用户提供的、除系统区之外的内存空间。这种方式无需进行内存保护。
这种方式的优点是简单、无外部碎片,可以釆用覆盖技术,不需要额外的技术支持。缺点是只能用于单用户、单任务的操作系统中,有内部碎片,存储器的利用率极低。固定分区分配
它将用户内存空间划分为若干个固定大小的区域,每个分区只装入一道作业。当有空闲分区时,便可以再从外存的后备作业队列中,选择适当大小的作业装入该分区,如此循环。
这种分区方式存在两个问题:一是程序可能太大而放不进任何一个分区中,这时用户不得不使用覆盖技术来使用内存空间;二是主存利用率低,当程序小于固定分区大小时,也占用了一个完整的内存分区空间,这样分区内部有空间浪费,这种现象称为内部碎片。
动态内存分配
动态分区分配又称为可变分区分配,是一种动态划分内存的分区方法。这种分区方法不预先将内存划分,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。
内存碎片问题
- 首次适应算法(顺序查找第一个分区)
- 最佳适应算法(最小的块)
- 最坏适应算法(最大的块)
- 临近适应算法(首次适应算法演变而成,从上次查找结束的位置查找)
静态链接和动态链接
- 静态链接
- 特点:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去。
- 优点:在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行。
- 缺点
- 程序体积会相对大一些。
- 如果静态库有更新的话,所有可执行文件都得重新链接才能用上新的静态库。
- 动态链接
- 特点: 在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。
- 优点:多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝
- 缺点:由于是运行时加载,可能会影响程序的前期执行性能。
- 库
- 静态链接库是
.lib
格式的文件,一般在工程的设置界面加入工程中,程序编译时会把lib
文件的代码加入你的程序中因此会增加代码大小,你的程序一运行lib
代码强制被装入你程序的运行空间,不能手动移除lib
代码。 - 动态链接库是程序运行时动态装入内存的模块,格式
*.dll
,在程序运行时可以随意加载和移除,节省内存空间。
- 静态链接库是
页面替换算法
最佳置换OPT
最佳置换算法是将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。但是这种算法可以作为衡量其它算法的基准。
先进先出FIFO
优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。该算法实现简单,只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。但该算法与进程实际运行时的规律不适应,因为在进程中,有的页面经常被访问。
最近最久未使用LRU
选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。该算法为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
belay和thrashing
Belady奇异现象,是指采用页面置换FIFO算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多,但缺页率反而提高的异常现象,这是一个违反直觉的现象。
原因是:FIFO算法的置换特征与进程访问内存的动态特征是矛盾的,即被置换的页面并不是进程不会访问的,因而FIFO并不是一个好的置换算法
Thrashing抖动现象,又叫颠簸。如果分配给进程的存储块数量小于进程所需要的最小值,进程的运行将很频繁地产生缺页中断,这种频率非常高的页面置换现象称为抖动。
原因是:进程的内存量不足。因而分配页面太少,总是缺页。
外存分配
- 连续分配
- 链式分配
- 索引分配
磁盘调度
- FCFS
- 先进先出的调度策略,这个策略具有公平的优点,因为每个请求都会得到处理,并且是按照接收到的顺序进行处理。
- SSTF(Shortest-seek-time First 最短寻道时间优先)
- 选择使磁头从当前位置开始移动最少的磁盘I/O请求,所以 SSTF 总是选择导致最小寻道时间的请求。
- 总是选择最小寻找时间并不能保证平均寻找时间最小,但是能提供比 FCFS 算法更好的性能,会存在饥饿现象。
- SCAN
- SSTF+中途不回折,每个请求都有处理机会。
- SCAN 要求磁头仅仅沿一个方向移动,并在途中满足所有未完成的请求,直到它到达这个方向上的最后一个磁道,或者在这个方向上没有其他请求为止。
- 由于磁头移动规律与电梯运行相似,SCAN 也被称为电梯算法。
- SCAN 算法对最近扫描过的区域不公平,因此,它在访问局部性方面不如 FCFS 算法和 SSTF 算法好。
- C-SCAN
- SCAN+直接移到另一端,两端请求都能很快处理。
- 把扫描限定在一个方向,当访问到某个方向的最后一个磁道时,磁道返回磁盘相反方向磁道的末端,并再次开始扫描。
- 其中“C”是Circular(环)的意思。
- LOOK 和 C-LOOK
- 釆用SCAN算法和C-SCAN算法时磁头总是严格地遵循从盘面的一端到另一端,显然,在实际使用时还可以改进,即磁头移动只需要到达最远端的一个请求即可返回,不需要到达磁盘端点。这种形式的SCAN算法和C-SCAN算法称为LOOK和C-LOOK调度。这是因为它们在朝一个给定方向移动前会查看是否有请求。