WEEK11 pintOS 3주차 가상메모리 회고

채림·2023년 5월 17일
0

이번주도 망했다... 분명 로직 회의할 때까지는 좋았고 금방 할 줄 알았는데 구현하고 나서 디버깅이 하루종일... 문제는 유의미하게 트러블슈팅한 건 없이 왜 안되는지 모르겠는 날만 며칠이라는 거다.

Trouble Shooting

1. 왜 유저 데이터를 커널영역에 저장할까?

11주차의 anonymous 코드를 짜다 보면, 유저 파일을 읽어서 palloc으로 할당받은 페이지에 저장하는 부분이 나온다. 처음에는 아무 생각 없이 그냥 저장 공간이 필요하길래 palloc으로 할당받았는데, 생각해보니 pallocpage alloc이기 때문에 커널 영역에 공간이 할당된다. 그렇다면 유저 코드를 커널 영역에 저장해도 되는가?

우리가 가장 익숙하게 아는 가상 메모리 구조는 이런 그림이다. 지금까지는 코드를 읽어서 저 맨 밑의 코드영역에 저장하는 줄 알았는데 사실 커널의 유저풀에 저장한다고?

깃북을 정독한 사람이라면 알았겠지만 핀토스에서는 커널 영역만 실제 물리 메모리에 1:1로 매핑이 되어있다. 우리가 사용하는 가상 메모리는 모두 "가상"이기 때문에 실제 데이터를 저장할 수 없고 해당 가상 메모리에 대응되는 물리 메모리 영역이 있어야 거길 찾아가서 데이터를 넣을 수 있는데, 핀토스는 커널영역만 물리메모리에 매핑되어 있기 때문에 커널영역에 저장할 수 밖에 없는 것이다.
그렇다면 다시 의문이 생긴다. 왜 핀토스는 커널 영역만 매핑을 해두어서 유저데이터를 유저영역 놔두고 커널영역에 저장하게 하는가? 그렇다면 분명한 주소값이 존재하는 저 큰 유저영역은 뭣하러 있는 것인가?

이건 우리의 추측인데, 그래야 가상-물리 메모리간 주소값 계산이 쉬워지기 때문이다. include/threads/vaddr.hvtop라는, virtual to physical 주소 전환 메크로가 있는데, 단순히 가상 주소에서 KERN_BASE를 빼는 것이 전부이다.

만약 유저 영역과 커널 영역을 따로 일부분씩만 물리메모리에 매핑했으면 계산이 좀 더 복잡해진다. 커널 영역일 때 물리 주소를 구하기 위해 연산해야 하는 값과 유저 영역일때의 값이 달라진다. 물론 엄청나게 복잡한 계산은 아니지만, 조금이라도 복잡해지면서 굳이 유저영역을 매핑할 이유가 없었던 것 아닐까?

이제 마지막 의문이 하나 남았다. 그렇다면 커널영역에 유저데이터를 포함한 모든 데이터를 저장할건데 굳이 유저 영역 주소를 따로 만들고, 그 유저 저장공간은 커널 영역을 가리키게 해서 물리메모리에 접근할 필요가 있나?
이것도 추측성이지만 우리가 지금까지 배워온 대로, 유저 데이터와 커널 데이터를 구분해서 보호하기 위해서이지 않을까 싶다. 유저 프로그램은 유저 영역의 주소값으로 데이터에 접근하려고 하고, 접근이 발생할 때 시스템콜이 호출되면서 커널이 커널공간의 유저풀에서 유저 관련 데이터를 갖다주는 로직이 된다. 저번주 코드에서 시스템콜이 호출되면 넘겨받은 주소가 유저공간의 주소인지 커널공간의 주소인지를 판별하는 check_address라는 함수를 작성했는데, 그게 이 로직을 실행하기 위한 검증이었던 것 같다.

추가로, 이건 '핀토스'이기 때문에 가능한 형태이다. 실제 리눅스에서는 유저영역이 물리메모리와 매핑되어있기도 한다. 핀토스는 교육용으로 제작된 운영체제이기 때문에 구현상의 편의를 위해 이렇게 만들지 않았을까?



2. anonymous page는 파일 기반이 아닌데 왜 유저 프로그램을 파일에서 읽어올 때 anonymous로 읽어올까?

깃북을 읽어보면 anonymous pagefile-backed page의 반댓말로, 파일을 기반으로 하지 않은 페이지라고 한다. 스택의 힙이나 큐에 사용되는, 파일에서 읽어오지 않았는데 실행 파일을 실행하는 중에 생성되는 데이터를 저장하는 페이지라는 뜻이다.
load_segment에서 anonymous 페이지를 할당하면서 initializer로 lazy_load_segment를 넣어준다. 그리고 lazy_load_segment에는 file_read가 들어간다. 그렇다면 익명 페이지를 파일에서 읽어와서 넣어준다는 말인가?
검색하다가 한 블로그에서 전에 조교님한테 같은 질문을 했을 때의 답변을 찾았다. 그런데 우리 반에서도 같은 날 동기가 같은 질문을 올렸길래 우리 반 답변을 첨부해본다.

결론적으로 원래 anonymous의 정의는 파일에서 읽어오는 것이 아니지만, 유저 프로그램을 읽어와서 수정하면 안되기때문에 anonymous로 로드하는 것 같다.



3. uninit_new에서의 page fault

uninit_new에는 uninit 페이지로 만들 페이지 구조체 포인터와, 그 페이지 구조체가 참조하는 가상 주소를 넘긴다. 처음에는 여기에 upageupage->va를 넘겼다. 그런데 uninit_new내부에서 페이지 폴트가 발생했다.
vm_alloc_page_with_initializer에서 받아온 upage가 무엇인고 하니 load_segment에서 호출하면서 넘긴 upage이고, load_segment에서의 upageload에서 넘긴 mem_page이다. 그리고 이 mem_page는 파일 헤더를 읽어서 pdhr->va_addr를 비트마스킹한 무언가이다. 그렇다면 이 주소는 파일이 위치한 주소라는건데, uninit_new에서는 이 주소에 대한 정보를 나타낼 page 구조체를 만들어야 한다. 따라서 upage를 넘기는 것이 아니라 page 구조체를 위한 포인터를 할당받아서 그걸 넘겨야 한다. 그리고 그 page 구조체가 가리키는 위치가 upage이므로 두번째 인자에는 upage를 그대로 넘기면 된다.
처음에 page fault가 발생했던 이유는 upage는 그냥 파일 위치를 나타내는 void 포인터인데 이걸 page 포인터로 만들어서 멤버 va를 가져오려고 하니, 이상한 접근이 되었던 것이다.

      struct page *new_page = (struct page *)malloc(sizeof(struct page));

      if (type == VM_ANON) {
        uninit_new(new_page, upage, init, type, aux, anon_initializer);
      }



4. load_segment의 aux

static bool
lazy_load_segment (struct page *page, void *aux) {
  struct file *file = (struct file *)aux;
  return file_read(file, page, PGSIZE);
}

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)
  {
    size_t page_read_bytes = read_bytes < PGSIZE ? read_bytes : PGSIZE;
    size_t page_zero_bytes = PGSIZE - page_read_bytes;

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

    read_bytes -= page_read_bytes;
    zero_bytes -= page_zero_bytes;
    upage += PGSIZE;
  }
  return true;
}

