IT/Linux Kernel

디버깅을 통해 배우는 리눅스 커널의 구조와 원리 1, 도서 공부하기 16 - 스레드 정보(thread_info)

변화의 물결1 2025. 4. 20. 01:05

 

 

안녕하세요.

 

 이전 글에서는 프로세스 속성을 관리하는 자료구조인 태스크 디스크립터를 확인했습니다. 이번 글에서는 프로세스 실행동작은 관리하는 thread_info에 대해서 확인합니다.

 

 이번 부분은 내용이 조금 많아서, 빠르게 요약하면서 따라가 보겠습니다. 그래서 지루할 수 있습니다.

 


 

1. thread_info 구조체란?

 

 태스크 디스크립터(task_struct)는 프로세스의 공통 속성 정보를 저장, 관리를 합니다. 

 thread_info 구조체는 프로세스의 세부 실행 정보를 저장하거나 로딩하는 자료구조를 관리합니다.

 

 핵심 실행정보 저장하는 하는 내용은

 - 선점 스케줄링 실행 여부

 - 시그널 전달 여부

 - 인터럽트 컨텍스트와 Soft IRQ 컨텍스트 상태

 - 휴먼 상태로 진입하기 직전 레지스터를 로딩 및 백업

 

 세부 실행정보 저장하는 하는 내용은

 - 컨텍스트 정보

 - 스케줄링 직전 실행했던 레지스터 세트

 - 프로세스 세부 실행 정보

 

 thread_info 구조체는 프로세스 스택의 최상단 주소에 있으며, 프로세스마다 1개의 구조체가 있게 되는 것입니다.

디버깅을 통해 배우는 리눅스 커널의 구조와 원리, wikibook, Linux Kernel, Raspberry Pi, 라즈베리 파이, thread_info

 CPU 아키텍처(Architecture)마다 다를 수 있습니다. 여기서는 라즈베리 파이의 ARMv7만 확인합니다.

 

linux# vim arch/arm/include/asm/thread_info.h

 

 

 

 thread_info 구조체에서 관리하는 커널의 세부 동작은

 - 현재 실행 중인 코드가 인터럽트 컨텍스트인지 여부

 - 현재 프로세스가 선점 가능한 조건인지 점검

 - 프로세스가 시그널을 받았는지 여부

 - 컨텍스트 스케줄링 전후로 실행했던 레지스터 세트를 저장하거나 로딩

 

 

2. thread_info 구조체 세부 내용 확인

 

 프로세스 스케줄링을 실행할 때 이전에 실행했던 레지스터 세트 정보와 프로세스의 컨텍스트 정보를 구조체 필드에서 확인할 수 있습니다.

 

1) 프로세스 동작을 관리

 

프로세스 동작을 관리하는 필드이며 동작 플래그(flag)를 저장합니다.

unsigned long  flags;

 

동작별 선언된 플래그는 아래와 같고 왼쪽 비트 시프트 연산 결과가 저장되며, 시프트 연산의 결괏값으로 플래그 값을 통해서 플래그 조건을 수시로 확인합니다. 간단하게 몇 가지를 보면

 

 _TIF_SIGPENDING : 프로세스에 시그널이 전달됐는지

 _TIF_NEED_RESCHED : 프로세스가 선점될 조건인지

 _TIF_SYSCALL_TRACE : 시스템 콜 트레이스 조건인지

 

 

 

2) 프로세스의 컨텍스트 정보 저장

 

int preempt_count;

프로세스 컨텍스트(인터럽트 컨텍스트, Soft IRQ 컨텍스트) 실행 정보와 프로세스가 선점 스케줄링될 조건을 저장합니다. thread_info에서 가장 중요한 필드라고 합니다. 

 

 

3) 실행 중인 CPU 번호 저장

  

  __u32 cpu

 프로세스가 실행 중인 CPU 번호를 저장하는 필드이며, raw_smp_process_id() 함수를 호출하면 알 수 있습니다.

 

 

4) 프로세스의 속성 저장

 

 struct task_struct  *task;

 실행 중인 프로세스의 태스크 디스크립터 주소를 저장하며 thread_info 구조체 주소만 알면 task 필드를 통해 task_struct의 주소를 알 수 있습니다.

 

 

5) 프로세스가 실행된 레지스터 세트 정보

 

 struct cpu_context_save  cpu_context;

 프로세스 컨텍스트 정보이며 스케줄링되기 전 실행했던 레지스터 세트 저장하는 필드입니다. 프로세스가 스케줄링되고 다시 실행될 때 cpu_context 필드에 저장된 레지스터를 프로세스 레지스터 세트로 로딩합니다. r4에서 pc필드는 ARM코어 레지스터를 의미합니다.

 

40줄은 스택 포인터(sp) 주소이고 41줄은 프로그램 카운트(pc)입니다.

 

 

 

 cpu_context 필드를 사용하는 코드는 컨텍스트 스위칭을 수행하는 switch_to()와 세부동작함수 __switch_to 레이블에 어셈블리코드로 구현되어 있습니다.

 

