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