第14讲:mmap 实现文件 LRU 缓存

目录

1. 基于大文件设计LRU缓存

  • LRU(Least Recently Used)即最近最少使用,是一种缓存淘汰算法。

LRU原理

  • 数据结构: LRU算法通常使用双向链表和哈希表两种数据结构来实现。
    • 双向链表: 用于存储缓存中的数据,并按照访问顺序排列。链表中的节点表示缓存中的数据项,节点按照数据的访问顺序进行排列。最近被访问的数据节点被移动到链表的头部,而最久未被使用的数据节点则位于链表的尾部。
    • 哈希表: 用于快速查找缓存中是否存在某个数据,以及定位该数据在双向链表中的位置。哈希表的键是数据的键,值是指向双向链表节点的指针。
  • 算法操作:
    • 获取数据(Get): 当需要获取某个数据时,首先在哈希表中查找。如果数据存在,则将其从双向链表中移动到链表头部(表示最近使用),并返回数据的值。如果数据不存在,则返回缓存未命中的标志。
    • 插入数据(Put): 当需要插入新数据时,首先在哈希表中查找。如果数据已经存在,则更新数据的值,并将其从双向链表中移动到链表头部。如果数据不存在,则将其添加到链表头部,并在哈希表中添加对应的映射。如果插入后缓存容量超过限制,则从双向链表尾部移除最久未使用的数据节点(即淘汰最久未使用的数据),并在哈希表中删除对应的映射。
  • 时间复杂度和空间复杂度:
    • 时间复杂度: LRU算法中,Get操作和Put操作的时间复杂度都是O(1)。这是因为哈希表的查找和修改操作以及双向链表中插入、删除和移动节点的操作都是常数时间复杂度。
    • 空间复杂度: 主要由哈希表和双向链表的大小决定。哈希表和双向链表的空间复杂度都是O(n),其中n是缓存中的数据量。

代码实现

LRUCache.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <list>
#include <memory>
#include <unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

namespace LRUCache
{
#define BLOCK_ADDR_ALIGN(off) (off & ~(0xFFF)) // 清除低12位
#define NORMAL (1 << 0)
#define NEW (1 << 1)
#define VISIT (1 << 2)
#define DELETE (1 << 3)

const int gdefaultfd = -1;
const int gblocksize = 4096; // 4KB
const int gcapacity = 3;     // 3个block

class DataBlock
{
private:
    void UpdateStatus(unsigned status) { _status = 0; _status |= status; }
    bool ConfirmStatus(unsigned status) { return _status & status; }

public:
    DataBlock(off_t off, off_t size) : _off(off), _size(size),
        _addr(nullptr), _status(NEW)
    {
    }

    // 映射载入内存
    bool DoMap(int fd)
    {
        _addr = ::mmap(nullptr, _size, PROT_READ | PROT_WRITE,
            MAP_SHARED, fd, _off);
        if (_addr == MAP_FAILED)
        {
            perror("mmap"); // 在计算正确的情形下,不可能
            return false;
        }
        std::cout << "mmap && 加载 " << _off << " 成功" << std::endl;
        return true;
    }

    // 取消映射,从内存中移除
    bool DoUnmap()
    {
        int n = ::munmap(_addr, _size);
        if (n < 0)
        {
            perror("munmap");
            return false;
        }
        std::cout << "munmap && 移除 " << _off << " 成功" << std::endl;
        return true;
    }

    // 设置状态
    void Status2Normal() { UpdateStatus(NORMAL); }
    void Status2New() { UpdateStatus(NEW); }
    void Status2Visit() { UpdateStatus(VISIT); }
    void Status2Delete() { UpdateStatus(DELETE); }

    // 确认状态
    bool IsNormal() { return ConfirmStatus(NORMAL); }
    bool IsNew() { return ConfirmStatus(NEW); }
    bool IsVisit() { return ConfirmStatus(VISIT); }
    bool IsDelete() { return ConfirmStatus(DELETE); }

    // 获取基本属性
    off_t Off() { return _off; }
    void *Addr() { return _addr; }
    off_t Size() { return _size; }

    ~DataBlock()
    {
    }

