주기억장치 관리 개요(Main Memory Management)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


메모리의 역사

  • Core Memory: 반지모양 철심에 자석물질을 발라둬 전기가 흐르면 자석이 울리는 방식을 사용
  • 진공관 메모리: 진공관 크기는 손가락 3~4개 정도
  • 트랜지스터 메모리(반도체 칩안에 들어있는 소재 트랜지스터라고 함)-> 크기가 손톱만함, 1비트 저장하는데 4~6개가 들어감(많이 못넣음)
  • 집적회로 메모리: SRAM(캐시메모리 만드는것), DRAM(메인메모리)

언제나 부족한 메모리

  • 프로그램의 변천
    • 기계어/어셈블리어 작성 > C언어 작성 > 자바, 객체지향형 언어 작성을 하게되면서 프로그램의 크기가 커짐
    • 숫자 처리 > 문자 처리 > 멀티미디어 처리 > Big data 즉, 처리하는 자료도 커짐
  • 메모리 용량 증가 vs 프로그램 크기 증가: 둘다 계속 증가! 언제나 부족한 메모리…!

  • 어떻게 메모리를 효과적으로 사용할 수 있을까?
    • 메모리 낭비 없애기: 효과적으로 어떻게 사용할까?
    • 가상 메모리 (virtual memory): 실제 물리적인 메모리는 작은데, 크게 보이도록 가상의 메모리.

프로그램을 메모리에 올리기

  • 메모리 구조: 주소(Address, 메모리의 입력) + 데이터(Data, 메모리의 출력)

  • 프로그램 개발
    • 원천파일(Resource file): 고수준언어 또는 어셈블리언어
    • 목적파일(Object file): 컴파일 또는 어셈블 결과(하이레벨 언어를 기계어로 변환)
    • 실행파일(Executable file): 링크결과
  • 컴파일러(compiler), 어셈블러(assembler), 링커(linker), 로더(loader)

  • 프로그램 실행: code + data + stack(함수호출해서 돌아오는 주소 저장, 지역변수 저장)

  • 실행파일을 메모리에 올리기
    • 메모리 몇번지에?
    • 다중 프로그래밍 환경에서는?
  • MMU사용: 재배치 레지스터(Relocation register) > base/limit외에도 Relocation이 있음
    os가 Relocation register를 이용해 번지수를 지정(address translation)해줌으로써 실제 메인메모리 어디에 들어가있는지와는 상관없이 프로그램이 실행될 수 있도록 함
    • 그래서 메모리의 몇번지에 올리는지는 중요한 이슈가 아니다.
    • 프로그램 설계자가 처음 hwp를 만들때 번지를 0번지로 설정을 해놓았는데 hwp가 실제 1000번지에 있다면 프로그램이 실행이 안된다.
    • 이를 제대로 실행될 수 있는 방법은 Relocation register가 담당한다
    • os가 hwp를 실행할 적에는 Relocation register에 1000이라는 값을 넣는다. 그래서 cpu가 hwp를 실행하려면 0번지에 있다고 생각하니
    • cpu는 0번지를 내어도 limit이 1000이 되어있어 1000번지에 있는 hwp도 실행이 가능하게 된다. (사실은 cpu가 속은것이다.)
    • cpu는 0에 있는줄 알지만, 실제로는 1000에 있는것이다.
  • 주소구분
    • 논리주소(logical address, cpu가 내는 주소) vs 물리주소(physical address, 실제로 mmu를 통과해서 메인메모리오 오는 주소)
    • cpu는 항상 모든 주소가 0번지에서 돌고있다고 생각하며 Relocation register를 통해 프로그램들이 메인메모리의 어느 위치든 올리기가 가능해진다.

동기화의 또다른 도구, 모니터(Monitors)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


이전에 동기화도구로 세마포에 대해서 공부를 했는데, 이 세마포는 굉장히 옛날에 사용하던 방식이다. 이제 많이 사용되는 것은 Monitors이고 이는 자바에서 사용된다.

