今天我们就来开启Linux的《任督二脉》的第二行——内存管理。执行free-h进行内存统计,结果如下图:其中,free为空闲内存,available为free+buff/cache中的可释放内存,即实际可用内存。当available耗尽时,会发生OOM(OutOfmemory),Linux内核的内存管理系统会运行OOMKiller来选择合适的进程进行kill。简单的内存分配及其问题计算器启动后,CPU首先进入实模式,在此基础上可以进入保护模式(分段)。这两种模式下的内存分配都是简单的模式,即segment+offset。在简单的内存分配模式下,主要会存在三个问题:内存碎片内存碎片化后,可能会出现多个不连续的小内存空间,从而无法使用大内存来完成任务。比如有多个不连续的10Byte的小空格,想申请100Byte的数组我做不到。访问其他进程的内存存在数据损坏或泄漏的风险。困难多任务处理需要仔细安排进程,这使得多任务处理变得困难。虚拟内存是分页模式。进程不能直接访问物理内存,而是使用虚拟内存,也称为线性地址空间。所有内存都以页面为单位进行管理。操作系统使用存储在内核使用的内存中的页表将线性地址转换为物理地址。申请虚拟内存的例子:mmap.gopackagemainimport("fmt""log""os""os/exec""golang.org/x/sys/unix")varALLOC_SIZE=100*1024*1024//100Mfuncmain(){pid:=os.Getpid()fmt.Println("***内存分配前的内存映射***")out1,err:=checkMaps(pid)iferr!=nil{log.Fatalf("检查映射在mmap因%s\n",err)}fmt.Println(out1)memory,err:=unix.Mmap(-1,0,ALLOC_SIZE,unix.PROT_READ|unix.PROT_WRITE,unix.MAP_PRIVATE|unix.MAP_ANON)iferr!=nil{log.Fatalf("mmap()failedwith%s\n",err)}deferunix.Munmap(memory)fmt.Printf("***成功分配内存:address-%p,size-%d***\n",memory,ALLOC_SIZE)fmt.Println("***内存分配后的内存映射***")out2,err:=checkMaps(pid)iferr!=nil{log.Fatalf("在mmap失败后检查映射,%s\n",err)}fmt.Println(out2)}funccheckMaps(pidint)(string,error){cmd:=exec.Command("bash","-c",fmt.Sprintf("cat/proc/%d/maps",pid))out,err:=cmd.CombinedOutput()返回字符串(out),err}cat/proc/{pid}/maps可以查看进程的虚拟内存。我使用mmap系统调用申请100M虚拟内存(其实用户空间malloc底层就是调用mmap申请内存),然后执行cat/proc/{申请前后pid}/maps查看应用前后虚拟内存的变化。结果如下:***memorymapbeforememoryallocation***00400000-0049e000r-xp0000000008:101382385/tmp/go-build3881664940/b001/exe/mmap0049e000-00541000r--p0009e0001/t8:1808:18/GO-BUILD3881664940/B001/EXE/MMAP00541000-0055C000RW-P0014100008:101382385/TMP/GO-BUILD3881664940/B001/exe/exe/exe/mmap005555c00000000000000000000000000000000000000000000000000000000000000000000000000000车:000c000400000-c004000000---p0000000000:0007f96fa7ec000-7f96fcb9d000rw-p0000000000:0007f96fcb9d000-7f970cd1d000---p0000000000:0007f970cd1d000-7f970cd1e000rw-p0000000000:0007f970cd1e000-7F971EBCD000---P0000000000:0007F971EBCD000-7F971EBCE000RW-P000000000000000000:0007F971EBCE000-77F9720FA3000FA3000FA-p0000000000:0007f972141d000-7f972141e000rw-p0000000000:0007f972141e000-7f972149d000---P000000000000:0007F972149D000-7F97214FD000RW-P000000000000000000:0007FFE050F1000-7FFE05112000RW-P0000000000000000000000000000000000000000:0000[Stack]7ffe051CACA000-7FFEE000-P-7FFEE00000000000000000000000000000000000000000000000来7ffe051ce000-7ffe051cf000r-xp0000000000:000[vdso]***内存分配成功:address-0x7f96f43ec000,size-104857600******内存分配后的内存映射***00400000-0049e000r-xp0000000008:101382385/tmp/go-build3881664940/b001/exe/mmap0049e000-00541000r--p0009e00008:101382385/tmp/go-build3881664940/b001/exe/mmap00541000-0055c000rw-p0014100008:101382385/TMP/GO-BUILD3881664940/B001/EXE/MMAP0055C000-00590000RW-P000000000000000000000000000000000000000000000000000000000c0000000-C000400000RW-P00000000000000000000000000000000000000000000来0000000000:0007f96fcb9d000-7f970cd1d000---p0000000000:0007f970cd1d000-7f970cd1e000rw-p0000000000:0007f970cd1e000-7f971ebcd000---p0000000000:0007f971ebcd000-7f971ebce000rw-p0000000000:0007f971ebce000-7f9720fa3000---p0000000000:0007f9720fa3000-7f9720fa4000rw-p0000000000:0007f9720fa4000-7f972141d000---p0000000000:0007f972141d000-7f972141e000rw-p0000000000:0007f972141e000-7f972149d000---p0000000000:0007f972149d000-7f97214fd000rw-p0000000000:0007ffe050f1000-7ffe05112000rw-p0000000000:000[stack]7ffe051ca000-7ffe051ce000r--p0000000000:000[vvar]7ffe051ce000-7ffe051cf000r-xp0000000000:000[vdso]从中可以看出:(略)***成功分配内存:address-0x7f96f43ec000,size-104857600***(略)7f96f43ec000-7f96fcb9d000rw-p0000000000:000(略)调用mmap返回的地址与cat/proc/{pid}/maps中显示的地址相同,说明内存申请成功d为。虚拟内存解决了简单内存分配中的三个问题:通过页表,将物理地址上的碎片整合成一个线性地址空间,上面的连续空间解决了内存碎片问题。每个进程都有自己的页表,解决了无法访问其他进程内存的问题。有了虚拟内存,我们就不用关心自己在哪个物理内存上了,可以轻松的进行多任务处理。虚拟内存的申请文件映射过程在访问文件时一般可以使用read()、write()、lseek()等系统调用。但是这样一来,内核缓冲区和进程缓冲区之间会有很多复制行为,效率低下。我们可以使用mmap将文件映射到进程的虚拟内存中,而读写虚拟内存就是读写文件。filemap.gopackagemainimport("log""os""golang.org/x/sys/unix")varALLOC_SIZE=100*1024*1024//100Mfuncmain(){memory,err:=mmap("foo")iferr!=nil{log.Fatalf("mmapfailedwith%s\n",err)}deferunix.Munmap(memory)copy(memory,[]byte("hello,linux"))unix.Msync(memory,unix.MS_ASYNC)}funcmmap(namestring)([]byte,error){file,err:=os.OpenFile(name,os.O_CREATE|os.O_RDWR,0644)iferr!=nil{返回nil,err}file.Truncate(10)deferfile.Close()returnunix.Mmap(int(file.Fd()),0,ALLOC_SIZE,unix.PROT_READ|unix.PROT_WRITE,unix.MAP_SHARED)}运行后,文件foo内容是“hello,lin”,因为文件长度为10Byte,所以截取了一部分。etcd使用了mmap,所以写文件的效率提高了。同时因为是堆外内存,不参与gc,也提高了效率。请求分页(demandpaging)进程申请内存后,Linux不会立即为其分配相应的物理内存。当实际使用虚拟内存时,会触发缺页中断,内核在进入内核态时才会真正分配物理内存。不浪费物理内存。demandpaging.gopackagemainimport("fmt""log""os""os/exec""golang.org/x/sys/unix")varALLOC_SIZE=100*1024*1024//100Mfuncmain(){pid:=os.Getpid()fmt.Println("***内存分配前的内存使用情况***")out1,err:=checkMemUsage(pid)iferr!=nil{log.Fatalf("checkMemUsage1失败,%s\n",err)}fmt.Println(out1)memory,err:=unix.Mmap(-1,0,ALLOC_SIZE,unix.PROT_READ|unix.PROT_WRITE,unix.MAP_PRIVATE|unix.MAP_ANON)iferr!=nil{log.Fatalf("mmap()failedwith%s\n",err)}deferunix.Munmap(memory)fmt.Printf("***成功分配内存:address-%p,size-%d***\n",memory,ALLOC_SIZE)fmt.Println("***内存分配后的内存使用情况***")out2,err:=checkMemUsage(pid)iferr!=nil{log.Fatalf("checkMemUsage2失败,%s\n",err)}fmt.Println(out2)memory[10*1024*1024]=1fmt.Println("***内存触摸后内存使用情况***")out3,err:=checkMemUsage(pid)iferr!=nil{log.Fatalf("checkMemUsage3failedwith%s\n",err)}fmt.Println(out3)}funccheckMemUsage(pidint)(字符串,错误){cmd:=exec.Command("bash","-c",fmt.Sprintf("psaux|grep%d",pid))out,err:=cmd.CombinedOutput()returnstring(out),err}输出结果为:***内存分配前的内存使用情况***hoo262710.00.07032643084pts/1Sl+23:510:00/tmp/go-build265496847/b001/exe/demandpaginghoo262760.00.086203052pts/1S+23:510:00bash-cpsaux|grep26271hoo262780.00.08164720pts/1S+23:510:00grep26271***成功分配内存:address-0x7faa0484b000,size-104857600******内存分配后的内存使用情况***hoo262710.00.08056643084点/1Sl+23:510:00/tmp/go-build265496847/b001/exe/demandpaginghoo262790.00.086202996点/1S+23:510:00bash-cpsaux|grep26271hoo262810.00.08164652pts/1S+23:510:00grep26271***内存触摸后的内存使用情况***hoo262710.00.08056645132点/1Sl+23:510:00/tmp/go-build265496847/b001/exe/demandpaginghoo262820.00.086203080点/1Sl+23:510:00bash-cpsaux|262840.00.08164656pts/1S+23:510:00grep26271可以看到申请100M虚拟内存后,虚拟内存从703264K变为805664K,但是物理内存还是3084K,直到一定量虚拟内存被触及。物理内存只变成了5132K。写时复制(copyonwrite)fork系统调用实际上为子进程复制了与父进程相同的页表。cow.gopackagemainimport("log""os""github.com/docker/docker/pkg/reexec")vari=10funcinit(){log.Printf("initstart,os.Args=%+v\n",os.Args)reexec.Register("childProcess",childProcess)ifreexec.Init(){os.Exit(0)}}funcchildProcess(){i=20log.Printf("2:%v",i)log.Println("childProcess")}funcmain(){log.Printf("mainstart,os.Args=%+v\n",os.Args)log.Printf("1:%v",i)cmd:=reexec.Command("childProcess")cmd.Stdin=os.Stdincmd.Stdout=os.Stdoutcmd.Stderr=os.Stderriferr:=cmd.Start();err!=nil{log.Panicf("运行命令失败:%s",err)}iferr:=cmd.Wait();err!=nil{log.Panicf("failedtowaitcommand:%s",err)}log.Printf("3:%v",i)log.Println("mainexit")}运行结果:102010、原因是:一开始,变量i所在的数据段是rwable的。fork之后,P1和P2数据段变为readonly。这时候无论P1还是P2改变变量i,都会产生pagefault。此时,变量i所在的页将被复制到新的物理地址,而P1和P2的虚拟地址保持不变。所以这个操作依赖于带有MMU内存管理单元的CPU。swapswap是linux对OOM的补救措施。当物理内存不足时,内核会将正在使用的物理内存中的一些页面换出到交换空间。然后以后用的时候换成内存。但是如果系统长期处于内存不足的状态,就会频繁的换出换入,造成系统抖动。虚拟内存/物理内存不足64bit虚拟内存高达128T,所以虚拟内存不足的情况很少见。物理内存不足是比较常见的。StandardHugePagesStandardHugePages可以减少页表占用的空间。fork会复制页表,这样也会提高fork的效率。
