Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

43107
469
422675


Spinlock(스핀락) - spin_lock() vs spin_lock_irq() vs spin_lock_irqsave() - 분석 9. 커널 동기화(spinlock/mutex)


자 그 동안 궁금해왔던 아래 API들의 차이점에 대해서 알아보는 시간을 갖도록 하겠습니다.
spin_lock(), spin_lock_irq(), spin_lock_irqsave()

리눅스 커널 책에서 마르고 닳도록 설명을 많이 하고 있는데요. 
직접 소스를 열어서 분석하는게 가장 좋은 리눅스 커널을 마스터하는 길인 것 같아요. 소스 코드가 오픈되어 있잖아요.

1> spin_lock()
아래 순서로 실제 구현부는 __raw_spin_lock() 함수 라는 걸 알 수 있어요.

spin_lock -> raw_spin_lock -> __raw_spin_lock
static inline void spin_lock(spinlock_t *lock)
{
 raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
 
전처리 파일로 __raw_spin_lock() 함수를 확인해보면 preempt_count_add(1) 함수 호출로 갑자기 irq가 호출될 시 preemption을 막고(*Appendix 1)
바로 do_raw_spin_lock() 함수를 호출하는군요.
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) void __raw_spin_lock(raw_spinlock_t *lock)
{
 do { preempt_count_add(1); __asm__ __volatile__("": : :"memory"); } while (0);
 do { } while (0);
 do_raw_spin_lock(lock);
}
 
2> spin_lock_irq()
역시나 이 함수의 정체는 __raw_spin_lock_irq() 임을 알 수 있어요.
spin_lock_irq -> raw_spin_lock_irq -> __raw_spin_lock_irq
static inline void spin_lock_irq(spinlock_t *lock)
{
 raw_spin_lock_irq(&lock->rlock);
}

#define raw_spin_lock_irq(lock)  _raw_spin_lock_irq(lock)
.
__raw_spin_lock_irq() 함수의 구현부를 보면,
arch_local_irq_disable()을 호출해버려서 "msr daifset, #2" ARM64(Aarch64) 인스트럭션이
수행되어 아예 IRQ가 하드웨어적으로 Trigger가 안되게 막아 버리는군요.

그리고 preempt_count_add(1)을 호출해서 IRQ가 혹시 trigger될 시 preemption이 안 되도록 방지하죠.(*Appendix 1)
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
 do { arch_local_irq_disable(); trace_hardirqs_off(); } while (0);
 do { preempt_count_add(1); __asm__ __volatile__("": : :"memory"); } while (0);
 do { } while (0);
 do_raw_spin_lock(lock);
}
 
3> spin_lock_irqsave()
이 함수의 정체는 __raw_spin_lock_irqsave() 인데요.
raw_spin_lock_irqsave -> _raw_spin_lock_irqsave -> __raw_spin_lock_irqsave

실제 구현부는 아래와 같아요.
static inline __attribute__((always_inline)) __attribute__((no_instrument_function)) unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
 unsigned long flags;

 do { do { ({ unsigned long __dummy; typeof(flags) __dummy2; (void)(&__dummy == &__dummy2); 1; }); flags = arch_local_irq_save(); } while (0); trace_hardirqs_off(); }
 do { preempt_count_add(1); __asm__ __volatile__("": : :"memory"); } while (0);
 do { } while (0);

 do_raw_spin_lock(lock);

 return flags;
}
 
__raw_spin_lock_irqsave() 함수의 핵심 코드는 arch_local_irq_save()이라 이 코드를 좀 더 분석할 필요가 있어요.

arch_local_irq_save()함수의 구현부를 보면, 크게 2가지 일을 하고 있어요.
[1]: "mrs %0, daif" // Interrupt disable flag을 설정한 후 해당 값을 flags에 저장
[2]:"msr daifset, #2"" // 하드웨어적으로 IRQ를 disable함
static inline unsigned long arch_local_irq_save(void)
{
 unsigned long flags;
 asm volatile(
  "mrs %0, daif  // arch_local_irq_save\n"  //<<--[1]
  "msr daifset, #2"            //<<--[2]
  : "=r" (flags)
  :
  : "memory");
 return flags;
}
.
여기서 중요한 포인트는 flags를 저장해서 리턴한다는 건데요.
나중에 spin_unlock_irqrestore() 함수가 호출되어 아래 순서로 arch_local_irq_restore() 함수의 파라미터로 쓰이게 되요.
 __raw_spin_unlock_irqrestore -> local_irq_restore -> arch_local_irq_restore
