debug_objects_early_init()

<kernel v5.0>

Debug Objects

커널에서 사용하는 객체를 트래킹하기 위해 별도로 Debug Object를 할당하고 상태 값을 기록해두어 life time을 트래킹할 수 있도록 한다.

 

객체를 커널에서 할당하여 사용 시 종종 다음과 같은 실수를 반복한다.

  • 사용중인 객체의 할당 해제
  • 사용중인 객체의 재초기화

 

Debug Object를 사용하면 다음을 수행할 때마다 트래킹한다.

  • 객체 초기화
  • 객체 추가
  • 객체 삭제

 

참고: infrastructure to debug (dynamic) objects

 

특징

  • Debug Object 코드를 사용하려면 다음 커널 옵션을 설정해야 한다.
    • CONFIG_DEBUG_KERNEL
    • CONFIG_DEBUG_OBJECTS
  • Debug Object 기능을 enable 하기 위해서는 부트업 타임에 다음을 준비하여야 한다.
    • CONFIG_DEBUG_OBJECTS_ENABLE_DEFAULT 커널 옵션을 사용하거나
    • “debug_objects=1” 커널 파라메터를 사용한다.
  • 문제가 발생하여 로그를 출력할 때 최대 5번까지로 제한한다.
  • /sys/kernel/debug/debug_objects 디렉토리에서 트래킹을 사용한다.
    • 예) cat stats

 

적용된 커널 소스

  • 타이머
    • CONFIG_DEBUG_OBJECTS_TIMERS 커널 옵션 필요
  • 워크큐
    • CONFIG_DEBUG_OBJECTS_WORK 커널 옵션 필요
  • RCU
    • CONFIG_DEBUG_OBJECTS_RCU_HEAD 커널 옵션 필요
  • per-cpu 카운터
    • DEBUG_OBJECTS_PERCPU_COUNTER 커널 옵션 필요
  • kfree() & vfree()
    • CONFIG_DEBUG_OBJECTS_FREE 옵션 필요
    • 오브젝트의 deactivation 과정을 감시할 수 있다. (leak 감시)

 

Debug Object 상태 전환

다음은 Debug Object의 상태들이다.

  • none
    • 객체를 할당 받지 않은 상태이다.
    • Debug Object가 처음 할당되어 객체 풀에서 대기한다.
  • init
    • 객체를 할당 받은 초기 상태이다.
    • Debug Object는 해시 리스트에서 관리된다.
  • active
    • 객체에 접근 가능한 상태이다.
    • Debug Object는 해시 리스트에서 관리된다.
  • inactive
    • 객체에 접근을 허용하지 않은 상태이다.
    • Debug Object는 해시 리스트에서 관리된다.
  • destroyed
    • 객체가 파괴된 상태이다.
    • Debug Object는 해시 리스트에서 관리된다.
  • notavailable
    • 객체가 할당 해제된 상태이다.
    • Debug Object는 재사용을 위해 객체 풀에서 대기한다.

 

Debug Object의 상태가 바뀌는 과정을 보여준다.

 

Debug Object 초기화

Debug Object를 사용하기 위해 커널은 다음과 같아 두 단계에 걸쳐 초기화를 수행된다.

  • 부트업 초반에 debug_objects_early_init()를 통해 early 초기화
    • 처음 슬랩을 사용하는 kmem 캐시를 사용하기 전까지 임시로 사용할 static debug object를 사용한다.
  • 부트업 후반에 debug_objects_mem_init()을 통해 정규 초기화
    • 슬랩을 사용하는 kmem 캐시가 준비된 후 기존 static debug object를 모두 kmem 캐시에서 할당한 객체로 migration 한다.

 

다음 그림은 debug object의 2 단계 초기화를 보여준다.

 

Debug Object 주요 API

Debug Object의 주요 API는 다음과 같다.

  • debug_object_init()
  • debug_object_init_on_stack()
  • debug_object_activate()
  • debug_object_deactivate()
  • debug_object_destroy()
  • debug_object_free()

 

Debug Object Life-time

Debug object의 할당은 아래 두 함수에서 요청되고 Debug Pool에서 할당해준다. Debug Pool의 Debug Object가 최소 레벨(default: 256) 보다 부족해지는 경우 kem 캐시를 통해 refill 한다.

  • debug_object_init()
  • debug_object_init_on_stack()

Debug Object의 할당 해제는 아래 함수에서 요청되고 Debug Pool로 이동시킨다. 만일 Debug Pool의 Debug Object가 pool size(defautl: 1024)를 초과하는 경우 할당 해제하여 kmem 캐시로 돌려보낸다.

  • debug_object_free()

 

초기화

debug_objects_early_init()

lib/debugobjects.c

/*
 * Called during early boot to initialize the hash buckets and link
 * the static object pool objects into the poll list. After this call
 * the object tracker is fully operational.
 */
void __init debug_objects_early_init(void)
{
        int i;

        for (i = 0; i < ODEBUG_HASH_SIZE; i++) 
                raw_spin_lock_init(&obj_hash[i].lock);

        for (i = 0; i < ODEBUG_POOL_SIZE; i++) 
                hlist_add_head(&obj_static_pool[i].node, &obj_pool);
}

 

kmem 캐시가 준비가되기 직전의 커널 부트업에서 사용할 Debug Object 사용을 위해 초기화를 수행한다. 커널 빌드 시 준비해 둔 1024개의 static debug object를 임시로 사용한다.

  • 코드 라인 5~6에서 Debug Object 해시를 초기화한다.
    • ODEBUG_HASH_SIZE=16K
  • 코드 라인 8~9에서 Debug Object 풀을 초기화한다.
    •  ODEBUG_POOL_SIZE=1024
      • s390 아키텍처를 위해 512 -> 1024로 증가시켰다.

 

 

debug_objects_mem_init()

lib/debugobjects.c

/*
 * Called after the kmem_caches are functional to setup a dedicated
 * cache pool, which has the SLAB_DEBUG_OBJECTS flag set. This flag
 * prevents that the debug code is called on kmem_cache_free() for the
 * debug tracker objects to avoid recursive calls.
 */
