linux内核中有许多种不同的地址类型
虚拟内存是用来描述一种不直接映射计算机物理内存的方法。分页是在虚拟内存与物理内存转换时用到的。请参阅intel手册了解更多分页系统的知识。
低于896MB的每页内存都被直接映射到内核空间。高于896的内存,又称高端内存,不会一直映射到内存空间,而是使用kmap和kmap_atomic来临时映射。剩余的126MB内存的一部分用于映射高端内存。
内核内存从PAGE_OFFSET开始,在x86架构中它的值是0xc0000000(3G),高于PAGE_OFFSET的虚拟内存用于内核空间,低于的用于用户空间。
阅读ULK3学习更多内存管理的细节
物理地址被分成离散的单元,成为页。目前大多数系统的页面大小都为4k。实际使用的时候应该使用指定体系架构下的页面大小PAGE_SIZE。PAGE_SHIFT可以将地址转换为页帧。
系统中逻辑地址和虚拟地址不一致的情况产生了高端内存和低端内存的说法。
通常linux x86内核将4GB的虚拟地址分割为用户空间和内核空间;在二者的上下文中使用相同的映射。一个典型的分配是将低地址3GB分给用户空间,将剩下的高地址1GB分给内核空间。这样由于内核只能直接操作已经映射了物理内存的虚拟地址,所以内核在大内存系统中就不能直接访问所有的物理内存。这样就产生了高端内存和低端内存的说法。
高端内存是没有直接映射到物理内存的内核逻辑地址
在访问特定的高端内存之前,内核必须建立明确的虚拟映射,使该页可以在内核地址空间被访问。
总的来说高端内存就是没有逻辑地址的内存,反之就是低端内存。
由于高端内存中无法使用逻辑地址,所以内核中处理内存的函数趋向于使用指向page结构的指针。该结构保存了内核需要知道的所有物理内存的信息。系统中的每个物理页都和一个page结构对应。
page结构和虚拟地址之间转换的函数和宏:
In a virtual memory system all of these addresses are virtual addresses and not physical addresses. These virtual addresses are converted into physical addresses by the processor based on information held in a set of tables maintained by the operating system.在虚拟内存系统中,所有的地址都是虚拟地址而不是物理地址。这些虚拟地址可以通过操作系统维护的一系列的表转换为物理地址。
To make this translation easier, virtual and physical memory are divided into handy sized chunks called pages. These pages are all the same size, they need not be but if they were not, the system would be very hard to administer. Linux on Alpha AXP systems uses 8 Kbyte pages and on Intel x86 systems it uses 4 Kbyte pages. Each of these pages is given a unique number; the page frame number (PFN).为了使这个转换更加简单,虚拟地址和物理地址都被分成叫做内存页面小的内存块。所有的页面都是同样大小。每页内存都有一个唯一的编号,这种编号叫做页帧号。
In this paged model, a virtual address is composed of two parts; an offset and a virtual page frame number. If the page size is 4 Kbytes, bits 11:0 of the virtual address contain the offset and bits 12 and above are the virtual page frame number. Each time the processor encounters a virtual address it must extract the offset and the virtual page frame number. The processor must translate the virtual page frame number into a physical one and then access the location at the correct offset into that physical page. To do this the processor uses page tables.在这种分页模式下,虚拟地址由两部分组成;页帧内的偏移和虚拟页帧号。如果页面大小是4KB,11:0这些位就是页帧内偏移,12位以上的叫做页帧号。每当处理器遇到虚拟内存地址,它就会把地址中的页内偏移和页帧号解出来。处理器通过页表把虚拟帧号转换成物理帧号,然后在加上页内偏移就可以找到对应的物理地址了。
现代系统中,处理器需要使用某种机制将虚拟地址转换成物理地址。这种机制被成为页表;它基本上是一个多层树形结构,结构化的数组中包含了虚拟地址到物理地址的映射和相关的标志位。
Linux uses demand paging to load executable images into a processes virtual memory. Whenever a command is executed, the file containing it is opened and its contents are mapped into the processes virtual memory. This is done by modifying the data structures describing this processes memory map and is known as memory mapping. However, only the first part of the image is actually brought into physical memory. The rest of the image is left on disk. As the image executes, it generates page faults and Linux uses the processes memory map in order to determine which parts of the image to bring into memory for execution.Linux使用按需分页来将可执行镜像载入到进程的虚拟内存空间。每当命令执行时,命令的文件被打开,内容被映射到进程的虚拟内存上。这里是通过修改进程的内存映射相关结构体来实现的,这个过程也叫做内存映射。不过,只有镜像的开头部分被真正的放进了物理内存。余下部分还在磁盘上。镜像执行的时候,它将持续的产生页面异常,linux通过进程的内存映射表来确定镜像的哪个部分需要被载入物理内存执行。
Virtual memory makes it easy for several processes to share memory. All memory access are made via page tables and each process has its own separate page table. For two processes sharing a physical page of memory, its physical page frame number must appear in a page table entry in both of their page tables. 虚拟内存使得多个进程共享内存更加简单。所有的内存访问都要通过页表来实现。对于共享一个物理页的两个进程来说,这个物理页面必须同时在两个进程的页表中都有相应的页表项。
VMA是用于管理进程地址空间中不同区域的内核数据结构。
进程的内存映射至少包含下面这些区域:
可以cat /proc/<pid/maps>
来查看具体进程的内存映射。
当用户空间进程调用mmap时,系统会创建一个新的VMA来相应它。
注意vm_area_struct这个重要的数据结构(定义在中)。
系统中每个进程(除了内核空间的辅助线程)都有一个struct mm_struct结构(定义在中),其中包含了大量的内存管理信息。多个进程可以共享内存管理结构,linux就是使用这种方法实现线程的。
mmap可以将用户空间的内存和设备内存映射起来,这样在访问分配地址范围内的内存时就相当于访问设备内存了。
并非所有的设备都能进行mmap抽象:
PAGE_SIZE
为单位进行映射,因为内核只能在页表一级上对虚拟地址进行管理。 为了执行mmap,驱动程序只需要为该地址范围建立合适的页表,并将vma->vm_ops
替换为一系列的新操作就可以了。有两种建立页表的方法:使用remap_pfn_range
函数一次全部建立;通过VMA的fault方法一次建立一个新页表。
这里我们来看看内核为设备驱动程序提供的内存管理接口。
kmalloc内存分配工具和malloc的使用方法很接近。 它的原型是:
\#include <linux/slab.h> void *kmalloc(size_t size, int flags);
最常用的标志是GFP_KERNEL(GFP的来源是因为kmalloc最终会调用get_free_pages函数),这个标志允许kmalloc在页面不足的情况下休眠。
如果在进程上下文之外使用kmalloc,比如中断处理例程中就需要使用GFP_ATOMIC标志,不会休眠
其他标志都定义在文件中,请阅读该文件后使用他们
内核中使用基于页面的方式管理内存,因此和用户空间的基于堆的简单内存管理有很大的差别
由于slab分配器(即kmalloc的底层实现)最大分配的内存单元是128KB,所以如果分配的内存过大,最好不要使用kmalloc方法
内核实现了一些内存池,内核驱动程序通过使用它们可以减少内存分配的次数。
它的api在中,类型为kmem_cache_t。
内存池其实是某种形式的高速缓冲,它试图始终保持空闲的状态,方便那些要求内存分配不能失败的代码使用。
它的api在中,类型为mempool_t。
如果驱动使用较大块的内存,则适合使用面向页的分配技术。
get_zeroed_page(unsigned int flags); 返回指向新页面的指针并清零
__get_free_page(unsigned int flags); 返回指针但不清零
__get_free_pages(unsigned int flags, unsigned int order); 分配2^order个连续页面,不清零
alloc_pages用来分配描述用struct page描述的页面内存,使用这种结构描述的内核内存在某些地方使用起来非常方便。
struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);
vmalloc分配虚拟地址空间的连续内存。尽管可能这段内存在物理上可能不是连续的。
通过vmalloc获得的内存使用起来效率不高,如果可能,应该直接和单个的页面打交道,也就是使用前面的函数来处理而不是使用vmalloc。vmalloc分配的虚拟地址上可能没有物理内存对应。
kmalloc和__get_free_pages返回的虚拟地址内存范围与物理内存的范围是一一对应的。但vmalloc和ioremap使用的地址范围则是完全虚拟的,每次分配都需要适当的设置页表来建立内存区域。
ioremap也和vmalloc一样建立新页表,但它不会分配内存。它更多用于映射设备缓冲到虚拟内核空间。值得注意的是不能把ioremap返回的指针直接当作内存使用,应该使用I/O函数来访问。
vmalloc的一个小缺点是它不能在原子上下文中使用。
相关的函数定义在中。
当建立一个per-CPU变量时,系统的每个处理器都会拥有该变量的副本。对于per-CPU变量的访问几乎不需要锁定,因为每个处理器有自己的副本。
注意当处理器在修改某个per-CPU变量的临界区中间时,它可能被抢占,需要避免这种情况发生。所以我们应该显式地调用get_cpu_var访问某给定变量的当前处理器副本,结束后调用put_cpu_var。
使用方法:
DMA(Direct Memory Access)是一种高级的硬件机制,它允许外设直接和主内存之间进行I/O传输而不用CPU的干预。
有两种方式可以引发DMA数据传输:软件对数据的请求;硬件异步地把数据传给系统。
第一种情况:
第二种情况:
可以看出,高效的DMA传输依赖于中断报告。
DMA缓冲区的主要问题是:当大于一页时,它必须占用连续的物理页,这是因为多数外设总线都使用物理地址。
使用get_free_pages分配大于128KB内存的时候很容易失败返回-ENOMEM。此时的办法是在引导时分配内存或者为缓冲区保留顶部物理内存。
如果要为DMA分配一大块内存,最好考虑分散聚集I/O。
硬件和程序代码使用不同的地址,所以需要有一个地址转换。
由于多种系统对缓存和DMA的处理不同,内核提供了一个通用DMA层,建议在用到DMA时使用该层。struct device隐藏了描述设备的总线细节,在使用通用DMA层时需要使用到该结构的指针。
接下来的DMA函数都需要包含文件
int dma_set_mask(struct device *dev, u64 mask); 可以用来确定设备是否支持DMA。
下面将介绍linux内核中针对不同的应用场景实现的不同内存分配算法。
在支持NUMA的linux内核当中,系统的物理内存被分为多个节点,在单独节点内,任一给定cpu访问页面所需要的时间都是相同的。每个节点的物理内存又分成多个内存区(zone)。x86下内存区有ZONE_DMA
、ZONE_NORMAL
和ZONE_HIGHMEM
。x86_32系统上,ZONE_HIGHMEM
中的内存没有直接映射到内核线性地址上,在每次使用之前都需要先设置页表映射内存。每个zone下面的内存都是以页框为单位来管理的。
每个zone的内存页面是通过buddy算法来管理的。
页框分配算法需要解决external fragmentation的内存管理问题。linux内核使用buddy算法来解决这个问题。把所有的空闲页分组为2^(order-1)大小的块链表。order的最大值为11,所以一共有11个这样的链表。链表的元素最小的为4k(1一个页面大小),最大的为4M(2^10个页面大小)。请求内存时,内核首先从最接近请求大小的链表中查询,如果有这样的空闲单元,则直接使用。如果没有则一次递增到更大块的内存链表中查询,如果有则将内存分出最接近请求大小的块,在把余下的内存拆分添加到较小的内存链表中。
核心接口
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order);
void __free_pages(struct page *page, unsigned int order);
核心实现
struct page * __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, nodemask_t *nodemask);