现代操作系统 Andrew S. Tanenbaum,Herbert Bos

第一章 引论

  • 操作系统的任务是在相互竞争的程序之间有序地控制对处理器、存储器以及其他IO接口设备的分配。
  • 微内核的设计思想:为了实现可靠性,将操作系统划分为小的、定义良好的模块,只有其中一个模块——微内核——运行在内核态,其余的模块由于功能相对弱些,则作为普通用户进程运行。
  • 第一类和第二类虚拟机管理程序的真正区别在于,后者利用宿主操作系统并通过其文件系统创建进程、存储文件等。第一类虚拟机管理程序没有底层支持,所以必须自行实现这些功能。
  • make的作用:在构建操作系统二进制码时,检查此刻需要那个目标文件,而且对于每个文件,检查自从上次目标文件创建之后是否有任何它以来的文件已经被修改。如果有,目标文件需要重新编译。
  • GPU指的是由成千上万个微核组成的处理器。它们擅长处理大量并行的简单计算,比如在图像应用中渲染多边形。它们不太能胜任串行任务,并且很难编程。虽然GPU对操作系统很有用(比如加密或者处理网络传输),但操作系统本身不太能运行在GPU上。
  • UNIX特殊文件:提供特殊文件是为了使IO设备看起来像文件一般。
  • 块特殊文件:那些可以随机存取的块组成的设备,如磁盘等
  • 字符特殊文件:用于打印机、调制解调器和其他接收或输出字符流的设备。
  • UNIX中的进程将其存储空间划分为三段:正文段(如程序代码)。数据段(如变量)以及堆栈段。数据向上增长而堆栈向下生长。
  • 实现输入输出的方式有三种:
    • 忙等待:用户程序发出一个系统调用,内核将其翻译成一个对应驱动程序的过程调用。然后设备驱动程序启动IO并在一个连续不断的循环中检查该设备。当IO结束后,设备驱动程序把数据送到指定的地方,并返回。缺点是要占据CPU,CPU一直轮询设备直到对应的IO操作完成。
    • 中断:设备驱动程序启动设备并且让该设备在操作完成时发出一个中断。设备驱动程序在这个时刻返回。当设备驱动程序检测到该设备的操作完毕时,它发出一个中断通知操作完成。
    • DMA(直接存储器访问):可以控制在内存和某些控制器之间的位流,而无需持续的CPU干预。

第二章 进程

  • 可写的内存是不可复制的。写时复制,开始共享的是只读的内存,当发生写操作时,复制页面写。
  • 中断发生后操作系统最底层的工作步骤:
    • 硬件压入堆栈程序计数器等
    • 硬件从中断向量表装入新的程序计数器
    • 汇编语言过程保存寄存器值
    • 汇编语言过程设置新的堆栈
    • C中断服务例程运行(典型地读和缓冲输入)
    • 调度程序决定下一个将运行的进程
    • C过程返回至汇编代码
    • 汇编语言过程开始运行新的当前进程
  • 多道程序设计模型:
    • CPU利用率 = 1- p^n
    • p为进程等待IO操作的时间与其停留在内存中的比。n为内存中存在n个进程。有n个进程都在等待IO(此时cpu空转)的概率是p^n
  • 线程:并行实体拥有共享同一地址空间和所有可用数据的能力。
  • 多线程对于CPU密集型的应用,并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多线程允许这些活动彼此重叠进行,从而加快应用程序执行的速度。
  • 用户空间实现线程
    • 每个进程需要有专用的线程表,用来跟踪进程中的线程
    • 线程和进程有一个关键区别:在线程完成运行时,保存该线程状态的过程和调度程序都是本地过程,所以启动它们比进行内核调用效率更高,另一方面,不需要陷入内核,不需要上下文切换,也不需要对内存高速缓存进行刷新,这就使得线程调度非常快捷。
    • 允许每个进程自己实现定制的调度算法。
    • 用户及线程还有较好的可扩展性,这是因为在内核空间中内核线程需要一些固定表格空间和堆栈空间,如果内核线程的数量非常大,就会出现问题
    • 问题:
      • 阻塞调用:让线程进行阻塞系统调用是不可接受的,因为这会停止所有线程。可以把系统调用全部改为非阻塞的,但这需要修改操作系统,不具有吸引力。也可以采用提前通知,如read调用,在UNIX上可以利用select调用提前预期read是否会阻塞
      • 缺页中断:当有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘I/O完成为止,尽管其他线程是可以运行的。
      • 进程内的线程,如果一个线程开始运行,那么该进程中的其他线程就不能运行,除非第一个线程主动放弃CPU。在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度(轮流)的方式调度线程。
  • 在内核中实现线程
  • 混合模型 多线程对多内核线程

竞争条件

  • race condition:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。
  • 临界区:共享内存进行访问的程序片段称为临界区域。
  • 忙等待的互斥:
    • 好的解决方案的条件
        1. 任何两个进程不能同时处于其临界区
        1. 不应对CPU的速度和数量做任何假设
        1. 临界区外运行的进程不得阻塞其他进程。
        1. 不得使进程无限期等待进入临界区
    • 屏蔽中断:在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽中断,并在就要离开之前再打开中断。
      • 屏蔽中断后,时钟中断也被屏蔽。CPU只有发生时钟中断或其他中断后才会进行进程切换,这样在屏蔽中断之后CPU将不会切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不必担心其他进程介入。
      • 这个方案并不好,因为把屏蔽中断的权利交给用户是不明智的。若进程屏蔽中断之后不打开,结果会导致系统终止。
      • 如果系统是多处理器,则屏蔽中断仅仅对执行disable指令的那个CPU有效。其他CPU仍将继续运行。屏蔽一个CPU的中断不会阻止其他CPU干预第一个CPU所做的操作。
      • 但是对于内核来说,当它更新变量或列表的几条指令期间将中断屏蔽是很方便的。当就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件。所以结论是:屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。
    • 严格轮换法:当进程0离开临界区时,将turn设置为1,以便允许进程1进入其临界区。假设进程1很快便离开了临界区,此时两个进程都在临界区之外,turn又被设置为0,进程0很快执行完整个循环,退出临界区,将turn设置为1,此时,两个进程都处于临界区外执行。突然进程0结束了临界区的操作并返回循环的开始,但是,它不能进入临界区,因为当前turn为1,而此时进程1还在忙于非临界区的操作,进程0只能循环等待,直到进程1把turn值改为0。这说明,在一个进程比另一个慢很多的情况下,轮流进入临界区并不是一个好办法。也违反了竞争条件3:进程0被一个临界区之外的进程阻塞。
    // 进程 0
    while(TRUE){
      while(turn != 0); // 循环
      critical_region();
      turn = 1;
      noncritical_region();
    }
    // 进程 1
    while(TRUE){
      while(turn != 1); // 循环
      critical_region();
      turn = 0;
      noncritical_region();
    }
    
    • Peterson解法
    #define FALSE 0
    #define TRUE 1
    #define N 2
    int turn;
    int interested[N];
    
    void enter_region(int process){
      int other;
      other = 1- process;
      interested[process] = TRUE;
      turn = process;
      while(turn == process && interested[other] = TRUE);
    }
    
    void leave_region(int process){
      interested[process] = FALSE;
    }
    
    • TSL指令
      • TSL(test and set lock):测试并加锁,它将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的,即该指令结束之前其他处理器均不允许访问该内存字。执行TSL指令指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。
      • 锁住总线不同于屏蔽中断。屏蔽中断,然后在读内存字之后跟着写操作并不能阻止总线上的第二个处理器在读操作和写操作之间访问该内存字。事实上,在处理器1上屏蔽中断对处理器2根本没有任何影响。让处理器2远离内存知道处理器1完成的唯一方法就是锁住总线。
      • 为了使用TSL指令,要使用一个共享变量lock来协调对共享内存的访问。当lock为0时,任何进程都可以使用TSL指令将其设置为1,并读写共享内存。当操作结束时,进程用一条普通的move指令将lock的值重新设置为0.
      enter_region:
        TSL REGISTER,LOCK ; 复制锁到寄存器并将锁设为1
        CMP REGISTER,#0   ; 锁是零吗?
        JNE enter_region  ; 若不是零,说明锁已被设置,所以循环
        RET               ; 返回调用者,进入临界区
      
      leave_region:
        MOVE LOCK,#0  ; 在锁中存入0
        RET           ; 返回调用者
      
      • XCHG:原子的交换两个位置的内容。所有的Intel x86 CPU在底层同步使用XCHG指令
      enter_region:
        MOVE REGISTER,#1    ; 在寄存器中放一个1
        XCHG REGISTER,LOCK ; 交换寄存器与锁变量
        CMP REGISTER,#0   ; 锁是零吗?
        JNE enter_region  ; 若不是零,说明锁已被设置,所以循环
        RET               ; 返回调用者,进入临界区
      
      leave_region:
        MOVE LOCK,#0  ; 在锁中存入0
        RET           ; 返回调用者
      

信号量

  • 设立两个操作:down和up(sleep和wakeup)
    • down:对一信号量执行down操作,则是检查其值是否大于0.若该值大于0,则将其减1(即用掉一个保存的唤醒信号)并继续;若该值为0,则进程将睡眠,而且此时down操作并未结束。检查数值、修改变量值以及可能发生的睡眠操作均作为一个单一的、不可分割的原子操作完成。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程不允许访问该信号量。
    • up:up操作对信号量增1.如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由操作系统选择其中的一个(如随机挑选)并允许该进程完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作之后,该信号量的值仍为0,但是其上睡眠的进程却少了一个。信号量的值增1和唤醒一个进程同样是不可分割的。
  • 通常up和down作为系统调用实现,而且操作系统只需在执行以下操作时屏蔽全部中断:测试信号量、更新信号量以及在需要时使某个进程睡眠。由于这些动作只需要几条指令,所以屏蔽中断不会带来什么副作用。如果使用多个CPU,则每隔信号量应由一个锁变量进行保护。通过TSL或XCHG指令来确保同一时刻只有一个CPU在对信号量进行操作。
  • 使用TSL或XCHG指令来防止几个CPU同时访问一个信号量,这与生产者或消费者使用忙等待来等待对方腾出或填充缓冲区是完全不同的。信号量操作仅需几个毫秒,而生产者或消费者则可能需要任意长的时间。

