PINTOS - Synchronization

JinJinJara·2023년 9월 22일
0

PINTOS

목록 보기
5/10

Synchronization

  • 동기화 하는 가장 단순한 방법 : 인터럽트를 불가능하게 하는 것 (= 일시적으로 CPU가 인터럽트에 응답하는 것을 막기)

  • 쓰레드 선점(preempt) : timer interrupt에 의해 이뤄짐

  • 인터럽트가 꺼지면, 다른 쓰레드는 진행중인 쓰레드를 선점 불가

  • 인터럽트가 켜져있으면, 진행 중인 쓰레드가 언제든지 다른 쓰레드에 의해서 선점 가능

  • Pintos는 “선점가능한(preemptible) 커널” -> 선점가능한 커널은 더 명시적인 동기화가 필요

  • 관습적인 Unix 시스템은 “선점불가능(nonpreemptible)”

1. intr_level

INTR_OFF 이거나 INTR_ON 의 값을 갖고, 각각 인터럽트가 비활성화 상태인지 활성화 상태인지를 알려줌

enum intr_level;

2. intr_get_level

현재 인터럽트 상태(inter_level)를 리턴

enum intr_level intr_get_level (void)

3. intr_set_level

현재 상태(inter_level)에 따라 인터럽트를 활성화하거나 비활성화하며, 인터럽트의 이전 상태를 리턴

enum intr_level intr_set_level (enum intr_level level);

4. intr_enable

인터럽트를 활성화하며, 인터럽트의 이전 상태를 리턴

enum intr_level intr_enable (void);

5. intr_disable

인터럽트를 비활성화 해주며, 인터럽트의 이전 상태를 리턴

enum intr_level intr_disable (void);

Semaphores 세마포어

세마포어는 비음수 정수 값을 갖는 변수로 두개의 연산자를 통해서 원자적으로 조작 가능

  • “Down” or “P” : 값이 양수가 되기를 기다렸다가, 양수가되면 감소
  • “UP” or “V” : 값을 증가 ( P연산에서 wait 중인 쓰레드가 있다면 하나를 깨우기)
  • ex) 쓰레드 A는 sema_down() 에서 실행을 멈추고 쓰레드 B가 sema_up()을 콜하길 기다린다.
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);
}

1. sema_init

구조체 세마포어 선언

struct semaphore;

2. sema_init

새로운 세마포어 구조체인 sema를 주어진 초기값으로 초기화

void sema_init (struct semaphore *sema, unsigned value);

3. sema_down

“down” or “P” 연산을 sema에 실행합니다. 세마의 값이 양수가 될 때까지 기다렸다가 양수가 되면 1만큼 빼게 됩니다.

void sema_down (struct semaphore *sema);

4. sema_up

sema의 값을 증가시키는 “up” or “V” 연산 실행

void sema_up (struct semaphore *sema);
  • 만약 sema에 기다리는 쓰레드가 있다면, 그들 중 하나를 깨움
  • 다른 동기화 함수들과 다르게 sema_up()은 외부 인터럽트 핸들러 안에서 호출 된다.

Locks 락

락은 초기값을 1로 하는 세마포어와 같다.
(sema) up = (lock) release
(sema) down = (lock) acquire

  • 오직, 락을 갖고있는 owner 쓰레드만이 락을 놓아줄 수 있음

1. lock

락 구조체 lock

struct lock;

2. lock_init

새로운 lock 구조체를 초기화 (처음엔 어떤 쓰레드도 해당 lock을 소유X)

void lock_init (struct lock *lock);

3. lock_acquire

현재 쓰래드에서 lock을 획득

void lock_acquire (struct lock *lock);
  • 만약 현재의 lock owner가 lock을 놓아주기를 기다려야한다면, 기다리기

4. lock_try_acquire

기다리지 않고 현재 쓰레드가 사용할 락을 얻으려는 목적
성공하면 true, 실패(이미 다른 쓰레드가 사용 중)하면 false 를 리턴

bool lock_try_acquire (struct lock *lock);
  • 이 함수만 들어있는 반복문을 만들어서 계속 호출하는 건 CPU 시간을 많이 낭비하기 때문에, 대신lock_acquire()를 사용하기

5. lock_release

락을 놓아주기 (현재 쓰레드가 소유 중이어야 함)

