[운영체제] 프로세스 간 통신(InterProcess Communication, IPC)

4 분 소요

IPC에는 공유 메모리메세지 전달 두 가지 모델이 있다.

공유 메모리

특징

  • 공유 메모리 영역에 데이터를 읽고 쓰면서 데이터를 교환한다.
  • 공유 메모리 영역을 구축할 때에만 시스템 콜이 필요하다.

장점

  • 적은 양의 데이터를 교환하는 데 유용하다.
  • 커널 의존성이 낮기 때문에 속도가 빠르다.

단점

  • 자원과 데이터를 공유하기 때문에 동기화 이슈가 발생한다.

메세지 전달

특징

  • 커널을 경유해 메세지를 전달하는 방식으로 데이터를 교환한다.

장점

  • 커널을 이용하기 때문에 별도의 코드를 구축할 필요 없기 때문에 구현이 비교적 쉽다.

단점

  • 커널을 이요하기 때문에 시스템 콜이 필요하며, 이로 인한 오버헤드가 발생한다.

공유 메모리를 이용했을 때

리눅스에서는 파일 프린트를 하고 싶으면 spool 이라는 이름의 디렉토리에 파일 이름을 기입한다. 프린터 데몬 프로세스는 주기적으로 이 디렉토리에서 프린트 할 파일이 있는지 검사하며, 있으면 이를 프린트하고 디렉토리에서 이름을 지운다.

http://dl.dropbox.com/s/gjrfram9b9ibcw3/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%20%EA%B0%84%20%ED%86%B5%EC%8B%A0-1.png

위와 같이 A와 B 두 프로세스가 동시에 공유 메모리를 접근하려는 상황은 다음과 같다. 먼저 프로세스 A가 in을 읽고 지역 변수 next_free_slot 에 7을 적는다. 이 때 클록 인터럽트가 발생하고, 프로세스 A가 충분히 오래 실행되었으므로 CPU는 이제 프로세스 B로 문맥 교환을 한다. 프로세스 B 역시 in을 읽고 지역 변수 next_free_slot에 7을 적는다. 이 순간 두 프로세스는 모두 다음 번 이용 가능한 슬롯이 7이라고 생각한다.

프로세스 B는 계속해서 수행하면서 자신의 파일 이름을 슬록 7에 저장하고, in 을 8로 변경한다. 그리고 다른 일을 수행한다.

언젠가 프로세스 A가 다시 수행하여 문맥교환 직전에 실행하던 위치부터 다시 실행한다. 프로세스는 next_free_slot 값이 7임을 확인하고 자신의 파일 이름을 슬롯 7에 작성하는데, 이 때 프로세스 B가 기록했던 파일 이름이 지워진다. 그리고 next_free_slot + 1 을 계산하여 in 에 8을 기록한다.

스풀러 디렉토리는 이제 내부적으로는 일관성이 훼손되지 않은 상태이므로, 프린터 데몬은 잘못된 사실을 인지하지 못한다. 그러나 프로세스 B는 자신의 인쇄물을 받을 수 없다.

이처럼 둘 이상의 프로세스가 공유 데이터를 읽거나 기록하는데 최종 결과는 누가 언제 수행하는가에 따라 달라지는 이런 상황경쟁 조건(race condition) 이라고 부른다.

임계 구역

위와 같은 상황이 발생하지 않도록 한 프로세스가 공유 변수나 파일을 사용중이면 다른 프로세스들은 똑같은 일을 수행하지 못하도록 하는 상호 배제(mutual exclusion) 가 필요하다.

공유 메모리를 접근하는 프로그램 부분임계 구역(critical region) 이라고 한다. 두 프로세스가 동시에 임계 구역에 존재하지 않도록 조절한다면, 경쟁 조건을 피할 수 있다.

상호 배제(mutual exclusion)을 구현하는 여러 방법

  1. 인터럽트 끄기
    • 각 프로세스가 임계 구역에 진입하자마자 인터럽트를 끄고, 임계 구역에서 나가기 직전에 인터럽트를 키는 방법. 인터럽트를 끄면, 클록 인터럽트가 발생하지 않는다.
    • 인터럽트를 끄는 방법을 사용하는 것은 운영체제 내부에서 사용할 수 있는 유용한 기법이지만, 사용자 프로세스간 상호 배제를 위해 일반적으로 사용하기에는 적절하지 않다.
  2. 락 변수(lock variable)을 사용
    • 0으로 초기화된 락 변수를 사용한다.
    • 임계 구역으로 진입하려는 프로세스는 먼저 락을 검사한다. 0이면 이를 1로 변경하고 임계구역에 진입한다.
    • 만약 락이 1이면, 이미 다른 프로세스가 임계 구역에 진입한 것이므로 0으로 바뀔 때까지 대기한다.
    • 다시 말해서, 0은 임계구역 내에 어떤 프로세스도 없다는 것을 의미하며, 1은 임계 구역에 어떤 프로세스가 존재함을 의미한다.
    • 하지만 락은 앞서 봤던 spooler directory와 동일한 결함을 가지고 있다. 락 변수 값을 바꾸기 전에 클록 인터럽트가 발생하게 되면 앞서 봤던 똑같은 상황이 만들어 진다.
  3. 엄격한 교대(strict alternation)

