计算机操作系统学习笔记 | 青训营笔记

76 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 15 天

今日学习总结内容

  1. 操作系统之虚拟化
  2. 操作系统之并发
  3. 操作系统之持久化

详细知识点

操作系统之虚拟化

操作系统虚拟化CPU和内存

虚拟化CPU

操作系统通过虚拟化CPU,实现了进程共享CPU,实现了进程之间的并发。虚拟化CPU给了所有进程一个假象,就是将单个CPU(或其中的一小部份)转换为看似无限数量的CPU,从而让许多程序看似同时运行。虚拟化CPU是通过实现底层机制和上层策略实现的进程时分共享CPU。

机制

受限直接执行协议是机制

上下文切换

上下文切换是由内核完成的。上下文切换中有两类寄存器的保存/恢复。
当进程A由于陷阱或者时钟中断要陷入内核时,硬件首先会将进程A的寄存器保存到A的内核栈中,然后转向内核模式,跳到陷阱处理程序。
在内核模式下,内核根据调度算法决定要从进程A切换到进程B,这时A的内核寄存器会被OS明确的保存在A的进程结构中。
然后恢复B进程的寄存器(从B的进程结构),然后通过改变栈指针来使用B的内核栈,在此之前栈指针指向的都是A内核栈。
最后操作系统从陷阱返回,恢复B的寄存器并开始运行B。

策略

由于只有一个CPU,进程要并发执行,所以CPU需要调度进程。每个进程的工作时长不同,首先调度长的或者短的进程都是不理想的。每个进程的操作也不同,有的进程操作是CPU密集的,有的却是IO型的,即该进程频繁的放弃CPU进行IO操作,这时应该在交替执行两种进程,在一个进程IO执行时,另一个进程使用CPU。这些都需要调度算法来实现。

调度算法有多种:FIFO(先进先出)、SJF(Shortest Job First)、STCF(Shortest Time-to-Completion >?First)、轮转、MLFQ(多级反馈队列)、比例份额(彩票调度)。

下面只讲述经常使用的多级反馈队列和比例份额算法。

MLFQ

多级反馈队列是利用最近一段时间的经验得知并预测今后某个进程的行为来进行调度。

多级反馈队列的5个规则:

  1. 如果A的优先级>B的优先级,运行A(不运行B)
  2. 如果A的优先级=B的优先级,轮转运行A和B
  3. 工作进入系统时,放在最高优先级(最上层队列)
  4. 一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入第一级队列)
  5. 经过一段时间S,就将系统中所有工作重新加入最高优先级队列中
比例份额

彩票调度利用了随机性。彩票数代表了进程占有某个资源的份额。

彩票有三个机制:

  1. 彩票货币:操作系统会根据用户的彩票数将不同用户工作的彩票数兑换为全局的彩票数。
  2. 彩票转让:进程之间可以将自己的彩票进行互相转让,使得某个进程加快执行。比如客户端/服务端交互场景中客户端会将自己的彩票转让给服务端,加快服务端的按照客户端的需求执行工作。
  3. 彩票通胀:在进程之间相互信任的环境下,利用通胀,一个进程可以临时提升或者降低自己拥有的彩票数量。

虚拟化内存

操作系统通过虚拟化内存给进程产生一种它在独占所有连续内存,并且地址空间非常大的假象。

地址转化

程序员在计算机中可以看到的所有内存地址都是虚拟化之后的,都不是真正的物理内存地址。

虚拟地址转换
虚拟地址转换有很多种实现:

  1. 物理地址=基址+虚拟地址,需要基址和界限两个硬件寄存器
  2. 分段:堆中数据的物理地址=基址+虚拟地址-堆本身的虚拟地址,栈中数据的物理地址=基址+虚拟地址-栈段虚拟地址-段最大地址,将进程的代码、堆和栈拆分开放入对应的段中,代码段、堆段和栈段中,每个段都有一组基址/界限寄存器
  3. 分页:页表中存放地址空间中每个虚拟页放在物理内存中的位置,即页表项(PTE)数组。每个PTE可以根据VPN(虚拟页号)索引到,PTE中有有效位、保护位、存在位、脏位、参考位和PFN(物理页号)。
分页之TLB

