페이징에서의 공유 페이지 (Shared Pages)
핵심 인사이트 (3줄 요약)
- 본질: 페이징 환경에서의 공유 페이지(Shared Pages)는 여러 프로세스가 공통으로 사용하는 코드나 라이브러리를 물리 메모리에 딱 **한 번만 적재(Load)**하고, 각 프로세스의 페이지 테이블이 동일한 물리 프레임을 가리키게 하여 메모리를 공유하는 기법이다.
- 가치: 메모리 소비량이 기하급수적으로 폭발하는 다중 사용자 환경(예: 40명의 사용자가 동시에 워드프로세서를 실행할 때)에서, 중복되는 코드 용량을 완벽하게 0으로 만들어 시스템 메모리를 수십~수백 배 절약해 준다.
- 융합: 이 기법이 성립하기 위해서는 공유되는 코드가 실행 중에 절대 내용이 변하지 않는 **재진입 가능 코드(Reentrant Code, Pure Code)**여야만 하며, 이는 동적 연결 라이브러리(DLL/SO)의 하드웨어적 구현 토대가 된다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 페이징 시스템은 프로세스마다 독립된 페이지 테이블을 가진다. 보통은 자기만의 물리 프레임을 가리키지만, '공유 페이지' 기법에서는 여러 프로세스의 페이지 테이블 엔트리(Entry)가 동일한 물리 프레임 번호를 가리키게끔 의도적으로 매핑(Mapping)한다.
-
필요성: 만약 50MB짜리 텍스트 에디터를 10명이 동시에 띄웠다고 가정해 보자. 연속 메모리 할당 환경에서는 50MB 코드를 10명분으로 각각 램에 올려 총 500MB의 메모리가 증발했다. 똑같은 코드(기계어 명령어)를 메모리에 10번이나 중복해서 올리는 것은 막대한 낭비다. "코드 원본은 하나만 두고, 각자 읽어(Read) 가기만 하자!"라는 공유의 철학이 절실했다.
-
💡 비유: 공유 페이지는 도서관의 베스트셀러 열람실 구조와 같다. 학생 10명이 해리포터를 읽어야 할 때 학교가 해리포터를 10권 사서 각자의 책상(로컬 메모리)에 올려주는 것이 아니라, 도서관 중앙 책상에 해리포터 1권(공유 프레임)을 묶어놓고, 10명의 학생에게 "해리포터는 중앙 책상 3번에 있다"라는 쪽지(페이지 테이블 매핑)만 쥐여주어 예산을 아끼는 방법이다.
-
등장 배경 및 아키텍처 제약:
- 초기 공유 불가: 과거에는 코드와 데이터가 섞여 있어(Self-modifying code 등), 한 놈이 코드를 바꾸면 다른 놈이 엉뚱한 코드를 실행하게 되어 공유가 불가능했다.
- 재진입 코드(Reentrant Code)의 등장: 개발자들이 프로그램의 '명령어(Code)' 부분과 '변수/데이터(Data)' 부분을 엄격히 분리해서 컴파일하기 시작했다. 코드는 절대 변하지 않는 순수 기계어(Pure Code)로 작성되었다.
- 페이징의 활용: OS는 이 변하지 않는 코드 조각들만 핀셋으로 집어 하나의 프레임에 올린 뒤, 수천 개의 프로세스가 이 프레임을 가리키게 하는 마법을 부렸다.
┌──────────────────────────────────────────────────────────────────────────┐
│ 페이징 시스템에서의 공유 페이지(Shared Page) 매핑 구조 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ [ 프로세스 P1 (유저 A의 크롬) ] [ 프로세스 P2 (유저 B의 크롬) ] │
│ P1의 페이지 테이블 P2의 페이지 테이블 │
│ ┌──────┬───────┐ ┌──────┬───────┐ │
│ │ Page │ Frame │ │ Page │ Frame │ │
│ ├──────┼───────┤ ├──────┼───────┤ │
│ │ 0(Ed)│ 3 │──┐ ┌────────│ 0(Ed)│ 3 │ (공유!) │
│ │ 1(Ed)│ 4 │──┼────┼──┐ │ 1(Ed)│ 4 │ (공유!) │
│ │ 2(Ed)│ 6 │──┼────┼──┼──┐ │ 2(Ed)│ 6 │ (공유!) │
│ │ 3(D1)│ 1 │─┐│ │ │ │ │ 3(D2)│ 8 │ (독립됨) │
│ └──────┴───────┘ ││ │ │ │ └──────┴───────┘ │
│ ││ │ │ │ │
│ ▼▼ ▼ ▼ ▼ │
│ [ 물리 메모리 (RAM) ] │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ Fr 0│Fr 1 │ Fr 2│Fr 3 │Fr 4 │ Fr 5│Fr 6 │ Fr 7│Fr 8 │ │
│ │ (빈) │ Data│ (빈) │ Ed 1│ Ed 2│ (빈) │ Ed 3│ (빈) │ Data│ │
│ │ │ (P1)│ │(공유)│(공유)│ │(공유)│ │ (P2)│ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ * Ed: Editor 코드 (읽기 전용, 재진입 가능) │
│ * Data: 사용자 개별 데이터 (읽기/쓰기 가능, 각각 독립적 프레임 할당) │
└──────────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] P1과 P2는 모두 크롬 브라우저다. 논리 주소의 0, 1, 2번 페이지는 크롬의 핵심 실행 코드(Ed)다. OS는 두 프로세스의 테이블에서 이 코드 영역이 모두 똑같은 물리 프레임 [3, 4, 6]을 가리키도록 연결한다. 하지만 3번 페이지(Data)는 유저가 검색창에 입력한 글자나 로컬 상태값이므로 절대 공유하면 안 된다. 그래서 P1의 데이터는 프레임 1에, P2의 데이터는 프레임 8에 완전히 격리해서 매핑해 둔다.
- 📢 섹션 요약 비유: 게임방에서 10명이 똑같은 게임을 할 때, 게임 설치 파일(공유 페이지)은 중앙 서버 1대(물리 메모리)에만 깔아두고 모니터로 화면만 공유받되, 각자의 게임 세이브 파일(개별 데이터)만 자기들 USB(독립 프레임)에 따로 저장하는 완벽한 분리형 시스템입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
재진입 가능 코드 (Reentrant Code / Pure Code)의 필수성
공유 페이지가 안전하게 작동하려면, 공유되는 프레임 안의 기계어 코드들이 **"실행 도중에 절대 자신의 내용(Data)을 변경하지 않는다"**는 속성을 무조건 보장해야 한다. 이를 재진입 가능(Reentrant) 하다고 부른다.
- 만약 프로그램 A가 공유 코드를 실행하다가 내부 변수 값을
count = 10으로 바꿔버렸는데, 스케줄링이 넘어가서 프로그램 B가 그 코드를 실행하면 B는 0이 아니라 10부터 시작하게 되어 완전히 버그가 나버린다. - 따라서 공유 페이지 영역은 페이지 테이블의 **R/W 비트가 반드시
Read-Only(읽기 전용)로 강제 락(Lock)**이 걸려 있어야 한다. 누군가 여기에 Write를 시도하면 MMU가 하드웨어 트랩(SegFault)을 날려 죽여버린다.
논리 주소 위치의 강제 일치성 (The Same Logical Address Rule)
과거 시스템에서 코드를 공유할 때 가장 까다로웠던 물리적 제약 조건이다.
-
공유되는 페이지(예: Ed 1, Ed 2, Ed 3)는 모든 공유 프로세스들의 논리 주소 공간에서 **정확히 똑같은 위치(동일한 논리 페이지 번호)**에 존재해야만 했다.
-
크롬 A에서 공유 코드가 페이지
0, 1, 2에 있다면, 크롬 B에서도 반드시 페이지0, 1, 2에 있어야 한다. -
이유: 기계어 코드 안에
JUMP 5000 (페이지 1번으로 점프)이라는 상대 주소가 박혀있기 때문이다. 만약 B에서 공유 코드가 페이지10, 11, 12에 매핑되어 있다면 저 JUMP 명령은 엉뚱한 곳으로 날아가 버린다. (현대에는 주소 독립 코드 PIC 기술로 이 제약이 많이 느슨해졌다.) -
📢 섹션 요약 비유: 두 학교가 영어 시험지(공유 페이지)를 공유하기로 했다면, 반드시 두 학교 모두 1교시(동일 논리 주소)에 영어 시험을 쳐야 합니다. 한 학교는 1교시, 한 학교는 3교시에 치면 부정행위(주소 점프 오류)가 발생하기 때문입니다.
Ⅲ. 융합 비교 및 다각도 분석
비교 1: 스레드(Thread) 공유 vs 페이징(Paging) 공유
메모리를 공유한다는 목적은 같지만, 범주와 아키텍처가 완전히 다르다.
| 비교 항목 | 멀티 스레드 (Thread Sharing) | 페이징 공유 페이지 (Process Paging Sharing) |
|---|---|---|
| 공유 대상 | 같은 프로세스 소속의 스레드들 간의 공유 | 서로 다른 독립된 프로세스들 간의 공유 |
| 공유 범위 | 코드(Code), 데이터(Data), 힙(Heap) 전부 공유 | 오직 읽기 전용 **코드(Code)**나 특정 DLL/SO만 제한적 공유 |
| 페이지 테이블 | 스레드 100개가 하나의 PTBR(장부)을 통째로 공유 | 프로세스 100개가 각자 자기 PTBR(장부)을 가짐 |
| 매핑 방식 | 그냥 장부 하나를 같이 쳐다보는 것 | 각자의 장부 특정 줄(Entry)이 우연히 같은 프레임을 가리킴 |
시스템적 파급 효과: DLL과 SO의 탄생
이 '공유 페이지' 하드웨어 아키텍처가 없었다면 윈도우의 .dll (Dynamic Link Library)이나 리눅스의 .so (Shared Object) 파일은 존재할 수 없었다.
- 모든 프로그램은
printf()같은 표준 C 라이브러리를 호출한다. - 100개의 앱이 실행될 때, 리눅스는
libc.so파일을 물리 프레임에 단 1번만 로드한다 (약 2MB). - 그리고 100개 앱의 페이지 테이블에
libc.so의 500개 페이지 프레임 번호를 스터브(Stub)를 통해 모두 똑같이 매핑해 준다. - 이로 인해 시스템 전체의 메모리 낭비가 테라바이트 수준으로 절약된다.
┌──────────┬────────────┬────────────┬──────────────────────────────────────────┐
│ 환경 │ 라이브러리 적재│ 물리 메모리 소비│ 페이지 테이블 세팅 │
├──────────┼────────────┼────────────┼──────────────────────────────────────────┤
│ 정적 링킹 │ 앱마다 통째로 복사│ 2MB × 100 = 200MB│ 각자 다른 프레임 매핑 │
│ 동적/페이징│ 램에 딱 1번 로드│ 2MB × 1 = 2MB │ 100명이 **같은 프레임** 가리킴│
└──────────┴────────────┴────────────┴──────────────────────────────────────────┘
[매트릭스 해설] 공유 페이지는 동적 링킹(Dynamic Linking)을 하드웨어 레벨에서 완성시켜주는 핵심 톱니바퀴다. 소프트웨어가 외부 라이브러리를 동적으로 연결하겠다고 포인터를 넘기면, 하드웨어(페이징 유닛)가 다른 앱이 쓰고 있는 프레임 주소를 똑같이 쏴주어 이 거대한 절약 시스템을 완성한다.
- 📢 섹션 요약 비유: 수백 명의 요리사가 각자 주방에 똑같은 공용 전자레인지(printf)를 돈 주고 들이는 것(정적 링킹)이 아니라, 복도에 있는 업소용 대형 전자레인지 하나(공유 라이브러리)를 모두가 자기 주방 문 열고 나가서 같이 쓰는(공유 페이지) 구조입니다.
Ⅳ. 실무 적용 및 기술사적 판단 (Strategy & Decision)
실무 시나리오: 안드로이드 Zygote(지고트) 프로세스의 흑마술
안드로이드 스마트폰이 램 용량이 적음에도 불구하고 수십 개의 앱을 부드럽게 띄울 수 있는 비밀이 바로 이 '공유 페이지'다.
- Zygote의 탄생: 안드로이드 커널이 부팅되면, 자바 가상 머신(Dalvik/ART)과 모든 안드로이드 앱이 공통으로 쓰는 수백 MB의 프레임워크 라이브러리(UI, 네트워크 등)를 메모리에 몽땅 올려서 **Zygote(수정란)**라는 마스터 프로세스를 하나 만든다.
- 앱 실행 (fork): 사용자가 카카오톡, 유튜브, 인스타 앱을 터치해서 켤 때마다, 안드로이드는 이 무거운 프레임워크를 디스크에서 새로 읽지 않는다. 그냥 Zygote 프로세스를
fork()해버린다. - Copy-on-Write와 페이지 공유:
fork()되는 순간, 카톡과 유튜브의 페이지 테이블은 몽땅 Zygote가 물고 있던 수백 MB의 물리 프레임(공유 페이지)을 그대로 똑같이 가리키게 된다.- 즉, 10개의 앱을 켜도 프레임워크 코드는 물리 램에 Zygote가 올린 딱 1벌(100MB)뿐이다.
- 앱이 각자 자기 데이터를 수정(Write)하는 페이지에 대해서만 Copy-on-Write가 발동해 물리 램을 1장씩 분리해 나간다.
- 결과: 안드로이드 앱 실행 속도가 빛처럼 빠르고, 메모리를 극단적으로 덜 먹게 된다. 이것이 스마트폰 OS 메모리 관리의 최고봉 기술이다.
- 📢 섹션 요약 비유: 김밥천국을 새로 차릴 때마다 레시피와 주방 기구를 몽땅 새로 세팅하는 게 아니라, 완벽하게 세팅된 본점(Zygote)을 도장 찍듯 그대로 복사(fork)해서 체인점(앱)을 내고 간판 이름(독립 데이터)만 살짝 바꿔 달아서 1초 만에 오픈하는 사기적인 프랜차이즈 비법입니다.
Ⅴ. 기대효과 및 결론 (Future & Standard)
정량/정성 기대효과
| 구분 | 내용 |
|---|---|
| 물리 램(RAM) 절약 극대화 | 다중 사용자 환경(터미널 서버 등)과 멀티 탭 브라우저에서 코드 중복 적재를 없애 메모리 소비량을 N분의 1로 압축 |
| 초기 로딩(Booting) 속도 향상 | 누군가 한 번 로드해둔 라이브러리(공유 페이지)를 내가 쓸 때는 디스크에서 읽어올 필요 없이 테이블 포인터만 연결하면 즉시 실행 |
| IPC(프로세스 간 통신) 성능 | 코드가 아닌 데이터 페이지를 의도적으로 R/W 권한으로 공유하면, 가장 빠른 프로세스 간 통신(Shared Memory IPC) 채널로 진화 |
결론 및 미래 전망
페이징에서의 공유 페이지 (Shared Pages)는 "메모리는 프로세스마다 독점해야 한다"는 원칙을 우아하게 깨버리고, "변하지 않는 것은 묶어두고 변하는 것만 찢어놓는다"는 완벽한 추상화를 이뤄냈다. 이 기술은 동적 라이브러리(.so/.dll) 생태계를 만들었고, 메모리 맵 매핑(mmap)을 통한 초고속 파일 I/O를 탄생시켰으며, 도커(Docker) 컨테이너와 모바일 OS(Android Zygote)의 근간을 이루는 뼈대가 되었다. 앞으로 AI 시대에 동일한 거대 가중치(LLM Weights) 모델을 수많은 세션이 공유해야 하는 추론(Inference) 서버 아키텍처에서도, 이 낡지만 위대한 공유 페이지 기술의 중요성은 더욱 눈부시게 빛날 것이다.
- 📢 섹션 요약 비유: 수만 명의 사람들이 각자 달(Moon)을 갖겠다고 우주로 날아가는 대신, 지구라는 거대한 텐트(OS) 안에서 모두가 고개만 들어 하늘에 뜬 단 하나의 달(공유 페이지)을 각자의 눈(페이지 테이블)에 담아 감상하는 가장 평화롭고 경제적인 우주 관측법입니다.
📌 관련 개념 맵 (Knowledge Graph)
- 재진입 가능 코드 (Reentrant Code) | 실행 중 내용이 절대 변하지 않는 읽기 전용 기계어로, 페이지를 안전하게 공유하기 위한 필수 전제 조건
- 페이지 테이블 (Page Table) | 프로세스마다 독립적으로 존재하지만, 공유 코드를 위해 엔트리 화살표가 같은 물리 프레임을 가리키게 되는 맵핑 장부
- Copy-on-Write (CoW) | 읽기 전용 공유 페이지에 누군가 쓰기(Write)를 시도할 때, 그 순간 해당 페이지만 물리적으로 복제하여 찢어놓는 최적화 기법
- 동적 라이브러리 (DLL, SO) | 하드디스크에 저장된 범용 함수 묶음으로, 메모리에 한 번만 올라가 수많은 프로세스에게 공유 페이지 형태로 제공됨
- Zygote 프로세스 | 안드로이드에서 모든 프레임워크를 공유 페이지로 물고 있다가, 앱 실행 시 자신을 복제(fork)하여 메모리 효율을 극대화하는 마스터 프로세스
👶 어린이를 위한 3줄 비유 설명
- 공유 페이지가 무엇인가요? 학교에서 30명의 친구들이 미술 시간에 똑같은 '모나리자 그림'을 보고 따라 그려야 할 때, 그림 30장을 복사해서 나눠주지 않고 교탁에 진짜 큰 모나리자 그림(공유 페이지) 1장만 딱 붙여놓는 거예요.
- 왜 그렇게 하나요? 종이(메모리)를 엄청나게 아낄 수 있잖아요! 어차피 모나리자 그림은 눈으로 '읽기(Read-Only)'만 하는 거라 1장만 있어도 다 같이 볼 수 있거든요.
- 각자 그림은 어디다 그리나요? 모나리자를 보고 그리는 도화지(데이터 영역)는 친구들 각자의 책상(독립 프레임)에 따로 놔둬서, 내가 그린 그림에 콧수염을 그려도 다른 친구 도화지에는 아무런 피해를 주지 않는답니다.