2017년 1월 25일 수요일

process descriptor 와 thread_info

커널은 프로세스 목록을 태스크 리스트(tast list)라고 하는 연결 리스트로 저장한다. 각 항목이 task_struct 구조체인 연결 리스트이다. 리눅스 시스템에서 생성된 전체 프로세스들이 모두 저장되기 때문에, 어느 한 task_struct를 선택하여 리스트를 따라가보면 전체 리스트를 탐색하고 다시 본인 task_struct 자리로 돌아온다. 모든 프로세스는 PID가 1 인 init_task 라는 initial task로부터 생성되어진 자식들이다. for_each_process 함수 코드를 보면 init_task부터 시작하여 모든 프로세스를 순회하는 코드를 확인할 수 있다.

이 구조체를 프로세스 서술자(process descriptor)라고 부르며, 프로세스 서술자에는 사용 중인 파일, 프로세서의 주소 공간, 대기 중인 시그널, 프로세스의 상태 등 실행 중인 프로그램을 설명하는 많은 정보가 들어있기 때문에 용량이 큰 구조체이다. 32비트 시스템에서는 약 1.7KB이며, 64비트 시스템에서는 약 3.2KB 정도이다.

프로세스 별로 프로세스 서술자를 할당하여, 프로세스에 관한 많은 정보를 저장한다고 하였다. 하지만 여기에 프로세스에 관련한 아직 말하지 않은 중요한 정보가 더 존재한다. 바로, 프로세스 커널 스택(process kernel stack)이다. 이 프로세스 커널 스택은 THREAD_SIZE 만큼의 메모리 크기로 할당되며, struct thread_info를 포함한다. 커널 스택이 상위 메모리 주소에서 아래로 스택을 쌓아사면서 자라나고, 그 반대 방향의 끝에는 thread_info가 자리하고 있다.

커널 스택과 thread_info task_struct 관계

THREAD_SIZE 는 ARM architecture에서 보통 32비트는 0x2000 를 사용했고, 64비트로 넘어오면서 0x4000로 2배로 커졌다.(아래 커널 코드 설명도 모두 ARM architecture 기반으로 설명하였다.)

프로세스에서 프로세스 서술자(혹은 커널 스택)에 접근하는 일은 매우 빈번한 일이기 때문에 latency가 없이 접근 가능한지 여부는 매우 중요하다. 이를 위해 ARM architecture에서는 sp 레지스터로 스택포인터(stack pointer)주소를 저장하고, 이를 참조하여 매우 빠르게 현재 스택 주소와 task_struct 를 접근 가능하다.

thread_info task_struct current_thread_info THREAD_SIZE

리눅스 커널 코드에서 sp, task_struct 와 thread_info 를 어떻게 접근하는 위 그림을 참고하여 각 코드를 확인해보자.

register unsigned long current_stack_pointer asm ("sp");

현재 스택 포인트가 가리키는 주소는 sp 레지스터를 읽어오면 확인 가능하다. 커널 코드에서 스택의 움직임에 따라(스택이 자라나고 줄어듦에 따라) sp 가 가리키는 주소는 계속 관리되고 있기 때문에, 항상 참조 가능하다.

static inline struct thread_info *current_thread_info(void)
{
    return (struct thread_info *)
        (current_stack_pointer & ~(THREAD_SIZE - 1));
}

thread_info 는 sp 레지스터에 하위 비트를 지워버리는 방법으로 참조 가능하다. THREAD_SIZE 만큼의 하위 비트를 0로 마스킹해버면, STACK 의 최하위 주소인 thread_info 의 주소를 얻어낼 수 있다. 위 current_thread_info 인라인 함수를 사용하면, 커널 코드에서 언제든지 현재 running 중인 thread 의 thread_info 정보를 가져올 수 있다.

#define get_current() (current_thread_info()->task)
#define current get_current()

