全部博文(333)
分类: LINUX
2016-12-16 17:34:46
在 Linux内核中对应进程内存区域的数据结构是: vm_area_struct, 内核将每个内存区域作为一个单独的内存对象管理,相应的操作也都一致。采用面向对象方法使VMA结构体可以代表多种类型的内存区域--比如内存映射文件或 进程的用户空间栈等,对这些区域的操作也都不尽相同。
vm_area_strcut 结构比较复杂,关于它的详细结构请参阅相关资料。我们这里只对它的组织方法做一点补充说明。vm_area_struct是描述进程地址空间的基本管理单 元,vm_area_struct代表当前的内存区域,包括起始地址,结束地址,标志,对于一个进程来说往往需要多个内存区域来描述它的虚拟空间,如何关 联这些不同的内存区域呢?大家可能都会想到使用链表,的确vm_area_struct结构确实是已链表形式链接,不过位了方便查找,内核又以红黑树(以 前的内核使用平衡树)的形式组织内存区域,以便降低搜索耗时。并存两种组织形式,并非冗余:
内核组织管理进程的内存区域的方法:
链表(vma)用于需要遍历全部节点的时候用,而
红黑树适用于在地址空间中定位特定内存区域的时候。内核为了内存区域上的各种不同操作都能获得高性能,所以同时使用了这两种数据结构。
下图反映了进程地址空间的管理模型:
注意区分下面两个概念:
进程的内存空间(地址空间):mm_struct *mmap
进程的内存区域:vm_area_struct : 每一个代表一个内存区域
进程虚拟地址
进程的地址空间对应的描述结构是“内存描述符结构”(mm_struct),它表示进程的全部地址空间,——包含了和进程地址空间有关的全部信息,其中当然包含进程的内存区域。
8.1.4如何由虚变实(缺页异常)
a. 何时会产生缺页异常?(两种情况)
b. nopage操作。(struct vm_operations_struct {}中)
从 上面已经看到进程所能直接操作的地址都为虚拟地址。当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存 (物理页框——页的概念请大家参与硬件基础一章,是对操作系统有利的错误),获得的仅仅是对一个新的线性地址区间的使用权。实际的物理内存只有当进程真的 去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的例程。
该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去真正为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在映射到了系统物理内存上。(当然如果页被换出到磁盘,也会产生缺页异常,不过这时不用再建立页表了)
这 种请页机制把页框的分配推迟到不能再推迟为止,并不急于把所有的事情都一次做完(这中思想由点想涉及模式中的代理模式(proxy))。之所以能这么做是 利用了内存访问的“局部性原理”,请页带来的好处是节约了空闲内存,提高了系统吞吐。要想更清楚的了解请页,可以看看《深入理解linux内核》一书。
这 里我们需要说明在内存区域结构(vm_area_struct)上的nopage操作,该操作是当发生访问的进程虚拟内存而发现并未真正分配页框时,该方 法变被调用来分配实际的物理页,并为该页建立页表项。在最后的例子中我们会演示如何使用该方法。这个方法需要我们自己去实现,否则会使用系统默认的方法。
虚拟内存管理: vm_area_struct
物理内存管理: struct page
Linux 内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数4k(在i386体系结构中)大小页,从而分配和回收内存的基本单位便是内存页了。利用分 页管理有助于灵活分配内存地址,因为分配时不必要求必须有大块的连续内存,系统可以东一页、西一页的凑出所需要的内存供进程使用。虽然如此,但是实际上系 统使用内存还是倾向于分配连续的内存块,因为分配连续内存时,页表不需要更改,因此能降低TLB的刷新率(频繁刷新会很大增加访问速度)。
鉴 于上述需求,内核分配物理页为了尽量减少不连续情况,采用了“伙伴”关系来管理空闲页框。伙伴关系分配算法大家不应陌生——几乎所有操作系统书都会提到, 我们不去详细说它了,如果不明白可以参看有关资料。这里只需要大家明白Linux中空闲页面的组织和管理利用了伙伴关系,因此空闲页面分配时也需要遵循伙 伴关系,最小单位只能是2的幂倍页面大小。内核中分配空闲页框的基本函数是get_free_page/get_free_pages,它们或是分配单页 或是分配指定的页框(2、4、8…512页)。
注 意:get_free_page是在内核中分配内存,不同于malloc在用户空间中分配,malloc利用堆动态分配,实际上是调用brk()系统调 用,该调用的作用是扩大或缩小进程堆空间(它会修改进程的brk域)。如果现有的内存区域不够容纳堆空间,则会以页面大小的倍数位单位,扩张或收缩对应的 内存区域,但brk值并非以页面大小为倍数修改,而是按实际请求修改。因此Malloc在用户空间分配内存可以以字节为单位分配,但内核在内部仍然会是以 页为单位分配的。
另外需要提及的是,物理页在系统中由页框结构struct page描述,系统中所有的页框存储在数组mem_map[]中,可以通过该数组找到系统中的每一页(空闲或非空闲)。而其中的空闲页框则可由上述提到的 以伙伴关系组织的空闲页链表(free_area[MAX_ORDER])索引。
叶框与页区别
页的状态
page cache, buffer cache
file inode
char tr = malloc(0);
内核空间申请内存的方法:
kmalloc()和__get_free_pages(): < 896M 且虚拟地址和PA都连续 . 返回的是虚拟地址,-3G = PA
vmalloc(): 高端内存分配 > 896M 虚拟地址是连续,PA不一定连续
内存分配: _get_free_pages
注意每一个函数的作用、参数、返回值、验证方法、释放函数
对于我们驱动而言:需要把握的是分配内存,使用内存,内存映射
在linux内核空间申请内存涉及的函数主要包括kmalloc()、__get_free_pages()和vmalloc()等。kmalloc() 和__get_free_pages()申请的内存位于物理内存映射区域(《896M,所以容易操作,可以得到虚拟地址与物理地址),而且在物理上也是连 续的,它们与真实的物理地址只有一个固定的偏移,因此存在简单的转换关系。而vmalloc()在虚拟内存空间给出一块连续的内存空间(>896, 虚拟地址上连续),实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc()申请的虚拟内存和物理内存之间也没有简单的换算关系。
kmalloc 函数内幕(基于slab分配器,分配空间为32K-128K)
一. 用kmalloc()分配内存后: 1。 要对返回的地址验证(任何一个分配内存的方法都要进行此验证) 2。分配的物理内存会保持原来的数据,所以要清除,如下:
if (!(fbdev->info.pseudo_palette = kmalloc(sizeof(u32) * 16, GFP_KERNEL))) {
return -ENOMEM;
}
memset(fbdev->info.pseudo_palette, 0, sizeof(u32) * 16);
二. Kmalloc()的释放:void kfree(void *)
kmalloc是建立在slab分配器之上一个功能强大且高速(除非被阻塞)的工具。所分配到的内存在物理内存中连续且保持原有的数据(不清零)。原型:
#include void kfree(void *) |
size 参数
内核管理系统的物理内存,物理内存只能按页面进行分配。kmalloc 和典型的用户空间 malloc 在实际上有很大的差别,内核使用特殊的基于页的分配技术,以最佳的方式利用系统 RAM。
必 须注意的是:内核只能分配一些预定义的、固定大小的字节数组。kmalloc 能够处理的最小内存块是 32 或 64 字节(体系结构依赖),而内存块大小的上限随着体系和内核配置而变化。考虑到移植性,不应分配大于 128 KB的内存。若需多于几个 KB的内存块,最好使用其他方法。
flags 参数
内存分配最终总是调用 __get_free_pages 来进行实际的分配,这就是 GFP_ 前缀的由来。
所有标志都定义在
主要的标志常被称为分配优先级,包括:
GFP_KERNEL
最 常用的标志,意思是这个分配代表运行在内核空间的进程进行。内核正常分配内存。当空闲内存较少时,可能进入休眠来等待一个页面。当前进程休眠时,内核会采 取适当的动作来获取空闲页。所以使用 GFP_KERNEL 来分配内存的函数必须是可重入,且不能在原子上下文中运行。
GFP_ATOMIC
内 核通常会为原子性的分配预留一些空闲页。当当前进程不能被置为睡眠时,应使用 GFP_ATOMIC,这样kmalloc 甚至能够使用最后一个空闲页。如果连这最后一个空闲页也不存在,则分配返回失败。常用来从中断处理和进程上下文之外的其他代码中分配内存,从不睡眠。
GFP_USER
用来为用户空间分配内存页,可能睡眠。
GFP_HIGHUSER
类似 GFP_USER,如果有高端内存,就从高端内存分配。
GFP_NOIO
GFP_NOFS
功能类似 GFP_KERNEL,但是为内核分配内存的工作增加了限制。具有GFP_NOFS 的分配不允许执行任何文件系统调用,而 GFP_NOIO 禁止任何 I/O 初始化。它们主要用在文件系统和虚拟内存代码。那里允许分配休眠,但不应发生递归的文件系统调用。
alloc_pages类(分配页,2的整数幂)
此类函数主要包括:
struct page * alloc_page(unsigned int gfp_mask)——分配一页物理内存并返回该页物理内存的page结构指针。
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)——分配 个连续的物理页并返回分配的第一个物理页的page结构指针,order为2的指数。
unsigned long get_free_page(unsigned int gfp_mask)——分配一页物理内存并将该物理页全部清零,最后返回一个虚拟(线形)地址。
unsigned long __get_free_page(unsigned int gfp_mask)——Allocates a single page and returns a virtual address。
Unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)——Allocates 2order number of pages and returns a virtual address。
struct page * __get_dma_pages(unsigned int gfp_mask, unsigned int order)——Allocates 2order number of pages from the DMA zone and returns a struct page。
I386体系的CPU将物理内存分为三个区:DMA区,normal区,高端内存区,阀值分别为:16M, 896M。 而ARM体系中没有这个划分。
此类函数主要通过伙伴分配系统进行分配,它们是linux内核最基本的内存分配函数,一次请求能分配的最大物理页数由变量MAX_ORDER决定。
相应的内存释放函数如下:
void __free_pages(struct page *page, unsigned int order)
Frees an order number of pages from the given page.
void __free_page(struct page *page)
Frees a single page.
void free_page(void *addr)
Frees a page from the given virtual address.
在以上的内存分配方法中都是围绕在一个struct page的结构体上进行的,下面我们来介绍一下struct page。
系统中每一个物理页有一个 struct page. 这个结构的一些成员包括下列:
atomic_t count;
这个页的引用数. 当这个 count 掉到 0, 这页被返回给空闲列表.
void *virtual;
这 页的内核虚拟地址, 如果它被映射; 否则, NULL. 低内存页一直被映射; 高内存页常常不是. 这个成员不是在所有体系上出现; 它通常只在页的内核虚拟地址无法轻易计算时被编译. 如果你想查看这个成员, 正确的方法是使用 page_address 宏, 下面描述.
unsigned long flags;
一套描述页状态的一套位标志. 这些包括 PG_locked, 它指示该页在内存中已被加锁, 以及 PG_reserved, 它防止内存管理系统使用该页.
有很多的信息在 struct page 中, 但是它是内存管理的更深的黑魔法的一部分并且和驱动编写者无关.
内 核维护一个或多个 struct page 项的数组来跟踪系统中所有物理内存. 在某些系统, 有一个单个数组称为 mem_map. 但是, 在某些系统, 情况更加复杂. 非一致内存存取( NUMA )系统和那些有很大不连续的物理内存的可能有多于一个内存映射数组, 因此打算是可移植的代码在任何可能时候应当避免直接对数组存取. 幸运的是, 只是使用 struct page 指针常常是非常容易, 而不用担心它们来自哪里.
有些函数和宏被定义来在 struct page 指针和虚拟地址之间转换:
struct page *virt_to_page(void *kaddr);
这个宏, 定义在
struct page *pfn_to_page(int pfn);
为给定的页帧号返回 struct page 指针. 如果需要, 它在传递给 pfn_to_page 之前使用 pfn_valid 来检查一个页帧号的有效性.
void *page_address(struct page *page);
返回这个页的内核虚拟地址, 如果这样一个地址存在. 对于高内存, 那个地址仅当这个页已被映射才存在. 这个函数在
#include
void *kmap(struct page *page); >896M的物理地址对应的虚拟地址,先把物理地址按页面分配,然后通过kmap得到对应的内核逻辑地址
void kunmap(struct page *page);
kmap 为系统中的任何页返回一个内核虚拟地址. 对于低内存页, 它只返回页的逻辑地址; 对于高内存, kmap 在内核地址空间的一个专用部分中创建一个特殊的映射. 使用 kmap 创建的映射应当一直使用 kunmap 来释放;一个有限数目的这样的映射可用, 因此最好不要在它们上停留太长时间. kmap 调用维护一个计数器, 因此如果 2 个或 多个函数都在同一个页上调用 kmap, 正确的事情发生了. 还要注意 kmap 可能睡眠当没有映射可用时.
#include
#include
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
kmap_atomic 是 kmap 的一种高性能形式. 每个体系都给原子的 kmaps 维护一小列插口( 专用的页表项); 一个 kmap_atomic 的调用者必须在 type 参数中告知系统使用这些插口中的哪个. 对驱动有意义的唯一插口是 KM_USER0 和 KM_USER1 (对于直接从来自用户空间的调用运行的代码), 以及 KM_IRQ0 和 KM_IRQ1(对于中断处理). 注意原子的 kmaps 必须被原子地处理; 你的代码不能在持有一个时睡眠. 还要注意内核中没有什么可以阻止 2 个函数试图使用同一个插口并且相互干扰( 尽管每个 CPU 有独特的一套插口). 实际上, 对原子的 kmap 插口的竞争看来不是个问题.
vmalloc详解
vmalloc 是一个基本的 Linux 内存分配机制,它在虚拟内存空间分配一块连续的内存区(注意是虚拟内存空间连续,不一定物理内存连续),尽管这些页在物理内存中不连续 (使用一个单独的 alloc_page 调用来获得每个页),但内核认为它们地址是连续的。 应当注意的是:vmalloc 在大部分情况下不推荐使用。因为在某些体系上留给 vmalloc 的地址空间相对小,且效率不高。函数原型如下:
#include |
kmalloc
和 _get_free_pages 返回的内存地址也是虚拟地址,其实际值仍需 MMU
处理才能转为物理地址(-3G)。vmalloc和它们在使用硬件上没有不同,不同是在内核如何执行分配任务上:kmalloc 和
__get_free_pages 使用的(虚拟)地址范围和物理内存是一对一映射的, 可能会偏移一个常量 PAGE_OFFSET
值,无需修改页表。
而vmalloc使用的地址范围完全是虚拟的,且每次分配都
要通过适当地设置页表来建立(虚拟)内存区域。 vmalloc 可获得的地址在从 VMALLOC_START 到 VAMLLOC_END
的范围中,定义在