Slub Memory Allocator -2- (캐시 생성)

커널 부트업 프로세스를 진행하는 중에 캐시를 초기화 시킨 이후부터 slub 페이지를 관리하기 위해 커널 메모리 캐시(kmem_cache, 이하 캐시)를 생성 시킬 수 있고, 특정 조건(object 사이즈 및 플래그 설정)을 만족하는 경우 캐시를 만들지 않고 기존 캐시와 병합되어 사용할 수도 있다. 전체적인 흐름은 아래 그림과 같은 함수 호출 순서로 캐시를 생성한다.

kmem_cache_create-1d

 

kmem_cache_create()

mm/slab_common.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.
 * @align: The required alignment for the objects.
 * @flags: SLAB flags
 * @ctor: A constructor for the objects.
 *
 * Returns a ptr to the cache on success, NULL on failure.
 * Cannot be called within a interrupt, but can be interrupted.
 * The @ctor is run when new pages are allocated by the cache.
 *
 * 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_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.
 */
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
                  unsigned long flags, void (*ctor)(void *))
{
        struct kmem_cache *s;
        const char *cache_name;
        int err;

        get_online_cpus();
        get_online_mems();
        memcg_get_cache_ids();
        
        mutex_lock(&slab_mutex);
        
        err = kmem_cache_sanity_check(name, size);
        if (err) {
                s = NULL;       /* suppress uninit var warning */
                goto out_unlock;
        }

병합될 캐시를 찾은 경우 alias 캐시로 등록하고 캐시는 생성하지 않는다. 만일 병합될 캐시를 찾지 못한 경우에는 캐시를 생성한다.

  •  get_online_cpus();
    • CONFIG_HOTPLUG_CPU 커널 옵션을 사용하는 경우에만 cpu_hotplug.refcount를 증가시켜 cpu를 분리하는 경우 동기화(지연)시킬 목적으로 설정한다.
  • get_online_mems();
    • CONFIG_MEMORY_HOTPLUG 커널 옵션을 사용하는 경우에만 mem_hotplug.refcount를 증가시켜 memory를 분리하는 경우 동기화(지연)시킬 목적으로 설정한다.
  • memcg_get_cache_ids();
    • MEMCG_KMEM 커널 옵션을 사용하여 Slab(Slub) 커널 메모리 사용량을 제어하고자 할 목적으로 read 세마포어 락을 사용한다.
  • err = kmem_cache_sanity_check(name, size);
    • CONFIG_DEBUG_VM 커널 옵션을 사용하는 경우에만 name과 size에 대한 간단한 체킹을 수행한다. 이 커널 옵션을 사용하지 않는 경우에는 항상 false를 반환한다.

 

        /*
         * Some allocators will constraint the set of valid flags to a subset
         * of all flags. We expect them to define CACHE_CREATE_MASK in this
         * case, and we'll just provide them with a sanitized version of the
         * passed flags.
         */
        flags &= CACHE_CREATE_MASK;
        
        s = __kmem_cache_alias(name, size, align, flags, ctor);
        if (s)  
                goto out_unlock;
        
        cache_name = kstrdup_const(name, GFP_KERNEL);
        if (!cache_name) {
                err = -ENOMEM;
                goto out_unlock;
        }

        s = do_kmem_cache_create(cache_name, size, size,
                                 calculate_alignment(flags, align, size),
                                 flags, ctor, NULL, NULL);
        if (IS_ERR(s)) {
                err = PTR_ERR(s);
                kfree_const(cache_name);
        }

out_unlock:
        mutex_unlock(&slab_mutex);

        memcg_put_cache_ids();
        put_online_mems();
        put_online_cpus();

        if (err) {
                if (flags & SLAB_PANIC)
                        panic("kmem_cache_create: Failed to create slab '%s'. Error %d\n",
                                name, err);
                else {
                        printk(KERN_WARNING "kmem_cache_create(%s) failed with error %d",
                                name, err);
                        dump_stack();
                }
                return NULL;
        }
        return s;
}
EXPORT_SYMBOL(kmem_cache_create);
  • flags &= CACHE_CREATE_MASK;
    • kmem_cache를 생성할 때 유효한 플래그만 통과시킨다.
  • s = __kmem_cache_alias(name, size, align, flags, ctor);
    • 병합될 캐시를 찾은 경우 캐시 생성을 포기하고 alias 캐시로 등록한다.
  • if (s) goto out_unlock;
    • 요청한 캐시는 alias 캐시로 등록되었으므로 이 루틴을 빠져나간다.
  • cache_name = kstrdup_const(name, GFP_KERNEL);
    • name을 clone한 후 cache_name으로 반환한다. 단 name이 .rodata 섹션에 있는 경우 name을 그대로 반환한다.
  • s = do_kmem_cache_create(cache_name, size, size, calculate_alignment(flags, align, size), flags, ctor, NULL, NULL);
    • 캐시를 생성한다.

 

다음 그림은 각종 플래그들에 대한  매크로 상수이다. (각 플래그 다음에 위치한 한 글자의 알파벳 문자는 slabinfo 유틸리티를 사용하여 플래그 속성값이 출력될 때 사용되는 알파벳 문자이다.)

slab-flags-1b

 

get_online_cpus()

mm/memory_hotplug.c

void get_online_cpus(void)
{
        might_sleep();
        if (cpu_hotplug.active_writer == current)
                return;
        cpuhp_lock_acquire_read();
        mutex_lock(&cpu_hotplug.lock);
        atomic_inc(&cpu_hotplug.refcount);
        mutex_unlock(&cpu_hotplug.lock);
}
EXPORT_SYMBOL_GPL(get_online_cpus);

CONFIG_HOTPLUG_CPU 커널 옵션을 사용하는 경우에만 cpu_hotplug.refcount를 증가시켜 cpu를 분리하는 경우 동기화(지연)시킬 목적으로 설정한다.

 

get_online_mems()

mm/memory_hotplug.c

