본문으로 건너뛰기

프로세스

초기의 컴퓨터 시스템은 한 번에 하나의 프로그램만을 실행하도록 허용하였다. 이 프로그램이 시스템을 완전히 제어하고, 시스템의 모든 자원에 접근할 수 있었다. 반면 오늘날의 컴퓨터 시스템들은 메모리에 다수의 프로그램이 적재되어 병행 실행되는 것을 허용한다. 이러한 발전은 다양한 프로그램을 보다 견고하게 제어하고 보다 구획화할 것을 필요로 했다. 이러한 필요성이 프로세스의 개념을 낳았으며, 프로세스란 실행 중인 프로그램을 말한다. 프로세스는 현대의 컴퓨팅 시스템에서 작업의 단위이다.

운영체제가 더욱 복잡해질수록, 사용자를 위해 더 많은 기능이 기대된다. 비록 운영체제의 주 관심은 사용자 프로그램을 실행하는 것이지만, 또한 운영체제는 커널 안에서 보다 사용자 공간에서 가장 잘 할 수 있는 다양한 시스템 작업을 처리할 필요가 있다. 그러므로 하나의 시스템의 일부는 사용자 코드를 실행하고 일부는 운영체제 코드를 실행하는 프로세스의 집합체이다. 이들 모든 프로세스는 잠재적으로 병행 실행이 가능하고, CPU는 이들 프로세스 가운데서 다중화(multiplex) 된다.

프로세스 개념

초창기 컴퓨터는 작업을 실행하는 일괄처리 시스템이었고, 사용자 프로그램 또는 태스크(task)를 실행하는 시분할 시스템이 뒤를 이었다. 단일 사용자 시스템에서도 사용자는 워드 프로세서, 웹 브라우저 및 전자 메일 패키지와 같은 여러 프로그램을 한 번에 실행할 수 있다. 또한 다중 태스킹을 지원하지 않는 임베디드 장치에서와 같이 컴퓨터가 한 번에 하나의 프로그램만 실행할 수 있더라도 운영체제는 메모리 관리와 같은 자체 프로그램된 내부 활동을 지원해야 할 수도 있다. 여러 측면에서 이러한 모든 활동을 프로세스라고 부를 수 있다.

프로세스

비공식적으로, 프로세스란 실행 중인 프로그램이다. 프로세스의 현재 활동의 상태는 프로그램 카운터 값과 프로세서 레지스터의 내용으로 나타낸다. 프로세스의메모리 배치는 일반적으로 여러 섹션으로 구분되며 아래와 같다.


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

텍스트 및 데이터 섹션의 크기는 고정되기 때문에 프로그램 실행 시간 동안 크기가 변하지 않는다. 그러나 스택 및 힙 섹션은 프로그램 실행 중에 동적으로 줄어들거나 커질 수 있다. 함수가 호출될 때마다 함수 매개변수, 지역 변수 및 복귀 주소를 포함하는 활성화 레코드(activation record)가 스택에 푸쉬된다. 함수에서 제어가 되돌아오면 스택에서 활성화 레코드가 팝된다. 마찬가지롬 메모리가 동적으로 할당됨에 따라 힙이 커지고 메모리가 시스템에 반환되면 축소된다. 스택 및 힙 섹션이 서로의 방햐응로 커지더라도 운영체제는 서로 겹치지 않도록 해야한다.

우리는 프로그램 그 자체는 프로세스가 아님을 강조한다. 프로그램은 명령어 리스트를 내용으로 가진 디스크에 저장된 파일(실행 파일이라고 불림)과 같은 수동적인 존재(passive entity)이다. 이와 대조적으로 프로세스는 다음에 실행할 명령어를 지정하는 프로그램 카운터와 관련 자원의 집합을 가진 능동적인 존재(active entity)이다. 실행 파일이 메모리에 적재될 때 프로그램은 프로세스가 된다. 실행 파일을 메모리에 적재하는 일반적인 두 가지 방식은 실행 파일을 나타내는 아이콘을 더블 클릭하는 방식과 명령어 라인상에서 prog.exe 또는 a.out과 같이 파일 이름을 입력하는 방식이다.

