抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

操作系统

是统一管理计算机硬件和软件资源的计算机程序, 用于管理硬件, 提供用户交互的软件系统.

微内核设计 -> 微服务设计

操作系统历史

一台计算机工作不一定需要操作系统.

单板电脑(Single Board Computer, SBC), 例如: 树莓派(Raspery PI)

  • 第一批计算机没有操作系统, 例如: ENIAC
  • 大型机操作系统. 和现在的操作系统相比, 当时的更像库和框架
  • 1969年 unix, 1990年开始普及
  • 2000年MacOS发布第一个版本, 前身 Unix-like 的 Next step 系统

Android 操作系统

基本功能

  • 处理器资源

  • 存储器资源

  • IO设备资源

  • 文件资源

  • 一种程序

    • 管理 硬件设备, 资源以及应用程序(管理能力)
    • 将硬件能力, 资源抽象成服务让应用程序使用(抽象能力)
  • 用户无需面向硬件接口编程

  • IO设备管理软件, 提供读写接口

  • 文件管理软件, 提供操作文件接口

操作系统实现了对计算资源的抽象.

操作系统提供了用户与计算机之间的接口.

相关概念

  • 并发性
    • 并行: 指两个或多个事件在 同一时刻 发生
    • 并发: 指两个或多个事件在 同一时间间隔 发生
    • 通过 多道程序设计 实现
  • 共享性
    • 例如多个程序共享主存资源
    • 资源共享
    • 互斥共享
  • 虚拟性
    • 把物理实体转变为若干格逻辑实体
    • 物理实体是真实存在的, 逻辑实体是虚拟的
    • 虚拟计数主要有 时分复用技术 和 空分复用技术
  • 异步性
    • 在多道程序环境下, 允许多个进程并发执行
    • 进程在使用资源时可能需要等待或放弃
    • 进程的执行并不是一气呵成的, 而是以走走停停的形式推进

时间复用技术

  • 资源在时间上进行复用, 不同程序并发使用
  • 多道程序分时使用计算机的硬件资源
  • 提供资源利用率

虚拟处理器技术

  • 借助多道程序设计技术
  • 为每个程序建立进程
  • 多个程序分时复用处理器

虚拟设备技术

  • 物理设备虚拟为多个逻辑设备
  • 每个程序占用一个逻辑设备
  • 多个程序可以通过逻辑设备并发访问

空分复用技术

  • 实现虚拟磁盘, 虚拟内存等
  • 提高资源利用率, 提升编程效率

虚拟磁盘技术

  • 物理磁盘虚拟成逻辑磁盘
  • 使用更加安全, 方便

虚拟内存技术

  • 在逻辑上扩大程序的存储容量
  • 使用比实际内存更大容量
  • 大大提升编程效率

开机后需要接管CPU的控制权

MBR – 位于硬盘初始位置的一段记录, 也是一个程序.

DDR – Double Data Rate, 双倍速率.

需要接收和发送消息给硬件

缓冲区. pHead 和 pTail 重叠就会发出鸣声. 通过 pTail 进行消费.

需要管理和调度应用

抽象是多方面的, 比如应用, 文件, 资源等…

需要让用户可以参与管理

内核

  • 内核是连接操作系统和硬件和软件的桥梁, 它掌控着计算机的一切资源.

主流操作系统的设计

权限问题

  • 没有权限控制
  • 解决方案
    • 拆分权限(端口权限, 文件权限, 操作权限等)
    • 一个过程多态–区分[内核态][用户态]

用户态: 模拟用户在操作系统, 权限会较低.

切换到内核态时, 会做权限的检查.

Console/Terminal/TTY

内核设计: B/S结构设计

分配内存是一个有中断参加的过程.

  • 内核管得越少, 扩展性就越强. 比如说调度算法, 文件系统得扩展性.
  • 类比现实系统的设计也是如此, 分布式系统最底层是在做日志的记录(账簿系统), 游戏引擎的最底层是在做渲染 – 底层做的事情要少而精.
  • OSI有7层模型.
  • 程序语言的最底层是机器指令, 最上面是高级语言.

进程管理

  • 进程是系统进行资源分配和调度的基本单位.
  • 系统系统对一个正在运行程序的抽象, 是操作系统调度的最小单位. 一个应用可以有多个进程.
  • 早期计算机CPU核心只有一个, 程序在共享时间片段, 操作系统需要提供一个模型去管理所有程序, 所以就诞生了最核心的概念.
  • 进程作为程序独立运行的载体, 保障程序的正常执行.
  • 进程的存在使得操作系统资源利用率大幅度提升.

进程管理的 寄存器, 是用来维护当被暂停时, 存储当前的程序指针.

这样看来, 其实进程是一个数据结构.

进程是对计算机的一种抽象, 是在软件上搭建的系统. 模拟, 映射着计算机相关资源.

与虚拟机对比

进程的实体

进程控制块(PCB): 描述和控制进程运行的通用数据结构. 记录当前状态和控制进程运行的全部信息.

PCB是进程能够独立运行的基本单位.

PCB是操作系统进行调度经常被读取到的信息.

PCB是常驻内存, 存放在系统专门开辟的PCB区域内.

  • 标识符, 唯一标记一个进程, 用于区别其它进程.
  • 状态, 如运行态.
  • 程序计数器, 指向进程即将被执行的下一条指令的地址.
  • 内存指针, 程序代码, 进程数据相关指针.
  • 上下文数据, 进程执行时处理器存储的数据.
  • IO状态信息, 被进程IO操作所占用的文件列表.
  • 记账信息, 使用处理器时间, 时钟总和等.

总结上面一共四类

  1. 进程标识符
  2. 处理机状态
  3. 进程调度信息
  4. 进程控制信息

进程之间通信–IPC

线程

进程–Process, 线程–Thread

轻量级的进程, 是抽象计算的, 是分配计算资源的最小单位. 进程是分配资源的最小单位.

一个经常有多个线程.

  • 线程是操作系统进行运行调度的最小单位.
  • 包含在进程之中, 是进程实际运行的工作单位.
  • 一个进程可以并发多个进程, 每个线程执行不同的任务.

