Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


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

유저 프로세스 실행 도중 인터럽트가 발생하면 __irq_usr 이란 레이블에서 선점 스케줄링 조건을 점검합니다. 마찬가지로 커널 모드에서 어떤 코드가 실행 도중 인터럽트가 발생할 때 선점 스케줄링(Preemptive Scheduling)을 시도합니다.

이번 소절에서 커널 모드에서 인터럽트가 발생했을때 선점 스케줄링 세부 동작을 살펴봅니다.

다음 블록 다이어그램을 같이 봅시다.
 
커널 모드에서 코드 실행 중 인터럽트가 발생하면 __irq_svc 이란 인터럽트 벡터로 프로그램 카운터를 브랜치합니다.

위 블록 다이어그램은 커널 모드에서 프로세스가 실행 도중 선점 스케줄링되는 흐름도입니다.
커널 모드에서 선점 스케줄링은 다음 과정으로 실행합니다.
  1. 인터럽트가 발생해서 __irq_svc 인터럽트 벡터 실행
  2. 인터럽트 핸들러 실행으로 인터럽트 핸들링 마무리
  3+4. __irq_svc 레이블에서 프로세스 struct thread_info 필드가 2가지 조건을 만족하면 
   svc_preempt 레이블로 브랜치
     - preempt_count: 0
     - flags: _TIF_NEED_RESCHED
5. preempt_schedule_irq() 함수와 __schedule() 함수 순서로 호출해서 스케줄링 실행

커널 모드에서 인터럽트가 발생했을 때 선점 스케줄링 과정을 리뷰했으니 이제 코드 분석으로 세부 동작을 알아봅시다.

