【操作系统】堆与内存管理概述

转自: https://blog.csdn.net/bitboss/article/details/70154146

—–要说到操作系统的堆与内存的管理的话,那内容真的是海了去了,从开始的地方就能不停的扩展,但内容的重要性也是不可言喻的,本片博客着重于总结以下三点:

Linux的虚拟地址空间布局
堆和栈的管理,堆和栈的区别
中间会涉及到一些扩展的知识,但是不会细说!

开始的地方: 程序的内存布局

要说堆和内存管理,那么开始的地方不得不是程序的内存布局,即虚拟地址空间,下面贴出一张虚拟地址空间图(盗的图):

分析 : 这张图可以让我们很明显的看明白虚拟地址空间的布局(32位);

内核部分就不说了,能力不够 -_-||, 重要的是我们知道虚拟地址空间的高1G的空间是给了内核就可以了,当然这是Linux内核,windows默认给内核分配2G,当然也可以配成1G;(有兴趣的同学可以去了解一下中断,或者进程切换时,内核栈和用户栈的切换,是如何保护现场的),下面我们都默认内核只占高1G的地址空间,而这1G的内核空间则是所有进程共享的,也就是那句话,每个进程都有4G的虚拟地址空间,但是其中的1G是所有进程共享的内核空间,而剩余的3G空间则是所有进程私有的用户地址空间,程序运行在内核空间和用户空间时的状态又被称为内核态和用户态,在内核态和用户态的运行级别是不同的,也就是权限是不同的,什么时候会进入内核态? 一般中断的时候会进入内核态运行,而中断又分为硬件中断和软中断,硬件中断,比如说你在键盘上输入的时候就是硬件中断,处理过程–保护现场–硬件处理–得到中断向量码–查找中断向量表–执行中断向量子程序–恢复现场;软件中断,比较常见的就是我们调用系统调用的时候,感觉不能扯远了,关于中断都是学校汇编课本学习的; 关于这块的内容建议参考**《程序员自我修养》**12章; 留一个问题,为什么要有内核态和用户态?

继续下一个,处于内核空间和栈之间的是环境变量和命令行参数,这些是在执行main函数之前就放好的,关于环境变量,在Linux下,输入env查看环境变量,set 查看所有变量,export 将普通变量变为环境变量,环境变量是会被子进程继承的,关于验证这一点其实也很简单 : 你在Linux命令行上设置一个普通变量,test=’aaaa’; 查看变量: echo $test; 此时test 是个普通变量;你用env | grep test 是找不到的,如果你用set | grep test 就可以看到设置的变量, 现在想要让test变为环境变量: export test; test变为环境变量; 现在就是验证子进程是否会继承环境变量了,很简单,命令行输入bash就会是一个子bash了,载看看test的值还在不在,退出子bash 按 ctrl+d;下面贴出一张演示图;命令行参数就太熟悉了,可以通过main函数的参数获取命令行参数,就在argv里面放着呢,不做赘述;

3.下一个,栈 不要太熟悉,可不是我们使用的容器stack啊;我们平时写代码的时候,局部变量都是在栈上存放的,而每个线程的默认栈的大小是1M(可以 修改),用于维护函数调用的上下文,(此时你就该回忆一下函数的调用约定,函数的栈帧等),同时图上很明显的可以看出栈的增长方向是从高地址向低地址;

这块留一个问题:

object return_fun
{
    //object 是一个类,这里返回一个类的对象;
    //方式 1
    object ret;
    return ret;

    //方式2: 优化版本
    return object();

    //问题: 方式1和方式2各调用了几次拷贝构造函数?  为什么?
}

参考《程序员的的自我修养》 P305

4.内存映射段:此处,内核将硬盘文件的内容直接映射到内存,任何程序都可以通过Linux的mmap系统调用请求这种映射。内存映射是一种方便高效的文件I/0方式,因而被用于装载动态共享库。用户也可创建匿名内存映射,该映射没有对应的文件,可用于存放数据,在Linux中,若通过malloc申请一块比较大的内存的话,就会使用mmap的匿名地址空间映射,这个在后面细说;

5.堆:和栈一样使用来存放数据,由低地址向高地址增长,用户可以申请的内存,在用户释放之前一直有效,不过对于C++来说,痛并着快乐,常常因为内存泄露搞得焦头烂额,经常羡慕Java的垃圾回收机制;同时不同的操作系统对堆的维护方式不太一样,这个下一部分细说;

6.BSS段:未初始化的数据段,但其实并不包含数据,仅维护一个开始地址和结束地址,BSS段的数据默认都是零,所以在二进制可执行文件的文件头内,其实是不存在BSS段的,所以,如果有人问你,BSS段节省的是什么的空间,你就告诉他,节省的是文件空间,而不是内存空间,因为当程序读取BSS段的数据时,内核将会将其转到一个全零页面,不会发生缺页,但会为其分配对应的物理内存 ; 
7.DATA段已初始化的数据,和BSS段共称为数据段;占文件空间,也占物理内存; 
8. 代码段: 也就是存放程序代码的地方,同时也是只读的,比如我们的char *str = “hello world”; 就是存放在只读数据区,其实和代码段在一起,因此,在程序中我们不可以修改这个字符串; 
9. 最后一部分:保留区位于虚拟地址空间的低地址处,未赋予物理地址,任何对它的引用都是非法的;比如,我们对NULL指针的操作就是非法的;

终于把上面的拖拖拉拉的说完了,当然,本篇博客的重点是下面要说的

堆与内存管理

1.先来试试自己在Linux和windows最多可以分到多少堆内存;

测试代码:

#include<iostream>

using namespace std;

unsigned maximum = 0;
int main()
{
    unsigned blocksize[3] = {1024*1024,1024,1};

    int count = 0;
    for(int i = 0; i < 3; ++i)
    {
        for(count = 1; ; count++)
        {
            void* block = malloc(maximum + blocksize[i]*count);
            if(block)
            {
                maximum = maximum + blocksize[i]*count;
                free(block);
            }else{
                break;
            }
        }
    }
    printf("maxsize is %u M\n",maximum/(1024*1024));
    system("pause");
    return 0;
}

windows执行结果: 1.6 GB


Linux执行结果:  1.9 GB,2.6版本以后堆最多可以开辟2.9G的空间


虚拟地址空间中有那么大的空间都可以被当来用作堆空间,而且2.6版本的Linux最多开辟1.9G的堆空间,2.6版本以后将内存映射段上调的和栈越来越近,堆最多可以开辟2.9G的空间,那么这么大的空间我们应该怎么来管理呢?结合STL的空间配置器来说,为什么要有空间配置器呢? 就是因为频繁的向操作系统申请内存的效率比较低,而且如果频繁申请小额内存,又会造成内存碎片问题,所以就直接扔给配置器一大块空间,让用户空间自己去处理,而windows和linux的底层内存管理其实都是类似的思想,只不过更复杂罢了;
Linux进程堆管理:

linux下的进程堆管理比较复杂,提供两种分配方式,就是两个系统调用,一个是brk()系统调用,另外一个是mmap();
brk的作用实际上就是设置进程数据段的结束地址,即它可以扩大或者缩小数据段;我们可以将数据段扩大的那一部分作为堆空间使用,这是最常见的做法之一。而glibc还有一个函数叫sbrk(),功能和brk一致,实际上就是对brk的一层包装,这里就不深入探究了,没意义;
mmap()的作用和windows下的VitualAlloc很相似,作用是向操作系统申请一段虚拟地址空间,当这块虚拟地址可以映射到某个文件的时候它的作用是共享内存,当它没有映射到文件的时候,那么这块虚拟地址空间成为匿名空间,可以被用来当做堆空间;

void *mmap

    void *start,      //申请的起始地址
    size_t length,    //长度
    int port,        
    int flags,
    int fd,
    off_t offset
);

mmap申请的虚拟地址空间大小都是页的倍数,所以比较消耗系统性能;所以如果申请比较小的内存总不能总是调用mmap吧,所以Linux的glibc有自己的一套分配算法;

这里我们着重说一下mmap的是怎么得到匿名空间的:看图

如果mmap映射的一个vm_area_struct存储的是一块没有被占用的空间,那么这块空间就可以被当做堆来使用;

glibc的堆分配算法: 对于小于64字节的空间申请是采用类似于对象池的方法;对于大于512字节的空间申请则是采用的最佳适配算法(操作系统课本有讲);对于大于64字节而小于512字节的,它会根据情况采取上述方法中的最佳折中策略;对于大于128KB的申请,它会使用mmap机制直接向操作系统申请空间;

栈和堆的比较

申请方式和效率: 
     栈是由系统自动分配释放;堆是由程序员自己来申请和回收;而且在堆分配的时候还需要遍历空闲链表,效率不言而喻;
申请大小的限制: 
     栈一般默认的大小是1M,超过栈剩余空间时会报栈溢出错误,因此从栈上获取的空间比较小; 
     堆如果是在新版本的Linxu可以获取2.9G最多,在windows下一般为1.5G,最多1.9G;
存储内容的生命周期: 
     栈一般用来做函数的调用栈帧,存储局部变量和寄存器值,所以当函数结束后,退栈清除数据; 
     堆申请的空间存放的数据,知道堆被回收,否则一直存在;
最后在遗留一个问题:malloc(0) 的返回值?
--------------------- 
作者:John__xs 
来源:CSDN 
原文:https://blog.csdn.net/bitboss/article/details/70154146 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/hemeinvyiqiluoben/article/details/84975808