PHP源码阅读笔记三十:PHP内存池中的存储层

PHP源码阅读笔记三十:PHP内存池中的存储层

【概述】
PHP的内存管理器是分层(hierarchical)的。这个管理器共有三层:存储层(storage)、堆(heap)层和 emalloc/efree 层。存储层通过 malloc()、mmap() 等函数向系统真正的申请内存,并通过 free() 函数释放所申请的内存。存储层通常申请的内存块都比较大,这里申请的内存大并不是指storage层结构所需要的内存大,只是堆层通过调用存储层的分配方法时,其以段的格式申请的内存比较大,存储层的作用是将内存分配的方式对堆层透明化。

首先看storage层的结构:
【结构】

210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/* Heaps with user defined storage */
typedef struct _zend_mm_storage zend_mm_storage;
 
typedef struct _zend_mm_segment {
    size_t    size;
    struct _zend_mm_segment *next_segment;
} zend_mm_segment;
 
typedef struct _zend_mm_mem_handlers {
    const char *name;
    zend_mm_storage* (*init)(void *params);    //    初始化函数
    void (*dtor)(zend_mm_storage *storage);    //    析构函数
    void (*compact)(zend_mm_storage *storage);
    zend_mm_segment* (*_alloc)(zend_mm_storage *storage, size_t size);    //    内存分配函数
    zend_mm_segment* (*_realloc)(zend_mm_storage *storage, zend_mm_segment *ptr, size_t size);    //    重新分配内存函数
    void (*_free)(zend_mm_storage *storage, zend_mm_segment *ptr);    //    释放内存函数
} zend_mm_mem_handlers;
 
struct _zend_mm_storage {
    const zend_mm_mem_handlers *handlers;    //    处理函数集
    void *data;
};

内存的分配方式,调用的函数是_zend_mm_storage结构中的处理函数集,而内存是以段的形式表现的。
【4种内存方案】
PHP在存储层共有4种内存分配方案: malloc,win32,mmap_anon,mmap_zero默认使用malloc分配内存,如果设置了ZEND_WIN32宏,则为windows版本,调用HeapAlloc分配内存,剩下两种内存方案为匿名内存映射,并且PHP的内存方案可以通过设置变量来修改。
官方说明如下:
The Zend MM can be tweaked using ZEND_MM_MEM_TYPE and ZEND_MM_SEG_SIZE environment
variables. Default values are “malloc” and “256K”. Dependent on target system you
can also use “mmap_anon”, “mmap_zero” and “win32″ storage managers.

在代码中,对于这4种内存分配方案,分别对应实现了zend_mm_mem_handlers中的各个处理函数。配合代码的简单说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* 使用mmap内存映射函数分配内存 写入时拷贝的私有映射,并且匿名映射,映射区不与任何文件关联。*/
# define ZEND_MM_MEM_MMAP_ANON_DSC {"mmap_anon", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_anon_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free}

/* 使用mmap内存映射函数分配内存 写入时拷贝的私有映射,并且映射到/dev/zero。*/
# define ZEND_MM_MEM_MMAP_ZERO_DSC {"mmap_zero", zend_mm_mem_mmap_zero_init, zend_mm_mem_mmap_zero_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_zero_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free}

/* 使用HeapAlloc分配内存 windows版本 关于这点,注释中写的是VirtualAlloc() to allocate memory,实际在程序中使用的是HeapAlloc*/
# define ZEND_MM_MEM_WIN32_DSC {"win32", zend_mm_mem_win32_init, zend_mm_mem_win32_dtor, zend_mm_mem_win32_compact, zend_mm_mem_win32_alloc, zend_mm_mem_win32_realloc, zend_mm_mem_win32_free}

/* 使用malloc分配内存 默认为此种分配 如果有加ZEND_WIN32宏,则使用win32的分配方案*/
# define ZEND_MM_MEM_MALLOC_DSC {"malloc", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_malloc_alloc, zend_mm_mem_malloc_realloc, zend_mm_mem_malloc_free}

static const zend_mm_mem_handlers mem_handlers[] = {
#ifdef HAVE_MEM_WIN32
    ZEND_MM_MEM_WIN32_DSC,
#endif
#ifdef HAVE_MEM_MALLOC
    ZEND_MM_MEM_MALLOC_DSC,
#endif
#ifdef HAVE_MEM_MMAP_ANON
    ZEND_MM_MEM_MMAP_ANON_DSC,
#endif
#ifdef HAVE_MEM_MMAP_ZERO
    ZEND_MM_MEM_MMAP_ZERO_DSC,
#endif
    {NULL, NULL, NULL, NULL, NULL, NULL}
};

【关于匿名内存映射的优点】
mmem_zero方案:
(SVR 4 ) /dev/zero Memory Mapping
1. 可以将伪设备 “/dev/zero” 作为参数传递给 mmap 而创建一个映射区。/dev/zero 的特殊在于,对于该设备文件所有的读操作都返回值为 0 的指定长度的字节流 ,任何写入的内容都被丢弃。我们的兴趣在于用它来创建映射区,用 /dev/zero 创建的映射区,其内容被初始为 0 。
2. 使用 /dev/zero 的优点在于,mmap创建映射区时,不需要一个时间存在的文件,伪文件 /dev/zero 就足够了。缺点是只能用在相关进程间。相对于相关进程间的通信,使用线程间通信效率要更高一些。不管使用那种技术,对共享数据的访问都需要进行同步。

