《C指针》学习笔记( 第一章)内存、实时内存管理和虚拟内存

C语言问世于1978年,最初为实现UNIX系统而设计,广泛应用于非UNIX平台的软件开发,C语言一直是底层开发、设备驱动程序、嵌入式系统开发、移动设备开发等领域的首选语言

C语言不是强类型语言,指针是C语言最重要的特性
关于强类型语言的定义:“一旦某一个变量被定义类型,如果不经过强制转换,则它永远就是该数据类型了”
为什么C是弱类型语言?

第一章:内存、实时内存管理和虚拟内存

1.1 内存与类型

内存用于存储指令和数据序列,内存分为永久和临时存储(RAM、cache、寄存器)两种类型
内存是一组以二进制方式存储信息的单元,存储容量取决于底层硬件或体系结构以及位长(1,2,4,8,16,32,64或128位)

通常情况下,cache是用于临时存储小部分小部分数据的高速内存,这往往是经常访问的数据

cache也有一定的层次结构,L1 cache较快,距离CPU较近,但容量较小,L2 cache较慢,距离CPU较远,但容量较大 。SRAM用作高速cache存储,比DRAM快。某些架构还有专用指令cache和数据cache,以使得处理指令时,指令代码驻留在指令cache,而数据驻留在数据cache

执行某个程序时,操作系统在主存中创建相应的进程,主存容量直接关系系统处理软件的能力

在这里插入图片描述
内存排列:
内存在空间呈线性序列排列,其中每一个位置对应存储数据位置的地址,内存地址是用于访问基本信息单元的数字,信息就是数据

在这里插入图片描述
如下图,在内存中,数据存储在连续单元中
在这里插入图片描述

1.2数据与指令

数据和指令是所有程序的固有部分,运行程序时,加载器首先加载程序到内存中,被加载的程序称为进程
在这里插入图片描述

1.2.1处理器如何访问主存

数据和指令通过地址和数据总线调入CPU中
假定CPU要执行一条指令:mov eax,A。该汇编指令将变量A的值传送到eax寄存器里。CPU对这条指令译码后并把变量A的地址发送到地址总线,紧接着检查该数据现在是否在L1cache。只有两种情况:如果在,它被命中;如果不在,它未被命中。
未命中时在下一级内存层(即,L2 cache)查找数据。如果数据被命中,复制数据到寄存器(最后目标),而且还被复制到上一级内存层

1.2.2 缓存

在这里插入图片描述
实际的cache实例如下:
L1 cache = 32 KB和64 B/行
L2 cache = 256 KB和 64 B/行
L3 cache = 4 MB和64B/行

在这里插入图片描述

在这里插入图片描述
你已看过L2 cache未命中/命中的情况。这种情形下需扩展到主存甚至辅存,如硬盘等外部存储,我们每次复制数据回到较上一层内存,直至最终目标。但复制到较上一层内存的数据量是变化的。在上述情况下,按cache行的大小复制数据,如果在主存中出现一个未命中,那么复制到主存的数据量为1页(4KB)。

1.3 编译过程链

编译是逐步过程,每一步的输出都作为下一步的输入
编译输出为某个特定平台( 32位/64位机)上运行的可执行文件。这些可执行文件有不同格式被操作系统识别。
Linux识别ELF(可执行和链接格式);
Windows 识别PE/COFF(可移植可执行/通用对象文件格式)。
这些格式都有特定文件头格式和关联偏移量,需要按指定规则去读取和理解文件头和相应部分。
编译过程链如下:
源代码→预处理→编译→汇编→目标文件→链接→可执行程序
对于编译器,输入称为源代码的一系列文件(.c文件与.h文件),最后输出一个可执行文件。

1.3.1预处理

预处理是扩展源文件中指定的宏的过程

1.3.2 编译

将预处理文件编译成汇编代码,输出是.asm/.s文件

1.3.3 汇编器

编译后,调用汇编器生成目标代码,汇编代码有指令助记符,汇编器生成与不同助记符等效的操作码。

源代码可能会使用外部函数(如printf(),pow()),这些外部函数的地址不由汇编器解析,其地址解析工作留给下一个步骤—链接

