内存管理与分页机制

一、问题提出:

我们经常会使用malloc()以及free()函数进行堆区内存申请与释放。那么你是否会这样做:

int * p = malloc(0);/*malloc分配了0个字节吗,如果是那么p指向谁呢,是NULL吗*/
free(p);/*假如malloc分配了0个字节,p指向了NULL,那么free(NULL)不会出现段错误吗*/

我想很少有人这样做,因为除了喜欢“打破砂锅问到底”,或者经常使用测试一些特例的方法去学习的的人,一般人不会注意到这个问题到底是怎样的结果。

我们可以做一个简单的测试:

/*****************************
**
**2016年12月25日16:09:44
**测试环境:Redhat 6.4
**测试int * p = malloc(0);p是否指向NULL
**
*****************************/
#include<stdio.h>
#include<stdlib.h>
int main(void){
    int * p = (int *)malloc(0);
    printf("%d,%d\n",*(p),*(p+1024));
    free(p);
    int * q = NULL;
    printf("%d,%d\n",*(q),*(q+1024));
    return 0;
}

这里写图片描述

在测试中我们可以看到,q指针指向NULL,所以对其取值会发生段错误,而对于p来说,虽然它申请了0字节的空间,但是free()释放以及取值时都不会发生段错误(读者可以拆开测试,否则有人会怀疑是free()引发的的段错误,而不是*q取q值时引发的段错误)。由此我们可以得知,malloc(0)分配的不是0个字节,p也不是指向NULL。那malloc(0)分配了几个字节?并且为什么*(p+1024)也不会越界发生段错误呢?这就是内存的分页机制与内存管理所决定。

二、虚拟内存(Virtual Memory)与物理内存(Physical memory):

1、内存类型细分:

内存由于用途不同,分类也不尽相同,一般我们对于内存的分类也就这几种:栈区(stack area)、堆区(heap area)、全局区(静态区)(存放全局变量与静态变量static)、BSS段(存放未初始化的全局变量,未初始化的全局变量默认值为0)、文字常量区、数据区(data area)、代码区(code area)等。

关于BSS段存储的未初始化全局变量的值,我们可以测试一下,如下(i为未初始化的全局变量,其值为0):
这里写图片描述

而关于这些不同类型的内存地址区域,其所在位置如下图所示:
这里写图片描述

2、Linux内存分配时的maps文件:

关于上面所讲内存划分的各段地址位置关系,我们可以用程序进行测试:

# include<stdio.h>
# include<stdlib.h>
int num1;/*BSS段*/
int num2 = 2;/*全局区*/
char * str1 = "str1";/*文字常量区*/
int main(void){
    printf("%d\n",getpid());/*获取当前进程id号*/

    int num3 = 3;/*栈区*/
    static int num4 = 4;/*全局区*/
    const int num5 = 5;/*栈区*/
    char * str2 = "str2";/*文字常量区*/
    char str3[] = "str3";/*栈区*/
    int * p = malloc(sizeof(0));/*&p在栈区,p在堆区*/

    printf("num1:%p\nnum2:%p\nnum3:%p\nnum4:%p\nnum5:%p\n",&num1,&num2,&num3,&num4,&num5);
    printf("str1:%p\nstr2:%p\nstr3:%p\n",str1,str2,str3);
    printf("&p:%p\np:%p\n",&p,p);

    while(1){}/*死循环以保证进程不会结束,方便查看/proc/pid/maps文件*/

    free(p);

    return 0;
}

我们可以查看/proc/pid/maps文件(pid表示以进程id号命名的文件名),其中有该pid的内存分配的详细情况。注意:proc下各个进程目录占磁盘大小都是0(读者可自行测试),因为其数据都存在于内存,该文件只是一个映射。实际不存在,如果该进程消亡,pid这个目录及其子目录将会消失。所以可以用循环测试,并且maps文件中的内存地址为已经映射了物理内存的虚拟内存地址。我们先运行程序,如下所示(获得当前进程pid为5052):

这里写图片描述

我们可以”vim /proc/5052/maps”查看该文件下的内存分配情况
cd proc/5052:
这里写图片描述
vim maps:

这里写图片描述

3、内存地址映射关系:

每个进程都先天设定了4G的虚拟内存地址(不是真实的地址,只是一个编号)。虚拟内存开始时不对应任何内存,直接使用会引发段错误,不进入内核就接触不到物理内存地址,只会接触到虚拟内存地址。虚拟内存地址必须映射物理内存(或者硬盘上的文件)以后才能存储数据(数据存储在物理内存上,打印地址为虚拟内存地址)。而内存分配其实就是虚拟内存地址映射物理内存的过程,内存回收则是解除映射关系的过程。
虚拟内存中,0~3G是用户控制,3~4G是内核空间。用户层不能直接访问内核层,可以通过Unix/Linux的系统函数访问内核层。我们通常所讲内存地址,其实都不是真正意义上的物理内存(PC机上内存硬件)的地址,而是虚拟内存地址。两个不同的进程,当其某个变量地址一样(虚拟),但是物理地址并不一样。

映射关系如图所示(A、B进程均已映射物理内存,而C进程未映射物理内存,注意:虚拟内存一般并不会全部映射):

这里写图片描述

对于不同进程的同一地址,是虚拟地址而不是物理地址我们可以做个测试:
这里写图片描述

由于两个不同进程有各自的虚拟内存,打印的进程1的内存地址为虚拟内存地址,而进程2的相同的虚拟内存地址,不能操作进程1的虚拟内存地址已映射的物理内存地址,并且进程2的*p并没有映射物理内存地址,所以进程2运行出现段错误。

三、内存分页机制(Memory Paging Mechanism)与malloc详解:

1、内存管理页机制:

最小存储单位是一个字节(1B),最小管理单位是一页(4KB),虚拟内存地址连续时物理内存地址可以不连续,即使一次分配6000字节(不到两页也分配两页),两个内存页物理地址可能不挨着。多次申请内存时,如果之前分配的页内存没用完,则不再分配,除非之前分配的内存页用完才继续映射新的一页。getpagesize()可以获取当前内存页的大小。硬盘也是如此(硬盘上称为Block块):即使一个.txt文件中只有一个“a”字母,其大小为1B而其占用大小为4K。

如图所示:test.txt文件中仅仅有14个’a’字符,但是现实其占用磁盘大小仍然是4K(一页)
这里写图片描述

Windows下也有相同的机制(文件大小小于实际占用空间大小,占用大小是磁盘分块单位的整数倍):
这里写图片描述

2、为什么要有这种机制(一次性最少分配1页(4K))?

一句话:为了方便管理。
不可能进程每次申请一次系统就需要向其分配一次。(就像你和弟弟管妈妈要1块钱买辣条,你妈妈给了你俩十块钱说:“一周内都不要给我再要”,其实就算你一周内再向她要,妈妈也会给你,她只是不想你们俩不停地要而已,这就是管理(只不过我管我妈要1块,她好像给我5毛钱…….))。系统也是这样,它一次分配至少一页,在你(进程)没用完之前它都不会再给你分配,而当你用完分配的内存之后,就需要重新分配了。

就拿malloc来说,第一次malloc(0)时一次性映射33个内存页(Redhat6.4),关于这点我们测试一下:

这里写图片描述

只malloc()了一次,分配了33页,对前33页操作不会出错,但是一超过33页(p相对位置不为0,p+33*1024为虚拟地址的第34页)就产生了段错误,因为超过的虚拟内存地址并没有映射(分配)物理内存。

3、malloc(0)分配了多少内存?

例如:malloc(sizeof(int))申请了4字节,系统却给它33页,而malloc()给变量分配给变量内存时,除了数据区域外,还额外需要保存一些信息。底层有一个双向链表保存额外信息。malloc()给指针了12个字节,其中4个字节存放数据,另外8个存放其他信息或者空闲,如果将12个字节中前(低位)几个字节清空或者进行修改,free就可能出错,因为free只有首地址不能释放,还得需要额外附加信息(如malloc分配的长度)。(低八位是附加数据,高四位是int型数据)

就拿我们测试内存划分时的例子来说(仅借用地址划分关系,程序不同):
这里写图片描述

p申请了0字节,但是系统分配了0X08fa9000~0X08fca000(共0X08fca000-0X08fa9000=21000H=(2^17+2^12)Byte=(2^7+2^2)KB=(2^5+1)页=33页)

这里写图片描述

而p指向的地址为0X08fa9008(偏移了8个字节),直接指向高四位的4个字节(共12字节)。
这里写图片描述
如果我们将低八位的数据进行清空或者修改(修改任意个字节),free就有可能失败,测试如下:
这里写图片描述
将p的低四位数据清零之后,附加信息出错,free失败,出错结果如下:
这里写图片描述

  • 17
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
在Windows操作系统中,内存分页机制是一种虚拟内存管理技术,用于将物理内存与进程的虚拟地址空间进行映射和管理。内存分页机制的核心概念是将进程的虚拟地址空间划分为固定大小的页面(通常为4KB),并将其映射到物理内存上。 Windows使用了两级的页表结构来实现内存分页机制: 1. 页面目录表(Page Directory Table):页面目录表是一个固定大小的数据结构,用于存储指向页面表的指针。每个页面目录表项(Page Directory Entry)对应着一个页面表。页面目录表通常在系统启动时创建,并且对于每个进程都有一个独立的页面目录表。 2. 页面表(Page Table):页面表是一个固定大小的数据结构,用于将虚拟地址映射到物理地址。每个页面表项(Page Table Entry)包含了虚拟地址与物理地址的对应关系。页面表通常在进程创建时动态生成,并且只包含当前进程所需的页面映射。 通过这两级的页表结构,Windows可以实现虚拟地址到物理地址的映射。当进程访问虚拟地址时,操作系统会根据页表将其转换为物理地址,并进行相应的读取或写入操作。如果所需的页面不在物理内存中,则会触发页面错误(Page Fault),操作系统会将页面从磁盘加载到物理内存中,并更新页表的映射关系内存分页机制的好处是可以实现虚拟内存的管理和保护。每个进程都有独立的虚拟地址空间,使得进程之间的内存不会相互干扰,同时允许操作系统灵活地分配和回收物理内存。此外,内存分页机制还支持内存权限控制、页面共享和延迟加载等功能,提高了系统的安全性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值