文章目录
- 系统内存布局
- 内核地址的低端和高端内存概念
- 低端内存
- 高端内存
- 地址转换和MMU
- Linux中的四级分页模型
- 虚拟地址字段
- 页表处理
- 将虚拟地址转换物理地址
Linux系统中的每个内存地址都是虚拟的,它们不直接指向任何物理内存地址。每当访问内存位置时,可以执行转换机制以匹配相应的物理内存,所以我们在程序中必须用虚拟地址来访问数据。
注:下面所说的内核空间和用户空间这样的术语指的都是虚拟地址空间
系统内存布局
在Linux系统中,每个进程都有自己独立的虚拟地址空间。它是一种内存沙箱,存在于进程的生命周期中。在32位系统上,该地址空间大小是4GB。针对每一个进程,4GB的地址空间被分割成两个部分:
- 内核空间虚拟地址
- 用户空间虚拟地址
分割方式依赖于特殊的内核配置选项CONFIG_PAGE_OFFSET,这个选项定义内核虚拟地址部分在进程地址空间的起始位置。典型的进程虚拟地址空间布局如下图:
内核空间和用户空间所使用的地址都是虚拟地址,不同的是,访问内核空间地址需要特权模式,当CPU运行用户空间代码时,活动进程被认为运行在用户模式下,当CPU运行内核空间代码时,活动进程被认为运行在内核模式下。
内核与每个进程共享其地址空间,原因如下:因为每个进程在给定的时刻都使用系统调用,这将涉及内核。将内核的虚拟内存地址映射到每个进程的虚拟地址空间能够避免每次进入(或者退出)内核时内存地址切换产生的开销。这就是内核地址空间被永久映射到每个进程顶部的原因—加快系统调用对内核的访问。
每个进程地址空间顶部都是内核的虚拟地址空间,这一部分每个进程都是相同的。
内存管理单元把内存组织为大小固定的单元—页面,内存页(虚拟页)指的是连续虚拟内存块,内核数据结构也使用相同的名称页面来表示内存页。帧(页面帧)指一段固定长度的连续物理内存块,操作系统在其上映射内存页。每个页面帧都有一个号码,叫做页面帧号(PFN)。
内核地址的低端和高端内存概念
Linux内核具有自己的虚拟地址空间。比如32位的x86,内核的虚拟地址空间是1GB大小,分成两个部分:
- 低端内存或LOWMEM:第一个896MB
- 高端内存或HIGHMEM:顶部的128MB
低端内存
内核地址空间的第一个896MB空间构成低端内存区域。在启动早期,内核永久映射这896MB的空间。该映射产生的地址为逻辑地址,这些都是虚拟地址,但是减去固定的偏移量后就可以将其转换为物理地址。因为映射是永久的,并且事先知道。大多数内核内存函数返回低端内存。事实上,为了满足不同的用途,内核内存被划分为区域,LOWMEM的第一个16MB内存保留为DMA使用。内核空间可以确定3种不同的内存区域:
- ZONE_DMA:包含的内存页面帧在0~16MB,用于直接内存访问(DMA)
- ZONE_NORMAL:包含的内存页面帧为16MB~896MB,常规使用
- ZONE_HIGHMEM:包含的内存页面帧位于896MB及其以上
这就是说,512MB的系统上,不存在以上的划分。
逻辑地址的另一个定义:线性映射到物理地址上的内核空间中的地址,可以用偏移量或者应用位掩码将其转为物理地址,使用__pa(地址)宏可以将逻辑地址(内核中的虚拟地址)转换为物理地址,使用__va(地址)可以做相反的操作。
高端内存
内核地址空间顶部顶部128MB称为高端内存区域,内核用它临时映射1GB以上的物理内存,当需要访问896MB以上的物理内存时,内核会使用这128MB创建到其虚拟地址空间的临时映射,也就是将需要访问数据的物理页映射到这128MB内核虚拟地址空间来,从而实现访问所有物理页面的目标。可以把高端内存定义为逻辑地址存在的内存,但不会将其永久映射到内核地址空间。896MB以上的物理内存按需映射到HIGHMEM区域的128MB。
访问高端内存的映射由内核动态创建,访问后销毁,这使高内存访问速度变慢,64位系统上不存在高端内存这一概念。
地址转换和MMU
每次访问内存位置时,由CPU完成从虚拟地址到物理地址的转换。该机制称为地址转换,这由CPU中的内存管理单元(MMU)来执行。MMU转换的都是虚拟地址,所以访问数据,必须是虚拟地址,不能是物理地址,否则访问不了数据。
对于虚拟内存,内存组织为固定大小的页,而物理内存则按帧组织,页面表(PTE)概念的引入是为了管理页面和帧之间的映射。页面分部在表间,因此每个PTE的表项对于一个页面和帧之间的映射,然后给每个进程一组页面表来描述其整个内存空间。
Linux中的四级分页模型
Linux采用了一种同时适用于32位和64位系统的普通分页模型。从2.6.11版本开始,采用了四级分页模型:
上图展示的4种页表分别被称为:
- 页全局目录(Page Global Directory,PGD)
- 页上级目录(Page Upper Directory,PUD)
- 页中间目录(Page Middle Directory,PMD)
- 页表(Page Table,PTE)
虚拟地址被分为5个部分,每个页表项指向一个页框,每一部分的大小与具体的计算机体系结构有关。
MMU如何知道进程页面表?很简单,MMU不存储任何地址。但CPU有一个特殊的寄存器,称为页面表基址寄存器(PTBR)或转换基址寄存器0(TTBR0),它指向进程1级页面表(PGD)的基址。这正是struct mm_struct
的字段pgd指向的地址:current->mm.pgd == TTBR0.
上面图中的cr3保存的就是该值
虚拟地址字段
下面宏简化了页表处理:
- PAGE_SHIFT:指定Offset字段的位数,这个宏由PAGE_SIZE使用返回页的大小。最后,PAGE_MASK宏用以屏蔽Offset字段的所有位。
- PMD_SHIFT:指定虚拟地址的offset字段和table字段的总位数,PMD_MASK宏用于屏蔽Offset字段与Table字段的所有位
- PUD_SHIFT:确定页上级目录项能映射的区域大小的对数,PUD_MASK宏用于屏蔽Offset字段,Table字段、Middle Air字段和Upper Air字段的所有位
- PGDIR_SHIFT:确定页全局目录项能映射的区域大小的对数。PGDIR_MASK宏用于屏蔽Offset、Table、Middle Air及Upper Air字段的所有位
- PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUS以及PTRS_PER_PGD:用于计算页表、页中间目录、页上级目录和页全局目录表中表现的个数。
页表处理
pte_t、pmd_t、pud_t和pgd_t分别描述页表项、页中间目录项、页上级目录项和页全局目录项。
五个类型转换宏(__pte,__pmd,__pgd和__pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val,pud_val,pgd_val和pgport_val)执行相反的转换。
如果相应的表项值位0,那么,宏pte_none、pmd_none、pud_none、和pgd_none产生的值为1,否则产生的值为0
将虚拟地址转换物理地址
进程访问的都是用户空间内虚拟地址,内核访问的都是内核虚拟地址,进程用不了内核虚拟地址,同样内核用不了进程内虚拟地址。如果我们将用户空间地址传给内核,内核必须先将找到该用户空间虚拟地址对应的物理地址,再将物理地址转换成内核虚拟地址,内核才能访问数据。
代码地址:http://edsionte.com/techblog/archives/1966
static void get_pgtable_macro(void)
{printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);printk("PUD_SHIFT = %d\n", PUD_SHIFT);printk("PMD_SHIFT = %d\n", PMD_SHIFT);printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}
static unsigned long vaddr2paddr(unsigned long vaddr)
{pgd_t *pgd;pud_t *pud;pmd_t *pmd;pte_t *pte;unsigned long paddr = 0;unsigned long page_addr = 0;unsigned long page_offset = 0;pgd = pgd_offset(current->mm, vaddr);printk("pgd_val = 0x%lx\n", pgd_val(*pgd));printk("pgd_index = %lu\n", pgd_index(vaddr));if (pgd_none(*pgd)) {printk("not mapped in pgd\n");return -1;}pud = pud_offset(pgd, vaddr);printk("pud_val = 0x%lx\n", pud_val(*pud));if (pud_none(*pud)) {printk("not mapped in pud\n");return -1;}pmd = pmd_offset(pud, vaddr);printk("pmd_val = 0x%lx\n", pmd_val(*pmd));printk("pmd_index = %lu\n", pmd_index(vaddr));if (pmd_none(*pmd)) {printk("not mapped in pmd\n");return -1;}pte = pte_offset_kernel(pmd, vaddr);printk("pte_val = 0x%lx\n", pte_val(*pte));printk("pte_index = %lu\n", pte_index(vaddr));if (pte_none(*pte)) {printk("not mapped in pte\n");return -1;}//页框物理地址机制 | 偏移量page_addr = pte_val(*pte) & PAGE_MASK;page_offset = vaddr & ~PAGE_MASK;paddr = page_addr | page_offset;printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);return paddr;
}
static int __init v2p_init(void)
{unsigned long vaddr = 0;printk("vaddr to paddr module is running..\n");get_pgtable_macro();printk("\n");vaddr = (unsigned long)vmalloc(1000 * sizeof(char));if (vaddr == 0) {printk("vmalloc failed..\n");return 0;}printk("vmalloc_vaddr=0x%lx\n", vaddr);vaddr2paddr(vaddr);printk("\n\n");vaddr = __get_free_page(GFP_KERNEL);if (vaddr == 0) {printk("__get_free_page failed..\n");return 0;}printk("get_page_vaddr=0x%lx\n", vaddr);vaddr2paddr(vaddr);return 0;
}
static void __exit v2p_exit(void)
{printk("vaddr to paddr module is leaving..\n");vfree((void *)vaddr);free_page(vaddr);
}
整个程序的结构如下:
-
get_pgtable_macro()打印当前系统分页机制中的一些宏。
-
通过vmalloc()在内核空间中分配内存,调用vaddr2paddr()将虚拟地址转化成物理地址。
-
通过__get_free_pages()在内核空间中分配页框,调用vaddr2paddr()将虚拟地址转化成物理地址。
-
分别通过vfree()和free_page()释放申请的内存空间。
vaddr2paddr()的执行过程如下:
-
通过pgd_offset计算页全局目录项的线性地址pgd,传入的参数为内存描述符mm和线性地址vaddr。接着打印pgd所指的页全局目录项。
-
通过pud_offset计算页上级目录项的线性地址pud,传入的参数为页全局目录项的线性地址pgd和线性地址vaddr。接着打印pud所指的页上级目录项。
-
通过pmd_offset计算页中间目录项的线性地址pmd,传入的参数为页上级目录项的线性地址pud和线性地址vaddr。接着打印pmd所指的页中间目录项。
-
通过pte_offset_kernel计算页表项的线性地址pte,传入的参数为页中间目录项的线性地址pmd和线性地址vaddr。接着打印pte所指的页表项。
-
pte_val(*pte)先取出页表项,与PAGE_MASK相与的结果是得到要访问页的物理地址;vaddr&~PAGE_MASK用来得到线性地址offset字段;两者或运算得到最终的物理地址。
-
打印物理地址。
我们可以获得物理地址了,就可以使用__pa(地址)宏可以将物理地址转换为逻辑地址(内核中的虚拟地址),使用__va(地址)可以做相反的操作