[혼공컴운 12] 프로세스 동기화

uuuu.jini·2023년 10월 14일
0

혼공컴운

목록 보기
4/5

✅   동기화란


운영체제의 프로세스 관리 서비스 중 가장 중요한 것은 스케줄링과 동기화이다.

동시다발적으로 실행되는 프로세스들은 공동의 목적을 올바르게 수행하기 위해 서로 협력하며 영향을 주고 받기도 한다. 이렇게 협력하여 실행되는 프로세스들은 실행 순서와 자원의 일관성을 보장해야 하기에 반드시 동기화(Synchronization)되어야 한다.

동기화의 의미

동시다발적으로 실행되는 많은 프로세스는 서로 데이터를 주고받으며 협력하며 실행될 수 있다. 협력적으로 실행되는 프로세스들은 아무렇게나 마구 동시에 실행해서는 안 된다. 올바른 실행을 위해서는 동기화가 필수이다.

프로세스 동기화란 프로세스들 사이의 수행 시기를 맞추는 것을 의미한다.

  • 실행 순서 제어: 프로세스를 올바른 순서대로 실행하기
  • 상호 배제: 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하기

실행의 흐름을 갖는 모든 것은 동기화의 대상이다.

동기화는 특정 자원에 접근할 때 한 개의 프로세스만 접근하게 하거나, 프로세스를 올바른 순서대로 실행한게 하는 것을 의미

즉, 동기화에는 1️⃣ 실행 순서 제어를 위한 동기화가 있고, 2️⃣ 상호 배제를 위한 동기화가 있다.

🔎   실행 순서 제어를 위한 동기화

Writer라는 프로세스와 Reader라는 프로세스가 있다고 가정한다. Writer프로세스는 Book.txt 파일에 값을 저장하는 프로세스이고, Reader 프로세스는 Book.txt 파일에 저장된 값을 읽어 들이는 프로세스이다. 이 두 프로세스는 아무 순서대로 실행되어서는 안된다. Reader 프로세스는 Writer 프로세스의 실행이 끝나야 실행할 수 있기 때문이다. Writer 프로세스가 Book.txt에 값을 저장하기도 전에 Reader프로세스가 Book.txt를 읽는 것은 올바른 실행 순서가 아니다.즉, Reader 프로세스는 Book.txt에 값이 존재한다 는 특정 조건이 만족되어야만 실행을 이어나갈 수 있다. 이렇게 동시에 실행되는 프로세스를 올바른 순서대로 실행하는 것이 첫 번째 프로세스 동기화이다.

🔎   상호 배제를 위한 동기화

상호 배제 (mutual exclusion) 는 공유가 불가능한 자원의 동시 사용을 피하기 위해 사용하는 알고리즘이다. 계좌에 10만원이 저축되어 있다고 가정한다. 그리고 프로세스 A는 현재 저축된 금액에 2만원을 넣는 프로세스, 프로세스 B는 현재 저축된 금액에 5만원을 넣는 프로세스라고 가정한다.

  • 프로세스 A
    1) 계좌의 잔액을 읽어 들인다.
    2) 읽어 들인 잔액에 2만원을 더한다.
    3) 더한 값을 저장한다.
  • 프로세스 B
    1) 계좌의 잔액을 읽어 들인다.
    2) 읽어 들인 잔액에 5만원을 더한다.
    3) 더한 값을 저장한다.

두 프로세스가 동시에 실행 시 결과가 17만원을 기대한다. 하지만, 동기화가 제대로 이루어지지 않는다면 엉뚱한 결과가 나올수 있다.

A와 B는 잔액이라는 데이터를 동시에 사용하는데, A가 끝나기도 전에 B가 잔액을 읽어 버리는 경우 엉뚱한 결과가 나온다. 즉, A와 B를 올바르게 실행하기 위해서는 한 프로세스가 잔액에 접근했을 때 다른 프로세스는 기다려야 한다. 이렇게 동시에 접근해서는 안 되는 자원에 동시에 접근하지 못하게 하는 것이 상호 배제를 위한 동기화이다.

  생산자와 소비자 문제

