Search

'spinlock'에 해당되는 글 2건

  1. 2018.11.07 스핀락, 뮤텍스, 세마포어
  2. 2018.07.23 스핀락

스핀락, 뮤텍스, 세마포어

컴퓨터공부/리눅스 2018.11.07 20:40 Posted by 아는 개발자 아는 개발자

여러 개의 프로세스가 동시에 실행 할 수 있는 멀티 코어 환경은 사용자의 시스템 전반의 성능을 향상 시켜 주었지만 개발자들에게는 '프로세스간 공유 자원 접근 관리'라는 골치아픈 숙제를 남겼다. 다수의 컴포넌트가 공유중인 자원을 동시에 읽거나 수정할 때 생기는 문제들을 포괄해서 '동기화 문제'라고 하며 대부분의 소프트웨어는 스핀락, 뮤텍스, 세마포어라는 자료구조들을 이용해 해결 한다. 이번 포스트에서는 각 자료구조들의 작동 원리와 차이점에 대해서 정리해보려고한다.


스핀락 (spinlock)


특정한 자료구조를 획득(lock) 또는 해제(unlock) 함으로서 공유 데이터에 대한 접근 권한을 관리하는 방법이다. 권한을 획득하기 전까지 CPU는 무의미한 코드를 수행하는 busy waiting 상태로 대기하고 있다가 접근 권한을 얻으면 내부 코드를 수행하고 종료후 권한을 포기한다. 상태가 획득/해제 밖에 없기 때문에 공유 영역에는 하나의 컴포넌트만 접근 할 수 있으며 획득과 해제의 주체는 동일해야한다.


// arch/arm64/kernel/debug-monitors.c
static DEFINE_SPINLOCK(step_hook_lock);

void register_step_hook(struct step_hook *hook)
{
    spin_lock(&step_hook_lock);
    list_add_rcu(&hook->node, &step_hook);
    spin_unlock(&step_hook_lock);
}

CPU를 선점하고 있는 busy waiting 상태로 대기하기 때문에 권한이 해제되는 대로 빨리 작업을 수행할 수 있는 장점이 있지만 선점 기간동안 다른 프로세스 작업이 지연될 수 있는 오버헤드도 존재한다. 그래서 짧게 수행할 수 있는 작업에 주로 사용된다.


뮤텍스 (mutex)


획득(lock), 해제(unlock) 두가지  상태가 존재해 하나의 컴포넌트만 공유영역에 접근 할 수 있고 획득과 해제의 주체가 동일해야한다는 점은 스핀락과 동일하나 권한을 획득 할 때 까지 busy waiting 상태에 머무르지 않고 sleep 상태로 들어가고 wakeup 되면 다시 권한 획득을 시도한다. 시스템 전반의 성능에 영향을 주고 싶지 않고 길게 처리해야하는 작업인 경우에 주로 사용한다. 주로 쓰레드 작업에서 많이 사용된다.


 
// arch/arm64/mm/dma-mapping.c
mutex_lock(&iommu_dma_notifier_lock);
list_for_each_entry_safe(master, tmp, &iommu_dma_masters, list) {
   if (data == master->dev && do_iommu_attach(master->dev,
        master->ops, master->dma_base, master->size)) {
    list_del(&master->list);
    kfree(master);
    break;
   }
}
mutex_unlock(&iommu_dma_notifier_lock);


세마포어 (Semaphore) 


스핀락, 뮤텍스와는 다르게 표현형이 정수형이며 이점을 살려 하나 이상의 컴포넌트가 공유자원에 접근하도록 허용할 수 있다. 예로 들면 뮤텍스와 스핀락은 옷가게에 한 개의 피팅룸만 있었던 반면 세마포어는 하나 이상의 피팅룸이존재하는 셈. 물론 세마포어를 이진수의 형태로 사용해 개념적으로 뮤텍스와 스핀락처럼 사용하는 것도 가능하다. 정수형인 만큼 획득과 해제 같은 명령이 아니라 값을 올리고 줄이는 방식으로 세마포어를 사용하다. 세마포어의 값이 0이면 기다려야 한다.


