五、内存结构

本文详细解析了虚拟内存、物理内存(包括半导体和换页文件)的概念,阐述了进程映射中的内存壁垒,并介绍了内存分配与释放的原理,如sbrk、brk和mmap/munmap函数。同时涵盖了内存映射标志、堆和栈的动态调整以及映射到文件的操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.虚拟内存、物理内存、半导体内存和换页文件

虚拟内存:地址空间,虚拟的存储区域,应用程序所访问的都是虚拟内存。
物理内存:存储空间,实际的存储区域,只有系统内核可以访问物理内存。
物理内存包括半导体内存和换页文件(磁盘)两部分。

虚拟内存和物理内存之间存在对应关系,当应用程序访问虚拟内存时,系统内核会依据这种对应关系找到与之相应的物理内存。上述对应关系存储在内核中的内存映射表中。

当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存中缓存到换页文件中,这叫页面换出,一旦需要使用被换出的代码和数据,再把它们从换页文件恢复到半导体内存中,这叫页面换入。因此,系统中的虚拟内存比半导体内存大得多。

2. 进程映射(Process Maps)

每个进程都拥有独立的4G字节的虚拟内存,分别被映射到不同的物理内存区域。 内存映射和换入换出都是以页为单位,1页=4096字节(byte)。

4G虚拟内存中高地址的1G被映射到内核的代码和数据区,这1个G在各个进程间共享。用户的应用程序只能直接访问低地址的3个G虚拟内存,因此该区域被称为用户空间,而高地址的1个G虚拟内存则被称为内核空间。用户空间中的代码只能直接访问用户空间的数据,如果要想访问内核空间中的代码和数据必须借助专门的系统调用完成。

4G虚拟内存划分为如下区域: modify by Helpsen

可通过size命令查看一个可执行程序的代码区、数据区和BSS区的大小。(单位为字节,dec十进制表示文件总大小,hex十六进制表示文件总大小)
在这里插入图片描述

每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,可称为进程间的内存壁垒。
作如下实验:

#include <stdio.h> int g_vm = 0; int main(void) {
    printf("虚拟内存地址:%p\n", &g_vm);//%*c表示接着读一个字符并且丢弃,
    									//这里把缓冲区里的回车符给丢弃了,防止下次读入错误
    printf("输入一个整数:");
    scanf("%d%*c", &g_vm);
    printf("启动另一个进程,输入不同数据,"
        "按<回车>键继续...");
    getchar();
    printf("虚拟内存数据:%d\n", g_vm);
    return 0; 
    }

结果如下:不同进程间即使虚拟地址一样,但所映射的物理地址不一样。所有两者之间不会产生干扰。 在这里插入图片描述

3.内存的分配与释放

层层调用关系如下:
malloc/calloc/realloc/free(标准C库的malloc函数族)—>brk/sbrk(unistd.h的)—>mmap/munmap(实现映射的)—>kmalloc/kfree
(内核级别的)

以增加方式分配或释放虚拟内存

分配:映射 + 占有
映射:在地址空间(虚拟内存)和 存储空间(物理内存)之间建立映射关系
占有:指定内存空间的归属性

释放:放弃占有 + 解除映射
放弃占有:解除对内存空间的归属约束
解除映射:消除地址空间(虚拟内存)和存储空间(物理内存)之间的映射关系

sbrk函数

#include <unistd.h>
void* sbrk(intptr_t increment);
成功返回调用该函数之前的堆顶指针,失败返回-1。
increment > 0 - 堆顶指针上移,增大堆空间,分配虚拟内存
increment < 0 - 堆顶指针下移,缩小堆空间,释放虚拟内存
increment = 0 - 不分配也不释放虚拟内存,仅仅返回当前堆顶指针
系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。

#include <stdio.h>
#include <unistd.h> 
int main(void) {
	setbuf(stdout, NULL);//为了避免堆内申请干扰,取消标准IO的缓存区,因为printf的缓冲区也是从堆里申请的
    int* p1 = (int*)sbrk(sizeof(int));
    if (p1 == (int*)-1) { 
        perror("sbrk");
        return -1;
    }
    *p1 = 0;
    printf("%d\n", *p1);

    double* p2 = (double*)sbrk(sizeof(double));
    if (p2 == (double*)-1) {
        perror("sbrk");
        return -1;
    }
    *p2 = 1.2;
    printf("%g\n", *p2);

    char* p3 = (char*)sbrk(256 * sizeof(char));
    if (p3 == (char*)-1) {
        perror("sbrk");
        return -1;
    }
    sprintf(p3, "Hello, World!");
    printf("%s\n", p3);

    //释放内存,通过传入所有申请内存大小之和的相反数
    if (sbrk(-(256 * sizeof(char) +
                     sizeof(double) +
                     sizeof(int))) == (void*)-1) {
        perror("sbrk");
        return -1;
    }
    
    
    return 0; 
   } 

