3장 학습 목표

  • 프로세스 개념에 대해 학습
  • 프로세스 스케줄링 방식 학습
  • 프로세스 생성과 종료 연산
  • 프로세스 간 통신 방법 학습
  • IPC 시스템 학습
  • 클라이언트 서버 환경에서의 통신에 대해 학습

프로세스 개념 (Process Concept)

초기의 컴퓨터 시스템은 한 번에 하나의 프로그램만을 실행하도록 허용했었다. 하지만 시간이 흘러 오늘날 시스템들은 메모리에 다수의 프로그램이 적재되어 병행 실행 되는 것을 허용한다. 이러한 발전이 실행 중인 프로그램을 뜻하는 프로세스란 개념을 낳았다.

프로그램이 컴파일 된 코드와 같은 명령어 리스트들을 포함하는 정적인 개체라면, 프로세스는 메모리 구조를 이루어 현재 활동 상태를 나타내는 등의 동적인 정보를 가지는 동적인 개체인 점을 기억해두자. (프로그램 카운터 값을 통해 현재 어떤 명령어를 수행 중 인지, 프로세서 레지스터의 내용을 통해 현재 데이터를 가지고 어떤 계산을 진행 중 인지 표현함.) 대부분 시스템에서 프로세스는 작업의 단위가 된다.

다음은 프로세스의 일반적인 메모리 배치이다. 

 

  • Text section : 실행 코드
  • Data section : 전역 변수
  • Heap section : 프로그램 실행 중에 동적으로 할당되는 메모리
  • Stack section : 함수를 호출할 때 활성화 레코드가 저장되는 장소

Text, Data 섹션은 고정된 크기를 가지는 반면, Heap과 Stack 섹션은 크기가 유동적이다. stack과 heap 두 섹션이 서로의 방향으로 커질 때, OS가 두 영역이 겹치지 않게끔 조절해주어야 한다.

같은 프로세스가 실행되더라도 그 프로세스가 실행되는 방식에 따라 가지는 메모리가 달라질 수 있다. 즉 text 섹션이 동일하더라도 data,heap,stack 섹션의 값은 다를 수 있다는 것이다.

 

이러한 프로세스는 프로세스 제어 블록(Process Control Block, PCB)에 의해 표현된다. PCB는 다음과 같은 프로세스의 정보들을 수록한다.

  • 프로세스 실행 상태 : 프로세스는 실행되면서 다음과 같은 형태 중 하나의 상태로 변화한다. 

  • new : 프로세스 생성 상태
  • ready : 프로세스가 처리기에 할당되기를 기다리는 상태
  • running : 명령어들이 실행되고 있는 상태
  • waiting : 프로세스가 이벤트 발생을 기다리는 상태
  • terminated : 프로세스의 실행이 종료된 상태

 

  • 프로그램 카운터(명령어 포인터) : 프로세스가 다음에 실행할 명령어의 주소를 가리킨다. 명령어의 순차적인 실행을 제어하므로 명령어 포인터라고 부르기도 한다.
  • CPU 레지스터 : 여러 레지스터들과 (컴퓨터 구조에 따라 레지스터들의 집합이 달라짐) 상태 코드 정보가 포함된다. PC 값과 상태 정보는 인터럽트 처리 후 제어를 돌려받아 올바르게 실행되기 위해서 필수적으로 저장되어야 한다.
  • CPU 스케줄링 정보 : 프로세스 우선순위, 스케줄 큐에 대한 포인터와 다른 스케줄 매개변수를 포함
    • 스케줄 큐는 ready 상태의 프로세스가 있는 준비 큐, waiting 상태의 프로세스들이 있는 대기 큐, 실행이 완료된 프로세스들이 있는 종료 큐가 있다. 스케쥴 큐에 대한 포인터는 각 프로세스가 어느 큐에 들어가 있는지를 나타낸다.
    • 다른 스케줄 매개변수에는 대기 시간, 응답 시간, 시간 할당량 등의 정보가 포함된다.
  • 메모리 관리 정보 : 프로세스가 자신에게 할당된 메모리 영역 밖을 나가는 것을 방지하기 위한 기준 레지스터와 한계 레지스터 정보, 효율적인 메모리 사용을 위한 페이지 테이블 또는 세그먼트 테이블 등과 같은 정보를 포함한다.
  • 회계 정보 : CPU 사용 시간과 경과 시간, 시간 제한, 프로세스 번호 등이 포함된다.
  • 입출력 상태 정보 : 프로세스에 할당된 입출력 장치들과 열린 파일의 목록 등을 포함

단일 스레드 시스템에서의 PCB 구조는 위와 같지만, 다중 스레드 시스템에서의 PCB는 병행 실행되는 스레드와 관련된 정보들도 가지게끔 구조가 변하게 된다. 이에 대한 자세한 내용은 4장을 정리한 포스팅에서 다룰 예정이다.

 


