linux驱动程序运行在“内核”空间。一般情况下驱动程序中都是调用kmalloc()来给数据结构分配内存,调用vmalloc()为活动的交换区分配数据结构,为某些I/O驱动程序分配缓冲区,或为模块分配空间;kmalloc和vmalloc分配的是内核的内存。
程序员必备接口测试调试工具:立即使用
Apipost = Postman + Swagger + Mock + Jmeter
Api设计、调试、文档、自动化测试工具
后端、前端、测试,同时在线协作,内容实时同步
本教程操作环境:linux7.3系统、Dell G3电脑。
linux驱动程序运行在“内核”空间。
对于一般编写的单片机程序来说应用程序和驱动程序往往是杂糅的,拥有一定能力水平的单片机程序编程人员可以实现应用和驱动的分层。而在Linux系统中已经强制将应用和驱动进行了分层。
在单片机程序中,应用可以直接操作底层的寄存器。而在Linux系统中却禁止这样的行为,举个例子:Linux应用的编写人员故意在应用中调用了驱动中关于电源管理的驱动,关闭了系统,那不就得不偿失了?
具体的Linux应用程序对驱动的调用如图所示:
应用程序运行在用户空间,驱动程序运行在内核空间。处于用户空间应用程序如果想要实现对内核的操作,必须经过一种"系统调用"的方法,实现从用户空间进入内核空间,实现对底层的操作。
Linux中的内核空间
内核也是程序,也应该具有自己的虚存空间,但是作为一种为用户程序服务的程序,内核空间有它自己的特点。
内核空间与用户空间的关系
在一个32位系统中,一个程序的虚拟空间最大可以是4GB,那么最直接的做法就是,把内核也看作是一个程序,使它和其他程序一样也具有4GB空间。但是这种做法会使系统不断的切换用户程序的页表和内核页表,以致影响计算机的效率。解决这个问题的最好做法就是把4GB空间分成两个部分:一部分为用户空间,另一部分为内核空间,这样就可以保证内核空间固定不变,而当程序切换时,改变的仅是程序的页表。这种做法的唯一缺点便是内核空间和用户空间均变小了。
例如:在i386这种32位的硬件平台上,Linux在文件page.h中定义了一个常量PAGE_OFFSET:
#ifdef CONFIG_MMU #define __PAGE_OFFSET (0xC0000000) //0xC0000000为3GB #else #define __PAGE_OFFSET (0x00000000) #endif #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
Linux以PAGE_OFFSET为界将4GB的虚拟内存空间分成了两部分:地址0~3G-1这段低地址空间为用户空间,大小为3GB;地址3GB~4GB-1这段高地址空间为内核空间,大小为1GB。
当系统中运行多个程序时,多个用户空间与内核空间的关系可以表示如下图:
如图中所示,程序1、2……n共享内核空间。当然,这里的共享指得是分时共享,因为在任何时刻,对于单核处理器系统来说,只能有一个程序在运行。
内核空间的总体布局
Linux在发展过程中,随着硬件设备的更新和技术水平的提高,其内核空间布局的发展也是一种不断打补丁的方式。这样的后果就是使得内核空间被分成不同的几个区域,而且在不同的区域具有不同的映射方式。通常,人们认为Linux内核空间有三个区域,即DMA区(ZONE_DMA)、普通区(ZONE_NORMAL)和高端内存区(ZONE_HIGHMEM)。
实际物理内存较小时内核空间的直接映射
早期计算机实际配置的物理内存通常只有几MB,所以为了提高内核通过虚拟地址访问物理地址内存的速度,内核空间的虚拟地址与物理内存地址采用了一种从低地址向高地址依次一一对应的固定映射方式,如下图所示:
可以看到,这种固定映射方式使得虚拟地址与物理地址的关系变得很简单,即内核虚拟地址与实际物理地址只在数值上相差一个固定的偏移量PAGE_OFFSET,所以当内核使用虚拟地址访问物理页框时,只需在虚拟地址上减去PAGE_OFFSET即可得到实际物理地址,比使用页表的方式要快得多!
由于这种做法几乎就是直接使用物理地址,所以这种按固定映射方式的内核空间也就叫做“物理内存空间”,简称物理内存。另外,由于固定映射方式是一种线性映射,所以这个区域也叫做线性映射区。
当然,这种情况下(计算机实际物理内存较小时),内核固定映射空间仅占整个1GB内核空间的一部分。例如:在配置32MB实际物理内存的x86计算机系统时,内核的固定映射区便是PAGE_OFFSET~(PAGE_OFFSET+0x02000000)这个32MB空间。那么内核空间剩余的内核虚拟空间怎么办呢?
当然还是按照普通虚拟空间的管理方式,以页表的非线性映射方式使用物理内存。具体来说,在整个1GB内核空间中去除固定映射区,然后在剩余部分中再去除其开头部分的一个8MB隔离区,余下的就是映射方式与用户空间相同的普通虚拟内存映射区。在这个区,虚拟地址和物理地址不仅不存在固定映射关系,而且通过调用内核函数vmalloc()获得动态内存,故这个区就被称为vmalloc分配区,如下图所示:
对于配置32MB实际物理内存的x86计算机系统来说,vmalloc分配区的起始位置为PAGE_OFFSET+0x02000000+0x00800000。
这里说明一下:这里说的内核空间与物理页框的固定映射,实质上是内核页对物理页框的一种“预定”,并不是说这些页就“霸占”了这些物理页框。即只有当虚拟页真正需要访问物理页框时,虚拟页才与物理页框绑定。而平时,当某个物理页框不被与它对应的虚拟页所使用时,该页框完全可以被用户空间以及后面所介绍的内核kmalloc分配区使用。
总之,在实际物理内存较小的系统中,实际内存的大小就是内核空间的物理内存区与vmalloc分配区的边界。
ZONE_DMA区与ZONE_NORMAL区
对于整个1GB的内核空间,人们还把该空间头部的16MB叫做DMA区,即ZONE_DMA区,因为以往硬件将DMA空间固定在了物理内存的低16MB空间;其余区则叫做普通区,即ZONE_NORMAL。
内核空间的高端内存
随着计算机技术的发展,计算机的实际物理内存越来越大,从而使得内核固定映射区(线性区)也越来越大。显然,如果不加以限制,当实际物理内存达到1GB时,vmalloc分配区(非线性区)将不复存在。于是以前开发的、调用了vmalloc()的内核代码也就不再可用,显然为了兼容早期的内核代码,这是不能允许的。
下图就表示了这种内核空间所面临的局面:
显然,出现上述问题的原因就是没有预料到实际物理内存可以超过1GB,因而没有为内核固定映射区的边界设定限制,而任由其随着实际物理内存的增大而增大。
解决上述问题的方法就是:对内核空间固定映射区的上限加以限制,使之不能随着物理内存的增加而任意增加。Linux规定,内核映射区的上边界的值最大不能大于一个小于1G的常数high_menory,当实际物理内存较大时,以3G+high_memory为边界来确定物理内存区。
例如:对于x86系统,high_memory的值为896M,于是1GB内核空间余下的128MB为非线性映射区。这样就确保在任何情况下,内核都有足够的非线性映射区以兼容早期代码并可以按普通虚存方式访问实际物理内存的1GB以上的空间。
也就是说,高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。当计算机是物理内存较大时,内核空间的示意图如下:
习惯上,Linux把内核空间3G+high_memory~4G-1的这个部分叫做高端内存区(ZONE_HIGHMEM)。
总结一下:在x86结构的内核空间,三种类型的区域(从3G开始计算)如下:
- ZONE_DMA:内核空间开始的16MB
- ZONE_NORMAL:内核空间16MB~896MB(固定映射)
- ZONE_HIGHMEM :内核空间896MB ~ 结束(1G)
根据应用目标不同,高端内存区分vmalloc区、可持久映射区和临时映射区。内核空间中高端内存的布局如下图所示:
vmalloc映射区
vmalloc映射区时高端内存的主要部分,该区间的头部与内核线性映射空间之间有一个8MB的隔离区,尾部与后续的可持久映射区有一个4KB的隔离区。
vmalloc映射区的映射方式与用户空间完全相同,内核可以通过调用函数vmalloc()在这个区域获得内存。这个函数的功能相当于用户空间的malloc(),所提供的内存空间在虚拟地址上连续(注意,不保证物理地址连续)。
可持久内核映射区
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从PKMAP_BASE开始,用于映射高端内存,就是可持久内核映射区。
在可持久内核映射区,可通过调用函数kmap()在物理页框与内核虚拟页之间建立长期映射。这个空间通常为4MB,最多能映射1024个页框,数量较为稀少,所以为了加强页框的周转,应及时调用函数kunmap()将不再使用的物理页框释放。
临时映射区
临时映射区也叫固定映射区和保留区。该区主要应用在多处理器系统中,因为在这个区域所获得的内存空间没有所保护,故所获得的内存必须及时使用;否则一旦有新的请求,该页框上的内容就会被覆盖,所以这个区域叫做临时映射区。
关于高端内存区一篇很不错的文章:linux 用户空间与内核空间——高端内存详解。
内核内存分配修饰符gfp
为了在内核内存请求函数对请求进行必要的说明,Linux定义了多种内存分配修饰符gfp。它们是行为修饰符、区修饰符、类型修饰符。
行为修饰符
在内存分配函数中的行为修饰符说明内核应当如何分配内存。主要行为修饰符如下:
修饰符 | 说明 |
__GFP_WAIT | 分配器可以休眠 |
__GFP_HIGH | 分配器可以访问紧急事件缓冲池 |
__GFP_IO | 分配器可以启动磁盘IO |
__GFP_FS | 分配器可以启动文件系统IO |
__GFP_COLD | 分配器应该使用高速缓冲中快要淘汰的页框 |
__GFP_NOWARN | 分配器不发出警告 |
__GFP_REPEAT | 分配失败时重新分配 |
__GFP_NOFAILT | 分配失败时重新分配,直至成功 |
__GFP_NORETRY | 分配失败时不再重新分配 |
区修饰符
区修饰符说明需要从内核空间的哪个区域中分配内存。内存分配器默认从内核空间的ZONE_NORMAL开始逐渐向高端获取为内存请求者分配内存区,如果用户特意需要从ZONE_DMA或ZONE_HOGNMEM获得内存,那么就需要内存请求者在内存请求函数中使用以下两个区修饰符说明:
修饰符 | 说明 |
__GFP_DMA | 从ZONE_DMA区分配内存 |
__GFP_HIGHMEM | 从ZONE_HIGHMEM区分配内存 |
类型修饰符
类型修饰符实质上是上述所述修饰符的联合应用。也就是:将上述的某些行为修饰符和区修饰符,用“|”进行连接并另外取名的修饰符。这里就不多介绍了。
内核常用内存分配及地址映射函数
函数vmalloc()
函数vmalloc()在vmalloc分配区分配内存,可获得虚拟地址连续,但并不保证其物理页框连续的较大内存。与物理空间的内存分配函数malloc()有所区别,vmalloc()分配的物理页不会被交换出去。函数vmalloc()的原型如下:
void *vmalloc(unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); }
void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot) { return kmalloc(size, (gfp_mask | __GFP_COMP) & ~__GFP_HIGHMEM); }
其中,参数size为所请求内存的大小,返回值为所获得内存虚拟地址指针。
与vmalloc()配套的释放函数如下:
void vfree(const void *addr) { kfree(addr); }
其中,参数addr为待释放内存指针。
函数kmalloc()
kmalloc()是内核另一个常用的内核分配函数,它可以分配一段未清零的连续物理内存页,返回值为直接映射地址。由kmalloc()可分配的内存最大不能超过32页。其优点是分配速度快,缺点是不能分配大于128KB的内存页(出于跨平台考虑)。
在linux/slab.h文件中,该函数的原型声明如下:
static __always_inline void *kmalloc(size_t size, gfp_t flags) { struct kmem_cache *cachep; void *ret; if (__builtin_constant_p(size)) { int i = 0; if (!size) return ZERO_SIZE_PTR; #define CACHE(x) if (size <= x) goto found; else i++; #include <linux/kmalloc_sizes.h> #undef CACHE return NULL; found: #ifdef CONFIG_ZONE_DMA if (flags & GFP_DMA) cachep = malloc_sizes[i].cs_dmacachep; else #endif cachep = malloc_sizes[i].cs_cachep; ret = kmem_cache_alloc_notrace(cachep, flags); trace_kmalloc(_THIS_IP_, ret, size, slab_buffer_size(cachep), flags); return ret; } return __kmalloc(size, flags); }
其中,参数size为以字节为单位表示的所申请空间的大小;参数flags决定了所分配的内存适合什么场合。
与函数kmalloc()对应的释放函数如下:
void kfree(const void *objp) { struct kmem_cache *c; unsigned long flags; trace_kfree(_RET_IP_, objp); if (unlikely(ZERO_OR_NULL_PTR(objp))) return; local_irq_save(flags); kfree_debugcheck(objp); c = virt_to_cache(objp); debug_check_no_locks_freed(objp, obj_size(c)); debug_check_no_obj_freed(objp, obj_size(c)); __cache_free(c, (void *)objp); local_irq_restore(flags); }
小结一下,kmalloc、vmalloc、malloc的区别:
- kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存;
- kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西;
- kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大;
- vmalloc比kmalloc要慢。
也就是说:kmalloc、vmalloc这两个函数所分配的内存都处于内核空间,即从3GB~4GB;但位置不同,kmalloc()分配的内存处于3GB~high_memory(ZONE_DMA、ZONE_NORMAL)之间,而vmalloc()分配的内存在VMALLOC_START~4GB(ZONE_HIGHMEM)之间,也就是非连续内存区。一般情况下在驱动程序中都是调用kmalloc()来给数据结构分配内存,而vmalloc()用在为活动的交换区分配数据结构,为某些I/O驱动程序分配缓冲区,或为模块分配空间。
可参考文章:Kmalloc和Vmalloc的区别。
函数alloc_pages()
与上述在虚拟空间分配内存的函数不同,alloc_pages()是在物理内存空间分配物理页框的函数,其原型如下:
static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) { if (unlikely(order >= MAX_ORDER)) return NULL; return alloc_pages_current(gfp_mask, order); }
其中,参数order表示所分配页框的数目,该数目为2^order。order的最大值由include/Linux/Mmzone.h文件中的宏MAX_ORDER决定。参数gfp_mask为说明内存页框分配方式及使用场合。
函数返回值为页框块的第一个页框page结构的地址。
调用下列函数可以获得页框的虚拟地址:
void *page_address(struct page *page) { unsigned long flags; void *ret; struct page_address_slot *pas; if (!PageHighMem(page)) return lowmem_page_address(page); pas = page_slot(page); ret = NULL; spin_lock_irqsave(&pas->lock, flags); if (!list_empty(&pas->lh)) { struct page_address_map *pam; list_for_each_entry(pam, &pas->lh, list) { if (pam->page == page) { ret = pam->virtual; goto done; } } } done: spin_unlock_irqrestore(&pas->lock, flags); return ret; }
使用函数alloc_pages()获得的内存应该使用下面的函数释放:
void __free_pages(struct page *page, unsigned int order) { if (put_page_testzero(page)) { if (order == 0) free_hot_page(page); else __free_pages_ok(page, order); } }
函数kmap()
kmap()是一个映射函数,它可以将一个物理页框映射到内核空间的可持久映射区。这种映射类似于内核ZONE_NORMAL的固定映射,但虚拟地址与物理地址的偏移不一定是PAGE_OFFSET。由于内核可持久映射区的容量有限(总共只有4MB),因此当内存使用完毕后,应该立即释放。
函数kmap()的函数原型如下:
void *kmap(struct page *page) { might_sleep(); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); }
小结
由于CPU的地址总线只有32位, 32的地址总线无论是从逻辑上还是从物理上都只能描述4G的地址空间(232=4Gbit),在物理上理论上最多拥有4G内存(除了IO地址空间,实际内存容量小于4G),逻辑空间也只能描述4G的线性地址空间。
为了合理的利用逻辑4G空间,Linux采用了3:1的策略,即内核占用1G的线性地址空间,用户占用3G的线性地址空间。所以用户进程的地址范围从0~3G,内核地址范围从3G~4G,也就是说,内核空间只有1G的逻辑线性地址空间。
如果Linux物理内存小于1G的空间,通常内核把物理内存与其地址空间做了线性映射,也就是一一映射,这样可以提高访问速度。但是,当Linux物理内存超过1G时,线性访问机制就不够用了,因为只能有1G的内存可以被映射,剩余的物理内存无法被内核管理,所以,为了解决这一问题,Linux把内核地址分为线性区和非线性区两部分,线性区规定最大为896M,剩下的128M为非线性区。从而,线性区映射的物理内存成为低端内存,剩下的物理内存被成为高端内存。与线性区不同,非线性区不会提前进行内存映射,而是在使用时动态映射。
低端内存又分成两部分:ZONE_DMA:内核空间开始的16MB、ZONE_NORMAL:内核空间16MB~896MB(固定映射)。剩下的就是高端内存:ZONE_HIGHMEM :内核空间896MB ~ 结束(1G)。
根据应用目标不同,高端内存区分vmalloc区、可持久映射区和临时映射区三部分。vmalloc区使用vmalloc()函数进行分配;可持久映射区使用allc_pages()获得对应的 page,在利用kmap()函数直接映射;临时映射区一般用于特殊需求。
内核空间(3G~4G) |
高端内存(3G+high_memory~4G)ZONE_HIGHMEM 非线性映射区 |
临时映射区 |
可持久映射区 | ||
vmalloc区 | ||
低端内存(3G~3G+high_memory-1) 线性映射区(固定映射区) |
ZONE_NORMAL | |
ZONE_DMA | ||
用户空间(0~3G-1) | 页目录–>中间页目录–>页表 |