. [PintOS, Project 4] Buffer Cache 구현
본문 바로가기
Pintos Project/Project 4

[PintOS, Project 4] Buffer Cache 구현

by 불냥이_ 2021. 3. 8.

히히히

이걸 언제 다해

 

 

 

 

 

 

 

 

 

++++ 벌레잡기 ++++

 page를 만들지 못한다. 왜일까

이걸 initializer에서 해줄려고 해서 문제일까? (hash가 만들어지기도 전에)

 

hash는 supplemental_page_table에서 해주는데 흠....

cache_init은 스레드만 만들고 페이지를 만드는 것은 여기서 하면 안되는걸까 

 

이걸 함수로 따로 만들고, setupstack할 때, 같이 만들어 봐야겠따.

 

void
create_cache_page(void)
{
	list_init(&cache_list);
	for (int i = 0 ; i < 8 ; i++)
	{
		void *tmp = UNDER_STACK - PGSIZE*(i+1);


		struct cache_entry *entry = malloc(sizeof(struct cache_entry));
		entry->cachep_va = tmp;
		entry->file = NULL;
		entry->offset = 0;
		entry->dirty = false;
		entry->tick = 0;
		if (!vm_alloc_page_with_initializer(VM_PAGE_CACHE, tmp, true, page_cache_initializer, entry))
		{
			PANIC("cache_page alloc ERROR!!!!!\n");
		}
		if (!vm_claim_page(tmp))
		{
			PANIC("cache_page claim ERROR!!!!!\n");
		}
		struct page *tmp_p = spt_find_page(&thread_current()->spt, tmp);
		list_push_back(&cache_list, &entry->cache_elem);
	}
}

page_cache.c에 위와 같은 함수를 추가하고, 이를 setup_stack할 때 이 함수를 호출하도록 했다.

 

/* Create a PAGE of stack at the USER_STACK. Return true on success. */
static bool
setup_stack(struct intr_frame *if_)
{
    // printf("---DEBUG // seti[] // START \n");
    bool success = false;
    void *stack_bottom = (void *)(((uint8_t *)USER_STACK) - PGSIZE);


    struct supplemental_page_table *spt = &thread_current ()->spt;

    success = vm_claim_page (stack_bottom);
    if(success){
        if_->rsp = USER_STACK;
    }

    struct page* page = spt_find_page(spt, stack_bottom);

    /* --------------------------- >> Project.4 Cache >> --------------------------- */
    create_cache_page();
    /* --------------------------- << Project.4 Cache << --------------------------- */

    return success;
}
#endif /* VM */

 

이 정도로 하고 딱히 수정한 것은 없는 것 같은데... 

현재 cache는 mmap할 때 통과하면 제대로 됐을 거라고 생각하는데, 대부분의 mmap이 통과하는 것 보면 문제는 없는 것 같다. 

 

그렇다면 페이지는 제대로 만든 것 같다. 현재 테스트케이스로 보이는 문제점은 fork()를 실행할 때, page_cache를 복사하지 않았고, 이로인해 kill할 때, 없는 것을 죽일려고 해서 (hash는 복사가 되니깐) 문제가 되는 것 같다. 

 

 

우선 spt_copy()에서 page의 type이 cache_page인 경우 cache_entry를 복사하도록 하자.

        /* --------------------------- >> Project.4 Cache >> --------------------------- */
        case VM_PAGE_CACHE:
        new_page->page_cache.entry = page->page_cache.entry;        
        /* --------------------------- << Project.4 Cache << --------------------------- */

 

 그리고 cache_entry 목록도 복사해야될 것 같은데, 어느 시점에 해야될지 모르겠다. 일단 spt_copy에서 같이 해주자.

그런데, spy_copy()는 child thread 기준이기때문에 parent thread의 cache_entry를 가져올 방법이 없다.

