六、内存映射mmap
mmap内存映射的实现过程,总的来说可以分为三个阶段:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
6.1mmap优点总结
由上文讨论可知,mmap优点共有一下几点:
1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
6.2mmap相关函数
函数原型
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
返回说明
成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1], error被设为以下的某个值:
返回错误类型
参数
start:映射区的开始地址
length:映射区的长度
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
prot
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
flag
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点
相关函数
int munmap( void * addr, size_t len )
成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;
当映射关系解除后,对原来映射地址的访问将导致段错误发生。
int msync( void *addr, size_t len, int flags )
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
七、虚拟内存空间
7.1线性空间
线性地址空间:是指Linux系统中从0x00000000到0xFFFFFFFF整个4GB虚拟存储空间。线性空间又分为用户空间和内核空间。
用户空间(进程地址空间)
用户空间是指从0x00000000到0xBFFFFFFF共3GB的线性地址空间,每个进程都有一个独立的3GB用户空间,所以用户空间由每个进程独有,但是内核线程没有用户空间,因为它不产生用户空间地址。另外子进程共享(继承)父进程的用户空间只是使用与父进程相同的用户线性地址到物理内存地址的映射关系,而不是共享父进程用户空间。运行在用户态和内核态的进程都可以访问用户空间。
linux采用虚拟内存管理技术,每一个进程都有一个3G大小的独立的进程地址空间,这个地址空间就是用户空间。每个进程的用户空间都是完全独立、互不相干的。进程访问内核空间的方式:系统调用和中断。
创建进程等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证,它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在的映射到了物理地址上。
内核空间
内核空间表示运行在处理器最高级别的超级用户模式(supervisor mode)下的代码或数据,内核空间占用从0xC0000000到0xFFFFFFFF的1GB线性地址空间,内核线性地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时所产生的地址都属于内核空间。
内核空间又可分为以下几个线性空间:
1. 内核逻辑地址空间:是指从PAGE_OFFSET(3G)3G+896)之间的线性地址空间,是系统物理内存映射区,它映射了全部或部分(如果系统包含高端内存)物理内存。内核逻辑地址空间中的地址与RAM内存物理地址空间中对应的地址只差一个固定偏移量(3G),如果RAM内存物理地址空间从0x00000000地址编址,那么这个偏移量就是PAGE_OFFSET。
2. 高端线性地址空间:从high_memory(3G+896M)到0xFFFFFFFF之间的线性地址空间属于高端线性地址空间,其中VMALLOC_START~VMALLOC_END之间线性地址:(1)被vmalloc()函数用来分配物理上不连续但线性地址空间连续的高端物理内存,或者(2)被vmap()函数用来映射高端或低端物理内存,或者(3)由ioremap()函数来重新映射I/O物理空间。其中PKMAP_BASE开始的LAST_PKMAP(一般等于1024)页线性地址空间:被kmap()函数用来永久映射高端物理内存。FIXADDR_START开始的KM_TYPE_NR*NR_CPUS页线性地址空间:被kmap_atomic()函数用来临时映射高端物理内存,其他未用高端线性地址空间可以用来在系统初始化期间永久映射I/O地址空间。
7.2 物理空间
低端内存(物理内存)
内核逻辑地址空间所映射物理内存就是低端内存(实际物理内存的大小,但是小于896),低端内存在Linux线性地址空间中始终有永久的一一对应的内核逻辑地址,系统初始化过程中将低端内存永久映射到了内核逻辑地址空间,为低端内存建立了虚拟映射页表。低端内存内物理内存的物理地址与线性地址之间的转换可以通过__pa(x)和__va(x)两个宏来进行,#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) __pa(x)将内核逻辑地址空间的地址x转换成对应的物理地址,相当于__virt_to_phys((unsigned long)(x)),__va(x)则相反,把低端物理内存空间的地址转换成对应的内核逻辑地址,相当于((void *)__phys_to_virt((unsigned long)(x)))。
高端内存(物理内存)
低端内存地址之上的物理内存是高端内存(物理内存896之上),高端内存在Linux线性地址空间中没有没有固定的一一对应的内核逻辑地址,系统初始化过程中不会为这些内存建立映射页表将其固定映射到Linux线性地址空间,而是需要使用高端内存的时候才为分配的高端物理内存建立映射页表,使其能够被内核使用,否则不能被使用。高端内存的物理地址于线性地址之间的转换不能使用上面的__pa(x)和__va(x)宏。
高端内存概念的由来:如上所述,Linux将4GB的线性地址空间划分成两部分,从0x00000000到0xBFFFFFFF共3GB空间作为用户空间由用户进程独占,这部分线性地址空间并没有固定映射到物理内存空间上;从0xC0000000到0xFFFFFFFF的第4GB线性地址空间作为内核空间,在嵌入式系统中,这部分线性地址空间除了映射物理内存空间之外还要映射处理器内部外设寄存器空间等I/O空间。0xC0000000~high_memory之间的内核逻辑地址空间专用来固定映射系统中的物理内存,也就是说0xC0000000~high_memory之间空间大小与系统的物理内存空间大小是相同的(当然在配置了CONFIG_DISCONTIGMEMD选项的非连续内存系统中,内核逻辑地址空间和物理内存空间一样可能存在内存孔洞),如果系统中的物理内存容量远小于1GB,那么内核线性地址空间中内核逻辑地址空间之上的high_memory~0xFFFFFFFF之间还有足够的空间来固定映射一些I/O空间。
可是,如果系统中的物理内存容量(包括内存孔洞)大于1GB,那么就没有足够的内核线性地址空间来固定映射系统全部物理内存以及一些I/O空间了,为了解决这个问题,在x86处理器平台设置了一个经验值:896MB,就是说,如果系统中的物理内存(包括内存孔洞)大于896MB,那么将前896MB物理内存固定映射到内核逻辑地址空间0xC0000000~0xC0000000+896MB(=high_memory)上,而896MB之后的物理内存则不建立到内核线性地址空间的固定映射,这部分内存就叫高端物理内存。此时内核线性地址空间high_memory~0xFFFFFFFF之间的128MB空间就称为高端内存线性地址空间,用来映射高端物理内存和I/O空间。
896MB是x86处理器平台的经验值,留了128MB线性地址空间来映射高端内存以及I/O地址空间,在嵌入式系统中可以根据具体情况修改这个阈值,比如,MIPS中将这个值设置为0x20000000B(512MB),那么只有当系统中的物理内存空间容量大于0x20000000B时,内核才需要配置CONFIG_HIGHMEM选项,使能内核对高端内存的分配和映射功能。什么情况需要划分出高端物理内存以及高端物理内存阈值的设置原则见上面的内存页区(zone)概念说明。
八、内存泄漏和内存溢出
内存溢出(OOM,out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
8.1如何判断内存泄露
用 jstat -gcutil PID
,观察Old这个参数,如果每次执行完FULLGC之后Old区的值一次比一次升高,就可以判断为发生了内存泄漏。
8.2如何判断内存溢出
Heap Dump(堆转储文件)它是一个Java进程在某个时间点上的内存快照。Heap Dump是有着多种类型的。不过总体上heap dump在触发快照的时候都保存了java对象和类的信息。通常在写heap dump文件前会触发一次FullGC,所以heap dump文件中保存的是FullGC后留下的对象信息。
通过设置如下的JVM参数,可以在发生OutOfMemoryError后获取到一份HPROF二进制Heap Dump文件:
-XX:+HeapDumpOnOutOfMemoryError
生成的文件会直接写入到工作目录。
注意:该方法需要JDK5以上版本。
转存堆内存信息后,需要对文件进行分析,从而找到OOM的原因。可以使用以下方式:
mat:eclipse memory analyzer, 基于eclipse RCP的内存分析工具。
jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问分析结果。不推荐使用。
8.3释放内存
在Linux系统下,我们一般不需要去释放内存,因为系统已经将内存管理的很好。但是凡事也有例外,有的时候内存会被缓存占用掉,导致系统使用SWAP空间影响性能,例如当你在linux下频繁存取文件后,物理内存会很快被用光,当程序结束后,内存不会被正常释放,而是一直作为caching。此时就需 要执行释放内存(清理缓存)的操作了。
释放内存操作:
sync # 强制将内存中的缓存写入磁盘 echo 数字 > /proc/sys/vm/drop_caches #数字可以是0-3的整数
数字含义:
- 0:不释放(系统默认值)
- 1:释放页缓存
- 2:释放dentries和inodes
- 3:释放所有缓存
九、内存性能参数
9.1 缓存命中率
缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度。
我们可以通过cachestat和cachetop命令进行查看:
$ cachestat 1 3 TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB 2 0 2 1 17 279 2 0 2 1 17 279 2 0 2 1 17 279
这些指标从左到右依次表示:
- TOTAL ,表示总的 I/O 次数;
- MISSES ,表示缓存未命中的次数;
- HITS ,表示缓存命中的次数;
- DIRTIES, 表示新增到缓存中的脏页数;
- BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
- CACHED_MB 表示 Cache 的大小,以 MB 为单位。
$ cachetop 11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT% 13029 root python 1 0 0 100.0% 0.0%
READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。除此之外,还可以使用pcstat
来查看文件的缓存情况,如查看/bin/ls
$ pcstat /bin/ls +---------+----------------+------------+-----------+---------+ | Name | Size (bytes) | Pages | Cached | Percent | |---------+----------------+------------+-----------+---------| | /bin/ls | 133792 | 33 | 0 | 000.000 | +---------+----------------+------------+-----------+---------+
9.2Swap和NUMA
当物理内存页不足的时候,kswapd0
会触发页面交换,但是在一些时候我们会发现Swap在剩余内存很多的情况下也异常升高,这就是和NUMA有关系了。numactl
命令可以查看处理器在Node的分布情况。
$ numactl --hardware available: 1 nodes (0) node 0 cpus: 0 1 node 0 size: 7977 MB node 0 free: 4416 MB ...
内存阈值(页最小阈值、页低阈值和页高阈值)可以通过内存域在 proc 文件系统中的接口/proc/zoneinfo
来查看
$ cat /proc/zoneinfo ... Node 0, zone Normal pages free 227894 min 14896 low 18620 high 22344 ... nr_free_pages 227894 nr_zone_inactive_anon 11082 nr_zone_active_anon 14024 nr_zone_inactive_file 539024 nr_zone_active_file 923986 ...
使用free
命令可以查看当前Swap的使用情况
$ free total used free shared buff/cache available Mem: 8169348 331668 6715972 696 1121708 7522896 Swap: 0 0 0
Linux 本身支持两种类型的 Swap,即 Swap 分区和 Swap 文件。以 Swap 文件为例,开启方式如下
# 创建Swap文件 $ fallocate -l 8G /mnt/swapfile # 修改权限只有根用户可以访问 $ chmod 600 /mnt/swapfile # 配置Swap文件 $ mkswap /mnt/swapfile # 开启Swap $ swapon /mnt/swapfile
9.3测试工具dd
dd
命令可以用于生成临时文件以及测试读取速度、
# 生成一个512MB的临时文件 $ dd if=/dev/sda1 of=file bs=1M count=512 # 清理缓存 $ echo 3 > /proc/sys/vm/drop_caches #测试读取 $ dd if=file of=/dev/null bs=1M 512+0 records in 512+0 records out 536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s
十、性能工具总结
具体的分析思路主要有这几步:
- 先用 free 和 top,查看系统整体的内存使用情况。
- 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
- 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。
10.1定位内存问题高效方法
1. 先用 free 和 top,查看系统整体的内存使用情况。
2. 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
3. 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。
10.2优化内存方法
1. 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
2.减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
3.尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
4.使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
5.通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。
附录:实战记录
【1】free 命令
- total 总内存大小;
- used 已使用内存的大小,包含了共享内存;
- free 未使用内存的大小;
- shared 共享内存的大小;
- buff/cache 缓存和缓冲区的大小;
- available 新进程可用内存的大小;
【2】top 命令
VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内;
RES 是常驻内存的大小,即进程实际使用的物理内存大小,但不包括 Swap 和共享内存;
SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等;
%MEM 是进程使用物理内存占系统总内存的百分比;
注意
- 1. 虚拟内存通常并不会全部分配物理内存;
- 2. 共享内存 SHR 并不一定是共享的,如程序的代码段、非共享的动态链接库,也都算在 SHR 里;
【3】磁盘和文件写案例——案例一,写文件
测试命令 $ dd if=/dev/urandom of=/tmp/file bs=1M count=500 监控命令 vmstat 2
- 1. 在 Cache 刚开始增长时,块设备 I/O 很少,而过一段时间后,才会出现大量的块设备写;
- 2. 当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且多次 I/O 写的结果加起来是 dd 要写的 500M 的数据;
【4】磁盘和文件写案例——案例二,写磁盘
# 运行dd命令向磁盘分区/dev/sdb1写入2G数据 $ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048 监控命令 vmstat 2 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 0 7584780 153592 97436 0 0 684 0 31 423 1 48 50 2 0 1 0 0 7418580 315384 101668 0 0 0 0 32 144 0 50 50 0 0 1 0 0 7253664 475844 106208 0 0 0 0 20 137 0 50 50 0 0 1 0 0 7093352 631800 110520 0 0 0 0 23 223 0 50 50 0 0 1 1 0 6930056 790520 114980 0 0 0 12804 23 168 0 50 42 9 0 1 0 0 6757204 949240 119396 0 0 0 183804 24 191 0 53 26 21 0 1 1 0 6591516 1107960 123840 0 0 0 77316 22 232 0 52 16 33 0
写磁盘时 (即 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多;
【5】磁盘和文件读案例——案例一、读文件
测试命令 # 运行dd命令读取文件数据 $ dd if=/tmp/file of=/dev/null 监控命令 vmstat 2 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 1 0 7724164 2380 110844 0 0 16576 0 62 360 2 2 76 21 0 0 1 0 7691544 2380 143472 0 0 32640 0 46 439 1 3 50 46 0 0 1 0 7658736 2380 176204 0 0 32640 0 54 407 1 4 50 46 0 0 1 0 7626052 2380 208908 0 0 32640 40 44 422 2 2 50 46 0
读取文件时 (即 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长;
【6】磁盘和文件读案例——案例二、读磁盘
测试命令 # 运行dd命令读取文件 $ dd if=/dev/sda1 of=/dev/null bs=1M count=1024 监控命令 vmstat 2 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7225880 2716 608184 0 0 0 0 48 159 0 0 100 0 0 0 1 0 7199420 28644 608228 0 0 25928 0 60 252 0 1 65 35 0 0 1 0 7167092 60900 608312 0 0 32256 0 54 269 0 1 50 49 0 0 1 0 7134416 93572 608376 0 0 32672 0 53 253 0 0 51 49 0 0 1 0 7101484 126320 608480 0 0 32748 0 80 414 0 1 50 49 0
读磁盘时 (也就是 bi 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快很多;
【1】文件系统与磁盘的区别
磁盘是一个存储设备(块设备),可以被划分为不同的磁盘分区,而在磁盘或者磁盘分区上,还可以再创建文件系统,并挂载到系统的某个目录中,这样,系统就可以通过这个挂载目录,来读写文件;即磁盘是存储数据的块设备,也是文件系统的载体;文件系统需要通过磁盘,来保证数据的持久化存储;
在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交互;在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的 “裸 I/O”;
【2】统计所有进程的物理内存使用量
每个进程的 PSS ,是指把共享内存平分到各个进程后,再加上进程本身的非共享内存大小的和;
# 使用grep查找Pss指标后,再用awk计算累加值 $ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }' 391266 kB
精选文章推荐阅读: