八股面经总结 - 操作系统

一、内核

内核的4种基本能力:

  • 进程调度:管理进程、线程,决定哪个进程、线程使用CPU;
  • 内存管理:管理内存的分配和回收;
  • 硬件通信:管理硬件设备,为进程与硬件设备之间提供通信能力;
  • 系统调用:如果应用程序要运行更高权限运行的服务,就需要系统调用,是用户程序与操作系统之间的接口。

内核是怎么工作的?

内核具有很高的权限,可以控制CPU、内存、硬盘等硬件,而应用程序权限很小,因此大多数操作系统把内存分为两个区域:内核空间和用户空间。

用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。应用程序如果需要进入内核空间,需要通过系统调用,产生一个中断。发生中断,CPU中断正在执行的用户程序,转而跳转到中断处理程序,开始执行内核程序。内核处理完成后,主动触发中断,CPU回到用户态继续工作。

二、内存管理

2.1 虚拟内存

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

进程持有的虚拟地址会通过CPU中的内存管理单元(MMU)的映射关系,来转换为物理地址,然后再通过物理地址访问内存。

操作系统如何管理虚拟地址与物理地址的关系?

内存分段和内存分页。

2.2 内存分段

程序是由若干个逻辑分段组成,不同的段有不同属性,因此可用分段形式把段分离出来。

分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。

八股面经总结 - 操作系统_第1张图片

  •  段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
  • 虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量到物理内存地址。

2.3 内存分页

分段的好处是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。

分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小的内存空间,称为页(Page)。Linux中,每一页的大小为4KB。

虚拟地址与物理地址之间通过页表来映射。

八股面经总结 - 操作系统_第2张图片

 页表是存储在内存里的,内存管理单元(MMU)负责将虚拟地址转换为内存地址的工作。

而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内存空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

分页是怎么解决分段的内存碎片、内存交换效率低的问题?

由于内存空间是预先划分好的,释放都是以页为单位释放的,不会产生无法给进程使用的小内存。

如果内存空间不够,操作系统会把正在运行的进程中“最近没被使用”的内存页释放,暂时写在硬盘上,即换出(Swap out)。一旦需要,再加载进来,即换入(Swap in)。

八股面经总结 - 操作系统_第3张图片

 分页不需要一次性把程序加载到物理内存中,只有在程序运行时,需要用到对应的虚拟内存页里面的指令和数据时,再加载到物理内存中。

分页机制下,虚拟地址和物理地址是如何映射的?

在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,基地址与页内偏移的组合就成了物理内存地址。

八股面经总结 - 操作系统_第4张图片

 2.4 多级页表

八股面经总结 - 操作系统_第5张图片

2.5 段页式内存管理

段页式内存管理实现的方式:

  • 先将程序划分为多个有逻辑意义的段;
  • 把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页

地址结构就由段号、段内页和页内位移三部分组成。

用于段页式地址变换的数据结构是每一个程序一张段表,每个段表又建立一张页表,段表中的地址是页表的起始地址,页表中的地址为某页的物理页号。

八股面经总结 - 操作系统_第6张图片

 2.6 Linux内存管理

在Linux操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间。

八股面经总结 - 操作系统_第7张图片

 用户空间内存,从低到高分别是7种不同的内存段:

  • 程序文件段,包括二进制可执行代码;
  • 已初始化数据段,包括静态常量;
  • 未初始化数据段,包括未初始化的静态变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(与硬件和内核版本有关);
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB。

在7个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用C标准库的malloc()或者mmap(),就可以分别在堆和文件映射段动态分配内存。

三、进程与线程

CPU可以从一个进程切换到另一个进程,在切换前必须要记录当前进程中运行的状态信息。

在一个进程的活动期间至少具备三种状态:

  •  运行状态(Running):该时刻进程占用CPU;
  • 就绪状态(Ready): 可运行,由于其他进程处于运行状态而暂时停止运行;
  • 阻塞状态(Blocked):该进程正等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,也无法运行;

八股面经总结 - 操作系统_第8张图片

此外,还有两个基本状态:

  • 创建状态(New):进程正在被创建时的状态;
  • 结束状态(Exit):进程正在从系统消失时的状态; 

八股面经总结 - 操作系统_第9张图片

 在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出硬盘,等需要再次运行的时候,再从硬盘换入到物理内存。

八股面经总结 - 操作系统_第10张图片

 这时,需要用挂起状态,描述进程没有占用实际的物理内存空间的情况。而阻塞状态是等待某个事件的返回。

挂起状态一共有两种:

  • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
  • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立即运行;

八股面经总结 - 操作系统_第11张图片

 导致进程挂起的原因不只是因为进程所使用的内存空间不在物理内存,还包括如下情况:

  • 通过sleep让进程间歇性挂起,工作原理是设置一个定时器,到期后唤醒进程。
  • 用户希望挂起一个程序的执行;

 进程的控制结构

在操作系统中,用进程控制块(Process control block, PCB)数据结构来描述进程的。

PCB是进程存在的唯一标识,具体包含以下信息:

  • 进程描述信息:
    • 进程标识符:标识各个进程,每个进程都有一个且唯一的标识符;
    • 用户标识符:进程归属的用户,主要为共享和保护服务;
  • 进程控制盒管理信息:
    • 进程当前状态,如new、ready、running、waiting或blocked等;
    • 进程优先级:进程抢占CPU时的优先级;
  • 资源分配清单:
    • 有关内存地址空间或虚拟地址空间的信息,所以打开文件的列表和所使用的I/O设备信息
  • CPU相关信息:
    • CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的PCB中,以便进程重新执行时,能从断电处继续执行。

每个PCB通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。

  • 将所有处于就绪状态的进程链在一起,称为就绪队列;
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
  • 另外,对于运行队列在单核CPU系统中则只有一个运行指针,因为单核CPU在某个时间,只能运行一个程序。

八股面经总结 - 操作系统_第12张图片

进程的控制

 01 创建进程:

  • 为新进程分配一个唯一的进程标识号,并申请一个空白的PCB,PCB是有限的,若申请失败则创建失败;
  • 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;
  • 初始化PCB;
  • 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行;

02 终止进程:

  • 查找需要终止进程的PCB;
  • 如果处于执行状态,则立即终止该进程的执行,然后将CPU资源分配给其他进程;
  • 如果还有子进程,则应该将所有子进程终止;
  • 将该进程所拥有的全部资源都归还给父进程或者操作系统;
  • 将其从PCB所在的队列移除;

03 阻塞进程

  • 找到将要被阻塞进程标识号对应的PCB;
  • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
  • 将该PCB插入到阻塞队列中去;

04 唤醒进程

  • 在该事件的阻塞队列中找到相应进程的PCB;
  • 将其从阻塞队列中移除,并置其状态为就绪状态;
  • 把该PCB插入到就绪队列中,等待调度程序调度;

进程的阻塞和唤醒是一对功能相反的语句。

进程的上下文切换

CPU寄存器(CPU内部的一个容量小,但是速度极快的内存)和程序计数器(存储CPU正在执行的指令位置,或者即将执行的下一条指令位置)是CPU在运行任何任务前,所必须依赖的环境,这种环境就是CPU上下文。

CPU上下文切换分为:进程上下文切换、线程上下文切换和中断上下文切换。

一个进程切换到另一个进程运行,称为进程的上下文切换。进程是由内核管理和调度的,所以进程的上下文切换只能发生在内核态。进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

线程

线程是进程当中的一条执行流程。同一个进程内的多个线程可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,可以确保线程的控制流是相对独立的。

八股面经总结 - 操作系统_第13张图片

 线程与进程的比较

  • 进程是资源(包括内存、打开的文件等)分配的单位,线程是CPU调度的单位;
  • 进程拥有一个完整的资源平台,而线程只共享必不可少的资源,如寄存器和栈;
  • 线程同样具有就绪、阻塞和执行三种基本状态,同样具有状态之间的转换关系;
  • 线程能减少并发执行的时间和空间开销;

线程的实现

主要有三种线程的实现方式:

  • 用户线程:在用户空间实现的线程,不是由内核管理的线程,由用户态的线程库来管理;
  • 内核线程:在内核中实现的线程,由内核来管理;
  • 轻量级进程:在内核中用来支持用户线程;

用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户态线程库函数直接来完成线程的管理,包括线程的创建、终止、同步和调度等。

八股面经总结 - 操作系统_第14张图片

 内核线程是由操作系统管理的,线程对应的TCB放在操作系统里,线程的创建、终止和管理由操作系统负责。

八股面经总结 - 操作系统_第15张图片

轻量级进程是内核支持的用户线程,一个线程可以有一个或多个LWP,每个LWP和内核线程是一对一映射的。 

八股面经总结 - 操作系统_第16张图片

调度

调度的原则

  1. CPU利用效率:在I/O事件致使CPU空闲的情况下,调度程序需要从就绪队列选择一个进程来运行。
  2. 系统吞吐量:调度程序要权衡长任务和短任务进程的运行完成数量。
  3. 周转时间:如果进程的等待时间很长而运行时间很短,那周转时间就很长,调度程序应该避免该情况。
  4. 等待时间:就绪队列中进程的等待时间也是调度程序所需要考虑的原则。
  5. 响应时间:对于交互式比较强的应用,响应时间也是调度程序需要考虑的因素。

调度算法 

01 先来先服务调度算法(First Come First Served, FCFS)

八股面经总结 - 操作系统_第17张图片

02 最短作业优先调度算法(Short Job First,SJF)

八股面经总结 - 操作系统_第18张图片

 03 高响应比优先调度算法(Highest response ratio next, HRRN)

八股面经总结 - 操作系统_第19张图片

 04 时间片轮转调度算法(Round Robin, RR)

八股面经总结 - 操作系统_第20张图片

05 最高优先级调度算法(Highest Priority First,HPF)

06 多级反馈调度算法(Multilevel Feedback Queue)

 八股面经总结 - 操作系统_第21张图片

 进程间通信

1. 管道

管道比较简单,也很容易得知管道里的数据是否被另一个进程读取了,但是效率低,不适合程序间频繁地交换数据。管道传输的数据是无格式的流且大小受限。

管道实际上是内核里的一串缓存。匿名管道的通信范围是存在于父子关系的进程。命名管道提前创建了一个类型为管道的设备文件,在进程里面只要使用这个设备文件,可以在不想关的进程间通信。不管是匿名管道还是命名管道,进程写入数据都是缓存在内核中,另一个进程读取数据时也是从内核获取。

2. 消息队列

消息队列是保存在内核中的消息链表,发送数据时会分成独立的消息体(数据块)。

消息队列的通信不及时,附件大小也有限制,不适合比较大数据的传输。

消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。

3. 共享内存

将两个进程的虚拟地址映射到相同的物理内存中。

八股面经总结 - 操作系统_第22张图片

 4. 信号量

信号量是为了防止多进程竞争共享资源,而造成的数据错乱,使共享的资源在任意时刻只能被一个进程访问的保护机制。

信号量实质上是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • P操作,把信号量减1,相减后:
    • 信号量 < 0,资源已被占用,进程需阻塞等待;
    • 信号量 >= 0,还有资源可用,正常执行。
  • V操作,把信号量加1,相加后:
    • 信号量 <= 0,有阻塞的进程,把该进程唤醒运行;
    • 信号量 > 0,当前没有阻塞的进程。

信号量初始化为1,代表着互斥信号量,可以保证共享内存在任何时刻只有一个进程在访问。

八股面经总结 - 操作系统_第23张图片

信号量初始化为0,为同步信号量,实现多进程的同步。

八股面经总结 - 操作系统_第24张图片

 5. 信号

对于异常状态下的工作模式,需要用信号的方式来通知进程。

八股面经总结 - 操作系统_第25张图片

 信号是进程间通信的唯一异步通信机制。

6. Socket

socket通信是跨网络与不同主机上的进程之间通信,也可以在同主机进程间通信。

创建socket的系统调用:

int socket(int domain, int type, int protocal)

三个参数分别代表:

  • domain指定协议族,如AF_INET, AF_INET6,AF_LOCAL/AF_UNIX;
  • type用来指定通信特性,如SOCK_STREAM,SOCK_DGRAM;
  • protoccal基本废弃,一般写为0; 

针对TCP协议的socket编程模型

八股面经总结 - 操作系统_第26张图片

  • 服务端和客户端初始化socket,得到文件描述符;
  • 服务端调用bind,将socket绑定在IP地址和端口;
  • 服务端调用listen,进行监听;
  • 服务端调用accept,等待客户端连接;
  • 客户端调用connect,向客户端的地址和端口发起连接请求;
  • 服务端accept, 范湖用于传输的socket的文件描述符;
  • 客户端调用write写入数据,服务端调用read读取数据;
  • 客户端断开连接时,会调用close,那么服务器端read读取数据的时候,就会读取到EOF,待处理完数据后,服务端调用close,表示连接关闭。

针对UDP协议的socket编程模型

八股面经总结 - 操作系统_第27张图片

 多线程同步

八股面经总结 - 操作系统_第28张图片

线程是调度的基本单位,进程是资源分配的基本单位。线程之间是可以共享进程的资源,不如代码段、堆空间、数据段、打开的文件等资源,但每个线程都有自己独立的栈空间。

八股面经总结 - 操作系统_第29张图片

 互斥与同步的实现和使用

01 锁

分为忙等待锁和无忙等待锁。

忙等待锁,也称为自旋锁,当获取不到锁时,线程会一直等待,不做任何事。

无等待锁在没有或得到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把CPU让给其他线程执行。

02 信号量

信号量是操作系统提供的一种协调共享资源访问的方法。通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。

  • P操作:将sem减1,相减后,如果sem < 0,则进程/线程进入阻塞等待,否则继续,表明P操作可能会阻塞;
  • V操作:将sem加1,相加后,如果sem<=0,唤醒一个等待中的进程/线程,表明V操作不会阻塞;

P操作是用在进入临界区之前,V操作是用在离开临界区之后,两个操作必须成对出现的。

生产者-消费者问题

八股面经总结 - 操作系统_第30张图片

 哲学家就餐问题

互斥访问有限的竞争问题(如I/O设备)一类的建模过程。

方法1:奇数和偶数编号的哲学家不同顺序拿起左右叉子

八股面经总结 - 操作系统_第31张图片

 方法2:用数组state记录每一个哲学家进程是在思考还是进食,只有在两个邻居都没有进食的情况下,才进入进餐状态。

八股面经总结 - 操作系统_第32张图片

 八股面经总结 - 操作系统_第33张图片

 读者-写者问题

数据库访问的模型。

读者优先和写者优先策略都会造成饥饿现象,因此实现公平策略:

  • 优先级相同;
  • 写者、读者互斥访问;
  • 只能一个写者访问临界区;
  • 可以多个读者同时访问临界资源;

八股面经总结 - 操作系统_第34张图片

 死锁

死锁只有同时满足以下四种条件才会发生:

  • 互斥条件:多个线程不能同时使用同一个资源;
  • 持有并等待条件:线程A在等待资源2的同时并不会释放自己已持有的资源1;
  • 不可剥夺条件:在自己使用完之前不能被其他线程获取;
  • 环路等待条件:两个线程获取资源的顺序构成了环形链;

避免死锁的方法:使用资源有序分配法,破坏环路等待条件。

悲观锁和乐观锁

互斥锁和自旋锁

最底层的两种锁,对于加锁失败后的处理方式不一样:

  • 互斥锁加锁失败后,线程会释放CPU,给其他线程;
  • 自旋锁加锁失败后,线程会忙等待,直到拿到锁;

线程互斥锁加锁失败后,会释放掉CPU,加锁的代码会被阻塞。互斥锁加锁失败而阻塞的现象是由操作系统内核实现的。加锁失败后,内核会将线程置为“睡眠”状态,锁被释放后,内核会在合适的时机唤醒线程。

八股面经总结 - 操作系统_第35张图片

互斥锁加锁失败后,会从用户态陷入到内核态,让内核帮忙切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本,即两次线程上下文切换的成本。

线程的上下文切换指的是:当两个线程同属一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存的资源保持不动,只需切换线程的私有数据、寄存器等不共享的数据。

自旋锁是通过CPU提供的CAS函数(Compare And Swap),在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销小一些。

一般加锁的过程包括两个步骤:

  • 查看锁的状态,如果锁时空闲的,则执行下一步;
  • 将锁设置为当前线程持有

CAS把两个步骤合并成一个原子指令。在单核CPU上,需要抢占式的调度器(不断通过时钟中断一个线程,运行其他线程),否则自旋锁在单CPU上无法使用(自旋线程永远不会放弃CPU)。

当加锁失败时,互斥锁用线程切换来应对,自旋锁用忙等待来应对。自旋锁开销小,在多核系统下一般不会产生进程切换,适合异步、协程等在用户态切换请求的编程方法,但如果代码持锁时间过长,自旋的线程会长时间占用CPU。

读写锁

读写锁适用于能明确区分读操作和写操作的场景。在读多写少的场景有优势。读写锁的工作原理:

  • 当写锁没有被线程持有时,多个线程能够并发地持有读锁,大大提高了共享资源的访问效率,因为读锁用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源。
  • 当写锁被线程持有时,读线程获取读锁的操作被阻塞,其他线程获取写锁的操作也被阻塞。

乐观锁与悲观锁

互斥锁、自旋锁、读写锁都是悲观锁。悲观锁的多线程同时修改共享资源的概率比较高,很容易出现冲突,因此访问共享资源前,先要上锁。

乐观锁假定冲突的概率很低,先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过资源,就放弃本次操作。乐观锁只有在冲突概率非常低,且加锁成倍非常高的场景时,才考虑用乐观锁。

四、调度算法

进程调度算法

调度算法影响的是等待时间(进程在就绪队列中等待调度的时间的总和),而不能影响进程真正在使用CPU的时间和I/O时间。

常见的调度算法:

  • 先来先服务调度算法
  • 最短作业优先调度算法
  • 高响应比优先调度算法
  • 时间片轮转调度算法
  • 多级反馈队列调度算法

内存页面置换算法

缺页异常(缺页中断)

当CPU访问的页面不存在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入到物理内存。与一般中断的主要区别在于:

  • 缺页中断在指令执行“期间”产生和处理中断信号,而一般中断在一条指令执行“完成”后检查和处理中断信号。
  • 缺页中断返回到该指令的开始重新执行“该指令”,而一般中断返回回到该指令的“下一个指令”执行。

八股面经总结 - 操作系统_第36张图片

 缺页中断的处理流程:

  • 在CPU里访问一条Load M指令,然后CPU会去找M所对应的页表项。
  • 如果该页表项的状态位是有效的,那CPU就可以直接去访问物理内存,如果状态为是无效的,则CPU会发送缺页中断请求。
  • 操作系统找到了缺页中断,则会执行缺页中断处理函数,先会查找该页面在磁盘中的页面的位置。
  • 找到磁盘中对应的页面后,需要把该页面换入到物理内存中,但在换入前,需要在物理内存中找空闲页,如果找到空闲页,就把页面换入到物理内存中。
  • 页面从磁盘换入到物理内存完成后,则把页表项中的状态位修改为“有效的”
  • CPU重新执行导致缺页异常的指令。

页表项的字段:

  • 状态位:表示该页是否有效,即是否在物理内存中,供程序访问时参考;
  • 访问字段:记录该页咋一段时间内被访问的次数,供页面置换算法选择出页面时参考;
  • 修改位:表示该页在调入内存后是否被修改过,由于内存中的每一页都在磁盘上保留一份副本,因此,如果没有修改,在置换该页时就不需要将该页写回到磁盘上,以减少系统的开销;如果已经被修改,则将该页重写到磁盘上,以保证磁盘中保留的始终是最新的副本。
  • 硬盘地址:用于指出该页在磁盘上的物理地址,通常是物理块号,供调入该页时使用。

八股面经总结 - 操作系统_第37张图片

 最佳页面置换算法

