linux系统编程---内存

147 阅读8分钟

内存分配

进程可以通过增加堆的大小来分配内存,所谓堆是一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。通常将堆的当前内存边界成为"program break"

brk()和sbrk():调整program break

改变堆的大小其实就像命令内核改变进程的program break位置一样简单。最初,program break正好位于未初始化数据段末尾之后。 在proggram break的位置抬升后,程序可以访问新分配内存区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存

操纵program break的系统调用:brk() sbrk()

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

系统调用brk()会将program break设置为end_data_segment所指定的位置。由于虚拟内存以页为单位进行分配,end_data_segment实际会四舍五入到下一个内存页的边界处

sbrk(0):返回program break的当前位置

在堆上分配内存:malloc()和free()

C语言使用malloc函数簇在堆上分配和释放内存,较之brk()和sbrk(),这些函数具备不少优点:

  • 属于C语言标准的一部分
  • 更易于在多线程中使用
  • 接口简单,允许分配小块内存
  • 允许随意释放内存块,他们被维护与一张空闲内存列表,在后续内存分配调用时循环使用

malloc()函数在堆上分配参数size字节大小的内存,并返回指向新分配内存的起始位置处的指针,其所分配的内存未经初始化

#include <stdlib.h>
void *malloc(size_t size);

由于malloc()的返回类型位void*,因而可以将其赋给任意类型的C指针。malloc()返回内存块所采用的字节对齐方式,总是适宜于高效访问任何类型的C语言数据结构

free()函数释放ptr参数所指向的内存块,该参数应该是之前由malloc()或者本章后续描述的其他内存分配函数之一所返回的地址

#include <stdlib.h>
void free(void *ptr);

一般情况下,free()并不降低program break的位置,而是将这块内存填加到空闲内存列表中,共后续的malloc()函数循环使用

  • 被释放的内存块通常位于堆的中间,而非堆的顶部,因而降低program break是不可能的
  • 它最大限度地减少了程序必须执行的sbrk()调用次数
  • 在大多数情况下,降低program break的位置不会对那些分配大量内存的程序有多少帮助。因为它们通常倾向于持有已分配内存或是反复释放和重新分配内存,而非释放所有内存后再持续运行一段时间
#include <stdio.h>
#include <stdlib.h>
#define MAX_ALLOCS 1000000

int main(int argc, char* argv[]){
	char *ptr[MAX_ALLOCS];
	int freeStep, freeMin, freeMax, blockSize, numAllocs, j;
	printf("\n");
	if(argc<3 || strcmp(argv[1], "--help")==0){
		printf("%s num-allocs block-size [step [min [max]]]\n", argv[0]);
		return 0;
	}
	numAllocs = atoi(argv[1]);
	if(numAllocs>MAX_ALLOCS){
		printf("err \n");
		return 0;
	}
	blockSize = atoi(argv[2]);
	
	freeStep = (argv>3)?atoi(argv[3]):1;
	freeMin = (argv>4)?atoi(argv[4]):1;
	freeMax = (argc>5)?atoi(argv[5]):numAllocs;
	
	if(freeMax>numAllocs){
		printf("err....1\n");
		return 0;
	}
	
	printf("initial program break: %10p\n", sbrk(0));
	
	printf("Allocating %d*%d bytes\n", numAllocs, blockSize);
	
	for(j=0; j<numAllocs; j++){
		ptr[j] = malloc(blockSize);
		if(ptr[j]==NULL){
			printf("malloc \n");
			return 0;
		}
	}
	
	printf("Program break is now:	%10p\n", sbrk(0));
	printf("Freeing blocks from %d to %d in steps of %d\n", freeMin, freeMax, freeStep);
	for(j=freeMin-1; j<freeMax; j+= freeStep)
		free(ptr[j]);
	printf("After free(), program break is: %10p\n", sbrk(0));
	return 0;
}	

malloc实现

malloc()实现,他会扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存(取决于具体实现,采用的扫描策略会有不同)。如果这一内存块的尺寸正好与要求相等,就会把它返回给调用者。

如果是一块较大的内存,那么将对其进行分割,将内存返回给调用者的同时,空闲内存保留在空闲列表中

