空闲列表:回收和再利用

news2025/4/25 6:40:22

空闲列表:回收和再利用

在这里插入图片描述

手动与自动内存管理

  • 手动管理:程序员需要明确地分配和释放内存。
  • 自动管理:例如使用垃圾收集器(GC),它能够自动检测并回收未使用的对象,不需要程序员干预。

对于某些数据结构如B树,通过实现删除回调函数可以手动管理未使用的节点,从而可能避免使用GC。

未使用对象列表

为了有效地重复利用被释放的空间(如磁盘上的页面),通常会维护一个未使用空间的列表,称为自由列表(free list)或对象池(object pool)。这种机制简化了内存管理,因为它处理的是相同大小的块,而不是像malloc那样需要处理任意大小的请求。

嵌入式链表

一种简单的策略是使用嵌入式(侵入式)链表,其中指向下一个对象的指针直接存放在对象内部。这种方式的优点是不需要额外的空间来存储链表结构,但是它不兼容写时复制机制,因为更新时可能会覆盖原数据。

外部链表

另一种方法是将未使用页面的指针存储在一个外部的数据结构中。虽然这种方法需要额外的空间来维持这个数据结构本身,但它避免了嵌入式链表的一些问题。

自由列表作为日志

当自由列表只是未使用页面编号的日志时,添加新项仅需追加到列表末尾。然而,如何有效移除条目以防止列表无限增长是一个挑战。

解决这个问题的一种方法可能是周期性地整理自由列表,合并连续的空闲段,或者采用更复杂的数据结构如平衡树来高效管理这些信息。这有助于确保即使频繁地分配和释放页面,系统的性能也不会受到显著影响。

在磁盘上实现链表(如自由列表)需要仔细设计,以确保其高效性、可靠性和与写时复制(copy-on-write, COW)机制的兼容性。以下是对该问题的详细分析和解决方案。


自由列表的设计要求

自由列表是一个存储未使用页面编号的日志式数据结构。由于在写时复制树中,每次更新都会生成新节点并删除旧节点,因此自由列表需要同时支持添加和移除操作:

  1. 从末尾移除:如果从末尾移除条目,新增加的条目会覆盖旧数据。这需要额外的崩溃恢复机制(如第3章讨论的内容)。
  2. 从开头移除:如果从开头移除条目,则面临如何回收这些被移除条目占用的空间的问题。

为了解决这些问题,自由列表应基于页面进行管理,这样它就可以自我维护。这种基于页面的链表本质上是一个“展开的链表”(unrolled linked list),其中每个页面可以存储多个条目。


自由列表的核心特性

  1. 独立的数据结构

    • 自由列表是一个基于页面的链表。
    • 当需要扩展新节点时,它会尝试从自身获取一个空闲页面。
    • 被移除的节点会被重新添加到自由列表中以供重用。
  2. 每个页面的多条目存储

    • 每个页面包含多个条目(例如页面编号)。
    • 页面是原地更新的,但在页面内部仍然是追加写入(append-only)。
  3. 条目的追加与消费

    • 条目被追加到尾节点(tail node)。
    • 条目从头节点(head node)消费。
    • 这种设计使得尾节点更容易保持追加写入的特性。

自由列表的磁盘布局

每个节点的格式如下:

| next | pointers | unused |
| 8B   | n*8B     | ...    |
  • next 是指向下一个节点的指针,占 8 字节。
  • pointers 是存储页面编号的数组,每个编号占 8 字节。
  • unused 是未使用的空间。

定义如下常量和方法:

type LNode []byte

const FREE_LIST_HEADER = 8                   // "next" 指针大小
const FREE_LIST_CAP = (BTREE_PAGE_SIZE - FREE_LIST_HEADER) / 8 // 每页能存储的条目数

// 获取和设置方法
func (node LNode) getNext() uint64 { /* 返回 next 指针 */ }
func (node LNode) setNext(next uint64) { /* 设置 next 指针 */ }
func (node LNode) getPtr(idx int) uint64 { /* 获取第 idx 个条目 */ }
func (node LNode) setPtr(idx int, ptr uint64) { /* 设置第 idx 个条目 */ }

元页面(meta page)中存储了指向头节点和尾节点的指针。尾节点指针的存在是为了实现 O(1) 的插入操作。

磁盘布局示例:

                        first_item
                        ↓
head_page -> [ next | xxxxx ]
                ↓
             [ next | xxxxxxxx ]
                ↓
tail_page -> [ NULL | xxxxxx    ]
                           ↑
                        last_item

自由列表节点的更新

在没有自由列表的情况下,元页面是唯一需要原地更新的页面。这是写时复制机制使得崩溃恢复变得简单的原因之一。

引入自由列表后,会有两种额外的原地更新操作:

  1. 更新链表节点的 next 指针。
  2. 在链表节点中追加条目。

尽管链表节点是原地更新的,但在页面内部不会覆盖已有数据。因此,即使更新中断,元页面仍然指向相同的数据,不需要额外的崩溃恢复机制。此外,与元页面不同,链表节点的原子性不是必需的。


嵌入式链表的可行性

嵌入式链表也可以工作,但前提是 B+ 树节点中需要预留 next 指针的空间。这种方法虽然可行,但会导致写放大效应加倍(write amplification doubles)。具体来说:

  • 写放大效应是指为了完成一次逻辑写操作而实际写入的物理数据量。
  • 如果在 B+ 树节点中嵌入 next 指针,则每次更新不仅需要更新节点本身,还需要更新链表结构,从而增加了写入量。

总结

通过将自由列表设计为基于页面的链表,我们实现了以下目标:

  1. 高效的内存管理:自由列表能够自我维护,并且支持快速分配和释放页面。
  2. 与写时复制的兼容性:通过追加写入的方式避免覆盖数据,减少了崩溃恢复的复杂性。
  3. 灵活性:可以选择外部链表或嵌入式链表,根据性能和存储需求进行权衡。

这种设计为数据库系统中的磁盘空间管理提供了一种优雅的解决方案。

在这一节中,我们详细讨论了自由列表(Free List)的实现方法。自由列表是一种用于管理未使用页面的数据结构,它通过链表的形式组织页面编号,并支持高效的分配和回收操作。以下是对自由列表实现的逐步分析和总结。


自由列表接口设计

核心数据结构

自由列表是 KV 数据库中的一个额外组件:

type KV struct {
    Path string
    // 内部字段
    fd   int
    tree BTree
    free FreeList // 自由列表
    // ...
}

FreeList 是自由列表的核心结构,包含以下内容:

  1. 回调函数:用于管理磁盘上的页面。
    • get:读取页面。
    • new:追加新页面。
    • set:更新现有页面。
  2. 持久化数据:存储在元页面中。
    • headPagetailPage:分别指向链表头节点和尾节点。
    • headSeqtailSeq:单调递增的序列号,用于索引头节点和尾节点中的条目。
  3. 内存状态
    • maxSeq:保存上一次的 tailSeq,以防止消费刚刚添加的条目。
type FreeList struct {
    // 回调函数
    get func(uint64) []byte // 读取页面
    new func([]byte) uint64 // 追加新页面
    set func(uint64) []byte // 更新现有页面

    // 持久化数据
    headPage uint64 // 头节点指针
    headSeq  uint64 // 头节点序列号
    tailPage uint64 // 尾节点指针
    tailSeq  uint64 // 尾节点序列号

    // 内存状态
    maxSeq uint64 // 上一次的 tailSeq
}
接口方法
  • PopHead():从链表头部移除一个条目。
  • PushTail(ptr uint64):向链表尾部添加一个条目。
  • SetMaxSeq():将 maxSeq 更新为当前的 tailSeq,以允许消费新添加的条目。

自由列表的操作

1. 从头部消费

从自由列表中移除条目时,只需递增 headSeq。如果当前头节点为空,则移动到下一个节点,并将空节点回收到自由列表中。

func (fl *FreeList) PopHead() uint64 {
    ptr, head := flPop(fl)
    if head != 0 { // 回收空的头节点
        fl.PushTail(head)
    }
    return ptr
}

func flPop(fl *FreeList) (ptr uint64, head uint64) {
    if fl.headSeq == fl.maxSeq {
        return 0, 0 // 无法继续消费
    }
    node := LNode(fl.get(fl.headPage))
    ptr = node.getPtr(seq2idx(fl.headSeq)) // 获取条目
    fl.headSeq++

    // 如果当前头节点已空,移动到下一个节点
    if seq2idx(fl.headSeq) == 0 {
        head, fl.headPage = fl.headPage, node.getNext()
        assert(fl.headPage != 0) // 确保链表至少有一个节点
    }
    return
}
2. 向尾部追加

向自由列表尾部添加条目时,递增 tailSeq。如果当前尾节点已满,则创建一个新的尾节点,并确保链表始终至少有一个节点。

func (fl *FreeList) PushTail(ptr uint64) {
    // 将条目添加到尾节点
    LNode(fl.set(fl.tailPage)).setPtr(seq2idx(fl.tailSeq), ptr)
    fl.tailSeq++

    // 如果尾节点已满,添加新的尾节点
    if seq2idx(fl.tailSeq) == 0 {
        next, head := flPop(fl) // 尝试从头节点回收空节点
        if next == 0 {
            // 如果没有可回收的节点,则分配新节点
            next = fl.new(make([]byte, BTREE_PAGE_SIZE))
        }
        // 链接到新尾节点
        LNode(fl.set(fl.tailPage)).setNext(next)
        fl.tailPage = next

        // 如果头节点被移除,则将其添加到新尾节点
        if head != 0 {
            LNode(fl.set(fl.tailPage)).setPtr(0, head)
            fl.tailSeq++
        }
    }
}

关键点分析

1. 序列号的设计
  • headSeqtailSeq 是单调递增的序列号,用于唯一标识链表中的位置。
  • 通过 seq2idx(seq) 将序列号映射到节点内的索引,避免直接操作环形缓冲区的复杂性。
func seq2idx(seq uint64) int {
    return int(seq % FREE_LIST_CAP)
}
2. 自我管理

自由列表是自我管理的:

  • 被移除的头节点会被重新添加到尾部。
  • 在需要新节点时,优先尝试从头节点回收空节点,而不是直接分配新节点。
3. 崩溃恢复

由于自由列表的更新是基于追加写入的,即使中断也不会覆盖已有数据。因此,崩溃恢复机制与写时复制树一致,无需额外处理。


总结

自由列表的实现通过以下几个关键特性,确保了其高效性和可靠性:

  1. 基于页面的链表:每个页面可以存储多个条目,减少了节点数量,提高了空间利用率。
  2. 自我管理能力:被移除的节点会自动回收,避免了外部干预。
  3. 崩溃安全性:通过追加写入的方式,保证了崩溃后的一致性。
  4. 灵活性:支持动态扩展和收缩,能够适应不同的工作负载。

这种设计使得自由列表成为数据库系统中管理未使用页面的理想选择。

在这一节中,我们详细讨论了如何将自由列表(Free List)集成到键值存储(KV)数据库中,并分析了页面管理的实现细节。通过引入自由列表,数据库能够高效地重用未使用的页面,从而减少磁盘空间的浪费。


1. 页面管理

核心数据结构

为了支持页面的重用和原地更新,KV 数据库新增了一个 page 结构,用于管理页面的状态:

type KV struct {
    // ...
    page struct {
        flushed  uint64            // 已刷新的页面数量
        nappend  uint64            // 待追加的页面数量
        updates  map[uint64][]byte // 待处理的页面更新,包括追加的页面
    }
}
  • updates:一个映射表,用于捕获待处理的页面更新。当页面被修改时,其新内容会先存储在这里,而不是直接写入磁盘。
  • flushednappend:分别记录已刷新的页面数量和待追加的页面数量。

2. 页面分配与更新

2.1 分配新页面

BTree.new 方法现在由 KV.pageAlloc 替代。在分配页面时,优先从自由列表中获取空闲页面,只有在自由列表为空时才追加新页面。

func (db *KV) pageAlloc(node []byte) uint64 {
    if ptr := db.free.PopHead(); ptr != 0 { // 尝试从自由列表获取页面
        db.page.updates[ptr] = node
        return ptr
    }
    return db.pageAppend(node) // 追加新页面
}
2.2 更新现有页面

KV.pageWrite 方法返回一个可写的页面副本,用于捕获原地更新。如果页面已经有待处理的更新,则直接返回该副本;否则从磁盘读取页面内容并创建一个新副本。

func (db *KV) pageWrite(ptr uint64) []byte {
    if node, ok := db.page.updates[ptr]; ok {
        return node // 返回待处理的更新
    }
    node := make([]byte, BTREE_PAGE_SIZE)
    copy(node, db.pageReadFile(ptr)) // 从磁盘初始化
    db.page.updates[ptr] = node
    return node
}
2.3 读取页面

由于页面可能已经被更新,KV.pageRead 方法需要优先检查 updates 映射表。如果页面有未提交的更新,则直接返回更新后的内容;否则从磁盘读取。

func (db *KV) pageRead(ptr uint64) []byte {
    if node, ok := db.page.updates[ptr]; ok {
        return node // 返回待处理的更新
    }
    return db.pageReadFile(ptr) // 从磁盘读取
}

3. 元页面更新

元页面(meta page)是数据库的核心元数据,包含了树根指针、自由列表指针等信息。引入自由列表后,元页面的格式如下:

| sig | root_ptr | page_used | head_page | head_seq | tail_page | tail_seq |
| 16B |    8B    |    8B     |    8B     |    8B    |    8B     |    8B    |
  • head_pagetail_page:分别指向自由列表的头节点和尾节点。
  • head_seqtail_seq:用于索引自由列表中的条目。
  • page_used:记录当前数据库中已使用的页面数量。
3.1 初始化空数据库

在初始化一个空数据库时,需要为元页面和自由列表预留至少两个页面:

  • 第一页是元页面。
  • 第二页是自由列表的初始节点。
func readRoot(db *KV, fileSize int64) error {
    if fileSize == 0 { // 空文件
        // 预留两个页面:元页面和自由列表节点
        db.page.flushed = 2
        // 添加一个初始节点到自由列表,确保它永远不会为空
        db.free.headPage = 1 // 第二个页面
        db.free.tailPage = 1
        return nil // 元页面将在第一次更新时写入
    }
    // ...
}
3.2 准备下一次更新

在每次更新之间,调用 SetMaxSeq() 方法以允许重用上一版本的页面。

func updateFile(db *KV) error {
    // ...
    // 为下一次更新准备自由列表
    db.free.SetMaxSeq()
    return nil
}

4. 关键点分析

4.1 自由列表的作用
  • 自由列表使得未使用的页面可以被重用,减少了磁盘空间的浪费。
  • 在分配新页面时,优先从自由列表中获取空闲页面,只有在自由列表为空时才追加新页面。
4.2 原地更新的安全性
  • 页面更新被捕获在 updates 映射表中,直到提交时才会写入磁盘。
  • 由于页面更新是基于追加写入的,即使中断也不会覆盖已有数据,因此崩溃恢复机制仍然有效。
4.3 元页面的一致性
  • 元页面包含自由列表的指针和序列号,这些信息与树根指针一起原子性地更新,确保了一致性。

