본문 바로가기

[pintOS] uninit 페이지 구현기

@정소민fan2025. 6. 3. 11:25

이제 지연 로딩의 바탕이 되는 uninit, 미초기화 페이지를 구현해보자

미초기화 페이지가 뭔지 아직도 모른다면 https://gooch123.tistory.com/25 여기를 참고하자

 

미초기화 페이지를 위해 구현해야할 함수는 다음과 같다

static bool load_segment (struct file *file, off_t ofs, uint8_t *upage,
        uint32_t read_bytes, uint32_t zero_bytes, bool writable);
        
static bool lazy_load_segment (struct page *page, void *aux);

bool vm_alloc_page_with_initializer (enum vm_type type, void *va,
        bool writable, vm_initializer *init, void *aux);
        
static bool uninit_initialize (struct page *page, void *kva);

void vm_anon_init (void);

bool anon_initializer (struct page *page, enum vm_type type, void *kva);

하나씩 어떻게 구현되었는지 알아보자

 

함수 구현하기

함수 구현 이전에, 위 함수들을 구현하면 어느 영역의 어떤 페이지들이 미초기화 페이지로 만들어지는지 확인해보자

pintOS 메모리 구조

위 그림은 pintOS 메모리 구조를 나타낸 것이다. pintOS는 x86-64 위에서 작동하는 OS이기에 호스트의 물리 주소를 알 수가 없다

대신 커널 가상 주소를 물리 주소와 일대일 매핑시켜 사용한다고 한다. 그러니까  그림 중간의 굵은 선 (kern_base)은 사실 모두 물리 주소처럼 동작한다는 것이다. kern_base 밑의 영역은 이제 유저 프로세스의 영역, 즉 가상 메모리 영역이다.

pintOS에서는 유저 프로그램에게 동적 할당이 제공되지 않기에 heap 영역은 없다.

load_segment

이 함수는 메모리 구조에서 가상 메모리 영약의 코드 세그먼트나 bss 같은 세그먼트 영역을 미초기화 페이지로 예약해주는 함수이다.

static bool
load_segment(struct file *file, off_t ofs, uint8_t *upage,
             uint32_t read_bytes, uint32_t zero_bytes, bool writable)
{
    ASSERT((read_bytes + zero_bytes) % PGSIZE == 0);
    ASSERT(pg_ofs(upage) == 0);
    ASSERT(ofs % PGSIZE == 0);

    while (read_bytes > 0 || zero_bytes > 0)
    {
      /* 이 페이지를 어떻게 채울지 계산합니다.
       * FILE에서 PAGE_READ_BYTES 바이트를 읽고
       * 남은 PAGE_ZERO_BYTES 바이트는 0으로 초기화합니다. */
      size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE; // 4KB까지만 읽어라
      size_t page_zero_bytes = PGSIZE - page_read_bytes;                  // 0 패딩 사이즈는 4KB - read_byte

      /* TODO: Set up aux to pass information to the lazy_load_segment. */
      struct lazy_load_info *aux = malloc(sizeof(struct lazy_load_info)); // 전달해야할 인자
      if (aux == NULL)
         return false;

      aux->file = file_reopen(file);
      aux->offset = ofs;
      aux->readbyte = page_read_bytes;
      aux->zerobyte = page_zero_bytes;

      if (!vm_alloc_page_with_initializer(VM_ANON, upage,
                                          writable, lazy_load_segment, aux))
         return false;

      /* Advance. */
      read_bytes -= page_read_bytes;
      zero_bytes -= page_zero_bytes;
      ofs += page_read_bytes;
      upage += PGSIZE;
    }
    return true;
}

인자로 총 6개를 받는다

  • file : 예약할 데이터를 가진 파일
  • ofs : 파일의 어느 오프셋부터 시작할지
  • upage : 가상 페이지의 주소
  • read_byte : 파일에서 읽어올 바이트 크기
  • zero_byte : padding 바이트 크기
  • writable : 해당 페이지가 쓰기 권한이 있는지