void __init debug_objects_mem_init(void)
{
        if (!debug_objects_enabled)
                return;

        obj_cache = kmem_cache_create("debug_objects_cache",
                                      sizeof (struct debug_obj), 0,
                                      SLAB_DEBUG_OBJECTS | SLAB_NOLEAKTRACE,
                                      NULL);

        if (!obj_cache || debug_objects_replace_static_objects()) {
                debug_objects_enabled = 0;
                kmem_cache_destroy(obj_cache);
                pr_warn("out of memory.\n");
        } else
                debug_objects_selftest();

        /*
         * Increase the thresholds for allocating and freeing objects
         * according to the number of possible CPUs available in the system.
         */
        debug_objects_pool_size += num_possible_cpus() * 32;
        debug_objects_pool_min_level += num_possible_cpus() * 4;
}

Debug Object 기능이 enable된 경우 사용을 위해 kmem 캐시등을 준비하고 기존 static debug object를 keme 캐시에서 할당받은 debug object로 migration 한다. 그 후 사용할 pool 사이즈와 최소 개수를 cpu 수에 맞게 적절히 조절한다.

  • 코드 라인 3~4에서 Debug Object 기능을 enable 하지 않은 경우 함수를 빠져나간다.
  • 코드 라인 6~9에서 debug_obj 구조체를 위해 kmem 캐시를 준비하고
  • 코드 라인 11~16에서 기존 static debug object를 keme 캐시에서 할당받은 debug object로 migration 한다.
  • 코드 라인 22에서 pool size를 possible cpu 개수 * 32 만큼 증가시킨다.
  • 코드 라인 23에서 pool 최소 레벨도 poosible cpu 개수 * 4 만큼 증가시킨다.

 

주요 함수들

debug_object_init()

lib/debugobjects.c

/**
 * debug_object_init - debug checks when an object is initialized
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_init(void *addr, struct debug_obj_descr *descr)
{
        if (!debug_objects_enabled)
                return;

        __debug_object_init(addr, descr, 0);
}
EXPORT_SYMBOL_GPL(debug_object_init);

객체를 초기화 시 디버그 체크를 수행한다.

 

/**
 * debug_object_init_on_stack - debug checks when an object on stack is
 *                              initialized
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_init_on_stack(void *addr, struct debug_obj_descr *descr)
{
        if (!debug_objects_enabled)
                return;

        __debug_object_init(addr, descr, 1);
}
EXPORT_SYMBOL_GPL(debug_object_init_on_stack);

스택위에 있는 객체를 초기화 시 디버그 체크를 수행한다.

  • 기존 상태가 active 및 destroyed에서 진입한 경우 에러 메시지를 출력한다.

 

__debug_object_init()

lib/debugobjects.c

static void
__debug_object_init(void *addr, struct debug_obj_descr *descr, int onstack)
{
        enum debug_obj_state state;
        struct debug_bucket *db;
        struct debug_obj *obj;
        unsigned long flags;

        fill_pool();

        db = get_bucket((unsigned long) addr);

        raw_spin_lock_irqsave(&db->lock, flags);

        obj = lookup_object(addr, db);
        if (!obj) {
                obj = alloc_object(addr, db, descr);
                if (!obj) {
                        debug_objects_enabled = 0;
                        raw_spin_unlock_irqrestore(&db->lock, flags);
                        debug_objects_oom();
                        return;
                }
                debug_object_is_on_stack(addr, onstack);
        }

        switch (obj->state) {
        case ODEBUG_STATE_NONE:
        case ODEBUG_STATE_INIT:
        case ODEBUG_STATE_INACTIVE:
                obj->state = ODEBUG_STATE_INIT;
                break;

        case ODEBUG_STATE_ACTIVE:
                debug_print_object(obj, "init");
                state = obj->state;
                raw_spin_unlock_irqrestore(&db->lock, flags);
                debug_object_fixup(descr->fixup_init, addr, state);
                return;

        case ODEBUG_STATE_DESTROYED:
                debug_print_object(obj, "init");
                break;
        default:
                break;
        }

        raw_spin_unlock_irqrestore(&db->lock, flags);
}

객체를 초기화 시 디버그 체크를 수행한다.

  • 코드 라인 8에서 object pool에 준비되어 있는 오브젝트가 min level 이하인 경우 추가 할당을 해둔다.
  • 코드 라인 10에서 객체 주소의 pfn을 이용한 해시를 통해 debug bucket을 가져온다.
  • 코드 라인 12~24에서 debug bucket에 락을 걸고 객체 주소에 해당하는 Debug Object를 검색한다. 만일 존재하지 않는 경우 Debug Object를 할당한다.
  • 코드 라인 26~45에서 Debug Object의 상태가 active나 destroyed이면 문제가 발생하였으므로 에러 메시지를 출력하고, active 상태인 경우는 추가 fixup 코드를 수행한다.
    • 예) ODEBUG: assert_init not available (active state 0) object type: timer_list hint: stub_timer+0x0/0x20

 

다음 그림은 debug_object_init()을 수행할 때 debug_object의 이동 또는 추가 할당되는 모습을 보여준다.

 

debug_object_activate()

lib/debugobjects.c

/**
 * debug_object_activate - debug checks when an object is activated
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 * Returns 0 for success, -EINVAL for check failed.
 */
