[운영체제] Part 2 - 프로세스 관리 :: 프로세스

서정범·2024년 2월 2일
0

운영체제

목록 보기
4/16

프로세스(Process)는 실행 중인 프로그램이라고 생각할 수 있습니다.

대부분 시스템에서는 프로세스가 작업의 단위가 됩니다.

시작하기 앞서...

프로세스에 대해서 간단히 정리하고 들어갑시다.

1. 기본 정의

프로세스(Process)는 컴퓨터에서 실행중인 프로그램의 인스턴스입니다. 프로세스는 운영 체제에 의해 관리되며, 자체적인 메모리 공간, 데이터, 프로세서 상태(레지스터) 등을 갖습니다.

2. 구성 요소

  • 프로그램 카운터: 현재 프로세스가 어떤 명령을 실행하고 있는지 나타내는 부분(프로그램 코드)
  • 메모리 스택: 함수 호출, 로컬 변수 등을 저장하는 영역
  • 데이터 세그먼트: 전역 변수와 정적 변수를 저장하는 영역
  • 힙 영역: 동적으로 할당된 메모리가 저장되는 영역

3. 프로세스 상태

  • 프로세스는 생성, 준비(ready), 실행(running), 대기(waiting/blocking), 종료(terminated) 등 여러 상태를 거칩니다.
  • 이러한 상태는 프로세스의 생명 주기를 나타내며, 운영 체제의 스케줄러에 의해 관리됩니다.

4. 프로세스 제어 블록(PCB)

  • 운영 체제는 각 프로세스에 대한 정보를 프로세스 제어 블록(PCB)에 저장합니다. PCB에는 프로세스의 상태, 프로그램 카운터, CPU 레지스터, 메모리 관리 정보 등이 포함됩니다.
  • PCB는 프로세스의 중요한 메타데이터를 담고 있으며, 프로세스의 스위칭 시 필요한 정보를 제공합니다.

5. 멀티태스킹과 컨택스트 스위칭

  • 현대 운영 체제는 멀티태스킹을 지원합니다. 이는 여러 프로세스가 동시에 실행되는 것처럼 보이도록 하는 기술입니다.
  • 컨택스트 스위칭은 한 프로세스에서 다른 프로세스로 CPU의 제어를 전환하는 과정입니다. 이때 PCB가 중요한 역할을 합니다.

6. 프로세스 간 통신(IPC)

  • 프로세스 간 통신(ICP)은 서로 다른 프로세스가 정보를 교환하거나 동기화하는 매커니즘입니다.
  • 파이프, 소켓, 공유 메모리, 메시지 큐 등 다양한 IPC 기술이 사용됩니다.

1. 프로세스 개념

초창기 컴퓨터는 작업(job)을 실행하는 일괄처리 시스템이었고, 사용자 프로그램 또는 태스크(task)를 실행하는 시분할 시스템이 뒤를 이었습니다.

1.1. 프로세스

프로세스의 현재 활동의 상태는 프로그램 카운터 값과 프로세서 레지스터의 내용으로 나타냅니다.

프로세스 메모리 배치는 다음과 같은 섹션으로 구분되어 있습니다.

  • 스택 섹션: 함수를 호출할 때 임시 데이터 저장장소(예: 함수 매개변수, 복귀 주소 및 지역 변수)
  • 힙 섹션: 프로그램 실행 중에 동적으로 할당되는 메모리
  • 데이터 섹션: 전역 변수
  • 텍스트 섹션: 실행 코드

여기서, 텍스트 섹션과 데이터 섹션은 크기가 고정되어 있습니다.

하지만, 스택 섹션과 힙 섹션은 크기가 동적으로 바뀔 수 있습니다.

함수가 호출될 때 마다 스텍에는 다음과 같은 요소들이 푸쉬 됩니다.

스택 푸쉬 요소

  • 함수 매개변수
  • 지역 변수 및 복귀 주소를 포함하는 활성화 레코드(activation record)

JAVA에서는 활성화 레코드를 스택 프레임이라고도 부릅니다.

스택 프레임 - 로컬 변수 배열, 오퍼랜드 스택, 프레임 데이터

메모리가 동적으로 바뀔 수 있기 때문에 운영체제는 서로의 메모리에 충돌이 발생하지 않도록 관리를 해줘야 합니다.

1.2. 프로세스 상태

프로세스는 실행되면서 그 상태가 변합니다. 프로세스는 다음 상태 중 하나에 있게 됩니다.

  • 새로운(new): 프로세스가 생성 중입니다.
  • 준비(ready): 프로세스가 처리기에 할당되기를 기다립니다.
  • 실행(running): 명령어들이 실행되고 있습니다.
  • 대기(waiting): 프로세스가 어떤 이벤트(입출력 완료 또는 신호의 수신 같은)가 일어나기를 기다립니다.
  • 종료(terminated): 프로세스의 실행이 종료됩니다.

어느 한순간에 한 처리기 코어에서는 오직 하나의 프로세스만이 실행된다는 것을 인식하는 것이 중요합니다.

1.3. 프로세스 제어 블록

