在虚拟地址空间那篇文章中我们简单地介绍了虚拟地址空间,知道了应用程序中使用的是虚拟地址,需要通过MMU转换成物理地址,本文将详细介绍虚拟地址如何转换成物理地址。
linux操作系统以页为单位管理虚拟内存,通常一页为4k,而物理内存是以块为单位管理的,物理内存被分成很多与页大小相同的块,被称为页框,每个页地址与页框对应,这种对应关系被记录在页表中,页表是MMU中的数据结构,下图是页表中较为常见的几个属性。
页框存在于物理内存上,而页可以放置在任意的页框中,这是能够进行地址转换的基础。
页表结构图中,p(存在位)表示虚拟地址对应的物理地址是否已经加载到内存中,如果虚拟地址没有被使用过,物理地址就不会加载到内存中,当需要使用虚拟地址时,操作系统先检查存在位,如果存在位为1则地址转换后可以直接访问,如果存在位为0,会引发缺页异常,操作系统将加载页框到内存中,然后继续访问。与普通中断不同的是,缺页异常返回后会再次执行陷入中断的那条指令,而普通中断会执行跳过该指令,执行下一条指令。
操作系统通常使用页面缓存算法来管理已经加载的页,当操作系统通过缺页异常准备加载新页面时,如果缓存已满,就需要先删除一页,再加载新页,删除页时,需要检查D(脏位),如果D为1,说明该页被修改过,需要先写入磁盘,再删除。linux操作系统中,名为pdflush的守护进程会周期性地刷脏页。
换页时选择被删除的页通过页面缓存置换算法来实现,常见的算法包括先进先出算法(FIFO)、最佳置换算法(OPT)和最近最少使用算法(LRU)。linux使用LRU算法实现页面置换,这里提供一种LRU的简单实现。
//双向链表,也可以使用stl的list
struct linkedlist{
linkedlist* prev;
linkedlist* next;
int value;
int key;
linkedlist():prev(nullptr),next(nullptr),key(0),value(0){};
linkedlist(int p1,int p2):prev(nullptr),next(nullptr),key(p1),value(p2){};
linkedlist(linkedlist* p1, linkedlist* p2, int q1,int q2):prev(p1), next(p2),key(q1),value(q2) {};
};
class LRUCache {
public:
LRUCache(int capacity) {
_capacity = capacity;
size = 0;
head = new linkedlist(0, 0);
tail = new linkedlist(head, nullptr, 0, 0);
head->next = tail;
}
//获取页面时,更新该页面位置到最前
int get(int key) {
if(mp.find(key)==mp.end())return -1;
moveToHead(mp[key]);
return mp[key]->value;
}
//添加页面时,删除最后一个页面
void put(int key, int value) {
if(mp.find(key)==mp.end()){
if(size==_capacity) {
delTail();
--size;
}
linkedlist* node = new linkedlist(key,value);
mp[key] = node;
addToHead(node);
++size;
}
else{
mp[key]->value = value;
moveToHead(mp[key]);
}
return;
}
private:
linkedlist* head;
linkedlist* tail;
unordered_map<int,linkedlist*> mp;
int size;
int _capacity;
void moveToHead(linkedlist* node){
node = delNode(node);
addToHead(node);
}
void addToHead(linkedlist* node){
linkedlist* tmp = head->next;
head->next = node;
node->prev = head;
node->next = tmp;
tmp->prev = node;
}
linkedlist* delNode(linkedlist* node){
linkedlist* p = node->prev;
linkedlist* n = node->next;
p->next = n;
n->prev = p;
node->next = nullptr;
node->prev = nullptr;
return node;
}
void delTail(){
linkedlist* node = tail->prev;
node = delNode(node);
mp.erase(node->key);
delete node;
node = nullptr;
}
};
OPT算法被称为完美但无法实现的页面置换算法,限于篇幅(困了)我们后续再介绍。
使用段页式管理内存的操作系统将虚拟地址先通过段表转换为线性地址,再通过页表将线性地址转换为物理地址。Linux操作系统使用页式管理,虚拟地址就是线性地址,我们仅详细介绍线性地址到物理地址的转换。
如上图,在32位操作系统中,页目录表的基址被记录在CR3寄存器中,地址的高10位为页目录表的偏移,通过基址和偏移可以找到页表基址,地址中间10为页表的偏移,通过基址和偏移可以找到对应的页,地址低12位记录了页中的偏移,就能够查找到对应的物理地址了。
上面介绍的多级页表中包括一张页目录表和一张页表,需要多次跳转才能访问到物理地址,并且Linux的实际实现中,页目录表的数量还要更多。为了减少跳转带来的消耗,MMU引入了快表(TLB),快表个人理解就是一种缓存,将经常使用的页面及其页框直接记录在快表中,当需要访问物理地址时,先从TLB中寻找,如果命中TLB,则直接取出页框地址访问。
快表实际上是一个单独的高速缓存寄存器,其大小通常受到限制,因此快表只能访问有限个表项。当虚拟地址空间切换时,快表也会切换,此时页面转换的消耗将大大增加。
本文较为深入地讨论了虚拟地址和物理地址的转换,相信大家都这个过程已经比较清楚,后续将对异常和中断进行介绍,帮助大家理解缺页异常的实际工作过程。