발표 자료 (Week 12‐13) - sihyun10/pintos-lab-vm GitHub Wiki
[Week 12~13] Virtual Memory 발표 자료
저는 tests/userprog/fork-read.c
테스트에서 마주쳤던 오류 해결과정에 대해 발표를 준비하였습니다.
이 테스트는 부모가 먼저 read()
를 실행한 다음 fork()
로 생성된 자식이 이어서 읽을 수 있는지 확인하는 테스트입니다.
실패 원인을 분석해보았을때,
VM_UNINIT
즉 lazy 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
바이트까지 실제로 파일에서 읽어서 메모리에 적재한다.
그리고 끝나고 나서 aux
를 free()
하는 코드를 넣었다면 이 시점에서 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으로 채워줌으로써 오류나 예측 불가능한 동작을 방지함