Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

5363
1898
209234


[리눅스커널] 스핀락(spinlock) 플러그인 함수: spin_lock_irqsave()/spin_unlock_irqrestore 9. 커널 동기화(spinlock/mutex)

리눅스 커널에서 spin_lock_irq() 함수 뿐만 아니라 spin_lock_irqsave() 함수로 임계 영역을 보호하는 기능을 지원합니다. 

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

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

spin_lock_irq() 함수를 써서 임계 영역 코드 구간을 보호하다 보니 다음과 같이 불편한 점이 생겼습니다.
spin_lock_irq() 함수를 호출한 후 인터럽트를 비활성화하다 보니 현재 인터럽트를 상태(활성화/비활성화)를 확인하기 어려움
함수 호출 깊이가 깊어지면 인터럽트를 상태(활성화/비활성화)를 확인하기 어려움  

이런 상황에서는 spin_lock_irq() 함수 대신 spin_lock_irqsave() 함수를 쓰면 됩니다.
spin_lock_irqsave() 함수 처리 과정은 다음과 같습니다. 
스핀락 획득 
인터럽트 라인 비활성화 
인터럽트 상태 반환 

spin_lock_irqsave() 함수는 스핀락 관점으로 보면 spin_lock()/spin_lock_irq() 함수와 같은 기능입니다. 또한 인터럽트 라인을 비활성화하는 동작은 spin_lock_irq() 함수와 같습니다. 다만 spin_lock_irqsave() 함수는 인터럽트 상태 플래그를 반환하는데 이 정보로 spin_lock_irqrestore() 함수를 호출한다는 점이 다릅니다.  

이전 소절에서는 스핀락을 획득하고 인터럽트 라인을 비활성화하는 부분을 살펴봤습니다. 이어서 spin_lock_irqsave()/spin_lock_irqrestore() 함수 분석은 spin_lock_irq()/spin_unlock_irq() 함수와 차이점을 비교하면서 진행하겠습니다.

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

이번에는 spin_lock_irqsave() 함수를 써서 임계영역을 보호하는 코드를 봅시다.
[https://elixir.bootlin.com/linux/v4.19.30/source/drivers/base/power/qos.c]
01 enum pm_qos_flags_status dev_pm_qos_flags(struct device *dev, s32 mask)
02 {
03 unsigned long irqflags;
04 enum pm_qos_flags_status ret;
05
06 spin_lock_irqsave(&dev->power.lock, irqflags);
07 ret = __dev_pm_qos_flags(dev, mask);
08 spin_unlock_irqrestore(&dev->power.lock, irqflags);
09
10 return ret;
11 }

07번째 줄 코드가 임계영역인데 이 코드 구간에서 실행 중인 CPU라인의 인터럽트를 비활성화합니다. 07번째 줄 코드 전후로 spin_lock_irqsave()와 spin_unlock_irqrestore() 함수를 호출해 임계 영역을 보호합니다.

이어서 spin_lock_irqsave() 함수와 spin_lock_irqrestore() 함수를 분석합니다.

spin_lock_irqsave() 함수 코드 분석하기 

spin_lock_irqsave() 함수 구현부 코드는 다음과 같습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock.h]
#define spin_lock_irqsave(lock, flags)    \
do {        \
 raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)

spin_lock_irqsave() 함수는 스핀락 관점으로 spin_lock() 함수와 동작이 같습니다.
그래서 spin_lock_irqsave() 함수에서 호출하는 처리 흐름도 spin_lock() 함수와 거의 유사합니다. 다양한 아키텍처에서 spin_lock_irqsave() 함수를 쓸 수 있게 인라인 함수를 호출하는 과정이 비슷합니다. spin_lock_irqsave() 함수가 호출하는 함수 목록과 함수 위치는 다음과 같습니다.

함수 이름  함수 위치
spin_lock_irqsave()  include/linux/spinlock.h
_raw_spin_lock_irqsave()   kernel/locking/spinlock.c
__raw_spin_lock_irqsave()  include/linux/spinlock_api_smp.h
do_raw_spin_lock()  include/linux/spinlock.h 
arch_spin_unlock()  arch/arm/include/asm/spinlock.h

위 테이블 함수 목록에서 spin_lock() 함수 내에서 호출되는 do_raw_spin_lock() 함수와 arch_spin_lock() 함수가 보입니다. 이 정보로 spin_unlock_irqrestore() 함수는 스핀락 동작 관점으로 spin_lock() 함수와 세부 동작이 같음을 알 수 있습니다.

그러면 spin_lock_irq() 함수 대비 spin_lock_irqsave() 함수에서 다른 동작을 하는 코드는 어디일까요? 정답은 __raw_spin_lock_irq() 함수입니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock_api_smp.h]
01 static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
02 {
03 unsigned long flags;
04
05 local_irq_save(flags);
06 preempt_disable();
07 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
08
09 #ifdef CONFIG_LOCKDEP
10 LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
11 #else
12 do_raw_spin_lock_flags(lock, &flags);
13 #endif
14 return flags;
15}

