일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 파이썬서버
- 언리얼
- 언리얼뮤지컬
- 알고풀자
- Ajax
- JWT
- VUE
- 스마일게이트
- Enhanced Input System
- Unseen
- R
- node
- Express
- 으
- 카렌
- Jinja2
- 정글사관학교
- 프린세스메이커
- 마인크래프트뮤지컬
- flask
- 레베카
- 프메
- Bootstrap4
- 게임개발
- EnhancedInput
- 스터디
- 언리얼프로그래머
- 데이터베이스
- 디자드
- 미니프로젝트
- Today
- Total
Showing
[Pintos, 디버깅] Project 3 : VM, lazy_load_segment 추적 본문
1. gdb를 통한 Project 3 추적
프로젝트3 VIRTUAL MEMORY부터는 본격적으로 gdb를 활용해서 코드파악을 하였습니다. 본 포스팅은 프로젝트2때와 다르게 vm일 경우 lazy_load_segment까지 도달하는 일련의 과정이 있어, 디버깅을 찍어보면서 추적한 기록을 남기는 포스팅입니다.
pintos --gdb -m 20 --fs-disk=10 -p tests/userprog/args-multiple:args-multiple --swap-disk=4 -- -q -f run 'args-multiple some arguments for you!'
위와 같은 명령어를 통해
Pintos 운영 체제를 가상 머신(VM) 상에서 테스트하였습니다.
--gdb 옵션: Pintos를 GDB 디버거와 함께 실행하라는 의미, 디버깅과 코드 추적을 가능하게 합니다.
-m 20 옵션 :은 Pintos의 메모리 크기를 20MB로 설정
--fs-disk=10 옵션: 파일 시스템 디스크 크기를 10MB로 설정
-p tests/userprog/args-multiple:args-multiple 옵션은 args-multiple 테스트 프로그램을 Pintos에 로드하는 옵션.
-q -f run 'args-multiple some arguments for you!' 옵션은 args-multiple 프로그램을 실행하고, 'args-multiple some arguments for you!'를 인자로 전달
따라서 주어진 명령어는 Pintos를 실행하고, 지정된 옵션과 테스트 프로그램을 사용하여 Pintos 운영 체제를 가상 머신 상에서 테스트하는 용도로 사용할 수 있습니다.
(1) 브레이크 포인트
vm 모드에서 load_segment 흐름을 관찰하기 위한 브레이크 포인트
🍊 load_segment
🍊 lazy_load_segment
🍊 vm_alloc_page_with_initializer
페이지 폴트 bt를 확인하기 위한 브레이크 포인트
🍊 page_fault
2. load_segment, vm_alloc_page_with_initializer 추적
디버깅 순서대로 따라가보겠습니다.
쓰레드가 실행되고, process_exec() 안에서 load를 합니다.
load 함수 내에서 load_segment를 부릅니다.
여기까지는 프로젝트2와 프로젝트3의 공통점입니다. 즉, process_exec으로 테스트에 진입하고, 실행 파일의 이름으로 파일내용을 불러오는 load 과정이 있습니다.
여기서부터 VM이 아닌 프로젝트2에서는 lazy loading 할 필요가 없기 때문에, load 과정에서 바로 물리 메모리를 할당받고, file_read 함수로 할당받은 메모리에 실행 파일 내용을 써주었습니다.
프로젝트2 load segment 코드를 보면, palloc_get_page를 통해 user pool 영역에 메모리를 할당 받는 것을 볼 수 있고, 바로 file_read 함수를 통해 실행파일 내용을 불러옵니다.
반면, 프로젝트 3에서는 lazy loading 기능이 추가되면서, 파일 내용을 불러오는 부분이 나중에 실행됩니다.
메모리 할당 요청이 들어오면 바로 물리 메모리를 할당해주지 않고, 페이지만 할당해줍니다. 그 페이지는 Supplemental Page Table에 들어가게 되고, 일련의 코드가 진행된 뒤 할당한 페이지에 대해 Page Fault가 발생합니다. Page Fault가 실행되는 과정에서 vm_do_claim_page 등으로 파고들어가면서 할당해준 페이지와 물리 프레임 간의 매핑이 진행됩니다. 매핑 뒤 마지막에 file_read 함수가 불리면서 실행 파일의 내용을 물리 메모리에 load합니다.
이러한 레이지 로딩(Lazy loading)은 메모리 로딩을 필요한 시점까지 연기하는 디자인입니다.
페이지 구조체는 할당되지만, 해당 페이지에 대한 전용 물리 프레임이 없으며 페이지의 실제 내용은 아직 로드되지 않습니다.
실제 필요한 시점에만 내용이 로드되며, 이는 페이지 폴트(page fault)에 의해 실제로 콘텐츠가 필요하다는 시그널을 받을 때 로드됩니다.
위의 내용을 디버깅으로 쫓아가보겠습니다.
을 거쳐서
구역의 load_segment을 찾아가게 됩니다.
먼저 vm_alloc_page_with_initializer 함수를 통해 메모리 할당 요청을 하게 됩니다.
load_segment(process.c)에서 vm_alloc_page_with_initializer(vm.c)을 찾아갑니다.
vm_alloc_page_with_initializer 함수는 가상 메모리 페이지를 새로 만들어 할당해주고 초기화합니다.
- VM_TYPE(type)이 VM_UNINIT인 경우가 정상입니다.
- 현재 실행 중인 스레드의 보조 페이지 테이블을 가져옵니다.
- pg_orund_down을 통해 upage를 페이지의 시작 주소로 정렬합니다.
- 스위치 문을 사용하여 VM_TYPE(type)에 따라 page_initializer 함수를 설정합니다. 이는 페이지 초기화에 사용될 초기화 함수를 지정하는 것으로, VM_ANON이면 anon_initializer, VM_FILE이면 file_backed_initializer를 page_initializer에 할당합니다.
- spt_find_page 함수를 사용하여 upage가 이미 페이지 테이블에 존재하는지 확인합니다. exist_page를 통해 이미 존재하는 페이지가 없는 상태인 NULL이라면 존재하지 않는 경우에만 새로운 페이지를 생성하고 초기화한 후 spt_insert_page를 사용하여 spt(보조 페이지 테이블)에 삽입하는 로직을 수행합니다.
- 페이지 할당 및 초기화가 성공한 경우 true를 반환하고, 오류가 발생한 경우 false를 반환합니다.
정리하자면 vm_alloc_page_with_initializer 함수는 가상 메모리 관리를 위해 사용되며, 새로운 페이지를 할당하고 초기화하여 보조 페이지 테이블에 추가합니다. 페이지의 유형에 따라 적절한 초기화 함수를 선택하여 페이지를 초기화하고 필요한 정보를 전달합니다.
3. uninit_new 진입
(gdb) b exception.c:125
Breakpoint 6 at 0x800421ccc7: file ../../userprog/exception.c, line 125.
(gdb) n
66 upage = pg_round_down(upage);
(gdb) n
69 switch (VM_TYPE(type))
(gdb) n
72 page_initializer = anon_initializer;
(gdb) n
73 break;
(gdb) n
78 if ((exist_page = spt_find_page(spt, upage)) == NULL)
(gdb) n
83 struct page *newpage = calloc(1, sizeof(struct page));
(gdb) n
Breakpoint 5, vm_alloc_page_with_initializer (type=VM_ANON, upage=0x400000, writable=false, init=0x800421c56c <lazy_load_segment>, aux=0x800424a018) at ../../vm/vm.c:84
84 uninit_new(newpage, upage, init, type, aux, page_initializer);
(gdb) s
uninit_new (page=0x800423d198, va=0x400000, init=0x800421c56c <lazy_load_segment>, type=VM_ANON, aux=0x800424a018, initializer=0x80042219f3 <anon_initializer>)
at ../../vm/uninit.c:30
30 ASSERT(page != NULL);
(gdb) bt
#0 uninit_new (page=0x800423d198, va=0x400000, init=0x800421c56c <lazy_load_segment>, type=VM_ANON, aux=0x800424a018, initializer=0x80042219f3 <anon_initializer>)
at ../../vm/uninit.c:30
#1 0x0000008004220e2b in vm_alloc_page_with_initializer (type=VM_ANON, upage=0x400000, writable=false, init=0x800421c56c <lazy_load_segment>, aux=0x800424a018)
at ../../vm/vm.c:84
#2 0x000000800421c7f7 in load_segment (file=0x8004241038, ofs=0, upage=0x400000 <error: Cannot access memory at address 0x400000>, read_bytes=19738, zero_bytes=742,
writable=false) at ../../userprog/process.c:892
#3 0x000000800421c3b8 in load (file_name=0x8004244000 "args-multiple", if_=0x8004245ecf) at ../../userprog/process.c:627
#4 0x000000800421bd83 in process_exec (f_name=0x8004244000) at ../../userprog/process.c:372
#5 0x000000800421b8fb in initd (f_name=0x8004244000) at ../../userprog/process.c:192
#6 0x00000080042076de in kernel_thread (function=0x800421b8af <initd>, aux=0x8004244000) at ../../threads/thread.c:513
#7 0x0000000000000000 in ?? ()
uninit_new에서는 위와 같은 page 구조체 멤버변수 매핑 작업을 해줍니다.
- operations: uninit_ops 구조체의 주소를 할당합니다. uninit_ops는 초기화되지 않은 페이지에 대한 연산 함수를 정의하는 구조체입니다.
- va: 페이지의 가상 주소를 설정
- frame: 현재는 프레임이 없으므로 NULL로 설정
- uninit: 초기화되지 않은 페이지(uninit_page)에 대한 필드를 설정. 이 필드는 초기화에 필요한 정보를 저장
- init: 페이지 초기화에 사용되는 초기화 함수를 설정
- type: 페이지의 유형을 설정
- aux: 추가적인 초기화에 필요한 보조 데이터를 설정
- page_initializer: 페이지 초기화 함수를 설정
이 함수는 초기화되지 않은 페이지를 생성하고, 해당 페이지의 필드를 설정하여 초기화에 필요한 정보를 저장합니다. 페이지의 가상 주소, 초기화 함수, 유형, 보조 데이터 등의 정보를 page 구조체에 저장하여 초기화되지 않은 페이지를 표현합니다.
4. page_fault(핸들러) 진입
위에서 vm_alloc_page_with_initializer는 페이지 구조체를 할당하고 해당 페이지 유형에 따라 적절한 initializer를 설정하여 새 페이지를 초기화하고, 컨트롤을 다시 사용자 프로그램에 반환했습니다.
사용자 프로그램이 실행되는 동안 일부 시점에서, 프로그램이 이미 가지고 있다고 믿는 페이지에 접근하려고 하면 페이지폴트가 발생합니다.
page_fault 함수가 인터럽트 핸들러로 등록되어 있으면, page_fault(frame)이 호출됩니다.
vm_try_handle_fault에서는 각종 예외처리를 통과하면 return vm_do_claim_page에 도달하게 됩니다.
5. lazy_load_segment 도달 전 후 조사
위에서 찍힌 콜스택을 하나씩 정리하면 아래와 같습니다.
vm_alloc_page_initializer에서 페이지를 새로 만들어 할당해준 뒤로 page fault가 발생하면, 이 페이지폴트 처리 절차에서는 uninit page의 swap in으로 등록되어있는 uninit_initialize 함수가 실행됩니다. 그리고 결국 이전에 설정한 init = lazy_load_segment를 호출할 것입니다.
결국 aux는 나중에 page fault handler 과정에서 swap in 함수가 실행되면서, init 함수로 설정해둔 lazy_load_segment 함수의 인자로 넘어갑니다.
(1) 2차 브레이크 포인트
lazy_load_segment에 도달하기까지의 콜스택에다 브레이크 포인트를 걸어보기로 하였습니다.
#0 lazy_load_segment (page=0x800423d198, aux_=0x800424a018) at ../../userprog/process.c:818
#1 0x00000080042219ab in uninit_initialize (page=0x800423d198, kva=0x8004a22000) at ../../vm/uninit.c:56
#2 0x000000800422145a in vm_do_claim_page (page=0x800423d198) at ../../vm/vm.c:258
#3 0x0000008004221252 in vm_try_handle_fault (f=0x8004245f40, addr=0x400c7f, user=true, write=false, not_present=true) at ../../vm/vm.c:209
#4 0x000000800421cd87 in page_fault (f=0x8004245f40) at ../../userprog/exception.c:151
#5 0x0000008004208d2f in intr_handler (frame=0x8004245f40) at ../../threads/interrupt.c:352
#6 0x000000800420914d in intr_entry ()
#7 0x00008004245ed000 in ?? ()
#8 0x0000800422add800 in ?? ()
#9 0x0000800424504800 in ?? ()
#10 0x0000010424500000 in ?? ()
#11 0x00008004245f1000 in ?? ()
#12 0x0000800420a46200 in ?? ()
#13 0x0000000000000000 in ?? ()
(gdb) quit
A debugging session is active.
vm 모드에서 lazy_load_segment 흐름을 관찰하기 위한 브레이크 포인트
🍊lazy_load_segment
🍊uninit_initialize
🍊vm_do_claim_page
🍊swap_in 진입점
🍊vm_try_handle_fault
vm_try_handle_fault call vm_do_claim_page call swap_in
swap in에서 uninit_initalize로 넘어간다.
또한 init(page, aux)를 통해 lazy_load_segment로 들어오게 된다.
이후 lazy_load_segment 함수에서는 aux가 가리키고 있는 구조체의 멤버 변수들을 하나하나 꺼내가며 그 값들을 필요한 인자로 사용하게 됩니다.
6. 추적 최종 결론
lazy loading 유무 차이가 있는 프로젝트2와 프로젝트3의 공통점으로는 process_exec으로 테스트에 진입하고, 실행 파일의 이름으로 파일내용을 불러오는 load 과정이 있다는 점입니다.
프로젝트2에서는 load 과정에서 바로 물리 메모리를 할당받고, file_read 함수로 할당받은 메모리에 실행 파일 내용을 써주었습니다. palloc_get_page를 통해 user pool 영역에 메모리를 할당 받는 것을 볼 수 있고, 바로 file_read 함수를 통해 실행파일 내용을 불러옵니다.
프로젝트 3에서는 lazy loading 기능이 추가되면서, 파일 내용을 불러오는 부분이 나중에 실행됩니다. 메모리 할당 요청이 들어오면 바로 물리 메모리를 할당해주지 않고, vm_alloc_page_with_initializer 함수에 들어가 페이지만 할당해줍니다. 그 페이지는 Supplemental Page Table에 들어가게 되고, 일련의 코드가 진행된 뒤 할당한 페이지에 대해 Page Fault가 발생합니다. Page Fault가 실행되는 과정에서 vm_do_claim_page로 파고들어가면서 할당해준 페이지와 물리 프레임 간의 매핑이 진행됩니다. 매핑 뒤 uninit page의 swap in으로 등록되어있는 uninit_initialize 함수가 실행됩니다. 마지막에 lazy_load_segment의 file_read 함수가 불리면서 실행 파일의 내용을 물리 메모리에 load합니다.
vm_alloc_page_with_initializer에서 uninit_new 함수의 인자로 init과 aux를 넘겨주는 것을 볼 수 있습니다. init 함수는 이후에 실행될 lazy_load_segment 함수이며, aux는 lazy_load_segment 함수에서 사용할 인자가 담겨있는 구조체의 포인터입니다. lazy_load_segment 함수에서는 aux가 가리키고 있는 구조체의 멤버 변수들을 하나하나 꺼내가며 그 값들을 필요한 인자로 사용하게 됩니다.
+7. 구조체 포인터 aux 변수
함수에 전달해야 할 변수의 개수가 많을 때 구조체를 사용하면 하나의 단위로 묶어서 전달할 수 있다는 것이 기본적인 이유가 될 수 있습니다.
특히 이번 프로젝트에서는 구조체를 따로 만들어 포인터를 넘기게 되면, uninit page가 다른 페이지로 바뀔때 실행되는 init 함수가 각각 달라, 다른 변수들을 필요로 할 때 대응이 수월합니다.
따라서, init 함수에서 사용할 변수인 aux의 크기는 매번 달라질 수 있고, 이러한 aux 변수를 유동적으로 사용하기 위해 변수들을 바로 넘겨주지 않고, 변수들을 담은 구조체를 만들어 그 구조체의 주소를 넘겨주는 방식으로 변수를 함수에 전달하게끔 설계할 수 있습니다.
'컴퓨터 공학, 전산학 > 핀토스' 카테고리의 다른 글
[Pintos] 벌레를 잡기 위한 GDB (1) | 2023.05.11 |
---|---|
[Pintos] Project 2 : syscall, 파일 디스크립터 fdt (0) | 2023.05.07 |
[Pintos] Project 2 : Argument Passing, 인터럽트 프레임 (0) | 2023.05.07 |