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


目录
相关文章
|
7天前
|
存储 Linux 调度
深入理解操作系统:从进程管理到内存分配
【8月更文挑战第44天】本文将带你深入操作系统的核心,探索其背后的原理和机制。我们将从进程管理开始,理解如何创建、调度和管理进程。然后,我们将探讨内存分配,了解操作系统如何管理计算机的内存资源。最后,我们将通过一些代码示例,展示这些概念是如何在实际操作系统中实现的。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和深入的理解。
|
15天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
23天前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
5天前
|
存储 安全 Linux
探索操作系统:从原理到实践
【9月更文挑战第14天】本文深入探讨了操作系统的核心概念,通过分析其设计原则和功能,揭示了操作系统如何管理计算机硬件资源、提供用户接口并确保系统安全。文章不仅阐述了操作系统的基本原理,还通过实际代码示例展示了如何在操作系统上进行编程,旨在帮助读者更好地理解并应用操作系统知识。
11 1
|
8天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
24 3
|
12天前
|
算法 调度 UED
操作系统中的进程管理:原理与实践
在数字世界的心脏跳动着无数进程,它们如同细胞一般构成了操作系统的生命体。本文将深入探讨进程管理的奥秘,从进程的诞生到成长,再到最终的消亡,揭示操作系统如何协调这些看似杂乱无章却又井然有序的活动。通过浅显易懂的语言和直观的比喻,我们将一起探索进程调度的策略、同步机制的重要性以及死锁问题的解决之道。准备好跟随我们的脚步,一起走进操作系统的微观世界,解锁进程管理的秘密吧!
23 6
|
8天前
|
存储 数据挖掘 Linux
服务器数据恢复—Linux操作系统网站服务器数据恢复案例
服务器数据恢复环境: 一台linux操作系统服务器上跑了几十个网站,服务器上只有一块SATA硬盘。 服务器故障: 服务器突然宕机,尝试再次启动失败。将硬盘拆下检测,发现存在坏扇区
|
20天前
|
安全 Linux 开发工具
探索Linux操作系统:从命令行到脚本编程
【8月更文挑战第31天】在这篇文章中,我们将一起潜入Linux操作系统的海洋,从最基础的命令行操作开始,逐步深入到编写实用的脚本。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技能。我们将通过实际代码示例,展示如何在日常工作中利用Linux的强大功能来简化任务和提高效率。准备好了吗?让我们一起开启这段旅程,探索Linux的奥秘吧!
|
23天前
|
存储 算法 网络协议
了解操作系统的基本原理和常见操作,提高计算机使用效率
了解操作系统的基本原理和常见操作,提高计算机使用效率
24 4
|
21天前
|
存储 人工智能 数据管理
深入理解Linux操作系统之文件系统管理探索人工智能:从理论到实践的旅程
【8月更文挑战第30天】在探索Linux的无限可能时,我们不可避免地会遇到文件系统管理这一核心话题。本文将深入浅出地介绍Linux文件系统的基础知识、操作命令及高级技巧,帮助你更有效地管理和维护你的系统。从基础概念到实践应用,我们将一步步揭开Linux文件系统的神秘面纱。

热门文章

最新文章