互斥量

  • 互斥量是一个可以处于两态之一的变量:解锁和加锁。
  • 用户级线程包互斥量实现:
mutex_lock:
    TSL REGISTER,MUTEX  ; 将互斥信号量复制到寄存器,并且将互斥信号量置为1
    CMP REGISTER,#0     ; 互斥信号量是0吗?
    JZE ok              ; 如果互斥信号量为0,它将解锁,所以返回
    CALL thread_yield   ; 互斥信号量忙;调度另一个线程
    JMP mutex_lock      ; 稍后再试
ok: RET                 ; 返回调用者;进入临界区    

mutex_unlock:
    MOVE MUTEX,#0       ; 将mutex置为0
    RET                 ; 返回调用者
  • 快速用户区互斥量futex:它实现了基本的锁,但避免了陷入内核,除非它真的不得不这样做。
    • 一个futex包含两个部分:一个内核服务和一个用户库。内核服务提供一个等待队列,它允许多个进程在一个锁上等待。它们将不会运行,除非内核明确地对它们解除阻塞。讲一个进程放到等待队列需要(代价很大的)系统调用,我们应该避免这种情况。因此,没有竞争时,futex完全在用户空间工作。这些进程共享一个锁变量。如果锁被一个线程持有,另一个线程需要锁,这时futex不自旋,而是使用一个系统调用把这个线程放在内核的等待队列上。当一个线程使用完锁,它通过原子操作"增加并检验"来释放锁,并检查结果,看是否任有进程阻塞在内核等待队列上。如果有,它会通知内核可以对等待队列里的一个或多个进程解除阻塞。如果没有锁竞争,内核则不需要参与其中。
  • pthread中的互斥量
    • pthread_mutex_init:创建一个互斥量
    • pthread_mutex_destroy:撤销一个已存在的互斥量
    • pthread_mutex_lock:获得一个锁或阻塞
    • pthread_mutex_trylock:获得一个锁或失败
    • pthread_mutex_unlock:释放一个锁
  • 条件变量:互斥量在允许或阻塞对临界区的访问很有效,条件变量则允许线程由于一些未达到的条件而阻塞。
    • pthread_cond_init
    • pthread_cond_destroy
    • pthread_cond_wait:阻塞以等待一个信号
    • pthread_cond_signal:向另一个线程发信号来唤醒它
    • pthread_cond_broadcast:向多个线程发信号来让它们全部唤醒。
  • 条件变量和互斥量的使用
package main

import (
	"fmt"
	"sync"
)

func main() {
	var buffer = 0
	lock := new(sync.Mutex)
	condProducer := sync.NewCond(lock) // 生产者条件变量
	condConsumer := sync.NewCond(lock)	// 消费者条件变量
	go func() {
		for i := 1; i < 10; i++ {
			lock.Lock()
			for buffer != 0 {
				condProducer.Wait() // buffer存在数据,则生产者阻塞
			}
			buffer = i
			fmt.Println("producer:", buffer)
			//time.Sleep(1*time.Second)
			condConsumer.Signal()	// 生产完成通知消费者
			lock.Unlock()
		}

	}()
	for i := 1; i < 10; i++ {
		lock.Lock()
		for buffer == 0 {
			condConsumer.Wait() // 无数据,消费者阻塞
		}
		fmt.Println("consumer:", buffer)
		buffer = 0
		condProducer.Signal()	// 消费完成,通知生产者
		lock.Unlock()
	}
}

管程monitor

  • 一个管程是由过程、变量和数据结构等组成的一个集合,它们组成一个特殊的模块和软件包。进程可在任何需要的时候调用管程中的过程,但它们不能在管程之外声明的过程中直接访问管程内的数据结构。
  • 任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。管程是编译语言的组成部分,编译器知道怎么处理它们。典型的处理办法是,当一个进程调用管程过程时,该过程中的前几条指令将检查在管程中是否有其他活跃进程。如果有,调用进程将被挂起,直到另一个进程离开管程将其唤醒。如果没有活跃进程在使用管程,则该调用进程可以进入。
  • 尽管管程提供了一种实现互斥的简便途径,但这还不够,还需要一种办法使得进程在无法继续运行时阻塞。
  • java的synchronized关键字实现了管程
  • 消息传递:send和receive
  • 屏障barrier:用于进程组,当进程组所有的进程都就绪准备着手下一阶段,否则任何进程都不能进入下一阶段。可以通过在每个阶段结尾安置屏障来实现这种行为。
  • 避免锁:读-复制-更新 RCU,最快的锁就是根本没有锁。

调度

  • 调度:为了选择正确的进程的运行,调度程序还要考虑cpu的利用率,因为进程切换的代价是比较高的。首先用户态必须切换到内核态,然后要保存当前进程的状态,包括在进程表中存储寄存器值以便以后重新加载。在许多系统中,内存映像(例如,页表内的内存访问位)也必须保存;接着,通过运行调度算法选定一个新进程;之后,应该将新进程的内存映像重新装入mmu,最后新进程开始运行。除此之外,进程切换还要使内存告诉缓存失效,强迫缓存从内存中动态重新装入两次(进入内核一次,离开内核一次)。总之,如果每秒钟切换进程的次数太多,会耗费大量cpu时间,所以有必要提醒注意。
  • 当一个进程等待外部设备完成工作而被阻塞时,才是I/O活动。
  • 何时调度
    • 在创建一个新进程之后,需要决定是运行父进程还是运行子进程。由于两种进程都处于就绪状态,所以这是一种正常的调度决策,可以任意决定,也就是说调度程序可以合法地选择先运行父进程还是子进程,
    • 在一个进程退出是必须做出调度决策。一个进程不再运行(因为它不再存在),所以必须从就绪进程集中选择另外的进程。如果没有就绪的进程,通常会运行一个系统提供的空闲进程。
    • 当一个进程阻塞在IO和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。有时,阻塞的原因会成为选择的因素。例如,如果A是一个重要的进程,并正在等待B退出临界区,从而可以让A运行。不过问题是,通常调度程序并不拥有做出这种相关考虑的必要信息。
    • 在一个I/O中断发生时,必须做出调度决策。如果中断来自IO设备,而该设备现在完成了工作,某些被阻塞的等待该I/O的进程就成为可运行的就绪进程。是否让新就绪的进程运行,这取决于调度程序的决定,或者让中断发生时运行的进程继续运行,或者应该让某个其他进程运行。
  • 非抢占式调度算法:选择一个进程,然后让该进程运行直至被阻塞(阻塞在IO上或者等待另一个进程),或者直到该进程自动释放CPU,即使该进程运行若干个小时,它也不会被强迫挂起。这样做的结果是,在时钟中断发生时,不会进行调度。在处理完时钟中断后,如果没有更高优先级的进程等待到时,则被中断的进程会继续执行。
  • 抢占式调度算法:挑选一个进程,并且让该进程运行某个固定时段的最大值。如果该时段结束时,该进程仍在运行,它就被挂起,而调度程序挑选另一个进程运行(如果存在一个就绪进程)。进行抢占式调度处理,需要在时间间隔末端发生时钟中断,以便把CPu控制返回给调度程序。如果没有可用的时钟,那么非抢占式调度就是唯一的选择了。
  • 批处理系统的调度算法
    • 先来先服务
    • 最短作业优先
    • 最短剩余时间优先
  • 交互系统的调度:
    • 轮转调度:分成时间片,假设所有进程优先级相同
    • 优先级调度:每个进程都赋予一个优先级,允许优先级最高的可运行进程优先运行
    • 多级队列:
    • 最短进程优先
    • 保证调度:
    • 彩票调度:把系统资源分为固定的彩票数
    • 公平分享调度
  • 实时系统是一种时间起着主导作用的系统。
  • 用户级线程和内核级线程之间的差别在于性能。用户级线程的线程切换需要少量的机器指令,而内核级线程需要完整的上下文切换,修改内存映射,使高速缓存失效,这导致了若干数量级的延迟。另一方面,在使用内核级线程时,一旦线程阻塞在I/O上就不需要像在用户级线程中那样将整个进程挂起。用户级线程可以使用专为应用程序定制的线程调度程序。

