Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配

文章目录

  • 虚拟地址空间
  • 用户空间
  • 内核空间
  • 用户空间内存分配
    • malloc
  • 内核空间内存分配
    • kmalloc
    • vmalloc


虚拟地址空间

在早期的计算机中,程序是直接运行在物理内存上的,而直接使用物理内存,通常都会面临以下几种问题:

  • 内存缺乏访问控制,安全性不足
  • 各进程同时访问物理内存时,可能会产生访问内存空间重叠的现象,没有独立性
  • 物理内存极小,而并发执行进程所需又大,容易导致内存不足
  • 进程所需空间不一,容易导致内存碎片化问题。

基于以上几种原因,Linux通过 mm_struct 结构体来描述了一个虚拟的,连续的,独立的地址空间,也就是我们所说的虚拟地址空间

原理: 当程序被载入内存时,向其呈现出比实际拥有的地址空间大得多的内存——虚拟地址空间,让程序误认为自己目前独占电脑内存,能够占用电脑所有的内存,访问所有内存地址,同时建立虚拟地址与物理地址之间的映射。这就允许多个程序可以同时运行且各个程序之间能够访问的物理内存区域不重叠,也杜绝了程序直接操作地址的风险,同时也提高物理地址的使用效率。

值得注意的是,在建立了虚拟地址空间后,并没有立刻分配实际的物理内存,而是当进程需要实际访问内存资源的时候,才由内核的 请求分页机制 产生 缺页中断 ,这时才会建立虚拟地址和物理地址的映射,调入物理内存页;如果此时物理内存已经耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。通过这种方法,就能够保证我们的物理内存只在实际使用时才进行分配,避免了内存浪费的问题。

下图则为Linux下的虚拟地址空间:
在这里插入图片描述

32位Linux 的地址空间(232 B = 4 GB)被一分为二:0~3G为用户空间3~4G为内核空间。

  • 操作系统和驱动程序运行在内核空间 ,内核模式下,操作系统可以访问机器的全部资源。
  • 应用程序运行在用户空间 , 用户模式下,应用程序不能完全访问硬件资源。

当进程运行在 内核空间 时,它就处于 内核态 ;当进程运行在 用户空间 时,它就处于 用户态 。两个空间不能简单地使用指针传递数据,因为 Linux 使用了虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在内存中。


用户空间

用户空间即进程在用户态下能够访问的虚拟地址空间,每个进程都有自己独立的用户空间,大小为 3G
在这里插入图片描述

用户空间由以下部分组成:

  • 栈: 栈用来存放程序中临时创建的局部变量,如函数的参数、内部变量等。每当一个函数被调用时,就会将参数压入进程调用栈中,调用结束后返回值也会被放回栈中。同时,每调用一次函数就会在调用栈上维护一个独立的 栈帧 ,所以在递归较深时容易导致栈溢出。栈内存的申请和释放由 编译器 自动完成,并且 栈容量由系统预先定义 。栈从高地址向低地址增长。

栈帧从低到上依次是(从高地址到低地址的方向):

  1. 参数
  2. 返回地址:将当前代码区 调用函数指令下一条指令地址 压入栈中,供函数返回时继续执行。
  3. ebp(帧指针):指向当前的栈帧的底部
  4. 局部变量
  5. esp(栈指针): 始终指向栈帧的顶部

在这里插入图片描述

  • 文件映射段: 也叫共享区,文件映射段中主要包括 共享内存、动态链接库 等共享资源,从低地址向高地址增长。

共享资源以动态链接库为例:

  1. 动态链接库中的函数都与位置无关,即每次被加载进入内存映射区时的位置都是不一样的,因此使用的是其本身的逻辑地址,经过变换成线性地址(虚拟地址),然后再映射到内存。

  2. 而静态库被链接到可执行文件中,因此其位于 代码段 ,每次在地址空间中的位置都是固定的。

  • 堆: 堆用来存放动态分配的内存。堆内存由 用户 申请分配和释放,从低地址向高地址增长。不同于数据结构中的堆,存储空闲内存的方式类似链表,因此空闲内存分布不连续。
  • BSS段: 存放程序中 未初始化全局变量静态变量 ,全局变量 未初始化 时,其 默认值为0 ,因此也保存 初始化为0的全局变量 。具体体现为一个占位符,并不给该段的数据分配空间,只是记录数据所需空间的大小。
  • 数据段: 存放程序中 已初始化全局变量静态变量
  • 代码段: 存放程序执行指令,也可能包含一些只读的常量(.rodata段)。这块区域的大小在程序运行时就已经确定,并且为了防止代码和常量遭到修改,代码段被设置为只读。
  • 保留区(受保护的地址): 大小为128M,位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。