int debug_object_activate(void *addr, struct debug_obj_descr *descr)
{
        enum debug_obj_state state;
        struct debug_bucket *db;
        struct debug_obj *obj;
        unsigned long flags;
        int ret;
        struct debug_obj o = { .object = addr,
                               .state = ODEBUG_STATE_NOTAVAILABLE,
                               .descr = descr };

        if (!debug_objects_enabled)
                return 0;

        db = get_bucket((unsigned long) addr);

        raw_spin_lock_irqsave(&db->lock, flags);

        obj = lookup_object(addr, db);
        if (obj) {
                switch (obj->state) {
                case ODEBUG_STATE_INIT:
                case ODEBUG_STATE_INACTIVE:
                        obj->state = ODEBUG_STATE_ACTIVE;
                        ret = 0;
                        break;

                case ODEBUG_STATE_ACTIVE:
                        debug_print_object(obj, "activate");
                        state = obj->state;
                        raw_spin_unlock_irqrestore(&db->lock, flags);
                        ret = debug_object_fixup(descr->fixup_activate, addr, state);
                        return ret ? 0 : -EINVAL;

                case ODEBUG_STATE_DESTROYED:
                        debug_print_object(obj, "activate");
                        ret = -EINVAL;
                        break;
                default:
                        ret = 0;
                        break;
                }
                raw_spin_unlock_irqrestore(&db->lock, flags);
                return ret;
        }

        raw_spin_unlock_irqrestore(&db->lock, flags);
        /*
         * We are here when a static object is activated. We
         * let the type specific code confirm whether this is
         * true or not. if true, we just make sure that the
         * static object is tracked in the object tracker. If
         * not, this must be a bug, so we try to fix it up.
         */
        if (descr->is_static_object && descr->is_static_object(addr)) {
                /* track this static object */
                debug_object_init(addr, descr);
                debug_object_activate(addr, descr);
        } else {
                debug_print_object(&o, "activate");
                ret = debug_object_fixup(descr->fixup_activate, addr,
                                        ODEBUG_STATE_NOTAVAILABLE);
                return ret ? 0 : -EINVAL;
        }
        return 0;
}
EXPORT_SYMBOL_GPL(debug_object_activate);

객체를 활성화 시 디버그 체크를 수행한다.

  • 코드 라인 12~13에서 Debug Object 기능을 enable 하지 않은 경우 함수를 빠져나간다.
  • 코드 라인 15에서 객체 주소의 pfn을 이용한 해시를 통해 debug bucket을 가져온다.
  • 코드 라인 17~19에서 debug bucket에 락을 걸고 객체 주소에 해당하는 Debug Object를 검색한다.
  • 코드 라인 20~45에서 Debug Object의 상태가 이미 activate 이거나 destroyed 이면 문제가 발생하였으므로 에러 메시지를 출력하고, active 상태인 경우는 추가 fixup 코드를 수행한다.
  • 코드 라인 55~64에서 Debug Object 검색이 안된 경우이다. Debug Object의 디스크립터가 static 객체인경우에만 Debug Object를 새로 초기화하고 activate상태로 변경하여 static 객체를 트래킹하게 한다. 그렇지 않고 dynamic 객체인 경우 에러 메시지를 출력하고 fixup 코드를 수행한다.

 

 

 

debug_object_deactivate()

lib/debugobjects.c

/**
 * debug_object_deactivate - debug checks when an object is deactivated
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_deactivate(void *addr, struct debug_obj_descr *descr)
{
        struct debug_bucket *db;
        struct debug_obj *obj;
        unsigned long flags;

        if (!debug_objects_enabled)
                return;

        db = get_bucket((unsigned long) addr);

        raw_spin_lock_irqsave(&db->lock, flags);

        obj = lookup_object(addr, db);
        if (obj) {
                switch (obj->state) {
                case ODEBUG_STATE_INIT:
                case ODEBUG_STATE_INACTIVE:
                case ODEBUG_STATE_ACTIVE:
                        if (!obj->astate)
                                obj->state = ODEBUG_STATE_INACTIVE;
                        else
                                debug_print_object(obj, "deactivate");
                        break;

                case ODEBUG_STATE_DESTROYED:
                        debug_print_object(obj, "deactivate");
                        break;
                default:
                        break;
                }
        } else {
                struct debug_obj o = { .object = addr,
                                       .state = ODEBUG_STATE_NOTAVAILABLE,
                                       .descr = descr };

                debug_print_object(&o, "deactivate");
        }

        raw_spin_unlock_irqrestore(&db->lock, flags);
}
EXPORT_SYMBOL_GPL(debug_object_deactivate);

객체를 비활성화 시 디버그 체크를 수행한다.

  • 코드 라인 7~8에서 Debug Object 기능을 enable 하지 않은 경우 함수를 빠져나간다.
  • 코드 라인 10에서 객체 주소의 pfn을 이용한 해시를 통해 debug bucket을 가져온다.
  • 코드 라인 12~14에서 debug bucket에 락을 걸고 객체 주소에 해당하는 Debug Object를 검색한다.
  • 코드 라인 15~31에서 Debug Object의 상태가 실제 activate 된 적이 없는 모든 경우에 대해 문제가 발생하였으므로 에러 메시지를 출력한다.
  • 코드 라인 32~38에서 Debug Object 검색이 안된 경우이다. 이 경우에도 에러 메시지를 출력한다.

 

debug_object_destroy()

lib/debugobjects.c

/**
 * debug_object_destroy - debug checks when an object is destroyed
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_destroy(void *addr, struct debug_obj_descr *descr)
{
        enum debug_obj_state state;
        struct debug_bucket *db;
        struct debug_obj *obj;
        unsigned long flags;

        if (!debug_objects_enabled)
                return;

        db = get_bucket((unsigned long) addr);

        raw_spin_lock_irqsave(&db->lock, flags);

        obj = lookup_object(addr, db);
        if (!obj)
                goto out_unlock;

        switch (obj->state) {
        case ODEBUG_STATE_NONE:
        case ODEBUG_STATE_INIT:
        case ODEBUG_STATE_INACTIVE:
                obj->state = ODEBUG_STATE_DESTROYED;
                break;
        case ODEBUG_STATE_ACTIVE:
                debug_print_object(obj, "destroy");
                state = obj->state;
                raw_spin_unlock_irqrestore(&db->lock, flags);
                debug_object_fixup(descr->fixup_destroy, addr, state);
                return;

        case ODEBUG_STATE_DESTROYED:
                debug_print_object(obj, "destroy");
                break;
        default:
                break;
        }
out_unlock:
        raw_spin_unlock_irqrestore(&db->lock, flags);
}
EXPORT_SYMBOL_GPL(debug_object_destroy);

객체를 소멸 시 디버그 체크를 수행한다.

  • 코드 라인 8~9에서 Debug Object 기능을 enable 하지 않은 경우 함수를 빠져나간다.
  • 코드 라인 11에서 객체 주소의 pfn을 이용한 해시를 통해 debug bucket을 가져온다.
  • 코드 라인 13~17에서 debug bucket에 락을 걸고 객체 주소에 해당하는 Debug Object를 검색한다.
  • 코드 라인 19~37에서 Debug Object의 상태가 active 또는 destroyed 상태인 경우 문제가 발생하였으므로 에러 메시지를 출력하고, active 상태인 경우 fixup 코드도 수행한다.

 

debug_object_free()

lib/debugobjects.c

/**
 * debug_object_free - debug checks when an object is freed
 * @addr:       address of the object
 * @descr:      pointer to an object specific debug description structure
 */
