Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 프로세스: 프로세스 컨택스트 정보는 어떻게 저장할까? 4. Process Management

리눅스 커널에서는 프로세스 실행 흐름은 컨택스트란 용어로 표현합니다.

컨택스트 종류는 다음과 같습니다.
   1. 프로세스 컨택스트: 프로세스가 스케줄링 될 수 있는 상태
   2. 인터럽트 컨택스트: 인터럽트가 발생한 후 인터럽트 핸들링을 위한 동작 중
   3. Soft IRQ 컨택스트: Soft IRQ 서비스를 실행 중이며 Soft IRQ 서브루틴(하부 함수 흐름) 동작 중

컨택스트 정보는 struct thread_info 구조체 preempt_count 멤버에서 확인할 수 있습니다. 커널은 이 값을 읽어서 컨택스트 종류를 식별합니다.

커널에서는 어떻게 프로세스가 어떤 컨택스트에서 실행하는지 파악할까요?
in_interrupt()와 in_softirq() 매크로 함수를 통해 확인할 수 있습니다. 해당 코드는 현재 실행 중인 코드가 어떤 컨택스트인지 알려주는 목적으로 구현됐습니다. 

4.9.1 컨택스트 소개
이번에는 컨택스트의 의미에 대해 알아봅시다.
컨택스트는 프로세스 실행 그 자체를 의미합니다. 예를 들어 리눅스 커널에서 가장 많이 쓰이는 컨택스트 스위칭은 실행 흐름을 바꾼다고 해석할 수 있습니다.

컨택스트는 어떤 코드로 표현할 수 있을까요? 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 구조체 필드들은 ARM 프로세스 레지스터들을 의미합니다. 컨택스트 스위칭을 수행할 때 마지막에 실행 중인 레지스터 세트를 위 구조체 필드에 저장합니다. 

컨택스트 스위칭을 수행하는 핵심 함수는 switch_to() 이며 세부 동작은 __switch_to 레이블에 어셈블리 코드로 구현되어 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/include/asm/switch_to.h]
#define switch_to(prev,next,last)
do {
__complete_pending_tlbi();
last = __switch_to(prev,task_thread_info(prev), task_thread_info(next));
} while (0)

