linux设备驱动八(分配内存)

kmalloc函数内幕

特定:运行很快,但是可能被阻塞,不对分配的内存空间清零,分配区域在物理上是连续的

flags参数

GFP前缀的含义,最终会调用get_free_pages来实现
GFP_KERNEL,调用它的函数正代表某个进程进行系统调用,GFP_KERNEL允许休眠来等待一个页面,所有调用的函数必须可重入的
GFP_ATOMIC,调用函数是原子的,例如中断处理,使用内核预留的一些页面,可能失败,不会休眠
GFP_USER,用于用户空间页面分配,可能会休眠
GFP_HIGHUSER,类似GFP_USER,如果有高端内存,则从那里分配
GFP_NOIO
GFP_NOFS ,这两个类似GFP_KERNEL,但是NOFS不会允许执行文件系统调用,而NOIO禁止任何IO初始化,主要在文件系统和虚拟内存中使用,避免发生递归的文件系统调用

如何分配标志

__GFP_DMA,分配发生在可进行DMA的内存区段
__GFP_HIGHMEM,分配可位于高端内存
__GFP_GOLD,通常分配器试图返回“cache warm”页面,即可在处理器缓存中找到的页面,而这些页面对DMA没有多大帮助,该标志请iufanhui未使用的冷页面
__GFP_NOWARM,可以避免内核在无法满足分配请求时产生警告
__GFP_HIGH,高优先级请求,允许紧急情况下使用保留的一些页面
__GFP_REPEAT
__GFP_NOFAIL
__GFP_NORETRY,这三个标志,是在分配失败时采取的行为。不鼓励使用NOFAIL

内存分段

__GFP_DMA和GFP_HIGHMEM和常规内存区,通常的分配发生在常规内存去。
DMA区,外设可利用该区内存执行DMA访问。大多数健全系统,所有内存都位于这一区段。在X86平台上,DMA区段是RAM前16M,老式ISA设备可在该区执行DMA,PCI设备无此限制。
高端内存是32位平台为了访问大量内存而存在的一种限制。(64位平台不存在,我的理解,32位系统只有1G内核地址空间,而其中800多M是固定的映射,加上初始启动时系统占有的空间,使用完了前1G物理内存,剩下100多M虚拟地址空间需要映射其他物理地址才能访问)
指定了__GFP_DMA则只会在DMA段搜索,不指定则会在常规段和DMA段艘多,如果指定了__GFP_HIGHMEM则三个段都会艘多,但是不能在kmalloc中指定__GFP_HIGHMEM,因为分配的高端内存可能还没有映射到地址空间
NUMA,nonuniform memory access不均匀内存访问,通常会试图在执行该分配的处理器的本地内存区中定位内存,也可以有一些方式来改变

size参数

malloc基于堆的分配,内核基于页面分配,kmalloc实际使用的slab分配器,实际分配可能比请求要多一些,最多可能是请求的两倍
kmalloc能处理的最小内存块是32或者64
kmalloc存在一个最大的上限,具体值和体系结构有关,希望代码具有可移植性,则不应该分配大于128K的内存

后备高速缓存

为反复使用的相同大小的内存块而设置的内存池,通常称为后备高速缓存(looaside 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));

name参数,是保留指针,而没有复制,所以应该使用静态字符串,或者常量字符串。名称中不能包含空白
offset参数,页面中第一个对象的偏移,用来确保对已分配的对象进行某种特殊的对其
flags参数,控制如何分配
SLAB_NO_REAP,保护高速缓存不会被减小
SLAB_HWCACHE_ALIGN,所有数据对象跟高速缓存航对其(cache line);依赖主机平台的硬件高速缓存布局。如果在SMP上,有频繁访问数据的话,设置该项是好的选择,但是会浪费内存用来填白。
SLAB_CACHE_DMA,要求每个数据对象从可用于DMA的内存区段中分配。
SLAB_CTOR_ATOMIC,标志constructor和destructor不可以休眠

constructor和destructor是可选参数,但是必须同时设置,或者不设置,前者用于初始化,后者在释放给系统之前进行清除
constructor和destructor可以使用同一个函数,在调用的是constructor的时候,会传递SLAB_CTOR_CONSTRUCTOR标志
constructor主要是在分配一组对象时使用,每个对象调用一次,destructor不会在还给缓存后立即调用。

void *kmem_cache_alloc(kmem_cache_t *cache, int flags); flags和传递给kmalloc的相同,当缓存扩展时也使用这个参数。

void kmem_cache_free(kmem_cache_t *cache, const void *obj);

int kmem_cache_destroy(kmem_cache_t *cache); 只有在把从缓存中分配的所有对象都归还之后才能成功。可以用来检查内存泄露。

内存池

内存池是某种形式的后备高速缓存,试图时钟保存空闲内存,以便在紧急状态下使用。

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t free_fn, void pool_data);

min_nr,表示内存池时钟保持的已分配的最少数目,对象的实际分配和释放由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中分配和释放内存,而是指从其他地方分配内存放入mempool中。

pool = mempool_create(MY_POOL_MINIMUM, mempool_alloc_slab, mempool_free_slab, cache);

void *mempool_alloc(mempool_t *pool, int gfp_mask);调用该函数时,先使用分配函数分配对象,如果失败了,再使用mempool中预先分配的对象
void mempool_free(void *element, mempool_t *pool);调用该函数时,如果mempool中的对象数目小于最低要求,则将对象保留在mempool中,否则返回给系统

