Jungle

[TIL] 인터럽트 / lock, monitor

손가든 2023. 11. 24. 15:59

 

인터럽트(Interrupt)

 

인터럽트는 컴퓨터 시스템에서 발생하는 이벤트로, 프로세서의 정상적인 실행을 중단하고 특정한 코드(인터럽트 핸들러 또는 인터럽트 서비스 루틴)를 실행하는 메커니즘이다.

 

 

어제 포스팅한 Synchronization(동기화)를 하는 가장 간단한 방법은 인터럽트를 불가능하게 하는 것이다. 

 

일시적으로 CPU가 인터럽트에 응답하는 것을 막으면 다른 쓰레드는 진행중인 쓰레드를 선점할 수 없게 된다.

 

쓰레드 선점(preempt)는 timer interrupt에 의해 이뤄지기 때문.

 

 

더보기

CPU 동작 측면에서 인터럽트는 다음과 같은 과정을 거친다.

 

1. 인터럽트 발생

 

하드웨어나 소프트웨어에서 인터럽트가 발생한다.

 

이는 외부 장치의 신호, 오류, 타이머, 입출력 완료 등의 다양한 이벤트에 의해 일어날 수 있다.

 

 

2. 현 프로세스 상태 저장

 

현재 실행 중인 프로세스의 상태(PC, 레지스터의 값)가 저장된다.

 

이는 현재 진행 중인 작업을 나중에 다시 시작하기 위한 것이다.

 

 

3. 인터럽트 서비스 루틴 실행

 

인터럽트 발생 시에는 특정한 코드 블록(인터럽트 서비스 루틴 또는 핸들러)이 실행된다.

 

이 코드는 인터럽트가 발생한 원인에 따라 다르다.

 

이후 해당 이벤트를 처리하면 다음 동작을 결정한다.

 

 

4. 인터럽트 서비스 루틴 실행 후 복구

 

인터럽트 서비스 루틴이 실행된 후, 이전에 저장된 현재 프로세스의 상태를 복원한다.

 

 

5. 프로세스 재개

 

복구된 상태에서 현재 실행 중이던 프로세스가 계속해서 실행된다.

 

 

우리는 커널 쓰레드와 외부 인터럽트의 발생 간의 동기화를 지켜주기 위해 외부 인터럽트를 비활성화하는 방법을 사용해야 한다.

 

외부 인터럽트는 끄지 않는 이상 막을 방법이 없기 때문이다.

(일부는 비활성화를 해도 막을 수 없는 마스크 불가능 인터럽트(NMls)인데, pintos에는 존재하지 않는다.)

 

따라서 인터럽트를 활성화, 비활성화시키는 자료형과 함수들을 이용하여 해당 과제를 해결해야 한다.

 

해당 자료형과 함수들은 'include/threds/interrupt.h' 에 존재한다.

 


 

 

락 (LOCK)

 

락은 세마포어의 value가 1인 동기화 방법과 같지만 바톤의 개념이 추가된다.

 

락의 구조체에는 바톤을 가지고 있는 주체 쓰레드가 기억되고, 구조체 내에는 또 다른 멤버로 value가 1인 semaphore 구조체를 가지고 있다.

 

바톤을 가진 주체가 또 바톤을 획득하려는 시도가 불가능하므로 재귀적인 함수에서는 사용할 수 없다.

 

또한 바톤을 가진 쓰레드만이 value가 1인 세마포어와 같은 락을 조절할 수 있다.

 

 

모니터 (Monitor)

 

모니터는 락을 가지고 있는 구조체에 컨디션 변수가 추가된다.

 

세마포어나 뮤텍스 같은 방식은 개발자가 임계구역으로 들어가기전 wait(), 임계구역을 빠져나올때 signal 혹은 release를 해주는 등의 코드를 프로그래머가 직접 넣어주어야 하므로, 실수를 하여 오류가 발생할 수 있다.

 

따라서 모니터라는 세마포어나 락보다 더 높은 추상화 수준의 동기화 방법을 사용할 수 있다.

 

 

모니터 구조체에는 동기화된 데이터, 모니터락, 한개 이상의 컨디션 변수로 이루어지는데,

 

모니터 락을 획득한 쓰레드는 모든 보호받는 데이터에 접근 할 수 있다.

 

모니터가 가진 특이점컨디션 변수가 구조체 안에 존재한다는 것인데, 이는 특정 조건이 충족될 때 까지 기다리게 할 수 있으며, 공유 자원의 특정 상태를 기다리고 있는 쓰레드들이 이 컨디션을 통해 자발적으로 대기하도록 할 수 있다.

 

락이나 뮤텍스, 세마포어는 다른 쓰레드가 블록킹 상태로 전환하였기 때문에 접근해도 이용할 수 없었다면,

 

모니터 락은 내가 진입하고 싶은 곳에 해당 컨디션이 만족될 경우 들어가겠다 라는 자발적 대기 방법을 사용하는 것이다.

 

 

전통적인 버퍼에 문자열을 넣고 빼는 두 쓰레드에 대한 예제를 보고 이해할 수 있다.

 

    char buf[BUF_SIZE];            /* Buffer. */
    size_t n = 0;                  /* 0 <= n <= BUF SIZE: # of characters in buffer. */
    size_t head = 0;               /* buf index of next char to write (mod BUF SIZE). */
    size_t tail = 0;               /* buf index of next char to read (mod BUF SIZE). */
    struct lock lock;              /* Monitor lock. */
    struct condition not_empty;    /* Signaled when the buffer is not empty. */
    struct condition not_full;     /* Signaled when the buffer is not full. */

    ...initialize the locks and condition variables...

    void put (char ch) {
      lock_acquire (&lock);
      while (n == BUF_SIZE)               /* Can't add to buf as long as it's full. */
        cond_wait (&not_full, &lock);
      buf[head++ % BUF_SIZE] = ch;        /* Add ch to buf. */
      n++;
      cond_signal (&not_empty, &lock);    /* buf can't be empty anymore. */
      lock_release (&lock);
    }

    char get (void) {
      char ch;
      lock_acquire (&lock);
      while (n == 0)                      /* Can't read buf as long as it's empty. */
        cond_wait (&not_empty, &lock);
      ch = buf[tail++ % BUF_SIZE];        /* Get ch from buf. */
      n--;
      cond_signal (&not_full, &lock);     /* buf can't be full anymore. */
      lock_release (&lock);
    }

 

 

put 함수와 get 함수가 1번씩 서로 끝났을 경우 실행되어야 하며, 이들은 자발적으로 서로가 signal을 보내줄 때 까지 기다리도록 설계되었다.

 

따라서 모니터 안에 쓰레드가 진입하는 방법인 락을 획득하는 것은 동시에 여러 쓰레드가 가능하지만

 

con_wait 과 cont_signal을 통해 서로 상호 배타적인 실행이 가능하다.