프로세스 스케줄링 (Process Scheduling)

다중 프로그래밍의 목적은 CPU 이용을 최대화하기 위해 항상 어떤 프로세스가 실행 상태에 놓이게끔 하는것에 있다. 해당 목적의 달성을 위해 존재하는 개념이 프로세스 스케줄링이다. 프로세스 스케줄러는 실행 가능한 즉, 준비큐에 존재하는 여러 프로세스 중에서 하나의 프로세스를 선택하여 CPU를 할당한다. (단일 코어에선 하나의 프로세스를, 다중 코어에서는 2개 이상의 프로세스를 선택)

스케줄링 큐(Scheduling Queue)

 

스케줄링 큐는 일반적으로 연결 리스트의 형태로 구성되는데 head는 다음 PCB를 가르키는 포인터 필드가, tail에는 마지막 PCB를 가르키는 포인터 필드가 포함된다. 

프로세스가 시스템에 들어가면 일단 준비 큐에 들어가서 ready 상태가 되어 CPU 코어에서 실행되기를 기다린다. 실행 도중에 CPU가 다른 프로세스에 할당되게 되면 해당 프로세스가 종료되거나 인터럽트 등의 발생으로 인해 다시 CPU를 돌려주기 전까지 대기 큐에 삽입된다. 이런 일련의 과정을 일반적으로 그려낸 것이 큐잉 다이어그램이다.

큐잉 다이어그램에서 원은 큐에 서비스를 제공하는 자원을 나타내고 화살표는 프로세스의 흐름을 나타낸다. 준비큐에 들어가 있는 프로세스는 디스패치 되어 실행이 시작되며 여러 이벤트를 만나 대기 큐에 들어가거나 종료가 되게된다.

CPU 스케줄링

위에서도 말했듯이 CPU 스케줄러의 역할은 준비 큐에 위치한 프로세스들에게 CPU를 할당해주는 것이다. CPU를 할당해주기 위해서 고려해주어야 할 요소가 여러개 있는데 그중 하나가 다중 프로그래밍 정도(메모리에 적재되어 있는 프로세스의 수)와 그 종류이다. 메모리를 차지하고 있는 프로세스의 동작에 따라 I/O 바운드 프로세스 CPU 바운드 프로세스로 나뉘며 이 둘은 CPU가 할당되는 방식과 시간이 달라지기 때문에 필수적으로 고려해야할 요소가 된다.

  • I/O 바운드 프로세스 : 계산에 소비하는 것보다 입출력에 더 많은 시간을 소비하는 프로세스( I/O burst > CPU burst)
  • CPU 바운드 프로세스 : 계산에 더 많은 시간을 소비하는 프로세스(CPU burst > I/O burst)
  • CPU burst : 프로세스가 실행되는 동안 CPU 연산에 소요되는 시간
  • I/O burst : 프로세스가 실행되는 동안 입출력 장치가 작업하는 시간

I/O 바운드 프로세스는 입출력 장치에게 작업을 요청하기 전에 아주 잠시 동안만 CPU를 사용하는 반면, CPU 바운드 프로세스는 연산을 위해 지속적으로 CPU를 사용해야 한다. 하지만 스케줄러는 단일 프로세스에게 오랜 시간동안 코어를 할당해 줄 수 없으므로 굉장히 짧은 시간동안 자주 할당해주는 방식을 사용하곤 한다. 

하지만 다중 프로그래밍 정도가 심한 경우 위의 방식을 적용하게 되면 프로세스의 대기 시간이 점점 늘어나게 되는 문제가 있다. 이를 위해 일부 운영체제는 메모리에서 프로세스를 제거하여 다중 프로그래밍 정도를 감소시키는 방식인 스와핑 기법을 사용하곤 한다. 스와핑 기법은 프로세스를 메모리에서 디스크로 스왑아웃하고, 이후 필요해지면 다시 메모리로 스왑인하여 상태를 복원시켜가며 프로세스의 수를 조절하는 기법이다. 보통 메모리가 초과 사용되어 가용 공간을 확보해야 할 때 사용하곤 한다.

문맥 교환(Context Switch)

CPU 코어를 다른 프로세스로 할당하려면 현재의 프로세스 상태를 보관하고, 교환하고자하는 프로세스의 상태를 복구하는 작업인 문맥 교환이 필요하다.(인터럽트 발생시, 현 상태 저장 및 복구 개념과 유사함) 문맥 교환이  일어나면 커널은 과거 프로세스의 문맥을 PCB에 저장하고 새로 실행할 프로세스의 문맥을 PCB에서 복구해와야 한다.

해당 과정이 발생하는 동안 시스템은 아무런 유용한 일을 하지 못하기 때문에 문맥 교환 시간은 순수한 오버헤드이다. 교환에 소요되는 시간은 메모리의 속도, 복사되어야 할 레지스터의 수, 하드웨어, 메모리 관리 기법 등 여러 요인에 의해 좌우된다.