각 프로세스는 운영체제에서 프로세스 제어 블록(process control block, PCB)(태스크 제어 블록이라고도 불린다)에 의해 표현됩니다.

  • 프로세스 상태: new, ready, running, waiting, halted
  • 프로그램 카운터: 다음에 실행할 명령어의 주소
  • CPU 레지스터들:
    • 누산기(accumulator)
    • 인덱스 레지스터
    • 스택 레지스터
    • 범용 레지스터들
    • 상태 코드
  • CPU-스케줄링 정보: 프로세스 우선순위, 스케줄 큐에 대한 포인터와 다른 다른 스케줄 매개변수
  • 메모리 관리 정보: 운영체제에 의해 사용되는 메모리 시스템에 따라 기준(base) 레지스터 값, 한계(limit) 레지스터의 값, 페이지 테이블 또는 세그먼트 테이블 등과 같은 정보 포함
  • 회계(accounting) 정보: CPU 사용 시간과 경과된 실시간, 시간 제한, 계정 정보, 잡 또는 프로세스 번호 등을 포함
  • 입출력 상태 정보: 프로세스에 할당된 입출력 장치들과 열린 파일의 목록 등이 포함

⚡️ 레지스터 ⚡️

  1. 누산기 (Accumulator):

    • 대부분의 연산의 결과를 임시로 저장하는데 사용
    • CPU의 성능 향상을 위해, 누산기를 통한 연산은 일반 메모리 접근보다 훨씬 빠르다.
  2. 인덱스 레지스터 (Index Register):

    • 인덱스 레지스터는 배열, 문자열 등의 데이터 구조에서 위치나 오프셋(offset)을 나타내는 데 사용
    • 예를 들어, 반복문에서 배열의 각 요소에 접근할 때 사용
    • 인덱스 레지스터는 특정 메모리 주소에 대한 계산을 단순화하고 빠르게 만든다.
  3. 스택 레지스터 (Stack Pointer):

    • 스택 레지스터는 스택의 최상단을 가리키는 데 사용
  4. 범용 레지스터 (General Purpose Register):

    • 다양한 목적으로 사용될 수 있는 레지스터
    • 임시 데이터 저장, 중간 계산 결과 보관, 메모리 주소 지정 등 다양한 용도로 사용
    • 이 레지스터들은 프로그래밍에서 가장 자주 사용되며, 각각의 용도는 CPu 설계와 명령어 세트에 따라 다를 수 있습니다.
  5. 상태 코드 레지스터 (Status Register):

    • CPU의 현재 상태를 나타내는 플래그(flag)들을 포함
    • 연산의 결과(예: 제로, 오버플로우, 캐리, 사인 등)에 대한 정보를 제공
    • 예를 들어, 산술 연산 후 결과가 0이면 '제로 플래그'가 설정
    • 이 정보는 조건부 분기, 오류 검출 등에서 사용

1.4 스레드

대부분의 현대 운영체제는 프로세스 개념을 확장하여 한 프로세스가 다수의 실행 스레드를 가질 수 있도록 허용합니다.

이러한 특성은 특히 다중 처리기 시스템에서 이익을 얻을 수 있는데, 여러 스레드가 병렬로 실행될 수 있습니다.

2. 프로세스 스케줄링

다중 프로그래밍의 목적CPU 이용을 최대화하기 위하여 항상 어떤 프로세스가 실행되도록 하는 데 있습니다.

시분할의 목적은 각 프로그램이 실행되는 동안 사용자가 상호 작용 할 수 있도록 프로세스들 사이에서 CPU 코어를 빈번하게 교체하는 것입니다.

프로세스 스케줄러는 이 목적을 달성하기 위해서 여러 실행 가능한 프로세스 중에서 하나의 프로세스를 선택해야 합니다.

일반적으로 대부분의 프로세스는 2가지로 설명할 수 있습니다.

  • I/O 바운드 프로세스: 계산에 소비하는 것보다 I/O에 더 많은 시간을 소비하는 프로세스
  • CPU 바운드 프로세스: 곅산에 더 많은 시간을 사용하여 I/O 요청을 자주 생성하지 않는다.

2.1 스케줄링 큐

프로세스가 준비 큐에 들어가게 되면 ready 상태가 된다. 그리고 CPU 코어에 의해서 running 상태로 전환되기를 기다립니다.

큐는 일반적으로 "연결 리스트"로 저장됩니다.

프로세스가 인터럽트 되거나 I/O 요청의 완료와 같은 특정 이벤트가 발생할 때까지 기다린다고 해봅시다.

장치들의 속도는 프로세서보다 상당히 느리게 실행되므로 프로세스는 준비 큐에서 나와 대기 큐(Wait queue)에 삽입됩니다.

프로세스 스케줄링의 일반적인 표현은 큐잉 다이어그램입니다. 아래 그림을 참고하시면 됩니다.

2.2 CPU 스케줄링

CPU 스케줄러의 역할은 준비 큐에 있는 프로세스 중에서 선택된 하나의 프로세스에 CPU 코어를 할당하는 것입니다.

CPU 스케줄러는 일반적으로 훨씬 더 자주 실행되지만 적어도 100ms마다 한번씩 실행됩니다.

스와핑은 필요에 따라 프로세스를 메모리에서 적재하거나 제거하는 것을 의미합니다. 메모리에서 디스크로 "스왑 아웃"하고 현재 상태를 저장하고, 디스크에서 메모리로 "스왑 인"하여 상태를 복원할 수 있습니다.

스와핑은 일반적으로 메모리가 초과 사용되어 가용공간을 확보해야 할 떄만 필요합니다.

현대의 자원은 높은 성능을 자랑하고, 스와핑의 경우 성능을 낮추는 동작이기 때문에 요즘에는 자주 발생하지 않는다고 설명되고 있습니다.

2.3 문맥 교환

문맥은 프로세스의 PCB에 표현됩니다.

일반적으로 커널 모드이건 사용자 모드이건 CPU의 현재 상태를 저장하는 작업을 수행(state save)하고, 나중에 연산을 재개하기 위하여 상태 복구 작업을 수행(state restore)합니다.

