각 스레드마다 별도 스택
Source: Dev.to
번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.
Core Concept
각 스레드는 자체 스택이 필요하지만, 프로세스 내의 스레드들은 코드, 데이터, 그리고 힙을 공유합니다. OS 커널이 모든 것을 조정합니다.
프로세스란?
프로세스는 다른 프로세스와 격리된 실행 중인 프로그램입니다.
Chrome, Spotify 등을 열면 OS가 프로세스를 생성합니다. 프로세스는 하나의 스레드( 메인 스레드 )로 시작하며 자체 메모리 공간을 가집니다. 커널은 프로세스에 대한 모든 정보를 추적하기 위해 프로세스 제어 블록 (PCB) 을 생성합니다.
스레드란 무엇인가?
스레드는 프로세스 내의 실행 경로입니다.
하나의 프로세스에 여러 스레드가 존재할 수 있으며 코드와 데이터를 공유하지만 각 스레드는 자신만의 스택을 가져야 합니다. 커널은 각 스레드의 상태를 추적하기 위해 **스레드 제어 블록(Thread Control Block, TCB)**을 생성합니다.
프로세스 메모리 레이아웃
| Segment | Type | Shared? | Notes |
|---|---|---|---|
| 코드 | 컴파일된 프로그램 명령 | ✅ All threads | 읽기 전용, 고정 크기 |
| 데이터 | 전역 변수 | ✅ All threads | 고정 크기, 초기화됨 |
| 힙 | 동적 메모리 (malloc/new) | ✅ All threads | 위쪽으로 성장, 프로그래머가 관리 |
| 스택 | 지역 변수, 함수 호출 | ❌ Per thread | 아래쪽으로 성장, 스레드마다 격리됨 |
Why Separate Stacks?
If two threads shared one stack, their function calls would collide and corrupt each other’s data.
Thread A calls a function → creates a stack frame.
Thread B calls a function → adds its frame to the same stack → frames overlap → data is overwritten.
When Thread B returns and pops its frame, Thread A’s data becomes corrupted.
Solution: each thread gets its own stack for its function calls and local variables. This allows threads to execute different functions simultaneously without interference.
스택 프레임과 LIFO
스택 프레임은 LIFO(Last‑In‑First‑Out) 순서를 따릅니다:
main() → functionA() → functionB()
main()이functionA()를 호출합니다 → 새로운 프레임이 푸시됩니다.functionA()가functionB()를 호출합니다 → 또 다른 프레임이 푸시됩니다.functionB()가 반환합니다 → 해당 프레임이 팝되어functionA()의 프레임이 드러납니다.functionA()가 반환합니다 → 해당 프레임이 팝되어main()으로 돌아갑니다.
각 스레드마다 자체 스택이 존재하므로, 여러 스레드가 동시에 같은 함수를 호출해도 서로의 프레임에 영향을 주지 않습니다.
스레드 생성: 메인 vs. 서브‑스레드
- 프로세스가 시작될 때, OS는 자동으로 하나의 스레드 – 메인 스레드 – 를 생성하며, 이는
main()에서 실행을 시작합니다. - 메인 스레드는
pthread_create()를 사용하여 추가 스레드를 만들 수 있습니다. 이러한 스레드를 서브‑스레드라고 부릅니다. - 생성된 후, 모든 스레드는 커널 관점에서 동등한 피어가 되며, 메인 스레드는 더 이상 특별한 권한을 갖지 않습니다.
스레드 계층 구조
Process (created by OS)
↓
Main Thread (created automatically by kernel)
├─→ Sub‑thread 1 (created by main thread)
├─→ Sub‑thread 2 (created by any thread)
└─→ Sub‑thread 3 (created by any thread)
각 서브‑스레드는 다음을 받습니다:
- 자체 스택 (커널에 의해 할당)
- 자체 TCB (Thread Control Block)
- 프로세스의 코드, 데이터 및 힙에 대한 공유 접근
필요에 따라 서브‑스레드가 더 많은 서브‑스레드를 생성할 수도 있습니다.

