저번 시간에 preempt_disable() 함수 호출로 커널 패닉이 일어나는 문제가 있었는데요.
이 매크로 함수와 preemption의 관계에 대해서 좀 더 알아보고자 해요.
preemption은 리눅스 커널 핵심 개념 중의 핵심이죠.
preempt_disable() 함수를 호출하면 스택 주소를 꺼내와서, current_thread_info에 있는 preempt_count에 1을 더하는 짓만 하거든요.
그럼 current_thread_info의 preempt_count를 어떻게 접근하냐구요?
어떤 프로그램이 돌던 스택 주소를 접근하면 해당 current_thread_info를 가져올 수 있어요.
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
}
실제 덤프에서 스택 주소를 아무거나 입력하면 해당 current_thread_info를 가져올 수 있어요.
v.v %all (struct thread_info*)([아무런 스택 주소] & ~0x1fff)
v.v %all (struct thread_info*)(0xD20C7F80 & ~0x1fff)
(struct thread_info *) (struct thread_info*)(0xD20C7F80 & ~0x1fff) = 0xD20C6000 -> (
(long unsigned int) flags = 0x0,
(int) preempt_count = 0x1, //<<--
(mm_segment_t) addr_limit = 0xBF000000,
(struct task_struct *) task = 0xD4AA9600,
(struct exec_domain *) exec_domain = 0xC1815CF8,
(__u32) cpu = 0x0,
v.v % (struct thread_info*)(0xDB1EC348 & ~0x1fff)
(struct thread_info *) (struct thread_info*)(0xDB1EC348 & ~0x1fff) = 0xDB1EC000
(struct thread_info*) = 0xDB1EC000
flags = 0x2,
preempt_count = 0x00010302, <<--// HARDIRQ_OFFSET | SOFTIRQ_OFFSET X 3| PREEMPT_OFFSET
addr_limit = 0xBF000000,
task = 0xDD72E540,
exec_domain = 0xC181B35C,
cpu = 0x3,
그럼 어떤 프로세가 돌고 있는데 커널이 언제 제어권을 획득할 수 있을까요?
크게 보면 IRQ와 시스템 콜이 실행될 때라고 말할 수 있는데요.
음, 갑자기 어떤 선배가 자신을 한탄하면서 했던 말이 떠오르네요.
나이 40 줄 먹은 개발자는 언제 짤릴지 모른다.(그러니 실력을 키우자.)
커널 프로그램은 언제 도중에 실행이 멈출 지 모르죠. IRQ가 Trigger되면요.
IRQ가 뜨면 실행 중인 프로그램은 바로 동작을 멈추고 IRQ vector tabel로 점프를 해요.
모든 프로그램은 언제 실행이 멈출 지 모르는 상태에서 돌고 있는 거죠. 맞나요? 항상 그런 건 아니죠.
그럼 irq vector 함수로 가봅시다.
현재 스택 주소가 0xD00C7034 라고 가정해볼께요.
아래 [1], [2], [3] 번 코드가 현재 실행 중인 코드의 스택 주소를 통해 preempt_count를 가져오는 코드거든요.
[1]: 0xD00C7034 >> 0xD(13) = 0x00068063
R9가 0x00068063으로 업데이트 되는군요. 참고로 lsr 명령어는 해당 값 만큼 Right Shift연산을 하고 나머지 값을 0을 채우죠.
[2]: 0x00068063 << 0xD(13) = 0xD00C6000
R9가 스택 Top 주소로 업데이트 되었어요.
v.v (struct thread_info*)(0xD00C7034 & ~0x1fff) 이 T32 명령어와 똑같다고 할 수 있죠.
[3]: struct thread_info.preempt_count 멤버는 0x8 만큼 오프셋이 떨어져 있거든요.
preempt_count값이 r8로 업데이트 됩니다.
[4]: preempt_count값이 0x0이면 svc_preempt를 호출해서 preemption을 실행하구요.
아니면 바로 빠져나오는 거죠.
0xc0fde6c0 <__irq_svc>: sub sp, sp, #76 ; 0x4c
0xc0fde6c4 <__irq_svc+0x4>: tst sp, #4
// .. 생략..
0xc0fde710 <__irq_svc+0x50>: ldr pc, [r1]
0xc0fde714 <__irq_svc+0x54>: lsr r9, sp, #13 //<<--[1]
0xc0fde718 <__irq_svc+0x58>: lsl r9, r9, #13 //<<--[2]
0xc0fde71c <__irq_svc+0x5c>: ldr r8, [r9, #4] //<<--[3]
0xc0fde720 <__irq_svc+0x60>: ldr r0, [r9]
0xc0fde724 <__irq_svc+0x64>: teq r8, #0
0xc0fde728 <__irq_svc+0x68>: movne r0, #0
0xc0fde72c <__irq_svc+0x6c>: tst r0, #2
0xc0fde730 <__irq_svc+0x70>: blne 0xc0fde750 <svc_preempt> //<<--[4]
0xc0fde734 <__irq_svc+0x74>: ldr r0, [sp, #72] ; 0x48
0xc0fde738 <__irq_svc+0x78>: mcr 15, 0, r0, cr3, cr0, {0}
0xc0fde73c <__irq_svc+0x7c>: msr SPSR_fsxc, r5
0xc0fde740 <__irq_svc+0x80>: sub r0, sp, #4
0xc0fde744 <__irq_svc+0x84>: strex r1, r2, [r0]
아주 어렵게 분석을 했는데요. __irq_svc 함수 전체의 기능을 요약하면 아래와 같아요.
1> irq_handler를 호출해서 IRQ 처리
[1] 번 코드에서 실행되요. irq_handler 매크로 정의대로 gic_handle_irq함수 호출이 되죠. (handle_arch_irq)
2> current_thread_info에 있는 preempt_count값을 보고 값이 0x0이면 preemption을 하는거죠.
[2]~[5]: 위에서 계속 분석한 코드죠.
0xc0fde6c0 <__irq_svc>: sub sp, sp, #76 ; 0x4c
0xc0fde6c4 <__irq_svc+0x4>: tst sp, #4
0xc0fde6c8 <__irq_svc+0x8>: subeq sp, sp, #4
0xc0fde6cc <__irq_svc+0xc>: stm sp, {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
0xc0fde6d0 <__irq_svc+0x10>: ldm r0, {r3, r4, r5}
0xc0fde6d4 <__irq_svc+0x14>: add r7, sp, #48 ; 0x30
0xc0fde6d8 <__irq_svc+0x18>: mvn r6, #0
0xc0fde6dc <__irq_svc+0x1c>: add r2, sp, #76 ; 0x4c
0xc0fde6e0 <__irq_svc+0x20>: addeq r2, r2, #4
0xc0fde6e4 <__irq_svc+0x24>: push {r3} ; (str r3, [sp, #-4]!)
0xc0fde6e8 <__irq_svc+0x28>: mov r3, lr
0xc0fde6ec <__irq_svc+0x2c>: stm r7, {r2, r3, r4, r5, r6}
0xc0fde6f0 <__irq_svc+0x30>: mrc 15, 0, r0, cr3, cr0, {0}
0xc0fde6f4 <__irq_svc+0x34>: str r0, [sp, #72] ; 0x48
0xc0fde6f8 <__irq_svc+0x38>: mov r0, #81 ; 0x51
0xc0fde6fc <__irq_svc+0x3c>: mcr 15, 0, r0, cr3, cr0, {0}
0xc0fde700 <__irq_svc+0x40>: isb sy
0xc0fde704 <__irq_svc+0x44>: ldr r1, [pc, #64] ; 0xc0fde74c //<<--[1]
0xc0fde708 <__irq_svc+0x48>: mov r0, sp
0xc0fde70c <__irq_svc+0x4c>: add lr, pc, #0
0xc0fde710 <__irq_svc+0x50>: ldr pc, [r1]
0xc0fde714 <__irq_svc+0x54>: lsr r9, sp, #13 //<<--[2]
0xc0fde718 <__irq_svc+0x58>: lsl r9, r9, #13 //<<--[3]
0xc0fde71c <__irq_svc+0x5c>: ldr r8, [r9, #4] //<<--[4]
0xc0fde720 <__irq_svc+0x60>: ldr r0, [r9]
0xc0fde724 <__irq_svc+0x64>: teq r8, #0
0xc0fde728 <__irq_svc+0x68>: movne r0, #0
0xc0fde72c <__irq_svc+0x6c>: tst r0, #2
0xc0fde730 <__irq_svc+0x70>: blne 0xc0fde750 <svc_preempt> //<<--[5]
어셈블 코드만 보니 지겹다구요? 이제 인라인 어셈블코드 좀 볼께요. 이제 눈에 좀 잘들어오죠?
[kernel/arch/arm/kernel/entry-armv.S]
__irq_svc:
svc_entry
irq_handler
#ifdef CONFIG_PREEMPT
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
정리 좀 할께요.
1> 어떤 코드가 실행되던 IRQ는 Trigger될 수 있다. (조건: local_irq_disable만 호출하지 않은 상태라면)
2> IRQ 핸들러가 실행된 다음에, current_thread_info에 있는 preempt_count값을 보고 값이 0x0이면 preemption한다.
2.1> 만약 preempt_disable()이 호출되어 preempt_count값이 0보다 크면 preemption을 안한다.
그럼, local_irq_disable() 함수를 실행하면 어떻게 될까요? 이 함수를 실행하면 "cpsid i" 인스터럽션이 실행되어 하드웨어적으로 IRQ가 안 뜬다고 할 수 있는데요.
IRQ가 안 뜨니 __irq_svc vector로 점프를 하지도 않을 터이고, preemption이 일어나지 않겠죠.
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) void arch_local_irq_disable(void)
{
asm volatile(
" cpsid i @ arch_local_irq_disable"
:
:
: "memory", "cc");
}
local_irq_disable() 함수는 정말 조심해서 써야 해요.
IRQ가 안 뜨면 시스템이 느려져 이상 동작(유식한 용어로 latency라고 하죠)을 할꺼에요.
한번 해보세요.
+local_irq_disable()
schedule();
+local_irq_enable()
태그 : linux, kernel, 프로세스, process, 리눅스, 커널, preempt_disable, preemption, current_thread_info, __irq_svc
최근 덧글