문맥 교환(Context Switching)은 CPU 코어를 다른 프로세스로 교환하는 작업을 의미하고, 이 과정에서 이전의 프로세스의 상태를 보관하고 새로운 프로세스의 보관된 상태를 복구하는 작업이 요구됩니다.

여기서 말하는 상태는 PCB라고 보면 됩니다.

문맥 교환이 진행될 동안 시스템은 아무런 유용한 일을 할 수 없기 때문에 순수한 오버헤드입니다.

문맥 교환이 발생할 때 메인 메모리(RAM) 혹은 2차 메모리(SSD)와 같은 정보들은 변경이 일어날 필요가 없겠지만, 하나의 CPU 코어가 사용하는 명령어 파이프라인, 레지스터등과 같은 자원들은 공유해서 사용하기 때문에 교체되는 작업이 필요합니다.

새로운 프로세스의 메모리 주소 공간으로 CPU의 접근을 재맵핑 하는 작업은 메모리 관리 유닛(MMU)를 통해 이루어집니다.

3. 프로세스에 대한 연산

여기서는 프로세스의 생성 및 종료를 위한 기법에 대해서 알아보고 UNIX와 Window 시스템에서의 프로세스 연산에 대해서 설명할 것입니다.

3.1 프로세스 생성

프로세스는 부모 프로세스자식 프로세스로 구분될 수 있는데, 이것은 프로세스의 트리를 형성합니다.

대부분의 현대 운영체제들은 유일한 프로세스 식별자(pid)를 사용하여 프로세스를 구분하는데 이 식별자는 보통 정수입니다.

이 식별자를 통하여 커널이 유지하고 있는 프로세스의 다양한 속성에 접근하기 위한 찾아보기(index)로 사용됩니다.

pid 1인 systemd 프로세스(부트 프로그램)가 모든 사용자의 프로세스의 루프 부모 프로세스 역할을 수행하고 시스템이 부트될 때 생성되는 사용자 프로세스입니다.

자식 프로세스가 생성될 때 자원 할당과 관련하여 여러 가지를 고려해볼 수 있는데,

  1. 운영체제가 직접 할당
  2. 부모 프로세스가 가진 자원의 부분 집합 사용

또한, 부모 프로세스는 자식 프로세스와 메모리를 공유해서 사용할 수도 있을 것입니다.

부모 프로세스 자원의 일부분만 사용하도록 자식 프로세스가 쓸 수 있게 제한하며, 자식 프로세스들을 많이 생성하여 시스템을 과부화 상태로 만드는 프로세스를 방지할 수 있습니다.

프로세스가 새로운 프로세스를 생성할 때, 두 프로세스를 실행시키는 데 두 가지 가능한 방법이 존재합니다.

  1. 부모는 자식과 병행하게 실행을 계속합니다.
  2. 부모는 일부 또는 모든 자식이 실행을 종료할 때까지 기다립니다.

새로운 프로세스들의 주소 공간 측면에서 볼 떄 다음과 같은 두 가지 가능성이 있습니다.

  1. 자식 프로세스는 부모 프로세스의 복사본이다(자식 프로세스는 부모와 똑같은 프로그램과 데이터를 가진다).
  2. 자식 프로세스가 자신에게 적재될 새로운 프로그램을 가지고 있다.

프로세스의 복사는 fork() 명령어를 생각하면 되고, 부모 프로세스와 자식 프로세스는 pid를 통해 연결되어 있다고 생각하면 됩니다.

fork() 명령어를 통해 프로세스가 생성되고 exec() 시스템 콜을 호출하여 이진 파일을 메모리로 적재(load)하여 프로그램을 실행시킬 수 있습니다.

만약, 부모 프로세스가 먼저 끝나서 자식 프로세스를 기다리는 상황이라면 wait() 시스템 콜을 통해 준비 큐에서 자신을 제거합니다.

좀 더 이해를 돕기 위해서 프로세스를 생성하는 코드의 이미지를 두개 남겨두겠습니다.

위의 코드는 fork() 시스템 콜을 활용하여 자식이 부모의 복사본을 실행하는 병행 실행 프로세스입니다.

매개 변수에 대해서 간단하게 설명을 적어보자면,

  • STARTUPINFO: 새로운 프로세스의 특징
  • PROCESS_INFOMATION: 새로 생성된 프로세스와 스레드에 대한 핸들식별자들을 포함
  • pi.hProcess: 자식 프로세스의 핸들

3.2 프로세스 종료

exit 시스템 콜을 사용하여 종료를 할 때 프로세는 자신을 기다리고 있는 부모 프로세스에(wait 시스템 콜을 통해) 상태 값(통상 정수값)을 반환할 수 있습니다.

그리고 할당 받은 모든 자원이 해제되고 운영체제로 반납됩니다.

부모는 다음과 같이 여러 가지 이유로 인하여 자식 중 하나의 실행을 종료할 수 있습니다.

  • 자식이 자신에게 할당된 자원을 초과하여 사용할 때. 이때는 부모가 자식들의 상태를 검사할 수 있는 방편이 주어져야 합니다.
  • 자식에게 할당된 태스크가 더 이상 필요 없을 때
  • 부모가 exit를 하는데, 운영체제는 부모가 exit 한 후에 자식이 실행을 계속하는 것을 허용하지 않는 경우

특히, 마지막 경우는 연쇄식 종료(cascading termination)이라고 부르며 이 작업은 운영체제가 시행합니다.

다음은 wait() 시스템 콜을 사용하는 코드입니다.

pid_t pid;
int status;

pid = wait(&status); // 자식 프로세스를 구별할 수 있게 해줍니다.

