본문 바로가기

# Foundation/운영체제

운영체제 정리 🦖 ch03. 프로세스

프로세스

프로세스의 개념

비공식적으로 프로세스란 실행 중인 프로그램을 가르킵니다. 다음과 같이 텍스트 영역, 데이터 영역, 힙 영역, 스택 영역, 자유 공간이 할당된 프로그램 이상의 개념이며 프로그램 카운터를 갖는 능동적 개체입니다. (Job)은 프로세스의 별칭입니다.

 

프로세스의 상태

프로세스는 실행되면서 그 상태가 변화합니다. 각 프로세스는 다음 상태들 중 하나입니다.

  • 생성 (New) : 프로세스가 생성 중 입니다.
  • 실행 (Running) : 해당 프로세스의 명령어가 실행 중 입니다.
  • 대기 (Waiting) : 입출력의 완료 신호를 기다리고 있습니다.
  • 준비 (Ready) : 프로세스의 실행 순번이 오길 기다리고 있습니다.
  • 종료 (Terminated) : 프로세스가 종료되었습니다.

 

한 가지 시나리오를 통해 변화를 알아보겠습니다. 사용자가 프로그램을 실행하면 New 상태인 프로세스가 만들어집니다. 운영체제는 현재 가용할 수 있는 메모리를 확인한 뒤 해당 프로세스를 승인하면 Ready 상태로 변경됩니다.

 

다만 하나의 컴퓨터는 여러개의 프로세스를 실행하고 있겠죠? 운영체제는 Ready 상태의 프로세스 중 하나를 선택하여 실행합니다. 여기서 선택된 프로세스가 Running 상태로 변화합니다.

 

Running 상태의 프로세스는 여러가지 입출력을 요청할 수 있습니다. 사용자에게 키보드 입력을 받는다거나 파일을 읽는다거나 파일을 쓴다거나 말이죠. 프로세스는 입출력이 완료될 때 까지 기다려야합니다. 입력 또는 출력작업이 끝나지도 않았는데 다음 단계로 넘어가면 안되겠죠?

 

이런식으로 Running 상태의 프로세스가 입출력을 요청하면 Waiting 상태로 변화하며 입출력의 완료 신호가 감지될 때 까지 대기합니다. 입출력이 완료되면 Waiting 상태의 프로세스는 Ready 상태로 변화하며 다시 자기의 차례가 돌아오기를 기다립니다.

 

Running 상태 프로세스의 모든 작업이 끝나면 Terminated 상태로 변화하며 운영체제가 종료된 프로세스에 할당했던 메모리를 회수합니다.

 

프로세스 제어 블록

프로세스 제어 블록(Process Control Block, PCB)는 특정 프로세스에 관련된 정보들을 저장합니다.

 

여기에 포함된 정보 :

  • 프로세스 상태 : 해당 프로세스의 현재 상태를 가르킵니다.
  • 프로그램 카운터 : 해당 프로세스가 다음에 실행할 명령어의 주소를 가르킵니다.
  • CPU 레지스터 백업본 : 인터럽트에서 복귀했을 때 사용합니다.
  • CPU 스케쥴링 정보 : 스케쥴링에서 사용되는 매개변수의 값을 저장합니다.
  • 메모리 관리 정보 : 해당 프로세스가 사용중인 메모리페이징 정보를 저장합니다.
  • 통계 정보 : 해당 프로세스가 사용한 CPU, 시간, 프로세스 식별자 등을 저장합니다.
  • 입출력 상태 정보 : 해당 프로세스가 소유한 입출력장치 또는 파일 목록을 가르킵니다.

 

프로세스 개념의 확장

위에서 설명했던 프로세스 모델은 하나의 프로세스가 하나의 쓰레드를 갖는다고 가정했습니다. 해당 모델의 문제점은 두 작업을 병렬로 할 수 없다라는 것 이겠죠. 예를 들어 하나의 파일을 읽는 작업이 2개 있다고 했을 때, 두 작업 중 하나만 실행될 수 있고. 먼저 실행된 작업이 프로세스를 Waiting 상태로 변화시키므로 2개의 파일을 동시에 읽을 수 없습니다.

 

