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

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

一、slab 分配器

1、高速缓存控制

(1)创建高速缓存

(a)kmem_cache_create

  这个函数的调用图如图 8.3 所示。这个函数负责创建一个新的高速缓存,然后根据大小进行批量处理。这个批量处理大约包括如下操作:

  • 进行基本的有效性检查以防错误使用。
  • 如果设置有 CONFIG_SLAB_DEBUG 则进行调试检查。
  • cache_cache slab 高速缓存中分配一个 kmem_cache_t
  • 将对象大小对齐为字大小。
  • 计算在 slab 中有多少合适的对象。
  • slab 大小对齐为硬件高速缓存。
  • 计算着色偏移。
  • 初始化在高速缓存描述符中的其余字段。
  • 将新高速缓存加入到高速缓存链。

传送门 8.1.6 创建高速缓存

// mm/slab.c

/**
 * kmem_cache_create - Create a cache.
 * @name: A string which is used in /proc/slabinfo to identify this cache.
 * @size: The size of objects to be created in this cache.
 * @offset: The offset to use within the page.
 * @flags: SLAB flags
 * @ctor: A constructor for the objects.
 * @dtor: A destructor for the objects.
 *
 * Returns a ptr to the cache on success, NULL on failure.
 * Cannot be called within a int, but can be interrupted.
 * The @ctor is run when new pages are allocated by the cache
 * and the @dtor is run before the pages are handed back.
 * The flags are
 *
 * %SLAB_POISON - Poison the slab with a known test pattern (a5a5a5a5)
 * to catch references to uninitialised memory.
 *
 * %SLAB_RED_ZONE - Insert `Red' zones around the allocated memory to check
 * for buffer overruns.
 *
 * %SLAB_NO_REAP - Don't automatically reap this cache when we're under
 * memory pressure.
 *
 * %SLAB_HWCACHE_ALIGN - Align the objects in this cache to a hardware
 * cacheline.  This can be beneficial if you're counting cycles as closely
 * as davem.
 */