    void DebugPrint()
    {
        std::cout << "_off: " << _off << std::endl;
        std::cout << "_size: " << _size << std::endl;
        std::cout << "_addr: " << _addr << std::endl;
        std::cout << "_status: ";
        if (IsNormal())
            std::cout << "NORMAL";
        if (IsNew())
            std::cout << "NEW";
        if (IsVisit())
            std::cout << "VISIT";
        if (IsDelete())
            std::cout << "delete";
        std::cout << std::endl;
    }

private:
    off_t _off;      // 该block在文件中的起始偏移量,4KB对齐的
    off_t _size;     // 该block的大小
    void *_addr;     // 该block映射的虚拟地址是什么位置
    unsigned _status; // 该block的状态
};

class FileCache
{
private:
    // 要访问的块,是否在文件和合法范围内
    bool IsOffLegal(off_t off) { return off < _total; }

    // 目标块,是否已经被缓存了
    bool IsCached(off_t off) { return _hash.find(off) != _hash.end(); }

    // 缓存是不是满了
    bool IsCacheFull() { return _cache.size() > _cachemaxnum; }

    // 根据偏移量,获取实际对应的块大小
    off_t GetSizeFromOff(off_t off)
    {
        off_t size = gblocksize;
        if (off + gblocksize > _total)
        {
            // 文件不一定会被4KB整除,求一下可能的剩余
            size = _total % gblocksize;
        }
        return size;
    }

    void DoLRU(off_t off)
    {
        if (!IsCached(off))
            return;
        if ((_hash[off]->IsNew())) // 如果是因插入触发的LRU,检测是否要移除尾部节点
        {
            _hash[off]->Status2Normal(); // 让节点成为普通节点
            if (IsCacheFull())
            {
                // 1. 让尾部block映射的内存,从地址空间中移除
                _cache.back()->DoUnmap();
                // 2. 从hash表中移除尾部block
                std::cout << "cache 移除: " << _cache.back()->Off() << std::endl;
                _hash.erase(_cache.back()->Off());
                // 从cache list中移除尾部block
                _cache.pop_back();
            }
        }
        else if (_hash[off]->IsVisit()) // 如果是访问节点触发LRU,检测是否要移除尾部节点
        {
            _hash[off]->Status2Normal();
            _cache.remove(_hash[off]);      // 从缓存中移除
            _cache.push_front(_hash[off]); // 重新插入到缓存头部
            std::cout << "将 " << off << "移动到cache头部" << std::endl;
        }
        else
        {
            // TODO
        }
    }

    void DoCache(off_t off)
    {
        // 计算指定偏移量下的数据块大小,这个块大小不一定完全是4KB大小哦
        off_t blocksize = GetSizeFromOff(off);
        // 构建block对象
        std::shared_ptr<DataBlock> block = std::make_shared<DataBlock>
            (off, blocksize);
        // 先加载并映射到地址空间
        block->DoMap(_fd);
        // 更新到hash表,方便随时提取
        _hash.insert(std::make_pair(off, block));
        // 头插到cache中,缓存起来
        _cache.push_front(block);
    }

public:
    FileCache(const std::string &file) : _file(file), _fd(gdefaultfd)
    {
        _fd = ::open(_file.c_str(), O_RDWR); // 这个文件要存在,这样缓存才有意义
        if (_fd < 0)
        {
            perror("open");
            return;
        }
        struct stat status;
        int n = ::fstat(_fd, &status);
        if (n < 0)
        {
            perror("stat");
            return;
        }
        _total = status.st_size;
        _cachemaxnum = gcapacity;
    }

    std::shared_ptr<DataBlock> GetBlock(off_t off)
    {
        // 1. 偏移量不合法,就没必要玩了
        if (!IsOffLegal(off))
            return nullptr;
        // 2. 先根据偏移量,计算真实的块在文件中的起始地址
        off = BLOCK_ADDR_ALIGN(off);
        // 3. ok,在合法偏移量的位置,下面查看cache是否命中
        if (_hash.find(off) != _hash.end()) // 命中
        {
            // 3-1 命中,更新block状态,标记为被访问
            _hash[off]->Status2Visit();
        }
        else
        {
            // 3-2 没有命中,执行加载
            DoCache(off);
        }
        DoLRU(off); // 检测并执行LRU算法
        return _hash[off];
    }

