【操作系统原理】—— 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()函数有时候会产生一个新的内存地址,有的时候不会。所以在分配完成后我们需要判断下空间是否等于可以进行再分配,并做相应的处理。


相关文章
|
3天前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
3天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
12 0
Vanilla OS:下一代安全 Linux 发行版
|
16天前
|
人工智能 分布式计算 大数据
Linux操作系统:开源力量的崛起与影响###
一场技术革命的火种,如何燎原? 本文将带您深入探索Linux操作系统的诞生背景、核心特性及其对现代科技世界的深远影响。从1991年芬兰学生Linus Torvalds的一个小众项目,到如今成为支撑全球无数服务器、超级计算机及物联网设备的基石,Linux的发展既是一部技术创新史,也是开源文化胜利的见证。通过剖析其设计哲学、安全性、灵活性等关键优势,结合实例展示Linux在云计算、大数据处理等领域的广泛应用,本文旨在揭示Linux为何能在众多操作系统中脱颖而出,以及它如何塑造了我们今天的数字生活。 ###
|
14天前
|
安全 Linux 编译器
探索Linux内核的奥秘:从零构建操作系统####
本文旨在通过深入浅出的方式,带领读者踏上一段从零开始构建简化版Linux操作系统的旅程。我们将避开复杂的技术细节,以通俗易懂的语言,逐步揭开Linux内核的神秘面纱,探讨其工作原理、核心组件及如何通过实践加深理解。这既是一次对操作系统原理的深刻洞察,也是一场激发创新思维与实践能力的冒险。 ####
|
1天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
2天前
|
物联网 Linux 云计算
Linux操作系统的演变与未来趋势####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统从诞生至今的发展历程,分析了其在服务器、桌面及嵌入式系统领域的应用现状,并展望了云计算、物联网时代下Linux的未来趋势。通过回顾历史、剖析现状、预测未来,本文旨在为读者提供一个全面而深入的视角,以理解Linux在当今技术生态中的重要地位及其发展潜力。 ####
|
7天前
|
边缘计算 人工智能 运维
Linux操作系统:开源力量的崛起与影响###
一场技术革命的回顾 回溯至1991年,当Linus Torvalds宣布Linux操作系统的诞生时,世界或许并未意识到这一举措将如何深刻地改变技术领域的面貌。本文旨在探讨Linux操作系统的发展历程、核心特性、以及它如何引领了一场开源运动,重塑了软件行业的生态。从最初的个人爱好项目成长为全球最广泛采用的服务器操作系统之一,Linux的故事是技术创新与社区精神共同推动下的辉煌篇章。 ###
|
6天前
|
人工智能 安全 Linux
|
8天前
|
物联网 Linux 5G
Linux操作系统的演变与未来趋势####
本文深入探讨了Linux操作系统的发展历程,从最初的一个学生项目到如今全球最流行的开源操作系统之一。文章将分析Linux的核心优势、关键特性以及它在云计算、物联网和嵌入式系统中的应用前景。通过具体案例展示Linux如何推动技术创新,并预测其在未来技术生态中的角色。本文旨在为读者提供一个全面而深入的理解,帮助他们认识到Linux在现代计算环境中的重要性及其未来的潜力。 ####
|
8天前
|
人工智能 安全 物联网
Linux操作系统的演变与未来:从开源精神到万物互联的基石###
本文是关于Linux操作系统的演变、现状与未来的深度探索。Linux,这一基于Unix的开源操作系统,自1991年由林纳斯·托瓦兹(Linus Torvalds)学生时代创造以来,已经彻底改变了我们的数字世界。文章首先追溯了Linux的起源,解析其作为开源项目的独特之处;随后,详细阐述了Linux如何从一个小众项目成长为全球最广泛采用的操作系统之一,特别是在服务器、云计算及嵌入式系统领域的主导地位。此外,文章还探讨了Linux在推动技术创新、促进协作开发模式以及保障信息安全方面的作用,最后展望了Linux在未来技术趋势中的角色,包括物联网、人工智能和量子计算等前沿领域的潜在影响。 ###