操作系统常见面试题

1. 进程和线程

进程是具有一定功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源调度和分配的一个独立单位。(关键词:资源调度分配的独立单位)

线程是进程的实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。(关键词:CPU调度分派的基本单位)

一个进程可以有多个线程(至少一个主线程),多个线程也可以并发执行。

进程作为资源(如内存)分配的基本单位,作为其下属的线程都是可以享用其被分配到的资源的,而且线程可以共享同一块被分配的资源。而进程之间是一般不能分享彼此的资源的,进程想要互相通信,必须通过进程间通信(Inter-process communication,IPC)的机制来完成,主要包括以下几种:

  1. 管道(pipe,半双工),流管道(s_pipe,全双工),有名管道(FIFO,全双工):管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
    无名管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用。
    命名管道:命名管道也是半双工的通信方式,在文件系统中作为一个特殊的设备文件而存在,但是它允许无亲缘关系进程间的通信。当共享管道的进程执行完所有的I/O操作以后,命名管道将继续保存在文件系统中以便以后使用。
  2. 信号量(sophomore/mutex):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  3. 信号(signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  4. 消息队列:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 共享内存:共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
  6. 套接字(socket) :套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

线程可以再分为两类:

  1. 一类是用户级线程(user level thread)。对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线“程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。 用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。

  2. 另一类是内核级线程(kernel level thread)。对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
    事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。

这里再说明一个“程序”和”进程“的区别。

程序是一个静态概念,它是指在计算机的文件系统里以文件形式存储的一段可运行代码。而进程是一个动态概念,它通常是指操作系统里一个程序在一个数据集合上一次运行过程的体现。即进程是程序的运行逻辑实际运作起来的载体。

好比扫雷是一个存在于你的开始菜单里的游戏程序,当你打开它时,你发现任务管理器里会有一个winmine的进程,而你关掉扫雷后,这个wunmine进程就消失了,但是扫雷程序还在你的菜单里。所谓一个是静态,一个是动态!

2. 线程同步的方式

首先要明白,什么是线程同步,为什么要同步?所谓同步,就是并发的线程在一些关键点上可能需要互相等待与互通信息,这种相互制约的等待与互通信息称为进程同步。
这里再引入一个临界资源与临界区的概念:

  • 临界资源是指一次仅允许一个线程使用的资源,许多物理设备,如打印机都有这种性质。除了物理设备外,还有一些软件资源,若被多线程所共享也具有这一特点,如变量、数据、表格、队列等。它们虽可以为若干线程所共享,但一次只能为一个线程所利用。

  • 临界区指的是一个访问共用资源(被多个线程共享的临界资源)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。

如上所述,同步机制所要解决的绝大多数问题,都出在临界区这儿,我们后面的同步机制都是在临界区上做文章,以避免出现问题。

同步有以下这样些个方式/机制:

  1. 互斥量(mutex):互斥量是一种公共资源,在指定时刻,它只能被一个线程占有(也就是所有权特性),而且占有它的线程可以反复申请这个互斥量。只有拥有互斥量的线程才有访问公共资源的权限。因为互斥量只有一个,所以可以保证公共资源不会被多个线程同时访问。(比如Java中的synchronized代码块,需要你提供一个类的对象或Class类作为锁,这个锁就可以理解为互斥量)

  2. 信号量(semaphore):每个信号量都是公共资源,其值是一个32位计数。信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。注意,信号量的值仅能由PV操作来改变。
    实现的P,V操作算法描述:

P操作:while s>0
s=s-1,
V操作:s=s+1

P表示申请一个资源,如果条件满足(即右可以分配的资源),则把资源分配给提出申请的进程,并且时资源数目s减1。V表示资源使用哪完毕之后,要把占有的资源释放,并且资源数目s加1 。

  1. 事件(信号 signal):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。(比如Java中的notify()唤醒wait()状态的阻塞线程)
  • 线程同步与线程互斥
    互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
    同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
    同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。
    互斥是一种特殊的同步。

3. 线程死锁死锁

在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。

比如说,你要吃饺子,就要醋和酱油,手上有一瓶醋了,结果你淘气的弟弟这时手上攥着那一瓶酱油不放,说让你把醋给他,他要先吃到同时蘸醋和酱油的饺子。那你也不谦让了,说不行,孔融让梨知道嘛,你先把酱油给哥哥,哥哥先尝。但是你的弟弟就是不给,非要你把醋先给他。于是你们僵持不下,最后谁也吃不了饺子,你们就”死锁“在这儿了。

死锁产生的四个必要条件(有一个条件不成立,则不会产生死锁)

  1. 互斥条件:一个资源一次只能被一个进程使用(醋或酱油一次只能被一个人用)
  2. 请求与保持(占有并申请)条件:一个进程因请求资源而阻塞时,对已获得资源保持不放(你攥着醋要酱油,你弟攥着酱油要醋)
  3. 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺。(你不能暴力夺取你弟的酱油)
  4. 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系。(你和你弟互相等着对方把作料交出来,不然谁也吃不上同时蘸醋和酱油的饺子)
    死锁的处理基本策略和常用方法。

解决死锁的基本方法有两种,一种是预防,即根本不要让死锁发生,一种是解锁,也就是发生死锁的事后来解决问题。

3.1 预防死锁

预防可以从死锁出现的根源原因上下手,即四个必要条件,我们可以彻底破坏其中任何一个条件,都可以让死锁永无超生之日。我们姑且称这种预防策略为【条件破坏法】。

不过,打破每个死锁的必要条件也是有一定代价的:
〈1〉打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

〈2〉打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

〈3〉打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:

[1]在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;

[2]资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;

[3]降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。    

(4)打破循环等待条件实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:

[1]限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;

[2]为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

如果破坏死锁条件总有这样那样的弊病,那么,我们想另一种预防策略:既然死锁总出现在进程申请资源的时候,那我们能不能通过去协调或者监督进程进行资源申请的那些个请求,来预防死锁呢?

答案是可以的!

那么,我们这里要暂且先引入一个【安全序列】与【系统安全】的概念:

所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列{P1,P2,...,Pn}就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的。

进程的安全序列{P1,P2,...,Pn}是这样组成的:若操作系统按照序列中的顺序为每个进程分配它所需要的资源,可以保证资源够用,进而所有进程的任务顺利执行至完毕,不会发生死锁。

虽然存在安全序列时一定不会有死锁发生,但是系统进入不安全状态(四个死锁的必要条件同时发生)也未必会产生死锁。当然,产生死锁后,系统一定处于不安全状态。 

安全也就是根本不会有死锁的可能性,一旦有死锁的可能性,系统就不安全了,就可能卡壳了,但不一定卡壳~

于是,防止死锁的思路也就有了,我们能不能用一个算法去找有没有安全序列来作为我们的参考,只要系统按照这个安全序列去分配资源,就可以预防死锁发生呢?

有!那就是【银行家算法】!

简单说一下银行家算法的思路:

有一系列进程共n个都要请求资源,这些资源共m种,每种有一定数量,每个进程都可能需要多种资源,并且每个进程可能已经占有了一些资源,但仍需要申请新的资源以完成任务。

首先设计一个【安全检测算法】,以用于判断”如果满足了某个进程的资源申请需求,系统是否还处于安全状态“,如果满足这个需求后,系统仍处于安全状态,那当然没问题,系统就分配资源,如果发现会处于不安全状态,那我们就暂时先把这个请求放一边,让它等待。

4. 进程有哪几种状态?

就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源
运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
阻塞状态: 进程等待某种条件,在条件满足之前无法执行

其实线程也是这三种状态,至于和进程间的区别,这个需要后面再仔细了解。

5. 内存----分页和分段

段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的 ;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
段的大小不固定,有它所完成的功能决定;页大大小固定,由系统决定(一般为4k)
段向用户提供二维地址空间;页向用户提供的是一维地址空间
段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。
分页和分段的详细讲解

6. 内存—页面置换算法

7.内存— 逻辑地址/物理地址/虚拟内存

所谓的逻辑地址,是指计算机用户(例如程序开发者),看到的地址。例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。

另一个重要概念是虚拟内存。操作系统读写内存的速度可以比读写磁盘的速度快几个量级。但是,内存价格也相对较高,不能大规模扩展。于是,操作系统可以通过将部分不太常用的数据移出内存,“存放到价格相对较低的磁盘缓存,以实现内存扩展。操作系统还可以通过算法预测哪部分存储到磁盘缓存的数据需要进行读写,提前把这部分数据读回内存。虚拟内存空间相对磁盘而言要小很多,因此,即使搜索虚拟内存空间也比直接搜索磁盘要快。唯一慢于磁盘的可能是,内存、虚拟内存中都没有所需要的数据,最终还需要从硬盘中直接读取。这就是为什么内存和虚拟内存中需要存储会被重复读写的数据,否则就失去了缓存的意义。

注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。而这个地址也就是虚拟地址。

8. 请阐述动态链接库与静态链接库的区别

静态链接库是.lib格式的文件,一般在工程的设置界面加入工程中,程序编译时会把lib文件的代码加入你的程序中因此会增加代码大小,你的程序一运行lib代码强制被装入你程序的运行空间,不能手动移除lib代码。

动态链接库是程序运行时动态装入内存的模块,格式*.dll,在程序运行时可以随意加载和移除,节省内存空间。

在大型的软件项目中一般要实现很多功能,如果把所有单独的功能写成一个个lib文件的话,程序运行的时候要占用很大的内存空间,导致运行缓慢;但是如果将功能写成dll文件,就可以在用到该功能的时候调用功能对应的dll文件,不用这个功能时将dll文件移除内存,这样可以节省内存空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值