从虚拟内存到物理内存

从虚拟内存到物理内存

原本打算针对Linux内存管理写一篇长文,准备了快一个月了,发现这里的内容实在太丰富,不是一篇能够讲解清楚的,于是作罢,还是多写几篇吧。这是第一篇,解释一下Linux从虚拟内存到物理内存的转换。

绝大部分现代操作系统是运行在所谓的“保护模式”下的,所谓“保护模式”是相对于“实模式”来说的,在上古时代,操作系统是直接访问的物理内存,所有进程都是访问的同一个地址空间,相互之间可以访问对方的数据。这样的工作方式,需要使用复杂的方式来安排各个进程的内存,非常不方便。另外,进程间可以随意访问对方的数据,这样也很不安全。

后来出现了保护模式,在此模式下,进程都有自己独立的内存地址空间,相互之间有一定的隔离性(在内核态还是互通的),进程自己的虚拟地址通过页表翻译映射到实际的物理地址。同时,内存在分配时,可以只分配虚拟内存,等到程序实际需要使用到的时候,再通过缺页错误分配实际的物理内存,提高了内存的使用效率。这种方式实际上印证了,计算机领域一项常见的模式,即通过引入一个中间层来解决问题,使用了页表,将程序使用的虚拟内存空间与物理内存空间进行了解耦。

分页和页表

还是从分页开始说吧,操作系统对内存进行管理有分段和分页两种方式,在Linux中,这两种实际上都有用到。所谓分页,就是把物理内存空间切割成多个固定大小的区域,实际使用时,每次分配一页或者多页使用。而页表是用来负责进行虚拟内存到物理内存的转换的。在Linux中,进程的地址空间是相互隔离的,每个进程都会有其自己的页表项。正常情况下,Linux系统的一页大小是4KB(212),假设一个进程使用4GB地址空间(32位情况下,64位情况下只会更多),如果只使用一级页表,那边这个进程会有(232 >> 12 = 2^20)个页表项,这是一个非常巨大的数字,实际情况尤其是在64位系统中,用户进程并不会使用到全部的虚拟空间,并不需要如此多的页表项。

在x86_64的Linux系统中,内存地址有48位(一个长整型是64位,在多余的位上,用户态为全0,内核态是全1,实际使用中,内核态和用户态地址之间还存在一个巨大的空洞),使用了4级页表来管理进程的虚拟内存。

页表类型 位数
PGD(全局页描述符) 9
PUD(上层页描述符) 9
PMD(中层页描述符) 9
PTE(页表项) 9
offset(页内偏移) 12

进程的虚拟地址被分成了5个部分来使用,如下图所示:

Linux-Memory-Page-2.png

在x86_64系统中,虚拟地址是64bits长,使用一个unsigned long类型来表示,最高位的16个字节目前没有使用。(在内核态这16位全部置为1---0xFFFF,而在用户态则置为0,因此,属于内核态和用户态的虚拟内存地址是很方便判断的),那么通过将虚拟地址右移的方式,就可以获得虚拟地址在各层页表中的偏移。

比如,如果我们要获取一个虚拟地址对应的PGD项,我们首先,将虚拟地址右移PGDIR_SHITF位(12+9+9+9=39),然后保留需要的位数(PGD占用9位,可以索引2^9即512个PGD项),这样我们获得了这个虚拟地址在PGD表中的索引。根据这个索引,再从这个进程相关的pgd中去获取相关的页表项。

#define PGDIR_SHIFT 39
#define PTRS_PER_PGD    512

/*
 * the pgd page can be thought of an array like this: pgd_t[PTRS_PER_PGD]
 *
 * this macro returns the index of the entry in the pgd page which would
 * control the given virtual address
 */
#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

/*
 * pgd_offset() returns a (pgd_t *)
 * pgd_index() is used get the offset into the pgd page's array of pgd_t's;
 */
#define pgd_offset(mm, address) ((mm)->pgd + pgd_index((address)))

此时,我们获得的这个pgd项中,存储的是对应的pud表的页框。要获得pud表中对应的表项,需要进行和之前类似的操作,获取pud表中的索引,与pud表的虚拟地址相加获得对应PUD项。

/*
 * 3rd level page
 */
#define PUD_SHIFT   30
#define PTRS_PER_PUD    512

#define pgd_val(x)  native_pgd_val(x)

static inline pgdval_t native_pgd_val(pgd_t pgd)
{
    return pgd.pgd;
}

#define PAGE_OFFSET     ((unsigned long)__PAGE_OFFSET)