void get_online_mems(void)
{       
        might_sleep();
        if (mem_hotplug.active_writer == current)
                return;
        memhp_lock_acquire_read();
        mutex_lock(&mem_hotplug.lock);
        mem_hotplug.refcount++;
        mutex_unlock(&mem_hotplug.lock);

}

CONFIG_MEMORY_HOTPLUG 커널 옵션을 사용하는 경우에만 mem_hotplug.refcount를 증가시켜 메모리를 분리하는 경우 동기화(지연)시킬 목적으로 설정한다.

 

kmem_cache_sanity_check()

mm/slab_common.c

#ifdef CONFIG_DEBUG_VM
static int kmem_cache_sanity_check(const char *name, size_t size)
{
        struct kmem_cache *s = NULL;

        if (!name || in_interrupt() || size < sizeof(void *) ||
                size > KMALLOC_MAX_SIZE) {
                pr_err("kmem_cache_create(%s) integrity check failed\n", name);
                return -EINVAL;
        }

        list_for_each_entry(s, &slab_caches, list) {
                char tmp;
                int res;

                /*
                 * This happens when the module gets unloaded and doesn't
                 * destroy its slab cache and no-one else reuses the vmalloc
                 * area of the module.  Print a warning.
                 */
                res = probe_kernel_address(s->name, tmp);
                if (res) {
                        pr_err("Slab cache with size %d has lost its name\n",
                               s->object_size);
                        continue;
                }
        }

        WARN_ON(strchr(name, ' '));     /* It confuses parsers */
        return 0;
}
#else
static inline int kmem_cache_sanity_check(const char *name, size_t size)
{
        return 0;
}
#endif

