ldd3-内核内存分配

来源:互联网 发布:知乎 模拟联合国 编辑:程序博客网 时间:2024/05/22 12:01
ldd3-内核内存分配
先总结内存分配,明天我再来总结Mmap和DMA技术
-------------------------------
以下忽略了内存池分配技术和per-CPU变量的相关内容。
kmalloc函数
不对所获取的内存空间清零,分配的区域在物理内存中也是连续的。
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
最常用的标志是GFP_KERNEL,表示内存分配是代表运行在内核空间的进程执行的,并且在空闲内存较少是把当前进程转入休眠以等待一个页面。如果在中断上下文中调用,则必须用GFP_ATOMIC标志,不会休眠。
linux内核把内存分为三个区段:可用于DMA的内存、常规内存和高端内存。x86平台上,DMA区段是RAM的前16MB。注意:kmalloc不能分配高端内存。
物理内存只能按页面进行分配,Linux处理内存分配的方法是:创建一系列的内存对象池,每个池中的内存块大小是固定一致的,处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给访问者。

如果模块需要大块的内存,使用面向页的分配技术会更好些:
get_zeroed_page(unsigned int flags);? 返回指向新页面指针并清零
__get_free_page(unsigned int flags);? 不清零
__get_gree_pages(unsigned int flags,unsigned int order);
分配物理连续的页面,返回指向第一个字节的指针,不清零页面
order是要申请的页面数的以2为底的对数,最大允许为10或11,不过以10分配成功的几率很小,/proc/buddyinfo可显示每个区段上每个阶数下可获得的数据块数目,例如我机器上:
Node 0, zone??? DMA?? 12? 27? 0? 1? 0? 1? 1? 1? 1? 0? 0
Node 0, zone Normal? 524?? 9? 5? 0? 1? 1? 0? 0? 1? 1? 0
使用函数int get_order(unsigned long size);获得分配阶数。
释放函数:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);

后备高速缓存(lookaside cache)
创建高速缓存对象:
kmem_cache_t *kmem_cache_create(const char *name, size_t size,
?????????????????????? size_t offset,
?????????????????????? unsigned long flags,
?????????????????????? void (*constructor)(void *, kmem_cache_t *,
?????????????????????????????????????????? unsigned long flags),
?????????????????????? void (*destructor)(void *, kmem_cache_t *,
????????????????????????????????????????? unsigned long flags));
可以容纳任意数目的内存区域,它们大小相同,由size指定。当高速缓存对象被创建后,就可以调用kmem_cache_alloc从中分配内存对象:
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
内核通过/proc/slabinfo来统计高速缓存的使用情况。下面是ldd3中的例子(scullc):
声明:
kmem_cache_t *scullc_cache;
创建:
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
???????? 0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* no ctor/dtor */
?if (!scullc_cache) {
???? scullc_cleanup(? ); //kmem_cache_destroy();
???? return -ENOMEM;
}
分配:
if (!dptr->data[s_pos]) {
???? dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
???? if (!dptr->data[s_pos])
???????? goto nomem;
???? memset(dptr->data[s_pos], 0, scullc_quantum);
}
释放:
for (i = 0; i < qset; i++) if (dptr->data[i])???????? kmem_cache_free(scullc_cache, dptr->data[i]);
销毁:(模块卸载时返回给系统)
if (scullc_cache)
???? kmem_cache_destroy(scullc_cache);

alloc_pages接口
主要用来分配高端内存,Linux页分配器的核心代码是:
struct page *alloc_pages_node(int nid, unsigned int flags,
??????????????????????????????? unsigned int order);
有两个宏用来方便调用:
struct page *alloc_pages(unsigned int flags, unsigned int order); struct page *alloc_page(unsigned int flags);
nid 是NUMA节点的ID号,NUMA计算机是多处理器系统,其中的内存对特定处理器来说是本地的,访问本地内存要比访问非本地内存快。在这类系统中,在正确节点上的内存分配非常重要。alloc_pages通过在当前NUMA节点上分配内存而简化了alloc_pages_node函数,它将 numa_node_id的返回值作为nid参数而调用了alloc_pages_node函数。释放页面的函数如下:
void _ _free_page(struct page *page); void _ _free_pages(struct page *page, unsigned int order); void free_hot_page(struct page *page); //页面在CPU Cache中 void free_cold_page(struct page *page); //页面不在CPU Cache中

