1. 하이브리드 시스템(Hybrid Paging/Segmentation System)

  • 페이징 시스템과 세그멘테이션 시스템의 장점을 결합
  • 프로그램을 논리적인 단위인 세그먼트(segment)로 분할한 뒤 각 세그먼트를 다시 고정된 크기의
    page로 분할하여 메모리에 적재할 때는 page 단위로 적재하는 방식
  • sharing, protection 문제를 해결하기 위해 프로세스 자체는 세그먼트 단위로 분할했지만 결국
    메모리에 적재되는 것은 page이기 때문에 메모리 관리 방식은 FPM과 유사

  • SMT, PMT를 모두 사용하기에 테이블 수가 증가하여 메모리 사용량이 증가하고 주소 매핑 과정이
    복잡해진다.
  • SMT, PMT를 모두 참조하기 위해 메모리 액세스 횟수가 3번(SMT, PMT, 목적 주소)으로 증가하기 때문에
    직접 매핑 방식으로는 성능이 저하되는 문제가 있음(TLB 사용이나 다른 여러 기법으로 극복 가능)

 

2. 주소 매핑(Address Mapping)

  • (segment number, page number, offset)의 쌍으로 이루어진 가상주소를 사용

  • SMT와 PMT를 모두 사용
    • 프로그램은 세그먼트 단위로 분할되었기 때문에 각 프로그램(프로세스)마다 SMT를 가짐
    • 세그먼트는 페이지 단위로 분할되었기 때문에 각 세그먼트마다 PMT를 가짐

  • 주소 변환 과정
    • SMT의 시작 주소가 bs 이고 세그먼트 번호 s, 페이지 번호 p, offset d가 주어졌을 때,
      해당 세그먼트의 엔트리 탐색 (= bs + s * SMT_entrysize), PMT의 시작주소인 bp를 구한다.

    • PMT에서 해당 페이지의 엔트리를 탐색(= bp + p * PMT_entrysize)하고  residence bit를 체크,
      메모리에 적재되어있지 않을 경우 page fault를 발생시켜 메모리에 적재하는 과정을 거친다.

    • PMT를 참조하여 페이지 프레임 번호를 구하고 offset을 더해 메모리상의 실제 주소를 계산
      (= page frame number * page_size + d)

  • 메모리 적재 단위는 page이기 때문에 SMT에는 residence bit 이 존재하지 않음

1. 세그멘테이션 시스템(Segmentation System)

  • 프로그램을 논리적인 단위로 분할(segment)하는 방식
  • 블록의 크기가 서로 다를 수 있음

  • 고정크기로 분할하는 방식이 아니기때문에 메모리를 미리 분할해둘 수 없음
    => 가변 분할 방식(Variable Partition Multi-Programming)과 유사

  • 가변 분할 방식과 마찬가지로 내부 단편화는 발생하지 않으나 외부 단편화는 발생할 수 있음

  • 페이징 시스템에 비해 주소 매핑과 메모리관리의 오버헤드가 큼
  • 프로그램의 논리적 흐름을 고려한 분할방식이기에 블록(세그먼트)의 공유, 보호가 쉬움

 

 

