当前位置 博文首页 > jacklin_01的博客:C语言内存机制详解

    jacklin_01的博客:C语言内存机制详解

    作者:[db:作者] 时间:2021-08-15 13:35

    参考链接:

    http://c.biancheng.net/view/423.html

    https://blog.csdn.net/greybeard/article/details/84132968

    https://blog.csdn.net/wws199304/article/details/44786581

    进程和线程

    首先了解一下进程和线程

    当启动一个程序时,操作系统创建一个进程,并在该进程中执行程序。一个进程包括一个或多个线程。每个线程又是一个局部进程,它以独立于其他局部进程的方式执行一个命令序列。

    当进程启动时,它的主线程则成为活动线程。这时,任何正在运行的线程都可以启动其他线程。当进程终止时,例如,通过在 main()函数中执行一个 return 语句或通过调用 exit()函数,所有已开启但还未结束的线程都会被终止。

    系统调度器为所有可运行的线程平均分配可用的 CPU 时间。通常,调度器是抢占式的:它会中断正在执行的线程,给中央处理单元(CPU)留出可用的短暂时间,并将 CPU 分配给其他线程使用一段时间。

    这种调度的结果是:即使是在单处理系统上,在用户面前运行的线程看上去像是在同时执行,实际上,只有在多处理器系统中,几个线程才可能真正地同时执行。

    每一个进程在内存中都有自己的地址空间,并拥有独占的资源,例如,打开的文件。一个进程中的所有线程都继承该进程的资源。最具有意义的是,在一个进程中的几个线程共享一个地址空间。这使得在一个进程中的任务切换比在不同进程间的任务切换要简单得多。

    然而,为了在不同线程间切换任务,每个线程也拥有自己的资源:包括栈存储器和 CPU 寄存器。这些资源允许每个线程在不受其他线程干扰的条件下,处理自身的本地数据。此外,一个线程也可以具有线程专用的永久内存。

    对于一个给定进程,由于它内部的所有线程均使用相同的地址空间,所以它们共享全局数据与静态数据。然而,这也意味着,同一个进程中的两个不同线程可以同时访问同一个内存单元。这种情况在 C 标准中被称为数据竞争(data race),或者通常称之为竞态条件(race condition)。

    为了防止在共享数据时出现冲突,当这些不同线程使用内存中相同位置时,程序员必须明确地同步这些不同线程的写操作或读写操作。

    ?

    ?

    对于一个C语言程序而言,内存空间主要由五个部分组成代码段(.text)、数据段(.data)、BSS段(.bss),堆和栈组成,其中代码段,数据段和BSS段是编译的时候由编译器分配的,而堆和?栈是程序运行的时候由系统分配的。布局如下

    在上图中,由编译器分配的地址空间都是在连接的时候分配的,而运行时分配的空间是在程序运行时由系统分配的

    ?

    BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量和静态变量 (这里注意一个问题:一般的书上都会说全局变量和静态变量是会自动初始化的,那么哪来的未初始化的变量呢?变量的初始化可以分为显示初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话的确也会被初始化,那就是不管什么类型都初始化为0,这种没有显示初始化的就是我们这里所说的未初始化。既然都是0那么就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。 BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存区能在运行时分配并被有效地清零。BSS节在应用程序的二进制映象文件中并不存在,即不占用磁盘空间?而只在运行的时候占用内存空间?,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。
    ??
    数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。?字符串常量等,但一般都是放在只读数据段中?。
    ??
    代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中?。?
    ??
    堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)?
    ??
    栈 (stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。注意:栈空间是向下增长的,每个线程有一个自己的栈,在linux上默认的大小是8M,可以用ulimit查看和修改。

    栈系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。

    3.1.2 栈和堆的区别

    前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)。栈和堆的主要区别有以下几点:

    (1)管理方式不同。

    栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

    (2)空间大小不同。

    栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

    堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

    (3)是否产生碎片。

    对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

    (4)增长方向不同。

    堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

    (5)分配方式不同。

    堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

    (6)分配效率不同。

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。
    ?

    以下是一个简单的c文件,环境是OS--Linux,ARCH--PPC

    ##sta.c###
    #include <stdio.h>

    int kk[100] = {1,2,3,4,5};
    int tt[100];
    int ii;

    int main()
    {
    int i;
    static int si;
    char a[10]= "abcd";
    printf("i is %d/n");
    return 0;
    }

    ?

    经过gcc -S sta.c之后,生成的汇编代码如下

    ##sta.s###

    ?

    ??????? .file?? "sta.c"
    ??????? .gnu_attribute 4, 2
    ??????? .gnu_attribute 8, 3
    ??????? .globl kk
    ??????? .section??????? ".data"
    ??????? .align 2
    ??????? .type?? kk, @object
    ??????? .size?? kk, 400
    kk:
    ??????? .long?? 1
    ??????? .long?? 2
    ??????? .long?? 3
    ??????? .long?? 4
    ??????? .long?? 5
    ??????? .zero?? 380
    ??????? .lcomm? si.2254,4,4
    ??????? .type?? si.2254, @object
    ??????? .section??????? .rodata
    ??????? .align 2
    .LC1:
    ??????? .string "i is %d/n"
    ??????? .align 2
    .LC0:
    ??????? .string "abcd"
    ??????? .zero?? 5
    ??????? .section??????? ".text"
    ??????? .align 2
    ??????? .globl main
    ??????? .type?? main, @function

    main:
    ??????? stwu 1,-32(1)
    ??????? mflr 0
    ??????? stw 0,36(1)
    ??????? stw 31,28(1)
    ??????? mr 31,1
    ??????? lis 9,.LC0@ha
    ??????? la 9,.LC0@l(9)
    ??????? lwz 0,0(9)
    ??????? lbz 9,4(9)
    ??????? stw 0,12(31)
    ??????? stb 9,16(31)
    ??????? li 0,0
    ??????? stb 0,17(31)
    ??????? li 0,0
    ??????? stb 0,18(31)
    ??????? li 0,0
    ??????? stb 0,19(31)
    ??????? li 0,0
    ??????? stb 0,20(31)
    ??????? li 0,0
    ??????? stb 0,21(31)
    ??????? lis 9,.LC1@ha
    ??????? la 3,.LC1@l(9)
    ??????? crxor 6,6,6
    ??????? bl printf
    ??????? li 0,0
    ??????? mr 3,0
    ??????? lwz 11,0(1)
    ??????? lwz 0,4(11)
    ??????? mtlr 0
    ??????? lwz 31,-4(11)
    ??????? mr 1,11
    ??????? blr
    ??????? .size?? main, .-main
    ??????? .comm?? tt,400,4
    ??????? .comm?? ii,4,4
    ??????? .ident? "GCC: (GNU) 4.2.3"
    ??????? .section??????? .note.GNU-stack,"",@progbits

    ?

    Note: 一般编译器和操作系统实现来说,对于虚拟地址空间的最低(从0开始的几K)的一段空间是未被映射的,也就是说它在进程空间中,但没有赋予物理地址,不能被访问。这也就是对空指针的访问会导致crash的原因?,因为空指针的地址是0。至于为什么预留的不是一个字节而是几K,是因为内存是分页的,至少要一页;另外几k的空间还可以用来捕捉使用空指针的情况。

    cs