    void PrintCache()
    {
        std::cout << "---------cache 内容----------" << std::endl;
        for (auto &iter : _cache)
        {
            iter->DebugPrint();
            std::cout << "|" << std::endl;
        }
        std::cout << "nullptr" << std::endl;
        std::cout << "-----------------------------" << std::endl;
    }

    ~FileCache()
    {
        if (_fd != gdefaultfd)
        {
            ::close(_fd);
        }
    }

private:
    std::string _file; // 文件名+路径
    int _fd;           // 文件fd
    off_t _total;      // 文件总大小
    std::list<std::shared_ptr<DataBlock>> _cache;
    int _cachemaxnum;
    std::unordered_map<off_t, std::shared_ptr<DataBlock>> _hash;
};
}
Main.cc
cpp 复制代码
#include "LRUCache.hpp"

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }
    std::string filename = argv[1];
    LRUCache::FileCache fc(filename);
    int count = 0;

    // 依次对不存在的block测试获取
    while (count < 10)
    {
        fc.GetBlock(count * 4096);
        fc.PrintCache();
        count++;
        sleep(1);
    }

    // 测试对已经存在的block进行获取
    while (true)
    {
        off_t off;
        std::cout << "Please Enter Off# ";
        std::cin >> off;
        auto b = fc.GetBlock(off);
        std::cout << "block addr: " << b->Addr() << std::endl;
        fc.PrintCache();
    }
    return 0;
}
Makefile
makefile 复制代码
lrucache:Main.cc
	g++ -o $@ $^ -std=c++17 -g
.PHONY:clean
clean:
	rm -f lrucache

实验

text 复制代码
// 伪造一个大文件
dd if=/dev/zero of=log.txt bs=4096 count=10
text 复制代码
(gdb) info proc mapping
process 236724
Mapped address spaces:
        Start Addr        End Addr    Size    Offset objfile
     0x7ffff7fc8000  0x7ffff7fc9000  0x1000   0x7000 /home/whb/code/test/test/log.txt
     0x7ffff7fca000  0x7ffff7fcb000  0x1000   0x9000 /home/whb/code/test/test/log.txt
     0x7ffff7ffb000  0x7ffff7ffc000  0x1000   0x8000 /home/whb/code/test/test/log.txt


mmap && 加载 0 成功
---------cache 内容----------
_off: 0
_size: 4096
_addr: 0x7ffff7ffb000
_status: NORMAL
|
nullptr
-----------------------------
mmap && 加载 4096 成功
---------cache 内容----------
_off: 4096
_size: 4096
_addr: 0x7ffff7fca000
_status: NORMAL
|
_off: 0
_size: 4096
_addr: 0x7ffff7ffb000
_status: NORMAL
|
nullptr
-----------------------------
mmap && 加载 8192 成功
---------cache 内容----------
_off: 8192
_size: 4096
_addr: 0x7ffff7fc9000
_status: NORMAL
|
_off: 4096
_size: 4096
_addr: 0x7ffff7fca000
_status: NORMAL
|
_off: 0
_size: 4096
_addr: 0x7ffff7ffb000
_status: NORMAL
|
nullptr
...
相关推荐
程序开发阿元36 分钟前
一台 20G 的 VPS 被日志吃掉一半:我按这个顺序查了二十分钟
linux·ssh
.冰块.1 小时前
流量大了自动加 Pod!K8s HPA 扩缩容与 ResourceQuota/LimitRange 资源管控实战
linux·云原生·容器·kubernetes·hpa·quota·limitrange
承渊政道1 小时前
Linux网络学习【深入理解多路转接select、poll的代码改写】
linux·ubuntu·select·多路转接·poll
雪落漂泊1 小时前
Linux进程(四)概念:命令行参数和环境变量篇
linux·运维·服务器
qq_377572772 小时前
linux x86 arm loongarch
linux
Dragon~Snow2 小时前
Centos Stream10 安装 MySQL-9.7.2-linux-glibc2.28-x86_64.tar
linux·mysql·centos
say_fall2 小时前
【Linux系统编程】Linux进程信号:从“外卖到了“到内核里的一张位图
linux·运维·服务器·车载系统·智能硬件
坤坤子吖3 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
Rabitebla3 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习