續上篇:html
這是本系列的第二篇,預計還會有2篇,感興趣的同窗記得關注,以便接收推送,等不及的推薦閱讀原文。node
先放圖鎮樓: linux
來源:Linux地址空間佈局 - by Gustavo Duartegit
關於圖片的解釋可參見上篇。程序員
開始吧。github
工具箱:面試
堆上的內存分配,最簡單的實現能夠是修改 program break[2](譯註:參見上圖中部右側)的上界,申請對原位置和新位置之間內存的訪問權限。若是就這麼搞的話,堆上內存分配和棧上分配同樣快(除了換頁的開銷,通常咱們認爲棧是被鎖定在內存中;譯註:指棧不會被換出到磁盤,能夠用mlock限制OS對一段地址空間使用swap)。但這裏仍是有隻貓(cat),我是說,有點毛病(catch),見鬼。(譯tu注cao:這個真難翻)c#
char *block = sbrk(1024 * sizeof(char));
man 3 sbrk — 各類系統的 sbrk 使用多種不一樣的參數類型,常見的包括 int, ssize_t, ptrdiff_t, intptr_t
因爲這些問題,libc要求實現統一的內存分配接口,具體實現有不少[3](譯註:例如glibc,jemalloc,tcmalloc等),但都給你提供了線程安全、支持任意尺寸的內存分配器……只是要付出點代價——延遲,由於得引入鎖機制,以及用來維護已用/可用內存的數據結構,和額外的內存開銷。還有,堆也不是惟一的選項,在大塊內存分配的狀況下經常也會用內存映射段(Memory Mapping Segment,MMS)。segmentfault
man 3 malloc —— 通常來講,malloc() 從堆上分配內存, ... 當分配的內存塊大於 MMAP_THRESHOLD 時,glibc的 malloc() 實現使用私有匿名映射來分配內存。
(譯註:Linux 下 mmap 的 flags 參數是個 bitmap,其中有兩個 bit 分別爲 MAP_PRIVATE、MAP_ANONYMOUS,對應引用中提到的「私有」、「匿名」) api
因爲堆空間在 start_brk 和 brk (譯註:即 heap 的下界和上界,建議對照圖中右側的標註)之間老是連續的(譯註:這裏指的是虛擬地址空間連續,但其中每一頁均可能映射到任一物理頁),所以你沒法在中間打個洞來減小數據段的尺寸。好比這個場景:
char *truck = malloc(1024 * 1024 * sizeof(char)); char *bike = malloc(sizeof(char)); free(truck);
堆分配器將會調大 brk,以便給 truck 騰出空間。對於 bike 也同樣。可是當 truck 被釋放後,brk 不能被調小,由於 bike 正佔着高位的地址。結果是,你的進程能夠重用 truck 的內存,但不能退還給OS,除非 bike 也被釋放。固然你也能夠用 mmap 來分配 truck 所需的空間,不放在堆內存段裏,就能夠不影響 program break ,但這仍然沒法解決分配小塊內存致使的空洞(換句話說就是「引發碎片化」)。
注意 free() 並不老是會縮小數據段,由於這是一個有很大潛在開銷的操做(參見後文「對按需調頁的解釋」)。對於須要長時間運行的程序(例如守護進程)來講這會是個問題。有個叫 malloc_trim() 的 GNU 擴展能夠用來從堆頂釋放內存,但可能會慢得使人蛋疼,尤爲對於大量小對象的狀況,因此應該儘可能少用。
有一些實際場景中通用分配器有短板,例如大量分配固定尺寸的小內存。這看起來不像是典型的場景,但實際上出現得很頻繁 。例如,用於查找的數據結構(典型如樹、字典樹)須要分配大量節點用於構造其層次結構。在這個場景下,不只碎片化會是個問題,數據的局部性也是。cache效率高的數據結構會將key放在一塊兒(最好在同一個內存頁),而不是和數據混在一塊兒。默認的分配器不能保證下次分配時還在同一個block,更糟的是分配小單元的額外空間開銷。解決辦法在此:
X
來源: Slab by wadem, on Flickr (CC-BY-SA)
(譯註:原圖沒法打開了,另貼一張)
來源:IBM - Linux slab 分配器剖析
工具箱:
Bonwick 爲內核對象緩存寫的這篇文章[4]介紹了 slab 分配器的原理,也可用於用戶空間。Okay,咱們對綁定在CPU上的 slab 不感興趣 —— 就是你找分配器要一塊內存,例如說一整頁,而後切成不少固定大小的小塊。若是每一個小塊都能保存至少一個指針或一個整數,你就能夠把他們串成一個鏈表 ,表頭指向第一個空閒元素。
/* Super-simple slab. */ struct slab { void **head; }; /* Create page-aligned slab */ struct slab *slab = NULL; posix_memalign(&slab, page_size, page_size); slab->head = (void **)((char*)slab + sizeof(struct slab)); /* Create a NULL-terminated slab freelist */ char* item = (char*)slab->head; for(unsigned i = 0; i < item_count; ++i) { *((void**)item) = item + item_size; item += item_size; } *((void**)item) = NULL;
譯註:
posix_memalign
分配了一頁內存,而且對齊到頁邊界,這意味着正好拿到了一個物理頁item_count
= page_size
/ item_size
;其中 item_size
能夠根據應用須要指定。而後內存分配就簡單到只要彈出鏈表的頭結點就好了,內存釋放則是插入頭結點。這裏還有個優雅的小技巧:既然 slab 對齊到了頁邊界,你只要將指針向下取整到 page_size 就能獲得這個 slab 的指針。
/* Free an element */ struct slab *slab = (void *)((size_t)ptr & PAGESIZE_BITS); *((void**)ptr) = (void*)slab->head; slab->head = (void**)ptr; /* Allocate an element */ if((item = slab->head)) { slab->head = (void**)*item; } else { /* No elements left. */ }
譯註:對於 page_size = 4KB 的頁面,PAGESIZE_BITS = 0xFFFFF000,ptr & PAGESIZE_BITS 清零了低12位,正好是這一頁的開始,也就是這個slab的起始地址。
太棒了,可是還有binning(譯註:應該是指按不一樣的長度分桶),變長存儲,cache aliasing(譯註:同一個物理地址中的數據出如今多個不一樣的緩存行中),咖啡因(譯註:這應該是做者在逗逼了),...怎麼辦?能夠看看我以前爲 Knot DNS 寫的代碼[6],或者其餘實現了這些點的庫。例如,(喘口氣),glib 裏有個很整齊的文檔[7],把它稱爲「memory slices」。
譯註:slab 分配器適合大量小對象的分配,能夠避免常見的碎片問題;在內核中的實現還能夠支持硬件緩存對齊,從而提升緩存的利用率。
工具箱:
(譯註:指 GNU 的 obstack ,用stack來保存object的內存池實現)
正如slab分配器同樣,內存池比通用分配器好的地方在於,你每次申請一大塊內存,而後像切蛋糕同樣一小塊一小塊切出去,直到不夠用了,而後你再申請一大塊。還有,當你都處理完了之後,你就能夠收工,一次性釋放全部空間。
是否是特別傻瓜化?由於確實如此,但只是針對特定場景如此。你不須要考慮同步,也不須要考慮釋放。再沒有忘記回收的坑了,數據的局部性也更加符合預期,並且對於小對象的開銷也幾乎爲0。
這個模式特別適合不少類型的任務,包括短生命週期的重複分配(例如網絡請求處理),和長生命週期的不可變數據(例如frozen set;譯註:建立後再也不改變的集合)。你再也不須要逐個釋放對象(譯註:能夠最後批量釋放)。若是你能合理推測出平均須要多少內存,你還能夠將多餘的內存釋放,以便用於其餘目的。這能夠將內存分配問題簡化成簡單的指針運算。
並且你很走運 —— GNU libc 提供了,嗬,一整套API來幹這事兒。這就是 obstacks ,用棧來管理對象。它的 HTML 文檔[8] 寫得不咋地,不過拋開這些小缺陷,它容許你完成基於內存池的分配和回收(包括部分回收和全量回收)。
/* Define block allocator. */ #define obstack_chunk_alloc malloc #define obstack_chunk_free free /* Initialize obstack and allocate a bunch of animals. */ struct obstack animal_stack; obstack_init (&animal_stack); char *bob = obstack_alloc(&animal_stack, sizeof(animal)); char *fred = obstack_alloc(&animal_stack, sizeof(animal)); char *roger = obstack_alloc(&animal_stack, sizeof(animal)); /* Free everything after fred (i.e. fred and roger). */ obstack_free(&animal_stack, fred); /* Free everything. */ obstack_free(&animal_stack, NULL);
譯註:obstack這些api實際都是宏;須要經過宏來指定找OS分配和回收整塊內存用的方法,如上第二、3行所示。因爲對象使用棧的方式管理(先分配的最後釋放),因此釋放 fred 的時候,會把 fred 和在 fred 以後分配的對象(roger)一塊兒釋放掉。
還有個小技巧:你能夠擴展棧頂的那個對象。例如帶緩衝的輸入,變長數組,或者用來替代 realloc()-strcpy() 模式(譯註:從新分配內存,而後把原數據拷貝過去):
/* This is wrong, I better cancel it. */ obstack_grow(&animal_stack, "long", 4); obstack_grow(&animal_stack, "fred", 5); obstack_free (&animal_stack, obstack_finish(&animal_stack)); /* This time for real. */ obstack_grow(&animal_stack, "long", 4); obstack_grow(&animal_stack, "bob", 4); char *result = obstack_finish(&animal_stack); printf("%s\n", result); /* "longbob" */
譯註:前三行是做者逗逼了,看後四行就行;用 obstack_grow 擴展棧頂元素佔用的內存,擴展結束後調用 obstack_finish 結束擴展,並返回棧頂元素的地址。
工具箱:
通用內存分配器不當即將內存返回給系統的緣由之一是,這個操做開銷很大。系統須要作兩件事:(1) 創建虛擬頁到真實(real)頁的映射,和 (2) 給你一個清零的真實頁。這個真實頁被稱爲幀(frame),如今你知道它們的差異了。每一幀都必須被清空,畢竟你不但願 OS 泄漏其餘進程的祕密,對吧。這裏還有個小技巧,還記得 overcommit 嗎?虛擬內存分配器只把這個交易剛開始的那部分當回事,而後就開始變魔術了 —— 頁表裏的大部分頁面並不指向一個真實頁,而是指向一個特殊的全 0 頁面。
每次你想要訪問這個頁面時,就會觸發一個 page fault,這意味着內核會暫停 進程的執行,分配一個真實頁、更新頁表,而後恢復進程,並僞裝什麼也沒發生。這是彙總在一句話裏、我能作出的最好解釋了,這裏[9]還有更個詳細的版本。這也被稱做「按需調頁」(demand paging) 或 「延遲加載」(lazy loading)。
斯波克船長說「人沒法召喚將來」,但這裏你能夠操控它。
(譯註:星際迷航,斯波克說「One man cannot summon the future.」,柯克說「But one man can change the present.」)
內存管理器不是先知,他只是保守地預測你訪問內存的方式,而你本身也未必更清楚(你將會怎樣訪問內存)。(若是你知道)你能夠將一段連續的內存塊鎖定在物理內存中,以免後續的page fault:
char *block = malloc(1024 * sizeof(char)); mlock(block, 1024 * sizeof(char));
(譯註:訪問被換出到swap的頁面會觸發page fault,而後內存管理器會從磁盤中載入頁面,這會致使較嚴重的性能問題;用 mlock 將這段區域鎖定後,OS就不會被操做系統換出到swap;例如,在容許的狀況下,MySQL會用mlock將索引保持在物理內存中)
注意:你還能夠根據本身的內存使用模式,給內核提出建議
char *block = malloc(1024 * sizeof(block)); madvise(block, 1024 * sizeof(block), MADV_SEQUENTIAL);
對建議的解釋是平臺相關的,系統甚至可能選擇忽略它,但大部分平臺都處理得很好。但不是全部建議都有良好的支持,有些平臺可能會改變建議的語義(如MADV_FREE移除私有髒頁;譯註:「髒頁」,dirty page,是指分配之後有過寫入,其中可能有未保存的數據),可是最經常使用的仍是MADV_SEQUENTIAL, MADV_WILLNEED, 和 MADV_DONTNEED 這神聖三人組(譯註:holy trinity,聖經裏的三位一體,做者用詞太跳脫……)。
譯註:還記得《踩坑記:go服務內存暴漲》裏對 MADV_DONTNEED 和 MADV_FREE 的解釋嗎?這裏再回顧下
又到休息點,這篇暫時到這裏。
下一篇會繼續翻譯下一節《Fun with memory mapping》,還有不少有意思的內容,敬請關注~
順便再貼下以前推送的幾篇文章,祝過個充實的五一假期~
參考連接:
1. What a C programmer should know about memory
2. sbrk(2) - Linux man page
3. C Programming/stdlib.h/malloc
4. The Slab Allocator: An Object-Caching Kernel Memory Allocator
5. linus torvalds answers your questions
6. Knot DNS - slab.h
7. glib - memory slices