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

文章目录

  • 前言
  • 连续分配
    • 单一连续分配
    • 分区式分配
      • 固定分区分配
      • 动态分区分配
      • 可重定位分区分配
  • 离散分配
    • 分段
    • 分页
      • 多级页表
      • 快表(TLB)
    • 段页式
    • Linux


前言

Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配
Linux 内存管理 | 物理内存、内存碎片、伙伴系统、SLAB分配器

在之前的两篇博客中,分别介绍了虚拟内存与物理内存的管理方式,那么对于操作系统来说,它是如何管理它们两个之间的关系的呢?如何进行地址的映射呢?

内存的分配方式有两种:

  • 连续分配: 每个进程分配一段地址空间连续的内存空间。
    连续内存分配的方式有:

    1. 单一连续分配
    2. 分区式分配
      • 固定分区分配
      • 动态分区分配
    3. 可重定位分区分配
  • 离散分配: 允许将一个进程分散的分配到许多不相邻的分区中,程序全部装入内存。

现在用到的更多的是离散的分配方式,因此我们简单介绍一下连续分配,再对离散分配加以详解。


连续分配

单一连续分配

使用这种内存分配方式,内存空间会被分成 系统区用户区 两部分,系统区仅提供给OS使用,系统区外的用户区提供给用户使用。

特点:

  • 只适用于 单道程序 的情况。
  • 若用户作业比用户区大,则无法运行
  • 若用户作业比用户区小,则造成内存浪费
  • 设置界限寄存器,限制用户程序访问操作系统

单道程序的特点:

  1. 资源独占性: 任何时候,位于内存中的程序可以使用系统中的一切资源,不可能有其他程序与之竞争。
  2. 执行的顺序性: 内存中只有一个程序,各个程序是按次序执行的。在做完一个程序的过程中,不可能夹杂进另一个程序执行。
  3. 结果的可再现性: 只要执行环境和初始条件相同,重复执行一个程序,获得的结果总是一样的。
  4. 运行结果的无关性: 程序的运行结果与程序执行的速度无关。系统中的作业以串行的方式被处理,CPU、内存的利用率低。

分区式分配

固定分区分配

固定分区分配是最简单的一种可以运行在 多道程序 的存储管理方式。

多道程序: 是指在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。当然,对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个。

基本原理:

  • 将内存空间划分为若干个固定大小的分区(大小可以不等);
  • 每个分区中只可以装入一道作业;
  • 当有空闲分区时,选择一个适当大小的作业装入该分区;
  • 当作业结束时,释放该分区。

缺点:

  • 程序的大小受分区大小的限制;程序数受分区数限制;
  • 每个分区都有可能产生 内部碎片,引起内存的浪费。

动态分区分配

基本思想:

  • 作业要求装入内存时,依照作业的大小划分分区。
  • 每个分区容纳一个进程。

可变分区的管理与组织方式:

  • 表格法:将内存按是否空心啊分别存在 空闲分区表已分配分区表 中。管理简单,但是需要占用一部分内存空间。

在这里插入图片描述

  • 链表法:维护一个链首指针,每个空闲区在首地址记录两个数据:本空闲区的大小、下个空闲区的起始地址。
    在这里插入图片描述
    动态分区分配的内存回收方式:
  • 上邻空闲区(F1):合并,改大小
  • 下邻空闲区(F2):合并,改大小,首址。
  • 上、下邻空闲区(F1、F2):合并,改大小。
  • 不邻接,则建立一新表项。