프로세스가 종료하면 사용하던 자원은 운영체제가 되찾아 갑니다. 그러나 프로세스의 종료 상태가 저장되는 프로세스 테이블의 해당 항목은 부모 프로세스가 wait()를 호출할 때까지 남아 있게 됩니다.

종료 되었지만 부모 프로세스가 wait() 호출을 하지 않은 프로세스를 좀비(zombie) 프로세스라고 합니다.

부모 프로세스가 wait()를 호출하는 대신 종료한다면 해당 프로세스의 자식 프로세스는 고아(orphan) 프로세스가 됩니다.

UNIX 는 고아 프로세스의 새로운 부모 프로세스로 init 프로세스를 지정함으로써 문제를 해결합니다.

init 프로세스는 주기적으로 wait()를 호출하여 고아 프로세스의 종료 상태를 수집하고 프로세스 식별자프로세스 테이블 항목을 반환합니다.

3.2.1 Android 프로세스 계층

Android는 자원 제약 때문에 제한된 시스템 자원을 회수하기 위해 기존 프로세스를 종료해야 할 수도 있습니다. 따라서, 프로세스의 중요도 계층을 식별했습니다.

우선순위 별로 정리를 해보겠습니다.

  • 전경 프로세스(forground process): 사용자가 현재 상호 작용하고 있는 응용 프로그램, 즉 화면에 보이는 현재 프로세스
  • 가시적 프로세스(visible process): 전경에서 직접 볼 수 없지만 전경 프로세스가 참조하는 활동을 수행하는 프로세스 (현재 상태가 전경 프로세스에 표시되는 활동)
  • 서비스 프로세스(service process): 백그라운드 프로세스와 유사하지만 사용자가 인지할 수 있는 활동을 수행하는 프로세스 (예: 음악 스트리밍)
  • 백그라운드 프로세스(background process): 활동을 수행하고 있지만 사용자가 인식하지 못하는 프로세스
  • 빈 프로세스(empty process): 응용 프로그램과 관련된 활성 구성요소가 없는 프로세스

4. 프로세스 간 통신

운영체제 내에서 실행되는 병행 프로세스들은 독립적이거나 협력적인 프로세스들일 수 있습니다.

프로세스 협력을 허용하는 환경을 제공하는 데는 몇 가지 이유가 있습니다.

  • 정보 공유(information sharing): 여러 응용 프로그램이 동일한 정보에 흥미를 느낄 수 있음으로, 그러한 정보를 병행적으로 접근할 수 있는 환경을 제공해야 합니다.
  • 계산 가속화(computation speedup): 특정 태스크를 빨리 실행하고자 한다면, 서브 태스크로 나누어 다른 서브태스크들과 병렬로 실행되게 해야 합니다. 이는 복수 개의 처리코어를 가진 경우에만 달성할 수 있습니다.
  • 모듈성(modularity): 시스템 기능을 별도의 프로세스들 또는 스레드들로 나누어, 모듈식 형태로 시스템을 구성할 수 있습니다.

협력적 프로세스들은 데이터를 교환할 수 있는 프로세스 간 통신(interprocess communication, IPC) 기법이 필요합니다.

프로세스 간 통신에는 기본적으로 공유 메모리(shared memory)메시지 전달(message passing)의 두 가지 모델이 있습니다.

분산 시스템 환경에서는 메시지 전달이 구현하기 더 쉽고 유용합니다. 하지만 시스템 콜을 이용하여 부가적인 시간 소비 작업이 필요하기 때문에 속도적인 측면에서는 공유 메모리가 더 빠릅니다.

공유 메모리 영역이 구축되면 모든 접근은 일반적인 메모리 접근으로 취급되어 커널의 도움이 필요 없습니다.

특징들을 정리하자면 다음과 같습니다.

메시지 전달

  • 접근 충돌을 회피할 필요가 X
  • 적은 양의 데이터 교환에 유용
  • 분산 시스템에서 유용

공유 메모리

  • 공유 메모리 영역을 구출할 때만 System call 사용
  • 더 빠르다.

5. 공유 메모리 시스템에서의 프로세스 간 통신

통상 공유 메모리 영역은 공유 메모리 세그먼트를 생성하는 프로세스의 주소 공간에 위치합니다.

일반적으로는 서로 다른 프로세스들 간의 메모리 영역 칩입을 허용하지 않지만, 공유 메모리는 둘 이상의 프로세스가 이 제약 조건을 제거하는 것에 동의를 하여 사용됩니다.

운영체제가 메모리의 영역을 할당해주는 것은 맞지만, 데이터의 형식과 위치는 프로세스에 의해 결정되며, 책임은 프로세스들한테 있습니다.

이 말은 곧 공유 메모리의 데이터에 대한 처리는 개발자의 몫이라는 의미입니다.

아주 일반적인 개념으로는 생산자-소비자 문제를 고려해볼 수 있습니다.

생산자 프로세스는 공유 메모리에 데이터에 대해서 쓰기 동작을 수행하며, 소비자 프로세스는 공유 메모리에 있는 데이터에 대해서 읽기만 수행하는 것입니다.

이렇게 단순한 경우일지라도, 두 프로세스 간에 동기화가 되어 있지 않다면 존재하지 않는 데이터들을 소비자가 소비하려고 계속 시도할 수 있습니다.

여기서 버퍼(Queue)에 대한 개념이 등장하는데,

생산자가 버퍼에 work를 넣어주고, 소비자는 해당 버퍼에서 하나씩 work를 꺼내가면서 동작을 수행할 수 있습니다. 이것은 다음 코드를 보면 이해할 수 있습니다.

