프로세스(Process)

dong5854·2022년 7월 17일
0
post-thumbnail

예전에도 프로세스와 스레드에 관한글을 쓴 적이 있다. 이 때는 멀티 프로세스(Multi-Process)와 멀티 스레드(Multi-Thread) 그리고 컨텍스트 스위칭(Context Switching)을 중심으로 공부하고 글을 썼는데, 이번에는 프로세스 자체에 좀 더 초점을 맞춰 정리를 해볼 생각이다.


프로세스의 개념

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

Stack영역: 지역변수와 함수 인자 값, 매개변수 반환값등의 일시적인 데이터가 저장되는 영역
Heap 영역: 동적 메모리 호출에 의해 할당되는 메모리 영역, C언어의 malloc()과 calloc() 함수에 의해 생성된 변수들이 이곳에 할당된다.
Data 영역: 프로그램의 전역변수(Global Variable)과 정적 변수(Static Variable)이 저장되는 있는 영역
Code 영역: 자료에 따라 Text영역이라고도 하고, 프로그램의 코드 그 자체를 뜻한다. 이 코드들은 CPU가 해석할 수 있는 Binary Code 상태로 올라간다.

code 영역과 data 영역의 크기는 고정되어 프로그램 실행 시간 동안 크기가 변하지 않는 반면 stack 및 heap 영역은 프로그램 실행 중에 동적으로 줄어들거나 커질 수 있다. 함수가 호출될 때마가 함수 매개변수, 지역 변수 및 복귀 주소를 포함하는 activate record(활성화 레코드) 가 스택에 푸시(push)된다. 그리고 함수에서 제어가 되돌아오면 스택에서 활성화 레코드가 팝(pop)된다. 마찬가지로 메모리가 동적으로 할당됨에 따라 힙이 커지고 메모리가 시스템에 반횐되면 축소된다. 위의 그림대로 스택과 힘 섹션이 서로의 방향으로 커지더라도 운영체제는 이들이 서로 겹치지 않도록 해야한다.

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

프로세스 자체가 다른 개체를 위한 실행 환경으로 동작할 수 있다는 사실을 가장 잘 나타내는 것은 Java 프로그래밍 환경인데, 대부분의 상황에서 실행 가능한 Java 프로그램은 JVM 안에서 실행된다. JVM은 적재된 Java 코드를 해석하고 그 코드를 대신하여 원 기계어를 이용해 행동을 취하는 프로세스로서 프로그램을 실행한다. 예를 들어, 컴파일된 Java 프로그램 Program.class를 실행하기 위해 서는 java Program이라는 명령어를 입력할 텐데, java 명령어는 JVM을 실행시키고 JVM은Program을 가상기계(virtual machine)안에서 실행한다. 즉 JVM이라는 프로세스는 Program.class라는 프로그램을 실행한 프로세스의 실행을 위해 존재하는 프로세스이다.

프로세스의 상태

프로세스는 실행되면서 그 상태가 변한다.

프로세스의 상태는 부분적으로 그 프로세스의 현재의 활동에 따라 정의된다. 프로세스는 다음 상태 중 하나에 있게 된다.

new: 프로세스가 생성 중이다.
running: 명령어들이 실행되고 있다.
waiting: 프로세스가 어떤 이벤트가 일어나기를 기다린다.
ready: 프로세스가 처리기에 할당되기를 기다린다.
terminated: 프로세스의 실행이 종료되었다.

상태의 이름은 운영체제마다 다를 수 있으나 각각에 해당하는 상태들은 모든 시스템에서 찾아볼 수 있다. 위의 상태 다이어그램은 이 상태들에 해당하는 다이어그램이다.

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

각 프로세스는 운영체제에 의해 프로세스 제어 블록(PCB)(테스크 제어 블록, TCB라고도 불린다)에 의해 표현된다. 프로세스 제어 블록은 특정 프로세스와 연관된 여러 정보를 수록하며, 아래와 같은 것들을 포함한다.

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

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

프로세스 스케줄링

