分配内存(Linux设备驱动程序)

分配内存
介绍设备驱动程序中使用内存的方法;
如何最好地利用系统内存资源。


kmalloc函数
kmalloc内存分配引擎是一个功能强大的工具。
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
参数分配表示flags能够以多种方式控制kmalloc的行为。
标志GFP_KERNEL表示内存分配是代表运行在内核空间的进程执行的,这意味着调用它的函数正代表


某个进程执行系统调用。
使用GFP_KERNEL允许kmalloc在空闲内存较少时把当前进程转入休眠以等待一个页面。
使用GFP_KERNEL分配内存的函数必须是可重入的。(可被休眠)
在进程休眠时,内核会把缓冲区的内容刷写到硬盘上,或者是从一个用户进程换出内存,以获取一


个内存页面。
在中断处理例程、tasklet以及内核定时器中调用时,进程不应该休眠,驱动程序则应该用


GFP_ATOMIC标志。
内核通常会为原子性的分配预留一些空闲页面。
使用GFP_ATOMIC标志时,kmalloc甚至可以用掉最后一个空闲页面。


所有的标志都定义在<linux/gfp.h>中,有个别的标志使用两个下划线作为前缀,比如__GFP_DMA。
GFP_USER 用于为用户空间页分配内存,可能会休眠
GFP_HIGHUSER 如果有高端内存就从哪里分配
GFP_NOIO
GFP_NOFS
类似于GFP_KERNEL,
具有GFP_NOFS标志的分配不允许执行任何文件系统调用;
GFP_NOIO禁止任何I/O的初始化。
这两个标志主要在文件系统和虚拟内存代码中使用。


上面的分配标志可以和下面的标志“或”起来使用。
__GFP_DMA
__GFP_HIGHMEM
__GFP_COLD
__GFP_NOWARN
__GFP_HIGH
__GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY


Linux内核把内存分为三个区段:
可用于DMA的内存、常规内存、高端内存。


通常的内存分配都发生在常规内存区。
每种计算平台都必须知道如何把自己特定的内存范围归类到这三个区段中。


可用于DAM的内存指存在于特别地址范围内的内存,外设可以用这些内存执行DMA访问。
在大多数健全的系统上,所有内存都位于这一区段。
在x86平台上,DMA区段是RAM的前16MB,老式的ISA设备可在该区段上执行DMA,PCI设备无此限制。


高端内存是32位平台为了访问(相对)大量的内存而存在的一种机制。
如果不首先完成一些特殊的映射,就无法从内核中直接访问这些内存。
如果驱动程序要使用大量的内存,那么在能够使用高端内存的大系统上可以工作得更好。


当一个新页面为满足kmalloc的要求被分配时,内核会创一个内存区段的列表一共搜索。
如果指定了__GFP_DMA标志,则只有DMA区段会被搜索;
如果如果没有指定特定的标志,则常规区段和DMA区段都会被搜索;
如果设置了__GFP_HIGHMEM标志,则所有三个区段都会被搜索以获取一个空闲页。
(但kmalloc不能分配高端内存)


内核负责管理系统物理内存,物理内存只能按页面进行分配。
kmalloc和典型的用户空间的malloc在实现上有很大的差别。
(简单的基于堆的内存分配技术)


内核使用特殊的基于页的分配技术,以最佳地利用系统RAM。


Linux处理内存分配的方法是,创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处


理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。


内核只能分配一些预定义的、固定大小的字节数组。


设备驱动程序常常会反复地分配很多同一大小的内存块。
内核实现了后备高速缓冲(lookaside cache).


设备驱动程序通常不会涉及这种使用后背高速缓存的内存行为,但也有例外,Linux 2.6中的USB和


SCSI驱动程序就使用了这种高速缓存。


Linux内核的高速缓存管理有时称为“slab分配器”。
相关函数和类型在<linxu/slab.h>中声明。
slab分配器实现的高速缓存具有kmem_cache_t类型,通过调用kmem_cache_create创建:
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));
参数flags控制如何完成分配,是一个位掩码。
SLAB_NO_REAP SLAB_HWCACHE_ALIGN SLAB_CACHE_DMA


高速缓存被创建后,可以调用kmem_cache_alloc从中分配内存对象:
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);


释放一个内存对象时使用kmem_cache_free:
void kmem_cache_free(kmem_cache_t *cache, const void *obj);


释放高速缓冲:
int kmem_cache_destroy(kmem_cache_t *cache);




内存池
内核中有些地方的内存分配是不允许失败的。
内核建立了一种称为内存池的抽象,其实就是某种形式的后备高速缓存,它始终保存空闲的内存,


以便在紧急状态下使用。


内存池对象的类型为mempool_t(在<linux/mempool.h>中定义),可使用mempool_create来建立内


存池对象。
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t 


*free_fn, void *pool_data);
对象的实际分配和释放由alloc_fn和free_fn函数处理:
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
mempool_create的最后一个参数,即pool_data,被传入alloc_fn和free_fn。


我们可以为mempool编写特定用途的函数来处理内存分配。
通常让内核的slab分配器为我们处理这个任务。
内核中有两个函数(mempool_alloc_slab和mempool_free_slab),可以利用kmem_cache_alloc和


kmem_cache_free处理内存分配和释放。


建立内存池之后,分配和释放对象:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);


调整mempool的大小:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);


