프로세스와 스레드
프로세스 (Process)
“실행 중인 프로그램(A program in execution)”
하드 디스크에 저장된 ‘my_program.exe’ 파일은 코드와 데이터의 묶음, 즉 정적인(Static) 존재인 ‘프로그램’ 입니다.
사용자가 이 프로그램을 더블 클릭하면, 운영체제는 이 파일을 메모리로 가져와 CPU를 할당해줍니다. 이 순간, 프로그램은 비로소 동적인(Dynamic) 존재인 ‘프로세스’ 가 됩니다.
- 프로그램 (Program) : 디스크에 저장된 정적인 코드 덩어리.
- 프로세스 (Process) : 메모리에 적재(Load)되어 OS 커널의 관리를 받으며 실행되는 프로그램. OS로부터 자원(CPU 시간, 메모리)을 할당받는 ‘작업(Job)의 단위’ 이자 ‘자원 할당의 단위’ 입니다.
프로세스의 주소 공간(Address Space)
운영체제가 프로세스를 실행하기 위해 할당하는 ‘전용 메모리 영역’을 의미합니다. 현대 OS는 가상 메모리(Virtual Memory) 를 통해, 모든 프로세스에게 자신만의 독립적인 주소 공간을 가진 것처럼 ‘추상화’하여 제공합니다.
이 주소 공간은 물리 메모리의 어디에 있든 상관없이, 각 프로세스 관점에서는 항상 0번지부터 시작하는 거대한 영역처럼 보입니다. 이 덕분에 한 프로세스가 다른 프로세스의 메모리를 침범하는 것이 원천적으로 차단됩니다.
주소 공간의 구성
- Code (or Text) 영역 : 실행할 프로그램의 기계어 코드(명령어)가 저장됩니다. 읽기 전용(Read-Only)이며, 여러 프로세스가 같은 프로그램을 실행할 경우 이 영역은 메모리에서 ‘공유’될 수 있습니다.
- Data 영역 : 프로그램이 시작할 때부터 존재하는 전역 변수(Global variables) 와 정적 변수(Static variables) 가 저장됩니다.
- 초기화된 데이터 (
.data세그먼트) - 초기화되지 않은 데이터 (
.bss세그먼트, 0으로 초기화됨)
- 초기화된 데이터 (
- Heap 영역 : 프로그래머가 코드 실행 중에 ‘동적으로’ 메모리를 할당하는 공간입니다. C의
malloc()이나 C++/Java의new연산자가 이 영역을 사용합니다. 주소값이 낮은 곳에서 높은 곳으로 할당됩니다. - Stack 영역 : 함수 호출, 지역 변수, 매개변수, 복귀 주소 등이 저장되는 임시 공간입니다. 함수가 호출될 때마다 ‘스택 프레임(Stack Frame)’이 쌓이고(Push), 함수가 반환되면 제거됩니다(Pop). 주소값이 높은 곳에서 낮은 곳으로 할당됩니다.
Heap과 Stack이 반대 방향으로 할당되는 이유
두 영역이 메모리 공간의 양 끝에서 서로를 향해 할당하도록 설계하는 것은 한정된 가상 주소 공간을 가장 유연하게 사용하기 위함입니다. Stack을 많이 쓰는 프로그램(재귀 함수 등)이나 Heap을 많이 쓰는 프로그램(대규모 객체 생성 등) 모두, 두 영역이 서로의 영역을 침범하기 전까지는 가용한 전체 공간을 최대한 활용할 수 있습니다.
프로세스 상태(Process State)
프로세스는 생성부터 소멸까지 계속해서 상태가 변합니다. OS는 이 ‘상태’를 기준으로 프로세스를 관리합니다.
- New (생성) : 프로세스가 방금 생성된 상태. (PCB 생성, 메모리 할당 준비)
- Ready (준비) : CPU를 할당받을 수 있지만, 아직 스케줄러의 선택을 받지 못하고 대기 중인 상태. (Ready Queue에 존재)
- Running (실행) : CPU를 점유하고 명령어를 실행 중인 상태. (시스템에 CPU 코어 수만큼만 Running 상태의 프로세스가 존재)
- Blocked (대기) : 프로세스가 실행 중 특정 이벤트를 기다리며 멈춰있는 상태.
- Terminated (종료) : 프로세스 실행이 완료되고 OS가 자원(메모리, PCB)을 정리하는 상태.
주요 상태 전이(State Transition) 시점
| 상태 변화 | 발생 시점 (Event) | 설명 |
|---|---|---|
Running → Ready | Time Slice Expired (타임아웃) | 할당된 CPU 시간(Quantum)을 다 사용함 (타이머 인터럽트) |
Running → Blocked | I/O Request (or wait()) | I/O 작업을 요청하는 시스템 콜을 호출하여 결과를 기다림 |
Blocked → Ready | I/O Completion (이벤트 완료) | 기다리던 I/O 작업이 완료되었음을 알리는 인터럽트가 발생함 |
Running → Terminated | Exit (종료) | exit() 시스템 콜 호출, 혹은 비정상 종료 (오류) |
New → Ready | Admitted (승인) | 장기 스케줄러가 프로세스 생성을 승인하고 메모리에 적재함 |
Running → Ready | Interrupt (선점) | (선택 사항) 우선순위가 더 높은 프로세스가 등장하여 CPU를 빼앗김 |
프로세스 제어 블록 (PCB)과 문맥 (Context)
OS는 수백 개의 프로세스를 동시에 관리해야 합니다. 이때 각 프로세스를 식별하고 그 모든 정보를 저장하는 ‘데이터 시트’가 필요한데, 이것이 바로 PCB (Process Control Block) 입니다.
PCB는 커널 영역(Kernel Space)에만 존재하며, OS가 프로세스 관리를 위해 사용하는 가장 핵심적인 자료구조입니다.
PCB에 저장되는 핵심 정보
- 프로세스 식별자 (PID) : 각 프로세스를 구분하는 고유 ID.
- 프로세스 상태 (State) : New, Ready, Running, Blocked, Terminated
- 프로그램 카운터 (PC) : 이 프로세스가 다음에 실행할 명령어의 메모리 주소
- CPU 레지스터 : CPU에서 연산 중이던 값들 (누산기, 범용 레지스터 등)을 저장하는 공간
- CPU 스케줄링 정보 : 프로세스의 우선순위, Ready 큐 포인터 등
- 메모리 관리 정보 : 이 프로세스의 주소 공간(Code, Data 등)을 가리키는 페이지 테이블 포인터.
- I/O 상태 정보 : 프로세스에 할당된 파일 디스크립터(File Descriptors) 목록.
문맥(Context)
‘문맥(Context)’ 은 특정 순간에 프로세스가 실행 중이던 ‘모든 상태의 총합’ 을 의미하며, 이는 대부분 PCB에 저장되거나 PCB를 통해 참조됩니다.
좁은 의미의 문맥(CPU 문맥)은 프로그램 카운터(PC)와 각종 CPU 레지스터 값들을 의미하며, 넓은 의미의 문맥은 이에 더해 프로세스의 주소 공간 정보(페이지 테이블), 열린 파일 목록 등 OS가 해당 프로세스를 재개하기 위해 알아야 할 모든 정보를 포함합니다.
PCB는 ‘문맥을 저장하는 그릇(자료구조)’이고, 문맥은 ‘PCB에 저장되는 실제 내용(데이터)’입니다.
프로세스 상태 관리
Queue
- Job Queue : 시스템에 들어온 모든 프로세스(디스크에 있음)의 큐.
- Ready Queue : 메모리에 적재되어 Ready 상태에 있는 프로세스들의 큐. CPU 스케줄러는 이 큐에서 다음 실행할 프로세스를 선택합니다. (보통 연결 리스트로 구현됨)
- Device Wait Queues : 특정 I/O 장치(특정 디스크)의 완료를 기다리는 Blocked 상태의 프로세스들만 모아놓은 큐. 각 장치마다 별도의 큐가 존재합니다.
프로세스가 I/O를 요청하는 시스템 콜을 호출하면, 해당 프로세스는
Blocked상태가 되어 특정 ‘장치 대기 큐(Device Wait Queue)’ 로 이동합니다. I/O 작업이 완료되어 인터럽트가 발생하면, OS는 이 큐에서 해당 프로세스를 꺼내Ready큐로 이동시킵니다.
Scheduler
- 장기 스케줄러 (Long-term Scheduler) : (과거 일괄 처리 시스템에서 사용) 어떤 프로세스를 Job Queue에서 Ready Queue로 승인할지 결정합니다. (메모리에 올릴지 말지 결정)
- 단기 스케줄러 (Short-term Scheduler) : (이것이 바로 CPU 스케줄러입니다) Ready Queue에 있는 프로세스 중 누구에게 CPU를 할당할지 결정합니다. (매우 빈번하게 호출됨)
- 중기 스케줄러 (Medium-term Scheduler) : (선택 사항) 메모리가 부족할 때, 일부 프로세스를 통째로 메모리에서 디스크로 쫓아내고(Swap-out), 나중에 다시 불러옵니다(Swap-in).
Suspended State
바로 이 중기 스케줄러에 의해 Swap-out 되어, 메모리에서 통째로 디스크로 쫓겨난 프로세스의 상태를
Suspended라고 합니다. 이 상태는 기존의Blocked나Ready상태와 조합되어 다음과 같이 나뉩니다.
- Suspended-Ready :
Ready상태였지만 메모리가 부족하여 디스크로 Swap-out 됨. (CPU만 받으면 되는 게 아니라, 메모리에도 다시 올라와야(Swap-in) 함)- Suspended-Blocked :
Blocked상태(I/O 대기 중)였는데 디스크로 Swap-out 됨. (I/O가 끝나도 Ready가 되지 않고, Suspended-Ready가 됨)
초기 운영체제에는 중기 스케줄러가 없었으나, 운영체제가 발전하고 메모리 자원의 효율적인 사용이 중요해지면서 중기 스케줄러가 추가적으로 등장하게 되었습니다.
프로세스 간 통신(IPC)
프로세스는 기본적으로 OS에 의해 독립된 주소 공간을 할당받아 서로를 보호합니다. 하지만 많은 경우, 여러 프로세스가 데이터를 주고받으며 협력(Cooperation) 할 필요가 있습니다. OS는 이러한 통신을 위해 IPC(Inter-Process Communication) 메커니즘을 제공합니다.
IPC 모델은 크게 두 가지로 나뉩니다.
- 메시지 패싱 (Message Passing)
- 커널이 제공하는
send(),receive()같은 시스템 콜을 통해 통신합니다. (파이프, 소켓 등) - 커널이 중재하므로 구현이 안전하지만, 매번 커널을 거쳐야 하므로(문맥 교환) 속도가 느립니다.
- 커널이 제공하는
- 공유 메모리 (Shared Memory)
- 여러 프로세스가 특정 메모리 영역을 ‘공유’하도록 OS에 요청하는 방식입니다.
- 일단 설정되면, 커널의 개입 없이 메모리에 직접 접근하므로 매우 빠릅니다.
- (문제 제기) ← 하지만 이 방식은 치명적인 위험을 내포합니다. 만약 두 프로세스가 ‘동시에’ 공유된 메모리의 변수를 수정하려 한다면, 접근 순서에 따라 데이터가 완전히 깨져버릴 수 있습니다.
이처럼 여러 실행 흐름이 공유 자원(Shared Resource) 에 동시에 접근할 때 발생하는 문제를 경쟁 상태(Race Condition) 라고 부릅니다.
공유 메모리 (Shared Memory)
커널의 도움을 받아, 여러 프로세스가 동일한 물리 메모리 영역을 자신들의 주소 공간에 매핑하여 공유하는 방식입니다. 일단 설정되면, 커널의 개입 없이 메모리에 직접 읽고 쓰므로 매우 빠릅니다.
- 연결 (Flow) : 하지만 여러 프로세스가 ‘공유 자원’에 동시에 접근하므로,
04. 동기화문서에서 다룬 경쟁 상태(Race Condition) 가 발생할 수 있습니다. 따라서 접근 시 반드시 세마포어(Semaphore) 같은 동기화 도구가 필요합니다.
메시지 패싱 (Message Passing)
커널이 제공하는 send()와 receive() 같은 시스템 콜을 통해 프로세스 간에 데이터를 ‘메시지’ 단위로 주고받습니다. 항상 커널을 거치므로(오버헤드 발생) 공유 메모리보다 느리지만, 커널이 동기화를 처리해 주므로 구현이 비교적 안전하고 간단합니다. (파이프, 소켓 등)
스레드(Threads)
초기 OS는 프로세스 하나가 곧 실행 흐름(Thread of Control) 하나였습니다. 하지만 프로세스를 생성하고 관리(문맥 교환)하는 비용은 매우 비쌉니다. (독립된 주소 공간 할당, PCB 생성 등) 이를 해결하기 위해 하나의 프로세스(하나의 주소 공간) 안에서 여러 개의 실행 흐름을 동시에 실행할 수 있도록 하기 위해 스레드가 고안되었습니다.
스레드 (Thread) 는 프로세스라는 ‘자원 할당의 단위’ 내부에서 실제로 CPU를 점유하여 실행되는 ‘실행의 단위(Unit of Execution)’ 입니다. 이 때문에 ‘경량 프로세스(Light-Weight Process, LWP)’라고도 불립니다.
공유하는 자원 (프로세스 단위)
- Code 영역
- Data 영역 (전역 변수)
- Heap 영역 (동적 할당 메모리)
- 파일 디스크립터 (File Descriptors)
독립적으로 가지는 자원 (스레드 단위)
- 스택 (Stack) : 각 스레드는 자신만의 함수 호출 스택을 가집니다. (스레드 1의 지역 변수가 스레드 2에 영향을 주지 않음)
- 프로그램 카운터 (PC) : 각 스레드가 어디까지 실행했는지 독립적으로 기억해야 합니다.
- CPU 레지스터 : 각 스레드의 연산 상태를 독립적으로 저장합니다.
멀티 스레딩의 장단점
- 장점 (Pros)
- 응답성 (Responsiveness) : 한 스레드가 I/O로 인해 Blocked 되어도, 다른 스레드는 계속 실행되어 사용자 응답성을 유지할 수 있습니다. (웹 서버 등)
- 자원 공유 (Sharing) : IPC 같은 복잡한 기법 없이도 Heap 영역 등을 통해 스레드 간 데이터 공유가 매우 쉽습니다.
- 효율성 (Efficiency) :
- 생성: 스레드 생성은 프로세스 생성보다 수십 배 빠릅니다. (주소 공간을 새로 만들 필요가 없음)
- 문맥 교환: 같은 프로세스 내의 스레드 간 문맥 교환은 OS가 주소 공간을 교체할 필요가 없으므로(TLB Flush 불필요), 프로세스 간 문맥 교환보다 훨씬 빠릅니다.
- 단점 (Cons)
- 동기화 문제 (Synchronization) : 여러 스레드가 Data나 Heap 영역을 ‘공유’하기 때문에, 한 스레드가 데이터를 수정하는 동안 다른 스레드가 접근하면 데이터가 깨질 수 있습니다(Race Condition). 이를 막기 위해 뮤텍스(Mutex), 세마포어(Semaphore) 같은 복잡한 동기화 기법이 반드시 필요합니다.
- 안정성 (Safety) : 한 스레드에서 발생한 오류(잘못된 포인터 접근, 처리되지 않은 예외 등)는 프로세스 전체를 다운(Crash)시킬 수 있습니다. (프로세스는 격리되어 있어 안정적)
Thread-Safe
멀티스레드 환경에서 여러 스레드가 동시에 특정 함수나 객체의 메서드를 호출해도, 경쟁 조건(Race Condition) 이 발생하지 않고 항상 의도된 대로 정확한 결과를 반환함을 보장하는 것을 의미합니다.
스레드 안정성을 확보하는 방법
- 재진입 가능하게 설계 (Re-entrancy) : 함수가 오직 자신의 스택에 있는 지역 변수와 매개변수만을 사용하고, 전역 변수나 정적 변수 같은 공유 자원에 절대 접근(특히 쓰기)하지 않도록 만듭니다. (가장 좋은 방법)
- 동기화 (Synchronization) : 공유 자원에 접근해야만 한다면, 해당 접근 코드 영역(임계 영역, Critical Section)을 뮤텍스(Mutex) 나 세마포어(Semaphore) 같은 락(Lock)으로 보호하여, 한 번에 오직 하나의 스레드만 해당 코드를 실행할 수 있도록 강제합니다.
- 스레드-로컬 스토리지 (TLS, Thread-Local Storage) : 전역 변수처럼 보이지만, 실제로는 각 스레드가 자신만의 독립적인 복사본을 갖도록 하는 기술입니다. (공유를 피하는 기술)
스레드의 두 가지 구현 모델
- User-Level Threads, ULTs (사용자 수준 스레드)
- OS 커널은 스레드의 존재를 모릅니다. 스레드 생성, 스케줄링, 동기화가 모두 사용자 영역의 스레드 라이브러리에 의해 관리됩니다. (커널 입장에서는 그저 평범한 단일 스레드 프로세스처럼 보임)
- 스레드 문맥 교환이 커널 모드 전환 없이(시스템 콜 X) 단순히 라이브러리 함수 호출로 이루어지므로, 매우 빠릅니다.
- 스레드 중 하나라도 입출력(I/O) 등 커널에 의해
Blocked되는 시스템 콜을 호출하면, 커널은 프로세스 전체를Blocked상태로 만들어버립니다. 즉, 프로세스 내의 다른 모든 스레드도 함께 멈춥니다.
- Kernel-Level Threads, KLTs (커널 수준 스레드):
- OS 커널이 스레드를 직접 인식하고 관리합니다. 스레드가 OS 스케줄링의 기본 단위가 됩니다. (Linux, Windows, macOS 등 현대 OS의 표준 방식)
- 한 스레드가
Blocked되어도, 커널은 프로세스 내의 다른 스레드를 스케줄링하여 실행할 수 있습니다. (멀티스레딩의 ‘응답성’ 장점을 극대화) 멀티코어 CPU에서 스레드들을 병렬로 실행하기에 용이합니다. - 스레드 생성 및 문맥 교환 시 반드시 커널 모드로 전환(시스템 콜)이 필요하므로, ULT보다 오버헤드가 큽니다.