In Veritate Libertas

The Restricted
Section

A collection of spells,
experiments, and forbidden knowledge.

Study/[ OS ] | 2026. 7. 15. 01:11

#12 Segmentation

728x90

힙이랑 스택 사이, 그 텅 빈 공간은 왜 낭비될까 — 세그멘테이션

base·bound 방식 하나로는 왜 메모리가 낭비되는지, 세그멘테이션이 그 문제를 어떻게 푸는지, 그리고 세그먼트별로 주소를 어떻게 변환하는지, 마지막으로 세그멘테이션이 낳는 새로운 문제(외부 단편화)까지 이 글로 같이 공부해보자!


기존 방식이 왜 비효율적일까

지난 글에서 본 base·bound 방식은 가상 주소 공간 전체를 물리 메모리에 통째로 하나의 덩어리로 배치했어. 근데 프로세스의 주소 공간을 들여다보면 Code, Heap은 아래쪽에 있고 Stack은 위쪽에 있고, 그 사이는 텅 비어있잖아. 이 방식은 이 텅 빈 부분까지도 물리 메모리에 자리를 잡아버려. 문제가 세 가지 생겨.

  • 안 쓰는 공간까지 자리를 차지하니까 메모리가 낭비돼
  • 주소 공간이 조금만 커져도(16KB 넘는 정도만 돼도) 물리 메모리가 부족해서 프로세스를 못 띄워
  • Code 영역이 프로세스마다 따로 자리 잡다 보니, 여러 프로세스가 똑같은 코드를 쓰는 경우에도 중복으로 공간을 차지해

이 세 가지를 풀려고 나온 아이디어가 세그멘테이션(Segmentation)이야.


세그멘테이션이 뭐야

가상 주소 공간을 통째로 하나로 매핑하는 게 아니라, 세그먼트 단위로 쪼개서 각각 독립적으로 물리 메모리에 매핑하는 방식이야. 일반적인 주소 공간은 Code, Heap, Stack 세 개의 세그먼트로 이뤄져있고, OS는 이 세 개를 물리 메모리 여기저기에 필요한 만큼만 배치해. 그러면 위에서 말한 문제가 이렇게 풀려.

  • 안 쓰는 중간 공간을 아예 매핑 안 하니까 메모리 낭비가 없어져
  • 훨씬 큰 주소 공간도 지원할 수 있게 돼
  • Code 세그먼트는 여러 프로세스가 공유할 수 있어서 메모리를 아껴

기존 방식이랑 세그멘테이션을 나란히 그려보면 차이가 확 보여.

왼쪽은 안 쓰는 중간 공간까지 물리 메모리를 차지하고 있지만, 오른쪽은 실제로 값이 있는 Stack이랑 Code+Heap만 각각 필요한 만큼 매핑돼. 가운데 빈 공간은 아예 물리 메모리에 자리를 안 잡으니까 그 공간을 다른 프로세스가 써도 되는 거야.

 

 

 

면접 포인트 ( •̀ ω •́ )✧

Q. 세그멘테이션은 무엇이고, 이로 인해 어떤 장점이 있는지 설명해 주세요.

더보기

세그멘테이션은 가상 주소 공간을 Code, Heap, Stack 같은 세그먼트 단위로 나눠서 각각 독립적으로 물리 메모리에 매핑하는 방식입니다. 기존 base·bound 방식은 힙과 스택 사이의 안 쓰는 공간까지 통째로 물리 메모리에 배치해서 낭비가 컸는데, 세그멘테이션은 실제로 쓰는 세그먼트만 매핑하기 때문에 메모리 낭비를 줄이고, 더 큰 주소 공간도 지원할 수 있고, Code 세그먼트를 여러 프로세스가 공유해서 메모리를 절약할 수 있습니다.


주소는 어떻게 변환할까

세그멘테이션에서 가상 주소는 segment ID + offset 조합이야. 상위 2비트가 segment ID, 나머지 하위 12비트가 offset이야.

세그먼트 테이블이 이렇게 돼있다고 해보자.

Segment  ID  Base  Bound
Code 00 30K 1K
Heap 01 26K 2K
Stack 10 36K 2K

가상 주소 01 000001101000(2진수)이 들어왔다고 해보자.

  1. 상위 2비트 01을 보고 Heap 영역을 찾아. Base는 26K야.
  2. 나머지 12비트 000001101000이 offset이야. 16진수로 보면 0x068이고, 10진수로 바꾸면 6×16 + 8 = 104야.
  3. 이 offset이 Bound를 안 넘는지 확인해. Heap의 Bound는 2K인데, 1K는 1024바이트니까 2K는 정확히 2048바이트야. 104는 2048보다 훨씬 작으니까 문제없이 통과해. 이 체크를 통과 못 하면 segmentation fault가 나 — 허용 안 된 메모리 영역에 접근하거나 허용 안 된 방식으로 접근할 때 발생하는 그 에러야.
  4. 물리 주소 = Base + Offset = 26KB + 104가 돼.

이 과정을 코드로 표현하면 이래.

// 상위 2비트로 Segment 번호를 구함
Segment = (VirtualAddress & SEG_MASK) >> SEG_SHIFT

// 하위 12비트로 offset을 구함
Offset = VirtualAddress & OFFSET_MASK

// offset이 bound를 넘는지 확인
if (Offset >= Bounds[Segment])
    RaiseException(PROTECTION_FAULT)
else
    PhysAddr = Base[Segment] + Offset
    Register = AccessMemory(PhysAddr)

 

 

Stack은 반대로 자란다

Stack은 Code, Heap이랑 다르게 위에서 아래로 자라. 그래서 Code, Heap의 Sign은 Positive(양수)인데, Stack의 Sign은 Negative(음수)야. Stack 세그먼트의 base는 가장 높은 주소에서 시작해.

Stack 예시로 한번 보자. 이번엔 Stack의 Bound를 2K가 아니라 3K로 잡아볼게 — 곧 나올 offset 값이 2K보다 커서, 2K 그대로면 계산해보기도 전에 바로 segmentation fault가 나버리거든. 그래서 이 예시에서만 Bound를 3K로 조금 넉넉하게 잡았다고 생각하면 돼.

  1. segment ID 10으로 Stack을 찾아. Base는 36K야.
  2. 나머지 12비트가 101000000000이고, 16진수로 0xA00, 10진수로는 10×256 = 2560이야.
  3. Bound(3K = 3072바이트)랑 비교해. 2560은 3072보다 작으니까 통과야.
  4. Stack은 아래로 자라니까 Physical address = Base Offset이야. 즉 36KB − 2560 = 34304바이트, KB로 바꾸면 33.5KB가 돼.

정리하면 Code·Heap은 Base + Offset, Stack은 Base − Offset으로 계산하는 게 핵심 차이야. 전체 흐름을 그려보면 이렇게 돼.

 

 

 

면접 포인트 ( •̀ ω •́ )✧

Q. 세그멘테이션으로 주소 변환하는 방법을 설명해 주세요.

더보기

가상 주소는 segment ID와 offset으로 나뉘고, segment ID로 세그먼트 테이블을 조회해서 base, bound, sign 값을 가져옵니다. offset이 bound보다 작은지 확인해서 넘으면 segmentation fault를 발생시키고, 통과하면 sign에 따라 계산 방식이 달라집니다. Code나 Heap처럼 sign이 양수인 세그먼트는 base에 offset을 더하고, Stack처럼 sign이 음수인 세그먼트는 base에서 offset을 빼서 물리 주소를 구합니다.


Code는 공유할 수 있다

세그먼트는 하드웨어 지원을 조금 더 받으면 여러 주소 공간 사이에서 공유될 수 있어. 그래서 여러 프로세스가 Code 세그먼트를 같이 쓸 수 있는데, 읽고 실행하는 건 되지만 값을 바꾸는 건 안 돼. 그러니까 코드 영역은 R-X(Read-Execute) 권한만 주고, Write 권한은 안 주는 거야. 이걸 위해 세그먼트 테이블에 Permission 컬럼이 하나 더 붙어.

두 프로세스가 같은 Code를 공유하는 예시를 보면:

 

Process 1의 세그먼트 테이블

Segment  ID  Base  Bound  Sign  Permission
Code 00 48K 1K Pos. R-X
Heap 01 46K 2K Pos. RW-
Stack 10 54K 2K Neg. RW-

 

Process 2의 세그먼트 테이블

Segment  ID  Base  Bound  Sign  Permission
Code 00 48K 1K Pos. R-X
Heap 01 24K 2K Pos. RW-
Stack 10 32K 2K Neg. RW-

두 프로세스의 Code 세그먼트가 정확히 같은 Base(48K), 같은 Bound(1K)를 가리키고 있는 거 보이지? 물리 메모리에도 Code는 딱 한 곳(48K)에만 존재하고, 두 프로세스의 세그먼트 테이블이 똑같은 그 위치를 같이 가리키는 거야. Heap이랑 Stack은 프로세스마다 다른 값이니까 각자 따로 배치되고.


세그멘테이션이 남긴 숙제

세그멘테이션도 실제로 OS가 처리해야 하는 문제가 몇 가지 있어.

 

Context Switch는 어떻게 처리해?

세그먼트 레지스터(base·bound 값들)를 Context Switch 시점에 저장하고 복원하면 돼. 지난 글에서 본 base·bound 방식이랑 똑같은 원리인데, 세그먼트가 여러 개니까 그만큼 저장/복원해야 할 레지스터 쌍도 늘어나는 거야.

 

