Nginx源码分析--数据对齐posix_memalign和memalign函数
posix_memalign函數()
/*
?* 背景:
?*????? 1)POSIX 1003.1d
?*????? 2)POSIX 標明了通過malloc( ), calloc( ), 和 realloc( ) 返回的地址對于
?*????? 任何的C類型來說都是對齊的
?* 功能:由posix_memalign分配的內存空間,需要由free釋放。
?* 參數:
?*????? p?????????? 分配好的內存空間的首地址
?*????? alignment?? 對齊邊界,Linux中,32位系統是8字節,64位系統是16字節
?*????? size??????? 指定分配size字節大小的內存
?*
?* 要求:
?*????? 1)要求alignment是2的冪,并且是p指針大小的倍數
?*????? 2)要求size是alignment的倍數
?* 返回:
?*????? 0?????? 成功
?*????? EINVAL? 參數不滿足要求
?*????? ENOMEM? 內存分配失敗
?* 注意:
?*????? 1)該函數不影響errno,只能通過返回值判斷
?*
?*/
memalign()函數與 posix_memalign 的不同是其將分配好的內存塊首地址做為返回值
?
封裝 posix_memalign,如果是 Solaris 則封裝 memalign
#if (NGX_HAVE_POSIX_MEMALIGN)void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{void *p;int err;err = posix_memalign(&p, alignment, size);if (err) {ngx_log_error(NGX_LOG_EMERG, log, err,"posix_memalign(%uz, %uz) failed", alignment, size);p = NULL;}ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,"posix_memalign: %p:%uz @%uz", p, size, alignment);return p;
}#elif (NGX_HAVE_MEMALIGN)void *
ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
{void *p;p = memalign(alignment, size);if (p == NULL) {ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,"memalign(%uz, %uz) failed", alignment, size);}ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,"memalign: %p:%uz @%uz", p, size, alignment);return p;
}#endif
?參考:Nginx源碼完全注釋(1)ngx_alloc.h / ngx_alloc.c
?
=========================
?
對齊
數 據的對齊(alignment)是指數據的地址和由硬件條件決定的內存塊大小之間的關系。一個變量的地址是它大小的倍數的時候,這就叫做自然對齊 (naturally aligned)。例如,對于一個32bit的變量,如果它的地址是4的倍數,--? 就是說,如果地址的低兩位是0,那么這就是自然對齊了。所以,如果一個類型的大小是2n個字節,那么它的地址中,至少低n位是0。對齊的規則是由硬件引起 的。一些體系的計算機在數據對齊這方面有著很嚴格的要求。在一些系統上,一個不對齊的數據的載入可能會引起進程的陷入。在另外一些系統,對不對齊的數據的 訪問是安全的,但卻會引起性能的下降。在編寫可移植的代碼的時候,對齊的問題是必須避免的,所有的類型都該自然對齊。
?
預對齊內存的分配在大多數情況下,編譯器和C庫透明地幫你處理對齊問題。 POSIX? 標明了通過malloc( ), calloc( ), 和 realloc( )? 返回的地址對于任何的C類型來說都是對齊的。在Linux中,這些函數返回的地址在32位系統是以8字節為邊界對齊,在64位系統是以16字節為邊界對齊 的。有時候,對于更大的邊界,例如頁面,程序員需要動態的對齊。雖然動機是多種多樣的,但最常見的是直接塊I/O的緩存的對齊或者其它的軟件對硬件的交 互,因此, POSIX 1003.1d提供一個叫做 posix_ memalign( )的函數:
/* one or the other -- either suffices */
#define _XOPEN_SOURCE 600
#define _GNU_SOURCE
#include <stdlib.h>
int posix_ memalign (void **memptr,
??????????????????? size_t alignment,
??????????????????? size_t size);
* See http://perens.com/FreeSoftware/ElectricFence/ and http://valgrind.org, respectively.
調用 posix_ memalign( )成功時會返回size字節的動態內存,并且這塊內存的地址是alignment的倍數。參數alignment必須是2的冪,還是void指針的大小的倍數。返回的內存塊的地址放在了memptr里面,函數返回值是0.
調用失敗時,沒有內存會被分配,memptr的值沒有被定義,返回如下錯誤碼之一:
EINVAL
參數不是2的冪,或者不是void指針的倍數。
ENOMEM
沒有足夠的內存去滿足函數的請求。
要注意的是,對于這個函數,errno不會被設置,只能通過返回值得到。
由 posix_ memalign( )獲得的內存通過free( )釋放。用法很簡單:
char *buf;
int ret;
/* allocate 1 KB along a 256-byte boundary */
ret = posix_ memalign (&buf, 256, 1024);
if (ret) {
??? fprintf (stderr, " posix_ memalign: %s\n",
???????????? strerror (ret));
??? return -1;
}
/* use 'buf'... */
free (buf);
更早的接口。在 POSIX定義了 posix_ memalign( )之前,BSD和SunOS分別提供了如下接口:
#include <malloc.h>
void * valloc (size_t size);
void * memalign (size_t boundary, size_t size);
函數valloc( )的功能和malloc( )一模一樣,但返回的地址是頁面對齊的。回想第四章,頁面的大小很容易通過getpagesize( )得到。
相似地,函數 memalign( )是以boundary字節對齊的,而boundary必須是2的冪。在這個例子中,兩個函數都返回一塊足夠大的內存去容納一個ship結構,并且地址都是在一個頁面的邊界上:
struct ship *pirate, *hms;
pirate = valloc (sizeof (struct ship));
if (!pirate) {
??? perror ("valloc");
??? return -1;
}
hms = memalign (getpagesize ( ), sizeof (struct ship));
if (!hms) {
??? perror (" memalign");
??? free (pirate);
??? return -1;
}
/* use 'pirate' and 'hms'... */
??? free (hms);
??? free (pirate);
在 Linux中,由這兩個函數獲得的內存都可以通過free(? )釋放。但在別的Unix系統卻未必是這樣,一些系統并沒有提供一個足夠安全的機制去釋放這些內存。考慮移植性的程序不得不放棄使用這些接口來獲得動態內 存。Linux程序員最好只在考慮對老系統的兼容性時才使用它們;posix_memalign( )更加強大。只有在malloc( )不能提供足夠大的對齊時,這三個接口才需要使用。
?
其它和對齊有關的與對齊有關的問題的范圍要超過標準類型的自然對齊和動態存儲器地分配。例如,非標準和復雜的類型比標準類型有更復雜的要求。另外,對對齊的關注在給指向不同類型的指針賦值和使用強轉時顯得加倍的重要。
非標準類型。非標準和復雜的數據類型的對齊比簡單的自然對齊有著更多的要求。這里四個有很有用的方法:
?一個結構的對齊要求是和它的成員中最大的那個類型一樣的。例如,一個結構中最大的是以4字節對齊的32bit的整形,那么這個結構至少以4字節對齊。
?結構也引入了填充的需要,用來保證每一個成員都符合自己的對齊要求。所以,如果一個char (可能以1字節對齊)后跟著一個int (可能以4字節對齊),編譯器會自動地插入3個字節作為填充來保證int以4字節對齊。
程序員有時候排列結構里面的成員-例如,以大小來遞減-來是使用作填充的垃圾空間最少。GCC的選項- Wpadded能對這些努力有幫助,因為它使得在編譯器偷偷插入填充時產生警告。
?一個聯合的對齊和聯合里最大的類型一樣。
?一個數組的對齊和數組里的元素一樣。所以,數組的對齊并不比單單的一個成員嚴格,這樣能使數組里面的所有成員都是自然對齊的。
與指針的快樂時光。因為編譯器明確地處理了絕大多數的對齊問題,所以要找到潛在的錯誤的時候也比較困難。然而,這樣的錯誤并不少見,特別是在處理指針和強轉的時候。
一個指針指向由小的對齊強轉到大的對齊的數據塊,通過這個指針使用數據,能引起進程加載對于大的類型來說并沒有適當對齊的數據。例如,在如下的代碼片段,c到badnews的強轉使得程序將c當unsigned long來讀:
char greeting[] = "Ahoy Matey";
char *c = greeting[1];
unsigned long badnews = *(unsigned long *) c;
一 個unsigned long? 可能以4或8字節為邊界對齊;當然c只以1字節為邊界對齊。明顯,強轉之后,c的加載,會違反對齊規則。在不同的系統中,這樣可能引起的后果,小者是性能 的打擊,大者是整個程序的崩潰。在能發現而不能處理對齊錯誤的機器結構中,內核向出問題的進程發送SIGBUS信號來終結進程。我們會在第九章討論信號。
這種錯誤在現實中的普遍程度超出我們的想象,現實世界的例子雖看上去沒有這么愚蠢,但亦更難以覺察了。
?
數據段的管理Unix 系統在歷史上提供過直接管理數據段的接口。然而,程序都沒有直接地使用這些接口,因為malloc(? )和其它的申請方法更容易使用和更加強大。我會在這里說一下這些接口來滿足一下大家的好奇心,同時也給那些想實現他自己的基于堆棧的動態內存申請機制的人 一個參考:
#include <unistd.h>
int brk (void *end);
void * sbrk (intptr_t increment);
這 些功能的名字源于老學校的Unix系統,那時堆和棧還在同一個段中。堆中動態存儲器的分配由數據段的底部向上生長;棧從數據段的頂部向著堆向下生長。堆和 棧的分界線叫做break或break point。在現代的系統里面,數據段存在于它自己的內存映射,我們繼續用斷點來標記映射的結束地址。
一個brk( )的調用設置斷點(數據段的末端)的地址為end。在成功的時候,返回0。失敗的時候,返回-1,并設置errno為ENOMEM。
一個sbrk( )的調用將數據段末端生長increment字節,increment可能是正數,也可能是負數。sbrk( )返回修改后的斷點。所以,increment為0時得到的是現在斷點的地址:
printf ("The current break point is %p\n", sbrk (0));
特意地,POSIX和C都沒有定義這些函數。但幾乎所有的Unix系統,都提供其中一個或全部。可移植的程序應該堅持使用基于標準的接口。
?
匿名存儲器映射glibc 的動態存儲器使用了數據段和內存映射。實現malloc(? )的經典方法是將數據段分為一系列的大小為2的冪的分區,返回最小的符合要求的那個塊來滿足請求。釋放內存就像免費的似的和標記內存一樣簡單了。如果臨近 的分區是空閑的,他們會被合成一個更大的分區。如果斷點的下面是空的,系統可以用brk( )來降低斷點,使堆收縮,將內存返回給系統。
這 個算法叫做伙伴內存分配算法(buddy memory allocation? scheme)。它的優勢是高速和簡單,但不好的地方是引入了兩種碎片。內部碎片(Internal? fragmentation)發生在用更大的塊來滿足一個分配。這樣導致了內存的低使用率。當有著足夠的空閑內存來滿足要求但這“塊”內存分布在兩個不相 鄰空間的時候,外部碎片(External? fragmentation)就產生了。這會導致內存的低使用率(因為一塊更大的不夠適合的塊可能被使用了),或者內存分配失敗(在沒有可供選擇的塊 時)。
更有甚者,這個算法允許一個內存的分配“釘”住另外一個,使得glibc不能向內核歸還內存。想象內存中的已被分配的兩個塊,塊A 和塊B。塊A剛好在斷點的下面,塊B剛好在A的下面,就算釋放了B,glibc也不能相應的調整斷點直到A被釋放。在這種情況,一個長期存在的內存分配就 把另外的空閑空間“釘”住了。
但這不需太過擔憂。因為glibc無論如何也不會總例行公事一成不變地將內存返回給系統。*通常來說,在每 次釋放后堆并不收縮。相反,glibc為后續的分配保留著些自由的空間。只有在堆與已分配的空間相比明顯太大的時候,glibc才會把堆縮小。然而,一個 更大的分配,就能防止這個收縮了。
*glibc也使用比這伙伴系統更加先進的存儲分配算法,叫做arena algorithm.
因 此,對于較大的分配,glibc并不使用堆。glibc使用一個匿名存儲器映射(anonymous memory? mapping)來滿足請求。匿名存儲器映射和在第四章討論的基于文件的映射是相似的,只是它并不基于文件-所以為之“匿名”。實際上,匿名存儲器映射是 一個簡單的全0填充的大內存塊,隨時可供你使用。因為這種映射的存儲不是基于堆的,所以并不會在數據段內產生碎片。
通過匿名映射來分配內存又下列好處:
?無需關心碎片。當程序不再需要這塊內存的時候,只是撤銷映射,這塊內存就直接歸還給系統了。
?匿名存儲器映射能改變大小,有著改變大小的能力,還能像普通的映射一樣接收命令(看第四章)。
?每個分配存在于獨立的內存映射。沒有必要再去管理一個全局的堆了。
下面是兩個使用匿名存儲器映射而不使用堆的劣處:
?每個存儲器映射都是頁面大小的整數倍。所以,如果大小不是頁面整數倍的分配會浪費大量的空間。這些空間更值得憂慮,因為相對于被分配的空間,被浪費掉的空間往往更多。
?建立一個存儲器映射比將堆里面的空間回收利用的負載更大,因為堆可能并不包含有任何的內核動作。越小的分配,這個劣處就明顯。
跟 變戲法似的,glibc的malloc( )? 能用用數據段來滿足小的分配,用存儲器映射來滿足大的分配。臨界點是可被設定的(看后面的高級內存分配),也有可能一個glibc版本是這樣,另外一個就 不是了。目前,臨界點一般是128KB:比128KB小的分配由堆實現,相應地,更大的由匿名存儲器映射來實現。
?
創建匿名存儲器映射可能你會想強制在堆上使用存儲器映射來滿足一個特定的內存分配,也可能你會想寫一個自己的存儲分配系統,總之你可能會要手動創建你自己的匿名內存映射,Linux讓這變得很簡單。回想第四章系統調用,用來創建存儲器映射的mmap( )和取消映射的munmap( ):
#include <sys/mman.h>
void * mmap (void *start,
size_t length,
int prot,
int flags,
int fd,
off_t offset);
int munmap (void *start, size_t length);
因為沒有文件需要打開和管理,創建匿名存儲器映射真的要比創建基于文件的存儲器映射簡單。兩者最關鍵的差別在于匿名標記是否出現。讓我們來看看這個例子:
void *p;
p = mmap (NULL, /* do not care where */
????????? 512 * 1024, /* 512 KB */
????????? PROT_READ | PROT_WRITE, /* read/write */
????????? MAP_ANONYMOUS | MAP_PRIVATE, /* anonymous, private */
????????? -1, /* fd (ignored) */
????????? 0); /* offset (ignored) */
if (p == MAP_FAILED)
??? perror ("mmap");
else
??? /* 'p' points at 512 KB of anonymous memory... */
對于大多數的匿名映射來說,mmap( )的參數都跟這個例子一樣,當然了,程序員決定的映射大小這個參數是個例外。別的參數一般都像這樣:
?第一個參數是start,被設為NULL,意味著匿名映射可以內核安排的在任意地址上發生。當然給定一個non-NULL值也是有可能的,那樣的話它的么地址是頁對齊的,但這樣會限制了可移植性。實際上很少有程序真正在意映射到哪個地址上去!
?prot參數經常都同時設置了PROT_READ和PROT_WRITE位,使得映射是可讀可寫的。一塊不能讀寫的空存儲器映射是沒有用的。另外一方面,很少將可執行代碼映射到匿名映射,因為那樣做能產生潛在的安全漏洞。
?flags參數設置MAP_ANONYMOUS位,來使得映射是匿名的,設置MAP_PRIVATE位,使得映射是私有的。
?假如MAP_ANONYMOUS被設置了,fd和offset參數將被忽略的。然而,在一些更早的系統里,需要讓fd為-1,如果要考慮移植性,像例子那樣做是個挺好的主意。
由 匿名映射獲得的內存塊,看上去和由堆獲得的一樣。使用匿名映射的一個好處是,那塊內存交給你的時候,已經是全0的了。這種映射還沒有額外的負載,因為內核 使用寫時復制(copy-on-write)將內存塊映射到了一個全0的頁面上。所以沒有必要對返回的內存塊使用memset(? )。說實在的,這是使用calloc( )而不是用malloc( )后跟著memset(? )的一個好處:知道匿名映射是本來就全0的了,calloc( )用來滿足一個不能明確是全0的映射。系統調用munmap(? )釋放一個匿名映射,歸還已分配的內存給內核。
int ret;
/* all done with 'p', so give back the 512 KB mapping */
ret = munmap (p, 512 * 1024);
if (ret)
perror ("munmap");
想復習一下mmap( ), munmap( ),和一般的映射,請翻開第四章。
映射到/dev/zero
其 它Unix系統,就像BSD,并沒有MAP_ANONYMOUS標記。作為替代,它們用一個特殊的設備文件/dev/zero實現了一個類似的解決方法。 這個設備文件提供了和匿名存儲器語義上一致的實現。一個映射包含了全0的寫時復制頁面;所以行為上和匿名存儲器一樣。Linux一直有一個/dev /zero設備,可以由映射這個文件來獲得全0的內存塊。實際上,在引入之前MAP_ANONYMOUS,Linux的程序員就是這樣做的。為了對早期的 Linux版本提供向后兼容性,或者對其他Unix系統的可移植性,程序員仍然可以將映射/dev/zero作為匿名映射的替代。這個映射其他文件的映射 是不一樣的:
void *p;
int fd;
/* open /dev/zero for reading and writing */
fd = open ("/dev/zero", O_RDWR);
if (fd < 0) {
??? perror ("open");
??? return -1;
}
/* map [0,page size) of /dev/zero */
p = mmap (NULL, /* do not care where */
????????? getpagesize ( ), /* map one page */
????????? PROT_READ | PROT_WRITE, /* map read/write */
????????? MAP_PRIVATE, /* private mapping */
????????? fd, /* map /dev/zero */
????????? 0); /* no offset */
if (p == MAP_FAILED) {
??? perror ("mmap");
??? if (close (fd))
??? perror ("close");
??? return -1;
}
/* close /dev/zero, no longer needed */
if (close (fd))
??? perror ("close");
/* 'p' points at one page of memory, use it... */
在這種情況下映射的存儲器當然也是用munmap( )來取消映射的。
這種實現引入了附加的打開和關閉文件的系統調用。所以,匿名映射是個更快的解決方法。
高級存儲器分配
本章所涉及的許多存儲分配操作都是為內核的參數所控制和限制的,但程序員可以修改這些參數。要這么做,可以使用mallopt( )調用:
#include <malloc.h>
int mallopt (int param, int value);
一個mallopt( )的調用將制param確定的存儲管理相關的參數設為value。成功時,調用返回一個非0值;失敗時,返回0.要注意的是mallopt( )不設置errno。雖然它往往都成功返回,但是別過于樂觀,要好好檢查返回值。
Linux目前支持param的六個值,所有都被定義在了<malloc.h>:
M_CHECK_ACTION
環境變量MALLOC_CHECK_的值(將在下一節討論)。
系統用來滿足動態存儲器請求的最大存儲器映射數。當映射數達到了限制,數據段將被用來滿足所有的分配,知道已有的映射中的某個被取消。值為0時將禁止匿名映射用于動態存儲的分配。
M_MMAP_THRESHOLD
決定該用匿名映射還是用數據段來滿足存儲器分配請求的臨界值的大小(以字節為單位)。要注意的是,有時候系統為了慎重起見,就算是比臨界值小,也有可能用匿名映射來滿足動態存儲器的分配。值為0時會啟用匿名映射來滿足所有的分配,而不再使用數據段來滿足請求。
M_MXFAST
Fast bin的最大大小(以字節為單位)。Fast bins是堆中特殊的內存塊,永遠不和臨近的內存塊合并,也永遠不歸還給系統,以碎片的增加為代價來滿足高速的內存分配。值為0時,fasy bin將不被啟用。
M_TOP_PAD
為 適應數據段的大小而使用的填充(padding)的大小(以字節為單位)。無論何時,在使用brk( )來使數據段變大的時候,為了以后少點調用brk(? ),glibc總會請求更多的內存。相似地,但glibc收縮數據段的時候,它會保持一些多余的內存,而不是將所有的歸還給系統。這多余的部分就叫做填 充。值為0時會取消填充的使用。
???????????????????????????????????????????????? ? ?Table 8-1. mallopt( ) parameters
| Parameter | Origin | Default value | Valid values | Special values |
| M_CHECK_ACTION | Linux-specific | 0 | 0 – 2 | ? |
| M_GRAIN | XPG standard | Unsupported on Linux | >= 0 | ? |
| M_KEEP | XPG standard | Unsupported on Linux | >= 0 | ? |
| M_MMAP_MAX | Linux-specific | 64 * 1024 | >=0 | 0 disables use of mmap( ) |
| M_MMAP_THRESHOLD | Linux-specific | 128 * 1024 | >=0 | 0 disables use of the heap |
| M_MXFAST | XPG standard | 64 | 0 – 80 | 0 disables fast bins |
| M_NLBLKS | XPG standard | Unsupported on Linux | >= 0 | ? |
| M_TOP_PAD | Linux-specific | 0 | >=0 | 0 disables padding |
?
程序在使用malloc( )或其它申請動態存儲分配的接口之前是不能使用mallopt()的。用法很簡單:
/* use mmap( ) for all allocations over 64 KB */
ret = mallopt (M_MMAP_THRESHOLD, 64 * 1024);
if (!ret)
??? fprintf (stderr, "mallopt failed!\n");
===================================
memalign
在GNU系統中,malloc或realloc返回的內存塊地址都是8的倍數(如果是64位系統,則為16的倍數)。如果你需要更大的粒度,請使用memalign或valloc。這些函數在頭文件“stdlib.h”中聲明。
???? 在GNU庫中,可以使用函數free釋放memalign和valloc返回的內存塊。但無法在BSD系統中使用,而且BSD系統中并未提供釋放這樣的內存塊的途徑。
???? 函數:void * memalign (size_t boundary, size_t size)
???? 函數memalign將分配一個由size指定大小,地址是boundary的倍數的內存塊。參數boundary必須是2的冪!函數memalign可以分配較大的內存塊,并且可以為返回的地址指定粒度。
???? 函數:void * valloc (size_t size)
???? 使用函數valloc與使用函數memalign類似,函數valloc的內部實現里,使用頁的大小作為對齊長度,使用memalign來分配內存。它的實現如下所示:
???? void *
???? valloc (size_t size)
???? {
?????? return memalign (getpagesize (), size);
???? }
?
總結
以上是生活随笔為你收集整理的Nginx源码分析--数据对齐posix_memalign和memalign函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: posix_memalign
- 下一篇: strcpy,memcpy和memmov