ChCore-lab2

木木ちゃん發表於2024-11-09

lab 2: Memory Manage(working)

新的環境好像不支援arm架構了,總是會在make build觸發錯誤
exec chbuild not found. 我們於是只能使用utm平臺+qemu模擬amd64架構的ubuntu系統來進行執行。
首先我們還是先進行make build來獲得我們想要的環境。

1 Buddy System

練習題1: 完成 kernel/mm/buddy.c 中的 split_chunkmerge_chunkbuddy_get_pages、 和 buddy_free_pages 函式中的 LAB 2 TODO 1 部分,其中 buddy_get_pages 用於分配指定階大小的連續物理頁,buddy_free_pages 用於釋放已分配的連續物理頁。

我們首先先來了解一下我們需要完成的任務。

  1. 從記憶體中獲取自由連結串列的情況。
  2. 在分配相對應的連結串列時,根據階次進行相對應地拆分,同時修改自由連結串列的填寫情況。
  3. 釋放已經分配的頁,並將他們合成。

我們首先閱讀一下kernel/mm/buddy.c的程式碼。

第一部分:初始化init_buddy()

在這一部分,我們首先初始化了實體記憶體池的相關資料。例如起始地址,起始頁表,起始記憶體大小等。這裡我們的每塊夥伴頁表的大小等常量的定義位於kernel/include/mm/buddy.h中。

這裡頁表的最大階次規定為14,並且其夥伴頁表的最大大小為0x1000, 因此我們的夥伴頁表階次為12.

接下來需要了解我們所需要的各個結構體組成。我們在kernel/include/mm/buddy.hkernel/include/common/list.h裡面可以找到相關的敘述。

page結構體

  • 節點(list_head)
  • 分配狀態(int)
  • 階次(int)
  • ...

free_list結構體

  • free_list(list_head)
  • 自由頁表的個數(nr_free)

物理執行緒池

  • 起始地址(虛擬地址)
  • 地址大小
  • 頁表相關資料
  • 記憶體鎖
  • 自由連結串列(不同階次的)
  • 物理頁表數目

list_head結構體

  • 前向指標
  • 後向指標
  • 初始化雙向連結串列
  • 增加,新增,刪除,清零,尋找下一個容器,列表下標,簡單迭代器...

其中list_head裡有一些宏定義十分有意思。我們之後會來看一看。

1.1 獲得未分配的頁表

首先我們先要分配夥伴連結串列。這時我們需要

  • 尋找空閒連結串列。
  • 從空閒連結串列去除夥伴塊。
  • 檢查夥伴塊階次並根據情況進行分裂。

因此,首先我們先完成buddy_get_pages()部分。在這一部分中,我們要為夥伴系統分配空閒的物理頁,並將其放入到自由連結串列中。

struct page *buddy_get_pages(struct phys_mem_pool *pool, int order)
{
        int cur_order;
        struct list_head *free_list;
        struct page *page = NULL;

        if (unlikely(order >= BUDDY_MAX_ORDER)) {
                kwarn("ChCore does not support allocating such too large "
                      "contious physical memory\n");
                return NULL;
        }

        lock(&pool->buddy_lock);

        /* LAB 2 TODO 1 BEGIN */
        /*
         * Hint: Find a chunk that satisfies the order requirement
         * in the free lists, then split it if necessary.
         */
        /* BLANK BEGIN */
        // UNUSED(cur_order);
        // UNUSED(free_list);
        for (cur_order = order; cur_order < BUDDY_MAX_ORDER; cur_order++)
        {
                // 檢查是否具有空閒連結串列。
                if(pool -> free_lists[cur_order].nr_free != 0) 
                {
                        // 獲取對應階次的空閒連結串列頭。
                        free_list = &(pool->free_lists[cur_order].free_list);
                        // 獲取這個連結串列節點所對應的頁表頭。檢查這一條連結串列是否為空。
                        if (!list_empty(free_list))
                        {
                                /**
                                 * 強行從free_list->next作為其成員,計算出page結構的首地址
                                 * 然後分配一段新的記憶體空間給這個頁表。
                                 * 因此我們需要將list_head->next,也就是第一個頁表節點傳入,計算出其在page中的偏移量
                                 * 然後讓編譯器進行型別轉換,將包含有連結串列指向的地址的一段地址解釋成為新的結構體。
                                */ 
                                page = list_entry(free_list->next, struct page, node);
                                // 分配空間之後其餘的值均為隨機值需要進行更改。
                                page -> allocated = 0;
                                page -> order = cur_order;
                                page -> pool = pool;
                                break;
                        }
                }
        }
        if (page == NULL)
                goto out;