mmem_anon方案:
(4.4 BSD) Anonymous Memory Mapping
1. 匿名内存映射 与 使用 /dev/zero 类型,都不需要真实的文件。要使用匿名映射之需要向 mmap 传入 MAP_ANON 标志,并且 fd 参数 置为 -1 。
2. 所谓匿名,指的是映射区并没有通过 fd 与 文件路径名相关联。匿名内存映射用在有血缘关系的进程间。

【win32方案中堆内存分配的声明】
windows API
函数HeapAlloc声明如下:

3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
WINBASEAPI
__out_opt
HANDLE
WINAPI
HeapCreate(
	__in DWORD flOptions,
	__in SIZE_T dwInitialSize,
	__in SIZE_T dwMaximumSize
	);
 
WINBASEAPI
BOOL
WINAPI
HeapDestroy(
	__in HANDLE hHeap
	);
 
WINBASEAPI
__bcount(dwBytes)
LPVOID
WINAPI
HeapAlloc(
	__in HANDLE hHeap,
	__in DWORD dwFlags,
	__in SIZE_T dwBytes
	);
 
 
WINBASEAPI
BOOL
WINAPI
HeapFree(
	__inout HANDLE hHeap,
	__in	DWORD dwFlags,
	__deref LPVOID lpMem
	);
 
WINBASEAPI
SIZE_T
WINAPI
HeapSize(
	__in HANDLE hHeap,
	__in DWORD dwFlags,
	__in LPCVOID lpMem
	);

hHeap是进程堆内存开始位置。
dwFlags是分配堆内存的标志。
dwBytes是分配堆内存的大小。

【初始化】
在zend_mm_startup启动时,程序会根据配置设置内存分配方案和段分配大小,如下所示代码:

1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
ZEND_API zend_mm_heap *zend_mm_startup(void)
{
    int i;
    size_t seg_size;
    char *mem_type = getenv("ZEND_MM_MEM_TYPE");
    char *tmp;
    const zend_mm_mem_handlers *handlers;
    zend_mm_heap *heap;
 
    if (mem_type == NULL) {
   	 i = 0;
    } else {
   	 for (i = 0; mem_handlers[i].name; i++) {
   		 if (strcmp(mem_handlers[i].name, mem_type) == 0) {
   			 break;
   		 }
   	 }
   	 if (!mem_handlers[i].name) {
   		 fprintf(stderr, "Wrong or unsupported zend_mm storage type '%s'\n", mem_type);
   		 fprintf(stderr, "  supported types:\n");
   		 for (i = 0; mem_handlers[i].name; i++) {
   			 fprintf(stderr, "	'%s'\n", mem_handlers[i].name);
   		 }
   		 exit(255);
   	 }
    }
    handlers = &mem_handlers[i];
 
    tmp = getenv("ZEND_MM_SEG_SIZE");
    if (tmp) {
   	 seg_size = zend_atoi(tmp, 0);
   	 if (zend_mm_low_bit(seg_size) != zend_mm_high_bit(seg_size)) {
   		 fprintf(stderr, "ZEND_MM_SEG_SIZE must be a power of two\n");
   		 exit(255);
   	 } else if (seg_size < ZEND_MM_ALIGNED_SEGMENT_SIZE + ZEND_MM_ALIGNED_HEADER_SIZE) {
   		 fprintf(stderr, "ZEND_MM_SEG_SIZE is too small\n");
   		 exit(255);
   	 }
    } else {
   	 seg_size = ZEND_MM_SEG_SIZE;
    }
 
    //....代码省略
}

第1121~1138行 遍历整个mem_handlers数组,确认内存分配方案,如果没有设置ZEND_MM_MEM_TYPE变量,默认使用malloc方案,如果是windows(即ZEND_WIN32),则默认使用win32方案,如果设置了ZEND_MM_MEM_TYPE变量,则采用设置的方案。

第1140~1152行 确认段分配大小,如果设置了ZEND_MM_SEG_SIZE变量,则使用设置的大小,此处会判断所设置的大小是否满足2的倍数,并且大于或等于ZEND_MM_ALIGNED_SEGMENT_SIZE + ZEND_MM_ALIGNED_HEADER_SIZE;如果没有设置没使用默认的ZEND_MM_SEG_SIZE

【参考资料】

http://blog.chinaunix.net/u1/38994/showart_2061729.html

【附录】
功能描述:
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。
基于文件的映射,在mmap和munmap执行过程的任何时刻,被映射文件的st_atime可能被更新。如果st_atime字段在前述的情况下没有得到更新,首次对映射区的第一个页索引时会更新该字段的值。用PROT_WRITE 和 MAP_SHARED标志建立起来的文件映射,其st_ctime 和 st_mtime
在对映射区写入之后,但在msync()通过MS_SYNC 和 MS_ASYNC两个标志调用之前会被更新。

用法:
#include
void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *start, size_t length);

参数:
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
offset:被映射对象内容的起点。

返回说明:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区

PHP源码阅读笔记三十:PHP内存池中的存储层》上有7条评论

  1. Pingback引用通告: PHP源码阅读笔记三十一:PHP内存池中的堆(heap)层基础 | PHP源码阅读,PHP设计模式,PHP学习笔记-胖子的空间

  2. Pingback引用通告: Thinking In LAMP Blog » Blog Archive » PHP每月通讯(2010年12月)

  3. laruence

    不能说是段的结构, 否则容易混淆。 只不过就是那个变量名叫做segment而已, 本质就是一次申请大块内存,创建自己的内存管理结构,自己管理, 减少系统调用。

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注


*

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>