深度解析 slab 内存池回收内存以及销毁全流程

在上篇文章 《深入理解 slab cache 内存分配全链路实现》 中,笔者详细地为大家介绍了 slab cache 进行内存分配的整个链路实现,本文我们就来到了 slab cache 最后的一部分内容了,当申请的内存使用完毕之后,下面就该释放内存了。

image

在接下来的内容中,笔者为大家介绍一下内核是如何将内存块释放回 slab cache 的。我们还是先从 slab cache 释放内存的内核 API 开始聊起~~~

image

内核提供了 kmem_cache_free 函数,用于将对象释放回其所属的 slab cache 中,参数 x 表示我们要释放的内存块(对象)的虚拟内存地址,参数 s 指向内存块所属的 slab cache。

void kmem_cache_free(struct kmem_cache *s, void *x)
{
    // 确保指定的是 slab cache : s 为对象真正所属的 slab cache
    s = cache_from_obj(s, x);
    if (!s)
        return;
    // 将对象释放会 slab cache 中
    slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);
}

1. 内存释放之前的校验工作

在开始释放内存块 x 之前,内核需要首先通过 cache_from_obj 函数确认内存块 x 是否真正属于我们指定的 slab cache。不能将内存块释放到其他的 slab cache 中。

随后在 virt_to_head_page 函数中通过内存块的虚拟内存地址 x 找到其所在的物理内存页 page。然后调用 slab_free 将内存块释放回 slab cache 中。

通过虚拟内存地址寻找物理内存页 page 的过程涉及到的背景知识比较复杂,这个笔者后面会单独拎出来介绍,这里大家只需要简单了解 virt_to_head_page 函数的作用即可。

static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x)
{
    struct kmem_cache *cachep;
    // 通过对象的虚拟内存地址 x 找到对象所属的 slab cache
    cachep = virt_to_cache(x);
    // 校验指定的 slab cache : s 是否是对象真正所属的 slab cache : cachep
    WARN_ONCE(cachep && !slab_equal_or_root(cachep, s),
          "%s: Wrong slab cache. %s but object is from %s\n",
          __func__, s->name, cachep->name);
    return cachep;
}

virt_to_cache 函数首先会通过释放对象的虚拟内存地址找到其所在的物理内存页 page,然后通过 struct page 结构中的 slab_cache 指针找到 page 所属的 slab cache。

image

static inline struct kmem_cache *virt_to_cache(const void *obj)
{
    struct page *page;
    // 根据对象的虚拟内存地址 *obj 找到其所在的内存页 page
    // 如果 slub 背后是多个内存页(复合页),则返回复合页的首页 head page
    page = virt_to_head_page(obj);
    if (WARN_ONCE(!PageSlab(page), "%s: Object is not a Slab page!\n",
                    __func__))
        return NULL;
    // 通过 page 结构中的 slab_cache 属性找到其所属的 slub
    return page->slab_cache;
}

2. slab cache 在快速路径下回收内存

static __always_inline void slab_free(struct kmem_cache *s, struct page *page,
                      void *head, void *tail, int cnt,
                      unsigned long addr)
{
    if (slab_free_freelist_hook(s, &head, &tail))
        do_slab_free(s, page, head, tail, cnt, addr);
}

slab cache 回收内存相关的逻辑封装在 do_slab_free 函数中:

static __always_inline void do_slab_free(struct kmem_cache *s,
                struct page *page, void *head, void *tail,
                int cnt, unsigned long addr)
  • 参数 kmem_cache *s 表示释放对象所在的 slab cache,指定我们要将对象释放到哪里。
  • 参数 page 表示释放对象所在的 slab,slab 在内核中使用 struct page 结构来表示。
  • 参数 head 指向释放对象的虚拟内存地址(起始内存地址)。
  • 该函数支持向 slab cache 批量的释放多个对象,参数 tail 指向批量释放对象中最后一个对象的虚拟内存地址。
  • 参数 cnt 表示释放对象的个数,也是用于批量释放对象
  • 参数 addr 用于 slab 调试,这里我们不需要关心。

slab cache 针对内存的回收流程其实和我们在上篇文章 《深入理解 slab cache 内存分配全链路实现》 中介绍的 slab cache 内存分配流程是相似的。

内存回收总体也是分为快速路径 fastpath 和慢速路径 slow path,在 do_slab_free 函数中内核会首先尝试 fastpath 的回收流程。

如果释放对象所在的 slab 刚好是 slab cache 在本地 cpu 缓存 kmem_cache_cpu->page 缓存的 slab,那么内核就会直接将对象释放回缓存 slab 中。

image

static __always_inline void do_slab_free(struct kmem_cache *s,
                struct page *page, void *head, void *tail,
                int cnt, unsigned long addr)
{
    void *tail_obj = tail ? : head;
    struct kmem_cache_cpu *c;
    // slub 中对象分配与释放流程的全局事务 id
    // 既可以用来标识同一个分配或者释放的事务流程,也可以用来标识区分所属 cpu 本地缓存
    unsigned long tid;
redo:
    // 接下来我们需要获取 slab cache 的 cpu 本地缓存
    // 这里的 do..while 循环是要保证获取到的 cpu 本地缓存 c 是属于执行进程的当前 cpu
    // 因为进程可能由于抢占或者中断的原因被调度到其他 cpu 上执行,所需需要确保两者的 tid 是否一致
    do {
        // 获取执行当前进程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了
        // 需要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

    // 如果释放对象所属的 slub (page 表示)正好是 cpu 本地缓存的 slub
    // 那么直接将对象释放到 cpu 缓存的 slub 中即可,这里就是快速释放路径 fastpath
    if (likely(page == c->page)) {
        // 将对象释放至 cpu 本地缓存 freelist 中的头结点处
        // 释放对象中的 freepointer 指向原来的 c->freelist
        set_freepointer(s, tail_obj, c->freelist);
        // cas 更新 cpu 本地缓存 s->cpu_slab 中的 freelist,以及 tid
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                c->freelist, tid,
                head, next_tid(tid)))) {

            note_cmpxchg_failure("slab_free", s, tid);
            goto redo;
        }
        stat(s, FREE_FASTPATH);
    } else
        // 如果当前释放对象并不在 cpu 本地缓存中,那么就进入慢速释放路径 slowpath
        __slab_free(s, page, head, tail_obj, cnt, addr);

}

既然是快速路径释放,那么在 do_slab_free 函数的开始首先就获取 slab cache 的本地 cpu 缓存结构 kmem_cache_cpu,为了保证我们获取到的 cpu 本地缓存结构与运行当前进程所在的 cpu 是相符的,所以这里还是需要在 do .... while 循环内判断两者的 tid。这一点,笔者已经在本文之前的内容里多次强调过了,这里不在赘述。

内核在确保已经获取了正确的 kmem_cache_cpu 结构之后,就会马上判断该释放对象所在的 slab 是否正是 slab cache 本地 cpu 缓存了的 slab —— page == c->page

如果是的话,直接将对象释放回缓存 slab 中,调整 kmem_cache_cpu->freelist 指向刚刚释放的对象,调整释放对象的 freepointer 指针指向原来的 kmem_cache_cpu->freelist 。

image

如果当前释放对象并不在 slab cache 的本地 cpu 缓存中,那么就会进入慢速路径 slowpath 释放内存。

3. slab cache 在慢速路径下回收内存

slab cache 在慢速路径下回收内存的逻辑比较复杂,因为这里涉及到很多的场景,需要改变释放对象所属 slab 在 slab cache 架构中的位置。

下面笔者会带大家一一梳理这些场景,我们一起来看一下内核在这些不同场景中到底是如何处理的?

在开始阅读本小节的内容之前,建议大家先回顾下 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 一文中的 ”8. slab 内存释放原理“ 小节。

