八、内存管理(一)

RAM的某些部分永久的分配给了内核,用来存放内核代码以及静态数据结构。RAM的其余部分被称为动态内存(dynamic memory),这不仅是进程所需,也是内核本身所需。

本章主要描述内核如何给自己分配动态内存。分为页框管理、内存区管理和非连续内存区管理三部分。页框管理和内存区管理针对连续物理内存区分配内存的技术,非连续内存区管理介绍了如何处理非连续内存分配的问题。

1、页框管理

1.1 页描述符

 

RAM由多个页框组成,内核必须了解这些页框的状态,比如页框是否空闲,页框属于进程还是内核等等。所以内核使用类型为page的描述符保存页框的描述信息。因为描述符本身长度为32字节,所以内核需要使用一些页框来专门存放page描述符数据。因为1个描述符对应1个页框,所有的page都存放在mem_map数组中。mem_map数组所需要的空间即为所所以共需要(页框数量X32字节即page描述符大小)的内存容量。所以mem_map占内存百分比取决于页框本身的大小,4KB的页框,百分比=32/4KB;8KB,则 32/8KB;

virt_to_page(addr)宏产生线性地址addr对应的page地址;pfn_to_page(pfn)宏产生与页框号pfn对应的page地址。

struct Page {
  unsigned long flags;//一组标志;也对页框所在的管理区进行排号
  atomic_t   _count;//页框的引用计数器
  atomic_t   _mapcount; //页框中的页表项数目
  unsigned long private;//可用于正在使用页的内核成分(例如,在缓冲页的情况下它是一个缓冲器头指针);如果页空闲,则该字段由伙伴关系使用.
  struct address_space *mapping;//当页被插入页高速缓存中使用,或当页属于匿名区时使用
  unsigned long index;//作为不同的含义被几种内核成分使用
  struct  list_head lru;//包含页最近最少使用双向链表的指针
}

page_count()函数返回_count+1的值,_count=-1时页框空闲。page_count()=0时,页框空闲。

1.2 非一致内存访问(NUMA)

UMA:内存单元无论处于何处,CPU无论处于何处,CPU对内存单元的访问都需要相同的时间。

NUMA:给定CPU对不同内存单元的访问所消耗时间可能不一样。系统的物理内存被划分为几个节点(node)。在一个单独的节点内,任意给定CPU访问页面所需时间相同。每个节点中的物理内存又可以分为几个内存管理区(Zone)。每个节点都有一个类型为pg_data_t的描述符,所有节点描述符存放在一个单向链表中,第一个元素由pgdat_list变量指向。Linux使用一个单独的节点包含了系统中的所有物理内存。因此pgdat_list变量指向的链表是由一个元素组成的。

1.3 内存管理区

理想的计算机体系结构中,一个页框就是一个内存存储单元,任何种类的数据页都可以存放在任何页框中,没有什么限制。但是实际的计算机体系结构有硬件制约,限制了页框可以使用的方式。例如80x86体系结构的有如下硬件约束:ISA总线的直接内存存取(DMA)处理器只能对RAM的前16MB寻址;在具有大容量RAM的现代32位计算机中,因为线性地址空间较小,CPU不能直接访问所有的物理内存。

为了应对这两种限制,linux2.6把每个内存节点的物理内存划分为3个管理区ZONE_DMA、ZONE_NORMAL(>16,<896)、ZONE_HIGHMEM(>896m).由于64位体系结构上,寻址能力是2^64,所以CPU可以直接访问较大内存,ZONE_HIGHMEM为空。问题:为何32位的寻址能力是2^32=4G,ZONE_NORMAL却设计为小于896M而不是小于4G?

通过page的flags字段的高几位可以找到page对应的内存节点以及内存节点下的内存区。page_zone(struct page *page)返回page相应管理区描述符地址。

当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。内核使用zonelist数据结构来代表管理区指针数组。

1.4 保留的页框池

