linux程序的内存结构 堆结构 内存分配函数 brk/sbrk

来源:互联网 发布:美图文字秀秀软件 编辑:程序博客网 时间:2024/04/27 15:34
1. Linux对内存的结构描述

1./proc/${pid}/  存放进程运行时候所有的信息(包括内存结构)

ps aue 查看进程
到正在运行的程序的目录,进程运行的所有信息都在这

exe 执行程序所执行的文件,指向执行的程序。
cwd 指向当前工作路径
fd 是程序打开的所有文件描述符
cpuset 是程序的cpu信息
cmdline 是命令行
environ 是环境参数和环境变量 main带的参数
cat maps 程序运行时的内存结构

 2.理解程序的变量与内存空间的关系

a1:0x80497c0     全局变量
a2:0x80497c4     静态全局变量
a3:0x8048600     全局常量
b1:0xbff995bc     局部变量
b2:0x80497c8     局部静态变量
b3:0xbff995b8     局部常量
MA:0x804842f
ad:0x8048424
c1:0x927a008

 
13884下文件全是0,这些文件是映射到内存的,文件和地址绑定,地址和内存绑定.
常量在代码区

代码空间以上部分的大小理论上是1个G,是这段空间映射到内核的动态库和共享库
程序的应从0804800开始, 4k =0x1000大小,4k是一页的大小,是16进制的1000
常量依据作用范围不同,放不同区,有放代码区的,也有放局部棧的,局部常量放临时棧区。
全局常量(字符串"djiao33"类似这些)放代码区。
 
内存空间权限 r是可读,x可执行,p是受保护的,私有的,s共享。
这个空间大小刚好4G。对照下图进程内存中的结构。注意高地址对应高地址
 
 结论:
                              任何程序的内存空间分成4个基本部分
                              1.代码区 特征是权限是可执行的  
                              2.全局栈区
                              3.堆
                              4.局部栈 (下面如果是两个,那都是棧)
linux的内存管理理解
          通过地址直接访问内存,非保护模式。
          不能直接通过地址访问内存,保护模式。用户看到的地址是地址逻辑编号,就是虚拟地址,(0x00000000~0xffffffff)
          每个虚拟地址要映射一个实际的物理存储空间才能访问。否则就错误,这个错误就是段错误,就是每个实际物理空间映射。
          这就叫内存映射<==>每个虚拟的地址必须要映射到物理的存储空间才能访问。
          比如当我们常规的内存不够的时候,把文件和磁盘映射到我们的地址上面,加大内存。
          内存映射的管理?
         1内存映射的基本单位是页(page=4k=4096byte)
          因为c/c++语言中地址变量都是4字节的整数
          实际c能表示的内存是4G
          每个程序都能访问4G的内存空间。
strace 可执行程序   跟踪程序的执行过程

