안동민 개발노트

본문 시작

프로세스의 개념

프로그램과 프로세스를 구분하고 코드·데이터·힙·스택으로 구성된 메모리 구조와 프로세스 상태 전이를 이해합니다.

디스크에 저장된 실행 파일은 프로그램입니다.

이 프로그램을 실행하면 메모리에 적재되어 CPU에 의해 실행되는데, 이 실행 중인 인스턴스가 프로세스(Process)입니다.

같은 실행 파일로 별도의 프로세스를 시작하면 각각 독립적인 가상 주소 공간을 가집니다.

다만 브라우저 창 수와 프로세스 수는 일치하지 않습니다. 브라우저는 기존 프로세스를 재사용하거나 탭·렌더러·GPU 작업을 여러 프로세스에 나눌 수 있습니다.

이 구분이 중요한 이유는, 프로그램은 그저 디스크에 저장된 정적인 바이트 시퀀스일 뿐이고, OS가 실제로 관리하는 단위는 프로세스이기 때문입니다.

프로세스는 주소 공간과 자원 관리의 단위이고, 현대 OS에서 CPU를 배정받는 실행 단위는 대체로 그 안의 스레드입니다.

운영체제를 이해한다는 것은 곧 프로세스를 이해한다는 것과 같습니다.


프로세스 메모리 구조

다음은 일반적인 C 프로그램의 메모리 구분입니다. 실제 주소 배치와 권한은 실행 형식·OS·런타임에 따라 달라집니다.

이 구조를 이해하면 스택 오버플로우, 메모리 누수, 세그먼테이션 폴트 같은 오류의 원인이 명확해집니다.

텍스트(Text/Code) 영역: 실행할 기계어 코드가 저장됩니다.

일반적인 실행 코드 매핑에는 쓰기 권한을 주지 않습니다. JIT처럼 실행 코드를 생성하는 런타임은 OS의 메모리 권한 API를 이용하므로, 모든 코드가 언제나 수정 불가능하다는 뜻은 아닙니다.

읽기 전용으로 보호하는 이유는 두 가지입니다.

첫째, 프로그램의 코드가 실행 중에 변경되면 예측 불가능한 동작이 발생합니다.

둘째, 같은 프로그램의 여러 프로세스가 텍스트 영역을 공유할 수 있어 메모리를 절약합니다.

크롬 탭 10개가 모두 같은 크롬 코드를 사용하는데, 각각 별도로 적재하면 낭비입니다.

데이터(Data) 영역: 전역 변수와 정적(static) 변수가 저장됩니다.

이 영역은 다시 두 개로 나뉩니다.

.data 섹션은 초기값이 있는 전역 변수(int count = 10;)를 저장하며, 프로그램 바이너리에 초기값이 포함됩니다.

.bss 섹션에는 명시적 초기값이 없거나 0으로 초기화되는 전역·정적 변수가 주로 배치됩니다. 바이너리에 0을 모두 저장하지 않고 실행 시 해당 메모리를 0으로 준비합니다.

.bss를 별도로 관리하는 이유는 바이너리 파일 크기를 줄이기 위해서입니다.

0으로 초기화할 변수를 위해 수 MB의 0을 바이너리에 저장할 필요가 없습니다.

힙(Heap) 영역: 프로그래머가 런타임에 동적으로 할당하는 메모리입니다.

C의 malloc() 같은 할당 함수와 관리형 언어의 객체 저장소가 동적 메모리를 사용합니다. 실제 할당 위치는 할당기와 최적화에 따라 달라지며, 모든 객체가 반드시 별도의 힙 공간을 차지하는 것은 아닙니다.

전통적인 도식에서는 높은 주소 방향으로 자라지만, 현대 할당기는 mmap() 등으로 별도 매핑도 사용합니다.

할당 후 해제하지 않으면 메모리 누수(Memory Leak)가 발생합니다.

Java·Python·Go 등은 런타임이 객체 수명을 관리합니다. C/C++에서는 소유권에 맞게 해제해야 하며, C++의 RAII·스마트 포인터로 이를 자동화할 수 있습니다.

스택(Stack) 영역: 함수 호출 시 생성되는 스택 프레임이 쌓입니다.

각 프레임에는 지역 변수, 함수 매개변수, 복귀 주소(함수가 끝나면 돌아갈 주소)가 저장됩니다.

x86 계열의 일반적인 스택은 낮은 주소 방향으로 자랍니다. 지역 값 일부는 최적화로 레지스터에만 존재할 수도 있습니다.

함수가 반환되면 프레임이 제거되므로, 지역 변수의 메모리는 자동으로 회수됩니다.

재귀 호출이 너무 깊으면 스택이 한계를 넘어 스택 오버플로우(Stack Overflow)가 발생합니다.

메모리 영역별 변수 위치
#include <stdio.h>
#include <stdlib.h>