thread에 cache_entry를 가리킬 수 있도록하자 (mmfile처럼). 

 

 여기서 드는 의문은, cache는 thread가 공통으로 가져야되는 것인가, 아니면 thread마다 따로 가지는 것인가인데, 근본적으로는 cache는 thread들이 모두 공유하고 있어야하는 것이다. 하지만 지금 실질적으로는 stack처럼 thread의 가상메모리안에 소속되어있기 때문에, stack처럼 복사해놔도 상관은 없을 것 같지만..

 

 하지만 pintos manual을 읽어보면, cache는 thread 공용으로 써야할 것 같다. 그렇다면 spt_kill할 때랑 filesys_done() (power_off()에서 실행한다.)할 때, cache를 비우도록하고, cache_page를 만드는 함수를 stack이 아니라 다른데서 실행해야할 지도 모르겠다. spt_copy()에서 stack은 그대로 복사하니깐.. fork()는 stack_setup()실행하지 않을 것이다. 그렇다면 어떻게 해야할까.

 

 

/* Initialize new supplemental page table */
void supplemental_page_table_init(struct supplemental_page_table *spt UNUSED)
{
    hash_init(&spt->pages, page_hash, page_less, NULL);
    /* --------------------------- >> Project.4 Cache >> --------------------------- */
    create_cache_page();
    /* --------------------------- << Project.4 Cache << --------------------------- */
}

 그러면 create_cache_page()를 setup_stack()이 아니라 spt_init()에서 하도록 하자. 그러면 spt_kill()에서 page_cache를 지워도 될 것 같다. 

 

 대충 setup_stack()과 비슷한 타이밍에 수행하면 될 것 같은데.. 문제는 process_execute()랑 do_fork()는 별개로 실행된다는 것이다. 양쪽에 다 넣어줄 수도 있긴한데.

 

 아니면, 쌩 thread는 setup_stack()에서 create_page_cache()를 수행하고, forked thread는 spt_copy()에서 실행하는 것은 어떨까. 그것도 괜찮을 것 같다.

 

 그러면 이제 문제는 page를 복사하기 전에 cache_page를 만들지, 복사하고 만들지의 문제인데, parent thread의 hash에는 이미 들어가있으므로 이걸 그대로 복사해버릴 수 있어서 문제이다. fork를 한다해서 부모 cache_page를 죽이는 것이 아니니깐.

 

 이건 어떨까. cache_list는 공용이니깐, cache_page를 복사하고, cache_page에 붙어있는 cache_entry의 pointer도 복사하는데, entry를 write-behind 시키고 초기화시키는 것이다. 

 

 

 흠 그런데 어렵네. 그러면 thread가 바뀔때마다 초기화시켜야되는 것이 아닌가. 아니면 pintos가 유지되는 동안은 절대 죽이면 안되는 것인가. 

 

-------

 

내가 thread와 process를 혼동해서 생각했던 것 같다. 정리해보면, 한 process 안에서 thread는 자원을 공유할 것이다. 그것은 cache또한 마찬가지이다. cache가 뒤집혀질때는 process가 바뀔 때 일것이다. 예를 들면, process_exit()이나 process_fork()또한. 

 

 그렇다면 process_fork()에서 부모는 cache를 계속 가지고 있어야할텐데....

 

아니다. 이렇게하자.

1) process가 새로 실행된다면 (process_exec() or do_fork()) cache는 비어있는 상태가 되어야할 것이다. 

2) 그렇기 때문에 process_exit() 에서 cache를 reset한다. 

3) 그렇기 때문에 spt_copy()에서 cache를 reset한다.

※ reset이라는 것은 page는 그대로 남긴 채, 내용만 비운다는 것(write-behind) 이다. 

4) cache reset할 때는, 그에 맞는 cache_entry도 비워준다.

 

이를 수행하려면, cache_entry의 list는 전역이 아니라 thread에 소속이 되어야할 것 같다. 취지에는 어긋나겠지만. 하지만 이렇게 한다면, process가 바뀌어도 문제가 없지 싶은데... 그리고 현재 cache는 mmap_area를 사용하므로 괜찮지싶다. 

 

 해보자. 

 

다시 할 일을 정리해보자.