while 루프를 돌면서 읽어야 할 영역이 크다면, 여러 개의 페이지를 만들도록 하고 있다. 여기서 특히 잘 봐야할 점은 aux에 lazy_load_info를 만들어서 넘겨준다는 것

lazy_load_info는 이 미초기화 페이지에 처음으로 접근해서 페이지 폴트가 발생했을 때 어디에서 어떻게 얼마만큼 데이터를 가져와야 하는지에 대한 메타데이터를 저장해두는 구조체다. lazy_load_segment에서는 이 구조체를 사용하여 로드를 완료할 것이다.

struct lazy_load_info
{
    struct file *file;
    off_t offset;
    size_t readbyte;
    size_t zerobyte;
};

사실 load_segment에서 받는 인자들은 바로 로드에 사용되는 것이 아니라 지연 로딩을 위한 메타데이터일 뿐이라는 것 !!

이거 완전 씽크빅이네

vm_alloc_page_with_initializer

우리는 미초기화 페이지를 생성할 때 반드시 이 함수를 사용해서 생성해야 한다.

bool vm_alloc_page_with_initializer(enum vm_type type, void *upage, bool writable,
                                    vm_initializer *init, void *aux)
{

    ASSERT(VM_TYPE(type) != VM_UNINIT)

    struct supplemental_page_table *spt = &thread_current()->spt;

    /* 이미 해당 page가 SPT에 존재하는지 확인합니다 */
    if (spt_find_page(spt, upage) == NULL)
    {
      /* TODO: VM 타입에 따라 페이지를 생성하고, 초기화 함수를 가져온 뒤,
       * TODO: uninit_new를 호출하여 "uninit" 페이지 구조체를 생성하세요.
       * TODO: uninit_new 호출 후에는 필요한 필드를 수정해야 합니다. */
      bool (*page_initializer)(struct page *, enum vm_type, void *kva);
      struct page *page = malloc(sizeof(struct page));

      if (page == NULL)
      {
         goto err;
      }

      switch (VM_TYPE(type))
      {
      case VM_ANON:
         page_initializer = anon_initializer;
         break;
      case VM_MMAP:
      case VM_FILE:
         page_initializer = file_backed_initializer;
         break;
      default:
         free(page);
         goto err;
         break;
      }

      uninit_new(page, upage, init, type, aux, page_initializer);
      page->writable = writable;

      /* TODO: 생성한 페이지를 spt에 삽입하세요. */
      if (!spt_insert_page(spt, page))
      {
         // 실패 시 메모리 누수 방지 위해 free
         free(page);
         // 실패 했으니까 에러로 가야겠지?
         goto err;
      }

      return true;
    }

err:
   return false;
}

여기서는 인자로 5개를 받는다

  • type : 미초기화 페이지가 초기화될 때 어느 페이지로 초기화될지 타입을 정한다
  • upage : 어느 가상 주소 영역에 이 페이지가 할당될지 정한다
  • writable : 이 페이지가 쓰기가 가능한지 나타낸다
  • init : 지연 로딩시에 실제 데이터를 적재할 함수
  • aux : 페이지가 초기화 될 때 필요한 메타데이터나 다른 부가 정보를 넘겨받는다

먼저 if 문으로 해당 가상 영역(upage)에 이미 할당된 페이지가 있는지 확인한다. 왜? 이미 있는 영역에 덮어씌워지면 안되니까

여기서 우린 page_initializer도 만들어야 한다. page_initializer는 이 페이지가 지연 로딩될 때 어떤 페이지로 초기화될지에 대한 로직을 담은 함수 포인터이다.

 

그럼 page_initializer와 인자로 받은 vm_initializer가 무엇이 다른가?

page_initializer는 단지 이 미초기화 페이지가 익명 페이지나 파일 페이지로 초기화되도록 해주는 것 뿐이고, 이 페이지에 매핑된 물리 주소에 실제 물리 데이터를 적재해주는 것은 vm_initializer가 해야한다.

 

