深入理解Linux虚拟内存管理(二)(上)

简介: 深入理解Linux虚拟内存管理(二)

第8章 slab分配器

  这一章将介绍一种通用的分配器:slab 分配器。它与 Solaris 系统 [MM01] 上的通用内核分配器在许多方面都很相似。Linux 中 slab 的实现主要是基于 Bonwick[Bon94 ] 的第 1 篇 slab 分配器的论文,并进一步做了大量的改进,这在他最近的论文中 [BA01] 有所描述。这一章我们先对 slab 分配器做一个快速的浏览,然后描述所用到的数据结构,并深入讨论 slab 分配器的每一项功能。


  slab 分配器的基本思想是:将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态。如果没有基于对象的分配器,内核将花费更多的时间分配、初始化以及释放一个对象。slab 分配器的目的是缓存这些空闲的对象,保留基本结构,从而能够重复使用它们 [Bon94]。


  slab 分配器由多个高速缓存组成,它们通过双向循环链表连接在一起,称为高速缓存链表。在 slab 分配器中,高速缓存可以管理各种不同类型的对象,如 mm_struct 或者 fs_cache,它们由 struct kmem_cache_s 管理,在后面我们将详细讨论这个结构。高速缓存链表是通过字段 next 链接在一起的。


  每一个高速缓存包括若干 slab,slab 由连续的页面帧组成,它们被划分成许多小的块以存放由高速缓存所管理的数据结构和对象。不同数据结构之间的关系如图 8.1 所示。

   slab 分配器有三个基本目标:

  • 减少伙伴系统分配小块内存时所产生的内部碎片。

把经常使用的对象缓存起来,减少分配、初始化以及释放对象的时间开销。在 Solaris 上的基准测试表明,使用 slab 分配器后速度有了很大的提高 [Bon94]。

调整对象以更好地使用 L1 和 L2 硬件高速缓存。

  为减少二进制伙伴分配器所产生的内部碎片,系统需要维护两个由小内存缓冲区所构成的高速缓存集,它们的大小从 25(32) 字节到 217(131 072) 字节。一个高速缓存集适用于使用 DMA 的设备。这些高速缓存叫作 size-N 和 size-N(DMA),N 是分配器的尺寸,而函数 kmalloc()(见 8.4.1 小节)负责分配这些高速缓存。这样就解决了低级页分配器最大的问题。我们将在 8.4 节详细讨论指定大小的高速缓存。


  slab 分配器的第 2 个任务是缓存经常使用的对象。初始化内核中许多数据结构所需要的时间与分配空间所花的时间相当,甚至超过了分配空间所花时间。在创建一个新的 slab 时,一些对象被存放在里面,并且由可用的构造器初始化它们。而在对象释放后,系统将保持它们为初始化时的状态,所以再次分配对象的速度很快。


  slab 分配器的第 3 个任务是充分利用硬件高速缓存。若对象放入 slab 还有剩余的空间,它们就用于为 slab 着色。slab 着色方案试图让不同 slab 中的对象使用不同的硬件高速缓存行。通过将对象放置在 slab 中不同的位移起始处,对象将很可能利用 CPU 中不同的硬件高速缓存行,因此来自同一个 slab 中的对象不会彼此刷新高速缓存。这样,空间可能会因为着色而浪费掉。如图 8.2 所示,从伙伴分配器分配的页面如何存储因对齐 CPU L1 硬件高速缓存而着色的对象。

 Linux 显然不会试图去给基于物理地址分配的页面帧着色 [Kes91],也不会将对象放在某一特定位置,如数据段 [GAV95]、代码段 [HK97]中,但是 slab 着色方案都可以提高硬件高速缓存行的利用率。在 8.1.5 小节中将深入讨论高速缓存着色。在 SMP 系统中,有另外的方案来利用高速缓存,即每一个高速缓存都有一个小的对象数组,而这些对象就是为每一个 CPU 所保留的。这些内容将会在 8.5 节中进一步讨论。


  在编译的时候,如果设置了选项 CONFIG_SLAB_DEBUG,slab 分配器会提供额外的 slab 调试选项。其中提供的两个调试特征称为红色区域和对象感染。在红色区域中,每一个对象的末尾有一个标志位。如果这个标志位被打乱,分配器就会知道发生缓冲区溢出的对象的位置并且向系统报告。创建和释放 slab 时,感染一个对象会将预先定义好的位模(在 mm/slab.c 中定义为 0x5A)填入其中。分配时会检查这个位模,如果被改变,分配器就知道这个对象在之前已经使用过,并把它标记出来。