进程的线程共享进程的资源.

思考: 多进程分时共享

上面方案产生的问题:

  • 多个进程如何共享文件和内存
  • 多个进程间的切换成本

所以, 在进程中创造一种更加轻量的执行单位, 它们共享进程绝大部分信息, 拥有独立的程序指针, 堆栈, 寄存器, 状态字等.

执行单元, 可以理解为抽象出一块CPU. 线程更加轻量, 所以切换成本更低.

POSIX = Portable OS Interface, 接口就是相同事物的行为.

报错 /mnt/e/WorkSpace/my-code-base/Computer/os/thread/main.c:21: undefined reference to 'pthread_create', 如果使用 gcc 编译时添加-lpthread参数, 如果是使用 cmake 的话, 在 CMakeLists.txt 文件中添加set(CMAKE_C_FLAGS -pthread).

通过OS创建. 像一些语言, Java就会自己创建线程.

线程切换

  • 线程主动交出控制权(yield), 或终止
  • 保存信息(线程表)
  • 本地选择另一个线程执行(由调度器决定)

五状态模型

  • 创建
    • 分配PCB
    • 插入就绪队列
    • 当进程拥有PCB, 但是资源还没准备好的时候就是创建状态
    • fork函数可以创建进程
  • 就绪
    • 当进程被分配除了CPU以外的所有必要资源之后
    • 只要获得CPU的使用权就可以立即运行
    • 系统中多个处于就绪状态的进程通常排成一个队列–就绪队列
  • 执行
    • 进程获得CPU, 其程序正在执行
    • 在单处理机, 在某个时刻只能有一个进程是处于执行状态
  • 阻塞
    • 其它设备未就绪(某些资源无法获得, 比如打印完成, 读取磁盘)而无法继续执行
    • 从而放弃CPU的状态
    • 多个处于阻塞状态的进程–阻塞队列
  • 终止
    • 系统清理
    • PCB归还

举例子: 读取硬盘的数据, 需要等硬盘的数据读取到缓冲区, 这时候就会产生硬件中断, 进程处于阻塞状态, 当资源到了, 进程会先回到就绪状态, 先排队, 等到了才进入运行状态.

阻塞态不能到运行态, 要先进入就绪态.

就绪态也不能进入阻塞态. 因为处于排队状态.

进程响应中断

Step1 保存当前状态

Step2 跳转OS中断响应程序

Step3 保存当前寄存器

Step4 设置新的栈指针

Step5 执行中断服务程序

Step6 决定下一个程序

这个步骤由OS的调度决定

Step7 恢复SP和寄存器

Step8 执行下一个进程

  • 进程随时都可以准备保存自己的状态(SP/PC/寄存器)
  • 进程随时都可以被切换
  • 无法指定马上执行哪个进程, 都需要排队

多核和多道程序

为了提高CPU利用率

多个进程共享一个CPU, 在内存中抽象出多个并发执行的[道]. 上图红色部分表示进行了IO请求.

CPU 利用率

  • 大体有两类工作, 处理I/O(输入输出)和进行计算
    • I/O举例: 读写磁盘, 读写内存, 等待打印机Ready
    • 计算: 加法运算, 逻辑运算, 浮点运算

多道程序的意义

由此可见, 多道程序设计可以提升CPU利用率.

进程的同步

生产者-消费者问题, 并发执行就可能出现问题, 临界区资源.

根源问题: 彼此之间没有通信.

进程同步: 对竞争资源在多个进程间进行使用次序的协调.

临界区资源: 是共享资源, 但是无法被多个线程共同访问的共享资源.

  • 空闲让进: 资源没有占用, 允许使用
  • 忙则等待: 资源有占用, 请求进程等待
  • 有限等待: 保证有限时间能够使用资源
  • 让权等待: 等待时, 进程需要让出CPU

同步的方法

  • 消息队列
  • 共享存储
  • 信号量

线程的同步

进程内多线程也需要同步.

  • 互斥量
  • 读写锁
  • 自旋锁
  • 条件变量

Linux的进程管理

  • 前台进程
    • 具有终端, 可以和用户进行交互的进程
  • 后台进程
    • 没有占用终端的, 基本不喝用户交互, 优先级比前台进程低
    • 在指令后面添加 & 启动后台进程
    • kill -9 %1 干掉进程
    • 2>&1 > /dev/null/ & 重定向输出
  • 守护进程
    • daemon进程是特殊的后台进程
    • 很多守护进程在系统引导的时候启动, 一直运行知道系统关闭
    • 例如: crond(定时任务), httpd, sshd, mysqld. 一般以d结尾的都是守护进程

进程ID是一个非负整数, 最大值由操作系统决定.

父子进程关系. 可以使用 pstree 指令查看.

ID 为0的idle进程, 是系统创建的第一个进程.

ID 为1的init进程, 是0号进程的子进程, 完成系统初始化.

init进程是所有用户进程的祖先进程

进程的标记

# 查看进程的所有父子关系
ps -ef --forest
# 按照使用 cpu 的频率来排序
ps -aux --sort=-pcpu
# 按照使用 内存 的频率来排序
ps -aux --sort=-pmem
# top指令
top 
# top - 09:44:19 up 13 min,  0 users,  load average: 0.00, 0.00, 0.00
# Tasks:   5 total,   1 running,   4 sleeping,   0 stopped,   0 zombie
# %Cpu(s):  0.0 us,  0.0 sy,  0.0 ni, 99.9 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
# MiB Mem :   7959.7 total,   7559.0 free,     73.4 used,    327.3 buff/cache
# MiB Swap:   8192.0 total,   8192.0 free,      0.0 used.   7662.6 avail Mem

#   PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
#     1 root      20   0    1744   1084   1012 S   0.0   0.0   0:00.00 init
#     7 root      20   0    1756     76      0 S   0.0   0.0   0:00.00 init
#     8 root      20   0    1756     88      0 S   0.0   0.0   0:00.03 init
#     9 devlgq    20   0    9988   5032   3316 S   0.0   0.1   0:00.08 bash
#   875 devlgq    20   0   10876   3744   3224 R   0.0   0.0   0:00.00 top

# PR 进程优先级
# VIRT 进程的虚拟内存
#  TIME+ 进程运行的时间
# total 所有的内存
# free 空闲的内存
# used 已经使用的内存
#  buff/cache 已经缓存的内存

进程的调度

指计算机通过决策决定哪个就绪进程可以获得CPU的使用权.

调度是对资源管理和利用, 应用方面不仅仅是操作系统领域.

  • Yarn调度Hadoop集群
  • Quartz调度任务
  • Spring调度请求响应
  • React Fiber调度绘制任务
  • Apache Flink调度作业

本质

  • 资源的稀缺
  • 根据不同的场景找到最优解(类型Programing-规划问题)

并发调度问题

多个进程和线程竞争CPU, 2个或以上处于就绪状态.

多道程序设计

  • 保留旧的进行运行信息
  • 选择新进程. 准备运行环境并分配CPU

关注点

  • 被调度任务的特性(计算密集型 VS IO密集型)
  • 执行时机
    • 新任务何时执行
    • 任务临时终止如何选择下一个任务
    • 任务阻塞如何选择下一个任务
    • 发生中断时(外部环境变化时)如何响应
  • 调度算法
    • 抢占式算法
    • 非抢占式算法

目标

通用目标

  • 公平, 每个进程公平地分享CPU份额(相对)
  • 策略强制执行, 保证规定的策略被执行
  • 平衡, 保持系统尽可能忙碌

不同系统的不同目标

  • 批处理系统(吞吐量, 周转时间, CPU利用率)
    • 不可抢占
  • 交互式系统(响应性, 体验)
    • 分时, 抢占
    • Windows, Android
  • 实时系统(精准, 稳定)
    • 完成时间确定
    • 车床控制

进程调度机制

  • 就绪队列的排队机制
    • 按一定方式排成队列, 以便调度程序可以最快找到就绪进程
  • 选择运行进程的委派机制
    • 调度程序以一定的调度策略选择就绪程序, 将CPU资源分配给它
  • 新老进程的上下文切换机制
    • 保存当前进程的上下文信息, 装入被委派执行进程的运行上下文就

进程调度分类

  • 非抢占式调度
    • 处理器一旦分配给某个进程, 就让该进程一直使用下去, 任务不分时
    • 调度程序不以任何原因抢占正在被使用的处理器
    • 相对公平
    • 专用系统
    • 频繁切换, 开销大
  • 抢占式调度
    • 允许调度程序以一定的策略暂停当前运行的经常(时间片用完)
    • 保存好旧进程的上下文信息, 分配处理器给新进程
    • 不公平
    • 通用系统
    • 切换少, 开销小

调度算法

  • 先来先服务算法
    • 队列, 按顺序
  • 短进程有限调度算法
    • 优先选择就绪队列估计运行时间最短的进程
    • 相同优先级可以考虑此算法. 通过执行的历史记录来进行评估.
    • 最短优先
  • 高优先权调度算法
    • 进程附带优先权, 优先选择权重高的进程
    • 使得紧迫的任务可以优先处理
    • 优先级
    • 优先级
  • 时间片轮转调度算法
    • 按先来先服务的原则排列就绪进程
    • 每次从队列取出待执行进程, 分配一个时间片执行
    • 相对公平的调度算法, 但不能保证及时响应用户
    • round_robin

优先级队列

实现结构: 最大堆(Max Heap)

树也可以使用数组来表示, 但是树必须是饱和的.

堆插入一个新值的步骤.

先插入堆最后的位置(数组). 而堆的父节点要比子节点的大.

把100与父节点交换. 往上比较交换.

提取最大值, 也就是数组的第一个值.

然后把2填补过去, 然后往下比较交换, 与较大的子节点进行比较.

复杂度分析

在 H 的范围内, 节点全部填满做复杂度分析.

可以看出树的高度和2的对数相关. 1T数据, 30次就能走完了. 提取最大值的速度很快.

Priority Queue

优先级队列提取全部

哲学家就餐问题

宏: 字符串查找替换

所有人同时拿起叉子–死锁问题

思考, 映射到mutex和semaphore.

拿起叉子的过程

放下叉子的过程

死锁

  • 竞争资源
  • 进程调度顺序不当

死锁四个必要条件

  • 互斥条件
    • 进程对资源的使用是排他性的使用
    • 某资源只能由一个进程使用, 其它进程需要使用只能等待
  • 请求保持条件
    • 进程至少保持一个资源, 又提出新的资源请求
    • 新资源被占用, 请求被阻塞
    • 被阻塞的进程不释放自己保持的资源
  • 不可剥夺条件
    • 进程获得的资源在未完成使用前不能被剥夺
    • 获得的资源只能由进程自身释放
  • 环路等待条件
    • 必然存在进程-资源的环形链

死锁的处理

防止: 破坏四个条件之中的一个即可.

  1. 程序一开始就申请所有资源, 这样就破坏了请求保持条件
  2. 一个进程请求新的资源得不到满足时, 必须释放占有的资源, 就是说可以被剥夺
  3. 可用资源线性排序, 申请必须按需要递增申请

银行家算法

P2 满足需求, 可以先P2获取资源先, 等P2完成后, 释放资源后, 其它进程就有更多资源使用了.

存储管理

  • 确保计算机有足够的内存处理数据
  • 确保程序可以从可用内存中获取一部分内存使用
  • 确保程序可以归还使用后的内存以供其它程序使用

内存分配与回收

需要合理分配, 如果用户程序不小心操作了操作系统内存中的数据, 有可能造成系统崩溃; 应用之间的访问也会变的不安全.

单一连续分配

  • 单一连续分配, 最简单的内存分配方式
  • 只能在单用户, 单进程的操作系统使用

