分类:
2010-07-28 12:29:14
11.1 页
内核把物理页作为内存管理的基本单位。内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以 页(page)大小为单位来管理系统中的页表。从虚拟内存的角度来看,页就是最小单位。大多数32位的体系结构支持4KB的页。
内核用struct page结构表示系统中的每个物理页,该结构位于
struct page {
page_flags_t flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};
flag域用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等等。flag的每一位单独表示一种状态,它至少可以同时表示出32种不同
状态。这些标志定义在
必须要了解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页(这里的页我理解为虚拟页)的描述只是短暂的。即使页中所包含的数据 继续存在,但是由于交换等原因,它们可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。这种数据 结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
11.2 区(跳过先)
11.3 获得页
内核提供了一种请求内存的底层机制,并提供了对它进行访问的接口。所有这些接口都以页为单位分配内存,定义于
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
该函数分配2的order次方个连续的页,并返回一个指针,该指针指向第一个页的page结构体(前面已经提及,所有的page结构体只占用了一小部分内存)。如果出错,就返回NULL。可以用下面的函数把给定的页转换为它的逻辑地址(关于逻辑地址、物理地址和虚拟地址):
void *page_address(struct page *page)
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。如果无须用到struct page,可以调用:
unsigned long_get_free_pages(unsigned int gfp_mask, unsigned int order )
这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。
获得填充为0的页: unsigned long get_zeroed_page(unsigned int gfp_mask);如果分配的页是给用户空间的,这个函数就非常有用了。
当不再需要页时可以用下面的一族函数来释放它们:
void _free_pages(struct page *page, unsigned int order)
void free_page(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
11.4 kmalloc()
kmalloc()函数与用户空间的malloc()一族函数非常相似,只不过它多了一个flags参数。kmalloc()函数是一个简单的接口,它可
以获得以字节为单位的一块内核内存。对于大多数内核分配来说,kmalloc()接口用的更多。kmalloc()
在
void *kmalloc(size_t size, int flags)
这个函数返回一个指向内存块的指针,其内存块至少要有size大小。出错时,返回NULL。
11.4.1 gfp_mask标志
我们已经看到几个例子,发现不管是在页分配函数中,还是在kmalloc()中,都用到了分配器标志。这类标志可分为三类:行为修饰符、区修饰符及类型。 行为修饰符表示内核应当如何分配所需要的内存。在某些特定情况下,只能使用某些特定的方法分配内存。区修饰符表示从哪儿分配内存。类型标志组合了行为修饰 符和区修饰符,将各种可能的组合归纳为不同的类型。我们最常用的也是类型标志:
GFP_ATOMIC 分配是高优先级的,且不会睡眠,这个标志用在中断处理程序、下半部、持有自旋锁,以及其他不能睡眠的地方。
GFP_NOIO 这种分配可以阻塞,但不会启动磁盘I/O。这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,可能导致令人不愉快的递归。
GFP_NOFS 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但是不会启动文件系统操作。这个标志在你不能再启动另一个文件系统的操作时用在文件系统部分的代码中
GFP_KERNEL 这是一种常规分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。为了获得调用者所需要的内存,内核会尽力而为,这个标志应该是首选标志
GFP_USER 这是一种常见的分配方式,可能会阻塞。这个标志用于为用户空间进程分配内存时
GFP_HIGHUSER 这是从ZONE_HIGHMEM进行分配,可能会阻塞。这个标志用于为用户空间进程分配内存时。
GFP_DMA 这是从ZONE_DMA进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志,通常与以上的某个标志组合在一起使用。
11.4.2 kfree
kmalloc()的另一端是kfree(),kfree()声明于
void kfree(const void *ptr)
它释放由kmalloc()分配出来的内存块。如果想要释放的内存不是kmalloc()分配的,或者想要释放的内存早就被释放了,调用这个函数会导致严重的后果。
11.5 vmalloc()
vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址则无须连续。很多内核代码都用 kmalloc()来获取内存,而不是vmalloc(),这主要出于性能的考虑。后者仅在不得已时才会使用——一般是在为了获得大块内存时,例如,当模 块被动态的插入到内核中时,就把模块装载到由vmalloc()分配的内存上。
vmalloc()函数在
void* vmalloc(unsigned long size)
该函数返回一个指针,指向逻辑上连续的一块内存区,大小至少为size。
释放: void vfree(void *addr )
11.6 slab层
slab层,也就是slab分配器,它扮演了通用数据结构缓存层的角色。
slab层根据对象的不同划分为所谓的高速存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存。而每个高速缓存又被划分为 slab(这也是这个子系统的来由,但要注意,slab层和slab是两个完全不同的概念,不要在此处犯迷糊)。slab是一个由一个或多个物理上连续的 页(通常是一页)组成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于三种 状态之一:满、部分满或空。一个满的slab没有空闲的对象(slab中的所有对象都已被分配)。当内核中的某一部分需要一个新的对象时,先从部分满的 slab中进行分配,其次再从空的slab中分配。没有空的,就创建一个slab。
每个高速缓存都是用kmem_cache_s结构来表示。这个结构包含三个链表slabs_full、slabs_partial和 slabs_empty,均存放在kmem_list3结构内。这些链表包含高速缓存中的所有slab。slab描述符struct slab用来描述每个slab:
struct slab {
struct list_head list; /* 满、部分满或空链表 */
unsigned long colouroff; /* slab着色的偏移量 */
void *s_mem; /* 在slab中的第一个对象 */
unsigned int inuse; /* 已分配的对象数 */
kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */
};
slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器进行的。接着,会调用 kmem_freepages()释放内存,而对给定的高速缓存页,kmem_freepages()最终调用的是free_pages()。但是不要忘 了,slab层的关键就是避免频繁的分配和释放页。由此可知,slab层只有当给定的高速缓存中即没有部分满也没有空的slab时才会调用页分配函数。而 只有在下列情况下才调用释放函数:当可用内存变得紧缺时,系统试图释放出更多的内存以供使用,或者当高速缓存显式的被销毁时。
slab的管理是在每个高速缓存的基础上,通过提供给整个内核一个简单的接口来完成的。通过接口就可以创建和销毁新的高速缓存,并在高速缓存内分配和释放 对象。高速缓存及其包含的slab的复杂管理完全通过slab层的内部机制来处理。当创建一个高速缓存后,slab层所起的作用就像一个专用的分配器,可 以为具体的对象类型进行分配。
11.7 slab分配器的接口
一个新的高速缓存是通过以下函数创建:
kmem_cache_t *kmem_cache_creat(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void*, kmem_cache_t *, unsigned long),
void (* dtor)(void*, kmem_cache_t *, unsigned long);
第一个参数是一个字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素的大小。第三个参数是高速缓存中第一个对象的偏移。这用来确保在页内进行 特定的对齐。通常情况下为0,也就是标准对齐。flags参数是可选的设置项,可以为0,表示没有特殊行为。最后两个参数ctor和dtor分别是高速缓 存的构造和析构函数。实际上,在Linux内核的高速缓存中不使用析构函数或构造函数。可以全部赋予NULL。
销毁一个高速缓存: int kmem_cache_destroy(kmem_cache_t *cachep)
顾名思义,这样就可以销毁给定的高速缓存。这个函数通常在模块的注销代码中被调用,当然,这里指创建了自己的高速缓存的模块。调用该函数之前必须确保存在以下两个条件:
(1)高速缓存中的所有slab都必须为空。
(2)在调用kmem_cache_destroy()期间不再访问这个高速缓存。
创建高速缓存之后,就可以通过下面的函数从中获取对象:
void *kmem_cache_alloc(kmem_cache_t *cachep, int flags)
该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给__get_free_pages()。
释放一个对象,并把它返回给原来的slab。
void kmem_cache_free(kmem_cache_t *cachep, void *objp)
这样就能把cachep中的对象objp标记为空闲了。
11.8 在栈上的静态分配
在任意一个函数中,你都必须尽量节省栈资源。这并不难,也没有什么固定的方法,只需要在具体的函数中让所有局部变量所占的空间不要超过几百个字节。在栈上 进行大量的静态分配,比如分配大型数组和大型结构体,是很危险的。栈溢出悄无声息,但势必会引起严重的问题。当栈溢出时,多出的数据就会直接溢出来,覆盖 掉进邻堆栈末端的东西。首先面临考验的是thread_info结构。因此,进行动态分配是一种明智的选择,本章前面有关大内存块的分配就是采用这种方 式。
11.9 分配函数的选择
如果需连续的物理页,就可以使用某个低级页分配器或kmalloc()。这是内核中内存分配的常用方式,也是大多数情况下你自己应该使用的内存分配方式。 回忆一下,传递给这些函数的两个最常见的标志是GFP_ATOMIC和GFP_KERNEL。前者表示进行不睡眠的高优先级分配。这是中断处理程序和其他 不能睡眠的代码段的需要。对于可以睡眠的代码,比如没有持有自旋锁的进程上下文代码,则应该使用GFP_KERNEL获取所需要的内存。这个标志表示,有 必要的情况下分配时可以睡眠。
如果不需要物理上连续的页,而仅仅需要虚拟地址上连续的页,那么就使用vmalloc()。
如果要创建和销毁很多较大的数据结构,则因该考虑建立slab高速缓存。