关于linux的内存分配

转自: http://yanxiang.blog.51cto.com/2034913/684265


基于栈的分配

到目前为止,我们学过的所有的动态内存分配机制都是使堆和存储器映射来实现的。我们可能觉得这么做是理所当然的,因为堆和存储器映射天生就是动态的。程序的自动变量(automatic variables)存在于地址空间中另外一个常见的结构,栈。

无论如何,实在是没有理由不让程序员使用栈来实现动态存储器的分配。只要一个分配不溢出栈外,这样的做法是很简单而完美的。如果要在一个栈中实现动态内存分配,使用系统调用alloca( ):

#include <alloca.h>
void * alloca (size_t size);
成功的时候,一个alloca( )调用会返回一个指向size字节大小的内存的指针。这块内存是在栈中的,当调用它的函数(例如main函数)返回时,这块内存将被自动释放。alloca( )的某些实现在失败时有时返回NULL,但大部分的实现没有失败的情况,或者不报告错误。其中常见的错误是栈溢出。


用法与malloc( )一样,但你不必(实际上,是不能)释放分配到的内存。这里有一个作为样例的函数,在系统配置 目录,可能是/etc, 里面打开一个给定的文件,在编译的时候就被确定了。这个函数必须申请一个新的缓冲区,复制系统配置路径到这个缓冲区里面,然后将提供的文件名拼接到缓冲区 的后面:

int open_sysconf (const char *file, int flags, int mode)
{
const char *etc = SYSCONF_DIR; /* "/etc/" */
char *name;
name = alloca (strlen (etc) + strlen (file) + 1);
strcpy (name, etc);
strcat (name, file);
return open (name, flags, mode);
}
在open_sysconf函数返回时,从alloca( )分配到的内存随着栈的收缩而被自动释放。这意味着当调用alloca( )的函数返回后,你不能再使用由alloca( )得到的那块内存!然而,你并不需要做任何释放工作,所以最终代码会简洁一些。这个是个用malloc( )实现的一样的函数:

int open_sysconf (const char *file, int flags, int mode)
{
const char *etc = SYSCONF_DIR; /* "/etc/" */
char *name;
int fd;
name = malloc (strlen (etc) + strlen (file) + 1);
if (!name) {
perror ("malloc");
return -1;
}
strcpy (name, etc);
strcat (name, file);
fd = open (name, flags, mode);
free (name);
return fd;
}
要注意的是你不能使用由alloca( )得到的内存来作为一个函数调用的参数,因为分配到的内存块会因保留参数而存在于栈的中间。例如,下面这样做是不行的:

/* DO NOT DO THIS! */
ret = foo (x, alloca (10));
alloca( )接口有着颠簸的历史。在许多系统,它表现得比较蹩脚,或者出现没被定义的行为。在栈大小较小而且是确定的系统中,使用alloca( )很容易导致栈溢出,使你的进程终止。在另外一些系统中,alloca( )甚至就不存在。随着年月增长,易产生bug和不协调的实现给了一个Over time坏名声。

所以,如果要让代码具有可移植性,你要避免使用alloca( )。然而,在Linux里,alloca( )是一个有用得出奇且没被充分使用的工具。它表现的异常出色— 在各种架构下,通过alloca( )的分配做的就和简单的增加栈指针一样少—直接得就比malloc( )好。对于Linux下较小的内存分配,alloca( )能收获让人激动的性能。

栈中的复制串

alloca( )经常被用来暂时性地复制一个字符串。例如:

/* we want to duplicate 'song' */
char *dup;
dup = alloca (strlen (song) + 1);
strcpy (dup, song);
/* manipulate 'dup'... */
return; /* 'dup' is automatically freed */
因为这种需要非常常见以及alloca( )提供的高速,Linux系统提供了一个strdup( )的变种来将一个给定的字符串复制到栈中:

#define _GNU_SOURCE
#include <string.h>
char * strdupa (const char *s);
char * strndupa (const char *s, size_t n);
一个strdupa( )的调用返回一个s的复制品。一个strndupa( )的调用返回s的前n个字节的复制品。如果s长度比n大,就复制s前n个字节,然后后面自动加上一个NULL。这些函数有着和一样的功能,当调用它的函数返回时复制品会被自动释放。POSIX并不定义alloca( ),strdupa( ),或者strndupa( )这些函数,因为他们在别的操作系统表现得劣迹斑斑。如果要考虑可移植性,这些函数是不鼓励使用的。但是,在Linux中,alloca( )和它的亲戚们表现得相当好,能得到激动人心的性能提高,仅仅用栈桢指针的调整就能代替复杂的动态存储器分配系统。

可变长数组

C99 引进了可变长数组(VLAs),可变数组的长度是在运行时决定的,而不是在编译的时候。GNUC有时候会支持可变长数组,但和C99定义的不一样,它的用处背后有着强烈的需求。VLAs用与alloca( )很相似的方法避免了动态存储的大负载。它的使用方法就跟你想象的一样:

for (i = 0; i < n; ++i) {
char foo[i + 1];
/* use 'foo'... */
}
在这个代码片段中,foo是一个由i + 1个char的数组。在每次循环的重复中,foo被动态的产生和在离开这个作用域时。如果我们使用alloca( )来代替VLA,这块内存不会被释放知道for返回。使用一个VLA确保了内存每次循环都被释放。所以,使用VLA最多使用n个字节而,而alloca( )会使用掉n*(n+1)/2个字节。使用一个变长数组,我们能够像这样重写我们的open_sysconf( )函数:

int open_sysconf (const char *file, int flags, int mode)
{
const char *etc; = SYSCONF_DIR; /* "/etc/" */
char name[strlen (etc) + strlen (file) + 1];
strcpy (name, etc);
strcat (name, file);
return open (name, flags, mode);
}

alloca( )和变长数组的主要的不同点在于通过前者获得的内存在整个函数中都被保留着,而通过后者获得的内存除出了作用域便释放 了。这种做法好处见仁见智了。我们仅仅看for循环,在每次循环回收内存,不附带任何副作用地减少了内存的消耗(我们不再需要多余的内存放在周围了)。然 而,当由于某种原因我们想将内存保留得比单个循环长的时候,使用alloca( )会更合理。


在单个函数中混淆了alloca( )和变长数组会给程序引入怪异的行为。但还是好好享用它们带来的好处吧,使用其中的一个或者另外一个。

选择一个合适的存储器分配机制

在这张图片中讨论了无数的存储器分配选项,可能会使程序员在面对特定作业时对解决办法产生疑惑。在多数的情况下,malloc( )是你最好的选择。然而,有时候,一个不一样的实现会提供一个更好的工具。表格8-2是一个分配机制的指南。


最后,我们要记住下面的两个选项:自动和静态内存分配。在栈中分配自动变量,或者堆中全局的变量,是相对容易的,也并不要求程序员管理指针和担忧内存的释放。

存储器操控

C语言提供了一系列的函数来操作内存的原始字节序列。这些函数的行为在很多方面和strcmp( )和strcpy( )等字符串操作接口相似,但它们以用户提供的缓冲区大小来分界而不是假定字符串结尾是NULL。要注意这些函数都不会返回错误信息。防范错误是程序员的责任-传递错误的内存区域作参数的话,毫无疑问,你将得到的是段错误。

字节的设置

在一系列的内存操作函数当中,最常见而简单的是memset( ):

#include <string.h>
void * memset (void *s, int c, size_t n);
调用memset( )会将地址s为开头的n个字节设为c然后返回s。常见的用法是将一块内存全设为0:

/* zero out [s,s+256) */
memset (s, '\0', 256);
bzero( )是更早的,被淘汰的接口,BSD引入它来实现一样的功能。新的代码应该使用,但Linux为了向下兼容和对其它系统的可移植性,提供了bzero( ):

#include <strings.h>
void bzero (void *s, size_t n);
下面的调用功能和先前memset( )的例子一样:

bzero (s, 256);
注意bzero( )-和其它b系列接口-需要头文件<strings.h>而不是<string.h>。