int global_init = 42;       /* .data 영역 */
int global_uninit;           /* .bss 영역 */

void func() {
    int local = 10;          /* 스택 영역 */
    int *heap = malloc(100); /* 힙 영역 (포인터 자체는 스택) */

    printf("Code:   %p\n", (void *)func);
    printf("Data:   %p\n", (void *)&global_init);
    printf("BSS:    %p\n", (void *)&global_uninit);
    printf("Heap:   %p\n", (void *)heap);
    printf("Stack:  %p\n", (void *)&local);

    free(heap);
}

int main() {
    func();
    return 0;
}

이 코드는 주소를 비교하는 예제입니다. 함수 포인터를 void *로 출력하는 부분은 이를 지원하는 Linux 환경을 전제로 하며 ISO C만으로 이식성을 보장하지 않습니다. 할당에 실패하면 heap은 NULL입니다.

주소 순서와 간격은 ASLR·컴파일 옵션·할당기에 따라 달라집니다. 힙과 스택 사이가 모두 비어 있거나 항상 서로를 향해 확장한다고 단정하지 않습니다.

Linux에서 실행 중인 프로세스의 메모리 맵을 확인하려면 /proc/<PID>/maps 파일을 읽으면 됩니다.

프로세스 메모리 맵 확인
cat /proc/self/maps
# 주소 범위          권한   오프셋    장치    inode  경로
# 5600a0000000-5600a0001000 r--p 00000000 08:01 1234 /usr/bin/cat
# 5600a0001000-5600a0005000 r-xp 00001000 08:01 1234 /usr/bin/cat  (텍스트)
# 7f1234000000-7f1234021000 rw-p 00000000 00:00 0    [heap]
# 7ffd12340000-7ffd12361000 rw-p 00000000 00:00 0    [stack]

주석의 주소는 형식을 설명하는 예시이며 이번 실행에서 관측한 값이 아닙니다. self는 파일을 여는 cat 프로세스 자신을 가리킵니다.

r-xp는 읽기+실행 권한(코드 영역), rw-p는 읽기+쓰기 권한(힙, 스택)입니다.

이 맵을 보면 프로세스가 어떤 메모리 영역을 어떤 권한으로 사용하는지 한눈에 파악할 수 있습니다.

메모리 영역은 이름만 외우기보다, 각 영역의 수명과 권한이 왜 다른지 함께 보면 오류 원인을 더 빨리 좁힐 수 있습니다.


프로세스의 상태

프로세스 상태는 CPU를 받을 수 있는지, 아니면 I/O나 이벤트를 기다리는지로 나뉩니다.

프로세스는 생성부터 종료까지 여러 상태를 거칩니다.

이 상태 모델은 OS가 수많은 프로세스를 효율적으로 관리하는 기반입니다.

New(생성): 프로세스가 만들어지고 있는 상태입니다.

OS가 PCB를 할당하고, 메모리 공간을 설정하는 중입니다.

Ready(준비): 실행될 준비가 완료되어 CPU 할당을 기다리는 상태입니다.

Ready Queue에 들어갑니다.

"나는 실행할 수 있다.

CPU만 주면 바로 간다."

현재 실행 가능한 상태라는 뜻입니다. 이후 실행 중 새로운 I/O나 잠금 대기를 만날 수 있습니다.

Running(실행): CPU를 점유하여 명령어를 실행하고 있는 상태입니다.

하나의 논리 CPU는 한 시점에 하나의 OS 스레드를 실행합니다. SMT를 지원하는 물리 코어는 여러 논리 CPU를 제공할 수 있으므로 물리 코어 수와 Running 스레드 수를 바로 같다고 보지 않습니다.

Waiting/Blocked(대기): I/O 완료나 이벤트 발생을 기다리는 상태입니다.

CPU가 비어 있어도 이 프로세스는 실행될 수 없습니다.

기다리는 조건이 충족되거나 허용된 신호 등으로 대기가 끝나면 다시 Ready로 돌아갈 수 있습니다.

예를 들어 디스크에서 파일을 읽고 있거나, 네트워크 응답을 기다리거나, 사용자의 키보드 입력을 기다리는 경우입니다.

Terminated(종료): 실행이 완료되었거나 강제 종료된 상태입니다.

사용하던 메모리, 파일 디스크립터 등의 자원이 OS에 반환됩니다.

단, 부모 프로세스가 종료 상태를 수거(wait())할 때까지 PCB는 남아 있습니다.

Linux에서는 추가로 두 가지 상태가 있습니다.

Stopped는 SIGSTOP·SIGTSTP 같은 작업 제어 신호 등으로 일시 정지된 상태입니다.

일반적인 터미널에서 Ctrl+Z는 foreground 작업 그룹에 SIGTSTP를 보냅니다.

fg 명령으로 재개할 수 있습니다.

Zombie는 프로세스가 종료되었지만 부모가 아직 wait()를 호출하지 않은 상태입니다.

