操作系统概念v9 Abraham Silberschatz 全文笔记

第一章 导论

  • 操作系统可以看做是资源分配器
  • 对于UNIX,首个系统进程为"init",它启动许多其他系统的后台程序。一旦这个阶段完成,系统就完全启动了,并且等待事件发生
    • 事件发生通常通过硬件或软件的中断(interrupt)来通知。硬件可以随时通过系统总线发送信号到CPU,以触发中断。软件也可通过特别操作即系统调用(system call)(也称为监督程序调用(monitor call)),以触发中断。
    • 当CPU被中断时,它停止正在做的事,并立即转到固定位置再继续执行。该固定位置通常包含中断服务程序的开始地址。中断服务程序开始执行,在执行完后,CPU重新执行被中断的计算。
  • 多处理器类型
    • 非对称处理:每个处理器都有特定的任务。一个主处理器控制系统,其他处理器或者向主处理器要任务或做预先规定的任务。
    • 对称多处理SMP:【最常用】,每个处理器都参与完成操作系统的所有任务。
  • 内存访问模型
    • 均匀内存访问UMA:CPU访问RAM的所需时间相同
    • 非均匀内存访问NUMA:有的内存访问需要的时间越多
  • 设备控制器:每个设备控制器维护一定量的本地缓冲存储和一组特定用途的寄存器。设备控制器负责在所控制的外围设备与本地缓冲存储之间进行数据传递。通常操作系统为每个设备控制器提供一个设备驱动程序。该设备驱动程序负责设备控制器,并且为操作系统的其他部分提供统一的设备访问接口。
  • 在开始I/O时,设备驱动程序加载设备控制器的适当寄存器。相应的,设备控制器检查这些寄存器内容,以便决定采取什么操作。控制器开始从设备向本地缓冲区传递数据。一旦完成数据传输,设备控制器会通过中断通知设备驱动程序,它已完成了操作。然后,设备驱动程序返回控制到操作系统。对于读操作,数据或指针也会返回;而对于其他操作,设备驱动程序返回状态信息。
  • 这种I/O中断驱动适合移动少量数据,但是对于大量数据的移动,如磁盘I/O,就会带来很高的开销。为了解决这个问题,可以采用直接内存访问(DMA)。在为这种I/O设备设置好缓冲,指针和计数器之后,设备控制器可在本地缓冲和内存之间传递整块的数据,而无需CPU的干预。每块只产生一个中断。当设备控制器执行这些操作时,CPU可以进行其他工作。
  • 现代操作系统是中断驱动(interrupt driven)的。
  • 陷阱(trap)(或异常(exception))是一种软件生成的中断,或源于出错(如除数为0或无效内存访问),或源于用户程序的特定请求(执行操作系统的某个服务)。这种操作系统的中断特性规定了系统的通用结构。对于某种中断,操作系统有不同的码段来处理。中断服务程序用于处理中断。
  • 双重模式保护【用户模式和内核模式】:
    • 将可能引起损害的机器指令作为特权指令,并且硬件只有在内核模式下才允许执行特权指令。如果在用户模式下试图执行特权指令,那么硬件并不执行该指令,而是认为该指令非法,并将其以陷阱形式通知操作系统。
    • 当要执行系统调用时,硬件通常将它作为软件中断。控制通过中断向量转到操作系统的中断服务程序,并且模式位也设为内核模式。系统调用服务例程是操作系统的一部分。内核检查中断指令,判断发生了什么系统调用;参数表示用户程序请求何种服务。请求所需的其他信息可以通过寄存器、堆栈或内存(内存指针也可通过寄存器传递)来传递。内核首先检验参数是否正确和合法,然后执行请求,最后控制返回到系统调用之后的指令。
  • 定时器可以防止用户程序运行过长。
  • 操作系统负责进程管理的以下活动:
    • 在CPU上调度进程和线程
    • 创建和删除用户进程和系统进程
    • 挂起和重启进程
    • 提供进程同步机制
    • 提供进程通信机制
  • 内存一般是CPU所能直接寻址和访问的、唯一的、大容量的存储器。如果CPU需要处理磁盘数据,那么这些数据必须通过CPU产生的I/O调用传到内存。同样,如果CPU需要执行指令,那么这些指令必须在内存中。操作系统负责的内存管理活动:
    • 记录内存的那部分在被使用以及被谁使用
    • 决定哪些进程(或其部分)会调入或调出内存
    • 根据需要分配和释放内存空间。
  • 操作系统的目的之一是为了用户隐藏具体硬件设备的特性。例如,在UNIX系统中,I/O子系统为操作系统本身隐藏了I/O设备的特性。
    • I/O子系统包括了以下几个组件:
      • 包括缓冲、高速缓存和假脱机的内存管理组件。
      • 设备驱动器的通用接口。
      • 特定硬件设备的驱动程序。
    • 只有设备驱动程序才能知道控制设备的特性。
  • 内存寻址硬件确保一个进程或仅可在自己的地址空间内执行,定时器确保没有进程可以一直占用CPU而不释放它。因此,保护是一种机制,用于控制进程或用户访问计算机系统的资源。

第二章 操作系统结构

用户角度:系统提供的服务
  • 操作系统为用户提供的一组功能
    • 用户界面:CLI和GUI
    • 程序执行:系统应能加载程序,并执行
    • I/O操作:程序运行可能需要I/O,这些I/O可能涉及文件或设备
    • 文件系统操作:
    • 通信:进程间需要通信。共享内存和消息交换
    • 错误检测:操作系统需要不断检测错误和更正错误。
  • 为了确保系统高效运行,多用户系统通过共享计算机资源可以提高效率。操作系统还提供另一组服务
    • 资源分配:当多个用户或多个作业同时运行时,每个都应该分配资源。
    • 记账:需要记录用户使用资源的类型和数量。
    • 保护和安全:对于保存在多用户或联网的计算机系统的信息,用户可能需要控制信息使用。当多个独立进程并发执行时,一个进程不应干预其他进程或操作系统本身。保护应该确保可以控制系统资源的所有访问。
程序员角度:接口
  • UNIX:POSIX API
  • 系统调用提供操作系统服务接口
  • 程序员可通过操作系统提供的库函数来调用API。对于运行于UNIX和Linux的用C语言编写的程序,该库名为libc
  • 系统调用分类
    • 进程控制
    • 文件管理
    • 设备管理:进程执行需要一些资源,如内存、磁盘驱动、所需文件等。
    • 信息维护:如time()
    • 通信:进程间通信的常用模型有两个:消息传递模型和共享内存模型
    • 保护:提供控制访问计算机的系统资源的机制。
操作系统设计人员角度:系统组件及其相互关系
  • 机制与策略分离。机制决定如何做,而策略决定做什么。
  • 操作系统结构:
    • 简单结构:系统调用接口和内核通信的开销非常小
    • 分层方法:简化了构造和调试;难点在于合理定义各层
    • 微内核:便于扩展操作系统。主要功能是为客户端程序和运行在用户空间中的各种服务提供通信。通信是通过消息传递来提供的。
    • 模块:可加载内核模块,内核有一组核心组件,无论在启动或运行时,内核都可通过模块链入额外服务。设计的思想是内核提供核心服务,而其他服务可在内核运行时动态实现。
      • linux也采用可加载内核模块,主要用于设备驱动程序和文件系统
  • Linux也采用可加载内核模块,主要用于设备驱动程序和文件系统。
  • 操作系统调试:DTrace可以动态探测正在运行的系统。
  • 系统引导:加载内核以启动计算机的过程