用mmap2进行内存映射,会把虚拟的地址转为实际的物理地址。
linux哪些地址是可以访问的,程序员基本素质。
           
 
任何程序都有自己的自加载器ld-linux.so.2, 他是标准的可执行程序,负责把执行程序拷贝到代码空间,并把指针指向首地址,分配全局棧。
 

 结论:
          1.内存分四个区.
          2.各种变量对应存放区
          3.堆栈是一种管理内存的数据结构.
          4.查看程序的内存地址.
       2.理解malloc的工作的原理  c/c++的内存是由一个链表结构维护的。
 malloc使用一个数据结构(链表)维护分配空间, 所以free才能不用传入数据大小,而用指针释放空间。
 链表的构成:表明当前内存空间位置的节点,上一个空间节点,下一个空间节点,当前区域大小的节点。(分配的空间/上一个空间数据/下一个空间/空间大小等信息.)
                       struct node                        {    T     value; //数据区                          node* next;                          node* prev;                          size   length;                         }
对malloc分配的空间不要越界访问.因为容易破坏后台维护结构.导致malloc/free/calloc/realloc不正常工作.
 main()
{

          int *p1=malloc(4);
          int *p2=malloc(4);
          int *p3=malloc(4);

          *p1=1;
          *(p1+1)=2;
          *(p1+2)=3;  //越界访问
          *(p1+3)=4;
          *(p1+4)=5;
          *(p1+5)=6;
          *(p1+6)=7;
          *(p1+7)=8;
          free(p1); //越界访问后free,执行出现错误:main:free():invalid next size(fast):ox099d4008
          printf("%d\n",*p2); //输出 5
}

越界访问破坏链表结构,出现常见错误!

                   

3.C++的new与malloc的关系
                    malloc           new new[]
                    realloc           new()
                    calloc            new[]
                    free               delete  delete[]?
        结论:new的实现使用的是malloc来实现的.
               区别:new使用malloc后,还要初始化空间为20,或者调用类型的构造函数.
               基本类型,直接初始化成默认值.
               UDT(user design type 用户自定义类型)类型,调用指定类型的构造器
               delete调用free实现.
               区别:delete负责调用析构器,将数据清零.然后再调用free
               new与new[]区别
                         new只调用一个构造器初始化.
                         new[]循环对每个区域调用构造器.
               delete 与delete[]
               内存分配:
                         系统分配->c的分配方式->c++分配方式->stl->智能指针
                         void* malloc(size_t) 分配不初始化
                         void* calloc(size_t num, size_t size) 分配初始化为0
                         void* realloc(void *ptr, size_t size)在指定位置重现分配指定大小的空间,初始化为0
                                   size=0 作用=free
                                   ptr=null 作用= malloc
                                   size!=0 && ptr!=null
                                            1 ptr的地址空间大小>size  返回指针等于ptr
                                                  释放ptr,并且在这个位置分配size空间
                                             2ptr的地址空间大小<size  返回指针不等于ptr
                                                  释放ptr,并且在其他位置分配size空间
                 内存操作
                              1初始化
                                        void* bzero(void*, size_t)  unix函数<string.h>中
                                        //内存初始化为0
                                         void* memset(void* ptr,int value,  size_t n);
                                        把指定位置ptr,指定大小n初始化为value,
                                       
                              2使用内存
                                        void*   memcpy(void* dset, const void* src, size_t n);     
                                        内存拷贝
                                         int      memcmp(const void* s1, const void* s2, size_t n);
                                        内存比较,返回0表示内存相同。
          5.函数调用栈空间的分配与释放              
               5.1.总结:
                         1.函数执行的时候有自己的临时栈.
                         2.函数的参数就在临时栈中.如果函数传递实参.则用来初始化临时参数变量.
                         3.通过积存器返回值.(使用返回值返回数据)
                         4.通过参数返回值.(参数必须是指针)
                                        指针指向的区域必须事先分配.
                         5.如果参数返回指针.参数就是双指针.
                        
               5.2.__stdcall __cdecl __fastcall 决定函数棧的产生方式
                         1.决定函数栈压栈的参数顺序.    
                         2.决定函数栈的清空方式
                         3.决定了函数的名字转换方式.
                                       
          6.far near huge指针
                    near 16
                    far  32
                    huge 综合
4.虚拟内存
 ( 每次我们在linux下面查看程序的内存结构时。可以看到代码区,全局栈区,局部栈区和堆区的内存地址总是从一个地址开始(每个操作系统是不一样的,但同一台电脑每次运行都会看到所有程序的起始地址都是同一个)。这个时候我们看到哦地址都是虚拟地址。
     虚拟地址是什么概念呢?就是系统返回给我们看的一个地址。因为我们是不可能去直接操作物理地址的,所有对物理地址的的操作就是由内核去完成。首先我们向内 核发送一个请求,然后内核响应请求然后去物理内存划分一块(就是映射)然后把这个划分的内存地址返回给我们。这个返回的内存地址就是刚才看到的虚拟地址。
而由物理地址到虚拟地址的这个转换过程就是映射。
通过映射,我们可以需要虚拟地址进行内存操作。内核负责修改真正的物理内存地址。作为C、C++程序员,也只能操作都虚拟地址这个层次了,物理地址那是驱动开发兄弟的地盘了。
这就解释了为什么每个程序的开始地址都是同一个地址了,那个地址只是一个给我们看的地址。我们操作这个地址的时候,内核会将这个地址转换成真正的物理地址进行操作。
映射说完了,我们就开始谈brk/sbrk吧。)

          问题:
                    一个程序不能访问另外一个程序的地址指向的空间.
          理解:
                    1.每个程序的开始地址0x80084000
                    2.程序中使用的地址不是物理,而是逻辑地址(虚拟内存).保护模式
                              逻辑地址仅仅是编号.编号使用int 4字节整数表示.
                              4294967296
                              每个程序提供了4G的访问能力
          问题:
                         逻辑地址与物理地址关联才有意义:过程称为内存映射.
          背景:
                         虚拟内存的提出:禁止用户直接访问物理存储设备.
                         有助于系统的稳定.
                        
          结论:
                         虚拟地址与物理地址映射的时候有一个基本单位:
                                        4k  就是0x1000大小 内存页.
                         段错误原因:无效访问.
                         合法访问:比如malloc分配的空间之外的空间可以访问,但访问非法.
5.虚拟内存的分配.
               栈:编译器自动生成代码维护
               堆:地址是否映射,映射的空间是否被管理.
         棧和堆这两个数据结构是系统管理的。
         这个时候就要使用brk/sbrk函数了。这两个函数都是我们自己申请一块内存,这块内存不再由系统托管。完完全全是我们自己管理。也就是说我们自己要负责这一块内存的使用和释放,系统不再负责了。
 1.brk/sbrk 内存映射函数 unix的函数
           问题1 怎么知道空闲空间?
             问题2 内核的内存分配方式?
                         
              每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这 一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理 下将虚拟地址空间映射到内存,供malloc函数使用。所以sbrk要分配新空间给进程。
            内核数据结构mm_struct中的成员变量start_code和end_code是进程代码段的起始和终止地址,
                                                                        start_data和 end_data是进程数据段的起始和终止地址,
                                                                        start_stack是进程栈段起始地址,
                                                                        start_brk是进程动态内存分配起始地址(堆的起始地址),
                                                                        还有一个 brk(堆的当前的终止地址),就是动态内存分配当前的终止地址。
C语言的动态内存分配基本函数是malloc(),在Linux上的基本实现是通过内核的brk系统调用。brk()是一个非常简单的系统调用,
brk只是简单地改变mm_struct结构的成员变量brk的值。
               
      分配释放内存:         
               int brk(void *end);//分配空间,释放空间 实际是改变end_ptr的值
                         移动访问的范围
                                   end_ptr > sbrk(0)  //分配
                                   end_ptr < sbrk(0) //释放
               void *sbrk(int size);//只做空间分配,返回分配空间的首地址
                        size = 0 得到大块空闲空间的首地址指针. 但这个指针没有映射还不能用
                             > 0 分配指定空间,end_ptr移动到末尾,每次分配从end_ptr开始,返回这片空间的首地址指针,并且把end_ptr指针位置+size
                            < 0 释放空间  返回释放的空间的end_ptr的地址(这个地址已经不能用了),并将空间指针回移。
               应用:
                         1.使用sbrk分配空间
                         2.使用sbrk得到没有映射的空间的虚拟地址.
                                   int *p = sbrk(0); //p这个指针还不能用,还没映射。
                                   *p = 40;//给没有映射的空间赋值,段错误。
                              如果参数不是0,就帮你映射size的空间,以页为单位的分配。
                         3.使用brk分配空间
                                   int* p = sbrk(0);  // int* p = sbrk(9);这里的效果一样,因为后面brk又把末尾指针往前移动了。
                                   brk(p+1); //这里虽然只是加1个字节,但是映射了一个页4k。
                                   int* q = sbrk(0);
               //这时候返回的地址与p相差4个字节,按brk移动的字节数末尾,开始向高地址(未分配区)分配。brk指向的地方是标记为已经用的。
                                   *p=40;
                                   *(p+40)=60;//证明映射的不只1个字节,不会有段错误,但逻辑上不这么用
                                   brk + 链表结构管理 = malloc
                                   malloc+初始化 = new
                         4.使用brk释放空间

               理解:
                         sbrk(int  size)
                         sbrk与brk后台系统维护一个指针.
                         指针默认是null.
                         调用sbrk,判定指针是否是0,是:得到大块空闲空间的首地址初始化指针.
                                                                                                    同时把指针+size
                                                                                          否:返回指针,并且把指针位置+size
               应用案例:
                         写一个程序查找1-10000之间所有的素数.
                         并且存放到缓冲,然后打印.
                        
                         缓冲的实现使用sbrk/brk
                         流程:
                                   循环
                                             判定是否素数(isPrimer)
                                             是,分配空间存放
                                             不是,继续下步.
#include <stdio.h>
#include <unistd.h>
int isPrimer(int a)
{    
     int i;
     for(i=2;i<a;i++)
     {
          if(a%i==0)
          {
               return 1;              
          }
     }
     return 0;
}

main()
{
     int i=2;
     int b;
     int *r;
     int *p;
     p=sbrk(0);
     r=p;
     for(;i<100;i++)
     {
          b=isPrimer(i);
          if(b==0)
          {
               brk(r+1); //末尾指针end_ptr=(r+1); 所以r被映射了。
               *r=i;
               r=sbrk(0);
          }
     }
     i=0;     r=p;
     while(r!=sbrk(0))
     {
          printf("%d\n",*r);
          r++;
     }
     brk(p);//free
}
           总结:
                         智能指针
                         stl
                         new
                         malloc (小而多数据) 多指数据类型多
                        下面两种速度和效率快:
                         brk/sbrk (同类型的大块数据,动态移动指针)
                         mmap/munmap(控制内存访问/使用文件映射/控制内存共享)性能对于brk/sbrk没有优势,只是可以用文件映射,这点别人没有。
               异常处理
                         int brk(void*)
                         void *sbrk(int);
                         如果成功.brk返回0   sbrk返回指针
                                失败 brk返回-1  sbrk返回(void*)-1

            linux的内存映射
                 mmap 映射空间
                              映射到内存
                              映射磁盘文件
                munmap  解除映射空间  //只有整个页不用了,才释放掉。

   

原创粉丝点击