본문 바로가기
Kernel/Slab Allocators

[Linux Kernel] slab_common 분석

by hyeyoo 2021. 10. 10.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

이 글은 독자가 슬랩에 대한 약간의 이해가 있다고 가정한다. 슬랩을 왜 쓰는지, cache와 slab이 어떤 관계가 있는지, kmalloc/kfree가 무엇인지 정도는 알고 있어야 한다. 아래 두 글을 먼저 읽는 것도 이해에 도움이 될것같다.

 

[Linux Kernel] SL[AUO]B: Kernel memory allocator design and philosophy

내가 SLAB/SLUB을 잘못 이해했는지 Christoph Lameter 아저씨가 발표한 영상을 한 번 보라고 추천해주셨다. 이 글은 SL[AUO]B: Kernel memory allocator design and philosophy를 정리하고 내 의견을 추가로 적은..

hyeyoo.com

 

The Slab Allocator: An Object-Caching Kernel Memory Allocator

이 글은 1994년 Jeff Bonwick (Sun Microsystems)의 The Slab Allocator: An Object-Caching Kernel Memory Allocator를 정리한 것이다. 그나저나 이 논문 우리 큰누나랑 동갑이다. 논문이 나보다 누나잖아..? 1...

hyeyoo.com

슬랩 서브시스템은 원래 SLAB만 존재하다가 SLOB, SLUB이 만들어지면서 아예 독립적인 세 개의 슬랩 할당자가 만들어졌다. 하지만 각 슬랩 할당자는 비슷한 부분이 많기 때문에 이 부분을 2012년에 공통되는 코드를 slab_common으로 분리했다. 따라서 SLAB/SLUB/SLOB 할당자를 분석하기 전에 slab_common에 무슨 코드가 있는지 분석하겠다. 다만 memcg, kasan, kfence, tracing 같은 경우에는 다른 서브시스템에서 슬랩 코드를 수정한 것이라 이해에 방해가 되므로 이 글에선 설명을 생략하겠다. 앞으로 kasan*, kfence*, *memcg*, *trace 등등 이상한 이름의 함수가 나오면 일단은 무시하자. 슬랩을 이해하는 데에는 전혀 도움이 되지 않는다.

 

이제 분석을 시작해보자. slab_common은 mm/slab_common.c에 구현되어있다.

State of slab subsystem

슬랩 서브시스템은 항상 사용이 가능한 것은 아니다. 부팅 초반에 슬랩을 위한 자료구조들을 초기화하기 전까지는 사용이 불가능하다. 이처럼 슬랩은 다양한 상태로 나뉘는데, 이는 mm/slab.h에 enum으로 정의되어있다.

/*
 * 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 */
};

extern enum slab_state slab_state;

아직 모든 slab_state를 이해할 필요는 없다. 대강 DOWN은 슬랩 서브시스템의 초기화 이전이고 UP이 초기화 이후라고만 알아두자.

Data structures

슬랩은 struct kmem_cache, struct kmem_cache_node, struct kmem_cache_cpu, struct page 등의 구조체에 접근하는데, 이 부분은 Christoph의 PPT에 잘 나와있으니 아래 글을 참고하자. 이 글에선 이런 구조체를 구체적으로 다루지 않을 것인데, 왜냐하면 구조체들이 SL[AUO]B에 의존적이기 때문이다.

 

[Linux Kernel] SL[AUO]B: Kernel memory allocator design and philosophy

내가 SLAB/SLUB을 잘못 이해했는지 Christoph Lameter 아저씨가 발표한 영상을 한 번 보라고 추천해주셨다. 이 글은 SL[AUO]B: Kernel memory allocator design and philosophy를 정리하고 내 의견을 추가로 적은..

hyeyoo.com

Global variables

slab_common에서 선언하는 전역변수는 대략 아래와 같다.

enum slab_state slab_state;
LIST_HEAD(slab_caches);
DEFINE_MUTEX(slab_mutex);
struct kmem_cache *kmem_cache;

/*
 * Merge control. If this is set then no merging of slab caches will occur.
 */
static bool slab_nomerge = !IS_ENABLED(CONFIG_SLAB_MERGE_DEFAULT);

slab_state: 현재 슬랩의 상태

slab_caches: 시스템 내에 존재하는 모든 캐시를 링크드 리스트로 연결한것

slab_mutex: 슬랩에서 사용하는 매우 광범위한 락(뮤텍스)

kmem_cache: 아래에서 설명할 kmem_cache를 할당하기 위한 캐시

