정글사관학교 개발일지/운영체제-PintOS

PintOS Project 3 - WIL(정글사관학교 84일차 TIL)

Woonys 2022. 1. 25. 11:08
반응형

이번주에는 Pintos의 가상 메모리를 구현하는 작업을 수행했다.

 

공부 및 작업한 것들은 아래와 같다.

 

1. 진행 상황

 

운영체제 (4) 메모리 관리 - 1, 2

운영체제 (5) 메모리 관리 - 3

 

PintOS Project 3 - Virtual Memory (1) Memory Management
PintOS Project 3 - Virtual Memory (2) Anonymous Page

PintOS Project 3 - Virtual Memory (3) Stack Growth

PintOS Project 3 - Virtual Memory (4) Memory Mapped Files

PintOS Project 3 - Virtual Memory (5) mmap() 및 page fault 관련 추가 정리

PintOS Project 3 - Virtual Memory (6) swap in/out

 

계속 올 fail인가 싶었는데 차근차근 버그를 잡으니 하나씩 해결되기 시작했다.

첫 버그는 syscall.c에서 check_valid_buffer()를 각각 SYS_READ, SYS_WRITE에 넣어줬는데 문제는 인자값을 잘못 넣어줬다는 것.

 

수정 전

case SYS_READ:
		{	
			check_valid_buffer(f->R.rdi, f->R.rsi, f->R.rdx, 1);
			f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		}

		case SYS_WRITE:
		{	
			check_valid_buffer(f->R.rdi, f->R.rsi, f->R.rdx, 0);
			f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
			break;

문제는 들어오는 인자 순서가 write에 맞춰져있지 check_valid_buffer의 인자 순서와 달라서 아예 버퍼가 아닌 다른 인자가 valid한지 체크하고 있었다는 것. rsp, to_write는 심지어 덜 가져왔다.

void check_valid_buffer(void* buffer, unsigned size, void* rsp, bool to_write)

int write(int fd, const void *buffer, unsigned size)

 

수정 후는 아래와 같다.

case SYS_READ:
		{	
			check_valid_buffer(f->R.rsi, f->R.rdx, f->rsp, 1);
			f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		}
			
		case SYS_WRITE:
		{	
			check_valid_buffer(f->R.rsi, f->R.rdx, f->rsp, 0);
			f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
			break;
		}

 

그 다음은 load().

 

수정 전

int
process_exec (void *f_name) {
    char *file_name = f_name;
    bool success;

    /* We cannot use the intr_frame in the thread structure.
     * This is because when current thread rescheduled,
     * it stores the execution information to the member. */
    struct intr_frame _if;
    _if.ds = _if.es = _if.ss = SEL_UDSEG;
    _if.cs = SEL_UCSEG;
    _if.eflags = FLAG_IF | FLAG_MBS;

    
    // for argument parsing
    char *argv[64];     // 인자 배열
    int argc = 0;       // 인자 개수

    char *token;        // 실제 리턴 받을 토큰
    char *save_ptr;     // 토큰 분리 후 문자열 중 남는 부분
    token = strtok_r(file_name, " ", &save_ptr);
    while (token != NULL) {
        argv[argc] = token;
        token = strtok_r(NULL, " ", &save_ptr);
        argc++;
    }
    
    /* We first kill the current context */
    process_cleanup ();
    

    /* And then load the binary */
    success = load (file_name, &_if);

    /* If load failed, quit. */
    if (!success) {
        palloc_free_page (file_name);
        return -1;
    }
    // 유저스택에 인자 넣기
    void **rspp = &_if.rsp;
    argument_stack(argv, argc, rspp);
    _if.R.rdi = argc;
    _if.R.rsi = (uint64_t)*rspp + sizeof(void *);
    
    /* Start switched process. */
    do_iret (&_if);
    NOT_REACHED ();
}

 

process_cleanup() 위치 수정하고 spt_init() 추가하니 해결됐다.

 

수정 후

 

int
process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	/* We cannot use the intr_frame in the thread structure.
	 * This is because when current thread rescheduled,
	 * it stores the execution information to the member. */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* We first kill the current context */
	process_cleanup ();

#ifdef VM
	supplemental_page_table_init(&thread_current()->spt);
#endif
	// for argument parsing
	char *argv[64]; 	// 인자 배열
	int argc = 0;		// 인자 개수

	char *token;		// 실제 리턴 받을 토큰
	char *save_ptr;		// 토큰 분리 후 문자열 중 남는 부분
	token = strtok_r(file_name, " ", &save_ptr);
	while (token != NULL) {
		argv[argc] = token;
		token = strtok_r(NULL, " ", &save_ptr);
		argc++;
	}

	/* And then load the binary */
	success = load (file_name, &_if);

	/* If load failed, quit. */
	if (!success) {
		palloc_free_page (file_name);
		return -1;
	}
	// 유저스택에 인자 넣기
	void **rspp = &_if.rsp;
	argument_stack(argv, argc, rspp);
	_if.R.rdi = argc;
	_if.R.rsi = (uint64_t)*rspp + sizeof(void *);

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

 

 

이외에도 thread yield할 때 외부 interrupt 체크하는 것 추가.

// priority scheduling 
void thread_test_preemption (void)
{
	if (!list_empty (&ready_list) && thread_current ()->priority 
	< list_entry (list_front(&ready_list), struct thread, elem)->priority)
	{
		if (!intr_context()){
			thread_yield ();
		}
	}
}

 

 

2. 질문

그간 공부하며 조교님께 질문했던 내용들 정리 

질문 1

안녕하세요, 조교님!

mmap 및 swap in/out을 공부하기 위해 CSAPP 책을 보던 중, 문구가 잘 이해가 되지 않아 질문드립니다.

9.8장 Memory Mapping에 보면 리눅스는 메모리 매핑이라는 프로세스를 통해 디스크에 있는 객체와 관련된 가상 메모리 영역의 정보를 초기화한다고 나와있습니다. 두 가지 분류가 나오는데요,

여기서 2번 - Anonymous file를 보면,

프로세스가 해당 가상 영역에 접근했을 때 커널은 적절한 물리 메모리 내 victim page를 찾은 다음 
만약 해당 페이지가 수정된 적이 있다면(dirty) swap out시키고
 그 다음 0으로 덮어씌운다고 되어 있습니다.(
노란색 밑줄 친 영역
) 이때, 
디스크와 메모리 사이에 tranfer된 데이터는 전혀 없다(초록색 밑줄 친 영역) 
고 설명합니다.

여기서 의문점은, 물론 swap area가 가상 메모리가 확장된 공간이긴 하나 물리적으로는 디스크 내에 존재하는 영역인데, 해당 영역이 수정됐다면  swap out을 위해 메모리에서 디스크로 데이터가 전송되었을 텐데도 초록색 밑줄과 같이 디스크와 메모리 사이에 어떤 데이터도 전송되지 않았다고 말할 수 있는 것인지 궁금합니다. 혹은 제가 이해 못한 다른 것이 있을까요?

 

A)

제가 이해한 바로도 anonymous page가 dirty하다면 memory와 swap disk로의 data transfer가 발생할 수 있다고 생각됩니다. 교재에서 언급한 disk는 앞서 설명한 regular file 등의 data가 저장된 file system disk로 이해하시는게 올바를 것 같습니다.

 

질문 2

답변 감사드립니다 조교님 :)

첫 질문과 약간 별개일 수 있는데, 여기서 anonymous file에 대한 개념 중 헷갈리는 게 한 가지 있습니다.

anonymous file을, 처음에는 anonymous page가 생성될 때 해당 페이지 내에 만들어지는 0으로 초기화되는 값의 집합을 파일로 인식하겠거니 했습니다. 그런데 해당 캡쳐 내용 중 가장 위에 보면, 리눅스는 가상 메모리 영역의 정보를 디스크에 있는 객체와 연관지음으로써 초기화한다고 나와있습니다.

그렇다면, anonymous file은 디스크에 존재하는 것인가요? 그러면 또 한 가지 의문이 드는 게, anonymous file은 전부 0으로 되어 있는데 굳이 디스크 내에 보관할 필요가 있는지가 이해되지 않습니다. anonymous page를 만들 때 해당 페이지 내 영역을 0으로 만들기만 하면 될 텐데 굳이 디스크에 보관해 비효율을 야기할 이유가 있을까요?

 

A)

말씀해주신 대로 anonymous file은 일반적인 파일과 달리, disk가 아닌 RAM에 존재합니다. memfd_create 등의 함수를 활용해 생성할 수 있고, 해당 파일을 가리키는 reference가 모두 없어지면 자동으로 삭제된다고 합니다.

