实际上这是一个内存方面的问题。要想研究这个问题,首先我们要将题目本身搞明白。由于我对Linux内核比较熟而对Windows的内存模型几乎毫不了解,因此在这篇文章中针对Linux环境对这个问题进行探讨。

在Linux的世界中,从大的方面来讲,有两块内存,一块叫做内存空间,Kernel Space,另一块叫做用户空间,即User Space。它们是相互独立的,Kernel对它们的管理方式也完全不同。

首先我们要知道,现代操作系统一个重要的任务之一就是管理内存。所谓内存,就是内存条上一个一个的真正的存储单元,实实在在的电子颗粒,这里面通过电信号保存着数据。

Linux Kernel为了使用和管理这些内存,必须要给它们分成一个一个的小块,然后给这些小块标号。这一个一个的小块就叫做Page,标号就是内存地址,Address。

Linux内核会负责管理这些内存,保证程序可以有效地使用这些内存。它必须要能够管理好内核本身要用的内存,同时也要管理好在Linux操作系统上面跑的各种程序使用的内存。因此,Linux将内存划分为Kernel Space和User Space,对它们分别进行管理。

只有驱动模块和内核本身运行在Kernel Space当中,因此对于这道题目,我们主要进行考虑的是User Space这一块。

在Linux的世界中,Kernel负责给用户层的程序提供虚地址而不是物理地址。举个例子:A手里有20张牌,将它们命名为1-20。这20张牌要分给两个人,每个人手里10张。这样,第一个人拿到10张牌,将牌编号为1-10,对应A手里面的1-10;第二个人拿到10张牌,也给编号为1-10,对应A的11-20。

这里面,第二个人手里的牌,他自己用的时候编号是1-10,但A知道,第二个人手里的牌在他这里的编号是11-20。

在这里面,A的角色就是Linux内核;他手里的编号,1-20,就是物理地址;两个人相当于两个进程,它们对牌的编号就是虚地址;A要负责给两个人发牌,这就是内存管理。

了解了这些概念以后,我们来看看kernel当中具体的东西,首先是mm_struct这个结构体:

C代码  

struct mm_struct {

struct vm_area_struct * mmap;           /* list of VMAs */

struct rb_root mm_rb;

struct vm_area_struct * mmap_cache;     /* last find_vma result */

...

unsigned long start_code, end_code, start_data, end_data;

unsigned long start_brk, brk, start_stack;

...

};

mm_struct负责描述进程的内存。相当于发牌人记录给谁发了哪些牌,发了多少张,等等。那么,内存是如何将内存进行划分的呢?也就是说,发牌人手里假设是一大张未裁剪的扑克纸,他是怎样将其剪成一张一张的扑克牌呢?上面的vm_area_struct就是基本的划分单位,即一张一张的扑克牌:

C代码  

struct vm_area_struct * mmap;

这个结构体的定义如下:

C代码  

struct vm_area_struct {

struct mm_struct * vm_mm;       /* The address space we belong to. */

unsigned long vm_start;         /* Our start address within vm_mm. */

unsigned long vm_end;           /* The first byte after our end address

within vm_mm. */

....

/* linked list of VM areas per task, sorted by address */

struct vm_area_struct *vm_next;

....

}

这样,内核就可以记录分配给用户空间的内存了。

Okay,了解了内核管理进程内存的两个最重要的结构体,我们来看看用户空间的内存模型。

Linux操作系统在加载程序时,将程序所使用的内存分为5段:text(程序段)、data(数据段)、bss(bss数据段)、heap(堆)、stack(栈)。

text segment(程序段)

text segment用于存放程序指令本身,Linux在执行程序时,要把这个程序的代码加载进内存,放入text segment。程序段内存位于整个程序所占内存的最上方,并且长度固定(因为代码需要多少内存给放进去,操作系统是清楚的)。

data segment(数据段)

data segment用于存放已经在代码中赋值的全局变量和静态变量。因为这类变量的数据类型(需要的内存大小)和其数值都已在代码中确定,因此,data segment紧挨着text segment,并且长度固定(这块需要多少内存也已经事先知道了)。

bss segment(bss数据段)

bss segment用于存放未赋值的全局变量和静态变量。这块挨着data segment,长度固定。