void debug_object_free(void *addr, struct debug_obj_descr *descr)
{
        enum debug_obj_state state;
        struct debug_bucket *db;
        struct debug_obj *obj;
        unsigned long flags;

        if (!debug_objects_enabled)
                return;

        db = get_bucket((unsigned long) addr);

        raw_spin_lock_irqsave(&db->lock, flags);

        obj = lookup_object(addr, db);
        if (!obj)
                goto out_unlock;

        switch (obj->state) {
        case ODEBUG_STATE_ACTIVE:
                debug_print_object(obj, "free");
                state = obj->state;
                raw_spin_unlock_irqrestore(&db->lock, flags);
                debug_object_fixup(descr->fixup_free, addr, state);
                return;
        default:
                hlist_del(&obj->node);
                raw_spin_unlock_irqrestore(&db->lock, flags);
                free_object(obj);
                return;
        }
out_unlock:
        raw_spin_unlock_irqrestore(&db->lock, flags);
}
EXPORT_SYMBOL_GPL(debug_object_free);

객체의 할당 해제 시 디버그 체크를 수행한다.

  • 코드 라인 8~9에서 Debug Object 기능을 enable 하지 않은 경우 함수를 빠져나간다.
  • 코드 라인 11에서 객체 주소의 pfn을 이용한 해시를 통해 debug bucket을 가져온다.
  • 코드 라인 13~17에서 debug bucket에 락을 걸고 객체 주소에 해당하는 Debug Object를 검색한다.
  • 코드 라인 19~37에서 Debug Object의 상태가 active 상태인 경우 문제가 발생하였으므로 에러 메시지를 출력하고, fixup 코드도 수행한다.

 

다음 그림은 debug_object_free() 함수 호출 시 Debug object가 할당 해제되어 객체 풀로 되돌아가거나 kmem 캐시로 회수되는 모습을 보여준다.

 

Fixup Operations

아래 API들을 사용할 때 이미 Debug Object가 active 상태인 경우 에러가 발생하고, 디스크립터에 구현된 fixup 후크 함수를 호출한다.

  • debug_object_init() -> active 상태를 만나면 fixup 후크 함수를 호출한다.
  • debug_object_activate() -> active 상태를 만나면 fixup 후크 함수를 호출한다.
  • debug_object_deactivate() -> 없음
  • debug_object_destroy() -> active 상태를 만나면 fixup 후크 함수를 호출한다.
  • debug_object_free() -> active 상태를 만나면 fixup 후크 함수를 호출한다.

 

다음 그림은 Debug Object를 deactivate 상태 변환 없이 destroy 상태로 변경할 때 fixup 후크 함수가 호출되는 모습을 보여준다.

 

구조체

debug_object 구조체

include/linux/debugobjects.h

/**
 * struct debug_obj - representaion of an tracked object
 * @node:       hlist node to link the object into the tracker list
 * @state:      tracked object state
 * @astate:     current active state
 * @object:     pointer to the real object
 * @descr:      pointer to an object type specific debug description structure
 */
struct debug_obj {
        struct hlist_node       node;
        enum debug_obj_state    state;
        unsigned int            astate;
        void                    *object;
        struct debug_obj_descr  *descr;
};
  • node
    • 객체 pool이나 해시리스트에 연결될 때 사용되는 노드이다.
  • state
    • 트래킹할 객체 상태 값이다.
  • astate
    • 현재 active 상태 값을 나타낸다. (초기화 시 0)
    • debug_object_active_state() API를 통해서 설정되며, 현재는 특정 gpu 드라이버에서만 사용되고 있다.
  • object
    • 실제 객체 주소
  • descr
    • debug object descriptor를 가리킨다.

 

debug_obj_descr 구조체

include/linux/debugobjects.h

/**
 * struct debug_obj_descr - object type specific debug description structure
 *
 * @name:               name of the object typee
 * @debug_hint:         function returning address, which have associated
 *                      kernel symbol, to allow identify the object
 * @is_static_object:   return true if the obj is static, otherwise return false
 * @fixup_init:         fixup function, which is called when the init check
 *                      fails. All fixup functions must return true if fixup
 *                      was successful, otherwise return false
 * @fixup_activate:     fixup function, which is called when the activate check
 *                      fails
 * @fixup_destroy:      fixup function, which is called when the destroy check
 *                      fails
 * @fixup_free:         fixup function, which is called when the free check
 *                      fails
 * @fixup_assert_init:  fixup function, which is called when the assert_init
 *                      check fails
 */
struct debug_obj_descr {
        const char              *name;
        void *(*debug_hint)(void *addr);
        bool (*is_static_object)(void *addr);
        bool (*fixup_init)(void *addr, enum debug_obj_state state);
        bool (*fixup_activate)(void *addr, enum debug_obj_state state);
        bool (*fixup_destroy)(void *addr, enum debug_obj_state state);
        bool (*fixup_free)(void *addr, enum debug_obj_state state);
        bool (*fixup_assert_init)(void *addr, enum debug_obj_state state);
};

Debug Object 디스크립터에는 이름과 몇 개의 fixup용 후크 함수들이 지정된다.

  • *name
    • 디스크립터를 설명하고 보여줄 수 있는 이름
  • (*is_static_object)
    • 객체가 static 상태인지 여부를 판단할 수 있는 후크 함수와 연결된다.
      • 예) timer_is_static_object(), work_is_static_object(), rcuhead_is_static_object()
  • (*debug_hint)
    • 에러 메시지를 출력할 때 사용 위치를 보여주는 기능이다.
    • 사용 위치가 커널에 심볼로 export 되어있는 함수의 경우 다음 사용예와 같이 함수 명과 함수에서의 Debug Object를 사용한 상대 위치가 표시된다.
      • 예) ODEBUG: free active (active state 0) object type: timer_list hint: process_timeout+0x0/0x10

 