5. 并发支持的展望

目前的实现假设页面访问是顺序的。在后续引入并发支持时,headSeq 的推进将受到最老的读取操作的限制,而不是简单的 maxSeq


总结

通过引入自由列表,KV 数据库实现了以下目标:

  1. 高效的页面重用:通过自由列表管理未使用的页面,减少了磁盘空间的浪费。
  2. 安全的原地更新:通过 updates 映射表捕获页面更新,确保了崩溃恢复的安全性。
  3. 一致的元数据管理:元页面包含自由列表的指针和序列号,与树根指针一起原子性地更新。

这种设计为数据库系统提供了一种高效且可靠的页面管理机制,同时为后续引入并发支持奠定了基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2342202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机组成与体系结构:直接内存映射(Direct Memory Mapping)

目录 CPU地址怎么找到真实的数据? 内存映射的基本单位和结构 1. Pages(页)——虚拟地址空间的基本单位 2. Frames(页框)——物理内存空间的基本单位 3. Blocks(块)——主存和缓存之间的数据…

STM32提高篇: 蓝牙通讯

STM32提高篇: 蓝牙通讯 一.蓝牙通讯介绍1.蓝牙技术类型 二.蓝牙协议栈1.蓝牙芯片架构2.BLE低功耗蓝牙协议栈框架 三.ESP32-C3中的蓝牙功能1.广播2.扫描3.通讯 四.发送和接收 一.蓝牙通讯介绍 蓝牙,是一种利用低功率无线电,支持设备短距离通信的无线电技…

SpringMVC处理请求映射路径和接收参数

目录 springmvc处理请求映射路径 案例:访问 OrderController类的pirntUser方法报错:java.lang.IllegalStateException:映射不明确 核心错误信息 springmvc接收参数 一 ,常见的字符串和数字类型的参数接收方式 1.1 请求路径的…

【程序员 NLP 入门】词嵌入 - 上下文中的窗口大小是什么意思? (★小白必会版★)

🌟 嗨,你好,我是 青松 ! 🌈 希望用我的经验,让“程序猿”的AI学习之路走的更容易些,若我的经验能为你前行的道路增添一丝轻松,我将倍感荣幸!共勉~ 【程序员 NLP 入门】词…

从物理到预测:数据驱动的深度学习的结构化探索及AI推理

在当今科学探索的时代,理解的前沿不再仅仅存在于我们书写的方程式中,也存在于我们收集的数据和构建的模型中。在物理学和机器学习的交汇处,一个快速发展的领域正在兴起,它不仅观察宇宙,更是在学习宇宙。 AI推理 我们…

大模型AI的“双刃剑“:数据安全与可靠性挑战与破局之道

在数字经济蓬勃发展的浪潮中,数据要素已然成为驱动经济社会创新发展的核心引擎。从智能制造到智慧城市,从电子商务到金融科技,数据要素的深度融合与广泛应用,正以前所未有的力量重塑着产业格局与经济形态。 然而,随着…

操作系统概述与安装