基本思路是置换“未来”最长时间不访问的页面。在实际系统中无法实现,作用是衡量效率。

先进先出置换算法

选择在内存中驻留很长时间的页面进行置换。

最近最久未使用(LRU)的置换算法

选择最长时间没有被访问的页面进行置换。代价很高,要在内存中维护一个所有页面的链表。

时钟页面置换算法

把所有的页面都保存在一个类似钟面的“环形链表”中,一个表针指向最老的页面,当发生缺页中断时,算法首先检查表针指向的页面:

  • 如果它的访问位是0就淘汰该页面,并把新的页面插入该位置,然后把表针前移一个位置;
  • 如果访问位是1就清除访问位,并把表针前移一个位置,重复这个过程直到找到一个访问位为0的页面为止;

八股面经总结 - 操作系统_第38张图片

 最不常用(LFU)算法

当发生缺页中断时,选择“访问次数”最少的那个页面,并将其淘汰。

对每个页面设置一个“访问计数器”,每当一个页面被访问时,该页面的访问计数器就累加1.在发生缺页中断时,淘汰计数器值最小的页面。

磁盘调度算法

磁盘调度算法是为了提高磁盘访问的性能,一般是通过优化磁盘的访问请求顺序来实现的。

先来先服务

最短寻道时间优先

优先选择从当前磁头位置所需寻道时间最短的请求。可能会产生饥饿(磁头在一块区域来回移动)。

扫描算法

磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向。

循环扫描算法

只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最边缘的磁道,即复位磁头。该过程很快,并且返回中途不处理任何请求。磁道只响应一个方向上的请求。

LOOK 与 C-LOOK算法

针对SCAN、C-SCAN算法优化,优化思路是磁头在移动到最远的请求位置,然后立即反向移动。

LOOK算法:磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,不需要移动到磁盘的最始端和最末端,反向移动的途中会响应请求

C-LOOK算法:磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,不需要移动到磁盘的最始端和最末端,反向移动的途中不会响应请求

五、文件系统

文件系统的基本组成

文件系统是操作系统中负责管理持久数据的子系统。文件系统的基本数据单位是文件,目的是对磁盘上的文件进行组织管理。

在LInux系统中,“一切皆文件”。不仅普通的文件和目录,块设备、管道、socket等也是统一交给文件系统管理的。

linux文件系统会为每个文件分配两个数据结构:索引节点(index node) 和 目录项(directory entry),主要用来记录文件的元信息和目录层次结构。

  • 索引节点(inode)用来记录文件的元信息,比如inode编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一 一对应,同样都被存储在硬盘中。
  • 目录项(dentry)用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。

文件系统把多个扇区组成一个逻辑块,一次性读写多个扇区。

八股面经总结 - 操作系统_第39张图片

磁盘进行格式化的时候,会分为三个存储区域,分别是超级块、索引节点区和数据块区。

  • 超级块,存储文件系统的详细信息,如块个数、块大小、空闲块等。当文件系统挂载时进入内存。
  • 索引节点区,存储索引节点。当文件被访问时进入内存。
  • 数据块区:存储文件或目录数据。 

虚拟文件系统

八股面经总结 - 操作系统_第40张图片

 文件的使用

八股面经总结 - 操作系统_第41张图片

操作系统在打开文件表中维护着打开文件的状态和信息:

  • 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的;
  • 文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件。该计数器跟踪打开和关闭的数量,当该计数为0时,系统关闭文件,删除该条目;
  • 文件磁盘位置:绝大多数文件操作都要求系统修改文件数据,该信息保存在内存中,以免每个操作都从磁盘读取;
  • 访问权限:每个进程打开文件都需要有一个访问模式 (创建、只读、读写、添加等),该信息保存在进程的打开文件表中,以便操作系统能允许或拒绝之后的I/O请求;

在用户视角里,文件就是一个持久化的数据结构,用户和操作系统对文件的读写操作是有差异的,用户习惯以字节的方式读写文件,而操作系统则是以数据块来读写文件,文件系统负责屏蔽掉这种差异。

读文件和写文件的过程:

  • 当用户进程从文件读取1个字节大小的数据时,文件系统则需要获取字节所在的数据块,再返回数据块对应的用户进程所需的数据部分。
  • 当用户进程把1个字节大小的数据写进文件时,文件系统则找到需要写入数据的数据块的位置,然后修改数据块中对应的部分,最后再把数据块写回磁盘。

文件系统的基本操作单位是数据块。

文件的存储

文件的数据存储在硬盘上的,数据在磁盘上的存放方式有以下两种:

  • 连续空间存放方式;
  • 非连续空间存放方式,又分为链表方式和索引方式;

连续空间存放方式

文件存放在磁盘连续的物理空间中。这种模式下,文件的数据都是紧密相连,读写效率很高。文件头里需要指定起始块的位置和长度。会产生磁盘空间碎片,文件长度不易扩展。

非连续空间存放方式

链表的方式存放是离散的,不连续的,可以消除磁盘碎片。分为隐式链表和显式链表。

隐式链表的文件头要包含第一块和最后一块的位置,并且每个数据块里面留出来一个指针空间,用来存放下一个数据块的位置。