debug_bucket 구조체

dll/debugobjects.c

struct debug_bucket {
        struct hlist_head       list;
        raw_spinlock_t          lock;
};

static struct debug_bucket      obj_hash[ODEBUG_HASH_SIZE];
  • list
    • 사용 중 상태의 Debug Object가 연결될 리스트이다.
  • lock
    • 위의 list 추가 삭제 시 필요한 lock이다.

 

obj_hash[] 배열

dll/debugobjects.c

static struct debug_bucket      obj_hash[ODEBUG_HASH_SIZE];

사용 중 상태의 Debug Object가 모여 있는 해시리스트이다.

 

obj_static_pool[] 배열

dll/debugobjects.c

static struct debug_obj         obj_static_pool[ODEBUG_POOL_SIZE] __initdata;

부트 업 과정 중 kmem 캐시가 활성화 되기 전까지 Debug Object를 사용해야 하는 경우 static pool을 활용한다.

  • 부트업이 완료되면 사용하지 않는다.

 

참고

smp_setup_processor_id()

<kernel v5.10>

리눅스는 물리 cpu 번호(id)를 사용하지 않고 로지컬 cpu 번호(id)를 사용하여 관리한다. 현재 부트된 물리 cpu를 로지컬 cpu id 0번으로 배치하여 사용한다.

  • DTB를 사용하는 경우 setup_arch() -> arm_dt_init_cpu_maps() 함수에서 로지컬 cpu id가 재조정된다.

 

NR_CPUS

  • 가능한 CPU의 최대 개수로 configuration 할 때 static하게 정해지는 값.
    • nr_cpu_ids
      • 런타임에 사용하는 cpu의 수
      • 런타임에 설정되기 전까지는 NR_CPUS 값과 동일하다.
  • 커널 버전에 따라 범위와 default 값이 약간씩 다르다.
  • 커널 버전이 높아질 때마다 조금씩 증가되는 추세이다.
  • arm
    • 2 ~ 32의 범위에 default는 4이다.
  • arm64
  • x86_32
    • 2 ~ 8의 범위에 default는 8이다.
    • 추가로 X86_BIGSMP 설정을 사용하는 경우 2 ~ 32의 범위에 default는 32이다.
  • x86_64
    • 2 ~ 64의 범위에 default는 64이다.
    • 추가로 MAXSMP 설정을 사용하는 경우 2 ~ 8192 범위에 default는 8192이다.

 


smp_setup_processor_id() – ARM64

arch/arm64/kernel/setup.c

void __init smp_setup_processor_id(void)
{
        u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
        set_cpu_logical_map(0, mpidr);

        /*
         * clear __my_cpu_offset on boot CPU to avoid hang caused by
         * using percpu variable early, for example, lockdep will
         * access percpu variable inside lock_release
         */
        set_my_cpu_offset(0);
        pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
                (unsigned long)mpidr, read_cpuid_id());
}

부트 cpu인 로지컬 cpu 0번에 대한 mpidr 값을 읽어 매핑한다. (처음 부팅 시 로지컬 cpu는 항상 0번이다.)

  • 코드 라인 3~4에서 MPIDR의 Aff3, Aff2, Aff1, Aff0 필드들만을 가져오기 위해 비트마스킹을 하여 읽어온다. 그런 후 이를 로지컬 cpu 0번에 해당하는 cpu_logical_map에 저장한다.
    • CPU Affinity는 여러 개의 CPU core 중에서 각각의 cpu가 가지는 고유 번호같은 것.
    • Affinity는 계층적으로 표현된다.
      • 아래 별도 섹션에서 다룬다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu에서 사용할 현재 부트 cpu에 대한 offset을 0으로 설정한다.
  • 코드 라인 12~13에서 부팅 cpu에 대한 mpidr 값과 midr 정보를 출력한다.
    • 예) “Booting Linux on physical CPU 0x0000000000 [0x410fd083]

 

다음 그림은 arm64 시스템에서 8개의 cpu가 있는 경우를 가정하여 위의 함수가 처음 동작한 경우이다.

  • arm64에서는 로지컬 cpu 0번에 해당하는 매핑만 설정해둔다.

 

__cpu_logical_map[] – ARM64

arch/arm64/kernel/setup.c

u64 __cpu_logical_map[NR_CPUS] = { [0 ... NR_CPUS-1] = INVALID_HWID};

 

MPIDR 관련 매크로 – ARM64

#define INVALID_HWID            ULONG_MAX

#define MPIDR_UP_BITMASK        (0x1 << 30)
#define MPIDR_MT_BITMASK        (0x1 << 24)
#define MPIDR_HWID_BITMASK      UL(0xff00ffffff)

#define MPIDR_LEVEL_BITS_SHIFT  3
#define MPIDR_LEVEL_BITS        (1 << MPIDR_LEVEL_BITS_SHIFT)
#define MPIDR_LEVEL_MASK        ((1 << MPIDR_LEVEL_BITS) - 1)

#define MPIDR_LEVEL_SHIFT(level) \
        (((1 << level) >> 1) << MPIDR_LEVEL_BITS_SHIFT)

#define MPIDR_AFFINITY_LEVEL(mpidr, level) \
        ((mpidr >> MPIDR_LEVEL_SHIFT(level)) & MPIDR_LEVEL_MASK)
  • MPIDR_LEVEL_BITS
    • 하나의 affinity level 값이 몇 비트로 이루어져 있는지 나타내고 8로 설정되어 있다 (1바이트).
  • MPIDR_LEVEL_MASK
    • 0xff

 

다음은 2개의 cpu가 빅 클러스터를 이루고, 4개의 cpu가 리틀 클러스터를 가진 odroid-N2 시스템의 mpidrmidr 값들을 보여준다.

  • 클러스터 id가 0과 1로 구분되고, cpu id가 0~3으로 구분됨을 알 수 있다.
   Booting Linux on physical CPU 0x0000000000 [0x410fd034]
