1 预备知识
为了增强程序的可移植性,有了size_t ,不同系统上,定义size_t不一样。
经测试发现,32位系统中size_t是4字节,64位系统中,size_t是8字节,这样利用该类型可以增加程序移植性。
size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。
2:指针的大小
- cpu位数(32位数4字节,64位数8字节)
- 操作系统位数(32位数4字节,64位数8字节)
- 编译器的位数(32位数4字节,64位数8字节)
当上述3种位数不同,取最小的位数。比如,如果CPU、系统都是64位的,但编译器是32位的,那么很显然指针只能是32位4字节大小。
众所周知,C语言中的指针描述的是内存中的地址。而内存地址这种东西则是由CPU进行编址的。对于一个4位的CPU来讲,它能同时输出的数据为4位,即0000-1111共24 种情况,故这些二进制数字只能对应到16个位置的内存地址,即CPU仅能识别出16个内存地址。即便你的内存再大,它也显示只有16个位置的内存可用。这种原理同样应用于32位和64位的CPU。
综上,因为指针存放的是地址,所以32位内存,共4个字节;64位系统的64位地址共8个字节——你应该明白什么了吧!没错,32位指针4字节,64位指针8字节。
当然,CPU只是影响指针大小的首要因素,除了它之外还要看操作系统和编译器的位数。这里指针的大小由这三个东西中位数最小的那项决定。比如,如果CPU、系统都是64位的,但编译器是32位的,那么很显然指针只能是32位4字节大小。
2 malloc中主要细节
注意以下具体举例计算以64位计算机为准。
1:用mallo分配内存的最小位数是MINSIZE
在探究操作系统的内存分配(malloc)对齐策略中有malloc.c文件的源码,摘录MINSIZE的定义,并解析如下:
#define MINSIZE /
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))其中MINSIZE的右边定义式,由上下文的宏得到等价式:
(unsigned long) (sizeof(struct malloc_chunk) + (2 * (sizeof(size_t)) - 1) ) & ~ (2 * (sizeof(size_t)) - 1)
即 sizeof(struct malloc_chunk) 以 (2 * (sizeof(size_t)) - 1)的大小向上内存对齐
在64位计算机中,MINSIZE最小值的大小就是sizeof(struct malloc_chunk),即两个size_t和两个指针的大小,共计4*8=32
综上,malloc_chunk 作为分配内存的数据结构管理部分,是每一块分配内存的一部分,也是用mallo分配内存的最小位数,在32位机器中是16(4*size_t),在64位机器中是32(4*size_t)。
2:若所需内存大小超过MINSIZE,内存对齐以2 * (sizeof(size_t))为基准
如上1中所述,在64位计算机中,2 * (sizeof(size_t)) = 2*8 = 16。这也就解释了关于malloc实际分配内存的探讨中16的由来。
3:分配内存中的头部和头部大小问题
一开始是因为看了上述2中链接中的文章,文中称之为mem_control_block。后来读了Redis底层详解(三) 内存管理豁然开朗。所谓的头部还有PREFIX_SIZE的概念。
存疑:malloc_chunk和mem_control_block,及PREFIX_SIZE的关系!可参考探索malloc和free
在redis底层讲解中解释了HAVE_MALLOC_SIZE和PREFIX_SIZE的概念。HAVE_MALLOC_SIZE是在1.6版本才引进的概念;PREFIX_SIZE的大小分平台,有的是sizeof(long long),有的是sizeof(size_t);HAVE_MALLOC_SIZE和PREFIX_SIZE互斥存在,因为有HAVE_MALLOC_SIZE的地方将PREFIX_SIZE的大小定义为0。
如果 HAVE_MALLOC_SIZE 未定义,那么就代表在申请内存空间的时候,需要额外申请一块空间来记录这个需要申请的空间的实际字节数(方便申请和释放的时候做内存统计),这个 “额外空间” 被放在申请空间的前面,它的字节数被定义为 PREFIX_SIZE。
有HAVE_MALLOC_SIZE时,内存分配成功之后,返回的是内存的首地址ptr。没有HAVE_MALLOC_SIZE有PREFIX_SIZE时,是返回(char*)ptr+PREFIX_SIZE,相应地释放内存也需要将指针挪到正在的初始指针再释放。
综上, HAVE_MALLOC_SIZE 未定义时,用分配内存的头部PREFIX_SIZE记录该内存的大小,并在内存首地址+PREFIX_SIZE之后真正存有效数据,若踩内存踩到PREFIX_SIZE中的数据,会引起释放的错误。若HAVE_MALLOC_SIZE 定义了,会在分配成功后,通过头指针就可以获取它申请的内存块的大小(这句话待验证)。
3 系统调用:brk和mmap
malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)。 brk分配的内存需要等到高地址内存释放以后才能释放。
malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)。mmap分配的内存可以单独释放。申请释放的开销大。内存都是按页组织,申请较小的内存会产生内存浪费。申请太大的内存,使用过程中会产生很多缺页中断。频繁使用会产生更多的内存碎片,最后可能导致没有足够的连续页面来映射。
详细内容可参考进程分配内存的两种方式--brk() 和mmap()(不涉及共享内存)
4 mmap的文件映射
4.1 系统调用read write/mmap/共享内存
读写文件的方式,除了read()和write(),还有mmap(),即内存映射。带上共享内存,看看之间的区别。
- read和write:磁盘上的数据先拷贝到内核,再由内核拷贝到用户进程。有内核切换用户态的开销,和两次拷贝。
- 共享内存:同一块物理内存映射到多个进程的地址空间,实现进程间通信,速度最快。内存上的数据重启会丢失。这些进程的数据传递不再涉及内核,因为它会以指针的方式读写内存,不涉及系统级调用。
- mmap:基于文件的映射,把文件映射到用户进程的地址空间,直到第一次访问数据,才真正分配物理内存,将这部分数据拷贝到内存中,同时触发page fault,可能会产出majorflt。
4.2 mmap文件映射的几点细节
在mmap的百度百科上,一些基础的定义和注意事项已诠释的很清楚,现就一些细节做以强调。
mmap的四种类型
mmap有共享/私有和文件/匿名的类别。目录和管道不能被映射。
- 文件共享映射:多个进程映射同一个文件,一个进程对文件内容的修改,对其他进程可见,需要程序员实现同步安全。对文件内容的修改会被写回到原文件。
可以用作内存映射IO来对大文件进行操作,比普通IO减少一次复制。
- 文件私有映射:多个进程映射同一个文件,当一个进程对文件内容做修改时,采用写时拷贝,修改不会被其他进程看到也不会被写回到原文件。当内存不够需要进行页回收时,私有映射的页被交换到交换区。
可以用作共享库二进制文件代码段,数据段的加载,一般用在加载共享代码库。
- 匿名共享映射:内核创建一个初始都是0的物理内存区域,多个进程映射这个共享的物理内存区域,对该区域内容的修改对所有进程可见。匿名文件在页回收时被交换到交换区。
可以用作fork时让父子进程共享匿名映射分配的内存。
- 匿名私有映射:内核创建一个初始都是0的物理内存区域,对该区域内容的修改只对创建者进程可见。匿名文件在页回收时被交换到交换区。
malloc()底层是用了匿名文件的私有映射来分配大块内存(brk用于分配小块内存)。
匿名共享映射
从原型可知,存在一个参数为fd,根据fd,存在一种情况叫匿名映射,所谓匿名映射,表示不存在fd这么个真实的文件。实现匿名映射的方式主要有以下两种:
1、BSD 提供匿名映射的办法是fd =-1,同时 flag 指定为MAP_SHARE|MAP_ANON。
ptr = mmap(NULL,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);2、SVR4 提供匿名映射的办法是 open /dev/zero设备文件,把返回的文件描述符,作为mmap的fd参数。
fd = open("/dev/zero",O_RDWR);
/dev/zero 是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。一个作用是用它作为源,产生一个特定大小的空白文件。
匿名内存映射适用于具有亲属关系的进程之间;由于父子进程之间的这种特殊的父子关系,在父进程中先调用mmap(),然后调用fork(),那么,在调用fork() 之后,子进程继承了父进程的所有资源,当然也包括匿名映射后的地址空间和mmap()返回的地址,这样父子进程就可以通过映射区域进行通信了;
这里不是一般的继承关系,一般来说,子进程单独维护从父进程继承下来的一些变量,而mmap()返回的地址却是由父子进程共同维护的;对于具有亲属关系的进程之间实现共享内存的最好方式应该是采用匿名映射的方式。此时,不必指定具体的条件,只要设置相应的标志即可。
Posix共享内存
共享内存区中最好不要有指针。因为同一个共享内存对象可能会映射到各个调用进程的不同逻辑地址,指针是指向地址的。
Posix提供了两种在无亲缘关系的进程之间通信。内存映射文件(有亲缘无亲缘关系均可)和共享内存区对象(使用Posix提供的一系列API)。
- 内存映射文件
(1)用于父子进程之间通信共享内存区
(2)用于无亲缘关系的进程之间通信共享内存区
- 共享内存区对象
需要使用Posix提供的API,有两个步骤要求:
(1)指定一个名字参数调用shm_open,创建一个新的共享内存共享区对象或者打开一个已经存在的内存共享区对象。
(2)调用mmap函数把这个共享内存区映射到调用进程的地址空间。
在Linux中,POSIX共享内存通过挂载在/dev/shm下的tmpfs内存文件系统实现,创建的每一个共享内存都对应tmpfs中的一个文件,因此POSIX共享内存也可视为共享文件映射。用mmap实现共享内存的更多细节可见Linux进程间通信--mmap共享内存。
文件大小和映射区大小
首先明白四个概念:逻辑地址---页表---物理内存---磁盘。拿到一个逻辑地址,根据虚拟地址和页表的映射关系,找到对应的页表项PTE,如果PTE没有分配,就报一个page fault,去加载相应的文件数据到物理内存。
进程通过指针操作内存中的数据,内存中的数据什么时候写进磁盘看munmap或msync等,能写多少到磁盘首先限于磁盘文件真实大小,再止于内存中实际数据的多少。因为内核的内存保护以页面为单位。
可以总结如下:
(1)没超过物理页面,没超过映射区大小 —> 正常读写
(2)没超过物理页面,超过映射区大小 —> 内核允许读写但不执行写入操作
(3)超过物理页面,没有超过映射区大小 —> 引发SIGBUS信号
(4)超过物理页面, 超过映射区大小 —> 引发SIGSEGV信号
相关问题
GLIBC内存分配机制引发的“内存泄露”及malloc的brk和mmap。
附:
- (1)用于SYSV共享内存,还有匿名内存映射;这部分由内核管理,用户不可见;
- (2)用于POSIX共享内存,由用户负责mount,而且一般mount到/dev/shm;依赖于CONFIG_TMPFS;
- (3)POSIX共享内存与SYS V共享内存在内核都是通过tmpfs实现,但对应两个不同的tmpfs实例,相互独立。
- (4)通过/proc/sys/kernel/shmmax可以限制SYS V共享内存(单个)的最大值,通过/dev/shm可以限制POSIX共享内存的最大值(所有之和)。