Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

97258
1323
114599


[리눅스커널][스케줄링] 유저 프로세스가 시스템 콜 처리를 마무리한 후 선점 스케줄링 10. Process Scheduling

유저 프로세스는 시스템 콜로 커널과 통신을 합니다. 유저 프로세스는 시스템 콜을 발생해서 시스템 콜 핸들러 함수를 호출합니다. 이후 시스템 콜 핸들러 하부 함수 실행을 마친 후 유저 공간으로 복귀하기 직전에 선점 스케줄링 실행 조건을 점검합니다.

다음 다이어그램을 보면서 세부 동작에 대해 살펴보겠습니다.
 

위 블록 다이어그램은 유저 프로세스가 시스템 콜을 처리한 후 선점 스케줄링되는 흐름도입니다.

선점 스케줄링은 다음 단계로 실행합니다.
  1. 시스템 콜 발생으로 시스템 콜 벡터인 vector_swi 레이블로 브랜치
  2. 시스템 콜 핸들러와 하부 루틴 실행을 마무리한 후 ret_fast_syscall 레이블로 복귀
3. ret_fast_syscall  레이블에서 프로세스 struct thread_info 구조체 flags 필드가 
   _TIF_NEED_RESCHED(2) 인지 점검
   4. _TIF_NEED_RESCHED(2) 이면 slow_work_pending 레이블로 브랜치 
   5. _TIF_NEED_RESCHED(2) 이 아니면 no_work_pending 레이블로 브랜치해서 유저 
      공간 복귀 
6. 차례로 do_work_pending() 함수와 schedule() 함수를 호출해서 스케줄링 실행

유저 프로세스에서 시스템 콜 핸들링을 마무리 한 후 선점 스케줄링 과정을 리뷰했으니 이제 코드 분석으로 세부 동작을 알아봅시다.

여기서 한 가지 의문이 생깁니다. 
시스템 콜 실행을 마무리한 다음 선점 스케줄링을 시도하는이유는 무엇일까요?

이 궁금증을 풀기 위해서 시스템 콜 특징에 대해서 살펴볼 필요가 있습니다. 시스템 콜은 유저 프로세스가 유저 공간에서 커널과 통신을 위해 시스템 콜을 발생합니다. 그런데 시스템 콜 발생 횟수는 유저 프로세스 실행 빈도에 비례합니다. 반대로 유저 프로세스가 개수가 적거나 휴면에 진입한 경우 시스템 콜 발생 횟수도 줄어듭니다.

유저 프로세스 실행 빈도에 비례해서 시스템 콜을 핸들링 하니 시스템 콜 핸들러를 수행한 후 선점 스케줄링을 시도하는 것입니다. 커널 프로세스는 유저 공간과 시스템 콜로 통신을 하지 않으니 유저 프로세스를 위한 선점 스케줄링 진입로라라 볼 수 있습니다.

유저 프로세스에서 시스템 콜 핸들링을 마무리한 다음에 어떤 함수로 복귀할까요? 
C 코드 대신 어셈블리 코드로 구현된 ret_fast_syscall 레이블입니다. 

ret_fast_syscall 레이블 코드를 분석하면서 선점 스케줄링 세부 동작에 대해 알아봅시다.

ret_fast_syscall 레이블 분석
다음 그림은 ret_fast_syscall 레이블에서 시작해서 schedule() 함수까지 호출하는 흐름도 입니다.
 

시스템 콜 핸들러 실행 후 유저 공간까지 복귀하는 과정은 시스템 콜 장을 참고하세요.

선점 스케줄링 관점으로 ret_fast_syscall 레이블 코드를 보겠습니다. 
ret_fast_syscall 레이블의 핵심 코드는 프로세스 struct thread_info 구조체 flag 필드가 _TIF_NEED_RESCHED 인지를 점검하는 루틴입니다. 이 조건을 만족하면 slow_work_pending 레이블과 do_work_pending() 함수와 schedule() 함수를 차례로 호출해서 스케줄링을 실행합니다.

