深入理解Linux内存管理机制(一)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 深入理解Linux内存管理机制(一)通过本文,您即可以: 1. 存储器硬件结构; 2.分段以及对应的组织方式; 3.分页以及对应的组织方式。 注1:本文以Linux内核2.6.32.59本版为例,其对应的代码可以在http://www.kernel.org/pub/linux/kernel/

深入理解Linux内存管理机制(一)通过本文,您即可以:

1. 存储器硬件结构

2.分段以及对应的组织方式

3.分页以及对应的组织方式

注1:本文以Linux内核2.6.32.59本版为例,其对应的代码可以在http://www.kernel.org/pub/linux/kernel/v2.6/longterm/v2.6.32/linux-2.6.32.59.tar.bz2找到。

注2:本文所有的英文专有名词都是我随便翻译的,请对照英文原文进行理解。

注3:推荐使用Source Insight进行源码分析。

内存组织

计算机内存属于随机存储器(RAM),目前PC机广泛使用的是DDR

SDRAM,即“双倍速率同步动态随机存储器”,其本质上仍然是由n bits*m KB个内存芯片组成的,比如如果我们需要8位64KB的内存,则我们就需要2*8=16块4bits*8KB的内存块。由于计算机通常是以字节(Byte)进行数据交换的,所以对内存的地址编码一般使用字节,如上我们有64KB内存,则其地址编码为0x0000~0xFFFF,称为物理地址。对于32位机来说,由于其“地址寄存器(AR)”是32位,也就限制了其内存的最大寻址范围是2^32=4GB。

Linux将物理地址按4KB的大小划分成“帧(Frame)”。为什么是4KB?因为每一个帧都需要用一个C结构体来描述,称之为“帧描述单元(Frame Discriptor)”,如果太小,帧描述单元显然太多了,如果太大,那么在内存分配时又会造成“内碎片(Inner

Fragments)”。早些时候,计算机的内存址都是直接映射的,由于程序里的地址是写死的,这就意味着每段程序每次都只能映射对应的地址空间。这无论对程序设计者与系统都是相当大的负担。Linux使用“分段”加“分页”来解决此问题。由于它们的存在,内存地址进入了逻辑地址时代。Linux有三种地址:逻辑地址(Logic

Address)、线性地址(Linear Address)与物理地址(Physics Address)。

另外,Linux支持众多CPU架构,这里只研究X86的,对应的源代码为:.../X86/... 路径。

Linux中的分段

Linux并不使用太多的分段,原因是某些RISC机器对分段的支持不好。为此Linux的分段都存在“全局描述表(GDT)”中,GDT是一个全局desc_struct数组(位于linux-2.6.32.59archx86includeasm),其结构如下:

  1. #define GDT_ENTRIES 16  
  2.   
  3. struct desc_struct gdt[GDT_ENTRIES];  
  4.   
  5. struct desc_struct {  
  6.     union {  
  7.         struct {  
  8.             unsigned int a;  
  9.             unsigned int b;  
  10.         };  
  11.         struct {  
  12.             u16 limit0; // 段大小  
  13.             u16 base0; // 段起始位置  
  14.             unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; // type表示段类型,占4位;dpl指的段运行权限,占2位  
  15.             unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; //d 表示内存地址位宽,占1位  
  16.         };  
  17.     };  
  18. } __attribute__((packed));  

所以我们可以看出,段描述结构体占8个字节,至于里面的a,b,那是老的方式,后来使用C++ Struts的Bit Fields后更方便了。type类型由以下几种:

  1. enum {  
  2.     DESC_TSS = 0x9,  
  3.     DESC_LDT = 0x2,  
  4.     DESCTYPE_S = 0x10,  /* !system */  
  5. };  

Linux主要使用以下几种段:

  • 内核代码段(Kernel Code Segment):type=10,dpl=0
  • 内核数据段(Kernel Data Segment):type=2,dpl=0
  • 用户代码段(User Code Segment):type=10,dpl=3
  • 用户数据段(User Data Segment):type=2,dpl=3
  • 任务状态段(Task State Segment),每进程一个:type=9,dpl=3

其它类型可以参见linux-2.6.32.59archx86includeasmsegment.h,里面有非常详细的说明。

