운영체제 요약 - 3. 프로세스

3.1. 프로세스 개념(Process Concept)

3.1.1. 프로세스(The Process)

잡(job)이라고도 불리는 프로세스는 실행 중인 프로그램이다. 메모리에 올라간 프로세스는 프로그램 코드를 지닌 텍스트, 전역 변수가 담긴 데이터, 함수 복귀 주소/로컬 변수 등이 담긴 스택, 동적으로 할당되는 힙을 가지고 있다.

프로세스 자체가 다른 프로세스를 위한 실행 환경으로 동작할 수 있다. JAVA 가상기계(JVM) 안에서 JAVA 프로그램이 실행된다.

3.1.2. 프로세스 상태(Process State)

프로세스는 실행되면서 그 상태가 변한다. 운영체제마다 다르지만 프로세스는 다음 상태들 중 하나에 있게 된다.

  • 새로운(new): 프로세스 생성 중
  • 실행(running): 명령어들 실행 중
  • 대기(waiting): 사건(신호, 입출력 등)을 기다림
  • 준비 완료(ready): 처리기에 할당되기를 기다림
  • 종료(terminated): 실행 종료

어느 한 순간에 한 처리기에는 오직 하나의 프로세스만이 실행된다. 하지만, 많은 프로세스가 준비 완료 및 대기 상태에 있을 수 있다.

3.1.3. 프로세스 제어 블록(PCB, Process Control Block)

프로세스 제어 블록은 프로세스와 연관된 정보를 기록한다.

  • 프로세스 상태
  • 프로그램 카운터
  • CPU 레지스터들
  • CPU 스케줄링 정보
  • 메모리 관리 정보
  • 회계(accounting) 정보
  • 입출력 상태 정보

3.1.4. 스레드(Threads)

프로세스는 한번에 여러 일을 할 수 있는 다수의 실행 스레드를 가질 수 있다.

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

프로세스 스케줄러는 CPU에서 프로그램 실행을 위해 가능한 프로세스들 중 하나를 선택한다.

3.2.1. 스케줄링 큐(Scheduling Queues)

프로세서들은 다양한 스케줄링 큐에서 대기한다.

메모리에서 처리기를 기다리는 프로세스들은 준비 완료 큐(ready queue)에서 대기한다. 이후 실행 후 입출력을 기다리거나, 자식 프로세스를 생성하여 이를 기다린 후 어떤 큐에 다시 넣어질 수 있다. 프로세스는 종료될 때까지 이 주기를 반복한다.

3.2.2. 스케줄러(Schedulers)

  1. 단기 스케줄러
    • 메모리에서 실행 준비가 완료된 프로세서들 중 하나를 CPU에 할당
    • 실행 간격이 짧고 매우 빠름
  2. 장기 스케줄러
    • 디스크에서 메모리로 적재함
    • 실행 빈도수가 적음
    • 입출력과 CPU 중심 프로세스들을 적절하게 선택
    • 사용 되지 않을 수 있음
  3. 중기 스케줄러
    • 메모리가 너무 복잡하여, 실행 중인 프로세스를 잠시 디스크로 이동시킴
    • 이를 스와핑이라 부름

3.2.3. 문맥 교환(Context Switch)

CPU가 현재 실행 중인 프로세스를 중단하고 다른 프로세스를 실행하려면, CPU의 현재 상태를 저장해야한다. 예를 들어, 인터럽트가 걸린 프로세스의 정보를 저장하고, 인터럽트를 마친 후 저장한 값을 사용해 상태를 복구한다.

이 작업을 문맥 교환이라 하며, 교환 시간동안 다른 유용한 일을 하지 못하기에 이는 순수한 오버헤드이다.

3.3. 프로세스에 대한 연산(Operation on Processes)

3.3.1. 프로세스 생성(Process Creation)

프로세스는 새로운 프로세스들을 생성할 수 있다. 생성하는 프로세서는 부모 프로세서이며 새로운 프로세스는 자식 프로세스이다.