03~05번째 줄 코드를 제외하고는 __raw_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 }

__raw_spin_lock_irqsave() 함수 05번째 줄 코드를 보면 local_irq_disable() 함수 대신 local_irq_save() 함수를 호출합니다. 이번에도 local_irq_save() 함수를 호출한 이유는 임계 영역에서 인터럽트가 발생하지 않게 설정하는 것입니다.

그런데 local_irq_save() 함수가 local_irq_disable() 함수와 다른 점은 인터럽트를 비활성화할 때 인터럽트 상태 플래그를 flags로 반환한다는 점입니다.
 
이어서 local_irq_save() 함수를 분석해볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
#define local_irq_save(flags)     \
 do {       \
  raw_local_irq_save(flags);   \
 } while (0)

local_irq_save() 함수는 매크로 타입으로 raw_local_irq_save() 함수로 치환됩니다.

이어서 raw_local_irq_save() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
01 #define raw_local_irq_save(flags)   \
02 do {      \
03  typecheck(unsigned long, flags); \
04  flags = arch_local_irq_save();  \
05 } while (0)

raw_local_irq_save() 함수도 매크로 타입으로 02~05번째 줄 코드로 치환됩니다.

03번째 줄 코드는 입력인자인 flags가 unsigned long 타입인지 확인하는 동작입니다. 
이어서 04번째 줄 코드는 arch_local_irq_save() 함수를 실행합니다.

다음으로 arch_local_irq_save() 함수 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/irqflags.h]
01 static inline unsigned long arch_local_irq_save(void)
02 {
03 unsigned long flags;
04
05 asm volatile(
06  " mrs %0, " IRQMASK_REG_NAME_R " @ arch_local_irq_save\n"
07  " cpsid i"
08  : "=r" (flags) : : "memory", "cc");
09 return flags;
10 }

arch_local_irq_save() 함수는 인라인 어셈블리 코드로 구현돼 있어습니다. 먼저 문법을 살펴볼까요? 08번째 줄에 보이는 "=r" (flags)는 입력 인자를 의미하며 "mrs %0" 명령어에서 %0 인자로 쓰입니다. 따라서 06번째 코드는 다음과 같이 바꿔서 읽어도 무방합니다.
" mrs flags, " IRQMASK_REG_NAME_R 

06번째 줄 코드를 어셈블리 코드에서 보면 다음과 같습니다.
mrs     r5,cpsr

인터럽트 상태 정보를 r5 레지스터에 저장하는 동작입니다. r5는 flags 플래그에 저장됩니다.

이어서 07번째 코드는 "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)

내용을 확인하면 인터럽트 라인을 잠시 비활성화(Disable)하는 명령어입니다.

spin_unlock_irqrestore() 함수 코드 분석하기 

spin_unlock_irqrestore() 함수는 spin_unlock_irq() 함수에서 스핀락 기능을 그대로 물려 받았습니다. 또한 스핀락을 해제 한 후 인터럽트를 다시 활성화하는 동작은 같습니다. 하지만 인터럽트를 비활성화할 때 spin_lock_irqsave() 함수를 호출해 저장한 인터럽트 상태 필드를 써서 인터럽트를 활성화하는 동작이 다릅니다.

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

위 테이블 함수 목록에서 spin_unlock() 함수 내에서 호출되는 do_raw_spin_unlock() 함수와 arch_spin_unlock() 함수가 보입니다. 이 정보로 spin_unlock_irqrestore() 함수는 스핀락 동작 관점으로 spin_unlock() 함수와 세부 동작이 같음을 알 수 있습니다. 

위 함수 목록 중 __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_irqrestore(raw_spinlock_t *lock,
02         unsigned long flags)
03 {
04 spin_release(&lock->dep_map, 1, _RET_IP_);
05 do_raw_spin_unlock(lock);
06 local_irq_restore(flags);
07 preempt_enable();
08 }

05번째 줄 코드와 같이 do_raw_spin_unlock() 함수를 호출해 스핀락을 해제합니다.
이후 06번째 줄 코드와 같이 local_irq_restore() 함수를 호출해 해당 인터럽트 라인을 다시 활성화합니다.

이어서 local_irq_restore() 함수 코드를 볼까요?
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
#define local_irq_restore(flags) do { raw_local_irq_restore(flags); } while (0)

local_irq_restore() 함수는 매크로 포멧으로 raw_local_irq_restore() 함수로 치환됩니다.

이어서 raw_local_irq_restore() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/irqflags.h]
01 #define raw_local_irq_restore(flags)   \
02 do {      \
03  typecheck(unsigned long, flags); \
04  arch_local_irq_restore(flags);  \
05 } while (0)

raw_local_irq_restore() 함수도 매크로 타입으로 02~05번째 줄 코드로 치환됩니다.

