[운영체제 기초] 프로세스 동기화

#cs
Written by Sungbin2026년 3월 2일 · 6 min read

시리즈의 글 (6개)

  1. [운영체제 기초] 운영체제 시작하기
  2. [운영체제 기초] 프로세스와 스레드
  3. [운영체제 기초] CPU 스케줄링
  4. [운영체제 기초] 프로세스 동기화
  5. [운영체제 기초] 교착 상태
  6. [운영체제 기초] 가상 메모리

banner

본 포스팅은 인프런의 개발자를 위한 컴퓨터공학 1: 혼자 공부하는 컴퓨터구조 + 운영체제를 참조하여 작성한 글입니다.

동기화란

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

동기화의 의미

동기화란 공동의 목적을 위해 동시에 수행되는 프로세스를 의미한다. 예를 들어 워드 프로세스를 들을 때 맞춤법 검사 프로세스와 입력 내용을 화면에 출력하는 프로세스처럼 말이다. 그런데 이렇게 자원의 일관성을 맞추기 위해 마구 실행되도 괜찮은걸까? 아니다! 올바른 수행을 위해 프로세스들은 동기화되어야 한다. 동기화의 사전적 의미를 찾아보면 프로세스들의 수행 시기를 맞춘다라고 나와있다. 하지만 이게 크게 와닿지 않을 것이다.

우리가 배울려는 동기화는 크게 아래 2가지를 일컫는다.

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

실행 문맥을 갖는 모든 대상은 동기화 대상이기에 스레드 또한 동기화 대상이다.

그러면 해당 2가지 경우를 예시를 통해 알아보자. 먼저 실행 순서 제어에 대해 알아보자. 대표적인 문제로 reader-writer problem 문제가 존재한다. 예를 들어 Writer라는 프로세스가 있다고 하자. 해당 프로세스는 Book.txt 파일에 값을 저장하는 프로세스이다. 또 하나로 Reader라는 프로세스가 있다고 하자. 해당 프로세스는 Book.txt의 파일에 저장된 값을 읽어들이는 프로세스이다. 그런데 Reader나 Writer 프로세스는 무작정 아무렇게나 실행되어선 안된다. 바로 실행 순서가 있기 때문이다. Reader 프로세스는 Book.txt 안에 값이 존재한다라는 특정 조건이 만족되어야만 실행이 가능하다.

다음 상호 배체를 위한 동기화 문제에 대해서도 알아보자. 유명한 문제가 바로 Bank Account Problem이다. 해당 문제는 공유가 불가능한 자원의 동시 사용을 피하기 위한 동기화이다. 예를 들어 계좌의 잔액을 공유 변수로 하여 프로세스 A와 프로세스 B가 동시에 작업을 위해 진입을 하려고 한다. 프로세스 A는 계좌의 잔액을 읽어들여 읽어들인 잔액에 2만원을 더한다. 그리고 더한 값을 저장한다. 반면 프로세스 B는 계좌의 잔액을 읽어들여서 읽어 들인 잔액에 5만원을 더하고 더한 값을 저장한다. 그러면 해당 프로세스들이 동시에 실행하면 잔액은 얼마 남았을까? 만약 잔액이 10만원이 남아 있었다면 우리는 17만원을 기대할 것이다. 하지만 동기화가 제대로 이루어지지 않은 경우 15만원이 남아 있을 수도 있다. 왜냐하면 컨텍스트 스위칭과 관련이 있는데 프로세스A가 잔액을 읽어들이고 더하려는 그 순간에 컨텍스트 스위칭이 되어서 B가 먼저 수행되다가 B도 값을 더하려는 순간 컨텍스트 스위칭이 되고 A로 넘어와 작업을 진행하고 이어 B가 진행하면 A에서 더한 값에 더하는 것이 아니라 덮어쓰기 때문이다. 이렇게 동시에 접근해서는 안 되는 자원에 동시에 접근하지 못하게 하는 것이 상호 배제를 위한 동기화 이다.

생산자 소비자 문제

상호 배제를 위한 동기화에 대해 좀 더 알아보자. 이와 관련된 고전적이고 유명한 문제로 생산자와 소비자 문제 가 존재한다. 물건을 계속해서 생산하는 생산자 프로세스가 있고 물건을 계속해서 소비하는 소비자 프로세스가 있다. 그리고 이 2개의 프로세스는 총합이라는 전역 변수를 공유한다고 하자. 생산자는 버퍼에 데이터를 삽입하고 전역 변수에 값을 1 증가시킨다. 소비자는 버퍼에서 데이터를 빼서 전역 변수에 1을 감소시킨다. 위와 같은 상태에서 생산자를 10만번, 소비자를 10만번 실행하면 어떻게 될까? 때로는 우리가 기대하는 0이 나올 수도 있지만 다른 값이 되거나 오류가 발생할 수도 있다. 이유는 바로 동기화가 되지 않았기 때문에 발생하는 것이다. 즉, 동시에 접근해서는 안되는 자원에 동시에 접근해서 발생하는 문제이다.

공유 자원과 임계 구역

  • 공유 자원: 여러 프로세스 혹은 스레드가 공유하는 자원으로 대표적으로 전역 변수, 파일, 입출력 장치, 보조기억장치가 존재한다.
  • 임계 구역: 동시에 실행하면 문제가 발생하는 자원에 접근하는 코드 영역을 말하며 대표적으로 이전 예시의 총합 변수 혹은 잔액에 접근하는 코드로 볼 수 있다.

임계구역에 진입하고자 하면 진입한 프로세스 외에는 대기를 해야한다. 만약 임계 구역에 동시에 접근을 하게 된다면 문제가 발생한다. 이렇게 임계 구역에 동시에 접근하면 자원의 일관성이 깨질 수 있다. 이를 레이스 컨디션 이라고 한다.

운영체제는 이러한 임계 구역 문제를 3가지 원칙으로 해결하려고 한다. 달리 말해 상호 배제를 위한 동기화를 위해서는 아래 3가지 원칙은 반드시 지켜야 한다.

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

동기화 기법

동기화 기법 중에서 대표적인 도구인 뮤텍스 락, 세마포, 모니터를 학습해보도록 하겠다.

뮤텍스 락

뮤텍스 락은 상호 배제를 위한 동기화 도구로 일종의 자물쇠 역할을 한다. 예를 들어 우리가 화장실을 들어간다고 해보자. 우리는 화장실을 들어갈 때 화장실에 들어가서 잠금 장치를 한다. 이것이 바로 뮤텍스 락이다. 이렇게 뮤텍스 락은 단순한 형태를 띄고 있다. 전역 변수 1개와 함수 2개면 충분히 구현이 가능하다. 자물쇠 역할은 프로세스들이 공유하는 전역 변수 lock이라는 것을 선언하여 활용하며 임계 구역을 잠구는 역할을 하는 함수 acquire 함수를 이용한다. 또한 임계 구역의 잠금을 해제하는 역할을 하는 release 함수를 통해 해제를 진행한다. 그러면 각각의 함수들을 상세히 살펴보자.

acquire 함수는 프로세스가 임계구역에 진입하기 전에 호출을 진행한다. 임계구역이 잠겨 있다면 임계 구역이 열릴때 까지(lock이 false가 될때까지) 임계구역을 반복적으로 확인한다. 만약 임계 구역이 열려 있다면 임계구역을 잠구는 역할을 한다. (lock = true) 실제 수도 코드로 작성한 코드는 아래와 같다.

acquire() {
  while (lock == true);
  lock = true;
}

release 함수는 임계 구역에서의 작업이 끝나고 호출을 하며 현재 잠긴 임계 구역을 열어주는 역할을 한다. (lock = false) 실제 수도 코드는 아래와 같다.

release() {
  lock = false;
}

이렇게 되면 프로세스는 락을 획들할 수 없다면 무작정 기다리고 락을 획득할 수 있다면 임계 구역을 잠근 뒤 임계 구역에서의 작업을 진행하고 임계 구역에서 빠져나올 때엔 다시 임계 구역의 잠금을 해제함으로 임계 구역을 보호할 수 있다. 하지만 뭔가 코드 상 문제가 있다라는 것을 독자들은 알아차릴 수 있을 것이다. acquire 함수를 보면 while문을 계속 돌리고 있다. 이것을 우리는 전문 용어로 바쁜 대기라고 하는데 이것은 정말 좋지 못한 방법이다.

C/C++, Python등의 일부 프로그래밍 언어에서는 사용자가 직접 acquire, release 함수를 구현하지 않도록 뮤텍스 락 기능을 제공한다.

세마포

그래서 우리는 좀 더 일반화된 동기화된 도구가 필요했다. 그래서 나온 것이 바로 세마포이다. 세마포는 공유자원이 여러개 있는 경우에도 적용이 가능하다.

세마포 종류에도 이진 세마포, 카운팅 세마포가 있지만 이진 세마포는 뮤텍스 락과 비슷한 개념으로 해당 글에서는 여러 공유 자원을 다룰 수 있는 카운팅 세마포를 다뤄보겠다.

세마포는 임계 구역 앞에서 멈춤 신호를 받으면 잠기 기다렸다가 임계 구역 앞에서 가도 좋다는 신호를 받으면 임계 구역에 진입하는 행위이다. 세마포도 뮤텍스 락과 비슷하게 하나의 변수와 2개의 함수로 단순하게 구현이 가능하다.

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

wait 함수는 만일 임계 구역에 진입할 수 있는 프로세스 개수가 0 이하라면 사용할 수 있는 자원이 있는지 반복적으로 확인하고 임계 구역에 진입할 수 있는 프로세스 개수가 1개 이상이면 S를 1 감소시키고 임계 구역에 진입한다.

