1. 程式人生 > >Nginx學習之路(六)NginX中的記憶體管理之---Nginx中的記憶體對齊和記憶體分頁

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記憶體池小塊記憶體管理單元中,其有效記憶體的最大值為一個頁面大小。