内核为原子内存分配请求保留了一个页框池,只在内存不足时使用。保留的内存数量以KB为单位存放在min_free_kbytes变量中。它的初始值在内核初始化时设置,并取决于直接映射到内核线性地址空间第4个GB的物理内存的数量。ZONE_DMA和ZONE_NORMAL内存管理区按各自内存比比例将一定数量的页框贡献给保留内存。管理区描述符的pages_min字段存储了管理区内保留页框的数目。

1.5 分区页框分配器

分区页框分配器处理对连续页框组的内存分配请求。管理区分配器接收动态内存分配与释放的请求。在请求分配的情况下,管理区分配器搜索一个能满足所请求的一组连续页框内存的管理区。在每个管理区内,页框被名为“伙伴系统”的部分来处理。为了达到更好的系统性能,一小部分页框保留在高速缓存中用于快速满足对单个页框的分配请求。

1.5.1 请求和释放页框

请求:

alloc_pages(gfp_mask,order) ;返回第一个所分配页框描述符的地址

alloc_page(gfp_mask);

__get_free_pages(gfp_mask,order);返回第一个所分配页的线性地址

__get_free_page(gfp_mask);

get_zerod_page(gfp_mask);返回所获取页框的线性地址

__get_dma_pages(gfp_mask,order);

释放:

__free_pages(page,order);先检查page指向的页描述符;如果该页框未被保留(PG_reserved标志为0),就把描述符count字段-1.如果count为0,就假定页框不再被使用,这种情况下释放页框。

free_pages(addr,order);

__free_page(page);

free_page(addr);

1.6 高端内存页框的内核映射

896M边界以上的页框并不映射在内核线性地址空间的第4个GB,因此,内核不能直接访问他们。这就意味着,返回所分配页框线性地址的方法不适用于高端内存。

linux采用如下方法:

①、高端内存页框的分配职能通过alloc_pages()函数和它的快捷函数alloc_page()。

②、没有线性地址的高端内存中的页框不能被内核访问。因此,内核线性地址空间最后128M中的一部分专门用于映射高端内存页框。当然,这种映射是暂时的,否则只有128M的高端内存可以被访问。通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。

内核采用三种不同的机制映射高端内存:永久映射、临时映射、以及非连续内存分配。

建立永久映射可能阻塞当前进程;这发生在空闲页表项不存在时。临时映射绝不会要求阻塞当前进程,但是只有很少的临时内核映射可以同时建立起来。

这些技术中没有一种可以确保对整个RAM同时进行寻址。

1.6.1 永久内核映射

永久内核映射允许内核建立高端页框到内核地址空间的长期映射。他们使用主内核页表中一个专门的页表,其地址存放在pkmap_page_table变量中。页表中的表项数由LAST_PKMAP宏产生。页表包含512或1024项。因此,内核一次最多访问2MB或4MB的高端内存。

该页表映射的线性地址从PKMAP_BASE开始。pkmap_count数组包含LAST_PKMAP个计数器,页表中每一项对应一个。

内核使用了page_address_htable散列表存放page_address_map数据结构,该数据结构保存了高端内存中的每一个页框与线性地址的映射关系:

struct page_address_map {
  struct page *page; //指向页描述符
  void *virtual; //页框的线性地址
  struct list_head list; //指向下一个该结构
};

page_address(struct page)函数返回页框对应的线性地址,如果页框在高端内存中并且没有被映射,则返回NULL。该函数根据page是否指向高端内存页框(PG_highmem标志是否为0)分为两种情况,具体实现可参看如下链接:http://hi.baidu.com/flikecn/item/a45313aee2c461ab29ce9d8cd。下图为高端内存永久映射数据结构图。

kmap()函数建立永久映射。

void *kmap(struct page *page){

  if(!PageHighMem(page))

    return page_address(page);

  return kmap_high(page);

}