생산자는 버퍼가 가득 차있을 경우에는 item을 넣지 않고, 넣을 수 있는 상황에서만 item을 넣습니다. 동시에 소비자는 item을 계속해서 소비하고 소비할 item이 없다면 아무것도 하지 않습니다.

5.1 추가 정리

위에서 정리한 것이 헷갈릴 수 있습니다.

그냥 공유 메모리에 저장해두고 프로세스들이 그것에 접근해가며 일반 데이터 접근과 같이 사용하면 되는 것 아닌가? 라는 생각이 들 수 있습니다.

하지만, 이러한 접근은 잘못된 접근입니다.

여기서는 몇 가지 개념을 추가적으로 정리를 할 필요가 있습니다.

  • 우선, 소비자 프로세스들이 공유 메모리에 있는 데이터들을 필요할 때마다 읽어오는 것이 아닙니다.
  • 생산자 프로세스가 공유 메모리에 데이터를 넣고 나서 소비자 프로세스들이 해당 데이터를 읽어와서 자신의 메모리 영역에 저장해두고 사용합니다.
  • 생산자 프로세스가 해당 데이터를 업데이트해야 되는 상황이 발생한다면 공유 메모리에 업데이트된 값을 넣고, 소비자 프로세스들에게 알릴 수 있습니다.(예: 조건 변수, 이벤트 등)
  • 생산자 프로세스가 여러 개일 수 있기 때문에 해당 과정에서 동기화 작업(예: 세마포어)이 필요할 수 있습니다.

헷갈릴 수 있는 이유는 멀티스레드 상황에서 여러 스레드들이 하나의 공유된 데이터에 접근하는 것과는 조금 다르기 때문입니다. 해당 과정에서는 복사본을 만들지 않죠.

복사본을 만드는 이유는 데이터의 일관성과 안전한 사용을 위한 일반적인 방법입니다.
이것은 공유 메모리 영역에 대한 의존도를 줄이고, 데이터 접근 속도를 향상시킬 수 있습니다.

하나씩 풀어가면서 설명해 봅시다.

우선, 공유 메모리에 데이터를 생성하는 과정부터 언급을 해봅시다.

  • 해당 공유 메모리에 데이터를 생성하려고 여러 프로세스들(생성자)이 접근할 수 있습니다.

    • 당연히, 이러한 과정에서는 데이터의 동시 접근을 제어하기 위해서 이를 위한 동기화 작업이 필요합니다. (예: 세마포어 등)
  • 데이터 Write 과정을 성공적으로 마쳤다고 가정하고, 이후 소비자 프로세스들에게 데이터를 Write를 했다고 알릴 수 있어야 합니다.

    • 이것은 이벤트 혹은 조건 변수를 사용할 수 있습니다.
  • 각 소비자 프로세스들이 Read하는 것도 성공했다고 합시다. 이 과정에서 각자의 프로세들은 해당 공유된 데이터의 주소값을 이용해서 그대로 사용하는 것이 아니라 자신들의 프로세스 메모리 영역에 복사하여 사용합니다.

    • 위에서도 말했지만 이것은 데이터의 일관성과 경쟁 상태(race condition) 문제를 해결하고, 빠른 접근 속도를 가져가기 위함에 있습니다.
  • 각자의 프로세스들은 사용 목적에 맞게 자신들의 특정 메모리 영역에 저장을 하여서 사용할 것이고, 이것은 해당 프로세스의 여러 스레드들이 공유할 수도 있으니 이 경우에도 동기화 작업이 별도로 수행될 수 있습니다.

  • 마지막으로, 생산자 프로세스가 해당 공유된 데이터를 업데이트해야 되는 상황이 발생했다고 가정해봅시다.

    • 이 경우에는 이미 소비자 프로세스들이 복사본을 만들어서 실컷 가지고 놀았을 수 있습니다. 즉, 이미 초기값은 날아가 버린 상태일 수 있다는 말입니다.
    • 즉, 생산자 프로세스가 해당 데이터 값을 업데이트 한 후 소비자 프로세스들에게 알려서 이전 복사본을 날리든지, 덮어쓰든지, 새로운 복사본을 만드는지는 데이터의 중요성과 필요성 즉, 설계에 따라 달라질 것입니다.

6. 메시지 전달 시스템에서의 프로세스 간 통신

메시지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신을 하고, 그들의 동작을 동기화할 수 있도록 허용하는 기법을 제공합니다.

메시지 전달 시스템은 최소한 두 가지 연산을 제공합니다.

send(message)
receive(message)

통신을 하기 위해서는 통신 연결(communication link)가 설정되어 있어야 하고, 이것은 논리적인 연결을 의미합니다.

하나의 링크와 send()/receive() 연산ㅇ르 논리적으로 구현하는 다수의 방법은 다음과 같습니다.

  • 직접 또는 간접 통신
  • 동기식 또는 비동기식 통신
  • 자동 또는 명시적 버퍼링

6.1 명명

직접 통신하에서, 통신을 원하는 각 프로세스는 통신의 수신자 또는 송신자의 이름을 명시해야 합니다.

  • send(P, messgage): 프로세스 P에 메시지를 전송한다.
  • receive(Q, message): 프로세스 Q로부터 메시지를 수신한다.

이 기법에서 통신 연결은 다음의 특성을 가집니다.

  • 통신을 원하는 각 프로세스의 쌍들 사이에 연결이 자동으로 구축된다. 프로세스들은 통신하기 위해 상대방의 신원(identity)만 알면 된다.
  • 연결은 정확히 두 프로세스 사이에만 연관된다.
  • 통신하는 프로세스들의 각 쌍 사이에는 정확하게 하나의 연결이 존재해야 한다.

