之前讲过linux进程调度,今天我们来开linux的“任督二脉”第二脉——内存管理。
内存统计信息
执行free -h,结果如下图所示:
其中,free是空闲内存,是free+buff/cache中可释放的内存,就是实际可用内存。当耗尽后,就会出现OOM(Out Of )的情况,linux内核的内存管理系统会运行OOM 选择合适的进程进行kill。
简单内存分配及其问题
计算器启动后,CPU首先进入实模式,在此基础上可以进入保护模式(分段)。这两种模式下进行的内存分配是简单模式,即段+偏移的方式。
在内存简单分配模式下,会出现三种主要的问题:
内存碎片化之后,可能会存在多个不连续的小块内存空间,这样的话不能利用一块大内存来完成任务。比如有多个不连续的的小空间,我想申请一个的数组没法做到。
存在数据被损毁或泄漏的风险。
需要小心翼翼地安排各个进程,给多任务带来很多困难。
即分页模式。进程无法直接访问物理内存,只是使用虚拟内存,也叫线性地址空间。所有内存都以页为单位进行管理。操作系统使用保存在内核使用内存的页表来完成线性地址到物理地址的转换。
申请虚拟内存的例子:
mmap.go
package main
import (
"fmt"
"log"
"os"
"os/exec"
"golang.org/x/sys/unix"
)
var ALLOC_SIZE = 100 * 1024 * 1024 // 100M
func main() {
pid := os.Getpid()
fmt.Println("*** memory map before memory allocation ***")
out1, err := checkMaps(pid)
if err != nil {
log.Fatalf("check maps before mmap failed with %sn", err)
}
fmt.Println(out1)
memory, err := unix.Mmap(-1, 0, ALLOC_SIZE, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
if err != nil {
log.Fatalf("mmap() failed with %sn", err)
}
defer unix.Munmap(memory)
fmt.Printf("*** succeed to allocate memory: address-%p, size-%d ***n", memory, ALLOC_SIZE)
fmt.Println("*** memory map after memory allocation ***")
out2, err := checkMaps(pid)
if err != nil {
log.Fatalf("check maps after mmap failed with %sn", err)
}
fmt.Println(out2)
}
func checkMaps(pid int) (string, error) {
cmd := exec.Command("bash", "-c", fmt.Sprintf("cat /proc/%d/maps", pid))
out, err := cmd.CombinedOutput()
return string(out), err
}
cat /proc/{pid}/maps可以查看进程的虚拟内存。
我用mmap系统调用申请100M的虚拟内存(其实用户空间底层就是调用mmap来申请内存),然后在申请前后执行cat /proc/{pid}/maps来查看申请前后虚拟内存的变化。结果如下:
*** memory map before memory allocation ***
00400000-0049e000 r-xp 00000000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
0049e000-00541000 r--p 0009e000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
00541000-0055c000 rw-p 00141000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
0055c000-00590000 rw-p 00000000 00:00 0
c000000000-c000400000 rw-p 00000000 00:00 0
c000400000-c004000000 ---p 00000000 00:00 0
7f96fa7ec000-7f96fcb9d000 rw-p 00000000 00:00 0
7f96fcb9d000-7f970cd1d000 ---p 00000000 00:00 0
7f970cd1d000-7f970cd1e000 rw-p 00000000 00:00 0
7f970cd1e000-7f971ebcd000 ---p 00000000 00:00 0
7f971ebcd000-7f971ebce000 rw-p 00000000 00:00 0
7f971ebce000-7f9720fa3000 ---p 00000000 00:00 0
7f9720fa3000-7f9720fa4000 rw-p 00000000 00:00 0
7f9720fa4000-7f972141d000 ---p 00000000 00:00 0
7f972141d000-7f972141e000 rw-p 00000000 00:00 0
7f972141e000-7f972149d000 ---p 00000000 00:00 0
7f972149d000-7f97214fd000 rw-p 00000000 00:00 0
7ffe050f1000-7ffe05112000 rw-p 00000000 00:00 0 [stack]
7ffe051ca000-7ffe051ce000 r--p 00000000 00:00 0 [vvar]
7ffe051ce000-7ffe051cf000 r-xp 00000000 00:00 0 [vdso]
*** succeed to allocate memory: address-0x7f96f43ec000, size-104857600 ***
*** memory map after memory allocation ***
00400000-0049e000 r-xp 00000000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
0049e000-00541000 r--p 0009e000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
00541000-0055c000 rw-p 00141000 08:10 1382385 /tmp/go-build3881664940/b001/exe/mmap
0055c000-00590000 rw-p 00000000 00:00 0
c000000000-c000400000 rw-p 00000000 00:00 0
c000400000-c004000000 ---p 00000000 00:00 0
7f96f43ec000-7f96fcb9d000 rw-p 00000000 00:00 0
7f96fcb9d000-7f970cd1d000 ---p 00000000 00:00 0
7f970cd1d000-7f970cd1e000 rw-p 00000000 00:00 0
7f970cd1e000-7f971ebcd000 ---p 00000000 00:00 0
7f971ebcd000-7f971ebce000 rw-p 00000000 00:00 0
7f971ebce000-7f9720fa3000 ---p 00000000 00:00 0
7f9720fa3000-7f9720fa4000 rw-p 00000000 00:00 0
7f9720fa4000-7f972141d000 ---p 00000000 00:00 0
7f972141d000-7f972141e000 rw-p 00000000 00:00 0
7f972141e000-7f972149d000 ---p 00000000 00:00 0
7f972149d000-7f97214fd000 rw-p 00000000 00:00 0
7ffe050f1000-7ffe05112000 rw-p 00000000 00:00 0 [stack]
7ffe051ca000-7ffe051ce000 r--p 00000000 00:00 0 [vvar]
7ffe051ce000-7ffe051cf000 r-xp 00000000 00:00 0 [vdso]
从中可见:
(略)
*** to : -, size- ***
(略)
– rw-p 00:00 0
(略)
调用mmap返回的地址和cat /proc/{pid}/maps中显示的地址一样,说明成功申请到了内存。
虚拟内存解决了简单内存分配出现的3个问题:通过页表,将物理地址上的碎片整合成线性地址空间上的连续空间,解决了内存碎片化问题。每个进程都有各自的页表,这样就解决了可以访问其他进程的内存的问题。有了虚拟内存,我们不用关心自身在哪个物理内存上,所以可以很方便地执行多任务。
虚拟内存的应用
进程在访问文件时,一般可以用read()、write()、lseek()等系统调用。但是这样会有很多内核缓冲区与进程缓冲区之间的复制行为发生,效率较低。我们可以使用mmap将文件映射到进程的虚拟内存,对虚拟内存的读写即对文件的读写。
.go
package main
import (
"log"
"os"
"golang.org/x/sys/unix"
)
var ALLOC_SIZE = 100 * 1024 * 1024 // 100M
func main() {
memory, err := mmap("foo")
if err != nil {
log.Fatalf("mmap failed with %sn", err)
}
defer unix.Munmap(memory)
copy(memory, []byte("hello, linux"))
unix.Msync(memory, unix.MS_ASYNC)
}
func mmap(name string) ([]byte, error) {
file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return nil, err
}
file.Truncate(10)
defer file.Close()
return unix.Mmap(int(file.Fd()), 0, ALLOC_SIZE, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
}
运行后,文件foo的内容为”hello, lin”,因为文件长度是,所以被截取了一部分。
etcd使用了mmap,所以提升了写文件的效率。同时,因为是堆外内存,所以不参与gc,也提升了效率。
进程在申请完内存后,其实linux不会马上为其分配对应的物理内存,当实际使用虚拟内存后,引发缺页中断,进入内核态,内核才真正分配物理内存,这样不会造成物理内存浪费。
.go
package main
import (
"fmt"
"log"
"os"
"os/exec"
"golang.org/x/sys/unix"
)
var ALLOC_SIZE = 100 * 1024 * 1024 // 100M
func main() {
pid := os.Getpid()
fmt.Println("*** memory usage before memory allocation ***")
out1, err := checkMemUsage(pid)
if err != nil {
log.Fatalf("checkMemUsage1 failed with %sn", err)
}
fmt.Println(out1)
memory, err := unix.Mmap(-1, 0, ALLOC_SIZE, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
if err != nil {
log.Fatalf("mmap() failed with %sn", err)
}
defer unix.Munmap(memory)
fmt.Printf("*** succeed to allocate memory: address-%p, size-%d ***n", memory, ALLOC_SIZE)
fmt.Println("*** memory usage after memory allocation ***")
out2, err := checkMemUsage(pid)
if err != nil {
log.Fatalf("checkMemUsage2 failed with %sn", err)
}
fmt.Println(out2)
memory[10*1024*1024] = 1
fmt.Println("*** memory usage after memory touch ***")
out3, err := checkMemUsage(pid)
if err != nil {
log.Fatalf("checkMemUsage3 failed with %sn", err)
}
fmt.Println(out3)
}
func checkMemUsage(pid int) (string, error) {
cmd := exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %d", pid))
out, err := cmd.CombinedOutput()
return string(out), err
}
输出结果为:
*** memory usage before memory allocation ***
hoo 26271 0.0 0.0 703264 3084 pts/1 Sl+ 23:51 0:00 /tmp/go-build265496847/b001/exe/demandpaging
hoo 26276 0.0 0.0 8620 3052 pts/1 S+ 23:51 0:00 bash -c ps aux | grep 26271
hoo 26278 0.0 0.0 8164 720 pts/1 S+ 23:51 0:00 grep 26271
*** succeed to allocate memory: address-0x7faa0484b000, size-104857600 ***
*** memory usage after memory allocation ***
hoo 26271 0.0 0.0 805664 3084 pts/1 Sl+ 23:51 0:00 /tmp/go-build265496847/b001/exe/demandpaging
hoo 26279 0.0 0.0 8620 2996 pts/1 S+ 23:51 0:00 bash -c ps aux | grep 26271
hoo 26281 0.0 0.0 8164 652 pts/1 S+ 23:51 0:00 grep 26271
*** memory usage after memory touch ***
hoo 26271 0.0 0.0 805664 5132 pts/1 Sl+ 23:51 0:00 /tmp/go-build265496847/b001/exe/demandpaging
hoo 26282 0.0 0.0 8620 3080 pts/1 S+ 23:51 0:00 bash -c ps aux | grep 26271
hoo 26284 0.0 0.0 8164 656 pts/1 S+ 23:51 0:00 grep 26271
可见,申请100M虚拟内存后,虚拟内存由变为,但是物理内存仍然是3084K,直到touch了一定量的虚拟内存后,物理内存才变化为5132K。
fork系统调用实际上是为子进程复制了一份父进程相同的页表。
cow.go
package main
import (
"log"
"os"
"github.com/docker/docker/pkg/reexec"
)
var i = 10
func init() {
log.Printf("init start, os.Args = %+vn", os.Args)
reexec.Register("childProcess", childProcess)
if reexec.Init() {
os.Exit(0)
}
}
func childProcess() {
i = 20
log.Printf("2: %v", i)
log.Println("childProcess")
}
func main() {
log.Printf("main start, os.Args = %+vn", os.Args)
log.Printf("1: %v", i)
cmd := reexec.Command("childProcess")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
log.Panicf("failed to run command: %s", err)
}
if err := cmd.Wait(); err != nil {
log.Panicf("failed to wait command: %s", err)
}
log.Printf("3: %v", i)
log.Println("main exit")
}
运行结果:10 20 10
原因是:一开始变量i所在的数据段是可rw的,fork以后P1和P2数据段变成,这时不管P1或P2谁去改变量i就会产生page fault缺页异常。这时就会copy变量i所在的page到新的物理地址,而P1和P2的虚拟地址保持不变。所以这个操作依赖有MMU内存管理单元的CPU。
swap算是linux对于OOM的一种补救。当物理内存不足时,内核会将正在使用的物理内存的一部分页面换出到swap空间。后续再使用时再换入内存。但是,如果系统长期处于内存不足状态时,会频繁地换出换入,造成系统抖动。
64bit的虚拟内存高达128T,所以虚拟内存不足非常罕见。物理内存不足比较常见。
标准大页可以减少页表占用的空间,fork会复制页表,所以也会提升fork的效率。
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,永久会员只需109元,全站资源免费下载 点击查看详情
站 长 微 信: nanadh666