프로세스 연산(Operation on Processes) - 생성과 종료 연산

프로세스 생성

실행되는 동안 프로세스는 여러 개의 프로세스들을 생성할 수 있다. 이때 근간이 되는 프로세스를 부모 프로세스라고 부르고, 새로이 생성된 프로세스를 자식 프로세스라고 부른다. 자식 프로세스들은 다시 자신들의 자식 프로세스를 생성할 수 있으며 해당 과정이 반복되어 트리 구조의 프로세스를 형성하게 된다.

해당 그림에선 systemd라는 프로세스가 최상단인 루트 부모 프로세스로써 존재한다. 운영체제마다 루트 부모 프로세스로 존재하는 프로세스는 달라지지만, 시스템이 부트될 때 생성되는 첫 사용자 프로세스인 점은 동일하다. 루트 프로세스는 생성된 이후 다양한 사용자 프로세스를 생성하게 된다. 

일반적으로 프로세스가 자식 프로세스를 생성할 때 자식 프로세스는 다음과 같은 제약 안에서 자원을 할당 받게 된다.

  • OS로부터 직접 자원을 할당 받음
  • 부모 프로세스의 자원 일부를 할당 받음
  • 부모의 자원을 분할하여 같이 사용

이렇게 생성된 자식 프로세스는 부모와 병행하게 실행을 계속하거나, 부모를 대기 시킨 후 홀로 실행될 수 있다. 자식 프로세스는 주소 공간 측면에서 다음과 같이 나뉜다.

  • 자식 프로세스는 부모 프로세스와 동일한 프로그램과 데이터를 가진다.
  • 자식 프로세스는 자신에게 적재될 새로운 프로그램을 가지고 있다.

위의 두 측면을 UNIX와 Windows의 프로세스 생성 시스템 콜을 통해 좀 더 알아보자

  • UNIX - fork(), 인자 없음
    • 프로세스 생성시, 새로운 프로세스는 기존 프로세스의 주소 공간 복사본으로 구성됨
    • fork 완료시 부모 프로세스는 자식 프로세스의 pid값을 반환 받으며, 자식 프로세스는 0을 반환 받음
    • 이후 exec() 시스템 콜을 통해 자신의 메모리 공간을 새로운 프로그램으로 완전히 교체한다.
    • exec()는 기존의 메모리 이미지를 완전히 파괴하고 새로 적재하는 것이기 때문에, 오류를 만나기 전까지는 이전 프로그램에게 CPU제어를 넘기지 않게 된다.
    • exec() 시스템 콜을 호출하지 않을 경우, 부모의 프로그램을 그대로 계속 실행하게 된다.
  • Windows - CreateProcess(), 매개변수 10개 필요
    • 부모의 주소 공간을 그대로 상속받는 fork() 시스템 콜과 달리 CreateProcess()는 주소 공간에 명시된 프로그램을 적재하여 실행할 프로그램을 자식 프로세스에게 부여한다.
    • 생성 완료시 자식의 pid값을 부모가 반환 받는것은 동일함.(반환 받지 않으면 이후 자식 프로세스를 Kill 하지 못함) 
BOOL CreateProcess(
  LPCSTR lpApplicationName,           // 실행할 애플리케이션의 경로 (선택 사항)
  LPSTR lpCommandLine,                // 실행할 명령어 라인 (필수)
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // 프로세스 보안 속성 (선택 사항)
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 스레드 보안 속성 (선택 사항)
  BOOL bInheritHandles,               // 부모 프로세스의 핸들 상속 여부
  DWORD dwCreationFlags,              // 프로세스 생성 옵션 (우선순위, 실행 상태 등)
  LPVOID lpEnvironment,               // 환경 변수 (선택 사항)
  LPCSTR lpCurrentDirectory,          // 프로세스의 작업 디렉터리 (선택 사항)
  LPSTARTUPINFO lpStartupInfo,        // 프로세스 시작 정보 (창 크기, 위치 등)
  LPPROCESS_INFORMATION lpProcessInformation // 프로세스 정보 (PID, 핸들 등)
);

 

 프로세스 종료

프로세스가 마지막 문장을 끝내고 exit 시스템 콜을 사용해 OS에 자신의 삭제를 요청하면 프로세스가 종료된다. 그 후 대기 상태에 있는 부모 프로세스에게 자신의 상태 값을 반환할 수 있다. 일련의 과정이 끝나면 프로세스의 모든 자원이 할당 해제되고 운영체제로 반납된다. 