linux# vim arch/arm/kernel/entry-v7m.S

 

 103줄은 현재 실행 중인 프로세스 레지스터 세트를 thread_info 구조체에 저장합니다.

 113줄은 스케줄링 실행으로 다시 실행할 프로세스는 thread_info 구조체의 cpu_context에 저장된 레지스터 세트 값을 ARM 레지스터로 불러옵니다.

 

 

 

3. thread_info 구조체의 주소 위치

 

 책의 내용을 빌리자면, 프로세스 스택의 최상단 주소는 0x80C0 0000이고, 스택의 최하단 주소는 0x80C0 2000입니다. 앞에서 이야기한 것처럼 프로세스의 최상단에 위치하므로 시작주소와 동일한 값을 가집니다.

 

 즉, thread_info 구조체는 프로세스 최상단에 위치하며, 프로세스마다 1개씩 있습니다.

 여기서, 하단 주소가 0x80C0 2000 이유는 ARM(32bit) 아키텍처에서 프로세스가 실행되는 스택의 크기는 0x2000바이트로 고정되어 있기 때문입니다.

 

 

 

 위의 예제에서 보여주는 것처럼 최하단 주소에서 상단 방향으로 실행되며,

__wake_up_common_lock() 함수가 실행을 하지면 pipe_write() 함수로 복귀하고, 순차적으로 스택 구조로 동작합니다.

 프로세스가 함수 실행을 마치고 이전에 호출했던 함수로 돌아가려고 할 때 스택에 팝(pop)을 합니다. 그런데 sys_write() 함수에는 Push, Pop 하는 내용이 없습니다.

 

linux# vim fs/read_write.c

 

 

 

 sys_write() 함수를 어셈블리어로 보면 힌트가 있습니다. objdump 명령어를 이용해서 확인합니다.

 496725줄에서 스택(sp) 주소를 ip(r12) 레지스터에 저장합니다.

 496726줄에서 push 명령어로 프로세스 스택 공간에  { r4...pc } 레지스터를 푸시합니다. 그리고 lr(r14) 레지스터를 ARM 프로그램 카운터인 PC에 불러오겠다는 의미입니다.

 

 496771줄에서 스택주소를 기준으로 이미 스택주소에 푸시된 {...} 레지스터 값을 다시 불러옵니다.

 

linux# objdump -d ../out/vmlinux | cat -n | grep -A 100 sys_write | head -n 100

 

 

 스택 메모리에 저장된 주소를 ARM 코어의 PC(Program Counter)에 저장하면 PC에 저장된 주소에 있는 어셈블리 코드를 가져와서(fetch) 실행합니다.

 

 

4. 컨텍스트 정보 흐름

 

1) 인터럽트 컨텍스트 설정 시의 함수 흐름

 

  아래 그림을 통해 인터럽트가 발생했을 때 어떤 흐름으로 thread_info 구조체의 preempt_count 필드가 바뀌는지 확인할 수 있습니다.

 

인터럽트 컨텍스트 설정 흐름을 풀어보면,

  (1) __wake_up_common_lock( ) 함수를 실행하는 도중에 인터럽트 발생

  (2) 인터럽트 백터에서 브랜치 되는 __irq_svc 레이블 실행

  (3) 아래와 같은 인터럽트 제어 함수를 호출

    - bcm2836_arm_irqchip_handle_irq( )

    - __handle_domain_irq( )

  (4) __handle_domain_irq( ) 함수에서 irq_enter( ) 매크로 함수를 호출

    (4.1) 프로세스 스택 최상단 주소에 접근한 후 thread_info 구조체의 preempt_count 필드에 HARDIRQ_OFFSET(0x10000) 비트를 더함

  (5) 최상단 주소 방향으로 함수를 계속 호출해서 인터럽트 핸들러 함수인 usb_hdc_irq( ) 함수를 호출

    (5.1) 서브루틴 함수를 실행

 

 

  irq_enter( ) 함수 이후로 호출되는 usb_hcd_irq( )에서 in_interrupt( ) 함수를 호출하면 true 반환하며 실행코드가 인터럽트 컨텍스트인지 확인할 수 있습니다.

 

 

 

2) 인터럽트 컨텍스트 해제 시의 함수 흐름

 

  인터럽트 서비스 루틴을 종료한 다음에 호출하는 irq_exit( ) 함수를 보면

 

 설정 시와 반대 방향으로 함수를 실행하면서 irq_exit( ) 함수를 실행해서 thread_info구조체 preempt_count 필드에 HARDIRQ_OFFSET(0x10000)를 빼는 연산을 수행합니다.

 이후 in_interrupt( ) 함수를 호출하면 false를 반환하며, irq_exit( ) 함수를 호출 이후에는 커널이 인터럽트 컨텍스트가 아니라고 판단할 수 있습니다.

 

 

 

