본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 28. · 2 Views
페이징과 세그멘테이션 완벽 가이드
운영체제의 메모리 관리 핵심 기법인 페이징과 세그멘테이션을 다룹니다. 가상 메모리가 물리 메모리로 변환되는 과정부터 TLB, 다단계 페이지 테이블까지 차근차근 이해할 수 있습니다.
목차
1. 페이징 기법
어느 날 김개발 씨가 대용량 데이터를 처리하는 프로그램을 실행했습니다. 분명히 컴퓨터 RAM은 8GB인데, 프로그램은 마치 무한한 메모리가 있는 것처럼 동작합니다.
"어떻게 이게 가능한 거지?" 김개발 씨는 고개를 갸웃거렸습니다.
페이징은 메모리를 고정된 크기의 블록으로 나누어 관리하는 기법입니다. 마치 아파트 단지에서 모든 집이 동일한 평수로 설계되어 있어 관리가 편리한 것과 같습니다.
이 기법을 이해하면 운영체제가 어떻게 효율적으로 메모리를 할당하고 관리하는지 알 수 있습니다.
다음 코드를 살펴봅시다.
// 페이징 주소 변환 시뮬레이션
public class PagingSimulator {
private static final int PAGE_SIZE = 4096; // 4KB 페이지
private int[] pageTable; // 페이지 테이블
// 가상 주소를 물리 주소로 변환
public int translateAddress(int virtualAddress) {
int pageNumber = virtualAddress / PAGE_SIZE; // 페이지 번호 추출
int offset = virtualAddress % PAGE_SIZE; // 오프셋 추출
int frameNumber = pageTable[pageNumber]; // 프레임 번호 조회
int physicalAddress = frameNumber * PAGE_SIZE + offset;
return physicalAddress; // 물리 주소 반환
}
}
김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘 운영체제 스터디 모임에서 메모리 관리에 대해 발표해야 하는데, 페이징이라는 개념이 도무지 이해되지 않았습니다.
책을 펼쳐보아도 "가상 메모리를 고정 크기 페이지로 분할한다"는 설명만 반복될 뿐이었습니다. 마침 선배 개발자 박시니어 씨가 커피를 마시러 나왔습니다.
"페이징이요? 아, 그거 아파트 단지 생각하면 돼요." 그렇다면 페이징이란 정확히 무엇일까요?
쉽게 비유하자면, 페이징은 마치 대형 아파트 단지와 같습니다. 모든 집이 똑같은 크기로 설계되어 있어서 어떤 세대가 이사를 가더라도 새로운 세대가 바로 들어올 수 있습니다.
집마다 크기가 다르면 맞는 크기의 집을 찾느라 곤란하겠지만, 모든 집이 같은 크기면 빈 집 아무데나 배정하면 됩니다. 페이징이 없던 시절에는 어땠을까요?
초창기 컴퓨터는 연속 메모리 할당 방식을 사용했습니다. 프로그램이 100MB가 필요하면 연속된 100MB 공간을 찾아야 했습니다.
문제는 시간이 지나면서 외부 단편화가 발생한다는 것이었습니다. 여기저기 빈 공간이 있어도 연속되지 않아서 사용할 수 없는 상황이 벌어졌습니다.
바로 이런 문제를 해결하기 위해 페이징이 등장했습니다. 페이징은 가상 메모리를 동일한 크기의 페이지로, 물리 메모리를 동일한 크기의 프레임으로 나눕니다.
보통 4KB 단위를 사용합니다. 페이지와 프레임은 크기가 같으므로 어떤 페이지든 어떤 프레임에나 들어갈 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 가상 주소가 주어지면 PAGE_SIZE로 나누어 페이지 번호를 구합니다.
나머지 연산으로는 오프셋을 구합니다. 페이지 번호는 "몇 번째 페이지인가"를, 오프셋은 "페이지 내에서 몇 번째 위치인가"를 나타냅니다.
페이지 테이블에서 해당 페이지 번호에 매핑된 프레임 번호를 찾습니다. 그리고 프레임 번호에 PAGE_SIZE를 곱하고 오프셋을 더하면 최종 물리 주소가 완성됩니다.
실제 현업에서는 어떻게 활용할까요? JVM에서 대용량 힙 메모리를 사용할 때, 운영체제는 페이징을 통해 실제 물리 메모리보다 큰 가상 메모리를 제공합니다.
16GB 메모리 컴퓨터에서 32GB 힙을 설정해도 프로그램이 동작할 수 있는 이유가 바로 페이징 덕분입니다. 하지만 주의할 점도 있습니다.
페이지 크기가 너무 작으면 페이지 테이블이 거대해지고, 너무 크면 내부 단편화가 발생합니다. 예를 들어 4KB 페이지에 1KB만 사용하면 3KB가 낭비됩니다.
적절한 균형이 필요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 환하게 웃었습니다. "아, 그래서 메모리가 무한한 것처럼 느껴졌군요!"
실전 팁
💡 - 페이지 크기는 보통 4KB이며, 대용량 메모리 시스템에서는 2MB나 1GB의 대형 페이지를 사용하기도 합니다
- 페이지 번호와 오프셋의 비트 수는 페이지 크기에 따라 결정됩니다 (4KB = 12비트 오프셋)
2. 페이지 테이블 구조
김개발 씨가 페이징 개념을 이해하고 나니 또 다른 의문이 생겼습니다. "페이지 번호로 프레임 번호를 찾는다고 했는데, 그 매핑 정보는 어디에 저장되어 있는 거죠?" 박시니어 씨가 화이트보드에 표 하나를 그리기 시작했습니다.
페이지 테이블은 가상 페이지 번호와 물리 프레임 번호의 매핑 정보를 저장하는 자료구조입니다. 마치 호텔 프런트에서 손님 이름과 객실 번호를 연결하는 장부와 같습니다.
각 프로세스마다 자신만의 페이지 테이블을 가지고 있어 메모리 격리가 가능합니다.
다음 코드를 살펴봅시다.
// 페이지 테이블 엔트리 구조
public class PageTableEntry {
private int frameNumber; // 프레임 번호 (20비트)
private boolean validBit; // 유효 비트 - 메모리에 존재하는지
private boolean dirtyBit; // 수정 비트 - 변경되었는지
private boolean referenceBit; // 참조 비트 - 최근 접근했는지
private int protectionBits; // 보호 비트 - 읽기/쓰기/실행 권한
public boolean isValid() { return validBit; }
public boolean isDirty() { return dirtyBit; }
public void access() {
this.referenceBit = true; // 접근 시 참조 비트 설정
}
public void modify() {
this.dirtyBit = true; // 수정 시 더티 비트 설정
}
}
김개발 씨는 페이징의 기본 원리를 이해했지만, 아직 풀리지 않는 의문이 있었습니다. 수백만 개의 페이지를 관리하려면 엄청난 양의 매핑 정보가 필요할 텐데, 이걸 어떻게 효율적으로 관리할까요?
박시니어 씨가 설명을 이어갔습니다. "페이지 테이블이라는 게 있어요.
호텔 프런트의 숙박 장부라고 생각하면 됩니다." 그렇다면 페이지 테이블이란 정확히 무엇일까요? 쉽게 비유하자면, 페이지 테이블은 마치 도서관의 대출 기록부와 같습니다.
회원 번호(페이지 번호)로 검색하면 현재 어떤 책장 위치(프레임 번호)에 책이 있는지 알 수 있습니다. 더 나아가 언제 빌려갔는지, 반납 기한은 언제인지 등 부가 정보도 함께 기록되어 있습니다.
페이지 테이블의 각 항목을 **페이지 테이블 엔트리(PTE)**라고 합니다. PTE에는 프레임 번호 외에도 여러 제어 비트가 포함됩니다.
가장 중요한 것은 유효 비트입니다. 이 비트가 0이면 해당 페이지가 현재 물리 메모리에 없다는 뜻입니다.
디스크의 스왑 영역에 있거나 아직 할당되지 않은 상태입니다. 더티 비트는 페이지 내용이 수정되었는지 표시합니다.
페이지를 디스크로 내보낼 때 더티 비트가 설정되어 있으면 반드시 디스크에 기록해야 합니다. 반대로 더티 비트가 없으면 그냥 버려도 됩니다.
나중에 필요하면 디스크에서 다시 읽어오면 되니까요. 참조 비트는 페이지 교체 알고리즘에서 사용됩니다.
LRU(Least Recently Used) 같은 알고리즘은 최근에 사용되지 않은 페이지를 우선적으로 내보냅니다. 참조 비트를 주기적으로 초기화하고 확인하면 어떤 페이지가 자주 사용되는지 파악할 수 있습니다.
보호 비트는 접근 권한을 제어합니다. 읽기만 가능한지, 쓰기도 가능한지, 실행할 수 있는 코드인지를 표시합니다.
코드 영역에 쓰기를 시도하면 운영체제가 이를 감지하고 프로세스를 종료시킵니다. 이것이 메모리 보호의 기본 원리입니다.
실제 현업에서 이 개념이 어떻게 적용될까요? Java 애플리케이션에서 NullPointerException이 발생하면, 내부적으로는 유효하지 않은 페이지에 접근하려다 페이지 폴트가 발생한 것입니다.
운영체제가 이를 JVM에게 시그널로 전달하고, JVM이 이를 Java 예외로 변환합니다. 하지만 페이지 테이블에도 문제가 있습니다.
32비트 시스템에서 4KB 페이지를 사용하면 페이지 테이블에 약 100만 개의 엔트리가 필요합니다. 각 엔트리가 4바이트라면 프로세스 하나당 4MB의 페이지 테이블이 필요합니다.
64비트 시스템에서는 이 문제가 더욱 심각해집니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
"그럼 페이지 테이블 자체도 메모리를 많이 차지하겠네요?" 박시니어 씨가 고개를 끄덕이며 말했습니다. "맞아요, 그래서 다단계 페이지 테이블이라는 기법이 나왔죠."
실전 팁
💡 - 페이지 테이블은 각 프로세스마다 별도로 존재하며, 프로세스 전환 시 운영체제가 페이지 테이블 베이스 레지스터를 갱신합니다
- 유효 비트가 0인 페이지에 접근하면 페이지 폴트가 발생하고, 운영체제가 해당 페이지를 디스크에서 로드합니다
3. TLB 변환 색인 버퍼
김개발 씨가 페이지 테이블을 이해하고 나니 성능에 대한 걱정이 생겼습니다. "메모리에 접근할 때마다 페이지 테이블도 읽어야 하면, 메모리 접근이 두 배로 느려지는 거 아닌가요?" 박시니어 씨가 미소를 지었습니다.
"좋은 질문이에요. 그래서 TLB라는 게 있습니다."
**TLB(Translation Lookaside Buffer)**는 최근에 사용된 페이지 테이블 엔트리를 캐싱하는 하드웨어 장치입니다. 마치 자주 가는 식당 전화번호를 스마트폰 즐겨찾기에 저장해두는 것과 같습니다.
TLB 덕분에 대부분의 주소 변환은 메모리 접근 없이 빠르게 처리됩니다.
다음 코드를 살펴봅시다.
// TLB 시뮬레이션
public class TLBSimulator {
private static final int TLB_SIZE = 64; // TLB 엔트리 수
private Map<Integer, Integer> tlbCache = new LinkedHashMap<>();
private int[] pageTable;
public int translateWithTLB(int pageNumber) {
// TLB 히트 - 빠른 경로
if (tlbCache.containsKey(pageNumber)) {
return tlbCache.get(pageNumber); // 캐시에서 바로 반환
}
// TLB 미스 - 페이지 테이블 조회 필요
int frameNumber = pageTable[pageNumber];
// TLB 갱신 (LRU 방식)
if (tlbCache.size() >= TLB_SIZE) {
tlbCache.remove(tlbCache.keySet().iterator().next());
}
tlbCache.put(pageNumber, frameNumber);
return frameNumber;
}
}
김개발 씨의 질문은 정확히 핵심을 찌른 것이었습니다. 페이지 테이블이 메모리에 있다면, 데이터를 읽기 위해 먼저 페이지 테이블을 읽고 그 다음 실제 데이터를 읽어야 합니다.
메모리 접근 횟수가 두 배가 되는 것입니다. 박시니어 씨가 CPU 구조 다이어그램을 꺼내 들었습니다.
"이것 봐요. CPU 안에 TLB라는 특별한 캐시가 있어요." 그렇다면 TLB란 정확히 무엇일까요?
쉽게 비유하자면, TLB는 마치 단골 손님 명단과 같습니다. 식당 직원이 자주 오는 손님의 선호 메뉴와 테이블을 기억하고 있으면, 손님이 올 때마다 예약 장부를 뒤질 필요가 없습니다.
바로 안내할 수 있습니다. TLB는 연관 메모리(Associative Memory) 또는 **CAM(Content-Addressable Memory)**으로 구현됩니다.
일반 메모리는 주소를 주면 데이터를 반환하지만, 연관 메모리는 데이터의 일부(페이지 번호)를 주면 일치하는 항목을 찾아줍니다. 모든 엔트리를 동시에 비교하므로 매우 빠릅니다.
대신 하드웨어 비용이 비싸서 보통 64~1024개 정도의 엔트리만 저장합니다. TLB의 동작 과정을 살펴보겠습니다.
CPU가 가상 주소를 보내면 먼저 TLB를 검색합니다. 해당 페이지 번호가 TLB에 있으면 TLB 히트라고 하며, 바로 프레임 번호를 얻습니다.
없으면 TLB 미스라고 하며, 페이지 테이블을 조회해야 합니다. TLB 히트율이 성능의 핵심입니다.
현대 시스템에서 TLB 히트율은 보통 99% 이상입니다. 왜 그럴까요?
지역성(Locality) 때문입니다. 프로그램은 최근에 접근한 메모리 근처를 다시 접근하는 경향이 있습니다.
4KB 페이지 안에서 많은 작업이 이루어지므로, 한 번 TLB에 올라온 엔트리는 여러 번 재사용됩니다. 프로세스가 전환되면 어떻게 될까요?
각 프로세스는 자신만의 페이지 테이블을 가지므로, 프로세스가 바뀌면 TLB 내용도 무효화됩니다. 이것을 TLB 플러시라고 합니다.
빈번한 프로세스 전환은 TLB 미스를 증가시켜 성능에 악영향을 줍니다. 현대 CPU는 **ASID(Address Space ID)**를 사용합니다.
각 TLB 엔트리에 프로세스 식별자를 함께 저장하면 프로세스 전환 시에도 TLB를 비우지 않아도 됩니다. 다른 프로세스의 엔트리는 ASID가 다르므로 자연스럽게 무시됩니다.
실제 현업에서 TLB가 중요한 이유가 있습니다. 고성능 서버 애플리케이션에서 **대형 페이지(Huge Pages)**를 사용하는 이유 중 하나가 TLB 효율성입니다.
2MB 대형 페이지를 사용하면 같은 TLB 엔트리 수로 512배 많은 메모리를 커버할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
"아, 그래서 메모리 접근이 두 배로 느려지지 않는군요!" 박시니어 씨가 덧붙였습니다. "맞아요.
TLB 덕분에 대부분의 경우 추가 비용 없이 주소 변환이 가능해요."
실전 팁
💡 - TLB 미스 비용을 줄이려면 데이터 지역성을 높이는 코드를 작성하세요. 연속된 메모리를 순차적으로 접근하는 것이 좋습니다
- Java에서 대용량 메모리를 사용할 때는 -XX:+UseLargePages 옵션으로 대형 페이지를 활성화하면 TLB 효율이 높아집니다
4. 다단계 페이지 테이블
김개발 씨가 TLB까지 이해하고 나니, 아까 잠깐 언급된 질문이 다시 떠올랐습니다. "그런데 64비트 시스템에서는 페이지 테이블이 어마어마하게 커지지 않나요?" 박시니어 씨가 고개를 끄덕였습니다.
"바로 그 문제를 해결한 게 다단계 페이지 테이블이에요."
다단계 페이지 테이블은 하나의 거대한 테이블 대신 여러 단계의 작은 테이블을 계층적으로 구성하는 방식입니다. 마치 회사 조직도처럼 본부, 팀, 개인 순으로 찾아가는 구조입니다.
사용하지 않는 주소 공간에 대한 테이블을 만들지 않아 메모리를 크게 절약합니다.
다음 코드를 살펴봅시다.
// 2단계 페이지 테이블 시뮬레이션
public class TwoLevelPageTable {
private static final int PAGE_SIZE = 4096;
private static final int ENTRIES_PER_TABLE = 1024;
private int[][] pageDirectory; // 1단계: 페이지 디렉토리
// pageDirectory[i]가 null이면 해당 영역 미할당
public int translate(int virtualAddress) {
int directoryIndex = (virtualAddress >> 22) & 0x3FF; // 상위 10비트
int tableIndex = (virtualAddress >> 12) & 0x3FF; // 중간 10비트
int offset = virtualAddress & 0xFFF; // 하위 12비트
// 1단계 조회
int[] pageTable = pageDirectory[directoryIndex];
if (pageTable == null) throw new PageFaultException();
// 2단계 조회
int frameNumber = pageTable[tableIndex];
return frameNumber * PAGE_SIZE + offset;
}
}
김개발 씨가 계산기를 꺼내 들었습니다. 64비트 시스템에서 4KB 페이지를 사용하면 페이지 번호가 52비트입니다.
페이지 테이블 엔트리 수가 2의 52승, 각 엔트리가 8바이트라면... "페이지 테이블 하나에 32페타바이트가 필요하다고요?!" 박시니어 씨가 웃으며 말했습니다.
"그래서 다단계 페이지 테이블이 필요한 거예요." 그렇다면 다단계 페이지 테이블은 어떻게 이 문제를 해결할까요? 쉽게 비유하자면, 다단계 페이지 테이블은 마치 우편 주소 체계와 같습니다.
"대한민국 서울특별시 강남구 테헤란로 123"을 찾을 때, 전 세계 모든 주소를 하나의 목록에 담아두지 않습니다. 먼저 국가 목록에서 대한민국을 찾고, 대한민국의 도시 목록에서 서울을 찾고, 서울의 구 목록에서 강남구를 찾아갑니다.
핵심 아이디어는 계층적 분할입니다. 32비트 가상 주소를 예로 들어보겠습니다.
상위 10비트는 페이지 디렉토리 인덱스, 중간 10비트는 페이지 테이블 인덱스, 하위 12비트는 오프셋입니다. 이렇게 나누면 각 테이블은 1024개의 엔트리만 가지면 됩니다.
왜 이 방식이 메모리를 절약할까요? 비밀은 희소성에 있습니다.
일반적인 프로그램은 4GB 주소 공간 중 극히 일부만 사용합니다. 다단계 페이지 테이블에서는 사용하지 않는 영역의 2단계 테이블을 아예 만들지 않습니다.
페이지 디렉토리 엔트리를 null로 두면 됩니다. 현대 64비트 시스템은 보통 4단계 페이지 테이블을 사용합니다.
x86-64 아키텍처에서는 PML4, PDPT, PD, PT라는 4단계 테이블을 거칩니다. 각 단계에서 9비트씩 사용하여 512개의 엔트리를 가지고, 마지막 12비트가 오프셋입니다.
이렇게 하면 실제 사용하는 메모리 영역에 대해서만 테이블을 할당합니다. 최신 Intel CPU는 5단계 페이지 테이블도 지원합니다.
서버용 시스템에서 수 테라바이트의 물리 메모리를 지원하기 위해 5단계가 도입되었습니다. 가상 주소 공간이 48비트에서 57비트로 확장되어 128PB까지 표현 가능합니다.
하지만 다단계에도 단점이 있습니다. TLB 미스가 발생하면 여러 단계의 테이블을 모두 읽어야 합니다.
4단계 페이지 테이블에서 TLB 미스가 발생하면 최대 4번의 메모리 접근이 필요합니다. 이것이 TLB 히트율이 중요한 또 다른 이유입니다.
실제 현업에서 이 개념이 중요한 이유가 있습니다. 메모리 매핑 파일이나 mmap을 사용할 때, 거대한 가상 주소 공간을 예약해도 실제로 접근하기 전까지는 물리 메모리가 할당되지 않습니다.
다단계 페이지 테이블 덕분에 페이지 테이블 메모리도 필요한 만큼만 사용됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
"아, 그래서 64비트 시스템에서도 페이지 테이블이 터무니없이 커지지 않는군요!" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
영리한 설계 덕분이죠."
실전 팁
💡 - 커널 개발이나 시스템 프로그래밍에서 /proc/[pid]/pagemap 파일을 통해 프로세스의 페이지 테이블 정보를 확인할 수 있습니다
- 다단계 페이지 테이블의 단계 수가 많아질수록 TLB 미스 페널티가 커지므로, 대형 페이지 사용을 고려해보세요
5. 세그멘테이션 기법
며칠 후 김개발 씨는 운영체제 책에서 세그멘테이션이라는 단어를 발견했습니다. "어라, 페이징이랑 비슷한 건가?" 박시니어 씨에게 물어보니 의외의 대답이 돌아왔습니다.
"페이징과는 완전히 다른 철학을 가진 기법이에요."
세그멘테이션은 프로그램을 논리적 단위(세그먼트)로 나누어 관리하는 메모리 기법입니다. 마치 책을 장(chapter)별로 나누는 것처럼, 코드, 데이터, 스택 등을 의미 있는 단위로 분리합니다.
각 세그먼트는 크기가 가변적이며 독립적인 보호 속성을 가질 수 있습니다.
다음 코드를 살펴봅시다.
// 세그멘테이션 시뮬레이션
public class SegmentationSimulator {
// 세그먼트 테이블 엔트리
static class Segment {
int base; // 시작 주소
int limit; // 세그먼트 크기
int access; // 접근 권한 (읽기/쓰기/실행)
}
private Segment[] segmentTable = new Segment[6];
// 0: 코드, 1: 데이터, 2: 스택, 3: 힙, 4: 공유 라이브러리, 5: 커널
public int translate(int segmentNumber, int offset) {
Segment seg = segmentTable[segmentNumber];
// 세그먼트 범위 검사
if (offset >= seg.limit) {
throw new SegmentationFaultException(); // 범위 초과!
}
return seg.base + offset; // 물리 주소 계산
}
}
김개발 씨는 페이징만 알면 메모리 관리는 끝인 줄 알았습니다. 그런데 세그멘테이션이라니, 왜 비슷한 일을 하는 기법이 또 있는 걸까요?
박시니어 씨가 설명을 시작했습니다. "페이징과 세그멘테이션은 목적이 달라요.
페이징은 운영체제의 편의를 위한 거고, 세그멘테이션은 프로그래머의 관점을 반영한 거예요." 그렇다면 세그멘테이션은 어떤 철학을 가지고 있을까요? 쉽게 비유하자면, 페이징이 아파트 단지라면 세그멘테이션은 맞춤 설계 주택단지입니다.
각 집(세그먼트)이 필요에 따라 다른 크기를 가집니다. 대가족 집은 크게, 1인 가구 집은 작게 지을 수 있습니다.
프로그램을 논리적으로 생각해보면 여러 영역으로 나뉩니다. 코드 세그먼트에는 실행 가능한 명령어가 들어있습니다.
데이터 세그먼트에는 전역 변수가 저장됩니다. 스택 세그먼트는 함수 호출 정보를 담고, 힙 세그먼트는 동적 할당 메모리입니다.
이들은 크기도 다르고 접근 방식도 다릅니다. 세그멘테이션에서 주소는 두 부분으로 구성됩니다.
(세그먼트 번호, 오프셋) 형태입니다. 세그먼트 번호로 세그먼트 테이블을 조회하여 베이스 주소와 **리밋(크기)**을 얻습니다.
오프셋이 리밋보다 크면 세그멘테이션 폴트가 발생합니다. 세그멘테이션의 장점은 논리적 구조를 반영한다는 것입니다.
코드 세그먼트는 읽기와 실행만 허용하고 쓰기는 금지할 수 있습니다. 여러 프로세스가 같은 라이브러리 세그먼트를 공유할 수도 있습니다.
스택과 힙은 반대 방향으로 성장하도록 배치하면 공간을 효율적으로 사용합니다. 하지만 세그멘테이션에는 치명적인 단점이 있습니다.
바로 외부 단편화입니다. 세그먼트 크기가 가변적이므로 메모리에 여기저기 빈 공간이 생깁니다.
전체 빈 공간은 충분해도 연속된 공간이 없어서 새 세그먼트를 할당하지 못하는 상황이 발생합니다. 유명한 세그멘테이션 폴트는 여기서 유래했습니다.
C 프로그램에서 배열 범위를 벗어나 접근하면 "Segmentation fault (core dumped)"라는 에러가 발생합니다. 이는 세그먼트의 리밋을 초과한 오프셋으로 접근했다는 의미입니다.
현대 시스템에서는 페이징 기반이지만, 용어는 그대로 남아있습니다. 실제 현업에서 세그멘테이션 개념이 적용되는 사례가 있습니다.
JVM의 메모리 구조를 보면 힙, 스택, 메서드 영역, PC 레지스터 등으로 나뉩니다. 각 영역은 세그먼트처럼 논리적 단위로 구분되어 있으며, 크기와 관리 방식이 다릅니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. "아, 그래서 segmentation fault가 그런 뜻이었군요!" 박시니어 씨가 덧붙였습니다.
"현대 시스템은 페이징을 주로 사용하지만, 세그멘테이션의 논리적 구분은 여전히 의미가 있어요."
실전 팁
💡 - 현대 운영체제(Linux, Windows)는 순수 세그멘테이션보다 페이징을 주로 사용하지만, x86에서는 세그먼트 레지스터(CS, DS, SS)가 여전히 존재합니다
- JVM이나 CLR 같은 가상머신도 내부적으로 메모리를 논리적 영역으로 구분하여 관리합니다
6. 세그멘테이션과 페이징 결합
김개발 씨가 운영체제 스터디를 마무리할 즈음, 한 가지 의문이 남았습니다. "페이징은 외부 단편화가 없고, 세그멘테이션은 논리적 구조를 반영하는 장점이 있다면...
둘을 합치면 어떨까요?" 박시니어 씨가 환하게 웃었습니다. "드디어 핵심에 도달했네요!"
세그멘테이션과 페이징의 결합은 두 기법의 장점을 모두 취하는 하이브리드 방식입니다. 마치 대형 마트가 층별로 구역을 나누고(세그멘테이션), 각 구역 내에서는 동일한 크기의 진열대를 사용하는 것(페이징)과 같습니다.
논리적 보호와 효율적인 물리 메모리 관리를 동시에 달성합니다.
다음 코드를 살펴봅시다.
// 세그멘티드 페이징 시뮬레이션
public class SegmentedPaging {
static class Segment {
int[] pageTable; // 각 세그먼트마다 별도 페이지 테이블
int pageCount;
int protection; // 세그먼트 레벨 보호
}
private Segment[] segments = new Segment[4];
private static final int PAGE_SIZE = 4096;
public int translate(int segNum, int pageNum, int offset) {
Segment seg = segments[segNum];
// 세그먼트 레벨 검사
if (pageNum >= seg.pageCount) {
throw new SegmentationFaultException();
}
// 페이지 테이블 조회
int frameNumber = seg.pageTable[pageNum];
return frameNumber * PAGE_SIZE + offset;
}
}
김개발 씨의 직관은 정확했습니다. 페이징과 세그멘테이션은 상호 보완적인 특성을 가지고 있어, 함께 사용하면 더 강력한 메모리 관리가 가능합니다.
박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "이걸 세그멘티드 페이징이라고 해요." 그렇다면 두 기법을 어떻게 결합할까요?
쉽게 비유하자면, 대형 쇼핑몰을 생각해보세요. 쇼핑몰은 식품관, 의류관, 전자제품관 등으로 구역이 나뉘어 있습니다.
이것이 세그먼트입니다. 각 구역 내부는 동일한 크기의 매장으로 구성되어 있습니다.
이것이 페이지입니다. 세그멘티드 페이징에서는 각 세그먼트가 자신만의 페이지 테이블을 가집니다.
가상 주소는 **(세그먼트 번호, 페이지 번호, 오프셋)**으로 구성됩니다. 먼저 세그먼트 번호로 해당 세그먼트의 페이지 테이블을 찾고, 그 페이지 테이블에서 프레임 번호를 얻습니다.
이 방식의 장점은 무엇일까요? 외부 단편화가 없습니다. 각 세그먼트 내부는 페이징으로 관리되므로 연속된 물리 메모리가 필요하지 않습니다.
논리적 보호가 가능합니다. 세그먼트 단위로 접근 권한을 설정할 수 있습니다. 공유가 쉽습니다. 여러 프로세스가 같은 세그먼트(예: 공유 라이브러리)를 공유할 때, 해당 세그먼트의 페이지 테이블만 공유하면 됩니다.
반대로 페이지드 세그멘테이션도 있습니다. 이 방식에서는 먼저 페이지 단위로 나누고, 페이지들을 논리적 세그먼트로 그룹화합니다.
Intel x86의 실제 구현이 이에 가깝습니다. 세그먼트 레지스터가 메모리 영역을 구분하고, 그 안에서 페이징이 동작합니다.
Intel x86 아키텍처가 대표적인 예입니다. 초기 x86은 세그멘테이션을 적극 활용했습니다.
코드 세그먼트(CS), 데이터 세그먼트(DS), 스택 세그먼트(SS) 레지스터가 각 영역을 가리켰습니다. 32비트 보호 모드에서는 세그멘테이션과 페이징이 함께 동작했습니다.
하지만 현대 운영체제는 플랫 메모리 모델을 사용합니다. Linux, Windows 등은 모든 세그먼트의 베이스를 0으로, 리밋을 최대로 설정하여 세그멘테이션을 사실상 비활성화합니다.
세그먼트 레지스터는 호환성을 위해 남아있지만, 실제 메모리 관리는 순수 페이징으로 이루어집니다. 그렇다면 세그멘테이션의 장점은 어떻게 살릴까요?
현대 시스템에서는 **가상 메모리 영역(VMA, Virtual Memory Area)**이라는 개념으로 세그먼트의 논리적 구분을 구현합니다. 커널은 각 VMA에 대해 접근 권한, 매핑 파일 등의 메타데이터를 관리합니다.
실제 현업에서 이 지식이 도움되는 상황이 있습니다. JNI로 네이티브 코드를 다루거나, 운영체제 수준의 디버깅을 할 때 세그먼트 관련 오류를 이해할 수 있습니다.
또한 가상머신이나 컨테이너 기술을 깊이 이해하려면 메모리 가상화의 기반인 페이징을 잘 알아야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
3개월간의 운영체제 스터디를 마친 김개발 씨는 이제 메모리 관련 버그를 만나도 당황하지 않습니다. 페이징, TLB, 세그멘테이션의 원리를 이해하니 시스템의 동작이 눈에 보이기 시작했습니다.
박시니어 씨가 마지막으로 말했습니다. "운영체제를 이해하면 어떤 언어, 어떤 프레임워크를 써도 그 아래에서 무슨 일이 일어나는지 알 수 있어요.
그게 진정한 개발자의 힘이죠."
실전 팁
💡 - 현대 64비트 시스템에서 세그멘테이션은 거의 사용되지 않지만, 보안 기능(GS 세그먼트를 이용한 스택 카나리 등)에는 여전히 활용됩니다
- /proc/[pid]/maps 파일을 확인하면 프로세스의 메모리 영역이 어떻게 구성되어 있는지 볼 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
디스크 스케줄링 완벽 가이드
운영체제가 하드디스크의 읽기/쓰기 요청을 효율적으로 처리하는 방법을 알아봅니다. 디스크 구조부터 FCFS, SSTF, SCAN, LOOK 알고리즘까지 실무 예제와 함께 쉽게 설명합니다.
파일 시스템 완벽 가이드
운영체제의 핵심인 파일 시스템을 쉽게 이해할 수 있도록 정리한 가이드입니다. 파일의 개념부터 저널링까지, 실무에서 꼭 알아야 할 내용을 담았습니다.
가상 메모리 완벽 가이드
운영체제의 핵심 개념인 가상 메모리를 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 요구 페이징부터 스래싱까지, 실무에서 마주치는 메모리 관리의 모든 것을 다룹니다.
메모리 관리 기초 완벽 가이드
운영체제가 메모리를 어떻게 관리하는지 기초부터 차근차근 알아봅니다. 논리 주소와 물리 주소의 차이부터 단편화 문제까지, 메모리 관리의 핵심 개념을 실무 스토리와 함께 쉽게 설명합니다.
교착 상태 (Deadlock) 완벽 가이드
운영체제에서 발생하는 교착 상태의 개념부터 예방, 회피, 탐지까지 완벽하게 이해할 수 있는 가이드입니다. 실무에서 마주칠 수 있는 동시성 문제를 해결하는 핵심 지식을 담았습니다.