第三章 进程

  • 进程是执行的程序:进程通常包括全文本段、当前活动,如程序计数器的值和处理寄存器的内容等;进程堆栈(stack)(包括临时数据,如函数参数、返回地址和局部变量),数据段(包括全局变量)。进程还可能包含堆(heap 进程运行时动态分配的内存)
  • 进程状态:
    • 新的:new 进程正在创建
    • 运行:running 指令正在运行
    • 等待:waiting 进程等待发生某个时间(如I/O完成或收到信号)
    • 就绪 :ready 进程等待分配处理器
    • 终止:terminated 进程已经完成执行
  • 进程控制块:Process Control Block 也称为任务控制块:包含很多信息
    • 进程状态
    • 程序计数器:计数器表示进程将要执行的下个指令的地址
    • cpu寄存器:
    • CPU调度信息:这类信息包括进程优先级、调度队列的指针和其他调度参数
    • 内存管理信息:根据操作系统使用的内存系统,这类信息可以包括基地址和界限寄存器的值、页表和段表。
    • 记账信息:这类信息包括CPU时间、实际使用时间、时间期限、记账数据、作业和进程数量等。
    • I/O状态信息:这类信息包括分配给进程的I/O设备列表,打开文件列表等。
  • 进程调度:进程调度器选择一个可用进程到CPU上执行。
  • 调度队列:
    • 作业队列(job queue):进程进入系统时加入,这个队列包括系统内的所有进程。
    • 就绪队列(ready queue):驻留在内存中、就绪的、等待运行的进程保存在就绪队列。
    • 设备队列(divice queu):等待特定I/O设备的进程列表。
  • 最初,新进程被加载到就绪队列;它在就绪队列中等待,直到被选中执行或被分派(dispatched)。当该进程分配到CPU并执行时,以下事件可能发生:
    • 进程可能发出I/O请求,并被放到I/O队列。
    • 进程可能创建一个新的子进程,并等待其终止
    • 进程可能由于中断而被强制释放CPU,并被放回到就绪队列。
    • 对于前两种情况,进程最终从等待状态切换到就绪状态,并放回到就绪队列。进程重复这一循环直到终止;然后他会从所有队列中删除,其PCB和资源也被释放。
  • I/O密集型进程:执行I/O比执行计算需要花费更多时间
  • CPU密集型进程:
  • 上下文切换:切换CPU到另一个进程需要保存当前进程状态和恢复另一个进程状态
  • 进程
    • 在执行过程中可能创建多个进程。创建进程的称为父进程,而新的进程称为子进程。每个新进程可以再创建其他进程,从而形成进程树。
    • 一般来说,当一个进程创建子进程时,该子进程会需要一定的资源(CPU时间、内存、文件、I/O设备等)来完成任务。子进程可以从操作系统哪里直接获得资源,也可以只从父进程哪里获得资源子集。父进程可能要在子进程之间分配资源或共享资源(如内存或文件)。限制子进程只能使用父进程的资源,可以防止创建过多进程,导致系统超载。
    • 除了提供各种物理和逻辑资源外,父进程也可能向子进程传递初始化数据(或输入)。
    • 当进程创建新进程时,可有两种执行可能:
      • 父进程与子进程并发执行
      • 父进程等待,直到某个或全部子进程执行完
    • 新进程的地址空间也有两种可能:
      • 子进程是父进程的复制品(它具有与父进程同样的程序和数据)。
      • 子进程加载另一个新程序。
  • 当一个进程终止时,操作系统会释放其资源。不过,它位于进程表中的条目还是存在的,直到它的父进程调用wait();这是因为进程表包含了进程的退出状态。当子进程终止时,但是其父进程尚未调用wait(),这样的进程称为僵尸进程(zombie process)。所有进程终止都会过度到这种状态,但是一般而言僵尸只是短暂存在的。一旦父进程调用wait,僵尸进程的进程标识符合他的进程表中的条目就会释放。
  • 如果父进程没有调用wait就终止,以至于子进程就变为孤儿进程,linux的处理是:将init进程作为孤儿进程的父进程。init会定期调用wait,以便收集任何孤儿进程的退出状态,并释放孤儿进程标识符和进程表条目。
  • 操作系统主要有两种队列:I/O请求队列和就绪队列
  • 操作系统内的并发执行进程可以是独立的或也可以是协作的。如果一个进程不能影响其他进程或受其他进程影响,那么该进程是独立的。能受影响,则是协作。显然,与其他进程共享数据的进程为协作进程。
  • 允许进程协作的诸多理由:
    • 信息共享:由于多个用户可能对同样的信息感兴趣(例如共享文件),所以应该提供环境以允许并发访问这些信息。
    • 计算加速:如果希望一个特定任务快速执行,那么应将它分成子任务,而每个子任务可以和其他子任务一起并行执行。注意,如果要实现这样的加速,那么计算机需要多核
    • 模块化:可能需要按照模块化方式构造系统。
    • 方便:即使单个用户也能同时执行许多任务
  • 协作进程间需要一种进程间通信(IPC)机制,以允许进程相互交换数据和信息。进程间通信的两种基本模型:共享内存和消息传递。
    • 共享内存模型会建立在一块提供协作进程共享的内存区域,进程通过向此共享区域读出或写入数据来交换信息。
    • 消息传递模型通过在协作进程间交换信息来实现通行。
    • 由于消息传递常常采用系统调用实现,因此需要消耗更多的时间以便内核介入。与此相反,共享内存只是在建立共享区域时需要系统调用;一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。
  • chrome多进程架构浏览器,三类进程:
    • 浏览器进程负责管理用户界面以及磁盘和网络I/O。当Chrome启动时,创建一个新的浏览器进程。只创建一个
    • 渲染器进程包含渲染网页的逻辑。一般每个tab都会创建一个进程
    • 对于每个插件,都有一个插件进程。
    • 多进程方法的优点是:网站彼此独立运行,如果一个网站崩溃,只有它的渲染进程收到影响。渲染进程是在沙箱中运行,这意味着访问磁盘和网络I/O是受限制的,进而最大限度地减少任何安全漏洞的影响。
  • 对于多个处理核系统的最新研究表明:在这类系统上,消息传递的性能要优于共享内存。共享内存会有高速缓存一致性问题,这是共享数据在多个高速内存之间迁移而引起的。随着系统处理核的数量的日益增加,可能导致消息传递成为IPC的首选机制。
  • 共享内存系统:
    • 采用共享内存的进程间通信,需要通信进程建立共享内存区域。通常,操作系统试图阻止一个进程访问另一个进程的内存。共享内存需要两个或者更多的进程同意取消这一限制;这样它们通过在共享区域内读出或者写入来交换信息。
    • 消费者进程生产消息,消费者进程消费。解决这对问题的方法之一:采用共享内存。为了允许生产者进程和消费者进程能并发执行,应有一个缓存区,以被生产者填充和被消费者清空。这个缓冲区驻留在生产者进程和消费者进程的共享区域内。
  • 缓冲区类型可分为两种:
    • 无界缓冲区:没有限制缓冲区大小。消费者可能不得不等待新的项,但是生产者总是可以产生新项。
    • 有界缓冲区:有固定大小的缓冲区。对于这种情况,如果缓冲区为空,那么消费者必须等待;如果缓冲区满了,那么生产者必须等待。
  • 消息传递系统
    • 消息传递工具至少有两种操作:send(message) receive(message)
    • 如果进程P和Q需要通信,那么它们需要相互发送消息和接受消息:它们之间需要有通信链路。该链路有多种实现方法,我们只关心逻辑实现:
      • 直接或者间接的通信
      • 同步或者异步的通信
      • 自动或者显示的缓冲
    • 直接通信:需要通信的每个进程必须明确指定通信的接收者或发送者。原语:send(P,message),向进程P发送message;receive(Q,message),从进程Q接受message。
    • 间接通信:通过邮箱或者端口来发送或者接受消息。邮箱可以抽象成一个对象,进程可以向其中存放信息,也可以从中删除消息,每个邮箱都有一个唯一的标识符。send(A,message);receive(A,message)
    • 同步或者异步:
      • 消息传递可以是阻塞或非阻塞的,也称为同步或异步
      • 阻塞发送:发送进程阻塞,直到消息由接受进程或邮箱所接受
      • 非阻塞发送:发送进程发送消息,。并且恢复操作。
      • 阻塞接收:接受进程阻塞,直到有消息可用。
      • 非阻塞接收:接收机制收到一个有效消息或空消息。
    • 同步和异步针对应用程序来,关注的是程序中间的协作关系;阻塞与非阻塞更关注的是单个进程的执行状态。
      • 同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。
      • 异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。
      • 阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。
      • 非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。
    • 缓存:
      • 不管通信是直接还是间接,通信进程交换的信息总是驻留在临时队列中。
      • 队列实现的三种方法:
        • 零容量:队列的最大长度为0;因此,链路不能有任何消息处于等待,对于这种情况,发送者应阻塞,直到接受者收到消息
        • 有限容量:队列长度为有限的n;因此,最多只能有n个消息驻存在其中。如果发送新消息时队列未满,那么该消息可以存放在队列中(或者复制信息或者保存消息的指针),且发送者可以继续执行而不必等待。然而,链路容量是有限的,如果链路容量已满,那么发送者应阻塞,直到队列空间可用为止
        • 无限容量:队列长度可以无限,因此,不管有多少消息都可以在其中等待,发送者从不阻塞。
      • 零容量情况称为无缓存消息系统,其他情况称为自动缓冲的消息系统。
  • POSIX共享内存的实现为内存映射文件,它将共享内存区域与文件相关联。下面为ubuntu下实现代码
// producer.c
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
// cc producer.c -pthread -lrt -o producer && ./producer
int main() {
    const int SIZE = 4096;
    const char *name = "OS";
    const char *msg0 = "Hello";
    const char *msg1 = " World!";
    const char *msg2 = " linux os!";

    int shm_fd;
    void *ptr;
    // 创建共享内存对象
    shm_fd = shm_open(name,O_CREAT|O_RDWR,0666);
    // 配置共享内存大小
    ftruncate(shm_fd,SIZE);
    // 创建内存映射文件,以便包含共享内存对象
    ptr = mmap(0,SIZE,PROT_WRITE,MAP_SHARED,shm_fd,0);

    // 写共享内存对象
    sprintf(ptr,"%s",msg0);
    ptr += strlen(msg0);
    sprintf(ptr,"%s",msg1);
    ptr += strlen(msg1);
    sprintf(ptr,"%s",msg2);
    ptr += strlen(msg2);
    return 0;
}

// consumer.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

// cc consumer.c -pthread -lrt -o consumer && ./consumer
int main() {
    const int SIZE = 4096;
    const char *name = "OS";
    int shm_fd;
    void *ptr;

    shm_fd = shm_open(name,O_RDONLY,0666);
    ftruncate(shm_fd,SIZE);
    ptr = mmap(0,SIZE,PROT_READ,MAP_SHARED,shm_fd,0);

    printf("%s\n",(char *)ptr);
    shm_unlink(name);
    return 0;
}
  • Windows消息传递工具:高级本地程序调用ALPC。
    • 服务器进程发布连接端口对象,以便所有进程都可访问。当一个客户需要子系统服务时,它会打开服务器连接端口对象的句柄,并向端口发送一个连接请求。然后,服务器创建一个通道,并把句柄返回给客户。通道包括一对私有通信端口:一个用于客户机到服务器消息,另一个用于服务器到客户机。此外,通道有个回调机制,允许客户和服务器在等待应答也能接收请求
    • 它不属于Windows API,不能被应用程序员所使用的。应用程序员使用RPC,但是当在同一系统内时,采用ALPC实现。
  • 客户机/服务器通信的其他三种策略:套接字,远程程序调用(rpc)和管道
  • 使用套接字的通信,虽然常用和高效,但是属于分布式进程之间的一种低级形式的通信。一个原因是,套接字只允许线程之间交换无结构的字节流。
  • rpc语义允许客户调用位于远程主机的过程,就如调用本地过程一样。
  • 管道允许两个进程通信:
    • 半双工:数据在同一时间内只能按照一个方向传输
    • 全双工:数据在同一个时间可以在两个风向上传输。
    • 普通管道:普通管道是单向的,只允许单向通信。通信的进程具有父子关系。
    • 命名管道提供了一个更强大的通信工具。通信可以是双向的。并且父子关系不是必须的,当建立一个命名管道后,多个进程都可用它通信。对于LINUX,命名管道为FIFO,但只允许半双工传输。(windows为全双工)
// unix pipe
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1

int main(void){
    char writeMsg[BUFFER_SIZE]="Greetings";
    char readMsg[BUFFER_SIZE];
    int fd[2];
    pid_t pid;
    if(pipe(fd)==-1){
        fprintf(stderr,"Pipe failed");
        return 1;
    }

    pid = fork();
    if(pid<0){
        fprintf(stderr,"Fork failed");
        return  1;
    }

    if(pid>0){
        close(fd[READ_END]);
        write(fd[WRITE_END],writeMsg, strlen(writeMsg)+1);
        close(fd[WRITE_END]);
    } else{
        close(fd[WRITE_END]);
        read(fd[READ_END],readMsg,BUFFER_SIZE);
        printf("child read %s %d %d\n",readMsg,fd[READ_END],fd[WRITE_END]);
        close(fd[READ_END]);
    }
    return 0;
}

第四章 多线程编程

  • 每个线程是CPU使用的一个基本单元;它包括线程ID、程序计数器、寄存器组合堆栈。它与同一个进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。每个传统或重量级进程只有单个控制线程。如果一个进程具有多个控制线程,那么它能同时执行多个任务。大多数操作系统的内核都是多线程的。
  • 多线程优点:
    • 响应:多线程并行执行,增加对用户的响应程度
    • 资源共享:进程只能通过共享内存和消息传递之类的技术共享资源。这些技术应由程序员显示的安排。不过,线程默认共享他们所属进程的内存和资源。代码和数据共享的优点是:它允许一个应用程序在同一个地址空间内有多个不同的活动线程。
    • 经济:创建简单,切换快
    • 可伸缩性:对于多处理器体系结构,多线程的优点更大,因为线程可在多核处理器上并行运行。不管有多少个CPU,单线程进程只能在一个CPU上。
  • 多核系统趋势继续迫使系统设计人员和编程开发人员更好的使用多个计算核。
  • 多核系统编程有5个方面的挑战:
    • 识别任务:这涉及分析应用程序,查找区域以便分为独立的、并发的任务。在理想情况下,任务是相互独立的,因此可以在多核上并发运行。
    • 平衡:确保任务执行具有同等价值的工作
    • 数据分割:操纵的数据也需要分开
    • 数据依赖:多任务之间的依赖关系
    • 测试与调试:调试路径多
  • 并行类型:数据并行和任务并行
    • 数据并行注重将数据分布在多个计算核上,并在每个核上执行相同操作
    • 任务并行涉及将任务(线程)而不是数据分配到多个计算核。
  • 多线程模型:
    • 用户线程:
    • 内核线程:
      • 多对一模型:多个用户线程映射到一个内核线程。线程管理是由用户空间的线程库来完成的,因此效率更高。不过,如果一个线程执行阻塞系统调用,那么整个进程将会阻塞。再者,因为任一时间只有一个线程可以访问内核,所以多个线程不能并行运行在多核处理器上
      • 一对一模型:每个用户线程映射到一个内核线程。由于创建内核线程的开销会影响程序的性能,所以这种模型的大多数实现限制了系统支持的线程数量。linux和windows操作系统的家族,都实现了一对一模型。
      • 多对多模型:多路复用多个用户线程到同样数量或者更少数量的内核线程。
  • 线程库:为程序员提供创建和管理线程的API
  • 实现线程库的主要方法有两种:
    • 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都位于用户空间中。这意味着,调用库内的一个函数只是导致了用户空间的一次本地函数的调用,而不是系统调用。
    • 实现由操作系统直接支持的内核级的一个库。对于这种情况,库内的代码和数据结构都位于内核空间。调用库中的API函数通常会导致对内核的系统调用。
  • 目前使用的三种主要线程库有:POSIX Pthreads、Windows 、java。Ptheads作为POSIX标准的扩展,可以提供用户级和内核级的库。
  • 多线程创建的两个常用策略:
    • 异步线程:一旦父线程创一个子线程后,父线程就恢复自由执行,这样父线程和子线程并发执行。每个线程的运行独立于其他线程,父线程无需知道子线程何时终止。由于线程是独立的,所以线程之间很少有数据共享。
    • 同步线程:如果父线程创建一个或多个子线程后,在恢复执行之前应等待所有子线程终止(分叉-连接策略)。这里父线程创建的线程并发执行工作,但是父线程在这个工作完成之前无法继续。一旦每个线程完成他的工作,他就会终止,并与父线程连接。只有在所有子线程都连接之后,父线程才恢复执行。通常,同步线程涉及线程之间的大量数据的共享。例如父线程可以组合子线程计算的结果。
  • pthreads是POSIX标准定义的线程创建和同步API,这是线程行为的规范而不是实现。
// pthreads简单使用,创建一个线程执行计算并等待返回结果
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

int sum;
void *runner(void *param);
int main(int argc,char *argv[]){
    pthread_t tid;
    pthread_attr_t attr; // 线程属性
    if(argc!=2){
        fprintf(stderr,"usage: a.out <integer value>\n");
        return  -1;
    }
    if(atoi(argv[1])<0){
        fprintf(stderr,"%d must be >=0\n",atoi(argv[1]));
        return -1;
    }

    pthread_attr_init(&attr); // 获取默认属性
    pthread_create(&tid,&attr,runner,argv[1]); // 创建线程
    pthread_join(tid,NULL);// 等待线程退出
    printf("sum=%d\n",sum);
    return 0;
}