다중 프로그래핑(multi programming)의 목적은 CPU 이용 효율을 최대화하기 위하여 항상 CPU에서 어떠한 프로세스가 실행되도록 하는 데 있다.
시분할의 목적은 프로세스들 사이에서 CPU 코어를 빈번하게 교체하여 사용자가 느끼기에 프로세스들이 동시에 동작하는 것처럼 느끼게 하는 것이다. 이를 실현하기 위해 프로세스 스케줄러는 코어에서 실행 가능한 여러 프로세스 중에서 한의 프로세스를 선택한다.

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

스케줄링 큐

프로세스가 시스템에 들어가면 준비 큐(ready queue)에 들어가서 준비 상태가 되어 CPU 코어에서 실행되기를 기다린다. 이 준비 큐의 헤더에는 리스트의 첫번째 PCB에 대한 포인터가 저장되고 각 PCB에는 준비큐에서 다름 PCB를 가리키는 포인터 필드가 포함된다. 즉 이 큐는 일반적으로 연결 리스트(linked list)로 저장된다.
시스템에는 준비 큐 외에 다른 큐도 존재한다. 프로세스에 CPU 코어가 할당되면 프로세스는 잠시 동안 실행되어 결국 종료되거나 인터럽트 되거나 I/O 요청의 완료와 같은 특정 이벤트를 기다린다. 프로세스가 디스크와 같은 장치에 I/O 요청을 한다고 가정한다면 장치는 프로세서보다 굉장히 느리게 실행되기 때문에 프로세스는 I/O거 사용가능할 때까지 기다려야한다. 이런 I/O완료와 같은 특정 이벤트가 발생하기를 기다리는 프로세스는 대기 큐(waiting queue)에 삽입된다.

위와 같이 준비큐와 다른 대기 큐들을 사용해 cpu 스케줄링을 할 수 있다.

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

새 프로세스는 처음에 준비 큐에 놓인다. 프로세스는 실행을 위해 선택되거나 또는 디스패치 될 때까지 기다린다. 프로세스에 CPU 코어가 할당되고 실행 상태가 되면, 여러 이번트 중 하나가 발생할 수 있다.

  • 프로세스가 I/O 요청을 보낸 후 다음 I/O 대기 큐에 들어간다.
  • 프로세스는 자식 프로세스를 만든 후 자식 프로세스의 종료를 기다리는 동안 대기 큐에 놓일 수 있다.
  • 인터럽트 또는 타임 슬라이스가 만료되어 프로세스가 코어에서 쫓겨나 강제로 준비 큐로 돌아갈 수 있다.

컨텍스트 스위칭

컨텍스트 스위칭에 대해서는 이 글에도 정리한 적있다.

컨텍스트(context, 문맥)란 프로세스의 입장에서 프로세스가 사용되고 있는 상태를 나타낸다. 이런 상태는 전부 PCB에 저장되어 있기 때문에 context는 운영체제 입장에서는 PCB정보라도고 할 수 있다.

인터럽트가 발행하면 시스템은 현재 실행 중인 프로세스의 컨텍스트(context, 문맥)을 저장한다.(여기서 가장 중요한 정보는 프로그램 카운터라고 할 수 있다) 이렇게 문맥을 저장해두면 나중에 복구 작업을 할 때 다시 시작되어야 하는 곳에서 다시 작업을 시작하도록 할 수 있다.

즉 컨텍스트 스위칭이란,

  • cpu 코어를 다른 프로세스에 넘기는 것
  • 현재 프로세스의 상태를 저장하는 것
  • 다른 프로세스의 상태를 복원하는 것


위의 그림은 프로세스에서 프로세스로의 문맥 교환을 보여주는 다이어그램이다.

프로세스 생성과 종료

프로세스는 실행 중에 여러 새로운 프로세스들을 생성할 수 있는데 생성하는 프로세스를 부모 프로세스라고 하고 새로운 프로세스는 자식 프로세스라고 한다.
대부분의 운영체제들은 유니크한 PID(프로세스 아이디)를 사용하 프로세스를 구분하는데 이 식별자는 정수이고, 각 프로세스에 고유한 값을 가지도록 할당된다.

