linux 虚拟内存使用
目录
remap_pfn_range().......................................................................................................................................... 2
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)........................... 2
alloc_pages(mask, order)............................................................................................................................. 2
Linux kernel内存分配....................................................................................................................................... 2
【Linux驱动编程】mmap方法............................................................................................................................ 3
1. 驱动mmap方法.................................................................................................................................................. 3
查看某个进程的虚拟地址情况:......................................................................................................................... 8
linux内存分配方法总结...................................................................................................................................... 8
a.伙伴算法(最原始的面向页的分配方式)....................................................................................................... 9
b.slab高速缓存(反复分配很多同一大小内存) 注:使用较少.................................................... 9
c.kmalloc(最常用的分配接口) 注:必须小于128KB.......................................... 9
d.vmalloc/vfree.................................................................................................................................................. 9
e.ioremap()/iounmap().................................................................................................................................... 9
remap_pfn_range()
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
该函数的功能是创建页表。也就是将pfn这个页物理地址的开始位置映射成虚拟地址virt_addr。所以在kernel分配的buffer应该是pfn的开始地址(char buffer = (char *)__get_free_pages(GFP_KERNEL, 0);),这样才能跟app获取到的虚拟地址virt_addr对应起来,从而实现kernel和app访问同一段buffer的功能。
参数vma是内核根据用户的请求自己填写的(上层app传入)
参数addr表示内存映射开始处的虚拟地址(上层app传入),因此,该函数为addr~addr+size之间的虚拟地址构造页表。
pfn(Page Fram Number)是虚拟地址应该映射到的物理地址的页面号,实际上就是物理地址右移PAGE_SHIFT位。如果PAGE_SHIFT为4kb,则PAGE_SHIFT为12,因为PAGE_SHIFT等于1<<PAGE_SHIFT。kernel提供
参数prot是新页所要求的保护属性,MAP_SHARED。
在驱动程序中,一般能使用remap_pfn_range()映射内存中的保留页(如X86系统中的640KB~1MB区域)和设备I/O内存。因此,如果想把kmalloc()申请的内存映射到用户空间,则可以通过mem_map_reserve()把相应的内存设置为保留后就可以(linux 2.6用SetPageReserved)。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
功能:以gfp_mask的方式分配2^order个物理页面
gfP_mask:分配的方式,指出如何分配在哪分配如GFP_KERNEL
order:分配2^order个页面
返回值:返回分配的第一个页的逻辑地址
alloc_pages(mask, order)
分配2order2order页并返回一个struct page的实例,表示分配的内存块的起始页
Linux kernel内存分配
kmalloc()
用于申请较小的、连续的物理内存
1. 以字节为单位进行分配,在<linux/slab.h>中
2. void *kmalloc(size_t size, int flags) 分配的内存物理地址上连续,虚拟地址上自然连续
3. gfp_mask标志:什么时候使用哪种标志?如下:
进程上下文,可以睡眠GFP_KERNEL
进程上下文,不可以睡眠GFP_ATOMIC
中断处理程序GFP_ATOMIC
软中断 GFP_ATOMIC
Tasklet GFP_ATOMIC
用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL
用于DMA的内存,不可以睡眠 GFP_DMA | GFP_ATOMIC
void kfree(const void *ptr)
释放由kmalloc()分配出来的内存块
vmalloc()
用于申请较大的内存空间,虚拟内存是连续的
1. 以字节为单位进行分配,在<linux/vmalloc.h>中
2. void *vmalloc(unsigned long size) 分配的内存虚拟地址上连续,物理地址不连续
3. 一般情况下,只有硬件设备才需要物理地址连续的内存,因为硬件设备往往存在于MMU之外,根本不了解虚拟地址;但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用vmalloc(),例如当模块被动态加载到内核当中时,就把模块装载到由vmalloc()分配 的内存上。
4.void vfree(void *addr),这个函数可以睡眠,因此不能从中断上下文调用。
malloc(), vmalloc()和kmalloc()区别
[*]kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
[*]kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(这点是自己猜测的,不一定正确)
[*]kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
[*]内存只有在要被DMA访问的时候才需要物理上连续
[*]vmalloc比kmalloc要慢
【Linux驱动编程】mmap方法
应用与驱动3种方式
1通过系统调用open(), read(), write()等方式实现对驱动层的交互,获取驱动层数据或调用方法操作硬件。
2通过IOCTL CMD命令方式与内核驱动层交互,从而获取驱动层数据或调用驱动层方法来操作硬件。
3通过mmap方式来实现应用层与内核共享内存来传递数据。这种方式优点是速度快,且可以数据大块的数据量。
mmap常规的应用是将普通文件映射到用户进程空间,提高文件读写效率。linux的思想是“一切皆文件”,对于设备文件而言,同样可以实现mmap映射。一个设备,一般涉及到帧缓存会考虑实现映射接口,常见的LCD framebuffer设备的显存空间,经过映射后,用户进程可以直接操作进程内存空间将LCD显示数据写入,提高刷新效率,节省CPU拷贝内存开销。
1. 驱动mmap方法
进程调用mmap()函数建立mmap映射,而mmap()是在内核空间(驱动)实现的,如果驱动层未实现该函数,进程调用时,会返回-ENODEV错误。mmap是标准虚拟文件系统(VFS)struct file_operations提供的接口之一,驱动实现时只需实现该函数指针实体,然后注册到驱动fops中。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
int (*mmap) (struct file *, struct vm_area_struct *);
.......
}
进程mmap到驱动mamp大体过程
1.1 mmap描述
linux内核采用struct vm_area_struct数据结构描述内核mmap,原型位于linux/mm_types.h中。
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*
* For private anonymous mappings, a pointer to a null terminated string
* in the user process containing the name given to the vma, or NULL
* if unnamed.
*/
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
const char __user *anon_name;
};
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};
mmap描述参数比较多,驱动编程下,我们关注几个参数
vm_start, 映射进程空间起始地址
vm_end,映射进程空间结束地址
vm_page_prot,映射保护属性
vm_flags,映射访问标识,常用标识如下
VM_IO,与IO相关映射或者类似的映射
VM_LOCKED,锁定物理空间,进制swap
VM_DONTEXPAND,不可通过mremap函数扩展
VM_DONTDUMP,不包括核心转存储空间
其他访问标识,查看"include/linux/mm.h"中定义。
1.2 构建页表函数
用户进程调用mmap()函数建立映射后,此时进程空间是没有实际内容的,只有触发缺页中断,最终是在驱动程序中建立页表,通过MMU映射到实际物理内存中才完成整个映射过程。
内存的最小颗粒度是页(page),一页大小一般为4K字节,每一页的编号称为页帧号(page frame number),简称pfn。
建立页表一般有两种构建方法。
【1】使用remap_pfn_range函数一次建立所有页表
remap_pfn_range原型位于linux/mm.h中。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
1
2
vma,映射进程空间描述符
addr,映射进程空间起始地址
pfn,映射内核物理地址
size,映射进程空间大小
prot,映射保护属性
返回,成功返回0
除了内核映射物理空间在驱动层外,其他参数是根据用户调用mmap()函数传入。
【2】使用nopage VMA方法每次建立一个页表
实现nopage函数实体,进程触发内核缺页中断时,由内核申请内存中的物理页,由driver在nopage函数中将page与vma挂钩。
nopage函数首先计算缺页虚拟内存地址的实际物理地址与映射文件偏移量offset;检查偏移量有效性(是否超出文件大小);如有效则将该缺页地址的虚拟地址变换成页帧号并申请该页,实际文件内容映射到该内存页上。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type)
1
vma,映射进程空间描述符
address,映射进程空间起始地址
type,保存返回错误类似
返回,成功则返回一个有效映射页地址,失败返回NULL,错误类型存入type中
两者区别:
remap_pfn_range不能映射常规内存,只存取保留页和在物理内存顶之上的物理地址。因为保留页和在物理内存顶之上的物理地址内存管理系统的各个子模块管理不到。640 KB 和1MB 是保留页可能映射,设备I/O内存也是可以映射。如果想把kmalloc()申请的内存映射到用户空间,可以通过mem_map_reserve()把相应的内存设置为保留后即可。
1.3 虚拟地址与物理地址转换
在linux内核下申请的内存也就是虚拟内存,只不过是属于内核态(32位系统为3G—4G范围),因此构建页表时,需要将内核虚拟内存的转换为物理内存。
常用的内核内存申请方式有三种:
kmalloc(),申请的内存物理空间地址是连续的,虚拟空间地址也是连续的;CPU DMA映射内存要求物理空间地址要连续
__get_free_pages ,与kmalloc类似
vmalloc ,申请的内存虚拟空间地址是连续,实际物理空间地址不一定连续;kmalloc一般用来分配小于128K内存,大内存块则使用vmallco分配
对于连续的物理内存,即是通过kamlloc和__get_free_pages申请的内存,可以使用virt_to_phys()和phys_to_virt()来实现物理地址和内核虚拟地址之间的相互转换。因为物理地址是连续的,所以上述两者函数仅仅做了简单的3G(32位系统来说)的地址偏移计算;通过vmalloc分配的大内存块或者高端内存块,则不能通过该函数转换,因为由于物理地址的不连续导致涉及到分离物理页和内存跨页等相关处理,而不是简单的偏移计算。
物理地址转虚拟地址函数phys_to_virt,对于32位CPU,位于"arch/arm/include/asm/memory.h"中,对于64位CPU,位于"arch/arm64/include/asm/memory.h"中。
#define __virt_to_phys(x) ({ \
phys_addr_t __x = (phys_addr_t)(x); \
__x & BIT(VA_BITS - 1) ? (__x & ~PAGE_OFFSET) + PHYS_OFFSET : \
(__x - kimage_voffset); })
/*
* Note: Drivers should NOT use these. They are the wrong
* translation for translating DMA addresses. Use the driver
* DMA support - see dma-mapping.h.
*/
#define virt_to_phys virt_to_phys
static inline phys_addr_t virt_to_phys(const volatile void *x)
{
return __virt_to_phys((unsigned long)(x));
}
#define phys_to_virt phys_to_virt
static inline void *phys_to_virt(phys_addr_t x)
{
return (void *)(__phys_to_virt(x));
}
查看某个进程的虚拟地址情况:
Cat /proc/pidxxx/regmap
linux内存分配方法总结
内存映射结构:
1、32位地址线寻址4G的内存空间,其中0-3G为用户程序所独有,3G-4G为内核占有。
2、struct page:整个物理内存在初始化时,每个4kb页面生成一个对应的struct page结构,这个page结构就独一无二的代表这个物理内存页面,并存放在mem_map全局数组中。
3、段式映射:首先根据代码段选择子cs为索引,以GDT值为起始地址的段描述表中选择出对应的段描述符,随后根据段描述符的基址,本段长度,权限信息等进行校验,校验成功后。cs:offset中的32位偏移量直接与本段基址相累加,得出最终访问地址。
0-3G与mem_map的映射方式:
因linux中采用的段式映射为flat模式,所以从逻辑地址到线性地址没有变化。从段式出来进入页式,每个用户进程都独自拥有一个页目录表 (pdt),运行时存放于CR3。 CR3(页目录) + 前10位 => 页面表基址 + 中10位 => 页表项 + 后12位 => 物理页面地址
3G-4G与mem_map的映射方式:
分为三种类型:低端内存/普通内存/高端内存。
低端内存:3G-3G+16M用于DMA __pa线性映射
普通内存:3G+16M-3G+896M __pa线性映射 (若物理内存<896M,则分界点就在3G+实际内存)
高端内存:3G+896-4G 采用动态的分配方式
4、高端内存(假设3G+896为高端内存起址)
作用:访问到1G以外的物理内存空间。
线性地址共分为三段:vmalloc段/kmap段/kmap_atomic段(针对与不同的内存分配方式)
从内存分配函数的结构来看主要分为下面几个部分:
a.伙伴算法(最原始的面向页的分配方式)
alloc_pages 接口:
struct page * alloc_page(unsigned int gfp_mask)--分配一页物理内存并返回该页物理内存的page结构指针。
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)--分配 个连续的物理页并返回分配的第一个物理页的page结构指针。
<释放函数:__free_page(s)>
内核中定义:#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
最终都是调用__alloc_pages.
其中MAX_ORDER 11,及最大分配到到页面个数为2^10(即4M)。
分配页后还不能直接用,需要得到该页对应的虚拟地址:
void *page_address(struct page *page);
低端内存的映射方式:__va((unsigned long)(page - mem_map) << 12)
高端内存到映射方式:struct page_address_map分配一个动态结构来管理高端内存。(内核是访问不到vma的3G以下的虚拟地址的) 具体映射由kmap / kmap_atomic执行。
get_free_page接口:(alloc_pages接口两步的替代函数)
unsigned long get_free_page(unsigned int gfp_mask)
unsigned long __get_free_page(unsigned int gfp_mask)
Unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
<释放函数:free_page>
与alloc_page(s)系列最大的区别是无法申请高端内存,因为它返回到是一个线性地址,而高端内存是需要额外映射才可以访问的。
b.slab高速缓存(反复分配很多同一大小内存) 注:使用较少
kmem_cache_t* xx_cache;
创建: xx_cache = kmem_cache_create(“name”,sizeof(struct xx), SLAB_HWCACHE_ALIGN, NULL, NULL);
分配:kmem_cache_alloc(xx_cache, GFP_KERNEL);
释放:kmem_cache_free(xx_cache, addr);
内存池
mempool 不使用。
c.kmalloc(最常用的分配接口) 注:必须小于128KB
GFP_ATOMIC 不休眠,用于中断处理等情况
GFP_KERNEL 会休眠,一般状况使用此标记
GFP_USER 会休眠
__GFP_DMA 分配DMA内存
kmalloc/kfree
d.vmalloc/vfree
vmalloc采用高端内存预留的虚拟空间来收集内存碎片引起的不连续的物理内存页,是用于非连续物理内存分配。
当kmalloc分配不到内存且无物理内存连续的需求时,可以使用。(优先从高端内存中查找)
e.ioremap()/iounmap()
ioremap()的作用是把device寄存器和内存的物理地址区域映射到内核虚拟区域,返回值为内核的虚拟地址。使用的线性地址区间也在vmmlloc段
注:
vmalloc()与alloc_pages(_GFP_HIGHMEM)+kmap();前者不连续,后者只能映射一个高端内存页面
__get_free_pages与alloc_pages(NORMAL)+page_address(); 两者完全等同
内核地址通过__va/__pa进行中低内存的直接映射
高端内存采用kmap/kmap_atomic的方式来映射
个人总结如下:
a.在<128kB的一般内存分配时,使用kmalloc
允许睡眠:GFP_KERNEL
不允许睡眠:GFP_ATOMIC
b.在>128kB的内存分配时,使用get_free_pages,获取成片页面,直接返回虚拟地址(<4M)(或alloc_pages + page_address)
c.b失败,
如果要求分配高端内存:alloc_pages(_GFP_HIGHMEM)+kmap(仅能映射一个页面)
如果不要求内存连续: 则使用vmalloc进行分配逻辑连续的大块页面.(不建议)/分配速度较慢,访问速率较慢。
d.频繁创建和销毁很多较大数据结构,使用slab.
e.高端内存映射:
允许睡眠:kmap (永久映射)
不允许睡眠:kmap_atomic (临时映射)会覆盖以前到映射(不建议)