int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);调整mempool大小
void mempool_destroy(mempool_t *pool);调用该函数之前必须把已分配的对象返回给mempool,否则会导致oops。
mempool会浪费大量内存,应该尽量避免使用mempool,(使用mempool的场景是什么?)

get_free_page和相关函数

分配大块内存时,应该使用面向页的分配函数

get_zeroed_page(unsigned int flags); 返回指向新页面的指针,并将页面清零。
__get_free_page(unsigned int fags);类似get_zeroed_page,但不清零页面。
__get_free_pages(unsigned int flags, unsigned int order);分配若干(物理连续)页面,并返回指向该内存区第一个字节的指针,不清零页面。可允许的最大order是10或11

/proc/buddyinfo可获取系统中每个内存区段上每个阶数下可获得的数据块数目

void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);试图释放和先前分配数目不等的页面会导致映射关系被坡缓,系统出错。

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

可以使用mmap把不相关的页面映射到一个线性区域。

alloc_pages接口

使用高端内存的地方必须使用struct page,因为没有固定的虚拟地址映射。

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order); nid参数,NUMA节点
通常使用封装了该函数的宏:
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags); 当前NUMA节点分配
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,效率不高,而且某些体系结构上,vmalloc地址区域较小。
如果可能,应该直接操作页,而不是使用vmalloc。
vmalloc可以获得地地址在VMALLOC_START到VMALLOC_END的范围中。
kmalloc和__get_free_pages使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量PAGE_OFFSET一个偏移(x86上为3G,虚拟地址固定映射区,896M之前),这两个函数不需要修改页表。另外一方面vmalloc和ioremap使用的地址范围是虚拟的,每次分配都要通过对页表的适当设置来建立内存区域
vmalloc分配得到的地址不能在微处理器之外使用,它们只有在处理器的内存管理单元才有意义
当驱动程序需要真正的物理地址时,比如DMA,就不能用vmalloc
vmalloc正确场合时分配一大块连续的,只在软件中存在的,用于缓冲的内存区域的时候。
vmalloc开销比__get_free_pages大,因为除了获取内存,还要建立页表。
ioremap和vmalloc都会建立新的页表,但是ioremap并不世界分配内存。ioremap返回一个特殊的虚拟地址,用来访问指定的物理内存,通过iounmap来释放。
ioremap更多用于映射(物理的)PCI缓冲区地址到(虚拟的)内核空间
为了保持可移植性,不应该把ioremap返回的地址作为内存指针直接访问,应该使用readb或其他io函数。pci规范和alpha处理器在数据传输上有差异,不能直接把PCI内存区映射到处理器的地址空间。
ioremap和vmaloc函数都是面向页的,会调整到最近一个页边界。
vmalloc不能在原子上下文中使用,因为内部调用了kmalloc(GFP_KERNEL),可能休眠

在x86_64平台上,物理地址和虚拟地址被映射到完全不同的地址范围(0x100和0xffffff00),在x86平台上,vmalloc返回的虚拟地址就在用于映射物理内存的地址之上(这句话的意思?需要查下英文原版)

void *vmalloc(unsigned long size);
void vfree(void *addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void *addr);

per-CPU变量

当建立一个per-CPu变量时,每个处理器都会拥有该变量的特有副本。
对per-cpu变量访问不需要加锁,每个cpu在自己副本上工作。
per-cpu变量还可以保存在对应处理器的高速缓存中。
使用per-cpu的例子可见于网络子系统中

DEFINE_PER_CPU(type, name);例如:DEFINE_PER_CPU(int[3], my_percpu_array);
当处理器修改某个per-cpu变量的临界区中间要避免抢占,以及被切换到另外一个处理器上运行。应该显式调用get_cpu_var
get_cpu_var的作用返回当前处理器变量版本,并禁止抢占。完成修改后记的调用put_cpu_var
可以调用per_cpu(variable,int cpu_id);访问其他cpu变量,但此时需要采用某种锁定机制来确保访问安全。
动态分配per-cpu:
void *alloc_percpu(type);
void __alloc_percpu(size_tsize, size_t align); 大部分情况下使用第一个,需要对齐时使用第二个
free_percpu释放
动态分配的per-cpu变量通过函数per_cpu_ptr(void per_cpu_var, int cpu_id);来访问
为了避免在访问时被切换到其他处理器,要使用get_cpu来阻止抢占
使用样例:
int cpu;
cpu=get_cpu();
ptr = per_cpu_ptr(per_cpu_var, cpu);
/
使用ptr
/
put_cpu();
编译期间的per-cpu变量,则get_cpu_var和put_cpu_var起到相同作用
per-cpu可以导出给模块
EXPORT_PER_CPU_SYMBOL(per_cpu_var);
EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);
在模块中使用时:
DECLARE_PER_CPU(type, name);

参考<linux/percpu_counter.h>的实现
某些体系架构,per-cpu可使用地址空间是受限制的。要确保变量较小。

获取大缓冲区
在获取大内存缓冲区之前,考虑其他实现途径
最好打的IO操作方式是离散/聚集操作。

引导时获取专用缓冲区
这是获取大量连续内存页面的唯一方法,它绕过了__get_free_pages函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。
模块不能引导时分配,只有编译时直接连接到内核中才可以。具体方法参看Documentation/kbuild

#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);
不适用__low版本可能分配到高端内存,高端内存并不总支持DMA,以__pages结尾,分配的是边界对齐的内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值