CONFIG_DEBUG_VM 커널 옵션을 사용하는 경우 size 및 캐시 name을 체크하고 그 체크 결과를 반환한다. CONFIG_DEBUG_VM 커널 옵션을 사용하지 않는 경우 0(성공)을 반환한다.

  • if (!name || in_interrupt() || size < sizeof(void *) || size > KMALLOC_MAX_SIZE) {
    • name이 null이거나 인터럽트 핸들러에서 호출된 경우(인터럽트 마스크된 상황)이거나 size가 워드보다 작거나 KMALLOC_MAX_SIZE(4M)를 초과한 경우 에러 메시지를 출력하고 함수를 에러로 빠져나간다.
  • list_for_each_entry(s, &slab_caches, list) {
    • 전역 slab_caches 리스트에 속한 모든 kmem_cache에 대해 루프를 돈다.
  • res = probe_kernel_address(s->name, tmp);
    • 캐시명을 tmp에 읽어온다. access 실패 시 음수 에러 값을 갖는다.
  • if (res) { pr_err(“Slab cache with size %d has lost its name\n”, s->object_size); continue; }
    • 루프 내에서 이러한 케이스가 나올 때 마다 에러 캐시에 대해 메시지를 출력한다.

 

__kmem_cache_alias()

mm/slub.c

struct kmem_cache *
__kmem_cache_alias(const char *name, size_t size, size_t align,
                   unsigned long flags, void (*ctor)(void *))
{
        struct kmem_cache *s, *c;

        s = find_mergeable(size, align, flags, name, ctor);
        if (s) {
                s->refcount++;

                /*
                 * Adjust the object sizes so that we clear
                 * the complete object on kzalloc.
                 */
                s->object_size = max(s->object_size, (int)size);
                s->inuse = max_t(int, s->inuse, ALIGN(size, sizeof(void *)));

                for_each_memcg_cache(c, s) {
                        c->object_size = s->object_size;
                        c->inuse = max_t(int, c->inuse,
                                         ALIGN(size, sizeof(void *)));
                }

                if (sysfs_slab_alias(s, name)) {
                        s->refcount--;
                        s = NULL;
                }
        }

        return s;
}

병합 가능한 캐시를 찾은 경우 그 캐시의 object_size와 inuse를 update하고 sysfs를 통해 요청한 캐시의 링크를 추가한다. 병합 가능한 캐시를 찾지못한 경우 null을 반환한다.

  • s= find_mergeable(size, align, flags, name, ctor);
    • 병합 가능한 캐시를 알아온다. 병합할 수 없으면 null을 반환한다.
      • 예) signal_cache, UNIX
        • signal_cache가 병합되어 사용되는 실제 캐시이고 UNIX는 alias 캐시로 사용된다. (size=704, align=64)
  • if (s) { cachep->refcount++;
    • 병합될 캐시를 찾은 경우 refcount를 증가시키고
      • 요청한 캐시는 alias 캐시로 등록되고 실제 병합될 캐시를 사용하므로 레퍼런스 카운터를 증가시킨다.
  • s->object_size = max_t(int, s->object_size, size); }
    • 병합될 캐시의 object_size보다 요청 캐시의 size가 더 큰 경우 갱신한다.
  • s->inuse = max_t(int, s->inuse, ALIGN(size, sizeof(void *)));
    • 병합될 캐시의 inuse(메타데이터까지의 offset, 메타데이터 없는 경우 0) 보다 워드 단위로 정렬한 size 값이 큰 경우 갱신한다.
      • inuse
        • 메타 데이터까지의 offset 값(메타 데이터가 없는 경우 워드 단위로 정렬한 size와 동일)
  • for_each_memcg_cache(c, s) {
    • 병합될 캐시의 모든 memcg 캐시에 대하여 루프를 돈다.
  • c->object_size = s->object_size;
    • memcg용 object size를 병합될 캐시의 object size와 동일하게 갱신한다.
    • 모든 alias 캐시와 동일한 사이즈를 유지
  • c->inuse = max_t(int, c->inuse, ALIGN(size, sizeof(void *))); }
    • memcg용 inuse를 memcg용 inuse와 워드 단위로 정렬한 size 값 중 큰 값으로 대입한다.
  • if (sysfs_slab_alias(s, name)) { s->refcount–; s = NULL; }
    • CONFIG_SYSFS 커널 옵션을 사용하는 경우 생성된 캐시에 대한 sysfs 링크를 만든다. 단 커널이 부트업 프로세스 중인 경우 링크 생성을 slab_sysfs 드라이버 가동후로 미룬다. 링크가 만들어지지 않는 경우 에러(0이 아닌)를 반환한다.
    • 에러를 반환한 경우 병합될 캐시로의 사용을 포기하기 위해 refcount를 감소 시키고 null을 반환한다.

 

다음 그림은 생성 요청한 캐시에 대해 병합될 캐시를 찾은 경우 새로운 캐시를 등록하지 않고 그냥 alias 캐시 리스트에 등록하는 경우를 보여준다.

__kmem_cache_alias-1b

 

find_mergeable()

mm/slab_common.c

struct kmem_cache *find_mergeable(size_t size, size_t align,
                unsigned long flags, const char *name, void (*ctor)(void *))
{
        struct kmem_cache *s;

        if (slab_nomerge || (flags & SLAB_NEVER_MERGE))
                return NULL;

        if (ctor)
                return NULL;

        size = ALIGN(size, sizeof(void *));
        align = calculate_alignment(flags, align, size);
        size = ALIGN(size, align);
        flags = kmem_cache_flags(size, flags, name, NULL);
                                         
        list_for_each_entry_reverse(s, &slab_caches, list) {
                if (slab_unmergeable(s))
                        continue;
                        
                if (size > s->size)
                        continue;

                if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
                        continue;
                /*
                 * Check if alignment is compatible.
                 * Courtesy of Adrian Drzewiecki
                 */
                if ((s->size & ~(align - 1)) != s->size)
                        continue;
                        
                if (s->size - size >= sizeof(void *))
                        continue;

                if (IS_ENABLED(CONFIG_SLAB) && align &&
                        (align > s->align || s->align % align))
                        continue;

                return s;
        }
        return NULL;
}

병합 가능한 캐시를 검색하여 아래의 엄격한 조건을 만족하는 캐시를 찾아 반환하고 찾지 못한 경우 null을 반환한다.

  • “slub_nomerge” 커널 파라메터를 사용한 경우는 안된다.
  • 요청 캐시의 플래그에서 SLAB_NEVER_MERGE 플래그들 중 하나라도 사용하면 안된다. (대부분 디버그용 플래그)
  • 요청 캐시가 별도의 object 생성자를 사용하면 안된다.
  • 전체 캐시들에 대해 루프를 돌며 다음 조건을 비교한다.
    • 캐시의 플래그에서 SLAB_NEVER_MERGE 플래그들 중 하나라도 사용하면 안된다.
    • 루트 캐시가 아니면 안된다.
    • 캐시에 별도의 object 생성자가 사용되면 안된다.
    • 레퍼런스 카운터가 0보다 작으면 안된다. (초기화 중)
    • 재요청된 캐시 사이즈가 다음 범위를 벗어나면 안된다.
      • 캐시 size >= 재조정된 요청 size > 캐시 size – 워드 사이즈
    • 요청 플래그에 사용한 SLAB_MERGE_SAME 플래그들이 캐시에서 사용된 플래그와 동일하지 않으면 안된다.
    • 캐시의 size가 재조정된 요청 align 단위로 정렬되지 않으면 안된다.

 

  • if (slab_nomerge || (flags & SLAB_NEVER_MERGE)) return NULL;
    • “slab_nomerge” 커널 파라메터를 사용했거나 SLAB_NERVER_MERGE(7개) 디버그 플래그들을 사용한 경우 null을 반환한다.
  • if (ctor) return NULL;
    • 별도의 object 생성자가 주어진 경우 null을 반환한다.
  • size = ALIGN(size, sizeof(void *));
    • 요청 size를 워드 단위로 정렬한다.
  • align = calculate_alignment(flags, align, size);
    • 플래그 옵션 및 size에 따라 적절한 align 값을 결정한다.
  • size = ALIGN(size, align);
    • 다시 size 값을 align 단위로 round up 한다.
  • flags = kmem_cache_flags(size, flags, name, NULL);
    • CONFIG_SLUB_DEBUG 커널 옵션을 사용하는 경우 전역 변수 slub_debug에 저장된 플래그들을 추가한다.
    • 커널이 slub을 사용하는 경우 size와 생성자에 들어가는 인수 값  NULL은 flags 값을 결정하는 연산에 사용되지 않는다.
  • list_for_each_entry_reverse(s, &slab_caches, list) {
    • 전역 slab_caches에 있는 모든 kmem_cache에 대해 역순으로 루프를 돈다.
  • if (slab_unmergeable(s)) continue;
    • 캐시가 병합될 수 없는 조건을 가진 경우 다음 캐시로 continue
  • if (size > s->size) continue;
    • 조정된 size가 비교할 캐시의 size보다 큰 경우 다음 캐시로 continue
  • if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME)) continue;
    • 요청 플래그와 캐시 플래그가 사용한 SLAB_MERGE_SAME(4개) 플래그 값이 서로 다른 경우 다음 캐시로 continue
  • if ((s->size & ~(align – 1)) != s->size) continue;
    • 캐시 사이즈가 재조정된 align 단위로 정렬이 되지 않는 경우 다음 slub으로 continue
  • if (s->size – size >= sizeof(void *)) continue;
    • 캐시 사이즈가 재조정된 size + 워드 사이즈 보다 같거나 큰 경우 다음 캐시로 continue
      • 성공 조건:
        • 캐시 size >= 재조정된 요청 size > 캐시 size – 워드 사이즈
          • 예) s->size=64, 병합가능한 사이즈=61~64
  • if (IS_ENABLED(CONFIG_SLAB) && align && (align > s->align || s->align % align)) continue;
    • CONFIG_SLAB 커널 옵션을 사용하는 경우에만 적용
      • 재조정된 align이 캐시의 align보다 크거나 캐시의 align이 재조정된 align 단위가 아닌 경우 다음 캐시로 continue

 

mm/slab_common.c

/*
 * Set of flags that will prevent slab merging
 */