http://manpages.ubuntu.com/manpages/bionic/man2/memfd_create.2.html

 

 

질문 3

한 가지 질문을 더 드리고 싶습니다. mmap()에 대해 공부하던 중, mmap()으로 할당되는 공간은 힙도 스택도 아닌 공간이라는 걸 읽었습니다. 그 위치는 대략 유저 가상 주소에서 스택과 힙 사이 미할당 공간이라고 하는데요(사진 첨부),

그런데 힙과 스택은 사용하는 과정에서 grow하게 될 텐데, 물론 물리 프레임 상으로는 각각 분절되어 있을테니 문제가 되진 않을 것 같지만 가상 메모리 상에서는 다를 것 같습니다. 즉, 스택과 힙이 자라다가 mmap()으로 할당된 가상 주소와 만나게 될 여지는 없는지, 아니면 이를 대비하는 장치가 pintos를 포함해 운영체제에 있는지, 있다면 어떤 방식으로 존재하는 것인지가 궁금합니다.

A)

우선 본격적인 질문 답변에 앞서, 다음과 같은 사항을 말씀드리겠습니다.

- Linux와 PintOS등 일반적인 OS에서 process 당 stack의 size는 제한되어 있습니다. 그러나 linux에서는 ulimit 등을 통해 이러한 제한을 해제할 수 있습니다.
- 추후에도 말씀드리겠지만, heap의 구현은 일반적으로 data segment의 크기를 linear하게 증가시키는 sbrk syscall과 mmap syscall을 동시에 사용하여 구현되어있습니다.
- 64bit 기준 VA의 range는 약 16EB(==1048576TB)로, 현실적으로는 질문과 아래에서 설명하는 VA collision이 일어나기 어렵습니다.이후 설명은 mmap, malloc, stack growth 과정에서 설명하신 VA collision의 발생 시 동작에 대해 linux을 기준으로 설명드리겠습니다.
- mmap syscall의 경우 인자로 제공된 VA에 무조건 mapping을 요구하는 MAP_FIXED 등의 특수 플래그가 없으면 VA collision 없이 mapping을 수행하도록 구현되어 있습니다.
만일 stack/heap의 size가 매우 커져 요구하는 mapping이 불가능할 시 mmap syscall이 실패할것으로 추측합니다.
- malloc의 경우 sbrk가 사용된다면 mmap된 VA와 충돌하여 해당 syscall이 실패할 것이고,
mmap이 사용된다면 새롭게 mapping될 VA를 찾지 못해 해당 syscall이 실패할것으로 추측됩니다.
어느쪽이던 결과적으로 malloc 함수의 호출이 실패하여 NULL 등의 error가 반환될 것입니다.
- stack의 경우, PintOS와 유사하게 fault handler에서 stack을 implicit하게 mapping하도록 구현되어 있습니다.
Linux document에서는 이 과정이 mremap 을 implicit하게 호출하는것과 동일하다고 하니, mmap syscall 실패시에도 비슷한 루틴이 진행된다고 생각하시면 될 것 같습니다.

