《操作系统导论》阅读

Posted by Sutdown on August 30, 2023

前言:

在读该书前1-9章时,难度尚可,但其实对于一个操作系统初学者来说,看懂并不一定等于完全理解,所以写下这篇文章是为了为前九章的学习做一个简单的回顾思考总结,在第10章时,书中明确写出该章节为高级章节,建议学习完第二部分并发后再阅读,所以现在的大体想法是写完今天这一篇后会从25章开始看起。

由于刚开始学习并且没有进行很多代码类型的实验,大多都是看看书或者看看视频对于操作系统进行一个初步的理解,所以若是有错误欢迎指正。

这本书分为三个部分:第1部分 虚拟化3-24章;第二部分 并发25-34章;第三部分 持久性35-50章。

该书并不建议按照顺序阅读,一定要结合个人思考阅读所需要的章节。

正文:

1.虚拟化,并发,持久性的来源,为什么是这三个部分?

首先我们要认识什么是操作系统,操作系统是一种软件,负责让程序运行变得更容易,甚至允许你同时运行多个程序,允许程序共享内存,允许程序和其它设备交互,以及其它类似操作,它们负责确保系统既易于使用又正确高效的运行。

很多程序运行时,它们需要同时访问计算机的指令和数据共享内存,需要访问设备共享磁盘……如何合理的安排好各个程序的内存空间,使得计算机内的空间能被最大化的利用,储存更多的事情,进行更好的运算时虚拟化所要进行的任务,也就是多个程序如何运行,按照什么样的方式运行能够最好的利用空间。

当大量程序交替进行时,称为并发问题,随着指令执行次数的增多,并发过程中可能会出现问题,所以是在于程序如何并发的。

磁盘中的空间比内存更大,但是内存更接近cpu,可如果在程序运行时发生中断,电脑断电或者长时间不用等意外事件发生时,电脑要如何更好的保持数据的存放,也就是持久性

2.进程
1)进程的存在是为了什么?

当你在电脑上打开多个程序时,比如音乐,游戏,浏览器等等,它们每一个都是一个进程,进程是对这些软件的运行进行一个概括,从而更好的对电脑中各种程序的运行进行一个解释。进程从字面上理解也就是正在运行的程序。

2)进程重要的机器状态有哪些?

进程的机器状态主要分为两部分,

一是内存,指令存在内存中,正在运行的程序的读取和写入的数据也都存在内存中,因此进程可以访问的内存(也可以说是地址空间)也是该进程的一部分。

二是寄存器,程序在运行时,很多都需要及时的读取和更新寄存器,比如Instruction Pointer指令指针寄存器(可以知道程序即将执行哪个指令),栈指针,桢指针(可以管理函数参数栈,局部变量和返回地址)。

3)进程是如何创建的?

步骤一:加载到内存。程序最初以某种可执行格式驻留在磁盘中,因此,操作系统需要先从磁盘读取这些字节,在加载到内存中进程的地址空间处。

在该过程中遵循两个原则,一是加载过程要尽早完成;二是惰性加载,即在程序执行期间,仅加载需要加载的代码或者数据片段。

步骤二:为该进程分配空间。要为运行时栈分配内存;也要为堆分配空间;同时执行一些初始化任务,特别是和输入输出有关的任务以便程序更快的运行。

以上两步骤完成之后,再跳转到main例程,cpu将控制权给到新创建的进程中,程序开始执行。

4)进程和线程的区别是什么?

进程是一个具有独立功能的程序关于某个数据集合的以此运行活动,是系统进行资源分配和调度的独立单位,也是基本的执行单元。

线程是进程中的执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(程序计数器、一组寄存器和栈),但它可与同属一个进程的其他线程共享进程所拥有的全部资源。

进程属于大量级,执行开销大,创建或者撤销时系统要为之分配和回收资源,并且通信机制复杂,但是可以很好的进行资源管理和保护;线程属于轻量级,执行开销小,通信机制简单,线程之间会进行协作同步,但是不利于资源保护。

5)进程的状态

进程由三个状态,分别是运行,就绪,阻塞。这三者和后面的调度密不可分,因为多个进程共用同一个cpu,只用通过调度才能在合适的时间将cpu的控制权给相应的进程。

6)UNIX中的系统调用API

fork, wait 和 exec 是 Linux 系统中常用的三个系统调用,它们可以用来创建和管理进程,以及执行不同的程序。下面是它们的用法和功能:

  • fork() 用于创建一个新的进程,它会复制当前进程的代码段、数据段、堆栈段等,然后在子进程中返回 0,在父进程中返回子进程的进程 ID。fork() 的函数签名是:
1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
  • wait() 用于等待一个子进程结束,并获取其退出状态。父进程调用 wait() 后会阻塞,直到有一个子进程结束,然后返回该子进程的进程 ID,并将其退出状态保存在 status 参数指向的变量中。wait() 的函数签名是:
1
2
3
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • exec() 是一组系统调用,它们可以用来执行一个新的程序,替换当前进程的代码段、数据段、堆栈段等。exec() 的函数签名有多种形式,其中一种是:
1
2
#include <unistd.h>
int execvp(const char *file, char *const argv[]);

其中 file 参数是要执行的程序的文件名,argv 参数是一个字符串数组,表示传递给程序的参数列表,最后一个元素必须是 NULL。

fork(), wait() 和 exec() 可以组合使用来实现多种功能,例如:

  • 创建一个和父进程完全相同的子进程,让它们同时或交替执行不同的任务。
  • 创建一个子进程,并让它执行另一个程序,例如 shell 命令或其他可执行文件,然后让父进程等待子进程结束。
  • 创建多个子进程,并让它们分别执行不同的程序,然后让父进程等待所有子进程结束。

如果你想了解更多关于 fork(), wait() 和 exec() 的用法和示例,你可以参考以下网页12345

3.如何更好的虚拟化cpu。
1)虚拟化时可能面临的困难。

虚拟化CPU指的是在多个进程运行时,给每个进程都有种独占CPU的错觉,采用了时分共享的方法,运行一个进程一段时间,再运行另一个进程。举个抽象例子,两个人吃一个桃子,桃子在两人不知情的情况下轮流在两人手中,然后在两人吃的时候都能吃到桃子,那么他们都以为自己是在独占桃子,不会想到有另一个人也在吃。桃子是CPU,人是进程,计算机中就是那么运作的。

在构建这样的虚拟化机制时面临两个难点:

一是性能;二是控制权。进程在切换时对计算机的资源的转换也是存在,频繁的切换进程也就是计算机在频繁的运作。而在运行进程时,计算机需要保持控制权,不能让进程无限制的运行接管机器,在保持控制权的同时也要有高性能,这就是计算机面临的难点。

2)进程之间如何切换,如何重获CPU的控制权?

在进程运行时,控制权在进程手上,在进程需要切换另一个进程时,切换的时候控制权就在操作系统手上。

协作方式:等待系统调用

只要当进程进行系统调用或者执行了某些非法操作时,控制权会转移给操作系统。但是这种方式很难应对一些程序的恶意行为。

非协作方式:操作系统进行控制

时钟中断:以固定的频率向cpu发送中断请求。防止某些恶意程序一直抢占控制权。当然时钟的中断频率和进程的切换频率的具体时间也会影响计算机的性能。

3)用户态和内核态是在什么情况下切换?

计算机一般处于用户态,只有当进行系统调用时,程序执行特殊的陷阱指令,该指令跳入内核并将特权级别提升到内核模式,进入内核模式后,系统执行相应的特权操作,从而为调用进程执行所需的工作,完成后操作系统调用特殊的从陷阱返回指令,然后回到发起调用的用户程序。

注:

  • 要初始化陷阱表。陷阱表时处理中断和异常处理程序的入口地址,操作系统启动时会让cpu记住陷阱表位置以供后续使用。
  • 在内核模式的例子,比如I/O输入输出就是位于内核态下,如果IO位于用户态,那么任何用户进程都可以随时向磁盘发出IO请求,如果IO处于特权态,那就只能在该模式时发出IO指令。
  • 计算机如何区分过程调用和系统调用?系统调用的部分是既定的汇编手工代码,这部分需要仔细遵循规定,以便正确处理参数和返回值并且执行硬件特定的陷阱指令。
  • 要明确陷阱表的位置也是一项特权指令,是在程序进入内核态后的第一件事情。
4)保护和恢复上下文

cpu获得控制权后,是继续运行当前进程还是切换下一个进程,是有调度程序决定的,如果要切换,就要执行上下文切换。

在计算机系统中,上下文是指进程或线程的执行环境,包括寄存器、内存、I/O 状态等。这是操作系统中非常重要的一个概念,因为它直接影响到系统的性能和响应速度。

在保护和恢复上下文的过程中,CPU 会将当前进程或线程的上下文信息保存到内存中,然后加载新进程或线程的上下文信息。这个过程需要保存和恢复大量的数据,包括 CPU 寄存器、内存、I/O 状态等。因此,上下文切换是一项非常耗费资源的操作。

在 Linux 系统中,保护和恢复上下文的过程主要是通过保存和恢复 CPU 寄存器来实现的。当一个进程或线程被挂起时,CPU 会将当前寄存器中的值保存到内存中;当它再次被调度时,CPU 会从内存中读取之前保存的值,并将其恢复到寄存器中。这个过程需要非常高效和精确的处理,以确保系统能够快速地响应用户请求。