CPU1: Booted secondary processor 0x0000000001 [0x410fd034]
CPU2: Booted secondary processor 0x0000000100 [0x410fd092]
CPU3: Booted secondary processor 0x0000000101 [0x410fd092]
CPU4: Booted secondary processor 0x0000000102 [0x410fd092]
CPU5: Booted secondary processor 0x0000000103 [0x410fd092]

 


smp_setup_processor_id() – ARM32

arch/arm/kernel/setup.c

void __init smp_setup_processor_id(void)
{
        int i;
        u32 mpidr = is_smp() ? read_cpuid_mpidr() & MPIDR_HWID_BITMASK : 0;
        u32 cpu = MPIDR_AFFINITY_LEVEL(mpidr, 0);

        cpu_logical_map(0) = cpu;
        for (i = 1; i < nr_cpu_ids; ++i)
                cpu_logical_map(i) = i == cpu ? 0 : i;

        /*
         * clear __my_cpu_offset on boot CPU to avoid hang caused by
         * using percpu variable early, for example, lockdep will
         * access percpu variable inside lock_release
         */
        set_my_cpu_offset(0);

        pr_info("Booting Linux on physical CPU 0x%x\n", mpidr);
}

현재 로지컬 cpu 0번에 대한 물리 cpu 번호를 읽어 매핑한다. (처음 부팅 시 로지컬 cpu는 항상 0번이다.)

  • 코드 라인 4~7에서 MPIDR의 하위 3바이트(Aff0, Aff1, Aff2)를 가져오기 위해 비트마스킹을 한 후 가장 단계가 낮은 affinity level 0번 값을 읽어온다. 읽어온 값은 물리 cpu 번호이며 이를 로지컬 cpu 0번에 해당하는 cpu_logical_map에 저장한다.
    • CPU Affinity는 여러 개의 CPU core 중에서 각각의 cpu가 가지는 고유 번호같은 것.
    • Affinity는 계층적으로 표현된다.
    • x86의 하이퍼스레딩과 같이 arm에서도 가상 코어를 상용화하려 하였다가 포기하였다.
  • 코드 라인 8~9에서 임시로 로지컬 cpu 1번 부터 나머지 로지컬 cpu에 대해 물리 cpu 번호와 동일하게 구성한다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu에서 사용할 현재 cpu에 대한 offset을 0으로 설정한다.
  • 코드 라인 13에서 어떤 물리 CPU로 리눅스 부팅이 되었는지 안내하는 정보 출력한다.

 

MPIDR 관련 매크로 – ARM32

arch/arm/include/asm/cputype.h

#define MPIDR_AFFINITY_LEVEL(mpidr, level) \
        ((mpidr >> (MPIDR_LEVEL_BITS * level)) & MPIDR_LEVEL_MASK)
#define MPIDR_LEVEL_BITS 8
#define MPIDR_LEVEL_MASK ((1 << MPIDR_LEVEL_BITS) - 1)
  • MPIDR_LEVEL_BITS
    • 하나의 affinity level 값이 몇 비트로 이루어져 있는지 나타내고 8로 설정되어 있다 (1바이트).
  • MPIDR_LEVEL_MASK
    • 하위 MPIDR_LEVEL_BITS개의 비트만 1이고 나머지는 0인 값이다.

 

다음 그림은 arm 시스템에서 8개의 cpu가 있는 경우를 가정하여 위의 함수가 처음 동작한 경우이다.

 


MPIDR(Multiprocessor Affinity Register)

멀티프로세서 시스템의 스케줄링을 위해 어떠한 코어들간에 친화력(affinity)이 있는지 레벨별로 제공한다.

 

Process Affnity(프로세스 친화력)

프로세스 스케쥴링시 한 번 배정되었던 프로세스를 어떤 CPU 코어를 사용하게 할 지 결정하기 위해 필요.

  • 리눅스 스케줄러는 가능하면 캐시 데이터 재활용을 위해 같은 코어에 배정
  • 로드 밸런스를 위해 같은 코어에 배정을 하지 않는 경우 affinity 0 레벨에서 검토하고 배정할 core가 busy한 경우 점차 상향하여 위로 올라간다.

 

1) ARM64(AArch64)

다음 그림은 ARM64의 MPIDR 레지스터를 보여준다.

  • 최대 4단계의 affinity 레벨을 제공한다.

 

U(Uniprocessor):

cpu가 UP 및 SMP 용도 두 가지로 표현되어 있다.

  • 0=Multiprocessor
  • 1=Uniprocessor

 

MT(Multi-Thread):

virtual core(hw 멀티스레드) 지원 여부 (멀티 스레딩 타입 접근으로 구현된 논리 프로세서의 밀결합 최소 레벨)

  • 0
    • 최소 affinity 레벨에서의 프로세스 성능이 최대 독립적
      • affinity 레벨 0은 독립적인 core id를 사용하므로 각각의 core 성능은 최대한 독립적으로 운영된다.
  • 1
    • 최소 affinity 레벨에서의 프로세스 성능이 매우 의존적
      • affinity 레벨 0은 virtual core id를 사용하므로 각각의 virtual core 성능은 같은 affinity 레벨의 virtual core의 성능에 영향을 끼친다.
      • x86의 하이퍼스레드와 동일한 개념이다.

 

ARM64 CPU Topology

리눅스 커널 구현에 따라 보통 2개 중 하나를 사용한다. ARM64의 경우도 virtual core를 개발하여 상용화하려다 계획을 포기하였다. 따라서 현재 모든 arm64 cpu들은 MT=0 모드만 사용한다. 향후 2021년 ARMv9 아키텍처 출시하고, 2022년 이후 어느 시점에서 추가 개발되어 출시할 예정이다.

  • 3/4단계 affinity 레벨 사용 (MT=1)
    • affinity 2 + (affnity 3 << 8):
      • package id
    • affinity 1:
      • core id
    • affinity 0:
      • thread id
  • 2단계 affinity 레벨 사용 (MT=0)
    • affinity 1 + (affnity 2 << 8) + (affnity 3 << 16) :
      • package id
    • affinity 0:
      • core id

 