第三章 内存管理

  • 地址空间为程序创造了一种抽象的内存,地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。
  • 动态重定位:简单滴把每个进程的地址空间映射到物理内存的不同部分。
    • 基址寄存器和界限寄存器:当运行一个程序时,程序的起始物理地址装载到基址寄存器,程序的长度装载到界限寄存器。
    • 缺点:每次访问内存都需要进行加法和比较运算。
  • 内存超载处理办法:
    • 交换技术:一个进程完整调入内存,让该进程运行一段时间,然后换回磁盘。交换在内存中产生了许多空闲区,可以把所有进程尽可能下移,把这些小空间合并成一大块,叫内存紧缩。
    • 虚拟内存:使程序在只有一部分被调入内存的情况下运行。基本思想是每个程序拥有自己的地址空间,这个空间分割成多个块,每一块称作一页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立即执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责把缺失的部分装入物理内存并重新执行失败的指令。
  • 空闲内存管理:
    • 位图:内存可以划分为小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)
    • 空闲区链表:维护一个记录已分配内存段和空闲内存段的链表。
      • 首次适配算法:存储管理器沿着段链表进行搜索,直到找到一个足够大的空闲区,供新进程使用
      • 下次适配算法:对首次适配做很小的修改;每次找到空闲区,都记录当前位置,下次从这里开始寻找
      • 最佳适配:找到能容纳进程的最小空闲区
      • 最差适配:找到能容纳进程的最大空闲区
      • 快速适配:维护多个常用大小的空闲区链表,如4kB、8KB、16KB等
  • 虚拟内存思想:每个进程拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页或页面(page)。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并执行失败的指令。
  • 在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟内存不是被直接送到内存总线上,而是被送到内存管理单元MMU,MMU把虚拟地址映射到物理内存地址。
  • 虚拟地址空间按照固定大小划分成被称为页面的若干单元。在物理内存中对应的单元称为页框。页面和页框的大小通常是一样的。
  • 页表:用页号作为页表的索引,以得出对应于虚拟页面的页框号。
  • 快表:TLB,将虚拟地址直接映射到物理地址,而不必再访问页表
  • 页表:二级页表 10+10+12 ;四级页表 9+9+9+9+12,寻址256TB,4KB页
  • 倒排页表:
    • 反向页表:顾名思义,以页帧号为index,页号地址为value,每次访问将value和逻辑地址比对,这样做的原因就是大大节省了内存的开销,全局只需要一张页表,但是当物理内存特别大的时候,这个表也就很大了,访问一个地址可能需要遍历整个表(因为是按照物理地址建立的,所以要挨个访问、判断),那么有什么方法是可以缓减访问速度的压力吗,那就是基于hash表的访问或者TLB
    • 现代操作系统 Andrew S. Tanenbaum,Herbert Bos_第1张图片
    • 很明显,由于访问查找的比对是根据页号,而index是页帧号(ppn),所以只能从第一个页帧号开始,一个一个做比对(PID是当前进程的标识),速度较慢
    • 现代操作系统 Andrew S. Tanenbaum,Herbert Bos_第2张图片
    • hash表的作用:将页号即图中的做一个hash计算(使用硬件加速)找到对应的hash index,hash index的值0x0是一个页帧号,也就是反向页表的index,但是这样可能会存在hash冲突,因此在传参的时候需要传入当前进程的PID作为标识,以确保找到对应的页帧号,NEXT中存放的是由于hash冲突导致的相同hash值的下一个条目的index(页帧号),这样就不需要一个一个进行比对了。如图,0x1经过计算得到0x0,因此去访问反向页表的index为0x0这个条目,比对之后发现PID不对,因此访问NEXT,找到相同hash值的下一个条目…找到之后,index+offset即为物理访问地址
  • 当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本;如果该页面没有被修改过(如一个包含程序正文的页面),那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面就可以了。
  • 置换算法:
    • 最优页面置换算法:不可能实现,提前知道那个页面不常用
    • 最近未使用页面置换算法:
    • 先进先出页面置换算法FIFO:新页面加入链表尾端,淘汰链表头页面。
    • 第二次机会页面置换算法:FIFO算法可能把经常使用的页面置换出去,对该算法进行修改,检查老页面的R位,如果为0,那么这个页面又老又没用,可以立刻置换。如果为1,就将R位清0,并把他放在链表尾端,修改它的装入时间和它刚装入一样,继续搜索。这样相当于有了二次机会
    • 时钟页面置换算法:采用环形链表,如果R为0,淘汰页面,R为1,置为0,向前移动一位
    • 最近最少使用页面置换算法LRU:在缺页中断发生时,置换未使用时间最长的页面。
    • 软件模拟LRU:采用计数器来代替时间统计,淘汰计数器最小的页面。计数器采用老化算法:每次添加计数器,先向右移一位,R位加到计数器最左端的位上。
    • 工作集页面置换算法:找出一个不在工作集中的页面并淘汰它
      • 一个进程当前正在使用的页面的集合称为它的工作集。
      • 若每执行几条指令程序就发生一次缺页中断,那么就称这个程序发生了颠簸。
      • 分页系统设法跟踪进程的工作集,以确保在让进程运行前,它的工作集已在内存中了,该模型称为工作集模型。
    • 工作集时钟页面置换算法:环形链表,如果R为0,且时间不在工作集时间内,淘汰页面,否则下一位,如果R为1,置为0,向前移动一位
    • 总之,最好的两种算法是老化算法和工作集时钟算法,它们分别基于LRU和工作集。
  • 共享页面,写时复制
  • 共享库:
    • 当一个进程和共享库链接时,链接器没有加载被调用的函数,而是加载了一小段能够运行时绑定被调用函数的存根例程(stub routine)。
    • 当一个共享库被装载和使用时,整个库并不是一次性读入内存。而是根据需要,以页面为单位装载的,因此没有被调用的函数是不会被装载到内存的。
    • 使用相对偏移量的代码称作位置无关代码。
  • 内存映射文件:这种机制思想是,进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时才会被每次一页地读入,磁盘文件则被当做后备存储。当进程退出或显式地解除文件映射时,所有被改动的页面会被写回到磁盘文件中。
    • 内存映射文件提供一种IO的可选模式。可以把一个文件当做一个内存中的大字符数组来访问,而不是通过读写操作来访问这个文件。
    • 如果两个或两个以上的进程同时映射了同一个文件,它们就可以通过共享内存来通信。
  • 操作系统与分页实现:
    • 创建新进程:操作系统要确定程序和数据在初始化时有多大,并为它们创建一个页表。操作系统还要在内存中为页表分配空间并对其进行初始化。当进程被换出时,页表不需要驻留在内存中,当进程运行时,它必须在内存中。操作系统必须把有关页表和磁盘交换区的信息存储在进程表中。
    • 进程调度:当调度一个进程执行时,必须为新进程重置MMU,刷新TLB,以清除以前的进程遗留的痕迹。新进程的页表必须成为当前页表,通常可以通过复制该页表或者把一个指向它的指针放进某个硬件寄存器来完成。
    • 当缺页中断发生时:操作系统必须通过读硬件寄存器来确定是哪个虚拟地址造成了缺页中断。找到该页面,找到合适的页框存放该页面,必要时置换老的页面,把所需的页面读入页框。最后,还要回退程序计数器,是程序计数器指向引起缺页中断的指令,并重新执行该指令。
    • 进程退出:操作系统必须释放进程的页表、页面和页面在硬盘上所占用的空间。如果某些页面是与其他进程共享的,当最后一个使用它们的进程终止的时候,才可以释放内存和磁盘上的页面。
  • 缺页中断处理流程
    • 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
    • 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
    • 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
    • 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或者杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
    • 如果选择的页面“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
    • 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面正在被装入时,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
    • 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态
    • 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
    • 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
    • 该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过一样。
  • 分段和分页的实现本质上是不同的,页面是定长的而段不是。
  • 内存紧缩消除段产生的外部碎片。
  • x86处理器中虚拟内存的核心是两张表,即LDT(局部描述符表)和GDT(全局描述符表)。每个程序都有自己的LDT,但是同一台计算机上的所有程序共享一个GDT。LDT描述局部于每个程序的段,包括其代码、数据、堆栈等;GDT描述系统段,包括操作系统本身。
  • MULTICS地址组成:18位段号+16位段内地址【6位页号+10位页内偏移量】
  • 从用户向内核看,所使用的内存表象形式会依次经历“逻辑地址”——“线性地址”——“物理地址”几种形式(关于几种地址的解释在前面已经讲述了)。逻辑地址经段机制转化成线性地址;线性地址又经过页机制转化为物理地址。(但是我们要知道Linux系统虽然保留了段机制,但是将所有程序的段地址都定死为0-4G,所以虽然逻辑地址和线性地址是两种不同的地址空间,但在Linux中逻辑地址就等于线性地址,它们的值是一样的)。