두 프로세스들이 동일한 프로그램에 연관될 수 있지만, 이들은 두 개의 별도의 실행 순서로 간주한다. 예를 들어, 여러 사용자가 메일 프로그램의 서로 다른 복사본을 실행하거나, 또는 동일 사용자가 웹 브라우저 프로그램의 여러 복사본을 호출할 수 있다. 이들 각각은 별도의 프로세스이며, 텍스트 섹션이 동등하다 할지라도 데이터, 힙 및 스택 섹션은 다를 수 있다.

프로세스 자체가 다른 개체를 위한 실행 환경으로 동작할 수 있다는 사실에 주목하십시오. Java 프로그래밍 환경이 좋은 예를 보인다. 대부분의 상황에서 실행 가능한 Java 프로그램은 Java 가상 기계(JVM)안에서 실행된다. JVM은 적재된 Java 코드를 해석하고 그 코드를 대신하여 원 기계어를 이용하는 행동을 취하는 프로세스로서 프로그램을 실행한다.

프로세스의 상태

프로세스는 실행되면서 그 상태가 변한다. 프로세스의 상태는 부분적으로 그 프로세스의 현재의 활동에 따라서 정의된다.

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

프로세스 제어 블록

각 프로세스는 운영체제에서 프로세스 제어 블록에 의해 표현된다. 프로세스 제어 블록은 특정 프로세스와 연관된 여러 정보를 수록하며 다음과 같은 것들을 포함한다.

  • 프로세스 상태: 상태는 new, ready, running, waiting 또는 halted 상태 등이다.
  • 프로그램 카운터: 프로그램 카운터는 이 프로세스가 다음에 실행할 명령어의 주소를 가리킨다.
  • CPU 레지스터들: CPU 레지스터는 컴퓨터의 구조에 따라 다양한 수와 유형을 가진다. 레지스터에는 누산기(accumulator), 인덱스 레지스터, 스택 레지스터, 범용(general purpose) 레지스터들과 상태 코드(condition code) 정보가 포함된다. 프로그램 카운터와 함께 이 상태 정보는, 나중에 프로세스가 다시 스케줄 될 때 계속 올바르게 실행되도록 하기 위해서 인터럽트 발생시 저장되어야 한다.
  • CPU 스케줄링 정보: 이 정보는 프로세스 우선순위, 스케줄 큐에 대한 포인터와 다른 스케줄 매개변수를 포함한다.
  • 메모리 관리 정보: 운영체제에 의해 사용되는 메모리 시스템에 따라 base 레지스터와 한계(limit) 레지스터의 값, 운영체제가 사용하는 메모리 시스템에 따라 페이지 테이블 또는 세그먼트 테이블 등과 같은 정보를 포함한다.
  • 회계 정보: CPU 사용 시간과 경과된 실시간, 시간 제한, 계정 번호, 잡 또는 프로세스 번호등을 포함한다.
  • 입출력 상태 정보: 프로세스에 의해 할당된 입출력 장치들과 열린 파일의 목록등을 포함한다.

요약하면 PCB는 약간의 회계 데이터와 함께 프로세스를 시작시키거나 다시 시작시키는데 필요한 모든 데이터를 위한 저장소의 역할을 한다.

스레드

대부분의 현대 운영체제는 프로세스 개념을 확장하여 한 프로세스가 다수의 실행 스레드를 가질 수 있도록 허용한다. 그들은 따라서 프로세스가 한 번에 하나 이상의 일을 수행할 수 있도록 허용한다. 이러한 특성은 특히 다중 처리기 시스템에서 이익을 얻을 수 있는데, 여러 스레드가 병렬로 실행가능하다.

예를 들어 다중 스레드 워드 프로세서는 하나의 스레드에 사용자 입력 관리를 맡기는 동안 다른 스레드에서 철자 검사기를 수행하도록 만들 수 있다.

스레드를 지원하는 시스템에서는 PCB는 각 스레드에 관한 정보를 포함하도록 확장된다.

프로세스 스케줄링

다중 프로그래밍의 목적은 CPU 이용을 최대화하기 위하여 항상 어떤 프로세스가 실행되도록 하는데 있다. 시분할의 목적은 각 프로그램이 실행되는 동안 사용자가 상호작용할 수 있도록 프로세스들 사이에서 CPU 코어를 빈번하게 교체하는 것이다.

이 목적을 달성하기 위해 프로세스 스케줄러는 코어에서 실행 가능한 여러 프로세스 중에서 하나의 프로세스를 선택한다. 각 CPU 코어는 한 번에 하나의 프로세스를 실행할 수 있다. 단일 CPU 코어가 있는 시스템의 경우 한 번에 2개 이상의 프로세스가 실행될 수 없지만 다중 코어 시스템은 한 번에 여러 프로세스를 실행할 수 있다. 코어보다 많은 프로세스가 있는 경우 초과 프로세스는 코어가 사용이 가능해지고 다시 스케줄 될 때까지 기다려야 한다.

현재 메모리에 있는 프로세스 수를 다중 프로그래밍 정도라고 한다. 다중 프로그래밍 및 시간 공유의 목표를 균형 있게 유지하려면 프로세스의 일반적인 동작을 고려해야 한다. 일반적으로 대부분의 프로세스는 I/O 바운드 또는 CPU 바운드로 설명할 수 있다. I/O 바운드 프로세스는 계산에 소비하는 것보다 I/O에 더 많은 시간을 소비하는 프로세스이다. 반대로 CPU 바운드 프로세스는 계산에 더 많은 시간을 사용하여 I/O 요청을 자주 생성하지 않는다.

스케줄링 큐

프로세스가 시스템에 들어가면 준비 큐에 들어가서 준비 상태가 되어 CPU 코어에서 실행되기를 기다린다. 이 큐는 일반적으로 연결 리스트로 저장된다. 준비 큐 헤더에는 리스트의 첫 번째 PCB에 대한 포인터가 저장되고 각 PCB에는 준비 큐의 다음 PCB를 가리키는 포인터 필드가 포함된다.

시스템에는 다른 큐도 존재한다. 프로세스에 CPU 코어가 할당되면 프로세스는 잠시 동안 실행되어 결국 종료되거나 인터럽트 되거나 I/O 요청의 완료와 같은 특정 이벤트가 발생할 때까지 기다린다. 프로세스가 디스크와 같은 장치에 I/O 요청을 한다고 가정하자. 장치는 프로세서보다 상당히 느리게 실행되므로 프로세스는 I/O가 사용 가능할 때까지 기다려야 한다. I/O 완료와 같은 특정 이벤트가 발생하기를 기다리는 프로세스는 대기 큐에 삽입된다.

프로세스 스케줄링의 일반적인 표현은 아래와 같은 큐잉 다이어그램이다. 준비 큐와 대기 큐 집합의 두 가지 유형의 큐가 제시되어 있다. 원은 큐에 서비스를 제공하는 자원을 나타내고 화살표는 시스템의 프로세스의 흐름을 나타낸다.

  • 프로세스가 I/O 요청을 공표한 다음 I/O 대기 큐에 놓일 수 있다.
  • 프로세스는 새 자식 프로세스를 만든 다음 자식의 종료를 기다리는 동안 대기 큐에 놓일 수 있다.
  • 인터럽트 또는 타임 슬라이스가 만료되어 프로세스가 코어에서 강제로 제거되어 준비 큐로 들어갈 수도 있다.

스케줄링

프로세스는 수명 주기 동안 준비 큐와 대기 큐를 이주한다. CPU 스케줄러의 역할은 준비 큐에 있는 프로세스 중에서 선택된 하나의 프로세스에 CPU 코어를 할당하는 것이다.

일부 운영체제는 스와핑으로 알려진 중간 형태의 스케줄링 알고리즘을 가지고 있는데, 핵심 아이디어는 때로는 메모리에서(및 CPU에 대한 능동적 경쟁에서) 프로세스를 제거하여 다중 프로그래밍 정도를 감소시키는 것이 유리할 수 있다는 것이다. 나중에 프로세스를 메모리에 다시 적재될 수 있으며 중단된 위치에서 실행을 계속할 수 있다. 프로세스를 메모리에서 디스크로 "스왑 아웃"하고 현재 상태를 저장하고, 이후 디스크에서 메모리로 "스왑 인"하여 상태를 복원할 수 있기 때문에 이 기법을 스와핑이라고 한다. 스와핑은 일반적으로 메모리가 초과 사용되어 가용공간을 확보해야 할 때만 필요하다.

문맥 교환

CPU 코어를 다른 프로세스로 교환하려면 이전의 프로세스의 상태를 보관하고 새로운 프로세스의 보관된 상태를 복구하는 작업이 필요하다. 이 작업을 문맥 교환(context switch)이라고 한다. 문맥 교환이 일어나면, 커널은 과거 프로세스의 문맥을 PCB에 저장하고, 실행이 스케줄된 새로운 프로세스의 저장된 문맥을 복구한다.

문맥 교환이 진행될 동안 시스템이 아무런 유용한 일을 못하기 대문에 문맥 교환 시간은 순수한 오버헤드다. 교환 속도는 메모리의 속도, 반드시 복사되어야 하는 레지스터의 수, 특수 명령어(모든 레지스터를 하나의 명령어로 보관하고 적재하는 것과 같은)의 존재에 좌우되므로, 기계마다 다르다.

문맥교환의 시간은 하드웨어의 지원에 크게 좌우된다. 예를 들어, 일부 처리기들은 여러 개의 레지스터 집합을 제공한다. 문맥 교환은 단순히 현행 레지스터 집합에 대한 포인터를 변경하는 것을 포함한다.

운영체제가 복잡할수록, 문맥 교환 시 해야 할 작업의 양이 더 많아진다. 복잡한 고급 메모리 관리 기법을 사용하면 문맥 교환 시 더 많은 자료를 교환해야 한다. 예를 들어, 문맥 교환 시 현재 프로세스의 주소 공간은 다음 태스크의 공간이 사용 준비되는 동안 반드시 보존되어야 한다. 주소 공간이 어떤 식으로 보존되고, 보존하기 위해 수행해야 할 작업의 양은 운영체제의 메모리 관리 기법에 따라 달라진다.

프로세스에 대한 연산

대부분 시스템 내의 프로세스들은 병행 실행될 수 있으며, 반드시 동적으로 생성되고, 제거되어야 한다. 그러므로 운영체제는 프로세스 생성 및 종료를 위한 기법을 제공해야 한다.

프로세스 생성

실행되는 동안 프로세스는 여러 개의 새로운 프로세스들을 생성할 수 있다. 생성하는 프로세스를 부모 프로세스라고 부르고, 새로운 프로세스는 자식 프로세스라고 부른다. 이 새로운 프로세스들은 각각 다시 다른 프로세스들을 생성할 수 있으며, 그 결과 트리를 형성한다.

UNIX, Linux 및 Windows와 같은 대부분의 현대 운영체제들은 유일한 프로세스 식별자(pid)를 사용하여 프로세스를 구분하는데 이 식별자는 보통 정수이다. pid는 시스템의 각 프로세스에 고유한 값을 가지도록 할당된다. 이 식별자를 통하여 커널이 유지하고 있는 프로세스의 다양한 속성에 접근하기 위한 index로 사용된다.

일반적으로 프로세스가 자식 프로세스를 생성할 때, 그 자식 프로세스는 자신의 임무를 달성하기 위해 어떤 자원(CPU 시간, 메모리, 파일, 입출력 장치)이 필요하다. 자식 프로세스는 이 자원을 운영체제로부터 직접 얻거나, 부모 프로세스가 가진 자원의 일부분 집합만을 사용하도록 제한될 수 있다. 부모 프로세스는 자원을 분할하여 자식 프로세스에게 나누어 주거나 메모리나 파일과 같은 몇몇 자원들은 자식 프로세스들이 같이 사용하게 할 수도 있다. 부모 프로세스 자원의 일부분만을 사용하도록 자식 프로세스가 쓸 수 있게 제한하며, 자식 프로세스들을 많이 생성하여 시스템을 과부하 상태로 만드는 프로세스를 방지할 수 있다.

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

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

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

  1. 자식 프로세스는 부모 프로세스의 복사본이다.
  2. 자식 프로세스가 자신에게 적재될 새로운 프로그램을 가지고 있다.

새로운 프로세스는 fork() 시스템 콜로 생성된다. 새로운 프로세스는 원래 프로세스의 주소 공간의 복사본으로 구성된다. 이 기법은 부모 프로세스가 쉽게 자식 프로세스와 통신할 수 있게 한다. 두 프로세스들은 fork() 후의 명령어에서부터 실행을 계속하며, 이때 한가지 다른 점은 fork()의 복귀 코드가 서로 다르다는 것이다. 자식 프로세스(0이 아닌)의 식별자가 부모로 복귀되는 데 반해, 새로운 프로세스는 ‘0’이 복귀된다.

fork() 시스템 콜 다음에 두 프로세스 중 한 프로세스가 exec() 시스템 콜을 사용하여 자신의 메모리 공간을 새로운프로그램으로 교체한다. exec() 시스템 콜은 이진 파일을 메모리로 적재하고 그 프로그램의 실행을 시작한다. 이와 같은 방법으로 두 프로세스는 통신을 할 수 있으며, 또는 자식이 실행하는 동안 할 일이 없으면, 자식이 종료도리 때까지 준비 큐에서 자신을 제거하기 위해 wait() 시스템 콜을 한다. exec()을 호출하면 프로세스의 주소 공간을 새 프로그램으로 덮어쓰기 때문에 exec() 시스템 콜은 오류가 발생하지 않는 한 제어를 반환하지 않는다.

프로세스 종료

프로세스가 마지막 문장의 실행을 끝내고 exit 시스템 콜을 사용하여 운영체제에 자신의 삭제를 요청하면 종료된다. 이 시점에서 프로세스는 자신을 기다리고 있는 부모 프로세스에(wait 시스템 콜을 통해) 상태 값(통상 정수 값)을 반환할 수 있다. 물리 메모리와 가상메모리, 열린 파일, 입출력 버퍼를 포함한 프로세스의 모든 자원이 할당 해제되고 운영체제로 반납된다.

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

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

프로세스가 종료하면 사용하던 자원은 운영체제가 되찾아 간다. 그러나 프로세스의 종료 상태가 저장되는 프로세스 테이블의 해당 항목은 부모 프로세스가 wait()를 호출할 때까지 남아 있게 된다. 종료되었지만 부모 프로세스가 아직 wait()를 호출하지 않은 프로세스를 좀비(zombie) 프로세스라고 한다. 종료하게 되면 모든 프로세스는 좀비 상태가 되지만 아주 짧은 시간 동안만 머무른다. 부모가 wait()을 호출하게 되면 좀비 프로세스의 프로세스 식별자와 프로세스 테이블의 해당 항목이 운영체제에게 반환된다.

부모 프로세스가 wait()를 호출하는 대신 종료하면 자식 프로세스는 고아(orphan) 프로세스가 된다. 전통적인 UNIX는 고아 프로세스의 새로운 부모 프로세스로 init 프로세스를 지정함으로써 문제를 해결한다. init 프로세스는 주기적으로 wait()를 호출하여 고아 프로세스의 종료 상태를 수집하고 프로세스 식별자와 프레스 테이블 항목을 반환한다.

IPC

운영체제 내에서 실행되는 병행 프로세스들은 독립적이거나 또는 협력적인 프로세스들일 수 있다. 프로세스가 시스템에서 실행 중인 다른 프로세스들과 데이터를 공유하지 않는 프로세스는 독립적이다. 프로세스가 시스템에서 실행 중인 다른 프로세스들에 영향을 주거나 받는다면 이는 협력적인 프로세스들이다.

  • 정보 공유(information sharing): 여러 응용 프로그램이 동일한 정보(예를 들면, 복사와 붙여넣기)에 흥미를 느낄 수 있으므로, 그러한 정보를 병행적으로 접근할 수 있는 환경을 제공해야 한다.
  • 계산 가속화(computation speedup): 특정 태스크를 서브 태스크로 나누어 이들 각각이 다른 서브태스크들과 병렬로 실행되게 해야한다.
  • 모듈성(modularity)

협력적 프로세스들은 데이터를 교환할 수 있는, 즉 서로 데이터를 보내거나 받을 수 있는 IPC기법이 필요하다. 프로세스 간 통신에는 기본적으로 공유 메모리(shared memory)와 메시지 전달(message passing)의 두가지 모델이 있다.

