http://blog.csdn.net/zhijianjingling00/article/details/9335333
在内核模块中申请分配内存需要使用内核中的专用API:kmalloc、vmalloc、kzalloc、kcalloc、get_free_pages;当然,设备驱动程序也不例外;
对于提供了MMU功能的处理器而言,Linux提供了复杂的内存管理系统,使得进程所能访问到的地址空间可以达到4GB;而这4GB的空间又被划分为两个部分:0GB~3GB(PAGE_OFFSET,x86中的值是0xC0000000)的区域被用作进程的用户空间,3GB~4GB的区域被用作内核空间;
在内核空间中,从3GB到vmalloc_start之间的这段地址区域作为物理内存映射区使用,该段映射区域内包含了内核镜像、物理页框表mem_map等等,比如,我们使用的系统物理内存为160MB,那么,3GB~3GB+vmalloc_start之间的区域就应该是映射的物理内存;在物理内存映射区域之后,就是虚拟内存vmalloc区域;对于160MB的系统而言,vmalloc_start的位置就应该在3GB+160MB位置附近(在物理内存映射区与vmalloc_start位置之间还存在一个8M的gap来防止越界),vmalloc_end的位置接近4GB的位置(系统会在最后的位置处保留一片128KB大小的区域专用于页面映射);
一、kmalloc
#include <linux/slab.h>
static inline void *kmalloc(size_t size, gfp_t flags);
参数:size:指定要分配的块的大小,单位是字节;flags:指定分配内存时的控制方式;
该函数用于在内核空间中分配内存使用,它的返回速度快(除非被阻塞),并且对其分配的内存不进行任何初始化(清零)操作,分配的内存区域仍然保留有他原有的内容;
kmalloc申请得到的是物理内存,位于物理内存映射区,而且在物理地址上是连续的;但是kmalloc返回的内存地址却是虚拟地址(线性地址),返回的这个虚拟地址(线性地址)与真实的物理地址之间仅仅相差一个固定的偏移值;因此,kmalloc申请得到的物理内存块的首地址与其返回的虚拟地址之间存在着比较简单的转换关系;通过内核提供的函数virt_to_phys()可以实现该虚拟地址到真实的内核物理地址之间的转换:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
static inline unsigned long virt_to_phys(volatile void* address)
{
return __pa(address);
}
参数address是kmalloc返回的一个虚拟地址;该转换过程就是虚拟地址减去3GB(PAGE_OFFSET=0xC0000000);
一般情况下,PAGE_OFFSET=3*1024*1024*1024=0xC0000000(3G);
与之对应的函数就是phys_to_virt()用于把内核物理地址转换为虚拟地址:
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
static inline void * phys_to_virt(unsigned long address)
{
return __va(address);
}
这两个函数都定义在include/asm-i386/io.h中;
kmalloc()函数用于小块内存的申请,最小可以申请的内存是32字节或64字节,最大可以申请的内存是128KB-16,其中,被减掉的16个字节用于存储页描述符结构;这些都依赖于体系架构所使用的页面大小;kmalloc申请的内存在物理地址上是连续的,这对于要进行DMA传输的设备来说,是非常重要的;
kmalloc()的内存分配是基于slab机制实现的,slab机制是为分配小内存而提供的一种高效的机制;但是slab机制也不是独立的,它本身也是在页分配器的基础上来划分更细粒度的内存供调用者使用;也就是说,系统先使用页分配器分配以页为最小单位的连续物理地址,然后,kmalloc()再在这个基础上根据调用者的需要进行切分的;另外,slab机制分配的内存在物理地址和虚拟地址(线性地址/逻辑地址)上都是连续的;
对于kmalloc()申请的内存,需要使用kfree()函数来释放;
备注:kmalloc是基于slab机制实现的;
二、get_free_pages
#include <asm/pages.h>
fastcall unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page * page;
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
参数gfp_mask用于指定申请内存时的控制方式,order用于指定申请的页数;它申请的内存位于(PAGE_OFFSET,HIGH_MEMORY)之间;
__get_free_pages()函数是页面分配器提供给调用者的最底层的内存分配函数,它申请的内存也是连续的物理内存,同样位于物理内存映射区;它是基于buddy机制实现的;在使用buddy机制实现的物理内存管理系统中,最小的分配粒度(单位)也是以页为单位的;在__get_free_pages()内部通过调用alloc_pages()来分配物理内存页;
__get_free_page()函数分配的是连续的物理内存,处理的是连续的物理地址,但是返回的也是虚拟地址(线性地址);如果想要得到正确的物理地址,也需要使用virt_to_phys()可进行转换;
对于__get_free_pages()函数申请的内存,需要使用__free_pages()函数来释放;
备注:__get_free_pages是基于buddy机制实现的;
三、vmalloc
#include <linux/vmalloc.h>
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 __vmalloc_node(size, gfp_mask, prot, -1);
}
void* __vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot, int node)
{
struct vm_struct *area;
size = PAGE_ALIGN(size);
if(!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = get_vm_area_node(size, VM_ALLOC, node);
if(!area)
return NULL;
return __vmalloc_area_node(area, gfp_mask, prot, node);
}
void* __vmalloc_area_node(struct vm_struct* area, gfp_t gfp_mask, pgprot_t prot, int node);
void* __vmalloc_area(struct vm_struct* area, gfp_t gfp_mask, pgprot_t prot)
{
return __vmalloc_area_node(area, gfp_mask, prot, -1);
}
vmalloc()函数也是用于申请内存的,但是它申请的内存是位于vmalloc_start到vmalloc_end之间的虚拟内存;它申请的内存在虚拟地址(线性地址/逻辑地址)上是连续的,但是并不要求在物理地址上连续,并且返回的地址与物理地址之间没有简单的转换关系;
vmalloc()函数适用于大块内存的申请环境中;但是它申请的内存不能直接用于DMA传输;因为DMA传输需要使用物理地址连续的内存块;
对于vmalloc()申请的内存,需要使用vfree()函数来释放;
备注:vmalloc是基于slab机制实现的;
四、比较
1).kmalloc/__get_free_pages申请的内存块都在物理内存映射区,即在(PAGE_OFFSET,HIGH_MEMORY)之间,处理的都是物理地址,且保证在物理地址空间上是连续的;二者返回的都是虚拟地址,如果需要得到正确的物理地址,需要使用virt_to_phys()进行转换;但是,kmalloc和vmalloc都是以字节为单位进行申请,而__get_free_pages()则是以页为单位进行申请;
2).vmalloc函数申请的内存块位于虚拟内存映射区,即在(VMALLOC_START,VMALLOC_END)之间,处理的都是虚拟内存,且保证在虚拟地址空间上是连续的,但是在物理地址空间上不要求连续;一般作为交换区、模块的内存使用;
3).kmalloc和vmalloc都是基于slab机制实现的,但是kmalloc的速度比vmalloc的速度快;__get_free_pages是基于buddy机制实现的,速度也较快;
4).kmalloc用于小块内存的申请,通常,一次所能申请的内存块的大小在(32/64字节,128KB-16)之间;而vmalloc可以用于分配大块内存的场合;
5).kmalloc申请的内存块在物理地址空间上是连续的,所以它申请的内存块可以直接用于DMA传输;vmalloc申请的内存块在虚拟地址空间上连续,但是在物理地址空间上不要求连续,所以它申请的内存块不能直接用于DMA传输;
6).kmalloc申请的内存块用kfree释放;vmalloc申请的内存块用vfree释放;__get_free_pages申请的内存页用__free_pages释放;
7).kmalloc申请得到的地址称为内核逻辑地址,vmalloc申请得到的地址称为内核虚拟地址;
五、其它函数
1).static inline void *kzalloc(size_t size, gfp_t flags);
该函数比kmalloc多了一个功能,就是会把申请得到的内存块初始化为0;
2).static inline void* kcalloc(size_t n, size_t size, gfp_t flags)
{
if(n != 0 && size > ULONG_MAX / n)
return NULL;
return kzalloc(n * size, flags);
}
该函数用于申请一个数组的内存空间,并把申请得到的内存都初始化为0;
六、GFP标记
kmalloc、kzalloc、kcalloc、vmalloc、get_free_pages函数在调用时都有一个gfp_t类型的控制标记flags;这个标记用于控制申请内存时的内存分配控制方式; #include <linux/gfp.h>
GFP的标记有两种:带双下划线前缀的和不带双下划线前缀的;
不带双下划线前缀的GFP标志:
GFP_ATOMIC:用于在中断上下文和进程上下文之外的其它代码中分配内存;从不睡眠;
GFP_KERNEL:内核正常分配内存;可能睡眠;
GFP_USER :用于为用户空间页分配内存;可能睡眠;
GFP_HIGHUSER:如同GFP_USER,但它是从高端内存中申请;
GFP_NOIO和GFP_NOFS:功能如同GFP_KERNEL,但是它俩增加限制到内核能做的来满足请求;GFP_NOFS分配不允许进行任何文件系统调用,而GFP_NOIO分配根本不允许进行任何IO初始化;它俩主要用于文件系统和虚拟内存代码,那里允许一个分配睡眠,但是递归的文件系统调用会是个坏主意;
带有双下划线前缀的GFP标志:
__GFP_DMA:这个标志要求分配的内存在能够进行DMA的内存区;平台依赖的;
__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存区;平台依赖的;
__GFP_COLD:正常地,内存分配器尽力返回"缓冲热"的页---可能在处理器缓冲中找到的页;相反,这个标志请求一个"冷"页---在一段时间内没被使用的页;它对分配页做DMA读是很有用的,此时在处理器缓冲中出现是没用的;
__GFP_NOWARN:这个标志用于分配内存时阻止内核发出警告,当一个分配请求无法满足时;
__GFP_HIGH:这个标志标识了一个高优先级请求,它被允许来消耗甚至被内核保留给紧急状况的最后的内存页;
__GFP_REPEAT:分配器的动作;当分配器有困难满足一个分配请求时,通过重复尝试的方式来"尽力尝试",但是分配操作仍然有可能失败;
__GFP_NOFAIL:分配器的动作;当分配器有困难满足一个分配请求时,这个标志告诉分配器不要失败,尽最大努力来满足分配请求;
__GFP_NORETRY:分配器的动作;当分配器有困难满足一个分配请求时,这个标志告诉分配器立即放弃,不再做任何尝试;
通常,一个或多个带双下划线前缀的标记相或,即可得到对应的不带双下划线前缀的标记;
最常用的标记就是GFP_KERNEL,它的意思就是当前的这个分配代表运行在内核空间的进程而进行的;换句话说,这意味着调用函数是代表一个进程在执行一个系统调用;使用GFP_KERNEL标记,就意味着kmalloc能够使当前进程在少内存的情况下通过睡眠来等待一个内存页;因此,一个使用GFP_KERNEL的函数必须是可重入的,且不能在原子上下文中运行;当当前进程睡眠,内核采取正确的动作来定位一些空闲的内存页,或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存页;
如果一个内存分配动作发生在中断处理或内核定时器的上下文中时,当前进程就不能被设置为睡眠,也就不能再使用GFP_KERNEL标志了,此时应该使用GFP_ATOMIC标志来代替;正常地,内核试图保持一些空闲页以便来满足原子的分配;当使用GFP_ATOMIC标志时,kmalloc标志能够使用甚至最后一个空闲页;如果这最后一个空闲页不存在,那分配就会失败
linux驱动程序一般工作在内核空间,但也可以工作在用户空间。下面我们将详细解析,什么是内核空间,什么是用户空间,以及如何判断他们。
Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G.Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为"内核空间".而将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为"用户空间)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。从图中可以看出(这里无法表示图),每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET.
内核空间和用户空间之间如何进行通讯?
内核空间和用户空间一般通过系统调用进行通信。
如何判断一个驱动是用户模式驱动还是内核模式驱动? 判断的标准是什么?
用户空间模式的驱动一般通过系统调用来完成对硬件的访问,如通过系统调用将驱动的io空间映射到用户空间等。因此,主要的判断依据就是系统调用。
内核空间和用户空间上不同太多了,说不完,比如用户态的链表和内核链表不一样;用户态用printf,内核态用printk;用户态每个应用程序空间是虚拟的,相对独立的,内核态中却不是独立的,所以编程要非常小心。等等。
还有用户态和内核态程序通讯的方法很多,不单单是系统调用,实际上系统调用是个不好的选择,因为需要系统调用号,这个需要统一分配。
可以通过ioctl、sysfs、proc等来完成。
内存管理,不用多说,言简意赅。在内核里分配内存还真不是件容易的事情,从根本上说,是因为内核不能像用户空间那样奢侈的使用内存。
先来说说内存管理。内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常是字,但是,内存管理单元MMU通常以页为单位进行处理。因此,从虚拟内存的交代来看,页就是最小单位。内核用struct page(linux/mm.h)结构表示系统中的每个物理页:
06 |
struct address_space*mapping; |
flag用来存放页的状态,每一位代表一种状态,所以至少可以同时表示出32中不同的状态,这些状态定义在linux/page-flags.h中。count记录了该页被引用了多少次。mapping指向与该页相关的address_space对象。virtual是页的虚拟地址,它就是页在虚拟内存中的地址。要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述是短暂的。内核仅仅用这个结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
在linux中,内核也不是对所有的也都一视同仁,内核而是把页分为不同的区,使用区来对具有相似特性的页进行分组。Linux必须处理如下两种硬件存在缺陷而引起的内存寻址问题:
1.一些硬件只能用某些特定的内存地址来执行DMA 2.一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。 为了解决这些制约条件,Linux使用了三种区: 1.ZONE_DMA:这个区包含的页用来执行DMA操作。 2.ZONE_NOMAL:这个区包含的都是能正常映射的页。 3.ZONE_HIGHEM:这个区包"高端内存",其中的页能不永久地映射到内核地址空间。 |
区的实际使用与体系结构是相关的。linux把系统的页划分区,形成不同的内存池,这样就可以根据用途进行分配了。需要说明的是,区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑上的分组。尽管某些分配可能需要从特定的区中获得页,但这并不是说,某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占用其他可用去的内存。下表给出每个区及其在X86上所占的列表:
每个区都用定义在linux/mmzone.h中的struct zone表示,如下:
03 |
unsigned long free_pages; |
04 |
unsigned long pages_min, pages_low, pages_high; |
05 |
unsigned long protection[MAX_NR_ZONES]; |
07 |
struct list_head active_list; |
08 |
struct list_head inactive_list; |
09 |
unsigned long nr_scan_active; |
10 |
unsigned long nr_scan_inactive; |
11 |
unsigned long nr_active; |
12 |
unsigned long nr_inactive; |
13 |
int all_unreclaimable; |
14 |
unsigned long pages_scanned; |
15 |
struct free_area free_area[MAX_ORDER]; |
16 |
wait_queue_head_t * wait_table; |
17 |
unsigned long wait_table_size; |
18 |
unsigned long wait_table_bits; |
19 |
struct per_cpu_pageset pageset[NR_CPUS]; |
20 |
struct pglist_data *zone_pgdat; |
21 |
struct page *zone_mem_map; |
22 |
unsigned long zone_start_pfn; |
25 |
unsigned long spanned_pages; |
26 |
unsigned long present_pages; |
其中的lock域是一个自旋锁,这个域只保护结构,而不是保护驻留在这个区中的所有页。没有特定的锁来保护单个页。free_pages域是这个区中空闲页的个数。内核尽可能的保护有pages_min个空闲页可用。name域是一个以NULL结束的字符串,表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page_alloc.h中,三个区的名字分别是"DMA","Normal","HighMem"。
内核提供了一种请求内层的底层机制,并提供了对它进行访问的几个接口。所有这些接口都是以页为单位进行操作的。下表给出所有底层的页分配方法:
当你不再需要页时可以用下列函数释放它们,只是提醒:仅能释放属于你的页,否则可能导致系统崩溃。内核是完全信任自己的,如果有非法操作,内核会开心的把自己挂起来,停止运行。列表如下:
上面提到都是以页为单位的分配方式,那么对于常用的以字节为单位的分配来说,内核通供的函数是kmalloc(),和mallloc很像吧,其实还真是这样,只不过多了一个flags参数。用它可以获得以字节为单位的一块内核内存。如果需要的是页----尤其是在你的需求总量接近2的幂次方的时候----那么,前面讨论的页分配接口可能是更好的选择。
接下来,注意的话,可能会发现无论是页分配接口还是kmalloc都有一个分配器标志(如GFP_KERNEL这样的)。这些标志可分为三类:行为修饰符,区修饰符及类型.下面就来讨论个问题.
1.行为修饰符(linux/gfp.h):表示内核应当如何分配所需的内存。在某些特定的情况下,只能使用某些特定的方法分配内存。可以同时使用这些标志,用|链接。列表如下:
2.区分配符:它只关心去应当从何处分配。通常,分配可以从任何区开始。不过,内核优先从ZONE_NORMAL开始,这样可以确保其他区在需要时有足够的空闲页可以使用。区修饰符如下:
不能给_get_free_pages()指定ZONE_HIGHMEM,因为这个函数返回都是逻辑地址,而不是page结构。这两个函数分配的内存当前可能有可能还没有映射到内核的虚拟地址空间,因此,也可能根本就没有逻辑地址。只有alloc_pages()才能分配高端内存。实际上,大多数ZONE_NORMAL就已经足够了。
3.类型标志:指定所需的行为和区描述符以完成特殊类型的处理。正因为这点,内核代码趋向于使用正确的类型标志,而不是一味地指定它可能需要用到的多个描述符。下面两个表分别给出了类型标志的列表和每个类型标志与哪些修饰符相关联:
上表中,左边是类型标志,右边是每种类型标志后隐含的修饰符列表。在编写的大多数代码中,用到的要么是GFP_KERNEL,要么是GFP_ATOMIC。下表是通常情形和所用标志的列表,不管使用那种分配类型,你都必须进行检查,并对错误进行处理:
有了kmalloc,当然就有kfree()(linux/slab.h),释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的内存早就被释放了,在这种情况下调用这个函数会导致严重的后果。特别说明kfree(NULL)是安全的。
vmalloc()和kmalloc是一样的作用,不同在于前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式,如malloc().kmalloc()可以保证在物理地址上都是连续的(当然,虚拟地址当然也是连续的)。vmalloc()函数只确保页在虚拟机地址空间内是连续的。它通过分配非联系的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。但很显然这样会降低处理性能,因为内核不得不做“拼接”的工作。所以这也是为什么不得已才使用vmalloc()的原因(比如获得大内存时)。大多数情况下,只有硬件设备需要得到物理地址连续的内存。硬件设备存在于内存管理单元以外,它根本不懂什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚地址连续的块。最后需要说明的是,vmalloc()可能睡眠,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。释放时必须使用vfree().
分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,常常会用到一个空间链表。它就相当于对象高速缓存以便快速存储频繁使用的对象类型。在内核中,空闲链表面临的主要问题之一是不能全局控制。当可用内存变得紧张的时候,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放一些内存来。实际上,内核根本不知道有这样的空闲离岸边。为了弥补这一缺陷,也为了是代码更加稳固,linux内核提供了slab层(也就是所谓的slab分类器),slab分类器扮演了通用数据结构缓存层的角色。slab分配器试图在如下几个原则中寻求一种平衡:
1.频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。 2.频繁分配和回收必然会导致内存碎片。为了避免这种情况,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,不会导致碎片。 3.回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。 4.如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。 5.对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。 |
slab层把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在slab层上,使用了一组通用高速缓存。这些缓存又被分为slabs,slab由一个或多个物理上连续的页组成,一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构,每个slab处于三种状态之一:满,部分满,空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。下图给出高速缓存,slab及对象之间的关系:
上图中的每个cache由kmem_cache_s结构表示,这个结构包含三个链表slabs_full,slab_partial和slabs_empty,均存放在kmem_list3结构内,这些链表包含高速缓存中的所有slab,slab描述符structslab:
3 |
unsigned long colouroff; |
slab描述符要么在slab之外另行分配,要么就在slab自身最开始的地方。如果slab很小或者slab内核有足够的空间容纳slab描述符,那么描述符就存放在slab里面.slab分配器创建新的slab是通过__get_free_pages()低级内存分配器进行的:
1 |
static inline void *kmem_getpages(kmem_cache_t *cachep, unsigned long flags) |
4 |
flags|= cachep->gfpflags; |
5 |
addr= ( void *)__get_free_pages(flags,cachep->gfporder); |
上面的是一个描述原理的简化版。接着,调用kmem_freepages()释放内存,而对给定的高速缓存页,kmem_freepages()最终调用的是free_pages().当然,slab层的关键就是避免频繁分配和释放页。由此可知,slab页只有当给定的高速缓存中既没有部分满也没有空的slab时候才会调用页分配函数。而只有在下列情况下才会调用释放函数:当可用内存变得紧缺时,系统试图释放出更多内存以供使用,或者当高速缓存显式地被销毁时。slab层的管理是在每个高速缓存的基础上,通过提供个整个内核一个简单的接口来完成的。通过接口就可以创建和销毁新的高速缓存,并在高速缓存内分配和释放对象。高速缓存及slab的复杂管理完全通过slab层的内部机制来处理。当创建一个高速缓存后,slab层所起的作用就像一个专用的分配器,可以为具体的对象类型进行分配。一个新的高速缓存是通过一下接口进行创建的:
1 |
kmem_cache_t *kmem_cache_create( const char *name, size_t size, size_t align,unsigned long flags, |
1 |
void (*ctor)( void *, kmem_cache_t *,unsigned long ), |
2 |
void (*dtor)( void *, kmem_cache_t *,unsigned long )); |
1 |
<FONTface=微软雅黑> 有关这个函数的说明,我就省略了,需要的网上一大堆。这个函数成功时会返回一个执行所创建高速缓存的指针,否则,返回空。这个函数由于会睡眠,因此不能在中断上下文中使用。要销毁一个高速缓存,调用: int kmem_cache_destroy(kmem_cache_t*cachep),同样,也是不能在中断上下文中使用。调用该函数之前必须确保存在以下两个条件:</FONT> |
1.高速缓存中的所有slab都必须为空。 2.在调用kmem_cache_destory()期间不再访问这个高速缓存,调用者必须确保这种同步。 |
创建了高速缓存以后,就可以通过下列函数从中获取对象:void * kmem_cache_alloc(kmem_cache_t*cachep, intflags)。该函数从高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给__get_free_pages().最后,释放一个对象,并把它返回给原来的slab,可以使用下面的函数:
1 |
void kmem_cache_free(kmem_cache_t*cachep, void *objp) |
这样就能把cachep中的对象objp标记为空闲了,关于slab分配器的使用实例,参考资料上有,我就不说了。相比较以前的用户空间栈而言,内核栈是非常小的。每个进程都有自己的内核栈进程在内核执行期间的整个调用链必须放在自己的内核栈上。中断处理程序也使用被它们打断的进程的堆栈。这就意味着,在最恶劣的情况下,8kB的内核栈可能会由多个函数的嵌套调用链和几个中断处理程序来共享。显然,深度的嵌套会导致溢出。
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。一旦这些页被分配,就必须映射到内核的逻辑地址空间上。要映射一个给定的page结构到内核地址空间,可以使用void*kmap(struct page *page)这个函数在高端内存或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址,如果页位于高端内存,则会建立一个永久映射,在返回地址。这个函数可以睡眠,所以kmap()只能用在进程上下文中。当不再需要内存映射的时候,就用下列函数进行解除映射:
1 |
void kunmem( struct page*page) |
当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时睡眠(也就是原子睡眠)。只要有一组保留的永久映射,它们就可以临时持有新创建的一个映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方。建立临时映射:void*kmap_atomic(struct page *page,enum km_typetype).参数type是下列枚举类型之一,描述了临时映射的目的,如下:
这个函数不会阻塞,它也禁止内核抢占,通过函数void *kunmap_atomic(void *kvaddr,enum km_typetype).这个函数还是不会映射。
最后,我们总结一下,说说分配函数的选择吧,总结如下:
1.如果需要连续的物理页,就可以使用某个低级页分配器或kmalloc(). 2.如果想从高端内存进行分配,使用alloc_pages(). 3.如果不需要物理上连续的页,而仅仅是虚拟地址上连续的页,那么就是用vmalloc 4.如果要创建和销毁很多大的数据结构,那么考虑建立slab高速缓存。 |
好了,有关内存管理基本概述完毕。可以选择几本书籍在做补充。