百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Linux内存管理(golang实现)

sinye56 2024-12-06 19:06 13 浏览 0 评论

之前讲过linux进程调度,今天我们来开linux的“任督二脉”第二脉——内存管理。


内存统计信息

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



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


简单内存分配及其问题

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

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

  • 内存碎片化

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

  • 可以访问其他进程的内存

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

  • 难以执行多任务

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


虚拟内存

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

申请虚拟内存的例子:

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 %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/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]

从中可见:

(略)

*** 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 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 %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,也提升了效率。

  • 请求分页(demand paging)

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

demandpaging.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 %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] = 1
	fmt.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/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虚拟内存后,虚拟内存由703264K变为805664K,但是物理内存仍然是3084K,直到touch了一定量的虚拟内存后,物理内存才变化为5132K。

  • 写时复制(copy on write)

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 = %+v\n", 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 = %+v\n", 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数据段变成readonly,这时不管P1或P2谁去改变量i就会产生page fault缺页异常。这时就会copy变量i所在的page到新的物理地址,而P1和P2的虚拟地址保持不变。所以这个操作依赖有MMU内存管理单元的CPU。

  • swap

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

  • 虚拟内存/物理内存不足

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

  • 标准大页

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

相关推荐

linux 查看当前应用内存状况,以及内存参数含义

1、查看进程号ps-ef|greptomcat2、查看当前内存分配,200ms打印一次jstat-gc进程号2001jstat-gc344802001S0CS1C...

如何显示 Linux 系统上的可用内存?这几个命令很好用!

在Linux系统中,了解可用内存是优化系统性能、故障排查以及资源管理的重要一环。本文将详细介绍如何在Linux系统上显示可用内存,包括多种方法和工具的使用。在讨论可用内存之前,我们需要了解一些...

Linux 下查看内存使用情况方法总结

Q:我想监视Linux系统的内存使用情况,在Linux下有哪些视图或者命令行工具可用呢?在做Linux系统优化的时候,物理内存是其中最重要的一方面。自然的,Linux也提供了非常多的方法来监控宝贵的内...

2、linux命令-用户管理

linux命令-用户管理用户切换[root@eric~]#sueric#切换到用户eric[eric@ericroot]$[eric@ericroot]$su#切换到rootPas...

Centos 7 进入单用户模式详解

1、开机在启动菜单按e进入编辑模式找到linux16行,在最后添加init=/bin/sh编辑完后,按ctrl+x退出2、进单用户模式后,使用passwd修改密码,提示以下错误:passwd:Aut...

每日一个Linux命令解析——newusers

newusers:在Linux系统中,newusers是一个用于批量创建用户的命令。它从一个文件中读取多行用户信息,每行描述一个用户的详细信息,并根据这些信息创建多个用户或对现有用户进行批量修改。一...

openEuler操作系统管理员指南:管理用户与用户组

在Linux中,每个普通用户都有一个账户,包括用户名、密码和主目录等信息。除此之外,还有一些系统本身创建的特殊用户,它们具有特殊的意义,其中最重要的是管理员账户,默认用户名是root。同时Linux也...

Linux用户管理

1、用户信息文件/etc/passwdroot:x:0:0:root:/root:/bin/bash第一列:用户名第二列:密码位第三列:用户ID0超级用户UID。如果用户UID...

centos7基础-用户、组、权限管理

用户和组(1)用户、组、家目录的概念linux系统支持多用户,除了管理员,其他用户一般不应该使用root,而是应该向管理员申请一个账号。组类似于角色,系统可以通过组对有共性的用户进行统一管理。每个用户...

LINUX基础 ----------组及用户的概念

在Linux中,用户和组都是非常重要的概念,可以控制文件访问权限和资源的管理。用户是标识一个进程、应用程序或系统管理员的账号,Linux中每个用户用一个用户ID(UID)来标识。对于一个...

从零入门Linux(四)用户与权限管理

在Linux系统中,用户和权限管理是系统安全的重要组成部分。通过合理配置用户和权限,可以确保系统的安全性和资源的合理分配。以下是一些与用户和权限管理相关的常用命令和概念。1.用户管理1.1添加...

如何在 Linux 中管理用户?

在Linux系统中,用户是系统资源的主要使用者,每个用户都有一个唯一的标识符(用户ID)。为了更好地组织和管理用户,Linux还引入了用户组的概念。用户组是用户的集合,有助于更有效地分配权限和资...

在 Linux 中将用户添加到特定组的四种方法

在Linux多用户操作系统中,用户组管理是系统安全架构的基石。通过合理的组权限分配,管理员可以实现:精确控制文件访问权限(chmod775project/)简化批量用户权限管理(setfacl-...

我不是网管 - 如何在Ubuntu Linux下创建sudo用户

Sudo用户是Linux系统的普通用户,具有一定的管理权限,可以对系统执行管理任务。在Linux中,root是超级用户,拥有完全的管理权限,但不建议将root凭证授予其他用户或作为r...

Linux创建普通用户,为密钥方式登录做准备

Hi,我是聪慧苹果8,就是江湖上人见人爱、花见花开,土到掉榨的Linux爱好者,一起学习吧!上一篇关于SSH安全加固的文字,有网友点评通过密钥登录更加安全,先创建一个普通用户,拒绝直接使用密码登录,这...

取消回复欢迎 发表评论: