[PintOS] Project 3 : VM - Memory Management & Anonymous Page - 2

CorinBeom·2025년 6월 2일

PintOS

목록 보기
11/19
post-thumbnail

지난 포스팅에 이어 이번에는 syscall.c, process.c 의 수정 사항들에 대해서 알아보자


syscall.c

우리는 syscall 함수들을 구현할 때, 인자값으로 포인터를 받아오는 것들에 대한 검사를 해주었다

아마 check_address를 많이 구현했을 듯 싶은데, 본인의 이전 팀은 validate.c 파일을 따로 만들어서
validate_ptr 함수를 따로 만들어 사용했다.

그 점을 짚고 시작해보겠다. 아마 check_address도 비슷하게 구현하면 되지 않을까?(진짜 모름)
.
.
.
모든 구현을 모두 마치고 테스트를 돌려보니 read_boundary 부분에서 Fail이 계속해서 발생했다 !
그래서 먼저 구현을 마친 팀원에게 이유를 물어보니 check_address 함수에서 조건 분기를 추가해줘야 한다고 이야기를 해줬다.

그런데 이게 웬걸 나의 코드에는 check_address 함수는 구현해놓았지만, 각 시스템 콜 함수에서는 호출하지 않고
위에서 얘기했던 validate_ptr 함수를 사용하고 있었다.

validate.c

validate_ptr 함수

void
validate_ptr (const void *uaddr, size_t size) {
    if (size == 0) return;  // 검사할 바이트 수가 0이면 return

    const uint8_t *usr = uaddr; // 현재 검사할 위치 (byte 단위 포인터)
    size_t left = size;         // 검사해야 할 남은 바이트 수

    // 검사할 전체 영역을 페이지 단위로 나누어 한 페이지씩 접근 가능 여부를 확인
    while (left > 0) {
        // 현재 usr 포인터가 가리키는 주소가 사용자 영역에 있고
        // 실제로 물리 메모리에 매핑되어 있는지 확인
        if (!check_page (usr))
            sys_exit (-1);  // 잘못된 주소일 경우, 즉시 프로세스 종료

        // 현재 페이지에서 끝까지 남은 바이트 수 계산
        size_t page_left = PGSIZE - pg_ofs (usr);

        // 남은 전체 바이트와 현재 페이지에서 가능한 바이트 중 더 작은 만큼만 이동
        size_t chunk = left < page_left ? left : page_left;

        usr  += chunk;  // 검사할 포인터를 다음 영역으로 이동
        left -= chunk;  // 검사해야 할 남은 바이트 수 갱신
    }
}

본인은 이 코드에서 while 문 안에서 check_page라는 함수를 호출하는 것을 발견했다 !

그래서 check_page() 함수를 확인해보았다.

기존의 check_page() 함수

static bool
check_page (const void *uaddr) {
     return uaddr != NULL &&
            is_user_vaddr(uaddr) &&
            pml4_get_page (thread_current ()->pml4, uaddr) != NULL;
}

기존에 구현해놓은 로직을 보면

  • 현재 프로세스의 PML4 페이지 테이블에서 해당 주소가 실제로 매핑되었는가 검사를 한다.

  • 즉, 이 함수는 uaddr -> 커널 주소로 변환이 가능한지 판단한다.

이 로직의 한계는

  • 아직 claim되지 않은 페이지(Lazy Load, Swap Out된 페이지 등)는 거짓으로 판단된다 !

  • vm_claim_page() 이전의 정상적인 유저 주소들도 거부를 당함 ㅠㅠ

이러한 문제들을 발견하여

변경된 check_page() 함수

static bool
check_page (const void *uaddr) {
    if (uaddr == NULL || !is_user_vaddr(uaddr))
        return false;

    void *va = pg_round_down(uaddr);
    struct supplemental_page_table *spt = &thread_current()->spt;
    return spt_find_page(spt, va) != NULL;
}

이런 식으로 구조를 바꾸었다 :

  • 주소가 유저 영역인지 확인 후, SPT에 등록된 페이지인지 여부만 판단한다

  • 아직 매핑되지 않았더라도 → lazy load 예정, swap-out 상태라면 → OK

로직 변경으로

  • 가상 메모리 시스템의 전체 설계와 일관되게 동작하고

  • Lazy Load, Swap In 등 페이지 폴트 후 로딩될 수 있는 주소도 안전하다고 판단이 가능하게 되었고

  • vm_claim_page() 호출을 유도하는 조건 검사로써 적절하게 되었다 !

항목기존 버전변경된 버전
판단 기준PML4 매핑 여부SPT 등록 여부
Lazy Page 지원❌ 거부함✅ 허용
Swap-out된 페이지❌ 거부함✅ 허용
페이지 폴트 전 허용❌ 안됨✅ 가능
보안성낮음 (직접 매핑 확인이 전부)높음 (VM 상태 기반 정밀 판단)

process.c

process.c 내부에 -DVM 플래그가 있을 시에 동작하는 코드 영역이 있는데 그 부분을 추가 구현 & 수정 해주었다 !

먼저 알아볼 함수는 lazy_load_segment() 함수이다

lazy_load_segment() - 페이지 폴트 시 파일로부터 세그먼트 로딩

이 함수는 페이지가 처음 접근되었을 때(vm_claim_page() 내부 → swap_in() 호출)
해당 페이지의 실제 데이터를 실행 파일로부터 읽어와 물리 프레임에 로드하는 역할을 수행한다.

즉, "지금은 로딩하지 않고, 나중에 fault 날 때 로딩할게요"라는
Lazy Loading(지연 로딩) 전략의 구현체다.

우리는 여기서 load_info 라는 구조체를 선언해주어야 한다.

구조체 : struct load_info

struct load_info {
	struct file *file;         // ELF 실행 파일 포인터
	off_t ofs;                 // 해당 데이터의 파일 내 오프셋
	uint32_t read_bytes;      // 파일에서 읽어야 할 바이트 수
	uint32_t zero_bytes;      // 이후 0으로 채울 바이트 수
};
  • 이 구조체는 페이지 초기화 시 aux 포인터로 전달된다

  • read_bytes + zero_bytes == PGSIZE 를 만족

struct load_info *load_info = (struct load_info *)aux;
  • 초기화 시 넘겨받았던 lazy-loading 정보를 불러온다

1. 파일에서 필요한 데이터 읽어오기

if (load_info->read_bytes > 0) {
	file_seek(load_info->file, load_info->ofs);

	if (file_read(load_info->file, page->frame->kva, load_info->read_bytes) 
	    != (int)(load_info->read_bytes)) {
		palloc_free_page(page->frame->kva);
		return false;
	}
}
  • 파일의 ofs 위치부터 read_bytes만큼 현재 페이지 프레임(kva)에 읽어옴

  • 읽기에 실패하면 프레임 해제 후 실패 처리

2. 나머지 공간은 0으로 초기화

memset(page->frame->kva + load_info->read_bytes, 0, load_info->zero_bytes);
  • ELF Segment는 일부만 실제로 파일에 존재하고, 나머지는 BSS같은 빈 공간일 수 있음

  • 이를 0으로 채우는 작업 수행

return true;
  • 읽기 및 초기화에 성공하면 true반환 → 페이지가 메모리에 완전히 로딩됨

호출 경로 예시

load_segment()vm_alloc_page_with_initializer(..., lazy_load_segment, aux)
 → page fault 발생 → vm_claim_page()vm_do_claim_page()swap_in()
       → page->operations->swap_in() == lazy_load_segment() 호출

load_segment() - ELF 파일의 세그먼트를 Lazy Loading으로 등록

유저 프로그램을 실행할 때, 실행 파일의 각 segment (code/data 영역)를 메모리에 올려야 한다.
하지만 모든 데이터를 미리 올리면 비효율적이므로, 실제 접근 시점에 로딩하는 Lazy Loading 방식을 사용한다.

이 함수는 ELF 로더에서 load_segment()로 호출되며,
가상 주소 공간에 필요한 페이지들을 Lazy Load 방식으로 등록한다.

이 함수도 인자값에 대해 알아보자

static bool
load_segment (struct file *file, off_t ofs, uint8_t *upage,
              uint32_t read_bytes, uint32_t zero_bytes, bool writable)
인자설명
fileELF 실행 파일 포인터
ofs해당 세그먼트의 파일 내 오프셋
upage매핑을 시작할 유저 가상 주소
read_bytes파일로부터 읽어야 할 총 바이트 수
zero_bytes그 외 0으로 채워야 할 바이트 수
writable쓰기 가능 여부

