
프로세스란 무엇일까? 대부분이 실행 중인 프로그램 으로 프로세스를 정의한다.
그럼 이때 실행 중이라는 것이 정확히 어떤 의미인지에 대해 자세히 알아보도록 하자.
프로그램을 실행하면 우선 프로그램이 메모리에 적재된다. 적재된 메모리 공간을 크게 4가지의 영역으로 나눌 수 있는데, 주소 공간이 높은 쪽에서 낮은 쪽으로 스택, 힙, 데이터, 코드 영역으로 나누어진다. 여기서는 사용자 공간(User Space)에 대해서만 설명할 예정이다.

함수 호출 시 함수에서 선언된 지역 변수, 매개변수, 리턴 주소 등이 저장되는 영역이다.
스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸되는데 아래쪽으로, 즉 낮은 주소 방향으로 확장된다.
이렇게 스택 영역에 차례대로 저장되는 함수의 호출 정보를 스택 프레임(stack frame)이라고 하는데, 스택 프레임은 함수 호출 시 생성되고, 반환 시 제거된다. 이러한 스택 프레임 덕분에 함수의 호출이 모두 끝난 뒤에, 해당 함수가 호출되기 이전 상태로 되돌아갈 수 있다.
아래의 코드로 함수 호출에 의한 스택 프레임의 변화를 통해 스택 프레임의 동작 방식에 대해 자세히 알아보자.

출처: https://www.tcpschool.com/c/c_memory_stackframe
1) 먼저 프로그램이 실행되면, 가장 먼저 main() 함수가 호출되어 main() 함수의 스택 프레임이 스택에 저장된다.
2) func1() 함수를 호출하면 해당 함수의 매개변수, 반환 주소값, 지역 변수 등의 스택 프레임이 스택에 저장된다.
3) func2() 함수를 호출하면 해당 함수의 스택 프레임이 마찬가지로 추가로 스택에 저장된다.

출처: https://www.tcpschool.com/c/c_memory_stackframe
1) func2() 함수의 모든 작업이 완료되어 반환되면, func2() 함수의 스택 프레임만이 스택에서 제거된다.
2) func1() 함수 또한 마찬가지로 스택 프레임이 스택에서 제거되고
3) main() 함수 또한 스택 프레임이 스택에서 제거되면서 프로그램이 종료된다.
이렇게 스택은 가장 나중에 저장된 데이터가 가장 먼저 인출되는 방식인 LIFO, Last-In First-Out으로 동작한다.

출처: https://www.tcpschool.com/c/c_memory_stackframe
재귀 호출을 너무 많이 하면 발생하는 스택 오버플로우도 스택 영역에서 발생하는 것이다.
만약 재귀 호출이 무한히 반복되면, 그림처럼 재귀 호출에 의한 스택 프레임이 계속해서 쌓여만 갈 것이다. 이렇게 스택의 모든 공간을 다 차지하고 난 후에 또 다시 스택 프레임을 저장하게 되면, 해당 데이터는 스택 영역을 넘어가서 저장되게 된다. 만약에 스택 영역을 넘어가지 못하고 프로그램이 이 오류로 인해 종료되는 것이 아니라 스택 영역을 넘어가도 데이터가 저장될 수 있으면, 개발자가 알 수 없는 오동작을 하게 되거나 보안상의 크나큰 취약점을 가지게 된다.

동적 메모리 할당을 위한 영역으로, 위쪽으로(높은 주소 방향으로) 확장된다. 프로그래머가 직접 또는 가비지 컬렉터가 메모리를 동적으로 요청하고 해제할 수 있으며 고정된 크기의 스택(Stack)과 달리 유연하다는 것이 장점이자 주의할 점이다. 메모리가 필요한 만큼 할당할 수 있어 유연하지만, C/C++ 같은 언어는 개발자가 메모리를 직접 관리해야 하기 때문에 메모리 누수 문제가 발생할 수 있다.
프로세스의 주소 공간은 물리 공간일까 가상 공간일까?
프로세스의 주소 공간은 물리 공간이 아니라 가상공간이다. 바로 이 가상주소 공간의 사용자 공간에 프로세스가 실행 중에 동적할당 받는 힙 영역이 있다.
개발자가 malloc()으로 동적할당을 했는데 메모리가 부족하다고 NULL을 리턴했다. 이것은 물리 메모리가 부족하기 때문에 NULL을 리턴한 것일까?
프로세스 힙 영역은 프로세스가 동적 할당받는 메모리로 사용하도록 프로세스에게 주어진 공간이므로 malloc()가 메모리가 부족하다고 NULL을 리턴한다면 운영체제가 설정한 사용자 공간의 최대 범위까지 도달한 것이지 물리 메모리가 부족한 것이 아니다.

