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 内核使用虚拟内存来允许程序进行内存预留。进行此保留后,其他应用程序无法保留相同的内存。进行预留是设置指针的问题,仅此而已。这并不意味着内存预留实际上也会被使用。
当程序必须使用它保留的内存时,它将发出系统调用,即内存实际上将被分配。
在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)创建一个包含一些随机文本的小文件:
$ 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_ratio 和 dirty_ratiodirty_background_ratio参数的脏写回阈值也考虑到了 HugePages。最终阈值以可脏内存(可能分配给页面缓存并被删除的内存)的百分比计算。
脏页面可能会保留在主内存中,直到系统关闭。但是,延迟写入策略有两个主要缺点:
如果发生硬件或电源故障,则无法再检索RAM的内容,因此自系统启动以来所做的许多文件更新将丢失。
页面缓存的大小,以及包含它所需的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 内存的基础知识。