【操作系统原理】—— Linux内存管理

简介: 【操作系统原理】—— Linux内存管理

实验原理与内容

     操作系统的发展使得系统完成了大部分的内存管理工作,对于程序员而言,这些内存管理的过程是完全透明不可见的。因此,程序员开发时从不关心系统如何为自己分配内存,而且永远认为系统可以分配给程序所需的内存。在程序开发时,程序员真正需要做的就是:申请内存、使用内存、释放内存。其它一概无需过问。

内存管理

     Linux 内存管理是操作系统对计算机内存进行分配、保护和回收的一系列机制。这些机制确保了程序能够访问到合适的内存空间,同时防止程序越界访问或滥用系统资源。 以下是 Linux 内存管理的一些关键方面:

     虚拟内存: Linux 使用虚拟内存机制,将物理内存和进程地址空间分隔开来。每个进程都有自己的虚拟地址空间,而不必担心物理内存的具体位置。这允许多个进程共享相同的代码,同时保持各自的独立地址空间。

     分页机制: 内存被划分为固定大小的页面,通常是4KB。虚拟地址空间和物理地址空间被分割成相同大小的页面,这样就可以按页进行映射。分页机制有助于提高内存的利用率和管理效率。

     页表: 为了实现虚拟地址到物理地址的映射,Linux 使用页表。页表记录了每个页面的虚拟地址到物理地址的映射关系。当进程访问某个虚拟地址时,操作系统通过页表查找相应的物理地址。

     分段机制: 除了分页,Linux 还使用分段机制。这将虚拟地址空间分为多个段,每个段都有不同的权限。例如,代码段、数据段、堆、栈等。

     内存保护: Linux 使用页面保护位(read, write, execute)来控制对内存的访问权限。这有助于防止程序越界访问和滥用内存。

     内存分配器: Linux 提供了多种内存分配器,如 malloc 和 free 函数使用的 glibc 库中的 ptmalloc。此外,Linux 内核也提供了 kmalloc 和 kfree 等用于内核空间的内存分配函数。

     交换空间: 为了支持虚拟内存,Linux 使用了交换空间(swap space),它是物理磁盘上的一部分,用于存储不常用的页面。当物理内存不足时,操作系统可以将不常用的页面交换到磁盘上,以释放物理内存。

     OOM Killer: 当系统内存不足时,Linux 内核可能会触发 Out Of Memory Killer(OOM Killer),它会杀死某些进程以释放内存。OOM Killer的目标是维护系统的稳定性,但它也可能导致未预期的程序中断。

     这些是 Linux 内存管理的基本原理和机制。内存管理在操作系统中扮演着至关重要的角色,它直接影响到系统的性能、稳定性和安全性。

实验要求

     练习一:用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果,分析遇到的问题和解决方法。

     练习二:用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果。

     练习三:用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果。

     改编实验中的程序,并运行出结果。

在虚拟机中编写好以下程序:

#include <stdio.h> 
#include <string.h> 
#include <malloc.h>
int main(void)
{
  char *str;
  /* 为字符串申请分配一块内存 */
  if ((str = (char *) malloc(10)) == NULL)
  {
      printf("Not enough memory to allocate buffer\n");
      return(1); /* 若失败则结束程序 */
  }
  /* 拷贝字符串“Hello”到已分配的内存空间 */
  strcpy(str, "Hello");
  /* 显示该字符串 */
  printf("String is %s\n", str);
  /* 内存使用完毕,释放它 */
  free(str);
  return 0; 
}

     调试过后得出的结果截图如下:

