目录
3.进程、线程的概念以及区别?进程间的通信方式?★★★★★★
4.进程有哪几种状态,状态之间的转换、进程调度策略?★★★★
5.什么是死锁?死锁产生的四个必要条件?如何预防死锁?★★★★★★
10.内存分配有哪些机制?(JVM的内存管理及垃圾回收算法)★★★
其他面试专业课:
1.操作系统的特点?功能?★
操作系统特点:
- 操作系统的四个基本特征是并发,共享,异步,虚拟。而每个操作系统又有其独特的特征,如我们常用的linux系统有开放性;多用户多任务;设备的独立性;强大的网络功能和网络可靠性等特点。
操作系统的功能:
- 管理计算机系统的全部软、硬件资源 , 合理组织计算机的工作流程 , 以达到充分发挥计算机资源的效率 , 为用户提供友好界面。
2.中断和系统调用的区别★★★
中断分两种,硬中断和软中断;硬中断是实实在在的硬件发出的中断,cpu检测到发生中断后,保护现场,查找中断向量地址,执行中断服务程序,之后,重新选择进程进行调度。软中断是由指令执行过程中发出的中断,但是并没有中断向量表,而是有对应的散转表,查找对应的中断号,转中断服务程序,之后的和硬中断相同。
系统调用是软中断的一种。
无论如何,发生中断时,要从目态转向管态。
3.进程、线程的概念以及区别?进程间的通信方式?★★★★★★
进程
我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)。
进程是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建。线程
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
区别
- 进程是资源分配的基本单位;线程是程序执行的基本单位,两者都可并发执行。
- 进程拥有自己的地址空间,每启动一个进程,系统就会为它分配地址空间;而线程与CPU资源分配无关,多个线程共享同一进程内的资源,使用相同的地址空间。
- 一个线程只能属于一个进程,线程依赖于进程而存在。而一个进程可以有多个线程,但至少有一个线程。
- 进程切换的开销也远大于线程切换的开销。
- 线程是处理器调度的基本单位,但是进程不是。
进程间的通信方式
1.管道
所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。
2.消息队列
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
3.共享内存
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
4.信号量
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
- 一个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
- 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
5.信号
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。
2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即
SIGKILL
和SEGSTOP
,它们用于在任何时候中断或结束某一进程。6.Socket
Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。
4.进程有哪几种状态,状态之间的转换、进程调度策略?★★★★
进程的三种状态
- 就绪(Ready)状态:进程已分配到除CPU以外的所有必要资源,只要获得处理机便可立即执行。
- 执行(Running)状态:进程已获得处理机,其程序正在处理机上执行。
- 阻塞(Blocked)状态:正在执行的程序,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的原因可能是等待I/O完成、申请缓冲区不能满足、等待信号等。
Q1:为什么在转换图中没有就绪到阻塞和阻塞到执行的转换方向?
就绪状态进程没有占有处理机,即不经过执行,其状态就不会改变;阻塞状态进程唤醒后要先进入到就绪队列,才会被调度程序选中,进行执行状态
进程调度策略
先来先服务调度算法
最简单的一个调度算法,就是非抢占式的先来先服务(First Come First Severd, FCFS)算法了。
顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。
FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
最短作业优先调度算法
最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
这显然对长作业不利,很容易造成一种极端现象。
比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。
高响应比优先调度算法
前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。
那么,高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。
每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:
从上面的公式,可以发现:
- 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
- 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
时间片轮转调度算法
最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法。
每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。
- 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
- 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
另外,时间片的长度就是一个很关键的点:
- 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
- 如果设得太长又可能引起对短作业进程的响应时间变长。将
通常时间片设为
20ms~50ms
通常是一个比较合理的折中值。最高优先级调度算法
前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。
但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。
进程的优先级可以分为,静态优先级或动态优先级:
- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
- 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
该算法也有两种处理优先级高的方法,非抢占式和抢占式:
- 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
- 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
但是依然有缺点,可能会导致低优先级的进程永远不会运行。
多级反馈队列调度算法
多级反馈队列(Multilevel Feedback Queue)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。
顾名思义:
- 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
- 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
来看看,它是如何工作的:
- 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
- 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
- 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
5.什么是死锁?死锁产生的四个必要条件?如何预防死锁?★★★★★★
在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件;
- 持有并等待条件;
- 不可剥夺条件;
- 环路等待条件;
那么避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?
线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
我们使用资源有序分配法的方式来修改前面发生死锁的代码,我们可以不改动线程 A 的代码。
我们先要清楚线程 A 获取资源的顺序,它是先获取互斥锁 A,然后获取互斥锁 B。
所以我们只需将线程 B 改成以相同顺序的获取资源,就可以打破死锁了。
6.哲学家进餐有哪些实现方式?★★★★
哲学家进餐问题的核心是保证至少有一位哲学家能拿到两只筷子就餐后释放筷子。
(1)最多只允许n-1个哲学家拿起筷子就餐。
(2)资源分级算法,奇数号哲学家先拿左手边的筷子,偶数号的哲学家先拿右手边的筷子。
(3)设立规则,当一位哲学家拿起一只筷子时,另一个筷子无法得到,则放下刚刚拿起的筷子.
(4 ) 服务生算法,一次只允许一名哲学家进餐,等到这名哲学家进餐完毕后才允许其他哲学家进餐。
7.简述下银行家算法★★★★
银行家算法(Banker’s Algorithm)是一种经典的死锁避免算法,这个算法主要用于操作系统中的资源分配,以确保系统能够安全地进行资源分配和释放,避免因资源分配不当而导致的死锁。
银行家算法的基本思想类似于银行在贷款时要保证其有足够的资金以应对所有客户的最大需求,以免因资金周转不灵而导致破产。在操作系统中,银行家算法通过模拟资源分配和检查系统是否进入安全状态来决定是否批准资源请求。
算法的核心包括以下几个步骤:
初始化系统状态:设置初始的
Available
(可用资源向量)、Max
(最大需求矩阵)、Allocation
(分配矩阵)和Need
(需求矩阵)。请求资源:当进程请求资源时,检查请求是否合法,即请求量不超过进程的最大需求和系统的可用资源。
试探性分配:临时分配资源给进程,并更新系统状态。
安全性检查:调用安全性算法检查系统是否处于安全状态。如果安全,正式分配资源;否则,恢复原状态,拒绝请求。
银行家算法中的几个关键数据结构包括:
- Available向量:系统中可利用的资源数目。
- Max矩阵:每个进程对每种资源的最大需求。
- Allocation矩阵:每个进程已分配的各类资源的数目。
- Need矩阵:每个进程还需要的各类资源数。
这个算法有效地避免了死锁问题,确保了系统的安全运行。通过这种方式,银行家算法在操作系统中发挥着重要作用,特别是在资源分配和进程管理方面。
8.介绍下几种常见的进程调度算法及其流程★★★★★★
见第四题: 进程调度策略
9.分页的作用,好处?和分段有什么区别?★★★
(1)页是信息的物理单位,分页是为了实现非连续分配,以便解决内存碎片问题,或者说分页是由于系统管理的需要。段是信息的逻辑单位,它含有一组意义相对完整的信息,分段的目的是为了更好地实现共享,满足用户的需要。
(2)页的大小固定且由系统确定,将逻辑地址划分为页号和页内地址是由机器硬件实现的,而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时根据信息的性质来划分。
(3)分页的作业地址空间是一维的;分段的地址空间是二维的。
10.内存分配有哪些机制?(JVM的内存管理及垃圾回收算法)★★★
11.什么是虚拟内存?什么是共享内存?★★★
虚拟内存
虚拟内存是一种内存管理技术,允许计算机程序认为自己拥有一个连续的可用内存空间,而实际上这个内存空间可能被物理内存(RAM)和磁盘存储(例如硬盘)共同实现。
虚拟内存通过页(pages)和页表(page tables)实现。程序运行时,虚拟地址通过页表转换为物理地址。如果所需数据不在物理内存中,会发生页错误(page fault),操作系统会从磁盘中读取相应数据并加载到物理内存中。
共享内存
共享内存是一种允许多个进程访问同一块内存区域的机制。通过共享内存,进程之间可以高效地进行数据交换,而无需通过文件或消息队列等方式进行较慢的数据传递。
在实际应用中,操作系统提供了一些API(如POSIX共享内存、System V共享内存)来创建和管理共享内存区域。使用共享内存时,需要注意同步问题,因为多个进程同时访问共享内存时可能会导致数据竞争和不一致性。
12.有哪些页面置换算法?★★★★
最佳页面置换算法
最佳页面置换算法基本思路是,置换在「未来」最长时间不访问的页面。
所以,该算法实现需要计算内存中每个逻辑页面的「下一次」访问时间,然后比较,选择未来最长时间不访问的页面。
我们举个例子,假设一开始有 3 个空闲的物理页,然后有请求的页面序列,那它的置换过程如下图:
在这个请求的页面序列中,缺页共发生了
7
次(空闲页换入 3 次 + 最优页面置换 4 次),页面置换共发生了4
次。这很理想,但是实际系统中无法实现,因为程序访问页面时是动态的,我们是无法预知每个页面在「下一次」访问前的等待时间。
所以,最佳页面置换算法作用是为了衡量你的算法的效率,你的算法效率越接近该算法的效率,那么说明你的算法是高效的。
先进先出置换算法
既然我们无法预知页面在下一次访问前所需的等待时间,那我们可以选择在内存驻留时间很长的页面进行中置换,这个就是「先进先出置换」算法的思想。
还是以前面的请求的页面序列作为例子,假设使用先进先出置换算法,则过程如下图:
在这个请求的页面序列中,缺页共发生了
10
次,页面置换共发生了7
次,跟最佳页面置换算法比较起来,性能明显差了很多。最近最久未使用的置换算法
最近最久未使用(LRU)的置换算法的基本思路是,发生缺页时,选择最长时间没有被访问的页面进行置换,也就是说,该算法假设已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。
这种算法近似最优置换算法,最优置换算法是通过「未来」的使用情况来推测要淘汰的页面,而 LRU 则是通过「历史」的使用情况来推测要淘汰的页面。
还是以前面的请求的页面序列作为例子,假设使用最近最久未使用的置换算法,则过程如下图:
在这个请求的页面序列中,缺页共发生了
9
次,页面置换共发生了6
次,跟先进先出置换算法比较起来,性能提高了一些。虽然 LRU 在理论上是可以实现的,但代价很高。为了完全实现 LRU,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。
困难的是,在每次访问内存时都必须要更新「整个链表」。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常费时的操作。
所以,LRU 虽然看上去不错,但是由于开销比较大,实际应用中比较少使用。
时钟页面置换算法
那有没有一种即能优化置换的次数,也能方便实现的算法呢?
时钟页面置换算法就可以两者兼得,它跟 LRU 近似,又是对 FIFO 的一种改进。
该算法的思路是,把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面。
当发生缺页中断时,算法首先检查表针指向的页面:
- 如果它的访问位位是 0 就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;
- 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为 0 的页面为止;
我画了一副时钟页面置换算法的工作流程图,你可以在下方看到:
了解了这个算法的工作方式,就明白为什么它被称为时钟(Clock)算法了。
最不常用算法
最不常用(LFU)算法,这名字听起来很调皮,但是它的意思不是指这个算法不常用,而是当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰。
它的实现方式是,对每个页面设置一个「访问计数器」,每当一个页面被访问时,该页面的访问计数器就累加 1。在发生缺页中断时,淘汰计数器值最小的那个页面。
看起来很简单,每个页面加一个计数器就可以实现了,但是在操作系统中实现的时候,我们需要考虑效率和硬件成本的。
要增加一个计数器来实现,这个硬件成本是比较高的,另外如果要对这个计数器查找哪个页面访问次数最小,查找链表本身,如果链表长度很大,是非常耗时的,效率不高。
但还有个问题,LFU 算法只考虑了频率问题,没考虑时间的问题,比如有些页面在过去时间里访问的频率很高,但是现在已经没有访问了,而当前频繁访问的页面由于没有这些页面访问的次数高,在发生缺页中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不高的页面。
那这个问题的解决的办法还是有的,可以定期减少访问的次数,比如当发生时间中断时,把过去时间访问的页面的访问次数除以 2,也就说,随着时间的流失,以前的高访问次数的页面会慢慢减少,相当于加大了被置换的概率。
13.说一说操作系统中缓冲区溢出怎么处理★★★
在操作系统中,缓冲区溢出(Buffer Overflow)是一个常见的安全漏洞,指的是程序在向缓冲区写入数据时超过了缓冲区的边界,从而覆盖了相邻的内存空间。这种漏洞可能会被攻击者利用来执行任意代码或导致程序崩溃。处理缓冲区溢出问题通常包括以下几个方面:
输入验证和边界检查:
- 确保所有输入都经过验证,检查长度和格式。
- 使用安全的函数,例如
strncpy
代替strcpy
,snprintf
代替sprintf
,确保不会写入超出缓冲区边界的数据。编程语言的选择:
- 使用带有内存安全特性的编程语言,例如Java、Python、Rust等,这些语言通常会自动进行边界检查,减少缓冲区溢出的风险。
使用现代编译器的保护机制:
- 现代编译器提供了一些选项来防止缓冲区溢出。例如,GCC和Clang提供了
-fstack-protector
选项,可以在栈上插入“栈保护”来检测溢出。- 使用地址空间布局随机化(ASLR)技术,使攻击者难以预测内存地址,增加攻击难度。
- 数据执行保护(DEP),防止数据段的代码执行,降低攻击者利用缓冲区溢出的可能性。
运行时检查:
- 在运行时进行额外的检查和保护,例如使用工具如Valgrind、AddressSanitizer等,检测并修复内存管理错误和缓冲区溢出。
代码审查和测试:
- 定期进行代码审查和安全测试,特别是对涉及内存操作的代码进行重点检查。
- 使用模糊测试(Fuzz Testing)自动生成大量输入数据,测试程序的稳定性和安全性。
安全库和框架:
- 使用经过安全审查和验证的第三方库和框架,可以减少手工实现内存操作的风险。
- 例如,使用C++的标准库提供的
std::vector
和std::string
等安全数据结构,自动管理内存和边界检查。通过以上这些方法,可以有效地减少缓冲区溢出的风险,提高系统的安全性和稳定性。
14.磁盘调度算法以及磁盘空间存储管理?★★★★
先来先服务
先来先服务(First-Come,First-Served,FCFS),顾名思义,先到来的请求,先被服务。
那按照这个序列的话:
98,183,37,122,14,124,65,67
那么,磁盘的写入顺序是从左到右,如下图:
先来先服务算法总共移动了
640
个磁道的距离,这么一看这种算法,比较简单粗暴,但是如果大量进程竞争使用磁盘,请求访问的磁道可能会很分散,那先来先服务算法在性能上就会显得很差,因为寻道时间过长。最短寻道时间优先
最短寻道时间优先(Shortest Seek First,SSF)算法的工作方式是,优先选择从当前磁头位置所需寻道时间最短的请求,还是以这个序列为例子:
98,183,37,122,14,124,65,67
那么,那么根据距离磁头( 53 位置)最近的请求的算法,具体的请求则会是下列从左到右的顺序:
65,67,37,14,98,122,124,183
磁头移动的总距离是
236
磁道,相比先来先服务性能提高了不少。但这个算法可能存在某些请求的饥饿,因为本次例子我们是静态的序列,看不出问题,假设是一个动态的请求,如果后续来的请求都是小于 183 磁道的,那么 183 磁道可能永远不会被响应,于是就产生了饥饿现象,这里产生饥饿的原因是磁头在一小块区域来回移动。
扫描算法
最短寻道时间优先算法会产生饥饿的原因在于:磁头有可能再一个小区域内来回得移动。
为了防止这个问题,可以规定:磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向,这就是扫描(Scan)算法。
这种算法也叫做电梯算法,比如电梯保持按一个方向移动,直到在那个方向上没有请求为止,然后改变方向。
还是以这个序列为例子,磁头的初始位置是 53:
98,183,37,122,14,124,65,67
那么,假设扫描调度算先朝磁道号减少的方向移动,具体请求则会是下列从左到右的顺序:
37,14,
0
,65,67,98,122,124,183
磁头先响应左边的请求,直到到达最左端( 0 磁道)后,才开始反向移动,响应右边的请求。
扫描调度算法性能较好,不会产生饥饿现象,但是存在这样的问题,中间部分的磁道会比较占便宜,中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异。
循环扫描算法
扫描算法使得每个磁道响应的频率存在差异,那么要优化这个问题的话,可以总是按相同的方向进行扫描,使得每个磁道的响应频率基本一致。
循环扫描(Circular Scan, CSCAN )规定:只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应一个方向上的请求。
还是以这个序列为例子,磁头的初始位置是 53:
98,183,37,122,14,124,65,67
那么,假设循环扫描调度算先朝磁道增加的方向移动,具体请求会是下列从左到右的顺序:
65,67,98,122,124,183,
199
,0
,14,37
磁头先响应了右边的请求,直到碰到了最右端的磁道 199,就立即回到磁盘的开始处(磁道 0),但这个返回的途中是不响应任何请求的,直到到达最开始的磁道后,才继续顺序响应右边的请求。
循环扫描算法相比于扫描算法,对于各个位置磁道响应频率相对比较平均。
LOOK 与 C-LOOK算法
我们前面说到的扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换方向。
那这其实是可以优化的,优化的思路就是磁头在移动到「最远的请求」位置,然后立即反向移动。
那针对 SCAN 算法的优化则叫 LOOK 算法,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中会响应请求。
而针 C-SCAN 算法的优化则叫 C-LOOK,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中不会响应请求。
15.文件系统的基本组成★★
文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。
- 索引节点,也就是 inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。
- 目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。
由于索引节点唯一标识一个文件,而目录项记录着文件的名字,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个别名。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。
注意,目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。
目录项和目录是一个东西吗?
虽然名字很相近,但是它们不是一个东西,目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。
如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了文件系统的效率。
注意,目录项这个数据结构不只是表示目录,也是可以表示文件的。
那文件数据是如何存储在磁盘的呢?
磁盘读写的最小单位是扇区,扇区的大小只有
512B
大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为
4KB
,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。以上就是索引节点、目录项以及文件数据的关系,下面这个图就很好的展示了它们之间的关系:
索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中。
另外,磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。
- 超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。
- 索引节点区,用来存储索引节点;
- 数据块区,用来存储文件或目录数据;
我们不可能把超级块和索引节点区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的:
- 超级块:当文件系统挂载时进入内存;
- 索引节点区:当文件被访问时进入内存;