分配器中小而强大的 API 如表 8.1 所列。

8.1 高速缓存

8.1.1 高速缓存描述符

  所有描述高速缓存的信息都存储在 mm/slab.c 中声明的 struct kmem_cache_s 中。它是一个非常大的结构,这里仅描述其中一部分。

struct kmem_cache_s {
/* 1) each alloc & free */
  // 下面这些字段在分配和释放对象时很重要。
  
  /* full, partial first, then free */
  // slabs_ * :在前一节中介绍的三个 slab 链表。
  struct list_head  slabs_full;
  struct list_head  slabs_partial;
  struct list_head  slabs_free;
  // slab 中每一个对象的大小。
  unsigned int    objsize;
  // 一组标志位,决定分配器应该如何处理高速缓存,见 8.1.2 小节。
  unsigned int    flags;  /* constant flags */
  // 每一个 slab 中包含的对象个数。
  unsigned int    num;  /* # of objs per slab */
  // 并发控制锁,避免并发访问高速缓存描述符。
  spinlock_t    spinlock;
#ifdef CONFIG_SMP
  // 在前面的章节中描述的为 per-cpu 高速缓存批量分配的对象数目。
  unsigned int    batchcount;
#endif
  
/* 2) slab additions /removals */
  // 从高速缓存中分配和释放 slab 时将用到这些字段。
  
  /* order of pgs per slab (2^n) */
  // slab 以页为单位的大小,每个 slab 占用用 2*fpoder 个连续页面帧,其中分配的大小
  // 由伙伴系统提供。
  unsigned int    gfporder;

  /* force GFP flags, e.g. GFP_DMA */
  // 调用伙伴系统分配页面帧时用到的一组 GFP 标志位,完整列表见 7.4 节。
  unsigned int    gfpflags;
  // 尽可能将 slab 中的对象存储在不同的硬件高速缓存行中。高速缓存着色将在
  // 8.1.5 小节中进一步讨论。
  size_t      colour;   /* cache colouring range */
  // 对齐 slab 中的字节。例如 ,size-X 高速缓存对齐 L1 硬件高速缓存。
  unsigned int    colour_off; /* colour offset */
  // 这是下一个将使用的着色行,其值超过 colour 时,它重新从 0 开始。
  unsigned int    colour_next;  /* cache colouring */
  kmem_cache_t    *slabp_cache;
  // 这个标志位用于指示高速缓存是否增长。如果设置了该标志位,就不太可能在
  // 处于内存压力的情况下选中该高速缓存以回收空闲 slab。
  unsigned int    growing;
  // 动态标志位,在高速缓存的生命期里动态变化,见 8.1.3 小节。
  unsigned int    dflags;   /* dynamic flags */

  /* constructor func */
  // 为复杂对象提供的构造函数,用以初始化每一个新的对象。这是一个函数指针,可
  // 能为空(NULL)。
  void (*ctor)(void *, kmem_cache_t *, unsigned long);

  /* de-constructor func */
  // 对象的析构函数指针,也可能为空。
  void (*dtor)(void *, kmem_cache_t *, unsigned long);
  // 仅仅初始化为 0,其他的地方都没有用到。
  unsigned long   failures;

/* 3) cache creation/removal */
  // 这两个字段在创建高速缓存时设置。
  