大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。

小结堆和栈的区别:
在这里插入图片描述
由于:

  1. 栈没有内存碎片问题,堆容易造成内存碎片。
  2. 堆没有专门的系统支持,效率很低,
  3. 堆可能引发用户态和内核态切换,内存申请的代价更为昂贵。

所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议仅在分配大量或大块内存空间时使用堆。


内核空间

内核空间即进程陷入 内核态 后才能够访问的空间。虽然每个进程都具有自己独立的虚拟地址空间,但是这些虚拟地址空间中的内核空间 ,其实都关联的是 同一块物理内存 ,如下图:
在这里插入图片描述
通过这种方法,保证了进程在切换至内核态后能够快速的访问内核空间。

内核空间主要分为 直接映射区高端内存映射区 两部分:

在这里插入图片描述
直接映射区:

从内核空间起始位置开始,从低地址往高地址增长,最大为 896M 的区域即为直接映射区。

直接映射区的 896M虚拟地址物理地址(ZONE_DMA + ZONE_NORMAL) 的前 896M 进行直接映射,所以虚拟地址和分配的物理地址都是连续的。

那么它们是如何转换的呢?其实它们之间存在着一个偏移量 PAGE_OFFSET ,偏移量的大小即为 0xC0000000

虚拟地址 = PAGE_OFFSET + 物理地址

高端内存映射区:

物理内存中 ZONE_DMA + ZONE_NORMAL 被直接联系到虚拟内存的 直接映射区 中,那么对于剩下的 896M~4G 大小的 ZONE_HIGHMEM ,寻址工作就交给了高端内存映射区。

由于我们的内核空间只有 1G ,而直接映射区又占据了 896M ,因此我们将剩下的 128M 空间划分成了三个高端内存的映射区,从上往下分别是:

  • 动态内存映射区: 该区域的特点是 虚拟地址连续,但是其对应的物理地址并不一定连续。该区域使用内核函数 vmalloc 进行分配,分配的虚拟地址的物理页可能会处于低端内存,也可能处于高端内存。
  • 永久内存映射区: 该区域可以访问 高端内存 。使用 alloc_page(_GFP_HIGHMEM) 分配高端内存页,或者使用 kmap 将分配的高端内存映射到该区域。
  • 固定内存映射区: 该区域的 每个地址项都服务于特定的用途 ,如 ACPI_BASE

用户空间内存分配

malloc

在C语言中,我们可以使用 malloc 来在用户空间中动态的分配内存,而 malloc 作为库函数,其本质就是对系统调用进行了一层封装,因此在不同的系统下其实现不同。

在Linux中,当我们申请的内存小于 128K 时,malloc 会使用 sbrk 或者 brk堆区分配内存。而当我们申请大于 128K 的大块空间时,会使用 mmap映射区进行分配。

但是由于上述的 brk/sbrk/mmap 都属于系统调用,因此当我们每次调用它们时,就会从用户态切换至内核态,在内核态完成内存分配后再返回用户态。

倘若每次申请内存都要因为系统调用而产生大量的CPU开销,那么性能会大打折扣。并且堆也有容易产生内存碎片的问题。

malloc是如何实现解决这个问题的呢?

为了减少内存碎片和系统调用的开销,malloc 在底层采用了 内存池 来解决这个问题。

它会先申请大块内存作为堆区,然后将这块内存拆分为多个不同大小的内存块,以 作为内存管理的基本单位。同时,会使用 隐式链表 来连接所有的 内存块 ,包括已分配块和未分配块。为了方便内存空闲块的管理,malloc 采用 显式链表 来管理所有的 空闲块

当我们调用 malloc 进行内存分配时,就会去搜索空闲链表,找到满足需求的内存块,如果内存块过大,则会将内存块拆分为两部分,即一部分用来分配,另一部分则变为新的空闲块。

同理,当我们释放内存块时,会通过遍历隐式链表,判断释放块前后内存块是否空闲,来决定是否需要合并内存块


内核空间内存分配

在内核空间中,通过与 malloc 类似的两个系统调用来进行内存的分配,它们 分别是 kmallocvmalloc .