heap(堆)

这块内存用于存放程序所需的动态内存空间,比如使用malloc函数请求内存空间,就是从heap里面取。这块内存挨着bss,长度不确定。

stack(栈)

stack用于存放局部变量,当程序调用某个函数(包括main函数)时,这个函数内部的一些变量的数值入栈,函数调用完成返回后,局部变量的数值就没有用了,因此出栈,把内存让出来给另一个函数的变量使用(程序在执行时,总是会在某一个函数调用里面)。

我们看一个图例说明:

为了更好的理解内存分段,可以撰写一段代码:

C代码  

#include

// 未赋值的全局变量放在dss段

int global_var;

// 已赋值的全局变量放在data段

int global_initialized_var = 5;

void function() {

int stack_var; // 函数中的变量放在stack中

// 放在stack中的变量

// 显示其所在内存地值

printf("the function's stack_var is at address 0x%08x\n", &stack_var);

}

int main() {

int stack_var; // 函数中的变量放在stack中

// 已赋值的静态变量放在data段

static int static_initialized_var = 5;

// 未赋值的静态变量放在dss段

static int static_var;

int *heap_var_ptr;

// 由malloc在heap中分配所需内存,

// heap_var_ptr这个指针指向这块

// 分配的内存

heap_var_ptr = (int *) malloc(4);

// 放在data段的变量

// 显示其所在内存地值

printf("====IN DATA SEGMENT====\n");

printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);

printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);

// 放在bss段的变量

// 显示其所在内存地值

printf("====IN BSS SEGMENT====\n");

printf("static_var is at address 0x%08x\n", &static_var);

printf("global_var is at address 0x%08x\n\n", &global_var);

// 放在heap中的变量

// 显示其所在内存地值

printf("====IN HEAP====\n");

printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);

// 放在stack中的变量

// 显示其所在内存地值

printf("====IN STACK====\n");

printf("the main's stack_var is at address 0x%08x\n", &stack_var);

function();

}

编译这个代码,看看执行结果:

理解了进程的内存空间使用,我们现在可以想想,这几块内存当中,最灵活的是哪一块?没错,是Heap。其它几块都由C编译器编译代码时预处理,相对固定,而heap内存可以由malloc和free进行动态的分配和销毁。

有关malloc和free的使用方法,在本文中我就不再多说,这些属于基本知识。我们在这篇文章中要关心的是,malloc是如何工作的?实际上,它会去调用mmap(),而mmap()则会调用内核,获取VMA,即前文中看到的vm_area。这一块工作由c库向kernel发起请求,而由kernel完成这个请求,在kernel当中,有vm_operations_struct进行实际的内存操作:

C代码  

struct vm_operations_struct {

void (*open)(struct vm_area_struct * area);

void (*close)(struct vm_area_struct * area);

...

};

可以看到,kernel可以对VMA进行open和close,即收发牌的工作。理解了malloc的工作原理,free也不难了,它向下调用munmap()。

下面是mmap和munmap的函数定义:

C代码  

void *

mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

这里面,addr是希望能够分配到的虚地址,比如:我希望得到一张牌,做为我手里编号为2的那张。需要注意的是,mmap最后分配出来的内存地址不一定是你想要的,可能你请求一张编号为2的扑克,但发牌人控制这个编号过程,他会给你一张在你手里编号为3的扑克。

prot代表对进程对这块内存的权限:

C代码  

PROT_READ 是否可读

PROT_WRITE 是否可写

PROT_EXEC IP指针是否可以指向这里进行代码的执行

PROT_NONE 不能访问

flags代表用于控制很多的内存属性,我们一会儿会用到,这里不展开。

fd是文件描述符。我们这里必须明白一个基本原理,任何硬盘上面的数据,都要读取到内存当中,才能被程序使用,因此,mmap的目的就是将文件数据映射进内存。因此,要在这里填写文件描述符。如果你在这里写-1,则不映射任何文件数据,只是在内存里面要上这一块空间,这就是malloc对mmap的使用方法。

offset是文件的偏移量,比如:从第二行开始映射。文件映射,不是这篇文章关心的内容,不展开。

okay,了解了mmap的用法,下面看看munmap:

C代码  

int

