Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 스핀락 플러그인 함수: spin_lock_irq()/spin_unlock_irq() 9. Synchronize(spinlock/mutex)

리눅스 커널 코드를 조금만 분석하면 spin_lock() 함수뿐만 아니라 spin_lock_irq() 함수를 써서 임계 영역을 보호하는 코드를 많이 볼 수 있습니다. 이렇게 커널은 스핀락 기능을 확장한 플러그인 형태 스핀락 함수들을 제공합니다.

이번 소절에서는 spin_lock() 함수 기능을 확장한 스핀락 플러그인 함수를 소개합니다.
spin_lock_irq()/spin_unlock_irq()

먼저 spin_lock_irq()/spin_unlock_irq() 함수를 리눅스 커널에서 지원하는 이유를 알아보고 세부 코드를 분석하겠습니다.

spin_lock_irq()/spin_unlock_irq() 함수가 생겨난 이유는 무엇일까?

spin_lock_irq()/spin_unlock_irq()  함수 코드 분석에 앞서 spin_lock_irq()/spin_unlock_irq() 함수가 생겨난 이유가 무엇인지 알아보겠습니다.

임베디드 리눅스 개발자들은 spin_lock()/spin_unlock() 함수를 써서 임계영역을 보호했습니다. 레이스 컨디션을 방지하게 됐습니다. 그런데 리눅스 개발자들은 spin_lock() 함수를 쓰다가 불만을 토로하게 됐습니다.

    임계영역에서 스핀락을 걸 때 인터럽트가 발생하지 않았으면 좋겠다.
    스핀락을 획득해 다른 모듈이 접근하지 못하는 것은 좋은데 임계영역에서 인터럽트가 
     발생하는 것이 문제다. 

그래서 리눅스 개발자들은 임계영역에서 예상치 못한 레이스 컨디션을 다시 겪게 됐습니다. 예를 들어볼까요?
임계영역 코드 실행 도중 인터럽트가 발생해 임계영역 코드가 있는 함수를 다시 실행 
스핀락으로 보호한 임계영역 코드가 실행 타이밍이 중요한데 인터럽트가 발생해 실행 시간이 일정하지 않음 

결국 리눅스 개발자들은 다음과 같은 요구를 하게 됐습니다.

     스핀락을 걸 때 인터럽트가 발생하지 않는 기능을 추가해달라.
 
그래서 리눅스 커널에서 spin_lock_irq() 함수와 spin_unlock_irq() 함수를 지원하게 됐습니다. 그렇다면 위에서 소개한 spin_lock_irq()/spin_unlock_irq() 함수는 어떤 방식으로 구현을 했을까요?

리눅스 개발자들이 요청한 요구 사항에 맞게 다음 방식으로 구현 했습니다.
먼저 spin_lock_irq() 함수 구현 방식을 알아볼까요?
local_irq_disable();
spin_lock();

위 코드를 해석하면 다음과 같습니다.

    local_irq_disable() 함수를 호출해 인터럽트 라인을 비활성화하자!
    spin_lock() 함수를 호출해 기존 스핀락을 획득하는 기능을 그대로 쓰자.

다음 spin_unlock_irq() 함수 구현 방식을 알아볼까요?
spin_unlock();
local_irq_enable();

위 코드는 다음과 같이 동작합니다. 

    spin_unlock() 함수를 호출해 기존 스핀락을 해제하는 기능을 그대로 쓰자.
    local_irq_enable 함수를 호출해 인터럽트 라인을 다시 활성화하자.

정리하면 spin_lock_irq() 함수와 spin_unlock_irq() 동작 원리는 다음과 같이 설명할 수 있습니다. 

    spin_lock()/spin_unlock() 함수에서 스핀락 기능은 그대로 유지하고 
     인터럽트를 비활성화하는 기능만 추가하자.

이렇게 spin_lock_irq() 함수는 스핀락 관점으로 spin_lock() 함수와 동작이 같습니다.
spin_lock_irq() 함수는 스핀락을 획득하는 과정에서 인터럽트 라인을 잠시 비활성화하는 동작만이 추가된 것입니다. 그렇다면 spin_lock() 함수 대신 spin_lock_irq() 함수는 어떤 상황에서 써야 할까요?  

    임계영역 코드 구간에서 인터럽트가 발생하면 안될 때 

그렇다면 임계영역에서 어떤 조건으로 실행중일 때 해당 CPU 라인의 인터럽트를 비활성화해야 할까요? 여러 가지 예시를 들 수 있으나 대표적인 상황은 다음과 같습니다.
1. 정확한 순서로 데이터 시트에 언급된 순서로 특정 메모리 구간에 어떤 값을 써야 할 경우
2. 각 디바이스 드라이버가 Suspend/Resume와 같은 슬립에 진입하거나 깨어나는 동작일 경우
3. 코드 실행 순서를 반드시 지켜야 하는 코드인 경우

spin_lock_irq() 함수를 쓰는 예제 코드 분석해보기

이번에는 spin_lock_irq() 함수를 써서 임계영역을 보호하는 코드를 소개합니다.
[https://elixir.bootlin.com/linux/v4.14.49/source/drivers/mfd/rtsx_pcr.c]
1 static void rtsx_pci_remove(struct pci_dev *pcidev)
2 {
3 struct pcr_handle *handle = pci_get_drvdata(pcidev);
4 struct rtsx_pcr *pcr = handle->pcr;
5
6 pcr->remove_pci = true;
7
8 /* Disable interrupts at the pcr level */
9 spin_lock_irq(&pcr->lock);
10 rtsx_pci_writel(pcr, RTSX_BIER, 0);
11 pcr->bier = 0;
12 spin_unlock_irq(&pcr->lock);

spin_lock() 함수와 마찬가지로 10~11번째 함수 전후로 spin_lock_irq()와 spin_unlock_irq() 함수로 임계 영역을 보호합니다.

10~11번째 줄 코드가 임계영역인데 이 코드 구간에서 다음과 같이 동작하게 됩니다.

    스핀락을 획득해서 임계영역을 보호
    임계영역 코드 실행 구간에서 인터럽트 비활성화

spin_lock_irq()/spin_unlock_irq() 함수를 써서 임계영역을 보호하는 예제 코드를 리뷰했습니다. 이어서 세부 코드를 분석해볼까요?

spin_lock_irq() 함수 코드 분석하기

spin_lock_irq() 함수 구현부 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock.h]
static __always_inline void spin_lock_irq(spinlock_t *lock)
{
raw_spin_lock_irq(&lock->rlock);
}

spin_lock_irq() 함수는 스핀락 관점으로 spin_lock() 함수와 동작이 같습니다.

그래서 spin_lock_irq() 함수에서 호출하는 처리 흐름도 spin_lock() 함수와 거의 유사합니다. 다양한 아키텍처에서 spin_lock_irq() 함수를 쓸 수 있게 다양한 인라인 함수를 호출하는 것입니다. spin_lock_irq() 함수가 호출하는 함수 목록과 함수 위치는 다음과 같습니다.
함수 이름 함수 위치
spin_lock_irq() include/linux/spinlock.h
_raw_spin_lock_irq() kernel/locking/spinlock.c
__raw_spin_lock_irq() include/linux/spinlock_api_smp.h
do_raw_spin_lock() include/linux/spinlock.h 
arch_spin_lock() arch/arm/include/asm/spinlock.h

spin_lock() 함수 대비 spin_lock_irq() 함수에서 다른 동작을 하는 코드는 어디일까요? 정답은 __raw_spin_lock_irq() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock_api_smp.h]
01 static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
02 {
03 local_irq_disable();
04 preempt_disable();
05 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
06 LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
07 }

03번째 줄 코드를 제외하고는 __raw_spin_lock() 함수와 구현부가 같습니다.

__raw_spin_lock() 함수를 보면서 이 차이점을 확인해볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock_api_smp.h]
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

__raw_spin_lock_irq() 함수 3번째 줄 코드를 눈으로 따라가 봅시다. preempt_disable() 함수 이전에 local_irq_disable() 함수를 호출했습니다. 여기서 local_irq_disable() 함수를 호출한 이유는 무엇일까요?  

    임계 영역에서 실행 중인 해당 CPU 인터럽트를 비활성화하자. 
 
이어서 local_irq_disable() 함수를 분석해볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
01 #define local_irq_disable() \
02 do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)

