Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

189162
807
85257


4.8 스레드 정보(struct thread_info) 4장. 프로세스 관리

프로세스 상세 정보를 저장하는 자료구조는 태스크 디스크립터입니다. 이와 더불러 프로세스 세부 실행 정보를 저장하는 struct thread_info 자료구조가 있습니다.

커널이 프로세스를 생성할 때 프로세스마다 고유한 스택 공간을 부여합니다. 스택 최상단 주소에 struct thread_info 구조체 필드가 저장돼 있습니다. 이 구조체 필드는 커널이 프로세스를 제어할 때 필요한 중요 정보를 담고 있습니다.
+ 프로세스 컨택스트 정보
+ 선점 스케줄링 여부
+ 컨택스트 스위칭 시 저장한 레지스터 세트

이번 절에서는 프로세스 동작을 표현하는 struct thread_info 구조체에 대해서 알아봅시다.

struct thread_info 구조체 각 멤버에 대해 알아보기 전에 이 구조체가 프로세스 어느 주소에 위치했는지 먼저 점검하겠습니다. 

4.8.1 프로세스는 스택을 어떻게 쓸까?
struct thread_info 구조체 필드는 프로세스 스택 최상단 주소에 저장됩니다. 
다음 그림을 함께 보겠습니다.
 

위 그림에서 프로세스 스택 최상단 주소는 0x80C00000 그리고 스택 최하단 주소는 0x80C02000입니다. 

스택 최하단 주소가 0x80C02000인 이유는 뭘까요? ARM 아키텍처에서 프로세스가 실행하는 스택 사이즈는 0x2000 바이트로 고정돼 있기 때문입니다.

프로세스가 스택을 어떻게 쓰는지 간단히 알아봅시다. 

리눅스 커널에서 프로세스 스택은 높은 주소에서 낮은 주소 방향으로 자랍니다.
그림에서 화살표 방향을 눈여겨봅시다. 함수 호출은 프로세스 스택 최하단 주소(0x80C02000)에서 최상단 주소(0x80c0000) 방향입니다.

먼저 프로세스가 실행할 때는 커널 함수를 호출합니다. sys_write() 함수에서 vfs_write() 함수 방향으로 함수를 호출하는 것입니다.

가장 먼저 호출된 함수는 프로세스 스택 최하단 주소에 위치해있고 프로세스 스택 최상단 주소 방향으로 스택을 사용합니다.

또한 함수를 실행하면서 로컬 변수나 복귀 레지스터(링크드 레지스터: 자신을 호출한 함수 주소)도 프로세스 스택에 저장(푸시) 합니다. 이 때 필요한 스택 메모리를 프로세스 스택 공간을 최하단 주소에서 최상단 주소 방향으로 사용합니다.

쉽게 설명하면, 프로세스 스택은 최하단 주소에서 최상단 주소로 자란다고 볼 수 있습니다.

__wake_up_common_lock() 함수가 실행을 마치면 pipe_write() 함수로 복귀하고,
pipe_write() 함수 실행이 끝나면 __vfs_write() 함수로 복귀합니다. 자료구조 시간에 배웠던 스택(푸시, 팝)과 같은 동작입니다.

이번에는 스택 용어로 프로세스가 스택 공간에서 어떻게 동작하는지 조금 더 알아봅시다.
프로세스가 새로운 함수를 호출하면 프로세스 스택 공간에 스택에 이전에 호출했던 복귀 레지스터를 스택에 푸시합니다. 함수 실행이 끝나고 자신을 호출한 함수 주소를 알아야 이전 함수로 복귀할 수 있기 때문입니다.

프로세스가 함수 실행을 마치고 이전에 자신을 호출했던 함수로 돌아가려고 할 때 복귀 레지스터를 스택에 팝(Pop)합니다. 함수 실행이 끝나고 자신을 호출한 함수 주소를 알아야 이전 함수로 복귀할 수 있기 때문입니다.

여기서 한 가지 의문이 생깁니다.
프로세스가 새로운 함수를 실행할 때 프로세스 스택 푸시와 팝하는 리눅스 커널 코드를 어디서 볼 수 있을까?

C 코드에서는 스택 푸시와 팝하는 코드를 볼 수는 없습니다. 대신 어셈블리 코드에서 관련 코드를 확인할 수 있습니다. 프로세스 스택 공간에 푸시 팝 동작은 CPU 아키텍처(ARM, x86, ARM64) 마다 다르기 때문입니다.

__wake_up_common_lock() 함수 코드를 봅시다.
1 NSR:8015F23C|E1A0C00D  __wake_up_common_lock:  cpy     r12,r13
2 NSR:8015F240|E92DDFF0                               push    {r4-r12,r14,pc}
...
3 NSR:8015F2E8|E89DAFF0                               ldm     r13,{r4-r11,r13,pc}

