Linux系统编程学习笔记(七)内存管理

原帖地址:http://fuliang.javaeye.com/blog/657650

 

内存管理: 
对于一个进程来说,内存是最基本的也是最重要的资源之一。内存管理包括:内存分配、内存操作和内存释放。 
1、进程地址空间: 
Linux将物理内存虚拟化,内核为每一个进程维护一个特殊的虚拟地址空间。这个地址是线性的,从0开始, 
到某个最大值。 
1)页和页面调度 
虚拟地址空间由很多页组成。系统的体系结构和机型决定了页的大小,典型的页大小包括4kb(32位系统) 
和8k(64位系统)。每个页面都只有无效和有效两种状态:一个有效的页面和一个物理页或者一个二级存储 
介质相关联(一个交换分区或者一个一盘文件),一个无效页没有关联,代表没有分配或者使用。地址空间 
无须是连续的,虽然是线性编址,但是中间也有很多未编址的小区域。 
2)共享和复制 
虚存中的多个页面,甚至是属于不同进程的虚拟地址空间,也有可能被映射到同一个物理页面。这样允许不同 
的虚拟地址空间共享物理内存的数据。 
另一种情况是MMU会截取这次写操作并产生一个异常;作为回应,内核会透明的创造一份这个页的拷贝以供该 
进程进行写操作,这种方式被称为写时拷贝。 
3)内存区域 
每一个进程都有以下区域: 
1)文本区域:包含了程序的代码,字符串,常量和一些只读数据。在Linux中,文本段被标示为只读,并且直接 
从目标文件映射到内存。 
2)栈段(stack segment)包括了一个进程的执行栈,随着栈的深度动态的增长和收缩。包括程序的局部变量和函数的返回值。 
3)数据段,又叫堆,包含了一个进程动态存储空间。它的大小可以变化,是有malloc动态申请,free动态释放的。 
4)bss段(bss segment)包含了没有被初始化的全局变量。这些变量根据不同的c标准都有相应的初始值。 
Linux从两个方面优化:首先,这个段是用来存放没有初始化的数据,所以连接器实际并不会将特殊的值存储在对象文件, 
这样可以减少二进制代码文件的大小。其次,当这个段被加载到内存时,内存只需要简单的根据写时复制的原则将它们 
映射到一个全是0的页上,这样非常高效的设置了初始值。 
5)很多地址空间包含多个映像文件,比如可执行文件的本身、c和其他的链接库,还有数据文件。可以查看/proc/self/maps 
或者pmap的输出查看进程的映像文件。 
2、动态申请内存: 
内存可以通过自动变量或者静态变量获得,但是所有的内存管理系统的基础都是动态内存的分配、使用和最终的释放。 
动态内存是在进程运行时才分配的,而不是在编译时就分配好了,分配的大小也只有在分配时才确定。 
1)C中最经典的为获得动态内存的接口是malloc: 

Cpp代码  
  1. #include <stdlib.h>   
  2.   
  3. void  *malloc( size_t  size);  

成功时,malloc会得到size大小的内存区域,并返回一个指向这部分内存首地址的指针。这块内存区域内容未定义,不要自认为  
全是0,失败是返回NULL,并设置errno错误值为ENOMEM。  
例子:  
Cpp代码  
  1. char  *p;  
  2.   
  3. p = malloc(2048);  
  4. if (!p)  
  5.     perror("malloc" );  

或者这样的:  
Cpp代码  
  1. struct  treasure_map *map;  
  2.   
  3. map = malloc(sizeof ( struct  treasure_map));  
  4. if (!map)  
  5.     perror("malloc" );  

每次调用时,c会自动把返回值由void指针转换为需要的类型,但是c++不提供这种自动转换。  
因而需要使用者强制转换。  
2)数组分配:  
当所需分配的内存大小本身是可变时,动态内存将更复杂。为数组分配动态内存是一个很好的例子:  
数组元素的大小已经确定,但是元素的个数是变化的,为了处理这种情况,c提供了一个calloc函数:  
Cpp代码  
  1. #include <stdlib.h>   
  2.   
  3. void  *calloc( size_t  nr, size_t  size);  

成功返回指向一个可以存储下整个数组的内存(nr个元素,每个size个字节),  
失败返回NULL,并设置errno为ENOMEM。  
以下是使用malloc和calloc申请方法:  
Cpp代码  
  1. int  *x, *y;  
  2. x = malloc(50 * sizeof ( int ));  
  3.   
  4. if (!x){  
  5.     perror("malloc" );  
  6.     return  -1;  
  7. }  
  8.   
  9. y = calloc(50,sizeof ( int ));  
  10. if (!y){  
  11.     perror("calloc" );  
  12.     return  -1;  
  13. }  

