骆驼空间站

简单就是美,不受任何商业的驱使,我们有自己的圈子

博客园 首页 新随笔 联系 订阅 管理
VM系统涉及的主要数据结构描述

  1. vmspace

  该结构用于描述一个进程的虚拟地址空间,其包含了平台无关性的vm_map结构和平台相关性的pmap结构,以及该进程内存使用的一些统计计量。

  2. vm_map

  该结构是描述与平台无关性的虚拟地址空间的最高层数据结构,其包含了一系列虚拟地址有效地址映射实体和这些映射的属性。

  3. vm_map_entry

  该结构描述了一段虚拟地址空间(start C end),以及该段地址空间代表的是一种VM对象、另一个地址映射还是一个地址子映射,及其相应的共享保护和继承等属性。

  4. vm_object

  该结构描述了一段虚拟地址空间的数据来源,它可以描述一个文件、一段为零的内存和一个设备等等。

  5. vm_page

  该结构描述了一页物理内存,是VM用于表述物理内存的低层数据结构。页尺寸是在系统启动时,由平台决定的。

  6. pagerops (vm_pager)

  该结构描述了VM对象的后台存储如何访问,在FreeBSD中,是通过pagerops结构描述函数指针,实现不同类型的对象的具体操作,在vm_object结构中,通过handle成员指定具体类型对象对应的数据结构,比如设备类型对应dev_t (cdev结构指针)。在一般OS描述中,采用vm_pager描述该目的的数据结构。

  本文集中讨论FreeBSD内核虚拟地址空间的管理,涉及到内核地址空间分配和内核地址空间动态分配。FreeBSD的内核空间总是被映射到每一个进程的地址空间的最高部分。和任何其它进程一样,内核也是通过包含一系列的vm_map_entry结构实体的vm_map结构来管理一段地址空间的使用。子映射是内核映射特有的,用于隔离、限制一段地址空间以提供给内核子系统使用,比如mbuf操作。本文主要讨论与平台无关性的内容,涉及到平台相关性时,以i386为例简要说明。

  1. SI_SUB_VM初始化

  在系统启动时,mi_startup()函数会调用SI_SUB_VM初始化与平台无关的VM系统,其定义是:“SYSINIT(vm_mem, SI_SUB_VM, SI_ORDER_FIRST, vm_mem_init, NULL)”。在vm_mem_init函数初始化之后,我们就只使用虚拟内存了,现在分析该函数的实现:

  vm_set_page_size();

  该函数设置页面尺寸,i386是PAGE_SIZE(4K),记录在系统统计vmmeter结构类型的全局变量cnt的v_page_size成员中。

  virtual_avail = vm_page_startup(avail_start, avail_end, virtual_avail);

  该语句初始化常驻内存模块。分析函数vm_page_startup的参数和返回值:avail_start的值是从系统启动时,汇编语言调用init386的入参first,指明有效内存的起始地址;avail_end是在getmemsize()函数结束时,通过avail_end = phys_avail[pa_indx];语句获得,该函数是一个与平台相关的函数,这里不作详细讨论,只须明白getmemsize()函数是获得具体物理内存的尺寸;virtual_avail是指向第一个可用页面的虚拟地址,在调用vm_page_startup函数前,是在pmap_bootstrap函数中获得初始值,并在vm_page_startup函数中调整获得真实的值。函数vm_page_startup将物理内存整理、分配为页面单元,并初始化页面管理模块所需信息,每一个页面单元被放置在自由链表中,该函数实现的详细讨论在页调度中讨论,作为内核管理涉及到的区域分配器初始化的一部分是在该函数中通过调用uma_startup函数实现的,该函数的实现在随后讨论。

  vm_object_init();

  初始化VM的对象模块,FreeBSD是通过统一的vm_object结构使用虚拟内存,该函数完成虚拟内存对象模块所需信息的初始化。

  vm_map_startup();

  初始化VM地址映射模块。

  kmem_init(virtual_avail, virtual_end);

  该函数创建内核虚拟地址映射关系,将内核文本、数据、BSS和所有系统启动时已经分配了的空间做一个映射,插入VM_MIN_KERNEL_ADDRESS和virtual_avail之间,余下的virtual_avail和virtual_end之间的地址空间是可用的自由空间。

  pmap_init(avail_start, avail_end);

  该函数初始化物理内存地址空间的映射关系。

  vm_pager_init();

  该函数实现系统所支持的所有页面接口类型的初始化,页面接口为数据在其支持的存储空间和物理内存之间的移动提供了一种机制,比如磁盘设备与内存之间,文件系统与内存之间。

  至此,vm_mem_init函数执行完成,VM系统初始化完成。

  2. 内核地址空间分配

  VM系统内核使用的虚拟地址空间段提供了一套用于分配和释放的函数,这些空间段可以从内核地址映射和子映射中分配获得。

  根据申请的页是否可以被pageout守护进程调度,内核内存分配有两种路径。在VM子系统初始化时,调用了kmem_init函数创建了内核映射。我们分析该函数的具体实现:

  函数void kmem_init(vm_offset_t start, vm_offset_t end)

  m = vm_map_create(kernel_pmap, VM_MIN_KERNEL_ADDRESS, end);

  函数vm_map_create根据给定了kernel_map物理地址,创建一个新的地址映射m,而VM_MIN_KERNEL_ADDRESS和end给出了该映射范围的下水位(lower address bound)和上水位(upper address bound)。

  kernel_map = m;

  kernel_map->system_map = 1;

  (void) vm_map_insert(m, NULL, (vm_offset_t) 0,

  VM_MIN_KERNEL_ADDRESS, start, VM_PROT_ALL, VM_PROT_ALL, 0);

  vm_map_unlock(m);

  由于函数kmem_init仅用于系统初始化,创建内核地址映射,因此,将获得的地址映射赋给全局变量kernel_map保存,通过vm_map_insert函数创建一个vm_map_entry实体记录相关值,VM_PROT_ALL和VM_PROT_ALL标识这段虚拟地址的访问权限,参见/sys/vm/vm.h定义。

  2.1 Wired (nonpageable,不可被pageout调度的页)分配函数

  固定页(wired page)是从来不会产生页错误(page fault)。其分配是由kmem_alloc函数和kmem_malloc函数实现的。

  函数vm_offset_t kmem_alloc(vm_map_t map, vm_size_t size)

  该函数用于在内核地址映射或子映射中,分配内存。

  size = round_page(size);

  调整申请内存的尺寸,使之为PAGE_SIZE的整数倍。

  vm_map_lock(map);

  if (vm_map_findspace(map, vm_map_min(map), size, &addr)) {

  vm_map_unlock(map);

  return (0);

  }

  offset = addr - VM_MIN_KERNEL_ADDRESS;

  vm_object_reference(kernel_object);

  vm_map_insert(map, kernel_object, offset, addr, addr + size,

  VM_PROT_ALL, VM_PROT_ALL, 0);

  vm_map_unlock(map);
  在vm_map结构的lock锁机制保护下。通过调用vm_map_findspace函数查询map地址映射是否有足够的空间满足申请的内存尺寸,如果成功,则可用空间的起始地址存于addr中;如果失败则返回1,则kmem_alloc函数调用失败。获得该内存分配空间起始地址与VM_MIN_KERNEL_ADDRESS的偏移。通过vm_object_reference函数对内核对象kernel_obj计数器ref_count加1,kernel_obj的初始化是在VM初始化时,调用vm_object_init实现的,其类型是OBJT_DEFAULT。调用vm_map_insert函数将刚找到的虚拟地址空间插入地址映射map的vm_map_entry链表中。

  for (i = 0; i < size; i += PAGE_SIZE) {

  vm_page_t mem;

  mem = vm_page_grab(kernel_object, OFF_TO_IDX(offset + i),

  VM_ALLOC_ZERO   VM_ALLOC_RETRY);

  if ((mem->flags & PG_ZERO) == 0)

  pmap_zero_page(mem);

  mem->valid = VM_PAGE_BITS_ALL;

  vm_page_flag_clear(mem, PG_ZERO);

  vm_page_wakeup(mem);

  }

  接下来的这段代码非常有意思,对于申请的内存空间每一页,通过调用vm_page_grab函数,查看该页是否已经被kernel_object持有,如果是,则根据vm_page成员flags标识,如果是PG_BUSY,则等待该标识清PG_BUSY,将该页重新设置为PG_BUSY,返回该地址映射(mem),如果该页没有被kernel_object持有,则分配一个新页(mem)。通过判断PG_ZERO标识,保证该页已经清零。最后通过vm_page_wakeup函数,给正在等待该页分配的线程一个唤醒的机会。这段代码在查找kernel_object持有页时,采用了自顶向下的展开算法(Sleator and Tarjan's top-down splay algorithm)。

  (void) vm_map_wire(map, addr, addr + size, FALSE);

  设置该段内存是wired。

  函数kmem_alloc是非常低层的,一般和平台相关性的函数在申请内存会调用该函数,比如sysarch()。通常kmem_alloc是使用kernel_map地址映射和kernel_object VM对象。

  函数vm_offset_t kmem_malloc(vm_map_t map, vm_size_t size, int flags)

  该函数同样是用于在内核地址映射或子映射中,分配内存。区别是:

  a) kmem_alloc函数在不能获得内存时,可以阻塞等待,而在中断层,分配内存是不能阻塞的,因此需要kmem_malloc以M_NOWAIT标识调用。

  b) kmem_malloc函数为malloc调用(malloc(9))提供一种实现机制,即:在内核需要动态分配内存(malloc)时,当申请尺寸大于其阀值,最终通过kmem_malloc实现空间分配。

  c) kmem_alloc路径是使用地址映射kernel_map和对象kernel_object;而kmem_malloc路径是使用kernel_map的子映射kmem_map和对象kmem_object,后者的具体讨论在后一节说明。

  size = round_page(size);

  addr = vm_map_min(map);

  首先,根据入参调整、设置尺寸和地址。

  vm_map_lock(map);

  if (vm_map_findspace(map, vm_map_min(map), size, &addr)) {

  vm_map_unlock(map);

  if (map != kmem_map) {

  static int last_report; /* when we did it (in ticks) */

  if (ticks < last_report    (ticks - last_report) >= hz) {

  last_report = ticks;

  }

  goto bad;

  }

  if ((flags & M_NOWAIT) == 0)

  panic("kmem_malloc(%ld): kmem_map too small: %ld total allocated",

  (long)size, (long)map->size);

  goto bad;

  }

  offset = addr - VM_MIN_KERNEL_ADDRESS;

  vm_object_reference(kmem_object);

  vm_map_insert(map, kmem_object, offset, addr, addr + size,

  VM_PROT_ALL, VM_PROT_ALL, 0);

  这段代码的思路和kmem_alloc函数相应的代码是一致的,找到足够的内存空间,返回该空间的起始地址addr,并获得该地址与内核地址映射的偏移(offset),不同之处在于区别c。

  retry:

  m = vm_page_alloc(kmem_object, OFF_TO_IDX(offset + i), pflags);

  if (m == NULL) {

  if ((flags & M_NOWAIT) == 0) {

  vm_map_unlock(map);

  VM_WAIT;

  vm_map_lock(map);

  goto retry;

  }

  while (i != 0) {

  i -= PAGE_SIZE;

  m = vm_page_lookup(kmem_object, OFF_TO_IDX(offset + i));

  vm_page_lock_queues();

  vm_page_free(m);

  vm_page_unlock_queues();

  }

  vm_map_delete(map, addr, addr + size);

  vm_map_unlock(map);

  goto bad;

  }

  if (flags & M_ZERO && (m->flags & PG_ZERO) == 0)

  pmap_zero_page(m);

  vm_page_flag_clear(m, PG_ZERO);

  m->valid = VM_PAGE_BITS_ALL;

  针对申请的内存空间的每一页(for (i = 0; i < size; i += PAGE_SIZE)):首先通过vm_page_alloc分配一页,如果分配成功(m != NULL),则根据入参是否含有M_ZERO决定是否需要对分配的页清零,最后设置该页的标识。

  如果分配失败(m == NULL):如果入参标识不含有M_NOWAIT,则可以阻塞等待(VM_WAIT),再次尝试;如果含有M_NOWAIT,则说明该次申请是不能阻塞等待,则释放所有已经分配的页面,申请失败(goto bad)。

  if (!vm_map_lookup_entry(map, addr, &entry)   

  entry->start != addr    entry->end != addr + size    entry->wired_count != 0)

  panic("kmem_malloc: entry not found or misaligned");

  entry->wired_count = 1;

  vm_map_simplify_entry(map, entry);

  对于该次申请分配空间对应的vm_map_entry实体成员设值,通过调用函数vm_map_simplify_entry建立该entry实体与map其它实体建立关联。

  for (i = 0; i < size; i += PAGE_SIZE) {

  m = vm_page_lookup(kmem_object, OFF_TO_IDX(offset + i));

  vm_page_lock_queues();

  vm_page_wire(m);

  vm_page_wakeup(m);

  vm_page_unlock_queues();

  pmap_enter(kernel_pmap, addr + i, m, VM_PROT_ALL, 1);

  vm_page_flag_set(m, PG_WRITEABLE   PG_REFERENCED);

  }

  vm_map_unlock(map);

  return (addr);

  这段代码是对该次申请的每一页,通过pmap_enter函数将每一页的虚拟地址映射关系加入在指定的kernel_pmap中。最后返回该次申请的内存起始地址。

  bad:

  return (0);

  如果该次申请出错,则返回空值。

  2.2 Pageable (可以由pageout调度的页)分配函数

  函数vm_offset_t kmem_alloc_pageable(vm_map_t map, vm_size_t size)

  该函数用于分配可以被pageout调度的内存空间,其中map只能是kernel_map或其子映射。

  size = round_page(size);

  addr = vm_map_min(map);

  result = vm_map_find(map, NULL, 0,

  &addr, size, TRUE, VM_PROT_ALL, VM_PROT_ALL, 0);

  if (result != KERN_SUCCESS) {

  return (0);

  }

  return (addr);

  这段代码十分清晰,通过vm_map_find函数,如果能找到足够的内存空间,则分配给addr,并在该map中插入一个vm_map_entry实体表示该段内存映射。如果不成功,则返回空值。

  函数vm_offset_t kmem_alloc_nofault (vm_map_t map, vm_size_t size)

  该函数的实现和kmem_alloc_pageable函数基本一致,只是在调用vm_map_find函数时,其最后一位参数是MAP_NOFAULT,具体用意,我不太确定。

  函数 vm_offset_t kmem_alloc_wait(vm_map_t map, vm_size_t size)

  该函数实现内核一个子映射的内存分配功能,在调用该函数时,可能会阻塞。

  vm_map_lock(map);

  if (vm_map_findspace(map, vm_map_min(map), size, &addr) == 0)

  break;

  if (vm_map_max(map) - vm_map_min(map) < size) {

  vm_map_unlock(map);

  return (0);

  }

  map->needs_wakeup = TRUE;

  vm_map_unlock_and_wait(map, FALSE);

  无限循环执行这段代码,除非break或是return。如果找到有足够空间分配这段内存vm_map_findspace返回为0,则跳出循环。如果没有,则判断是否该子映射是否有足够空间给申请者,如果没有则返回空值。如果该子映射有足够空间,只是暂时没有,则通过vm_map_unlock_and_wait睡眠,等待有空间可用。

  vm_map_insert(map, NULL, 0, addr, addr + size, VM_PROT_ALL, VM_PROT_ALL, 0);

  vm_map_unlock(map);

  当分配了足够的空间后,将对应的vm_map_entry实体插入map中。

  2.3 内核内存空间释放函数

  函数void kmem_free(vm_map_t map, vm_offset_t addr, vm_size_t size)

  该函数用于释放内核内存分配的内存空间。

  (void) vm_map_remove(map, trunc_page(addr), round_page(addr + size));

  函数kmem_free通过调整需要释放的空间的起始地址add和size,作为vm_map_remove的入参释放这段空间。

  函数void kmem_free_wakeup (vm_map_t map, vm_offset_t addr, vm_size_t size)

  vm_map_lock(map);

  (void) vm_map_delete(map, trunc_page(addr), round_page(addr + size));

  if (map->needs_wakeup) {

  map->needs_wakeup = FALSE;

  vm_map_wakeup(map);

  }

  vm_map_unlock(map);

  该函数和kmem_free函数的功能一样,只不过在释放空间后,会检查needs_wakeup成员,如果有其它线程阻塞于该map,则通过vm_map_wakeup函数试图唤醒。

  2.4 内核子映射分配

  FreeBSD5.0通过kernel_map提供了内核地址映射,基于kernel_map,FreeBSD5.0提供了kmem_map、clean_map和exec_map子映射。其中clean_map由分出buffer_map和pager_map两个子映射。

  &Oslash; kmem_map:为malloc机制和mbuf提供内核内存空间映射。

  &Oslash; exec_map:为exec进程执行等系统调用提供内核内存空间映射。

  &Oslash; buffer_map:为文件系统提供内核内存映射。

  &Oslash; pager_map:为页机制提供内核内存映射。
  本小节讨论内核内存地址子映射分配的实现。

  函数vm_map_t kmem_suballoc(vm_map_t parent, vm_offset_t* min,

  vm_offset_t* max, vm_size_t size)

  该函数实现了内核内存地址的子映射分配。

  GIANT_REQUIRED;

  size = round_page(size);

  *min = (vm_offset_t) vm_map_min(parent);

  ret = vm_map_find(parent, NULL, (vm_offset_t) 0,

  min, size, TRUE, VM_PROT_ALL, VM_PROT_ALL, 0);

  if (ret != KERN_SUCCESS) {

  printf("kmem_suballoc: bad status return of %d.\n", ret);

  panic("kmem_suballoc");

  }

  在父映射parent中,查找是否有足够的空间给申请分配的子映射,如果失败,则出现异常错误。

  *max = *min + size;

  result = vm_map_create(vm_map_pmap(parent), *min, *max);

  if (result == NULL)

  panic("kmem_suballoc: cannot create submap");

  通过vm_map_create函数创建新的地址映射vm_map,赋给result,如果不能创建,也是出现异常错误。

  if (vm_map_submap(parent, *min, *max, result) != KERN_SUCCESS)

  panic("kmem_suballoc: unable to change range to submap");

  return (result);

  通过vm_map_submap函数,建立parent和result之间的联系,如果不能建立也是异常错误。最后返回新创建的子映射结构的指针。

  3. 区域分配器机制(UMA子系统)

  FreeBSD5.0通过区域分配器(zone allocator)提供了对动态大小的内存分配的有效管理。内核中经常使用的malloc函数也是区域分配器的一个封装。区域分配器第一次出现在FreeBSD3.0中,但是在FreeBSD5.0中已经完全重写了。

  3.1 数据结构

  区域分配器涉及到的主要数据结构是:uma_zone、uma_slab、uma_bucket、uma_cache和uma_hash,其中uma_hash是为了提高效率。

  一个uma_zone可以有多个uma_slab,每个uma_slab是通过单向链表串联(根据slab的状态,可以在不同的链表中);每个slab是由多个item组成,每个item是真正供数据存储所用的空间。而uma_bucket和uma_cache是为每个CPU提供cache和并行处理机制。

  3.2 区域分配器初始化

  区域分配器的初始化分为三个部分,分别在内核执行SI_SUB_VM、SI_SUB_KMEM和SI_SUB_VM_CONF模块中调用。

  函数void uma_startup(void *bootmem)

  该函数实现了区域分配器初始化的一部分,其入参bootmem是由系统启动时,在vm_page_startup函数给出的,指向了系统分配给区域管理所需内存的首地址。该段内存的尺寸应该是UMA_SLAB_SIZE * UMA_BOOT_PAGES,即30页。

  mtx_init(&uma_mtx, "UMA lock", NULL, MTX_DEF);

  初始化全局互斥体uma_mtx,该uma_mtx用于保护对全局区域管理器链表uma_zones的操作。

  args.name = "UMA Zones";

  args.size = sizeof(struct uma_zone) + (sizeof(struct uma_cache) * (maxcpu - 1));

  args.ctor = zone_ctor;

  args.dtor = zone_dtor;

  args.uminit = zero_init;

  args.fini = NULL;

  args.align = 32 - 1;

  args.flags = UMA_ZONE_INTERNAL;

  zone_ctor(zones, sizeof(struct uma_zone), &args);

  手工创建zones,通过zone_ctor函数对zones初始化,并插入全局链表uma_zones中。关于zones的定义参考:

  static struct uma_zone masterzone;

  static uma_zone_t zones = &

  所有的区域分配管理器都是从masterzone派生的产物。

  for (i = 0; i < UMA_BOOT_PAGES; i++) {

  slab = (uma_slab_t)((u_int8_t *)bootmem + (i * UMA_SLAB_SIZE));

  slab->us_data = (u_int8_t *)slab;

  slab->us_flags = UMA_SLAB_BOOT;

  LIST_INSERT_HEAD(&uma_boot_pages, slab, us_link);

  uma_boot_free++;

  }

  根据bootmem提供的内存地址,分配30个页面,串联在uma_boot_pages链表中。

  slabsize = UMA_SLAB_SIZE - sizeof(struct uma_slab); /*4k C 24*/

  slabsize /= UMA_MAX_WASTE; /*(4k-24)/(4k/10) = 9*/

  slabsize++; /* 10*/

  slabsize += sizeof(struct uma_slab); /*10 + 24 = 34*/

  计算slabsize尺寸,在i386体系中,是34。

  slabzone = uma_zcreate("UMA Slabs", slabsize, NULL, NULL, NULL, NULL,

  UMA_ALIGN_PTR, UMA_ZONE_INTERNAL);

  hashzone = uma_zcreate("UMA Hash", sizeof(struct slabhead *) * UMA_HASH_SIZE_INIT, NULL, NULL, NULL, NULL, UMA_ALIGN_PTR, UMA_ZONE_INTERNAL);

  bucketzone = uma_zcreate("UMA Buckets", sizeof(struct uma_bucket),

  NULL, NULL, NULL, NULL, UMA_ALIGN_PTR, UMA_ZONE_INTERNAL);

  调用uma_zcreate函数创建三个新的区域,分别赋给slabzone、hashzone和bucketzone。系统中所有的uma_slab结构都是从slabzone区域分配;hashzone负责哈希表的初始化分配;bucketzone负责所有uma_bucket分配。

  在区域分配器执行完uma_startup之后,如图所示其结构:

  masterzone

  zones->uz_part_slab

  item => slabzone

  item => hashzone

  item => bucketzone

  函数void uma_startup2(void)

  该函数十分简单,就是几个全局变量的设置。通过bucket_enable函数设置bucketdisable的值,该值决定了bucketzone区域是否可用。

  booted = 1;

  bucket_enable();

  函数static void uma_startup3(void)

  callout_init(&uma_callout, 0);

  callout_reset(&uma_callout, UMA_WORKING_TIME * hz, uma_timeout, NULL);

  该函数用于设置定时执行器,每隔UMA_WORKING_TIME * hz的时间,执行一个uma_timeout函数。主要是用于统计系统的区域使用信息,以及相应的每个CPU的cache使用情况。

  3.3 区域接口函数

  关于区域分配器的接口函数描述可以参考zone(9)。本小节基于zone(9)的论述,详细讨论区域分配器的接口函数的实现。

  函数uma_zone_t uma_zcreate(char *name, size_t size, uma_ctor ctor, uma_dtor dtor,

  uma_init uminit, uma_fini fini, int align, u_int16_t flags)

  该函数用于创建新的区域管理器(uma_zone结构类型)。入参:name是用于调试所用的文本字符串;size表示需要创建的区域分配管理器的每一个item的尺寸;ctor和dtor都是函数指针,分别用于uma_zalloc和uma_zfree系列函数回调使用;uminit和fini同样是函数指针,用于区域分配对象的优化;align是位掩码;flags说明了需要创建的区域的特性。

  flags定义如下:

  #define UMA_ZFLAG_OFFPAGE 0x0001 /* Struct slab/freelist off page */

  #define UMA_ZFLAG_PRIVALLOC 0x0002 /* Zone has supplied it's own alloc */

  #define UMA_ZFLAG_INTERNAL 0x0004 /* Internal zone, no offpage no PCPU */

  #define UMA_ZFLAG_MALLOC 0x0008 /* Zone created by malloc */

  #define UMA_ZFLAG_NOFREE 0x0010 /* Don't free data from this zone */

  #define UMA_ZFLAG_FULL 0x0020 /* This zone reached uz_maxpages */

  #define UMA_ZFLAG_BUCKETCACHE 0x0040 /* Only allocate buckets from cache */

  #define UMA_ZFLAG_HASH 0x0080 /* Look up slab via hash */

  函数uma_zcreate的实现主要是通过调用uma_zalloc_internal函数,我们先看看函数uma_zcreate的代码:

  args.name = name;

  args.size = size;

  args.ctor = ctor;

  args.dtor = dtor;

  args.uminit = uminit;

  args.fini = fini;

  args.align = align;

  args.flags = flags;

  return (uma_zalloc_internal(zones, &args, M_WAITOK));

  正如我们前面讨论启动时,提及的所有的区域创建都是从zones中派生的。现在分析uma_zalloc_internal函数,可以分为4个主体部分讨论:

  if (bucketdisable && zone == bucketzone)

  return (NULL);

  在系统启动时,指定的UMA_BOOT_PAGES空间耗尽(bucketdisable = 1),并且指定的父区域管理器是bucketzone,则返回NULL值。

  ZONE_LOCK(zone);

  slab = uma_zone_slab(zone, flags);

  if (slab == NULL) {

  ZONE_UNLOCK(zone);

  return (NULL);

  }

  通过调用uma_zone_slab函数,从zone中分配一个uma_slab类型结构,其指针赋给slab。

  item = uma_slab_alloc(zone, slab);

  ZONE_UNLOCK(zone);

  通过uma_zone_slab函数,从zone中的一个slab中分配一个item,该item用于存储新的uma_zone类型的管理结构。

  if (zone->uz_ctor != NULL)

  zone->uz_ctor(item, zone->uz_size, udata);

  if (flags & M_ZERO)

  bzero(item, zone->uz_size);

  return (item);

  如果制定了uz_ctor,则调用回调函数对item里存储的新的uma_zone进行构造初始化。最后返回新的uma_zone地址。

  函数void * uma_zalloc(uma_zone_t zone, int flags)

  该函数实现了从zone中分配一个item的功能,该函数只是uma_zalloc_arg函数一个简单封装。

  return uma_zalloc_arg(zone, NULL, flags);

  因此,我们讨论uma_zalloc_arg函数实现的主体部分。

  zalloc_restart:

  cpu = PCPU_GET(cpuid);

  CPU_LOCK(zone, cpu);

  cache = &zone->uz_cpu[cpu];

  根据当前调用者的CPU号,获得相应的uma_cache结构。

  zalloc_start:

  bucket = cache->uc_allocbucket;

  成员uc_allocbucket记录了该item应该分配的来源。

  if (bucket) {

  如果bucket有值,则说明可以检查是否能从该bucket中分配item。

  if (bucket->ub_ptr > -1) {

  如果ub_ptr > -1,说明该bucket目前存在可以分配的item。

  item = bucket->ub_bucket[bucket->ub_ptr];

  bucket->ub_ptr--;

  cache->uc_allocs++;

  CPU_UNLOCK(zone, cpu);

  if (zone->uz_ctor)

  zone->uz_ctor(item, zone->uz_size, udata);

  if (flags & M_ZERO)

  bzero(item, zone->uz_size);

  return (item);

  分配该item,并做相应的初始化,返回该item。

  } else if (cache->uc_freebucket) {

  如果uc_allocbucket没有可用空间,检查是否存在free bucket,并且可用。

  if (cache->uc_freebucket->ub_ptr > -1) {

  uma_bucket_t swap;

  swap = cache->uc_freebucket;

  cache->uc_freebucket = cache->uc_allocbucket;

  cache->uc_allocbucket = swap;

  goto zalloc_start;

  将该可用的uc_freebucket赋给uc_allocbucket,重新对bucket赋值,再次试图分配item。

  }

  }

  }

  以上是对如果存在bucket的情况处理。分析一下,什么情况会继续执行下面的代码。可能的情况:a. 不存在uc_allocbucket;b. uc_allocbucket已经耗尽。

  ZONE_LOCK(zone);

  zone->uz_allocs += cache->uc_allocs;

  cache->uc_allocs = 0;

  更新uz_allocs值,该值说明了该zone区域被分配的次数。比如当uc_allocbucket已经耗尽,需要将该cache的uc_allocs加入uz_allocs中,因为后面会更新uc_allocbucket。

  if (cache->uc_allocbucket) {

  LIST_INSERT_HEAD(&zone->uz_free_bucket, cache->uc_allocbucket, ub_link);

  cache->uc_allocbucket = NULL;

  }

  现在uc_allocbucket是一个可以释放item的bucket,将其加入到uz_free_bucket中。

  if ((bucket = LIST_FIRST(&zone->uz_full_bucket)) != NULL) {

  LIST_REMOVE(bucket, ub_link);

  cache->uc_allocbucket = bucket;

  ZONE_UNLOCK(zone);

  goto zalloc_start;

  }

  更新cache结构的uc_allocbucket指针,试图重新分配item。

  CPU_UNLOCK(zone, cpu);

  if (zone->uz_count < UMA_BUCKET_SIZE - 1)

  zone->uz_count++;

  if (uma_zalloc_bucket(zone, flags)) {

  ZONE_UNLOCK(zone);

  goto zalloc_restart;

  }

  ZONE_UNLOCK(zone);

  如果zone确实没有可用bucket使用,因此,需要通过uma_zalloc_bucket分配新的bucket,然后完全重新开始试图分配item。

  return (uma_zalloc_internal(zone, udata, flags));

  什么时候会执行到这一步?比如,对于系统的某个模块的链表,需要malloc分配每个节点空间,而且该链表很长,以至于在uma_zcreate创建的slab里的item都使用完了。因此,需要再分配slab来满足需要。

  函数void uma_zfree(uma_zone_t zone, void *item)

  该函数实现了将item释放回zone中,同uma_zalloc类似,该函数是uma_zfree_arg的一个封装。

  uma_zfree_arg(zone, item, NULL);

  现在,我们分析uma_zfree_arg的实现。

  if (zone->uz_flags & UMA_ZFLAG_FULL)

  goto zfree_internal;

  标识UMA_ZFLAG_FULL说明该区域zone已经达到峰值,直接放在后面处理。后面再讨论。

  if (zone->uz_dtor)

  zone->uz_dtor(item, zone->uz_size, udata);

  如果该zone的解析函数指针不为空,则首先调用其解析函数处理。

  zfree_restart:

  cpu = PCPU_GET(cpuid);

  CPU_LOCK(zone, cpu);

  cache = &zone->uz_cpu[cpu];

  同uma_zalloc_arg函数类似,根据当前调用者的CPU号,获得相应的uma_cache结构。

  zfree_start:

  bucket = cache->uc_freebucket;

  if (bucket) {

  if (bucket->ub_ptr < zone->uz_count) {

  bucket->ub_ptr++;

  bucket->ub_bucket[bucket->ub_ptr] = item;

  CPU_UNLOCK(zone, cpu);

  return;

  } else if (cache->uc_allocbucket) {

  if (cache->uc_allocbucket->ub_ptr < cache->uc_freebucket->ub_ptr) {

  uma_bucket_t swap;

  swap = cache->uc_freebucket;

  cache->uc_freebucket = cache->uc_allocbucket;

  cache->uc_allocbucket = swap;

  goto zfree_start;

  }

  }

  }

  比较uma_zalloc_arg函数相应的代码,只不过bucket代表需要释放的uc_freebucket。如果该uc_freebucket还有剩余item空间,就不真正的释放该item,只是将计数器加1。如果uc_freebucket满了,并且uc_allocbucket有值,而且uc_allocbucket还有item空间,则交换这两个值,重新对bucket赋值,并再次试图对bucket计数器加1。

  如果该zone原来的uc_freebucket为空,或者uc_freebucket和uc_allocbucket都满了,则需要继续执行。

  ZONE_LOCK(zone);

  bucket = cache->uc_freebucket;

  cache->uc_freebucket = NULL;

  if (bucket != NULL) {

  LIST_INSERT_HEAD(&zone->uz_full_bucket, bucket, ub_link);

  }

  如果是uc_freebucket的item已经满了,则将uc_freebucket插入uz_full_bucket链表中。

  if ((bucket = LIST_FIRST(&zone->uz_free_bucket)) != NULL) {

  LIST_REMOVE(bucket, ub_link);

  ZONE_UNLOCK(zone);

  cache->uc_freebucket = bucket;

  goto zfree_start;

  }

  从该zone需要释放的bucket链表uz_free_bucket(如果其不为空)中,移出第一个节点,将其赋给uc_freebucket,重新执行zfree_start。

  CPU_UNLOCK(zone, cpu);

  ZONE_UNLOCK(zone);

  bflags = M_NOWAIT;

  if (zone->uz_flags & UMA_ZFLAG_BUCKETCACHE)

  bflags  = M_NOVM;

  bucket = uma_zalloc_internal(bucketzone, NULL, bflags);

  if (bucket) {

  bucket->ub_ptr = -1;

  ZONE_LOCK(zone);

  LIST_INSERT_HEAD(&zone->uz_free_bucket, bucket, ub_link);

  ZONE_UNLOCK(zone);

  goto zfree_restart;

  }

  当执行到这段代码,说明以前分配给该zone的bucket都已经使用完了,因此,通过uma_zalloc_internal函数试图从bucketzone中再为zone分配新的bucket。如果成功,则插入uz_free_bucket链表中,再次试图调整该bucket的ub_ptr值。

  zfree_internal:

  uma_zfree_internal(zone, item, udata, 0);

  return;

  当执行到这段代码,说明该zone的bucket已经完全满了,需要真正释放该item返回给zone。函数uma_zfree_internal的实现在uma_zdestroy函数中讨论。

  函数void uma_zdestroy(uma_zone_t zone)

  该函数用于销毁一个空的区域。

  uma_zfree_internal(zones, zone, NULL, 0);

  函数uma_zdestroy是uma_zfree_internal的一个封装,因此,我们讨论uma_zfree_internal的实现。static void uma_zfree_internal(uma_zone_t zone, void *item, void *udata, int skip)

  if (!skip && zone->uz_dtor)

  zone->uz_dtor(item, zone->uz_size, udata);

  如果没有指定跳过该zone的解析函数,并且该zone的解析函数指针不为空,则执行该解析函数。

  ZONE_LOCK(zone);

  if (!(zone->uz_flags & UMA_ZFLAG_MALLOC)) {

  mem = (u_int8_t *)((unsigned long)item & (~UMA_SLAB_MASK));

  if (zone->uz_flags & UMA_ZFLAG_HASH)

  slab = hash_sfind(&zone->uz_hash, mem);

  else {

  mem += zone->uz_pgoff;

  slab = (uma_slab_t)mem;

  }

  } else {

  slab = (uma_slab_t)udata;

  }
  根据标识UMA_ZFLAG_MALLOC,如果uz_flags含有该标识,说明该zone是用于malloc机制,则通过udata入参获得uma_slab_t(slab)的值;如果不含有该标识,则通过计算获得该slab的值。

  if (slab->us_freecount+1 == zone->uz_ipers) {

  LIST_REMOVE(slab, us_link);

  LIST_INSERT_HEAD(&zone->uz_free_slab, slab, us_link);

  } else if (slab->us_freecount == 0) {

  LIST_REMOVE(slab, us_link);

  LIST_INSERT_HEAD(&zone->uz_part_slab, slab, us_link);

  }

  根据slab成员us_freecount不同的值,将slab移入不同队列中。

  freei = ((unsigned long)item - (unsigned long)slab->us_data) / zone->uz_rsize;

  slab->us_freelist[freei] = slab->us_firstfree;

  slab->us_firstfree = freei;

  slab->us_freecount++;

  调整该slab的item指针,以及相应的统计信息。

  zone->uz_free++;

  调整zone的可用空间(item)计数器。

  if (zone->uz_flags & UMA_ZFLAG_FULL) {

  if (zone->uz_pages < zone->uz_maxpages)

  zone->uz_flags &= ~UMA_ZFLAG_FULL;

  wakeup_one(zone);

  }

  ZONE_UNLOCK(zone);

  如果uz_flags含有UMA_ZFLAG_FULL,说明该zone在释放该item支前,已经达到饱和状态,因此,释放了该item,说明该zone又有了可用的item。所以,去掉标识UMA_ZFLAG_FULL,并通过wakeup_one函数,试图唤醒等待于该zone的第一个线程。

  3.4 关于区域分配器的一点后话

  FreeBSD5.0提供的区域分配器,为内核动态分配内存提供了全面的跟踪机制,同时通过指针的移动极大的提高了内存分配和释放的速度。区域分配器含有很多链表,分配和释放空间,主要是通过节点在链表间的转移实现。

  4. 内核动态内存管理-malloc机制

  内核和用户调用malloc函数的形式不一样,在这里,我们讨论内核部分的实现。首先,看看malloc的定义:“void * malloc(unsigned long size, struct malloc_type *type, int flags);”,内核在使用malloc前,需要通过宏MALLOC_DEFINE定义malloc_type的type实体,将该宏展开:

  MALLOC_DEFINE(type, shortdesc, longdesc) à

  struct malloc_type type[1] = {

  { NULL, 0, 0, 0, 0, 0, M_MAGIC, shortdesc, {} }

  };

  SYSINIT(type##_init, SI_SUB_KMEM, SI_ORDER_SECOND, malloc_init, type);

  SYSUNINIT(type##_uninit, SI_SUB_KMEM, SI_ORDER_ANY, malloc_uninit, type)

  因此,内核使用malloc函数的变量type都会在开机的时候通过malloc_init初始化,在reboot的时候通过malloc_uninit释放。

  查看malloc_type结构:

  struct malloc_type {

  struct malloc_type *ks_next; /* next in list */

  u_long ks_memuse; /* total memory held in bytes */

  u_long ks_size; /* sizes of this thing that are allocated */

  u_long ks_inuse; /* # of packets of this type currently in use */

  uint64_t ks_calls; /* total packets of this type ever allocated */

  u_long ks_maxused; /* maximum number ever used */

  u_long ks_magic; /* if it's not magic, don't touch it */

  const char *ks_shortdesc; /* short description */

  struct mtx ks_mtx; /* lock for stats */

  };

  该结构用于对同一种申请动态分配的类型使用情况的控制。成员ks_next用于将内核所有需要动态分配空间的类型通过kmemstatistics链接。别的成员参考英文注释。

  4.1 malloc子系统初始化

  通过mi_startup()函数对SI_SUB_KMEM子系统初始化。

  A:SYSINIT(kmem, SI_SUB_KMEM, SI_ORDER_FIRST, kmeminit, NULL)

  分析kmeminit函数的主体部分:

  mtx_init(&malloc_mtx, "malloc", NULL, MTX_DEF);

  初始化全局互斥体malloc_mtx,该互斥体用于保护全局malloc_type链表kmemstatistics,该链表用于维护系统通过MALLOC_DEFINE宏定义的type。

  vm_kmem_size = VM_KMEM_SIZE;

  mem_size = cnt.v_page_count * PAGE_SIZE;

  if ((mem_size / VM_KMEM_SIZE_SCALE) > vm_kmem_size)

  vm_kmem_size = mem_size / VM_KMEM_SIZE_SCALE;

  if (vm_kmem_size >= VM_KMEM_SIZE_MAX)

  vm_kmem_size = VM_KMEM_SIZE_MAX;

  TUNABLE_INT_FETCH("kern.vm.kmem.size", &vm_kmem_size);

  if ((vm_kmem_size / 2) > (cnt.v_page_count * PAGE_SIZE))

  vm_kmem_size = 2 * cnt.v_page_count * PAGE_SIZE;

  设置、调整内核内存尺寸vm_kmem_size,其中TUNABLE_INT_FETCH宏用于从/boot/defaults/loader.conf或/boot/loader.conf文件中获得kern.vm.kmem.size值,如果设置了该值,则赋给vm_kmem_size。最后将vm_kmem_size的值限制在物理内存的两倍以内。

  npg = (nmbufs * MSIZE + nmbclusters * MCLBYTES + nmbcnt *

  sizeof(u_int) + vm_kmem_size) / PAGE_SIZE;

  kmem_map = kmem_suballoc(kernel_map, (vm_offset_t *)&kmembase,

  (vm_offset_t *)&kmemlimit, (vm_size_t)(npg * PAGE_SIZE));

  kmem_map->system_map = 1;

  计算用于网络通信所需缓冲的尺寸,并通过kmem_suballoc函数分配vm_map类型的结构用于管理该段内存(kmem_map)。

  uma_startup2();

  for (i = 0, indx = 0; kmemzones[indx].kz_size != 0; indx++) {

  int size = kmemzones[indx].kz_size;

  char *name = kmemzones[indx].kz_name;

  kmemzones[indx].kz_zone = uma_zcreate(name, size, NULL, NULL, NULL, NULL,

  UMA_ALIGN_PTR, UMA_ZONE_MALLOC);

  for (;i <= size; i+= KMEM_ZBASE)

  kmemsize[i >> KMEM_ZSHIFT] = indx;

  }

  这段代码是区域分配器初始化的一个部分。函数uma_startup2完成区域分配器所需的一些全局变量的设置。全局变量kmemzones的定义如下:

  struct {

  int kz_size;

  char *kz_name;

  uma_zone_t kz_zone;

  } kmemzones[] = {

  {16, "16", NULL},

  {32, "32", NULL},

  {64, "64", NULL},

  {128, "128", NULL},

  {256, "256", NULL},

  {512, "512", NULL},

  {1024, "1024", NULL},

  {2048, "2048", NULL},

  {4096, "4096", NULL},

  {8192, "8192", NULL},

  {16384, "16384", NULL},

  {32768, "32768", NULL},

  {65536, "65536", NULL},

  {0, NULL},

  };

  当内核需要分配一个较小内存空间时(<65536),函数malloc通过该变量实现内存区域分配,区域分配器是通过uma_zone结构管理的,因此,在这段代码中通过uma_zcreate创建相应的区域,并将该区域指针赋给kz_zone成员,UMA_ZONE_MALLOC标识指明了该区域用于malloc调用。涉及到uma的函数系列,我们在后面的区域分配器中讨论。

  B:SYSINIT(type##_init, SI_SUB_KMEM, SI_ORDER_SECOND, malloc_init, type)

  和SYSUNINIT(type##_uninit, SI_SUB_KMEM, SI_ORDER_ANY, malloc_uninit, type)

  该宏是MALLOC_DEFINE宏的一条语句。所有内核通过malloc动态分配的空间都需通过MALLOC_DEFINE定义,即在系统初始化时,需要malloc_init对该变量初始化,下面分析该函数主体:

  mtx_lock(&malloc_mtx);

  if (type->ks_magic != M_MAGIC)

  panic("malloc type lacks magic");

  if (cnt.v_page_count == 0)

  panic("malloc_init not allowed before vm init");

  if (type->ks_next != NULL)

  return;

  type->ks_next = kmemstatistics;

  kmemstatistics = type;

  mtx_init(&type->ks_mtx, type->ks_shortdesc, "Malloc Stats", MTX_DEF);

  mtx_unlock(&malloc_mtx);

  这段代码非常简单,就是在malloc_mtx互斥体的保护下,将一个新的malloc_type 类型的type变量插入kmemstatistics链表首部,并初始化malloc_type结构的ks_mtx互斥体。

  同样,malloc_uninit用于在系统关闭时,将type变量从kmemstatistics抽出,并销毁其ks_mtx互斥体。

  4.2 malloc接口函数的实现

  函数void * malloc(unsigned long size, struct malloc_type *type, int flags)

  该函数实现了内核动态分配内存的功能,FreeBSD根据其分配内存尺寸与阀值KMEM_ZMAX(64K)的关系决定如何实现该内存区域分配,其实现主体如下。

  if (size <= KMEM_ZMAX) {

  if (size & KMEM_ZMASK)

  size = (size & ~KMEM_ZMASK) + KMEM_ZBASE;

  indx = kmemsize[size >> KMEM_ZSHIFT];

  zone = kmemzones[indx].kz_zone;

  va = uma_zalloc(zone, flags);

  mtx_lock(&ksp->ks_mtx);

  if (va == NULL)

  goto out;

  ksp->ks_size  = 1 << indx;

  size = zone->uz_size;

  } else {

  size = roundup(size, PAGE_SIZE);

  zone = NULL;

  va = uma_large_malloc(size, flags);

  mtx_lock(&ksp->ks_mtx);

  if (va == NULL)

  goto out;

  }

  如果申请的内存空间尺寸小于等于阀值KMEM_ZMAX,采用2的幂的关系来分配空间,即如果申请500字节的空间,满足该尺寸的最小kmemzones是kz_size为512的项;如果申请513字节的空间,则满足该尺寸的最小kmemzones是kz_size为1024的项,因此,实际通过uma_zalloc函数分配的内存尺寸应该是512和1024字节。

  如果申请的内存空间尺寸大于阀值KMEM_ZMAX,采用满足该尺寸的最小整数倍PAGE_SIZE(4K)的空间尺寸。即如果申请65537字节的内存,则需要分配的尺寸是17个PAGE_SIZE(68K),更新尺寸后,通过uma_large_malloc函数实现具体区域空间分配,其本质是通过kmem_malloc函数实现。

  ksp->ks_memuse += size;

  ksp->ks_inuse++;

  out:

  ksp->ks_calls++;

  if (ksp->ks_memuse > ksp->ks_maxused)

  ksp->ks_maxused = ksp->ks_memuse;

  mtx_unlock(&ksp->ks_mtx);

  return ((void *) va);

  这段代码是malloc函数实现的剩余部分,其中略过了与流程关系不大的代码。主要用于更新申请分配内存的类型控制结构的成员信息。

  函数void free(void *addr, struct malloc_type *type)

  该函数释放动态分配的内存空间。

  slab = vtoslab((vm_offset_t)addr & (~UMA_

posted on 2008-06-12 13:12  骆驼SPACE  阅读(674)  评论(0编辑  收藏  举报