slab_nomerge: 뒤에서 설명하겠지만 슬랩에는 비슷한 캐시를 병합해서 하나로 쓰는 기능이 있는데 그걸 사용할지 말지

Slab's interface

슬랩에는 캐시의 생성, 소멸, 오브젝트의 할당, 해제를 위한 인터페이스가 존재한다. kmalloc/kfree를 제외하고는 SL[AUO]B에 의존적인 함수들이기 때문에, 작동 원리는 각각의 할당자를 분석할 때 정리하겠다.

kmem_cache_create, kmem_cache_destroy

슬랩 캐시를 생성하고 소멸하는 함수이다. 캐시를 생성해야 아래의 인터페이스로 할당과 해제를 할 수 있다.

kmem_cache_{alloc,free}, kmem_cache_alloc{_node,node_trace,trace}

kmem_cache_alloc과 kmem_cache_free는 슬랩에서 할당과 해제를 할 때 사용하는 인터페이스이다. SL[AUO]B마다 내부 구조가 다르므로 각각 구현되어있다.

 

alloc 뒤에 _node, _trace가 붙은 함수도 있는데, 커널 config에 따라 NUMA나 tracing이 켜져있을 때 사용하기 위함이다.

_node 관련 함수는 메모리의 접근 성능이 균일하지 않은 시스템에 대하여 특정 노드에서 할당받기 위해 존재한다. NUMA가 무엇인지는 아래 글에서 설명한다. tracing은 할당/해제를 추적하기 위해 사용되는데 이것도 다른 서브시스템이라 생략하겠다.

 

 

NUMA: Non-Uniform Memory Access

NUMA: Non-Uniform Memory Access 메모리 관련 부분을 공부하다보니 NUMA가 많이 나와서 정리해본다. 이 글은 Christoph Lameter의 2013년 문서 "NUMA: An Overview"를 리뷰한 것이다. NUMA는 멀티 프로세서 환경..

hyeyoo.com

kmalloc{,_array}, kfree{,_bulk}, kcalloc{,_node}, krealloc_array,  ... etc

kmalloc과 kfree도 변종 함수들이 되게 많다. 위에 있는 함수가 어떻게 다 다른지 설명하는 건 시간낭비이므로 생략한다. 다만 핵심은 특정 크기(sizeof(struct bio), sizeof(struct inode), ... etc)에 대한 할당을 할 때는 kmem_cache_{alloc,free}를, 임의의 크기에 대해 할당/해제할 때는 kmalloc, kfree를 사용한다는 특성만 알아두자.

Initialization on boot process

위에서 말했듯 슬랩이 사용 가능해지려면 부팅시에 초기화를 해야하는데, 이는 크게 kmem_cache_initkmem_cache_init_late 함수를 통해 이루어진다. 둘 모두 커널의 시작점인 start_kernel에서 직간접적으로 호출한다.

 

이름에서 유추할 수 있듯이 kmem_cache_init에서 필수적인 초기화를 우선 하고, kmem_cache_init_late는 부팅 프로세스의 나중에 필요한 부분을 더 초기화한다. kmem_cache_init, kmem_cache_init_late는 SL[AUO]B마다 각각 구현되어있으므로 이 글에서는 공통적인 부분만 설명하겠다. kmem_cache_init_late는 주로 SLAB에서 중요하므로 이것도 생략한다.

kmem_cache_init

초기화 과정에서는 크게 두 가지 작업을 한다. 하나는 kmem_cache를 할당하기 위한 캐시를 생성하는 것이다. 나중에 슬랩에서 캐시를 만들 때 캐시 디스크립터인 kmem_cache 자체도 할당을 해야하는데, 캐시를 만드는데 캐시가 필요하면 닭과 달걀 문제가 생기므로 kmem_cache를 위한 캐시를 부팅시에 만들어주는 것이다. 그리고 두번째 작업은 kmalloc에서 사용할 kmalloc_caches를 할당하는 것이다.

void __init kmem_cache_init(void)
{
		/* ... 생략 ... */

        create_boot_cache(kmem_cache, "kmem_cache",
                        offsetof(struct kmem_cache, node) +
                                nr_node_ids * sizeof(struct kmem_cache_node *),
                       SLAB_HWCACHE_ALIGN, 0, 0);
        
        /* ... 생략 ... */

        /* Now we can use the kmem_cache to allocate kmalloc slabs */
        setup_kmalloc_cache_index_table();
        create_kmalloc_caches(0);
}