void * kmap_high(struct page *page){

  unsigned long vaddr;

  spin_lock(&kmap_lock);

  vaddr = (unsigned long)page_address(page);

  if(!vaddr)

    vaddr = map_new_virtual(page);

  pkmap_count[(vaddr-PKMAP_BASE)>>PAGE_SHIFT]++;

  spin_unlock(&kmap_lock);

  return (void*)vaddr;

}

void * map_new_virtual(struct page *page){

  for(;;){

    int count;

    DECLARE_WAITQUEUE(wait,current);

    for(count=LAST_PKMAP;count>0;--count){

      last_pkmap_nr = (last_pkmap_nr+1)&(LAST_PKMAP-1);//保证last_pkmap_nr的值永远小于LAST_PKMAP-1;

      if(!last_pkmap_nr){

         flush_all_zero_pkmaps();

         count = LAST_PKMAP;

      }

      if(!pkmap_count[last_pkmap_nr]){

          unsigned long vaddr = PKMAP_BASE + last_pkmap_nr<<PAGE_SHIFT;

          set_pte(&(pkmap_page_table[last_pkmap_nr]),mk_pte(page,_ _pgprot(0x63)));//把page对应页框的物理地址插入到pkmap_page_table下标           为last_pkmap_nr的页表项中,建立映射关系。

          pkmap_count[last_pkmap_nr] = 1;

          set_page_address(page,(void *)vaddr);//在page_address_htable散列表中加入page和其线性地址。

          return vaddr;

      }

    }

    current->state = TASK_UNINTERUPTIBLE;

    add_wait_queue(&pkmap_map_wait,&wait);

    spin_unlock(&kmap_lock);

    schedule();

    remove_wait_queue(&pkmap_map_wait,&wait);

    spin_lock(&kmap_lock);

    if(page_address(page))

      return (unsigned long)page_address(page);

}

kunmap()函数撤销先前由kmap()建立的永久内核映射。如果页确实在高端内存中,则调用kunmap_high()函数。

void kunmap_high(struct page *page){

  spin_lock(&kmap_lock);

  if((--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT])==1)

    if(waitqueue_active(&pkmap_map_wait))

      wake_up(&pkmap_map_wait);

  spin_unlock(&kmap_lock);

}

1.6.2 临时内核映射

每个CPU都有它自己的包含13个窗口的集合,他们用enum km_type数据结构表示。在km_type中的每个符号(除最后一个)都是固定映射的线性地址的一个下标。enum fixed_addresses数据结构包含符号FIX_KMAP_BEGIN和FIX_KMAP_END;系统中每个CPU都有KM_TYPE_NR个固定映射的线性地址。

kmap_atomic(struct page *page,enmu km_type type){

  enmu fixed_address idx;

  unsigned long vaddr;

  current_thread_info()->preempt_count++;

  if(!PageHighMem(page))

    return page_address(page);

  idx = type + KM_TYPE_NR*smp_processor_id();

  vaddr = fix_to_virt(FIX_KMAP_BEGIN+idx);

  set_pte(kmap_pte-idx,mk_pte(page,0x63));//内核用fix_to_virt(FIX_KMAP_BEGIN)线性地址对应的页表项的地址初始化kmap_pte

  _ _flush_tlb_single(vaddr);

  return (void *)vaddr;

}

1.7 伙伴系统算法

外碎片:频繁的请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了虚度小块的空闲页框。由此带来的问题时,即使有足够的空闲页框可以满足请求,但是要分配一个大块的连续页框可能就无法满足。

从本质上讲,避免外碎片的方法有两种:

①、利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。

②、开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而分割大的空闲块。

Linux采用伙伴系统(buddy system)算法来解决外碎片问题。把所有的空闲页框分为11个块链表,每个块链表包含大小为1,2,4…… 1024个连续的页框。对1024个页框的最大请求对应着4MB大小的连续RAM块。每个块的第一个页框物理地址是该块大小的整数倍。

1.7.1 数据结构