상호 배제를 위한 동기화에 대해 더 알아본다. 이와 관련된 고전적이고 유명한 문제로 생산자와 소비자 문제가 있다. 생산자와 소비자 문제는 물건을 계속해서 생산하는 프로세스인 생산자와 물건을 계속해서 소비하는 프로세스인 소비자로 이루어져 있다.

생산자와 소비자는 동시에 실행되는 스레드가 될 수도 있다.

생산자와 소비자는 총합이라는 데이터를 공유하고 있다. 생산자는 버퍼에 물건을 넣은 후, 물건의 총합에 해당하는 변수를 1 증가시키고, 소비자는 버퍼에 물건을 빼낸 후 물건의 총합에 해당하는 변수를 1 감소시키나.

// 생산자
생산자() {
	버퍼에 데이터 삽입
    '총합' 변수 1 증가
}
// 소비자
소비자() {
	버퍼에 데이터 빼내기
    '총합' 변수 1 감소
}

총합이 처음에 10개 있다고 가정한다. (물건의 총합 변수를 10으로 초기화한다.)

생산자를 100,000번, 소비자를 100,000번 동시에 실행 시 총합 변수가 계속 10개로 머물러 있을 것으로 기대한다. 하지만, 막상 실행해보면 예상치 못한 결과를 받을 수 있다. 10이 아닌 다른 수가 되거나 실행 중 오류가 발생하기도 한다.

이는 생산자 프로세스와 소비자 프로세스가 제대로 동기화되지 않았기 때문에 발생한 문제이다. 생산자와 소비자는 총합이라는 데이터를 동시에 사용하는데, 앞서의 예제에서는 소비자가 생산자의 작업이 끝나기도 전에 총합을 수정했고, 생산자가 소비자의 작업이 끝나기도 전에 총합을 수정했기 때문에 엉뚱한 결과가 발생한 것이다. 한 마디로 동시에 접근해서는 안 되는 자원에 동시에 접근했기 때문에 발생한 문제라고 볼 수 있다.

  공유 자원과 임계 구역

동시에 접근해서는 안 되는 자원이란 무엇일까?

동시에 실행되는 프로세스들이 공동의 자원을 두고 작업하는 경우 이러한 자원을 공유 자원 (shared resource) 라고 한다. 공유 자원은 변수가 될 수도 있고, 파일이 될 수도 있고, 입출력장치, 보조기억장치가 될 수도 있다.

그리고 이 공유 자원 중에서는 두 개 이상의 프로세스를 동시에 실행하면 문제가 발생하는 자원이 있다. 이렇게 동시에 실행하면 문제가 발생하는 자원에 접근하는 코드 영역을 임계 구역 (critical section)이라고 한다.

두 개 이상의 프로세스가 임계 구역에 진입하고자 하면 둘 중 하나는 대기해야 한다. 임계 구역에 먼저 진입한 프로세스의 작업이 마무리되면 그제서야 비로소 기다렸던 프로세스가 임계 구역에 진입한다.

공동으로 이용하는 변수, 파일, 장치 등의 자원을 공유 자원이라고 하며, 공유 자원에 접근하는 코드 중 동시에 실행하면 문제가 발생하는 코드 영역을 임계 구역이라고 한다.

임계 구역은 두 갱 이상의 프로세스가 동시에 실행되면 안 되는 영역이지만, 잘못된 실행으로 인해 여러 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 경우가 있다. 이를 레이스 컨디션 (race condition)이라고 한다.

레이스 컨디션이 발생하는 경우 데이터의 일관성이 깨지는 문제가 발생한다.

🔎 레이스 컨디션 발생 근본 이유