void *runner(void *param){
    int i,upper=atoi(param);
    sum=0;
    for(i=0;i<=upper;i++) sum+=i;
    pthread_exit(0);
}
  • 隐式多线程:将多线程的创建与管理交给编译器和运行时库来完成。
  • 线程池:在线程开始时创建一定数量的线程,并加到池中以等待工作,当服务器收到请求时,它会唤醒池内的一个线程(如果有可用线程),并将需要服务的请求传递给它。一旦线程完成服务,它会返回池中继续等待工作。如果池中没有可用线程,那么服务器会等待,直到有空线程为止。
  • 线程池的优点:
    • 用现有的线程服务请求比等待创建一个线程更快。
    • 线程池限制了任何时候可用线程的数量。
    • 将要执行任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务。
  • openmp
  • 大中央调度:GCD
  • 多线程问题:
    • fork和exec
    • 信号处理
    • 线程撤销:线程完成之前终止线程。
    • 线程本地存储:
    • 调度程序激活
  • linux线程:linux通过系统调用fork()提供进程复制的传统功能。通过系统调用clone()也提供创建线程的功能。然而,Linux不区分进程和线程。
  • 当调用fork()时,创建的新任务具有父进程的所有数据结构的副本。当调用clone()系统调用时,也创建了新任务。不过,新任务并不具有所有数据结构的副本;新任务,根据传递给clone()的标志组,指向父任务的数据结构。

第五章 进程调度

  • 对于支持线程的操作系统,操作系统实际调度的是内核级线程而非进程。不过,术语进程调度 (process scheduling)和线程调度(thread scheduling)常常交替使用
  • 进程执行包括周期进行CPU执行和I/O等待
  • 在非抢占调度中,一旦某个进程分配到CPU,该进程就会一直使用CPU,直到它终止或切换到等待状态
  • 调度程序是一个模块,用来将CPU控制交给由短期调度程序选择的进程。这个功能包括:
    • 1.切换上下文
    • 2.切换到用户模式
    • 3.跳转到用户程序的合适位置,以便重新启动程序。
    • 调度程序应该尽量快,因为每次进程切换时都要使用。调度程序停止一个进程而启动另一个所需要的时间称为调度时延(dispatch latency)
  • 调度准则:
    • CPU使用率:应使CPU尽可能地忙碌
    • 吞吐量:一个单位时间内,进程完成的数量。对于长进程,吞吐量可能每一小时一个进程;对于短进程,吞吐量可能每秒10个进程
    • 周转时间:从进程提交到进程完成的时间段称为周转时间。周转时间为所有时间段之和
    • 等待时间:CPU调度算法并不影响进程运行和执行I/O的时间,它只影响进程在就绪队列中因等待所需的时间。等待时间为在就绪队列中等待所花时间之和。
    • 响应时间:对于交互系统,进程可以相当早的产生输出,并且继续计算新的结果同时输出以前的结果给用户、
  • 调度算法:
    • 先到先服务调度:FCFS,非抢占,缺点是:平均等待时间往往很长
    • 最短作业优先调度:
    • 优先级调度:主要问题是阻塞,可是使用老化,逐渐增加在系统中等待很长时间的进程的优先级
    • 轮转调度:抢占
    • 多级队列调度:在固定的队列中
      • 前台进程(交互进程),高优先级
      • 后台进程(批处理进程)
    • 多级反馈队列调度:允许进程在队列之间迁移,最通用的CPU调度算法
  • 线程调度:在支持现成的操作系统上,内核级线程(而不是进程)才是操作系统所调度的。用户级线程是由线程库来管理的。用户级线程运行在CPU上,最终应映射到相关的内核级线程,但是这种映射可能不是直接的,可能采用轻量级进程(LWP);
    • 进程竞争范围PCS:竞争CPU发生在同一个进程的线程之间。
    • 系统竞争范围SCS:发生在系统内的所有线程之间
// 设置线程调度竞争范围
#include <pthread.h>
#include <stdio.h>

#define NUM_THREADS 5

void *runner(void *param);

int main(int argc, char *argv[]) {
    int i, scope;
    pthread_t tid[NUM_THREADS];
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    if (pthread_attr_getscope(&attr, &scope) != 0) {
        fprintf(stderr, "unable to get scheduling scope\n");
    } else {
        if (scope == PTHREAD_SCOPE_PROCESS) { // 按PCS来调度线程
            printf("PTHREAD_SCOPE_PROCESS");
        } else if (scope == PTHREAD_SCOPE_SYSTEM) { // 按SCS来调度线程
            printf("PTHREAD_SCOPE_SYSTEM");
        } else {
            fprintf(stderr, "Illegal scope value.\n");
        }
    }

    pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS); // 设置调度线程的竞争范围

    for (i = 0; i < NUM_THREADS; i++) pthread_create(&tid[i], &attr, runner, NULL);
    for (i = 0; i < NUM_THREADS; i++)pthread_join(tid[i], NULL);
}

void *runner(void *param) {
    printf("work !!!!\n");
    pthread_exit(0);
}
  • 多处理器调度:
    • 非对称多处理器:让一个处理器处理所有调度决定、I/O处理以及其他活动,其他处理器只执行用户代码
    • 对称多处理(SMP):每个处理器自我调度,windows、linux、mac osx都支持
  • 处理器亲和性:一个进程对它运行的处理器具有亲和性。保持一个进程运行在同一处理器上的好处是进程可以利用它在该处理器缓存内的数据。
  • 对于SMP系统,重要的是保持所有处理器的负载均衡,以便充分利用多处理器的优点。负载平衡通常有两种方法:推迁移(push migration)和拉迁移(pull migration)。
    • 对于推迁移,一个特定的任务周期性地检查每个处理器的负载,如果发现不平衡,那么通过将进程从超载处理器推到(push)空闲或不太忙的处理器,从而平均分配负载。
    • 当空闲处理器从一个忙的处理器上拉(pull)一个等待任务时,发生拉迁移。
    • 推迁移和拉迁移不必相互排斥,事实上,在负载平衡系统中它们常被并行实现。
  • 当一个处理器访问内存时,它花费大量的时间等待所需数据,这种情况称为内存停顿。
    • 硬件采用高多线程的处理器核,即每个核会分配到两个或多个硬件线程。这样当一个线程发生内存停顿时,该核可切换到另一个线程
    • 四核8线程,就是每个核有两个线程,当一个线程在内存停顿(等待数据)时,切换另一个运行,这样就就有8个逻辑处理器了
  • 实时CPU调度:
    • 软实时系统:不保证会调度关键实时系统,而只保证这类进程会优先于非关键进程。
    • 硬实时系统:有严格的要求。一个任务在他的截止期限之前完成;在截止之后完成与没有完成是完全一样的。
  • 两种类型的延迟影响实时系统的性能:
    • 中断延迟是从CPU收到中断到中断处理程序开始的时间。当一个中断发生时,操作系统应先完成正在执行的指令,再确定发生中断的类型。然后,它应保存当前进程的状态,再采用特定的中断服务例程(ISR)来处理中断。影响中断延迟的一个重要因素是:在更新内核数据结构时中断可能会被禁用的时间量。
    • 调度延迟是从停止一个进程到启动另一个进程所需的时间量称为调度延迟。保持调度延迟尽可能低的最有效技术是,提供抢占式内核。
  • 单调速率调度:采用抢占式、静态优先级的策略调度周期性任务。周期越短,优先级越高;周期越长,优先级越低。
  • 最早截止期限优先调度:根据截止期限动态分配优先级。期限越早,优先级越高。
  • POSIX实时调度
    • SCHED_FIFO:先来先服务策略来调度线程。不过,具有同等优先级的线程之间没有分时,因此,位于FIFO队列前面的最高优先级的实时线程,在得到CPU后,会一直占有,直到它终止或阻塞。
    • SCHED_RR:使用轮询策略,类似于SCHED_FIFO,但是它提供了在同等优先级的线程之间进行分时
  • Linux调度算法:默认为完全公平调度CFS,也实现了实时调度
    • CFS每个任务分配一定比例的CPU处理时间。每个任务分配的具体比例根据友好值计算,友好值范围[-20,19],每个任务通过维护虚拟运行时间vruntime,来记录每个任务运行多久。虚拟运行时间与基于任务优先级的衰减因子有关:更低优先级的任务比更高优先级的任务具有更高的衰减速率。
    • linux优先级采用两个单独的范围[0,99]用于实时任务,[100,139]用于正常任务,对应nice值[-20,19]

