[php]php内存管理

本文深入探讨了PHP的内存管理,从操作系统层面的分段和分页管理开始,讲解了Linux虚拟内存的工作原理。接着,文章转向内存分配器的设计思路,并介绍了内存池的优势。最后,重点阐述了PHP内存管理器的数据模型、small、large和huge内存分配方案,以及内存释放等关键概念,总结了PHP内存管理的特点和策略。
摘要由CSDN通过智能技术生成

目录

第一章 从操作系统内存管理说起

1. 分段管理

2.分页管理

3.linux虚拟内存

第二章 说说内存分配器

1.内存分配器设计思路

第三章 内存池

第四章 切入主题——PHP内存管理

1.PHP内存管理器数据模型

2.PHP small内存分配方案

3.large内存分配:

4.huge内存分配:

5.内存释放

6.zend_mm_heap和zend_mm_chunk

7. PHP内存管理器初始化流程:

8. PHP内存管理总结:


第一章 从操作系统内存管理说起

程序是代码和数据的集合,进程是运行着的程序;操作系统需要为进程分配内存;进程运行完毕需要释放内存;内存管理就是内存的分配和释放;

1. 分段管理

分段最早出现在8086系统中,当时只有16位地址总线,其能访问的最大地址是64k;当时的内存大小为1M;如何利用16位地址访问1M的内存空间呢?

于是提出了分段式内存管理;
将内存地址分为段地址与段偏移,段地址会存储在寄存器中,段偏移即程序实际使用的地址;当CPU需要访问内存时,会将段地址左移4位,再加上段偏移,即可得到物理内存地址;
即内存地址=段地址*16+段偏移地址。

后来的IA-32在内存中使用一张段表来记录各个段映射的物理内存地址,CPU只需要为这个段表提供一个记录其首地址的寄存器就可以了;如下图所示:

进程包含多个段:代码段,数据段,链接库等;系统需要为每个段分配内存;
一种很自然地想法是,根据每个段实际需要的大小进行分配,并记录已经占用的空间和剩余空间:
当一个段请求内存时,如果有内存中有很多大小不一的空闲位置,那么选择哪个最合理?

a)首先适配:空闲链表中选择第一个位置(优点:查表速度快) 
b)最差适配:选择一个最大的空闲区域 
c)最佳适配:选择一个空闲位置大小和申请内存大小最接近的位置,比如申请一个40k内存,而恰巧内存中有一个50k的空闲位置;

内存分段管理具有以下优点:

a)内存共享: 对内存分段,可以很容易把其中的代码段或数据段共享给其他程序;
b)安全性: 将内存分为不同的段之后,因为不同段的内容类型不同,所以他们能进行的操作也不同,比如代码段的内容被加载后就不应该允许写的操作,因为这样会改变程序的行为
c)动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。

尽管分段管理的方式解决了内存的分配与释放,但是会带来大量的内存碎片;即尽管我们内存中仍然存在很大空间,但全部都是一些零散的空间,当申请大块内存时会出现申请失败;为了不使这些零散的空间浪费,操作系统会做内存紧缩,即将内存中的段移动到另一位置。但明显移动进程是一个低效的操作。

2.分页管理

先说说虚拟内存的概念。CPU访问物理内存的速度要比磁盘快的多,物理内存可以认为是磁盘的缓存,但物理内存是有限的,于是人们想到利用磁盘空间虚拟出的一块逻辑内存
(这部分磁盘空间Windows下称之为虚拟内存,Linux下被称为交换空间(Swap Space));

虚拟内存和真实的物理内存存在着映射关系;

为了解决分段管理带来的碎片问题,操作系统将虚拟内存分割为虚拟页,相应的物理内存被分割为物理页;而虚拟页和物理页的大小默认都是4K字节;

操作系统以页为单位分配内存:假设需要3k字节的内存,操作系统会直接分配一个4K页给进程
,这就产生了内部碎片(浪费率优于分段管理)

前面说过,物理内存可以认为是磁盘的缓存;虚拟页首先需要分配给进程并创建与物理页的映射关系,然后才能将磁盘数据载入内存供CPU使用;由此可见,虚拟内存系统必须能够记录一个虚拟页是否已经分配给进程;是否已经将磁盘数据载入内存,对应哪个物理页;假如没有载入内存,这个虚拟页存放在磁盘的哪个位置;
于是虚拟页可以分为三种类型:已分配,未缓存,已缓存;

当访问没有缓存的虚拟页时,系统会在物理内存中选择一个牺牲页,并将虚拟页从磁盘赋值到物理内存,替换这个牺牲页;而如果这个牺牲页已经被修改,则还需要写回磁盘;这个过程就是所谓的缺页中断;

虚拟页的集合就称为页表(pageTable),页表就是一个页表条目(page table entry)的数组;每个页表条目都包含有效位标志,记录当前虚拟页是否分配,当前虚拟页的访问控制权限;同时包含物理页号或磁盘地址;

进程所看到的地址都是虚拟地址;在访问虚拟地址时,操作系统需要将虚拟地址转化为实际的物理地址;而虚拟地址到物理地址的映射是存储在页表的;

将虚拟地址分为两部分:虚拟页号,记录虚拟页在页表中的偏移量(相当于数组索引);页内偏移量;而页表的首地址是存储在寄存器中;

 

对于32位系统,内存为4G,页大小为4K,假设每个页表项4字节;则页表包含1M个页表项,占用4M的存储空间,页表本身就需要分配1K个物理页;
页表条目太大时,页表本身需要占用更多的物理内存,而且其内存还必须是连续的;

目前有三种优化技术:

1)多级页表
一级页表中的每个PTE负责映射虚拟地址空间中一个4M的片(chunk),每一个片由1024个连续的页面组成;二级页表的每个PTE都映射一个4K的虚拟内存页面;

优点:节约内存(假如一级页表中的PTE为null,则其指向的二级页表就不存在了,而大多数进程4G的虚拟地址空间大部分都是未分配的;只有一级页表才总是需要在主存中,系统可以在需要的时候创建、调入、调出二级页表)
缺点:虚拟地址到物理地址的翻译更复杂了

2)TLB
多级页表可以节约内存,但是对于一次地址翻译,增加了内存访问次数,k级页表,需要访问k次内存才能完成地址的翻译;

由此出现了TLB:他是一个更小,访问速度更快的虚拟地址的缓存;当需要翻译虚拟地址时,先在TLB查找,命中的话就可以直接完成地址的翻译;没命中再页表中查找;

3)hugePage

因为内存大小是固定的,为了减少映射表的条目,可采取的办法只有增加页的尺寸。hugePage便因此而来,使用大页面2m,4m,16m等等。如此一来映射条目则明显减少。

3.linux虚拟内存

linux为每个进程维护一个单独的虚拟地址空间,进程都以为自己独占了整个内存空间,如图所示:


linux将内存组织为一些区域(段)的集合,如代码段,数据段,堆,共享库段,以及用户栈都是不同的区域。每个存在的虚拟页面都保存在某个区域中,不属于任何一个区域的虚拟页是不存在的,不能被进程使用;

内核为系统中的每个进程维护一个单独的任务结构task_struct,任务中的一个字段指向mm_struct,他描述了虚拟内存的当前状态。其中包含两个字段:pgd指向第一级页表的基址(当内核运行这个进程时,就将pgd的内容存储在cr3控制寄存器中);mmap指向一个vm_area_struct区域结构的链表;区域结构主要包括以下字段: 
vm_start:区域的起始地址; 
vm_end:区域的结束地址;
vm_port:指向这个区域所包含页的读写许可权限;
vm_flags:描述这个区域是与其他进程共享的,还是私有的等信息;

当我们访问虚拟地址时,内核会遍历vm_area_struct链表,根据vm_start和vm_end能够判断地址合法性;根据vm_por能够判断地址访问的合法性;
遍历链表时间性能较差,内核会将vm_area_struct区域组织成一棵树;

说到这里就不得不提一下系统调用mmap,其函数声明为

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

函数mmap要求内核创建一个新的虚拟内存区域(注意是新的区域,和堆是平级关系,即mmap函数并不是在堆上分配内存的,);最好是从地址addr开始(一般传null),并将文件描述fd符指定的对象的一个连续的chunk(大小为len,从文件偏移offset开始)映射到这个新的区域;当fd传-1时,可用于申请分配内存;

参数port描述这个区域的访问控制权限,可以取以下值:

PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问

参数flags由描述被映射对象类型的位组成,如MAP_SHARED 表示与其它所有映射这个对象的进程共享映射空间;MAP_PRIVATE 表示建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件。

php在分配2M以上大内存时,就是直接使用mmap申请的;

第二章 说说内存分配器

malloc是c库函数,用于在堆上分配内存;操作系统给进程分配的堆空间是若干个页,我们再调用malloc向进程请求分配若干字节大小的内存;
malloc就是一种内存分配器,负责堆内存的分配与回收;

同样我们可以使用mmap和munmap来创建和删除虚拟内存区域,以达到内存的申请与释放;

观察第一章第三小节中的虚拟地址空间描述图,每个进程都有一个称为运行时堆的虚拟内存区域,操作系统内核维护着一个变量brk,指向了堆的顶部;并提供系统调用brk(void* addr)和sbrk(incr)来修改变量brk的值,从而实现堆内存的扩张与收缩;

brk函数将brk指针直接设置为某个地址,而sbrk函数将brk从当前位置移动incr所指定的增量;(如果将incr设置为0,则可以获得当前brk指向的地址)

因此我们也可以使用brk()或sbrk()来动态分配/释放内存块;

需要注意的一点是:系统为每一个进程所分配的资源不是无限的,包括可映射的内存空间,即堆内存并不是无限大的;所以当调用malloc将堆内存都分配完时,malloc会使用mmap函数额外再申请一个虚拟内存区域(由此发现,使用malloc申请的内存也并不一定是在堆上)

1.内存分配器设计思路

内存分配器用于处理堆上的内存分配或释放请求;

要实现分配器必须考虑以下几个问题:

1.空闲块组织:如何记录空闲块;如何标记内存块是否空闲;
2.分配:如何选择一个合适的空闲块来处理分配请求;
3.分割:空闲块一般情况会大于实际的分配请求,我们如何处理这个空闲块中的剩余部分;
4.回收:如何处理一个刚刚被释放的块;

思考1:空闲块组织
内存分配与释放请求时完全随机的,最终会造成堆内存被分割为若干个内存小块,其中有些处于已分配状态,有些处于空闲状态;我们需要额外的空间来标记内存状态以及内存块大小;
下图为malloc设计思路:

注:图中显示额外使用4字节记录当前内存块属性,其中3比特记录是否空闲,29比特记录内存块大小;实际malloc头部格式可能会根据版本等调整;不论我们使用malloc分配多少字节的内存,实际malloc分配的内存都会多几个字节;
注:空闲内存块可能会被组织为一个链表结构,由此可以遍历所有空闲内存块,直到查找到一个满足条件的为止;

思考2:如何选择合适的空闲块
在处理内存分配请求时,需要查找空闲内存链表,找到一个满足申请条件的空闲内存块,选择什么查找算法;而且很有可能存在多个符合

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值