해당 방식은 대칭성을 보이는데, 비대칭성을 사용할 수도 있습니다.

  • send(P, message): 메시지를 프로세스 P에 전송한다.
  • receive(id, message): 임의의 프로세스로부터 메시지를 수신한다. 변수 id는 통신을 발생시킨 프로세스의 이름으로 설정된다.

두 방식의 단점은 프로세스를 지정하는 방식 때문에 모듈성을 제한한다는 것입니다.

간접 통신에서 메시지들은 메일박스(mailbox) 또는 포트(port)로 송신되고, 그것으로부터 수신됩니다.

각 메일박스는 고유한 id의 정수값을 가지고 그것을 이용하여 통신을 합니다.

  • send(A, message): 메시지를 메일박스 A로 송신한다.
  • receive(A, message): 메시지를 메일박스 A로부터 수신한다.

특징은 다음과 같다.

  • 한 쌍의 프로세스들 사이의 연결은 이들 프로세스가 메일박스를 가질 때만 구축된다.
  • 연결은 두 개 이상의 프로세스들과 연관될 수 있다.
  • 통신하고 있는 각 프로세스 사이에는 다수의 서로 다른 연결이 존재할 수 있고, 각 연결은 하나의 메일박스에 대응됩니다.

6.2 동기화

메시지 전달은 봉쇄형(blocking)이거나 비봉쇄형(nonblocking) 방식으로 전달됩니다. 이 두 방식은 각각 동기식, 비동기식으로 알려져 있습니다.

만약, 송신자와 수신자 모두 동기식 방식으로 구현을 했다면 랑데부(rendezvous)가 발생하여, 동기화는 해결되지만 성능적인 측면에서 문제가 될 것입니다.

6.3 버퍼링

메시지가 들어가게 되는 큐를 구현하는 방식은 세 가지가 있습니다.

  • 무용량(zero capacity): 큐의 최대길이 = 0. 즉, 링크는 자체 안에 대기하는 메시지들을 가질 수 없습니다. 이 경우에, 송신자는 수신자가 메시지를 수신할 때까지 기다려야 합니다.
  • 유한 용량(bounded capacity): 큐의 유한한 길이 n을 가진다. 큐가 만원이 아니라면, 메시지는 큐에 놓이며, 송신자는 대기하지 않고 실행을 계속한다. 반대로 만원이라면, 송신자는 뮤 안에 공간이 이용 가능할 때까지 반드시 봉쇄되어야 합니다.
  • 무한 용량(unbounded capacity): 큐는 잠재적으로 무한한 길이를 가집니다. 송신자는 봉쇄될 일이 없습니다.

7. IPC 시스템의 사례

간단하게 이해를 돕기위한 차원에서 정리를 하겠습니다.

7.1 POSIX 공유 메모리

POSIX 공유 메모리는 메모리-사상 파일을 사용하여 구현합니다.

fd = shm_open(name, O_CREAT | O_RDWR, 0666); // 공유 메모리 객체
  • 첫 번째 인자: 공유 메모리 객체 이름
  • 두 번째 인자: 객체가 존재하지 않으면 생성, 읽기와 쓰기가 가능한 상태로 열린다.
  • 세 번째 인자: 공유 메모리 객체에 파일-접근 허가권을 부여
ftruncate(fd, 4096); // 객체의 크기 설정

마지막으로 nmap() 함수를 사용하여, 공유 메모리 객체를 포함하는 메모리-사상 파일을 구축합니다.

nmap() 함수는 파일의 포인터를 반환합니다.

공유 메모리 제거는 shm_unlink() 함수를 호출하여 제거할 수 있습니다.

코드는 생략하겠습니다.

7.2 Mach 메시지 전달

Mach 커널은 프로세스와 유사하지만 제어 스레드가 많고 관련 자원이 적은 다중 태스크의 생성 및 제거를 지원합니다.

통신은 메시지로 수행되며, 포트(port)라고 하는 메일박스로 메시지를 주고 받습니다.

포트의 크기는 정해져 있고 단방향입니다.

Mach는 포트를 사용하여 자원을 나타내며, 메시지 전달은 객체 지향 접근 방식을 제공합니다.

각 포트에는 그 포트와 상호 작용하는 데 필요한 자격을 식별하는 포트 권한 집합이 연관됩니다.

즉, 태스크가 다른 태스크로 메시지를 전달하려고 할 때 태스크가 소유하고 있는 포트 권한이 필요합니다.

포트 권한은 태스크(프로세스) 단위로, 동일한 태스크에 속하는 모든 스레드가 동일한 포트 권한을 공유합니다. 따라서 동일한 태스크에 속하는 두 개의 스레드는 각 스레드와 관련된 스레드-별 포트를 통해 메시지를 교환하여 쉽게 통신할 수 있습니다.

태스크가 생성되면 두 개의 특별한 포트도 생성됩니다.

  • Task Self 포트: 포트에 대한 수신 권한을 가지고 있어 태스크가 커널에 메시지를 보낼 수 있다.
  • Notify 포트: 커널은 이벤트 발생 알림을 해당 포트를 이용하여 보낼 수 있다.

다음 함수는 새 포트를 작성하고, 메시지 큐를 위한 공간을 할당하며 포트에 대한 권한을 식별합니다.

mach_port_t port;	// 포트 권한의 이름

mach_port_allocate(
	mach_task_self(),	// a task referring to itself
    MACH_PORT_RIGHT_RECEIVE,	// the right for this port
    &port);	// the name of the port right