프로세스들은 유일한 프로세스 식별자(pid)를 통해 구분된다. 프로세스가 새로운 프로세스를 생성했을 때, 두 프로세스가 실행되는 두 가지 경우가 있다.

  1. 부모와 자식이 병행하게 실행됨
  2. 부모가 일부 또는 모든 자식의 실행 종료를 기다림

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

  1. 자식 프로세스는 부모 프로세스의 복사본(둘이 똑같은 프로그램과 데이터를 가짐)
  2. 자식 프로세스가 새로운 프로그램을 갖고 있음

UNIX를 예로 들면, 새로운 프로세스는 원래 프로세스의 복사본이다. 이후 exec() 시스템 호출을 통해 새로운 프로그램을 자신의 메모리 공간에 올릴 수 있다.

Windows의 경우, CreateProcess API를 통해 명시된 프로그램을 실행하는 프로세스를 생성한다.

3.3.2. 프로세스 종료(Process Termination)

프로세스는 마지막 문장의 실행을 끝내고, exit() 시스템 호출을 사용하여 종료될 수 있다. 메모리, 파일, 버퍼등 모든 자원이 운영체제로 반납된다.

부모 프로세스가 자식 프로세스를 종료시킬 수 있으며, wait() 시스템 호출을 통해 자식의 상태를 알 수 있다. 몇몇 운영체제에서는 부모가 종료되면 자식들도 연쇄적으로 종료된다.

프로세스가 종료되면 자원은 운영체제로 돌아가지만, 프로세스의 엔트리는 부모가 wait()를 호출할 때까지 프로세스 테이블에 남아있는다. 종료되었지만 부모가 아직 wait를 호출하지 않은 프로세스를 좀비(zombie)프로세스라고 한다.

만약 부모가 wait를 호출하지 않고 종료된다면, 좀비 프로세스는 고아(orphan) 프로세스가 된다. Linux와 UNIX의 경우 고아 프로세스의 부모를 최상위 부모인 init에게 할당한다. init 프로세스는 주기적으로 wait를 호출하여 프로세스 식별자와 프로세스 테이블의 엔트리를 반환한다.

3.4. 프로세스간 통신(interprocess communication, IPC)

프로세스간 통신에는 기본적으로 공유 메모리와 메세지 전달 두 가지 모델이 있다. 보통 두 모델 전부 사용된다.

3.4.1. 공유 메모리 시스템(Shared-Memory Systems)

통신하고자 하는 프로세스들은 공유 메모리 세그먼트를 자신의 주소 공간에 추가해야 한다. 공유 메모리의 동일한 위치에 쓰지 않도록 유의해야 한다.

공유 메모리 영역에 접근할 때, 커널의 도움은 필요 없다. 많은 처리 코어를 가진 시스템에서는 캐시 일관성 문제로 인하여 성능 저하가 발생한다.

3.4.2. 메세지 전달 시스템(Message-Passing Systems)

이 방법은 운영체제가 메세지 전달에 참여한다. 프로세스들이 통신을 원하면 먼저 이들 사이에 통신이 연결되어야 한다.

3.4.2.1. 명명(Naming)

프로세스들은 직접 또는 간접 통신을 할 수 있다.

직접 통신은 상대 프로세스의 이름이 바뀔때 마다 새로운 이름으로 변경해야 하기에 바람직하지 않다. 직접 통신은 아래와 같은 특성을 가진다.

  • 서로 상대방의 이름만 알면 프로세스간 연결이 자동으로 구축된다.
  • 연결은 정확히 두 프로세스들 사이만 연결하고, 각 쌍에는 하나만 존재해야 한다.

간접 통신에서 메세지는 메일박스 또는 포트로 송신되고, 그것으로부터 수신된다. 간접 통신은 다음과 같은 특성을 가진다.

  • 프로세스간 연결은 공유 메일박스를 통해 구축된다.
  • 공유 메일박스를 통해 두 개 이상의 프로세스와 연결될 수 있다.
  • 메일박스는 프로세스 혹은 운영체제가 소유할 수 있다.

3.4.2.2. 동기화(Synchronization)