固定分区分配

  • 是支持多道程序的最简单的存储分配方式
  • 内存空间划分为若干个固定大小的区域
  • 每个分区只提供一个程序使用, 互不干扰

动态分区分配

  • 更加实际需要, 动态分配
  • 相关数据结构和分配算法

动态分区空闲表数据结构

动态分区空闲链数据结构

连续的空闲区域可合并到一个节点; 节点需要记录可存储的容量.

动态分区分配算法

  • 首次适应算法(FF算法)
    • 从头开始遍历链表, 没有找到合适的空闲区则分配失败
    • 问题: 每次都是从头部开始, 使得头部地址不断被分配
  • 最佳适应算法(BF算法)
    • 空闲区链表按照容量大小排序
    • 遍历空闲区链表找到最佳适合空闲区
    • 避免大材小用
  • 快速适应算法(QF算法)
    • 要求有多个空闲区链表
    • 每个空闲区链表存储一种容量的空闲区

内存回收

四种情况

进程使用内存划分

  • 本质原因: 资源的稀缺
  • 解决方案: 基地址+界限寄存器, 交换技术, 虚拟化技术等

地址空间–Address Space: 把真实的内存划分成不同的区间

  • 进程间内存需要隔离
  • 地址空间是进程用来寻址的独立地址集合

解决方案1: 基地址寄存器+界限寄存器

每个进程运行在自己的地址空间. 有界限范围.

进程1切换到进程2的时候, 中断后会把进程1的寄存器都保存下来, 调度系统再去调度进程2.

缺点

  • 每次都需要做一次加法(基地址寄存器)和一次比较(界限寄存器)
  • 进程太多, 内存不够分怎么办? 内存超载.
    • 交换(Swapping)
    • 虚拟内存(VistualMemory)

段页式存储管理

操作系统管理进程的空间的方式. 虚拟内存的基础.

如果不对内存空间进行划分, 交换内存会有问题.

本质上都是为了解决资源稀缺的问题.

页式存储管理

字块是相对物理设备的定义

页面则是相对逻辑空间的定义

  • 将进程的逻辑空间等分成若干大小的页面
  • 相应得把内存空间分成与页面大小的物理块
  • 以页面为单位把进程空间装进物理内存中分散的物理块

碎片化问题

所以, 页面大小应该适中, 过大难以分配, 过小内存碎片过多

页面大小通常是 512B ~ 8K

页表

  • 记录进程逻辑空间与物理空间的映射

问题: 现代计算机系统中, 逻辑地址很大, 这样页表会很占用空间.

如32位逻辑地址的空间分页系统, 规定页面大小为4kb, 则在每个进程页表可达1M(2^20)个, 如果每个页表占用1Byte, 故每个进程仅仅页表就要占用1MB的内存空间.

分级页表, 按需区加载即可.

如果一段连续的逻辑分布再多个页面中, 将大大降低执行效率

段式存储管理

  • 将进程逻辑空间划分成若干段(非等分)
  • 段的长度由连续的长度决定
  • 主函数MAIN, 子程序段X, 子函数Y等.

段式管理和页式管理都离散地管理了进程的逻辑空间.

  • 页是物理单位, 段是逻辑单位
  • 分页是为了合理利用空间, 分段是满足用户要求
  • 页大小由硬件固定, 段长度可动态变化
  • 页表信息是一维的, 段表信息是二维的

段页式存储管理

  • 分页可以有效提高内存利用率(虽然说有内存碎片问题)

  • 分段可以更好满足用户需求

  • 两者结合, 形成段页式存储管理

  • 先将逻辑空间按段式管理分成若干段

  • 再把段内空间按页式管理等分成若干页

虚拟内存

虚拟化: 看似是无穷, 其实上资源是有限的.

原因

  • 有些进程需要的实际内存很大, 超过物理内存.
  • 多道程序设计, 使得每个进程可用物理内存更加稀缺.
  • 不可能无限增加物理内存, 物理内存总有不够的时候.

实现

  • 把程序使用内存使用数据结构划分, 切割成更小的块, 将一块块分配, 将部分暂时不使用的内存放置到辅存.
  • 维护一张映射表, 这张表让进程认为自己是连续的.

程序的局部性原理

CPU访问存储器时, 无论时存取指令还是存取数据, 所访问的存储单元都趋于聚集在一个较小的连续区域中.

所以程序运行时, 无需全部装入内存, 转载部分即可.

如果访问页不在内存, 则发出缺页中断, 发起页面置换.

从用户层看, 程序拥有很大的空间, 即是虚拟内存.

总得来说, 实际是对物理内存的补充, 速度接近于内存, 成本接近于辅存.

虚拟地址空间

页表

页表项

  • FRAME: 真实内存的一块.

  • 访问位: 可以判断是否可以被swapping到磁盘中去.

  • [在不在]位, 如果不在会触发缺页中断, 通过内核把真实的内存拿进来.

举例子:

10000 其实是个虚拟地址

内存管理单元

MMU – Memory Management Unit

通过专门的硬件电路解决内存映射计算的问题.

MMU工作流程

MMU的页表由来

  • 操作系统告诉 MMU 页表在内存中的位置 -> MMU 加载页表 -> MMU会自己利用页表进行地址转换
  • CPU 对MMU中页表中的条目数量大小是有限制的, 有的会几个M, 也可以更大
  • 操作系统内核中有负责MMU管理模块

缺页中断

页面调度不在内核, 因为内核是基于最小程序来设计的.

多级页表

使用多级页表节省空间.

  • 如果只有一级, 比如: 4G/4kb就需要1m的页表条目(以4kb为一块进行划分)
  • 如果是两级
    • 顶级页表需要1k条目
    • 每个二级页表需要1k条目, 一共1k个二级页表, 这样反而还多了1k个
  • 多级页表是个跳跃结构, 因为进程不会把所有4G内存都占满, 用到了再添加, 这样就节省了空间.

多级页表虽然节省了空间, 但是会增加 MMU 的计算次数. 所以引入了快表.

快表

是一个小型硬件设备–也叫做转换检测缓冲区(Translation Lookaside Buffer), 可以加速 MMU 的读写.

