Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널][스케줄링] 유저 프로세스 실행 중 인터럽트 발생으로 선점 스케줄링 10. Process Scheduling

선점 스케줄링 실행 진입점 중 하나가 인터럽트를 핸들링 후 입니다. 

이번 소절에서 유저 프로세스가 실행하던 도중 인터럽트가 발생했을 때 어떤 방식으로 선점 스케줄링(Preemption)이 시작하는지 살펴보겠습니다.

다음 블록 다이어그램을 같이 봅시다.
 

위 블록 다이어그램은 유저 레벨 프로세스가 실행 도중 선점 스케줄링되는 흐름도입니다.
유저 레벨 프로세스 실행 도중 선점 스케줄링은 다음 과정으로 실행합니다.
  1. 인터럽트가 발생해서 __irq_usr란 인터럽트 벡터 실행
  2. 인터럽트 핸들러 실행으로 인터럽트 핸들링 마무리
  3. __irq_usr 레이블에서 ret_to_user_from_irq 레이블 실행
  4. 프로세스 struct thread_info 구조체 flags 필드를 점검해서 _TIF_NEED_RESCHED 
   이면 선점 스케줄링 실행

이제부터 유저 프로세스 실행 도중 인터럽트가 발생하면 브랜치하는 __irq_usr 인터럽트 벡터부터 __schedule() 함수까지 실행 흐름을 알아보겠습니다.

유저 공간에서 프로세스 실행 도중 인터럽트가 발생하면 인터럽트 벡터로 __irq_usr 이란 레이블을 실행합니다. 

그러면 __irq_usr 레이블과 __irq_svc 레이블의 차이점은 무엇일까요?
유저 공간에서 어떤 코드가 실행 중에 인터럽트가 발생하면 __irq_usr 레이블을 실행하듯이 커널 공간에서 코드 실행 중 인터럽트가 발생하면 __irq_svc 레이블을 실행합니다.

이렇게 유저 공간과 커널 공간 별 인터럽트 벡터를 구분해서 정의한 이유는 무엇일까요? 유저 공간에서 코드 실행 중 인터럽트가 발생하면 커널 공간인 __irq_usr 레이블을 실행한 다음 유저 공간으로 복귀해야 하기 때문입니다.

하지만 커널 공간 코드 실행 중 인터럽트가 발생하면 __irq_svc 레이블을 실행한 다음 기존에 실행 중인 코드(커널 공간)로 복귀를 하면 됩니다.

__irq_usr 레이블 코드를 보면서 선점 스케줄링을 실행 과정을 살펴봅시다.
__irq_usr 레이블 동작은 크게 3단계로 구분할 수 있습니다.

1단계: 인터럽트 핸들링
발생한 인터럽트에 대한 처리를 합니다. 인터럽트 종류에 따라 인터럽트 핸들러를 실행하는 겁니다.

2단계: 선점 스케줄링 실행 조건 점검
현재 실행 중인 프로세스의 struct thread_info flags 필드에 접근한 후 선점 스케줄링을 실행할 조건을 점검합니다. 만약 flags 필드가 _TIF_NEED_RESCHED이면 선점 스케줄링을 시작합니다.

3단계: 유저 공간 복귀
만약 선점 스케줄링을 실행할 조건이 아니면 유저 공간으로 복귀합니다. 유저 공간으로 복귀하는 코드는 아키텍처에 의존적인 코드로 구성돼 있습니다. 라즈베리파이는 ARM 프로세서를 탑재했으니 ARM 어셈블리 코드로 구성돼 있습니다.

이번 소절은 선점 스케줄링 동작에 초점을 맞추고 위에서 명시한 3단계 중에 1~2단계에 초점을 맞춰서 코드를 분석하겠습니다.

먼저 유저 공간에서 프로세스 실행 도중 인터럽트가 발생하면 실행하는 __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번째 줄 코드를 보겠습니다.
5 get_thread_info tsk

현재 프로세스 스택 주소를 통해 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.70/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
8 no_work_pending:
9 asm_trace_hardirqs_on save = 0
10
11 /* perform architecture specific actions before user return */
12 arch_ret_to_user r1, lr
13 ct_user_enter save = 0
14
15 restore_user_regs fast = 0, offset = 0
16 ENDPROC(ret_to_user_from_irq)

선점 스케줄링을 실행할지 점검하는 코드는 5~7 번째 줄입니다. 이번 소절에서 가장 중요한 코드라 할 수 있습니다.

어렵게 보이는 어셈블리 코드를 C 코드로 쉽게 풀면 다음과 같습니다.
 

C 코드에서 current_thread_info() 함수는 프로세스 스택 최상단 주소에 있는 struct thread_info 구조체 주소를 의미합니다. current_thread_info()->flags를 _TIF_WORK_MASK와 AND 비트 연산한 다음 결과가 1이면 slow_work_pending() 레이블을 호출하는 것입니다.

선점 스케줄링 관점으로 current_thread_info()->flags 값이 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블로 브랜치합니다. 이 코드의 목적은 선점 스케줄링 실행 여부를 점검하는 것입니다.