lozy_load_segment에서 받는 auxload_segment에서 넘겨주는 aux이다. 이 자리에 file만을 넣어서 넘겨주었었는데 파일이 제대로 읽히지 않는 문제가 발생했다.
다시 생각해보니 기존(vm이전의, user program 때의) load_segment에서 파일을 읽었을 때는 받았던 offset 등의 인자를 모두 활용해서 읽었었는데 현재의 코드에서는 아무것도 사용하지 않는 것이 이상해서 파일 포인터뿐만 아니라 파일과 관련된 변수들을 다 구조체로 묶어서 aux로 넘겼다. 처음에는 file_read를 하면 어차피 읽은 그 위치까지 offset을 자동으로 옮겨주는데, 뭐하러 따로 받아서 file_seek로 설정해주나 싶었지만 꼭 필요한 작업이었다. 파일을 읽을 때 마다 오프셋이 바뀌는 건 맞지만, 한 파일에서도 여러 세그먼트를 읽을 것이고, 그 때마다 load_segment가 호출될텐데, 앞에서부터 순서대로 & 연속적으로 호출되리라는 보장이 없다. 따라서 load_segment에서 받은 오프셋을 알아야 그 위치부터 제대로 원하는 데이터들을 읽을 수 있는 것이다. 사실 file 포인터랑 오프셋 이외에는 넘겨주지 않아도 동작하기는 하는데, read_bytes는 에러 처리를 위해 넘겨주었고 zero_bytes 등은 lazy_load_segment 내에서 계산해도 되어서 넘겨주지 않았다.
그래도 제대로 동작하지 않았는데, 이건 aux 선언의 문제였다. aux를 지역변수로 선언하면 load_segment가 끝나면 사라지는 변수가 되는데, 정작 page fault가 발생해야lazy_load_segment가 호출되고 그 때 내부에서 aux를 사용하기 때문에 읽어들일 수 없게 된다.
또한 aux 선언을 while문 밖에 하는 것도 안된다. while문이 한 번 돌 때마다 한 페이지씩 읽는건데, while문 밖에서 aux를 선언하면 모든 페이지가 같은 aux를 참조하게 된다. 그러면 어느 페이지에서 먼저 page fault가 일어날 지 모르는 상태에서 오프셋을 잘못된 값으로 받을 수 있다.
추가적으로 lazy_load_segment에서 읽은 곳 이후부터 0으로 memset 해주어야 하는데, 파일을 읽고 남은 자리에 쓰레기값이 들어가있지 않게 하기 위해서이다!

5. 이번 주차 가장 핵심적인 가상메모리 개념

lazy_load에서 file_read 할 때 읽어서 쓰는 곳의 주소에도 고민이 있었다. 처음에는 page를 그대로 넣어주었는데, page->fram->kva가 맞다.
page는 우리가 유저 영역에 만든, 데이터 저장 공간에 대한 정보를 가지고 있는 구조체이고(page 자체는 커널영역에 위치해 있다), 얘랑 1:1로 연결되는 물리 공간에 대한 정보를 가지고 있는 것이 frame이다(얘도 커널 영역에 있다). framekva는 실제로 물리 메모리에 있는 데이터 공간을 할당받기 위해 가상 메모리에 만든 공간을 가리킨다. 그래서 get_frame 할 때 frame 구조체 자체를 위한 공간도 할당받았지만 그 안의 kva가 가리킬 공간도 할당받은 것이었다.
처음에는 이 개념이 이해가 가지 않아서 분명 가상메모리에는 어떠한 정보도 적히지 않고 모두 물리메모리에 적힌다고 했는데 그러면 kva가 가리키는 공간은 뭐하러 할당받아서 비워두는 것인가? 했는데, 커널 가상메모리와 물리메모리는 1:1로 매핑되기때문에 어떤 데이터를 저장하고 싶으면 무조건 가상 메모리에도 같은 크기만큼의 공간을 할당받긴 해야 한다. 그리고 이제 저 할당받은 공간의 주소들 중 하나에 접근하면 페이지테이블을 통해 실제 물리메모리로 이동해 데이터를 쓰는 것이다.

따라서 유저가 유저 영역에 데이터를 쓰려고 하면, 그 주소를 멤버로 가지고 있는 페이지를 찾아가서 페이지 구조체 안에 있는 프레임을 찾아가고, 프레임 안의 kva가 가리키는 주소에 쓰려고 하는데 이 때 페이지테이블을 통해 물리 주소로 변환해서 물리메모리에 쓰게 되는 것이 최종 과정이다.



6. page_round_down의 쓰임새

  • spt_find를 할 때는 찾는 주소값을 pg_round_down 해주어야 한다. spt의 page 구조체에 들어있는 va는 그 페이지의 시작주소인데, 우리가 찾으려는 주소는 페이지의 한가운데일수도 있기 때문이다. 나는 매번 해주기 번거로워서 spt_find 내부에서 받은 주소를 바꿔주는 과정을 추가했다.
profile
나는 말하는 감자... 감자 나부랭이....

0개의 댓글