모니터 (Monitors)

  • 세마포 이후 프로세스 동기화 도구
  • 세마포보다 고수준 개념

개념

  • 공유자원 + 공유자원 접근함수로 구성됨
  • 2개의 queues: 배타동기(Mutual exclusion queue) + 조건동기(Conditional synchronization)
  • 공유자원 접근함수에는 최대 1개의 쓰레드만 집입가능: 나머지는 큐에서 진입못하고 기다리고 있어야한다.(Mutual exclusion queue에서 대기)
  • 진입 쓰레드가 조건 동기로 블록되면 새 쓰데르 진입가능: wait call> 들어왔던 쓰레드는 Conditions synchronization에 갇히게 된다. 그러면 새 쓰레드가 진입가능하다.
  • 새 쓰레드는 조건동기로 블록된 쓰레드를 깨울 수 있음: notify()> 블록된 쓰레드를 깨워준다.
  • 깨워진 쓰레드는 현재 쓰레드가 나가면 재진입할 수 있음: 하나의 쓰레드만 있을 수 있으니까, 그 비어진 자리에 깨워진 쓰레드가 들어올 수 있게 된다.

자바 모니터

자바에서는 모든 객체가 모니터가 될 수 있다.

  • 배타동기: synchronization 키워드를 사용하여 지정 > 한 쓰레드에만 접근이 가능하다.
  • 조건동기: wait(), notify(), notifyAll() 메소드를 사용
class C {
  private int value;
  synchronization void f() { // f를 호출하면 다른 쓰레드는 접근 불가능
    ////////code//////////
  }
  synchronization void g() {
    ////////code//////////
  }
  void h() { // 공통변수를 업데이트하는 함수가 아니라는 의미, 이때 이 쓰레드는 동작이 가능
    ////////code//////////
  }
}

모니터 (Monitors) 사용하는 방법

Mutual exclusion

synchronization {
  ////////critical section//////////
}

세마포처럼 공통변수의 앞과 뒤에 acquire, release 혹은 value = 1와 같은 지정이 특별히 필요없고 단시 synchronization만 써주면 된다.

class BankAccount {
  int balance;
  synchronization void deposit(int eat) {
    int temp = balance + ;
    System.out.println('');
    balance = temp;
  }
  synchronization void withdraw(int eat) {
    int temp = balance + eat;
    System.out.println('');
    balance + temp;
  }
  int getBalance() {
    return balance;
  }
}

Ordering

P1 P2
  wait();
S1; S2;
notify();  

초기값도 지정해주지않고 P1은 곧바로 S1코드를 실행하고 실행이 끝날때 notify()만 해주고, P2는 처음 시작하자마자 wait()을 실행하고 S2 코드를 실행해주게 된다. (깔-끔)

class BankAccount {
  int balance;
  synchronized void deposit(int eat) {
    int temp = balance + ;
    System.out.println('');
    balance = temp;
    notify();
  }
  synchronized void withdraw(int eat) {
    while (balance =< 0)
      try {
        wait();
      } catch (InterruptedException e) {}
    int temp = balance + eat;
    System.out.println('');
    balance + temp;
  }
  int getBalance() {
    return balance;
  }
}

The Dining Philosopher Problem: Monitor로 구현해보기

class Chopsticks {
  private boolean inUse = false //아무도 젓가락을 사용하지 않겠지
  synchronized void acquire() throws InterruptedException { //젓가락을 잡는것 acquire();
    while (inUse) // 누군가가 젓가락을 집고있으면
      wait(); // 기다리고 철학자는 갇혀있게 됨
    inUse = true;
  }
  synchronized void release() { // 젓가락을 놓는것 release;
    inUse = false;
    notify(); // 누군가 큐에서 기다리고 있다면 깨워줘야지
  }
}

교착상태(Deadlocks)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


교착상태: Deadlocks

