본문 바로가기

시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리

ARM64 - 프로세스(Process) preempt_disable(), preemption 스케줄(Schedule) 조건 분석

아래 제 블로그에서 ARM32 아키텍처에서 preempt_disable() 매크로 함수를 호출하면 __irq_svc 벡터에서 svc_preempt 함수를 호출하지 않아, preemption이 수행되지 않는다고 확인했어요.
 
이번에는 ARM64(Aarch64) 아키텍처에서는 어떤 코드로 구현 되는지 살펴 볼께요.
 
ARM64(Aarch64) 아키텍처는 ARM32 아키텍처와는 다르게 아예 어떤 코드가 실행이 되던 current_thread_info을 읽어올 수 있는 Instruction을 제공해요.
코드는 아래와 같은데, "mrs" 명령어로 스택 Top 주소를 가져오네요. 
 
current_thread_info()가 호출될 때 속도를 더 빠르게 하기 위해서 ARM64에서 Instruction을 추가해준 것 같아요. 
static inline struct thread_info *current_thread_info(void)
{
   unsigned long sp_el0;
 
   asm ("mrs %0, sp_el0" : "=r" (sp_el0));
 
   return (struct thread_info *)sp_el0;
}
 
참고로, ARM32에서는 아래 코드로 스택 Top 주소를 얻어올 수 있었죠.
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));  // THREAD_SIZE = 0x2000
}
 
