Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

176162
807
85244


[라즈베리파이] 동기화 - 스핀락(spinlock): spin_lock() 함수 분석 9장. 커널 동기화 소개

스핀락을 획득할 때 쓰는 spin_lock()와 함수의 선언부를 봅시다.
static __always_inline void spin_lock(spinlock_t *lock);

입력 인자는 포인터 타입 lock 변수이며 spinlock_t 구조체입니다. lock이란 변수가 가르키는 메모리 공간에 스핀락 인스턴스가 있는 겁니다.

 함수 선언부가 void이니 반환 값은 없습니다. __always_inline 매크로로 선언했으니 커널 함수 내에서 자주 호출되는 함수임을 알 수 있습니다.

다음은 스핀락을 해제할 때 쓰는 spin_unlock()와 함수의 선언부를 봅시다.
static __always_inline void spin_unlock(spinlock_t *lock);

spin_lock() 함수와 마찬가지로, 입력 인자는 포인터 타입 lock 변수이며 spinlock_t 구조체입니다. 함수 선언부가 void이니 반환 값은 없습니다. 

이제 spin_lock() 구현부 코드를 분석할 차례입니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/spinlock.h#L315]
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}
static inline  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);
}
static inline  void do_raw_spin_lock(raw_spinlock_t *lock)
{
 (void)0;
 arch_spin_lock(&lock->raw_lock);
}

함수를 계속 따라가 보니 spin_lock() 함수는 실제 arch_spin_lock() 함수임을 알 수 있습니다.
spin_lock() -> _raw_spin_lock() -> __raw_spin_lock() -> do_raw_spin_lock() -> arch_spin_lock()