Music Player Example
| 스레드 | 역할 |
|---|---|
| 메인 스레드 | UI와 사용자 상호작용을 처리합니다 |
| 오디오 스레드 | 오디오를 지속적으로 디코딩하고 재생합니다 |
| I/O 스레드 | 파일 시스템에서 곡을 로드합니다 |
| 타이머 스레드 | 재생 타이머 표시를 업데이트합니다 |
모든 스레드는 플레이어의 코드와 데이터(곡 목록, 설정 등)를 공유하지만, 각 스레드는 로컬 변수와 함수 실행을 위한 자체 스택을 가집니다. 커널은 이들 사이를 빠르게 전환하며 각 스레드에 시간 슬라이스를 할당하여 작업이 동시에 실행되는 것처럼 보이게 합니다.
OS 커널: 중앙 오케스트레이터
커널은 운영 체제의 중앙 관리자입니다. 프로세스와 스레드의 모든 자원 할당 및 조정을 담당합니다.

프로세스 관리
- 커널은 각 프로세스마다 **프로세스 제어 블록 (PCB)**을 생성합니다.
- PCB는 프로세스 ID, 상태(실행 중, 대기, 준비 등), 메모리 레이아웃, 파일 디스크립터, 시그널 핸들러 및 기타 메타데이터를 저장합니다.
- 이 정보는 커널이 각 프로세스를 관리하고, 격리하며, 보안할 수 있게 합니다.
스레드 관리
- 각 스레드마다 커널은 **스레드 제어 블록 (TCB)**을 생성합니다.
- TCB는 스레드 ID, 상태, CPU 레지스터(프로그램 카운터, 스택 포인터), 스택 주소 및 크기, 스레드‑로컬 스토리지, 스케줄링 정보를 보유합니다.
- 커널은 TCB를 사용해 스레드를 올바르게 스케줄링하고 관리합니다.
메모리 관리
- 커널은
mmap()과 같은 시스템 콜을 사용해 스레드 스택에 메모리를 할당합니다. - 각 스택에 대해 가상 주소 공간을 예약하여 스택이 겹치지 않도록 하고, 각 스레드가 프레임을 위한 안전한 영역을 갖도록 합니다.
Thread Stack
커널은 스택 경계에 가드 페이지를 생성하여 스택 오버플로우 상황을 감지합니다.
스레드가 실제로 스택을 사용할 때, 커널은 물리 메모리를 필요에 따라 할당합니다.
CPU 스케줄링
커널은 어떤 스레드가 어느 CPU 코어에서, 얼마나 오래 실행될지를 결정합니다.
스케줄링 알고리즘을 사용해 모든 스레드에 CPU 시간을 공정하게 분배합니다.
스레드의 타임 슬라이스가 만료되거나 I/O 대기가 필요할 때, 커널은 context switch 를 수행합니다.
컨텍스트 스위칭
컨텍스트 스위치가 발생할 때 커널은:
- 현재 스레드의 CPU 상태(프로그램 카운터, 스택 포인터 및 모든 CPU 레지스터)를 스레드의 TCB에 저장합니다.
- 다음 스레드의 저장된 상태를 해당 TCB에서 CPU 레지스터로 로드합니다.
- 복원된 프로그램 카운터에서 실행을 재개하여 제어를 다음 스레드로 전달합니다.
재개된 스레드는 마치 중단된 적이 없었던 것처럼 계속 실행됩니다.
기본 스택 크기 (OS별)
(이 값들은 가상 주소 공간을 의미하며, 물리적 RAM을 의미하지 않습니다.)
| OS / 런타임 | 기본 스택 크기 | 비고 |
|---|---|---|
| Linux | 스레드당 8 MB | 예약된 가상 공간; 물리적 RAM은 필요에 따라 할당됩니다. |
| Windows | 스레드당 1 MB | 안정성과 자원 사용의 균형을 맞춥니다. |
| macOS | 스레드당 512 KB | 메모리를 보다 보수적으로 사용합니다. |
| Java | 스레드당 1 MB | JVM에 의해 관리됩니다. |
| Go | goroutine당 ≈ 2 KB | goroutine 스택은 동적으로 성장/축소되며, OS가 아니라 Go 런타임이 관리합니다. |
왜 다른 크기인가?
- Linux (8 MB) – 복잡한 애플리케이션에서 깊은 재귀와 큰 로컬 변수를 지원합니다.
- Windows (1 MB) – 안정성과 메모리 사용량 사이의 절충을 제공합니다.
- macOS (512 KB) – 자원 효율성에 중점을 둔 설계 철학을 반영합니다.
- Go (≈ 2 KB) – 작은 스택도 Go 런타임이 메모리와 컨텍스트 전환을 커널보다 훨씬 효율적으로 처리하기 때문에 가능합니다.
Virtual vs. Physical Memory
커널이 스레드에 대해 8 MB의 스택 공간을 예약할 때, 가상 주소 공간만 예약합니다.
물리 RAM은 필요에 따라 (요구 페이징) 할당됩니다.
예시: 스레드가 8 MB 스택 중 100 KB만 사용한다면 실제로 소비되는 RAM은 약 100 KB에 불과하고, 나머지 7.9 MB는 사용되지 않은 채로 남습니다.
이 방식은 커널이 수백 개 또는 수천 개의 스레드가 있는 시스템에서도 메모리를 낭비하지 않고 많은 스택을 할당할 수 있게 해 줍니다.
Kernel Data Structures
Process Control Block (PCB)
- 프로세스 ID
- 프로세스 상태
- 메모리 레이아웃 (코드, 데이터, 힙, 스택)
- 파일 디스크립터 테이블
- 시그널 핸들러
- 프로세스 내 모든 스레드 목록
PCB는 커널이 프로세스의 수명 주기와 자원을 관리하도록 합니다.
Thread Control Block (TCB)
- 스레드 ID
- 스레드 상태
- CPU 레지스터 (PC, SP 등)
- 스레드의 user‑mode stack 주소와 크기
- 스레드‑로컬 저장소 정보
- 스케줄링 우선순위
- kernel stack에 대한 포인터
TCB는 컨텍스트 스위치 시 스레드 상태를 저장하고 복원하는 데 사용됩니다.
Kernel Stack vs. User Stack
| Stack | Purpose | Location |
|---|---|---|
| User‑mode stack | 사용자 코드의 로컬 변수, 함수 매개변수, 반환 주소를 보관합니다. | 프로세스의 가상 주소 공간 (예: Linux에서는 8 MB) |
| Kernel‑mode stack | 시스템 콜, 인터럽트 등을 처리할 때 커널이 사용합니다. | 커널 메모리 (사용자 코드로부터 보호됨) |
스레드가 시스템 콜(read() 또는 write() 등)을 호출하면 CPU가 커널 모드로 전환되고, 해당 스레드의 커널 스택 포인터를 로드한 뒤 그 스택 위에서 커널 코드를 실행합니다. 호출이 끝나면 CPU가 사용자 모드로 돌아가고 사용자‑mode 스택 포인터를 복원합니다.
주요 요점
- 프로세스는 격리됩니다 – 각각 고유한 보호 메모리 공간을 가집니다.
- 스레드는 자원을 공유합니다 – 코드, 데이터, 힙이 프로세스 내 모든 스레드에 공통입니다.
- 각 스레드는 자체 스택을 가집니다 – 프로세스 내에서 스레드당 유일한 메모리 영역입니다.
- 메인 스레드는 초기 단계에서만 특별합니다 –
main()에서 시작하지만, 생성 후 모든 스레드는 동등한 동료가 됩니다. - 스레드 생성 –
pthread_create()(또는 동등한 API)를 통해 수행됩니다. - 커널이 모든 스택을 할당합니다 – 기본 크기는 OS마다 다릅니다(리눅스 8 MB, 윈도우 1 MB, macOS 512 KB).
- 커널이 스케줄링을 관리합니다 – 어떤 스레드가 실행될지, 얼마나 오래 실행될지, 어느 CPU 코어에서 실행될지를 결정합니다.
- 컨텍스트 스위칭이 멀티태스킹을 가능하게 합니다 – 빠른 전환으로 단일 코어에서도 병렬 실행처럼 보이게 합니다.
- 스택 프레임은 LIFO를 구현합니다 – 함수 호출 시 프레임을 푸시하고, 반환 시 팝합니다.
- 요구 페이징이 RAM을 절약합니다 – 가상 스택 공간은 실제 사용될 때만 물리 메모리를 차지합니다.
- TCB가 모든 스레드 상태를 추적합니다 – 커널은 TCB를 사용해 스레드의 전체 수명 동안 관리합니다.
이 문제를 이해하기
운영 체제 수준에서 스레드가 어떻게 작동하는지를 이해하는 것은 컴퓨터 과학의 기본입니다. 이는 코드가 실제로 하드웨어 수준에서 어떻게 실행되는지를 보여줍니다. 단순히 스레딩 API를 사용하는 것이 아니라, 동시 프로그래밍을 가능하게 하는 실제 메커니즘을 이해하는 것입니다. 이러한 기반은 효율적이고 안전한 멀티스레드 애플리케이션을 구축하고, Go의 고루틴과 같은 고급 동시성 개념을 파악하는 데 필수적입니다.