八股面经总结 - 操作系统_第42张图片

 无法直接访问数据块,只能通过指针顺序访问文件,以及数据块指针消耗了一定的存储空间。分配的稳定性较差,导致链表中的指针丢失或损坏,会导致文件数据的丢失。

显式链接把用于链表文件各数据的指针,显式地存放在内存的一张链表中,每个表项中存放链接指针,指向下一个数据库。提高了检索速度,大大减小了访问磁盘的次数,但不适用于大磁盘。

索引的实现是为每个文件创建一个索引数据块,里面存放的是指向文件数据块的指针列表。文件头需要包含指向索引数据块的指针。

八股面经总结 - 操作系统_第43张图片

 索引的优点在于:

  • 文件的创建、增大、缩小很方便;
  • 不会有碎片的问题;
  • 支持随机读写和顺序读写;

索引数据也是存储在磁盘,会带来额外的开销。

链式索引块的实现方式是在索引数据块留出一个存放下一个索引数据块的指针。

八股面经总结 - 操作系统_第44张图片

 多级索引块的实现方式是通过一个索引块来存放多个索引数据块。

八股面经总结 - 操作系统_第45张图片

 八股面经总结 - 操作系统_第46张图片

 Unix文件的实现方式

根据文件的大小,存放的方式会有所变化:

  • 如果存放文件所需的数据块小于10块,采用直接查找的方式;
  • 如果存放文件所需的数据块超过10块,采用一级间接索引方式;
  • 如果前面两种方式都不够存放大文件,采用二级间接索引方式;
  • 如果二级间接索引方式也不够存放大文件,采用三级间接索引方式;

文件头(Inode)需要包含13个指针:

  • 10个指向数据块的指针;
  • 第11个指向索引块的指针;
  • 第12个指向二级索引的指针;
  • 第13个指向三级索引块的指针;

对于小文件使用直接查找的方式可以减少索引数据块的开销;

对于大文件则以多级索引的方式来支持,所以大文件的访问需要大量查询,效率较低。

空闲空间管理

空闲表法

为所有空闲空间创建一张表,内容包含空闲区的第一个块号和该空闲区的块个数。

八股面经总结 - 操作系统_第47张图片

 请求分配裁判空间和回收空间,都需要顺序扫描空闲表。仅当有少量空闲区时效果好

空闲链表法

每一个空闲块里有一个指针指向下一个空闲块。

八股面经总结 - 操作系统_第48张图片

 只需在主存中保存一个指针,令它指向第一个空闲块。特点是简单,但不能随机访问,效率低。

位图法

利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。Linux文件系统使用位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于inode空闲块的管理。

文件系统的结构

八股面经总结 - 操作系统_第49张图片

  •  超级块,包含文件的重要信息,如inode总个数、块总个数、每个块组的inode个数、每个块组的块个数等等。
  • 块组描述符,包含文件系统中各个块组的状态。比如块组中空闲块和inode的数目相等,每个块组都包含了文件系统中“所有块组的组描述符信息”。
  • 数据位图和inode位图,表示对应的数据块或inode是空闲的还是被使用中。
  • inode列表,包含了块组中的所有inode,inode用于保存文件系统中与各个文件和目录相关的所有元数据。
  • 数据块,文件的有用数据。

目录的存储

普通文件的块里保存的是文件数据,而目录文件的块里保存的是目录里面的一项文件信息。

在目录文件的块中,最简单的格式就是列表。

八股面经总结 - 操作系统_第50张图片

 Linux的wxt文件系统采用的是哈希表格式的保存目录,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,说明该文件的信息在相应的块里。查找非常迅速,插入和删除也比较简单,不过需要避免哈希冲突。

软链接和硬链接

都是比较特殊的文件,在Linux中可以通过软硬链接给某个文件取名。

硬链接是多个目录项中的“索引节点”指向一个文件,不可用于跨文件系统。只有删除文件所有的硬链接以及源文件时,系统才会彻底删除该文件。

八股面经总结 - 操作系统_第51张图片

 软链接相当于重新创建一个文件,该文件有独立的inode,文件的内容是另外一个文件的路径。软链接是可以跨系统的,如果目标文件被删除,软链接仍然存在。

文件I/O

缓冲与非缓冲I/O

根据“是否利用标准库缓冲”区分:

  • 缓冲I/O,利用标准库的缓存(标准库内部实现的缓冲)实现文件的加速访问,标准库再通过系统调用访问文件。
  • 非缓冲I/O:直接通过系统调用访问文件。

直接与非直接I/O

根据“是否用操作系统的缓存区分”:

  • 直接I/O,不会发生内核缓存和用户缓存之间的数据复制,而是直接经过文件系统访问磁盘。
  • 非直接I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。

以下场景会触发内核缓存的数据写入到磁盘:

  • 调用write的最后,发现内核缓存的数据太多的时候,内核会把数据写到数据上;
  • 用户主动调用sync,内核缓存会刷新到磁盘上;
  • 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷新到磁盘上;;
  • 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷新到磁盘上;