각 태스크는 부트스트랩 포트에 액세스 할 수 있어서 태스크가 생성한 포트를 시스템 전체의 부트스트랩 서버에 등록할 수 있습니다.

해당 서버의 레지스트리에서 포트 검색을 수행할 수 있습니다.

각 포트와 관련된 큐는 크기가 제한되어 있으며 처음에는 비어 있습니다. 메시지가 포트로 전송되면 큐에 복사됩니다. 모든 메시지는 안정적으로 전달되며 동일한 우선순위를 가집니다.

여기서 살펴봐야되는 것은 메시지가 고정 크기인지 가변 길이인지를 살펴봐야 합니다.

메시지는 단순하거나 복잡할 수 있습니다.

메시지가 복잡하다면,

  • "out-of-line"를 통해 데이터를 포함하는 메모리 위치에 대한 포인터를 전달
  • 다른 태스크에 포트 권한을 전송

이와 같은 방법이 유용합니다.

여기서는 코드를 넣어놓도록 하겠습니다. 메시지 전달을 하기위해 어떠한 매개변수들이 필요한지 확인할 수 있습니다.

만약 포트의 큐가 가득찬 경우 송신자는 mach_msg()의 매개변수를 통해 특정 동작을 취할 수 있습니다.

  • 큐에 공간이 생길때 까지 무기한 대기
  • 최대 n밀리초 동안 대기
  • 대기 없이 즉시 복귀
  • 메시지를 일시적으로 캐시. 메시지를 큐에 넣을 수 있을 때, 통지 메시지가 송신자에게 전송. 큐가 가득 찼ㅇ르 경우 송신 스레드마다 하나의 메시지만 커널에 보관할 수 있음.

마지막 옵션은 서버 태스크를 위한 것입니다.

메시지 시스템 주요 문제점은 일반적으로 송신자 포트에서 수신자의 포트로 메시지를 복사해야 하므로 발생하는 성능 저하입니다.

Mach 메시지 시스템은 가상 메모리 기술을 사용하여 복사 연산을 피하려고 합니다. 기본적으로 Mach는 송신자의 메시지가 포함된 주소 공간을 수신자의 주소 공간에 매핑합니다. 따라서 송신자와 수신자 모두 동일한 메모리에 액세스 하므로 메시지 자체는 실제로 복사되지 않습니다.

이 기술은 시스템 내 메시지에만 작동합니다.

7.3 Windows

Windows 운영체제는 모듈화를 이용하여 기능을 향상시키고 새로운 기능을 구현하는 시간을 감소시킨 최신 설계의 예입니다.

Windows는 다중 운영 환경 또는 서브 시스템을 지원하며, 응용 프로그램은 메시지 전달 기법을 통해 이들과 통신합니다.

Windows의 메시지 전달 설비는 고급 로컬 프로시저 호출 설비(advanced local procedure call facility, ALPC)라고 불립니다.

ALPC는 동일 기계상에 있는 두 프로세스 간의 통신에 사용됩니다. 이것은 널리 사용되는 RPC 기법과 같으나, Windows에 맞게 특별히 최적화되었습니다.

Mach와 유사하게, 포트를 사용하고 연결 포트(connection port)통신 포트(communication port)의 두 가지 유형의 포트를 사용합니다.

이 둘의 사용은 필자의 생각으로는 TCP Connection 매커니즘과 매우 유사합니다.

클라이언트는 서브시스템으로부터 서비스를 원할 경우에, 먼저 연결 포트 객체에 대한 핸들을 열고 연결 요청을 보냅니다. -> 환영 소켓으로 연결 요청

서버는 채널을 생성하고 핸들을 클라이언트에게 반환합니다. -> 통신 소켓 준비

채널은 한 쌍의 사적인 통신 포트로 구성되고, 메시지를 주고 받습니다. -> 메시지 송수신

ALPC 채널이 생성되면 다음 3가지 중 하나의 메시지 전달 기법이 선택됩니다.

  1. 256바이트까지의 작은 메시지의 경우, 포트의 메시지가 큐가 중간 저장소로 사용되고, 메시지는 프로세스에서 프로세스로 복사됩니다.
  2. 대용량 메시지는 반드시 섹션 객체(section object)를 통하여 전달. 섹션 객체란 채널과 연관된 공유 메모리의 영역을 말합니다.
  3. 데이터의 양이 너무 많아서 섹션 객체에 저장할 수 없는 경우, 서버 프로세스가 클라이언트의 주소 공간을 직접 읽거나 쓸 수 있는 API를 사용

Windows의 고급 로컬 프로시저 호출 설비는 Windows API의 부분이 아닙니다.
따라서 프로그래머가 사용할 수 없고 내부적으로 외부 통신은 RPC를 내부 통신은 ALPC을 사용합니다.

7.4 파이프

파이프는 두 프로세스가 통신할 수 있게 하는 전달자로서 동작합니다.

일반적으로 같은 시스템에서 다른 프로세스끼리 통신을 할 때 사용됩니다.

7.4.1 일반 파이프

일반 파이프는 생산자-소비자 형태로 두 프로세스 간의 통신을 허용합니다.

생산자는 파이프의 한 종단(쓰기 종단)에 쓰고, 소비자는 다른 종단(읽기 종단)에서 읽습니다. 단방향 통신을 하게 됩니다.

만약 양방향 통신이 필요하다면 두 개의 파이프를 사용해야 합니다.

UNIX 시스템에서 일반 파이프는 다음 함수를 사용합니다.

pipe(int fd[])