在这里插入图片描述
动态分配分区内存分配算法:

  1. 首次适应算法: 将空闲分区按 地址顺序 排列,进行内存分配时,从低地址开始顺序查找,分配第一个足够大的分区。
    • 优点:优先分配低地址部分的空闲分区,保留高地址部分;
    • 缺点: 在低址部分集中了许多小分区,难以利用。
  2. 循环首次适应算法: 首次适应算法的改进版本。将空闲分区按地址顺序构成循环链表,进行内存分配时,不再从链首开始查找,而是从 上次找到的空闲分区的下一个空闲分区 开始查找,循环查找。
    • 优点:内存中的空闲分区分布均匀,比起首次适应算法减少了查找空闲分区时的开销;
    • 缺点:缺乏大的空闲分区。
  3. 最佳适应算法: 空闲分区按大小 递增排序 构成队列,从队列头开始查找,当找到第一个满足要求的空闲区时,则停止查找。
    • 优点:找到的空闲分区最接近要求的大小;
    • 缺点:会产生非常小的碎片,难以利用。
  4. 最差适应算法: 空闲分区按大小递增排序构成队列,查找最大的空闲区。与上面三个同属 顺序搜索法
    • 优点:剩余的分区空间最大;
    • 缺点:在空间利用率方面较差。

可重定位分区分配

假设现在有这样一个情况,用户内存空间中有几个较小的空闲分区,但是现在有一个作业请求连续的内存空间,这几个较小的空闲分区任何一个都不能单独满足请求空间的大小。 现在一种可行的办法就是:移动内存,使所有空闲区域合并为一整块空闲区域。

这种通过移动内存中的作业位置,将原来分散的小分区拼接成一个大分区的思想就是 紧凑 。 说的直白一点,可重定位分区分配就是 动态分区分配+紧凑

在这里插入图片描述
动态重定位的实现:

作业装入内存后的所有地址都是相对地址,在程序将要执行的时候,才会将相对地址转换为物理地址。为了不影响指令执行的速度,系统中增设了一个 重定位寄存器(即段寄存器、基址寄存器) ,用它来存放程序(数据)在内存中的起始地址。

程序真正执行时访问的地址是 重定位寄存器中的地址+相对地址


离散分配

分段

为了简化地址管理,所以将虚拟内存空间中的 虚拟内存 按照其逻辑划分为代码段、数据段、堆段、栈段几部分。编译、连接、加载过程都以段为单位。

在这里插入图片描述
段的特点:

  • 虚拟内存空间是段的集合。
  • 每个段都有其名称和长度。
  • 地址是由段名(段号)和段内偏移构成。

地址结构:

  • 虚拟地址是二维的:[段号,段内位移]
  • 32位地址结构中,
    • 段号s:16位表示,216 个段
    • 段内位移d:16位表示,每段最大长度是64KB。
      在这里插入图片描述

通过 段寄存器 中的 段表 ,将虚拟地址与物理地址进行映射。段表由三部分组成:

  • 段号:用于区别每个段。
  • 段基址(segment base):该段在物理内存中的首地址。
  • 段长(segment limit ):记录该段的实际长度。
    在这里插入图片描述

因此虚拟地址与物理地址的转换方式如下:

  1. 根据虚拟地址中的段号查询段表,得到对应的段的物理内存起始地址;
  2. 物理内存起始地址加上段内偏移,即为其对应的物理地址。

在这里插入图片描述
分段存储方式解决了两个问题 —— 地址空间不隔离程序运行的地址无法确定。但还存在 内存使用效率低 的问题。内存使用效率低主要是因为两个原因造成的:内存碎片内存交换的效率低

内存碎片问题

例如我们有 1G 的物理内存,倘若我们运行了 512M 的程序A,接着运行了 128M 的程序B,128M 的程序C。剩余内存为 256M

在这里插入图片描述
倘若我们此时结束程序B,释放内存,此时总剩余空间为 384M

在这里插入图片描述
倘若我们此时需要运行 300M 的进程D,但是这时候就会因为剩余空间不连续,导致我们的程序无法运行,这也就是我们常说的 内存外碎片 问题。

那么如何解决这个问题呢?这就会使用到类似于 紧凑 的思想。先将程序C写入硬盘的 SWAP分区 (交换分区,用于内存和硬盘的空间交换)。紧接着再将其从硬盘中读取回来,让其紧挨着程序A的那块内存,这样就能保证后面的空闲内存都是连续的了。

内存交换效率低