// 这一块进行基本的有效性检查防止错误使用。
// 这个函数的参数如下:
// name是该高速缓存的可读名。
// size是一个对象的大小。
// offset是用于指定高速缓存中对象的边界,但一般设为0。
// flags是静态高速缓存标志位。
// ctor是在slab创建时为每个对象调用的构造函数。
// dtor是相应的销毁函数。销毁函数可以使一个对象回到初始态。 
kmem_cache_t *
kmem_cache_create (const char *name, size_t size, size_t offset,
  unsigned long flags, void (*ctor)(void*, kmem_cache_t *, unsigned long),
  void (*dtor)(void*, kmem_cache_t *, unsigned long))
{
  const char *func_nm = KERN_ERR "kmem_create: ";
  size_t left_over, align, slab_size;
  kmem_cache_t *cachep = NULL;

  /*
   * Sanity checks... these are all serious usage bugs.
   */
// 在创建高速缓存时有各种bug的使用方法。
  if ((!name) ||
  // 这里用于可读名大于最大的高速缓存名(CACHE_MAXELEN)的情况。 
    ((strlen(name) >= CACHE_NAMELEN - 1)) ||
  // 中断处理程序不能创建高速缓存,因为需要访问中断安全的自旋锁以及信号量。  
    in_interrupt() ||
  // 对象大小必须至少是一个字大小。slab分配器不适合于以单个字节为度量的对象。   
    (size < BYTES_PER_WORD) ||
  // 最大可能的slab利用32个页面可以创建 2^MAX_OBJ_ORDER 个页面数。   
    (size > (1<<MAX_OBJ_ORDER)*PAGE_SIZE) ||
  // 如果没有构造函数可用,也不能使用销毁函数。    
    (dtor && !ctor) ||
  // 偏移不能在slab前,也不能超出第1个页面的边界。    
    (offset < 0 || offset > size))
  // 调用BUG()退出。   
      BUG();

// 如果设置了 CONFIG_SLAB_CONFIG,则这一块进行调试检查。
#if DEBUG
  // SLAB_DEBUG_INITIAL需要构造函数对对象进行检查以保证它们处于初始状
  // 态,所以,必须退出一个构造函数。如果没有退出,则清除该标志。
  if ((flags & SLAB_DEBUG_INITIAL) && !ctor) {
    /* No constructor, but inital state check requested */
    printk("%sNo con, but init state check requested - %s\n", func_nm, name);
    flags &= ~SLAB_DEBUG_INITIAL;
  }
  // slab可能会被某种已知模式感染,以保证某个对象在分配之前没有被使用,但
  // 是一个构造函数可能会破坏这种模式,它会错误地报告一个bug。如果存在这样
  // 一个构造函数,则这里如果设置了 SLAB_POISON 就移除该标志位。
  if ((flags & SLAB_POISON) && ctor) {
    /* request for poisoning, but we can't do that with a constructor */
    printk("%sPoisoning requested, but con given - %s\n", func_nm, name);
    flags &= ~SLAB_POISON;
  }
#if FORCED_DEBUG
  // 仅有很少一部分对象是调试的红色区域。很大的红色区域对象将导致严重的碎片。
  if ((size < (PAGE_SIZE>>3)) && !(flags & SLAB_MUST_HWCACHE_ALIGN))
    /*
     * do not red zone large object, causes severe
     * fragmentation.
     */
    flags |= SLAB_RED_ZONE;
  // 如果没有构造函数,这里设置感染位。    
  if (!ctor)
    flags |= SLAB_POISON;
#endif
#endif

  /*
   * Always checks flags, a caller might be expecting debug
   * support which isn't available.
   */
  // 调用所有可以调用的kmem_cache_alloc()来设置CREATE_MASK以
  // 及所有允许设置的标志位。这里阻止调用者在没有这些标志位的时候使用调试标志位,如果
  // 使用则调用BUG()。 
  BUG_ON(flags & ~CREATE_MASK);

  /* Get cache's description obj. */
  // 利用kmem_cache_alloc()从cache_cache中分配一个高速缓存描述符对象。
  cachep = (kmem_cache_t *) kmem_cache_alloc(&cache_cache, SLAB_KERNEL);
  // 如果内存不足.则转到oops.在那里处理OOM。
  if (!cachep)
    goto opps;
  // 用0填充对象以防止意外地使用未初始化的数据。   
  memset(cachep, 0, sizeof(kmem_cache_t));

  /* Check that size is in terms of words.  This is needed to avoid
   * unaligned accesses for some archs when redzoning is used, and makes
   * sure any on-slab bufctl's are also correctly aligned.
   */
// 将对象大小对齐于某个字大小边界。
  // 如果大小没有与某个字大小边界对齐,则……  
  if (size & (BYTES_PER_WORD-1)) {
  // 增加一个对象的字大小,然后屏蔽低位。这能有效地将对象大小向上取整到下
  // 一个字边界
    size += (BYTES_PER_WORD-1);
    size &= ~(BYTES_PER_WORD-1);
  // 为了调试打印一个提示信息。    
    printk("%sForcing size word alignment - %s\n", func_nm, name);
  }
  
#if DEBUG
// 如果调试可用,则必须稍微改变一下对齐方式。
  if (flags & SLAB_RED_ZONE) {
    /*
     * There is no point trying to honour cache alignment
     * when redzoning.
     */
  // 如果slab将红色分区的话,就不需要尝试将硬件高速缓存中的东西对齐了。对象的
  // 红色分区是从高速缓存边界处移动一个字的偏移。    
    flags &= ~SLAB_HWCACHE_ALIGN;
  // 对象的大小增加两个BYTES_PER_WORD,以在对象的两端标记红色区域。   
    size += 2*BYTES_PER_WORD; /* words for redzone */
  }
#endif
  // 初始化对齐方式为一个字边界。如果调用者请求的是一个CPU高速缓存对齐方式,
  // 则在这里改变。
  align = BYTES_PER_WORD;
  // 如果有请求,则这里将对象对齐到LI CPU高速缓存。
  if (flags & SLAB_HWCACHE_ALIGN)
    align = L1_CACHE_BYTES;

  /* Determine if the slab management is 'on' or 'off' slab. */
  // 如果对象比较大,在这里存储slab描述符off-slab,这将更好地允许打包对象到
  // slab 中 
  if (size >= (PAGE_SIZE>>3))
    /*
     * Size is large, assume best to place the slab management obj
     * off-slab (should allow better packing of objs).
     */
    flags |= CFLGS_OFF_SLAB;

  // 如果请求硬件高速缓存对齐,则对象的大小将与硬件高速缓存对齐
  if (flags & SLAB_HWCACHE_ALIGN) {
    /* Need to adjust size so that objs are cache aligned. */
    /* Small obj size, can get at least two per cache line. */
    /* FIXME: only power of 2 supported, was better */
  // 如果对象符合对齐,则试着将对象打包到高速缓存中的一行。对于那些带有大
  // L1高速缓存字节的系统(如Alpha和奔腾4),这很重要。align将被调整为对齐硬件高速缓存
  // 边界的最小值。对大的L1高速缓存行,两个或者更多的小对象将可以填入到一行中。例如,
  // 从32位高速缓存的两个对象将在奔腾4中填入一个高速缓存行。    
    while (size < align/2)
      align /= 2;
  // 将高速缓存大小向上取整为硬件高速缓存边界。      
    size = (size+align-1)&(~(align-1));
  }

  /* Cal size (in pages) of slabs, and the num of objs per slab.
   * This could be made much more intelligent.  For now, try to avoid
   * using high page-orders for slabs.  When the gfp() funcs are more
   * friendly towards high-order requests, this should be changed.
   */
// 计算slab中将填入多少个对象,并在需要时调整slab大小。  
  do {
    unsigned int break_flag = 0;
cal_wastage:
  // kmem_cache_estimate。计算以gfp次序填入slab的对象数,并
  // 计算剩余的字节数。
    kmem_cache_estimate(cachep->gfporder, size, flags,
            &left_over, &cachep->num);
  // 在使用了 offslab slab描述符后,如果填入slab的对象数超过了 slab存放的对
  // 象数,则设置break_flag。            
    if (break_flag)
      break;
  // 页面使用的次数不能超过MAX_GFP_ORDER(5)。     
    if (cachep->gfporder >= MAX_GFP_ORDER)
      break;
  // 如果对象不能填入,则跳转到next,在那里将增加高速缓存使用的gfporder。     
    if (!cachep->num)
      goto next;
  // 如果在高速缓存中没有slab描述符,但对象数超过了bufctl的off-slab所确定的数
  // 目,测……      
    if (flags & CFLGS_OFF_SLAB && cachep->num > offslab_limit) {
      /* Oops, this num of objs will cause problems. */
  // 降低使用页面的次数。     
      cachep->gfporder--;
  // 设置break_flag标志位,这样将退出循环。
      break_flag++;
  // 计算新的消耗数。     
      goto cal_wastage;
    }

    /*
     * Large num of objs is good, but v. large slabs are currently
     * bad for the gfp()s.
     */
  // 除非填充了0号对象,slab_break_gfp_order将是不超过slab的次数。这里检查以保证不会超过次数。   
    if (cachep->gfporder >= slab_break_gfp_order)
      break;

// 对外部碎片的粗略检查。如果高速缓存的消耗量小于八分之一,则可以接受。
    if ((left_over*8) <= (PAGE_SIZE<<cachep->gfporder))
      break;  /* Acceptable internal fragmentation. */
next:
  // 如果碎片过多,则这里增加gfp次,并重新计算存储的对象数目以及消耗量。
    cachep->gfporder++;
  } while (1);

  // 在调整后如果对象还是不能填入高速缓存,则不能创建该对象。
  if (!cachep->num) {
    printk("kmem_cache_create: couldn't create cache %s.\n", name);
  // 释放高速缓存描述符并设指针指向NULL。   
    kmem_cache_free(&cache_cache, cachep);
  // 跳转到oops,在那里仅返回NULL指针。    
    cachep = NULL;
    goto opps;
  }
// 这一块将slab大小对齐于硬件高速缓存。
  // slab_size是slab描述符的总大小,不是slab自身的大小。它是一个固定的slab_t结
  // 构,大小为对象数乘以bufctl的结果。 
  slab_size = L1_CACHE_ALIGN(cachep->num*sizeof(kmem_bufctl_t)+sizeof(slab_t));

  /*
   * If the slab has been placed off-slab, and we have enough space then
   * move it on-slab. This is at the expense of any extra colouring.
   */
  // 如果有足够的空间留给slab描述符,则这里指定为放置off-slab描述符,这里
  // 移除标志位并更新left_over字节量。这样做会影响到高速缓存着色,但是对off-slab描述符 
  // 而言,这不是问题。
  if (flags & CFLGS_OFF_SLAB && left_over >= slab_size) {
    flags &= ~CFLGS_OFF_SLAB;
    left_over -= slab_size;
  }

  /* Offset must be a multiple of the alignment. */
// 计算着色偏移。
  // offset是请求调用者页面中的偏移。这里保证所请求偏移在使用中的高速缓存
  // 的正确对齐方式中。  
  offset += (align-1);
  offset &= ~(align-1);
  // 如果由于某个原因偏移为0,则这里设置它与CPU高速缓存对齐。
  if (!offset)
    offset = L1_CACHE_BYTES;
  // 这个偏移用于在不同的高速缓存行中存入对象。对创建的每个slab,将给予不同的
  // 颜色偏移。    
  cachep->colour_off = offset;
  // 可以使用的不同偏移数。
  cachep->colour = left_over/offset;

// 这一块初始化高速缓存的其他字段。
  /* init remaining fields */
  // 对只有一个页面的slab高速缓存,设置CFLAGS_OPTIMIZE标志位,这实际
  // 上没有什么效果,因为并没有用到该标志位。
  if (!cachep->gfporder && !(flags & CFLGS_OFF_SLAB))
    flags |= CFLGS_OPTIMIZE;

  // 设置高速缓存静态标志位。
  cachep->flags = flags;
  // 将gfpflags清0。这是个无效操作,因为可以使用memset()在分配了高速缓存描述
  // 符后清除这些标志位。
  cachep->gfpflags = 0;
  // 如果slab用于DMA,则这里设置GFP_DMA标志位,这样伙伴分配器将使用ZONE_DMA。
  if (flags & SLAB_CACHE_DMA)
    cachep->gfpflags |= GFP_DMA;
  // 为访问高速缓存初始化自旋锁。   
  spin_lock_init(&cachep->spinlock);
  // 复制对象大小,如果需要,则按硬件高速缓存大小对齐。
  cachep->objsize = size;
  // 初始化slab链表。
  INIT_LIST_HEAD(&cachep->slabs_full);
  INIT_LIST_HEAD(&cachep->slabs_partial);
  INIT_LIST_HEAD(&cachep->slabs_free);
  // 如果描述符是off-slab的,这里分配和放置一个slab管理器以在slabp_cache使用。
  if (flags & CFLGS_OFF_SLAB)
    cachep->slabp_cache = kmem_find_general_cachep(slab_size,0);
  // 设置指向构造函数和销毁函数的指针。    
  cachep->ctor = ctor;
  cachep->dtor = dtor;
  /* Copy name over so we don't have problems with unloaded modules */
  // 复制可读名。
  strcpy(cachep->name, name);

#ifdef CONFIG_SMP
  // 如果per-CPU可用,这里为该高速缓存创建一个集(见8.5节)。
  if (g_cpucache_up)
    enable_cpucache(cachep);
#endif
// 这一块将新的高速缓存加入到高速缓存链表中。
  /* Need the semaphore to access the chain. */
  // 获取对高速缓存链表同步访问的信号量。
  down(&cache_chain_sem);
  {
    struct list_head *p;
// 检查在高速缓存链表中的每个高速缓存,并保证没有其他的高速缓存具有相同
// 的名字。如果有相同的名字,则意味着创建了具有相同类型的两个高速缓存,这是一个严重
// 的 bug。
    list_for_each(p, &cache_chain) {
  // 从链表中获取高速缓存。    
      kmem_cache_t *pc = list_entry(p, kmem_cache_t, next);

      /* The name field is constant - no lock needed. */
  // 比较名字,如果相同则调用BUG()。值的注意的是并不删除新的高速缓存,这
  // 个错误是开发时由于不规范编程而造成的,是一个常见的问题。     
      if (!strcmp(pc->name, name))
        BUG();
    }
  }

  /* There is no reason to lock our new cache before we
   * link it in - no one knows about it yet...
   */
  // 将高速缓存链到链表中。   
  list_add(&cachep->next, &cache_chain);
  // 释放高速缓存链表信号量。
  up(&cache_chain_sem);
opps:
  // 返回新的高速缓存指针。
  return cachep;
}

    使用 kmem_cache_alloc 分配一个 kmem_cache_t,然后根据 size 计算 cachep->gfporder,最后把这个 kmem_cache_t 加入到全局链表中。