__switch_to 레이블 구현부 코드를 잠깐 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-v7m.S]
ENTRY(__switch_to)
.fnstart
2 .cantunwind
3 add ip, r1, #TI_CPU_SAVE
4 stmia ip!, {r4 - r11} @ Store most regs on stack
5 str sp, [ip], #4
6 str lr, [ip], #4
7 mov r5, r0
8 add r4, r2, #TI_CPU_SAVE
9 ldr r0, =thread_notify_head
10 mov r1, #THREAD_NOTIFY_SWITCH
11 bl atomic_notifier_call_chain
12 mov ip, r4
13 mov r0, r5
14 ldmia ip!, {r4 - r11} @ Load all regs saved previously
15 ldr sp, [ip]
16 ldr pc, [ip, #4]!
17 .fnend
ENDPROC(__switch_to)

위 코드 4번째 줄 코드에서 실행 중인 레지스터 세트를 struct thread_info.cpu_context를 저장합니다.

스케줄링 실행으로 다시 실행할 프로세스는 자신의 스택 최상단 주소에 접근해서 struct thread_info.cpu_context에 저장된 레지스터 값을 ARM 레지스터로 로딩합니다. 이 동작은 14번째 줄 코드에서 동작합니다.

이렇게 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 멤버 cpu_context에 실행 중인 ARM 레지스터 세트를 저장하는 것입니다. 컨택스트 스위칭할 때 프로세스 실행 정보를 담고 있는 레지스터 세트를 저장하는 세부 동작은 스케줄링 장에서 상세히 다룹니다.

4.9.2 인터럽트 컨택스트 정보 확인하기
인터럽트 컨택스트란 현재 실행 중인 코드가 인터럽트가 발생한 후 호출되는 서브 함수 중 하나란 의미입니다. 즉, 인터럽트에 대한 핸들링을 진행 중인 상태라 볼 수 있습니다.

리눅스 시스템에서 인터럽트는 언제든 발생할 수 있습니다. 인터럽트가 발생하면 프로세스 실행을 멈추고 인터럽트에 해당하는 인터럽트 핸들러를 실행합니다. 리눅스 커널에서는 인터럽트가 발생해서 인터럽트 서비스 루틴을 실행하는 동작을 인터럽트 컨택스트라고 말합니다. 

컨텍스트란 실행 그 자체란 의미라고 설명을 드렸습니다. 이 용어 앞에 인터럽트가 추가됐으니 인터럽트를 핸들링 중인 상태를 의미합니다.

프로세스가 인터럽트 컨택스트인지 어떻게 확인할 수 있을까요? 프로세스 최상단 주소에 있는 struct thread_info 구조체 preempt_count 필드를 읽어 확인합니다.

인터럽트 컨택스트 시작 정보는 어느 함수에서 설정할까?
커널은 인터럽트가 발생 한 후 인터럽트를 핸들링을 시작할 때 struct thread_info 구조체 preempt_count 필드에 인터럽트가 실행 중인 상태를 나타내는 비트를 설정합니다. 커널은 이 비트를 읽어서 인터럽트 컨택스트 유무를 식별합니다. 리눅스 커널에서는 현재 실행 중인 코드가 인터럽트 컨택스트인지 알 수 있는 in_interrupt() 매크로 함수를 제공합니다.

다음 그림을 보면서 인터럽트가 발생했을 때 어떤 흐름으로 struct 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 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 
  멤버에 HARDIRQ_OFFSET(0x10000) 비트 설정
5. 화살표 방향으로 함수를 계속 호출해서 인터럽트 핸들러 함수인 usb_hcd_irq() 함수 호출
  5.1 서브 루틴 함수 실행

위에서 소개한 함수 실행 흐름에서 4.1 단계 이후 실행하는 모든 서브 함수에서 in_interrupt() 함수를 호출하면 0x10000을 반환합니다. 따라서 다음과 같이 if 문과 in_interrupt() 매크로를 함께 쓰면 실행 코드가 인터럽트 컨택스트인지 확인할 수 있습니다.
if (in_interrupt()) {
// 인터럽트 컨택스트 실행 시 제어 코드
}

인터럽트 서비스 루틴 실행을 시작할 때 struct thread_info 구조체 preempt_count 멤버에 HARDIRQ_OFFSET 비트를 더하는 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/hardirq.h]
1 #define __irq_enter()
2 do {
3 account_irq_enter_time(current);
4 preempt_count_add(HARDIRQ_OFFSET);
5 trace_hardirq_enter();
6 } while (0)

4번째 줄 코드를 실행해서 struct thread_info 구조체 preempt_count 멤버에 HARDIRQ_OFFSET(0x10000) 비트를 더합니다..

preempt_count_add(val) 매크로 함수는 프로세스 스택 최상단 주소에 접근해서 preempt_count 멤버에 val인자를 더하는 동작입니다.
#define preempt_count_add(val) __preempt_count_add(val)

