Linux内存管理 —— 为buddy做准备:MMU, TLB, ZONE【转】
转自:https://blog.csdn.net/jasonchen_gbd/article/details/79460984
本文都是假设系统是32位,页大小为4KB,基于ARM架构(不过和体系结构相关的内容不多)。
1. 了解MMU
在启用MMU的Linux内核中,CPU是通过虚拟地址来访问物理内存的。MMU(Memory Management Unit),即内存管理单元,它集成在CPU中,负责虚拟地址到物理地址的映射,以及物理地址的访问,并提供内存访问权限检查机制,以达到内存保护的作用(ARM里面的MPU(Memory Protection Unit)也可以做内存保护,但不能做虚拟内存映射)。
一旦启动MMU,CPU就只通过虚拟地址访问内存了。虚拟地址通过页表找到其映射的物理地址,前面已经讲过,根据页号和页内偏移找到物理地址。页表的起始地址要告知MMU,这样,CPU访存的时候,MMU拿到虚拟地址,查找页表找到其物理地址,然后访问(读/写等)物理内存或cache。
由于每个进程都有自己的虚拟地址空间,因此每个进程要有自己独立的页表。在进程切换时,就要向MMU更新页表的起始地址。在ARM中,页表的起始地址就存放在TTB(Translation table base)寄存器中,在进程切换时,通过$(CPU_NAME)_switch_mm(),如cpu_v7_switch_mm来更新页表的base pointer,实际上就是协处理器的操作。
如上所说,页表是有权限管理的,权限也存放于页表中,包括RWX权限和kernel/user+kernel权限(权限都是以页即4KB为单位的),例如你试图对const变量或代码段进行写操作,而页表中这些页的权限是只读,就会发生page fault,导致应用程序段错误。
当MMU找不到对应的物理地址或权限不对,都会触发page fault异常,当然不是所有page fault都是导致程序崩溃,后面会讲到。
注意,页表也是存放在内存里的,kernel只是创建和更新页表,MMU会访问页表。内存映射是以一页(通常是4KB)为单位的,所以一个页表中肯定存放了4KB的整数倍的内存映射关系。
MMU每次都通过页表寻址很慢,TLB(Translation Look-aside Buffers)也是CPU里的一个硬件单元,是页表的高速缓存,用于改善虚拟地址到物理地址转换的速度。因此在查找某个虚拟地址到物理地址映射关系的时候,MMU会先尝试在tlb中命中,如果命中就省去和内存打交道(查页表即TTW)了;如果没命中,MMU就去查找内存中的页表,同时将相应条目放到tlb中去命中。
所以,tlb相对于页表,就相当于cache相对于内存。只是把频繁最近的页表项目放入tlb,这样查起来快。
还有一个机制叫做“lazy tlb”,本文略过,因为我看到mips和arm里面都没有lazy tlb,而x86且开启CONFIG_SMP则支持lazy_tlb。lazy的意思是说如果是用户态进程切换到一个内核线程时,由于用不到用户态的映射关系并且内核态的页表完全相同,就无需刷tlb了,可以带来一点性能提升。每个CPU的tlb有两个状态,TLBSTATE_OK和TLBSTATE_LAZY,前者表示该tlb没有设置为lazy_tlb,后者表示该tlb设置成了lazy_tlb。在进程切换过程中刷新tlb和mmu之前,就会做此检查。
2. 页表中虚拟地址空间的范围
进程被创建后,页表随着使用内存的增加逐渐增大。用户态申请的内存映射到0~3G的虚拟地址,但3G以上也是有内容的。实际上,内核态没有进程的概念,因此内核态的内存映射使用同一张表即可,但内核为了方便,将3G以上的映射关系copy每个进程的页表中,也就是说,每个进程的页表都涵盖0~4G,其中0~3G的表项是根据用户情况各自映射,而3G~4G的表项每个进程是完全相同的,因为内核态没有独立的页表,一个进程陷入内核态之后,还是用该进程的页表(内核使用current进程的页表),就省去了切换页表的开销(但是meltdown漏洞修复后,内核态就使用单独页表了,但速度慢了)。
给页表添加条目的过程可参考vmalloc的实现,vmalloc允许申请虚拟地址连续但物理地址不连续的页,要重新建立映射关系并填入页表中。相应函数为(mm/vmalloc.c):
static int vmap_page_range(unsigned long start, unsigned long end,
pgprot_t prot, struct page **pages)
- 1
- 2
该函数将start~end这段连续虚拟地址,映射到数组pages包含的各个页帧中,页的保护状态在prot中指定。
另外,在内核中进行mapping操作(vmap/ioremap/vmalloc)时,还需要调用flush_cache_vmap()进行一次full cache flush,因为如果cache是non-VIPT的,这些被映射的页中如果还有被cached的数据,如果不及时写回,就可能通过新映射写入的数据覆盖掉。
3. 内存ZONE的划分
在前面讲bootmem的时候,看到物理内存被分成了几个ZONE。这些ZONE并非每个都是必需的,因此我们要了解一下每个ZONE的用途。
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
首先强调一点,ZONE都是物理地址的概念,bootmem是将物理内存划分成几个ZONE。所以内核中的各ZONE的定义确切的讲应该叫做zone的映射区。
对于32位系统,虚拟地址和物理地址都可能是0~4GB的地址空间,内核空间的虚拟地址为3G~4G。内核为了访问方便,在启动的时候(paging_init)将3G~低于某个地址的内存进行简单的线性映射。那么,我们把低于这个地址的内存区域称为lowmem,高于这个地址的内存区域称为highmem。所以,虽然lowmem也是虚拟地址,但转换成物理地址并不麻烦(也是MMU通过页表转换的!即使只是简单的加法或与操作)。
DMA ZONE: 这个zone是因为DMA引擎的一些缺陷才存在的。DMA可以直接在内存和外设之间进行数据搬移。CPU访存一般是通过CPU<->cache<->memory,但如果外设支持DMA,就可以直接通过device<->DMA<->memory。DMA可以直接访问内存,在访问内存上和CPU的地位是对等的,在二者同时访问内存时,硬件上有一个仲裁器来决定谁当前可以优先访问。
但是由于某些设备的DMA会有一些限制,它不能访问到所有的内存地址:例如x86的alsa总线,它的地址线只能访问到16MB以下的地址空间,再高的地址就寻址不到了,x86为了适应这种DMA,简单粗暴地将16MB以下的内存作为了DMA ZONE,有了这个ZONE,CPU在使用dma_alloc_coherence()分配一致性内存时,就知道从0~16MB里面去分配,但前提要加上GFP_DMA标记,这样就保证了这个设备要申请的内存肯定是地址线能寻址到的。因此,DMA ZONE的大小完全是硬件决定的。如果你所有硬件的所有DMA都没有寻址限制,那DMA ZONE就没有存在的必要了。
64位系统中可能会有两个DMA ZONE,一个DMA,一个DMA32,让32位的硬件也能在这两个区域申请到内存。
注意,DMA ZONE不是只为DMA用的,谁都可以用。只是对于DMA可以指定在这里分配而已。
HIGHMEM ZONE: 这个zone是因为内存空间整体不够的场景才存在的。如果你的物理内存小于1G,那就全部线程映射好了,不需要HIGHMEM ZONE。而对于64位机,肯定能全部直接映射因此就不需要HIGHMEM ZONE。
我们通常说的highmem(高端内存)是指HIGHMEM区域。lowmem(低端内存)是指DMA/NORMAL等区域。区分这两种区域,是因为内核中提供的一个zone中物理地址到虚拟地址映射时的API不同。highmem使用kmap()(和vmalloc()等),而lowmem使用phys_to_virt()/virt_to_phys()直接映射。
lowmem上限和DMA_ZONE之间便是NORMAL_ZONE,实际上,如果你的物理内存很小,可能连NORMAL ZONE都不需要。
kmalloc的时候常用GFP_KERNEL,它从低端区域申请内存。kmalloc的语义就是从线性映射区去拿内存,因此最好用GFP_KERNEL就OK了,而如果要去highmem申请,就改用vmalloc。
vmalloc或用户态malloc内存也可以来自lowmem(这是肯定的,因为可能根本就没有高端内存域。当然,此时需要额外页表项来映射),只是优先从较高端的空间申请,vmalloc其实就是在GFP_KERNEL基础上加了__GFP_HIGHMEM标记。
所以,两个虚拟地址可以映射到同一个页,例如一个物理页,在内核启动时被线性映射了(但是尚未被使用),一个用户态进程也可以申请到这个页并将其虚拟地址映射到这个页。但这时由于这个页被使用了,内核就无法申请到这个页了(但由于内核权限足够大,是可以直接读写这个页的,只要能找到映射关系,很不道德……)。这里要注意区分“映射”和“申请”,映射只是修改页表或线性映射,但还没有被使用,而申请就是被占用了,别人用不了了。
内存空间不仅是内存,它可以包括寄存器甚至其他外设的存储区域。我们在通过ioremap将一组寄存器映射到vmalloc映射区时,也是有对应页表项的,通过ioremap返回的虚拟地址就可以访问寄存器了。
在/proc/vmallocinfo中,可以看到vmalloc和ioremap的映射情况。