<audio id="audio" title="22 | 答疑（三）：文件系统与磁盘的区别是什么？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e1/2f/e1dac76322b09d7b35d04cd0e676aa2f.mp3"></audio>

你好，我是倪朋飞。

专栏更新至今，四大基础模块的第二个模块——内存性能篇，我们就已经学完了。很开心你还没有掉队，仍然在积极学习和实践操作，并且热情地留言与讨论。

这些留言中，我非常高兴看到，很多同学用学过的案例思路，解决了实际工作中的性能问题。我也非常感谢 espzest、大甜菜、Smile 等积极思考的同学，指出了文章中某些不当或者不严谨的地方。另外，还有我来也、JohnT3e、白华等同学，积极在留言区讨论学习和实践中的问题，也分享了宝贵的经验，在这里也非常感谢你们。

今天是性能优化的第三期。照例，我从内存模块的留言中摘出了一些典型问题，作为今天的答疑内容，集中回复。为了便于你学习理解，它们并不是严格按照文章顺序排列的。

每个问题，我都附上了留言区提问的截屏。如果你需要回顾内容原文，可以扫描每个问题右下方的二维码查看。

## 问题1：内存回收与OOM

虎虎的这个问题，实际上包括四个子问题，即，

<li>
怎么理解 LRU 内存回收？
</li>
<li>
回收后的内存又到哪里去了？
</li>
<li>
OOM 是按照虚拟内存还是实际内存来打分？
</li>
<li>
怎么估计应用程序的最小内存？
</li>

<img src="https://static001.geekbang.org/resource/image/90/36/905b15ee0df924038befe0e61ce81436.png" alt="">

