5. 프로세스 동기화

2021. 5. 3. 19:53Computer Sciences/OS

※ 이 내용은 ‘쉽게 배우는 운영체제’ 책 내용을 바탕으로 작성되었습니다.

프로세스는 시스템 내에서 독립적으로 실행되기도 하고 데이터를 주고받으며 협업하기도 합니다. 프로세스 간 통신(IPC)하는 방식도 있는데 이 경우 같은 컴퓨터 뿐만 아니라 다른 컴퓨터와 네트워크를 통해 데이터를 주고받기도 합니다. 프로세스 간 통신의 종류는 다음과 같습니다.

  • 프로세스 내부 데이터 통신: 멀티쓰레드인 경우의 통신입니다. 전역 변수나 파일을 이용하여 데이터를 주고받습니다.
  • 프로세스 간 데이터 통신: 같은 컴퓨터에 있는 서로 다른 프로세스간의 통신입니다. 공용 파일 또는 운영체제가 제공하는 파이프를 사용하여 통신합니다.
  • 네트워크를 이용한 데이터 통신: 여러 컴퓨터가 네트워크로 연결되어 있을 때의 통신입니다. 소켓을 통하여 데이터를 주고받습니다.

프로세스 간 통신의 분류

통신 방향에 따른 분류

  • 양방향 통신(전이중): 데이터를 동시에 양방향으로 전송할 수 있는 구조입니다. 프로세스 간 통신에서 소켓에 해당합니다.
  • 반양방향 통신(반이중): 데이터를 양방향으로 전송할 순 있지만 동시 전송은 불가능하고 특정 시점에 한쪽 방향으로 전송할 수 있는 구조입니다. 무전기가 대표적입니다.
  • 단방향 통신: 한 쪽 방향으로만 전송할 수 있는 구조입니다. 프로세스 간 통신에서 전역 변수, 파이프가 단방향 통신에 해당합니다.

통신 구현 방식에 따른 분류

  • 바쁜 대기(busy waiting): 상태 변화를 살펴보기 위해 반복문을 무한 실행하며 기다리는 것. 시스템 차원에서 자원 낭비입니다.
  • 동기화(synchronization): 바쁜 대기를 하지 않고 상태 변화 발생 시 알리는 방법.
  • 대기가 있는 통신(blocking communication): 동기화를 지원하는 통신 방식입니다. 데이터를 받는 쪽은 데이터가 도착할 때까지 자동으로 대기 상태에 머물러 있습니다.
  • 대기가 없는 통신(non-blocking communication): 동기화를 지원하지 않는 통신 방식입니다. 데이터를 받는 쪽은 바쁜 대기를 사용하여 데이터가 도착했는지 여부를 직접 확인합니다.

전역 변수 GV에 데이터를 담고 읽음으로써 프로세스 간에 통신을 합니다. 이 방식은 주로 직접적으로 관련이 있는 프로세스 간에 사용합니다. 예를 들면 부모 프로세스가 전역 변수를 선언한 후 자식 프로세스를 만들면 부모 프로세스와 자식 프로세스는 전역 변수를 공유하므로 통신을 할 수 있게 됩니다. 두 프로세스간에 전역 변수를 이용하여 양방향으로 통신하려면 두 개의 전역 변수를 사용하면 됩니다. 하지만 이런 방식은 동기화 문제가 있습니다.int L; // 부모 프로세스에서 쓰기, 자식 프로세스에서 읽기. 부모->자식int R; // 자식 프로세스에서 쓰기, 부모 프로세스에서 읽기. 자식->부모int main(){ int pid; pid=fork(); // ...}

부모 프로세스에서 전역 변수 L에 데이터를 쓰지 않았는데 자식 프로세스에서 L에 접근할 수 있기 때문입니다. 이럴 경우 자식 프로세스는 부모 프로세스가 언제 데이터를 쓸지 모르기 때문에 바쁜 대기를 하며 계속 기다려야 합니다.

파일을 이용한 통신

저장장치에 저장된 파일을 통해 통신하는 방법입니다. 파일에 대한 입출력으로 통신합니다. 파일을 이용한 통신은 주로 부모-자식 관계 프로세스 간 통신에 많이 사용되며 운영체제가 프로세스 동기화를 지원하지 않습니다. 그래서 프로세스가 알아서 동기화를 해야 하는데 주로 부모 프로세스가 wait() 함수를 이용하여 자식 프로세스의 작업이 끝날 때까지 기다렸다가 작업을 시작합니다.

파이프를 이용한 통신

파이프는 운영체제가 제공하는 동기화 통신 방식입니다. 파일 입출력을 예로 들면, 프로세스 A가 파일에 쓰는 부분과 프로세스 B가 파일을 읽는 부분을 파이프로 연결합니다. 만약 프로세스 A가 데이터를 쓰지 않았는데 B가 읽으려고 한다면 B는 대기 상태가 되고, A가 데이터를 쓰는 것을 마치면 대기 상태가 풀려 동기화가 이루어지고 B가 읽게 됩니다. 따라서 B는 바쁜 대기를 하지 않아도 됩니다.

파이프는 이름 없는 파이프와 이름 있는 파이프가 있습니다.

  • 이름 없는 파이프(anonymous pipe): 일반적인 파이프를 가리키는 용어입니다. 부모와 자식 프로세스 혹은 같은 부모를 가진 자식 프로세스와 같이 서로 관련있는 프로세스 간 통신에 사용됩니다.
  • 이름 있는 파이프(named pipe): FIFO라 불리는 특수 파일을 이용하며 서로 관련 없는 프로세스 간 통신에 사용됩니다.

소켓을 이용한 통신

여러 컴퓨터에 있는 프로세스 간 통신을 네트워킹이라 합니다. 네트워킹 상황에서의 통신은 원격 프로시저 호출이나 소켓을 이용합니다. 프로시저 호출이 한 컴퓨터에 있는 함수를 호출하는 것이라면, 원격 프로시저 호출은 외부 컴퓨터에 있는 함수를 호출하는 것입니다.

일반적으로 원격 프로시저 호출은 소켓을 이용하여 구현합니다. 다른 컴퓨터에 있는 프로세스와 통신하려면 그 컴퓨터의 위치(IP 주소, MAC 등)를 파악해야 하고, 그 컴퓨터에서 어떤 프로세스와 통신할지도(포트 번호) 결정해야 합니다. 이때 통신하고자 하는 프로세스는 소켓에 쓰기 연산을 하면 데이터가 전송되고, 읽기 연산을 하면 데이터를 받게 됩니다. 소켓은 동기화를 지원하므로 데이터를 받는 쪽이 바쁜 대기를 하지 않아도 됩니다. 양방향 통신을 하려면 파이프는 두 개를 사용했지만 소켓은 하나로도 가능합니다.

정리하면 다음과 같습니다.

종류 운영체제 동기화 지원 open()/close() 사용
전역 변수 X(바쁜 대기) X
파일 X(wait() 함수 이용) O
파이프 O O
소켓 O O

공유 자원과 임계구역

프로세스는 독립적으로 작업할 수도 있고 공유 자원을 바탕으로 공동 작업을 할 수도 있습니다. 여러 프로세스가 공동 자원을 가지고 공동 작업을 할 때 발생할 수 있는 문제를 살펴봅시다.

공유 자원의 접근

공유 자원(shared resource)은 여러 프로세스가 공동으로 이용하는 변수, 메모리, 파일 등을 말합니다. 공유 자원은 공동으로 이용되기 때문에 누가 언제 데이터를 읽거나 쓰느냐에 따라 그 결과가 달라질 수 있습니다. 따라서 프로세스들의 공유 자원 접근 순서를 정하여 예상치 못한 문제가 발생하지 않도록 해야 합니다. 만약 2개 이상의 프로세스가 공유 자원을 동시에 읽거나 쓰는 상황을 ‘’경쟁 조건(race condition)이 발생했다’’고 합니다. 경쟁 조건이 발생하면 자원 접근 순서에 따라 실행 결과가 달라질 수 있습니다.

임계구역