#define SLAB_NEVER_MERGE (SLAB_RED_ZONE | SLAB_POISON | SLAB_STORE_USER | \
                SLAB_TRACE | SLAB_DESTROY_BY_RCU | SLAB_NOLEAKTRACE | \
                SLAB_FAILSLAB)
  • 디버그 옵션들을 사용한 캐시에 대해 병합을 할 수 없다.

 

#define SLAB_MERGE_SAME (SLAB_DEBUG_FREE | SLAB_RECLAIM_ACCOUNT | \
                SLAB_CACHE_DMA | SLAB_NOTRACK)
  • 캐시 병합을 하기 위해서는 대상 캐시의 플래그와 요청 캐시의 플래그에 대해 위의 4개 플래그가 서로 동일하여야 한다.

 

kmem_cache_flags()

mm/slub.c

unsigned long kmem_cache_flags(unsigned long object_size,
        unsigned long flags, const char *name,
        void (*ctor)(void *))
{
        /*
         * Enable debugging if selected on the kernel commandline.
         */     
        if (slub_debug && (!slub_debug_slabs || (name &&
                !strncmp(slub_debug_slabs, name, strlen(slub_debug_slabs)))))
                flags |= slub_debug;

        return flags;
}

CONFIG_SLUB_DEBUG 커널 옵션을 사용하는 경우 전역 변수 slub_debug에 저장된 플래그들을 추가한다.

  • “slub_debug=” 커널 파라메터를 통해서 각종 디버그 옵션을 선택하면 slub_debug 값(SLAB_DEBUG_FREE, SLAB_RED_ZONE, SLAB_POISON, SLAB_STORE_USER, SLAB_TRACE, SLAB_FAILSLAB)이 결정되고 “,” 뒤의 문자열이 slub_debug_slabs에 저장된다.

 

 

slab_unmergeable()

mm/slab_common.c

/*
 * Find a mergeable slab cache
 */
int slab_unmergeable(struct kmem_cache *s)
{       
        if (slab_nomerge || (s->flags & SLAB_NEVER_MERGE))
                return 1;
        
        if (!is_root_cache(s))
                return 1;
        
        if (s->ctor)
                return 1;

        /*
         * We may have set a slab to be unmergeable during bootstrap.
         */     
        if (s->refcount < 0)
                return 1;
                        
        return 0;
}

캐시를 병합할 수 없는 경우 true를 반환한다. 다음은 병합이 불가능한 조건이다.

  • “slub_nomerge” 커널 파라메터를 사용하는 경우 또는 캐시의 플래그에 SLAB_NEVER_MERGE 관련 플래그를 사용한 경우 병합 불가능
  • 루트 캐시가 아닌 경우
  • 별도의 object 생성자가 주어진 경우
  • refcount가 0보다 작은 경우
    • 캐시가 부트업 프로세스 중에 만들어져 아직 동작하지 않는 상태

 

sysfs_slab_alias()

mm/slub.c

static int sysfs_slab_alias(struct kmem_cache *s, const char *name)
{
        struct saved_alias *al;

        if (slab_state == FULL) {
                /*
                 * If we have a leftover link then remove it.
                 */
                sysfs_remove_link(&slab_kset->kobj, name);
                return sysfs_create_link(&slab_kset->kobj, &s->kobj, name);
        }

        al = kmalloc(sizeof(struct saved_alias), GFP_KERNEL);
        if (!al)
                return -ENOMEM;

        al->s = s;
        al->name = name;
        al->next = alias_list;
        alias_list = al;
        return 0;
}