간혹 동기화를 하다보면 Deadlock에 빠지는 경우가 있다. (프로세스 관리에서 해결해야하는 문제중 하나!)

프로세스는 실행을 위해 여러 자원을 필요로 한다. (CPU, 메모리, 파일, 프린터 등..) 어떤 자원은 갖고 있으나 다른 자원은 갖지 못할때(다른 프로세스가 이미 사용중일때) 나머지는 대기를 해야한다. 그리고 다른 프로세스 역시 다른 자원을 가지려고 대기할 때 교착상태 가능성 > 이를 os가 잘못 나누어주면 교착상태에 빠진다.

  • 교착상태 필요조건(Necessary Conditions)
    • Mutual exclusive(상호배타): 하나가 사용하면 나머지는 기다려야한다.
    • Hold and wait(보유 및 대기): 하나 가지고 있으면서 나머지가꺼도 기다린다.
    • No Preemption(비선점): 순서를 강제할 수 없고 순서대로 이루어진다.
    • Circular wait(환형대기): 하나의 원을 만들어놓고 있다.

자원(Resource)

Deadlock이 일어나는 이유는 결국 자원 때문이다. 자원(=하드웨어 자원) 자원을 할당받기 위해서는 os에 요청을 하고 요청이 올바르면 자원을 할당해주는데, 이를 다 사용하면 다시 os에게 반납을 하게 되는데, 이런식으로 자원을 사용하게 된다. 이 똑같은 자원이 여러개 있을 수도 있다.(여러개의 자원 = instance)

  • 동일 자원
    • 동일 형식(type)자원이 여러개 있을 수 있다.
    • 예) 동일 CPU 2개, 동일 프린터 3개 등
  • 자원의 사용
    • 요청(request) -> 사용(use) -> 반납(release)

- 자원 할당도(Resource Allocation Graph)

  • 어떤 자원이 어떤 프로세스에게 할당되었는가?
  • 어떤 프로세스가 어떤 자원을 할당받으려고 기다리고 있었는가?
  • 자원: 사각형, 프로세스: 원, 할당: 화살표
  • 교착상태가 되려면 이 그림이 원을 만들고 있어야 한다.
  • 교착상태의 필요조건
    • 자원 할당도 상에 원이 만들어져야 한다(환형조건): 충분조건은 아님!
    • 피하려면: 짝수(오->왼), 홀수(왼->오)
위 경우에는 교착상태가 아니다

교착상태를 처리하는 방법

교착상태 방지(Deadlock Prevention)

교착상태의 4가지 필요조검 중 한 가지 이상을 불만족시킨다.

  • 상호배타(Mutual exclusive): 자원을 공유 가능하게 -> 원천적으로는 실현이 불가능하다.
  • 보유 및 대기(Hold & Wait): 자원을 가지고 있으면서 다른 자원을 기다리지 않게 -> 일부는 가능하다.
    • 예) 자원이 없는 상태에서 모든 자원 대기, 일부 자원만 가용하면 보유자원을 모두 놓아주기(혹은 가지고 있다면 두개를 동시에 가지도록!)
    • 단점: 자원 활용률 저하, 기아 (starvation)
  • 비선점(NonPreemptive): 자원을 선점가능하게 -> 원천적으로는 실현이 불가능하다. (예; 프린터)
  • 환형대기(Circular wait)
    • 예) 자원에 번호부여하여 번호 오름차순으로 자원 요청
    • 단점: 자원 활용률 저하

교착상태 회피(Deadlock Avoidance)

교착상태 자체를 자원 요청에 대해 os에서의 잘못된 승인이 이루어져 발생한 것으로 생각한다.

첫번째 예제 - 안전한 할당(Safe Allocation)

  • 12개의 magnetic tape 및 3개의 process
Process Max needs Current needs
P0 10 5
P1 4 2
P2 9 2

두번째 예제 - 불안전한 할당(Unsafe Allocation)

  • 12개의 magnetic tape 및 3개의 process