第四章 文件系统

  • 文件是进程创建的信息逻辑单元。
  • 操作系统提供处理器的概念来建立进程的抽象,以及提取物理存储器的概念来建立进程(虚拟)地址空间的抽象。
  • 文件是一种抽象机制,它提供了一种在磁盘上保存信息而且方便读取的方法。这种方法可以使用户不必了解存储信息的方法、位置和实际磁盘工作方式等细节。
  • 文件类型
    • 普通文件
      • ASCII文件
      • 二进制文件:可执行二进制文件和Unix的归档文件
    • 目录
    • 字符特殊文件
    • 块特殊文件
  • 文件访问
    • 顺序访问
    • 随机访问
  • 每个进程都有自己的工作目录,这样在进程改变工作目录并退出后,其他进程不会受到影响,文件系统也不会有改变的痕迹。
  • 文件系统布局:文件系统存放在磁盘上。多数磁盘磁盘划分为一个或多个分区,每个分区中有一个独立的文件系统。磁盘0号扇区称为主引导记录MBR。在MBR的结尾是分区表,该表给出每个分区的起始和结束地址。
    • 在计算机被引导时,BIOS读入并执行MBR。MBR做的第一件事是确定活动分区,读入它的第一个块。称为引导块(boot block),并执行之。引导块中的程序将装载该分区中的操作系统。为统一起见,每个分区都从一个引导块开始,即使它不含有一个可启动的操作系统。
    • 通常,磁盘分区的布局随着文件系统的不同而变化的。文件系统经常包含有如下部分:
      • 超级块superblock:包含文件系统的所有关键参数:确定文件系统类型的魔数、文件系统的块数量以及其他重要的管理信息
      • 文件系统中空闲块的信息
      • i节点
      • 根目录:文件系统目录树的根部
      • 文件和目录
  • 文件分配:
    • 连续分配:把每个文件作为一连串连续数据块存储在磁盘上。
      • 实现简单:记录每个文件用到的磁盘只需两个数字即可:磁盘地址和文件的块数
      • 读操作性能很好,因为单个操作就可以从磁盘中读出整个文件
      • 碎片问题:随着时间的推移,磁盘会变得零碎,
    • 链表分配:构建磁盘块链表。充分利用每个磁盘块,不会因为磁盘碎片而浪费存储空间。另一方面,尽管读文件非常方便,但是随机访问却相当缓慢。要获得块n,操作系统每一次都必须从头开始,并且要先读取前面的n-1块。
    • 基于内存的链表分配:主要缺点是必须把整个表存放在内存中。
    • i节点:记录各个文件分别包括哪些磁盘块的方法是给每个文件赋予一个称为i节点的数据结构,其中列出了文件属性和文件块的磁盘地址。当文件大于第一层所能指向的文件块的大小,则建立二级磁盘块地址列表,第一层有指针指向它。Unix和Windows的NTFS采用此方法。
  • 目录系统的主要功能是把ASCII文件名映射成定位文件数据所需的信息。将查找结果放入高速缓存
  • 共享文件的两种方式:
    • 硬链接,多目录指向一个文件的inode
    • 符号链接:一个目录存储真实文件,其他目录建立LInk类型文件,存储共享文件地址
  • 日志结构文件系统(LFS):基本思想是将整个磁盘结构化为一个日志。i节点不再集中存放,而是分散在整个日志中。
    • 所有的写操作最初都被缓冲在内存中,然后周期性把所有已缓冲的写作为一个单独的段,在日志的末尾处写入磁盘。要打开一个文件,则首先需要从i节点图中找到文件的i节点,一旦i节点定位之后就可以找到相应的块的地址。所有的块都放在段中,在日志的某个位置上。
    • LFS有一个清理线程,该清理线程周期的扫描日志进行磁盘压缩。整个磁盘成为一个大的环形的缓冲区,写线程将新的段写到前面,而清理线程则将旧的段从后面移走。
  • 日志文件系统:保存一个用于记录系统下一步将要做什么的日志,这样当系统在完成它们即将完成的任务前崩溃时,重新启动后,可以通过查看日志,获取崩溃前计划完成的任务,并完成它们,这样的文件系统被称为日志文件系统。
    • unix删除文件的三个步骤:
      • 在目录中删除文件
      • 释放i节点到空闲i节点池
      • 将所有磁盘块归还空闲磁盘块池
    • 日志文件系统先写一个日志项,列出三个将要完成的动作。然后日志项被写入磁盘(并且为了良好地实施,可能从磁盘读回来验证它的完整性),只有当日志项已经被写入,不同的操作才可以进行。当所有的操作成功完成后,擦除日志项。如果系统这时崩溃,系统恢复后,文件系统可以通过检查日志来查看是不是有未完成的操作。如果有,可以重新运行所有未完成操作(这个过程在系统崩溃重复发生时执行多次),直到文件被正确地删除。
  • 虚拟文件系统VFS:绝大多数UNIX操作系统都使用虚拟文件系统概念尝试将多种文件系统统一成一个有序的结构。关键的思想就是抽象出所有文件系统都共有的部分,并且将这部分代码放在单独的一层,该层调用底层的实际文件系统来具体管理数据。
  • 磁盘空间管理:
    • 磁盘分割为一个个大小相等的块。
    • 记录空闲块:
      • 把空闲表存储在链表中【对于1KB大小的块和32位的磁盘块号,空闲表每个块包含255个空闲块的块号。】;
      • 位图。
  • 磁盘配额
  • 文件系统备份:
    • 增量转储:最简单的增量转储形式就是周期性的做全面的转储(备份),而每天只对从上一次全面转储起发生变化的数据做备份。
    • 磁盘转储:
      • 物理转储:从磁盘的第0块开始,将全部的磁盘块按序输出到磁带上,直到最后一块复制完毕。物理转储的主要优点是简单、极为快速(基本上是以磁盘的速度运行)。主要缺点是,既不能跳过选定的目录,也无法增量存储,还不能满足恢复个人文件的请求。正因为如此,绝大多数配置都使用逻辑转储。
      • 逻辑转储从一个或几个指定目录开始,递归地转储其自给定基准日期(例如,最近一次增量转储或全面系统转储的日期)后有所更改的全部文件和目录。所以,在逻辑转储中,转储磁带上会有一连串精心标识的目录和文件,这样就更容易满足恢复特定文件或目录的请求。
  • 文件系统的一致性:UNIX fsck
    • 磁盘块计数:检查分配和空闲的块,块要么是空闲,要么是分配,二者计数,要么01,要么10,否则错误
    • 目录检查:检查文件计数,从根目录扫描,得到一个inode节点索引的计数,和inode上存储的文件计数对比。
  • 文件系统性能:
    • 高速缓存:最常用的减少磁盘访问次数技术是块高速缓存(block cache)或者缓冲区高速缓存(buffer cache)。高速缓存指的是一系列的块,它们在逻辑上属于磁盘,但实际上基于性能的考虑被保存在内存中。
    • 块提前读:在需要用到块之前,试图提前将其写入高速缓存,从而提高命中率。块提前读策略只适用于顺序读取的文件。对于随机访问文件,块提前读丝毫不起作用。
    • 减少磁盘臂运动:把有可能顺序访问的块放在一起,当然最好在同一个柱面上,从而减少磁盘臂的移动次数。块簇技术:不按单个块,而是用连续块簇来跟踪磁盘存储器。