1번째 줄 어셈블리 코드를 실행하면 현재 스택(r13) 주소를 r12 레지스터에 저장합니다.
조금 더 알아보기: cpy 명령어에 대해서

r1 레지스터가 0x1이고 r2 레지스터가 0xFFFF 라고 가정합시다.
다음 명령어를 실행하면 r1가 0xFFFF로 변경됩니다.
cpy r1 r2

cpy 어셈블리 명령어는 오른쪽에 있는 레지스터가 저장한 값을 왼쪽 레지스터에 저장하는 명령어입니다.
2번째 줄 코드에서 push란 명령어를 써서 프로세스 스택 공간에 r4, r5, r6, r7, r8, r9, r10, r11, r12, r14 그리고 pc 레지스터를 푸시합니다.

3번째 줄 코드는 __wake_up_common_lock() 함수 마지막 어셈블리 코드입니다.
ldm이란 명령어를 입력해서 스택에 푸시한 r4, r5, r6, r7, r8, r9, r10, r11, r12, r14 그리고 pc 레지스터를 팝하는 동작을 수행합니다. 스택에 저장한 레지스터 값들을 ARM 프로세스 레지스터로 다시 복사하는 과정입니다.

스택 메모리에 저장된 주소를 ARM 프로세스 PC(Program Counter)에 저장하면 PC에 저장된 주소에 있는 어셈블리 코드 Fetch해서 실행합니다.

4.8.2 struct thread_info 구조체 소개
struct thread_info 구조체에는 프로세스 실행 흐름에 대한 중요한 정보를 저장합니다. 프로세스 스케줄링 실행 시 이전에 실행했던 레지스터 세트와 프로세스 컨택스트 정보를 확인할 수 있습니다.

struct thread_info 구조체 코드를 살펴봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
};


구조체 파일 위치가 arch/arm/include 이므로 CPU 아키텍처에 의존적인 코드임을 짐작할 수 있습니다.

unsigned long flags;

아키텍처에 의존적인 프로세스 동작을 저장하는 멤버입니다.
[https://elixir.bootlin.com/linux/v3.18.71/source/arch/arm/include/asm/thread_info.h] 
#define TIF_SIGPENDING 0
#define TIF_NEED_RESCHED 1
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */
#define TIF_UPROBE 7
#define TIF_SYSCALL_TRACE 8

int preempt_count;

프로세스 컨택스트(인터럽트 컨택스트, Soft IRQ 컨택스트) 실행 정보와 프로세스가 스케줄링(Preemption)될 수 있는 조건을 저장합니다. struct thread_info 구조체에서 가장 중요한 멤버 중 하나입니다.

다음 소절에서 이 멤버를 리눅스 커널 코드에서 어떻게 접근하고 활용하는지 점검합니다.

__u32 cpu;
프로세스가 실행 중인 CPU 번호를 저장하는 멤버입니다.

struct task_struct *task;

실행 중인 프로세스의 태스크 디스크립터 주소를 저장합니다.

struct cpu_context_save cpu_context;

컨택스트 스위칭 되기 전 실행했던 레지스터 세트를 저장하는 멤버입니다. 프로세스가 스케줄링되고 다시 실행을 시작할 때 cpu_context란 멤버에 저장된 레지스터를 로딩합니다.

struct cpu_context_save 구조체 선언부에 있는 각각 필드는 레지스터를 의미합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/thread_info.h]
struct cpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /* Xscale 'acc' register, etc */
};

프로세스가 컨택스트 스위칭할 때 struct cpu_context_save 구조체에 정의된 멤버에 실행 중인 레지스터를 저장하는 것입니다.

