文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Linux内存管理(Golang实现)

2024-12-01 17:59

关注

今天我们来开linux的“任督二脉”第二脉——内存管理。

内存统计信息

执行free -h,结果如下图所示:

其中,free是空闲内存,available是free+buff/cache中可释放的内存,就是实际可用内存。当available耗尽后,就会出现OOM(Out Of memory)的情况,linux内核的内存管理系统会运行OOM Killer选择合适的进程进行kill。

简单内存分配及其问题

计算器启动后,CPU首先进入实模式,在此基础上可以进入保护模式(分段)。这两种模式下进行的内存分配是简单模式,即段+偏移的方式。

在内存简单分配模式下,会出现三种主要的问题:

内存碎片化之后,可能会存在多个不连续的小块内存空间,这样的话不能利用一块大内存来完成任务。比如有多个不连续的10Byte的小空间,我想申请一个100Byte的数组没法做到。

存在数据被损毁或泄漏的风险。

需要小心翼翼地安排各个进程,给多任务带来很多困难。

虚拟内存

即分页模式。进程无法直接访问物理内存,只是使用虚拟内存,也叫线性地址空间。所有内存都以页为单位进行管理。操作系统使用保存在内核使用内存的页表来完成线性地址到物理地址的转换。

申请虚拟内存的例子:

mmap.go

package mainimport ("fmt""log""os""os/exec""golang.org/x/sys/unix")var ALLOC_SIZE = 100 * 1024 * 1024 // 100Mfunc 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 %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)if err != nil {log.Fatalf("mmap() failed with %s\n", 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 %s\n", 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的虚拟内存(其实用户空间malloc底层就是调用mmap来申请内存),然后在申请前后执行cat /proc/{pid}/maps来查看申请前后虚拟内存的变化。结果如下:

*** memory map before memory allocation ***00400000-0049e000 r-xp 00000000 08:10 1382385                            /tmp/go-build3881664940/b001/exe/mmap0049e000-00541000 r--p 0009e000 08:10 1382385                            /tmp/go-build3881664940/b001/exe/mmap00541000-0055c000 rw-p 00141000 08:10 1382385                            /tmp/go-build3881664940/b001/exe/mmap0055c000-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/mmap0049e000-00541000 r--p 0009e000 08:10 1382385                            /tmp/go-build3881664940/b001/exe/mmap00541000-0055c000 rw-p 00141000 08:10 1382385                            /tmp/go-build3881664940/b001/exe/mmap0055c000-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]

从中可见:

()

*** succeed to allocate memory: address-0x7f96f43ec000, size-104857600 ***

()

7f96f43ec000-7f96fcb9d000 rw-p 00000000 00:00 0

()

调用mmap返回的地址和cat /proc/{pid}/maps中显示的地址一样,说明成功申请到了内存。

虚拟内存解决了简单内存分配出现的3个问题:通过页表,将物理地址上的碎片整合成线性地址空间上的连续空间,解决了内存碎片化问题。每个进程都有各自的页表,这样就解决了可以访问其他进程的内存的问题。有了虚拟内存,我们不用关心自身在哪个物理内存上,所以可以很方便地执行多任务。

虚拟内存的应用

进程在访问文件时,一般可以用read()、write()、lseek()等系统调用。但是这样会有很多内核缓冲区与进程缓冲区之间的复制行为发生,效率较低。我们可以使用mmap将文件映射到进程的虚拟内存,对虚拟内存的读写即对文件的读写。

filemap.go

package mainimport ("log""os""golang.org/x/sys/unix")var ALLOC_SIZE = 100 * 1024 * 1024 // 100Mfunc main() {memory, err := mmap("foo")if err != nil {log.Fatalf("mmap failed with %s\n", 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",因为文件长度是10Byte,所以被截取了一部分。

etcd使用了mmap,所以提升了写文件的效率。同时,因为是堆外内存,所以不参与gc,也提升了效率。

进程在申请完内存后,其实linux不会马上为其分配对应的物理内存,当实际使用虚拟内存后,引发缺页中断,进入内核态,内核才真正分配物理内存,这样不会造成物理内存浪费。

demandpaging.go

package mainimport ("fmt""log""os""os/exec""golang.org/x/sys/unix")var ALLOC_SIZE = 100 * 1024 * 1024 // 100Mfunc main() {pid := os.Getpid()fmt.Println("*** memory usage before memory allocation ***")out1, err := checkMemUsage(pid)if err != nil {log.Fatalf("checkMemUsage1 failed with %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)if err != nil {log.Fatalf("mmap() failed with %s\n", 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 %s\n", err)}fmt.Println(out2)memory[10*1024*1024] = 1fmt.Println("*** memory usage after memory touch ***")out3, err := checkMemUsage(pid)if err != nil {log.Fatalf("checkMemUsage3 failed with %s\n", 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/demandpaginghoo      26276  0.0  0.0   8620  3052 pts/1    S+   23:51   0:00 bash -c ps aux | grep 26271hoo      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/demandpaginghoo      26279  0.0  0.0   8620  2996 pts/1    S+   23:51   0:00 bash -c ps aux | grep 26271hoo      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/demandpaginghoo      26282  0.0  0.0   8620  3080 pts/1    S+   23:51   0:00 bash -c ps aux | grep 26271hoo      26284  0.0  0.0   8164   656 pts/1    S+   23:51   0:00 grep 26271

可见,申请100M虚拟内存后,虚拟内存由703264K变为805664K,但是物理内存仍然是3084K,直到touch了一定量的虚拟内存后,物理内存才变化为5132K。

fork系统调用实际上是为子进程复制了一份父进程相同的页表。

cow.go

package mainimport ("log""os""github.com/docker/docker/pkg/reexec")var i = 10func init() {log.Printf("init start, os.Args = %+v\n", os.Args)reexec.Register("childProcess", childProcess)if reexec.Init() {os.Exit(0)}}func childProcess() {i = 20log.Printf("2: %v", i)log.Println("childProcess")}func main() {log.Printf("main start, os.Args = %+v\n", os.Args)log.Printf("1: %v", i)cmd := reexec.Command("childProcess")cmd.Stdin = os.Stdincmd.Stdout = os.Stdoutcmd.Stderr = os.Stderrif 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数据段变成readonly,这时不管P1或P2谁去改变量i就会产生page fault缺页异常。这时就会copy变量i所在的page到新的物理地址,而P1和P2的虚拟地址保持不变。所以这个操作依赖有MMU内存管理单元的CPU。

swap算是linux对于OOM的一种补救。当物理内存不足时,内核会将正在使用的物理内存的一部分页面换出到swap空间。后续再使用时再换入内存。但是,如果系统长期处于内存不足状态时,会频繁地换出换入,造成系统抖动。

64bit的虚拟内存高达128T,所以虚拟内存不足非常罕见。物理内存不足比较常见。

标准大页可以减少页表占用的空间,fork会复制页表,所以也会提升fork的效率。

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