프로세스가 스스로 자신의 작업을 종료하는 것 외에도, 부모 프로세스가 kill하는 방식으로도 프로세스를 종료시킬 수 있다. 다른 프로세스를 종료시키는 건 부모 프로세스 외에는 불가하다는 점을 유의하자. 부모는 다음과 같은 이유로 자식 프로세스를 중단시킬 수 있다.

  • 자식 프로세스가 할당된 자원 이상을 사용할 때
  • 자식 프로세스에게 할당된 테스크가 더 이상 필요없을 때
  • 부모가 exit했는데 자식이 활성화 된 상태인 것을 운영체제가 허락하지 않을 때
    • 위와 같은 이유로 인해 부모가 종료되고 관련된 모든 자식 프로세스가 종료되는 것을 연쇄식 종료라고 칭한다.

 

Linux와 UNIX 시스템에서는 프로세스 종료를 위해 exit() 시스템 콜을 사용중이다. 또한 부모 프로세스는 wait() 시스템 콜을 사용해서 자식이 종료되기를 기다릴 수 있다. 이때, 부모는 종료된 자식의 pid값을 통해 어느 프로세스가 종료된 것인지 식별한다.

프로세스가 종료되면 사용하던 자원을 운영체제가 모두 되찾아 간다.

하지만 프로세스의 종료상태가 저장되는 프로세스 테이블의 항목은 부모 프로세스가 wait()을 호출할 때 까지 남아 있게 된다. 이와 관련된 용어는 다음 두가지가 있다.

  • 좀비 프로세스 : 프로세스가 종료되었지만 부모 프로세스가 wait을 호출하지 않은 상태의 프로세스. 모든 프로세스는 종료하게 되면 아주 잠깐 동안 좀비 상태가 된다.
  • 고아 프로세스 : 부모가 wait을 호출하지 않고 그대로 종료된 프로세스. 대부분 init 프로세스가 일괄적인 처리를 통해 자원을 반환하지만 Linux의 경우 systemd(루트 프로세스) 이외의 프로세스가 자원 해제하는 것을 허용하곤 한다.

 

비교적 자원이 제한적인 모바일 운영체제에서는 자원 회수를 위해 기존 프로세스를 종료해야 한다. 이를 위해 Android는 프로세스의 중요도 계층을 식별하여 프로세스 종료 우선순위를 정해두었다. 

  • 전경 프로세스(forground process) : 사용자가 현재 상호 작용하고 있는 응용 프로그램. 즉 화면에 현재 보이는 프로세스
    • ex) 사용자가 현재 사용중인 웹 브라우저
  • 가시적 프로세스(visible process) : 전경에서 직접 볼 수 없지만 전경 프로세스가 참조하는 활동을 하는 프로세스
    • ex) 화면에서 사라진 다중 애플리케이션 창
  • 서비스 프로세스(service process) : 백그라운드 프로세스와 유사하지만 사용자가 인지하고 있는 활동을 하는 프로세스
    • ex) 음악 스트리밍 서비스
  • 백그라운드 프로세스(background process) : 실행 중이지만 사용자가 인식하지 못하는 프로세스
    • ex) 자동 백업 시스템
  • 빈 프로세스(empty process) : 응용 프로그램과 관련된 활성 구성 요소가 없는 프로세스
    • ex) 종료되었지만 잔여 프로세스가 남은 게임 앱

 


프로세스 간 통신(Interprocess Communication)

운영체제 내에서 실행되는 프로세스들은 통신 여부에 따라 독립적인 혹은 협력적인 프로세스로 나뉜다

  • 독립적인 프로세스 : 다른 프로세스와 데이터를 공유하지 않는 프로세스
  • 협력적인 프로세스 : 다른 프로세스와 데이터를 공유하거나 영향을 주는 프로세스

프로세스가 협력을 허용하는 환경을 제공하는 이유는 다음과 같다

  • 정보 공유(information sharing) : 여러 응용 프로그램이 동일한 정보를 요구할 수 있으므로 병행적인 접근 환경을 제공하기 위해.
  • 계산 가속화(computation speedup) : 서브 태스크들끼리 병렬로 실행되게 하여 실행 속도를 높이기 위해.
  • 모듈성(modularity) : 시스템 기능을 프로세스 혹은 스레드들로 나눠 모듈 형식으로 구성하기 위해.

 

이러한 프로세스 간의 통신을 위해서 사용되는 방식 중 가장 대표적인 것은 공유 메모리와 메시지 전달이다.

  • 공유 메모리 : 협력 프로세스들에 의해서 공유되는 메모리 영역이 구축됨으로써 정보가 공유된다. 커널 간섭등의 시간 소비가 없기 때문에 속도가 빠르지만 충돌 가능성이 높다.
  • 메시지 전달 : 협력 프로세스들 사이에서 메시지 전달에 의해서 정보가 공유된다. 충돌 가능성이 적으며 구현이 좀 더 쉽다. 하지만 매번 시스템 콜이 발생하기 때문에 속도가 느린편이다.