① ⇒ kmem_cache_alloc

    kmem_cache_alloc 函数

② ⇒ kmem_cache_estimate

    kmem_cache_estimate 函数

③ ⇒ kmem_cache_free

    kmem_cache_free 函数

④ ⇒ kmem_find_general_cachep

    kmem_find_general_cachep 函数

(2)计算 slab 上的对象数量

(a)kmem_cache_estimate

传送门 8.2.1 存储 slab 描述符

    在创建高速缓存时,可以存放多少个对象以及需要耗费多少空间是确定的。下面的函数计算有多少个对象可以存储,并且考虑到 slabbufctls 必须以 on-slab 方式存放。

// mm/slab.c
/* Cal the num objs, wastage, and bytes left over for a given slab size. */
// 这个函数的参数如下:
// gfporder为每个slab分配2^gfporder个页面。
// size是每个对象的大小。
// flags是高速缓存标志位。
// left_over是slab中剩余的字节数,由调用者返回。
// num是填入slab的对象数,由调用者返回。
static void kmem_cache_estimate (unsigned long gfporder, size_t size,
     int flags, size_t *left_over, unsigned int *num)
{
  int i;
  // wastage是一个递减函数。它从可能的最大消耗量开始。
  size_t wastage = PAGE_SIZE<<gfporder;
  // extra是需要存储kmem_bufctl_t的字节数。
  size_t extra = 0;
  // base是在slab开始处的可用内存地址
  size_t base = 0;

  // 如果slab描述符保存在高速缓存中,基地址开始于slab_t结构的末端,存放bufctl所
  // 需的字节数是kmem_bufctl_t的大小。
  if (!(flags & CFLGS_OFF_SLAB)) {
    base = sizeof(slab_t);
    extra = sizeof(kmem_bufctl_t);
  }
  // i变成slab可以拥有的对象数。
  i = 0;
  // 将高速缓存可以容纳的对象数加起来。i * size 是对象自身的大小。
  // L1_CACHE_ALIGN(base + i * extra)有一点灵活性。这是一个计算需要存储在slab中每个
  // 对象所需kmem_bufctl_t的内存量函数。由于它在slab的起点,所以它与L1高速缓存对齐,
  // 这样slab中的第1个对象将与硬件高速缓存对齐。i* extra将计算为容纳该对象kmem_bufctl_t
  // 所需的空间。由于消耗量以slab的大小计算,所以在这里用于负载。
  while (i*size + L1_CACHE_ALIGN(base+i*extra) <= wastage)
    i++;
  // 由于前面循环计数直到slab溢出,所以对象的数量记为 i-1。    
  if (i > 0)
    i--;
  // SLAB_LIMIT是一个slab可存储对象的最大绝对值。它定义为OxffffFFFE, 
  // 因为这是kmem_bufctl_t可以容纳的最大无符号整数中最大的数值。
  if (i > SLAB_LIMIT)
    i = SLAB_LIMIT;
  // num现在是slab可以容纳的对象数。
  *num = i;
  // 减去消耗对象所占据的空间。
  wastage -= i*size;
  // 减去由kmem_bufctl_t所占据的空间。
  wastage -= L1_CACHE_ALIGN(base+i*extra);
  // 现在消耗量计算为slab中剩下的空间。
  *left_over = wastage;
}

    计算 gfporder 页面可以容纳多少个 slab 对象。

