linux-debug/synchronization
created : 2020-11-10T15:07:13+00:00
modified : 2020-12-09T04:31:12+00:00
커널 동기화
- 주요 개념 : critical section, race condition
- SMP : symmetric multiprocessing
커널 동기화 기법
- 스핀락과 뮤텍스
스핀락
- 구현부가 상대적으로 간단하다
- 아키텍쳐에 의존적인 코드로 구현
- 휴면상태로 가지 않고 계속 기다림 (Busy-Wait)
- spin_lock_irq(), spin_lock_irq_save()
- spin_lock data structure
typedef struct spinlock { union { struct raw_spinlock rlock; }; } spinlock_t;
typedef struct raw_spinlock { arch_spinlock_t raw_lock; #ifdef CONFIG_DEBUG_SPINLOCK unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif } raw_spinlock_t;
-
아키텍쳐마다 다른데, x86은 asm-generic의 qspinlock 을 사용한다. ```c typedef struct qspinlock { union { atomic_t val;
/*
- By using the whole 2nd least significant byte for the
- pending bit, we can allow better optimization of the lock
- acquisition for the pending bit holder. */ #ifdef __LITTLE_ENDIAN struct { u8 locked; u8 pending; }; struct { u16 locked_pending; u16 tail; }; #else struct { u16 tail; u16 locked_pending; }; struct { u8 reserved[2]; u8 pending; u8 locked; }; #endif }; } arch_spinlock_t; ```
-
- 스핀락 사용 예제
static __always_inline void spin_lock(spinlock_t *lock);
- 스핀락 획득 과정
- 스핀락을 이미 획득했는지 판단 : lock instance의 next와 owner 필드를 확인한다.
- 스핀락을 획득한 후 바뀌는 자료구조 : next 값을 1증가 시킨다.
- 획득한 스핀락을 해제한 후 바뀌는 자료구조 : owner를 1만큼 증가시킨다.
- 스핀락 구현부
- spin_lock()
static __alwyas_inline void spin_lock(spinlock_t *lock) { raw_spin_lock(&lock->rlock); }
- raw_spin_lock()
#define raw_spin_lock(lock) _raw_spin_lock(lock)
- _raw_spin_lock()
void __lock_func _raw_spin_lock(raw_spinlock_t *lock) { __raw_spin_lock(lock); }
- __raw_spin_lock()
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); }
- 스핀락을 획득 한 후 preempt_disable() 함수를 호출할 필요가 없다. 내부적으로 호출해준다.
- do_raw_spin_lock()
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock) { __acquire(lock); arch_spin_lock(&lock->raw_lock); }
- arch_spin_lock()
```c
#define arch_spin_lock(l) queued_spin_lock(l)
/**
- queued_spin_lock - acquire a queued spinlock
- @lock: Pointer to queued spinlock structure */ static __always_inline void queued_spin_lock(struct qspinlock *lock) { u32 val;
val = atomic_cmpxchg_acquire(&lock->val, 0, _Q_LOCKED_VAL); if (likely(val == 0)) return; queued_spin_lock_slowpath(lock, val); }
```c struct pv_lock_ops pv_lock_ops = { #ifdef CONFIG_SMP .queued_spin_lock_slowpath = native_queued_spin_lock_slowpath, .queued_spin_unlock = PV_CALLEE_SAVE(__native_queued_spin_unlock), .wait = paravirt_nop, .kick = paravirt_nop, .vcpu_is_preempted = PV_CALLEE_SAVE(__native_vcpu_is_preempted), #endif /* SMP */ };
- 이후부터는 링크를 참고했다. LWN.net
- 간단하게 하면 0-1 비트는 4개의 entries로 per-cpu array 안에 들어있는 queue node의 index, 2-16은 cpu number + 1 로 (최대 cpu 개수는 16383)
- ticket spinlock 보다 빠르며, [[NUMA]] 를 사용하는 여러개의 코어가 존재하는 곳에서 더욱더 적합하다.
- 추가적인 링크는 큐 스핀락 여기를 참고하자.
- spin_lock()
- [[assembly]]
- 흐으으음… 책은 대부분 arm 아키텍쳐와 관련된 내용이 많네 스킵
스핀락 플러그인 함수
- spin_lock_irq()
- spin_unlock_irq()
- 임계 영역에서 스핀락을 걸 때 인터럽트가 발생하지 않으면 좋겠다. 스핀락을 획득해 다른 모듈이 접근하지 못하는건 좋지만, 임계 영역에서 인터럽트 발생이 문제다.
-
임계 영역의 코드 구간에서 인터럽트를 비활성화
- spin_lock_irq()
static __always_inline void spin_lock_irq(spinlock_t *lock) { raw_spin_lock_irq(&lock->rlock); }
- raw_spin_lock_irq()
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable(); 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()
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); }
- local_irq_disable()
#define local_irq_disable() \ do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
- raw_local_irq_disable()
#define raw_local_irq_disable() arch_local_irq_disable()
- arch_local_irq_disable()
static inline notrace void arch_local_irq_disable(void) { PVOP_VCALLEE0(pv_irq_ops.irq_disable); }
- irq_disable is driver specific.
struct irq_chip i8259A_chip = { .name = "XT-PIC", .irq_mask = disable_8259A_irq, .irq_disable = disable_8259A_irq, .irq_unmask = enable_8259A_irq, .irq_mask_ack = mask_and_ack_8259A, };
- irq_disable is driver specific.
- spin_unlock_irq()
static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock) { spin_release(&lock->dep_map, 1, _RET_IP_); do_raw_spin_unlock(lock); local_irq_enable(); preempt_enable(); }
- local_irq_enable()
#define local_irq_enable() \ do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0) #define raw_local_irq_enable() arch_local_irq_enable()
- spin_lock_irqsave()/spin_unlock_irqrestore()
- acquire spinlock, disable interrupt line, return interrupt state.
- spin_lock_irq_save()
#define spin_lock_irqsave(lock, flags) \ do { \ raw_spin_lock_irqsave(spinlock_check(lock), flags); \ } while (0)
- __raw_spin_lock_irqsave()
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { unsigned long lfags; local_irq_save(flags); preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #else do_raw_spin_lock_falgs(lock, &flags); #endif return flags; }
Mutex?
- mutex data structure
struct mutex { atomic_long_t owner; spinlock_t wait_lock; #ifdef CONFIG_MUTEX_SPIN_ON_ONWER struct optimistic_spin_queue osq; /* Spinner MCS lock */ #endif struct list_head wait_list; #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };
- owner : 뮤텍스를 획득한 프로세스의 태스크 디스크립터 주소, 이 필드를 보고 잠겼는지 확인
- wait_list : 뮤텍스를 기다리는 프로세스의 정보
struct mutex_waiter { struct list_head list; struct task_struct *task; struct ww_acquire_ctx *ww_ctx; #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif }
- list : 뮤텍스를 기다리는 잠든 프로세스 리스트
- task : 뮤텍스를 기다리는 프로세스의 테스크 디스크립터 주소(???)
- ww_ctx : wait/wound deadlock proof mutex 로 deadlock을 회피하여 뮤텍스 락을 처리하는 알고리즘에 사용
- fastpath : 뮤텍스를 다른 프로세스가 이미 획득하지 않은 상태때 동작 방식
- mutex 구조체의 owner 필드 점검
- owner 가 0x0이니 뮤텍스를 다른 프로세스가 획득하지 않은 상태로 판단
-
뮤텍스 자료구조인 mutex 구조체의 onwer 필드에 획득한 프로세스의 테스크 디스크립터 저장
- mutex_lock()
void __sched mutex_lock(struct mutex *lock) { might_sleep(); if (!__mutex_trylock_fast(lock)) __mutex_lock_slowpath(lock); } EXPORT_SYMBOL(mutex_lock);
- __mutex_trylock_fast()
static __always_inline bool __mutex_trylock_fast(struct mutex *lock) { unsigned long curr = (unsigned long)current; if (!atomic_long_cmpxchg_acquire(&lock->owner, 0UL, curr)) return true; return false; }
- __mutx_unlock_fast()
static __always_inline bool_mutex_unlock_fast(struct mutex *lock) { unsigned long curr = (unsigned long)current; if (atomic_long_cmpxchg_release(&lock->owner, curr, 0UL) == curr) return true; return false; }
- slowpath : 뮤텍스를 이미 획득해 휴면 상태에 진입한 후 깨어남
- __mutex_lock_slowpath()
static noinline void __sched __mutex_lock_slowpath(struct mutex *lock) { __mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_); }
- __mutex_lock()
static int __sched __mutex_lock(struct mutex *lock, long state, unsgiend int subclass, struct lockdep_map *nest_lock, unsigned long ip) { return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false); }
- __mutex_lock_common()
static __always_inline int __sched __mutex_lock_common(struct mutex *lock, long state, unsgiend int subclass, struct lockdep_map *nest_lock, unsigned long ip, struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx) { struct mutex_waiter waiter; bool first = false; struct ww_mutex *ww; int ret; /* skip */ waiter.task = current; set_current_state(state); for (;;) { /* skip */ spin_unlock(&lock->wait_lock); schedule_preempt_disabled(); /* skip */ set_current_state(state); } spin_lock(&lock->wait_lock); acquired: __set_current_state(TASK_RUNNING); /* skip */ mutex_remove_waiter(lock, &waiter, current); if (likely(list_empty(&lock->wait_list))) __mutex_clear_flag(lock, MUTEX_FLAGS); debug_mutex_free_waiter(&waiter); /* skip */ }
- mutex_unlock()
void __sched mutex_unlock(struct mutex *lock) { #ifndef CONFIG_DEBUG_LOCK_ALLOC if (__mutex_unlock_fast(lock)) return; #endif __mutex_unlock_slowpath(lock, _RET_IP_); }
- __mutex_unlock_slowpath(struct mutex *lock, unsigned long ip)
struct noinline void __sched __mutex_unlock_slowpath(struct mutex *lock, unsgiend long ip) { struct task_struct *next = NULL; DEFINE_WAKE_Q(wake_q); unsigned long owner; /* skip */ if (!list_empty(&lock->wait_list)) { /* get the first entry from the wait-list */ struct mutex_waiter *waiter = list_first_entry(&lock->wait_list, struct mutex_waiter, list); next = waiter->task; debug_mutex_wake_waiter(lock, waiter); wake_q_add(&wake_q, next); } /* skip */ wake_up_q(&wake_q); }
정리
- 커널 동기화란 1개의 프로세스만 특정 코드 구간을 실행할 때 접근하거나 정해진 순서로 코드 구간을 실행하도록 설계하는 기업
- 임계 영역은 2개 이상의 프로세스가 동시에 실행하면 동시 접근 문제를 일으킬 수 있는 코드 블록
- 레이스 컨디션은 임계 영역에 두 개의 프로세스가 동시에 접근하는 상황
- 레이스 컨디션이 발생하는 요인은 선점 스케줄링, 인터럽트 발생, SMP 시스템에서 2개 이상의 프로세스가 같은 코드를 싱행하는 상황 등
- 레이스 컨디션 방지를 위해 선점 스케줄링이나 인터럽트 발생을 비활성화하거나 임계 영역에 락을 걸어야한다.
- 리눅스 커널에서 가장 많이 쓰이는 커널 동기화 기법은 스핀락과 뮤텍스
- 스핀락 획득을 시도할 때 __raw_tickets 구조체의 next 필드와 owner를 체크. next 와 owner가 동일하다면 스핀락을 획득한 적이 없으니 바로 획득 가능, next 필드가 owner 필드보다 크면 스핀락을 획득하기 전까지 계속 기다린다.
- 뮤텍스 획득을 시도할 때 mutex 구조체의 owner 필드를 체크한다. owner 필드가 0이면 프로세스는 바로 뮤텍스를 획득. 뮤텍스 획득에 성공했다면 mutex 구조체의 owner에 테스크 디스크립터 주소를 저장한다.
- 뮤텍스를 해제하는 프로세스는 뮤텍스 대기열을 체크하고 뮤텍스를 기다리며 잠든 프로세스를 깨운다. 이후 깨어난 프로세스는 뮤텍스를 획득한다.