如果空闲列表中根本找不到足够大的空闲内存块,那么malloc()会调用sbrk()以分配更多的内存。为减少对sbrk()的调用次数,malloc()并为只是严格按所需字节数来分配,而是以更大幅度(虚拟内存页大小的倍数)来增加program break,并将超出部分置于空闲内存列表

free()函数实现

当free()将内促置于空闲列表之上时,是如何知晓内存块大小的。==>当malloc()分配内存块时,会额外分配几个字节来记录存放在者块内存大小的数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度记录字节之后

内存泄漏:在编写需要长时间运行的程序(例如shell或网络守护进程)时,处于各种目的,如果需要反复分配内存,那么应该确保释放所有已经使用完毕的内存。如若不然,堆将稳定增长,直至抵达虚拟内存上限,在此之后是分配内存的任何尝试都将以失败告终,这种情况被称为“内存泄漏”

calloc

calloc()用于给一组相同对象分配内存

#include <stdlib.h>
void *calloc(size_t numitems, size_t size);

numitems指定分配对象的数量,size指定每个对象的大小,在分配了适当大小的内存块后,calloc()返回指向这块内存起始处的指针(如果无法分配内存,返回NULL)

calloc()会将已分配的内存初始化为0

struct mystruct *p;
p = calloc(100m sizeof(struct mystruct));
if(p==NULL)
	exxExit("calloc");

realloc

realloc()函数用来调整(通常是增加)一块内存的大小,此块内存是由之前malloc包中的函数所分配的

#include <stdlib.h>
void *realloc(void* ptr, size_t size);

成功,realloc()返回指向大小调整后内存块的指针

通常情况下,当增加已分配内存时,realloc()会试图去合并在空闲列表中紧随其后且大小满足要求的内存块。若原内存与堆的顶部,那么realloc()将对堆空间进行扩展。如果这块内存位于堆的中部,且紧邻其后的空闲内存空间大小不足,realloc()会分配一块新内存,并将原有数据复制到新内存

nptr = realloc(ptr, newsize);
if(nptr==NULL)
	//handle error
else 
	ptr = nptr;
//没有把realloc()的返回值直接赋给ptr,因为一旦调用realloc()失败,那么ptr会被置为NULL,从而无法访问现有内存块

内存映射

mmap()系统调用在调用进出虚拟地址空间中创建一个新内存映射。映射分为两种

文件映射: 文件映射将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中(自动)加载。这种映射也被称为基于文件的映射或内存映射文件

匿名映射:一个匿名映射没有对应的文件。相反,这种映射的分页会被初始化为0

一个进程的映射中的内存可以与其他进程中的映射共享(即各个进程的分页表条目指向RAM中相同的分页)这种行为会在两种情况下发生:

  • 当两个进程映射了一个文件的同一个区域时它们会共享物理内存的相同分页
  • 通过fork()创建的子进程会继承父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同

私有映射: 在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会再底层文件上进行。尽管一个私有映射的分页在初始时是共享的,但对映射内容所做出的变更对各个进程来讲是私有的。内核使用了写实复制技术完成这个任务。这意味着当一个进程试图写改一个分页的内容时,内核首先会为进程创建一个新分页并将需修改的分页中的内容复制到新分页中(以及调整进程的页表)

共享映射: 在映射内容给上发生的变更对所有共享同一个映射的其他进程都可见,对于文件映射来讲,变更将会发生在底层的文件上

							映射类型

变更的可见性		文件						匿名 
私有			根据文件内容初始化内存			内存分配 
共享 		内存映射I/O:进程间共享内存(IPC)	进程间共享内存 

创建一个映射

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int falgs, int fd, offt_t offset)
//addr:指定了映射被放置的虚拟地址
//length:指定了映射的字节数
//prot:位掩码,制定了施加于映射之上的保护信息(区域读写执行权限)

文件映射

1.获取文件的一个描述符,通常通过调用open()来完成

  1. 将文件描述符作为fd参数传入mmap

执行上述步骤后mmap()会将打开的文件的内容映射到调用进程的地址空间中,一旦mmap()被调用之后就能关闭文件描述符了,而不会对映射产生任何影响