ps 명령어에서 볼 수 있는 프로세스 상태 코드를 정리하면 다음과 같습니다.

코드Linux 상태의 의미
R실행 중이거나 실행 가능한 상태
S신호로 깨어날 수 있는 이벤트 대기
D중단 불가 대기; I/O 등 커널 내부 대기 지점 확인
T / t작업 제어 정지 / 디버거 추적 정지
Z종료했지만 종료 상태 수거가 남음

D 상태는 특별한 경우입니다.

NFS 마운트가 응답하지 않거나, 디스크가 불량 섹터를 읽고 있을 때 프로세스가 D 상태에 빠질 수 있습니다.

중단 불가 대기에서는 SIGKILL을 보내도 대기에서 벗어날 때까지 종료 처리가 지연될 수 있습니다. 원인은 해당 커널 대기 지점과 장치를 확인해야 합니다.

D 상태가 오래 지속되거나 누적되면 저장 장치·네트워크 파일 시스템 등의 대기 원인을 조사합니다. 순간 상태 하나만으로 장애를 확정하지 않습니다.


상태 전이

상태 간의 전환은 다음과 같이 발생합니다.

각 전이의 원인을 이해하면, OS의 동작 흐름이 자연스럽게 따라옵니다.

프로세스 상태와 전이 원인
실행·대기·종료의 갈림길생성 뒤 Ready, CPU 배정 뒤 Running. 선점되면 Ready, 대기가 필요하면 Waiting, 조건 충족 후 Ready. 실행 종료 시 Terminated.NewReadyRunningWaitingTerminated생성 완료CPU 배정선점대기 필요대기 조건충족종료

실행 가능해지는 Ready와 실제 CPU를 받는 Running을 구분합니다. New·Terminated는 생성·종료 단계이며 Linux STAT 문자와 일대일 대응하는 목록은 아닙니다.

  • New → Ready: 프로세스 생성이 완료되어 Ready Queue에 편입됩니다. fork() 시스템 콜이 성공적으로 완료된 시점입니다.

  • Ready → Running: 스케줄러가 이 프로세스를 선택하여 CPU를 할당합니다. 이 행위를 디스패치(Dispatch)라 합니다. 5장에서 스케줄러가 어떤 기준으로 선택하는지 다룹니다.

  • Running → Waiting: 프로세스가 I/O를 요청하거나 이벤트를 기다려야 할 때 발생합니다. 블로킹 read()가 즉시 결과를 제공할 수 없어 기다려야 한다면 Waiting 상태로 전환될 수 있습니다. 캐시 적중이나 논블로킹 I/O는 같은 경로를 항상 거치지 않습니다. 이때 CPU가 놀지 않도록 스케줄러가 Ready Queue에서 다른 프로세스를 꺼내 실행합니다.

  • Running → Ready: 선점(Preemption)이 발생합니다. 타이머 인터럽트가 발생하여 현재 프로세스의 타임 슬라이스가 만료되었거나, 더 높은 우선순위의 프로세스가 Ready 상태가 되어 현재 프로세스를 밀어내는 경우입니다. 2장에서 다룬 타이머 인터럽트가 바로 이 전이를 일으킵니다.

  • Waiting → Ready: 기다리던 조건이 충족되어 커널이 실행 가능하게 만듭니다. 장치 완료 인터럽트 처리는 그 계기 중 하나입니다. 곧바로 Running이 되는 것이 아니라, Ready Queue에서 스케줄러의 선택을 기다려야 합니다.

  • Running → Terminated: 프로세스가 exit() 시스템 콜을 호출하거나, main() 함수가 반환하거나, 처리되지 않은 시그널(SIGSEGV 등)로 강제 종료됩니다.

실무에서 이 상태 전이를 관찰하는 가장 간단한 방법은 top 또는 htop 명령어입니다.

프로세스 상태 실시간 관찰
top
# Tasks: 312 total,   1 running, 295 sleeping,   0 stopped,  16 zombie
#         ↑           ↑ Running    ↑ Waiting               ↑ 지속 여부 확인

htop  # 더 직관적인 인터페이스, 트리 뷰 지원

top 출력의 첫 줄에서 Running, Sleeping(Waiting), Stopped, Zombie 프로세스의 수를 한눈에 볼 수 있습니다.

주석의 top 수치는 설명용 예시입니다. 좀비는 종료와 수거 사이에 잠깐 나타날 수 있습니다. 지속적으로 남거나 증가할 때 부모의 wait() 처리와 수거 책임을 조사합니다.

상태 코드를 읽을 때는 실행 가능성, 대기 원인, 종료 상태 수거 여부를 구분합니다.

다음 절에서는 OS가 프로세스의 모든 정보를 관리하는 PCB(Process Control Block)와 프로세스 전환의 핵심인 컨텍스트 스위칭을 다루겠습니다.