第六章 同步

  • 协作进程能与系统内的其他执行进程相互影响。协作进程或直接共享逻辑代码空间,或能够通过文件或消息来共享数据。
  • 竟态条件:多个进程并发访问和操作同一数据并且执行结果与特定访问顺序有关。
  • 每一个进程有一段代码,称为临界区,进程在执行该区时可能修改公共变量、更新一个表、写一个文件等。
  • 单处理器环境,临界区问题可以简单执行:在修改共享变量时只要禁止中断出现。这样就能确保当前指令流可以有序执行,且不会被抢占。由于不可能执行其他指令,所以共享变量不会意外地修改。这种方法往往为非抢占式内核所采用。
  • 多处理器环境:中断禁止会很耗时,因为消息要传递到所有处理器。因此,许多现代系统提供特殊硬件指令,用于检测和修改字的内容,或者用于原子地交换两个字(作为不可中断指令);
  • 互斥锁:
  • 信号量:当一个进程执行wait()并且发现信号量值不为正时,它必须等待。然而,该进程不是忙等待而是阻塞自己。阻塞操作将一个进程放到与信号量相关的等待队列中,并且将该进程状态切换成等待状态。然后,控制转到CPU调度程序,以便选择执行另一个进程。等待信号量S而阻塞的进程,在其他进程执行操作signal()后,应被重新执行。
    • 对于信号量的操作是需要原子执行的,在单处理器上,可以通过禁止中断,对于多处理器,采用系统提供的原子加锁指令。
  • 优先级反转:如果一个较高优先级的进程需要读取或修改内核数据,而且这个内核数据当前正被较低优先级的进程访问(这种串联方式可涉及更多进程),那么就会出现一个调度挑战。由于内核数据通常是用锁保护的,较高优先级的进程将不得不等待较低优先级的进程用完资源。如果较低优先级的进程被较高优先级的进程抢占,那么情况变得更加复杂。
  • 管程(monitor):管程类型属于ADT类型,提供有由程序员定义的、在管程内互斥的操作。
    • 只有管程内定义的函数才能访问管程内的局部声明的变量和形式参数。
    • 管程结构确保每次只有一个进程在管程内处于活动状态。

第七章 死锁

  • 当死锁时,进程永远不能完成,系统资源被阻碍使用,以至于阻止了其他作业开始执行
  • 死锁产生的必要条件:
    • 互斥:至少有一个资源必须处于非共享模式,即一次只能有一个进程占用
    • 占有并等待:一个进程占有至少一个资源,并等待另一个资源,而该资源被其他进程所占有
    • 非抢占:资源不能被抢占,即只能在完成任务后自愿释放
    • 循环等待:资源等待形成环路。
  • 满足上述四个条件也不一定出现死锁,如果资源存在多个实例,即使存在循环等待,也不一定死锁。
  • 死锁处理方法
    • 通过协议来预防或避免死锁,确保系统不会进入死锁状态
    • 可以允许系统进入死锁状态,然后检测它,并加以恢复
    • 可以忽视这个问题,认为死锁不可能在系统内发生。这为大多数操作系统所使用的,包括Linux和Windows。因此,应用开发人员需要自己编写程序,以便处理死锁。
  • 死锁预防:确保发生死锁的四个必要条件有一个不成立,就能预防死锁发生。缺点:设备使用率低和系统吞吐量低。
    • 破坏互斥:资源共享
    • 破坏持有并等待:确保每个进程申请一个资源时,它不能占有其他资源
    • 破坏无抢占:如果一个进程持有一个资源并申请另一个不能立即分配的资源,那么它现在分配的资源都可被抢占
    • 破坏循环等待:对所有资源进行完全排序,而且要求进程按递增顺序申请资源
  • 死锁避免:
    • 安全状态:当进程分配后系统仍然处于安全状态才允许分配。(如果系统按照一定顺序为每个进程分配资源,不超过它的最大需求,仍然避免死锁,那么系统的状态就是安全的)
    • 银行家算法:当一个进程进入系统时,它应声明需要的每种资源实例的最大数量,这一数量不能超过系统资源的总和。
  • 死锁检测:
  • 死锁恢复
    • 进程终止
      • 中止所有死锁进程
      • 一次中止一个进程,直到消除死锁循环为止
    • 资源抢占:不断抢占一些进程的资源给其他进程,直到死锁循环被打破为止

第八章 内存管理策略

  • 通过一组进程共享cpu。通过CPU调度,我们可以提高CPU的利用率和计算机响应用户的速度。然而,为了实现性能的改进,应该将多个进程保存在内存中;也就是说,必须共享内存
  • 内存由一个很大的字节数组来组成,每个字节都有各自的地址。CPU根据程序计数器的值从内存中提取指令,这些指令可能引起对特定内存地址的额外加载与存储。
  • CPU可以直接访问的通用存储只有内存和处理器内置的寄存器
  • CPU生成的地址通常称为逻辑地址,而内存单元看到的地址(即加载到内存地址寄存器的地址)通常称为物理地址。
  • 从虚拟地址到物理地址的运行映射是由内存管理单元(Memory Management Unit,MMU)的硬件设备来完成的。
  • 动态加载:一个程序只有在调用时才会加载。所有程序都以可重定位加载格式保存在磁盘上。
  • 动态链接库为系统库,可链接到用户程序,以便运行。有的操作系统只支持静态链接,它的系统库与其他目标模块一样,通过加载程序,被合并到二进制程序映像。
  • 有动态链接的程序,在二进制映像里,每个库程序的引用都有一个存根stub。如果执行存根时,他首先检查所需程序是否已经在内存中,不在,才加载到内存
  • 共享库:动态链接用于库的更新,加载多个版本到内存,由程序来决定用哪个版本
  • 与动态加载不同,动态链接通常需要操作系统的帮助。如果内存中的进程是彼此保护的,那么只有操作系统才可以检查所需程序是否在某个进程的内存空间内,或是允许多个进程访问同样的内存地址。
  • 交换:进程必须在内存中以便执行。不过,进程可以暂时从内存交换(swap)到备份存储(backing store),当再次执行时再调回到内存中。交换有可能让所有进程的总的物理空间超过真实系统的物理空间地址,从而增加了系统的多到程序调度。
    • 交换变种:正常情况,禁止交换;当空闲内存低于阈值,启动交换。空闲内存增加,就停止交换。另一种是交换进程的部分,以减少交换时间。
    • 移动系统:ios和安卓不支持交换,
  • 连续内存分配:
    • 内存保护:重定位寄存器含有最小的物理地址,界限寄存器含有逻辑地址的范围值。
    • 动态存储分配:根据一组空闲的孔来分配大小为n的请求
      • 首次适应:分配首个足够大的孔
      • 最优适应:分配最小的足够大的孔
      • 最差适应:分配最大的孔
    • 碎片:随着进程的加载和退出,空闲内存被分为小的片段,这就出现了外部碎片问题。进程请求内存可能比所需内存要大,这是内部碎片问题。
      • 外部碎片问题解决办法:
        • 紧缩,移动内存内容,但是对于重定位是静态的,不可使用。只有重定位是动态的,并且在运行时进程的,才可以。
        • 允许进程的逻辑空间是不连续的,这样,只有物理内存可用,就允许为进程分配内存。有两种互补的技术可以实现这个解决方案:分段和分页
  • 分段 分段硬件
    • 分段(segmentation)就是支持这种用户视图的内存管理方案。逻辑地址空间是由一组段构成。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称和段偏移
    • 我们定义一个实现方式,以便映射用户定义的二维地址到一维物理地址,这个地址是通过段表(segment table)来实现的。段表的每个条目都有段基地址(segment base)和段界限(segment limiy)。段基地址包含该段在内存中的开始物理地址,而界限指定该段的长度。
    • 分段允许进程的物理地址空间是非连续的。
  • 分页避免了外部碎片和紧缩,而分段不可以。分页也避免了将不同大小的内存匹配到交换空间的麻烦问题。
  • 分页:实现分页的基本方法涉及将物理内存分为固定大小的块,称为帧或页帧(frame);而将逻辑内存也分为同样大小的块,称为页或页面(page)。
    • 分页的硬件支持:由CPU生成的每个地址分为两部分:页码(page number)和页偏移。页码作为页表的索引
    • 分页允许我们使用的物理内存大于CPU地址指针可访问的空间。但是一个进程只能访问CPU指针可访问的空间。如32位系统,4KB页,每一个进程可访问虚拟内存232字节(4GB),系统可访问物理内存为244(16TB)
    • 由于操作系统管理物理内存,它应该直到物理内存的分配细节:那些帧已分配,那些帧空着,总共多少帧,等等。这些信息保存在称为帧表的数据结构中。在帧表中,每个条目对应这一个帧,以表示该帧是空闲还是已占用;如果占用,是被那个(或那些)进程所占用。
    • 操作系统为每个进程维护一个页表的副本,就如同它需要维护指令计数器和寄存器的内容一样。每当操作系统自己将逻辑地址映射成物理地址时,这个副本可用作交换。当一个进程可分配到CPU时,CPU分派器也根据该副本来定义硬件页表。因此,分页增加了上下文切换的时间。
    • 页表条目包含物理地址基址
    • 页表过大,将页表存在内存,并将页表基地址寄存器指向页表。改变页表只需要改变此寄存器即可
    • TLB加速。
    • 分页环境下的内存保护是通过与每个帧关联的保护位来实现的。通常这些位保存在页表中。用一个位可以定义一个页是可读可写或只可读
    • 分页的优点之一是可以共享公共代码。
    • getconf PAGESIZE
  • 页表结构:
    • 分层分页;2-4层
    • 哈希页表:虚拟地址的虚拟页码哈希到哈希表,用虚拟页码与链表内的第一个元素的第一个字段相比较,如果匹配,那么相应的帧码(第二个字段)就用来形成物理地址,如不匹配,那么与后续节点的第一个字段继续比较。
    • 倒置页表
      • 反向页表:顾名思义,以页帧号为index,页号地址为value,每次访问将value和逻辑地址比对,这样做的原因就是大大节省了内存的开销,全局只需要一张页表,但是当物理内存特别大的时候,这个表也就很大了,访问一个地址可能需要遍历整个表(因为是按照物理地址建立的,所以要挨个访问、判断),那么有什么方法是可以缓减访问速度的压力吗,那就是基于hash表的访问
      • 来看两张图片(引用自网络)
      • 首先是不用hash表的反向页表
      • 很明显,由于访问查找的比对是根据页号,而index是页帧号(ppn),所以只能从第一个页帧号开始,一个一个做比对(PID是当前进程的标识),速度较慢
      • 再来看看用了hash表的反向页表
      • hash表的作用:
      • 将页号即图中的vpn做一个hash计算(使用硬件加速)找到对应的hash index,hash index的值0x0是一个页帧号,也就是反向页表的index,但是这样可能会存在hash冲突,因此在传参的时候需要传入当前进程的PID作为标识,以确保找到对应的页帧号,NEXT中存放的是由于hash冲突导致的相同hash值的下一个条目的index(页帧号),这样就不需要一个一个进行比对了。如图,0x1经过计算得到0x0,因此去访问反向页表的index为0x0这个条目,比对之后发现PID不对,因此访问NEXT,找到相同hash值的下一个条目,找到之后,index+offset即为物理访问地址
  • IA-32架构分页支持PAE扩展,可使32位机器访问64GB物理内存。