(3)收缩高速缓存

   kmem_cache_shrink() 的调用图如图 8.5 所示。提供两套收缩函数。kmem_cache_shrink() 从 slabs_free 中移除所有 slab ,并返回释放的页面数作为结果。__kmem_cache_shrink() 从 slabs_free 中释放所有的 slab ,然后验证 slabs_partial 和 slabs_free 是否为空。这在销毁高速缓存时比较重要,在那时它不关心释放的页面数,而只关心高速缓存是否为空。

(a)kmem_cache_shrink

传送门 8.1.8 收缩高速缓存

    这个函数进行基本的调试检查,然后获取高速缓存描述符,接着释放 slab。在这时,它也用于调用 drain_cpu_caches() 来释放在 per-CPU 高速缓存中的对象。很奇怪的是,由于对象可以在 per-CPU 高速缓存中分配,slab 可能不能被释放,这里却移除,并不使用。

// mm/slab.c
/**
 * kmem_cache_shrink - Shrink a cache.
 * @cachep: The cache to shrink.
 *
 * Releases as many slabs as possible for a cache.
 * Returns number of pages released.
 */
// 参数是被收缩的高速缓存。
// 进行如下检查:
//    高速缓存不为NULL。
//    调用者不是一个异常。
//    高速缓存在高速缓存链表中,且不是一个坏指针。
int kmem_cache_shrink(kmem_cache_t *cachep)
{
  int ret;

  if (!cachep || in_interrupt() || !is_chained_kmem_cache(cachep))
    BUG();
  // 获取高速缓存描述符锁并关中断。
  spin_lock_irq(&cachep->spinlock);
  // 收缩高速缓存区。
  ret = __kmem_cache_shrink_locked(cachep); 
  // 释放高速缓存锁并开中断。
  spin_unlock_irq(&cachep->spinlock);
  // 返回释放的页面数,但是并没有考虑消耗 CPU 时释放的对象。 
  return ret << cachep->gfporder;
}
(b)__kmem_cache_shrink

    这个函数与 kmem_cache_shrink() 相似,除了它在高速缓存为空时返回。这在销毁高速缓存时比较重要,在那时,释放的内存量并不很重要,重要的是安全地删除高速缓存,且不泄漏内存。

// mm/slab.c
static int __kmem_cache_shrink(kmem_cache_t *cachep)
{
  int ret;
  // 从per-CPU对象高速缓存中移除所有对象。
  drain_cpu_caches(cachep);
  // 获取高速缓存描述符锁并关中断
  spin_lock_irq(&cachep->spinlock);
  // 释放slabs_free链表中的所有slab。
  __kmem_cache_shrink_locked(cachep);
  // 检查slabs_partial和slabs_full链表是否为空
  ret = !list_empty(&cachep->slabs_full) ||
    !list_empty(&cachep->slabs_partial);
  // 释放高速缓存描述符锁并重新打开中断  
  spin_unlock_irq(&cachep->spinlock);
  // 如果释放了高速缓存中所有slab,则返回。
  return ret;
}


(c)__kmem_cache_shrink_locked

    这里完成释放 slab 的实际工作。它将不断地销毁 slab 直到设置了增长标志位,表明高速缓存正在使用或者直到 slabs_free 中没有更多的 slab

// mm/slab.c

/*
 * Called with the &cachep->spinlock held, returns number of slabs released
 */
static int __kmem_cache_shrink_locked(kmem_cache_t *cachep)
{
  slab_t *slabp;
  int ret = 0;

  /* If the cache is growing, stop shrinking. */
  // 当高速缓存不再增长时,这里释放slab。
  // 每创建一个 slab_t ,growing 增长一次,所以这里遍历所有的 slab_t ?
  while (!cachep->growing) {
    struct list_head *p;
// 获取slabs_free链表中最后一个slab。
    p = cachep->slabs_free.prev;
    if (p == &cachep->slabs_free)
      break;

    slabp = list_entry(cachep->slabs_free.prev, slab_t, list);
#if DEBUG
  // 如果调试可用,则这里保证它目前不在使用中。如果不可用,则它首先就不会
  // 在slabs_free链表中。
    if (slabp->inuse)
      BUG();
#endif
  // 从链表中移除slab。
    list_del(&slabp->list);
  // 重新打开中断。调用这个函数时关闭了中断,所以需要尽快地释放中断。
    spin_unlock_irq(&cachep->spinlock);
  // 利用 kmem_slab_destroy() 删除 slab。    
    kmem_slab_destroy(cachep, slabp);
  // 记录释放的slab数。    
    ret++;
  // 获取高速缓存描述符锁并关中断。    
    spin_lock_irq(&cachep->spinlock);
  }
  return ret;
}

    主要释放 kmem_cache_t->slabs_free 中的 slab_t