task_struct 구조체는 thread_info->task 포인터 주소를 참고하는 방법으로 참조 가능하다. sp 를 이용하여 현재의 thread_info 를 빠르게 가져올 수 있으니, thread_info 의 멤버 중 task_struct 를 가리키고 있는 task 포인트를 이용하는 것이다. 커널 코드에서 current 라는 매크로로 현재 running 중인 process descriptor(task_struct)를 가져올 수 있다. 커널 코드를 보다보면 current 매크로를 사용하는 부분이 많이 나오는데, running 중인 process 정보를 가져오는 것이라고 생각하면 된다.

지금까지 살펴 본 위의 코드 내용은 ARM architecture v7 까지의 이야기이다. 즉, 32비트 계열에서의 주소 참조 방식이며, ARMv8 (64bit - Aarch64)에서는 sp_el0 레지스터를 하나 더 할당하여, thread_info 의 주소를 저장한다. 아래 그림을 참고 하자.


ARMv8 에서는 sp 는 스택의 현재 주소를 가리키며, sp_el0 는 thread_info 주소를 가리킨다. 따라서 current_thread_info 를 수행할때, sp 값의 마스킹없이 sp_el0 값을 읽어서 바로 참조하면 된다. thread_info 주소값 참조를 위해 레지스터가 하나 더 추가된 것이다.
/*
 * struct thread_info can be accessed directly via sp_el0.
 */
static inline struct thread_info *current_thread_info(void)
{
    unsigned long sp_el0;

    asm ("mrs %0, sp_el0" : "=r" (sp_el0));

    return (struct thread_info *)sp_el0;
}

sp_el0 레지스터의 추가로, current_thread_info 를 가져올때, 항상 실행하던 THREAD_SIZE 의 마스킹 동작이 없어졌다. 이점은 작은 변화이지만, 코드의 호출 빈도를 생각해보면 큰 이점이다.

위에서 확인한 task_struct 와 thread_info 의 참조 관계는 상당히 오랜시간을 지켜왔다. 하지만 이 관계를 깨는 변경 사항이 커널 4.9 에 반영(commit id : c65eacbe29) 되었다. THREAD_INFO_IN_TASK feature 이다. CONFIG_THREAD_INFO_IN_TASK 가 enable 되면 커널 스택에 포함되어 있던 thread_info 구조체 변수는 task_struct 속으로 들어간다.

CONFIG_THREAD_INFO_IN_TASK

위의 그림에서도 확인해보면, 커널 스택은 온전히 스택 데이터 만으로 채워지게 된다. task_struct 의 정의를 보면, thread_info 는 구조체의 맨 앞에 위치하도록 되어있다. task_struct 와 thread_info 접근을 동일한 주소값을 참조하여 접근하기 위함이다. 해당 주소값은 sp_el0 에 저장되어 있고, 해당 주소를 읽어서 task_struct 로 캐스팅하면 current task descriptor 를 접근할 수 있고, thread_info 로 캐스팅하면 thread_info 를 접근할 수 있다.

static __always_inline struct task_struct *get_current(void)
{
    unsigned long sp_el0;

    asm ("mrs %0, sp_el0" : "=r" (sp_el0));

    return (struct task_struct *)sp_el0;
}

sp_el0 레지스터를 읽어서, task_sturct 구조체로 캐스팅을 하면 current task_struct 구조체 변수가 된다.

#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
 * For CONFIG_THREAD_INFO_IN_TASK kernels we need  for the
 * definition of current, but for !CONFIG_THREAD_INFO_IN_TASK kernels,
 * including  can cause a circular dependency on some platforms.
 */
#include 
#define current_thread_info() ((struct thread_info *)current)
#endif

current는 sp_el0 레지스터가 가리키는 주소라고 하였다. CONFIG_THREAD_INFO_IN_TASK 가 세팅되면, task_sturct 구조체에 thread_info 구조체가 맨 앞에 위치하기 때문에 둘을 참조하는 메모리 주소는 동일하다. 이 주소를 thread_info 로 캐스팅하면 current_thread_info를 참조할 수 있다.

Navigation of this blog:이 블로그 한눈에 보기

댓글 없음:

댓글 쓰기