임계구역(critical section)은 공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역을 말합니다.

프로세스 A가 임계구역에서 작업을 마친 후 프로세스 B가 임계구역에 접근하면 문제가 발생하지 않습니다. 하지만 동시에 임계구역에 접근하면 문제가 발생합니다. 이때문에 임계구역에는 반드시 하나의 프로세스만 접근하여 작업해야 합니다. 그리고 그동안에 다른 프로세스는 임계구역에 접근할 수 없어야 합니다.

생산자-소비자 문제

임계구역과 관련된 전통적인 문제로 생산자-소비자 문제가 있습니다.

생산자 프로세스와 소비자 프로세스는 독립적으로 작업합니다. 생산자는 물건을 생산하여 계속 저장소에 넣고, 소비자는 저장소에 있는 물건을 계속 가져갑니다. 또한 저장소에 물건의 상태를 확인하기 위해 전역 변수를 하나 사용합니다. 그런데 두 프로세스가 동시에 저장소에 접근하면 문제가 발생합니다.

  • 현재 상태 - 저장소의 물건 - 3개
  • 생산자가 물건을 생산하여 저장소에 넣습니다. 이제 변수의 개수를 1 늘려야 하지만 아직 바꾸지 못했습니다.
  • 소비자가 저장소에 있는 물건을 빼갑니다. 이제 변수의 개수를 1 감소시켜야 하지만 아직 바꾸지 못했습니다.
  • 이 상태에서 거의 동시에 변수의 개수를 바꾸는 작업을 하면 문제가 발생합니다. 두 프로세스가 독립적이기 때문에 둘 다 변수가 3인것으로 판단하고 작업합니다.
  • 생산자의 처리가 먼저된다면 생산자의 변수 = 4, 소비자의 변수 = 2, 결과 = 2
  • 소비자의 처리가 먼저된다면 소비자의 변수 = 2, 생산자의 변수 = 4, 결과 = 4

이뿐만 아니라 프린터에서도 예시를 찾을 수 있습니다. 만약 동시에 두 사람이 인쇄를 요청했는데 임계구역에 두 프로세스가 모두 접근한다면 두 인쇄물이 섞여서 나올것입니다.

임계구역 해결 조건

상호 배제(mutual exclusion)

한 프로세스가 임계구역에 들어가면 다른 프로세스는 임계구역에 들어갈 수 없습니다.

한정 대기(bounded waiting)

한 프로세스가 무한 대기하지 않아야 합니다. 즉 특정 프로세스가 임계구역에 진입하지 못하면 안 됩니다.

진행의 융통성(progress flexibility)

한 프로세스가 다른 프로세스의 진행을 방해해서는 안 된다는 것을 의미합니다.

임계구역 해결 방법

기본 코드는 다음과 같습니다.

 

#include <stdio.h>
typedef enum boolean;
extern boolean lock = false;
extren int balance;

void main() {
  while(lock == true); 
  lock = true;
  balance += 10; // 임계구역
  lock = false;
}

피터슨 알고리즘

임계구역 문제를 해결하기 위해 게리 피터슨이 제안한 알고리즘입니다. lock과 turn이라는 공유 변수를 사용합니다.

// 공유 변수
boolean lock1 = false;
boolean lock2 = false;
int turn = 1;
// 프로세스 P1
lock1 = true;
turn = 2;
while(lock2 == true && turn == 2); // P2가 임계구역에 진입했고, P2의 차례라면 대기
// 임계구역 코드
lock1 = false;  
  // 프로세스 P2
  lock2 = true;
  turn = 1;
  while(lock1 == true && turn 1);
  // P1이 임계구역에 진입했고, P1의 차례라면 대기
  // 임계구역 코드
  lock2 = false;

이 알고리즘은 2개의 프로세스만 사용 가능하다는 한계가 있습니다. 만약 더 많은 프로세스를 이 알고리즘을 통해 이용하고자 한다면 그 만큼의 공우 변수와 처리를 해야 합니다.

데커 알고리즘