① ⇒ kmem_slab_destroy

    kmem_slab_destroy 函数

(4)销毁高速缓存

    当卸载某个模块时,如果创建了高速缓存,则它负责销毁高速缓存。由于在载入一个模块时已经保证没有两个相同名字的高速缓存。内核代码经常不销毁它自己的高速缓存,因为它们在整个系统生命周期中都一直存在。销毁一个高速缓存的步骤如下:

  • 从高速缓存链表中删除该高速缓存。
  • 收缩高速缓存,删除所有的 slab(见 8.1.8 小节)。
  • 释放所有的 per-CPU 高速缓存( kfree() )。
  • cache_cache 中删除高速缓存描述符(见 8.3.3 小节)。
(a)kmem_cache_destroy

    这个函数的调用图如图 8.7 所示。

// mm/slab.c

/**
 * kmem_cache_destroy - delete a cache
 * @cachep: the cache to destroy
 *
 * Remove a kmem_cache_t object from the slab cache.
 * Returns 0 on success.
 *
 * It is expected this function will be called by a module when it is
 * unloaded.  This will remove the cache completely, and avoid a duplicate
 * cache being allocated each time a module is loaded and unloaded, if the
 * module doesn't have persistent in-kernel storage across loads and unloads.
 *
 * The cache must be empty before calling this function.
 *
 * The caller must guarantee that noone will allocate memory from the cache
 * during the kmem_cache_destroy().
 */
int kmem_cache_destroy (kmem_cache_t * cachep)
{
  // 有效性检查。保证cachep不为空,没有中断正尝试使用这个函数,以及高速
  // 缓存没有标记为增长,表明它正在使用中。
  if (!cachep || in_interrupt() || cachep->growing)
    BUG();

  /* Find the cache in the chain of caches. */
  // 获取访问高速缓存链表的信号量。
  down(&cache_chain_sem);
  /* the chain is never empty, cache_cache is never destroyed */
  // 从高速缓存链表中获取一个链表项。
  if (clock_searchp == cachep)
    clock_searchp = list_entry(cachep->next.next,
            kmem_cache_t, next);
  // 从高速缓存链表中删除该高速缓存。           
  list_del(&cachep->next);
  // 释放高速缓存链表信号量。
  up(&cache_chain_sem);
  // 利用__kmem_cache_shrink()收缩高速缓存,释放所有的slab 。
  if (__kmem_cache_shrink(cachep)) {
  // 如果在高速缓存中还存在slab,则这个收缩函数返回真。如果它们是无法销
  // 毁的高速缓存,则这里将其加入到高速缓存链表中,并报告错误。
    printk(KERN_ERR "kmem_cache_destroy: Can't free all objects %p\n",
           cachep);
    down(&cache_chain_sem);
    list_add(&cachep->next,&cache_chain);
    up(&cache_chain_sem);
    return 1;
  }
  
#ifdef CONFIG_SMP
  {
    int i;
  // 如果SMP可用,则利用 kfree() 删除per-CPU数据结构。   
    for (i = 0; i < NR_CPUS; i++)
      kfree(cachep->cpudata[i]);
  }
#endif
  // 利用kmem_cache_free()从cache_cache中删除高速缓存描述符。 
  kmem_cache_free(&cache_cache, cachep);

  return 0;
}
① ⇒ __kmem_cache_shrink

    __kmem_cache_shrink 函数

② ⇒ kfree

    kfree 函数

③ ⇒ kmem_cache_free

    kmem_cache_free 函数

(5)回收高速缓存

(a)kmem_cache_reap

    这个函数的调用如图 8.4 所示。由于这个函数比较大,所以将其分成几个独立的小节。第 1 部分是一个简单的函数开始部分,第 2 部分选择要回收的高速缓冲,第 3 部分是释放 slab,基本的任务在 8.1.7 小节中描述。

传送门 8.1.7 回收高速缓存

// mm/slab.c
/**
 * kmem_cache_reap - Reclaim memory from caches.
 * @gfp_mask: the type of memory required.
 *
 * Called from do_try_to_free_pages() and __alloc_pages()
 */