第五章 输入输出

  • IO设备:块设备和字符设备

    • 块设备:把信息存储在固定大小的块中,每个块有自己的地址。通常块的大小在512字节至65536字节之间。所有传输以一个或多个完整的连续的块为单位。块设备的基本特征是每个块都能独立于其他块而读写。硬盘、蓝光光盘和USB盘是最常用的块设备。
    • 字符设备:以字符为单位发送和接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。打印机、网络接口、鼠标以及大多数与硬盘不同的设备可看做字符设备。
  • I/O设备一般由机械部件和电子部件两部分组成。电子部件称作设备控制器或适配器。

    • 设备控制器:把串行的位流转换为字节块,并进行必要的错误校正工作。字节块通常首先在控制器内部的一个缓冲区中按位进行组装,然后再对校验和进行校验并证明字节块没有错误后,再将它复制到主存中。
  • 每个设备控制器有几个寄存器用来与CPU进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接收数据、开启或关闭,或者执行某些其他操作。通过读取这些寄存器,操作系统可以了解设备的状态,是否准备好接收一个新的命令。除了控制寄存器之外,许多设备还有一个操作系统可以读写的数据缓冲区

  • I/O端口空间:每个控制寄存器被分配一个I/O端口(I/O port)号,所有的I/O端口号形成I/O端口空间(I/O port space)。大多数早期计算机采用这种方式工作。

    • IN REG,PORT CPU可以读取控制寄存器PORT的内容并将结果存入到CPU寄存器REG中。
    • OUT PORT,REG CPU可以将REG的内容写入到控制寄存器中。
  • 内存映射I/O:每个控制寄存器被分配唯一的内存地址,并且不会有内存被分配这一地址。

  • 当CPU想要读入一个字的时候,不论是从内存中读入还是从I/O端口中读入,它都要将需要的地址放到总线的地址线上,然后在总线的一条控制线上置起一个READ信号。还要在第二条信号线上表明需要的是I/O空间还是内存空间。如果是内存空间,内存将响应请求。如果是I/O空间,I/O设备将响应请求。

  • DMA 直接存储器存储

    • 没有DMA的磁盘读写
      • 首先,控制器从磁盘驱动器串行地、一位一位地读一个块(一个或多个扇区),直到将整块信息放入控制器的内部缓冲区中。接着,它计算校验和,以保证没有读错误发生。然后控制器产生一个中断。当操作系统开始运行时,他重复地从控制器的缓冲区中一次一个字节或一个字地读取该块的信息,并将其存入内存中。
    • 使用DMA的磁盘读写:只有硬件具有DMA控制器时操作系统才能使用DMA。
      • CPU通过设置DMA控制器的寄存器对它进项编程,所以DMA控制器知道将什么数据传送到什么地方。
      • DMA控制器通过在总线上发出一个读请求到磁盘控制器而发起DMA传送。
      • 写内存。一般情况下,要写的内存地址在总线的地址线上,所以当磁盘控制器从其内部缓冲区中读取下一个字的时候,它知道将该字写到什么地方。
      • 写操作完成时,磁盘控制器在总线上发出一个应答信号到DMA控制器。于是DMA控制器步增要使用的内存地址,并且步减字节计数。直到字节计数为0。
      • 此时,DMA控制器将中断CPU以便让CPU知道传送现在已经完成了。当操作系统开始工作时,用不着将磁盘块复制到内存中,因为它已经存在在内存中了
      • 现代操作系统 Andrew S. Tanenbaum,Herbert Bos_第3张图片
    • 许多总线能够以两种方式操作:每次一字模式和块模式。
    • 周期窃取(cycle stealing):DMA控制器请求传送一个字并且得到这个字。如果CPU也想使用总线,它必须等待,因为设备控制器偶尔偷偷溜入并且从CPU偷走一个临时的总线周期,从而轻微的延迟了CPU。
    • 突发模式(burst mode):在块模式中,DMA控制器通知设备获得总线,发起一连串的传送,然后释放总线。它比周期窃取效率更高,因为获得总线占用了时间,并且以一次总线获得的代价能够传送多个字。突发模式的缺点是,如果正在进行的是长时间突发传送,有可能将CPU和其他设备阻塞相当长的周期。
    • 在DMA开始运行之前,磁盘首先要将数据读入其内部的缓冲区中。原因如下
      • 通过进行内部缓冲,磁盘控制器可以在开始传送之前检验校验和。如果校验和错误,那么将发出一个表明错误的信号并且不会进行传送
      • 一旦磁盘传送开始工作,从磁盘读出的数据就是以固定速率到达的,而不论控制器是否准备好接收数据。如果没有内部缓冲,控制器需要处理总线忙等导致传送工作。如果块被放入内部缓冲区,则在DMA启动前不需要使用总线,这样控制器的设计就可以简化,因为DMA到内存的传送没有严格的时间要求。
  • 中断:在硬件层面,当一个I/O设备完成交给它的工作时,它就产生一个中断(假设操作系统已经开放中断),它是通过在分配给它的一条总线信号线上置起信号而产生中断的。该信号被主板上的中断控制器芯片检测到,由中断控制器芯片决定做什么。

    • 为了处理中断,中断控制器在地址线上放置一个数字表明那个设备需要关注,并且置起一个中断CPU的信号。
    • 中断信号导致CPU停止当前正在做的工作并且开始做其他的事情。地址线上的数字被用作指向一个称为中断向量(interrupt vector)的表格的索引,以便读取一个新的程序计数器。这一程序计数器指向相应的中断服务过程的开始。
    • 中断服务过程开始运行后,它立刻通过将一个确定的值写到中断控制器的某个I/O端口来对中断做出应答。这一应答告诉中断控制器可以自由的发出另一个中断。通过让CPU延迟这一应答直到它准备好处理下一个中断,就可以避免与多个几乎同时发生的中断相牵涉的竞争状态。
    • 现代的CPU大量采用流水线并且有时还采用超标量(内部并行)。在老式的系统中,每条指令完成执行之后,微程序或硬件将检查是否存在悬而未决的中断。如果存在,那么程序计数器和PSW将被压入堆栈中而中断序列将开始。在中断处理程序之后,相反的过程发生。
      • 精确中断:将机器留在一个明确状态的中断。
        • PC寄存器保存在一个已知的地方
        • PC所指向的指令之前的指令已经完成执行
        • PC所指向的指令之后的指令还没有执行
        • PC所指向的指令状态已知
      • 不精确中断:不满足上述要求的中断
  • I/O软件目标

    • 设备独立性:可以访问任意I/O设备而无需事先指定。接口编程
    • 统一命名:一个文件或设备的名字应该是一个简单的字符串或一个整数,它不应依赖于设备。
    • 错误处理:错误应该尽可能地在接近硬件的层面得到处理。
    • 同步(阻塞)和异步(中断驱动)传输:大多数物理I/O是异步的——CPU启动传输后便去做其他工作,直到中断发生。如果I/O是阻塞的,那么用户程序就更加容易编写——在read系统调用之后,程序将自动被挂起,直到缓冲区中的数据准备好。正是操作系统使实际上是中断驱动的操作变为在用户看来是阻塞式的操作。
    • 缓冲(buffering):数据离开一个设备之后通常并不能直接存放到其最终的目的地。例如,在网络上进来一个数据报时,直到将该数据包存放在某个地方并对其进行检查,操作系统才知道要将其置于何处。
    • 共享设备和独占设备:独占设备的引入带来了各种各样的问题,如死锁。
  • IO方式:

    • 程序控制IO:让CPU做全部工作。直到全部IO完成之前需要占用CPU的全部时间。
      • 首先数据被复制到内核空间,然后操作系统进入一个密闭的循环,一次输出一个字符,输出一个字符后,CPU不断查询设备【一般设备除了数据寄存器之外还有状态寄存器】以了解它是否准备接收另一个字符(叫轮询或忙等待)。
      • 实现简单但是也有缺点,即直到全部I/O完成之前要占用CPU的全部时间。如果“打印”一个字符的时间非常短(因为打印机所做的全部事情就是将新的字符复制到一个内部缓冲区),那么忙等待还是不错的。此外,在嵌入式系统中,CPU没有其他事情可做,忙等待也是合理的。然而,在更加复杂的系统中,CPU有其他工作要做,忙等待是低效的。
    • 中断驱动IO:允许CPU在等待设备变为就绪的同时做某些其他事情的方式就是使用中断。当设备准备好接收下一个字符时,发出一个中断。这一中断将停止当前进程并且保存其状态。然后打印机中断服务过程将运行。
      • 中断驱动I/O的明显缺点是中断发生在每个字符上。中断要花费时间,所以这一方法将浪费一定量的CPU时间。
    • 使用DMA的IO:中断驱动IO的明显缺点是中断发生在每个字符上。DMA的IO思路是让DMA控制器一次给设备提供一个字符,而不必打扰CPU。本质上,DMA是程序控制IO,只是由DMA控制器而不是主CPU做全部工作。
  • 现代操作系统 Andrew S. Tanenbaum,Herbert Bos_第4张图片

  • 中断处理程序:隐藏中断最好的办法就是将I/O操作的驱动程序阻塞起来,直到I/O操作完成并且产生一个中断。当中断发生时,中断处理程序将做它必须要做的全部工作以便对中断进行处理。然后,它可以将启动中断的驱动程序解除阻塞。I/O硬件中断完成之后,软件中断步骤(根据系统不同,可能有些差别):

    • 保存没有被中断硬件保存的所有寄存器(包括PSW)。
    • 为中断服务过程设置上下文,可能包括设置TLB、MMU和页表
    • 为中断服务过程设置堆栈
    • 应答中断控制器,如果不存在集中的中断控制器,则再次开放中断
    • 将寄存器从它们被保存的地方(可能是某个堆栈)复制到进程表中。
    • 运行中断服务过程,从发生中断的设备控制器的寄存器中提取信息。
    • 选择下一次运行哪个进程,如果中断导致某个被阻塞的高优先级进程变为就绪,则可能选择它现在就运行。
    • 为下一次要运行的进程设置MMU上下文,也许还需要设置某个TLB
    • 装入新进程的寄存器,包括其PSW。
    • 开始运行新进程
  • 设备驱动程序:每个连接到计算机上的I/O设备都需要某些设备特定的代码来对其进行控制。

    • 大多数操作系统都定义了一个所有块设备必须支持的标准接口,并且还定义了另一个所有字符设备都必须支持的标准接口。
    • 功能:接收来自其上方与设备无关的软件所发出的抽象的读写请求,并且目睹这些请求被执行。格式化设备,电源需求和日志事件管理
    • 驱动程序必须是可重入的,这意味着一个正在运行的驱动程序必须预料到第一次调用完成之前第二次被调用。
  • 与设备无关的I/O软件:执行对所有设备公共的I/O功能,并且向用户层软件提供统一的接口。

    • 设备驱动程序的统一接口:
      • 操作系统定义一组驱动程序必须支持的函数
      • 设备命名:在UNIX系统中,像/dev/disk0这样的设备名唯一确定了一个特殊文件的i节点,这个i节点包含了主设备号(major device number),主设备号用于定位相应的驱动程序。i节点还包含次设备号(minor device number),次设备号作为参数传递给驱动程序,用来确定要读或写的具体单元。所有设备都具有主设备号和次设备号,并且驱动程序都是通过主设备号来选择驱动程序而得到访问。
    • 缓冲:以接收网络包为例
      • 无缓冲输入:对于每到来的字符,大批必须启动用户进程。对于短暂的数据流量让一个进程运行许多次效率会很低,所以这不是一个良好的设计
      • 用户空间中的缓冲:当一个字符到来时,如果缓冲区被分页调出了内存会出现问题。解决办法是将缓冲区锁定在内存中,但是如果许多进程都在内存中锁定页面,那么可用页面就会收缩并且系统性能将下降。
      • 内核空间的缓冲区接着复制到用户空间的缓冲:在内核空间中创建一个缓冲区并且让中断处理程序将字符放到这个缓冲区。当该缓冲区被填满的时候,将包含用户缓冲区的页面调入内存(如果需要的话),并且在一次操作中将内核缓冲区的内容复制到用户缓冲区中。
      • 内核空间的双缓冲:上一种缓冲存在的问题,当用户缓冲区的页面从磁盘调入内存时,有新的字符到来,此时内核缓冲区已满,没有地方存放这些新来的字符。可以采用两个内核缓冲区,当地一个缓冲区填满之后,在被清空之前,使用第二个缓冲区,第一个缓冲区复制,当第二个缓冲区满的时候,复制到用户空间的时候,使用第一个缓冲区接收新字符。
      • 缓冲存在多次复制:用户空间复制到内核空间,再复制到网络控制器,才能发出一个数据报。接收也是一样
    • 错误报告:许多错误是设备特定的并且必须由适当的驱动程序来处理,但是错误处理的框架是设备无关的
    • 分配与释放专用设备:使用专有的机制,试图得到不可用的设备可以将调用者阻塞,而不是让其失败,阻塞的进程被放入一个队列。迟早被请求的设备会变得可用,这时就可以让队列中的第一个进程得到该设备并且继续执行
    • 与设备无关的块大小:不同的磁盘可能具有不同的扇区大小,应该由与设备无关软件来隐藏这一事实并向高层提供一个块大小。
  • 用户空间的I/O软件

  • IO系统层次: 例如,当一个用户程序试图从一个文件中读一个块时,操作系统被调用以实现这一请求。与设备无关的软件在缓冲区高速缓存中查找有无要读的块。如果需要的块不在其中,则调用设备驱动程序,向硬件发出一个请求,让它从磁盘中获取该块。然后,进程被阻塞直到磁盘操作完成并且数据在调用者的缓冲区中安全可用。当磁盘操作完成时,硬件产生一个中断。中断处理程序就会运行,它要查明发生了什么事情,也就是说此刻需要关注那个设备。然后,中断处理程序从设备提取状态信息,唤醒休眠的进程以结束此次IO请求,并且让用户进程继续运行。

  • 磁盘块读取时间:

    • 寻道时间:将磁盘臂移动到适当的柱面上所需的时间
    • 旋转时间:等待适当扇区旋转到磁头下所需的时间
    • 实际数据传输时间
    • 算法
      • 最短寻道算法
      • 电梯算法
  • 对于现代硬盘,寻道和旋转延迟是如此影响性能,所以一次只读取一个或两个扇区的效率是非常低下的。

  • 磁盘控制器的高速缓存完全独立于操作系统的高速缓存。控制器的高速缓存通常保存还没有实际被请求的块,但是这对于读操作是很便利的,因为它们只是作为某些其他读操作的附带效应而恰巧要在磁头下通过。与之相反,操作系统所维护的任何高速缓存由显示地读出的块组成,并且操作系统认为它们在较近的将来可能再次需要。

  • 时钟:又称为定时器。时钟负责维护时间,并且防止一个进程垄断CPU。

  • 时钟硬件所做的全部工作是根据已知的时间间隔产生中断。

  • 时钟软件:

    • 维护日时间
    • 防止进程超时运行
    • 对CPU使用情况记账
    • 处理用户进程提出的alarm系统调用
    • 为系统本身的各个部分提供监视定时器。
    • 完成概要剖析、监视和统计信息收集。
  • 软定时器(soft timer)避免了中断。无论何时当内核因某种其他原因在运行时,在它返回到用户态之前,它都要检查实时时钟以了解软定时器是否到期。如果这个定时器到期,则执行被调度的事件(例如,传送数据包或者检查到来的数据包),而无需切换到内核态,因为系统已经在内核态。在完成工作之后,软定时器被复位以便再次闹响

  • 大多数Unix系统使用X窗口系统作为用户界面的基础。它包含与特殊的库相绑定并发出绘图命令的程序以及在显示器上执行绘图的服务器。