ret_to_user_from_irq 레이블 동작에 대해 리뷰를 했으니 어셈블리 코드를 보겠습니다.

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

struct thread_info 구조체에서 flags 필드를 r1 레지스터에 로딩하는 명령어입니다.

 TI_FLAGS는 다음 코드와 같이 struct thread_info 구조체에서 flags 필드가 위치한 주소 오프셋을 의미합니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/arch/arm/kernel/asm-offsets.c]
DEFINE(TI_FLAGS, offsetof(struct thread_info, flags));


다음 r1(current_thread_info()->flags) 레지스터와 와 _TIF_WORK_MASK 매크로와 비트 AND 연산을 수행합니다.
6 tst r1, #_TIF_WORK_MASK
7 bne slow_work_pending
8 no_work_pending:
9 asm_trace_hardirqs_on save = 0
10
11 /* perform architecture specific actions before user return */
12 arch_ret_to_user r1, lr
13 ct_user_enter save = 0
14
15 restore_user_regs fast = 0, offset = 0

_TIF_WORK_MASK 매크로와 r1 레지스터를 AND 비트 연산한 결과가 1이면 slow_work_pending 레이블을 브랜치하는 동작입니다.

만약 _TIF_WORK_MASK 매크로와 r1 AND 비트 연산 결과가 0이면 slow_work_pending 레이블을 실행하지 않습니다. 4 번째 줄 코드와 같이 no_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 비트 연산한 결괏값(0xF)입니다.

프로세스의 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)

위 정의문 중에서 _TIF_NEED_RESCHED는 TIF_NEED_RESCHED만큼 왼쪽 비트 시프트한 결과입니다. 이 기준으로 각각 매크로의 실제 값을 계산한 결과는 다음과 같습니다.
#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 )

각각 정의문 결과값을 바탕으로 _TIF_WORK_MASK 매크로는0xF임을 알 수 있습니다.
_TIF_SIGPENDING | _TIF_NEED_RESCHED | _TIF_NOTIFY_RESUME | _TIF_UPROBE
0x1 | 0x2 | 0x4 | 0x8
0xF

여기까지 ret_to_user_from_irq 레이블에서 가장 중요한 코드를 분석했습니다. 선점 스케줄링 동작 기준으로 정리하면 프로세스 struct thread_info 구조체 flags필드가 _TIF_NEED_RESCHED 이면 slow_work_pending 레이블을 실행합니다.

다음 slow_work_pending 레이블을 실행하면 schedule() 함수를 호출하게 됩니다..

이번에 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

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

다음 3번째 줄 코드에서 do_work_pending() 함수로 브랜치합니다.

bl 명령어는 어셈블리 코드에서 지정한 주소나 함수로 프로그램 카운터를 바꾼다는 의미입니다.
어셈블리 코드에서 “bl [함수]” 명령어가 보이면 [함수]를 호출하는 동작입니다.

이번에는 이어서 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() 함수를 호출합니다. 즉 스케줄링을 실행하는 것입니다.

이번 소절에서 다룬 분석한 내용을 다음과 정리합시다.
1. 인터럽트 발생
  : 유저 공간에서 실행 중인 프로세스이므로 __irq_usr 인터럽트 벡터 실행
2. 인터럽트 핸들러 실행
: 인터럽트 핸들러를 호출합니다.
3. 선점 스케줄링 시작 조건 점검
: 프로세스 struct thread_info 구조체 flags 필드가 _TIF_NEED_RESCHED 이면 
  slow_work_pending 레이블을 호출
4. slow_work_pending 레이블에서 do_work_pending() 함수를 호출해서 스케줄링 실행

인터럽트가 발생할 때 마다 선점 스케줄링 조건을 점검한다는 내용을 파악했습니다. 

여기서 한 가지 의문이 생깁니다. 
과연 리눅스 시스템에서 인터럽트가 얼마나 자주 발생할까요? 
이번 장 디버깅 절에서 라즈베리파이에서 1초에 인터럽트가 발생하는지 측정을 합니다. 라즈베리파이에서 Terminal 프로그램만 띄운 상태에서 다른 프로그램을 실행하지 않고 가만히 있으면 714번, 크롬 브라우저를 띄우고 유투브 동영상을 4개를 플레이 시킬 때는 2949 번 발생합니다.

라즈베리파이에서 아무런 액션을 취하지 않아도 인터럽트는 1초에 714번 발생하니, 유저 프로세스의 struct thread_info 구조체 flags 필드가 _TIF_NEED_RESCHED 이면 바로 선점 스케줄링됩니다.

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

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

Reference(프로세스 스케줄링)

스케줄링 소개
프로세스 상태 관리
   어떤 함수가 프로세스 상태를 바꿀까?
스케줄러 클래스
런큐
CFS 스케줄러
   CFS 관련 세부 함수 분석  
선점 스케줄링(Preemptive Scheduling)   
프로세스는 어떻게 깨울까?
스케줄링 핵심 schedule() 함수 분석
컨택스트 스위칭
스케줄링 디버깅
   스케줄링 프로파일링
     CPU에 부하를 주는 테스트   
     CPU에 부하를 주지 않는 테스트 

핑백

덧글

댓글 입력 영역