전역 변수와 정적 변수가 저장되는 영역이다. 보통 프로그램의 실행부터 종료 때까지 데이터가 존재한다.
세부적으로 나누자면 초기화된 전역 변수 및 정적 변수와 초기화되지 않은 전역 변수와 정적 변수로 영역을 나눌 수 있는데 초기화되지 않은 전역 변수와 정적 변수 영역은 bss라고 부른다.

함수나 메서드의 명령어 같이 프로그램의 실행 코드가 저장되는 영역이다. 주로 읽기 전용(Read-Only)으로 설정되어 있어 코드가 변경되지 않도록 보호된다.
프로그램이 메모리에 적재되면, 운영 체제는 해당 프로그램을 관리하기 위해 프로세스 제어 블록(PCB)을 생성한다. PCB는 프로세스의 메타데이터를 포함하는 데이터 구조이며, 프로세스 생성시 만들어지고, 실행이 끝나면 없어진다. 즉, 프로세스가 실행을 중단하고 대기 상태로 전환될 때 나중에 해당 프로세스를 다시 실행하기 위한 정보를 PCB에 저장하는 것이다!
PCB(Process Control Block)구조는 다음과 같다.

1) 프로세스 식별자(Process ID)
2) 프로세스 상태(Process State) : 생성, 준비, 실행, 대기, 완료 상태
3) 프로그램 카운터(Program Counter) : 이 프로세스가 다음에 실행할 명령어의 주소를 가리킨다.
4) CPU 레지스터 및 일반 레지스터
5) CPU 스케줄링 정보 : 우선 순위, 최종 실행시각, CPU 점유시간 등
6) 메모리 관리 정보 : 해당 프로세스의 주소 공간 등
7) 프로세스 계정 정보 : 페이지 테이블, 스케줄링 큐 포인터, 소유자, 부모 등
8) 입출력 상태 정보 : 프로세스에 할당된 입출력장치 목록, 열린 파일 목록 등
9) 포인터 : 부모프로세스에 대한 포인터, 자식 프로세스에 대한 포인터, 프로세스가 위치한 메모리 주소에 대한 포인터, 할당된 자원에 대한 포인터 정보 등.
등등이 있다.
PCB에서 프로세스의 상태를 저장한 것을 보았을 텐데, 이처럼 프로세스는 특정한 상태를 가진다.

ready 상태에서 dispatch(스케줄 이라고도 한다)가 되어 CPU를 할당받아 실제 수행되고 있는 상태를 말한다. 그런데 프로세스 하나가 CPU를 독점하는 것을 방지하기 위해, timeout을 시켜 강제로 다시 ready 상태로 돌아가게 할 수도 있다. 이를 선점(preemptive) 한다라고도 한다.ready상태로 돌아간다.running 상태인 프로그램이 종료되면 terminated 상태가 된다.+) 아래와 같이 각 상태로 변경되는 경우를 작성해보았다.

따라서 실행 중인 프로그램이란 프로그램이 메모리에 적재되어 운영 체제가 해당 프로그램에 프로세스 제어 블록(PCB)을 생성하고, 필요한 자원을 할당하며, 스케줄링을 통해 실행 상태로 되어져 있는 상태를 말하며 이것이 이제 진정한 프로세스라 할 수 있다.

스레드는 CPU 이용의 기본 단위이며 스레드 ID(tid), 프로그램 카운터(PC), 레지스터 집합, 스택으로 구성된다. 스레드는 같은 프로세스에 속한 다른 스레드와 코드 섹션, 데이터 섹션, 그리고 열린 파일이나 신호와 같은 운영체제 자원들을 공유한다.
이러한 스레드의 구조는 멀티코어 프로세서 환경에서 특히 유용하다. 각 스레드는 서로 다른 코어에서 병렬적으로 실행될 수 있어, 전체적인 시스템의 성능을 향상시킬 수 있다. 또한 스레드들은 같은 프로세스 내의 자원을 공유하기 때문에, 스레드 간 통신이 프로세스 간 통신보다 훨씬 효율적이다.
전통적인 프로세스는 하나의 스레드를 가지고 있었는데, 이 구조는 하나의 프로그램이 여러 개의 비슷한 작업을 해야하는 경우 효율적이지 못하다. 만약에 웹서버가 단일 스레드를 가지고 있는 프로세스로 동작한다면, 단일 프로세스로 한 번에 하나의 요청만 처리할 수 있게 되어 요청을 한 클라이언트는 자신의 요청이 처리되기까지 긴 시간을 기다려야할 것이다.
이 문제를 해결하기 위해서는 요청을 처리하는 대상(Ex) 웹 서버)이 하나의 프로세스로 동작하게 하는 것이다. 즉, 각 요청마다 해당 요청을 처리하는 프로세스를 생성하는 것이다. 하지만 이 방식은 여러 스레드를 만드는 방식보다 매우 비효율적이다. 프로세스를 생성하는 것은 많은 오버헤드가 발생한다. 반면에 스레드는 프로세스 내에서 실행되는 작업 흐름의 단위로, 프로세스보다 훨씬 적은 자원을 필요로 한다. 스레드는 프로세스의 자원을 공유하기 때문에 생성과 컨텍스트 스위칭에 드는 비용이 상대적으로 적다. 따라서 멀티스레딩을 통해 동시에 여러 작업을 효율적으로 처리할 수 있다.