1) cache_list를 static이 아닌 thread 소속으로 만들기. (page는 전역으로 만들 수 없고, cache_entry랑 cache_page는 묶여있기 때문에 이렇게 해야한다. 

 

2) process_cleanup()에서 cache_list와 cache_page를 초기화하기. process_cleanup()은 process_exec()과 process_exit()에서 호출된다. process_exec()에서는 load()가 호출되기 직전에 process_cleanup()을 할 테니, process_cleanup()은 cache_list와 cache_page를 완전 삭제하면 될 것 같다. (또한, process_cleanup()은 spt_kill()을 호출하는데. 이 때, cache_page는 사라질 것이다.) 그리고 load()에서 create_cache_page()를 호출한다. 

 

3) fork()시에는 parent thread의 cache_page와 cache_list를 복사해온다. 이는 spt_copy()에서 시행한다. (이 때, cache_list를 복사해오는 것이 안된다면, process_fork()나 do_fork()에서 복사한다.)

 

1)

#ifdef VM
    /* Table for whole virtual memory owned by thread. */
    struct supplemental_page_table spt;
    uintptr_t sc_rsp;
#endif
/* --------------------------- >> Project.4 Cache >> --------------------------- */
#ifdef FILESYS
    struct cache_entry *cache_entry;
    struct list cache_list;
#endif
/* --------------------------- << Project.4 Cache << --------------------------- */

우선 thread에 cache_list를 붙였다.

 

 

/* Does basic initialization of T as a blocked thread named
   NAME. */
static void
init_thread(struct thread *t, const char *name, int priority)
{
    ASSERT(t != NULL);
    ASSERT(PRI_MIN <= priority && priority <= PRI_MAX);
    ASSERT(name != NULL);

    memset(t, 0, sizeof *t);
    t->status = THREAD_BLOCKED;
    strlcpy(t->name, name, sizeof t->name);
    t->tf.rsp = (uint64_t)t + PGSIZE - sizeof(void *);

    // Priority donation 초기화
    t->priority = priority;
    t->init_priority = priority;
    t->wait_on_lock = NULL;
    list_init(&t->donations);

    //Process
    list_init(&t->children);

    // don't touch MF
    t->magic = THREAD_MAGIC;

    // MLFQ
    t->nice = NICE_DEFAULT;
    t->recent_cpu = RECENT_CPU_DEFAULT;

    /* --------------------------- >> Project.4 Cache >> --------------------------- */
    list_init(&cache_list);
    /* --------------------------- << Project.4 Cache << --------------------------- */
}

그러면, init_thread()에서 list_init()을 해야할 것이다. 이로서, cache_list는 thread에 소속되었다. 

그리고 create_cache_page()를 수정하자. 

 

void
create_cache_page(void)
{
	// list_init(&cache_list);
	for (int i = 0 ; i < 8 ; i++)
	{
		void *tmp = UNDER_STACK - PGSIZE*(i+1);


		struct cache_entry *entry = malloc(sizeof(struct cache_entry));
		entry->cachep_va = tmp;
		entry->file = NULL;
		entry->offset = 0;
		entry->dirty = false;
		entry->tick = 0;
		if (!vm_alloc_page_with_initializer(VM_PAGE_CACHE, tmp, true, page_cache_initializer, entry))
		{
			PANIC("cache_page alloc ERROR!!!!!\n");
		}
		if (!vm_claim_page(tmp))
		{
			PANIC("cache_page claim ERROR!!!!!\n");
		}
		struct page *tmp_p = spt_find_page(&thread_current()->spt, tmp);
		list_push_back(&thread_current()->cache_list, &entry->cache_elem);
	}

 

list_push_back()을 전역 리스트가 아니라 thread소속 리스트에 실행한다.

또한, 다른 cache 관련 함수에서 list를 호출할 때, 현재 thread의 cache_list를 불러오도록 고치자. 

 

for (e = list_begin(&cur_t->cache_list); e != list_end(&cur_t->cache_list) ; e = list_next(e))

 위 부분만 고치면 될 것 같은데..

 

2) cache_page를 지우는 함수를 만들자.정확히 말하면 cache_page 안의 내용물을 지우고, 그 cache_page에 해당하는 entry를 제거하는 함수이다.

