프로세스들은 작업을 진행하며 서로 데이터를 주고 받아야 하는 경우가 생깁니다. 프로세스끼리 통신을 하는 경우 누가 먼저 작업할지, 언제 작업이 끝날지 등을 서로 알려주어야 하는데 이를 "동기화"라고 합니다.

프로세스 간 통신

프로세스는 시스템 내에서 독립적으로 실행되기도 하고 데이터를 주고 받으며 협업하기도 합니다. 프로세스의 통신은 크게 다음과 같은 종류가 있습니다.

  • 프로세스 내부 데이터 통신: 하나의 프로세스에 2개 이상의 스레드가 존재하는 경우의 통신으로 스레드끼리의 통신입니다. 전역 변수나 파일을 이용하여 데이터를 주고 받습니다.
  • 프로세스 간 데이터 통신: 같은 컴퓨터에 있는 여러 프로세스끼리 통신하는 경우로, 공용 파일이나 운영체제가 제공하는 파이프를 사용하여 통신합니다.
  • 네트워크를 이용한 데이터 통신: 여러 컴퓨터가 네트워크로 연결되어 있을 때 소켓을 이용하여 데이터를 주고 받습니다. 다른 컴퓨터에 있는 함수를 호출하여 통신하는 원격 프로시저 호출도 여기에 해당합니다.

공유 자원과 임계구역

여러 프로세스가 통신하며 한정된 자원을 공동으로 작업할 경우 문제가 발생할 수 있습니다.

공유 자원 접근

공유 자원은 변수, 메모리, 파일 등 여러 프로세스가 공동으로 이용하는 자원을 의미합니다. 공동으로 이용되기 때문에 누가 언제 데이터를 읽고 쓰느냐에 따라 결과가 달라질 수 있습니다. 따라서 프로세스들의 공유 자원 접근 순서를 잘 정해서 예상치 못한 문제가 발생하지 않도록 해야 합니다.
위 그림에서 전역 변수 amount에 p1과 p2가 접근해 입금을 진행합니다. p1은 예금이 1만원 인 것을 확인하고 이후 2만원을 저장하는 프로세스를 진행하고, p2도 마찬가지로 1만원인 것을 확인하고 1만5천원을 저장하는 프로세스를 진행합니다.

여기서 p1이 amount를 확인한 뒤 문맥 교환이 일어나 p2가 작업을 다 진행하고, 이후 다시 p1의 작업이 진행되었다고 가정해봅시다.

실제 돈은 1만원에서 5천원, 1만원이 입금되어 2만 5천원이어야 하지만, 문맥교환이 크리티컬한 타이밍에 일어나는 바람에 실제 amount는 2만원으로 덮어 씌워져 버린 상황입니다.

위 예는 조금 비약이 있지만, 2개 이상의 프로세스가 공유 자원을 사용하는 경우 발생할 수 있는 문제를 보여주고 있습니다.

이처럼 2개 이상의 프로세스가 공유 자원을 병행적으로 읽거나 쓰는 상황을 '경쟁 조건(race condition)'이라고 합니다. 경쟁 조건이 발생하면 실행 결과가 달라질 수 있습니다.

임계구역

위에서 살펴본 것 처럼 공유 자원 접근 순서에 따라 실행 결과가 달라질 수 있는 프로그램의 영역을 "임계구역(critical section)"이라고 합니다. 조금 더 풀어쓰면 "공유 자원에 접근하는 코드 구역"이라고 할 수 있겠습니다.

위의 예에선 입금확인~amount저장까지의 구간에 해당하는 코드가 임계구역 입니다. 임계구역에서는 프로세스들이 동시에 작업해선 안됩니다. 어떤 프로세스가 임계구역에 들어가면 다른 프로세스는 임계구역 밖에서 기다려야 하며, 임계구역의 프로세스가 나와야 들어갈 수 있도록 해야합니다.

생산자-소비자 문제

임계구역과 관련된 전통적 문제로 생산자-소비자 문제가 있습니다. 이 문제의 상황은 이렇습니다.

  1. 생산자 프로세스와 소비자 프로세스는 서로 독립적으로 작업한다.
  2. 생산자는 물건을 생산하여 버퍼에 넣고(input(buf)) 소비자는 계속 버퍼에서 물건을 가져온다.(output(buf))
  3. 버퍼는 원형 버퍼를 사용한다. 또한, 버퍼가 비었는지 확인하기 위해 sum이라는 전역 변수를 사용한다. sum안에는 현재 버퍼에 있는 상품의 총 수가 저장된다.