다음 그림은 ARM64 CPU topology에서 MT(Multi Thread) 지원 여부에 따라 구성된 모습을 보여준다.

  • 리눅스는 MT=1일 때 8개의 cpu로 인식하고, MT=0일 때 4개의 cpu로 인식한다.

 

2) ARM(AArch32)

다음 그림은 32bit ARM의 MPIDR 레지스터를 보여준다.

  • cpu가 UP 및 SMP 용도 두 가지로 표현되어 있다.
  • 최대 3단계의 affinity 레벨을 제공한다.

mpidr

 

ARM CPU Topology

구현에 따라 보통 2개 중 하나를 사용한다. ARM의 경우 virtual core를 개발하여 상용화하려다 계획을 포기하였다. 따라서 현재 모든 arm cpu들은 MT=0 모드만 사용한다.

  • 3단계 affinity 레벨 사용 (MT=1)
    • affinity 2:
      • socket id
    • affinity 1:
      • core id
    • affinity 0:
      • thread id
  • 2단계 affinity 레벨 사용 (MT=0)
    • affinity 2:
      • (reserved)
    • affinity 1:
      • socket id
    • affinity 0:
      • core id

 

다음 그림은 ARM CPU topology에서 MT(MultiThread) 지원 여부에 따라 구성된 모습을 보여준다.

  • 리눅스는 MT=1일 때 8개의 cpu로 인식하고, MT=0일 때 4개의 cpu로 인식한다.

 


per-cpu 관련 offset 설정

set_my_cpu_offset() – ARM64

arch/arm64/include/asm/percpu.h

static inline void set_my_cpu_offset(unsigned long off)
{
        asm volatile(ALTERNATIVE("msr tpidr_el1, %0",
                                 "msr tpidr_el2, %0",
                                 ARM64_HAS_VIRT_HOST_EXTN)
                        :: "r" (off) : "memory");
}

현재 cpu의 per-cpu offset을 @offset 값으로 설정한다

  • TPIDR 레지스터를 사용하여 현재 cpu에 대한 per-cpu offset 값을 저장하여 per-cpu 변수에 대한 빠른 access를 가능하게 한다.
  • 부팅 cpu를 제외한 나머지 cpu들에 대해 nVHE 기능 지원 여부에 따라 VHE 지원하는 경우 tpidr_el1으로 동작하고, nVHE의 경우 tpidr_el2 레지스터를 사용해야 한다.

 

TPIDR_EL1 또는 TPIDR_EL2 – ARM64

  • TPID(스레드 ID) 정보가 기록된 레지스터
    • 현재 리눅스에서는 TPID를 저장하는 목적으로 사용하지 않고 각 cpu의 per-cpu offset를 저장하여 더 빠른 per-cpu data의 access를 위해 사용된다.

 

set_my_cpu_offset() – ARM32

arch/arm/include/asm/percpu.h

/*
 * Same as asm-generic/percpu.h, except that we store the per cpu offset 
 * in the TPIDRPRW. TPIDRPRW only exists on V6K and V7
 */
#if defined(CONFIG_SMP) && !defined(CONFIG_CPU_V6)
static inline void set_my_cpu_offset(unsigned long off)
{
        /* Set TPIDRPRW */

        asm volatile("mcr p15, 0, %0, c13, c0, 4" : : "r" (off) : "memory");
}

현재 cpu의 per-cpu offset을 @offset 값으로 설정한다

  • per-cpu 자료구조에서 사용되는 cpu마다 개별적으로 가지는 offset 값.
  • ARMv7에서는 속도 향상을 위해서 TPIDRPRW register를 사용함.
    • ARMv7 이전 아키텍처에서는 이 레지스터를 사용하지 않고 메모리를 사용하여 연산하느라 메모리에 대한 접근이 2번 필요하여 느렸었고 이를 극복하기 위해 본래의 목적으로 사용하지 않는 레지스터인 TPIDRPRW를 사용하여 메모리 접근을 한 번으로 줄이기 위해 사용한다.
    • 참고: ARM: implement optimized percpu variable | LWN.net

 

TPIDRPRW (Thread ID-R) – ARM32

  • Multiprocessor Extension에서 사용하며 PL1(Previlidge Level 1으로 커널 레벨) 이상에서만 사용가능하다.
  • Security Extension에서는 레지스터는 뱅크된다.
  • TPID(스레드 ID) 정보가 기록된 레지스터
    • 현재 리눅스에서는 TPID를 저장하는 목적으로 사용하지 않고 각 cpu의 per-cpu offset를 저장하여 더 빠른 per-cpu data의 access를 위해 사용된다.

 

참고

 

Priority Inversion & Priority Inheritance

Priority Inversion

realtime 스케줄러는 우선 순위에 따라 태스크가 먼저 수행되어야 하는데 lock 리소스를 획득한 후 우선 순위 원칙에 위배

  • 문제점
    • cpu#1의 A 스레드는 cpu#0 C의 작업이 끝날때 까지 lock을 얻지 못해 기다리는데 C 작업 중 B에 의해 선점당하면서 A는 더 오랜 시간을 기다려야 한다.
    • A 스레드가 B 스레드보다 우선 순위가 높은 상황인데도 위와 같은 상황에서 우선 순위가 더 낮은 B 스레드가 먼저 처리되는 불합리한 상황이 벌어진다.
  • 해결 방법
    • 이러한 점을 보강하기 위하여 Priority Inheritance Protocol이 개발되었고, 리눅스 커널은 priority가 중요한 RT 태스크들 사이에서 사용된다.

 

다음 그림과 같이 A의 lock 획득이 느려지는 상황을 보여준다.

 

Priority Inheritance

  • Priority Inversion 상황과 다르게 A 스레드가 lock을 얻다 실패하는 경우 현재 해당 리소스 lock을 얻어 동작하는 스레드 C의 우선 순위를 A 스레드와 같이 높은 우선 순위로 상속시키면서 그 보다 낮은 우선 순위의 B 스레드에게 선점되지 않게 막는다. 결국 A 스레드는 보다 빠르게 공유된 S 자원의 할당을 받아 처리할 수 있다.
  • 리눅스 커널은 RT 태스크들 사이에서 사용되며 RT Mutex API를 통해 구현되었다.

 

