背景
进程通过malloc函数申请内存时,分配到的内存是虚拟内存,并不是实际的物理内存,且此时内存管理单元没有完成虚拟内存到物理内存的映射。当程序需要访问虚拟内存时,发现该内存并没有映射到物理内存上,会产生缺页中断。进入缺页中断后判断是否有可分配的物理内存,如果有,这时才会分配物理页并通过MMU的页表来建立虚拟内存到物理内存的映射。
如果没有可分配的物理内存,则会按顺序进行后台内存回收、直接内存回收,如果进行内存回收后仍无法满足需求,则会出发OOM机制,杀死占用内存较大的进程。
若缺页中断后发现物理内存中没有可分配的空闲页,则需要使用页面置换算法选择物理页(脏页)置环到磁盘,本文主要介绍其中LRU、LFU。
LRU算法选择最长时间没被访问的内存页进行置环,一般通过维护链表来实现,链表头部区域是经常访问的内存,尾部区域是不常访问的内存,也就是优先回收链表尾部的内存。LFU算法选择访问次数最少的页面进行置换,若访问次数相同,则对相同次数的页面使用LRU算法来进一步判断置换掉哪一页。
LRU
题目
数据结构
LRU算法保证在每次访问元素时,需将该元素插入到链表头部,若当前容量大于维护的容量时,需要从尾部删除元素,链表的插入删除很方便,时间复杂度满足要求;但是查找元素时需要遍历,这时可考虑用哈希表映射,实现查询也满足O(1)的时间复杂度。
如下图所示:哈希表里存放key–>node的映射关系,双向链表里也含有key的原因是因为后续删除链表时,需要同时删除哈希表里的对应元素。
具体实现
双向链表结点的定义
四个元素:key、value以及前驱节点与后继结点
class DoubleList{
public:
int key;
int value;
DoubleList *prev;
DoubleList *next;
DoubleList():key(0), value(0), prev(nullptr), next(nullptr){}
DoubleList(int _key, int _value):key(_key), value(_value), prev(nullptr), next(nullptr){}
};
LRUCache类
初始化
定义初始化的成员变量:头尾结点、哈希表、容量、当前大小
class LRUCache {
public:
LRUCache(int capacity) : capacity_(capacity), size_(0){ //数据结构初始化
head = new DoubleList();
tail = new DoubleList();
head->next = tail;
tail->prev = head;
//*******************
//
//*******************
}
private:
DoubleList *head;
DoubleList *tail;
unordered_map<int, DoubleList *> map;
int size_;
int capacity_;
};
主要接口实现
put()
若没有该元素,则new出该元素结点,将其加入链表头部,在哈希表里也加入映射关系,因为新加入元素,需要判断大小是否超出LRUCache的容量,若超出,删除尾结点以及对应的哈希表映射。若有,则更新value,并加入链表头部。
void put(int key, int value) {
if (map.find(key) == map.end()) {
DoubleList *node = new DoubleList(key, value);
//map[key] = node;
map.insert(pair<int, DoubleList*>(key, node));
addHead(node);
size_++;
if (size_ > capacity_) {
DoubleList *tmp = tail->prev;
removeNode(tmp);
map.erase(tmp->key); //需要删除哈希表,所以node里也有key
delete(tmp);
size_--;
}
}
else {
DoubleList *node = map[key];
node->value = value;
moveToHead(node);
}
}
get()
若元素存在,则返回该值,并将元素放入链表头节点
int get(int key) {
if (map.find(key) == map.end()) {
return -1;
}
else {
DoubleList *node = map[key];
moveToHead(node);
return node->value;
}
}
addHead()
,removeNode()
,moveToHead()
等与双链表相关的操作就不赘述了…
这里与操作链表相关的函数写在了LRUCache类中,其实应该定义一个结点类和一个双向链表的类,将链表相关操作封装在链表类里。后面LFU会写规范一点。
LFU
题目
数据结构
特点:
- FreqDoubleList链表存放的是按访问次数从小到大排序的结点
- DoubleList里的是真正的存放数据的结点
- FreqDoubleList中的每一个freqNode结点下面都有自己的DoubleList
- freq_map存放访问的key与存储访问该key的次数的结点freqNode的映射;
- lru_map与前述LRU算法中的map一样,不同的是这里可以映射到多个链表的结点。每个freqNode下面都有一个LRU双链表,因为访问次数相同的话,要根据LRU算法再进一步决策。
具体实现
结点类与链表类的定义
- LRUDoubleList链表与其Node 的定义:
在链表类中实现插入删除结点的接口,与前面类似。不同之处在于isempty()
,该函数表示判断此LRUDoubleList是否没存数据,因为该链表是属于freqNode的,若进行操作后没有访问次数等于freqNode->fre_的数据,则ferqNode要删除,所以要判断该链表是否为空,进而对没用的freqNode结点删除。
class Node {
public:
int key_;
int value_;
Node *prev;
Node *next;
Node():key_(0), value_(0), prev(nullptr), next(nullptr){}
Node(int key, int value):key_(key), value_(value), prev(nullptr), next(nullptr){}
};
class LRUDoubleList {
public:
Node *head;
Node *tail;
LRUDoubleList():head(new Node()), tail(new Node()) {
head->next = tail;
tail->prev = head;
}
void addHead(Node *node) {
node->next = head->next;
node->prev = head;
head->next->prev = node;
head->next = node;
}
Node* delNode(Node *node){
node->next->prev = node->prev;
node->prev->next = node->next;
return node;
}
Node* delLast() {
return delNode(tail->prev);
}
bool isempty() {
return head->next == tail;
}
};
- FreDoubleList链表与其freqNode 的定义:
比较重要的地方在FreDoubleList头尾节点赋值与addCurFreq()
函数
class FreqNode {
public:
int freq_;
FreqNode *prev;
FreqNode *next;
//*****************一个freqNode结点对应一个LRU链表*****************
LRUDoubleList *dl;
//explicit防止隐式转换,其实就是想稍微规范一点所以写成这样
explicit FreqNode(int freq):freq_(freq), prev(nullptr), next(nullptr), dl(new LRUDoubleList()){}
};
class FreDoubleList {
public:
FreqNode *head;
FreqNode *tail;
// 初始化freqDoubleList时头尾节点可设置为 0 与 INT_MAX
FreDoubleList():head(new FreqNode(0)), tail(new FreqNode(INT_MAX)) {
head->next = tail;
tail->prev = head;
}
//当前频率次数加一
void addCurFreq(FreqNode *cur) {
//if not exist, create new freq;
if (cur->freq_ + 1 != cur->next->freq_) {
FreqNode *newFreqNode = new FreqNode(cur->freq_ + 1);
newFreqNode->next = cur->next;
newFreqNode->prev = cur;
cur->next->prev = newFreqNode;
cur->next = newFreqNode;
}
}
void delFreqNode(FreqNode *node) {
node->next->prev = node->prev;
node->prev->next = node->next;
}
};
LFUCache类
成员变量定义
前文所述需要的两个散列表;表示访问次数的链表,还有其下的LRU链表(创建freqNode时创建);存放数据的容量
private:
//int size_;
int capacity_;
unordered_map<int, FreqNode*> fre_map;
unordered_map<int, Node*> lru_map;
FreDoubleList *fl;
接口实现
get()
没找到直接返回-1;
若找到,先删除原有的结点,然后更新数据结构
int get(int key) {
//没找到该数据,直接返回-1
if (lru_map.find(key) == lru_map.end()) {
return -1;
}
//数据存在,进行两个步骤:1。删除原有的;2.更新数据结构
//1 、delete old
//访问次数+1,故先增加对应的freqNode结点
FreqNode *before = fre_map[key];
fl->addCurFreq(before);
//访问了,所以fre + 1,即before->next就是刚增加的ferqNode结点
FreqNode* after = before->next;
Node *targetNode = lru_map[key];
//删除原先的freqNode下的LRU链表中的结点
before->dl->delNode(targetNode);
//判断该LRU链表还是否有元素,没有则阐述freqNode结点
if (before->dl->isempty()) {
fl->delFreqNode(before);
}
//2、update to the next freq node
after->dl->addHead(targetNode); //更新frqNode下的LRU链表里的结点
fre_map[key] = after; //访问次数改变了,需要更新映射
return targetNode->value_;
}
put()
void put(int key, int value) {
if (capacity_ == 0) return;
if (lru_map.find(key) != lru_map.end()) {
lru_map[key]->value_ = value;
get(key);
}
else { //若没有元素,则需要添加
if(lru_map.size() == capacity_) {
//if will out of size, shoule delete node(Last LruNode in first LRUDoubledList)
FreqNode *node = fl->head->next;
Node *nodeToDel = node->dl->delLast();
//delete map
lru_map.erase(nodeToDel->key_);
fre_map.erase(nodeToDel->key_);
//最少频率列没有元素了,删除 ....
if (node->dl->isempty()) {
fl->delFreqNode(node);
}
}
//add node must update fl->head
fl->addCurFreq(f1->head);
Node *newNode = new Node(key, value);
fre_map[key] = fl->head->next;
lru_map[key] = newNode;
//************注意这里的结构嵌套****************
fl->head->next->dl->addHead(newNode);
}
}
小结
LFU的编写更加规范,将链表与其结点分别定义,结构较清晰。今天的一天都在写LRU与LFU。。。溜了写基金去了