偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一文看懂Linux內(nèi)存分配,告別“內(nèi)存小白”!

系統(tǒng) Linux
htop命令是一個(gè)增強(qiáng)版的top命令,提供了更多的交互式功能和可視化顯示,允許我們查看進(jìn)程的內(nèi)存使用情況,以及各個(gè)進(jìn)程所占用的內(nèi)存大小 。這些工具和命令相互配合,能夠幫助我們?nèi)妗⑸钊氲亓私?Linux 系統(tǒng)的內(nèi)存分配情況,為系統(tǒng)的優(yōu)化和管理提供有力的支持 。

在 Linux 系統(tǒng)的復(fù)雜生態(tài)中,內(nèi)存管理堪稱是維持系統(tǒng)高效穩(wěn)定運(yùn)行的核心樞紐。想象一下,Linux 系統(tǒng)就像是一座繁忙的超級(jí)大都市,內(nèi)存則是城市里的土地資源,每一個(gè)運(yùn)行的程序、進(jìn)程都如同在這片土地上建造的建筑,它們都需要占用一定的內(nèi)存空間來(lái)存儲(chǔ)數(shù)據(jù)和執(zhí)行指令 。如果沒有一個(gè)科學(xué)合理的內(nèi)存管理機(jī)制,這座 “城市” 將會(huì)陷入混亂,出現(xiàn)內(nèi)存資源分配不均、內(nèi)存泄漏等問題,進(jìn)而導(dǎo)致程序運(yùn)行緩慢、系統(tǒng)響應(yīng)遲鈍,甚至引發(fā)系統(tǒng)崩潰。

比如,當(dāng)你在 Linux 系統(tǒng)上同時(shí)運(yùn)行多個(gè)大型程序時(shí),若內(nèi)存管理不佳,可能會(huì)使某些程序因得不到足夠的內(nèi)存而無(wú)法正常運(yùn)行,或者因?yàn)閮?nèi)存的不合理分配,導(dǎo)致部分內(nèi)存空間被浪費(fèi),造成資源的低效利用。所以,深入了解 Linux 內(nèi)存的常見分配方式,對(duì)于系統(tǒng)管理員、開發(fā)者以及追求系統(tǒng)高性能的用戶來(lái)說,就如同掌握了城市規(guī)劃的秘籍,能夠更好地調(diào)配內(nèi)存資源,讓 Linux 系統(tǒng)這座 “城市” 有序且高效地運(yùn)轉(zhuǎn),極大地激發(fā)讀者探索 Linux 內(nèi)存分配方式的興趣,為后續(xù)深入講解內(nèi)容做好鋪墊。

一、內(nèi)存分配三劍客

在 Linux 系統(tǒng)中,malloc、kmalloc、vmalloc 屬于不同層次的內(nèi)存分配函數(shù),它們看似相似,卻各懷絕技:一個(gè)服務(wù)用戶態(tài),一個(gè)專注內(nèi)核連續(xù)物理頁(yè),另一個(gè)則駕馭虛擬映射的離散空間。理解它們的差異與妙用,正是優(yōu)化性能、規(guī)避陷阱的關(guān)鍵一步。

1.1 malloc用戶空間內(nèi)存分配

malloc 函數(shù)是 C 標(biāo)準(zhǔn)庫(kù)中用于動(dòng)態(tài)內(nèi)存分配的函數(shù),其底層由 C 庫(kù)實(shí)現(xiàn)。C 庫(kù)維護(hù)著一個(gè)緩存,當(dāng)該緩存中的內(nèi)存足夠滿足分配需求時(shí),malloc 會(huì)直接從 C 庫(kù)緩存中分配內(nèi)存。而當(dāng) C 庫(kù)緩存中的內(nèi)存不足時(shí),malloc 則需借助系統(tǒng)調(diào)用與操作系統(tǒng)交互來(lái)獲取更多內(nèi)存。

在 Linux 系統(tǒng)中,這主要涉及 brk 和 mmap 這兩個(gè)系統(tǒng)調(diào)用。當(dāng)申請(qǐng)的內(nèi)存小于 128KB 時(shí),malloc 通常會(huì)通過系統(tǒng)調(diào)用 brk 向內(nèi)核申請(qǐng)內(nèi)存,具體來(lái)說是從堆空間申請(qǐng)一個(gè)虛擬內(nèi)存區(qū)域(VMA)。brk 系統(tǒng)調(diào)用通過移動(dòng)程序數(shù)據(jù)段的結(jié)束地址(即 “堆頂” 指針)來(lái)增加堆的大小,從而分配新的內(nèi)存。當(dāng)申請(qǐng)的內(nèi)存大于等于 128KB 時(shí),malloc 一般會(huì)使用 mmap 系統(tǒng)調(diào)用。mmap 系統(tǒng)調(diào)用是在進(jìn)程的虛擬地址空間中(堆和棧中間,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存來(lái)滿足內(nèi)存分配請(qǐng)求。

malloc實(shí)現(xiàn)步驟:

  1. 請(qǐng)求大小調(diào)整:首先,malloc 需要調(diào)整用戶請(qǐng)求的大小,以適應(yīng)內(nèi)部數(shù)據(jù)結(jié)構(gòu)(例如,可能需要存儲(chǔ)額外的元數(shù)據(jù))。通常,這包括對(duì)齊調(diào)整,確保分配的內(nèi)存地址滿足特定硬件要求(如對(duì)齊到8字節(jié)或16字節(jié)邊界)。
  2. 空閑鏈表搜索:接下來(lái),malloc 會(huì)在一個(gè)空閑內(nèi)存塊鏈表中搜索一個(gè)足夠大的空閑塊。這個(gè)鏈表通常由多個(gè)空閑塊組成,每個(gè)塊都記錄了大小和指向下一個(gè)塊的指針。
  3. 分裂或合并空閑塊:如果找到的空閑塊大小大于請(qǐng)求的大小,malloc 可能會(huì)將這個(gè)塊分裂成兩部分:一部分用于滿足當(dāng)前請(qǐng)求,另一部分保留在鏈表中以供未來(lái)使用。如果空閑塊正好等于請(qǐng)求的大小,則直接使用該塊。
  4. 更新元數(shù)據(jù):在使用選定的空閑塊之前,malloc 需要更新其元數(shù)據(jù)(如大小和下一個(gè)塊的指針),以反映內(nèi)存已經(jīng)被分配的事實(shí)。這可能涉及到修改當(dāng)前塊的大小字段或設(shè)置一個(gè)特殊的標(biāo)記來(lái)表示該塊已被占用。
  5. 返回指針:malloc 返回指向已分配內(nèi)存的指針給用戶。
typedef struct node {
    size_t size;       // 塊大小
    struct node* next; // 下一個(gè)節(jié)點(diǎn)指針
    struct node* prev; // 上一個(gè)節(jié)點(diǎn)指針(可選)
} Node;

Node* free_list = NULL; // 空閑鏈表頭指針

void* malloc(size_t size) {
    // 步驟1: 調(diào)整大?。ɡ缣砑釉獢?shù)據(jù)大小)
    size += sizeof(Node); // 為元數(shù)據(jù)留出空間
    // 步驟2: 搜索空閑鏈表
    Node* block = find_suitable_block(free_list, size);
    if (!block) {
        // 步驟3: 分裂或合并(如果需要)
        // 步驟4: 更新元數(shù)據(jù)和鏈表結(jié)構(gòu)
        block = allocate_new_block(size); // 可能需要擴(kuò)展堆或分裂現(xiàn)有塊
    } else {
        remove_from_list(block); // 從空閑鏈表中移除
    }
    // 步驟5: 設(shè)置元數(shù)據(jù)并返回指針(跳過Node頭)
    block->size = size; // 設(shè)置大小
    return (void*)((char*)block + sizeof(Node)); // 返回用戶數(shù)據(jù)的指針部分
}

⑴_(tái)do_sys_brk函數(shù)

經(jīng)過平臺(tái)相關(guān)實(shí)現(xiàn),malloc最終會(huì)調(diào)用SYSCALL_DEFINE1宏,擴(kuò)展為__do_sys_brk函數(shù):

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
	unsigned long retval;
	unsigned long newbrk, oldbrk, origbrk;
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *next;
	unsigned long min_brk;
	bool populate;
	bool downgraded = false;
	LIST_HEAD(uf);

	if (down_write_killable(&mm->mmap_sem))  ///申請(qǐng)寫類型讀寫信號(hào)量
		return -EINTR;

	origbrk = mm->brk;    ///brk記錄動(dòng)態(tài)分配區(qū)的當(dāng)前底部

#ifdef CONFIG_COMPAT_BRK
	/*
	 * CONFIG_COMPAT_BRK can still be overridden by setting
	 * randomize_va_space to 2, which will still cause mm->start_brk
	 * to be arbitrarily shifted
	 */
	if (current->brk_randomized)
		min_brk = mm->start_brk;
	else
		min_brk = mm->end_data;
#else
	min_brk = mm->start_brk;
#endif
	if (brk < min_brk)
		goto out;

	/*
	 * Check against rlimit here. If this check is done later after the test
	 * of oldbrk with newbrk then it can escape the test and let the data
	 * segment grow beyond its set limit the in case where the limit is
	 * not page aligned -Ram Gupta
	 */
	if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
			      mm->end_data, mm->start_data))
		goto out;

	newbrk = PAGE_ALIGN(brk);
	oldbrk = PAGE_ALIGN(mm->brk);
	if (oldbrk == newbrk) {
		mm->brk = brk;
		goto success;
	}

	/*
	 * Always allow shrinking brk.
	 * __do_munmap() may downgrade mmap_sem to read.
	 */
	if (brk <= mm->brk) {  ///請(qǐng)求釋放空間
		int ret;

		/*
		 * mm->brk must to be protected by write mmap_sem so update it
		 * before downgrading mmap_sem. When __do_munmap() fails,
		 * mm->brk will be restored from origbrk.
		 */
		mm->brk = brk;
		ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);
		if (ret < 0) {
			mm->brk = origbrk;
			goto out;
		} else if (ret == 1) {
			downgraded = true;
		}
		goto success;
	}

	/* Check against existing mmap mappings. */
	next = find_vma(mm, oldbrk);
	if (next && newbrk + PAGE_SIZE > vm_start_gap(next))   ///發(fā)現(xiàn)有重疊,不需要尋找
		goto out;

	/* Ok, looks good - let it rip. */
	if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0)  ///無(wú)重疊,新分配一個(gè)vma
		goto out;
	mm->brk = brk;   ///更新brk地址

success:
	populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
	if (downgraded)
		up_read(&mm->mmap_sem);
	else
		up_write(&mm->mmap_sem);
	userfaultfd_unmap_complete(mm, &uf);
	if (populate)  ///調(diào)用mlockall()系統(tǒng)調(diào)用,mm_populate會(huì)立刻分配物理內(nèi)存
		mm_populate(oldbrk, newbrk - oldbrk);
	return brk;

out:
	retval = origbrk;
	up_write(&mm->mmap_sem);
	return retval;
}

總結(jié)下_do_sys_brk()功能:

  • (1)從舊的brk邊界去查詢,是否有可用vma,若發(fā)現(xiàn)有重疊,直接使用;
  • (2)若無(wú)發(fā)現(xiàn)重疊,新分配一個(gè)vma;
  • (3)應(yīng)用程序若調(diào)用mlockall(),會(huì)鎖住進(jìn)程所有虛擬地址空間,防止內(nèi)存被交換出去,且立刻分配物理內(nèi)存;否則,物理頁(yè)面會(huì)等到使用時(shí),觸發(fā)缺頁(yè)異常分配;

⑵do_brk_flags函數(shù)

  • (1)尋找一個(gè)可使用的線性地址;
  • (2)查找最適合插入紅黑樹的節(jié)點(diǎn);
  • (3)尋到的線性地址是否可以合并現(xiàn)有vma,所不能,新建一個(gè)vma;
  • (4)將新建vma插入mmap鏈表和紅黑樹中
/*
 *  this is really a simplified "do_mmap".  it only handles
 *  anonymous maps.  eventually we may be able to do some
 *  brk-specific accounting here.
 */