命中率会很高.

虚拟内存置换算法

  • 先进先出算法(FIFO)
  • 最不经常使用算法(LFU)
  • 最近最少使用算法(LRU)

主存页面替换时机–主存缺页的时候.

  • 替换策略发生在Cache-主存层次, 主存-辅存层次
  • Cache-主存层次的替换策略主要是为了解决速度问题
  • 主存-辅存层次主要是解决容量问题

Linux存储管理

Buddy内存管理算法

  • 基于计算机处理二进制的优势, 具有极高的效率
  • 主要是为了解决内存外碎片问题

页内碎片: 已经被分配出去的内存空间大于请求所需的内存空间, 不能被利用的内存空间就是内存碎片

页外碎片: 还没有分配出去, 不属于任何进程, 但是由于大小而无法分配给申请内存空间的新进程的内存空闲块

  • 向上取整为2的幂大小. 70k -> 128k, 129k -> 256k

  • 伙伴系统. 一片连续内存的”伙伴”是相邻的另一片大小一样的连续内存.

  • 创建一系列空闲块链表, 每一种都是2的幂

  • 对内存的处理都是以2的幂来处理的, 计算机处理2的幂时速度时最快

其实是将内存外碎片问题 -> 内存内碎片问题

Linux 交换空间

  • 交换空间(Swap)是磁盘的一个分区
  • Linux 物理内存满时, 会把一些内存交换到Swap空间
  • Swap空间时初始化系统时配置的

特点

  • 冷启动内存依赖(冷启动程序只在启动时要使用的那一块内存)
  • 系统睡眠依赖
  • 打井程依赖

这个概念其实和虚拟内存很相似.

程序语言的内存管理模型

段和段之间随着内存的增长碰撞怎么办? 虚拟化.

进程是对计算机资源的虚拟化.

线程是对CPU的虚拟化.

内存是对物理内存的虚拟化.

段是对虚拟内存(页表)的虚拟化.

所以, 资源稀缺的时候, 就可以想到虚拟化, 让资源可以无限抽象(例如货币).

Stack分配内存其实是离散的.

生活在虚拟世界, 不断虚拟, 虚拟到不知道底层是什么.

  • 一种数据结构(二叉树, 然后用数组去描述了).
  • 操作系统中是程序对内存的一种抽象(杂乱无章的对象).

上存的是对象的地址, 而对象是存在上的, 而堆是由组成, 每个段在页表有对应的映射, 而页表就映射到物理内存.

堆如何分配有很多算法.

如何管理内存

如果分配的基本单位不能被申请的内存整除, 那么怎么办?

另一种方式, 基于链表, 把内存划分成不同大小. 这样可以根据对象的大小来选择, 一个个填进去.

现在主流做法–slot

程序语言也用slot法管理内存吗?

  • 部分没有虚拟机的语言, 创建对象时在 cache-slot 上的. 比如 c 的 malloc 就是直接分配一个合适大小的 cache, 依赖于操作系统
  • 还有一些虚拟机的语言(v8, jvm), 有自己的内存管理
  • 另一方面, 程序语言不需要像操作系统一样预留大量类型的slot等待分配, 因为程序大小不一, 功能也不一样

内存回收这一块, 没有通用解!!

回收a,c的时候, 如何尽量不去影响b和d.

年代分层, 解决整理时STW的问题.

垃圾回收算法

图和引用计数

不使用的对象怎么处理?

  • 如何衡量一个对象不再被使用了

可以使用二维数组存下图.

广度优先搜索(BFS算法)

产生的问题

  • 基于扫描的引用计数策略性能太慢了–基于图策略
  • 每个对象需要多维护一个引用(ref)–开销问题
  • 如果GC程序是另外的线程, ref的维护会产生竞争条件, 处理GC时会 STW

标记, 扫地, 整理

gc使用的重要场景

  • 内存需要被反复利用的场景
  • 大量的对象被分配和回收
  • 高并发场景

标记清除算法

GC角度看程序世界

对于GC而言, 除了自己做的事情, 其它事情都属于Mutator.

STW : 一个串行的模型. 一次回收较多, 消耗时间更多

Increment Update: 增量更新模型. 每次更新少点, 所以体验更好.

Concurrent Update: 并发更新模型.

很难做到不STW, 但是可以缩短STW的时间, 让用户感知程序是一个连续的模型.

三色标记-Tri-Color Mark

白: 需要回收; 黑: 不需要回收; 灰: 中间地带.

GC的目的就是跑赢mutation.

文件管理

  • 文件系统
  • 文件的逻辑结构
  • 辅存的存储空间分配
  • 目录管理

文件系统

一种抽象机制, 通过给存储在磁盘上的数据每个起一个名字, 每个叫做一个文件, 提供根据文件名操作这些信息的方法(读写修改等).

目录是一种特殊的文件, 它用来对文件进行分类.

由于不同的存储设备, 物理结构不一样, 所以操作系统在这之上进行了抽象–物理块, 目前使用比较多的是 4kb 的块.

物理块是操作系统最底层的抽象了. 在这之上就可以构建文件系统了.

空闲物理块

文件系统的布局

引导块 一般硬盘最低的地址空间(MBR).

魔数(Magic Number), 也叫[幻数], 原指原子核中质子数和中子数的某个特定数值. 处于这个数值的原子核会很稳定.

Java.class 文件, 开头4个字节: 0xCAFEBABE

DOS可执行文件开头, 0x00004550

文件的逻辑结构

  • 磁盘上信息的一种抽象. 把信息抽象成拥有名字的一个集合
  • 不同文件系统对于文件的表示方式不同, 但是提供相似的操作接口(POSIX标准)
    • Portable Operation System Interface

逻辑结构的文件类型

  • 有结构文件: 由一个定长的记录和可变长的记录组成; 定长记录存储文件格式, 文件描述等结构化数据项; 可变存储文件的具体内容.
    • 文本文件
    • 文档
    • 多媒体文件
  • 无结构文件: 也称为流式文件; 文件内容长度以字节为单位.
    • 二进制文件
    • 链接库