3) Soft IRQ 컨텍스트 설정 시의 함수 흐름

 

 프로세스가 Soft IRQ 서비스를 처리 중이면 preempt_count 필드에 SOFTIRQ_OFFSET 매크로를 저장합니다. 아래 흐름도에서 irq_exit( ) 함수가 Soft IRQ 시작점이며 인터럽트 핸들링이 끝나면 실행을 시작합니다.

 irq_exit( ) 함수에서 Soft IRQ 서비스 요청 여부를 체크한 다음 __do_softirq( ) 호출하고 이 함수에서 프로세스 스택의 최상단 주소에 접근합니다.

 

 

 

linux# vim kernel/softirq.c

 

269줄에 __local_bh_disable_ip 함수 호출하는 부분이 있는데, 이것을 따라가 보면,

 

 

 

 123줄에 preempt_count_add(cnt) 함수는 전달된 인자를 thread_info 구조체의 preempt_count 필드에 더합니다. 여기서는 SOFTIRQ_OFFSET(0x100) 매크로가 됩니다.

 

 

 

4) Soft IRQ 컨텍스트 설정 해제 시의 함수 흐름

 

 Soft IRQ 컨텍스트가 종료됐다는 정보는 __do_softirq( ) 함수에서 Soft IRQ 서비스 핸들러 함수를 호출한 후 설정합니다.

 

  (1) 프로세스 스택 최상단 주소에 접근한다.

  (2) thread_info 구조체의 preempt_count 필드에서 SOFTIRQ_OFFSET(0x100) 비트를 뺀다.

 

 Soft IRQ 컨텍스트가 종료됐다고 업데이트한 후 A구간으로 표시된 함수에서는 in_softirq( ) 함수를 호출하면 false를 반환하며 __local_bh_enable( ) 함수를 호출해서 preempt_count를 빼는 연산을 수행했기 때문입니다.

 

 

 

5. CPU 필드의 흐름

 

 thread_info 구조체의 cpu 필드는 CPU 번호를 저장합니다. 어떤 CPU가 사용하는지 어떻게 확인할 수 있을까요? 바로 smp_processor_id( ) 함수를 사용해서 알 수 있습니다.

 

 smp_processor_id( )는 매크로 타입의 함수이며 raw_smp_processor_id( ) 함수로 치환되고 다시 current_thread_info()->cpu 코드로 치환됩니다.

 

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

 

linux# vim include/linux/smp.h

 

 

linux# vim arch/arm/include/asm/smp.h

 

 

 

 

6. thread_info 구조체 초기화 코드

 

 프로세스가 생성될 때 thread_info 구조체를 초기화하는 과정을 확인해 보겠습니다.

 이전에 fork() 함수를 호출할 때 copy_process() 함수를 호출한다는 것을 알고 있습니다. copy_process() 함수에서는 dup_task_struct() 함수를 호출해서 태스크 디스크립터와 프로세스가 실행될 스택 공간을 만듭니다.

 

 dup_task_struct() 함수에서 호출하는 핵심함수는 아래와 같습니다.

  - alloc_task_struct_node() : 슬럽 할당자로 태스크 디스크립터인 task_struct 구조체를 할당받음

  - alloc_thread_stread_stack_node() : 프로세스 스택 공간을 할당받음

   

linux# vim kernel/fork.c

 

 

807줄에서 태스크 디스크립터를 할당받습니다.

811줄에서 스택 메모리 공간을 할당받습니다. 프로세스가 생성될 때 스택 공간을 할당받으며 그 크기는  ARMv7에서는 0x2000이며, 64비트 기반 ARMv8 아키텍처에서 프로세스 스택 크기는 0x4000입니다. ( 0x2000 = 2 x 0x1000 = 2 x 4k = 8kb

 

 824줄은 task_struct 구조체의 stack 필드에 할당받은 스택의 주소를 저장하며, 할당받은 스택의 최상단 주소를 태스크 디스크립터에 설정합니다.

 

 845줄은 태스크 디스크립터의 주소를 thread_info 구조체의 task 필드에 저장합니다.

 

   

 

setup_thread_stack() 함수를 따라가 보면,

 

linux# vim include/linux/sched/task_stack.h

 

  함수 인자는 부모 프로세스의 태스크 디스크립터(*org)와 생성한 프로세스의 태스크 디스크립터(*p)입니다.

 37줄은 부모 프로세스의 thread_info 구조체의 필드를 자식 프로세스의 thread 구조체 공간에 복사합니다.

 38줄은 thread_info 구조체의 task 필드에 태스크 디스크립터의 주소를 저장합니다.

 

 

 

 

 

 

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

 

 

 

긴 글 지루했을 것이라 생각됩니다.

그렇지만, 힘내 보아요.

 

 

 

감사합니다.

 

 

<참고 자료>

1. [도서] 디버깅을 통해 배우는 리눅스 커널의 구조와 원리 p216~252 , wikibook

 

 

 

 

반응형