  // 便于识别的高速缓存名称。
  char      name[CACHE_NAMELEN];
  // 指向高速缓存链表中的下一个高速缓存。
  struct list_head  next;
#ifdef CONFIG_SMP
/* 4) per-cpu data */
  // per-cpu 数据,在 8.5 中进一步讨论。
  cpucache_t    *cpudata[NR_CPUS];
#endif
  // 在编译时设置 CONFIG_SLAB_DEBUG 选项时,这些数字才有效。它们都是不重要的计
  // 数器,一般不必关心。读取/proc/slabinfo 里的统计信息时,与其依赖这些字段是否有效,还不
  // 如通过另一个进程检查每个高速缓存里的每个 slab 使用情况。
#if STATS
  // 当前在高速缓存中活动的对象数目。
  unsigned long   num_active;
  // 已经分配的对象数目。
  unsigned long   num_allocations;
  // num_active 的上限。
  unsigned long   high_mark;
  // kmem_cache_grow()的调用次数。
  unsigned long   grown;
  // 该高速缓存被回收的次数。
  unsigned long   reaped;
  // 这个字段从未使用过。
  unsigned long     errors;
#ifdef CONFIG_SMP
  // 分配器使用 per-cpu 高速缓存的次数。
  atomic_t    allochit;
  // 对 allochit 的补充,是分配器未命中 per-cpu 高速缓存的次数。
  atomic_t    allocmiss;
  // 在 per-cpu 高速缓存中空闲的次数。
  atomic_t    freehit;
  // 对象释放后被置于全局池中的次数。
  atomic_t    freemiss;
#endif
#endif
};

8.1.2 高速缓存静态标志位

   一些标志位在高速缓存初始化时就被设置了,并且在生命周期内一直不变。这些标志位影响到 slab 的结构如何组织以及对象如何存储在 slab 中。这些标志位组成一个位掩码,存储在高速缓存描述符的字段 flag 中。所有的标志位都在 linux/slab.h 中声明。

   有 3 组标志位,第 1 组是内部标志位,仅由 slab 分配器使用,如表 8.2 所列。

CFGS_OFF_SLAB 标志位决定 slab 描述符存储的位置。

   第 2 组在创建高速缓存时设置,这些标志位决定分配器如何处理 slab,如何存储对象。列表如 8.3 所列。

   第 3 组在编译选项 CONFIG_SLAB_DEBUG 设置后才有效,列表如 8.4 所列。它们决定了对 slab 和对象做哪些附加的检查,主要与新建的高速缓存相关。


   为了防止调用错误的标志位,mm/slab.c 中定义了 CREATE_MASK,它由所有允许的标志位所组成。在创建一个高速缓存时,请求的标志位会和 CREATE_MASK 作比较,如果使用了无效的标志位,系统将会报告一个错误。

8.1.3 高速缓存动态标志位

   虽然字段 dflag 中只有一个标志位 DFLGS_GROWN,但它却非常重要。该标志位在 kmem_cache_grow() 中设置,这样 kmem_cache_reap() 就就不会选择该高速缓存进行回收。kmem_cache_reap() 将跳过设置了该标志位的高速缓存,并清除该标志位。

8.1.4 高速缓存分配标志位

  这些标志位,与为 slab 分配页面帧的 GFP 页面标志位选项相对应,列表如 8.5 所列。调用者既可以使用 SLAB_* 标志位,也可以使用 GFP * 标志位,但实际上应该仅仅使用 SLAB_* 标志位。它们和 6.4 节中所述的标志位直接对应,所以在这里不再详细讨论。假设存在这些标志位是为了明确不同的作用,在这种情况下 slab 分配器对不同的标志位就要作出不同的响应。而事实上它们没有任何区别。

   极少部分的标志位可能会传递给构造函数和析构函数,列表如 8.6 所列。