void lock_release (struct lock *lock);

6. lock_held_by_current_thread

running 상태의 쓰레드가 락을 갖고있다면 true, 아니면 false 를 리턴

bool lock_held_by_current_thread (const struct lock *lock):

Monitors

세마포어나 락 보다 더 높은 (추상화) 수준의 동기화 방법

구성

: 동기화된 데이터, 모니터락이라 부르는 락, 한개 이상의 컨디션 변수로 이루어짐

  • 보호받는 데이터에 접근하기전에 쓰레드는 모니터락을 얻는다

  • 모니터락을 얻으면 in the monitor (모니터 안에 있다) 라고 부른다.

  • 모니터안에서 쓰레드는 모든 보호받는 데이터에 접근 가능

  • 컨디션 변수

    • 특정 컨디션(조건)이 참이 될 때까지 모니터안의 코드가 기다릴 수 있게한다.

    • 각 컨디션 변수는 추상적인 조건과 관련있다.

      • ex) 프로세스가 진행되는 동안 몇가지 데이터가 도착하는 조건
        , 유저의 마지막 키보드 입력으로 부터 10초가 넘게 지나는 조건
    • 모니터안의 코드가 특정 조건이 참이 되기를 기다릴 때, 그 코드는 원하는 조건과 관련된 컨디션 변수를 wait.

    • 그 컨디션 변수는 락을 놓아주고 컨디션이 신호받는 것을 기다리기

    • 반대로 이 코드가 특정 조건이 참이되도록 만드는 경우에는, 그 참이된 조건을 신호로 보내서(signal) 기다리는 코드 하나를 깨우거나, 전부한테 알려서(broadcast) 자는 코드를 전부 깨우기

1. condition

컨디션 변수 구조체 condition

struct condition;

2. cond_init

void cond_init (struct condition *cond);

cond 를 새로운 컨디션 변수로 초기화


3. cond_wait

원자적으로 락 (모니터락)을 놓아주고 컨디션 변수 cond가 다른 코드로부터 신호받기를 기다린다. cond가 신호를 받으면 리턴전에 락을 다시 획득한다. 이 함수를 콜하기전에 꼭 락을 갖고 있어야 한다.

void cond_wait (struct condition *cond, struct lock *lock);

4. cond_broadcast

cond를 기다리는 쓰레드가 있다면 (cond는 모니터락으로 보호받습니다), 모든 쓰레드를 깨운다.
이 함수를 콜하기전에 꼭 락을 갖고 있어야 한다.

void cond_broadcast (struct condition *cond, struct lock *lock);

Optimization Barriers 최적화 장벽

컴파일러가 메모리 상태에 대해 어떤 가정을 못하게 막아주는 특별한 명령문

  • 메모리의 읽기(read), 쓰기(write) 순서를 강제로 정해줄 때 사용 가능

  • 만약 장벽이 없으면, 컴파일러는 아래의 루프를 없애 버릴 수도 있다
    ( 이 반복문이 어떤 결과물도 만들어내지 않고 부작용도 없기 때문 )

   while (loops-- > 0)
      barrier ();
/*
   busy-wait 하게 반복문을 돌면서 원래의 값이 0 으로 내려갈 때 까지 기다리는 반복문
*/
  • 장벽은 그 반복문이 중요한 영향이 있다고 컴파일러를 속인다.

    • 컴파일러 : 장벽에 막혀서 read와 write의 순서를 재정렬하지 않고, 변수의 값이 수정되지 않는다고 가정함 (지역 변수의 주소가 주어지지 않은 경우는 제외)

장벽 사용 예시

  • 목표 : 기능 추가
    • Boolean전역 변수 timer_do_put 이 참 일때만 전역 변수 timer_put_char 가 콘솔에 출력 되는 기능
    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가 명시적인 장벽을 거의 갖고있지 않는 이유이다.

같은 소스 파일안에서 정의된 함수포함된 헤더를 최적화 장벽으로 기대하면 안된다.

최적화 장벽은 심지어 함수를 정의하기 전에도, 해당 함수의 사용에 대해 적용될 수 있다.

이유는 컴파일러는 최적화를 수행하기 전에 먼저 전체 소스 파일을 읽고 파싱하기 때문입니다.

0개의 댓글