다음 문서는 [http:www.kelp.or.kr KELP]의 목요세미나를 위한 참고 자료이다. [[TableOfContents]] == 커널 동기화 방법 == 다음의 내용은 ''리눅스 커널 심층 분석''의 ''8장 커널 동기화 방법''을 정리한 것이다. === 원자적 동작 === '''원자적 동작(atomic operation)'''이란 중단됨이 없이 한번에 실행되는 명령을 뜻한다. ==== 원자적인 정수 연산 ==== 원자적 정수 연산은 특별한 자료 구조인 ''atomic_t''를 사용한다. 리눅스 커널 2.6.11의 ''include/asm/atomic.h''에 선언된 내용은 다음과 같다. {{{ typedef struct { volatile int counter; } atomic_t; }}} 책의 내용중 sparc에서는 24비트만을 사용한다는 부분은 초기 커널 버전에서의 이야기이다. 현 커널 2.6.11에서는 앞서 보는 것과 같은 자료형으로 선언되어 있으며 spin_lock_irqsave/spin_unlock_irqrestore를 이용하여 원자적 동작을 처리하고 있다. 이에 대한 부분은 커널 소스 ''include/asm-sparc/atomic.h''와 ''arch/sparc/lib/atomic32.c''를 보라. atomic_t는 다음과 같이 정의한다. {{{ atomic_t u; /* 초기화 하지 않은 상태로 u를 정의 */ atomic_t v = ATOMIC_INIT(0); /* v를 정의하고 0으로 초기화 */ }}} 특히 다음과 같이 초기화 하지 않도록 '''주의'''해야 한다. {{{ atomic_t v = 0; /* 잘못된 초기화다 */ }}} 함수들은 모두 간단한 형태로 돼 있다. {{{ atomic_set(&v, 4); /* v->counter = 4 (atomically) */ atomic_add(2, &v); /* v->counter = v->counter + 2 = 6 (atomically) */ atomic_inc(&v); /* v->counter = v->counter + 1 = 7 (atomically) */ }}} 관련 함수는 ''''에서 찾을 수 있다. 관련 함수 목록은 책 p119의 '''표 8.1 원자적 정수 연산 목록'''을 참고하라. ==== 원자적인 비트 연산 ==== 커널은 원자적인 정수 연산과 함께, 비트 단위로 동작하는 함수들을 제공한다. 이들 함수는 ''''에 정의되어 있다. 자세한 내용은 책 p120, 121을 참고한다. 관련 함수 목록은 책 p121의 '''표 8.2 원자적 비트 연산 목록'''을 참고하라. === 스핀록 === 모든 위험구역(critical region)을 하나의 변수를 늘리는 것과 같은 같은 간단한 동작으로 항상 보존할 수 없다. 이러한 경우에 충분한 보호를 제공할 필요가 있으며 이를 위해 록(lock)이 필요하다. 리눅스 커널에서는 이를 위해 가장 많이 사용하는 것이 스핀록(spin lock)이다. '''스핀록은 최대한 하나의 스레드에 의해 잠길 수 있는 록을 말한다.''' 만약 어떤 스레드가 이미 잠겨진 스핀록을 다시 잠그려 시도한다면 그 스레드는 루프(busy loop)를 돌면서(spin) 록을 잠글 수 있을 때까지 기다린다. 스핀록은 가볍고 소유자가 하나인 록으로서 짧은 기간 동안만 소유 돼야 한다. 스핀록과 관련된 인터페이스는 ''''에 정의되어 있다. 스핀록의 기본적인 사용법은 다음과 같다. {{{ spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; spin_lock(&mr_lock); /* critical region ... */ spin_unlock(&mr_lock); }}} 스핀록은 SMP에서만 유효하다. UP 기기에서는 컴파일시 록이 제거되어 포함되지 않는다. 그러나 커널의 선점이 가능한가를 나타내는 표지로 사용될 수 있다. 만약 커널 선점이 비활성화 되어 있는 경우에는 컴파일 시 완전히 제거된다. '''주의: 스핀록은 재귀적이지 않다.''' 스핀록은 '''인터럽트 핸들러에서도 사용 가능하다.''' 인터럽트 핸들러에서 록을 사용할 경우에, 커널 내의 데이터를 인터럽트 핸들러에서 공유하는 경우 록을 얻기 전에 반드시 로컬 인터럽트를 비활성화해야 한다. 이때 로컬 인터럽트의 활성화 여부를 알 수 없다면 다음과 같은 인터페이스를 이용해야 한다. {{{ spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; unsigned long flags; spin_lock_irqsave(&mr_lock, flags); /* critical region ... */ spin_unlock_irqrestore(&mr_lock, flags); }}} 만약 인터럽트 활성화 상태임을 알고 있다면 다음의 인터페이스를 사용해도 된다. {{{ spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; spin_lock_irq(&mr_lock); /* critical region ... */ spin_unlock_irq(&mr_lock); }}} '''스핀록 디버깅''' 설정 옵션의 하나인 CONFIG_DEBUG_SPINLOCK은 스핀록 코드의 많은 디버깅 검사를 활성화시킨다. ==== 다른 스핀록 함수들 ==== 책 p124와 '''표 8.3 스핀록 함수 목록'''을 참고하라. ==== 스핀록과 보톰하프 ==== * spin_lock_bh() 주어진 록을 잠그고 모든 보톰하프를 비활성화 한다. * spin_unlock_bh() 위 함수와 반대로 작동한다. 보폼하프는 프로세스 컨텍스트 코드를 선점할 수 있으므로, 만약 어떤 데이터가 보폼하프와 프로세스 컨텍스트간에 공유된다면 록을 사용함과 함께 보폼하프를 비활성화시켜 데이터를 보호해야 한다. (문제) 1. 같은 형식의 태스크릿 내에서만 공유되는 데이터는 보호해야 하는가? 1. 서로 다른 형식의 태스크릿에서 공유되는 데이터는 보호해야 하는가? 1. softirq의 경우 같은 형식내에서만 공유되는 데이터는 보호해야 하는가? 1. softirq의 경우 서로 다른 형식내에서 공유되는 데이터는 보호해야 하는가? === 리더-라이터 스핀록 === 록의 사용이 리더(reader)와 라이터(writer)로 확연히 구분되는 경우에 사용한다. 읽는 중인 경우에는 쓰기만 방지해야 하며, 쓰기 중일 때는 읽기와 쓰기 모두 방지해야 한다. 리더-라이터 스핀록을 초기화하려면 다음과 같이 한다. {{{ rwlock_t mr_rwlock = RW_LOCK_UNLOCKED; }}} 리더인 경우에는 다음과 같이 사용한다. {{{ read_lock(&mr_rwlock); /* critical section (read only) ... */ read_unlock(&mr_rwlock); }}} 라이터인 경우에는 다음과 같이 사용한다. {{{ write_lock(&mr_lock); /* critical section (read and write) ... */ write_unlock(&mr_rwlock); }}} 리더록을 한 상태에서 라이트록을 할 수 없다. read_lock(&mr_rwlock); write_lock(&mr_rwlock); 이와 같은 경우엔 데드록에 빠지게 된다. 만약, 리더와 라이터가 섞이는 경우가 있다면 이때는 스핀록을 사용한다. 그리고 인터럽트 핸들러가 있을 때 프로세스 컨텍스트에서는 리더 스핀록을 인터럽트 핸들러에서는 라이터 스핀록을 사용한다면 인터럽트를 비활성화 시키는 록을 사용해야만 한다. 관련 함수는 책 p127의 '''표 8.4 리더-라이터 스핀록 함수'''를 참고하라. === 세마포어 === 리눅스의 세마포어(semaphore)는 '''휴면하는 록'''이라 생각할 수 있다. ==== 세마포어를 생성하고 초기화 ==== 세마포어 구현은 ''''에 정의돼 있다. 자료형은 struct smeaphore 이다. 세마포어는 정적과 동적으로 생성할 수 있다. * 정적 선언 static DECLARE_SEMAPHORE_GENERIC(name, count); 또는 count가 1인 뮤텍스를 다음과 같이 간단히 선언할 수도 있다. static DECLARE_MUTEX(name); * 동적 선언 sema_init(sem, count); 뮤텍스의 경우에는 다음과 같이 한다. init_MUTEX(sem); ==== 세마포어 사용 ==== 세마포어를 얻는 방법으로는 두가지가 있다. * down_interruptible() - 세마포어를 얻지 못하는 경우 휴면하게 된다. * down_trylock() - 세마포어를 얻지 못하는 경우 0이 아닌 값을 바로 리턴한다. 세마포어를 반환하는 경우에는 up()을 호출하면 된다. {{{ if (down_interruptible(&mr_sem)) return -ERESTARTSYS; /* 시그널을 받았지만 세마포어를 얻지 못함 */ /* critical region ... */ up(&mr_sem); }}} 관련함수는 책 p130 '''표 8.5 세마포어 함수'''를 참고하라. ==== 리더-라이터 세마포어 ==== 스핀록과 같은 방식으로 스핀록보다는 세마포어를 이용해야 하는 경우에 사용한다. 리더-라이터 세마포어는 ''''에 정의된 struct rw_semaphore 자료형에 의해 표현된다. 선언 방법은 다음과 같다. * 정적 선언 static DECLARE_RWSEM(name); *동적 선언 init_rwsem(struct rw_semaphore *sem) {{{ down_read(&mr_rwsem); /* critical region (read only) ... */ up_read(&mr_rwsem); down_write(&mr_rwsem); /* critical region (read and write) ... */ up_write(&mr_rwsem); }}} '''표8.6 스핀록과 세마포어중 어느 쪽이 접한한가''' ||'''요구사항'''||'''추천방법'''|| ||록 부담이 적어야 하는 경우||스핀록을 추천|| ||록 기간이 짧은 경우||스핀록을 추천|| ||록 기간이 긴 경우||세마포어를 추천|| ||인터럽트 핸들러에서 록을 해야 하는 경우||반드시 스핀록을 사용|| ||록을 소유한 채 휴면해야 하는 경우||반드시 세마포어를 사용|| === 완료 변수 === 완료 변수(completion variable)는 커널의 두 태스크를 동기화시키기 위해 사용되는 편리한 방법 중 하나이다. 완료 변수는 '''' 정의된 struct completion 자료형에 의해 표현된다. 선언은 다음과 같다. * 정적 선언 DECLARE_COMPLETION(mr_comp); * 동적 선언 init_completion() 어떤 완료 변수에 대하여 대기하기를 원하는 태스크들은 wait_for_competion()을 호출한다. 이 후 이벤트가 도착하면 complete()을 호출하여 대기중인 모든 태스크를 깨우게 된다. 관련 함수는 책 p132 '''표 8.7 완료 변수 함수'''을 참고하라. === 큰 커널 록 === 큰 커널 록(Big Kernel Lock(BKL)은 전역 스핀록이다. * lock_kernel() * unlock_kernel() 자세한 내용은 책 p133, 134을 참고하라. === Seq 록 === 커널 2.6에서부터 도입된 새로운 종류의 록이다. 이 록은 시퀀스 카운터(sequence counter)를 사용하여 구현된다. 록은 0부터 시작하여 록을 잠그면 홀수가 되고 다시 록을 풀면 짝수가 된다. {{{ seqlock_t mr_seq_lock = SEQLOCK_UNLOCKED; write_seqlock(&mr_seq_lock); /* write lock is obtained ... */ write_sequnlock(&mr_seq_lock); unsigned long seq; do { seq = read_seqbegin(&mr_seq_lock); /* read data here ... */ } while (read_seqretry(&mr_seq_lock, seq)); }}} === 선점의 비활성화 === 커널 역시 선점형이므로, 커널 안에 있는 프로세스는 언제라도 더 높은 우선순위를 갖는 프로세스를 실행시키기 위해 중단될 수 있다. {{{ preempt_disable(); /* 선점이 비활성화됨 ... */ preempt_enable(); }}} 관련 함수는 책 p136 '''표 8.9 커널 선점 관련 함수'''를 참고하라. === 배리어 === 다수의 프로세서나 하드웨 디바이스간의 동기화 문제를 다룰 때는, 종종 메모리 읽기(load)와 메모리 쓰기(store)를 프로그램 코드에 정의된 순서대로 행해야 하는 경우가 발생한다. 순서를 바꾸지 말도록 지시하는 명령을 '''배리어(barrier)'''라고 한다. * rmb() - 읽기 메모리 배리어를 제공 * wmb() - 쓰기 메모리 배리어를 제공 * mb() - 읽기/쓰기 배리어 제공 관련 함수는 책 p139 '''표 8.10 메모리/컴파일러 배리어 함수'''를 참고하라.