[Week11] Project3 : Stack growth, Memory Mapped Files

안나경·2024년 4월 3일

크래프톤정글

목록 보기
47/57

! 주의. project3에 swap 관련에서 에러가 걸린게 여기에 원인 있을 수 있으니 플로우를 참고만 할것.
...
이전 프로젝트 글을 보고왔는데
가독성 왕 구리다

지금 시간이 더 걸리는게 당연하다
지금 쓴게 시간 차 적으로 낫다
(이것도 별로 좋은 편은 아님.)

Stack growth

Stack growth.

만약 fault가 일어난 addr이
기존에 할당받은 user stack 이상이지만,
1MB 사이라면 추가로 할당해주는 함수다.

그러기 위해서 고려해야하는 것은...

  • fault addr이 내가 허용하는 user stack 범위 내인지.
  • 앞에서 user stack에 추가로 적재했기때문에 rsp가 이동한 거리에서 -8만큼의 위치인지.

전자는 대충 당연히 납득이 가겠지만
후자는 무슨 소리인가 싶을 것이다.

나도 무슨 소리인지 모르고 구현했다가
나중에 뼈아프게 알게 되었다.

user stack은 다양한 경우에 쓰게 되지만,
대표적으로는 char a[4096]; 같이 선언만 해도 그를 쓰기 위한 공간을 확보하게 된다. (지역 변수...)

그래서 그 선언하는 순간
user stack에 적재되면서
rsp는 이만큼 들어오겠지~ 하고
이동한다.
(rsp는 user stack의 포인터다. 대개 intr_frame안에 있으며, 일반적으로는 page fault로 들어오는 Intr_frame에 있는 rsp로 찾을 수 있다.)

하지만 vm 시스템에서는
접근했을 때야 메모리를 할당하기때문에
rsp는 이동했어도 그 자리의 물리메모리는 아직 할당받지 못했기때문에
실제로 접근하면 page fault가 발생하면서 우리가 그 자리에 page size의 메모리를 할당한다.

즉...
1) 기본

(stack 바닥) - (현 rsp)

2) 지역 변수 잔뜩 선언 후

(stack 바닥) - (이전 rsp) --- (이동한 rsp)

여기서 ---부분은 비어있다.
물리메모리가 할당되지 않았다.

3) addr이 지역 변수 접근 시도

(stack 바닥) - (이전 rsp) -(addr이 찾으려는 장소) -- (이동한 rsp)

...
그러나 고려해야하는 것은
맨처음 접근 시도가, 변수 선언 당시
이미 addr이 한번 접근하려한다는 점인데,

사실 2번 3번이 저렇게 되는게 아니라

2+) 지역 변수 잔뜩 선언 후

(높)(stack 바닥) - (이전 rsp) --- (이동한 rsp) (addr == rsp -8) (낮)

..로 rsp-8 지점의, addr이 먼저 도착한다.

왜 저런 지점의 addr이 도달하냐면
지역 변수를 rsp로 쭉쭉 쌓을때
맨 마지막에 PUSH라는 인스트럭션을 선언하는데,

대개 movq의 경우 rsp를 먼저 움직이고 addr을 확인한다면(실제로는 나도 잘 모른다. 아무 예시다.)
PUSH는 rsp를 움직이기 전에 addr 자체가 접근이 가능한지 확인하기때문이다.

그래서 ...
addr이 rsp-8 지점으로 들어왔을때
추가로 할당한다면

  • PGSIZE 만큼만 그때그때 할당하는지
  • rsp - 8 지점이 기존 rsp와 차이가 얼마인지 계산하고 그만큼까지 stack을 growth 시키는지

..로 두 가지 방법을 고려해 구현할 수 있다.
후자라면 기존 rsp도 기억해둬야겠지... 아무튼.

그 두 가지 방법 무엇을 쓰든
알아야 하는 인자가 있다.

  • 현 process의 rsp

...
그런데 이 rsp가
보통은 vm try handle fault의 intr_frame 의 rsp 로 찾을 수 있지만,
이전 project 2에서 fork에서 했듯이
systemcall 당시에는 intr_frame이 그대로 계승되지 않기때문에, 시스템콜 호출 시에는 rsp 자체를 thread 구조체에 마련해둔 공간에 저장하여, 그 rsp를 써야한다.

일반적으로 들어오는 page fault와
syscall로 들어오는 fault는 ...

vm try handle fault의 user라는 인자로
user로 접근했다면 rsp를 syscall에서 받아온걸로,
user 외라면 그냥 intr_frame의 rsp를 가져와서

조건문 비교에 쓴다.

...
나의 경우는
다른 사람 코드를 참고한 해결법이긴 하지만
PGSIZE만큼만 그때그때 할당하는 방식으로
기존 rsp같은건 안 구하고

만약 현 rsp 내라면 또 할당이 가능하도록 조건문을 구성하였다.

void
syscall_handler (struct intr_frame *f) {
	thread_current()->cur_rsp = f->rsp;
    ...

thread 구조체에 syscall로 넘어왔을때의 rsp를 계승하기.

bool
vm_try_handle_fault (struct intr_frame *f UNUSED, void *addr UNUSED,
		bool user UNUSED, bool write UNUSED, bool not_present UNUSED) {
...
	void *stack_max = (void *) (((uint8_t *) USER_STACK) - (1<<20)); //0x4757F000
...
	if(not_present){
		if(user){
			rsp = f->rsp;
		}
		else{
			rsp = thread_current()->cur_rsp;
		}
		if(addr <= USER_STACK &&rsp <= addr && stack_max<= rsp){
		
			vm_stack_growth(addr);
		}
		else if((rsp-8) == addr && stack_max<= rsp-8 && rsp-8 <= USER_STACK){

			vm_stack_growth(addr);
		}
 ...

vm try handle fault에서 rsp를 경우에 따라 다르게 계승하고, 조건문으로 stack growth로 처리하기.

static void
vm_stack_growth (void *addr UNUSED) {
	vm_alloc_page(VM_ANON | VM_MARKER_0, pg_round_down(addr), true);
}

VM_MARKER_0으로 stack임을 표시하고, 페이지 단위에 맞춰서 할당하기.

주의해야할 것은

  • c언어에서 여러가지 조건문, rsp <=addr <= USER_STACK이라고 쓰면 전부 성립해야 통과하는게 아니라 rsp<=USER_STACK 자체가 성립하기만 해도 되기때문에 &&로 잘 나눠주도록 하자.

...
나의 경우는

  • rsp를 구별은 해야겠지만 어떤 게 유저 접근인지, 커널 접근인지 구별해야할지 몰랐음.
  • syscall 당시에 rsp를 저장해야한다는건 알았지만, 그 외의 경우는 그냥 intr_frame으로 가능한지 몰랐음.
  • 선언하는 순간 rsp가 이동하는지 몰랐음. 그래서 나는 rsp와 fault addr 사이의 size를 계산해서 그만큼 PGSIZE 갯수로 alloc 하려고 했음.

1은 내장함수의 인자를 면밀히 봐야했고
2는 지금도 그걸 파악할 자신이 없다 깃북만으로
3은 진짜 어떻게 알라는 말이냐

주소가 높은 곳에서 낮은 곳이라
어디가 증가인지 어디가 끝인지
addr은 어디서 fault 난건지 16진수 붙잡고 울며함

Memory Mapped Files

...

여기서는
file을 memory에 매핑하는 시스템콜을 구현한다.
그래서 mmap, munmap을 하게 되는데,

첫번째로는 syscall.c에
해당하는 시스템콜을 case에 추가해주고,
시스템 콜 함수도 만들어준다.
(수행하는 함수는 do_mmap, do_munmap으로
file.c 안에 있다.)

...
	case SYS_MMAP:{
		f->R.rax = mmap(f->R.rdi, f->R.rsi, f->R.rdx, f->R.r10, f->R.r8);
		break;
	}
	case SYS_MUNMAP:{
		munmap(f->R.rdi);
		break;
	}
    ...

이게 왜 이렇게 되는지는 project2를 했다면 충분히 알 것이다.

mmap

syscall.c의
mmap 수준에서는 do mmap에 넘기기전
인자들을 확인하여 이상한 값이라면 주로 넘기기전에
exit(-1)이든 NULL로 return 하든 한다.

kernel등을 고려하는건 당연하지만,
KERN_BASE - PGSIZE은 mmap kernel case를 위해 추가했으나 정확히 무슨 상관인지는 파악하지 못했다.

생각해볼만한건...

  • length가 0 이면 null.
  • fd가 stdin, stdout이면 null.
  • fd로 연 file size가 0이면 exit(-1).
  • addr자체가 페이지 정렬되지 않았다면 null.
  • addr이 null이어도 null.

그외 bad-off 등 케이스에서

  • offset이 length보다 크면 null.

등이 있다.
pg_ofs는 addr의 offset을 구하는 매크로로
만약 정렬되어있다면 0이 나올것이다.

테스트 케이스에 따라 디버깅하면 금방 찾을수 있는 요건으로,
어떤건 null로 끝내고 어떤건 exit(-1)로 끝낼지
케이스로 쉽게 판단할 수 있다.

void *mmap (void *addr, size_t length, int writable, int fd, off_t offset){
	if(is_kernel_vaddr(addr) || addr == KERN_BASE - PGSIZE){
		return NULL;
	}
	struct file *file = thread_current()->fdt[fd];
	if (fd >64 || fd <0){
		return NULL;
	}
	if(fd == 0 || fd == 1){
		return NULL;
	}
	if(addr == NULL || pg_ofs(addr) != 0){
		return NULL;
	} 
	if(file_length(file)== 0){
		exit(-1);
	}
	if(length == 0){
		return NULL;
	}
	if(addr != pg_round_down(addr)){
		return NULL;
	}

	if(offset > length){
		return NULL;
	}
	if(file != NULL){
	return do_mmap(addr, length, writable, file, offset);
	}

	return NULL;
}
	

munmap

munmap은 실제로 mmap 되었던 addr만 해제해야하므로,
이 수준에서는 판단하기가 힘들어
check address로 최소 요건만 확인하고 바로 넘긴다.

void munmap (void *addr){
	check_address(addr);
	do_munmap(addr);
}

do mmap

mmap 쯤 왔을떄는 슈도 코드를 짤?만 했던거같다.
실력이...늘은...것?일까?
아마도...

아무튼...

mmap의 역할은 이렇다.

fd로 연 file을 length byte 만큼
offset에서 시작하는 부분을 addr로 시작하는 공간에 writable을 함께 기재하여 할당.

fd로 연 file은 mmap 단계에서 찾아서
file을 갖고 진입하게 된다.

그래서 가진 인자는,
addr, length, writable, file, offset.

mmap은 몇번 하든 독자적으로 포인터를 움직일수 있게 하기위해, 포인터를 따로 만들어주는 file_reopen 함수를 쓴다.(file duplicate로도 비슷한 역할을 수행할 수 있다고 한다.)

순서는 이렇다.

  • file reopen으로 file 포인터 생성.

단, mmap 도 lazy load 정책을 따르기때문에 lazy loag segment 함수를 그대로 쓰되 적재하진 않기때문에 page initalizer만 진행해준다.

  • length와 file size를 고려하여 read byte, zero byte 계산.(나중에 page load 참고에 쓸 추가 데이터를 만들어, page alloc initializer 을 할 때 aux에 담아줄, 관련 구조체를 init하기 위해 사용.)
    length만큼 page를 적재해야하지만,
    단위는 page size가 되도록.

....
length를 page size로 나누면 대강 page n장, 거기에 추가 바이트 m바이트가 나오지 않겠는가?
그럼 n개 만큼의 page를 만들고, 추가로 m바이트 + (PGSIZE - m)만큼의 제로 바이트로 page를 alloc해야할 것이다.
대강 n+1개의 page가 필요하게 되겠지...

  • 그렇게 총 필요 page count 만큼 page를 alloc.
    read byte, zero byte, offset, writable 적절히 동봉.

...

그래서 그 계산만 헷갈리지 않는다면
(헷갈리겠지만)

load segment에서 했던 것과
유사하게 진행하면 된다.

addr도, offset도 갱신해주는것을 잊지말자.
또, 맨처음 할당했던 addr도
이후 연속으로 munmap할 수 있게
내가 mapping한 갯수도 Thread 구조체에 더해준다.

void *
do_mmap (void *addr, size_t length, int writable,
		struct file *file, off_t offset) {
		
	bool succ = false;
	struct file * f = file_reopen(file);
	void * new_addr = addr;
	int count = length <= PGSIZE ? 1 : (length%PGSIZE ? length/PGSIZE +1: length/PGSIZE);
	
	size_t read_bytes = file_length(f) < length ? file_length(f) : length;
	size_t zero_bytes = PGSIZE - (read_bytes%PGSIZE) ;
	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;
		struct page_load_data *aux_d = malloc(sizeof(struct page_load_data));
		aux_d->file = f;

		aux_d->ofs = offset;
		aux_d->read_bytes = page_read_bytes;
		aux_d->zero_bytes = page_zero_bytes;

		if(!vm_alloc_page_with_initializer(VM_FILE, new_addr, writable, lazy_load_segment, aux_d)){
	
			return NULL;
		}

		read_bytes -= page_read_bytes;
		zero_bytes -= page_zero_bytes;
		new_addr += PGSIZE;
		offset += page_read_bytes;

	}
	struct page *p = spt_find_page(&thread_current()->spt, addr);
	p->mapping_count = count;
	if(read_bytes== 0){
		succ = true;
	}
	if(succ){
		return addr;
	}
	else{
		return NULL;
	}
	

}

나의 경우는

  • length와 file size가 다른 것을 알지 못했음.
  • length로 read byte, zero byte를 계산하다보니 굳이 read byte, zero byte 변수를 쓰지 않으려 하다가 계산 혼동을 너무 겪음.
  • 여러번 할당할 경우의 count를 동봉해서 munmap을 count 횟수만큼 해야한다는 것을 고려하지 못했음.

do munmap

...
그래서 munmap을 해주되
만약 page 정렬 자체가 안되어

spt find page가 안되었다면 죽이고

그 addr이,
mmap 한 애라면 분명 mapping count가 있을테니,
page size 단위만큼 이동하면서 destroy 해준다.

여기서 유의할 것은

  • count를 p->count 식이 되어버리면 p가 계속 변하므로 오류 생김
  • 맨처음 destroy하고 다음으로 넘기는 식이니, for문의 갯수와 삭제하는 mapping count가 일치하는지 유념할 것.
void
do_munmap (void *addr) {
	struct page *p;
	p = spt_find_page(&thread_current()->spt, addr);
	struct file *f;
	if(p == NULL){
		exit(-1);
	}
	int count = p->mapping_count;


	for(int i =0; i < count;i++){
		if(p){
			destroy(p);
			addr += PGSIZE;
			p = spt_find_page(&thread_current()->spt, addr);
		}
	}
}

나의 경우는

  • count를 고려하지 않고 destroy만 해줌. page 구조체는 분명 free 하지 말라고 했는데, 어떻게 해야할지..... 고민하고 있었음.
profile
개발자 희망...

0개의 댓글