동기화 하는 가장 단순한 방법 : 인터럽트를 불가능하게 하는 것 (= 일시적으로 CPU가 인터럽트에 응답하는 것을 막기)
쓰레드 선점(preempt) : timer interrupt에 의해 이뤄짐
인터럽트가 꺼지면, 다른 쓰레드는 진행중인 쓰레드를 선점 불가
인터럽트가 켜져있으면, 진행 중인 쓰레드가 언제든지 다른 쓰레드에 의해서 선점 가능
Pintos는 “선점가능한(preemptible) 커널” -> 선점가능한 커널은 더 명시적인 동기화가 필요
관습적인 Unix 시스템은 “선점불가능(nonpreemptible)”
INTR_OFF 이거나 INTR_ON 의 값을 갖고, 각각 인터럽트가 비활성화 상태인지 활성화 상태인지를 알려줌
enum intr_level;
현재 인터럽트 상태(inter_level)를 리턴
enum intr_level intr_get_level (void)
현재 상태(inter_level)에 따라 인터럽트를 활성화하거나 비활성화하며, 인터럽트의 이전 상태를 리턴
enum intr_level intr_set_level (enum intr_level level);
인터럽트를 활성화하며, 인터럽트의 이전 상태를 리턴
enum intr_level intr_enable (void);
인터럽트를 비활성화 해주며, 인터럽트의 이전 상태를 리턴
enum intr_level intr_disable (void);
세마포어는 비음수 정수 값을 갖는 변수로 두개의 연산자를 통해서 원자적으로 조작 가능
struct semaphore sema;
/* Thread A */
void threadA (void) {
sema_down (&sema);
}
/* Thread B */
void threadB (void) {
sema_up (&sema);
}
/* main function */
void main (void) {
sema_init (&sema, 0);
thread_create ("threadA", PRI_MIN, threadA, NULL);
thread_create ("threadB", PRI_MIN, threadB, NULL);
}
구조체 세마포어 선언
struct semaphore;
새로운 세마포어 구조체인 sema를 주어진 초기값으로 초기화
void sema_init (struct semaphore *sema, unsigned value);
“down” or “P” 연산을 sema에 실행합니다. 세마의 값이 양수가 될 때까지 기다렸다가 양수가 되면 1만큼 빼게 됩니다.
void sema_down (struct semaphore *sema);
sema의 값을 증가시키는 “up” or “V” 연산 실행
void sema_up (struct semaphore *sema);
락은 초기값을 1로 하는 세마포어와 같다.
(sema)up
= (lock)release
(sema)down
= (lock)acquire
락 구조체 lock
struct lock;
새로운 lock 구조체를 초기화 (처음엔 어떤 쓰레드도 해당 lock을 소유X)
void lock_init (struct lock *lock);
현재 쓰래드에서 lock을 획득
void lock_acquire (struct lock *lock);
기다리지 않고 현재 쓰레드가 사용할 락을 얻으려는 목적
성공하면 true, 실패(이미 다른 쓰레드가 사용 중)하면 false 를 리턴
bool lock_try_acquire (struct lock *lock);
락을 놓아주기 (현재 쓰레드가 소유 중이어야 함)
void lock_release (struct lock *lock);
running 상태의 쓰레드가 락을 갖고있다면 true, 아니면 false 를 리턴
bool lock_held_by_current_thread (const struct lock *lock):
세마포어나 락 보다 더 높은 (추상화) 수준의 동기화 방법
: 동기화된 데이터, 모니터락이라 부르는 락, 한개 이상의 컨디션 변수로 이루어짐
보호받는 데이터에 접근하기전에 쓰레드는 모니터락을 얻는다
모니터락을 얻으면 in the monitor (모니터 안에 있다)
라고 부른다.
모니터안에서 쓰레드는 모든 보호받는 데이터에 접근 가능
컨디션 변수
특정 컨디션(조건)이 참이 될 때까지 모니터안의 코드가 기다릴 수 있게한다.
각 컨디션 변수는 추상적인 조건과 관련있다.
모니터안의 코드가 특정 조건이 참이 되기를 기다릴 때, 그 코드는 원하는 조건과 관련된 컨디션 변수를 wait.
그 컨디션 변수는 락을 놓아주고 컨디션이 신호받는 것을 기다리기
반대로 이 코드가 특정 조건이 참이되도록 만드는 경우에는, 그 참이된 조건을 신호로 보내서(signal) 기다리는 코드 하나를 깨우거나, 전부한테 알려서(broadcast) 자는 코드를 전부 깨우기
컨디션 변수 구조체 condition
struct condition;
void cond_init (struct condition *cond);
cond 를 새로운 컨디션 변수로 초기화
원자적으로 락 (모니터락)을 놓아주고 컨디션 변수 cond가 다른 코드로부터 신호받기를 기다린다. cond가 신호를 받으면 리턴전에 락을 다시 획득한다. 이 함수를 콜하기전에 꼭 락을 갖고 있어야 한다.
void cond_wait (struct condition *cond, struct lock *lock);
cond를 기다리는 쓰레드가 있다면 (cond는 모니터락으로 보호받습니다), 모든 쓰레드를 깨운다.
이 함수를 콜하기전에 꼭 락을 갖고 있어야 한다.
void cond_broadcast (struct condition *cond, struct lock *lock);
컴파일러가 메모리 상태에 대해 어떤 가정을 못하게 막아주는 특별한 명령문
메모리의 읽기(read), 쓰기(write) 순서를 강제로 정해줄 때 사용 가능
만약 장벽이 없으면, 컴파일러는 아래의 루프를 없애 버릴 수도 있다
( 이 반복문이 어떤 결과물도 만들어내지 않고 부작용도 없기 때문 )
while (loops-- > 0)
barrier ();
/*
busy-wait 하게 반복문을 돌면서 원래의 값이 0 으로 내려갈 때 까지 기다리는 반복문
*/
장벽은 그 반복문이 중요한 영향이 있다고 컴파일러를 속인다.
timer_put_char = 'x';
barrier ();
timer_do_put = true;
장벽이 없다면 버그 발생
그 외 솔루션 : 인터럽트 비활성화
enum intr_level old_level = intr_disable ();
timer_put_char = 'x';
timer_do_put = true;
intr_set_level (old_level);
컴파일러는 다른 소스 파일(외부)에서 정의된 함수의 호출에 대해 최소의 최적화 장벽으로서 대한다.
컴파일러는 외부에서 정의된 함수들이 정적, 동적으로 할당된 어떤 데이터나 주소가 정해진 어떤 지역변수들
에도 접근 할 수 있다고 가정한다. (= 명시적인 장벽이 생략될 수 있다)
Pintos가 명시적인 장벽을 거의 갖고있지 않는 이유이다.
같은 소스 파일안에서 정의된 함수
나 포함된 헤더
를 최적화 장벽으로 기대하면 안된다.
최적화 장벽은 심지어 함수를 정의하기 전
에도, 해당 함수의 사용에 대해 적용될 수 있다.
이유는 컴파일러는 최적화를 수행하기 전에 먼저 전체 소스 파일
을 읽고 파싱
하기 때문입니다.