공유 메모리 모델에서는 협력 프로세스들에 의해 공유되는 메모리의 영역이 구축도니다. 프로세스들은 그 영역에 데이터를 쓰고 읽고 함으로써 정보를 교환한다.

메시지 전달 모델에서는 통신이 협력 프로세스들 사이에 교환되는 메시지를 통하여 이루어진다.

언급된 두 모델은 운영체제에서는 통상적이며 많은 시스템이 두 가지를 모두 구현한다. 메시지 전달 모델은 충돌을 회피할 필요가 없기 때문에 적은 양의 데이터를 교환하는데 유용하다. 메시지 전달은 또한 분산 시스템에서 공유 메모리보다 구현하기 쉽다. 메시지 전달 시스템은 통상 시스템 콜을 사용하여 구현되므로 커널 간섭 등의 부가적인 시간 소비 작업이 필요하기 때문에 공유 메모리 모델이 메시지 전달보다 빠르다. 공유 메모리 시스템에서는 공유 메모리 영역을 구축할 때만 시스템 콜이 필요하다. 공유 메모리 영역이 구축되면 모든 접근은 일반적인 메모리 접근으로 취급되어 커널의 도움이 필요없다.

다중 프로세스 구조 - Chrome 브라우저

현대의 브라우저들은 탭 형식의 브라우징을 지원하는데 웹 브라우저의 한 인스턴스가 탭마다 하나씩 동시에 여러 웹사이트를 열 수 있게 한다. 서로 다른 사이트로 전환하기 위해서는 사용자는 단순히 적절한 탭을 클릭하기만 하면 된다.

이 방식의 문제점은 탭 중 하나의 웹 응용프로그램이라도 갑자기 고장나면 다른 웹 사이트를 보이는 다른 탭을 포함한 전체 프로세스가 고장난다는 것이다.

Google의 Chorme 웹 브라우저는 다중 프로세스 구조를 활용하여 이 문제를 해결하게끔 설계되었다. Chorme은 브라우저, 렌더러, 플러그인 세 유형의 프로세스를 구분한다.

  • 브라우저 프로세스는 사용자 인터페이스와 함께 디스크와 네트워크 입출력을 관리하는 책임을 진다. Chrome이 시작될 때 새로운 브라우저 프로세스가 생성된다. 오직 하나의 브라우저 프로세스만이 생성된다.
  • 렌더러 프로세스는 웹 페이지를 표시하기 위한 프로그램 논리를 포함한다. 따라서 HTML, JS, 이미지등을 처리하기 위한 프로그램 로직을 포함한다. 일반적으로 새 탭에서 열린 웹 사이트마다 새로운 렌더러 프로세스가 생성되고 여러 렌더러 프로세스가 동시에 활동하게 된다.
  • 플러그인 프로세스는 Flash 또는 QuickTime과 같이 사용 중인 플러그인 종류마다 생성된다. 플러그인 프로세스는 플러그인을 위한 코드뿐 아니라 플러그인이 연관된 렌더러 프로세스와 브라우저 프로세스와 통신할 수 있게 하는 코드를 포함한다.

다중 프로세스 방식의 이점은 웹 사이트가 다른 웹사이트와는 고립되어 보인다는 것이다. 만일 한 웹사이트가 갑자기 고장나면 오직 해당 렌더러 프로세스만 영향을 받고 나머지 프로세스는 손해를 입지 않게 된다. 게대가 렌더러 프로세스는 샌드박스 안에서 실행되는데 이는 보안 취약점의 영향을 최소화하기 위해 디스크와 네트워크 입출력에 대한 접근이 제한된다는 것을 의미한다.

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

공유 메모리를 사용하는 프로세스 간 통신에서는 통신하는 프로세스들이 공유 메모리 영역을 구축해야한다. 통상 공유 메모리 영역은 공유 메모리 세그먼트를 생성하는 프로세스의 주소 공간에 위치한다. 이 공유 메모리 세그먼트를 이용하여 통신하고자하는 다른 프로세스들은 이 세그먼트를 자신의 주소 공간에 추가하여야 한다.

