<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>内存管理 on 夜云泊</title>
    <link>https://lifeislife.cn/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/</link>
    <description>feedId:57980998056508425+userId:73222296380546048 Recent content in 内存管理 on 夜云泊</description>
    <generator>Hugo -- 0.161.1</generator>
    <language>zh</language>
    <lastBuildDate>Sat, 04 Jan 2025 10:34:10 +0000</lastBuildDate>
    <atom:link href="https://lifeislife.cn/tags/%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Per-CPU 变量</title>
      <link>https://lifeislife.cn/posts/per-cpu-%E5%8F%98%E9%87%8F/</link>
      <pubDate>Sat, 04 Jan 2025 10:34:10 +0000</pubDate>
      <guid>https://lifeislife.cn/posts/per-cpu-%E5%8F%98%E9%87%8F/</guid>
      <description>&lt;h3 id=&#34;引入原因&#34;&gt;引入原因&lt;/h3&gt;
&lt;p&gt;Per-CPU 变量是 Linux 内核中的一个重要概念，主要解决以下问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;锁竞争问题&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 SMP 系统中，如果多个 CPU 同时访问同一个变量，需要使用锁来保证数据一致性&lt;/li&gt;
&lt;li&gt;这种锁机制会导致性能下降，特别是在高并发场景下&lt;/li&gt;
&lt;li&gt;Per-CPU 变量为每个 CPU 核心分配独立的变量副本，无需加锁即可安全访问&lt;/li&gt;
&lt;li&gt;需要注意的一点是，Per-CPU 变量是独立的变量，每个 CPU 分别有一个自己的变量，这些变量不是互相同步的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缓存效率&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传统共享变量会导致频繁的缓存失效（cache invalidation）&lt;/li&gt;
&lt;li&gt;Per-CPU 变量位于各自 CPU 的本地缓存中，提高缓存命中率&lt;/li&gt;
&lt;li&gt;减少了 CPU 间的缓存同步开销&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NUMA 友好&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 NUMA 架构中，访问远端内存的开销很大&lt;/li&gt;
&lt;li&gt;Per-CPU 变量确保数据位于本地节点，减少跨 NUMA 节点访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这时候你可能会问，为什么不直接定义 16 个变量分别用于不同的 CPU？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;代码冗余&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果手动为每个 CPU 定义变量，代码会变得冗长且难以维护。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如，对于 128 个 CPU 的系统，可能需要定义 128 个变量：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;var_cpu0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;var_cpu1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;var_cpu2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;...,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;var_cpu127&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不能动态支持多 CPU&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在支持 CPU 热插拔的系统中，CPU 的数量可能动态变化。&lt;/li&gt;
&lt;li&gt;手动定义变量无法适应这种动态变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问效率低&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;访问特定 CPU 的变量时，需要手动计算偏移量或使用条件语句，效率较低。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;get_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;var_cpu0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;var_cpu1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存局部性问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手动定义的变量可能位于同一缓存行中，导致 &lt;strong&gt;缓存行共享（False Sharing）&lt;/strong&gt;，降低性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Per-CPU 变量的优势&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;代码简洁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Per-CPU 变量通过内核提供的机制自动为每个 CPU 分配变量，代码更简洁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;动态适应 CPU 数量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Per-CPU 变量支持动态 CPU 数量，能够自动适应 CPU 热插拔。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如，动态分配 Per-CPU 变量：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;__percpu&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_var&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;alloc_percpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高效访问&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Per-CPU 变量通过内核提供的宏（如 &lt;code&gt;this_cpu_ptr&lt;/code&gt; 和 &lt;code&gt;per_cpu_ptr&lt;/code&gt;）高效访问当前或指定 CPU 的变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ptr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;this_cpu_ptr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// 访问当前 CPU 的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ptr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;per_cpu_ptr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// 访问指定 CPU 的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存优化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Per-CPU 变量会自动对齐到缓存行，避免缓存行共享问题。&lt;/li&gt;
&lt;li&gt;例如，内核会确保每个 CPU 的变量位于不同的缓存行中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;静态声明和定义&#34;&gt;静态声明和定义&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 1. 定义 Per-CPU 变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;                    &lt;span class=&#34;c1&#34;&gt;// 定义并初始化为 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;           &lt;span class=&#34;c1&#34;&gt;// 定义并指定初值
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 2. 声明外部 Per-CPU 变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DECLARE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;                  &lt;span class=&#34;c1&#34;&gt;// 声明在其他地方定义的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 3. 定义 Per-CPU 数组
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;             &lt;span class=&#34;c1&#34;&gt;// 定义数组类型
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 4. 定义只读 Per-CPU 变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU_READ_MOSTLY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;       &lt;span class=&#34;c1&#34;&gt;// 主要用于只读数据，优化缓存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;内部实现机制&#34;&gt;内部实现机制&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define DEFINE_PER_CPU(type, name) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;        DEFINE_PER_CPU_SECTION(type, name, &amp;#34;&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;__attribute__&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;section&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;.data..percpu&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;per_cpu_n&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define DEFINE_PER_CPU_SECTION(type, name, sec) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;    __PCPU_ATTRS(sec) __typeof__(type) name
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define __PCPU_ATTRS(sec)                        \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;    __percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))    
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PER_CPU_BASE_SECTION &amp;#34;.data..percpu&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;比如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 在编译时定义 Per-CPU 变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_counter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 展开后实际上是
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;__attribute__&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;section&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;.data..percpu&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_counter&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里为什么要引入 section 机制？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 kernel 中的普通变量，经过了编译和链接后，会被放置到.data 或者.bss 段，系统在初始化的时候会准备好一切（例如 clear bss），由于 per CPU 变量的特殊性，内核将这些变量放置到了其他的 section，位于 kernel address space 中&lt;code&gt;__per_cpu_start&lt;/code&gt;和&lt;code&gt;__per_cpu_end&lt;/code&gt;之间，我们称之 Per-CPU 变量的原始变量。
摘自：&lt;a href=&#34;http://www.wowotech.net/kernel_synchronization/per-cpu.html&#34;&gt;Linux 内核同步机制之（二）：Per-CPU 变量&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Section 机制允许编译器将特定的数据放在目标文件的特定区域&lt;/li&gt;
&lt;li&gt;对于 Per-CPU 变量，Linux 使用 .data..percpu section 将所有 Per-CPU 变量集中存放&lt;/li&gt;
&lt;li&gt;这种集中存放便于运行时进行整体复制和管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Per-CPU 变量如何建立副本的？&lt;/p&gt;
&lt;p&gt;在内核初始化阶段会调用&lt;code&gt;setup_per_cpu_areas&lt;/code&gt;函数，为每个 CPU 设置 Per-CPU 变量的偏移地址。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;__init&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;setup_per_cpu_areas&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;delta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;delta&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pcpu_base_addr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;__per_cpu_start&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nf&#34;&gt;for_each_possible_cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;__per_cpu_offset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;delta&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pcpu_unit_offsets&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__per_cpu_offset&lt;/code&gt; 是一个全局数组，用于存储每个 CPU 的 Per-CPU 变量的偏移地址。&lt;/li&gt;
&lt;li&gt;在初始化时，内核会为每个 CPU 计算其 Per-CPU 变量的基地址，并将其存储在 &lt;code&gt;__per_cpu_offset[cpu]&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;这个偏移地址是相对于 Per-CPU 变量的起始地址（&lt;code&gt;__per_cpu_start&lt;/code&gt;）的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 CPU 初始化时，内核会使用调用&lt;code&gt;set_my_cpu_offset&lt;/code&gt;函数设置每个 CPU 静态 Per-CPU 变量的偏移地址。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define per_cpu_offset(x) (__per_cpu_offset(x))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;notrace&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;cpu_init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#ifndef CONFIG_CPU_V7M
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;smp_processor_id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;stack&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stk&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stacks&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NR_CPUS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nf&#34;&gt;pr_crit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;CPU%u: bad primary CPU number&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nf&#34;&gt;BUG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;cm&#34;&gt;/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;	 * This only works on resume and secondary cores. For booting on the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;	 * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;	 */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;set_my_cpu_offset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;per_cpu_offset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;cpu_proc_init&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在&lt;code&gt;set_my_cpu_offset&lt;/code&gt;函数中，根据&lt;code&gt;ARM64_HAS_VIRT_HOST_EXTN&lt;/code&gt;的值，选择将偏移地址写入&lt;code&gt;tpidr_el1&lt;/code&gt;或者&lt;code&gt;tpidr_el2&lt;/code&gt;寄存器。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;/* arch/arm64/include/asm/percpu.h */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;inline&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;set_my_cpu_offset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;asm&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;volatile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;ALTERNATIVE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;msr tpidr_el1, %0&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 &lt;span class=&#34;s&#34;&gt;&amp;#34;msr tpidr_el2, %0&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 &lt;span class=&#34;n&#34;&gt;ARM64_HAS_VIRT_HOST_EXTN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;o&#34;&gt;::&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;r&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在访问时使用&lt;code&gt;this_cpu_ptr&lt;/code&gt;宏，展开后可以发现相当于 percpu 变量指针 ptr 加上&lt;code&gt;__my_cpu_offset&lt;/code&gt;。&lt;code&gt;__my_cpu_offset&lt;/code&gt;宏即是从当前cpu的&lt;code&gt;tpidr_el1&lt;/code&gt;、&lt;code&gt;tpidr_el2&lt;/code&gt;寄存器中取出此前设置的&lt;code&gt;__per_cpu_offset[cpu]&lt;/code&gt;值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define this_cpu_ptr(ptr)    raw_cpu_ptr(ptr)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define raw_cpu_ptr(ptr)                        \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;({                                    \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;    __verify_pcpu_ptr(ptr);                        \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;    arch_raw_cpu_ptr(ptr);                        \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;inline&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;__my_cpu_offset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;cm&#34;&gt;/*
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * We want to allow caching the value, so avoid using volatile and
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     * instead use a fake stack read to hazard against barrier().
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;     */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;asm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;ALTERNATIVE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;mrs %0, tpidr_el1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;s&#34;&gt;&amp;#34;mrs %0, tpidr_el2&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;ARM64_HAS_VIRT_HOST_EXTN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;=r&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s&#34;&gt;&amp;#34;Q&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_stack_pointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;使用方式&#34;&gt;使用方式&lt;/h3&gt;
&lt;p&gt;Per-CPU 变量的常用操作方式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;静态定义&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 定义 Per-CPU 整型变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_percpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 定义 Per-CPU 结构体
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;DEFINE_PER_CPU&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_struct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;my_percpu_struct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;访问变量&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 获取本地 CPU 的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu_var&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;get_cpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_percpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;put_cpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_percpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// 必须配对使用
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 访问指定 CPU 的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;var_cpu0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;per_cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_percpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 遍历所有 CPU 的变量
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nf&#34;&gt;for_each_possible_cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;per_cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;my_percpu_var&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cpu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// 处理 value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description>
      <content:encoded><![CDATA[<h3 id="引入原因">引入原因</h3>
<p>Per-CPU 变量是 Linux 内核中的一个重要概念，主要解决以下问题：</p>
<ol>
<li>
<p><strong>锁竞争问题</strong></p>
<ul>
<li>在 SMP 系统中，如果多个 CPU 同时访问同一个变量，需要使用锁来保证数据一致性</li>
<li>这种锁机制会导致性能下降，特别是在高并发场景下</li>
<li>Per-CPU 变量为每个 CPU 核心分配独立的变量副本，无需加锁即可安全访问</li>
<li>需要注意的一点是，Per-CPU 变量是独立的变量，每个 CPU 分别有一个自己的变量，这些变量不是互相同步的。</li>
</ul>
</li>
<li>
<p><strong>缓存效率</strong></p>
<ul>
<li>传统共享变量会导致频繁的缓存失效（cache invalidation）</li>
<li>Per-CPU 变量位于各自 CPU 的本地缓存中，提高缓存命中率</li>
<li>减少了 CPU 间的缓存同步开销</li>
</ul>
</li>
<li>
<p><strong>NUMA 友好</strong></p>
<ul>
<li>在 NUMA 架构中，访问远端内存的开销很大</li>
<li>Per-CPU 变量确保数据位于本地节点，减少跨 NUMA 节点访问</li>
</ul>
</li>
</ol>
<p>这时候你可能会问，为什么不直接定义 16 个变量分别用于不同的 CPU？</p>
<ol>
<li>
<p>代码冗余</p>
<ul>
<li>
<p>如果手动为每个 CPU 定义变量，代码会变得冗长且难以维护。</p>
</li>
<li>
<p>例如，对于 128 个 CPU 的系统，可能需要定义 128 个变量：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">var_cpu0</span><span class="p">,</span> <span class="n">var_cpu1</span><span class="p">,</span> <span class="n">var_cpu2</span><span class="p">,</span> <span class="p">...,</span> <span class="n">var_cpu127</span><span class="p">;</span>
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>不能动态支持多 CPU</p>
<ul>
<li>在支持 CPU 热插拔的系统中，CPU 的数量可能动态变化。</li>
<li>手动定义变量无法适应这种动态变化。</li>
</ul>
</li>
<li>
<p>访问效率低</p>
<ul>
<li>
<p>访问特定 CPU 的变量时，需要手动计算偏移量或使用条件语句，效率较低。</p>
</li>
<li>
<p>例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="o">*</span><span class="nf">get_var</span><span class="p">(</span><span class="kt">int</span> <span class="n">cpu</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">switch</span> <span class="p">(</span><span class="n">cpu</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="mi">0</span><span class="o">:</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">var_cpu0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="mi">1</span><span class="o">:</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">var_cpu1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ...
</span></span></span><span class="line"><span class="cl">        <span class="k">default</span><span class="o">:</span> <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>缓存局部性问题</p>
<ul>
<li>手动定义的变量可能位于同一缓存行中，导致 <strong>缓存行共享（False Sharing）</strong>，降低性能。</li>
</ul>
</li>
</ol>
<p><strong>Per-CPU 变量的优势</strong></p>
<ol>
<li>
<p>代码简洁</p>
<ul>
<li>
<p>Per-CPU 变量通过内核提供的机制自动为每个 CPU 分配变量，代码更简洁。</p>
</li>
<li>
<p>例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="n">my_var</span><span class="p">);</span>
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>动态适应 CPU 数量</p>
<ul>
<li>
<p>Per-CPU 变量支持动态 CPU 数量，能够自动适应 CPU 热插拔。</p>
</li>
<li>
<p>例如，动态分配 Per-CPU 变量：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">__percpu</span> <span class="o">*</span><span class="n">my_var</span> <span class="o">=</span> <span class="nf">alloc_percpu</span><span class="p">(</span><span class="kt">int</span><span class="p">);</span>
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>高效访问</p>
<ul>
<li>
<p>Per-CPU 变量通过内核提供的宏（如 <code>this_cpu_ptr</code> 和 <code>per_cpu_ptr</code>）高效访问当前或指定 CPU 的变量。</p>
</li>
<li>
<p>例如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">int</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="nf">this_cpu_ptr</span><span class="p">(</span><span class="o">&amp;</span><span class="n">my_var</span><span class="p">);</span>  <span class="c1">// 访问当前 CPU 的变量
</span></span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="o">*</span><span class="n">ptr</span> <span class="o">=</span> <span class="nf">per_cpu_ptr</span><span class="p">(</span><span class="o">&amp;</span><span class="n">my_var</span><span class="p">,</span> <span class="n">cpu</span><span class="p">);</span>  <span class="c1">// 访问指定 CPU 的变量
</span></span></span></code></pre></div></li>
</ul>
</li>
<li>
<p>缓存优化</p>
<ul>
<li>Per-CPU 变量会自动对齐到缓存行，避免缓存行共享问题。</li>
<li>例如，内核会确保每个 CPU 的变量位于不同的缓存行中。</li>
</ul>
</li>
</ol>
<h3 id="静态声明和定义">静态声明和定义</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 1. 定义 Per-CPU 变量
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">name</span><span class="p">);</span>                    <span class="c1">// 定义并初始化为 0
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>           <span class="c1">// 定义并指定初值
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 2. 声明外部 Per-CPU 变量
</span></span></span><span class="line"><span class="cl"><span class="nf">DECLARE_PER_CPU</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">name</span><span class="p">);</span>                  <span class="c1">// 声明在其他地方定义的变量
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 3. 定义 Per-CPU 数组
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">name</span><span class="p">[</span><span class="n">SIZE</span><span class="p">]);</span>             <span class="c1">// 定义数组类型
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 4. 定义只读 Per-CPU 变量
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU_READ_MOSTLY</span><span class="p">(</span><span class="n">type</span><span class="p">,</span> <span class="n">name</span><span class="p">);</span>       <span class="c1">// 主要用于只读数据，优化缓存
</span></span></span></code></pre></div><h3 id="内部实现机制">内部实现机制</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define DEFINE_PER_CPU(type, name) \
</span></span></span><span class="line"><span class="cl"><span class="cp">        DEFINE_PER_CPU_SECTION(type, name, &#34;&#34;)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">__attribute__</span><span class="p">((</span><span class="nf">section</span><span class="p">(</span><span class="s">&#34;.data..percpu&#34;</span><span class="p">)))</span> <span class="kt">int</span> <span class="n">per_cpu_n</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define DEFINE_PER_CPU_SECTION(type, name, sec) \
</span></span></span><span class="line"><span class="cl"><span class="cp">    __PCPU_ATTRS(sec) __typeof__(type) name
</span></span></span><span class="line"><span class="cl"><span class="cp">#define __PCPU_ATTRS(sec)                        \
</span></span></span><span class="line"><span class="cl"><span class="cp">    __percpu __attribute__((section(PER_CPU_BASE_SECTION sec)))    
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define PER_CPU_BASE_SECTION &#34;.data..percpu&#34;
</span></span></span></code></pre></div><p>比如：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 在编译时定义 Per-CPU 变量
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="n">my_counter</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 展开后实际上是
</span></span></span><span class="line"><span class="cl"><span class="nf">__attribute__</span><span class="p">((</span><span class="nf">section</span><span class="p">(</span><span class="s">&#34;.data..percpu&#34;</span><span class="p">)))</span> <span class="kt">int</span> <span class="n">my_counter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span></code></pre></div><p>这里为什么要引入 section 机制？</p>
<blockquote>
<p>对于 kernel 中的普通变量，经过了编译和链接后，会被放置到.data 或者.bss 段，系统在初始化的时候会准备好一切（例如 clear bss），由于 per CPU 变量的特殊性，内核将这些变量放置到了其他的 section，位于 kernel address space 中<code>__per_cpu_start</code>和<code>__per_cpu_end</code>之间，我们称之 Per-CPU 变量的原始变量。
摘自：<a href="http://www.wowotech.net/kernel_synchronization/per-cpu.html">Linux 内核同步机制之（二）：Per-CPU 变量</a></p>
</blockquote>
<ul>
<li>Section 机制允许编译器将特定的数据放在目标文件的特定区域</li>
<li>对于 Per-CPU 变量，Linux 使用 .data..percpu section 将所有 Per-CPU 变量集中存放</li>
<li>这种集中存放便于运行时进行整体复制和管理</li>
</ul>
<p>Per-CPU 变量如何建立副本的？</p>
<p>在内核初始化阶段会调用<code>setup_per_cpu_areas</code>函数，为每个 CPU 设置 Per-CPU 变量的偏移地址。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">__init</span> <span class="nf">setup_per_cpu_areas</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">delta</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">cpu</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">...</span>
</span></span><span class="line"><span class="cl">    <span class="n">delta</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span><span class="p">)</span><span class="n">pcpu_base_addr</span> <span class="o">-</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span><span class="p">)</span><span class="n">__per_cpu_start</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">for_each_possible_cpu</span><span class="p">(</span><span class="n">cpu</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">__per_cpu_offset</span><span class="p">[</span><span class="n">cpu</span><span class="p">]</span> <span class="o">=</span> <span class="n">delta</span> <span class="o">+</span> <span class="n">pcpu_unit_offsets</span><span class="p">[</span><span class="n">cpu</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><ul>
<li><code>__per_cpu_offset</code> 是一个全局数组，用于存储每个 CPU 的 Per-CPU 变量的偏移地址。</li>
<li>在初始化时，内核会为每个 CPU 计算其 Per-CPU 变量的基地址，并将其存储在 <code>__per_cpu_offset[cpu]</code> 中。</li>
<li>这个偏移地址是相对于 Per-CPU 变量的起始地址（<code>__per_cpu_start</code>）的。</li>
</ul>
<p>在 CPU 初始化时，内核会使用调用<code>set_my_cpu_offset</code>函数设置每个 CPU 静态 Per-CPU 变量的偏移地址。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define per_cpu_offset(x) (__per_cpu_offset(x))
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">notrace</span> <span class="nf">cpu_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="cp">#ifndef CONFIG_CPU_V7M
</span></span></span><span class="line"><span class="cl">	<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">cpu</span> <span class="o">=</span> <span class="nf">smp_processor_id</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">	<span class="k">struct</span> <span class="n">stack</span> <span class="o">*</span><span class="n">stk</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">stacks</span><span class="p">[</span><span class="n">cpu</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="p">(</span><span class="n">cpu</span> <span class="o">&gt;=</span> <span class="n">NR_CPUS</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">		<span class="nf">pr_crit</span><span class="p">(</span><span class="s">&#34;CPU%u: bad primary CPU number</span><span class="se">\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">cpu</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">		<span class="nf">BUG</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm">	 * This only works on resume and secondary cores. For booting on the
</span></span></span><span class="line"><span class="cl"><span class="cm">	 * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
</span></span></span><span class="line"><span class="cl"><span class="cm">	 */</span>
</span></span><span class="line"><span class="cl">	<span class="nf">set_my_cpu_offset</span><span class="p">(</span><span class="nf">per_cpu_offset</span><span class="p">(</span><span class="n">cpu</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="nf">cpu_proc_init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>在<code>set_my_cpu_offset</code>函数中，根据<code>ARM64_HAS_VIRT_HOST_EXTN</code>的值，选择将偏移地址写入<code>tpidr_el1</code>或者<code>tpidr_el2</code>寄存器。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cm">/* arch/arm64/include/asm/percpu.h */</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">set_my_cpu_offset</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">off</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="nf">ALTERNATIVE</span><span class="p">(</span><span class="s">&#34;msr tpidr_el1, %0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                 <span class="s">&#34;msr tpidr_el2, %0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                 <span class="n">ARM64_HAS_VIRT_HOST_EXTN</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">::</span> <span class="s">&#34;r&#34;</span> <span class="p">(</span><span class="n">off</span><span class="p">)</span> <span class="o">:</span> <span class="s">&#34;memory&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>在访问时使用<code>this_cpu_ptr</code>宏，展开后可以发现相当于 percpu 变量指针 ptr 加上<code>__my_cpu_offset</code>。<code>__my_cpu_offset</code>宏即是从当前cpu的<code>tpidr_el1</code>、<code>tpidr_el2</code>寄存器中取出此前设置的<code>__per_cpu_offset[cpu]</code>值。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define this_cpu_ptr(ptr)    raw_cpu_ptr(ptr)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define raw_cpu_ptr(ptr)                        \
</span></span></span><span class="line"><span class="cl"><span class="cp">({                                    \
</span></span></span><span class="line"><span class="cl"><span class="cp">    __verify_pcpu_ptr(ptr);                        \
</span></span></span><span class="line"><span class="cl"><span class="cp">    arch_raw_cpu_ptr(ptr);                        \
</span></span></span><span class="line"><span class="cl"><span class="cp">})
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cp">#define arch_raw_cpu_ptr(ptr) SHIFT_PERCPU_PTR(ptr, __my_cpu_offset)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kr">inline</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="nf">__my_cpu_offset</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm">     * We want to allow caching the value, so avoid using volatile and
</span></span></span><span class="line"><span class="cl"><span class="cm">     * instead use a fake stack read to hazard against barrier().
</span></span></span><span class="line"><span class="cl"><span class="cm">     */</span>
</span></span><span class="line"><span class="cl">    <span class="k">asm</span><span class="p">(</span><span class="nf">ALTERNATIVE</span><span class="p">(</span><span class="s">&#34;mrs %0, tpidr_el1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s">&#34;mrs %0, tpidr_el2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">ARM64_HAS_VIRT_HOST_EXTN</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">:</span> <span class="s">&#34;=r&#34;</span> <span class="p">(</span><span class="n">off</span><span class="p">)</span> <span class="o">:</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;Q&#34;</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="o">*</span><span class="p">)</span><span class="n">current_stack_pointer</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="使用方式">使用方式</h3>
<p>Per-CPU 变量的常用操作方式：</p>
<p><strong>静态定义</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 定义 Per-CPU 整型变量
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="n">my_percpu_var</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 定义 Per-CPU 结构体
</span></span></span><span class="line"><span class="cl"><span class="nf">DEFINE_PER_CPU</span><span class="p">(</span><span class="k">struct</span> <span class="n">my_struct</span><span class="p">,</span> <span class="n">my_percpu_struct</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>访问变量</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 获取本地 CPU 的变量
</span></span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">cpu_var</span> <span class="o">=</span> <span class="nf">get_cpu_var</span><span class="p">(</span><span class="n">my_percpu_var</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">put_cpu_var</span><span class="p">(</span><span class="n">my_percpu_var</span><span class="p">);</span>  <span class="c1">// 必须配对使用
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 访问指定 CPU 的变量
</span></span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">var_cpu0</span> <span class="o">=</span> <span class="nf">per_cpu</span><span class="p">(</span><span class="n">my_percpu_var</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 遍历所有 CPU 的变量
</span></span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">cpu</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nf">for_each_possible_cpu</span><span class="p">(</span><span class="n">cpu</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="nf">per_cpu</span><span class="p">(</span><span class="n">my_percpu_var</span><span class="p">,</span> <span class="n">cpu</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 处理 value
</span></span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>uCore 实验第 4 章 - 地址空间</title>
      <link>https://lifeislife.cn/posts/ucore-%E5%AE%9E%E9%AA%8C%E7%AC%AC4%E7%AB%A0-%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4/</link>
      <pubDate>Mon, 04 Sep 2023 11:11:48 +0000</pubDate>
      <guid>https://lifeislife.cn/posts/ucore-%E5%AE%9E%E9%AA%8C%E7%AC%AC4%E7%AB%A0-%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;为何指定 TRAMPOLINE 和 TRAPFRAME 在 va 的最高位？
TRAMPOLINE 和 TRAPFRAME 被定义在最高的虚拟内存地址上，是因为它们在操作系统的内存布局中起着重要作用。
TRAMPOLINE 被用作从用户模式切换到内核模式的跳转目标。当发生异常或中断时，处理器将从用户模式切换到内核模式，并将控制权转移到内核中预定义的位置，也就是陷阱处理程序。TRAMPOLINE 页面被映射到最高虚拟地址，以便处理器能够在这个转换过程中方便地引用它。通过将其放置在最高地址，确保了无论系统的具体内存布局如何，它始终是可访问的。
另一方面，TRAPFRAME 用于在发生异常或中断时存储机器状态。它包含寄存器、标志和其他操作系统处理异常所需的信息。TRAPFRAME 也被放置在最高的虚拟地址上，以确保它易于访问，并且陷阱处理程序可以高效地访问它。
通过将 TRAMPOLINE 和 TRAPFRAME 定义在最高的虚拟内存地址上，内核可以方便而可靠地处理异常和中断，而无需关心它们在内存中的特定位置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;如何确定分页方案---satp&#34;&gt;如何确定分页方案 - satp&lt;/h1&gt;
&lt;p&gt;在 MMU 没有使能的情况下，虚拟地址和物理地址是相同的。在 MMU 使能的情况下，虚拟地址会被转换成物理地址。这个转换过程是由操作系统来管理的，操作系统需要维护一个数据结构来记录虚拟地址和物理地址的映射关系。这个数据结构就是页表。&lt;/p&gt;
&lt;p&gt;转换的过程需要分页机制，分页机制有多种。RISC-V 的分页方案以 SvX 的模式命名，其中 X 是以位为单位的&lt;strong&gt;虚拟地址的长度&lt;/strong&gt;。在 RV64 架构下，RISC-V 支持多种分页方案，包括 Sv39，Sv48，Sv57 以及 Sv64。Sv39 最大支持 39 位的虚拟地址，这意味着它可以支持 512 GB 的虚拟地址空间。Sv48 最大支持 48 位的虚拟地址，这意味着它可以支持 256 TB 的虚拟地址空间。我们将在本章中实现 Sv39 分页方案。&lt;/p&gt;
&lt;p&gt;如何开启分页机制呢？RISC-V 的分页机制是通过 satp（Supervisor address translation and protection）寄存器来开启的。satp 寄存器字段分布如下：&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/5d1ec6e9adaf743f7c9abc177cd12eb1.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/5d1ec6e9adaf743f7c9abc177cd12eb1.png&#34; alt=&#34;&#34;  title=&#34;RV64 架构下的 satp 寄存器&#34; style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mode 字段可以决定是否开启分页以及分页级数。Mode=0 时，不开启分页；Mode=8 时，开启 Sv39 分页机制。&lt;/li&gt;
&lt;li&gt;ASID（Address Space Identifier，地址空间标识符）域是可选的，它可以用来降低上下文切换的开销。目前我们暂不考虑这个字段的作用。&lt;/li&gt;
&lt;li&gt;PPN（Physical Page Number，物理页号），保存了根页表的物理地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;sv39-多级页表机制&#34;&gt;SV39 多级页表机制&lt;/h1&gt;
&lt;h2 id=&#34;页表项描述&#34;&gt;页表项描述&lt;/h2&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/86e06238c562bdd238e868fcd819df3c.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/86e06238c562bdd238e868fcd819df3c.png&#34; alt=&#34;&#34;  title=&#34;Sv39 页表项&#34; style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;Sv39 页表项（page-table entry，PTE）的布局，从左到右分别包含如下所述的域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;V 位决定了该页表项的其余部分是否有效 (V=1 时有效)。若 V=0，则任何遍历到此页表项的虚址转换操作都会导致页错误。&lt;/li&gt;
&lt;li&gt;R、W 和 X 位分别表示此页是否可以读取、写入和执行。如果这三个位都是 0，那么这个页表项是指向下一级页表的指针，否则它是页表树的一个叶节点。&lt;/li&gt;
&lt;li&gt;U 位表示该页是否是用户页面。若 U=0，则 U 模式不能访问此页面，但 S 模式可以。若 U=1，则 U 模式下能访问这个页面，而 S 模式不能。&lt;/li&gt;
&lt;li&gt;G 位表示这个映射是否对所有虚址空间有效，硬件可以用这个信息来提高地址转换的性能。这一位通常只用于属于操作系统的页面。&lt;/li&gt;
&lt;li&gt;A 位表示自从上次 A 位被清除以来，该页面是否被访问过。&lt;/li&gt;
&lt;li&gt;D 位表示自从上次清除 D 位以来页面是否被弄脏（例如被写入）。&lt;/li&gt;
&lt;li&gt;RSW 域留给操作系统使用，它会被硬件忽略。&lt;/li&gt;
&lt;li&gt;PPN 域包含物理页号，这是物理地址的一部分。若这个页表项是一个叶节点，那么 PPN 是转换后物理地址的一部分。否则 PPN 给出下一节页表的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;虚拟地址转换物理地址过程&#34;&gt;虚拟地址转换物理地址过程&lt;/h2&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/48e6ce48ffb827a10371344ad07324c2.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/48e6ce48ffb827a10371344ad07324c2.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;当 satp 寄存器中开启分页时，S 模式和 U 模式中访存的地址都会被视为虚拟地址，需要将其转换为物理地址。虚拟地址转换物理地址的过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 satp 寄存器中读取 PPN，得到根页表的物理地址，为了表述方便，我们将其记做三级页表基地址 satp.PPN；&lt;/li&gt;
&lt;li&gt;从虚拟地址中取出三级虚拟页号 L2&lt;/li&gt;
&lt;li&gt;处理器会读取地址位于 satp.PPN * 4096 + L2 * 4 的页表项，得到下一级页表的基地址 L1.PPN；&lt;/li&gt;
&lt;li&gt;从虚拟地址中取出二级虚拟页号 L1&lt;/li&gt;
&lt;li&gt;处理器会读取地址位于 L1.PPN * 4096 + L1 * 4 的页表项，得到下一级页表的基地址 L0.PPN；&lt;/li&gt;
&lt;li&gt;从虚拟地址中取出一级虚拟页号 L0&lt;/li&gt;
&lt;li&gt;处理器会读取地址位于 L0.PPN * 4096 + L0 * 4 的页表项，得到物理页号 PPN；&lt;/li&gt;
&lt;li&gt;将 PPN 和虚拟地址的低 12 位也就是 Offset 拼接起来，得到物理地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看代码中是如何实现的：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PTE2PA(pte) (((pte) &amp;gt;&amp;gt; 10) &amp;lt;&amp;lt; 12)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 从虚拟地址中提取三个 9 位的页表索引
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PXMASK 0x1FF &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 9
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// PGSHIFT = 12，这段宏定义用于定位 VPNx 的位置
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PXSHIFT(level) (PGSHIFT + (9 * (level)))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 从虚拟地址 VA 中提取出第 level 级页表的索引
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PX(level, va) ((((uint64)(va)) &amp;gt;&amp;gt; PXSHIFT(level)) &amp;amp; PXMASK)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面这三个工具宏可以用来提取虚拟页号 VPN。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 返回页表 pagetable 中与虚拟地址 va 对应的 PTE 的地址。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 如果 alloc != 0，则创建所需的页表页。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// RISC-V Sv39 方案有三级页表页。一个页表页包含 512 个 64 位的 PTEs。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 一个 64 位的虚拟地址被分为五个字段：
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//   39..63 -- 必须为零。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//   30..38 -- 2 级索引的 9 位。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//   21..29 -- 1 级索引的 9 位。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//   12..20 -- 0 级索引的 9 位。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//    0..11 -- 页面内的 12 位字节偏移量。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// pagetable 页表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// va 虚拟地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// alloc 页表项不存在时是否分配
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;pte_t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;walk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;pagetable_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;va&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;alloc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;va&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;MAXVA&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nf&#34;&gt;panic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;walk&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;level&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;level&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;level&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;kt&#34;&gt;pte_t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;PX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;level&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;va&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 通过 PTE 的标志位判断每一级的 pte 是否是有效的（V 位）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PTE_V&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;pagetable_t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;PTE2PA&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// 如果该项无效且 alloc 标志被设置，则分配一个新的页表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// 如果 alloc 参数=0 或者已经没有空闲的内存了，那么遇到中途 V=0 的 pte 整个 walk 过程就会直接退出
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alloc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;pde_t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;kalloc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// 清空分配的页表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nf&#34;&gt;memset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PGSIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// 更新页表项，将其指向新分配的页表，并设置有效位 PTE_V
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;PA2PTE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PTE_V&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// 返回最低级和虚拟地址的页表项，不是返回物理地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;PX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;va&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每次从虚拟地址 va 中提取出一个虚拟页号，然后根据这个虚拟页号从页表中取出下一级页表的基地址。如果这个页表项无效，那么根据 alloc 参数决定是否分配一个新的页表。如果 alloc 参数为 0 或者已经没有空闲的内存了，那么遇到中途 V=0 的 pte 整个 walk 过程就会直接退出。如果 alloc 参数为 1，那么就会分配一个新的页表，然后将这个页表项指向新分配的页表，并设置有效位 PTE_V。&lt;/p&gt;
&lt;p&gt;我们可以发现 walk 返回的结果不是物理地址，而是页表项的地址。这是因为 walk 函数的作用是将虚拟地址转换为物理地址，而页表项中的 PPN 只是物理地址的一部分，&lt;strong&gt;还需要加上虚拟地址的低 12 位偏移量才能得到物理地址&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;如何建立页表&#34;&gt;如何建立页表&lt;/h2&gt;
&lt;p&gt;前面的过程实际上是以用户的角度来考虑的，也就是给你一个虚拟地址按照分页的规则将其转化成物理地址就能访问了。但是作为一个操作系统，我们还需要多考虑一下，页表是哪来的？我们知道从虚拟地址中去获取页表地址，但是&lt;strong&gt;页表的内容是哪来的呢&lt;/strong&gt;？页表是如何建立起来的呢？这些是需要操作系统来完成的。&lt;/p&gt;
&lt;p&gt;建立页表也就是建立虚拟地址到物理地址的映射关系。也就是给你一个虚拟地址，你需要告诉我如何查到物理地址，实际上这个过程就是建立页表的过程。这个过程也是通过 walk 函数来完成的，从上文我们知道如果页表都建好的情况下 walk 就是不断查页表的过程，那么在没有页表的情况下，walk 还可以建立一个个页表。稍有不同的是，walk 返回的是最后一级页表项的地址，我们需要将物理地址写入这个页表项中。&lt;/p&gt;
&lt;p&gt;在 uCore 中使用 mappages 函数封装了 walk 函数，具体如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#define PA2PTE(pa) ((((uint64)pa) &amp;gt;&amp;gt; 12) &amp;lt;&amp;lt; 10)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * 为从虚拟地址 va 开始的页面创建指向物理地址 pa 开始的页表项（PTE）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * 注意：va 和 size 可能不是页面对齐的
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * 如果无法分配所需的页表，则返回 0，否则返回 -1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @param pagetable 根页表地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @param va        虚拟地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @param size      映射的字节数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @param pa        物理地址
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @param perm      权限位
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; * @return          成功返回 0，否则返回 -1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;mappages&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;pagetable_t&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;va&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;perm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;uint64&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;virtualAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastVirtualAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;pte_t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// 地址必须是页面对齐的
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;virtualAddress&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;PGROUNDDOWN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;va&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;lastVirtualAddress&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;PGROUNDDOWN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;va&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;size&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(;;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 返回最低级的虚拟地址的页表项，如果不存在会创建一个新的页表项
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 页表项可能会因为内存不足创建失败，如果创建失败，则返回 -1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;walk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pagetable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;virtualAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 如果 PTE 已经有效，则输出错误信息并返回 -1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PTE_V&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nf&#34;&gt;errorf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;remap&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 将物理地址 pa 转换为页表项，并设置权限位 perm 和 有效位 PTE_V
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pte&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;PA2PTE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pa&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;perm&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PTE_V&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// 如果当前是最后一个地址，则结束循环
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;virtualAddress&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastVirtualAddress&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;virtualAddress&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PGSIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;pa&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PGSIZE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id=&#34;问答作业&#34;&gt;问答作业&lt;/h1&gt;
&lt;h2 id=&#34;请列举-sv39-页表页表项的组成结合课堂内容描述其中的标志位有何作用潜在作用&#34;&gt;请列举 SV39 页表页表项的组成，结合课堂内容，描述其中的标志位有何作用／潜在作用？&lt;/h2&gt;
&lt;p&gt;Sv39 页表页表项的组成如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;有效位 (V)&lt;/strong&gt;：这是页表项的最高位，用于指示页表项是否有效。如果有效位设置为 1，表示页表项有效，可以使用；如果设置为 0，表示页表项无效，禁止使用。这是虚拟内存中页表项的基本有效性标志。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入位 (W)&lt;/strong&gt;：这个标志位用于指示是否可以对此页进行写入操作。如果设置为 1，表示允许写入；如果设置为 0，表示禁止写入。它是页表项的访问权限控制标志之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户位 (U)&lt;/strong&gt;：用户位用于指示是否允许用户态程序访问此页。如果设置为 1，表示允许用户态访问；如果设置为 0，表示只允许内核态访问。它是页表项的访问权限控制标志之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行位 (X)&lt;/strong&gt;：执行位用于指示是否允许执行此页上的指令。如果设置为 1，表示允许执行；如果设置为 0，表示禁止执行。它也是页表项的访问权限控制标志之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局位 (G)&lt;/strong&gt;：全局位用于指示此页是否是全局的，即无需 TLB 缓存，通常用于内核页。如果设置为 1，表示是全局的；如果设置为 0，表示不是全局的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已访问位 (A)&lt;/strong&gt;：已访问位表示是否已经访问过此页，通常由硬件设置。操作系统可以用它来实现页面置换算法，如 LRU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已修改位 (D)&lt;/strong&gt;：已修改位表示是否已经对此页进行了写入操作。与已访问位类似，操作系统可以用它来实现页面置换算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理页框地址 (PPN)&lt;/strong&gt;：这是页表项中存储的物理页框的地址。它指示了虚拟页到物理页的映射关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Sv39 页表的页表项标志位允许操作系统和硬件实现对虚拟内存的细粒度控制和保护。不同的标志位组合可以实现不同级别的内存保护和权限控制，从而提高系统的安全性和可用性。例如，有效位、写入位、用户位和执行位的不同组合可以实现不同级别的内存保护，使操作系统可以将不同的内存区域分配给用户态和内核态，并设置不同的权限。已访问位和已修改位则用于实现页面置换算法，帮助操作系统决定哪些页面应该被置换出去，以优化内存利用率。全局位可以用于标识全局共享的页，从而节省 TLB 缓存空间。物理页框地址是页表项的核心，它建立了虚拟地址到物理地址的映射关系，使虚拟内存管理成为可能。&lt;/p&gt;
&lt;h2 id=&#34;缺页相关问题&#34;&gt;缺页相关问题&lt;/h2&gt;
&lt;h3 id=&#34;请问哪些异常可能是缺页导致的&#34;&gt;请问哪些异常可能是缺页导致的？&lt;/h3&gt;
&lt;p&gt;缺页异常是由于进程访问的页面不在页表中或者在页表中无效而引发的异常。以下这些异常可能是因为缺页导致的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Load Page Fault（Load 异常）：当进程试图读取一个不在页表中或者无效的页面时，会引发 Load Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 5。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Store Page Fault（Store 异常）：当进程试图写入一个不在页表中或者无效的页面时，会引发 Store Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 7。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Instruction Page Fault（指令页异常）：当进程试图执行一个不在页表中或者无效的页面上的指令时，会引发 Instruction Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 12。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;发生缺页时描述相关的重要寄存器的值lab2-中描述过的可以简单点&#34;&gt;发生缺页时，描述相关的重要寄存器的值（lab2 中描述过的可以简单点）。&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;sepc（Exception Program Counter）：trap 发生时会将当前指令的下一条指令地址写入其中，用于 trap 处理完成后返回。&lt;/li&gt;
&lt;li&gt;stval（Machine Trap Value）：mtval 寄存器包含导致异常的原因，即导致异常的指令的具体信息。例如，如果是缺页异常，那么 mtval 寄存器包含导致缺页异常的虚拟地址。&lt;/li&gt;
&lt;li&gt;scause: 中断/异常发生时， CSR 寄存器 scause 中会记录其信息， Interrupt 位记录是中断还是异常， Exception Code 记录中断/异常的种类。&lt;/li&gt;
&lt;li&gt;sstatus: 记录处理器当前状态，其中 SPP 段记录当前特权等级。&lt;/li&gt;
&lt;li&gt;stvec: 记录处理 trap 的入口地址，现有两种模式  Direct 和 Vectored 。&lt;/li&gt;
&lt;li&gt;sscratch: 其中的值是指向hart相关的S态上下文的指针，比如内核栈的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;以下行为的好处&#34;&gt;以下行为的好处？&lt;/h3&gt;
&lt;p&gt;缺页有两个常见的原因，其一是 Lazy 策略，也就是直到内存页面被访问才实际进行页表操作。比如，一个程序被执行时，进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做，而是会保存 .text 段在磁盘的位置信息，在这些代码第一次被执行时才完成从磁盘的加载操作。&lt;/p&gt;
&lt;p&gt;Lazy Loading 策略有以下好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;减少初始化开销&lt;/strong&gt;：Lazy Loading 允许操作系统在程序启动时只加载必需的页面，而不是一次性加载整个程序。这可以减少启动时间和初始化开销，因为不需要将整个程序加载到内存中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节省内存&lt;/strong&gt;：Lazy Loading 策略避免了不必要的内存占用。如果程序的某些部分从不被访问，那么它们就不会被加载到内存中，从而节省了内存资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高响应速度&lt;/strong&gt;：通过仅在需要时加载页面，Lazy Loading 可以提高系统的响应速度。只有当程序访问某个页面时，操作系统才会执行磁盘加载操作，而不会在程序启动时浪费时间加载可能永远不会被访问的内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好的磁盘利用率&lt;/strong&gt;：Lazy Loading 允许操作系统将程序的不同部分分散在磁盘上，根据需要加载。这可以提高磁盘利用率，因为不需要在磁盘上为整个程序分配连续的空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;请问处理-10g-连续的内存页面需要操作的页表实际大致占用多少内存-给出数量级即可&#34;&gt;请问处理 10G 连续的内存页面，需要操作的页表实际大致占用多少内存 (给出数量级即可)？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;此外 COW(Copy On Write) 也是常见的容易导致缺页的 Lazy 策略，这个之后再说。其实，我们的 mmap 也可以采取 Lazy 策略，比如：一个用户进程先后申请了 10G 的内存空间，然后用了其中 1M 就直接退出了。按照现在的做法，我们显然亏大了，进行了很多没有意义的页表操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;处理 10GB 连续的内存页面所需的页表实际上占用的内存量取决于操作系统的页表结构和管理策略。在 RISC-V 的页表结构中，一个页表项（Page Table Entry，PTE）通常占据 8 字节（64 位系统），其中包括物理页框号和一些标志位。让我们假设一个 PTE 占用 8 字节。&lt;/p&gt;
&lt;p&gt;为了估算 10GB 连续内存页面所需的页表实际占用内存量，我们可以按照以下步骤进行计算：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先，将 10GB 转换为字节数。1GB 等于 1,073,741,824 字节，所以 10GB 等于 10 * 1,073,741,824 = 10,737,418,240 字节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后，计算每个页面表项覆盖的内存范围。假设每个页面表项管理 4KB（4 * 1024 字节）的内存页面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算需要多少个页面表项来管理 10GB 的内存。这可以通过将 10GB 除以每个页面表项管理的内存范围来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，将所需的页面表项数量乘以每个 PTE 的大小来估算所需的总内存量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;让我们进行具体计算：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存大小：10,737,418,240 字节&lt;/li&gt;
&lt;li&gt;每个页面表项管理的内存范围：4KB = 4 * 1024 字节&lt;/li&gt;
&lt;li&gt;需要的页面表项数量：10,737,418,240 字节 / 4KB = 2,621,440 个页表项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设每个页表项占用 8 字节，则需要的总内存量为：&lt;/p&gt;
&lt;p&gt;2,621,440 个页表项 * 8 字节/页表项 = 20,971,520 字节&lt;/p&gt;
&lt;p&gt;所以，处理 10GB 连续的内存页面所需的页表实际占用内存量约为 20,971,520 字节，或者大约 20MB。这只是一个估算，实际内存占用可能会因操作系统的管理策略和对齐等因素而有所不同。&lt;/p&gt;
&lt;h3 id=&#34;请简单思考如何才能在现有框架基础上实现-lazy-策略缺页时又如何处理描述合理即可不需要考虑实现&#34;&gt;请简单思考如何才能在现有框架基础上实现 Lazy 策略，缺页时又如何处理？描述合理即可，不需要考虑实现。&lt;/h3&gt;
&lt;p&gt;要在现有框架基础上实现 Lazy 策略，可以采取以下简单思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;延迟加载（Lazy Loading）&lt;/strong&gt;：在用户进程请求内存映射时，不立即将整个内存区域加载到物理内存中。而是仅创建虚拟内存映射和页表项，记录对应的磁盘位置等信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缺页处理（Page Fault Handling）&lt;/strong&gt;：当用户进程访问虚拟内存中的某个尚未加载的内存页面时，会触发缺页异常。在缺页异常处理程序中，操作系统会根据页表中的磁盘位置信息，将相应的磁盘数据加载到物理内存中，并更新页表项，使其指向新加载的物理页面。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;惰性加载（Demand Paging）&lt;/strong&gt;：为了提高性能，可以采用惰性加载策略，即只加载实际被访问的内存页面，而不是一次性加载整个区域。这可以通过在缺页处理程序中进行懒加载操作来实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存回收（Memory Reclamation）&lt;/strong&gt;：当系统内存不足时，操作系统可以选择回收一些不常访问的内存页面，将其写回磁盘，并更新页表项为无效。这需要根据页面访问模式和策略来确定哪些页面可以被回收。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：为了提高性能，可以采用预读取（Prefetching）策略，即在缺页处理时，不仅加载当前访问的页面，还预先加载相邻的页面，以减少未来可能的缺页次数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;此时页面失效如何表现在页表项-pte-上&#34;&gt;此时页面失效如何表现在页表项 (PTE) 上？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;缺页的另一个常见原因是 swap 策略，也就是内存页面可能被换到磁盘上了，导致对应页面失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Dirty bit (D 位)：当页面被修改并且尚未写回到主存时，该位会被设置为 1。如果页面已经被换出到磁盘上，D 位将保持为 1，以指示页面数据已过期。&lt;/p&gt;
&lt;p&gt;Valid bit (V 位)：当页面在主存中有效时，V 位被设置为 1。如果页面被换出到磁盘上，V 位将被清除为 0，表示该页无效。&lt;/p&gt;
&lt;p&gt;通过检查页表项的 D 位和 V 位，操作系统可以确定页面是否需要从磁盘重新加载到内存中。如果 D 位为 1，说明页面需要写回到主存，在将其置为有效之前，必须将页数据从磁盘读取到内存中。如果 V 位为 0，说明页面当前无效，需要将其从磁盘加载到内存中，并将 V 位设置为 1，表示页面有效。&lt;/p&gt;
&lt;h2 id=&#34;双页表与单页表&#34;&gt;双页表与单页表&lt;/h2&gt;
&lt;p&gt;为了防范侧信道攻击，我们的 os 使用了双页表。但是传统的设计一直是单页表的，也就是说，用户线程和对应的内核线程共用同一张页表，只不过内核对应的地址只允许在内核态访问。请结合课堂知识回答如下问题：(备注：这里的单/双的说法仅为自创的通俗说法，并无这个名词概念，详情见 KPTI )&lt;/p&gt;
&lt;h2 id=&#34;单页表情况下如何更换页表&#34;&gt;单页表情况下，如何更换页表？&lt;/h2&gt;
&lt;p&gt;在单页表情况下，页表的更换通常是由操作系统的上下文切换来触发的。当从用户态切换到内核态或从一个进程切换到另一个进程时，操作系统会根据相应的上下文信息加载不同的页表，实现页表的更换。&lt;/p&gt;
&lt;h2 id=&#34;单页表情况下如何控制用户态无法访问内核页面tips看看第一题最后一问&#34;&gt;单页表情况下，如何控制用户态无法访问内核页面？（tips:看看第一题最后一问）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设置页面权限：内核页面通常会被设置为只能在内核态下访问（例如，设置 PTE_U 位为 0），这样用户态无法访问内核页面。&lt;/li&gt;
&lt;li&gt;操作系统权限：操作系统内核态拥有较高的权限，可以通过特权级别或访问控制机制来确保用户态无法直接访问内核页面。用户程序只能通过系统调用进入内核态，并在内核态下由操作系统执行，从而实现对内核页面的访问控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;单页表有何优势回答合理即可&#34;&gt;单页表有何优势？（回答合理即可）&lt;/h2&gt;
&lt;p&gt;单页表的主要优势在于简化了地址转换过程，减少了内存访问的开销。由于用户线程和内核线程共享同一张页表，不需要在上下文切换时频繁切换页表，这可以提高地址转换的效率。此外，单页表还可以节省内存，因为不需要为每个用户线程分配独立的页表。&lt;/p&gt;
&lt;h2 id=&#34;双页表实现下何时需要更换页表假设你写一个单页表操作系统你会选择何时更换页表回答合理即可&#34;&gt;双页表实现下，何时需要更换页表？假设你写一个单页表操作系统，你会选择何时更换页表（回答合理即可）？&lt;/h2&gt;
&lt;p&gt;在双页表实现下，页表的更换通常在发生上下文切换时需要。当从用户态切换到内核态或从一个进程切换到另一个进程时，需要加载相应的页表，以确保正确的地址转换。如果操作系统采用了每个进程独立的页表，那么在进程切换时需要更换页表。&lt;/p&gt;
&lt;p&gt;如果我写一个单页表操作系统，我会选择在发生进程切换时更换页表，因为这是最频繁的上下文切换情况之一。在其他情况下，如从用户态切换到内核态，可能不需要更换整张页表，而只需修改页表项的权限位来实现访问控制。这样可以减少页表更换的开销，提高性能。&lt;/p&gt;
&lt;h1 id=&#34;附录&#34;&gt;附录&lt;/h1&gt;
&lt;p&gt;修改user项目中的makefile，删除ch4_&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<blockquote>
<p>为何指定 TRAMPOLINE 和 TRAPFRAME 在 va 的最高位？
TRAMPOLINE 和 TRAPFRAME 被定义在最高的虚拟内存地址上，是因为它们在操作系统的内存布局中起着重要作用。
TRAMPOLINE 被用作从用户模式切换到内核模式的跳转目标。当发生异常或中断时，处理器将从用户模式切换到内核模式，并将控制权转移到内核中预定义的位置，也就是陷阱处理程序。TRAMPOLINE 页面被映射到最高虚拟地址，以便处理器能够在这个转换过程中方便地引用它。通过将其放置在最高地址，确保了无论系统的具体内存布局如何，它始终是可访问的。
另一方面，TRAPFRAME 用于在发生异常或中断时存储机器状态。它包含寄存器、标志和其他操作系统处理异常所需的信息。TRAPFRAME 也被放置在最高的虚拟地址上，以确保它易于访问，并且陷阱处理程序可以高效地访问它。
通过将 TRAMPOLINE 和 TRAPFRAME 定义在最高的虚拟内存地址上，内核可以方便而可靠地处理异常和中断，而无需关心它们在内存中的特定位置。</p>
</blockquote>
<h1 id="如何确定分页方案---satp">如何确定分页方案 - satp</h1>
<p>在 MMU 没有使能的情况下，虚拟地址和物理地址是相同的。在 MMU 使能的情况下，虚拟地址会被转换成物理地址。这个转换过程是由操作系统来管理的，操作系统需要维护一个数据结构来记录虚拟地址和物理地址的映射关系。这个数据结构就是页表。</p>
<p>转换的过程需要分页机制，分页机制有多种。RISC-V 的分页方案以 SvX 的模式命名，其中 X 是以位为单位的<strong>虚拟地址的长度</strong>。在 RV64 架构下，RISC-V 支持多种分页方案，包括 Sv39，Sv48，Sv57 以及 Sv64。Sv39 最大支持 39 位的虚拟地址，这意味着它可以支持 512 GB 的虚拟地址空间。Sv48 最大支持 48 位的虚拟地址，这意味着它可以支持 256 TB 的虚拟地址空间。我们将在本章中实现 Sv39 分页方案。</p>
<p>如何开启分页机制呢？RISC-V 的分页机制是通过 satp（Supervisor address translation and protection）寄存器来开启的。satp 寄存器字段分布如下：</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/5d1ec6e9adaf743f7c9abc177cd12eb1.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/5d1ec6e9adaf743f7c9abc177cd12eb1.png" alt=""  title="RV64 架构下的 satp 寄存器" style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<ul>
<li>Mode 字段可以决定是否开启分页以及分页级数。Mode=0 时，不开启分页；Mode=8 时，开启 Sv39 分页机制。</li>
<li>ASID（Address Space Identifier，地址空间标识符）域是可选的，它可以用来降低上下文切换的开销。目前我们暂不考虑这个字段的作用。</li>
<li>PPN（Physical Page Number，物理页号），保存了根页表的物理地址。</li>
</ul>
<h1 id="sv39-多级页表机制">SV39 多级页表机制</h1>
<h2 id="页表项描述">页表项描述</h2>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/86e06238c562bdd238e868fcd819df3c.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/86e06238c562bdd238e868fcd819df3c.png" alt=""  title="Sv39 页表项" style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>Sv39 页表项（page-table entry，PTE）的布局，从左到右分别包含如下所述的域：</p>
<ul>
<li>V 位决定了该页表项的其余部分是否有效 (V=1 时有效)。若 V=0，则任何遍历到此页表项的虚址转换操作都会导致页错误。</li>
<li>R、W 和 X 位分别表示此页是否可以读取、写入和执行。如果这三个位都是 0，那么这个页表项是指向下一级页表的指针，否则它是页表树的一个叶节点。</li>
<li>U 位表示该页是否是用户页面。若 U=0，则 U 模式不能访问此页面，但 S 模式可以。若 U=1，则 U 模式下能访问这个页面，而 S 模式不能。</li>
<li>G 位表示这个映射是否对所有虚址空间有效，硬件可以用这个信息来提高地址转换的性能。这一位通常只用于属于操作系统的页面。</li>
<li>A 位表示自从上次 A 位被清除以来，该页面是否被访问过。</li>
<li>D 位表示自从上次清除 D 位以来页面是否被弄脏（例如被写入）。</li>
<li>RSW 域留给操作系统使用，它会被硬件忽略。</li>
<li>PPN 域包含物理页号，这是物理地址的一部分。若这个页表项是一个叶节点，那么 PPN 是转换后物理地址的一部分。否则 PPN 给出下一节页表的地址。</li>
</ul>
<h2 id="虚拟地址转换物理地址过程">虚拟地址转换物理地址过程</h2>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/48e6ce48ffb827a10371344ad07324c2.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img//2023/09/05/48e6ce48ffb827a10371344ad07324c2.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>当 satp 寄存器中开启分页时，S 模式和 U 模式中访存的地址都会被视为虚拟地址，需要将其转换为物理地址。虚拟地址转换物理地址的过程如下：</p>
<ul>
<li>从 satp 寄存器中读取 PPN，得到根页表的物理地址，为了表述方便，我们将其记做三级页表基地址 satp.PPN；</li>
<li>从虚拟地址中取出三级虚拟页号 L2</li>
<li>处理器会读取地址位于 satp.PPN * 4096 + L2 * 4 的页表项，得到下一级页表的基地址 L1.PPN；</li>
<li>从虚拟地址中取出二级虚拟页号 L1</li>
<li>处理器会读取地址位于 L1.PPN * 4096 + L1 * 4 的页表项，得到下一级页表的基地址 L0.PPN；</li>
<li>从虚拟地址中取出一级虚拟页号 L0</li>
<li>处理器会读取地址位于 L0.PPN * 4096 + L0 * 4 的页表项，得到物理页号 PPN；</li>
<li>将 PPN 和虚拟地址的低 12 位也就是 Offset 拼接起来，得到物理地址。</li>
</ul>
<p>我们看代码中是如何实现的：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define PTE2PA(pte) (((pte) &gt;&gt; 10) &lt;&lt; 12)
</span></span></span><span class="line"><span class="cl"><span class="c1">// 从虚拟地址中提取三个 9 位的页表索引
</span></span></span><span class="line"><span class="cl"><span class="cp">#define PXMASK 0x1FF </span><span class="c1">// 9
</span></span></span><span class="line"><span class="cl"><span class="c1">// PGSHIFT = 12，这段宏定义用于定位 VPNx 的位置
</span></span></span><span class="line"><span class="cl"><span class="cp">#define PXSHIFT(level) (PGSHIFT + (9 * (level)))
</span></span></span><span class="line"><span class="cl"><span class="c1">// 从虚拟地址 VA 中提取出第 level 级页表的索引
</span></span></span><span class="line"><span class="cl"><span class="cp">#define PX(level, va) ((((uint64)(va)) &gt;&gt; PXSHIFT(level)) &amp; PXMASK)
</span></span></span></code></pre></div><p>上面这三个工具宏可以用来提取虚拟页号 VPN。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 返回页表 pagetable 中与虚拟地址 va 对应的 PTE 的地址。
</span></span></span><span class="line"><span class="cl"><span class="c1">// 如果 alloc != 0，则创建所需的页表页。
</span></span></span><span class="line"><span class="cl"><span class="c1">//
</span></span></span><span class="line"><span class="cl"><span class="c1">// RISC-V Sv39 方案有三级页表页。一个页表页包含 512 个 64 位的 PTEs。
</span></span></span><span class="line"><span class="cl"><span class="c1">// 一个 64 位的虚拟地址被分为五个字段：
</span></span></span><span class="line"><span class="cl"><span class="c1">//   39..63 -- 必须为零。
</span></span></span><span class="line"><span class="cl"><span class="c1">//   30..38 -- 2 级索引的 9 位。
</span></span></span><span class="line"><span class="cl"><span class="c1">//   21..29 -- 1 级索引的 9 位。
</span></span></span><span class="line"><span class="cl"><span class="c1">//   12..20 -- 0 级索引的 9 位。
</span></span></span><span class="line"><span class="cl"><span class="c1">//    0..11 -- 页面内的 12 位字节偏移量。
</span></span></span><span class="line"><span class="cl"><span class="c1">// pagetable 页表
</span></span></span><span class="line"><span class="cl"><span class="c1">// va 虚拟地址
</span></span></span><span class="line"><span class="cl"><span class="c1">// alloc 页表项不存在时是否分配
</span></span></span><span class="line"><span class="cl"><span class="kt">pte_t</span> <span class="o">*</span><span class="nf">walk</span><span class="p">(</span><span class="kt">pagetable_t</span> <span class="n">pagetable</span><span class="p">,</span> <span class="n">uint64</span> <span class="n">va</span><span class="p">,</span> <span class="kt">int</span> <span class="n">alloc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">va</span> <span class="o">&gt;=</span> <span class="n">MAXVA</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nf">panic</span><span class="p">(</span><span class="s">&#34;walk&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">level</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">level</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">;</span> <span class="n">level</span><span class="o">--</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">pte_t</span> <span class="o">*</span><span class="n">pte</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">pagetable</span><span class="p">[</span><span class="nf">PX</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">va</span><span class="p">)];</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 通过 PTE 的标志位判断每一级的 pte 是否是有效的（V 位）
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">pte</span> <span class="o">&amp;</span> <span class="n">PTE_V</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">pagetable</span> <span class="o">=</span> <span class="p">(</span><span class="kt">pagetable_t</span><span class="p">)</span><span class="nf">PTE2PA</span><span class="p">(</span><span class="o">*</span><span class="n">pte</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 如果该项无效且 alloc 标志被设置，则分配一个新的页表
</span></span></span><span class="line"><span class="cl">            <span class="c1">// 如果 alloc 参数=0 或者已经没有空闲的内存了，那么遇到中途 V=0 的 pte 整个 walk 过程就会直接退出
</span></span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">alloc</span> <span class="o">||</span> <span class="p">(</span><span class="n">pagetable</span> <span class="o">=</span> <span class="p">(</span><span class="kt">pde_t</span> <span class="o">*</span><span class="p">)</span><span class="nf">kalloc</span><span class="p">())</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 清空分配的页表
</span></span></span><span class="line"><span class="cl">            <span class="nf">memset</span><span class="p">(</span><span class="n">pagetable</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">PGSIZE</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 更新页表项，将其指向新分配的页表，并设置有效位 PTE_V
</span></span></span><span class="line"><span class="cl">            <span class="o">*</span><span class="n">pte</span> <span class="o">=</span> <span class="nf">PA2PTE</span><span class="p">(</span><span class="n">pagetable</span><span class="p">)</span> <span class="o">|</span> <span class="n">PTE_V</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 返回最低级和虚拟地址的页表项，不是返回物理地址
</span></span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="n">pagetable</span><span class="p">[</span><span class="nf">PX</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">va</span><span class="p">)];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>每次从虚拟地址 va 中提取出一个虚拟页号，然后根据这个虚拟页号从页表中取出下一级页表的基地址。如果这个页表项无效，那么根据 alloc 参数决定是否分配一个新的页表。如果 alloc 参数为 0 或者已经没有空闲的内存了，那么遇到中途 V=0 的 pte 整个 walk 过程就会直接退出。如果 alloc 参数为 1，那么就会分配一个新的页表，然后将这个页表项指向新分配的页表，并设置有效位 PTE_V。</p>
<p>我们可以发现 walk 返回的结果不是物理地址，而是页表项的地址。这是因为 walk 函数的作用是将虚拟地址转换为物理地址，而页表项中的 PPN 只是物理地址的一部分，<strong>还需要加上虚拟地址的低 12 位偏移量才能得到物理地址</strong>。</p>
<h2 id="如何建立页表">如何建立页表</h2>
<p>前面的过程实际上是以用户的角度来考虑的，也就是给你一个虚拟地址按照分页的规则将其转化成物理地址就能访问了。但是作为一个操作系统，我们还需要多考虑一下，页表是哪来的？我们知道从虚拟地址中去获取页表地址，但是<strong>页表的内容是哪来的呢</strong>？页表是如何建立起来的呢？这些是需要操作系统来完成的。</p>
<p>建立页表也就是建立虚拟地址到物理地址的映射关系。也就是给你一个虚拟地址，你需要告诉我如何查到物理地址，实际上这个过程就是建立页表的过程。这个过程也是通过 walk 函数来完成的，从上文我们知道如果页表都建好的情况下 walk 就是不断查页表的过程，那么在没有页表的情况下，walk 还可以建立一个个页表。稍有不同的是，walk 返回的是最后一级页表项的地址，我们需要将物理地址写入这个页表项中。</p>
<p>在 uCore 中使用 mappages 函数封装了 walk 函数，具体如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define PA2PTE(pa) ((((uint64)pa) &gt;&gt; 12) &lt;&lt; 10)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * 为从虚拟地址 va 开始的页面创建指向物理地址 pa 开始的页表项（PTE）
</span></span></span><span class="line"><span class="cl"><span class="cm"> * 注意：va 和 size 可能不是页面对齐的
</span></span></span><span class="line"><span class="cl"><span class="cm"> * 如果无法分配所需的页表，则返回 0，否则返回 -1
</span></span></span><span class="line"><span class="cl"><span class="cm"> * 
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param pagetable 根页表地址
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param va        虚拟地址
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param size      映射的字节数
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param pa        物理地址
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param perm      权限位
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @return          成功返回 0，否则返回 -1
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">mappages</span><span class="p">(</span><span class="kt">pagetable_t</span> <span class="n">pagetable</span><span class="p">,</span> <span class="n">uint64</span> <span class="n">va</span><span class="p">,</span> <span class="n">uint64</span> <span class="n">size</span><span class="p">,</span> <span class="n">uint64</span> <span class="n">pa</span><span class="p">,</span> <span class="kt">int</span> <span class="n">perm</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">uint64</span> <span class="n">virtualAddress</span><span class="p">,</span> <span class="n">lastVirtualAddress</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">pte_t</span> <span class="o">*</span><span class="n">pte</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 地址必须是页面对齐的
</span></span></span><span class="line"><span class="cl">    <span class="n">virtualAddress</span>     <span class="o">=</span> <span class="nf">PGROUNDDOWN</span><span class="p">(</span><span class="n">va</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">lastVirtualAddress</span> <span class="o">=</span> <span class="nf">PGROUNDDOWN</span><span class="p">(</span><span class="n">va</span> <span class="o">+</span> <span class="n">size</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 返回最低级的虚拟地址的页表项，如果不存在会创建一个新的页表项
</span></span></span><span class="line"><span class="cl">        <span class="c1">// 页表项可能会因为内存不足创建失败，如果创建失败，则返回 -1
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">((</span><span class="n">pte</span> <span class="o">=</span> <span class="nf">walk</span><span class="p">(</span><span class="n">pagetable</span><span class="p">,</span> <span class="n">virtualAddress</span><span class="p">,</span> <span class="mi">1</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 如果 PTE 已经有效，则输出错误信息并返回 -1
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">pte</span> <span class="o">&amp;</span> <span class="n">PTE_V</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nf">errorf</span><span class="p">(</span><span class="s">&#34;remap&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 将物理地址 pa 转换为页表项，并设置权限位 perm 和 有效位 PTE_V
</span></span></span><span class="line"><span class="cl">        <span class="o">*</span><span class="n">pte</span> <span class="o">=</span> <span class="nf">PA2PTE</span><span class="p">(</span><span class="n">pa</span><span class="p">)</span> <span class="o">|</span> <span class="n">perm</span> <span class="o">|</span> <span class="n">PTE_V</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 如果当前是最后一个地址，则结束循环
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">virtualAddress</span> <span class="o">==</span> <span class="n">lastVirtualAddress</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">virtualAddress</span> <span class="o">+=</span> <span class="n">PGSIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">pa</span> <span class="o">+=</span> <span class="n">PGSIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h1 id="问答作业">问答作业</h1>
<h2 id="请列举-sv39-页表页表项的组成结合课堂内容描述其中的标志位有何作用潜在作用">请列举 SV39 页表页表项的组成，结合课堂内容，描述其中的标志位有何作用／潜在作用？</h2>
<p>Sv39 页表页表项的组成如下：</p>
<ol>
<li><strong>有效位 (V)</strong>：这是页表项的最高位，用于指示页表项是否有效。如果有效位设置为 1，表示页表项有效，可以使用；如果设置为 0，表示页表项无效，禁止使用。这是虚拟内存中页表项的基本有效性标志。</li>
<li><strong>写入位 (W)</strong>：这个标志位用于指示是否可以对此页进行写入操作。如果设置为 1，表示允许写入；如果设置为 0，表示禁止写入。它是页表项的访问权限控制标志之一。</li>
<li><strong>用户位 (U)</strong>：用户位用于指示是否允许用户态程序访问此页。如果设置为 1，表示允许用户态访问；如果设置为 0，表示只允许内核态访问。它是页表项的访问权限控制标志之一。</li>
<li><strong>执行位 (X)</strong>：执行位用于指示是否允许执行此页上的指令。如果设置为 1，表示允许执行；如果设置为 0，表示禁止执行。它也是页表项的访问权限控制标志之一。</li>
<li><strong>全局位 (G)</strong>：全局位用于指示此页是否是全局的，即无需 TLB 缓存，通常用于内核页。如果设置为 1，表示是全局的；如果设置为 0，表示不是全局的。</li>
<li><strong>已访问位 (A)</strong>：已访问位表示是否已经访问过此页，通常由硬件设置。操作系统可以用它来实现页面置换算法，如 LRU。</li>
<li><strong>已修改位 (D)</strong>：已修改位表示是否已经对此页进行了写入操作。与已访问位类似，操作系统可以用它来实现页面置换算法。</li>
<li><strong>物理页框地址 (PPN)</strong>：这是页表项中存储的物理页框的地址。它指示了虚拟页到物理页的映射关系。</li>
</ol>
<p>Sv39 页表的页表项标志位允许操作系统和硬件实现对虚拟内存的细粒度控制和保护。不同的标志位组合可以实现不同级别的内存保护和权限控制，从而提高系统的安全性和可用性。例如，有效位、写入位、用户位和执行位的不同组合可以实现不同级别的内存保护，使操作系统可以将不同的内存区域分配给用户态和内核态，并设置不同的权限。已访问位和已修改位则用于实现页面置换算法，帮助操作系统决定哪些页面应该被置换出去，以优化内存利用率。全局位可以用于标识全局共享的页，从而节省 TLB 缓存空间。物理页框地址是页表项的核心，它建立了虚拟地址到物理地址的映射关系，使虚拟内存管理成为可能。</p>
<h2 id="缺页相关问题">缺页相关问题</h2>
<h3 id="请问哪些异常可能是缺页导致的">请问哪些异常可能是缺页导致的？</h3>
<p>缺页异常是由于进程访问的页面不在页表中或者在页表中无效而引发的异常。以下这些异常可能是因为缺页导致的：</p>
<ul>
<li>
<p>Load Page Fault（Load 异常）：当进程试图读取一个不在页表中或者无效的页面时，会引发 Load Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 5。</p>
</li>
<li>
<p>Store Page Fault（Store 异常）：当进程试图写入一个不在页表中或者无效的页面时，会引发 Store Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 7。</p>
</li>
<li>
<p>Instruction Page Fault（指令页异常）：当进程试图执行一个不在页表中或者无效的页面上的指令时，会引发 Instruction Page Fault 异常。在 RISC-V 中，这个异常对应的异常代码是 12。</p>
</li>
</ul>
<h3 id="发生缺页时描述相关的重要寄存器的值lab2-中描述过的可以简单点">发生缺页时，描述相关的重要寄存器的值（lab2 中描述过的可以简单点）。</h3>
<ul>
<li>sepc（Exception Program Counter）：trap 发生时会将当前指令的下一条指令地址写入其中，用于 trap 处理完成后返回。</li>
<li>stval（Machine Trap Value）：mtval 寄存器包含导致异常的原因，即导致异常的指令的具体信息。例如，如果是缺页异常，那么 mtval 寄存器包含导致缺页异常的虚拟地址。</li>
<li>scause: 中断/异常发生时， CSR 寄存器 scause 中会记录其信息， Interrupt 位记录是中断还是异常， Exception Code 记录中断/异常的种类。</li>
<li>sstatus: 记录处理器当前状态，其中 SPP 段记录当前特权等级。</li>
<li>stvec: 记录处理 trap 的入口地址，现有两种模式  Direct 和 Vectored 。</li>
<li>sscratch: 其中的值是指向hart相关的S态上下文的指针，比如内核栈的指针。</li>
</ul>
<h3 id="以下行为的好处">以下行为的好处？</h3>
<p>缺页有两个常见的原因，其一是 Lazy 策略，也就是直到内存页面被访问才实际进行页表操作。比如，一个程序被执行时，进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做，而是会保存 .text 段在磁盘的位置信息，在这些代码第一次被执行时才完成从磁盘的加载操作。</p>
<p>Lazy Loading 策略有以下好处：</p>
<ol>
<li><strong>减少初始化开销</strong>：Lazy Loading 允许操作系统在程序启动时只加载必需的页面，而不是一次性加载整个程序。这可以减少启动时间和初始化开销，因为不需要将整个程序加载到内存中。</li>
<li><strong>节省内存</strong>：Lazy Loading 策略避免了不必要的内存占用。如果程序的某些部分从不被访问，那么它们就不会被加载到内存中，从而节省了内存资源。</li>
<li><strong>提高响应速度</strong>：通过仅在需要时加载页面，Lazy Loading 可以提高系统的响应速度。只有当程序访问某个页面时，操作系统才会执行磁盘加载操作，而不会在程序启动时浪费时间加载可能永远不会被访问的内容。</li>
<li><strong>更好的磁盘利用率</strong>：Lazy Loading 允许操作系统将程序的不同部分分散在磁盘上，根据需要加载。这可以提高磁盘利用率，因为不需要在磁盘上为整个程序分配连续的空间。</li>
</ol>
<h3 id="请问处理-10g-连续的内存页面需要操作的页表实际大致占用多少内存-给出数量级即可">请问处理 10G 连续的内存页面，需要操作的页表实际大致占用多少内存 (给出数量级即可)？</h3>
<blockquote>
<p>此外 COW(Copy On Write) 也是常见的容易导致缺页的 Lazy 策略，这个之后再说。其实，我们的 mmap 也可以采取 Lazy 策略，比如：一个用户进程先后申请了 10G 的内存空间，然后用了其中 1M 就直接退出了。按照现在的做法，我们显然亏大了，进行了很多没有意义的页表操作。</p>
</blockquote>
<p>处理 10GB 连续的内存页面所需的页表实际上占用的内存量取决于操作系统的页表结构和管理策略。在 RISC-V 的页表结构中，一个页表项（Page Table Entry，PTE）通常占据 8 字节（64 位系统），其中包括物理页框号和一些标志位。让我们假设一个 PTE 占用 8 字节。</p>
<p>为了估算 10GB 连续内存页面所需的页表实际占用内存量，我们可以按照以下步骤进行计算：</p>
<ol>
<li>
<p>首先，将 10GB 转换为字节数。1GB 等于 1,073,741,824 字节，所以 10GB 等于 10 * 1,073,741,824 = 10,737,418,240 字节。</p>
</li>
<li>
<p>然后，计算每个页面表项覆盖的内存范围。假设每个页面表项管理 4KB（4 * 1024 字节）的内存页面。</p>
</li>
<li>
<p>计算需要多少个页面表项来管理 10GB 的内存。这可以通过将 10GB 除以每个页面表项管理的内存范围来实现。</p>
</li>
<li>
<p>最后，将所需的页面表项数量乘以每个 PTE 的大小来估算所需的总内存量。</p>
</li>
</ol>
<p>让我们进行具体计算：</p>
<ul>
<li>内存大小：10,737,418,240 字节</li>
<li>每个页面表项管理的内存范围：4KB = 4 * 1024 字节</li>
<li>需要的页面表项数量：10,737,418,240 字节 / 4KB = 2,621,440 个页表项</li>
</ul>
<p>假设每个页表项占用 8 字节，则需要的总内存量为：</p>
<p>2,621,440 个页表项 * 8 字节/页表项 = 20,971,520 字节</p>
<p>所以，处理 10GB 连续的内存页面所需的页表实际占用内存量约为 20,971,520 字节，或者大约 20MB。这只是一个估算，实际内存占用可能会因操作系统的管理策略和对齐等因素而有所不同。</p>
<h3 id="请简单思考如何才能在现有框架基础上实现-lazy-策略缺页时又如何处理描述合理即可不需要考虑实现">请简单思考如何才能在现有框架基础上实现 Lazy 策略，缺页时又如何处理？描述合理即可，不需要考虑实现。</h3>
<p>要在现有框架基础上实现 Lazy 策略，可以采取以下简单思路：</p>
<ol>
<li>
<p><strong>延迟加载（Lazy Loading）</strong>：在用户进程请求内存映射时，不立即将整个内存区域加载到物理内存中。而是仅创建虚拟内存映射和页表项，记录对应的磁盘位置等信息。</p>
</li>
<li>
<p><strong>缺页处理（Page Fault Handling）</strong>：当用户进程访问虚拟内存中的某个尚未加载的内存页面时，会触发缺页异常。在缺页异常处理程序中，操作系统会根据页表中的磁盘位置信息，将相应的磁盘数据加载到物理内存中，并更新页表项，使其指向新加载的物理页面。</p>
</li>
<li>
<p><strong>惰性加载（Demand Paging）</strong>：为了提高性能，可以采用惰性加载策略，即只加载实际被访问的内存页面，而不是一次性加载整个区域。这可以通过在缺页处理程序中进行懒加载操作来实现。</p>
</li>
<li>
<p><strong>内存回收（Memory Reclamation）</strong>：当系统内存不足时，操作系统可以选择回收一些不常访问的内存页面，将其写回磁盘，并更新页表项为无效。这需要根据页面访问模式和策略来确定哪些页面可以被回收。</p>
</li>
<li>
<p><strong>性能优化</strong>：为了提高性能，可以采用预读取（Prefetching）策略，即在缺页处理时，不仅加载当前访问的页面，还预先加载相邻的页面，以减少未来可能的缺页次数。</p>
</li>
</ol>
<h3 id="此时页面失效如何表现在页表项-pte-上">此时页面失效如何表现在页表项 (PTE) 上？</h3>
<blockquote>
<p>缺页的另一个常见原因是 swap 策略，也就是内存页面可能被换到磁盘上了，导致对应页面失效。</p>
</blockquote>
<p>Dirty bit (D 位)：当页面被修改并且尚未写回到主存时，该位会被设置为 1。如果页面已经被换出到磁盘上，D 位将保持为 1，以指示页面数据已过期。</p>
<p>Valid bit (V 位)：当页面在主存中有效时，V 位被设置为 1。如果页面被换出到磁盘上，V 位将被清除为 0，表示该页无效。</p>
<p>通过检查页表项的 D 位和 V 位，操作系统可以确定页面是否需要从磁盘重新加载到内存中。如果 D 位为 1，说明页面需要写回到主存，在将其置为有效之前，必须将页数据从磁盘读取到内存中。如果 V 位为 0，说明页面当前无效，需要将其从磁盘加载到内存中，并将 V 位设置为 1，表示页面有效。</p>
<h2 id="双页表与单页表">双页表与单页表</h2>
<p>为了防范侧信道攻击，我们的 os 使用了双页表。但是传统的设计一直是单页表的，也就是说，用户线程和对应的内核线程共用同一张页表，只不过内核对应的地址只允许在内核态访问。请结合课堂知识回答如下问题：(备注：这里的单/双的说法仅为自创的通俗说法，并无这个名词概念，详情见 KPTI )</p>
<h2 id="单页表情况下如何更换页表">单页表情况下，如何更换页表？</h2>
<p>在单页表情况下，页表的更换通常是由操作系统的上下文切换来触发的。当从用户态切换到内核态或从一个进程切换到另一个进程时，操作系统会根据相应的上下文信息加载不同的页表，实现页表的更换。</p>
<h2 id="单页表情况下如何控制用户态无法访问内核页面tips看看第一题最后一问">单页表情况下，如何控制用户态无法访问内核页面？（tips:看看第一题最后一问）</h2>
<ul>
<li>设置页面权限：内核页面通常会被设置为只能在内核态下访问（例如，设置 PTE_U 位为 0），这样用户态无法访问内核页面。</li>
<li>操作系统权限：操作系统内核态拥有较高的权限，可以通过特权级别或访问控制机制来确保用户态无法直接访问内核页面。用户程序只能通过系统调用进入内核态，并在内核态下由操作系统执行，从而实现对内核页面的访问控制。</li>
</ul>
<h2 id="单页表有何优势回答合理即可">单页表有何优势？（回答合理即可）</h2>
<p>单页表的主要优势在于简化了地址转换过程，减少了内存访问的开销。由于用户线程和内核线程共享同一张页表，不需要在上下文切换时频繁切换页表，这可以提高地址转换的效率。此外，单页表还可以节省内存，因为不需要为每个用户线程分配独立的页表。</p>
<h2 id="双页表实现下何时需要更换页表假设你写一个单页表操作系统你会选择何时更换页表回答合理即可">双页表实现下，何时需要更换页表？假设你写一个单页表操作系统，你会选择何时更换页表（回答合理即可）？</h2>
<p>在双页表实现下，页表的更换通常在发生上下文切换时需要。当从用户态切换到内核态或从一个进程切换到另一个进程时，需要加载相应的页表，以确保正确的地址转换。如果操作系统采用了每个进程独立的页表，那么在进程切换时需要更换页表。</p>
<p>如果我写一个单页表操作系统，我会选择在发生进程切换时更换页表，因为这是最频繁的上下文切换情况之一。在其他情况下，如从用户态切换到内核态，可能不需要更换整张页表，而只需修改页表项的权限位来实现访问控制。这样可以减少页表更换的开销，提高性能。</p>
<h1 id="附录">附录</h1>
<p>修改user项目中的makefile，删除ch4_</p>
]]></content:encoded>
    </item>
    <item>
      <title>理解虚拟内存</title>
      <link>https://lifeislife.cn/posts/%E7%90%86%E8%A7%A3%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/</link>
      <pubDate>Sun, 17 Jul 2022 21:45:20 +0000</pubDate>
      <guid>https://lifeislife.cn/posts/%E7%90%86%E8%A7%A3%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/</guid>
      <description>&lt;h2 id=&#34;为什么需要虚拟内存&#34;&gt;为什么需要虚拟内存？&lt;/h2&gt;
&lt;p&gt;CPU 访问内存的最自然的方式就是使用物理地址，这种方式称为&lt;strong&gt;物理寻址&lt;/strong&gt;。1，计算机中并不是只有一个程序在运行，如果它们都是用物理寻址的方式，那么所有程序必须在链接之前确定好自己所用到的内存范围，否则两个程序就可能会发生冲突。2，程序大于内存的问题早在上世纪六十年代就出现，后来出现了&lt;strong&gt;覆盖技术&lt;/strong&gt;（Overlay），把程序分割成许多片段。程序开始执行时，将覆盖管理模块装入内存，该管理模块立即装入并运行覆盖 0。执行完成后，覆盖 0 通知管理模块装入覆盖 1，或者占用覆盖 0 的上方位置（如果有空间），或者占用覆盖 0（如果没有空间）。把一个大程序分割成小的、模块化的片段是非常费时和枯燥的，并且易于出错。很少程序员擅长使用覆盖技术。&lt;/p&gt;
&lt;p&gt;为了更加有效地管理内存并且少出错，现代系统提供了一种对主存的抽象概念，叫做&lt;strong&gt;虚拟内存&lt;/strong&gt;(VM)。主要有三个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它将主存看成是一个存储在磁盘上的地址空间的高速缓存，在主存中只保存活动区域，并根据需要在磁盘和主存之间来回传送数据，通过这种方式，它&lt;strong&gt;高效地使用了主存&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它为每个进程提供了一致的地址空间，从而&lt;strong&gt;简化了内存管理&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它&lt;strong&gt;保护了每个进程&lt;/strong&gt;的地址空间不被其他进程破坏。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;什么是虚拟寻址&#34;&gt;什么是虚拟寻址？&lt;/h2&gt;
&lt;p&gt;如果主存被分为长度为$M$的单字节大小的数组，每个字节都对应一个物理地址，CPU 通过这个唯一的地址访问主存，这样的方式就是&lt;strong&gt;物理寻址&lt;/strong&gt;。


&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212125636.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212125636.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
现代处理器使用&lt;strong&gt;虚拟寻址&lt;/strong&gt;的方式。CPU 通过生成的&lt;strong&gt;虚拟地址&lt;/strong&gt;来访问内存，这个地址在送到内存之前会被转换成&lt;strong&gt;物理地址&lt;/strong&gt;。这个过程称为&lt;strong&gt;地址翻译&lt;/strong&gt;。CPU 芯片上叫做&lt;strong&gt;内存管理单元&lt;/strong&gt;（Memory Management Unit, MMU）的专用硬件，利用存放在主存中的&lt;strong&gt;查询表&lt;/strong&gt;来动态翻译虚拟地址，该表的内容由操作系统管理。


&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212128486.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212128486.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;h2 id=&#34;虚拟内存作为缓存的工具&#34;&gt;虚拟内存作为缓存的工具&lt;/h2&gt;
&lt;p&gt;概念上而言，虚拟内存被组织成为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组，也就是&lt;strong&gt;字节数组&lt;/strong&gt;。每个字节都有一个唯一的虚拟地址作为数组的索引。磁盘上活动的数组内容被缓存在主存中。在存储器结构中，较低层次上的磁盘的数据被分割成块，这些块作为和较高层次的主存之间的传输单元。&lt;strong&gt;主存作为虚拟内存的缓存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虚拟内存被分割为大小固定的块，这些块叫&lt;strong&gt;虚拟页&lt;/strong&gt;（Virtual Page，VP），类似的物理内存也有&lt;strong&gt;物理页&lt;/strong&gt;(Physical Page, PP)。虚拟页有三种不同的状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未分配：VM 系统还未分配 (或者创建）的页。未分配的块没有任何数据和它们相关联，因此也就&lt;strong&gt;不占用任何磁盘空间&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;已缓存：当前已缓存在物理内存中的已分配页。&lt;/li&gt;
&lt;li&gt;未缓存：未缓存在物理内存中的已分配页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了有助于清晰理解存储层次结构中不同的缓存概念，我们将使用术语&lt;strong&gt;SRAM&lt;/strong&gt;缓存来表示位于 CPU 和主存之间的 Ll、L2 和 L3 &lt;strong&gt;高速缓存&lt;/strong&gt;，并且用术语 &lt;strong&gt;DRAM&lt;/strong&gt; 缓存来表示&lt;strong&gt;虚拟内存系统的缓存&lt;/strong&gt;，它在主存中缓存虚拟页。&lt;/p&gt;
&lt;p&gt;在存储层次结构中，DRAM 缓存的位置对它的组织结构有很大的影响。回想一下，DRAM 比 SRAM 要慢大约 10 倍，而磁盘要比 DRAM 慢大约 100000 多倍。因此，&lt;strong&gt;DRAM 缓存中的不命中比起 SRAM 缓存中的不命中要昂贵得多&lt;/strong&gt;。因此，与硬件对 SRAM 缓存相比，操作系统对 DRAM 缓存使用了更复杂精密的替换算法。（这些替换算法超出了我们的讨论范围）。最后，因为对磁盘的访问时间很长，&lt;strong&gt;DRAM 缓存总是使用写回，而不是直写&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;页表&#34;&gt;页表&lt;/h3&gt;
&lt;p&gt;虚拟内存系统可以完成以下这些功能，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判定一个虚拟页是否缓存在 DRAM 中的某个地方；&lt;/li&gt;
&lt;li&gt;可以确定这个虚拟页存放在哪个物理页中；&lt;/li&gt;
&lt;li&gt;如果不命中，系统必须判断这个虚拟页存放在磁盘的哪个位置，在物理内存中选择一个牺牲页，并将虚拟页从磁盘复制到 DRAM 中，替换这个牺牲页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些功能是由&lt;strong&gt;软硬件联合提供的&lt;/strong&gt;，包括操作系统软件、MMU（内存管理单元）中的地址翻译硬件和一个&lt;strong&gt;存放在物理内存中叫做页表&lt;/strong&gt;（page table）的数据结构。页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时，都会读取页表。操作系统负责维护页表的内容，以及在磁盘与 DRAM 之间来回传送页。&lt;/p&gt;
&lt;p&gt;图 9-4 展示了一个页表的基本组织结构。页表就是一个页表条目（Page Table Entry，PTE）的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。&lt;/p&gt;
&lt;p&gt;PTE 由两部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有效位：表明了该虚拟页当前是否被缓存在 DRAM 中；&lt;/li&gt;
&lt;li&gt;地址：表示 DRAM 中相应的物理页的起始位置，这个物理页中缓存了该虚拟页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208122157633.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208122157633.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;h3 id=&#34;页命中与缺页&#34;&gt;页命中与缺页&lt;/h3&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208130940717.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208130940717.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;l&lt;/p&gt;
&lt;p&gt;当 CPU 访问已被缓存的地址时，就叫做&lt;strong&gt;页命中&lt;/strong&gt;。如访问上图 VP2，虚拟地址索引到 PTE2，此时有效位为 1，地址翻译硬件就知道该地址被缓存了。&lt;/p&gt;
&lt;p&gt;当 CPU 访问未被缓存的地址时，会导致&lt;strong&gt;缺页&lt;/strong&gt;。如访问上图的 VP3，虚拟地址索引到 PTE3，此时有效位为 0，地址翻译硬件就知道该地址未被缓存，需要从磁盘中读取。&lt;/p&gt;
&lt;p&gt;这时会触发一个&lt;strong&gt;缺页异常&lt;/strong&gt;。&lt;strong&gt;缺页异常调用内核中的缺页异常处理程序&lt;/strong&gt;，该程序会选择一个牺牲页，在此例中就是存放在 PP 3 中的 VP 4。如果 VP 4 已经被修改了，那么内核就会将它复制回磁盘。无论哪种情况，内核都会修改 VP 4 的页表条目，反映出 VP 4 不再缓存在主存中这一事实。&lt;/p&gt;
&lt;p&gt;接下来，内核从磁盘复制 VP 3 到内存中的 PP 3，更新 PTE 3，随后返回。当异常处理程序返回时，它会&lt;strong&gt;重新启动导致缺页的指令&lt;/strong&gt;，该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在，VP 3 已经缓存在主存中了，那么页命中也能由地址翻译硬件正常处理了。图 9-7 展示了在缺页之后我们的示例页表的状态。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131434643.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131434643.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;在虚拟内存的习惯说法中，块被称为页。在磁盘和内存之间传送页的活动叫做&lt;strong&gt;交换&lt;/strong&gt;（swapping）或者&lt;strong&gt;页面调度&lt;/strong&gt;（paging）。页从磁盘换入（或者页面调入）DRAM 和从 DRAM 换出（或者页面调出）磁盘。一直等待，直到最后时刻，也就是当有不命中发生时，才换入页面的这种策略称为按需页面调度（demand paging）。&lt;/p&gt;
&lt;h2 id=&#34;虚拟内存作为内存管理的工具&#34;&gt;虚拟内存作为内存管理的工具&lt;/h2&gt;
&lt;p&gt;之前我们只讨论了一个页表的情况，但是实际上操作系统为&lt;strong&gt;每个进程都分配了一个独立的页表&lt;/strong&gt;。多个虚拟页面可以映射到同一个共享物理页面上。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131441255.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131441255.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;按需页面调度和独立的虚拟地址空间的结合，对系统中内存的使用和管理造成了深远的影响。特别地，VM 简化了链接和加载、代码和数据共享，以及应用程序的内存分配。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简化链接&lt;/strong&gt;。独立的地址空间允许每个进程的内存映像使用相同的基本格式，而&lt;strong&gt;不管代码和数据实际存放在物理内存的何处&lt;/strong&gt;。例如，一个给定的 Linux 系统上的每个进程都使用类似的内存格式。对于 64 位地址空间，代码段总是从虚拟地址 0x400000 开始。数据段跟在代码段之后，中间有一段符合要求的对齐空白。栈占据用户进程地址空间最高的部分，并向下生长。这样的一致性极大地简化了链接器的设计和实现，&lt;strong&gt;允许链接器生成完全链接的可执行文件，这些可执行文件是独立于物理内存中代码和数据的最终位置的&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化加载&lt;/strong&gt;。虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中 .text 和 .data 节加载到一个新创建的进程中，Linux 加载器为代码和数据段分配虚拟页，把它们标记为无效的（即未被缓存的），将页表条目指向目标文件中适当的位置。有趣的是，加载器从不从磁盘到内存实际复制任何数据。在每个页初次被引用时，要么是 CPU 取指令时引用的，要么是一条正在执行的指令引用一个内存位置时引用的，虚拟内存系统会按照需要自动地调入数据页。将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作内存映射（memory mapping）。Linux 提供一个称为 mmap 的系统调用，允许应用程序自己做内存映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化共享&lt;/strong&gt;。独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。一般而言，每个进程都有自己私有的代码、数据、堆以及栈区域，是不和其他进程共享的。在这种情况中，操作系统创建页表，将相应的虚拟页映射到不连续的物理页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化内存分配&lt;/strong&gt;。虚拟内存为向用户进程提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时（如调用 malloc 的结果），&lt;strong&gt;操作系统分配一个适当数字（例如 k）个连续的虚拟内存页面，并且将它们映射到物理内存中任意位置的 k 个任意的物理页面&lt;/strong&gt;。由于页表工作的方式，操作系统没有必要分配 k 个连续的物理内存页面。&lt;strong&gt;页面可以随机地分散在物理内存中&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;虚拟内存作为内存保护的工具&#34;&gt;虚拟内存作为内存保护的工具&lt;/h2&gt;
&lt;p&gt;操作系统中的用户程序不应该修改只读的代码段，也不应该读取或者修改内核中的代码和数据结构或者访问私有的以及其他的进程的内存，如果无法对用户进程的内存访问进行限制，攻击者就可以访问和修改其他进程的内存影响系统的安全。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131522719.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131522719.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;通过在页表中添加页面的保护属性，可以让操作系统在页面被访问时进行检查，如果页面被保护为只读，则操作系统会报错。&lt;/p&gt;
&lt;p&gt;在图 9-10 这个示例中，每个 PTE 中已经添加了三个许可位。SUP 位表示进程是否必须运行在内核（超级用户）模式下才能访问该页。运行在内核模式中的进程可以访问任何页面，但是运行在用户模式中的进程只允许访问那些 SUP 为 0 的页面。READ 位和 WRITE 位控制对页面的读和写访问。例如，如果进程 i 运行在用户模式下，那么它有读 VP 0 和读写 VP 1 的权限。然而，不允许它访问 VP 2。&lt;/p&gt;
&lt;p&gt;如果一条指令违反了这些许可条件，那么 CPU 就触发一个一般保护故障，将控制传递给一个内核中的异常处理程序。Linux shell 一般将这种异常报告为&lt;strong&gt;段错误&lt;/strong&gt;（segmentation fault）。&lt;/p&gt;
&lt;h2 id=&#34;地址翻译&#34;&gt;地址翻译&lt;/h2&gt;
&lt;p&gt;基本参数&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;符号&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;描述&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;$$\small N=2^n$$&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;虚拟地址空间中的地址数量&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;$$\small M=2^m$$&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;物理地址空间中的地址数量&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;$$\small P=2^p$$&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;页的大小（字节）&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;虚拟地址（VA）的组成部分&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;符号&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;描述&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;VPO&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;虚拟页面偏移量（字节）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;VPN&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;虚拟页号&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;TLBI&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;TLB 索引&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;TLBT&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;TLB 标记&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;物理地址（PA）的组成部分&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;符号&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;描述&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;PPO&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;物理页面偏移量（字节）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;PPN&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;物理页号&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;CO&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;缓冲块内的字节偏移量&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;CI&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;高速缓存索引&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;CT&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;高速缓存标记&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527131.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527131.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;图 9-12 展示了 MMU 如何利用页表来实现地址翻译。CPU 中的一个控制寄存器，&lt;strong&gt;页表基址寄存器&lt;/strong&gt;（Page Table Base Register，PTBR）指向当前页表。$n$ 位的虚拟地址包含两个部分：一个 $p$ 位的&lt;strong&gt;虚拟页面偏移&lt;/strong&gt;（Virtual Page Offset，VPO）和一个$\small (n-p)$位的&lt;strong&gt;虚拟页号&lt;/strong&gt;（Virtual Page Number，VPN）。MMU 利用 VPN 来选择适当的 PTE。例如，VPN 0 选择 PTE 0，VPN 1 选择 PTE 1，以此类推。将页表条目中&lt;strong&gt;物理页号&lt;/strong&gt;（Physical Page Number，PPN）和虚拟地址中的 VP。串联起来，就得到相应的物理地址。注意，因为物理和虚拟页面都是 P 字节的，所以&lt;strong&gt;物理页面偏移（Physical Page Offset，PPO）和 VPO 是相同的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527491.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527491.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;图 9-13a 展示了当页面命中时，CPU 硬件执行的步骤。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 步：处理器生成一个
虚拟地址，并把它传送给 MMU。&lt;/li&gt;
&lt;li&gt;第 2 步：MMU 生成 PTE 地址，并从高速缓存/主存请求得到它。&lt;/li&gt;
&lt;li&gt;第 3 步：高速缓存/主存向 MMU 返回 PTE。&lt;/li&gt;
&lt;li&gt;第 4 步：MMU 构造物理地址，并把它传送给高速缓存/主存。&lt;/li&gt;
&lt;li&gt;第 5 步：高速缓存/主存返回所请求的数据字给处理器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;页面命中完全是由硬件来处理的，与之不同的是，处理缺页要求硬件和操作系统内核协作完成，如图 9-13b 所示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 - 3 步：和图 9-13a 中的第 1 步到第 3 步相同。&lt;/li&gt;
&lt;li&gt;第 4 步：PTE 中的有效位是零，所以 MMU 触发了一次异常，传递 CPU 中的控制到操作系统内核中的缺页异常处理程序。&lt;/li&gt;
&lt;li&gt;第 5 步：缺页处理程序确定出物理内存中的牺牲页，如果这个页面已经被修改了，则把它换出到磁盘。&lt;/li&gt;
&lt;li&gt;第 6 步：缺页处理程序页面调入新的页面，并更新内存中的 PTE。&lt;/li&gt;
&lt;li&gt;第 7 步：缺页处理程序返回到原来的进程，再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重新发送给 MMU。因为虚拟页面现在缓存在物理内存中，所以就会命中，在 MMU 执行了图 9-13b 中的步骤之后，主存就会将所请求字返回给处理器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;利用-tlb-加速地址翻译&#34;&gt;利用 TLB 加速地址翻译&lt;/h3&gt;
&lt;p&gt;每次 CPU 访问一个虚拟地址，MMU 就必须查找 PTE，以便将虚拟地址翻译为物理地址。在最糟糕的情况下，这会要求从内存多取一次数据，代价是几十到几百个周期。如果 PTE 碰巧缓存在 L1 中，那么开销就下降到 1 个或 2 个周期。为了消除这样的开销，在 MMU 中包括了一个关于 PTE 的小的缓存，称为&lt;strong&gt;翻译后备缓冲器&lt;/strong&gt;（Translation Lookaside Buffer，TLB）。&lt;/p&gt;
&lt;p&gt;TLB 是一个小的、虚拟寻址的缓存，其中&lt;strong&gt;每一行都保存着一个由单个 PTE 组成的块&lt;/strong&gt;。TLB 通常有高度的相联度。如图 9-15 所示，用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果 TLB 有$\small T = 2^t$个组，那么 &lt;strong&gt;TLB 索引&lt;/strong&gt;（TLBI）是由 VPN 的 $t$ 个最低位组成的，而 &lt;strong&gt;TLB 标记&lt;/strong&gt;（TLBT）是由 VPN 中剩余的位组成的。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131546427.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131546427.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;图 9-16a 展示了当 TLB 命中时（通常情况）所包括的步骤。这里的关键点是，所有的地址翻译步骤都是在芯片上的 MMU 中执行的，因此非常快。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 步：CPU 产生一个虚拟地址。&lt;/li&gt;
&lt;li&gt;第 2 - 3 步：MMU 从 TLB 中取出相应的 PTE。&lt;/li&gt;
&lt;li&gt;第 4 步：MMU 将这个虚拟地址翻译成一个物理地址，并且将它发送到高速缓存/主存。&lt;/li&gt;
&lt;li&gt;第 5 步：高速缓存/主存将所请求的数据字返回给 CPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131547576.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131547576.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;当 TLB 不命中时，MMU 必须从 L1 缓存中取出相应的 PTE，如图 9-16b 所示。新取出的 PTE 存放在 TLB 中，可能会覆盖一个已经存在的条目。&lt;/p&gt;
&lt;h3 id=&#34;多级页表&#34;&gt;多级页表&lt;/h3&gt;
&lt;p&gt;32 位环境下，虚拟地址空间共 4GB。如果分成 4KB 一个页，那就是 1M 个页。每个页表项需要 4 个字节来存储，那么整个 4GB 空间的映射就需要 4MB 的内存来存储映射表。如果每个进程都有自己的映射表，100 个进程就需要 400MB 的内存。对于内核来讲，有点大了。&lt;/p&gt;
&lt;p&gt;页表中所有页表项必须提前建好，并且要求是连续的。如果不连续，就没有办法通过虚拟地址里面的页号找到对应的页表项了。&lt;/p&gt;
&lt;p&gt;那怎么办呢？我们可以试着将页表再分页，4G 的空间需要 4M 的页表来存储映射。我们把这 4M 分成 1K（1024）个 4K，每个 4K 又能放在一页里面，这样 1K 个 4K 就是 1K 个页，这 1K 个页也需要一个表进行管理，我们称为页目录表，这个页目录表里面有 1K 项，每项 4 个字节，页目录表大小也是 4K。&lt;/p&gt;
&lt;p&gt;页目录有 1K 项，用 10 位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项，也即 4K 的页表项。每个页表项也是 4 个字节，因而一整页的页表项是 1K 个。再用 10 位就可以表示访问页表项的哪一项，页表项中的一项对应的就是一个页，是存放数据的页，这个页的大小是 4K，用 12 位可以定位这个页内的任何一个位置。&lt;/p&gt;
&lt;p&gt;这样加起来正好 32 位，也就是用前 10 位定位到页目录表中的一项。将这一项对应的页表取出来共 1k 项，再用中间 10 位定位到页表中的一项，将这一项对应的存放数据的页取出来，再用最后 12 位定位到页中的具体位置访问数据。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/20211129192245.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/20211129192245.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
&lt;p&gt;你可能会问，如果这样的话，映射 4GB 地址空间就需要 4MB+4KB 的内存，这样不是更大 了吗？当然如果页是满的，当时是更大了，但是，我们往往不会为一个进程分配那么多内 存。
比如说，上面图中，我们假设只给这个进程分配了一个数据页。如果只使用页表，也需要完 整的 1M 个页表项共 4M 的内存，但是如果使用了页目录，页目录需要 1K 个全部分配，占用内存 4K，但是里面只有一项使用了。到了页表项，只需要分配能够管理那个数据页的页表项页就可以了，也就是说，最多 4K，这样内存就节省多了&lt;/p&gt;
&lt;p&gt;当然对于 64 位的系统，两级肯定不够了，就变成了四级目录，分别是全局页目录项 PGD（Page Global Directory）、上层页目录项 PUD（Page Upper Directory）、中间
页目录项 PMD（Page Middle Directory）和页表项 PTE（Page Table Entry）。也就是一级页表，二级页表，三级页表，四级页表。&lt;/p&gt;
&lt;p&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&#34;en&#34;&gt;
&lt;head&gt;
    &lt;meta charset=&#34;UTF-8&#34;&gt;
    &lt;meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;&gt;
    &lt;title&gt;Responsive Image&lt;/title&gt;
    &lt;style&gt;
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    
    &lt;div class=&#34;post-img-view&#34;&gt;
        &lt;a data-fancybox=&#34;gallery&#34; href=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131820091.png&#34;&gt;
            &lt;img class=&#34;responsive-image&#34; src=&#34;https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131820091.png&#34; alt=&#34;&#34;  style=&#34;margin: 0 auto;&#34;/&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    

    &lt;script&gt;
        document.addEventListener(&#34;DOMContentLoaded&#34;, function() {
            var images = document.querySelectorAll(&#34;.responsive-image&#34;);
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + &#34;px&#34;;
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h2 id="为什么需要虚拟内存">为什么需要虚拟内存？</h2>
<p>CPU 访问内存的最自然的方式就是使用物理地址，这种方式称为<strong>物理寻址</strong>。1，计算机中并不是只有一个程序在运行，如果它们都是用物理寻址的方式，那么所有程序必须在链接之前确定好自己所用到的内存范围，否则两个程序就可能会发生冲突。2，程序大于内存的问题早在上世纪六十年代就出现，后来出现了<strong>覆盖技术</strong>（Overlay），把程序分割成许多片段。程序开始执行时，将覆盖管理模块装入内存，该管理模块立即装入并运行覆盖 0。执行完成后，覆盖 0 通知管理模块装入覆盖 1，或者占用覆盖 0 的上方位置（如果有空间），或者占用覆盖 0（如果没有空间）。把一个大程序分割成小的、模块化的片段是非常费时和枯燥的，并且易于出错。很少程序员擅长使用覆盖技术。</p>
<p>为了更加有效地管理内存并且少出错，现代系统提供了一种对主存的抽象概念，叫做<strong>虚拟内存</strong>(VM)。主要有三个功能：</p>
<ul>
<li>它将主存看成是一个存储在磁盘上的地址空间的高速缓存，在主存中只保存活动区域，并根据需要在磁盘和主存之间来回传送数据，通过这种方式，它<strong>高效地使用了主存</strong>。</li>
<li>它为每个进程提供了一致的地址空间，从而<strong>简化了内存管理</strong>。</li>
<li>它<strong>保护了每个进程</strong>的地址空间不被其他进程破坏。</li>
</ul>
<h2 id="什么是虚拟寻址">什么是虚拟寻址？</h2>
<p>如果主存被分为长度为$M$的单字节大小的数组，每个字节都对应一个物理地址，CPU 通过这个唯一的地址访问主存，这样的方式就是<strong>物理寻址</strong>。


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212125636.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212125636.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html>
现代处理器使用<strong>虚拟寻址</strong>的方式。CPU 通过生成的<strong>虚拟地址</strong>来访问内存，这个地址在送到内存之前会被转换成<strong>物理地址</strong>。这个过程称为<strong>地址翻译</strong>。CPU 芯片上叫做<strong>内存管理单元</strong>（Memory Management Unit, MMU）的专用硬件，利用存放在主存中的<strong>查询表</strong>来动态翻译虚拟地址，该表的内容由操作系统管理。


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212128486.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202207212128486.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<h2 id="虚拟内存作为缓存的工具">虚拟内存作为缓存的工具</h2>
<p>概念上而言，虚拟内存被组织成为一个由存放在磁盘上的 N 个连续的字节大小的单元组成的数组，也就是<strong>字节数组</strong>。每个字节都有一个唯一的虚拟地址作为数组的索引。磁盘上活动的数组内容被缓存在主存中。在存储器结构中，较低层次上的磁盘的数据被分割成块，这些块作为和较高层次的主存之间的传输单元。<strong>主存作为虚拟内存的缓存</strong>。</p>
<p>虚拟内存被分割为大小固定的块，这些块叫<strong>虚拟页</strong>（Virtual Page，VP），类似的物理内存也有<strong>物理页</strong>(Physical Page, PP)。虚拟页有三种不同的状态：</p>
<ul>
<li>未分配：VM 系统还未分配 (或者创建）的页。未分配的块没有任何数据和它们相关联，因此也就<strong>不占用任何磁盘空间</strong>。</li>
<li>已缓存：当前已缓存在物理内存中的已分配页。</li>
<li>未缓存：未缓存在物理内存中的已分配页。</li>
</ul>
<p>为了有助于清晰理解存储层次结构中不同的缓存概念，我们将使用术语<strong>SRAM</strong>缓存来表示位于 CPU 和主存之间的 Ll、L2 和 L3 <strong>高速缓存</strong>，并且用术语 <strong>DRAM</strong> 缓存来表示<strong>虚拟内存系统的缓存</strong>，它在主存中缓存虚拟页。</p>
<p>在存储层次结构中，DRAM 缓存的位置对它的组织结构有很大的影响。回想一下，DRAM 比 SRAM 要慢大约 10 倍，而磁盘要比 DRAM 慢大约 100000 多倍。因此，<strong>DRAM 缓存中的不命中比起 SRAM 缓存中的不命中要昂贵得多</strong>。因此，与硬件对 SRAM 缓存相比，操作系统对 DRAM 缓存使用了更复杂精密的替换算法。（这些替换算法超出了我们的讨论范围）。最后，因为对磁盘的访问时间很长，<strong>DRAM 缓存总是使用写回，而不是直写</strong>。</p>
<h3 id="页表">页表</h3>
<p>虚拟内存系统可以完成以下这些功能，</p>
<ul>
<li>判定一个虚拟页是否缓存在 DRAM 中的某个地方；</li>
<li>可以确定这个虚拟页存放在哪个物理页中；</li>
<li>如果不命中，系统必须判断这个虚拟页存放在磁盘的哪个位置，在物理内存中选择一个牺牲页，并将虚拟页从磁盘复制到 DRAM 中，替换这个牺牲页。</li>
</ul>
<p>这些功能是由<strong>软硬件联合提供的</strong>，包括操作系统软件、MMU（内存管理单元）中的地址翻译硬件和一个<strong>存放在物理内存中叫做页表</strong>（page table）的数据结构。页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时，都会读取页表。操作系统负责维护页表的内容，以及在磁盘与 DRAM 之间来回传送页。</p>
<p>图 9-4 展示了一个页表的基本组织结构。页表就是一个页表条目（Page Table Entry，PTE）的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。</p>
<p>PTE 由两部分组成：</p>
<ul>
<li>有效位：表明了该虚拟页当前是否被缓存在 DRAM 中；</li>
<li>地址：表示 DRAM 中相应的物理页的起始位置，这个物理页中缓存了该虚拟页。</li>
</ul>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208122157633.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208122157633.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<h3 id="页命中与缺页">页命中与缺页</h3>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208130940717.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208130940717.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html>l</p>
<p>当 CPU 访问已被缓存的地址时，就叫做<strong>页命中</strong>。如访问上图 VP2，虚拟地址索引到 PTE2，此时有效位为 1，地址翻译硬件就知道该地址被缓存了。</p>
<p>当 CPU 访问未被缓存的地址时，会导致<strong>缺页</strong>。如访问上图的 VP3，虚拟地址索引到 PTE3，此时有效位为 0，地址翻译硬件就知道该地址未被缓存，需要从磁盘中读取。</p>
<p>这时会触发一个<strong>缺页异常</strong>。<strong>缺页异常调用内核中的缺页异常处理程序</strong>，该程序会选择一个牺牲页，在此例中就是存放在 PP 3 中的 VP 4。如果 VP 4 已经被修改了，那么内核就会将它复制回磁盘。无论哪种情况，内核都会修改 VP 4 的页表条目，反映出 VP 4 不再缓存在主存中这一事实。</p>
<p>接下来，内核从磁盘复制 VP 3 到内存中的 PP 3，更新 PTE 3，随后返回。当异常处理程序返回时，它会<strong>重新启动导致缺页的指令</strong>，该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在，VP 3 已经缓存在主存中了，那么页命中也能由地址翻译硬件正常处理了。图 9-7 展示了在缺页之后我们的示例页表的状态。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131434643.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131434643.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>在虚拟内存的习惯说法中，块被称为页。在磁盘和内存之间传送页的活动叫做<strong>交换</strong>（swapping）或者<strong>页面调度</strong>（paging）。页从磁盘换入（或者页面调入）DRAM 和从 DRAM 换出（或者页面调出）磁盘。一直等待，直到最后时刻，也就是当有不命中发生时，才换入页面的这种策略称为按需页面调度（demand paging）。</p>
<h2 id="虚拟内存作为内存管理的工具">虚拟内存作为内存管理的工具</h2>
<p>之前我们只讨论了一个页表的情况，但是实际上操作系统为<strong>每个进程都分配了一个独立的页表</strong>。多个虚拟页面可以映射到同一个共享物理页面上。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131441255.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131441255.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>按需页面调度和独立的虚拟地址空间的结合，对系统中内存的使用和管理造成了深远的影响。特别地，VM 简化了链接和加载、代码和数据共享，以及应用程序的内存分配。</p>
<ul>
<li><strong>简化链接</strong>。独立的地址空间允许每个进程的内存映像使用相同的基本格式，而<strong>不管代码和数据实际存放在物理内存的何处</strong>。例如，一个给定的 Linux 系统上的每个进程都使用类似的内存格式。对于 64 位地址空间，代码段总是从虚拟地址 0x400000 开始。数据段跟在代码段之后，中间有一段符合要求的对齐空白。栈占据用户进程地址空间最高的部分，并向下生长。这样的一致性极大地简化了链接器的设计和实现，<strong>允许链接器生成完全链接的可执行文件，这些可执行文件是独立于物理内存中代码和数据的最终位置的</strong>。</li>
<li><strong>简化加载</strong>。虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中 .text 和 .data 节加载到一个新创建的进程中，Linux 加载器为代码和数据段分配虚拟页，把它们标记为无效的（即未被缓存的），将页表条目指向目标文件中适当的位置。有趣的是，加载器从不从磁盘到内存实际复制任何数据。在每个页初次被引用时，要么是 CPU 取指令时引用的，要么是一条正在执行的指令引用一个内存位置时引用的，虚拟内存系统会按照需要自动地调入数据页。将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作内存映射（memory mapping）。Linux 提供一个称为 mmap 的系统调用，允许应用程序自己做内存映射。</li>
<li><strong>简化共享</strong>。独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。一般而言，每个进程都有自己私有的代码、数据、堆以及栈区域，是不和其他进程共享的。在这种情况中，操作系统创建页表，将相应的虚拟页映射到不连续的物理页面。</li>
<li><strong>简化内存分配</strong>。虚拟内存为向用户进程提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时（如调用 malloc 的结果），<strong>操作系统分配一个适当数字（例如 k）个连续的虚拟内存页面，并且将它们映射到物理内存中任意位置的 k 个任意的物理页面</strong>。由于页表工作的方式，操作系统没有必要分配 k 个连续的物理内存页面。<strong>页面可以随机地分散在物理内存中</strong>。</li>
</ul>
<h2 id="虚拟内存作为内存保护的工具">虚拟内存作为内存保护的工具</h2>
<p>操作系统中的用户程序不应该修改只读的代码段，也不应该读取或者修改内核中的代码和数据结构或者访问私有的以及其他的进程的内存，如果无法对用户进程的内存访问进行限制，攻击者就可以访问和修改其他进程的内存影响系统的安全。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131522719.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131522719.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>通过在页表中添加页面的保护属性，可以让操作系统在页面被访问时进行检查，如果页面被保护为只读，则操作系统会报错。</p>
<p>在图 9-10 这个示例中，每个 PTE 中已经添加了三个许可位。SUP 位表示进程是否必须运行在内核（超级用户）模式下才能访问该页。运行在内核模式中的进程可以访问任何页面，但是运行在用户模式中的进程只允许访问那些 SUP 为 0 的页面。READ 位和 WRITE 位控制对页面的读和写访问。例如，如果进程 i 运行在用户模式下，那么它有读 VP 0 和读写 VP 1 的权限。然而，不允许它访问 VP 2。</p>
<p>如果一条指令违反了这些许可条件，那么 CPU 就触发一个一般保护故障，将控制传递给一个内核中的异常处理程序。Linux shell 一般将这种异常报告为<strong>段错误</strong>（segmentation fault）。</p>
<h2 id="地址翻译">地址翻译</h2>
<p>基本参数</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">符号</th>
          <th style="text-align: left">描述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">$$\small N=2^n$$</td>
          <td style="text-align: left">虚拟地址空间中的地址数量</td>
      </tr>
      <tr>
          <td style="text-align: left">$$\small M=2^m$$</td>
          <td style="text-align: left">物理地址空间中的地址数量</td>
      </tr>
      <tr>
          <td style="text-align: left">$$\small P=2^p$$</td>
          <td style="text-align: left">页的大小（字节）</td>
      </tr>
  </tbody>
</table>
<p>虚拟地址（VA）的组成部分</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">符号</th>
          <th style="text-align: left">描述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">VPO</td>
          <td style="text-align: left">虚拟页面偏移量（字节）</td>
      </tr>
      <tr>
          <td style="text-align: left">VPN</td>
          <td style="text-align: left">虚拟页号</td>
      </tr>
      <tr>
          <td style="text-align: left">TLBI</td>
          <td style="text-align: left">TLB 索引</td>
      </tr>
      <tr>
          <td style="text-align: left">TLBT</td>
          <td style="text-align: left">TLB 标记</td>
      </tr>
  </tbody>
</table>
<p>物理地址（PA）的组成部分</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">符号</th>
          <th style="text-align: left">描述</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">PPO</td>
          <td style="text-align: left">物理页面偏移量（字节）</td>
      </tr>
      <tr>
          <td style="text-align: left">PPN</td>
          <td style="text-align: left">物理页号</td>
      </tr>
      <tr>
          <td style="text-align: left">CO</td>
          <td style="text-align: left">缓冲块内的字节偏移量</td>
      </tr>
      <tr>
          <td style="text-align: left">CI</td>
          <td style="text-align: left">高速缓存索引</td>
      </tr>
      <tr>
          <td style="text-align: left">CT</td>
          <td style="text-align: left">高速缓存标记</td>
      </tr>
  </tbody>
</table>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527131.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527131.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>图 9-12 展示了 MMU 如何利用页表来实现地址翻译。CPU 中的一个控制寄存器，<strong>页表基址寄存器</strong>（Page Table Base Register，PTBR）指向当前页表。$n$ 位的虚拟地址包含两个部分：一个 $p$ 位的<strong>虚拟页面偏移</strong>（Virtual Page Offset，VPO）和一个$\small (n-p)$位的<strong>虚拟页号</strong>（Virtual Page Number，VPN）。MMU 利用 VPN 来选择适当的 PTE。例如，VPN 0 选择 PTE 0，VPN 1 选择 PTE 1，以此类推。将页表条目中<strong>物理页号</strong>（Physical Page Number，PPN）和虚拟地址中的 VP。串联起来，就得到相应的物理地址。注意，因为物理和虚拟页面都是 P 字节的，所以<strong>物理页面偏移（Physical Page Offset，PPO）和 VPO 是相同的</strong>。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527491.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131527491.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>图 9-13a 展示了当页面命中时，CPU 硬件执行的步骤。</p>
<ul>
<li>第 1 步：处理器生成一个
虚拟地址，并把它传送给 MMU。</li>
<li>第 2 步：MMU 生成 PTE 地址，并从高速缓存/主存请求得到它。</li>
<li>第 3 步：高速缓存/主存向 MMU 返回 PTE。</li>
<li>第 4 步：MMU 构造物理地址，并把它传送给高速缓存/主存。</li>
<li>第 5 步：高速缓存/主存返回所请求的数据字给处理器。</li>
</ul>
<p>页面命中完全是由硬件来处理的，与之不同的是，处理缺页要求硬件和操作系统内核协作完成，如图 9-13b 所示。</p>
<ul>
<li>第 1 - 3 步：和图 9-13a 中的第 1 步到第 3 步相同。</li>
<li>第 4 步：PTE 中的有效位是零，所以 MMU 触发了一次异常，传递 CPU 中的控制到操作系统内核中的缺页异常处理程序。</li>
<li>第 5 步：缺页处理程序确定出物理内存中的牺牲页，如果这个页面已经被修改了，则把它换出到磁盘。</li>
<li>第 6 步：缺页处理程序页面调入新的页面，并更新内存中的 PTE。</li>
<li>第 7 步：缺页处理程序返回到原来的进程，再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重新发送给 MMU。因为虚拟页面现在缓存在物理内存中，所以就会命中，在 MMU 执行了图 9-13b 中的步骤之后，主存就会将所请求字返回给处理器。</li>
</ul>
<h3 id="利用-tlb-加速地址翻译">利用 TLB 加速地址翻译</h3>
<p>每次 CPU 访问一个虚拟地址，MMU 就必须查找 PTE，以便将虚拟地址翻译为物理地址。在最糟糕的情况下，这会要求从内存多取一次数据，代价是几十到几百个周期。如果 PTE 碰巧缓存在 L1 中，那么开销就下降到 1 个或 2 个周期。为了消除这样的开销，在 MMU 中包括了一个关于 PTE 的小的缓存，称为<strong>翻译后备缓冲器</strong>（Translation Lookaside Buffer，TLB）。</p>
<p>TLB 是一个小的、虚拟寻址的缓存，其中<strong>每一行都保存着一个由单个 PTE 组成的块</strong>。TLB 通常有高度的相联度。如图 9-15 所示，用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果 TLB 有$\small T = 2^t$个组，那么 <strong>TLB 索引</strong>（TLBI）是由 VPN 的 $t$ 个最低位组成的，而 <strong>TLB 标记</strong>（TLBT）是由 VPN 中剩余的位组成的。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131546427.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131546427.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>图 9-16a 展示了当 TLB 命中时（通常情况）所包括的步骤。这里的关键点是，所有的地址翻译步骤都是在芯片上的 MMU 中执行的，因此非常快。</p>
<ul>
<li>第 1 步：CPU 产生一个虚拟地址。</li>
<li>第 2 - 3 步：MMU 从 TLB 中取出相应的 PTE。</li>
<li>第 4 步：MMU 将这个虚拟地址翻译成一个物理地址，并且将它发送到高速缓存/主存。</li>
<li>第 5 步：高速缓存/主存将所请求的数据字返回给 CPU。</li>
</ul>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131547576.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131547576.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>当 TLB 不命中时，MMU 必须从 L1 缓存中取出相应的 PTE，如图 9-16b 所示。新取出的 PTE 存放在 TLB 中，可能会覆盖一个已经存在的条目。</p>
<h3 id="多级页表">多级页表</h3>
<p>32 位环境下，虚拟地址空间共 4GB。如果分成 4KB 一个页，那就是 1M 个页。每个页表项需要 4 个字节来存储，那么整个 4GB 空间的映射就需要 4MB 的内存来存储映射表。如果每个进程都有自己的映射表，100 个进程就需要 400MB 的内存。对于内核来讲，有点大了。</p>
<p>页表中所有页表项必须提前建好，并且要求是连续的。如果不连续，就没有办法通过虚拟地址里面的页号找到对应的页表项了。</p>
<p>那怎么办呢？我们可以试着将页表再分页，4G 的空间需要 4M 的页表来存储映射。我们把这 4M 分成 1K（1024）个 4K，每个 4K 又能放在一页里面，这样 1K 个 4K 就是 1K 个页，这 1K 个页也需要一个表进行管理，我们称为页目录表，这个页目录表里面有 1K 项，每项 4 个字节，页目录表大小也是 4K。</p>
<p>页目录有 1K 项，用 10 位就可以表示访问页目录的哪一项。这一项其实对应的是一整页的页表项，也即 4K 的页表项。每个页表项也是 4 个字节，因而一整页的页表项是 1K 个。再用 10 位就可以表示访问页表项的哪一项，页表项中的一项对应的就是一个页，是存放数据的页，这个页的大小是 4K，用 12 位可以定位这个页内的任何一个位置。</p>
<p>这样加起来正好 32 位，也就是用前 10 位定位到页目录表中的一项。将这一项对应的页表取出来共 1k 项，再用中间 10 位定位到页表中的一项，将这一项对应的存放数据的页取出来，再用最后 12 位定位到页中的具体位置访问数据。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/20211129192245.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/20211129192245.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
<p>你可能会问，如果这样的话，映射 4GB 地址空间就需要 4MB+4KB 的内存，这样不是更大 了吗？当然如果页是满的，当时是更大了，但是，我们往往不会为一个进程分配那么多内 存。
比如说，上面图中，我们假设只给这个进程分配了一个数据页。如果只使用页表，也需要完 整的 1M 个页表项共 4M 的内存，但是如果使用了页目录，页目录需要 1K 个全部分配，占用内存 4K，但是里面只有一项使用了。到了页表项，只需要分配能够管理那个数据页的页表项页就可以了，也就是说，最多 4K，这样内存就节省多了</p>
<p>当然对于 64 位的系统，两级肯定不够了，就变成了四级目录，分别是全局页目录项 PGD（Page Global Directory）、上层页目录项 PUD（Page Upper Directory）、中间
页目录项 PMD（Page Middle Directory）和页表项 PTE（Page Table Entry）。也就是一级页表，二级页表，三级页表，四级页表。</p>
<p>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Responsive Image</title>
    <style>
        .post-img-view {
            text-align: center;
        }
        .responsive-image {
            display: block;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    
    <div class="post-img-view">
        <a data-fancybox="gallery" href="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131820091.png">
            <img class="responsive-image" src="https://picbed-1311007548.cos.ap-shanghai.myqcloud.com/markdown_picbed/img/202208131820091.png" alt=""  style="margin: 0 auto;"/>
        </a>
    </div>
    

    <script>
        document.addEventListener("DOMContentLoaded", function() {
            var images = document.querySelectorAll(".responsive-image");
            var maxHeight = window.innerHeight / 3;
            images.forEach(function(image) {
                image.style.maxHeight = maxHeight + "px";
            });
        });
    </script>
</body>
</html></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