// fs/btrfs/disk-io.c
    down_read(&fs_info->cleanup_work_sem);
    if ((ret = btrfs_orphan_cleanup(fs_info->fs_root)) ||
        (ret = btrfs_orphan_cleanup(fs_info->tree_root))) {
        up_read(&fs_info->cleanup_work_sem);
        close_ctree(tree_root);
        return ret; 
    }    
    up_read(&fs_info->cleanup_work_sem);


스핀락과 뮤텍스와 달리 세마포어는 해제(Unlock)의 주체가 획득(Lock)과 같지 않아도 된다. 즉 어떤 프로세스가 세마포어의 값을 감소시켜도 다른 프로세스가 풀어줄 수 있다. 이 특징을 고려해 세마포어를 시그널(Signal) 원리로 사용 할 수도 있다.


아래의 그림처럼 TASK A가 먼저 스케줄링돼 나중에 해야할 작업(post_work)을 먼저 할 수도 있는 상황이라고 해보자. 초기 세마포어의 값을 0으로 초기화해 post_work를 호출하지 못하게 막는다. 그러고 난 후 TASK B의 pre_work가 끝난 후에 세마포어의 값을 올려서 TASK A가 post_work를 호출할 수 있도록 할 수 있다.




참고자료


- FEABAHAS 블로그

'컴퓨터공부 > 리눅스' 카테고리의 다른 글

스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
RCU (Read-Copy Update)  (0) 2018.10.30
Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18

스핀락

컴퓨터공부/리눅스 2018.07.23 22:23 Posted by 아는 개발자 아는 개발자

멀티프로세서 환경에서는 시스템에서 생성된 프로세스가 하드웨어에 설치된 CPU의 수만큼 동시에 실행된다. CPU에서 동작 중인 프로세스는 메모리 영역에 할당된 데이터중 연산이 필요한 값을 읽고 쓰는 작업을 수행하게 되는데 이때 여러 개의 프로세스가 같은 데이터 영역을 공유하게 되는 경우가 있다. 여러 개의 프로세스가 공통된 전역 변수의 값을 읽는 경우가 대표적인 예다.




데이터 값이 고정되고 모든 프로세스가 고정된 값을 읽기만 하면 별 다른 문제는 없다. 그런데 누군가가 쓰게 되는 경우부터 싱크 문제가 생긴다. A랑 B 프로세스가 있다고 해보자. A 프로세스는 C와 D의 데이터 값을 수정하게 되어있고, B 프로세스는 A프로세스보다 조금 늦게 생성되어 A 프로세스가 수정한 값을 읽도록 짜여져 있다. 싱글코어에선 A 프로세스 다음에 B 프로세스가 스케줄링돼 수정 작업과 읽는 작업이 순차적으로 이뤄진다. 그런데 멀티코어에서는 B프로세스가 A 프로세스의 작업이 끝나기 전에 스케줄링 될 수도 있다. 이런 경우에는 수정된 C, D 값을 읽지못하고 예전 값을 읽게 될수도 있다.


static __always_inline void spin_lock(spinlock_t *lock)
static __always_inline void spin_unlock(spinlock_t *lock)


이런 경우를 막기위해 리눅스에서는 spin_lock이라는 API를 제공한다. spin_lock은 매개변수(spinlock_t)의 값을 이용해 다른 프로세스의 공유 영역에 대한 접근을 막을 수 있는 함수다. spin_lock을 부르면 매개 변수의 값을 보고 다른 프로세스의 점유 유무를 확인한다. 점유한 상태가 아니면 매개변수의 값을 점유 상태로 바꾸고 공유 영역에서 수행할 작업을 수행하고 그렇지 않으면 다른 프로세스가 놓을 때까지 (busy waiting으로)기다린다. 수행한 작업을 완료한 후 spin_unlock 함수를 통해 점유 상태를 취소한다. 취소 후에는 대기중이던 프로세스가 매개 변수의 값을 읽고 재점유한다.