wait() {
  while (S <= 0);
  S--;
}

signal 함수는 임계 구역에서의 작업을 마친 뒤 S를 1 증가시킨다.

signal() {
  S++;
}

만약 3개의 프로세스 p1, p2, p3가 2개의 공유 자원에 p1, p2, p3 순서로 접근한다고 해보자. 그러면 과정은 다음과 같을 것이다.

  • p1 wait 호출. S는 현재 2이므로 1로 감소시키고 임계 구역 진입
  • p2 wait 호출. S는 현재 1이므로 0로 감소시키고 임계 구역 진입
  • p3 wait 호출. S는 현재 0이므로 무한히 반복하며 S를 확인
  • p1 임계 구역 작업 종료. signal() 호출. S를 1로 증가
  • p3가 S가 1이 됨을 확인. S는 현재 1이므로 S를 1 감소시키고 임계 구역 진입

하지만 이 과정을 보면 wait 함수에서 바쁜 대기 과정이 일어난다. 마치 탈의실 문이 열려있는지 닫혀있는지 반복적으로 노크를 하는 행위와 같다. 이것은 컴퓨터 차원에서 CPU 사이클이 낭비된다. 그러면 해당 문제를 해결하기 위해서는 사용할 수 있는 자원이 없을 경우 대기 상태로 만들고(해당 프로세스의 PCB를 대기큐에 삽입) 사용할 수 있는 자원이 생길 경우 대기 큐의 프로세스를 준비상태로 만들면 될 것이다.(해당 프로세스의 PCB를 대기 큐에 꺼내서 준비 큐에 삽입) 코드로 한번 살펴보자.

wait() {
  S--;
  if (S < 0) {
    sleep();
  }
}
signal() {
  S++;
  if (S <= 0) {
    wakeup();
  }
}

위와 같이 하면 바쁜 대기 과정을 안 할 수 있다. 이것을 적용하여 이전 시나리오에 반영하면 다음과 같을 것이다.

  • p1 wait 호출. S를 1 감소시키면 S는 1이므로 임계 구역에 진입.
  • p2 wait 호출. S를 1 감소시키면 S는 0이므로 임계 구역에 진입.
  • p3 wait 호출. S를 1 감소시키면 S는 -1이므로 본인의 PCB를 대기 큐에 넣고 대기 상태로 전환
  • p1 임계 구역 작업 종료. signal 함수 호출. S를 1 증가시키면 0이므로 대기 상태였던 p3를 대기 큐에 꺼내 준비 큐로 옯겨줌
  • 깨어난 p3 임계 구역 진입
  • p2 임계 구역 작업 종료. signal 함수 호출. S가 1 증가하면 1
  • p3 임계 구역 작업 종료. signal 함수 호출. S가 1 증가하면 2

이렇게 상호 배제를 위한 동기화를 할 수 있지만 세마포는 실행 순서 동기화도 가능하다. 세마포 변수 S를 0으로 두고 먼저 실행 할 프로세스 뒤에 signal 함수를 붙이고 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 된다.

모니터

하지만 매번 임계 구역 앞 뒤로 wait, signal 함수를 호출하는 것은 문제가 있다. 세마포를 누락하거나, 순서를 바꿔서 배치한다던지, 혹은 중복해서 사용하면 문제가 된다. 그래서 등장한 기법이 모니터이다. 모니터는 사용자가 다루기 편한 동기화 도구이다. 모니터는 상호 배제를 위한 동기화와 실행 순서 제어를 위한 동기화를 제공한다. 상호 배제를 위한 동기화로는 인터페이스를 위한 큐가 있고 공유자원에 접근하고자 하는 프로세스를 인터페이스를 위한 큐에 삽입한다. 그리고 큐에 삽입된 순서대로 한번에 하나의 프로세스만 공유자원을 이용한다. 다음으로 실행 순서 제어를 위한 동기화를 알아보자. 실행 순서를 위한 동기화는 조건 변수를 이용하면 된다.

조건 변수: 프로세스나 스레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수

조건변수.wait()을 사용하여 대기 상태로 변경하고 조건 변수에 대한 큐에 삽입을 한다. 다음으로 조건변수.signal()을 호출하면 wait()으로 대기 상태에 접어든 조건 변수를 실행상태로 변경한다. 그런대 해당 방식에는 2가지가 존재한다. 모니터 안에는 하나의 프로세스만 있을 수 있다. wait 함수를 호출했던 프로세스는 signal 함수를 호출한 프로세스가 모니터를 떠난 뒤 재개할 수 있고 signal 함수를 호출한 프로세스의 실행일 일시중지하고 자신이 실행 뒤에 다시 signla 함수를 호출한 프로세스가 재개하는 방식이 존재한다. 이 방식은 상황에 따라 적절히 이용하면 될 것이다.