第六章 死锁

  • 资源:在进程对设备、文件等取得了排他性访问权时,有可能会出现死锁。为了尽可能使关于死锁的讨论通用,我们把这类需要排他性使用的对象称为资源。简单来说,资源就是随着时间推移,必须能获得、使用以及释放的任何东西。
  • 资源分类两种:
    • 可抢占资源:可以从拥有它的进程中抢占而不会产生任何副作用,如存储器。
    • 不可抢占资源:在不引起相关的计算失败的情况下,无法把它从占有它的进程中抢占过来。
  • 总的来说。死锁与不可抢占资源有关,有关可抢占资源的潜在死锁通常可以通过在进程之间重新分配资源而化解。
  • 死锁发生的四个必要条件:
    • 互斥条件:每个资源要么已经分配给了一个进程,要么就是可用的。
    • 占有和等待条件:已经得到了某个资源的进程可以再请求新的资源。
    • 不可抢占条件:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
    • 环路等待条件:死锁发生时,系统中一定有由两个或两个以上的进程组成的一条环路,该环路中的每个进程都在等待着下一个进程所占有的资源。
    • 死锁发生时,以上四个条件一定是同时满足的。如果其中任何一个条件不成立,死锁就不会发生。
  • 处理死锁的策略:
    • 忽略该问题。也许如果你忽略它,它也会忽略你。
    • 检测死锁并恢复。让死锁发生,检测它们是否发生,一旦发生死锁,采取行动解决问题。
    • 仔细对资源进行分配,动态地避免死锁。
    • 通过破坏引起死锁的四个必要条件之一,防止死锁的产生。
  • 死锁恢复:
    • 利用抢占恢复
    • 利用回滚恢复
    • 通过杀死进程恢复
  • 死锁预防方法汇总:
    • 互斥:一切都使用假脱机技术
    • 占有和等待:在开始就请求全部资源
    • 不可抢占:抢占资源
    • 环路等待:对资源按序编号
  • 通过跟踪那一个状态是安全状态,哪一个状态是不安全状态,可以避免资源死锁。安全状态就是这样一个状态:存在一个事件序列,保证所有的进程都能完成。不安全状态就没有这种保证。银行家算法可以通过拒绝可能引起不安全状态的请求来避免死锁。
  • 两阶段加锁:在第一阶段,进程试图对所有所需的记录进行加锁,一次锁一个记录。如果第一阶段加锁成功,就开始第二阶段,完成更新然后释放锁。如果第一阶段的某个进程需要的记录已经被加锁,那么该进程释放它所有加锁的记录,然后重新开始第一阶段。
  • 通信死锁:一个普遍的情形是进程A向进程B发送请求消息,然后阻塞直至B回复。假设请求消息丢失,A将阻塞以等待回复,而B会阻塞等待一个向其发送命令的请求,因此发生死锁。
    • 一种技术可以中断通信死锁:超时。发送端在发送请求的同时开启一个计时器,超过时间则断开。
  • 活锁:在某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会尝试礼貌地释放已经获得的锁,然后等待1ms,再尝试一次。但是如果另一个进程也做了相同的事,就会由于相同的步调而无法前进,就造成了活锁。在这个过程中,没有进程阻塞,甚至可以说进程正在活动,所以不是死锁。
    • 避免的活锁办法有,可以把等待时间设置为一个随机值,而不是固定值。
  • 饥饿:假设打印机的分配,总是把打印机分配给最小文件的进程,那么有一个很大的文件就会一直分配不到打印机,这就构成了饥饿。
    • 饥饿可以通过先来先服务资源分配策略来避免。

第七章 虚拟化和云

  • 第一类虚拟机管理程序运行在裸机上。像一个操作系统,因为它是唯一一个运行在最高特权级的程序。
  • 第二类虚拟机管理程序依赖于宿主操作系统的系统服务。像宿主操作系统的进程。
  • 每个包含内核态和用户态的CPU都有一个特殊的指令集合,其中指令在内核态和用户态的行为不同:
    • 敏感指令:包括进行I/O操作和修改MMU设置的指令
    • 特权指令:指令在用户态执行会导致陷入。
    • 机器可虚拟化的一个必要条件是敏感指令为特权指令的子集。简单来说,用户态想做不应该在用户态做的事情,硬件必须陷入。
  • Intel CPU的VT技术,基本思想是创建可以运行虚拟机的容器。客户操作系统在容器中启动并持续运行,直到触发异常并陷入虚拟机管理程序。例如试图执行I/O指令时,会造成陷入的指令集合由虚拟机管理程序设置的硬件位图控制。
  • 全虚拟化:呈现出一个与底层一模一样的虚拟机
  • 半虚拟化:提供一个类似物理机器的软件接口,显示暴露出自身是一个虚拟化环境。例如,它提供一组虚拟调用,允许客户机向虚拟机管理系统发送显示的请求,就像系统调用为应用程序服务一样。
  • 系统虚拟化与进程虚拟化。系统虚拟化试图使客户机认为它拥有整个系统。
  • 虚拟机管理程序:VMM在同一物理硬件上创建出有多台虚拟机器的假象。
    • 安全性:虚拟机管理程序应完全掌握虚拟资源
    • 保真性:程序在虚拟机上执行的行为应该与裸机相同。
    • 高效性:虚拟机中运行的大部分代码应不受虚拟机管理程序干涉。
  • 在不支持虚拟化的平台实现虚拟化:二进制翻译。
    • 第一类虚拟机管理系统:CPU 0级:虚拟机管理程序,1级:客户操作系统,3级:用户进程
    • 第二类虚拟机管理系统:有一个在第0级运行的内核模块,能够使用特权指令操作硬件;0级:宿主机操作系统,3级:客户操作系统。
    • 虚拟机管理程序确保客户操作系统中的敏感指令不再执行,具体做法是代码改写,一次改写一个基本代码块。
    • 基本块:以转移指令结尾的一小段顺序指令序列,除最后一条指令外,内部不包含跳转、调用、陷入、返回或其他改变控制流的指令。
    • 在执行一个基本块之前,虚拟机管理程序扫描该基本块以寻找敏感指令,如果存在,就替换成调用虚拟管理程序中处理例程的指令。
    • 翻译的基本块会缓存起来,而且大多数基本块并不包含敏感指令或特权指令,所以性能损耗很小。
  • 内存虚拟化
    • 操作系统通过设置CPU的一个指向顶级页表的控制寄存器改变映射
    • 影子页表:对每台虚拟机,虚拟机管理程序都需要创建一个影子页表,将虚拟机使用的虚拟页映射到它分配给虚拟机的实际物理页上。
      • 糟糕的是,每次客户操作系统修改页表,虚拟机管理程序都需要修改影子页表,问题是客户操作系统只要修改内存就能修改页表,但是修改内存并不涉及敏感命令,所以虚拟机无法察觉。
      • 一种解决方案是,虚拟机管理程序跟踪客户机虚拟内存中保存顶级页表的页面。当客户机操作系统首次尝试载入指向顶级页表的硬件寄存器时,因为需要使用敏感指令,所以虚拟机管理程序能获得顶级页表所在的页面信息。虚拟机管理程序可在此时创建影子页表,并将顶级页表及其指向的下级页表设置为只读。这样当客户操作系统试图修改页表就会导致缺页异常,并将控制流交给虚拟机管理程序分析。
      • 另一种解决办法,虚拟机管理程序允许客户机向页表添加任何新映射,而影子页表不做任何改动,但是只要客户机试图访问新页面,就会产生缺页异常,将控制流还给虚拟机操作系统。这时虚拟机管理程序就可以探测客户机页表,看看影子页表是否需要添加新的映射,如果需要就添加后重新执行触发缺页异常的指令。从页表删除映射后需要执行INVLPG指令(意图是使TLB)失效,虚拟机管理程序可以截获次敏感指令并删除影子页表中的对应项。
      • 上述两种办法都会带来大量的缺页异常。缺页异常的开销很大,在虚拟化的环境中尤为突出,因为缺页异常会导致虚拟机退出(VM exit),虚拟机管理程序重新获得控制流。
      • 在半虚拟化中,客户机的半虚拟化操作系统知道,完成修改页表操作后要通知虚拟机管理程序。这样就不需要每次页面表改动都触发缺页异常,只需要全部修改完进行一次虚拟化调用即可。
    • 嵌套页表的硬件支持:目的是在无需陷入的情况下由硬件处理虚拟化引发的额外页表操作,以降低开销。
    • 去重技术:虚拟机共享一些相同内容的页面(如Linux内核)
  • I/O虚拟化
    • I/O MMU:消除了虚拟化中DMA绝对地址的问题。
      • 可以将设备地址和虚拟机地址映射为相同的空间,并且这一映射对设备和虚拟机来说都是透明的
      • 设备隔离:保证设备可以直接访问其分配到虚拟机的内存空间而不影响其他虚拟机的完整性
      • 中断重映射:使设备产生的中断以正确的中断号抵达正确的虚拟机。
    • 设备域:专门指定一个虚拟机运行普通操作系统,将其他虚拟机的所有I/O调用映射过来。
    • 单根I/O虚拟化:允许驱动程序与设备间绕过虚拟机管理程序进行通信
  • 云服务:
    • 按需自助服务:无需人为操作就可以为用户提供资源
    • 普适的网络访问。所有资源都可以通过网络标准化的机制访问,以支持各种异构设备
    • 资源池。云服务商拥有的资源可以服务多个用户并动态再分配,用户通常不知道他们使用的资源的具体位置
    • 快速可伸缩。能根据用户需求弹性甚至是自动地获取和释放资源
    • 服务可计量。云提供商按服务类型计量用户使用的资源。
  • 虚拟机迁移:暂停迁移
    • 热迁移:虚拟机迁移时仍能运行。
  • VMware
    • 第二类虚拟机管理程序:
      • VMM:负责执行虚拟机指令。包含直接执行子系统和二进制翻译子系统
      • VMX:负责与宿主操作系统交互
    • 第一类虚拟机管理程序ESX Server