vmalloc函数
kmalloc 和__get_free_pages返回的内存地址是虚拟地址,其实际值要由MMU处理才能转化为物理地址。它们使用的地址范围于物理内存是一一对应的,可能会有基于常量PAGE_OFFSET的一个偏移;但是vmalloc和ioremap使用的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立(虚拟)内存区域。vmalloc可以获得的地址在VMALLOC_START到VMALLOC_END的范围中。当驱动程序需要真正的物理地址时是不能使用vmalloc的,使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。注意vmalloc 的开销要比__get_free_pages大,因为它不但获取内存,还要建立页表。因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。
使用vmalloc函数的一个例子是create_module系统调用,它利用vmalloc函数来获取装载模块所需的内存空间。在调用insmod来重定位模块代码后,接着会调用copy_from_user函数把模块代码和数据复制到分配而得的空间内。这样,模块看来是在连续的内存空间内。但通过检查 /proc/ksyms文件就能发现模块到处的内核符号和内核本身导出的符号分布在不同的内存范围上。
vmalloc分配得到的内存空间要用vfree函数来释放。

引导时分配专用缓冲区
这是获得大量连续内存页面的唯一方法,粗暴不灵活但是基本不会失败。
#include <linux/bootmem.h> void *alloc_bootmem(unsigned long size);
void *alloc_bootmem_low(unsigned long size);
void *alloc_bootmem_pages(unsigned long size);
void *alloc_bootmem_low_pages(unsigned long size);
使用引导时分配时,应该将驱动程序直接链接到内核,具体细节参考Documentation/kbuild目录下的文件。


ldd3-内存映射(2)
首先需要清楚以下概念:
用户虚拟地址,物理地址,总线地址,内核逻辑地址,内核虚拟地址,高端与低端内存,page结构,虚拟内存区VMA。
kmalloc返回的就是内核逻辑地址,内核虚拟地址与物理地址不必是一一对应的,高端内存需要使用alloc_pages分配,得到的页指针可以通过kmap得到映射的虚拟地址。
在x86 结构中,内核将4GB的虚拟地址空间分割为用户空间和内核空间,内核无法直接操作没有映射到内核地址空间的内存。只有内存的低端部分有逻辑地址,高端部分是没有的,在访问特定的高端内存页前,内核必须建立明确的虚拟映射(如kmap),是该页可以在内核空间中被访问。有两个宏完成逻辑地址与物理地址之间的转换__pa()和__va()。
page结构体中包含的几个成员比较重要:
atomic_t count;
访问计数,为0时返回给空闲页链表
void *virtual;
如果页面被映射,则指向页的内核虚拟地址;否则为NULL
unsigned long flags;
PG_locked表示页已经被锁住,PG_reserved表示禁止内存管理系统访问该页
如下宏用来进行page于虚拟地址之间的转换:
struct page *virt_to_page(void *kaddr)
kaddr必须是逻辑地址,不能操作vmalloc生成的地址以及高端内存
struct page *pfn_to_page(int pfn);
根据指定的页帧号,返回page结构指针
#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);
kmap为系统中的页返回内核虚拟地址。对于低端内存页来说,它只返回页的逻辑地址;对于高端内存,kmap在专用的内核地址空间创建特殊的映射。可能会休眠。
虚拟内存区VMA用于管理进程地址空间中不同区域的内核数据结构。一个VMA表示在进程的虚拟内存中的一个同类区域:拥有同样权限标志位和被同样对象备份的一个连续的虚拟内存地址范围。进程的内存映射至少包含.text,.data,.bss这三个区域。