static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long flags, struct list_head *uf)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma, *prev;
	struct rb_node **rb_link, *rb_parent;
	pgoff_t pgoff = addr >> PAGE_SHIFT;
	int error;
	unsigned long mapped_addr;

	/* Until we need other flags, refuse anything except VM_EXEC. */
	if ((flags & (~VM_EXEC)) != 0)
		return -EINVAL;
	flags |= VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;  ///默認(rèn)屬性,可讀寫

	mapped_addr = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED); ///返回未使用過的,未映射的線性地址區(qū)間的,起始地址
	if (IS_ERR_VALUE(mapped_addr))
		return mapped_addr;

	error = mlock_future_check(mm, mm->def_flags, len);
	if (error)
		return error;

	/* Clear old maps, set up prev, rb_link, rb_parent, and uf */
	if (munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf)) ///尋找適合插入的紅黑樹節(jié)點(diǎn)
		return -ENOMEM;

	/* Check against address space limits *after* clearing old maps... */
	if (!may_expand_vm(mm, flags, len >> PAGE_SHIFT))
		return -ENOMEM;

	if (mm->map_count > sysctl_max_map_count)
		return -ENOMEM;

	if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
		return -ENOMEM;

	/* Can we just expand an old private anonymous mapping? */  ///檢查是否能合并addr到附近的vma,若不能,只能新建一個(gè)vma
	vma = vma_merge(mm, prev, addr, addr + len, flags,
			NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
	if (vma)
		goto out;

	/*
	 * create a vma struct for an anonymous mapping
	 */
	vma = vm_area_alloc(mm);
	if (!vma) {
		vm_unacct_memory(len >> PAGE_SHIFT);
		return -ENOMEM;
	}

	vma_set_anonymous(vma);
	vma->vm_start = addr;
	vma->vm_end = addr + len;
	vma->vm_pgoff = pgoff;
	vma->vm_flags = flags;
	vma->vm_page_prot = vm_get_page_prot(flags);
	vma_link(mm, vma, prev, rb_link, rb_parent);  ///新vma添加到mmap鏈表和紅黑樹
out:
	perf_event_mmap(vma);
	mm->total_vm += len >> PAGE_SHIFT;
	mm->data_vm += len >> PAGE_SHIFT;
	if (flags & VM_LOCKED)
		mm->locked_vm += (len >> PAGE_SHIFT);
	vma->vm_flags |= VM_SOFTDIRTY;
	return 0;
}

mm_populate()函數(shù)

依次調(diào)用:

mm_populate()
	->__mm_populate()
	->populate_vma_page_range()
	->__get_user_pages()

當(dāng)設(shè)置VM_LOCKED標(biāo)志時(shí),表示要馬上申請(qǐng)物理頁(yè)面,并與vma建立映射;否則,這里不操作,直到訪問該vma時(shí),觸發(fā)缺頁(yè)異常,再分配物理頁(yè)面,并建立映射;

⑶get_user_pages()函數(shù)

static long __get_user_pages(struct mm_struct *mm,
		unsigned long start, unsigned long nr_pages,
		unsigned int gup_flags, struct page **pages,
		struct vm_area_struct **vmas, int *locked)
{
	long ret = 0, i = 0;
	struct vm_area_struct *vma = NULL;
	struct follow_page_context ctx = { NULL };

	if (!nr_pages)
		return 0;

	start = untagged_addr(start);

	VM_BUG_ON(!!pages != !!(gup_flags & (FOLL_GET | FOLL_PIN)));

	/*
	 * If FOLL_FORCE is set then do not force a full fault as the hinting
	 * fault information is unrelated to the reference behaviour of a task
	 * using the address space
	 */
	if (!(gup_flags & FOLL_FORCE))
		gup_flags |= FOLL_NUMA;

	do {  ///依次處理每個(gè)頁(yè)面
		struct page *page;
		unsigned int foll_flags = gup_flags;
		unsigned int page_increm;

		/* first iteration or cross vma bound */
		if (!vma || start >= vma->vm_end) {
			vma = find_extend_vma(mm, start);  ///檢查是否可以擴(kuò)增vma
			if (!vma && in_gate_area(mm, start)) {
				ret = get_gate_page(mm, start & PAGE_MASK,
						gup_flags, &vma,
						pages ? &pages[i] : NULL);
				if (ret)
					goto out;
				ctx.page_mask = 0;
				goto next_page;
			}

			if (!vma) {
				ret = -EFAULT;
				goto out;
			}
			ret = check_vma_flags(vma, gup_flags);
			if (ret)
				goto out;

			if (is_vm_hugetlb_page(vma)) {  ///支持巨頁(yè)
				i = follow_hugetlb_page(mm, vma, pages, vmas,
						&start, &nr_pages, i,
						gup_flags, locked);
				if (locked && *locked == 0) {
					/*
					 * We've got a VM_FAULT_RETRY
					 * and we've lost mmap_lock.
					 * We must stop here.
					 */
					BUG_ON(gup_flags & FOLL_NOWAIT);
					BUG_ON(ret != 0);
					goto out;
				}
				continue;
			}
		}
retry:
		/*
		 * If we have a pending SIGKILL, don't keep faulting pages and
		 * potentially allocating memory.
		 */
		if (fatal_signal_pending(current)) {  ///如果當(dāng)前進(jìn)程收到SIGKILL信號(hào),直接退出
			ret = -EINTR;
			goto out;
		}
		cond_resched();  //判斷是否需要調(diào)度,內(nèi)核中常用該函數(shù),優(yōu)化系統(tǒng)延遲

		page = follow_page_mask(vma, start, foll_flags, &ctx);  ///查看VMA的虛擬頁(yè)面是否已經(jīng)分配物理內(nèi)存,返回已經(jīng)映射的頁(yè)面的page
		if (!page) {
			ret = faultin_page(vma, start, &foll_flags, locked); ///若無(wú)映射,主動(dòng)觸發(fā)虛擬頁(yè)面到物理頁(yè)面的映射
			switch (ret) {
			case 0:
				goto retry;
			case -EBUSY:
				ret = 0;
				fallthrough;
			case -EFAULT:
			case -ENOMEM:
			case -EHWPOISON:
				goto out;
			case -ENOENT:
				goto next_page;
			}
			BUG();
		} else if (PTR_ERR(page) == -EEXIST) {
			/*
			 * Proper page table entry exists, but no corresponding
			 * struct page.
			 */
			goto next_page;
		} else if (IS_ERR(page)) {
			ret = PTR_ERR(page);
			goto out;
		}
		if (pages) {
			pages[i] = page;
			flush_anon_page(vma, page, start);  ///分配完物理頁(yè)面,刷新緩存
			flush_dcache_page(page);
			ctx.page_mask = 0;
		}
next_page:
		if (vmas) {
			vmas[i] = vma;
			ctx.page_mask = 0;
		}
		page_increm = 1 + (~(start >> PAGE_SHIFT) & ctx.page_mask);
		if (page_increm > nr_pages)
			page_increm = nr_pages;
		i += page_increm;
		start += page_increm * PAGE_SIZE;
		nr_pages -= page_increm;
	} while (nr_pages);
out:
	if (ctx.pgmap)
		put_dev_pagemap(ctx.pgmap);
	return i ? i : ret;
}

follow_page_mask函數(shù)返回已經(jīng)映射的頁(yè)面的page,最終會(huì)調(diào)用follow_page_pte函數(shù),其實(shí)現(xiàn)如下:

⑷f(wàn)ollow_page_pte函數(shù)

static struct page *follow_page_pte(struct vm_area_struct *vma,
		unsigned long address, pmd_t *pmd, unsigned int flags,
		struct dev_pagemap **pgmap)
{
	struct mm_struct *mm = vma->vm_mm;
	struct page *page;
	spinlock_t *ptl;
	pte_t *ptep, pte;
	int ret;

	/* FOLL_GET and FOLL_PIN are mutually exclusive. */
	if (WARN_ON_ONCE((flags & (FOLL_PIN | FOLL_GET)) ==
			 (FOLL_PIN | FOLL_GET)))
		return ERR_PTR(-EINVAL);
retry:
	if (unlikely(pmd_bad(*pmd)))
		return no_page_table(vma, flags);

	ptep = pte_offset_map_lock(mm, pmd, address, &ptl);  ///獲得pte和一個(gè)鎖
	pte = *ptep;
	if (!pte_present(pte)) {  ///處理頁(yè)面不在內(nèi)存中,作以下處理
		swp_entry_t entry;
		/*
		 * KSM's break_ksm() relies upon recognizing a ksm page
		 * even while it is being migrated, so for that case we
		 * need migration_entry_wait().
		 */
		if (likely(!(flags & FOLL_MIGRATION)))
			goto no_page;
		if (pte_none(pte))
			goto no_page;
		entry = pte_to_swp_entry(pte);
		if (!is_migration_entry(entry))
			goto no_page;
		pte_unmap_unlock(ptep, ptl);
		migration_entry_wait(mm, pmd, address);   ///等待頁(yè)面合并完成再嘗試
		goto retry;
	}
	if ((flags & FOLL_NUMA) && pte_protnone(pte))
		goto no_page;
	if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
		pte_unmap_unlock(ptep, ptl);
		return NULL;
	}

	page = vm_normal_page(vma, address, pte); ///根據(jù)pte,返回物理頁(yè)面page(只返回普通頁(yè)面,特殊頁(yè)面不參與內(nèi)存管理)
	if (!page && pte_devmap(pte) && (flags & (FOLL_GET | FOLL_PIN))) { ///處理設(shè)備映射文件
		/*
		 * Only return device mapping pages in the FOLL_GET or FOLL_PIN
		 * case since they are only valid while holding the pgmap
		 * reference.
		 */
		*pgmap = get_dev_pagemap(pte_pfn(pte), *pgmap);
		if (*pgmap)
			page = pte_page(pte);
		else
			goto no_page;
	} else if (unlikely(!page)) {   ///處理vm_normal_page()沒返回有效頁(yè)面情況
		if (flags & FOLL_DUMP) {
			/* Avoid special (like zero) pages in core dumps */
			page = ERR_PTR(-EFAULT);
			goto out;
		}

		if (is_zero_pfn(pte_pfn(pte))) {   ///系統(tǒng)零頁(yè),不會(huì)返回錯(cuò)誤
			page = pte_page(pte);
		} else {
			ret = follow_pfn_pte(vma, address, ptep, flags);
			page = ERR_PTR(ret);
			goto out;
		}
	}

	/* try_grab_page() does nothing unless FOLL_GET or FOLL_PIN is set. */
	if (unlikely(!try_grab_page(page, flags))) {
		page = ERR_PTR(-ENOMEM);
		goto out;
	}
	/*
	 * We need to make the page accessible if and only if we are going
	 * to access its content (the FOLL_PIN case).  Please see
	 * Documentation/core-api/pin_user_pages.rst for details.
	 */
	if (flags & FOLL_PIN) {
		ret = arch_make_page_accessible(page);
		if (ret) {
			unpin_user_page(page);
			page = ERR_PTR(ret);
			goto out;
		}
	}
	if (flags & FOLL_TOUCH) { ///FOLL_TOUCH, 標(biāo)記頁(yè)面可訪問
		if ((flags & FOLL_WRITE) &&
		    !pte_dirty(pte) && !PageDirty(page))
			set_page_dirty(page);
		/*
		 * pte_mkyoung() would be more correct here, but atomic care
		 * is needed to avoid losing the dirty bit: it is easier to use
		 * mark_page_accessed().
		 */
		mark_page_accessed(page);
	}
	if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
		/* Do not mlock pte-mapped THP */
		if (PageTransCompound(page))
			goto out;

		/*
		 * The preliminary mapping check is mainly to avoid the
		 * pointless overhead of lock_page on the ZERO_PAGE
		 * which might bounce very badly if there is contention.
		 *
		 * If the page is already locked, we don't need to
		 * handle it now - vmscan will handle it later if and
		 * when it attempts to reclaim the page.
		 */
		if (page->mapping && trylock_page(page)) {
			lru_add_drain();  /* push cached pages to LRU */
			/*
			 * Because we lock page here, and migration is
			 * blocked by the pte's page reference, and we
			 * know the page is still mapped, we don't even
			 * need to check for file-cache page truncation.
			 */
			mlock_vma_page(page);
			unlock_page(page);
		}
	}
out:
	pte_unmap_unlock(ptep, ptl);
	return page;