在将对象释放回对应的 slab 中之前,内核需要首先清理一下对象所占的内存,重新填充对象的内存布局恢复到初始未使用状态。因为对象所占的内存此时包含了很多已经被使用过的无用信息。这项工作内核在 free_debug_processing 函数中完成。

image

在将对象所在内存恢复到初始状态之后,内核首先会将对象直接释放回其所属的 slab 中,并调整 slab 结构 page 的相关属性。

接下来就到复杂的处理部分了,内核会在这里处理多种场景,并改变 slab 在 slab cache 架构中的位置。

  1. 如果 slab 本来就在 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

  2. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 full slab 变为了一个 partial slab,为了利用局部性的优势,内核需要将该 slab 插入到 kmem_cache_cpu->partial 链表中。

image

  1. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 由于对象的释放刚好由一个 partial slab 变为了一个 empty slab,说明该 slab 并不是很活跃,内核会将该 slab 放入对应 NUMA 节点缓存 kmem_cache_node->partial 链表中,刀枪入库,马放南山。

image

  1. 如果不符合第 2, 3 种场景,但是 slab 本来就在对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中,那么对象在释放之后,slab 的位置不做任何改变。

下面我们就到内核的源码实现中,来一一验证这四种慢速释放场景。

static void __slab_free(struct kmem_cache *s, struct page *page,
            void *head, void *tail, int cnt,
            unsigned long addr)

{
    // 用于指向对象释放回 slub 之前,slub 的 freelist
    void *prior;
    // 对象所属的 slub 之前是否在本地 cpu 缓存 partial 链表中
    int was_frozen;
    // 后续会对 slub 对应的 page 结构相关属性进行修改
    // 修改后的属性会临时保存在 new 中,后面通过 cas 替换
    struct page new;
    unsigned long counters;
    struct kmem_cache_node *n = NULL;
    stat(s, FREE_SLOWPATH);

    // free_debug_processing 中会调用 init_object,清理对象内存无用信息,重新恢复对象内存布局到初始状态
    if (kmem_cache_debug(s) &&
     !free_debug_processing(s, page, head, tail, cnt, addr))
        return;

    do {
        // 获取 slub 中的空闲对象列表,prior = null 表示此时 slub 是一个 full slub,意思就是该 slub 中的对象已经全部被分配出去了
        prior = page->freelist;
        counters = page->counters;
        // 将释放的对象插入到 freelist 的头部,将对象释放回 slub
        // 将 tail 对象的 freepointer 设置为 prior
        set_freepointer(s, tail, prior);
        // 将原有 slab 的相应属性赋值给 new page
        new.counters = counters;
        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 链表中
        was_frozen = new.frozen;
        // inuse 表示 slub 已经分配出去的对象个数,这里是释放 cnt 个对象,所以 inuse 要减去 cnt
        new.inuse -= cnt;
        // !new.inuse 表示此时 slub 变为了一个 empty slub,意思就是该 slub 中的对象还没有分配出去,全部在 slub 中
        // !prior 表示由于本次对象的释放,slub 刚刚从一个 full slub 变成了一个 partial slub (意思就是该 slub 中的对象部分分配出去了,部分没有分配出去)
        // !was_frozen 表示该 slub 不在 cpu 本地缓存中
        if ((!new.inuse || !prior) && !was_frozen) {
            // 注意:进入该分支的 slub 之前都不在 cpu 本地缓存中
            // 如果配置了 CONFIG_SLUB_CPU_PARTIAL 选项,那么表示 cpu 本地缓存 kmem_cache_cpu 结构中包含 partial 列表,用于 cpu 缓存部分分配的 slub
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                // 如果 kmem_cache_cpu 包含 partial 列表并且该 slub 刚刚由 full slub 变为 partial slub
                // 冻结该 slub,后续会将该 slub 插入到 kmem_cache_cpu 的 partial 列表中
                new.frozen = 1;

            } else { 
                // 如果 kmem_cache_cpu 中没有配置 partial 列表,那么直接释放至 kmem_cache_node 中
                // 或者该 slub 由一个 partial slub 变为了 empty slub,调整 slub 的位置到 kmem_cache_node->partial 链表中
                n = get_node(s, page_to_nid(page));
                // 后续会操作 kmem_cache_node 中的 partial 列表,所以这里需要获取 list_lock
                spin_lock_irqsave(&n->list_lock, flags);

            }
        }
        // cas 更新 slub 中的 freelist 以及 counters
    } while (!cmpxchg_double_slab(s, page,
        prior, counters,
        head, new.counters,
        "__slab_free"));

    // 该分支要处理的场景是:
    // 1: 该 slub 原来不在 cpu 本地缓存的 partial 列表中(!was_frozen),但是该 slub 刚刚从 full slub 变为了 partial slub,需要放入 cpu-> partial 列表中
    // 2: 该 slub 原来就在 cpu 本地缓存的 partial 列表中,直接将对象释放回 slub 即可
    if (likely(!n)) {
        // 处理场景 1
        if (new.frozen && !was_frozen) {
            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中
            put_cpu_partial(s, page, 1);
            stat(s, CPU_PARTIAL_FREE);
        }
        
        // 处理场景2,因为之前已经通过 set_freepointer 将对象释放回 slub 了,这里只需要记录 slub 状态即可
        if (was_frozen)
            stat(s, FREE_FROZEN);
        return;
    }
    
    // 后续的逻辑就是处理需要将 slub 放入 kmem_cache_node 中的 partial 列表的情形
    // 在将 slub 放入 node 缓存之前,需要判断 node 缓存的 nr_partial 是否超过了指定阈值 min_partial(位于 kmem_cache 结构)
    // nr_partial 表示 kmem_cache_node 中 partial 列表中缓存的 slub 个数
    // min_partial 表示 slab cache 规定 kmem_cache_node 中 partial 列表可以容纳的 slub 最大个数
    // 如果 nr_partial 超过了最大阈值 min_partial,则不能放入 kmem_cache_node 里
    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial
        // 跳转到 slab_empty 分支,将 slub 释放回伙伴系统中
        goto slab_empty;

    // 如果 cpu 本地缓存中没有配置 partial 列表并且 slub 刚刚从 full slub 变为 partial slub
    // 则将 slub 插入到 kmem_cache_node 中
    if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
        remove_full(s, n, page);
        add_partial(n, page, DEACTIVATE_TO_TAIL);
        stat(s, FREE_ADD_PARTIAL);
    }
    spin_unlock_irqrestore(&n->list_lock, flags);
    // 剩下的情况均属于 slub 原来就在 kmem_cache_node 中的 partial 列表中
    // 直接将对象释放回 slub 即可,无需改变 slub 的位置,直接返回
    return;

slab_empty:
    // 该分支处理的场景是: slub 太多了,将 empty slub 释放会伙伴系统
    // 首先将 slub 从对应的管理链表上删除
    if (prior) {
        /*
         * Slab on the partial list.
         */
        remove_partial(n, page);
        stat(s, FREE_REMOVE_PARTIAL);
    } else {
        /* Slab must be on the full list */
        remove_full(s, n, page);
    }
    spin_unlock_irqrestore(&n->list_lock, flags);
    stat(s, FREE_SLAB);
    // 释放 slub 回伙伴系统,底层调用 __free_pages 将 slub 所管理的所有 page 释放回伙伴系统
    discard_slab(s, page);
}

3.1 直接释放对象回 slab,调整 slab 相关属性

static void __slab_free(struct kmem_cache *s, struct page *page,
            void *head, void *tail, int cnt,
            unsigned long addr)

