This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="17 基础篇 | CPU是如何执行任务的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/fc/8cb9696ba6fde7d76b15e3eb0466d7fc.mp3"></audio>
你好,我是邵亚方。
如果你做过性能优化的话,你应该有过这些思考,比如说:
- 如何让CPU读取数据更快一些
- 同样的任务,为什么有时候执行得快,有时候执行得慢?
- 我的任务有些比较重要CPU如果有争抢时我希望可以先执行这些任务这该怎么办呢
- 多线程并行读写数据是如何保障同步的?
-
要想明白这些问题你就需要去了解CPU是如何执行任务的只有明白了CPU的执行逻辑你才能更好地控制你的任务执行从而获得更好的性能。
## CPU是如何读写数据的
我先带你来看下CPU的架构因为你只有理解了CPU的架构你才能更好地理解CPU是如何执行指令的。CPU的架构图如下所示
<img src="https://static001.geekbang.org/resource/image/a4/7f/a418fbfc23d96aeb4813f1db4cbyy17f.jpg" alt="" title="CPU架构">
你可以直观地看到对于现代处理器而言一个实体CPU通常会有两个逻辑线程也就是上图中的Core 0和Core 1。每个Core都有自己的L1 CacheL1 Cache又分为dCache和iCache对应到上图就是L1d和L1i。L1 Cache只有Core本身可以看到其他的Core是看不到的。同一个实体CPU中的这两个Core会共享L2 Cache其他的实体CPU是看不到这个L2 Cache的。所有的实体CPU会共享L3 Cache。这就是典型的CPU架构。
相信你也看到在CPU外还会有内存DRAM、磁盘等这些存储介质共同构成了体系结构里的金字塔存储层次。如下所示
<img src="https://static001.geekbang.org/resource/image/6e/3d/6eace3466bc42185887a351c6c3e693d.jpg" alt="" title="金字塔存储层次">
在这个“金字塔”中越往下存储容量就越大它的速度也会变得越慢。Jeff Dean曾经研究过CPU对各个存储介质的访问延迟具体你可以看下[latency](https://gist.github.com/jboner/2841832)里的数据,里面详细记录了每个存储层次的访问延迟,这也是我们在性能优化时必须要知道的一些延迟数据。你可不要小瞧它,在某些场景下,这些不同存储层次的访问延迟差异可能会导致非常大的性能差异。
我们就以Cache访问延迟L1 0.5nsL2 10ns和内存访问延迟100ns为例我给你举一个实际的案例来说明访问延迟的差异对性能的影响。
之前我在做网络追踪系统时为了更方便地追踪TCP连接我给Linux Kernel提交了一个PATCH来记录每个连接的编号具体你可以参考这个commit[net: init sk_cookie for inet socket](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&amp;id=c6849a3ac17e336811f1d5bba991d2a9bdc47af1)。该PATCH的大致作用是在每次创建一个新的TCP连接时关于TCP这部分知识你可以去温习上一个模块的内容它都会使用net namespace网络命名空间中的cookie_gen生成一个cookie给这个新建的连接赋值。
可是呢在这个PATCH被合入后Google工程师Eric Dumazet发现在他的SYN Flood测试中网络吞吐量会下降约24%。后来经过分析发现这是因为net namespace中所有TCP连接都在共享cookie_gen。在高并发情况下瞬间会有非常多的新建TCP连接这时候cookie_gen就成了一个非常热的数据从而被缓存在Cache中。如果cookie_gen的内容被修改的话Cache里的数据就会失效那么当有其他新建连接需要读取这个数据时就不得不再次从内存中去读取。而你知道内存的延迟相比Cache的延迟是大很多的这就导致了严重的性能下降。这个问题就是典型的False Sharing也就是Cache伪共享问题。
正因为这个PATCH给高并发建连这种场景带来了如此严重的性能损耗所以它就被我们给回退Revert你具体可以看[Revert "net: init sk_cookie for inet socket"](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&amp;id=a06ac0d67d9fda7c255476c6391032319030045d)这个commit。不过cookie_gen对于网络追踪还是很有用的比如说在使用ebpf来追踪cgroup的TCP连接时所以后来Facebook的一个工程师把它从net namespace这个结构体里移了出来[改为了一个全局变量](https://github.com/torvalds/linux/commit/cd48bdda4fb82c2fe569d97af4217c530168c99c)。
由于net namespace被很多TCP连接共享因此这个结构体非常容易产生这类Cache伪共享问题Eric Dumazet也在这里引入过一个Cache伪共享问题[net: reorder struct net fields to avoid false sharing](https://github.com/torvalds/linux/commit/2a06b8982f8f2f40d03a3daf634676386bd84dbc)。
接下来我们就来看一下Cache伪共享问题究竟是怎么回事。
<img src="https://static001.geekbang.org/resource/image/ed/9f/ed552cedfb95d0a3af920eca78c3069f.jpg" alt="" title="Cache Line False Sharing">
如上图所示两个CPU上并行运行着两个不同线程它们同时从内存中读取两个不同的数据这两个数据的地址在物理内存上是连续的它们位于同一个Cache Line中。CPU从内存中读数据到Cache是以Cache Line为单位的所以该Cache Line里的数据被同时读入到了这两个CPU的各自Cache中。紧接着这两个线程分别改写不同的数据每次改写Cache中的数据都会将整个Cache Line置为无效。因此虽然这两个线程改写的数据不同但是由于它们位于同一个Cache Line中所以一个CPU中的线程在写数据时会导致另外一个CPU中的Cache Line失效而另外一个CPU中的线程在读写数据时就会发生cache miss然后去内存读数据这就大大降低了性能。
Cache伪共享问题可以说是性能杀手我们在写代码时一定要留意那些频繁改写的共享数据必要的时候可以将它跟其他的热数据放在不同的Cache Line中避免伪共享问题就像我们在内核代码里经常看到的____cacheline_aligned所做的那样。
那怎么来观测Cache伪共享问题呢你可以使用[perf c2c](https://man7.org/linux/man-pages/man1/perf-c2c.1.html)这个命令但是这需要较新版本内核支持才可以。不过perf同样可以观察cache miss的现象它对很多性能问题的分析还是很有帮助的。
CPU在写完Cache后将Cache置为无效invalidate, 这本质上是为了保障多核并行计算时的数据一致性一致性问题是Cache这个存储层次很典型的问题。
我们再来看内存这个存储层次中的典型问题并行计算时的竞争即两个CPU同时去操作同一个物理内存地址时的竞争。关于这类问题我举一些简单的例子给你说明一下。
以C语言为例
```
struct foo {
int a;
int b;
};
```
在这段示例代码里我们定义了一个结构体该结构体里的两个成员a和b在地址上是连续的。如果CPU 0去写a同时CPU 1去读b的话此时不会有竞争因为a和b是不同的地址。不过a和b由于在地址上是连续的它们可能会位于同一个Cache Line中所以为了防止前面提到的Cache伪共享问题我们可以强制将b的地址设置为Cache Line对齐地址如下:
```
struct foo {
int a;
int b ____cacheline_aligned;
};
```
接下来,我们看下另外一种情况:
```
struct foo {
int a:1;
int b:1;
};
```
这个示例程序定义了两个位域bit fielda和b的地址是一样的只是属于该地址的不同bit。在这种情况下CPU 0去写a a = 1同时CPU 1去写b b = 1就会产生竞争。在总线仲裁后先写的数据就会被后写的数据给覆盖掉。这就是执行RMW操作时典型的竞争问题。在这种场景下就需要同步原语了比如使用atomic操作。
关于位操作我们来看一个实际的案例。这是我前段时间贡献给Linux内核的一个PATCH[psi: Move PF_MEMSTALL out of task-&gt;flags](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&amp;id=1066d1b6974e095d5a6c472ad9180a957b496cd6)它在struct task_struct这个结构体里增加了一个in_memstall的位域在该PATCH里无需考虑多线程并行操作该位域时的竞争问题你知道这是为什么吗我将它作为一个课后思考题留给你欢迎你在留言区与我讨论交流。为了让这个问题简单些我给你一个提示如果你留意过task_struct这个结构体里的位域被改写的情况你会发现只有current当前在运行的线程可以写而其他线程只能去读。但是PF_*这些全局flag可以被其他线程写而不仅仅是current来写。
Linux内核里的task_struct结构体就是用来表示一个线程的每个线程都有唯一对应的task_struct结构体它也是内核进行调度的基本单位。我们继续来看下CPU是如何选择线程来执行的。
## CPU是如何选择线程执行的
你知道一个系统中可能会运行着非常多的线程这些线程数可能远超系统中的CPU核数这时候这些任务就需要排队每个CPU都会维护着自己运行队列runqueue里的线程。这个运行队列的结构大致如下图所示
<img src="https://static001.geekbang.org/resource/image/66/62/6649d7e5984a3b9cd003fcbc97bfde62.jpg" alt="" title="CPU运行队列">
每个CPU都有自己的运行队列runqueue需要运行的线程会被加入到这个队列中。因为有些线程的优先级高Linux内核为了保障这些高优先级任务的执行设置了不同的调度类Scheduling Class如下所示
<img src="https://static001.geekbang.org/resource/image/15/b1/1507d0ef23d5d1cd33769dd1953cffb1.jpg" alt="">
这几个调度类的优先级如下Deadline &gt; Realtime &gt; Fair。Linux内核在选择下一个任务执行时会按照该顺序来进行选择也就是先从dl_rq里选择任务然后从rt_rq里选择任务最后从cfs_rq里选择任务。所以实时任务总是会比普通任务先得到执行。
如果你的某些任务对延迟容忍度很低比如说在嵌入式系统中就有很多这类任务那就可以考虑将你的任务设置为实时任务比如将它设置为SCHED_FIFO的任务
>
$ chrt -f -p 1 1327
如果你不做任何设置的话用户线程在默认情况下都是普通线程也就是属于Fair调度类由CFS调度器来进行管理。CFS调度器的目的是为了实现线程运行的公平性举个例子假设一个CPU上有两个线程需要执行那么每个线程都将分配50%的CPU时间以保障公平性。其实各个线程之间执行时间的比例也是可以人为干预的比如在Linux上可以调整进程的nice值来干预从而让优先级高一些的线程执行更多时间。这就是CFS调度器的大致思想。
好了,我们这堂课就先讲到这里。
## 课堂总结
我来总结一下这节课的知识点:
- 要想明白CPU是如何执行任务的你首先需要去了解CPU的架构
- CPU的存储层次对大型软件系统的性能影响会很明显也是你在性能调优时需要着重考虑的
- 高并发场景下的Cache Line伪共享问题是一个普遍存在的问题你需要留意一下它
- 系统中需要运行的线程数可能大于CPU核数这样就会导致线程排队等待CPU这可能会导致一些延迟。如果你的任务对延迟容忍度低你可以通过一些手段来人为干预Linux默认的调度策略。
## 课后作业
这节课的作业就是我们前面提到的思考题:在[psi: Move PF_MEMSTALL out of task-&gt;flags](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.9-rc4&amp;id=1066d1b6974e095d5a6c472ad9180a957b496cd6)这个PATCH中为什么没有考虑多线程并行操作新增加的位域in_memstall时的竞争问题欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,201 @@
<audio id="audio" title="18 案例篇 | 业务是否需要使用透明大页:水可载舟,亦可覆舟?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/db/bb/dbb6d45c207540a177d925bae52d74bb.mp3"></audio>
你好,我是邵亚方。
我们这节课的案例来自于我在多年以前帮助业务团队分析的一个稳定性问题。当时业务团队反映说他们有一些服务器的CPU利用率会异常飙高然后很快就能恢复并且持续的时间不长大概几秒到几分钟从监控图上可以看到它像一些毛刺。
因为这类问题是普遍存在的所以我就把该问题的定位分析过程分享给你希望你以后遇到CPU利用率飙高的问题时知道该如何一步步地分析。
CPU利用率是一个很笼统的概念在遇到CPU利用率飙高的问题时我们需要看看CPU到底在忙哪类事情比如说CPU是在忙着处理中断、等待I/O、执行内核函数还是在执行用户函数这个时候就需要我们细化CPU利用率的监控因为监控这些细化的指标对我们分析问题很有帮助。
## 细化CPU利用率监控
这里我们以常用的top命令为例来看看CPU更加细化的利用率指标不同版本的top命令显示可能会略有不同
%Cpu(s): 12.5 us, 0.0 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
top命令显示了us、sy、ni、id、wa、hi、si和st这几个指标这几个指标之和为100。那你可能会有疑问细化CPU利用率指标的监控会不会带来明显的额外开销答案是不会的因为CPU利用率监控通常是去解析/proc/stat文件而这些文件中就包含了这些细化的指标。
我们继续来看下上述几个指标的具体含义,这些含义你也可以从[top手册](https://man7.org/linux/man-pages/man1/top.1.html)里来查看:
```
us, user : time running un-niced user processes
sy, system : time running kernel processes
ni, nice : time running niced user processes
id, idle : time spent in the kernel idle handler
wa, IO-wait : time waiting for I/O completion
hi : time spent servicing hardware interrupts
si : time spent servicing software interrupts
st : time stolen from this vm by the hypervisor
```
上述指标的具体含义以及注意事项如下:
<img src="https://static001.geekbang.org/resource/image/37/a5/3756d973a1f7f350bf600c9438f1a4a5.jpg" alt="">
在上面这几项中idle和wait是CPU不工作的时间其余的项都是CPU工作的时间。idle和wait的主要区别是idle是CPU无事可做而wait则是CPU想做事却做不了。你也可以将wait理解为是一类特殊的idle即该CPU上有至少一个线程阻塞在I/O时的idle。
而我们通过对CPU利用率的细化监控发现案例中的CPU利用率飙高是由sys利用率变高导致的也就是说sys利用率会忽然飙高一下比如在usr低于30%的情况下sys会高于15%,持续几秒后又恢复正常。
所以接下来我们就需要抓取sys利用率飙高的现场。
## 抓取sys利用率飙高现场
我们在前面讲到CPU的sys利用率高说明内核函数执行花费了太多的时间所以我们需要采集CPU在sys飙高的瞬间所执行的内核函数。采集内核函数的方法有很多比如
- 通过perf可以采集CPU的热点看看sys利用率高时哪些内核耗时的CPU利用率高
- 通过perf的call-graph功能可以查看具体的调用栈信息也就是线程是从什么路径上执行下来的
- 通过perf的annotate功能可以追踪到线程是在内核函数的哪些语句上比较耗时
- 通过ftrace的function-graph功能可以查看这些内核函数的具体耗时以及在哪个路径上耗时最大。
不过,这些常用的追踪方式在这种瞬间消失的问题上是不太适用的,因为它们更加适合采集一个时间段内的信息。
那么针对这种瞬时的状态我希望有一个系统快照把当前CPU正在做的工作记录下来然后我们就可以结合内核源码分析为什么sys利用率会高了。
有一个工具就可以很好地追踪这种系统瞬时状态即系统快照它就是sysrq。sysrq是我经常用来分析内核问题的工具用它可以观察当前的内存快照、任务快照可以构造vmcore把系统的所有信息都保存下来甚至还可以在内存紧张的时候用它杀掉内存开销最大的那个进程。sysrq可以说是分析很多疑难问题的利器。
要想用sysrq来分析问题首先需要使能sysyrq。我建议你将sysrq的所有功能都使能你无需担心会有什么额外开销而且这也没有什么风险。使能方式如下
>
$ sysctl -w kernel.sysrq = 1
sysrq的功能被使能后你可以使用它的-t选项把当前的任务快照保存下来看看系统中都有哪些任务以及这些任务都在干什么。使用方式如下
>
$ echo t &gt; /proc/sysrq-trigger
然后任务快照就会被打印到内核缓冲区这些任务快照信息你可以通过dmesg命令来查看
>
$ dmesg
当时我为了抓取这种瞬时的状态,写了一个脚本来采集,如下就是一个简单的脚本示例:
```
#!/bin/sh
while [ 1 ]; do
top -bn2 | grep &quot;Cpu(s)&quot; | tail -1 | awk '{
# $2 is usr, $4 is sys.
if ($2 &lt; 30.0 &amp;&amp; $4 &gt; 15.0) {
# save the current usr and sys into a tmp file
while (&quot;date&quot; | getline date) {
split(date, str, &quot; &quot;);
prefix=sprintf(&quot;%s_%s_%s_%s&quot;, str[2],str[3], str[4], str[5]);
}
sys_usr_file=sprintf(&quot;/tmp/%s_info.highsys&quot;, prefix);
print $2 &gt; sys_usr_file;
print $4 &gt;&gt; sys_usr_file;
# run sysrq
system(&quot;echo t &gt; /proc/sysrq-trigger&quot;);
}
}'
sleep 1m
done
```
这个脚本会检测sys利用率高于15%同时usr较低的情况也就是说检测CPU是否在内核里花费了太多时间。如果出现这种情况就会运行sysrq来保存当前任务快照。你可以发现这个脚本设置的是1分钟执行一次之所以这么做是因为不想引起很大的性能开销而且当时业务团队里有几台机器差不多是一天出现两三次这种状况有些机器每次可以持续几分钟所以这已经足够了。不过如果你遇到的问题出现的频率更低持续时间更短那就需要更加精确的方法了。
## 透明大页:水可载舟,亦可覆舟?
我们把脚本部署好后就把问题现场抓取出来了。从dmesg输出的信息中我们发现处于R状态的线程都在进行compcation内存规整线程的调用栈如下所示这是一个比较古老的内核版本为2.6.32
```
java R running task 0 144305 144271 0x00000080
ffff88096393d788 0000000000000086 ffff88096393d7b8 ffffffff81060b13
ffff88096393d738 ffffea003968ce50 000000000000000e ffff880caa713040
ffff8801688b0638 ffff88096393dfd8 000000000000fbc8 ffff8801688b0640
Call Trace:
[&lt;ffffffff81060b13&gt;] ? perf_event_task_sched_out+0x33/0x70
[&lt;ffffffff8100bb8e&gt;] ? apic_timer_interrupt+0xe/0x20
[&lt;ffffffff810686da&gt;] __cond_resched+0x2a/0x40
[&lt;ffffffff81528300&gt;] _cond_resched+0x30/0x40
[&lt;ffffffff81169505&gt;] compact_checklock_irqsave+0x65/0xd0
[&lt;ffffffff81169862&gt;] compaction_alloc+0x202/0x460
[&lt;ffffffff811748d8&gt;] ? buffer_migrate_page+0xe8/0x130
[&lt;ffffffff81174b4a&gt;] migrate_pages+0xaa/0x480
[&lt;ffffffff81169660&gt;] ? compaction_alloc+0x0/0x460
[&lt;ffffffff8116a1a1&gt;] compact_zone+0x581/0x950
[&lt;ffffffff8116a81c&gt;] compact_zone_order+0xac/0x100
[&lt;ffffffff8116a951&gt;] try_to_compact_pages+0xe1/0x120
[&lt;ffffffff8112f1ba&gt;] __alloc_pages_direct_compact+0xda/0x1b0
[&lt;ffffffff8112f80b&gt;] __alloc_pages_nodemask+0x57b/0x8d0
[&lt;ffffffff81167b9a&gt;] alloc_pages_vma+0x9a/0x150
[&lt;ffffffff8118337d&gt;] do_huge_pmd_anonymous_page+0x14d/0x3b0
[&lt;ffffffff8152a116&gt;] ? rwsem_down_read_failed+0x26/0x30
[&lt;ffffffff8114b350&gt;] handle_mm_fault+0x2f0/0x300
[&lt;ffffffff810ae950&gt;] ? wake_futex+0x40/0x60
[&lt;ffffffff8104a8d8&gt;] __do_page_fault+0x138/0x480
[&lt;ffffffff810097cc&gt;] ? __switch_to+0x1ac/0x320
[&lt;ffffffff81527910&gt;] ? thread_return+0x4e/0x76e
[&lt;ffffffff8152d45e&gt;] do_page_fault+0x3e/0xa0
[&lt;ffffffff8152a815&gt;] page_fault+0x25/0x30
```
从该调用栈我们可以看出此时这个java线程在申请THPdo_huge_pmd_anonymous_page。THP就是透明大页它是一个2M的连续物理内存。但是因为这个时候物理内存中已经没有连续2M的内存空间了所以触发了direct compaction直接内存规整内存规整的过程可以用下图来表示
<img src="https://static001.geekbang.org/resource/image/db/51/db981eb703a88ae85458618355789251.jpg" alt="" title="THP Compaction">
这个过程并不复杂在进行compcation时线程会从前往后扫描已使用的movable page然后从后往前扫描free page扫描结束后会把这些movable page给迁移到free page里最终规整出一个2M的连续物理内存这样THP就可以成功申请内存了。
direct compaction这个过程是很耗时的而且在2.6.32版本的内核上该过程需要持有粗粒度的锁所以在运行过程中线程还可能会主动检查_cond_resched是否有其他更高优先级的任务需要执行。如果有的话就会让其他线程先执行这便进一步加剧了它的执行耗时。这也就是sys利用率飙高的原因。关于这些你也都可以从内核源码的注释来看到
```
/*
* Compaction requires the taking of some coarse locks that are potentially
* very heavily contended. Check if the process needs to be scheduled or
* if the lock is contended. For async compaction, back out in the event
* if contention is severe. For sync compaction, schedule.
* ...
*/
```
在我们找到了原因之后为了快速解决生产环境上的这些问题我们就把该业务服务器上的THP关掉了关闭后系统变得很稳定再也没有出现过sys利用率飙高的问题。关闭THP可以使用下面这个命令
>
$ echo never &gt; /sys/kernel/mm/transparent_hugepage/enabled
关闭了生产环境上的THP后我们又在线下测试环境中评估了THP对该业务的性能影响我们发现THP并不能给该业务带来明显的性能提升即使是在内存不紧张、不会触发内存规整的情况下。这也引起了我的思考**THP究竟适合什么样的业务呢**
这就要从THP的目的来说起了。我们长话短说THP的目的是用一个页表项来映射更大的内存大页这样可以减少Page Fault因为需要的页数少了。当然这也会提升TLB命中率因为需要的页表项也少了。如果进程要访问的数据都在这个大页中那么这个大页就会很热会被缓存在Cache中。而大页对应的页表项也会出现在TLB中从上一讲的存储层次我们可以知道这有助于性能提升。但是反过来假设应用程序的数据局部性比较差它在短时间内要访问的数据很随机地位于不同的大页上那么大页的优势就会消失。
因此我们基于大页给业务做性能优化的时候首先要评估业务的数据局部性尽量把业务的热点数据聚合在一起以便于充分享受大页的优势。以我在华为任职期间所做的大页性能优化为例我们将业务的热点数据聚合在一起然后将这些热点数据分配到大页上再与不使用大页的情况相比最终发现这可以带来20%以上的性能提升。对于TLB较小的架构比如MIPS这种架构它可以带来50%以上的性能提升。当然了,我们在这个过程中也对内核的大页代码做了很多优化,这里就不展开说了。
针对THP的使用我在这里给你几点建议
- 不要将/sys/kernel/mm/transparent_hugepage/enabled配置为always你可以将它配置为madvise。如果你不清楚该如何来配置那就将它配置为never
- 如果你想要用THP优化业务最好可以让业务以madvise的方式来使用大页即通过修改业务代码来指定特定数据使用THP因为业务更熟悉自己的数据流
- 很多时候修改业务代码会很麻烦如果你不想修改业务代码的话那就去优化THP的内核代码吧。
好了,这节课就讲到这里。
## 课堂总结
我们来回顾一下这节课的要点:
- 细化CPU利用率监控在CPU利用率高时你需要查看具体是哪一项指标比较高
- sysrq是分析内核态CPU利用率高的利器也是分析很多内核疑难问题的利器你需要去了解如何使用它
- THP可以给业务带来性能提升但也可能会给业务带来严重的稳定性问题你最好以madvise的方式使用它。如果你不清楚如何使用它那就把它关闭。
## 课后作业
我们这节课的作业有三种,你可以根据自己的情况进行选择:
- 如果你是应用开发者请问如何来观察系统中分配了多少THP
- 如果你是初级内核开发者请问在进行compaction时哪些页可以被迁移哪些不可以被迁移
- 如果你是高级内核开发者假设现在让你来设计让程序的代码段也可以使用hugetlbfs那你觉得应该要做什么
欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="19 案例篇 | 网络吞吐高的业务是否需要开启网卡特性呢?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/6f/4bd85ab1bf99d82a0c98b3a1879efd6f.mp3"></audio>
你好,我是邵亚方。
通过上一讲我们对CPU利用率的细化相信你已经知道对于应用而言它的目标是让CPU的开销尽量用在执行用户代码上而非其他方面。usr利用率越高说明CPU的效率越高。如果usr低就说明CPU执行应用的效率不高。在[第18讲](https://time.geekbang.org/column/article/292060)里我们还讲了CPU时间浪费在sys里的案例。那今天这一讲我们一起来看看CPU在softirq上花费过多时间所引起的业务性能下降问题这也是我们在生产环境中经常遇到的一类问题。接下来我会为你讲解相关案例以及这类问题常用的观察方法。
## 中断与业务进程之间是如何相互干扰的?
这是我多年以前遇到的一个案例当时业务反馈说为了提升QPSQuery per Second他们开启了RPSReceivce Packet Steering来模拟网卡多队列没想到开启RPS反而导致了QPS明显下降不知道是什么原因。
其实这类特定行为改变引起的性能下降问题相对好分析一些。最简单的方式就是去对比这个行为前后的性能数据。即使你不清楚RPS是什么也不知道它背后的机制你也可以采集需要的性能指标进行对比分析然后判断问题可能出在哪里。这些性能指标包括CPU指标内存指标I/O指标网络指标等我们可以使用dstat来观察它们的变化。
在业务打开RPS之前的性能指标
```
$ dstat
You did not select any stats, using -cdngy by default.
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
64 23 6 0 0 7| 0 8192B|7917k 12M| 0 0 | 27k 1922
64 22 6 0 0 8| 0 0 |7739k 12M| 0 0 | 26k 2210
61 23 9 0 0 7| 0 0 |7397k 11M| 0 0 | 25k 2267
```
打开了RPS之后的性能指标
```
$ dstat
You did not select any stats, using -cdngy by default.
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
62 23 4 0 0 12| 0 0 |7096k 11M| 0 0 | 49k 2261
74 13 4 0 0 9| 0 0 |4003k 6543k| 0 0 | 31k 2004
59 22 5 0 0 13| 0 4096B|6710k 10M| 0 0 | 48k 2220
```
我们可以看到打开RPS后CPU的利用率有所升高。其中siq即软中断利用率明显增加int即硬中断频率也明显升高而net这一项里的网络吞吐数据则有所下降。也就是说在网络吞吐不升反降的情况下系统的硬中断和软中断都明显增加。由此我们可以推断出网络吞吐的下降应该是中断增加导致的结果。
那么,接下来我们就需要分析到底是什么类型的软中断和硬中断增加了,以便于找到问题的源头。
系统中存在很多硬中断,这些硬中断及其发生频率我们都可以通过/proc/interruptes这个文件来查看
```
$ cat /proc/interrupts
```
如果你想要了解某个中断的详细情况,比如中断亲和性,那你可以通过/proc/irq/[irq_num]来查看,比如:
```
$ cat /proc/irq/123/smp_affinity
```
软中断可以通过/proc/softirq来查看
```
$ cat /proc/softirqs
```
当然了,你也可以写一些脚本来观察各个硬中断和软中断的发生频率,从而更加直观地查看是哪些中断发生得太频繁。
关于硬中断和软中断的区别,你可能多少有些了解,软中断是用来处理硬中断在短时间内无法完成的任务的。硬中断由于执行时间短,所以如果它的发生频率不高的话,一般不会给业务带来明显影响。但是由于内核里关中断的地方太多,所以进程往往会给硬中断带来一些影响,比如进程关中断时间太长会导致网络报文无法及时处理,进而引起业务性能抖动。
我们在生产环境中就遇到过这种关中断时间太长引起的抖动案例比如cat /proc/slabinfo这个操作里的逻辑关中断太长它会致使业务RT抖动。这是因为该命令会统计系统中所有的slab数量并显示出来在统计的过程中会关中断。如果系统中的slab数量太多就会导致关中断的时间太长进而引起网络包阻塞ping延迟也会因此明显变大。所以在生产环境中我们要尽量避免去采集/proc/slabinfo否则可能会引起业务抖动。
由于/proc/slabinfo很危险所以它的访问权限也从2.6.32版本时的0644[更改为了后来](https://lwn.net/Articles/431818/)[](https://lwn.net/Articles/431818/)[0400](https://lwn.net/Articles/431818/)也就是说只有root用户才能够读取它这在一定程度上避免了一些问题。如果你的系统是2.6.32版本的内核,你就需要特别注意该问题。
如果你要分析因中断关闭时间太长而引发的问题有一种最简单的方式就是使用ftrace的irqsoff功能。它不仅可以统计出中断被关闭了多长时间还可以统计出为什么会关闭中断。不过你需要注意的是irqsoff功能依赖于CONFIG_IRQSOFF_TRACER这个配置项如果你的内核没有打开该配置项那你就需要使用其他方式来去追踪了。
如何使用irqsoff呢首先你需要去查看你的系统是否支持了irqsoff这个tracer
```
$ cat /sys/kernel/debug/tracing/available_tracers
```
如果显示的内容包含了irqsoff说明系统支持该功能你就可以打开它进行追踪了
```
$ echo irqsoff &gt; /sys/kernel/debug/tracing/current_tracer
```
接下来,你就可以通过/sys/kernel/debug/tracing/trace_pipe和trace这两个文件来观察系统中的irqsoff事件了。
我们知道相比硬中断软中断的执行时间会长一些而且它也会抢占正在执行进程的CPU从而导致进程在它运行期间只能等待。所以相对而言它会更容易给业务带来延迟。那我们怎么对软中断的执行频率以及执行耗时进行观测呢你可以通过如下两个tracepoints来进行观测
```
/sys/kernel/debug/tracing/events/irq/softirq_entry
/sys/kernel/debug/tracing/events/irq/softirq_exit
```
这两个tracepoint分别表示软中断的进入和退出退出时间减去进入时间就是该软中断这一次的耗时。关于tracepoint采集数据的分析方式我们在之前的课程里已经讲过多次所以就不在这里继续描述了。
如果你的内核版本比较新支持eBPF功能那你同样可以使用bcc里的[softirqs.py](https://github.com/iovisor/bcc/blob/master/tools/softirqs.py)这个工具来进行观测。它会统计软中断的次数和耗时,这对我们分析软中断引起的业务延迟来说,是比较方便的。
为了避免软中断太过频繁进程无法得到CPU而被饿死的情况内核引入了ksoftirqd这个机制。如果所有的软中断在短时间内无法被处理完内核就会唤醒ksoftirqd处理接下来的软中断。ksoftirqd与普通进程的优先级一样也就是说它会和普通进程公平地使用CPU这在一定程度上可以避免用户进程被饿死的情况特别是对于那些更高优先级的实时用户进程而言。
不过这也会带来一些问题。如果ksoftrirqd长时间得不到CPU就会致使软中断的延迟变得很大它引起的典型问题也是ping延迟。如果ping包无法在软中断里得到处理就会被ksoftirqd处理。而ksoftirqd的实时性是很难得到保障的可能需要等其他线程执行完ksoftirqd才能得到执行这就会导致ping延迟变得很大。
要观测ksoftirqd延迟的问题你可以使用bcc里的[runqlat.py](https://github.com/iovisor/bcc/blob/master/tools/runqlat.py)。这里我们以网卡中断为例它唤醒ksoftirqd的逻辑大致如下图所示
<img src="https://static001.geekbang.org/resource/image/8b/01/8b3f5bfa5571dcc855c4f6cd7dc4ce01.jpg" alt="" title="中断与ksoftirqd">
我们具体来看看这个过程软中断被唤醒后会检查一遍软中断向量表逐个处理这些软中断处理完一遍后它会再次检查如果又有新的软中断要处理就会唤醒ksoftrqd来处理。ksoftirqd是per-cpu的内核线程每个CPU都有一个。对于CPU1而言它运行的是ksoftirqd/1这个线程。ksoftirqd/1被唤醒后会检查软中断向量表并进行处理。如果你使用ps来查看ksoftirqd/1的优先级会发现它其实就是一个普通线程对应的Nice值为0
```
$ ps -eo &quot;pid,comm,ni&quot; | grep softirqd
9 ksoftirqd/0 0
16 ksoftirqd/1 0
21 ksoftirqd/2 0
26 ksoftirqd/3 0
```
总之,在软中断处理这部分,内核需要改进的地方还有很多。
## softirq是如何影响业务的
在我们对硬中断和软中断进行观察后发现使能RPS后增加了很多CALFunction Call Interrupts硬中断。CAL是通过软件触发硬中断的一种方式可以指定CPU以及需要执行的中断处理程序。它也常被用来进行CPU间通信IPI当一个CPU需要其他CPU来执行特定中断处理程序时就可以通过CAL中断来进行。
如果你对RPS的机制有所了解的话应该清楚RPS就是通过CAL这种方式来让其他CPU去接收网络包的。为了验证这一点我们可以通过mpstat这个命令来观察各个CPU利用率情况。
使能RPS之前的CPU利用率如下所示
```
$ mpstat -P ALL 1
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
Average: all 70.18 0.00 19.28 0.00 0.00 5.86 0.00 0.00 0.00 4.68
Average: 0 73.25 0.00 21.50 0.00 0.00 0.00 0.00 0.00 0.00 5.25
Average: 1 58.85 0.00 14.46 0.00 0.00 23.44 0.00 0.00 0.00 3.24
Average: 2 74.50 0.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 5.50
Average: 3 74.25 0.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 4.75
```
使能RPS之后各个CPU的利用率情况为
```
$ mpstat -P ALL 1
Average: CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
Average: all 66.21 0.00 17.73 0.00 0.00 11.15 0.00 0.00 0.00 4.91
Average: 0 68.17 0.00 18.33 0.00 0.00 7.67 0.00 0.00 0.00 5.83
Average: 1 60.57 0.00 15.81 0.00 0.00 20.80 0.00 0.00 0.00 2.83
Average: 2 69.95 0.00 19.20 0.00 0.00 7.01 0.00 0.00 0.00 3.84
Average: 3 66.39 0.00 17.64 0.00 0.00 8.99 0.00 0.00 0.00 6.99
```
我们可以看到使能RPS之后softirq在各个CPU之间更加均衡了一些本来只有CPU1在处理softirq使能后每个CPU都会处理softirq并且CPU1的softirq利用率降低了一些。这就是RPS的作用让网络收包软中断处理在各个CPU间更加均衡以防止其在某个CPU上达到瓶颈。你可以看到使能RPS后整体的%soft比原来高了很多。
理论上处理网络收包软中断的CPU变多那么在单位时间内这些CPU应该可以处理更多的网络包从而提升系统整体的吞吐。可是在我们的案例中为什么会引起业务的QPS不升反降呢
其实答案同样可以从CPU利用率中得出。我们可以看到在使能RPS之前CPU利用率已经很高了达到了90%以上也就是说CPU已经在超负荷工作了。而打开RPSRPS又会消耗额外的CPU时间来模拟网卡多队列特性这就会导致CPU更加超负荷地工作从而进一步挤压用户进程的处理时间。因此我们会发现在打开RPS后%usr的利用率下降了一些。
我们知道%usr是衡量用户进程执行时间的一个指标%usr越高意味着业务代码的运行时间越多。如果%usr下降那就意味着业务代码的运行时间变少了在业务没有进行代码优化的前提下这显然是一个危险的信号。
由此我们可以发现RPS的本质就是把网卡特性网卡多队列给upload到CPU通过牺牲CPU时间来提升网络吞吐。如果你的系统已经很繁忙了那么再使用该特性无疑是雪上加霜。所以你需要注意使用RPS的前提条件是系统的整体CPU利用率不能太高。
找到问题后我们就把该系统的RPS特性关闭了。如果你的网卡比较新它可能会支持硬件多队列。硬件多队列是在网卡里做负载均衡在这种场景下硬件多队列会很有帮助。我们知道与upload相反的方向是offload就是把CPU的工作给offload到网卡上去处理这样可以把CPU解放出来让它有更多的时间执行用户代码。关于网卡的offload特性我们就不在此讨论了。
好了,这节课就讲到这里。
## 课堂总结
我们来简单回顾一下这节课的重点:
- 硬中断、软中断以及ksoftirqd这个内核线程它们与用户线程之间的关系是相对容易引发业务抖动的地方你需要掌握它们的观测方式
- 硬中断对业务的主要影响体现在硬中断的发生频率上,但是它也容易受线程影响,因为内核里关中断的地方有很多;
- 软中断的执行时间如果太长,就会给用户线程带来延迟,你需要避免你的系统中存在耗时较大的软中断处理程序。如果有的话,你需要去做优化;
- ksoftirqd的优先级与用户线程是一致的因此如果软中断处理函数是在ksoftirqd里执行的那它可能会有一些延迟
- RPS的本质是网卡特性unload到CPU靠牺牲CPU时间来提升吞吐你需要结合你的业务场景来评估是否需要开启它。如果你的网卡支持了硬件多队列那么就可以直接使用硬件多队列了。
## 课后作业
我们这节课的作业有两种,你可以根据自己的情况进行选择。
- 入门:
请问如果软中断以及硬中断被关闭的时间太长,会发生什么事?
- 高级:
如果想要追踪网络数据包在内核缓冲区停留了多长时间才被应用读走,你觉得应该如何来追踪?
欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。

View File

@@ -0,0 +1,157 @@
<audio id="audio" title="20 分析篇 | 如何分析CPU利用率飙高问题 " controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/60/1966ed0049810c56aa6cd161453ae260.mp3"></audio>
你好,我是邵亚方。
如果你是一名应用开发者那你应该知道如何去分析应用逻辑对于如何优化应用代码提升系统性能也应该有自己的一套经验。而我们这节课想要讨论的是如何拓展你的边界让你能够分析代码之外的模块以及对你而言几乎是黑盒的Linux内核。
在很多情况下,应用的性能问题都需要通过分析内核行为来解决,因此,内核提供了非常多的指标供应用程序参考。当应用出现问题时,我们可以查看到底是哪些指标出现了异常,然后再做进一步分析。不过,这些内核导出的指标并不能覆盖所有的场景,我们面临的问题可能更加棘手:应用出现性能问题,可是系统中所有的指标都看起来没有异常。相信很多人都为此抓狂过。那出现这种情况时,内核到底有没有问题呢,它究竟在搞什么鬼?这节课我就带你探讨一下如何分析这类问题。
我们知道对于应用开发者而言应用程序的边界是系统调用进入到系统调用中就是Linux内核了。所以要想拓展分析问题的边界你首先需要知道该怎么去分析应用程序使用的系统调用函数。对于内核开发者而言边界同样是系统调用系统调用之外是应用程序。如果内核开发者想要拓展分析问题的边界也需要知道如何利用系统调用去追踪应用程序的逻辑。
## 如何拓展你分析问题的边界?
作为一名内核开发者,我对应用程序逻辑的了解没有对内核的了解那么深。不过,当应用开发者向我寻求帮助时,尽管我对他们的应用逻辑一无所知,但这并不影响我对问题的分析,因为我知道如何借助分析工具追踪应用程序的逻辑。经过一系列追踪之后,我就能对应用程序有一个大概的认识。
我常用来追踪应用逻辑的工具之一就是strace。strace可以用来分析应用和内核的“边界”——系统调用。借助strace我们不仅能够了解应用执行的逻辑还可以了解内核逻辑。那么作为应用开发者的你就可以借助这个工具来拓展你分析应用问题的边界。
strace可以跟踪进程的系统调用、特定的系统调用以及系统调用的执行时间。很多时候我们通过系统调用的执行时间就能判断出业务延迟发生在哪里。比如我们想要跟踪一个多线程程序的系统调用情况那就可以这样使用strace
>
$ strace -T -tt -ff -p pid -o strace.out
不过在使用strace跟踪进程之前我希望你可以先明白strace的工作原理这也是我们这节课的目的你不只要知道怎样使用工具更要明白工具的原理这样在出现问题时你就能明白该工具是否适用了。
## 了解工具的原理,不要局限于如何使用它
strace工具的原理如下图所示我们以上面的那个命令为例来说明
<img src="https://static001.geekbang.org/resource/image/bc/6f/bc04236262f16e0b69842dafd503616f.jpg" alt="" title="strace基本原理">
我们从图中可以看到对于正在运行的进程而言strace可以attach到目标进程上这是通过ptrace这个系统调用实现的gdb工具也是如此。ptrace的PTRACE_SYSCALL会去追踪目标进程的系统调用目标进程被追踪后每次进入syscall都会产生SIGTRAP信号并暂停执行追踪者通过目标进程触发的SIGTRAP信号就可以知道目标进程进入了系统调用然后追踪者会去处理该系统调用我们用strace命令观察到的信息输出就是该处理的结果追踪者处理完该系统调用后就会恢复目标进程的执行。被恢复的目标进程会一直执行下去直到下一个系统调用。
你可以发现目标进程每执行一次系统调用都会被打断等strace处理完后目标进程才能继续执行这就会给目标进程带来比较明显的延迟。因此在生产环境中我不建议使用该命令如果你要使用该命令来追踪生产环境的问题那就一定要做好预案。
假设我们使用strace跟踪到线程延迟抖动是由某一个系统调用耗时长导致的那么接下来我们该怎么继续追踪呢这就到了应用开发者和运维人员需要拓展分析边界的时刻了对内核开发者来说这才算是分析问题的开始。
## 学会使用内核开发者常用的分析工具
我们以一个实际案例来说明吧。有一次业务开发者反馈说他们用strace追踪发现业务的pread(2)系统调用耗时很长经常会有几十毫秒ms的情况甚至能够达到秒级但是不清楚接下来该如何分析因此让我帮他们分析一下。
因为已经明确了问题是由pread(2)这个系统调用引起的所以对内核开发者而言后续的分析就相对容易了。分析这类问题最合适的工具是ftrace我们可以使用ftrace的function_trace功能来追踪pread(2)这个系统调用到底是在哪里耗费了这么长的时间。
要想追踪pread(2)究竟在哪里耗时长,我们就需要知道该系统调用对应的内核函数是什么。我们有两种途径可以方便地获取到系统调用对应的内核函数:
- 查看[include/linux/syscalls.h](https://elixir.bootlin.com/linux/v5.9-rc6/source/include/linux/syscalls.h)文件里的内核函数:
你可以看到与pread有关的函数有多个由于我们的系统是64bit的只需关注64bit相关的系统调用就可以了所以我们锁定在ksys_pread64和sys_read64这两个函数上。[通过该头文件里的注释](https://elixir.bootlin.com/linux/v5.9-rc6/source/include/linux/syscalls.h#L1234)我们能知道,前者是内核使用的,后者是导出给用户的。那么在内核里,我们就需要去追踪前者。另外,请注意,不同内核版本对应的函数可能不一致,我们这里是以最新内核代码(5.9-rc)为例来说明的。
- 通过/proc/kallsyms这个文件来查找
>
<p>$ cat /proc/kallsyms | grep pread64<br>
<br>
ffffffffa02ef3d0 T ksys_pread64<br>
</p>
/proc/kallsyms里的每一行都是一个符号其中第一列是符号地址第二列是符号的属性第三列是符号名字比如上面这个信息中的T就表示全局代码符号我们可以追踪这类的符号。关于这些符号属性的含义你可以通过[man nm](https://man7.org/linux/man-pages/man1/nm.1p.html)来查看。
接下来我们就使用ftrace的function_graph功能来追踪ksys_pread64这个函数看看究竟是内核的哪里耗时这么久。function_graph的使用方式如下
```
# 首先设置要追踪的函数
$ echo ksys_pread64 &gt; /sys/kernel/debug/tracing/set_graph_function
# 其次设置要追踪的线程的pid如果有多个线程那需要将每个线程都逐个写入
$ echo 6577 &gt; /sys/kernel/debug/tracing/set_ftrace_pid
$ echo 6589 &gt;&gt; /sys/kernel/debug/tracing/set_ftrace_pid
# 将function_graph设置为当前的tracer来追踪函数调用情况
$ echo function_graph &gt; /sys/kernel/debug/tracing/current_trace
```
然后我们就可以通过/sys/kernel/debug/tracing/trace_pipe来查看它的输出了下面就是我追踪到的耗时情况
<img src="https://static001.geekbang.org/resource/image/68/fc/689eacfa3ef10c236221f1b2051ab5fc.png" alt="">
我们可以发现pread(2)有102ms是阻塞在io_schedule()这个函数里的io_schedule()的意思是该线程因I/O阻塞而被调度走线程需要等待I/O完成才能继续执行。在function_graph里我们同样也能看到**pread****(**<strong>2**</strong>)**是如何一步步执行到io_schedule的由于整个流程比较长我在这里只把关键的调用逻辑贴出来
```
21) | __lock_page_killable() {
21) 0.073 us | page_waitqueue();
21) | __wait_on_bit_lock() {
21) | prepare_to_wait_exclusive() {
21) 0.186 us | _raw_spin_lock_irqsave();
21) 0.051 us | _raw_spin_unlock_irqrestore();
21) 1.339 us | }
21) | bit_wait_io() {
21) | io_schedule() {
```
我们可以看到,**pread2**是从__lock_page_killable这个函数调用下来的。当pread(2)从磁盘中读文件到内存页page会先lock该page读完后再unlock。如果该page已经被别的线程lock了比如在I/O过程中被lock那么pread(2)就需要等待。等该page被I/O线程unlock后pread(2)才能继续把文件内容读到这个page中。我们当时遇到的情况是在pread(2)从磁盘中读取文件内容到一个page中的时候该page已经被lock了于是调用pread(2)的线程就在这里等待。这其实是合理的内核逻辑没有什么问题。接下来我们就需要看看为什么该page会被lock了这么久。
因为线程是阻塞在磁盘I/O里的所以我们需要查看一下系统的磁盘I/O情况我们可以使用iostat来观察
>
$ iostat -dxm 1
追踪信息如下:
<img src="https://static001.geekbang.org/resource/image/ca/04/ca94121ff716f75c171e2a3380d14d04.png" alt="">
其中sdb是业务pread(2)读取的磁盘所在的文件,通常情况下它的读写量很小,但是我们从上图中可以看到,磁盘利用率(%util会随机出现比较高的情况接近100%。而且avgrq-sz很大也就是说出现了很多I/O排队的情况。另外w/s比平时也要高很多。我们还可以看到由于此时存在大量的I/O写操作磁盘I/O排队严重磁盘I/O利用率也很高。根据这些信息我们可以判断之所以pread(2)读磁盘文件耗时较长很可能是因为被写操作饿死导致的。因此我们接下来需要排查到底是谁在进行写I/O操作。
通过iotop观察I/O行为我们发现并没有用户线程在进行I/O写操作写操作几乎都是内核线程kworker来执行的也就是说用户线程把内容写在了Page Cache里然后kwoker将这些Page Cache中的内容再同步到磁盘中。这就涉及到了我们这门课程第一个模块的内容了如何观测Page Cache的行为。
## 自己写分析工具
如果你现在还不清楚该如何来观测Page Cache的行为那我建议你再从头仔细看一遍我们这门课程的第一个模块我在这里就不细说了。不过我要提一下在Page Cache模块中未曾提到的一些方法这些方法用于判断内存中都有哪些文件以及这些文件的大小。
常规方式是用fincore和mincore不过它们都比较低效。这里有一个更加高效的方式通过写一个内核模块遍历inode来查看Page Cache的组成。该模块的代码较多我只说一下核心的思想伪代码大致如下
```
iterate_supers // 遍历super block
iterate_pagecache_sb // 遍历superblock里的inode
list_for_each_entry(inode, &amp;sb-&gt;s_inodes, i_sb_list)
// 记录该inode的pagecache大小
nrpages = inode-&gt;i_mapping-&gt;nrpages;
/* 获取该inode对应的dentry然后根据该dentry来查找文件路径
* 请注意inode可能没有对应的dentry因为dentry可能被回收掉了
* 此时就无法查看该inode对应的文件名了。
*/
dentry = dentry_from_inode(inode);
dentry_path_raw(dentry, filename, PATH_MAX);
```
使用这种方式不仅可以查看进程正在打开的文件,也能查看文件已经被进程关闭,但文件内容还在内存中的情况。所以这种方式分析起来会更全面。
通过查看Page Cache的文件内容我们发现某些特定的文件占用的内存特别大但是这些文件都是一些离线业务的文件也就是不重要业务的文件。因为离线业务占用了大量的Page Cache导致该在线业务的workingset大大减小所以pread(2)在读文件内容时经常命中不了Page Cache进而需要从磁盘来读文件也就是说该在线业务存在大量的pagein和pageout。
至此问题的解决方案也就有了我们可以通过限制离线业务的Page Cache大小来保障在线业务的workingset防止它出现较多的refault。经过这样调整后业务再也没有出现这种性能抖动了。
你是不是对我上面提到的这些名字感到困惑呢也不清楚inode和Page Cache是什么关系如果是的话那就说明你没有好好学习我们这门课程的Page Cache模块我建议你从头再仔细学习一遍。
好了,我们这节课就讲到这里。
## 课堂总结
我们这节课的内容,对于应用开发者和运维人员而言是有些难度的。我之所以讲这些有难度的内容,就是希望你可以拓展分析问题的边界。这节课的内容对内核开发者而言基本都是基础知识,如果你看不太明白,说明你对内核的理解还不够,你需要花更多的时间好好学习它。我研究内核已经有很多年了,尽管如此,我还是觉得自己对它的理解仍然不够深刻,需要持续不断地学习和研究,而我也一直在这么做。
我们现在回顾一下这节课的重点:
- strace工具是应用和内核的边界如果你是一名应用开发者并且想去拓展分析问题的边界那你就需要去了解strace的原理还需要了解如何去分析strace发现的问题
- ftrace是分析内核问题的利器你需要去了解它
- 你需要根据自己的问题来实现特定的问题分析工具,要想更好地实现这些分析工具,你必须掌握很多内核细节。
## 课后作业
关于我们这节课的“自己写分析工具”这部分我给你留一个作业这也是我没有精力和时间去做的一件事请你在sysrq里实现一个功能让它可以显示出系统中所有R和D状态的任务以此来帮助开发者分析系统load飙高的问题。
我在我们的内核里已经实现了该功能不过在推给Linux内核时maintainer希望我可以用另一种方式来实现。由于那个时候我在忙其他事情这件事便被搁置了下来。如果你实现得比较好你可以把它提交给Linux内核提交的时候你也可以cc一下我laoar.shao@gmail.com。对了你在实现时也可以参考我之前的提交记录[scheduler: enhancement to show_state_filter and SysRq](https://lore.kernel.org/patchwork/patch/818962/)。欢迎你在留言区与我讨论。
最后,感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友。