第九章 虚拟内存管理

  • 虚拟内存技术允许执行进程不完全处于内存。这种方案的一个主要优点就是,程序可以大于物理内存。此外,虚拟内存将内存抽象成一个巨大的、统一的存储数组,进而实现了用户看到的逻辑内存和物理内存的分离。这种技术使得程序员不用再大有内存容量的限制。虚拟内存还允许进程轻松共享文件和实现共享内存。此外,他为创建进程提供了有效地机制。
  • 虚拟内存将用户逻辑内存和物理内存分开
  • 进程的虚拟地址空间就是进程如何在内存中存放的逻辑(或虚拟)视图。
  • 请求调页:仅在需要时才加载页面。
    • 通过缺页错误加载需要的程序页面到内存。
    • 请求调页的关键要求是在缺页错误之后重新启动任何指令的能力。
  • 缺页错误的处理时间主要为3部分
    • 处理缺页错误中断
    • 读入页面
    • 重新启动进程
  • 缺页率:有效访问时间 = (1-p)* ma + p*缺页错误时间
    • p:缺页率
    • ma:内存访问时间
    • 一般 ma=10~200ns 缺页错误时间:8ms
    • 取ma=200ns 缺页错误时间为8ms 如果只要求性能下降10%,那么缺页率为0.0000025,
    • 即只能允许每399990次访问中有一次缺页错误
  • s(秒)、ms(毫秒)、μs(微秒)、ns(纳秒),其中:1s=1000ms,1 ms=1000μs,1μs=1000ns
  • 交换空间的磁盘I/O通常要快于文件系统的。交换空间的文件系统更快,因为它是按更大的块来分配的,且不采用文件查找和间接分配方法。因此,系统可以在进程启动时将整个文件映射复制到交换空间中,然后从交换空间执行请求调页,从而获得更好的分页吞吐量。另一种选择是,开始时从文件系统进行请求调页,但是在置换页面时则将页面写入交换空间。这种方法确保,只从文件系统读取所需的页面,而所有后续调页都是从交换空间完成的。
  • 写时复制:系统调用fork()创建了父进程的一个一个复制,以作为子进程。传统上,fork()为子进程创建一个父进程地址空间的副本,复制属于父进程的页面。然而,考虑到许多子进程在创建之后立即调用系统调用exec(),父进程地址空间的复制可能没有必要。因此,可以采用一种称为写时复制(copy-on-write)的技术,它允许父进程和子进程最初共享相同的页面来工作。这些共享页面标记为写时复制,这意味着如果任何一个进程写入共享页面,那么就创建共享页面的副本。
  • 注意只有可以修改的页面才需要标记为写时复制
  • 页面置换:页面置换是请求调页的基础,它完成了逻辑内存和物理内存之间的分离。采用这种机制,较小的物理内存能为程序员提供巨大的虚拟内存。
  • 置换算法:
    • FIFO:置换最先创建的页面
    • OPT:最优页面置换算法,置换最长时间不会使用的页面
    • LRU:最近最少使用算法,置换最长时间没有使用的页面
    • LFU:最不经常使用
    • MFU:最经常使用
  • 系统抖动:如果一个进程的调页时间多于它的执行时间,那么这个进程就在抖动。页面置换分为全局和局部,全局是从所有进程中选择置换,局部是从它分配的帧中选择置换,当发生抖动时,那么采用局部置换,这样限制抖动只发生在该进程。
  • 工作集模型:基于局部性假设。把系统最近引用的页面称为一个工作集。
  • 抖动及其导致的交换对性能的负面影响很大。目前处理这一问题的最佳实践是,在可能的情况下提供足够物理内存以避免抖动和交换。
  • 内存映射文件:
    • 假设采用标准系统调用open()、read()、和write()来顺序读取磁盘文件。每个文件访问都需要系统调用和磁盘访问。或者采用所讨论的虚拟内存技术,以将文件I/O作为常规内存访问。这种方法称为内存映射(memory mapping)文件,允许一部分虚拟内存与文件进行逻辑关联。正如我们将会看到的,这可能导致显著的性能提高。
    • 实现文件的内存映射是,将每个磁盘块映射到一个或多个内存页面。最初,文件访问按普通请求调页来进行,从而产生缺页错误。这样,文件的页面大小部分从文件系统读取到物理页面(有些系统可以一次读取多个页面大小的数据块)。以后,文件的读写就按常规内存访问来处理。通过内存的文件操作,没有采用系统调用read()和write()的开销,而且简化了文件的访问和使用。
    • 请注意,内存映射文件的写入不一定是对磁盘文件的即时(同步)写入。有的操作系统定期检查文件的内存映射页面是否已被修改,以便选择是否更新到物理文件。当关闭文件时,所有内存映射的数据会写到磁盘,并从进程的虚拟内存中删除。
    • 很多时候,共享内存实际上是通过内存映射来实现的。在这种情况下,进程可以通过共享内存来通信,而共享内存是通过映射同样文件到通信进程的虚拟地址空间来实现的。内存映射文件充当通信进程之间的共享内存区域。
    • 内存映射I/O:一组内存地址专门映射到设备寄存器。对这些内存地址的读取和写入,导致数据传到或取自设备寄存器。
  • 分配内核内存:
    • 伙伴系统:从连续的大小固定的段进行分配,分配的块都是2的幂大小。
    • slab分配:每个slab由一个或多个物理连续的页面组成。每个cache由一个或多个slab组成。每个内核数据结构都有一个cache
  • I/O联锁与页面锁定:
    • 当对用户内存进行I/O操作时,需要锁定
    • 要么不允许用户内存I/O,只允许系统内存I/O,然后系统内存复制到用户内存