컴퓨터는 고급 언어를 컴퓨터 내부에서 여러 줄의 저급 언어로 변환되어 실행된다. 여러 줄의 저급 언어를 실행하는 과정에서 문맥 교환이 일어나면 문제가 발생할 수 있다. 이때, 상호배제를 위한 동기화는 이와 같은 일이 발생하지 않도록 두 개 이상의 프로세스가 임계 구역에 동시에 접근하지 못하도록 관리하는 것을 말한다.

🔎 임계 구역 문제 해결 원칙

  • 상호 배제 (mutual execlusion): 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 임계 구역에 들어올 수 없다.
  • 진행 (progress): 임계 구역에 어떤 프로세스도 진입하지 않았다면 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
  • 유한 대기 (bounded waiting): 한 프로세스가 임계 구역에 진입하고 싶다면 그 프로세스는 언젠가는 입계 구역에 들어올 수 있어야 한다. (임계 구역에 들어가기 위해서 무한정 대기해서는 안된다.)

  ✅ 동기화 기법


임계 구역에 오직 하나의 프로세스만 진입하게 하고, 실행 순서를 보장하는 도구 => 뮤텍스 락, 세마포, 모니토

  뮤텍스 락

동기화를 옷 가게에서 탈의실을 이용하는 것에 비유해 본다. 옷 가게에 마음에 드는 옷이 있는 경우 손님은 탈의실에 들어가서 옷을 입어볼 수 있다. 이때 탈의실에는 한 명의 인원만 들어갈 수 있다. 손님들은 탈의실이라는 자원을 이용하고 탈의실 안에는 손님 한 명씩만 들어올 수 있으니, 손님은 프로세스 , 탈의실은 임계 구역이라고 할 수 있다.

만약 밖에서 탈의실에 사람이 있는지 없는지 알 수 없는 상황이라면 탈의실의 이용중임을 자물쇠의 여부로 판단할 수 있다. 이 자물쇠 기능을 코드로 구현한 것이 뮤텍스 락 Mutex lock: MUTual EXclusion lock 이다.

뮤텍스 락은 동시에 접근해서는 안 되는 자원에 동시에 접근하지 않도록 만드는 도구, 다시 말해 상호 배제를 위한 동기화 도구이다.

임계 구역에 진입하는 프로세스는 뮤텍스 락을 이용해 임계 구역에 자물쇠를 걸어둘 수 있고, 다른 프로세스는 임계 구역이 잠겨 있다며 기다리고, 잠겨 있지 않다면 임계 구역에 진입할 수 있다.

뮤텍스 락의 매우 단순한 형태는 하나의 전역 변수와 두 개의 함수로 구현할 수 있다 .

  • 자물쇠 역할: 프로세스들이 공유하는 전역 변수 lock
  • 임계 구역을 잠그는 역할: acquire 함수
  • 임계 구역의 잠금을 해제하는 역할: release 함수

🔎   acquire 함수

프로세스가 임계 구역에 진입하기 전에 호출하는 함수이다. 만일 임계 구역이 잠겨 있다면 임계 구역이 열릴 때까지 (lock이 false가 될 때까지) 임계 구역을 반복적으로 확인하고, 임계 구역이 열려 있다면 임계 구역을 잠그는 (lock을 true로 바꾸는) 함수이다.

🔎   release 함수