如果不需要内存池,将其返回给系统:
void mempool_destroy(mempool_t *pool);
在销毁mempool之前,必须将所有已分配的对象返回到内存池中,否则会导致内核oops。


mempool会分配一些内存块,空闲且不会真正得到使用。使用mempool很容易浪费大量内存。


get_free_page和相关函数
分配页面可使用下面的函数:
get_zeroed_page(unsigned int flags);
__get_free_page(unsigned int flags);
__get_free_pages(unsigned int flags, unsigned int order);
参数flags的作用和kmalloc中的一样;
参数order是要申请或释放的页面数以2为底的对数。


get_order函数可根据宿主平台上的大小返回order值。


void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);


只要符合和kmalloc同样的规则,get_free_pages和其他函数可以在任何时间调用。
在使用了GFP_ATOMIC时,某些情况下函数分配内存时会失败。


基于页的分配策略的优点在于更有效地使用了内存,按页分配不会浪费内存空间。
kmalloc函数则会因分配粒度的原因而浪费一定数量的内存。


使用__get_free_page函数的最大优点是这些分配的页面完全属于我们自己,而且理论上可以通过适


当地调整页表将它们合并成一个线性区域。
例如,可以允许用户进程对这些单一但互不相关的页面分配得到的内存区域进行mmap。


alloc_pages接口
struct page是内核用来描述单个内存页的数据结构。
内核中很多地方需要使用page结构,尤其在需要使用高端内存的地方。


Linux页分配器的核心代码是称为alloc_pages_node的函数:
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号,表示要在其中分配内存。


void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);


vmalloc及其辅助函数
vmalloc分配虚拟地址空间的连续区域。
这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数alloc_page),


内核却认为它们在地址上是连续的。
vmalloc在发生错误时返回0(NULL地址),成功时返回一个指针,该指针指向一个线性的、大小最


少为size的线性内存区域。


vmalloc是Linux内存分配机制的基础。
(大多数情况下不鼓励使用vmalloc)


#include <linxu/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void *addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);


由kmalloc和__get_free_pages返回的内存地址也是虚拟地址,其实际值也要由MMU处理才能转为物


理内存地址。
kmalloc和__get_free_pages使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量


的PAGE_OFFSET的一个偏移。不需要为该地址段修改页表。


vmalloc和ioremap使用的地址范围完全是虚拟的,每次分配都要通过对页表的适当设置来建立(虚


拟)内存区域。
vmalloc可以获得的地址在VMALLOC_START到VMALLOC_END的范围中,这两个符号都在


<asm/pgtable.h>中定义。


用vmalloc分配得到的地址是不能在微处理器之外使用的,它们只在处理器的内存管理单元上才有意


义。


使用vmalloc函数的一个例子函数是create_module系统调用,它利用vmalloc函数来获取装载模块所


需的内存空间。
在调用insmod来重定位模块代码后,接着会调用copy_from_user函数把模块代码和数据复制到分配


而得的空间内。


用vmalloc分配得到的内存空间要用vfree函数来释放。




ioremap也建立新的页表,但不实际分配内存。
ioremap的返回值是一个特殊的虚拟地址,可以用来访问指定的物理内存区域,这个虚拟地址最后要


调用iounmap来释放掉。


ioremap更多用于映射(物理的)PCI缓冲区地址到(虚拟的)内核空间。
例如,可以用来访问PCI视频设备的帧缓冲区,该缓冲区被映射到高物理地址,超出了系统初始化时


建立的页表地址范围。


ioremap和vmalloc函数都是面向页的(它们都会修改页表),因此重新定位或分配的内存空间实际


上都会上调到最近的一个页边界。
ioremap通过把重新映射的地址向下下调到页边界,并返回在第一个重新映射页面中的偏移量的方法


模拟了不对齐的映射。


vmalloc函数不能在原子上下文中使用,因为它的内部实现调用了kmalloc(GFP_KERNEL)来获取页表


的存储空间,因而可能休眠。




per-CPU变量




获取大的缓冲区
大的、连续内存缓冲区的分配易于流于失败。
系统内存会随着时间的流逝而碎片化,这导致无法获得真正的大内存区域。


执行大的I/O操作的最好方式是通过离散/聚集操作。


如果需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。
在引导时就进行分配是获得大量连续内存页的唯一方法。
在引导时分配缓冲区有点“脏”,因为它通过保留私有内存池而跳过了内核的内存管理策略。


模块不能在引导时分配内存,只有直接链接到内核的设备驱动程序才能在引导时分配内存。
这种机制只对链接到内核映像中的代码可用。


内核被引导时,它可以访问系统所有的物理内存,然后调用各个子系统的初始化函数进行初始化,


它允许初始化代码分配私有的缓冲区,同时减少了留给常规系统操作的RAM数量。


通过调用下列函数之一可完成引导时的内存分配:
#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);
这些函数要么分配整个页(若以_pages结尾),要么分配不在页面边界上对齐的内存区。
除非使用具有_low后缀的版本,否则分配的内存可能会是高端内存。
如果希望将其用于DMA操作,而高端内存并不总是支持DMA操作,需要使用一个_low变种。


内核提供了一种释放这种内存的接口:
void frdd_bootmem(unsigned long addr, unsigned long size);





你可能感兴趣的:(分配内存(Linux设备驱动程序))