munmap(void *addr, size_t len);

munmap很简单,告诉它要还回去的内存地址(即哪张牌),然后告诉它还回去的数量(多少张),其实更准确的说:尺寸。

现在让我们回到题目上来,如何部分地回收一个数组中的内存?我们知道,使用malloc和free是无法完成的:

C代码  

#include

int main() {

int *p = malloc(12);

free(p);

return 0;

}

因为无论是malloc还是free,都需要我们整体提交待分配和销毁的全部内存。于是自然而然想到,是否可以malloc分配内存后,然后使用munmap来部分地释放呢?下面是一个尝试:

C代码  

#include

#include

#include

int main() {

int *arr;

int *p;

p = arr = (int*) malloc(3 * sizeof(int));

int i = 0;

for (i=0;i<3;i++) {

*p = i;

printf("address of arr[%d]: %p\n", i, p);

p++;

}

printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));

}

运行这段代码输出如下:

注意到munmap调用返回-1,说明内存释放未成功,这是由于munmap处理的内存地址必须页对齐(Page Aligned)。在Linux下面,kernel使用4096 byte来划分页面,而malloc的颗粒度更细,使用8 byte对齐,因此,分配出来的内存不一定是页对齐的。为了解决这个问题,我们可以使用memalign或是posix_memalign来获取一块页对齐的内存:

C代码  

#include

#include

#include

int main() {

void *arr;

printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 4096));

printf("address of arr: %p\n", arr);

printf("munmap: %d\n", munmap(arr, 4096));

}

运行上述代码得结果如下:

可以看到,页对齐的内存资源可以被munmap正确处理(munmap返回值为0,说明执行成功)。仔细看一下被分配出来的地址:

Bash代码  

0x7fe09b804000

转换到10进制是:140602658275328

试试看是否能被4096整除:140602658275328 / 4096 = 34326820868

可以被整除,验证了分配出来的地址是页对齐的。

接下来,我们试用一下mmap,来分配一块内存空间:

C代码  

mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)

注意上面mmap的使用方法。其中,我们不指定虚地址,让内核决定内存地址,也就是说,我们要是要一张牌,但不关心给牌编什么号。然后PROT_READ|PROT_WRITE表示这块内存可读写,接下来注意flags里面有MAP_ANONYMOUS,表示这块内存不用于映射文件。下面是完整代码:

C代码  

#include

#include

#include

int main() {

int *arr;

int *p;

p = arr = (int*) mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

int i = 0;

for (i=0;i<3;i++) {

*p = i;

printf("address of arr[%d]: %p\n", i, p);

p++;

}

printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));

}

运行结果如下:

注意munmap返回值为0,说明内存释放成功了。因此,验证了mmap分配出来的内存是页对齐的。

okay,了解了所有这些背景知识,我们现在应该对给内存打洞这个问题有一个思路了。我们可以创建以Page为基本单元的内存空间,然后用munmap在上面打洞。下面是实验代码:

C代码  

#include

#include

#include

int main() {

void *arr;

printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 3 * 4096));

printf("address of arr: %p\n", arr);

printf("address of arr[4096]: %p\n", &arr[4096]);

printf("munmap: %d\n", munmap(&arr[4096], 4096));

}

我们申请了3*4096 byte的空间,也就是3页的内存,然后通过munmap,在中间这页上开个洞 。运行上面的代码,结果如下:

看到munmap的返回为0,说明内存释放成功,我们在arr数组上成功地开了一个洞。

这种方法,最大的局限在于,你操作的内存必须是page对齐的。如果想要更细颗粒度的打洞,纯靠User Space的API调用是不行的,需要在Kernel Space直接操作进程的VMA结构体来实现。实现思路如下:

1. 通过kernel提供的page map映射,找到要释放的内存虚地址所对应的物理地址。

2. 撰写一个内核模块,帮助你user space的程序来将实际的物理内存放回free list。

