深入探索Linux内存管理:初学者指南

一、Linux 内存管理概述

Linux 内存管理是一个非常广泛的主题,无法在一篇文章中涵盖所有领域。本文主要提供主要领域的概述,并帮助了解与 Linux 内存管理相关的重要术语。

计算机的核心部分是 CPU,RAM 是 CPU 的前端门户,进入 CPU 的所有内容都将通过 RAM。例如,如果有一个正在加载的进程,则该进程将首先加载到 RAM 中,CPU 将从 RAM 获取进程数据。但为了使其更快,CPU 具有一级、二级、三级缓存。这些缓存就像 RAM一样,但是它集成在 CPU 上,并且CPU 上的缓存量非常小,因为它非常昂贵,而且对所有指令也不是很有用。

因此,进程信息是从 RAM 复制到 CPU,CPU 构建其缓存。缓存在 Linux 上的内存管理中也起着重要作用。
在这里插入图片描述

如果用户从硬盘请求信息,则将其复制到 RAM,并从 RAM 为用户提供服务。当信息从硬盘复制时,它被放置在所谓的页面缓存中。因此,页面缓存存储最近请求的数据,以便在再次需要相同的数据时使其更快。

如果用户开始修改数据,它也会进入RAM,从RAM到硬盘,但这只有在数据在RAM中停留足够长的时间时才会发生。数据不会立即从RAM写入硬盘,但为了优化对硬盘的写入,Linux采用了脏缓存的概念。它尝试缓冲尽可能多的数据,以创建有效的写入请求。因此,计算机上发生的一切都通过RAM。

所以,在 Linux 计算机上使用 RAM 对于 Linux 操作系统的良好运行至关重要。接下来讨论 Linux 内存管理的各个部分,并了解与此流程相关的不同术语。

二、了解虚拟内存

在分析 Linux 内存使用情况时,应该了解 Linux 如何使用虚拟内存和驻留内存。Linux 上的虚拟内存从字面上理解:它是可以引用 Linux 内核的不存在的内存量。

查看总物理内存:

$ grep MemTotal /proc/meminfo
MemTotal:        7023012 kB

查看虚拟内存:

$ grep VmallocTotal /proc/meminfo
VmallocTotal:   34359738367 kB

可以看到有32TB的虚拟内存。

如果在 Linux 中,当一个进程加载时,这个进程需要内存指针,而这些内存指针实际上不必引用实际的物理 RAM,这就是使用虚拟内存的原因。Linux 内核使用虚拟内存来允许程序进行内存预留。进行此保留后,其他应用程序无法保留相同的内存。进行预留是设置指针的问题,仅此而已。这并不意味着内存预留实际上也会被使用。
当程序必须使用它保留的内存时,它将发出系统调用,即内存实际上将被分配。

三、什么是内存过度分配和 OOM?

top命令中,可以看到 virt 列和 res 列。

  • VIRT用于虚拟内存。这是进程当前分配的千字节数。
  • RES是Resident,这就是真正使用的内存。
top - 15:03:09 up  4:49,  1 user,  load average: 0.00, 0.00, 0.00
Tasks:  33 total,   1 running,  32 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.4 sy,  0.0 ni, 99.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   6858.4 total,   5317.5 free,    530.4 used,   1010.5 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   6082.8 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
      1 root      20   0   19512  12496   8292 S   0.0   0.2   0:01.09 systemd
      2 root      20   0    2280   1300   1188 S   0.0   0.0   0:00.00 init-systemd(Ub
      7 root      20   0    2280      4      0 S   0.0   0.0   0:00.00 init
     36 root      19  -1   47736  15292  14280 S   0.0   0.2   0:00.15 systemd-journal
     56 root      20   0   21964   5916   4552 S   0.0   0.1   0:00.16 systemd-udevd
     74 root      20   0    4492    172     20 S   0.0   0.0   0:00.00 snapfuse
     75 root      20   0    4624    176     24 S   0.0   0.0   0:00.00 snapfuse
     76 root      20   0    4848   1808   1340 S   0.0   0.0   0:00.76 snapfuse
     77 root      20   0    4728   1744   1240 S   0.0   0.0   0:01.96 snapfuse
     80 root      20   0    4972   1748   1240 S   0.0   0.0   0:01.19 snapfuse
     87 systemd+  20   0   25532  12636   8440 S   0.0   0.2   0:00.16 systemd-resolve
     99 root      20   0    4304   2648   2404 S   0.0   0.0   0:00.02 cron
    103 message+  20   0    8584   4652   4120 S   0.0   0.1   0:00.28 dbus-daemon
    130 root      20   0   30104  19232  10436 S   0.0   0.3   0:00.10 networkd-dispat
    135 syslog    20   0  222400   5140   4312 S   0.0   0.1   0:00.02 rsyslogd
    136 root      20   0 1984488  44964  18804 S   0.0   0.6   0:06.75 snapd
    137 root      20   0   15324   7344   6400 S   0.0   0.1   0:00.19 systemd-logind
    194 root      20   0    4780   3372   3132 S   0.0   0.0   0:00.08 subiquity-serve
    201 root      20   0    3236   1064    980 S   0.0   0.0   0:00.00 agetty
    213 root      20   0    3192   1088   1004 S   0.0   0.0   0:00.00 agetty
    214 root      20   0  107216  21880  13256 S   0.0   0.3   0:00.05 unattended-upgr
    310 root      20   0  721880  80592  21184 S   0.0   1.1   0:16.32 python3.10
    343 root      20   0    2288    112      0 S   0.0   0.0   0:00.00 SessionLeader
    344 root      20   0    2304    116      0 S   0.0   0.0   0:00.00 Relay(345)
    345 fly       20   0    6216   5124   3376 S   0.0   0.1   0:00.04 bash
    346 root      20   0    7524   4916   3992 S   0.0   0.1   0:00.00 login
    403 fly       20   0   16928   9336   7860 S   0.0   0.1   0:00.04 systemd
    404 fly       20   0  168756   3376     16 S   0.0   0.0   0:00.00 (sd-pam)
    409 fly       20   0    6120   4900   3316 S   0.0   0.1   0:00.01 bash
    421 root      20   0   44208  37496  10048 S   0.0   0.5   0:24.72 python3
    609 root      20   0  293000  20340  17432 S   0.0   0.3   0:00.18 packagekitd
    613 root      20   0  234492   6680   6048 S   0.0   0.1   0:00.06 polkitd
   4847 fly       20   0    7788   3648   3044 R   0.0   0.1   0:00.00 top

可以看到,Linux系统为进程分配了大量的内存,如果将所有这些VIRT内存相加,会发现远远超过该系统中可用的物理 RAM 的总和。这就是所说的内存过度分配。

若要调整过度分配内存的行为,可以在/proc/sys/vm/overcommit_memory写入参数。此参数的值:

  • 默认值为 0,这意味着内核在授予内存之前会检查它是否仍有可用内存。
  • 1 表示系统认为在所有情况下都有足够的内存。这有利于执行内存密集型任务,但可能会导致进程自动终止。
  • 2 表示如果没有足够的可用内存,内核的内存请求将失败。

当任何应用程序或进程请求内存时,内核将始终接受该请求并“给予”。请注意,内核提供了一定数量的内存,但该内存未标记为“已使用”。相反,这被视为“虚拟内存”,只有当应用程序或进程尝试将某些数据写入内存时,内核才会将内存的该部分标记为“已使用”。

因此,如果突然一个或某个进程开始使用更多的驻留内存 (RES),内核需要满足该请求,结果是可能会遇到没有更多可用内存的情况。当系统内存不足时,这种情况被称为OOM(内存不足)情况,并且它正在耗尽内存。在这种情况下,kill命令将处于活动状态,它将杀死最不需要内存的进程,这是一种随机情况。因此,如果系统内存不足,内核将随机杀死一个进程。

四、了解缓冲区与页面缓存

不用于存储应用程序数据的 RAM 可作为缓冲区和页面缓存。因此,基本上,页面缓存和缓冲区是RAM中不用于其他任何用途的任何内容。

当系统想要对写入硬盘上的数据执行任何操作时,它首先需要从磁盘读取数据并将其存储在RAM中。为这些数据分配的内存称为 pagecache。缓存是内存的一部分,它透明地存储数据,以便可以更快地处理未来对该数据的请求。内核利用此内存来缓存磁盘数据并提高 I/O 性能。

当请求任何类型的文件/数据时,内核将查找用户正在处理的文件部分的副本,如果不存在此类副本,它将分配一页新的缓存内存,并用从磁盘读出的适当内容填充它。缓存被视为系统内存,它们被报告为可释放的,因为它们可以根据系统的工作负载需求轻松收缩或回收。

缓冲区是存储在页面缓存下的数据的磁盘块表示形式。缓冲区包含驻留在页面缓存下的文件/数据的元数据。

工作过程:
当对页面缓存中存在的任何数据进行请求时,内核首先检查包含元数据的缓冲区中的数据,这些元数据指向页面缓存中包含的实际文件/数据。一旦从元数据中知道了文件的实际块地址,它就会被内核拾取进行处理。

五、内核如何执行磁盘读写操作?

从磁盘读取:

  • 在大多数情况下,内核在读取或写入磁盘时引用页面缓存。
  • 新页面将添加到页面缓存中,以满足用户模式进程的读取请求。
  • 如果该页尚未在缓存中,则会将一个新条目添加到缓存中,并填充从磁盘读取的数据。
  • 如果有足够的可用内存,则该页面将无限期地保留在缓存中,然后可以由其他进程重用,而无需访问磁盘。

写入磁盘:

  • 同样,在将数据页写入块设备之前,内核会验证相应的页面是否已经包含在缓存中;
  • 如果没有,则会将一个新条目添加到缓存中,并填充要写入磁盘的数据。
  • I/O 数据传输不会立即开始:磁盘更新会延迟几秒钟,从而为进程提供进一步修改要写入的数据的机会(换句话说,内核实现延迟写入操作)。

内核读取和写入操作都在主内存上运行。每当执行任何读取或写入操作时,内核首先需要将所需的数据复制到内存中。

读取操作:

  1. 转到磁盘并搜索数据。
  2. 将数据从磁盘写入内存。
  3. 执行读取操作。

写入操作:

  1. 转到磁盘并搜索数据。
  2. 将数据从磁盘写入内存。
  3. 执行写入操作。
  4. 将修改后的数据复制回磁盘。

六、保留页面缓存有什么好处?

接下来将通过简单的例子告诉你为什么我们不应该如此频繁地清除缓冲区和页面缓存?

(1)创建一个包含一些随机文本的小文件:

$ echo "Understanding Linux memory management" > my_file

(2)将缓存的写入同步到持久性存储:

$ sync

(3)清除缓冲区和缓存。警告:这将清除 Linux 系统中所有现有的缓冲区和缓存,因此必须谨慎使用,避免在生产环境中使用,尤其是在繁重的 I/O 操作中。

$ echo 3 > /proc/sys/vm/drop_caches

(4)由于缓存是清除的,可以看到从磁盘读取文件大约需要 0.031 秒。

$ time cat my_file
Understanding Linux memory management

real    0m0.031s
user    0m0.000s
sys     0m0.003s

(5)由于已经读取了一次文件,因此它在页面缓存中可用,如果再次读取同一文件,则是从页面缓存中读取它。

$ time cat my_file
Understanding Linux memory management

real    0m0.001s
user    0m0.000s
sys     0m0.001s

可以看到,读取时间几乎是0.001秒,与之前的结果相比要快得多。

七、了解脏页面

众所周知,内核不断用包含块设备数据的页面填充页面缓存。因此,每当进程修改某些数据时,相应的页面都会被标记为脏;即它的PG_dirty标志被设置了。基于dirty_ratiodirty_ratiodirty_background_ratio参数的脏写回阈值也考虑到了 HugePages。最终阈值以可脏内存(可能分配给页面缓存并被删除的内存)的百分比计算。

脏页面可能会保留在主内存中,直到系统关闭。但是,延迟写入策略有两个主要缺点:

  1. 如果发生硬件或电源故障,则无法再检索RAM的内容,因此自系统启动以来所做的许多文件更新将丢失。
  2. 页面缓存的大小,以及包含它所需的RAM的大小,必须很大,至少与访问的块设备的大小一样大。

因此,在以下情况下,脏页会刷新(写入)到磁盘,也称为脏写回:

  • 页面缓存太满,需要更多页面,或者脏页面数量过大。
  • 由于页面保持脏污,已经过去了太多时间。

八、脏页是如何刷新或写回磁盘的?

sysctl 有几个参数用于控制何时以及如何执行此磁盘写回。

  • dirty_bytes / dirty_ratio
  • dirty_background_bytes / dirty_background_ratio
  • dirty_writeback_centisecs
  • dirty_expire_centisecs

所有这些参数都可以在运行时(无需重新启动)中检查和修改。它们在/proc/sys/vm/目录下。

一般可分为3个阶段。

第一阶段:定期(Periodic)。负责刷新脏数据的内核线程会定期唤醒(基于周期)以刷新数据,这些数据的脏数据至少dirty_expire_centisecs或比dirty_expire_centisecs更长。

第二阶段:后台(Background)。一旦有足够的脏数据超过基于dirty_background_*参数的阈值,内核将尝试尽可能多地刷新或至少刷新到低于后台阈值。注意,这是在内核刷新器线程中异步完成的,虽然会产生一些开销,但它不一定会影响其他应用程序的工作流(因此得名:Background)。

第三阶段:主动(Active)。如果超过dirty_*参数的阈值(通常合理地高于后台阈值),即应用程序生成更多脏数据的速度快于刷新线程设法及时写回的速度。为了防止内存不足,在系统调用write()(和类似调用)中阻止产生脏数据的任务,主动等待数据被刷新。

九、了解TLB(Translation Lookaside Buffers)

访问内存页时,TLB 用于查找虚拟内存中引用的这些页所在的位置。它们可以存在于物理内存中。如果它们已经在物理内存中,则称之为 TLB 命中,并且页面可以非常快速地提供。

如果发生 TLB 未命中,则会发出页面遍历以查找内存页面的位置,然后加载该页面。这也称为次要页面错误。

主要的页面错误是另一回事。只有需要从交换中获取内存页才会发生这种情况。

TLB 包含对所有内存页的管理,因此可以变得相当大。整个 TLB 本身都在 RAM 中,最常用的部分可以存储在 CPU 缓存中,以加快分配正确内存页的过程。

十、了解活动内存和非活动内存

活动内存是真正用于做某事的内存,非活动内存是只是坐在那里而不用于做事的内存。两者区别的本质在于,如果内存不足,Linux 内核可以对非活动内存做一些事情。

可以通过/proc/meminfo方式轻松监控它们两者之间的差异。

$ grep -i active /proc/meminfo
Active:           222672 kB
Inactive:         968684 kB
Active(anon):       2608 kB
Inactive(anon):   229140 kB
Active(file):     220064 kB
Inactive(file):   739544 kB

在这里总共有 222672 kB 活动内存,968684 kB 非活动内存。同时可以看到Active(anon)Active(file)内存之间有所区别。

  • Anon (anonymous) 内存是指由程序分配的内存。
  • 匿名文件内存是指用作缓存或缓冲区的内存。
  • 在任何 Linux 系统上,这两种类型的内存都可以标记为“活动”或“非活动”。非活动文件内存通常存在于不需要 RAM 用于其他任何事情的服务器上。如果出现内存压力,内核可以立即清除此内存以提供更多可用 RAM。
  • Inactive Anon内存是必须分配的内存。但是,由于它尚未被主动使用,因此可以将其移动到较慢的内存中。这正是swap 的用途。
  • 如果在swap中只有非活动的匿名内存,则swap有助于优化系统的内存性能。
  • 通过移出这些非活动内存页,可以有更多的内存可用于缓存,这对服务器的整体性能有好处。

不同类型的swapping场景和风险:

  • 如果使用了交换空间,应该查看/proc/meminfo文件,以将交换的使用与非活动匿名内存页的数量相关联。
  • 如果使用的交换量大于在/proc/meminfo中观察到的匿名内存页数,则表示正在交换活动内存。这对性能来说是个坏消息,如果发生这种情况,必须安装更多的RAM。
  • 如果正在使用的交换量小于/proc/meminfo中的非活动匿名内存页量,则没有问题。
  • 但是,如果交换的内存多于非活动匿名页面的数量,则可能有麻烦,因为正在交换活动内存。这意味着 I/O 流量过多,这会降低系统速度。

总结

了解了 Linux 内核用于增强和优化系统性能的内存的不同领域。这不是 Linux 内存管理的完整指南,但这将帮助初学者了解 Linux 内存的基础知识。
在这里插入图片描述

  • 10
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值