【OS】新国立nus操作系统知识点(中文版)

news/2024/11/16 12:43:28/

文章目录

  • 1. Introduction to OS
    • 本章涉及
    • 1.1 什么是操作系统?
    • 1.2 为什么我们需要操作系统?
      • 抽象 Abstraction
      • 控制程序
      • Summary
    • 1.3 现代操作系统分类
    • 1.4 操作系统结构
    • OS结构
    • OS是一个程序
    • OS的实现
      • 单片OS `Monolithic OS`
      • 微核OS `Microkernel`
    • 虚拟机 `Virtual Machines`
  • 2. 进程抽象 `Process Abstraction`
    • 2.1 进程管理
      • 回忆:C程序和编译代码
      • 回忆:程序执行——内存
      • 回忆: 通用计算机架构
      • 回忆:组件介绍
      • 回忆:基本指令执行
      • 回忆:你需要知道的
    • 2.2 内存层面 Memory Context:函数调用 Function Call
      • 2.2.1 函数调用 Function Call
        • Function Call: Challenges
        • Function Call:控制流和数据 Control Flow and Data
      • 2.2.2 栈内存 `Stack Memory`
    • 2.2.3 Illustration: Stack Frame v1.0
      • 函数调用约定`function call convention`
        • 1. 准备进行一个函数调用:
        • 2. 调用函数g()
        • 3. 结束函数调用,回到调用者
        • 帧指针 `frame pointer`
        • 保存的寄存器 `saved register`
    • 2.2.4 Illustration: Stack Frame v2.0
    • 注意,这只是个例子。
      • 函数调用小结
    • 2.3 内存层面 Memory Context:动态分配内存 Dynamically Allocated Memory
      • 堆内存 `Heap memory`
    • 2.4 OS context:进程ID和进程状态 ProcessID & Process State
    • 2.5 进程表和进程控制块 Process Table & Process Control Block
      • 进程表
    • 2.6 进程和OS之间交互:系统调用 `System Calls`
    • 2.7 进程和OS之间交互:Exception and Interrupt
      • Exception
      • Interrupt
      • Exception / Interrupt Handler
      • Summary
  • 3. 进程调度 `Process Scheduling`
    • 3.1 并发执行 `concurrent execution`
      • 简化的并发示例
    • 3.2 交错执行 Interleaved Execution:context switch
      • 多任务OS
    • 3.3 调度 Scheduling
      • 进程行为 Process Behavior
      • 处理环境 Processing Environment
      • 调度算法的评判标准 Criteria for Scheduling Algorithm
      • 什么时候执行调度?
      • 调度步骤
    • 3.4 批处理调度 `Scheduling Fro Batch Processing`
      • (1)先到先得 First-Come-First-Served
      • (2)短任务优先 Shortest-Job-First SJF
      • (3)最短剩余时间 Shortest Remaining Time SRT
    • 3.5 交互式系统的调度 Scheduling For Interactive System
      • 交互式环境的评判标准
      • 确保周期性调度
        • Timer & Time Quantum
      • 调度算法
      • (1)循环赛 Round Robin (RR)
      • (2)优先调度 Priority Scheduling
      • (3)多级反馈队列 Multi-Level Feedback Queue (MLFQ)
        • 规则
        • 例子1
        • 例子2: 例子1的基础上 + 中间有时候会出现短任务
        • 例子3
      • (4)彩票调度 Lottery Scheduling
        • 性质
      • Summary
  • 4. 线程 Threads
    • 4.1 Motivation for Thread
    • 4.2 线程 Thread of Control
      • 使用fork()创建多线程
      • 使用Thread..do多线程
      • Process and Thread
        • 进程context交换 vs 线程交换
      • 线程优点
      • 线程的问题
    • 4.3 线程模型 Thread Models
      • 用户线程 `User Thread`
      • 内核线程 Kernel Thread
      • 混合线程模型 Hybrid Thread Model
  • 5. 进程间通信 Inter-Process Communication
    • 5.1 共享内存 `Shared-Memory`
      • POSIX Shared Memory in *nix
        • Master Program
        • Slave Program
    • 5.2 信息传递 Message Passing
      • 命名机制:直接通信 Naming Scheme: Direct Communication
      • 命名机制:间接通信 Naming Scheme: Indirect Communication
      • 两种同步行为 Two Synchronization Behaviors
      • 优缺点
    • 5.3 Unix Pipes
      • Piping in Shell
      • Unix Pipes
        • Unix Pipes:as an IPC Mechanism
        • Unix Pipes: Semantic
        • Unix Pipe:System Calls
      • 示例代码
    • 5.4 Unix Signal
  • 6. 进程管理:同步 Process Management: Synchronization
    • 并发执行的问题
    • 竞争条件 Race Condition
    • 临界区 Critical Section
    • Critical Section的实现
      • (1)汇编层面实现 Assembly Level Implementation
      • (2)高级语言层面实现 High-Level-Language Implementation
      • (3)高级抽象 High-Level Abstraction
        • Wait() and Signal()
        • 信号量例子:Critical Section
        • 互斥量的非正式证明 Mutex:Correct CS - Informal Proof
        • 不正确地使用信号量:死锁
      • (4)其他高阶抽象
    • 经典的同步问题 Classical Synchronization Problems
      • (1)Producer-Consumer
        • 生产者-消费者:忙等方式
        • 生产者-消费者:阻塞方式
      • (2)读者写者 Readers Writers
      • 哲学家进餐问题 Dining Philosophers: Specification
    • **`Tanenbaum Solution`**
    • `有限进食者 Limited Eater `
    • 同步的实现 Synchronization Implementations
      • POSIX信号量
      • pthread 互斥量和条件变量 Mutex and Conditional Variables
      • 其他
  • 7. 内存抽象 Memory Management:Memory Abstraction
    • 7.1 内存基础
      • 硬件
      • 进程的内存使用
      • 操作系统的作用
      • Key Topics
    • 7.2 内存抽象
      • 实际地址-没有内存抽象
        • 优点
        • 缺点
      • 逻辑地址
        • 改进:地址搬迁 Address Relocation
        • 改进:Base + Limit Registers
    • 7.3 连续内存分配
        • 多任务、context切换和交换 `Multitasking, Context Switching & Swapping`
      • 内存分区 `Memory Partitioning`
      • 固定大小分区
      • 可变大小分区
        • 分配算法 `Allocation Algorithm`
        • 合并和压缩 `Merging and compaction`
          • Example
        • 动态分配算法:伙伴系统 `Buddy system`
        • 伙伴系统:分配算法 Buddy System: Allocation Algorithm
        • 伙伴系统:释放算法 Deallocation Algorithm
        • 如何找到Buddy
  • 8. 内存管理:不相交内存 Disjoint Memory Schemes
    • 分页 Paging:基本思想
    • 页表:查找机制
    • 逻辑地址转换
      • 逻辑地址转换:基本技巧
      • 逻辑地址转换:公式
        • 示例:4 个逻辑页,8 个物理帧
      • 分页:观察
      • 实施分页
      • 分页机制:硬件支持
        • TLB:对内存访问时间的影响
        • TLB 和进程切换
        • 分页方案:保护
        • 分页方案:页面共享 Page Sharing
    • 分割机制 segmentation scheme
      • 细分方案:动机
      • 分割方案:基本思想
      • 分段:逻辑地址转换
      • Segementation 优缺点
    • 分页分割
      • 分页分割:基本思想
      • Summary
  • 9. 虚拟内存管理 Virtual Memory Management
    • 虚拟内存:Motivation
      • 虚拟内存:基本思想
      • 扩展分页机制
      • 访问页面 X:一般步骤
      • 虚拟内存:理由
      • 回顾:局部性原则 Locality Principles
      • 虚拟内存和局部性原则
      • 虚拟内存:总结
      • 更多
    • 页表结构 Page Table Structures
      • 页表结构
      • 页表结构:直接分页
      • 2 级分页:基本思想
      • 2 级分页:优势
      • 倒页表:基本思想
    • 页面替换算法
      • 内存引用建模
      • 页面替换算法:评价
      • 最佳页面替换 (`Optimal Page Replacement, OPT`)
      • FIFO页面替换算法
        • 先进先出:问题
      • 最近最少使用的页面替换 (Least Recently Used Page Replacement, LRU)
        • LRU:实施细节
        • Second-Chance页面替换 (CLOCK)
        • Second-Chance:实施细节
    • 帧分配
      • 帧分配和页面替换
      • 本地与全局替换
      • 帧分配和抖动 Frame Allocation and Thrashing
      • 找到正确的帧数
      • 工作集模型 Working Set Model
    • Summary

1. Introduction to OS

本章涉及

操作系统的基本概念:

  • 什么是操作系统?
  • 操作系统结构:组成部分、kernel的类型
  • 虚拟机 Virtual Machines

1.1 什么是操作系统?

简单定义:在用户和电脑硬件之间的桥梁程序。
在这里插入图片描述
操作系统历史:略

1.2 为什么我们需要操作系统?

抽象 Abstraction

  • 各种硬件配置之间有很大的差别,同一类硬件有定义清晰且通用的功能(例如硬盘用于存取数据)。
  • 操作系统是一种抽象,隐藏了低阶的细节,只展示通用的、高阶的功能给用户。用户可以通过操作系统操作各种重要任务,而不用考虑底层实现的细节。由此提供了高效率和便捷性。
  • 程序的执行需要不同的资源(CPU、内存、IO设备等)。为了充分利用资源,多个(使用不同资源)程序应该可以同时执行。OS是一个资源分配者,它管理所有的资源(CPU、内存、IO设备等),处理可能存在冲突的请求,高效和公平地使用资源。

控制程序

  • 如果没有OS,程序可能错误地使用电脑硬件,有可能是意外导致(程序bug),也有可能是有意为之(病毒、恶意程序)。
  • 不同地用户可以共享某台电脑,而【保证用户空间是相互隔离的】是一件复杂的事情。
  • OS是一种控制程序,控制不同程序的执行,防止错误/不正确地使用电脑,提供了安全保障。

Summary

  • 管理资源和协调:进程同步,资源共享
  • 简化编程:硬件抽象,服务便捷
  • 执行使用政策
  • 安全和保护
  • 用户程序可移植性:跨越不同的硬件
  • 效率:复杂的实现;针对特定用途和硬件进行了优化

1.3 现代操作系统分类

在这里插入图片描述

1.4 操作系统结构

我们已经确定了OS的主要功能,现在我们来考虑实现这些功能的最好方式。
操作系统的结构是各种组成部分的组织方式,需要考虑这几个重要因素:灵活性Flexibility、稳定性Robustness、可维护性Maintainability

OS结构

在这里插入图片描述

  • 操作系统是运行在核心模式下kernel mode的软件,拥有对所有硬件资源的操作权。
  • 其他软件运行在用户模式下user mode,拥有对硬件资源有限的/被控制的权限。

在这里插入图片描述
A:OS执行机器指令
B:执行的普通的机器指令(用户程序、库程序)
C:使用系统调用接口来调用OS
D:用户程序调用库程序
E:系统进程:提供了高阶的服务,通常部分是OS

OS是一个程序

  • OS也被叫做kernel,其实他就是有一些特殊功能的程序而已,例如:(1)处理硬件事务 (2)提供系统调用接口 (3)中断处理,设备驱动
  • kernel程序需要和普通程序有所区别
    • kernel代码里没有对系统的调用
    • 不使用普通库函数
    • 没有通常的I/O
  • 考虑这个:普通程序使用OS,那OS用什么呢?

OS的实现

程序语言:

  • 原先用的是编译语言/机器指令
  • 现在用一些高阶语言High level language, HLL:C/C++
  • 非常依赖硬件架构

常见的代码组织方式:与硬件无关的HHL,依赖硬件的HLL,依赖硬件的编译语言。
挑战:没有其他底层可以给OS依赖(没有包装好的服务),debug很困难,复杂度高,代码复杂

OS的架构方式有:单片Monolithic、微内核Microkernel、分层Layered、客户端服务器Client-Server、外核Exokernel
我们下面会细致讨论单片Monolithic、微内核Microkernel这两种模式,它们代表了所有的可能性,大多数其他方法是变体或改进。

单片OS Monolithic OS

kernel是一个很大的特殊程序,各种服务和组件是不可或缺的一部分。
软件工程原则包括模块化modularization和接口/实现分开seperation of interfaces and implementation
单片OS是多数Unix变种和Windows NT/XP的实现方式。

  • 优点
    • 容易理解
    • 性能良好
  • 缺点
    • 各组成部分高度耦合
    • 通常会演变成非常复杂的内部结构

在这里插入图片描述

微核OS Microkernel

kernel是非常小且干净的,它只提供基础的、核心的功能,如:

  • 进程间通信 Inter-Process Communication, IPC
  • 地址空间管理 Address space management
  • 线程管理 Thread Management

高阶的服务:

  • 建立在基础功能之上
  • 在OS外面作为服务器进程运行
  • 使用IPC进行通信。

优点:

  • kernel更加稳定、可扩展
  • 将kernel和高阶服务独立开来,提高保护性

缺点:

  • 低性能

在这里插入图片描述

  • 分层系统 Layered Systems
    • 单体系统的泛化 Generalization of monolithic system
    • 将组件组织成层的层次结构 :上层调用下层,最低层是硬件,最高层是用户界面
  • 客户端-服务器模型 Client-Server Model
    • 微内核模式的变种
    • 两类进程:
      • 客户端进程从服务器进程请求服务
      • 建立在微内核之上的服务器进程
      • 客户端和服务器进程可以在不同的机器上

虚拟机 Virtual Machines

操作系统完全控制硬件:如果我们想同时在同一硬件上运行多个操作系统怎么办?

操作系统难以调试/监控:

  • 我们如何观察操作系统的工作?
  • 我们如何测试具有潜在破坏性的实现?

虚拟机是硬件的软件仿真,将底层硬件虚拟化(将硬件抽象成上层水平:内存、CPU、硬盘等),然后可以在虚拟机之上运行普通(原始)操作系统。

虚拟机也称为管理程序Hypervisor,接下来展示两类hypervisor的实现:
在这里插入图片描述
第一类hypervisor:为客户OS提供了独立的虚拟机。例如:IBM VM/37

在这里插入图片描述
第二类hypervisor:hypervisor在主机OS中运行,客户OS在虚拟机里运行。

2. 进程抽象 Process Abstraction

本章内容:

  • 进程管理简介
  • 进程抽象:
    • 内存context
      代码和数据
      函数调用
      动态分配的内存
    • 硬件环境
    • 操作系统环境:进程状态
    • 进程控制块和进程表
  • 操作系统与进程的交互

2.1 进程管理

