ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

203239
1625
172600


[리눅스커널] 프로세스: thread_info 구조체 초기화 코드 분석 4. 프로세스(Process) 관리

이전 절에서는 thread_info 구조체가 프로세스의 세부 동작 방식을 관리하는 내용을 살펴봤습니다. 이번에는 프로세스가 생성될 때 thread_info 구조체를 초기화하는 과정을 살펴보겠습니다. 이번 절에서는 다음 내용을 다룹니다.

프로세스가 생성될 때 스택 공간을 할당받는 동작 
태스크 디스크립터인 task_struct 구조체와의 관계

dup_task_struct() 함수 분석

이전 절에서 프로세스를 처음 생성할 때 copy_process() 함수를 호출한다는 사실을 확인했습니다. copy_process() 함수에서는 dup_task_struct() 함수를 호출해서 태스크 디스크립터와 프로세스가 실행할 스택 공간을 새로 만듭니다.

dup_task_struct() 함수에서 호출하는 핵심 함수는 다음과 같습니다.

alloc_task_struct_node() 함수: 슬럽 할당자로 태스크 디스크립터 task_struct 구조체를 할당받음 
alloc_thread_stack_node() 함수: 프로세스 스택 공간을 할당받음

그런데 여기서 한 가지 의문이 생깁니다. thread_info 구조체를 초기화하는 과정을 알아보는 이유는 무엇일까요? 그 이유는 thread_info 구조체를 초기화하는 코드를 보면 thread_info 구조체와 해당 태스크 디스크립터가 어떻게 연결돼 있는지 알 수 있기 때문입니다.

