操作系统常见面试题整理(Most Common Problems in Interview for Operating System)

参考链接:https://blog.csdn.net/qq_32690999/article/details/78069414

将网上的相关面试题整理下来,去掉重复的,留下我觉得讲得很清楚的,或者自己在面试里碰到过的题目。

参考:
1. 常见面试题整理–操作系统篇(每位开发者必备)
2. Wiki-Synchronization (computer science)
3. 计算机操作系统(第2版)——庞丽萍、阳富民编著


请分别简单说一说进程和线程以及它们的区别

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

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

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

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

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 管道(pipe,半双工),流管道(s_pipe,全双工),有名管道(FIFO,全双工)
  • 信号量(sophomore/mutex)
  • 信号(signal)
  • 消息队列
  • 共享内容
  • 套接字(socket)

线程可以再分为两类:

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

  • 另一类是内核级线程(kernel level thread)。对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。

事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。

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

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

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


线程同步的方式有哪些?

首先要明白,什么是线程同步,为什么要同步?

所谓同步,就是并发的线程在一些关键点上可能需要互相等待与互通信息,这种相互制约的等待与互通信息称为进程同步。

  
  
  • 1

“同”其实是协同,而不是同时,因为我们知道多线程终究是不能同时执行的(看起来那么多程序同时运行互相不干扰那是从宏观层面看是这样),那么线程之间由于执行权在不断地切换,如果不同线程都做不同的事,处理不同的数据倒也没什么问题,关键是有时候会有那么一些数据,是被多个线程共享的,大家都有可能对这些数据进行一些操作。

比如,银行系统同时收到两个请求向同一个账户A存钱,都是要存入100元.账户A中本来有1000元。那么系统对两个请求肯定是分别生成一个线程去完成各自的对账户进行加上100元的操作,可是当其中一个线程1运行完以下语句:

account=account+100 

  
  
  • 1

当线程1读取到account在内存中的值1000并加上了100得到1100,但还没更新到数据库中时,线程1由于丧失了执行权,线程2来接手了,线程2也执行这一句,但它读到的account值依然是1000(数据库的数据尚未更新),也把它加上100,变成1100,然后要更新到数据库。那后面不管谁先更新到数据库,数据库里A账户的记录都只会是1100,而不是1200了,这就出岔子了。

要防范这种问题也不难,我们可以让给执行账户值更改一直到把新的值更新到数据库里的整个操作给”封闭“起来,一次只允许一个线程来对同一个账户执行这样一些操作,如果多个线程想同时来执行这个操作,对不起,因为这些操作被”封闭“了,不等里面的一个线程把它要做的修改更新到数据库之前,其它线程一概不许进入,乖乖在外面排队等着吧。

这只是一个简单的例子,于是我们需要一些机制来保证这样的问题不会出现,让不同的线程在操作这样一些共同的数据时不会出问题,再或者是有时候有些事必须等A线程做完了,B线程才能接着后面的做,我们要让这些需要多个线程合作的任务都正常进行,因此就要让它们”同步“!

这里再引入一个临界资源与临界区的概念:

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

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

  
  
  • 1
  • 2
  • 3

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

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

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

信号量(semaphore):每个信号量都是公共资源,其值是一个32位计数。信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。注意,信号量的值仅能由PV操作来改变。
实现的P,V操作算法描述:
P操作:while s>0
s=s-1,
V操作:s=s+1。
P表示申请一个资源,如果条件满足(即右可以分配的资源),则把资源分配给提出申请的进程,并且时资源数目s减1。V表示资源使用哪完毕之后,要把占有的资源释放,并且资源数目s加1 。

事件(信号 signal):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。(比如Java中的notify()唤醒wait()状态的阻塞线程)

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果你对线程同步的方式不太熟悉,建议阅读:线程同步的几种方式


什么是缓冲区溢出?有什么危害?其原因是什么?

缓冲区为暂时置放输出或输入资料的内存。

缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

缓冲区溢出是一种非常普遍、非常危险的漏洞,在很多软件与操作系统都存在这种问题。

举个例子帮助理解其本质,好比你往一个100ml容量的烧杯里倒200ml浓硫酸,实际情况下你肯定不会这么做,但是溢出的问题就在于有人想这么做了,而且这个倒的行为没有被提前检查并阻止。于是100ml的浓硫酸溢出烧杯,并且流到烧杯外的周围区域,造成破坏。而哪怕你倒的不是浓硫酸(非恶意程序),只是普通的水,由于水的溢出,也会对周围的环境造成意料外的冲刷(对临近数据的覆盖)。