8.1.5 高速缓存着色

  为了更好地利用硬件高速缓存,在不同的 slab 中,slab 分配器将对象放置在不同的偏移处,这取决于 slab 中剩余多少空间。偏移量都以 BYTES_PER_WORD 为单位,除非设置了标志位 SLAB_HWCACHE_ALIGN,在后者的情况下,将按照 L1_CACHE_BYTES 的块大小与 L1 硬件高速缓存对齐。


  在创建一个高速缓存时,需要要计算一个 slab 适合多少个对象(见 8.2.7 小节),以及将浪费掉多少字节。正是由于这种字节的损失,Linux 将为高速缓存描述符计算如下两个数字。


colour:能够使用的不同的偏移量数目。

colour_off:每一个对象在 slab 中的整数倍偏移量。

  通过对象偏移,它们将利用关联硬件高速缓存上不同的行。因此,从内存中调入 slab 中的对象就不会彼此覆盖。


  最好是通过一个例子来解释。为方便起见,在 slab 中设置 s_mem(第 1 个对象的地址)为 0,且 slab 中共有 100 B 被浪费掉,另外奔腾 II 的硬件高速缓存 L1 是 32 B。


  在这个例子中,第 1 个创建的 slab 使得它的对象从 0 开始。第 2 个从 32 开始,第 3 个从 64 开始,第 4 个从 96 开始,第 5 个又从 0 开始。这样,从不同 slab 中来的对象就不会命中同一硬件高速缓存行了。colour 和 colour_off 的值分别是 3 和 32。

8.1.6 创建高速缓存

   函数 kmem_cache_create() 用于创建高速缓存并把他们添加到高速缓存链表中去。创建高速缓存有以下几步:

为了防止错误的调用,执行基本的检查。

如果设置了 CONFIG_SLAB_DEBUG,则执行调试检查。

从 cache_cache slab 高速缓存中分配一个 kmem_cache_t 结构。

将对象的大小调整为字对齐。

计算有多少对象可以放入一个 slab 中。

调整对象大小以适应硬件高速缓存。

  • 计算着色偏移量。
  • 初始化高速缓存描述符中其他字段。
  • 将这个新的高速缓存添加到高速缓存链表中。

   图 8.3 是创建高速缓存相关的函数调用图,每一个函数在代码注释部分都有完整描述。

8.1.7 回收高速缓存

  在一个 slab 空闲时,系统将其放到 slab_free 链表中,以备将来使用。高速缓存不会自动收缩它们自己,因此当 kswapd 发现内存紧张时,就调用 kmem_cache_reap() 释放放一些内存。这个函数负责选择一个需要收缩它自己内存使用的高速缓存。值得注意的是,回收高速缓存不会考虑哪些内存节点或者哪些管理区处于压力下。这意味着,在 NUMA 或者有高端内存的机器上,内核可能会在没有内存压力的区域花大量的时间释放内存资源,但是在诸如 x86 体系结构的机器上不会出现这样的问题,因为它只有一个内存区。


  图 8.4 中调用图虽然简单但其实是个假象,因为选择一个适当的高速缓存进行回收其实是一个非常漫长的过程。在有大量高速缓存的系统中,每次调用却只检查 REAP_SCANLEN(目前定义为 10)个高速缓存。最后一个被检查的高速缓存保存在变量 clock_searchp 中,以防止重复检查同一个高速缓存。对于每个已经检查过的高速缓存,回收器做如下事情:


检查 SLAB_NO_REAP 标志位,如果设置了就跳过。

如果高速缓存正在增长,也跳过。

如果高速缓存最近已经增长了,或正在增长,就设置标志位 DFLGS_GROW。如果该标志位已经被设置了,则跳过 slab,但同时要清除这个标志位,以便作为下一次回收的候选对象。

统计 slabs_free 链表中空闲 slab 的数量,并且计算在变量 pages 这么多个页面中将释放多少页面。