其实在 Linux [内存的原理篇](https://time.geekbang.org/column/article/74272)和 [Swap 原理篇](https://time.geekbang.org/column/article/75797)中我曾经讲到，一旦发现内存紧张，系统会通过三种方式回收内存。我们来复习一下，这三种方式分别是 ：

<li>
基于 LRU（Least Recently Used）算法，回收缓存；
</li>
<li>
基于 Swap 机制，回收不常访问的匿名页；
</li>
<li>
基于 OOM（Out of Memory）机制，杀掉占用大量内存的进程。
</li>

前两种方式，缓存回收和 Swap 回收，实际上都是基于 LRU 算法，也就是优先回收不常访问的内存。LRU 回收算法，实际上维护着 active 和 inactive 两个双向链表，其中：

<li>
active 记录活跃的内存页；
</li>
<li>
inactive 记录非活跃的内存页。
</li>

越接近链表尾部，就表示内存页越不常访问。这样，在回收内存时，系统就可以根据活跃程度，优先回收不活跃的内存。

活跃和非活跃的内存页，按照类型的不同，又分别分为文件页和匿名页，对应着缓存回收和 Swap 回收。

当然，你可以从 /proc/meminfo 中，查询它们的大小，比如：

```
# grep表示只保留包含active的指标（忽略大小写）
# sort表示按照字母顺序排序
$ cat /proc/meminfo | grep -i active | sort
Active(anon):     167976 kB
Active(file):     971488 kB
Active:          1139464 kB
Inactive(anon):      720 kB
Inactive(file):  2109536 kB
Inactive:        2110256 kB

```

第三种方式，OOM 机制按照 oom_score 给进程排序。oom_score 越大，进程就越容易被系统杀死。

当系统发现内存不足以分配新的内存请求时，就会尝试[直接内存回收](https://time.geekbang.org/column/article/75797)。这种情况下，如果回收完文件页和匿名页后，内存够用了，当然皆大欢喜，把回收回来的内存分配给进程就可以了。但如果内存还是不足，OOM就要登场了。

OOM 发生时，你可以在 dmesg 中看到 Out of memory 的信息，从而知道是哪些进程被 OOM 杀死了。比如，你可以执行下面的命令，查询 OOM 日志：

```
$ dmesg | grep -i &quot;Out of memory&quot;
Out of memory: Kill process 9329 (java) score 321 or sacrifice child

```

当然了，如果你不希望应用程序被 OOM 杀死，可以调整进程的 oom_score_adj，减小 OOM 分值，进而降低被杀死的概率。或者，你还可以开启内存的 overcommit，允许进程申请超过物理内存的虚拟内存（这儿实际上假设的是，进程不会用光申请到的虚拟内存）。

这三种方式，我们就复习完了。接下来，我们回到开始的四个问题，相信你自己已经有了答案。

<li>
LRU 算法的原理刚才已经提到了，这里不再重复。
</li>
<li>
内存回收后，会被重新放到未使用内存中。这样，新的进程就可以请求、使用它们。
</li>
<li>
OOM 触发的时机基于虚拟内存。换句话说，进程在申请内存时，如果申请的虚拟内存加上服务器实际已用的内存之和，比总的物理内存还大，就会触发 OOM。
</li>
<li>
要确定一个进程或者容器的最小内存，最简单的方法就是让它运行起来，再通过 ps 或者 smap ，查看它的内存使用情况。不过要注意，进程刚启动时，可能还没开始处理实际业务，一旦开始处理实际业务，就会占用更多内存。所以，要记得给内存留一定的余量。
</li>

## 问题2: 文件系统与磁盘的区别

文件系统和磁盘的原理，我将在下一个模块中讲解，它们跟内存的关系也十分密切。不过，在学习Buffer 和 Cache 的原理时，我曾提到，Buffer 用于磁盘，而 Cache 用于文件。因此，有不少同学困惑了，比如 JJ 留言中的这两个问题。

<li>
读写文件最终也是读写磁盘，到底要怎么区分，是读写文件还是读写磁盘呢？
</li>
<li>
读写磁盘难道可以不经过文件系统吗？
</li>

<img src="https://static001.geekbang.org/resource/image/6a/b1/6ac5f2e0bf43098a3ba2d14f057eeeb1.png" alt="">

如果你也有相同的疑问，主要还是没搞清楚，磁盘和文件的区别。我在“[怎么理解内存中的Buffer和Cache](https://time.geekbang.org/column/article/74633)”文章的留言区简单回复过，不过担心有同学没有看到，所以在这里重新讲一下。

磁盘是一个存储设备（确切地说是块设备），可以被划分为不同的磁盘分区。而在磁盘或者磁盘分区上，还可以再创建文件系统，并挂载到系统的某个目录中。这样，系统就可以通过这个挂载目录，来读写文件。

换句话说，磁盘是存储数据的块设备，也是文件系统的载体。所以，文件系统确实还是要通过磁盘，来保证数据的持久化存储。

你在很多地方都会看到这句话， Linux 中一切皆文件。换句话说，你可以通过相同的文件接口，来访问磁盘和文件（比如 open、read、write、close 等）。

<li>
我们通常说的“文件”，其实是指普通文件。
</li>
<li>
而磁盘或者分区，则是指块设备文件。
</li>

你可以执行 “ls -l &lt;路径&gt;” 查看它们的区别。如果不懂ls 输出的含义，别忘了man一下就可以。执行 man ls 命令，以及 info ‘(coreutils) ls invocation’ 命令，就可以查到了。

在读写普通文件时，I/O 请求会首先经过文件系统，然后由文件系统负责，来与磁盘进行交互。而在读写块设备文件时，会跳过文件系统，直接与磁盘交互，也就是所谓的“裸I/O”。

这两种读写方式使用的缓存自然不同。文件系统管理的缓存，其实就是 Cache 的一部分。而裸磁盘的缓存，用的正是Buffer。

更多关于文件系统、磁盘以及 I/O 的原理，你先不要着急，往后我们都会讲到。

## 问题3: 如何统计所有进程的物理内存使用量

这其实是 [怎么理解内存中的Buffer和Cache](https://time.geekbang.org/column/article/74633) 的课后思考题，无名老卒、Griffin、JohnT3e 等少数几个同学，都给出了一些思路。

比如，无名老卒同学的方法，是把所有进程的 RSS 全部累加：

<img src="https://static001.geekbang.org/resource/image/ba/64/baa48809addf1f7b4d7c280f4ce03764.png" alt="">

这种方法，实际上导致不少地方会被重复计算。RSS 表示常驻内存，把进程用到的共享内存也算了进去。所以，直接累加会导致共享内存被重复计算，不能得到准确的答案。

留言中好几个同学的答案都有类似问题。你可以重新检查一下自己的方法，弄清楚每个指标的定义和原理，防止重复计算。

当然，也有同学的思路非常正确，比如 JohnT3e 提到的，这个问题的关键在于理解 PSS 的含义。

<img src="https://static001.geekbang.org/resource/image/f5/1c/f5c56462ba5c821de1454a9c021e0f1c.png" alt="">

你当然可以通过 stackexchange 上的[链接](https://unix.stackexchange.com/questions/33381/getting-information-about-a-process-memory-usage-from-proc-pid-smaps)找到答案，不过，我还是更推荐，直接查 proc 文件系统的[文档](https://www.kernel.org/doc/Documentation/filesystems/proc.txt)：

> 
The “proportional set size” (PSS) of a process is the count of pages it has in memory, where each page is divided by the number of processes sharing [it. So](http://it.%20So) if a process has 1000 pages all to itself, and 1000 shared with one other process, its PSS will be 1500.


这里我简单解释一下，每个进程的PSS ，是指把共享内存平分到各个进程后，再加上进程本身的非共享内存大小的和。

就像文档中的这个例子，一个进程的非共享内存为  1000 页，它和另一个进程的共享进程也是  1000 页，那么它的PSS=1000/2+1000=1500 页。

这样，你就可以直接累加 PSS ，不用担心共享内存重复计算的问题了。

比如，你可以运行下面的命令来计算：

```
# 使用grep查找Pss指标后，再用awk计算累加值
$ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf &quot;%d kB\n&quot;, total }'
391266 kB

```

## 问题4: CentOS系统中如何安装 bcc-tools

很多同学留言说用的是 CentOS 系统。虽然我在文章中也给出了一个[参考文档](https://github.com/iovisor/bcc/issues/462)，不过 bcc-tools 工具安装起来还是有些困难。

比如白华同学留言表示，网络上的教程不太完整，步骤有些乱：

<img src="https://static001.geekbang.org/resource/image/03/91/036cde548f2455e3d80b6b1c50e33c91.png" alt="">

不过，白华和渡渡鸟_linux同学在探索实践后，留言分享了他们的经验，感谢你们的分享。

<img src="https://static001.geekbang.org/resource/image/8b/17/8b80a335c3fa543226f42dcb2c506017.png" alt=""><img src="https://static001.geekbang.org/resource/image/f3/0d/f34b80fc9f7eefc928959bfb41ce590d.png" alt="">

在这里，我也统一回复一下，在 CentOS 中安装 bcc-tools 的步骤。以 CentOS 7 为例，整个安装主要可以分两步。

第一步，升级内核。你可以运行下面的命令来操作：

```
# 升级系统
yum update -y

# 安装ELRepo
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm

# 安装新内核
yum remove -y kernel-headers kernel-tools kernel-tools-libs
yum --enablerepo=&quot;elrepo-kernel&quot; install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel

# 更新Grub后重启
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default 0
reboot

# 重启后确认内核版本已升级为4.20.0-1.el7.elrepo.x86_64
uname -r

```

第二步，安装 bcc-tools：

```
# 安装bcc-tools
yum install -y bcc-tools

# 配置PATH路径
export PATH=$PATH:/usr/share/bcc/tools

# 验证安装成功
cachestat 

```

## 问题5: 内存泄漏案例的优化方法

这是我在 [内存泄漏了，我该如何定位和处理](https://time.geekbang.org/column/article/75670) 中留的一个思考题。这个问题是这样的：

在内存泄漏案例的最后，我们通过增加 free() 调用，释放了函数 fibonacci() 分配的内存，修复了内存泄漏的问题。就这个案例而言，还有没有其他更好的修复方法呢？

很多同学留言写下了自己的想法，都很不错。这里，我重点表扬下郭江伟同学，他给出的方法非常好：

<img src="https://static001.geekbang.org/resource/image/75/e4/757c532b561d142306c435a57277cae4.png" alt="">

他的思路是不用动态内存分配的方法，而是用数组来暂存计算结果。这样就可以由系统自动管理这些栈内存，也不存在内存泄漏的问题了。

这种减少动态内存分配的思路，除了可以解决内存泄漏问题，其实也是常用的内存优化方法。比如，在需要大量内存的场景中，你就可以考虑用栈内存、内存池、HugePage 等方法，来优化内存的分配和管理。

除了这五个问题，还有一点我也想说一下。很多同学在说工具的版本问题，的确，生产环境中的 Linux 版本往往都比较低，导致很多新工具不能在生产环境中直接使用。

不过，这并不代表我们就无能为力了。毕竟，系统的原理都是大同小异的。这其实也是我一直强调的观点。

<li>
在学习时，最好先用最新的系统和工具，它们可以为你提供更简单直观的结果，帮你更好的理解系统的原理。
</li>
<li>
在你掌握了这些原理后，回过头来，再去理解旧版本系统中的工具和原理，你会发现，即便旧版本中的很多工具并不是那么好用，但是原理和指标是类似的，你依然可以轻松掌握它们的使用方法。
</li>

最后，欢迎继续在留言区写下你的疑问，我会持续不断地解答。我的目的不变，希望可以和你一起，把文章的知识变成你的能力，我们不仅仅在实战中演练，也要在交流中进步。


