Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

8200
629
98815


[라즈베리파이] 동기화 - 스핀락(spinlock) 소개 9장. 커널동기화(스핀락/뮤텍스)

 리눅스 커널에서 가장 많이 쓰는 동기화 기법 중 하나가 스핀락입니다. 먼저 스핀락의 특징을 알아봅시다. 
1. 뮤텍스락 비해 구현 복잡도가 낮습니다.
2. spin_lock_irq(), spin_lock_irq_save() 등등 추가 기능이 포함된 함수를 제공합니다.
3. 스핀락 구현부는 아키텍처에 의존적입니다. ARM, ARM64 그리고 x86 CPU에 따라 구현부가 다릅니다. 스핀락 동작을 제대로 이해하기 위해선 어셈블리 코드를 분석해야 합니다.

임계 영역에 특정 프로세스에 접근하기 위해서 임계 영역 코드 구간에 스핀락을 걸어 다른 프로세스 접근을 막아야 합니다. 이를 위해 다음과 같은 함수를 호출해야 합니다.
static DEFINE_SPINLOCK(static_spinlock);
void kernel_function() 
{
 spin_lock(&static_spinlock);
 // 임계 영역 코드 시작
 // ...
 // ...
 // 임계 영역 코드 마무리
spin_unlock(&static_spinlock);

이제부터 스핀락을 표현하는 구조체와 스핀락이 어떻게 동작하는지 알아봅시다.

스핀락 동작을 살펴보기에 앞서 스핀락을 표현하는 자료구조부터 확인합시다. 다음은 스핀락을 표현하는 자료구조인 spinlock_t 이란 구조체 코드입니다.
1 typedef struct spinlock {
2 union {
3 struct raw_spinlock rlock;
4 };
5 } spinlock_t;
6
7 typedef struct raw_spinlock {
8 arch_spinlock_t raw_lock;
9 } raw_spinlock_t;
10
11 typedef struct {
12 union {
13 u32 slock;
14 struct __raw_tickets {
15 u16 owner;
16 u16 next;
17 } tickets;
18 };
19 } arch_spinlock_t;

1~9번째 줄 코드 선언부는 아키텍처 공용으로 쓰는 구조체이고, arch_spinlock_t 구조체는 아키텍처별로 선언된 구조체입니다.

리눅스 커널을 여러 아키텍처(ARM,ARM64,x86)에서 호환해서 쓰도록 구조체를 이 구조로 설계한 겁니다.

복잡해 보이는 spinlock_t 구조체를 Trace32로 확인하면 다음 값을 확인할 수 있습니다.
  (spinlock_t *) (spinlock_t*)0xb7e0eb00 = 0xB7E0EB00 -> (
    (struct raw_spinlock) rlock = (
      (arch_spinlock_t) raw_lock = (
        (u32) slock = 0x134C134C,
        (struct __raw_tickets) tickets = (
          (u16) owner = 0x134C,
          (u16) next = 0x134C))))

owner가 0x134C 그리고 next가 0x134C이며 slock은 0x134C134C입니다.

spinlock_t 구조체의 실체는 arch_spinlock_t이며 slock과 tickets.owner 그리고 tickets.next로 구성돼 있습니다. 생각보다 구조체는 간단합니다.

이 상태에서 스핀락을 획득하면 owner와 next 값 그리고 slock이 어떻게 바뀔까요?
owner는 0x134C 그리고 next가 0x134D이며 slock은 0x134C134D로 바뀝니다.

스핀락을 획득하면 owner와 next가 어떻게 바뀌는지 다음 시간에 자세히 다룹니다.

스핀락을 표현하는 구조체를 확인했으니 스핀락을 획득할 때 쓰는 spin_lock()와 함수의 선언부를 봅시다.
static __always_inline void spin_lock(spinlock_t *lock);

입력 인자는 포인터 타입 lock 변수이며 spinlock_t 구조체입니다. 함수 선언부가 void이니 반환 값은 없습니다.

__always_inline 매크로로 선언했으니 커널 함수 내에서 자주 호출되는 함수임을 알 수 있습니다.

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

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

이전 시간에 살펴봤듯 임계 영역에 한 개 프로세스만 접근하려면 임계 영역 코드 구간을 보호해야 합니다. 이 코드 구간을 spin_lock() 함수와 spin_unlock() 함수로 감싸는 겁니다.

이번에는 스핀락을 쓰는 코드를 함께 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/kernel/fork.c#L1025]
1 struct file *get_task_exe_file(struct task_struct *task)
2 {
3 struct file *exe_file = NULL;
4 struct mm_struct *mm;
5
6 task_lock(task);  // spin_lock(task);
7 mm = task->mm;
8 if (mm) {
9 if (!(task->flags & PF_KTHREAD))
10 exe_file = get_mm_exe_file(mm);
11 }
12 task_unlock(task);  // spin_unlock(task);
13 return exe_file;
14}

위 코드는 태스크 디스크립터 struct task_struct 인자에서 메모리 디스크립터인 mm 멤버에 접근해서 파일 디스크립터를 읽는 동작입니다. 그런데 코드를 유심히 보면 6번과 12번 줄에 task_lock()와 task_unlock()이라는 함수가 보입니다. 7~11줄 코드 블록을 스핀락을 걸어 다른 프로세스가 접근 못 하게 보호하는 동작입니다.

스핀락을 걸고 임계 영역인 7~11번째 줄 코드 실행 중에 다른 프로세스가 6번 줄 코드를 실행하면 6번 코드인 spin_lock() 함수에서 기다립니다.  

이 두 함수 구현부를 보면 task_lock() 함수는 spin_lock() 함수를 호출하고 task_unlock() 함수는 spin_unlock() 함수를 호출합니다. 
[https://elixir.bootlin.com/linux/v4.14.43/source/include/linux/sched/task.h#L129]
static inline void task_lock(struct task_struct *p)
{
spin_lock(&p->alloc_lock);
}

static inline void task_unlock(struct task_struct *p)
{
spin_unlock(&p->alloc_lock);
}

따라서 task_lock() 함수 실체는 spin_lock() 함수입니다.

struct raw_spinlock 구조체와 arch_spinlock_t 구조체가 같은 주소가 있으니, arch_spinlock_t 구조체 기준으로 코드 설명을 합니다.

같은 주소에 서로 다른 형태 구조체를 정의해 쓰는 기법은 리눅스 커널에서 C++ 다형성과 상속을 흉내 내는 기법입니다. 유연하게 자료구조를 쓰려는 목적입니다.

스핀락 획득, 해제 시 동작
스핀락 동작을 쉽게 표현하면 다음 그림과 같습니다.
 

우선 위 그림에서 쓴 용어부터 정리하겠습니다.

스핀락 지역 변수부터 알아봅시다.
스핀락을 획득하려는 프로세스 스택 공간에서 실행하는 스핀락 값입니다. 지역 변수이니 각각 프로세스 스택 공간에서 실행합니다. 만약 A와 B란 프로세스가 있다고 가정하면 A와 B 프로세스별로 각각 스핀락 지역변수가 있는 겁니다.

이번에는 스핀락 인스턴스입니다.
특정 메모리 공간에 있는 스핀란 자료구조이며 전역 변수와 같은 개념입니다. 스핀락 인스턴스를 A나 B란 프로세스가 같은 시간에 읽으면 값은 같습니다.

스핀락 코드를 분석할 때 이 두 개념을 그리면서 봐야 이해가 쉽습니다.

다음으로 단계별로 스핀락 동작을 알아봅시다.
스핀락 획득 이전에 스핀락 인스턴스의 owner와 next가 각각 0x134C이라고 가정합니다.

1. 스핀락 획득
spin_lock() 함수를 실행하면 스핀락 획득을 시작합니다.

a> 스핀락 인스턴스의 owner와 next 값을 스핀락 지역 변수 owner와 next에 저장합니다.
스핀락 인스턴스의 next를 +1 증감합니다.
b> spin_lock() 함수를 처음 실행했을 때 스핀락 인스턴스의 owner와 next가 각각 0x134C으로 같으니 스핀락을 다른 프로세스가 획득한 적이 없다고 판단합니다.

따라서 바로 스핀락을 획득합니다.

2. 임계영역
스핀락을 획득했으니 임계 영역에서 코드 실행을 합니다.
임계 영역 구간에서 스핀락 인스턴스 값은 다음과 같습니다.
owner: 0x134C
next:  0x134D  
 
3. 스핀락 해제
스핀락 인스턴스의 owner를 +1 증감합니다.

스핀락을 해제 한 후 스핀락 인스턴스 값은 다음과 같습니다.
owner: 0x134D
next:  0x134D  

owner와 next가 같은 값입니다. 이제 스핀락을 획득한 상태는 아니란 의미입니다.

스핀락을 기다릴 때 동작
이번에는 누군가 스핀락을 획득한 상태에서 스핀락을 기다리는 동작을 그려 봅시다.

이전 그림에서 2단계 임계 영역 코드 실행 도중 스핀락을 획득하는 상황입니다.
 
이미 스핀락을 누군가 획득했으니 spin_lock() 함수 호출 전 스핀락 인스턴스는 다음 값과 같습니다. 
owner: 0x134C
next:  0x134D 

스핀락 인스턴스는 어느 프로세스에서 접근할 수 있는 전역 변수와 같은 상태입니다.

스핀락 획득 시도
스핀락 지역변수 owner, next에 스핀락 인스턴스 owner, next를 저장합니다. 스핀락 인스턴스 next를 +1만큼 증감합니다.

Busy-Wait
스핀락 지역변수 owner, next가 같지 않으니 스핀락을 누군가 획득한 상태라 판단합니다. 스핀락 인스턴스를 계속 읽으면서 스핀락 인스턴스 중 owner가 0x134D로 바뀔 때까지 무한 루프를 돌며 기다립니다.

다른 프로세스가 스핀락을 획득하고 임계 영역을 실행 중인 상태입니다. 그런데 다른 프로세스가 스핀락을 해제하면 스핀락 인스턴스가 어떻게 바뀔까요? 스핀락 인스턴스의 owner가 +1만큼 증감해서 0x134D가 됩니다.

무한루프를 돌며 스핀락 지역 변수 next와 스핀락 인스턴스 owner 값이 같을 때까지 기다립니다.

이전 시간에 커널 동기화 기법을 소개할 때 화장실에 이미 누군가 있으면 밖에서 계속 기다린다고 설명했습니다. 바로 이 동작입니다.

스핀락 획득
다른 프로세스가 스핀락을 해제했으니 스핀락 인스턴스 owner가 0x134D로 증감합니다.
스핀락이 해제되기를 계속 기다린 프로세스의 스핀락 지역 변수 next가 0x134D입니다.

바로 스핀락을 획득하고 임계 영역을 실행합니다.

스핀락 동작에 대해서 전체 흐름을 알아봤습니다. 다음 시간에 스핀락 함수 코드를 직접 분석 하면서 동작을 알아볼 예정입니다. 

코드를 볼 때 스핀락 지역변수와 스핀락 인스턴스 구조를 머릿속으로 그리면서 보시면 훨씬 이해가 빠르다는 점 참고하세요.

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

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

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

핑백

덧글

댓글 입력 영역