如果你能使用calloc( )不要使用memset( )!避免用malloc( )分配内存然后马上用memset( )将它清0.同样效果,单单使用一个calloc( )比使用两个函数好得多,而返回的都是清0后的内存块。不仅是少用了一个函数,还有可能calloc( )能直接从内核获得清0后的内存。在这种情况下,你不用手工地让每一个字节设为0,从而提高了效能。

字节的比较

和strcmp( )相似,memcmp( )比较两块内存是否一样:

#include <string.h>
int memcmp (const void *s1, const void *s2, size_t n);
一个调用比较s1和s2的前n字节,如果一样返回0,如果s1小于s2返回一个负值,如果s1大于s2返回一个正值。

BSD也提供了一个现在被反对的接口执行大致一样的任务:

#include <strings.h>
int bcmp (const void *s1, const void *s2, size_t n);
一个bcmp( )调用比较s1和s2的前n字节,如果两块内存就一样返回0,否则返回非0值。因为结构填充的存在(参照这章前些的“其它和对齐有关的”),通过memcmp( )或者bcmp( )来比较两个结构是否等价是不可靠的。同一个结构的两个实例也能有未初始化的垃圾内容在填充里面。因此,下面的代码是不安全的:

/* are two dinghies identical? (BROKEN) */
int compare_dinghies (struct dinghy *a, struct dinghy *b)
{
return memcmp (a, b, sizeof (struct dinghy));
}
作为替代,程序员想要比较两个结构时就应该比较结构的每一个字段,一个一个地来。这些例子应该有些优化,但它明显地比不安全的memcmp( )实现好。下面是一个等价的代码:

/* are two dinghies identical? */
int compare_dinghies (struct dinghy *a, struct dinghy *b)
{
int ret;
if (a->nr_oars < b->nr_oars)
return -1;
if (a->nr_oars > b->nr_oars)
return 1;
ret = strcmp (a->boat_name, b->boat_name);
if (ret)
return ret;
/* and so on, for each member... */
}
移动字节

memmove( )复制src的前n字节到dst,返回dst:

#include <string.h>
void * memmove (void *dst, const void *src, size_t n);
同样,BSD提供了一个被批评的接口来实现同样的功能:

#include <strings.h>
void bcopy (const void *src, void *dst, size_t n);
注意虽然两个函数用的是同样的参数,但前两个的顺序是相反的。

bcopy( )和memmove( )在处理内存区域重叠时都是安全的(就是说,dst的一部分在src 里面)。例如,它们允许内存块在一个给定的区域内向上或下移动。由于这种情况比较少见,而且若果是这样的话,程序员应该知道。所以C标准定义了一个不支持 内存区域覆盖的memmove( )变种。这个变种可能会快一点:

#include <string.h>
void * memcpy (void *dst, const void *src, size_t n);
除了dst和 src间可能不能重叠,这个函数表现的和memmove( )一样。如真重叠了,结果是未被定义的。另外一个安全的复制函数是memccpy( ):

#include <string.h>
void * memccpy (void *dst, const void *src, int c, size_t n);
函数memccpy( )行为上和memcpy( )一样,除了函数在src的前n字节中发现了字节c。函数返回指向dst 中的c下一字节的指针,或者在没发现c的时候返回NULL。最后,你能用mempcpy( )在内存中漫步:

#define _GNU_SOURCE
#include <string.h>
void * mempcpy (void *dst, const void *src, size_t n);
函数mempcpy( )和memcpy( )一样,除了memccpy( )返回的是指向被复制的内存的最后一个字节的下一个字节的指针。当一系列的数据要复制到列需的内存区域内,这是非常有用的-但它也不算很大的提高,因为返回的仅仅是dst + n。这个函数是GNU特有的。

搜索字节

函数memchr( )和memrchr( )在一个内存块里面搜索一个给定的字节:

#include <string.h>
void * memchr (const void *s, int c, size_t n);
函数memchr( )扫描s指向的内存的前n字节看是否含有c,c将被转换为unsigned char:

#define _GNU_SOURCE
#include <string.h>
void * memrchr (const void *s, int c, size_t n);
函数返回指向第一个符合c的内容的字节的指针,或者没发现c时返回NULL。