사전 조건 검사

ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
ASSERT(pg_ofs(upage) == 0);
ASSERT(ofs % PGSIZE == 0);
  • 총 페이지 크기와 정렬이 정확해야 가상 메모리로 올릴 수 있다

  • 페이지 단위로 올라가야 하기 때문에 엄격하게 체크해야 함 !

루프 : 페이지 단위로 분할해서 등록

while (read_bytes > 0 || zero_bytes > 0)

각 반복마다 :

  1. 현재 페이지에서 읽을 바이트 수와 0으로 채울 바이트 수를 계산 (합쳐서 항상 PGSIZE가 되도록)
size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
size_t page_zero_bytes = PGSIZE - page_read_bytes;
  1. lazy loading용 보조 정보(load_info)를 생성해서 aux로 넘김
struct load_info *load_info = malloc(sizeof(struct load_info));
load_info->file = file_duplicate(file);
load_info->ofs = ofs;
load_info->read_bytes = page_read_bytes;
load_info->zero_bytes = page_zero_bytes;

file_duplicate()를 사용하는 이유:

Lazy load 시점이 훨씬 나중일 수도 있으므로,
현재 열린 파일이 닫혀도 해당 파일을 안전하게 참조할 수 있도록 하기 위함.

  1. vm_alloc_page_with_initializer() 호출
if (!vm_alloc_page_with_initializer(VM_FILE, upage, writable, lazy_load_segment, load_info))
	return false;
  • lazy_load_segment()는 페이지 폴트 시점에 호출될 함수

  • 여기서는 실제 프레임을 만들지 않고 예약만 해두는 상태

  1. 포인터 및 바이트 수 업데이트
read_bytes -= page_read_bytes;
zero_bytes -= page_zero_bytes;
upage += PGSIZE;
ofs += page_read_bytes;

최종적으로 하는 일

  • ELF의 한 segment를 여러 페이지로 분할하여

  • 각 페이지를 lazy loading 방식으로 SPT에 등록

  • 실제 로딩은 vm_claim_page()swap_in()lazy_load_segment() 순으로 이루어짐

호출 위치

load()load_segment()vm_alloc_page_with_initializer()lazy_load_segment()
  • ELF Loader 내부에서 사용

  • 실행 파일의 .text, .data, .bss 등을 메모리에 올릴 준비를 하는 함수


setup_stack() - 유저 스택 초기 페이지 설정

PintOS에서 유저 프로그램은 USER_STACK (0x47480000)에서 스택을 시작한다.
스택은 페이지 단위로 아래로 확장되며,
처음 실행 시점에는 최상단 한 페이지만 미리 확보되어 있어야 한다.

이 함수는 그 한 페이지를:

  • SPT에 등록하고,

  • 즉시 물리 메모리를 할당 및 매핑

  • rsp를 USER_STACK으로 설정

하는 역할을 한다.

void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);
  • USER_STACK 바로 아래 주소를 스택의 바닥(bottom)으로 설정

  • 이 한 페이지만 미리 확보함 (이후 자동 확장은 stack growth에서 처리)

if (vm_alloc_page_with_initializer(VM_ANON | VM_MARKER_0,
                                    stack_bottom, 1, NULL, NULL))
  • 익명 페이지로 등록 (VM_ANON)

  • 추가적으로 VM_MARKER_0를 OR하여 이 페이지는 "스택이다" 라는 표시를 남김

  • 이후 vm_try_handle_fault() 등에서 stack growth 여부 판단 시 사용

  • lazy-load가 아니므로 init, aux는 NULL

success = vm_claim_page(stack_bottom);
  • 페이지를 즉시 확보

  • 물리 프레임을 할당하고 PML4에 매핑 → swap_in() 호출됨

if (success)
	if_->rsp = USER_STACK;
  • rsp (유저 스택 포인터)를 스택 최상단으로 초기화

  • 이 주소부터 인자 스택, 환경 변수 등을 push 할 수 있게 됨.

호출 위치

process_exec()load()setup_stack()

VM_MARKER_0의 역할은?

  • 페이지가 스택인지 여부를 식별하는 flag 역할

  • vm_claim_page()vm_try_handle_fault()에서 해당 페이지가 스택인 경우
    stack growth 조건을 활성화하는 데 사용됨


__do_fork()