linux2.6为每个内存管理区使用不同的伙伴管理系统。因此,在80x86结构中,有三种伙伴系统:第一种处理适合ISA DMA的页框,第二种处理“常规”页框,第三种处理高端内存页框。每个管理区都关系到mem_map元素的子集。子集中第一个元素和元素个数分别由管理区描述符的zone_mem_map和size字段指定。包含有11个元素、元素类型为free_area的一个数组,每个元素对应一种块大小,该数组存放在管理区描述符的free_area字段中。

free_area[k]的free_list字段是双向循环链表的头,链表集中了大小为2^k的空闲块对应的页描述符。更精确的说,该链表包含每个空闲页框块(大小为2^k)的起始页框的页描述符;指向链表中相邻元素的指针存放在页描述符的lru字段中。free_area[k]同样包含nr_free,指定了2^k大小的空闲块的个数。2^k空闲块的第一个页描述符的private字段存放了块的阶数order。正是由于这个字段,当块被释放时,内核可以确定这个块的伙伴是否也空闲,如果是的话,它可以把两个块结合成2^k+1页的单一块。结构图如下:

1.7.2 分配块

_ _rmqueue(struct zone_t *zone,int order){

  struct free_area *area;

  unsigned int current_order;

  for(current_order=order;current_order<11;++current_order){

      area = zone->free_area +current_order;

      if(!list_empty(&area->free_list))

         goto block_found;

  }

  return NULL;

  block_found:

    page = list_entry(area->free_list.next,struct page,lru);

    list_del(&page->lru);

    ClearPagePrivate(page);

    page->private = 0;

    area->nr_free--;

    zone->free_pages -=1UL<<order;

    size = 1<<curr_order;

    while(curr_order>order){

      area--;

      curr_order--;

      size>>=1;

      buddy = page +size;

      list_add(&buddy->lru,&area->free_list);

      area->nr_free++;

      buddy->private = current_order;

      SetPagePrivate(buddy);

    }

    return page;

}

1.7.3 释放块

_ _free_pages_bulk(struct page *page,struct zone_t *zone,int order){

  struct page *base = zone->zone_mem_map;

  unsigned long buddy_idx,page_idx = page - base;

  struct page *buddy,*coalesced;

  int order_size = 1 << order;

  zone->free_pages += order_size;

  while(order<10){

    buddy_idx = page_idx ^ (1<<order);

    buddy = base + buddy_idx;

    if(!page_is_buddy(buddy,order))

      break;

    list_del(&buddy->lru);

    zone->free_area[order].nr_free--;

    ClearPagePrivate(buddy);

    buddy->private = 0;

    page_idx &= buddy_idx;//这一句实在不明所以,希望高手解读

    order++;

  }

  coalesced = base + page_idx;

  coalesced->private = order;

  SetPagePrivate(coalesced);

  list_add(&coalesed->lru,&zone->free_area[order].free_list);

  zone->free_area[order].nr_free++;

}

int page_is_buddy(struct page *page,int order){

  if(PagePrivate(page)&&page->private == order&&!PageReserved(page)&&page_count(page)==0)

    return 1;

  return 0;

}

1.8 每CPU页框高速缓存

内核经常请求和释放单个页框。为了提升系统性能,每个内存管理区定义了一个“每CPU”页框高速缓存。所有每CPU高速缓存包含一些预先分配的页框,他们被用于满足本地CPU发出的单一内存请求。

每个内存管理区和每个CPU可以使用两个页框告诉缓存:热高速缓存,它存放的页框中包含的内容很可能就在CPU硬件高速缓存中;冷高速缓存。如果内核或用户态进程在刚刚分配到页框后就立刻向页框写,那么从热高速缓存中获得页框就对系统性能有利。实际上,每次对页框存储单元的访问都将会导致从另一个页框中给硬件高速缓存“窃取”一行——除非硬件高速缓存包含一行:它映射到刚被访问的“热”页框单元。(CPU硬件高速缓存中存在有最近使用过的页框单元,我们每次对页框存储单元的访问,如果页框存储单元不在硬件高速缓存中,那么硬件高速缓存会重新添加我们访问的页框单元,这样可能会替换之前硬件高速缓存中的内容)。

