[作者:byeyear。首发于cnblogs,转载请注明。联系:east3@163.com]
本文对Linux内存管理使用到的一些数据结构和函数作了简要描述,而不深入到它们的内部。对这些数据结构和函数有了一个总体上的了解后,再针对各项分别作深入了解的时候,也许会简单一些。
Linux内存访问限制(仅针对32位系统)
- 默认情况下Linux的内核空间映射到4G虚拟地址的最高1G(即0xC0000000 - 0xFFFFFFF)
在x86中,这个偏移量表示为TASK_SIZE,也表示为PAGE_OFFSET。
对于其他arch,TASK_SIZE和PAGE_OFFSET之间可能存在HOLE。
- 如果不考虑HIGH_MEMORY,Kernel中内存物理地址和逻辑地址(此时就是虚拟地址)有线性对应关系
有两个函数:virt_to_phys和phys_to_virt用于在内核物理地址和逻辑地址间进行转换。
- 所以,32位系统中,Linux能直接访问的物理内存最大为1G
PHY_ADDR[0] -> LOG_ADDR[3G]
PHY_ADDR[1G] -> LOG_ADDR[4G]
- 除去保留给vmalloc、kmap和其他一些用途的地址空间,可直接访问的物理内存最大为896M
- 进程的虚拟地址空间仍然是4G。0-TASK_SIZE(3G)给user space,各个processor不同;kernel space的1G对每个processor都是一样的。
- 后面我们会说到如何访问1G以上的物理内存。
一些数据结构
- struct vm_area_struct
该结构用来描述进程虚拟地址空间中一段连续的虚拟内存。但是我们知道,进程所占用的虚拟地址空间通常是不连续的(例如要分为代码、Heap和stack;或者考虑经常申请/释放内存的情况),所以一个进程需要多个struct vm_area_struct来描述其virtual memory area。根据进程需要的该结构体数量的大小,这一堆的vm_area_struct将组织成Linear或Linear+AVL的形式。
- struct mm_struct
这个结构用来管理上文所述那一堆的vm_area_struct。在这个结构中有个指针,指向那堆struct vm_area_struct的链表头,另有一个指针指向AVL tree的root(如果有的话)。而mm_struct本身又是task_struct里的一个成员。
- zone和node
在多CPU系统中,每个CPU对应一个node用于描述该CPU所“拥有”的内存区域。这里说的“拥有”是指与该CPU具有最佳亲缘性的内存区域,该CPU访问这些内存时将获得最高的性能。一块CPU也可以访问*非*它所“拥有”的内存区域,但访问“非拥有内存区域”的性能不如访问“拥有的内存区域”的性能。
每个node又拆分成多个zone,不同的zone有不同的属性。例如分配给ISA总线设备进行DMA的内存必须在16M以下(天知道现在的计算机上还有木有ISA设备了),这些内存就归为ZONE_DMA。High Mem归属于ZONE_HIGHMEM。不具有特殊性质的内存都归结到ZONE_NORMAL。
描述node和zone的结构分别为typedef struct pglist_data pg_data_t和struct zone。
每个pg_data_t里有一个struct zone数组,数组的元素个数就是ZONE的类型数。
每个pg_data_t里还有一个struct zonelist结构的数组,数组的每个元素都用来描述一种特定的“分配策略”。所谓“分配策略”,就是在分配内存的时候根据分配需要(kmalloc的第二参数就是用来描述这方面的需要的)确定从哪个zone开始,以及可以使用哪些zone。用于ISA DMA的内存必须在16M以下,所以需要一种“策略”;从High Mem中分配内存必须在1G以上,也需要一种策略;普通的没有特殊要求的内存分配也需要一种策略。有多少种策略,struct zonelist数组中就有多少个元素。
- struct zonelist
该结构用于描述某种特定的分配“策略”,其主要成员是一个数组struct zone *zones[MAX_ZONES_PER_ZONELIST + 1],[...]中的“+1”用来仿照字符串结尾的方式确定数组元素的实际个数。该数组的元素是所有可用于某个特定“策略”的zone们。假定可用于ISA DMA的zone是zoneA,zoneB,zoneC,分配顺序也是从A开始,那么这个数组中的三个元素就是指向zoneA,zoneB,zoneC的指针。
- struct page
Linux会为它所管辖的物理内存的每个Page Frame建立一个struct page数据结构,而不管这个Page Frame是否是HIGH_MEMORY。
- high_memory
这是一个变量,表示能够直接管辖的物理内存尾。如果物理内存小于896M,那么它就是实际物理内存大小;否则就是896M。
vmalloc .vs. kmap
vmalloc用于分配一段虚拟地址空间连续、但是物理地址空间未必连续的内存。vmalloc所能使用的虚拟地址空间在high_memory之上(high_memory + 8M)。由于vmalloc分配得到的内存在虚拟地址和物理地址之间不存在线性关系,所以vmalloc可以从HIGH_MEMORY处分配page frame,而不局限于896M以下。事实上,vmalloc优先考虑HIGH_MEMORY。这里顺便说下,ioremap所得到的虚拟地址也占用vmalloc可用的虚拟地址空间。一般情况下,调用vmalloc能够分配到的内存空间可以远大于一次kmalloc所能得到的。不过vmalloc内部仍然依赖kmalloc逐个分配page frame。
kmap用于在page frame和虚拟地址之间建立映射。kmap本身并不分配物理内存,在使用前需要使用alloc_page先分配page_frame。对于Low memory,kmap将直接返回page frame对应的物理地址。
kmap和vmalloc在应用上的最大区别在于,kmap往往应用于“空间分配”与“地址映射”可以分开的场合。考虑下面的场景:
a) App想要通过网络发送数据;
b) Linux将用户数据复制到内核buffer;
c) NIC driver通过DMA发送数据。
在上述场景的步骤b)中,Linux将执行类似下面的代码(仅作示意,不代表真实代码):
while(more_data_to_copy) { struct page* page = alloc_page(...); void* vadr = kmap_atomic(page); copy_to_kernel(vadr, user_buf, size); kunmap_atomic(page); link_page_to_list(page); }
上面的代码中,一页数据拷贝完成后该页的地址映射就可以立即解除,因为后续的DMA操作是不需要用到内核虚拟地址的。在linux源代码的net目录中的代码大量使用了kmap(_atomic)。这样做(将空间分配和地址映射分离)的好处是可以节约大量的页表(考虑网络服务器中存在大量待收发数据的情况),可以很好的利用HIGH_MEMORY。快速、少量或不可休眠的页映射使用kmap_atomic,其他情况可使用kmap。
vmalloc与page fault
执行vmalloc(包括ioremap)时页表的设置是写在init_mm中的,这样当某个进程试图通过驱动访问该虚拟地址时必然发生page fault。do_page_fault函数判断出page fault发生在vmalloc region,于是从init_mm中将pgd/pud/pmd复制到当前进程,但pte是不复制的——内核地址空间的pte由所有进程共享。如果在将来的某个时刻映射解除,修改的也是init_mm的页表设置。后续对该地址的访问(假设仍为同一进程)在pgd、pud、pmd这三层仍然能够通过(进程有自己的pgd/pud/pmd),但到pte这一层就会发生page fault(init_mm已删除该pte)。类似的情况还有ioremap[注]。
[注] 根据64位atom上安装32位Linux(kernel 2.6.32)的测试结果,页表是3级,各级位宽2/9/9/12,第一级将内存分为4个1G区间,进程pgd的第四条目总是指向init_mm的pmd。这样,只要ioremap已建立好,init_mm对应的pmd就一定有效,也就不会发生page fault。(这种情况下vmalloc的效果未确认)
DirectIO
对于DirectIO来说,数据传输直接发生在设备与应用程序缓冲区之间。因此,一般来说DirectIO往往和DMA配合使用。DirectIO的步骤大体如下:
- 应用程序申请内存
最简单的当然是malloc或new之类;如果你要求获得的内存对齐于页边界,可以用anonymous mmap——使用mmap函数分配虚拟内存,并将mmap函数的flags参数设定为MAP_ANONYMOUS,同时将fd设置为-1(对linux非必须,仅作兼容性考虑)。具体请man mmap。或者使用valloc函数。
- Driver为其分配物理pages
应用程序调用malloc或new或valloc得到的虚拟地址,在初始状态是没有物理页面和其对应的。我们如果要对其做DMA,需要为其分配物理内存,并将其lock,不允许换出。kernel提供get_user_pages函数做这些工作。如果需要的内存空间很大,get_user_pages的执行过程会比较长。
- DMA
一般来说get_user_pages得到的物理页面是不连续的,所以我们需要dma_map_sg。
- 进一步说明
因为DirectIO的数据传输直接发生在设备和应用程序缓冲区之间,所以一般来说Driver是不需要访问缓冲区内容的。如果Driver也需要访问缓冲区内容,需要通过kmap获得这些物理页面对应的内核虚拟地址。
没有对应的put_user_pages函数;你需要自己做个循环调用page_cache_release。
- IBM有篇关于DirectIO的文章写得不错。
应用程序直接访问设备内存
- 应用程序要访问设备内存(例如PCI的memory bar),有两种方法:一种是使用ioremap(或ioremap_nocache),将设备内存物理地址映射到内核虚拟地址,App使用ioctl或read/write通过驱动访问;
- 另一种方法是使用mmap,让App直接访问设备内存。使用这种方法,App需要通过mmap系统调用开辟一块虚拟地址空间,并在驱动中实现file_operations中的mmap函数。App调用mmap时,fd参数设置为打开设备的fd。这块虚拟地址空间大小就是设备内存的大小。驱动在mmap的实现中,调用remap_pfn_range为App开辟的那块虚拟地址空间建立与设备物理内存的映射。和DirectIO相比,由于待映射的物理内存已存在(即设备内存),不需要分配并锁定page,所以速度很快。
- 驱动不需要实现unmap。
- 可能需要对映射得到的内存禁止cache。
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
if(rempa_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot)) return - EAGAIN;
- 应用程序调用如下:
void *p = mmap(0, DEV_MEM_LEN, // 设备内存长度 prot, flags, dev_fd, DEV_MEM_PHY); // 设备内存物理地址
注意:如果你需要对mmap映射区进行写操作,在建立设备文件的时候将文件属性设置为rw。
使用系统内存作为设备内存
- 在嵌入式系统中,我们也许对系统有较大的控制权,可以使用mem=启动参数。这个时候我们可以保留一部分系统内存作为设备内存,设备直接将数据DMA到保留下的系统内存中(这块系统内存在mem=启动参数设定的内存大小之外,因此不会被Linux看到)。然后使用上文所说的“应用程序直接访问设备内存”的方法获取数据。
内核物理页面的分配
内核中有多种情况需要分配物理页面,比如处理page fault,比如driver需要临时或永久存储区,等等,一般来说都是通过kmalloc或辅助函数完成(核心数据结构是struct page)。管理内核物理页面的模块并不关心是谁要用物理页面,你要就给你(如果有),你还就收下。至于分配出去后怎么用,那是调用者的事。
相对而言,应用程序中调用malloc仅仅是划出一段虚拟内存,但并不立即分配对应的物理内存空间。只有当有人读或写了这个页面,才会通过page fault为其分配物理页面,并且随时可能被换出。即,应用程序在用户空间所使用的物理页面都是在内核态分配的,纯用户态代码永远无法获得一块实际的物理内存:这是理所当然的事,因为物理页面的使用必须全部由内核所掌控。
物理页面交换
分配到的物理页面只有被挂在某个可换出的链表上,才会被页面换出代码扫描到。所以,如果你的驱动通过kmalloc获得了一些物理页面自用,它们是不可能被换出的——因为没有挂在某个可换出链表上。上文中通过get_user_pages得到的页面也是不会换出的。另一方面,发生page fault后分配到的物理页面是会被内核挂到某个可换出链表上的。