但这两个函数的行为是有区别的,与malloc不同的是,calloc将分配的区域全部初始化为0。  
memset可以用指定得值填充指定的内存块,但calloc会更快,因为内核可以提供本已清0的  
内存块。  
3)调整已存在内存大小:  
C语言提供了一个接口来改变已经得到的动态内存大小:  
Cpp代码  
  1. #inlcude <stdlib.h>   
  2.   
  3. void  *realloc( void  *ptr,  size_t  size);  

成功调用realloc将ptr指向的内存区域的大小为size字节。它返回一个指向新空间的指针,当  
试图扩大内存空间时,由于不能在已有的空间增加size大小,返回的指针可能不再是ptr,这需要  
申请另外一块size大小的空间,将原本的数据拷贝到新空间中,然后再将旧的空间释放,所以  
可能相当耗时。  
如果size是0,效果就跟ptr上调用free相同。  
如果ptr是NULL,则效果和malloc一样,如果ptr是非NULL的,那么它必须是malloc,calloc和  
realloc之一的返回值。  
失败时返回NULL,并设置errno为ENOMEM,这时ptr指向的内存区域没有改变。  
Cpp代码  
  1. p = calloc(2, sizeof ( struct  map));  
  2. if (!p){  
  3.     perror("calloc" )  
  4.     return  -1;  
  5. }  
  6. /**使用p[0],p[1]**,使用完想复用p的内存空间**/   
  7.    
  8. r = realloc(p,sizeof ( struct  map));  
  9. if (!r){  
  10.     perror("realoc" );  
  11.     return  -1;  
  12. }  
  13. free(r);  

3、动态内存的释放:  
自动内存分配,当栈不在使用,空间会被自动释放,而动态内存空间需要显示的释放,否则会一直占用进程的地址  
空间,导致内存泄露。  
当通过malloc、calloc、realloc分配到的内存不再使用的时候,需要使用free来将内存归还给系统:  
Cpp代码  
  1. #include <stdlib.h>   
  2.   
  3. void  free( void  *ptr);  

ptr可能是NULL,但是free这时候什么不做就返回了,不需要调用free之前检查ptr是否为NULL。  
4、对齐:  
数据对齐是指数据地址和由硬件确定的内存块之间的关系,一个变量的地址是它大小的整数倍,就叫做自然对齐。  
如果一个32bit长的变量,它的地址是4(字节)的整数倍,那么这就是自然对齐了。所以一个大小为2n个字节的  
变量,那么它的地址中,至少低n为是0,才能自然对齐。  
对齐是由硬件规定的,载入一个没有对齐的数据可能导致处理器错误或者性能的下降,在编写可移植的代码时,应  
注意对齐问题。  
在大多数情况下,编译器和c库会自动处理对齐问题。POSIX规定通过malloc、calloc、realloc返回的内存对于c中  
的标准类型应该是对齐的,在linux中,这些函数返回的地址在32位系统中以8字节为边界对齐,64为则以16字节为  
边界对齐。  
有时候,对于更大的边界,例如页面,程序员需要动态的对齐。POSIX提供了一个叫posix_memalign的函数:  
Cpp代码  
  1. #include <stdlib.h>   
  2.   
  3. int  posix_memalign( void  **memptr,  size_t  alignment,  size_t  size);  

成功返回0,并且保证按照alignment对齐。参数aligment必须是2的整数幂和void指针大小的倍数。  
失败返回:  
EINVAL:参数不是2的幂,或者不是void指针的整数倍。  
ENOMEM:没有足够的内存。  
例子:  
Cpp代码  
  1. char  * buf;  
  2. int  ret;  
  3. ret = posix_memalign(&buf,256,1024);//申请1kb,256-byte边界对齐   
  4. if (ret){  
  5.     fprintf(stderr,"posix_memalign: %s/n" ,strerror(ret));  
  6. }  
  7. free(buf);  

其他对齐问题:  
比如复杂的类型,不同类型的指针进行赋值以及强制类型转换。  
使用时可以按照下列四条规则:  
1、一个结构的对齐要求和它的成员中最大的那个类型是一样的。  
2、结构体也引入对填充的需求,以此来保证每一个成员都符合各自对齐要求。  
如果一个char后跟着一个int,编译器会自动插入3个字节作为填充来保证int  
是4字节对齐的。程序员应该注意成员变量的顺序,来减少填充导致的空间浪费,比如  
可以按照成员变量的大小顺序来排序。gcc编译加入-Wpadded选项可以帮助你应付这个问题。  
3、一个联合的对齐和联合最大的类型一致。  
4、一个数组的对齐和数组元素类型一致。  
指针从一个较少的字节对齐强制转化成一个较多字节的对齐类型,当通过这样的指针访问时,  
会导致处理器不能对较多字节类型的数据正确对齐。比如:  
Cpp代码  
  1. char  greeting[] =  "Ahoy Matey" ;  
  2. char  * c = greeting[1];  
  3. unsigned long  badnews = *(unsigned  long  *) c;  