如果页框将要被DMA操作填充,那么从冷高速缓存中获得页框是有利的。在这种情况下,不会涉及到CPU,并且硬件高速缓存的行不会修改。从冷高速缓存中获得页框为其他类型的内存分配保存了热页框储备。

数据结构图:

如果每CPU页框高速缓存中,页框个数低于low,内核通过从伙伴系统中分配batch个单一页框来补充对应的高速缓存;如果页框个数高于high,内核从高速缓存中释放batch个页框到伙伴系统中。值batch,low,high本质上取决于内存管理区中包含的页框个数。

1.8.1 通过每CPU页框高速缓存分配页框

buffered_rmqueue()函数在指定的内存管理区中分配页框。它使用每CPU页框高速缓存来处理单一页框请求。参数为内存管理去描述符地址,请求分配的内存大小对数order,以及分配标志gfp_flags。该函数本质上等于以下伪代码:

void* buffered_rmqueue(struct zone_t *zone,int order, gfp_flags flag){

  struct page *page;

  if(!order){

    page = _ _rmqueue(zone,order);

  }else{

    if(check(zone,flag)/*判断管理区的页框高速缓存是否需要补充*/){

        while(batch--)

          list_add(per_cpu_pages->list,_ _rmqueue(zone,1));//将已经分配的页框描述符插入到链表

        per_cpu_pages->count+=batch;

    }

    if(per_cpu_pages->count){

      per_cpu_pages->count--;

      page = list_entry(per_cpu_pages->list,struct page,lru);

    } 

   }

   page->private = 0;

   page->count = 1;

   if(_ _GPF_ZERO) page_zero(page);//将被分配的内存区填0;

  return page;

}

1.8.2 释放页框到每CPU页框高速缓存

free_host_page()

free_code_page()

free_hot_code_page():

①、从page->flags字段获取包含该页框的内存管理区描述符地址

②、获取有code标志选择的管理区高速缓存的per_cpu_pages描述符的地址

③、检查高速缓存是否应该被清空:如果count>high:free_pages_bluk()。

④、把释放的页框添加到高速缓存链表中,并增加count字段

 

1.9 管理区分配器

管理区分配器是内核页框分配器的前端。必须满足几个目标:

①、应当保护保留的页框池(原子分配)

②、当内存不足且允许阻塞当前进程时,它应当触发页框回收算法;一旦某些页框被释放,管理区分配器将再次尝试分配。

③、尽可能保存ZONE_DMA内存管理区。

1.9.1 分配页框

__alloc_pages(gfp_mask gfp,int order,zonelist list);

__alloc_pages()函数扫描包含在zonelist数据结构中的每个内存管理区。实现代码如下:

for(i=0;(z=zonelist->zone[i])!=NULL;i++){

  if(zone_watermark_ok(z,order,...)){

    page = buffered_rmqueue(z,order,gfp_mask);

    if(page)

      return page;

  }

}

zone_watermark_ok()函数接收几个参数,他们决定内存管理区中空闲页框的个数的阀值min。如果满足如下条件,函数返回1:

①、除了被分配的页框外,在内存管理区中至少还有min个空闲页框,不包括保留页框

②、除了被分配的页框外,这里在order至少为k的块中起码还有min/2^k个空闲页框。

1.9.2 释放一组页框

__free_pages(struct page *page,int order)函数:

①、检查page是否属于动态内存(PG_reserved标志为0),不是则无法释放

②、减少page->count值,如果count>=0,停止

③、如果order=0,使用free_hot_page()释放到内存管理区的每CPU热高速缓存

④、如果order>0,那么将页框加入到本地链表中,并调用free_pages_bulk()函数把他们释放到适当的内存管理区的伙伴系统中。

posted @ 2013-04-22 22:12  shuying1234  阅读(433)  评论(0编辑  收藏  举报