计算机中,缓冲区溢出会造成的危害主要有以下两点:

  • 程序崩溃,导致拒绝服务
  • 跳转并且执行一段恶意代码

造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入是否合理。

如果你想深入了解缓冲区溢出,推荐阅读:缓冲区溢出攻击


死锁

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

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

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

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

死锁的处理基本策略和常用方法。

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

预防死锁

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

不过,打破每个死锁的必要条件也是有一定代价的:

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

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

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

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

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

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

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

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

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

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

答案是可以的!

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

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

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

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

  
  
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

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

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

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

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

安全检测算法如下所示:

1) 设置两个向量

① 工作向量Work:int[m],它表示系统可提供给进程继续运行所需的各类资源数目,它含有m个元素,在执行安全算法开始时,Work:=Available,Available:int[m]表示系统当前所拥有的各类资源数目,是一个数组int[m],int[i]表示第i种资源的系统拥有数量。

② Finish:int [n],它表示进程是否完成运行任务。开始时先做Finish[i]:=false,即所有的进程都还没有运行完成(完成了的进程就不会进行资源申请了);当有足够资源分配给进程时,再令Finish[i]:=ture,表示系统将资源分配给进程使得进程完成了运行任务.

③ Need:int[n][m]:表示每个进程所需的各类资源的数目,Need[i]表示第i个进程所需的资源向量。

④ Allocation:int[n][m]:各个进程当前所已经持有的各类资源数目,即系统之前可能已经分配给各个进程的一部分资源。

2) 从进程集合中尝试找到一个满足下述条件的进程:

① Finish[i]=false;(该进程尚未完成任务)

② Need[i,j]<=Work[j] for all j=1 to m;(系统当前可分配的各类资源数大于或等于进程i所需的各类资源数)若找不到,执行步骤3),否则,执行步骤4)。

3) 分配资源给进程pi,进程Pi获得资源后,可顺利执行,直至完成,并释放/返回给系统之前分配给它的资源,故应执行:

     Work[j]:=Work[j]+Allocation[i,j] for all j;

     Finish[i]:=true;

     Go to step 2;

4) 如果所有进程的Finish[i]=true都满足(所有进程都能顺利分配资源,执行完毕),则表示系统处于安全状态;否则,系统处于不安全状态。

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

有了以上的安全监测算法后,银行家算法的框架也出来了:

pic

(图像来自这里)

Request:int[n][m]是一个进程发出的资源申请请求,当请求进来时我,我们首先检查:

  1. Request[i]<=Need[i]?
    即当前进程的资源请求是否超出它实际所需要以完成任务的资源数目,如果超过了,这种请求显然是不合理的,在浪费资源,那么我们就直接抛出异常。

  2. 如果上一步检查通过,我们再检查:Request[i]<=Available,即系统当前拥有的资源是否可以满足这个资源请求,如果不可以,那么说明系统暂时无能为力,这个请求只能先搁置一边,进入等待状态。而如果可以,那么我们就尝试把进程所要求的资源”试探性“地分配给进程,具体做法如下:

Available=Available-Request[i];
Allocation[i]=Allocation[i][j]+Request[i][j];
Need[i]=Need[i]-Request[i];
  
  
  • 1
  • 2
  • 3

执行完以上变量更新后,我们就执行一次安全检测算法,如果算法表明结果安全,好,那么就分配资源给进程,如果不安全,那么久回滚上述的资源”试探性“分配操作,使得这个资源请求也进入等待状态。

更多具体内容有关银行家算法的,请参考这个博客银行家算法

解决死锁的常用策略如下:

鸵鸟策略、预防策略、避免策略、检测与解除死锁

如果你对死锁的处理策略不是太熟悉,推荐阅读:产生死锁的原因和必要条件+解决死锁的基本方法

如果对死锁还不是太熟悉,建议阅读:死锁产生的原因和解锁的方法


进程有哪几种状态?

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

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


内存

分页和分段

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

  
  
  • 1
  • 2
  • 3
  • 4

如果你对分页和分段还不太了解,建议阅读:分段和分页

逻辑地址/物理地址/虚拟内存

参考:
1. 虚拟内存
2. 操作系统

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

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

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

用下图来帮助理解虚拟内存的作用。

这里写图片描述

现代计算机中有一个专门的转译缓冲区(Translation Lookaside Buffer,TLB),用来实现虚拟地址到物理地址的快速转换。

与内存/虚拟内存相关的还有如下两个概念:

1) Resident Set

当一个进程在运行的时候,操作系统不会一次性加载进程的所有数据到内存,只会加载一部分正在用,以及预期要用的数据。其他数据可能存储在虚拟内存,交换区和硬盘文件系统上。被加载到内存的部分就是resident set。