다음 리눅스 커널 소스 분석 사이트에 접근해서 arch_spin_lock 키워드로 검색해 보겠습니다. 
[https://elixir.bootlin.com/linux/v4.14.43/ident]

결과 다음과 같이 각각 아키텍처별로 arch_spin_lock() 함수는 다르게 구현돼 있습니다.
arch/alpha/include/asm/spinlock.h, line 31 (as a function)
arch/arc/include/asm/spinlock.h, line 21 (as a function)
arch/arc/include/asm/spinlock.h, line 236 (as a function)
arch/arm/include/asm/spinlock.h, line 58 (as a function)
arch/arm64/include/asm/spinlock.h, line 32 (as a function)
arch/blackfin/include/asm/spinlock.h, line 34 (as a function)
arch/ia64/include/asm/spinlock.h, line 110 (as a function)


arm64 및 ia64 CPU별로 서로 다른 해더 파일에 arch_spin_lock() 함수가 구현돼 있습니다. 
arch_spin_lock() 함수는 CPU에 맞는 어셈블리 코드로 구현됐으므로 스핀락은 아키텍처에 의존적인 겁니다.

spin_lock() 함수에서 바로 arch_spin_lock() 함수를 호출하지 않는 이유는 리눅스 커널이 다양한 CPU(아키텍처)를 지원하기 위해서입니다.

라즈베리파이는 ARM 아키텍처를 쓰니 [arch/arm/include/asm/spinlock.h] 해더 파일에 있는 어셈블리 코드를 분석합시다.

스핀락을 제대로 이해하려면 arch_spin_lock() 함수를 제대로 분석해야 합니다. 그런데 아래 arch_spin_lock() 함수 코드를 잠깐 보면 평소에 볼 수 없는 인라인 어셈블리 코드를 확인할 수 있습니다.
[arch/arm/include/asm/spinlock.h]
1 static inline void arch_spin_lock(arch_spinlock_t *lock)
2 {
3 unsigned long tmp;
4 u32 newval;
5 arch_spinlock_t lockval;
6
7 prefetchw(&lock->slock);
8  __asm__ __volatile__(
9  "1: ldrex %0, [%3]n"
10 " add %1, %0, %4n"
11 " strex %2, %1, [%3]n"
12 " teq %2, #0n"
13 " bne 1b"
14  : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
15  : "r" (&lock->slock), "I" (1 << 16) 
16 : "cc");
17
18 while (lockval.tickets.next != lockval.tickets.owner) {
19  __asm__ __volatile__ ("wfe" : : : "memory");
20  lockval.tickets.owner = (*(volatile typeof(lock->tickets.owner) *)&(lock->tickets.owner));
21 }
22 __asm__ __volatile__ ("dmb " "ish" : : : "memory");
23 }

큰마음 먹고 리눅스 커널 코드를 분석하다가 인라인 어셈블리 코드가 나타날 때 포기할 때가 많습니다. 그런데 인라인 어셈블리 코드는 생각보다 해석하기 쉽습니다. 겁먹지 마세요.

인라인 어셈블리 코드 분석에 앞서 코드 문법부터 점검합시다.
3 unsigned long tmp;
4 u32 newval;
5 arch_spinlock_t lockval;
...
8  __asm__ __volatile__(
9  "1: ldrex %0, [%3]n"
10 " add %1, %0, %4n"
11 " strex %2, %1, [%3]n"
12 " teq %2, #0n"
13 " bne 1b"
14  : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
15  : "r" (&lock->slock), "I" (1 << 16) 
16 : "cc");

9~12줄 코드에 %0, %1, %2, %3이란 기호가 보입니다. 이 기호가 무슨 의미인지 파악하려면 우선 14~15번째 줄 코드를 볼 필요가 있습니다.
14  : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
15  : "r" (&lock->slock), "I" (1 << 16) 

14번째 줄 코드는 인라인 어셈블리 코드의 입력 인자 그리고 15번째 줄 코드는 아웃풋 인자를 표시합니다.
 
먼저 입력 인자부터 알아보겠습니다. 
각 변수 왼쪽에 "=&r" 란 기호가 보이면 인라인 어셈블리 코드의 입력 인자를 의미합니다.
%0은 lockval이고 %1은 newval 그리고 %2는 tmp를 의미합니다. 

다음 아웃풋 인자를 확인해봅시다.
"r" 기호는 인라인 어셈블리 코드의 아웃풋 인자를 의미합니다. 각각 아웃풋 인자를 살펴보면, %3은 &(lock->slock) %4는 (1 << 16) 즉 0x10000입니다.

각 인풋 아웃풋 인자를 각 %에 해당하는 변수로 아래와 같이 정리할 수 있습니다.
%0: &r" (lockval), 
%1: "=&r" (newval), 
%2: "=&r" (tmp)
%3:"r" (&lock->slock), 
%4: "I" (1 << TICKET_SHIFT)

인라인 어셈블리 문법에 따라 각각 인자들을 %0, %1, %2로 치환하면 다음과 같습니다.
9  "1: ldrex lockval, [&(lock->slock)]n"
10 " add newval, lockval, 0x10000n"
11 " strex tmp, newval, [&(lock->slock)]n"
12 " teq tmp, #0n"

인라인 어셈블리 코드 아웃풋 인풋 인자 문법 규칙을 알아봤으니 코드를 분석할 차례입니다.

먼저 9번째 줄 코드를 봅시다.
9  "1: ldrex lockval, [&(lock->slock)]n"

LDREX 명령어는 메모리에서 데이터를 로드하는데, 물리 주소를 처리하는 방식에 약간 차이가 있습니다. LDREX은 LDR과 같은 동작이라고 봐도 무방합니다.

LDR 명령어는 메모리 공간에 있는 값을 워드 단위로 읽는 동작입니다. 스핀락 변수를 lockval이라는 지역 변수에 저장합니다. C 코드로 “lockval = &lock->slock;” 로 표현할 수 있습니다.

다음 10번째 줄 코드를 보겠습니다.
10 " add newval, lockval, 0x10000n"

두 피연산자를 더하는 명령어입니다. lockval 값에 0x10000을 더해서 newval에 더하는 동작입니다. C 코드로 다음과 같이 표현할 수 있습니다.
 newval = lockval.tickets.next + 1;

lockval을 펼쳐 놓으면 다음과 같습니다. 이번에 lockval.tickets.next와 lockval.tickets.owner가 각각 0x8이라고 가정합니다. 이때 lockval은 0x00080008입니다.
0x0008 | 0x0008
(next)   (owner)

0x00080008 값에 0x10000을 더하면 어떻게 될까요? 결과 0x00090008이 됩니다. 이 연산으로 lockval.tickets.next 만 1만큼 증가합니다.
  0x00080008
+   0x10000
============
0x00090008
Trace32 프로그램으로 확인해도 같은 정보를 볼 수 있습니다.
(arch_spinlock_t *) (arch_spinlock_t *)0xb000c048 = 0xB000C048 -> (
    (u32) slock = 0x00090008,
    (struct __raw_tickets) tickets = (
      (u16) owner = 0x8,
      (u16) next = 0x9))

스핀락은 매우 자주 호출되는 루틴으로 0x10000을 더해서 연산 횟수를 줄인 겁니다.
이해를 돕기 위해 10번째 어셈블리 코드와 이 동작을 C 코드로 변환한 코드를 같이 봅시다. 

10 " add newval, lockval, 0x10000n"
newval = lockval.tickets.next + 1;

다음 11번째 줄 코드를 보겠습니다.
11 " strex tmp, newval, [&(lock->slock)]n"

STREX는 메모리에 대한 조건부 저장을 수행하는데 기본적으로 STR 명령어와 같은 동작입니다.

10번째 줄 코드에서 스핀락 변수에서 tickets.next를 1만큼 증감한 결과를 newval에 저장했습니다. 

str 명령어는 메모리 공간에 저장하는 동작입니다. &(lock->slock) 메모리 공간에 newval을 저장합니다. 이 동작이 완료됐으면 tmp는 0으로 업데이트됩니다.

이번에도 이해를 돕기 위해 11번째 줄 어셈블리 코드와 이 동작을 C 코드로 변환한 코드를 같이 봅시다.
11 " strex tmp, newval, [&(lock->slock)]n"
&(lock.tickets.next) = newval;

여기까지 분석한 8~16번째 줄 코드와 이에 해당하는 C 코드는 각각 다음과 같습니다.
8  __asm__ __volatile__(
9  "1: ldrex %0, [%3]n"
10 " add %1, %0, %4n"
11 " strex %2, %1, [%3]n"
12 " teq %2, #0n"
13 " bne 1b"
14  : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
15  : "r" (&lock->slock), "I" (1 << 16) 
16 : "cc");

위 코드를 이전 절에 스핀락 전체 구조 그림으로 보면 다음과 같이 검은색으로 된 부분입니다.
 
다음 18번 줄 코드를 분석하겠습니다.
18 while (lockval.tickets.next! = Lockval.tickets.owner) {
19 __asm__ __volatile__ ("wfe"::: "memory");
20 lockval.tickets.owner = (* (volatile typeof (lock-> tickets.owner) *) & (lock-> tickets.owner));
21}

wfe는 ARM Core가 아무 일도 하지 않을 때 소모 전류를 최적화하며 실행하는 어셈블리 명령어입니다. 더 구체적인 내용은 다음 사이트를 참고하세요.
[http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15473.html]


18번 줄 코드는 만약 arch_spin_lock() 함수에 처음 진입하기 직전에, next가 owner보다 크면 이미 spinlock을 잡고 있다고 판단합니다. 이럴 때 18~21번째 줄 코드인 while 루프를 돌면서 기존에 spinlock을 잡고 있는 이미 획득한 spinlock을 해제(owner를 1만큼 증가)할 때까지 기다립니다.

보통 spinlock을 다른 모듈에서 획득하지 않았으면 next가 owner와 같으니 spinlock을 획득하고 바로 18번 줄 while 루프를 빠져나옵니다.


#Reference 시스템 콜


Reference(워크큐)
워크큐(Workqueue) Overview


덧글

댓글 입력 영역