발표 자료 (Week 12‐13) - sihyun10/pintos-lab-vm GitHub Wiki

[Week 12~13] Virtual Memory 발표 자료


저는 tests/userprog/fork-read.c 테스트에서 마주쳤던 오류 해결과정에 대해 발표를 준비하였습니다.

이 테스트는 부모가 먼저 read()를 실행한 다음 fork()로 생성된 자식이 이어서 읽을 수 있는지 확인하는 테스트입니다.

실패 원인을 분석해보았을때,
VM_UNINITlazy loading 상태 페이지인 경우,
lazy loading을 위한 설정만 해두고, 실제 데이터는 아직 넣지 않습니다.

부모든 자식이든 해당 주소를 처음 읽으려고 접근하는 순간
lazy_load_segment()가 호출됩니다.

제 코드의 문제점으로는
자식을 fork()하기 전에 부모가 읽을 땐, 문제가 없습니다.
하지만 자식을 fork()한 후에 자식이 읽으려할때, aux 정보가 필요한데
이전에 부모가 읽기 진행 후 aux정보를 free하였기에 문제가 발생하게 됩니다.

즉, 자식은 사라진 aux를 참조하려 하면서 실패하게 됩니다.
그래서 free() 해주는 부분을 삭제해서 이 테스트를 통과하게 하였습니다.

그치만 aux를 동적 할당하기에 언젠가 free를 해주어야하는데, 언제가 적절할까? 생각해보았습니다.
부모와 fork()된 자식이 같은 aux를 참조하게 하지 않고,
aux자체를 복제하여서 부모 따로 자식 따로 aux를 가지고있어서 free()를 했을때,
서로 영향을 받지않도록 하면 되지않을까? 생각해보았습니다.


추가적으로 발표를 준비하면서 정리했던 내용을 적어보고자한다.

fork-read.c 테스트 동작과정

void
test_main (void) 
{
  pid_t pid;
  int handle;
  int byte_cnt;
  char *buffer;

  CHECK ((handle = open ("sample.txt")) > 1, "open \"sample.txt\"");
  buffer = get_boundary_area () - sizeof sample / 2;
  byte_cnt = read (handle, buffer, 20);
  
  if ((pid = fork("child"))){
    wait (pid);

    byte_cnt = read (handle, buffer + 20, sizeof sample - 21);
    if (byte_cnt != sizeof sample - 21)
      fail ("read() returned %d instead of %zu", byte_cnt, sizeof sample - 21);
    else if (strcmp (sample, buffer)) {
        msg ("expected text:\n%s", sample);
        msg ("text actually read:\n%s", buffer);
        fail ("expected text differs from actual");
    } else {
      msg ("Parent success");
    }

    close(handle);
  } else {
    msg ("child run");

    byte_cnt = read (handle, buffer + 20, sizeof sample - 21);
    if (byte_cnt != sizeof sample - 21)
      fail ("read() returned %d instead of %zu", byte_cnt, sizeof sample - 21);
    else if (strcmp (sample, buffer)) 
      {
        msg ("expected text:\n%s", sample);
        msg ("text actually read:\n%s", buffer);
        fail ("expected text differs from actual");
      }

    char magic_sentence[17] = "pintos is funny!";
    memcpy(buffer, magic_sentence, 17);

    msg ("Child: %s", buffer);
    close(handle);
  }
}
  • 부모가 먼저 read()를 한 번 수행한다.
byte_cnt = read (handle, buffer, 20);

이때, 해당 영역은 lazy loading상태임으로, page fault가 발생한다.
lazy_load_segment()가 호출되어 aux를 사용해서 0~20바이트까지 실제로 파일에서 읽어서 메모리에 적재한다.
그리고 끝나고 나서 auxfree()하는 코드를 넣었다면 이 시점에서 aux는 사라진다.

  • 그 다음 fork()를 호출한다.
if ((pid = fork("child"))){

supplemental_page_table_copy()가 실행된다.
VM_UNINIT 상태의 페이지는 아직 메모리에 없는 페이지들만 그대로 복사한다.
즉, 0~20바이트까지 읽힌 페이지는 복사가 되지않고,
나머지 페이지들(20~끝)은 VM_UNINIT으로 복사되며, aux포인터도 그대로 공유된다.

  • 자식이 자기 메모리의 아직 로딩되지 않은 주소에 접근한다.
byte_cnt = read (handle, buffer + 20, sizeof sample - 21);

이때, 자식도 page fault가 발생하여, lazy_load_segment()가 호출된다.
그런데 이때, 부모가 위의 과정에서 free(aux)를 해버렸다면
자식은 사라진 aux를 참조하려 하면서 fault가 발생하게 된다.


lazy_load_segment() 코드

static bool
lazy_load_segment(struct page *page, void *aux)
{
  struct load_info *info = (struct load_info *)aux;

  struct file *file = info->file;
  off_t ofs = info->ofs;
  size_t page_read_bytes = info->read_bytes;
  size_t page_zero_bytes = info->zero_bytes;

  // 실제 프레임의 커널 가상 주소 얻기
  uint8_t *kva = page->frame->kva;

  // 파일에서 필요한 만큼 읽기
  if (file_read_at(file, kva, page_read_bytes, ofs) != (int)page_read_bytes)
  {
    free(info);
    return false;
  }

  // 남은 영역은 0으로 채우기
  memset(kva + page_read_bytes, 0, page_zero_bytes);
  return true;
}
if (file_read_at(file, kva, page_read_bytes, ofs) != (int)page_read_bytes)
{
  free(info);      // 실패한 경우, 실행됨
  return false;
}

memset(kva + page_read_bytes, 0, page_zero_bytes);  // 읽기 성공한 경우 실행됨
return true;

읽기 성공한 뒤,
그렇다면 왜 남은 영역을 0으로 채울까?

  • 정상적인 메모리 초기화
    • 명시적으로 0으로 채워줌으로써 오류나 예측 불가능한 동작을 방지함