2) Thrashing

由于resident set包含预期要用的数据,理想情况下,进程运行过程中用到的数据都会逐步加载进resident set。但事实往往并非如此:每当需要的内存页面(page)不在resident set中时,操作系统必须从虚拟内存或硬盘中读数据,这个过程被称为内存页面错误(page faults)。当操作系统需要花费大量时间去处理页面错误的情况就是thrashing。

页面置换算法

请求调页,也称按需调页,即对不在内存中的“页”,当进程执行时要用时才调入,否则有可能到程序结束时也不会调入。而内存中给页面留的位置是有限的,在内存中以帧为单位放置页面。为了防止请求调页的过程出现过多的内存页面错误(即需要的页面当前不在内存中,需要从硬盘中读数据,也即需要做页面的替换)而使得程序执行效率下降,我们需要设计一些页面置换算法,页面按照这些算法进行相互替换时,可以尽量达到较低的错误率。

  • FIFO算法

先入先出,即淘汰最早调入的页面。

  • OPT(MIN)算法

选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。

可惜,MIN需要知道将来发生的事,只能在理论中存在,实际不可应用。

  • LRU(Least-Recently-Used)算法
    用过去的历史预测将来,选最近最长时间没有使用的页淘汰(也称最近最少使用)。

LRU准确实现:计数器法,页码栈法。

由于代价较高,通常不使用准确实现,而是采用近似实现,例如Clock算法。

  • 内存抖动现象:

页面的频繁更换,导致整个系统效率急剧下降,这个现象称为内存抖动(或颠簸)。抖动一般是内存分配算法不好,内存太小引或者程序的算法不佳引起的。

  • Belady现象:

对有的页面置换算法,页错误率可能会随着分配帧数增加而增加。

FIFO会产生Belady异常。

栈式算法无Belady异常,LRU,LFU(最不经常使用),OPT都属于栈式算法。


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

参考:
操作系统

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

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

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


中断与系统调用

参考:
操作系统

中断

所谓的中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一事件的程序。等这些特殊事情处理完之后再回去执行之前的程序。中断一般分为三类:

由计算机硬件异常或故障引起的中断,称为内部异常中断;
由程序中执行了引起中断的指令而造成的中断,称为软中断(这也是和我们将要说明的系统调用相关的中断);
由外部设备请求引起的中断,称为外部中断。简单来说,对中断的理解就是对一些特殊事情的处理。
与中断紧密相连的一个概念就是中断处理程序了。当中断发生的时候,系统需要去对中断进行处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序了。

另一个与中断紧密相连的概念就是中断的优先级。中断的优先级说明的是当一个中断正在被处理的时候,处理器能接受的中断的级别。中断的优先级也表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理某一中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。

典型的中断优先级如下所示:

机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断
当发生软件中断时,其他所有的中断都可能发生并被处理;但当发生磁盘中断时,就只有时钟中断和机器错误中断能被处理了。

系统调用

在讲系统调用之前,先说下进程的执行在系统上的两个级别:用户级和核心级,也称为用户态和系统态(user mode and kernel mode)。

程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件等,就需要向操作系统发出调用服务的请求,这就是系统调用。

Linux系统有专门的函数库来提供这些请求操作系统服务的入口,这个函数库中包含了操作系统所提供的对外服务的接口。当进程发出系统调用之后,它所处的运行状态就会由用户态变成核心态。但这个时候,进程本身其实并没有做什么事情,这个时候是由内核在做相应的操作,去完成进程所提出的这些请求。

系统调用和中断的关系就在于,当进程发出系统调用申请的时候,会产生一个软件中断。产生这个软件中断以后,系统会去对这个软中断进行处理,这个时候进程就处于核心态了。

那么用户态和核心态之间的区别是什么呢?(以下区别摘至《UNIX操作系统设计》)

用户态的进程能存取它们自己的指令和数据,但不能存取内核指令和数据(或其他进程的指令和数据)。然而,核心态下的进程能够存取内核和用户地址
某些机器指令是特权指令,在用户态下执行特权指令会引起错误
对此要理解的一个是,在系统中内核并不是作为一个与用户进程平行的估计的进程的集合,内核是为用户进程运行的。


IO多路复用

参考:
操作系统

  • 基本概念

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

  • 常见的IO复用实现

select(Linux/Windows/BSD Unix), epoll(Linux),kqueue(BSD/Mac OS X)

有关IO多路复用的具体知识,可以看通俗讲解 异步,非阻塞和 IO 复用Redis 和 I/O 多路复用

猜你喜欢

转载自blog.csdn.net/weixin_43956456/article/details/107749733
今日推荐