        // 分配了頁表,刪除掉自由連結串列內的該值。減少改階次下的連結串列數。
        list_del(&page -> node);
        pool -> free_lists[cur_order].nr_free -= 1;
        // 必要的情況下分裂連結串列。
        page = split_chunk(pool, order, page);
        page -> allocated = 1;
        /* BLANK END */
        /* LAB 2 TODO 1 END */
out: __maybe_unused
        unlock(&pool->buddy_lock);
        return page;
}

1.2 分裂部分

接下來我們要完成分裂連結串列部分,採用遞迴進行。注意每次我們都需要減少order後獲得新的夥伴塊,直到獲得的夥伴塊和我們需要的order相同。
我們需要自行不斷減少頁表的order數,透過get_buddy_chunk()的方式,從當前的頁表中獲得其子頁(夥伴頁),直到符合條件為止。
採用遞迴的方式進行分裂,我們可以得到:

__maybe_unused static struct page *split_chunk(struct phys_mem_pool *__maybe_unused pool,
                                int __maybe_unused order,
                                struct page *__maybe_unused chunk)
{
        /* LAB 2 TODO 1 BEGIN */
        /*
         * Hint: Recursively put the buddy of current chunk into
         * a suitable free list.
         */
        /* BLANK BEGIN */
        int cur_order = chunk -> order;
        if (cur_order == order)
                return chunk;
        else
        {
                // 分裂連結串列。
                chunk->order -= 1;
                struct list_head *free_list = &(pool -> free_lists[chunk->order].free_list);
                struct page *buddy_chunk;
                buddy_chunk = get_buddy_chunk(pool, chunk);
                buddy_chunk -> allocated = 0;
                buddy_chunk -> order = chunk -> order;
                buddy_chunk -> pool = chunk -> pool;
                buddy_chunk -> slab = chunk -> slab;
                list_add(&(buddy_chunk -> node), free_list);
                pool -> free_lists[buddy_chunk->order].nr_free += 1;
                return split_chunk(pool, order, chunk);
        }       
        /* BLANK END */
        /* LAB 2 TODO 1 END */
}

接下來完成釋放頁表部分。只需要將頁表標記成空閒後,嘗試合併頁表塊,將合併的頁表塊加入到對應order的自由連結串列之中。

void buddy_free_pages(struct phys_mem_pool *pool, struct page *page)
{
        int order;
        struct list_head *free_list;
        lock(&pool->buddy_lock);

        /* LAB 2 TODO 1 BEGIN */
        /*
         * Hint: Merge the chunk with its buddy and put it into
         * a suitable free list.
         */
        /* BLANK BEGIN */
        // UNUSED(free_list);
        // UNUSED(order);
        page->allocated = 0;
        page = merge_chunk(pool, page);
        list_add(&page->node, &pool->free_lists[page->order].free_list);
        pool->free_lists[page->order].nr_free += 1;
        /* BLANK END */
        /* LAB 2 TODO 1 END */

        unlock(&pool->buddy_lock);
}

遞迴式合併夥伴。只需要每次獲取chunk直到無法獲取更高階次的為止。注意檢查階次是否到達合併頂點,是否被分配或夥伴頁表為空的情況。

/* The most recursion level of merge_chunk is decided by the macro of
 * BUDDY_MAX_ORDER. */
