簡單瞭解 Nginx 中的記憶體池
一、背景
之前在內部分享了一次 Nginx,並寫了一篇文章《 ofollow,noindex" target="_blank">一次 Nginx 分享 》。
現在繼續來看看 Nginx 裡面有意思的程式碼吧。
第一個就是 Nginx 造的一堆輪子:記憶體池。
二、記憶體池
Nginx 實現了一個簡單的記憶體池,效率非常高,但是可用性非常差。
記憶體池,顧名思義,就是預先從作業系統裡申請好記憶體,程式實際需要記憶體的時候,直接在使用者態分配即可。
為啥要這樣做呢?
因為預設分配記憶體涉及到與作業系統打交道,比較慢。
而自己管理記憶體則只需要第一次和作業系統打交道,之後都是在自己的資料上操作的,無非一些變數的加加減減,效能非常高。
具體怎麼實現呢?
先申請一塊比較大的記憶體,然後業務申請記憶體的時候,先儲存可用記憶體的首地址,然後更新可用記憶體的偏移量。
虛擬碼如下:
void * ngx_palloc_s(ngx_pool_t* pool, size_t size){ char* m = pool->d.last; p->d.last =m + size; return m; }
看了上面的程式碼,很多獨立思考的人會有很多疑問。
1.比如記憶體不夠了怎麼辦?
2.記憶體需要對齊怎麼辦?
3.記憶體怎麼回收?
這三個問題也很容易回答。
三、記憶體不夠了怎麼辦?
不夠了建立新的記憶體池即可。
也就是記憶體池是一個連結串列,這樣就不存在不夠的問題了。
void * ngx_palloc_s(ngx_pool_t* pool, size_t size){ char* m ; ngx_pool_t* p = pool->current; //第一個記憶體池 do{ m = p->d.last; if(p->d.end - m >= size){ p->d.last =m + size; return m; } p = p->d.next; //下一個記憶體池 }while(p); //現有的記憶體池都不夠,建立新的記憶體池 return ngx_palloc_n(pool, size); }
建立新記憶體池的程式碼也很簡單。
呼叫系統的申請記憶體函式,然後把新記憶體池掛在連結串列的最後面。
這裡面有一個優化,由於連結串列要在最後插入一個元素的時候,需要遍歷連結串列。
nginx就在每個記憶體池上加了一個計數器,如果一個記憶體池被遍歷四次,就把當前記憶體池設定為current。
這樣就可以保證記憶體池有效連結串列節點的個數不會超過7個。
void* ngx_palloc_b(ngx_pool* pool, size_t size){ ngx_pool_t* new = new_pool(); for(p p pool->currentl p_d.next; p = p->d.next){ if(p->d.failed++ > 4){ pool->current = p->d.nexr; } } p->d.next = new;//插入到最後 }
四、記憶體需要對齊怎麼辦?
那就先對齊記憶體,在分配空間。
void * ngx_palloc_s(ngx_pool_t* pool, size_t size){ char* m = pool->d.last; m = ngx_align(m); p->d.last =m + size; return m; }
可能有人會問:為什麼要對齊記憶體?
這個涉及到的學問就有意思了。
第一個原因是為了效能。
因為 CPU 讀資料就是位元組記憶體對齊的。
假設分配的記憶體不對齊,我們的地址從 0x9
開始,要讀 8
位元組,則這個資料跨越了兩個對齊的空間。
CPU 要讀這個資料就需要兩次。
第二個是為了方便計算地址。
比如對於整數陣列,我們使用下標偏移的時候,每次加的是 4
位元組。
a[n] = a + n * 4
如果位元組對齊的話,就可以使用位操作來快速計算 a[n] = ((a>>2) + n) << 2;
第三是很多CPU只支援位元組對齊的程式。
這個就比較霸道了,硬體不支援不對齊的程式,那我們只好對齊了。
具體可以參考這個地址吧, https://www.ibm.com/developerworks/library/pa-dalign/
五、記憶體怎麼回收?
這個是最魔幻的地方。
答案是業務自己回收,這裡不提供回收函式。
吐血。。。
那隨便找個使用記憶體池的程式碼,看看怎麼回收的吧。
以 array
資料結構為例。
程式碼中可以看到,回收的時候有個判斷。
回收的資料必須是記憶體池最後一個分配的記憶體才可以回收。
這個就要求業務自己嚴格保證申請記憶體的順序和回收記憶體的順序完全相反了。
一丁點都不能錯,錯一個就全部不能回收了。
另外,再考慮我們分配記憶體的時候曾有過記憶體對齊操作。
如果分配記憶體時進行了記憶體對齊,將永遠也滿足不了這個回收條件了,也就是永遠也回收不了了。
六、最後
看到這裡, Nginx 的記憶體池就看完了。
是不是實現的特別簡單、效能特別高。
但是也應了我上篇文章《 一次 Nginx 分享 》說的那句話:
而 Nginx 為了提高效能,所有的功能都自己來實現了。 比如 SLAB 記憶體管理,array、list、hash、紅黑樹、regex等等。 自己實現的時候,僅僅實現自己需要的功能。 這樣做出來的功能沒有那麼通用,但是也沒有冗餘,自己使用時效能會更高。
還有這句話:
對於任何系統,不管如何高效能,都存在其侷限性,都有對應的使用場景。 越過了使用場景,可能就不是高效能了,或者良好的模組設計就不那麼良好了。 Nginx 是如此, Redis 依舊如此。
本文首發於公眾號:天空的程式碼世界,微信號:tiankonguse-code。