메세지 전달은 동기식, 비동기식이 있다.

  • 동기식 송신: 송신 프로세스는 수신 프로세스가 메세지를 수신할 때까지 기다린다.
  • 비동기식 송신: 송신 프로세스는 메세지를 보내고 작업을 재시작한다.
  • 동기식 수신: 메세지가 올 때까지 수신 프로세스는 기다린다.
  • 비동기식 수신: 수신 프로세스는 유효한 메세지 또는 널(NULL)을 받는다.

3.4.2.3. 버퍼링(Buffering)

통신이 직접적이든 간접적이든, 메세지는 임시 큐(버퍼)에서 대기한다. 큐(버퍼)가 없는 시스템에서 송신자는 수신자가 메세지를 수신할 때까지 기다려야 한다.

3.5. 클라이언트 서버 환경에서의 통신(Communication in Cilent-Server System)

클라이언트-서버 환경에서 공유 메모리, 메세지 전달 기법 외에 소켓, 원격 프로시저 호출(RPCs) 및 파이프가 쓰일 수 있다.

3.6.1. 소켓(socket)

소켓은 통신의 극점(endpoint)을 뜻한다. 두 프로세스가 네트워크상에서 통신을 하려면 프로세스마다 하나씩, 총 두 개의 소켓이 필요하다.

각 소켓은 IP주소와 포트 번호를 통하여 구분된다. 클라이언트가 연결을 요청하면 서버가 포트 번호를 부여한다.

예를 들어 161.25.19.8:80 (http 포트 번호 80)에 접속하려고 하면, 클라이언트에게 1024(1024 이하는 well known port)보다 큰 임의의 정수를 할당한다. 161.25.19.8:80과 146.86.5.20:1625의 유일한 소켓 쌍으로 구성된다.

저수준인 소켓에서는 스레드들 간에 구조화 되지 않은 바이트 스트림만 통신이 가능하다. 바이트 스트림 데이터를 구조화 하여 해석하는 것은 어렵다.

6.2. 원격 프로시저 호출(Remote Procedure Calls, RPC)

RPC 통신에서 전달되는 메세지는 구조화 되어있다. 송신측에서 실행되어야 할 함수, 함수에 필요한 매개변수 등을 구조화 하여 보낸다. 수신측에서 요청된 함수를 실행하고 출력 혹은 다른 메세지를 반환한다.

big-endian/little-endian 처럼 송신 시스템과 수신 시스템이 서로 다를 수 있다. 이를 해결하기 위해 양 측에 stub이라는 것이 존재한다. stub은 기종 중립적인 형태(XDR, external data representation)같이 적절한 형태로 매개변수 정돈(marshalling 또는 직렬화; serialization)을 한다.

RPC호출은 “최대 한 번”, “정확히 한 번” 수행되어야 한다. “최대 한번”은 메세지에 타임스탬프를 기록하는 것으로, “정확히 한 번”은 요청 수신 및 실행 ACK신호를 보냄으로써 해결한다.

포트 번호 문제도 해결되어야 한다. RPC호출 시 클라이언트는 서버의 포트 번호를 모른다.

고정된 포트 번호를 이용하는 방법은 서버측에서 컴파일 후 포트 번호를 바꿀 수 없다. 동적 바인딩 방법은 이보다 유연하지만, 고정된 포트 번호를 가진 랑데부 디먼(rendezvous daemon, matchmaker 라고도 불림)를 한번 더 거쳐야 한다.

클라이언트는 먼저 랑데부 디몬에게 호출하고자 하는 함수의 포트 번호를 요청한다. 서버의 랑데부 디먼이 응답하면, 클라이언트가 RPC호출을 할 수 있다.

3.6.3. 파이프(Pipes)

6.3.1 일반 파이프(Ordinary Pipes)

이는 같은 컴퓨터 내의 부모-자식 프로세스에서만 사용된다. 또한 단방향 통신만 가능하다. 양방향 통신을 위해서는 두 개의 파이프를 사용해야 한다.

6.3.2. 지명 파이프(Named Pipes)

UNIX의 지명 파이프는 같은 컴퓨터 내 여러 프로세스들이 사용하여 통신할 수 있다. Windows의 지명 파이프는 서로 다른 컴퓨터끼리도 통신이 가능하다.

댓글남기기