일반적으로 운영체제는 한 프로세스가 다른 프로세스의 메모리에 접근하는 것을 금지한다. 공유 메모리는 둘 이상의 프로세스가 이 제약 조건을 제거하는 것에 동의하는 것을 필요로 한다. 그런 후에 프로세스들은 공유 영역에 읽고 씀으로써 정보를 교환할 수 있다. 데이터의 형식과 위치는 이들 프로세스에 의해 결정되며 운영체제의 소관이 아니다. 또한 프로세스들은 동시에 동일한 위치에 쓰지 않도록 책임져야 한다.

생산자 소비자 문제를 생각해보자. 생산자 프로세스는 정보를 생산하고 소비자 프로세스는 정보를 소비한다. 예를 들어 컴파일러는 어셈블리 코드를 생산하고, 어셈블러는 이것을 소비한다. 어셈블러는 이어 목적 모듈(object module)을 생산할 수 있고, loader는 이들을 소비한다.

생산자 소비자 문제의 하나의 해결책은 공유 메모리를 사용하는 것이다. 생산자와 소비자 프로세스들이 병행으로 실행되도록 하려면, 생산자가 정보를 채워 넣고 소비자가 소모할 수 있는 항목들의 버퍼가 반드시 사용 가능해야 한다. 이 버퍼는 생산자와 소비자가 공유하는 메모리 영역에 존재하게 된다. 생산자가 한 항목을 생산하고, 그 동안에 소비자는 다른 항목을 소비할 수 있다. 생산자와 소비자가 반드시 동기화되어야 생산되지도 않은 항목들을 소비자가 소비하려고 시도하지도 않을 것이다.

두가지 유형의 버퍼가 사용된다. 부한 버퍼(unbounded buffer)의 생산자 소비자 문제에서는 버퍼의 크기에 실질적인 한계가 없다. 소비자는 새로운 항목을 기다려야만 할 수도 있지만, 생산자는 항상 새로운 항목을 생산할 수 있다. 유한 버퍼(bounded buffer)는 버퍼의 크기가 고정되어 있다고 가정한다. 이 경우 버퍼가 비어있으면 소비자는 반드시 대기해야 하며, 모든 버퍼가 채워져 있으면 생산자가 대기해야 한다.

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

메시지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신을 하고, 그들의 동작을 동기화할 수 있도록 허용하는 기법을 제공한다. 메시지 전달 방식은 통신하는 프로세스들이 네트워크에 의해 연결된 다른 컴퓨터들에 존재할 수 있는 분산 환경에서 특히 유용하다. 메시지 전달 시스템은 최소한 두 가지 연산을 제공한다.

  • send(mesage)
  • receive(message)

프로세스가 보낸 메시지는 고정 길이일 수도 있도 가변 길이일 수도 있다. 고정 길이 메시지만 보낼 수 있다면, 시스템 수준의 구현은 직선적이다. 그렇지만, 이러한 제한은 프로그래밍 작업을 더 힘들게 한다. 반면에 가변 길이 메시지는 보다 복잡한 시스템의 구현이 있어야 하지만 프로그래밍 작업은 더 간단해지낟. 이러한 일은 운영체제 전반에 걸쳐 흔히 볼 수 있는 교환이다.

만약 프로세스 P와 Q가 통신을 원하면 반드시 서로 메시지를 보내고 받아야 한다. 이들 사이에 통신 연결(communication link)가 설정되어야 한다. 이 연결은 다양한 방법으로 구현할 수 있다. 우리는 연결의 물리적인 구현에 관심이 있는 것이 아니라, 논리적인 구현에 관심이 있다. 하나의 링크와 send()/ receive() 연산을 논리적으로 구현하는 다수의 방법은 다음과 같다.

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

Naming

통신을 원하는 프로세스들은 서로를 가리킬 방법이 있어야 한다. 이들은 간접 통신 또는 직접 통신을 할 수 있다. 직접 통신에서 통신을 원하는 프로세스는 통신의 수신자 또는 송신자의 이름을 명시해야 한다.

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

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

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

이 기법은 주소 방식에서 대칭성을 보인다. 즉, 송신자와 수신자 프로세스 모두 통신하려면 상대방의 이름을 제시해야 한다. 이 기법의 변형으로서 주소 지정 시에 비대칭을 사용할 수도 있다. 송신자만 수신자의 이름을 지명하며, 수신자는 송신자의 이름을 제시할 필요가 없다.

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