kmem_cache_init은 create_boot_cache로 kmem_cache를 위한 캐시를 만들고, setup_kmalloc_cache_index_table로 할당 사이즈별 캐시 인덱스를 정한 뒤, create_kmalloc_caches로 kmalloc_caches를 초기화한다.

create_boot_cache

부팅시에 만드는 캐시는 create_boot_cache로 만들어진다. 위에서 말한 두 종류의 캐시 모두 부팅시에 만들어지므로 create_boot_cache를 분석해보자. create_boot_cache함수가 하는 일은 별로 없다. 적절한 align값을 계산하고 name, size, useroffset, usersize, align 등 kmem_cache의 공통적인 필드를 초기화해주고 나머지는 SL[AUO]B별 함수인 __kmem_cache_create에서 처리한다.

#ifndef CONFIG_SLOB
/* Create a cache during boot when no slab services are available yet */
void __init create_boot_cache(struct kmem_cache *s, const char *name,
		unsigned int size, slab_flags_t flags,
		unsigned int useroffset, unsigned int usersize)
{
	int err;
	unsigned int align = ARCH_KMALLOC_MINALIGN;

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

	/*
	 * For power of two sizes, guarantee natural alignment for kmalloc
	 * caches, regardless of SL*B debugging options.
	 */
	if (is_power_of_2(size))
		align = max(align, size);
	s->align = calculate_alignment(flags, align, size);

	s->useroffset = useroffset;
	s->usersize = usersize;

	err = __kmem_cache_create(s, flags);

	if (err)
		panic("Creation of kmalloc slab %s size=%u failed. Reason %d\n",
					name, size, err);

	s->refcount = -1;	/* Exempt from merging for now */
}

 

create_boot_cache 코드의 대부분은 직관적이지만, 직관적이지 않은 두 가지 부분이 있다. 바로 usercopy를 위한 필드와 부트 캐시에 대해서 슬랩 머징을 피하기 위해서 refcount를 음수로 초기화하는 것인데, 이 부분은 글의 마지막에 있는 usercopy와 슬랩 머징의 설명을 보면 이해가 될 것이다.

kmalloc_caches

보통 슬랩은 특정 크기(sizeof(struct bio), sizeof(struct inode), ...)의 오브젝트를 할당할때 사용한다. 이에 비해 kmalloc은 임의의 크기를 갖는 오브젝트를 할당하는 데에 사용된다. 임의의 크기가 정확히 몇인지는 알 수 없으므로, 8, 16, 32, 64, 128, ..., 2^n 등 다양한 크기의 캐시를 만들어둔 후에 적절한 캐시를 할당한다. 아, 참고로 SLOB은 kmalloc을 구현하지 않는다.

 

이때 kmalloc에서 생성할 가장 작은 캐시의 크기와 가장 큰 캐시의 크기는 SL[AOU]B, 아키텍처마다 다르다. 아래는 kmalloc의 할당 크기와 관련된 매크로이다.

/* Maximum allocatable size */
#define KMALLOC_MAX_SIZE	(1UL << KMALLOC_SHIFT_MAX)
/* Maximum size for which we actually use a slab cache */
#define KMALLOC_MAX_CACHE_SIZE	(1UL << KMALLOC_SHIFT_HIGH)
/* Maximum order allocatable via the slab allocator */
#define KMALLOC_MAX_ORDER	(KMALLOC_SHIFT_MAX - PAGE_SHIFT)

/*
 * Kmalloc subsystem.
 */
#ifndef KMALLOC_MIN_SIZE
#define KMALLOC_MIN_SIZE (1 << KMALLOC_SHIFT_LOW)
#endif

 

그리고 kmalloc은 다양한 용도로 사용될 수 있기 때문에 특정 크기에 대해서 용도별로 캐시를 따로 생성해주어야 한다. 이때 용도는 NORMAL (일반적인 경우), DMA (Direct Memory Access), CGROUP/MEMCG (cgroup 관련), RECLAIM (메모리 부족시 reclaimable한 kmalloc캐시) 등등 다양한 경우가 존재한다.

enum kmalloc_cache_type {
	KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
	KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG_KMEM
	KMALLOC_CGROUP = KMALLOC_NORMAL,
#else
	KMALLOC_CGROUP,
#endif
	KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
	KMALLOC_DMA,
#endif
	NR_KMALLOC_TYPES
};

 

따라서 kmalloc_caches는 타입별로, 크기별로 서로 다른 캐시를 2차원  배열의 형태로 저장한다.

