轉自:https://blog.csdn.net/Vince_/article/details/79668199
轉載:http://www.cnblogs.com/tolimit/
首先為什么要說slub分配器,內核里小內存分配一共有三種,SLAB/SLUB/SLOB,slub分配器是slab分配器的進化版,而slob是一種精簡的小內存分配算法,主要用於嵌入式系統。慢慢的slab分配器或許會被slub取代,所以對slub的了解是十分有必要的。
我們先說說slab分配器的弊端,我們知道slab分配器中每個node結點有三個鏈表,分別是空閑slab鏈表,部分空slab鏈表,已滿slab鏈表,這三個鏈表中維護着對應的slab緩沖區。我們也知道slab緩沖區的內存是從伙伴系統中申請過來的,我們設想一個情景,如果沒有內存回收機制的情況下,只要申請的slab緩沖區就會存入這三個鏈表中,並不會返回到伙伴系統里,如果這個類型的SLAB迎來了一個分配高峰期,將會從伙伴系統中獲取很多頁面去生成許多slab緩沖區,之后這些slab緩沖區並不會自動返回到伙伴系統中,而是會添加到node結點的這三個slab鏈表中去,這樣就會有很多slab緩沖區是很少用到的。
而slub分配器把node結點的這三個鏈表精簡為了一個鏈表,只保留了部分空slab鏈表,而SLUB中對於每個CPU來說已經不使用空閑對象鏈表,而是直接使用單個slab,並且每個CPU都維護有自己的一個部分空鏈表。在slub分配器中,對於每個node結點,也沒有了所有CPU共享的空閑對象鏈表。我們用以下圖來表示以下slab分配器和slub分配器的區別(上圖為SLAB,下圖為SLUB):
單個SLAB分配器結構
單個SLUB分配器結構
SLUB分配器
發明SLUB分配器的主要目的就是減少slab緩沖區的個數,讓更多的空閑內存得到使用。首先,SLUB和SLAB一樣,都分為多種,同時也分為專用SLUB和普通SLUB。如TCP,UDP,dquot這些,它們都是專用SLAB,專屬於它們自己的模塊。而后面這張圖,如kmalloc-8,kmalloc-16...還有dma-kmalloc-96,dma-kmalloc-192...在這方面與SLAB是一樣的,同樣地,也是使用一個struct kmem_cache結構來描述一個SLUB(與SLAB一樣)。並且這個struct kmem_cache與SLAB的struct kmem_cache幾乎是同一個,而且對於SLAB和SLUB,向外提供的接口是統一的(函數名、參數以及返回值一模一樣),這樣也就讓驅動和其他模塊在編寫代碼時無需操心系統使用的是SLAB還是SLUB。這是為了同一個內核可以通過編譯選項使用SLAB或者SLUB。
SLUB分配器中的slab緩沖區結構與SLAB分配器中的slab緩沖區的結構也有了明顯的不同,對於SLAB分配器的slab緩沖區,其結構如下:
而在SLUB分配器的slab緩沖區結構中,已經沒有了對象描述符數組,而freelist也拆分成了每個對象有一個指向下一個對象的指針,如下:
雖然這兩個slab緩沖區的結構上有所不同,但其實際原理還是一樣,每次分配或釋放都會設置對象的下個空閑對象指針,讓其指向正確的位置。有疑問的同學可以看看我之前寫的linux內存源碼分析 - SLAB分配器概述。在初始化一個slab緩沖區時,默認第一個空閑對象是對象0,然后對象0后面跟着的下一個空閑對象指針指向對象1,對象1的空閑對象指針指向對象2,以此類推。
我們看看SLUB分配器的描述符,struct kmem_cache結構:
struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; /* 標志 */ unsigned long flags; /* 每個node結點中部分空slab緩沖區數量不能低於這個值 */ unsigned long min_partial; /* 分配給對象的內存大小(大於對象的實際大小,大小包括對象后邊的下個空閑對象指針) */ int size; /* 對象的實際大小 */ int object_size; /* 存放空閑對象指針的偏移量 */ int offset; /* cpu的可用objects數量范圍最大值 */ int cpu_partial; /* 保存slab緩沖區需要的頁框數量的order值和objects數量的值,通過這個值可以計算出需要多少頁框,這個是默認值,初始化時會根據經驗計算這個值 */ struct kmem_cache_order_objects oo; /* 保存slab緩沖區需要的頁框數量的order值和objects數量的值,這個是最大值 */ struct kmem_cache_order_objects max; /* 保存slab緩沖區需要的頁框數量的order值和objects數量的值,這個是最小值,當默認值oo分配失敗時,會嘗試用最小值去分配連續頁框 */ struct kmem_cache_order_objects min; /* 每一次分配時所使用的標志 */ gfp_t allocflags; /* 重用計數器,當用戶請求創建新的SLUB種類時,SLUB 分配器重用已創建的相似大小的SLUB,從而減少SLUB種類的個數。 */ int refcount; /* 創建slab時的構造函數 */ void (*ctor)(void *); /* 元數據的偏移量 */ int inuse; /* 對齊 */ int align; int reserved; /* 高速緩存名字 */ const char *name; /* 所有的 kmem_cache 結構都會鏈入這個鏈表,鏈表頭是 slab_caches */ struct list_head list; #ifdef CONFIG_SYSFS /* 用於sysfs文件系統,在/sys中會有個slub的專用目錄 */ struct kobject kobj; #endif #ifdef CONFIG_MEMCG_KMEM /* 這兩個主要用於memory cgroup的,先不管 */ struct memcg_cache_params *memcg_params; int max_attr_size; #ifdef CONFIG_SYSFS struct kset *memcg_kset; #endif #endif #ifdef CONFIG_NUMA /* 用於NUMA架構,該值越小,越傾向於在本結點分配對象 */ int remote_node_defrag_ratio; #endif /* 此高速緩存的SLAB鏈表,每個NUMA結點有一個,有可能該高速緩存有些SLAB處於其他結點上 */ struct kmem_cache_node *node[MAX_NUMNODES]; };
掃一下整個kmem_cache結構,知識點最重要的有4個:每CPU對應的cpu_slab結構,每個node結點對應的kmem_cache_node結構,slub重用以及struct kmem_cache_order_objects結構對應的oo,max,min這三個值。
除去以上4個知識點,我們先簡單說說kmem_cache中的一些成員變量:
- size:size = 對象大小 + 對象后面緊跟的下個空閑對象指針。
- object_size:對象大小。
- offset:對象首地址 + offset = 下個空閑對象指針地址
- min_partial:node結點中部分空slab緩沖區數量不能小於這個值,如果小於這個值,空閑slab緩沖區則不能夠進行釋放,而是將空閑slab加入到node結點的部分空slab鏈表中。
- cpu_partial:同min_partial類似,只是這個值表示的是空閑對象數量,而不是部分空slab數量,即CPU的空閑對象數量不能小於這個值,小於的情況下要去對應node結點的部分空鏈表中獲取若干個部分空slab。
- name:該kmem_cache的名字。
我們再來看看struct kmem_cache_cpu __percpu *cpu_slab,對於同一種kmem_cache來說,每個CPU對應有自己的struct kmem_cache_cpu結構,這個結構如下:
struct kmem_cache_cpu { /* 指向下一個空閑對象,用於快速找到對象 */ void **freelist; /* 用於保證cmpxchg_double計算發生在正確的CPU上,並且可作為一個鎖保證不會同時申請這個kmem_cache_cpu的對象 */ unsigned long tid; /* CPU當前所使用的slab緩沖區描述符,freelist會指向此slab的下一個空閑對象 */ struct page *page; /* CPU的部分空slab鏈表,放到CPU的部分空slab鏈表中的slab會被凍結,而放入node中的部分空slab鏈表則解凍,凍結標志在slab緩沖區描述符中 */ struct page *partial; #ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS]; #endif };
在此結構中主要注意有個partial部分空slab鏈表以及page指針,page指針指向當前使用的slab緩沖區描述符,內核中slab緩沖區描述符與頁描述符共用一個struct page結構。SLUB分配器與SLAB分配器有一部分不同就在此,SLAB分配器的每CPU結構中保存的是空閑對象鏈表,而SLUB分配器的每CPU結構中保存的是一個slab緩沖區。而對於tid,它主要用於檢查是否有並發,對於一些操作,操作前讀取其值,操作結束后再檢查其值是否與之前讀取的一致,非一致則要進行一些相應的處理,這個tid一般是遞增狀態,每分配一次對象加1。這個結構說明了一個問題,就是每個CPU有自己當前使用的slab緩沖區,CPU0不能夠使用CPU1所在使用的slab緩存,CPU1也不能夠使用CPU0正在使用的slab緩存。而CPU從node獲取slab緩沖區時,一般傾向於從該CPU所在的node結點上分配,如果該node結點沒有空閑的內存,則根據memcg以及node結點的zonelist從其他node獲取slab緩沖區。這些具體可以在代碼中見到。
我們再看看kmem_cache_node結構:
struct kmem_cache_node { /* 鎖 */ spinlock_t list_lock; /* SLAB使用 */ #ifdef CONFIG_SLAB /* 只使用了部分對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_partial; /* partial list first, better asm code */ /* 不包含空閑對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_full; /* 只包含空閑對象的SLAB描述符的雙向循環鏈表 */ struct list_head slabs_free; /* 高速緩存中空閑對象個數(包括slabs_partial鏈表中和slabs_free鏈表中所有的空閑對象) */ unsigned long free_objects; /* 高速緩存中空閑對象的上限 */ unsigned int free_limit; /* 下一個被分配的SLAB使用的顏色 */ unsigned int colour_next; /* Per-node cache coloring */ /* 指向這個結點上所有CPU共享的一個本地高速緩存 */ struct array_cache *shared; /* shared per node */ struct alien_cache **alien; /* on other nodes */ /* 兩次緩存收縮時的間隔,降低次數,提高性能 */ unsigned long next_reap; /* 0:收縮 1:獲取一個對象 */ int free_touched; /* updated without locking */ #endif /* SLUB使用 */ #ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial; #ifdef CONFIG_SLUB_DEBUG /* 該node中此kmem_cache的所有slab的數量 */ atomic_long_t nr_slabs; /* 該node中此kmem_cache中所有對象的數量 */ atomic_long_t total_objects; struct list_head full; #endif #endif };
這個結構中我們只需要看#ifdef CONFIG_SLUB部分,這個結構里正常情況下只有一個node結點部分空slab鏈表partial,如果在編譯內核時選擇了CONFIG_SLUB_DEBUG選項,則會有個node結點滿slab鏈表。對於SLAB分配器,SLUB分配器在這個結構也做出了相應的變化,去除了滿slab緩沖區鏈表和空閑slab緩沖區鏈表,只使用了一個部分空slab緩沖區鏈表。對於所有的CPU來說,它們可以使用這個node結點里面部分空鏈表中保存的那些slab緩沖區,當它們需要使用時,要先將緩沖區拿到CPU對應自己的鏈表或者當前使用中,也就是說node結點上部分空slab緩沖區同一個時間只能讓一個CPU使用。
而關於slub重用,這里只做一個簡單的解釋,其作用是為了減少slub的種類,比如我有個kmalloc-8類型的slub,里面每個對象大小是8,而我某個驅動想申請自己所屬的slub,其對象大小是6,這時候系統會給驅動一個假象,讓驅動申請了自己專屬的slub,但系統實際把kmalloc-8這個類型的slub返回給了驅動,之后驅動中分配對象時實際上就是從kmalloc-8中分配對象,這就是slub重用,將相近大小的slub共用一個slub類型,雖然會造成一些內碎片,但是大大減少了slub種類過多以及減少使用了跟多的內存。
最后說說struct kmem_cache_order_objects結構對應的oo,max,min這三個值,struct kmem_cache_order_objects結構實際上就是一個unsigned long,這個結構有兩個作用,保存一個slab緩沖區占用頁框的order值和一個slab緩沖區對象數量的值。當kmem_cache需要創建一個新的slab緩沖區時,會使用它們當中保存的oder值去申請2的order次方個數的頁框。oo是一個默認值,在大多數情況下創建一個新的slab緩沖區時會用oo中的值來申請頁框,而min是在oo申請失敗的情況下使用,它是一個比oo更小的值,當伙伴系統拿不出oo中指定的數量的頁框,會嘗試向伙伴系統申請min中指定的頁框數量(這個slab緩沖區連續頁框數量少,對象數量也會少)。而max的值是在做slab緩沖區壓縮時使用,其作用更多的是作為一個安全值,在這個kmem_cache中所有slab緩沖區的objects數量都不會大於max中的值。所有情況都是max >= oo > min。
現在,我們描述一下SLUB分配器是如何運作的,kmem_cache初始化后其是沒有slab緩沖區的,當其他模塊需要從此kmem_cache中申請一個對象時,kmem_cache會從伙伴系統獲取連續的頁框作為一個slab緩沖區,然后通過kmem_cache中的cotr函數指針指向的構造函數構造初始化這個slab緩沖區后,將其設置為該cpu的當前使用slab緩沖區,當此slab緩沖區使用完后,外部模塊在申請對象時,會把這個滿的slab緩沖區移除,再從伙伴系統獲取一段連續頁框作為一個新的空閑slab緩沖區,也是設置為該CPU當前使用的slab緩沖區。而那些滿slab緩沖區中有對象釋放時,SLUB分配器優先把這些緩沖區放入該CPU對應的部分空slab鏈表。而當一個部分空slab通過釋放對象成為了一個空閑slab緩沖區時,SLUB分配器會視情況而定將此空閑slab釋放還是加入到node結點的部分空slab鏈表中。
我們先看看一個slub初始化結束的情況:
初始化完成后,slub中並沒有一個slab緩沖區,只有在第一次申請時,才會從伙伴系統中獲取一段連續頁框作為一個slab緩沖區,如下:
這時候當前CPU獲得了一個空閑slab緩沖區,並將其中的一個空閑對象分配出去,而下次申請對象時也會從該slab緩沖區中獲取對象,直到此緩沖區中對象用完為止。
上面描述的是初始化完成后第一次申請對象的情況,現在我們描述一下運行時申請對象的情況,一種情況是當前CPU使用的slab緩沖區有多余的空閑對象,這樣直接從這些多余的空閑對象中分配一個出去即可,這種情況很簡單。我們着重說明CPU使用的slab緩沖區沒有多余的空閑對象的情況,這種情況又分為CPU的部分空slab鏈表是否為空的情況,如果CPU部分空slab鏈表不為空,則CPU會將當前使用的滿slab移除,並從CPU的部分空slab鏈表中獲取一個部分空的slab緩沖區,並設置為CPU當前使用的slab緩沖區,如下圖:
如果node的部分空鏈表和CPU的部分空鏈表都為空的情況,那就與我們第一次申請對象的情況一樣,直接從伙伴系統中獲取連續頁框用於一個slab緩沖區。
現在我們再說說CPU當前使用的slab已滿,CPU的部分空slab鏈表為空的情況,這種情況下,會從node結點的部分空slab鏈表獲取若干個部分空slab緩沖區,將它們放入CPU的部分空slab鏈表中,獲取的slab緩沖區個數根據一個規則就是:cpu空閑的對象數量必須要大於kmem_cache中的cpu_partial的值的一半。具體如下:
各種情況的申請對象都已經說明了,接下來我們說說釋放對象的情況,釋放對象也分很多種,我們先說說最簡單的一種釋放情況,就是部分空的slab釋放其中一個使用着的對象,釋放后這個部分空slab還是部分空slab(有些部分空slab只使用了一個對象,釋放這個對象后就變為空閑slab),這些部分空slab可能處於CPU當前使用slab,CPU部分空鏈表,node部分空鏈表中,但是它們的處理都是一樣的,直接釋放掉該對象即可,如下:
另一種情況是滿slab緩沖區釋放對象后變為了部分空slab緩沖區,這種情況下系統會將此部分空slab緩沖區放入CPU的部分空鏈表中,如下:
最后一種釋放情況就是部分空slab釋放一個對象后轉變成了空閑slab緩沖區,而對於這個空閑slab緩沖區的處理,系統首先會檢查node部分空鏈表中slab緩沖區的個數,如果node部分空鏈表中slab緩沖區數量小於kmem_cache中的min_partial,則將這個空閑slab緩沖區放入node部分空鏈表中。否則釋放此空閑slab,將其占用頁框返回伙伴系統中。我們知道部分空slab有可能存在於3個地方,CPU當前使用的slab緩沖區,CPU部分空鏈表,node部分空鏈表,這三個地方對於這種情況下的處理都是一樣的,如下:
這樣看來只有空閑的slab緩沖區會被放入node結點的部分空鏈表中,這只是從釋放對象的角度看是這樣的,當刷新kmem_cache時,會將kmem_cache中所有的slab緩沖區放回到node結點的部分空鏈表(也包括當前CPU使用的slab緩沖區),這種情況node結點的部分空鏈表就會有部分空slab緩沖區了。而還有一種情況就是編譯時禁用了CPU的部分空鏈表,即CPU只有一個當前使用的slab緩沖區,這樣其他的部分空緩沖區都會保存在node結點的部分空鏈表上,更多詳細細節請看內核源碼中的mm/slub.c文件。
slab緩沖區壓縮技術
本來不想寫這一節,不過擔心以后懶得去用一篇文章去描述slab壓縮技術,這里就簡單說一下吧。
說是壓縮技術,其實就是把kmem_cache中所有的slab緩沖區放回到node結點的部分空鏈表中(包括所有CPU當前正在使用的slab),然后node結點的部分空鏈表中的空閑的slab緩沖區釋放掉,然后將node結點中的其他部分空slab緩沖區按照空閑對象數量進行重新排列,把空閑數量少的放在前面,空閑數量多的放在后面,這樣空閑數量少的更容易被移去cpu的部分空鏈表。其實思想就是讓那些更容易成為滿slab的部分空slab優先被使用。總結出來就是釋放空閑slab和對部分空slab排序。
我們知道,在node結點的部分空鏈表中,slab緩沖區數量少於kmem_cache中的min_partial的值時,即使空閑slab緩沖區也不會被釋放,而是放入node結點部分空鏈表中,這樣一來之后會有一些空閑slab緩沖區無法自動釋放回伙伴系統,壓縮技術就是在系統內存緊急時會去釋放這些空閑的伙伴系統,然后對其他部分空的slab緩沖區重新排列。代碼如下:
int __kmem_cache_shrink(struct kmem_cache *s) { int node; int i; struct kmem_cache_node *n; struct page *page; struct page *t; /* 所有slab緩沖區的最大對象數量 */ int objects = oo_objects(s->max); /* 申請objects個鏈表頭,每個inuse相同的slab緩沖區會放入對應的鏈表中 */ struct list_head *slabs_by_inuse = kmalloc(sizeof(struct list_head) * objects, GFP_KERNEL); unsigned long flags; if (!slabs_by_inuse) return -ENOMEM; /* 刷新這個kmem_cache中所有的slab,這個操作會將所有CPU中的slab放回到node結點的部分空鏈表中 */ flush_all(s); /* 變量kmem_cache中的每個node結點 */ for_each_kmem_cache_node(s, node, n) { /* node結點部分空鏈表為空則直接下一個結點 */ if (!n->nr_partial) continue; /* node結點部分空鏈表不為空,初始化slabs_by_inuse鏈表中每個鏈表頭結點 */ for (i = 0; i < objects; i++) INIT_LIST_HEAD(slabs_by_inuse + i); /* kmem_cache_node上鎖 */ spin_lock_irqsave(&n->list_lock, flags); /* 遍歷node結點部分空鏈表中所有的部分空slab緩沖區 */ list_for_each_entry_safe(page, t, &n->partial, lru) { /* 將node結點中所有的部分空slab緩沖區移到slabs_by_inuse中inuse鏈表中,也就是所有inuse=1的slab放入同一個鏈表,inuse=2的放入同一個鏈表 */ list_move(&page->lru, slabs_by_inuse + page->inuse); /* 如果inuse == 0,則node結點的部分空slab數量-- */ if (!page->inuse) n->nr_partial--; } /* 重建node結點的部分空鏈表,將slabs_by_inuse中inuse高的放在前面,inuse低的放在后面,讓inuse高的更容易得到分配機會,也就是讓inuse高的更快用完 */ for (i = objects - 1; i > 0; i--) list_splice(slabs_by_inuse + i, n->partial.prev); spin_unlock_irqrestore(&n->list_lock, flags); /* 如果有空的slab緩沖區,空的slab緩沖區保存在slabs_by_inuse + 0的鏈表位置,釋放他們 */ list_for_each_entry_safe(page, t, slabs_by_inuse, lru) discard_slab(s, page); } /* 釋放objects個鏈表頭 */ kfree(slabs_by_inuse); return 0; }