由于分段对物理内存的映射是以 程序 为单位,按照其逻辑进行分段映射,如果我们的内存不足,那么被换入换出到硬盘中的都是整个程序,这样就必然会造成大量的磁盘访问操作,总所周知,磁盘IO的速度特别慢,因此就会严重影响我们的访问速度。

而且,当一个程序在运行时,在某个时间段内,它只是 频繁地用到了一小部分数据 ,也就是说程序中的很多数据其实在一个时间段内都是不会被用到的。因此我们将整个程序装入内存其实是对内存的一种浪费。


分页

总结一下,分段技术仍未解决的问题有:

  1. 虽然分段式存储方式不畏惧 内存外碎片,但将内存中的数据暂时写入到硬盘中,之后再重新写回来这样的换入换出操作在程序很大时是很废时间的。值得一提的是,两者都无法摆脱 内碎片 的桎梏。
  2. 而且分段需要将程序全部装入内存,这就对程序的大小有了限制——不能超过剩余空闲内存的大小。

而分页技术解决上述两个问题的方法是:

  1. 使用页为单位后,即使我们还是需要进行磁盘IO,但是由于我们交换的容量仅仅只有几个页,所以也不会花费过多的时间。
  2. 分页技术下并不需要将程序整个装入内存。在建立了虚拟内存空间后并不会直接分配物理内存,而是在程序运行中需要访问物理内存的时候,再将其加载进内存中。所以如果在页表中查找不到时,此时就会由内核的 请求分页机制 产生 缺页中断 ,然后进入 内核态 中分配物理内存、更新进程页表,最后再返回用户态,恢复进程的运行。

基本概念:

  • 帧/物理块/页框(frame): 物理内存分为固定大小的块。
  • 页(page): 逻辑内存分为同样大小的块,在 Linux 中,一页是 4KB
  • 页表(page table): MMU(内存管理单元) 中的页面映射表,记录了 页框 的映射关系。

在这里插入图片描述
页表中不仅保存了页号,物理内存地址,还保留了该物理页的 访问权限 ,用以实现对页的访问控制。

在分页机制下,虚拟地址由 页号 以及 页内偏移 组成

因此在分页机制下,虚拟地址与物理地址的转换方式如下:

  1. 根据虚拟地址中的页号查询页表,获得对应的页的物理内存起始地址;
  2. 物理内存起始地址加上页内偏移,即为其对应的物理地址。

在这里插入图片描述


多级页表

在上面所介绍的 页表 有一个非常致命的缺点,就是空间占用大。

Linux 中,可以并发的执行多个进程,而每个进程都有其自己的虚拟内存空间,那么也自然都有自己独有的页表。在32位Linux系统下,我们的虚拟内存空间的大小为 4G ,而每页的大小为 4K ,这也就意味着我们至少有 220 个内存页,倘若每个页表项为 4Byte ,那么每个页表大小也至少为 4M

倘若我们此时并发了两百个进程,那么占用则高达 800M ,即使对于如今的操作系统而言,这个数字也是非常庞大的,因为并发数百个进程是非常常见的情况,更别提64位的操作系统,随着寻址范围的增加,页表将更为庞大。

为了解决这个问题,就引入了多级页表。

我们将 一级页表 再进行分页,分成 1024二级页表 ,并且每个 二级页表 中存有 1024 个页表项,形成如下的 二级分页 的结构。

在这里插入图片描述

在这里插入图片描述
对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。

如果一级页表所有表项都被用到,那么此时二级页表大小为 4M(1024 * 4K) ,假设我们只是用了一级页表的 20%

在这种情况下,页表所占用的物理内存就只有 4K(一级页表大小) + 20% * 4M(存在的二级页表) ,即 0.804M ,比起只用了二级页表的 4M (一级页表没有用到的表项不用创建对应的二级页表,因此此时存在的二级页表共 20% * 4M ,但单极页表时,二级页表必须建满 4M),大大的节约了内存。

而在64位系统中,两级页表是肯定不够用的,因此又演变成了四级目录:

  • 全局页目录项 PGD
  • 上层页目录项 PUD
  • 中间页目录项 PMD
  • 页表项 PTE