{
    // 后续会对 slub 对应的 page 结构相关属性进行修改
    // 修改后的属性会临时保存在 new 中,后面通过 cas 替换
    struct page new;

              ....... 省略 ..........

    do {
        prior = page->freelist;
        counters = page->counters;
        // 将对象直接释放回 slab 中,调整 slab 的 freelist 指针,以及对象的 freepointer 指针
        set_freepointer(s, tail, prior);

        new.counters = counters;
        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 中
        was_frozen = new.frozen;
        // inuse 表示 slub 已经分配出去的对象个数,这里是释放 cnt 个对象,所以 inuse 要减去 cnt
        new.inuse -= cnt;

              ....... 省略 ..........

        // cas 更新 slub 中的 freelist 
    } while (!cmpxchg_double_slab(s, page,
        prior, counters,
        head, new.counters,
        "__slab_free"));

 .            ...... 省略 ..........
}

这一部分的逻辑比较简单,在 __slab_free 内存释放流程的开始,内核不管三七二十一,首先会将对象直接释放回其所在的 slab 中。

当对象被释放回 slab 中之后,slab 结构中的相应属于就需要做出相应的调整,比如:

  • 调整 page 结构中的 freelist,它需要指向刚刚被释放的对象。
  • 调整 page 结构中的 inuse,inuse 表示 slab 中已经被分配出去的对象个数,此时对象已经释放回 slab 中,需要调整 inuse 字段。
  • 后续内核会根据不同情况,调整 page 结构的 frozen 属性。

内核会定义一个新的 page 结构 new,将原有 slab 的 page 结构需要更新的上述属性的新值,先一一复制给 new 的对应属性,最后通过 cmpxchg_double_slab 原子更新 slab 对应的属性。

struct page {

        struct {    /*  slub 相关字段 */
             ........ 省略 .........

            // 指向 page 所属的 slab cache
            struct kmem_cache *slab_cache;
            // 指向 slab 中第一个空闲对象
            void *freelist;     /* first free object */
            union {
                unsigned long counters;
                struct {            /* SLUB */             
                    // slab 中已经分配出去的对象
                    unsigned inuse:16;
                    // slab 中包含的对象总数
                    unsigned objects:15;
                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
                    // frozen = 1 表示缓存再本地 cpu 缓存中
                    unsigned frozen:1;
                };
            };
        };

}

按照正常的更新套路来说,我们在更新原有 slab 结构中的 freelist,inuse,frozen 这三个属性之前,首先需要将原有 slab 的这三个旧的属性值一一赋值到临时结构 new page 中,然后在 slab 结构旧值的基础上调整着三个属性的新值,最后通过 cmpxchg_double_slab 将这三个属性的新值原子地更新回 slab 中。

但是我们查看 __slab_free 的代码发现,内核并不是这样操作的,内核只是将原有 slab 的 counter 属性赋值给 new page,而原有 slab 中的 frozen,inuse 属性并没有赋值过去。

此时 new page 结构中的 frozen,inuse 属性依然是上述 struct page 结构中展示的初始值。

而内核后续的操作就更加奇怪了,直接使用 new.frozen 来判断原有 slab 是否在 slab cache 本地 cpu 的 partial 链表中,直接把 new.inuse 属性当做原有 slab 中已经分配出去对象的个数。

而 new.frozen, new.inuse 是 page 结构初始状态的值,并不是原有 slab 结构中的值,这样做肯定不对啊,难道是内核的一个 bug ?

其实并不是,这是内核非常骚的一个操作,这一点对于 Java 程序员来说很难理解。我们在仔细看一下 struct page 结构,就会发现 counter 属性和 inuse,frozen 属性被定义在一个 union 结构体中。

union 结构体中定义的字段全部共享一片内存,union 结构体的内存占用由其中最大的属性决定。而 struct 结构体中的每个字段都是独占一片内存的。

image

由于 union 结构体中各个字段都是共享一块内存,所以一个字段的改变就会影响其他字段的值,从另一方面来看,通过一个字段就可以将整个 union 结构占用的内存块拿出来。明白这些,我们在回头来看内核的操作。

struct page {
            union {
                unsigned long counters;
                struct {            /* SLUB */             
                    // slab 中已经分配出去的对象
                    unsigned inuse:16;
                    // slab 中包含的对象总数
                    unsigned objects:15;
                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
                    // frozen = 1 表示缓存再本地 cpu 缓存中
                    unsigned frozen:1;
                };
            };
}

page 结构中的 counters 是和 inuse,frozen 共用同一块内存的,内核在 __slab_free 中将原有 slab 的 counters 属性赋值给 new.counters 的一瞬间,counters 所在的内存块也就赋值到 new page 的 union 结构中了。

而 inuse,frozen 属性的值也在这个内存块中,所以原有 slab 中的 inuse,frozen 属性也就跟着一起赋值到 new page 的对应属性中了。这样一来,后续的逻辑处理也就通顺了。

        counters = page->counters;
        new.counters = counters;
        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 中
        was_frozen = new.frozen;
        // inuse 表示 slub 已经分配出去的对象个数,这里是释放 cnt 个对象,所以 inuse 要减去 cnt
        new.inuse -= cnt;

同样的道理,我们再来看内核 cmpxchg_double_slab 中的更新操作:

内核明明在 do .... while 循环中更新了 freelist,inuse,frozen 这三个属性,而 counters 属性只是读取并没有更新操作,那么为什么在 cmpxchg_double_slab 只是更新 page 结构的 freelist 和 counters 呢?inuse,frozen 这两个属性又在哪里更新的呢?

   do {
             ....... 省略 ..........
        // cas 更新 slub 中的 freelist 
    } while (!cmpxchg_double_slab(s, page,
        prior, counters,
        head, new.counters,
        "__slab_free"));

我想大家现在一定能够解释这个问题了,由于 counters,inuse,frozen 共用一块内存,当 inuse,frozen 的值发生变化之后,虽然 counters 的值没有发生变化,但是我们可以通过更新 counters 来将原有 slab 中的这块内存一起更新掉,这样 inuse,frozen 的值也跟着被更新了。

由于 page 的 freelist 指针在 union 结构体之外,所以需要在cmpxchg_double_slab 中单独更新。

笔者曾经为了想给大家解释清楚 page->counters 这个属性的作用,而翻遍了 slab 的所有源码,发现内核源码中对于 page->counters 的使用都是只做简单的读取,并不做改变,然后直接在更新,这个问题也困扰了笔者很久。

直到为大家写这篇文章的时候,才顿悟。原来 page->counters 的作用只是为了指向 inuse,frozen 所在的内存,方便在 cmpxchg_double_slab 中同时原子地更新这两个属性。

接下来的内容就到了 slab cache 回收内存最为复杂的环节了,大家需要多一些耐心,继续跟着笔者的思路走下去,我们一起来看下内核如何处理三种内存慢速释放的场景。

3.2 释放对象所属 slab 本来就在 cpu 缓存 partial 链表中

image

was_frozen 指向释放对象所属 slab 结构中的 frozen 属性,用来表示 slab 是否在 slab cache 的本地 cpu 缓存 partial 链表中。

 was_frozen = new.frozen;

如果 was_frozen == true 表示释放对象所属 slab 本来就在 kmem_cache_cpu->partial 链表中,内核将对象直接释放回 slab 中,slab 的原有位置不做改变。

image

下面我们看下 was_frozen == fasle 也就是 slab 不在 kmem_cache_cpu->partial 链表中 的时候,内核又是如何处理的 ?

3.3 释放对象所属 slab 从 full slab 变为了 partial slab

image

如果释放对象所属 slab 原来是一个 full slab,恰恰说明该 slab 拥有比较好的局部性,进程经常从该 slab 中分配对象,slab 十分活跃,才导致它变为了一个 full slab

 prior = page->freelist = null

随着对象的释放,该 slab 从一个 full slab 变为了 partial slab,内核为了更好的利用该 slab 的局部性,所以需要将该 slab 插入到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->partial 链表中。

        if (kmem_cache_has_cpu_partial(s) && !prior) {
                new.frozen = 1;

        } 

        if (new.frozen && !was_frozen) {
            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中
            put_cpu_partial(s, page, 1);
            stat(s, CPU_PARTIAL_FREE);
        }
        

image

将 slab 插入到 kmem_cache_cpu->partial 链表的逻辑封装在 put_cpu_partial 中,put_cpu_partial 函数最重要的一个考量逻辑是需要确保 kmem_cache_cpu->partial 链表中所有 slab 中包含的空闲对象总数不能超过 kmem_cache->cpu_partial 的限制。

struct kmem_cache {
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中空闲对象的总数
    unsigned int cpu_partial;
};

在释放对象所在的 slab 插入到 kmem_cache_cpu->partial 链表之前,put_cpu_partial 函数需要判断当前 kmem_cache_cpu->partial 链表中包含的空闲对象总数 pobjects 是否超过了 kmem_cache->cpu_partial 的限制。

image

如果超过了,则需要先将当前 kmem_cache_cpu->partial 链表中所有的 slab 转移到其对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中。转移完成之后,在将释放对象所属的 slab 插入到 kmem_cache_cpu->partial 链表中。

image

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
// 只有配置了 CONFIG_SLUB_CPU_PARTIAL 选项,kmem_cache_cpu 中才有会 partial 列表
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 指向原有 kmem_cache_cpu 中的 partial 列表
    struct page *oldpage;
    // slub 所在管理列表中的 slub 个数,这里的列表是指 partial 列表
    int pages;
    // slub 所在管理列表中的包含的空闲对象总数,这里的列表是指 partial 列表
    // 内核会将列表总体的信息存放在列表首页 page 的相关字段中
    int pobjects;
    // 禁止抢占
    preempt_disable();
    do {
        pages = 0;
        pobjects = 0;
        // 获取 slab cache 中原有的 cpu 本地缓存 partial 列表首页
        oldpage = this_cpu_read(s->cpu_slab->partial);
        // 如果 partial 列表不为空,则需要判断 partial 列表中所有 slub 包含的空闲对象总数是否超过了 s->cpu_partial 规定的阈值
        // 超过 s->cpu_partial 则需要将 kmem_cache_cpu->partial 列表中原有的所有 slub 转移到 kmem_cache_node-> partial 列表中
        // 转移之后,再把当前 slub 插入到 kmem_cache_cpu->partial 列表中
        // 如果没有超过 s->cpu_partial ,则无需转移直接插入
        if (oldpage) {
            // 从 partial 列表首页中获取列表中包含的空闲对象总数
            pobjects = oldpage->pobjects;
            // 从 partial 列表首页中获取列表中包含的 slub 总数
            pages = oldpage->pages;

            if (drain && pobjects > s->cpu_partial) {
                unsigned long flags;
                // 关闭中断,防止并发访问
                local_irq_save(flags);
                // partial 列表中所包含的空闲对象总数 pobjects 超过了 s->cpu_partial 规定的阈值
                // 则需要将现有 partial 列表中的所有 slub 转移到相应的 kmem_cache_node->partial 列表中
                unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
                // 恢复中断
                local_irq_restore(flags);
                // 重置 partial 列表
                oldpage = NULL;
                pobjects = 0;
                pages = 0;
                stat(s, CPU_PARTIAL_DRAIN);
            }
        }
        // 无论 kmem_cache_cpu-> partial 列表中的 slub 是否需要转移
        // 释放对象所在的 slub 都需要填加到  kmem_cache_cpu-> partial 列表中
        pages++;
        pobjects += page->objects - page->inuse;

        page->pages = pages;
        page->pobjects = pobjects;
        page->next = oldpage;
        // 通过 cas 将 slub 插入到 partial 列表的头部
    } while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)
                                != oldpage);

    // s->cpu_partial = 0 表示 kmem_cache_cpu->partial 列表不能存放 slub
    // 将释放对象所在的 slub 转移到  kmem_cache_node-> partial 列表中
    if (unlikely(!s->cpu_partial)) {
        unsigned long flags;
        local_irq_save(flags);
        unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
        local_irq_restore(flags);
    }
    preempt_enable();
#endif  /* CONFIG_SLUB_CPU_PARTIAL */
}

那么我们如何知道 kmem_cache_cpu->partial 链表所包含的空闲对象总数到底是多少呢?

这就用到了 struct page 结构中的两个重要属性:

struct page {
      // slab 所在链表中的包含的 slab 总数
      int pages;  
      // slab 所在链表中包含的对象总数
      int pobjects; 
}

我们都知道 slab 在内核中的数据结构用 struct page 中的相关结构体表示,slab 在 slab cache 架构中一般是由 kmem_cache_cpu->partial 链表和 kmem_cache_node->partial 链表来组织管理。

那么我们如何知道 partial 链表中包含多少个 slab ?包含多少个空闲对象呢?

答案是内核会将 parital 链表中的这些总体统计信息存储在链表首个 slab 结构中。也就是说存储在首个 page 结构中的 pages 属性和 pobjects 属性中。

在 put_cpu_partial 函数的开始,内核直接获取 parital 链表的首个 slab —— oldpage,并通过 oldpage->pobjectss->cpu_partial 比较,来判断当前 kmem_cache_cpu->partial 链表中包含的空闲对象总数是否超过了 kmem_cache 结构中规定的 cpu_partial 阈值。

如果超过了,则通过 unfreeze_partials 转移 kmem_cache_cpu->partial 链表中的所有 slab 到对应的 kmem_cache_node->partial 链表中。

既然 kmem_cache_cpu->partial 链表有容量的限制,那么同样 kmem_cache_node->partial 链表中的容量也会有限制。

kmem_cache_node->partial 链表中所包含 slab 个数的上限由 kmem_cache 结构中的 min_partial 属性决定。

struct kmem_cache {

    // slab cache 在 numa node 中缓存的 slab 个数上限,slab 个数超过该值,空闲的 empty slab 则会被回收至伙伴系统
    unsigned long min_partial;
}

如果当前要转移的 slab 是一个 empty slab,并且此时 kmem_cache_node->partial 链表所包含的 slab 个数 kmem_cache_node->nr_partial 已经超过了 kmem_cache-> min_partial 的限制,那么内核就会直接将这个 empty slab 释放回伙伴系统中。