如果高速缓存有构造器,或者有大型的 slab,则调整 pages 以减少该高速缓存被选中的机会

如果待释放的页面数量超过 REAP_PERFECT,则释放 slabs_free 中一半的 slab。

否则,扫描剩下的高速缓存,并选择可释放最多页数的高速缓存,以释放 slabs_free 中它一半的 slab。

8.1.8 收缩高速缓存

   在选中一个高速缓存以收缩它自己时,步骤既简单又直接:

  • 删除 per-CPU 高速缓存中所有的对象。

删除 slabs_free 中所有的 slab,除非设置了增长标志位。

  如果不是如此的微妙,Linux 其实什么也不是。


  这里提供了两个易混淆的有相似名称却互不相同的收缩函数。如图 8.5 所示,kmem_cache_shrink() 删除 slabs_free 链表中所有的 slab 并返回已释放页面的数量。它是提供给 slab 分配器用户所使用的主要函数。


  第 2 个函数 __kmem_cache_shrink() 如图 8.6 所示,它释放 slabs_free 链表中所有的 slab,并且核实 slab_partial 链表和 slab_full 链表是否为空。该函数仅用于内部,在销毁高速缓存的过程中显得非常重要,它不管要释放多少页面,只需高速缓存为空。

8.1.9 销毁高速缓存

  在卸载一个模块时,Linux 需要由函数 kmem_cache_destroy() 销毁所有与之相关的高速缓存,如图 8.7 所示。是否适当地销毁高速缓存显得很重要,因为不允许有两个相同名称的高速缓存同时存在。核心内核代码一般不用关心去销毁它自己的高速缓存,因为这些高速缓存在整个系统生存期中一直存在。销毁一个高速缓存的步骤如下:


从高速缓存链表中删除该高速缓存。

收缩高速缓存,删除其中所有的 slab。

释放任意的 per-CPU 高速缓存( kfree() )。

从 cache_cache 结构中删除高速缓存描述符。

8.2 slabs

// mm/slab.c
/*
 * slab_t
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.
 * Slabs are chained into three list: fully used, partial, fully free slabs.
 */
typedef struct slab_s {
  // 该 slab 所属的链接列表,可能是高速缓存管理器中 slab_full,slab_partial 
  // 或者slab_free 其中的一个。
  struct list_head  list;
  // slab 中的相对于第一个对象基地止的着色偏移。第一个对象的地址是:s_mem + colouroff。
  unsigned long   colouroff;
  // slab 中第一个对象的起始地址。
  void      *s_mem;   /* including colour offset */
  // slab 中活动的对象数目。
  unsigned int    inuse;    /* num of objs active in slab */
  // 这是一个 bufctls 数组,用于存储空闲对象的位置。更多的细节见 8.2.3 小节。
  kmem_bufctl_t   free;
} slab_t;

  读者应该注意到,对于 slab 中给定的 slab 管理器或者对象,没有明显的方法判断它应属于哪一个 slab 或者高速缓存。这里通过 struct page 里的 list 字段来整合高速缓存。SET_PAGE_CACHE() 和 SET_PAGE_SLAB() 函数使用 page→list 中的 next 和 prev 字段来跟踪对象属于哪一个高速缓存或 slab。也可以通过宏 GET_PAGE_CACHE() 和 GET_PAGE_SLAB() 从页面中得到高速缓存描述符和 slab 描述符。这些关系如图 8.8 所示。


  最后讨论保存 slab 描述符的位置。slab 描述符既可保存在 slab 中(CFLGS_OFF_SLAB 在静态标志位中设置)也可保存在 slab 外。具体保存的位置由创建高速缓存时对象的大小决定。但是 struct slab_t 可以存储在页面帧的首部,尽管图 8.8 中显示的 struct slab_t 和页面帧是分开的。