위의 이슈를 해결하기 위해 현대 운영체제는 쓰레드(Thread) 개념을 사용하여 프로세스를 더욱 확장합니다. 하나의 프로세스를 여러개의 쓰레드로 나누고 여러개의 대기 프로세스 중 하나를 선택하는 것이 아니라 여러개의 대기 쓰레드 중 하나를 선택하도록 확장하는 것 이죠.

 

스케줄링

장기 스케줄러

현대 시스템은 가용 CPU 코어 수보다 실행해야 할 프로세스 수가 훨씬 많습니다. 따라서 즉시 실행할 수 없는 프로그램도 생기겠죠. 이처럼 실행이 요청되었지만 즉시 자원을 할당받지 못한 프로세스는 실행대기 큐에 옮겨지며 나중에 운영체제에게 선택될 때 까지 대기합니다.

 

새롭게 실행할 프로그램을 선택하는 것은 매우 신중해야 합니다. CPU 중심의 프로그램I/O 중심의 프로그램의 개수를 균형있게 조절해야 최선의 성능을 이끌어낼 수 있기 때문이죠. 따라서 장기 스케줄러는 오랜시간을 심사숙고하여 실행대기 큐에서 새롭게 실행할 프로그램 하나를 선택합니다. 마침 실행요청은 그렇게 자주 일어나는 일이 아니므로, 더 오랜시간 생각해도 괜찮습니다. 선택된 프로그램은 프로세스로 지위가 올라가며 준비완료 큐로 이동됩니다.

 

다만 유닉스 또는 윈도우같은 시분할 시스템장기 스케줄러가 없는 경우도 있습니다.

 

단기 스케줄러

같은 맥락으로 가용 CPU 코어 수보다 준비완료된 프로세스 수가 훨씬 많습니다. 운영체제가 자원을 할당했어도 아직 자기 차례가 되지 않은 프로세스는 준비완료 큐에 옮겨지며 자신이 선택 될 까지 대기합니다.

 

하나의 프로세스가 오랜시간동안 CPU를 독점하면 안되기 때문에 단기 스케줄러는 매우 빈번하게 준비완료 큐에서 다음에 실행할 프로세스를 선택해야 합니다. 다음에 실행할 프로세스를 선택하면 인터럽트를 보내 현재 실행중인 프로세스를 일시정지시키고 선택된 프로세스를 실행시킵니다.

 

중기 스케줄러

시분할 시스템과 같은 일부 시스템은 중간 수준의 스케줄링을 도입한 경우도 있습니다. 현대의 시스템은 준비완료 큐에 들어있는 프로세스가 너무 많아지는 바람에 단기 스케줄러에서 하나를 선택하는 작업이 무거워졌다는 것 이죠.

 

중기 스케줄러준비완료 큐에서 일부 프로세스들을 꺼내서 다중 프로그래밍 정도를 낮추는 역할을 합니다. 이러면 단기 스케쥴러의 부담이 줄어들고 전체 성능의 향상으로 이어지겠죠.

 

준비완료 큐에서 꺼내질 때에는 할당된 메모리도 같이 꺼내지는데, 이후에 다시 준비완료 큐로 돌아갈 때 할당된 메모리의 백업본을 활용하여 이전 상태로 복구됩니다.

 

문맥 교환

단기 스케쥴러에 의해 현재 실행중인 프로세스가 바뀌는 상황은 매우 흔합니다. 하지만 실행중인 프로세스가 바뀌었다고 이전 정보가 손실되면 안되겠죠. 프로세스가 교체될 당시의 CPU 레지스터 값을 포함한 다양한 정보들을 문맥이라고 하며 PCB에 저장되어 있습니다. 프로세스가 교체될 때 마다 해당 프로세스의 문맥을 복구하는 작업이 요구되며, 이것을 문맥 교환이라고 부릅니다.

 

문맥 교환도 당연히 오버헤드가 존재하는 작업이며 PCB에 저장된 정보가 많아질수록 문맥 교환의 비용도 증가합니다. 즉, 프로세스를 관리하기 위한 정보들이 많아질수록 문맥 교환의 비용도 증가합니다.

 

프로세스 연산

프로세스 생성

