向下之旅(十五):内存管理(一)

  

  内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是,内存管理单元(MMU,管理内存并把虚地址转换为物理地址的硬件)通常以页为单位进行处理。正因如此,MMU以页大小为单位来管理系统中的页表(这也是页表名的由来)。从虚拟内存的角度来看,页就是最小单位。体系结构不同,支持的页大小也不尽相同。大多32位体系结构支持4KB的页,64位体系结构一般会支持8KB的页。这意味着,在支持4KB也大小并有1GB物理内存的机器上物理内存会被划分为262144个页。

  内核用 struct page 结构表示系统中的每个物理页。结构如下:

  

  flag域用来存放页的状态,这些状态包括页是不是脏的,是不是被锁定在内存中等等。flag的每一位单独表示一种状态,所以它至少可以同时表示32中不同的状态。

  _count域存放页的引用计数——也就是这一页被引用了多少次。当计数值变0时,就说明当前内核并没有引用这一页,在新的分配中就可以使用它。

  virtual域是页的虚拟地址。通常,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久的映射到内核地址空间上。在这种情况下,这个值为NULL,需要的时候,必须动态的映射这些页。

  page 结构与物理页相关,并非与虚拟页相关。内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲(也就是页有没有被分配),若被分配,内核还要知道拥有者是用户空间进程或是动态分配的内核数据或是静态的内核代码,或页高速缓存等等。

  系统中的每个物理页都要分配这样的结构体,若struct pape 占40字节内存,假定系统的物理页为4KB大小,系统有128M物理内存,那么所有的page结构消耗的内存只不过是1MB多一点。

  

  由于硬件的限制,一些页位于内存中特定的物理地址上,不能将其用于特定的任务,由于存在这种限制,所以内核把页划分为不同的区。内核使用区对具有相似特性的页进行分组。由硬件缺陷引起的内存寻址问题:

  1.一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。

  2.一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多,这样,就有一些内存不能永久的映射到内核空间上。

  所以Linux使用了三种区:

  ZONE_DMA——这个区包含的页能用来执行DMA操作。

  ZONE_NORMAL——这个区包含的都是能正常映射的页。

  ZONE_HIGHMEM——这个区包含”高端内存“,其中的页不能永久的映射到内核地址空间。

  区的实际使用和分布是与体系结构相关的。  

  其中x86上每个区的分布情况:

  

  例如,ZONE_DMA内存池让内核有能力为DMA分配所需内存,如果需要这样的内存,那么,内核就可以从ZONE_DMA中按照请求的数目取出页。当一般用途的内存不足时,也可从其他两区获得内存。

  每个区用 struct zone 表示,结构如下:

  

  系统中只有三个区,因此也只有三个这样的结构。

  lock域是一个自旋锁,它防止该结构被并发访问,注意,这个域只保护结构,而不保护驻留咋这个区中的所有页。没有特定的锁来保护单个页,但是,部分内核可以锁住在页中驻留的数据。

  free_pages域是这个区中空闲页的个数,内核尽可能的保证(通过交换达到目的)有page_min个空闲也可用。

  name域是一个以NULL结束的字符串。表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page_alloc.c中。三个区的名字分别为"DMA","Normal","HighMem"

  获得页

  我们通过内核实现的接口在内核中分配和释放内存。内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所以这些接口都以页为单位分配内存,最核心的函数是:

  struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)

  该函数分配2的order次方(即1<<order)个连续的物理页,并返回一个指针,指向第一个页的page 结构体,如果出错,就返回NULL。使用下面的函数将给定的页转换成它的逻辑地址:

  void *page_address(struct page *page)

  该函数返回一个指针,指向给定物理页当前所在的逻辑地址。如果你无须用到struct page,你可以调用:

  unsigned long_get_free_pages(unsigned int gfp_mask, unsigned int order)

  这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,因此其他页也会紧随其后。

  如果只需一页,可用下面两个封装好的函数:

  struct page * alloc_page(unsigned int gfq_mask)

  unsigned long_get_free_page(unsigned int gfp_mask)

  俩函数与其兄弟函数工作方式相同,只不过传递给order的值为0(2^0 = 1页)。

  如果你想让返回的页的内容全为0,则使用下面的函数:

  unsigned long get_zeroed_page(unsigned int gfp_mask)

  该函数与_get_free_page()工作方式相同,只不过把分配好的页都填充了0。这个函数在分配给用户空间页上非常有用,一般分配好的页中应该包含的都是随机产生的垃圾信息,但其实这些信息可能并不是完全随机的——很可能是一些敏感数据,所以在用户空间分配的页返回时,必须保证数据都被填充0,或者其他清理工作。保证系统的安全。以下为所有底层的页分配方法的列表:

  

  释放页

  通过下面一族函数释放掉页:

  void _free_pages(struct page *page , unsigned int order)

  void free_pages(unsigned long addr, unsigned int order)

  void free_page(unsigned long addr)

  释放页时需谨慎,只能释放属于你的页,传了错误的struct page 或地址,用了错误的order值,这些都可能导致系统崩溃。

  调用_get_free_pages()之后要注意进行错误检查,内核分配可能失败,因此你的代码必须进行检查并做相应的处理,这意味在此之前,你所做的所有工作可能前功尽弃,甚至还需要回滚到原来的状态。

  kmalloc()

  kmalloc()函数可以获得以字节为单位的一块内核内存。

  void *kmalloc(size_t size, int flags)

  这个函数返回一个指向内存块的指针,至少为 size大小,出错时会返回NULL,所以在调用kmalloc()之后,必须检查返回的是不是NULL,如果是,要适当的处理错误。

  gfp_mask标志

  不管是低级页分配函数,还是kmalloc()中,都用到了分配器标志,这些标志可分为三类:行为修饰符,区修饰符及类型。行为修饰符表示内核应当如何分配所需内存,去修饰符标明内核应该从哪个区获取内存,类型标志指定所需的行为和区描述符以完成特殊类型的处理。

  行为修饰符

  区修饰符

  不能给_get_free_pages()或kmalloc()指定_GFP_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟地址空间,因此,可能根本就没有逻辑地址。只有alloc_pages()才能分配高内存。实际上,你的分配在大多数情况下都不必指定修饰符,ZONE_NORMAL就足矣。

  类型标志

  标志的选择:

  kfree()

  通过kmalloc()分配出来的内存块,使用  void kfree( const void *ptr )函数来释放。如果想要释放的内存不是由kmalloc()分配的,或是想要释放的内存已经被释放,调用这个函数会产生严重的后果。调用 kfree(NULL) 是安全的。

  一个在中断处理程序中分配内存的例子:中断处理程序想分配一个缓冲区来保存输入的数据,BUF_SIZE预定义为以字节为单位的缓冲区长度,它应该是大于两个字节的。

  char *buf;

  buf = kmalloc(BUF_SIZE, GFP_ATOMIC);

  if(!buf)

    /* 内存分配错误 */

  若不再需要这个内存,需要释放:

  kfree(buf);

  vmalloc()

  vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址无需连续,这也是用户空间上malloc()函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是这并不保证它们在物理RAM中也连续。kmalloc()函数确保页在物理地址上是连续的(虚拟地址上自然也是连续的)。vmalloc()函数只确保页在虚拟地址空间内是连续的,它通过分配非连续的物理内存块,在“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。

  一般只有硬件设备需要得到物理地址连续的内存,在很多体系结构上,硬件设备存在于内存管理单元之外,它根本不理解什么是虚拟地址。而仅供软件使用的内存块就可以使用只有虚拟地址连续的内存块。但是很多内核代码都用kmalloc()而不是vmalloc()来获取内存,这主要是出于性能考虑。vmalloc()函数还要专门建立页表项并一个一个进行映射(以为物理地址不连续),只有当需要较大的内存时,才会使用vmalloc()来获取内存。

  通过void * vmalloc(unsigned long size)函数来分配内存页,该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size,在发生错误时,函数返回NULL,函数可能睡眠,所以不能在中断上下文中调用,也不能从其他不允许阻塞的情况下使用。

  通过使用 void vfree(void *addr)函数来释放内存块。addr 是由vmalloc()分配的内存块地址。这个函数也可以睡眠,所以不能在中断上下文中调用,它没有返回值。使用如下:

  char *buf;

  buf = vmalloc(16*PAGE_SIZE);  /* 获得16页 */

  if(!buf)

    /* 错误!不能分配内存 */

  在分配内存后,需要进行释放:

  vfree(buf);

  slab层

  分配和释放数据结构是所有内核中最普遍的操作之一,为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表,该空闲链表包含有可供使用的、已经分配好的数据结构块,当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,在把数据放进去。当不需要这个数据结构的实例时,在将它放回空闲链表,而不是释放它。于是Linux内核提供了slab层(也就是所谓的slab分配器)。扮演了通用数据结构缓存层的角色。

  设计原则:

  1.频繁实用的数据结构也会频繁的分配和释放,因此应当缓存它们。

  2.频繁分配和回收必然会导致内存碎片(难以找到大块连续的可用内存)。为了避免这种假象,空闲链表的缓存会连续的存放。因为已释放的数据结构又会放回空闲链表,因此不会导致碎片。

  3.回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。

  4.如果分配器知道对象大小,页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。

  5.如果让部分缓存专属于单个处理器(对系统上的每个处理器独立而唯一),那么,分配和释放就可以在不加SMP锁的情况下进行。

  6.如果分配器是与NUMA相关的,它就可以从相同的内存节点为请求者进行分配。

  7.对存放的对象进行着色,以防止多个对象映射相同的高速缓存行

  slab层把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象。每种对象类型对应一个高速缓存。例如一个高速缓存用于存放进程描述符(task_struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。kmalloc()接口建立在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:

  slab描述符要么在slab之外另行分配,要么就放在slab自身最开始的地方。如果slab很小,或者slab内部有足够的空间容纳slab描述符,那么描述符就放在slab里面。

  slab分配器可以创建新的slab,通过函数 void *kem_getpages(kmem_cache *cachep, int flags, int nodeid) 里面调用_get_free_papges()低级内核页分配器进行的。通过 keme_freepages释放内存。

  slab层只有当给定的高速缓存中既没有部分满也没有空的slab时才会调用页分配函数。而只有在下列情况下才会调用释放函数:当可用内存变的紧张时,系统试图释放出更多内存以供使用,或者当高速缓存显示的被销毁时。

  slab分配器的接口

  一个高速缓存是通过以下函数创建的:

  第一个参数是一个字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素大小。第三个参数是高速缓存内第一个对象的偏移。这用来确保在页内进行特定的对齐。通常情况下,0就可以满足要求,也就是标准对齐。flags参数是可选的设置项,用来控制高速缓存的行为。它可以为0,表示没有特殊的行为,或者与以下标志中的一个或多个进行“或”运算。

  SLAB_NO_REAP:这个标志命令slab层不要在内存短缺时自动回收对象。

  SLAB_HWCACHE_ALIGN:这个标志命令slab层把一个slab内的所有对象按高速缓存行对齐。可以防止“错误的共享”(两个或多个对象尽管位于不同的内存地址,但映射到相同的高速缓存行)。可以提高性能,但是以增加内存踪迹为代价。

  SLAB_MUST_HWCACHE_ALIGN:这个标志强制slab层缓存对齐对象。通常这个标志是不需要的,上一个标志就足够了。

  SLAB_POSION:这个标志使slab层用已知的值(a5a5a5a5)填充slab。这就是所谓的“中毒”,有利于对未初始化内存的访问。

  SLAB_RED_ZONE:这个标志导致slab层在已分配的内存周围插入“红色警戒区”以探测缓冲越界。

  SLAB_PANIC:这个标志当分配失败时提醒slab层。这在要求分配只能成功的时候非常有用。

  SLAB_CACHE_DMA:这个标志命令slab层使用可以执行DMA的内存给每个slab分配空间,只有在分配的对象用于DMA,而且必须驻留在ZONE_DMA区时才需要这个标志。

  最后两个参数ctor和dtor分别是高速缓存的构造和析构函数。只有在新的页追加到高速缓存时,构造函数才被使用,只有从高速缓存中删除页时,析构函数才被使用。

  kmem_cache_create()在成功时返回一个指向创建高速缓存的指针,否则,返回NULL。这个函数不能在中断上下文中使用,它可能会睡眠。

  通过函数 int kmem_cache_destory(kmem_cache_t *cachep)来销毁一个高速缓存。调用该函数之前必须确保存在以下两个条件:

  1.高速缓存中的所有slab都必须为空。其实,不管哪个slab中只要还有一个对象被分配出去,并正在使用的话,不可能销毁这个高速缓存。

  2.在调用kmem_cahce_destory()期间(更不用说在调用之后了)不再访问这个高速缓存。调用者必须确保这种同步。

  该函数成功执行,返回0,否则,返回非0值。

  创建高速缓存之后,就可以通过下列函数从中获取对象:

  void *kmem_cache_alloc(kmem_cache_t *chchep, int flags)

  该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象。那么slab层必须通过kmem_getpages()获取新的页。flags的值传递给_get_free_pages()。

  最后释放一个对象,并把它返回给原先的slab,可以使用下面的函数:

  void kmem_cache_free(kmem_cache_t *cachep, void *objp)

  这样就能把cachep中的对象objp标记为空闲了。

  

  参考自:《Linux Kernel Development》.

posted on 2016-03-25 13:23  画家丶  阅读(241)  评论(0编辑  收藏  举报