Search

프로세스의 본질과 상태 관리

운영체제 학습에서 프로세스는 핵심적인 추상화 개념입니다. 일상적으로 사용하는 프로그램들이 실제로 어떻게 동작하는지 이해하려면, "실행 중인 프로그램"이라는 단순한 정의를 넘어서는 체계적인 접근이 필요합니다. 프로세스는 현대 컴퓨팅 시스템에서 작업의 기본 단위이며, 메모리 관리부터 스케줄링까지 운영체제의 모든 핵심 기능과 밀접하게 연관됩니다.
이번 학습을 통해 프로세스와 프로그램의 본질적 차이를 명확히 하고, 메모리 구조와 상태 관리 메커니즘을 체계적으로 파악하고자 합니다. 특히 교과서를 읽으면서 생긴 의문들을 해결하고, 이론적 개념과 실제 구현 사이의 연결점을 찾아보겠습니다.

프로그램과 프로세스의 본질적 구분

정적 존재와 동적 존재

프로그램과 프로세스의 차이는 정적 존재와 동적 존재의 차이로 이해할 수 있습니다. 프로그램은 디스크에 저장된 명령어 집합으로서 수동적 존재입니다. 반면 프로세스는 메모리에 적재되어 실행 중인 프로그램으로서 능동적 존재입니다.
이 구분이 중요한 이유는 동일한 프로그램이라도 여러 개의 서로 다른 프로세스가 될 수 있기 때문입니다. 웹 브라우저의 여러 창이나 텍스트 에디터의 여러 인스턴스가 대표적인 예입니다. 각각은 동일한 실행 파일에서 시작되었지만, 독립적인 메모리 공간과 실행 상태를 가지는 별개의 프로세스입니다.

프로세스 생성 시점

프로그램이 프로세스가 되는 정확한 시점은 실행 파일이 메모리에 적재될 때입니다. 이 과정에서 운영체제는 새로운 주소 공간을 할당하고, 프로그램의 코드와 데이터를 메모리에 복사하며, 프로세스 제어 블록(PCB)을 생성합니다. UNIX 계열 시스템에서는 fork() 시스템 콜로 새 프로세스를 생성한 후 exec() 시스템 콜로 프로그램 이미지를 변경하는 방식을 사용합니다.

프로세스 메모리 구조의 심화 이해

4개 섹션의 기본 배치

프로세스의 메모리 구조는 텍스트, 데이터, 힙, 스택의 4개 주요 섹션으로 구성됩니다. 이들은 가상 메모리 공간에서 특정한 배치를 가지며, 각각 다른 특성과 용도를 가집니다.
3.1 - 프로세스 메모리 배치

텍스트 섹션과 데이터 섹션

학습 과정에서 힙과 스택에 비해 텍스트와 데이터 섹션의 역할이 덜 명확했는데, 이들의 정확한 기능을 이해하는 것이 중요합니다.
텍스트 섹션은 컴파일된 기계어 명령어가 저장되는 영역입니다. 가장 중요한 특징은 읽기 전용(Read-Only)이라는 점입니다. 프로그램 실행 중에 코드가 변경되면 안 되기 때문입니다. 또한 여러 프로세스가 동일한 프로그램을 실행할 때 텍스트 섹션을 공유할 수 있어 메모리 효율성을 높입니다.
데이터 섹션은 더 세분화됩니다. 초기화된 데이터 섹션에는 초기값이 있는 전역 변수와 정적 변수가 저장되고, BSS(Block Started by Symbol) 섹션에는 초기화되지 않거나 0으로 초기화되는 전역 변수가 저장됩니다.
// 메모리 섹션별 데이터 배치 예시 int global_var = 100; // 초기화된 데이터 섹션 static int static_var = 50; // 초기화된 데이터 섹션 int uninitialized_var; // BSS 섹션 int main() { // 텍스트 섹션 int local_var = 10; // 스택 char* heap_ptr = malloc(100); // 힙 영역 할당 return 0; }
C
복사
텍스트와 데이터 섹션의 크기는 컴파일 시점에 결정되며, 프로그램 실행 중에는 변하지 않습니다.

힙과 스택의 동적 특성

