文末领取大图。html
这不是一篇教你如何建立一个操做系统的文章,相反,这是一篇指导性文章,教你从几个方面来理解操做系统。首先你须要知道你为何要看这篇文章以及为何要学习操做系统。node
首先你要搞明白你学习操做系统的目的是什么?操做系统的重要性如何?学习操做系统会给我带来什么?下面我会从这几个方面为你回答下。程序员
操做系统也是一种软件,可是操做系统是一种很是复杂的软件。操做系统提供了几种抽象模型算法
这些抽象和咱们的平常开发息息相关。搞清楚了操做系统是如何抽象的,才能培养咱们的抽象性思惟和开发思路。shell
不少问题都和操做系统相关,操做系统是解决这些问题的基础。若是你不学习操做系统,可能会想着从框架层面来解决,那是你了解的还不够深刻,当你学习了操做系统后,可以培养你的全局性思惟。数据库
学习操做系统咱们可以有效的解决并发
问题,并发几乎是互联网的重中之重了,这也从侧面说明了学习操做系统的重要性。编程
学习操做系统的重点不是让你从头制造一个操做系统,而是告诉你操做系统是如何工做的,可以让你对计算机底层有所了解,打实你的基础。windows
相信你必定清楚什么是编程数组
Data structures + Algorithms = Programming缓存
操做系统内部会涉及到众多的数据结构和算法描述,可以让你了解算法的基础上,让你编写更优秀的程序。
我认为能够把计算机比做一栋楼
计算机的底层至关于就是楼的根基,计算机应用至关于就是楼的外形,而操做系统就至关因而告诉你大楼的构造原理,编写高质量的软件就至关因而告诉你构建一个稳定的房子。
在了解操做系统前,你须要先知道一下什么是计算机系统:现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口以及各类输入/输出设备构成的系统。这些都属于硬件
的范畴。咱们程序员不会直接和这些硬件打交道,而且每位程序员不可能会掌握全部计算机系统的细节。
因此计算机科学家在硬件的基础之上,安装了一层软件,这层软件可以根据用户输入的指令达到控制硬件的效果,从而知足用户的需求,这样的软件称为 操做系统
,它的任务就是为用户程序提供一个更好、更简单、更清晰的计算机模型。也就是说,操做系统至关因而一个中间层,为用户层和硬件提供各自的借口,屏蔽了不一样应用和硬件之间的差别,达到统一标准的做用。
上面一个操做系统的简化图,最底层是硬件,硬件包括芯片、电路板、磁盘、键盘、显示器等咱们上面提到的设备,在硬件之上是软件。大部分计算机有两种运行模式:内核态
和 用户态
,软件中最基础的部分是操做系统
,它运行在 内核态
中。操做系统具备硬件的访问权,能够执行机器可以运行的任何指令。软件的其他部分运行在 用户态
下。
在大概了解到操做系统以后,咱们先来认识一下硬件都有哪些
计算机硬件是计算机的重要组成部分,其中包含了 5 个重要的组成部分:运算器、控制器、存储器、输入设备、输出设备。
运算器
:运算器最主要的功能是对数据和信息进行加工和运算。它是计算机中执行算数和各类逻辑运算的部件。运算器的基本运算包括加、减、乘、除、移位等操做,这些是由 算术逻辑单元(Arithmetic&logical Unit)
实现的。而运算器主要由算数逻辑单元和寄存器构成。控制器
:指按照指定顺序改变主电路或控制电路的部件,它主要起到了控制命令执行的做用,完成协调和指挥整个计算机系统的操做。控制器是由程序计数器、指令寄存器、解码译码器等构成。运算器和控制器共同组成了 CPU
存储器
:存储器就是计算机的记忆设备
,顾名思义,存储器能够保存信息。存储器分为两种,一种是主存,也就是内存,它是 CPU 主要交互对象,还有一种是外存,好比硬盘软盘等。下面是现代计算机系统的存储架构
输入设备
:输入设备是给计算机获取外部信息的设备,它主要包括键盘和鼠标。
输出设备
:输出设备是给用户呈现根据输入设备获取的信息通过一系列的计算后获得显示的设备,它主要包括显示器、打印机等。
这五部分也是冯诺伊曼的体系结构,它认为计算机必须具备以下功能:
把须要的程序和数据送至计算机中。必须具备长期记忆程序、数据、中间结果及最终运算结果的能力。可以完成各类算术、逻辑运算和数据传送等数据加工处理的能力。可以根据须要控制程序走向,并能根据指令控制机器的各部件协调操做。可以按照要求将处理结果输出给用户。
下面是一张 intel 家族产品图,是一个详细的计算机硬件分类,咱们在根据图中涉及到硬件进行介绍
总线(Buses)
:在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。一般总线被设计成传送定长的字节块,也就是 字(word)
。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。如今大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。I/O 设备(I/O Devices)
:Input/Output 设备是系统和外部世界的链接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。
每一个I/O 设备链接 I/O 总线都被称为控制器(controller)
或者是 适配器(Adapter)
。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备自己或者系统的主印制板电路(一般称做主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。不管组织形式如何,它们的最终目的都是彼此交换信息。
主存(Main Memory)
,主存是一个临时存储设备
,而不是永久性存储,磁盘是 永久性存储
的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory)
动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它惟一的地址编号,从 0 开始。通常来讲,组成程序的每条机器指令都由不一样数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。好比,在 Linux 的 x86-64 机器上,short 类型的数据须要 2 个字节,int 和 float 须要 4 个字节,而 long 和 double 须要 8 个字节。
处理器(Processor)
,CPU(central processing unit)
或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)
。在任什么时候刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。
从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操做。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操做,而后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(好比 jmp 指令就不会顺序读取)
下面是 CPU 可能执行简单操做的几个步骤
加载(Load)
:从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容存储(Store)
:将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容操做(Operate)
:把两个寄存器的内容复制到 ALU(Arithmetic logic unit)
。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。
跳转(jump)
:从指令中抽取一个字,把这个字复制到程序计数器(PC)
中,覆盖原来的值关于进程和线程,你须要理解下面这张脑图中的重点
操做系统中最核心的概念就是 进程
,进程是对正在运行中的程序的一个抽象。操做系统的其余全部内容都是围绕着进程展开的。
在多道程序处理的系统中,CPU 会在进程
间快速切换,使每一个程序运行几十或者几百毫秒。然而,严格意义来讲,在某一个瞬间,CPU 只能运行一个进程,然而咱们若是把时间定位为 1 秒内的话,它可能运行多个进程。这样就会让咱们产生并行
的错觉。由于 CPU 执行速度很快,进程间的换进换出也很是迅速,所以咱们很难对多个并行进程进行跟踪。因此,操做系统的设计者开发了用于描述并行的一种概念模型(顺序进程),使得并行更加容易理解和分析。
一个进程就是一个正在执行的程序的实例,进程也包括程序计数器、寄存器和变量的当前值。从概念上来讲,每一个进程都有各自的虚拟 CPU,可是实际状况是 CPU 会在各个进程之间进行来回切换。
如上图所示,这是一个具备 4 个程序的多道处理程序,在进程不断切换的过程当中,程序计数器也在不一样的变化。
在上图中,这 4 道程序被抽象为 4 个拥有各自控制流程(即每一个本身的程序计数器)的进程,而且每一个程序都独立的运行。固然,实际上只有一个物理程序计数器,每一个程序要运行时,其逻辑程序计数器会装载到物理程序计数器中。当程序运行结束后,其物理程序计数器就会是真正的程序计数器,而后再把它放回进程的逻辑计数器中。
从下图咱们能够看到,在观察足够长的一段时间后,全部的进程都运行了,但在任何一个给定的瞬间仅有一个进程真正运行。
所以,当咱们说一个 CPU 只能真正一次运行一个进程的时候,即便有 2 个核(或 CPU),每个核也只能一次运行一个线程。
因为 CPU 会在各个进程之间来回快速切换,因此每一个进程在 CPU 中的运行时间是没法肯定的。而且当同一个进程再次在 CPU 中运行时,其在 CPU 内部的运行时间每每也是不固定的。
这里的关键思想是认识到一个进程所需的条件
,进程是某一类特定活动的总和,它有程序、输入输出以及状态。
操做系统须要一些方式来建立进程。下面是一些建立进程的方式
从技术上讲,在全部这些状况下,让现有流程执行流程是经过建立系统调用来建立新流程的。该进程多是正在运行的用户进程,是从键盘或鼠标调用的系统进程或批处理程序。这些就是系统调用建立新进程的过程。该系统调用告诉操做系统建立一个新进程,并直接或间接指示在其中运行哪一个程序。
在 UNIX 中,仅有一个系统调用来建立一个新的进程,这个系统调用就是 fork
。这个调用会建立一个与调用进程相关的副本。在 fork 后,一个父进程和子进程会有相同的内存映像
,相同的环境字符串和相同的打开文件。
在 Windows 中,状况正相反,一个简单的 Win32 功能调用 CreateProcess
,会处理流程建立并将正确的程序加载到新的进程中。这个调用会有 10 个参数,包括了须要执行的程序、输入给程序的命令行参数、各类安全属性、有关打开的文件是否继承控制位、优先级信息、进程所须要建立的窗口规格以及指向一个结构的指针,在该结构中新建立进程的信息被返回给调用者。在 Windows 中,从一开始父进程的地址空间和子进程的地址空间就是不一样的。
进程在建立以后,它就开始运行并作完成任务。然而,没有什么事儿是永不停歇的,包括进程也同样。进程迟早会发生终止,可是一般是因为如下状况触发的
正常退出(自愿的)
: 多数进程是因为完成了工做而终止。当编译器完成了所给定程序的编译以后,编译器会执行一个系统调用告诉操做系统它完成了工做。这个调用在 UNIX 中是 exit
,在 Windows 中是 ExitProcess
。错误退出(自愿的)
:好比执行一条不存在的命令,因而编译器就会提醒并退出。严重错误(非自愿的)
被其余进程杀死(非自愿的)
: 某个进程执行系统调用告诉操做系统杀死某个进程。在 UNIX 中,这个系统调用是 kill。在 Win32 中对应的函数是 TerminateProcess
(注意不是系统调用)。在一些系统中,当一个进程建立了其余进程后,父进程和子进程就会以某种方式进行关联。子进程它本身就会建立更多进程,从而造成一个进程层次结构。
在 UNIX 中,进程和它的全部子进程以及子进程的子进程共同组成一个进程组。当用户从键盘中发出一个信号后,该信号被发送给当前与键盘相关的进程组中的全部成员(它们一般是在当前窗口建立的全部活动进程)。每一个进程能够分别捕获该信号、忽略该信号或采起默认的动做,即被信号 kill 掉。整个操做系统中全部的进程都隶属于一个单个以 init 为根的进程树。
相反,Windows 中没有进程层次的概念,Windows 中全部进程都是平等的,惟一相似于层次结构的是在建立进程的时候,父进程获得一个特别的令牌(称为句柄),该句柄能够用来控制子进程。然而,这个令牌可能也会移交给别的操做系统,这样就不存在层次结构了。而在 UNIX 中,进程不能剥夺其子进程的 进程权
。(这样看来,仍是 Windows 比较渣
)。
尽管每一个进程是一个独立的实体,有其本身的程序计数器和内部状态,可是,进程之间仍然须要相互帮助。当一个进程开始运行时,它可能会经历下面这几种状态
图中会涉及三种状态
运行态
,运行态指的就是进程实际占用 CPU 时间片运行时就绪态
,就绪态指的是可运行,但由于其余进程正在运行而处于就绪状态阻塞态
,除非某种外部事件发生,不然进程不能运行操做系统为了执行进程间的切换,会维护着一张表,这张表就是 进程表(process table)
。每一个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配情况、所打开文件的状态、帐号和调度信息,以及其余在进程由运行态转换到就绪态或阻塞态时所必须保存的信息。
下面展现了一个典型系统中的关键字段
第一列内容与进程管理
有关,第二列内容与 存储管理
有关,第三列内容与文件管理
有关。
如今咱们应该对进程表有个大体的了解了,就能够在对单个 CPU 上如何运行多个顺序进程的错觉作更多的解释。与每一 I/O 类相关联的是一个称做 中断向量(interrupt vector)
的位置(靠近内存底部的固定区域)。它包含中断服务程序的入口地址。假设当一个磁盘中断发生时,用户进程 3 正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。这就是硬件所作的事情。而后软件就随即接管一切剩余的工做。
当中断结束后,操做系统会调用一个 C 程序来处理中断剩下的工做。在完成剩下的工做后,会使某些进程就绪,接着调用调度程序,决定随后运行哪一个进程。而后将控制权转移给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行,下面显示了中断处理和调度的过程。
硬件压入堆栈程序计数器等
硬件从中断向量装入新的程序计数器
汇编语言过程保存寄存器的值
汇编语言过程设置新的堆栈
C 中断服务器运行(典型的读和缓存写入)
调度器决定下面哪一个程序先运行
C 过程返回至汇编代码
汇编语言过程开始运行新的当前进程
一个进程在执行过程当中可能被中断数千次,但关键每次中断后,被中断的进程都返回到与中断发生前彻底相同的状态。
在传统的操做系统中,每一个进程都有一个地址空间和一个控制线程。事实上,这是大部分进程的定义。不过,在许多状况下,常常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程。下面咱们就着重探讨一下什么是线程
或许这个疑问也是你的疑问,为何要在进程的基础上再建立一个线程的概念,准确的说,这实际上是进程模型和线程模型的讨论,回答这个问题,可能须要分三步来回答
更轻量级
,因为线程更轻,因此它比进程更容易建立,也更容易撤销。在许多系统中,建立一个线程要比建立一个进程快 10 - 100 倍。进程中拥有一个执行的线程,一般简写为 线程(thread)
。线程会有程序计数器,用来记录接着要执行哪一条指令;线程实际上 CPU 上调度执行的实体。
下图咱们能够看到三个传统的进程,每一个进程有本身的地址空间和单个控制线程。每一个线程都在不一样的地址空间中运行
下图中,咱们能够看到有一个进程三个线程的状况。每一个线程都在相同的地址空间中运行。
线程不像是进程那样具有较强的独立性。同一个进程中的全部线程都会有彻底同样的地址空间,这意味着它们也共享一样的全局变量。因为每一个线程均可以访问进程地址空间内每一个内存地址,所以一个线程能够读取、写入甚至擦除另外一个线程的堆栈。线程之间除了共享同一内存空间外,还具备以下不一样的内容
上图左边的是同一个进程中每一个线程共享
的内容,上图右边是每一个线程
中的内容。也就是说左边的列表是进程的属性,右边的列表是线程的属性。
线程之间的状态转换和进程之间的状态转换是同样的。
每一个线程都会有本身的堆栈,以下图所示
进程一般会从当前的某个单线程开始,而后这个线程经过调用一个库函数(好比 thread_create
)建立新的线程。线程建立的函数会要求指定新建立线程的名称。建立的线程一般都返回一个线程标识符,该标识符就是新线程的名字。
当一个线程完成工做后,能够经过调用一个函数(好比 thread_exit
)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程当中,能够经过调用函数例如 thread_join
,表示一个线程能够等待另外一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种状况下,线程的建立和终止很是相似于进程的建立和终止。
另外一个常见的线程是调用 thread_yield
,它容许线程自动放弃 CPU 从而让另外一个线程运行。这样一个调用仍是很重要的,由于不一样于进程,线程是没法利用时钟中断强制让线程让出 CPU 的。
POSIX 线程 一般称为 pthreads
是一种独立于语言而存在的执行模型,以及并行执行模型。
它容许程序控制时间上重叠的多个不一样的工做流程。每一个工做流程都称为一个线程,能够经过调用 POSIX Threads API 来实现对这些流程的建立和控制。能够把它理解为线程的标准。
POSIX Threads 的实如今许多相似且符合POSIX的操做系统上可用,例如 FreeBSD、NetBSD、OpenBSD、Linux、macOS、Android、Solaris,它在现有 Windows API 之上实现了pthread。
IEEE 是世界上最大的技术专业组织,致力于为人类的利益而发展技术。
线程调用 | 描述 |
---|---|
pthread_create | 建立一个新线程 |
pthread_exit | 结束调用的线程 |
pthread_join | 等待一个特定的线程退出 |
pthread_yield | 释放 CPU 来运行另一个线程 |
pthread_attr_init | 建立并初始化一个线程的属性结构 |
pthread_attr_destory | 删除一个线程的属性结构 |
全部的 Pthreads 都有特定的属性,每个都含有标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这个属性包括堆栈大小、调度参数以及其余线程须要的项目。
主要有三种实现方式
下面咱们分开讨论一下
第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。全部的这类实现都有一样的通用结构
线程在运行时系统之上运行,运行时系统是管理线程过程的集合,包括前面提到的四个过程: pthread_create, pthread_exit, pthread_join 和 pthread_yield。
当某个线程但愿建立一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用经过对线程表的更新来完成线程建立或销毁工做。
内核中的线程表持有每一个线程的寄存器、状态和其余信息。这些信息和用户空间中的线程信息相同,可是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。
全部可以阻塞的调用都会经过系统调用的方式来实现,当一个线程阻塞时,内核能够进行选择,是运行在同一个进程中的另外一个线程(若是有就绪线程的话)仍是运行一个另外一个进程中的线程。可是在用户实现中,运行时系统始终运行本身的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。
结合用户空间和内核空间的优势,设计人员采用了一种内核级线程
的方式,而后将用户级线程与某些或者所有内核线程多路复用起来
在这种模型中,编程人员能够自由控制用户线程和内核线程的数量,具备很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。
进程是须要频繁的和其余进程进行交流的。下面咱们会一块儿讨论有关 进程间通讯(Inter Process Communication, IPC)
的问题。大体来讲,进程间的通讯机制能够分为 6 种
下面咱们分别对其进行概述
信号是 UNIX 系统最早开始使用的进程间通讯机制,由于 Linux 是继承于 UNIX 的,因此 Linux 也支持信号机制,经过向一个或多个进程发送异步事件信号
来实现,信号能够从键盘或者访问不存在的位置等地方产生;信号经过 shell 将任务发送给子进程。
你能够在 Linux 系统上输入 kill -l
来列出系统使用的信号,下面是我提供的一些信号
进程能够选择忽略发送过来的信号,可是有两个是不能忽略的:SIGSTOP
和 SIGKILL
信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操做,SIGKILL 信号会通知当前进程应该被杀死。除此以外,进程能够选择它想要处理的信号,进程也能够选择阻止信号,若是不阻止,能够选择自行处理,也能够选择进行内核处理。若是选择交给内核进行处理,那么就执行默认处理。
操做系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行均可以中断,若是进程已经注册了新号处理程序,那么就执行进程,若是没有注册,将采用默认处理的方式。
Linux 系统中的进程能够经过创建管道 pipe 进行通讯
在两个进程之间,能够创建一个通道,一个进程向这个通道里写入字节流,另外一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的管线 pipelines
就是用管道实现的,当 shell 发现输出
sort <f | head
它会建立两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间创建一个管道使得 sort 进程的标准输出做为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,若是管道满了系统会中止 sort 以等待 head 读出数据
管道实际上就是 |
,两个应用程序不知道有管道的存在,一切都是由 shell 管理和控制的。
两个进程之间还能够经过共享内存进行进程间通讯,其中两个或者多个进程能够访问公共内存空间。两个进程的共享工做是经过共享内存完成的,一个进程所做的修改能够对另外一个进程可见(很像线程间的通讯)。
在使用共享内存前,须要通过一系列的调用流程,流程以下
(shmget())
(shmat())
(shmdt())
(shmctl())
先入先出队列 FIFO 一般被称为 命名管道(Named Pipes)
,命名管道的工做方式与常规管道很是类似,可是确实有一些明显的区别。未命名的管道没有备份文件:操做系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具备支持文件和独特 API ,命名管道在文件系统中做为设备的专用文件存在。当全部的进程通讯完成后,命名管道将保留在文件系统中以备后用。命名管道具备严格的 FIFO 行为
写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。
一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部连接列表。能够按几种不一样的方式将消息按顺序发送到队列并从队列中检索消息。每一个消息队列由 IPC 标识符惟一标识。消息队列有两种模式,一种是严格模式
, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 非严格模式
,消息的顺序性不是很是重要。
还有一种管理两个进程间通讯的是使用 socket
,socket 提供端到端的双相通讯。一个套接字能够与一个或多个进程关联。就像管道有命令管道和未命名管道同样,套接字也有两种模式,套接字通常用于两个进程之间的网络通讯,网络套接字须要来自诸如TCP(传输控制协议)
或较低级别UDP(用户数据报协议)
等基础协议的支持。
套接字有如下几种分类
顺序包套接字(Sequential Packet Socket)
: 此类套接字为最大长度固定的数据报提供可靠的链接。此链接是双向的而且是顺序的。数据报套接字(Datagram Socket)
:数据包套接字支持双向数据流。数据包套接字接受消息的顺序与发送者可能不一样。流式套接字(Stream Socket)
:流套接字的工做方式相似于电话对话,提供双向可靠的数据流。原始套接字(Raw Socket)
: 能够使用原始套接字访问基础通讯协议。当一个计算机是多道程序设计系统时,会频繁的有不少进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时,就会发生这种状况。若是只有一个 CPU 可用,那么必须选择接下来哪一个进程/线程能够运行。操做系统中有一个叫作 调度程序(scheduler)
的角色存在,它就是作这件事儿的,该程序使用的算法叫作 调度算法(scheduling algorithm)
。
毫无疑问,不一样的环境下须要不一样的调度算法。之因此出现这种状况,是由于不一样的应用程序和不一样的操做系统有不一样的目标。也就是说,在不一样的系统中,调度程序的优化也是不一样的。这里有必要划分出三种环境
批处理(Batch)
: 商业领域交互式(Interactive)
: 交互式用户环境实时(Real time)
如今让咱们把目光从通常性的调度转换为特定的调度算法。下面咱们会探讨在批处理中的调度。
最简单的非抢占式调度算法的设计就是 先来先服务(first-come,first-serverd)
。当第一个任务从外部进入系统时,将会当即启动并容许运行任意长的时间。它不会由于运行时间太长而中断。当其余做业进入时,它们排到就绪队列尾部。当正在运行的进程阻塞,处于等待队列的第一个进程就开始运行。当一个阻塞的进程从新处于就绪态时,它会像一个新到达的任务,会排在队列的末尾,即排在全部进程最后。
这个算法的强大之处在于易于理解和编程,在这个算法中,一个单链表记录了全部就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程便可;要添加一个新的做业或者阻塞一个进程,只要把这个做业或进程附加在队列的末尾便可。这是很简单的一种实现。
批处理中,第二种调度算法是 最短做业优先(Shortest Job First)
,咱们假设运行时间已知。例如,一家保险公司,由于天天要作相似的工做,因此人们能够至关精确地预测处理 1000 个索赔的一批做业须要多长时间。当输入队列中有若干个同等重要的做业被启动时,调度程序应使用最短优先做业算法
须要注意的是,在全部的进程均可以运行的状况下,最短做业优先的算法才是最优的。
最短做业优先的抢占式版本被称做为 最短剩余时间优先(Shortest Remaining Time Next)
算法。使用这个算法,调度程序老是选择剩余运行时间最短的那个进程运行。
交互式系统中在我的计算机、服务器和其余系统中都是很经常使用的,因此有必要来探讨一下交互式调度
一种最古老、最简单、最公平而且最普遍使用的算法就是 轮询算法(round-robin)
。每一个进程都会被分配一个时间段,称为时间片(quantum)
,在这个时间片内容许进程运行。若是时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另外一个进程。若是进程在时间片结束前阻塞或结束,则 CPU 当即进行切换。轮询算法比较容易实现。调度程序所作的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾,就像下图的 b。
轮询调度假设了全部的进程是同等重要的。但事实状况可能不是这样。例如,在一所大学中的等级制度,首先是院长,而后是教授、秘书、后勤人员,最后是学生。这种将外部状况考虑在内就实现了优先级调度(priority scheduling)
它的基本思想很明确,每一个进程都被赋予一个优先级,优先级高的进程优先运行。
最先使用优先级调度的系统是 CTSS(Compatible TimeSharing System)
。CTSS 在每次切换前都须要将当前进程换出到磁盘,并从磁盘上读入一个新进程。为 CPU 密集型进程设置较长的时间片比频繁地分给他们很短的时间要更有效(减小交换次数)。另外一方面,如前所述,长时间片的进程又会影响到响应时间,解决办法是设置优先级类。属于最高优先级的进程运行一个时间片,次高优先级进程运行 2 个时间片,再下面一级运行 4 个时间片,以此类推。当一个进程用完分配的时间片后,它被移到下一类。
最短进程优先是根据进程过去的行为进行推测,并执行估计运行时间最短的那一个。假设每一个终端上每条命令的预估运行时间为 T0
,如今假设测量到其下一次运行时间为 T1
,能够用两个值的加权来改进估计时间,即aT0+ (1- 1)T1
。经过选择 a 的值,能够决定是尽快忘掉老的运行时间,仍是在一段长时间内始终记住它们。当 a = 1/2 时,能够获得下面这个序列