8.2.1 存储 slab 描述符

  如果对象大于阈值(在 x86 架构中是 512 B),就需要设置高速缓存中的标志位 CFGS_OFF_SLAB,slab 描述符保存在 slab 之外的某个指定大小的高速缓存中(见 8.4 节)。所选择的指定大小的高速缓存要足够大,能够容纳 struct slab_t,在需要的时候,函数 kmem_cache_slabmgmt() 从中进行分配。这将限制存储在 slab 中的对象数目,因为只有有限的空间分配给 bufctls 数组 。然而当对象很大时,这就不重要了,因此没有必要将很多对象存储在单个 slab 中。


  或者说,可以将 slab 管理器保存在 slab 的起始处。当保存在 slab 中时,在起始处要留有足够的空间以存储 slab_t 和无符号整形数组 kmem_bufctl_t。这个数组用于跟踪下一个可用空闲对象的索引,这将在 8.2.3 小节作进一步讨论。而实际的对象保存在 kmem_bufctl_t 数组之后。


  图 8.9 可以帮助我们清楚地理解描述符在 slab 内部时的 slab 结构。图 8.10 描述了 slab 描述符保存在外部时,高速缓存如何用指定大小的高速缓存来存储 slab 描述符。

8.2.2 创建 slab

  在这里,我们已经看到怎样创建一个高速缓存,但在创建时,它是一个空的高速缓存,而且链表 slab_full,slab_partial 和 slabs_free 也都是空的。通过调用函数 kmem_cache_grow() 给高速缓存分配新的 slab,其调用图如图 8.11 所示 。通常称之为 “高速缓存增长” ,当 slab_partial 链表中没有对象或者 slabs_free 链表中没有 slab 时调用该函数。所有的任务如下:


为了防止错误的调用,执行基本的全面检查。

计算 slab 中对象的着色偏移量。

为 slab 分配内存,并获取 slab 描述符。

把用于 slab 的页面链接起来放置到 slab 和高速缓存描述符中,见 8.2 节。

初始化 slab 中的对象。

将这个 slab 添加到高速缓存中。

8.2.3 跟踪空闲对象

  slab 分配器有一个快速简单的方法来跟踪在部分填充的 slab 中的空闲对象。通过使用与每个 slab 管理器相关联的称为 kmem_bufctl_t 的无符号整形数组来达到这个目的。很明显,slab 管理器需要知道它的空闲对象在哪。


  参照以往描述 slab 分配器的文章 [Bon94],kmem_bufctl_t 是一个对象链表。在 Linux2.2. x 内核中,这个结构是个具有 3 项的联合体:一个指向下个空闲对象的指针,一个指向 slab 管理器的指针,以及一个指向对象本身的指针。在联合中具体是哪一个字段取决于对象的状态。


  现在,对象属于哪一个 slab 和高速缓存是由 struct page 决定的,而 kmem_bufctl_t 是一个简单的对象索引整型数组。数组中元素的个数与 slab 中对象的个数相同。

typedef unsigned int kmem_bufctl_t;

  因为数组保存在 slab 描述符之后,并且没有指针直接指向第一个元素,所以需要提供辅助宏 slab_bufctl()

#define slab_bufctl(slabp) \
  ((kmem_bufctl_t *)(((slab_t*)slabp)+1))

 这个看起来很神秘的宏展开后却相当简单。参数 slabp 是一个指向 slab 管理器的指针。表达式 ((slab_t *)slabp)+1 将结构 slabp 转换成 slab_t 结构,然后加 1。这个操作实际上返回了指向数组 kmem_bufctl_t 起始地址的指针。(kmem_bufctl_t *) 又将这个指针转换为所需的类型。这个代码块的返回值是 slab_bufctl(slabp)[i]。或者说是,“通过指向 slab 描述符的指针,由宏 slab_bufctl() 进行偏移得到数组 kmem_bufctl_t 的起始地址,然后返回该数组中第 i 个元素”。


  在 slab 里下一个空闲对象的索引存储在 slab_t→free 中,删除了为跟踪空闲对象而需要的链表。分配或者释放对象时,这个指针基于数组 kmem_bufctl_t 中的信息更新。