阻塞与非阻塞I/O VS 同步与异步I/O

阻塞I/O:当用户程序执行read,线程会被阻塞,直到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区,拷贝完成后,read才会返回。

阻塞等待的是“内核数据准备好”和“数据从内核态拷贝到用户态”两个过程。

八股面经总结 - 操作系统_第52张图片

 非阻塞I/O:read请求在为准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read调用才可以获取到结果。

八股面经总结 - 操作系统_第53张图片

 最后一次read调用,获取数据的过程,是一个同步的过程,是需要等待的过程。

I/O多路复用

都是同步调用。在read调用时,内核将数据从内核空间拷贝到用户空间,过程都是需要等待的。如果内核拷贝效率不高,read调用就会在这个同步过程中等待较长的时间。

真正的异步I/O是内核数据准备好和数据从内核拷贝到用户态都不用等待。

select:

八股面经总结 - 操作系统_第54张图片

 aio_read(异步):

八股面经总结 - 操作系统_第55张图片

六、设备管理

I/O控制方式

DMA:

八股面经总结 - 操作系统_第56张图片

DMA的工作方式如下:

  • CPU需对DMA控制器下发指令,告诉它想读取多少数据,读完的数据放在内存;
  • DMA控制器向磁盘控制器发送指令,通知它从磁盘读数据到其内部的缓冲区中,接着磁盘控制器将缓冲区的数据传输到内存;
  • 磁盘控制器把数据传输到内存的操作完成后,磁盘控制器在总线上发送一个确认成功的信号到DMA控制器;
  •  DMA控制器收到信号后,DMA控制器发出中断通知CPU指令完成,CPU就可以直接用内存里面现成的数据了;

设备驱动程序

设备驱动程序会提供统一的接口给操作系统。设备驱动程序初始化的时候,需要先注册一个该设备的中断处理函数。

八股面经总结 - 操作系统_第57张图片

  •  在I/O时,设备控制器如果已经准备好数据,则会通过中断控制器向CPU发送中断请求;
  • 保护被中断进程的CPU上下文;
  • 转入相应的设备中断处理函数;
  • 进行中断处理;
  • 恢复被中断进程的上下文;

通用块层

Linux通过一个统一的块层,来管理不同的块设备。通用块层是处于文件系统和磁盘驱动中间的一个块设备抽象层,主要有两个功能:

  • 向上为文件系统和应用程序,提供访问设备的标准接口,向下把各种不同的磁盘设备抽象为统一的块设备,并在内核层面,提供一个框架来管理这些设备的驱动程序;
  • 通用层还会给文件系统和应用程序发来的I/O请求排队,接着会对队列重新排序,请求合并等方式,也就是I/O调度,提高磁盘读写的效率。

Linux内存支持五种I/O调度算法:

  • 没有调度算法,不做任何处理,常用在虚拟I机I/O;
  • 先入先出调度算法;
  • 完全公平调度算法;
  • 优先级调度算法,适用于运行大量进程的系统,如桌面环境、多媒体应用等;
  • 最终期限调度算法,分别为读、写请求创建不停的I/O队列,适用于I/O压力较大的场景,如数据库。

存储系统I/O软件分层

Linux存储系统的I/O由上到下可以分为三个层次,分别是文件系统层、通用块层、设备层。

八股面经总结 - 操作系统_第58张图片

 三个层次的作用是:

  • 文件系统层,包括虚拟文件系统和其他文件系统的具体实现,它向上为应用程序统一提供了标准的文件访问接口,向下会通过通用块层来存储和管理磁盘数据。
  • 通用块层,包括块设备的I/O队列和I/O调度器,它会对文件系统的I/O请求进行排队,再通过I/O调度器,选择一个I/O发给下一层的设备层。
  • 设备层,包括硬件设备、设备控制器和驱动程序,负责最终物理设备的I/O操作。

七、网络系统

网络模型

  • 应用层,负责向用户提供一组应用程序,比如HTTP、DNS、FTP等;
  • 传输层,负责端到端的通信,比如TCP、UDP等;
  • 网络层,负责网络包的封装、分片、路由、转发,比如IP、ICMP等;
  • 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、MAC寻址、差错测试,以及通过网卡传输网络帧等;

Linux网络协议栈

八股面经总结 - 操作系统_第59张图片

  •  应用程序需要通过系统调用,跟socket层进行数据交互;
  • socket层的下面就是传输层、网络层和网络接口层;
  • 最下面的一层,则是网卡驱动程序和硬件网卡设备;

Linux收发网络包的流程

网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过DMA技术,将网络包放入到Ring Buffer。

为了解决频繁中断带来的开销,linux内核在2.6版本引入了NAPI机制,首先采用中断唤醒数据接收的服务程序,然后poll的方法来轮询数据。

零拷贝

八股面经总结 - 操作系统_第60张图片

零拷贝的实现

mmap()代替read(),直接把内核缓冲区里的数据映射到用户空间,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

八股面经总结 - 操作系统_第61张图片

 sendfile

八股面经总结 - 操作系统_第62张图片

 SG - DMA