03번째 줄 코드는 입력인자인 flags가 unsigned long 타입인지 확인하는 동작입니다. 
이어서 04번째 줄 코드는 arch_local_irq_restore() 함수를 실행합니다.

마지막으로 arch_local_irq_restore() 함수를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/arch/arm/include/asm/irqflags.h]
static inline void arch_local_irq_restore(unsigned long flags)
{
 asm volatile(
  " msr " IRQMASK_REG_NAME_W ", %0 @ local_irq_restore"
  :
  : "r" (flags)
  : "memory", "cc");
}

arch_local_irq_restore() 함수 flags 인자로 인터럽트를 활성화하는 동작입니다.

여기까지 spin_lock_irqsave()/spin_lock_irqrestore() 함수 코드를 spin_lock_irq()/spin_unlock_irq() 함수와 차이점을 비교하면서 분석했습니다.

코드 분석으로 다음 내용을 알게 됐습니다.
spin_lock_irqsave() 함수는 스핀락 관점으로 spin_lock() 함수와 spin_lock_irq() 함수와 동작이 같음 
spin_lock_irqsave() 함수는 인터럽트를 비활성화 때 인터럽트 플래그 정보를 반환함 

* 유튜브 강의 동영상도 있으니 같이 들으면 좋습니다.





"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)

#Reference(커널 동기화)
커널 동기화 기본 개념 소개
레이스 발생 동작 확인
커널 동기화 기법 소개
스핀락
뮤텍스란
커널 동기화 디버깅


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2


Thanks,
Austin Kim



핑백

덧글

  • 최재국 2020/05/27 00:06 # 삭제 답글

    spin_lock_irq() 함수를 호출한 후 인터럽트를 비활성화하다 보니 현재 인터럽트를 상태(활성화/비활성화)를 확인하기 어려움 이라고 적혀있는데

    의미를 제대로 모르겠습니다. 인터럽트를 비활성화하고 들어갔는데 현재 인터럽트 상태를 확인하기가 어렵다?? 무슨말이죵 ..

    인터럽트 비활성화하면 스케줄링(타이머 인터럽트)도 안되니까 딱히 흐름이 바뀌는것도 아닌거같은데 순차적으로 코드가 실행되고 나오면 끝아닌가요???

    궁금합니다.!!
  • AustinKim 2020/05/27 11:17 #

    커널 내부에서 여러 수 많은 함수들이 호출될 수 있습니다. 그러다보니 함수 호출의 깊이가 깊어질 수 있죠.
    여러 함수에서 spin_lock_irq() 함수를 사용하면 인터럽트를 '비활성화/활성화'하게 되고 이 결과 인터럽트를 설정한 순서에 맞게 '비활성화/활성화'하지 못할 수 있습니다. 그래서 인터럽트 플래그 상태를 저장해 놓고 이 플래그에 맞게 '비활성화/활성화'하면 순서에 맞게 인터럽트를 '비활성화/활성화'를 할 수 있습니다.
    이런 용도로 사용하는게 spin_lock_irqsave() 함수입니다.

    더 자세한 내용은 아래 링크를 참고하시면 좋겠습니다.

    https://kldp.org/node/54671
    https://stackoverflow.com/questions/2559602/spin-lock-irqsave-vs-spin-lock-irq

    Thanks,
    Austin Kim
  • AustinKim 2020/05/27 11:18 # 답글

    커널 내부에서 여러 수 많은 함수들이 호출될 수 있습니다. 그러다보니 함수 호출의 깊이가 깊어질 수 있죠.
    여러 함수에서 spin_lock_irq() 함수를 사용하면 인터럽트를 '비활성화/활성화'하게 되고 이 결과 인터럽트를 설정한 순서에 맞게 '비활성화/활성화'하지 못할 수 있습니다. 그래서 인터럽트 플래그 상태를 저장해 놓고 이 플래그에 맞게 '비활성화/활성화'하면 순서에 맞게 인터럽트를 '비활성화/활성화'를 할 수 있습니다.
    이런 용도로 사용하는게 spin_lock_irqsave() 함수입니다.

    더 자세한 내용은 아래 링크를 참고하시면 좋겠습니다.

    https://kldp.org/node/54671
    https://stackoverflow.com/questions/2559602/spin-lock-irqsave-vs-spin-lock-irq

    Thanks,
    Austin Kim
  • 최재국 2020/08/04 13:16 # 삭제 답글

    __raw_spin_lock() 함수를 보면서 이 차이점을 확인해볼까요?
    [https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/spinlock_api_smp.h]
    01 static inline void __raw_spin_lock(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 }

    local_irq_disable(); 함수가 있다는 말은 함수 이름이 __raw_spin_lock_irq 인것같은데 오타인가요??

  • AustinKim 2020/08/04 15:40 #

    오타가 맞습니다.
    __raw_spin_lock() -> __raw_spin_lock_irq() 함수로 수정했습니다.

    감사합니다.
  • 2020/08/04 15:40 # 답글 비공개

    비공개 덧글입니다.
댓글 입력 영역