8.2.4 初始化 kmem_bufctl_t 数组

  当高速缓存增长时,slab 中的对象和数组 kmem_bufctl_t 将被初始化。数组被填充每一个对象的索引,从 1 开始,并以标记 BUFCTL_END 结束。例如具有 5 个对象的 slab,数组中的元素如图 8.12 所示。


  数值 0 存储在 slab_t→free 中,是因为第 0 个对象 BUFCTL_END 表示第一个被使用的空闲对象。对于给定的第 n 个对象,其下一个空闲对象索引将被存储在 kmem_bufctl_t[n] 中。 看着上面的数组,0 之后的下一个空闲对象图 8.12 初始化数组 kmem_bufctl_t 是 1。而 1 之后是 2,依此类推。正如数组所使用的,这样的排列使数组看起来更像一个空闲对象的后进先出队列(LIFO)。

8.2.5 查找下一个空闲对象

  当分配一个对象时,kmem_cache_alloc() 调用 kmem_cache_alloc_one_tail() 执行更新数组 kmem_bufctl_t() 的实际动作。字段 slab_t→free 具有第一个空闲对象的索引。而下一个空闲对象的索引在 kmem_bufctl_t[slab_t→free] 中。它在代码中如下:

  objp = slabp->s_mem + slabp->free * cachep->objsize;
  slabp->free = slab_bufctl(slabp)[slabp->free];

  字段 slabp→s_mem 是指向 slab 中第一个对象的指针。slabp→free 是将被分配的对象的索引,而且必须和一个对象的大小相乘。


  下一个空闲对象的索引存储在 kmem_bufctl_t[slabp→free] 中。由于没有指针直接指向数组,因此使用宏 slab_bufctl() 作为辅助手段。请注意数组 kmem_bufctl_t 在分配过程中并没有改变,但是没有被分配的元素不能被访问。例如,分配 2 个元素后,数组 kmem_bufctl_t 中的索引 0 和 1 并没有被任何其他的元素所指向。

8.2.6 更新 kmem_bufctl_t

   只有在函数 kmem_cache_free_one() 中释放一个对象时才需要更新数组 kmem_bufctl_t。数组更新的代码如下:

  unsigned int objnr = (objp-slabp->s_mem)/cachep->objsize;
  slab_bufctl(slabp)[objnr] = slabp->free;
  slabp->free = objnr;

 指针 objp 指向将要被释放的对象,而 objnr 是其索引。kmem_bufctl_t[objnr] 被更新为指向 slabp→free 的当前值,通过字段 free,有效地替换了虚拟链表上的对象。slabp→free 被更新为即将被释放的对象,因此它也会是下一个被分配的对象。

8.2.7 计算 slab 中对象的数量

  创建高速缓存时,系统通过调用 kmem_cache_estimate() 函数计算在单个的 slab 上可以存放多少个对象。这要考虑 slab 描述符是存储在 slab 之内还是之外,并且跟踪每一个 kmem_bufctl_t 的大小,无论对象是否空闲。该函数返回可能存储的对象个数以及浪费的字节数。若使用高速缓存着色,则浪费的字节数就非常可观。


基本的计算步骤如下:


初始化 wastage 为整个 slab 的大小,例如 PAGE_SIZEgfp_order。

减去 slab 描述符所占用的空间。

计算可能存储的对象个数。如果 slab 描述符存储在 slab 之内,还要考虑 kmem_bufctl_t 的大小。持续增加 i 的大小直至 slab 被填充。

返回对象的个数和浪费的字节数。