能够看到,在三轮事后,T0 在新的估计值中所占比重降低至 1/8。
一种彻底不一样的调度方法是对用户作出明确的性能保证。一种实际并且容易实现的保证是:若用户工做时有 n 个用户登陆,则每一个用户将得到 CPU 处理能力的 1/n。相似地,在一个有 n 个进程运行的单用户系统中,若全部的进程都等价,则每一个进程将得到 1/n 的 CPU 时间。
对用户进行承诺并在随后兑现承诺是一件好事,不过很难实现。可是存在着一种简单的方式,有一种既能够给出预测结果而又有一种比较简单的实现方式的算法,就是 彩票调度(lottery scheduling)
算法。
其基本思想是为进程提供各类系统资源(例如 CPU 时间)的彩票。当作出一个调度决策的时候,就随机抽出一张彩票,拥有彩票的进程将得到该资源。在应用到 CPU 调度时,系统能够每秒持有 50 次抽奖,每一个中奖者将得到好比 20 毫秒的 CPU 时间做为奖励。
到目前为止,咱们假设被调度的都是各个进程自身,而不用考虑该进程的拥有者是谁。结果是,若是用户 1 启动了 9 个进程,而用户 2 启动了一个进程,使用轮转或相同优先级调度算法,那么用户 1 将获得 90 % 的 CPU 时间,而用户 2 将之获得 10 % 的 CPU 时间。
为了阻止这种状况的出现,一些系统在调度前会把进程的拥有者考虑在内。在这种模型下,每一个用户都会分配一些CPU 时间,而调度程序会选择进程并强制执行。所以若是两个用户每一个都会有 50% 的 CPU 时间片保证,那么不管一个用户有多少个进程,都将得到相同的 CPU 份额。
实时系统(real-time)
是一个时间扮演了重要做用的系统。实时系统能够分为两类,硬实时(hard real time)
和 软实时(soft real time)
系统,前者意味着必需要知足绝对的截止时间;后者的含义是虽然不但愿偶尔错失截止时间,可是能够容忍。
实时系统中的事件能够按照响应方式进一步分类为周期性(以规则的时间间隔发生)
事件或 非周期性(发生时间不可预知)
事件。一个系统可能要响应多个周期性事件流,根据每一个事件处理所需的时间,可能甚至没法处理全部事件。例如,若是有 m 个周期事件,事件 i 以周期 Pi 发生,并须要 Ci 秒 CPU 时间处理一个事件,那么能够处理负载的条件是
只有知足这个条件的实时系统称为可调度的
,这意味着它实际上可以被实现。一个不知足此检验标准的进程不能被调度,由于这些进程共同须要的 CPU 时间总和大于 CPU 能提供的时间。
下面咱们来了解一下内存管理,你须要知道的知识点以下
若是要使多个应用程序同时运行在内存中,必需要解决两个问题:保护
和 重定位
。第一种解决方式是用保护密钥标记内存块
,并将执行过程的密钥与提取的每一个存储字的密钥进行比较。这种方式只能解决第一种问题(破坏操做系统),可是不能解决多进程在内存中同时运行的问题。
还有一种更好的方式是创造一个存储器抽象:地址空间(the address space)
。就像进程的概念建立了一种抽象的 CPU 来运行程序,地址空间也建立了一种抽象内存供程序使用。
最简单的办法是使用动态重定位(dynamic relocation)
技术,它就是经过一种简单的方式将每一个进程的地址空间映射到物理内存的不一样区域。还有一种方式是使用基址寄存器和变址寄存器。
每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将基址值
添加到进程生成的地址中,而后再将其发送到内存总线上。同时,它检查程序提供的地址是否大于或等于变址寄存器
中的值。若是程序提供的地址要超过变址寄存器的范围,那么会产生错误并停止访问。
在程序运行过程当中,常常会出现内存不足的问题。
针对上面内存不足的问题,提出了两种处理方式:最简单的一种方式就是交换(swapping)
技术,即把一个进程完整的调入内存,而后再内存中运行一段时间,再把它放回磁盘。空闲进程会存储在磁盘中,因此这些进程在没有运行时不会占用太多内存。另一种策略叫作虚拟内存(virtual memory)
,虚拟内存技术可以容许应用程序部分的运行在内存中。下面咱们首先先探讨一下交换
下面是一个交换过程
刚开始的时候,只有进程 A 在内存中,而后从建立进程 B 和进程 C 或者从磁盘中把它们换入内存,而后在图 d 中,A 被换出内存到磁盘中,最后 A 从新进来。由于图 g 中的进程 A 如今到了不一样的位置,因此在装载过程当中须要被从新定位,或者在交换程序时经过软件来执行;或者在程序执行期间经过硬件来重定位。基址寄存器和变址寄存器就适用于这种状况。
交换在内存建立了多个 空闲区(hole)
,内存会把全部的空闲区尽量向下移动合并成为一个大的空闲区。这项技术称为内存紧缩(memory compaction)
。可是这项技术一般不会使用,由于这项技术会消耗不少 CPU 时间。
在进行内存动态分配时,操做系统必须对其进行管理。大体上说,有两种监控内存使用的方式
位图(bitmap)
空闲列表(free lists)
使用位图方法时,内存可能被划分为小到几个字或大到几千字节的分配单元。每一个分配单元对应于位图中的一位,0 表示空闲, 1 表示占用(或者相反)。一块内存区域和其对应的位图以下
位图
提供了一种简单的方法在固定大小的内存中跟踪内存的使用状况,由于位图的大小取决于内存和分配单元的大小。这种方法有一个问题是,当决定为把具备 k 个分配单元的进程放入内存时,内容管理器(memory manager)
必须搜索位图,在位图中找出可以运行 k 个连续 0 位的串。在位图中找出制定长度的连续 0 串是一个很耗时的操做,这是位图的缺点。(能够简单理解为在杂乱无章的数组中,找出具备一大长串空闲的数组单元)
另外一种记录内存使用状况的方法是,维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲区域。可用上面的图 c 来表示内存的使用状况。链表中的每一项均可以表明一个 空闲区(H)
或者是进程(P)
的起始标志,长度和下一个链表项的位置。
当按照地址顺序在链表中存放进程和空闲区时,有几种算法能够为建立的进程(或者从磁盘中换入的进程)分配内存。咱们先假设内存管理器知道应该分配多少内存,最简单的算法是使用 首次适配(first fit)
。内存管理器会沿着段列表进行扫描,直到找个一个足够大的空闲区为止。 除非空闲区大小和要分配的空间大小同样,不然将空闲区分为两部分,一部分供进程使用;一部分生成新的空闲区。首次适配算法是一种速度很快的算法,由于它会尽量的搜索链表。
首次适配的一个小的变体是 下次适配(next fit)
。它和首次匹配的工做方式相同,只有一个不一样之处那就是下次适配在每次找到合适的空闲区时就会记录当时的位置,以便下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次匹配算法那样每次都会从头开始搜索。
另一个著名的而且普遍使用的算法是 最佳适配(best fit)
。最佳适配会从头至尾寻找整个链表,找出可以容纳进程的最小空闲区。
尽管基址寄存器和变址寄存器用来建立地址空间的抽象,可是这有一个其余的问题须要解决:管理软件的不断增大(managing bloatware)
。虚拟内存的基本思想是,每一个程序都有本身的地址空间,这个地址空间被划分为多个称为页面(page)
的块。每一页都是连续的地址范围。这些页被映射到物理内存,但并非全部的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,硬件会马上执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操做系统负责将缺失的部分装入物理内存并从新执行失败的指令。
大部分使用虚拟内存的系统中都会使用一种 分页(paging)
技术。在任何一台计算机上,程序会引用使用一组内存地址。当程序执行
MOV REG,1000
这条指令时,它会把内存地址为 1000 的内存单元的内容复制到 REG 中(或者相反,这取决于计算机)。地址能够经过索引、基址寄存器、段寄存器或其余方式产生。
这些程序生成的地址被称为 虚拟地址(virtual addresses)
并造成虚拟地址空间(virtual address space)
,在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存中线上,读写操做都使用一样地址的物理内存。在使用虚拟内存时,虚拟地址不会直接发送到内存总线上。相反,会使用 MMU(Memory Management Unit)
内存管理单元把虚拟地址映射为物理内存地址,像下图这样
下面这幅图展现了这种映射是如何工做的
页表给出虚拟地址与物理内存地址之间的映射关系。每一页起始于 4096 的倍数位置,结束于 4095 的位置,因此 4K 到 8K 实际为 4096 - 8191 ,8K - 12K 就是 8192 - 12287
在这个例子中,咱们可能有一个 16 位地址的计算机,地址从 0 - 64 K - 1,这些是虚拟地址
。然而只有 32 KB 的物理地址。因此虽然能够编写 64 KB 的程序,可是程序没法所有调入内存运行,在磁盘上必须有一个最多 64 KB 的程序核心映像的完整副本,以保证程序片断在须要时被调入内存。
虚拟页号可做为页表的索引用来找到虚拟页中的内容。由页表项能够找到页框号(若是有的话)。而后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,造成物理地址。
所以,页表的目的是把虚拟页映射到页框中。从数学上说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。
经过这个函数能够把虚拟地址中的虚拟页转换为页框,从而造成物理地址。
下面咱们探讨一下页表项的具体结构,上面你知道了页表项的大体构成,是由页框号和在/不在位构成的,如今咱们来具体探讨一下页表项的构成
页表项的结构是与机器相关的,可是不一样机器上的页表项大体相同。上面是一个页表项的构成,不一样计算机的页表项可能不一样,可是通常来讲都是 32 位的。页表项中最重要的字段就是页框号(Page frame number)
。毕竟,页表到页框最重要的一步操做就是要把此值映射过去。下一个比较重要的就是在/不在
位,若是此位上的值是 1,那么页表项是有效的而且可以被使用
。若是此值是 0 的话,则表示该页表项对应的虚拟页面不在
内存中,访问该页面会引发一个缺页异常(page fault)
。
保护位(Protection)
告诉咱们哪种访问是容许的,啥意思呢?最简单的表示形式是这个域只有一位,0 表示可读可写,1 表示的是只读。
修改位(Modified)
和 访问位(Referenced)
会跟踪页面的使用状况。当一个页面被写入时,硬件会自动的设置修改位。修改位在页面从新分配页框时颇有用。若是一个页面已经被修改过(即它是 脏
的),则必须把它写回磁盘。若是一个页面没有被修改过(即它是 干净
的),那么从新分配时这个页框会被直接丢弃,由于磁盘上的副本仍然是有效的。这个位有时也叫作 脏位(dirty bit)
,由于它反映了页面的状态。
访问位(Referenced)
在页面被访问时被设置,无论是读仍是写。这个值可以帮助操做系统在发生缺页中断时选择要淘汰的页。再也不使用的页要比正在使用的页更适合被淘汰。这个位在后面要讨论的页面置换
算法中做用很大。
最后一位用于禁止该页面被高速缓存,这个功能对于映射到设备寄存器仍是内存中起到了关键做用。经过这一位能够禁用高速缓存。具备独立的 I/O 空间而不是用内存映射 I/O 的机器来讲,并不须要这一位。
下面咱们就来探讨一下有哪些页面置换算法。
最优的页面置换算法的工做流程以下:在缺页中断发生时,这些页面之一将在下一条指令(包含该指令的页面)上被引用。其余页面则可能要到 十、100 或者 1000 条指令后才会被访问。每一个页面均可以用在该页首次被访问前所要执行的指令数做为标记。
最优化的页面算法代表应该标记最大的页面。若是一个页面在 800 万条指令内不会被使用,另一个页面在 600 万条指令内不会被使用,则置换前一个页面,从而把须要调入这个页面而发生的缺页中断推迟。计算机也像人类同样,会把不肯意作的事情尽量的日后拖。
这个算法最大的问题时没法实现。当缺页中断发生时,操做系统没法知道各个页面的下一次将在何时被访问。这种算法在实际过程当中根本不会使用。
为了可以让操做系统收集页面使用信息,大部分使用虚拟地址的计算机都有两个状态位,R 和 M,来和每一个页面进行关联。每当引用页面(读入或写入)时都设置 R,写入(即修改)页面时设置 M,这些位包含在每一个页表项中,就像下面所示
由于每次访问时都会更新这些位,所以由硬件
来设置它们很是重要。一旦某个位被设置为 1,就会一直保持 1 直到操做系统下次来修改此位。
若是硬件没有这些位,那么能够使用操做系统的缺页中断
和时钟中断
机制来进行模拟。当启动一个进程时,将其全部的页面都标记为不在内存
;一旦访问任何一个页面就会引起一次缺页中断,此时操做系统就能够设置 R 位(在它的内部表中)
,修改页表项使其指向正确的页面,并设置为 READ ONLY
模式,而后从新启动引发缺页中断的指令。若是页面随后被修改,就会发生另外一个缺页异常。从而容许操做系统设置 M 位并把页面的模式设置为 READ/WRITE
。
能够用 R 位和 M 位来构造一个简单的页面置换算法:当启动一个进程时,操做系统将其全部页面的两个位都设置为 0。R 位按期的被清零(在每一个时钟中断)。用来将最近未引用的页面和已引用的页面分开。
当出现缺页中断后,操做系统会检查全部的页面,并根据它们的 R 位和 M 位将当前值分为四类:
尽管看起来好像没法实现第一类页面,可是当第三类页面的 R 位被时钟中断清除时,它们就会发生。时钟中断不会清除 M 位,由于须要这个信息才能知道是否写回磁盘中。清除 R 但不清除 M 会致使出现一类页面。
NRU(Not Recently Used)
算法从编号最小的非空类中随机删除一个页面。此算法隐含的思想是,在一个时钟内(约 20 ms)淘汰一个已修改可是没有被访问的页面要比一个大量引用的未修改页面好,NRU 的主要优势是易于理解而且可以有效的实现。
另外一种开销较小的方式是使用 FIFO(First-In,First-Out)
算法,这种类型的数据结构也适用在页面置换算法中。由操做系统维护一个全部在当前内存中的页面的链表,最先进入的放在表头,最新进入的页面放在表尾。在发生缺页异常时,会把头部的页移除而且把新的页添加到表尾。
咱们上面学到的 FIFO 链表页面有个缺陷
,那就是出链和入链并不会进行 check 检查
,这样就会容易把常用的页面置换出去,为了不这一问题,咱们对该算法作一个简单的修改:咱们检查最老页面的 R 位
,若是是 0 ,那么这个页面就是最老的并且没有被使用,那么这个页面就会被马上换出。若是 R 位是 1,那么就清除此位,此页面会被放在链表的尾部,修改它的装入时间就像刚放进来的同样。而后继续搜索。
这种算法叫作 第二次机会(second chance)
算法,就像下面这样,咱们看到页面 A 到 H 保留在链表中,并按到达内存的时间排序。
a)按照先进先出的方法排列的页面;b)在时刻 20 处发生缺页异常中断而且 A 的 R 位已经设置时的页面链表。
假设缺页异常发生在时刻 20 处,这时最老的页面是 A ,它是在 0 时刻到达的。若是 A 的 R 位是 0,那么它将被淘汰出内存,或者把它写回磁盘(若是它已经被修改过),或者只是简单的放弃(若是它是未被修改过)。另外一方面,若是它的 R 位已经设置了,则将 A 放到链表的尾部而且从新设置装入时间
为当前时刻(20 处),而后清除 R 位。而后从 B 页面开始继续搜索合适的页面。
寻找第二次机会的是在最近的时钟间隔中未被访问过的页面。若是全部的页面都被访问过,该算法就会被简化为单纯的 FIFO 算法
。具体来讲,假设图 a 中全部页面都设置了 R 位。操做系统将页面依次移到链表末尾,每次都在添加到末尾时清除 R 位。最后,算法又会回到页面 A,此时的 R 位已经被清除,那么页面 A 就会被执行出链处理,所以算法可以正常结束。
一种比较好的方式是把全部的页面都保存在一个相似钟面的环形链表中,一个表针指向最老的页面。以下图所示
当缺页错误出现时,算法首先检查表针指向的页面,若是它的 R 位是 0 就淘汰该页面,并把新的页面插入到这个位置,而后把表针向前移动一位;若是 R 位是 1 就清除 R 位并把表针前移一个位置。重复这个过程直到找到了一个 R 位为 0 的页面位置。了解这个算法的工做方式,就明白为何它被称为 时钟(clokc)
算法了。
在前面几条指令中频繁使用的页面和可能在后面的几条指令中被使用。反过来讲,已经好久没有使用的页面有可能在将来一段时间内仍不会被使用。这个思想揭示了一个能够实现的算法:在缺页中断时,置换未使用时间最长的页面。这个策略称为 LRU(Least Recently Used)
,最近最少使用页面置换算法。
虽然 LRU 在理论上是能够实现的,可是从长远看来代价比较高。为了彻底实现 LRU,会在内存中维护一个全部页面的链表,最频繁使用的页位于表头,最近最少使用的页位于表尾。困难的是在每次内存引用时更新整个链表。在链表中找到一个页面,删除它,而后把它移动到表头是一个很是耗时的操做,即便使用硬件
来实现也是同样的费时。
尽管上面的 LRU 算法在原则上是能够实现的,可是不多有机器可以拥有那些特殊的硬件。上面是硬件的实现方式,那么如今考虑要用软件
来实现 LRU 。一种能够实现的方案是 NFU(Not Frequently Used,最不经常使用)
算法。它须要一个软件计数器来和每一个页面关联,初始化的时候是 0 。在每一个时钟中断时,操做系统会浏览内存中的全部页,会将每一个页面的 R 位(0 或 1)加到它的计数器上。这个计数器大致上跟踪了各个页面访问的频繁程度。当缺页异常出现时,则置换计数器值最小的页面。
只须要对 NFU 作一个简单的修改就可让它模拟 LRU,这个修改有两个步骤
修改之后的算法称为 老化(aging)
算法,下图解释了老化算法是如何工做的。
咱们假设在第一个时钟周期内页面 0 - 5 的 R 位依次是 1,0,1,0,1,1,(也就是页面 0 是 1,页面 1 是 0,页面 2 是 1 这样类推)。也就是说,在 0 个时钟周期到 1 个时钟周期之间,0,2,4,5 都被引用了,从而把它们的 R 位设置为 1,剩下的设置为 0 。在相关的六个计数器被右移以后 R 位被添加到 左侧
,就像上图中的 a。剩下的四列显示了接下来的四个时钟周期内的六个计数器变化。
CPU正在以某个频率前进,该频率的周期称为
时钟滴答
或时钟周期
。一个 100Mhz 的处理器每秒将接收100,000,000个时钟滴答。
当缺页异常出现时,将置换(就是移除)
计数器值最小的页面。若是一个页面在前面 4 个时钟周期内都没有被访问过,那么它的计数器应该会有四个连续的 0 ,所以它的值确定要比前面 3 个时钟周期内都没有被访问过的页面的计数器小。
这个算法与 LRU 算法有两个重要的区别:看一下上图中的 e
,第三列和第五列
当缺页异常发生后,须要扫描整个页表才能肯定被淘汰的页面,所以基本工做集算法仍是比较浪费时间的。一个对基本工做集算法的提高是基于时钟算法可是却使用工做集的信息,这种算法称为WSClock(工做集时钟)
。因为它的实现简单而且具备高性能,所以在实践中被普遍应用。
与时钟算法同样,所需的数据结构是一个以页框为元素的循环列表,就像下面这样
工做集时钟页面置换算法的操做:a) 和 b) 给出 R = 1 时所发生的情形;c) 和 d) 给出 R = 0 的例子
最初的时候,该表是空的。当装入第一个页面后,把它加载到该表中。随着更多的页面的加入,它们造成一个环形结构。每一个表项包含来自基本工做集算法的上次使用时间,以及 R 位(已标明)和 M 位(未标明)。
与时钟算法同样,在每一个缺页异常时,首先检查指针指向的页面。若是 R 位被是设置为 1,该页面在当前时钟周期内就被使用过,那么该页面就不适合被淘汰。而后把该页面的 R 位置为 0,指针指向下一个页面,并重复该算法。该事件序列化后的状态参见图 b。
如今考虑指针指向的页面 R = 0 时会发生什么,参见图 c,若是页面的使用期限大于 t 而且页面为被访问过,那么这个页面就不会在工做集中,而且在磁盘上会有一个此页面的副本。申请从新调入一个新的页面,并把新的页面放在其中,如图 d 所示。另外一方面,若是页面被修改过,就不能从新申请页面,由于这个页面在磁盘上没有有效的副本。为了不因为调度写磁盘操做引发的进程切换,指针继续向前走,算法继续对下一个页面进行操做。毕竟,有可能存在一个老的,没有被修改过的页面能够当即使用。
原则上来讲,全部的页面都有可能由于磁盘I/O
在某个时钟周期内被调度。为了下降磁盘阻塞,须要设置一个限制,即最大只容许写回 n 个页面。一旦达到该限制,就不容许调度新的写操做。
那么就有个问题,指针会绕一圈回到原点的,若是回到原点,它的起始点会发生什么?这里有两种状况:
在第一种状况中,指针仅仅是不停的移动,寻找一个未被修改过的页面。因为已经调度了一个或者多个写操做,最终会有某个写操做完成,它的页面会被标记为未修改。置换遇到的第一个未被修改过的页面,这个页面不必定是第一个被调度写操做的页面,由于硬盘驱动程序为了优化性能可能会把写操做重排序。
对于第二种状况,全部的页面都在工做集中,不然将至少调度了一个写操做。因为缺少额外的信息,最简单的方法就是置换一个未被修改的页面来使用,扫描中须要记录未被修改的页面的位置,若是不存在未被修改的页面,就选定当前页面并把它写回磁盘。
咱们到如今已经研究了各类页面置换算法,如今咱们来一个简单的总结,算法的总结概括以下
算法 | 注释 |
---|---|
最优算法 | 不可实现,但能够用做基准 |
NRU(最近未使用) 算法 | 和 LRU 算法很类似 |
FIFO(先进先出) 算法 | 有可能会抛弃重要的页面 |
第二次机会算法 | 比 FIFO 有较大的改善 |
时钟算法 | 实际使用 |
LRU(最近最少)算法 | 比较优秀,可是很难实现 |
NFU(最不常常食用)算法 | 和 LRU 很相似 |
老化算法 | 近似 LRU 的高效算法 |
工做集算法 | 实施起来开销很大 |
工做集时钟算法 | 比较有效的算法 |
最优算法
在当前页面中置换最后要访问的页面。不幸的是,没有办法来断定哪一个页面是最后一个要访问的,所以实际上该算法不能使用
。然而,它能够做为衡量其余算法的标准。
NRU
算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,可是性能不是很好。存在更好的算法。
FIFO
会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长可是还在使用的页面,所以这个算法也不是一个很好的选择。
第二次机会
算法是对 FIFO 的一个修改,它会在删除页面以前检查这个页面是否仍在使用。若是页面正在使用,就会进行保留。这个改进大大提升了性能。
时钟
算法是第二次机会算法的另一种实现形式,时钟算法和第二次算法的性能差很少,可是会花费更少的时间来执行算法。
LRU
算法是一个很是优秀的算法,可是没有特殊的硬件(TLB)
很难实现。若是没有硬件,就不能使用 LRU 算法。
NFU
算法是一种近似于 LRU 的算法,它的性能不是很是好。
老化
算法是一种更接近 LRU 算法的实现,而且能够更好的实现,所以是一个很好的选择
最后两种算法都使用了工做集算法。工做集算法提供了合理的性能开销,可是它的实现比较复杂。WSClock
是另一种变体,它不只可以提供良好的性能,并且能够高效地实现。
总之,最好的算法是老化算法和WSClock算法。他们分别是基于 LRU 和工做集算法。他们都具备良好的性能而且可以被有效的实现。还存在其余一些好的算法,但实际上这两个多是最重要的。
下面来聊一聊文件系统,你须要知道下面这些知识点
文件是一种抽象机制,它提供了一种方式用来存储信息以及在后面进行读取。可能任何一种机制最重要的特性就是管理对象的命名方式。在建立一个文件后,它会给文件一个命名。当进程终止时,文件会继续存在,而且其余进程能够使用名称访问该文件
。
文件命名规则对于不一样的操做系统来讲是不同的,可是全部现代操做系统都容许使用 1 - 8 个字母的字符串做为合法文件名。
某些文件区分大小写字母,而大多数则不区分。UNIX
属于第一类;历史悠久的 MS-DOS
属于第二类(顺便说一句,尽管 MS-DOS 历史悠久,但 MS-DOS 仍在嵌入式系统中很是普遍地使用,所以它毫不是过期的);所以,UNIX 系统会有三种不一样的命名文件:maria
、Maria
、MARIA
。在 MS-DOS ,全部这些命名都属于相同的文件。
许多操做系统支持两部分的文件名,它们之间用 .
分隔开,好比文件名 prog.c
。原点后面的文件称为 文件扩展名(file extension)
,文件扩展名一般表示文件的一些信息。一些经常使用的文件扩展名以及含义以下图所示
扩展名 | 含义 |
---|---|
bak | 备份文件 |
c | c 源程序文件 |
gif | 符合图形交换格式的图像文件 |
hlp | 帮助文件 |
html | WWW 超文本标记语言文档 |
jpg | 符合 JPEG 编码标准的静态图片 |
mp3 | 符合 MP3 音频编码格式的音乐文件 |
mpg | 符合 MPEG 编码标准的电影 |
o | 目标文件(编译器输出格式,还没有连接) |
pdf 格式的文件 | |
ps | PostScript 文件 |
tex | 为 TEX 格式化程序准备的输入文件 |
txt | 文本文件 |
zip | 压缩文件 |
在 UNIX 系统中,文件扩展名只是一种约定,操做系统并不强制采用。
文件的构造有多种方式。下图列出了经常使用的三种构造方式
三种不一样的文件。 a) 字节序列 。b) 记录序列。c) 树
上图中的 a 是一种无结构的字节序列,操做系统不关心序列的内容是什么,操做系统能看到的就是字节(bytes)
。其文件内容的任何含义只在用户程序中进行解释。UNIX 和 Windows 都采用这种办法。
图 b 表示在文件结构上的第一部改进。在这个模型中,文件是具备固定长度记录的序列,每一个记录都有其内部结构。 把文件做为记录序列的核心思想是:读操做返回一个记录,而写操做重写或者追加一个记录。第三种文件结构如上图 c 所示。在这种组织结构中,文件由一颗记录树
构成,记录树的长度不必定相同,每一个记录树都在记录中的固定位置包含一个key
字段。这棵树按 key 进行排序,从而能够对特定的 key 进行快速查找。
不少操做系统支持多种文件类型。例如,UNIX(一样包括 OS X)和 Windows 都具备常规的文件和目录。除此以外,UNIX 还具备字符特殊文件(character special file)
和 块特殊文件(block special file)
。常规文件(Regular files)
是包含有用户信息的文件。用户通常使用的文件大都是常规文件,常规文件通常包括 可执行文件、文本文件、图像文件,从常规文件读取数据或将数据写入时,内核会根据文件系统的规则执行操做,是写入可能被延迟,记录日志或者接受其余操做。
早期的操做系统只有一种访问方式:序列访问(sequential access)
。在这些系统中,进程能够按照顺序读取全部的字节或文件中的记录,可是不能跳过并乱序执行它们。顺序访问文件是能够返回到起点的,须要时能够屡次读取该文件。当存储介质是磁带而不是磁盘时,顺序访问文件很方便。
在使用磁盘来存储文件时,能够不按照顺序读取文件中的字节或者记录,或者按照关键字而不是位置来访问记录。这种可以以任意次序进行读取的称为随机访问文件(random access file)
。许多应用程序都须要这种方式。
随机访问文件对许多应用程序来讲都必不可少,例如,数据库系统。若是乘客打电话预约某航班机票,订票程序必须可以直接访问航班记录,而没必要先读取其余航班的成千上万条记录。
有两种方法能够指示从何处开始读取文件。第一种方法是直接使用 read
从头开始读取。另外一种是用一个特殊的 seek
操做设置当前位置,在 seek 操做后,从这个当前位置顺序地开始读文件。UNIX 和 Windows 使用的是后面一种方式。
文件包括文件名和数据。除此以外,全部的操做系统还会保存其余与文件相关的信息,如文件建立的日期和时间、文件大小。咱们能够称这些为文件的属性(attributes)
。有些人也喜欢把它们称做 元数据(metadata)
。文件的属性在不一样的系统中差异很大。文件的属性只有两种状态:设置(set)
和 清除(clear)
。
使用文件的目的是用来存储信息并方便之后的检索。对于存储和检索,不一样的系统提供了不一样的操做。如下是与文件有关的最经常使用的一些系统调用:
Create
,建立不包含任何数据的文件。调用的目的是表示文件即将创建,并对文件设置一些属性。Delete
,当文件再也不须要,必须删除它以释放内存空间。为此总会有一个系统调用来删除文件。Open
,在使用文件以前,必须先打开文件。这个调用的目的是容许系统将属性和磁盘地址列表保存到主存中,用来之后的快速访问。Close
,当全部进程完成时,属性和磁盘地址再也不须要,所以应关闭文件以释放表空间。不少系统限制进程打开文件的个数,以此达到鼓励用户关闭再也不使用的文件。磁盘以块为单位写入,关闭文件时会强制写入最后一块
,即便这个块空间内部还不满。Read
,数据从文件中读取。一般状况下,读取的数据来自文件的当前位置。调用者必须指定须要读取多少数据,而且提供存放这些数据的缓冲区。Write
,向文件写数据,写操做通常也是从文件的当前位置开始进行。若是当前位置是文件的末尾,则会直接追加进行写入。若是当前位置在文件中,则现有数据被覆盖,而且永远消失。append
,使用 append 只能向文件末尾添加数据。seek
,对于随机访问的文件,要指定从何处开始获取数据。一般的方法是用 seek 系统调用把当前位置指针指向文件中的特定位置。seek 调用结束后,就能够从指定位置开始读写数据了。get attributes
,进程运行时一般须要读取文件属性。set attributes
,用户能够本身设置一些文件属性,甚至是在文件建立以后,实现该功能的是 set attributes 系统调用。rename
,用户能够本身更改已有文件的名字,rename 系统调用用于这一目的。文件系统一般提供目录(directories)
或者 文件夹(folders)
用于记录文件的位置,在不少系统中目录自己也是文件,下面咱们会讨论关于文件,他们的组织形式、属性和能够对文件进行的操做。
目录系统最简单的形式是有一个可以包含全部文件的目录。这种目录被称为根目录(root directory)
,因为根目录的惟一性,因此其名称并不重要。在最先期的我的计算机中,这种系统很常见,部分缘由是由于只有一个用户。下面是一个单层目录系统的例子
含有四个文件的单层目录系统
该目录中有四个文件。这种设计的优势在于简单,而且可以快速定位文件,毕竟只有一个地方能够检索。这种目录组织形式如今通常用于简单的嵌入式设备(如数码相机和某些便携式音乐播放器)上使用。
对于简单的应用而言,通常都用单层目录方式,可是这种组织形式并不适合于现代计算机,由于现代计算机含有成千上万个文件和文件夹。若是都放在根目录下,查找起来会很是困难。为了解决这一问题,出现了层次目录系统(Hierarchical Directory Systems)
,也称为目录树
。经过这种方式,能够用不少目录把文件进行分组。进而,若是多个用户共享同一个文件服务器,好比公司的网络系统,每一个用户能够为本身的目录树拥有本身的私人根目录。这种方式的组织结构以下
根目录含有目录 A、B 和 C ,分别属于不一样的用户,其中两个用户个字建立了子目录
。用户能够建立任意数量的子目录,现代文件系统都是按照这种方式组织的。
当目录树组织文件系统时,须要有某种方法指明文件名。经常使用的方法有两种,第一种方式是每一个文件都会用一个绝对路径名(absolute path name)
,它由根目录到文件的路径组成。
另一种指定文件名的方法是 相对路径名(relative path name)
。它经常和 工做目录(working directory)
(也称做 当前目录(current directory)
)一块儿使用。用户能够指定一个目录做为当前工做目录。例如,若是当前目录是 /usr/ast
,那么绝对路径 /usr/ast/mailbox
能够直接使用 mailbox
来引用。
不一样文件中管理目录的系统调用的差异比管理文件的系统调用差异大。为了了解这些系统调用有哪些以及它们怎样工做,下面给出一个例子(取自 UNIX)。
Create
,建立目录,除了目录项 .
和 ..
外,目录内容为空。Delete
,删除目录,只有空目录能够删除。只包含 .
和 ..
的目录被认为是空目录,这两个目录项一般不能删除opendir
,目录内容可被读取。例如,未列出目录中的所有文件,程序必须先打开该目录,而后读其中所有文件的文件名。与打开和读文件相同,在读目录前,必须先打开文件。closedir
,读目录结束后,应该关闭目录用于释放内部表空间。readdir
,系统调用 readdir 返回打开目录的下一个目录项。之前也采用 read 系统调用来读取目录,可是这种方法有一个缺点:程序员必须了解和处理目录的内部结构。相反,不论采用哪种目录结构,readdir 老是以标准格式返回一个目录项。rename
,在不少方面目录和文件都类似。文件能够更换名称,目录也能够。link
,连接技术容许在多个目录中出现同一个文件。这个系统调用指定一个存在的文件和一个路径名,并创建从该文件到路径所指名字的连接。这样,能够在多个目录中出现同一个文件。有时也被称为硬连接(hard link)
。unlink
,删除目录项。若是被解除连接的文件只出如今一个目录中,则将它从文件中删除。若是它出如今多个目录中,则只删除指定路径名的连接,依然保留其余路径名的连接。在 UNIX 中,用于删除文件的系统调用就是 unlink。文件系统存储在磁盘
中。大部分的磁盘可以划分出一到多个分区,叫作磁盘分区(disk partitioning)
或者是磁盘分片(disk slicing)
。每一个分区都有独立的文件系统,每块分区的文件系统能够不一样。磁盘的 0 号分区称为 主引导记录(Master Boot Record, MBR)
,用来引导(boot)
计算机。在 MBR 的结尾是分区表(partition table)
。每一个分区表给出每一个分区由开始到结束的地址。
当计算机开始引 boot 时,BIOS 读入并执行 MBR。
MBR 作的第一件事就是肯定活动分区
,读入它的第一个块,称为引导块(boot block)
并执行。引导块中的程序将加载分区中的操做系统。为了一致性,每一个分区都会从引导块开始,即便引导块不包含操做系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操做系统。
除了从引导块开始以外,磁盘分区的布局是随着文件系统的不一样而变化的。一般文件系统会包含一些属性,以下
文件系统布局
紧跟在引导块后面的是 超级块(Superblock)
,超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的全部关键参数
在计算机启动或者文件系统首次使用时,超级块会被读入内存。
接着是文件系统中空闲块
的信息,例如,能够用位图或者指针列表的形式给出。
BitMap 位图或者 Bit vector 位向量
位图或位向量是一系列位或位的集合,其中每一个位对应一个磁盘块,该位能够采用两个值:0和1,0表示已分配该块,而1表示一个空闲块。下图中的磁盘上给定的磁盘块实例(分配了绿色块)能够用16位的位图表示为:0000111000000110。
使用链表进行管理
在这种方法中,空闲磁盘块连接在一块儿,即一个空闲块包含指向下一个空闲块的指针。第一个磁盘块的块号存储在磁盘上的单独位置,也缓存在内存中。
这里不得不提一个叫作碎片(fragment)
的概念,也称为片断。通常零散的单个数据一般称为片断。 磁盘块能够进一步分为固定大小的分配单元,片断只是在驱动器上彼此不相邻的文件片断。
而后在后面是一个 inode(index node)
,也称做索引节点。它是一个数组的结构,每一个文件有一个 inode,inode 很是重要,它说明了文件的方方面面。每一个索引节点都存储对象数据的属性和磁盘块位置
有一种简单的方法能够找到它们 ls -lai
命令。让咱们看一下根文件系统:
inode 节点主要包括了如下信息
文件分为两部分,索引节点和块。一旦建立后,每种类型的块数是固定的。你不能增长分区上 inode 的数量,也不能增长磁盘块的数量。
紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其余部分存放了其余全部的目录和文件。
最重要的问题是记录各个文件分别用到了哪些磁盘块。不一样的系统采用了不一样的方法。下面咱们会探讨一下这些方式。分配背后的主要思想是有效利用文件空间
和快速访问文件
,主要有三种分配方案
最简单的分配方案是把每一个文件做为一连串连续数据块存储在磁盘上。所以,在具备 1KB 块的磁盘上,将为 50 KB 文件分配 50 个连续块。
使用连续空间存储文件
上面展现了 40 个连续的内存块。从最左侧的 0 块开始。初始状态下,尚未装载文件,所以磁盘是空的。接着,从磁盘开始处(块 0 )处开始写入占用 4 块长度的内存 A 。而后是一个占用 6 块长度的内存 B,会直接在 A 的末尾开始写。
注意每一个文件都会在新的文件块开始写,因此若是文件 A 只占用了 3 又 1/2
个块,那么最后一个块的部份内存会被浪费。在上面这幅图中,总共展现了 7 个文件,每一个文件都会从上个文件的末尾块开始写新的文件块。
连续的磁盘空间分配有两个优势。
第一,连续文件存储实现起来比较简单,只须要记住两个数字就能够:一个是第一个块的文件地址和文件的块数量。给定第一个块的编号,能够经过简单的加法找到任何其余块的编号。
第二点是读取性能比较强,能够经过一次操做从文件中读取整个文件。只须要一次寻找第一个块。后面就再也不须要寻道时间和旋转延迟,因此数据会以全带宽进入磁盘。
所以,连续的空间分配具备实现简单
、高性能
的特色。
不幸的是,连续空间分配也有很明显的不足。随着时间的推移,磁盘会变得很零碎。下图解释了这种现象
这里有两个文件 D 和 F 被删除了。当删除一个文件时,此文件所占用的块也随之释放,就会在磁盘空间中留下一些空闲块。磁盘并不会在这个位置挤压掉空闲块,由于这会复制空闲块以后的全部文件,可能会有上百万的块,这个量级就太大了。
第二种存储文件的方式是为每一个文件构造磁盘块链表,每一个文件都是磁盘块的连接列表,就像下面所示
以磁盘块的链表形式存储文件
每一个块的第一个字做为指向下一块的指针,块的其余部分存放数据。若是上面这张图你看的不是很清楚的话,能够看看整个的链表分配方案
与连续分配方案不一样,这一方法能够充分利用每一个磁盘块。除了最后一个磁盘块外,不会由于磁盘碎片而浪费存储空间。一样,在目录项中,只要存储了第一个文件块,那么其余文件块也可以被找到。
另外一方面,在链表的分配方案中,尽管顺序读取很是方便,可是随机访问却很困难(这也是数组和链表数据结构的一大区别)。
还有一个问题是,因为指针会占用一些字节,每一个磁盘块实际存储数据的字节数并再也不是 2 的整数次幂。虽然这个问题并不会很严重,可是这种方式下降了程序运行效率。许多程序都是以长度为 2 的整数次幂来读写磁盘,因为每一个块的前几个字节被指针所使用,因此要读出一个完成的块大小信息,就须要当前块的信息和下一块的信息拼凑而成,所以就引起了查找和拼接的开销。
因为连续分配和链表分配都有其不可忽视的缺点。因此提出了使用内存中的表来解决分配问题。取出每一个磁盘块的指针字,把它们放在内存的一个表中,就能够解决上述链表的两个不足之处。下面是一个例子
上图表示了链表造成的磁盘块的内容。这两个图中都有两个文件,文件 A 依次使用了磁盘块地址 四、七、 二、 十、 12,文件 B 使用了六、三、11 和 14。也就是说,文件 A 从地址 4 处开始,顺着链表走就能找到文件 A 的所有磁盘块。一样,从第 6 块开始,顺着链走到最后,也可以找到文件 B 的所有磁盘块。你会发现,这两个链表都以不属于有效磁盘编号的特殊标记(-1)结束。内存中的这种表格称为 文件分配表(File Application Table,FAT)
。
文件只有打开后才可以被读取。在文件打开后,操做系统会使用用户提供的路径名来定位磁盘中的目录。目录项提供了查找文件磁盘块所须要的信息。根据系统的不一样,提供的信息也不一样,可能提供的信息是整个文件的磁盘地址,或者是第一个块的数量(两个链表方案)或 inode的数量。不过无论用那种状况,目录系统的主要功能就是 将文件的 ASCII 码的名称映射到定位数据所需的信息上。
当多个用户在同一个项目中工做时,他们一般须要共享文件。若是这个共享文件同时出如今多个用户目录下,那么他们协同工做起来就很方便。下面的这张图咱们在上面提到过,可是有一个更改的地方,就是 C 的一个文件也出如今了 B 的目录下。
若是按照如上图的这种组织方式而言,那么 B 的目录与该共享文件的联系称为 连接(link)
。那么文件系统如今就是一个 有向无环图(Directed Acyclic Graph, 简称 DAG)
,而不是一棵树了。
技术的改变会给当前的文件系统带来压力。这种状况下,CPU 会变得愈来愈快,磁盘会变得愈来愈大而且愈来愈便宜(但不会愈来愈快)。内存容量也是以指数级增加。可是磁盘的寻道时间(除了固态盘,由于固态盘没有寻道时间)并无得到提升。
为此,Berkeley
设计了一种全新的文件系统,试图缓解这个问题,这个文件系统就是 日志结构文件系统(Log-structured File System, LFS)
。旨在解决如下问题。
不断增加的系统内存
顺序 I/O 性能赛过随机 I/O 性能
现有低效率的文件系统
文件系统不支持 RAID(虚拟化)
另外一方面,当时的文件系统不管是 UNIX 仍是 FFS,都有大量的随机读写(在 FFS 中建立一个新文件至少须要5次随机写),所以成为整个系统的性能瓶颈。同时由于 Page cache
的存在,做者认为随机读不是主要问题:随着愈来愈大的内存,大部分的读操做都能被 cache,所以 LFS 主要要解决的是减小对硬盘的随机写操做。
在这种设计中,inode 甚至具备与 UNIX 中相同的结构,可是如今它们分散在整个日志中,而不是位于磁盘上的固定位置。因此,inode 很定位。为了可以找到 inode ,维护了一个由 inode 索引的 inode map(inode 映射)
。表项 i 指向磁盘中的第 i 个 inode 。这个映射保存在磁盘中,可是也保存在缓存中,所以,使用最频繁的部分大部分时间都在内存中。
到目前为止,全部写入最初都缓存在内存
中,而且追加在日志末尾
,全部缓存的写入都按期在单个段中写入磁盘。因此,如今打开文件也就意味着用映射定位文件的索引节点。一旦 inode 被定位后,磁盘块的地址就可以被找到。全部这些块自己都将位于日志中某处的分段中。
真实状况下的磁盘容量是有限的,因此最终日志会占满整个磁盘空间,这种状况下就会出现没有新的磁盘块被写入到日志中。幸运的是,许多现有段可能具备再也不须要的块。例如,若是一个文件被覆盖了,那么它的 inode 将被指向新的块,可是旧的磁盘块仍在先前写入的段中占据着空间。
为了处理这个问题,LFS 有一个清理(clean)
线程,它会循环扫描日志并对日志进行压缩。首先,经过查看日志中第一部分的信息来查看其中存在哪些索引节点和文件。它会检查当前 inode 的映射来查看 inode 否在在当前块中,是否仍在被使用。若是不是,该信息将被丢弃。若是仍然在使用,那么 inode 和块就会进入内存等待写回到下一个段中。而后原来的段被标记为空闲,以便日志能够用来存放新的数据。用这种方法,清理线程遍历日志,从后面移走旧的段,而后将有效的数据放入内存等待写到下一个段中。由此一来整个磁盘会造成一个大的环形缓冲区
,写线程将新的段写在前面,而清理线程则清理后面的段。
虽然日志结构系统的设计很优雅,可是因为它们和现有的文件系统不相匹配,所以尚未普遍使用。不过,从日志文件结构系统衍生出来一种新的日志系统,叫作日志文件系统
,它会记录系统下一步将要作什么的日志。微软的 NTFS
文件系统、Linux 的 ext3
就使用了此日志。 OS X
将日志系统做为可供选项。为了看清它是如何工做的,咱们下面讨论一个例子,好比 移除文件
,这个操做在 UNIX 中须要三个步骤完成:
UNIX 操做系统使用一种 虚拟文件系统(Virtual File System, VFS)
来尝试将多种文件系统构成一个有序的结构。关键的思想是抽象出全部文件系统都共有的部分,并将这部分代码放在一层,这一层再调用具体文件系统来管理数据。下面是一个 VFS 的系统结构
仍是那句经典的话,在计算机世界中,任何解决不了的问题均可以加个代理
来解决。全部和文件相关的系统调用在最初的处理上都指向虚拟文件系统。这些来自用户进程的调用,都是标准的 POSIX 系统调用
,好比 open、read、write 和 seek 等。VFS 对用户进程有一个 上层
接口,这个接口就是著名的 POSIX 接口。
可以使文件系统工做是一回事,可以使文件系统高效、稳定的工做是另外一回事,下面咱们就来探讨一下文件系统的管理和优化。
文件一般存在磁盘中,因此如何管理磁盘空间是一个操做系统的设计者须要考虑的问题。在文件上进行存有两种策略:分配 n 个字节的连续磁盘空间;或者把文件拆分红多个并不必定连续的块。在存储管理系统中,主要有分段管理
和 分页管理
两种方式。
正如咱们所看到的,按连续字节序列
存储文件有一个明显的问题,当文件扩大时,有可能须要在磁盘上移动文件。内存中分段也有一样的问题。不一样的是,相对于把文件从磁盘的一个位置移动到另外一个位置,内存中段的移动操做要快不少。所以,几乎全部的文件系统都把文件分割成固定大小的块来存储。
一旦把文件分为固定大小的块来存储,就会出现问题,块的大小是多少?按照磁盘组织方式,扇区、磁道和柱面显然均可以做为分配单位。在分页系统中,分页大小也是主要因素。
拥有大的块尺寸意味着每一个文件,甚至 1 字节文件,都要占用一个柱面空间,也就是说小文件浪费了大量的磁盘空间。另外一方面,小块意味着大部分文件将会跨越多个块,所以须要屡次搜索和旋转延迟才能读取它们,从而下降了性能。所以,若是分配的块太大
会浪费空间
;分配的块过小
会浪费时间
。
一旦指定了块大小,下一个问题就是怎样跟踪空闲块。有两种方法被普遍采用,以下图所示
第一种方法是采用磁盘块链表
,链表的每一个块中包含很可能多的空闲磁盘块号。对于 1 KB 的块和 32 位的磁盘块号,空闲表中每一个块包含有 255 个空闲的块号。考虑 1 TB 的硬盘,拥有大概十亿个磁盘块。为了存储所有地址块号,若是每块能够保存 255 个块号,则须要将近 400 万个块。一般,空闲块用于保存空闲列表,所以存储基本上是空闲的。
另外一种空闲空间管理的技术是位图(bitmap)
,n 个块的磁盘须要 n 位位图。在位图中,空闲块用 1 表示,已分配的块用 0 表示。对于 1 TB 硬盘的例子,须要 10 亿位表示,即须要大约 130 000 个 1 KB 块存储。很明显,和 32 位链表模型相比,位图须要的空间更少,由于每一个块使用 1 位。只有当磁盘快满的时候,链表须要的块才会比位图少。
为了防止一些用户占用太多的磁盘空间,多用户操做一般提供一种磁盘配额(enforcing disk quotas)
的机制。系统管理员为每一个用户分配最大的文件和块分配,而且操做系统确保用户不会超过其配额。咱们下面会谈到这一机制。
在用户打开一个文件时,操做系统会找到文件属性
和磁盘地址
,并把它们送入内存中的打开文件表。其中一个属性告诉文件全部者
是谁。任何有关文件的增长都会记到全部者的配额中。
配额表中记录了每一个用户的配额
第二张表包含了每一个用户当前打开文件的配额记录,即便是其余人打开该文件也同样。如上图所示,该表的内容是从被打开文件的全部者的磁盘配额文件中提取出来的。当全部文件关闭时,该记录被写回配额文件。
当在打开文件表中创建一新表项时,会产生一个指向全部者配额记录的指针。每次向文件中添加一个块时,文件全部者所用数据块的总数也随之增长,并会同时增长硬限制
和软限制
的检查。能够超出软限制,但硬限制不能够超出。当已达到硬限制时,再往文件中添加内容将引起错误。一样,对文件数目也存在相似的检查。
作文件备份很耗费时间并且也很浪费空间,这会引发下面几个问题。首先,是要备份整个文件仍是仅备份一部分呢?通常来讲,只是备份特定目录及其下的所有文件,而不是备份整个文件系统。
其次,对上次未修改过的文件再进行备份是一种浪费,于是产生了一种增量转储(incremental dumps)
的思想。最简单的增量转储的形式就是周期性
的作全面的备份,而天天只对增量转储完成后发生变化的文件作单个备份。
稍微好一点的方式是只备份最近一次转储以来更改过的文件。固然,这种作法极大的缩减了转储时间,但恢复起来却更复杂,由于最近的全面转储先要所有恢复,随后按逆序进行增量转储。为了方便恢复,人们每每使用更复杂的转储模式。
第三,既然待转储的每每是海量数据,那么在将其写入磁带以前对文件进行压缩就颇有必要。可是,若是在备份过程当中出现了文件损坏的状况,就会致使破坏压缩算法,从而使整个磁带没法读取。因此在备份前是否进行文件压缩需慎重考虑。
第四,对正在使用的文件系统作备份是很难的。若是在转储过程当中要添加,删除和修改文件和目录,则转储结果可能不一致。所以,由于转储过程当中须要花费数个小时的时间,因此有必要在晚上将系统脱机进行备份,然而这种方式的接受程度并不高。因此,人们修改了转储算法,记下文件系统的瞬时快照
,即复制关键的数据结构,而后须要把未来对文件和目录所作的修改复制到块中,而不是处处更新他们。
磁盘转储到备份磁盘上有两种方案:物理转储和逻辑转储。物理转储(physical dump)
是从磁盘的 0 块开始,依次将全部磁盘块按照顺序写入到输出磁盘,并在复制最后一个磁盘时中止。这种程序的万无一失性是其余程序所不具有的。
第二个须要考虑的是坏块的转储。制造大型磁盘而没有瑕疵是不可能的,因此也会存在一些坏块(bad blocks)
。有时进行低级格式化后,坏块会被检测出来并进行标记,这种状况的解决办法是用磁盘末尾的一些空闲块所替换。
然而,一些块在格式化后会变坏,在这种状况下操做系统能够检测到它们。一般状况下,它能够经过建立一个由全部坏块组成的文件
来解决问题,确保它们不会出如今空闲池中而且永远不会被分配。那么此文件是彻底不可读的。若是磁盘控制器将全部的坏块从新映射,物理转储仍是可以正常工做的。
Windows 系统有分页文件(paging files)
和 休眠文件(hibernation files)
。它们在文件还原时不发挥做用,同时也不该该在第一时间进行备份。
影响可靠性的一个因素是文件系统的一致性。许多文件系统读取磁盘块、修改磁盘块、再把它们写回磁盘。若是系统在全部块写入以前崩溃,文件系统就会处于一种不一致(inconsistent)
的状态。若是某些还没有写回的块是索引节点块,目录块或包含空闲列表的块,则此问题是很严重的。
为了处理文件系统一致性问题,大部分计算机都会有应用程序来检查文件系统的一致性。例如,UNIX 有 fsck
;Windows 有 sfc
,每当引导系统时(尤为是在崩溃后),均可以运行该程序。
能够进行两种一致性检查:块的一致性检查和文件的一致性检查。为了检查块的一致性,应用程序会创建两张表,每一个包含一个计数器的块,最初设置为 0 。第一个表中的计数器跟踪该块在文件中出现的次数,第二张表中的计数器记录每一个块在空闲列表、空闲位图中出现的频率。
访问磁盘的效率要比内存满的多,是时候又祭出这张图了
从内存读一个 32 位字大概是 10ns,从硬盘上读的速率大概是 100MB/S,对每一个 32 位字来讲,效率会慢了四倍,另外,还要加上 5 - 10 ms 的寻道时间等其余损耗,若是只访问一个字,内存要比磁盘快百万数量级。因此磁盘优化是颇有必要的,下面咱们会讨论几种优化方式
最经常使用的减小磁盘访问次数的技术是使用 块高速缓存(block cache)
或者 缓冲区高速缓存(buffer cache)
。高速缓存指的是一系列的块,它们在逻辑上属于磁盘,但实际上基于性能的考虑被保存在内存中。
管理高速缓存有不一样的算法,经常使用的算法是:检查所有的读请求,查看在高速缓存中是否有所须要的块。若是存在,可执行读操做而无须访问磁盘。若是检查块再也不高速缓存中,那么首先把它读入高速缓存,再复制到所需的地方。以后,对同一个块的请求都经过高速缓存
来完成。
高速缓存的操做以下图所示
因为在高速缓存中有许多块,因此须要某种方法快速肯定所需的块是否存在。经常使用方法是将设备和磁盘地址进行散列操做,而后,在散列表中查找结果。具备相同散列值的块在一个链表中链接在一块儿(这个数据结构是否是很像 HashMap?),这样就能够沿着冲突链查找其余块。
若是高速缓存已满
,此时须要调入新的块,则要把原来的某一块调出高速缓存,若是要调出的块在上次调入后已经被修改过,则须要把它写回磁盘。
第二个明显提升文件系统的性能是,在须要用到块以前,试图提早
将其写入高速缓存,从而提升命中率
。许多文件都是顺序读取。若是请求文件系统在某个文件中生成块 k,文件系统执行相关操做而且在完成以后,会检查高速缓存,以便肯定块 k + 1 是否已经在高速缓存。若是不在,文件系统会为 k + 1 安排一个预读取,由于文件但愿在用到该块的时候可以直接从高速缓存中读取。
固然,块提早读取策略只适用于实际顺序读取的文件。对随机访问的文件,提早读丝绝不起做用。甚至还会形成阻碍。
高速缓存和块提早读并非提升文件系统性能的惟一方法。另外一种重要的技术是把有可能顺序访问的块放在一块儿,固然最好是在同一个柱面上,从而减小磁盘臂的移动次数。当写一个输出文件时,文件系统就必须按照要求一次一次地分配磁盘块。若是用位图来记录空闲块,而且整个位图在内存中,那么选择与前一块最近的空闲块是很容易的。若是用空闲表,而且链表的一部分存在磁盘上,要分配紧邻的空闲块就会困难不少。
在初始安装操做系统后,文件就会被不断的建立和清除,因而磁盘会产生不少的碎片,在建立一个文件时,它使用的块会散布在整个磁盘上,下降性能。删除文件后,回收磁盘块,可能会形成空穴。
磁盘性能能够经过以下方式恢复:移动文件使它们相互挨着,并把全部的至少是大部分的空闲空间放在一个或多个大的连续区域内。Windows 有一个程序 defrag
就是作这个事儿的。Windows 用户会常用它,SSD 除外。
磁盘碎片整理程序会在让文件系统上很好地运行。Linux 文件系统(特别是 ext2 和 ext3)因为其选择磁盘块的方式,在磁盘碎片整理上通常不会像 Windows 同样困难,所以不多须要手动的磁盘碎片整理。并且,固态硬盘并不受磁盘碎片的影响,事实上,在固态硬盘上作磁盘碎片整理反却是画蛇添足,不只没有提升性能,反而磨损了固态硬盘。因此碎片整理只会缩短固态硬盘的寿命。
下面咱们来探讨一下 I/O 流程问题。
什么是 I/O 设备?I/O 设备又叫作输入/输出设备,它是人类用来和计算机进行通讯的外部硬件。输入/输出设备可以向计算机发送数据(输出)
并从计算机接收数据(输入)
。
I/O 设备(I/O devices)
能够分红两种:块设备(block devices)
和 字符设备(character devices)
。
块设备是一个能存储固定大小块
信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每一个块都有本身的物理地址
。一般块的大小在 512 - 65536 之间。全部传输的信息都会以连续
的块为单位。块设备的基本特征是每一个块都较为对立,可以独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘
与字符设备相比,块设备一般须要较少的引脚。
基于给定固态存储器的块设备比基于相同类型的存储器的字节寻址要慢一些,由于必须在块的开头开始读取或写入。因此,要读取该块的任何部分,必须寻找到该块的开始,读取整个块,若是不使用该块,则将其丢弃。要写入块的一部分,必须寻找到块的开始,将整个块读入内存,修改数据,再次寻找到块的开头处,而后将整个块写回设备。
另外一类 I/O 设备是字符设备
。字符设备以字符
为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操做。常见的字符设备有 打印机、网络设备、鼠标、以及大多数与磁盘不一样的设备。
设备控制器是处理 CPU 传入和传出信号的系统。设备经过插头和插座链接到计算机,而且插座链接到设备控制器。设备控制器从链接的设备处接收数据,并将其存储在控制器内部的一些特殊目的寄存器(special purpose registers)
也就是本地缓冲区中。
每一个设备控制器都会有一个应用程序与之对应,设备控制器经过应用程序的接口经过中断与操做系统进行通讯。设备控制器是硬件,而设备驱动程序是软件。
每一个控制器都会有几个寄存器用来和 CPU 进行通讯。经过写入这些寄存器,操做系统能够命令设备发送数据,接收数据、开启或者关闭设备等。经过从这些寄存器中读取信息,操做系统可以知道设备的状态,是否准备接受一个新命令等。
为了控制寄存器
,许多设备都会有数据缓冲区(data buffer)
,来供系统进行读写。
那么问题来了,CPU 如何与设备寄存器和设备数据缓冲区进行通讯呢?存在两个可选的方式。第一种方法是,每一个控制寄存器都被分配一个 I/O 端口(I/O port)
号,这是一个 8 位或 16 位的整数。全部 I/O 端口的集合造成了受保护的 I/O 端口空间,以便普通用户程序没法访问它(只有操做系统能够访问)。使用特殊的 I/O 指令像是
IN REG,PORT
CPU 能够读取控制寄存器 PORT 的内容并将结果放在 CPU 寄存器 REG 中。相似的,使用
OUT PORT,REG
CPU 能够将 REG 的内容写到控制寄存器中。大多数早期计算机,包括几乎全部大型主机,如 IBM 360 及其全部后续机型,都是以这种方式工做的。
第二个方法是 PDP-11 引入的,它将全部控制寄存器映射到内存空间中。
不管一个 CPU 是否具备内存映射 I/O,它都须要寻址设备控制器以便与它们交换数据。CPU 能够从 I/O 控制器每次请求一个字节的数据,可是这么作会浪费 CPU 时间,因此常常会用到一种称为直接内存访问(Direct Memory Access)
的方案。为了简化,咱们假设 CPU 经过单一的系统总线访问全部的设备和内存,该总线链接 CPU 、内存和 I/O 设备,以下图所示
DMA 传送操做
现代操做系统实际更为复杂,可是原理是相同的。若是硬件有 DMA 控制器
,那么操做系统只能使用 DMA。有时这个控制器会集成到磁盘控制器和其余控制器中,但这种设计须要在每一个设备上都装有一个分离的 DMA 控制器。单个的 DMA 控制器可用于向多个设备传输,这种传输每每同时进行。
首先 CPU 经过设置 DMA 控制器的寄存器对它进行编程,因此 DMA 控制器知道将什么数据传送到什么地方。DMA 控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区并检验校验和。当有效数据位于磁盘控制器的缓冲区中时,DMA 就能够开始了。
DMA 控制器经过在总线上发出一个读请求
到磁盘控制器而发起 DMA 传送,这是第二步。这个读请求就像其余读请求同样,磁盘控制器并不知道或者并不关心它是来自 CPU 仍是来自 DMA 控制器。一般状况下,要写的内存地址在总线的地址线上,因此当磁盘控制器去匹配下一个字时,它知道将该字写到什么地方。写到内存就是另一个总线循环了,这是第三步。当写操做完成时,磁盘控制器在总线上发出一个应答信号到 DMA 控制器,这是第四步。
而后,DMA 控制器会增长内存地址并减小字节数量。若是字节数量仍然大于 0 ,就会循环步骤 2 - 步骤 4 ,直到字节计数变为 0 。此时,DMA 控制器会打断 CPU 并告诉它传输已经完成了。
在一台我的计算机体系结构中,中断结构会以下所示
中断是怎样发生的
当一个 I/O 设备完成它的工做后,它就会产生一个中断(默认操做系统已经开启中断),它经过在总线上声明已分配的信号来实现此目的。主板上的中断控制器芯片会检测到这个信号,而后执行中断操做。
使机器处于良好状态的中断称为精确中断(precise interrupt)
。这样的中断具备四个属性:
不知足以上要求的中断称为 不精确中断(imprecise interrupt)
,不精确中断让人很头疼。上图描述了不精确中断的现象。指令的执行时序和完成度具备不肯定性,并且恢复起来也很是麻烦。
I/O 软件设计一个很重要的目标就是设备独立性(device independence)
。这意味着咱们可以编写访问任何设备的应用程序,而不用事先指定特定的设备。
除了设备独立性
外,I/O 软件实现的第二个重要的目标就是错误处理(error handling)
。一般状况下来讲,错误应该交给硬件
层面去处理。若是设备控制器发现了读错误的话,它会尽量的去修复这个错误。若是设备控制器处理不了这个问题,那么设备驱动程序应该进行处理,设备驱动程序会再次尝试读取操做,不少错误都是偶然性的,若是设备驱动程序没法处理这个错误,才会把错误向上抛到硬件层面(上层)进行处理,不少时候,上层并不须要知道下层是如何解决错误的。
I/O 软件实现的第三个目标就是 同步(synchronous)
和 异步(asynchronous,即中断驱动)
传输。这里先说一下同步和异步是怎么回事吧。
同步传输中数据一般以块或帧的形式发送。发送方和接收方在数据传输以前应该具备同步时钟
。而在异步传输中,数据一般以字节或者字符的形式发送,异步传输则不须要同步时钟,可是会在传输以前向数据添加奇偶校验位
。大部分物理IO(physical I/O)
是异步的。物理 I/O 中的 CPU 是很聪明的,CPU 传输完成后会转而作其余事情,它和中断心灵相通,等到中断发生后,CPU 才会回到传输这件事情上来。
I/O 软件的最后一个问题是缓冲(buffering)
。一般状况下,从一个设备发出的数据不会直接到达最后的设备。其间会通过一系列的校验、检查、缓冲等操做才能到达。
I/O 软件引发的最后一个问题就是共享设备和独占设备的问题。有些 I/O 设备可以被许多用户共同使用。一些设备好比磁盘,让多个用户使用通常不会产生什么问题,可是某些设备必须具备独占性,即只容许单个用户使用完成后才能让其余用户使用。
一共有三种控制 I/O 设备的方法
I/O 软件一般组织成四个层次,它们的大体结构以下图所示
下面咱们具体的来探讨一下上面的层次结构
在计算机系统中,中断就像女人的脾气同样无时无刻都在产生,中断的出现每每是让人很不爽的。中断处理程序又被称为中断服务程序
或者是 ISR(Interrupt Service Routines)
,它是最靠近硬件的一层。中断处理程序由硬件中断、软件中断或者是软件异常启动产生的中断,用于实现设备驱动程序或受保护的操做模式(例如系统调用)之间的转换。
中断处理程序负责处理中断发生时的全部操做,操做完成后阻塞,而后启动中断驱动程序来解决阻塞。一般会有三种通知方式,依赖于不一样的具体实现
up
进行通知;signal
操做每一个链接到计算机的 I/O 设备都须要有某些特定设备的代码对其进行控制。这些提供 I/O 设备到设备控制器转换的过程的代码称为 设备驱动程序(Device driver)
。
设备控制器的主要功能有下面这些
接收和识别命令:设备控制器能够接受来自 CPU 的指令,并进行识别。设备控制器内部也会有寄存器,用来存放指令和参数
进行数据交换:CPU、控制器和设备之间会进行数据的交换,CPU 经过总线把指令发送给控制器,或从控制器中并行地读出数据;控制器将数据写入指定设备。
地址识别:每一个硬件设备都有本身的地址,设备控制器可以识别这些不一样的地址,来达到控制硬件的目的,此外,为使 CPU 能向寄存器中写入或者读取数据,这些寄存器都应具备惟一的地址。
差错检测:设备控制器还具备对设备传递过来的数据进行检测的功能。
在这种状况下,设备控制器会阻塞,直到中断来解除阻塞状态。还有一种状况是操做是能够无延迟的完成,因此驱动程序不须要阻塞。在第一种状况下,操做系统可能被中断唤醒;第二种状况下操做系统不会被休眠。
设备驱动程序必须是可重入
的,由于设备驱动程序会阻塞和唤醒而后再次阻塞。驱动程序不容许进行系统调用,可是它们一般须要与内核的其他部分进行交互。
I/O 软件有两种,一种是咱们上面介绍过的基于特定设备的,还有一种是设备无关性
的,设备无关性也就是不须要特定的设备。设备驱动程序与设备无关的软件之间的界限取决于具体的系统。下面显示的功能由设备无关的软件实现
与设备无关的软件的基本功能是对全部设备执行公共的 I/O 功能,而且向用户层软件提供一个统一的接口。
不管是对于块设备仍是字符设备来讲,缓冲都是一个很是重要的考量标准。缓冲技术应用普遍,但它也有缺点。若是数据被缓冲次数太多,会影响性能。
在 I/O 中,出错是一种再正常不过的状况了。当出错发生时,操做系统必须尽量处理这些错误。有一些错误是只有特定的设备才能处理,有一些是由框架进行处理,这些错误和特定的设备无关。
I/O 错误的一类是程序员编程
错误,好比尚未打开文件前就读流,或者不关闭流致使内存溢出等等。这类问题由程序员处理;另一类是实际的 I/O 错误,例如向一个磁盘坏块写入数据,不管怎么写都写入不了。这类问题由驱动程序处理,驱动程序处理不了交给硬件处理,这个咱们上面也说过。
咱们在操做系统概述中说到,操做系统一个很是重要的功能就是屏蔽了硬件和软件的差别性,为硬件和软件提供了统一的标准,这个标准还体如今为设备驱动程序提供统一的接口,由于不一样的硬件和厂商编写的设备驱动程序不一样,因此若是为每一个驱动程序都单独提供接口的话,这样无法搞,因此必须统一。
一些设备例如打印机,它只能由一个进程来使用,这就须要操做系统根据实际状况判断是否可以对设备的请求进行检查,判断是否可以接受其余请求,一种比较简单直接的方式是在特殊文件上执行 open
操做。若是设备不可用,那么直接 open 会致使失败。还有一种方式是不直接致使失败,而是让其阻塞,等到另一个进程释放资源后,在进行 open 打开操做。这种方式就把选择权交给了用户,由用户判断是否应该等待。
不一样的磁盘会具备不一样的扇区大小,可是软件不会关心扇区大小,只管存储就是了。一些字符设备能够一次一个字节的交付数据,而其余的设备则以较大的单位交付数据,这些差别也能够隐藏起来。
虽然大部分 I/O 软件都在内核结构中,可是还有一些在用户空间实现的 I/O 软件,凡事没有绝对。一些 I/O 软件和库过程在用户空间存在,而后以提供系统调用的方式实现。
盘能够说是硬件里面比较简单的构造了,同时也是最重要的。下面咱们从盘谈起,聊聊它的物理构造
盘会有不少种类型。其中最简单的构造就是磁盘(magnetic hard disks)
, 也被称为 hard disk,HDD
等。磁盘一般与安装在磁臂上的磁头配对,磁头可将数据读取或者将数据写入磁盘,所以磁盘的读写速度都一样快。在磁盘中,数据是随机访问的,这也就说明能够经过任意的顺序来存储
和检索
单个数据块,因此你能够在任意位置放置磁盘来让磁头读取,磁盘是一种非易失性
的设备,即便断电也能永久保留。
为了组织和检索数据,会将磁盘组织成特定的结构,这些特定的结构就是磁道、扇区和柱面
磁盘被组织成柱面形式,每一个盘用轴相连,每个柱面包含若干磁道,每一个磁道由若干扇区组成。软盘上大约每一个磁道有 8 - 32 个扇区,硬盘上每条磁道上扇区的数量可达几百个,磁头大约是 1 - 16 个。
对于磁盘驱动程序来讲,一个很是重要的特性就是控制器是否可以同时控制两个或者多个驱动器进行磁道寻址,这就是重叠寻道(overlapped seek)
。对于控制器来讲,它可以控制一个磁盘驱动程序完成寻道操做,同时让其余驱动程序等待寻道结束。控制器也能够在一个驱动程序上进行读写草哦作,与此同时让另外的驱动器进行寻道操做,可是软盘控制器不能在两个驱动器上进行读写操做。
RAID 称为 磁盘冗余阵列
,简称 磁盘阵列
。利用虚拟化技术把多个硬盘结合在一块儿,成为一个或多个磁盘阵列组,目的是提高性能或数据冗余。
RAID 有不一样的级别
磁盘由一堆铝的、合金或玻璃的盘片组成,磁盘刚被建立出来后,没有任何信息。磁盘在使用前必须通过低级格式化(low-levvel format)
,下面是一个扇区的格式
前导码至关因而标示扇区的开始位置,一般以位模式开始,前导码还包括柱面号
、扇区号
等一些其余信息。紧随前导码后面的是数据区,数据部分的大小由低级格式化程序来肯定。大部分磁盘使用 512 字节的扇区。数据区后面是 ECC,ECC 的全称是 error correction code ,数据纠错码
,它与普通的错误检测不一样,ECC 还能够用于恢复读错误。ECC 阶段的大小由不一样的磁盘制造商实现。ECC 大小的设计标准取决于设计者愿意牺牲多少磁盘空间来提升可靠性,以及程序能够处理的 ECC 的复杂程度。一般状况下 ECC 是 16 位,除此以外,硬盘通常具备必定数量的备用扇区,用于替换制造缺陷的扇区。
下面咱们来探讨一下关于影响磁盘读写的算法,通常状况下,影响磁盘快读写的时间由下面几个因素决定
这三种时间参数也是磁盘寻道的过程。通常状况下,寻道时间对总时间的影响最大,因此,有效的下降寻道时间可以提升磁盘的读取速度。
若是磁盘驱动程序每次接收一个请求并按照接收顺序完成请求,这种处理方式也就是 先来先服务(First-Come, First-served, FCFS)
,这种方式很难优化寻道时间。由于每次都会按照顺序处理,无论顺序如何,有可能此次读完后须要等待一个磁盘旋转一周才能继续读取,而其余柱面可以立刻进行读取,这种状况下每次请求也会排队。
一般状况下,磁盘在进行寻道时,其余进程会产生其余的磁盘请求。磁盘驱动程序会维护一张表,表中会记录着柱面号看成索引,每一个柱面未完成的请求会造成链表,链表头存放在表的相应表项中。
一种对先来先服务的算法改良的方案是使用 最短路径优先(SSF)
算法,下面描述了这个算法。
假如咱们在对磁道 6 号进行寻址时,同时发生了对 11 , 2 , 4, 14, 8, 15, 3 的请求,若是采用先来先服务的原则,以下图所示
咱们能够计算一下磁盘臂所跨越的磁盘数量为 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,至关因而跨越了 51 次盘面,若是使用最短路径优先,咱们来计算一下跨越的盘面
跨越的磁盘数量为 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了两倍的时间。
可是,最短路径优先的算法也不是天衣无缝的,这种算法照样存在问题,那就是优先级
问题,
这里有一个原型能够参考就是咱们平常生活中的电梯,电梯使用一种电梯算法(elevator algorithm)
来进行调度,从而知足协调效率和公平性这两个相互冲突的目标。电梯通常会保持向一个方向移动,直到在那个方向上没有请求为止,而后改变方向。
电梯算法须要维护一个二进制位
,也就是当前的方向位:UP(向上)
或者是 DOWN(向下)
。当一个请求处理完成后,磁盘或电梯的驱动程序会检查该位,若是此位是 UP 位,磁盘臂或者电梯仓移到下一个更高跌未完成的请求。若是高位没有未完成的请求,则取相反方向。当方向位是 DOWN
时,同时存在一个低位的请求,磁盘臂会转向该点。若是不存在的话,那么它只是中止并等待。
咱们举个例子来描述一下电梯算法,好比各个柱面获得服务的顺序是 4,7,10,14,9,6,3,1 ,那么它的流程图以下
因此电梯算法须要跨越的盘面数量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22
电梯算法一般状况下不如 SSF 算法。
通常坏块有两种处理办法,一种是在控制器中进行处理;一种是在操做系统层面进行处理。
这两种方法常常替换使用,好比一个具备 30 个数据扇区和两个备用扇区的磁盘,其中扇区 4 是有瑕疵的。
控制器能作的事情就是将备用扇区之一从新映射。
还有一种处理方式是将全部的扇区都向上移动一个扇区
上面这这两种状况下控制器都必须知道哪一个扇区,能够经过内部的表来跟踪这一信息,或者经过重写前导码来给出从新映射的扇区号。若是是重写前导码,那么涉及移动的方式必须重写后面全部的前导码,可是最终会提供良好的性能。
磁盘常常会出现错误,致使好的扇区会变成坏扇区,驱动程序也有可能挂掉。RAID 能够对扇区出错或者是驱动器崩溃提出保护,然而 RAID 却不能对坏数据中的写错误提供保护,也不能对写操做期间的崩溃提供保护,这样就会破坏原始数据。
咱们指望磁盘可以准确无误的工做,可是事实状况是不可能的,可是咱们可以知道的是,一个磁盘子系统具备以下特性:当一个写命令发给它时,磁盘要么正确地写数据,要么什么也不作,让现有的数据完整无误的保留。这样的系统称为 稳定存储器(stable storage)
。 稳定存储器的目标就是不惜一切代价保证磁盘的一致性。
稳定存储器使用两个一对相同的磁盘,对应的块一同工做造成一个无差异的块。稳定存储器为了实现这个目的,定义了下面三种操做:
稳定写(stable write)
稳定读(stable read)
崩溃恢复(crash recovery)
时钟(Clocks)
也被称为定时器(timers)
,时钟/定时器对任何程序系统来讲都是必不可少的。时钟负责维护时间、防止一个进程长期占用 CPU 时间等其余功能。时钟软件(clock software)
也是一种设备驱动的方式。下面咱们就来对时钟进行介绍,通常都是先讨论硬件再介绍软件,采用由下到上的方式,也是告诉你,底层是最重要的。
在计算机中有两种类型的时钟,这些时钟与现实生活中使用的时钟彻底不同。
电压周期
会产生一个中断,大概是 50 - 60 HZ。这些时钟过去一直占据支配地位。这种时钟称为可编程时钟
,可编程时钟有两种模式,一种是 一键式(one-shot mode)
,当时钟启动时,会把存储器中的值复制到计数器中,而后,每次晶体的振荡器的脉冲都会使计数器 -1。当计数器变为 0 时,会产生一个中断,并中止工做,直到软件再一次显示启动。还有一种模式时 方波(square-wave mode)
模式,在这种模式下,当计数器变为 0 并产生中断后,存储寄存器的值会自动复制到计数器中,这种周期性的中断称为一个时钟周期。
时钟硬件所作的工做只是根据已知的时间间隔产生中断,而其余的工做都是由时钟软件
来完成,通常操做系统的不一样,时钟软件的具体实现也不一样,可是通常都会包括如下这几点
时钟软件也被称为可编程时钟,能够设置它以程序须要的任何速率引起中断。时钟软件触发的中断是一种硬中断,可是某些应用程序对于硬中断来讲是不可接受的。
这时候就须要一种软定时器(soft timer)
避免了中断,不管什么时候当内核由于某种缘由呢在运行时,它返回用户态以前都会检查时钟来了解软定时器是否到期。若是软定时器到期,则执行被调度的事件也无需切换到内核态,由于自己已经处于内核态中。这种方式避免了频繁的内核态和用户态以前的切换,提升了程序运行效率。
软定时器由于不一样的缘由切换进入内核态的速率不一样,缘由主要有
死锁问题也是操做系统很是重要的一类问题
大部分的死锁都和资源有关,在进程对设备、文件具备独占性(排他性)时会产生死锁。咱们把这类须要排他性使用的对象称为资源(resource)
。资源主要分为 可抢占资源和不可抢占资源
资源主要有可抢占资源和不可抢占资源。可抢占资源(preemptable resource)
能够从拥有它的进程中抢占而不会形成其余影响,内存就是一种可抢占性资源,任何进程都可以抢先得到内存的使用权。
不可抢占资源(nonpreemtable resource)
指的是除非引发错误或者异常,不然进程没法抢占指定资源,这种不可抢占的资源好比有光盘,在进程执行调度的过程当中,其余进程是不能获得该资源的。
若是要对死锁进行一个定义的话,下面的定义比较贴切
若是一组进程中的每一个进程都在等待一个事件,而这个事件只能由该组中的另外一个进程触发,这种状况会致使死锁。
针对咱们上面的描述,资源死锁可能出现的状况主要有
发生死锁时,上面的状况必须同时会发生。若是其中任意一个条件不会成立,死锁就不会发生。能够经过破坏其中任意一个条件来破坏死锁,下面这些破坏条件就是咱们探讨的重点
Holt 在 1972 年提出对死锁进行建模,建模的标准以下:
从资源节点到进程节点表示资源已经被进程占用,以下图所示
在上图中表示当前资源 R 正在被 A 进程所占用
由进程节点到资源节点的有向图表示当前进程正在请求资源,而且该进程已经被阻塞,处于等待这个资源的状态
在上图中,表示的含义是进程 B 正在请求资源 S 。Holt 认为,死锁的描述应该以下
这是一个死锁的过程,进程 C 等待资源 T 的释放,资源 T 却已经被进程 D 占用,进程 D 等待请求占用资源 U ,资源 U 却已经被线程 C 占用,从而造成环。
有四种处理死锁的策略:
下面咱们分别介绍一下这四种方法
最简单的解决办法就是使用鸵鸟算法(ostrich algorithm)
,把头埋在沙子里,伪装问题根本没有发生。每一个人看待这个问题的反应都不一样。数学家认为死锁是不可接受的,必须经过有效的策略来防止死锁的产生。工程师想要知道问题发生的频次,系统由于其余缘由崩溃的次数和死锁带来的严重后果。若是死锁发生的频次很低,而常常会因为硬件故障、编译器错误等其余操做系统问题致使系统崩溃,那么大多数工程师不会修复死锁。
第二种技术是死锁的检测和恢复。这种解决方式不会尝试去阻止死锁的出现。相反,这种解决方案会但愿死锁尽量的出现,在监测到死锁出现后,对其进行恢复。下面咱们就来探讨一下死锁的检测和恢复的几种方式
每种资源类型都有一个资源是什么意思?咱们常常提到的打印机就是这样的,资源只有打印机,可是设备都不会超过一个。
能够经过构造一张资源分配表来检测这种错误,好比咱们上面提到的
若是这张图包含了一个或一个以上的环,那么死锁就存在,处于这个环中任意一个进程都是死锁的进程。
若是有多种相同的资源存在,就须要采用另外一种方法来检测死锁。能够经过构造一个矩阵来检测从 P1 -> Pn 这 n 个进程中的死锁。
如今咱们提供一种基于矩阵的算法来检测从 P1 到 Pn 这 n 个进程中的死锁。假设资源类型为 m,E1 表明资源类型1,E2 表示资源类型 2 ,Ei 表明资源类型 i (1 <= i <= m)。E 表示的是 现有资源向量(existing resource vector)
,表明每种已存在的资源总数。
如今咱们就须要构造两个数组:C 表示的是当前分配矩阵(current allocation matrix)
,R 表示的是 请求矩阵(request matrix)
。Ci 表示的是 Pi 持有每一种类型资源的资源数。因此,Cij 表示 Pi 持有资源 j 的数量。Rij 表示 Pi 所须要得到的资源 j 的数量
通常来讲,已分配资源 j 的数量加起来再和全部可供使用的资源数相加 = 该类资源的总数。
死锁的检测就是基于向量的比较。每一个进程起初都是没有被标记过的,算法会开始对进程作标记,进程被标记后说明进程被执行了,不会进入死锁,当算法结束时,任何没有被标记过的进程都会被断定为死锁进程。
上面咱们探讨了两种检测死锁的方式,那么如今你知道怎么检测后,你什么时候去作死锁检测呢?通常来讲,有两个考量标准:
上面咱们探讨了如何检测进程死锁,咱们最终的目的确定是想让程序可以正常的运行下去,因此针对检测出来的死锁,咱们要对其进行恢复,下面咱们会探讨几种死锁的恢复方式
在某些状况下,可能会临时将某个资源从它的持有者转移到另外一个进程。好比在不通知原进程的状况下,将某个资源从进程中强制取走给其余进程使用,使用完后又送回。这种恢复方式通常比较困难并且有些简单粗暴,并不可取。
若是系统设计者和机器操做员知道有可能发生死锁,那么就能够按期检查流程。进程的检测点意味着进程的状态能够被写入到文件以便后面进行恢复。检测点不只包含存储映像(memory image)
,还包含资源状态(resource state)
。一种更有效的解决方式是不要覆盖原有的检测点,而是每出现一个检测点都要把它写入到文件中,这样当进程执行时,就会有一系列的检查点文件被累积起来。
为了进行恢复,要从上一个较早的检查点上开始,这样所须要资源的进程会回滚到上一个时间点,在这个时间点上,死锁进程尚未获取所须要的资源,能够在此时对其进行资源分配。
最简单有效的解决方案是直接杀死一个死锁进程。可是杀死一个进程可能照样行不通,这时候就须要杀死别的资源进行恢复。
另一种方式是选择一个环外的进程做为牺牲品来释放进程资源。
咱们上面讨论的是如何检测出现死锁和如何恢复死锁,下面咱们探讨几种规避死锁的方式
银行家算法是 Dijkstra 在 1965 年提出的一种调度算法,它自己是一种死锁的调度算法。它的模型是基于一个城镇中的银行家,银行家向城镇中的客户承诺了必定数量的贷款额度。算法要作的就是判断请求是否会进入一种不安全的状态。若是是,就拒绝请求,若是请求后系统是安全的,就接受该请求。
相似的,还有多个资源的银行家算法,读者能够自行了解。
死锁本质上是没法避免的,由于它须要得到未知的资源和请求,可是死锁是知足四个条件后才出现的,它们分别是
咱们分别对这四个条件进行讨论,按理说破坏其中的任意一个条件就可以破坏死锁
咱们首先考虑的就是破坏互斥使用条件。若是资源不被一个进程独占,那么死锁确定不会产生。若是两个打印机同时使用一个资源会形成混乱,打印机的解决方式是使用 假脱机打印机(spooling printer)
,这项技术能够容许多个进程同时产生输出,在这种模型中,实际请求打印机的惟一进程是打印机守护进程,也称为后台进程。后台进程不会请求其余资源。咱们能够消除打印机的死锁。
后台进程一般被编写为可以输出完整的文件后才能打印,假如两个进程都占用了假脱机空间的一半,而这两个进程都没有完成所有的输出,就会致使死锁。
所以,尽可能作到尽量少的进程能够请求资源。
第二种方式是若是咱们能阻止持有资源的进程请求其余资源,咱们就可以消除死锁。一种实现方式是让全部的进程开始执行前请求所有的资源。若是所需的资源可用,进程会完成资源的分配并运行到结束。若是有任何一个资源处于频繁分配的状况,那么没有分配到资源的进程就会等待。
不少进程没法在执行完成前就知道到底须要多少资源,若是知道的话,就能够使用银行家算法;还有一个问题是这样没法合理有效利用资源。
还有一种方式是进程在请求其余资源时,先释放所占用的资源,而后再尝试一次获取所有的资源。
破坏不可抢占条件也是能够的。能够经过虚拟化的方式来避免这种状况。
如今就剩最后一个条件了,循环等待条件能够经过多种方法来破坏。一种方式是制定一个标准,一个进程在任什么时候候只能使用一种资源。若是须要另一种资源,必须释放当前资源。对于须要将大文件从磁带复制到打印机的过程,此限制是不可接受的。
另外一种方式是将全部的资源统一编号,以下图所示
进程能够在任什么时候间提出请求,可是全部的请求都必须按照资源的顺序提出。若是按照此分配规则的话,那么资源分配之间不会出现环。
尽管经过这种方式来消除死锁,可是编号的顺序不可能让每一个进程都会接受。
下面咱们来探讨一下其余问题,包括 通讯死锁、活锁是什么、饥饿问题和两阶段加锁
虽然不少状况下死锁的避免和预防都能处理,可是效果并很差。随着时间的推移,提出了不少优秀的算法用来处理死锁。例如在数据库系统中,一个常常发生的操做是请求锁住一些记录,而后更新全部锁定的记录。当同时有多个进程运行时,就会有死锁的风险。
一种解决方式是使用 两阶段提交(two-phase locking)
。顾名思义分为两个阶段,一阶段是进程尝试一次锁定它须要的全部记录。若是成功后,才会开始第二阶段,第二阶段是执行更新并释放锁。第一阶段并不作真正有意义的工做。
若是在第一阶段某个进程所须要的记录已经被加锁,那么该进程会释放全部锁定的记录并从新开始第一阶段。从某种意义上来讲,这种方法相似于预先请求全部必需的资源或者是在进行一些不可逆的操做以前请求全部的资源。
不过在通常的应用场景中,两阶段加锁的策略并不通用。若是一个进程缺乏资源就会半途中断并从新开始的方式是不可接受的。
咱们上面一直讨论的是资源死锁,资源死锁是一种死锁类型,但并非惟一类型,还有通讯死锁,也就是两个或多个进程在发送消息时出现的死锁。进程 A 给进程 B 发了一条消息,而后进程 A 阻塞直到进程 B 返回响应。假设请求消息丢失了,那么进程 A 在一直等着回复,进程 B 也会阻塞等待请求消息到来,这时候就产生死锁
。
尽管会产生死锁,可是这并非一个资源死锁,由于 A 并无占据 B 的资源。事实上,通讯死锁并无彻底可见的资源。根据死锁的定义来讲:每一个进程由于等待其余进程引发的事件而产生阻塞,这就是一种死锁。相较于最多见的通讯死锁,咱们把上面这种状况称为通讯死锁(communication deadlock)
。
通讯死锁不能经过调度的方式来避免,可是能够使用通讯中一个很是重要的概念来避免:超时(timeout)
。在通讯过程当中,只要一个信息被发出后,发送者就会启动一个定时器,定时器会记录消息的超时时间,若是超时时间到了可是消息尚未返回,就会认为消息已经丢失并从新发送,经过这种方式,能够避免通讯死锁。
可是并不是全部网络通讯发生的死锁都是通讯死锁,也存在资源死锁,下面就是一个典型的资源死锁。
当一个数据包从主机进入路由器时,会被放入一个缓冲区,而后再传输到另一个路由器,再到另外一个,以此类推直到目的地。缓冲区都是资源而且数量有限。以下图所示,每一个路由器都有 10 个缓冲区(实际上有不少)。
假如路由器 A 的全部数据须要发送到 B ,B 的全部数据包须要发送到 D,而后 D 的全部数据包须要发送到 A 。没有数据包能够移动,由于在另外一端没有缓冲区可用,这就是一个典型的资源死锁。
某些状况下,当进程意识到它不能获取所须要的下一个锁时,就会尝试礼貌的释放已经得到的锁,而后等待很是短的时间再次尝试获取。能够想像一下这个场景:当两我的在狭路相逢的时候,都想给对方让路,相同的步调会致使双方都没法前进。
如今假想有一对并行的进程用到了两个资源。它们分别尝试获取另外一个锁失败后,两个进程都会释放本身持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程当中没有进程阻塞,可是进程仍然不会向下执行,这种情况咱们称之为 活锁(livelock)
。
与死锁和活锁的一个很是类似的问题是 饥饿(starvvation)
。想象一下你何时会饿?一段时间不吃东西是否是会饿?对于进程来说,最重要的就是资源,若是一段时间没有得到资源,那么进程会产生饥饿,这些进程会永远得不到服务。
咱们假设打印机的分配方案是每次都会分配给最小文件的进程,那么要打印大文件的进程会永远得不到服务,致使进程饥饿,进程会无限制的推后,虽然它没有阻塞。
关注二维码回复"os脑图"便可获取高清思惟导图
回复 "os" 领取操做系统 PDF