日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > linux >内容正文

linux

《Linux内核设计与实现》读书笔记(十二)- 内存管理

發(fā)布時(shí)間:2024/4/18 linux 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Linux内核设计与实现》读书笔记(十二)- 内存管理 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

內(nèi)核的內(nèi)存使用不像用戶空間那樣隨意,內(nèi)核的內(nèi)存出現(xiàn)錯(cuò)誤時(shí)也只有靠自己來(lái)解決(用戶空間的內(nèi)存錯(cuò)誤可以拋給內(nèi)核來(lái)解決)。

所有內(nèi)核的內(nèi)存管理必須要簡(jiǎn)潔而且高效。

主要內(nèi)容:

  • 內(nèi)存的管理單元
  • 獲取內(nèi)存的方法
  • 獲取高端內(nèi)存
  • 內(nèi)核內(nèi)存的分配方式
  • 總結(jié)

?

1. 內(nèi)存的管理單元

內(nèi)存最基本的管理單元是頁(yè),同時(shí)按照內(nèi)存地址的大小,大致分為3個(gè)區(qū)。

?

1.1 頁(yè)

頁(yè)的大小與體系結(jié)構(gòu)有關(guān),在 x86 結(jié)構(gòu)中一般是 4KB或者8KB。

可以通過(guò) getconf 命令來(lái)查看系統(tǒng)的page的大小:

[wangyubin@localhost ]$ getconf -a | grep -i 'page'PAGESIZE 4096 PAGE_SIZE 4096 _AVPHYS_PAGES 637406 _PHYS_PAGES 2012863

以上的 PAGESIZE 就是當(dāng)前機(jī)器頁(yè)大小,即 4KB

?

頁(yè)的結(jié)構(gòu)體頭文件是: <linux/mm_types.h> 位置:include/linux/mm_types.h