세그먼트 크기가 늘어나거나 줄어들면?

예를 들어 Heap 공간이 부족해서 malloc()을 호출했는데 더 이상 힙에 여유가 없는 상황을 생각해보자. 이럴 때 힙을 늘려주는 시스템콜이 sbrk()야. sbrk()는 힙 영역을 늘리거나 줄여주는 함수고, 이걸 통해 세그먼트 크기 자체를 동적으로 조정할 수 있어.

 

물리 메모리 여유 공간은 어떻게 관리해?

여기서 세그멘테이션의 진짜 약점이 드러나. 프로세스마다 세그먼트 크기가 다 다르다 보니, 프로세스들이 물리 메모리에 들어갔다 나갔다 하는 과정에서 자잘한 빈 공간들이 여기저기 흩어져. 이걸 외부 단편화(external fragmentation)라고 불러 — 말 그대로 여유 공간이 자잘하게 쪼개진 상태를 말해.

예를 들어 20KB짜리 세그먼트를 새로 할당하려는데, 전체 여유 공간을 다 더하면 24KB가 있지만 16KB, 4KB, 4KB로 뿔뿔이 흩어져 있다면? 어느 한 조각도 20KB를 못 채우니까 할당이 실패해.

 

이걸 풀려고 나온 방법이 Compaction이야. 기존 세그먼트들을 재배치해서 빈 공간들을 한쪽으로 몰아주는 거지. 근데 이건 메모리에 있는 데이터를 통째로 복사해서 옮기는 작업이라 비용이 커서 성능이 떨어져.

그래서 결국 나온 게 Paging이야. 세그먼트를 가변 크기로 두는 대신, 아예 고정 크기(fixed-size) 블록으로 쪼개서 관리하면 외부 단편화 자체가 생기질 않아. 세 가지를 나란히 보면 이래.

Compaction은 흩어진 걸 한쪽으로 밀어서 24KB짜리 연속 공간을 만들어주지만, 데이터를 통째로 복사하는 비용이 크다는 게 흠이야. Paging은 애초에 고정 크기 블록으로만 쪼개서 관리하니까 이런 재배치 자체가 필요 없어져.

근데 Paging도 공짜는 아니야. **내부 단편화(internal fragmentation)**라는 새로운 문제가 생겨. 예를 들어 15KB를 요청했는데 고정 크기가 8KB 단위라서 8KB짜리 두 개, 총 16KB를 줘버리면 1KB가 그냥 버려지는 거야. 이 1KB가 내부 단편화야.

 

 

 

면접 포인트 ( •̀ ω •́ )✧

Q. 세그멘테이션의 문제점과 해결 방안은 무엇인지 설명해 주세요.

더보기

세그먼트마다 크기가 제각각이라 프로세스들이 물리 메모리에 들어왔다 나가는 과정에서 여유 공간이 자잘하게 흩어지는 외부 단편화가 발생합니다. 예를 들어 전체 여유 공간은 충분해도 연속된 공간이 없어서 큰 세그먼트를 할당하지 못하는 상황이 생깁니다. Compaction으로 기존 세그먼트를 재배치해서 연속 공간을 만들 수 있지만 비용이 커서 성능이 떨어지고, 결국 고정 크기 블록으로 나누는 Paging 기법으로 외부 단편화 자체를 없애는 방향으로 발전합니다.


세그멘테이션은 힙이랑 스택 사이의 안 쓰는 공간을 없애서 메모리 효율을 크게 끌어올렸어. 근데 그 대가로 외부 단편화라는 새로운 골칫거리가 생겼고, 이걸 풀기 위해 다음 글에서 다룰 Paging이 등장하게 돼.

꼭 기억할 것 하나만 꼽으면 세그멘테이션은 "필요한 만큼만 매핑한다"는 아이디어로 낭비를 줄였지만, 그 대가로 세그먼트 크기가 제각각이라는 문제(외부 단편화)를 떠안았어. 그리고 이 문제를 고정 크기라는 완전히 다른 접근으로 푸는 게 Paging이야. "가변 크기의 효율성 vs 고정 크기의 단순함"이라는 이 트레이드오프가, 사실 다음 글에서 계속 따라다니는 핵심 주제야.

728x90

'Study > [ OS ]' 카테고리의 다른 글

#14 변환참조버퍼 TLB _ 기본편  (0) 2026.07.15
#13 페이징 (Paging)  (0) 2026.07.15
#11 주소 변환(Address Translation)  (0) 2026.07.15
#10 주소 공간과 가상 메모리  (0) 2026.07.06
#09 교착상태 (DeadLock)  (0) 2026.07.06
흐이서

흐이서

Writer

공순이 흐이서