no_page:
	pte_unmap_unlock(ptep, ptl);
	if (!pte_none(pte))
		return NULL;
	return no_page_table(vma, flags);
}

(1)malloc函數(shù),從C庫(kù)緩存分配內(nèi)存,其分配或釋放內(nèi)存,未必馬上會(huì)執(zhí)行;

(2)malloc實(shí)際分配內(nèi)存動(dòng)作,要么主動(dòng)設(shè)置mlockall(),人為觸發(fā)缺頁(yè)異常,分配物理頁(yè)面;或者在訪問內(nèi)存時(shí)觸發(fā)缺頁(yè)異常,分配物理頁(yè)面;

(3)malloc分配虛擬內(nèi)存,有三種情況:

  1. malloc()分配內(nèi)存后,直接讀,linux內(nèi)核進(jìn)入缺頁(yè)異常,調(diào)用do_anonymous_page函數(shù)使用零頁(yè)映射,此時(shí)PTE屬性只讀;
  2. malloc()分配內(nèi)存后,先讀后寫,linux內(nèi)核第一次觸發(fā)缺頁(yè)異常,映射零頁(yè);第二次觸發(fā)異常,觸發(fā)寫時(shí)復(fù)制;
  3. c.malloc()分配內(nèi)存后, 直接寫,linux內(nèi)核進(jìn)入匿名頁(yè)面的缺頁(yè)異常,調(diào)alloc_zeroed_user_highpage_movable分配一個(gè)新頁(yè)面,這個(gè)PTE是可寫的;

1.2 kmalloc內(nèi)核空間常規(guī)內(nèi)存分配

一般來(lái)說內(nèi)核程序中對(duì)小于一頁(yè)的小塊內(nèi)存的請(qǐng)求會(huì)通過slab分配器提供的接口kmalloc來(lái)完成(雖然它可分配32到131072字節(jié)的內(nèi)存)。從內(nèi)核內(nèi)存分配角度講kmalloc可被看成是get_free_page(s)的一個(gè)有效補(bǔ)充,內(nèi)存分配粒度更靈活了。

kmalloc()函數(shù)類似與我們常見的malloc()函數(shù),前者用于內(nèi)核態(tài)的內(nèi)存分配,后者用于用戶態(tài);kmalloc()函數(shù)在物理內(nèi)存中分配一塊連續(xù)的存儲(chǔ)空間,且和malloc()函數(shù)一樣,不會(huì)清除里面的原始數(shù)據(jù),如果內(nèi)存充足,它的分配速度很快,其原型如下:

static inline void *kmalloc(size_t size, gfp_t flags);  /*返回的是虛擬地址*/
  • size:待分配的內(nèi)存大小。由于Linux內(nèi)存管理機(jī)制的原因,內(nèi)存只能按照頁(yè)面大?。ㄒ话?2位機(jī)為4KB,64位機(jī)為8KB)進(jìn)行分配,這樣就導(dǎo)致了當(dāng)我們僅需要幾個(gè)字節(jié)內(nèi)存時(shí),系統(tǒng)仍會(huì)返回一個(gè)頁(yè)面的內(nèi)存,顯然這是極度浪費(fèi)的。所以,不同于malloc的是,kmalloc的處理方式是:內(nèi)核先為其分配一系列不同大小(32B、64B、128B、… 、128KB)的內(nèi)存池,當(dāng)需要分配內(nèi)存時(shí),系統(tǒng)會(huì)分配大于等于所需內(nèi)存的最小一個(gè)內(nèi)存池給它。即kmalloc分配的內(nèi)存,最小為32字節(jié),最大為128KB。如果超過128KB,需要采樣其它內(nèi)存分配函數(shù),例如vmalloc()。
  • flag:該參數(shù)用于控制函數(shù)的行為,最常用的是GFP_KERNEL,表示當(dāng)當(dāng)前沒有足夠內(nèi)存分配時(shí),進(jìn)程進(jìn)入睡眠,待系統(tǒng)將緩沖區(qū)中的內(nèi)容SWAP到硬盤中后,獲得足夠內(nèi)存后再喚醒進(jìn)程,為其分配。

使用 GFP_ KERNEL 標(biāo)志申請(qǐng)內(nèi)存時(shí),若暫時(shí)不能滿足,則進(jìn)程會(huì)睡眠等待頁(yè),即會(huì)引起阻塞,因此不能在中斷上下文或持有自旋鎖的時(shí)候使用GFP_KERNE 申請(qǐng)內(nèi)存。所以,在中斷處理函數(shù)、tasklet 和內(nèi)核定時(shí)器等非進(jìn)程上下文中不能阻塞,此時(shí)驅(qū)動(dòng)應(yīng)當(dāng)使用 GFP_ATOMIC 標(biāo)志來(lái)申請(qǐng)內(nèi)存。當(dāng)使用 GFP_ATOMIC 標(biāo)志申請(qǐng)內(nèi)存時(shí),若不存在空閑頁(yè),則不等待,直接返回。

kmalloc函數(shù)

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
		unsigned int index;
#endif
		if (size > KMALLOC_MAX_CACHE_SIZE)
			return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
		index = kmalloc_index(size);  ///查找使用的哪個(gè)slab緩沖區(qū)

		if (!index)
			return ZERO_SIZE_PTR;

		return kmem_cache_alloc_trace(    ///從slab分配內(nèi)存
				kmalloc_caches[kmalloc_type(flags)][index],
				flags, size);
#endif
	}
	return __kmalloc(size, flags);
}

kmem_cache_alloc_trace分配函數(shù)

void *
kmem_cache_alloc_trace(struct kmem_cache *cachep, gfp_t flags, size_t size)
{
	void *ret;

	ret = slab_alloc(cachep, flags, size, _RET_IP_);  ///分配slab緩存

	ret = kasan_kmalloc(cachep, ret, size, flags);
	trace_kmalloc(_RET_IP_, ret,
		      size, cachep->size, flags);
	return ret;
}

可見,kmalloc()基于slab分配器實(shí)現(xiàn),因此分配的內(nèi)存,物理上都是連續(xù)的。

1.3 vmalloc內(nèi)核空間虛擬內(nèi)存分配

vmalloc()一般用在為只存在于軟件中(沒有對(duì)應(yīng)的硬件意義)的較大的順序緩沖區(qū)分配內(nèi)存,當(dāng)內(nèi)存沒有足夠大的連續(xù)物理空間可以分配時(shí),可以用該函數(shù)來(lái)分配虛擬地址連續(xù)但物理地址不連續(xù)的內(nèi)存。由于需要建立新的頁(yè)表,所以它的開銷要遠(yuǎn)遠(yuǎn)大于kmalloc及后面將要講到的__get_free_pages()函數(shù)。且vmalloc()不能用在原子上下文中,因?yàn)樗膬?nèi)部實(shí)現(xiàn)使用了標(biāo)志為 GFP_KERNEL 的kmalloc(),其函數(shù)原型如下:

void *vmalloc(unsigned long size);
void vfree(const void *addr);

使用 vmalloc 函數(shù)的一個(gè)例子函數(shù)是create_module()系統(tǒng)調(diào)用,它利用 vmalloc()函數(shù)來(lái)獲取被創(chuàng)建模塊需要的內(nèi)存空間。

內(nèi)存分配是一項(xiàng)要求嚴(yán)格的任務(wù),無(wú)論什么時(shí)候,都應(yīng)該對(duì)返回值進(jìn)行檢測(cè),在驅(qū)動(dòng)編程中可以使用copy_from_user()對(duì)內(nèi)存進(jìn)行使用。下面舉一個(gè)使用vmalloc函數(shù)的示例:

static int xxx(...)
{
  ...
  cpuid_entries = vmalloc(sizeof(struct kvm_cpuid_entry) * cpuid->nent);
  if(!cpuid_entries)
  goto out;
  if(copy_from_user(cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry)))
    goto out_free;
  for(i=0; i<cpuid->nent; i++){
    vcpuid->arch.cpuid_entries[i].eax = cpuid_entries[i].eax;
    ...
    vcpuid->arch.cpuid_entries[i].index = 0;
  }
  ...
out_free:
  vfree(cpuid_entries);
out:
  return r;
}

核心函數(shù)__vmalloc_node_range

static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
				 pgprot_t prot, unsigned int page_shift,
				 int node)
{
	const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
	unsigned long addr = (unsigned long)area->addr;
	unsigned long size = get_vm_area_size(area);   ///計(jì)算vm_struct包含多少個(gè)頁(yè)面
	unsigned long array_size;
	unsigned int nr_small_pages = size >> PAGE_SHIFT;
	unsigned int page_order;
	struct page **pages;
	unsigned int i;

	array_size = (unsigned long)nr_small_pages * sizeof(struct page *);
	gfp_mask |= __GFP_NOWARN;
	if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
		gfp_mask |= __GFP_HIGHMEM;

	/* Please note that the recursion is strictly bounded. */
	if (array_size > PAGE_SIZE) {
		pages = __vmalloc_node(array_size, 1, nested_gfp, node,
					area->caller);
	} else {
		pages = kmalloc_node(array_size, nested_gfp, node);
	}

	if (!pages) {
		free_vm_area(area);
		warn_alloc(gfp_mask, NULL,
			   "vmalloc size %lu allocation failure: "
			   "page array size %lu allocation failed",
			   nr_small_pages * PAGE_SIZE, array_size);
		return NULL;
	}

	area->pages = pages;  ///保存已分配頁(yè)面的page數(shù)據(jù)結(jié)構(gòu)的指針
	area->nr_pages = nr_small_pages;
	set_vm_area_page_order(area, page_shift - PAGE_SHIFT);

	page_order = vm_area_page_order(area);

	/*
	 * Careful, we allocate and map page_order pages, but tracking is done
	 * per PAGE_SIZE page so as to keep the vm_struct APIs independent of
	 * the physical/mapped size.
	 */
	for (i = 0; i < area->nr_pages; i += 1U << page_order) {
		struct page *page;
		int p;

		/* Compound pages required for remap_vmalloc_page */
		page = alloc_pages_node(node, gfp_mask | __GFP_COMP, page_order); ///分配物理頁(yè)面
		if (unlikely(!page)) {
			/* Successfully allocated i pages, free them in __vfree() */
			area->nr_pages = i;
			atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
			warn_alloc(gfp_mask, NULL,
				   "vmalloc size %lu allocation failure: "
				   "page order %u allocation failed",
				   area->nr_pages * PAGE_SIZE, page_order);
			goto fail;
		}

		for (p = 0; p < (1U << page_order); p++)
			area->pages[i + p] = page + p;

		if (gfpflags_allow_blocking(gfp_mask))
			cond_resched();
	}
	atomic_long_add(area->nr_pages, &nr_vmalloc_pages);

	if (vmap_pages_range(addr, addr + size, prot, pages, page_shift) < 0) { ///建立物理頁(yè)面到vma的映射
		warn_alloc(gfp_mask, NULL,
			   "vmalloc size %lu allocation failure: "
			   "failed to map pages",
			   area->nr_pages * PAGE_SIZE);
		goto fail;
	}

	return area->addr;

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

可見,vmalloc是臨時(shí)在vmalloc內(nèi)存區(qū)申請(qǐng)vma,并且分配物理頁(yè)面,建立映射;直接分配物理頁(yè)面,至少一個(gè)頁(yè)4K,因此vmalloc適合用于分配較大內(nèi)存,并且物理內(nèi)存不一定連續(xù);

二、mmap函數(shù)詳解最后

2.1mmap函數(shù)

mmap 即 memory map,也就是內(nèi)存映射。mmap 是一種內(nèi)存映射文件的方法,即將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)映關(guān)系。實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)的文件磁盤上,即完成了對(duì)文件的操作而不必再調(diào)用 read、write 等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。如下圖所示:

圖片圖片

mmap的作用,在應(yīng)用這一層,是讓你把文件的某一段,當(dāng)作內(nèi)存一樣來(lái)訪問。將文件映射到物理內(nèi)存,將進(jìn)程虛擬空間映射到那塊內(nèi)存。這樣,進(jìn)程不僅能像訪問內(nèi)存一樣讀寫文件,多個(gè)進(jìn)程映射同一文件,還能保證虛擬空間映射到同一塊物理內(nèi)存,達(dá)到內(nèi)存共享的作用。

