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