시스템 콜 핸들러 실행 마무리 후 유저 공간으로 복귀하는 출발점은 ret_fast_syscall 레이블입니다. 해당 코드 분석을 시작합시다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-common.S]
ret_fast_syscall:
1 str r0, [sp, #S_R0 + S_OFF]! @ save returned r0
2 disable_irq_notrace @ disable interrupts
3 ldr r2, [tsk, #TI_ADDR_LIMIT]
4 cmp r2, #TASK_SIZE
5 blne addr_limit_check_failed
6 ldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing
7 tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK
8 beq no_work_pending
9 ENDPROC(ret_fast_syscall)
10 tst r1, #_TIF_SYSCALL_WORK
11 bne __sys_trace_return_nosave
12 slow_work_pending:
13 mov r0, sp @ 'regs'
14 mov r2, why @ 'syscall'
15 bl do_work_pending
16 cmp r0, #0
17 beq no_work_pending

먼저 6~8번째 줄 코드를 보겠습니다.
6 ldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing
7 tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK
8 beq no_work_pending

6번째 어셈블리 코드를 분석하겠습니다.
프로세스 스택 최상단 주소를 저장한 r9(tsk) 레지스터를 통해 struct thread_info 첫 번째 필드인 flag를 r1 레지스터로 로딩합니다.

6번째 줄 코드에서 보이는 tsk는 r9 레지스터를 의미합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-header.S]
tsk .req r9 @ current thread_info

시스템 콜이 발생하면 vector_swi 레이블을 실행합니다. 이 때 r9 레지스터에 프로세스 스택 최상단 주소를 저장(푸시)합니다. 이후 시스템 콜 핸들러 서브 루틴을 실행한 다음에 ret_fast_syscall 레이블로 복귀합니다. ret_fast_syscall 레이블에서 이전에 vector_swi 레이블에서 시스템 콜 핸들러를 호출할 때 스택에 푸시한 r9 레지스터를 로딩해서 프로세스 flags 정보를 확인합니다.

flags가 _TIF_SYSCALL_WORK | _TIF_WORK_MASK 매크로가 아닌 경우 no_work_pending 레이블을 실행해서 유저 공간으로 복귀합니다. 

다음10~11번째 줄은 struct thread_info flag 가 _TIF_SYSCALL_WORK | _TIF_WORK_MASK 매크로일 경우 실행하는 코드입니다.
10 tst r1, #_TIF_SYSCALL_WORK
11 bne __sys_trace_return_nosave


flags가 _TIF_SYSCALL_TRACE, _TIF_SYSCALL_AUDIT, _TIF_SYSCALL_TRACEPOINT 그리고 _TIF_SECCOMP 매크로 중 하나 비트로 설정돼 있으면 __sys_trace_return_nosave 레이블을 실행합니다.
[https://elixir.bootlin.com/linux/latest/source/arch/arm/include/asm/thread_info.h]
#define _TIF_SYSCALL_WORK (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_AUDIT | \
   _TIF_SYSCALL_TRACEPOINT | _TIF_SECCOMP)

위 매크로는 현재 시스템이 시스템 콜을 디버깅하도록 설정돼 있다는 의미입니다.

__sys_trace_return_nosave 레이블은 ftrace 시스템 콜 디버깅 정보를 저장하고 유저 공간으로 복귀합니다.
이번에는 _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(2) 이면 slow_work_pending 레이블로 브랜치합니다.

이해를 돕기 위해 여기까지 분석한 어셈블리 코드를 C 코드로 변환하면 다음과 같습니다.
1 if(!(current_thread_info()->flag & (_TIF_SYSCALL_WORK | _TIF_WORK_MASK)) ) {
2 no_work_pending();
3 } else if(current_thread_info()->flag & _TIF_SYSCALL_WORK) {
4 __sys_trace_return_nosave();
5 } else {
6 do_work_pending();
7 }

위 코드를 눈으로 따라가보면 current_thread_info()->flag가 _TIF_WORK_MASK 비트 중 하나일 때만 6 번째 줄 else 문을 실행한다는 사실을 알 수 있습니다.

이번에는 flag 값이 _TIF_WORK_MASK 매크로일 때 실행하는 slow_work_pending 레이블 어셈블리 코드를 분석할 차례입니다.
12 slow_work_pending:
13 mov r0, sp @ 'regs'
14 mov r2, why @ 'syscall'
15 bl do_work_pending
16 cmp r0, #0
17 beq no_work_pending

13번째 줄 코드에서 스택 주소를 r0 레지스터에 저장하고, r2에는 시스템 콜 테이블 주소를 저장합니다.

vector_swi 레이블에서 시스템 콜 주소를 저장한 r8레지스터에 접근하는 동작입니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/arch/arm/kernel/entry-header.S]
tbl .req r8 @ syscall table pointer
why .req r8 @ Linux syscall (!= 0)

위 선언문과 같이 why는 시스템 콜 테이블 주소를 저장하고 있는 r8 레지스터임을 알 수 있습니다.

15번째 줄 코드에서는 do_work_pending() 함수를 호출합니다.

do_work_pending() 함수를 분석하기 전에 이 함수로 전달하는 인자를 점검합시다.
r0: 유저 공간에서 실행된 레지스터가 저장된 스택(커널 프로세스 스택 주소)
r1: struct thread_info 구조체 flags 멤버
r2: 시스템 콜 테이블 주소

어셈블리 코드에서 C 언어로 구현된 함수를 호출할 때는 ARM 함수 호출 규약에 따라 인자를 레지스터에 어떻게 전달하는지 파악할 필요가 있습니다.

이번에는 do_work_pending() 함수 코드를 분석하겠습니다.
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 {
9 if (unlikely(!user_mode(regs)))
10 return 0;
11 local_irq_enable();
12 if (thread_flags & _TIF_SIGPENDING) {
13 int restart = do_signal(regs, syscall);

do_work_pending() 함수를 분석하기 전에 함수에 전달된 인자에 대해 점검합시다.
struct pt_regs *regs;
유저 공간에서 실행된 레지스터가 저장된 스택 주소(커널 프로세스 스택 주소)
 
unsigned int thread_flags;
struct thread_info 구조체 flags 필드

int syscall;
시스템 콜 테이블 주소

함수에 전달된 인자에 대해 살펴봤으니 이제 세부 코드 분석을 시작합시다.
6번째 줄 코드를 먼저 보겠습니다.
6 if (likely(thread_flags & _TIF_NEED_RESCHED)) {
7 schedule();

thread_flags가 _TIF_NEED_RESCHED 이면 schedule() 함수를 호출해서 스케줄링을 실행합니다. 시스템 콜 처리 완료 후 스케줄링되는 동작입니다.

이번 소절에서 분석한 내용을 정리합니다.
1. ret_fast_syscall  레이블에서 프로세스 struct thread_info 구조체 flags 필드가
_TIF_NEED_RESCHED(2) 인지 점검
2.  _TIF_NEED_RESCHED(2) 이면 slow_work_pending 레이블로 브랜치 
3. 차례로 do_work_pending() 함수와 schedule() 함수를 호출해서 스케줄링 실행

선점 스케줄링을 시도하는 시점은 유저 프로세스가 시스템 콜 핸들링을 마무리 한 후 입니다.
프로세스 struct thread_info 구조체 flags 필드가 _TIF_NEED_RESCHED(2) 이면 선점 스케줄링을 실행합니다.


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

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

Reference(프로세스 스케줄링)

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




핑백

덧글

댓글 입력 영역