테오도뤼스 데커가 제안한 알고리즘입니다. 하드웨어의 도움 없이 소프트웨어만으로 임계구역 문제를 해결할 수 있습니다.

// 공유 변수
boolean lock1 = false;
boolean lock2 = false;
int turn = 1;
// P1
lock1 = true;
// 1. 먼저 잠금을 겁니다.
while(lock2 == true) // 만약 P2가 임계구역 접근 중이라면
{
  if(turn == 2) // 차례 또한 P2의 차례라면
  { 
    lock1 = false; // 잠금을 풀고
    while(turn == 2); // 차례가 될 때까지 기다립니다.
    lock1 = true; // 차례가 돌아오면 다시 잠그고 P2의 잠금이 풀리면 임계구역에 진입합니다.
  }
}
// 임계구역 코드
turn = 2; // 처리를 마치고 차례를 넘깁니다.
lock1 = false; // 잠금을 해제합니다.
// P2
lock2 = true; // 1. 먼저 잠금을 겁니다.
while(lock1 == true) // 만약 P1이 임계구역에 접근 중이라면
{
  if(turn == 1) // 차례 또한 P1의 차례라면 
  { 
    lock2 = false; // 잠금을 풀고
    while(turn == 1); // 차례가 될 때까지 기다립니다.
    lock2 = true; // 차례가 돌아오면 다시 잠근 후 P1의 잠금이 풀리면 임계구역에 진입합니다. 
  }
}
// 임계구역 코드
turn = 1; // 작업을 마치고 차례를 넘깁니다.
lock2 = false; // 잠금을 해제합니다.

위 두 알고리즘은 모두 임계구역 해결 방법 중 세 가지를 모두 만족하지만 매우 복잡하고, 프로세스가 늘어나면 변수도 늘어나고 전체적인 알고리즘도 복잡해집니다. 임계구역을 보호하기 위해 복잡한 알고리즘을 구현하는 것은 바람직한 접근 방법이 아닙니다.

세마포어

다익스트라가 제안한 알고리즘입니다. 앞의 알고리즘에 비해 간단하고 사용이 쉽습니다. 원리는 간단합니다.

  1. 임계구역에 진입하기 전에 스위치를 사용 중으로 놓고 임계구역으로 들어갑니다.
  2. 이후에 도착하는 프로세스는 앞의 프로세스가 작업을 마칠 때까지 기다립니다.
  3. 작업을 마치면 세마포어는 다음 프로세스에 임계구역을 사용하라는 동기화 신호를 보냅니다.

세마포어는 다른 알고리즘과 달리 임계구역이 잠겼는지 직접 점검하거나, 바쁜 대기를 하거나, 다른 프로세스에 동기화 메세지를 보낼 필요가 없습니다. 내부 코드를 통해 확인해봅시다.

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

P();
// if(RS > 0)
// RS = RS -1;
// else block();

// 임계구역 코드...

V();
// RS = RS + 1;
// wake_up();

모니터

다 해결될 것 같았던 세마포어도 문제점이 있습니다. 가장 큰 문제점은 잘못된 사용으로 임계구역이 보호받지 못할 수도 있다는 것입니다. 기본 원칙대로 코드를 구현하면 문제가 없지만, 고의로 세마포어를 사용하지 않거나, 실수로 잘못 구현해 문제가 생기는 경우입니다.

모니터는 공유 자원을 사용할 때 모든 프로세스가 세마포어 알고리즘을 따르도록 구현한 것입니다. 모니터는 공유 자원을 내부적으로 숨기고 공유 자원에 접근하기 위한 인터페이스만 제공함으로써 자원을 보호하고 프로세스 간에 동기화를 시킵니다. 이는 시스템 콜과 같은 개념입니다.

 

Uploaded by Notion2Tistory v1.1.0

'Computer Sciences > OS' 카테고리의 다른 글

6. 교착상태  (0) 2021.05.03
4. CPU 스케줄링  (0) 2021.05.03
3-3. 프로세스의 연산  (0) 2021.05.02
3-2. PCB와 문맥 교환  (0) 2021.05.02
3-1. 프로세스의 개요  (0) 2021.05.02