后果不同的系统各部相同,小则性能损失,大则整个程序崩溃。在发现不能处理对齐错误的体系  
结构中,内核向出问题的进程发送SIGBUS信号来终止进程。  

5、数据段的管理:  
linux提供了直接管理数据段的接口,然而由于malloc和其他的方法更强大和易于使用,大多数程序  
都不会使用这些结构,但是如果你想基于堆的动态分配机制,可以使用这些接口。  
Cpp代码  
  1. #include <unistd.h>   
  2.   
  3. int  brk( void  *end);  
  4. void  *sbrk( intptr_t  increment);  

堆中动态存取器的分配有数据段底部向上生长,栈从数据段的顶部向下生长。堆和段之间的分界线  
叫做中断(break)或者中断点(break point)。  
调用brk会设置中断点(数据段的末端)地址为end。成功返回0,失败返回-1,并设置errno为ENOMEM。  
sbrk可以将数据末端增加increment字节,increment可正可负。  
POSIX和C没有定义这些函数,但几乎所有的UNIX系统都至少支持其中之一。  

6、变长数组:  
C99引入了变长数组,变长数组的长度在运行时决定,而不是编译的时候:  
Cpp代码  
  1. void  fun( int  size){  
  2.     char  foo[size];  
  3.     /* do something with foo */   
  4. }  

foo被动态的创建,并且在作用域之外自动释放。  
Cpp代码  
  1. int  open_sysconf( const   char  *file,  int  flags,  int  mode){  
  2.     const   char  *etc = SYSCONF_DIR;  
  3.     char  name[strlen(etc) + strlen(file) + 1];  
  4.     strcpy(name,etc);  
  5.     strcat(name,file);  
  6.     return  open(name,flags,mode);  
  7. }  

7、存储器操作:  
C语言提供了很多函数进行内存操作。这些函数的功能和字符串操作函数(strcmp,strcpy)类似,但他们处理的对象  
是用户提供的内存区域,而不是字符串。这些函数不返回错误信息,因此防范错误时程序员的责任。  
1)字节设定:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memset( void  *s,  int  c,  size_t  n);  

将从s指向区域开始的n个字节设置为c,并返回s,它经常被用于将一块内存清零:  
Cpp代码  
  1. memset(s, '/0' ,256);  

calloc从内存中获得一个已经清零的内存,效果会更好。  
2)字节比较:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. int  memcmp( const   void  *s1,  const   void  *s2,  size_t  n);  

比较s1和s2的头n个字节,如果内存相同则返回0,如果s1 < s2则返回小于0的数,否则返回大于0的数。  
3)字节移动:  
memmove复制src的前n个字节到dst,返回dst:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  * memmove( void  *dst,  const   void  * src,  size_t  n);  

memmove可以安全的处理内存区重叠的问题。  
C标准定义了一个不支持内存区域重叠的memmove的变种,效率可能更高一些:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memcpy( void  *dst,  const   void  *src,  size_t  n);  

如果重叠,结果未定义。  
另一个安全的复制函数memccpy():  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memccpy( void  *dst,  const   void  *src,  int  c,  size_t  n);  

这个函数和memcpy类似,但如果它在src的前n个字节发现c,那么就停止拷贝,返回指向dst中c后一个字节的指针,  
或者没有找到c时返回NULL。  
Cpp代码  
  1. #include <string.h>   
  2. #include <stdio.h>   
  3.   
  4. int  main(){  
  5.     char  a[] =  "string[a]" ;  
  6.     char  b[] =  "string[b]" ;  
  7.     memccpy(a,b,'b' , sizeof (b));  
  8.     printf("memccpy():%s/n" ,a);  
  9. }  

mempcpy:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *mempcpy( void  *dst,  const   void  *src,  size_t  n);  

和memcpy功能类似,但是返回的是被复制内存的最后一个字节的下一个字节指针。当在内存中有连续的  
一系列数据需要拷贝时比较有用。  
4)字节搜索:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memchr( const   void  *s,  int  c,  size_t  n);  

函数返回指向第一个匹配c的字节指针,如果没有找到c则返回NULL。  
memrchar和memchr类似,只是从反向搜索:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memrchr( const   void  *s,  int  c,  size_t  n);  

memrchr是GNU的扩展函数,不是C语言的一部分。  
在一块内存中搜索任意的字节数组:  
Cpp代码  
  1. #include <string.h>   
  2.   
  3. void  *memmem( const   void  *haystack,  size_t  haystacklen,  const   void  *needle,  size_t  needlelen);  

这个函数名字比较烂,它在指向长度为haystacklen的内存块haystack中查找,并返回第一块和长为needlelen  
匹配的子块指针。如果找不到,返回NULL,这个同样是GNU的扩展函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值