메모리 페이징 원리와 구현: Rust를 이용한 효율적 운영체제 메모리 관리 시스템 구축 7단계

메모리 페이징 원리와 구현: Rust를 이용한 효율적 운영체제 메모리 관리 시스템 구축 7단계

메모리 페이징은 현대 운영체제에서 효율적인 메모리 관리를 구현하는 핵심 기술로, 가상 메모리와 물리 메모리를 고정된 크기의 블록(페이지와 프레임)으로 분할하여 메모리 단편화 문제를 해결하고 메모리 격리 기능을 제공합니다. 본문에서는 'Rust로 운영체제 작성' 프로젝트를 기반으로 메모리 페이징의 작동 원리와 Rust를 사용하여 효율적인 메모리 관리 시스템을 구현하는 방법을 상세히 소개하며, 초보 개발자들이 이 핵심 기술을 빠르게 익힐 수 있도록 돕습니다.

1. 메모리 페이징의 핵심 개념 이해

메모리 페이징은 가상 메모리 관리 기술로, 가상 주소 공간과 물리 주소 공간을 고정된 크기의 블록으로 각각 분할합니다. 이 블록들은 각각 **페이지(Page)**와 **프레임(Frame)**이라고 불립니다. 각 페이지는 독립적으로 하나의 프레임에 매핑될 수 있어, 연속적인 가상 메모리 영역이 비연속적인 물리 메모리에 매핑될 수 있으며 이는 메모리 단편화 문제를 효과적으로 해결합니다.

x86_64 아키텍처에서 페이징 시스템은 4단계 페이지 테이블 구조를 사용하며, 일반적으로 페이지 크기는 4KiB입니다. 이러한 다단계 페이지 테이블 설계는 메모리 공간을 절약하면서 가상 주소를 물리 주소로 변환하는 효율적인 작업을 가능하게 합니다.

2. 메모리 페이징이 필요한 이유

전통적인 세그먼트 메모리 관리 방식은 심각한 외부 단편화 문제를 가지고 있습니다. 메모리의 할당과 해제가 반복되면서 효율적으로 활용할 수 없는 작은 메모리 조각들이 많이 발생하기 때문입니다. 페이징 기술은 다음과 같은 방식으로 이 문제를 해결합니다:

  • 고정된 크기의 페이지: 모든 페이지와 프레임의 크기가 동일하여 외부 단편화를 방지
  • 비연속 매핑: 연속적인 가상 메모리가 분산된 물리 프레임에 매핑될 수 있음
  • 메모리 격리: 페이지 테이블 권한 비트를 통해 프로세스 간 메모리 격리 구현
  • 요구에 따른 로딩: 자주 사용되지 않는 페이지를 디스크로 내보내 메모리 사용량 증대

3. 페이징 시스템의 작동 원리

페이징 시스템의 핵심은 주소 변환 과정으로, 프로그램이 사용하는 가상 주소를 실제 물리 주소로 변환하는 것입니다. 이 과정은 하드웨어(MMU)가 자동으로 수행하며, 다음과 같은 핵심 구성 요소가 관여합니다:

페이지 테이블 구조

x86_64 아키텍처의 4단계 페이지 테이블 구조는 다음과 같습니다:

  • P4(4단계) 페이지 테이블: 최상위 페이지 테이블로 CR3 레지스터가 가리킴
  • P3(3단계) 페이지 테이블: 중간 단계 페이지 테이블
  • P2(2단계) 페이지 테이블: 중간 단계 페이지 테이블
  • P1(1단계) 페이지 테이블: 최하위 페이지 테이블로 물리 프레임을 직접 가리킴

각 페이지 테이블은 512개의 항목을 포함하며, 각 항목의 크기는 8바이트이므로 각 페이지 테이블은 정확히 하나의 4KiB 페이지를 차지합니다.

주소 변환 과정

가상 주소는 4단계 페이지 테이블에서 인덱싱하기 위해 5개 부분으로 나뉩니다:

  • 48-63비트: 부호 확장 비트(반드시 47번째 비트와 동일해야 함)
  • 39-47비트: P4 테이블 인덱스(9비트)
  • 30-38비트: P3 테이블 인덱스(9비트)
  • 21-29비트: P2 테이블 인덱스(9비트)
  • 12-20비트: P1 테이블 인덱스(9비트)
  • 0-11비트: 페이지 내 오프셋(12비트)

주소 변환 단계:

  1. CR3 레지스터에서 P4 페이지 테이블의 물리 주소를 가져옴
  2. P4 인덱스를 사용하여 P4 테이블에서 P3 페이지 테이블의 물리 주소 찾기
  3. P3 인덱스를 사용하여 P3 테이블에서 P2 페이지 테이블의 물리 주소 찾기
  4. P2 인덱스를 사용하여 P2 테이블에서 P1 페이지 테이블의 물리 주소 찾기
  5. P1 인덱스를 사용하여 P1 테이블에서 물리 프레임 주소 찾기
  6. 물리 프레임 주소와 페이지 오프셋을 조합하여 최종 물리 주소 얻기

4. Rust 개발 환경 준비

페이징 시스템 구현을 시작하기 전에 Rust 개발 환경과 프로젝트 구조를 준비해야 합니다. 'Rust로 운영체제 작성' 프로젝트의 코드 구조를 사용하며, 주로 다음 파일들이 관련됩니다:

  • 08-introduction-to-paging.md: 페이징 원리 소개
  • 09-paging-implementation.md: 페이징 구현 세부 사항
  • dummy.rs: 예제 코드 파일

필요한 도구체인이 설치되어 있는지 확인합니다:

rustup target add x86_64-unknown-none
cargo install bootimage

5. 주소 변환 기능 구현

먼저 가상 주소를 물리 주소로 변환하는 함수를 구현해야 합니다. 이는 4단계 페이지 테이블을 순회해야 하며, 다음과 같이 구현할 수 있습니다:

// src/memory.rs
use x86_64::{
    PhysAddr, VirtAddr,
    structures::paging::PageTable
};

/// 가상 주소를 물리 주소로 변환
pub unsafe fn translate_virt_to_phys(addr: VirtAddr, phys_memory_offset: VirtAddr) -> Option<PhysAddr> {
    use x86_64::registers::control::Cr3;
    use x86_64::structures::paging::page_table::FrameError;

    // CR3 레지스터에서 P4 페이지 테이블의 물리 프레임 읽기
    let (level_4_table_frame, _) = Cr3::read();
    
    let table_indices = [
        addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index()
    ];
    let mut current_frame = level_4_table_frame;
    
    // 다단계 페이지 테이블 순회
    for &index in &table_indices {
        // 프레임을 페이지 테이블 참조로 변환
        let virt_addr = phys_memory_offset + current_frame.start_address().as_u64();
        let table_ptr: *const PageTable = virt_addr.as_ptr();
        let table_ref = &*table_ptr;
        
        // 페이지 테이블 항목 읽기 및 프레임 업데이트
        let entry = &table_ref[index];
        current_frame = match entry.frame() {
            Ok(frame) => frame,
            Err(FrameError::FrameNotPresent) => return None,
            Err(FrameError::HugeFrame) => panic!("대형 페이지 지원 안 함"),
        };
    }
    
    // 물리 주계 계산
    Some(current_frame.start_address() + u64::from(addr.page_offset()))
}

6. 페이지 테이블 매핑 생성

다음으로 새 매핑을 생성하는 기능을 구현해야 합니다. 이는 페이지 테이블 항목을 수정하는 작업이며, 새 물리 프레임을 얻기 위해 프레임 할당자를 사용해야 합니다:

// src/memory.rs
use x86_64::{
    structures::paging::{MemoryPage, PhysFrame, MemoryMapper, PageSize, FrameAllocator},
    PhysAddr
};