http://dl.dropbox.com/s/lowtpuo3lbgwvw3/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%20%EA%B0%84%20%ED%86%B5%EC%8B%A0-2.png

http://dl.dropbox.com/s/qt47vr7ii5kkkel/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%20%EA%B0%84%20%ED%86%B5%EC%8B%A0-3.png

  • 프로세스 0과 프로세스 1이 위와 같이 존재할 때, 프로세스 0은 turn 변수가 0이 될 때까지 계속해서 검사한다. 이처럼 변수가 특정 값이 될 때까지 계속해서 검사하는 것을 바쁜 대기(busy waiting) 이라고 한다.
  • CPU 시간을 낭비하는 일이므로, 기다리는 시간이 짧을 것이라고 예상할 수 있는 경우에만 바쁜 대기가 사용된다. 위에 turn 변수와 같이 바쁜 대기에 사용되는 락 변수를 스핀 락이라고 한다.

슬립과 깨움

sleep 은 다른 프로세스가 sleep 을 호출한 프로세스를 깨워줄 때까지 block 상태에 머무르게 한다. Wakeup 은sleep 상태인 프로세스를 block 상태에서 벗어나게 하도록 깨우는 기능을 하는 함수다.

생산자-소비자 문제를 생각해보자. 생산자 프로세스는 정보를 생산하고 버퍼에 쓴다(write). 소비자 프로세스는 버퍼를 읽어와(read) 정보를 소비한다. 생산자 프로세스 입장에서 저장할 공간이 없는 문제가 발생할 수 있으며, 소비자 프로세스 입장에서는 소비할 정보가 없는 문제가 발생할 수 있다.

#define N 100                                  // number of slots in the buffer
int count = 0;                                 // number of items in the buffer

void producer() {
	int item;

	while (TRUE) {                             // repeat forever
		item = produce_item();                 // generate next item
		if (count == N) sleep();               // if buffer is full, go to sleep
		insert_item(item);                     // put item in buffer
		count++;                               // increment count of items in buffer
		if (count == 1) wakeup(consumer);      // was buffer empty?
	}
}

void consumer() {
	int item;

	while (TRUE) {                              // repeat forever
		if (count == 0) sleep();                // if buffer is empty, go to sleep
		item = remove_item();                   // take item out of buffer
		count--;                                // decrement count of itmes in buffer
		if (count == N - 1) wakeup(producer);   // was buffer full?
		consume_item(item);                     // print item
	}
}

생산자 및 소비자 프로세스는 각각 count 변수를 확인해 깨어나 다음 작업을 수행하던지, sleep 상태로 들어가게 된다.

위의 코드도 경쟁 조건(호출 순서)에 따라 코드가 의도한 대로 수행되지 않는 모습을 볼 수 있다. sleep 을 호출하기 전에 문맥 교환이 일어나 다른 프로세스가 수행된다면, 두 프로세스는 영원히 sleep 상태에 머무를 수 있다.

위 문제의 본질은 sleep을 호출하기 전에 문맥 교환으로 다른 프로세스가 호출되었고, 이어서 sleep 상태에 들어가지 않은 프로세스를 wakeup 시키려는 시도에 있다.

세마포어

다익스트라는 세마포어라는 정수 변수를 사용하여 미리 호출했던 wakeup의 횟수를 저장할 것을 제안했다. 세마포어는 down 과 up 이라는 두 연산과 같이 사용된다.

down 연산

세마포어 값이 0보다 큰 지를 검사한다. 0보다 크다면 값을 감소시키고 down의 나머지 코드 수행을 계속한다. 0이면 프로세스는 down의 수행을 완료하지 않고 즉시 잠들게 된다.

값을 검사하고, 변경하고, 경우에 따라 잠드는 이 모든 동작들이 원자적 행위(atomic action) 로 진행된다. 따라서 연산을 수행하는 도중에 스케줄링이 일어날 수 없다.

up 연산

up 연산은 조건에 따라 다음과 같이 수행된다.

  1. sleep 상태의 블락된 프로세스가 있다면 그 프로세스를 깨우고 실행되지 못한 나머지 부분을 마저 실행시킨다.
  2. 블락된 프로세스가 없다면, 세마포어 값만 증가시킨다.

세마포어를 활용해 sleep과 wakeup을 사용한 생산자-소비자 문제를 해결할 수 있다.

뮤텍스

세마포어의 개수를 셀 필요가 없을 때 뮤텍스를 사용할 수 있다. 링크를 통해 그림으로 쉽게 세마포어와 뮤텍스의 차이를 확인할 수 있다.

뮤텍스도 세마포어처럼 변수이며, 락과 언락 둘 중 하나의 상태를 가진다. 뮤텍스가 언락 상태이면 임계 구역에 진입할 수 있고, 락 상태이면 현재 임계 구역에 진입해있는 스레드가 수행을 마치고 뮤텍스를 언락 상태로 되돌려놓기 전까지 블럭된다.

모니터

하나의 프로세스 내에서 여러 다른 스레드간의 동기화에 사용된다.

카테고리:

업데이트:

댓글남기기