实验原理与内容
操作系统的发展使得系统完成了大部分的内存管理工作,对于程序员而言,这些内存管理的过程是完全透明不可见的。因此,程序员开发时从不关心系统如何为自己分配内存,而且永远认为系统可以分配给程序所需的内存。在程序开发时,程序员真正需要做的就是:申请内存、使用内存、释放内存。其它一概无需过问。
内存管理
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()函数有时候会产生一个新的内存地址,有的时候不会。所以在分配完成后我们需要判断下空间是否等于可以进行再分配,并做相应的处理。