mmap 是 Linux 中用處非常廣泛的一個(gè)系統(tǒng)調(diào)用,它將一個(gè)文件或者其它對(duì)象映射進(jìn)內(nèi)存。文件被映射到多個(gè)頁(yè)上,如果文件的大小不是所有頁(yè)的大小之和,最后一個(gè)頁(yè)不被使用的空間將會(huì)清零。mmap 必須以 PAGE_SIZE 為單位進(jìn)行映射,而內(nèi)存也只能以頁(yè)為單位進(jìn)行映射,若要映射非 PAGE_SIZE 整數(shù)倍的地址范圍,要先進(jìn)行內(nèi)存對(duì)齊,強(qiáng)行以 PAGE_SIZE 的倍數(shù)大小進(jìn)行映射。

其函數(shù)原型為:void *mmap (void start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void start, size_t length);。下面介紹一下內(nèi)存映射的步驟:

  • 用 open 系統(tǒng)調(diào)用打開文件,并返回描述符 fd。
  • 用 mmap 建立內(nèi)存映射,并返回映射首地址指針 start。
  • 對(duì)映射(文件)進(jìn)行各種操作,如顯示(printf)、修改(sprintf)等。
  • 用 munmap (void *start, size_t length) 關(guān)閉內(nèi)存映射。
  • 用 close 系統(tǒng)調(diào)用關(guān)閉文件 fd。

2.2mmap工作原理

mmap函數(shù)創(chuàng)建一個(gè)新的vm_area_struct結(jié)構(gòu),并將其與文件/設(shè)備的物理地址相連。

vm_area_struct:linux使用vm_area_struct來(lái)表示一個(gè)獨(dú)立的虛擬內(nèi)存區(qū)域,一個(gè)進(jìn)程可以使用多個(gè)vm_area_struct來(lái)表示不用類型的虛擬內(nèi)存區(qū)域(如堆,棧,代碼段,MMAP區(qū)域等)。

vm_area_struct結(jié)構(gòu)中包含了區(qū)域起始地址。同時(shí)也包含了一個(gè)vm_opt指針,其內(nèi)部可引出所有針對(duì)這個(gè)區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)。從而,進(jìn)程可以通過vm_area_struct獲取操作這段內(nèi)存區(qū)域所需的任何信息。

進(jìn)程通過vma操作內(nèi)存,而vma與文件/設(shè)備的物理地址相連,系統(tǒng)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)的文件磁盤上(或?qū)懭氲皆O(shè)備地址空間),實(shí)現(xiàn)內(nèi)存映射文件。

內(nèi)存映射文件的原理:

首先創(chuàng)建虛擬區(qū)間并完成地址映射,此時(shí)還沒有將任何文件數(shù)據(jù)拷貝至主存。當(dāng)進(jìn)程發(fā)起讀寫操作時(shí),會(huì)訪問虛擬地址空間,通過查詢頁(yè)表,發(fā)現(xiàn)這段地址不在物理頁(yè)上,因?yàn)橹唤⒘说刂酚成?,真正的?shù)據(jù)還沒有拷貝到內(nèi)存,因此引發(fā)缺頁(yè)異常。缺頁(yè)異常經(jīng)過一系列判斷,確定無(wú)非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過程。

最終會(huì)調(diào)用nopage函數(shù)把所缺的頁(yè)從文件在磁盤里的地址拷貝到物理內(nèi)存。之后進(jìn)程便可以對(duì)這片主存進(jìn)行讀寫,如果寫操作修改了內(nèi)容,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)的磁盤地址,完成了寫入到文件的過程。另外,也可以調(diào)用msync()來(lái)強(qiáng)制同步,這樣所寫的內(nèi)存就能立刻保存到文件中。

mmap內(nèi)存映射的實(shí)現(xiàn)過程,總的來(lái)說可以分為三個(gè)階段:

⑴進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域

  • 進(jìn)程在用戶空間調(diào)用庫(kù)函數(shù)mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  • 在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
  • 為此虛擬區(qū)分配一個(gè)vm_area_struct結(jié)構(gòu),接著對(duì)這個(gè)結(jié)構(gòu)的各個(gè)域進(jìn)行了初始化
  • 將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹中

⑵調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系

  • 為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對(duì)應(yīng)的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file),每個(gè)文件結(jié)構(gòu)體維護(hù)著和這個(gè)已打開文件相關(guān)各項(xiàng)信息。
  • 通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫(kù)函數(shù)。
  • 內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
  • 通過remap_pfn_range函數(shù)建立頁(yè)表,即實(shí)現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。此時(shí),這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中。

⑶進(jìn)程發(fā)起對(duì)這片映射空間的訪問,引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

注:前兩個(gè)階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r(shí)。

  • 進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁(yè)表,發(fā)現(xiàn)這一段地址并不在物理頁(yè)面上。因?yàn)槟壳爸唤⒘说刂酚成洌嬲挠脖P數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁(yè)異常。
  • 缺頁(yè)異常進(jìn)行一系列判斷,確定無(wú)非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過程。
  • 調(diào)頁(yè)過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁(yè),如果沒有則調(diào)用nopage函數(shù)把所缺的頁(yè)從磁盤裝入到主存中。
  • 之后進(jìn)程即可對(duì)這片主存進(jìn)行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)磁盤地址,也即完成了寫入到文件的過程。

注:修改過的臟頁(yè)面并不會(huì)立即更新回文件中,而是有一段時(shí)間的延遲,可以調(diào)用msync()來(lái)強(qiáng)制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。

2.3如何使用mmap技術(shù)

(1)mmap使用細(xì)節(jié)

使用mmap需要注意的一個(gè)關(guān)鍵點(diǎn)是,mmap映射區(qū)域大小必須是物理頁(yè)大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))。原因是,內(nèi)存的最小粒度是頁(yè),而進(jìn)程虛擬地址空間和內(nèi)存的映射也是以頁(yè)為單位。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁(yè)。

內(nèi)核可以跟蹤被內(nèi)存映射的底層對(duì)象(文件)的大小,進(jìn)程可以合法的訪問在當(dāng)前文件大小以內(nèi)又在內(nèi)存映射區(qū)以內(nèi)的那些字節(jié)。也就是說,如果文件的大小一直在擴(kuò)張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù),進(jìn)程都可以合法得到,這和映射建立時(shí)文件的大小無(wú)關(guān)。

映射建立之后,即使文件關(guān)閉,映射依然存在。因?yàn)橛成涞氖谴疟P的地址,不是文件本身,和文件句柄無(wú)關(guān)。同時(shí)可用于進(jìn)程間通信的有效地址空間不完全受限于被映射文件的大小,因?yàn)槭前错?yè)映射。

在上面的知識(shí)前提下,我們下面看看如果大小不是頁(yè)的整倍數(shù)的具體情況:

情形一:一個(gè)文件的大小是5000字節(jié),mmap函數(shù)從一個(gè)文件的起始位置開始,映射5000字節(jié)到虛擬內(nèi)存中。

分析:因?yàn)閱挝晃锢眄?yè)面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié),但是對(duì)應(yīng)到進(jìn)程虛擬地址區(qū)域的大小需要滿足整頁(yè)大小,因此mmap函數(shù)執(zhí)行后,實(shí)際映射到虛擬內(nèi)存區(qū)域8192個(gè) 字節(jié),5000~8191的字節(jié)部分用零填充。映射后的對(duì)應(yīng)關(guān)系如下圖所示:

圖片圖片

此時(shí):(1)讀/寫前5000個(gè)字節(jié)(0~4999),會(huì)返回操作文件內(nèi)容。(2)讀字節(jié)50008191時(shí),結(jié)果全為0。寫50008191時(shí),進(jìn)程不會(huì)報(bào)錯(cuò),但是所寫的內(nèi)容不會(huì)寫入原文件中 。(3)讀/寫8192以外的磁盤部分,會(huì)返回一個(gè)SIGSECV錯(cuò)誤。

情形二:一個(gè)文件的大小是5000字節(jié),mmap函數(shù)從一個(gè)文件的起始位置開始,映射15000字節(jié)到虛擬內(nèi)存中,即映射大小超過了原始文件的大小。

分析:由于文件的大小是5000字節(jié),和情形一一樣,其對(duì)應(yīng)的兩個(gè)物理頁(yè)。那么這兩個(gè)物理頁(yè)都是合法可以讀寫的,只是超出5000的部分不會(huì)體現(xiàn)在原文件中。由于程序要求映射15000字節(jié),而文件只占兩個(gè)物理頁(yè),因此8192字節(jié)~15000字節(jié)都不能讀寫,操作時(shí)會(huì)返回異常。如下圖所示:

圖片圖片

此時(shí):(1)進(jìn)程可以正常讀/寫被映射的前5000字節(jié)(0~4999),寫操作的改動(dòng)會(huì)在一定時(shí)間后反映在原文件中。(2)對(duì)于5000~8191字節(jié),進(jìn)程可以進(jìn)行讀寫過程,不會(huì)報(bào)錯(cuò)。但是內(nèi)容在寫入前均為0,另外,寫入后不會(huì)反映在文件中。(3)對(duì)于8192~14999字節(jié),進(jìn)程不能對(duì)其進(jìn)行讀寫,會(huì)報(bào)SIGBUS錯(cuò)誤。(4)對(duì)于15000以外的字節(jié),進(jìn)程不能對(duì)其讀寫,會(huì)引發(fā)SIGSEGV錯(cuò)誤。

情形三:一個(gè)文件初始大小為0,使用mmap操作映射了10004K的大小,即1000個(gè)物理頁(yè)大約4M字節(jié)空間,mmap返回指針ptr。

分析:如果在映射建立之初,就對(duì)文件進(jìn)行讀寫操作,由于文件大小為0,并沒有合法的物理頁(yè)對(duì)應(yīng),如同情形二一樣,會(huì)返回SIGBUS錯(cuò)誤。但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內(nèi)部的操作就是合法的。例如,文件擴(kuò)充4096字節(jié),ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴(kuò)充的范圍在1000個(gè)物理頁(yè)(映射范圍)內(nèi),ptr都可以對(duì)應(yīng)操作相同的大小。這樣,方便隨時(shí)擴(kuò)充文件空間,隨時(shí)寫入文件,不造成空間浪費(fèi)。

(2)函數(shù)定義及參數(shù)解釋

在 Linux 中,mmap 函數(shù)定義如下:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);。參數(shù)解釋如下:

  • addr:希望映射的起始地址,通常為 NULL,表示由內(nèi)核決定映射的地址。
  • length:映射區(qū)域的大小(以字節(jié)為單位)。
  • prot:映射區(qū)域的保護(hù)權(quán)限,決定映射的頁(yè)面是否可讀、可寫等。常見的權(quán)限選項(xiàng)包括:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執(zhí)行)、PROT_NONE(無(wú)權(quán)限)。
  • flags:映射的類型和行為控制。常見的標(biāo)志包括:MAP_SHARED(共享映射,對(duì)該內(nèi)存的修改會(huì)同步到文件)、MAP_PRIVATE(私有映射,對(duì)該內(nèi)存的修改不會(huì)影響原文件,寫時(shí)拷貝)、MAP_ANONYMOUS(匿名映射,不涉及文件,通常用于分配未初始化的內(nèi)存)。
  • fd:文件描述符,指向要映射的文件。如果使用匿名映射,應(yīng)將 fd 設(shè)置為 -1,并且需要設(shè)置 MAP_ANONYMOUS 標(biāo)志。
  • offset:文件映射的偏移量,必須是頁(yè)面大小的整數(shù)倍(通常為 4096 字節(jié))。

返回值:返回映射區(qū)域的起始地址,如果映射失敗,則返回 MAP_FAILED。

(3)mmap映射

在內(nèi)存映射的過程中,并沒有實(shí)際的數(shù)據(jù)拷貝,文件沒有被載入內(nèi)存,只是邏輯上被放入了內(nèi)存,具體到代碼,就是建立并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)(struct address_space),這個(gè)過程有系統(tǒng)調(diào)用mmap()實(shí)現(xiàn),所以建立內(nèi)存映射的效率很高。既然建立內(nèi)存映射沒有進(jìn)行實(shí)際的數(shù)據(jù)拷貝,那么進(jìn)程又怎么能最終直接通過內(nèi)存操作訪問到硬盤上的文件呢?