생산자는 수를 증가시키며 물건을 채우고, 소비자는 물건을 소비합니다. 위 그림에서 버퍼는 1~3까지 차있는 상태이며 sum의 값은 3입니다.

위 로직은 문제없이 잘 동작하는 것으로 보여집니다. 그러나 생산자 코드와 소비자 코드는 sum이라는 변수를 동시에 사용하고 있습니다. 접근 타이밍이 크리티컬할 경우 문제가 발생할 수 있습니다.

생산자의 sum=sum+1;과 소비자의 sum=sum-1;이 거의 동시에 실행되면 문제가 발생하게 됩니다. 생산자와 소비자가 독립적이기 때문에 서로가 sum을 바꾸려고 하는 사실을 인지하지 못하고 sum=3을 읽은 후에 작업을 진행합니다.

여기서 생산자와 소비자가 실행되는 순서에 따라 잘못된 결과가 나오게 됩니다.

  1. sum=4, sum=2, 결과sum=2
  2. sum=2, sum=4, 결과sum=4

이렇듯 임계구역에 2개 이상의 프로세스가 작업하게 되면 문제가 발생할 수 있습니다.

임계구역은 전역 변수뿐 아니라 하드웨어 자원을 사용할 때도 적용됩니다. 예를 들어 프린터 1대를 여러 명이 동시에 사용하는 경우 프린터는 임계구역이 됩니다.

이러한 임계구역을 해결하기 위한 조건은 다음과 같습니다.

  1. 상호 배제(mutual exclusion): 한 프로세스가 임계구역에 들어가면 다른 프로세스는 임계구역에 들어갈 수 없다.
  2. 한정 대기(bounded waiting): 어떤 프로세스도 무한대기 하지 않아야 한다. 즉 특정 프로세스가 임계구역에 진입하지 못하면 안 된다.
  3. 진행의 융통성(progress flexibility): 한 프로세스가 다른 프로세스의 진행을 방해해선 안된다.

임계구역 해결 방법

임계구역 문제를 해결하는 단순한 방법은 잠금(lock)을 이용하는 것입니다. 지금부터 상호 배제, 한정 대기, 진행의 융통성을 모두 만족하는 잠금, 잠금 해제, 동기화 구현 방법을 알아보겠습니다.

세마포어(Semaphore)

임계구역 문제를 해결하기 위해 위대한 다익스트라 선생님께서 제안한 알고리즘 입니다.

세마포어는 임계구역에 진입하기 전에 스위치를 "사용중"으로 놓고 임계구역으로 들어갑니다. 이후 도착하는 프로세스들은 앞의 프로세스가 작업을 마칠 때까지 기다립니다. 프로세스가 작업을 마치면 세마포어는 다음 프로세스에 임계 구역을 사용하라는 동기화 신호를 보냅니다. 세마포어는 임계구역이 잠겼는지 직접 점검하거나, 바쁜 대기를 할 필요가 없습니다.

세마포어는 사용전 초기 설정을 합니다(Semaphore(n)). 이때 n은 공유 가능한 자원의 수를 나타냅니다. 예를 들어 프린터가 1대이면 1, 2대이면 2가 됩니다. 세마포어는 초기화가 끝난 후 임계구역에 들어가기 전에 사용중이라고 표시하고(p()) 임계구역을 나올 때 비었다고 표시하는(v()) 방법으로 임계구역을 보호합니다.

  • Semaphore(n): 전역 변수 RS를 n으로 초기화합니다. RS에는 현재 사용 가능한 자원의 수가 저장됩니다.
  • P(): 잠금을 수행하는 코드로, RS가 0보다 크면 1감소시키고 임계구역에 진입합니다. 만약 0보다 작으면 0보다 커질 때까지 기다립니다.
  • V(): 잠금 해제와 동기화를 같이 수행하는 코드로, RS 값을 1 증가시키고 세마포어에서 기다리는 프로세스에게 임계구역에 진입해도 좋다는 wake_up 신호를 보냅니다.