__maybe_unused static struct page * merge_chunk(struct phys_mem_pool *__maybe_unused pool,
                                struct page *__maybe_unused chunk)
{
        /* LAB 2 TODO 1 BEGIN */
        /*
         * Hint: Recursively merge current chunk with its buddy
         * if possible.
         */
        /* BLANK BEGIN */
        if (chunk->order == BUDDY_MAX_ORDER - 1) 
                return chunk;

        struct page *buddy = get_buddy_chunk(pool, chunk);
        if (buddy == NULL || buddy->allocated == 1 || buddy->order != chunk->order) 
                return chunk;
        else 
        {
                list_del(&buddy->node);
                --pool->free_lists[buddy->order].nr_free;
                if (chunk > buddy) 
                        chunk = buddy;
                chunk->order += 1;
                return merge_chunk(pool, chunk);
        }

        /* BLANK END */
        /* LAB 2 TODO 1 END */
}

夥伴系統完成。
其中一個很重要的宏定義函式完成了透過結構體成員構造新結構體的方式。即我們有:

list_entry(ptr, type, field);
// 第一個引數是我們需要的結構體成員,第二個是我們想要轉換成的型別,
// 第三個是在想要轉換成為的型別中的成員。
// 本質上計算了成員與結構體開始地址的差值,
// 並告訴編譯器當前所需要的成員所在的結構體的首地址。
// 宏定義如下:
#define list_entry(ptr, type, field) \
	container_of(ptr, type, field)

#define container_of(ptr, type, field) \
	((type *)((void *)(ptr) - (void *)(&(((type *)(0))->field))))

2. SLAB分配器

完成 kernel/mm/slab.c 中的 choose_new_current_slaballoc_in_slab_implfree_in_slab 函式中的 LAB 2 TODO 2 部分,其中 alloc_in_slab_impl 用於在 slab 分配器中分配指定階大小的記憶體,而 free_in_slab 則用於釋放上述已分配的記憶體。

在SLAB分配時,我們需要先觀察其具有的結構體。

我們可以發現,SLAB指標(slab_pointer,也就是我們第一個要完成函式的傳入指標型別)包含著一個指標和一個list_head成員。結合課上知識,第一個為當前slab分配器的頭指標current,第二個則是所謂的partial指標,其中含有所有擁有空閒塊。因此,我們需要獲得partial指標的next,也就是第一個空閒的SLAB。這樣我們就有:

static void choose_new_current_slab(struct slab_pointer * __maybe_unused pool)
{
        /* LAB 2 TODO 2 BEGIN */
        /* Hint: Choose a partial slab to be a new current slab. */
        /* BLANK BEGIN */
        // 檢查是否為空。
        if(list_empty(&pool -> partial_slab_list))
                pool -> current_slab = NULL;
        else
        {
                // 選擇一個沒有分配的物理頁塊,返回並從partial中刪除。
                pool -> current_slab = list_entry(pool -> partial_slab_list.next, struct slab_header, node);
                list_del(pool -> partial_slab_list.next);
        }
        /* BLANK END */
        /* LAB 2 TODO 2 END */
}

接下來需要分配指定階次大小的記憶體。首先,我們可以看到在函式中,首次分配記憶體時我們需要初始化我們的每個slab,形成固定大小的slab塊並且組織好slab的free_list. free_list是一個連結串列結構,每個指標儲存著指向下一個沒有被分配的實體記憶體頁。將slab的header槽中的free_list_head轉換成slab_slot_list型別的一個指標後,其成員next_free就是下一個未被分配的物理頁。我們具有以下的圖示結構:(有點難理解)

接下來我們閱讀我們的函式。首先看函式給我們前面的部分:

static void *alloc_in_slab_impl(int order)
{
	struct slab_header *current_slab;
	struct slab_slot_list *free_list;
	void *next_slot;
	UNUSED(next_slot);
	// ...
}

這一部分給出了我們需要的幾個變數。首先,current_slab自然無需多言是我們需要分配的slab。其次,free_list是一個slab_slot_list型別,有:

/* Each free slot in one slab is regarded as slab_slot_list. */
struct slab_slot_list {
        void *next_free;
};