다음 그림은 C 스레드가 lock을 잡고 있는 동안 priority가 A 스레드 처럼 잠시 높아져서 lock을 해제할 때까지 preempt 되지 않고 빠르게 처리하여, 결국 A 스레드 역시 빠르게 처리되는 것을 보여준다.

 

참고

Lock Problem(Dead-Lock)

Lock이 의도되지 않은 상태에 빠져 나오지 못하는 상태가 여러 가지 있는데 그 중 대표적인 3가지를 알아본다.

Dead-Lock

  • 두 개의 태스크간에 자원을 하나 씩 소유하고 상대방의 자원에 접근하려 하는 경우 교착 상태에 빠져 나올 수 없는 상태가 된다.

lock_problem1

Circular Lock Dependency

  • 같은 lock을 두 번 사용하는 경우 두 번째 lock 호출 시 무한 대기 상태가 된다.

lock_problem2

Interrupt Safety

  • 루틴에서 같은 lock을 두 번 호출하지 않았는데도 불구하고 인터럽트 루틴에 의해 같은 lock이 호출되어 결국 두 번 사용되어지는 경우가 발생하는 경우에 두 번째 lock 호출에서 무한 대기 상태가 된다.

lock_problem3

  • 인터럽트 루틴에 의해 우선 순위가 더 높은 태스크로 스케쥴 되어 이미 호출된 lock을 또 사용하게 되는 경우에도 같은 상황이 발생한다.

lock_problem4a

참고

 

User stack vs Kernel stack

user task가 생성될때마다 스택이 각각 유저 스택과 커널 스택이 하나씩 만들어진다.

user stack

  • 유저 스택의 크기는 스레드 생성 시 지정될 수 있고, default 크기는 역시 아키텍처마다 다른다. 최대 사이즈 확인 방법은 “ulimit -a”를 사용한다.(32bit ARM은 8MB)
$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 14846
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 14846
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

 

kernel stack

  • task 생성 시 마다 kernel stack이 생성된다.
    • 사이즈는 유저 스택보다 훨씬 적으며 커널 버전 및 커널 옵션에 따라 조금씩 다르며 보통 1 ~ 4 페이지를 사용한다.
    • 예)
      • arm: 2 page (8K)
      • 4K 페이지를 사용하는 arm64: 4 page (16K)
      • 16K 페이지를 사용하는 arm64: 1 page (16K)

 

Exception 처리용 stack

  • arm 아키텍처에서는 각 exception에 대해 하나씩 있는 mini 스택을 사용한다.
    • exception이 발생되어 해당 exception 핸들러에 진입한 경우에는 각 exception에서 사용하는 3 word의 mini 스택을 이용하고 곧바로 svc 모드로 바꾼 후에는 svc용 스택(커널 스택)을 이용해 처리를 계속한다.

 

context switch시 스택 사용

  • user mode에서 task(thread)가 수행되고 있을 때 syscall을 하여 kernel mode로 jump하고 context switching을 하게되는데 이 때 부터는 해당 유저 task(thread)가 소유한 kernel stack을 사용한다.
  • 다시 kernel mode에서 user mode로 context switching을 하여 돌아가게 되면 task(thread)용 user stack을 사용한다.
  • 참고로 arm에서 sp 레지스터는 각 모드에서 각각 운영되며 arm64에서는 exception level별로 각각 운영된다.
    • arm: sp_usr, sp_svc, sp_irq, …
    • arm64: el0_sp, el1_sp, el2_sp

stack2

 

인터럽트 발생 시 스택

  • 유저 모드에서 swi나 인터럽트 등이 발생하는 경우는 조금 전 설명한 것과 같이 해당 태스크가 소유한 커널 스택을 이용한다.
    • arm 커널은 유저 모드에서 irq 모드를 통해 svc 모드로 전환될 때 irq 모드용 12바이트의 미니 스택을 운용하는데 이의 설명은 생략한다
  • 커널 모드에서 인터럽트 등이 발생하는 경우는 다음과 같이 2개로 분리된다.
    • 커널 스레드 동작 중 인터럽트가 발생한 경우
      • arm: 커널 스레드가 소유한 스택을 그대로 사용한다.
      • arm64: 인터럽트 스택을 사용한다.
    • 인터럽트 context 수행 중 더 높은 우선 순위 등의 인터럽트가 nest 된 경우
      • arm: 현재 사용하고 있는 상태의 커널 스택을 사용한다.
      • arm64: 인터럽트 스택을 사용한다.
  • 아키텍처에 따라 hardirq, softirq용 스택을 분리하여 운용하기도 한다.
    • arm의 경우 hardirq는 위에서 설명한 조건에 따라 적절한 커널 스택을 사용하고 softirq는 ksoftirqd가 소유한 커널 스택을 사용한다.
    • arm64의 경우 hardirq는 인터럽트용 스택을 사용하고 softirq는 커널옵션에 따라 ksoftirqd가 소유한 커널 스택을 사용하게 할 수 있다.

 

예) arm 시스템, 어떠한 태스크도 스케줄되지 않은 cpu에서 인터럽트 발생하는 경우?

  • 이미 커널 스레드인 idle 태스크가  스케줄링되어 동작하고 있다. arm의 경우 wfi등이 수행되어 절전 모드로 내부 클럭이 정지된 상태이다.
    • 인터럽트가 발생하는 경우 wfi에 의해 멈춘 cpu가 계속 동작하고 idle 태스크가 소유한 커널 스택을 사용하여 인터럽트 context가 진행된다.
  • idle 태스크는 커널 부트업 시  각 cpu별로 초기화 수행 후 idle 태스크로 이름이 바뀌어 idle 스케줄러에 등록되어 어떠한 태스크도 스케줄되지 않을 때에만 동작하는 특수한 태스크이다.
    • idle 태스크의 pid는 모든 cpu에서 공통으로 0이다.
  • idle 태스크는 부트업시 사용하던 init_task가 나중에 변경된 태스크이므로 인터럽트가 발생하면 레지스터 백업은 이 스택을 사용한다.

참고