数据的对齐(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");![](https://i-blog.csdnimg.cn/blog_migrate/b22a2fde69c694ec1cf3d43c46bca5e9.jpeg)
想复习一下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");