第八章 多处理机系统

多处理机

  • 共享存储多处理机(多处理机):两个或多个CPU全部共享访问一个公共的RAM。
  • 所有多处理机都具有每个CPU可访问全部存储器的性质。对于读出每个存储器字的速度一样快的称为UMA(统一存储器访问)多处理机,NUMA多处理机无这种特性。
  • 基于总线的UMA多处理机体系结构:基于单总线。两个或更多的CPU以及一个或多个存储器模块都使用同一总线进行通信。
    • 检查总线忙否,如果总线忙,则CPU只是等待,直到空闲
    • 解决总线忙问题是为每个CPU添加一个高速缓存(cache)。由于许多读操作可以从高速缓存上得到满足,总线流量就大大减少了,
    • 每一个高速缓存块被标记为读(在这种情况下,它可以同时存在于多个高速缓存中),或者标记为读写(在这种情况下,它不能在其他高速缓存中存在)。遵循高速缓存一致性协议。
      • 如果CPU试图写入一个字,总线硬件检测到写,并把一个信号放到总线上通知其他的高速缓存。如果其他高速缓存有"干净"的缓存,即同存储器一样的副本,那么它们丢弃该副本并让写者在修改之前从存储器取出高速缓存块。
      • 如果其他缓存器有"脏"副本,它必须在处理器写之前把数据写到存储器或者把它通过总线直接传送到写者上。
    • 最好把CPU限制为16或者32个以内
  • 基于交叉开关的UMA多处理机:
    • 处理机和存储器形成网格,交叉点是一个电子开关。
    • 交叉开关最好的特性是它是一个非阻塞网络,即不会因为有些交叉点或连线已经被占据而拒绝连接。
    • 最差的特性是交叉点的数量以n*n方式增长。
    • 适合于中等规模的系统。
  • 使用多级交换网络的UMA多处理机
  • NUMA多处理机:单总线UMA多处理机通常不超过几十个CPU,而开关或交换网络多处理机需要许多(昂贵)的硬件,所以模块也不是很大。
    • 和UMA一样,都为所有CPU提供统一的地址空间.
    • 关键特性
      • 具有对所有CPU都可见的单个地址空间
      • 通过LOAD和STORE指令访问远程存储器
      • 访问远程存储器慢与访问本地存储器
    • 在对远程存储器的访问不被隐藏时(因为没有高速缓存)称为NC-NUMA(NoCache NUMA)。在有一致性高速缓存时,称为CC-NUMA(Cache-Coherent NUMA)
    • 目前构造大型CC-NUMA最常见的办法是基于目录的多处理机。基本思想是,维护一个数据库来记录高速缓存行的位置及其状态。当一个高速缓存行被引用时,就查询数据库找出高速缓存行的位置以及它是"干净"还是"脏"的。由于每条访问存储器的指令都必须查询这个数据库,所以它必须配有极高速的专用硬件,从而可以在一个总线周期的几分之一内做出响应。
  • 多核芯片:也被称为片级多处理机(Chip-level MultiProcessors,CMP)
  • 众核芯片:包含几十、成百甚至更多个核心的多喝处理器。
    • 超大核量的问题,保存缓存一致性的机制变得非常复杂和昂贵。更糟糕的是,保持缓存目录的一致性还会消耗大量的内存,这就是著名的一致性壁垒。
    • 唯一已证明可适用于众核的编程模型是采用消息传递和分布式内存实现的。图形处理单元(GPU)作为当今最为常见的众核,与通用处理器相比,GPU在运算单元上预留了更多的晶体管,而在缓存和控制逻辑上则更小。
  • 异构多核:一片芯片上封装不同类型的处理器。通用处理器核GPU;
    • IXP网络处理器。典型的网络处理器包含一个通用控制核(运行Linux或Windows)和几十几百个高度专门化的流处理器,这些流处理器十分擅长处理网络数据包。

多处理机操作系统

  • 每个CPU都有自己的操作系统:静态将存储器分为和CPU一样多的各个部分,为每个CPU提供私有存储器以及操作系统的各自私有副本。
  • 主从多处理机:操作系统的一个副本机器数据表在CPU1上,而不是在所有其他CPU上。为了在该CPU1上进行处理,所有系统调用都重定向到CPU1上。如果有空闲的CPU时间,还可以在CPU1上运行用户进程
    • 如果有许多CPU,主CPU会是瓶颈
  • 对称多处理机:在存储器中有操作系统的一个副本,但任何CPU都可以运行它。
    • 数据同步问题。加统一大内核锁,但是性能不好。操作系统中很多部分是独立的,这样就可以把操作系统划分为不同的临界区,由不同的CPU并行执行不同的临界区而互不干扰。
  • 多处理机同步
    • TSL:锁住总线。但是使用了自旋锁,因为请求锁的CPU总是在原地尽可能地对锁进行循环测试。
      • 引用常用的高速缓存解决不了问题,因为TSL是一个写操作,会导致高速缓存行不断失效,造成更大的总线流量。解决办法是先读,发现锁空闲再用TSL指令,这样就不会大量的缓存行失效了。
    • 自旋与切换:当CPU访问加锁的互斥信号量时,只能等待。访问共享的进程就绪链表选择进程时,如果被锁,也只能等待。但是如访问文件系统高速缓冲区高速缓存,而该文件系统缓冲区高速缓存正好锁住了,那么CPU可以决定切换到另一个进程而不是等待。
      • 当等待的时间小于进程切换的时间,可以采用自旋来等待。
      • 操作系统也可以设置一个阈值,当大于阈值时,切换,小于则等待。也可以跟踪历史信息决定
      • X86的MONITOR/NWAIT指令允许程序阻塞,直到某个其他处理器修改先前定义的存储器区域的数据。MWAIT指令会阻塞线程直到有人写入该区域。阻塞时,线程会进行自旋,但不会浪费太多时钟周期。
  • 多处理机调度:
    • 分时
      • 亲和调度:尽量使一个线程在它前一次运行过的同一个CPU上运行。
    • 空间共享:在CPU上同时调度多个线程。一次调度,多个CPU并行执行,如某线程遇到I/O阻塞,则该线程等待。
    • 群调度:
      • 把一组相关线程作为一个单位,即一个群(gang),一起调度
      • 一个群中的所有成员在不同的分时CPU上同时运行
      • 群中的所有成员共同开始和结束其时间片