CONFIG_SYSFS 커널 옵션을 사용하는 경우 생성된 캐시에 대한 sysfs 링크를 만든다. 단 커널이 부트업 프로세스 중인 경우 링크 생성을 slab_sysfs 드라이버 가동후로 미룬다. 링크가 만들어지지 않는 경우 에러(0이 아닌)를 반환한다.

  • if (slab_state == FULL) {
    • slub 메모리 할당자가 완전히 동작을 시작한 경우
  • sysfs_remove_link(&slab_kset->kobj, name);
    • name 링크를 삭제한다.
  • return sysfs_create_link(&slab_kset->kobj, &s->kobj, name);
    • 다시 name으로 링크를 만들고 반환한다.
  • al = kmalloc(sizeof(struct saved_alias), GFP_KERNEL);
    • saved_alias 구조체 메모리 영역을 할당한다.
  • al->s = s; al->name = name; al->next = alias_list; alias_list = al;
    • saved_alias 에 slub 정보를 담고 전역 alias_list에 추가한다.
    • 이렇게 추가된 alias_list는 나중에 커널이 각 드라이버를 호출할 때 __initcall() 함수에 등록된 slab_sysfs_init() 루틴이 호출될 때 slab_state를 full 상태로 바꾸고 alias_list에 있는 모든 alias 캐시에 대해 sysfs_slab_alias() 루틴이 다시 호출되면서 sysfs를 사용하여 링크를 만들게 된다.

 

다음은 :at-000032 캐시가 slab_sysfs에 반영되어 링크가 만들진 예를 보여준다. (위치: /sys/kernel/slab/<캐시명>)

$ /sys/kernel/slab/:at-0000032$ ls
aliases      ctor            objects_partial  reserved           total_objects
align        destroy_by_rcu  objs_per_slab    sanity_checks      trace
alloc_calls  free_calls      order            shrink             validate
cache_dma    hwcache_align   partial          slabs
cgroup       min_partial     poison           slabs_cpu_partial
cpu_partial  objects         reclaim_account  slab_size
cpu_slabs    object_size     red_zone         store_user

 

do_kmem_cache_create() – (slab, slub)

mm/slab_common.c

static struct kmem_cache *
do_kmem_cache_create(const char *name, size_t object_size, size_t size,
                     size_t align, unsigned long flags, void (*ctor)(void *),
                     struct mem_cgroup *memcg, struct kmem_cache *root_cache)
{
        struct kmem_cache *s;
        int err;

        err = -ENOMEM;
        s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
        if (!s)
                goto out;

        s->name = name;
        s->object_size = object_size;
        s->size = size;
        s->align = align;
        s->ctor = ctor;

        err = init_memcg_params(s, memcg, root_cache);
        if (err)
                goto out_free_cache;

        err = __kmem_cache_create(s, flags);
        if (err)
                goto out_free_cache;

        s->refcount = 1;
        list_add(&s->list, &slab_caches);
out:
        if (err)
                return ERR_PTR(err);
        return s;

out_free_cache:
        destroy_memcg_params(s);
        kmem_cache_free(kmem_cache, s);
        goto out;
}
  • s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
    • 새로운 캐시를 만들어 관리하기 위해 kmem_cache에서 slub object를 할당 받는다.
      • 이 루틴은 slub 캐시가 이미 가동중에 호출된다.
  • s->name = name; s->object_size = object_size; s->size = size; s->align = align; s->ctor = ctor;
    • 할당받은 kmem_cache 구조체에 각 인수를 대입한다.
  • err = init_memcg_params(s, memcg, root_cache);
    • CONFIG_MEMCG_KMEM 커널 옵션을 사용하는 경우 memcg에서 커널 메모리에 대한 관리를 위해 각 파라메터들을 초기화한다.
  • err = __kmem_cache_create(s, flags);
    • 요청한 캐시를 생성한다.
  • s->refcount = 1; list_add(&s->list, &slab_caches);
    • 캐시 레퍼런스  카운터를 1로 만들고 캐시를 전역 slab_caches에 추가한다.
      • 실제 캐시 또는 alias 캐시를 생성할 때 마다 실제 캐시의 레퍼런스가 증가되고, 반대로 캐시 또는 alias 캐시를 소멸(삭제)시킬 때 마다 실제 캐시의 레퍼런스 카운터 값을 감소시킨다. 0 값이 되는 경우 실제 캐시를 소멸(삭제)시킬 수 있다.

 

__kmem_cache_create() – (slub)

mm/slub.c

int __kmem_cache_create(struct kmem_cache *s, unsigned long flags)
{
        int err;

        err = kmem_cache_open(s, flags);
        if (err)
                return err;

        /* Mutex is not taken during early boot */
        if (slab_state <= UP)
                return 0;

        memcg_propagate_slab_attrs(s);
        err = sysfs_slab_add(s);
        if (err)
                kmem_cache_close(s);

        return err;
}

캐시를 생성하고, 커널이 부트업 중이 아닌 경우 memcg용 속성들을 읽어서 설정한 후 sysfs에 생성한 캐시에 대한 링크들을 생성한다.

  • err = kmem_cache_open(s, flags);
  • if (slab_state <= UP) return 0;
    • 완전히 full slab 시스템이 가동되기 전, 즉 early bootup이 진행 중에는 함수를 빠져나간다.
  • memcg_propagate_slab_attrs(s);
    • CONFIG_MEMCG_KMEM 커널 옵션을 사용한 경우 생성한 캐시의 루트 캐시에 대한 모든 속성을 읽어서 다시 한 번 재 설정한다.
  • err = sysfs_slab_add(s);
    • 생성된 캐시에 대한 내용을 파일 시스템을 통해 속성들을 보거나 설정할 수 있도록 링크들을 생성한다.

 

/*              
 * State of the slab allocator.
 *
 * This is used to describe the states of the allocator during bootup.
 * Allocators use this to gradually bootstrap themselves. Most allocators
 * have the problem that the structures used for managing slab caches are
 * allocated from slab caches themselves.
 */
enum slab_state {
        DOWN,                   /* No slab functionality yet */
        PARTIAL,                /* SLUB: kmem_cache_node available */
        PARTIAL_NODE,           /* SLAB: kmalloc size for node struct available */
        UP,                     /* Slab caches usable but not all extras yet */
        FULL                    /* Everything is working */
};

slab(slub) 메모리 할당자의 운영 상태로 slub 메모리 시스템에 대해서는 다음과 같다.

  • DOWN
    • 아직 커널이 부트업 프로세스를 진행중이며 slub 메모리 시스템이 만들어지지 않은 상태
  • PARTIAL
    • kmem_cache_node가 존재하는 상태
      • 캐시를 생성하기 위해 캐시 내부에 필요한 kmem_cache_node가 필요한데 이를 만들기 위해 부트업 처리 중 가장 먼저 만든 캐시이다.
  • UP
    • kmem_cache 시스템은 동작하나 다른 엑스트라 시스템이 아직 활성화 되지 않은 상태
  • FULL
    • slub 메모리 할당자에 대한 부트업 프로세스가 완료되어 slub에 대한 모든 것이 동작하는 상태

 

kmem_cache_open()

mm/slub.c

static int kmem_cache_open(struct kmem_cache *s, unsigned long flags)
{
        s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
        s->reserved = 0;

        if (need_reserve_slab_rcu && (s->flags & SLAB_DESTROY_BY_RCU))
                s->reserved = sizeof(struct rcu_head);

        if (!calculate_sizes(s, -1))
                goto error;
        if (disable_higher_order_debug) {
                /*
                 * Disable debugging flags that store metadata if the min slab
                 * order increased.
                 */
                if (get_order(s->size) > get_order(s->object_size)) {
                        s->flags &= ~DEBUG_METADATA_FLAGS;
                        s->offset = 0;
                        if (!calculate_sizes(s, -1))
                                goto error;
                }
        }

#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
    defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
        if (system_has_cmpxchg_double() && (s->flags & SLAB_DEBUG_FLAGS) == 0)
                /* Enable fast mode */
                s->flags |= __CMPXCHG_DOUBLE;
#endif

        /*
         * The larger the object size is, the more pages we want on the partial
         * list to avoid pounding the page allocator excessively.
         */
        set_min_partial(s, ilog2(s->size) / 2);
  • s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
    • “slab_debug=” 커널 파라메터에 의해 몇 개의 디버그 요청이 있는 경우 object를 만들 때 반영하기 위해 해당 기능의 slub 디버그 플래그를 추가한다.
  • if (need_reserve_slab_rcu && (s->flags & SLAB_DESTROY_BY_RCU)) s->reserved = sizeof(struct rcu_head);
    • SLAB_DESTROY_BY_RCU 플래그가 사용된 경우 캐시의 reserved에 rcu_head 사이즈를 대입한다.
    • RCU(lock-less) 방식으로 해당 캐시 사용을 구현한 경우 빠르게 slub object를 free할 수 있는 장점이 있다.
      • 반대 급부로 구현(사용)이 복잡한다.
  • if (!calculate_sizes(s, -1)) goto error;
    • 객체 size에 따른 order 및 객체 수 등이 산출되지 않는 경우 에러 처리로 이동 한다.

 

“slub_debug=O” 커널 파라메터가 사용된 경우 디버깅 기능으로 인해 order 값이 상승되는 경우 디버깅 기능을 disable하게 한다.

  • if (disable_higher_order_debug) {
    • disable_higher_order_debug 기능이 설정된 경우
      • “slub_debug=O” 커널 파라메터와 같이 “O” 옵션이 사용되는 경우 disable_higher_order_debug 변수 값이 enable로 변경 된다.
  • if (get_order(s->size) > get_order(s->object_size)) {
    • s->size를 할당하기 위한 계산된 order 값이 s->object_size를 할당하기 위해 계산된 order 값보다 큰 경우
    • 즉 디버그등 목적으로 메타데이터가 추가되어 페이지 할당이 더 필요한 경우
  • s->flags &= ~DEBUG_METADATA_FLAGS; s->offset = 0;
    • 메타 데이타가 추가되는 플래그들을 클리어하고, offset을 0으로 대입한다.
      • 메타 데이터로 인해 order가 커지게 되면 메타 데이터를 추가하지 못하게 막는다.
  • if (!calculate_sizes(s, -1)) goto error;
  • CONFIG_HAVE_CMPXCHG_DOUBLE && CONFIG_HAVE_ALIGNED_STRUCT_PAGE
    • 해당 커널 옵션을 사용하는 경우 page 구조체의 멤버 변수 counters를 8바이트 형인 unsigned long 타입으로 변경하여 사용하게 한다.
  • if (system_has_cmpxchg_double() && (s->flags & SLAB_DEBUG_FLAGS) == 0) s->flags |= __CMPXCHG_DOUBLE;
    • 시스템이 더블 워드 데이터 형에 대해 cmpxchg 기능을 지원하면서 slab 디버그 플래그를 사용하지 않는 경우 플래그에 __CMPXCHG_DOUBLE가 추가된다.
      • x86 아키텍처나 64bit arm 아키텍처 등에서 지원하고 32bit arm에서는 지원하지 않는다.
  • set_min_partial(s, ilog2(s->size) / 2);
    • object의 size를 표현하는데 필요한 비트 수의 절반을 min_partial에 저장한다. 단 5~10 범위 사이로 조정한다.
      • 예)
        • size가 4K -> 필요한 비트 수=12 -> min_partial = 6
        • size가 1M -> 필요한 비트 수=20 -> min_partial = 10

 

        /*
         * cpu_partial determined the maximum number of objects kept in the
         * per cpu partial lists of a processor.
         *
         * Per cpu partial lists mainly contain slabs that just have one
         * object freed. If they are used for allocation then they can be
         * filled up again with minimal effort. The slab will never hit the
         * per node partial lists and therefore no locking will be required.
         *
         * This setting also determines
         *
         * A) The number of objects from per cpu partial slabs dumped to the
         *    per node list when we reach the limit.
         * B) The number of objects in cpu partial slabs to extract from the
         *    per node list when we run out of per cpu objects. We only fetch
         *    50% to keep some capacity around for frees.
         */
        if (!kmem_cache_has_cpu_partial(s))
                s->cpu_partial = 0;
        else if (s->size >= PAGE_SIZE)
                s->cpu_partial = 2;
        else if (s->size >= 1024)
                s->cpu_partial = 6;
        else if (s->size >= 256)
                s->cpu_partial = 13;
        else
                s->cpu_partial = 30;

#ifdef CONFIG_NUMA
        s->remote_node_defrag_ratio = 1000;
#endif
        if (!init_kmem_cache_nodes(s))
                goto error;

        if (alloc_kmem_cache_cpus(s))
                return 0;

        free_kmem_cache_nodes(s);
error:
        if (flags & SLAB_PANIC)
                panic("Cannot create slab %s size=%lu realsize=%u "
                        "order=%u offset=%u flags=%lx\n",
                        s->name, (unsigned long)s->size, s->size,
                        oo_order(s->oo), s->offset, flags);
        return -EINVAL;
}

size에 적합한 cpu_partial 갯수를 산출한다.

  • if (!kmem_cache_has_cpu_partial(s)) s->cpu_partial = 0;
    • 지정된 slab의 cpu partial이 지원되지 않는 경우 cpu_partial에 0을 대입한다.
      • 첫 번째, CONFIG_SLUB_CPU_PARTIAL 커널 옵션을 사용하지 않을 때에 cpu partial 리스트를 지원하지 않는다.
      • 두 번째, SLAB_DEBUG_FLAGS 중 하나라도 사용하는 경우 cpu partial 리스트를 지원하지 않는다.
        • SLAB_DEBUG_FLAGS
          • SLAB_RED_ZONE, SLAB_POISON, SLAB_STORE_USER, SLAB_TRACE, SLAB_DEBUG_FREE
  • else if (s->size >= PAGE_SIZE) s->cpu_partial = 2;
    • size가 PAGE_SIZE 이상인 경우 cpu_partial에 2를 대입한다.
  • else if (s->size >= 1024) s->cpu_partial = 6;
    • size가 1024 이상인 경우 cpu_partial에 6을 대입한다.
  • else if (s->size >= 256) s->cpu_partial = 13;
    • size가 256 이상인 경우 cpu_partial에 13을 대입한다.
  • else s->cpu_partial = 30;
    • 그렇지 않은 경우 cpu_partial에 30을 대입한다.
  • s->remote_node_defrag_ratio = 1000;
    • NUMA 시스템인 경우 remote_node_defrag_ratio에 1000을 대입한다.
      •  로컬 노드의 partial 리스트가 부족할 때 리모트 노드의 partial 리스트를 이용할 수 있도록 하는데 이의 허용률을 1000으로 설정한다.
        • 허용 수치는 0~100까지 입력하는 경우 그 값을 10배 곱하고, 100이상 수치 입력하는 경우 그대로 허용한다.
        • 1000으로 설정하는 경우 약 98%의 성공률로 설정된다.
          • 하드웨어 딜레이 타이머를 1024로 나눈 나머지가 이 수치 이하인 경우에만 허용(성공)한다.
          • 1024 이상으로 설정하는 경우 100% 허용(성공)
  • if (!init_kmem_cache_nodes(s)) goto error;
    • per 노드의 초기화가 실패하는 경우 error로 이동한다.
  • if (alloc_kmem_cache_cpus(s)) return 0;
    • per cpu에 대한 할당이 성공하면 함수를 종료한다.
  • free_kmem_cache_nodes(s);
    • 실패한 경우 per 노드를 해제하고 에러를 리턴한다.

 

 

set_min_partial()

mm/slub.c

static void set_min_partial(struct kmem_cache *s, unsigned long min)
{
        if (min < MIN_PARTIAL)
                min = MIN_PARTIAL;
        else if (min > MAX_PARTIAL)
                min = MAX_PARTIAL;
        s->min_partial = min;
}

지정한 캐시의 min_partial 값을 설정한다. 단 min 값은 MIN_PARTIAL ~ MAX_PARTIAL을 벗어나는 경우 조정된다.

  • if (min < MIN_PARTIAL) min = MIN_PARTIAL;
    • min 값이 MIN_PARTIAL 이상이 되게 조정한다.
      • MIN_PARTIAL=5
  • else if (min > MAX_PARTIAL) min = MAX_PARTIAL;
    • min 값이 MAX_PARTIAL을 초과하지 않도록 조정ㅎ나다.
      • MAX_PARTIAL=10
  • s->min_partial = min;
    • 조정된 min 값을 지정된 캐시의 min_partial에 저장한다.

 

kmem_cache_has_cpu_partial()

mm/slub.c

static inline bool kmem_cache_has_cpu_partial(struct kmem_cache *s)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
        return !kmem_cache_debug(s);
#else
        return false;
#endif
}

CONFIG_SLUB_CPU_PARTIAL 커널 옵션이 사용되는 경우 지정된 캐시에서 디버그 플래그가 설정되어 사용되지 않는 경우에 cpu partial 리스트의 사용이 지원된다. 그렇지 않은 경우 false를 반환한다.

  • true=지정된 캐시의 cpu partial 리스트 사용을 지원한다.
  • false=지정된 캐시의 cpu partial 리스트 사용을 지원하지 않는다.

 

kmem_cache_debug()

mm/slub.c

static inline int kmem_cache_debug(struct kmem_cache *s)
{
#ifdef CONFIG_SLUB_DEBUG
        return unlikely(s->flags & SLAB_DEBUG_FLAGS);
#else
        return 0;
#endif
}

CONFIG_SLUB_DEBUG 커널 옵션이 사용되는 경우 지정된 캐시에서 SLAB_DEBUG_FLAGS 들 중 하나라도 설정되었는지 여부를 반환하고 그렇지 않은 경우 0을 반환한다.

  • 1=지정된 캐시의 디버그 플래그 설정됨
  • 0=지정된 캐시의 디버그 플래그가 설정되지 않음

 

init_kmem_cache_nodes()

mm/slub.c

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
        int node;

        for_each_node_state(node, N_NORMAL_MEMORY) {
                struct kmem_cache_node *n;

                if (slab_state == DOWN) {
                        early_kmem_cache_node_alloc(node);
                        continue;
                }
                n = kmem_cache_alloc_node(kmem_cache_node,
                                                GFP_KERNEL, node);

                if (!n) {
                        free_kmem_cache_nodes(s);
                        return 0;
                }

                s->node[node] = n;
                init_kmem_cache_node(n);
        }
        return 1;
}

