Linux System Programming --Chapter Eight

内存管理

微笑一.分配动态内存的几个函数

用户空间内存分配:malloc、calloc、realloc
1、malloc原型如下:
extern void *malloc(unsigned int num_bytes);
功能:
分配长度为num_bytes字节块。
工作机制:
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。

2、calloc原型如下:
void *calloc(unsigned n,unsigned size);
功能:
在内存的动态存储区中分配n个长度为size的连续空间。

3、realloc原型如下:
extern void *realloc(void *mem_address, unsigned int newsize);
功能:
先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域,同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

注意:malloc和calloc的区别:
calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据。

realloc注意事项:
  a、realloc失败的时候,返回NULL。
  b、realloc失败的时候,原来的内存不改变,不会释放也不会移动。
  c、假如原来的内存后面还有足够多剩余内存的话,realloc的内存等于原来的内存加上剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被free掉,realloc返回新内存的地址。
  d、如果size为0,效果等同于free()。
  e、传递给realloc的指针必须是先前通过malloc(), calloc(), 或realloc()分配的。
    f、传递给realloc的指针可以为空,等同于malloc。


以上三者的事例代码如下:

#include <stdio.h>
#include <malloc.h>
#include <string.h>

int main()
{
  //最好每次内存申请都检查申请是否成功
  //下面这段仅仅作为演示的代码没有检查
char *pt1;
char *pt2;
char *pt3;

pt1 = (char *)malloc(sizeof(char)*10);
printf("pt1 = %p\n", pt1);
//以下可能会输出乱码,说明malloc分配的空间没有被初始化为0
printf("%s\n", pt1);
scanf("%s", pt1);

pt2 = (char *)calloc(10,sizeof(char));
printf("pt2 = %p\n", pt2);
//以下输出为空,说明calloc分配的空间被初始化为0
printf("%s\n", pt2); 

pt3 = (char *)realloc(pt1, sizeof(char)*20);
printf("pt3 = %p\n", pt3);
//以下输出pt1中原先的内容。
printf("%s\n", pt3);

//以下是释放申请的内存空间
free(pt2);
free(pt3);
return 0;
}


1、calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据

2、realloc是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度

3、alloca不调用free.

4、free可用于释放ptr所指向的内存。参数ptr的值必须是先前所调用的malloc(),calloc()或realloc()的返回值。也就是说,你无法通过将指针指向所分配的块的某个部门,让free释放部分内存块。



释放动态内存函数--free(void* ptr)

free的实现原理:

操作系统在调用malloc函数时,会默认在malloc分配的物理内存前面分配一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的。

那么当用户需要free时,free函数会把指针退回到这个结构体中,找到该内存的大小,这样就可以正确的释放内存了。


微笑二.对齐

说明:对于这部分,博主懂的也不是很多

数据的对齐(alignment)是指数据的地址和由硬件条件决定的内存块大小之间的关系。一个变量的地址是它大小的倍数的时候,这就叫做自然对齐(naturally aligned)。例如,对于一个32bit的变量,如果它的地址是4的倍数,-- 就是说,如果地址的低两位是0,那么这就是自然对齐了。所以,如果一个类型的大小是2n个字节,那么它的地址中,至少低n位是0。对齐的规则是由硬件引起的。一些体系的计算机在数据对齐这方面有着很严格的要求。在一些系统上,一个不对齐的数据的载入可能会引起进程的陷入。在另外一些系统,对不对齐的数据的访问是安全的,但却会引起性能的下降。在编写可移植的代码的时候,对齐的问题是必须避免的,所有的类型都该自然对齐。


微笑三.内存泄露

内存泄漏可能真正令人讨厌。下面的列表描述了一些导致内存泄漏的场景。

  • 重新赋值

    我将使用一个示例来说明重新赋值问题。

char *memoryArea = malloc(10);
char *newArea = malloc(10);

这向如下面的图 4 所示的内存位置赋值。



图 4. 内存位置
Linux System Programming --Chapter Eight_第1张图片 

memoryArea 和 newArea 分别被分配了 10 个字节,它们各自的内容如图 4 所示。如果某人执行如下所示的语句(指针重新赋值)……