多计算机

  • 一个多计算机系统的基本节点包括一个CPU、存储器、一个网络接口,有时还有一个硬盘。
  • 云计算服务都建立在多计算机上,因为它们需要大的计算能力。
  • 互联技术:采用网络相连
    • 存储转发包交换:当包通过一个路由器时,首先会被存储,直到一个完整包被接收,再转发
    • 电路交换:建立直连路径,比特流就从源到目的地尽快的传输
  • 网络接口:接口板需要一些用来存储进出包的RAM。通常,这些包被送到第一个交换机之前,这个要送出的包必须被复制到接口板的RAM中。这样设计的原因是许多互联网络是同步的,所以一旦一个包开始传送,比特流必须以恒定的速率连续进行。如果包在主RAM中,由于内存总线上有其他的信息流,所以这个送到网络上的连线流是不能保证的。对于收包也有同样的问题。
    • 接口板可以由一个或多个DMA通道,甚至板上有一个或多个完整的CPU,它们被称为网络处理器。通过请求在系统总线上的块传送(block transfer),DMA通道可以在接口板和主RAM之间以非常高的速率复制包,因而可以一次性传送若干字节而不需要为每个字分别请求总线。
  • 在多计算机系统中高性能通信的敌人是对包的过度复制。
    • 把接口板映射到用户空间,减少用户空间到内核空间的复制,但必须采用预防机制防止竞争,例如不同的用户进程映射到接口板RAM的不同部分。一个接口板映射到内核空间供操作系统使用。
    • 较新的网络接口通常是多队列的,这意味着它们有多个缓冲区可以有效地支持多用户。
    • 网卡支持核的亲和性,它有自己的散列逻辑来将每个数据包引导到一个合适的进程。由于将同一个TCP流中的所有段交给一个处理器处理速度更快(因为缓存中总是有数据),因此可以使用散列逻辑(按照IP地址和TCP端口号)保证它被特定的处理器处理。
    • 如何将包发送至接口板上
      • DMA芯片直接将它们从RAM复制到板上。为防止页面交换,需要钉住页面,但是钉住和释放页面增加了软件的复杂度,这样做的代价也不小,如果包很小,则不能忍受钉住和释放开销,大包也许会容忍此开销。
    • 远程直接内存访问:允许一台机器直接访问另一台机器的内存。
  • 用户层通信
    • 阻塞调用:当一个进程调用send时,它指定一个目标以及用以发送消息到该目标的一个缓冲区。当消息发送完成之前,发送进程被挂起(阻塞)。消息无需可以不用复制到内核空间
    • 非阻塞调用,在消息发出之前,它立即将控制返回给调用者。
    • 带有复制操作的非阻塞发送,消息复制到内核缓冲区,此阶段阻塞,复制完成进程解除阻塞。缺点是CPU浪费在额外的复制上
    • 带有中断操作的非阻塞发送,当消息发送之后中断发送者,告知缓冲区又可以使用了,但是中断造成编程困难
    • 写时复制:让缓冲区写时复制,也就是说,在消息发送之前将其标记为只读。在消息发出去之前,如果缓冲区被重用,则进行复制。
    • 对于receive的非阻塞调用,可以使用中断,但是响应缓慢,也可以使用poll调用
  • 远程过程调用RPC:基本思想是尽可能使远程过程调用像本地调用
    • 客户程序绑定在一个客户端存根(client stub)的过程上,它在客户机地址空间中代表服务器过程。
    • 服务器程序也绑定一个服务器存根(server stub)的过程上,客户机到服务器的过程调用并不是本地调用
    • 操作步骤如下:
      • 客户机调用客户端存根,该调用是一个本地调用,其参数以通常方式压入栈内
      • 客户端存根将有关参数打包成一条消息,并进行系统调用来发出该消息,这个参数打包的过程称为编排(marshaling)
      • 内核将该消息从客户机发送给服务器。
      • 内核将接收进来的消息发送给服务器存根(通常服务器存根已经提前调用了receive)。
      • 最后服务器存根调用服务器过程,
      • 应答则是在相反的方向上沿着同一步骤进行
    • 问题:
      • 指针参数使用,
  • 现代操作系统 Andrew S. Tanenbaum,Herbert Bos_第5张图片
  • 分布式系统想要做的是,将松散连接的大量机器转化为基于一种概念的一致系统。
  • 基于文档:是一个分布式系统看起来像一个巨大的、超链接的集合。
  • 基于文件系统:看起来像一个大型文件系统
  • 基于对象:对象是变量的集合,这些变量与一套称为方法的访问过程绑定在一起。进程不允许直接访问这些变量。相反,要求它们调用方法来访问。
  • 基于协作:发布订阅
  • 分布式系统是一个松散耦合的系统,其中每一个节点是一台完整的计算机,配有全部的外部设备以及自己的操作系统。这些系统常常分布在较大的地理区域内。在操作系统上通常设计有中间件,从而提供一个统一的层次以方便与应用程序的交互。中间件的类型包括基于文档、基于文件、基于对象以及基于协调的中间件。

第九章 安全

  • 后门陷阱:由程序员跳过一些常见的检测并插入一段代码造成的。防止后门的一个方法是代码审查(code review);
  • 木马:用户自己下载的恶意软件
  • 病毒(virus):是一种特殊的程序,它可以通过把自己植入到其他程序中来进行“繁殖“。
  • 信息系统安全性:
    • 数据机密性:数据暴露
    • 数据完整性:数据篡改
    • 系统可用性:拒绝攻击
  • 主动攻击会使计算机程序行为异常。
  • 被动攻击试图窃取信息。
  • 加密是将一条消息或文件进行转码,除非获得秘钥,否则很难恢复出原信息。
  • 程序加固是指在程序中加入保护机制从而使得攻击者很难破坏程序。操作系统运用加固,组织攻击者在运行的软件中插入新代码,确保每一个进程都遵循最小权限原则。
  • 现在已知的建立安全系统仅有的办法是保持系统的简单性。特性是安全的大敌。
  • 要构建一个安全的系统,需要操作系统的核心中实现安全模型,且该模型要非常简单,从而设计人员确实能够理解模型的内涵,并且顶住所有压力,避免偏离安全模型的要求去添加新的功能特性。
  • 最低权限原则
  • ACL访问控制列表:按列存放矩阵。
file1--------->[ A:R; C:W ]
file2--------->[ A:RW; B:E ]
file3--------->[ B:W; D:RWE ]
  • 权能字列表:按行存储矩阵。
A------->[ file1: R; file2: RW ]
B------->[ file2: E; file3: W ]
C------->[file1: W ]
D------->[ file3: RWE ]
  • 图片隐写术:彩色图片是1024 * 768像素的,每个像素包括3个8位数字,分别代表红、绿、蓝三原色的亮度。像素的颜色是通过三原色线性重叠形成的。编码程序使用每个RGB色度的低位作为隐秘信道。这样每个像素就有三位的秘密空间存放信息。这种情况下,图片大小将增加1024 * 768 *3位的空间来存放信息。
  • 数字签名
  • Kerckhoffs原则:加密算法应该完全公开,而加密的安全性由独立于加密算法之外的秘钥决定。
  • 公钥加密技术:加密秘钥和解密秘钥是不同的,并且当给出了一个筛选过的加密秘钥后不可能推出对应的解密秘钥。加密秘钥可公开,如RSA
    • 在公钥密码体系中,加密运算比较简单,而没有秘钥的解密运算却十分繁琐。
    • 公钥机制的主要问题在于运算速度要比对称秘钥机制慢数千倍。
  • 单向函数:加密散列函数。md5 sha1
  • 数字签名
    • 原始文档经过单向函数获得散列值
    • 文件所有者运用它的私钥加密散列值,获得签名块,附加到原始文档发送
    • 接收方接收到文档后,先用一样的单向函数获取散列值
    • 使用发送方的公钥对签名块进行解密,和上面得到的散列值比较
  • 为保证公钥的安全,常用方法是数字证书,证书包含了用户姓名、公钥和可信任的第三方数字签名。数字证书由认证机构CA颁发。浏览器内置了著名CA的公钥。
  • 硬件加密:可信平台模块TPM芯片:使用内部的非易失性存储介质来保存密钥。
    • 软件漏洞:
      • 缓冲区溢出攻击:
        • 数据溢出覆盖栈,修改栈的返回地址,执行黑客注入的代码
        • 采用数据执行保护,禁止执行栈上的代码
        • 代码重用攻击:返回地址指向已存在的代码片段
        • 利用溢出修改数据值
      • 空指针间接引用攻击
      • 垂悬指针
      • 整数溢出攻击
      • 命令注入攻击
    • 后门陷阱:免密登录账号等,可以用代码审查避免
    • 逻辑炸弹
  • rootkit:一个程序或一些程序和文件的集合,它试图隐藏自身的存在,即使被感染主机的拥有者对其进行定位和删除
  • 防御
    • 防火墙:硬件防火墙和软件防火墙
    • 反病毒程序
    • 代码签名
    • IDS

第十章 Unix Linux

  • 内核坐落在硬件之上,负责实现与I/O设备和存储管理单元的交互,并控制CPU对前述设备的访问。

  • 所有的linux驱动程序都可以分为字符驱动程序或块驱动程序,两者之间的主要区别是块设备允许查找和随机访问而字符设备不允许。

  • linux中的进程:每一个进程都拥有一个独立的程序计数器,用这个程序计数器可以追踪下一条将要被执行的指令。一旦进程开始运行,Linux系统允许它创建额外的线程。

  • linux调用fork生成子进程。但是父进程和子进程可以共享已经打开的文件。但是父进程和子进程的内存映像、变量、寄存器以及其他所有的东西都是相同的。区分fork父进程和子进程在于它的返回值,对于父进程,返回大于0的子进程号,对于子进程,返回0。

    • linux进程间通信:
    • 消息传递:
    • 管道
    • 软中断:信号
  • 进程组:父进程、兄弟进程、子进程。

  • 一个进程可以利用系统调用给它所在的进程组中所有成员发送信号。

  • linux 32位机器上,每个linux通常有3GB的虚拟内存空间,还有1GB留给其页表和其他内核数据。在用户态下运行时,内核的1GB是不可见的,但是当进程陷入到内核时是可以访问的。内核内存通常驻留在低端物理内存中,但是被映射到每个进程虚拟地址空间顶部的1GB中。

  • 日志文件系统的基本思想:

    • 维护一个日志,该日志顺序记录所有文件系统操作。通过顺序写出文件系统数据或元数据(i节点,超级快等)的改动,该操作不必忍受随机磁盘访问时磁头移动带来的开销。最后,这些改动将被写到适当的磁盘地址,而相应的日志项可以被丢弃。如果系统崩溃或电源故障在改动提交之前发生,那么在重启动过程中,系统将检测到文件系统没有被正确的卸载。然后系统遍历日志,并执行日志记录所描述的文件系统改动。
  • 网络文件系统:NFS

    • 基本思想:允许任意选定的一些客户端和服务器共享一个公共文件系统。
    • 体系结构:
    • 协议:从客户端发送到服务器的一组请求以及从服务器返回给客户端的响应的集合。
    • 实现:
    • 每一个NFS服务器都导出一个或多个目录供远程客户端访问。当一个目录可用时,它的所有子目录也都可用,正因如此,通常整个目录树通常作为一个单元导出。

现代操作系统设计

  • 操作系统重要的要素
    • 定义抽象概念:
    • 提供基本操作:
    • 确保隔离
    • 管理硬件
  • 接口设计
    • 简单:一个简单的接口更加易于理解并且更加易于以无差错的方式实现。
    • 完备:接口必须能够做用户需要做的一切事情。操作系统应该不多不少准确地做它需要做的事情。
      • 万事都应该尽可能简单,但是不能过于简单。——爱因斯坦
      • 重要的是强调简单和精炼的价值,因为复杂容易导致增加困难并且产生错误。正如我们已经看到的那样。我对精炼的定义是以机制的最小化和清晰度的最大化实现特定的功能
    • 效率
  • 不要隐藏能力。如果硬件具有极其高效的方法做某事,它就应该以简单的方法展露给程序员,而不应该掩埋某些抽象的内部。抽象的目的是隐藏不合需要的特性,而不是隐藏值得需要的特性。

你可能感兴趣的:(读书笔记,操作系统,内存管理)