1.3.4 链接

链接就是链接器解析所有外部函数的地址,并输出ELF/COFF格式的可执行文件或操作系统能识别的其他格式文件的过程。链接器大多需要一个或多个目标文件,如编译器生成的源文件的目标代码,也可能是程序中用到的某些库函数的目标代码(例如printf,数学库中的数学函数,以及字符串库中的字符串函数),最后生成一个可执行文件。
重要的是,它链接实际上调用程序主例程的起动例程/STUB。在Windows系统中,启动例程由CRT DLL提供;而在Linux系统中,启动程序由 glibc (libc - start.c)提供。图1-9给出启动存根信息。

1.3.5 加载器

严格讲,加载器不属于编译过程。相反,它是负责加载可执行文件到内存的操作系统的一部分。通常,一个UNIX 加载器的主要职责如下:

  • 验证

  • 从硬盘复制可执行文件到主存

  • 配置栈

  • 配置寄存器

  • 跳转到程序入口点(_start)

图1-11描述了内存中运行加载器加载程序helloworld.exe的情形。以下是加载器在加载一个可执行文件时操作系统所采取的步骤:

1.加载器请求操作系统创建一个新进程。
2.接着操作系统为新进程建立页表。
3.用无效入口标记页表。
4.开始执行该程序,生成即时页面错误异常。

操作系统对内存中每个正运行的程序执行上述步骤。
让我们看看不同程序如何同时共享物理内存。假定操作系统为程序helloworld.exe分配了一个进程id-5。分配帧0&1并加载页0&1存储代码段和数据段的部分内容。页( page)是虚拟内存单元,帧( frame)是物理内存中所使用的单元

在这里插入图片描述

1.4 内存模型

进程通过硬件架构所采用的底层内存模型访问内存。内存模型为进程建立物理内存映射便于CPU访问内存。英特尔架构中为进程提供三种访问物理内存的模型

  • 实地址内存模型
  • 扁平内存模型
  • 分段内存模型
1.4.1 实地址内存模型