메모리를 가진 모든 노드에 대해 부트 프로세스 진행 상태에 따라 다음 2가지 중 하나를 수행한다.

  • 부트 프로세스가 진행 중인 경우에는 아직 slub 메모리 할당자가 완전히 가동되지 않았기 때문에 slub object를 할당받지 못하는 상태이다. 따라서 kmem_cache_node 구조체를 준비하기 위해 slub object가 아닌 버디 시스템을 통해 slub 용도로 사용하기 위해 페이지를 하나 할당 받아 이 페이지에 kmem_cache_node 구조체 정보를 구성한다.
  • 부트 프로세스가 진행 중이지 않는 경우, 즉 slub 메모리 관리자가 동작 가능한 상태이면  kmem_cache_node 캐시에서 object를 할당 받고 초기화 루틴을 수행한다.

 

  • for_each_node_state(node, N_NORMAL_MEMORY) {
    • N_NORMAL_MEMORY 비트 설정된 모든 노드에 대해 루프를 돈다.
      • 메모리를 소유한 노드
  • if (slab_state == DOWN) { early_kmem_cache_node_alloc(node); continue; }
    • 부트 프로세스가 진행 중인 경우에는 아직 slub 메모리 할당자가 완전히 가동되지 않았기 때문에 slub object를 할당받지 못하는 상태이다. 따라서 kmem_cache_node 구조체를 준비하기 위해 slub object가 아닌 버디 시스템을 통해 slub 용도로 사용하기 위해 slub 페이지를 할당 받아 이 slub 페이지에 kmem_cache_node 구조체 정보를 구성한다.
  • n = kmem_cache_alloc_node(kmem_cache_node, GFP_KERNEL, node);
    • slab 상태가 부트업 프로세스 중이 아닌 경우 kmem_cache_node 캐시에서 object를 할당 받는다.
  • if (!n) { free_kmem_cache_nodes(s); return 0; }
    • object 할당이 실패한 경우 kmem_cache_node object를 해지하고 함수를 종료한다.
  •  s->node[node] = n;
    • 캐시의 per 노드에 할당된 kmem_cache_node object의 주소를 대입한다.
  • init_kmem_cache_node(n);
    • 할당된 kmem_cache_node 구조체 object에 대해 초기 정보를 설정한다.

 

early_kmem_cache_node_alloc()

mm/slub.c

/*
 * No kmalloc_node yet so do it by hand. We know that this is the first
 * slab on the node for this slabcache. There are no concurrent accesses
 * possible.
 *
 * Note that this function only works on the kmem_cache_node
 * when allocating for the kmem_cache_node. This is used for bootstrapping
 * memory on a fresh node that has no slab structures yet.
 */
static void early_kmem_cache_node_alloc(int node)
{
        struct page *page;
        struct kmem_cache_node *n;

        BUG_ON(kmem_cache_node->size < sizeof(struct kmem_cache_node));

        page = new_slab(kmem_cache_node, GFP_NOWAIT, node);

        BUG_ON(!page);
        if (page_to_nid(page) != node) {
                pr_err("SLUB: Unable to allocate memory from node %d\n", node);
                pr_err("SLUB: Allocating a useless per node structure in order to be able to continue\n");
        }

        n = page->freelist;
        BUG_ON(!n);
        page->freelist = get_freepointer(kmem_cache_node, n);
        page->inuse = 1;
        page->frozen = 0;
        kmem_cache_node->node[node] = n;
#ifdef CONFIG_SLUB_DEBUG
        init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
        init_tracking(kmem_cache_node, n);
#endif
        kasan_kmalloc(kmem_cache_node, n, sizeof(struct kmem_cache_node));
        init_kmem_cache_node(n);
        inc_slabs_node(kmem_cache_node, node, page->objects);

        /*
         * No locks need to be taken here as it has just been
         * initialized and there is no concurrent access.
         */
        __add_partial(n, page, DEACTIVATE_TO_HEAD);
}

커널이 부트업 프로세스 중에 있는 경우 아직 slub 메모리 할당자가 동작하지 않는 상태라서 slub object를 할당받는 kmem_cache_alloc() 함수 등을 사용할 수 없어서 버디시스템으로 부터 slub 용도의 페이지를 할당 받아올 수 있는 new_slab() 함수를 대신 사용하였다.  이 과정은 동시 처리를 보장하지 않으며 오직 슬럽캐시를 만들기 위해 처음 kmem_cache_node를 구성하기 위해서만 사용될 수 있다. (커널 메모리 캐시 시스템을 운영하기 위해 가장 처음에 만들 캐시는 kmem_cache_node 라는 이름이며 이 캐시를 통해 kmem_cache_node object를 공급해 주어야 한다.)

  • page = new_slab(kmem_cache_node, GFP_NOWAIT, node);
    • kmem_cache_node 구조체에 사용할 페이지를 할당받아 frozen된 slub 페이지로 구성한다.
      • slub 시스템이 아직 동작하지 않아 slub object로 할당을 받지 못하므로 버디 시스템으로 부터 페이지를 할당 받아 frozen된 slub 페이지로 구성한다.
  • if (page_to_nid(page) != node) {
    • 할당 받은 slub 페이지가 지정된 노드와 다른 경우 경고를 출력한다.
      • “SLUB: Unable to allocate memory from node %d\n”
      • “SLUB: Allocating a useless per node structure in order to be able to continue\n”
  • n = page->freelist; page->freelist = get_freepointer(kmem_cache_node, n);
    • n에 첫 object 주소를 대입하고, page->freelist에 다음 slub object 주소를 가리키게 한다.
  • page->inuse = 1;
    • 1개의 object가 사용 중 상태로 설정한다.
  • page->frozen = 0;
    • slub 페이지를 unfrozen 상태로 설정한다.
  • kmem_cache_node->node[node] = n;
    • per 노드에 할당받은 첫 object를 연결한다.

 

init_kmem_cache_node()

mm/slub.c

static void
init_kmem_cache_node(struct kmem_cache_node *n)
{
        n->nr_partial = 0;
        spin_lock_init(&n->list_lock);
        INIT_LIST_HEAD(&n->partial);
#ifdef CONFIG_SLUB_DEBUG
        atomic_long_set(&n->nr_slabs, 0);
        atomic_long_set(&n->total_objects, 0);
        INIT_LIST_HEAD(&n->full);
#endif
}

per 노드에 대한 초기화를 수행한다.

  • per 노드의 partial 리스트에 등록된 slub 페이지는 0으로 초기화
  • per 노드의 partial 리스트를 초기화

 

inc_slabs_node()
static inline void inc_slabs_node(struct kmem_cache *s, int node, int objects)
{
        struct kmem_cache_node *n = get_node(s, node);

        /*
         * May be called early in order to allocate a slab for the
         * kmem_cache_node structure. Solve the chicken-egg
         * dilemma by deferring the increment of the count during
         * bootstrap (see early_kmem_cache_node_alloc).
         */
        if (likely(n)) {
                atomic_long_inc(&n->nr_slabs);
                atomic_long_add(objects, &n->total_objects);
        }
}

CONFIG_SLUB_DEBUG 커널 옵션이 사용될 때에 동작하며 지정 노드에 대한 slab 수를 증가시킨다.

 

__add_partial()
/*
 * Management of partially allocated slabs.
 */
static inline void
__add_partial(struct kmem_cache_node *n, struct page *page, int tail)
{
        n->nr_partial++;
        if (tail == DEACTIVATE_TO_TAIL)
                list_add_tail(&page->lru, &n->partial);
        else
                list_add(&page->lru, &n->partial);
}

slub 페이지를 지정된 per 노드의 partial 리스트의 선두 또는 후미에 tail 옵션에 따라 방향을 결정하여 추가한다. 또한 해당 노드의 partial 수를 증가시킨다.

 

참고

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.