8.2.8 销毁 slab

   在收缩、销毁高速缓存时,系统将删除其中的 slab。由于对象可能有析构函数,如果这样它们就必须被调用,因此该功能的任务如下:

  • 如果析构函数可用的话,调用 slab 中所有对象的析构函数。
  • 如果允许调试,检查红色标志位和感染模式。
  • 释放 slab 使用的页面。

   函数调用图如图 8.13 所示,非常简单。

8.3 对 象

   这一节主要内容是如何管理对象。大多数真正需要做的工作已经被高速缓存或 slab 管理器完成。

8.3.1 初始化 slab 中的对象

  当创建一个 slab 时,所有的对象都处于初始化状态。如果构造函数可用,它将为每一个对象所调用,而一旦对象被释放,就认为它处于初始化状态。从概念上讲,这个初始化过程很简单。遍历所有的对象,调用构造函数,并为它初始化 kmem_bufctl。函数 kmem_cache_init_objs() 负责初始化对象。

8.3.2 分配对象

   函数 kmem_cache_alloc() 负责分配一个对象给调用者,这在 UPSMP 系统中稍微有所不同。在 SMP 系统中分配对象时的基本函数调用图如图 8.14 所示。

  有 4 个基本步骤:第 1 步(kmem_cache_alloc_head() 进行基本的检查,确保允许分配。第 2 步选择从哪一个 slab 链表中分配对象。slabs_partial 或者 slabs_free 中的任一个都有可能。如果在 slabs_free 中没有,就增大高速缓存空间(见 8.2.2 小节)并在 slabs_free 中创建一个新的 slab。最后一步从选中的 slab 中分配对象。


  在 SMP 系统中,还有一步,在分配对象前,系统会检查 per-CPU 高速缓存并查找一个可用的高速缓存,如果有的话,则使用它。如果没有,系统在会成批地分配对象的 batchcount 数量并置于它们自己的 per-CPU 高速缓存中。关于 per-CPU 高速缓存更多的信息见 8.5 节。

8.3.3 释放对象

  函数 kmem_cache_free() 的调用图如图 8.15 所示,它用于释放对象,这是一个相对简单的任务。跟 kmem_cache_alloc() 函数一样,它在 UP 以及 SMP 中表现不一样。在这两个系统中主要的区别是,在 UP 中,对象直接返回给 slab;而在 SMP 中,对象却返回给 per-CPU 高速缓存。但在这两种情况下,如果对象有可用的析构函数的话,则函数都会被调用。析构函数负责把对象状态返回到初始化状态。

深入理解Linux虚拟内存管理(二)(中):https://developer.aliyun.com/article/1597425?spm=a2c6h.13148508.setting.14.51734f0e1J7OoF


目录
相关文章
|
14天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
23天前
|
Linux 调度
深入理解Linux虚拟内存管理(七)(下)
深入理解Linux虚拟内存管理(七)
35 4
|
23天前
|
存储 Linux 索引
深入理解Linux虚拟内存管理(九)(中)
深入理解Linux虚拟内存管理(九)
19 2
|
23天前
|
Linux 索引
深入理解Linux虚拟内存管理(九)(上)
深入理解Linux虚拟内存管理(九)
25 2
|
23天前
|
Linux
深入理解Linux虚拟内存管理(七)(中)
深入理解Linux虚拟内存管理(七)
23 2
|
23天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
16 1
|
23天前
|
Linux
深入理解Linux虚拟内存管理(七)(上)
深入理解Linux虚拟内存管理(七)
24 1
|
21天前
|
缓存 Linux 调度
Linux服务器如何查看CPU占用率、内存占用、带宽占用
Linux服务器如何查看CPU占用率、内存占用、带宽占用
67 0
|
23天前
|
Linux API
深入理解Linux虚拟内存管理(六)(下)
深入理解Linux虚拟内存管理(六)
13 0
|
23天前
|
Linux
深入理解Linux虚拟内存管理(六)(中)
深入理解Linux虚拟内存管理(六)
18 0