在这里插入图片描述


快表(TLB)

多级页表虽然解决了空间占用大的问题,但是由于其复杂化了地址的转换,因此也带来了 大量的时间开销 ,使得地址转换速度减慢。

解决这个问题最简单的方式就是降低查询页表的频率,那么如何实现呢?这时候就需要用到 缓存 的技术

对于热点资源,我们可以将其提前缓存下来,到以后使用时就可以直接到缓存中查找。对于操作系统来说,也是这么一个道理。

在操作系统中,这个缓存就是 CPU 中的 TLB ,也就是我们通常所说的 快表 。我们将 最常访问的几个页表项存储到 TLB ,在之后进行寻址时, CPU 就会先到 TLB 中进行查找,如果没有找到,这时才会去查询页表。


段页式

虽然分段和分页各有优缺点,但他们直接并不是对立的,所以如今大部分的内存管理方式,都是将分段与分页相结合,也就是我们常说的段页式。

它的原理非常简单,就是先对 虚拟内存空间进行分段管理,然后再对每一个段进行分页管理。 如下图:

在这里插入图片描述
所以此时的虚拟地址结构,就由 段号、段内页号、页内偏移 所组成。此时对于每个进程来说,都会建立一个段表,而对于段表中的每一个段,又会再分别建立一个页表:

在这里插入图片描述

在这里插入图片描述
以此时的虚拟地址转换为物理地址,就需要以下三个步骤:

  1. 访问段表,得到页表的起始地址;
  2. 访问页表,得到物理页的起始地址;
  3. 访问物理页,加上页内偏移,得到实际的物理地址。

这种方法虽然增加了系统开销以及硬件成本,但是内存的利用率得到了巨大的提升。


Linux

由于硬件问题的限制,Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。

在往常的机制中,地址的转换流程如下:

在这里插入图片描述
但是在 Linux 中,并没有逻辑地址这一说(所有段起始地址相同),因为其将段机制进行了弱化,此时段只用于进行访问控制以及内存保护。

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。

这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

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

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

相关文章

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

文章目录中断过程调用系统调用过程调用和系统调用的区别中断 用户态、内核态之间的切换是怎么实现的? 用户态→内核态 是通过中断实现的。并且 中断是唯一途径 。核心态→用户态 的切换是通过执行一个特权指令,将程序状态字 (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;我提…

C++ 泛型编程(二):非类型模板参数,模板特化,模板的分离编译

文章目录非类型模板参数函数模板的特化类模板的特化全特化偏特化部分参数特化参数修饰特化模板分离编译解决方法非类型模板参数 模板的参数分为两种&#xff1a; 类型参数&#xff1a; 则是我们通常使用的方式&#xff0c;就是在模板的参数列表中在 class 后面加上参数的类型…

数据结构 | B树、B+树、B*树

文章目录搜索结构B树B树的插入B树的遍历B树的性能B树B树的插入B树的遍历B*树B*树的插入总结搜索结构 如果我们有大量的数据需要永久存储&#xff0c;就需要存储到硬盘之中。但是硬盘的访问速度远远小于内存&#xff0c;并且由于数据量过大&#xff0c;无法一次性加载到内存中。…

MySQL 索引 :哈希索引、B+树索引、全文索引

文章目录索引引言常见的索引哈希索引自适应哈希索引B树索引聚集索引非聚集索引使用方法联合索引最左前缀匹配规则覆盖索引全文索引使用方法索引 引言 为什么需要索引&#xff1f; 倘若不使用索引&#xff0c;查找数据时&#xff0c;MySQL必须遍历整个表。而表越大&#xff0c;…

服装店怎么引流和吸引顾客 服装店铺收银系统来配合

实体店的同城引流和经营是实体经济的一个重要的一环&#xff0c;今天我们来分享服装行业的实体店铺怎么引流和吸引、留住顾客&#xff0c;并实现复购。大家点个收藏&#xff0c;不然划走就再也找不到了&#xff0c;另外可以点个关注&#xff0c;下次有新的更好的招&#xff0c;…