임계 구역에서의 작업이 끝나고 호출하는 함수이다. 현재 잠긴 임계 구역을 열어주는 (lock을 false로 바꾸는) 함수라고 보면 된다.

	acquire() {
    	while(lock == true) { // 만약 임계 구역이 잠김 
        	; 				  // 임계 구역이 잠겨있는지 반복 확인
		lock = true;		  // 임계 구역이 잠겨있지 않는다면 임계 구역 잠금
	}
    
    release() {
    	lock = false; 		// 임계 구역 잠금 해제
	}

acquire 와 release 함수를 아래와 같이 임계 구역 전후로 호출함으로써 하나의 프로세스만 임계 구역에 진입할 수 있다.

acquire();		// 자물쇠 잠겨있는지 확인, 잠겨 있지 않다면 잠그고 들어가기
// 임계 구역
release();		// 자물쇠 반환

이렇게 되면 프로세스는

  • 락을 획득할 수 없다면 (임계 구역에 진입할 수 없다면) 무작정 기다리고,
  • 락을 획득할 수 있다면 (임계 구역에 진입할 수 있다면) 임계 구역을 잠근 뒤 임계 구역에서의 작업을 진행하고,
  • 임계 구역에서 빠져나올 때엔 다시 임계 구역의 잠금을 해제함으로써 임계 구역을 보호할 수 있다.

임계 구역이 잠겨있는 경우 acquire() 함수는 반복적으로 lock을 확인한다.

이는 마치 바쁜 탈의실 문이 잠겨있는지 쉴새 없이 반복하며 확인하는 거소가 같다. 이런 대기 방식을 바쁜 대기라고 한다.

  세마포

세마포semaphore는 뮤텍스 락과 비슷하지만, 조금 더 일반화된 방식의 동기화 도구이다. 뮤텍스 락은 하나의 공유 자원에 접근하는 프로세스를 상정한 방식이다. 즉 탈의실이 하나 있는 경우를 가정하고 만든 동기화 도구이다. 하지만 탈의실이 여러 개 있는 상황처럼 공유 자원이 여러 개 있을 경우 (각 공유자원에는 하나의 프로세스만 진입이 가능할지라도) 여러 개의 프로세스가 각각 공유자원에 접근이 가능해야 한다.

탈의실이 세 개 있는 경우 탈의실에 총 세명이 동시에 이용이 가능하다. 한 번에 하나의 프로세스만 이용할 수 있는 프린터가 세 대 있는 경우 총 세개의 프로세스가 공유 자원(세대의 프린터)를 이용할 수 있다.

이처럼 세마포는 공유 자원이 여러 개 있는 상황에서도 적용이 가능한 동기화 도구이다.

세마포의 종류에는 이진 세마포와 카운팅 세마포가 있다. (이진 세마포는 뮤텍스락과 비슷한 개념이다.)

세마포는 철도 신호기에서 유래한 단어이다. 기차는 신호기가 내려가 있을 때는 멈춤 신호로 간주하고 잠시 멈춘다. 반대로 신호기가 올라와 있을 때는 가도 좋다라는 신호로 간주하고 다시 움직인다. 세마포는 이와 같이 멈춤 신호와 가도 좋다는 신호로서 임계 구역을 관리한다. 즉, 프로세는 임계 구역앞에서 멈춤 신호를 받으면 잠시 기다리고, 가도 좋다는 신호를 받으면 그제서야 임계 구역에 들어간다.

세마포는 뮤텍스락과 같이 한개의 변수와 두개의 함수로 구현이 가능하다.

  • 임계 구역에 진입할 수 있는 프로세스의 개수 (사용가능한 공유자원의 개수)를 나타내는 전역 변수 S
  • 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려주는 wait함수
  • 임계 구역 앞에서 기다리는 프로세스에 이제 가도 좋다고 신호를 주는 signal함수

세마포는 임계 구역 진입 전후로 wait()signal() 을 호출한다.

 	wait()
    // 임계 구역
    signal()

변수 S는 임계 구역에 진입할 수 있는 프로세스의 개수, 혹은 사용 가능한 자원의 개수이다.


wait() {
	while(S <= 0)  // 임계 구역에 진입할 수 있는 프로세스가 0개 이하라면 사용할 수 있는 자원이 있는 지 반복 확인
    ;
    S--; // 사용가능 자원이 1개 이상이라면 공유 자원 수를 하나 죽이고 임계 구역 진입
}
signal() {
	S++; // 임계 구역에서의 작업을 마친뒤 사용가능 자원 수 증가
}

사용할 수 있는 공유자원이 없는 경우 프로세스는 무작정 무한히 반복하며 S를 확인해야 한다. 이렇게 바쁜 대기를 반복하며 확인할 시간에 CPU는 더 생산성 있는 작업을 할 수 있지만, CPU 주기를 낭비한다는 점에서 손해이다.

실제로는 세마포보다 다른 더 좋은 방법을 사용한다. wait 함수는 만일 사용할 수 있는 자원이 없을 경우 해당 프로세스 상태를 대기 상태로 만들고, 그 프로세스의 PCB를 세마포를 위한 대기 큐에 집어 넣는다. 그리고 다른 프로세스가 임계 구역에서 작업이 끝나고 signal 함수를 호출시 signal 함수는 대기 중인 프로세스를 대기 큐에서 제거하고, 프로세스 상태를 준비 상태로 변경한 뒤 준비 큐로 옮겨준다.

wait() {
	S--;
    if ( S < 0 ) {
    	add this process to Queue; // 해당 프로세스를 대기큐에 넣기
        sleep(); 	// 대기 상태에 접어든다.
	}
}

signal() {
	S++;
    if ( S <= 0 ) {
    	remove a process p from Queue; // 대기 큐에 있는 프로세스 p를 제거
        wakeup(p); 	// 프로세스 p를 대기 상태에서 준비상태로 만든다.
	}
}

세마포를 이용하면 실행되는 프로세스의 실행 순서도 원하는 대로 제어할 수 있다. 세마포의 변수 S를 0으로 두고 먼저 실행할 프로세스 뒤에 signal 함수, 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 된다.

  모니터

세마포는 그 자체로 훌륭한 프로세스 동기화 도구이지만, 사용하기가 조금 불편한 면이 있다. 매번 임계 구역 앞뒤로 일일이 wait와 signal 함수를 명시하는 것은 번거로운 일이기 때문이다. 더군다나 자칫 잘못된 코드로 예기치 못한 결과를 얻을 수도 있다. (세마포 누락, wait과 signal함수 순서 헷갈린 경우, wait과 signal 을 중복해서 사용한 경우)

이에 최근에 등장한 동기화 도구가 모니터이다. 모니터는 세마포에 비하면 사용자가 사용하기에 훨씬 편리한 도구이다. 모니터는 공유 자원과 공유자원에 접근하기 위한 인터페이스(통로)를 묶어 관리한다. 그리고 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근하도록 한다.

이를 위해 모니터를 통해 공유 자원에 접근하고자 하는 프로세스를 큐에 삽입하고, 큐에 삽입된 순서대로 하나씩 공유자원을 이용하도록 한다. 즉, 모니터는 공유 자원을 다루는 인터페이스에 접근하기 위한 큐(모니터에 진입하기 위한 큐)를 만들고, 모니터 안에 항상 하나의 프로세스만 들어오도록 하여 상호 배제를 위한 동기화를 제공한다.

이 밖에도 모니터는 세마포와 마찬가지로 실행 순서 제어를 위한 동기화도 제공한다. 특정 조건을 바탕으로 프로세스를 실행하고 일시 중단하기 위해 모니터는 조건 변수를 사용하는데, 조건 변수는 프로세스나 스레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수이다.

조건 변수로는 wait와 signal 연산을 수행할 수 있다. 우선 wait는 호출한 프로세스의 상태를 대기 상태로 전환하고 일시적으로 조건 변수에 대한 대기 큐에 삽입하는 연산이다.모니터에 진입하기 위해 삽입되는 큐(상호 배제를 위한 큐)와 wait가 호출되어 실행이 중단된 프로세스들이 삽입된 큐(조건 변수에 대한 큐)는 다르다. 전자는 모니터에 한 번에 하나의 프로세스만 진입하도록 하기 위한 큐이고, 후자는 이미 진입한 프로세스의 실행 조건이 만족될 때까지 잠시 실행이 중단되어 기다리기 위해 만들어진 큐이다.

wait 연산으로 일시 중지된 프로세스는 다른 프로세스의 signal 연산을 통해 실행이 재개될 수 있다. 즉, signal은 wait 를 호출하여 큐에 삽입된 프로세스의 실행을 재개하는 연산이다.

profile
멋쟁이 토마토

0개의 댓글