위의 그림은 리눅스 운영체제의 프로세스트리를 보여주고 있으며 프로세스 이름과 pid를 볼 수 있다. init 프로세스(systemd)는 pid 1을 갖고 모든 사용자 프로세스의 루트 부모 프로세스 역할을 수행하고 시스템이 부트될 때 생성되는 첫 번째 사용자 프로세스이다. pid가 1인 이유는 시스템이 부트 될 때 생성되는 첫 번째 사용자 프로세스이기 때문이다. 그리고 init 프로세스와 systemd 프로세스는 둘 다 리눅스의 정상적인 부팅을 위해 초기화를 해주는 프로세스인데, systemd는 이에 추가적으로 시스템을 총 관리해주는 데몬이다. 리눅스의 7이상 버전은 systemd를 사용하고 init은 6.9 이하 버전에서 사용한다.

프로세스가 새로운 프로세스를 생성할 때 두 프로세스를 실행시키는 데 두 가지 가능성이있다.

  1. 부모가 자식과 같이 동시적(concurrent)으로 실행된다.
  2. 부모는 일부 또는 모든 자식이 실행을 종료할 때까지 기다린다.

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

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

자식 프로세스가 부모 프로세스의 복사본일 경우, os의 입장에서 프로그램 카운터만을 다르게 하고 같은 자원을 공유하게 된다.

자식 프로세스의 생성은 fork() 시스템 콜로 하는데 자식 프로세스는 리턴값이 0이고 부모 프로세스에게는 자식의 pid가 리턴된다.
fork() 시스템 콜 다음에는 두 프로세스 중 한 프로세스가 exec() 시스템 콜을 사용해 자신의 메모리 공간을 새로운 프로그램으로 교체한다.


위의 이미지는 fork() 시스템 콜을 사용한 프로세의 생성을 보여준다. child 프로세스가 fork()를 통해 생성되고 exec() 통해 새로운 프로그램으로 교체되었으며 이 프로세스가 exit() 되자 부모 프로세스가 재개된다.

부모 프로세스는 자식 프로세스가 종료되었는지 여부를 체크할 필요가 있다. wait함수를 사용하면 부모 자식의 종료시점이 서로 동기화되는 방식으로 작동한다. 프로세스가 종료되면 사용하던 자원은 운영체제가 되찾아 가는데 프로세스의 종료 상태가 저장되는 프로세스 테이블의 해당 항목은 부모 프로세스가 wait()를 호출할 떄까지 남아있게 된다.종료되었지만 부모 프로세스가 아직 wait() 호출을 하지 않은 프로세스를 좀비(zombie) 프로세스라고 한다.
만약 부모 프로세스가 wait()를 호출하는 대신 종료한다면 자식프로세스가 고아(orphan)프로세스가 된다. 전통적인 UNIX는 고아 프로세스의 새로운 부모 프로세스로 init 프로세스를 지정해 해당 문제를 해결한다. init 프로세스는 주기적으로 wait()를 호출하여 고아 프로세스의 종료 상태를 수집하고 프로셋 식별자외 프로세스 테이블 항목을 반환한다.
리눅스 시스템이 init을 systemd로 대체하면서 systemd가 init과 마찬가지로 주기적으로 wait()을 호출하여 고아 프로세스의 종료 상태를 수집할 수도 있지만 systemd 이외의 프로세스가 고아 프로세스를 상속하고 종료를 관리하도록 할 수 있다.


해당 게시물은 흔히 공룡책이라고 불리는 Avraham Silberschatz, ⌜Operating System Concepts 10th edition⌟, 박민규 옮김 과 인프런 주니온님의 운영체제 공룡책 강의 및 구글링을 통해 접한 글들을 보며 공부한 내용을 작성한 것입니다.

수강 강의
운영체제 공룡책 강의, 인프런, 주니온

출처 및 참고자료

Avraham Silberschatz, ⌜Operating System Concepts 10th edition⌟, 박민규 옮김
https://jobdong7757.tistory.com/102

profile
https://github.com/dong5854?tab=repositories

0개의 댓글