extern struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];

create_kmalloc_caches

이 함수도 직관적이다. KMALLOC_NORMAL부터 KMALLOC_RECLAIM까지 타입별로, KMALLOC_SHIFT_LOW부터 KMALLOC_SHIFT_HIGH까지 크기별로 new_kmalloc_cache를 통해 캐시를 생성한다. 그 후 kmalloc까지 준비가 끝났으므로 slab_state를 UP로 바꾼다.

/*
 * Create the kmalloc array. Some of the regular kmalloc arrays
 * may already have been created because they were needed to
 * enable allocations for slab creation.
 */
void __init create_kmalloc_caches(slab_flags_t flags)
{
        int i;
        enum kmalloc_cache_type type;

        /*
         * Including KMALLOC_CGROUP if CONFIG_MEMCG_KMEM defined
         */
        for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
                for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
                        if (!kmalloc_caches[type][i])
                                new_kmalloc_cache(i, type, flags);

                        /*
                         * Caches that are not of the two-to-the-power-of size.
                         * These have to be created immediately after the
                         * earlier power of two caches
                         */
                        if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
                                        !kmalloc_caches[type][1])
                                new_kmalloc_cache(1, type, flags);
                        if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
                                        !kmalloc_caches[type][2])
                                new_kmalloc_cache(2, type, flags);
                }
        }

        /* Kmalloc array is now usable */
        slab_state = UP;

#ifdef CONFIG_ZONE_DMA
        for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
                struct kmem_cache *s = kmalloc_caches[KMALLOC_NORMAL][i];

                if (s) {
                        kmalloc_caches[KMALLOC_DMA][i] = create_kmalloc_cache(
                                kmalloc_info[i].name[KMALLOC_DMA],
                                kmalloc_info[i].size,
                                SLAB_CACHE_DMA | flags, 0,
                                kmalloc_info[i].size);
                }
        }
#endif
}

그런데 create_kmalloc_caches는 DMA에 대해서는 create_kmalloc_cache를, 그 외에 대해서는 create_kmalloc_cache를 호출한다. 왤까?

new_kmalloc_cache

new_kmalloc_cache에서도 캐시 자체는 create_kmalloc_cache에서 생성하는데, KMALLOC_{RECLAIM,CGROUP}에 대해서는 별도의 처리를 해주어야 하기 때문에 별도의 wrapper 함수를 만든 것이다.

static void __init
new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
        if (type == KMALLOC_RECLAIM) {
                flags |= SLAB_RECLAIM_ACCOUNT;
        } else if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_CGROUP)) {
                if (cgroup_memory_nokmem) {
                        kmalloc_caches[type][idx] = kmalloc_caches[KMALLOC_NORMAL][idx];
                        return;
                }
                flags |= SLAB_ACCOUNT;
        }

        kmalloc_caches[type][idx] = create_kmalloc_cache(
                                        kmalloc_info[idx].name[type],
                                        kmalloc_info[idx].size, flags, 0,
                                        kmalloc_info[idx].size);

        /*
         * If CONFIG_MEMCG_KMEM is enabled, disable cache merging for
         * KMALLOC_NORMAL caches.
         */
        if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_NORMAL))
                kmalloc_caches[type][idx]->refcount = -1;
}

create_kmalloc_cache

struct kmem_cache *__init create_kmalloc_cache(const char *name,
                unsigned int size, slab_flags_t flags,
                unsigned int useroffset, unsigned int usersize)
{
        struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);

        if (!s)
                panic("Out of memory when creating slab %s\n", name);

        create_boot_cache(s, name, size, flags, useroffset, usersize);
        kasan_cache_create_kmalloc(s);
        list_add(&s->list, &slab_caches);
        s->refcount = 1;
        return s;
}

create_kmalloc_cache도 로직이 매우 간단하다. kmem_cache를 할당받고, create_boot_cache로 캐시를 생성한 후에, slab_caches라는 슬랩 리스트에 삽입한 후에 refcount를 초기화한다.

kmalloc

이제 위에서 초기화한 kmalloc_caches로 kmalloc이 어떻게 동작하는지 살펴보자.

/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * for objects smaller than page size in the kernel.
 *
 * The allocated object address is aligned to at least ARCH_KMALLOC_MINALIGN
 * bytes. For @size of power of two bytes, the alignment is also guaranteed
 * to be at least to the size.
 * ... 생략 ...
 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
		unsigned int index;