PID

새롭게 프로세스가 생성되면 운영체제는 고유한 프로세스 식별자 (PID)를 할당합니다. PID를 통해 각각의 프로세스를 구분할 수 있죠. 특별한 프로세스는 PID가 고정되어 있습니다. 예를 들어 솔라리스에서 첫 번째로 실행되는 sched 프로세스는 항상 PID가 0으로 고정되어 있습니다.

 

트리 구조

대부분의 운영체제에서 프로세스는 트리 형태로 관리됩니다. 즉, 부모 프로세스에 의해 자식 프로세스가 생성되는 형태이죠. 보통 부모 프로세스가 필요한 기능을 자식 프로세스에게 시키기 위해 사용되기 때문에, 이를 위해 운영체제는 자식 프로세스의 종료를 알리는 기능 또는 부모 자식간의 통신 파이프를 생성하는 기능을 함께 제공해야 합니다.

 

자식 프로세스부모 프로세스가 소멸할 때 같이 소멸되도록 운영체제가 관리하지만, 매우 가끔 자식 프로세스만 살아남는 경우가 있습니다. 이렇게 부모 프로세스가 사라졌음에도 실행되고 있는 프로세스를 좀비 프로세스라고 부릅니다.

 

프로세스 복제

부모 프로세스는 다양한 프로그램을 자식 프로세스로 가질 수 있지만 자기 자신을 복제하는 경우도 꽤 있습니다. 이러한 기능은 fork()를 호출하여 사용할 수 있으며, 부모 프로세스에게는 복제된 자식 프로세스의 PID를 반환하고 자식 프로세스에게는 0을 반환합니다.

 

부모 프로세스의 모든것이 복제되었으므로 당연히 프로그램 카운터도 복제됩니다. 즉, 부모 자식이 모두 fork()가 실행된 다음 라인부터 실행됩니다.

unsigned int pid = fork();

if( pid != 0 ) {
    // 부모 프로세스
}
else {
    // 자식 프로세스
}

 

프로세스 종료

종료 코드

exit()가 명시적 또는 암묵적으로 실행되면 운영체제는 해당 프로세스에 할당되었던 자원을 회수합니다. 이 때, 프로세스는 자신이 정상적으로 종료되었는지 알리기 위해 종료 코드를 함께 반환할 수 있습니다. 정상종료는 오직 0이며 다른 값은 비정상종료를 나타냅니다. 부모 프로세스가 있는 경우 자식 프로세스의 종료코드를 수신할 수 있습니다.

int main(){
    cout << "Hello, World!";

    //
    // 정상종료 값인 0을 반환하면서 종료.
    // 이것은 exit(0); 과 같습니다.
    return 0; 
}

 

신원 확인

어떤 시스템 콜은 PID를 사용하여 특정 프로세스를 강제로 종료시킬 수 있습니다. 다만, 무조건적으로 종료시키는 것은 아니고 운영체제가 프로세스 트리를 살펴서 적절한 권한이 있는 경우에만 허락됩니다. 다른 사용자의 프로세스를 악의적으로 종료시키는 것을 방지하기 위함입니다.

 

프로세스간 통신

같은 호스트

통칭 IPC (Inter-Process Communication)로 불리며, 서로다른 두 개의 프로세스가 메세지를 주고받는 방식을 의미합니다. 여기서는 같은 컴퓨터에 있는 프로세스간 통신을 설명합니다. 다른 컴퓨터에 있는 프로세스간 통신네트워크 통신이기 때문입니다.

 

IPC 모델의 종류는 다음이 있으며, 일반적으로 현대의 운영체제들은 이 중에서 하나만 지원하는 것이 아니라 모든 모델을 지원합니다.

 

IPC 모델 종류 :

  • 공유 메모리 모델 : 공유 메모리 영역을 생성하고, 여기에 문자열을 쓰고 읽어서 통신.
  • 메세지 전달 모델 : 모든 프로세스가 사용가능한 공용 채널을 통해 통신.
  • 파이프 모델 : 두 프로세스만 사용가능한 전체 채널을 통해 통신.

 

공유 메모리 모델

