linux 非連續記憶體區管理 vmalloc

kuraxii發表於2024-04-26

非連續記憶體管理

從前面的章節我們知道,把記憶體對映待一組連續的頁框是最好的選擇,這樣會充分利用快取記憶體並獲得較低的平均訪問時間。不過,如果對記憶體區的請求不是很頻繁,那麼,透過連續的線性地址來訪問非連續的頁框這樣的一種分配方式就會顯得很有意義。
這種模式主要是避免了外碎片,而缺點是打亂了核心頁表。顯然,非連續記憶體區的大小必須是4096的倍數。Linux在幾個地方使用非連續記憶體區,例如,為活動的交換區分配資料結構,為模組分配空間(linux驅動模組),或者給某些I/O驅動程式分配快取。

非連續記憶體區的線性地址

高階記憶體對映的順序

在實體記憶體對映的末尾與第一個記憶體之間差入一個大小為8MB的安全區,目的是為了捕獲記憶體的越界訪問。出於同樣的理由,插入其他4kb大小的安全區來隔離非連續的記憶體區。

非連續記憶體區的描述符

型別 名稱 說明
void* addr 記憶體區內第一個記憶體單元的線性地址
unsigned long size 記憶體區的大小 + 4096(記憶體區之間的安全區間的大小)
unsigned long flag 非連續記憶體對映的記憶體的型別
strutc page** pages 指向 nr_pages陣列的指標,該陣列由指向頁描述符的指標組成
unsugned int nr_pages 記憶體區填充的頁框個數
unsugned long phys_addr 該欄位設為0,除非記憶體已被建立來對映一個硬體的I/O共享裝置
struct vm_struct* next 指向下一個vm_struct結構的指標
struct vm_struct {
	void			*addr;
	unsigned long		size;
	unsigned long		flags;
	struct page		**pages;
	unsigned int		nr_pages;
	unsigned long		phys_addr;
	struct vm_struct	*next;
};

透過next欄位,這些描述符被插入到一個簡單的連結串列中,連結串列的第一個元素的地址存放在vm_list變數中。flags欄位標識了非連續區對映的記憶體的型別:VM_ALLOC表示使用vmalloc()申請得到的頁,VM_MAP表示使用vmap()對映的已分配的頁,而VM_IOREMAP表示使用ioremap()對映的硬體裝置版的版上記憶體。

get_vm_area()函式線上性地址VMALLOC_STARTVMALLOC_END之間查詢一個空閒區域。
函式原型

struct vm_struct *get_vm_area(unsigned long size, unsigned long flags);

執行步驟如下

  1. 呼叫kmalloc()獲得一個vm_struct型別的記憶體區描述符。kmallocslab分配記憶體。
  2. 為寫得到vmlist_lock鎖, 並掃描型別為vm_struct的描述符表來查詢空閒的線性地址,空閒線性地址大小至少為size + 4096
  3. 如果存在這樣的空間,函式就初始化描述符的欄位,釋放vmlist_lock鎖,並以返回這個非連續記憶體區的其實地址而結束。
    初始化描述符包括addr,size,flag
  4. 否則,get_vm_area()釋放先前得到的描述符,釋放vmlist_lock,然後返回NULL。

分配非連續記憶體區

vmalloc() 函式給核心分配一個非連續的記憶體區。