멀티 스레딩을 앞 상황에 적용해보자. 웹 서버가 여러 개의 스레드를 가진 프로세스로 동작하면 서버는 클라이언트의 요청을 listen 하는 별도의 스레드를 생성한다. 요청이 들어오면 또 하나의 프로세스를 생성하는 것이 아니라, 해당 요청을 처리해줄 새로운 스레드를 생성하고 추가적인 요청을 대기하는 것이다!
스레드의 구현은 사용자 수준 스레드와 커널 수준 스레드, 두 가지로 나눌 수 있다. 사용자 수준 스레드는 사용자 공간에서 관리되며, 커널의 지원 없이 스레드 라이브러리에 의해 관리된다. 반면 커널 수준 스레드는 운영체제에 의해 직접 관리되고 스케줄링된다.
사용자 스레드와 커널 스레드는 어떤 연관 관계가 존재해야 한다. 그 이유는, 커널 수준 스레드가 실제로 CPU에서 실행되는 단위이기 때문이다. 사용자 수준 스레드가 실행되려면 반드시 커널 수준 스레드에 매핑되어야 한다. 이러한 매핑 관계는 다양한 모델(다대일, 일대일, 다대다)로 구현될 수 있다.

다대일 모델에서는 여러 개의 사용자 수준 스레드가 하나의 커널 스레드에 매핑된다. 이 모델은 스레드 관리가 사용자 공간의 스레드 라이브러리에 의해 이루어지기 때문에 효율적이라는 장점이 있다. 하지만 하나의 사용자 스레드가 블로킹 시스템 콜을 호출하면 전체 프로세스가 블록될 수 있다는 심각한 단점이 있다. 또한, 한 번에 하나의 스레드만이 커널에 접근할 수 있어 다중 스레드가 다중 코어 시스템에서 병렬로 실행될 수 없다.

일대일 모델에서는 각각의 사용자 수준 스레드가 하나의 커널 스레드에 매핑된다. 이 모델은 더 많은 병렬성을 제공하며, 한 스레드가 블로킹되어도 다른 스레드는 계속 실행될 수 있다. 그러나 각 사용자 스레드마다 커널 스레드를 생성해야 하므로 시스템 자원을 많이 사용하여 성능에 부담을 줄 수 있다는 단점이 있다.

다대다 모델은 여러 개의 사용자 수준 스레드를 그보다 적은 수 또는 같은 수의 커널 스레드로 멀티플렉싱하는 방식이다. 이 모델은 앞서 설명한 두 모델의 단점을 보완하고 장점을 결합한 방식으로, 개발자가 필요한 만큼의 사용자 수준 스레드를 생성할 수 있게 하면서도 커널 스레드는 효율적으로 관리할 수 있게 한다.
이 장점 덕분에 다대다 모델은 스레드 풀링을 효과적으로 구현할 수 있다. 스레드 풀을 통해 커널 스레드의 수를 적절히 조절하면서도, 필요에 따라 사용자 스레드를 유연하게 생성하고 관리할 수 있게 된다.
이러한 매핑 모델들은 각각 장단점이 있다. 일대일 모델은 사용자 스레드와 커널 스레드가 1:1로 매핑되어 병렬성을 최대한 활용할 수 있지만, 시스템 자원을 많이 사용한다는 단점이 있다. 다대일 모델은 여러 사용자 스레드가 하나의 커널 스레드에 매핑되어 자원 사용이 효율적이지만, 한 스레드가 블로킹되면 모든 스레드가 블로킹될 수 있다는 제약이 있다. 다대다 모델은 이러한 두 모델의 장단점을 절충한 방식으로, 여러 사용자 스레드를 여러 커널 스레드에 매핑한다. 이 모델은 유연성이 높고 효율적인 자원 관리가 가능하지만, 구현이 복잡하다는 특징이 있다. 대부분의 현대 운영체제들은 일대일 모델이나 다대다 모델을 채택하고 있다.