기본적으로 프로세스는 서로의 영역에 침범할 수 없도록 운영체제가 제한하고 있습니다. 하지만 공유 메모리 모델은 이러한 제한조건을 깨는 것을 운영체제와 합의하고 각각 자신의 PCB에 상대방 세그먼트 주소를 기록합니다. 이후로 두 프로세스는 공유 세그먼트를 통해 같은 데이터를 공유할 수 있지만 공유자원 문제도 같이 따라오기에 조심해서 사용해야 합니다.

 

메세지 전달 모델

운영체제에서 지원하는 공용 채널을 사용하여 메세지를 주고받습니다. 우편배달을 생각하면 쉽습니다. 편지를 작성할 때와 똑같이 보내는 사람받는 사람이 필요하며, 이것은 보내는 프로세스의 PID 받는 프로세스의 PID에 대응됩니다.

 

현실에서도 그렇듯이 편지가 중간에 사라지는 경우가 있음을 인지해야합니다. 이러한 현상의 허용여부에 따라 봉쇄형 비봉쇄형으로 나뉩니다. 이것을 보통 동기화라고 부릅니다.

  • 봉쇄형 송/수신 : 메세지가 성공적으로 전송될 때 까지 메세지를 반복적으로 전달.
  • 비봉쇄형 송/수신 : 1번만 메세지를 보내고 성공여부만 반환.

 

동기화 옵션송신/수신에 서로 다르게 적용될 수 있습니다.

  • 봉쇄형 송신 : 소비자가 메세지를 받을 때 까지 대기
  • 봉쇄형 수신 : 송신자가 메세지를 보낼 때 까지 대기
  • 비봉쇄형 송신 : 메세지를 보내고 자기는 하던일을 다시 시작
  • 비봉쇄형 수신 : 수신된 메세지가 없다면 null로 해석하고 하던일을 다시 시작

 

당연하지만 해당 모델은 공유자원 문제가 발생하지 않습니다.

 

파이프 모델

컴퓨터 공학에서는 제약이 강할수록 예외사항이 줄어들기 때문에 인터페이스가 간단해지죠. 파이프 모델은 가장 까다로운 제약 조건을 가졌지만 사용법이 간단합니다. 두 프로세스를 직접적으로 잇는 채널을 파일로 제공합니다. 두 프로세스는 해당 파일에 데이터를 적거나 읽어서 메세지를 교환합니다.

 

공유 메모리 모델과 다른 점은 파일처럼 보이지만 사실은 메세지 채널이라는 것입니다. 따라서 파일보다는 파이프로 해석하는 것이 정확합니다. 구체적으로는 자신이 파이프에 적은 내용은 파이프를 통해 자신도 다시 읽지 못하고, 파이프에서 읽은 데이터는 영구히 사라집니다.

 

이것은 파이프방향성이 있다는 것을 의미하며, 다음 중 하나의 방향성을 가질 수 있습니다.

  • 단방향 파이프
  • 양방향 파이프
    • 반이중 파이프
    • 전이중 파이프

 

또 하나 중요한 것은 파이프 이름의 여부입니다. 파이프가 생성될 시점에 이름을 붙일 수 있으며, 상대방 프로세스가 해당 파이프의 이름을 사용하여 해당 파이프에 접속할 수 있습니다. 다만 파이프의 이름이 없어도 파이프에 접속할 수 있는데 이것을 익명 파이프라고 하며 복제된 부모-자식 관계에서만 사용할 수 있습니다.

 

다른 호스트

다른 운영체제에서 실행중인 프로세스와 통신하는 것을 네트워크 통신이라고 합니다. 보통 운영체제는 다음과 같은 기법들을 제공합니다.

 

소켓 통신

소켓은 통신의 단말점(endpoint)를 의미하며 두 소켓이 연결되어 하나의 채널이 됩니다. 소켓 통신을 하기 위해서는 상대방의 네트워크 주소포트 번호를 알고 있어야 하며 소켓 통신에 사용된 전송 계층 프로토콜에 따라 이름이 나뉘어집니다. ex) TCP 소켓 또는 UDP 소켓으로 나뉘어집니다.

 

파이프 통신

파이프의 중간 계층에서 소켓통신이 적용될 수 있습니다.