顺序文件: 按照顺序存放在存储介质中的文件.

磁带的存储特性使得磁带只能存储顺序文件.

顺序文件时所有逻辑文件中存储效率最高的.

顺序文件对增删改效率很低.

索引文件

  • 可变长文件不适合使用顺序文件格式存储
  • 索引文件是为了解决可变长文件存储的文件格式

辅存的存储空间分配

分配方式

  • 连续分配
    • 顺序读取文件内容非常容易, 速度很快
  • 链表分配
    • 将文件存储在离散的盘块中
    • 需要额外的存储空间存储文件的盘块链接顺序
  • 索引分配

连续分配

连续分配–碎片问题. 解决: 磁盘碎片整理.

链表分配

隐式分配

每一块有额外的开销, 所以会不足4kb.

显式分配

也是内存中的分配表, 简单来说就是在内存中维护了一份链表.

-1 说明文件结束了.

这个也是FAT(File Allocation Table)的工作原理.

  • 不支持高效的直接存储(FAT记录项多)
  • 检索时FAT表占用较大的存储空间(需要将整个FAT加载到内存)
    • 1T的硬盘, 每个块 4k 需要多大的 FAT 表?
    • 1T = 2^30^ k. 1T/4k * 4byte = 2^30^ / 4 * 4byte = 1G

索引分配

  • 把文件的所有盘块集中存储(索引–index-node)
  • 读取某个文件时, 将索引文件读取到内存即可
  • 每个文件拥有一个索引块, 记录所有盘块信息
  • 索引分配方式支持直接访问盘块
  • 文件较大时, 索引分配方式具有明显优势

文件由索引节点组成, 而一个索引节点的大小是固定的. 需要的时候再载入内存, 而 FAT 是要整个载入内存.

要注意的是目录也是文件, 会有自己的inode.

如果文件的大小大于索引节点的大小, 会进行分级, 会是一个树状结构, 而树的查询效率和树的高度相关.

存储空间管理

空闲表, 空闲链表, 位示图

空闲链表

  • 把所有空闲盘区组成一个空闲链表
  • 每个链表节点存储空闲盘块和空闲的数目

位示图

0表示未使用; 1表示已经使用了

  • 位示图维护成本很低
  • 可以非常容易找到空闲盘块
  • 使用0/1比特位, 占用空间小

目录管理

目录用来管理文件的集合, 形成树状结构–目录树

任何文件和目录都只有一个路径. 下图是目录的结构:

从图可以知道, 文件除了在自身的属性, 在目录中也有会一些对应的属性.

寻址文件. 先读取目录的内容, 根据目录的内容描述再一步步找到对应的文件.

实际效率不够高. 如果一个目录下的文件很多, 这样的话, 链表就会很长, 查询的时候效率就会出现问题.

利用 哈希表 加速目录–文件查询

共享文件

文件共享 – 链接

硬链接/符号链接

软链接速度没有硬链接快, 因为两次寻址, 但是软链接是解耦的.

Linux文件系统

Linux的VFS, 虚拟文件系统, 可以挂载各种各样的文件系统.

  • FAT: File Allocation Table, 微软Dos/Windows使用的文件系统

  • NTFS: New Technology File System, WindowsNT环境文件系统

  • EXT2/3/4: Extended file system, 扩展文件系统, Linux的文件系统

EXT文件系统

  • 文件名不是存放在 Inode 节点上的, 而是存放在目录的 Inode 几点的
  • 列出目录文件的时候, 无需加载文件的Inode

Inode bitmap : Inode 的位示图; 记录已分配的 Inode 和为分配的 Inode

Data block : 存放文件内容的地方; 每一个block都有唯一的编号; 文件的 block 记录再文件的 Inode 上.

Block bitmap: 和 Inode bitmap 类似; 记录 Data block 的使用情况.

Superblock: 记录整个文件系统相关信息的地方; block和inode的使用情况; 时间信息, 控制信息等.

# 查看文件系统
df -T
# 查看 Inode 信息
dump2fs /dev/sdb > dump2fs.log
# 查看文件的具体信息. 修改名字再查看信息. 会发现 Inode 值是不变的, 也就是说 名字 不是文件的唯一标识.
stat dump2fs.log

Linux目录

  • bin 可执行二进制文件
  • etc 配置文件
  • home 用户的主目录
  • usr 系统应用目录. local 管理员安装的文件目录
  • opt 额外安装的可选应用程序所放置的位置
  • proc 虚拟文件系统目录, 是系统内存的映射. 里面数字就是进程. 一切皆文件的思想
  • root 超级管理员的主目录
  • sbin 存放二进制可执行文件, 只有root才能访问
  • dev 设备目录
  • mnt 系统管理员安装临时文件系统的安装点, 系统提供这个目录让用户临时挂载其他的文件系统
  • boot 系统引导时需要的文件
  • lib 系统运行时的库
  • var 用于存放运行时需要改变数据的文件

Linux 文件类型

  • 套接字(s)
  • 普通文件(-)
  • 目录文件(d)
  • 符号链接(l)
  • 设备文件(b, c)
  • FIFO(p)

VFS和基于日志的文件系统

虚拟文件系统(Virtual File System)

和进程对接的是 POSIX 的 API

任何架构的设计都离不开缓存的设计.

高速缓存区: 类似CPU的缓存, 内存管理的快表. 频繁使用的文件会被缓存.

由操作系统提供是因为操作系统能全局地看待问题.

删除文件操作

  1. 更新A所在的目录
  2. 释放A的inode
  3. 释放A占用的物理块(将物理块加入空闲列表/位图)

基于日志的文件管理

故障恢复

高并发下socket如何处理

select/poll/epoll

  • 场景: 有100w个socket连接需要处理
  • linux下socket连接抽象成文件(100w个文件句柄)
  • 系统其实就是不断检查100w个文件中有没有数据到来
  • 100w个socket并发很高要, 稍有不慎, 就造成系统雪崩