它们都存储在“全局描述符表(GDT)”。Linux本身并不使用“局部描述符表(LDT)”,当一个进程被创建时,其指向的是一个默认的LDT,不过系统并不阻止进程创建它。也就是说一个进程最多两个段描述符:TSS与LDT。由于Segment Selector为16位(为什么只有16位,这个就是历史原因了,由于X86在Real Mode下段地址只有20位,其中有效的就是16位,详见:x86

memory segmentation,但Linux段内偏移地址高达32位,所以线性地址总共是48位),其中有效的索引位仅有13位,所以GDT的最大长度为213-1=8192,除去系统保留的12个,留给进程的只有8180个入口,那么就意味Linux进程的最大数为8180/2=4090。需要注意的是,进程在创建的时候并不会马上创建自己的LDT,其指向的是GDT一个默认的LDT,里面的SD为null。只有在需要的时候进程才创建自己的LDT并把它放入GDT中。所以不管是LDT也好,TSS也好,它们都存放在GDT里面。而对于UCS与UDS,所有的进程共享一个。这样地址空间不会重复吗?不会,因为线性不是最终的物理地址,每个进程还有自己的页表,所以最终映射到物理地址是不同的。

下面我们来看看段中地址是如何转换的。假设我们需要访问内核数据段的0x00124部分,由代码知其GDT的入口为13,那么其对应的内存地址=gdtr+13*8+0x00124,假设gptr为0x02000,则最终的结果为0x02228。gdtr是一个寄存器,其为48位,用来保存GDT的第一个字节线性地址与表限。


分页

相对于分段来说,分页更主流更流行一些。原因是其更灵活,其能把不同的线性地址映射到同一个物理地址上,缺点是内存必须以页大小的整数倍分配。按现在主流的4KB一页来说,如果程序只申请100B的数据,那内存浪费还是相当的大。为此,Linux使用了一种称为Slab的方法来解决这个问题,后面的文章会讲到。

因为页表本身也需要存储空间,按每页32B来算,对于4GB内存,每页4KB,共有1M页,则页表的大小为32MB,这显然不可以接受,所以后来出现了多级页表这个概念。2004年后Linux版本使用的是四级页表:第一级叫“全局目录(Page

Global Directory)“、第二级叫“页上级目录(Page

Upper Directory)”、第三级叫”页中间目录(Page

Middle Derectory)”、第四级叫”页面表(Page

Table Entry)”,最后页内偏移量“offset”.


cr3是一个寄存器,它存储“Global DIR”的地址。当进程切换发生时,它将被保存在TSS中,前面说过了TSS段表是每个进程一个。分页在Linux内使用的地方很多,特别是进程内的地址转换。分页有硬件支持的,特别是旁路转换缓冲(Translation

Lookaside Buffer)的出现,使用即使使用三级页表的Linux在地转转换中的实际效果也是非常好的。与段表所有的进程都共用一个的是,每个进程都拥有自己的分页。其实也正是因为所有进程都共享一个段表,每个进程才必须有自己的页表,否则相同的linear地址如何映射到不同的物理地址去?下面我们着重来研究一下Linux系统中是如何表示分页中所用到的数据结构的。

