Python 이 Single Thread 에서 동작하는 이유에 대해 알아보던 중에 GIL(Global Interpreter Lock) 에 대해 알게 되었고, 이참에 Process 와 Thread 를 다시 정리해보면 좋을 것 같아 작성하게 된 글이다.
📌 Process
A process is the instance of a computer program that is being executed by one or many threads.
Process(Computing) - Wikipedia
프로세스는 하나 혹은 다수의 스레드에 의해 실행중인 프로그램 인스턴스 를 의미한다.
즉, 프로그래밍 언어로 작성되어 디스크에 저장된 프로그램이 실행되고, CPU 자원을 할당받을 준비가 되었을 때, 프로세스라는 작업 단위로 불리는 것이다.
위에서 CPU 자원을 할당받는다는 의미는 프로그램이 RAM에 올려져 독립된 메모리 자원을 할당받는다는 것을 의미하며, 이는 OS에 의해 이루어진다.
그렇다면, 할당받은 메모리 구조는 어떻게 이루어져있을까.
위 그림은 각각의 프로세스가 OS로부터 할당받은 메모리의 구조를 나타내고 있으며, 크게 Stack, Heap, Data, Code 로 나눌 수 있다.
설명 상 편의를 위해 Code 부터 설명하자면, Code 에는 우리가 작성한 코드가 저장된다. 더욱 정확하게는 프로그램 실행 도중 변경될 일이 없는 상수, 조건문, 반복문 등의 코드들이 컴파일 과정에서 기계어로 변환되어 Read-Only 로 저장되어 있다고 볼 수 있다.
따라서, 당연하게도 Code 에 저장되어 있는 내용들은 읽기 전용이므로 런타임에 수정 및 제거가 불가능하다.
Data 에는 전역변수(Global), 정적변수(Static), 구조체(Structure type) 와 같이, 프로그램의 시작시점부터 종료시점까지 유지되어야 하는 값들이 저장된다. 다만 Code 에 저장되는 값들과는 달리, 프로그램 실행 도중 변경될 가능성이 있기 때문에 미리 기계어로 변환되어 저장되지는 않는다.
Heap 은 프로그래머에 의해 의도적으로 할당되는 값들이 저장되는 영역으로, 프로그램 실행 도중 malloc()
혹은 free()
와 같은 의도적으로 작성된 코드에 의해 할당 혹은 해제된다. 즉, 런타임에 동적으로 메모리가 할당되는 동적 영역이다.
마지막으로, Stack 에는 지역변수, 리턴값 등 특정 함수 및 구문에서 사용되는 일반적인 매개변수 들이 저장된다. 따라서, 컴파일 시 메모리가 정적으로 할당되지만, 이는 해당 함수가 종료되면 자동으로 Stack에서 해제되는 값들이므로 동적으로 그 크기가 변하게 된다.
📌 정적 할당 vs 동적 할당
추가적으로, Stack 과 Heap 메모리는 동적 영역, Data 와 Code 메모리는 정적 영역 으로 분류된다. 하지만, 이를 정적 할당, 동적 할당 과 헷갈려서는 안된다.
예를 들어, Stack 은 Heap 과 함께 동적 영역으로 분류되지만, Stack 에 저장되는 지역 변수 혹은 매개 변수는 컴파일 과정에 정적으로 메모리를 할당받기 때문이다.
반면, Heap 은 런타임에 동적으로 메모리를 할당받으면서, 동시에 동적 영역 으로 분류된다.
프로세스가 생성되면서 PCB(Process Control Block) 라는 자료구조가 OS 의 커널에 저장된다. 이는 운영체제가 프로세스를 표현하는 방법이며, 내부의 정보(PID)를 활용해 각각의 프로세스를 식별 및 제어하게 된다.
특히, OS 의 Context Switching(문맥 교환) 이나 Process Management(프로세스 상태 관리) 를 위해 필수적으로 필요한 정보들을 담고 있다.
위 그림은 PCB 의 구조를 나타낸 그림이며, 이외에도 여러가지 정보가 담겨 있지만 여기서는 Pointer, State, PID, Program Counter 대해서만 간단하게 살펴보도록 하자.
먼저, Pointer 는 부모 프로세스와 자식 프로세스의 주소, 현재 프로세스의 주소, 프로세스에 할당된 메모리의 주소 등을 저장하고 있다.
하나의 프로세스는 새로운 프로세스를 생성할 수 있는데, 이 때 생성되는 프로세스를 자식 프로세스, 기존의 프로세스를 부모 프로세스라고 한다.
Process State 는 현재 프로세스의 상태를 의미하며, New(생성), Ready(준비), Running(실행), Waiting(대기), Terminated(완료) 의 다섯 가지 상태로 분류된다.
추가적으로, 생성된 프로세스는 종료되기 전까지 Ready, Running, Waiting 의 단계를 반복적으로 거칠 수 있고 각각의 상태 전이는 아래와 같다.
PID 는 Process Identifier 로서, OS 가 각각의 프로세스를 구별 및 식별하는 데 사용되는 고유 번호이며, 이를 통해 직접 프로세스를 제어할 수도 있다.
마지막으로, Program Counter 는 프로그램 계수기, 명령어 포인터라고도 불린다.
이는 Program Counter 가 다음에 실행될 명령어의 주소를 가지고 있어 실행할 기계어 코드의 위치를 지정하기 때문이다.
이 정보를 통해, Process 는 Context Switching 이 발생하더라도 이전의 작업 내용을 잃지 않고 이어서 실행할 수 있게 된다.
이외에도 PCB 는 Priority, Memory Management Info, Accounting Info, I/O Status Info 등 Process 에 대한 다양한 정보들을 담고 있다.
앞서, Process 의 상태 전이에 대해 살펴보면서 Ready, Running, Waiting 의 세 가지 상태를 반복적으로 가질 수 있다고 설명하였다.
이는 Process 가 생성되어 CPU 를 할당받아 실행되고 있는 도중이라도, 할당된 시간 초과, I/O 작업, 특정 Event 발생 등의 이유로 CPU 를 반납하고 대기하는 경우가 있다는 의미이다.
그렇다면, CPU 가 Process 를 교체하는 이유는 무엇일까?
기본적으로 하나의 CPU 는 하나의 Process 작업만을 수행할 수 있으며, 사용자는 컴퓨터에서 다양한 프로그램을 동시에 사용하게 된다.
(사용하고 있는 프로그램이 없더라도 기본적으로 시스템의 유지를 위해 실행되고 있는 프로세스가 굉장히 많다.)
따라서 일반적인 프로그램을 CPU 연산 작업과 I/O 작업의 반복이라고 볼 때, 프로그램이 I/O 작업 중일 때도 CPU 를 할당하는 것은 비효율적인 방식인 것이다.
(사용자 로그인을 위한 ID 와 Password 입력을 대기하고 있을 때 혹은 문서를 프린터로 출력하는 중일 때, CPU 가 할당되어 있는 것은 비효율적이다.)
위와 같은 이유로, 실행 중인 Process 는 특정 상황에서 할당받은 CPU 를 반납하고 Ready 혹은 Waiting 상태로 전이되며, 새로운 Process 가 CPU 를 차지하고 Running 된다.
바로 이렇게, 기존의 Process 에서 새로운 Process 로 CPU 가 새롭게 할당되는 작업을 Context Switching, 문맥 교환 이라고 부른다.
Context Switching 과정을 간단히 살펴보면 아래와 같다.
먼저, Timer run out 이나 I/O 작업, System Call 등의 사유로 문맥 교환의 조건이 만들어지면, 실행 중인 Process 는 CPU 스케줄러에 의해 Interrupt 가 발생하게 된다.
이후, 기존의 Process 는 실행 중인 사용자 모드에서 커널 모드로 전환되며, 전이된 새로운 상태, 지금까지의 작업 내용, 다음 번에 실행될 명령어의 주소 등의 정보를 PCB 에 저장한다.
CPU 스케줄러에 의해 선택된 새로운 프로세스는 자신의 PCB 에서 다양한 정보를 불러와 메모리와 같은 H/W 에 복구한다. 이 때, Program Counter 에서 다음 번에 실행될 명령어의 주소를 불러와 복구하고, Pointer 나 레지스터에서 Stack 의 메모리 주소 등을 저장하고 있기 때문에 사용자는 이전의 작업 내용을 잃지 않고 이어서 작업할 수 있다.
마지막으로, 새로운 Process 는 기존의 커널 모드에서 사용자 모드로 전환된다.
이러한 문맥 교환의 과정은 Process 의 상태가 전이될 때 발생하게 되며, 이 과정에서 사용되는 시간 및 메모리 등의 리소스는 시스템 상의 Overhead 이기 때문에 문맥 교환이 빈번하게 일어날 경우, Overhead 가 커지는 문제가 발생할 수 있어 주의해야 한다.
추가적으로, 다음에 실행될 Process 를 선택하는 기법을 Scheduling, 스케줄링 이라 부르며, 여기에는 다양한 스케줄링 알고리즘이 존재한다.
스케줄링에 대해서는 이후 별도의 포스팅으로 정리를 할 예정이다.
이렇게, Process 의 개념부터 Process 가 할당받는 가상메모리의 구조, PCB, Context Switching 까지, Process 의 핵심적이고 기본적인 내용들을 정리해보았다.
이후에는 Thread 에 대해 정리해보면서, GIL(Global Interpreter Lock) 을 통해 Python 이 Single Thread 에서 동작하는 이유에 대해서까지 포스팅할 계획이다.