// 将 kmem_cache_cpu->partial 列表中包含的 slub unfreeze
// 并转移到对应的 kmem_cache_node->partial 列表中
static void unfreeze_partials(struct kmem_cache *s,
        struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
    struct kmem_cache_node *n = NULL, *n2 = NULL;
    struct page *page, *discard_page = NULL;
    // 挨个遍历 kmem_cache_cpu->partial 列表,将列表中的 slub 转移到对应 kmem_cache_node->partial 列表中
    while ((page = c->partial)) {
        struct page new;
        struct page old;
        // 将当前遍历到的 slub 从 kmem_cache_cpu->partial 列表摘下
        c->partial = page->next;
        // 获取当前 slub 所在的 numa 节点对应的 kmem_cache_node 缓存
        n2 = get_node(s, page_to_nid(page));
        // 如果和上一个转移的 slub 所在的 numa 节点不一样
        // 则需要释放上一个 numa 节点的 list_lock,并对当前 numa 节点的 list_lock 加锁
        if (n != n2) {
            if (n)
                spin_unlock(&n->list_lock);

            n = n2;
            spin_lock(&n->list_lock);
        }

        do {

            old.freelist = page->freelist;
            old.counters = page->counters;
            VM_BUG_ON(!old.frozen);

            new.counters = old.counters;
            new.freelist = old.freelist;
            // unfrozen 当前 slub,因为即将被转移到对应的 kmem_cache_node->partial 列表
            new.frozen = 0;
            // cas 更新当前 slub 的 freelist,frozen 属性
        } while (!__cmpxchg_double_slab(s, page,
                old.freelist, old.counters,
                new.freelist, new.counters,
                "unfreezing slab"));
        // 因为 kmem_cache_node->partial 列表中所包含的 slub 个数是受 s->min_partial 阈值限制的
        // 所以这里还需要检查 nr_partial 是否超过了 min_partial
        // 如果当前被转移的 slub 是一个 empty slub 并且 nr_partial 超过了 min_partial 的限制,则需要将 slub 释放回伙伴系统中
        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
            // discard_page 用于将需要释放回伙伴系统的 slub 串联起来
            // 后续统一将 discard_page 链表中的 slub 释放回伙伴系统
            page->next = discard_page;
            discard_page = page;
        } else {
            // 其他情况,只要 slub 不为 empty ,不管 nr_partial 是否超过了 min_partial
            // 都需要将 slub 转移到对应 kmem_cache_node->partial 列表的末尾
            add_partial(n, page, DEACTIVATE_TO_TAIL);
            stat(s, FREE_ADD_PARTIAL);
        }
    }

    if (n)
        spin_unlock(&n->list_lock);
    // 将 discard_page 链表中的 slub 统一释放回伙伴系统
    while (discard_page) {
        page = discard_page;
        discard_page = discard_page->next;

        stat(s, DEACTIVATE_EMPTY);
        // 底层调用 __free_pages 将 slub 所管理的所有 page 释放回伙伴系统
        discard_slab(s, page);
        stat(s, FREE_SLAB);
    }
#endif  /* CONFIG_SLUB_CPU_PARTIAL */
}

3.4 释放对象所属 slab 从 partial slab 变为了 empty slab

image

如果释放对象所在的 slab 原来是一个 partial slab ,由于对象的释放刚好变成了一个 empty slab,恰恰说明该 slab 并不是一个活跃的 slab,它的局部性不好,内核已经好久没有从该 slab 中分配对象了,所以内核选择刀枪入库,马放南山。将它释放回 kmem_cache_node->partial 链表中作为本地 cpu 缓存的后备选项。

在将这个 empty slab 插入到 kmem_cache_node->partial 链表之前,同样需要检查当前 partial 链表中的容量 kmem_cache_node->nr_partial 不能超过 kmem_cache-> min_partial 的限制。如果超过限制了,直接将这个 empty slab 释放回伙伴系统中。

image

        if ((!new.inuse || !prior) && !was_frozen) {
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                new.frozen = 1;
            } else { 
                // !new.inuse 表示当前 slab 刚刚从一个 partial slab 变为了 empty slab
                n = get_node(s, page_to_nid(page));
                spin_lock_irqsave(&n->list_lock, flags);

            }
        }

      if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial
        // 跳转到 slab_empty 分支,将 slub 释放回伙伴系统中
        goto slab_empty;

释放对象所属的 slab 本来就在 kmem_cache_node->partial 链表中,这种情况下就是直接释放对象回 slab 中,无需改变 slab 的位置。

image

4. slab cache 的销毁

终于到了本文最后一个小节了, slab cache 最为复杂的内容我们已经踏过去了,本小节的内容将会非常的轻松愉悦,这一次笔者来为大家介绍一下 slab cache 的销毁过程。

slab cache 的销毁过程刚刚好和 slab cache 的创建过程相反,笔者在 《从内核源码看 slab 内存池的创建初始化流程》的内容中,通过一步一步的源码演示,最终勾勒出 slab cache 的完整架构:

image

slab cache 销毁的核心步骤如下:

  1. 首先需要释放 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包括被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们统统归还到伙伴系统中。

  2. 释放 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,统统释放回伙伴系统中。

  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相关信息。

  4. 从 slab cache 的全局列表中删除该 slab cache。

  5. 释放 kmem_cache_cpu 结构,kmem_cache_node 结构,kmem_cache 结构。释放对象的过程就是 《1. slab cache 如何回收内存》小节中介绍的内容。

下面我们一起到内核源码中看一下具体的销毁过程:

image

void kmem_cache_destroy(struct kmem_cache *s)
{
    int err;

    if (unlikely(!s))
        return;

    // 获取 cpu_hotplug_lock,防止 cpu 热插拔改变 online cpu map
    get_online_cpus();
    // 获取 mem_hotplug_lock,防止访问内存的时候进行内存热插拔
    get_online_mems();
    // 获取 slab cache 链表的全局互斥锁
    mutex_lock(&slab_mutex);
    // 将 slab cache 的引用技术减 1
    s->refcount--;
    // 判断 slab cache 是否还存在其他地方的引用
    if (s->refcount)
        // 如果该 slab cache 还存在引用,则不能销毁,跳转到 out_unlock 分支
        goto out_unlock;
    // 销毁 memory cgroup 相关的 cache ,这里不是本文重点
    err = shutdown_memcg_caches(s);
    if (!err)
        // slab cache 销毁的核心函数,销毁逻辑就封装在这里
        err = shutdown_cache(s);

    if (err) {
        pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",
               s->name);
        dump_stack();
    }
out_unlock:
    // 释放相关的自旋锁和信号量
    mutex_unlock(&slab_mutex);

    put_online_mems();
    put_online_cpus();
}

在开始正式销毁 slab cache 之前,首先需要将 slab cache 的引用计数 refcount 减 1。并需要判断 slab cache 是否还存在其他地方的引用。

slab cache 这里在其他地方存在引用的可能性,相关细节笔者在《从内核源码看 slab 内存池的创建初始化流程》 一文中的 ”1. __kmem_cache_alias“ 小节的内容中已经详细介绍过了。

当我们利用 kmem_cache_create 创建 slab cache 的时候,内核会检查当前系统中是否存在一个各项参数和我们要创建 slab cache 参数差不多的一个 slab cache,如果存在,那么内核就不会再继续创建新的 slab cache,而是复用已有的 slab cache。

一个可以被复用的 slab cache 需要满足以下四个条件:

  1. 指定的 slab_flags_t 相同。

  2. 指定对象的 object size 要小于等于已有 slab cache 中的对象 size (kmem_cache->size)。

  3. 如果指定对象的 object size 与已有 kmem_cache->size 不相同,那么它们之间的差值需要再一个 word size 之内。

  4. 已有 slab cache 中的 slab 对象对齐 align (kmem_cache->align)要大于等于指定的 align 并且可以整除 align 。 。

随后会在 sys 文件系统中为复用 slab cache 起一个别名 alias 并创建一个 /sys/kernel/slab/aliasname 目录,但是该目录下的文件需要软链接到原有 slab cache 在 sys 文件系统对应目录下的文件。这里的 aliasname 就是我们通过 kmem_cache_create 指定的 slab cache 名称。

在这种情况,系统中的 slab cache 就可能在多个地方产生引用,所以在销毁的时候需要判断这一点。

如果存在其他地方的引用,则需要停止销毁流程,如果没有其他地方的引用,则调用 shutdown_cache 开始正式的销毁流程。

static int shutdown_cache(struct kmem_cache *s)
{
    // 这里会释放 slab cache 占用的所有资源
    if (__kmem_cache_shutdown(s) != 0)
        return -EBUSY;
    // 从 slab cache 的全局列表中删除该 slab cache
    list_del(&s->list);
    // 释放 sys 文件系统中移除 /sys/kernel/slab/name 节点的相关资源
    sysfs_slab_unlink(s);
    sysfs_slab_release(s);
    // 释放 kmem_cache_cpu 结构
    // 释放 kmem_cache_node 结构
    // 释放 kmem_cache 结构
    slab_kmem_cache_release(s);

    }

    return 0;
}