函数memrchr( )和memchr( )一样,除了从s指向的地址它向后扫描个字节而不是向前。不像memchr( ),memrchr( )是GNU的扩展,而不是C语言的一部分。对于更加复杂的搜索,有个名字很烂的memmem( )函数在一块内存中搜索任意的一字节数组:

#define _GNU_SOURCE
#include <string.h>
void * memmem (const void *haystack,
size_t haystacklen,
const void *needle,
size_t needlelen);
memmem( )函数返回指向长为haystacklen的内存块haystack中的,第一块和长为needlelen的needle一样的子块的指针。如果函数在haystack中不能找到needle,它会返回NULL。这个函数也是个扩展。

掩盖你的字节序列

Linux的C库提供一个很一般的接口来掩盖数据:

#define _GNU_SOURCE
#include <string.h>
void * memfrob (void *s, size_t n);
一个memfrob( )的调用掩盖s开始的前n个字节,将每个字节跟42异或。函数返回s。

对同一个内存区域调用memfrob( )两次,结果就和没有调用过一样。所以,下面的代码片段是没有效果的:

memfrob (memfrob (secret, len), len);
这个函数不能算是一个合适的(甚至说差劲的)加密函数;它的用途仅仅是在字节序列上加了层薄纱。它也是GNU特有的。

内存锁定

Linux 实现了请求页面调度,页面调度是说页面从硬盘按需交换进来,当不再需要的时候交换出去。这样做允许系统中每个进程的虚拟地址空间和实际物理内存的总量再没有直接的联系,因为在硬盘上的交换空间能给进程一个物理内存几乎无限大的错觉。

交换对进程来说是透明的,应用程序一般都不需要关心(甚至不需要知道)内核页面调度的行为。然而,在下面两种情况下,应用程序可能像影响系统的页面调度:

确定性(Determinism)

时间约束严格的应用程序需要确定的行为。如果一些内存操作引起了页错误-这会导致昂贵的磁盘操作-应用程序的速度便不能达到要求,不能按时做计划中 的操作。如果能确保需要的页面总在内存中且从不被交换进磁盘,应用程序就能保证内存操作不会导致页错误,提供一致的,可确定的程序行为,从而提供了效能。

安全性(Security)

如果内存中含有私人秘密,这秘密可能最终被页面调度以不加密的方式储存到硬盘上。例如,如果一个用户的私人密钥正常情况下是以加密的方式保存在磁盘 上的,一个在内存中为加密的密钥备份最后保存在了交换文件中。在一个高度注重安全的环境中,这样做可能是不能被接受的。这样的应用程序可以请求将密钥一直 保留在物理内存上。当然,改变内核的行为会导致系统整体性能的负面影响。当页面被锁定在内存中,一个应用程序的安全性可能提高了,但这能使得另外一个应用 程序的页面被交换出去。如果内核的设计是值得信任的,它总是最优地将页面交换出去-就是说,看上去将来最不会被使用的页面-所以,当你改变了它的行为,它 必须将一个没那么适当的页面交换出内存。

锁定部分地址空间

POSIX1003.1b-1993定义两个接口将一个或更多的页面“锁定”在物理内存,来保证它们不会被交换到磁盘。第一个函数锁定给定的一个地址区间:

#include <sys/mman.h>
int mlock (const void *addr, size_t len);
一个mlock( )的调用锁定addr开始的虚拟内存,在物理内存中延伸len字节。成功的话,函数返回0;失败时,函数返回-1,并适当设置errno。

一个成功的调用会将包含[addr,addr+len)的物理内存页锁定。例如,一个调用只是指定了一个字节,包含这个字节的整个物理页都将被锁 定。POSIX标准指定addr应该与页边界对齐。但Linux并不有这个强制要求,如果真要这样做的时候,会悄悄的将addr向下拉到最近的页面。然 而,对于强调对其它系统的可移植性的程序员,需要保证addr位于页的边界。

合法的errno包括:

EINVAL
参数 len 是负数。
ENOMEM
函数要锁定的页面数比RLIMIT_MEMLOCK限制的要多(看下面一节“锁定的限制”).
EPERM
RLIMIT_MEMLOCK是0,但进程并没有CAP_IPC_LOCK权限。(再次,看“锁定的限制”)。


一个由fork( )产生的子进程并不继承锁定的内存。然而,由于Linux对地址空间的写时复制,一个子进程的页面被有效地锁定在内存中直到子进程对它们执行写操作。

作为例子,假设程序内存中包含着未加密地串。包含下面代码的进程能锁住页面:

int ret;
/* lock 'secret' in memory */
ret = mlock (secret, strlen (secret));
if (ret)
perror ("mlock");
锁定全部地址空间

如果一个进程想在物理内存中锁定它的全部地址空间,mlock( )会是个笨重地接口。对面这样一个实时应用程序中常见的意图POSIX定义了一个系统调用来锁定全部地地址空间:

#include <sys/mman.h>
int mlockall (int flags);
mlockall( )函数锁定一个进程现有地址空间中所有页面到物理内存。flags参数,是下面两个值的按位或操作,用以控制函数行为:

MCL_CURRENT
如果被设置了,会使得mlockall( )将所有已被映射的页面-栈,数据段,映射文件,等等-锁定
在进程的地址空间。

MCL_FUTURE
如果被设置了,会使得mlockall( )确保所有映射到进程地址空间的页面在将来也是锁定地。
大部分应用程序制定这两个值地按位或为参数。成功时,函数返回0;失败时,返回-1,并设置

errno为下面错误码之一:

EINVAL
参数 len 是负数。

ENOMEM
函数要锁定的页面数比RLIMIT_MEMLOCK限制的要多(看下面一节“锁定的限制”).

EPERM
RLIMIT_MEMLOCK是0,但进程并没有CAP_IPC_LOCK权限。(再次,看“锁定的限制”)。
解除内存锁定

为了解除物理内存中页面地锁定,再次允许内核将页交换到磁盘,POSIX多规范了两个接口:

#include <sys/mman.h>
int munlock (const void *addr, size_t len);
int munlockall (void);
系统调用munlock( )解除addr开始长为len的内存所在的页面地锁定。它是mlock( )的逆函数。系统调用munlockall( )是mlockall( )的逆函数。两个函数在成功时都返回0,失败时返回-1,像如下设置errno:

EINVAL
参数 len 是负数(仅对munlock( ))。
ENOMEM
被指定的页面中有些是不合法的。
EPERM
RLIMIT_MEMLOCK是0,但进程并没有CAP_IPC_LOCK权限。(看下一节“锁定的限制”)。
内存锁定并不会重叠。所以,不管被mlock( )或mlockall( )锁定了多少次,仅一个mlock( )或者munlock( ),即会解除一个页面的锁定。

锁定的限制

因为内存的锁定能影响一个系统的整体性能-实际上,如果太多的页面被锁定,内存分配会失败-Linux对于一个进程能锁定的页面数添加了限制。

拥有CAP_IPC_LOCK权限的进程能锁定任意多的页面。没有这个权限的进程只能锁定RLIMIT_MEMLOCK个字节。默认情况下,这个资源限制是32KB-足够大来将一或两个秘密信息锁定在内存中,对系统性能也没有什么负面影响。(第6章讨论资源限制和怎样找到和设置这些值)

这个页面在物理内存中吗?

为了调试,Linux提供了可以确定一个区域内的内存是在物理内存中或被交换出磁盘的mincore( )函数:

#include <unistd.h>
#include <sys/mman.h>
int mincore (void *start,
size_t length,
unsigned char *vec);
一个mincore( )的调用提供了一个向量,表明调用时刻映射中哪个页面是在物理内存中。函数通过vec来返回向量,这个向量描述start(必需页面对齐)开始长为length(不需要对其)字节的内存中的页面的情况。vec的每个字节对应指定区域内的一个页面,第一个字节对应着第一个页面,然后就这样一对一向后对应。因此,vec必须足够大来装入(length - 1 + page size) / page size字节。如果那页面在物理内存中,对应字节的最低位是1,否则是0。其它的位目前还没有定义,留待日后使用。