kmalloc

kmalloc 用于为内核空间的 直接内存映射区 分配内存。

kmalloc 以字节为分配单位,通常用于分配小块内存,并且 kmalloc 确保分配的页在 物理地址 上是 连续的 ( 虚拟地址 也必然 连续 ) 。并且 kmalloc 为了防止内存碎片的问题,其底层页面分配算法是基于 slab分配器 实现的。

vmalloc

vmalloc 用于为内核空间中的 动态内存映射区 进行内存分配。

vmalloc 分配的内存 只保证了虚拟地址是连续的,而物理地址不一定连续 。它 记录非连续的物理内存块至页表 ,再通过 修正页表的映射关系 ,把内存映射到虚拟地址空间的连续区域。

在这里插入图片描述

如上图,就是内核空间中进行内存分配的具体流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443777.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile

文章目录编译原理预处理编译汇编链接gcc的常用命令参数make 和 Makefile 的概念make的运行通配符自动化变量伪目标.PHONE:【命令】编译原理 在解释 makefile 前,首先解释一下 .c 文件变成 .exe 文件要经过的四个步骤——预处理、编译、汇编和链接(参考来…

Linux | 进程概念、进程状态(僵尸进程、孤儿进程、守护进程)、进程地址空间

文章目录进程和程序操作系统如何控制和调度程序进程控制块–PCB子进程进程状态僵尸进程孤儿进程守护进程(精灵进程)进程地址空间引言页表进程和程序 程序: 一系列有序的指令集合(就是我们写的代码)。进程:…

Linux 进程控制 :进程创建,进程终止,进程等待,程序替换

文章目录进程创建进程等待程序替换进程终止进程创建 fork函数: 操作系统提供的创建新进程的方法,父进程通过调用 fork函数 创建一个子进程,父子进程代码共享,数据独有。 当调用 fork函数 时,通过 写时拷贝技术 来拷贝…

Linux 内存管理 | 连续分配方式 和 离散分配方式

文章目录前言连续分配单一连续分配分区式分配固定分区分配动态分区分配可重定位分区分配离散分配分段分页多级页表快表(TLB)段页式Linux前言 Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配 Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器…

操作系统 | 用户态和内核态的切换(中断、系统调用与过程(库函数)调用)

文章目录中断过程调用系统调用过程调用和系统调用的区别中断 用户态、内核态之间的切换是怎么实现的? 用户态→内核态 是通过中断实现的。并且 中断是唯一途径 。核心态→用户态 的切换是通过执行一个特权指令,将程序状态字 (PSW) 的标志位设置为 用户态 。 中断…

管道实现父子进程的信息传递(二)【标准流和其文件描述符、fwrite函数、perror函数】

文章目录代码实现标准流 和 标准流文件描述符代码中用到的函数fwrite()perror()在复习进程间的通信方式时又写了一遍,和 管道实现父子进程的信息传递(一)【fork函数、pipe函数、write/read操作、wait函数】 的区别不是特别大,只是…

命名管道实现进程的信息传递【mkfifo函数、open函数】

文章目录代码实现mkfifo函数open函数代码实现 #include<fcntl.h> // open() #include<sys/wait.h> // wait() #include<sys/types.h> // mkfifo() #include<sys/stat.h> // mkfifo() #include<iostream> #include<unistd.h> // fork()usi…

Linux 进程 | 进程间的通信方式

文章目录管道匿名管道 pipe命名管道 FIFO共享内存共享内存的使用流程&#xff1a;消息队列信号量套接字在之前的博客中讲过&#xff0c;虚拟空间出现的其中一个目的就是解决 进程没有独立性&#xff0c;可能访问同一块物理内存 的问题。因为这种独立性&#xff0c;进程之间无法…

Linux网络编程 | socket介绍、网络字节序与主机字节序概念与两者的转换、TCP/UDP 连接中常用的 socket 接口

文章目录套接字socket 地址通用 socket 地址专用 socket 地址网络字节序与主机字节序地址转换TCP/UDP 连接中常用的 socket 接口套接字 什么是套接字&#xff1f; 所谓 套接字 (Socket) &#xff0c;就是对网络中 不同主机 上的应用进程之间进行双向通信的端点的抽象。 UNIX/L…

网络协议分析 | 传输层 :史上最全UDP、TCP协议详解,一篇通~

文章目录UDP概念格式UDP如何实现可靠传输基于UDP的应用层知名协议TCP概念格式保证TCP可靠性的八种机制确认应答、延时应答与捎带应答超时重传滑动窗口滑动窗口协议后退n协议选择重传协议流量控制拥塞控制发送窗口、接收窗口、拥塞窗口快速重传和快速恢复连接管理机制三次握手连…

JDom,jdom解析xml文件

1.要解析的文件模板如下&#xff1a; <?xml version"1.0" encoding"GBK"?> <crsc> <data><举报信息反馈><R index"1"><举报编号>1</举报编号><状态>1</状态><答复意见>填写…

网络协议分析 | 应用层:HTTP协议详解、HTTP代理服务器

文章目录概念URLHTTP协议的特点HTTP协议版本格式请求报文首行头部空行正文响应报文首行头部空行正文Cookie与SessionHTTP代理服务器正向代理服务器反向代理服务器透明代理服务器概念 先了解一下 因特网&#xff08;Internet&#xff09; 与 万维网&#xff08;World Wide Web&…

MySQL命令(一)| 数据类型、常用命令一览、库的操作、表的操作

文章目录数据类型数值类型字符串类型日期/时间类型常用命令一览库的操作显示当前数据库创建数据库使用数据库删除数据库表的操作创建表显示当前库中所有表查看表结构删除表数据类型 mysql 的数据类型主要分为 数值类型、日期/时间类型、字符串类型 三种。 数值类型 数值类型可…

C++ 继承 | 对象切割、菱形继承、虚继承、对象组合

文章目录继承继承的概念继承方式及权限using改变成员的访问权限基类与派生类的赋值转换回避虚函数机制派生类的默认成员函数友元与静态成员多继承菱形继承虚继承组合继承 继承的概念 继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。 当创建一个类时&…

博弈论 | 博弈论简谈、常见的博弈定律、巴什博弈

文章目录博弈论什么是博弈论&#xff1f;博弈的前提博弈的要素博弈的分类非合作博弈——有限两人博弈囚徒困境合作博弈——无限多人博弈囚徒困境常见的博弈定律零和博弈重复博弈智猪博弈斗鸡博弈猎鹿博弈蜈蚣博弈酒吧博弈枪手博弈警匪博弈海盗分金巴什博弈博弈论 什么是博弈论…

MySQL命令(二)| 表的增删查改、聚合函数(复合函数)、联合查询

文章目录新增 (Create)全列插入指定列插入查询 (Retrieve)全列查询指定列查询条件查询关系元素运算符模糊查询分页查询去重&#xff1a;DISTINCT别名&#xff1a;AS升序 or 降序更新 (Update)删除 (Delete)分组&#xff08;GROUP BY&#xff09;联合查询内连接&#xff08;inne…

MySQL | 数据库的六种约束、表的关系、三大范式

文章目录数据库约束NOT NULL&#xff08;非空约束&#xff09;UNIQUE&#xff08;唯一约束&#xff09;DEFAULT&#xff08;缺省约束&#xff09;PRIMARY KEY&#xff08;主键约束&#xff09;AUTO_INCREMENT 自增FOREIGN KEY&#xff08;外键约束&#xff09;CHECK&#xff08…

哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

文章目录哈希哈希&#xff08;散列&#xff09;函数常见的哈希函数字符串哈希函数哈希冲突闭散列&#xff08;开放地址法&#xff09;开散列&#xff08;链地址法/拉链法&#xff09;负载因子以及增容对于闭散列对于开散列结构具体实现哈希表&#xff08;闭散列&#xff09;创建…

C++ 泛型编程(一):模板基础:函数模板、类模板、模板推演成函数的机制、模板实例化、模板匹配规则

文章目录泛型编程函数模板函数模板实例化隐式实例化显式实例化函数模板的匹配规则类模板类模板的实例化泛型编程 泛型编程旨在削减重复工作&#xff0c;如&#xff1a; 将一个函数多次重载不如将他写成泛型。 void Swap(int& left, int& right) {int temp left;lef…

你真的了解静态变量、常量的存储位置吗?

文章目录引言C对内存的划分如何落实在Linux上自由存储区和堆之间的问题栈常量区静态存储区静态局部变量静态局部变量、静态全局变量、全局变量的异同macOS系统的测试结果总结引言 在动态内存的博客中&#xff0c;我提到&#xff1a; 在Linux 内存管理的博客中&#xff0c;我提…