void *vmalloc(unsigned long size)
{
       return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
	struct vm_struct *area;
	struct page **pages;
	unsigned int nr_pages, array_size, i;

    /* 將size擴充為4096的整數倍 */
	size = PAGE_ALIGN(size); 
	if (!size || (size >> PAGE_SHIFT) > num_physpages)
		return NULL;

    /* 建立一個新的描述符 描述符的flag欄位被初始化為 VM_ALLOC */
	area = get_vm_area(size, VM_ALLOC);
	if (!area)
		return NULL;

    /* 計算出 page 計數 */
	nr_pages = size >> PAGE_SHIFT;
	array_size = (nr_pages * sizeof(struct page *));

	area->nr_pages = nr_pages;
	/* Please note that the recursion is strictly bounded. */
	if (array_size > PAGE_SIZE)
		pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL);
	else
        /* __vmalloc 呼叫kmalloc為pages欄位分配記憶體 */
		pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
	area->pages = pages;

    /* 若分配失敗,則清除記憶體區描述符,並返回NULL */
	if (!area->pages) {
		remove_vm_area(area->addr);
		kfree(area);
		return NULL;
	}

    /* 將pages頁描述符陣列指標初始化為 0 */
	memset(area->pages, 0, array_size);

    /* 使用alloc_page 函式逐個申請頁框 頁框很可能不是連續的*/
	for (i = 0; i < area->nr_pages; i++) {
		area->pages[i] = alloc_page(gfp_mask);
		if (unlikely(!area->pages[i])) {
			/* Successfully allocated i pages, free them in __vunmap() */
			area->nr_pages = i;
			goto fail;
		}
	}
	
    /* 現在已經分配了一組非連續的頁框來對映這些線性地址,最後至關重要的是修改核心使用的頁表項
    以此表明分配給非連續記憶體區的每個頁框對應著一個線性地址 */
	if (map_vm_area(area, prot, &pages))
		goto fail;
	return area->addr;

fail:
	vfree(area->addr);
	return NULL;
}

map_vm_area函式將非連續頁框對映到連續頁框上
函式定義在mm/vmalloc.c line201

/* 引數
*  area:vm_struct 描述符的指標   
*  prot:已分配頁框的保護位。在vmalloc中始終為 PAGE_KERNEL
*  pages:struct page* 陣列的指標,
*/
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
    /*
     * 先將非連續記憶體區的開始和末尾分別賦值給 address end
     */
	unsigned long address = (unsigned long) area->addr;
	unsigned long end = address + (area->size-PAGE_SIZE);
	unsigned long next;
	pgd_t *pgd;
	int err = 0;
	int i;

    /*
      linux 2.6使用 4級頁表
      PGD --> PUD --> PMD --> PT
        |       |       |      |
        |       |       |      +--> 實際物理頁面 (pages)
        |       |       +------> 中間目錄
        |       +------------> 上級目錄
        +------------------> 全域性目錄
        
    pgd_offset_k 用於獲取pgd條目得指標
    pgd_index 用於獲取pgd條目得索引
    類似於陣列的下標和指標關係
    */
    /* 獲取address所在的全域性頁目錄表起始條目 */
	pgd = pgd_offset_k(address);
	
    // 獲取頁表自旋鎖,避免競爭條件
    spin_lock(&init_mm.page_table_lock);

    /*從起始條目開始遍歷,直到所有得頁框都被對映*/
	for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
        // 新建以一個條目,用來存放所有的page,起始點為address
		pud_t *pud = pud_alloc(&init_mm, pgd, address);
		if (!pud) {
			err = -ENOMEM;
			break;
		}

        // 計算線性地址的所在的下一個全域性頁目錄項的起始地址
		next = (address + PGDIR_SIZE) & PGDIR_MASK;
        // 如果下一個起始地址 超出了當表項範圍,說明在當前表項可以對映所有的實體記憶體
		if (next < address || next > end)
			next = end;

        // 然後向下依次新增更第一記的條目
		if (map_area_pud(pud, address, next, prot, pages)) {
			err = -ENOMEM;
			break;
		}

        /*****************************************************************************************************************
        上層目錄對映
        The map_area_pud( ) function executes a similar cycle for all the page tables that a Page Upper Directory points to:
        */
        do {
        pmd_t * pmd = pmd_alloc(&init_mm, pud, address);
        if (!pmd)
        return -ENOMEM;
        if (map_area_pmd(pmd, address, end-address, prot, pages))
        return -ENOMEM;
        address = (address + PUD_SIZE) & PUD_MASK;
        pud++;
        } while (address < end);
        /* 中間目錄對映
        The map_area_pmd( ) function executes a similar cycle for all the Page Tables that a Page Middle Directory points to:
        */
        do {
        /* 分配一個新的頁表 */
        pte_t * pte = pte_alloc_kernel(&init_mm, pmd, address);
        if (!pte)
        return -ENOMEM;
        if (map_area_pte(pte, address, end-address, prot, pages))
        return -ENOMEM;
        address = (address + PMD_SIZE) & PMD_MASK;
        pmd++;
        } while (address < end);

        /* 頁框對映
        The main cycle of map_area_pte( ) is:
        */
        do {
        struct page * page = **pages;
        // 將物理頁的內容以及已分配頁框的保護位 填入該頁表實現對映
        set_pte(pte, mk_pte(page, prot));
        address += PAGE_SIZE;
        pte++;
        (*pages)++;
        } while (address < end);
        /****************************************************************************************************************/

		address = next;
		pgd++;
	}

    /* 物理頁對映完成,釋放鎖 並重新整理快取*/
	spin_unlock(&init_mm.page_table_lock);
	flush_cache_vmap((unsigned long) area->addr, end);
	return err;
}