/// 샘플 매핑 생성
pub fn create_sample_mapping(
    page: MemoryPage,
    mapper: &mut impl MemoryMapper<PageSize>,
    frame_allocator: &mut impl FrameAllocator<PageSize>,
) {
    use x86_64::structures::paging::PageTableFlags as Flags;
    
    let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); // VGA 버퍼
    let flags = Flags::PRESENT | Flags::WRITABLE;
    
    let mapping_result = unsafe {
        // 안전하지 않음: 프레임이 다른 매핑에서 사용되지 않도록 해야 함
        mapper.create_mapping(page, frame, flags, frame_allocator)
    };
    mapping_result.expect("매핑 실패").flush();
}

7. 프레임 할당자 구현

물리 프레임을 할당할 수 있도록 프레임 할당자를 구현해야 합니다. 프레임 할당자는 부트로더가 제공하는 메모리 맵을 사용하여 사용 가능한 물리 프레임을 추적합니다:

// src/memory.rs
use bootloader::bootinfo::{MemoryMap, MemoryRegionType};
use x86_64::structures::paging::PhysFrame;

/// 부트 정보 기반 프레임 할당자
pub struct BootInfoFrameAllocator {
    memory_regions: &'static MemoryMap,
    next_index: usize,
}

impl BootInfoFrameAllocator {
    /// 새 프레임 할당자 생성
    pub unsafe fn initialize(memory_map: &'static MemoryMap) -> Self {
        BootInfoFrameAllocator {
            memory_regions: memory_map,
            next_index: 0,
        }
    }
    
    /// 사용 가능한 프레임 이터레이터 반환
    fn available_frames(&self) -> impl Iterator<Item = PhysFrame> {
        // 사용 가능한 메모리 영역 필터링
        let regions = self.memory_regions.iter();
        let usable_regions = regions.filter(|r| r.region_type == MemoryRegionType::Usable);
        
        // 영역을 주소 범위로 변환
        let address_ranges = usable_regions.map(|r| r.range.start_addr()..r.range.end_addr());
        
        // 프레임 시작 주소 이터레이터로 변환
        let frame_addresses = address_ranges.flat_map(|r| r.step_by(4096));
        
        // PhysFrame 타입 생성
        frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))
    }
}

unsafe impl FrameAllocator<PageSize> for BootInfoFrameAllocator {
    fn allocate_frame(&mut self) -> Option<PhysFrame> {
        let frame = self.available_frames().nth(self.next_index);
        self.next_index += 1;
        frame
    }
}

결론

위 7단계를 통해 기본적인 메모리 페이징 시스템을 구현했습니다. 이 시스템은 가상 주소를 물리 주소로 변환하고 새 메모리 매핑을 생성할 수 있습니다. 메모리 페이징은 운영체제 개발의 핵심 기술로, 프로세스에 격리된 메모리 공간을 제공하면서 물리 메모리를 효율적으로 활용합니다.

실제 애플리케이션에서는 대형 페이지(Huge Pages), 페이지 교체 알고리즘, 쓰기 시 복사(Copy-on-Write)와 같은 고급 기능을 고려해야 합니다. 이러한 기술들은 메모리 관리의 효율성과 성능을 더욱 향상시킬 수 있습니다.

'Rust로 운영체제 작성' 프로젝트를 통해 메모리 페이징의 원리와 구현뿐만 아니라 Rust에서 저수준 시스템 프로그래밍을 하는 방법도 배웠습니다. Rust의 메모리 안전성 특성과 제로 비트 추상화는 운영체제 개발에 이상적인 선택지가 됩니다.

다음 글에서는 페이징 시스템을 기반으로 힙 메모리 할당을 구현하여 커널에 동적 메모리 관리 기능을 제공하는 방법을 학습할 것입니다.

태그: 메모리 관리 가상 메모리 페이징 Rust 운영체제

7월 8일 19:08에 게시됨