/* Destory the page_cache. */
static void
page_cache_destroy (struct page *page) {
	struct cache_entry *cache_e = page->page_cache.entry;

	struct file *f = cache_e->file;
	bool dirty_bit = cache_e->dirty;	
	int offset = cache_e->offset;

	if (dirty_bit)
	{
		if (file_write_at(f, page->va, PGSIZE, offset) != PGSIZE)
		{
			PANIC("cache destroy write ERROR");
		}
	}

	if (page->frame)
	{
		pml4_clear_page(thread_current()->pml4, page->va);
		if (page->frame->pml4)
		{
			palloc_free_page(page->frame->kva);
		}
		free(page->frame);
		page->frame = NULL;
	}
    
    list_remove(&cache_e->cache_elem);
}

 dirty라면, write-behind를 실행하도록 하고, frame과 pml4를 해제하자.

이러면 spt_kill()이 작업하면서 page 안의 내용물을 정리할 것이다. 

 

 그리고 elem을 list에서 제거하자. 이 작업이 완료되고 나면, cache_list는 빈 껍데기만 남을 것이며 이마저도 thread가 해체될 때 같이 해체될 것이다.

 

 이렇게 process_cleanup()에서 page와 동시에 처리해주면, filesys_done()에서는 해줄 일이 없지 않을까.

 

 

 ...싶었는데 그러지 않았다.

halt()가 호출된 경우에는 power_off()에서 process_exit()를 호출하지 않고, filesys_done()만 해준다. 

지금 상태라면 halt()가 호출된다면 write-behind가 실행되지 않는 것이다.

 

 filesys_done()에서도 write-behind를 해줄 수 있도록 구현하자. 

 

 

 

 

 

 

 

이제 fork()시에 cache_page와 cache_list를 복사할 수 있도록하자.

 

 

        /* --------------------------- >> Project.4 Cache >> --------------------------- */
        case VM_PAGE_CACHE:
            struct cache_entry *new_entry = malloc(sizeof(struct cache_entry));
            struct cache_entry *old_entry = page->page_cache.entry;

            new_entry->cachep_va = old_entry->cachep_va;
            new_entry->file = old_entry->file;
            new_entry->offset = old_entry->offset;
            new_entry->dirty = old_entry->dirty;
            new_entry->tick = old_entry->tick;
            new_page->page_cache.entry = new_entry;

            list_push_back(&thread_current()->cache_list, &new_entry->cache_elem);
        /* --------------------------- << Project.4 Cache << --------------------------- */

 

우선 spt_copy()에서 page 자체는 자동으로 복사가 될 것이다. 그러면 switch 문에서 VM_PAGE_CACHE로 들어왔을 때, new_entry로 struct cache_entry만큼 malloc받고, 내용물을 복사한다.

그리고 new_entry의 주소를 page->page_cache로 붙여준다. 

 

 한번 확인하고 가자. 

 cache_page를 해체하는 것은 process_cleanup()에서만 시행되고, 이는 process_exit()이나 process_exec()시에 context switching을 수행하면서 호출된다. 즉, fork와는 상관없다. 그렇다면 fork 시에는 어떻게 해야할까? parent thread는 죽은 것이 아니라 sleep 상태에 들어가는 것 일뿐이므로 cache를 비롯한 page들은 그대로 놔둬야한다. child thread는 parent thread의 page를 그대로 복사해오므로, page와 함께 cache_entry도 복사해와서 child thread의 cache_list에 넣어준다. 

 

 가장 크게 드는 의문은 entry의 내용이 그대로 child thread에 담겨있을까가 문제인데, 일단 cache는 file을 가리키는 것이 아니라 file이 가지고 있는 내용물을 메모리에 가지고 있는 것이고, 그 메모리는 fork()가 되어서도 같이 가리키니깐 문제가 없을 것 같다.

 

 

 

 

 

 

 

  

 

 

 

 

 

 

댓글