static inline void arch_local_irq_restore(unsigned long flags)
{
 asm volatile(
  "msr daif, %0  // arch_local_irq_restore"
 :
 : "r" (flags)
 : "memory");
}

그럼 간단히 코드 리뷰를 했으니 정리 좀 하려고 해요.

spin_lock()
spinlock을 거는 구간이 빨리 수행되거나 IRQ가 trigger되어 preemption되어도 문제가 안될 때 쓸 수 있어요.
spin_lock 내 함수 호출이 없는 경우 쓰면 좋겠죠.

spin_lock_irq()
위 코드를 봤듯이 arch_local_irq_disable()을 호출해버려서 "msr daifset, #2" ARM64(Aarch64) 인스트럭션이
수행되어 spin_unlock_irq()이 수행될 때 까지 아예 IRQ가 하드웨어적으로 Trigger가 먹통이 되버리거든요.
spin_lock_irq()는 거는 구간이 너무 오래 수행되는 코드면 시스템이 느려지게 되겠죠.

spin_lock_irqsave() 
코드로 봤듯이 spin_lock_irq() 함수랑 기능적으로 다른게 아무것도 없거든요.

대신 arch_local_irq_save() 호출로  "mrs %0, daif" Instruction이 수행되어 interrupt flags를 가져온다는 점만 다른데요.

spin_lock_irq() 혹은 spin_lock() 함수가 서브루틴에 많이 쓰게 되면 Interrupt가 disable 혹은 enable 되었는지 파악하기 어려운 경우가 있거든요.
A -> B -> C 함수가 호출된다고 가정해보아요.
A함수: spin_lock_irq() 
-B함수: spin_lock_irq() 
--C함수: spin_lock_irq() 

그래서 ARM64 Instruction을 활용해서 Interrupt을 enable 때의 고유한 flag를  "msr daifset, #2"로 리턴하고
Interrupt을 disable 때 이전에 설정한 flag로  "msr daif, %0"  Interrupt을 disable하게 되요.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

[*Appendix 1]
http://rousalome.egloos.com/9964816
http://rousalome.egloos.com/9966282


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

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

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

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

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

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




    핑백

    덧글

    • NJY 2020/06/30 23:08 # 삭제 답글

      안녕하세요. 잘 해석도 못하는 영어로 공부하는데 귀한 한글자료 올려 주셔서 너무 감사드립니다. 본문에 "spin_lock_irq() 혹은 spin_lock() 함수가 서브루틴에 많이 쓰게 되면 Interrupt가 disable 혹은 enable 되었는지 파악하기 어려운 경우"에 대한 설명이 있는데, spin_lock_irq쪽에는 하나의 쓰레드만 들어갈수 있는데 spin_lock_irq가 중첩해서 사용되는 이유가 뭔가요? spin_lock안에 또 spin_lock을 잡아야하는 상황이 뭔지 궁금합니다! 감사합니다


    • AustinKim 2020/07/01 11:13 #

      질문에 답을 드리면,
      리눅스 커널 소스 코드가 너무 방대해서 그런 것입니다.

      우리가 드라이버를 작성할 때 spin_lock_irq() 함수를 사용해 특정 코드 구간(크리티컬 섹션)을 보호하는데,
      이 코드 구간에서 커널의 세부 함수에서 spin_lock_irq() 함수가 다시 호출될 수 있습니다.

      예를 들면;
      spin_lock_irq(); // 인터럽트 비활성화
      ... // 크리티컬 섹션 코드 시작
      커널 API 호출
      ...
      ...
      ... spin_lock_irq(); // 인터럽트 비활성화

      또 다른 예로, 이미 spin_lock_irq() 함수를 호출되는 상황을 들 수 있어요.

      spin_lock_irq(); // 인터럽트 비활성화
      ... // 크리티컬 섹션 코드 시작
      디바이스 드라이버 관련 함수 실행
      ...
      ...
      ... spin_lock_irq(); // 우리가 작성한 드라이버 코드, 인터럽트 비활성화
      ... // 크리티컬 섹션 코드 시작
    • NJY 2020/07/01 14:43 # 삭제

      답변 감사합니다!
    • 2020/07/01 11:13 # 답글 비공개

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