使用阻塞IO? 所谓阻塞是进程的阻塞. 肯定不行, 有巨大的调度成本, 包括切换和等待成本.

多路复用(Multiplexing): 多个信号复用一条链路.

把100w个socket分成1000组, 每组由一个线程负责.

select/poll 是异步模型, 内核有数据才通知线程.

select/poll 是阻塞模型, 发生后线程会进入阻塞状态.

句柄的遍历受数据结构的影响. select 用数组, poll使用链表.

本质问题: 在遍历过程中, 不断拷贝句柄到内核, 然后内核不断返回数据, 线程又要遍历返回的数据.

解决拷贝FD问题

增量传递, 一次传所有句柄, 之后可以增删.

允许增加操作系统内核的FD, 那么应该使用怎样的数据结构保证高效? insert, find, delete都在O(1)

  • 数组: insertO(1), findO(n), delete~O(n)
  • 链表: insertO(1), findO(n), delete~O(1)
  • 哈希表: 效果不好, 适合全集很大, 抽样很少的场景
  • 平衡树(二叉搜索树–红黑树): insertO(logn), findO(logn), delete~O(logn)

通过以上思路, 就出现了 epoll

  • 增量向内核传输FD(同理增加删除FD)
  • 内核返回事件(不是FD)
    • 事件中带有可以被读取的FD(避免线程遍历)

设备管理

广义的IO设备

对CPU而言, 凡是对CPU进行数据输入的都是输入设备.

对CPU而言, 凡是CPU进行数据输出的都是输出设备.

按照信息交换单位分类(ls指令列举出来的设备文件类型就是按照这个分类的)

  • 块设备(Block): 磁盘, SD卡
  • 字符设备(Char): shell, 打印机

IO设备的缓冲区(CPU与IO设备的速率不匹配)

  • 减少CPU处理IO请求的频率
  • 提高CPU与IO设备之间的并行性
  • 专用缓冲区只适用于特定的IO进程
  • 如果这样的IO进程较多时, 对内存的消耗也很大
  • 操作系统划出了可供多个进程使用的公共缓冲区, 称之为缓冲池.

缓冲池化, 缓冲区不是进程专属, 由缓冲池来管理.

SPOOLing 技术

  • 是关于慢速字符设备如何与计算机主机交换信息的一种技术.
  • 利用高速共享设备将低速的独享设备模拟为高速的共享设备.
  • 逻辑上, 系统为每个用户都分配了一台独立的高速共享设备.

所以该技术属于虚拟设备技术.

总结: SPOOLing技术把同步调用低速设备改为异步调用.

  • 在输入, 输出之间增加了排队转储环节(输入井, 输出井)
  • SPOOLing负责输入(出)井与低速设备之间的调度
  • 逻辑上, 进程是直接与高度设备进行交互的, 减少了进程的等待时间.

线程同步

竞争条件和临界区

临界区: 访问共享资源的程序片段, 而这些资源又不能同时被多个线程访问.

竞争条件

解决方案: 互斥, 例如: 给临界区上锁.

  • 最简单的解决方案就是让临界区互斥(mutual exclusion), 就是同时不会有两段程序在临界区. 严格来说, 应该满足四个条件
    1. 任何两个进程不同时在临界区
    2. 不对CPU数量和速度做任何假设
    3. 临界区外运行的进程不得阻塞其他进程
    4. 不得使进程过长等待临界区

互斥–屏蔽中断

进程的切换依赖中断, 所以屏蔽中断可以阻止进程切换. 将屏蔽中断的能力下放给用户进行临界区管理.

这样做会有问题, 屏蔽的是一个核的, 中断是跟着CPU核走的, 多核还是会有问题; 如果临界区出现异常了, 没办法处理.

锁的硬件基础

TSL 和 XCHG

基于硬件指令一般是基于冲突检测的乐观并发策略: 先进行操作, 如果没有其他线程争用共享数据, 操作就成功了, 如果产生了冲突, 就进行补偿, 不断重试.

乐观并发策略需要硬件指令集的发展才能进行, 需要硬件指令实现: 操作+冲突检测的原子性.

TSL : Test and Set Lock, 测试并设置锁.

TSL RX, LOCK

作用是将一个内存字lock读到寄存器RX, 然后将lock设置为一个非0值.

执行原理:执行TSL指令的CPU会锁住内存总线, 禁止其他CPU在这个指令结束之前访问内存.

为了使用TSL指令, 需要使用一个共享变量lock来协调多内存的访问. lock=0时, 任何进程都可以使用TSL指令将其设置为非0值, 并读写共享内存; 而当操作结束时, 进程要使用move指令将lock的值重新设置为0.

enter_region:
    TSL REGISTER,LOCK       # 复制锁到寄存器并将锁设为1
    CMP REGISTER,#0         # 锁是0吗?
    JNE enter_region        # 若不是0,说明锁已被设置,所以循环
    RET                     # 返回调用者,进入临界区

leave_region:
    MOVE LOCK,#0            # 在锁中存入 0
    RET                     # 返回调用者

如果TSL原子操作没有成功, 则重新跳转到enter_region方法循环执行. 这个跟Peterson算法有点类似, 不过TSL可以支持任意多个线程的并发执行.

还有一个可以替换 TSL 的指令是 XCHG, 它原子性的交换了两个位置的内容, 例如: 一个寄存器与一个内存字.

enter_region:
    MOVE REGISTER,#1         # 把 1 放在寄存器中
    XCHG REGISTER,LOCK     # 交换寄存器和锁变量的内容
    CMP REGISTER,#0           # 锁是 0 吗?
    JNE enter_region            # 若不是 0 ,锁已被设置,进行循环
    RET                                 # 返回调用者,进入临界区

leave_region:
    MOVE LOCK,#0             # 在锁中存入 0 
    RET                                # 返回调用者

XCHG 的本质上与 TSL 的解决办法一样. 所有的 Intel x86 CPU 在底层同步中使用 XCHG 指令.

解决竞争条件

严格轮换法