第十章 文件系统

  • 对于大多数用户来说,文件系统是操作系统最明显的部分。它提供机制,以便对计算机操作系统与所有用户的数据与程序进行在线存储和访问。文件系统由两个不同的部分组成:文件集合,每个文件存储相关数据;目录结构,用于组织系统内的所有文件并提供信息。
  • 文件(file):操作系统对存储设备的物理属性加以抽象,从而定义的逻辑存储单位。
    • 文件是由操作系统映射到物理设备上。
    • 文件是记录在外存上的相关信息的命名集合。
  • 文件属性:
    • 名称:符号文件名是以人类可读形式来保存的唯一信息。
    • 标识符:这种唯一标记(通常是数字)标识文件系统的文件;它是文件的非人类可读的名称
    • 类型:支持不同类型文件的系统需要这种信息
    • 位置
    • 尺寸
    • 保护
    • 时间、日期和用户标识:
  • 文件操作:
    • 创建文件:创建文件需要两个步骤。首先,必须在文件系统中为文件找到空间。第二,必须在目录中创建新文件的条目。
    • 写文件:为了写文件,使用一个操作系统调用指定文件名称和要写入文件的信息。根据给定的文件名称,系统搜索目录以查找文件位置。系统应保留写指针(write pointer),用于指向需要下次写操作的文件位置。每当发生写操作时,写指针必须被更新。
    • 读文件:为了读文件,使用一个系统调用,指明文件名称和需要文件的下一个块应该放在那里(在内存中)。同样,搜索目录以找到相关条目,系统需要保留一个读指针(read pointer),指向要进行下一次读取操作的文件位置。一旦发生读取,读指针必须被更新。因为进程通常从文件读取或写到文件,所以当前操作位置可以作为进程的当前文件位置指针(current-file-position pointer)。读和写操作都是用相同的指针,可以节省空间并降低系统复杂度。
    • 重新定位文件:搜索目录以寻找适当的条目,并且将当前文件位置指针重新定位到给定值。重新定位文件不需要涉及任何实际的I/O。这个文件操作也称为文件定位(file seek)。
    • 删除文件:为了删除文件,在目录中搜索给定名称的文件。找到关联的目录条目后,释放所有文件空间,以便它可以被其他文件重复使用,并删除目录条目
    • 截断文件:用户可能想要删除文件的内容,并保留它的属性。不是强制用户删除文件再创建文件,这个功能允许所有属性保持不变,(除了文件长度),但让文件重置为零,并释放它的文件空间。
  • 通常情况下,操作系统采用两级的内部表:
    • 每个进程表:跟踪它打开的所有文件。该表所存的是进程对文件的使用信息。
    • 整个系统表:包含与进程无关的信息,如文件在磁盘上的位置、访问时间和文件大小。单个进程表的每个条目相应的指向整个系统的打开文件表
  • 每个打开的文件关联如下信息:
    • 文件指针:对于没有将文件偏移作为系统调用read()和write()参数的系统,系统必须跟踪上次读写位置,以作为当前文件位置指针。该指针对操作文件的每个进程是唯一的,因此必须与磁盘文件属性分开保存。
    • 文件打开计数:当文件关闭时,操作系统必须重新使用它的打开文件表的条目,否则表的空间会不够用。多个进程同时打开同一文件,这样在删除它的打开文件表条目之前,系统必须等待最后一个进程关闭这个文件。文件打开计数跟踪打开和关闭的次数,在最后关闭时为0。然后,系统可以删除这个条目。
    • 文件磁盘位置:大多数操作系统要求系统修改文件数据。查找磁盘上的文件所需的信息保存在内存中,以便系统不必为每个操作从磁盘上读取该信息。
    • 访问权限:每个进程采用访问模式打开文件。这种信息保存在进程的打开文件表中,因此操作系统可以允许或拒绝后续的I/O请求。
  • 文件锁(file lock)允许一个进程锁定文件,以防止其他进程访问他。文件锁对于多个进程共享的文件很有用。
  • 访问方法:
    • 顺序访问:文件信息按顺序(一个记录一个记录地)加以处理
    • 直接访问(相对访问):文件由固定长度的逻辑记录(logical records)组成,以允许程序按任意顺序进行快速读取和写入记录。直接访问方法基于文件的磁盘模型,因为磁盘允许对任何文件块的随机访问。对于直接访问,文件可作为块或记录的编号序列。对于大量信息的立即访问,直接访问文件极为有用。数据库通常是这种类型的。当需要查询特定主题时,首先计算那个块包含答案,然后直接读取相应块以提供期望的信息。
  • 文件共享:链接
    • 软链接:创建一个指针,指向另一个文件或子目录的指针。UNIX符号链接。文件删除,符号链接不会被删除。
    • 硬链接:在文件信息块(inode)中有引用计数,当发生删除文件时,只有所有引用为0,才可以删除文件。

第十一章 文件系统实现

  • 文件系统提供了机制,以在线存储和访问文件内容,包括数据和程序。文件系统永久驻存在外存上,而外存设计成永久容纳大量数据。
  • 磁盘提供大多数的外存,以便维护文件系统。为了实现这个目的,磁盘具有两大优势:
    • 磁盘可以原地重写;可以从磁盘上读取一块,修改该块,并写回到原来的位置
    • 磁盘可以直接访问它包含信息的任何块,因此,可以简单按顺序或随机地访问文件;
    • 并且从一个文件切换到另一个文件只需移动读写磁头,并且等待磁盘旋转。
  • 为了提高I/O效率,内存和磁盘之间的I/O传输以块(block)为单位执行。每个块具有一个或多个扇区。扇区通常为512字节
  • 文件系统提供高效和便捷的磁盘访问,以便允许轻松存储、定位、提取数据:
  • 文件系统分层:【下到上】
    • I/O控制:包括设备驱动程序和中断处理程序,以在主内存和磁盘系统之间传输信息。
    • 基本文件系统:只需向适当设备驱动程序发送通用命令,以读取和写入磁盘的物理块。也管理内存缓冲区和保存各种文件系统、目录和数据块的缓存。
    • 文件组织模块(file-organization module)知道文件及其逻辑块以及物理块,可以将逻辑地址转换为物理地址
    • 逻辑文件系统(logical file system)管理元数据信息。元数据包括文件系统的所有结构,而不包括实际数据(或文件内容)。逻辑文件系统管理目录结构,以便根据给定文件名称和为文件组织模块提供所需信息。他通过文件控制块来维护文件结构。文件控制块(File Control Block,FCB)包含有关文件信息,包括所有者、权限、文件内容的位置等
  • 虚拟文件系统VFS
    • 定义通用的VFS接口,将文件系统的通用操作和实现分开
    • 它提供了一种机制,以唯一表示网络上的文件。VFS基于V节点(vnode)的文件表示结构,它包含一个数字指示符以唯一表示网络上的一个文件。
  • Linux VFS对象
    • 索引节点对象(inode):保存一个文件的元数据
    • 文件对象(file):一组逻辑上相关联的数据,被一个进程打开并关联使用
    • 超级块对象(superblock):存储整个文件系统的元数据
    • 目录条目对象(dentry):保存了文件(目录)名称和具体的inode的对应关系,也实现了目录和文件之间的映射关系。
  • 目录实现:
    • 线性列表:采用文件名称和数据块指针的线性列表。
    • 哈希表:用于文件目录的另一个数据结构是哈希表:
  • 磁盘空间分配方法:
    • 连续分配:每个文件在磁盘上占有一组连续的块。磁盘地址为磁盘定义了一个线性排序。缺点:【外部碎片;大小提前声明】
    • 链接分配:每个文件是磁盘块的链表。磁盘块可能会散步在磁盘的任何地方。目录包括文件第一块和最后一块的指针。缺点:【只能有效用于顺序访问文件;指针需要空间;可靠性】
    • 索引分配:将所有指针放在一起,即索引块解决了问题。索引块的第i条目指向文件的第i块。
  • 索引块分配:
    • 链接方案:链表链接多个索引块
    • 多级索引:一级索引块指向二级索引块
    • 组合索引:直接块,一级间接块,二级间接块,三级间接块。参考UNIX
  • 空闲空间管理:为了跟踪空闲磁盘空间,系统需要维护一个空闲空间列表(free-space list)。空闲空间列表记录了所有空闲磁盘空间,,即未分配给文件或目录的空间。
  • 统一缓冲区缓存
  • NFS:无状态,每一个请求必须提供整套参数,包括唯一文件标识符和用于特定操作的文件内绝对偏移。
  • 磁盘是计算机主要部件中最慢的,磁盘往往成为系统性能的主要瓶颈。