인터럽트 컨택스트 종료는 어느 함수에서 설정할까?
이번에는 인터럽트 컨택스트가 끝나는 시점을 어느 코드에서 설정하는지 확인하겠습니다.
이를 위해 인터럽트 서비스 루틴을 종료한 다음에 호출하는 irq_exit() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/softirq.c]
1 void irq_exit(void)
2 {
...
3 account_irq_exit_time(current);
4 preempt_count_sub(HARDIRQ_OFFSET);

4번째 줄 코드를 실행하면 struct thread_info 구조체 preempt_count 필드에 HARDIRQ_OFFSET 비트를 뺍니다.

다음 그림에서 인터럽트 핸들링을 마무리한 다음 irq_exit() 함수를 실행하는 코드 흐름을 확인할 수 있습니다.
 

인터럽트 핸들링을 끝냈으니 다시 검은색 화살 방향으로 함수 호출이 수행됩니다.

irq_exit() 매크로 함수를 실행해서 struct thread_info.preempt_count 필드에서 HARDIRQ_OFFSET 비트를 뺍니다. 그러니 in_interrupt() 매크로는 FALSE를 반환합니다.
irq_exit() 함수 호출 이후에는 리눅스 커널은 인터럽트 컨택스트가 아니라 판단하는 것입니다.
인터럽트 컨택스트에 대한 세부 동작은 5장 인터럽트에서 확인할 수 있습니다.

리눅스 커널에서 실행 중인 코드가 인터럽트 컨택스트인지 점검하는 루틴은 매우 많습니다. 

커널은 인터럽트 컨택스트 실행 여부를 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 preempt_count 멤버에 저장합니다.

현재 실행 중인 코드가 인터럽트 컨택스트인지 식별하는 것을 매우 중요합니다. 인터럽트 컨택스트에서 호출할 수 있는 커널 함수가 제한돼 있기 때문입니다. 예를 들어 스케줄링을 할 수 있는 뮤덱스, 세마포어아 같은 함수를 쓸 수 없습니다. 그 이유는 인터럽트가 발생하면 빨리 코드를 실행해야 하기 때문입니다.

다음 소절에서는 프로세스 struct thread_info 구조체 preempt_count 필드에 Soft IRQ 컨택스트를 어떻게 저장하고 읽는 지 살펴봅니다.

4.9.3 Soft IRQ 컨택스트 정보 확인하기
“어떤 프로세스가 Soft IRQ 컨택스트이다.”란 문장은 현재 실행 중인 코드가 Soft IRQ 서비스를 실행 중이라는 의미입니다.

Soft IRQ 컨택스트 정보 설정는 어디서 설정할까?
인터럽트 컨택스트와 마찬가지로 프로세스가 Soft IRQ를 처리 중인 상태면 프로세스 struct thread_info 구조체 preempt_count 필드에 SOFTIRQ_OFFSET 매크로를 저장합니다.

Soft IRQ가 실행하는 과정을 다음 그림에서 같이 봅시다.
 

Soft IRQ는 인터럽트 핸들링이 끝나면 실행을 시작합니다. Soft IRQ 서비스를 실행하는 __do_softirq() 함수에서 프로세스 스택 최상단 주소에 접근해서 struct thread_info preempt_count 필드에 SOFTIRQ_OFFSET(0x100) 매크로를 설정합니다. SOFTIRQ_OFFSET(0x100)는 Soft IRQ 실행 중 혹은 Soft IRQ 컨택스트임을 알려줍니다.

Soft IRQ 컨택스트 시작을 프로세스 struct thread_info preempt_count 필드에 저장하는 코드를 분석하겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/softirq.c]
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2 {
...
3 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
4           // Soft IRQ 서비스 실행
5 ...
6 void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
7 {
...
8 __preempt_count_add(cnt);

3번째 줄 코드와 같이 __do_softirq() 함수에서 __local_bh_disable_ip() 함수, 
8번째 줄 코드에서 __local_bh_disable_ip() 함수는 __preempt_count_add() 함수를 호출합니다.

__preempt_count_add() 함수는 전달된 인자를 실행 중인 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 필드에 SOFTIRQ_OFFSET 매크로 비트를 더합니다.

Soft IRQ 서비스를 실행하는 서브 함수에서 in_softirq() 매크로를 if 문과 함께 쓰면 현재 실행 중인 코드가 Soft IRQ 컨택스트인지 알 수 있습니다.

Soft IRQ 컨택스트 종료 상태는 어디서 저장할까?
이번에는 Soft IRQ 서비스 함수 호출을 마친 다음에 어떤 동작을 하는지 알아보겠습니다.
해당 코드 흐름은 다음 그림과 같습니다.
 

Soft IRQ 서비스 함수 실행을 완료하면 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 멤버에서 SOFTIRQ_OFFSET(0x100) 비트를 뺍니다.

이 동작은 irq_exit() 함수에서 실행하면 이후 검은색 화살표 방향으로 호출되는 함수에서 in_softirq() 함수는 FALSE를 반환합니다.

 __do_softirq() 함수 마지막 부분 코드를 봅시다. Soft IRQ 실행이 끝났다는 정보를 저장하는 동작입니다.
1 asmlinkage __visible void __softirq_entry __do_softirq(void)
2 {
3
4 // Soft IRQ 서비스 루틴
...
5 lockdep_softirq_end(in_hardirq);
6 account_irq_exit_time(current);
7 __local_bh_enable(SOFTIRQ_OFFSET);
8
9 static void __local_bh_enable(unsigned int cnt)
10 {
...
11 preempt_count_sub(cnt);
}

7번째 줄 코드와 같이 __local_bh_enable() 함수를 호출하고 preempt_count_sub() 함수를 실행합니다.

이번에는 실행 중인 프로세스 스택 최상단 주소에 접근 후 struct thread_info 구조체 preempt_count 멤버에 SOFTIRQ_OFFSET 매크로 비트를 뺍니다.

4.9.4 선점 스케줄링 여부 정보 저장
프로세스는 실행 도중 언제든 선점 스케줄링이 될 수 있습니다. 즉, 언제든 실행을 멈추고 다른 프로세스로 스케즐링 될 수 있습니다. 

물론 프로세스는 스스로 schedule() 함수를 호출해서 명시적으로 스케줄링할 수 있지만 많은 경우 선점 스케줄링됩니다. 선점 스케줄링의 진입점은 다음과 같습니다.
 + 인터럽트 핸들링 후
 + 시스템 콜 핸들링 후

선점 스케줄링 여부를 점검할 때 프로세스 스택 최상단에 접근 후 struct thread_info 구조체 preempt_count 필드 정보를 읽습니다. 이 값을 기준으로 선점 스케줄링을 실행할 지 결정합니다. 

관련 코드를 함께 봅시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-armv.S]
1 __irq_svc:
2 svc_entry
3 irq_handler
4
5 #ifdef CONFIG_PREEMPT
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0
10 tst r0, #_TIF_NEED_RESCHED
11 blne svc_preempt
12 #endif

낯선 어셈블리 코드이지만 차근차근 코드 의미를 분석해겠습니다.

1~3번째 줄 코드를 보겠습니다.
1 __irq_svc:
2 svc_entry
3 irq_handler

__irq_svc 레이블은 인터럽트가 발생했을 때 실행하는 인터럽트 벡터를 의미하며 2~3번째 줄 코드는 인터럽트에 대한 핸들링을 수행합니다. 인터럽트가 발생하면 인터럽트에 대응하면 인터럽트 핸들러 함수를 실행하는 역할을 수행합니다.

6~11번째 줄 코드가 스케줄링을 실행할 지 결정하는 어셈블리 코드입니다.
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0
10 tst r0, #_TIF_NEED_RESCHED
11 blne svc_preempt

어셈블리 코드를 이해하기 쉽게 C 코드로 표현하면 다음과 같습니다.
a    struct thread_info *current_thread_info = task_thread_info(current);
b
c   if (current_thread_info.preempt_count == 0 ) {
d if (current_thread_info.flags & _TIF_NEED_RESCHED) {
e  svc_preempt();  // 스케줄링 실행
f            }
g    else {
h current_thread_info.flags = 0;
i    }

a 코드는 프로세스 스택 최상단 주소에 접근해 struct thread_info 구조체에 접근하며 이 값을 current_thread_info 란 변수에 저장합니다.

preempt_count 멤버가 0이면 flags 멤버와 _TIF_NEED_RESCHED(0x2) 와 AND 비트 연산을 수행해 연산 결과가 true이면 svc_preempt() 함수를 호출해서 스케줄링을 실행하는 것입니다.

struct thread_info 구조체 preempt_count와 flags 멤버는 스케줄링 실행을 결정하는 중요한 정보를 저장하고 있습니다.

이번 소절에서 다룬 내용을 정리하면 preempt_count 멤버는 프로세스 컨택스트와 스케쥴링 가능 정보를 저장합니다.
1. 인터럽트 컨택스트
2. Soft IRQ 컨택스트
3. 프로세스 스케줄링 가능 여부


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

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

#Reference(프로세스 관리)
프로세스 디버깅
   glibc fork 함수 gdb 디버깅


핑백

덧글

댓글 입력 영역