成功时,函数返回0。失败时,返回-1,并设置errno为如下值之一:

EAGAIN
内核目前没有足够的可用资源来满足请求。
EFAULT
参数vec指向一个非法地址。
EINVAL
参数start 不是页对齐。
ENOMEM
[address,address+1)中的内存包含着非基于文件的映射部分.
目前来说, 这个系统调用只能在基于文件的用MAP_SHARED创建的映射中正常工作。这样做很大程度上限制了这个函数的使用。

投机性存储分配策略

Linux 使用投机分配策略。当一个进程向内核请求额外的内存-就是说,扩大它的数据段,或者创建一个新的存储器映射-内核作出了分配承诺但实际上并没有分给进程任何的物理存储。仅当进程对新“分配到”的内存区域作写操作的时候,内核才履行承诺,分配一块物理内存。内核在一次一页基础上做上述工作,并在需要时进行页面的按需调度和写时复制。

这样做有着不少的优点。首先,懒惰分配允许内核推迟大部分工作直到可能的最后一刻-当实际上,它根本就不需要作出分配时。第二,由于请求是按需地一 页一页地被满足,只有物理内存是真被需要的时候才会消耗物理存储。最后,分配到的内存能比实际的物理内存甚至比可用的交换空间多得多。最后的特征叫做超量 使用(overcommitment)。

超量使用和内存耗尽

和页面请求于分配时就划出物理存储相比,在使用时刻才分配物理存储的超量使用允许系统运行更多,更大的应用程序。若果没有超量使用,用写时复制映射 2GB文件需要内核划出2GB的物理存储。若有超量使用,映射2GB文件需要的存储量仅仅是进程映射区域中真正作写操作的所有页面的大小。相似的,没有超量使用,就算大多数页面都没有被写时复制复制过,也需要一样多的空闲存储来复制一模一样的地址空间。

但是,如果系统中所有进程尝试使用的是比物理内存和交换空间加起来还多但又是内核已承诺分配的内存呢?在这种情况下,一个或者更多的分配一定会失 败。因为内核已经承诺给予进程内存了-系统调用成功返回-而这个进程尝试使用分到的内存,内核只能杀死另一个进程并释放它的空间,才能再次拥有资源去履行 这个分配承诺。

当超量使用导致内存不足以满足一个承诺时,我们就说发生了内存耗尽(OOM)(out of memory)。为了处理OOM,内核使用OOM killer来挑选并终止一个进程。为了这样做,内核尝试选出一个最不重要且又占用很多内存的进程。

OOM其实很少出现-所以具有巨大效用的超量使用最有实际意义的。然而,可以肯定的是,OOM这种情况谁也不想看到,而且进程突然被OOM killer终结了是往往不能被接受的。

对于不想这种情况出现的系统,内核允许通过文件/proc/sys/vm/overcommit_memory关闭超量使用,和这作用相似的还有sysctl的vm.overcommit_memory参数。

参数的默认值是0,告诉内核执行适度的超量使用策略,在合理范围内实施超量使用,但在超过一定值时就不使用了。值为1时,答应所有的分配承诺,将一 切顾虑抛诸脑后。一些存储敏感的应用程序,例如在科学领域,倾向于请求比他们实际需要的更多的内存,这时这个参数值就显得很有意义。

当值为2时,关闭所有的过量使用,启用严格计数(strict accounting)策略。这个模式中,承诺的内存大小被严格限制在交换空间大小加上可调比例的物理内存大小。这个比例可以在文件/proc/sys/vm/overcommit_ratio里面设置,作用和vm.overcommit_ratio的sysctl参数相似。 默认是50,限制承诺的内存总量是交换空间加上物理内存的一半。因为物理内存还必须包含着内核,页表,系统保留页,锁定页等等东西。仅它的一部分能被交换和满足承诺请求。

小心使用严格计数!许多系统设计者,被OOM killer的思想,搞得崩溃了,认为严格计数才是灵丹妙药。然而,应用程序常常请求分配不必要的内存导致到达了超量使用才能满足的地步,而允许这种行为原是虚拟存储的主要动机之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值