那就要看內(nèi)存映射之后的幾個(gè)相關(guān)的過程了。mmap()會(huì)返回一個(gè)指針ptr,它指向進(jìn)程邏輯地址空間中的一個(gè)地址,這樣以后,進(jìn)程無(wú)需再調(diào)用read或write對(duì)文件進(jìn)行讀寫,而只需要通過ptr就能夠操作文件。但是ptr所指向的是一個(gè)邏輯地址,要操作其中的數(shù)據(jù),必須通過MMU將邏輯地址轉(zhuǎn)換成物理地址,這個(gè)過程與內(nèi)存映射無(wú)關(guān)。

前面講過,建立內(nèi)存映射并沒有實(shí)際拷貝數(shù)據(jù),這時(shí),MMU在地址映射表中是無(wú)法找到與ptr相對(duì)應(yīng)的物理地址的,也就是MMU失敗,將產(chǎn)生一個(gè)缺頁(yè)中斷,缺頁(yè)中斷的中斷響應(yīng)函數(shù)會(huì)在swap中尋找相對(duì)應(yīng)的頁(yè)面,如果找不到(也就是該文件從來(lái)沒有被讀入內(nèi)存的情況),則會(huì)通過mmap()建立的映射關(guān)系,從硬盤上將文件讀取到物理內(nèi)存中,如圖1中過程3所示。這個(gè)過程與內(nèi)存映射無(wú)關(guān)。如果在拷貝數(shù)據(jù)時(shí),發(fā)現(xiàn)物理內(nèi)存不夠用,則會(huì)通過虛擬內(nèi)存機(jī)制(swap)將暫時(shí)不用的物理頁(yè)面交換到硬盤上,這個(gè)過程也與內(nèi)存映射無(wú)關(guān)。

mmap內(nèi)存映射的實(shí)現(xiàn)過程:

  • 進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
  • 調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
  • 進(jìn)程發(fā)起對(duì)這片映射空間的訪問,引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

適合的場(chǎng)景

  • 您有一個(gè)很大的文件,其內(nèi)容您想要隨機(jī)訪問一個(gè)或多個(gè)時(shí)間
  • 您有一個(gè)小文件,它的內(nèi)容您想要立即讀入內(nèi)存并經(jīng)常訪問。這種技術(shù)最適合那些大小不超過幾個(gè)虛擬內(nèi)存頁(yè)的文件。(頁(yè)是地址空間的最小單位,虛擬頁(yè)和物理頁(yè)的大小是一樣的,通常為4KB。)
  • 您需要在內(nèi)存中緩存文件的特定部分。文件映射消除了緩存數(shù)據(jù)的需要,這使得系統(tǒng)磁盤緩存中的其他數(shù)據(jù)空間更大 當(dāng)隨機(jī)訪問一個(gè)非常大的文件時(shí),通常最好只映射文件的一小部分。映射大文件的問題是文件會(huì)消耗活動(dòng)內(nèi)存。如果文件足夠大,系統(tǒng)可能會(huì)被迫將其他部分的內(nèi)存分頁(yè)以加載文件。將多個(gè)文件映射到內(nèi)存中會(huì)使這個(gè)問題更加復(fù)雜。

不適合的場(chǎng)景

  • 您希望從開始到結(jié)束的順序從頭到尾讀取一個(gè)文件
  • 這個(gè)文件有幾百兆字節(jié)或者更大。將大文件映射到內(nèi)存中會(huì)快速地填充內(nèi)存,并可能導(dǎo)致分頁(yè),這將抵消首先映射文件的好處。對(duì)于大型順序讀取操作,禁用磁盤緩存并將文件讀入一個(gè)小內(nèi)存緩沖區(qū)
  • 該文件大于可用的連續(xù)虛擬內(nèi)存地址空間。對(duì)于64位應(yīng)用程序來(lái)說,這不是什么問題,但是對(duì)于32位應(yīng)用程序來(lái)說,這是一個(gè)問題
  • 該文件位于可移動(dòng)驅(qū)動(dòng)器上
  • 該文件位于網(wǎng)絡(luò)驅(qū)動(dòng)器上

示例代碼

//
//  ViewController.m
//  TestCode
//
//  Created by zhangdasen on 2020/5/24.
//  Copyright ? 2020 zhangdasen. All rights reserved.
//

#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
    NSLog(@"path: %@", path);
    NSString *str = @"test str2";
    [str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];

    ProcessFile(path.UTF8String);
    NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"result:%@", result);
}


int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize)
{
    int outError;
    int fileDescriptor;
    struct stat statInfo;

    // Return safe values on error.
    outError = 0;
    *outDataPtr = NULL;
    *outDataLength = 0;

    // Open the file.
    fileDescriptor = open( inPathName, O_RDWR, 0 );
    if( fileDescriptor < 0 )
    {
        outError = errno;
    }
    else
    {
        // We now know the file exists. Retrieve the file size.
        if( fstat( fileDescriptor, &statInfo ) != 0 )
        {
            outError = errno;
        }
        else
        {
            ftruncate(fileDescriptor, statInfo.st_size + appendSize);
            fsync(fileDescriptor);
            *outDataPtr = mmap(NULL,
                               statInfo.st_size + appendSize,
                               PROT_READ|PROT_WRITE,
                               MAP_FILE|MAP_SHARED,
                               fileDescriptor,
                               0);
            if( *outDataPtr == MAP_FAILED )
            {
                outError = errno;
            }
            else
            {
                // On success, return the size of the mapped file.
                *outDataLength = statInfo.st_size;
            }
        }

        // Now close the file. The kernel doesn’t use our file descriptor.
        close( fileDescriptor );
    }

    return outError;
}


void ProcessFile(const char * inPathName)
{
    size_t dataLength;
    void * dataPtr;
    char *appendStr = " append_key2";
    int appendSize = (int)strlen(appendStr);
    if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
        dataPtr = dataPtr + dataLength;
        memcpy(dataPtr, appendStr, appendSize);
        // Unmap files
        munmap(dataPtr, appendSize + dataLength);
    }
}
@end

(5)解除映射的方法

使用 mmap 后,必須調(diào)用 munmap 來(lái)解除映射,釋放分配的虛擬內(nèi)存。其函數(shù)定義如下:int munmap(void *addr, size_t length);。

  • addr:要解除映射的內(nèi)存區(qū)域的起始地址。
  • length:要解除映射的大小。

返回值:成功返回 0,失敗返回 -1。

⑴利用 mmap 訪問硬件,減少數(shù)據(jù)拷貝次數(shù)

mmap 可以將文件、設(shè)備等外部資源映射到內(nèi)存地址空間,進(jìn)程可以像訪問內(nèi)存一樣訪問文件數(shù)據(jù)或硬件資源。當(dāng)使用 mmap 訪問硬件時(shí),數(shù)據(jù)可以直接從硬件設(shè)備通過 DMA 拷貝到內(nèi)核緩沖區(qū),然后進(jìn)程可以直接訪問這個(gè)緩沖區(qū),減少了數(shù)據(jù)拷貝的次數(shù)。

例如,在嵌入式系統(tǒng)中,可以使用 mmap 將物理地址映射到用戶虛擬地址空間,實(shí)現(xiàn)對(duì)硬件設(shè)備的直接訪問。在進(jìn)行數(shù)據(jù)傳輸時(shí),避免了傳統(tǒng)方式中從內(nèi)核空間到用戶空間的多次數(shù)據(jù)拷貝,提高了數(shù)據(jù)傳輸?shù)男省?/span>

通過 mmap 實(shí)現(xiàn)將物理地址映射到用戶虛擬地址空間:

  • 打開 /dev/mem 文件獲得文件描述符 dev_mem_fd。
  • 使用 mmap 函數(shù)進(jìn)行映射,將物理地址映射到用戶虛擬地址空間。例如,定義一個(gè)函數(shù) dma_mmap 來(lái)實(shí)現(xiàn)這個(gè)功能,函數(shù)原型為 int dma_mmap(unsigned long addr_p, unsigned int len, unsigned char** addr_v)。在這個(gè)函數(shù)中,首先打開 /dev/mem 文件,然后使用 mmap 函數(shù)進(jìn)行映射,最后返回虛擬地址。
  • 使用映射后的虛擬地址進(jìn)行操作,例如讀寫硬件設(shè)備。
  • 在使用完后,調(diào)用 dma_munmap 函數(shù)解除映射,釋放資源。函數(shù)原型為 unsigned int dma_munmap(unsigned char* addr_v, unsigned long addr_p, unsigned int len)。

在嵌入式系統(tǒng)中,還可以通過以下方式實(shí)現(xiàn)物理地址到用戶虛擬地址空間的映射:

  • 在驅(qū)動(dòng)程序中,實(shí)現(xiàn) mmap 方法,建立虛擬地址到物理地址的頁(yè)表。例如,可以使用 remap_pfn_range 函數(shù)一次建立所有頁(yè)表,或者使用 nopage VMA 方法每次建立一個(gè)頁(yè)表。
  • 在用戶空間程序中,使用 mmap 函數(shù)進(jìn)行映射,將文件描述符、映射大小、保護(hù)權(quán)限等參數(shù)傳入,獲得映射后的虛擬地址。然后可以通過這個(gè)虛擬地址對(duì)硬件設(shè)備進(jìn)行操作。

三、頁(yè)分配函數(shù)最后

該函數(shù)定義在頭文件/include/linux/gfp.h中,它既可以在內(nèi)核空間分配,也可以在用戶空間分配,它返回分配的第一個(gè)頁(yè)的描述符而非首地址,其原型為:

#define alloc_page(gfp_mask)  alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)//分配連續(xù)2^order個(gè)頁(yè)面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) 
{
  if(unlikely(order >= MAX_ORDER))
    return NULL;
  if(nid < 0)
    nid = numa_node_id();
  return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}

圖片圖片

根據(jù)返回頁(yè)面數(shù)目分類,分為僅返回單頁(yè)面的函數(shù)和返回多頁(yè)面的函數(shù)。

3.1alloc_page()和alloc_pages()函數(shù)

該函數(shù)定義在頭文件/include/linux/gfp.h中,它既可以在內(nèi)核空間分配,也可以在用戶空間分配,它返回分配的第一個(gè)頁(yè)的描述符而非首地址,其原型為:

#define alloc_page(gfp_mask)  alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)//分配連續(xù)2^order個(gè)頁(yè)面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) 
{
  if(unlikely(order >= MAX_ORDER))
    return NULL;
  if(nid < 0)
    nid = numa_node_id();
  return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}

3.2_get_free_pages()系列函數(shù)

它是kmalloc函數(shù)實(shí)現(xiàn)的基礎(chǔ),返回一個(gè)或多個(gè)頁(yè)面的虛擬地址。該系列函數(shù)/宏包括 get_zeroed_page()、_ _get_free_page()和_ _get_free_pages()。在使用時(shí),其申請(qǐng)標(biāo)志的值及含義與 kmalloc()完全一樣,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。

/*分配多個(gè)頁(yè)并返回分配內(nèi)存的首地址,分配的頁(yè)數(shù)為2^order,分配的頁(yè)不清零。
order 允許的最大值是 10(即 1024 頁(yè))或者 11(即 2048 頁(yè)),依賴于具體
的硬件平臺(tái)。*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
  struct page *page;
  page = alloc_pages(gfp_mask, order);
  if(!page)
    return 0;
  return (unsigned long)page_address(page);
}

#define __get_free_page(gfp_mask)  __get_free_pages(gfp_mask, 0)

/*該函數(shù)返回一個(gè)指向新頁(yè)的指針并且將該頁(yè)清零*/
unsigned long get_zeroed_page(unsigned int flags);

使用_ _get_free_pages()系列函數(shù)/宏申請(qǐng)的內(nèi)存應(yīng)使用free_page(addr)或free_pages(addr, order)函數(shù)釋放:

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

void free_pages(unsigned long addr, unsigned int order)
{
  if(addr != 0){
    VM_BUG_ON(!virt_addr_valid((void*)addr));
    __free_pages(virt_to_page((void *)addr), order);
  }
}

void __free_pages(struct page *page, unsigned int order)
{
  if(put_page_testzero(page)){
    if(order == 0)
      free_hot_page(page);
    else
      __free_pages_ok(page, order);
  }
}

