Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

29137
1186
128674


[라즈베리파이] 프로세스 - 스케줄링(Preemption): 유저 공간 실행 중 인터럽트 발생 4. 프로세스(Process) 관리

이번에는 인터럽트가 발생했을때 스케줄링(Preemption)하는 동작을 살펴봅니다.

우리는 그 동안 schedule() 함수를 호출했을때만 스케줄링이 실행하는지 알고 있습니다. 그것은 맞는 말입니다.
그런데, 인터럽트가 발생했을때 스케줄링이 실행된다는 사실은 잘 모릅니다.

이 사실을 알면 많은 레이스 컨디션 발생 원인을 알 수 있습니다.

이제부터 __irq_usr이란 인터럽트 벡터부터 __schedule() 함수가 실행하는 코드 흐름을 알아보겠습니다.

유저 공간에서 프로세스 실행 도중 인터럽트가 발생하면 인터럽트 벡터로 __irq_usr이란 레이블을 실행합니다.
먼저 __irq_usr 코드를 보겠습니다. 인터럽트 처리가 아키텍처에 의존적이니 어셈블리 코드로 구현됐습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
1 __irq_usr:
2 usr_entry
3 kuser_cmpxchg_check
4 irq_handler
5 get_thread_info tsk
6 mov why, #0
7 b ret_to_user_from_irq

4번째 줄 코드는 irq_handler이란 매크로를 실행합니다.
이는 인터럽트 핸들러를 실행하는 코드입니다.

5번째 줄 코드는 현재 프로세스 스택 주소를 통해 struct thread_info 구조체를 읽어오는 동작입니다.
struct thread_info.flags 멤버에 프로세스 스케줄링을 제어하는 필드가 있기 때문입니다.

7번째 줄 코드를 보면 ret_to_user_from_irq 이란 레이블로 branch하는 어셈블리 명령어를 볼 수 있습니다.
어셈블리어로 다음 형식으로 쓰며 C 코드로 함수 호출하는 동작과 같습니다.
b [주소]
b [함수 이름]

ret_to_user_from_irq 레이블을 분석합시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-common.S]
1 ENTRY(ret_to_user_from_irq)
2 ldr r2, [tsk, #TI_ADDR_LIMIT]
3 cmp r2, #TASK_SIZE
4 blne addr_limit_check_failed
5 ldr r1, [tsk, #TI_FLAGS]
6 tst r1, #_TIF_WORK_MASK
7 bne slow_work_pending

먼저 5번째 줄 코드를 보겠습니다.
5 ldr r1, [tsk, #TI_FLAGS]

struct thread_info 구조체에서 flags 멤버를 r1이란 레지스터에 로딩하는 명령어입니다.
참고:다음 코드를 보면 TI_FLAGS 매크로가 struct thread_info 구조체 어떤 오프셋인지 알 수 있습니다.

[https://elixir.bootlin.com/linux/v4.14.43/source/arch/arm/kernel/asm-offsets.c]
DEFINE(TI_FLAGS, offsetof(struct thread_info, flags));

다음은 r1와 _TIF_WORK_MASK 매크로와 AND 연산을 수행합니다.
6 tst r1, #_TIF_WORK_MASK

_TIF_WORK_MASK 매크로와 r1 AND 연산한 결과가 1이면 slow_work_pending 레이블을 실행하는 동작입니다.

이 때 ARM CPSR 레지스터 Z 비트(0x1D3)가 0입니다.
_TIF_WORK_MASK 매크로와 r1 AND 연산한 결과가 0이면 ARM CPSR 레지스터((0x40001D3)) Z 비트가 1이니 slow_work_pending 레이블을 실행하지 않습니다.
tst 명령어는 C 코드에서 if문을 처리할 때 쓰는 어셈블리 명령어이니 잘 알아둡시다.

_TIF_WORK_MASK 매크로 정의를 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/thread_info.h]
#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
 _TIF_NOTIFY_RESUME | _TIF_UPROBE)

_TIF_WORK_MASK 매크로는 _TIF_NEED_RESCHED, _TIF_SIGPENDING, _TIF_NOTIFY_RESUME 그리고 _TIF_UPROBE 매크로를 OR 연산한 값입니다.

프로세스의 struct thread_info->flags 가 _TIF_NEED_RESCHED, _TIF_SIGPENDING, _TIF_NOTIFY_RESUME 그리고 _TIF_UPROBE 값 중 하나이면
slow_work_pending 레이블을 호출한다는 겁니다.
참고:
각각 매크로는 다음 코드에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/include/asm/thread_info.h] 
#define TIF_SIGPENDING 0 /* signal pending */
#define TIF_NEED_RESCHED 1 /* rescheduling necessary */
#define TIF_NOTIFY_RESUME 2 /* callback before returning to user */
#define TIF_UPROBE 3 /* breakpointed or singlestepping */
...
#define _TIF_SIGPENDING (1 << TIF_SIGPENDING)
#define _TIF_NEED_RESCHED (1 << TIF_NEED_RESCHED)
#define _TIF_NOTIFY_RESUME (1 << TIF_NOTIFY_RESUME)
#define _TIF_UPROBE (1 << TIF_UPROBE)

각각 매크로의 실제 값은 다음과 같습니다.
#define _TIF_SIGPENDING 1 ( 1<< 0 )
#define _TIF_NEED_RESCHED 2 ( 1<< 1 )
#define _TIF_NOTIFY_RESUME 4 ( 1<< 2 )
#define _TIF_UPROBE 8 ( 1<< 4 )

유저 공간에서 실행 중인 프로세스 입장에서는 어떤 함수 실행 중 인터럽트가 발생해서 동작을 멈춘 상태였습니다.

스케줄링 동작에 초점을 맞추면 struct thread_info.flags에 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블을 실행합니다.

slow_work_pending 레이블 코드를 볼 차례입니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-common.S]
slow_work_pending:
1 mov r0, sp @ 'regs'
2 mov r2, why @ 'syscall'
3 bl do_work_pending

3번째 줄 코드와 같이 do_work_pending() 함수로 PC(프로그램 카운터)를 변경합니다.

1번째 줄 코드를 보면 r0에 스택 포인터 주소를 저장합니다. ARM 함수 호출 규약에서 r0는 함수 첫 번째 인자를 전달하는 역할을 수행합니다.

이번에는 do_work_pending() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/signal.c#L609]
1 asmlinkage int
2 do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
3 {
4 trace_hardirqs_off();
5 do {
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();
8 } else {

6~7번째 줄 코드를 봅시다.
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();

thread_flags 이란 인자는 struct thread_info.flags에 있는 값입니다.
이 값이 _TIF_NEED_RESCHED이면 schedule() 함수를 호출해서 스케줄링(Preemption)을 실행합니다.

분석한 내용을 정리합시다.
1> 인터럽트 발생(유저 공간에서 실행 중인 프로세스)으로 __irq_usr 이란 인터럽트 벡터 실행
2> 인터럽트 핸들러 실행
3> struct thread_info.flags 값이 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블을 호출
4> slow_work_pending 레이블에서 do_work_pending() 함수를 호출해서 스케줄링 실행

다음에는 커널 모드에서 커널 함수 실행 도중 인터럽트가 발생했을때 스케줄링(Preemption)이 어떻게 동작하는지 알아봅시다.

덧글

댓글 입력 영역