switch 문에서 page를 동적 할당해주고, 넘겨받은 타입에 따라 page_initializer에 필요한 초기화 함수를 담아준다

그리고 uninit_new를 통해 실제 미초기화 페이지를 만들어주는데, 이 함수는 절대 수정하지 말라고 주석에 적혀있으니 딱히 건드리지는 말자.

 

그리고 page의 writable을 설정해준다. 여기서 위치가 중요한데, uninit_new 보다 먼저 page의 writable을 설정하면 uninit_new를 하면서 writable이 쓰레기 값이 되어버린다. uninit_new가 내부적으로 page의 값 자체를 덮어씌워주는 함수라서 그런 것 같다.

 

그 다음, 만들어진 미초기화 페이지를 spt_insert_page를 통해 SPT에 삽입한다.

lazy_load_segment

이 함수는 load_segment에서 vm_alloc_page_with_initializer를 호출할 때 인자로 넘겨주었던 함수이다.

미초기화 페이지에 맨 처음 접근해서 페이지 폴트가 발생했을 때 여러 단계를 거쳐 이 lazy_load_segment가 호출되게 된다.

어느 과정을 거쳐 호출되는지 모르겠다고? 그럼 여기를 보자 https://gooch123.tistory.com/25#4

bool lazy_load_segment(struct page *page, void *aux)
{
   /* TODO: Load the segment from the file */
   /* TODO: 이 함수는 해당 VA(가상 주소)에서 첫 페이지 폴트가 발생할 때 호출됩니다. */
   /* TODO: 이 함수를 호출할 때 VA는 사용할 수 있습니다. */
   struct lazy_load_info *lazy_info = (struct lazy_load_info *)aux;
   struct file *read_file = lazy_info->file;

   off_t my_read_byte = file_read_at(read_file, page->frame->kva, lazy_info->readbyte, lazy_info->offset);

   if (my_read_byte != (off_t)lazy_info->readbyte)
   {
      return false;
   }
   memset(page->frame->kva + lazy_info->readbyte, 0, lazy_info->zerobyte);

   free(lazy_info);
   return true;
}

여러 단계를 거쳐 넘겨받은 aux를 통해 파일에서 데이터를 읽어 매핑된 물리 주소에 write 해야 한다.

bogus 폴트로 인해 물리 프레임 공간을 할당받았으므로 프레임의 kva가 이 페이지에 매핑된 물리 주소이다.

file_read_at을 통해 실제 파일에서 데이터를 읽어 넘겨받은 물리 주소에 write 할 수 있다. 그리고 실제로 읽은 바이트 크기를 반환하는데 이를 통해 실패하지 않고 읽었는지 검사할수 있다.

그리고 마지막으로 padding 값이 있을 수 있으니 memset으로 0으로 만들어주자.

 

그리고 남은건 vm_anon_init, uninit_initialize, anon_initializer인데, 이 부분은 지금 딱히 추가하거나 수정할 부분이 없으니 넘어가자 !!

 

아 그리고 한가지 더

vm_try_handle_fault

bool vm_try_handle_fault(struct intr_frame *f UNUSED, void *addr UNUSED,
						 bool user UNUSED, bool write UNUSED, bool not_present UNUSED)
{
    struct supplemental_page_table *spt UNUSED = &thread_current()->spt;
    struct page *page = spt_find_page(spt, addr);
    if (page == NULL)
        return false; // 찐 폴트

    ASSERT(page->operations != NULL && page->operations->swap_in != NULL);

    return vm_do_claim_page(page);
}

이 함수는 exception.c의 page_fault 에서 호출되는 함수이다. 여기서 spt_find_page를 통해 해당 페이지가 예약되어있는지 확인하고 vm_do_claim_page로 초기화를 진행해주자

 

 

포스팅 끝 포스팅 재미없엉 핀토스 코딩이 더 재밌엉

정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차