// 惟一的一个参数是GFP标志位。惟一做的检查是对__GFP_WAIT标志位进行检
// 查。作为惟一的调用者,kswapd可以睡眠。这个参数实际上没有用。 
int kmem_cache_reap (int gfp_mask)
{
  slab_t *slabp;
  kmem_cache_t *searchp;
  kmem_cache_t *best_cachep;
  unsigned int best_pages;
  unsigned int best_len;
  unsigned int scan;
  int ret = 0;
  
  // 调用者可以睡眠吗?如果可以,则在这里获取信号量。
  if (gfp_mask & __GFP_WAIT)
    down(&cache_chain_sem);
  else
  // 如果不可以睡眠,则这里试着获取信号量。如果没有信号量可用,则返回。
    if (down_trylock(&cache_chain_sem))
      return 0;

  // REAP_SCANLEN(10)是要检查的高速缓存数目。
  scan = REAP_SCANLEN;
  best_len = 0;
  best_pages = 0;
  best_cachep = NULL;
  // 设置searchp为上次回收时检查过的最后一个高速缓存。
  searchp = clock_searchp;
// 这一块考察REAP_SCANLEN数量个高速缓存,并选择一个来释放。 
  do {
    unsigned int pages;
    struct list_head* p;
    unsigned int full_free;

    /* It's safe to test this without holding the cache-lock. */
    if (searchp->flags & SLAB_NO_REAP)
      goto next;
  // 获取一个对高速缓存描述符的中断安全锁。      
    spin_lock_irq(&searchp->spinlock);
  // 如果高速缓存在增长,这里跳过它。   
    if (searchp->growing)
      goto next_unlock;
  // 如果高速缓存最近增长过,则这里跳过它并清除标志位。      
    if (searchp->dflags & DFLGS_GROWN) {
      searchp->dflags &= ~DFLGS_GROWN;
      goto next_unlock;
    }
#ifdef CONFIG_SMP
  // 释放per-CPU对象到全局池中。
    {
      cpucache_t *cc = cc_data(searchp);
      if (cc && cc->avail) {
        __free_block(searchp, cc_entry(cc), cc->avail);
        cc->avail = 0;
      }
    }
#endif

    full_free = 0;
    p = searchp->slabs_free.next;
  // 计算在slab_free链表中的slab数量。    
    while (p != &searchp->slabs_free) {
      slabp = list_entry(p, slab_t, list);
#if DEBUG
      if (slabp->inuse)
        BUG();
#endif
      full_free++;
      p = p->next;
    }

    /*
     * Try to avoid slabs with constructors and/or
     * more than one page per slab (as it can be difficult
     * to get high orders from gfp()).
     */
  // 计算所有slab持有的页面数量。    
    pages = full_free * (1<<searchp->gfporder);
  // 如果对象有构造函数,则这里将其页面计数减为1/5,这样就不太可能选择它来回收。    
    if (searchp->ctor)
      pages = (pages*4+1)/5;
  // 如果slab由多于一页组成,这里减少页面计数到1/5。这是因为难以获取高次的页面。      
    if (searchp->gfporder)
      pages = (pages*4+1)/5;
  // 如果这是目前找到的最好的回收候选高速缓存,这里检查它是否适合回收。      
    if (pages > best_pages) {
  // 记录新的最大量。   
      best_cachep = searchp;
  // 记录best_len ,这样很容易知道空闲链表中 slab 一半的slab数量。     
      best_len = full_free;
      best_pages = pages;
  // 如果这个高速缓存适合回收,则 ...     
      if (pages >= REAP_PERFECT) {
  // 更新 clock_searchp。      
        clock_searchp = list_entry(searchp->next.next,
              kmem_cache_t,next);
  // 转到perfect在那里释放一半的slab。             
        goto perfect;
      }
    }
next_unlock:
    spin_unlock_irq(&searchp->spinlock);
next:
    searchp = list_entry(searchp->next.next,kmem_cache_t,next);
  // 在达到REAP_SCANLEN之前,且在还没有循环遍历整个高速缓存链表之前一直扫描
  } while (--scan && searchp != clock_searchp);
  
// 这一块从选择的高速缓存中释放一半的slab.
  // 更新clock_searchp 以准备下一次高速缓存回收。
  clock_searchp = searchp;
  // 如果没有找到一个高速缓存,则转到 out 以释放高速缓存链表并退出。
  if (!best_cachep)
    /* couldn't find anything to reap */
    goto out;
  
  // 获取高速缓存链表自旋锁,并关闭中断。cachep描述符必须持有一个中断安全的
  // 锁,因为一些高速缓存可能在中断上下文中使用。slab分配器无法区分中断安全
  // 和中断不安全的高速缓存。
  spin_lock_irq(&best_cachep->spinlock);
perfect:
  /* free only 50% of the free slabs */
  // 调整best_len为要释放的slab数量。
  best_len = (best_len + 1)/2;
// 释放 best_len 个 slab。  
  for (scan = 0; scan < best_len; scan++) {
    struct list_head *p;
  // 如果高速缓存正在增长,则在这里退出。
    if (best_cachep->growing)
      break;
  // 从链表中获得一个slab。    
    p = best_cachep->slabs_free.prev;
  // 如果在链表中没有一个slab,则在这里退出。     
    if (p == &best_cachep->slabs_free)
      break;
  // 获取slab指针。    
    slabp = list_entry(p,slab_t,list);
#if DEBUG
  // 如果调试可用,则这里保证没有活动的对象在slab中。
    if (slabp->inuse)
      BUG();
#endif
  // 从 slabs_free 链表中移除 slab。
    list_del(&slabp->list);
  // 如果可用则更新统计计数。   
    STATS_INC_REAPED(best_cachep);

    /* Safe to drop the lock. The slab is no longer linked to the
     * cache.
     */
  // 释放高速缓存并开中断。     
    spin_unlock_irq(&best_cachep->spinlock);
  // 释放 slab(见 8.2.8 小节)。   
    kmem_slab_destroy(best_cachep, slabp);
  // 重新获取高速缓存描述符的自旋锁并关中断。   
    spin_lock_irq(&best_cachep->spinlock);
  }
  // 释放高速缓存描述符并开中断。
  spin_unlock_irq(&best_cachep->spinlock);
  // ret是已经释放的页面数。
  ret = scan * (1 << best_cachep->gfporder);
out:
  // 释放高速缓存信号量并返回释放的页面数。
  up(&cache_chain_sem);
  return ret;
}

    考察 clock_searchp 开始 REAP_SCANLEN(10)kmem_cache_tslabs_freeslat_t 的数量。选择最多的进行释放。

2、slabs

(1)存储 slab 描述符

传送门 8.2 slabs

(a)kmem_cache_slabmgmt

    这个函数或者为不在高速缓存中的 slab 描述符分配空间,或者在 slab 开始的地方为描述符和 bufctls 保留足够的空间。

// mm/slab.c