memoryArea = newArea; 

  • 则它肯定会在该模块开发的后续阶段给您带来麻烦。

    在上面的代码语句中,开发人员将 memoryArea 指针赋值给 newArea 指针。结果,memoryArea 以前所指向的内存位置变成了孤立的,如下面的图 5 所示。它无法释放,因为没有指向该位置的引用。这会导致 10 个字节的内存泄漏。



    图 5. 内存泄漏
    Linux System Programming --Chapter Eight_第2张图片 

    在对指针赋值前,请确保内存位置不会变为孤立的。

  • 首先释放父块

    假设有一个指针 memoryArea,它指向一个 10 字节的内存位置。该内存位置的第三个字节又指向某个动态分配的 10 字节的内存位置,如图 6 所示。



    图 6. 动态分配的内存 
free(memoryArea)

如果通过调用 free 来释放了 memoryArea,则 newArea 指针也会因此而变得无效。newArea 以前所指向的内存位置无法释放,因为已经没有指向该位置的指针。换句话说,newArea 所指向的内存位置变为了孤立的,从而导致了内存泄漏。

每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子内存位置(在此例中为newArea),并从那里开始释放,然后再遍历回父节点。

这里的正确实现应该为:

free( memoryArea->newArea);
free(memoryArea);

返回值的不正确处理

有时,某些函数会返回对动态分配的内存的引用。跟踪该内存位置并正确地处理它就成为了 calling 函数的职责。

char *func ( )
{
		return malloc(20); // make sure to memset this location to ‘\0’…
}

void callingFunc ( )
{
		func ( ); // Problem lies here
}

在上面的示例中,callingFunc() 函数中对 func() 函数的调用未处理该内存位置的返回地址。结果,func() 函数所分配的 20 个字节的块就丢失了,并导致了内存泄漏。


微笑四.数据段的管理

Unix系统在历史上提供过直接管理数据段的接口。然而,程序都没有直接地使用这些接口,因为malloc( )和其它的申请方法更容易使用和更加强大。我会在这里说一下这些接口来满足一下大家的好奇心,同时也给那些想实现他自己的基于堆栈的动态内存申请机制的人一个参考:

#include <unistd.h>int brk (void *end);void * sbrk (intptr_t increment);

这些功能的名字源于老学校的Unix系统,那时堆和栈还在同一个段中。堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向着堆向下生长。堆和栈的分界线叫做breakbreak point。在现代的系统里面,数据段存在于它自己的内存映射,我们继续用断点来标记映射的结束地址。

一个brk( )的调用设置断点(数据段的末端)的地址为end。在成功的时候,返回0。失败的时候,返回-1,并设置errnoENOMEM

一个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填充的大内存块,随时可供你使用。因为这种映射的存储不是基于堆的,所以并不会在数据段内产生碎片。

通过匿名映射来分配内存又下列好处:


•无需关心碎片。当程序不再需要这块内存的时候,只是撤销映射,这块内存就直接归还给系统了。

•匿名存储器映射能改变大小,有着改变大小的能力,还能像普通的映射一样接收命令(看第四章)。

•每个分配存在于独立的内存映射。没有必要再去管理一个全局的堆了。

下面是两个使用匿名存储器映射而不使用堆的劣处:

•每个存储器映射都是页面大小的整数倍。所以,如果大小不是页面整数倍的分配会浪费大量的空间。这些空间更值得忧虑,因为相对于被分配的空间,被浪费掉的空间往往更多。

•建立一个存储器映射比将堆里面的空间回收利用的负载更大,因为堆可能并不包含有任何的内核动作。越小的分配,这个劣处就明显。


跟变戏法似的,glibcmalloc( ) 能用用数据段来满足小的分配,用存储器映射来满足大的分配。临界点是可被设定的(看后面的高级内存分配),也有可能一个glibc版本是这样,另外一个就不是了。目前,临界点一般是128KB:比128KB小的分配由堆实现,相应地,更大的由匿名存储器映射来实现。


微笑六.基于堆栈的分配

到目前为止,我们学过的所有的动态内存分配机制都是使堆和存储器映射来实现的。我们可能觉得这么做是理所当然的,因为堆和存储器映射天生就是动态的。程序的自动变量(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( )和变长数组会给程序引入怪异的行为。但还是好好享用它们带来的好处吧,使用其中的一个或者另外一个。



微笑八.操作内存

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的思想,搞得崩溃了,认为严格计数才是灵丹妙药。然而,应用程序常常请求分配不必要的内存导致到达了超量使用才能满足的地步,而允许这种行为原是虚拟存储的主要动机之一。


你可能感兴趣的:(c,linux,内存,存储,行业数据)