위에서 다룬 struct thread_info 구조체가 어떤 값을 저장하는지 Trace32로 확인해 보겠습니다.
v.v %t %h %d %i  (struct thread_info*)0xCE4F8000 
1   (struct thread_info *) (struct thread_info*)0xCE4F8000 = 0xCE4F8000 -> (
2   (long unsigned int) flags = 2 = 0x2,
3    (int) preempt_count = 2097155 = 0x00100003,
4    (mm_segment_t) addr_limit = 3204448256 = 0xBF000000,
5    (struct task_struct *) task = 0xD2FB2B80,
6    (struct exec_domain *) exec_domain = 0xC1A1AFDC,
7    (__u32) cpu = 5 = 0x5,
8    (__u32) cpu_domain = 0 = 0x0,
9    (struct cpu_context_save) cpu_context = (
10      (__u32) r4 = 4056868736 = 0xF1CEE780,
11      (__u32) r5 = 3539676032 = 0xD2FB2B80,
12      (__u32) r6 = 3664491200 = 0xDA6BB2C0,
13      (__u32) r7 = 3688475136 = 0xDBD9AA00,
14      (__u32) r8 = 3248547128 = 0xC1A0E538,
15      (__u32) r9 = 3496897024 = 0xD06E6A00,
16      (__u32) sl = 3539677168 = 0xD2FB2FF0,
17     (__u32) fp = 3461324268 = 0xCE4F9DEC,
18      (__u32) sp = 3461324200 = 0xCE4F9DA8,
19      (__u32) pc = 3237955160 = 0xC0FF4658,
20      (__u32 [2]) extra = ([0] = 0 = 0x0, [1] = 0 = 0x0)),

2번째 줄을 봅시다.
2   (long unsigned int) flags = 2 = 0x2,

flags가 2이니 TIF_NOTIFY_RESUME입니다. 해당 프로세스가 시스템 콜을 실행한 다음 커널에서 실행 중이란 의미입니다.
[https://elixir.bootlin.com/linux/v3.18.71/source/arch/arm/include/asm/thread_info.h] 
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */

유저 레벨 프로세스는 시스템 콜 실행 후 유저 모드로 돌아갑니다.

3번째 줄을 봅시다.
3    (int) preempt_count = 2097155 = 0x00100003,

preempt_count 멤버에 0x00100000 비트가 포함됐으니 인터럽트 컨택스트이고 3이란 값은 현재 프로세스가 스케줄링 가능한 조건임을 의미합니다.

5번 줄을 볼 차례입니다.
5    (struct task_struct *) task = 0xD2FB2B80,

태스크 디스크립터 주소를 저장하는 task 멤버입니다.

7번째 줄 정보는 현재 프로세스가 실행 중인 CPU 번호입니다.
7    (__u32) cpu = 5 = 0x5,

마지막으로 9~20번째 줄 정보를 보겠습니다.
9    (struct cpu_context_save) cpu_context = (
10      (__u32) r4 = 4056868736 = 0xF1CEE780,
11      (__u32) r5 = 3539676032 = 0xD2FB2B80,
12      (__u32) r6 = 3664491200 = 0xDA6BB2C0,
13      (__u32) r7 = 3688475136 = 0xDBD9AA00,
14      (__u32) r8 = 3248547128 = 0xC1A0E538,
15      (__u32) r9 = 3496897024 = 0xD06E6A00,
16      (__u32) sl = 3539677168 = 0xD2FB2FF0,
17     (__u32) fp = 3461324268 = 0xCE4F9DEC,
18      (__u32) sp = 3461324200 = 0xCE4F9DA8,
19      (__u32) pc = 3237955160 = 0xC0FF4658,
20      (__u32 [2]) extra = ([0] = 0 = 0x0, [1] = 0 = 0x0)),

cpu_context 멤버가 컨택스트 스위칭 전 실행했던 레지스터 세트 정보를 저장합니다.

이번 절에는 struct thread_info 구조체를 소개했습니다. 다음 절에서는 struct thread_info 멤버가 어떤 코드에서 변경되며 커널 입장에서 세부 동작을 살펴보겠습니다.

4.8.3 실행 중인 CPU 번호 확인하기
struct thread_info 필드 중 cpu는 현재 프로세스가 실행 중인 cpu 번호를 의미합니다.

현재 코드가 어떤 CPU에서 구동 중인지 알려면 어떤 함수를 써야 할까요? 리눅스 커널에서는 smp_processor_id() 이라는 매크로를 제공합니다.

이 매크로의 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/smp.h]
1 # define smp_processor_id() raw_smp_processor_id()

[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/smp.h]
2 #define raw_smp_processor_id() (current_thread_info()->cpu)

1~2번째 줄 코드를 보면 smp_processor_id() 함수는 raw_smp_processor_id() 함수로 치환되는데, raw_smp_processor_id() 함수를 보면 current_thread_info()->cpu 멤버를 반환합니다.

current_thread_info() 란 함수는 실행 중인 함수 스택 주소를 읽어서 프로세스 스택 최상단 주소를 얻어옵니다.

커널 곳곳에 smp_process_id() 란 함수를 써서 실행 코드가 어떤 CPU에서 실행 중인지 점검합니다. 예제 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
1void resched_curr(struct rq *rq)
2 {
3 int cpu;
...
4 cpu = cpu_of(rq);
5
6 if (cpu == smp_processor_id()) {
7 set_tsk_need_resched(curr);
8 set_preempt_need_resched();
9 return;
10 }

4번째 줄 코드를 보면 함수 인자인 런큐 구조체에서 런큐 CPU 번호를 cpu란 지역 변수에 저장합니다. 

6번째 줄 코드를 보면 smp_processor_id() 이란 함수를 호출해서 현재 wake_up_idle_cpu() 함수가 어떤 CPU에서 실행 중인지 확인합니다. 현재 실행 중인 CPU 번호와 런큐 CPU 번호가 같으면 7~8번째 줄 코드를 실행하고 함수 실행을 끝냅니다.

6번째 줄 코드 매크로를 풀어서 실제 동작하는 코드로 바꾸면 주석문 코드와 같습니다.
if (cpu == current_thread_info()->cpu) {

이 동작을 그림으로 표현하면 다음과 같습니다.
 

위 함수 흐름은 라즈베리파이 SDIO 드라이버에서 워크 핸들러인 brcmf_sdio_dataworker() 함수를 실행합니다. 이 후 __wake_up() 함수를 호출해서 프로세스 스케줄링 요청을 수행하는 resched_curr() 함수를 실행합니다.

resched_curr() 함수에서 smp_processor_id() 함수를 호출하면 실행 중인 프로세스 스택 최상단 주소에 접근해서 cpu 란 멤버를 읽어 오는 겁니다.

이번에는 언제 struct thread_info.cpu 멤버에 현재 실행 중인 CPU 번호를 저장하는지 점검합시다.
 [https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c]
void set_task_cpu(struct task_struct *p, unsigned int new_cpu)
{
...
__set_task_cpu(p, new_cpu);
}

[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/sched.h]
static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
{
...
task_thread_info(p)->cpu = cpu;

위 코드와 같이 set_task_cpu() 함수를 실행하면 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 cpu 멤버에 실행 중인 cpu 인자 정수값을 저장합니다.

다음 그림은 set_task_cpu() 함수가 동작하는 흐름입니다.
 
set_task_cpu() 함수를 호출하면 __set_task_cpu() 함수를 호출해서 프로세스 스택 최상단 주소 접근 후 struct thread_info 구조체 cpu 멤버에 cpu 번호를 저장합니다.

4.8.4 struct thread_info 초기화 코드 분석
이번 소절에서는 struct thread_info를 초기화하는 코드를 분석하겠습니다. 

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

dup_task_struct() 함수 주요 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/fork.c#L511]
1 static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
2 {
3 struct task_struct *tsk;
4 unsigned long *stack;
5 struct vm_struct *stack_vm_area;
6 int err;
...
7 tsk = alloc_task_struct_node(node);
8 if (!tsk)
9 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);

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() 함수를 호출해서 스택 메모리 공간을 할당 받습니다.
프로세스가 생성할 때 스택 공간을 할당 받으며 그 크기는 정해져 있습니다.

ARM32 아키텍처는 프로세스 스택 크기가 0x2000입니다. 

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

14번째 줄 코드를 볼 차례입니다.
14 tsk->stack = stack;

struct task_struct.task 이란 멤버에 할당 받은 스택 포인터를 저장합니다.
이제 할당 받은 스택 최상단 주소를 태스크 디스크립터에 설정하는 겁니다.

마지막으로 15번째 줄 코드를 보겠습니다.
15 setup_thread_stack(tsk, orig);

setup_thread_stack() 함수를 호출해서 태스트 디스크립터 주소를 current_thread_info() 구조체에 저장합니다.

setup_thread_stack() 함수를 보면서 어떤 설정을 하는지 조금 더 짚어 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/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 }

4번째 줄 코드를 보겠습니다. task_thread_info() 함수를 호출해서 프로세스 스택 최상단 주소로 struct thread_info 구조체를 읽어 옵니다.

4.8.5 struct thread_info 디버깅
Trace32로 struct thread_info 구조체를 확인하면 다음과 같습니다.
1   (struct thread_info *) (struct thread_info*)0xAE4F8000 = 0xAE4F8000 -> (
2    (long unsigned int) flags = 0x2,
3    (int) preempt_count = 0x00200003,
4    (mm_segment_t) addr_limit = 0xBF000000,
5    (struct task_struct *) task = 0xA2FB2B80,
6    (__u32) cpu = 0x0,
7    (__u32) cpu_domain = 0x0,

5번째 줄 정보를 보면 task이란 멤버가 0xA2FB2B80 주소를 저장하고 있습니다.
이 주소가 태스크 디스크립터 구조체 위치입니다.

이번에는 태스크 디스크립터 주소인 0xA2FB2B80에 접근해서 스택 주소를 점검합시다.
1    (struct task_struct *) task = 0xA2FB2B80 -> (
2      (long int) state = 0x0,
3      (void *) stack = 0xAE4F8000,
4      (atomic_t) usage = ((int) counter = 0x0),

3 번째 줄 정보를 보면 stack이란 필드에 스택 최상단 주소인 0xAE4F8000 가 저장돼 있습니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
혹시 글을 읽고 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려 드리겠습니다!"


Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적 


#Reference 시스템 콜

Reference(워크큐)
워크큐(Workqueue) Overview


핑백

덧글

댓글 입력 영역