/** 頁(yè)中包含的成員非常多,還包含了一些聯(lián)合體* 其中有些字段我暫時(shí)還不清楚含義,以后再補(bǔ)上。。。*/ struct page {unsigned long flags; /* 存放頁(yè)的狀態(tài),各種狀態(tài)參見(jiàn)<linux/page-flags.h> */atomic_t _count; /* 頁(yè)的引用計(jì)數(shù) */union {atomic_t _mapcount; /* 已經(jīng)映射到mms的pte的個(gè)數(shù) */struct { /* 用于slab層 */u16 inuse;u16 objects;};};union {struct {unsigned long private; /* 此page作為私有數(shù)據(jù)時(shí),指向私有數(shù)據(jù) */struct address_space *mapping; /* 此page作為頁(yè)緩存時(shí),指向關(guān)聯(lián)的address_space */}; #if USE_SPLIT_PTLOCKSspinlock_t ptl; #endifstruct kmem_cache *slab; /* 指向slab層 */struct page *first_page; /* 尾部復(fù)合頁(yè)中的第一個(gè)頁(yè) */};union {pgoff_t index; /* Our offset within mapping. */void *freelist; /* SLUB: freelist req. slab lock */};struct list_head lru; /* 將頁(yè)關(guān)聯(lián)起來(lái)的鏈表項(xiàng) */ #if defined(WANT_PAGE_VIRTUAL)void *virtual; /* 頁(yè)的虛擬地址 */ #endif /* WANT_PAGE_VIRTUAL */ #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGSunsigned long debug_flags; /* Use atomic bitops on this */ #endif#ifdef CONFIG_KMEMCHECK/** kmemcheck wants to track the status of each byte in a page; this* is a pointer to such a status block. NULL if not tracked.*/void *shadow; #endif };

物理內(nèi)存的每個(gè)頁(yè)都有一個(gè)對(duì)應(yīng)的 page 結(jié)構(gòu),看似會(huì)在管理上浪費(fèi)很多內(nèi)存,其實(shí)細(xì)細(xì)算來(lái)并沒(méi)有多少。

比如上面的page結(jié)構(gòu)體,每個(gè)字段都算4個(gè)字節(jié)的話,總共40多個(gè)字節(jié)。(union結(jié)構(gòu)只算一個(gè)字段)

?

那么對(duì)于一個(gè)頁(yè)大小 4KB 的 4G內(nèi)存來(lái)說(shuō),一個(gè)有 4*1024*1024 / 4 = 1048576 個(gè)page,

一個(gè)page 算40個(gè)字節(jié),在管理內(nèi)存上共消耗內(nèi)存 40MB左右。

?

如果頁(yè)的大小是 8KB 的話,消耗的內(nèi)存只有 20MB 左右。相對(duì)于 4GB 來(lái)說(shuō)并不算很多。

?

1.2 區(qū)

頁(yè)是內(nèi)存管理的最小單元,但是并不是所有的頁(yè)對(duì)于內(nèi)核都一樣。

內(nèi)核將內(nèi)存按地址的順序分成了不同的區(qū),有的硬件只能訪問(wèn)有專門(mén)的區(qū)。

?

內(nèi)核中分的區(qū)定義在頭文件 <linux/mmzone.h> 位置:include/linux/mmzone.h

內(nèi)存區(qū)的種類參見(jiàn) enum zone_type 中的定義。

?

內(nèi)存區(qū)的結(jié)構(gòu)體定義也在 <linux/mmzone.h> 中。

具體參考其中 struct zone 的定義。

?

其實(shí)一般主要關(guān)注的區(qū)只有3個(gè):

區(qū)

描述

物理內(nèi)存

ZONE_DMADMA使用的頁(yè)<16MB
ZONE_NORMAL正常可尋址的頁(yè)16~896MB
ZONE_HIGHMEM動(dòng)態(tài)映射的頁(yè)>896MB

?

某些硬件只能直接訪問(wèn)內(nèi)存地址,不支持內(nèi)存映射,對(duì)于這些硬件內(nèi)核會(huì)分配 ZONE_DMA 區(qū)的內(nèi)存。

某些硬件的內(nèi)存尋址范圍很廣,比虛擬尋址范圍還要大的多,那么就會(huì)用到 ZONE_HIGHMEM 區(qū)的內(nèi)存,

對(duì)于 ZONE_HIGHMEM 區(qū)的內(nèi)存,后面還會(huì)討論。

對(duì)于大部分的內(nèi)存申請(qǐng),只要用 ZONE_NORMAL 區(qū)的內(nèi)存即可。

?

2. 獲取內(nèi)存的方法

內(nèi)核中提供了多種獲取內(nèi)存的方法,了解各種方法的特點(diǎn),可以恰當(dāng)?shù)膶⑵溆糜诤线m的場(chǎng)景。

?

2.1 按頁(yè)獲取 - 最原始的方法,用于底層獲取內(nèi)存的方式

以下分配內(nèi)存的方法參見(jiàn):<linux/gfp.h>

方法

描述

alloc_page(gfp_mask)只分配一頁(yè),返回指向頁(yè)結(jié)構(gòu)的指針
alloc_pages(gfp_mask, order)分配 2^order 個(gè)頁(yè),返回指向第一頁(yè)頁(yè)結(jié)構(gòu)的指針
__get_free_page(gfp_mask)只分配一頁(yè),返回指向其邏輯地址的指針
__get_free_pages(gfp_mask, order)分配 2^order 個(gè)頁(yè),返回指向第一頁(yè)邏輯地址的指針
get_zeroed_page(gfp_mask)只分配一頁(yè),讓其內(nèi)容填充為0,返回指向其邏輯地址的指針

?

alloc** 方法和 get** 方法的區(qū)別在于,一個(gè)返回的是內(nèi)存的物理地址,一個(gè)返回內(nèi)存物理地址映射后的邏輯地址。

如果無(wú)須直接操作物理頁(yè)結(jié)構(gòu)體的話,一般使用 get** 方法。

?

相應(yīng)的釋放內(nèi)存的函數(shù)如下:也是在 <linux/gfp.h> 中定義的

extern void __free_pages(struct page *page, unsigned int order); extern void free_pages(unsigned long addr, unsigned int order); extern void free_hot_page(struct page *page);

在請(qǐng)求內(nèi)存時(shí),參數(shù)中有個(gè) gfp_mask 標(biāo)志,這個(gè)標(biāo)志是控制分配內(nèi)存時(shí)必須遵守的一些規(guī)則。

gfp_mask 標(biāo)志有3類:(所有的 GFP 標(biāo)志都在 <linux/gfp.h> 中定義)

  • 行為標(biāo)志 :控制分配內(nèi)存時(shí),分配器的一些行為
  • 區(qū)標(biāo)志?? :控制內(nèi)存分配在那個(gè)區(qū)(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之類)
  • 類型標(biāo)志 :由上面2種標(biāo)志組合而成的一些常用的場(chǎng)景
  • ?

    行為標(biāo)志主要有以下幾種:

    行為標(biāo)志

    描述

    __GFP_WAIT分配器可以睡眠
    __GFP_HIGH分配器可以訪問(wèn)緊急事件緩沖池
    __GFP_IO分配器可以啟動(dòng)磁盤(pán)I/O
    __GFP_FS分配器可以啟動(dòng)文件系統(tǒng)I/O
    __GFP_COLD分配器應(yīng)該使用高速緩存中快要淘汰出去的頁(yè)
    __GFP_NOWARN分配器將不打印失敗警告
    __GFP_REPEAT分配器在分配失敗時(shí)重復(fù)進(jìn)行分配,但是這次分配還存在失敗的可能
    __GFP_NOFALL分配器將無(wú)限的重復(fù)進(jìn)行分配。分配不能失敗
    __GFP_NORETRY分配器在分配失敗時(shí)不會(huì)重新分配
    __GFP_NO_GROW由slab層內(nèi)部使用
    __GFP_COMP添加混合頁(yè)元數(shù)據(jù),在 hugetlb 的代碼內(nèi)部使用

    ?

    區(qū)標(biāo)志主要以下3種:

    區(qū)標(biāo)志

    描述

    __GFP_DMA從 ZONE_DMA 分配
    __GFP_DMA32只在 ZONE_DMA32 分配 (注1)
    __GFP_HIGHMEM從 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2)

    注1:ZONE_DMA32 和 ZONE_DMA 類似,該區(qū)包含的頁(yè)也可以進(jìn)行DMA操作。
    ???????? 唯一不同的地方在于,ZONE_DMA32 區(qū)的頁(yè)只能被32位設(shè)備訪問(wèn)。
    注2:優(yōu)先從 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 沒(méi)有多余的頁(yè)則從 ZONE_NORMAL 分配。

    ?

    類型標(biāo)志是編程中最常用的,在使用標(biāo)志時(shí),應(yīng)首先看看類型標(biāo)志中是否有合適的,如果沒(méi)有,再去自己組合 行為標(biāo)志和區(qū)標(biāo)志。

    類型標(biāo)志

    實(shí)際標(biāo)志

    描述

    GFP_ATOMIC__GFP_HIGH這個(gè)標(biāo)志用在中斷處理程序,下半部,持有自旋鎖以及其他不能睡眠的地方
    GFP_NOWAIT0與 GFP_ATOMIC 類似,不同之處在于,調(diào)用不會(huì)退給緊急內(nèi)存池。
    這就增加了內(nèi)存分配失敗的可能性
    GFP_NOIO__GFP_WAIT這種分配可以阻塞,但不會(huì)啟動(dòng)磁盤(pán)I/O。
    這個(gè)標(biāo)志在不能引發(fā)更多磁盤(pán)I/O時(shí)能阻塞I/O代碼,可能會(huì)導(dǎo)致遞歸
    GFP_NOFS(__GFP_WAIT | __GFP_IO)這種分配在必要時(shí)可能阻塞,也可能啟動(dòng)磁盤(pán)I/O,但不會(huì)啟動(dòng)文件系統(tǒng)操作。
    這個(gè)標(biāo)志在你不能再啟動(dòng)另一個(gè)文件系統(tǒng)的操作時(shí),用在文件系統(tǒng)部分的代碼中
    GFP_KERNEL(__GFP_WAIT | __GFP_IO | __GFP_FS )這是常規(guī)的分配方式,可能會(huì)阻塞。這個(gè)標(biāo)志在睡眠安全時(shí)用在進(jìn)程上下文代碼中。
    為了獲得調(diào)用者所需的內(nèi)存,內(nèi)核會(huì)盡力而為。這個(gè)標(biāo)志應(yīng)當(dāng)為首選標(biāo)志
    GFP_USER(__GFP_WAIT | __GFP_IO | __GFP_FS )這是常規(guī)的分配方式,可能會(huì)阻塞。用于為用戶空間進(jìn)程分配內(nèi)存時(shí)
    GFP_HIGHUSER(__GFP_WAIT | __GFP_IO | __GFP_FS )|__GFP_HIGHMEM)從 ZONE_HIGHMEM 進(jìn)行分配,可能會(huì)阻塞。用于為用戶空間進(jìn)程分配內(nèi)存
    GFP_DMA__GFP_DMA從 ZONE_DMA 進(jìn)行分配。需要獲取能供DMA使用的內(nèi)存的設(shè)備驅(qū)動(dòng)程序使用這個(gè)標(biāo)志
    通常與以上的某個(gè)標(biāo)志組合在一起使用。

    ?

    以上各種類型標(biāo)志的使用場(chǎng)景總結(jié):

    場(chǎng)景

    相應(yīng)標(biāo)志

    進(jìn)程上下文,可以睡眠使用 GFP_KERNEL
    進(jìn)程上下文,不可以睡眠使用 GFP_ATOMIC,在睡眠之前或之后以 GFP_KERNEL 執(zhí)行內(nèi)存分配
    中斷處理程序使用 GFP_ATOMIC
    軟中斷使用 GFP_ATOMIC
    tasklet使用 GFP_ATOMIC
    需要用于DMA的內(nèi)存,可以睡眠使用 (GFP_DMA|GFP_KERNEL)
    需要用于DMA的內(nèi)存,不可以睡眠使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠之前執(zhí)行內(nèi)存分配

    ?

    2.2 按字節(jié)獲取 - 用的最多的獲取方法

    這種內(nèi)存分配方法是平時(shí)使用比較多的,主要有2種分配方法:kmalloc()和vmalloc()

    kmalloc的定義在 <linux/slab_def.h> 中

    /*** @size - 申請(qǐng)分配的字節(jié)數(shù)* @flags - 上面討論的各種 gfp_mask*/ static __always_inline void *kmalloc(size_t size, gfp_t flags) #+end_srcvmalloc的定義在 mm/vmalloc.c 中 #+begin_src C /*** @size - 申請(qǐng)分配的字節(jié)數(shù)*/ void *vmalloc(unsigned long size)

    kmalloc 和 vmalloc 區(qū)別在于:

    • kmalloc 分配的內(nèi)存物理地址是連續(xù)的,虛擬地址也是連續(xù)的
    • vmalloc 分配的內(nèi)存物理地址是不連續(xù)的,虛擬地址是連續(xù)的

    ?

    因此在使用中,用的較多的還是 kmalloc,因?yàn)閗malloc 的性能較好。

    因?yàn)閗malloc的物理地址和虛擬地址之間的映射比較簡(jiǎn)單,只需要將物理地址的第一頁(yè)和虛擬地址的第一頁(yè)關(guān)聯(lián)起來(lái)即可。

    而vmalloc由于物理地址是不連續(xù)的,所以要將物理地址的每一頁(yè)都和虛擬地址關(guān)聯(lián)起來(lái)才行。

    ?

    kmalloc 和 vmalloc 所對(duì)應(yīng)的釋放內(nèi)存的方法分別為:

    void kfree(const void *) void vfree(const void *)

    ?

    2.3 slab層獲取 - 效率最高的獲取方法

    頻繁的分配/釋放內(nèi)存必然導(dǎo)致系統(tǒng)性能的下降,所以有必要為頻繁分配/釋放的對(duì)象內(nèi)心建立緩存。

    而且,如果能為每個(gè)處理器建立專用的高速緩存,還可以避免 SMP鎖帶來(lái)的性能損耗。

    ?

    2.3.1 slab層實(shí)現(xiàn)原理

    linux中的高速緩存是用所謂 slab 層來(lái)實(shí)現(xiàn)的,slab層即內(nèi)核中管理高速緩存的機(jī)制。

    整個(gè)slab層的原理如下:

  • 可以在內(nèi)存中建立各種對(duì)象的高速緩存(比如進(jìn)程描述相關(guān)的結(jié)構(gòu) task_struct 的高速緩存)
  • 除了針對(duì)特定對(duì)象的高速緩存以外,也有通用對(duì)象的高速緩存
  • 每個(gè)高速緩存中包含多個(gè) slab,slab用于管理緩存的對(duì)象
  • slab中包含多個(gè)緩存的對(duì)象,物理上由一頁(yè)或多個(gè)連續(xù)的頁(yè)組成
  • ?

    高速緩存->slab->緩存對(duì)象之間的關(guān)系如下圖:

    ?

    2.3.2 slab層的應(yīng)用

    slab結(jié)構(gòu)體的定義參見(jiàn):mm/slab.c

    struct slab {struct list_head list; /* 存放緩存對(duì)象,這個(gè)鏈表有 滿,部分滿,空 3種狀態(tài) */unsigned long colouroff; /* slab 著色的偏移量 */void *s_mem; /* 在 slab 中的第一個(gè)對(duì)象 */unsigned int inuse; /* slab 中已分配的對(duì)象數(shù) */kmem_bufctl_t free; /* 第一個(gè)空閑對(duì)象(如果有的話) */unsigned short nodeid; /* 應(yīng)該是在 NUMA 環(huán)境下使用 */ };

    ?

    slab層的應(yīng)用主要有四個(gè)方法:

    • 高速緩存的創(chuàng)建
    • 從高速緩存中分配對(duì)象
    • 向高速緩存釋放對(duì)象
    • 高速緩存的銷毀
    /*** 創(chuàng)建高速緩存* 參見(jiàn)文件: mm/slab.c* 這個(gè)函數(shù)的注釋很詳細(xì),這里就不多說(shuō)了。*/ struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))/*** 從高速緩存中分配對(duì)象也很簡(jiǎn)單* 函數(shù)參見(jiàn)文件:mm/slab.c* @cachep - 指向高速緩存指針* @flags - 之前討論的 gfp_mask 標(biāo)志,只有在高速緩存中所有slab都沒(méi)有空閑對(duì)象時(shí),* 需要申請(qǐng)新的空間時(shí),這個(gè)標(biāo)志才會(huì)起作用。** 分配成功時(shí),返回指向?qū)ο蟮闹羔?/span>*/ void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)/*** 向高速緩存釋放對(duì)象* @cachep - 指向高速緩存指針* @objp - 要釋放的對(duì)象的指針*/ void kmem_cache_free(struct kmem_cache *cachep, void *objp)/*** 銷毀高速緩存* @cachep - 指向高速緩存指針 */ void kmem_cache_destroy(struct kmem_cache *cachep)

    ?

    我做了創(chuàng)建高速緩存的例子,來(lái)嘗試使用上面的幾個(gè)函數(shù)。

    測(cè)試代碼如下:(其中用到的 kn_common.h 和 kn_common.c 參見(jiàn)之前的博客《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》讀書(shū)筆記(六)- 內(nèi)核數(shù)據(jù)結(jié)構(gòu))

    #include <linux/slab.h> #include <linux/slab_def.h> #include "kn_common.h"MODULE_LICENSE("Dual BSD/GPL");#define MYSLAB "testslab"static struct kmem_cache *myslab;/* 申請(qǐng)內(nèi)存時(shí)調(diào)用的構(gòu)造函數(shù) */ static void ctor(void* obj) {printk(KERN_ALERT "constructor is running....\n"); }struct student {int id;char* name; };static void print_student(struct student *);static int testslab_init(void) {struct student *stu1, *stu2;/* 建立slab高速緩存,名稱就是宏 MYSLAB */myslab = kmem_cache_create(MYSLAB,sizeof(struct student),0,0,ctor);/* 高速緩存中分配2個(gè)對(duì)象 */printk(KERN_ALERT "alloc one student....\n");stu1 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);stu1->id = 1;stu1->name = "wyb1";print_student(stu1);printk(KERN_ALERT "alloc one student....\n");stu2 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);stu2->id = 2;stu2->name = "wyb2";print_student(stu2);/* 釋放高速緩存中的對(duì)象 */printk(KERN_ALERT "free one student....\n");kmem_cache_free(myslab, stu1);printk(KERN_ALERT "free one student....\n");kmem_cache_free(myslab, stu2);/* 執(zhí)行完后查看 /proc/slabinfo 文件中是否有名稱為 “testslab”的緩存 */return 0; }static void testslab_exit(void) {/* 刪除建立的高速緩存 */printk(KERN_ALERT "*************************\n");print_current_time(0);kmem_cache_destroy(myslab);printk(KERN_ALERT "testslab is exited!\n");printk(KERN_ALERT "*************************\n");/* 執(zhí)行完后查看 /proc/slabinfo 文件中是否有名稱為 “testslab”的緩存 */ }static void print_student(struct student *stu) {if (stu != NULL){printk(KERN_ALERT "**********student info***********\n");printk(KERN_ALERT "student id is: %d\n", stu->id);printk(KERN_ALERT "student name is: %s\n", stu->name);printk(KERN_ALERT "*********************************\n");}elseprintk(KERN_ALERT "the student info is null!!\n"); }module_init(testslab_init); module_exit(testslab_exit);

    ?

    Makefile文件如下:

    # must complile on customize kernel obj-m += myslab.o myslab-objs := testslab.o kn_common.o#generate the path CURRENT_PATH:=$(shell pwd) #the current kernel version number LINUX_KERNEL:=$(shell uname -r) #the absolute path LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL) #complie object all:make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modulesrm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned #clean clean:rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned

    ?

    執(zhí)行測(cè)試代碼:(我是在 centos6.3 x64 上實(shí)驗(yàn)的)

    [root@vbox chap12]# make [root@vbox chap12]# insmod myslab.ko [root@vbox chap12]# dmesg | tail -220 # 可以看到第一次申請(qǐng)內(nèi)存時(shí),系統(tǒng)一次分配很多內(nèi)存用于緩存(構(gòu)造函數(shù)執(zhí)行了多次) [root@vbox chap12]# cat /proc/slabinfo | grep test #查看我們建立的緩存名在不在系統(tǒng)中 testslab 0 0 16 202 1 : tunables 120 60 0 : slabdata 0 0 0 [root@vbox chap12]# rmmod myslab.ko #卸載內(nèi)核模塊 [root@vbox chap12]# cat /proc/slabinfo | grep test #我們的緩存名已經(jīng)不在系統(tǒng)中了

    ?

    3. 獲取高端內(nèi)存

    高端內(nèi)存就是之前提到的 ZONE_HIGHMEM 區(qū)的內(nèi)存。

    在x86體系結(jié)構(gòu)中,這個(gè)區(qū)的內(nèi)存不能映射到內(nèi)核地址空間上,也就是沒(méi)有邏輯地址,

    為了使用 ZONE_HIGHMEM 區(qū)的內(nèi)存,內(nèi)核提供了永久映射和臨時(shí)映射2種手段:

    ?

    3.1 永久映射

    永久映射的函數(shù)是可以睡眠的,所以只能用在進(jìn)程上下文中。

    /* 將 ZONE_HIGHMEM 區(qū)的一個(gè)page永久的映射到內(nèi)核地址空間* 返回值即為這個(gè)page對(duì)應(yīng)的邏輯地址*/ static inline void *kmap(struct page *page)/* 允許永久映射的數(shù)量是有限的,所以不需要高端內(nèi)存時(shí),應(yīng)該及時(shí)的解除映射 */ static inline void kunmap(struct page *page)

    ?

    3.2 臨時(shí)映射

    臨時(shí)映射不會(huì)阻塞,也禁止了內(nèi)核搶占,所以可以用在中斷上下文和其他不能重新調(diào)度的地方。

    /*** 將 ZONE_HIGHMEM 區(qū)的一個(gè)page臨時(shí)映射到內(nèi)核地址空間* 其中的 km_type 表示映射的目的,* enum kn_type 的定義參見(jiàn):<asm/kmap_types.h>*/ static inline void *kmap_atomic(struct page *page, enum km_type idx)/* 相應(yīng)的解除映射是個(gè)宏 */ #define kunmap_atomic(addr, idx) do { pagefault_enable(); } while (0)

    以上的函數(shù)都在 <linux/highmem.h> 中定義的。

    ?

    4. 內(nèi)核內(nèi)存的分配方式

    內(nèi)核的內(nèi)存分配和用戶空間的內(nèi)存分配相比有著更多的限制條件,同時(shí)也有著更高的性能要求。

    下面討論2個(gè)和用戶空間不同的內(nèi)存分配方式。

    ?

    4.1 內(nèi)核棧上的靜態(tài)分配

    用戶空間中一般不用擔(dān)心棧上的內(nèi)存不足,也不用擔(dān)心內(nèi)存的管理問(wèn)題(比如內(nèi)存越界之類的),

    即使出了異常也有內(nèi)核來(lái)保證系統(tǒng)的正常運(yùn)行。

    ?

    而在內(nèi)核空間則完全不一樣,不僅棧空間有限,而且為了管理的效率和盡量減少問(wèn)題的發(fā)生,

    內(nèi)核棧一般都是小而且固定的。

    ?

    在x86體系結(jié)構(gòu)中,內(nèi)核棧的大小一般就是1頁(yè)或2頁(yè),即 4KB ~ 8KB

    內(nèi)核棧可以在編譯內(nèi)核時(shí)通過(guò)配置選項(xiàng)將內(nèi)核棧配置為1頁(yè),

    配置為1頁(yè)的好處是分配時(shí)比較簡(jiǎn)單,只有一頁(yè),不存在內(nèi)存碎片的情況,因?yàn)橐豁?yè)是本就是分配的最小單位。

    當(dāng)有中斷發(fā)生時(shí),如果共享內(nèi)核棧,中斷程序和被中斷程序共享一個(gè)內(nèi)核棧會(huì)可能導(dǎo)致空間不足,

    于是,每個(gè)進(jìn)程除了有個(gè)內(nèi)核棧之外,還有一個(gè)中斷棧,中斷棧一般也就1頁(yè)大小。

    ?

    查看當(dāng)前系統(tǒng)內(nèi)核棧大小的方法:

    [xxxxx@localhost ~]$ ulimit -a | grep 'stack' stack size (kbytes, -s) 8192

    ?

    4.2 按CPU分配

    與單CPU環(huán)境不同,SMP環(huán)境下的并行是真正的并行。單CPU環(huán)境是宏觀并行,微觀串行。

    真正并行時(shí),會(huì)有更多的并發(fā)問(wèn)題。

    ?

    假定有如下場(chǎng)景:

    void* p;if (p == NULL) { /* 對(duì) P 進(jìn)行相應(yīng)的操作,最終 P 不是NULL了 */ } else { /* P 不是NULL,繼續(xù)對(duì) P 進(jìn)行相應(yīng)的操作 */ }

    在上述場(chǎng)景下,可能會(huì)有以下的執(zhí)行流程:

  • 剛開(kāi)始 p == NULL
  • 線程A 執(zhí)行到 [if (p == NULL)] ,剛進(jìn)入 if 內(nèi)的代碼時(shí)被線程B 搶占
    ? 由于線程A 還沒(méi)有執(zhí)行 if 內(nèi)的代碼,所以 p 仍然是 NULL
  • 線程B 搶占到CPU后開(kāi)始執(zhí)行,執(zhí)行到 [if (p == NULL)]時(shí), 發(fā)現(xiàn) p 是 NULL,執(zhí)行 if 內(nèi)的代碼
  • 線程B 執(zhí)行完后,線程A 重新被調(diào)度,繼續(xù)執(zhí)行 if 的代碼
    ? 其實(shí)此時(shí)由于線程B 已經(jīng)執(zhí)行完,p 已經(jīng)不是 NULL了,線程A 可能會(huì)破壞線程B 已經(jīng)完成的處理,導(dǎo)致數(shù)據(jù)不一致
  • ?

    在單CPU環(huán)境下,上述情況無(wú)需加鎖,只需在 if 處理之前禁止內(nèi)核搶占,在 else 處理之后恢復(fù)內(nèi)核搶占即可。

    而在SMP環(huán)境下,上述情況必須加鎖,因?yàn)榻箖?nèi)核搶占只能禁止當(dāng)前CPU的搶占,其他的CPU仍然調(diào)度線程B 來(lái)?yè)屨季€程A 的執(zhí)行

    ?

    SMP環(huán)境下加鎖過(guò)多的話,會(huì)嚴(yán)重影響并行的效率,如果是自旋鎖的話,還會(huì)浪費(fèi)其他CPU的執(zhí)行時(shí)間。

    所以內(nèi)核中才有了按CPU分配數(shù)據(jù)的接口。

    按CPU分配數(shù)據(jù)之后,每個(gè)CPU自己的數(shù)據(jù)不會(huì)被其他CPU訪問(wèn),雖然浪費(fèi)了一點(diǎn)內(nèi)存,但是會(huì)使系統(tǒng)更加的簡(jiǎn)潔高效。

    ?

    4.2.1 按CPU分配的優(yōu)勢(shì)

    按CPU來(lái)分配數(shù)據(jù)主要有2個(gè)優(yōu)點(diǎn):

  • 最直接的效果就是減少了對(duì)數(shù)據(jù)的鎖,提高了系統(tǒng)的性能
  • 由于每個(gè)CPU有自己的數(shù)據(jù),所以處理器切換時(shí)可以大大減少緩存失效的幾率 (*注1)
  • ?

    注1:如果一個(gè)處理器操作某個(gè)數(shù)據(jù),而這個(gè)數(shù)據(jù)在另一個(gè)處理器的緩存中時(shí),那么存放這個(gè)數(shù)據(jù)的那個(gè)

    處理器必須清理或刷新自己的緩存。持續(xù)的緩存失效成為緩存抖動(dòng),對(duì)系統(tǒng)性能影響很大。

    ?

    4.2.2 編譯時(shí)分配

    可以在編譯時(shí)就定義分配給每個(gè)CPU的變量,其分配的接口參見(jiàn):<linux/percpu-defs.h>

    /* 給每個(gè)CPU聲明一個(gè)類型為 type,名稱為 name 的變量 */ DECLARE_PER_CPU(type, name) /* 給每個(gè)CPU定義一個(gè)類型為 type,名稱為 name 的變量 */ DEFINE_PER_CPU(type, name)

    注意上面兩個(gè)宏,一個(gè)是聲明,一個(gè)是定義。

    其實(shí)也就是 DECLARE_PER_CPU 中多了個(gè) extern 的關(guān)鍵字

    ?

    分配好變量后,就可以在代碼中使用這個(gè)變量 name 了。

    DEFINE_PER_CPU(int, name); /* 為每個(gè)CPU定義一個(gè) int 類型的name變量 */get_cpu_var(name)++; /* 當(dāng)前處理器上的name變量 +1 */ put_cpu_var(name); /* 完成對(duì)name的操作后,激活當(dāng)前處理器的內(nèi)核搶占 */

    ?

    通過(guò) get_cpu_var 和 put_cpu_var 的代碼,我們可以發(fā)現(xiàn)其中有禁止和激活內(nèi)核搶占的函數(shù)。

    相關(guān)代碼在 <linux/percpu.h> 中

    #define get_cpu_var(var) (*({ \extern int simple_identifier_##var(void); \preempt_disable();/* 這句就是禁止當(dāng)前處理器上的內(nèi)核搶占 */ \&__get_cpu_var(var); })) #define put_cpu_var(var) preempt_enable() /* 這句就是激活當(dāng)前處理器上的內(nèi)核搶占 */

    ?

    4.2.3 運(yùn)行時(shí)分配

    除了像上面那樣靜態(tài)的給每個(gè)CPU分配數(shù)據(jù),還可以以指針的方式在運(yùn)行時(shí)給每個(gè)CPU分配數(shù)據(jù)。

    動(dòng)態(tài)分配參見(jiàn):<linux/percpu.h>

    /* 給每個(gè)處理器分配一個(gè) size 字節(jié)大小的對(duì)象,對(duì)象的偏移量是 align */ extern void *__alloc_percpu(size_t size, size_t align); /* 釋放所有處理器上已分配的變量 __pdata */ extern void free_percpu(void *__pdata);/* 還有一個(gè)宏,是按對(duì)象類型 type 來(lái)給每個(gè)CPU分配數(shù)據(jù)的,* 其實(shí)本質(zhì)上還是調(diào)用了 __alloc_percpu 函數(shù) */ #define alloc_percpu(type) (type *)__alloc_percpu(sizeof(type), \__alignof__(type))

    ?

    動(dòng)態(tài)分配的一個(gè)使用例子如下:

    void *percpu_ptr; unsigned long *foo;percpu_ptr = alloc_percpu(unsigned long); if (!percpu_ptr)/* 內(nèi)存分配錯(cuò)誤 */foo = get_cpu_var(percpu_ptr); /* 操作foo ... */ put_cpu_var(percpu_ptr);

    ?

    5. 總結(jié)

    在眾多的內(nèi)存分配函數(shù)中,如何選擇合適的內(nèi)存分配函數(shù)很重要,下面總結(jié)了一些選擇的原則:

    應(yīng)用場(chǎng)景

    分配函數(shù)選擇

    如果需要物理上連續(xù)的頁(yè)選擇低級(jí)頁(yè)分配器或者 kmalloc 函數(shù)
    如果kmalloc分配是可以睡眠指定 GFP_KERNEL 標(biāo)志
    如果kmalloc分配是不能睡眠指定 GFP_ATOMIC 標(biāo)志
    如果不需要物理上連續(xù)的頁(yè)vmalloc 函數(shù) (vmalloc 的性能不如 kmalloc)
    如果需要高端內(nèi)存alloc_pages 函數(shù)獲取 page 的地址,在用 kmap 之類的函數(shù)進(jìn)行映射
    如果頻繁撤銷/創(chuàng)建教導(dǎo)的數(shù)據(jù)結(jié)構(gòu)建立slab高速緩存

    總結(jié)

    以上是生活随笔為你收集整理的《Linux内核设计与实现》读书笔记(十二)- 内存管理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。