힙과 스택은 프로그램 실행 중에 크기가 변할 수 있는 동적 섹션입니다. 전형적인 메모리 레이아웃에서 스택은 높은 주소에서 낮은 주소로 성장하고, 힙은 낮은 주소에서 높은 주소로 성장합니다.
이러한 배치에서 자연스럽게 드는 의문은 "힙과 스택이 서로를 향해 확장된다면 충돌은 어떻게 방지할까?"입니다. 운영체제는 여러 메커니즘으로 이를 해결합니다.
가장 기본적인 방법은 가상 메모리 시스템입니다. 각 프로세스가 독립적인 가상 주소 공간을 가지므로, 물리 메모리 매핑을 통해 충돌을 방지할 수 있습니다. 또한 허용되지 않은 메모리 영역에 접근하면 세그먼테이션 폴트가 발생하여 프로그램이 종료됩니다. 일부 시스템에서는 스택과 힙 사이에 스택 가드 페이지를 설치하여 접근 시 즉시 예외가 발생하도록 합니다.

메모리 섹션의 커스터마이징

C 프로그램의 메모리 배치가 일반적인 개념과 차이가 있다는 언급에서 의문이 생겼는데, 실제로 메모리 섹션은 어느 정도 커스터마이징이 가능합니다.
링커 스크립트를 통해 섹션 배치를 제어할 수 있고, 컴파일러 지시어를 사용하여 특정 변수를 원하는 섹션에 배치할 수도 있습니다.
// GCC에서 특정 섹션에 변수 배치 int special_var __attribute__((section(".special"))) = 42;
C
복사
운영체제별로도 차이가 있습니다. Linux는 ELF 포맷을 사용하여 유연한 섹션 구성이 가능하고, Windows는 PE 포맷, macOS는 Mach-O 포맷을 사용합니다. 하지만 기본적인 구조(텍스트, 데이터, 힙, 스택)는 대부분의 시스템에서 공통적으로 사용됩니다.

프로세스 상태와 상태 전이

5가지 프로세스 상태

프로세스는 실행되면서 상태가 변합니다. 기본적인 5가지 상태는 다음과 같습니다:
새로운(New): 프로세스가 생성 중인 상태
준비(Ready): CPU 할당을 기다리는 상태
실행(Running): 명령어가 실행되고 있는 상태
대기(Waiting): 이벤트 발생을 기다리는 상태
종료(Terminated): 실행이 완료된 상태
3.2 - 프로세스 상태 다이어그램

상태 전이의 실제 시나리오

각 상태 전이는 특정한 조건과 상황에서 발생합니다.
준비에서 실행으로의 전이는 CPU 스케줄러가 해당 프로세스를 선택할 때 발생합니다. 시분할 시스템에서는 각 프로세스에게 타임 슬라이스가 할당됩니다.
실행에서 대기로의 전이는 주로 I/O 요청이 발생하거나, 자식 프로세스의 종료를 기다리거나, 동기화 객체를 대기할 때 발생합니다. 이때 프로세스는 자발적으로 CPU를 양보합니다.
실행에서 준비로의 전이는 비자발적인 상황에서 발생합니다. 타임 슬라이스가 만료되거나, 더 높은 우선순위의 프로세스가 등장하거나, 인터럽트가 발생할 때입니다.

멀티코어 환경에서의 고려사항

멀티코어 시스템에서는 여러 프로세스가 동시에 실행 상태에 있을 수 있습니다. 각 코어마다 독립적인 실행 상태를 가지므로, 스케줄러는 로드 밸런싱과 CPU 친화성을 고려해야 합니다. NUMA(Non-Uniform Memory Access) 환경에서는 메모리 접근 지연을 최소화하기 위해 프로세스를 적절한 코어에 배치하는 것이 중요합니다.

프로세스 제어 블록(PCB)

3.3 - 프로세스 제어 블록 (PCB)

PCB의 전체적 구조

프로세스 제어 블록은 운영체제가 프로세스를 관리하기 위해 필요한 모든 정보를 담고 있는 핵심 자료구조입니다. 각 프로세스마다 하나의 PCB가 존재하며, 이는 프로세스의 완전한 상태를 나타내는 일종의 "신분증" 역할을 합니다.
PCB는 크게 다섯 가지 범주의 정보를 포함합니다:
프로세스 식별 정보: 프로세스 ID, 부모-자식 관계
프로세서 상태 정보: 프로그램 카운터, CPU 레지스터 값들
프로세스 제어 정보: 스케줄링 우선순위, 상태 정보
메모리 관리 정보: 페이지 테이블, 세그먼트 테이블
입출력 및 파일 정보: 열린 파일 목록, I/O 상태
이러한 구조화된 정보 관리를 통해 운영체제는 수백 개의 프로세스를 동시에 효율적으로 관리할 수 있습니다.