八股面经总结 - 操作系统_第63张图片

  •  通过DMA将磁盘上的数据拷贝到内核缓冲区中;
  • 缓冲区描述符和数据长度传到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,不需要将数据从操作系统内核缓冲区拷贝到socket缓冲区

PageCache(磁盘缓存)

缓存最近被访问的数据;预读功能。但是大文件传输时会不起作用。

在高并发场景下,针对大文件的传输方式,应该选用异步I/O + 直接I/O,代替零拷贝。

I/O多路复用

服务器首先调用socket函数,创建网络协议为IPv4,以及传输协议为TCP的Socket,接着调用bind()函数,给Socket绑定一个IP地址和端口:

  • 绑定端口的目的:当内核收到TCP报文,通过TCP头部的端口号,找到对应的应用程序,然后传递给其数据。
  • 绑定IP地址的目的:一台计算机可以有多个有对应IP地址网卡,内核会先收到对应网卡的包。

绑定完IP地址和端口后,就可以调用listen()函数进行监听,可以通过netstat命令查看对应端口号是否被监听。

服务器进入监听状态后,通过调用accept()函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。

客户端在创建好Socket后,调用connect()函数发起连接,指定服务端的IP地址和端口号。

在TCP连接的过程中,服务器的内核为每个Socket维护了两个队列:

  • TCP半连接队列,服务端处于syn_rcvd状态;
  • TCP全连接队列,服务端处于established状态;

当TCP全连接队列不为空后,服务端的accept函数就会从内核中的TCP全连接队列中取出一个socket返回应用程序,后续数据传输都用这个socket。

每一个进程都有一个数据结构task_struct,该结构里有一个指向“文件描述符组”的成员指针。该数组里列出这个进程打开文件的所有文件描述符。数组的下标是文件描述符,是一个整数,而数组的内容是一个指针,指向内核中所有打开的文件的列表,也就是说内核可以通过文件描述符找到对应打开的文件。

每个文件都有一个inode,Socket文件的inode指向了内核中的Socket结构,在这个结构体里有两个队列,分别是发送队列和接收队列,在两个队列里保存的是一个个struct sk_buff,用链表的组织形式串起来。

sk_buff可以表示各个层的数据包,在应用层数据包叫data,在TCP层称为segment,在IP层叫做packet,在数据链路层叫做frame。

八股面经总结 - 操作系统_第64张图片

 多进程编程

为每个客户端分配一个进程来处理请求。

服务器的主进程负责监听客户端的连接,一旦与客户端连接完成,accept()函数就会返回一个“已连接Socket”,通过fork()函数创建一个子进程,把父进程的所有相关数据都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。

两个进程刚复制完的时候几乎一模一样,会根据返回值来区分是父进程还是子进程。

每一个进程会占用一定系统资源,进程的上下文切换还包含了虚拟内存、栈、全局变量等用户空间资源,以及内核堆栈、寄存器等内核空间资源。

八股面经总结 - 操作系统_第65张图片

多线程编程

线程是运行在进程中的一个“逻辑流”,单进程中可以运行多个线程,同进程里的线程可以共享进程的部分资源,如文件描述符、进程空间、代码、全局数据、堆、共享库等,上下文切换只需切换进程的私有数据、寄存器等不共享的数据。

当服务器与客户端TCP完成连接后,通过pthread_create()函数创建线程,然后将“已连接Socket”的文件描述符传递给线程函数,接着在线程里和客户端进行通信。

 八股面经总结 - 操作系统_第66张图片

 I/O多路复用

进程可以通过一个系统调用函数从内核获取多个事件

select和poll

将已连接的Socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,内核通过遍历文件描述符集合的方式,当检查到有事件产生后,socket标记为可读或可写,再把整个文件描述符集合拷贝回用户态,再通过遍历找到可读或可写socket。select使用固定长度的BitsMap表示文件描述符的集合。

poll通过使用动态数组,以链表形式组织文件描述符。

poll 和 select 都是使用线性结构存储进程关注的Socket集合,因此都需要遍历文件描述符集合来找到可读或可写的socket,也需要在用户态和内核态之间拷贝文件描述符集合。

epoll

在内核中使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的socket通过epoll_ctl()函数加入内核中的红黑树里,每次操作时只需要传入一个待检测的socket。

epoll使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个socket有事件发生时,通过回调函数内核会将其加入到该就绪事件列表中,当用户调用epoll_wait()函数时,只会返回事件发生的文件描述符的个数。

八股面经总结 - 操作系统_第67张图片

 八、高性能网络模式:Reactor 和 Proactor

reactor模式是非阻塞同步网络模式,感知的是就绪可读写的事件,I/O多路复用监听事件,收到事件后,根据事件类型分配给某个进程/线程。

proactor是异步网络模式,感知的是已完成的读写事件。

九、Linux命令

八股面经总结 - 操作系统_第68张图片

 八股面经总结 - 操作系统_第69张图片

 

八股面经总结 - 操作系统_第70张图片

 八股面经总结 - 操作系统_第71张图片

 八股面经总结 - 操作系统_第72张图片

九、日志

 

你可能感兴趣的:(linux,运维,服务器)