#endif
		if (size > KMALLOC_MAX_CACHE_SIZE)
			return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
		index = kmalloc_index(size);

		if (!index)
			return ZERO_SIZE_PTR;

		return kmem_cache_alloc_trace(
				kmalloc_caches[kmalloc_type(flags)][index],
				flags, size);
#endif
	}
	return __kmalloc(size, flags);
}

 

kmalloc은 두 단계로 이루어진다. (1) 사이즈에 맞는 kmem_cache의 인덱스를 찾는다 (2) 해당 kmem_cache에서 할당한다.

 

그런데 size가 상수인 경우에는 kmem_cache의 인덱스를 런타임에 계산할 필요가 없으므로 약간의 최적화가 가능하다. 따라서 kmalloc에서는 __builtin_constant_p라는 GCC의 내장 함수로 size가 상수인지 확인한다. 상수라면, kmalloc_index라는 인라인 함수로 인덱스를 구한다. 상수가 아니라면 __kmalloc을 호출해서 런타임에 인덱스를 계산한다.

 

그리고 할당 사이즈가 KMALLOC_MAX_CACHE_SIZE보다 크면 kmalloc_large로 할당하는데, 이 함수는 페이지 크기보다 오브젝트의 크기가 클때, 슬랩 관련 구조체를 관리하는 오버헤드를 줄이기 위해 버디할당자로부터 메모리를 할당받는다.

create_cache

create_cache는 초기화 과정 이후에 생성되는 캐시를 만들 때 호출된다. create_boot_cache와의 차이점은 create_cache에서는 kmem_cache를 할당하는 캐시를 사용할 수 있다는 점이다. 그리고 create_cache로 생성되는 캐시는 slab_caches 링크드 리스트에 추가된다.

static struct kmem_cache *create_cache(const char *name,
		unsigned int object_size, unsigned int align,
		slab_flags_t flags, unsigned int useroffset,
		unsigned int usersize, void (*ctor)(void *),
		struct kmem_cache *root_cache)
{
	struct kmem_cache *s;
	int err;

	if (WARN_ON(useroffset + usersize > object_size))
		useroffset = usersize = 0;

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

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

	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:
	kmem_cache_free(kmem_cache, s);
	goto out;
}

shutdown_cache

shutdown_cache는 slab_caches 리스트 상에서 캐시를 제거하고, debugfs와 sysfs의 등록을 해제한다. (등록은 __kmem_cache_create에서 한다.) SL[AUO]B에 의존적인 코드는 __kmem_cache_shutdown에서 처리한다.

static int shutdown_cache(struct kmem_cache *s)
{
        /* free asan quarantined objects */
        kasan_cache_shutdown(s);

        if (__kmem_cache_shutdown(s) != 0)
                return -EBUSY;

        list_del(&s->list);

        if (s->flags & SLAB_TYPESAFE_BY_RCU) {
#ifdef SLAB_SUPPORTS_SYSFS
                sysfs_slab_unlink(s);
#endif
                list_add_tail(&s->list, &slab_caches_to_rcu_destroy);
                schedule_work(&slab_caches_to_rcu_destroy_work);
        } else {
                kfence_shutdown_cache(s);
                debugfs_slab_release(s);
#ifdef SLAB_SUPPORTS_SYSFS
                sysfs_slab_unlink(s);
                sysfs_slab_release(s);
#else
                slab_kmem_cache_release(s);
#endif
        }

        return 0;
}

What is slab merging?

슬랩 머징은 크기가 비슷한 캐시를 합치는 것이다. 사실 합친다기 보단, kmem_cache_create로 캐시를 만들 때 이미 비슷한 캐시가 존재하면 새로운 캐시를 만들지 않고 기존에 존재하는 캐시를 리턴한다. 부트 파라미터 slab_nomerge로 런타임에 활성화/비활성화할 수 있다.

 

슬랩 머징을 사용하면 다양한 서브시스템에 걸쳐서 캐시를 공유하기 때문에 하드웨어 캐시를 더 효율적으로 사용할 수 있다. 그리고 메타데이터를 덜 사용하기 때문에 메모리도 효율적으로 쓸 수 있다.

 

근데 모든 캐시를 합칠 수 있는 건 아니라, 합칠 수 있는 캐시를 찾아서 합쳐야한다.

slab_unmergable

/*
 * Set of flags that will prevent slab merging
 */
#define SLAB_NEVER_MERGE (SLAB_RED_ZONE | SLAB_POISON | SLAB_STORE_USER | \
                SLAB_TRACE | SLAB_TYPESAFE_BY_RCU | SLAB_NOLEAKTRACE | \
                SLAB_FAILSLAB | kasan_never_merge())

 