作为OS,从运行A程序转为运行B程序需要:

  1. 和A程序执行相关的信息需要存储起来
  2. 相关信息会被执行B程序所需的信息所替换
    因此,我们需要:描述一个在运行中的程序的抽象(也就是进程 Process

主要话题:

  • 进程抽象 Process Abstration:描述一个在执行程序的信息
  • 进程调度 Process Scheduling:决定要执行哪个进程
  • 进程间通信与同步 Inter-Process Communication & Synchronization:在进程之间传递信息
  • 进程的替代方案 Alternative to Process:轻量级进程(也就是线程 Thread)

进程抽象:进程/任务/工作 (Process/Task/Job)是一个在执行程序的动态抽象。描述一个在运行程序的信息包括:

  • 内存层面:code、data等
  • 硬件层面:registers、PC等
  • OS层面:process properties、resources used等

回忆:C程序和编译代码

int i = 0;
i = i + 20;
lw 		$1, 4096   		//Assume address of i = 4096
addi  	$1, $0, 0      	//register $1 = 0
sw  	$1, 4096   		//i = 0lw   	$2, 4096   		//read iaddi  $3, $2, 20 		//$3 = $2 + 20
sw  	$3, 4096   		//i = i + 20

回忆:程序执行——内存

在这里插入图片描述

回忆: 通用计算机架构

在这里插入图片描述

回忆:组件介绍

  • 内存Memory:存储指令和数据
  • 缓存Cache:复制部分内存以加快访问速度,通常分为指令缓存和数据缓存。
  • 获取单元 Fetch Unit:将指令从内存载入,特殊寄存器的地址(如程序计数器Program Counter, PC
  • 功能单元 Functional Unit:指令执行,不同的指令类型
  • 寄存器 registers:为了高速访问的内部存储器,
    • 通用寄存器 General Purpose Register, GPR:可由用户程序访问(即编译器可见)
    • 特殊寄存器Special Register:如程序计数器Program Counter, PC

回忆:基本指令执行

  • 获取指令 X:程序计数器指示的内存位置
  • 指令 X 分派到相应的功能单元
    • 读取操作数(如果适用)
    • 通常来自内存或 GPR
    • 计算结果
    • 如果适用,写入值:通常写到内存或 GPR
  • 指令 X 完成:为下一条指令更新了 PC

回忆:你需要知道的

可执行文件(二进制)由两个主要组件组成:指令和数据

当一个程序正在执行时,有更多的信息:
内存层面:文本和数据,
硬件层面:通用寄存器、程序计数器等

实际上,程序执行期间还有其他类型的内存使用。

2.2 内存层面 Memory Context:函数调用 Function Call

2.2.1 函数调用 Function Call

Function Call: Challenges

c程序块:

int i = 0;
i = i + 20;

带函数的c代码:

int g(int i, int j)
{int a;a = i + jreturn a;
}

考虑:
我们如何为变量 i、j 和 a 分配内存空间?
我们可以只使用“数据”内存空间吗?
有哪些关键问题?

Function Call:控制流和数据 Control Flow and Data

f() 调用 g(),f是调用者caller,g是被调用者callee

重要步骤:
1. 设置参数
2. 将控制权转移给被调用者
3. 设置局部变量
4. 存储结果(如果适用)
5. 返回给调用者
在这里插入图片描述
控制流问题:

  • 需要跳转到函数体
  • 函数调用完成后需要恢复:至少要存储调用者的PC

数据存储问题:

  • 需要给函数传参数
  • 需要捕获返回结果
  • 可能有局部变量声明
    ⇒ 需要一个新的内存区域,供函数调用动态使用

2.2.2 栈内存 Stack Memory

栈内存区域stack memory region:用于存储信息函数调用的新内存区域

函数调用的信息由栈帧stack frame描述。栈帧包含:

  • 调用者的返回地址
  • 函数的参数(参数)
  • 局部变量的存储
  • 其他信息

栈指针 stack pointer
栈区域的顶部(第一个未使用的位置)在逻辑上由堆栈指针指示:

  • 大多数 CPU 都有一个专门用于此目的的寄存器
  • 调用函数时在顶部添加栈帧:堆栈“增长”
  • 函数调用结束时从顶部移除栈帧:堆栈“收缩”

栈内存 Stack Memory
在这里插入图片描述
某些系统上的内存布局是翻转的,即栈在顶部,text在底部。

在这里插入图片描述

2.2.3 Illustration: Stack Frame v1.0

在这里插入图片描述

函数调用约定function call convention

设置栈帧的不同方法:称为函数调用约定function call convention
这些方法的主要区别:

  • 哪些信息存储在堆栈帧或寄存器中?
  • 调用者/被调用者准备了堆栈帧的哪一部分?
  • 调用者/被调用者清除了堆栈帧的哪一部分?
  • 调用者/被调用者之间谁来调整堆栈指针?

没有万能的方法:取决于硬件和编程语言

接下来描述一个示例方案:

1. 准备进行一个函数调用:

在这里插入图片描述

  • 调用者:将参数传递给寄存器和/或栈
  • 调用者:将返回的程序计数器存储在栈中
  • 将控制权从调用者传递给被调用者
  • 被调用者:保存旧的栈指针 stack pointer
  • 被调用者:在栈中为被调用者的局部变量分配空间
  • 被调用者:调整栈指针到新的栈顶

2. 调用函数g()

在这里插入图片描述

3. 结束函数调用,回到调用者

栈帧分解 stack frame teardown
在这里插入图片描述

  • 被调用者:在栈中放置要返回的结果(如有)
  • 被调用者:恢复保存的栈指针
  • 使用保存的程序计数器PC,将控制权还给调用者
  • 调用者:使用返回的结果
  • 调用者:继续执行接下来的指令
    在这里插入图片描述
    我们已经描述了以下基本思想:
  • 栈帧 stack frame
  • 调用约定calling convention:setup和teardown

让我们看一下堆栈帧中的一些常见附加信息:

  • 帧指针 frame pointer
  • 保存的寄存器 saved registers

帧指针 frame pointer

为了方便各种栈帧项的访问:

  • 栈指针很难使用,因为它可以改变
  • 一些处理器提供专用寄存器:帧指针

帧指针指向栈帧中的固定位置,其他项目通过相对于帧指针的位移访问

FP 的使用取决于平台。

保存的寄存器 saved register

大多数处理器上的通用寄存器 (GPR) 的数量非常有限:例如, MIPS 有 32 个 GPR,x86 有 16 个 GPR。

当 GPR 用尽时:

  • 使用内存临时保存 GPR 值
  • 然后可以将该 GPR 重新用于其他目的
  • 之后可以恢复 GPR 值
  • 这个过程称为寄存器溢出

类似地,函数可以在函数启动之前“溢出”它打算使用的寄存器,在函数结束时恢复那些寄存器。

2.2.4 Illustration: Stack Frame v2.0

在这里插入图片描述
1. 执行函数调用

  • 调用者:将参数传递给寄存器和/或栈
  • 调用者:将要返回的PC保存在栈中
  • 将控制权从调用者转给被调用者
  • 被调用者:保存 被调用者callee 使用的寄存器。保存旧的frame pointer, stack pointer
  • 被调用者:为被调用者的局部变量在栈中分配空间
  • 被调用者:调整stack pointer,指向最新的栈顶

2. 回到调用者

  • 被调用者:重新载入 saved registers, frame pointer, stack pointer
  • 将控制权还给调用者
  • 调用者:继续执行后续指令

注意,这只是个例子。

函数调用小结

在这一部分中,我们了解到:

  • 另一部分内存空间用作栈内存 stack memory
  • 栈内存使用栈帧stack frame存储正在执行的函数
    • 通常存储在栈帧上的信息
    • 建立和拆除栈帧的典型方案
  • 栈指针stack pointer和帧指针frame pointer的使用

2.3 内存层面 Memory Context:动态分配内存 Dynamically Allocated Memory

大多数编程语言都允许动态分配内存:
即在执行期间获取内存空间

例子:
在 C 中, malloc() 函数调用
在 C++ 中,new 关键字
在 Java 中,new 关键字

问题:
我们可以使用现有的“数据”或“堆栈”内存区域吗?

观察:
这些内存块有不同的行为:

  1. 仅在运行时分配,即在编译期间不知道大小 -> 不能放置在数据区域中
  2. 没有明确的释放时间,例如 可以由 C/C++ 中的程序员来显式释放,可以由 Java 中的垃圾收集器隐式释放 -> 不能放入堆栈区域

解决方案:
设置一个单独的堆内存区域。

堆内存 Heap memory

在这里插入图片描述
`
堆内存由于其性质而难以管理:

  • 各种不同的尺寸
  • 各种不同的分配/释放时间

我们可以构建一个场景:堆内存不断地被分配/释放,从而在内存中创建“洞”,也就是空闲内存块挤在占用内存块之间

Context:
描述进程的信息:

  • 内存context:文本、数据、堆栈和堆
  • 硬件context:通用寄存器、程序计数器、堆栈指针、堆栈帧指针等。

2.4 OS context:进程ID和进程状态 ProcessID & Process State

区分进程常用的方法是使用进程 ID (PID),这是一个唯一的数字。

有几个取决于操作系统的问题:

  • PID 是否被重复使用?
  • 它是否限制最大数量。 进程?
  • 是否有保留的 PID?

多进程场景中:一个进程可以是:运行或不运行(例如:另一个进程正在运行)
一个进程可以准备好运行,但实际上并没有执行。例如, 等待轮到它使用 CPU。
因此,每个进程都应该有一个进程状态:作为执行状态的指示。

  • 简单的进程状态模型
    在这里插入图片描述
    状态和转换的集合称为进程模型 process model,描述进程的行为。

  • 通用的五状态进程模型
    注:通用进程状态,具体在实际操作系统中有所不同。
    在这里插入图片描述
    进程状态:

  • new:新进程创建。可能仍在初始化中,尚未准备好。

  • ready:进程正在等待运行

  • running:CPU上正在执行的进程

  • blocked:进程因为事件等待(休眠)。在事件可用之前无法执行。

  • terminated:进程已完成执行,可能需要操作系统清理。

状态转换:

  • Create (nil → New): 新进程已创建
  • Admit (New → Ready):准备运行的进程
  • Switch (Ready → Running): 选择运行的进程
  • Switch (Running → Ready): 进程主动放弃CPU或被调度器抢占
  • Event wait (Running → Blocked): 处理不可用/正在进行的请求事件/资源/服务(例如系统调用,等待 I/O)
  • Event occurs (Blocked → Ready): 事件发生 ⇒ 进程可以继续

给定 n 个进程:
使用 1 个 CPU:<= 1 个进程处于运行状态。概念上,一次只有 1 个状态转换transition
使用 m 个 CPU:<= m个 进程处于运行状态,可能并行转换。

不同的进程可能处于不同的状态,每个进程可能位于其状态图的不同部分。

在这里插入图片描述
Note:

  • 超过 1 个进程可以处于就绪 + 阻塞队列
  • 可能有单独的事件队列
  • 排队模型提供进程的全局视图,即操作系统怎么观测它们。

Context Updated:
当一个程序正在执行时,有更多的信息:

  • Memory Context:文本和数据、栈和堆
  • Hardware context:通用寄存器、程序计数器、栈指针、栈帧指针等
  • OS context:进程 ID、进程状态等

2.5 进程表和进程控制块 Process Table & Process Control Block

进程的整个执行context,传统上称为过程控制块 (Process Control Block, PCB) 或进程表条目(Process Table Entry)。

Kernel 为所有进程维护 PCB,存储为一个代表所有进程的表。

有趣的问题:

  • 可扩展性:你可以有多少个并发进程?
  • 效率:应提供有效的访问和最小的空间浪费

进程表

在这里插入图片描述

2.6 进程和OS之间交互:系统调用 System Calls

到操作系统的应用程序接口 (API) 提供在 kernel 中调用设施/服务的方式,与普通函数调用不同,系统调用必须从用户模式user mode更改为内核模式kernel mode

不同的操作系统有不同的 API:

  • Unix 变体:
    • 大多数遵循 POSIX 标准
    • 少量调用:~100
  • Windows系列:
    • 跨不同 Windows 版本使用 Win API
    • 新版本的 windows 通常会增加更多的调用
    • 大量调用:~1000

Unix 在C/C++中的系统调用:

  • 在 C/C++ 程序中,几乎可以直接调用System calls,大多数系统调用具有相同名称和相同参数的库版本library version(库版本充当函数包装器function wrapper)。
  • 除此之外,一些库函数为程序员提供了一个更加用户友好的版本(例如,更少的参数,更灵活的参数值等)。
    库版本也能作为功能适配器 function adapter

在这里插入图片描述
在这个例子里触发的系统调用有:

  • getpid()
  • write(): 通过printf()这个库函数调用引发。

通常的系统调用机制
在这里插入图片描述

  1. 用户程序调用库函数:使用普通函数调用机制。
  2. 库函数(通常在汇编代码中)将系统调用号system call number放在指定位置。例如: register。
  3. 库函数执行特殊指令,从用户模式切换到内核模式,该指令通常称为 TRAP
  4. 在内核模式下,确定一个合适的系统调用处理程序appropriate system call handler:使用系统调用号作为索引。这一步通常由调度员dispatcher处理。
  5. 系统调用处理程序system call handler被执行:执行实际请求
  6. 系统调用处理程序结束:控制权返回到库函数,从内核模式切换到用户模式
  7. 库函数返回用户程序:通过普通的函数返回机制

2.7 进程和OS之间交互:Exception and Interrupt

Exception

  • 执行机器级指令可能会导致异常。例如:算术错误 / 上溢、下溢、除以零 / 内存访问错误 / 非法内存地址,未对齐的unaligned内存访问等。
  • Exception是同步的synchronous,由于程序执行而发生
  • Exception的影响:必须执行异常处理程序 exception handler;类似于强制调用函数forced function call

Interrupt

外部事件可以中断程序的执行。通常与硬件相关,例如:定时器、鼠标移动、键盘按下等;
中断是异步的asynchronous,独立于程序执行而发生的事件
中断效果:程序执行暂停;必须执行中断处理程序interrupt handler

Exception / Interrupt Handler

在这里插入图片描述

  1. 发生异常/中断:控制权自动转移到处理程序例程handler routine
  2. 从处理程序例程handler routine返回:恢复程序执行状态;可能表现得好像什么都没发生。

Summary

使用进程作为运行程序的抽象:

  • 程序执行所需的信息(环境)
  • 内存、硬件和操作系统环境

从操作系统角度看进程:

  • PCB及进程表

操作系统 <= => 进程交互

  • 系统调用
  • 异常/中断

3. 进程调度 Process Scheduling

本章内容:

  • 并发执行
  • 进程调度
    • 定义
    • 进程行为
    • 进程环境
    • 良好调度的标准
    • 进程调度程序
  • 调度算法
    • 用于批处理系统
    • 对于交互式系统

3.1 并发执行 concurrent execution

并发进程:涵盖多任务流程的逻辑概念

  • 可能是虚拟并行:平行错觉(psedoparallelism)
  • 也可能是物理并行。例如, 多 CPU / 多核 CPU,允许并行执行多个进程

我们可以假设在以下讨论中没有区分这两种形式的并行性。

简化的并发示例

在这里插入图片描述
在 1 个 CPU 上并发执行:交错执行来自两个进程的指令,也称为时间片timeslicing

3.2 交错执行 Interleaved Execution:context switch

在这里插入图片描述
多任务处理需要改变 A 和 B 之间的context:操作系统在切换进程中有一定开销。

多任务OS

  • 1 CPU:分时执行任务
    在这里插入图片描述
  • 多处理器:在 n 个 CPU 上进行时间切片
    在这里插入图片描述

3.3 调度 Scheduling

多个进程的问题:如果ready-to-run进程多于可用CPU,应该选择哪个来运行?(线程级调度中的类似思想)。这也称为调度问题。

术语:
调度器 Scheduler:做出调度决策的操作系统的一部分
调度算法Scheduling algorithm:调度器使用的算法

在这里插入图片描述
每个进程对CPU时间的要求不同:Process Behavior
多种分配方式:受进程环境process environment影响;称为调度算法scheduling algorithm
评估调度程序的一些标准 criteria to evaluate the scheduler

进程行为 Process Behavior

一个典型的进程会经历以下阶段:

  • CPU活动:计算(如数字运算)。计算型任务Compute-Bound Process大多数时间花在这一步。
  • IO活动:从IO设备中请求、获取服务(如在屏幕上打印,从文件中读取数据)。IO型任务IO-Bound Process大多数时间花在这一步。

处理环境 Processing Environment

三类:

  1. 批量处理:无用户,无需交互,无需响应。
  2. 交互式(或多道程序):活跃用户与系统交互,应该响应迅速,响应时间一致。
  3. 实时处理:有最后期限,通常是周期性的过程。

调度算法的评判标准 Criteria for Scheduling Algorithm

评估调度算法的许多标准:

  • 受处理环境影响较大
  • 可能有冲突

所有处理环境的标准:

  • 公平Fairness
    • 应该获得公平的 CPU 时间份额
      • 基于每个进程 或 基于每个用户
    • 也意味着没有进程“饿死” starvation
  • 平衡Balance
    • 应利用计算系统的所有部分

什么时候执行调度?

两种调度策略(由何时触发调度定义)

  • 非抢占式(合作)Non-preemptive (Cooperative):进程保持调度(处于运行状态)直到它自愿阻塞或放弃 CPU
  • 抢先Preemptive:一个进程被给到一个固定的时间配额来运行,可能会提前阻止或放弃;时间配额结束时,正在运行的进程暂停,(如有另一个进程)将选择另一个进程继续执行。

调度步骤

  1. 调度器被触发 (OS获得控制权)
  2. 如果需要进行context交换,当前进程的context会被保存,用阻塞队列/准备队列中的任务的context来代替。
  3. 根据调度算法,选择一个合适的进程P来执行
  4. 准备P的context
  5. 运行进程P

3.4 批处理调度 Scheduling Fro Batch Processing

关于批处理系统:没有用户交互,非抢占式调度占主导地位。
调度算法通常更容易理解和实现,通常使得这种算法可用于其他类型系统的变体/改进。

涵盖了三种算法:

  • 先到先得 (First-Come First Served, FCFS)
  • 最短作业优先 (Shortest Job First, SJF)
  • 下一个最短剩余时间 (Shortest Remaining Time Next, SRT)

批处理的评判标准:

  • 周转时间Turnaround time:总时间,即完成-到达时间。与等待时间相关(等待CPU的时间)。
  • 吞吐量Throughput:单位时间内完成的任务数,即任务完成率。
  • CPU利用率CPU utilization:CPU 处理任务的时间百分比。

(1)先到先得 First-Come-First-Served

想法:

  • 任务根据到达时间存储在先进先出 (FIFO) 队列中
  • 选择队列中的第一个任务运行,直到任务完成或任务被阻止
  • 阻塞的任务从 FIFO 队列中移除。当它再次准备好时,把他放在队列的后面,即就像一个新到的任务。

保证不饿死 Starvation
FIFO中任务X前面的任务数一直在递减 ⇒ 任务 X 最终会得到它的机会

在这里插入图片描述
3个任务的平均总等待时间 = (0 + 8 + 13)/3 = 7 个时间单位

缺点:

  • 简单的重新排列就可以减少平均等待时间!
  • 另外,考虑这种情况:
    第一个任务(任务 A)是 CPU 密集型任务,然后是一些 IO 密集型任务 X
    任务 A 运行:所有任务 X 在就绪队列中等待(I/O 设备空闲)
    任务 A 在 I/O 上阻塞:所有任务 X 快速执行并在 I/O 上阻塞(CPU 空闲)
    称为车队效应Convoy Effect

(2)短任务优先 Shortest-Job-First SJF

思想:选择需要最少CPU事件的任务先运行。

Notes:
需要提前知道任务的总 CPU 时间,如果不知道它的执行时间,则必须“猜测”。
给定一组固定的任务:减少平均等待时间
饥饿是可能发生的:因为偏向于短期任务,长期任务可能永远没有获得CPU的机会

在这里插入图片描述
3个任务的平均总等待时间 = (0 + 3 + 8)/3 = 3.66 时间单位
可以证明 SJF 保证最小的平均等待时间。

一个任务通常会经历几个 CPU-Activity 阶段:可以通过之前的 CPU-Bound 阶段猜测未来的 CPU 时间需求

常用方法(指数平均):
P r e d i c t e d n + 1 = α ∗ A c t u a l n + ( 1 − α ) ∗ P r e d i c t e d n Predicted_{n+1}= α *Actual_n+(1-α) * Predicted_n Predictedn+1=αActualn+(1α)Predictedn
Actual_n = 最近一次消耗的 CPU 时间
Predicted_n = 过去的 CPU 时间消耗历史
α = 对最近事件或过去历史的权重
Predicted_{n+1} = 最新预测

(3)最短剩余时间 Shortest Remaining Time SRT

思想:
SJF的变体:使用剩余时间作为选择依据,抢先策略。
选择剩余(或预期)时间最短的工作

Notes:

  • 剩余时间较短的新作业可以抢占当前正在运行的作业
  • 为短期工作(即使是晚到达的)提供良好的服务

在这里插入图片描述

3.5 交互式系统的调度 Scheduling For Interactive System

交互式环境的评判标准

响应时间response time:系统请求和响应之间的时间

可预测性predictability:响应时间的变化,较小的变化 == 更可预测
抢占式调度算法用于确保良好的响应时间 ⇒ 调度器需要周期性地运行

确保周期性调度

问题:

  • 调度程序如何定期“接管”CPU?
  • 我们如何确保用户程序永远不会阻止调度程序的执行?

答案:
定时器中断 = 周期性中断(基于硬件时钟 clock
Timer interrupt = Interrupt that goes off periodically (based on hardware clock)
操作系统确保定时器中断timer interrupt不能被任何其他程序拦截 ⇒ 定时器中断处理程序 调用 调度程序 Timer interrupt handler invokes scheduler.

Timer & Time Quantum

定时器中断间隔 (Interval of Timer Interrupt, ITI):

  • 每次定时器中断都会触发 OS 调度程序
  • 通常的值在1ms 至 10ms之间

时间限额 Time Quantum

  • 给进程的执行时间配额
  • 在进程之间可以是常量或变量
  • 必须是定时器中断间隔的倍数
  • 值范围大:通常为 5 毫秒到 100 毫秒

在这里插入图片描述

调度算法

涵盖的算法:
循环赛 Round Robin (RR)
基于优先级Priority Based
多级反馈队列Multi-Level Feedback Queue (MLFQ)
彩票调度Lottery Scheduling

(1)循环赛 Round Robin (RR)

思想:

  • 任务存储在 FIFO 队列中
  • 从队列前面选择第一个任务运行,直到:
    • 经过了固定的时间片(time slice / quantum)
    • 任务主动放弃CPU
    • 任务阻塞
  • 然后将任务放置在队列的末尾以等待另一轮
    • 阻塞的任务将被移动到其他队列以等待其请求
  • 当阻塞的任务再次就绪时,它被放在队列的末尾

Notes:

  • 基本上是 FCFS 的抢占式版
  • 响应时间保证:
    • 给定 n 个任务和时间配额quantum q
    • 任务获得 CPU 之前的等待时间的上界是 (n-1)q
  • 需要定时器中断,让调度程序检查时间配额是否到期
  • 时间配额持续时间的选择很重要:
    • Big Quantum:更好的 CPU 利用率但更长的等待时间
    • Small Quantum:更大的开销(更差的 CPU 利用率)但更短的等待时间

在这里插入图片描述

(2)优先调度 Priority Scheduling

思想:

  • 有些进程比其他进程重要,不能平等对待所有进程
  • 为所有任务分配优先级值
  • 选择具有最高优先级值的任务

变体:

  • 抢占式版本:优先级高的进程可以抢占正在运行的优先级低的进程
  • 非抢占式版本:迟来的高优先级进程必须等待下一轮调度

在这里插入图片描述
缺点:
低优先级进程可能饿死:高优先级进程不断占用CPU,这是一种更糟糕的抢占式变体。

可能的解决方案:

  • 在每个时间片后降低当前运行进程的优先级,最终低于第二高的优先级。
  • 给当前运行的进程一个时间片,下一轮调度不考虑这个进程。

通常,很难保证或控制 分配给使用优先级的进程的 CPU 时间的确切数量

优先级反转Priority Inversion
考虑以下场景:
优先级:{A = 1, B=3, C= 5}(1 最高)
任务 C 启动并锁定资源(例如文件)
⇒ 任务 B 抢占 C
⇒ C 无法解锁文件
⇒ 任务 A 到达并需要与 C 相同的资源,但是资源被锁定了!(即使任务 A 具有更高的优先级,任务 B 也会继续执行)

这种情况称为优先级反转:低优先级任务抢占高优先级任务

(3)多级反馈队列 Multi-Level Feedback Queue (MLFQ)

旨在解决一个 BIG + HARD 问题:

  • 我们如何在没有完美知识的情况下安排时间?
  • 大多数算法都需要某些信息(进程行为 process behavior、运行时间running time等)

MLFQ 是:自适应:“自动学习进程行为” Adaptive: Learn the process behavior automatically
最小化 【IO 密集型进程的响应时间response time for IO-bound processes】和【CPU 密集型进程的周转时间turnaround time for CPU-bound processes

规则

基本规则:
如果优先级(A) > 优先级(B) ⇒ A 运行
如果 Priority(A) == Priority(B) ⇒ A 和 B 在 RR 中运行

优先级设置/更改规则:

  1. 新任务 ⇒ 最高优先级
  2. 如果作业用尽其时间片 ⇒ 优先级降低
  3. 如果作业在用尽时间片之前放弃/阻塞 ⇒ 保留优先级

例子1

3 个队列:Q2(最高优先级)、Q1、Q0
一个长时间运行的作业
在这里插入图片描述

例子2: 例子1的基础上 + 中间有时候会出现短任务

在这里插入图片描述

例子3

两种任务:
A = CPU密集型(已经在系统中存在一段时间)
B = I/O密集型

在这里插入图片描述
你能想出一种滥用算法的情况吗? ⇒ 等效问题:MLFQ 不适用于什么样的工作组合?
有什么方法可以纠正以上问题?

(4)彩票调度 Lottery Scheduling

进程发放各种系统资源的“彩票”。例如, CPU 时间、I/O 设备等。
当需要调度决策时,在符合条件的彩票中随机抽取一张彩票,获胜者被授予资源。
从长远来看,一个进程持有 X% 的票,就可以赢得所持彩票的X%,即使用资源 X% 的时间

性质

1. 响应式responsive 一个新创建的进程可以参与下一次抽奖
2. 提供良好的控制水平provides good level of control

  • 可以给一个进程 Y 数量的彩票,然后它可以分发给它的子进程。
  • 一个重要的过程可以给更多的彩票,可以控制使用比例。
  • 每个资源都可以有自己的一组票,每个任务每个资源的不同使用比例。

3. 实现简单

Summary

操作系统中的调度:
基本定义
影响调度的因素
工艺、环境
良好调度的标准

调度算法:
批处理系统——FCFS、SJF、SRT
交互式系统—— RR、优先级、多级队列、MLFQ 和彩票调度

4. 线程 Threads

本章内容:
线程:动机、基本理念
线程模型:内核与用户线程Kernel vs User Thread、混合模型 Hybrid Model
Unix 中的线程:POSIX 线程:创建、退出、同步、勘探

4.1 Motivation for Thread

进程的开销很大:

  • fork() 模型下的进程创建:
    • 重复的内存空间
    • 复制大部分进程context等
  • context切换:需要保存/恢复进程信息

独立进程之间很难相互通信
他们各自有独立的内存空间 ⇒ 没有简单的方法传递信息 ⇒ 需要进程间通信 (IPC)

发明线程是为了克服进程模型的问题 ,最初是一种“快速破解 quick hack”,最终成熟为非常流行的机制

基本思想:

  • 传统进程有一个控制线程thread of control:任何时候整个程序只有一条指令在执行
  • 我们“简单地”向同一个进程添加更多的控制线程:程序的多个部分在概念上同时执行

4.2 线程 Thread of Control

假设我们正在准备午餐,其中包括以下任务:蒸饭 \ 炸鱼 \ 煮汤
一个伪 C 程序:

int main()
{steamRice( twoBowls );fryFish( bigFish );cookSoup( cornSoup );printf( "Lunch READY!!\n“ );return 0;
}

单线程的进程会按顺序执行这三项任务。
假设任务之间是相互独立的,尝试使用多线程。

使用fork()创建多线程

在这里插入图片描述

使用Thread…do多线程

(下面这个是伪代码,这三个线程是并发的)
在这里插入图片描述

Process and Thread

  • 一个进程可以有多个线程 :称为多线程进程
  • 同一进程中的线程共享以下资源
    • 内存context:文本、数据、堆
    • 操作系统context:进程 ID、文件等其他资源
  • 每个线程所需的唯一信息
    • 标识(通常是线程 ID)
    • 寄存器(通用和特殊)
    • “堆栈”(稍后会详细介绍)

在这里插入图片描述

进程context交换 vs 线程交换

  • 进程context切换涉及:操作系统context、硬件context、内存context
  • 同一进程内的线程切换涉及:硬件context(寄存器、stack【实际上只是改变 FP 和 SP 寄存器】)

线程比进程“轻”得多:又名轻量级进程 lightweight process

线程优点

  • 经济economy:与多个进程相比,同一进程中的多个线程需要更少的资源来管理。
  • 资源共享 resource sharing:线程共享进程的大部分资源,不需要额外的机制来传递信息。
  • 响应及时 responsiveness:多线程程序的响应速度可能会更快
  • 可扩展性 scalability:多线程程序可以利用多个 CPU

线程的问题

系统调用并发 Sysytem call concurrency
多线程并行执行 ⇒ 可能并行进行系统调用(必须保证正确性并确定正确的行为)

进程行为 Process behavior
对进程操作的影响
例子:

  • fork() 复制了一份进程,线程呢?
  • 如果单个线程执行exit(),那么整个进程呢?
  • 如果单个线程调用 exec(),那么其他线程呢?

4.3 线程模型 Thread Models

用户线程 User Thread

  • 线程作为用户库user library实现,运行时系统(在进程中)将处理与线程相关的操作
  • 内核感受不到进程中的线程

内核线程 kernel thread

  • 线程在操作系统中实现,线程操作作为系统调用被处理 thread operation is handled as system calls
  • 线程级调度是可以做到的:内核按线程调度,而不是按进程
  • 内核可以使用线程来执行它自己

用户线程 User Thread

在这里插入图片描述
优点:

  • 可以在任何操作系统上拥有多线程程序
  • 线程操作只是库调用 library calls
  • 通常更具可配置性和灵活性 configurable and flexible。例如,可以自定义线程调度策略。

缺点:

  • 操作系统不知道线程,调度是在进程级别 执行的
  • 一个线程阻塞 ⇒ 进程阻塞 ⇒ 所有线程阻塞
  • 无法利用多个 CPU

内核线程 Kernel Thread

在这里插入图片描述
优点:

  • 内核可以在线程级别上进行调度:
  • 同一进程中的 1 个以上线程可以在多个 CPU 上同时运行

缺点:

  • 线程操作现在是一个系统调用 ⇒ 速度较慢且更加资源密集
  • 通常不太灵活:因为由所有多线程程序使用。如果实现了许多功能:开销很大,简单的程序可能会被复杂化。如果实现的功能很少:对于某些程序不够灵活。

混合线程模型 Hybrid Thread Model

同时拥有内核和用户线程:仅内核线程上的OS调度;用户线程可以绑定到内核线程。
提供极大的灵活性:可以限制任何进程/用户的并发

Solaris混合线程模型
在这里插入图片描述
线程最初是一种软件机制。User space library ⇒ OS aware mechanism

现代处理器有硬件支持: 本质上提供多组寄存器(GPR 和特殊寄存器)以允许线程在同一内核上 本地并行运行,称为同时多线程(simultaneous multi-threading, SMT)。

例子:英特尔处理器上的超线程 hyperthreading

5. 进程间通信 Inter-Process Communication

本章内容:

  • Motivation
  • Common communication mechanisms
    • Shared memory
    • Message passing
    • Pipe (Unix specific)
    • Signal (Unix specific)

协作进程很难共享信息,因为内存空间是独立的,所以需要进程间通信机制(IPC)。

两种常见的IPC机制:共享内存Shared-memory和消息传递message passing

两种特定于 Unix 的 IPC 机制:管道pipe和信号signal

5.1 共享内存 Shared-Memory

思想:

  • 进程 P1 创建一个共享内存区域 M
  • 进程 P2 将内存区域 M 附加到它自己的内存空间
  • P1 和 P2 现在可以使用内存区域 M 进行通信
    • M 的表现与普通内存区域非常相似
    • 但所有其他方都可以看到对该区域的任何写入

同一模型适用于共享同一内存区域的多个进程。

在这里插入图片描述
OS只参与步骤1和2。

优点:

  • 高效:OS只参与初始化的步骤(也就是创建和绑定共享内存区域 create and attach shared memory region
  • 使用方便:共享内存区域的行为与普通内存空间相同,即任何类型或大小的信息都可以轻松写入

缺点:

  • 同步 Synchronization:共享资源 ⇒ 需要同步访问
  • 实施通常更难

POSIX Shared Memory in *nix

基本使用步骤:

  1. 创建/定位共享内存区域 M
  2. 将 M 添加到进程内存空间
  3. 读取/写入 M:写入的值对所有共享 M 的进程可见
  4. 使用后从内存空间中解除绑定 M
  5. 销毁M:只有一个进程需要这样做,仅当 M 未附加到任何进程时才能销毁

Master Program

在这里插入图片描述
在这里插入图片描述

Slave Program

在这里插入图片描述

5.2 信息传递 Message Passing

思想:
进程 P1 准备消息 M 并将其发送给进程 P2
⇒ 进程 P2 收到消息 M
⇒ 消息发送和接收通常作为系统调用

附加属性:
命名naming:如何识别通讯中的对方
同步synchronization:发送/接收操作的行为

在这里插入图片描述
Msg 必须存储在内核内存空间中。
每个发送/接收操作都需要通过操作系统(即系统调用)。

命名机制:直接通信 Naming Scheme: Direct Communication

消息的发送者/接收者需要明确表明对方是谁
例子:
Send(P2, Msg):向进程P2发送消息Msg
Receive(P1, Msg):从进程P1接收消息Msg

特征:

  • 每对通信进程一个链接
  • 需要知道对方的身份

命名机制:间接通信 Naming Scheme: Indirect Communication

消息被发送到消息存储/从消息存储接收sent to / received from message storage:通常称为邮箱或端口 mailbox or port

例子:
Send(MB,Msg):发送消息Msg到邮箱MB
Receive(MB, Msg) : 从邮箱 MB 接收消息 Msg

特征:
一个邮箱可以在多个进程之间共享

两种同步行为 Two Synchronization Behaviors

阻塞原语(同步) Blocking Primitives (Synchronous)
Send():发送者被阻塞,直到收到消息
Receive():接收者被阻塞,直到消息到达

非阻塞原语(异步)Non-Blocking Primitives (asynchronous)
Send():发送方立即恢复操作
Receiver():如果有消息,接受者接收消息;如果没有,接受者接收【消息还未准备好】的指示信息。

优缺点

优点:
可移植portable:可以很容易地在不同的处理环境中实现,例如 分布式系统、广域网等
更容易同步Easier synchronization:例如,当使用同步原语时,发送者和接收者是隐式同步的

缺点:
低效Inefficient:通常需要操作系统干预
更难使用harder to use:消息的大小和/或格式通常受到限制

5.3 Unix Pipes

在Unix里,一个进程有三个默认的通信通道。
在这里插入图片描述
例子:
在典型的 C 程序中,printf() 使用标准输出,scanf() 使用标准输入。

Piping in Shell

Unix shell 提供了“|” 将一个进程的输入/输出通道链接到另一个进程的符号,这称为管道piping

例如(“A | B”):
在这里插入图片描述
A 的输出(而不是进入屏幕)直接进入 B 作为输入(就好像它来自键盘一样)

Unix Pipes

最早的IPC机制之一

思想:
一个通信通道由 2 个端点创建:一个读端,一个写端。就像现实世界中的水管。

在shell里的piping “|” 在内部就是使用这种机制实现的:
在这里插入图片描述

Unix Pipes:as an IPC Mechanism

管道可以在两个进程之间共享,是生产者-消费者关系的一种形式

  • P 产生(写入)n 个字节
  • Q 消耗(读取)m 个字节

像一个匿名文件。
FIFO ⇒ 必须按顺序访问数据。
在这里插入图片描述

Unix Pipes: Semantic

管道作为具有隐式同步的循环有界字节缓冲区circular bounded byte buffer with implicit synchronization

  • 当缓冲区已满时 写入程序等待
  • 缓冲区为空时 读者等待

变体:

  • 可以有多个读者/作者:普通 shell 管道有 1 个写入器和 1 个读取器
  • 取决于 Unix 版本,管道可能是半双工的或全双工
    • 半双工half-duplex:单向unidirectional:有一个写端和一个读端
    • 全双工full-duplex:双向bidirectional:读/写的任何一端

Unix Pipe:System Calls

在这里插入图片描述
返回内容:
0表示成功; !0 表示错误
返回一个 文件描述符 数组:
fd[0] == 读取结束
fd[1] == 写结束

在这里插入图片描述

示例代码

我们在读写的时候,需要关闭另外一边。如果执行读操作,要关闭写入口;如果执行写操作,要关闭读入口,否则就会丢失End of File标志。
在这里插入图片描述
我们可以将标准通信通道(stdin、stdout、stderr)附加/更改 attach/change到其中一个管道,也就是将输入/输出从一个程序重定向到另一个程序。

Unix system calls需要考虑:dup()、dup2()。

5.4 Unix Signal

Unix SIgnal是进程间通信的一种形式

  • 关于事件的异步通知 An asynchronous notification regarding an event
  • 发送到进程/线程 sent to a process/thread

信号的接收者必须通过以下方式处理信号:一组默认处理程序a default set of handlers或用户提供的处理程序(仅适用于某些信号)user supplied handler

Unix中的常见信号:杀死、停止、继续、内存错误、算术错误等。kill, stop, continue, memory error, arithmetic error

在这里插入图片描述

6. 进程管理:同步 Process Management: Synchronization

Race Condition :Problems with concurrent execution
Critical Section

  • Properties of correct implementation
  • Symptoms of incorrect implementation
    Implementations of Critical Section
  • Low level
  • High level language
  • High level abstraction
    Classical synchronization problems

并发执行的问题

当有两个或多个进程时:进程以交错方式同时执行 并且 共享可修改的资源 ⇒ 这可能导致同步问题

  • 单个顺序过程的执行是确定的deterministic,重复执行将得到相同的结果。
  • 执行并发进程可能是不确定的,执行结果取决于访问/修改共享资源的顺序(也就是取决于竞争条件race conditions

竞争条件 Race Condition

在这里插入图片描述
进程 P1 和 P2 共享一个变量 X
X = X + 1000 可以大致翻译为以下机器指令:

  • Load X ⇒ register1
  • Add 1000 to register1
  • Store register1 ⇒ X

良好表现:给出正确结果2345
在这里插入图片描述
出错情况:交错执行,结果出错,例如下面这样
在这里插入图片描述

解决方法:
不正确的执行是由于对共享的可修改资源的不同步访问 unsynchronized access to a shared modifiable resource

  • 将具有竞争条件 的代码段指定为临界区 critical section
  • 在任何时刻,只有一个进程可以在临界区执行 ⇒ 防止其他进程进入同一临界区

临界区 Critical Section

在这里插入图片描述
操作示例:
在这里插入图片描述
Critical Section的正确实现需要具备以下特征:

  • 互斥 Mutual Exclusion:如果进程 Pi 在临界区执行,则所有其他进程都无法进入临界区。
  • 推进 Progress:如果临界区中没有进程,则应将访问权限授予等待进程之一。
  • 有界等待 Bounded Wait:在进程 Pi 请求进入临界区后,其他进程可以在 Pi 之前进入临界区的次数存在上限。
  • 独立 Independence:不在临界区执行的进程永远不应该阻塞其他进程。

错误的同步有以下特征:

  • 死锁 Deadlock:所有进程都阻塞,没有任何进展。
  • 活锁 Livelock:通常与死锁避免机制有关,进程不断改变状态以避免死锁并且没有取得其他进展,通常进程不会被阻塞。(反复横跳,P1占用R1,P2占用R2,P1需要R2,但是等不到,他俩都释放,然后又分别锁定R1、R2,他们就只能不断地锁定-释放-锁定-释放)
  • 饿死 Starvation:某些进程永远被阻止。

Critical Section的实现

汇编层面实现:处理器提供的机制
高级语言层面实现:仅使用正常的编程结构
高级抽象:提供一种有额外特性的抽象机制,通常由汇编级机制实现

(1)汇编层面实现 Assembly Level Implementation

Test and Set:一个原子指令。Atomic Instruction
处理器提供的用于实现同步的通用机器指令: TestAndSet Register, MemoryLocation
这个指令会做到:

  1. 将位于MemoryLocation的内容载入Register
  2. 将一个1放入MemoryLocation

这两个过程作为一个单一的机器操作来实现 ⇒ 原子性 atomic

为了便于讨论,假设 TestAndSet 机器指令具有等效的高级语言版本。
在这里插入图片描述
这种方式是可以实现锁的。但是,它采用忙等的方式Busy Waiting(不断检查条件,直到可以安全进入临界区)⇒ 浪费处理能力

大多数处理器上都存在此指令的变体:

  • 比较交流 Compare and Exchange
  • 原子交换 Atomic Swap
  • 加载链接/存储条件Load Link /Store Conditional

(2)高级语言层面实现 High-Level-Language Implementation

尝试: 竞争锁
在这里插入图片描述
这看起来好像可以,其实并不。因为读写锁不是原子性的,可能在P0读到lock为0时,进程切换到P1,此时P1也读到lock为0,两者同时进入Critical Section。这样违反了互斥Mutual Exclusion的条件(如果进程 Pi 在临界区执行,则所有其他进程都无法进入临界区)。

我们可以通过防止进程切换来解决问题:
在这里插入图片描述

然而这种方式仍然存在以下问题:

  • buggy的critical section可能会使整个系统停滞
  • 忙等 busy waiting
  • 需要禁用/启用中断的权限

让进程P0和P1轮流进入Critical Section:
在这里插入图片描述
但是,如果P0没进入Critical Section,P1就会饿死。这样违反了独立independence原则。

进一步解决独立性问题:如果 P0 或 P1 没在Critical Section里,另一个进程仍然可以进入 Critical Section。

在这里插入图片描述
这样做可能导致死锁,互相等待对方释放资源。

接下来我们看Peterson算法:
假设写入Turn是一个原子性操作。

在这里插入图片描述
Peterson算法的缺点:

  • 忙等:在等待的进程会反复测试 while 循环条件而不是进入阻塞状态;
  • 这是一种低阶的设计:可以使用更高级的编程构造:使用简化互斥、不易出错的方式
  • 不具备一般性:我们希望同步机制是可以泛化的,不仅仅是互斥。

(3)高级抽象 High-Level Abstraction

信号量 semaphore是一种通用的同步机制,仅指定需要有什么表现,可以有不同的实现。
信号量提供了一种阻塞多个进程的方法,称为休眠进程sleeping process
信号量提供了一种解锁/唤醒一个或多个休眠进程的方法。

Wait() and Signal()

信号量 S 包含一个整数值,最初可以初始化为任何非负值

两个原子信号量操作:
在这里插入图片描述
注意:上面说的是这两个方法应该有怎样的表现,而不是具体实现。

为了更好地理解信号量,我们可以将其可视化为:一个protected的Integer和一list的等待中的进程。
在这里插入图片描述
在这里插入图片描述
信号量的特性:
给定: S i n i t i a l > = 0 S_{initial} >= 0 Sinitial>=0
以下不变量invariant应该恒为真: S c u r r e n t = S i n i t i a l + # s i g n a l ( S ) − # w a i t ( S ) S_{current} = S_{initial} + \#signal(S) - \#wait(S) Scurrent=Sinitial+#signal(S)#wait(S)
#signal(S):执行signals()操作的次数
#wait(S):执行成功wait()的次数

  • 通用信号量S General semaphore S > = 0 ( S = 0 , 1 , 2 , 3 , . . . ) S>=0 (S = 0, 1, 2, 3, ...) S>=0(S=0,1,2,3,...),也称作计数信号量。
  • 二元信号量S Binary semaphore S = 0 o r 1 S = 0 or 1 S=0or1

为方便起见,存在通用信号量,实际上二元信号量就足够了,即通用信号量可以被二进制信号量模拟。

信号量例子:Critical Section

二元信号量S=1
对于任意进程:
在这里插入图片描述
在这种情况下,S可以是0或1,可由信号量不变量semaphore invariant推导出来。
信号量的这种用法通常称为互斥量(互斥)mutex, mutual exclusion

互斥量的非正式证明 Mutex:Correct CS - Informal Proof

在这里插入图片描述

  • 死锁 Deadlock意味着所有的进程都卡在wait(S) ⇒ S_current = 0 && N_cs = 0 ⇒ 这违背了 S_current + N_cs = 1的不变量
  • 饿死:假设P1阻塞在wait(S),P2在Critical Section中,通过signal(S)退出Critical Section。如果没有其他进程在休眠,P1被唤醒;如果有其他进程,在公平调度下,P1最终也会被唤醒。

不正确地使用信号量:死锁

假设信号量初始化为 P = 1, Q = 1。
P0先Wait§ ⇒ P = 0,切换到P1 Wait(Q) ⇒ Q = 0,切换到P0,无法获得资源,因为已经被P1锁定了,同理切换到P1,需要的资源也被P0锁定了,双方都在等待对方释放资源,陷入死锁。
在这里插入图片描述

(4)其他高阶抽象

信号量非常强大:
到目前为止,信号量没有无法解决的同步问题
其他高级抽象实质上提供了单独使用信号量难以实现的扩展功能

常见的替代方案:条件变量 Conditional Variable

  • 允许任务等待特定事件发生
  • 具有广播broadcast能力,即唤醒所有等待任务
  • 与监控者monitor有关

经典的同步问题 Classical Synchronization Problems

(1)Producer-Consumer

进程共享一个大小为K的有界缓冲区

  • 生产者 Producer:生产任务,当缓冲区未满时,插入缓冲区(< K items)。
  • 消费者 Consumer:当缓冲区非空时,移除任务(> 0 items)。

在这里插入图片描述

生产者-消费者:忙等方式

在这里插入图片描述
初始值:
count = in = out = 0
mutex = S(1) (初始值为1的信号量)
canProduce = TRUE
canConsume = FALSE

  • canConsume:触发消费者去获取一个item
  • canProduce:触发生产者去生产一个item
  • wait(mutex) + signal(mutex):创建一个Critical Section
  • in = (in + 1) % K
  • out = (out + 1) % K:循环队列

这段代码可以解决问题,但存在忙等问题。

生产者-消费者:阻塞方式

在这里插入图片描述
初始值:
count = in = out = 0
mutex = S(1), notFull = S(K), notEmpty = S(0)

  • wait(notFull):让生产者休眠
  • wait(notEmpty):让消费者休眠
  • signal(notFull):一个消费者唤醒一个生产者
  • signal(notEmpty):一个生产者唤醒一个消费者

这段代码也可以正确地解决问题,不存在忙等问题,不需要的生产者/消费者会根据各自的信号量情况进入睡眠。

(2)读者写者 Readers Writers

进程共享数据结构D

  • Reader:从D中获取数据
  • Writer:在D中修改数据

一次只能有一个写者,但是可以有多个读者。
在这里插入图片描述
Readers Writers:简单版本
在这里插入图片描述
当房间是空的时候,writer可以进入并修改数据,否则他要等到roomEmpty信号量为1时才能进入。
当一个读者进入房间时,将roomEmpty信号量置为0,可以有多个读者,当最后一个读者离开房间时,将roomEmpty置为1。

哲学家进餐问题 Dining Philosophers: Specification

在这里插入图片描述
五位哲学家围坐在一张桌子旁,每对哲学家之间放着五根筷子。
任何哲学家想吃饭时,他/她都必须从他/她的左右手拿筷子
设计一种无死锁和无饥饿的方式让哲学家自由进食

尝试1:
对于哲学家i,当他思考完毕后,他拿起左边的筷子、右边的筷子,吃东西,然后放下左边的筷子、右边的筷子。
在这里插入图片描述
死锁:所有哲学家同时拿起左筷子,无人能继续

尝试解决这个死锁:
如果右筷子拿不到,让哲学家放下左筷子,稍后再试
没有死锁 ⇒ 也可能有活锁:所有哲学家左筷子拿起,放下,拿起,放下,…………

尝试2:
使用互斥锁mutex将拿筷子-吃-放筷子变成一个原子操作。
在这里插入图片描述
这确实可以没有死锁问题,但是本来同一时间至少有两个人能吃东西,现在只能一个人了,效率降低。

Tanenbaum Solution

在这里插入图片描述

#define N 5
#define LEFT ((i+N-1) % N)
#define RIGHT ((i+1) % N)#define THINKING 0
#define HUNGRY 1
#define EATING 2int state[N];
Semaphore mutex = 1;
Semaphore s[N];void philosopher( int i ){while (TRUE){Think( );takeChpStcks( i );Eat( );putChpStcks( i );}
}void takeChpStcks( i )
{wait( mutex );state[i] = HUNGRY;safeToEat( i );signal( mutex );wait( s[i] );  // 如果safeToEat不成功,等待左右来唤醒他
}void safeToEat( i )
{if( (state[i] == HUNGRY) && (state[LEFT] != EATING) && (state[RIGHT] != EATING) ) {state[ i ] = EATING;signal( s[i] ); // 这一步是用于唤醒左右去吃}
}void putChpStcks( i )
{wait( mutex );state[i] = THINKING;safeToEat( LEFT );safeToEat( RIGHT );signal( mutex );
}

有限进食者 Limited Eater

如果最多允许 4 位哲学家坐在桌子旁(留一个空位)⇒ 死锁就不会发生
在这里插入图片描述
(chpstck=s(1)[5]指的是对五个筷子,都设置chpstk为S(1))

同步的实现 Synchronization Implementations

POSIX信号量

Unix下信号量的常见实现
头文件:#include <信号量.h>
编译标志:gcc something.c –lrt,代表“实时library”
基本用法:初始化一个信号量,在信号量上执行 wait() 或 signal()

pthread 互斥量和条件变量 Mutex and Conditional Variables

pthread 的同步机制
互斥量(pthread_mutex):

  • 二进制信号量(即等效的信号量(1))。
  • 锁:pthread_mutex_lock()
  • 解锁:pthread_mutex_unlock()

条件变量( pthread_cond ):

  • wait:pthread_cond_wait()
  • signal:pthread_cond_signal()
  • broadcast:pthread_cond_broadcast()

其他

支持线程的编程语言会有某种形式的同步机制

例子:
Java:所有对象都有内置锁(mutex)、同步方法访问等
Python:支持互斥量、信号量、条件变量等
C++:在C++11中添加了内置线程; 支持互斥量、条件变量

7. 内存抽象 Memory Management:Memory Abstraction

7.1 内存基础

硬件

物理内存存储:
随机存取存储器 (Random Access Memory, RAM),可以被视为一个字节数组。每个字节都有一个唯一的索引,称为物理地址physical address

一个连续的内存区域:连续地址的间隔an interval of consecutive addresses
在这里插入图片描述

在这里插入图片描述
在说内存时通常是指RAM。
Flash RAM不是很robust,通常用在PC,而不是server。

进程的内存使用

在这里插入图片描述
在这里插入图片描述

可执行文件通常包含:代码(用于文本区域)、数据布局data layout(用于数据区域)

在这里插入图片描述

通常,一个进程中有两种类型的数据:

  • 瞬态数据 Transient Data
    仅在有限的时间内有效,例如,在函数调用期间。
    例如,参数,局部变量。
  • 持久数据 Persistent Data
    在计划期间有效,除非明确删除(如果适用)
    例如 全局变量、常量变量、动态分配内存

两种类型的数据部分都可以在执行期间增长/缩小。

操作系统的作用

操作系统处理以下与内存相关的任务:

  • 为新进程分配内存空间
  • 管理进程的内存空间(如进程需要更多的内存空间)
  • 保护进程各自的内存空间(如chrome每一个tag是一个进程,不能读其他进程的信息)
  • 给进程提供与内存相关的系统调用
  • 管理内部使用的内存空间

Key Topics

  • Memory Abstraction:为内存访问提供逻辑接口
  • Contiguous Memory Allocation:分配和管理连续的内存块
  • Disjoint Memory Allocation:在不相交的区域分配和管理内存
  • Virtual Memory Management:使用辅助存储作为扩展内存区域

7.2 内存抽象

实际地址-没有内存抽象

假设一个进程直接使用物理地址:即没有内存抽象

使用示例代码,让我们思考 :
如何访问进程中的内存位置?
多个进程可以正确共享物理内存吗?
进程的地址空间是否可以轻松保护?

优点

在这里插入图片描述
内存访问很简单
程序中的地址 == 物理地址
无需转换/映射
在编译期间固定地址

缺点

在这里插入图片描述
如果两个进程占用相同的物理内存:
冲突:两个进程都假设内存从 0 开始
⇒ 难以保护内存空间

逻辑地址

改进:地址搬迁 Address Relocation

【这是由OS做的】

进程加载到内存时重新计算内存引用:
例如,为进程 B 中的所有内存引用添加 8000 的偏移量(进程 B 在地址 8000 处开始)

问题:
加载时间慢
不容易区分内存引用memory reference和普通整数常量normal iteger constant

改进:Base + Limit Registers

【这是由CPU做的】

  1. 使用特殊寄存器作为所有内存引用的基础:称为基本寄存器 Base Register
    在编译期间,所有内存引用都编译为相对于该寄存器的偏移量
    在加载时,基址寄存器被初始化为进程内存空间的起始地址

  2. 添加另一个特殊寄存器来指示当前进程的内存空间范围:称为极限寄存器 limit register
    所有内存访问都根据限制检查以保护内存空间完整性
    超过范围会引发segementation fault。
    在这里插入图片描述

问题:

  • 访问地址 Adr:实际 = 基础地址 + Adr
  • 检查实际地址 < 有效边界

因此,每次内存访问都会产生一个加法和比较

这个想法非常有用:
后来推广到分割机制segmentation mechanism,内存抽象:进程 A 和 B 中的地址 4096 不再是同一个物理位置

在程序中嵌入物理内存地址是个坏主意

逻辑地址的思想:

  • 逻辑地址 == 进程如何查看其内存空间
  • 逻辑地址 != 一般的物理地址
    相反,需要逻辑地址和物理地址之间的映射
  • 每个进程都有一个自包含的、独立的逻辑内存空间

7.3 连续内存分配

进程在执行期间必须在内存中

  • 存储内存概念 store memory concept
  • 加载存储内存执行模型 Load-store memory execution model

让我们假设:

  1. 每个进程占用一个连续的内存区域
  2. 物理内存足够大,可以容纳一个或多个具有完整内存空间的进程

这些假设在后面的主题中不会再重复说。

多任务、context切换和交换 Multitasking, Context Switching & Swapping

多任务处理需要允许多个进程同时在物理内存中,这样我们就可以从一个进程切换到另一个进程。

当物理内存已满时,通过以下方式释放内存:删除终止的进程 / 将阻塞的进程交换到辅助存储secondary storage(harddisk / SSD)。

内存分区 Memory Partitioning

内存分区:分配给单个进程的连续内存区域

两种分配分区的方案:

  • 固定大小的分区
    物理内存被分成固定数量的分区
    一个进程将占用其中一个分区
  • 可变大小分区
    根据进程的实际大小创建分区
    操作系统跟踪占用和空闲的内存区域,必要时进行拆分和合并splitting and merging

固定大小分区

在这里插入图片描述
如果一个进程没有占用整个分区,剩余的空间就都被浪费了,这些浪费的空间称为内部碎片internal fragmentation
优点:

  • 易于管理
  • 快速分配
  • 每个空闲分区都一样 ⇒ 无需选择

缺点:

  • 分区大小需要足够大以包含最大的进程
  • 较小的进程会浪费内存空间 ⇒ 内部碎片

可变大小分区

在这里插入图片描述
空闲内存空间称为空洞 hole
通过进程创建/终止/交换 ⇒ 往往有大量的空洞,称为外部碎片 external fragmentation ⇒ 导致本来够放的空间变得支离破碎不够放了
通过移动占用的分区来合并空洞可以创建更大的空洞(更有可能有用)

  • 优点:
    灵活并消除内部碎片
  • 缺点:
    需要在操作系统中维护更多信息
    需要更多时间来找合适的区域

分配算法 Allocation Algorithm

假设操作系统维护一个分区和空洞列表 a list of partitions and holes
定位大小为 N 的分区的算法:

  • 搜索大小为 M > N 的空洞。几种变体:
    • 首次拟合 First Fit
      取第一个足够大的空洞
    • 最合适Best-Fit
      找到足够大的最小孔
      可以按大小排列holes,然后找第一个大于N的hole
      这种做法可能导致M-N非常小,以至于不能有其他用处了
    • 最差拟合Worst-Fit
      找到最大的洞
  • 把洞分成N和M-N
    N 将是新的分区
    M-N 将是剩余空间 ⇒ 一个新洞

合并和压缩 Merging and compaction

当一个占用的分区被释放时,如果周围有洞,就与相邻的洞合并

也可以使用压缩compation:移动占用的分区以合并洞(不能太频繁调用,因为它非常耗时)

Example

操作系统为分区信息维护一个链表:使用 First-Fit 算法
在这里插入图片描述
在这里插入图片描述

动态分配算法:伙伴系统 Buddy system

伙伴内存分配算法提供高效的:

  • 分区拆分 partition splitting
  • 自由分区(洞)定位 locating good match of free partition (hole)
  • 分区解除分配和合并 patition de-allocation and coalescin

思想:

  • 空闲块被重复分成两半以满足请求,这两半形成兄弟块(伙伴块sibling blocks, buddy blocks)。
  • 当伙伴块都空闲时,可以合并形成更大的块

在这里插入图片描述
保留一个数组 A[0…K],其中 2^K 是最大可分配块大小

  • 每个数组元素 A[J] 是一个链表,用于跟踪大小为 2^J 的空闲块
  • 每个空闲块仅由起始地址标识

在实际实现中,也可能有一个最小的可分配块大小 ,因为太小的区块管理起来不划算(我们将在讨论中忽略这一点)

伙伴系统:分配算法 Buddy System: Allocation Algorithm

分配大小为 N 的块:

  1. 找到最小的 S,使得 2^S >= N
  2. 访问 A[S] 以获得空闲块
    • 如果存在空闲块:
      • 从空闲块列表中删除块
      • 分配块
    • 否则
      • 从 S+1 到 K 中找到最小的 R,使得 A[R] 有一个空闲块 B
      • 对于(R-1 至 S)
        • 反复拆分 B ⇒ A[S…R-1] 有一个新的空闲块
      • 转到第 2 步

伙伴系统:释放算法 Deallocation Algorithm

要释放块 B:

  1. 检查 A[S] ,其中 2 S = = s i z e o f B 2^S == size of B 2S==sizeofB
  2. IF B 的好友 C 存在且空闲
    • a. 从列表中删除 B 和 C
    • b. 合并 B 和 C 以获得更大的块 B’
    • c. 转到步骤 1,其中 B ⇐ B’
  3. Else(B的好友还没空闲) ⇒ 将 B 插入到 A[S] 中的列表中

如何找到Buddy

观察到:

  • 给定块地址 A 是 xxxx00…00
  • 拆分后得到2个一半大小的块:
    B = xxxxx0…00 和 C = xxxxx1…00

Example:
A = 0 (000000),大小 = 32
拆分后:
B = 0 (000000),大小 = 16
C = 16 (010000),大小 = 16
因此,两个块 B 和 C 是大小为 S 的伙伴,B和C的第S位是补码complement,B 和 C 的第 S 位之前的前导位相同。

Example:
假设:

  • 最大的块是 512 (2^9)
  • 最初只有一个大小为 512 的空闲块
    在这里插入图片描述
    在这里插入图片描述

8. 内存管理:不相交内存 Disjoint Memory Schemes

在第 17 课中,我们在两个假设下讨论了内存管理:

  • (1)进程在连续的物理内存中
  • (2)物理内存足够大,可以容纳整个进程

让我们在本章中删除假设(1):进程内存空间现在可以位于不相交的物理内存位置,通过分页机制paging

分页 Paging:基本思想

物理内存被分成固定大小的区域(由硬件决定),称为物理帧 physical frame

进程的逻辑内存同样被分成相同大小的区域,称为逻辑页logical page

在执行时,进程的页面被加载到任何可用的内存帧中
⇒ 逻辑内存空间保持连续
⇒ 占用的物理内存区域可以分离

页表:查找机制

在连续内存分配中,跟踪进程的使用情况非常简单:base + limit (起始地址和进程大小)

在分页机制下:
逻辑页面 ⇐ ⇒ 物理帧映射不再简单,需要一个查找表来提供转换,这种结构称为页表

有一个page table register来定位page table的起始点。
在这里插入图片描述

逻辑地址转换

程序代码使用逻辑内存地址
但是,要实际访问该值,需要物理内存地址

要在分页方案中定位物理内存中的值,我们需要知道:
F:物理帧号
offset:从物理帧开始的位移

实际地址 = F x 物理帧大小 + 偏移量

逻辑地址转换:基本技巧

两个重要的设计简化了地址转换计算

  1. 将帧大小(page大小)保持为 2 的幂
  2. 物理帧大小 == 逻辑页面大小
    在这里插入图片描述

逻辑地址转换:公式

给定:
页面/帧大小为 2^n
m位逻辑地址

逻辑地址 Logical Address LA:
p = LA 的最高有效 m-n 位
d = LA 的剩余 n 位

使用 p 找到帧号 f
根据页表等映射机制

物理地址 PA:
PA = f*2^n + d

示例:4 个逻辑页,8 个物理帧

在这里插入图片描述

分页:观察

分页消除外部碎片 external fragmentation
没有剩余的物理内存区域
所有空闲的帧都可以使用,没有浪费

分页仍然可以有内部碎片 internal fragentation
逻辑内存空间不能是页面大小的倍数

逻辑和物理地址空间的清晰分离
灵活性大
地址转换简单

实施分页

常见的纯软件解决方案:
操作系统将页表信息与进程信息一起存储(例如 PCB)
进一步理解 ⇒ 进程的内存context == 页表

问题:
每个内存引用都需要两次内存访问 ⇒ 慢

  • 第一次访问是读取索引页表条目以获取帧号
  • 第二次访问是访问实际的内存项

分页机制:硬件支持

现代处理器提供专门的芯片上的组件来支持分页,称为转换后备缓冲区 (Translation Look-Aside Buffer, TLB),TLB 充当一些页表条目的缓存。very small, fully associated

使用 TLB 进行逻辑地址转换:

  • 使用页码关联搜索 TLB
  • 找到条目(TLB-Hit):获得帧号以生成物理地址
  • 未找到条目(TLB-Miss):访问整个页表的内存,检索到的帧号用于生成物理地址和更新 TLB。

在这里插入图片描述

TLB:对内存访问时间的影响

假设:
TLB 访问耗时 1ns
主存访问需要 50ns
如果 TLB 包含整个页表的 40%,则平均内存访问时间是多少?

内存访问时间
= TLB hit + TLB miss
= 40% x (1ns + 50ns) + 60%(1ns + 50ns + 50ns)
= 81ns

Note: 忽略填入 TLB 条目的开销和缓存的影响。

TLB 和进程切换

进一步理解:TLB 是进程硬件context的一部分

当发生context切换时:TLB 条目被刷新,这样新进程就不会被错误转换。

因此,当进程恢复运行时,会遇到许多 TLB miss 来填入 TLB。所以在最初可以放置一些条目,例如 放入一些代码页以减少 TLB miss。

分页方案:保护

基本的分页方案可以很容易地扩展,以保护进程之间的内存,通过:

  • 访问权限位 access-right bits
  • 有效位 valid bit

访问权限位 access-right bits 每个页表条目都附加了几个bit来标识 【是否可写、可读、可执行】。例如,包含代码的页面应该是可执行的,包含数据的页面应该是可读可写的等。
可以根据访问权限位 检查内存访问权限。

我们可以观察到:
所有进程的逻辑内存范围通常是一样的;然而,并非所有进程都使用了整个范围 ⇒ 某些page超出一些进程的范围

有效位 valid bit 附在每一页table 条目,标识进程访问是否可以有效访问页面。
操作系统将在进程运行时设置有效位。
通过有效位检查内存访问权限:超出范围的访问将被操作系统处理。

分页方案:页面共享 Page Sharing

页表可以允许多个进程共享同一个物理内存帧。在页表条目中使用相同的物理帧号。

可能的用法:

  • 共享代码页:一些代码会被多个进程使用,例如C标准库,系统调用等
  • 实现写时复制copy-on-write:像我们在前面“进程抽象”章节讨论的,父进程和子进程可以共享一个页面,直到其中一个进程尝试更改它的值。

分页方案:页面共享
在这里插入图片描述

在这里插入图片描述

分页方案:写时复制
在这里插入图片描述

分割机制 segmentation scheme

为什么内存错误通常被称为分段错误segmentation fault

细分方案:动机

到目前为止,进程的内存空间被视为单个实体。然而,一个进程中实际上有许多不同用途的内存区域。
例如,在典型的 C 程序中:

  • 用户代码区域
  • 全局变量区域:静态数据(只要程序执行就一直存在)
  • 堆区域:动态数据(只要用户不免费就一直存在)
  • 堆栈区域
  • 库代码区域等

某些区域在执行时可能会增长/缩小。例如,栈区、堆区、库代码区。

很难将不同的区域放置在连续的内存空间中,并且仍然允许它们自由增长/收缩,也很难检查一个区域中的内存访问是否在范围内。

分割方案:基本思想

在这里插入图片描述

将区域分成多个内存段:进程的逻辑内存空间现在是内存段segments的集合

每个内存段:有自己的名字name(便于引用),有限制limit(表示内存段的范围)
所有内存引用现在指定为:段名 + 偏移量
在这里插入图片描述

分段:逻辑地址转换

  • 每个段都映射到一个连续的物理内存区域,有一个基地址和一个范围限制。
    -分段名称通常表示为单个数字,称为段 id
  • 逻辑地址<SegID, Offset>:
    • SegID用于在段表中查找段的<Base, Limit>
    • 物理地址 PA = Base + Offset
    • 偏移量 < 有效访问上限范围 (这里的limit是最大offset)

在这里插入图片描述
硬件支持:
在这里插入图片描述

Segementation 优缺点

优点:
每个段是一个独立的连续内存空间
⇒ 可以独立增长/收缩
⇒ 可以独立保护/共享

缺点:
分段需要可变大小的连续内存区域 ⇒ 可能导致外部碎片

Important observation:
分段不等于分页,他们解决不同的问题

分页分割

P 好; S也不错;那就S+P吧!

分页分割:基本思想

直观的下一步是将分段与分页结合起来

基本理念:
每个段现在由几个页面而不是一个连续的内存区域组成。本质上,每个段都有一个页表。

内存段可以通过分配新页面来增长,然后添加到它的页表中,收缩也同理。
在这里插入图片描述
每个segment都有一个page table。
在这里插入图片描述
在这里插入图片描述

Summary

讨论了两种流行的内存管理方案
两者都允许逻辑地址空间位于分离的物理区域
分页将逻辑地址拆分为固定大小的页面
存储在固定大小的物理内存帧中
分段根据用途将逻辑地址分成可变大小的段
存储在可变大小的物理分区中

9. 虚拟内存管理 Virtual Memory Management

本章内容

  • 虚拟内存
    动机
    基本理念
    页面错误
  • 虚拟内存的常见应用
    请求分页
  • 虚拟内存管理方面
    页表结构
    页面替换算法
    帧分配

虚拟内存:Motivation

我们对内存使用的最后一个假设:物理内存足够大,可以完全容纳一个或多个进程逻辑内存空间

这个假设太严格了:
如果进程的逻辑内存空间是>>然后是物理内存呢?
如果在物理内存较少的计算机上执行相同的程序会怎样?

虚拟内存:基本思想

观察:
与物理内存相比,二级存储的容量要大得多

基本理念:
将逻辑地址空间分成小块:一些块驻留在物理内存中,其他存储在辅助存储中。

最流行的方法:
上一课分页机制的扩展:逻辑内存空间分割成固定大小的页面,有些页面可能在物理内存中,其他在二级存储。
在这里插入图片描述

扩展分页机制

基本思路不变: 使用页表将虚拟地址转换为物理地址
加入新内容:

  • 区分两种页面类型
    • 内存驻留(物理内存中的页面)
    • 非内存驻留(二级存储中的页面) ⇒ 在页表条目中使用(内存常驻的?)bit
  • CPU 只能访问内存常驻页面:
    • Page Fault:当 CPU 尝试访问非内存驻留页面时
    • 操作系统需要将非内存驻留页面带入物理内存

访问页面 X:一般步骤

  1. 检查页表:
    页面 X 是内存常驻的吗?
    是:访问物理内存位置。完毕。
    否:继续下一步
  2. 页面错误:陷阱到操作系统(通知操作系统)
  3. 在二级存储中定位页面 X
  4. 将页面 X 加载到物理内存帧中
  5. 更新页表
  6. 转到步骤 1 重试

虚拟内存访问:插图

虚拟内存:理由

观察:二级存储访问时间>>物理内存访问时间

如果内存访问在大多数情况下导致页面错误,需要加载非内存驻留页面,称为颠簸thrashing

我们怎么知道颠簸不太可能发生?
相关:我们如何知道页面加载后,它可能对未来的访问有用?

回顾:局部性原则 Locality Principles

大多数程序表现出以下行为:
大多数时间只花在一小部分代码上
在一段时间内,只访问相对较小部分的数据

形式化为局部性原则 locality principles
时间局部性Temporal locality:被使用的内存地址很可能被再次使用
空间局部性Spatial locality:接近已使用地址的内存地址可能会被使用

虚拟内存和局部性原则

利用时间局部性:页面加载到物理内存后,很可能在不久的将来会被访问,加载页面的成本已摊销amortized
利用空间局部性:页面包含可能在不久的将来访问的连续位置,以后访问附近的位置不会导致页面错误。

然而,总有例外 。程序由于不良设计或恶意设计 ⇒ 表现恶劣。

虚拟内存:总结

  • 将逻辑内存地址与物理内存完全分开,物理内存量不再限制逻辑内存地址的大小。
  • 更有效地使用物理内存,当前不需要的页面可以在辅助存储上。
  • 允许更多进程驻留在内存中,提高 CPU 利用率,因为有更多进程可供选择运行。

更多

更深入的看几个方面:

  • 庞大的页表,大的逻辑内存空间 ⇒ 如何构建页表以提高效率?
    页表结构 Page Table Structures
  • 每个进程都有有限数量的常驻内存页面 ⇒ 需要时应该替换哪个页面?
    页面替换算法 Page Replacement Algorithms
  • 有限的物理内存帧 ⇒ 如何在进程之间分配?
    帧分配策略 Frame Allocation Policies

页表结构 Page Table Structures

页表结构

页表信息与进程信息一起保存,占用物理内存空间

现代计算机系统提供了巨大的逻辑内存空间

  • 4GiB(32bit)是常态,8TiB或更多现在是可能的
  • 巨大的逻辑内存空间 ⇒ 大量的页面
  • 每个页面都有一个页表条目 ⇒ 巨大的页表

大页表的问题

  • 高开销
  • 分片页表:页表占用几个内存页

页表结构:直接分页

直接分页:将所有条目保存在一个表中

在逻辑内存空间中有 2^p 页

  • p 位指定一个唯一页面
  • 2^p 页表条目 (page table entries, PTE),每个包含:物理帧号,附加信息位(有效/无效、访问权限等)

例子:
虚拟地址:32 位,页面大小 = 4KiB(2^12 bytes, D = 12bits)
P = 32 – 12 = 20 (P + D = 32)
PTE 的大小 = 2 个字节
页表大小 = 2^20 * 2 字节 = 2MiB

2 级分页:基本思想

观察:
并非所有进程都使用全部虚拟内存空间 ⇒ 完整页表是一种浪费

基本理念:

  • 将整个页表拆分为区域
  • 仅使用少数区域:随着内存使用量的增长,可以分配新的区域
  • 这个想法类似于将逻辑内存空间拆分为页面
  • 需要一个目录来跟踪区域,类似于 页表 ⇐ ⇒ 页

将页表拆分为更小的页表,每个都有一个页表编号page table number

如果原始页表有 2^P 项:
使用 2^M 个较小的页表,需要 M 位来唯一标识一个页表
每个较小的页表包含 2^(P-M) 个条目

继续看较小的页表:需要单页目录,页目录包含 2^M 个索引来定位每个较小的页表

在这里插入图片描述

2 级分页:优势

我们可以在页面目录中有空条目 ⇒ 不需要分配相应的页表

使用与上一个示例相同的设置:
假设只使用了 3 个页表
开销 = 1 页目录 + 3 个较小的页表

倒页表:基本思想

页表是每个进程的信息:内存中有M个进程,有M个独立的页表。

Observation:
只能占用N个物理内存帧
在 M 个页表中,只有 N 个条目是有效的
巨大的浪费:N << M 页表的开销

Idea:
物理帧到 <pid, page#> 的单个映射

  • pid = 进程 id , page# = 页码
  • page# 在进程中不是唯一的
  • pid + page# 可以唯一标识一个内存页

在普通页表中,条目按页码排序:要查找页面 X,只需访问第 X 个条目
在倒排页表中,条目按帧号排序:要查找页面 X,需要搜索整个表

优势:Huge saving:一张表用于所有流程
坏处:slow translation
在这里插入图片描述

页面替换算法

假设在page fault期间没有可用的物理内存帧:需要驱逐(释放evict, free)一个内存页
当页面被释放时: 【检查dirty bit】
干净页clean page:未修改 ⇒ 无需回写
脏页dirty page:修改 ⇒ 需要回写

寻找合适替换页面的算法

  • 最佳(选择)Optimum, OPT
  • 先进先出 FIFO
  • 最近最少使用 Least Recently Used
  • 第二次机会(时钟)Second-Chance (Clock)

内存引用建模

在实际内存参考中:逻辑地址 = 页码 + 偏移量 logical address = page number + offset

但是,要研究页面替换算法,只有页码是很重要的。

⇒ 为了简化讨论,内存引用通常被建模为内存引用字符串,即页码序列 memory reference strings, i.e. a sequence of page numbers

页面替换算法:评价

在这里插入图片描述

𝒑 = 页面错误的概率
Tmem = 内存驻留页的访问时间
Tpage_fault = 发生缺页时的访问时间

因为 Tpage_fault >> Tmem,需要降低 p 以保持 Taccess 合理

自己尝试找到𝒑,如果:
Tmem = 100ns,Tpage_fault = 10ms,Taccess = 120ns
好的算法应该减少缺页中断的总数。

最佳页面替换 (Optimal Page Replacement, OPT)

思想:

  • 更换最长时间不会再次使用的页面
  • 保证最少的页面错误数

不幸的是,无法实现,因为需要内存引用相关的后面才能得到的信息 future knowledge

但他还是有用的:
作为其他算法比较的基础
越接近 OPT == 算法越好

示例:OPT(6 个页面错误)
在这里插入图片描述

FIFO页面替换算法

思想:
内存页面根据其加载时间被释放
释放最旧的内存页

实现:
操作系统维护一个常驻页码队列
如果需要替换,则删除队列中的第一页
在缺页TRAP期间更新队列

这种算法易于实施,无需硬件支持。

示例:FIFO(9 个页面错误)
在这里插入图片描述

先进先出:问题

如果物理帧增加(例如更多 RAM),号码页错误应该减少。

FIFO违反了这个简单的直觉,使用 3 / 4 帧尝试:1 2 3 4 1 2 5 1 2 3 4 5

相反的行为(↑ 帧 ⇒ ↑ 页面错误),被称为 Belady 的异常现象 Belady's Anomaly

原因:FIFO 不利用时间局部性 temporal locality

最近最少使用的页面替换 (Least Recently Used Page Replacement, LRU)

思想:

  • 利用时间局部性:替换最长时间未使用的页面
  • 期望在短时间内重用页面:已经有一段时间没有使用了 ⇒ 很可能不会再次使用

Note:
逼近 OPT 算法,总体效果不错
不受Belady’s Anomaly的影响

示例:LRU(7 个页面错误)
在这里插入图片描述

LRU:实施细节

实现 LRU 并不容易:需要以某种方式跟踪“最后访问时间” + 需要大量的硬件支持

  • 方法 A - 使用计数器:
    • 逻辑“时间”计数器,每次内存引用都会递增
    • 页表条目有一个“使用时间”字段
      • 每当发生引用时存储时间计数器值
      • 用最小的“使用时间”替换页面
    • 问题:
      • 需要搜索所有页面
      • “使用时间”永远在增加(可能溢出!)
  • 方法 B - 使用“栈”:
    • 维护一个页码栈
    • 如果引用了第 X 页
      • 从栈中移除(对于现有条目)
      • 压入栈顶
    • 将页面移到堆栈底部,无需搜索所有条目
    • 问题:
      • 不是纯栈:可以从栈中的任何位置删除条目
      • 很难在硬件中实现

Second-Chance页面替换 (CLOCK)

思想:

  • 修改了 FIFO,为被访问的页面提供第二次机会
  • 现在每个 PTE 都维护一个“参考位”:1 = 已访问,0 = 未访问
  • 算法:
  1. 最旧的 FIFO 页被选中
  2. 如果参考位 == 0 ⇒ 页面被替换
  3. 如果参考位 == 1 ⇒ 页面有第二次机会
    - 参考位清零
    - 到达时间重置 ⇒ 页面作为新页面载入
    - 选择下一个 FIFO 页,转到步骤 2
  • 退化为先进先出算法。当所有页面都有参考位 == 1。

Second-Chance:实施细节

使用循环队列circular queue来维护页面:带有指向最旧页面(受害者页面victim page)的指针

要查找要替换的页面:
前进直到具有“0”参考位的页面
指针经过时清除参考位
在这里插入图片描述

示例:时钟(6 个页面错误)
在这里插入图片描述

帧分配

考虑:
有N个物理内存帧
有 M 个进程竞争帧
在 M 个进程之间分配 N 帧的最佳方法是什么?

简单的方法:
平等分配Equal Allocation:每个进程获得 N/M 帧
比例分配Proportional Allocation

  • 进程大小不同(内存使用情况)
  • 设 size_p = 进程 p 的大小,size_total = 所有进程的总大小
  • 每个进程分配到 size_p/size_total*N 帧

帧分配和页面替换

页面替换算法的隐含假设:【在导致页面错误的进程 的页面中】选择受害者页victim page,称为本地替换local replacement

如果可以【在所有物理帧中】选择受害者页面 ⇒ 进程 P 可以通过在替换期间释放 Q 的帧从进程 Q 中获取一个帧,被称为全局替换global replacement

本地与全局替换

本地替换:
优点: 分配给进程的帧保持不变 ⇒ 多次运行之间的性能稳定
缺点: 如果分配的帧不够 ⇒ 阻碍进程的进行

全局替换:
优点:
允许进程之间的自我调整 ⇒ 需要更多帧的进程可以从其他进程获取
缺点: 错误进程会影响其他进程;分配给进程的帧可能因运行而异。

帧分配和抖动 Frame Allocation and Thrashing

物理框架不足 ⇒ 进程抖动:大量 I/O 将非常驻页面带入 RAM

很难找到正确的帧号:

  • 如果使用全局替换:
    一个抖动进程从其他进程“窃取”页面
    导致其他进程抖动(Cascading Thrashing)
  • 如果使用本地替换:
    抖动可以限制在一个进程中
    但是单个进程会占用 I/O 并降低其他进程的性能

找到正确的帧数

Observation:
一个进程引用的页面集合在一段时间内是相对固定的,称为局部性locality
但是,随着时间的推移,页面集可能会发生变化。

例子:
当一个函数正在执行时,引用可能在:该函数中的局部变量、参数、代码;这些页面定义了函数的位置。
函数终止后,引用将更改为另一组页面

工作集模型 Working Set Model

使用对局部性locality的观察:
在一个新的locality:一个进程将导致页面集的缺页中断
使用帧中的页面集:在进程转移到新位置之前没有/很少出现缺页中断

工作集模型:
定义工作集窗口 d:一个时间间隔
W(t, d) = 时间 t 间隔内的活动页面
为 W(t, d) 中的页面分配足够的帧以减少页面错误的可能性

在这里插入图片描述

工作集模型的准确性直接受 d 的选择影响

  • 太小:可能会漏掉当前本地的页面
  • 太大:可能包含来自不同地方的页面

在这里插入图片描述

假设:
delta = 有5个内存引用的窗口

W(t1, d) = {1, 2, 5, 6, 7} ⇒ 需要5个帧
W(t2, d) = {3, 4} ⇒ 需要2个帧

可以尝试使用不同的d值。

Summary

  • 虚拟内存:WHY & HOW
  • 讨论了虚拟内存管理的不同方面
    使用不同的页表结构来减少页表开销
    使用不同的页面替换算法来减少页面错误
    帧分配如何影响进程的页面错误

http://www.ppmy.cn/news/773356.html

相关文章

【网络安全带你练爬虫-100练】第8练:json数据的最小项提取

目录 一、目标1&#xff1a;爬取指定json中数据 二、目标2&#xff1a;循环取json中数据 三、目标3&#xff1a;提取每个数据中的某一项 四、网络安全小圈子 一、目标1&#xff1a;爬取指定json中数据 爬取data里数据 核心代码&#xff1a; dirt1 json.loads(res.text)pr…

启动游戏提示缺少(或丢失)xinput1_3.dll的解决办法

在我们打开游戏的或者软件的时候&#xff0c;电脑提示“找不到xinput1_3.dll&#xff0c;无法继续执行此代码”怎么办&#xff1f;相信困扰着不少小伙伴&#xff0c;我再在打开吃鸡的时候&#xff0c;然后花了一上午的时候时间研究&#xff0c;现在终于知道xinput1_3.dll文件是…

雨听 | 解决连接蓝牙后谷歌浏览器无声音(其他应用有声音)问题

问题描述 在使用蓝牙音箱时&#xff0c;打开谷歌浏览器播放网页视频&#xff0c;发现没有声音。但是使用其他浏览器或者应用都是有声音的。 解决办法 1.鼠标移动至电脑音量图标&#xff0c;右键&#xff0c;选择"打开音量合成器" 2.关闭静音

电子计算机说明文作文,电脑事物说明文

电脑事物说明文500字【篇一】 提起电脑&#xff0c;相信大家都非常熟悉。电脑又叫电子计算机。如今&#xff0c;随着科技的发展&#xff0c;电脑已走进千家万户。 电脑一般分为两大类;一类是个人电脑&#xff0c;个人电脑有台式的&#xff0c;笔记本的&#xff0c;最新式的是平…

计算机主机箱工作电流,电脑使用常识

原标题&#xff1a;电脑使用常识 关于电脑的常识有很多&#xff0c;今天蝈蝈给大家列举20条你不得不知道的电脑常识&#xff0c;希望对你有所帮助&#xff01; 1、在关闭电脑时&#xff0c;请不要直接按电源进行强制性关机。请先关闭运行的程序&#xff0c;再关机。强制性关机有…

linux电脑主机国产,“小皮匠”换工作电脑,国产“中国芯”迷你主机能否够用?...

我平时除了做数码自媒体&#xff0c;其实还是一个手工达人&#xff0c;只要空下时间我就会玩玩皮子&#xff0c;做做皮包、皮具。这些年我亲手做了不少东西&#xff0c;下面都是我的“作品”。 顺便展示一下我的工具(专业皮友不要笑话哈&#xff0c;这些都是入门级别的&#xf…

360浏览器下载安装网易有道词典鼠标取词插件导致电脑蓝屏问题解决办法

问题详情 在360浏览器下载网易有道词典鼠标取词插件2.5.2&#xff0c;下载途中电脑鼠标失灵而后出现蓝屏&#xff0c;多次尝试重装360浏览器仍无法解决&#xff1f; 产生原因 多次重装仍然出现蓝屏&#xff0c;原因在于安装360浏览器时使用了一个自定义的路径。 解决方式 安装…

五分钟了解--指纹浏览器背后的一切

​​什么是指纹浏览器&#xff1f;在回答这个问题之前&#xff0c;我们首先需要了解什么是IP地址、什么是Cookie&#xff0c;以及什么是浏览器指纹。 首先我们需要搞清楚的是&#xff0c;当打开浏览器访问一个网站的时候&#xff0c;网站如何判断你是谁&#xff1f;或者说&…