4.进程的调度的发展历程,由简到繁。
1)两个看似矛盾的指标。
周转时间

周转时间指的是用任务完成的时间减去任务到达系统的时间。

第一种最为原始的策略是先进先出(FIFO)。也就是先到的程序先执行,后到的程序需要等到先到的程序执行完后再执行。这样问题就来了,加入先到的程序执行时间很长,那么紧接着到的程序就要等很长的时间,周转时间就变大了,这对计算机并不是件好事。这也成为计算机中的一个护航效应,一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。所以这个方案很难对计算机有很大的好处。

第二种是最短任务优先(SJF,shortest job first),这也貌似是个好方法,解决了先进先出中的重量级程序的问题,但其实并没有完全解决,只有在轻量级和重量级同时到达时,会让轻量级优先,要是轻量级晚了一点点,都是要等到重量级完成之后的,周转时间依旧很长。

现在有没有注意到,以上两种方案都属于非抢占式调度程序(non-preemptive),这样的系统会等每项工作做完,再去考虑新的工作。但是几乎现在所有的程序都是抢占式程序(preemptive)也就是会进行上下文切换,临时停止当前进程启动新的进程。

第三种是抢占式最短作业优先(PSJF,preemptive shortest job first)也可以被称为最短完成时间优先(SCJF,shortest time-to-completion job first)。也就是在新的工作进入系统时,会确定剩余工作和新的工作中谁的完成时间最少,然后调度该工作。

响应时间

响应时间指的是从任务到达系统到首次运行的时间。

这里采取的解决思路是轮转(Round-Robin,RR),也就是RR在一个时间片内运行,然后时间片结束后,切换下一个工作。时间片越短,响应时间越好,但是上下文切换的成本也就更高。这里出现了一种摊销(amortization)的方法,也就是设置更高的时间片时间,以减少在程序运行中上下文切换所消耗时间的比例。

这两种指标在各自的方法领域中可以达到不错的效果,但是很难同时拥有较好的周转时间和响应时间,并且我们也没有考虑到IO的因素,在程序进行IO时,不会占用CPU的控制权,即会到磁盘中运行,那次是CPU的控制权如果交给其它程序,那么能更好的提高系统的利用率,这种操作也叫重叠(overlap)。

2)多级反馈队列(Multi-level Feedback Queue,MLFQ)

要如何同时实现减少响应时间,减少周转时间,并且协调IO的调度程序?

MLFQ的调度策略存在自己的规则,也就是存在多个队列,每个队列有不同的优先级,一个工作只存在一个队列中,一个队列中可以有多个工作,MLFQ总是执行优先级较高的工作,因此它的关键在于如何设置它的优先级,接下来我们来看看它的基本规则。

规则1:如果A的优先级>B的优先级,运行A(不运行B)。

规则2:如果A的优先级=B的优先级,轮转运行A和B。

规则3:工作进入系统时,进入最高优先级(最上层队列)。

如果新的工作是短工作,那么就能先得到运行,符合减少周期时间。但如果是长工作,也会在运行一部分之后降到低优先级,不影响其它工作,同时规则5有力的保障了如果长工作仍然很多,不会滞留在低优先级。

规则4a:工作完整个时间片后,降低优先级(移入下一个队列)。

规则4b:如果工作在其时间片以内主动释放CPU,则优先级不变。

如果有IO操作时,也就是在时间片之内主动放弃CPU,那么不会影响它的优先级。交互型和短工作能够很好的在高优先级得到实现,长工作也能公平的共享cpu。

规则4(修订):一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。

在规则4ab中,如果主动释放之后再次进入,时间是会重新开始即使的,那么狡猾的程序如果设计主动释放CPU,以保持在高优先级,占用更多的CPU时间,那就大事不妙,所以修订的规则4中将时间修改成了时间配额的限制,以防止这种情况。

规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。

规则5避免了饥饿问题,也就是交互型工作太多,长工作滞留在低队列。

以上只是基础规则,如果涉及具体的调度操作,时间片的时间,交互的时间,进程的时间,上下文切换的时间,规则5中的时间……这些都需要一个合适的时间结果,已取得更好的调度结果,提高计算机的性能。

3)比例份额

比例份额调度(proportional-share)是一种调度程序,也被称为公平份额调度(fair-share)。它的目标是为每个进程分配一定比例的CPU使用时间,而不考虑周转时间和响应时间。比例份额调度有一个很优秀的例子,由Waldspurger和Weihl提出的彩票调度,顾名思义,就是让进程像彩票一样分配占用时间,哪个进程中奖就能获得更多的占用CPU时间,更越活越的进程,也就得到更多的抽奖机会。