主流操作系统概述 信创平台概述 虚拟机软件介绍与安装 windows server 安装 centos7 安装 银河麒麟V10 安装 一:主流服务器操作系统 (1)Windows Server 发展历程: 1993年推出第一代 WindowsNT(企业级内核&am…

开发了一个b站视频音频提取器

B站资源提取器-说明书 一、功能说明 本程序可自动解密并提取B站客户端缓存的视频资源,支持以下功能: - 自动识别视频缓存目录 - 将加密的.m4s音频文件转换为标准MP3格式 - 将加密的.m4s视频文件转换为标准MP4格式(合并音视频流)…

基于javaweb的SpringBoot校园服务平台系统设计与实现(源码+文档+部署讲解)

技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

PHYBench:首个大规模物理场景下的复杂推理能力评估基准

2025-04-23, 由北京大学物理学院和人工智能研究所等机构共同创建的 PHYBench 数据集,这是一个专门用于评估大型语言模型在物理场景下的复杂推理能力的高质量基准。该数据集包含 500 道精心策划的物理问题,覆盖力学、电磁学、热力学、光学、现代物理和高级…

Red:1靶场环境部署及其渗透测试笔记(Vulnhub )

环境介绍: 靶机下载: https://download.vulnhub.com/red/Red.ova 本次实验的环境需要用到VirtualBox(桥接网卡),VMware(桥接网卡)两台虚拟机(网段都在192.168.152.0/24&#xff0…

深入详解人工智能数学基础——概率论中的KL散度在变分自编码器中的应用

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用…

测试模版x

本篇技术博文摘要 🌟 引言 📘 在这个变幻莫测、快速发展的技术时代,与时俱进是每个IT工程师的必修课。我是盛透侧视攻城狮,一名什么都会一丢丢的网络安全工程师,也是众多技术社区的活跃成员以及多家大厂官方认可人员&a…

Openharmony 和 HarmonyOS 区别?

文章目录 OpenHarmony 与 HarmonyOS 的区别:开源生态与商业发行版的定位差异一、定义与定位二、技术架构对比1. OpenHarmony2. HarmonyOS 三、应用场景差异四、开发主体与生态支持五、关键区别总结六、如何选择?未来展望 OpenHarmony 与 HarmonyOS 的区别…

uniapp 仿小红书轮播图效果

通过对小红书的轮播图分析&#xff0c;可得出以下总结&#xff1a; 1.单张图片时容器根据图片像素定高 2.多图时轮播图容器高度以首图为锚点 3.比首图长则固高左右留白 4.比首图短则固宽上下留白 代码如下&#xff1a; <template><view> <!--轮播--><s…

R/G-B/G色温坐标系下对横纵坐标取对数的优势

有些白平衡色温坐标系会分别对横纵坐标取对数运算。 这样做有什么优势呢? 我们知道对数函数对0-1之间的因变量值具有扩展作用。即自变量x变化比较小时,经过对数函数作用后可以把因变量扩展到较大范围内,即x变化较小时,y变化较大,增加了识别数据的识别性。 由于Raw数据中的…

AI赋能安全调度系统:智能升级与功能跃迁

安全调度系统通过AI技术的深度整合&#xff0c;实现了从传统监控到智能决策的质变升级。这种智能化转型不仅提升了系统的响应速度和处理精度&#xff0c;更重塑了整个安全管理的运行范式。以下是AI技术为安全调度系统带来的核心功能强化&#xff1a; 智能风险识别与预警能力跃…

数据结构与算法(十二):图的应用-最小生成树-Prim/Kruskal

相关文献&#xff1a; 数据结构与算法(一)&#xff1a;基础理论 数据结构与算法(二)&#xff1a;线性表的实现 数据结构与算法(三)&#xff1a;线性表算法设计练习 数据结构与算法(四)&#xff1a;斐波那契数列 数据结构与算法(五)&#xff1a;LRU 数据结构与算法(六)&#xff…

项目——高并发内存池

目录 项目介绍 做的是什么 要求 内存池介绍 池化技术 内存池 解决的问题 设计定长内存池 高并发内存池整体框架设计 ThreadCache ThreadCache整体设计 哈希桶映射对齐规则 ThreadCache TLS无锁访问 CentralCache CentralCache整体设计 CentralCache结构设计 C…

系统与网络安全------弹性交换网络(2)

资料整理于网络资料、书本资料、AI&#xff0c;仅供个人学习参考。 Eth-Trunk 组网中经常会遇到的问题 链路聚合技术 概述 Eth-Trunk&#xff08;链路聚合技术&#xff09;作为一种捆绑技术&#xff0c;可以把多个独立的物理接口绑定在一起&#xff0c;作为一个大带宽的逻辑…