dup_task_struct() 함수의 주요 코드를 보면서 세부 처리 방식을 살펴보겠습니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
02 {
03 struct task_struct *tsk;
04 unsigned long *stack;
05 struct vm_struct *stack_vm_area;
06 int err;
...
07 tsk = alloc_task_struct_node(node);
08 if (!tsk)
09 return NULL;
10
11 stack = alloc_thread_stack_node(tsk, node);
12 if (!stack)
13 goto free_tsk;
...
14 tsk->stack = stack;
...
15 setup_thread_stack(tsk, orig);
...
16 return tsk;

먼저 7번째 줄을 봅시다.

7 tsk = alloc_task_struct_node(node);

alloc_task_struct_node() 함수를 호출해서 태스크 디스크립터를 할당받습니다.

다음으로 11번째 줄을 봅시다.

11 stack = alloc_thread_stack_node(tsk, node);

alloc_thread_stack_node() 함수를 호출해서 스택 메모리 공간을 할당받습니다. 프로세스가 생성될 때 스택 공간을 할당받으며 그 크기는 정해져 있습니다. ARMv7 아키텍처 기반 리눅스 커널에서는 프로세스 스택의 크기가 0x2000입니다. 

---
라즈베리 파이는 32비트 ARMv7 아키텍처를 채용했으니 프로세스 스택의 크기는 0x2000입니다.
참고로 ARM64비트 아키텍처를 적용한 시스템에서는 프로세스 스택 크기가 0x4000입니다.
---

다음으로 14번째 줄을 보겠습니다.

14 tsk->stack = stack;

task_struct 구조체의 task 필드에 할당받은 스택의 주소를 저장합니다. 할당받은 스택의 최상단 주소를 태스크 디스크립터에 설정하는 것입니다.

마지막으로 15번째 줄을 보겠습니다.

15 setup_thread_stack(tsk, orig);

setup_thread_stack() 함수를 호출해서 태스크 디스크립터 주소를 thread_info 구조체의 task 필드에 저장합니다. setup_thread_stack() 함수를 보면서 세부 동작 방식을 살펴봅시다.

https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/sched/task_stack.h
1 static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
2 {
3 *task_thread_info(p) = *task_thread_info(org);
4 task_thread_info(p)->task = p;
5 }

함수를 분석하기에 앞서 인자의 의미를 확인합시다.

struct task_struct *org: 부모 프로세스의 태스크 디스크립터
struct task_struct *p: 생성한 프로세스의 태스크 디스크립터

3 번째 줄에서는 부모 프로세스의 thread_info 구조체 필드를 자식 프로세스의 thread 구조체 공간에 복사합니다.

3 *task_thread_info(p) = *task_thread_info(org);

이로써 자식 프로세스는 부모 프로세스의 컨텍스트 정보를 공유 받습니다.

4번째 줄을 보겠습니다.

4 task_thread_info(p)->task = p;

thread_info 구조체의 task 필드에 태스크 디스크립터의 주소를 저장합니다. thread_info 구조체 선언부의 05 번째 줄을 보겠습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h
01 struct thread_info {
02 unsigned long flags; /* low level flags */
03 int preempt_count; /* 0 => preemptable, <0 => bug */
04 mm_segment_t addr_limit; /* address limit */
05 struct task_struct *task; /* main task structure */

thread_info 구조체 선언부의 05번째 줄을 보면 struct task_struct 타입인 포인터형 task 필드가 보입니다.

위 코드를 실행하면 프로세스의 태스크 디스크립터와 thread_info 구조체는 다음 그림과 같은 구조로 연결됩니다.

 
그림 4.22 thread_info 구조체와 task_struct 구조체의 관계

task_struct 구조체의 stack 필드는 프로세스 스택의 최상단 주소를 가리킵니다. 프로세스 스택의 최상단 주소에 있는 thread_info 구조체의 task 필드는 태스크 디스크립터의 주소를 가리킵니다. 그래서 프로세스 스택의 주소나 태스크 디스크립터의 주소만 알면 스택의 최상단 주소에 접근할 수 있습니다.

 
그림 4.23 스택 주소와 태스크 디스크립터 주소와의 관계

커널은 어셈블리 코드로 실행 중인 프로세스 스택 주소를 언제든지 알아낼 수 있습니다. 달리 보면 커널은 프로세스의 스택 최상단 주소와 태스크 디스크립터를 언제든 계산할 수 있습니다.

alloc_task_struct_node() 함수 분석 

이어서 프로세스를 생성하는 과정에서 태스크 디스크립터를 할당하는 alloc_task_struct_node() 함수를 살펴보겠습니다. 분석할 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static struct kmem_cache *task_struct_cachep;
02 
03 static inline struct task_struct *alloc_task_struct_node(int node)
04 {
05 return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
06 }

alloc_task_struct_node() 함수를 호출하면 task_struct_cachep라는 슬럽 캐시를 통해 task_struct 구조체를 할당합니다. 05번째 줄과 같이 kmem_cache_alloc_node() 함수를 호출해 지정한 슬럽 캐시에 대한 메모리를 할당받습니다.

task_struct_cachep라는 슬럽 캐시는 다음과 같은 기능을 수행합니다.

task_struct 구조체 크기만큼 메모리를 미리 확보해 놓고 대기
task_struct 구조체 할당 요청이 오면 이미 할당해 놓은 task_struct 구조체의 시작 주소를 반환


슬럽 캐시와 슬럽 오브젝트는 14.9절에서 상세히 분석합니다. 

alloc_thread_stack_node() 함수 분석

프로세스를 생성하는 과정에서 스택 메모리 공간을 할당하는 alloc_thread_stack_node() 함수를 살펴보겠습니다. 분석할 코드는 다음과 같습니다.

https://elixir.bootlin.com/linux/v4.19.30/source/kernel/fork.c
01 static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node)
02 {
03 struct page *page = alloc_pages_node(node, THREADINFO_GFP,
04      THREAD_SIZE_ORDER);
05
06 return page ? page_address(page) : NULL;
07 }

03번째 줄은 페이지를 할당하는 동작입니다. THREAD_SIZE_ORDER 플래그가 1이니 2개의 페이지를 할당합니다. 

https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/thread_info.h
#define THREAD_SIZE_ORDER 1

---
리눅스에서 메모리를 관리하는 단위를 페이지(Page)라고 부르며, 커널의 '메모리 서브 시스템'에서 페이지를 관리합니다. 페이지의 크기는 일반적으로 4KB입니다.
---

다음은 06번째 줄입니다.

06 return page ? page_address(page) : NULL;

페이지를 할당한 다음에는 반드시 가상 주소로 변환해야 합니다. page_address() 함수를 호출해 페이지를 가상 주소로 바꾼 다음 이를 반환합니다. 페이지를 2개 할당했으니 0x2000 크기의 물리 메모리를 확보합니다.

따라서 32비트 ARMv7 아키텍처를 채용한 라즈비안에서는 프로세스의 스택 크기가 0x2000입니다.

이번 절에서 배운 내용을 정리하면 다음과 같습니다.

첫째, 태스크 디스크립터로 프로세스 스택의 최상단 주소는 어떻게 알 수 있을까?
task_struct 구조체의 stack 필드를 통해 프로세스 스택의 최상단 주소를 확인할 수 있습니다.

둘째, thread_info 구조체에서 태스크 디스크립터의 주소는 어떻게 알 수 있을까?
thread_info 구조체의 task 필드에 태스크 디스크립터 주소가 저장돼 있습니다.

* 유튜브 강의 동영상도 있으니 같이 들으시면 좋습니다. 




#프로세스

프로세스 소개 
프로세스 확인하기  
프로세스는 어떻게 생성할까?  
유저 레벨 프로세스 실행 실습  
커널 스레드  
커널 내부 프로세스의 생성 과정   
프로세스의 종료 과정 분석  
태스크 디스크립터(task_struct 구조체)  
스레드 정보: thread_info 구조체  
프로세스의 태스크 디스크립터에 접근하는 매크로 함수  
프로세스 디버깅  
   * glibc의 fork() 함수를 gdb로 디버깅하기  


"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2



핑백

덧글

댓글 입력 영역