세마포어에서 잠금이 해제되기를 기다리는 프로세스는 세마포어 큐에 저장되어 있다가 wake_up 신호를 받으면 큐에서 나와 임계구역에 진입합니다. 따라서 바쁜 대기를 하는 프로세스가 없습니다.

물론 눈치 빠른 분들은 P()나 V() 내부 코드가 실행되는 도중 다른 코드가 실행되면 상호 배제와 한정 대기 조건을 보장하지 못한다는 사실을 눈치채셨을 것입니다. 이러한 문제를 해결하기 위해 P()와 V()내부 코드는 검사와 지정(test-and-set)이라는 방식을 사용하여 분리 실행되지 않고 완전히 실행되게 구현되어 있습니다.

검사와 지정(test-and-set)은 하드웨어의 지원을 받아 해당하는 부분을 한꺼번에 실행합니다. 이런 검사와 지정을 사용하면 명령어 실행 중간에 타임아웃이 걸려 임계구역을 보호하지 못하는 문제가 발생하지 않습니다.

이러한 방식을 이용해 세마포어는 바쁜 대기 없이 간단하게 임계구역을 보호하고 문제를 해결합니다.

자원에 해당하는 프로세스만큼 임계구역에 접근할 수 있으며, 모든 자원이 접근중이라면 프로세스들은 큐에 들어가 대기합니다. 이후 wake_up신호를 받으면 다음 프로세스가 임계구역에 들어갈 수 있는 구조입니다.

이 세마포어 중 공유 자원의 갯수가 1인 것을(n=1) 뮤텍스(Mutex)라고 합니다. 뮤텍스는 상호배제를 의미합니다.

모니터

세마포어는 정말 효과적인 임계구역 보호 방법이지만, P()와 V()를 사용자에게 맡겨 놓는다는 단점이 있습니다. 개발하는 개발자가 P()와 V()의 순서를 틀리거나 하면 문제가 발생할 수 있는 것이죠(이건 개발자가..).

어쨋든 이러한 문제를 해결하기 위해 굳이 P()나 V()를 사용할 필요 없이 자동으로 처리할 수 있도록 구현한 것이 모니터(Monitor)입니다.

모니터는 공유자원을 내부적으로 숨기고 공유 자원에 접근하기 위한 인터페이스만 제공함으로써 자원을 보호하고 프로세스 간에 동기화를 시켜줍니다. 시스템 호출과 같은 방법이라고 볼 수 있습니다.

  1. 직접 P()나 V()를 사용하지 않고 모니터에 작업을 요청한다.
  2. 모니터는 요청받은 작업을 모니터 큐에 저장한 후 순서대로 처리하고 그 결과만 해당 프로세스에 알려준다.

입금내역을 모니터로 구현한 코드는 다음과 같습니다.

monitor shared_balance {
  private:
    int balance=10;
    boolean busy=false;
    condition mon;
    
  public:
    increase(int amount) {
      if(busy==true) mon.wait();
      busy=true;
      balance=balance+amount;
      mon.signal();
    }
}

사용자는 increase()함수만을 이용하여 모니터를 사용할 수 있습니다. increase는 임계구역이 잠겼는지 확인하고, 다른 프로세스가 사용하지 않으면 잠금을 건 후 예금액을 증가시킵니다. 모니터를 사용하면 P()와 V()를 사용할 필요가 없습니다.

모니터는 임계구역 보호와 동기화를 위해 내부적으로 상태 변수(mon)을 사용합니다. 상태 변수에는 wait()과 signal()기능이 있는데 각각의 기능은 다음과 같습니다.

  • wait(): 모니터 큐에서 자신의 차례가 올 때까지 기다린다. 세마포어의 P()에 해당
  • signal(): 모니터 큐에서 기다리는 다음 프로세스에 순서를 넘겨준다. 세마포어의 V()에 해당

불필요한 정보를 숨기고 공유 자원에 대한 인터페이스만 제공하는 모니터는 오늘날의 객체지향 언어와 매우 닮아 있습니다.

[참고자료]

  • 쉽게 배우는 운영체제, 조성호, 한빛아카데미
profile
웹 개발을 공부하고 있는 윤석주입니다.

1개의 댓글

comment-user-thumbnail
2021년 10월 30일

오와~~ 너무 어려워요

답글 달기