local_irq_disable() 함수는 매크로 타입으로 raw_local_irq_disable() 함수로 치환됩니다. 커널 코드 컴파일 과정에서 전처리기가 local_irq_disable() 함수를 보면 02번째 줄 코드로 바꿉니다.

이번에는 raw_local_irq_disable() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
#define raw_local_irq_disable() arch_local_irq_disable()

구현부를 보니 raw_local_irq_disable() 함수는 매크로 타입으로 arch_local_irq_disable() 함수로 치환됩니다.

이어서 arch_local_irq_disable() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/irqflags.h]
static inline void arch_local_irq_disable(void)
{
asm volatile(
" cpsid i @ arch_local_irq_disable"
:
:
: "memory", "cc");
}
인라인 어셈블리 코드로 "cpsid i" ARM 명령어를 실행합니다.

이 “cpsid i" 명령어에 대해 더 자세히 알려면 다음 ARM 사이트를 방문하면 됩니다. 
[http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0203ik/ch02s08s01.html]
CPSID i  ; Disable interrupts and configurable fault handlers (set PRIMASK)

내용을 확인하니 실행 중인 CPU 인터럽트 라인을 잠시 비활성화(Disable)하는 어셈블리 명령어입니다.

spin_lock_irq() 함수 코드를 분석하면서 알게 된 내용을 정리해볼까요?
spin_lock() 함수에서 호출한 함수를 그대로 쓴다.
local_irq_disable() 함수를 호출해 인터럽트를 비활성화한다.

spin_unlock_irq() 함수 코드 분석하기

이어서 spin_unlock_irq() 함수 세부 코드를 분석하겠습니다.

spin_unlock_irq() 함수는 spin_unlock() 함수에서 스핀락 기능을 그대로 물려 받았습니다. 대신 스핀락을 해제 한 후 인터럽트를 다시 활성화하는 동작만이 추가된 것입니다.

spin_unlock_irq() 함수를 호출하면 커널 내부에서 실행하는 함수 목록은 다음과 같습니다.
함수 이름 함수 위치
spin_unlock_irq() include/linux/spinlock.h
_raw_spin_unlock_irq() kernel/locking/spinlock.c
__raw_spin_unlock_irq() include/linux/spinlock_api_smp.h
do_raw_spin_unlock() include/linux/spinlock.h 
arch_spin_unlock() arch/arm/include/asm/spinlock.h

위 함수 목록에서 __raw_spin_unlock_irq() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock_api_smp.h]
01 static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock)
02 {
03 spin_release(&lock->dep_map, 1, _RET_IP_);
04 do_raw_spin_unlock(lock);
05 local_irq_enable();
06 preempt_enable();
07 }

04번째 줄 코드와 같이 do_raw_spin_unlock() 함수를 호출해 스핀락을 해제합니다.
다음 05번째 줄 코드를 보면 local_irq_enable() 함수를 호출해 다음 동작을 수행합니다.

    인터럽트를 다시 활성화한다.

이어서 local_irq_enable() 함수 코드 분석으로 어느 코드에서 인터럽트를 활성화하는지 확인해 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
01 #define local_irq_enable() \
02 do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
...
03 #define raw_local_irq_enable() arch_local_irq_enable()

01~02번째 줄 코드와 같이 local_irq_enable() 함수는 매크로 타입으로 raw_local_irq_enable() 함수로 치환됩니다. 

이어서 03번째 줄 코드를 볼까요?
raw_local_irq_enable() 함수는 arch_local_irq_enable() 함수로 치환됩니다.

마지막으로 arch_local_irq_enable() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/irqflags.h]
01 static inline void arch_local_irq_enable(void)
02 {
03 asm volatile(
04 " cpsie i @ arch_local_irq_enable"
05 :
06 :
07 : "memory", "cc");
08 }

함수 구현부를 보니 인라인 어셈블리 타입으로 "cpsie i" ARM 어셈블리 명령어를 실행하는 코드입니다. "cpsie i" 명령어는 인터럽트 라인을 활성화하는 동작입니다.

spin_unlock_irq() 함수 코드를 분석하면서 알게 된 내용을 정리해볼까요?
spin_unlock() 함수에서 호출한 함수를 그대로 쓴다.
local_irq_enable() 함수를 호출해 인터럽트를 활성화한다.


,

덧글

댓글 입력 영역