다시 예제로 돌아와보자. A 프로세스가 값을 수정하기 전에 B 프로세스가 읽는 것을 막기 위해선 값을 수정하는 A 프로세스가 값을 수정하는 작업과 B 프로세스가 값을 읽는 작업 앞에 spin_lock을 걸어두면 된다. 수정 작업이 먼저 실행되기 때문에 A 프로세스가 먼저 lock을 획득하게 되고 중간에 읽기 작업을 수행하는 B 프로세스는 lock을 획득하지 못해 busy waiting 상태로 기다린다. 수정작업이 모두 끝난 A는 다른 프로세스가 접근 할 수 있도록 spin_unlock을 수행하게 되고 이 이후에 B 프로세스는 lock을 획득해 수정된 값을 읽을 수 있게 된다.




여기서 더 나아가 하드웨어에서 인터럽트가 들어온 상황을 생각해보자. 프로세스가 lock을 획득해서 작업하는 도중에 인터럽트가 들어오면 CPU는 하던일을 중단하고 인터럽트 서비스 루틴을 처리한다. 그런데 만약 인터럽트를 처리하는 작업중에서 인터럽트가 들어오기전 프로세스가 획득한 lock을 갖기 위해 spin_lock을 부르는 코드가 있다고 해보자. 인터럽트 서비스 루틴도 일반 프로세스와 마찬가지로 lock을 획득 할 수 있을 때까지 대기 상태에 있다. 그런데 lock을 놓아줘야하는 프로세스는 여전히 인터럽트에 걸린 상태에서 다시 스케줄링 되지 못하고 있다. 서로가 서로의 진행을 막고있는 셈이다. 이런 경우 프로세스는 교착 상태(Dead Lock)에 빠진다.


static __always_inline void spin_lock_irq(spinlock_t *lock)
static __always_inline void spin_unlock_irq(spinlock_t *lock)


이를 해결하기 위해 리눅스에서는 lock을 획득한 상태에서는 cpu의 인터럽트를 막을 수 있는 함수를 제공한다. spin_lock_irq 함수를 사용하면 해당 lock을 획득하고 다시 놓을 때까지 프로세스의 인터럽트를 막는다. 이 시간 동안 들어오는 인터럽트는 pending 상태로 대기하고 있다가 spin_unlock_irq가 불리면 다시 재실행 된다.


그런데 spin_unlock_irq는 disable 되어있던 인터럽트를 모두 enable 시키는데 이러면 함수의 depth가 깊어지는 경우 문제가 발생 할 수 있다. 어떤 함수가 spin_lock_irq를 부르고 lock 내부 루틴을 타는 함수가 또 spin_lock_irq를 부르면 정상적으로 함수가 종료 될 때 spin_unlock_irq가 두번 불리게 될 것이다. 그런데 spin_unlock_irq는 예전에 인터럽트가 몇번 disable 됐는지 상관없이 모두다 enable하게돼 첫번째 spin_unlock_irq와 두번째 spin_unlock_irq가 호출되는 사이 지점은 인터럽트가 disable이 되지 않는다. 즉 spin_lock_irq를 못쓰고 spin_lock을 쓰는것과 마찬가지인 셈. 이 사이에 하드웨어 인터럽트가 들어와 똑같은 lock에 spin_lock을 걸면 예전처럼 deadlock이 걸린다.


#define spin_lock_irqsave(lock, flags)              \
do {                                \
    raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)
static __always_inline void spin_unlock_irqrestore(spinlock_t *lock,
                                 unsigned long flags)
{
    raw_spin_unlock_irqrestore(&lock->rlock, flags);
}


이런 경우를 해결하고자 spin_lock_irqsave라는 함수가 있다. 잘 보면 이 함수는 매개변수로 flags라는 값을 하나 더 주는 것을 볼 수 있다. flag에다가 현재 interrupt의 상태를 보존해 놓는 것이다. 이 값을 활용해 더 안전하게 스핀락을 처리할 수 있도록 한다.



'컴퓨터공부 > 리눅스' 카테고리의 다른 글

Cgroup (Control Group)  (0) 2018.09.15
CPU pinning과 taskset  (0) 2018.08.27
스핀락  (0) 2018.07.23
eventfd  (0) 2018.07.18
workqueue 사용법  (0) 2018.07.16
tasklet 사용법  (0) 2018.06.17