이 함수는 fd[] 파일 설명자를 통해 접근되는 파이프를 생성합니다. UNIX는 파이프를 파일의 특수한 유형으로 취급합니다. 따라서 파이프는 일반적인 read()write() 시스템 콜을 사용하여 접근할 수 있습니다.

앞서 fork()로 생성한 자식 프로세스는 열린 파일을 부모 프로세스로부터 상속받는다고 했는데, 파이프는 파일의 특수한 유형이기 때문에 자식 프로세스는 부모로부터파이프를 상속받습니다.

이러한 특성 때문에 일반 파이프는 부모 프로세스와 fork()로 생성한 자식 프로세스와 통신하기 위해 사용됩니다.

Windows 시스템의 일반 파이프는 익명 파이프(anonymous pipe)라고 불리며 UNIX에 대응되는 파이프와 유사하게 동작합니다.

차이점은 UNIX의 경우 자식 프로세스가 부모 프로세스가 생성한 파이프를 자동으로 상속받는 것에 비해 Windows는 프로그래머가 어던 속성을 상속받는지 명시해야 한다는 점입니다.

7.4.2 지명 파이프

일반 파이프는 오로지 프로세스들이 서로 통신하는 동안에만 존재합니다.

지명 파이프(named pipes)는 좀 더 강력한 통신 도구를 제공합니다.

  • 양방향 통신
  • 부모-지삭 관계 제약 X
  • 여러 프로세스들이 사용하여 통신
  • 통신 프로세스가 종료되더라도 계속 존재

8. 클라이언트 서버 환경에서 통신

여기서는 클라이언트 서버에서 사용할 수 있는 두 가지 다른 통신 전략에 대해서 설명을 합니다.

  • 소켓(sockets)
  • 원격 프로시저 호출(RPCs)

8.1 소켓

소켓(socket)은 통신의 극적(endpoint)를 뜻합니다.

소켓을 사용한 통신 같은 경우 Network에서 많이 다루었기 때문에 자세한 정리는 하지 않겠습니다.

1024 미만의 well-known 포트를 이용하여 연결을 하고 이후 다른 포트를 이용하여 통신을 합니다.

식별자로 사용하는 것은 송수신자의 IP 주소와, 포트 번호를 사용하여 4개의 값으로 연결을 구분합니다.

소켓을 이용한 통신은 분산된 프로세스 간에 널리 사용되고 효율적이기는 하지만 너무 낮은 수준입니다.

소켓은 스레드 간에 구조화 되지 않은 바이트 스트림만을 통신하도록 하기 때문입니다.

이러한 원시적인 바이트 스트림 데이터를 구조화하여 해석하는 것은 클라이언트 서버의 책임입니다.

8.2 원격 프로시저 호출

원격 서비스와 관련한 가장 보편적인 형태 중 하나는 RPC 패러다임으로서, 네트워크에 연결된 두 시스템 사이의 통신에 사용하기 위하여 프로시저 호출 기법을 추상화 하는 방법으로 설계되었습니다.

IPC 방식과는 달리 RPC 통신에서 전달되는 메시지는 구조화되어 있고, 따라서 데이터 패킷 수준을 넘어서게 된다.

RPC 시스템은 클라이언트 쪽에 스텁(stub)을 제공하여 통신을 하는데 필요한 자세한 사항들을 숨겨 줍니다.

  • 클라이언트가 원격 프로시저 호출
  • RPC가 그에 대응하는 스텁을 호출
  • 원격 프로시저가 필요로 하는 매개변수를 건네줌
  • 스텁이 원격 서버의 포트를 찾고 매개변수를 정돈(marshall)
    • 메시지 정돈을 통해 데이터 표현 방식의 차이 해결
    • 중립적인 데이터 표현방식 XDR(external data representation) 사용
  • 스텁은 메시지 전달 기법을 사용하여 서버에게 메시지를 전송
  • 서버측 스텁이 받아서 동작 수행

이러한 방식으로 동작하기 때문에 RPC는 클라이언트가 원격 프로시저 호출을 마치 자기의 프로시저 호출하는 것처럼 해줍니다.

RPC의 경우 지역 프로시저 호출과는 다르게 네트워크 오류 때문에 실패할 수 있고, 메시지가 중복되어 여러 번 실행될 수 있습니다.

이 문제를 해결하는 방법은 운영체제가 메시지가 최대 한번 실행 되는 것이 아니라 정확히 한번 처리되도록 보장하는 것입니다.

"최대 한 번"의 의미는 각 메시지에 타임 스탬프를 매기는 것으로 보장할 수 있다.

"정확히 한 번"은 ARQ 방식을 통해 보장할 수 있다.

마지막으로 사용 포트의 결정은 처음에 고정된 포트 번호를 활용하여 연결 포트의 정보를 주고 받으면서 해결될 수 있다. (3-way handshake와 유사)

RPC에서는 RPC포트를 통해 랑데부용 디먼을 제공한다고 한다. 그러면 클라이언트가 자신이 실행하기를 원하는 RPC 이름을 담고 있는 메시지를 랑데부 디먼에게 보내서, RPC 이름에 대응하는 포트 번호가 무엇인지 알려달라고 요청합니다.

포트 번호가 클라이언트에게 반환되고, 클라이언트는 그 포트 번호로 RPC 요청을 계속 보냅니다.

RPC는 분산 파일 시스템(distributed file system, DFS)을 구현하는 데 유용하다고 합니다.

외부 서버에 데이터에 대한 특정 동작을 맡기고, 응답을 결과로 받아오는 것이죠.

참고한 자료

  • 운영체제[공룡책]
profile
개발정리블로그

0개의 댓글