/* Get the memory for a slab management obj. */
// 这个函数的参数如下:
//  cachep是slab要分配的高速缓存。
//  objp是当调用该函数时指向slab的开始处。
//  colour_off是这个slab的颜色偏移。
//  local_flags是高速缓存的标志位。
static inline slab_t * kmem_cache_slabmgmt (kmem_cache_t *cachep,
      void *objp, int colour_off, int local_flags)
{
  slab_t *slabp;

// 如果slab描述符并不存放在高速缓存中,则... 
  if (OFF_SLAB(cachep)) {
    /* Slab management obj is off-slab. */
  // 从指定大小的高速缓存中分配内存。在创建高速缓存时,slabp_cache设置为分配
  // 内存指定大小的高速缓存。   
    slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags);
  // 如果分配失败,这里返回。   
    if (!slabp)
      return NULL;
  } else {
    /* FIXME: change to
      slabp = objp
     * if you enable OPTIMIZE
     */
// 在slab的起点保留空间。  
  // slab的地址将是slab的起点(objp)加上颜色偏移。  
    slabp = objp+colour_off;
  // colour_off 计算为放置第一个对象的偏移位置。该地址对齐于L1高速缓存,
  // cachep—>num * sizeof(kmem_bufctl_t) 是容纳slab中各对象的bufctls所需的空间大小,
  // sizeof(slab_t)是slab描述符的大小。这里在slab起点已经有效地保留了空间。   
    colour_off += L1_CACHE_ALIGN(cachep->num *
        sizeof(kmem_bufctl_t) + sizeof(slab_t));
  }
  // 在slab中使用的对象数为0。
  slabp->inuse = 0;
  // 更新 colouroff 以放置新对象。
  slabp->colouroff = colour_off;
  // 第1个对象的地址计算为slab的开始地址加上偏移。
  slabp->s_mem = objp+colour_off;

  return slabp;
}


kmem_cache_slabmgmt 函数主要用来分配一个 slab_t 对象(slab 管理对象):

  • 如果 slab_tslab 没放在一起(OFF_SLAB(cachep)),则调用 kmem_cache_alloc 分配一个 slab_t
  • 否则,从 objp 内存处分配一个 slab_t ,同时调整 colour_off 偏移。

    最后更新 slabpinusecolouroffs_mem 字段域。

① ⇒ kmem_cache_alloc

    kmem_cache_alloc 函数

(b)kmem_find_general_cachep

    如果 slab 描述符不在 slab 中保存,则这个函数在创建高速缓存时被调用,它将找到合适大小的高速缓存供使用,并将其存放在 slabp_cache 的高速缓存描述符中。

// mm/slab.c
// size是slab描述符的大小,gfpflags总是为0,因为DMA内存不需要slab描述符。
kmem_cache_t * kmem_find_general_cachep (size_t size, int gfpflags)
{
  cache_sizes_t *csizep = cache_sizes;

  /* This function could be moved to the header file, and
   * made inline so consumers can quickly determine what
   * cache pointer they require.
   */
  // 从最小的大小开始,这里不断增加大小,直至找到一个足够大的缓冲区来存
  // 放slab描述符的高速缓存。  
  for ( ; csizep->cs_size; csizep++) {
    if (size > csizep->cs_size)
      continue;
    break;
  }
  // 返回一个普通的或DMA大小的高速缓存,这取决于传入的gfpflags标志位。
  // 实际上,仅传回 cs_cachep。
  return (gfpflags & GFP_DMA) ? csizep->cs_dmacachep : csizep->cs_cachep;
}

(2)创建 slab

(a)kmem_cache_grow

    这个函数的调用图如图 8.11 所示。这个函数的基本步骤如下:

  • 进行基本的有效性检查以防止错误使用。
  • 计算该 slab 中对象的颜色偏移。
  • slab 分配内存,并获取 slab 描述符。
  • slab 使用的页面链接到 slab 和高速缓存描述符。
  • 初始化 slab 中的对象。
  • slab 加入高速缓存。

传送门 8.2.2 创建 slab

// mm/slab.c
/*
 * Grow (by 1) the number of slabs within a cache.  This is called by
 * kmem_cache_alloc() when there are no active objs left in a cache.
 */
// 这是一些基本的声明。这个函数的参数如下:
//  cachep是要分配新slab的高速缓存
//  flags创建slab的标志位。 
static int kmem_cache_grow (kmem_cache_t * cachep, int flags)
{
  slab_t  *slabp;
  struct page *page;
  void    *objp;
  size_t     offset;
  unsigned int   i, local_flags;
  unsigned long  ctor_flags;
  unsigned long  save_flags;

  /* Be lazy and only check for valid flags here,
   * keeping it out of the critical path in kmem_cache_alloc().
   */  
// 这里进行基本的有效性检查以防止错误使用。在这里进行检查,而不是利用 kmem_cache_alloc()
// 来保护速度优先的路径。这里不需要在每次分配对象时都检查这些标志位。 
  // 保证仅使用允许的标志位来进行分配。
  if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW))
    BUG();
  // 如果设置了这些标志位,则不增长高速缓存,实际上,从来不设置这些标志位。    
  if (flags & SLAB_NO_GROW)
    return 0;

  /*
   * The test for missing atomic flag is performed here, rather than
   * the more obvious place, simply to reduce the critical path length
   * in kmem_cache_alloc(). If a caller is seriously mis-behaving they
   * will eventually be caught here (where it matters).
   */
  // 如果在中断上下文中调用,保证设置了ATOMIC标志位。这样我们在调用
  // kmem_getpages() 时不会睡眠。  
  if (in_interrupt() && (flags & SLAB_LEVEL_MASK) != SLAB_ATOMIC)
    BUG();
  // 这个标志位告知构造函数初始化对象。
  ctor_flags = SLAB_CTOR_CONSTRUCTOR;
  // local_flags 仅与页面分配器相关。
  local_flags = (flags & SLAB_LEVEL_MASK);
  // 如果设置了 SLAB_ATOMIC 标志位,则构造函数在创建一次新分配时就需要知道它。
  if (local_flags == SLAB_ATOMIC)
    /*
     * Not allowed to sleep.  Need to tell a constructor about
     * this - it might need to know...
     */
    ctor_flags |= SLAB_CTOR_ATOMIC;

// 计算该slab中对象的颜色偏移。
  /* About to mess with non-constant members - lock. */
  // 获取访问高速缓存描述符的中断安全锁。
  spin_lock_irqsave(&cachep->spinlock, save_flags);

  /* Get colour for the slab, and cal the next value. */
  // 获取该slab中的对象偏移。
  offset = cachep->colour_next;
  // 移到下一个颜色偏移。
  cachep->colour_next++;
  // 如果达到colour,就没有更多的偏移,这里将 colour_next 重置为0。
  if (cachep->colour_next >= cachep->colour)
    cachep->colour_next = 0;
  // colour_off是每个偏移的大小。所以 offset * colour_off 表明对象偏移的字节数。    
  offset *= cachep->colour_off;
  // 将高速缓存标记为增长,这样 kmem_cache_reap() 将忽略该高速缓存。
  cachep->dflags |= DFLGS_GROWN;
  // 增加增长该高速缓存的调用者计数。
  cachep->growing++;
  // 释放自旋锁并重新打开中断。
  spin_unlock_irqrestore(&cachep->spinlock, save_flags);

  /* A series of memory allocations for a new slab.
   * Neither the cache-chain semaphore, or cache-lock, are
   * held, but the incrementing c_growing prevents this
   * cache from being reaped or shrunk.
   * Note: The cache could be selected in for reaping in
   * kmem_cache_reap(), but when the final test is made the
   * growing value will be seen.
   */

