Nginx學習之路(六)NginX中的內存管理之---Nginx中的內存對齊和內存分頁

Nginx由於極高的性能受到大家的追捧,而Nginx的高性能與它優秀的內存管理方式是分不開的,今天就來聊一聊Nginx中的內存對齊和內存分頁。

先說下Nginx中的內存對齊,Nginx中的內存對齊機制是它高性能的關鍵因素之一,先說點基礎的東西,什麼是內存對齊呢? 內存對齊是操作系統爲了快速訪問內存而採取的一種策略。那麼爲什麼要內存對齊呢?因爲處理器讀寫數據,並不是以字節爲單位,而是以塊(2,4,8,16字節)爲單位進行的,而且由於操作系統的原因,塊的起始地址必須整除塊大小。如果不進行對齊,那麼本來只需要一次進行的訪問,可能需要好幾次才能完成,並且還要進行額外的數據分離和合並,導致效率低下。更嚴重地,有的CPU因爲不允許訪問unaligned address,就報錯,或者打開調試器或者dump core,比如sun sparc solaris絕對不會容忍你訪問unaligned address,都會以一個core結束你的程序的執行。所以一般編譯器都會在編譯時做相應的優化以保證程序運行時所有數據地址都是在'aligned address'上的,這就是內存對齊的由來。

      爲了更好理解上面的意思,這裏給出一個示例。在32位系統中,假如一個int變量在內存中的地址是0x00ff42c3,因爲int是佔用4個字節,所以它的尾地址應該是0x00ff42c6,這個時候CPU爲了讀取這個int變量的值,就需要先後讀取兩個4字節的塊,分別是0x00ff42c0~0x00ff42c3和0x00ff42c4~0x00ff42c7,然後通過移位等一系列的操作來得到,在這個計算的過程中還有可能引起一些總線數據錯誤的。但是如果編譯器對變量地址進行了對齊,比如放在0x00ff42c0,CPU就只需要一次就可以讀取到,這樣的話就加快讀取效率。

       綜合,內存對齊的原因有2點:
      (1) 平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。

      (2) 性能原因:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要至少要兩次內存訪問;而對齊的內存訪問僅需要一次訪問

再說一下linux下如何進行內存對齊的:

內存對齊可以用一句話來概括:"數據項只能存儲在地址是數據項大小的整數倍的內存位置上"。

每個特定平臺上的編譯器都有自己的默認“對齊係數”(也叫對齊模數)。程序員可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16 來改變這一系數,其中的n 就是你要指定的“對齊係數”。

規則1:

數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0 的地方,以後每個數據成員的對齊按照#pragma pack 指定的數值和這個數據成員自身長度中,比較小的那個進行

規則2:

結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。

規則3:

結合1、2 顆推斷:當#pragma pack 的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。

Nginx是怎麼做內存對齊的呢?:

//Nginx中的內存對齊
#ifndef NGX_ALIGNMENT
#define NGX_ALIGNMENT   sizeof(unsigned long)    /* platform word *///4byte
#endif
//把d對齊到a的整數倍
// 兩個參數d和a,d代表未對齊內存地址,a代表對齊單位,必須爲2的冪。假設a是8,那麼用二進制表示就是1000,a-1就是0111.
// d + a-1之後在第四位可能進位一次(如果d的前三位不爲000,則會進位。反之,則不會),
// ~(a-1)是1111...1000,&之後的結過就自然在4位上對齊了。注意二進制中第四位的單位是8,也就是以8爲單位對齊。
// 例如,d=17,a=8,則結果爲24.所以該表達式的結果就是對齊了的內存地址
#define ngx_align(d, a)     (((d) + (a - 1)) & ~(a - 1))
//把指針p的地址對齊到a的整數倍
#define ngx_align_ptr(p, a)                                                   \
    (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

說完內存對齊,再來說下Nginx中的內存分頁機制,nginx中的內存分頁實現很簡單,在ngx_create_pool(size_t size, ngx_log_t *log)中

//限定內存池的大小不超過NGX_MAX_ALLOC_FROM_POOL
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
//NGX_MAX_ALLOC_FROM_POOL是一個內存池分配的最大容量,值爲ngx_pagesize - 1,ngx_pagesize是一塊內存頁的大小,在x86下通常爲4096
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

這麼做有什麼好處呢?

在存儲器管理中,連續分配方式會形成許多“碎片”,雖然可通過“緊湊”方法將許多碎片拼接成可用的大塊空間,但須爲之付出很大開銷。如果允許將一個進程直接分散地裝入到許多不相鄰的分區中,則無須再進行“緊湊”。基於這一思想而產生了離散分配方式。如果離散分配的基本單位是頁,則稱爲分頁存儲管理方式。

分頁管理器把地址空間劃分成4K大小的頁面(非Intel X86體系與之不同),當進程訪問某個頁面時,操作系統首先在Cache中查找頁面,如果該頁面不在內存中,則產生一個缺頁中斷(Page Fault),進程就會被阻塞,直至要訪問的頁面從外存調入內存中。

綜上,內存分頁管理使得進程的地址空間可以爲整個物理內存地址空間(如4G),一個頁面經過映射後在實際物理空間中是連續存儲的。根據程序局部性原理,如果程序的指令在一段時間內訪問的內存都在同一頁面內,則會提高cache命中率,也就提高了訪存的效率。

總結一下,內存分頁管理使得程序向系統申請一個頁面的內存時,該頁內存地址在物理內存地址空間中是連續分佈的,這提高了cache命中率。如果申請的內存大於一個內存頁,則會降低程序指令訪存cache命中率。所以,在nginx內存池小塊內存管理單元中,其有效內存的最大值爲一個頁面大小。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章