arch/x86/mm/fault.c의 fault handler do_user_addr_fault함수에서 expand_stack 호출
(https://github.com/torvalds/linux/blob/dd81e1c7d5fb126e5fbc5c9e334d7b3ec29a16a0/arch/x86/mm/fault.c#L1369)
->
expand_stack은 expand_downwards 호출 후 현재 stack size 제한, VA collision 여부 등을 고려해 stack growth 수행
(https://github.com/torvalds/linux/blob/763978ca67a3d7be3915e2035e2a6c331524c748/mm/mmap.c#L2474)
->
모종의 이유로 fault handling 실패시 fault handler는 bad_area -> __bad_area_nosemaphore 함수를 통해 SIGSEGV issue
(https://github.com/torvalds/linux/blob/dd81e1c7d5fb126e5fbc5c9e334d7b3ec29a16a0/arch/x86/mm/fault.c#L846)자세한 구현은 첨부드린 링크를 참조해주세요.

 

 

질문 4

정말 마지막(…!)으로 질문을 하나만 더 드리고 싶습니다. mmap()과 malloc()의 차이에 대해 공부하고 있는데 둘의 차이가 명확하게 잘 와닿지 않아 질문드립니다.

 malloc()의 경우 C 라이브러리 함수로 런타임 상에서 메모리를 동적으로 할당(힙에 할당)해주는 인터페이스라고 배웠습니다. 기본적으로 블록을 크기별로 관리하며(segregated list를 예시로) 동적으로 요청이 들어오는 크기에 맞는 블록을 자신이 관리하는 리스트에서 찾아 할당해줍니다. 그런데 예전에 공부할 때 보기로, 들고 있는 블록 list보다 더 큰 블록 요청이 들어오면 mmap()으로 페이지 크기의 블록을 할당해준다는 글을 읽은 적이 있습니다.

위의 말이 맞다면 기본적으로는 페이지를 요청할 때는 mmap(), 런타임 상에서 구조체 혹은 변수 등에 대한 공간을 동적으로 할당받고 싶을 때는 malloc()을 사용하는 게 둘의 기본 용도이나 malloc()의 경우 만약 요청받은 메모리 크기가 자신이 관리하고 있는 블록 list 내에 있는 블록보다 크다면 따로 mmap()을 호출해 페이지 크기만큼 받아온다고 이해해도 될까요?

만약 이게 맞다면 또 하나 궁금증이 생기는 게 있습니다. 가장 첫번째 질문으로 드렸던 것처럼, mmap()의 경우 힙도 스택도 아닌 anonymous page를 할당해준다고 배웠습니다. 물론 힙 역시 anonymous page의 집합이겠으나, 만약 malloc() 내부 소스코드에서 mmap()을 호출해주면 그때의 mmap()으로 받아온 페이지는 힙에 해당하는 것인지 아니면 기존 mmap() 정의처럼 힙도 스택도 아닌 공간이 되는 것인지가 헷갈립니다.

 

A)

앞서 말씀드렸다시피, 일반적으로 일컫는 user level API malloc의 경우 kernel로 부터 memory를 할당받는 sbrk나 mmap 등의 syscall을 통해 heap memory를 확보하고, 이를 user에게 배분하도록 구현되어 있습니다.
그리고 malloc 함수의 구현은 heap allocator의 구현과 정책에 따라 천차만별이기에, 말씀하신 질문에 대한 정확한 답변은 드리기 어렵습니다. 이해를 돕기 위한 몇가지 heap allocator들의 구현들은 다음과 같습니다.
glibc 2.23~2.28에서 사용하는 linux의 기본 user allocator ptmalloc2는 다음과 같이 구현되어있습니다.

- main arena에 대해서는 sbrk 수행 (
https://github.com/bminor/glibc/blob/b92a49359f33a461db080a33940d73f47c756126/malloc/malloc.c#L2727
)
- usable한 arena가 없을 시 mmap을 통해 새로운 arena 할당 (
https://github.com/bminor/glibc/blob/b92a49359f33a461db080a33940d73f47c756126/malloc/malloc.c#L3809
)
- 관리하는 bin들의 block size보다 현저히 큰 (system config에 지정된 
mmap_threshold
 보다 큰 크기의) 요청에 대해서 mmap 수행 (
https://github.com/bminor/glibc/blob/b92a49359f33a461db080a33940d73f47c756126/malloc/malloc.c#L2579
)

facebook이 채택한 성능 중심 설계 allocator jemalloc은 다음과 같이 구현되어있습니다.

--enable-dss
 옵션을 통해 sbrk와 mmap 중 보다 선호되는 system allocator를 사용하도록 지정 가능, 아닐 시 mmap만 사용.
- (
https://github.com/jemalloc/jemalloc/blob/12cd13cd418512d9e7596921ccdb62e25a103f87/src/extent_dss.c#L41
)
- (
https://github.com/jemalloc/jemalloc/blob/9015e129bd7de389afa4196495451669700904d0/src/pages.c#L112
)

google이 개발한 multi-threading 특화 allocator TCMalloc는 다음과 같이 구현되어 있습니다.

system-alloc
 class를 통해 sbrk/mmap 등의 system memory allocator를 선택할 수 있도록 분리하였으나 기본적으로 mmap을 사용 (
https://github.com/google/tcmalloc/blob/3adbf95a80eb20f3c6e5b2c0e6a84a2633810e30/tcmalloc/system-alloc.cc#L567
)
위와같이 대부분의 allocator들은 오래된 syscall인 sbrk 보다는 mmap syscall을 통해 memory를 확보하도록 구현되어 있습니다.
따라서 마지막에 주신 질문에 대해서는, heap allocator에 의해 할당되고 관리되는 memory는 전부 heap이라고 말할 수 있겠습니다.

 

 

 

반응형