2. 주소 매핑(Address Mapping)

  • (segment number, offset) 의 쌍으로 이루어진 가상 주소를 사용

  • SMT(Segment Map Table)을 사용하여 가상주소를 실제주소에 매핑

  • SMT에서는 세그먼트가 메모리에 적재되어있는지 여부를 나타내는 residence bit와 
    swap device 상의 어느 위치에 저장되어있는지를 나타내는 secondary storage address(= 실제주소),
    메모리상의 주소(segment address in memory), 세그먼트의 길이(segment legnth)
    세그먼트 보호를 위한 protection bits 등의 값을 관리한다.

  • 프레임 번호만 알면 실제 메모리에서의 위치를 알 수 있었던 페이징 시스템과 달리 세그먼트의 길이가
    서로 다르기 때문에 segment length 가 추가적으로 필요해진다.

  • 페이징 시스템에서는 페이지가 논리적으로 분할된 블록이 아니기에 protections bits를 사용한 권한 관리가
    복잡했지만 세그멘테이션 시스템의 경우 한 블록이 하나의 논리적인 단위이기에 세그먼트 단위로 권한을 
    관리할 수 있어 세그먼트 보호가 훨씬 용이해진다.

  • 주소 변환 과정
    • SMT의 시작주소가 b이고 세그먼트 번호는 s, offset은 d 일 때

    • SMT에 접근하여 세그먼트(s)의 엔트리 위치를 탐색(b + (entry_size * s)

    • 해당 세그먼트의 residence bit를 체크, 메모리에 적재되지 않은 상태라면 segment fault가
      발생하고 페이징 시스템의 page fault 때와 마찬가지로 커널에 요청하여 세그먼트를 메모리에 적재

    • offset(d) 이 세그먼트의 범위를 벗어나는 경우 overflow 가 발생, 이를 처리하는 루틴을 거침 
    • protection bits 를 체크하여 허용되지 않은 연산을 시도할 경우 예외가 발생, 이를 처리하는 루틴을 거침

    • 문제가 없다면 실제 주소(r = entry_address + d)를 계산하여 메모리에 접근

 

 

3. 세그멘테이션 시스템의 메모리 관리

  • 페이징 시스템이 FPM과 유사했듯이 세그멘테이션 시스템은 VPM과 유사한 방식으로 메모리를 관리

  • 세그먼트 적재시 크기에 맞춰 메모리를 분할하여 적재, Partition Table을 통해 이를 관리한다.

  • 파티션 테이블(Partition Table)
    • 페이징 시스템의 프레임 테이블에 대응하는 메모리 관리를 위한 테이블

    • 시작 주소(start address)
      • 파티션의 시작 주소

    • 크기(size)
      • 파티션의 크기

    • PID(Process ID)
      • 해당 파티션을 사용중인 프로세스의 ID

    • 세그먼트 번호(segment number)
      • 해당 파티션에 적재된 세그먼트의 번호

    • 기억장치 접근권한(storage protection key)
      • 해당 파티션에 대한 접근 권한 정보

  • 세그먼트 공유(Segment Sharing)
    • 페이징 시스템의 경우 페이지가 논리적 단위가 아니기 때문에 특정 페이지에서 branch(=jump)가
      발생할 경우 프로세스에 따라 목적지 주소가 달라져야하는 모호한 경우가 생길 수 있었으며
      이를 공유하는 페이지에 대한 정보는 PMT의 같은 엔트리에 저장하는 등의 방식으로 해결하였음

    • 세그먼트의 경우 애초에 논리적인 단위로 프로그램을 분할하였기에 jump가 세그먼트 내에서 발생하므로
      이러한 문제가 발생할 일이 없어 세그먼트 공유가 용이함

  • 세그먼트 보호(Segment Protection)
    • 페이징 시스템의 경우 하나의 페이지가 하나의 논리적 흐름을 가지는 것이 아니기에 페이지 보호가 복잡했음

    • 세그멘테이션 시스템의 경우 하나의 세그먼트는 하나의 논리적 단위이기에 세그먼트 단위로 protection bits를 
      적용하는 것만으로 간단하게 세그먼트 보호가 가능해짐

1. 페이징 시스템(Paging System)

  • 프로그램(프로세스)을 같은 크기의 블록(= page)으로 분할하여 관리하는 시스템
    => 고정 분할 방식(Fixed Partition Multi-Programming)과 유사

  • 메모리를 같은 크기의 페이지로 분할하는 방식이기에 간편하고 효율적인 관리가 가능

  • 프레임단위로 메모리를 할당하기에 외부 단편화는 발생하지 않음

  • 프로그램의 크기가 페이지의 크기로 나누어 떨어지지 않는다면 마지막 페이지에서 내부 단편화가 발생

  • 프로그램의 논리적 구조를 고려하지 않은 방식이기에 페이지 공유, 페이지 보호 등이 복잡함

 

  • 페이지(Page)
    • 프로그램의 분할된 블록

 

  • 프레임(Frame)
    • 메모리가 분할된 블록
    • Page와 같은 크기를 가짐
    • 프로그램의 page -> 메모리의 frame에 대응

 

 

2. 주소 매핑(Address Mapping)

  • (page number, offset) 의 쌍으로 이루어진 가상 주소를 사용

  • PMT(Page Map Table)을 사용하여 가상주소를 실제주소에 매핑

  • PMT에서는 페이지가 메모리에 적재되어있는지 여부를 나타내는 residence bit와 
    swap device 상의 어느 위치에 저장되어있는지를 나타내는 secondary storage address(= 실제주소),
    메모리상의 어느 프레임에 적재되어있는지를 나타내는 page frame number 등의 값을 관리한다.

 

  • 직접 매핑(Direct Mapping)
    • PMT의 시작 주소가 b 이고 page 번호는 p, offset 이 d 일 때, 먼저 PMT에서 해당 엔트리를 참조
      => PMT에서의 해당 엔트리는 b + (entry_size * p)

    • residenct bit 이 1이라면 page frame number를 참조하여 적재된 메모리 프레임의 주소를 계산
      => 메모리에서의 시작 주소는 (page frame number * page_size) + d

    • 만약 residence bit 이 0이라면 메모리에 적재되지 않은 상태이기 때문에 page fault가 발생하고
      프로그램(프로세스)은 커널에 swap device상의 페이지를 메모리 프레임에 적재하도록 요청한 뒤
      요청이 완료될 때 까지 Block 상태가 되어 대기, 요청이 완료되면 다시 Ready 상태가 되어
      단기 스케줄러에 의해 프로세서를 할당받으면 다시 작업을 수행하게 된다. 

    • PMT가 커널에 존재하는 방식이기에 PMT 참조를 위해 한 번, 실제로 메모리를 참조하기 위해 한 번
      총 두 번의 메모리에 대한 접근이 필요하기 때문에 성능이 저하되며 PMT를 저장해둘 메모리 공간도
      별도로 필요하다.

 

  • 관계 매핑(Associated Mapping)
    • 직접 매핑에서 PMT의 참조 과정을 별도의 장비(TLB)에게 맡기는 방식
       
    • TLB(Translation Lookaside Buffer)
      • 메모리 접근 횟수에 따른 성능 저하를 막기 위해 사용하는 전용 기억장치

      • PMT 참조 과정이 병렬 탐색을 통해 매우 빠른 속도로 이루어지도록 설계된 전용 하드웨어

      • 성능만큼이나 가격이 매우 비싸기 때문에 큰 PMT를 다루기엔 어려움이 따름

 

  • 절충안(Hybrid Direct/Associative Mapping)
    • 직접 매핑과 관계 매핑을 절충하여 비용은 최소화하고 성능은 최대화하는 방식

    • 작은 크기의 TLB를 PMT 전용의 캐시메모리처럼 사용하여 PMT 자체는 커널공간에 저장하고
      PMT중 일부 엔트리만을 TLB에 적재하여 성능을 높인다.

    • 물론 기존에 배웠던 캐시메모리와 마찬가지로 최근에 사용된 영역이나 그 인접영역이
      또다시 사용될 가능성이 높기 때문에(locality) 최근에 사용된 페이지들에 대한 entry 위주로
      TLB에 적재한다.

 

  • TLB에 PMT의 엔트리가 적재되어있다면 바로 TLB를 통해 작업을 수행하고(Associative Mapping)
    적재되어있지 않다면 Direct Mapping 방식으로 작업을 수행, 해당 엔트리를 TLB에 캐싱한다.

 

 

3. 페이징 시스템의 메모리 관리

  • 페이지와 같은 크기로, 즉 프레임(frame) 단위로 메모리를 분할하여 관리하고 사용
    => 연속 메모리 할당에서 봤던 고정 분할 방식과 유사

 

  • 프레임 테이블(Frame Table)
    • 각 프레임을 관리하기 위한 테이블

    • 구성
      • Page Frame Number
        • 페이지 프레임 번호
        • 메모리상의 몇 번째 프레임인지 나타내는 필드

      • Allocated/Available
        • 해당 프레임이 할당된 상태인지, 혹은 사용 가능한 상태인지를 나타내는 필드

      • PID
        • 어느 프로세스가 사용중인지 나타내는 필드

      • Link
        • 사용 가능한 프레임들에 바로 접근하기 위한 연결리스트

      • AV(Availabe)
        • Link의 시작점(head)

 

  • 페이지 공유(Page Sharing)
    • 여러 프로세스가 특정 page를 공유할 수 있음

    • 연속 메모리할당 방식이 아니기 때문에 가능한 방식

    • 공유 가능한 페이지
      • 프로시저 페이지 => 함수 등 재사용되는 순수 코드
      • 데이터 페이지 => 읽기 전용의 경우 공유 가능, 쓰기도 가능한 경우 병행 제어 기법 필요(동기화 등)

    • 문제점
      • 여러 프로세스가 공유하는 페이지에서 branch(= jump)가 발생하는 경우
        jump의 목적지 주소를 어디로 잡아야할지 모호해지는 문제가 발생
        => 페이지는 고정된 크기로 분할했을 뿐 논리적인 단위가 아니기 때문

      • 해결책
        • 공유 페이지에 대한 정보는 PMT의 같은 엔트리에 저장하도록 할 수 있음

 

  • 페이지 보호(Page Protection)
    • 공유되는 페이지에 대한 각 프로세스의 작업 권한을 protection bit를 통해 관리

    • 각 프로세스는 자신의 페이지 테이블을 참조하여 허용된 작업만을 수행할 수 있음

    • Valid / Read / Write / Execution 의 4비트로 이루어지며 각각 메모리 적재여부,
      읽기 가능 여부, 쓰기 가능 여부, 실행 가능 여부를 나타낸다

 

1. 가상 메모리(Virtual Memory)

  • 실제로 사용 가능한 저장 장치를 논리적으로 추상화하여 실제 메모리처럼 보여주는 메모리 관리 기법

  • 사용자가 가상 주소(혹은 논리주소)를 사용하여 프로그램을 작성하면  가상 주소는
    메모리 관리 장치(Memory Management Unit)에 의해 실제 주소(혹은 물리주소)로 변환
  • 가상 주소는 논리적으로 연속적인 것처럼 보이지만 대응하는 실제 주소는 물리적으로
    연속적일 필요가 없기 때문에 비연속 할당(Non-Continuous Allocation) 방식이라고 할 수 있다.

  • 물리적으로 연속된 공간에 적재할 필요가 없기 때문에 프로세스 전체를 메모리에 적재할 필요 없이
    매 순간 필요한 부분만을 메모리에 적재하여 실행하는 방식을 취할 수 있다.
    => 적재되지 않은 부분은 swap device 에 저장되어있다.

  • 프로세스가 실제로 어떻게 적재되어있던 사용자는 프로세스가 연속된 메모리공간에 적재된 것으로
    간주하고 작업을 진행할 수 있다는 이점이 있다.

 

 

2. BMT(Block Map Table)

  • 커널 공간에 각 프로세스마다 하나씩 가지는 주소 매핑 테이블

  • 프로세스를 여러개의 블록(block)으로 나누고 각 블록이 메모리에 적재되어있는지 여부(residence bit),
    블록이 적재된 실제 주소(real address) 등의 정보를 관리

  • 사용자가 가상주소로 블록 b의 주소 d를 참조하려 할 때, b가 메모리에 적재되어있고(residence bit = 1)
    실제 주소가 a 라면 사용자가 참조하려는 실제 주소는 a + d가 된다.

 

 

3. 가상 메모리 기법의 종류

  • 페이징 시스템(Paging System)

  • 세그멘테이션 시스템(Segmentation System)

  • 하이브리드 시스템(Hybrid Paging-Segementation System)

1. 연속 메모리 할당(Continuous Memory Allocation)

  • 프로세스에 연속된 메모리 공간을 할당하는 방식
  • 고려해야할 사항
    • 메모리상에 동시에 적재될 수 있는 프로세스의 갯수
    • 각 프로세스에게 할당할 수 있는 메모리 크기
    • 메모리 분할 방식

 

 

2. 단일 프로그래밍(Uni-Programming)

  • 메모리상에 단 하나의 프로세스만이 적재될 수 있는 시스템

  • 프로그램의 크기가 메모리 전체의 크기보다 큰 경우?
    • 매 순간 필요한 부분만을 메모리상에 적재하도록 해야한다.
    • 운영체제에서 지원할 수 는 없기 때문에 사용자(프로그래머)가 컨트롤해야하는 부분

  • 커널 보호
    • 프로그램을 메모리에 적재할 때 커널 영역을 침범하지 않도록 보호할 필요가 있음
    • 경계 레지스터(Boundary Register) 를 사용하여 보호

  • 문제점
    • 동시에 하나의 프로세스만을 메모리상에 적재하기 때문에 시스템 자원 활용도가 낮음
    • 위의 이유로 시스템의 성능도 낮음

 

 

3. 다중 프로그래밍(Multi-Programming)

  • 메모리상에 동시에 여러개의 프로세스가 적재될 수 있는 시스템

 

  • 각 프로세스마다 분할된 메모리공간, 파티션(partition)을 할당해주기 위한 분할 정책이 필요
    • 고정 분할 방식(Fixed Partition Multi-Programming)
    • 가변 분할 방식(Variable Partition Multi-Programming)

 

  • 단편화(Fragmentation)
    • 내부 단편화(Internal Fragmentation)
      • 파티션의 크기가 적재된 프로세스의 크기보다 크기 때문에 메모리 공간이 낭비되는 상황

    • 외부 단편화(External Fragmentation)
      • 메모리상의 남은 공간의 크기는 적재해야할 프로세스의 크기보다 크지만
        남은 공간이 연속해있지 않아서 프로세스를 적재할 수 없는 상황

 

 

4. 고정 분할 방식(Fixed Partition Multi-Programming)

  • 메모리 공간을 고정된 크기로 미리 분할해두고 각 프로세스에 1:1로 할당하는 방식

  • 각 파티션의 시작 주소와 크기, 어느 프로세스에게 할당됐는지의 등의 정보를 관리하는
    테이블을 작성하여 관리할 수 있음

  • 단일 프로그래밍과 마찬가지로 커널의 영역을 침범하지 않도록 하기 위해 경계 레지스터를
    사용하는데, 여기서는 파티션간의 침범을 막기 위해서도 사용된다. 
  • 단순한 방식이기에 메모리 관리로 인한 오버헤드가 매우 적음

  • 내부, 외부 단편화로 인한 시스템 자원 낭비가 심함

 

 

5. 가변 분할 방식(Variable Partition Multi-Programming)

  • 적재해야할 프로세스의 크기에 맞는 파티션을 만들어 이를 할당하는 방식

  • 고정 분할 방식과 마찬가지로 각 파티션의 시작 주소와 크기, 어느 프로세스에게 할당됐는지
    등의 정보를 관리하는 테이블을 작성하여 관리할 수 있음

  • 프로세스가 필요로하는 크기만큼의 메모리를 할당하기에 내부 단편화는 발생하지 않음
  • 그러나 할당된 프로세스가 메모리에서 해제되는 경우로 인해 외부 단편화는 발생할 수 있음

 

 

6. 배치(Placement)

  • 가변 분할 방식에서 파티션을 할당받았던 프로세스가 실행을 마치고 메모리에서 해제되면 
    처음처럼 연속된 빈 공간이 아닌 사이사이의 빈 파티션이 생김

  • 비어있는 파티션중 어느것을 분할하여 프로세스에게 할당할지 결정하기 위한 정책이 필요
  • First Fit(최초 적합)
    • 프로세스가 필요로하는 크기보다 큰 파티션중 가장 처음 찾아낸 파티션을 사용
    • 단순하고 오버헤드가 적지만 공간 활용률이 떨어질 수 있음

  • Best Fit(최적 적합)
    • 프로세스가 필요로하는 크기와 가장 가까운 크기의 파티션을 사용
    • 크기가 큰 파티션을 최대한 유지할 수 있음
    • 너무 작은 크기의 파티션(거의 사용될 여지가 없는)을 다수 발생시킬 수 있음
    • 최소 파티션을 탐색하기 위해 모든 파티션을 탐색해야하기 때문에 시간이 오래걸림
      크기가 큰 파티션을 최대한 유지할 수 있음
  • Worst Fit(최악 적합)
    • Best Fit과는 반대로 프로세스가 들어갈 수 있는 파티션중 가장 큰 것을 사용
    • 작은 크기의 파티션의 발생을 최소화할 수 있음
    • 크기가 큰 파티션을 확보하기 어려움
    • 최대 파티션을 탐색하기 위해 모든 파티션을 탐색해야하기 때문에 시간이 오래걸림

  • Next-Fit(순차 최초 적합)
    • First Fit과 유사한 방식이지만 이전에 마지막으로 탐색한 위치부터 탐색을 시작
    • 메모리 영역의 사용 빈도를 균등화할 수 있음
    • 모든 파티션을 탐색할 필요가 없기 때문에 오버헤드가 적음

 

 

7. 외부 단편화의 해결

  • 가변 분할 방식에서 프로세스가 필요로하는 크기만큼의 연속된 메모리 공간의 부족(외부단편화)으로
    프로세스를 적재할 수 없게될 경우 이를 해결하기 위한 방안이 필요

 

  • 공간 통합(Coalescing Holes)
    • 다른 프로세스가 실행을 마치고 메모리상에서 해제되어 충분한 크기의 연속된 파티션이
      생길 때 까지 프로세스의 적재를 보류

    • 충분한 크기의 연속된 파티션이 생기면 인접한 두 파티션을 통합하여 프로세스를 적재

    • 파티션 테이블을 조금 수정하는 것만으로 처리 가능한 오버헤드가 적은 방식

    • 오버헤드가 적은 작업인 만큼 프로세스가 메모리상에서 해제될 때마다 실행해줄 수 있다.

 

  • 메모리 압축(Storage Compaction)
    • 모든 빈 공간을 하나의 연속된 빈 파티션으로 통합

    • 외부단편화를 완전히 제거할 수 있지만 모든 프로세스의 메모리상의 위치를 재배치해야 하기 때문에
      프로세스의 작업이 일시적으로 중단되며 오버헤드가 매우 큼

    • 오버헤드가 매우 크기 때문에 적당한 주기를 두고 실행해줘야하는 방식
      => 실행 주기는 OS의 메모리 관리 정책에 따라 달라짐

 

※ OS 레벨에서의 메모리의 할당은 기본적으로 상당히 많은 시간이 걸리는 작업이기에

   메모리의 잦은 할당, 해제가 이루어지는 시스템의 경우 미리 필요한 만큼의 메모리를 할당받아
   메모리 풀(Memory Pool)을 만들고 유저 레벨에서 이를 해결하는 것으로 오버헤드를 줄일 수 있다

1. 메모리 계층구조

  • 레지스터(Register)에서 보조기억장치(Auxiliary Memory)로 갈수록 용량은 커지고
    가격은 싸지며 접근 속도는 느려진다.

  • Word
    • 주기억장치(Main Memory)와 레지스터(Register) 사이의 데이터 전송 단위
    • 16~64bit의 크기를 가진다.

  • Block
    • 보조기억장치(Auxiliary Memory)와 주기억장치(Main Memory) 사이의 데이터 전송 단위
    • 1~4KB의 크기를 가진다.

 

 

2. 주소 바인딩(Address Binding)

  • 프로그램 상에서 사용되는 논리 주소를 실제 메모리의 주소인 물리 주소와 매핑시켜주는 작업

  • 컴파일 시간 바인딩(Compile Time Binding)
    • 컴파일 시간에 주소를 바인딩하는 방식
    • 프로세스 전체를 메모리에 적재해야한다.
    • 프로세스가 메모리상의 어느 위치에 적재될지를 컴파일러가 알고있어야 한다.

  • 적재 시간 바인딩(Load Time Binding)
    • 프로세스가 메모리상의 어느 위치에 적재될지를 컴파일러가 모르는 경우 상대주소를 사용
    • 적재 시점에서 프로세스가 적재된 시작위치를 기준으로 상대주소를 실제주소로 변환하여 매핑하는 방식
    • 마찬가지로 프로세스 전체를 메모리에 적재해야한다.

  • 실행 시간 바인딩(Run Time Binding)
    • 주소 바인딩을 실행시간 전까지 미루는 방식
    • 프로세스의 메모리 주소가 실행 도중에 변경될 수 있는 경우에 사용
    • 주소 변환(relocation)을 위한 레지스터가  필요 - MMU(Memory Management Unit)
    • 대부분의 OS는 이 방식을 사용

 

 

3. 스와핑(Swapping)

  • 메모리 공간의 효율적인 사용을 위해 당장 실행되지 않는 프로세스를 디스크의 swap 영역으로
    swap out해두고 실제로 사용되는 시점에 다시 메모리로 swap in 해주는 방식
  • 할당받은 프로세서 사용시간이 만료된 프로세스는 디스크의 swap 영역으로 swap out 되어
    메모리상에서 해제된다.

  • 실행될 프로세스는 swap 영역에서 swap in 되어 메모리에 적재된다.

1. 교착 상태(Deadlock)란?

  • Blocked/Asleep 상태
    • 특정 이벤트나 자원의 사용을 위한 대기상태

  • Deadlock 상태
    • 멀티프로세스 시스템에서 프로세스들이 서로 다른 프로세스의 작업이 끝나고 이벤트가
      발생하거나 공유 자원에 대한 lock이 풀리는 것을 대기하느라 아무도 작업을 시작하지 못하고
      대기상태만을 유지하는 상태


    • 즉, Blocked/Asleep 상태에 들어간 프로세스가 기다리는 것이 발생할 가능성이 없는
      이벤트거나 절대 얻을 수 없는 자원일 경우 Deadlock 상태에 빠진다.

  • 절대 발생할 수 없는 이벤트나 절대 얻을 수 없는 자원을 기다리는 상태라는 점에서
    단순히 우선순위에서 밀려나서 자원(CPU)의 할당이 미뤄지고있을 뿐인 기아(starvation)
    현상과는 구분된다.

 

 

2. 자원의 분류

  • 선점 가능여부에 따른 분류
    • 선점 가능한 자원(Preemptive Resources)
      • 선점당한 뒤 돌아와도 문제가 발생하지 않는 자원
      • 프로세서(CPU), 메모리 등

    • 선점 불가능한 자원(Non-Preemptive Resources)
      • 선점당할 경우 이후 진행에 문제가 발생하는 자원
      • 디스크 드라이브 등

  • 할당 단위
    • 전체 할당 자원(Total Allocation Resources)
      • 할당될 때 자원 전체가 통채로 할당되어야하는 자원
      • 프로세서, 디스크 드라이브 등

    • 부분 할당 자원(Paritioned Allocation Resources)
      • 할당될 때 여러 조각으로 나누어 할당될 수 있는 자원
      • 메모리 등

  • 동시 사용 가능 여부
    • 배타적 할당 자원(Exclusive Allocation Resources)
      • 한번에 하나의 프로세스만 사용 가능한 자원
      • 프로세서, 디스크 드라이브, 메모리
      • 여기서 말하는 메모리는 메모리의 동일 영역을 의미한다.

    • 공유 할당 자원(Shared Allocation Resources)
      • 여러 프로세스가 동시에 사용할 수 있는 자원
      • 공유데이터 등

  • 재사용 가능 여부
    • SR(Serially-Reusable Resources)
      • 시스템 내에 항상 존재하는 자원
      • 사용이 끝나면 반환되어 다른 프로세스에게 할당될 수 있음
      • 프로세서, 메모리, 디스크 드라이브 등

    • CR(Consumable Resources)
      • 하나의 프로세스에 의해 사용되면 사라지는 자원
      • 시그널, 메세지 등

  • Deadlock을 발생시킬 수 있는 자원의 종류
    • Non-preemptive Resources
      • 선점이 불가능한 자원은 사용중인 프로세스가 반환하기 전까지
        다른 프로세스가 사용할 수 없기에 교착 상태를 발생시킬 수 있다.

    • Exclusive Allocation Resources
      • 배타적 할당 자원은 동시에 하나의 프로세스만 사용 가능하기에
        교착상태를 발생시킬 수 있다.

 

 

3. Deadlock 발생의 예시

3개의 프로세스가 Deadlock 상태에 빠진 상황

  • 예를들어 프로세스 A와 B가 모두 실행을 위해 자원 R1과 R2를 필요로 할 때, A와 B가 각각 R1, R2를
    나누어가지 가진 상태라면 서로 상대가 나머지 한쪽 자원을 release 하기만 기다리고 실행되지 않아
    Deadlock 상태에 빠지게된다. 프로세스가 n개일 때도 마찬가지의 상황이 발생할 수 있다.

 

 

4. Deadlock 발생의 조건

  • Exclusive use of Resources & Non-Preemptive Resources
    => 프로세스가 요구하는 자원이 배타적 할당 자원 / 선점 불가능한 자원일 경우

  • Hold & Wait
    => 프로세스들이 하나의 자원을 hold 한 채로 다른 자원을 요청하는 상태

  • Circular Wait
    => Hold & Wait 상태에 있는 프로세스들의 관계가 순환을 이루는 경우

  • 위 네 가지 조건을 모두 만족할 경우 Deadlock이 발생한다.

 

 

5. Deadlock 의 방지(Prevention)

  • 애초에 데드락이 발생할 수 없도록 하는 방법
  • Deadlock 발생의 조건 네가지중 하나를 제거

  • Exclusive use of Resources 조건 제거
    • 모든 자원을 동시에 사용할 수 있도록 공유
    • 현실적으로 불가능

  • Non-Preemptive Resources 조건 제거
    • 모든 자원을 선점 가능하도록 함
    • 현실적으로 불가능

  • Hold & Wait 조건 제거
    • 필요한 자원을 한번에 할당받도록 함(Total Allocation)
    • 자원의 낭비가 발생 => 이미 사용이 끝난 자원도 작업이 끝날 때까지 반납하지 않음
    • 특정 프로세스들의 대기시간이 과하게 길어질 수 있다는 문제가 발생

  • Circular Wait 조건 제거
    • 자원들에게 순서를 부여하여 자원의 순서에 따라서만 요청 가능하도록 함

    • 위 예시의 경우 자원은 R1, R2, R3, R4 순으로 요청해야하며 P1은 R1, R2, R4를 필요로하고
      P2는 R1, R4를 필요로한다.  그런데 P1이 R1을 먼저 할당받았기에 P2는 R1을 할당받지 못하고
      R4를 할당받으려면 R1을 먼저 할당받아야하기 때문에 P1이 R1을 반납할 때 까지 대기해야한다.
      이렇게 하면 순환대기 형태가 되는 것을 막을 수 있다.

    • 그러나 Total Allocation과 마찬가지로 자원이 필요없을 때도 점유하고있는 자원낭비가 발생할 수 있다.

 

 

6. Deadlock 회피(Avoidance)

  • 시스템의 상태를 계속해서 감시하고 시스템이 deadlock 상태에 빠질 가능성이 있다면
    자원의 할당을 보류하는 방식

  • 시스템을 항상 safe state로 유지하는 것으로 Deadlock이 일어나지 않도록 한다.

  • Safe State
    • 모든 프로세스가 정상적으로 종료 가능한 상태
    • Safe Sequence 가 존재한다
      => Deadlock 상태가 되지않고 모든 프로세스를 정상적으로 실행, 종료할 수 있는 방법이 존재한다

  • Unsafe State
    • Deadlock 상태가 되지 않는다는 것을 보장할 수 없는 상태
    • 반드시 Deadlock에 빠지는 것은 아니지만 그럴 가능성이 있다.

  • Deadlock 회피의 가정
    • 프로세스의 수가 고정
    • 자원의 종류와 수가 고정
    • 프로세스가 요구하는 자원 및 최대수량을 미리 알고있음
    • 프로세스는 자원을 사용한 후 반드시 반납
    • 현실적이지 못한 조건을 가정하기에 그리 실용적이진 못함

  • Dijkstra의 은행원 알고리즘(Banker's Algorithm)
    • Deadlock 회피를 위한 가장 간단한 기법

    • 한 종류의 자원이 여러개일 경우를 가정

    • 각 프로세스의 요청 자원 최대수량(max_claim), 현재 할당받은 수량(current_allocated)을 알고있으면
      앞으로 추가로 필요해질 수 있는 최대 수량을 알 수 있다.(additional_need = max_claim - current_allocated)

    • 모든 프로세스는 추가로 필요한 자원을 받고나면 작업을 완료하고 즉각 할당받은 자원을 반납한다
      => (current_allocated + additional_need)

    • 만약 남아있는 자원을 적절한 순서로 분배하여 모두에게 필요한 자원을 할당할 수 있는 방법(순서)이
      단 하나라도 존재한다면 현재 상태는 safe state라고 할 수 있다.

    • 은행원 알고리즘에서는 자원할당 요청이 들어왔을 때 자원을 할당해주기 전에 미리 테이블 상에서
      max_claim, current_allocated, additional_need 값에 자원을 할당한 후의 상태를 반영하여 그 상태에서
      safe sequence가 존재하는지 확인해보고 만약 존재한다면 자원을 할당해주며 존재하지 않는다면
      자원의 할당을 보류한다.

  • Habermann의 알고리즘
    • Dijkstra의 은행원 알고리즘을 확장

    • 은행원 알고리즘과 달리 여러 종류의 자원이 존재할 경우를 상정
    • 은행원 알고리즘과 마찬가지로 항상 safe state를 유지

    • 마찬가지로 각 프로세스의 max_calim, current_allocated, additional_need 값을 가진다.
      => 다만 은행원 알고리즘에서와 달리 n개의 자원에 대해 위 세가지 값을 각각 가지고있다.

    • 방식 또한 은행원 알고리즘과 마찬가지로 자원을 할당했을때의 상태를 시뮬레이트하여
      safe state임이 확인되면 자원을 할당해준다.

  • 문제점
    • 위에서도 말했듯이 프로세스의 수, 자원의 수가 고정이며 필요한 최대 자원 수를
      미리 알고있다는 전제조건이 비현실적이기에 실용적이지 못함

    • 시스템을 항상 감시해야하기 때문에 오버헤드가 심함
    • safe state 유지를 위해 할당할 수 있음에도 할당되지 않는 자원이 존재하게된다.(자원낭비)

 

 

7. Deadlock 탐지 및 복구(Detection & Recovery)

  • 위에서 알아봤듯이 Deadlock의 발생을 원천적으로 차단하거나 회피하기 위한 방법은
    현실적으로 구현이 어렵거나 효율성이 떨어짐

  • Deadlock이 발생할 것을 전제로 하고 주기적으로 이를 탐지해내고 복구하는 방식을 고안

  • Resource Allocation Graph(RAG) 사용
    • 자원이 어느 프로세스에 할당되어있는지 나타내는 유향 그래프

    • 노드를 자원(Resource) 과 프로세스(Process)의 두 그룹으로 나누어 자원에서
      프로세스로(할당), 프로세스에서 자원으로(요청) 가는 두 가지 경우의 간선만이 존재

  • Graph Reduction
    • 모든 자원을 할당받아 실행될 수 있는 프로세스에 연결된 간선을 모두 지워나간다.

    • 주어진 RAG에서 간선을 하나씩 지워나갔을 때 모든 간선을 지울 수 없다면
      하나 이상의 프로세스가 Deadlock 상태에 빠졌다고 생각할 수 있다.

    • 검사 주기가 짧을수록, 노드가 수가 많을수록 높은 오버헤드를 보인다.

  • Process Termination
    • Deadlock 복구 기법중 하나

    • Deadlock 상태인 프로세스중 일부를 종료시키는 것으로 Deadlock 상태에서 회복
    • Termination Cost Model
      • Deadlock 상태인 프로세스중 어느 프로세스를 종료시킬지 선택하기 위한 기준
      • 프로세스의 종류, 우선순위, 총 실행시간, 남은 실행시간, 종료 비용 등을 고려

    • Lowest Termination Cost First
      • 종료 비용이 가장 적은 프로세스를 먼저 종료시키는 방식
      • 단순하고 오버헤드가 적지만 데드락의 해결에 큰 도움이 되지않는 프로세스가 종료될 수 있음

    • Minimum cost recovery
      • 데드락을 해결할 수 있는 모든 경우중 비용이 최소가되는 경우를 찾아 종료
      • 최선의 선택을 할 수 있지만 모든 경우의 수를 고려하기 위한 절차가 복잡함
      • 높은 오버헤드

  • Resource Preemption
    • 또다른 Deadlock 복구 기법

    • Deadlock 상태를 해결하기 위해 선접할 자원을 선택

    • 해당 자원을 가진 프로세스를 종료시키는 것으로 자원을 회수
      => Deadlock 상태가 아닌 프로세스가 종료될 가능성이 있음

    • Process Termination 때와 마찬가지로 자원의 선점에 드는 비용을 책정하기 위한 모델 사용

  • Checkpoint-Restart Method
    • Deadlock 복구를 위해 종료된 프로세스의 재실행 시 처음부터 다시시작하는 비효율적인
      상황을 방지하기 위해 일정 주기로 진행 내용을 저장하여 종료된 프로세스가 가장 최근의
      checkpoint에서 재시작되도록 하는 기법

1. Low Level Solution

  • 이전까지 알아본 HW, SW, OS Supported 솔루션들은 low level에서의 솔루션
  • low level인 만큼 보다 세세한 부분까지 컨트롤 가능하지만 그만큼 구현과 사용이 여러움
  • 잘못 사용하면 에러가 발생하기 쉬움

 

 

2. High Level Solution

  • Monitor
    • 공유 데이터와 임계 영역(Critical Section)의 집합
    • 개체지향적인 관점으로 접근하는 동기화 기법

  • Path Expression
    • 프로세스의 실행에 있어 허용되는 시퀀스(순서)를 표헌하기 위한 방식
    • 프로세스 동기화 정책을 표현식으로 나타내는 것으로 복잡한 시스템의 설계오류를 방지할 수 있음

  • Serializer
    • 프로세스 실행의 직렬화, 즉 프로세스를 동시(concurrently)에 실행하지 않고 순차적으로 실행
    • 당연하게도 한순간에 하나의 프로세스만을 실행하기에 성능이 떨어진다.
    • 그러나 올바른 순서에 따라 순차적으로 프로세스를 수행할 경우에 얻을 수 있는 올바른 결과는
      동기화가 적절하게 이루어졌는지 확인하기 위한 기준점이 될 수 있다.

 

 

3. Monitor

  • 구조
    • 진입 큐(Entry Queue)
      • 모니터에 진입하려는 프로세스들의 대기열 
      • 모니터의 프로시저의 수(함수의 수)만큼 존재

    • 상호 배제(Mutual Exclusion)
      • 모니터 내에는 동시에 하나의 프로세스만이 존재할 수있음

    • 정보 은폐(Information Hiding)
      • 공유 데이터에는 모니터 내의 프로세스만이 접근할 수 있음

    • 조건 큐(Condition Queue)
      • 모니터 내의 특정 이벤트를 기다리는 대기열

    • 신호 큐(Signaler Queue)
      • signal 명령을 실행한 프로세스가 신호를 보내기 위해 잠시 들어가는 대기열

  • 모니터를 사용한 자원 할당 문제의 해결
    // 자원 요청 프로시저
    void request() {
        // 자원이 없을 경우 자원이 생겼다는 신호가 올 때까지 대기
        if(!R_Available)
            R_Free.wait();
        
        // 자원이 있을경우 가져감
        R_Available = false;
    }
    
    
    // 자원 반납 프로시저
    void release() {
        // 자원 반납 후 조건 큐에서 대기중인 프로세스를 깨우기 위해 시그널을 보냄
        R_Available = true;
        R_Free.signal();
    }
    • 공유자원 R과 그 자원을 요청하는 함수(request)와 반납하는 함수(release)가 있을 때
      자원을 요청/반납하는 여러 프로세스들의 동기화를 구현

    • 기본적인 방식은 세마포어 등과 크게 다르지 않지만 객체지향적인 방식으로 접근하는 것으로
      더 직관적이고 구현이 쉬움

    • 생산자-소비자 문제나 Reader-Writer 문제 등도 모니터를 사용하여 보다 쉽게 해결 가능

  • high level solution인 만큼 사용이 쉽고 에러발생의 가능성이 낮음
  • 프로그래밍 언어에서 이를 지원해야만 사용할 수 있음

  • C#, C++, Java 등의 언어가 모니터 클래스를 지원

1. Spinlock

// s는 임계영역에 진입하기 위한 권한의 갯수

// P 연산
// s가 1이상이 될 때까지 대기하여 권한을 획득
void P(*s) {
    while(*s <= 0);
    *s -= 1;
}
    

// V 연산
// 획득한 권한을 반납
void V(*s) {
    *s -= 1;
}
  • 초기화, P, V 의 세 가지 연산을 지원
  • 위 세 가지 연산은 원자적으로 수행됨을 보장한다.(수행중에 interrupt 당하지 않는다.)
  • 임계영역 진입전에 P연산을, 작업 완료후 V연산을 수행하는 것으로 간단하게 상호배제를 구현할 수 있음
  • 멀티프로세서 시스템에서만 사용 가능한 방식(P, V연산 실행중에는 interrupt가 발생하지 않기때문)
  • Busy Waiting 문제는 여전히 해결되지 않음

 

 

2. Semaphore

// P 연산
// 세마포어 값이 1 이상이라면 권한을 획득(s -= 1)하고 임계영역에 접근
// 그렇지 않다면 대기열에 들어가 기다린다
void P(*s) {
    if(s > 0)
        s -= 1;
    else
        /* wait in ready_queue */
}


// V 연산
// 작업을 끝낸 뒤 만약 ready_queue에서 대기중인 프로세스가 있다면
// 대기중인 프로세스중 하나를 깨워 실행되도록 해준다.
// 대기중인 프로세스가 없다면 권한을 반납(s += 1)한다.
void V(*s) {
    if(/* ready_queue is not empty */)
        /* wake up one of waiting processes */
    else
        s += 1;
}
  • Dijkstra 가 고안한 방식
  • Spinlock 과 유사하게 초기화, P, V 연산을 지원
  • 권한을 획득하지 못한 프로세스는 계속 실행되지 않고 ready_queue에 들어가 block 상태로 대기
  • Busy Waiting 문제를 해결함
  • 그러나 ready_queue 에서 대기중인 프로세스중 어느것을 먼저 깨울지는 비결정적(기아문제 발생할 수 있음)

  • Semaphore의 종류
    • Binary Semaphore
      • 세마포어 값(s)이 0 또는 1 두 종류의 값만 가지는 경우
      • 상호배제와 프로세스 동기화의 목적으로 사용
      • 뮤텍스(mutex)가 이에 속한다

    • Counting Semaphore
      • 세마포어 값(s)이 0 이상의 정수값을 가지는 경우
      • 생산자-소비자 문제 등의 해결을 위해 사용

  • Semaphore로 해결할 수 있는 문제
    • 상호배제(Mutual Exclusion)
      • 임계영역 진입전에 P연산을, 작업 완료후 V연산을 수행하여 해결

    • 프로세스 동기화(Process Synchronization)
      • 세마포어 값을 0으로 초기화

      • 먼저 수행되어야할 작업은 P연산 없이 작업을 수행 후 V연산 수행

      • 나중에 수행되어야할 작업은 P연산을 수행하여 대기, 먼저 수행되어야할 작업이 완료되어
        V연산이 실행되면 세마포어 값이 증가하여 작업을 수 행할 수 있게 됨

    • 생산자-소비자(Producer-Cosumer)
      // 생산자 프로세스
      void Producer() {
          while(true) {
              /* produce data K */
          
              // 버퍼의 데이터가 소비자에 의해 소비될 때 까지 대기
              P(consumed)
          
              // 소비자가 데이터를 가져가면 버퍼에 생성한 데이터를 저장
              write(buffer, K)
          
              // 데이터 생성이 완료되었음을 알림
              V(produced)
          }
      }
      
      
      // 소비자 프로세스
      void Consumer() {
          while(true) {
              // 생산자가 데이터를 생성할 때 까지 까지 대기
              P(produced)
          
              // 생산자가 데이터를 버퍼에 저장하면 이를 읽어옴
              K = read(buffer)
          
              // 데이터가 소비되었음을 알림
              V(consumed)
          
              // 읽어온 데이터 소비
              /* consume data(k) */
          }
      }
      }
      • 생산자가 데이터를 생산하기전에 소비자가 데이터를 읽으려할 때 생기는 문제
      • 생산자 둘이 서로 같은 영역(버퍼 등)에 동시에 데이터를 생성, 저장하려 할 때 생기는 문제
      • Single Buffer일 때 사용가능한 방법

    • 버퍼가 여러개일 때의 생산자-소비자 문제
      // 생산자 프로세스
      void Producer() {
          while(true) {
              /* create data K */
              
              // 다른 작업중인 생산자가 없을 때 까지 대기
              P(mutexP)
              
              // 비어있는 버퍼가 생길 때 까지 대기
              P(nrempty)
              
              // buffer[in]에 생성한 데이터를 저장하고 in의 위치를 한칸 이동
              write(buffer[in], K)
              in = (in + 1) % N
              
              // 버퍼 하나가 채워졌음을 알림
              V(nrfull)
              
              // 생산자의 작업이 끝났음을 알림
              V(mutexP)
          }
      }
      
      
      // 소비자 프로세스
      void Producer() {
          while(true) {
              // 다른 작업중인 소비자가 없을 때 까지 대기
              P(mutexC)
              
              // 채워진 버퍼가 생길 때 까지 대기
              P(nrfull)
              
              // buffer[out]에서 데이터를 읽어오고 out의 위치를 한칸 이동
              K = read(buffer[out])
              out = (out + 1) % N
              
              // 버퍼 하나가 비워졌음을 알림
              V(nrempty)
              
              // 소비자의 작업이 끝났음을 알림
              V(mutexC)
          }
      }

      • mutexP : 생산자 뮤텍스
      • mutexC : 소비자 뮤텍스
      • buffer[0] ~ buffer[n-1] : n개의 버퍼
      • in, out : 생산한 데이터를 넣을 위치, 데이터를 소비할 위치
      • nrfull : 채워진 버퍼의 수
      • nrempty : 비어있는 버퍼의 수
      • 환형 큐를 사용하여 문제를 해결

    • Reader-Writer
      // Reader 프로세스
      void Reader() {
          while(true) {
              /* 읽기작업 진입 전 단계 */
              P(mutexR);
              // 다른 Reader가 없을 경우
              // Writer가 작업을 수행하지 않도록 막아둠
              // 다른 Reader가 있을경우 이미 이 작업이 수행됐기에 다시 수행할 필요가 없음
              if(nreaders == 0)
                  P(mutexW);
                  
              // Reader의 수 증가
              nreaders += 1;
              V(mutexR);
              
              /* 읽기 작업 수행 */
              
              /* 읽기작업 완료 후 단계 */
              P(mutexR);
              // 더이상 다른 Reader가 없을 경우
              // Writer가 작업을 수행할 수 있도록 lock을 해제
              // 다른 Reader가 남아있을경우 아직 lock을 해제해선 안됨
              if(nreaders == 0)
                  V(mutexW);
                  
              // Reader의 수 감소
              nreaders -= 1;
              V(mutexR);
          }
      }
      
      
      // Writer 프로세스
      void Writer() {
          while(true) {
              // 쓰기 작업 진입
              P(mutexR);
              
              /* 쓰기 작업 수행 */
              
              // 쓰기 작업 완료
              V(mutexR);
          }
      }

      • Writer는 동시에 작업을 수행해선 안됨(상호 배제 보장이 필요)

      • Reader의 경우 기본적으로는 여러 작업이 동시에 실행되어도 문제가 없음

      • Reader쪽에게 우선권을 주는 Reader Preference 와 Writer에게 우선권을 주는
        Writer Preferece 두가지 방법이 있으며 위의 코드는 Reader Preference 기준의 설명

 

 

3. Eventcount / Sequencer

  • 은행의 번호표와 유사한 개념을 사용한 기법

  • Sequencer
    • 정수형 변수 S는 지금까지 발생한 사건(실행된 프로세스)의 수를 의미(처음엔 0으로 초기화)

    • ticket(S)
      • 프로세스는 실행되기 전에 ticket(S) 를 호출하여 자신의 순번(번호표)을 발급받음
      • ticket 연산이 수행될 때마다 S값은 1씩 증가

  • Eventcount
    • 정수형 변수 E는 깨워야할 차례인 사건(다음으로 실행될 프로세스)의 순번을 의미(처음엔 0으로 초기화)

    • read(E)
      • 현재 E값 반환

    • advance(E)
      • E번에 해당하는 프로세스를 깨움(wake-up)
      • E를 1 증가시켜 다음에 실행될 순번을 갱신

    • await(E, V)
      • V는 프로세스가 발급받은 순번
      • E < V 일 경우, 즉 자신의 순번이 아직 오지 않았을 경우 E에 연결된 대기열에 프로세스를 삽입
      • CPU 스케줄러에 E 값이 V가 됐을 때, 즉 자신의 순번이 왔을 때 깨워줄것을 요청

  • Eventcount / Sequence 를 활용한 생산자-소비자 문제 해결
    // 버퍼의 갯수는 n개
    // 생산자, 소비자의 Sequencer 는 각각 Pticket, Ctikcet
    // 생삱자, 소비자의 Evnetcount 는 각각 In, Out
    
    // 생산자 프로세스
    void Producer() {
        int u;
        while(true) {
            /* create data K */
            
            // 순번 발급
            u = ticket(Pticket);
            
            // 자신의 순번이 올 때까지 대기
            await(In, u);
            
            // 데이터를 삽입할 버퍼가 있을 때 까지 대기
            // 버퍼의 총 갯수는 n개
            // 버퍼가 채워진 횟수는 u개(생산자가 실행된 횟수)
            // 버퍼가 비워진 횟수는 Out개(소비자가 실행된 횟수)
            // 남은 버퍼의 갯수는 (n - u + Out)개
            // n - u + Out > 0, 즉 Out > u - n 이어야 빈 버퍼가 존재하므로
            // u - n + 1 까지 기다려야 빈 공간이 생긴다.
            await(Out, u - n + 1);
            
            // buffer[u % n]에 데이터를 저장
            buffer[u % n] = K;
            
            // 생산자의 작업이 하나 완료되었음을 알림
            advance(In);
        }
    }
    
    
    // 소비자 프로세스
    void Producer() {
        int v;
        while(true) {
            // 순번 발급
            v = ticket(Cticket);
            
            // 자신의 순번이 올 때까지 대기
            await(Out, v);
            
            // 데이터를 읽을 버프가 있을 때 까지 대기
            // In 이 자신의 순번보다 크다면 채워진 버퍼가 있음
            await(In, v + 1);
            
            // buffer[v % n] 의 데이터를 읽어옴
            K = buffer[v % n];
            
            // 소비자의 작업이 하나 완료되었음을 알림
            advance(Out);
        }
    }​
    • 프로세스는 대기열에서 순번을 기다리다가 스케줄러에 의해 깨어나기에 Busy Waiting 문제 없음

    • Semaphore에서 대기중인 프로세스의 wake-up 순서가 비결정적이기 때문에 생길 수 있었던
      기아(starvation) 문제는 발생하지 않음 => 티켓을 발급받은 순번대로 깨우기 때문

    • Sequencer 를 통해 프로세스가 실행되는 순서를 컨트롤할 수 있음

1. Test And Set(TAS) 명령어

boolean TestAndSet(boolean *target) {
    boolean tmp = *target;
    *target = true;
    return tmp;
}
  • Test, Set 연산을 한번에 원자적으로 수행하는 기계 명령어
    => 실행중 인터럽트를 받지않는것이 보장됨

 

2. TAS 를 사용한 상호배제

while True:
    while TAS(lock):
        pass
 
 	''' 임계 영역 '''
 
	lock = False
  • lock 을 획득하는 과정을 기계적으로 보장
  • 위 방식만으로는 3개 이상의 프로세스에 대해서는 Bounded Wait 규칙에 위배될 수 있음

 

 

3. TAS를 사용한 n개의 프로세스의 상호배제

while True:
    waiting[i] = True
    key = True
    # 자신이 대기상태이고 lock이 걸려있는 상태일 경우 대기
    # 지속적으로 lock의 상태를 확인하여 lock이 해제된 경우 
    # key는 False가 되고 lock은 다시 True가 되어 다른 프로세스의 진입을 막고 작업시작
    while waiting[i] & key:
        key = TAS(lock)
        
    # lock을 획득했기 때문에 대기상태 해제
    waiting[i] = False
    
    ''' 임계 영역 '''
    
    # 자신을 제외한 다른 대기중인 프로세스들이 있는지 확인
    j = (i + 1) % n
    while j != i and not waiting[j]:
        j = (j + 1) % n
        
    # 대기중인 프로세스가 없을 경우 lock을 해제
    if j == i:
        lock = False
        
    # 대기중인 프로세스를 찾았을 경우 굳이 lock을 해제하지 않고 
    # 해당 프로세스의 대기상태를 해제(waiting[j] = False)하여 진입시켜준다.
    # 이렇게 하는것으로 대기중인 모든 프로세스들은 순서대로 수행되기에 
    # n개의프로세스의 상호배제에서도 bounded waiting 규칙을 만족시킬 수 있다.
    else:
        waiting[j] = False
  • 임계구역에의 진입 조건을 lock의 획득만이 아니라 waiting 상태의 해제로도 가능하도록 한 방식

  • 임계구역에 진입한 프로세스는 작업이 완료되면 굳이 lock을 해제하지 않고 대기중인 다른 프로세스중
    가장 먼저 찾아낸 프로세스의 대기상태를 해제시키는 것으로 진입을 허가한다. 

  • 이렇게 하면 대기중이었던 프로세스들은 다른 프로세스들과의 경쟁없이 자신의 순서가 오면 자연스레
    임계구역에 진입하게 되기 때문에 bounded waiting 문제가 해결된다. 

  • TAS의 존재덕분에 SW Solution들에 비해 구현은 훨씬 간단해졌지만 busy waiting 문제는 여전히 남아있다.

+ Recent posts