이들 기법(대칭적 그리고 비대칭적) 모두 프로세스를 지정하는 방식 때문에 모듈성을 제한한다는 것이 단점이다. 프로세스의 이름을 바꾸면 모두 다른 프로세스 지정 부분을 검사할 필요가 있다. 옛 이름들에 대한 모든 참조를 반드시 찾아서 새로운 이름으로 변경해야 할 것이다. 이러한 하드 코딩(hard-coding)기법은, 이 상황에서는 신원을 명시적으로 표시해야 한다.

간접 통신에서 메시지들은 메일 박스(mailbox) 또는 포트(port)로 송신되고, 그것으로부터 수신된다. 메일박스는 추상적으로 프로세스들에 의해 메시지들이 넣어지고, 메시지들이 제거될 수 있는 객체라고도 볼 수 있다. 각 메일 박스는 고유의 id를 가진다. 예를 들어 POSIX 메시지 큐는 메일박스를 식별하기 위해서 정수 값을 사용한다. 이 기법에서 프로세스는 다수의 상이한 메일박스를 통해 다른 프로세스들과 통신할 수 있다. 두 프로세스들이 공유 메일박스를 가질 때만 이들 프로세스가 통신할수 있다.

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

이 방법에서 통신 연결은 다음 성질을 지닌다.

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

메일박스는 한 프로세스 또는 운영체제에 의해 소유될 수 있다. 새로운 메일박스를 생성하는 프로세스는 디폴트로 메일박스의 소유자가 된다. 초기에는 소유자만이 이 메일박스를 통해 메시지를 수신할 수 있는 유일한 프로세스이다. 그러나 소유권과 수신 특권은 적절한 시스템 콜을 통해 다른 프로세스에 전달될 수 있다. 물론 이런 규칙으로 인해 메일박스마다 복수의 수신자들을 낳을 수 있다.

동기화

프로세스 간의 통신은 send와 receive 프리미티브에 대한 호출에 의해 발생한다. 각 프리미티브를 구현하기 위한 서로 다른 설계 옵션이 있다. 메시지 전달은 blocking 이거나 non-blocking방식으로 전달된다. 이 두가지 방식은 동기식, 비동시식이라고도 알려져 있다.

  • blocking 보내기: 송신하는 프로세스는 메시지가 수신 프로세스 또는 메일 박스에 의해 수신될 때까지 봉쇄된다.
  • nonblocking 보내기: 송신하는 프로세스가 메시지를 보내고 작업을 재시작한다.
  • blocking 받기: 메시지가 이용 가능할 때까지 수신 프로세스가 blocking 된다.
  • nonblocking 받기: 송신하는 프로세스가 유효한 메시지 또는 널(null)을 받는다.

버퍼링

통신이 직접적이든 간접적이든 간에, 통신하는 프로세스들에 의해 교환되는 메시지는 임시 큐에 들어 있다. 기본적으로 이러한 큐를 구현하는 방식에는 세 가지가 있다.

  • zero capacity: 큐의 최대 길이가 0이다. 즉, 링크는 자체 안에 대기하는 메시지들을 가질 수 없다. 이 경우에, 송신자는 수신자가 메시지를 수신할 때까지 기다려야 한다.
  • bounded capacity: 큐의 유한한 길이 n을 가진다. 즉, 최대 n개의 메시지가 그 안에 들어갈 수 있다. 새로운 메시지가 전송될 때 큐가 만원이 아니라면, 메시지는 큐에 놓이며, 송신자는 대기하지 않고 실행을 계속한다. 그렇지만 링크는 유한한 용량을 가지고 있기 때문에 만원이면, 송신자는 큐 안에 공간이 이용 가능할 때까지 blocking 되어야 한다.
  • unbounded capacity: 큐는 잠재적으로 무한한 길이를 가진다. 따라서 메시지들이 얼마든지 큐 안에서 대기할 수 있다. 송신자는 절대 봉쇄되지 않는다.

zero capacity 같은 경우 때때로 버퍼가 없는 메시지 시스템이라고 부른다. 다른 경우들은 자동 버퍼링이라 불린다.