먼저 볼 코드는 __irq_svc 레이블입니다. 
[https://elixir.bootlin.com/linux/v4.14.49/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
#endif
12 svc_exit r5, irq = 1 @ return from exception

사실 라즈베리파이에서 CONFIG_PREEMPT 컨피그가 꺼져 있어 5~11 번째 줄 코드는 컴파일되지 않습니다. 커널 코드가 실행 도중 인터럽트가 발생했을 때 선점 스케줄링을 시도하지 않겠다는 의도입니다.

그런데 대부분 리눅스 상용 시스템에서 CONFIG_PREEMPT 컨피그가 켜져 있습니다. 따라서 이번 소절에선 CONFIG_PREEMPT 가 켜져 있는 코드 기준으로 분석을 진행하겠습니다.

6~7번째 줄 코드부터 봅시다.
6 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
7 ldr r0, [tsk, #TI_FLAGS] @ get flags

프로세스 struct thread_info 구조체 preempt_count필드를 r8 레지스터 그리고 flags 필드를 r0레지스터에 저장합니다.

이번에 8~9번째 줄 코드를 봅시다.
8 teq r8, #0 @ if preempt count != 0
9 movne r0, #0 @ force flags to 0

r8에는 프로세스 struct thread_info preempt_count 필드가 저장돼 있습니다. r8 레지스터가 값이 0이면 r0 레지스터를 0으로 강제로 바꿔 버립니다.

만약 r8이 0이 아니면 r0 레지스터는 값을 변경하지 않습니다. 

r8이 thread_info_preempt_count_r8이고 r0이 thread_info_flags_r0이면 다음과 같이 어셈블리 코드를 C 언어 형태로 바꿀 수 있습니다.
if (thread_info_preempt_count_r8 == 0 ) {
if (thread_info_flags_r0 == _TIF_NEED_RESCHED) {
 svc_preempt();
}
else {
thread_info_flags_r0 = 0;
}

struct thread_info 구조체 기준으로 preempt_count 필드가 0이어야 flags 필드가 _TIF_NEED_RESCHED(2) 인지 점검하는 것입니다.

정리하면 현재 실행 중인 프로세스 struct thread_info 필드가 다음 조건을 만족하면 선점 스케줄링을 실행하는 것입니다.
1. preempt_count가 0
2. flags가 _TIF_NEED_RESCHED(2)

svc_preempt 레이블 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.14.49/source/arch/arm/kernel/entry-armv.S]
1 svc_preempt:
2 mov r8, lr
3 1: bl preempt_schedule_irq @ irq en/disable is done inside

svc_preempt 레이블을 특별한 동작을 수행하지 않습니다. 3번째 줄 코드와 같이 preempt_schedule_irq() 함수를 호출합니다.

이어서 preempt_schedule_irq() 함수 코드를 분석하겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/kernel/sched/core.c#L3596]
1 asmlinkage __visible void __sched preempt_schedule_irq(void)
2 {
3 enum ctx_state prev_state;
4
5 /* Catch callers which need to be fixed */
6 BUG_ON(preempt_count() || !irqs_disabled());
7
8 prev_state = exception_enter();
9
10 do {
11 preempt_disable();
12 local_irq_enable();
13 __schedule(true);
14 local_irq_disable();
15 sched_preempt_enable_no_resched();
16 } while (need_resched());

preempt_schedule_irq() 함수를 보면 13번째 줄 코드에서 __schedule() 함수를 호출합니다.

선점 스케줄링 시 콜스택 소개
이번에는 Trace32 프로그램으로 이번 소절에서 분석한 함수들의 호출 흐름을 소개합니다.
-000|__schedule()
-001|preempt_schedule_irq()
-002|svc_preempt(asm)
-003|__irq_svc(asm)
 -->|exception
-004|blk_flush_plug_list()
-005|current_thread_info(inline)
-005|blk_finish_plug()
-006|ext4_writepages()
-007|__filemap_fdatawrite_range()
-008|filemap_write_and_wait_range()
-009|ext4_sync_file()
-010|vfs_fsync()
-011|fdput(inline)
-011|do_fsync()
-012|ret_fast_syscall(asm)

__schedule() 함수에 브레이크 포인트를 걸고 확인한 콜스택입니다. 콜스택으로 인터럽트 발생 후 선점 스케줄링을 시도합니다.

004번째 콜스택을 보면 blk_flush_plug_list() 함수 실행 중이었습니다.
003번째 콜스택에 __irq_svc() 이란 레이블이 보이니 인터럽트가 발생했음을 알 수 있습니다.

이후 svc_preempt() -> preempt_schedule_irq() -> __schedule() 흐름으로 선점 스케줄링을 실행합니다.

이번엔 다른 리눅스 시스템에서 커널 모드에서 선점 스케줄링을 실행할 때 확보한 ftrace 콜스택 로그를 소개합니다.
01 sh-1359 [000] d.h1 125.454337: irq_handler_entry: irq=18 name=arch_timer
02 sh-1359 [000] dnh1 125.454363: irq_handler_exit: irq=18 ret=handled
03 sh-1359 [000] dn.1 125.454368: preempt_schedule_irq+0x10/0x7c <-svc_preempt+0x8/0x18
04 sh-1359 [000] dn.1 125.454397: <stack trace>
05  => preempt_schedule_irq+0x14/0x7c
06 => svc_preempt+0x8/0x18
07 => user_path_at_empty+0x50/0x58
08 => vfs_fstatat+0x60/0xa0
09 => vfs_stat+0x28/0x2c
10 => SyS_stat64+0x24/0x40
11 => ret_fast_syscall+0x0/0x3c

02 번째 메시지를 보면 인터럽트 핸들링을 마무리한 시점임을 알 수 있습니다.
02 sh-1359 [000] dnh1 125.454363: irq_handler_exit: irq=18 ret=handled

이 후 이번 소절에서 분석한 함수를 볼 수 있습니다.
함수 호출 방향은 11번 째 줄에서 03 번째 줄입니다.

이 콜스택에서 실행 중에 선점 당하는 코드는 다음과 같습니다.
06 => svc_preempt+0x8/0x18 
07 => user_path_at_empty+0x50/0x58

user_path_at_empty() 함수 시작 주소 기준으로 +0x50 만큼 떨어진 주소 코드가 
실행 도중 선점 스케줄링이 실행했다는 것입니다.

로그 뒤에 숨겨진 세부 정보까지 이끌어내면 다음과 같이 해석할 수 있습니다.
1. user_path_at_empty+0x50/0x58 코드 실행 중 arch_timer 인터럽트 발생
2. arch_timer 인터럽트 핸들링 마무리
3. __irq_srv 레이블로 다시 복귀
4. sh-1359 프로세스의 struct thread_info 구조체 flags와 preempt_count 필드 점검 후 다음 조건 만족
 - flag == _TIF_NEED_RESCHED;
 - preempt_count == 0;
5. svc_preempt 레이블 실행

커널 코드 분석으로 선점 스케줄링에 대해 파악하고 나니 로그가 쉽게 보이는 것 같습니다.

이번 소절에서 분석한 내용을 정리합시다.
1. 인터럽트 발생(커널 모드에서 실행 중인 프로세스)
2. 인터럽트 핸들러 실행
3. 프로세스 struct thread_info 구조체 preempt_count가 0이고 _TIF_NEED_RESCHED 이면 svc_preempt 레이블 실행
4. svc_preempt 레이블에서 preempt_schedule_irq() 함수를 호출해서 __schedule() 함수 호출

3번 단계가 선점 스케줄링을 실행할 조건을 점검하는 동작입니다.


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

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

Reference(프로세스 스케줄링)

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

핑백

덧글

댓글 입력 영역