根据 turn 的值判断是否能够访问临界区. 进程1访问完就修改 turn 的值, 进程2才能进入临界区.

假设进程2的非临界区运算很慢, 会导致进程1卡住. 也就是说, 一个进程非临界区的执行会导致另一个进程临界区的执行.

如何让非临界区不影响其它进程临界区的执行?

  • 每个线程进入临界区之前都标记自己对临界区资源感兴趣.
  • 进程执行完临界区后, 不应该主动交出控制权, 而是观察其它进程对临界区是否感兴趣.

Peterson算法

turn == pid 的判断防止两个感兴趣的进程同时进入临界区. interested 数组判断其它进程是否对临界区感兴趣.

信号量

  • 是一个控制同时访问一个资源的线程(进程)数量的抽象数据类型(不同语言对于信号量的实现可能会有差异).
  • Semaphore 内部封装一个整数(p)和一个睡眠线程队列(s), 并提供两个原子操作.
    • up()
      • p=p+1
      • 如果s.size()>0, 从s中选择一个唤醒并执行
    • down()
      • if(p==0){sleep(&semaphore)}
      • p–

信号量解决有界缓冲区问题: 生产者/消费者

empty: 有多少个空位. 生产者每生产一个就减一. 等于0的时候说明满了. 消费者相反.

full: 消费了多少个. 生产者每生产完一个就加一. 消费者相反.

mutex: 可以同时进入的线程数, 保护临界区. down和up操作都是原子操作, 不会出现竞争问题.

不保护临界区, 会导致数据的统计取决于精确的执行时序了.

上面的程序, 只用mutex实现也可以, 但是需要加判断, 而我们是希望程序是可读的, 信号量可以给程序提供足够的抽象.

多信号量造成的死锁

解决: 调换顺序.

互斥量

  • 问题: 两个线程的指令交叉执行
  • 互斥量保证先后执行
  • 初始值为1的semaphore, 是信号量的特殊情况

原子性: 一系列操作不可被中断的特性.

互斥量是线程同步最简单的方法.

互斥量也叫做互斥锁, 处于两态之一的变量: 解锁和加锁.

两个状态就保证了资源访问的串行.

操作系统直接提供了互斥量的API

demo文件: mutex_lock_demo.cpp

自旋锁

  • 自旋锁也是一种多线程同步的变量
  • 使用自旋锁的线程会反复检查锁变量是否可用
  • 自旋锁不会让出 CPU, 是一种忙等待的状态

死循环等待锁被释放.

  • 自旋锁避免了进程或线程上下文切换的开销.
  • 操作系统内部很多地方使用的是自旋锁.
  • 自旋锁不适合在 单核CPU 使用.

demo文件: spin_lock_demo.cpp

读写锁

从临界资源的角度思考. 是一种特殊的 自旋锁.

  • 允许多个读者同时访问资源以提高读性能.
  • 对于写操作是互斥的.

demo文件: rwlock_demo.cpp

条件变量

  • 一种相对复杂的线程同步方法, 但是有更灵活的使用场景
  • 条件变量允许线程睡眠, 直到满足某种条件
  • 当满足条件时, 可以向该线程发送信号, 通知唤醒

生产者与消费者问题

  • 缓冲区小于等于0时, 不允许消费者消费, 消费者必须等待
  • 缓冲区满时, 不允许生产者往缓冲区生产, 生产者必须等待
  • 生产者生产一个产品时, 唤醒可能等待的消费者
  • 消费者消费一个产品时, 唤醒可能等待的生产者

需要配置互斥量来使用.

demo文件: condition_demo.cpp

线程总结

  • TSL/XCHG是基础能力(否则就得用忙等待算法)
  • Semaphore是一种基于TSL成立的算法和数据结构(解决方案)
    • Mutex是Semaphore的一种特殊产出
  • 算法数据结构无穷无尽, 例如: AQS(Abstract Queued Synchronization)

CAS : Compare and Swap. 比较是否有冲突, 有就先同步再更新, 有点像git. 被叫做乐观锁

  • 读不需要同步
  • 写需要同步
  • 适合写操作少, 读操作较多的场景
  • 注意: 临界区的操作依然需要加锁

AQS

  • 一种Java的数据结构, 用于实现多数场景的并发同步模型
    • 可以用来实现Semaphore/Mutex
    • 可以用来实现更多的并发控制场景

进程同步

fork系统调用创建进程

  • fork 创建的进程初始状态与父进程一样
  • 系统会为 fork 的进程分配新的资源
  • fork 调用无参数
  • fork 会返回两次, 分别返回子进程id和0(子进程id是父进程返回, 0是子进程返回)

demo文件: fork_demo.cpp

共享内存

  • 在某种程度上, 多进程是共同使用物理内存的.
  • 但是由于操作系统的进程管理, 进程间的内存空间是独立的.
  • 进程默认是不能访问进程之外的内存空间的.
  • 共享内存允许不相关的进程访问同一片物理内存.
  • 共享内存是两个进程之间共享和传递数据的最快的方式.
  • 共享内存未提供同步机制, 需要借助其他机制管理访问.
  1. 申请共享内存
  2. 连接到进程空间
  3. 使用共享内存
  4. 脱离进程空间&删除

demo文件: shm文件夹.

共享内存是高性能后台开发中最常用的进程同步方式.

Unix 域套接字

  • 域套接字是一种高级的进程间通信的方法

  • Unix域套接字可以用于同一机器进程间通信

  • 套接字(Socket)原是网络通信中使用的术语

  • Unix系统提供的域套接字提供了网络套接字类似的功能

  • Nginx, uWSGI 都使用了这个技术

服务端

  1. 创建套接字
  2. 绑定(bind)套接字
  3. 监听(listen)套接字
  4. 接收&处理信息

客户端

  1. 创建套接字
  2. 连接套接字
  3. 发送信息

demo文件: socket文件夹.

会在指定路径创建一个s(套接字)类型的文件.

  • 提供了单机可靠的进程通信同步服务
  • 只能在单机使用, 不能跨机器使用

评论