关于包含 MMU 的处理器而言, Linux 体系供给了杂乱的存储办理体系,使得进程所能拜访的内存到达 4GB。进程的 4GB 内存空间被分为两个部分—用户空间与内核空间。用户空间地址一般散布为 0~3GB(即 PAGE_OFFSET),这样,剩余的 3~4GB 为内核空间。
内核空间恳求内存触及的函数首要包含 kmalloc()、__get_free_pages()和 vmalloc()等。
经过内存映射,用户进程能够在用户空间直接拜访设备。
内核地址空间
每个进程的用户空间都是彻底独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核担任映射,它并不会跟着进程改动,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。用户进程只要经过体系调用(代表用户进程在内核态履行)等办法才能够拜访到内核空间。
Linux 中 1GB 的内核地址空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和体系保存映射区这几个区域,如图所示。
保存区
Linux 保存内核空间最顶部 FIXADDR_TOP~4GB 的区域作为保存区。
专用页面映射区
紧接着最顶端的保存区以下的一段区域为专用页面映射区(FIXADDR_START~FIXADDR_TOP),它的总尺度和每一页的用处由 fixed_address 枚举结构在编译时预界说,用__fix_to_virt(index)可获取专用区内预界说页面的逻辑地址。
高端内存映射区
当体系物理内存大于 896MB 时,超越物理内存映射区的那部分内存称为高端内存(而未超越物理内存映射区的内存一般被称为惯例内存),内核在存取高端内存时有必要将它们映射到高端页面映射区。
虚存内存分配区
用于 vmalloc()函数,它的前部与物理内存映射区有一个隔离带,后部与高端映射区也有一个隔离带。
物理内存映射区
一般状况下,物理内存映射区最大长度为 896MB,体系的物理内存被次序映射在内核空间的这个区域中。
虚拟地址与物理地址联系
关于内核物理内存映射区的虚拟内存,运用 virt_to_phys()能够完结内核虚拟地址转化为物理地址, virt_to_phys()的完结是体系结构相关的,关于 ARM 而言, virt_to_phys()的界说如代码:
static inline unsigned long virt_to_phys(void *x) { return __virt_to_phys((unsigned long)(x)); } /* PAGE_OFFSET 一般为 3GB,而 PHYS_OFFSET 则定于为体系 DRAM 内存的基地址 */ #define __virt_to_phys(x) ((x) – PAGE_OFFSET + PHYS_OFFSET)
内存分配
在 Linux 内核空间恳求内存触及的函数首要包含 kmalloc()、__get_free_pages()和 vmalloc()等。kmalloc()和__get_free_pages()( 及其相似函数) 恳求的内存坐落物理内存映射区域,而且在物理上也是接连的,它们与实在的物理地址只要一个固定的偏移,因而存在较简略的转化联系。而vmalloc()在虚拟内存空间给出一块接连的内存区,实质上,这片接连的虚拟内存在物理内存中并不必定接连,而 vmalloc()恳求的虚拟内存和物理内存之间也没有简略的换算联系。
kmalloc()
void *kmalloc(size_t size, int flags);
给 kmalloc()的第一个参数是要分配的块的巨细,第二个参数为分配标志,用于操控 kmalloc()的行为。
flags
最常用的分配标志是 GFP_KERNEL,其含义是在内核空间的进程中恳求内存。 kmalloc()的底层依靠__get_free_pages()完结,分配标志的前缀 GFP 正好是这个底层函数的缩写。运用 GFP_KERNEL 标志恳求内存时,若暂时不能满意,则进程会睡觉等候页,即会引起堵塞,因而不能在中止上下文或持有自旋锁的时分运用 GFP_KERNEL 恳求内存。
在中止处理函数、 tasklet 和内核定时器等非进程上下文中不能堵塞,此刻驱动应当运用GFP_ATOMIC 标志来恳求内存。当运用 GFP_ATOMIC 标志恳求内存时,若不存在闲暇页,则不等候,直接回来。
其他的相对不常用的恳求标志还包含 GFP_USER(用来为用户空间页分配内存,或许堵塞)、GFP_HIGHUSER(相似 GFP_USER,可是从高端内存分配)、 GFP_NOIO(不答应任何 I/O 初始化)、 GFP_NOFS(不答应进行任何文件体系调用)、 __GFP_DMA(要求分配在能够 DMA 的内存区)、 __GFP_HIGHMEM(指示分配的内存能够坐落高端内存)、 __GFP_COLD(恳求一个较长时刻不拜访的页)、 __GFP_NOWARN(当一个分配无法满意时,阻挠内核宣布正告)、 __GFP_HIGH(高优先级恳求,答应取得被内核保存给紧迫状况运用的最终的内存页)、 __GFP_REPEAT(分配失利则极力重复测验)、 __GFP_NOFAIL(标志只许恳求成功,不引荐)和__GFP_NORETRY(若恳求不到,则当即抛弃)。
运用 kmalloc()恳求的内存应运用 kfree()开释,这个函数的用法和用户空间的 free()相似。
__get_free_pages ()
__get_free_pages()系列函数/宏是 Linux 内核本质上最底层的用于获取闲暇内存的办法,由于底层的同伴算法以 page 的 2 的 n 次幂为单位办理闲暇内存,所以最底层的内存恳求总是以页为单位的。
__get_free_pages()系列函数/宏包含 get_zeroed_page()、 __get_free_page()和__get_free_pages()。
/* 该函数回来一个指向新页的指针而且将该页清零 */ get_zeroed_page(unsigned int flags); /* 该宏回来一个指向新页的指针可是该页不清零 */ __get_free_page(unsigned int flags); /* 该函数可分配多个页并回来分配内存的首地址,分配的页数为 2^order,分配的页也不清零 */ __get_free_pages(unsigned int flags, unsigned int order); /* 开释 */ void free_page(unsigned long addr); void free_pages(unsigned long addr, unsigned long order);
__get_free_pages 等函数在运用时,其恳求标志的值与 kmalloc()彻底相同,各标志的含义也与kmalloc()彻底一致,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。
vmalloc()
vmalloc()一般用在为只存在于软件中(没有对应的硬件含义)的较大的次序缓冲区分配内存,vmalloc()远大于__get_free_pages()的开支,为了完结 vmalloc(),新的页表需求被树立。因而,仅仅调用 vmalloc()来分配少数的内存(如 1 页)是不当的。
vmalloc()恳求的内存应运用 vfree()开释, vmalloc()和 vfree()的函数原型如下:
void *vmalloc(unsigned long size); void vfree(void * addr);
vmalloc()不能用在原子上下文中,由于它的内部完结运用了标志为 GFP_KERNEL 的 kmalloc()。
slab
一方面,彻底运用页为单元恳求和开释内存简单导致糟蹋(假如要恳求少数字节也需求 1 页);另一方面,在操作体系的运作进程中,经常会触及很多目标的重复生成、运用和开释内存问题。在Linux 体系中所用到的目标,比较典型的比如是 inode、 task_struct 等。假如咱们能够用适宜的办法使得在目标前后两次被运用时分配在同一块内存或同一类内存空间且保存了根本的数据结构,就能够大大提高功率。 内核确实完结了这种类型的内存池,一般称为后备高速缓存(lookaside cache)。内核对高速缓存的办理称为slab分配器。实际上 kmalloc()便是运用 slab 机制完结的。
留意, slab 不是要替代__get_free_pages(),其在最底层依然依靠于__get_free_pages(), slab在底层每次恳求 1 页或多页,之后再分隔这些页为更小的单元进行办理,然后节省了内存,也提高了 slab 缓冲目标的拜访功率。
#include /* 创立一个新的高速缓存目标,其间可包容恣意数目巨细相同的内存区域 */ struct kmem_cache *kmem_cache_create(const char *name, /* 一般为即将高速缓存的结构类型的姓名 */ size_t size, /* 每个内存区域的巨细 */ size_t offset, /* 第一个目标的偏移量,一般为0 */ unsigned long flags, /* 一个位掩码: SLAB_NO_REAP 即便内存紧缩也不主动缩短这块缓存,不主张运用 SLAB_HWCACHE_ALIGN 每个数据目标被对齐到一个缓存行 SLAB_CACHE_DMA 要求数据目标在DMA内存区分配 */ /* 可选参数,用于初始化新分配的目标,多用于一组目标的内存分配时运用 */ void (*constructor)(void*, struct kmem_cache *, unsigned long), void (*destructor)(void*, struct kmem_cache *, unsigned long) ); /* 在 kmem_cache_create()创立的 slab 后备缓冲中分配一块并回来首地址指针 */ void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags); /* 开释 slab 缓存 */ void kmem_cache_free(struct kmem_cache *cachep, void *objp); /* 收回 slab 缓存,假如失利则阐明内存走漏 */ int kmem_cache_destroy(struct kmem_cache *cachep);
TIp: 高速缓存的运用计算状况能够从/proc/slabinfo取得。
内存池(mempool)
内核中有些当地的内存分配是不答应失利的,内核开发者树立了一种称为内存池的笼统。内存池其实便是某种方式的高速后备缓存,它企图始终保持闲暇的内存以便在紧迫状态下运用。mempool很简单糟蹋很多内存,应尽量防止运用。
#include /* 创立 */ mempool_t *mempool_create(int min_nr, /* 需求预分配目标的数目 */ mempool_alloc_t *alloc_fn, /* 分配函数,一般直接运用内核供给的mempool_alloc_slab */ mempool_free_t *free_fn, /* 开释函数,一般直接运用内核供给的mempool_free_slab */ void *pool_data); /* 传给alloc_fn/free_fn的参数,一般为kmem_cache_create创立的cache */ /* 分配开释 */ void *mempool_alloc(mempool_t *pool, int gfp_mask); void mempool_free(void *element, mempool_t *pool); /* 收回 */ void mempool_destroy(mempool_t *pool);
内存映射
一般状况下,用户空间是不或许也不应该直接拜访设备的,可是,设备驱动程序中可完结mmap()函数,这个函数可使得用户空间直能接拜访设备的物理地址。
这种才能关于显现适配器一类的设备十分有含义,假如用户空间可直接经过内存映射拜访显存的话,屏幕帧的各点的像素将不再需求一个从用户空间到内核空间的仿制的进程。
从 file_operaTIons 文件操作结构体能够看出,驱动中 mmap()函数的原型如下:
int(*mmap)(struct file *, struct vm_area_struct*);
驱动程序中 mmap()的完结机制是树立页表,并填充 VMA 结构体中 vm_operaTIons_struct 指针。VMA 即 vm_area_struct,用于描绘一个虚拟内存区域:
struct vm_area_struct { unsigned long vm_start; /* 开端虚拟地址 */ unsigned long vm_end; /* 完毕虚拟地址 */ unsigned long vm_flags; /* VM_IO 设置一个内存映射I/O区域; VM_RESERVED 告知内存办理体系不要将VMA交流出去 */ struct vm_operaTIons_struct *vm_ops; /* 操作 VMA 的函数集指针 */ unsigned long vm_pgoff; /* 偏移(页帧号) */ void *vm_private_data; … } struct vm_operations_struct { void(*open)(struct vm_area_struct *area); /*翻开 VMA 的函数*/ void(*close)(struct vm_area_struct *area); /*封闭 VMA 的函数*/ struct page *(*nopage)(struct vm_area_struct *area, unsigned long address, int *type); /*拜访的页不在内存时调用*/ /* 当用户拜访页前,该函数答应内核将这些页预先装入内存。驱动程序一般不用完结 */ int(*populate)(struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock); …
树立页表的办法有两种:运用remap_pfn_range函数一次悉数树立或许经过nopage VMA办法每次树立一个页表。
remap_pfn_range
remap_pfn_range担任为一段物理地址树立新的页表,原型如下:
int remap_pfn_range(struct vm_area_struct *vma, /* 虚拟内存区域,必定规模的页将被映射到该区域 */ unsigned long addr, /* 从头映射时的开始用户虚拟地址。该函数为处于addr和addr+size之间的虚拟地址树立页表 */ unsigned long pfn, /* 与物理内存对应的页帧号,实际上便是物理地址右移 PAGE_SHIFT 位 */ unsigned long size, /* 被从头映射的区域巨细,以字节为单位 */ pgprot_t prot); /* 新页所要求的维护特点 */
demo:
static int xxx_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 = &xxx_remap_vm_ops; xxx_vma_open(vma); return 0; }/* VMA 翻开函数 */void xxx_vma_open(struct vm_area_struct *vma) { … printk(KERN_NOTICE “xxx VMA open, virt %lx, phys %lx\n”, vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);}/* VMA 封闭函数 */void xxx_vma_close(struct vm_area_struct *vma){ ... printk(KERN_NOTICE "xxx VMA close.\n");}static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA 操作结构体 */ .open = xxx_vma_open, .close = xxx_vma_close, ...};
nopage
除了 remap_pfn_range()以外,在驱动程序中完结 VMA 的 nopage()函数一般能够为设备供给愈加灵敏的内存映射途径。当拜访的页不在内存,即产生缺页反常时, nopage()会被内核主动调用。
static int xxx_mmap(struct file *filp, struct vm_area_struct *vma){ unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; /* 预留 */ vma->vm_ops = &xxx_nopage_vm_ops; xxx_vma_open(vma); return 0;}struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type){ 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; pageptr = pfn_to_page(pageframe); /* 页帧号->页描绘符 */ get_page(pageptr); /* 取得页,添加页的运用计数 */ if (type) *type = VM_FAULT_MINOR; return pageptr; /*回来页描绘符 */}
上述函数对惯例内存进行映射, 回来一个页描绘符,可用于扩展或缩小映射的内存区域。
由此可见, nopage()与 remap_pfn_range()的一个较大差异在于 remap_pfn_range()一般用于设备内存映射,而 nopage()还可用于 RAM 映射,其调用产生在缺页反常时。