SLAB_NEVER_MERGE는 슬랩 캐시의 플래그가 머지가 가능한 종류인지 아닌지를 나타낸다.

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

        if (s->ctor)
                return 1;

        if (s->usersize)
                return 1;

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

        return 0;
}

조건을 보면 아래의 조건 중 하나가 충족될 때 머지를 하지 않는다.

(1) no_merge로 시스템 전체에서 비활성화한경우

(2) 캐시 생성 플래그의 특성상 머지가 불가능한 경우

(3) 생성자가 존재하는 경우

(4) usersize가 설정된 경우

(5) 부트 캐시처럼 의도적으로 refcount를 조정해서 합치지 못하게 한 경우

find_mergable

이 함수는 slab_caches를 순회하면서 머지가 가능한 캐시가 있는지 찾는다.

#define SLAB_MERGE_SAME (SLAB_RECLAIM_ACCOUNT | SLAB_CACHE_DMA | \
                         SLAB_CACHE_DMA32 | SLAB_ACCOUNT)

SLAB_MERGE_SAME은 두 슬랩 캐시의 성질이 같은지 확인할 때 사용된다. 예를 들어 DMA 캐시는 DMA 캐시끼리, RECLAIMABLE한 캐시는 RECLAIMABLE한 캐시끼리 머지해야 하므로, 두 캐시에 대해서 s->flags & SLAB_MERGE_SAME이 같아야 한다.

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

        if (slab_nomerge)
                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);

        if (flags & SLAB_NEVER_MERGE)
                return 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;
}

find_mergeable도 크게 다르지 않은데, 다음의 조건이 모두 맞는 캐시를 찾아서 리턴한다.

(1) 생성하려는 오브젝트의 크기(정렬 포함)가 기존 캐시의 오브젝트의 크기보다 작거나 같다.

(2) slab_unmergeable가 0을 리턴해야 한다.

(3) 두 캐시의 align이 호환되어야 한다.

(4) 두 캐시의 오브젝트 크기 차이가 sizeof(void *) 작다.

(5) 두 캐시가 SLAB_MERGE_SAME을 &한 결과가 같다

(6) SLAB인 경우, align이 존재해야하며 기존에 존재하는 캐시의 align이 생성하려는 캐시의 align에 대한 배수여야한다.

What is Usercopy in slab?

슬랩에서 usercopy는 2017-8년 즈음에 보안 기능이 강화되면서 추가된 것이다. 아래 LWN.net 글에서 잘 설명해준다. 요약하자면 시스템 호출을 통해서 슬랩으로 할당한 객체의 일부 필드를 커널 공간에서 사용자 공간으로 copy_to_user 함수를 통해 복사할 때가 있다. 보통 객체 전체를 복사해서 주는 게 아니라 일부 필드만을 복사한다.

 

그런데 취약점으로 인해 원래 복사하려는 것보다 더 많은 양을 복사하면 사용자 공간으로 보안에 민감한 데이터를 유출할 가능성이 있다.  따라서 usercopy가 가능한 영역을 whitelist로 지정해서 지정한 영역만 복사가 가능하게 만드는 것이다. 이를 위해서 kmem_cache에 useroffset과 usersize로 오브젝트 내에서 사용자 공간으로 복사가 가능한 영역을 명시한다.

 

Hardened usercopy whitelisting [LWN.net]

Did you know...?LWN.net is a subscriber-supported publication; we rely on subscribers to keep the entire operation going. Please help out by buying a subscription and keeping LWN on the net. By Jonathan Corbet July 7, 2017 There are many ways to attempt to

lwn.net

What is RCU in slab?

슬랩 코드에 RCU를 사용하는 부분(SLAB_TYPESAFE_BY_RCU, slab_caches_rcu_destroy{,work,work_fn})이 조금 있는데, 아쉽게도 내가 아직 RCU(Read Copy Update)가 뭔지 모른다. RCU를 모르는데 슬랩에서 RCU가 어떻게 쓰이는지 알 수는 없으므로 RCU를 먼저 정리한 후에 글을 업데이트해야겠다.

 

The End

이번 글에선 slab_common에 무엇이 있는지 알아보았다. 모든 함수를 다루지는 않았지만 중요한 건 다 다뤘다. 다음에는 SLUB, SLAB, SLOB 순서대로 분석을 해보려고 한다. (RCU도...)

댓글