除了vmalloc()函式外,非來納許記憶體區還能由vmalloc_32()函式分配,但是它只從ZONE_NORMALZONE_DMA記憶體管理區中分配頁框

Linux2.6還特別提供了一個vmap函式與vmalloc很相似,但是他不分配頁框。完成的工作是,在vmalloc虛擬地址空間中找到一個空閒區域,然後將page頁面陣列對應的實體記憶體對映到該區域,最終返回對映的虛擬起始地址。

釋放非連續記憶體區

vfree()函式釋放vmalloc()或者vmalloc_32()建立的非連續記憶體區,而vunmap()函式釋放vmap()建立的記憶體區。兩個函式都是用同一個引數————將要釋放的記憶體區的線性地址address,他們都依賴於__vunmap()函式來做實質上的工作。該函式執行以下操作:

  1. 呼叫remove_vm_area()函式得到vm_struct描述符的地址area,並清除非連續記憶體區中的線性地址對應的核心的頁表項
  2. 如果deallocate被置位,函式掃描指向頁描述符的area->pages指標陣列,對於陣列的每一個元素,呼叫__free_page()函式釋放頁框到分割槽頁框分配器。此外,執行kfree(area->pages)來釋放陣列本身。
  3. 呼叫kfree(area)來釋放vm_struct描述符。
/*  vfree 和 vunmap 透過置位__vunmap函式的deallocate_pages的值來決定是否釋放pages中的頁框,
	當然一般是 vmalloc vfree,vmap vunmap成對使用
*/
void vfree(void *addr)
{
	BUG_ON(in_interrupt());
	__vunmap(addr, 1);
}
void vunmap(void *addr)
{
	BUG_ON(in_interrupt());
	__vunmap(addr, 0);
}



void __vunmap(void *addr, int deallocate_pages)
{
	struct vm_struct *area;

	if (!addr)
		return;
	
	// 檢查 address是否頁對齊
	if ((PAGE_SIZE-1) & (unsigned long)addr) {
		printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
		WARN_ON(1);
		return;
	}

	// 從全域性核心頁表目錄開始,逐級取消對映
	area = remove_vm_area(addr);
	if (unlikely(!area)) {
		printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
				addr);
		WARN_ON(1);
		return;
	}
	// 根據傳入的標誌決定是否釋放頁框,vfree釋放,vunmap不釋放頁框
	if (deallocate_pages) {
		int i;

		// 釋放歸還頁框
		for (i = 0; i < area->nr_pages; i++) {
			if (unlikely(!area->pages[i]))
				BUG();
			__free_page(area->pages[i]);
		}

		// 然後再釋放為頁框描述符申請的頁框描述符陣列
		if (area->nr_pages > PAGE_SIZE/sizeof(struct page *))
			vfree(area->pages);
		else
			kfree(area->pages);
	}

	kfree(area);
	return;
}

```c
struct vm_struct *remove_vm_area(void *addr)
{
	struct vm_struct **p, *tmp;

	write_lock(&vmlist_lock);
	for (p = &vmlist ; (tmp = *p) != NULL ;p = &tmp->next) {
		 if (tmp->addr == addr)
			 goto found;
	}
	write_unlock(&vmlist_lock);
	return NULL;

found:
	unmap_vm_area(tmp);
	*p = tmp->next;
	write_unlock(&vmlist_lock);
	return tmp;
}

相關文章