4.1 释放 slab cache 占用的所有资源

  1. 首先需要释放 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包括被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们统统归还到伙伴系统中。

  2. 释放 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,统统释放回伙伴系统中。

  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相关信息。

/*
 * Release all resources used by a slab cache.
 */
int __kmem_cache_shutdown(struct kmem_cache *s)
{
    int node;
    struct kmem_cache_node *n;
    // 释放 slab cache 本地 cpu 缓存 kmem_cache_cpu 中缓存的 slub 以及 partial 列表中的 slub,统统归还给伙伴系统
    flush_all(s);

    // 释放 slab cache 中 numa 节点缓存 kmem_cache_node 中 partial 列表上的所有 slub
    for_each_kmem_cache_node(s, node, n) {
        free_partial(s, n);
        if (n->nr_partial || slabs_node(s, node))
            return 1;
    }
    // 在 sys 文件系统中移除 /sys/kernel/slab/name 节点相关信息
    sysfs_slab_remove(s);
    return 0;
}

4.2 释放 slab cache 在各个 cpu 中的缓存资源

内核通过 on_each_cpu_cond 挨个遍历所有 cpu,在遍历的过程中通过 has_cpu_slab 判断 slab cache 是否在该 cpu 中还占有缓存资源,如果是则调用 flush_cpu_slab 将缓存资源释放回伙伴系统中。

// 释放 kmem_cache_cpu 中占用的所有内存资源
static void flush_all(struct kmem_cache *s)
{
    // 遍历每个 cpu,通过 has_cpu_slab 函数检查 cpu 上是否还有 slab cache 的相关缓存资源
    // 如果有,则调用 flush_cpu_slab 进行资源的释放
    on_each_cpu_cond(has_cpu_slab, flush_cpu_slab, s, 1, GFP_ATOMIC);
}

static bool has_cpu_slab(int cpu, void *info)
{
    struct kmem_cache *s = info;
    // 获取 cpu 在 slab cache 上的本地缓存
    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);
    // 判断 cpu 本地缓存中是否还有缓存的 slub
    return c->page || slub_percpu_partial(c);
}

static void flush_cpu_slab(void *d)
{
    struct kmem_cache *s = d;
    // 释放 slab cache 在 cpu 上的本地缓存资源
    __flush_cpu_slab(s, smp_processor_id());
}

static inline void __flush_cpu_slab(struct kmem_cache *s, int cpu)
{
    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);

    if (c->page)
        // 释放 cpu 本地缓存的 slub 到伙伴系统
        flush_slab(s, c);
    // 将 cpu 本地缓存中的 partial 列表里的 slub 全部释放回伙伴系统
    unfreeze_partials(s, c);
}

4.3 释放 slab cache 的核心数据结构

这里的释放流程正是笔者在本文 《1. slab cache 如何回收内存》小节中介绍的内容。

void slab_kmem_cache_release(struct kmem_cache *s)
{
    // 释放 slab cache 中的 kmem_cache_cpu 结构以及 kmem_cache_node 结构
    __kmem_cache_release(s);
    // 最后释放 slab cache 的核心数据结构 kmem_cache
    kmem_cache_free(kmem_cache, s);
}

总结

整个 slab cache 系列篇幅非常庞大,涉及到的细节非常丰富,为了方便大家回顾,笔者这里将 slab cache 系列涉及到的重点内容再次梳理总结一下。

  • 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》

  • 《从内核源码看 slab 内存池的创建初始化流程》

  • 《深入理解 slab cache 内存分配全链路实现》

在本文正式进入 slab 相关内容之后,笔者首先为大家详细介绍了 slab 内存池中对象的内存布局情况,如下图所示:

image

在此基础之上,我们继续采用一步一图的方式,一步一步推演出 slab 内存池的整体架构,如下图所示:

image

随后基于此架构,笔者介绍了在不同场景下 slab 内存池分配内存以及回收内存的核心原理。在交代完核心原理之后,我们进一步深入到内核源码实现中来一一验证。

在内核源码章节的开始,笔者首先为大家介绍了 slab 内存池的创建流程,流程图如下:

image

在 slab 内存池创建出来之后,随后笔者又深入介绍了 slab 内存池如何分配内存块的相关源码实现,其中详细介绍了在多种不同场景下,内核如何处理内存块的分配。

image

在我们清除了 slab 内存池如何分配内存块的源码实现之后,紧接着笔者又介绍了 slab 内存池如何进行内存块的回收,回收过程要比分配过程复杂很多,同样也涉及到多种复杂场景的处理:

image

最后笔者介绍了 slab 内存池的销毁过程:

image

好了,整个 slab cache 相关的内容到此就结束了,感谢大家的收看,我们下篇文章见~~~

本文转载于网络 如有侵权请联系删除

