preempt_count
thread_info
구조체의 필드 중 preempt_count
는 프로세스의 컨텍스트 정보를 저장한다. 그리고 다음의 상황에서 preempt_count
필드의 값이 바뀌게 된다:
irq_enter()
함수가 호출되면 irq_enter_rcu()
가 호출되고 이어서 __irq_enter_raw()
매크로가 preempt_count_add()
매크로를 호출하여 최종적으로 preempt_count
의 값을 바꾸게 된다.
in_interrupt()
현재 컨텍스트가 인터럽트인지 확인하는 것은 in_interrupt()
매크로로 확인 가능하다:
in_interrupt()
는 최종적으로 preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK)
로 expand 된다. 인터럽트를 끝낼 때에는 irq_exit()
함수를 호출하게 되고 이 과정에서 다시 preempt_count
의 값을 초기화하게 된다:
irq_exit()
-> __irq_exit_rcu()
-> preempt_count_sub()
순서로 호출되어 preempt_count
의 값을 변경한다. 이후 invoke_softirq()
함수를 호출하여 Soft IRQ
의 처리를 수행하게 된다. 이는 아래에서 다시 자세하게 기술한다.
프로세스가 Soft IRQ
서비스를 처리할 때 preempt_count
필드에 SOFTIRQ_OFFSET
매크로의 값을 저장한다. 이는 handle_softirqs()
함수에서 이뤄지며 다음의 순서로 호출된다: invoke_softirq()
-> do_softirq_own_stack()
-> call_on_irq_stack()
-> ____do_softirq()
-> __do_softirq()
-> handle_softirqs()
.
책에서는 __do_softirq()
함수로 나와있는데 최신 버전의 커널(rpi-6.6.y
) 에서는 handle_softirqs()
가 이 기능을 수행한다. 이름만 다르고 내용은 거의 동일하다.
여기에서 softirq_handle_begin()
함수가 __local_bh_disable_ip()
함수를 호출하고 이 함수는 preempt_count_add(cnt);
매크로를 호출한다. 여기에서 cnt
는 두 번째 인자인 SOFTIRQ_OFFSET
이다. 이를 통해 preempt_count
의 값을 업데이트한다.
Soft IRQ
컨텍스트 여부는 in_softirq()
함수를 호출하여 확인해볼 수 있다. 이는 preempt_count() & SOFTIRQ_MASK
로 확장된다. 그러므로 in_interrupt()
상태에서는 무조건 in_softirq()
상태가 true
이다.
Soft IRQ
의 종료 처리는 handle_softirqs()
함수 마지막에 softirq_handle_end()
함수를 호출하면서 이뤄진다. 이 함수는 다시 __local_bh_enable()
함수를 호출하게 되고 최종적으로 preempt_count
를 업데이트한다.
책에서 나온 내용이 너무 이해가 안되서 직접 코드를 하나 하나 뜯어가면서 분석했다. 일단 인터럽트가 발생하는 최초의 호출은 ARM
기준에서는 Exception Vector
일 것이고, 그 다음에는 관련된 서브루틴으로 점프하는 것이다. 서브루틴의 명칭은 ELn
에 따라, 그리고 Exception
종류에 따라 달라지게 되는데 크게 하는 일은 다음과 같다:
EL1
한정)책에서는 2번의 상황에 대해서 설명하고 있는데 kernel/sched.c
코드에서는 다시 스케줄링하는 코드를 찾아볼 수 없다. 이건 아키텍처 specific 한 코드에 정의하고 있다:
arch/arm64/kernel/entry.s
에서는 다음과 같이 Excpetion Vector Table
을 정의하고 있는데 저 kernel_ventry
가 도무지 이해가 안되서 대신 object file 을 dump 해서 확인해보았다.
kernel_ventry
는 매크로로 저 내용을 el1t_64_irq
와 같은 형식으로 확장하는 것 같다:
뭐가 되었든 el1h_irq_handler
는 반드시 호출되게 되어 있고 이 과정에서 arm64_preempt_schedule_irq()
가 호출되기 때문에 arm64_preempt_schedule_irq()
-> preempt_schedule_irq()
을 거쳐 __schedule()
이 호출될 수 있다.
arm64_preempt_schedule_irq()
함수는 preempt_count
의 값이 0 인지 아닌지 확인해서 선점 스케줄 여부를 확인한다. 0 이라면 선점 스케줄이므로 몇 가지 조건을 더 검사한 뒤에 schdule
한다.
thread_info
구조체의 cpu
필드에는 프로세스가 실행 중인 CPU 번호를 저장한다. 그렇다면 이 CPU 번호는 어떻게 획득할 수 있을까? 이는 커널에서 제공하는 smp_processor_id()
매크로를 호출하여 획득할 수 있다:
책에서는 raw_smp_processor_id()
를 current_thread_info()->cpu
로 구현했다고 했으나 최신 구현에서는 다르게 구현이 되어 있어서 commit log 를 살펴 보았다:
정확하게 이해하는건 아니지만 핵심적인 내용은 CONFIG_THREAD_INFO_IN_TASK
가 활성화 됐을 때, 헤더간의 복잡성으로 인해 저수준의 아키텍처 코드에 접근할 수 없다는 뜻인 것 같다.
set_task_cpu()
thread_info
구조체에 실행 중인 CPU 번호를 저장하기 위해 set_task_cpu()
함수를 호출할 수 있다:
초기화는 다음과 같이 이뤄지게 된며 이를 읽어올 때에는 task_cpu()
라는 이름의 함수를 호출한다.
[출처] https://lwn.net/Articles/831678/
[출처] https://thinkty.net/general/2024/04/29/bottom_half.html
[출처] https://bluemoon-1st.tistory.com/77
[출처] https://velog.io/@mythos/커널-스터디iamroot-18기-2주차-내용-정리-2