linux 读取内存颗粒,Linux中的内存管理模型浅析相关推荐

  1. 跟着石头哥哥学cocos2d-x(三)---2dx引擎中的内存管理模型

    2019独角兽企业重金招聘Python工程师标准>>> 2dx引擎中的对象内存管理模型,很简单就是一个对象池+引用计数,本着学好2dx的好奇心,先这里开走吧,紧接上面两节,首先我们看 ...

  2. linux 读取内存颗粒,linux查看主板内存槽与内存信息的命令dmidecode怎么用

    在Linux中,我们常常使用命令来实现许多操作,比如查看内存信息等,下面小编就为大家带来一篇linux查看主板内存槽与内存信息的命令dmidecode方法.小编觉得挺不错的,现在就分享给大家,也给大家 ...

  3. Linux驱动学习--android中的内存管理机制ION(一)--简单介绍

    目录 一.引言 二.ION的介绍及使用 ------> ION介绍 ------> ION的使用 ------> HEAP种类 三.接口分析 ------> 主要数据结构 -- ...

  4. Linux驱动学习--android中的内存管理机制ION(二)--ION的使用

    目录 一.引言 二.ION框架 ------> heap类型 ------> ion特性 三.ion的使用 ------> 基本数据结构 ------> 具体的使用 四.ion ...

  5. linux 读取内存颗粒,Linux虚拟内存地址转化成物理内存地址

    背景 现代手机这种SOC(system on chip),因为功耗.Modem等功能soc上集成了很多core,他们还可以是独立的系统在运转. 比如ADSP简介ADSP(Application Dig ...

  6. optee中MMU内存管理模型-页表的建立模型

    思考: 1.__identity_map_init_start 的干啥的,作用? ----看起来是给ALSR用的,暂且不研究 MMU页表的创建模型,在optee os系统中,有很多种类型的memory ...

  7. 分布式存储开发:Curve中的内存管理

    前言 Curve 实践过程中遇到过几次内存相关的问题,与操作系统内存管理相关的是以下两次: chunkserver 上内存无法释放 mds 出现内存缓慢增长的现象 内存问题在开发阶段大多很难发现,测试 ...

  8. 6.关于QT中的内存管理,动态的制作,动态库的调用,静态库的制作

     一  QT的内存管理 1  QT中的内存管理是QObject来管理的 2  QT中的内存管理没有cocos2dx中的引用计数 3  组件可以指定父对象 QTimer *timer = QTime ...

  9. JVM基础 之Java HotSpot虚拟机中的内存管理

    1  简介 依托JavaTM 2平台的力量,标准版(J2SETM)实现了内存的自动管理,将开发人员从复杂的显式内存管理中解放出来. 本文将对Sun公司的J2SE发行版中的Java HotSpot虚拟机 ...

最新文章

  1. JavaScript创建对象–如何在JS中定义对象
  2. extract ,eval的用法
  3. 关于vue,angularjs1,react之间的对比
  4. lintcode:二叉树的中序遍历
  5. 辗转相除法(欧几里得算法)求 最大公约数与最小公倍数+推论与证明。
  6. 从青铜到王者,代码人生之路 | 凌云时刻
  7. 安装多个不同版本chrome浏览器
  8. HART/EtherNet IP网关HEI-612
  9. smobiler介绍(一)
  10. 各类图像数据大集合(下载链接)
  11. 抽奖小程序可以用html写吗,jquery 抽奖小程序实现代码
  12. 利用朴素贝叶斯算法解决“公园凉鞋问题”
  13. javascript技巧收集(200多个)
  14. 工作流Airflow的性能优化,应对dag数目的激增
  15. 光电池和光电二极管的区别
  16. jupyter notebook 打开md文件
  17. Ubuntu 18.04 LTS 安装JDK1.8-Linux-64
  18. Android HDMI audio设备插拔事件
  19. Albedo Color and Transparency 反照率颜色与透明度 Standard Shader系列6
  20. 什么是嵌入式编程?如何入门和提高?

热门文章

  1. [摘录]第3章 终局谈判策略
  2. Allure报告的安装及环境变量的配置和在pytest中调用
  3. 微信第三方平台 错误码
  4. PHP 万能查询代码
  5. 读书百客:《嘲鲁儒》赏析
  6. 愿世界没有技术面全是Hr面
  7. 贸易融资名词解析:出口押汇与进口押汇
  8. 爬取中国所有银行官网网址信息
  9. c语言和vb哪个好,请问计算机二级考试我是考c语言好还是考VB好
  10. MQTT与paho.mqtt