相关文章

  • TCP编程

    一、客户端与服务端通信流程二、客户端与服务端通信代码服务端(server.py)<spanclass="hljs-keyword">import</span>socket <spanclass="hljs-comment">#1、创建socket</span> <spanclass="hljs-comment">#参数1:选择使用ip类型,AF_INET代表ipv4,AF_INET6表示ipv6</span> <spanclass="hljs-comment">#参数2:选择协议,SOCK_STREAM表示TCP协议</span> server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) <spanclass="hljs-comment">#2、绑定</span> <spanclass="hljs

  • ST7789 SPI LCD硬件垂直滚动功能的使用

    一、想法萌生小熊派开发板的LCD小屏幕使用是ST7789驱动IC,之前一直有在该块屏幕上实现滚动显示的想法,最初构想在MCU侧创建一个大的队列,将整个屏幕显示内容交由队列管理,然后不停的去整屏刷新以实现屏幕滚动。理论上这样的实现比较耗资源,耗费CPU资源去操作SPI外设,耗费内存资源去存储显示数据,所以一直没有去做这件事。今天在查看ST7789数据手册写论文时,偶然发现ST7789手册中:咦?Scroll?滚动?发现新大陆!根据手册中的8.14章节描述,旋转滚动仅仅是垂直滚动的一种模式,由垂直滚动区域定义指令(0x33H)和垂直滚动起始地址指令(0x77)决定。原来我一直想做的功能,ST7789竟然硬件支持,研究研究如何实现,开干!二、实现过程1.准备工作需要了解SPI驱动ST7789的基本原理,可以参考我的这篇教程:STM32Cube-17|使用硬件SPI驱动TFT-LCD(ST7789)2.设置滚动区域小熊派板载LCD屏幕的大小是240x240,水平和垂直都有240个像素点,但是ST7789驱动IC的显存大小为320x240,垂直有320行像素点,水平有240行像素点,平常我们只是

  • 一文读懂Redis的三种模式(下)

    在上一篇中,我们简单介绍了Redis三种模式中的两种模式,即主从模式、Sentinel模式,并对这两种模式的优缺点进行了总结: 模式主从Sentinel优点1、安全可靠2、主从分离,读写分离1、高可用2、数据量不限缺点1、如果某一个节点挂掉,需要人工介入,尤其是当master节点挂掉之后,整个集群不能写2、不支持大数据量的操作3、不支持扩容1、不支持动态扩容从上述可以看出,虽然Sentinel模式在一定程度上解决了主从模式所存在的问题,但是二者有一个共同的缺点,就是不支持动态扩容,为了解决这个问题,Redis引入了第三种模式,即Cluster模式。Cluster模式的特点: 1、client直连,没有代理2、去中心化,两两交互,每个节点都有其他节点的想详细信息3、内部传输采用自有的gossip协议,减小数据传输量一个稳定的Cluster集群必须有6个节点,即三主三从,之所以是6个,是因为里面涉及到了投票选举。如果此时只有两个节点,即A和B,A说B下线,B说A下线,这样永远都不会有定论,如果有三个节点的话,即AB和C。A和B均发现C不通,那么得到一致结论:C已经下线。 Cluster中的

  • 如何将R语言普通矩阵转换为非负矩阵

    #=============================================================== #=============================================================== setwd('C:\\Users\\czh\\Desktop') library(Matrix) rm(list=ls()) options(stringsAsFactors=F) library(ConsensusClusterPlus) dt<-read.csv("train.csv",header=T, row.names=1, stringsAsFactors=F) colnames(dt) head(dt) #=============================================================== #========================================================

  • CentOS7下部署GitBook

    GitBook是一个基于Node.js的命令行工具,可使用Github/Git和Markdown来制作精美的电子书 GitBook支持输出多种文档格式:1)静态站点:GitBook默认输出该种格式,生成的静态站点可直接托管搭载GithubPages服务上;2)PDF:需要安装gitbook-pdf依赖;3)eBook:需要安装ebook-convert;4)单HTML网页:支持将内容输出为单页的HTML,不过一般用在将电子书格式转换为PDF或eBook的中间过程;5)JSON:一般用于电子书的调试或元数据提取。使用GitBook制作电子书,必备两个文件:README.md和SUMMARY.md下面介绍如何在CentOS7系统下部署GitBook 1)官网下载Node.js的Linux64位的二进制包 下载wgethttps://nodejs.org/dist/v12.16.1/node-v12.16.1-linux-x64.tar.xz2)解压到指定目录tar-xJfnode-v12.16.1-linux-x64.tar.xz-C/usr/local/lib/ cd/usr/local

  • web 使用 HTML5 可以做的五件很棒的事情

    HTML5如今已被应用得非常广泛,特别是在移动端。作为下一代的网页语言,HTML5拥有很多让人期待已久的新特性,它可以说是近十年来Web标准最巨大的飞跃。这篇文章将向大家展示使用HTML5可以做的五件很棒的事情。1.制作时尚的表单表单是Web设计的重要组成部分,常见的有注册表单、联系表单以及反馈表单,表单设计应该在不影响用户体验和可用性的前提下尽量美观,以吸引用户填写内容。下面这些示例将带您体验HTML5的强大。2.构建实用的HTML5框架框架帮助我们更快速、更容易实现功能,让你集中精力于更重要的方面,而不会浪费时间做重复的任务。下面列举了几个值得关注的HTML5框架。3.开发丰富多彩的游戏尽管HTML5标准还在不断完善过程中,一些游戏开发者已经使用HTML5开发出了非常有趣的游戏。下面列举了几个游戏示例,更多精彩内容见文章 推荐21款最佳HTML5网页游戏 和 分享29款基于HTML5Canvas的网页游戏。4.以更直观的方式让数据可视化呈现有的时候,你需要在网站中以更直观的可视化方式呈现大量的数据或者信息,这个时候就需要图表解决方法帮助你实现。下面列举了几个不错的HTML5图表方案

  • 7.1.1简单二维CAD图纸(使用javascript多快好省绘制)

    [题引]:君子性非异也,善假于物也。javascript本身不能绘制CAD图纸,但借助第三方库就可以绘制一些简单的二维图纸了。到被戏称为“全球最大的同性交友网站”(注:程序员之间认识先了解对方的代码,github为全球最大规模的代码托管站点,故被戏称为“全球最大的同性交友网站”)https://github.com/中搜索js-dxf,会有如下结果:点击并下载,然后解压得到一些文件:点击其中的index文件,使用浏览器打开:点击超链接下载demo.dxf文件,保存到本地,使用libreCAD打开,结果如下:综上,可以使用javascript生成CAD图纸。而生成这个图纸的js代码很少:这就为我们用js绘制二维CAD图纸提供了便利。

  • 如何从公网访问内网WebSphere

    公网访问内网WebSphere本地安装了WebSphere,只能在局域网内访问,怎样从公网也能访问本地WebSphere?本文将介绍具体的实现步骤。1.准备工作1.1安装Java1.7及以上版本执行命令java-version检查Java安装和配置是否正确。1.2安装并启动WebSphere默认安装的WebSphere端口是9080。2.实现步骤2.1下载并解压holer软件包Holer软件包:holer-client.zip2.2修改holer配置文件在holer官网上申请专属的holeraccesskey或者使用开源社区上公开的accesskey信息。例如申请得到的holer信息如下,这里以此holer信息为例:--------------------------------------------- HolerClient:holerdemo@gmail.com AccessKey:6688daebe02846t88s166733595eee5d --------------------------------------------- DomainName:holer65004

  • Go 1.11 Beta 3 发布

    原文作者:DmitriShuralyovGolang开发者,你们好!我们刚刚发布了go1.11beta3,这是Go1.11的测试版。它是在标记为go1.11beta3的修订版中从主分支中删除的。请尝试使用新版本进行生产负载测试和单元测试。您测试这些预发布版本的帮助非常宝贵。使用问题跟踪器报告任何问题:https://golang.org/issue/new如果你已经安装了Go,那么最简单的方法是尝试go1.11beta3是使用go命令:$gogetgolang.org/dl/go1.11beta3$go1.11beta3下载您可以从通常的位置下载二进制和源代码分发:https://golang.org/dl/#go1.11beta3要了解Go1.11中的更改,请阅读草稿发行说明:https://tip.golang.org/doc/go1.11Cheers,版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

  • 爬虫

    1爬虫原理 1.1爬虫概念 1.2爬虫流程 2爬虫模块及使用 2.1requests模块   2.11get请求   2.12post请求   2.13编码问题,json解析   2.14使用代理,超时设置,认证设置,异常处理,上传文件 2.2selenium模块   2.21基本使用   2.22获取标签   2.23获取属性,内容   2.24其他操作 2.3beautifulsoup模块   2.31基本使用   2.32获取标签搜索文档树五种方法 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1爬虫原理 1.1爬虫概念 网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂

  • vscode code命令安装方式

    1.打开vscode,shift+cmd+p 2.输入"shellcommand" 3.安装

  • Yii2.0框架 ActiveRecord

    1.ActiveRecord    ActiveRecord(活动记录,简称AR类)提供了一套面向对象的接口,用以访问数据库中的数据。       (1)、一个AR类关联一张数据表,每个AR对象对应表中的一行       (2)、AR对象的属性,对应为数据库的列       (3)、可以直接以面向对象的方式来操纵数据表中的数据,这样就不需要写sql语句就能实现数据库的访问 2.声明ActiveRecord类    通过继承yii\db\ActiveRecord基类来声明一个AR类,并实现tableName方法,返回与之相关联的数据表的名称     例子:以模型名为Post为例。 classPostextends\yii\db\ActiveRecord { publicstaticfunctiontableName() { return'post'; }复制 3.查询数据     AR提供了两种方法来构建DB查询,返回AR对象。       yii\db\ActiveRecord::find()         $model=Post::find()->where([["id

  • CenOS6.3 ssh 公钥认证报错:Permission denied (publickey,gssapi-keyex,gssapi-with-mic)

    转载自 http://laowafang.blog.51cto.com/251518/1364298   1.说明:  ssh无密码用户远程登录,一直以来使用是debian操作系统,对用户目录权限要求没有关注过,生成了密钥,放对位置直接就可以使用,今天测试ansible工具,用到了CentOS6.3发现它对目录权限要求比较严格,本博文仅仅记录一下本次权限修改操作。 2、操作记录 (1)创建用户:ansible (2)修改sshd_config文件 #vi/etc/ssh/sshd_config  //开启以下内容 HostKey/etc/ssh/ssh_host_rsa_key RSAAuthenticationyes PubkeyAuthenticationyes AuthorizedKeysFile   .ssh/authorized_keys #/etc/init.d/sshdrestart (3)权限设置 #mkdir/home/ansible/.ssh #chown-Ransible:ansibl

  • 学习笔记:《Kali Linux 2 网络渗透测试 实践指南 第2版》之漏洞扫描及渗透(Nmap+Metasploit)

    本章重点内容: 1、使用Nmap扫描漏洞 2、使用Metasploit进行渗透   接下来针对本章内容展开详细说明 扫描漏洞需要一个目标主机,在此我们使用Metasploitable这个测试环境 至于这个测试环境是什么以及如何安装,可以参考以下这篇文章: 手把手教你安装Metasploitable虚拟机 1、使用Nmap扫描漏洞 首先,我们获取目标的IP地址,在Metasploitable环境中,通过ifconfig来查看IP地址,如下:    从中看到IP地址是192.160.220.132,然后在kali的系统下面,通过Nmap来扫描目标端口,如下:    通过扫描发现有众多的开发端口,这些开发的端口就是练习渗透测试的目标 之前文章中有说明Nmap扫描主机,其实Nmap也可以扫描目标是否存在漏洞 检查目标系统是否有漏洞,需要通过特定的脚本来实现,而Nmap集成了很多现有的脚本,可以检查目标是否存在漏洞 通过Nmap扫描特定漏洞的格式是:Nmap【--script】+脚本名称+目标IP,以脚本”ftp-vsftpd-backdoo

  • 设计模式之 单例模式

    单例模式是保证系统实例唯一性的重要手段。单例模式首先通过将类的实例化方法私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例。 单例模式的设计保证了一个类在整个系统中同一时刻只有一个实例存在,主要被用于一个全局类的对象在多个地方使用并且对象的状态是全局变化的场景下。 需要注意的是,单例模式的类构造函数是私有的,只能由自身创建和销毁对象。   1.懒汉模式 publicclassLazySingleton{ privatestaticLazySingletoninstance; privateLazySingleton(){} publicstaticsynchronizedLazySingletongetInstance(){ if(instance==null){ instance=newLazySingleton(); } returninstance; } }复制 之所以instance定位为静态的,是因为静态属性或方法是属于类的,能够很好的保障单例对象的唯一性。   2.饿汉模式 publiccla

  • openui5(SAPUI5)组件-项目搭建

    openui5这个组件库对于很多前端来说都是比较陌生的,在之前我甚至都没有听说过这个东西。现在公司项目以这组件库为主,那就必须要加以熟悉。但是在百度上搜到的资料比较少,所以把自己对于这个组件的了解和使用记录一下。 openui5官网:https://openui5.org/,目前文档没有中文版的,因此都是英语。 在官网中点击Documentation,在下面有个Demos,可以看到有个DemoApps,这里面有一些官网提供的demo和组件的效果演示,大家可以自己下载然后看下具体的项目目录结构,从目录结构可以看到,demo很明显是mvc模式。现在我们来从零搭建一个openui5的项目。在官网上,点击Releasea,在最下面有个AllReleases,这里面都是关于openui5组件的一些sdk,这里我下载的是runtime,并没有下载sdk,因为sdk里面不仅包含了openui5的所有组件,还有一些组件的文档说明,体积比较大。所以我选择了只有组件的runtime。下载解压后有个resources文件夹,这个文件夹下面就是oepnui5提供的所有组件了。 之前我们用的vue脚手架或者re

  • 洛谷P4475 巧克力王国

    洛谷P4475巧克力王国 题目描述 巧克力王国里的巧克力都是由牛奶和可可做成的。 但是并不是每一块巧克力都受王国人民的欢迎,因为大家都不喜欢过于甜的巧克力。 对于每一块巧克力,我们设x和y为其牛奶和可可的含量。 由于每个人对于甜的程度都有自己的评判标准,所以每个人都有两个参数a和b,分别为他自己为牛奶和可可定义的权重,因此牛奶和可可含量分别为x和y的巧克力对于他的甜味程度即为ax+by。 而每个人又有一个甜味限度c,所有甜味程度大于等于c的巧克力他都无法接受。 每块巧克力都有一个美味值h。 现在我们想知道对于每个人,他所能接受的巧克力的美味值之和为多少。 输入输出格式 输入格式:   第一行两个正整数n和m,分别表示巧克力个数和询问个数。接下来n行,每行三个整数x,y,h,含义如题目所示。再接下来m行,每行三个整数a,b,c,含义如题目所示。 输出格式:   输出m行,其中第i行表示第i个人所能接受的巧克力的美味值之和。 输入输出样例 输入样例:  33 125 314 221 216 135 137复制 输出样例:  5 0 4复制 说

  • xmpp关于后台挂起的消息接收,后台消息推送,本地发送通知

    想问下,在xmpp即时通讯的项目中,我程序如果挂起了,后台有消息过来,我这边的推送不过来,所以我的通知就会收不到消息,当我重新唤醒应用的时候,他才会接收到通知,消息就会推送过来,我在plist哪里设置了他的电话服务,因为那是长连接不会睡眠,可是我上架AppStore的时候就会被拒绝了,说我没实现那个服务。就是Requiredbackgroundmodes这个服务,但是只要我使用这个服务,他就必须要实现打电话功能,我项目中没有这个功能,不知道怎么解决后台挂起不让他进入睡眠状态,等接收到后台消息后,他会立马发送通知过来,求解答。。谢谢。。

  • LeetCode206反转链表、24两两交换节点

    206.反转链表 反转一个单链表。 示例: 输入:1->2->3->4->5->NULL 输出:5->4->3->2->1->NULL 进阶: 你可以迭代或递归地反转链表。你能否用两种方法解决这道题? solution1:循环迭代 classSolution{ publicListNodereverseList(ListNodehead){ //循环迭代,三指针 ListNodeprev=null; ListNodecurr=head; while(curr!=null){ ListNodenext=curr.next; curr.next=prev; prev=curr; curr=next; } returnprev; } } 复制 classSolution{ publicListNodereverseList(ListNodehead){ //循环迭代,三指针,head代替一指针 ListNodepre=null; while(head!=null){ ListNodenext=head.next; head.next

  • spring下载

    下载安装 编辑 下载和安装Spring请按如下步骤进行。 (1)登录站点,下载Spring的最新稳定版本。最新版本为spring-framework-5.0.建议下载spring-framework-spring-framework-4.0.0.M2-dist这个压缩包不仅包含Spring的开发包,而且包含Spring编译和运行所依赖的第三方类库。 解压缩下载到的压缩包,解压缩后的文件夹应用如下几个文件夹。 dist:该文件夹下放Spring的jar包,通常只需要Spring.jar文件即可。该文件夹下还有一些类似spring-Xxx.jar的压缩包,这些压缩包是spring.jar压缩包的子模块压缩包。除非确定整个J2EE应用只需要使用Spring的某一方面时,才考虑使用这种分模块压缩包。通常建议使用Spring.jar docs:该文件夹下包含spring的相关文档、开发指南及API参考文档。 lib:该文件夹下包含spring编译和运行所依赖的第三方类库,该路径下的类库并不是spring必需的,但如果需要使用第三方类库的支持,这里的类库就是必需要的。 samples:该文件夹下包

  • Agile PLM 文件服务器报错解决

    ​   上传附件报错:未上传和错误。 问题描述: 文件服务器控制台出现如下报错信息: Userbrowseroriginhttp://110.66.52.212:7001isnotinconfiguredAccessControlAllowOriginurlslist.PleasecontactyourAdministrator 错误截图: ​ ​  解决方案 不要使用IP访问Agile应用。如果要用IP访问,需要在配置文件server.conf的最后一行添加例外 ​

相关推荐

推荐阅读