设备映射
当用户空间进程调用mmap,将设备内存映射到它的地址空间时,系统创建一个新的VMA,然后驱动程序中的相应方法必须完成对这个VMA的初始化操作。VMA由vm_area_struct结构表示,主要成员如下:
unsigned long vm_start;
unsigned long vm_end;
VMA所覆盖的虚拟地址范围
struct file *vm_file;
指向与该区域相关联的file结构指针
unsigned long vm_pgoff;
一般用来保存虚拟内存要被映射到的物理内存的页帧号
unsigned long vm_flags;
驱动程序只关心VM_IO和VM_RESERVED
struct vm_operations_struct *vm_ops;
open:当对VMA产生一个新的引用时(如fork),则调用open。(mmap第一次创建是除外,调用驱动的mmap方法)
close:销毁一个VMA
nopage:访问的页不在内存中时调用
内存映射可以提供给用户程序直接访问设备内存的能力。但是要除了串口设备和面向流的设备。mmap还必须以PAGE_SIZE的大小进行映射。
系统调用如下:
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
驱动中的方法如下:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
vma 包含了用于访问设备的虚拟地址的信息。因此大量的工作由内核完成;为了执行mmap,驱动程序之需要为该地址范围建立合适的页表,并将vma-> ops替换为一系列的新操作就可以了。所以驱动程序的方法中需要调用remap_pfn_range(),ldd3中给出了一个简单的mmap方法:
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma) {
???? if (remap_pfn_range(vma, vma->vm_start vm->vm_pgoff,
???????????????? vma->vm_end - vma->vm_start,
???????????????? vma->vm_page_prot))
???????? return -EAGAIN;
???? vma->vm_ops = &simple_remap_vm_ops;
???? simple_vma_open(vma);
???? return 0;
}
void simple_vma_open(struct vm_area_struct *vma)? {
???? printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx/n",
???????????? vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
?void simple_vma_close(struct vm_area_struct *vma) {
???? printk(KERN_NOTICE "Simple VMA close./n");
}
static struct vm_operations_struct simple_remap_vm_ops = {
???? .open =? simple_vma_open,
???? .close = simple_vma_close,
?? ? .nopage = simple_vma_nopage,
};
当应用程序调用mremap重新一个已经被绑定的地址时,使用nopage方法。当VMA的大小变化是shrink时没有必要通知驱动程序,如果VMA的大小expand时,就调用nopage为新页进行设置,驱动程序会发现这个情况。
struct page *(*nopage)(struct vm_area_struct *vma,
???????????????????????? unsigned long address, int *type);
address包含了引起错误的虚拟地址,已经与页对齐。nopage返回用户所需要的page结构指针,并调用get_page宏增加页的使用计数:
get_page(struct page *pageptr);
需要的下一个页帧号可以这样计算得到:
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physaddr = address - vma->vm_start + offset;
unsigned long pageframe = physaddr >> PAGE_SHIFT;
?需要保证帧页号的合法:
if (!pfn_valid(pageframe))
???? return NOPAGE_SIGBUS;
然后根据帧页号得到指向page结构的指针并增加它的计数:
struct page *pageptr = pfn_to_page(pageframe);
get_page(pageptr);
?
重映射特定的I/O区域
下面的代码表示驱动程序对起始于物理地址simple_region_start,大小为simple_region_size字节的区域进行映射的工作过程(注意,这里ldd3中出现了严重错误,请以以下代码为准):
unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
unsigned long pfn = page_to_pfn(simple_region_start + off);
unsigned long vsize = vma->vm_end - vma->vm_start;
unsigned long psize = simple_region_size - off;
if (vsize > psize)
???? return -EINVAL; /*? spans too high */
remap_pfn_range(vma, vma_>vm_start, pfn, vsize, vma->vm_page_prot);


http://www.linuxforum.net/forum/printthread.php?Board=linuxK&main=259069

http://www.linuxforum.net/forum/showthreaded.php?Cat=&Board=linuxK&Number=295201&page=0&view=collapsed&sb=5&o=7
原创粉丝点击