Intel 8086架构采用的是实地址内存模型。Intel 8086有16个处理器,具有16位宽数据和地址总线和20位宽的外部地址总线。由于有20位宽的外部地址总线,该处理器可访问0-220 -1)=1MB的内存;但是因为16位宽的地址总线,该处理器仅能访问[0 –2 16 -1=64KB内存。为突破64KB限制和访问1MB的更高地址范围,就必须使用分段。Intel 8086有4个16位段寄存器。分段是在实模式下通过偏移一个段寄存器的4位并加上16位偏移量,最终形成一个20位的物理地址来实现的。有32位寄存器的Intel 80386仍在采用该分段模式。这为Intel 8086处理器上运行的现有程序提供了兼容性。

1.4.2 实地址模式下的地址转换

图1-12表示实模式下利用分段如何进行地址转换。

在这里插入图片描述

1.4.3 扁平内存模型

在扁平内存模型中,对于程序来说,内存空间是连续的。该线性地址空间(即,处理器可访问的地址空间)包括代码段、数据段等。程序生成的逻辑地址用于在全局描述符表中选择入口,并将该逻辑地址的偏移部分加到基本段上,它最终等同于实际物理地址。扁平内存模型为代码极快运行和系统极简配置提供了便利性。其性能优于16位实模式或分段保护模式。

1.4.4 分段内存模型

不同于实模式下的分段,分段内存模型下的分段是一个机制,它将线性地址空间分割成称为段的小部分。代码、数据和栈被放置于不同段中。进程通过逻辑地址从任意段访问数据。处理器将逻辑地址转换成线性地址并使用线性地址访问内存。使用分段内存有助于防止其他进程损害栈,覆盖数据和指令。明确定义的分段能提高系统的可靠性

图1-13给出了一个内存转换如何发生与地址对于进程如何可视的概述图。

在这里插入图片描述

1.5 使用分段的内存排列

多道程序环境要求将目标文件明确分离成不同部分来控制多进程和物理内存。物理内存属于有限资源,用户程序与操作系统共享。为管理内存中运行的程序,它们分布在不同部分,并根据操作系统中的运行策略加载和删除。
再次重申,当加载某个C程序并在内存中运行时,它由若干段组成。程序被编译时创建这些段并形成一个可执行文件。通常情况下,程序员或编译器会将程序/数据赋给不同段。可执行文件的头部包含它们的大小、长度、偏移量等信息。

1.5.1 分段

分段是一种用于实现以下目标的技术:

  • 多道程序
  • 内存保护
  • 动态迁移

源代码编译后被分成五个主要部分/段——代码、数据、BSS、栈和堆

1.5.2 代码段

该段包括指令代码。代码段被以相同二进制形式运行的多个进程共享。它通常具有读和运行权限。静态链接库增加可执行程序空间和最后代码段大小,比动态链接库运行得快。
动态链接库虽减少可执行程序空间和最后代码段的大小,但运行速度会变慢。因为它们在运行时花更多时间来加载目标库

1.5.3 数据段

数据段包括全局变量和非零值初始变量,以及静态分配的变量和非零值初始化变量。数据段的专用副本通过运行同一程序的每个进程来维护。
程序运行前可用目标值对静态变量初始化,但它会在程序整个运行期间占用内存。以下程序给出了源代码中使用数据段变量的实例。

在这里插入图片描述

1.5.4 未初始化/BSS块

BSS意为“以符号开始的块”。它包括所有未初始化的全局变量,以及用static关键字声明且未初始化的静态局部变量。本节中所有变量都默认初始化为零。运行同一程序的每个进程都有各自的数据段BSS运行所需空间记录在目标文件中BSS不在目标文件中占用实际空间。该部分的初始化过程在进程启动过程中完成。为方便启动,程序启动过程中需要初始化的任意变量都可存放在这里。下面给出一个声明变量是BSS段的一部分的源代码实例。

在这里插入图片描述

1.5.5 栈段

栈段用于存储局部变量、函数参数和返回地址。(返回地址是CPU返回函数调用后继续运行的内存地址。)
在函数体的左括号内声明局部变量,包括在main()或未定义为静态的其他左括号内。所以这些变量作用域仅限于函数体内。局部变量生命期就是执行控制的各函数体内。
在这里插入图片描述

调用函数main()时整型变量var1与var2将是栈的一部分。同样,当调用函数foo()时,整型变量var3与var4也将是栈的一部分。

1.5.6 堆段

创建进程时堆区由操作系统分配给每个进程。动态内存从堆中获取。调用malloc()、calloc(和 realloc()函数分配动态内存。从堆分配的内存仅能通过指针访问。进程地址空间会随着内存分配和释放在运行时扩大和缩小。调用free()函数将内存归还给堆。使用堆内存易于实现数据结构如链表和树。跟踪堆内存开销较大。如果使用不当,可能会导致内存泄漏。

1.6 实时内存组织

从下图可以看出,操作系统占用部分内存,剩余内存被不同进程占用,单个进程的不同段和属于其他进程的不同段在运行时是同时存在的
在这里插入图片描述

1.6.1 函数调用的复杂性

调用某个函数时,首先操作系统为运行程序中每个调用函数分配栈帧/激活记录。函数运行完控制权返回给调用者,分配的栈帧销毁。因此,我们不能访问函数的局部变量,因为函数的生命期随着其栈帧的销毁而结束。因此栈帧用来控制函数内部定义的局部变量的作用域

分配的栈帧用来存储automatic变量、参数和返回地址。递归或嵌套调用同一个函数会创建单独的栈帧。编程时要考虑栈帧的大小是一个有限的资源

栈帧的维护和其中包括的实体(局部变量、返回地址等)通过以下寄存器来实现:

基指针/帧指针(EBP):用来引用当前栈帧中的局部变量和函数参数。
栈指针(ESP):始终指向栈中使用的最后一个元素。
指令指针(EIP):保存要执行的下一条CPU指令地址,并将其保存到栈中作为CALL指令的一部分。

1.6.2 函数调用步骤
  • 1.从右向左将参数压入栈中
    在这里插入图片描述

  • 2.调用函数
    处理器将EIP压入栈中。这时EIP指向CALL指令后的首字节。
    在这里插入图片描述

  • 3.保存与更新EBP。
    此刻,我们在新函数中。保存当前EBP(属于被调用函数)
    压入EBP。
    使EBP指向栈顶:
    mov ebp, esp
    EBP现可访问的函数参数如下:
    8(%ebp):访问第1个参数
    12(%ebp):访问第2个参数
    上面的汇编代码是由编译器生成的每个调用函数的源代码。

在这里插入图片描述

保存临时CPU寄存器
分配局部变量
在这里插入图片描述

  • 4.从调用函数返回。
    释放局部存储。
    使用一系列POP指令。(恢复保存的寄存器。恢复旧的基指针。使用RET指令从函数返回)

在这里插入图片描述
考虑程序执行时呈现的时间和空间局部性行为,栈段是存储数据的最佳位置,因为许多程序结构(如 for循环和 do while)趋向于重复使用同一内存单元。这使得函数调用变成花销较大的操作,因为它涉及栈帧的耗时设置。当函数体很小时内联函数是首选替代

1.7 内存段

1.8 虚拟内存组织

多道程序让多个进程在任意时间内同时执行。因此这些进程没必要互相关联。硬件(内存管理单元)和操作系统对此做了支持。虚拟内存允许操作系统以最佳方式使用系统资源。虚拟内存组织的最重要特性就是不同进程彼此受操作系统保护。
虚拟内存有以下特性:
物理组织
逻辑组织
保护
迁移
共享
在多道程序环境中,许多进程共享主存。进程作为一个整体把主存看成其本身专用(进程)的完整资源。但操作系统仅仅加载/保持当前内存中运行程序的部分。

1.8.1 一窥虚拟内存系统

图1-15描述了虚拟地址空间如何被映射到物理地址。参与该转换的主要实体的MMU、TLB和页表

地址空间
内存空间必须在两个实体之间共享:
口操作系统内核
口用户程序
根据程序定义,无论操作系统或用户程序,当加载到内存中时都被称为内核进程或用户进程

在这里插入图片描述

虚拟地址空间

虚拟内存是逻辑实体,由用户进程假定它被加载。虚拟内存中的地址被称为虚拟地址空间。
图1-16给出一个假定进程被加载的典型场景。虚拟地址空间0-7FFFFFFE被用来加载用户进程。虚拟地址0x7FFFFFFF-更高被系统内核占用。当程序加载到内存中,假定整个用户空间都分配给该进程。

在这里插入图片描述
在这里插入图片描述虚拟地址组成

  • 虚拟页号
  • 页偏移字段
    在这里插入图片描述
    物理地址空间

物理地址空间是页面加载后在主存中的实际地址。
在这里插入图片描述

1.8.2 分页

分页是虚拟内存中最重要的部分之一。这种方案允许操作系统加载和卸载进程的部分页到物理内存中的任意不连续单元。分页概念假定主/物理内存被划分成可容纳任何进程页的相等和大小固定的帧/页帧。页是进程的基本部分,进程被分成相等且大小固定的页,每页通常为1KB/4KB。
图1-19给出进程A和进程B驻留在物理内存的不同帧中的分页场景。
典型分页系统包括以下任务:
1.地址空间管理:负责分配和管理进程的地址空间。
2.地址转换:在专用硬件的MMU上完成。也要考虑异常处理(如页面错误)。
3.内存共享:已在图1-19中展示。
在这里插入图片描述

1.8.3 页表

操作系统为内存中每个运行进程维护一个单独页表。参照该页表,判断是否访问某个有效页还是在访问某些无效页,访问无效页时会产生错误异常。图1-20表示地址转换到内存实际物理地址时引用的一个典型页表。
在这里插入图片描述

1.9 小结

本章讨论内存相关方面,特别是内存分类和 cache内存

推荐阅读

【小神仙讲 虚拟内存机制】 页 & 页框 & 页表 物理内存和虚拟内存怎么联系起来的呢?

小神仙讲 | 虚拟内存机制

小神仙讲| 计算机存储结构

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值