fork 시 자식 프로세스는 부모의 주소 공간을 그대로 복사해야 하므로,
보조 페이지 테이블(SPT)도 함께 복제해야 한다

PintOS에서는 VM 기능이 켜져 있을 때(#ifdef VM)만 SPT 관련 코드가 활성화되므로,
다음 코드를 __do_fork() 함수 내부의 #ifdef VM 블록 안에 작성해야 한다 :

#ifdef VM
	// 보조 페이지 테이블 초기화 및 복사 (VM 기능이 켜져 있는 경우)
	supplemental_page_table_init(&current->spt);
	supplemental_page_table_copy(&current->spt, &parent->spt);
#else
  • supplemental_page_init()
    → 자식 프로세스의 SPT 구조를 빈 상태로 초기화

  • supplemental_page_table_copy()
    → 부모의 SPT를 순회하며 자식에게 그대로 복제

    • lazy 상태인 페이지는 lazy로 복사

    • 초기화된 페이지는 프레임까지 확보해서 memcpy()로 복사

왜 #ifdef VM 안에서 ?

  • supplemental_page_table_* 함수들은 VM 활성화 조건 하에서만 정의됨

  • 만약 이 코드가 #ifdef VM 밖에 있으면 컴파일 오류가 발생한다

  • 따라서 fork에서 테스트를 통과하려면 반드시 이 조건부 컴파일 블록 내에 있어야 한다 !


이전 포스팅과 이번 포스팅까지 잘 따라온다면

pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
pass tests/userprog/fork-once
pass tests/userprog/fork-multiple
pass tests/userprog/fork-recursive
pass tests/userprog/fork-read
pass tests/userprog/fork-close
pass tests/userprog/fork-boundary
pass tests/userprog/exec-once
pass tests/userprog/exec-arg
pass tests/userprog/exec-boundary
pass tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
pass tests/userprog/exec-read
pass tests/userprog/wait-simple
pass tests/userprog/wait-twice
pass tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
pass tests/userprog/multi-recurse
pass tests/userprog/multi-child-fd
pass tests/userprog/rox-simple
pass tests/userprog/rox-child
pass tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
FAIL tests/vm/pt-grow-stack
pass tests/vm/pt-grow-bad
FAIL tests/vm/pt-big-stk-obj
pass tests/vm/pt-bad-addr
pass tests/vm/pt-bad-read
pass tests/vm/pt-write-code
FAIL tests/vm/pt-write-code2
FAIL tests/vm/pt-grow-stk-sc
pass tests/vm/page-linear
pass tests/vm/page-parallel
pass tests/vm/page-merge-seq
FAIL tests/vm/page-merge-par
FAIL tests/vm/page-merge-stk
FAIL tests/vm/page-merge-mm
pass tests/vm/page-shuffle
FAIL tests/vm/mmap-read
FAIL tests/vm/mmap-close
FAIL tests/vm/mmap-unmap
FAIL tests/vm/mmap-overlap
FAIL tests/vm/mmap-twice
FAIL tests/vm/mmap-write
FAIL tests/vm/mmap-ro
FAIL tests/vm/mmap-exit
FAIL tests/vm/mmap-shuffle
FAIL tests/vm/mmap-bad-fd
FAIL tests/vm/mmap-clean
FAIL tests/vm/mmap-inherit
FAIL tests/vm/mmap-misalign
FAIL tests/vm/mmap-null
FAIL tests/vm/mmap-over-code
FAIL tests/vm/mmap-over-data
FAIL tests/vm/mmap-over-stk
FAIL tests/vm/mmap-remove
FAIL tests/vm/mmap-zero
FAIL tests/vm/mmap-bad-fd2
FAIL tests/vm/mmap-bad-fd3
FAIL tests/vm/mmap-zero-len
FAIL tests/vm/mmap-off
FAIL tests/vm/mmap-bad-off
FAIL tests/vm/mmap-kernel
FAIL tests/vm/lazy-file
pass tests/vm/lazy-anon
FAIL tests/vm/swap-file
FAIL tests/vm/swap-anon
FAIL tests/vm/swap-iter
pass tests/vm/swap-fork
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
pass tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
pass tests/filesys/base/syn-write
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
FAIL tests/vm/cow/cow-simple
37 of 141 tests failed.

Project 2의 테스트는 전부 pass가 나온다 !

profile
Before Sunrise

0개의 댓글