采用分页进行地址转换时,页表存放在内存中,访问内存地址时,需要通过VPN索引到页表中的PFN,增加了一次内存访问,使得分页地址转换速度变慢。为了加快速度,引入TLB(快速地址转换)。

利用进程的局部性,通过将最近用到过的VPN和FPN直接缓存到硬件TLB中,减少了一步获取内存中PTE的内存操作,从而加快了地址转换。

分页之多级页表

将页表分为多段,每段都用另一个页表(页目录)条目指针指向,就形成了多级页表。通过这种间接方式,我们可以将页表页放在物理内存中的任意位置。

操作系统之并发

这里的并发有区别于虚拟化CPU中的并发,虚拟化CPU中的并发是指进程之间的并发,而这里的并发是指线程并发。

线程

线程类似于独立进程,进程可以有多个线程,线程是一段执行的代码序列(比如一个执行中的函数代码序列),它有点像函数调用,但是它可以独立于调用者运行,可能在创建者返回之前运行,也许会晚很多。一个进程的所有线程共享地址空间,从而能够访问相同的数据。

多个线程之间对共享数据的非原子性读取和修改可能会产生冲突(临界区),对于临界区的代码周围需要加锁和释放锁保证代码段的原子性执行,加锁之后,同一时刻只可能有一个线程在临界区。
锁的实现中要保证上锁操作本身是原子性的,实现方案有测试并设置、比较并交换和获取并增加。
锁有自旋锁和休眠锁
自旋锁:
当前线程获取不了锁则一直自旋,直到时间片用完,切换至其他线程。
休眠锁:
当前线程获取不了锁则进入休眠,让出自己的时间片给其他线程。

条件变量

条件变量可以实现线程之间的通信,条件变量上面可以等等容纳等待线程(wait(),释放锁,休眠,从wait()返回之后线程会重新获取锁),上面的等待线程可以被其他线程唤醒(signal(),通知其他线程运行,运行之前需要获取锁),有了信号量和锁,可以控制多个不同种类线程之间数据共享的问题,例如生产消费者问题。

信号量

信号量也可以代替锁和条件变量来编写并发程序。 二值信号量就是锁。
线程1调用sem_wait()会将信号量减一,之后再判断信号量是否大于等于0,如果大于0,当前线程继续执行,反之则进入睡眠,等待信号量的大于等于0。线程2要运行结束时调用sem_post将信号量加一,此时信号量大于等于0,唤醒正在睡眠的进程1,最后进程1运行调用sem_post将信号量加一。
信号量值为负值时,绝对值就是等待线程个数。

死锁

进程互相等待的对方持有的锁,就形成了死锁。

预防死锁的方法:

  1. 规定所有进程获取锁的顺序相同
  2. 加全局锁保证多个枪锁原子发生
  3. 频繁的加锁、尝试获得锁,如果已经被占有,则放弃占有锁,并重复上述操作

并发之基于事件的并发

基于实践的并发基于主进程的一个事件循环,当事件发生时,主进程通过事件处理程序处理事件即可。当处理一个事件时,它是系统中发生的唯一活动。基于事件的系统不允许阻塞调用,因为只有一个主进程在运行。

操作系统之持久化

操作系统中有大量的设备驱动程序,事实上,设备驱动程序代码占内核代码的很大部分。

设备驱动程序

设备驱动程序(内核)可以读写寄存器来访问设备

文件系统的逻辑结构

超级块+bitmap(位图,data bitmap和inode bitamap)+inodes表+datablock(数据块)

每个文件一个inode号,inode表中的inode存储了文件的元信息,如读写权限、字节数、拥有者、datablock位置数组等。

Unix/Linux系统中,目录(directory)也是一种文件。
打开目录,实际上就是打开目录文件。
目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。

日志记录

日志文件系统比传统的文件系统安全,因为它用独立的日志文件跟踪磁盘内容的变化。

总结

将操作系统分为三个部分:虚拟化、并发和持久性,理解这些抽象是理解操作系统原理的概念。虚拟化CPU和虚拟化内存分别是进程对CPU和内存的共享的保证,并发中锁、条件变量和信号量是对线程并发冲突的解决,持久性是对计算机持久性数据存储的保证。