#define __PAGE_OFFSET_BASE      _AC(0xffff880000000000, UL)
#ifdef CONFIG_RANDOMIZE_MEMORY
#define __PAGE_OFFSET           page_offset_base
#else
#define __PAGE_OFFSET           __PAGE_OFFSET_BASE
#endif /* CONFIG_RANDOMIZE_MEMORY */

#ifndef __va
#define __va(x)         ((void *)((unsigned long)(x)+PAGE_OFFSET))
#endif

static inline unsigned long pgd_page_vaddr(pgd_t pgd)
{
    return (unsigned long)__va((unsigned long)pgd_val(pgd) & PTE_PFN_MASK);
}

static inline unsigned long pud_index(unsigned long address)
{
    return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
    return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}

下面轮到pmd了,其实也是类似的:

#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
#define PMD_SIZE (_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK (~(PMD_SIZE - 1))

static inline pudval_t pud_pfn_mask(pud_t pud)
{
    if (native_pud_val(pud) & _PAGE_PSE)
        return PHYSICAL_PUD_PAGE_MASK;
    else
        return PTE_PFN_MASK;
}

static inline unsigned long pud_page_vaddr(pud_t pud)
{
    return (unsigned long)__va(pud_val(pud) & pud_pfn_mask(pud));
}

/*
 * the pmd page can be thought of an array like this: pmd_t[PTRS_PER_PMD]
 *
 * this macro returns the index of the entry in the pmd page which would
 * control the given virtual address
 */
static inline unsigned long pmd_index(unsigned long address)
{
    return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}

static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
  return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}

最后终于到页表项了

/*
 * the pte page can be thought of an array like this: pte_t[PTRS_PER_PTE]
 *
 * this function returns the index of the entry in the pte page which would
 * control the given virtual address
 */
static inline unsigned long pte_index(unsigned long address)
{
    return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}

static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
    return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

从上面的过程,我们可以看出整个内存访问过程如下:

  1. PGD--->PUD
  2. PUD--->PMD
  3. PMD--->PTE
  4. PTE--->物理内存页框
  5. 物理页框地址加上页内偏移得到物理地址

整个地址翻译过程访问了4次内存,如果没有一些加速的手段,这个性能是非常差的。

缓存与TLB

为了加速地址翻译的过程,一般的处理器会使用TLB来缓存虚拟地址的翻译结果。TLB和普通的缓存基本类似,其不同之处在于,因为缓存的是虚拟地址的翻译结果,TLB一定是使用的VIVT(virtual index virtual tag),基于TLB本身的特性,其不存在歧义(存在相同的tag和index)或者别名的情况(同一个物理地址映射到不同的虚拟地址)。究其原因,为了区分不同进程的虚拟地址,TLB又额外使用了ASID(address space id)进行区分。在虚拟地址翻译的过程中,MMU会另外使用ASID进行比对,只有在ASID也相同的情况下。

v2-80141749c349c85b28ee001e2d3f88c5_720w.png

借用网上一张图来进行说明。

MMU在进行地址转译时,会先在TLB中进程查找。由于是页帧的翻译,低12位作为页内偏移,在TLB中是不需要的。在查找TLB时,会使用12-19位作为索引,20-47作为tag进行匹配。除了tag以外还是用了ASID对不同进程的地址空间进行区分,并使用一个比特位对用户态和内核态的内存进行区分,以达到较高的效率。

由于ASID的存在,在进程切换过程中,并不需要将全部TLB进行刷新,只需要在匹配时,比对ASID,将不匹配的行刷新即可。

ASID是使用时动态分配的,一般系统使用8bit或者16bit位进行管理。最多28或者216个ASID,使用位图进行管理。在位图满或者建立页表映射时,进行全部TLB刷新。

页帧和页帧号

在系统中,每个物理页框有一个page结构体对应,所有的这些page结构体是连续存放的(根据物理内存模型不同有所区别,平坦模型是完全连续,非一致内存是Zone内连续,而稀疏模型是section内一致),从内核的角度看,其存放page结构体的内存,是一个struct page结构体,通过其索引号即页帧号,即可访问对应的page结构体。由于page结构体和物理内存是一一对应的,如果获得了页帧号,同样也可算的其物理内存的位置。同时页帧和页帧号是可以相互转换的。

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)  (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)


/*
 * Convert a physical address to a Page Frame Number and back
 */
#define __phys_to_pfn(paddr)    PHYS_PFN(paddr)
#define __pfn_to_phys(pfn)  PFN_PHYS(pfn)

后记

越接近底层,接近物理设备,我发现自己的语言描述能力愈发地有些,内存这块已经看了超过一个月了,但是发现自己还是很难用非常恰当的语言对其进行描述。先写成这样吧,后面对这块理解加深后,我可能会回来重新组织语言。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355