第十二章 大容量存储结构

  • 硬盘:盘片、柱面、磁道、扇区
  • 寻道时间:磁盘臂移动磁头到包含目标扇区的柱面的时间。
  • 旋转延迟:磁盘旋转目标扇区到磁头下的额外时间。
  • 磁盘带宽:传输字节的总数除以从服务请求开始到最后传递结束时的总时间。
  • 低级格式化:为每个扇区使用特殊的数据结构,填充磁盘。
  • 分区:将磁盘分为由柱面组成的多个分区,操作系统可以将每个分区作为一个单独磁盘。
  • 逻辑格式化:创建文件系统
  • 操作系统管理磁盘块。首先,必须低级格式化磁盘,以便在原始硬件上创建扇区,新磁盘通常是预先格式化的。然后,对磁盘分区,创建文件系统,并分配启动块以存储系统的引导程序。最后,当有一个块损坏时,系统必须有一个方法来锁定它,或者用另一备份从逻辑上替代它。
  • 磁盘调度:电梯调度算法
  • 对于SSD,没有移动磁头,通常采用简单的FCFS(先来先服务)策略。Linux调度程序Noop使用FCFS策略,并且通过修改以便合并相邻请求
  • 为了提高效率,大多数操作系统将块组成在一起变成更大的块。经常称为簇,磁盘I/O按块完成,而文件系统I/O按簇完成,有效确保了I/O具有更多的顺序访问和更少的随机访问的特点。

第十三章 I/O系统

  • 计算机的操作系统I/O功能是:管理和控制I/O操作和I/O设备
  • 设备驱动程序(device driver)为I/O子系统提供了统一的设备访问接口,就像系统调用为应用程序与操作系统之间提供了标准接口。
  • 总线(bus)是一组线路和通过线路传输信息的严格定义的一个协议
  • 控制器是可以操作端口、总线或设备的一组电子器件。
  • 处理器如何对控制器发出命令和数据以便完成I/O传输?
    • I/O指令:控制器具有一个或多个寄存器,用于数据和控制信号。处理器通过读写这些寄存器的位模式来与控制器通信。
    • 内存映射I/O:设备控制寄存器被映射到处理器的地址空间。处理器执行I/O请求是通过标准数据传输指令读写映射到物理内存的设备控制器。
  • I/O操作:轮询和中断
  • 中断:设备控制器通过中断请求线发送信号而引起中断,CPU捕获中断并且分派到中断处理程序,中断处理程序通过处理设备来清除中断。
  • 现代操作系统通过中断处理异步事件,并且陷阱进入内核的管理态程序。
  • 调度I/O操作是I/O子系统提高计算机效率的一种方法。
  • 缓冲:缓冲区是一块内存区域,用于保存在两个设备之间或在设备和应用程序之间传输的数据。
    • 1.处理数据流的生产者与消费者之间速度不匹配。
    • 2.协调传输大小不一的数据的设备。
    • 3.支持应用程序I/O的复制语义。操作系统保证复制语义的一种简单方式:系统调用write【应用程序缓冲区写入】在返回到应用程序之前,复制应用程序缓冲区到内核缓冲区。磁盘写入通过内核缓冲区来执行,以便应用程序缓冲区的后续更改没有影响。
  • 为了改善I/O效率,可以采用多种方法:
    • 减少上下文切换的次数
    • 减少设备和应用程序之间传递数据时内存数据的复制次数。
    • 通过大传输、智能控制器、轮询(如果忙等可以最小化),减少中断频率。
    • 通过DMA智能控制器和通道为主CPU承担简单数据复制,增加并发。
    • 将处理原语移到硬件,允许控制操作与CPU和总线操作并发。
    • 平衡CPU、内存子系统、总线和I/O的性能,因为任何一处的过载都会引起其他部分的空闲。

第十四章 系统保护

  • 保护作为一种机制,用于控制程序、进程和用户访问计算机系统定义的资源。
  • 机制和策略不同,机制决定如何做,策略决定做什么
  • 一个经过时间检验的重要保护指导原则是最低特权原则,它规定了程序、用户甚至系统只能拥有足够特权以便执行任务
  • 每个计算机系统是进程和对象的一组集合。
  • 对象:
    • 硬件对象:cpu 内存段 打印机 磁盘等
    • 软件对象:文件 程序 信号量等
  • 进程只应访问获得授权的资源。

第十五章 系统安全

第十六章 Linux系统

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《操作系统概念:第九版》是由Abraham Silberschatz、Peter B. Galvin和Greg Gagne合著的一本计算机科学领域的经典教材。本书系统地介绍了操作系统的基本概念、原理和设计方法,深入讨论了多道程序、处理器调度、内存管理、文件系统等重要主题。 该书可以分为七个主要部分。第一部分介绍了操作系统的概述,包括操作系统的作用、功能和发展历史。第二部分探讨了进程管理,包括进程的创建、调度、同步和通信。第三部分涵盖了内存管理,包括分区管理、页面置换算法和虚拟内存等内容。 第四部分介绍了文件系统,包括文件和目录的组织、磁盘调度以及文件系统的实现。第五部分讨论了输入/输出系统,包括驱动程序、设备管理和磁盘存储等。第六部分介绍了分布式系统和并行操作系统,包括网络和分布式文件系统。最后一部分讨论了安全性和保护机制,包括访问控制和安全策略等。 该书采用了清晰的语言和生动的例子,结合了最新的研究成果和实际案例,使读者更容易理解和应用操作系统概念。此外,书中还包含了大量的习题和实践项目,帮助读者加深理解,并将所学知识应用到实际问题中。 《操作系统概念:第九版》是一本全面且权威的操作系统教材。它不仅适用于计算机科学专业的学生,也适用于从业人员和对操作系统感兴趣的读者。无论是在理论研究还是在实践应用方面,这本教材都是值得一读的重要参考书。 ### 回答2: 《操作系统概念(abraham silberschatz pdf)第九版》是一本面向计算机科学专业学生和专业人士的经典教材。本书对操作系统概念和原理进行了详细而全面的介绍。 首先,本书从操作系统的角度定义了计算机系统,并解释了它的组成部分。书中详细讨论了进程管理、内存管理、文件系统、输入输出管理等关键概念,并提供了相应的算法和实例。此外,本书还介绍了多处理器系统、并行计算和分布式系统等现代操作系统的新兴技术。 此外,本书还对操作系统的设计与实现进行了探讨。作者详细解释了操作系统的内核设计和模块化,提供了实践项目和实验手册,帮助读者深入理解操作系统的实际运作。并且,本书还对操作系统的安全性和保护机制进行了介绍,让读者了解如何保护计算机系统免受恶意软件和非法访问的侵害。 此外,本书在自愿顺序和并发性方面提供了许多实例和案例研究。它详细介绍了进程同步、进程互斥、死锁的原因和解决方法,让读者有系统地了解并理解操作系统的关键概念。 综上所述,《操作系统概念(abraham silberschatz pdf)第九版》是一本经典的操作系统教材,对操作系统概念和原理进行了全面而深入的介绍。无论是对于计算机科学专业学生还是专业人士,都是一本值得阅读和参考的书籍。 ### 回答3: 《操作系统概念(第九版)》是Abraham Silberschatz等人合著的经典教材。本书详细介绍了操作系统概念、原理、设计和实现等方面的知识。 首先,操作系统是计算机系统中最基础的软件之一,它负责管理计算机的硬件和软件资源,为用户和应用程序提供一个友好且高效的运行环境。本书从操作系统的基本概念入手,探讨了各种操作系统的常见功能和特性,例如进程管理、内存管理、文件系统等。 其次,在介绍操作系统的各个组成部分时,本书还深入讨论了各种操作系统的设计和实现原理,包括进程调度算法、内存管理策略、磁盘调度算法等等。读者可以通过学习这些原理,了解操作系统是如何高效地管理计算机资源的。 此外,本书还涵盖了一些当前热门的操作系统相关技术和概念,如多核处理器、虚拟化技术、云计算等。这些内容让读者能够紧跟操作系统领域的最新发展,并理解这些技术对操作系统设计和实现带来的挑战与机遇。 《操作系统概念(第九版)》以其全面、系统的内容、清晰易懂的语言和详细的例子,深受师生们的喜爱。无论是作为本科生的教材还是研究生的参考书,它都是一本不可或缺的操作系统经典教材。通过学习这本书,读者可以全面掌握操作系统的基本概念和原理,提升计算机科学领域的专业技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值