프로세서 상태 정보의 메커니즘

PCB에서 가장 중요한 부분 중 하나는 프로세서 상태 정보입니다. 여기서 저장되는 "CPU 레지스터" 정보는 물리적 CPU 레지스터 값들의 정확한 스냅샷을 의미합니다.
이 메커니즘의 동작 과정을 살펴보면:
1.
실행 중: 프로세스가 물리적 CPU 레지스터를 직접 사용
2.
중단 시점: 인터럽트나 문맥교환 발생 시 모든 레지스터 값을 PCB에 저장
3.
대기 기간: 다른 프로세스가 실행되는 동안 PCB에 상태 보존
4.
재실행: PCB에서 저장된 값들을 물리적 레지스터에 복원
// x86-64 아키텍처에서의 레지스터 상태 정보 struct cpu_context { unsigned long rax, rbx, rcx, rdx; // 범용 레지스터 unsigned long rsp, rbp; // 스택 포인터, 베이스 포인터 unsigned long rip; // 명령어 포인터 unsigned long rflags; // 상태 플래그 // 추가 레지스터들... };
C
복사
이는 단순한 데이터 저장이 아닌 프로세스의 실행 문맥을 완전히 보존하는 정교한 메커니즘입니다. 레지스터는 CPU가 직접 연산에 사용하는 고속 저장소이므로, 각 프로세스가 독립적인 실행 환경을 유지하려면 이러한 저장과 복원 과정이 필수적입니다.

스케줄링 제어 정보

스케줄링 정보는 운영체제의 스케줄러가 프로세스 실행 순서를 결정하는 데 사용하는 메타데이터들입니다. 이 정보들은 시스템의 성능과 공정성을 결정하는 핵심 요소들입니다.
주요 구성 요소로는 프로세스 우선순위, nice 값, 할당된 시간 슬라이스, 누적 실행 시간, 스케줄링 정책 등이 있습니다. 각 스케줄링 알고리즘은 이 정보들을 서로 다른 방식으로 활용합니다. 우선순위 기반 스케줄링에서는 priority 값으로 실행 순서를 결정하고, 시분할 시스템에서는 time_slice로 CPU 할당 시간을 제어합니다.
특히 Linux의 CFS(Completely Fair Scheduler)는 이러한 정보들을 red-black tree 자료구조로 관리하여 O(log n) 시간에 다음 실행할 프로세스를 선택할 수 있도록 최적화되어 있습니다.

메모리 관리 구조

메모리 관리 정보는 프로세스가 사용하는 가상 메모리 공간의 매핑과 관련된 모든 정보를 포함합니다. 이는 현대 운영체제의 핵심 추상화인 가상 메모리 시스템을 지원하는 데 필수적입니다.
페이지 테이블 포인터는 가상 페이지를 물리 페이지로 변환하는 테이블의 위치를 가리키고, 세그먼트 테이블은 논리 주소를 선형 주소로 변환하는 데 사용됩니다. 베이스/한계 레지스터는 세그먼트 기반 메모리 관리에서 프로세스의 메모리 경계를 정의합니다.
이 정보들은 MMU(Memory Management Unit)와 밀접하게 협력하여 프로세스마다 독립적인 주소 공간을 제공하며, 메모리 보호와 가상 메모리 기능을 구현합니다.

실제 구현 사례: Linux task_struct

Linux에서는 PCB가 task_struct라는 복잡한 구조체로 구현됩니다:
struct task_struct { long state; // 프로세스 상태 struct sched_entity se; // 스케줄링 엔티티 struct task_struct *parent; // 부모 프로세스 참조 struct list_head children; // 자식 프로세스 목록 struct mm_struct *mm; // 메모리 관리 구조 struct files_struct *files; // 파일 시스템 정보 // 수백 개의 추가 필드들... };
C
복사
이 구조체는 1000줄이 넘는 복잡한 정의를 가지며, 프로세스의 모든 측면을 관리하는 운영체제의 중추적 역할을 합니다.

문맥 교환의 체계적 분석

실행 모드와 권한 계층

문맥 교환을 정확히 이해하려면 현대 CPU의 권한 계층 구조를 먼저 파악해야 합니다. 이는 시스템 보안과 안정성의 기반이 되는 메커니즘입니다.
사용자 모드는 일반 응용 프로그램이 실행되는 제한된 환경입니다. 이 모드에서는 특정 명령어들이 금지되고, 하드웨어에 대한 직접 접근이 차단됩니다. 반면 커널 모드는 운영체제 커널이 실행되는 특권 환경으로, 모든 명령어를 실행할 수 있고 하드웨어를 직접 제어할 수 있습니다.
중요한 점은 문맥 교환이 현재 실행 모드에 관계없이 발생할 수 있다는 것입니다. 사용자 프로그램이 실행되는 도중에도, 커널이 시스템 콜을 처리하는 중에도 타이머 인터럽트나 다른 하드웨어 인터럽트가 발생하여 문맥 교환이 필요한 상황이 생길 수 있습니다. 따라서 운영체제는 어떤 모드에서든 현재 실행 상태를 완전히 보존할 수 있는 메커니즘을 갖춰야 합니다.

문맥 교환의 단계별 메커니즘

문맥 교환은 정교하게 설계된 다단계 프로세스입니다:
1단계 - 인터럽트 인식: 타이머 인터럽트, I/O 완료 신호, 시스템 콜 등이 문맥 교환을 촉발합니다. 이때 CPU는 즉시 커널 모드로 전환됩니다.
2단계 - 상태 보존: 현재 실행 중인 프로세스의 모든 레지스터 값을 해당 프로세스의 PCB에 저장합니다. 이는 프로그램 카운터부터 상태 플래그까지 CPU의 모든 실행 상태를 포함합니다.
3단계 - 스케줄링 결정: 커널의 스케줄러가 실행 큐에서 다음에 실행할 프로세스를 선택합니다. 이 선택은 스케줄링 알고리즘과 각 프로세스의 우선순위에 따라 결정됩니다.
4단계 - 상태 복원: 선택된 프로세스의 PCB에서 저장된 레지스터 값들을 물리적 CPU 레지스터에 로드합니다.
5단계 - 실행 재개: 새로운 프로세스가 이전에 중단된 지점부터 실행을 계속합니다.
3.6 - 프로세스에서 프로세스로의 문맥 교환을 보여주는 다이어그램

성능 영향과 최적화 전략

문맥 교환은 본질적으로 순수한 오버헤드입니다. 이 기간 동안 시스템은 실제 작업을 수행하지 않고 단순히 실행 환경을 전환하는 데만 시간을 소모합니다.
하드웨어 수준의 최적화가 핵심적인 역할을 합니다. 일부 고급 프로세서는 다중 레지스터 집합을 제공하여 문맥 교환을 물리적 레지스터 복사 없이 단순히 레지스터 집합 포인터를 변경하는 것으로 처리할 수 있습니다.
그러나 문맥 교환의 실제 비용은 레지스터 저장과 복원을 넘어섭니다. 캐시 무효화TLB(Translation Lookaside Buffer) 플러시가 숨겨진 주요 비용입니다. 새로운 프로세스는 이전 프로세스가 캐시에 적재한 데이터를 사용할 수 없으므로, 실행 초기에 캐시 미스가 빈번하게 발생합니다. 마찬가지로 가상 메모리 변환 정보를 캐시하는 TLB도 플러시되어 메모리 접근 성능이 일시적으로 저하됩니다.
이러한 특성으로 인해 문맥 교환의 실질적인 성능 영향은 단순한 레지스터 조작 시간보다 수십 배에서 수백 배까지 클 수 있으며, 이는 시스템 설계에서 중요한 고려사항이 됩니다.

정리

프로세스의 본질을 이해하는 것은 운영체제 전반을 이해하는 기초가 됩니다. 프로세스는 단순히 실행 중인 프로그램이 아니라, 운영체제가 제공하는 정교한 추상화 계층입니다.
메모리 구조는 프로그램의 다양한 구성 요소를 효율적으로 관리하기 위한 설계입니다. 텍스트와 데이터 섹션의 정적 특성, 힙과 스택의 동적 특성은 각각 다른 용도와 관리 방식을 가집니다.
상태 관리와 문맥 교환은 시분할 시스템의 핵심 메커니즘입니다. 여러 프로세스가 하나의 CPU를 공유하면서도 각각 독립적으로 실행되는 것처럼 보이게 하는 것은 정교한 상태 관리와 빠른 문맥 교환 덕분입니다.
이러한 기초 개념들은 다음 단계인 프로세스 스케줄링과 생명주기 관리, 그리고 프로세스 간 통신으로 이어집니다. 각 개념은 독립적이면서도 서로 밀접하게 연관되어 현대 운영체제의 복잡한 시스템을 구성합니다.

참고