// 为slab分配内存并获取一个slab描述符。
  /* Get mem for the objs. */
  // 利用 kmem_getpages() 从页面分配器中为slab分配页面。
  if (!(objp = kmem_getpages(cachep, flags)))
    goto failed;

  /* Get slab management. */
  // 利用 kmem_cache_slabmgmt() 获取一个 slab 描述符。
  if (!(slabp = kmem_cache_slabmgmt(cachep, objp, offset, local_flags)))
    goto opps1;

// 将slab使用的页面链接到slab和高速缓存描述符。
  /* Nasty!!!!!! I hope this is OK. */
  // i是slab使用的页面数。每个页面都必须链接到slab和高速缓存描述符。
  i = 1 << cachep->gfporder;
  // objp是一个指向slab起点的指针。宏virt_to_page()将赋予struct page该地址值。
  page = virt_to_page(objp);
// 将每个页面链接到slab字段以及高速缓存描述符。 
  do {
  // SET_PAGE_CACHE()使用page->link_next字段将页面链接到高速缓存描述符。  
    SET_PAGE_CACHE(page, cachep);
  // SET_PAGE_SLAB()使用page->link_prev字段将页面链接到高速缓存描述符。   
    SET_PAGE_SLAB(page, slabp);
  // 设置PG_slab页面字段。整个PG_flags在表2.1中列出。    
    PageSetSlab(page);
  // 移到slab中下一个页面进行链接。    
    page++;
  } while (--i);

  // 初始化对象
  kmem_cache_init_objs(cachep, slabp, ctor_flags);
// 将slab加入到高速缓存中。
  // 以一种中断安全的方式获取高速缓存描述符自旋锁。
  spin_lock_irqsave(&cachep->spinlock, save_flags);
  // 将增长计数减1。
  cachep->growing--;

  /* Make slab active. */
  // 将slab加入到slabs_free链表的末尾。
  list_add_tail(&slabp->list, &cachep->slabs_free);
  // 如果设置了 STATS,这里增加 cachepf->grown 字段 STATS_INC_GROWN()
  STATS_INC_GROWN(cachep);
  // 设置失败为0,这个字段在其他地方没有用到。
  cachep->failures = 0;
  // 以一种中断安全的方式释放高速缓存描述符自旋锁。
  spin_unlock_irqrestore(&cachep->spinlock, save_flags);
  // 返回成功。
  return 1;
  
// 这一块进行错误处理: 
  // 如果为slab分配了页面,则转到oopsl。必须在这里释放这些页面。
opps1:
  kmem_freepages(cachep, objp);
failed:
  // 为访问高速缓存描述符获取自旋锁。
  spin_lock_irqsave(&cachep->spinlock, save_flags);
  // 将增长计数减1。
  cachep->growing--;
  // 释放自旋锁。
  spin_unlock_irqrestore(&cachep->spinlock, save_flags);
  // 返回假。
  return 0;
}
① ⇒ kmem_getpages

    kmem_getpages 函数

② ⇒ kmem_cache_slabmgmt

    kmem_cache_slabmgmt 函数

③ ⇒ kmem_cache_init_objs

    kmem_cache_init_objs 函数

④ ⇒ kmem_freepages

    kmem_freepages 函数

(3)销毁 slab

(a)kmem_slab_destroy

    这个函数的调用图如图 8.13 所示。为了便于阅读,调试部分已经从这个函数中略去,它们几乎与分配对象时的调试部分相同。如何标记以及检查感染模式见 H.3.1.1。

传送门 8.2.8 销毁 slab

// mm/slab.c
/* Destroy all the objs in a slab, and release the mem back to the system.
 * Before calling the slab must have been unlinked from the cache.
 * The cache-lock is not held/needed.
 */
static void kmem_slab_destroy (kmem_cache_t *cachep, slab_t *slabp)
{
// 如果有销毁函数可用,这里将对slab中每个对象调用它。
  if (cachep->dtor
#if DEBUG
    || cachep->flags & (SLAB_POISON | SLAB_RED_ZONE)
#endif
  ) {
    int i;
// 遍历slab中每个对象。   
    for (i = 0; i < cachep->num; i++) {
  // 计算要销毁对象的地址。    
      void* objp = slabp->s_mem+cachep->objsize*i;
#if DEBUG
      if (cachep->flags & SLAB_RED_ZONE) {
        if (*((unsigned long*)(objp)) != RED_MAGIC1)
          BUG();
        if (*((unsigned long*)(objp + cachep->objsize
            -BYTES_PER_WORD)) != RED_MAGIC1)
          BUG();
        objp += BYTES_PER_WORD;
      }
#endif
  // 调用销毁函数。
      if (cachep->dtor)
        (cachep->dtor)(objp, cachep, 0);
#if DEBUG
      if (cachep->flags & SLAB_RED_ZONE) {
        objp -= BYTES_PER_WORD;
      } 
      if ((cachep->flags & SLAB_POISON)  &&
        kmem_check_poison_obj(cachep, objp))
        BUG();
#endif
    }
  }
  // 释放slab中使用的页面。
  kmem_freepages(cachep, slabp->s_mem-slabp->colouroff);
  // 如果slab描述符不在slab中,则这里使用它所用到的内存。
  if (OFF_SLAB(cachep))
    kmem_cache_free(cachep->slabp_cache, slabp);
}
① ⇒ kmem_freepages

    kmem_freepages 函数

② ⇒ kmem_cache_free

    kmem_cache_free 函数

深入理解Linux虚拟内存管理(六)(中):https://developer.aliyun.com/article/1597819

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