커널이 구동될 때 IRQ가 Trigger되면 아래 el1_irq vector가 실행이 되는데요.
아래 코드를 유심히 보면 preempt_count 값을 읽어와서 이 값이 0x0이면 el1_preempt() 함수를 호출해서
preemption을 수행하죠. 동작 원리는 ARM32 아키텍처랑 똑같은 것 같은데요.
 el1_irq:
         kernel_entry 1
         enable_dbg
 #ifdef CONFIG_TRACE_IRQFLAGS
         bl      trace_hardirqs_off
 #endif
 
         get_thread_info tsk
         irq_handler
 
 #ifdef CONFIG_PREEMPT
         ldr     w24, [tsk, #TI_PREEMPT]         // get preempt count
         cbnz    w24, 1f                         // preempt count != 0
         ldr     x0, [tsk, #TI_FLAGS]            // get flags
         tbz     x0, #TIF_NEED_RESCHED, 1f       // needs rescheduling?
         bl      el1_preempt
 1:
 #endif
 #ifdef CONFIG_TRACE_IRQFLAGS
         bl      trace_hardirqs_on
 #endif
         kernel_exit 1
 ENDPROC(el1_irq)
.
세부 동작을 점검하려면 어셈블러 코드까지 분석을 해봐야 해요.
이제 코드 리뷰 시작.
 
[1]: "mrs     x28,#0x3,#0x0,c4,c1,#0x0" 명령어로 current_thread_info를 x24로 가져와요.
[2]: irq handler가 처리되는 군요. 보통 gic_handle_irq() 함수가 호출되죠.
[3]: ISR 처리가 완료된 후, current_thread_info(struct thread_info*)에서 0x20만큼 떨어진 멤버를 가져와요.
    이 멤버가 바로 preempt_count인데 이 값을 x24에 로딩해요.
[4]: x24가 0이 아니면, [7] 0xFFFFFF8008082934 주소로 점프해서 함수를 빠져나갈 준비를 하죠.
[5]: (struct thread_info*)에서 flag 값을 x0로 가져와요.  
[6]: x0이 1보다 같거나 작으면, [7] 0xFFFFFF8008082934 주소로 점프해서 함수를 빠져나갈 준비를 하고,
     x0이 1보다 크면  el1_preempt이 수행되는 거에요.
NSX:FFFFFF9F99682840|D104C3FF  el1_irq:   sub     SP,SP,#0x130     ; SP,SP,#304
NSX:FFFFFF9F99682844|A90007E0             stp     x0,x1,[SP]
NSX:FFFFFF9F996828D4|D538411C             mrs     x28,#0x3,#0x0,c4,c1,#0x0   ; x28, SP_EL0 //<<--[1]
// .. 생략 ..
NSX:FFFFFF8008082914|910003FD            mov     x29,SP
NSX:FFFFFF8008082918|D63F0020            blr     x1  //<<--[2]
NSX:FFFFFF9F99682920|B9402398             ldr     w24,[x28,#0x20]   ; w24,[x28,#32] //<<--[3]
NSX:FFFFFF9F99682924|35000098             cbnz    w24,0xFFFFFF9F99682934  //<<--[4]
NSX:FFFFFF9F99682928|F9400380             ldr     x0,[x28]  //<<--[5]
NSX:FFFFFF9F9968292C|36080040             tbz     x0,#0x1,0xFFFFFF9F99682934   ; x0,#1,0xFFFFFF9F99682934  //<<--[6]
NSX:FFFFFF9F99682930|9400001F             bl      0xFFFFFF9F996829AC   ; el1_preempt   //<<--[8]
NSX:FFFFFF8008082934|F94093F4            ldr     x20,[SP,#0x120]   ; x20,[SP,#288]  //<<--[7]
 
Trace32로 실제 (struct thread_info*) 정보를 올려보면 아래와 같은데요.
 
current_thread_info 시작 주소가 0xFFFFFFE4DE6A4000인데 flags란 멤버가 0xFFFFFFE4DE6A4000 주소에 위치 해있고,
0xFFFFFFE4DE6A4020 즉 0xFFFFFFE4DE6A4000+0x20 만큼 주소에 preempt_count이 있네요.  
 
만약 current_thread_info가 아래와 같으면 preempt_count가 2이므로 el1_preempt() 호출이 안되겠죠.
  (struct thread_info *) [-] (struct thread_info*)0xFFFFFFE4DE6A4000 = 0xFFFFFFE4DE6A4000 -> (
    (long unsigned int) [D:0xFFFFFFE4DE6A4000] flags = 0x2,  // TIF_NOTIFY_RESUME
    (mm_segment_t) [D:0xFFFFFFE4DE6A4008] addr_limit = 0x0000008000000000,
    (struct task_struct *) [D:0xFFFFFFE4DE6A4010] task = 0xFFFFFFE577E1A400,
    (u64) [D:0xFFFFFFE4DE6A4018] ttbr0 = 0x028F000159067000,
    (int) [D:0xFFFFFFE4DE6A4020] preempt_count = 0x2,
    (int) [D:0xFFFFFFE4DE6A4024] cpu = 0x7)
.
ARM64 비트 아키텍처에서는 struct thread_info->flags 값 조건이 추가됐는데요.
아래 매크로로 정의되어 있네요.  struct thread_info->flags값이 TIF_SIGPENDING, TIF_NEED_RESCHED이면 el1_preempt() 
preemption을 안 하네요.
#define TIF_SIGPENDING          0
#define TIF_NEED_RESCHED        1
#define TIF_NOTIFY_RESUME       2       /* callback before returning to user */
#define TIF_FOREIGN_FPSTATE     3       /* CPU's FP state is not current's */
#define TIF_NOHZ                7
#define TIF_SYSCALL_TRACE       8
#define TIF_SYSCALL_AUDIT       9
#define TIF_SYSCALL_TRACEPOINT  10
#define TIF_SECCOMP             11
#define TIF_MEMDIE              18      /* is terminating due to OOM killer */
#define TIF_FREEZE              19
#define TIF_RESTORE_SIGMASK     20
#define TIF_SINGLESTEP          21
#define TIF_32BIT               22      /* 32bit process */
#define TIF_MM_RELEASED         24
 
이제 정리 좀 하려고 해요.
1. Interrupt 신호가 뜨면 el1_irq() 벡터로 프로그램 카운터로 바뀌어요.
2. 시스템에서 IRQ가 Trigger된 후 ISR 루틴이 실행이 되요.
3. current_thread_info에서 preempt_count 값을 읽어서
   0이면 preemption, 아니면 그냥 빠져 나와요.
 
 
 
Reference(프로세스 관리)
4.9 프로세스 컨택스트 정보는 어떻게 저장할까?
 4.9.1 컨택스트 소개
 4.9.2 인터럽트 컨택스트 정보 확인하기
 4.9.3 Soft IRQ 컨택스트 정보 확인하기
 4.9.4 선점 스케줄링 여부 정보 저장
4.10 프로세스 디스크립터 접근 매크로 함수
 4.10.1 current_thread_info()
 4.10.2 current 매크로란
4.11 프로세스 디버깅
 4.11.1 glibc fork 함수 gdb 디버깅
 4.11.2 유저 프로그램 실행 추적