free_pages()函數(shù)是調(diào)用__free_pages()函數(shù)完成內(nèi)存釋放的。

四、Buddy算法詳解最后

4.1Buddy算法

Buddy 算法堪稱 Linux 內(nèi)存管理的基石,主要用于管理 DMA(直接內(nèi)存存取)、常規(guī)、高端內(nèi)存這 3 個(gè)區(qū)域 。它的核心理念十分巧妙,將空閑的頁(yè)以 2 的 n 次方為單位進(jìn)行管理,這意味著 Linux 最底層的內(nèi)存申請(qǐng)都是以 2 的 n 次方為單位。其最大的優(yōu)勢(shì)在于能夠有效避免內(nèi)存的外部碎片。在傳統(tǒng)的內(nèi)存管理算法中,頻繁的內(nèi)存分配和釋放容易導(dǎo)致內(nèi)存空間被分割成許多小塊,這些小塊之間的空閑區(qū)域無(wú)法被有效利用,就形成了外部碎片。而 Buddy 算法通過將內(nèi)存頁(yè)按照 2 的冪次方大小進(jìn)行組織和管理,使得系統(tǒng)在分配和回收內(nèi)存時(shí),能夠更好地合并相鄰的空閑塊,從而避免了這種外部碎片的產(chǎn)生 。

例如,假設(shè) ZONE_NORMAL 區(qū)域有 16 頁(yè)內(nèi)存(即 2 的 4 次方),當(dāng)有程序申請(qǐng)一頁(yè)內(nèi)存時(shí),Buddy 算法會(huì)把剩下的 15 頁(yè)巧妙地拆分成 8 + 4 + 2 + 1,分別放到不同的鏈表中。此時(shí)若再有程序申請(qǐng) 4 頁(yè)內(nèi)存,系統(tǒng)可以直接從 4 頁(yè)的鏈表中分配;若再申請(qǐng) 4 頁(yè),就從 8 頁(yè)的鏈表中拿出 4 頁(yè)進(jìn)行分配,正好剩下 4 頁(yè)。其精髓就在于任何正整數(shù)都可以拆分成 2 的 n 次方之和。

然而,金無(wú)足赤,人無(wú)完人,Buddy 算法也有其局限性。長(zhǎng)期運(yùn)行后,系統(tǒng)中大片的連續(xù)內(nèi)存會(huì)逐漸減少,而 1 頁(yè)、2 頁(yè)、4 頁(yè)這種小內(nèi)存塊會(huì)越來(lái)越多。當(dāng)需要分配大片連續(xù)內(nèi)存時(shí),就可能會(huì)出現(xiàn)問題,也就是說它是以產(chǎn)生內(nèi)部碎片為代價(jià)來(lái)避免外部碎片的產(chǎn)生。所謂內(nèi)部碎片,就是系統(tǒng)已經(jīng)分配給用戶使用,但用戶自己沒有用到的那部分存儲(chǔ)空間。我們可以通過/proc/buddyinfo文件來(lái)查看內(nèi)存空閑的相關(guān)情況,以便及時(shí)了解系統(tǒng)內(nèi)存的分配狀態(tài),為系統(tǒng)優(yōu)化提供依據(jù) 。

4.2伙伴算法原理

Linux 便是采用這著名的伙伴系統(tǒng)算法來(lái)解決外部碎片的問題。把所有的空閑頁(yè)框分組為 11 塊鏈表,每一塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512 和 1024 個(gè)連續(xù)的頁(yè)框。對(duì)1024 個(gè)頁(yè)框的最大請(qǐng)求對(duì)應(yīng)著 4MB 大小的連續(xù)RAM 塊。每一塊的第一個(gè)頁(yè)框的物理地址是該塊大小的整數(shù)倍。例如,大小為 16個(gè)頁(yè)框的塊,其起始地址是 16 * 2^12 (2^12 = 4096,這是一個(gè)常規(guī)頁(yè)的大?。┑谋稊?shù)。

下面通過一個(gè)簡(jiǎn)單的例子來(lái)說明該算法的工作原理:

假設(shè)要請(qǐng)求一個(gè)256(129~256)個(gè)頁(yè)框的塊。算法先在256個(gè)頁(yè)框的鏈表中檢查是否有一個(gè)空閑塊。如果沒有這樣的塊,算法會(huì)查找下一個(gè)更大的頁(yè)塊,也就是,在512個(gè)頁(yè)框的鏈表中找一個(gè)空閑塊。如果存在這樣的塊,內(nèi)核就把512的頁(yè)框分成兩等分,一般用作滿足需求,另一半則插入到256個(gè)頁(yè)框的鏈表中。

如果在512個(gè)頁(yè)框的塊鏈表中也沒找到空閑塊,就繼續(xù)找更大的塊——1024個(gè)頁(yè)框的塊。如果這樣的塊存在,內(nèi)核就把1024個(gè)頁(yè)框塊的256個(gè)頁(yè)框用作請(qǐng)求,然后剩余的768個(gè)頁(yè)框中拿512個(gè)插入到512個(gè)頁(yè)框的鏈表中,再把最后的256個(gè)插入到256個(gè)頁(yè)框的鏈表中。如果1024個(gè)頁(yè)框的鏈表還是空的,算法就放棄并發(fā)出錯(cuò)誤信號(hào)。

⑴相關(guān)數(shù)據(jù)結(jié)構(gòu)

#define MAX_ORDER 11

struct zone {
  ……
	struct free_area	free_area[MAX_ORDER];
	……
} 
struct free_area {
	struct list_head	free_list;
	unsigned long		nr_free;//該組類別塊空閑的個(gè)數(shù)
};

Zone結(jié)構(gòu)體中的free_area數(shù)組的第k個(gè)元素,它保存了所有連續(xù)大小為2^k的空閑塊,具體是通過將連續(xù)頁(yè)的第一個(gè)頁(yè)插入到free_list中實(shí)現(xiàn)的,連續(xù)頁(yè)的第一個(gè)頁(yè)的頁(yè)描述符的private字段表明改部分連續(xù)頁(yè)屬于哪一個(gè)order鏈表。

⑵伙伴算法系統(tǒng)初始化

Linux內(nèi)核啟動(dòng)時(shí),伙伴算法還不可用,linux是通過bootmem來(lái)管理內(nèi)存,在mem_init中會(huì)把bootmem位圖中空閑的內(nèi)存塊插入到伙伴算法系統(tǒng)的free_list中。

調(diào)用流程如下:

mem_init----->__free_all_bootmem()—>free_all_bootmem()>free_all_bootmem_core(NODE_DATA(0))–>free_all_bootmem_core(pgdat)

//利用free_page 將頁(yè)面分給伙伴管理器
free_all_bootmem
    return(free_all_bootmem_core(NODE_DATA(0)));  //#define NODE_DATA(nid)		(&contig_page_data)
        bootmem_data_t *bdata = pgdat->bdata;
        page = virt_to_page(phys_to_virt(bdata->node_boot_start));
		idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
		map = bdata->node_bootmem_map;

		for (i = 0; i < idx; ) 
			unsigned long v = ~map[i / BITS_PER_LONG];
			//如果32個(gè)頁(yè)都是空閑的
			if (gofast && v == ~0UL)
				count += BITS_PER_LONG;
				__ClearPageReserved(page);
				order = ffs(BITS_PER_LONG) - 1;
				//設(shè)置32個(gè)頁(yè)的引用計(jì)數(shù)為1
				set_page_refs(page, order)

				//一次性釋放32個(gè)頁(yè)到空閑鏈表
				__free_pages(page, order);
									__free_pages_ok(page, order);
					list_add(&page->lru, &list);
					//page_zone定義如下return zone_table[page->flags >> NODEZONE_SHIFT];
					//接收一個(gè)頁(yè)描述符的地址作為它的參數(shù),它讀取頁(yè)描述符的flags字段的高位,并通過zone_table數(shù)組來(lái)確定相應(yīng)管理區(qū)描述符的地址,最終將頁(yè)框回收到對(duì)應(yīng)的管理區(qū)中
					free_pages_bulk(page_zone(page), 1, &list, order);
				i += BITS_PER_LONG;
				page += BITS_PER_LONG;

			//這32個(gè)頁(yè)中,只有部分是空閑的	
			else if (v)
				for (m = 1; m && i < idx; m<<=1, page++, i++)
					if (v & m)
						count++;
						__ClearPageReserved(page);
						set_page_refs(page, 0);
						//釋放單個(gè)頁(yè)
						__free_page(page);
			else
				i+=BITS_PER_LONG;
				page += BITS_PER_LONG;

		//釋放內(nèi)存分配位圖本身		
		page = virt_to_page(bdata->node_bootmem_map);
		for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++)
			__ClearPageReserved(page);
			set_page_count(page, 1);
			__free_page(page);

⑶伙伴算法系統(tǒng)分配空間

page = __rmqueue(zone, order);
	//從所請(qǐng)求的order開始,掃描每個(gè)可用塊鏈表進(jìn)行循環(huán)搜索。
    for (current_order = order; current_order < MAX_ORDER; ++current_order)
        area = zone->free_area + current_order;
        if (list_empty(&area->free_list))
            continue; 
        page = list_entry(area->free_list.next, struct page, lru);  
		//首先在空閑塊鏈表中刪除第一個(gè)頁(yè)框描述符。
        list_del(&page->lru);
		//清楚第一個(gè)頁(yè)框描述符的private字段,該字段表示連續(xù)頁(yè)框?qū)儆谀囊粋€(gè)大小的鏈表
        rmv_page_order(page);
        area->nr_free--;
        zone->free_pages -= 1UL << order;
		//如果是從更大的order鏈表中申請(qǐng)的,則剩下的要重新插入到鏈表中
        return expand(zone, page, order, current_order, area);
            unsigned long size = 1 << high;
            while (high > low)
                area--;
                high--;
                size >>= 1; 
				//該部分連續(xù)頁(yè)面插入到對(duì)應(yīng)的free_list中
                list_add(&page[size].lru, &area->free_list); 
                area->nr_free++;
				//設(shè)置該部分連續(xù)頁(yè)面的order
                set_page_order(&page[size], high);
                    page->private = order;
                    __SetPagePrivate(page);
                         __set_bit(PG_private, &(page)->flags)
            return page;

⑷伙伴算法系統(tǒng)回收空間

free_pages_bulk
	//linux內(nèi)核將空間分為三個(gè)區(qū),分別是ZONE_DMA、ZONE_NORMAL、ZONR_HIGH,zone_mem_map字段就是指向該區(qū)域第一個(gè)頁(yè)描述符
    struct page *base = zone->zone_mem_map;
    while (!list_empty(list) && count--)  
        page = list_entry(list->prev, struct page, lru);
        list_del(&page->lru);
        __free_pages_bulk
            int order_size = 1 << order;
			//該段空間的第一個(gè)頁(yè)的下標(biāo)
            page_idx = page - base; 
            zone->free_pages += order_size;
			//最多循環(huán)10 - order次。每次都將一個(gè)塊和它的伙伴進(jìn)行合并。
            while (order < MAX_ORDER-1)
				//尋找伙伴,如果page_idx=128,order=4,則buddy_idx=144
                buddy_idx = (page_idx ^ (1 << order)); 
                buddy = base + buddy_idx;
                /**
                 * 判斷伙伴塊是否是大小為order的空閑頁(yè)框的第一個(gè)頁(yè)。
                 * 首先,伙伴的第一個(gè)頁(yè)必須是空閑的(_count == -1)
                 * 同時(shí),必須屬于動(dòng)態(tài)內(nèi)存(PG_reserved被清0,PG_reserved為1表示留給內(nèi)核或者沒有使用)
                 * 最后,其private字段必須是order
                 */
                if (!page_is_buddy(buddy, order))
                    break;
                list_del(&buddy->lru);
                area = zone->free_area + order;
				//原先所在的區(qū)域空閑頁(yè)減少
                area->nr_free--;   
                rmv_page_order(buddy);
                    __ClearPagePrivate(page);
                    page->private = 0;
                page_idx &= buddy_idx;
                order++;

			/**
			 * 伙伴不能與當(dāng)前塊合并。
			 * 將塊插入適當(dāng)?shù)逆湵恚⒁詨K大小的order更新第一個(gè)頁(yè)框的private字段。
			 */
            coalesced = base + page_idx;
            set_page_order(coalesced, order);
            list_add(&coalesced->lru, &zone->free_area[order].free_list);
            zone->free_area[order].nr_free++;