brk函数

以绝对地址的方式分配或释放虚拟内存(即根据参数与栈顶的位置关系决定分配还是释放)
#include <unistd.h>
int brk(void* end_data_segment);
成功返回0,失败返回-1。
end_data_segment > 当前堆顶,则分配虚拟内存
end_data_segment < 当前堆顶,则释放虚拟内存
end_data_segment = 当前堆顶,则空操作
系统内核维护一个指针,指向当前堆顶,brk函数根据指针参数end_data_segment设置堆顶的新位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。

#include <stdio.h>
#include <unistd.h> 
 int main(void) {
    setbuf(stdout, NULL);//为了避免堆内申请干扰,取消标准IO的缓存区,因为printf的缓冲区也是从堆里申请的
    int* p1 = (int*)sbrk(0);//先拿到堆顶的地址
    if (p1 == (int*)-1) {
        perror("sbrk");
        return -1;
    }
    double* p2 = (double*)(p1 + 1);
    if (brk(p2) == -1) {
        perror("brk");
        return -1;
    }
    *p1 = 0;
    printf("%d\n", *p1);
    char* p3 = (char*)(p2 + 1);
    if (brk(p3) == -1) {
        perror("brk");
        return -1;
    }
    *p2 = 1.2;
    printf("%g\n", *p2);
    void* p4 = p3 + 256;
    if (brk(p4) == -1) {
        perror("brk");
        return -1;
    }
    sprintf(p3, "Hello, World!");
    printf("%s\n", p3);
    if (brk(p1) == -1) {
        perror("brk");
        return -1;
    }
    return 0; } 

mmap函数

建立虚拟内存到物理内存文件的映射
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void*类型的-1)
前4个参数处理物理内存,后2个参数处理文件

start - 选择要映射区虚拟内存的起始地址,NULL表示自动选择(NULL安全) ,若自己指定也是按换页的大小4096的整数倍(向下取整)来做

length - 映射区的字节数,自动按页取整

prot - 访问权限,可取以下值:
PROT_READ - 可读
PROT_WRITE - 可写
PROT_EXEC - 可执行
PROT_NONE - 不可访问

flags - 映射标志,可取以下值:
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存,函数的最后两个参数fd和offset被忽略
MAP_PRIVATE - 私有映射,将虚拟内存映射到文件的内存缓冲区中而非磁盘文件(此时有内存壁垒)
MAP_SHARED - 共享映射,将虚拟内存映射到磁盘文件中 (此时没有内存壁垒)
MAP_DENYWRITE - 拒写映射,文件中被映射区域不能存在其它写入操作
MAP_FIXED - 固定映射,若在start上无法创建映射,则失败(无此标志系统会自动调整到没被映射的区域)
MAP_LOCKED - 锁定映射,禁止被换出到换页文件(即一直存在与半导体内存中)

fd - 文件描述符

offset - 文件偏移量,自动按页对齐

munmap函数

解除虚拟内存到物理内存或文件的映射(按换页大小的整数倍来操作)
#include <sys/mman.h>
int munmap(void* start, size_t length);
成功返回0,失败返回-1。
start - 映射区的起始地址
length - 映射区的字节数

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h> int main(void) {
    char* psz = mmap(NULL, 8192,
        PROT_READ | PROT_WRITE,
        MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
    if (psz == MAP_FAILED) {
        perror("mmap");
        return -1;
    }
    sprintf(psz, "第一页");
    sprintf(psz + 4096, "第二页");
    printf("%s\n", psz);
    printf("%s\n", psz + 4096);
    if (munmap(psz, 4096) == -1) {
        perror("munmap");
        return -1;
    }
    //这里会出现段错误,因为前面把psz的第一页解除了
    //printf("%s\n", psz);

    //这里不会出现段错误,因为前面只把psz的第一页解除了,第二页没有,还能访问到
    printf("%s\n", psz + 4096);

    //这里映射就全部解除了
    if (munmap(psz + 4096, 4096) == -1) {
        perror("munmap");
        return -1;
    }
    return 0; } ```

可通过cd /proc/(pid号)目录下的maps文件查看该进程内存堆的使用情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值