每个“帧”在Linux中都是以一个名为page(位于linux-2.6.32.59includelinuxMm_types.h)的结构体来存储的。所有的页被放在一个类型为page名为mem_map的数组中(位于linux-2.6.32.59mmMemory.c)。代码如下(为了显示方便,仅列出部分:

  1. struct page {  
  2. unsigned long flags;          /* 帧的标志位,用枚举pageflags(位于:linux-2.6.32.59includelinuxPage-flags.h)表示,每个值的意义详见注释 */  
  3. ...  
  4.     atomic_t _count;        /* 该帧被引用的数量 */  
  5.     union {  
  6.         atomic_t _mapcount; /* 所有指向该帧的页表数量*/  
  7.         ...  
  8.     };  
  9.     union {  
  10.         struct {  
  11.         unsigned long private;      /*根据此页的使用情况会有不同的意义,详见源码注释*/  
  12.         ...  
  13.         };  
  14. ...  
  15.     };  
  16.       
  17. union {  
  18.         pgoff_t index;      /* 重要:类型即unsinged long, 指向物理帧号 */  
  19. ...  
  20.     };  
  21.   
  22.   
  23.     struct list_head lru;       /* 指向最近被使用的页的双向链表,cache相关*/  
  24. };  

下面我们再来看看PGD页表。每个进程的mm_struct->pgd(位于:linux-2.6.32.59includelinuxMm_types.h)指向自己的PGD:

  1. struct mm_struct {  
  2.         ...  
  3.     pgd_t * pgd;  
  4.     ...       
  5. }  

可以看出pdg实际上是一个pgd_t结构数组,pgd_t在X86系统中就是一个usinged long,其指向的就是下一级页表的地址。就这样找下去,直到找到对应的页为止,再加上页内偏移,就可以进行内存访问了。

例如线性地址为:0x91220B01,如果PGD、PUD、PMD以及PTE均5位。页内偏移12位,即页大小



那么这段内存的解析步骤是:

  1. PGD号为24,查PGD[24]得到PUD入口;
  2. PUD号为4,再查PUD[4];
  3. PMD号为36,再查PMD[36];
  4. PTE号为2,再查PTE[2];
  5. 如果最终帧地址为a:那么最后的物理地址就是a+0x0301

需要补充的是,并不是所有的内存都是使用“分页”,在内核初始化的时候,有100MB内存的样子是使用直接映射的,这是因为总是要先装入分页的初始化代码才能进行页表初始化。

总结:不知不觉也写了不少了。这次我们介绍了操作系统最基本的内存管理概念“分段”与“分页”在Linux中的实现,可以看出其与通过的概念还是很接近的。这正证明了基础知识的重要性。下一次我们将介绍Linux的内存初始化过程,如页表的建立与初始化。

------------------一些资源与参考-------------------

Linux SLUB 分配器详解:http://www.ibm.com/developerworks/cn/linux/l-cn-slub/

Page Frame Management:http://www.makelinux.net/books/ulk3/understandlk-CHP-8-SECT-1

Linux memory management:http://www.cse.psu.edu/~anand/spring01/linux/memory.ppt

linux内存管理浅析http://hi.baidu.com/_kouu/blog/item/f72e707ffa8478310cd7da28.html

Linux内存之页表:http://biancheng.dnbcw.info/linux/335152.html

相关文章
|
3天前
|
存储 Linux Android开发
Volatility3内存取证工具安装及入门在Linux下的安装教程
Volatility 是一个完全开源的工具,用于从内存 (RAM) 样本中提取数字工件。支持Windows,Linux,MaC,Android等多类型操作系统系统的内存取证。针对竞赛这块(CTF、技能大赛等)基本上都是用在Misc方向的取证题上面,很多没有听说过或者不会用这款工具的同学在打比赛的时候就很难受。以前很多赛项都是使用vol2.6都可以完成,但是由于操作系统更新,部分系统2.6已经不支持了,如:Win10 等镜像,而Volatility3是支持这些新版本操作系统的。
|
27天前
|
算法 安全 Linux
探索Linux内核的虚拟内存管理
【5月更文挑战第20天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分之一——虚拟内存管理。通过剖析其关键组件和运作机制,揭示虚拟内存如何提供高效的内存抽象,支持庞大的地址空间,以及实现内存保护和共享。文章将重点讨论分页机制、虚拟内存区域(VMAs)的管理、页面置换算法,并简要分析这些技术是如何支撑起现代操作系统复杂而多变的内存需求的。
|
5天前
|
算法 Linux 测试技术
Linux编程:测试-高效内存复制与随机数生成的性能
该文探讨了软件工程中的性能优化,重点关注内存复制和随机数生成。文章通过测试指出,`g_memmove`在内存复制中表现出显著优势,比简单for循环快约32倍。在随机数生成方面,`GRand`库在1000万次循环中的效率超过传统`rand()`。文中提供了测试代码和Makefile,建议在性能关键场景中使用`memcpy`、`g_memmove`以及高效的随机数生成库。
|
5天前
|
缓存 Linux Shell
Linux 内存管理与 Swap 空间扩展实践
该文介绍了Linux系统中`free`命令的使用,解析了其输出信息,包括物理内存(总内存、已用、空闲、缓存)和交换空间(总大小、使用和空闲)。Linux优先使用物理内存作缓存,当内存紧张时使用Swap空间。文章还提供了扩展Swap空间的步骤,并强调适度Swap使用对性能的影响,建议合理平衡物理内存和Swap的比例。
|
12天前
|
网络协议 Java Linux
Linux常用操作命令、端口、防火墙、磁盘与内存
Linux常用操作命令、端口、防火墙、磁盘与内存
12 0
|
13天前
|
消息中间件 算法 Unix
【Linux】System V 共享内存
【Linux】System V 共享内存
|
18天前
|
缓存 算法 安全
探索Linux内核的虚拟内存管理
【5月更文挑战第29天】 在现代操作系统中,虚拟内存是支持多任务处理和内存保护的关键组件。本文深入分析了Linux操作系统中的虚拟内存管理机制,包括其地址空间布局、分页系统以及内存分配策略。我们将探讨虚拟内存如何允许多个进程独立地访问它们自己的地址空间,同时由操作系统管理物理内存资源。此外,文章还将涉及虚拟内存所带来的性能影响及其优化方法。
|
25天前
|
消息中间件 存储 安全
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)
|
25天前
|
消息中间件 算法 Linux
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(上)
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(上)
|
1月前
|
存储 Linux 程序员
【操作系统原理】—— Linux内存管理
【操作系统原理】—— Linux内存管理
20 0