結合slab_header的free_list_head,我們可以發現,free_list_head可以轉換成slab_slot_list型別。而這個free_list_head有next_free指標,指向的正好是下一個自由的slot。要注意的是,每一個slab的第一個slot在本系統中不儲存實際的物理頁,相當於是頭指標,因此計算當前free_slot的數目時需要減去1。
這樣我們有:

        /* LAB 2 TODO 2 BEGIN */
        /*
         * Hint: Find a free slot from the free list of current slab.
         * If current slab is full, choose a new slab as the current one.
         */
        /* BLANK BEGIN */
        // 首先,我們先尋找當前slab的free_list. 也就是slab_header中的free_list_head.
        // 注意型別的轉換。從註釋中可以得到答案。
        free_list = (struct slab_slot_list*) current_slab -> free_list_head;
        next_slot = free_list -> next_free;
        current_slab -> free_list_head = next_slot;
        current_slab -> current_free_cnt--;
        // 檢查是否slab已滿。注意slab_header的第一個slot用於儲存頭資訊!因此需要-1
        if (current_slab -> current_free_cnt == 0) 
                choose_new_current_slab(&slab_pool[order]); // 分配新的slab
        /* BLANK END */
        /* LAB 2 TODO 2 END */

接下來就是free_in_slab. 我們要進行的是釋放這個slot,並將其放回free_list.

        struct page *page;
        struct slab_header *slab;
        struct slab_slot_list *slot;
        int order;

        slot = (struct slab_slot_list *)addr;
        page = virt_to_page(addr);
        if (!page) {
                kdebug("invalid page in %s", __func__);
                return;
        }


        slab = page->slab;
        order = slab->order;
        lock(&slabs_locks[order]);

        try_insert_full_slab_to_partial(slab);

首先從實體地址轉換成slot,這樣我們就可以操作其next_free指標指向下一個離他最近的free_slot.隨後,判斷是否是一個全滿的slab釋放出空slot,將其插入partial_list(用於指明當前記憶體池中的具有空閒槽的slab)中。
接下來,理論上我們應該去尋找離我們最近的free_slot,讓我們的next_free指向它。但是我們並沒有一個插入連結串列的介面。因此我們可以用另一種方法,讓這個新的slot成為我們的slab新的free_list_head,讓原先的free_list_head指向我們的slot。如下圖所示:

這樣我們就有:

        /* LAB 2 TODO 2 BEGIN */
        /*
         * Hint: Free an allocated slot and put it back to the free list.
         */
        /* BLANK BEGIN */
        slot -> next_free = slab -> free_list_head;
        slab -> free_list_head = (void*) slot;
        slab -> current_free_cnt += 1;
        UNUSED(slot);
        /* BLANK END */
        /* LAB 2 TODO 2 END */

這樣我們完成了簡單的slab。

3. kmalloc

完成 kernel/mm/kmalloc.c 中的 _kmalloc 函式中的 LAB 2 TODO 3 部分,在適當位置呼叫對應的函式,實現 kmalloc 功能

根據提示,首先我們直接呼叫alloc_in_slab()功能,分配slab給我們的addr。當此時的size與當前slab的size相比過大時,我們則需要重新從夥伴系統中獲得新的物理頁,再轉換成slab分配器,獲得我們的地址。

if (size <= SLAB_MAX_SIZE) {
                /* LAB 2 TODO 3 BEGIN */
                /* Step 1: Allocate in slab for small requests. */
                /* BLANK BEGIN */
                UNUSED(addr);
                UNUSED(order);
                addr = alloc_in_slab(size, real_size);
                /* BLANK END */
#if ENABLE_MEMORY_USAGE_COLLECTING == ON
                if(is_record && collecting_switch) {
                        record_mem_usage(*real_size, addr);
		}
#endif
        } else {
                /* Step 2: Allocate in buddy for large requests. */
                /* BLANK BEGIN */
                order = size_to_page_order(size);
                addr = _get_pages(order, is_record);
                /* BLANK END */
                /* LAB 2 TODO 3 END */
        }

這樣我們完成了實驗二第一部分,應該獲得30分。

待續...