Process Max needs Current needs
P0 10 5
P1 4 2
P2 9 3

운영체제는 자원을 할당할 때 불안전 할당이 되지 않도록 해야한다. (불안전할당 -> 교착상태, 대출전문 은행과 유사:Banker’s algorithm)
자원을 할당할때 위험한 불안전할당을 하지 않도록 하는 방법

교착상태 검출 및 복수(Deadlock Detection & Recovery)

위의 두가지 방법은 애초에 교착상태가 일어나지 않게 하는 방법이었다면 이는 교착상태는 일어나도록 허용은 하되, 발생하면 복구하도록 하는 방법이다.

  • 교착상태가 일어나는 것을 허용
  • 주기적 검사: 검사에 따른 추가 부담(overhead)
  • 교착상태 발생 시 복수: 프로세스 일부 강제 종료, 자원 선정하여 일부 프로세스에게 할당

이 방법은 os로 하여금 교착상태가 일어나지 않도록 유의하세요. 라고 알려주는게 아니라 프로세스가 필요한대로 자원을 일단 다 나눠주는데, 나눠주다보면 잘 일어나지 않지만 어쩌다가 교착상태가 발생할 수 있다. 이렇게 교착상태가 나타나면 시스템은 동작을 멈추게 되고 이때 이를 해결해줘야 한다.

os의 프로세스 관리부서에서 프로그램이 돌면서 계속적으로 컴퓨터 내부를 교착상태가 일어났는지를 주기적으로 확인한다. 그런데 주기적 검사에서 주기적이라는 단어가 상당히 모호한데, 자주하면 할수록 교착상태를 빨리 알아챌 수 있기 때문에 좋지만 우리가 사용할 수 있는 자원은 한정적이기 때문에 오버헤드의 문제점이 존재한다. 그리고 더 나아가 이렇게 검사를 통해 교착상태를 발견하게 되면 이를 복구해줘야 하는데, 이를 복구해주기 위해서는 주기적으로 현재 상태를 기억을 해야한다. (그러면 또 메모리가 들게 된다) -> 이렇듯 비용이 많이 발생하게 된다.

교착상태가 발생하면 한 프로세스를 강제로 종료를 시키던가, 그게 불가능하다면 자원을 강제로 뺏어서 다른 프로세스에게 할당을 해주면 된다.

이 방법은 말그대로 교착상태 자체가 잘 일어나는 것이 아니기 때문에 자원을 그때그때 그냥 할당을 해주는것을 허용해주는 것인데, 사실 현실적으로 이 방법을 사용하기에는 추가적으로 드는 비용적인 부분들이 많이 들게된다.

교착상태 무시 (Don’t Care)

방법이라고 하기도 그렇지만 이런 교착상태 자체를 무시하는 것이다. 실제로 교착상태가 자주 일어나기도 힘들고(4가지의 필수조건을 충족시키기가 힘들다) pc는 일반적으로 우리가 혼자 쓰고 있기때문에 아예 무시하는 방법을 적용하기도 한다.

전통적 동기화 예제(생산자-소비자문제, RW문제, 식사하는 철학자 문제)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


전통적 동기화 예제 (Classical Synchronization Problem)

1. Producer and Consumer Problem (생산자-소비자 문제)

다른말로 유한버퍼 문제(Bounded Buffer Problem)라고도 한다.
생산자가 데이터를 생산하면 소비자는 그것을 소비하는데, 그 생산자와 소비자 사이에는 저장공간인 buffer가 있고 이 buffer의 크기는 유한하다.

예) 컴파일러 -> 어셈블러, 파일서버 > 클라이언트, 웹서버 > 웹 클라이언트

Bounded Buffer?

Bounded: 한계가 있다.
buffer: 데이터를 모아둘 수 있는 메모리/디스크 공간

  • 생산된 데이터는 버퍼에 일단 저장(속도차이 등)
  • 현실 시스템에서 버퍼 크기는 유한
  • 생산자는 버퍼가 가득 차면 더 넣을 수 없다 (size = count)
  • 소비자는 버퍼가 비면 뺄 수 없다.