4.3分配過程

現(xiàn)內(nèi)存總?cè)萘繛?6KB,用戶請(qǐng)求分配4KB大小的內(nèi)存空間,且規(guī)定最小的內(nèi)存分配單元是2KB。于是位圖分為8個(gè)區(qū)域,用1表示已分配,用0表示未分配,則初始位圖和空閑鏈表如圖所示。從上到下依次是位圖、內(nèi)存塊、空閑鏈表。

圖片圖片

由于需要分配4KB內(nèi)存,數(shù)顯到鏈表中4KB位置進(jìn)行查看,發(fā)現(xiàn)為空,于是繼續(xù)向后查找8KB位置,發(fā)現(xiàn)仍為空,直到到達(dá)鏈表尾16KB位置不為空。16KB塊分裂成兩個(gè)8KB的塊,其中一塊插入到鏈表相應(yīng)位置,另一塊繼續(xù)分裂成兩個(gè)4KB的塊,其中一個(gè)交付使用,另一個(gè)插入到鏈表中,結(jié)果如下圖所示:

圖片圖片

內(nèi)存回收是內(nèi)存分配的逆過程,假設(shè)以上存儲(chǔ)要釋放4KB內(nèi)存,首先到鏈表中4KB位置查看是否有它的“伙伴”,發(fā)現(xiàn)該位置不為空,于是合并成一個(gè)8KB的塊,繼續(xù)尋找它的“伙伴”,然后合并成一個(gè)16KB的塊,插入鏈表中。若在查找過程中沒有發(fā)現(xiàn)“伙伴”,則直接插入到鏈表中,然后將位圖中的標(biāo)記清零,表示內(nèi)存可用。

當(dāng)程序請(qǐng)求內(nèi)存時(shí),Linux 內(nèi)核內(nèi)存伙伴算法會(huì)按照以下步驟進(jìn)行分配:

首先從空閑的內(nèi)存中搜索比申請(qǐng)的內(nèi)存大的最小的內(nèi)存塊。例如,如果程序請(qǐng)求一個(gè)大小為特定值(假設(shè)為 N)的內(nèi)存塊,伙伴算法會(huì)先在與 N 大小相同的塊鏈表中查找空閑塊。如果沒有找到合適的塊,就會(huì)去更大的塊鏈表中繼續(xù)查找。

如果這樣的內(nèi)存塊存在,則將這塊內(nèi)存標(biāo)記為 “已用”,同時(shí)將該內(nèi)存分配給應(yīng)用程序。比如,假設(shè)程序請(qǐng)求的內(nèi)存大小處于某個(gè)特定范圍(比如 129 - 256 個(gè)頁(yè)框),算法會(huì)先在 256 個(gè)頁(yè)框的鏈表中檢查是否有空閑塊。若存在這樣的塊,就可以直接分配給程序使用。

如果這樣的內(nèi)存不存在,則操作系統(tǒng)將尋找更大塊的空閑內(nèi)存,然后將這塊內(nèi)存平分成兩部分,一部分返回給程序使用,另一部分作為空閑的內(nèi)存塊等待下一次被分配。例如,若在特定大小的鏈表中(如 256 個(gè)頁(yè)框的鏈表)沒有找到空閑塊,算法會(huì)查找下一個(gè)更大的頁(yè)塊,如在 512 個(gè)頁(yè)框的鏈表中找一個(gè)空閑塊。如果存在這樣的塊,內(nèi)核就把 512 的頁(yè)框分成兩等分,一半用作滿足需求,另一半則插入到 256 個(gè)頁(yè)框的鏈表中。如果在 512 個(gè)頁(yè)框的塊鏈表中也沒找到空閑塊,就繼續(xù)找更大的塊,如 1024 個(gè)頁(yè)框的塊。如果這樣的塊存在,內(nèi)核就把 1024 個(gè)頁(yè)框塊的一部分(滿足程序需求的大?。┯米髡?qǐng)求,然后將剩余部分按照大小插入到相應(yīng)的鏈表中等待后續(xù)分配。

4.4釋放過程

當(dāng)程序釋放內(nèi)存時(shí),Linux 內(nèi)核內(nèi)存伙伴算法會(huì)執(zhí)行以下操作:

操作系統(tǒng)首先將該內(nèi)存回收,然后檢查與該內(nèi)存相鄰的內(nèi)存是否是同樣大小并且同樣處于空閑的狀態(tài)。例如,當(dāng)釋放一個(gè)特定大小的內(nèi)存塊(假設(shè)為 256 個(gè)頁(yè)框的塊)時(shí),算法就把其插入到 256 個(gè)頁(yè)框的鏈表中,然后檢查與該內(nèi)存相鄰的內(nèi)存。

如果是,則將這兩塊內(nèi)存合并,然后程序遞歸進(jìn)行同樣的檢查,試圖合并更大的塊。如果存在同樣大小為 256 個(gè)頁(yè)框的并且空閑的內(nèi)存,就將這兩塊內(nèi)存合并成 512 個(gè)頁(yè)框,然后插入到 512 個(gè)頁(yè)框的鏈表中。如果合并后的 512 個(gè)頁(yè)框的內(nèi)存存在大小為 512 個(gè)頁(yè)框的相鄰且空閑的內(nèi)存,則將兩者合并,然后插入到 1024 個(gè)頁(yè)框的鏈表中。如果不存在合適的伙伴塊,就直接把要釋放的塊掛入鏈表頭等待后續(xù)可能的合并操作。

五、CMA算法

在應(yīng)用程序中,我們申請(qǐng)的內(nèi)存從虛擬地址角度看是連續(xù)的,因?yàn)樘摂M地址本身具有連續(xù)性。但在實(shí)際的物理內(nèi)存空間中,所申請(qǐng)的這片內(nèi)存未必是連續(xù)的。不過對(duì)于有內(nèi)存管理單元(MMU)的系統(tǒng)來(lái)說,這并不會(huì)影響應(yīng)用程序的正常運(yùn)行,因?yàn)?MMU 可以將物理地址映射成虛擬地址,應(yīng)用程序無(wú)需關(guān)心實(shí)際的內(nèi)存情況。

但如果是沒有 MMU 的情況呢?當(dāng)設(shè)備需要通過 DMA 直接訪問內(nèi)存時(shí),就迫切需要一片連續(xù)的內(nèi)存空間,而 CMA 機(jī)制正是為了解決這一棘手問題而誕生的。DMA zone 區(qū)域并非 DMA 設(shè)備專屬,其他程序也可以申請(qǐng)?jiān)搮^(qū)域的內(nèi)存。當(dāng)設(shè)備要申請(qǐng) DMA zone 空間中的一大片連續(xù)內(nèi)存時(shí),如果此時(shí)已經(jīng)沒有連續(xù)的大片內(nèi)存,只有 1 頁(yè)、2 頁(yè)、4 頁(yè)等連續(xù)的小內(nèi)存,CMA 機(jī)制就會(huì)發(fā)揮作用。

系統(tǒng)會(huì)事先標(biāo)記某一片連續(xù)區(qū)域?yàn)?CMA 區(qū)域,在沒有大片連續(xù)內(nèi)存申請(qǐng)時(shí),這片區(qū)域只給可移動(dòng)(moveable)的程序使用。當(dāng)有大片連續(xù)內(nèi)存請(qǐng)求到來(lái)時(shí),系統(tǒng)會(huì)前往這片 CMA 區(qū)域,將所有可移動(dòng)的小片內(nèi)存移動(dòng)到其它的非 CMA 區(qū)域,并更改對(duì)應(yīng)的程序頁(yè)表,然后把空出來(lái)的 CMA 區(qū)域分配給設(shè)備,從而完美實(shí)現(xiàn)了 DMA 大片連續(xù)內(nèi)存的分配。CMA 機(jī)制并非孤立存在,它通常是為 DMA 設(shè)備服務(wù)的。當(dāng)設(shè)備調(diào)用dma_alloc_coherent函數(shù)申請(qǐng)內(nèi)存后,為了確保得到一片連續(xù)的內(nèi)存,CMA 機(jī)制就會(huì)被觸發(fā)調(diào)用,以保證申請(qǐng)內(nèi)存的連續(xù)性。此外,CMA 區(qū)域通常被分配在高端內(nèi)存,以滿足特定的內(nèi)存需求場(chǎng)景 。

六、slab算法

當(dāng)在驅(qū)動(dòng)程序中,遇到反復(fù)分配、釋放同一大小的內(nèi)存塊時(shí)(例如,inode、task_struct等),建議使用內(nèi)存池技術(shù)(對(duì)象在前后兩次被使用時(shí)均分配在同一塊內(nèi)存或同一類內(nèi)存空間,且保留了基本的數(shù)據(jù)結(jié)構(gòu),這大大提高了效率)。在linux中,有一個(gè)叫做slab分配器的內(nèi)存池管理技術(shù),內(nèi)存池使用的內(nèi)存區(qū)叫做后備高速緩存。

salb相關(guān)頭文件在linux/slab.h中,在使用后備高速緩存前,需要?jiǎng)?chuàng)建一個(gè)kmem_cache的結(jié)構(gòu)體。

6.1創(chuàng)建slab緩存區(qū)

該函數(shù)創(chuàng)建一個(gè)slab緩存(后備高速緩沖區(qū)),它是一個(gè)可以駐留任意數(shù)目全部同樣大小的后備緩存。其原型如下:

struct kmem_cache *kmem_cache_create(const char *name, size_t size, \
                   size_t align, unsigned long flags,\
                   void (*ctor)(void *, struct kmem_cache *, unsigned long),\
                   void (*dtor)(void *, struct kmem_cache *, unsigned ong)));

其中:

  • name:創(chuàng)建的緩存名;
  • size:可容納的緩存塊個(gè)數(shù);
  • align:后備高速緩沖區(qū)中第一個(gè)內(nèi)存塊的偏移量(一般置為0);
  • flags:控制如何進(jìn)行分配的位掩碼,包括 SLAB_NO_REAP(即使內(nèi)存緊缺也不自動(dòng)收縮這塊緩存)、SLAB_HWCACHE_ALIGN ( 每 個(gè) 數(shù) 據(jù) 對(duì) 象 被 對(duì) 齊 到 一 個(gè) 緩 存 行 )、SLAB_CACHE_DMA(要求數(shù)據(jù)對(duì)象在 DMA 內(nèi)存區(qū)分配)等);
  • ctor:是可選的內(nèi)存塊對(duì)象構(gòu)造函數(shù)(初始化函數(shù));
  • dtor:是可選的內(nèi)存對(duì)象塊析構(gòu)函數(shù)(釋放函數(shù))。

6.2分配slab緩存函數(shù)

一旦創(chuàng)建完后備高速緩沖區(qū)后,就可以調(diào)用kmem_cache_alloc()在緩存區(qū)分配一個(gè)內(nèi)存塊對(duì)象了,其原型如下:

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

cachep指向開始分配的后備高速緩存,flags與傳給kmalloc函數(shù)的參數(shù)相同,一般為GFP_KERNEL。

6.3釋放slab緩存

該函數(shù)釋放一個(gè)內(nèi)存塊對(duì)象:

void *kmem_cache_free(struct kmem_cache *cachep, void *objp);

6.4銷毀slab緩存

與kmem_cache_create對(duì)應(yīng)的是銷毀函數(shù),釋放一個(gè)后備高速緩存:

int kmem_cache_destroy(struct kmem_cache *cachep);

它必須等待所有已經(jīng)分配的內(nèi)存塊對(duì)象被釋放后才能釋放后備高速緩存區(qū)。

6.5slab緩存使用舉例

創(chuàng)建一個(gè)存放線程結(jié)構(gòu)體(struct thread_info)的后備高速緩存,因?yàn)樵趌inux中涉及頻繁的線程創(chuàng)建與釋放,如果使用__get_free_page()函數(shù)會(huì)造成內(nèi)存的大量浪費(fèi),效率也不高。所以在linux內(nèi)核的初始化階段就創(chuàng)建了一個(gè)名為thread_info的后備高速緩存,代碼如下:

/* 創(chuàng)建slab緩存 */
static struct kmem_cache *thread_info_cache;
thread_info_cache = kmem_cache_create("thread_info", sizeof(struct thread_info), \
                    SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);

/* 分配slab緩存 */
struct thread_info *ti;
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL);

/* 使用slab緩存 */
...
/* 釋放slab緩存 */
kmem_cache_free(thread_info_cache, ti);
kmem_cache_destroy(thread_info_cache);

七、內(nèi)存池

在 Linux 內(nèi)核中還包含對(duì)內(nèi)存池的支持,內(nèi)存池技術(shù)也是一種非常經(jīng)典的用于分配大量小對(duì)象的后備緩存技術(shù)。

7.1創(chuàng)建內(nèi)存池

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, \
              mempool_free_t *free_fn, void *pool_data);

mempool_create()函數(shù)用于創(chuàng)建一個(gè)內(nèi)存池,min_nr 參數(shù)是需要預(yù)分配對(duì)象的數(shù)目,alloc_fn 和 free_fn 是指向內(nèi)存池機(jī)制提供的標(biāo)準(zhǔn)對(duì)象分配和回收函數(shù)的指針,其原型分別為:

typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data); 
typedef void (mempool_free_t)(void *element, void *pool_data);

pool_data 是分配和回收函數(shù)用到的指針,gfp_mask 是分配標(biāo)記。只有當(dāng)_ _GFP_WAIT 標(biāo)記被指定時(shí),分配函數(shù)才會(huì)休眠。

7.2分配和回收對(duì)象

在內(nèi)存池中分配和回收對(duì)象需由以下函數(shù)來(lái)完成:

void *mempool_alloc(mempool_t *pool, int gfp_mask); 
void mempool_free(void *element, mempool_t *pool);

mempool_alloc()用來(lái)分配對(duì)象,如果內(nèi)存池分配器無(wú)法提供內(nèi)存,那么就可以用預(yù)分配的池。

7.3銷毀內(nèi)存池

void mempool_destroy(mempool_t *pool);

mempool_create()函數(shù)創(chuàng)建的內(nèi)存池需由 mempool_destroy()來(lái)回收。

八、應(yīng)用案例分析

8.1實(shí)際場(chǎng)景中的應(yīng)用

在服務(wù)器運(yùn)維領(lǐng)域,內(nèi)存分配方式的選擇對(duì)系統(tǒng)性能有著顯著影響。以運(yùn)行數(shù)據(jù)庫(kù)服務(wù)的服務(wù)器為例,數(shù)據(jù)庫(kù)系統(tǒng)需要頻繁地讀寫數(shù)據(jù),對(duì)內(nèi)存的需求十分龐大且復(fù)雜。在這種場(chǎng)景下,Buddy 算法就發(fā)揮著重要作用,它能夠?yàn)閿?shù)據(jù)庫(kù)分配連續(xù)的大塊內(nèi)存,確保數(shù)據(jù)的快速存儲(chǔ)和讀取。同時(shí),slab 算法也不可或缺,數(shù)據(jù)庫(kù)中存在許多頻繁使用的小數(shù)據(jù)結(jié)構(gòu),如數(shù)據(jù)庫(kù)連接池中的連接對(duì)象、緩存中的數(shù)據(jù)塊描述符等,slab 算法能夠高效地管理這些小內(nèi)存塊的分配和回收,大大提高了數(shù)據(jù)庫(kù)系統(tǒng)的運(yùn)行效率。

在嵌入式開發(fā)場(chǎng)景中,CMA 算法的應(yīng)用則尤為關(guān)鍵。比如在開發(fā)一款基于 Linux 系統(tǒng)的智能攝像頭設(shè)備時(shí),攝像頭需要通過 DMA 將拍攝的圖像數(shù)據(jù)快速傳輸?shù)絻?nèi)存中進(jìn)行處理。由于圖像數(shù)據(jù)量較大,需要連續(xù)的內(nèi)存空間來(lái)保證數(shù)據(jù)傳輸?shù)姆€(wěn)定性和高效性。此時(shí),CMA 算法就可以提前預(yù)留出一片連續(xù)的內(nèi)存區(qū)域作為 CMA 區(qū)域,當(dāng)攝像頭設(shè)備需要申請(qǐng)內(nèi)存時(shí),系統(tǒng)能夠及時(shí)從 CMA 區(qū)域中分配出連續(xù)的內(nèi)存,滿足攝像頭對(duì) DMA 連續(xù)內(nèi)存的需求,確保圖像數(shù)據(jù)的正常傳輸和處理,為智能攝像頭的穩(wěn)定運(yùn)行提供了有力保障 。

8.2遇到的問題及解決辦法

在實(shí)際使用過程中,內(nèi)存分配可能會(huì)引發(fā)各種問題。例如,在一個(gè)長(zhǎng)時(shí)間運(yùn)行的服務(wù)器應(yīng)用中,由于頻繁地進(jìn)行內(nèi)存分配和釋放操作,可能會(huì)導(dǎo)致內(nèi)存碎片過多。當(dāng)內(nèi)存碎片達(dá)到一定程度時(shí),即使系統(tǒng)中存在足夠的空閑內(nèi)存,也可能無(wú)法分配出連續(xù)的大塊內(nèi)存,從而導(dǎo)致某些需要大塊連續(xù)內(nèi)存的操作失敗,如數(shù)據(jù)庫(kù)的大規(guī)模數(shù)據(jù)加載。為了解決這個(gè)問題,可以采用內(nèi)存整理工具,如defragment命令(需 root 權(quán)限運(yùn)行),對(duì)內(nèi)存進(jìn)行碎片整理,使內(nèi)存空間變得更加連續(xù),提高內(nèi)存的利用率 。

再比如,在嵌入式設(shè)備開發(fā)中,可能會(huì)遇到內(nèi)存分配失敗的情況。這可能是由于設(shè)備內(nèi)存資源有限,而應(yīng)用程序?qū)?nèi)存的需求過大導(dǎo)致的。當(dāng)這種情況發(fā)生時(shí),可以通過優(yōu)化應(yīng)用程序的內(nèi)存使用方式來(lái)解決。例如,采用內(nèi)存池技術(shù),預(yù)先分配一塊連續(xù)的內(nèi)存空間,在需要時(shí)從內(nèi)存池中分配內(nèi)存,避免頻繁地進(jìn)行系統(tǒng)級(jí)的內(nèi)存分配和釋放操作,從而減少內(nèi)存碎片的產(chǎn)生,提高內(nèi)存分配的成功率。同時(shí),合理調(diào)整應(yīng)用程序的功能和數(shù)據(jù)結(jié)構(gòu),減少不必要的內(nèi)存占用,也是解決內(nèi)存分配失敗問題的有效途徑 。

8.3教你如何查看內(nèi)存分配情況

了解了 Linux 內(nèi)存的常見分配方式后,學(xué)會(huì)如何查看內(nèi)存分配情況也是非常重要的,這有助于我們實(shí)時(shí)監(jiān)控系統(tǒng)內(nèi)存的使用狀態(tài),及時(shí)發(fā)現(xiàn)潛在的問題 。

在 Linux 系統(tǒng)中,我們可以通過/proc/buddyinfo文件來(lái)查看 Buddy 算法的內(nèi)存分配情況。這個(gè)文件詳細(xì)記錄了每個(gè)內(nèi)存節(jié)點(diǎn)和區(qū)域中不同階的空閑內(nèi)存塊數(shù)量 。例如,在一個(gè)單節(jié)點(diǎn)系統(tǒng)中,/proc/buddyinfo文件的內(nèi)容可能如下:

Node 0, zone      DMA1      2      1      0      1      0      1      0      1      0      1
Node 0, zone    DMA32100    200    150     80     40     20     10      5      2      1      0
Node 0, zone   Normal500    400    300    200    100     50     20     10      5      2      1

從這些數(shù)據(jù)中,我們可以清晰地看到不同區(qū)域(如 DMA、DMA32、Normal)中不同大小內(nèi)存塊(從 order0 到 order10,對(duì)應(yīng)不同的 2 的冪次方大小)的可用數(shù)量。通過分析這些數(shù)據(jù),我們可以判斷系統(tǒng)內(nèi)存的碎片化程度。如果高階塊(如 order10)的可用塊較少,可能表明內(nèi)存碎片化嚴(yán)重,難以分配到大塊連續(xù)內(nèi)存;如果低階塊(如 order0 或 order1)的可用塊較少,則可能表示系統(tǒng)內(nèi)存緊張 。

對(duì)于 slab 算法的內(nèi)存分配情況,我們可以使用cat /proc/slabinfo命令來(lái)查看。執(zhí)行該命令后,會(huì)輸出一系列的信息,每一行代表一個(gè) slab 緩存 。例如:

slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-16         9284     9284      16          256           1 : tunables    0    0    0 : slabdata     36     36      0
kmalloc-32         4567     4567      32          128           1 : tunables    0    0    0 : slabdata     36     36      0

在這些輸出信息中,active_objs表示當(dāng)前活躍的對(duì)象數(shù)量,num_objs表示對(duì)象總數(shù),objsize表示每個(gè)對(duì)象的大小,objperslab表示每個(gè) slab 中包含的對(duì)象數(shù)量,pagesperslab表示每個(gè) slab 占用的頁(yè)數(shù)。通過這些數(shù)據(jù),我們可以了解到各個(gè) slab 緩存的使用情況,判斷是否存在內(nèi)存分配不合理的情況 。比如,如果某個(gè) slab 緩存中active_objs數(shù)量長(zhǎng)期很高,而num_objs與active_objs相差不大,可能說明該 slab 緩存中的對(duì)象頻繁被使用,需要進(jìn)一步優(yōu)化內(nèi)存分配策略 。

除了上述方法外,我們還可以使用free命令查看系統(tǒng)的物理內(nèi)存和交換空間使用情況,包括空閑和已用內(nèi)存的數(shù)量、緩沖區(qū)和緩存的大小等信息。運(yùn)行free -h可以以易于閱讀的方式顯示結(jié)果 。top命令也是一個(gè)實(shí)時(shí)監(jiān)測(cè)系統(tǒng)資源的工具,它可以顯示當(dāng)前系統(tǒng)的進(jìn)程、CPU、內(nèi)存、磁盤等使用情況,在top的輸出中,我們能夠查看內(nèi)存使用情況的相關(guān)信息,包括總內(nèi)存、已用內(nèi)存、空閑內(nèi)存等 。

htop命令是一個(gè)增強(qiáng)版的top命令,提供了更多的交互式功能和可視化顯示,允許我們查看進(jìn)程的內(nèi)存使用情況,以及各個(gè)進(jìn)程所占用的內(nèi)存大小 。這些工具和命令相互配合,能夠幫助我們?nèi)妗⑸钊氲亓私?Linux 系統(tǒng)的內(nèi)存分配情況,為系統(tǒng)的優(yōu)化和管理提供有力的支持 。 

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2021-04-24 09:02:36

Linux 內(nèi)存分配

2021-06-06 13:06:34

JVM內(nèi)存分布

2021-08-30 11:13:28

內(nèi)存交換機(jī)制

2021-05-12 18:22:36

Linux 內(nèi)存管理

2021-10-06 20:23:08

Linux共享內(nèi)存

2019-07-01 09:22:15

Linux操作系統(tǒng)硬件

2020-01-14 12:08:32

內(nèi)存安全

2025-07-01 02:25:00

2019-11-22 09:36:00

Redis數(shù)據(jù)存儲(chǔ)

2020-03-31 14:40:24

HashMap源碼Java

2024-10-10 17:55:57

LinuxACL訪問控制列表

2021-06-30 08:45:02

內(nèi)存管理面試

2016-08-18 00:21:12

網(wǎng)絡(luò)爬蟲抓取網(wǎng)絡(luò)

2022-10-08 00:21:55

內(nèi)存芯片RAM

2024-08-05 00:05:00

操作系統(tǒng)內(nèi)存管理

2019-05-22 09:50:42

Python沙箱逃逸網(wǎng)絡(luò)攻擊

2021-08-01 08:05:39

Linux信號(hào)原理

2024-08-12 12:30:27

2025-04-09 05:22:00

2021-08-02 06:56:19

TypeScript編程語(yǔ)言編譯器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)