转一个solaris虚拟内存管理的wiki

Virtual Meory management Wikipedia,自由的百科全书

1. 内存管理 1.1. 虚拟地址空间 1.1.1. 概述 Solaris的进程地址空间分配分为两个阶段:内核地址空间的分配和用户地址空间的分配。内核地址空间的分配只在系统启动时进行一次,当创建第一个内核进程,该进程的地址空间就是刚刚分配好的内核地址空间,而且之后所有的内核进程都共享该内核地址空间。在Solaris中,只有内核地址空间是经过分配得到的,之后利用fork进行进程创建时,都是直接复制父进程的地址空间。第一个用户进程创建时,它会复制父进程的地址空间,也就是内核地址空间。 1.1.2. 数据结构 1.1.2.1. 重要数据结构间关系 图1 数据结构关系图 每个用户进程都拥有自己独立的地址空间,而所有的内核进程则共享唯一的内核地址空间。每个地址空间将若干segment driver封装起来,这些segment driver以AVL树的形式组织起来。每个地址空间都要有hat结构,在Solaris中,硬件地址转换(Hardware Address Translation)实现利用hat数据结构来存放一个地址空间的顶级地址转换信息。当segment driver试图操作硬件MMU时,HAT层会被调用。如果一个segment driver试图创建或者撤销某个地址空间映射,它就会调用相应的HAT函数。而htable结构则用来描述硬件页表。 1.1.2.2.as as结构是进程地址空间结构,每一个进程都对应一个as结构的变量。as结构在uts/common/vm/as.h中定义,它的定义如下: struct as { /*as结构的一些属性*/ kmutex_t a_contents; /* 保护as结构中的一些域 */ uchar_t a_flags; /* 描述as的属性*/ uchar_t a_vbits; /* 用来收集统计信息 */ kcondvar_t a_cv; /* 被as_rangelock使用*/ struct hrmstat *a_hrm; /* 维护引用信息和修改信息 */ caddr_t a_userlimit; /* 该地址空间的最高允许地址 */ size_t a_size; /* 地址空间的大小 */ /* 与hat结构相关的字段 */ struct hat *a_hat; /* hat数据结构*/ /*与segment相关*/ struct seg *a_seglast; /* 该地址空间中上次命中的segment */ krwlock_t a_lock; /* 保护与segment相关的一些域 */ struct seg *a_lastgap; /* 由as_gap()发现的最近一次的segment */ struct seg *a_lastgaphl; /* last seg saved in as_gap() either for */ /* AS_HI or AS_LO used in as_addseg() */ avl_tree_t a_segtree; /* 地址空间中的segment,以AVL树的形式组织 */ avl_tree_t a_wpage; /* 守护页 (procfs) */ uchar_t a_updatedir; /* 映射改变时,重建a_objectdir */ timespec_t a_updatetime; /* 映射上一次改变的时间 */ vnode_t **a_objectdir; /* 对象目录 (procfs) */ size_t a_sizedir; /* 对象目录的大小*/ struct as_callback *a_callbacks; /* callback列表*/ void *a_xhat; /* xhat提供者列表 */ }; as中的字段分成三个部分。第一个部分是用于描述as的一般性字段,包括用于保护as中域的互斥变量a_contents,描述as属性的a_flags域,用来收集统计信息的a_vbits域,用来保护as_rangelock的条件变量,以及描述该地址空间大小的size域。 第二部分是与hat结构相关的字段。每个地址空间都需要有相应的hat结构,hat结构中主要存放地址转换信息,帮助MMU完成虚拟地址到物理地址的转换。第三部分是与segment相关的字段。 a_seglast表示该地址空间中最近一次命中的segment。每次对地址空间的访问都需要更新该字段。 a_lock保护与segment相关的域,当对as中某个segment相关的域操作时,都需要用该读写锁对这些域进行保护。 a_lastgaphl表示在as_gap()中存放的最近一次访问的segment。在向地址空间增加一个segment时,为了提高效率,并没有采用avl_find()方法来寻找插入点,而是简单地以a_lastgaphl为起点寻找插入点(如果a_lastgaphl不空)。 a_segtree表示地址空间中所有segment组成的AVL树。在Solaris中,地址空间中的所有segment都以AVL树的形式组织起来。这样,对segment进行增删查改操作效率比较高。 a_wpage表示该地址空间的守护页。这些守护页也就是procfs。利用守护页可以对内存中的一些段进行监控。 a_updatedir标示地址空间的映射发生变化,通知进程需要重建a_objectdir。例如,当向进程地址空间中,新增一个segment时,需要将该地址空间的a_updatedir域置为1。 a_updatetime表示最近一次地址映射发生变化的时间。 a_objectdir表示对象目录。 a_callbacks是callback列表。该列表中主要是挂在segment driver上的回调函数列表。 1.1.2.3.seg seg结构是描述进程中segment的结构,每一个进程地址空间都由若干segment组成,这些segment组成一个AVL树。seg结构在uts/common/vm/seg.h中,它的定义如下: struct seg { caddr_t s_base; /* 虚拟基地址 */ size_t s_size; /* 以byte计算的segment大小 */ uint_t s_szc; /* 该段所支持的最大的页大小 */ uint_t s_flags; /* segment的标记*/ struct as *s_as; /* 该segment从属的地址空间 */ avl_node_t s_tree; /* 在该地址空间中,针对该segment的AVL树链接 */ struct seg_ops *s_ops; /* 对该segment的操作向量*/ void *s_data; /* 针对具体实例的私有数据*/ }; 每个地址空间都包含了若干segment,这些segment由不同的segment driver管理。Seg结构包含该segment的虚拟基地址,segment的大小,指向其所属地址空间的指针,用来维护AVL树的指针,以及挂在该segment driver上的回调函数和数据。可以看出,在seg结构中既包含描述segment的属性信息,也包含对该segment进行操作、访问的函数信息。 1.1.2.4.hat hat结构是进程地址空间结构,每一个进程都对应一个as结构的变量。as结构在uts/common/vm/as.h中,它的定义如下: struct hat { /* hat结构的一些属性 */ kmutex_t hat_mutex; /* 整个hat结构的互斥量 */ kmutex_t hat_switch_mutex; /* hat切换时的互斥量 */ struct as *hat_as; /* 指向hat所属的地址空间 */ uint_t hat_stats; /* hat结构的统计信息 */ pgcnt_t hat_pages_mapped[MAX_PAGE_LEVEL + 1]; cpuset_t hat_cpus; uint16_t hat_flags; /* hat的标记 */ /* 维护hat链表的字段 */ struct hat *hat_next; /* 指向下一个hat结构 */ struct hat *hat_prev; /* 指向上一个hat结构 */ /* 与htable相关的字段 */ htable_t *hat_htable; /* 指向顶级硬件页表的指针 */ uint_t hat_num_hash; /* htable哈希数目 */ htable_t **hat_ht_hash; htable_t *hat_ht_cached; /* 空闲的htables缓存 */ x86pte_t hat_vlp_ptes[VLP_NUM_PTES]; }; hat中的字段分成三个部分。第一部分主要描述hat结构的属性,包括一些互斥变量,hat所属的地址空间指针,hat结构的统计信息,及hat的标记等。第二部分则包含了如何组织hat双向链表的字段。 第三部分是一些与htable相关的字段。 hat_htable是指向顶级硬件页表的指针。当进行地址转换,调用HAT层时,通过hat结构就可以找到顶级硬件页表,从而进行地址转换。 1.1.3. 情景 1.1.3.1. 内核地址空间的分配 这个情景描述如何在系统启动时分配整个系统中的第一个地址空间---内核地址空间,其中涉及到的主要函数包括:表1 内核地址空间分配中的主要函数函数名 文件名 功能描述 kvm_init uts/i86pc/os/Startup.c 内核虚拟地址空间的初始化工作 as_avlinit uts/common/vm/Vm_as.c 为地址空间分配所需的avl树 as_addseg uts/common/vm/Vm_seg.c 将segment添加到指定的地址空间上 as_setprot uts/common/vm/Vm_as.c 为指定区域设置映射分配内核地址空间的工作主要在kvm_init()函数中完成。这个函数的流程如图2所示。 图2 kvm_init()的函数流程图 kvm_init()首先为kas(内核地址空间的as结构)创建segment AVL树和守护页AVL树,该过程是由as_avlinit()函数完成的。然后kvm_init()会调用seg_attach()及segkmem_create()函数来为内核地址空间添加必要的段,如内核代码段,kvalloc段,内核debugger段。seg_attach()函数的主要实现工作是由as_addseg()函数完成的。as_addseg()函数的流程如图2所示。 图3 as_addseg()的流程图 在as_addseg()函数中,为newseg寻找插入点时,为了提高效率,并没有直接利用avl_find()来寻找插入点,而是先利用as的a_lastgaphl,该域存放的是as_gap()函数最近使用的segment。如果a_lastgaphl不为空,那么则以它作为初始点,为newseg寻找合适的插入点,如果找到符合条件的插入点,则调用avl_insert_here()函数将newseg插入到AVL树中;如果a_lastgaphl为空,则利用avl_find()等函数来寻找符合条件的插入点。最后还需要判断所找到的插入点表示的段与newseg段是否有重合,如果有重合,在sparc处理器下就需要调用seg_unmap()函数取消插入点所表示的段的映射,之后调用avl_insert()函数将newseg插入到AVL树中。 利用seg_attach()及segkmem_create()函数来为内核地址空间添加好内核代码段,kvalloc段及内核debugger段之后,kvm_init()调用as_setprot()来对Red Zone、内核代码段及内核数据段设置相应的访问权限。在这里,为了确保Red Zone域不为访问,将其权限置为0;将内核代码段的权限置为可读/可写/可执行;将内核数据段的权限置为可读/可写/可执行。图4将给出as_setprot()函数的流程: 1.1.3.2.虚拟地址空间的释放 这个情景描述如何释放一个虚拟地址空间,其中涉及到的主要函数包括:表2 虚拟地址空间释放中的主要函数函数名 文件名 功能描述 as_free uts/common/vm/Vm_as.c 释放虚拟地址空间 hat_free_end uts/i86pc/vm/Hat_i86.c 进程地址空间正被销毁,该函数将销毁相应的hat xhat_free_end_all uts/common/vm/Xhat.c 销毁相应的xhat 释放虚拟地址空间的工作主要在as_free ()函数中完成。这个函数的流程如图4所示: 图4 as_free()的流程图 as_free()函数将释放一个地址空间,它的具体过程: 1. 它会调用as_do_callback()函数将所有的回调函数执行完毕,将回调函数列表清空 2. 进行hat结构释放的开始工作 a) 首先将as的flag置为AS_BUSY,以阻止新的XHAT被附到as上 b) 调用hat_free_start来设置hat_flag为HAT_FREEING(在销毁HAT时,需要设置该标志),为释放hat做好准备 c) 调用xhat_free_start_all,主要的工作就是将xhat链表中的holder都设为curthread,为释放xhat列表做好准备 3. 释放as中的segment a) 利用SEGOP_UNMAP来取消对as中各段的映射,并会判断其返回值err。若err=0表明正常执行;否则err==EAGAIN(表明当前段资源不可用),出现该err有两种情况:callback未处理完,内存被加锁,要进行等待。并回到步骤1重新进行。 4. 进行hat结构释放的结尾工作 a) 调用hat_free_end来真正清除相应的hat结构 确保hat当前没有被page table stealing,然后从hat链表中删除hat结构,并重置kas的hat链表 将所有htables都释放 利用kmem_cache_free来释放hat结构所占用的cache b) 调用xhat_free_end_all来释放XHAT 5. 释放as结构本身所占的空间 a) 释放object directory(procfs)中的vnode b) 利用kmem_cache_fee释放as结构 1.1.3.3.虚拟地址空间的复制 这个情景描述如何复制一个虚拟地址空间,其中涉及到的主要函数包括:表2 虚拟地址空间复制中的主要函数 函数名 文件名 功能描述 as_dup uts/common/vm/Vm_as.c 复制虚拟地址空间 seg_alloc uts/common/vm/Vm_seg.c 分配一个段,并将它附到相应的地址空间上 hat_dup uts/sfmmu/vm/hat_sfmmu.c 复制地址空间的地址转换 xhat_dup_all uts/sfmmu/vm/Xhat.c 复制地址空间的xhat列表复制虚拟地址空间的工作主要在as_dup ()函数中完成。这个函数的流程如图2所示: 图5 as_dup流程图 as_dup将复制一个地址空间,具体过程如下: 6. 调用as_alloc()为新的地址空间分配一个与之对应的as数据结构 7. 将原来地址空间所有的段都拷贝到新的地址空间中(利用一个循环) a) 调用seg_alloc()来分配一个段,并将这个新分配的段映射到新的地址空间中 b) 如果上面的seg_alloc()工作失败,则会调用as_free()将新的地址空间释放 c) 调用SEGOP_DUP()将原来段的操作复制到刚刚分配好的段中 d) 若复制段操作时发生错误,需调用seg_free()将刚刚分配的段释放,然后调用as_fee()将新的地址空间释放 8. 调用hat_dup()进行hat的复制工作 9. 调用xhat_dup_all()进行xhat的复制工作这里需要指出,hat_dup()和xhat_dup_all()只进行了一些字段取值的验证,并没有去进行实际的复制工作,这里hat和xhat都采用copy-on-write的策略完成复制。也就是说,只有真正去使用hat或xhat时,才会去进行复制工作。 1.2. 匿名内存 1.2.1. 概述 Solaris匿名内存是由segvn管理的,但并不直接与文件相关联,匿名内存用于进程的栈,堆以及写入时拷贝页。匿名页是通过anon层接口被创建的。当一个段第一次收到一个页错误,它分配一个anon映像结构(该结构说明anon头部在哪里)并在匿名映射的amp域里置入指向该anon头部的指针。然后分配插槽数组,需要足够大以至于能放下段内潜在的其他页。插槽数组采用一次间接寻址和两次间接寻址(一维数组和二维数组),这取决于需要插槽的数目。 32位系统由于要支持大于16MB的段,需要两次间接寻址;64位系统,因为指针的长度更长,当支持大于8MB时需要两次间接寻址。当只用一次间接寻址时,anon头部的 anon_chunk直接引用了anon的插槽数组。当我们使用两次间接寻址时,anon_chunk被分成两大块:针对32位系统,由2048个插槽组成的插槽块和针对64位系统,由1024个插槽组成的插槽块。这一分配过程由anon层接口anon_create实现。每一个anon插槽指向一个anon结构,anon结构描述了与地址空间内一页大小的区域内容一致的虚拟内存页。 使用匿名内存会有很多的优点。例如,当创建一个进程时,被创建的进程的所有地址都映射到物理内存的相同位(相同的页)。但是如果子进程在此时要对内存做些不同的操作(例如子进程管理内存中的一个数组),vm子系统会将这些页复制,并在子进程中改变映射指向新的页。这些新页即为匿名内存,子进程可以正常的修改数组,而不必知道这个数组已经有了一个新的物理内存了。这保证了存储部分对子进程的透明。 1.2.2. 数据结构 1.2.2.1.重要数据结构之间的关系 1.2.2.2.anon结构 struct anon { struct vnode *an_vp; /* vnode of anon page */ struct vnode *an_pvp; /* vnode of physical backing store */ anoff_t an_off; /* offset of anon page */ anoff_t an_poff; /* offset in vnode */ struct anon *an_hash; /* hash table of anon slots */ int an_refcnt; /* # of people sharing slot */ }; 每个匿名页,不论其在内存还是在对换区,都有一个anon结构。这个结构(也称为插槽)提供了匿名页和其后备存储器之间的一个间接级的映射。(an_vp,an_off)为这个插槽指向匿名页的vnode,(an_pvp,an_poff)指向这个插槽代表的页的物理存储器的位置。An_hash是anon插槽的一个散列表。这个列表由相关的匿名页的(an_vp,an_off)进行散列,并且提供一个方法用于从一个匿名页转到相关的匿名anon插槽。An_refcnt用于一个引用计数,记录了在写入时拷贝的情况下,需要建立的各个分散的拷贝的数目。一个大于零的refcnt保护了插槽的存在。在anon_alloc被调用时,refcnt初始化为1 。 1.2.2.3.anon_hdr结构 struct anon_hdr { kmutex_t serial_lock; /* serialize array chunk allocation */ pgcnt_t size; /* number of pointers to (anon) pages */ void **array_chunk; /* pointers to anon pointers or chunks of */ /* anon pointers */ int flags; /* ANON_ALLOC_FORCE force preallocation of */ /* whole anon array */ }; anon数组指针在块中分配。每个块都有anon指针的pagesize/sizeof(u_long*),anon_hdr结构指向匿名数组,控制着匿名插槽的分配。 anon数组是二维指针数组比一个块要大。第一级指针指向anon数组的块,第二级包含anon指针的块。如果anon数组比一个块要小则建立整个的anon数组。如果anon数组比一个块要大则仅仅第一维数组被分配。则另一维数组只在它们被anon指针初始化时分配。serial_lock,分配一个连续的数组块。Size,记录指向匿名页的指针的数目。array_chunk,指向anon指针的指针。 1.2.2.4.anon_map结构 struct anon_map { krwlock_t a_rwlock; /* 保护anon_map结构和anon数组*/ size_t size; /* anon数组大小*/ struct anon_hdr *ahp; /*anon数组的头指针, 包含anon指针数组*/ size_t swresv; /* 为anon_map结构而保存的交换空间 */ uint_t refcnt; /* 这个结构的引用计数 */ ushort_t a_szc; /*在共享的进程中最大的 szc */ void *locality; /* lgroup locality info */ }; 这是匿名内存中最核心的数据结构。 Anon_map结构用于各种各样的anon从客户端来进行匿名内存的管理。当匿名内存被共享时,不同的共享客户端将会指向同一个anon_map结构。同样的如果一个段在anon_map结构存在中间没有被安排,则新创建的段将仍会共享anon_map结构,尽管这两个段用到的是anon数组的不同范围。 1.2.3.情景 1.2.3.1.匿名内存的分配 所用的函数: anonmap_alloc()为给定交换区的相关段分配并初始化一个anon_map结构. anon_create()创建指针列表. anon_get_slot()从列表中返回指定的匿名索引的指针。 anon_alloc()分配一个anon插槽,上锁后返回该插槽. Anon_zero()分配一个私有的用零填充的anon页 Anon_set_ptr()用一给定的指针设置列表项,该指针为一指定偏移量的指针. 流程图如下: 详细的说明: 1. 进入anonmap_alloc()函数,为给定交换区的相关段分配并初始化一个anon_map结构 2. 进入anon_create()函数,来创建指针列表,指向匿名数组(一维或二维) 3. 创建工作结束,进入匿名数组的操作,首先通过page_get_pagecnt()函数来得到可用匿名插槽的数目,如果可用插槽数目为0则将其标记为忙碌,若大于0则调用anon_array_lock()来锁定插槽区域,获取第一个插槽,并将此插槽置位忙碌,之后用anon_get_slot()来获取一个匿名插槽。 4. 调用Anon_zero()来分配一个私有的用零填充的anon页,在此函数中,用anon_alloc()来分配匿名插槽和它的锁。通过[vp,offset]来寻找页(page_lookup()),没有找到,所以要调用page_lookup_create()来建立一个页,并将其置位vp,offset。 5. 从页空闲列表中根据所给的vp和offset选择一个最合适的页(page_get_freelist()),并将此页上锁(page_trylock()),并将此页从空闲列表中移出(page_sub()),插入到散列表中(page_hashin())。请求此页的i/o锁(page_io_lock()),并将此页插入到引用列表中(page_add)。 6. 调用Anon_zero()来分配一个私有的用零填充的anon页,在此函数中,用anon_alloc()来分配匿名插槽和它的锁。通过[vp,offset]来寻找页(page_lookup()),没有找到,所以要调用page_lookup_create()来建立一个页,并将其置位vp,offset。 7. 从页空闲列表中根据所给的vp和offset选择一个最合适的页(page_get_freelist()),并将此页上锁(page_trylock()),并将此页从空闲列表中移出(page_sub()),插入到散列表中(page_hashin())。请求此页的i/o锁(page_io_lock()),并将此页插入到引用列表中(page_add)。 8. 将此页添0(pagezero()),将此页锁的等级从独占锁降为共享锁(page_downgrade()). 9. 若有匿名页,也将此匿名页添0(anon_zero()),并用给定的指针设置列表项(anon_set_ptr()),通过hat层从实际的内存空间中分配此页(在对换区域中)。退出匿名内存数组的操作(anon_array_exit())。 10. 至此,所有的操作完成。一个问题要注意:为什么anon结构要设置成一个数组?答案:每个anon结构代表了一个内存页。而每一个段可能比一页的大小要大,所以需要不止一个的anon结构去描述它,所以我们需要一个数组。 1.2.3.2.匿名内存的释放 涉及到的函数: anonmap_free() 释放anon结构 anon_release() 释放匿名数组的指针 lgrp_shm_policy_fini()为anon_map结构释放共享内存的决策树和为零的本地空间。此函数在Lgrp.c文件中。流程图: 详细解释: 1. 判断anon_map结构中的ahp指针是否为空,若为空,则说明此anon页已经被释放,错误。若不为空则进入下一步。 2. 判断匿名页是否还有进程在引用,若refcnt=0则说明没有进程在引用,则可以释放。若refcnt!=0则错误。 3. 释放在物理内存中的页面 4. 释放匿名数组,要分为两种情况,即一维数组和二维数组的情况。 5. 清空对象缓存。 1.2.3.3. 分配一个anon插槽 函数anon_alloc() 流程图: 详细说明:anon_alloc参数为vnode和其偏移。首先声明一个anon结构类型变量ap,ap是一个anon插槽。为ap在缓存中分配空间,若失败则调用swap_alloc进行分配,若成功则将ap的两个分量an_vp和an_off置为参数的值,并将ap的其他分量都初始化。根据an_vp和an_off进行散列,并插入散列表中并上锁,最后返回指针ap。 1.2.3.4.匿名数组的拷贝 函数:anon_copy_ptr():拷贝anon数组sahp到另一个anon数组dahp. 流程图: 详细过程:拷贝anon数组sahp到另一个anon数组dahp。几个参数的意义,s_idx指sahp数组块的大小,d_idx指dahp数组块的大小,npages指复制需要的页面数。函数的执行过程:如果两个数组都是一维的,要对sahp和dahp所指向的空间进行测试,(1)如果其小于ANON_CHUNK_SIZE的值才是合法值,然后调用bcopy函数对数组进行复制。(2)如果两个数组都是二维的则相对要麻烦一些。同一维数组一样,首先要测试sahp和dahp所指向的空间的大小是否合法,然后不断测试npage的值,当其值大于零的时候则进入循环,相当于调用若干次的bcopy对若干个一维数组进行复制,最终得到二维数组的复制。(3)如果至少有一个数组是二维的,则在npage大于零的情况下,要求anon索引的指针不能为空,用anon_get_ptr函数得到索引指针并且放在ap中,并通过anon_set_ptr函数直接复制给dahp。 1.2.3.5.设置指针数组结构 函数:anon_create() 流程图: 详细说明:这个函数可以分配和回收指针,返回和设置给定的偏移的指针数组的入口。首先在内核内存中分配数组指针的空间,对数组指针的互斥锁进行初始化。若数组是一维的,整个指针数组占npages个页,则在内核内存中分配这npages个页的anon指针的空间(用函数kmem_zalloc),若没有分配成功则调用kmem_free寻找内核中的空闲区。若是二维数组,则只是在计算指针数组大小的时候麻烦一下,其余均一样。最后返回这个数组的指针(anon_hdr*类型)。这里要说一下kmem_free函数的算法,将内核内存中空闲的块建立为一个堆,从根部开始在其中查找与给定块最相近的空闲块,如果找到,则将给定的块写入到空闲块中。 1.2.3.6.匿名页引用计数的操作 函数:Anon_decref():对anon页的引用减1,如果引用计数为零,则将其和其相关页释放。流程图: 详细过程:在散列表中找到ap指向的匿名页,并通过临界区访问此页,以保证互斥。(1)若引用此页的计数不为零,则对其进行减1操作,如果减1以后对此匿名页的引用变为零,则调用函数swap_xlate(),将一个匿名插槽转换为其相关的vnode和vnode中的偏移,并查找此匿名插槽的页,如果找到,则调用VN_DISPOSE函数将与之相关的匿名页归还到空闲列表中,以表示此页真正成为空闲页了。(2)若引用计数为零,则直接从散列表中将ap所指的匿名页移除,如果ap_pvp所指的物理页不为null则也要调用函数swap_phys_free将物理页释放。 1.2.3.7.匿名内存中数据的复制函数:anon_dup():复制anon页size字节长度的内容。流程图: 详细过程:首先,调用btopr函数将要复制的长度size转换为以页为单位的度量即npages个页。每复制一页就将npages减1,找到index处开始第一个合法的anon指针赋给ap。然后,调用anon_set_ptr函数将old的内容复制到new处,并对每一个匿名页的引用数都加1。函数中用off来记录每页中的偏移,进行复制。须注意,对页的引用计数进行修改时要保证互斥进行,代码如下: mutex_enter(ahm); ap->an_refcnt++; mutex_exit(ahm); 1.2.3.8.释放一组anon页 函数:anon_free():释放一组anon页,长度为size字节,并清空指向这些anon表项的指针。流程图: 详细过程:首先将size字节的长度转换为以页为单位,npages个页。即转换为释放这npages个页的函数。找出index后的第一个合法的anon指针ap,ap指向的是要释放的第一页。将此页的列表项设置为null,调用anon_decref函数减少一个此页的引用计数。当该引用的计数为0时,释放该页和与之关联的页(如果有的话)。将npages减1再进入循环,直至npages等于零,表明所有的匿名页已被释放。 1.3.Swap文件系统 1.3.1.概述现代操作系统都实现了“虚拟内存”这一技术,在功能上突破了物理内存的限制,使程序可以操纵大于实际物理内存的空间。有两个基本的虚拟内存管理模型:交换(swapping)和按需换页(demand pages)。交换模型的内存管理粒度是进程,当物理内存不足时,最不活跃的进程被换出内存。按需换页的内存管理粒度是页面,在内存匮乏时,只有最不经常使用的页面被换出。Solaris中使用了这两种虚拟内存管理方法。通常情况下使用按需换页方式,在内存严重不足的时候采用交换的方式。在进行虚拟内存管理的时候,在系统的磁盘空间中,必须专门划分出一个部分作为系统的Swap空间。Swap空间的作用可简单描述为:当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。需要声明的是,并不是所有从物理内存中交换出来的数据都会被放到Swap空间中,有相当一部分的数据直接交换到操作系统的文件系统中了。例如,有的程序会打开一些文件,对文件进行读写(其实每个程序都至少打开一个文件,那就是运行程序本身),当这些程序的内存空间需要交换出去时,文件部分的数据就没有必要放到Swap空间中了,如果是读文件操作,那么内存数据直接就释放了,不需要交换出来,因为下次需要时,直接从文件系统就能恢复;如果是写文件,只需要将变化的数据保存到文件中,以便恢复。但是那些用malloc( )和new等函数生成的对象的数据则不同,需要Swap空间,因为它们在文件系统中没有相应的“储备”文件,因此被称为“匿名”(Anonymous)的内存数据,这类数据还包括堆栈中的一些状态和变量数据等,所以说,Swap空间是“匿名”数据的交换空间。内存的每个物理页面都由它的vnode和offset指定。当所要寻找的页面不在内存中时,则vnode和offset指出该页在后备存储器中的位置。对于一个文件来说,物理页缓存了文件的vnode和offset.交换空间就像一个后备存储器,存储内存的匿名页,因此,当内存不足时,可以把内存的某页交换到磁盘中,以增加内存空闲空间。因为交换空间是作为匿名内存的后备存储器,所以我们必须首先确定是否有足够的交换空间,以便可以把页面交换出去。因此,在创建一个可写入的映象前,我们要先申请交换空间。当有足够的内存空间可装入进程的内容时,Solaris内核允许匿名内存可不申请交换空间。这意味着在某些情况下,一个系统可以在很少或没有交换空间下运行。通常情况下,Swap空间应大于或等于物理内存的大小,最小不应小于64M,通常Swap空间的大小应是物理内存的2-2.5倍。对于传统的UNIX,在可写入的虚拟内存中,其每个页大小的单元都需要一个同等大小的交换空间。比如,在传统的UNIX系统里,一个malloc要求分配8 Mbytes空间,那同时,它也要申请8 Mbytes的交换磁盘空间,尽管这些交换空间它可能从未用到。一般地,粗略估计进程所要的空间的大小是其所使用的物理页的两倍,这就导致了交换空间要等于两倍大小的内存空间。而swapfs层允许Solaris在分配时更加谨慎,我们只需要让交换空间等于虚拟内存的大小即可,该虚拟内存比机器中可用的可分页物理内存大。 Solaris的交换使用swapfs来实现交换区域分配,以提高空间的使用效率。swapfs文件系统位于anon层和物理交换设备之间,是一个虚拟的文件系统。即使没有分配物理交换空间,swapfs文件系统也使每个页面如同有真实的后备交换空间。 swapfs文件系统使用一个全局变量:availrmen来跟踪系统中可用且可分页的物理内存,并把添加到可使用的交换空间中。当我们申请虚拟交换时,我们只是简单地减少了可用的虚拟内存的总量。只要有充足的内存和物理交换空间可用,则该交换分配就是成功的。物理交换空间直到需要使用时才会分配。当我们创建一个私有段时,我们申请交换分区和分配anon结构。在这状态中,直到一个真正的内存页作为ZFOD或copy-on-write的结果被创建时,anon结构的分配等才会真正发生。当一个物理页默认加入时,用vnode/offset对它进行标记。在Solaris中,当段驱动调用anon_alloc()去获取一个新的匿名页时,该匿名页被分配swapfs vnode和offset。anon_alloc()函数通过swafs_getvp()进入swapfs,然后调用swapfs_getpage()创建一个带有vnode/offset的新页面。an_vp和an_off被初始化成swapfs虚拟交换设备的vnode和offset,这两个an_vp和an_off是在anon结构中用来标明该页的后备存储的地址的。 在没有page-out请求之前,并不需要任何物理交换空间。当段请求虚拟交换空间时,可用的虚拟交换空间的总量减少,但是,因为我们还不需要把页置换到物理交换区,所以,物理交换空间并没有被分配。当发生第一个page-out请求时,真正的交换分区才被分配。这时,页扫描器为该页检查vnode,然后调用putpage()方法。因为该页的vnode是swapfs vnode,因而调用swapfs_putpage()把该页置换到交换设备。swapfs_putpage()分配物理交换分区的一个页大小的块给该页面,然后把anon槽中的vnode an_pvp和an_poff设置为指向物理交换设备,接着该页就被置换到交换设备中了。 另外,系统中有可能包含多个Swap分区,分区的数量对性能也有很大的影响。因为Swap交换的操作是磁盘I/O的操作,如果有多个Swap交换区,Swap空间的分配会以轮流的方式操作于所有的Swap,这样会大大均衡I/O的负载,加快Swap交换的速度。 1.3.2.数据结构 1.3.2.1.重要数据结构关系 1.3.2.2.swapinfo 每个交换空间都有一个swapinfo结构来记录该区域的相关信息。这些结构连成一个线性表,决定交换空间在逻辑交换设备中的顺序。每个结构包含一个指针,指向相应的位图,同时也说明该交换空间的大小,和它相应的vnode。Swapinfo结构在/uts/common/sys/swap.h中,它的定义如下: struct swapinfo { ulong_t si_soff; /*指明文件开始的偏移量*/ ulong_t si_eoff; /* 指明文件结束的偏移量*/ struct vnode *si_vp; /*指向一个结点*/ struct swapinfo *si_next; /*指向下一个交换空间 */ int si_allocs; /*该交换空间的分配结果*/ short si_flags; /* 标记*/ pgcnt_t si_npgs; /* 交换空间的页面数 */ pgcnt_t si_nfpgs; /* 交换空间的空闲页面数*/ int si_pnamelen; /*交换文件的名字长度加一 */ char *si_pname; /* 交换文件的名字*/ ssize_t si_mapsize; /*为位图分配的字节 */ uint_t *si_swapslots; /*插槽的位图,未置位标明该插槽为空*/ pgcnt_t si_hint; /*空闲页的第一页 first page to check if free */ ssize_t si_checkcnt; /*寻找空的插槽 # of checks to find freeslot */ ssize_t si_alloccnt; /* 用来获取ave结果used to find ave checks */ }; ulong_t si_soff和ulong_t si_eoff分别指明文件在vnode中的开始地址和结束地址。 vnode *si_vp指向某设备的vnode。 swapinfo *si_next指向下一个交换空间,从而把所有的交换空间串连起来。 si_allocs指出交换空间的分配结果,si_flags是一个标记,以后会有详细的定义。 pgcnt_t si_npgs和pgcnt_t si_nfpgs分别指出交换空间的页面数和空闲页面数 si_pnamelen的值为交换文件的名字长度加一 si_pname指出交换文件的名字 si_mapsize指出交换空间对应的位图的比特数,系统中,每个物理交换空间都有一个相对应的位图,该位图用来表示它的物理容量。位图记录了哪个交换槽已经被使用或尚未使用。分配时是通过遍历位图来找到第一个空闲槽。因而,在交换设备中的偏移量和插槽所支持的页面地址之间不存在线性关系。相反,这是一个一对一的映像。 si_swapslots插槽的位图,未置位则标明该插槽可用。 si_hint指出空闲页的第一页; si_checkcnt寻找空的插槽 si_alloccnt用来获取ave结果,ave为一个宏,表明空间分配了多长时间。 1.3.2.3. swapre swapre结构指出将要进入或移出交换空间的资源的路径。Swapre结构在/uts/common/sys/swap.h中,其定义如下: typedef struct swapres { char *sr_name; /* 特定资源的路径名*/ off_t sr_start; /*被交换资源的起始偏移地址g*/ off_t sr_length; /*交换空间的长度 */ } swapres_t; sr_name指出将要移入或移出交换空间的资源的路径名称 sr_start 指出被交换资源的起始偏移地址 sr_length指出交换空间的长度 1.3.2.4.swapent swapent结构保存交换文件(相当于交换设备,暂存换出页面)的名字,将要进行交换的页数等,它在/uts/common/sys/swap.h中,其结构定义如下: typedef struct swapent { char *ste_path; /* 获取交换文件的名字 */ off_t ste_start; /* 交换的起始块*/ off_t ste_length; /*交换空间的长度 */ long ste_pages; /* 可交换的页数*/ long ste_free; /*空闲的交换页数 */ int ste_flags } swapent_t; ste_path获取交换文件的名字 ste_start指出交换的起始块 ste_length交换空间的长度 ste_pages可以进行交换的页数 ste_free 空闲的交换页数 1.3.2.5.swaptable swaptable结构是一个数组,存储swapent,指出有多少个交换文件。它在/uts/common/sys/swap.h中,其结构定义如下: typedef struct swaptable { int swt_n; /*指出有多少个交换文件*/ struct swapent swt_ent[1]; /* array of swt_n swapents */ } swaptbl_t; 1.3.3.情景 1.3.3.1.从设备中分配交换页 表6 添加交换文件时用到的主要函数函数名 文件名 功能描述 swap_phys_alloc uts/common/vm/Vm_swap.c 分配指定大小的连续页 swap_getoff uts/common/vm/Vm_swap.c 获取设备中空闲页的开始偏移量从物理设备中分配交换页是通过函数swap_phys_alloc来完成的, 这个函数的流程如图所示: 图11 swap_phys_alloc函数流程图 分配指定大小的,连续的物理交换页。分配成功返回1,若一个页面都分配不了,则返回0。首先对swapinfo表加锁。然后在列表中逐个搜索,搜索有空闲页的设备。如果调用者表明交换页不从某个设备中分配,则交换页应从其他设备中分配,于是应寻找不相同的设备,如果找到,转到found;如果调用者没有这种需求,则直接转到found。sip指向所找到设备的swapinfo。调用swap_getoff,找到设备空闲页的偏移地址soff。设备的空闲页数减掉1。如果soff为-1,出错。开始从设备中分配所需的页面数。若分配过程中,设备的页已分配完或剩下的比特数不够一页大小,则完成分配过程,尽管可能还未分配够要求的页数。把设备的vnode,偏移量soff,刚才分配的比特数len等返回给调用者。如果设备分配的交换页数超过swap_maxconfig(一个宏,在anon_init中定义其大小),为了把负载平衡分配到各个设备,把该设备的si_allocs设为0,并且如果该设备排在链表的最后,把silast的值改为swapinfo链表的开头结点。保存分配的信息,完成程序。 1.3.3.2.释放物理交换页 表7 释放物理页时用到的主要函数 函数名 文件名 功能描述 swap_phys_free uts/common/vm/Vm_swap.c 释放指定设备的物理页从指定设备中释放页是通过函数swap_phys_free来完成的, 这个函数的流程如图所示: 图12 swap_phys_free函数流程图释放一个物理页。调用者给出欲释放页所在设备的vnode,页的偏移量和页大小。调用者给出的欲删页大小,并不符合设备中页的规格,即调用者眼里的一页和设备中的一页大小是不一样的。 首先,在swapinfo链表中开始查找。如果某个结点的vnode和指定vnode相同,且给出的偏移量超出结点范围,则计算在指定偏移量之前的页数pagenumber, 而npage是pangnumber和释放页数之和。 释放每一页。如果某页对应的位图表示该页未曾使用,则打印:“释放空闲页。”把该页对应的位图清0,设备的空闲页数加一。 1.3.3.3.释放一个正在使用的交换页 表8 释放一个正在使用的交换页用到的主要函数函数名 文件名 功能描述 swapslot_free uts/common/vm/Vm_swap.c 释放指定设备的正在使用的物理页 VOP_GETPAGE /on/usr/src/uts/common/sys/vnode.h 找到指定页,并设置该页属性 swap_anon uts/common/vm/Vm_swap.c 找寻和指定vnode,off相关的anon swap_phys_free uts/common/vm/Vm_swap.c 释放指定的交换页页 hat_setmod /on/usr/src/uts/common/vm/hat.h 为指定页设置指定的属性从设备中释放正在使用的交换页是通过函数swapslot_free来完成的, 这个函数的流程如图所示: 图13 swapslot_free函数流程图释放某个设备正被使用的交换页,调用者需提供该设备的vnode,欲释放页在设备中的偏移量,和设备的swapinfo结点。使用VOP_GETPAGE获取欲释放的页,如果VOP_GETPAGE返回错误信息,转出错处理。对找到的页加锁,添加互斥量。找到欲释放页所在的anon结构,把anon结构返回给ap;如果找不到相应的anon,出错处理;如果ap的后备存储的vnode和指定设备的vnode相同,且ap在设备中的偏移量没超出设备的范围,则调用swap_phys_free释放该页。把ap的an_pvp和an_poff置为空,调用hat_setmod对该页属性进行设置。 1.3.3.4.添加交换文件 表4 添加交换文件时用到的主要函数 函数名 文件名 功能描述 swapadd uts/common/vm/Vm_swap.c 增加新的交换设备到列表中,并在更新anoninfo计数器前转移分配给它 common_specvp uts/common/fs/specfs/specsubr.c 对给定设备的vnode,函数返回和该设备vnode相关联的通用vnode格式 mutex_enter /on/usr/src/lib/libzpool/common/kernel.c 对文件上锁 mutex_exit /on/usr/src/lib/libzpool/common/kernel.c 对文件开锁 kmem_zalloc /on/usr/src/uts/common/os/kmem.c 该函数在这里为swapinfo 结构分配指定大小的内存空间添加交换文件是通过函数swapadd来完成的,这个函数的流程如图 7所示: 图 8 swapadd函数流程图 交换文件是用于交换的有固定长度的、规则的文件,交换设备相当于磁盘分区,交换设备和交换文件的作用一样,设备挂靠到系统上时,系统就把设备当作一个文件来操作,添加交换文件可看作添加设备,以下的交换文件和交换设备都可统一看作交换文件。系统中所说的块和页其实是一样的,都是页的意思。 在函数的开始,先把交换设备的vnode转化成文件系统通用的的vnode格式,这通过函数common_specvp实现。common_specvp把返回的vnode赋给cvp,cvp就表示了欲添加设备的vnode。 改变设备vnode的 标志位。先对设备加锁,然后计算wasswap和vnode的v_flag,其中,wasswap表明设备是否是交换设备,计算完后开锁。对互斥量swap_lock加锁,调用VOP_OPEN打开欲添加的设备,如果成功,VOP_OPEN会为设备返回一个新的vnode;如果打开不成功,则恢复刚才改变的wasswap值。开锁。获取交换设备的属性,如果设备的大小为零,或无法确定大小,则进行出错处理。如果设备大小超过系统所能接受的范围,则把它的大小限定为MAXOFF32_T(0x7fffffff),因为32位的操作系统不支持超过32位寻址的交换设备。判断该交换设备是否可写入。这通过VOP_SETATTR设置设备属性来决定,如果成功设置,则设备可写,否则进入出错处理。判断设备是否可进行页I/O,如果不支持文件系统操作,则转出错处理。一般地,如果在根文件系统上进行交换,不要把和miniroot文件系统相应的交换块放在空闲的交换列表中。因此,如果加入的设备作为根文件,则可用块的起始地址要进行计算,具体前面多少块不能用由klustsize(外部定义)决定。如果设备不是根文件,则起始地址由调用者指定,如果未指定,则从第二张页面开始,因为第一张页面存储设备的标志。计算开始的偏移地址soff,如果大于设备的大小,则转出错处理。计算尾端的偏移地址eoff,如果大于设备大小,转出错处理。开始的偏移量soff和尾端的偏移量eoff进行页面对齐,如果soff>=eoff,转出错处理。调用kmem_zalloc函数分配给swapinfo结构相应大小的内存。分配的空间的指针返回给nsip,则nsip指出交换设备的相关交换信息。对nsip的部分变量进行赋值:它的vnode,起始偏移量,尾端偏移量,对应的设备名称。。。计算设备插槽(插槽相当于页)相应位图需要的字节数,给位图分配相应的内存,并对每一位进行置位,然后检查是否可以把交换设备添加到系统中。首先对swapinfo进行加锁,由mutex_enter(&swapinfo_lock)完成。接着检查全局变量swapinfo中是否已有该设备的vnode,如果找到的vnode和欲添加的vnode的偏移量一样,只是先前被删去的,把它恢复即可,然后解锁,跳出程序;如果找到的vnode的偏移量的范围被设备的偏移量完全覆盖,则出错。把设备加到列表中。判断k_anoninfo中申请的页数是否大于加上锁的页数,如小于,出错;再判断k_anoninfo中页数的总量是否大于申请的数目,如果小于,出错处理。把设备的页数加到k_anoninfo的总数上,然后把cpu中相关线程的ani_count加上设备的页数。如果在k_anoninfo中申请的页面数大于已加锁的,说明有一些申请为满足,现在加入了一个设备,可把这个设备的页面分配给请求者。如果系统中尚未有备份装置,把刚添加的设备初始化为备份装置。 1.3.3.5.删除交换文件 表5 删除交换文件时用到的主要函数 函数名 文件名 功能描述 swapdel uts/common/vm/Vm_swap.c 删除某个设备删除交换文件是通过函数swapdel这个函数的流程如图 9所示: 图 10swapdel函数流程图 删除交换文件是通过swapdel函数来完成的,调用者必须传给swapdel欲删除文件的vnode和删除区域的起始块地址。首先,函数把设备文件的vnode转换成文件系统通用的vnode格式,这通过函数common_specvp实现。common_specvp把返回的vnode赋给cvp,cvp就表示了欲删除设备的vnode。进行页面对齐,获取设备的开始偏移量soff。对全局变量swapinfo上锁,在swapinfo中寻找设备的vnode。如果未找到,转出错处理。设备的信息赋给osip变量。对匿名内存k_anoninfo结构的信息进行判断。如果申请的内存页数小于被锁的内存交换页数,跳出程序;如果可申请的磁盘交换页数小于已申请的磁盘交换页数,跳出程序。如果系统中的所有的空闲页数目小于欲删除设备的页数目,说明设备正在被使用,或系统无法腾出空间装载在删除设备中的内容,转出错处理。如果删除设备后,请求的磁盘交换空间不足,可申请内存交换空间补足。先计算差额,然后从availrmem中分配内存。如果全局变量k_anoninfo中申请的内存交换空间小于被锁住的,跳出程序;如果可分配的磁盘交换空间小于申请的数目,跳出程序;从系统中减去所删除设备的页数。对设备信息Osip的的标志位进行置位,防止再从该设备分配交换空间。准备释放该设备的物理交换页。在系统中,每个匿名页都有一个anon结构。这个anon结构(slot)提供了匿名页和其对应后备存储的关系。对整个anon哈希表进行遍历,找出有交换页在欲删除设备的anon slot,更新anon slot。在每个页释放后,都要返回anon slot相应的桶的开始,因为在释放页的时候,并没有对整个哈希表加锁,所以在释放时哈希表可能会被别的进程改变。对哈希表一个个桶逐个遍历。首先,获取全局变量anon_hash的初始地址,然后对第一个桶进行分析,先对其加锁。然后找这个桶中的每一个anon结构,如果某个anon的后备存储为欲删设备的页,测试该页对应的位图情况,如果该页已被使用,则把该页对应vnode的v_count加1,把该页从slot中释放,然后把该页对应的v_count减1。如果释放成功,返回当前桶,继续查找。如果释放失败,要把该页恢复,全局变量k_anoninfo和availrmem等的相关值要重新加上该页,然后转出错处理程序。遍历完哈希表后,判断是否完全完成释放,这时应有空闲页数和设备的页数相等,否则终止程序。把设备从swapinfo列表中删除。如果设备处于列表中的最后一个,则修改指针silast;释放设备对应位图的内存,释放设备swapinfo对应的内存。如果设备属于后备装置,释放。释放设备的vnode,程序完成。 1.4.物理页面管理 1.5.Vmem分配器 1.6.内核内存的初始化与布局 1.7.内核内存分配 1.7.1.概述 Solaris的内核内存分配器分成两个层次。下层是后备分配器,它负责从内核地址空间分配整块的内存供上层分配器使用,它分配的内存块大小一般是一个页或者是页的整数倍。上层是slab分配器,它从后备分配器那里获得整块内存,然后把整块内存分成小的内存块,分配给内核程序。一整块内存被称为一个slab,这也是slab分配器名字的由来。后备分配器是一个vmem分配器,这是一个通用的资源分配器。后面的章节会对vmem分配器进行分析,这一节主要是分析slab分配器。 slab分配器吸收了面向对象的思想,它可以直接给用户分配已经初始化好的对象,而且在释放对象的时候,它也会调用相应的析构函数来销毁对象。Slab分配器还采用了缓存的技术,每种类型的对象有一个单独的缓存,以前分配的对象在释放时并不是马上销毁,而是放回这个缓存中。以后再分配时,如果缓存中有空闲对象,就直接从缓存中分配,没有的话再去创建一个新对象。采用缓存的办法避免了每次分配对象都要进行一次初始化,从而加快了分配的速度。用户在创建对象缓存的时候需要指定对象的规格,包括对象的尺寸、对齐边界、构造函数和析构函数。除了可以分配初始化好的对象,slab分配器还实现了传统的内存分配接口,可以分配任意大小的未初始化内存。在多CPU平台中,不同的CPU在同时向内存分配器请求分配内存的时候会发生冲突。为了防止冲突破坏内存分配器的一致性,一个CPU在分配内存的时候必须对分配器加锁,防止其他CPU同时分配内存。这样的后果是不同CPU的内存分配操作必须顺序进行,后一个CPU必须在前一个CPU分配完后才能开始分配,这就会造成效率的降低。CPU数量越多,这个问题越严重。为了解决这个问题,Solaris为每一个CPU配置了一个自己的局部缓存。CPU在分配内存的时候,首先从自己的缓存中分配,如果自己的缓存已空再从全局缓存中分配,这样就降低了CPU冲突的概率。 CPU的局部缓存借用了自动步枪的原理,一个CPU是一支步枪,缓存中的对象是步枪的子弹,每分配一个对象就相当于打出一发子弹。子弹被放到弹夹(magazine)中,一个CPU的局部缓存中有两个弹夹。分配对象时如果所有弹夹都被打空,就从全局分配器中换上一个满弹夹。CPU释放一个对象的时候相当于又获得了一发子弹,它把这发子弹压入弹夹中,供以后使用。释放对象时如果CPU局部缓存中所有弹夹都是满的,则用从全局分配器中换上一个空弹夹。为了存放全局的弹夹,slab分配器又引入了一个depot层,这个层相当于一个弹药库。depot层中有两个链表,一个是空弹夹链表,一个是满弹夹链表,每个CPU从弹药库中换弹夹时就是对这两个链表进行操作。因此slab分配器可以看作是由三个层组成。最下层是slab层,它与页分配器进行交互,申请或释放整块的内存,并把整块内存划分成小块,供上层使用。中间是depot层,它是一个全局的弹夹管理器。最上面是CPU层,主要处理CPU的局部缓存。 Solaris中的slab分配器还考虑到了对CPU高速cache的影响。如果所有的对象都是从相同的对齐边界开始(例如512字节对齐边界),那么不同对象映射到同一个CPU缓存线(cache line)的概率就会增加,相应地也会增加cache的冲突和失效率,从而造成系统性能的下降。Solaris中引入了一个简单的染色机制来解决这个问题。在对象缓存中,一个slab中对象的起始地址由一个染色值(color)决定,染色值不同,对象的起始地址也不同,所以映射到同一个缓存线的概率就会降低。一个对象缓存有一组可用的染色值,在创建slab的时候,这组染色值被循环使用,使得具有相同染色值的slab数量尽可能少。这样就减少了cache冲突的次数,提高了系统的整体性能。 1.7.2.数据结构 1.7.2.1.重要数据结构间关系 图14 数据结构关系图 slab分配器中主要的实体是对象缓存。对象缓存的控制结构是kmem_cache,从kmem_cache出发可以访问到缓存相关的数据结构,如kmem_slab、kmem_magazine等。系统中所有的对象缓存被串成一个双向链表,通过全局变量kmem_null_cache可以访问这个链表。一个对象缓存中可以包括多个slab,所有的slab链成一个双向循环链表,通过kmem_cache中的cache_freelist字段可以引用这个链表。slab的控制结构是kmem_slab。一个slab被分成多个对象,每一个对象用一个kmem_bufctl结构来控制。slab中空闲的对象链成一个单向链表,kmem_slab中的slab_head字段指向这个链表的表头。对象缓存中有两个弹夹的链表,一个是空弹夹链表,一个是满弹夹链表。弹夹的链表用kmem_maglist结构表示,链表中每一个弹夹用kmem_magazine结构表示。一个弹夹中可以装载多个对象,对每一个装载的对象,kmem_magazine中保存一个指向该对象起始地址的指针。每一个CPU有自己的局部缓存,这个局部缓存由kmem_cpu_cache结构控制。kmem_cache结构中有一个kmem_cpu_cache结构的数组,其中每一个元素表示一个CPU的局部缓存。每个CPU的局部缓存中包含两个弹夹,一个是当前装载的弹夹,一个是前一个装载的弹夹。 1.7.2.2.kmem_cache kmem_cache结构是对象缓存的控制结构,每一个对象缓存都对应一个kmem_cache结构的变量。kmem_cache结构在uts/common/sys/kmem_impl.h中定义,它的定义如下: struct kmem_cache { /* 以下变量用于统计 */ uint64_t cache_slab_create; /* slab创建的次数 */ uint64_t cache_slab_destroy; /* slab销毁的次数 */ uint64_t cache_slab_alloc; /* slab层分配的次数 */ uint64_t cache_slab_free; /* slab层释放的次数 */ uint64_t cache_alloc_fail; /* 分配失败的总次数 */ uint64_t cache_buftotal; /* 总的对象个数 */ uint64_t cache_bufmax; /* 出现过的最大对象个数 */ uint64_t cache_rescale; /* 重新调整hash表的次数 */ uint64_t cache_lookup_depth; /* hash查找的深度 */ uint64_t cache_depot_contention; /* depot层互斥冲突的次数 */ uint64_t cache_depot_contention_prev; /* depot层互斥冲突次数的前一个快照 */ /* 对象缓存的属性 */ char cache_name[KMEM_CACHE_NAMELEN + 1]; /* 缓存名称 */ size_t cache_bufsize; /* 对象大小 */ size_t cache_align; /* 对象的对齐边界 */ int (*cache_constructor)(void *, void *, int); /* 构造函数 */ void (*cache_destructor)(void *, void *); /* 析构函数 */ void (*cache_reclaim)(void *); /* 回收函数 */ void *cache_private; /* 构造函数、析构函数和回收函数的参数 */ vmem_t *cache_arena; /* slab的后备分配器 */ int cache_cflags; /* 缓存的创建标识 */ int cache_flags; /* 缓存的状态信息 */ uint32_t cache_mtbf; /* induced alloc failure rate */ uint32_t cache_pad1; /* to align cache_lock */ kstat_t *cache_kstat; /* exported statistics */ kmem_cache_t *cache_next; /* 系统中后一个缓存 */ kmem_cache_t *cache_prev; /* 系统中前一个缓存 */ /* Slab层 */ kmutex_t cache_lock; /* 保护slab层的互斥锁 */ size_t cache_chunksize; /* 对象在slab中的实际大小 */ size_t cache_slabsize; /* 一个slab的大小 */ size_t cache_bufctl; /* buf起始地址到kmem_bufctl的偏移 */ size_t cache_buftag; /* buf起始地址到kmem_buftag的偏移 */ size_t cache_verify; /* 需要验证的字节数 */ size_t cache_contents; /* bytes of saved content */ size_t cache_color; /* 创建下一个slab时用的染色值 */ size_t cache_mincolor; /* 可用的最小染色值 */ size_t cache_maxcolor; /* 可用的最大染色值 */ size_t cache_hash_shift; /* hash运算时地址移位的位数 */ size_t cache_hash_mask; /* hash表的掩码 */ kmem_slab_t *cache_freelist; /* 空闲slab链表 */ kmem_slab_t cache_nullslab; /* 空闲链表尾部的标记 */ kmem_cache_t *cache_bufctl_cache; /* 分配kmem_bufctls结构的缓存 */ kmem_bufctl_t **cache_hash_table; /* hash表的基地址 */ void *cache_pad2; /* to align depot_lock */ /* Depot层 */ kmutex_t cache_depot_lock; /* 保护depot层的互斥锁 */ kmem_magtype_t *cache_magtype; /* 弹夹类型 */ void *cache_pad3; /* to align cache_cpu */ kmem_maglist_t cache_full; /* 满弹夹列表 */ kmem_maglist_t cache_empty; /* 空弹夹列表 */ /* CPU层 */ kmem_cpu_cache_t cache_cpu[1]; /* CPU局部缓存控制结构数组 */ }; kmem_cache中的字段分成五个部分。第一个部分是用于统计的字段,系统每次执行与该缓存相关的操作时会更新这些字段,它们的值用于做系统负载、性能等方面的统计。第二个部分是表示缓存属性的字段。 cache_bufsize和cache_align保存了缓存中对象的大小和对齐边界。 cache_constructor、cache_destructor和cache_reclaim是回调函数,它们分别是构造函数、析构函数和回收函数,这些函数由用户在创建缓存的时候指定。cache_private是调用这三个回调函数时用到的参数。 cache_arena是比缓存低一级的分配器,缓存从这里获取整块内存(一般是页的整数倍)来创建slab,然后再划分成对象,释放slab时slab对应的内存也归还到这里。系统中所有的缓存通过cache_next和cache_prev字段链成一个双向循环链表,这个链表的表头是kmem_null_cache,这是一个kmem_cache类型的全局变量,定义在uts/common/os/kmem.c中。第三个部分是与slab层相关的字段。 cache_chunksize表示一个对象在slab中的实际大小。一个对象在slab中除了它本身要占用空间外,为了对齐还要占用一些空间,另外可能还会有一些调试信息,所有这些空间合称为一个chunk,而cache_chunksize就是它的大小。 cache_slabsize是缓存中一个slab的大小,这个大小与cache_chunksize有关。一般小对象(chunksize小于512字节)对应的slab大小都是一个整页(4K字节)。在slab中,如果一个小对象是空闲的,而且这个对象的大小可以放下kmem_bufctl和kmem_buftag结构,那么这个对象对应的内存块会被用来存放这两个控制结构。kmem_bufctl和kmem_buftag结构位于内存块的末尾,cache_bufctl和cache_buftag字段分别表示对象的起始地址到这两个结构的起始地址之间的距离。如果对象太大,使得一个slab可能跨越多个页,或者对象太小,不能放下kmem_bufctl结构,那么即便对象是空闲的,kmem_bufctl也不能放在对象所占的内存块中,而是要另外为kmem_bufctl分配一块内存。在为kmem_bufctl分配内存的时候,把kmem_bufctl也看作是小对象,用的是与其它对象相同的分配方法。cache_bufctl_cache字段就是指向用于分配kmem_bufctl结构的对象缓存。 cache_hash_table是一个kmem_bufctl结构的hash表。当kmem_bufctl结构不位于对象所占的内存块中时,就不能通过对象的起始地址计算出kmem_bufctl的地址。为了能快速找到对象的kmem_bufctl,Solaris为kmem_bufctl建了一个hash表,表的索引值就是对象的起始地址,这样就可以通过起始地址快速在表中它的kmem_bufctl。 kmem_hash_shift和kmem_hash_mask字段与hash函数有关。slab中hash函数的过程是先把kmem_bufctl的地址右移kmem_hash_shift位,然后再与kmem_hash_mask做与操作,得到的值就是kmem_bufctl结构在hash表中的索引。 cache_color、cache_mincolor和cache_maxcolor这三个字段用于对slab进行染色。cache_color是创建下一个slab时用到的染色值,cache_mincolor是可用的最小染色值,cache_maxcolor是可用的最大染色值。一个对象缓存中所有的slab串成一个双向循环链表,kmem_nullslab标识这个链表的结尾。这个链表分成三个部分,所有对象都已被分配的slab位于最前面的部分,部分被分配的slab位于中间,而完全空闲的slab位于尾部。部分被分配的部分和完全空闲的部分合称为空闲列表,cache_freelist指向这个链表的表头。kmem_nullslab是整个链表的尾部,因此它也是空闲链表的尾部。第四个部分是depot层相关的字段。 cache_magtype表示弹夹的类型。弹夹分成不同的类型,不同类型的弹夹中可以存放的对象数各不相同,可装载对象的尺寸范围也不同。这个部分中还有两个链表字段,一个是满弹夹链表cache_full,一个是空弹夹链表cache_empty,这两个链表中存放的是全局的弹夹。第五个部分是与CPU层相关的字段。这个部分中只有一个字段cache_cpu,这是一个kmem_cpu_cache结构的数组,数组中有多少个元素由CPU的个数决定。每一个元素对应一个CPU,表示该CPU的局部缓存。在kmem_cache结构的声明中,这个数组只有一个元素,但是在创建缓存的时候,系统会根据CPU的数目给kmem_cache结构多分配一些内存,保证数组中可以放下所有CPU局部缓存的控制结构。 1.7.2.3.kmem_slab kmem_slab结构是slab的控制结构,它在uts/common/sys/kmem_impl.h中定义,定义如下: typedef struct kmem_slab { struct kmem_cache *slab_cache; /* 所属对象缓存的控制结构指针 */ void *slab_base; /* 第一个对象的起始地址 */ struct kmem_slab *slab_next; /* 空闲链表中的下一个slab */ struct kmem_slab *slab_prev; /* 空闲链表中的上一个slab */ struct kmem_bufctl *slab_head; /* 空闲对象链表的表头 */ long slab_refcnt; /* 已经分配的对象数 */ long slab_chunks; /* 共有多少个对象 */ } kmem_slab_t; slab_cache字段是一个指向这个slab所属对象缓存控制结构的指针。 slab_base字段表示这个slab中第一个对象的起始地址。前面提到,slab的第一个对象不一定位于slab的起始位置,它的地址与创建slab时分配的染色值有关。slab_base保存了第一个对象的地址,在实现中它就等于slab的起始地址加上染色值。 slab_next和slab_prev用来构建空闲slab链表。一个slab中所有空闲对象的控制结构组成一个单向链表,slab_head就指向这个链表的表头。 slab_refcnt是引用数,实际上就表示这个slab中已经有多少个对象已经被分配出去。 slab_chunks表示slab中包括多少个chunk。一个chunk对应一个对象,因此这个字段实际上也指出了slab中有多少个对象。 1.7.2.4.kmem_bufctl kmem_bufctl是对象所占内存块的控制结构,它在uts/common/sys/kmem_impl.h中定义,定义如下: typedef struct kmem_bufctl { struct kmem_bufctl *bc_next; /* 空闲列表中下一个内存块 */ void *bc_addr; /* 内存块的起始地址 */ struct kmem_slab *bc_slab; /* 所属slab的控制结构指针 */ } kmem_bufctl_t; 这个结构中只有三个字段。bc_next表示空闲列表中的下一个内存块;bc_addr表示这个内存块的起始地址,实际上也是对象的地址;bc_slab表示这个内存块所属slab的控制结构的指针。 1.7.2.5.kmem_magazine、kmem_maglist和kmem_magtype 这三个数据结构是在管理弹夹的时候用到的主要数据结构,它们都是在uts/common/sys/kmem_impl.h中定义,定义如下: typedef struct kmem_magazine { void *mag_next; /* 弹夹链表中的下一个弹夹 */ void *mag_round[1]; /* 一发或多发子弹 */ } kmem_magazine_t; /* 每个CPU进行分配时所用的弹夹类型 */ typedef struct kmem_magtype { int mt_magsize; /* 弹夹尺寸(有多少发子弹) */ int mt_align; /* 弹夹对齐边界 */ size_t mt_minbuf; /* 适用的最小内存块尺寸 */ size_t mt_maxbuf; /* 允许调整的最大内存块尺寸 */ kmem_cache_t *mt_cache; /* 用于分配弹夹数据结构的对象缓存 */ } kmem_magtype_t; /* 用于depot层的弹夹列表 */ typedef struct kmem_maglist { kmem_magazine_t *ml_list; /* 弹夹链表 */ long ml_total; /* 总弹夹数 */ long ml_min; /* 上次更新后弹夹数的最小值 */ long ml_reaplimit; /* 最多可以释放的弹夹数 */ uint64_t ml_alloc; /* 从这个链表中分配了多少个弹夹 */ } kmem_maglist_t; kmem_magazine是弹夹的控制结构,它有两个字段。mag_next字段指向弹夹链表中的下一个弹夹。mag_round是一个数组,其中每一个位置可以装一发子弹。如果一个位置装了一发子弹,那么这个位置的指针就指向一个空闲对象。在声明中这个数组只有一个元素,但在实际分配内存的时候,系统会根据弹夹中能装的子弹数分配出足够的内存。 kmem_magtype表示弹夹的类型。 mt_magsize表示弹夹中可以装多少发子弹,kmem_magazine中mag_round数组的大小就是由这个字段来决定。 mt_align表示弹夹的对齐边界。 mt_minbuf和mt_maxbuf限定了这个弹夹类型的适用范围。一种类型的弹夹并不是可以用于所有的对象,它所能装对象的尺寸是有一定限制的,这两个字段就给出了尺寸范围。这两个字段表示的尺寸不是对象本身的大小,而是前面所说的chunksize,也就是对象加上对齐和调试信息之后实际所占的内存块大小。一个弹夹中装载的对象的chunksize必须大于mt_minbuf。mt_maxbuf的用法与mt_minbuf的用法有所不同,它用于调整弹夹的类型。一个CPU在运行的时候可以调整它所用的弹夹类型,如果它发现当前类型中的弹夹装载的子弹数太少,就会选择子弹数更多的弹夹类型。在调整弹夹的时候,需要判断对象的chunksize是否比当前弹夹类型的mt_maxbuf小。如果chunksize小于mt_maxbuf,则允许调整,CPU可以选择下一级别的弹夹类型,否则的话就不能进行弹夹的调整。 mt_cache表示分配这种类型的弹夹使用哪一个对象缓存,分配不同类型的弹夹用到的对象缓存可能是不同的,但是分配同一类型的弹夹用的都是同一个对象缓存。系统中共定义了九种弹夹类型,每个弹夹必定属于其中一种类型。这九种类型在uts/common/os/kmem.c中定义,定义如下: static kmem_magtype_t kmem_magtype[] = { { 1, 8, 3200, 65536}, { 3, 16, 256, 32768}, { 7, 32, 64, 16384}, { 15, 64, 0, 8192 }, { 31, 64, 0, 4096 }, { 47, 64, 0, 2048 }, { 63, 64, 0, 1024 }, { 95, 64, 0, 512 }, { 143, 64, 0, 0 }, }; 在这个定义中,只给出了每种类型前四个字段的值,最后一个字段,也就是mt_cache字段的值没有给出。mt_cache字段指向分配弹夹所用的对象缓存,这些对象缓存是在uts/common/os/kmem.c中的kmem_cache_init函数中创建的,所以mt_cache字段也是在kmem_cache_init函数中赋的值。 kmem_maglist表示一个弹夹链表。 ml_list是链表的表头,ml_total是链表中的总弹夹数。 ml_min表示从上次更新到当前时间弹夹链表中出现的弹夹数最小值。Solaris会定期更新对象缓存,也就是做对象缓存维护,包括更新对象缓存的统计信息、更新depot层的工作集和调整弹夹大小。每次在更新depot层工作集的时候,它会把depot层的空弹夹链表和满弹夹链表中的ml_min设成链表的当前总弹夹数。随着对象的分配和回收,弹夹链表中的弹夹数也在不断变化,而ml_min保存了自上次更新以后出现过的最小弹夹数。 ml_reaplimit用于回收depot层的弹夹。这里有一个弹夹工作集的概念。在系统稳定工作的时候,所需的内存会保持在一个稳定的值,因此depot层中所需的弹夹数也会在一个稳定的范围内,这时,所需的弹夹就构成一个弹夹工作集。例如在一段时间内,一个弹夹链表的弹夹数在30到40之间浮动,那么弹夹工作集就是10个弹夹(40 - 30),其余的弹夹一直闲置在弹夹链表中,在回收的时候就要回收这些闲置的弹夹。实际上ml_min就是闲置的弹夹数,因为它表示了一段时间内不同弹夹的最小值,在更新depot工作集的时候,Solaris会把ml_min的值赋给ml_reaplimit。在下次更新depot工作集之前,ml_reaplimit的值就不会再改变了,但是弹夹链表中的弹夹数是会发生变化的,所以在实际回收的时候,回收的弹夹数会取ml_reaplimit和当前弹夹总数中的最小值。 1.7.2.6.kmem_cpu_cache kmem_cpu_cache结构是CPU局部缓存的控制结构,它在uts/common/sys/kmem_impl.h中定义,定义如下: typedef struct kmem_cpu_cache { kmutex_t cc_lock; /* 保护CPU局部缓存的互斥锁 */ uint64_t cc_alloc; /* 这个局部缓存中已分配的对象数 */ uint64_t cc_free; /* 这个局部缓存中的空闲对象数 */ kmem_magazine_t *cc_loaded; /* 当前装载的弹夹 */ kmem_magazine_t *cc_ploaded; /* 前一个装载的弹夹 */ int cc_rounds; /* 当前装载的弹夹中还有多少发子弹 */ int cc_prounds; /* 前一个装载的弹夹中还有多少发子弹 */ int cc_magsize; /* 满弹夹中有多少发子弹 */ int cc_flags; /* 全局对象缓存cache_flags字段的拷贝 */ char cc_pad[KMEM_CPU_PAD]; /* 用于边界对齐 */ } kmem_cpu_cache_t; cc_alloc表示当前CPU的局部缓存中已经分配了多少个对象,cc_free表示还有多少个空闲对象。 cc_loaded和cc_ploaded表示CPU装载的两个弹夹。当从depot层装载一个新弹夹的时候,cc_loaded指向新装载的弹夹,cc_ploaded指向cc_loaded原来所指的弹夹,而cc_ploaded原来所指的弹夹被归还到depot层。保存两个弹夹的目的是为了防止抖动。如果只有一个弹夹,现在考虑弹夹中只有一个对象的情况,如果CPU要连续分配两个对象,那么第一个对象可以直接从弹夹中分配,而分配第二个对象时就需要局部缓存从depot层装载一个满弹夹,并把当前的空弹夹还给depot层,再从满弹夹中分配;下一步CPU要连续释放两个对象,由于弹夹中还有一个空位,所以第一个对象可以直接存到弹夹中,但是释放第二个对象时就需要从depot层装载一个空弹夹,并把满弹夹还给depot层,然后把空闲对象放到空弹夹中。这时,CPU的局部缓存中的弹夹又变成只有一个对象的情况。如果这种操作反复进行,那么局部缓存就需要反复地与depot层交互,从而出现抖动。如果有两个弹夹的话,只要其中有一个弹夹中有空闲对象,就可以分配对象,而只要其中一个弹夹有空位,就可以释放对象,这就避免了抖动的情况。 cc_rounds和cc_prounds分别表示当前装载的弹夹和前一个装载的弹夹中的空闲对象数,而cc_magsize表示弹夹中最多可以装载多少个对象。 cc_flags是局部缓存所在的对象缓存中cache_flags字段的拷贝,在这里放一份拷贝是为了操作方便。 1.7.3.情景 1.7.3.1.创建对象缓存 表9 创建对象缓存时用到的主要函数函数名 文件名 功能描述 kmem_cache_create uts/common/os/kmem.c 在内核空间分配一个已初始化的对象创建对象缓存是通过kmem_cache_create函数完成的,这个函数的流程如图2所示: 图15 kmem_cache_create函数流程图创建对象缓存的时候需要给对象缓存起一个名字,这个名字要符合C语言的命名规范,kmem_cache_create函数首先会调用strident_valid函数来验证缓存的名字。然后要为对象缓存的控制结构分配空间,这通过调用vmem_xalloc函数来完成。vmem_xalloc函数从kmem_cache_arena区域分配一块内存,内存的大小通过KMEM_CACHE_SIZE宏来计算。这个宏根据系统中CPU的个数调整内存块的大小,保证位于控制结构末尾的局部缓存数组可以容下所有CPU的局部缓存。新分配的内存块中所有的字节都被初始化为0。接下来就要设置控制结构的各个字段了,这大致分成设置对象缓存属性、设置slab层、设置depot层和设置CPU层几个部分,下面就部分字段进行说明。 cache_align字段根据align参数来设置。首先要检查align的值,如果它的值为0,就把它设成默认的对齐边界(8字节,在KMEM_ALIGN宏中定义)。另外align的值必须是2的幂,而且它不能大于下层分配器的基本分配单位,否则会报错。接下来kmem_cache_create函数会对cflags进行检验和设置,保证里面的各个标识位都是合理的,然后把cflags的值赋给cache_cflags字段。 cache_constructor、cache_destructor等字段由调用者通过参数来设置。 cache_chunksize字段的值需要根据cache_bufsize字段的值来计算。首先把cache_bufsize按对齐边界对齐;如果cache_flags中的KMF_BUFTAG位被置位,就再加上kmem_buftag结构的大小;最后再按对齐边界对齐一次,得到的值就是chunk的大小。有了chunk的大小,就可以算出cache_bufctl、cache_buftag、cache_contents等字段的值。然后要找一个合适的slab大小。如果cache_chunksize小于下层分配器基本分配单位的1/KMEM_VOID_FRACTION(KMEM_VOID_FRACTION的值为8),而且cache_cflags的 KMF_NOHASH位被置位,则slab的大小就等于基本分配单位的大小。否则的话就分别考虑slab中分别包括1个到8个chunk的情况,从中找出内存浪费最少的一个情况,slab的大小就等于这种情况下的chunk数乘以chunk大小,再按基本分配单位对齐。 depot层的设置主要就是考虑选择一个合适的弹夹类型,选择的原则就是从弹夹类型链表中找第一个适用的弹夹类型(mt_minbuf字段小于chunk大小的类型),找到后就把这个类型赋给cache_magtype字段。设置完各个字段的值后,kmem_cache_create函数会把新建的对象缓存添加到全局对象缓存链表中,把它插在kmem_null_cache之前。最后,如果kmem_ready的值非0,也就是内核内存的初始化已经完成,说明用于分配弹夹的对象缓存已经创建完毕,这时就开启对象缓存的弹夹机制。 1.7.3.2.分配对象 这个情景描述在内核空间分配一个已经初始化好的对象,其中涉及到的主要函数包括:表10 内核对象分配中的主要函数函数名 文件名 功能描述 kmem_cache_alloc uts/common/os/kmem.c 在内核空间分配一个已初始化的对象 kmem_depot_alloc uts/common/os/kmem.c 从depot层申请一个满弹夹 kmem_depot_free uts/common/os/kmem.c 把CPU换下的空弹夹加入到depot层的空弹夹链表中 kmem_slab_alloc uts/common/os/kmem.c 在slab层创建一个对象,并把它分配给调用者 kmem_slab_create uts/common/os/kmem.c 为一个对象缓存创建一个新的slab 内核程序要分配一个对象的时候,需要调用kmem_cache_alloc函数。这个函数的流程如图2所示: 图16 kmem_cache_alloc函数流程图 kmem_cache_alloc首先从CPU自己的缓存中分配。如果CPU的两个弹夹中都没有可分配的对象,则调用kmem_depot_alloc函数从depot层的满弹夹链表中装载一个满弹夹,然后把满弹夹设为当前弹夹,重新执行分配过程。装载满弹夹时会替换下CPU前一个装载的弹夹,被换下的弹夹一定是一个空弹夹,所以要调用kmem_depot_free函数把这个弹夹放入depot层的空弹夹链表中。如果获取满弹夹失败,则说明满弹夹列表为空,depot层也没有可以分配的对象,这时就调用kmem_slab_alloc函数直接从slab层分配一个对象。kmem_slab_alloc分配的对象是未初始化的对象,最后还要用构造函数对这个对象进行初始化。从slab层分配对象时调用的是kmem_slab_alloc函数,这个函数的流程如图3所示: 图17 kmem_slab_alloc函数流程图 这个函数中需要说明的是如何获取slab中一个空闲内存块的地址。获取地址的代码如下: if (cp->cache_flags & KMF_HASH) { /* * Add buffer to allocated-address hash table. */ buf = bcp->bc_addr; hash_bucket = KMEM_HASH(cp, buf); bcp->bc_next = *hash_bucket; *hash_bucket = bcp; if ((cp->cache_flags & (KMF_AUDIT | KMF_BUFTAG)) == KMF_AUDIT) { KMEM_AUDIT(kmem_transaction_log, cp, bcp); } } else { buf = KMEM_BUF(cp, bcp); } 从kmem_slab结构中只能获取空闲内存块的控制结构,也就是一个kmem_bufctl变量,然后需要从kmem_bufctl得出空闲块的地址。在kmem_cache结构的介绍中提到内存块的控制结构可能放在内存块中,也可能另外为它分一块内存,如果另外分配内存的话,就需要一个存放控制结构的hash表。kmem_cache结构的cache_flag字段中有一位会标出是否要用到hash表。在从kmem_bufctl结构获得内存块地址的时候,要对这一位进行判断。如果用到hash表,则kmem_bufctl的bc_addr字段就指向内存块的地址;如果kmem_bufctl就在内存块中,就可以从控制结构直接计算出内存块的地址。这个计算是通过KMEM_BUF宏完成的,这个宏在uts/common/sys/kmem_impl.h中定义。在没有空闲slab时需要调用kmem_slab_create函数创建一个新的slab,它的流程如图4所示: 图18 kmem_slab_create函数流程 这个函数在为kmem_slab控制结构分配内存的时候,会根据对象缓存是否使用hash表来决定是为控制结构另外分配一块内存还是利用slab末尾的内存。同样,在为每个小内存块的控制结构分配内存的时候,也要根据是否使用hash表来决定是为控制结构另外分配内存还是直接利用小内存块本身的内存。如果不使用hash表,则不需要另外分配内存。为了节省空间,在流程图中为小内存块设置控制结构这一块简略表示了。为slab控制结构分配内存的代码如下: if (cache_flags & KMF_HASH) { if ((sp = kmem_cache_alloc(kmem_slab_cache, kmflag)) == NULL) goto slab_alloc_failure; chunks = (slabsize - color) / chunksize; } else { sp = KMEM_SLAB(cp, slab); chunks = (slabsize - sizeof (kmem_slab_t) - color) / chunksize; } 代码中为控制结构另外分配内存时调用的也是kmem_cache_alloc函数,它从kmem_slab对应的对象缓存中分配。kmem_slab的对象缓存是不使用hash表的,否则在分配kmem_slab对象的时候有可能引起对kmem_slab对象缓存的递归分配,最后导致内存分配失败。 KMEM_SLAB宏用来根据slab的地址计算slab控制结构的地址。相应地,在分配小内存块控制结构的时候,KMEM_BUFCTL宏用来根据小内存块的地址计算小内存块控制结构的地址。 1.7.3.3.释放对象 表11 释放对象时用到的主要函数函数名 文件名 功能描述 kmem_cache_free uts/common/os/kmem.c 释放一个已初始化的对象 kmem_slab_free uts/common/os/kmem.c 在slab层释放一个内存块 kmem_slab_destroy uts/common/os/kmem.c 销毁一个slab 释放对象是与分配对象相反的过程。释放对象时调用的是kmem_cache_free,这个函数在uts/common/os/kmem.c中定义,它的流程如图5所示: 图19 kmem_alloc_free函数流程图如果CPU当前装载的弹夹中有空位,那么就把被释放的对象放到空位中,也就是让对象数组中的一个空闲指针指向被释放的对象。如果当前装载的弹夹是满的,那么就看前一个装载的弹夹是否还有空位。如果有空位的话就把当前装载的弹夹和前一个装载的弹夹交换一下位置,再重新做释放操作。如果局部缓存中的两个弹夹都是满的,那么就需要调用kmem_depot_alloc函数从depot层获取一个空弹夹,并通过调用kmem_depot_free函数把一个满弹夹还给depot层,然后再重新做释放操作。如果depot层的空弹夹链表中已经没有空弹夹,就调用kmem_cache_alloc函数从弹夹的对象缓存中分配一个新弹夹(空弹夹),然后重新做释放操作。这也就是说一个对象缓存在刚创建的时候,depot层中的满弹夹链表和空弹夹链表都是空的,以后用到的弹夹都是在释放对象时创建的。弹夹在被创建后,除非出现内存紧张或者需要调整弹夹尺寸的情况,它们不会被销毁,而是随着申请对象和释放对象操作的交替进行,慢慢分布到depot层的空弹夹链表或满弹夹链表中。如果创建新弹夹失败,就无法把被释放的对象缓存到depot层,这时就调用析构函数销毁对象,再调用kmem_slab_free函数把对象所占的内存块还给slab层。 kmem_slab_free函数的功能是释放一个已分配的未初始化内存块,它在uts/common/os/kmem.c中定义,它的流程如图6所示: 图20 kmem_slab_free函数流程图 kmem_slab_free首先要得到内存块的控制结构和slab的控制结构。如果这个slab使用hash表存储控制结构,则查询hash表,否则直接从内存块的地址算出控制结构的地址。由于这个slab释放了一个内存块,也就是说这个slab现在肯定包含有空闲内存块,所以如果这个slab以前不在对象缓存的空闲slab链表中,现在就把它加进去。然后把被释放的内存块插入到这个slab的空闲内存块链表的头部。如果释放完这个内存块后slab中所有的内存块都是空闲的,就可以回收这个slab所占的内存空间。回收slab时首先要把这个slab从空闲slab链表中移除,如果这个slab恰好是空闲链表的表头,那么还要修改表头,让表头指向下一个空闲slab。然后就可以调用kmem_slab_destroy函数销毁slab了。 kmem_slab_destroy函数比较简单,它完成的工作包括两步:第一步,如果slab的控制结构占用了另外的空间,就释放这些空间;第二步,调用底层分配器的释放函数(vmem_free),释放slab所占的内存。 1.7.3.4.分配与释放未初始化内存块 表12分配与释放未初始化内存块时用到的主要函数函数名 文件名 功能描述 kmem_alloc uts/common/os/kmem.c 分配一块任意大小的未初始化内存 kmem_zalloc uts/common/os/kmem.c 分配一块任意大小的内存块,内存块全部初始化为0 kmem_free uts/common/os/kmem.c 释放一个未初始化内存块 Solaris的slab内存分配器除了可以分配已初始化的对象,它还可以像普通的分配器一样分配和释放任意大小的未初始化的内存,完成这两个操作的函数分别是kmem_alloc和kmem_free。 kmem_alloc函数在uts/common/os/kmem.c中定义。在调用kmem_alloc函数的时候需要给出所需内存块的尺寸,kmem_alloc会根据尺寸进行不同的分配操作。对于小于16k(在常量KMEM_MAXBUF中定义)的内存块,slab分配器采用了与分配对象相同的方法,它把未初始化的内存看作构造函数和析构函数都为空的对象。系统在初始化的时候创建了32个用于分配未初始化内存的对象缓存,它们对应的内存块尺寸在kmem_alloc_sizes数组中给出。 static const int kmem_alloc_sizes[] = { 1 * 8, 2 * 8, 3 * 8, 4 * 8, 5 * 8, 6 * 8, 7 * 8, 4 * 16, 5 * 16, 6 * 16, 7 * 16, 4 * 32, 5 * 32, 6 * 32, 7 * 32, 4 * 64, 5 * 64, 6 * 64, 7 * 64, 4 * 128, 5 * 128, 6 * 128, 7 * 128, P2ALIGN(8192 / 7, 64), P2ALIGN(8192 / 6, 64), P2ALIGN(8192 / 5, 64), P2ALIGN(8192 / 4, 64), P2ALIGN(8192 / 3, 64), P2ALIGN(8192 / 2, 64), P2ALIGN(8192 / 1, 64), 4096 * 3, 8192 * 2, }; 为了实现内存块尺寸到32个对象缓存的映射,Solaris声明了一个全局数组kmem_alloc_table。数组中有2048个元素(KMEM_MAXBUF >> KMEM_ALIGH_SHIFT),每个元素是一个指向对象缓存的指针。数组中每个元素对应一个内存块尺寸,尺寸从前往后依次递增,每次递增的值是8字节(也就是最小对齐边界,在常量KMEM_ALIGN中定义)。第一个元素对应8字节的内存块,第二个元素对应16字节的内存块,……,最后一个元素对应16k字节的内存块。在初始化的时候,系统令kmem_alloc_table数组中某一元素指向大于它所对应内存块尺寸的第一个对象缓存。kmem_alloc_table数组元素和32个对象缓存的对应关系如图5所示。 图21 kmem_alloc_talbe数组与32个对象缓存之间的关系如果要分配一定尺寸的一块内存,分配器首先在kmem_alloc_table数组中找到大于该尺寸的第一个元素,再根据该元素中的指针找到相应的对象缓存,然后就调用前面所讲的kmem_cache_alloc函数从对象缓存中分配一块内存。如果要分配大于16k字节的内存块,就不能用上面提到的这套机制了,这时solaris就调用vmem_alloc函数直接从kmem_oversize_arena区域分配所需的内存。 kmem_free函数的功能是释放未初始化内存块,它的原理与kmem_alloc差不多,只不过它执行了一个相反的过程。kmem_free函数也是根据内存块的大小执行不同的释放操作,如果内存块的尺寸小于16k,它就调用kmem_cache_free函数释放内存块,否则它就直接调用vmem_free函数把内存块归还到kmem_oversize_arena区域中去。

你可能感兴趣的:(转一个solaris虚拟内存管理的wiki)