#include <stdio.h>
#include <malloc.h>
#include <string.h>
int main(void)
{
  char *str; /* 为字符串申请分配一块内存 */
  if ((str = (char *) malloc(10)) == NULL)
  {
    printf("Not enough memory to allocate buffer\n");
    return(1); /* 若失败则结束程序 */
  } /* 复制 "Hello" 字符串到分配到的内存 */
  strcpy(str, "Hello"); /* 打印出字符串和其所在的地址 */
  printf("String is %s\n Address is %p\n", str, str); /* 重分配刚才申请到的内存空间, 申请 增大一倍 */
  if ((str = (char *) realloc(str, 20)) == NULL) { 
    printf("Not enough memory to allocate
    buffer\n"); return(1); /* 监测申请结果, 若失败则结束程序,养成这个好习惯 */ 
  } /* 打印出重分配后的地址 */
  printf("String is %s\n New address is %p\n", str, str); /* 释放内存空间 */ free(str);
  return 0;
}

调试过后得出结果截图如下:

#include <stdio.h>
#include <alloca.h>
void test(int a)
{
  char *newstack;/*  申请一块内存空间 */
  newstack = (char *) alloca(len);
  if (newstack)/*  若成功,则打印出空间大小和起始地址 */
    printf("Alloca(0x%X) returned %p\n”, len, newstack);
  else/*  失败则报告错误, 我们是做实验, 目前无需退出 */
  printf("Alloca(0x%X) failed\n",len);
} /*  函数退出, 内存自动释放, 无需干预 */
void main()
{
/*  申请一块 256 字节大小的内存空间,观察输出情况 */
  test(256);
/*  再申请一块更大内存空间,观察输出情况 */
  test(16384);
}

调试结果截图如下:

     对上述程序改写, 将程序中的“Hello”改为“Myname is 自己的名字!”,调试并观察结果。再将程序中的“10”和“20”分别改成“20”和“40”再进行调试,观察结果,并对比说明原因。

实验设备与软件环境

安装环境:分为软件环境和硬件环境

硬件环境:内存ddr3 4G及以上的x86架构主机一部

系统环境:windows 、linux或者mac os x

软件环境:运行vmware或者virtualbox

软件环境:Ubuntu操作系统

实验内容

练习一:

     用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果。

运行代码及截图:

结果分析:

     练习一的代码通过malloc(10)分配10字节的内存,通过(char*)进行强制类型转换,因为malloc函数的返回类型是void*,需要强制转换成需要的类型(char)。如果成功为字符串申请分配一块内存,程序则通过strcpy()函数拷贝字符串"Hello"到已分配的内存空间里面,接着程序进行输出,最后通过free函数释放内存空间。

如果没有成功为字符串申请分配一块内存,程序会输出报错信息"Not enough memory to allocate buffer"并结束程序。

改编实验中的程序,并运行出结果。

改编内容:将程序中的“Hello”改为“My name is 自己的名字!”

改编后的程序的运行代码及截图:

结果分析:

     改编后的练习一的代码通过malloc(10)分配10字节的内存,通过(char*)进行强制类型转换,因为malloc函数的返回类型是void*,需要强制转换成需要的类型(char)。如果成功为字符串申请分配一块内存,程序则通过strcpy()函数拷贝字符串"My name is 姓名!"到已分配的内存空间里面,接着程序进行输出,最后通过free函数释放内存空间。

     如果没有成功为字符串申请分配一块内存,程序会输出报错信息"Not enough memory to allocate buffer"并结束程序。

     修改前后的程序不同的地方就是strcpy()函数拷贝的内容不同。


练习二:

     用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果。

运行代码及截图:

结果分析:

     练习二的代码通过malloc(10)分配10字节的内存,通过(char*)进行强制类型转换,因为malloc函数的返回类型是void*,需要强制转换成需要的类型(char)。如果成功为字符串申请分配一块内存,程序则通过strcpy()函数拷贝字符串"Hello"到已分配的内存空间里面,接着程序进行输出,打印出字符串和其所在的地址。

如果没有成功为字符串申请分配一块内存,程序会输出报错信息"Not enough memory to allocate buffer"并结束程序。

     接着通过realloc函数将数组扩容,对于刚才申请到的内存空间进行重分配,申请增大一倍的内存容量(分配20字节的内存),通过检测判断是否重分配成功,若失败则结束程序,反之则打印出字符串和重分配后的地址,最后通过free函数释放内存空间。

     值得注意的是通过重分配后的地址和重分配前的地址是一样的。这是因为如果当前连续内存块足够realloc进行分配的话,只是将str所指向的空间扩大,并返回它的指针地址。 这个时候重新分配前后指向的地址是一样的。

改编实验中的程序,并运行出结果。

改编内容1:将程序中的“10”和“20”分别改成“20”和“40”再进行调试

运行代码及截图:

结果分析:

     改编后的练习二的代码和之前不同的地方在于程序则通过strcpy()函数拷贝字符串"Hello"变为了"My name is 姓名!"。

     但是值得注意的是通过重分配后的地址和重分配前的地址还是一样的。,只是和上一次的地址不一样,因为每次分配的地址都是随机的。

改编内容2:将程序中的“10”和“20”分别改成“20”和“40”再进行调试

运行代码及截图:

结果分析:

     再次改编后的练习二的代码通过malloc(10)分配20字节的内存,通过(char*)进行强制类型转换,因为malloc函数的返回类型是void*,需要强制转换成需要的类型(char)。如果成功为字符串申请分配一块内存,程序则通过strcpy()函数拷贝字符串"My name is 姓名!"到已分配的内存空间里面,接着程序进行输出,打印出字符串和其所在的地址。

     如果没有成功为字符串申请分配一块内存,程序会输出报错信息"Not enough memory to allocate buffer"并结束程序。

     接着通过realloc函数将数组扩容,对于刚才申请到的内存空间进行重分配,申请增大一倍的内存容量(分配40字节的内存),通过检测判断是否重分配成功,若失败则结束程序,反之则打印出字符串和重分配后的地址,最后通过free函数释放内存空间。

     值得注意的我们通过对比可以发现修改后的通过重分配后的地址和重分配前的地址是不一样的。这是因为如果当前连续内存块不够长度的时候,需要再找一个足够长的地方来分配一块新的内存,并将原本指向的内容copy到新地址,返回新地址。并将str所指向的原来的内存空间删除。

     这样也就是说realloc()函数有时候会产生一个新的内存地址,有的时候不会。所以在分配完成后我们需要判断下空间是否等于可以进行再分配,并做相应的处理。


练习三:

     用 vim 编辑创建下列文件,用 GCC 编译工具,生成可调试的可执行文件,记录并分析执行结果。

运行代码及截图:

结果分析:

     练习三的代码写了一个void test(int a)函数,通过这个函数可以申请一块内存空间,如果成功申请到内存空间,则打印出空间大小和起始地址,如果申请失败则报告错误。

     在主函数我们申请了两块内存不同大小的空间,一块256字节,一块更大的16384字节。

     通过运行结果我们可以看到两次申请都成功分配了空间,而且对应的空间大小和起始地址都不同。和预期效果一致。

问题与解决方法

问题一:

     编译练习三的时候出现报错,下面为报错信息:

解决方法:

     通过检查代码发现问题为写成了中文的引号,以及练习所给的代码没有对于len进行定义。通过修改后再次运行便没有问题了。

问题二:

     为什么通过重分配后的地址和重分配前的地址有的时候是一样的,有的时候是不一样的。

解决方法:

     如果当前连续内存块足够realloc进行分配的话,只是将str所指向的空间扩大,并返回它的指针地址。 这个时候重新分配前后指向的地址是一样的。

     如果当前连续内存块不够长度的时候,需要再找一个足够长的地方来分配一块新的内存,并将原本指向的内容copy到新地址,返回新地址。并将str所指向的原来的内存空间删除。

     这样也就是说realloc()函数有时候会产生一个新的内存地址,有的时候不会。所以在分配完成后我们需要判断下空间是否等于可以进行再分配,并做相应的处理。


相关文章
|
12天前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
13天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
34 0
Vanilla OS:下一代安全 Linux 发行版
|
24天前
|
安全 Linux 编译器
探索Linux内核的奥秘:从零构建操作系统####
本文旨在通过深入浅出的方式,带领读者踏上一段从零开始构建简化版Linux操作系统的旅程。我们将避开复杂的技术细节,以通俗易懂的语言,逐步揭开Linux内核的神秘面纱,探讨其工作原理、核心组件及如何通过实践加深理解。这既是一次对操作系统原理的深刻洞察,也是一场激发创新思维与实践能力的冒险。 ####
|
6天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
27 9
|
6天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
6天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
22 2
|
11天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
10天前
|
缓存 网络协议 Linux
Linux操作系统内核
Linux操作系统内核 1、进程管理: 进程调度 进程创建与销毁 进程间通信 2、内存管理: 内存分配与回收 虚拟内存管理 缓存管理 3、驱动管理: 设备驱动程序接口 硬件抽象层 中断处理 4、文件和网络管理: 文件系统管理 网络协议栈 网络安全及防火墙管理
31 4
|
8天前
|
安全 网络协议 Linux
Linux操作系统的内核升级与优化策略####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统内核升级的重要性,并详细阐述了一系列优化策略,旨在帮助系统管理员和高级用户提升系统的稳定性、安全性和性能。通过实际案例分析,我们展示了如何安全有效地进行内核升级,以及如何利用调优技术充分发挥Linux系统的潜力。 ####
28 1
|
12天前
|
物联网 Linux 云计算
Linux操作系统的演变与未来趋势####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统从诞生至今的发展历程,分析了其在服务器、桌面及嵌入式系统领域的应用现状,并展望了云计算、物联网时代下Linux的未来趋势。通过回顾历史、剖析现状、预测未来,本文旨在为读者提供一个全面而深入的视角,以理解Linux在当今技术生态中的重要地位及其发展潜力。 ####