class Buffer {
  int[] buf;
  int size; //버퍼 갯수(용량), 크기
  int count; //버퍼안의 생산되어 저장되어있는 갯수
  int in; //처음 in 인덱스 값부터 생산자의 데이터를 넣겠다.
  int out; // 빼내는 위치
}

Buffer(int size){
  buf = new int(size);
  this.size = size;
  count = in = out = 0
}
// check if buf is full
void insert(int item) { // 생산자가 버퍼에 데이터를 집어넣는 함수
  while (count == size) // 같다면 무한루프를 돌며 여기서 멈춰있다.

// buf is not full
  buf(in) = item; //소비자가 하나를 가져가 1의 공간이 생긴다면 item을 in에 넣고
  in = (in + 1) % size; //나머지가 0
  count ++;
}

int remove() {
  // check if buf is empty
  while (count == 0) //버퍼가 비어져있으면 무한루프 돌면서 소비자는 기다리고 있음

// buf is not empty
  int item = buf(out); //buf에 out한 값을 item에 넣는다.
  out = (out + 1)%size; //나머지가 0
  count --;
  return item
}
class Test {
  public static void main(String[] args) {
    Buffer b = new Buffer(100);
    Producer p = new Producer(b, 10000);
    Consumer c = new Consumer(b, 10000);
    p.start()
    c.start()
    try {
      p.join()
      c.join()
    } catch (InterruptedException e) {
      System.out.println("Number of items in the buf", b.count);
    }
  }
}

정상적으로 실행시켜보면 count=0이 나올것이다(동기화를 하는 이유)

그러나 잘못된 결과가 실행되는 경우가 있다.

  1. 실행불가
  2. count ≠ 0

이런 결과가 나타나는 이유는 결국 아래와 같다.

  • 공통변수 count(), buf[]에 대한 동시 업데이트 진행
  • 공통변수 업데이트 구간(임계구역)에 대한 동시 진입이 진행

따라서 이를 해결하기 위한 방법은 아래와 같다.

  • 임계구역에 대한 동시 접근 방지(상호배타)
  • 세마포를 사용한 상호배타 (mutual exclusion)
  • 세마포: mutex.value = 1(# of permit)

Busy-Wait

바쁘게 기다린다는 의미로 cpu에서 생산자는 버퍼가 가득차있으면 더이상 생산을 해낼수가 없고 버퍼가 비어있으면 소비자는 버퍼가 찰때까지 기다려야 한다. 이 기다리는 코드는 아래와 같다.

  • 생산자: 버퍼가 가득차면 기다려야 = 빈공간이 있어야 한다.
  • 소바자: 버퍼가 비면 기다려야 = 찬 공간이 있어야 한다.
Buffer(int size){
  buf = new int(size);
  this.size = size; //버퍼 크기
  count = in = out = 0 // 생산된 항목 갯수
}

size == count라면 무한루프 돌면서 기다려라라고 함으로써 cpu는 이 시간에 계속해서 size == count한 것을 계속 세면서 다른일을 하지못하고 있다. (비효율적) 이런 상황을 busy wait이라고 한다.

os는 성능을 높여줘야 하는데, cpu는 아무일도 못하고 무한루프 돌고 있으니 낭비가 굉장히 심하다.

세마포를 사용한 busy-wait 회피

  • mutex.value = 1
  • 생산자: empty.acquire() # of permit = BUF SIZE
  • 소비자: full.acquire() # of permit = 0
[생산자] [소비자]
empty.acquire(); full.acquire();
PRODUCE; CONSUME;
full.release(); empty.release();

즉, 만약 버퍼가 가득차있다면 생산자쪽에 semaphore가 block 처리를 시켜줌으로써 더이상 cpu가 여기서 무한루프를 돌지않도록 해준다. 이를 깨워주는것은 소비자가 이 버퍼에서 빼내어주어서 빈공간이 생기면 다시 생산자가 넣을 수 있도록 해준다. 따라서 block은 자고있다는 뜻으로 서로가 서로를 가두고 풀어줌으로써 효율을 높여준다.

따라서 semaphore를 통해서 busy-wait이 일어나지 않도록, 즉 운영체에의 성능을 높여주도록 한다.

2. Reader-Writer Problem

주로 공통된 데이터베이스를 접근할 때 발생하는 문제이다. 데이터베이스는 공통이니까, 임계구역이 반드시 존재해야한다. 즉 어느 경우에도 한사람만 들어올 수 있도록 해야하는데 그렇게 하면 상당히 비효율적이다. 더 나아가 reader, writer의 경우는 조금 다르다.

  • 공유 데이터베이스 접근
    • Reader: read data. never modify it
    • Writer: read data and modify it
    • 상호배타: 한번에 한개의 포로세스만 접근 > 비효율적
  • 효율성 제고
    • Each read or write of the shared data must happen within a critical action
    • Guarantee mutual exclusion for Writer
    • Allow multiple readers to execute in the critical section at once

서로 공유하는 데이터에 한해서, 읽기쓰기는 임계구역으로 구현해야하는데 writer 두명이 들어오면 임계구역 해야하지만 reader는 여러명이 들어오기를 혀용한다. (내용을 바꾸는것이 아니니까) 그러다가 reader가 들어와서 database를 조회하고 있는데 writer가 온다면 writer는 기다려야 한다.

  • 변종
    • The first R/W problem(readers-preference): 항상 reader 에게 우선권을 준다.
    • The second R/W problem(writer-preference): writer에게 우선권을 준다.
    • The third R/W problem: 우선권을 아무에게도 주지 않는것

즉, reader가 들어왔는데 writer는 block되어야 하고 반대의 상황도 같다. 그리고 reader가 들어와있는데 다른 reader가 들어오고 싶다면 그건 효율성 측면에서 허용이 된다!

3. Dining Philosopher Problem (식사하는 철학자 문제)

  • 식사하는 철학자 문제
    • 5명의 철학자, 5개의 젓가락, 생각->식사->생각->식사…
    • 식사하려면 2개의 젓가락이 필요
  • 프로그래밍
    • 젓가락: 세마포(# of permit =1) // 세마포의 초기값을 1로 두고
    • 젓가락과 세마포에 일련번호: 0 - 4 //
    • 왼쪽 젓가락 -> 오른쪽 젓가락
public void run() {
  try {
    while (true) {
      lstick.acquire();
      rstick.acquire();
      eating();

      lstick.release();
      rstick.release();
      thinking();
    }
  } catch (InterruptedException e)
}

void eating() {
  System.out.println('I' + id + "eating");
}

void thinking() {
  System.out.println('I' + id I "thinking");
}
class Test {
  static final int num = 3

  public static void main(Sting[] args) {

    // Chopsticks
    Semaphore[] stick = new Semaphore[num];
    for (i=0l i=num; i++) {
      stick[i] = new Semaphore(1)
    }
    // Philosopher
    Philosopher[] phil =new Philosopher[num];
    for (i=0l i=num; i++;){
      phil[i] = new Philosopher(i, stick[i], stick[(i+1)%num]);
    }
    // let Philosopher eat and think
    for (i=0; i<num; i++;){
      phil[i].start();
    }
  }
}

무한루프임에도 실행되다가 중지가 되는데, 그 이유가 뭘까?

  • starvation: 모든 철학자가 식사를 하지 못해 굶어죽는 상황 -> 이유: 교착상태(deadlock) > 모든 사람들이 다 왼쪽 젓가락을 든 상태

동기화를 위한 도구 - Semaphore(Mutual exclusive, Ordering)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


동기화를 위한 도구

  • Semaphores
  • Monitors

1. Semaphores: 전통적인

  • 철도의 신호기(깃발) 등의 사전적 의미를 가짐
  • 동기화 문제 해결을 위한 소프트웨어 도구
  • 네덜란드 사람이 만들었음
  • 구조: 정수형 변수 + 두개의 동작 (P:Proberen(test)->acquire(), V:Verhogen(increment)->release())
    • 정수형 변수를 테스트하고 값을 증가시키는 동작

acquire()

void acquire() {
  value --; // number of permit(무언가를 할 수 있는 권한)
  if (value < 0) {
    add this process/thread to list
    block;
  }
}
  • acquire()가 실행되면 value값은 1만큼 감소된다.
  • value < 0이 되면 acquire를 호출했던 프로세스나 쓰레드를 큐(list)안에 넣는다.
    • 즉, acquire내부를 살펴보면 value, P, V, 큐(list)가 존재
  • 누가 꺼내주기 전까지는 block한다. (멈춰있다)

release()

void release() {
  value ++;
  if (value <= 0) {
    remove a process P from list
    wakeup P;
  }
}
  • release()가 실행되면 value 값은 1만큼 증가한다.
  • value <= 0이 되면 release를 갖혀있던 한 쓰레드를 꺼내서 깨워준다
  • 즉 큐에서 해방시켜준다.

이러한 Semaphores는 어떠한 때에 사용할까?

상황1. Mutual exclusion: 상호 배타 목적으로 사용

  • value = 1을 둔다.(권한을 1로 두었다는 것을 의미)
  • acquire()를 호출하면 아직 value<0이 되지 않음으로 바로 critical section 으로 진입
  • context switching이 실행되어 다른 프로세스가 돈다고 해보자
  • 다른 프로세스가 acquire()하여 value=-1이 되었으니 큐에 갇히게 됨
  • 즉, critical section으로 진입이 되지 못함
import java.util.concurrent;

class BankAccount {
  int balance;
  Semaphore sem;
  BankAccount() {
    sem = new Semaphore(1);
  }
  void deposit(int n) {
    try{
    sem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("+");
    balance = temp;
    //
    sem.release();
  }
  void withdraw(int n ){
    try{
    sem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("-");
    balance = temp;
    //
    sem.release();
  }
  int getBalance() {
    return balance;
  }
}

상황2. Ordering

앞에서 이야기 했듯이 프로세스/쓰레드 동기화에서는 임계구역 문제를 해결하는 것도 중요하지만, 프로세스의 실행 순서를 제어하는 것 또한 중요하다.

  • sem.value = 0
P1 P2
  sem.acquire();
S1; s2;
sem.release();  
  • CPU가 P1을 먼저 가리킨다면 자연스럽게 S1이 실행될 것이다.
  • 공교롭게 CPU가 P2를 가리키게 된다면 sem.acquire()를 실행하게 되어 value = -1로 만든다.
  • 그러면 P2는 sem.acquire()에 block 될 것이고, S1이 실행될 것이다.
  • S1을 끝내고 sem.release()를 실행시키면 value = 0 이 되고
  • block 된 P2는 다음 코드로 진행이 가능할 것이다.

문제: 원하는 방식으로 입금/출금 프로그램 만들기

import java.util.concurrent;

class BankAccount {
  int balance;
  Semaphore sem; dsem; wsem;
  BankAccount() {
    sem = new Semaphore(1);
    dsem = new Semaphore(0);
    wsem = new Semaphore(0);
  }
  void deposit(int n) {
    try{
    sem.acquire();
    // critical section
    int temp = balance + n;
    System.out.println("+");
    balance = temp;
    sem.release();
    //
    wsem.release();
    dsem.acquire();
    } catch (InterruptedException e) ()
  }
  void withdraw(int n ){
    try{
    wsem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("-");
    balance = temp;
    sem.release();
    //
    dsem.release();
  }
  int getBalance() {
    return balance;
  }
}