<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Nay&apos;s Blog</title><description>Let me drive your world!</description><link>https://chenmiaoi.github.io/</link><language>en</language><item><title>GSOC2025: How To Support jump_label For OpenRISC</title><link>https://chenmiaoi.github.io/2025/06/15/GSOC2025-How-To-Support-jump-label-For-OpenRISC/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2025/06/15/GSOC2025-How-To-Support-jump-label-For-OpenRISC/</guid><pubDate>Sun, 15 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;jump_label&lt;/code&gt;是Linux内核中的一种机制，用于优化频繁切换的分支代码（如 if-else、switch 等），特别是在动态启停的调试或性能监控场景中。其核心思想是通过运行时动态修改代码，将条件分支转换为无条件跳转或直接空操作(&lt;code&gt;NOP&lt;/code&gt;)，从而减少分支预测开销，提升性能(&lt;s&gt;By DeepSeek&lt;/s&gt;)。&lt;/p&gt;
&lt;p&gt;在过去的几周内，我通过学习&lt;code&gt;jump_label&lt;/code&gt;的各种博客，源码剖析以及对应的内核文档。了解到移植并实现&lt;code&gt;jump_label&lt;/code&gt;需要的步骤。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.kernel.org/doc/html/latest/staging/static-keys.html&quot;&gt;Linux Kernel Documentation about static_key&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/699724456&quot;&gt;【Linux内核|代码技巧】ARM64 jump label源码分析&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/JiMoKuangXiangQu/articles/18812838&quot;&gt;Linux：Jump label 实现简析 - JiMoKuangXiangQu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/dog250/article/details/106715700&quot;&gt;Linux内核jump label与static key的原理与示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hyuuhit.com/2025/05/05/static-key-jump-label/&quot;&gt;static key &amp;amp; jump label | 属乌鸦的&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在内核的文档中，有关于最为基础的&lt;code&gt;jump_label&lt;/code&gt;实现顺序：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250616231404.png&quot; alt=&quot;linux documentation jump_label&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HAVE_ARCH_JUMP_LABEL&lt;/code&gt;是内核中决定是否开启&lt;code&gt;static_key&lt;/code&gt;的第一步，这指出当前硬件架构是否实现了&lt;code&gt;jump_label&lt;/code&gt;所需的底层支持。而&lt;code&gt;JUMP_LABEL_NOP_SIZE&lt;/code&gt;则是为了确保后续跳转指令和NOP指令替换时，指令大小一致(如果不一致，则会导致指令撕裂等问题)。&lt;/p&gt;
&lt;p&gt;可能单单讲解概念会比较模糊，我们接下来先看看对应源码的剖析进而来分析如何移植OpenRISC的&lt;code&gt;jump_label&lt;/code&gt;实现。&lt;/p&gt;
&lt;h2&gt;JumpLabel Source Code For RISC-V&lt;/h2&gt;
&lt;p&gt;要了解内核中&lt;code&gt;jump_label&lt;/code&gt;的整体运行流程，我们就需要找到&lt;code&gt;jump_label&lt;/code&gt;的运行入口(&lt;code&gt;jump_label_init&lt;/code&gt;)。但这在之前，我们还需要了解到一个概念，&lt;code&gt;jump_label&lt;/code&gt;实际上是一张表格，为了实现快速跳转，内核将每一个跳转点(也就是&lt;code&gt;static_key&lt;/code&gt;)的信息都记录在&lt;code&gt;__jump_table&lt;/code&gt;中，分别由&lt;code&gt;__start___jump_table&lt;/code&gt;和&lt;code&gt;__stop___jump_table&lt;/code&gt;控制起始和结尾地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define JUMP_TABLE_DATA							\
	. = ALIGN(8);							\
	BOUNDED_SECTION_BY(__jump_table, ___jump_table)
	
/*
 * Allow architectures to handle ro_after_init data on their
 * own by defining an empty RO_AFTER_INIT_DATA.
 */
#ifndef RO_AFTER_INIT_DATA
#define RO_AFTER_INIT_DATA						\
	. = ALIGN(8);							\
	__start_ro_after_init = .;					\
	*(.data..ro_after_init)						\
	JUMP_TABLE_DATA							\
	STATIC_CALL_DATA						\
	__end_ro_after_init = .;
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码是内核对内存布局的指示，其中就包含了&lt;code&gt;jump_table&lt;/code&gt;的布局信息。我们可以看到，&lt;code&gt;jump_table&lt;/code&gt;以八字节对齐在内存中，并且其中有一个关键信息&lt;code&gt;RO_AFTER_INIT_DATA&lt;/code&gt;。这一点是在&lt;code&gt;Patch V2&lt;/code&gt;中和Shorne一起检查出的问题。&lt;/p&gt;
&lt;p&gt;了解到这一点后(&lt;code&gt;__jump_table&lt;/code&gt;)，我们现在就可以开始从&lt;code&gt;jump_label_init&lt;/code&gt;开始分析了。&lt;/p&gt;
&lt;h3&gt;JumpLabel Init Stage&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;jump_label_init&lt;/code&gt;函数中，我们忽视掉其他无关代码(并非说它们不重要，而是去除掉这些代码并不会影响理解)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void __init jump_label_init(void)
{
	struct jump_entry *iter_start = __start___jump_table;
	struct jump_entry *iter_stop = __stop___jump_table;
	struct static_key *key = NULL;
	struct jump_entry *iter;
	
	jump_label_sort_entries(iter_start, iter_stop);

	for (iter = iter_start; iter &amp;lt; iter_stop; iter++) {
		struct static_key *iterk;
		bool in_init;

		in_init = init_section_contains((void *)jump_entry_code(iter), 1);
		jump_entry_set_init(iter, in_init);

		iterk = jump_entry_key(iter);
		if (iterk == key)
			continue;

		key = iterk;
		static_key_set_entries(key, iter);
	}
	static_key_initialized = true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，这就是&lt;code&gt;jump_label_init&lt;/code&gt;的核心代码部分。由于&lt;code&gt;jump_label&lt;/code&gt;的功能是通过注入汇编实现的，因此在编译阶段&lt;code&gt;jump_label&lt;/code&gt;的信息便已经写入到&lt;code&gt;__jump_table&lt;/code&gt;中。这里便涉及到上面内核文档中所提到的&lt;code&gt;arch_static_branch&lt;/code&gt;和&lt;code&gt;arch_static_branch_jump&lt;/code&gt;的实现。(此时，&lt;code&gt;jump_entry&lt;/code&gt;的信息都是乱序的)。&lt;/p&gt;
&lt;p&gt;对于一个&lt;code&gt;static_key&lt;/code&gt;来说，每一个&lt;code&gt;static_key&lt;/code&gt;都不止对应了一个&lt;code&gt;jump_entry&lt;/code&gt;因此为了防止重复处理以及性能考虑，内核首先需要对&lt;code&gt;jump_entry&lt;/code&gt;进行排序(通常是按照&lt;code&gt;jump_entry&lt;/code&gt;指向&lt;code&gt;static_key&lt;/code&gt;的地址按升序排列)，然后才能开始下一步操作。&lt;/p&gt;
&lt;p&gt;然后需要对该&lt;code&gt;jump_entry&lt;/code&gt;的地址进行检查是否位于&lt;code&gt;.init&lt;/code&gt;段内，然后通过&lt;code&gt;jump_entry_set_init&lt;/code&gt;对&lt;code&gt;jump_entry-&amp;gt;key&lt;/code&gt;的倒数第二比特进行配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline void jump_entry_set_init(struct jump_entry *entry, bool set)
{
	if (set)
		entry-&amp;gt;key |= 2; // set 1
	else
		entry-&amp;gt;key &amp;amp;= ~2; // clear 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后通过&lt;code&gt;static_key_set_entries&lt;/code&gt;来确保&lt;code&gt;static_key&lt;/code&gt;对应的一组&lt;code&gt;jump_entry&lt;/code&gt;被关联到一起。如果想要启用&lt;code&gt;jump_label&lt;/code&gt;的功能，则需要通过传入参数或其他方式，触发&lt;code&gt;jump_label_update&lt;/code&gt;函数，而&lt;code&gt;update&lt;/code&gt;函数则会触发上述文档中关于&lt;code&gt;arch_jump_label_transform&lt;/code&gt;的实现。&lt;/p&gt;
&lt;p&gt;至此，简单的&lt;code&gt;jump_label&lt;/code&gt;的原理便已经阐述完毕。接下来开始陈述我的工作以及对应处理。&lt;/p&gt;
&lt;h2&gt;Implement JumpLabel For Or1k&lt;/h2&gt;
&lt;h3&gt;PATCH Draft&lt;/h3&gt;
&lt;p&gt;2025年5月25日凌晨，我对Shorne发送了一个名为&amp;lt;b&amp;gt;《[PATCH] openrisc: tracing: Support the jump_label draft》&amp;lt;/b&amp;gt;的邮件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618163046.png&quot; alt=&quot;openrisc: tracing: Support the jump_label draft&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这一份&lt;code&gt;dratf patch&lt;/code&gt;中，我针对于OpenRISC架构的&lt;code&gt;jump_label&lt;/code&gt;进行移植和实现，但始终卡在了虚拟机启动界面。其中，一开始我一直认为是&lt;code&gt;text_patch&lt;/code&gt;的问题，因为当时我的&lt;code&gt;patch&lt;/code&gt;看上去只有&lt;code&gt;text_patch&lt;/code&gt;没有实现(因为OpenRISC目前确实还没有&lt;code&gt;text_patch feature&lt;/code&gt;)。因此我还有了以下的一个提问：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://lore.kernel.org/openrisc/f5a0b134-f82b-4f97-8f31-4520055d4be2@gmail.com/T/#t&quot;&gt;《Need help implementing text_patch for JUMP_LABEL》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但在随后与Shorne的讨论中，发现这并不是一个重点(或许说，这不是一个关键因素)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shorne: Note I was reading other jump_label implementation, the text patching feature seems options. but it would be good to implement a clean text patching API&lt;br /&gt;
Shorne: But not required&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2025年5月25日下午，Shorne对我的&lt;code&gt;draft patch&lt;/code&gt;进行阅读，然后给出了许多提问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; --- a/arch/openrisc/Kconfig
&amp;gt; +++ b/arch/openrisc/Kconfig
&amp;gt; @@ -44,6 +44,7 @@ config OPENRISC
&amp;gt;       select GENERIC_IRQ_MULTI_HANDLER
&amp;gt;       select MMU_GATHER_NO_RANGE if MMU
&amp;gt;       select TRACE_IRQFLAGS_SUPPORT
&amp;gt; +     select HAVE_ARCH_JUMP_LABEL

SHould this be added last?  Maybe its better to have in in the same location of
the other &apos;select HAVE_*&apos; definitions.

&amp;gt; +obj-$(CONFIG_JUMP_LABEL)     += jump_label.o
&amp;gt; +

Why have this separated out with newlines?  Can it be alphabetically sorted?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是关于&lt;code&gt;HAVE_ARCH_JUMP_LABEL&lt;/code&gt;和&lt;code&gt;CONFIG_JUMP_LABEL&lt;/code&gt;的添加位置的提问，Shorne希望我与其他内核风格保持一直，因此我立刻修改了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; +#define WASM(inst) &quot;l.&quot; #inst

Is this needed?  Why not just write l.nop and l.j below?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对于这个问题，实际上是我参考了&lt;code&gt;arm&lt;/code&gt;的风格所导致的，因为&lt;code&gt;arm&lt;/code&gt;有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define WASM(instr)	#instr &quot;.w&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但仔细想想之后，直接硬编码进去可能更好一些。所以这一点也进行了修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; +             offset = jump_entry_target(entry) - jump_entry_code(entry);
&amp;gt; +             WARN_ON(offset &amp;lt; -33554432 || offset &amp;gt; 33554428);

Can we avoid using the integers here?  it is hard to read, also can you have
some comment explaining why this check isneeded?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要进行详细解释了。在一开始，我参考了&lt;code&gt;arm&lt;/code&gt;架构的&lt;code&gt;jump_label&lt;/code&gt;的移植实现。而&lt;code&gt;arm&lt;/code&gt;架构的&lt;code&gt;b.w&lt;/code&gt;的格式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/477f8bc1-00e3-4281-8bfd-88e0d8b9438c.png&quot; alt=&quot;arm b.w insn&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;b.w&lt;/code&gt;中，&lt;code&gt;arm&lt;/code&gt;提供了24位的立即数，也就是对应了&lt;code&gt;b.w label&lt;/code&gt;。而对于&lt;code&gt;arm&lt;/code&gt;架构的实际立即数计算为：&lt;code&gt;1 &amp;lt;&amp;lt; 24 &amp;lt;&amp;lt; 2&lt;/code&gt;。然后换算为有符号数即为：&lt;code&gt;-33554432 ~ 33554428&lt;/code&gt;。因此，我在&lt;code&gt;draft patch&lt;/code&gt;中直接沿用了这一数据(因为OpenRISC的&lt;code&gt;l.j&lt;/code&gt;的立即数是26位的，和&lt;code&gt;1 &amp;lt;&amp;lt; 24 &amp;lt;&amp;lt; 2&lt;/code&gt;相同)。&lt;/p&gt;
&lt;p&gt;但是我忽略了一点，&lt;code&gt;arm&lt;/code&gt;之所以后面需要&lt;code&gt;&amp;lt;&amp;lt; 2&lt;/code&gt;，是因为需要扩展对齐；在OpenRISC中也是如此：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618171437.png&quot; alt=&quot;openrisc l.j insn&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The immediate value is shifted left two bits, sign-extended to program counter width, and
then added to the address of the jump instruction. The result is the effective address of the
jump.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，实际上的OpenRISC对应的数值应该按照26比特再向左位移两个比特的有符号扩展进行计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; +               /*
&amp;gt; +                * The actual maximum range of the l.j instruction&apos;s offset is -134,217,728
&amp;gt; +                * ~ 134,217,724 (sign 26-bit imm).
&amp;gt; +                * For the original jump range, we need to right-shift N by 2 to obtain the
&amp;gt; +                * instruction&apos;s offset.
&amp;gt; +                */
&amp;gt; +               if (unlikely(offset &amp;lt; -134217728 || offset &amp;gt; 134217724)) {
&amp;gt; +                       WARN_ON_ONCE(true);
&amp;gt; +               }
&amp;gt; +               /* 26bit imm mask */
&amp;gt; +               offset = (offset &amp;gt;&amp;gt; 2) &amp;amp; 0x03ffffff;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而最后一个问题，Shorne指出：&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;也应被实现。&lt;/p&gt;
&lt;p&gt;我查阅了主流架构(x86、arm64、RISC-V)，发现它们确实都实现了&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;这一个feature。因此，我在后续的&lt;code&gt;patch&lt;/code&gt;中进行了添加。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;会影响两个地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;struct jump_label&lt;/li&gt;
&lt;li&gt;arch_static_branch和arch_static_branch_jump&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; SHould this be added last?  Maybe its better to have in in the same location of
&amp;gt; the other &apos;select HAVE_*&apos; definitions.

Additionally, I think this needs: HAVE_ARCH_JUMP_LABEL_RELATIVE

Also, we should update:

Documentation/features/core/jump-labels/arch-support.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到现在，OpenRISC的&lt;code&gt;jump_label&lt;/code&gt;的实现就已经有了大致雏形了。如下是无法开机的界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618172514.png&quot; alt=&quot;cannot start with jump_label&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;PATCH V1&lt;/h3&gt;
&lt;p&gt;2025年6月6日，自&lt;code&gt;draft patch&lt;/code&gt;的完成已经过去快两周，因为毕业答辩和毕业论文的事情，因此之前都没有进展。当天我突然发现一个现象：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618173604.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当我把这一段代码的&lt;code&gt;jump_entry&lt;/code&gt;写入的逻辑注释掉后，我发现内核可以正常启动。我立即将这一现象告诉Shorne，但Shorne问我: &quot;&lt;strong&gt;Do you understand this section of code and what it does?&lt;/strong&gt;&quot;&lt;/p&gt;
&lt;p&gt;当时我回答了正确了，但是后面回来一想，确实我当时对这一段代码的理解不够深入。还记得&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;这个flag吗？&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;在这里就起了作用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;.long   1b - ., &quot; label &quot; - . \n\t&quot;
&quot;.word   &quot; key &quot; - .           \n\t&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;，那么实际上就应该如同&lt;code&gt;arm&lt;/code&gt;架构一样的编写格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;.word 1b, &quot; label &quot;, &quot; key &quot;\n\t&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，&lt;code&gt;HAVE_ARCH_JUMP_LABEL_RELATIVE&lt;/code&gt;实际上决定了&lt;code&gt;jump_label&lt;/code&gt;的所有字段是保存偏移量的，如果没有这个flag，则就会类似于&lt;code&gt;arm&lt;/code&gt;这样保存绝对地址。&lt;/p&gt;
&lt;p&gt;然后我就卡在这里了，过了几天后，Shorne问我：&quot;&lt;strong&gt;do you need my help to get it working?&lt;/strong&gt;&quot;&lt;/p&gt;
&lt;p&gt;YES, I NEED. 然后我就把第一版也就是&lt;code&gt;PATCH V1&lt;/code&gt;发送给了Shorne。&lt;/p&gt;
&lt;h3&gt;PATCH V2&lt;/h3&gt;
&lt;p&gt;两天后，2025年6月13日，我和Shorne开始一起调试。一开始，Shorne提供了一个关键信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[    0.000000] jump_label_cmp: a: c0988d24 vs b: c0988d30 keya: c09cc614 vs keyb: c09cc634
[    0.000000] jump_label_cmp: a: c098687c vs b: c0988d30 keya: c09c7174 vs keyb: c09cc634

[    0.000000] Stack: 
[    0.000000] Call trace:
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] __sort_r+0x2e4/0x3c0
--Type &amp;lt;RET&amp;gt; for more, q to quit, c to continue without paging-- 
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? start_kernel+0x0/0x770
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] sort+0x34/0x44
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? setup_arch+0x138/0x1b0
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? jump_label_cmp+0x0/0x84
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? jump_label_swap+0x0/0x6c
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] jump_label_init+0x74/0x134
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? start_kernel+0x90/0x770
[    0.000000] [&amp;lt;(ptrval)&amp;gt;] ? start_kernel+0x0/0x770
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;貌似之前的崩溃是因为&lt;code&gt;jump_label_init&lt;/code&gt;在排序时出了问题。然后Shorne指出: &quot;&lt;strong&gt;Maybe this is the issue, the jump_table is stored in the .rodata section which is read only data&lt;/strong&gt;.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c00f6de8 &amp;lt;jump_label_swap&amp;gt;:
c00f6de8:       9c 21 ff f8     l.addi r1,r1,-8
c00f6dec:       e2 23 20 02     l.sub r17,r3,r4
c00f6df0:       86 64 00 00     l.lwz r19,0(r4)
c00f6df4:       e2 73 88 02     l.sub r19,r19,r17
c00f6df8:       86 a3 00 00     l.lwz r21,0(r3)
c00f6dfc:       d4 03 98 00     l.sw 0(r3),r19 &amp;lt;--- this line
c00f6e00:       86 e4 00 04     l.lwz r23,4(r4)
c00f6e04:       e2 f7 88 02     l.sub r23,r23,r17
c00f6e08:       86 63 00 04     l.lwz r19,4(r3)

static void jump_label_swap(void *a, void *b, int size)                        
{                                                                              
      long delta = (unsigned long)a - (unsigned long)b;                      
      struct jump_entry *jea = a;                                            
      struct jump_entry *jeb = b;                                            
      struct jump_entry tmp = *jea;                                          
                                                                             
      jea-&amp;gt;code       = jeb-&amp;gt;code - delta;    &amp;lt;---   this line                             
      jea-&amp;gt;target     = jeb-&amp;gt;target - delta;                                 
      jea-&amp;gt;key        = jeb-&amp;gt;key - delta;                                    
                                                                             
      jeb-&amp;gt;code       = tmp.code + delta;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一点上，Shorne和我达成了一致：一定是因为jump_label_init在排序时，对&lt;code&gt;.rodata&lt;/code&gt;的数据进行了修改。但是其他架构的排序也是发生在&lt;code&gt;.rodata&lt;/code&gt;内，&lt;strong&gt;因此肯定有一种机制使得&lt;code&gt;.rodata&lt;/code&gt;当时是允许修改的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在和Shorne的交流中，他的一句话引起了我的注意：&quot;&lt;strong&gt;but according the the failure it is not, we should if the page table is marking it as read only during setup&lt;/strong&gt;&quot;。确实，&lt;code&gt;.rodata&lt;/code&gt;按理来说是无法被修改的，但是如果在启动时，&lt;code&gt;.rodata&lt;/code&gt;还未被标记为只读状态，那么是否这时候的&lt;code&gt;.rodata&lt;/code&gt;是能够被修改的呢？&lt;/p&gt;
&lt;p&gt;答案是肯定的，接近二十分钟后，我找到了答案。我惊喜的和Shorne说，&quot;wow, look this! &lt;a href=&quot;https://github.com/torvalds/linux/blob/27605c8c0f69e319df156b471974e4e223035378/arch/riscv/kernel/setup.c#L312&quot;&gt;linux kernel arch riscv setup.c#L312&lt;/a&gt;&quot;。在RISC-V架构中，&lt;code&gt;setup_arch&lt;/code&gt;函数(每一个架构都会进行实现)中在最开始就会调用一个&lt;code&gt;setup_initial_init_mm&lt;/code&gt;和&lt;code&gt;paging_init&lt;/code&gt;函数。当我转过头去查看OpenRISC的对应函数时，发现了惊喜。&lt;/p&gt;
&lt;p&gt;在OpenRISC的&lt;code&gt;paging_init&lt;/code&gt;中有这样的一个调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern const char _s_kernel_ro[], _e_kernel_ro[];

static void __init map_ram(void)
{
    ...
    if (v &amp;gt;= (u32) _e_kernel_ro ||
        v &amp;lt; (u32) _s_kernel_ro)
        prot = PAGE_KERNEL;
    else
        prot = PAGE_KERNEL;

    set_pte(pte, mk_pte_phys(p, prot));
    ...
}

void __init paging_init(void)
{
    ...
    map_ram();
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我将&lt;code&gt;jump_label_init&lt;/code&gt;在&lt;code&gt;paging_init&lt;/code&gt;之前进行调用时，就能够发现原本不会输出任何信息的终端开始有日志输出了。&lt;/p&gt;
&lt;p&gt;不过当时，Shorne和我说，&quot;&lt;strong&gt;I don&apos;t think so, did you see the last message I pasted about mm being null, causing the failure? it was not the read only issue.&lt;/strong&gt;&quot;。然后转而对&lt;code&gt;dtlb_miss_handler&lt;/code&gt;和&lt;code&gt;itlb_miss_handler&lt;/code&gt;进行研究。&lt;/p&gt;
&lt;p&gt;但随后，Shorne同意了我的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; readelf -s vmlinux | grep -e _kernel_ro -e jump_table
29270: c0982440     0 NOTYPE  GLOBAL DEFAULT    2 __start___jump_table
31424: c0000000     0 NOTYPE  GLOBAL DEFAULT    1 _s_kernel_ro
33126: c098c000     0 NOTYPE  GLOBAL DEFAULT    8 _e_kernel_ro
37354: c0986d9c     0 NOTYPE  GLOBAL DEFAULT    2 __stop___jump_table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里就可以清晰的看到，&lt;code&gt;_s_kernel_ro&lt;/code&gt;和&lt;code&gt;_e_kernel_ro&lt;/code&gt;被写入到&lt;code&gt;.rodata&lt;/code&gt;中了。但是我们发现，经过这样处理后还是会出现失败的情况，然后我对&lt;code&gt;jump_label_init&lt;/code&gt;进行了分析，增加了一条测试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+       printk(&quot;static_key_initialized: %d&quot;, static_key_initialized);
+
        if (static_key_initialized)
                return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在日志中可以发现，&lt;code&gt;jump_label_init&lt;/code&gt;实际上会调用两次，第一次在&lt;code&gt;setup_arch&lt;/code&gt;中(位于&lt;code&gt;paging_init&lt;/code&gt;之前)被调用，在这次调用的时候，&lt;code&gt;__jump_table&lt;/code&gt;的内容是可写的，因此成功解决了&lt;code&gt;jump_label_sort_entries&lt;/code&gt;的崩溃问题。需要注意，&lt;strong&gt;第一次运行时，如果成功了会将&lt;code&gt;static_key_initialized&lt;/code&gt;设置为&lt;code&gt;1&lt;/code&gt;。因此在第二次执行时，这里就直接返回，不再进行初始化&lt;/strong&gt;。因此崩溃的实际上在另外的地方。&lt;/p&gt;
&lt;p&gt;然后，Shorne对控制台的权限进行了获取，这样能够看到更多的日志输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0xefc68a77:     &quot;&quot;
0xefc68a78:     &quot;?\377\3742printk: legacy bootconsole [ns16550a0] disableded\n&quot;
0xefc68aaf:     &quot;&quot;
0xefc68ab0:     &quot;?\377\3743&quot;, &apos;[&apos; &amp;lt;repeats 16 times&amp;gt;, &quot;entry wrong place&quot;, &apos;]&apos; &amp;lt;repeats 20 times&amp;gt;, &quot;40.00 BogoMIPS (lpj=200000)0)\n&quot;
0xefc68b08:     &quot;?\377\3744pid_max: default: 32768 minimum: 30101\n&quot;
0xefc68b34:     &quot;?\377\3745Mount-cache hash table entries: 2048 (order: 0, 8192 bytes, linear)r)\n&quot;
0xefc68b7f:     &quot;&quot;
0xefc68b80:     &quot;?\377\3746Mountpoint-cache hash table entries: 2048 (order: 0, 8192 bytes, linear)r)\n&quot;
0xefc68bd0:     &quot;&quot;
0xefc68bd1:     &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实证明了，&lt;code&gt;jump_label_init&lt;/code&gt;成功，这里是位于&lt;code&gt;arch_jump_label_transform_queue&lt;/code&gt;出现了问题。当时我始终认为&lt;code&gt;text_patch&lt;/code&gt;是必须的，因此在这里加了一个日志，如果运行到这里就会输出&lt;code&gt;entry wrong place&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&quot;&lt;strong&gt;because missing the text_patch to flush?&lt;/strong&gt;&quot;，我对Shorne说，然后Shorne对这块进行了补全，并且验证了这个&lt;code&gt;PATCH V2&lt;/code&gt;确实可行。&lt;/p&gt;
&lt;p&gt;最终，Shorne通过参考RISC-V架构的&lt;code&gt;text_patch&lt;/code&gt;，发现RISC-V会通过&lt;code&gt;patch_map&lt;/code&gt;对&lt;code&gt;.rodata&lt;/code&gt;的数据进行映射，这样就不会引发&lt;code&gt;paging_init&lt;/code&gt;后的&lt;code&gt;jump_label_update&lt;/code&gt;的非法地址访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;waddr = patch_map(addr, FIX_TEXT_POKE0);

	ret = copy_to_kernel_nofault(waddr, insn, len);

/*
 * We could have just patched a function that is about to be
 * called so make sure we don&apos;t execute partially patched
 * instructions by flushing the icache as soon as possible.
 */
local_flush_icache_range((unsigned long)waddr,
             (unsigned long)waddr + len);

patch_unmap(FIX_TEXT_POKE0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，Shorne发送了&lt;code&gt;PATCH V2&lt;/code&gt;给我。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Initial support for OpenRISC jumplabel support.  Currently causes
crashes after jump_label is initialized.

  - Need to support writing to memory by patching the page table to temporarily allow writes.
  - Need to see what needs to be done to flush SMP pages.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618184508.png&quot; alt=&quot;PATCH V2&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;PATCH V3&lt;/h3&gt;
&lt;p&gt;当天深夜，Shorne突然和我说，&lt;/p&gt;
&lt;p&gt;&quot;&lt;strong&gt;I think I figured out what the failure is., it boots now. You made a basic mistake, sending patch.&lt;/strong&gt;&quot;&lt;/p&gt;
&lt;p&gt;我当时十分震惊，心想是什么错误。然后Shorne说，&lt;/p&gt;
&lt;p&gt;&quot;&lt;strong&gt;You were missing l.nop in the branch delay slots&lt;/strong&gt;&quot;。&lt;/p&gt;
&lt;p&gt;是的，OpenRISC需要延迟槽。这里简单解释下延迟槽：&lt;/p&gt;
&lt;p&gt;延迟槽（Delay Slot）是RISC（精简指令集计算机）架构（如MIPS、SPARC）中的一种特殊设计，用于优化流水线执行效率。它的核心思想是：在分支指令（如跳转、调用）生效之前，允许执行紧随其后的下一条指令，从而减少流水线停顿（Pipeline Stall）—— by dpsk(deepseek)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j    target      # jump to target
nop              # delay slot insn (Will be executed no matter what)

beq  $t0, $t1, label
add  $t2, $t3, $t4   # delay slot insn (Will be executed even if the branch is not taken)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决完这个问题后，Shorne给我发送了&lt;code&gt;PATCH V3&lt;/code&gt;，应该说，这就是我的&lt;code&gt;jump_label first PATCH&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618185504.png&quot; alt=&quot;PATCH V3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然，这个&lt;code&gt;PATCH V3&lt;/code&gt;并没有那么完美，还有很多事情需要补充。但是，终于可以开机了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250618190005.png&quot; alt=&quot;Start OpenRISC Linux&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;PATCH MORE&lt;/h3&gt;
&lt;p&gt;TO BE DONE...&lt;/p&gt;
</content:encoded></item><item><title>GSOC2025: How To Start With OpenRISC</title><link>https://chenmiaoi.github.io/2025/06/15/GSOC2025-How-To-Start-With-OpenRISC/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2025/06/15/GSOC2025-How-To-Start-With-OpenRISC/</guid><pubDate>Sun, 15 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2025年5月9日的清晨，阳光透过窗帘洒在书桌上，我像往常一样打开邮箱，例行公事般地扫视着堆积如山的垃圾邮件和社区订阅信件（尽管这些内容我从未真正点开过^v^）。距离我提交GSOC提案已经过去整整一个月了，最初的几天里，我几乎每隔几分钟就会刷新一次收件箱，满心期待能收到回复；而如今，这份期待早已被时间冲淡，只剩下一种机械般的习惯，让我每天早晨仍会不自觉地检查邮箱。&lt;/p&gt;
&lt;p&gt;正当我准备关掉页面，心想今天大概又是一无所获时，屏幕突然一闪——邮箱自动刷新，一封崭新的邮件赫然出现在最上方。&lt;/p&gt;
&lt;p&gt;《&lt;strong&gt;GSoC 2025: Congratulations, your proposal with Free and Open Source Silicon Foundation has been accepted&lt;/strong&gt;!》&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250615182136.png&quot; alt=&quot;GSoC 2025: Congratulations&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我知道，我的任务开始了——这不再是一个普通的周五早晨，而是我开源生涯真正的起点。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在提案通过的那天，我正式与导师Shorne展开合作。他给我的第一个任务是&lt;strong&gt;搭建OpenRISC架构的编译工具链，并通过QEMU启动一个OpenRISC架构的Linux系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;起初，我花费了大量时间编写自己的构建脚本(&lt;a href=&quot;https://github.com/ChenMiaoi/GSO2025-OpenRISC&quot;&gt;OpenRISC-Build&lt;/a&gt;)，但后来才发现Shorne早已维护了一个成熟的工具库(&lt;a href=&quot;https://github.com/stffrdhrn/or1k-utils&quot;&gt;or1k-util&lt;/a&gt;)。由于走了弯路，两周过去，我仍未能成功启动OpenRISC Linux系统。&lt;/p&gt;
&lt;p&gt;当Shorne询问进展时，我告诉他我的脚本卡在了启动阶段。他反问道：“为什么不直接用&lt;code&gt;or1k-util&lt;/code&gt;？”这时我才意识到，自己又陷入了过度发散思维的陷阱，白白浪费了不少时间。&lt;/p&gt;
&lt;p&gt;于是，我转而使用&lt;code&gt;or1k-util&lt;/code&gt;，按照文档指引一步步配置，最终顺利运行起了OpenRISC Linux系统。&lt;/p&gt;
&lt;h2&gt;Really Docs For How To Start&lt;/h2&gt;
&lt;p&gt;本章节是正式的对如何通过&lt;a href=&quot;https://github.com/stffrdhrn/or1k-utils&quot;&gt;or1k-util&lt;/a&gt;实现在本地通过QEMU运行起一个真正的OpenRISC架构的Linux内核，并且可以通过该内核进行调试的教程。&lt;/p&gt;
&lt;p&gt;我当前使用的环境如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ubuntu 24.10

$ uname -a
Linux nyaos 6.11.0-26-generic #26-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 12 11:25:41 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于OpenRISC架构的主线版本的glibc暂时有bug，因此我们暂时使用了指定版本的编译链版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OR1K_GCC_URL=&quot;https://mirrors.aliyun.com/gnu/gcc/gcc-14.2.0/gcc-14.2.0.tar.gz&quot;
OR1K_BINUTILS_GDB_URL=&quot;https://github.com/bminor/binutils-gdb.git&quot;
OR1K_NEWLIB_URL=&quot;ftp://sourceware.org/pub/newlib/newlib-4.5.0.20241231.tar.gz&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一切开始之前，我们需要确定&lt;strong&gt;运行并调试一个OpenRISC架构的Linux内核到底需要什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;QEMU OpenRISC&lt;/li&gt;
&lt;li&gt;rootfs&lt;/li&gt;
&lt;li&gt;or1k-linux-gcc&lt;/li&gt;
&lt;li&gt;or1k-elf-gdb&lt;/li&gt;
&lt;li&gt;linux OpenRISC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然在前面提到，我浪费了很多时间在自己的脚本上，但是由于&lt;code&gt;or1k-utils&lt;/code&gt;本身并不会自动下载对应的源码/软件包，只会对指定目录下是否存在对应的源码/软件包进行检测。因此我们的下载工具使用&lt;code&gt;OpenRISC-Build&lt;/code&gt;进行自动下载，后续的编译工作则是交给&lt;code&gt;or1k-utils&lt;/code&gt;进行。&lt;/p&gt;
&lt;h3&gt;DownLoad Source With OpenRISC-Build&lt;/h3&gt;
&lt;p&gt;首先，我们需要将脚本仓库克隆到一个任意位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/ChenMiaoi/GSO2025-OpenRISC.git OpenRISC-Build
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;code&gt;OpenRISC-Build&lt;/code&gt;现只有下载源码的功能，一切的编译操作由&lt;code&gt;or1k-utils&lt;/code&gt;来进行&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当仓库克隆完毕后，我们就可以进入到仓库中，然后进行源码的下载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd OpenRISC-Build
./build-or1k.sh --get-rootfs
./build-or1k.sh --get-tools

source ~/.bashrc

./build-or1k.sh --get-qemu
./build-or1k.sh --get-linux
./build-or1k.sh --get-or1k-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下表是关于脚本下载各种源码的命令，下载的内容以及各自源码存放的位置(根据&lt;code&gt;or1k-utils&lt;/code&gt;所设置)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Command&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Work&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Dir&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;get rootfs&lt;/td&gt;
&lt;td&gt;download rootfs&lt;/td&gt;
&lt;td&gt;$HOME/work/openrisc/buildroot-rootfs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get tools&lt;/td&gt;
&lt;td&gt;download or1k-linux-&lt;/td&gt;
&lt;td&gt;$HOME/work/gnu-toolchain/local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;download gcc-14.2.0&lt;/td&gt;
&lt;td&gt;$HOME/work/gnu-toolchain/gcc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;download binutils-gdb&lt;/td&gt;
&lt;td&gt;$HOME/work/gnu-toolchain/binutils-gdb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;download newlib-4.5.0.20241231&lt;/td&gt;
&lt;td&gt;$HOME/work/gnu-toolchain/newlib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get qemu&lt;/td&gt;
&lt;td&gt;download qemu&lt;/td&gt;
&lt;td&gt;$HOME/work/openrisc/qemu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get linux&lt;/td&gt;
&lt;td&gt;download linux&lt;/td&gt;
&lt;td&gt;$HOME/work/linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get or1k-utils&lt;/td&gt;
&lt;td&gt;download or1k-utils&lt;/td&gt;
&lt;td&gt;$HOME/work/openrisc/or1k-utils&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;strong&gt;一切的源码均会下载到&lt;code&gt;$HOME/work/openrisc&lt;/code&gt;、&lt;code&gt;$HOME/work/linux&lt;/code&gt;以及&lt;code&gt;$HOME/work/toolchain&lt;/code&gt;中&lt;/strong&gt;&lt;br /&gt;
注意：&lt;strong&gt;当使用&lt;code&gt;--get-tools&lt;/code&gt;后，建议&lt;code&gt;source ~/.bashrc&lt;/code&gt;一下&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面是关于各自命令下载后的目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tree -L 1 work/
work/
├── gnu-toolchain
├── linux
└── openrisc

$ tree -L 1 work/gnu-toolchain/
work/gnu-toolchain/
├── binutils
├── gcc
├── gdb
├── local
└── newlib

$ tree -L 1 work/openrisc/
work/openrisc/
├── buildroot-rootfs
├── or1k-utils
├── qemu
└── toolchain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一切准备就绪后，我们就可以开始进行编译了。&lt;/p&gt;
&lt;h3&gt;Build Source With Or1k-utils&lt;/h3&gt;
&lt;p&gt;虽然已经说过很多次了，但在这里还需要强调：&lt;strong&gt;一切的编译步骤都是通过下载的&lt;code&gt;or1k-utils&lt;/code&gt;进行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，我们进入&lt;code&gt;or1k-utils&lt;/code&gt;目录中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd or1k-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;BUILD QEMU&lt;/h4&gt;
&lt;p&gt;首先，我们应该编译&lt;code&gt;QEMU&lt;/code&gt;和&lt;code&gt;OR1K-ELF-&lt;/code&gt;工具链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./qemu/config.qemu
./scripts/qemu-build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译完成后，可以查看对应目录以及查看其对应的版本信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls $HOME/work/openrisc/qemu/build
qemu-or1k
qemu-system-or1k

$HOME/work/openrisc/qemu/build/qemu-system-or1k --version
QEMU emulator version 10.0.50
Copyright (c) 2003-2025 Fabrice Bellard and the QEMU Project developers
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;strong&gt;我们并不需要安装QEMU到一个指定路径，因为后续的使用均直接使用了编译目录的QEMU&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;BUILD OR1K-ELF&lt;/h4&gt;
&lt;p&gt;然后，我们开始编译&lt;code&gt;OR1K-ELF-&lt;/code&gt;工具链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NOTIFY=n ./toolchains/newlib.build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们传递了一个&lt;code&gt;NOTIFY&lt;/code&gt;参数，这时因为在&lt;code&gt;newlib.build&lt;/code&gt;中有一个&lt;a href=&quot;https://github.com/stffrdhrn/or1k-utils/blob/82f7c73cf60c79c282297e7ec4e43311f20b2118/toolchains/newlib.config#L56&quot;&gt;MAILTO&lt;/a&gt;参数，我并不需要发送邮件给自己或其他人，因此，这里使用&lt;code&gt;NOTIFY=n&lt;/code&gt;禁止发送。&lt;/p&gt;
&lt;p&gt;由于编译脚本会将所有的编译信息重定向到&lt;code&gt;$HOME/work/gnu-toolchain/log/${CROSS}-build.log&lt;/code&gt;中，因此你可以使用下面的命令来实时监控：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -f $HOME/work/gnu-toolchain/log/or1k-elf-build.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译完成后，我们可以查看指定目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls $HOME/work/gnu-toolchain/local/
bin  include  lib  libexec  or1k-elf  share

ls $HOME/work/gnu-toolchain/local/bin/
or1k-elf-addr2line  or1k-elf-c++      or1k-elf-elfedit  or1k-elf-gcc-14.2.0  or1k-elf-gcc-ranlib  or1k-elf-gcov-tool      or1k-elf-gprof   or1k-elf-ld.bfd   or1k-elf-objdump  or1k-elf-run      or1k-elf-strip
or1k-elf-ar         or1k-elf-c++filt  or1k-elf-g++      or1k-elf-gcc-ar      or1k-elf-gcov        or1k-elf-gdb            or1k-elf-gstack  or1k-elf-nm       or1k-elf-ranlib   or1k-elf-size
or1k-elf-as         or1k-elf-cpp      or1k-elf-gcc      or1k-elf-gcc-nm      or1k-elf-gcov-dump   or1k-elf-gdb-add-index  or1k-elf-ld      or1k-elf-objcopy  or1k-elf-readelf  or1k-elf-strings
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们着重需要注意的是两个东西：&lt;code&gt;or1k-elf-gcc&lt;/code&gt;和&lt;code&gt;or1k-elf-gdb&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$HOME/work/gnu-toolchain/local/bin/or1k-elf-gcc -v
Using built-in specs.
COLLECT_GCC=/home/nya/work/gnu-toolchain/local/bin/or1k-elf-gcc
COLLECT_LTO_WRAPPER=/home/nya/work/gnu-toolchain/local/libexec/gcc/or1k-elf/14.2.0/lto-wrapper
Target: or1k-elf
Configured with: /home/nya/work/gnu-toolchain/gcc/configure --target=or1k-elf --prefix=/home/nya/work/gnu-toolchain/local --with-gnu-ld --with-gnu-as --disable-nls --disable-lto --disable-libssp --disable-shared --with-multilib-list=mcmov --enable-languages=c,c++ --with-newlib
Thread model: single
Supported LTO compression algorithms: zlib
gcc version 14.2.0 (GCC) 

$HOME/work/gnu-toolchain/local/bin/or1k-elf-gdb -v
GNU gdb (GDB) 17.0.50.20250605-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later &amp;lt;http://gnu.org/licenses/gpl.html&amp;gt;
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;BUILD LINUX&lt;/h4&gt;
&lt;p&gt;当前面的所有准备就绪后，我们就可以编译OpenRISC架构的Linux内核了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/make-or1k-linux virt_defconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Shorne曾问过我: &quot;did you compile the kernel with debug symbols?&quot;&lt;br /&gt;
当时我以为我会，事实上，我并不会。因此，Shorne手把手带我编译了一遍，感谢Shorne&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接下来我们需要编译一个带调试信息的内核，因此我们使用&lt;code&gt;menuconfig&lt;/code&gt;进行配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/make-or1k-linux menuconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250615202715.png&quot; alt=&quot;or1k menuconfig&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，当你执行&lt;code&gt;menuconfig&lt;/code&gt;后会进入如下的TUI界面，我们需要通过这样的路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Kernel hacking  ---&amp;gt;
    Kernel debugging  ---&amp;gt; y
    
Kernel hacking  ---&amp;gt;
    Compile-time checks and compiler options  ---&amp;gt;
        Debug information (Disable debug information)  ---&amp;gt;
            Rely on the toolchain&apos;s implicit default DWARF version  ---&amp;gt; y
        Provide GDB scripts for kernel debugging  ---&amp;gt; y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250615202956.png&quot; alt=&quot;menuconfig debug info&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过上面的设置，我们就将三个关键选项设置好了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kernel debugging&lt;/li&gt;
&lt;li&gt;Rely on the toolchain&apos;s implicit default DWARF version&lt;/li&gt;
&lt;li&gt;Provide GDB scripts for kernel debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后我们就可以开始编译内核：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/make-or1k-linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切没有报错，我们就可以检查内核目录下是否存在&lt;code&gt;vmlinux&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls $HOME/work/linux/
vmlinux

file $HOME/work/linux/vmlinux
/home/nya/work/linux/vmlinux: ELF 32-bit MSB executable, OpenRISC, version 1 (SYSV), statically linked, BuildID[sha1]=bab6c2d0b0a5111785bda2c9268c7a3871e3dc6e, with debug_info, not stripped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，所有的编译工作就完成了。&lt;/p&gt;
&lt;h3&gt;Starting And Debugging&lt;/h3&gt;
&lt;p&gt;在启动内核之前，我们需要了解启动脚本的一些参数。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Shorne又一次拷打我：&quot;do you know what commands to run? For example, how to start qemu in debug mode?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在&lt;code&gt;or1k-utils&lt;/code&gt;中，启动内核所使用的脚本为&lt;code&gt;qemu-or1k-linux&lt;/code&gt;，其中有一个&lt;code&gt;-S&lt;/code&gt;参数与QEMU的调试相关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Debug, add -S to stop on startup
DEBUG=&quot;-gdb tcp::10001&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;我当时回答Shorne：&quot;add the -S, using the qemu-linux -S&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;显而易见的，这是截然相反的。如果我们想要调试内核，就不应该加上&lt;code&gt;-S&lt;/code&gt;参数，这个脚本默认启动了调试端口。因此，对于调试，我们可以直接运行脚本(当然，直接启动也是可以的)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./scripts/qemu-or1k-linux
[    0.000000] FDT at (ptrval)
[    0.000000] random: crng init done
[    0.000000] Linux version 6.15.0-dirty (nya@nyaos) (or1k-linux-gcc (GCC) 14.2.0, GNU ld (GNU Binutils) 2.43.1) #2 SMP Fri Jun 13 16:02:54 UTC 2025
[    0.000000] OF: reserved mem: Reserved memory: No reserved-memory node in the DT
[    0.000000] CPU: OpenRISC-13 (revision 8) @20 MHz
[    0.000000] -- dmmu:  128 entries, 1 way(s)
[    0.000000] -- immu:  128 entries, 1 way(s)
[    0.000000] -- additional features:
[    0.000000] -- power management
[    0.000000] -- PIC
[    0.000000] -- timer
[    0.000000] Initial ramdisk not found
......
[    0.360000] EXT4-fs (vda2): INFO: recovery required on readonly filesystem
[    0.360000] EXT4-fs (vda2): write access will be enabled during recovery
[    1.590000] EXT4-fs (vda2): orphan cleanup on readonly fs
[    1.590000] EXT4-fs (vda2): recovery complete
[    1.600000] EXT4-fs (vda2): mounted filesystem d4e1e6f8-942f-4d45-9afe-4e73dcfff064 ro with ordered data mode. Quota mode: disabled.
[    1.600000] VFS: Mounted root (ext4 filesystem) readonly on device 254:2.
[    1.600000] devtmpfs: mounted
[    1.640000] Freeing unused kernel image (initmem) memory: 264K
[    1.640000] This architecture does not have kernel memory protection.
[    1.640000] Run /sbin/init as init process
INIT: version 3.13 booting
INIT: No inittab.d directory found
[    1.800000] EXT4-fs (vda2): re-mounted d4e1e6f8-942f-4d45-9afe-4e73dcfff064 r/w.
[    1.920000] Adding 2097144k swap on /dev/vda1.  Priority:-2 extents:1 across:2097144k 
INIT: Entering runlevel: 3
Seeding 256 bits without crediting
Saving 256 bits of creditable seed for next boot
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Starting network: OK
Starting sntp: sntp 4.2.8p18@1.4062-o Wed Apr 16 22:01:36 UTC 2025 (1)
libgcc_s.so.1 must be installed for pthread_exit to work
/etc/init.d/S48sntp: line 15:   124 Aborted                 /usr/bin/$DAEMON $SNTP_ARGS -K $SNTP_KEY_CACHE $SNTP_SERVERS
FAIL
Starting crond: OK
Starting sshd: OK

Welcome to Linux on OpenRISC
buildroot login: 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认的用户为&lt;code&gt;root&lt;/code&gt;，并且没有密码，因此我们键入&lt;code&gt;root&lt;/code&gt;敲击回车后，即可直接进入系统。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Welcome to Linux on OpenRISC
buildroot login: root
  _      __    __
 | | /| / /__ / /______  __ _  ___
 | |/ |/ / -_) / __/ _ \/  &apos; \/ -_)
 |__/|__/\__/_/\__/\___/_/_/_/\__/
                   / /____
                  / __/ _ \
  ____     _____  \__/\___/________  ______
 / __ \___&amp;lt;  / /_____| | / /  _/ _ \/_  __/
/ /_/ / __/ /  &apos;_/___/ |/ // // , _/ / /
\____/_/ /_/_/\_\    |___/___/_/|_| /_/

 32-bit OpenRISC CPUs on a QEMU Virt Platform
# uname -a
Linux buildroot 6.15.0-dirty #2 SMP Fri Jun 13 16:02:54 UTC 2025 openrisc GNU/Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们想要调试内核，则通过编译好的&lt;code&gt;or1k-elf-gdb&lt;/code&gt;进行调试。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;strong&gt;远程调试端口号为10001&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;or1k-elf-gdb vmlinux
(gdb) target remote :10001
Remote debugging using :10001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250615204830.png&quot; alt=&quot;qemu debug&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>rCore ch3 Details</title><link>https://chenmiaoi.github.io/2025/04/09/rCore-ch3-Details/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2025/04/09/rCore-ch3-Details/</guid><pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本篇文章是帮助初学者加快理解rCore的设计原理的文章，并不设计任何关于opencamp实现相关。部分细节参考&lt;a href=&quot;https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter3/index.html#&quot;&gt;rCore Tutorial Book chapter3&lt;/a&gt; 和 &lt;a href=&quot;https://learningos.cn/rCore-Tutorial-Guide-2025S/chapter3/index.html&quot;&gt;rCore Tutorial Guild chapter3&lt;/a&gt;。&lt;br /&gt;
如有任何问题，请到对应仓库(&lt;a href=&quot;https://github.com/ChenMiaoi/ChenMiaoi.github.io/tree/hexo&quot;&gt;nya blog repo&lt;/a&gt;)下提出相关&lt;code&gt;issue&lt;/code&gt;，参见格式：[Book Name]: Question&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Reference
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/riscv/riscv-isa-manual/releases/download/riscv-isa-release-8696121-2025-04-04/riscv-unprivileged.pdf&quot;&gt;RISC-V ISA MANUAL UNPRIVILEGE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/riscv/riscv-isa-manual/releases/download/riscv-isa-release-8696121-2025-04-04/riscv-privileged.pdf&quot;&gt;RISC-V ISA MANUAL PRIVILEGE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;下面，我会以我们实际阅读代码的角度来依次分析rCore的具体实现细节，当然部分简单机制会被忽略。&lt;/p&gt;
&lt;p&gt;在开始分析rCore操作系统的具体实现时，我们首先需要确保开发环境的正确性。通过执行以下命令切换到ch3分支并验证其可运行性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout ch3
cd os; make run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;面对复杂的操作系统实现，合理的分析顺序能显著提高理解效率。在rCore项目中，我建议采用以下分析路径：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建系统分析(&lt;a&gt;os/Makefile&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;用户程序分析(&lt;a&gt;user/src/lib.rs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;内核入口分析(&lt;a&gt;os/src/main.rs&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;构建系统是理解整个项目的关键入口。我们重点分析&lt;code&gt;os/Makefile&lt;/code&gt;中&lt;code&gt;make run&lt;/code&gt;的执行流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build: $(KERNEL_BIN)

$(KERNEL_BIN): kernel
	@$(OBJCOPY) $(KERNEL_ELF) --strip-all -O binary $@

kernel:
	@make -C ../user build TEST=$(TEST) CHAPTER=$(CHAPTER) BASE=$(BASE)
	@echo Platform: $(BOARD)
	@cargo build $(MODE_ARG)

run: run-inner

run-inner: build
	@qemu-system-riscv64 \
		-machine virt \
		-nographic \
		-bios $(BOOTLOADER) \
		-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;os/Makefile&lt;/code&gt;中，&lt;code&gt;make run&lt;/code&gt;依赖于&lt;code&gt;build&lt;/code&gt;，而&lt;code&gt;build&lt;/code&gt;会依赖于&lt;code&gt;kernel&lt;/code&gt;，在&lt;code&gt;kernel&lt;/code&gt;中，会做两件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译出&lt;code&gt;os&lt;/code&gt;中需要的应用程序，用来验证我们的多道程序操作系统&lt;/li&gt;
&lt;li&gt;编译出&lt;code&gt;os&lt;/code&gt;可执行文件，或者说内核镜像文件，用于启动我们的内核&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，我们现在讲目光跳转到&lt;code&gt;user&lt;/code&gt;中。&lt;/p&gt;
&lt;h2&gt;User&lt;/h2&gt;
&lt;p&gt;进入&lt;code&gt;user&lt;/code&gt;目录后，我们首先也需要讲目光放在&lt;code&gt;user/Makefile&lt;/code&gt;身上，其中，我们主要关注两个点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内核需要的&lt;code&gt;binary&lt;/code&gt;是如何产出的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;binary&lt;/code&gt;的内存布局是如何的&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于上述的两个重点，在下面会被同时涉及到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;binary:
	@echo $(ELFS)
	@if [ ${CHAPTER} -gt 3 ]; then \
		cargo build $(MODE_ARG) ;\
	else \
		CHAPTER=$(CHAPTER) python3 build.py ;\
	fi
	@$(foreach elf, $(ELFS), \
		$(OBJCOPY) $(elf) --strip-all -O binary $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.bin, $(elf)); \
		cp $(elf) $(patsubst $(TARGET_DIR)/%, $(TARGET_DIR)/%.elf, $(elf));)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;chapter3&lt;/code&gt;中会调用一个&lt;code&gt;build.py&lt;/code&gt;脚本进行具体的操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base_address = 0x80400000
step = 0x20000

for app in apps:
  os.system(
    &quot;cargo rustc --bin %s %s -- -Clink-args=-Ttext=%x&quot;
    % (app, mode_arg, base_address + step * app_id)
  )
  if chapter == &apos;3&apos;:
    app_id = app_id + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在该构建脚本中，关键点在于 app_id 会随着应用程序的生成而递增，从而确保每个编译出的用户程序的起始地址（.text.entry 段）均不相同。这一机制与 chapter2 的实现形成鲜明对比，后者采用固定地址加载方式，而当前方案通过动态计算 base_address + step * app_id 来分配入口地址，确保不同应用程序在内存中的执行位置相互隔离。&lt;/p&gt;
&lt;p&gt;接下来，我们需要关注 user/src/lib.rs 文件，其中定义了用户程序的入口逻辑。该文件包含两个关键部分：&lt;/p&gt;
&lt;p&gt;​1. ​_start 函数​​：作为用户程序的初始入口点，由链接脚本显式定位到 .text.entry 段，负责初始化环境并调用 main 函数。
​​2. 弱符号 main 函数​​（#[linkage = &quot;weak&quot;]）：通过弱符号机制允许不同用户程序提供自己的 main 实现，而链接器会在加载时解析到正确的版本。这种设计实现了入口点的统一管理，同时支持不同应用程序的灵活逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
#[link_section = &quot;.text.entry&quot;]
pub extern &quot;C&quot; fn _start(argc: usize, argv: usize) -&amp;gt; ! {
    clear_bss();
    unsafe {
        HEAP.lock()
            .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE);
    }
    let mut v: Vec&amp;lt;&amp;amp;&apos;static str&amp;gt; = Vec::new();
    for i in 0..argc {
        let str_start =
            unsafe { ((argv + i * core::mem::size_of::&amp;lt;usize&amp;gt;()) as *const usize).read_volatile() };
        let len = (0usize..)
            .find(|i| unsafe { ((str_start + *i) as *const u8).read_volatile() == 0 })
            .unwrap();
        v.push(
            core::str::from_utf8(unsafe {
                core::slice::from_raw_parts(str_start as *const u8, len)
            })
            .unwrap(),
        );
    }
    exit(main(argc, v.as_slice()));
}

#[linkage = &quot;weak&quot;]
#[no_mangle]
fn main(_argc: usize, _argv: &amp;amp;[&amp;amp;str]) -&amp;gt; i32 {
    panic!(&quot;Cannot find main!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在用户程序构建过程中，&lt;code&gt;_start&lt;/code&gt;函数被显式地定位到&lt;code&gt;src/linker.ld&lt;/code&gt;链接脚本中定义的&lt;code&gt;.text.entry&lt;/code&gt;段，该段作为用户程序的入口点。通过&lt;code&gt;build.py&lt;/code&gt;编译脚本的配置，&lt;code&gt;.text.entry&lt;/code&gt;段的起始地址被动态计算为&lt;code&gt;base_address + step * app_id&lt;/code&gt;，其中&lt;code&gt;base_address&lt;/code&gt;是基地址，&lt;code&gt;step&lt;/code&gt;为地址间隔，&lt;code&gt;app_id&lt;/code&gt;为应用程序标识符。这种地址分配机制确保了不同用户程序被载入内核后，其入口点位于互不重叠的独立内存区域，从而避免执行地址冲突。&lt;/p&gt;
&lt;p&gt;在程序执行流程中，&lt;code&gt;_start&lt;/code&gt;函数会进一步调用&lt;code&gt;exit(main(argc, v.as_slice()))&lt;/code&gt;。此处引用的main函数被声明为弱符号(&lt;code&gt;weak symbol&lt;/code&gt;)，这种设计允许在链接阶段存在多个&lt;code&gt;main&lt;/code&gt;函数定义。最终链接器会根据实际加载的用户程序选择对应的&lt;code&gt;main&lt;/code&gt;函数实现，从而实现多态入口机制。该架构既保持了入口点的统一性，又支持不同用户程序拥有独立的业务逻辑入口。&lt;/p&gt;
&lt;p&gt;至此，用户态程序的准备工作已全部完成，其核心机制可总结如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态基址分配​​&lt;/li&gt;
&lt;li&gt;剥离 ELF 信息的二进制文件​​
&lt;ul&gt;
&lt;li&gt;编译生成的用户程序被处理为纯二进制文件（binary），移除了 ELF 格式的元数据（如节头表、符号表等），仅保留可执行代码和数据。&lt;/li&gt;
&lt;li&gt;内核可直接加载这些轻量化的二进制文件，无需解析复杂 ELF 结构，提升了运行效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;标准化入口与弱符号机制​​&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kernel&lt;/h2&gt;
&lt;p&gt;在 rCore 操作系统的构建流程中，用户程序的编译与内核镜像的生成遵循严格的自动化构建策略。当用户程序完成编译后，os/Makefile 会立即触发 cargo build 命令以生成最终的内核镜像。&lt;/p&gt;
&lt;p&gt;为确保用户程序能够正确嵌入内核并建立可执行环境，rCore 在编译前通过 ​​构建脚本（os/build.rs）​​ 执行关键配置操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut f = File::create(&quot;src/link_app.S&quot;).unwrap();
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 rCore 操作系统的构建过程中，os/build.rs 脚本会在编译阶段动态生成 os/src/link_app.S 汇编文件，其结构如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    .align 3
    .section .data
    .global _num_app
_num_app:
    .quad 2
    .quad app_0_start
    .quad app_1_start
    .quad app_1_end

    .section .data
    .global app_0_start
    .global app_0_end
app_0_start:
    .incbin &quot;../user/build/bin/xxx.bin&quot;
app_0_end:

    .section .data
    .global app_1_start
    .global app_1_end
app_1_start:
    .incbin &quot;../user/build/bin/yyy.bin&quot;
app_1_end:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在完成用户程序的静态嵌入与内核镜像的构建后，系统已具备启动条件。但在正式运行内核之前，需深入理解以下关键模块的设计与协作机制。&lt;/p&gt;
&lt;h3&gt;Load &amp;amp; Task&lt;/h3&gt;
&lt;p&gt;在操作系统的设计与实现中，每个任务（Task）、程序（Program）或进程（Process）均需通过一个核心数据结构来维护其执行状态和上下文信息。以传统操作系统理论中的进程控制块（Process Control Block, PCB）为参照，rCore操作系统采用了任务控制块（Task Control Block, TCB）这一抽象结构，用于集中管理应用程序的元数据、资源描述符、执行上下文以及调度相关属性，从而实现对任务生命周期的全流程管控：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub enum TaskStatus {
    /// uninitialized
    UnInit,
    /// ready to run
    Ready,
    /// running
    Running,
    /// exited
    Exited,
}

pub struct TaskControlBlock {
    /// The task status in it&apos;s lifecycle
    pub task_status: TaskStatus,
    /// The task context
    pub task_cx: TaskContext,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在操作系统的多任务管理机制中，单个 ​​任务控制块（Task Control Block, TCB）​​ 仅能维护一个任务的执行视图。为了全局管理所有任务的调度与状态，rCore 引入了 ​​任务管理器（TaskManager）​​ 作为顶层抽象，负责维护系统中所有 TCB 的集合。&lt;/p&gt;
&lt;p&gt;然而，在多任务并发环境下，直接访问 TCB 可能导致数据竞争或状态不一致问题。为此，rCore 采用 ​​内部封装（TaskManagerInner）​​ 的设计模式，将核心任务管理逻辑（如任务调度、状态切换、资源分配等）封装在受保护的内部结构中，并通过 ​​同步原语（如互斥锁或原子操作）​​ 确保线程安全。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct TaskManager {
    /// total number of tasks
    num_app: usize,
    /// use inner value to get mutable access
    inner: UPSafeCell&amp;lt;TaskManagerInner&amp;gt;,
}

/// Inner of Task Manager
pub struct TaskManagerInner {
    /// task list
    tasks: [TaskControlBlock; MAX_APP_NUM],
    /// id of current `Running` task
    current_task: usize,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在介绍更多之前，我们还需要了解一下任务上下文。在操作系统的任务调度机制中，​​任务上下文（Task Context）​​ 是实现多任务并发的核心抽象，其本质是任务执行状态的快照，用于在任务切换时保存和恢复关键寄存器状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct TaskContext {
    /// Ret position after task switching
    ra: usize,
    /// Stack pointer
    sp: usize,
    /// s0-11 register, callee saved
    s: [usize; 12],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ra(return address)&lt;/code&gt;：保存任务切换后应跳转的指令地址（如 __restore 或用户程序入口）。在上下文切换时，CPU 通过 ret 指令返回到 ra 指向的地址，实现执行流的无缝衔接。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sp(stack pointer)&lt;/code&gt;：维护任务的独立栈空间。每个任务需拥有专属内核栈（或用户栈），sp 确保切换后能正确访问局部变量和函数调用链。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sn(callee-saved register)&lt;/code&gt;：RISC-V 规定这些寄存器由被调用函数（Callee）保存。任务切换时需手动保存它们，避免破坏任务自身的计算状态（如循环变量、指针等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们就可以继续分析内核的执行：在rCore操作系统初始启动阶段，系统通过lazy_static宏实现了TASK_MANAGER全局变量的惰性初始化。对于每个用户任务，内核会调用goto_restore(init_app_cx(i))函数进行上下文初始化：其中init_app_cx(i)负责在内核栈中构建指定任务的初始执行上下文，其返回值作为该任务的内核栈指针；而goto_restore则将该任务的返回地址设置为__restore符号地址，从而建立从内核态到用户态的正确执行流切换路径。&lt;/p&gt;
&lt;p&gt;在任务管理初始化完成后，系统通过load_apps函数加载所有用户程序到物理内存。这些用户程序的加载地址严格遵循build.py构建脚本中预设的入口地址配置，确保用户态程序的二进制映像被精确映射到预期的内存区域，为后续的任务执行建立正确的地址空间布局。&lt;/p&gt;
&lt;p&gt;此时，我们还需要一个能够切换任务上下文的函数，因此使用&lt;code&gt;switch.S&lt;/code&gt;来实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存当前任务的&lt;code&gt;sp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;保存当前任务的&lt;code&gt;ra&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;保存当前任务的&lt;code&gt;sn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;载入下一个任务的&lt;code&gt;ra&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;载入下一个任务的&lt;code&gt;sn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;载入下一个任务的&lt;code&gt;sp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;最后通过&lt;code&gt;ret&lt;/code&gt;指令返回到&lt;code&gt;ra&lt;/code&gt;(即&lt;code&gt;__restore&lt;/code&gt;)指向的地址处继续执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;值得注意的是：&lt;code&gt;__switch&lt;/code&gt;&lt;strong&gt;只处理内核栈和寄存器的保存与回复，不会涉及用户态的回复和特权级切换，因此需要转入&lt;code&gt;__restore&lt;/code&gt;中继续执行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因从，我们继续分析 &lt;code&gt;run_first_task&lt;/code&gt;的全流程会发生什么。&lt;code&gt;run_first_task&lt;/code&gt;会取出第一个任务，然后与一个空任务进行交换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__switch(&amp;amp;mut _unused as *mut TaskContext, next_task_cx_ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，&lt;code&gt;__switch&lt;/code&gt;会载入第一个任务的栈和指针，并将&lt;code&gt;ra(__restore)&lt;/code&gt;载入，通过&lt;code&gt;ret&lt;/code&gt;进入到&lt;code&gt;__restore&lt;/code&gt;入口。在Load阶段有一点没有说明的是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每一个任务在初始化上下文时，都需要载入以下数据&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;寄存器值&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sstatus寄存器(设置为User模式)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sepc寄存器(设置为用户程序的入口点)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sp寄存器(用户栈指针，x[2])&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过查阅&lt;a&gt;RISC-V ISA PREVILEGE Supervisor-Level ISA&lt;/a&gt;可以了解到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;sstatus&lt;/strong&gt;寄存器用于恢复CPU特权级状态（例如重新允许中断、切换回用户态）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sepc&lt;/strong&gt;寄存器用于设定&lt;code&gt;sret&lt;/code&gt;指令的返回地址（即返回到用户程序的哪条指令）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sscratch&lt;/strong&gt;寄存器用于保存用户栈指针，用于后续交换栈指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并且，&lt;strong&gt;在&lt;code&gt;init_app_cx&lt;/code&gt;函数中我们将每一个任务的上下文地址压入了内核栈中&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
    get_base_i(app_id),
    USER_STACK[app_id].get_sp(),
))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，这里就有了一个很清晰的对于用户栈和内核栈的概念了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TaskContext
    |--&amp;gt; ra -&amp;gt; __restore
    |--&amp;gt; sp -&amp;gt; kstack --&amp;gt; TrapContext
    ---&amp;gt; sn                   |--&amp;gt; xn
                              |--&amp;gt; sstatus-&amp;gt; User Mode
                              |--&amp;gt; sepc   -&amp;gt; User Task Base Entry
                              |              (APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT)
                              ---&amp;gt; sp(x2) -&amp;gt; user stack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250409203607718.png&quot; alt=&quot;__switch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在我们重新看回&lt;code&gt;run_first_task&lt;/code&gt;的流程，当我们执行到&lt;code&gt;__restore&lt;/code&gt;时，我们直接载入第一个任务的&lt;code&gt;sstatus&lt;/code&gt;、&lt;code&gt;sepc&lt;/code&gt;和&lt;code&gt;sp&lt;/code&gt;指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，操作系统已经知道了&lt;code&gt;sret&lt;/code&gt;要返回的用户程序入口以及用户程序的栈，然后再载入其他寄存器到操作系统中(&lt;strong&gt;注意，此时的&lt;code&gt;sp&lt;/code&gt;还是指向的内核栈&lt;/strong&gt;)；载入成功后，&lt;strong&gt;我们需要将内核指针移动$34*8$个字节&lt;/strong&gt;(&lt;code&gt;sizeof TrapContex&lt;/code&gt;)，然后再通过&lt;code&gt;csrrw&lt;/code&gt;交换内核栈和用户栈(此时&lt;code&gt;sscratch -&amp;gt; kstack, sp -&amp;gt; user stack&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;最后，我们通过&lt;code&gt;sret&lt;/code&gt;进入到第一个用户程序的入口点，便可以开始执行了。&lt;/p&gt;
&lt;p&gt;在系统实现层面，部分开发者可能对任务调度机制存在疑问：&lt;strong&gt;为何仅调用&lt;code&gt;run_first_task&lt;/code&gt;函数即可完成所有应用程序的执行流程。虽然直觉上可能联想到&lt;code&gt;trap&lt;/code&gt;中断机制的作用，但在分析&lt;code&gt;hello_world&lt;/code&gt;这类无显式异常触发的用例时仍会产生困惑&lt;/strong&gt;。需要特别说明的是，当前内核采用了一种精简的设计架构，其任务调度必然依赖&lt;code&gt;trap&lt;/code&gt;异常处理机制实现——具体而言，内核在异常处理例程中隐式地执行了任务上下文保存与切换操作，从而实现了多任务的轮转调度。这种设计使得用户态程序无需主动触发异常即可被系统透明地管理，体现了中断驱动式调度（interrupt-driven scheduling）的典型特征。&lt;/p&gt;
&lt;h3&gt;Trap&lt;/h3&gt;
&lt;p&gt;对于&lt;code&gt;rCore&lt;/code&gt;的&lt;code&gt;trap&lt;/code&gt;机制，个人认为&lt;a href=&quot;https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter2/4trap-handling.html#trap&quot;&gt;rCore trap&lt;/a&gt;讲的比较详细，便不再过于赘述。可以给出一些&lt;code&gt;debug&lt;/code&gt;图例仅供参考。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250409204246034.png&quot; alt=&quot;__switch ra &amp;amp; sp&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是关于&lt;code&gt;__switch&lt;/code&gt;函数中的&lt;code&gt;ra&lt;/code&gt;和&lt;code&gt;sp&lt;/code&gt;地址的查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250409204347288.png&quot; alt=&quot;__restore sp&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是关于&lt;code&gt;__restore&lt;/code&gt;函数中的&lt;code&gt;kstack&lt;/code&gt;地址的查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/20250409204458874.png&quot; alt=&quot;__restore user sp&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是关于&lt;code&gt;__restore&lt;/code&gt;函数中的&lt;code&gt;user stack&lt;/code&gt;地址的查看。&lt;/p&gt;
&lt;h3&gt;Syscall&lt;/h3&gt;
&lt;p&gt;对于操作系统来说，系统调用是一件十分简单的事情(如果你已经搞明白了&lt;code&gt;trap&lt;/code&gt;机制的原理)，系统调用本质上就是个精心设计的陷阱(trap)。当你理解了&lt;code&gt;ecall&lt;/code&gt;这个魔法指令，实现系统调用就像搭积木一样简单。：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn syscall(id: usize, args: [usize; 3]) -&amp;gt; isize {
    let mut ret: isize;
    unsafe {
        core::arch::asm!(
            &quot;ecall&quot;,
            inlateout(&quot;x10&quot;) args[0] =&amp;gt; ret,
            in(&quot;x11&quot;) args[1],
            in(&quot;x12&quot;) args[2],
            in(&quot;x17&quot;) id
        );
    }
    ret
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，如果你了解过&lt;code&gt;xv6-riscv&lt;/code&gt;的话，系统调用也不在话下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.globl start
start:
    la a0, init
    la a1, argv
    li a7, SYS_exec
    ecall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们回到上面没有解决的问题：&lt;strong&gt;为什么类似于&lt;code&gt;hello_world&lt;/code&gt;的应用程序能够自动的切换为下一个应用程序呢&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;我们需要回忆起：我们在编译出应用程序的&lt;code&gt;binary&lt;/code&gt;时，使用的&lt;code&gt;weak main&lt;/code&gt;，因此，当我们执行到应用程序的&lt;code&gt;_start&lt;/code&gt;入口时可以发现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[link_section = &quot;.text.entry&quot;]
pub extern &quot;C&quot; fn _start(argc: usize, argv: usize) -&amp;gt; ! {
  exit(main(argc, v.as_slice()));
}

pub fn exit(exit_code: i32) -&amp;gt; ! {
  console::flush();
  sys_exit(exit_code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;main&lt;/code&gt;调用居然包裹了一个&lt;code&gt;exit&lt;/code&gt;调用，而这个&lt;code&gt;exit&lt;/code&gt;调用正是一个系统调用&lt;code&gt;sys_exit&lt;/code&gt;，因此，当&lt;code&gt;hello_world&lt;/code&gt;执行完毕之后，会因为系统调用而进入&lt;code&gt;trap_handler&lt;/code&gt;从而切换了下一个应用程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn exit_current_and_run_next() {
    mark_current_exited();
    run_next_task();
}

Trap::Exception(Exception::UserEnvCall) =&amp;gt; {
    // jump to next instruction anyway
    cx.sepc += 4;
    // get system call return value
    cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) =&amp;gt; {
    println!(&quot;[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.&quot;, stval, cx.sepc);
    exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) =&amp;gt; {
    println!(&quot;[kernel] IllegalInstruction in application, kernel killed it.&quot;);
    exit_current_and_run_next();
}
Trap::Interrupt(Interrupt::SupervisorTimer) =&amp;gt; {
    set_next_trigger();
    suspend_current_and_run_next();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，&lt;code&gt;rCore&lt;/code&gt;的第三章实现流程便分析完毕了。&lt;/p&gt;
</content:encoded></item><item><title>STL17: New Features</title><link>https://chenmiaoi.github.io/2025/02/15/STL17-New-Features/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2025/02/15/STL17-New-Features/</guid><pubDate>Sat, 15 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在讨论C++语言的背景之前，我们首先需要认识到一个现实：由于国内就业压力和IT行业的相对保守性，大多数C++学习者并未深入研究STL(标准模板库)以及更新的C++标准。即便是C++23标准已经发布，许多C++开发人员和学习者仍然停留在较为传统的编程风格，常常只掌握&lt;code&gt;C with Class&lt;/code&gt;或&lt;code&gt;C with Simple STL&lt;/code&gt;，这导致了C++语言的潜力未能得到充分发挥。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// C with Class             // C with Simple STL
class Person {              class Person {
public:                     public:
    char name[32];              std::string name;
    int age;                    int age;
    char gender[5];             std::string gender;
    ...                         ...
};                          };

class Person person;        class Person person;
person.name = &quot;Miao&quot;;       person.name = &quot;Miao&quot;;
person.age = 18;            person.age = 18;
person.gender = &quot;male&quot;;     person.gender = &quot;male&quot;;
...                         ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种现象的存在在一定程度上阻碍了C++的学习和应用，限制了开发者对语言本身的深入理解和有效使用。C++作为一种多范式编程语言，本应在面向对象、泛型编程、模板元编程等领域展现出更强大的优势。然而，过于依赖传统编程模式的做法，不仅无法体现C++语言的真正风采，甚至有可能使开发者在处理复杂任务时错失现代C++所提供的诸多便利和性能优化。&lt;/p&gt;
&lt;p&gt;尽管C++语言本身近年来出现了诸如特性膨胀等问题，这些问题并不在本系列讨论的范围内，但需要明确的是，C++语言的演进过程仍在持续，理解和掌握语言的最新特性对提升开发者的能力是至关重要的。&lt;/p&gt;
&lt;p&gt;本系列的目标是帮助那些已经具备C++基础，但尚未深入理解和运用STL(标准模板库)以及C++各种现代特性的开发者，掌握如何使用C++编写更加现代、有效且高效的代码。如果你还没有学习过C++，那么本系列可能并不适合你，因为我们将假定读者已经对C++的基本语法和常见概念有所了解，并且目标是让你能够充分利用语言的最新功能来编写高质量的代码。&lt;/p&gt;
&lt;p&gt;值得注意的是，许多开发者对C++11有一定的了解，毕竟《C++ Primer》和《Effective C++》等经典书籍都是基于C++11编写的。然而，自C++11之后，尤其是C++14、C++17及更晚的标准发布后，关于这些版本的深入学习和实践的机会相对较少。因此，本系列将从C++17开始进行讲解。对于C++17之前的内容，只有在必要的情况下才会进行详细阐述。如果读者对C++11之前的特性有疑问，建议自行查阅相关文档，以便更好地跟进本系列的内容。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Structured Binding&lt;/h2&gt;
&lt;p&gt;在现代编程语言中，解构(unpacking)已成为常见的功能之一，例如在Rust中，可以很方便地解构元组(tuple)或结构体(struct)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let tup = (1, &quot;hello&quot;, 3.14);

let (a, b, c) = tup;
println!(&quot;a: {}, b: {}, c: {}&quot;, a, b, c);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在C++11中，除了&lt;code&gt;std::tuple&lt;/code&gt;，还引入了一个非常有用(?)的函数——&lt;code&gt;std::tie&lt;/code&gt;。&lt;code&gt;std::tie&lt;/code&gt;可以将一个元组的元素与变量绑定，从而提供类似解构的功能，虽然它在语法上和结构化绑定(&lt;code&gt;Structured Binding&lt;/code&gt;)有所不同。&lt;code&gt;std::tie&lt;/code&gt;的&lt;strong&gt;作用是将元组或结构体的元素与现有变量进行关联&lt;/strong&gt;，而不像结构化绑定那样直接创建新变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::tuple&amp;lt;int, std::string, double&amp;gt; tup = {1, &quot;hello&quot;, 3.14};

int a;
std::string b;
double c;
// 使用 std::tie 来解构元组
std::tie(a, b, c) = tup;
std::cout &amp;lt;&amp;lt; &quot;a: &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;, b: &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; &quot;, c: &quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出，std::tie要求我们在解构之前提前规划好每个元素的类型和相应的局部变量，这无疑对编码效率和灵活性造成了一定的限制。具体来说，使用std::tie时，必须显式地指定每个变量的类型，并且这些变量必须在解构之前已经声明好。&lt;/p&gt;
&lt;p&gt;并且，&lt;code&gt;std::tie&lt;/code&gt;并不支持直接的引用解构，需要使用&lt;code&gt;std::ref&lt;/code&gt;，这也使得学习和编码难度上升。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::tuple&amp;lt;int, std::string, double&amp;gt; tup = {1, &quot;hello&quot;, 3.14};

int a;
std::string b;
double c;

// 使用 std::ref 传递引用
std::tie(std::ref(a), std::ref(b), std::ref(c)) = tup;

// 修改解构后的值
a = 10;
b = &quot;world&quot;;
c = 6.28;

std::cout &amp;lt;&amp;lt; &quot;a: &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;, b: &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; &quot;, c: &quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，为了更好的支持更方便的解构，C++17引入了结构化绑定(Structured Binding)。通过这种语法糖，C++能够以类似的方式解构std::pair、std::tuple、结构体和数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto [var1, var2, ...] = &amp;lt;pair, tuple, struct, or array expression&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::pair&lt;/code&gt;和&lt;code&gt;std::tuple&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct&lt;/code&gt;：结构体的成员必须是非静态的(non-static)，且定义在同一个类中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;array&lt;/code&gt;：必须是固定大小的数组(即大小在编译时已知)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常，我们会结合&lt;code&gt;auto&lt;/code&gt;来自动推导类型，&lt;strong&gt;如果条件允许的情况下，尽可能的使用引用以减少拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::tuple&amp;lt;int, std::string, double&amp;gt; tup = {1, &quot;hello&quot;, 3.14};
auto [a, b, c] = tup;  // 解构元组
std::cout &amp;lt;&amp;lt; &quot;a: &quot; &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;, b: &quot; &amp;lt;&amp;lt; b &amp;lt;&amp;lt; &quot;, c: &quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;QUESTION: 许多人可能会认为大量使用解构(比如通过结构化绑定来解构元组、std::pair 或者其他数据结构)会导致性能问题，尤其是在涉及到返回值的情况下。传统上，C++开发者习惯于通过引用参数来传递结果(即使用“out parameter”模式)，认为这样能够避免拷贝操作，提高效率。然而，现代C++编译器通常会对返回值进行返回值优化(RVO，Return Value Optimization)或命名返回值优化(NRVO，Named Return Value Optimization)，从而大大减少了不必要的拷贝开销。&lt;br /&gt;
关于RVO和NRVO会在额外的单独章节中进行讲解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当然，&lt;code&gt;structured binding&lt;/code&gt;并非全都是好处，相比&lt;code&gt;std::tie&lt;/code&gt;，&lt;code&gt;structured binding&lt;/code&gt;没有提供一个直接的机制来忽略元组或结构体中的某些元素，类似于Rust中的&lt;code&gt;_&lt;/code&gt;或者说&lt;code&gt;std::ignore&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此，你必须显示地声明每一个位置的变量进行接收，甚至是你根本不关心的值。&lt;/p&gt;
&lt;h3&gt;Action&lt;/h3&gt;
&lt;p&gt;在实际的开发中，我们会在以下几个方面经常用到&lt;code&gt;structured binding&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::map&lt;/code&gt;(实际上是&lt;code&gt;std::pair&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::map&amp;lt;int, std::string&amp;gt; m = {{1, &quot;one&quot;}, {2, &quot;two&quot;}, {3, &quot;three&quot;}};
for (const auto&amp;amp; [key, value] : m) {
    std::cout &amp;lt;&amp;lt; &quot;Key: &quot; &amp;lt;&amp;lt; key &amp;lt;&amp;lt; &quot;, Value: &quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; std::endl;
}

struct Point {
    int x, y;
};
std::vector&amp;lt;Point&amp;gt; points = {{1, 2}, {3, 4}, {5, 6}};
for (auto&amp;amp; [x, y] : points) {
    std::cout &amp;lt;&amp;lt; &quot;x: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot;, y: &quot; &amp;lt;&amp;lt; y &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;function return value&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::tuple&amp;lt;int, double, std::string&amp;gt; get_values() {
    return {42, 3.14, &quot;example&quot;};
}

auto [x, y, z] = get_values();
std::cout &amp;lt;&amp;lt; &quot;x: &quot; &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &quot;, y: &quot; &amp;lt;&amp;lt; y &amp;lt;&amp;lt; &quot;, z: &quot; &amp;lt;&amp;lt; z &amp;lt;&amp;lt; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;error handling&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;std::pair&amp;lt;bool, std::string&amp;gt; get_optional_value() {
    return std::make_pair(true, &quot;example&quot;);
}

if (auto [x, y] = get_optional_value(); x) {
    std::cout &amp;lt;&amp;lt; &quot;y: &quot; &amp;lt;&amp;lt; y &amp;lt;&amp;lt; std::endl;
} else {
    std::cout &amp;lt;&amp;lt; &quot;No value present!&quot; &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How To Work&lt;/h3&gt;
&lt;p&gt;现在我们来简单讲解一下结构体绑定的实现。在 &lt;code&gt;C++17&lt;/code&gt; 的 结构化绑定(&lt;code&gt;structured binding&lt;/code&gt;) 机制中，编译器会为 &lt;code&gt;identifier-list&lt;/code&gt; ($v0, v1, v2, ...$) 生成一组变量，并将其绑定到 &lt;code&gt;initializer&lt;/code&gt;(初始化表达式)的元素。该机制的底层实现依赖于 &lt;code&gt;自动类型推导&lt;/code&gt; 和 &lt;code&gt;对象解构&lt;/code&gt;，并有以下假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设&lt;code&gt;cv&lt;/code&gt;代表&lt;code&gt;cv-qualifiers&lt;/code&gt;：即&lt;code&gt;cv&lt;/code&gt;限定符(&lt;code&gt;const&lt;/code&gt;和&lt;s&gt;volatile&lt;/s&gt;)。&lt;em&gt;在C++20中，&lt;code&gt;volatile&lt;/code&gt;的用法已被移除&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;设&lt;code&gt;S&lt;/code&gt;代表&lt;code&gt;decl-specifier-seq&lt;/code&gt;中的&lt;code&gt;storage-class specifiers&lt;/code&gt;(例如：&lt;code&gt;static&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;设&lt;code&gt;A&lt;/code&gt;代表元素类型&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;当 &lt;code&gt;initializer&lt;/code&gt; 的 赋值表达式(&lt;code&gt;assignment-expression&lt;/code&gt;) 具有 &lt;code&gt;cv A&lt;/code&gt; 类型的数组，且没有引用限定符(&lt;code&gt;ref-qualifier&lt;/code&gt;)时，结构化绑定的行为如下：
$$
attribute-specifier-seqopt \ S \ cv \ A \ e ;
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// Seem To:
const int arr[3] = {1, 2, 3};  // 这个数组类似 initializer
auto [x, y, z] = arr;

const int e[3] = arr;          // e 的声明方式类似于这里 attribute-specifier-seqopt S cv A e ;
auto&amp;amp; x = e[0];  // x 绑定到 e 的第 0 个元素
auto&amp;amp; y = e[1];  // y 绑定到 e 的第 1 个元素
auto&amp;amp; z = e[2];  // z 绑定到 e 的第 2 个元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;e&lt;/code&gt;&lt;strong&gt;的每个元素都会从&lt;code&gt;assignment-expression&lt;/code&gt;的对应元素中拷贝初始化或直接初始化，具体方式由&lt;code&gt;initializer&lt;/code&gt;的形式所决定&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 &lt;code&gt;initializer&lt;/code&gt; 的 赋值表达式(&lt;code&gt;assignment-expression&lt;/code&gt;)不是数组类型，则结构化绑定的行为如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
attribute-specifier-seqopt \ decl-specifier-seq \ ref-qualifieropt \ e = initializer ;
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;attribute-specifier-seqopt&lt;/code&gt;(可选的属性说明符序列)：允许使用属性(如&lt;code&gt;[[nodiscard]]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;decl-specifier-seq&lt;/code&gt;：通常为 &lt;code&gt;auto&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ref-qualifier&lt;/code&gt;：可能是&lt;code&gt;&amp;amp;&lt;/code&gt;或&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;，表示是否使用引用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Seem To:
std::tuple&amp;lt;int, double, char&amp;gt; t = {1, 2.5, &apos;a&apos;};
auto [x, y, z] = t;

auto e = t;   // 这里的 e 相当于 attribute-specifier-seqopt decl-specifier-seq ref-qualifieropt e = initializer;
auto&amp;amp; x = std::get&amp;lt;0&amp;gt;(e);
auto&amp;amp; y = std::get&amp;lt;1&amp;gt;(e);
auto&amp;amp; z = std::get&amp;lt;2&amp;gt;(e);

// 实际上可能的内部展开
decltype(auto) __binding_obj = tuple;
decltype(auto) a = std::get&amp;lt;0&amp;gt;(__binding_obj);
decltype(auto) b = std::get&amp;lt;1&amp;gt;(__binding_obj);
decltype(auto) c = std::get&amp;lt;2&amp;gt;(__binding_obj);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Scoped Variable With If and Switch&lt;/h2&gt;
&lt;p&gt;在 C++ 代码设计中，变量的作用域管理是影响代码可读性和可维护性的关键因素之一。良好的作用域管理不仅有助于避免命名冲突，还能减少无关变量对代码逻辑的干扰。在 C++17 之前，开发者经常面临一种常见情况：&lt;/p&gt;
&lt;p&gt;在 C++ 代码中，经常需要对某个表达式的计算结果进行条件判断。例如，考虑以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto s = is_ok(something);
if (!s) {
    // error handling
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在此代码中，&lt;code&gt;s&lt;/code&gt; 仅用于 if 语句的判断条件，而在 &lt;code&gt;if&lt;/code&gt; 语句之外并无实际用途。然而，由于 C++17 之前 if 语句不支持在条件部分定义变量，因此 s 不得不提前声明，从而污染了外部作用域。&lt;/p&gt;
&lt;p&gt;这种不必要的变量泄露可能导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命名冲突 (Name Clashes)：在相同作用域下反复使用相同名称的变量，可能增加代码的复杂性和可读性负担。&lt;/li&gt;
&lt;li&gt;作用域污染 (Scope Pollution)：某些变量仅在特定语句块内使用，但由于语言特性，不得不提升其作用域。&lt;/li&gt;
&lt;li&gt;代码可读性下降 (Reduced Readability)：读者需要关注不必要的变量定义，降低代码逻辑的直观性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了提高代码的可维护性，现代 C++ 遵循 最小作用域原则 (Minimizing Scope of Variables)，即“变量的作用域应尽可能小，仅限于其被使用的最小范围”。&lt;/p&gt;
&lt;p&gt;这一原则的核心思想包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部化 (Localization)：变量的生命周期应尽可能短，避免被误用或无意中修改。&lt;/li&gt;
&lt;li&gt;封装性 (Encapsulation)：将变量限定在尽可能小的作用域，以减少潜在的命名冲突。&lt;/li&gt;
&lt;li&gt;即时初始化 (Immediate Initialization)：在变量定义时立即初始化，减少未初始化变量的风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 C++17 之前，由于 if 语句不支持直接在条件部分定义变量，开发者不得不在 if 语句之前定义额外的变量，导致作用域扩展。因此，C++17 引入了一种新特性来优化这一模式。&lt;/p&gt;
&lt;p&gt;C++17 引入了 if 语句的 初始化子句 (Init-Statement)，允许开发者在 if 语句的条件部分直接声明和初始化变量，从而优化作用域管理。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (auto s = is_ok(something); !s) {
    // error handling
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Action&lt;/h3&gt;
&lt;p&gt;在 C++ 代码中，资源的生命周期通常与变量的作用域绑定，这是 资源获取即初始化 (RAII, Resource Acquisition Is Initialization) 的核心思想。RAII 机制确保资源在对象构造时获取，并在对象析构时释放。例如，在多线程编程中，std::lock_guard&lt;a&gt;std::mutex&lt;/a&gt; 通过 RAII 自动管理互斥锁 (std::mutex) 的加锁与解锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// using if init-statement                                              // using traditional if 
if (std::lock_guard&amp;lt;std::mutex&amp;gt; lg {my_mutex}; some_condition) {        {
    // do something                                                         std::lock_guard&amp;lt;std::mutex&amp;gt; lg {my_mutex};
}                                                                           if (some_condition) {
                                                                                // do something
                                                                            }
                                                                        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式适用于任何 RAII 类型的资源管理，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std::unique_lock&amp;lt;std::mutex&amp;gt;&lt;/code&gt;(支持更灵活的锁管理)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::ifstream&lt;/code&gt;(文件流自动关闭)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::scoped_lock&lt;/code&gt;(C++17 引入的通用多互斥锁管理)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;How To Work&lt;/h3&gt;
&lt;p&gt;实际上，&lt;code&gt;if init-statement&lt;/code&gt; 本质上是一种基础的语法糖，它可以被等价转换为如下形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp;\texttt{if constexpr}&lt;em&gt;{opt} \ (\texttt{init-statement}&lt;/em&gt;{opt} \ \texttt{condition}) \ \texttt{statement} = \
{\
&amp;amp;\texttt{init-statement}&lt;em&gt;{opt} \
&amp;amp;\texttt{if constexpr}&lt;/em&gt;{opt} \ (\texttt{condition}) \ \texttt{statement} \
}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;这意味着 if init-statement 语句的行为等同于在 if 语句之前引入一个新的作用域 {}，并在其中执行 init-statement，然后再执行 if 条件判断。这种转换不会改变底层语义，只是提供了一种更加紧凑的写法，使代码更易读、更符合局部作用域管理的最佳实践。&lt;/p&gt;
&lt;h2&gt;Automatically deduce the resulting class type&lt;/h2&gt;
&lt;h2&gt;Constexpr&lt;/h2&gt;
&lt;p&gt;在现代编程中，C++关注到编译时常量的引入可显著减少运行时的计算负担，特别是在高频调用的场景中，避免了重复的运行时计算。因此，在C++11中，C++首次提出了&lt;code&gt;constexpr&lt;/code&gt;关键字允许开发者显式声明一个变量或函数的值在编译时求解，确保其为常量表达式，进而实现更早的优化和更严格的编译期检查。&lt;/p&gt;
&lt;h2&gt;Inline&lt;/h2&gt;
&lt;p&gt;在C/C++编程语言中，&lt;code&gt;inline&lt;/code&gt;关键字是一个广为人知的特性，大多数开发者对其基本功能有一定的了解。然而，对于C语言中的inline与C++中的inline之间的区别，以及随着C++标准的演进，inline语义的变化，却鲜有人深入探讨。&lt;/p&gt;
&lt;h3&gt;C Inline&lt;/h3&gt;
&lt;p&gt;首先，我们将详细探讨C语言中的inline关键字的定义及其作用(鉴于本系列教程主要关注C++，因此我们将仅讨论到C11标准)。&lt;/p&gt;
&lt;p&gt;第一步我们先来了解下C语言中inline的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提高执行效率：通过消除函数调用开销，减少函数调用的栈操作，适合小函数。&lt;/li&gt;
&lt;li&gt;优化编译器的优化机会：可以让编译器更好地进行进一步的优化，如常量折叠、死代码消除等。&lt;/li&gt;
&lt;li&gt;改善代码可读性和可维护性：比宏更加清晰、类型安全且易于调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在C11标准(ISO/IEC 9800:201x文档)中对&lt;code&gt;inline&lt;/code&gt;关键字有明确的约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inline&lt;/code&gt;应该只能被用于一个函数标识符的声明中。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;inline int x; // Error, inline specifier allowed on function declarations only
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具有外部链接(external linkage)的内联函数定义不应该包含含有静态存储期(static storage duration)或线程存储期(thread storage duration)的可修改对象的定义，也不应该引用具有内部链接(internal linkage)的标识符。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;静态存储期的对象(如 static 变量)或线程存储期的对象(如 thread_local 变量)在程序运行期间具有持久性。内联函数可能会被多次展开到不同的调用点，如果允许定义这类对象，可能会导致多个副本或冲突，从而引发未定义行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;inline void counter() {
    static int x = 0;
    x++;
}

note: use &apos;static&apos; to give inline function &apos;counter&apos; internal linkage
warning: non-constant static local variable in inline function may be different in different files
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;内部链接(internal linkage)的标识符(如 static 函数或 static 全局变量)仅在当前翻译单元(translation unit)内可见。如果内联函数引用了这类标识符，而该内联函数被其他翻译单元使用，就会导致链接错误或未定义行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int internal_var = 10;

inline void counter() {
    int y = internal_var;
}

warning: static variable &apos;internal_var&apos; is used in an inline function with external linkage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这些限制的目的是确保内联函数的行为在不同翻译单元之间保持一致，并避免潜在的链接或运行时问题。在实际测试中，编译器并不会将其视作inline的&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;hosted environment&lt;/code&gt;中，&lt;code&gt;main&lt;/code&gt;函数的声明不得出现任何函数说明符(function specifier)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;标准明确禁止在 main 函数的声明中使用任何函数说明符。这是因为 main 函数的调用和返回机制是由运行时环境(runtime environment)管理的，任何额外的修饰都可能破坏其标准行为。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C Standard Enviroment&lt;/p&gt;
&lt;p&gt;C语言标准规定了两种主要的执行环境：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hosted Environment&lt;br /&gt;
完整的C语言执行环境，通常依赖于操作系统，并提供完整的标准库支持；程序从&lt;code&gt;main&lt;/code&gt;函数开始执行&lt;/p&gt;
&lt;p&gt;Freestanding Environment&lt;br /&gt;
不依赖于操作系统的简化执行环境，通常用于嵌入式系统、操作系统内核或裸机程序；程序的入口点不一定是 &lt;code&gt;main&lt;/code&gt; 函数，具体由实现定义&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;任何具有内部链接(internal linkage)的函数都可以是内联函数。对于具有外部链接(external linkage)的函数，适用以下限制：&lt;/p&gt;
&lt;p&gt;如果一个函数被声明为 inline，那么它必须在同一翻译单元(translation unit)中定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline void func() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在某个翻译单元中，某个函数的所有文件作用域声明都包含 inline 函数说明符且没有 extern，那么该翻译单元中的定义是一个内联定义(inline definition)&lt;/p&gt;
&lt;p&gt;首先我们需要理解什么叫做：&lt;strong&gt;所有文件作用域(all of the file scope declarations for a function in a translation unit)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// file1.c
inline void func(); // 
inline void func() {} //
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// file2.c
extern void func(); //
inline void func() {} //
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内联定义不会为该函数提供外部定义(external definition)，也不禁止在其他翻译单元中提供外部定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// file1.c
inline void func() {}

// file2.c
void func() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内联定义是外部定义的替代方案，编译器可以使用它来实现同一翻译单元中对函数的调用。对于函数的调用是使用内联定义还是外部定义，是由实现定义的(unspecified)。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;Using C Inline&lt;/h4&gt;
&lt;p&gt;在C语言中，inline关键字用于提示编译器将函数内联展开，以减少函数调用的开销。然而，inline的行为会受到extern和static关键字的影响，尤其是在涉及函数的链接属性(linkage)时。在实际开发中，许多C语言开发者通常更倾向于使用static inline，这也是最为常见和推荐的用法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;static inline&lt;/code&gt;用于定义具有内部链接(internal linkage)的内联函数。这种函数的作用域仅限于当前翻译单元(translation unit)，其他翻译单元无法访问该函数。由于函数的作用域被限制在当前翻译单元内，因此不会与其他翻译单元中的同名函数产生冲突。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// file1.c
static inline int add(int a, int b) {
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，add函数仅在file1.c中可见，其他翻译单元无法调用它。编译器可以选择将add函数内联展开，以减少函数调用的开销。&lt;/p&gt;
&lt;p&gt;extern inline用于定义具有外部链接(external linkage)的内联函数。这种函数的作用域不限于当前翻译单元，其他翻译单元可以访问该函数。然而，使用extern inline时，&lt;strong&gt;必须确保在其他翻译单元中提供一个非内联的外部定义，以便链接器能够正确解析函数的引用&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// file1.c
extern inline int add(int a, int b) {
    return a + b;
}

// file2.c
int add(int a, int b) {
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，file1.c中的add函数是一个内联定义，编译器可以选择将其内联展开。而在file2.c中，提供了一个非内联的外部定义，以确保链接器能够找到该函数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果仅使用inline关键字而不加extern或static修饰，其行为类似于extern inline，但具体的链接属性由编译器决定。这种用法可能会导致不确定的行为，尤其是在跨翻译单元使用时。因此，为了明确函数的链接属性，建议在使用inline时显式指定extern inline或static inline&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过合理使用static inline和extern inline，可以在C语言中实现高效的内联函数，同时避免链接冲突和未定义行为。在实际开发中，static inline因其简单性和明确的作用域限制，成为最为常见和推荐的用法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上，在开发中的99%都不会用上奇怪的&lt;code&gt;extern inline&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;C++ inline&lt;/h3&gt;
&lt;h4&gt;C++98&lt;/h4&gt;
&lt;p&gt;C++98标准中引入了inline关键字，而国内许多C++开发者和学习者往往是从C语言过渡到C++的，因此可能会误认为C语言中的inline与C++98中的inline功能相同，使用方式也一致。然而，尽管两者在语法上相似，但其设计目标和行为存在显著差异。&lt;/p&gt;
&lt;p&gt;在C语言中，inline关键字主要用于优化提示，鼓励编译器将函数内联展开，以减少函数调用的开销。C99标准引入inline时，更注重与C语言的编译模型和链接规则的兼容性。C语言中的inline函数通常需要在某个翻译单元中提供非内联的外部定义，以确保链接器能够正确解析函数的引用。此外，C语言禁止在具有外部链接的inline函数中定义静态存储期对象(如static变量)，以避免多个翻译单元中的重复定义问题。&lt;/p&gt;
&lt;p&gt;C++98中的inline关键字不仅是一种优化提示，还是一种链接属性机制。其主要目标是支持头文件中的函数定义，避免在多个翻译单元中重复定义函数导致的链接错误。因此，在C++98中，在多个翻译单元中定义相同的inline函数(通常通过头文件实现)是合法的，链接器会确保只有一个实例被使用。此外，C++98不需要显式提供非内联的外部定义，编译器会自动处理inline函数的链接问题。&lt;/p&gt;
&lt;p&gt;并且，C++98允许在inline函数中定义静态存储期对象(如static变量)，并且这些对象在多个翻译单元中是共享的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline int counter() {
    static int x = 0;
    return ++x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，static变量x在多个翻译单元中共享，确保了其唯一性和一致性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;值得注意的是，由于C++中有&lt;code&gt;class&lt;/code&gt;的概念，因此，在完全定义的&lt;code&gt;class/struct/union&lt;/code&gt;中，无论是成员函数还是友元函数，都是一个隐式的inline函数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于大多数C++开发者来说，上面涉及到的C++98前的inline则被一直延续下去(此时的inline主要是作为编译器是否优化的指标)；但在C++11后，更加强调 inline 函数能够在多个翻译单元中出现且不导致重复定义的链接错误。&lt;/p&gt;
&lt;h4&gt;C++11&lt;/h4&gt;
&lt;p&gt;在C++11中，inline关键字的功能发生了显著变化。它不再主要作为编译器优化函数的参考指标，而是更多地用于解决链接问题，确保inline函数能够在多个翻译单元中重复定义而不会导致链接错误。同时，C++11沿用了C++98中class、struct和union成员函数隐式内联的规则，即类内定义的成员函数默认具有inline属性。&lt;/p&gt;
&lt;p&gt;C++11引入了constexpr关键字，用于定义编译时常量表达式函数。constexpr函数在首次定义时隐式地具有inline属性，这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;constexpr函数可以在多个翻译单元中定义(通常通过头文件实现)，而不会导致链接错误。&lt;/li&gt;
&lt;li&gt;编译器会自动处理constexpr函数的链接问题，无需显式提供非内联的外部定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;constexpr int add(int a, int b) {
    return a + b;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++11引入了delete关键字，用于禁用类的某些成员函数(如拷贝构造函数、拷贝赋值运算符等)。被删除的函数隐式地具有inline属性，因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;被删除函数的定义可以出现在多个翻译单元中，而不会导致链接错误。&lt;/li&gt;
&lt;li&gt;这种设计解决了= delete声明可能引发的重复定义问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    MyClass() = default;
    MyClass(const MyClass&amp;amp;) = delete;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随着软件规模的扩大，类名和函数名重复的可能性显著增加。为了避免命名冲突，开发者通常使用namespace来组织代码。然而，嵌套过多的namespace以及版本控制的需求，使得开发者常常依赖宏或符号前缀(Symbol Prefix Overlay, SPO)来区分不同版本的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Using Macro
#define LIB_VERSION 1

namespace Lib {
#if LIB_VERSION == 1
    void func() { std::cout &amp;lt;&amp;lt; &quot;v1&quot; &amp;lt;&amp;lt; std::endl; }
#elif LIB_VERSION == 2
    void func() { std::cout &amp;lt;&amp;lt; &quot;v2&quot; &amp;lt;&amp;lt; std::endl; }
#endif
}

// Using SOO
namespace Lib {
    void v1_func() { std::cout &amp;lt;&amp;lt; &quot;v1&quot; &amp;lt;&amp;lt; std::endl; }
    void v2_func() { std::cout &amp;lt;&amp;lt; &quot;v2&quot; &amp;lt;&amp;lt; std::endl; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++11引入了&lt;code&gt;inline namespace&lt;/code&gt;，为版本控制提供了更安全、更灵活的解决方案。&lt;code&gt;inline namespace&lt;/code&gt;中的内容会被视为外层命名空间的一部分，从而简化了版本切换和符号管理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace Lib {
    inline namespace v1 { // default version
        void func() { std::cout &amp;lt;&amp;lt; &quot;v1&quot; &amp;lt;&amp;lt; std::endl; }
    }
    namespace v2 { // 新版本
        void func() { std::cout &amp;lt;&amp;lt; &quot;v2&quot; &amp;lt;&amp;lt; std::endl; }
    }
}

int main() {
    Lib::func(); // using v1 version func by default
    Lib::v2::func(); // using v2 version func explicit
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;C++17&lt;/h4&gt;
&lt;p&gt;在C++17中，考虑到当前inline已经是确保重复定义而不会导致链接错误，在之前的标准中只提供了函数的inline版本，而如果我们想要在头文件中声明一个变量，则必须使用如下的方式去声明和定义，二者缺一不可：&lt;/p&gt;
&lt;p&gt;在C++17之前，如果需要在头文件中声明一个全局变量，必须使用extern关键字在头文件中声明变量，并在某个源文件中提供定义。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In header
extern int x;

// In source
int x = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式需要分别在头文件和源文件中进行声明和定义，二者缺一不可，否则会导致链接错误。&lt;/p&gt;
&lt;p&gt;C++17 引入了inline变量，允许在头文件中直接定义全局变量或类静态成员变量，而不会导致链接错误。具体规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当inline关键字用于具有静态存储期(static storage duration)的变量(如全局变量或静态类成员变量)的声明说明符序列时，它将该变量声明为内联变量(inline variable)。&lt;/li&gt;
&lt;li&gt;内联变量可以在多个翻译单元中重复定义，链接器会确保只有一个实例被使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;inline int GLOBAL_VAR = 42;

class MyClass {
public:
    inline static int STATIC_VAR = 42;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就解决了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局变量的定义：通过inline关键字，可以在头文件中直接定义全局变量，而无需在源文件中提供额外定义。&lt;/li&gt;
&lt;li&gt;类静态成员变量的初始化：通过inline关键字，可以在类内直接初始化静态成员变量，而无需在类外定义。&lt;/li&gt;
&lt;li&gt;C++头文件库的支持：这一特性解决了C++头文件库开发中的主要障碍，使得头文件库的实现更加简洁和高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并且，C++17 还扩展了constexpr关键字的功能，使其可以用于静态数据成员。当静态数据成员在首次声明时被声明为constexpr，它会隐式地具有inline属性。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以在类内直接初始化constexpr静态数据成员，而无需在类外定义。&lt;/li&gt;
&lt;li&gt;该静态数据成员可以在多个翻译单元中共享，而不会导致链接错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    static constexpr int STATIC_VAR = 42;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++17中，对于inline的行为做出了更为明显的规定：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;inline 函数和变量的定义需要在访问它们的翻译单元中可见&lt;/strong&gt;。这一规则确保了编译器能够在需要时正确地内联展开函数或解析变量的定义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内联函数或变量可以有多个定义，只要每个定义出现在不同的翻译单元中，并且(对于非静态内联函数和变量)所有定义都是相同的&lt;/strong&gt;，这就是良构的。&lt;/p&gt;
&lt;p&gt;在命名空间作用域中，inline 的 const 变量默认具有外部链接(external linkage),对于非 inline 的 const 变量而言，默认具有内部链接。因此，一旦定义了&lt;code&gt;const inline&lt;/code&gt;，那么原先的非 inline 的 const 变量都会共享同一个 inline 定义，从而不会出现链接错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// file1.cc
const int globalVar = 42; // internal linkage

// file2.cc
const int globalVar = 100; // internal linkage, differ with file1

-----------------------------------------

// myheader.h
inline const int globalVar = 42; // external linkage

// file1.cpp
#include &quot;myheader.h&quot;
void func1() {
    int value = globalVar; // Using external linkage variable
}

// file2.cpp
#include &quot;myheader.h&quot;
void func2() {
    int value = globalVar; // Using the same external linkage variable
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CephV10.2.1: Universal Modules</title><link>https://chenmiaoi.github.io/2024/10/14/CephV10-2-1-Universal-Modules/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/10/14/CephV10-2-1-Universal-Modules/</guid><pubDate>Mon, 14 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;本章介绍Ceph源码中的一些复杂且通用的数据结构&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Object&lt;/h2&gt;
&lt;p&gt;在Ceph中，Object通常默认是$4MB$大小的数据块，而&lt;strong&gt;一个对象就对应了本地文件系统中的一个文件&lt;/strong&gt;。在具体的代码中，Object有很多不同的类型。&lt;/p&gt;
&lt;h3&gt;object&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/include/object.h#L32&quot;&gt;object_t&lt;/a&gt;对应本地文件系统中的一个文件，其中字段名$name$就是对象名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct object_t {
  string name;

  object_t() {}
  // cppcheck-suppress noExplicitConstructor
  object_t(const char *s) : name(s) {}
  // cppcheck-suppress noExplicitConstructor
  object_t(const string&amp;amp; s) : name(s) {}

  void swap(object_t&amp;amp; o) {
    name.swap(o.name);
  }
  void clear() {
    name.clear();
  }
  
  void encode(bufferlist &amp;amp;bl) const;
  void decode(bufferlist::iterator &amp;amp;bl);
};
WRITE_CLASS_ENCODER(object_t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，$object_t$提供了最主要的构造方法以及&lt;code&gt;encode&lt;/code&gt;和&lt;code&gt;decode&lt;/code&gt;两个主要方法，同时根据&lt;code&gt;WRITE_CLASS_ENCODER&lt;/code&gt;宏，实现了&lt;code&gt;encode&lt;/code&gt;和&lt;code&gt;decode&lt;/code&gt;的外部调用。&lt;/p&gt;
&lt;p&gt;当然，$object_t$也重载了比较运算符和输出，这样能够方便的进行比较和打印。&lt;/p&gt;
&lt;h3&gt;sobject_t&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/include/object.h#L135&quot;&gt;sobject_t&lt;/a&gt;在$object_t$之上增加了$snapshot$信息，&lt;strong&gt;用于标识其是否是快照对象&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在正式介绍$sobject_t$之前，我们应该先了解一下&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/include/object.h#L113&quot;&gt;snapid_t&lt;/a&gt;类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct snapid_t {
  uint64_t val;
  // cppcheck-suppress noExplicitConstructor
  snapid_t(uint64_t v=0) : val(v) {}
  snapid_t operator+=(snapid_t o) { val += o.val; return *this; }
  snapid_t operator++() { ++val; return *this; }
  operator uint64_t() const { return val; }  
};

inline void encode(snapid_t i, bufferlist &amp;amp;bl) { encode(i.val, bl); }
inline void decode(snapid_t &amp;amp;i, bufferlist::iterator &amp;amp;p) { decode(i.val, p); }

#define CEPH_SNAPDIR ((__u64)(-1))  /* reserved for hidden .snap dir */
#define CEPH_NOSNAP  ((__u64)(-2))  /* &quot;head&quot;, &quot;live&quot; revision */
#define CEPH_MAXSNAP ((__u64)(-3))  /* largest valid snapid */

inline ostream&amp;amp; operator&amp;lt;&amp;lt;(ostream&amp;amp; out, snapid_t s) {
  if (s == CEPH_NOSNAP)
    return out &amp;lt;&amp;lt; &quot;head&quot;;
  else if (s == CEPH_SNAPDIR)
    return out &amp;lt;&amp;lt; &quot;snapdir&quot;;
  else
    return out &amp;lt;&amp;lt; hex &amp;lt;&amp;lt; s.val &amp;lt;&amp;lt; dec;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，$snapid_t$实际上就是对一个&lt;code&gt;uint64_t&lt;/code&gt;变量的封装，我们主要关注最后重载的输出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CEPH_SNAPDIR&lt;/code&gt;标识快照目录(一个隐藏的&lt;code&gt;.snap&lt;/code&gt;目录)，用于存放快照&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CEPH_NOSNAP&lt;/code&gt;表示没有快照，即文件或对象的最新版本(被称为$head$或$live\ revision$)&lt;/li&gt;
&lt;li&gt;否则输出快照ID的十六进制数值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;了解完$snapid_t$后，我们现在回过头看$sobject_t$的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sobject_t {
  object_t oid;
  snapid_t snap;

  sobject_t() : snap(0) {}
  sobject_t(object_t o, snapid_t s) : oid(o), snap(s) {}

  void swap(sobject_t&amp;amp; o) {}

  void encode(bufferlist&amp;amp; bl) const {}
  void decode(bufferlist::iterator&amp;amp; bl) {}
};
WRITE_CLASS_ENCODER(sobject_t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$sobject_t$和$object_t$相差无几，只不过多了$snapid_t$信息，需要注意的是，$sobject_t$的&lt;code&gt;encode&lt;/code&gt;和&lt;code&gt;decode&lt;/code&gt;也需要对&lt;code&gt;snapshot&lt;/code&gt;信息进行处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果一个对象不是快照对象(也就是说该对象是$head$)，那么这个&lt;code&gt;snap&lt;/code&gt;字段就应该被置为&lt;code&gt;CEPH_NOSNAP&lt;/code&gt;值&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;hobject_t&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/hobject.h#L37&quot;&gt;hobject_t&lt;/a&gt;是 Ceph 中最复杂的对象标识符结构。它不仅包含对象名称和快照信息，还包含哈希值、哈希种子、命名空间、对象在存储池中的位置等。$hobject_t$ &lt;strong&gt;是 Ceph 用来唯一标识和管理对象存储的核心数据结构，它直接用于 Ceph 的 $CRUSH$ 算法来决定对象的物理存储位置&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct hobject_t {
  object_t oid;
  snapid_t snap;
private:
  uint32_t hash;
  bool max;
  uint32_t nibblewise_key_cache;
  uint32_t hash_reverse_bits;
  static const int64_t POOL_META = -1;
  static const int64_t POOL_TEMP_START = -2; // and then negative
  friend class spg_t;  // for POOL_TEMP_START
public:
  int64_t pool;
  string nspace;

private:
  string key;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;oid
&lt;ul&gt;
&lt;li&gt;对象的唯一标识符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;snap
&lt;ul&gt;
&lt;li&gt;快照ID&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;hash
&lt;ul&gt;
&lt;li&gt;用于定位该对象的哈希值，结合$CRUSH$算法决定对象的存储位置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hash&lt;/code&gt;和&lt;code&gt;key&lt;/code&gt;不能同时设置，&lt;code&gt;hash&lt;/code&gt;值一般设置为$PG$的ID值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;max
&lt;ul&gt;
&lt;li&gt;表示该对象是否是某种最大值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nibblewise_key_cache
&lt;ul&gt;
&lt;li&gt;缓存字段，可能用于加速对象定位或哈希计算中的某种优化操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;hash_reverse_bits
&lt;ul&gt;
&lt;li&gt;存储对象哈希值的反转版本。这可能用于某些哈希算法或排序算法中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;pool
&lt;ul&gt;
&lt;li&gt;表示对象所属的存储池 ID。Ceph 支持多种存储池，每个池可以有不同的冗余和分布策略。该字段用来记录对象属于哪个池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nspace
&lt;ul&gt;
&lt;li&gt;对象的命名空间(namespace)。命名空间允许在同一个存储池内进一步细分对象，提供更细粒度的管理和隔离&lt;/li&gt;
&lt;li&gt;一般为空，用于标识特殊对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key
&lt;ul&gt;
&lt;li&gt;对象的特殊标记&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在$hobject_t$中，比较主要的函数为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set_hash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match_hash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build_hash_cache&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_reverse_bits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_reverse_nibbles&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmp_bitwise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmp_nibblewise&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parse&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在$hobject_t$中，&lt;code&gt;set_hash&lt;/code&gt;会根据传入的&lt;code&gt;hash&lt;/code&gt;值调用&lt;code&gt;build_hash_cache&lt;/code&gt;函数，该函数会通过调用&lt;code&gt;_reverse_nibbles&lt;/code&gt;和&lt;code&gt;_reverse_bits&lt;/code&gt;分别计算出&lt;code&gt;nibblewise_key_cache&lt;/code&gt;和&lt;code&gt;hash_reverse_bits&lt;/code&gt;的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static uint32_t _reverse_bits(uint32_t v) {
  if (v == 0)
    return v;
  // reverse bits
  // swap odd and even bits
  v = ((v &amp;gt;&amp;gt; 1) &amp;amp; 0x55555555) | ((v &amp;amp; 0x55555555) &amp;lt;&amp;lt; 1);
  // swap consecutive pairs
  v = ((v &amp;gt;&amp;gt; 2) &amp;amp; 0x33333333) | ((v &amp;amp; 0x33333333) &amp;lt;&amp;lt; 2);
  // swap nibbles ...
  v = ((v &amp;gt;&amp;gt; 4) &amp;amp; 0x0F0F0F0F) | ((v &amp;amp; 0x0F0F0F0F) &amp;lt;&amp;lt; 4);
  // swap bytes
  v = ((v &amp;gt;&amp;gt; 8) &amp;amp; 0x00FF00FF) | ((v &amp;amp; 0x00FF00FF) &amp;lt;&amp;lt; 8);
  // swap 2-byte long pairs
  v = ( v &amp;gt;&amp;gt; 16             ) | ( v               &amp;lt;&amp;lt; 16);
  return v;
}
static uint32_t _reverse_nibbles(uint32_t retval) {
  // reverse nibbles
  retval = ((retval &amp;amp; 0x0f0f0f0f) &amp;lt;&amp;lt; 4) | ((retval &amp;amp; 0xf0f0f0f0) &amp;gt;&amp;gt; 4);
  retval = ((retval &amp;amp; 0x00ff00ff) &amp;lt;&amp;lt; 8) | ((retval &amp;amp; 0xff00ff00) &amp;gt;&amp;gt; 8);
  retval = ((retval &amp;amp; 0x0000ffff) &amp;lt;&amp;lt; 16) | ((retval &amp;amp; 0xffff0000) &amp;gt;&amp;gt; 16);
  return retval;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;_reverse_bits&lt;/code&gt;而言，则是将&lt;code&gt;uint32_t&lt;/code&gt;的值的执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换奇偶位&lt;/li&gt;
&lt;li&gt;交换相邻的两位&lt;/li&gt;
&lt;li&gt;交换半字节(nibble)&lt;/li&gt;
&lt;li&gt;交换每8位&lt;/li&gt;
&lt;li&gt;交换每16位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;_reverse_nibbles&lt;/code&gt;，则是将&lt;code&gt;uint32_t&lt;/code&gt;的值执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;交换每半字节&lt;/li&gt;
&lt;li&gt;交换每8位&lt;/li&gt;
&lt;li&gt;交换每16位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;match_hash&lt;/code&gt;，主要是用于比较指定的最低&lt;code&gt;bits&lt;/code&gt;位数的值是否相等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static bool match_hash(uint32_t to_check, uint32_t bits, uint32_t match) {
  return (match &amp;amp; ~((~0)&amp;lt;&amp;lt;bits)) == (to_check &amp;amp; ~((~0)&amp;lt;&amp;lt;bits));
}
bool match(uint32_t bits, uint32_t match) const {
  return match_hash(hash, bits, match);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cmp_nibblewise&lt;/code&gt;用于比较两个$hobject_t$对象的&lt;code&gt;max&lt;/code&gt;、&lt;code&gt;pool&lt;/code&gt;、&lt;code&gt;nibblewise_key&lt;/code&gt;、&lt;code&gt;nspace&lt;/code&gt;、&lt;code&gt;key&lt;/code&gt;、&lt;code&gt;oid&lt;/code&gt;和&lt;code&gt;snap&lt;/code&gt;的大小关系；而&lt;code&gt;cmp_bitwise&lt;/code&gt;只是将&lt;code&gt;nibblewise_key&lt;/code&gt;的比较替换为&lt;code&gt;bitwise_key&lt;/code&gt;的比较。&lt;/p&gt;
&lt;p&gt;然后根据这两个函数分别实现了伪函数用作于比较器：&lt;code&gt;NibblewiseComparator&lt;/code&gt;、&lt;code&gt;BitwiseComparator&lt;/code&gt;和&lt;code&gt;Comparator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于最后的&lt;code&gt;parse&lt;/code&gt;函数用于解析字符串形如下面的格式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MIN/MAX/pool_id:hash:namespace:key:object_name:snap_id
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ghobject_t&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/hobject.h#L349&quot;&gt;ghobject_t&lt;/a&gt;是对于$hobject_t$的封装，添加了&lt;code&gt;generation&lt;/code&gt;字段和&lt;code&gt;shard_id&lt;/code&gt;字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ghobject_t {
  hobject_t hobj;
  gen_t generation;
  shard_id_t shard_id;
  bool max;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$ghobject_t$主要用于$ErasureCode$模式下的$PG$&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;generation
&lt;ul&gt;
&lt;li&gt;用于记录对象的版本号。&lt;strong&gt;当$PG$为$EC$时，写操作需要区分写前后两个版本的$object$，写操作保存对象的上一个版本的对象，当写失败时，可以$rollback$到上一个版本&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;shard_id
&lt;ul&gt;
&lt;li&gt;用于标识对象所在的$OSD$在$EC$类型的$PG$中的序号，对于$EC$来说，每个OSD在$PG$中的序号在数据恢复时非常关键。&lt;/li&gt;
&lt;li&gt;如果是$Replicated$类型的$PG$，那么字段就设置为&lt;code&gt;NO_SHARD&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在$ghobject_t$中，只有两个函数需要着重注意：&lt;code&gt;make_pgmeta&lt;/code&gt;和&lt;code&gt;parse&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ghobject_t make_pgmeta(int64_t pool, uint32_t hash, shard_id_t shard) {
  hobject_t h(object_t(), string(), CEPH_NOSNAP, hash, pool, string());
  return ghobject_t(h, NO_GEN, shard);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而该&lt;code&gt;parse&lt;/code&gt;用于解析这样的字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GHMIN/GHMAX/shard_id#object_id:hash:namespace:key:object_name:snap_id#generation_id
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Buffer&lt;/h2&gt;
&lt;p&gt;在实际的Ceph源码中，Buffer就是一个命名空间，在其内部有很多相关的数据结构；最为基础的则是&lt;code&gt;buffer::raw&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;buffer::raw&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L162&quot;&gt;buffer::raw&lt;/a&gt;用于表示和管理一个存储数据的内存块。它包含了数据的管理、复制、校验（CRC）等功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class buffer::raw {
public:
  char *data;
  unsigned len;
  atomic_t nref;

  mutable simple_spinlock_t crc_spinlock;
  map&amp;lt;pair&amp;lt;size_t, size_t&amp;gt;, pair&amp;lt;uint32_t, uint32_t&amp;gt; &amp;gt; crc_map;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;data
&lt;ul&gt;
&lt;li&gt;用于维护数据缓冲区&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;len
&lt;ul&gt;
&lt;li&gt;标识当前数据的长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nref
&lt;ul&gt;
&lt;li&gt;引用计数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;crc_spinlock
&lt;ul&gt;
&lt;li&gt;简单的一个$spinlock$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;crc_map
&lt;ul&gt;
&lt;li&gt;crc校验信息，第一个&lt;code&gt;pair&lt;/code&gt;为数据段的起始和结束，第二个&lt;code&gt;pair&lt;/code&gt;是crc32校验码：第一个字段为$base\ crc32$校验码，第二个字段为加上数据段后计算出的crc32校验码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是：$buffer::raw$&lt;strong&gt;不允许拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$buffer::raw$提供了以下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;virtual char *get_data() {
  return data;
}
virtual raw* clone_empty() = 0;
raw *clone() {
  raw *c = clone_empty();
  memcpy(c-&amp;gt;data, data, len);
  return c;
}

virtual bool can_zero_copy() const {}
virtual int zero_copy_to_fd(int fd, loff_t *offset) {}
virtual bool is_page_aligned() {}
bool is_n_page_sized() {}
virtual bool is_shareable() {} 

bool get_crc(const pair&amp;lt;size_t, size_t&amp;gt; &amp;amp;fromto,
     pair&amp;lt;uint32_t, uint32_t&amp;gt; *crc) const {
  simple_spin_lock(&amp;amp;crc_spinlock);
  map&amp;lt;pair&amp;lt;size_t, size_t&amp;gt;, pair&amp;lt;uint32_t, uint32_t&amp;gt; &amp;gt;::const_iterator i =
  crc_map.find(fromto);
  if (i == crc_map.end()) {
      simple_spin_unlock(&amp;amp;crc_spinlock);
      return false;
  }
  *crc = i-&amp;gt;second;
  simple_spin_unlock(&amp;amp;crc_spinlock);
  return true;
}
void set_crc(const pair&amp;lt;size_t, size_t&amp;gt; &amp;amp;fromto,
     const pair&amp;lt;uint32_t, uint32_t&amp;gt; &amp;amp;crc) {
  simple_spin_lock(&amp;amp;crc_spinlock);
  crc_map[fromto] = crc;
  simple_spin_unlock(&amp;amp;crc_spinlock);
}
void invalidate_crc() {
  simple_spin_lock(&amp;amp;crc_spinlock);
  if (crc_map.size() != 0) {
    crc_map.clear();
  }
  simple_spin_unlock(&amp;amp;crc_spinlock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;raw_malloc&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L291&quot;&gt;raw_malloc&lt;/a&gt;是对$buffer::raw$申请缓存的封装，实际上由$buffer::raw$和三个全局变量控制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static atomic_t buffer_total_alloc;
static atomic64_t buffer_history_alloc_bytes;
static atomic64_t buffer_history_alloc_num;
const bool buffer_track_alloc = get_env_bool(&quot;CEPH_BUFFER_TRACK&quot;);

class buffer::raw_malloc : public buffer::raw {};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;buffer::raw_combined&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L246&quot;&gt;buffer::raw_combined&lt;/a&gt;&lt;strong&gt;用于在单个内存分配中同时存放数据缓冲区；其设计目的是将数据和元数据一同进行内存管理，优化内存分配和访问性能&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class buffer::raw_combined : public buffer::raw {
  size_t alignment;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在某些平台上，为了提高性能或满足硬件需求，数据需要按照特定字节进行对齐。&lt;/p&gt;
&lt;p&gt;在这个结构中，最主要的函数为&lt;code&gt;create&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static raw_combined *create(unsigned len, unsigned align=0) {
  if (!align)
    align = sizeof(size_t);
  size_t rawlen = ROUND_UP_TO(sizeof(buffer::raw_combined),
              alignof(buffer::raw_combined));
  size_t datalen = ROUND_UP_TO(len, alignof(buffer::raw_combined));

#ifdef DARWIN
  char *ptr = (char *) valloc(rawlen + datalen);
#else
  char *ptr = 0;
  int r = ::posix_memalign((void**)(void*)&amp;amp;ptr, align, rawlen + datalen);
  if (r)
    throw bad_alloc();
#endif /* DARWIN */
  if (!ptr)
    throw bad_alloc();

  // actual data first, since it has presumably larger alignment restriction
  // then put the raw_combined at the end
  return new (ptr + datalen) raw_combined(ptr, len, align);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有指定对其字节，那么我们使用&lt;code&gt;sizeof(size_t)&lt;/code&gt;作为默认对齐大小。然后通过&lt;code&gt;ROUND_UP_TO&lt;/code&gt;计算出需要申请空间对齐后的大小和该数据结构元数据的大小，然后申请出对应的内存空间($rawlen+datalen$)，然后通过定位$new$，将数据结构元数据放置在缓冲区的最后。&lt;/p&gt;
&lt;h4&gt;buffer::raw_mmap_pages&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L320&quot;&gt;buffer::raw_mmap_pages&lt;/a&gt;通过实现&lt;code&gt;mmap&lt;/code&gt;来把内存匿名映射到进程的地址空间，用于专门处理内存映射的页面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data = (char*)::mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其主要核心代码就上面这一行，调用&lt;code&gt;mmap&lt;/code&gt;接口，设置其权限为可读可写、私有且匿名的映射。&lt;/p&gt;
&lt;h4&gt;buffer::raw_posix_aligned&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L340&quot;&gt;buffer::raw_posix_aligned&lt;/a&gt;通过调用&lt;code&gt;posix_memalign&lt;/code&gt;来申请内存地址对齐的内存空间。&lt;/p&gt;
&lt;p&gt;其核心代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;align = _align;
assert((align &amp;gt;= sizeof(void *)) &amp;amp;&amp;amp; (align &amp;amp; (align - 1)) == 0);
::posix_memalign((void**)(void*)&amp;amp;data, align, len);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;buffer::raw_pipe&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/buffer.cc#L402&quot;&gt;buffer::raw_pipe&lt;/a&gt;使用管道机制来实现内存缓冲区。&lt;/p&gt;
&lt;p&gt;这里需要注意的便是如何初始化管道以及复制管道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;explicit raw_pipe(unsigned len) : raw(len), source_consumed(false) {
  size_t max = get_max_pipe_size();
  if (len &amp;gt; max) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: requested length &quot; &amp;lt;&amp;lt; len
          &amp;lt;&amp;lt; &quot; &amp;gt; max length &quot; &amp;lt;&amp;lt; max &amp;lt;&amp;lt; bendl;
    throw malformed_input(&quot;length larger than max pipe size&quot;);
  }
  pipefds[0] = -1;
  pipefds[1] = -1;

  int r;
  if (::pipe(pipefds) == -1) {
    r = -errno;
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error creating pipe: &quot; &amp;lt;&amp;lt; cpp_strerror(r) &amp;lt;&amp;lt; bendl;
    throw error_code(r);
  }

  r = set_nonblocking(pipefds);
  if (r &amp;lt; 0) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error setting nonblocking flag on temp pipe: &quot;
          &amp;lt;&amp;lt; cpp_strerror(r) &amp;lt;&amp;lt; bendl;
    throw error_code(r);
  }

  r = set_pipe_size(pipefds, len);
  if (r &amp;lt; 0) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: could not set pipe size&quot; &amp;lt;&amp;lt; bendl;
    // continue, since the pipe should become large enough as needed
  }

  inc_total_alloc(len);
  inc_history_alloc(len);
  bdout &amp;lt;&amp;lt; &quot;raw_pipe &quot; &amp;lt;&amp;lt; this &amp;lt;&amp;lt; &quot; alloc &quot; &amp;lt;&amp;lt; len &amp;lt;&amp;lt; &quot; &quot;
    &amp;lt;&amp;lt; buffer::get_total_alloc() &amp;lt;&amp;lt; bendl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在最开始，我们需要获取管道的最大容量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static atomic_t buffer_max_pipe_size;
int update_max_pipe_size() {
#ifdef CEPH_HAVE_SETPIPE_SZ
  char buf[32];
  int r;
  std::string err;
  struct stat stat_result;
  if (::stat(&quot;/proc/sys/fs/pipe-max-size&quot;, &amp;amp;stat_result) == -1)
    return -errno;
  r = safe_read_file(&quot;/proc/sys/fs/&quot;, &quot;pipe-max-size&quot;,
             buf, sizeof(buf) - 1);
  if (r &amp;lt; 0)
    return r;
  buf[r] = &apos;\0&apos;;
  size_t size = strict_strtol(buf, 10, &amp;amp;err);
  if (!err.empty())
    return -EIO;
  buffer_max_pipe_size.set(size);
#endif
  return 0;
}

size_t get_max_pipe_size() {
#ifdef CEPH_HAVE_SETPIPE_SZ
  size_t size = buffer_max_pipe_size.read();
  if (size)
    return size;
  if (update_max_pipe_size() == 0)
    return buffer_max_pipe_size.read();
#endif
  // this is the max size hardcoded in linux before 2.6.35
  return 65536;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Linux中，管道的最大容量是写在&lt;code&gt;/proc/sys/fs/pipe-max-size&lt;/code&gt;中的，因此我们只需要读取这个文件并保存其数值到&lt;code&gt;buffer_max_pipe_size&lt;/code&gt;中，然后通过&lt;code&gt;get_max_pipe_size&lt;/code&gt;返回即可。&lt;/p&gt;
&lt;p&gt;然后我们就需要创建管道，并设置管道属性为非阻塞模式，同时尝试设置管道大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int set_pipe_size(int *fds, long length) {
#ifdef CEPH_HAVE_SETPIPE_SZ
  if (::fcntl(fds[1], F_SETPIPE_SZ, length) == -1) {
    int r = -errno;
    if (r == -EPERM) {
      // pipe limit must have changed - EPERM means we requested
      // more than the maximum size as an unprivileged user
      update_max_pipe_size();
      throw malformed_input(&quot;length larger than new max pipe size&quot;);
    }
    return r;
  }
#endif
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于复制管道，我们需要做出以下操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *copy_pipe(int *fds) {
  /* preserve original pipe contents by copying into a temporary
   * pipe before reading.
   */
  int tmpfd[2];
  int r;

  assert(!source_consumed);
  assert(fds[0] &amp;gt;= 0);

  if (::pipe(tmpfd) == -1) {
    r = -errno;
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error creating temp pipe: &quot; &amp;lt;&amp;lt; cpp_strerror(r)
          &amp;lt;&amp;lt; bendl;
    throw error_code(r);
  }
  r = set_nonblocking(tmpfd);
  if (r &amp;lt; 0) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error setting nonblocking flag on temp pipe: &quot;
          &amp;lt;&amp;lt; cpp_strerror(r) &amp;lt;&amp;lt; bendl;
    throw error_code(r);
  }
  r = set_pipe_size(tmpfd, len);
  if (r &amp;lt; 0) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error setting pipe size on temp pipe: &quot;
          &amp;lt;&amp;lt; cpp_strerror(r) &amp;lt;&amp;lt; bendl;
  }
  int flags = SPLICE_F_NONBLOCK;
  if (::tee(fds[0], tmpfd[1], len, flags) == -1) {
    r = errno;
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error tee&apos;ing into temp pipe: &quot; &amp;lt;&amp;lt; cpp_strerror(r)
          &amp;lt;&amp;lt; bendl;
    close_pipe(tmpfd);
    throw error_code(r);
  }
  data = (char *)malloc(len);
  if (!data) {
    close_pipe(tmpfd);
    throw bad_alloc();
  }
  r = safe_read(tmpfd[0], data, len);
  if (r &amp;lt; (ssize_t)len) {
    bdout &amp;lt;&amp;lt; &quot;raw_pipe: error reading from temp pipe:&quot; &amp;lt;&amp;lt; cpp_strerror(r)
          &amp;lt;&amp;lt; bendl;
    free(data);
    data = NULL;
    close_pipe(tmpfd);
    throw error_code(r);
  }
  close_pipe(tmpfd);
  return data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要创建一个临时管道，然后将原管道的数据复制到该临时管道中，以便读取数据。同样，我们需要将临时管道设置为非阻塞模式，然后设置管道的大小。&lt;/p&gt;
&lt;p&gt;通过&lt;code&gt;tee&lt;/code&gt;系统调用，将原管道&lt;code&gt;fds[0]&lt;/code&gt;中的内容复制到临时管道的写端&lt;code&gt;tmpfd[1]&lt;/code&gt;中，然后再从&lt;code&gt;tmpfd[0]&lt;/code&gt;中读取数据。&lt;/p&gt;
&lt;h3&gt;buffer::ptr&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/include/buffer.h#L164&quot;&gt;buffer::ptr&lt;/a&gt;是对$buffer::raw$的一种slice，其具体示意为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410141431478.png&quot; alt=&quot;raw和ptr&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CEPH_BUFFER_API ptr {
  raw *_raw;
  unsigned _off, _len;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;_off
&lt;ul&gt;
&lt;li&gt;该字段是对于$buffer::raw$数据指针的偏移距离&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_len
&lt;ul&gt;
&lt;li&gt;标识其长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;buffer::list&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/include/buffer.h#L261&quot;&gt;buffer::list&lt;/a&gt;是一个使用广泛的类，是多个$buffer::ptr$的列表，也是多个内存数据段的列表。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CEPH_BUFFER_API list {
  // my private bits
  std::list&amp;lt;ptr&amp;gt; _buffers;
  unsigned _len;
  unsigned _memcopy_count;    //the total of memcopy using rebuild().
  ptr append_buffer;          // where i put small appends.
  
  mutable iterator last_p;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;_buffers
&lt;ul&gt;
&lt;li&gt;用于保存所有的ptr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_len
&lt;ul&gt;
&lt;li&gt;是所有ptr的数据总长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_memcopy_count
&lt;ul&gt;
&lt;li&gt;调用函数&lt;code&gt;rebuild&lt;/code&gt;用来内存对齐时，需要内存拷贝的数据量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;append_buffer
&lt;ul&gt;
&lt;li&gt;当有小的数据就添加到这个buffer中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;last_p
&lt;ul&gt;
&lt;li&gt;访问list的迭代器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该结构体最为主要的便是如何操作list了，因此我们主要探究&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push_front/back&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rebuld&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;append&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write/read&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;push_front/back&lt;/h4&gt;
&lt;p&gt;对于&lt;code&gt;push_front&lt;/code&gt;而言，我们只需要调用&lt;code&gt;vector::push_front&lt;/code&gt;进行插入即可，如果是$buffer::raw$类型，则通过$buffer:ptr$封装后进行处理。&lt;/p&gt;
&lt;p&gt;自然，&lt;code&gt;push_back&lt;/code&gt;也是同理。&lt;/p&gt;
&lt;h4&gt;rebuild&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rebuild&lt;/code&gt;函数主要用于将内部的数据通过对齐的方式重组合并，主要依赖于&lt;a href=&quot;#bufferraw_posix_aligned&quot;&gt;buffer::raw_posix_aligned&lt;/a&gt;，然后通过$buffer::ptr::copy_in$将每一个数据拷贝进去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (std::list&amp;lt;ptr&amp;gt;::iterator it = _buffers.begin(); it != _buffers.end(); ++it) {
  nb.copy_in(pos, it-&amp;gt;length(), it-&amp;gt;c_str(), false);
  pos += it-&amp;gt;length();
}
_memcopy_count += pos;
_buffers.clear();
if (nb.length())
  _buffers.push_back(nb);
invalidate_crc();
last_p = begin();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rebuild&lt;/code&gt;还有另一个版本&lt;code&gt;rebuild_aligned_size_and_memory&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void buffer::list::rebuild_aligned_size_and_memory(unsigned align_size,
  						   unsigned align_memory) {
  std::list&amp;lt;ptr&amp;gt;::iterator p = _buffers.begin();
  while (p != _buffers.end()) {
    // keep anything that&apos;s already align and sized aligned
    if (p-&amp;gt;is_aligned(align_memory) &amp;amp;&amp;amp; p-&amp;gt;is_n_align_sized(align_size)) {
      /*cout &amp;lt;&amp;lt; &quot; segment &quot; &amp;lt;&amp;lt; (void*)p-&amp;gt;c_str()
       &amp;lt;&amp;lt; &quot; offset &quot; &amp;lt;&amp;lt; ((unsigned long)p-&amp;gt;c_str() &amp;amp; (align - 1))
       &amp;lt;&amp;lt; &quot; length &quot; &amp;lt;&amp;lt; p-&amp;gt;length()
       &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; (p-&amp;gt;length() &amp;amp; (align - 1)) &amp;lt;&amp;lt; &quot; ok&quot; &amp;lt;&amp;lt; std::endl;
      */
      ++p;
      continue;
    }
    
    // consolidate unaligned items, until we get something that is sized+aligned
    list unaligned;
    unsigned offset = 0;
    do {
      /*cout &amp;lt;&amp;lt; &quot; segment &quot; &amp;lt;&amp;lt; (void*)p-&amp;gt;c_str()
             &amp;lt;&amp;lt; &quot; offset &quot; &amp;lt;&amp;lt; ((unsigned long)p-&amp;gt;c_str() &amp;amp; (align - 1))
             &amp;lt;&amp;lt; &quot; length &quot; &amp;lt;&amp;lt; p-&amp;gt;length() &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; (p-&amp;gt;length() &amp;amp; (align - 1))
             &amp;lt;&amp;lt; &quot; overall offset &quot; &amp;lt;&amp;lt; offset &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; (offset &amp;amp; (align - 1))
       &amp;lt;&amp;lt; &quot; not ok&quot; &amp;lt;&amp;lt; std::endl;
      */
      offset += p-&amp;gt;length();
      unaligned.push_back(*p);
      _buffers.erase(p++);
    } while (p != _buffers.end() &amp;amp;&amp;amp;
       (!p-&amp;gt;is_aligned(align_memory) ||
        !p-&amp;gt;is_n_align_sized(align_size) ||
        (offset % align_size)));
    if (!(unaligned.is_contiguous() &amp;amp;&amp;amp; unaligned._buffers.front().is_aligned(align_memory))) {
      ptr nb(buffer::create_aligned(unaligned._len, align_memory));
      unaligned.rebuild(nb);
      _memcopy_count += unaligned._len;
    }
    _buffers.insert(p, unaligned._buffers.front());
  }
  last_p = begin();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首先我们需要检查数据块是否已经对齐
&lt;ul&gt;
&lt;li&gt;内存上是否对齐&lt;code&gt;is_aligned&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;大小上是否对齐&lt;code&gt;is_n_align_sized&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果其中一个数据块未对齐，则需要合并未对齐的数据块，使用&lt;code&gt;unaligned&lt;/code&gt;列表存储&lt;/li&gt;
&lt;li&gt;检查合并后的数据块是否满足对齐要求，如果不满足，则需要创建新的对齐缓冲区&lt;/li&gt;
&lt;li&gt;将对齐后的数据块插回原列表&lt;/li&gt;
&lt;li&gt;更新迭代器指针位置&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;append&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;append&lt;/code&gt;归根结底是在内部调用了&lt;code&gt;push_back&lt;/code&gt;接口，&lt;code&gt;append&lt;/code&gt;有多种重载，允许多种类型的参数传入，此处不再做过多介绍。最为重要的是，我们需要检查&lt;code&gt;append_buffer&lt;/code&gt;是否有足够的空间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned gap = append_buffer.unused_tail_length();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;write/read&lt;/h4&gt;
&lt;p&gt;在$buffer::list$中，&lt;code&gt;write&lt;/code&gt;主要有三种方式写：写入流、写入文件、写入fd；而&lt;code&gt;read&lt;/code&gt;有两种方式：读取文件、读取fd。&lt;/p&gt;
&lt;p&gt;其余方法没有特别需要说明的，就是正常读写操作，但在&lt;code&gt;write_fd&lt;/code&gt;中，使用了$iovec$。&lt;/p&gt;
&lt;p&gt;$iovec$ &lt;strong&gt;是用于描述输入/输出操作中数据缓冲区的结构体，常用于执行分散/聚集 I/O(scatter/gather I/O）操作&lt;/strong&gt;。在 I/O 操作中，&lt;strong&gt;它允许应用程序使用多个缓冲区作为输入或输出，避免了将数据拷贝到单一的连续缓冲区&lt;/strong&gt;。$iovec$ 结构通常用于 Linux 系统调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct iovec {
  void  *iov_base;  // 指向数据缓冲区的指针
  size_t iov_len;   // 缓冲区的长度
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;readv&lt;/code&gt;和&lt;code&gt;writev&lt;/code&gt;实现分散度和聚集写操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iovec iov[IOV_MAX];
int iovlen = 0;
ssize_t bytes = 0;

std::list&amp;lt;ptr&amp;gt;::const_iterator p = _buffers.begin();
while (p != _buffers.end()) {
  if (p-&amp;gt;length() &amp;gt; 0) {
    iov[iovlen].iov_base = (void *)p-&amp;gt;c_str();
    iov[iovlen].iov_len = p-&amp;gt;length();
    bytes += p-&amp;gt;length();
    iovlen++;
  }
  ++p;

  if (iovlen == IOV_MAX-1 || p == _buffers.end()) {
    iovec *start = iov;
    int num = iovlen;
    ssize_t wrote;
  retry:
    wrote = ::writev(fd, start, num);
    if (wrote &amp;lt; 0) {
      int err = errno;
      if (err == EINTR)
        goto retry;
      return -err;
    }
    if (wrote &amp;lt; bytes) {
  // partial write, recover!
      while ((size_t)wrote &amp;gt;= start[0].iov_len) {
        wrote -= start[0].iov_len;
        bytes -= start[0].iov_len;
        start++;
        num--;
      }
      if (wrote &amp;gt; 0) {
        start[0].iov_len -= wrote;
        start[0].iov_base = (char *)start[0].iov_base + wrote;
        bytes -= wrote;
      }
      goto retry;
    }
    iovlen = 0;
    bytes = 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ThreadPool&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L28&quot;&gt;ThreadPool&lt;/a&gt;在Ceph中随处可见且极为重要，在介绍$ThreadPool$之前，我们需要了解一些预备知识。&lt;/p&gt;
&lt;h3&gt;Mutex&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/Mutex.h#L34&quot;&gt;Mutex&lt;/a&gt;是Ceph自行实现的互斥锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Mutex {
private:
  std::string name;     // 锁的名字
  int id;               // 锁的唯一标识符
  bool recursive;       // 递归锁，允许同一线程多次获取同一锁
  bool lockdep;         // 依赖检测，防止死锁
  bool backtrace;       // gather backtrace on lock acquisition

  pthread_mutex_t _m;   // POSIX MUTEX API
  int nlock;            // 记录锁的嵌套次数，如果是递归锁，则会在加锁时自增
  pthread_t locked_by;  // 记录当前加锁的线程TID
  CephContext *cct;     // Ceph上下文
  PerfCounters *logger; // 性能日志记录器
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：$Mutex$&lt;strong&gt;不允许拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;初始化锁过程&lt;/h4&gt;
&lt;p&gt;$Mutex$默认不允许递归锁，且开启依赖检测，不允许收集调用栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mutex::Mutex(const std::string &amp;amp;n, bool r, bool ld,
	     bool bt,
	     CephContext *cct) :
  name(n), id(-1), recursive(r), lockdep(ld), backtrace(bt), nlock(0),
  locked_by(0), cct(cct), logger(0) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于Ceph是由$Valgrind$所监控的，因此我们首先需要告知$Valgrind$，我们的一些变量在多线程中的数据竞争是预期之中且安全的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANNOTATE_BENIGN_RACE_SIZED(&amp;amp;id, sizeof(id), &quot;Mutex lockdep id&quot;);
ANNOTATE_BENIGN_RACE_SIZED(&amp;amp;nlock, sizeof(nlock), &quot;Mutex nlock&quot;);
ANNOTATE_BENIGN_RACE_SIZED(&amp;amp;locked_by, sizeof(locked_by), &quot;Mutex locked_by&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们暂时不对$CephContext$进行分析，看向下一个，如果我们开启了&lt;code&gt;recursive&lt;/code&gt;功能，那么我们需要初始化一个递归类型的&lt;code&gt;pthread_mutex_t&lt;/code&gt;，因此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (recursive) {
  pthread_mutexattr_t attr;
  pthread_mutexattr_init(&amp;amp;attr);
  pthread_mutexattr_settype(&amp;amp;attr, PTHREAD_MUTEX_RECURSIVE);
  pthread_mutex_init(&amp;amp;_m,&amp;amp;attr);
  pthread_mutexattr_destroy(&amp;amp;attr);
  if (lockdep &amp;amp;&amp;amp; g_lockdep)
    _register();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就允许了同一线程能够被多次锁定，但必须以相同次数解锁。如果锁不是递归锁，但启用了&lt;code&gt;lockdep&lt;/code&gt;，则初始化一个带有错误检查的&lt;code&gt;pthread_mutex_t&lt;/code&gt;，这种类型的锁会在尝试二次锁定或解锁未锁定的情况下返回错误，从而不会导致死锁或未定义行为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else if (lockdep) {
  pthread_mutexattr_t attr;
  pthread_mutexattr_init(&amp;amp;attr);
  pthread_mutexattr_settype(&amp;amp;attr, PTHREAD_MUTEX_ERRORCHECK);
  pthread_mutex_init(&amp;amp;_m, &amp;amp;attr);
  pthread_mutexattr_destroy(&amp;amp;attr);
  if (g_lockdep)
    _register();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么现在我们来探究&lt;code&gt;lockdep&lt;/code&gt;需要通过&lt;code&gt;_register&lt;/code&gt;注册锁依赖信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void _register() {
  id = lockdep_register(name.c_str());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，该&lt;code&gt;lockdep_register&lt;/code&gt;接受锁的名字，最终会返回一个线程的唯一标识符。在执行具体操作之前，我们需要放置并发访问导致共享数据的竞态，使用一个额外的锁来处理这部分逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# define PTHREAD_MUTEX_INITIALIZER \
  { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
static pthread_mutex_t lockdep_mutex = PTHREAD_MUTEX_INITIALIZER;

int lockdep_register(const char *name) {
  int id;
  pthread_mutex_lock(&amp;amp;lockdep_mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;PTHREAD_MUTEX_INITIALIZER&lt;/code&gt;来初始化一个锁，比调用接口初始化更为高效。&lt;/p&gt;
&lt;p&gt;当加上锁后，我们需要检查传入的锁名是否已经在哈希表中存在，该哈希表是维护在全局中的变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ceph::unordered_map&amp;lt;std::string, int&amp;gt; lock_ids;
ceph::unordered_map&amp;lt;std::string, int&amp;gt;::iterator p = lock_ids.find(name);
id = p-&amp;gt;second;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在哈希表中存在，那么就找到了该锁的唯一标识符；如果没有找到，那么会通过&lt;code&gt;free_ids&lt;/code&gt;分配一个唯一标识符，&lt;code&gt;free_ids&lt;/code&gt;是一个从$0 \sim MAX_LOCKS$的列表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (p == lock_ids.end()) {
  if (free_ids.empty()) {
    lockdep_dout(0) &amp;lt;&amp;lt; &quot;ERROR OUT OF IDS .. have &quot; &amp;lt;&amp;lt; free_ids.size()
            &amp;lt;&amp;lt; &quot; max &quot; &amp;lt;&amp;lt; MAX_LOCKS &amp;lt;&amp;lt; dendl;
    for (auto&amp;amp; p : lock_names) {
      lockdep_dout(0) &amp;lt;&amp;lt; &quot;  lock &quot; &amp;lt;&amp;lt; p.first &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; p.second &amp;lt;&amp;lt; dendl;
    }
    assert(free_ids.empty());
  }
  id = free_ids.front();
  free_ids.pop_front();

  lock_ids[name] = id;
  lock_names[id] = name;
  lockdep_dout(10) &amp;lt;&amp;lt; &quot;registered &apos;&quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot;&apos; as &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; dendl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后通过&lt;code&gt;lock_refs&lt;/code&gt;维护一个引用计数，来表明一个锁注册了多少次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static map&amp;lt;int, int&amp;gt; lock_refs;

++lock_refs[id];
pthread_mutex_unlock(&amp;amp;lockdep_mutex);
return id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;销毁锁过程&lt;/h4&gt;
&lt;p&gt;显然的，我们在一开始需要对$Valgrind$进行声明，标识对&lt;code&gt;_m&lt;/code&gt;的操作不会导致锁的竞态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mutex::~Mutex() {
  assert(nlock == 0);

  // helgrind gets confused by condition wakeups leading to mutex destruction
  ANNOTATE_BENIGN_RACE_SIZED(&amp;amp;_m, sizeof(_m), &quot;Mutex primitive&quot;);
  pthread_mutex_destroy(&amp;amp;_m);

  if (cct &amp;amp;&amp;amp; logger) {
    cct-&amp;gt;get_perfcounters_collection()-&amp;gt;remove(logger);
    delete logger;
  }
  if (lockdep &amp;amp;&amp;amp; g_lockdep) {
    lockdep_unregister(id);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们在一开始启用了&lt;code&gt;lockdep&lt;/code&gt;，那么在最后销毁时，需要将注册的依赖全部注销掉&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void lockdep_unregister(int id)
{
  if (id &amp;lt; 0) {
    return;
  }

  pthread_mutex_lock(&amp;amp;lockdep_mutex);

  map&amp;lt;int, std::string&amp;gt;::iterator p = lock_names.find(id);
  assert(p != lock_names.end());

  int &amp;amp;refs = lock_refs[id];
  if (--refs == 0) {
    lockdep_dout(10) &amp;lt;&amp;lt; &quot;unregistered &apos;&quot; &amp;lt;&amp;lt; p-&amp;gt;second &amp;lt;&amp;lt; &quot;&apos; from &quot; &amp;lt;&amp;lt; id
                     &amp;lt;&amp;lt; dendl;
    lock_ids.erase(p-&amp;gt;second);
    lock_names.erase(id);
    lock_refs.erase(id);
    free_ids.push_back(id);
  } else {
    lockdep_dout(20) &amp;lt;&amp;lt; &quot;have &quot; &amp;lt;&amp;lt; refs &amp;lt;&amp;lt; &quot; of &apos;&quot; &amp;lt;&amp;lt; p-&amp;gt;second &amp;lt;&amp;lt; &quot;&apos; &quot;
                     &amp;lt;&amp;lt; &quot;from &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; dendl;
  }
  pthread_mutex_unlock(&amp;amp;lockdep_mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中间有一部分是对$BackTrace$的清空，我们此处并不深究这一块，因此没有体现这一部分。&lt;/p&gt;
&lt;p&gt;我们最主要做的事情便是，如果该锁的引用计数为0后，在&lt;code&gt;lock_ids&lt;/code&gt;哈希表中删除该键值对，然后删除&lt;code&gt;lock_names&lt;/code&gt;和&lt;code&gt;lock_refs&lt;/code&gt;中的记录，同时将空闲的锁唯一标识符追加到&lt;code&gt;free_ids&lt;/code&gt;中。&lt;/p&gt;
&lt;h4&gt;上锁过程&lt;/h4&gt;
&lt;p&gt;在这里，我们依旧不讨论$CephContext$的情况。&lt;code&gt;Lock&lt;/code&gt;函数接受一个&lt;code&gt;no_lockdep&lt;/code&gt;参数，如果在开启&lt;code&gt;lockdep&lt;/code&gt;的情况下，$no_lockdep=false$就会触发&lt;code&gt;_will_lock&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Mutex::Lock(bool no_lockdep) {
  if (lockdep &amp;amp;&amp;amp; g_lockdep &amp;amp;&amp;amp; !no_lockdep) _will_lock();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_will_lock&lt;/code&gt; 函数的意义是配合锁依赖追踪机制，提前告知系统某个锁即将被获取，从而帮助 Ceph 进行锁依赖关系的管理和检测。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void _will_lock() { // about to lock
  id = lockdep_will_lock(name.c_str(), id, backtrace);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数会将锁的名字和锁的ID传递给 &lt;code&gt;lockdep_will_lock&lt;/code&gt; 函数。&lt;code&gt;lockdep_will_lock&lt;/code&gt; 函数会在内部登记或更新该锁的依赖信息，表示该锁即将被某个线程尝试获取。如果启用了回溯$backtrace = true$，Ceph 还会记录锁被获取时的调用栈信息，方便后续的调试和分析。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lockdep_will_lock&lt;/code&gt;会在最开始获取线程TID，以及检查传入的锁ID是否有效，如果无效，则通过&lt;code&gt;lockdep_register&lt;/code&gt;注册一个有效的锁ID&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int lockdep_will_lock(const char *name, int id, bool force_backtrace) {
  pthread_t p = pthread_self();
  if (id &amp;lt; 0) id = lockdep_register(name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认ID有效后，我们从维护的&lt;code&gt;held&lt;/code&gt;表中，获取对应的依赖图，然后检查依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map&amp;lt;int, BackTrace *&amp;gt; &amp;amp;m = held[p];
for (map&amp;lt;int, BackTrace *&amp;gt;::iterator p = m.begin(); p != m.end(); ++p) {
  if (p-&amp;gt;first == id) {
    lockdep_dout(0) &amp;lt;&amp;lt; &quot;\n&quot;;
    *_dout &amp;lt;&amp;lt; &quot;recursive lock of &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot; (&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot;)\n&quot;;
    BackTrace *bt = new BackTrace(BACKTRACE_SKIP);
    bt-&amp;gt;print(*_dout);
    if (p-&amp;gt;second) {
      *_dout &amp;lt;&amp;lt; &quot;\npreviously locked at\n&quot;;
      p-&amp;gt;second-&amp;gt;print(*_dout);
    }
    delete bt;
    *_dout &amp;lt;&amp;lt; dendl;
    assert(0);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;p-&amp;gt;first == id&lt;/code&gt;就说明发生了递归加锁，此时$BackTrace$会打印出当前发生递归的锁名和对应的锁ID，然后判断该锁之前是否有其他锁，如果有，则打印出其调用栈；如果没有发生递归锁，则需要建立新的依赖关系&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else if (!follows[p-&amp;gt;first][id]) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建依赖关系时，我们需要判断是否会发生循环，&lt;code&gt;followers[a][b]&lt;/code&gt;表示&lt;code&gt;b&lt;/code&gt;会发生在&lt;code&gt;a&lt;/code&gt;之后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (does_follow(id, p-&amp;gt;first)) {
  BackTrace *bt = new BackTrace(BACKTRACE_SKIP);
  lockdep_dout(0) &amp;lt;&amp;lt; &quot;new dependency &quot; &amp;lt;&amp;lt; lock_names[p-&amp;gt;first]
      &amp;lt;&amp;lt; &quot; (&quot; &amp;lt;&amp;lt; p-&amp;gt;first &amp;lt;&amp;lt; &quot;) -&amp;gt; &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot; (&quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot;)&quot;
      &amp;lt;&amp;lt; &quot; creates a cycle at\n&quot;;
  bt-&amp;gt;print(*_dout);
  *_dout &amp;lt;&amp;lt; dendl;

  lockdep_dout(0) &amp;lt;&amp;lt; &quot;btw, i am holding these locks:&quot; &amp;lt;&amp;lt; dendl;
  for (map&amp;lt;int, BackTrace *&amp;gt;::iterator q = m.begin(); q != m.end(); ++q) {
    lockdep_dout(0) &amp;lt;&amp;lt; &quot;  &quot; &amp;lt;&amp;lt; lock_names[q-&amp;gt;first] &amp;lt;&amp;lt; &quot; (&quot; &amp;lt;&amp;lt; q-&amp;gt;first &amp;lt;&amp;lt; &quot;)&quot; &amp;lt;&amp;lt; dendl;
    if (q-&amp;gt;second) {
      lockdep_dout(0) &amp;lt;&amp;lt; &quot; &quot;;
      q-&amp;gt;second-&amp;gt;print(*_dout);
      *_dout &amp;lt;&amp;lt; dendl;
    }
  }

  lockdep_dout(0) &amp;lt;&amp;lt; &quot;\n&quot; &amp;lt;&amp;lt; dendl;

  // don&apos;t add this dependency, or we&apos;ll get aMutex. cycle in the graph, and
  // does_follow() won&apos;t terminate.

  assert(0);  // actually, we should just die here.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们一开始判断的是&lt;code&gt;!follower[p-&amp;gt;first][id]&lt;/code&gt;，也就意味着&lt;code&gt;p-&amp;gt;first&lt;/code&gt;在&lt;code&gt;id&lt;/code&gt;锁之前发生，而&lt;code&gt;dose_follow(id, p-&amp;gt;first)&lt;/code&gt;为&lt;code&gt;True&lt;/code&gt;又表示了&lt;code&gt;id&lt;/code&gt;在&lt;code&gt;p-&amp;gt;first&lt;/code&gt;锁之前发生；这样就发生了一个循环依赖，所以最终会导致死锁。&lt;/p&gt;
&lt;p&gt;一旦上面的依赖检查无误，我们就可以正确的加入依赖关系，并且表明&lt;code&gt;id&lt;/code&gt;是能够被上锁的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else {
  BackTrace *bt = NULL;
  if (force_backtrace || lockdep_force_backtrace()) {
    bt = new BackTrace(BACKTRACE_SKIP);
  }
  follows[p-&amp;gt;first][id] = true;
  follows_bt[p-&amp;gt;first][id] = bt;
  lockdep_dout(10) &amp;lt;&amp;lt; lock_names[p-&amp;gt;first] &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; name &amp;lt;&amp;lt; &quot; at&quot; &amp;lt;&amp;lt; dendl;
  //bt-&amp;gt;print(*_dout);
}
return id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们回到&lt;code&gt;Lock&lt;/code&gt;函数中，如果&lt;code&gt;_will_lock&lt;/code&gt;没有触发$BackTrace$就说明该&lt;code&gt;id&lt;/code&gt;是可以正确上锁的，因此我们就可以调用上锁API&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r = pthread_mutex_lock(&amp;amp;_m);
assert(r == 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们开启了&lt;code&gt;lockdep&lt;/code&gt;功能，那么在成功上锁后，还需要进行&lt;code&gt;_locked&lt;/code&gt;操作检查，记录锁的状态和相关的调试信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (lockdep &amp;amp;&amp;amp; g_lockdep) _locked();

void _locked() {    // just locked
  id = lockdep_locked(name.c_str(), id, backtrace);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要的核心操作在于更新&lt;code&gt;held&lt;/code&gt;哈希表状态，如果启用了$backtrace$机制，则保存下$BackTrace$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (force_backtrace || lockdep_force_backtrace())
  held[p][id] = new BackTrace(BACKTRACE_SKIP);
else
  held[p][id] = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新完锁的状态后，还需要调用&lt;code&gt;_post_lock&lt;/code&gt;对锁的持有者进行更新&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void _post_lock() {
  if (!recursive) {
    assert(nlock == 0);
    locked_by = pthread_self();
  };
  nlock++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意，如果没有启用&lt;code&gt;recursive&lt;/code&gt;，会对&lt;code&gt;nlock&lt;/code&gt;进行断言，表示不允许获取已经持有的锁。&lt;/p&gt;
&lt;h4&gt;解锁过程&lt;/h4&gt;
&lt;p&gt;解锁相对于上锁而言，简单了不少，我们需要对锁的状态进行更改，然后判断是否能够解锁后，进行解锁即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Mutex::Unlock() {
  _pre_unlock();
  if (lockdep &amp;amp;&amp;amp; g_lockdep) _will_unlock();
  int r = pthread_mutex_unlock(&amp;amp;_m);
  assert(r == 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_pre_unlock&lt;/code&gt;会确认引用计数，如果没有启用&lt;code&gt;recursive&lt;/code&gt;还会释放其持有者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void _pre_unlock() {
  assert(nlock &amp;gt; 0);
  --nlock;
  if (!recursive) {
    assert(locked_by == pthread_self());
    locked_by = 0;
    assert(nlock == 0);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已经准备完解锁后，如果启用了&lt;code&gt;lockdep&lt;/code&gt;则需要判断依赖条件是否能够解锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void _will_unlock() {  // about to unlock
  id = lockdep_will_unlock(name.c_str(), id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接删除掉&lt;code&gt;held&lt;/code&gt;哈希表中的记录即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete held[p][id];
held[p].erase(id);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Locker&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/Mutex.h#L110&quot;&gt;Locker&lt;/a&gt;是对$Mutex$的封装，可以通过$Locker$直接调用&lt;code&gt;Lock&lt;/code&gt;和&lt;code&gt;UnLock&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Cond&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/Cond.h#L29&quot;&gt;Cond&lt;/a&gt;用于线程同步的封装，主要基于 POSIX 的 &lt;code&gt;pthread_cond_t&lt;/code&gt; 条件变量来实现。条件变量用于在多线程编程中协调线程之间的执行顺序，与$Mutex$配合使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Cond {
  // my bits
  pthread_cond_t _c;
  Mutex *waiter_mutex;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;_c
&lt;ul&gt;
&lt;li&gt;封装&lt;code&gt;pthread_cond_t&lt;/code&gt;，用于线程之间的等待和信号传递&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;waiter_mutex
&lt;ul&gt;
&lt;li&gt;指向当前等待该条件变量的$Mutex$,&lt;strong&gt;确保一个条件变量只能与一个互斥锁绑定&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于$Cond$的初始化和销毁操作没有特别需要说明的，当然，$Cond$&lt;strong&gt;仍旧不允许拷贝&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在进入等待时释放锁，被唤醒后重新持有锁，这样可以保证对共享资源的修改是线程安全的&lt;/strong&gt;。这也是我们需要遵循的设计原理。&lt;/p&gt;
&lt;h4&gt;Wait&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/Cond.h#L48&quot;&gt;Wait&lt;/a&gt;在$Cond$中有多个重载，但最终的操作都是由这样的代码进行处理的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int Wait(Mutex &amp;amp;mutex)  { 
  // make sure this cond is used with one mutex only
  assert(waiter_mutex == NULL || waiter_mutex == &amp;amp;mutex);
  waiter_mutex = &amp;amp;mutex;

  assert(mutex.is_locked());

  mutex._pre_unlock();
  int r = pthread_cond_wait(&amp;amp;_c, &amp;amp;mutex._m);
  mutex._post_lock();
  return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用&lt;code&gt;pthread_cond_wait&lt;/code&gt;前，我们需要保证$Mutex$是未上锁状态，因此需要使用&lt;code&gt;_pre_lock&lt;/code&gt;来保证$Mutex$维护的信息是表示解锁状态的：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为一旦&lt;code&gt;pthread_cond_wait&lt;/code&gt;进入&lt;code&gt;wait&lt;/code&gt;状态，其会自动解锁；&lt;code&gt;pthread_cond_wait&lt;/code&gt;被唤醒后，会重新获取锁，因此我们要恢复之前$Mutex$维护的信息，表示其是上锁的&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Signal&lt;/h4&gt;
&lt;p&gt;对于$Signal$，通过调用&lt;code&gt;pthread_cond_broadcast&lt;/code&gt;和&lt;code&gt;pthread_cond_signal&lt;/code&gt;可以选择唤醒所有等待线程，还是一个等待线程。&lt;/p&gt;
&lt;h3&gt;ThreadPool Structure&lt;/h3&gt;
&lt;p&gt;现在我们回到$ThreadPool$的代码上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ThreadPool : public md_config_obs_t {
  CephContext *cct;
  string name;
  string thread_name;
  string lockname;
  Mutex _lock;
  Cond _cond;
  bool _stop;
  int _pause;
  int _draining;
  Cond _wait_cond;
  int ioprio_class, ioprio_priority;
  
  unsigned int _num_threads;
  string _thread_num_option;
  const char** _conf_keys;
  vector&amp;lt;WorkQueue_*&amp;gt; work_queues;
  int last_work_queue;
  set&amp;lt;WorkThread*&amp;gt; _threads;
  list&amp;lt;WorkThread*&amp;gt; _old_threads;
  int processing;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;name
&lt;ul&gt;
&lt;li&gt;线程池的名字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;lockname
&lt;ul&gt;
&lt;li&gt;锁的名字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_lock
&lt;ul&gt;
&lt;li&gt;线程互斥的锁，也是工作队列访问互斥的锁&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_cond
&lt;ul&gt;
&lt;li&gt;锁对应的条件变量，用于控制并发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_stop
&lt;ul&gt;
&lt;li&gt;是否停止线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_pause
&lt;ul&gt;
&lt;li&gt;暂时中止线程池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_draining
&lt;ul&gt;
&lt;li&gt;在停止线程池前，等待已经提交的任务完成执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_wait_cond
&lt;ul&gt;
&lt;li&gt;通常用于等待所有线程完成或等待任务队列清空的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ioprio_class, ioprio_priority
&lt;ul&gt;
&lt;li&gt;ioprio_class 表示 I/O 优先级类别，比如实时优先级($IOPRIO_CLASS_RT$)、标准优先级($IOPRIO_CLASS_BE$)或空闲优先级($IOPRIO_CLASS_IDLE$)&lt;/li&gt;
&lt;li&gt;ioprio_priority 则是具体的优先级值，通常是一个整数，值越小优先级越高&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_num_threads
&lt;ul&gt;
&lt;li&gt;表示线程池中线程的数量。这个值决定了线程池启动时会创建多少个工作线程。线程池的大小可以动态调整，也可以通过配置文件或参数设置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_thread_num_option
&lt;ul&gt;
&lt;li&gt;线程数量配置选项的名称&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_conf_keys
&lt;ul&gt;
&lt;li&gt;指向配置键的数组，可能用于监听或监控与线程池相关的配置更改&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;work_queues
&lt;ul&gt;
&lt;li&gt;任务队列的集合，每个任务队列($WorkQueue_$)通常代表一组待处理的工作项。$ThreadPool$通过这些任务队列来分发任务给线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;last_work_queue
&lt;ul&gt;
&lt;li&gt;记录上一次被调度的任务队列索引。通常线程池在处理多个任务队列时，会轮询这些队列来选择下一个需要处理的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_threads
&lt;ul&gt;
&lt;li&gt;正在运行的工作线程的集合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;_old_threads
&lt;ul&gt;
&lt;li&gt;旧线程的列表，可能用于管理即将退出或已经停止的线程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;processing
&lt;ul&gt;
&lt;li&gt;表示当前正在处理的任务数量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TPHandle&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L42&quot;&gt;ThreadPoolHandle&lt;/a&gt;主要用于监控和管理线程池的超时、心跳机制等功能。通过与 ThreadPool 的交互来控制线程池的超时行为，并防止线程池中的线程长时间无响应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class TPHandle {
  friend class ThreadPool;
  CephContext *cct;       // Ceph全局上下文信息
  heartbeat_handle_d *hb; // 心跳机制处理句柄
  time_t grace;           // 表示宽限时间（grace period）的变量,用于控制线程的最大响应时间
  time_t suicide_grace;   // 用于控制线程的“自杀”时间，即当线程长时间不响应时，
                          // 系统可能会采取强制终止的手段，确保系统的整体健康
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;heartbeat_handle_d&lt;/h5&gt;
&lt;p&gt;首先我们先需要搞懂&lt;code&gt;heartbeat_handle_d&lt;/code&gt;这一玩意到底做了些什么，如果我们了解过&lt;a href=&quot;https://en.wikipedia.org/wiki/Raft_(algorithm)&quot;&gt;Raft&lt;/a&gt;的话，就理应知晓心跳机制的作用：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用于保持集群中的Leader和Follower之间的同步状态，并确保集群能够在一致的状态下正常运行&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct heartbeat_handle_d {
  const std::string name;
  atomic_t timeout, suicide_timeout;
  time_t grace, suicide_grace;
  std::list&amp;lt;heartbeat_handle_d*&amp;gt;::iterator list_item;

  explicit heartbeat_handle_d(const std::string&amp;amp; n)
    : name(n), grace(0), suicide_grace(0)
  { }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从&lt;code&gt;heartbeat_handle_d&lt;/code&gt;的结构上看，其主要是管理了一个可维护的时间序列。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;timeout&lt;/code&gt;表示常规的心跳超时。在超时时间内，组件或线程必须发送心跳信号，否则系统将视其为不可用；而&lt;code&gt;grace&lt;/code&gt;表示宽限期。宽限期是允许组件或线程在不发送心跳信号的情况下，继续运行的一段时间。在 grace 期内，即使没有收到心跳信号，系统不会立即判定线程失效。它是一种缓冲机制，防止短暂的延迟或网络波动导致不必要的超时。&lt;/p&gt;
&lt;p&gt;一旦超时后，如果组件或线程在&lt;code&gt;suicide_timeout&lt;/code&gt;时间内仍未发送心跳信号，系统可能会采取更严重的措施，如强制结束线程或将其标记为失败。&lt;code&gt;suicide_grace&lt;/code&gt; 是类似于 &lt;code&gt;grace&lt;/code&gt; 的宽限期，但它与&lt;code&gt;suicide&lt;/code&gt;相关联&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;现在，我们回到$TPHandle$的实现中，这里主要给出了两个函数用于处理心跳机制：&lt;code&gt;reset_tp_timeout&lt;/code&gt;和&lt;code&gt;suspend_tp_timeout&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;reset_tp_timeout&lt;/code&gt;的内部由$CephContext$调用&lt;code&gt;reset_timeout()&lt;/code&gt;实现，因为$CephContext$会管理全局心跳信息，因此应该从全局上修改；该函数用于&lt;strong&gt;重置心跳超时时间，并且设置新的宽恕时间&lt;/strong&gt;(如果有的话)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cct-&amp;gt;get_heartbeat_map()-&amp;gt;reset_timeout(hb, grace, suicide_grace);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，&lt;code&gt;suspend_tp_timeout&lt;/code&gt;也是由$CephContext$调用&lt;code&gt;clear_timeout()&lt;/code&gt;实现；其作用是清空超时时间和宽恕时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cct-&amp;gt;get_heartbeat_map()-&amp;gt;clear_timeout(hb);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;WorkQueue_&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L61&quot;&gt;WorkQueue_&lt;/a&gt;是 Ceph 中一个工作队列的基本接口，供工作线程使用。工作队列在 Ceph 这样的分布式系统中，通常用来管理需要处理的任务。该接口定义了与队列管理、任务处理相关的核心操作和抽象方法，具体实现将由继承该接口的类来完成。&lt;/p&gt;
&lt;p&gt;其中，&lt;code&gt;timeout_interval&lt;/code&gt;表示工作队列中的任务超时时间。每个任务在队列中不应停留超过这个时间，如果超时，可能会触发警告或其他恢复机制。&lt;code&gt;suicide_interval&lt;/code&gt;表示自杀间隔时间。如果队列长时间处于无响应状态或任务处理过于缓慢，可能会根据这个时间采取终止处理或回收资源的措施。&lt;/p&gt;
&lt;p&gt;不论在哪一种$WorkQueue$中，我们只需要记住：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进队时需要加入任务并唤醒一个线程处理&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;BatchWorkQueue&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L101&quot;&gt;BatchWorkQueue&lt;/a&gt;用于处理一批工作项。该类通过泛型模板来灵活适应不同类型的工作项，并提供了批量处理任务的机制&lt;/p&gt;
&lt;p&gt;对于$BatchWorkQueue$而言，实际上还是一个基类，主要用于需要进行批处理的Work去实例化。&lt;/p&gt;
&lt;p&gt;我们需要注意的函数可能是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;queue&lt;/code&gt;和&lt;code&gt;dequeue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要衍生类实现的&lt;code&gt;_enqueue&lt;/code&gt;和&lt;code&gt;_dequeue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;需要衍生类实现的&lt;code&gt;_process&lt;/code&gt;和&lt;code&gt;_process_finish&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;WorkQueueVal_&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L182&quot;&gt;WorkQueueVal_&lt;/a&gt;专门用于处理按值传递的任务。与其他工作队列不同，它通过模板参数来支持值类型（而非指针类型）的任务处理，适合于处理较小的、内存占用较少的任务对象。&lt;/p&gt;
&lt;p&gt;在该类型中，多出来两个成员用于管理任务队列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;to_process&lt;/code&gt;：任务处理列表，存储待处理的任务。在队列取出任务时，会将其放入 &lt;code&gt;to_process&lt;/code&gt; 列表。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to_finish&lt;/code&gt;：任务完成列表，存储已处理但未完成的任务。任务处理完成后，会被移入此列表，以便进一步的后续处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面类似，其实这也是需要具体实现各种实际操作的，不过在入队出队时，需要根据实际情况填写&lt;code&gt;to_process&lt;/code&gt;和&lt;code&gt;to_finish&lt;/code&gt;逻辑。&lt;/p&gt;
&lt;h5&gt;WorkQueue&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L266&quot;&gt;WorkQueue&lt;/a&gt;主要用于处理较大或包含动态分配内存的工作项。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;该类适用于需要异步处理大量数据的场景，例如在 Ceph 中处理存储请求的后台任务，能够有效管理动态分配的内存，并确保任务的并发处理&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;PointerWQ&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/3a66dd4f30852819c1bdaa8ec23c795d4ad77269/src/common/WorkQueue.h#L346&quot;&gt;PointerWQ&lt;/a&gt;实现了一个按指针提交的工作队列。这个类的设计主要用于处理类型为 T 的工作项，适合于需要动态内存管理的场景。&lt;/p&gt;
&lt;p&gt;和上面不同的是，$PointerWQ$多了两个成员用于管理内部任务信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;m_items&lt;/code&gt;: 存储指向工作项的指针的列表，使用双向链表方便在队列头部和尾部进行插入和删除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;m_processing&lt;/code&gt;: 记录当前正在处理的工作项的数量，保证线程安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;WorkThread&lt;/h4&gt;
&lt;h5&gt;Thread&lt;/h5&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ceph/ceph/blob/c5faa93d2696af5c9c4f130b73e6e13e30e206d7/src/common/Thread.h#L34&quot;&gt;Thread&lt;/a&gt;是Ceph中对于$POSIX$线程的封装。&lt;strong&gt;额外提供了优先级处理&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Thread {
private:
  pthread_t thread_id;
  pid_t pid;
  int ioprio_class, ioprio_priority;
  int cpuid;
  const char *thread_name;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们主要需要注意的是Ceph在创建、销毁和设置线程属性时的处理：&lt;/p&gt;
&lt;p&gt;对于创建线程，$Thread$特别提供了&lt;code&gt;try_create&lt;/code&gt;用于处理创建线程的核心部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stacksize &amp;amp;= CEPH_PAGE_MASK;  // must be multiple of page
  if (stacksize) {
    thread_attr = &amp;amp;thread_attr_loc;
    pthread_attr_init(thread_attr);
    pthread_attr_setstacksize(thread_attr, stacksize);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们通过传入的线程栈大小与设置的页掩码比较，确定了传入的参数是合乎规范的，因此设定了线程的栈大小属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sigset_t old_sigset;
if (g_code_env == CODE_ENVIRONMENT_LIBRARY) {
  block_signals(NULL, &amp;amp;old_sigset);
}
else {
  int to_block[] = { SIGPIPE , 0 };
  block_signals(to_block, &amp;amp;old_sigset);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;在 Ceph 中，许多组件都依赖于多线程来处理请求（如存储请求、数据复制等）。在创建新线程之前阻塞信号，可以确保新线程在执行关键初始化逻辑时不会意外中断，从而维护系统的稳定性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;允许 Ceph 在不同的部署或运行模式下采取不同的信号处理策略。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r = pthread_create(&amp;amp;thread_id, thread_attr, _entry_func, (void*)this);
restore_sigset(&amp;amp;old_sigset);

if (thread_attr) {
  pthread_attr_destroy(thread_attr);	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当确保Ceph需要新启动的线程不会收到其他线程干扰后，就可以开始启动线程，然后恢复信号状态。如果在之前设置了线程属性，则需要销毁保证资源回收。&lt;/p&gt;
&lt;p&gt;我们发现，这里通过回调，使得线程从&lt;code&gt;_entry_func&lt;/code&gt;启动，并且将自身传入作为参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void *Thread::_entry_func(void *arg) {
  void *r = ((Thread*)arg)-&amp;gt;entry_wrapper();
  return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;entry_wrapper&lt;/code&gt;函数中，会对线程的属性进一步设置，同时调用真正线程应该执行的函数(这一点由衍生类进行实现)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void *Thread::entry_wrapper() {
  int p = ceph_gettid(); // may return -ENOSYS on other platforms
  if (p &amp;gt; 0)
    pid = p;
  if (pid &amp;amp;&amp;amp;
      ioprio_class &amp;gt;= 0 &amp;amp;&amp;amp;
      ioprio_priority &amp;gt;= 0) {
    ceph_ioprio_set(IOPRIO_WHO_PROCESS,
		    pid,
		    IOPRIO_PRIO_VALUE(ioprio_class, ioprio_priority));
  }
  if (pid &amp;amp;&amp;amp; cpuid &amp;gt;= 0)
    _set_affinity(cpuid);

  pthread_setname_np(pthread_self(), thread_name);
  return entry();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ceph_gettid&lt;/code&gt;对&lt;code&gt;SYS_gettid&lt;/code&gt;进行直接调用，获取当前线程的线程ID，如果存在，那且有优先级设置，我们就需要对优先级进行设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syscall(SYS_ioprio_set, whence, who, ioprio);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;whence&lt;/code&gt;指明了是用于线程还是进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;who&lt;/code&gt;指明用于哪一个目标&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ioprio&lt;/code&gt;则是优先级&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设置完完优先级后，我们还需要对CPU进行处理，将特定线程绑定到特定的CPU上，提高资源利用率&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cpu_set_t cpuset;
CPU_ZERO(&amp;amp;cpuset);

CPU_SET(id, &amp;amp;cpuset);

if (sched_setaffinity(0, sizeof(cpuset), &amp;amp;cpuset) &amp;lt; 0)
  return -errno;
/* guaranteed to take effect immediately */
sched_yield();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们开始调用真正的线程需要执行的函数入口&lt;code&gt;entry&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;现在我们回到$WorkThread$结构体，该结构主要针对&lt;code&gt;entry&lt;/code&gt;进行实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void *entry() {
  pool-&amp;gt;worker(this);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在$WorkThread$中，为了方便的共享数据，我们将$ThreadPool$作为指针成员放置在$WorkThread$中，然后通过$ThreadPool$来进行$work$(在后面详细介绍其工作原理)。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Start Thread&lt;/h3&gt;
&lt;p&gt;到现在为止，我们已经了解了关于$ThreadPool$的所有零碎的片段，现在，应该从$ThreadPool$会做什么事情开始进行分析了。&lt;/p&gt;
&lt;p&gt;在$ThreadPool$中会提供一个$start$函数入口作为线程池的启动函数，而该函数会简单的调用$start_thread$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WorkThread *wt = new WorkThread(this);
_threads.insert(wt);
int r = wt-&amp;gt;set_ioprio(ioprio_class, ioprio_priority);
wt-&amp;gt;create(thread_name.c_str());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;start_thread&lt;/code&gt;会通过$WorkThread$创建一个工作线程，然后设置其优先级，并真正的创建一个$POSIX\ Thread$。启动的线程会自动的调用&lt;code&gt;worker&lt;/code&gt;函数，对于$ThreadPool$，&lt;code&gt;worker&lt;/code&gt;是主要的核心逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将当前工作线程加入到心跳管理中&lt;/li&gt;
&lt;li&gt;检查并处理已经结束的线程，确保线程池中的线程数量在规定范围内
&lt;ol&gt;
&lt;li&gt;如果当前线程数量超过了最大线程数，则记录并将该线程移交到&lt;code&gt;old_thread&lt;/code&gt;中管理&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;轮询每一个工作队列中的任务
&lt;ol&gt;
&lt;li&gt;如果正确从工作队列中得到任务，通过$TPHandle$设置心跳时间，进行处理
2.处理完毕后，worker需要等待下一个任务&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;当所有任务轮询完毕，将该线程的心跳管理移除&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Stop Thread&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;stop&lt;/code&gt;函数需要做的工作很简单，只需要将自身在&lt;code&gt;observer&lt;/code&gt;的监视中移除，然后唤醒所有线程，并处理完休眠的线程还未完成的任务，然后逐一清空即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cct-&amp;gt;_conf-&amp;gt;remove_observer(this);

_cond.Signal();
join_old_threads();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CephV10.2.1: Overall Architecture</title><link>https://chenmiaoi.github.io/2024/10/12/CephV10-2-1-Overall-Architecture/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/10/12/CephV10-2-1-Overall-Architecture/</guid><pubDate>Sat, 12 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本节主要对Ceph的系统架构进行介绍。&lt;/p&gt;
&lt;h2&gt;Ceph的设计目标&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Ceph的设计目标是构建大规模的、具有高可用性的、高可扩展性的、高性能的分布式存储系统&lt;/strong&gt;&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;系统的高可用性指的是系统某个部件失效后，系统依然可以提供正常服务的能力。一半用设备部件和数据的冗余来提高可用性。Ceph通过数据多副本、纠删码来提供数据的冗余。&lt;/p&gt;
&lt;p&gt;高可扩展性指的是系统可以灵活地应对集群的伸缩。一般值两个方面，一方面指集群的容量可以伸缩，集群可以任意地添加和删除存储节点和存储设备；另一方面指的是系统的性能随集群的增加而线性增加。&lt;/p&gt;
&lt;h2&gt;Ceph基本架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131738213.png&quot; alt=&quot;Ceph基本架构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ceph的整体架构由三个层次组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最底层/最核心的部分RADOS对象存储系统，基于RADOS($Reliable, Autonomous, Distributed\ object\ store$)，是一个可靠的、自组织的、可自动修复、自我管理的分布式对象存储系统。内部包括&lt;code&gt;ceph-osd&lt;/code&gt;后台服务和&lt;code&gt;ceph-mon&lt;/code&gt;监控&lt;/li&gt;
&lt;li&gt;&lt;code&gt;librados&lt;/code&gt;层用于本地或者远程通过网络访问RADOS对象存储系统&lt;/li&gt;
&lt;li&gt;Ceph的存储接口实现层
&lt;ul&gt;
&lt;li&gt;块存储接口，通过&lt;code&gt;librbd&lt;/code&gt;提供了块存储访问接口。可以为虚拟机提供虚拟磁盘，或者通过内核映射为物理主机提供磁盘空间。&lt;/li&gt;
&lt;li&gt;对象存储接口，目前提供了两种类型的API，
&lt;ul&gt;
&lt;li&gt;AWS S3 API&lt;/li&gt;
&lt;li&gt;OpenStack Swift API&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;文件存储接口，提供了两种类型的接口，文件系统的元数据通过&lt;code&gt;MDS&lt;/code&gt;访问，而数据直接通过&lt;code&gt;librados&lt;/code&gt;访问
&lt;ul&gt;
&lt;li&gt;标准Posix接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;libcephfs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ceph客户端接口&lt;/h2&gt;
&lt;h3&gt;RBD&lt;/h3&gt;
&lt;p&gt;RBD($Rados\ Block\ Device$)通过&lt;code&gt;librbd&lt;/code&gt;对应用提供块存储，主要面向云平台的虚拟机提供虚拟磁盘。&lt;/p&gt;
&lt;p&gt;目前RBD提供了两个接口，一种是直接在用户态中实现，通过&lt;code&gt;QEMU Driver&lt;/code&gt;供KVM虚拟机使用。另一种在操作系统内核态中实现了一个内核模块。通过该模块可以把块设备映射给物理主机，由物理主机直接访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;块存储既需要有较好的随机I/O，有要求有较好的顺序I/O，且对延迟有比较严格的要求&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;CephFS&lt;/h3&gt;
&lt;p&gt;CephFS通过在$RADOS$基础之上增加了$MDS(Metadata\ Server)$来提供文件存储。其提供了&lt;code&gt;libcephfs&lt;/code&gt;和标准的POSIX文件接口。&lt;strong&gt;CephFS通过NFS或CIFS协议提供文件系统或文件目录文件&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;RadosGW&lt;/h3&gt;
&lt;p&gt;$RadosGW$基于&lt;code&gt;librados&lt;/code&gt;提供了和Amazon S3接口以及OpenStack Swift接口兼容的对象存储接口。可将其简单地理解为提供基本文件的上传和下载的需求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提供$RESTful\ Web\ API$&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;采用扁平化数据组织形式&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;RESTful存储接口&lt;/h4&gt;
&lt;p&gt;其简单的提供了&lt;code&gt;GET&lt;/code&gt;、&lt;code&gt;PUT&lt;/code&gt;、&lt;code&gt;DEL&lt;/code&gt;等接口，对应文件的上传、下载、删除、查询等操作。&lt;/p&gt;
&lt;h4&gt;扁平化数据组织形式&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131752728.png&quot; alt=&quot;Amazon S3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131752185.png&quot; alt=&quot;OpenStack Swift&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;RADOS&lt;/h2&gt;
&lt;p&gt;$RADOS$是Ceph存储系统的基石，它完成了一个存储系统的核心功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Monitor模块为整个存储集群提供全局的配置和系统信息&lt;/li&gt;
&lt;li&gt;CRUSH算法实现对象的寻址过程&lt;/li&gt;
&lt;li&gt;完成对象的读写以及其他数据功能&lt;/li&gt;
&lt;li&gt;提供数据均衡功能&lt;/li&gt;
&lt;li&gt;通过Peering完成一个PG内存达成数据一致性的过程&lt;/li&gt;
&lt;li&gt;提供数据自恢复的功能&lt;/li&gt;
&lt;li&gt;提供克隆和快照功能&lt;/li&gt;
&lt;li&gt;实现对象分层存储的功能&lt;/li&gt;
&lt;li&gt;实现数据一致性检查工具Scrub&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Monitor&lt;/h3&gt;
&lt;p&gt;$Monitor$是一个独立部署的daemon进程。通过组成Monitor集群来保证自己的高可用性。Monitor集群通过Paxos算法实现了自己数据的一致性。它提供了整个存储系统的节点信息等全局的配置信息。&lt;/p&gt;
&lt;p&gt;$Cluster\ Map$中保存了系统的全局信息，主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Monitor Map
&lt;ul&gt;
&lt;li&gt;集群的fsid&lt;/li&gt;
&lt;li&gt;所有Monitor的地址和端口&lt;/li&gt;
&lt;li&gt;current epoch&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OSD Map
&lt;ul&gt;
&lt;li&gt;所有OSD的列表，和OSD的状态等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MDS Map
&lt;ul&gt;
&lt;li&gt;所有的MDS的列表和状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$Cluster\ Map$是直接保存在$Monitor$的内存中的，不过我们可以通过$mon/store.db/*.sst$文件进行查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ceph-kvstore-tool rocksdb */mon.a/store.db/*.sst list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是：&lt;strong&gt;该文件中的内容很多，不宜查看&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而$Monitor\ Map$、$OSD\ Map$和$MDS\ Map$都是保存在&lt;code&gt;rocksdb&lt;/code&gt;的数据库中，因此也无法直接查看，我们可以通过Ceph命令进行查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ceph mon getmap -o ./monmap.bin
monmaptool --print ./monmap.bin

monmaptool: monmap file ./monmap.bin
epoch 1
fsid 5bbbc20d-a725-4e68-a426-4c9210a4062e
last_changed 2024-10-13 18:02:39.282195
created 2024-10-13 18:02:39.282195
0: &amp;lt;mon_ip&amp;gt;:6789/0 mon.a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我启动时只启动了一个&lt;code&gt;MON&lt;/code&gt;节点，因此这里只会输出一个&lt;code&gt;MON&lt;/code&gt;节点信息，&lt;code&gt;fsid&lt;/code&gt;($File\ System\ ID$)是&lt;strong&gt;一个唯一标识符，用于识别和区分 Ceph 集群。每个 Ceph 集群在初始化时都会生成一个 &lt;code&gt;fsid&lt;/code&gt;，并在集群的配置文件和元数据中进行存储&lt;/strong&gt;。&lt;code&gt;fsid&lt;/code&gt; 是一个 128 位的 UUID，以十六进制格式表示。&lt;/p&gt;
&lt;p&gt;其余节点的信息均可以通过以下命令查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 方式一
ceph mon/osd dump
ceph mds stat

# 方式二
ceph mon/osd/mds getmap
*maptool --print /path/to/*map.bin #V10.2.1版本未提供mdsmaptool
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;对象存储&lt;/h3&gt;
&lt;p&gt;在这里的对象均指$RADOS$对象，需要和$RadosGW$的SE或Swift对象区别开来。&lt;strong&gt;对象是数据存储的基本单元，一般默认$4MB$大小&lt;/strong&gt;，如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131823505.png&quot; alt=&quot;对象示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个对象由三部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;标识(ID)，唯一标识一个对象&lt;/li&gt;
&lt;li&gt;数据，在本地文件系统中对应一个文件，对象的数据均保存在文件中&lt;/li&gt;
&lt;li&gt;元数据，以$Key-Value$的形式保存，$RADOS$以$LevelDB$等本地KV存储系统来保存对象的元数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;pool &amp;amp; PG&lt;/h3&gt;
&lt;p&gt;$pool$是一个抽象的存储池，其规定了数据冗余的类型以及对应的副本分布策略。目前实现了两种类型的$pool$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Replicated$&lt;/li&gt;
&lt;li&gt;$Erasure\ Code$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个$pool$由多个$PG$组成。$PG(Placement\ Group)$从名字上可理解为一个放置策略组，&lt;strong&gt;它是对象的集合，该集合的所有对象都具有相同的放置策略：&lt;em&gt;对象的副本都分布在相同的$OSD$列表上，一个对象只能属于一个PG，一个PG对应于放置在其上的OSD列表，而一个OSD上可以分布多个PG&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131859238.png&quot; alt=&quot;PG概念示意图&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$PG1$和$PG2$都属于同一个$pool$，所以都是副本类型，并且都是两副本&lt;/li&gt;
&lt;li&gt;$PG1$和$PG2$都包含许多对象，$PG1$上的所有对象，主从副本分布在$OSD1$和$OSD2$上，$PG2$上的所有对象的主从副本都分布在$OSD2$和$OSD3$上&lt;/li&gt;
&lt;li&gt;一个对象只能属于一个$PG$，一个$PG$包含多个对象&lt;/li&gt;
&lt;li&gt;一个$PG$的副本分布在对应的$OSD$列表中，在一个$OSD$上可以分布多个$PG$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;对象寻址过程&lt;/h3&gt;
&lt;p&gt;对象取址过程指的是&lt;strong&gt;查找对象在集群中分布的位置信息&lt;/strong&gt;，该过程分为两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对象到$PG$的映射。这个过程是静态$Hash$映射(加入$pg\ split$后实际变为动态$Hash$映射)，通过对$object_id$计算出$hash$值，使用该$pool$的$PG$的总数量$pg_num$对$hash$取模，就能够得到该对象所在的$PG$的$id$：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pg_id = hash(object_id) % pg_num
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;$PG$到$OSD$列表映射。这里指$PG$上对象的副本如何分布在$OSD$上，使用了Ceph创新的$CRUSH$算法实现：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131907133.png&quot; alt=&quot;对象寻址过程&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;数据读写过程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131908931.png&quot; alt=&quot;读写过程&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client向该$PG$所在的主$OSD$发送写请求&lt;/li&gt;
&lt;li&gt;主$OSD$接收到写请求后，同时向两个$OSD$发送写副本的请求，并同时写入主$OSD$的本地存储中&lt;/li&gt;
&lt;li&gt;主$OSD$接收到两个从$OSD$发送写成功的$ACK$应答，同时确认自己写成功，就向客户端返回写成功的$ACK$应答&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;写操作中，主$OSD$必须等待所有从$OSD$返回正确应答，才能向客户端返回写操作成功&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;数据均衡&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;当在集群中添加一个新OSD存储设备时，整个集群会发生数据的迁移，使得数据分布达到均衡&lt;/strong&gt;。&lt;em&gt;Ceph数据迁移的基本单位是&lt;/em&gt;$PG$，即数据迁移是将$PG$中的所有对象作为一个整体来迁移。&lt;/p&gt;
&lt;p&gt;迁移触发的流程为：&lt;strong&gt;新加入一个OSD，改变系统的$CRUSH\ MAP$，从而引起对象寻址过程中的第二步，$PG$到$OSD$列表的映射发生了变化。从而引发数据的迁移&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131917345.png&quot; alt=&quot;数据迁移&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Peering&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;当OSD启动，或者某个OSD失效时，该OSD上的主$PG$会发起一个Peering的过程&lt;/strong&gt;。&lt;em&gt;Ceph的Peering过程指的是一个$PG$内的所有副本通过$PG$日志来打成数据一致的过程&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;当Peering完成之后，该$PG$就可以对外提供读写服务。此时的$PG$的某些对象可能处于数据不一致的状态，则被标记出来，需要恢复。系统优先恢复该对象，才能继续完成写操作。&lt;/p&gt;
&lt;h4&gt;Recovery &amp;amp; Backfill&lt;/h4&gt;
&lt;h4&gt;纠删码&lt;/h4&gt;
&lt;h4&gt;快照和克隆&lt;/h4&gt;
&lt;p&gt;$SnapShot$&lt;strong&gt;是一个存储设备在某一时刻的&lt;em&gt;全部只读镜像&lt;/em&gt;&lt;/strong&gt;。而$Clone$&lt;strong&gt;是在某一时刻的&lt;em&gt;全部可写镜像&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$RADOS$对象存储系统本身支持$Copy-on-Write$方式的快照机制。基于这个机制，Ceph可以实现两种类型的$SnapShot$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$pool$级别，给整个$pool$中的所有对象统一做快照操作&lt;/li&gt;
&lt;li&gt;用户自定义快照，$RBD$快照实现就属于这一级别&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Cache Tier&lt;/h4&gt;
&lt;p&gt;$RADOS$实现了以$pool$为基础的自动分层存储机制。在第一层可以设置$cache\ pool$，&lt;strong&gt;其为高速存储设备&lt;/strong&gt;；第二层为$data\ pool$，&lt;strong&gt;使用大容量低速存储设备，可以使用EC模式来降低存储空间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过$Cache\ Tier$，&lt;strong&gt;可以提高关键数据或热点数据的性能，同时降低存储开销&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410131927609.png&quot; alt=&quot;Cache Tier&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ceph Client对于Cache层是透明的&lt;/li&gt;
&lt;li&gt;Objecter负责请求是发送给Cache Tier层，还是发给Storage Tier层&lt;/li&gt;
&lt;li&gt;Cache Tier层为高速I/O层，保存热点数据，或称为活跃的数据&lt;/li&gt;
&lt;li&gt;Storage Tier层为慢速层，保存非活跃的数据&lt;/li&gt;
&lt;li&gt;在Cache Tier层和Storage Tier层之间，数据根据活跃度自动进行迁移&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Scrub&lt;/h4&gt;
&lt;p&gt;$Scrub$&lt;strong&gt;用于系统检查数据一致性，通过在后台定期扫描，比较一个$PG$内的对象分别在其他$OSD$上的各个副本的元数据和数据来检查是否一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根据扫描内容分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只比较对象各个副本的元数据：代价小、效率高，对系统影响小&lt;/li&gt;
&lt;li&gt;$deep\ scrub$，不仅需要比较元数据，还需要比较数据&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>CephV10.2.1: Deploy Ceph</title><link>https://chenmiaoi.github.io/2024/10/11/CephV10-2-1-Deploy-Ceph/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/10/11/CephV10-2-1-Deploy-Ceph/</guid><pubDate>Fri, 11 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;编译 &amp;amp; 构建&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ceph部署环境
&lt;ul&gt;
&lt;li&gt;OS: CentOS7.9&lt;/li&gt;
&lt;li&gt;Version: v10.2.1&lt;/li&gt;
&lt;li&gt;Method: Cmake Build&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ceph模块分析和作用&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410111735229.png&quot; alt=&quot;Ceph Architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ceph 的最底层基于 RADOS(Reliable, Autonomous, Distributed object store)，是一个可靠的、自组织的、可自动修复、自我管理的分布式对象存储系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;块存储接口：通过 librbd(rados block device)库提供了块存储访问接口。可以为虚拟机提供虚拟磁盘，或通过内核映射为物理主机提供磁盘空间。&lt;/li&gt;
&lt;li&gt;对象存储接口：
&lt;ul&gt;
&lt;li&gt;AWS S3 接口 API&lt;/li&gt;
&lt;li&gt;OpenStack Swift 接口 API&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;文件系统接口：
&lt;ul&gt;
&lt;li&gt;标准 Posix 接口&lt;/li&gt;
&lt;li&gt;libcephfs 库接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Monitor(MON)节点&lt;/h3&gt;
&lt;p&gt;mon节点是Ceph集群的管理节点，负责维护集群的全局状态，包括存储节点（OSD）的健康状态、认证信息、CRUSH映射（存储位置算法），以及其他元数据。&lt;/p&gt;
&lt;p&gt;通过 Paxos 分布式算法来确保集群状态的一致性。如果集群中的 mon 节点出现故障，只要多数（即至少一半以上的 mon 节点正常工作），集群就能继续正常运行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在启动Ceph集群时，mon节点应该最先启动，因为它负责维持整个集群的健康状态、存储管理、并为其他组件提供集群的配置信息&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当 mon 节点启动后，其他节点（如 OSD 和 MDS）才能与它们进行通信，进行注册并获取集群状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Object Storage Daemon(OSD)节点&lt;/h3&gt;
&lt;p&gt;OSD 是Ceph中的对象存储守护进程，每个 OSD 代表一个物理或虚拟的存储设备。其负责存储数据、复制数据、恢复故障、重平衡数据，并提供给客户端读写访问的数据。OSD 节点通过 CRUSH 映射算法与 mon 交互，决定如何在集群中分布存储数据。集群中的所有 OSD 节点通过监控自身的状态，并定期向 mon 节点报告其状态（如在线、离线、正在恢复等）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OSD 节点在 mon 节点启动并成功运行后启动，因为它们需要从 mon 节点获取存储分布信息（CRUSH 映射）并注册自身&lt;/strong&gt;。OSD 节点启动后，会根据CRUSH映射将数据分布到集群中，并确保数据的冗余和复制。&lt;/p&gt;
&lt;h3&gt;Metadata Server(MDS)节点&lt;/h3&gt;
&lt;p&gt;MDS 节点是Ceph文件系统（CephFS）的元数据服务器。它负责处理文件系统的元数据操作，比如目录结构、文件路径的解析、权限管理等。MDS 的作用是将CephFS中的元数据与数据操作分离。它只负责元数据的管理，而实际的文件数据则由 OSD 节点存储和处理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MDS 节点在 OSD 节点启动后启动，因为 MDS 需要依赖 OSD 提供底层的对象存储服务&lt;/strong&gt;。在 MDS 节点启动时，它会与 OSD 节点和 mon 节点通信，以了解集群的存储状态和元数据分布。&lt;/p&gt;
&lt;h3&gt;Ceph File System(CephFS)&lt;/h3&gt;
&lt;p&gt;CephFS 是基于 Ceph 构建的分布式文件系统，允许客户端以文件和目录的方式访问数据，类似于传统的文件系统（如 NFS 或 ext4）。其将数据存储在 OSD 节点中，MDS 节点则处理文件系统的元数据操作，如目录树的管理、文件路径的解析等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CephFS 依赖于 mon、OSD 和 MDS 节点，因此 CephFS 需要在这些组件启动后才能正常工作&lt;/strong&gt;。在集群中配置和启用 CephFS 时，首先需要确保 OSD 和 mon 正常运行，然后 MDS 节点启动，最后是 CephFS 文件系统的启用和挂载。&lt;/p&gt;
&lt;h3&gt;Rados GateWay(RGW)&lt;/h3&gt;
&lt;p&gt;RADOS Gateway（RGW）是Ceph的对象存储网关，它提供兼容Amazon S3和OpenStack Swift协议的RESTful API，用于对象存储。RGW允许用户通过HTTP协议直接与Ceph集群交互，适合需要对象存储的应用场景，比如云存储服务或大规模数据存储。它为Ceph提供了对象存储的前端，用户可以通过RGW将文件存储为对象，并通过唯一的对象键来检索数据，类似于云服务中的S3。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RGW依赖于mon和OSD节点来存储和检索数据，因此它必须在mon和OSD节点启动后才能启动&lt;/strong&gt;。RGW 作为 Ceph 的一个&lt;strong&gt;额外组件，不是必须的&lt;/strong&gt;，但在需要对象存储功能时，RGW 需要启动。启动时，&lt;em&gt;RGW会使用Ceph集群的认证系统（通常通过cephx身份验证机制），并与OSD节点交互以存储对象数据&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410111740748.png&quot; alt=&quot;S3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202410111740702.png&quot; alt=&quot;Swift&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;vstart.sh脚本分析&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;vstart.sh&lt;/code&gt;中提供了一些可选的参数，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-d, --debug
-s, --standby_mds
-l, --localhost
-i &amp;lt;ip&amp;gt;
-r
-n, --new
--valgrind[_{osd,mds,mon}]
--nodaemon
--smallmds
-m ip:port
-k
-x/X
--hitset &amp;lt;pool&amp;gt; &amp;lt;hit_set_type&amp;gt;
-e
-o config
--mon_num
--osd_num
--mds_num
--rgw_port
--bluestore
--memstore
--cache &amp;lt;pool&amp;gt;
--short
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;了解完参数含义后，我们就开始逐段解析&lt;code&gt;vstart.sh&lt;/code&gt;的部署过程。&lt;/p&gt;
&lt;h3&gt;前期准备&lt;/h3&gt;
&lt;p&gt;Ceph的部署需要在一个特定的文件夹中生成各类配置文件，包括mon、mds、osd等的配置目录，而&lt;code&gt;vstart.sh&lt;/code&gt;会专门为&lt;code&gt;Cmake&lt;/code&gt;编译后的部署做出处理，因此我们需要通过&lt;code&gt;run-cmake-check.sh&lt;/code&gt;进行编译。&lt;/p&gt;
&lt;p&gt;编译完成后，我们会在项目根目录下生成这样的目录树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ceph
├── ...
└── build
          ├── bin
          ├── CMakeCache.txt
          ├── CMakeFiles
          ├── include
          ├── lib
          ├── Makefile
          └── src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;bin&lt;/code&gt;保存了我们所有编译出来的与&lt;code&gt;ceph&lt;/code&gt;有关的二进制可执行程序，&lt;code&gt;lib&lt;/code&gt;包含了&lt;code&gt;ceph&lt;/code&gt;二进制可执行程序所需要的一切动静态库。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;vstart.sh&lt;/code&gt;而言，如果环境变量中没有特殊配置，则会自动检索&lt;code&gt;CMakeCache.txt&lt;/code&gt;来得到项目的根目录和&lt;code&gt;build&lt;/code&gt;目录的路径。如果我们正确获取到这两个路径后，那么就会进行这样的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elif [ -n &quot;$CEPH_ROOT&quot; ]; then
        [ -z &quot;$PYBIND&quot; ] &amp;amp;&amp;amp; PYBIND=$CEPH_ROOT/src/pybind
        [ -z &quot;$CEPH_BIN&quot; ] &amp;amp;&amp;amp; CEPH_BIN=$CEPH_BUILD_DIR/bin
        [ -z &quot;$CEPH_ADM&quot; ] &amp;amp;&amp;amp; CEPH_ADM=$CEPH_BIN/ceph
        [ -z &quot;$INIT_CEPH&quot; ] &amp;amp;&amp;amp; INIT_CEPH=$CEPH_BIN/init-ceph
        [ -z &quot;$CEPH_LIB&quot; ] &amp;amp;&amp;amp; CEPH_LIB=$CEPH_BUILD_DIR/lib
        [ -z &quot;$OBJCLASS_PATH&quot; ] &amp;amp;&amp;amp; OBJCLASS_PATH=$CEPH_LIB
        [ -z &quot;$EC_PATH&quot; ] &amp;amp;&amp;amp; EC_PATH=$CEPH_LIB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，Ceph就能够知道二进制可执行文件的位置以及所需要的动静态库位置。&lt;/p&gt;
</content:encoded></item><item><title>Raft: An Understandable Consensus Algorithm</title><link>https://chenmiaoi.github.io/2024/09/26/Raft-An-Understandable-Consensus-Algorithm/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/09/26/Raft-An-Understandable-Consensus-Algorithm/</guid><pubDate>Thu, 26 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Raft是一个用于管理$replicated log$的算法，其通过$electing a leader$，并赋予该$leader$管理$replicated log$的完全权限来实现一致性。该$leader$会从客户端接收$log entries$，并将其复制到其他服务器上同时告知服务器该$log entries$何时能够安全地被它们的$state machine$所使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Raft通过$electing a leader$这样的方式来简化管理$replicated log$。$leader$可以在不咨询其他服务器的情况下决定日志中放置$new log entry$的位置，并且数据以一种简单的方式从$leader$流向其他服务器。&lt;br /&gt;
一个$leader$可以失败或者与其他服务器断开连接，在这种情况下，一个新的$leader$将会被$electing$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过$leader$这样的方式，$Raft$将一致性问题分解为了三个相对独立的子问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Leader Election$
&lt;ul&gt;
&lt;li&gt;当一个现存的$leader$失败时，一个新的$leader$必须被$electing$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$Log Replicated$
&lt;ul&gt;
&lt;li&gt;$leader$必须能够从客户端中接收$log entries$，并且将其复制到集群中，同时强制其他集群日志与$leader$保持一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$Safety$
&lt;ul&gt;
&lt;li&gt;$Raft$的关键$safety property$是下图所给出的状态机的$safety property$：如果任何服务器将特定的$log entry$应用到其状态机，那么其他服务器都不能对相同的日志索引应用不同的命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;a name=&quot;Figure3&quot;&amp;gt;&amp;lt;/a&amp;gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409261547094.png&quot; alt=&quot;Figure 3&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Raft Basics&lt;/h3&gt;
&lt;p&gt;一个$Raft$集群包含了数个服务器，通常而言，我们用五个服务器作为一个集群，这就使得该集群系统能够容忍两次错误。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是因为 Raft 使用多数投票原则来决定集群的$leader$和状态更新。在 5 台服务器的集群中，即使 2 台服务器失效，剩下的 3 台服务器依然能够形成多数（3 台服务器中的多数为2），继续提供服务。这使得系统具有一定的容错能力，即可以在部分服务器故障的情况下，保证数据一致性和可用性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;在任给的时间内，每一个服务器都只能有三个状态中的其中一个状态：$leader$，$follower$或者$candidate$&lt;/strong&gt;。在一个正常的操作中，&lt;strong&gt;系统中恰好只有一个$leader$，而其他服务器都是$follower$。$follower$是被动的，因此他们不会主动发出任何请求，只是响应来自$leader$和$candidate$的请求&lt;/strong&gt;。$leader$处理所有客户端的请求(如果客户端与$follower$相连，$follower$会将其重定向到$leader$)。第三种状态是$candidate$，用于选举出新的$leader$，这将会在下一个小结描述，现在给出其状态和转换图例：&lt;/p&gt;
&lt;p&gt;&amp;lt;a name=&quot;Figure4&quot;&amp;gt;&amp;lt;/a&amp;gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409261710112.png&quot; alt=&quot;Figure 4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;$Raft$将时间划分为任意长度的$Terms$，如下图所示。$Terms$被一个连续的整数所编号。&lt;strong&gt;每一个$Terms$都以一个$election$开始，该$election$中，一个或多个$candidate$尝试成为$leader$&lt;/strong&gt;。&lt;strong&gt;如果一个$candidate$在$election$中胜出，那么他会在$Term$的剩余时间内胜任&lt;/strong&gt;$leader$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在某些情况下，$election$会导致投票(vote)出现分歧。一旦处于这种情况，该$Term$内将以$no leader$的情况结束；新的$Term$(带有新的$election$)将会很快开始&lt;/strong&gt;。$Raft$&lt;strong&gt;确保在给定$Term$内最多只有一个&lt;/strong&gt;$leader$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$election$可能出现投票分裂：这就意味着多个$candidate$得到了相同数量的选票&lt;br /&gt;
$Raft$确保在每个$Term$内，最多只有一个$leader$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同服务器之间可能在不同时间观察到$Term$之间的转换，在某些情况下，某个服务器可能无法观察到某次$election$，甚至错过整个$Term$。$Term$在$Raft$中充当一个$logic\ clock$，这使得服务器能够检测过时的信息，例如失效的$leader$。每个服务器都会存储一个$current\ term\ number$，并且这个$number$随着时间单调递增。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这里，$logic\ clock$指的是一种用于排序事件发生顺序的机制。在 $Raft$ 中，$Term\ number$充当$logic\ clock$，帮助服务器跟踪系统状态并识别过时信息。通过增加的$Term\ number$，服务器能够判断哪些信息是最新的，从而避免与过时的领导者或数据进行交互。这种机制确保系统的一致性和稳定性，尽管服务器之间的观察顺序可能不同&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;$current\ term\ number$在服务器通信时会被交换；如果一个服务器的$current\ term\ number$比另一个服务器的小，他会将自己的$term\ number$更新为较大的值。如果一个$candidate$或$leader$发现自己的$Term$过期，则其会立即退回到$follower\ state$。如果一个服务器收到了带有过期的$term\ number$的请求，则会拒绝该请求&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$Raft$服务器之间通过$RPCs$协议进行通信，对于最为基础的$Raft$一致性算法只要求了两种类型的$RPCs$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$RequestVote\ RPCs$由$candidate$在$election$期间发起&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$AppendEntries\ RPCs$由$leader$发起的，用于复制$log\ entries$，并提供一种$heartbeat$机制&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;$heartbeat$机制是分布式系统中的一种通信方式，&lt;strong&gt;用于维护节点之间的活跃状态和协调&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果服务器没有及时收到响应，它们会重试$RPCs$，并且会并行地发出$RPCs$以获得最佳性能。&lt;/p&gt;
&lt;h3&gt;Leader election&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;$Raft$使用$heartbeat$机制来触发$election$。当服务器启动时，他们会作为$follower$开始运行。只要服务器从&lt;code&gt;leader&lt;/code&gt;或&lt;code&gt;candidate&lt;/code&gt;收到有效的$RPCs$，他就会保持在$follower\ state$。$leader$会定期向所有跟随者发送$heartbeat$(也就是不包含$log\ entries$的$AppendEntries\ RPCs$)以维持其领导权&lt;/strong&gt;。&lt;strong&gt;如果一个$follower$在一段时间内没有收到任何通信，他将假设当前没有有效的$leader$，并开始一次$election$以选出新的&lt;/strong&gt;$leader$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里就给出了$heartbeat$机制的细节：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;保持$leader\ state$：$leader$定期向$follower$发送$heartbeat$(即不带$log\ entry$的$AppendEntries\ RPCs$），以表明自己仍然是有效的$leader$。&lt;br /&gt;
检测失效：如果$follower$在一定时间内没有收到来自$leader$的$heartbeat$，它们可能会认为$leader$已经失效，并触发新的$election$。&lt;br /&gt;
更新$Term$：$heartbeat$中包含$current\ term\ number$，这有助于$follower$更新其状态，确保它们不会与过时的$leader$进行交互。&lt;br /&gt;
维护一致性：通过定期的$heartbeat$，系统能够及时检测并处理节点之间的状态变化，维护系统的一致性和可用性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;开始$election$时，$follower$&lt;strong&gt;会增加他自身的$current\ term\ number$并且转变为$candidate\ state$。然后会为自己投票，并且并行地发送$RequestVote\ RPCs$给集群中的每一个服务器&lt;/strong&gt;。$candidate$状态会一直持续，直到发生以下事件中的其一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;他在$election$中胜出&lt;/li&gt;
&lt;li&gt;另一个服务器胜出并成为$leader$&lt;/li&gt;
&lt;li&gt;在一段时间内没有任何胜出者&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果一个$candidate$在相同的$Term$内收到了在整个集群中的大部分服务器的$vote$，那么就认为该$candidate\ wins\ an\ election$。每一个服务器至多可以投票给一个$candidate$，根据先来后到原则&lt;/strong&gt;。$majority\ rule$确保了在一个特定$Term$内，至多只有一个$candidate$在这次$election$中获胜。一旦一个$candidate$赢得这次$election$，他就会立即变为$leader$，&lt;strong&gt;并且发送$heartbeat$信息给其他所有服务器以建立其权力和阻止新的&lt;/strong&gt;$election$。&lt;/p&gt;
&lt;p&gt;在等待投票期间，$candidate$&lt;strong&gt;可能会收到来自另一个服务器的$AppendEntries\ RPCs$，该服务器声称自己是$leader$。如果该$leader$的$Term$(包含在$AppendEntries\ RPCs$中)至少与$candidate$的$current\ term\ number$一样大，那么$candidate$就会认可该$leader$的合法性并回到$follower$状态。如果小于$candiate$的$current\ term\ number$，则$candidate$会拒绝该$RPCs$并继续保持在$candidate$状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;第三种可能的结果是$candidate$既没有赢得也没有输掉$election$：如果有许多$follower$同时成为$candidate$，选票可能会分散，导致没有$candidate$获得多数票。当这种情况发生时，每个$candidate$都会超时，并通过增加其$Term$并发起新一轮的 $RequestVote\ RPCs$ 来开始新一轮$election$。然而，如果没有额外的措施，选票分裂的情况可能会无限期地重复下去。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$Raft$使用$randomized\ election\ timeouts$(随机选举超时)来确保投票分裂很少发生并且其会在短时间内结束&lt;/strong&gt;。为了防止选票分裂，$election$&lt;strong&gt;超时时间试从一个固定区间中随机选取的&lt;/strong&gt;($eg. 150 ~ 300ms$)。这使得服务器的超时时间分散开来，从而在大多数情况下只有一个服务器会超时；它赢得$election$并在其他服务器超时之前发送$heartbeat$。相同的机制也用于处理选票分裂。每个候选人在$election$开始时都会重新启动其随机化的选举超时时间，并等待该超时结束后再开始下一次$election$；这减少了在新一轮$election$中再次发生选票分裂的可能性。&lt;/p&gt;
&lt;h3&gt;Log Replication&lt;/h3&gt;
&lt;p&gt;一旦一个$leader$被选择出来，他就会开始处理客户端请求。每一个客户端请求都包含一个会由$RSM$($replicated\ state\ machine$)执行的命令。$leader$&lt;strong&gt;会追加这些命令作为一个新的$entry$到自身的$log$中，然后并行地调用$AppendEntries\ RPCs$以复制该$entry$到其他服务器上&lt;/strong&gt;。当$entry$被安全复制后，$leader$将该$entry$应用到其状态机，并将执行结果返回给客户端。如果$follower$崩溃、运行缓慢或网络数据包丢失，$leader$会无限期地重试$AppendEntries\ RPCs$，直到所有$followers$最终存储了所有的$log\ entries$。&lt;/p&gt;
&lt;p&gt;$log$的组织方式如下图所示，每个$log\ entry$存储一个状态机命令以及该条目被$leader$接收时的$term\ number$。$log\ entry$中的$term\ number$用于检测$log$之间的不一致性，并确保一些特性得到保障。每一个$log\ entry$也会有一个整数索引来标识在$log$中的当前位置信息。&lt;/p&gt;
&lt;p&gt;&amp;lt;a name=&quot;Figure7&quot;&amp;gt;&amp;lt;/a&amp;gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409271118027.png&quot; alt=&quot;Figure 7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$leader$决定何时将$log\ entry$安全地应用到状态集中，这样已经被应用的$log\ entry$被称为$committed$&lt;/strong&gt;。$Raft$保证$committed$是持久的，并且最终会在所有可用的状态机上执行。&lt;strong&gt;当创建该$log\ entry$的$leader$将其复制到大多数服务器上时，该$log\ entry$就被认为是$committed$（如上图所示的$entry\ 7$）&lt;/strong&gt;。这也会提交$leader&apos;s\ log$中所有先前的$entries$，包括由之前的$leader$创建的$entries$。下一节会讨论在$leader$变更后应用此规则的一些细微之处，并且表明这种$commit$的定义是安全的。$leader$跟踪其已知的$committed$的$highest\ index$(最高索引)，并在未来的$AppendEntries\ RPCs$(包含$heartbeat$)中包含该索引，以便其他服务器最终得知。一旦$follower$得知$log\ entry$已被$committed$，他将按日志顺序将该条目应用到本地状态机中。&lt;/p&gt;
&lt;p&gt;这种机制不仅简化了系统的行为，并且使其更容易预测，而且是确保安全性的重要组件。&lt;code&gt;Raft&lt;/code&gt;维护了以下属性，这些属性共同构成了$Log\ Matching\ Property$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果在不同$log$的两条$entry$具有相同的$index$和$term$，那么它们存储相同的$command$&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果在不同$log$的两条$entry$具有相同的$index$和$term$，那么它们所有前面的$entries$都是相同的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第一个属性源于$leader$在给定任期内至多只能创建一个具有特定$log\ index$的$entry$，并且$log\ entries$在$log$中的位置永远不会发生变化。第二个属性通过$AppendEntries\ RPCs$执行的简单一致性检查得以保障&lt;/strong&gt;。&lt;strong&gt;在发送$AppendEntries\ RPCs$时，$leader$包括其$log$中紧接着新$entry$之前的$entry$的$index$和$Term$。如果$follower$在其$log$中未找到具有相同$index$和$Term$的$entry$，则会拒绝新的&lt;/strong&gt;$entry$。这个一致性检查充当了归纳步骤：$log$&lt;strong&gt;的初始空状态满足$Log\ Matching\ Property$，并且一致性检查在扩展$log$时保留&lt;/strong&gt;$Log\ Matching\ Property$。因此，每当$AppendEntries\ RPCs$返回成功时，$leader$就知道$follower$的$log$与他自己的$log$在新的$entry$之前时相同的。&lt;/p&gt;
&lt;p&gt;在正常运行期间，$leader$和$follower$的日志保持一致性，所以$AppendEntries\ RPCs$的一致性检查不会失败。然而，$leader$&lt;strong&gt;故障会使得$log$不一致(之前的$leader$可能没有完全复制其$log$中的全部$entries$)&lt;/strong&gt;。这些不一致性可以在一些列的$leader$和$follower$故障中累积。&lt;a href=&quot;#Figure7&quot;&gt;这张图片&lt;/a&gt;说明了$follower$与新的$leader$上的$log$的差异。&lt;strong&gt;$follower$可能缺少$leader$中存在的$entries$，或者可能拥有$leader$所不存在的额外的$entries$，或者两者都有&lt;/strong&gt;。$log$中的缺失和额外$entry$可能跨越多个$Term$。&lt;/p&gt;
&lt;p&gt;在$Raft$中，$leader$&lt;strong&gt;通过强制$follower&apos;s\ log$复制自身的$log$来处理不一致性。这就意味着在$follower$中发生冲突的$entry$会被来自于$leader$中的$entry$所覆盖&lt;/strong&gt;。在下一个小节中会讲述增加一些限制会使得其确保是安全的。&lt;/p&gt;
&lt;p&gt;为了使$follower&apos;s\ log$与自己的$log$保持一致，$leader$&lt;strong&gt;必须找到两个日志一致的最新的$log\ entry$，删除$follower$的$log$中该节点之后的任何$entries$，并将该点之后的所有$leader$的$entries$发送给&lt;/strong&gt;$follower$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要意识到，如果$follower$中在相同$term$和$index$下有$commit$与$leader$中的不一致，这就违反了$Log Matching Property$属性，就说明$follower$在这之后是有问题的，因此删掉该点之后的所有$entry$，复制下$leader$的后续$entry$以确保一致性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所有这些操作都是响应$AppendEntries\ RPCs$执行的一致性检查。$leader$为每个$follower$维护一个$nextIndex$，它是$leader$将发送给该$follower$的下一条$log\ entry$的$index$。当$leader$首次上任时，他将所有$nextIndex$的值初始化为其$log$中的最后一个$entry$的$index$之后的$index$(如下图中的11)。如果$follower$的$log$与$leader$的不一致，那么在下一个$AppendEntries\ RPCs$中，一致性检查将会失败。在拒绝接收之后，$leader$会将$nextIndex$减少并重传$AppendEntries\ RPCs$。最终$nextIndex$将到达一个点，在该点上$leader$和$follower$的$log$匹配。当这种情况发生时，$AppendEntries\ RPCs$将成功，这将删除$follower&apos;s\ log$中的任何冲突$entries$，并将$leader&apos;s\ log$中的$entries$追加到$follower$中(如果有的话)。一旦$AppendEntries\ RPCs$成功，$follower&apos;s\ log$将会与$leader$的一致，并在剩余的$Term$内保持一致。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果有需要，可以优化协议来减少被拒绝的$AppendEntries\ RPCs$数量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了这种机制，$leader$上任时不需要采取任何特殊行为来恢复$log$的一致性。他只需要开始正常操作，$log$会自动根据$AppendEntries\ RPCs$一致性检查的失败而趋于一致。$leader$&lt;strong&gt;永远不会覆盖或删除自己的&lt;/strong&gt;$log&apos;s entries$(比如&lt;a href=&quot;#Figure3&quot;&gt;这张图中&lt;/a&gt;的$Leader\ Append-Only\ Property$)&lt;/p&gt;
&lt;h3&gt;Safety&lt;/h3&gt;
&lt;p&gt;上一个小节中描述了$Raft$如何选举出$leader$和复制$log\ entries$。&lt;strong&gt;然而，以目前描述的机制还不足以确保让每一个状态机在同一个顺序中运行相同的命令&lt;/strong&gt;。例如，一个 $follower$ 可能在 $leader$ 提交多个$log\ entries$ 时不可用，然后它可能被选为 $leader$，并用新的 $entries$ 覆盖这些旧的$entries$；结果，可能会导致不同的状态机执行不同的命令序列。&lt;/p&gt;
&lt;p&gt;这个小节就会通过增加一个限制来完善$Raft$算法，该限制能够表明&lt;strong&gt;哪些服务器可以被选为&lt;/strong&gt;$leader$。这个限制确保了&lt;strong&gt;在任何给定$Term$内的$leader$包含所有在上一个$Term$中的&lt;/strong&gt;$commit$(如&lt;a href=&quot;#Figure3&quot;&gt;这张图&lt;/a&gt;中的$Leader\ Completeness\ Property$)。&lt;/p&gt;
&lt;h4&gt;Election Restriction&lt;/h4&gt;
&lt;p&gt;在任何$leader-base$的共识算法中，$leader$最终必须存储所有已提交的$log\ entries$。$Raft$采用了一种更为简单的方式，&lt;strong&gt;它确保每一个新$leader$从选举时起就包含上一个$Term$的所有已提交的$entries$，而无需将这些$entries$传输给$leader$。这意味着$log\ entries$只朝一个方向流动($leaders \rightarrow follower$)，并且$leaders$永远不会覆盖$log$中现有的&lt;/strong&gt;$entries$。&lt;/p&gt;
&lt;p&gt;$Raft$试用投票过程来防止$candidate$赢得选举，除非他的$log$包含所有的$committed$。$candidate$必须联系集群中的大多数节点才能被$elected$，这就意味着每个$committed$必须至少存在于其中一个服务器上。如果$candidate$的$log$至少与多数中的任何其他$log$一样$up-to-date$(这将在稍后介绍)，那么该$candidate$将包含所有的$committed\ entries$。$RequestVote RPCs$实现了这一限制：&lt;strong&gt;该$RPCs$包含了关于$candidate&apos;s\ log$的信息，如果投票者自己的$log$比$candidate$的更新，则拒绝投票&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$Raft$通过比较$log$中最后$entry$的$index$和$Term$来确定两个$log$哪一个更$up-to-date$。&lt;br /&gt;
&lt;strong&gt;如果$log$的最后的$entry$具有不同的$Term$，则$Term$较大(较晚)的$log$为&lt;/strong&gt;$up-to-date$。&lt;br /&gt;
&lt;strong&gt;如果$log$的最后的$entry$具有相同的$Term$，则$index$较大的(较长)$log$为&lt;/strong&gt;$up-to-date$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Committing entries from previous terms&lt;/h4&gt;
&lt;p&gt;就如之前所描述的一样，$leader$&lt;strong&gt;知道一旦在大多数服务器上都存储了来自$current\ term$的$entry$，那么这条$entry$就是$committed$的&lt;/strong&gt;。如果$leader$在提交$entry$之前就崩溃了，那么下一个$leader$就会尝试完成对这条$entry$的复制。然而，&lt;strong&gt;现在的$leader$不能立即认为：一旦来自上一个$Term$的$entry$在大多数服务器上存储，那么该$entry$就是$committed$的&lt;/strong&gt;。下图就解释了这一种情况，其中一个旧的$log\ entry$存储在大多数服务器上，但仍然可能被下一个$leader$覆盖。&lt;/p&gt;
&lt;p&gt;&amp;lt;a name=&quot;Figure8&quot;&amp;gt;&amp;lt;/a&amp;gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409271744503.png&quot; alt=&quot;Figure 8&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中展示了这样一个问题：如果$S1$在(c)情况发生了崩溃，尽管此时$[2]$已经被复制到大多数服务器上，但其仍不是$committed$的；(在(d)这种情况，$S5$被选为$leader$，此时$S5$会复制$[3]$给其他服务器，导致$[2]$丢失；如果$S1$在(e)这种情况下，$S1$在崩溃前在$Term3$时复制了一条$entry$，并且该$entry$被$committed$，即使$S1$崩溃，那么这时$S5$就不再会被选为$leader$)。&lt;/p&gt;
&lt;p&gt;为了消除像&lt;a href=&quot;#Figure8&quot;&gt;上图&lt;/a&gt;中的问题，$Raft$&lt;strong&gt;不通过计数复制的$entry$来提交来自上一个$Term$的$log\ entry$。只有来自$leader$的$current\ term\ number$的$log\ entry$才能够通过计数复制的$entry$被$committed$；一旦以这种方式提交了$current\ term$的$entry$，所有之前的$entry$就会因为$Log Matching Property$而间接提交($Log Matching Property$要求了&lt;em&gt;如果在不同$log$的两条$entry$具有相同的$index$和$term$，那么它们所有前面的$entries$都是相同的&lt;/em&gt;)。在某些情况下，$leader$就可以安全地得出结论：认为一个旧的$log\ entry$是$committed$的，但$Raft$采取了更为保守的方法以简化实现&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$Raft$在提交规则中增加了这一额外的复杂性，&lt;strong&gt;因为$log\ entry$在$leader$复制来自上一个$Term$的$entry$时保留了其原始的&lt;/strong&gt;$Term\ number$。在其他共识算法中，如果新的$leader$重新复制来自上一个$Term$的$entry$，则必须使用其新的$Term\ number$。此外，$Raft$中的新的$leader$发送的来自上一个$Term$的$log\ entry$比其他算法少。&lt;/p&gt;
&lt;h4&gt;Safety Argument&lt;/h4&gt;
&lt;p&gt;鉴于$Raft$算法的完善，我们现在可以更精确地论证&lt;a href=&quot;#Figure3&quot;&gt;$Leader\ Completeness\ Property$&lt;/a&gt;的成立。我们先假设该属性没有成立，然后我们逐步证明这是矛盾的。&lt;em&gt;假设一个$Term\ T$的$leader$(记作$leader_T$)在他的$Term$内提交了$log\ entry$，但是该$log\ entry$没有被之后某个$Term$的$leader$所存储。考虑最小的$Term\ U \gt T$，其中$leader_U$没有存储该&lt;/em&gt;$log\ entry$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$Leader\ Completeness\ Property$
&lt;strong&gt;如果在给定$Term$中提交了一条$log\ entry$，那么该$entry$将会出现在所有$higher-number\ Term$的$leader$的$log$中&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409291025322.png&quot; alt=&quot;Figure 9&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$leader_T$提交的$entry$在$leader_U$当选时必须缺失($leader$不会删除或覆盖$entry$)&lt;/li&gt;
&lt;li&gt;$leader_T$在集群的多数节点上复制了该$entry$，而$leader_U$也从集群的多数节点获得了投票。因此，至少有一个服务器既接收了来自$leader_T$的$entry$，又投票给了$leader_U$，如上图所示。&lt;em&gt;该投票者是出现矛盾的关键&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;该投票者在投票给$leader_U$之前必须接受来自于$leader_T$的$committed\ entry$；否则他会拒绝来自$leader_T$的$AppendEntries\ RPCs$请求(因为他的$current\ term$比$leader_T$更高)&lt;/li&gt;
&lt;li&gt;该投票者在投票给$leader_U$时仍然存储该$entry$，因为假设每个中间$leader$都包含了该$entry$，需要注意$leader$&lt;em&gt;永远不会删除$entry$，而$follower$仅在$entry$与$leader$冲突时才会被删除&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;该投票者投票给$Leader_U$的前提之一是：$leader_U$的$log$必须与投票者的一样最新，这样就导致了一个矛盾。&lt;/li&gt;
&lt;li&gt;首先，如果该投票者和$leader_U$共享相同的$last\ log\ term$，则$leader_U$的$log$必须至少与投票者的$log$一样长，因此其$log$包含了投票者$log$中的每一条$entry$。这是矛盾的，因为我们假设&lt;em&gt;投票者包含了$committed\ entry$，而$leader_U$不包含&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;否则，$leader_U$的$last\ log\ term$必须大于投票者的。此外，它还必须大于$T$，因为投票者的$last\ log\ term$至少为$T$(因为其包含了来自于$term\ T$的$committed\ entry$)。根据假设，创建$leader_U$的$last\ log\ entry$的之前的$leader$必须在其$log$中包含$committed\ entry$。然后根据$Log\ Matching\ Property$，$leader_U$的$log$也必须包含该$committed\ entry$，者又是一个矛盾。&lt;/li&gt;
&lt;li&gt;因此，所有大于$Term\ T$的$leader$必须包含所有在$Term\ T$内的$committed\ entry$&lt;/li&gt;
&lt;li&gt;而$Log\ Matching\ Property$确保未来的$leader$也将包含间接的$committed\ entry$&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Follower and Candidate crashes&lt;/h3&gt;
&lt;p&gt;到目前为止，我们专注于$leader$发生了故障的问题。$follower$和$candidate$的崩溃比$leader$的崩溃要简单得多，而且两者的处理方式相同。如果$follower$或$candidate$崩溃，那么将发送给他的$RequestVote$和$AppendEntries RPCs$失败。$Raft$通过无限重试来处理这些故障；如果崩溃的服务器重新启动，那么$RPCs$将成功完成。如果一个服务器在完成$RPCs$后但在响应之前崩溃，那么他在重启之后会再次收到相同的$RPC$。$Raft\ RPCs$是幂等($idempotent$)的，因此这不会造成任何损害。例如：如果一个$follower$收到一个包含已在其$log$中的$log\ entry$的$AppendEntries\ RPCs$请求，它会忽略掉新请求中的$entries$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Idempotent
幂等是指在多次执行相同操作时，结果保持不变的特性。在网络请求中，如果一个请求是幂等的，无论它被执行多少次，最终状态都是一致的，不会造成额外的影响。这样可以确保系统在出现故障时的可靠性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Timing and Availability&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;对于$Raft$而言，其中一个要求便是其安全性不能依赖于时序：系统不能因为某些事情发生得比预期快或慢而产生不正确的结果&lt;/strong&gt;。然而，可用性($availability$，系统及时响应客户端的能力)不可避免地依赖于时序。例如，如果消息交换所需的时间超过服务器崩溃之间的典型时间，$candidate$将无法持续足够长的时间以赢得$election$；没有稳定的$leader$，$Raft$将无法继续。&lt;/p&gt;
&lt;p&gt;$leader election$是$Raft$中时序最为关键的方面。只要系统满足以下时序要求，$Raft$就能$election$并维持一个稳定的$leader$:&lt;/p&gt;
&lt;p&gt;$$
broadcastTime \ll electionTimeout \ll MTBF
$$&lt;/p&gt;
&lt;p&gt;在上面的不等式中，$broadcastTime$是服务器并行向集群中的每个服务器发送$RPCs$并接收响应的平均时间；$electionTimeout$是在&lt;a href=&quot;#leader-election&quot;&gt;Leader Election&lt;/a&gt;小节中所描述的$election$超时；$MTBF$是单个服务器的平均故障间隔时间。$broadcastTime$&lt;strong&gt;应比$electionTimeout$小几个数量级，以便$leader$可以可靠地发送$heartbeat$，从而防止$follower$启动$election$；考虑到随机化的选举超时方法，这个不等式也使得分裂投票变得不太可能。$electionTimeout$应比 $MTBF$ 小几个数量级，以确保系统能够持续稳步推进&lt;/strong&gt;。当$leader$崩溃时，系统将在大约$electionTimeout$内不可用；我们希望这仅占总时间的一小部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$broadcastTime$和 $MTBF$ 是底层系统的属性，而 $electionTimeout$ 是我们必须选择的参数&lt;/strong&gt;。$Raft$ 的 $RPCs$ 通常要求接收方将信息持久化到稳定存储中，因此广播时间可能在 $0.5 ~ 20ms$之间，具体取决于存储技术。因此，$electionTimeout$ 可能在 $10 ~ 500ms$之间。典型的服务器 $MTBF$ 通常为几个月或更长，这很容易满足时序要求。&lt;/p&gt;
&lt;h2&gt;Appendix A&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202409261551936.png&quot; alt=&quot;Raft&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选举过程
&lt;ul&gt;
&lt;li&gt;假设一开始都是$follower$状态，其都有一个$electionTimeout$，当任一$follower$到达$electionTimeout$时，其会立即转变为$candidate$状态，&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>LevelDB: A whole Open Operation</title><link>https://chenmiaoi.github.io/2024/09/16/LevelDB-A-whole-Open-Operation/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/09/16/LevelDB-A-whole-Open-Operation/</guid><pubDate>Mon, 16 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;我在最近接触了分布式存储计算这一个方向，因此想从leveldb作为一个最初的学习项目；起初，我是通过&lt;a href=&quot;https://hardcore.feishu.cn/mindnotes/bmncnzpUmXNQruVGOwRwisHyxoh&quot;&gt;硬核课堂 LevelDB源码分析&lt;/a&gt;这一飞书文档学习，但是这个文档有些学习顺序我个人不是很习惯，因此打算分模块去将整个LevelDB的整个源码细节&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;首先第一个模块，便是&lt;code&gt;DB::Open&lt;/code&gt;这一操作&lt;/em&gt;。我们需要分析&lt;code&gt;Open&lt;/code&gt;的操作，就首先需要从一个宏观的角度分析&lt;code&gt;Open&lt;/code&gt;会做出什么操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DB::Open&lt;/code&gt;是用于打开一个数据库的核心函数。它的主要任务是&lt;code&gt;初始化数据库&lt;/code&gt;、&lt;code&gt;加载数据库元数据&lt;/code&gt;、恢复/修复数据库数据&lt;code&gt;并&lt;/code&gt;确保数据库处于可用状态`&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们首先从宏观角度来观察&lt;code&gt;DB::Open&lt;/code&gt;的实现具体做了些什么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步：初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Status DB::Open(
    const Options&amp;amp; options, 
    const std::string&amp;amp; dbname, 
    DB** dbptr) {
    *dbptr = nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Open&lt;/code&gt;函数会在一开始就将想要返回的&lt;code&gt;db&lt;/code&gt;句柄设置为&lt;code&gt;nullptr&lt;/code&gt;这是为了确保&lt;strong&gt;函数执行过程中，一旦出现任何问题，&lt;code&gt;dbptr&lt;/code&gt;就会保持为&lt;code&gt;nullptr&lt;/code&gt;，表示数据库打开失败&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二步：创建&lt;code&gt;DBImpl&lt;/code&gt;数据库实例&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;DBImpl* impl = new DBImpl(options, dbname);
impl-&amp;gt;mutex_.Lock();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DBImpl&lt;/code&gt;是&lt;code&gt;DB&lt;/code&gt;的内部接口，&lt;code&gt;DBImpl&lt;/code&gt;是实际上的操作接口，这一点会在后面详细介绍。然后我们需要保证数据库的操作在多线程环境下是安全的，因此对&lt;code&gt;DBImpl::mutex_&lt;/code&gt;进行上锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第三步：恢复操作的初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;VersionEdit edit;
bool save_manifest = false;
Status s = impl-&amp;gt;Recover(&amp;amp;edit, &amp;amp;save_manifest);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;LevelDB&lt;/code&gt;中，&lt;code&gt;VersionEdit&lt;/code&gt;的&lt;strong&gt;主要作用是记录数据库版本的更改。具体来说，它用于描述数据库的元数据变更，如文件的添加、删除以及日志编号的更新。这些更改会被写入 &lt;code&gt;MANIFEST&lt;/code&gt; 文件中，以确保数据库的状态在系统重启或恢复时能够一致且可靠地重建&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;save_manifest&lt;/code&gt;用于标识是否需要更新 &lt;code&gt;MANIFEST&lt;/code&gt; 文件，而&lt;code&gt;Recover&lt;/code&gt;函数负责处理数据库的恢复，&lt;strong&gt;确保数据库处于一致的状态&lt;/strong&gt;，并根据选项&lt;code&gt;create_if_missing&lt;/code&gt;和&lt;code&gt;error_if_exists&lt;/code&gt;来决定是否创建或报告错误。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第四步：创建MemTable和对应的日志文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (s.ok() &amp;amp;&amp;amp; impl-&amp;gt;mem_ == nullptr) {
  uint64_t new_log_number = impl-&amp;gt;versions_-&amp;gt;NewFileNumber();
  WritableFile* lfile;
  s = options.env-&amp;gt;NewWritableFile(LogFileName(dbname, new_log_number), &amp;amp;lfile);
  if (s.ok()) {
    edit.SetLogNumber(new_log_number);
    impl-&amp;gt;logfile_ = lfile;
    impl-&amp;gt;logfile_number_ = new_log_number;
    impl-&amp;gt;log_ = new log::Writer(lfile);
    impl-&amp;gt;mem_ = new MemTable(impl-&amp;gt;internal_comparator_);
    impl-&amp;gt;mem_-&amp;gt;Ref();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;LevelDB&lt;/code&gt;中，&lt;code&gt;MemTable&lt;/code&gt;是一个核心的数据结构，&lt;strong&gt;它用于暂时存储写入操作的数据，并在合适的时机将其写入磁盘。&lt;code&gt;MemTable&lt;/code&gt;是一个内存中的&lt;code&gt;跳表(skip list)&lt;/code&gt;，支持高效的插入、查询、和删除操作&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当数据被写入 LevelDB 时，首先会写入到 MemTable 中，而不是直接写入磁盘。这是为了避免频繁的磁盘 I/O，提升写入性能&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入到 MemTable 的数据也会被同时记录在一个日志文件（WAL，Write-Ahead Log）中，以确保在崩溃时可以通过日志文件恢复未写入磁盘的数据&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果在第三步时的&lt;code&gt;Recover&lt;/code&gt;操作正常进行，并且此时需要打开的数据库中没有相应的&lt;code&gt;MemTable&lt;/code&gt;，那么就应该创建新的日志文件和一块新的&lt;code&gt;MemTable&lt;/code&gt;用于记录操作和存储内存中的数据，稍后通过&lt;code&gt;MemTable&lt;/code&gt;写入到磁盘中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第五步：保存&lt;code&gt;MANIFEST&lt;/code&gt;文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (s.ok() &amp;amp;&amp;amp; save_manifest) {
  edit.SetPrevLogNumber(0);
  edit.SetLogNumber(impl-&amp;gt;logfile_number_);
  s = impl-&amp;gt;versions_-&amp;gt;LogAndApply(&amp;amp;edit, &amp;amp;impl-&amp;gt;mutex_);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果恢复过程中需要保存 &lt;code&gt;MANIFEST&lt;/code&gt; 文件，函数会更新新的日志文件编号，并通过 &lt;code&gt;LogAndApply&lt;/code&gt; 应用这些更改。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第六步：清理与压缩&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (s.ok()) {
  impl-&amp;gt;RemoveObsoleteFiles();
  impl-&amp;gt;MaybeScheduleCompaction();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切仍然成功，函数将删除无用的文件（例如旧日志文件），并根据需要安排一次压缩操作。压缩操作可以减少数据库大小并提升性能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第七步：返回DB句柄&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;impl-&amp;gt;mutex_.Unlock();
if (s.ok()) {
  assert(impl-&amp;gt;mem_ != nullptr);
  *dbptr = impl;
} else {
  delete impl;
}
return s;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们从宏观角度的视角了解了&lt;code&gt;DB::Open&lt;/code&gt;的操作，以及其对应的作用，现在我们需要更细节的将整个&lt;code&gt;Open&lt;/code&gt;的操作进行解构。&lt;/p&gt;
&lt;h2&gt;Options&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Options&lt;/code&gt;主要&lt;strong&gt;用于配置和控制数据库的行为，包括读写性能、资源使用、压缩策略等方面。&lt;code&gt;Options&lt;/code&gt;类包含多种配置选项，允许用户在创建和使用数据库时根据具体需求进行定制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;LevelDB::Options&lt;/code&gt;而言，在整个数据库中有三种类型的&lt;code&gt;Options&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Options&lt;/code&gt;: 用于控制数据库级别的全局配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReadOptions&lt;/code&gt;: 用于控制数据库读取操作的行为&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WriteOptions&lt;/code&gt;: 用于控制数据库写入操作的行为&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Options&lt;/h3&gt;
&lt;p&gt;首先我们先给出&lt;code&gt;Options&lt;/code&gt;的详细定义，实际上&lt;code&gt;Options&lt;/code&gt;是一个开放式的结构体，其中内部的所有参数都是公开可修改的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Options to control the behavior of a database (passed to DB::Open)
struct LEVELDB_EXPORT Options {
  // Create an Options object with default values for all fields.
  Options();
  const Comparator* comparator;
  bool create_if_missing            = false;
  bool error_if_exists              = false;
  bool paranoid_checks              = false;
  Env* env;
  Logger* info_log                  = nullptr;
  size_t write_buffer_size          = 4 * 1024 * 1024;
  int max_open_files                = 1000;
  Cache* block_cache                = nullptr;
  size_t block_size                 = 4 * 1024;
  int block_restart_interval        = 16;
  size_t max_file_size              = 2 * 1024 * 1024;
  CompressionType compression       = kSnappyCompression;
  int zstd_compression_level        = 1;
  bool reuse_logs                   = false;
  const FilterPolicy* filter_policy = nullptr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;comparator&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于定义表中键的排序方式。&lt;/li&gt;
&lt;li&gt;默认值：按字节进行字典序排序&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;用户可以提供自定义的比较器，用于定义键的顺序。必须确保与以前打开数据库时使用的比较器一致，否则会出现排序不一致的问题，导致数据无法正确读取&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create_if_missing&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：如果数据库文件不存在，是否自动创建对应的数据库文件&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;当打开数据库时，如果该数据库不存在，设置为 true 时，LevelDB 会自动创建一个新数据库。如果设置为 false 并且数据库不存在，则返回错误&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error_if_exists&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：如果数据库文件已经存在，是否返回错误&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;当创建新数据库时，如果该数据库已经存在，设置为 true 可避免覆盖现有数据&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;paranoid_checks&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：是否开启严格的数据检查&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;开启后，数据库操作过程中会进行更严格的检查。如果发现数据损坏，操作会提前终止。适合在对数据完整性要求非常高的场景下使用&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;env&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用: 用于与环境交互，例如读取/写入文件、调度后台任务等。&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;Env::Default()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;限制：可自定义 env，例如用于模拟不同的操作环境或自定义文件系统行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;info_log&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于记录数据库内部的进度和错误信息&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;nullptr&lt;/code&gt;，表示日志信息会被写入数据库所在目录中的文件&lt;/li&gt;
&lt;li&gt;限制：如果用户希望将日志信息记录到指定位置，可以提供自定义的日志对象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write_buffer_size&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：在内存中构建数据的大小，达到该值后会将其转换为排序的磁盘文件&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;4MB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;增大此值可以提升写入性能，尤其是批量写入时。不过，较大的 write_buffer_size 也会导致在数据库打开时恢复时间更长&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_open_files&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：数据库可以打开的最大文件数&lt;/li&gt;
&lt;li&gt;默认值：1000&lt;/li&gt;
&lt;li&gt;限制：设置更大的值可以减少文件频繁打开和关闭的开销，适用于工作集较大的数据库。每个 SSTable 文件需要一个文件句柄，因此较大的工作集需要较多的文件打开数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_cache&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：为块提供缓存。如果为 &lt;code&gt;nullptr&lt;/code&gt;，则 LevelDB 会自动创建并使用一个 8MB 的内部缓存&lt;/li&gt;
&lt;li&gt;默认值: &lt;code&gt;nullptr&lt;/code&gt;(&lt;code&gt;8MB&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;限制：用户可以自定义块缓存对象，以优化读取性能，减少磁盘读取次数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_size&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：每个块中存放的用户数据的近似大小，块是从磁盘读取的基本单位&lt;/li&gt;
&lt;li&gt;默认值: &lt;code&gt;4KB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：较大的块可以减少元数据的存储量，但可能增加读取非必要数据的开销（读放大）。较小的块适合随机读取，较大的块则适合顺序读取&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block_restart_interval&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于键的增量编码，每隔多少个键进行一次重启点的记录&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;16&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;大多数情况下无需修改此参数。该值影响键的存储和读取的效率&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_file_size&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：写入到一个文件中的最大字节数，达到该值后会切换到新文件&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;2MB&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：在初次加载大量数据时，增大此值可以减少文件数，但会导致更长的压缩时间，从而影响性能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compression&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：指定使用的压缩算法，块可能会在存储到文件之前进行压缩&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;kSnappyCompression&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：Snappy 是轻量且快速的压缩算法，适合大多数场景。如果需要更高的压缩率，可以选择其他算法，但可能会牺牲一些性能。如果不希望压缩，可以选择 kNoCompression&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zstd_compression_level&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：为 &lt;code&gt;Zstandard&lt;/code&gt; 压缩算法指定压缩级别&lt;/li&gt;
&lt;li&gt;默认值：1&lt;/li&gt;
&lt;li&gt;限制：Zstandard 支持不同的压缩级别，较高的压缩级别提供更好的压缩率，但压缩和解压缩速度会变慢。该参数适用于使用 Zstandard 压缩时的调优&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reuse_logs&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：是否在打开数据库时重用现有的 &lt;code&gt;MANIFEST&lt;/code&gt; 和日志文件。&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：&lt;strong&gt;设置为 true 时，数据库在打开时会重用现有的日志文件，可能会显著加快打开速度，但这个特性是实验性的&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter_policy&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：使用指定的过滤策略来减少磁盘读取。很多应用将从 &lt;code&gt;NewBloomFilterPolicy()&lt;/code&gt; 的结果中受益&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;nullptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：可以使用布隆过滤器来减少不必要的磁盘读取，适用于大多数场景。布隆过滤器能有效判断某个键是否存在于 SSTable 中，从而避免读取不存在的键&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;ReadOptions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Options that control read operations
struct LEVELDB_EXPORT ReadOptions {
  bool verify_checksums     = false;
  bool fill_cache           = true;
  const Snapshot* snapshot  = nullptr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;verify_checksums&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：是否验证从底层存储读取的数据的校验和（checksum）&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：当设置为 true 时，每次读取数据时都会对其进行校验和验证，以确保数据在存储和传输过程中没有被损坏。启用此选项可以增强数据完整性保证，但会增加读取操作的开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fill_cache&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：读取的数据是否应缓存到内存中（块缓存）&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：当设置为 true 时，读取的数据会被放入块缓存中，后续读取相同的数据可以直接从内存中获取，从而提高读取速度。如果设置为 false，读取的数据不会被缓存，适合于不常访问或一次性读取的大数据块场景，例如批量扫描操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;snapshot&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：用于指定读取操作的快照。&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;nullptr&lt;/code&gt;(隐式使用读取操作开始时的数据库状态快照)&lt;/li&gt;
&lt;li&gt;限制: 快照是一种数据库在某一时刻的静态视图，允许在一致性要求较高的场景中进行读取操作。即使在读取期间数据库发生了写操作，读取的数据也依然是快照时刻的数据。如果不指定快照（snapshot = nullptr），则默认读取操作会使用该操作开始时数据库的当前状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;WriteOptions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Options that control write operations
struct LEVELDB_EXPORT WriteOptions {
  WriteOptions() = default;
  bool sync      = false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sync&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;作用：决定写操作在操作系统的缓冲区缓存被刷新到磁盘之前，是否需要等待&lt;/li&gt;
&lt;li&gt;默认值：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;限制：
&lt;ul&gt;
&lt;li&gt;sync = true：表示写操作完成后，必须将数据同步刷新到磁盘。这通过调用 fsync() 或类似的系统调用来确保数据已真正写入磁盘，而不是仅仅保存在操作系统的内存缓存中。这种方式会使写操作变得较慢，但提供了更强的持久性保证。如果机器或操作系统崩溃，数据不会丢失。&lt;/li&gt;
&lt;li&gt;sync = false：表示写操作不会立即同步到磁盘，数据会暂时保存在操作系统的缓存中。这会使写操作更快，但如果机器崩溃（例如断电或操作系统崩溃），缓存中的数据可能丢失。但如果只是进程崩溃（机器没有重启），数据依然可以从操作系统缓存中恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;DBImpl::DBImpl&lt;/h2&gt;
&lt;p&gt;这里只会局部的介绍&lt;code&gt;DBImpl&lt;/code&gt;，并不会对&lt;code&gt;DBImpl&lt;/code&gt;的所有实现进行分析，做到了解必要，忽略其他的干扰项。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DBImpl::DBImpl(const Options&amp;amp; raw_options, const std::string&amp;amp; dbname)
    : env_(raw_options.env),
      internal_comparator_(raw_options.comparator),
      internal_filter_policy_(raw_options.filter_policy),
      options_(SanitizeOptions(
        dbname, &amp;amp;internal_comparator_, &amp;amp;internal_filter_policy_, raw_options)),
      owns_info_log_(options_.info_log != raw_options.info_log),
      owns_cache_(options_.block_cache != raw_options.block_cache),
      dbname_(dbname),
      table_cache_(new TableCache(dbname_, options_, TableCacheSize(options_))),
      db_lock_(nullptr),
      shutting_down_(false),
      background_work_finished_signal_(&amp;amp;mutex_),
      mem_(nullptr),
      imm_(nullptr),
      has_imm_(false),
      logfile_(nullptr),
      logfile_number_(0),
      log_(nullptr),
      seed_(0),
      tmp_batch_(new WriteBatch),
      background_compaction_scheduled_(false),
      manual_compaction_(nullptr),
      versions_(
        new VersionSet(dbname_, &amp;amp;options_, table_cache_, &amp;amp;internal_comparator_)) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在构造上述中，对于从&lt;code&gt;Options&lt;/code&gt;中取出并直接赋值的字段不做其他解释，这里重点介绍其他字段的含义和作用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SanitizeOptions&lt;/code&gt;函数用于对用户提供的&lt;code&gt;raw_options&lt;/code&gt;进行合理性验证和修改，确保数据库操作不会因为不合理的参数导致问题。&lt;code&gt;SanitizeOptions&lt;/code&gt;函数根据默认值和限制，对选项中的字段进行调整。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Options SanitizeOptions(const std::string&amp;amp; dbname,
                        const InternalKeyComparator* icmp,
                        const InternalFilterPolicy* ipolicy,
                        const Options&amp;amp; src) {
  Options result = src;
  result.comparator = icmp;
  result.filter_policy = (src.filter_policy != nullptr) ? ipolicy : nullptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先&lt;code&gt;SanitizeOptions&lt;/code&gt;会创建一个&lt;code&gt;src options&lt;/code&gt;的副本&lt;code&gt;result&lt;/code&gt;用于验证和修改为合理的选项，并在最后返回。然后将将内部的比较器 &lt;code&gt;icmp&lt;/code&gt; 赋值给 &lt;code&gt;result.comparator&lt;/code&gt;，确保使用内部的比较器，而不是用户自定义的 comparator。&lt;/p&gt;
&lt;p&gt;如果用户在 src 中提供了 filter_policy，则将内部的 ipolicy 赋值给 result.filter_policy。否则，设为 nullptr。这保证了如果用户没有指定过滤策略，系统不会使用过滤器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;class T, class V&amp;gt;
static void ClipToRange(T* ptr, V minvalue, V maxvalue) {
  if (static_cast&amp;lt;V&amp;gt;(*ptr) &amp;gt; maxvalue) *ptr = maxvalue;
  if (static_cast&amp;lt;V&amp;gt;(*ptr) &amp;lt; minvalue) *ptr = minvalue;
}

ClipToRange(&amp;amp;result.max_open_files, 64 + kNumNonTableCacheFiles, 50000);
ClipToRange(&amp;amp;result.write_buffer_size, 64 &amp;lt;&amp;lt; 10, 1 &amp;lt;&amp;lt; 30);
ClipToRange(&amp;amp;result.max_file_size, 1 &amp;lt;&amp;lt; 20, 1 &amp;lt;&amp;lt; 30);
ClipToRange(&amp;amp;result.block_size, 1 &amp;lt;&amp;lt; 10, 4 &amp;lt;&amp;lt; 20);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数&lt;code&gt;ClipToRange&lt;/code&gt;确保某个指针指向的值 ptr 被限制在 $[minvalue, maxvalue]$ 范围内。如果 ptr 的值超出这个范围，函数将其裁剪到范围的边界值。因此，这里会对&lt;code&gt;max_open_files&lt;/code&gt;、&lt;code&gt;write_buffer_size&lt;/code&gt;、&lt;code&gt;max_file_size&lt;/code&gt;和&lt;code&gt;block_size&lt;/code&gt;选项的值进行判断是否位于合理的范围内，如果不在则进行裁剪为合适的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (result.info_log == nullptr) {
    // Open a log file in the same directory as the db
    src.env-&amp;gt;CreateDir(dbname);  // In case it does not exist
    src.env-&amp;gt;RenameFile(InfoLogFileName(dbname), OldInfoLogFileName(dbname));
    Status s = src.env-&amp;gt;NewLogger(InfoLogFileName(dbname), &amp;amp;result.info_log);
    if (!s.ok()) {
      // No place suitable for logging
      result.info_log = nullptr;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 info_log 为 nullptr（用户没有指定日志记录器），则在数据库路径中创建日志文件目录。如果目录不存在，则创建该目录。同时将将当前的日志文件重命名为 &quot;旧的日志文件&quot;。InfoLogFileName(dbname) 获取日志文件名，OldInfoLogFileName(dbname) 获取旧日志文件名。这样做可以避免覆盖现有日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 实际逻辑为：
# 如果目前有一个日志文件为`dbname/LOG`，那么首先先重命名该文件
mv dbname/LOG dbname/LOG.old
# 然后再将logger指向新的`dbname/LOG`，这样就不会覆盖原有的`dbname/LOG`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建新的日志记录器 Logger，并将其指向新的日志文件。NewLogger 函数负责创建并打开日志文件用于写入。如果创建日志文件失败（即 s.ok() 返回 false），将 info_log 置为 nullptr，表示无法创建日志记录器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (result.block_cache == nullptr) {
    result.block_cache = NewLRUCache(8 &amp;lt;&amp;lt; 20);
}
return result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用户没有提供缓存，则创建一个新的 &lt;code&gt;LRUCache&lt;/code&gt; 对象，大小为 $8MB(8 &amp;lt;&amp;lt; 20)$。&lt;code&gt;LRUCache&lt;/code&gt;用于缓存数据库的块，以减少磁盘读取，提高性能。&lt;/p&gt;
&lt;p&gt;最终返回验证完合理性的选项&lt;code&gt;result&lt;/code&gt;。以下是对&lt;code&gt;DBImpl:DBImpl&lt;/code&gt;初始化的成员含义做出解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;owns_info_log_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;owns_info_log_ 用于标记当前对象是否拥有 info_log_ 日志文件对象。如果 options_.info_log 与用户提供的 raw_options.info_log 不同，则表示数据库自己拥有并负责管理日志文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;owns_cache_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;owns_cache_ 用于标记数据库是否拥有 block_cache 块缓存。如果 options_.block_cache 和用户传入的 raw_options.block_cache 不同，则表示数据库拥有自己的块缓存并负责其管理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table_cache_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;table_cache_ 是用于缓存数据库表（即 SSTable）的缓存系统。它基于数据库路径 dbname_ 和选项 options_ 创建，内部管理 SSTable 文件的元数据信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db_lock_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;db_lock_ 用于文件锁定，防止多个数据库实例同时打开同一个数据库路径。初始化时为 nullptr，稍后会在打开数据库时进行文件锁定操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shutting_down_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;shutting_down_ 是一个标志，用于指示数据库是否正在关闭操作。当 true 时，表示后台任务和写操作需要终止。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background_work_finished_signal_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;background_work_finished_signal_ 是一个条件变量，用于在后台线程完成工作（如压缩）后，通知主线程或其他等待的线程。它与 mutex_ 关联，保证多线程同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mem_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;mem_ 是当前内存中的 MemTable，用于存储最近的写入操作，尚未持久化到磁盘。在数据库启动时为 nullptr，后续会分配&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;imm_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;imm_ 是不可变的 MemTable，即当一个 MemTable 达到大小限制后，它被标记为不可变，等待写入到磁盘。初始化时为 nullptr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;has_imm_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;has_imm_ 用于标记是否存在不可变的 MemTable。当为 true 时，表示 imm_ 需要被持久化到磁盘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logfile_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;logfile_ 是当前日志文件，用于记录写入操作（Write-Ahead Logging）。每个 MemTable 都有一个相对应的日志文件，初始化时为 nullptr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logfile_number_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;logfile_number_ 是日志文件的编号，用于标识当前的日志文件。初始化时为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;log_ 是日志写入器，用于将写操作序列化并写入日志文件。初始化时为 nullptr，稍后会创建。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seed_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;seed_ 是随机数生成的种子，主要用于某些随机化操作，例如压缩的延时处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tmp_batch_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;tmp_batch_ 是一个临时的批量写入对象，帮助合并多个写入操作，以提高效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background_compaction_scheduled_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;background_compaction_scheduled_ 是一个标志，指示是否已经安排了后台压缩任务。若为 true，表示压缩任务正在进行或已经计划好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manual_compaction_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;manual_compaction_ 用于手动触发的压缩任务。初始化时为 nullptr，表示当前没有手动压缩任务&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;versions_&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;versions_ 管理数据库的版本信息和元数据。VersionSet 负责维护 SSTable 文件的列表以及压缩操作的策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>LevelDB: A whole Put Operation</title><link>https://chenmiaoi.github.io/2024/09/16/LevelDB-A-whole-Put-Operation/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/09/16/LevelDB-A-whole-Put-Operation/</guid><pubDate>Mon, 16 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我在最近接触了分布式存储计算这一个方向，因此想从leveldb作为一个最初的学习项目；起初，我是通过&lt;a href=&quot;https://hardcore.feishu.cn/mindnotes/bmncnzpUmXNQruVGOwRwisHyxoh&quot;&gt;硬核课堂 LevelDB源码分析&lt;/a&gt;这一飞书文档学习，但是这个文档有些学习顺序我个人不是很习惯，因此打算分模块去将整个LevelDB的整个源码细节。&lt;/p&gt;
&lt;p&gt;首先第一个模块，便是&lt;code&gt;DB::Put&lt;/code&gt;这一操作。&lt;/p&gt;
</content:encoded></item><item><title>ArkTS: analyze riscv64 assember</title><link>https://chenmiaoi.github.io/2024/07/16/ArkTS-analyze-riscv64-assember/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/07/16/ArkTS-analyze-riscv64-assember/</guid><pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这一节中，我们会主要分析&lt;code&gt;ArkTS&lt;/code&gt;中的&lt;code&gt;RISC-V&lt;/code&gt;架构的汇编，这也是我们的主要目标之一。当前分析的版本位于&lt;a href=&quot;https://gitee.com/riscv-sig/arkcompiler_ets_runtime/tree/weekly_20230905/&quot;&gt;ArkTS Runtime&lt;/a&gt;[commit 2d8e197]。&lt;/p&gt;
&lt;h2&gt;RISC-V64 Detail&lt;/h2&gt;
&lt;p&gt;在开始分析实际代码前，我们需要了解一点&lt;code&gt;RISC-V&lt;/code&gt;的相关知识。&lt;code&gt;RISC-V&lt;/code&gt;是一种&lt;code&gt;RISC&lt;/code&gt;架构的汇编语言，因此和&lt;code&gt;CISC&lt;/code&gt;相比，其汇编更加精简。在&lt;code&gt;RISC-V&lt;/code&gt;中主要把汇编分为了几种对应的类型，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407180955152.png&quot; alt=&quot;RISCV I format&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在基础的&lt;code&gt;RV32I&lt;/code&gt;指令集中，如上图所示，有四种主要的指令格式(&lt;code&gt;R&lt;/code&gt;/&lt;code&gt;I&lt;/code&gt;/&lt;code&gt;S&lt;/code&gt;/&lt;code&gt;U&lt;/code&gt;)。在这里，&lt;strong&gt;所有的指令集的长度都被固定为$32-bits$，并且基础指令集是$IALIGN=32$的吗，这就意味着内存中的指令集必须以四字节对齐&lt;/strong&gt;。如果目标地址没有按$IALIGN$位对齐，则在获取的分支或无条件跳转上生成指令地址不对齐异常。该异常报告在分支或跳转指令上，而不是在目标指令上。对于未采用的条件分支，不会生成指令地址不对齐异常。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RISC-V&lt;/code&gt;&lt;strong&gt;指令集架构在所有格式中都保持源寄存器(&lt;code&gt;rs1&lt;/code&gt;和&lt;code&gt;rs2&lt;/code&gt;)和目标寄存器(&lt;code&gt;rd&lt;/code&gt;)处于相同位置，以简化解码。除了用于&lt;code&gt;CSR&lt;/code&gt;指令的&lt;code&gt;5&lt;/code&gt;位立即数外，立即数总是符号扩展的，并且通常打包到指令中最左边的可用位，这样分配是为了减少硬件复杂性。特别是，所有立即数的符号位总是位于指令的第&lt;code&gt;31&lt;/code&gt;位，以加快符号扩展电路的速度&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;除了上面的四种主要指令格式之外，基础指令集还提供了关于&lt;code&gt;B&lt;/code&gt;和&lt;code&gt;J&lt;/code&gt;指令格式的变种：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181004346.png&quot; alt=&quot;RISCV I format variant&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;S&lt;/code&gt;格式和&lt;code&gt;B&lt;/code&gt;格式之间的唯一区别在于，&lt;code&gt;B&lt;/code&gt;&lt;strong&gt;格式中&lt;code&gt;12&lt;/code&gt;位立即数字段用于以&lt;code&gt;2&lt;/code&gt;的倍数编码分支偏移。通常做法是在硬件中将指令编码的立即数的所有位左移一位，而在这里，中间位(&lt;code&gt;imm[10:1]&lt;/code&gt;)和符号位保持在固定位置，而&lt;code&gt;S&lt;/code&gt;格式中的最低位(&lt;code&gt;inst[7]&lt;/code&gt;)在&lt;code&gt;B&lt;/code&gt;格式中编码为高位&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;RV32&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;RISC-V&lt;/code&gt;汇编中，单独分析一条汇编是繁琐的，因为&lt;code&gt;RISC-V&lt;/code&gt;通常是一组汇编一起分析，通过对应的&lt;code&gt;funct code&lt;/code&gt;和&lt;code&gt;opcode&lt;/code&gt;就能够确定其具体为哪一条指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181014022.png&quot; alt=&quot;RISCV opcode&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;RV32-Immediate&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181022943.png&quot; alt=&quot;RV32 Immediate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ADDI&lt;/code&gt;指令将符号扩展的&lt;code&gt;12&lt;/code&gt;位立即数加到寄存器&lt;code&gt;rs1&lt;/code&gt;。算术溢出被忽略，结果仅保留低&lt;code&gt;XLEN&lt;/code&gt;位。&lt;code&gt;ADDI rd, rs1, 0&lt;/code&gt;用于实现汇编伪指令&lt;code&gt;MV rd, rs1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLTI&lt;/code&gt;指令将值&lt;code&gt;1&lt;/code&gt;写入寄存器&lt;code&gt;rd&lt;/code&gt;，如果寄存器&lt;code&gt;rs1&lt;/code&gt;小于符号扩展的立即数(两者都作为有符号数处理)，否则将&lt;code&gt;0&lt;/code&gt;写入&lt;code&gt;rd&lt;/code&gt;。&lt;code&gt;SLTIU&lt;/code&gt;与&lt;code&gt;SLTI&lt;/code&gt;类似，但将值作为无符号数进行比较(即，立即数首先符号扩展到$XLEN$位，然后作为无符号数处理)。注意，&lt;code&gt;SLTIU rd, rs1, 1&lt;/code&gt;如果$rs1 = 0$则将&lt;code&gt;rd&lt;/code&gt;置为&lt;code&gt;1&lt;/code&gt;，否则置为&lt;code&gt;0&lt;/code&gt;(汇编伪指令&lt;code&gt;SEQZ rd, rs&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ANDI&lt;/code&gt;, &lt;code&gt;ORI&lt;/code&gt;, &lt;code&gt;XORI&lt;/code&gt;是逻辑操作指令，对寄存器&lt;code&gt;rs1&lt;/code&gt;和符号扩展的&lt;code&gt;12&lt;/code&gt;位立即数执行按位与、或和异或操作，并将结果放入&lt;code&gt;rd&lt;/code&gt;。注意，&lt;code&gt;XORI rd, rs1, -1&lt;/code&gt;对寄存器&lt;code&gt;rs1&lt;/code&gt;执行按位逻辑反转(汇编伪指令&lt;code&gt;NOT rd, rs&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181040556.png&quot; alt=&quot;RV32 Immediate shift&quot; /&gt;&lt;/p&gt;
&lt;p&gt;按常数进行移位的指令被编码为&lt;code&gt;I&lt;/code&gt;型格式的特化形式。要移位的操作数在&lt;code&gt;rs1&lt;/code&gt;中，移位量编码在&lt;code&gt;I&lt;/code&gt;型立即数字段的低&lt;code&gt;5&lt;/code&gt;位。右移的类型编码在第&lt;code&gt;30&lt;/code&gt;位。&lt;code&gt;SLLI&lt;/code&gt;是逻辑左移(零被移入低位)；&lt;code&gt;SRLI&lt;/code&gt;是逻辑右移(零被移入高位)；&lt;code&gt;SRAI&lt;/code&gt;是算术右移(原符号位被复制到空出的高位)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181048420.png&quot; alt=&quot;RV32 Immediate other&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LUI&lt;/code&gt;用于构建&lt;code&gt;32&lt;/code&gt;位常量，使用&lt;code&gt;U&lt;/code&gt;型格式。&lt;code&gt;LUI&lt;/code&gt;将&lt;code&gt;32&lt;/code&gt;位&lt;code&gt;U&lt;/code&gt;型立即数值放入目标寄存器&lt;code&gt;rd&lt;/code&gt;中，最低的&lt;code&gt;12&lt;/code&gt;位填充为零。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AUIPC&lt;/code&gt;用于构建相对于程序计数器(&lt;code&gt;pc&lt;/code&gt;)的地址，使用&lt;code&gt;U&lt;/code&gt;型格式。&lt;code&gt;AUIPC&lt;/code&gt;形成一个&lt;code&gt;32&lt;/code&gt;位偏移量，最低的&lt;code&gt;12&lt;/code&gt;位填充为零，将此偏移量加到&lt;code&gt;AUIPC&lt;/code&gt;指令的地址上，然后将结果放入寄存器&lt;code&gt;rd&lt;/code&gt;中。&lt;/p&gt;
&lt;h4&gt;RV32-Register&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RV32I&lt;/code&gt;定义了几种算术&lt;code&gt;R&lt;/code&gt;型操作。所有操作都读取&lt;code&gt;rs1&lt;/code&gt;和&lt;code&gt;rs2&lt;/code&gt;寄存器作为源操作数，并将结果写入&lt;code&gt;rd&lt;/code&gt;寄存器。&lt;code&gt;funct7&lt;/code&gt;和&lt;code&gt;funct3&lt;/code&gt;字段选择操作的类型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181115880.png&quot; alt=&quot;RV32 Register&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ADD&lt;/code&gt;执行&lt;code&gt;rs1&lt;/code&gt;和&lt;code&gt;rs2&lt;/code&gt;的加法。&lt;code&gt;SUB&lt;/code&gt;执行&lt;code&gt;rs1&lt;/code&gt;减去&lt;code&gt;rs2&lt;/code&gt;的减法。溢出被忽略，结果的低$XLEN$位写入目标寄存器&lt;code&gt;rd&lt;/code&gt;。&lt;code&gt;SLT&lt;/code&gt;和&lt;code&gt;SLTU&lt;/code&gt;分别执行有符号和无符号比较，如果$rs1 lt rs2$，则将&lt;code&gt;1&lt;/code&gt;写入&lt;code&gt;rd&lt;/code&gt;，否则写入&lt;code&gt;0&lt;/code&gt;。注意，&lt;code&gt;SLTU rd, x0, rs2&lt;/code&gt;如果$rs \ne 0$则将&lt;code&gt;rd&lt;/code&gt;置为&lt;code&gt;1&lt;/code&gt;，否则置为&lt;code&gt;0&lt;/code&gt;(汇编伪指令&lt;code&gt;SNEZ rd, rs&lt;/code&gt;)。&lt;code&gt;AND&lt;/code&gt;、&lt;code&gt;OR&lt;/code&gt;和&lt;code&gt;XOR&lt;/code&gt;执行按位逻辑运算。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLL&lt;/code&gt;、&lt;code&gt;SRL&lt;/code&gt;和&lt;code&gt;SRA&lt;/code&gt;分别对寄存器&lt;code&gt;rs1&lt;/code&gt;中的值按寄存器&lt;code&gt;rs2&lt;/code&gt;低&lt;code&gt;5&lt;/code&gt;位中的移位量执行逻辑左移、逻辑右移和算术右移。&lt;/p&gt;
&lt;h4&gt;RV32-NOP&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181422238.png&quot; alt=&quot;RV32 NOP&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NOP&lt;/code&gt;指令不会改变任何架构上可见的状态，除了推进程序计数器(pc)和增加任何适用的性能计数器。&lt;code&gt;NOP&lt;/code&gt;被编码为&lt;code&gt;ADDI x0, x0, 0&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;RV32-Jump&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RV32I&lt;/code&gt;提供了两种类型的控制转移指令：&lt;strong&gt;无条件跳转和条件分支&lt;/strong&gt;。&lt;code&gt;RV32I&lt;/code&gt;&lt;strong&gt;中的控制转移指令没有架构上可见的延迟槽&lt;/strong&gt;。如果在跳转或已执行分支的目标上发生指令访问故障或指令页故障异常，则异常在目标指令上报告，而不是在跳转或分支指令上报告。&lt;/p&gt;
&lt;p&gt;跳转并链接(&lt;code&gt;JAL&lt;/code&gt;)指令使用&lt;code&gt;J&lt;/code&gt;型格式，其中&lt;code&gt;J&lt;/code&gt;型立即数编码为以&lt;code&gt;2&lt;/code&gt;字节为单位的有符号偏移量。偏移量被符号扩展并加到跳转指令的地址上，以形成跳转目标地址。因此，跳转可以目标$±1 MiB$的范围。&lt;code&gt;JAL&lt;/code&gt;将跳转后指令$pc+4$的地址存储到寄存器&lt;code&gt;rd&lt;/code&gt;中。标准的软件调用约定使用&lt;code&gt;x1&lt;/code&gt;作为返回地址寄存器，并使用&lt;code&gt;x5&lt;/code&gt;作为备用链接寄存器。&lt;/p&gt;
&lt;p&gt;普通无条件跳转(汇编伪指令&lt;code&gt;J&lt;/code&gt;)被编码为$rd=x0$的&lt;code&gt;JAL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181447591.png&quot; alt=&quot;JAL&quot; /&gt;&lt;/p&gt;
&lt;p&gt;间接跳转指令&lt;code&gt;JAL&lt;/code&gt;(跳转并链接寄存器)使用&lt;code&gt;I&lt;/code&gt;型编码。目标地址通过将符号扩展的&lt;code&gt;12&lt;/code&gt;位&lt;code&gt;I&lt;/code&gt;型立即数加到寄存器&lt;code&gt;rs1&lt;/code&gt;上获得，然后将结果的最低有效位设为零。跳转后指令($pc+4$)的地址写入寄存器&lt;code&gt;rd&lt;/code&gt;。如果不需要结果，可以使用寄存器&lt;code&gt;x0&lt;/code&gt;作为目标寄存器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181455224.png&quot; alt=&quot;JALR&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果目标地址未对齐到四字节边界, &lt;code&gt;JAL&lt;/code&gt;和&lt;code&gt;JALR&lt;/code&gt;指令将生成指令地址未对齐异常&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;返回地址预测堆栈是高性能指令获取单元的常见特性，但需要准确检测用于过程调用和返回的指令才能有效&lt;/strong&gt;。对于&lt;code&gt;RISC-V&lt;/code&gt;，指令使用的提示通过使用的寄存器编号隐式编码。仅当&lt;code&gt;JAL&lt;/code&gt;指令的&lt;code&gt;rd&lt;/code&gt;是&lt;code&gt;x1&lt;/code&gt;或&lt;code&gt;x5&lt;/code&gt;时，才应将返回地址推入返回地址堆栈(&lt;code&gt;RAS&lt;/code&gt;)。&lt;code&gt;JALR&lt;/code&gt;指令应根据下图所示推入/弹出&lt;code&gt;RAS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181500273.png&quot; alt=&quot;Return-address stack Table&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;RV32-Branch&lt;/h4&gt;
&lt;p&gt;所有分支指令都使用&lt;code&gt;B&lt;/code&gt;型指令格式。&lt;code&gt;12&lt;/code&gt;位&lt;code&gt;B&lt;/code&gt;型立即数以&lt;code&gt;2&lt;/code&gt;字节为单位编码有符号偏移量。偏移量被符号扩展并加到分支指令的地址上，以得出目标地址。条件分支范围为$±4 KiB$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181502814.png&quot; alt=&quot;Branch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分支指令比较两个寄存器。&lt;code&gt;BEQ&lt;/code&gt;和&lt;code&gt;BNE&lt;/code&gt;分别在寄存器&lt;code&gt;rs1&lt;/code&gt;和&lt;code&gt;rs2&lt;/code&gt;相等或不等时执行分支。&lt;code&gt;BLT&lt;/code&gt;和&lt;code&gt;BLTU&lt;/code&gt;分别在使用有符号和无符号比较时$rs1 \lt rs2$时执行分支。&lt;code&gt;BGE&lt;/code&gt;和&lt;code&gt;BGEU&lt;/code&gt;分别在使用有符号和无符号比较时$rs1 \ge rs2$时执行分支。注意，&lt;code&gt;BGT&lt;/code&gt;、&lt;code&gt;BGTU&lt;/code&gt;、&lt;code&gt;BLE&lt;/code&gt;和&lt;code&gt;BLEU&lt;/code&gt;可以通过反转&lt;code&gt;BLT&lt;/code&gt;、&lt;code&gt;BLTU&lt;/code&gt;、&lt;code&gt;BGE&lt;/code&gt;和&lt;code&gt;BGEU&lt;/code&gt;的操作数来合成。&lt;/p&gt;
&lt;h4&gt;RV32-LoadStore&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RV32I&lt;/code&gt;是一种&lt;code&gt;load-store&lt;/code&gt;架构，其中只有加载和存储指令访问内存，算术指令只对&lt;code&gt;CPU&lt;/code&gt;寄存器进行操作。&lt;code&gt;RV32I&lt;/code&gt;提供一个&lt;code&gt;32&lt;/code&gt;位的地址空间，以字节为单位寻址。&lt;code&gt;EEI&lt;/code&gt;(执行环境接口)将定义哪些部分的地址空间可以使用哪些指令进行访问(例如，某些地址可能是只读的，或者只支持字访问)。即使目标寄存器是&lt;code&gt;x0&lt;/code&gt;的加载操作，仍必须引发任何异常并引起其他副作用，即使加载的值被丢弃。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EEI&lt;/code&gt;将定义内存系统是小端序还是大端序。在&lt;code&gt;RISC-V&lt;/code&gt;中，端序是字节地址不变的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181512721.png&quot; alt=&quot;RV32I load-store&quot; /&gt;&lt;/p&gt;
&lt;p&gt;加载和存储指令在寄存器和内存之间传输值。加载指令使用&lt;code&gt;I&lt;/code&gt;型格式编码，存储指令使用&lt;code&gt;S&lt;/code&gt;型格式。有效地址通过将寄存器&lt;code&gt;rs1&lt;/code&gt;与符号扩展的&lt;code&gt;12&lt;/code&gt;位偏移量相加获得。加载指令将内存中的值复制到寄存器&lt;code&gt;rd&lt;/code&gt;。存储指令将寄存器&lt;code&gt;rs2&lt;/code&gt;中的值复制到内存中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LW&lt;/code&gt;指令将&lt;code&gt;32&lt;/code&gt;位值从内存加载到&lt;code&gt;rd&lt;/code&gt;中。&lt;code&gt;LH&lt;/code&gt;从内存加载&lt;code&gt;16&lt;/code&gt;位值，然后符号扩展到&lt;code&gt;32&lt;/code&gt;位后存储在&lt;code&gt;rd&lt;/code&gt;中。&lt;code&gt;LHU&lt;/code&gt;从内存加载&lt;code&gt;16&lt;/code&gt;位值，但随后零扩展到&lt;code&gt;32&lt;/code&gt;位后存储在&lt;code&gt;rd&lt;/code&gt;中。&lt;code&gt;LB&lt;/code&gt;和&lt;code&gt;LBU&lt;/code&gt;类似地定义为加载&lt;code&gt;8&lt;/code&gt;位值。&lt;code&gt;SW&lt;/code&gt;、&lt;code&gt;SH&lt;/code&gt;和&lt;code&gt;SB&lt;/code&gt;指令将&lt;code&gt;32&lt;/code&gt;位、&lt;code&gt;16&lt;/code&gt;位和&lt;code&gt;8&lt;/code&gt;位值从寄存器&lt;code&gt;rs2&lt;/code&gt;的低位存储到内存中。&lt;/p&gt;
&lt;h4&gt;RV32-Memory&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181539309.png&quot; alt=&quot;FENCE&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FENCE&lt;/code&gt;指令用于对设备&lt;code&gt;I/O&lt;/code&gt;和内存访问进行排序，使其在其他&lt;code&gt;RISC-V&lt;/code&gt;处理器核(&lt;code&gt;hart&lt;/code&gt;)和外部设备或协处理器看来有序。可以将设备输入(&lt;code&gt;I&lt;/code&gt;)、设备输出(&lt;code&gt;O&lt;/code&gt;)、内存读取(&lt;code&gt;R&lt;/code&gt;)和内存写入(&lt;code&gt;W&lt;/code&gt;)的任意组合相对于相同的任意组合进行排序。&lt;strong&gt;非正式地说, &lt;code&gt;FENCE&lt;/code&gt;之后的继承集合中的任何操作，在&lt;code&gt;FENCE&lt;/code&gt;之前的前置集合中的任何操作完成之前，不能被其他&lt;code&gt;RISC-V&lt;/code&gt;处理器核或外部设备观察到&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FENCE&lt;/code&gt;&lt;strong&gt;指令还对处理器核进行的内存读写操作相对于外部设备进行的内存读写操作进行排序。然而，&lt;code&gt;FENCE&lt;/code&gt;不对外部设备使用任何其他信号机制进行的事件观察进行排序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EEI&lt;/code&gt;&lt;strong&gt;将定义哪些&lt;code&gt;I/O&lt;/code&gt;操作是可能的，特别是哪些内存地址在被加载和存储指令访问时将分别被视为设备输入和设备输出操作，而不是内存读写操作&lt;/strong&gt;。例如，通常使用未缓存的加载和存储访问内存映射的&lt;code&gt;I/O&lt;/code&gt;设备，这些操作使用&lt;code&gt;I&lt;/code&gt;和&lt;code&gt;O&lt;/code&gt;位进行排序，而不是使用&lt;code&gt;R&lt;/code&gt;和&lt;code&gt;W&lt;/code&gt;位。指令集扩展可能还会描述新的&lt;code&gt;I/O&lt;/code&gt;指令，这些指令也将使用&lt;code&gt;FENCE&lt;/code&gt;中的&lt;code&gt;I&lt;/code&gt;和&lt;code&gt;O&lt;/code&gt;位进行排序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181540312.png&quot; alt=&quot;Fence mode encoding&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fence&lt;/code&gt;模式字段&lt;code&gt;fm&lt;/code&gt;定义了&lt;code&gt;FENCE&lt;/code&gt;指令的语义。$fm=0000$的&lt;code&gt;FENCE&lt;/code&gt;指令在其继承集合中的所有内存操作之前排序其前置集合中的所有内存操作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FENCE.TSO&lt;/code&gt;指令被编码为$fm=1000$、前置集合=&lt;code&gt;RW&lt;/code&gt;、继承集合=&lt;code&gt;RW&lt;/code&gt;的&lt;code&gt;FENCE&lt;/code&gt;指令。&lt;code&gt;FENCE.TSO&lt;/code&gt;指令在其前置集合中的所有加载操作之前排序其继承集合中的所有内存操作，并在其前置集合中的所有存储操作之前排序其继承集合中的所有存储操作。这使得&lt;code&gt;FENCE.TSO&lt;/code&gt;的前置集合中的非&lt;code&gt;AMO&lt;/code&gt;存储操作与其继承集合中的非&lt;code&gt;AMO&lt;/code&gt;加载操作不排序。&lt;/p&gt;
&lt;h4&gt;RV32-Env&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SYSTEM&lt;/code&gt;指令用于访问可能需要特权访问的系统功能，并使用&lt;code&gt;I&lt;/code&gt;型指令格式编码。这些指令可以分为两大类：&lt;strong&gt;atomically read-modify-write control&lt;/strong&gt;和&lt;strong&gt;状态寄存器&lt;/strong&gt;(&lt;strong&gt;CSR&lt;/strong&gt;)的指令，以及所有其他潜在的特权指令。&lt;code&gt;CSR&lt;/code&gt;指令需要单独描述，基本的非特权指令在以下部分描述。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181553919.png&quot; alt=&quot;SYSTEM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ECALL&lt;/code&gt;指令用于向执行环境发出服务请求。&lt;code&gt;EEI&lt;/code&gt;将定义如何传递服务请求的参数，但通常这些参数将在整数寄存器文件中的指定位置。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;EBREAK&lt;/code&gt;指令用于将控制权返回到调试环境。&lt;/p&gt;
&lt;h3&gt;RV64&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;RV64I&lt;/code&gt;将整数寄存器和支持的用户地址空间扩展到&lt;code&gt;64&lt;/code&gt;位($XLEN=64$)。&lt;/p&gt;
&lt;h4&gt;RV64-Immediate&lt;/h4&gt;
&lt;p&gt;大多数整数计算指令操作$XLEN$位值。在&lt;code&gt;RV64I&lt;/code&gt;中提供了额外的指令变体，用于操作&lt;code&gt;32&lt;/code&gt;位值，这些指令的操作码以 &lt;code&gt;W&lt;/code&gt; 后缀表示。这些 &lt;code&gt;*W&lt;/code&gt; 指令忽略输入的高&lt;code&gt;32&lt;/code&gt;位，并始终生成&lt;code&gt;32&lt;/code&gt;位有符号值，将它们符号扩展到&lt;code&gt;64&lt;/code&gt;位，即位 $XLEN-1$ 到 &lt;code&gt;31&lt;/code&gt; 是相等的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181620677.png&quot; alt=&quot;ADDIW&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ADDIW&lt;/code&gt;是&lt;code&gt;RV64I&lt;/code&gt;指令，将符号扩展的&lt;code&gt;12&lt;/code&gt;位立即数加到寄存器&lt;code&gt;rs1&lt;/code&gt;，并在&lt;code&gt;rd&lt;/code&gt;中生成正确符号扩展的&lt;code&gt;32&lt;/code&gt;位结果。溢出被忽略，结果是低&lt;code&gt;32&lt;/code&gt;位的符号扩展到&lt;code&gt;64&lt;/code&gt;位的结果。注意，&lt;code&gt;ADDIW rd, rs1, 0&lt;/code&gt;将寄存器&lt;code&gt;rs1&lt;/code&gt;的低&lt;code&gt;32&lt;/code&gt;位的符号扩展写入寄存器&lt;code&gt;rd&lt;/code&gt;(汇编伪指令 &lt;code&gt;SEXT.W&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181631534.png&quot; alt=&quot;immediate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;常量移位指令使用与&lt;code&gt;RV32I&lt;/code&gt;相同的指令操作码作为&lt;code&gt;I&lt;/code&gt;型格式的特化。待移位的操作数在&lt;code&gt;rs1&lt;/code&gt;中，移位量在&lt;code&gt;RV64I&lt;/code&gt;的&lt;code&gt;I&lt;/code&gt;型立即数字段的低&lt;code&gt;6&lt;/code&gt;位中编码。右移类型在第&lt;code&gt;30&lt;/code&gt;位中编码。&lt;code&gt;SLLI&lt;/code&gt;是逻辑左移(零被移入低位)；&lt;code&gt;SRLI&lt;/code&gt;是逻辑右移(零被移入高位)；&lt;code&gt;SRAI&lt;/code&gt;是算术右移(原始符号位被复制到腾出的高位)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181633545.png&quot; alt=&quot;Shift&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLLIW&lt;/code&gt;、&lt;code&gt;SRLIW&lt;/code&gt; 和 &lt;code&gt;SRAIW&lt;/code&gt; 是仅 &lt;code&gt;RV64I&lt;/code&gt; 支持的指令，它们以类似的方式定义，但作用于 &lt;code&gt;32&lt;/code&gt; 位值，并将其 &lt;code&gt;32&lt;/code&gt; 位结果符号扩展到 &lt;code&gt;64&lt;/code&gt; 位。$imm[5] \ne 0$ 的 &lt;code&gt;SLLIW&lt;/code&gt;、&lt;code&gt;SRLIW&lt;/code&gt; 和 &lt;code&gt;SRAIW&lt;/code&gt; 编码是保留的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181657447.png&quot; alt=&quot;other&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LUI&lt;/code&gt;使用与 &lt;code&gt;RV32I&lt;/code&gt; 相同的操作码。&lt;code&gt;LUI&lt;/code&gt; 将 &lt;code&gt;32&lt;/code&gt; 位 &lt;code&gt;U&lt;/code&gt; 型立即数放入寄存器 &lt;code&gt;rd&lt;/code&gt;，并用零填充最低的 &lt;code&gt;12&lt;/code&gt; 位。&lt;code&gt;32&lt;/code&gt; 位结果被符号扩展到 &lt;code&gt;64&lt;/code&gt; 位。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AUIPC&lt;/code&gt;使用与 &lt;code&gt;RV32I&lt;/code&gt; 相同的操作码。&lt;code&gt;AUIPC&lt;/code&gt; 用于构建相对于 &lt;code&gt;pc&lt;/code&gt; 的地址，并使用 &lt;code&gt;U&lt;/code&gt; 型格式。&lt;code&gt;AUIPC&lt;/code&gt; 从 &lt;code&gt;U&lt;/code&gt; 型立即数形成一个 &lt;code&gt;32&lt;/code&gt; 位偏移量，填充最低的 &lt;code&gt;12&lt;/code&gt; 位为零，将结果符号扩展到 &lt;code&gt;64&lt;/code&gt; 位，将其加到 &lt;code&gt;AUIPC&lt;/code&gt; 指令的地址，然后将结果放入寄存器 &lt;code&gt;rd&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;RV64-Register&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181700821.png&quot; alt=&quot;RV64 Register&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ADDW&lt;/code&gt; 和 &lt;code&gt;SUBW&lt;/code&gt; 是仅 &lt;code&gt;RV64I&lt;/code&gt; 支持的指令，定义类似于 &lt;code&gt;ADD&lt;/code&gt; 和 &lt;code&gt;SUB&lt;/code&gt;，但作用于 &lt;code&gt;32&lt;/code&gt; 位值并生成有符号的 &lt;code&gt;32&lt;/code&gt; 位结果。溢出被忽略，结果的低 &lt;code&gt;32&lt;/code&gt; 位被符号扩展到 &lt;code&gt;64&lt;/code&gt; 位并写入目标寄存器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLL&lt;/code&gt;、&lt;code&gt;SRL&lt;/code&gt; 和 &lt;code&gt;SRA&lt;/code&gt; 对寄存器 &lt;code&gt;rs1&lt;/code&gt; 中的值执行逻辑左移、逻辑右移和算术右移，移位量由寄存器 &lt;code&gt;rs2&lt;/code&gt; 中的值决定。在 &lt;code&gt;RV64I&lt;/code&gt; 中，仅考虑 &lt;code&gt;rs2&lt;/code&gt; 的低 &lt;code&gt;6&lt;/code&gt; 位作为移位量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLLW&lt;/code&gt;、&lt;code&gt;SRLW&lt;/code&gt; 和 &lt;code&gt;SRAW&lt;/code&gt; 是仅 &lt;code&gt;RV64I&lt;/code&gt; 支持的指令，定义类似，但作用于 &lt;code&gt;32&lt;/code&gt; 位值并将其 &lt;code&gt;32&lt;/code&gt; 位结果符号扩展到 &lt;code&gt;64&lt;/code&gt; 位。移位量由 &lt;code&gt;rs2[4:0]&lt;/code&gt; 给出。&lt;/p&gt;
&lt;h4&gt;RV64-LoadStore&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RV64I&lt;/code&gt; 将地址空间扩展到 &lt;code&gt;64&lt;/code&gt; 位。执行环境将定义哪些部分的地址空间是合法访问的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181704209.png&quot; alt=&quot;Load Store&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;RV64I&lt;/code&gt; 中，&lt;code&gt;LD&lt;/code&gt; 指令从内存中加载一个 &lt;code&gt;64&lt;/code&gt; 位值到寄存器 &lt;code&gt;rd&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;RV64I&lt;/code&gt; 中，&lt;code&gt;LW&lt;/code&gt; 指令从内存中加载一个 &lt;code&gt;32&lt;/code&gt; 位值，并将其符号扩展到 &lt;code&gt;64&lt;/code&gt; 位后存储到寄存器 &lt;code&gt;rd&lt;/code&gt;。而 &lt;code&gt;LWU&lt;/code&gt; 指令则将内存中的 &lt;code&gt;32&lt;/code&gt; 位值零扩展到 &lt;code&gt;64&lt;/code&gt; 位。同样，&lt;code&gt;LH&lt;/code&gt; 和 &lt;code&gt;LHU&lt;/code&gt; 对 &lt;code&gt;16&lt;/code&gt; 位值的操作与此类似，&lt;code&gt;LB&lt;/code&gt; 和 &lt;code&gt;LBU&lt;/code&gt; 对 &lt;code&gt;8&lt;/code&gt; 位值的操作也类似。&lt;code&gt;SD&lt;/code&gt;、&lt;code&gt;SW&lt;/code&gt;、&lt;code&gt;SH&lt;/code&gt; 和 &lt;code&gt;SB&lt;/code&gt; 指令分别将寄存器 &lt;code&gt;rs2&lt;/code&gt; 的低位的 &lt;code&gt;64&lt;/code&gt; 位、&lt;code&gt;32&lt;/code&gt; 位、&lt;code&gt;16&lt;/code&gt; 位和 &lt;code&gt;8&lt;/code&gt; 位值存储到内存中。&lt;/p&gt;
&lt;h3&gt;RV Instructions&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181712540.png&quot; alt=&quot;RV32 Instructions&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407181714021.png&quot; alt=&quot;RV64 Instructions&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;RISC-V64 Code Detail&lt;/h2&gt;
&lt;p&gt;对于&lt;code&gt;ArkTS&lt;/code&gt;的&lt;code&gt;RISC-V64&lt;/code&gt;来说，我们的第一步还是先要确定其对应的寄存器(因为过长，此处就不贴出代码)。我们使用&lt;code&gt;enum RegisterId : uint8_t&lt;/code&gt;来对每一个寄存器进行定义。&lt;/p&gt;
&lt;p&gt;为了简化操作，我们同时也需要对&lt;code&gt;Opcode&lt;/code&gt;和&lt;code&gt;funct&lt;/code&gt;进行定义，这样就能够明确的确定一条指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum opCode {
    opCodeI = 0x13,       // SLLI,SRLI,SRAI,ADDI,SLTI,SLTIU,XORI,ORI,ANDI,SLLI,SRLI,SRAI,
    opCodeILoad = 0x03,   // LWU,LD,LB,LH,LW,LBU,LHU
    opCodeR = 0x33,       // ADD,SUB,SLL,SLT,SLTU,XOR,SRL,SRA,OR,AND
    opCodeW = 0x1b,       // ADDW,SUBW,SLLW,SRLW,SRAW
    opCodeS = 0x23,       // SD,SB,SH,SW
    opCodeB = 0x63,       // BEQ,BNE,BLT,BGE,BLTU,BGEU
    opCodeULui = 0x37,    // LUI
    opCodeUAuipc = 0x17,  // AUIPC
    opCodeJal = 0x6f,     // JAL
    opCodeJalr = 0x67,    // JALR
    opCodeIF = 0X0f,      // FENCE,FENCE.I
    opCodeIE = 0x73,      // ECALL,EBREA,
    opCodeIC = 0x73,      // CSRRW,CSRRS,CSRRC,CSRRWI,CSRRSI,CSRRCI,ADDIW,SLLIW,SRLIW,SRAIW
    opCodeIA = 0x3b       // ADDW,SUBW,SLLW,SRLW,SRAW
};

enum funct3 {
    funct3Slli = 0x1000,
    funct3Srli = 0x5000,
    funct3Srai = 0x5000,
    funct3Add = 0x0,
    funct3Sub = 0x0,
    funct3Sll = 0x1000,
    ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;ArkTS&lt;/code&gt;中，我们依旧需要考虑$32bits$和$64bits$这两种情况，因此需要定义一个结构用于声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum RegisterType {
    W = 0,  /* a word for 32 bits */
    D = 1,  /* a double-word for 64 bits */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时，和&lt;code&gt;Aarch64&lt;/code&gt;一致，我们在处理汇编时需要对汇编的字段进行操作，因此我们需要分割每一条指令的字段，因此就采用了通常的方式使用宏进行分割：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define DECL_FIELDS_IN_INSTRUCTION(INSTNAME, FIELD_NAME, HIGHBITS, LOWBITS) \
static const uint32_t INSTNAME##_##FIELD_NAME##_HIGHBITS = HIGHBITS;  \
static const uint32_t INSTNAME##_##FIELD_NAME##_LOWBITS = LOWBITS;    \
static const uint32_t INSTNAME##_##FIELD_NAME##_WIDTH = ((HIGHBITS - LOWBITS) + 1); \
static const uint32_t INSTNAME##_##FIELD_NAME##_MASK = (((1 &amp;lt;&amp;lt; INSTNAME##_##FIELD_NAME##_WIDTH) - 1) &amp;lt;&amp;lt; LOWBITS);

#define DECL_INSTRUCTION_FIELDS(V)  \
    R_TYPE_FIELD_LIST(V) \
    B_TYPE_FIELD_LIST(V) \
    S_TYPE_FIELD_LIST(V) \
    U_TYPE_FIELD_LIST(V) \
    J_TYPE_FIELD_LIST(V) \
    I_TYPE_FIELD_LIST(V)

DECL_INSTRUCTION_FIELDS(DECL_FIELDS_IN_INSTRUCTION)
#undef DECL_INSTRUCTION_FIELDS
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RISC-V64 Header&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;assembler_riscv64.h&lt;/code&gt;中，我们首先需要注意的就是$XLEN$和各种字段类型处理。对于字段类型的处理而言，我们可以参考&lt;code&gt;aarch64&lt;/code&gt;的实现过程，因此我们也分为了&lt;code&gt;Register&lt;/code&gt;，&lt;code&gt;Immediate&lt;/code&gt;，&lt;code&gt;LogicalImmediate&lt;/code&gt;，&lt;code&gt;Operand&lt;/code&gt;，&lt;code&gt;Extend&lt;/code&gt;和&lt;code&gt;Shifte&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先需要分析的是$XLEN$，对于该字段而言，&lt;code&gt;RV32&lt;/code&gt;对应的则是$XLEN = 32$；而&lt;code&gt;RV64&lt;/code&gt;为$XLEN = 64$：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum RegisterWidth {
    XLEN_32 = 32,  // 32bits
    XLEN_64 = 64   // 64
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;Register&lt;/code&gt;类型而言，我们还是对&lt;code&gt;RegisterId&lt;/code&gt;进行一个封装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Register {
public:
    //TODO NOT SURE
    Register(RegisterId reg, RegisterWidth width) : reg_(reg), width_(width) {}
    Register(RegisterId reg) : reg_(reg) {};

    RegisterWidth getWidth() const;
    inline RegisterId GetId() const;
    
    inline bool IsValid() const;
    inline bool operator !=(const Register &amp;amp;other);
    inline bool operator ==(const Register &amp;amp;other);

private:
    RegisterId reg_;
    RegisterWidth width_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个疑惑：&lt;strong&gt;RV的官方手册中对于指令的长度有记录，&lt;code&gt;RV64&lt;/code&gt;是基于&lt;code&gt;RV32&lt;/code&gt;而来的，其给出的指令长度也是$32bits$的，只是立即数和操作数数据会被扩展为$XLEN$大小，但在这里我们的&lt;code&gt;getWidth&lt;/code&gt;返回$64bits$是否会有问题&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;然后我们就需要简单封装我们的立即数&lt;code&gt;Immediate&lt;/code&gt;和&lt;code&gt;LogicalImmediate&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Immediate {
public:
    Immediate(int32_t value) : value_(value) {}
    ~Immediate() = default;

    int32_t Value() const;

private:
    int32_t value_;
};

class LogicalImmediate {
public:
    static LogicalImmediate Create(uint64_t imm, int width);
    int Value() const;

    bool IsValid() const;
    // TODO NOT SURE
private:
    explicit LogicalImmediate(int value)
        : imm_(value) {}
    static const int InvalidLogicalImmediate = -1;
    int imm_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再封装&lt;code&gt;Shift&lt;/code&gt;和&lt;code&gt;Extend&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Extend : uint8_t {
    NO_EXTEND = 0xFF,
    UXTB = 0,   /* zero extend to byte */
    UXTH = 1,   /* zero extend to half word */
    UXTW = 2,   /* zero extend to word */
    UXTX = 3,   /* zero extend to 64bit */
    SXTB = 4,   /* sign extend to byte */
    SXTH = 5,   /* sign extend to half word */
    SXTW = 6,   /* sign extend to word */
    SXTX = 7,   /* sign extend to 64bit */
};

enum Shift : uint8_t {
    NO_SHIFT = 0xFF,
    LSL = 0x0,
    LSR = 0x1,
    ASR = 0x2,
    ROR = 0x3,
    MSL = 0x4,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，&lt;code&gt;RV64&lt;/code&gt;中的&lt;code&gt;Shift&lt;/code&gt;和&lt;code&gt;Extend&lt;/code&gt;也完全参照&lt;code&gt;Aarch64&lt;/code&gt;的一个实现。那么对于&lt;code&gt;Operand&lt;/code&gt;就不再多余赘述。&lt;/p&gt;
&lt;p&gt;最后，我们需要和&lt;code&gt;Aarch&lt;/code&gt;一样，继承最开始的&lt;code&gt;Assembler&lt;/code&gt;，对&lt;code&gt;RV64&lt;/code&gt;的结构进行一个实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AssemblerRiscv64 : public Assembler {
public:
    explicit AssemblerRiscv64(Chunk *chunk)
        : Assembler(chunk) {}
private:
    // R_TYPE field defines
    inline uint32_t Rd(uint32_t id);
    inline uint32_t Rs1(uint32_t id);
    inline uint32_t Rs2(uint32_t id);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RV INST&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;对于&lt;code&gt;RV&lt;/code&gt;而言，其指令集分为了大致五个类型，其中每一个类型的结构都大致相同，因此，我们可以通过宏直接对相同结构的指令进行实现，而不需要手动实现(主要为&lt;code&gt;RV32&lt;/code&gt;)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，这里便提供了宏&lt;code&gt;EMIT_INSTS&lt;/code&gt;和&lt;code&gt;AssemblerRiscv64::INSTNAME&lt;/code&gt;进行实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define EMIT_INSTS \
    EMIT_R_TYPE_INSTS(EMIT_R_TYPE_INST) \
    EMIT_B_TYPE_INSTS(EMIT_B_TYPE_INST) \
    EMIT_S_TYPE_INSTS(EMIT_S_TYPE_INST) \
    EMIT_U_TYPE_INSTS(EMIT_U_TYPE_INST) \
    EMIT_J_TYPE_INSTS(EMIT_J_TYPE_INST)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们主要分析&lt;code&gt;EMIT_R_TYPE_INSTS()&lt;/code&gt;和&lt;code&gt;EMIT_R_TYPE_INST&lt;/code&gt;这两个宏，有了这两个宏的理解能力，再去分析其他宏就信手拈来了。&lt;/p&gt;
&lt;p&gt;首先我们分析内层的&lt;code&gt;ENIT_R_TYPE_INST&lt;/code&gt;宏，该宏是&lt;code&gt;R&lt;/code&gt;型指令实现的具体接口，所有的&lt;code&gt;R&lt;/code&gt;型指令均可以通过该宏进行实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define EMIT_R_TYPE_INST(INSTNAME, INSTID) \
void AssemblerRiscv64::INSTNAME(const Register &amp;amp;rd, const Register &amp;amp;rs1, const Register &amp;amp;rs2) \
{ \
    uint32_t rd_id = Rd(rd.GetId()); \
    uint32_t rs1_id = Rs1(rs1.GetId()); \
    uint32_t rs2_id = Rs2(rs2.GetId()); \
    uint32_t code = rd_id | rs1_id | rs2_id | INSTID; \
    EmitU32(code); \
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，&lt;code&gt;EMIT_R_TYPE_INST&lt;/code&gt;接受两个参数，第一个则是实例化函数的名字，第二个则是其&lt;code&gt;opcode&lt;/code&gt;和&lt;code&gt;funct&lt;/code&gt;的结合。&lt;strong&gt;因为，在&lt;code&gt;RV&lt;/code&gt;中，&lt;code&gt;opcode&lt;/code&gt;和&lt;code&gt;funct&lt;/code&gt;的组合便足以确定一条确切的指令&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum AddSubOpFunct {
    ADD     = 0x00000033,
    ADDW    = 0x0000003b,
    ...
}

#define EMIT_R_TYPE_INSTS(V) \
    V( Add,  ADD)            \
    V(Addw, ADDW)            \
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，&lt;code&gt;EMIT_R_TYPE_INSTS&lt;/code&gt;则是所有&lt;code&gt;R&lt;/code&gt;型指令实现的入口，通过在&lt;code&gt;EMIT_R_TYPE_INSTS&lt;/code&gt;宏中定义对应的实例化名字和&lt;code&gt;OpFunct&lt;/code&gt;，就能够通过&lt;code&gt;EMIT_R_TYPE_INST&lt;/code&gt;实现对应确定的指令处理的函数。&lt;/p&gt;
&lt;p&gt;至此，&lt;code&gt;RV64&lt;/code&gt;在&lt;code&gt;Aarch64&lt;/code&gt;的结构便分析完毕了。&lt;/p&gt;
&lt;h2&gt;剩余任务分析&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;INST&lt;/th&gt;
&lt;th&gt;TYPE&lt;/th&gt;
&lt;th&gt;INST&lt;/th&gt;
&lt;th&gt;TYPE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jr&lt;/td&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;td&gt;Blr&lt;/td&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Br&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Ret&lt;/td&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLLIW&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;SRLIW&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SRAIW&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;SLLI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SRLI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;SRAI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRRW&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;CSRRS&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRRC&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;CSRRWI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRRSI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;CSRRCI&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADDIW&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;FENCE&lt;/td&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>ArkTS: analyze compiler_assembler</title><link>https://chenmiaoi.github.io/2024/07/04/ArkTS-analyze-compiler-assember/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/07/04/ArkTS-analyze-compiler-assember/</guid><pubDate>Thu, 04 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在此次的任务中，我们主要是对于&lt;code&gt;riscv64&lt;/code&gt;架构下的汇编进行处理，按照之前的经验来看，&lt;strong&gt;目前已有的&lt;/strong&gt;&lt;code&gt;riscv64&lt;/code&gt;&lt;strong&gt;汇编代码处理逻辑是依托于&lt;/strong&gt;&lt;code&gt;aarch64&lt;/code&gt;&lt;strong&gt;而来&lt;/strong&gt;。因此，在本节中会详细解析两个部分：&lt;code&gt;assembler.h&lt;/code&gt;和&lt;code&gt;assembler_aarch64&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Assembler&lt;/h2&gt;
&lt;p&gt;在这个小节中，我们会聚焦于&lt;code&gt;assembler.h&lt;/code&gt;中的关键代码，并且给出后续&lt;code&gt;riscv64&lt;/code&gt;可能的方向。&lt;/p&gt;
&lt;p&gt;首先，在&lt;code&gt;assembler.h&lt;/code&gt;中首先有一个名为&lt;code&gt;GCStackMapRegisters&lt;/code&gt;的类，在&lt;code&gt;ArkTS&lt;/code&gt;中，存在&lt;code&gt;GC&lt;/code&gt;机制。因此，这里的&lt;code&gt;GCStackMapRegisters&lt;/code&gt;&lt;strong&gt;可能是用于垃圾回收机制中的栈映射寄存器。在垃圾回收算法中，栈映射寄存器用于记录程序执行过程中的栈帧信息，以便正确地识别和回收不再使用的对象。栈映射寄存器通常用于确定每个栈帧的边界和对象引用的位置。通过分析栈映射寄存器中的值，垃圾回收器可以构建出程序的栈帧结构，识别出栈中的对象引用，并进行相应的垃圾回收操作&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class GCStackMapRegisters {
public:
#if defined(PANDA_TARGET_AMD64)
    static constexpr int SP = 7;  /* x7 */
    static constexpr int FP = 6;  /* x6 */
#elif defined(PANDA_TARGET_ARM64)
    static constexpr int SP = 31;  /* x31 */
    static constexpr int FP = 29;  /* x29 */
#elif defined(PANDA_TARGET_ARM32)
    static constexpr int SP = 13;  /* x13 */
    static constexpr int FP = 11;  /* x11 */
#elif defined(PANDA_TARGET_RISCV64)
    static constexpr int SP = 2;
    static constexpr int FP = 8;
#else
    static constexpr int SP = -1;
    static constexpr int FP = -1;
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而在该内中的存在两个尚未完全的函数&lt;code&gt;GetFpRegByTriple&lt;/code&gt;和&lt;code&gt;GetSpRegByTriple&lt;/code&gt;，因为这两个函数中均未对&lt;code&gt;RISCV64&lt;/code&gt;进行实现，因此可能在后续实现中，需要对其进行补全。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int GetFpRegByTriple(Triple triple)
{
    int fp = -1;
    switch (triple) {
        case Triple::TRIPLE_AMD64:
            fp = 6;  /* x6 */
            break;
        case Triple::TRIPLE_ARM32:
            fp = 11;  /* x11 */
            break;
        case Triple::TRIPLE_AARCH64:
            fp = 29;  /* x29 */
            break;
        default:
            UNREACHABLE();
            break;
    }
    return fp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;code&gt;GetFpRegByTriple&lt;/code&gt;和&lt;code&gt;GetSpRegByTriple&lt;/code&gt;唯一一次被调用则是位于&lt;code&gt;ecmascript/stackmap/llvm_stackmap_type.cpp&lt;/code&gt;中。至于具体作用，如果后续需要则会继续分析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auto fpReg = GCStackMapRegisters::GetFpRegByTriple(triple);
auto spReg = GCStackMapRegisters::GetSpRegByTriple(triple);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续往下看，则会看见一段异常重要的代码&lt;code&gt;FrameCompletionPos&lt;/code&gt;，(&lt;strong&gt;根据自己的猜测&lt;/strong&gt;)&lt;strong&gt;用于表示从&lt;code&gt;C++&lt;/code&gt;代码到汇编代码的转换和从汇编代码返回到&lt;code&gt;C++&lt;/code&gt;代码时的指令数量&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum FrameCompletionPos : uint64_t {
    // X64
    X64CppToAsmInterp = 28,
    X64AsmInterpToCpp = 9,
    X64EntryFrameDuration = 70,
    // ARM64
    ARM64CppToAsmInterp = 56,
    ARM64AsmInterpToCpp = 40,
    ARM64EntryFrameDuration = 116,
    // RISCV64
    RISCV64EntryFrameDuration = 140,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;code&gt;X64(x86-64)&lt;/code&gt;体系结构：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X64CppToAsmInterp&lt;/code&gt; 表示从&lt;code&gt;C++&lt;/code&gt;代码转换到汇编代码时的指令数量为 28。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X64AsmInterpToCpp&lt;/code&gt; 表示从汇编代码返回到&lt;code&gt;C++&lt;/code&gt;代码时的指令数量为 9。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X64EntryFrameDuration&lt;/code&gt; 表示使用汇编解释器(&lt;code&gt;AsmInterpreterEntryFrame&lt;/code&gt;)时的汇编帧的指令数量为 70。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于&lt;code&gt;ARM64(ARM64)&lt;/code&gt;体系结构：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ARM64CppToAsmInterp&lt;/code&gt; 表示从&lt;code&gt;C++&lt;/code&gt;代码转换到汇编代码时的指令数量为 56。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARM64AsmInterpToCpp&lt;/code&gt; 表示从汇编代码返回到&lt;code&gt;C++&lt;/code&gt;代码时的指令数量为 40。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARM64EntryFrameDuration&lt;/code&gt; 表示使用汇编解释器时的汇编帧的指令数量为 116。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于&lt;code&gt;RISCV64(RISC-V64)&lt;/code&gt;体系结构：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RISCV64EntryFrameDuration&lt;/code&gt; 表示使用汇编解释器时的汇编帧的指令数量为 140。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里的代码实际上在我们处理&lt;code&gt;RISC-V64 Assembler&lt;/code&gt;时没有影响，而是会在另外一个极其重要、并且需要我们处理的模块&lt;code&gt;stub trapoline&lt;/code&gt;中进行使用，这里不做过多介绍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ((end - begin) != FrameCompletionPos::RISCV64EntryFrameDuration)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Label&lt;/h3&gt;
&lt;p&gt;在这个部分中，主要介绍&lt;strong&gt;用于处理汇编逻辑中的跳转指令的标签&lt;/strong&gt;。在&lt;code&gt;ArkTS&lt;/code&gt;的运行时中使用一个名为&lt;code&gt;Label&lt;/code&gt;的类进行标识：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Label {
public:
    bool IsBound() const;
    bool IsLinked() const;
    bool IsLinkedNear() const;
    uint32_t GetPos() const;
    uint32_t GetLinkedPos() const;
    void BindTo(int32_t pos);
    void LinkTo(int32_t pos);
    void UnlinkNearPos();
    void LinkNearPos(uint32_t pos);
    uint32_t GetLinkedNearPos() const;

private:
    int32_t pos_ = 0;
    uint32_t nearPos_ = 0;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先我们需要区分两个概念：&lt;code&gt;绑定(bound)&lt;/code&gt;和&lt;code&gt;链接(link)&lt;/code&gt;。在&lt;code&gt;ArkTS&lt;/code&gt;中，绑定和链接是两个相似，但行为不同的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bound&lt;/code&gt;指的是&lt;strong&gt;将标签绑定到特定的位置。绑定标签意味着将标签与某个位置相关联，&lt;em&gt;通常用于表示位于后面的标签&lt;/em&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;link&lt;/code&gt;指的是&lt;strong&gt;将标签链接到特定的位置。链接标签意味着将标签与某个位置相关联，&lt;em&gt;通常用于表示位于前面的标签&lt;/em&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们可以使用实际的汇编代码进行解释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 绑定标签示例
section .text
    global _start

_start:             ; 链接的位置
    ; 绑定标签到位置
    jmp bind_label

bind_label:
    ; 这里是绑定的位置
    ; ...
    jmp _start       ; 链接回起始位置

    ; 继续执行其他代码...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在来解释各个函数的作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IsBound()&lt;/code&gt; 函数用于检查标签是否已绑定。如果 $pos_ \gt 0$，则表示标签已绑定。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IsLinked()&lt;/code&gt; 函数用于检查标签是否已链接。如果 $pos_ \lt 0$，则表示标签已链接。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IsLinkedNear()&lt;/code&gt; 函数用于检查标签是否已链接至近跳转位置。如果 $nearPos_ \gt 0$，则表示标签已链接至近跳转位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetPos()&lt;/code&gt; 函数用于获取标签的位置。返回 $pos_ - 1$ 的无符号整数值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetLinkedPos()&lt;/code&gt; 函数用于获取链接的位置。如果标签未绑定，则返回 $-pos_ - 1$ 的无符号整数值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BindTo()&lt;/code&gt; 函数将标签绑定到给定的位置。将 &lt;code&gt;pos_&lt;/code&gt; 设置为 $pos + 1$，以跳过偏移为 0 的位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkTo()&lt;/code&gt; 函数将标签链接到给定的位置。将 &lt;code&gt;pos_&lt;/code&gt; 设置为 $-(pos + 1)$，以跳过偏移为 0 的位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UnlinkNearPos()&lt;/code&gt; 函数取消链接至近跳转位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LinkNearPos()&lt;/code&gt; 函数将标签链接至近跳转位置。将 &lt;code&gt;nearPos_&lt;/code&gt; 设置为 $pos + 1$，以跳过偏移为 0 的位置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetLinkedNearPos()&lt;/code&gt; 函数用于获取链接至近跳转位置的位置。如果标签未绑定，则返回 $nearPos_ - 1$ 的无符号整数值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Assembler&lt;/h3&gt;
&lt;p&gt;接下来则是整个&lt;code&gt;assembler.h&lt;/code&gt;中最为重要的模块，所有架构的汇编处理逻辑都需要继承于&lt;code&gt;Assembler&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Assembler {
public:
    explicit Assembler(Chunk *chunk)
        : buffer_(chunk) {}
    ~Assembler() = default;

    void EmitU8(uint8_t v);
    void EmitI8(int8_t v);
    void EmitU16(uint16_t v);
    void EmitU32(uint32_t v);
    void EmitI32(int32_t v);
    void EmitU64(uint64_t v);
    void PutI8(size_t offset, int8_t data);
    void PutI32(size_t offset, int32_t data);
    uint32_t GetU32(size_t offset) const;
    int8_t GetI8(size_t offset) const;
    uint8_t GetU8(size_t offset) const;
    size_t GetCurrentPosition() const;
    uint8_t *GetBegin() const;
    static bool InRangeN(int32_t x, uint32_t n);
    static bool InRange8(int32_t x);
    static void GetFrameCompletionPos(uint64_t &amp;amp;headerSize, uint64_t &amp;amp;tailSize, uint64_t &amp;amp;entryDuration);
private:
    DynChunk buffer_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;Assembler&lt;/code&gt;中，&lt;code&gt;buffer_&lt;/code&gt;&lt;strong&gt;用于存储汇编的机器码&lt;/strong&gt;，而&lt;code&gt;Emit*&lt;/code&gt;则是会调用&lt;code&gt;buffer_.Emit*&lt;/code&gt;进行处理逻辑，这样能够根据汇编的机器码依次正确地存入内存中，以便后续处理。&lt;/p&gt;
&lt;p&gt;比如：在&lt;code&gt;X64&lt;/code&gt;架构中的&lt;code&gt;Assembler::Addq&lt;/code&gt;，其要处理的汇编为：&lt;code&gt;REX.W + 03 /r&lt;/code&gt;。这里需要注意的是，你应该知晓&lt;code&gt;x64&lt;/code&gt;架构下汇编的格式为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407021625400.png&quot; alt=&quot;intel x64 asm&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此，首先我们需要处理&lt;code&gt;Instruction Prefixes&lt;/code&gt;，然后处理&lt;code&gt;Opcode&lt;/code&gt;，最后处理&lt;code&gt;ModR/M&lt;/code&gt;。因为这里不需要处理&lt;code&gt;SIB&lt;/code&gt;。我们重新看回&lt;code&gt;REX.W + 03 /r&lt;/code&gt;，其中&lt;code&gt;REX.W&lt;/code&gt;是&lt;code&gt;Instruction Prefixes&lt;/code&gt;，&lt;code&gt;03&lt;/code&gt;是&lt;code&gt;Opcode&lt;/code&gt;，而&lt;code&gt;/r&lt;/code&gt;则是&lt;code&gt;ModR/M&lt;/code&gt;。而每一个段在此处都是一个字节，因此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerX64::Addq(Register src, Register dst)
{
    EmitRexPrefix(dst, src);
    // 03 : add r64, r/m64
    EmitU8(0x03);
    EmitModrm(dst, src);
}

void EmitRexPrefix(Register reg, Register rm)
{
    // 0: Extension to the MODRM.rm field B
    // 2: Extension to the MODRM.reg field R
    EmitU8(REX_PREFIX_W | (HighBit(reg) &amp;lt;&amp;lt; 2) | HighBit(rm));
}

void EmitModrm(int32_t reg, Register rm)
{
    EmitU8(MODE_RM | (static_cast&amp;lt;uint32_t&amp;gt;(reg) &amp;lt;&amp;lt; LOW_BITS_SIZE) | LowBits(rm));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们能够看见，&lt;code&gt;Assembler&lt;/code&gt;中的任何处理逻辑，都是依托于其真实汇编的机器码逻辑，并且根据其真实字节位宽进行处理，就比如&lt;code&gt;EmitU8&lt;/code&gt;则是填入&lt;code&gt;uint8_t&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;至于&lt;code&gt;Put*&lt;/code&gt;，此处不做过多介绍，其实际内部逻辑也是进行填入操作，但是会直接通过偏移量进行填入，需要我们自行保证其边界问题和内存安全问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inline void PutU32(size_t offset, uint32_t data) const
{
    // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
    *reinterpret_cast&amp;lt;uint32_t *&amp;gt;(buf_ + offset) = data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Aarch64 Assembler Detail&lt;/h2&gt;
&lt;p&gt;这一个小节主要是介绍&lt;code&gt;aarch64&lt;/code&gt;架构下的汇编处理逻辑，因为&lt;code&gt;RISC-V64&lt;/code&gt;架构大部分逻辑是临摹&lt;code&gt;aarch64&lt;/code&gt;架构所写，因此分析&lt;code&gt;aarch64&lt;/code&gt;的完整逻辑对后续开发有很大帮助。&lt;/p&gt;
&lt;h3&gt;Register&lt;/h3&gt;
&lt;p&gt;处理一个汇编首先需要解决的问题是，&lt;strong&gt;如何处理对应架构汇编下的对应寄存器&lt;/strong&gt;。在&lt;code&gt;aarch64&lt;/code&gt;中有两组寄存器：通用寄存器和&lt;code&gt;SIMD&lt;/code&gt;寄存器。&lt;/p&gt;
&lt;h4&gt;General Purpose Register&lt;/h4&gt;
&lt;p&gt;通用寄存器中的$31$个通用寄存器被命名为$R_0-R_{30}$，并在指令寄存器字段中以值$0-30$进行编码。在通用寄存器字段中，&lt;strong&gt;值$31$表示当前的堆栈指针或零寄存器&lt;/strong&gt;，具体取决于指令和操作数位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407031538142.png&quot; alt=&quot;Naming of general-purpose registers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于上图所给出的寄存器，这里有几个点需要注意的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$X_n$和$W_n$实际上指向的是同一组寄存器$R_n$，其只有位宽的差距；当然$WSP$和$SP$也是类似的情况。&lt;/li&gt;
&lt;li&gt;没有名为$W_{31}$或$X_{31}$的寄存器，使用$WZR$或$XZR$来表示第$31$号的零寄存器，当读写该寄存器时，其表现为返回零值或抛弃写入值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;enum RegisterId : uint8_t {
    X0, X1, X2, X3, X4, X5, X6, X7,
    X8, X9, X10, X11, X12, X13, X14, X15,
    X16, X17, X18, X19, X20, X21, X22, X23,
    X24, X25, X26, X27, X28, X29, X30, SP,
    Zero = SP,
    FP = X29,
    INVALID_REG = 0xFF,
};

enum RegisterType {
    W = 0,    // 32-bits
    X = 1,    // 64-bits
};

static const int RegXSize = 64;
static const int RegWSize = 32;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得注意的是，在&lt;code&gt;arm&lt;/code&gt;中$SP = 31，ZERO = 31，FP = 29$。&lt;/p&gt;
&lt;h5&gt;General Condition&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407031629956.png&quot; alt=&quot;General Condition&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Condition {
    EQ = 0,
    NE = 1,
    HS = 2,
    CS = HS,
    LO = 3,
    CC = LO,
    MI = 4,
    PL = 5,
    VS = 6,
    VC = 7,
    HI = 8,
    LS = 9,
    GE = 10,
    LT = 11,
    GT = 12,
    LE = 13,
    AL = 14,
    NV = 15,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Register Code&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Register {
public:
    Register(RegisterId reg, RegisterType type = RegisterType::X) : reg_(reg), type_(type) {};

    Register W() const;
    Register X() const;
    
    RegisterType GetType() const;
    inline RegisterId GetId() const;
    
    inline bool IsSp() const;
    inline bool IsW() const;
    inline bool IsValid() const;
    
    inline bool operator !=(const Register &amp;amp;other);
    inline bool operator ==(const Register &amp;amp;other);

private:
    RegisterId reg_;
    RegisterType type_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里不需要过多的解释，根据上面的所有信息就能够自然的构建出这样的一个&lt;code&gt;Register&lt;/code&gt;类型。&lt;/p&gt;
&lt;h4&gt;SIMD Register&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;aarch&lt;/code&gt;下的&lt;code&gt;SIMD&lt;/code&gt;寄存器总共有$32$个，$V_0 - V_{31}$，&lt;strong&gt;其用于保存标量(scalar)浮点指令的浮点操作数以及&lt;code&gt;SIMD&lt;/code&gt;指令的标量(scalar)和矢量(vector)操作数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当它们以特定的指令形式使用时，必须进一步限定名称以指示数据形状，即数据元素大小和寄存器内元素或通道的数量&lt;/strong&gt;。数据类型由对数据进行操作的指令助记符描述。数据类型不是由寄存器名描述的。数据类型是对每个寄存器或向量元素中的位的解释，无论这些是整数、浮点值、多项式还是加密哈希。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407031601724.png&quot; alt=&quot;SIMD and floating-point&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中的左图表示了&lt;code&gt;SIMD&lt;/code&gt;的标量寄存器名，&lt;strong&gt;其指定了操作标量数据时只访问高级&lt;code&gt;SIMD&lt;/code&gt;和浮点寄存器的低位。未使用的高位在读取时会被忽略，在写入时会被清零&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而右图表示向量寄存器名，在一开始我们就说过：&lt;strong&gt;必须进一步限定名称以指示数据形状&lt;/strong&gt;，也就是说，这里就给出了向量寄存器的数据形状，即数据元素大小和内部通道的数量。其中$V_n$在$V_0 - V_31$中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum VectorRegisterId : uint8_t {
    v0, v1, v2, v3, v4, v5, v6, v7,
    v8, v9, v10, v11, v12, v13, v14, v15,
    v16, v17, v18, v19, v20, v21, v22, v23,
    v24, v25, v26, v27, v28, v29, v30, v31,
    INVALID_VREG = 0xFF,
};

enum Scale {
    B = 0,
    H = 1,
    S = 2,
    D = 3,
    Q = 4,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;SIMD Code&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class VectorRegister {
public:
    explicit VectorRegister(VectorRegisterId reg, Scale scale = D) : reg_(reg), scale_(scale) {};

    inline VectorRegisterId GetId() const;
    inline bool IsValid() const;
    inline Scale GetScale() const;
    inline int GetRegSize() const;

private:
    VectorRegisterId reg_;
    Scale scale_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Operand&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;aarch&lt;/code&gt;中，&lt;code&gt;Operand&lt;/code&gt;的类型可以大致分为两类：&lt;code&gt;Data Processing&lt;/code&gt;和&lt;code&gt;Load and Store&lt;/code&gt;。为了处理这两种类型，我们在实际代码编写中需要构建两个类：&lt;code&gt;Operand&lt;/code&gt;和&lt;code&gt;MemoryOperand&lt;/code&gt;。其中，这两个类会由&lt;code&gt;Register&lt;/code&gt;、&lt;code&gt;Immediate&lt;/code&gt;、&lt;code&gt;Extend&lt;/code&gt;、&lt;code&gt;Shift&lt;/code&gt;和&lt;code&gt;AddrMode&lt;/code&gt;分别构成。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;Register&lt;/code&gt;类型这里就不再赘述，其主要&lt;strong&gt;管理了寄存器的编号和类型&lt;/strong&gt;，如果是向量寄存器，则会额外管理&lt;code&gt;标量(scalar)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;Immediate&lt;/code&gt;类型而言，只是一个最为简单的数据封装，分为&lt;code&gt;Immediate&lt;/code&gt;和&lt;code&gt;LogicImmediate&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Immediate {
public:
    Immediate(int64_t value) : value_(value) {}
    ~Immediate() = default;

    int64_t Value() const;
    
private:
    int64_t value_;
};

class LogicalImmediate {
public:
    static LogicalImmediate Create(uint64_t imm, int width);
    int Value() const;

    bool IsValid() const;
    bool Is64bit() const;
private:
    explicit LogicalImmediate(int value)
        : imm_(value) {}
    static const int InvalidLogicalImmediate = -1;
    int imm_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;code&gt;Extend&lt;/code&gt;则是作为&lt;code&gt;Immediate&lt;/code&gt;的扩展类型指导，&lt;strong&gt;用于标记是否需要进行立即数扩展和以哪一种方式进行扩展&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Extend : uint8_t {
    NO_EXTEND = 0xFF,
    UXTB = 0,   /* zero extend to byte */
    UXTH = 1,   /* zero extend to half word */
    UXTW = 2,   /* zero extend to word */
    UXTX = 3,   /* zero extend to 64bit */
    SXTB = 4,   /* sign extend to byte */
    SXTH = 5,   /* sign extend to half word */
    SXTW = 6,   /* sign extend to word */
    SXTX = 7,   /* sign extend to 64bit */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;Shift&lt;/code&gt;，是作为对应了位移和旋转操作(逻辑位移、算数位移、旋转以及条件性移动指令)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Shift : uint8_t {
    NO_SHIFT = 0xFF,
    LSL = 0x0,  // 逻辑左移
    LSR = 0x1,  // 逻辑右移
    ASR = 0x2,  // 算术右移
    ROR = 0x3,  // 旋转
    MSL = 0x4,  // 条件性移动
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;AddrMode&lt;/code&gt;来说，&lt;strong&gt;用于内存访问指令的寻址模式&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum  AddrMode {
    OFFSET,
    PREINDEX,
    POSTINDEX
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OFFSET&lt;/code&gt;：仅计算偏移量，但不更新基地址寄存器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PREINDEX&lt;/code&gt;：在访问之前计算偏移量并更新基地址寄存器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTINDEX&lt;/code&gt;：在访问之后计算偏移量并更新基地址寄存器。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;; OFFSET
LDR X0, [X1, #8]    ; 从地址 X1 + 8 处加载数据到 X0 寄存器
; PREINDEX
LDR X0, [X1, #8]!   ; 从地址 X1 + 8 处加载数据到 X0 寄存器，并更新 X1 为 X1 + 8
; POSTINDEX
LDR X0, [X1], #8    ; 从地址 X1 处加载数据到 X0 寄存器，并在加载后将 X1 更新为 X1 + 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由这些组合而成的&lt;code&gt;Operand&lt;/code&gt;能够为之后&lt;code&gt;Assembler&lt;/code&gt;提供相应的操作数，这样能够避免过多冗余数据的产生。&lt;/p&gt;
&lt;h4&gt;Data Operand&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class Operand {
public:
    Operand(Immediate imm)
        : reg_(RegisterId::INVALID_REG), extend_(Extend::NO_EXTEND), shift_(Shift::NO_SHIFT),
          shiftAmount_(0), immediate_(imm) {}
    Operand(Register reg, Shift shift = Shift::LSL, uint8_t shift_amount = 0)
        : reg_(reg), extend_(Extend::NO_EXTEND), shift_(shift), shiftAmount_(shift_amount), immediate_(0) {}
    Operand(Register reg, Extend extend, uint8_t shiftAmount = 0)
        : reg_(reg), extend_(extend), shift_(Shift::NO_SHIFT), shiftAmount_(shiftAmount), immediate_(0) {}
    ~Operand() = default;

    inline bool IsImmediate() const;
    inline bool IsShifted() const;
    inline bool IsExtended() const;

    inline Register Reg() const;
    inline Shift GetShiftOption() const;
    inline Extend GetExtendOption() const;
    inline uint8_t GetShiftAmount() const;
    inline int64_t ImmediateValue() const;
    inline Immediate GetImmediate() const;

private:
    Register reg_;
    Extend  extend_;
    Shift  shift_;
    uint8_t  shiftAmount_;
    Immediate immediate_;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Memory Operand&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class MemoryOperand {
public:
    MemoryOperand(Register base, Register offset, Extend extend, uint8_t  shiftAmount = 0)
        : base_(base), offsetReg_(offset), offsetImm_(0), addrmod_(AddrMode::OFFSET),
          extend_(extend), shift_(Shift::NO_SHIFT), shiftAmount_(shiftAmount) {}
    MemoryOperand(Register base, Register offset, Shift shift = Shift::NO_SHIFT, uint8_t  shiftAmount = 0)
        : base_(base), offsetReg_(offset), offsetImm_(0), addrmod_(AddrMode::OFFSET),
          extend_(Extend::NO_EXTEND), shift_(shift), shiftAmount_(shiftAmount) {}
    MemoryOperand(Register base, int64_t offset, AddrMode addrmod = AddrMode::OFFSET)
        : base_(base), offsetReg_(RegisterId::INVALID_REG), offsetImm_(offset), addrmod_(addrmod),
          extend_(Extend::NO_EXTEND), shift_(Shift::NO_SHIFT), shiftAmount_(0) {}
    ~MemoryOperand() = default;

    Register GetRegBase() const;
    bool IsImmediateOffset() const;
    Immediate GetImmediate() const;
    AddrMode GetAddrMode() const;
    Extend GetExtendOption() const;
    Shift GetShiftOption() const;
    uint8_t GetShiftAmount() const;
    Register GetRegisterOffset() const;

private:
    Register base_;
    Register offsetReg_;
    Immediate offsetImm_;
    AddrMode addrmod_;
    Extend extend_;
    Shift shift_;
    uint8_t shiftAmount_;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里与&lt;code&gt;Operand&lt;/code&gt;不同的是，因为&lt;code&gt;aarch64&lt;/code&gt;是以基址作为跳转的，因此我们需要一个基址寄存器和一个偏移量(这个偏移量可能是寄存器值也可能是立即数值)。还需要通过&lt;code&gt;AddrMode&lt;/code&gt;来判断具体的寻址模式。&lt;/p&gt;
&lt;h3&gt;Assembler Implement&lt;/h3&gt;
&lt;p&gt;在前面讲解&lt;code&gt;assembler.h&lt;/code&gt;时，我有提到：&lt;strong&gt;所有的实际架构的汇编都是基于&lt;code&gt;Assembler&lt;/code&gt;的，因此，我们实际上是要对&lt;code&gt;buffer_&lt;/code&gt;进行操作，对数据进行处理后，写入实际的汇编对应的机器码&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AssemblerAarch64 : public Assembler;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里还需要注意一点，在&lt;code&gt;ArkTS&lt;/code&gt;实现&lt;code&gt;aarch64&lt;/code&gt;时，使用了宏来确定了指令集的字段、位宽和掩码等数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define DECL_FIELDS_IN_INSTRUCTION(INSTNAME, FIELD_NAME, HIGHBITS, LOWBITS) \
static const uint32_t INSTNAME##_##FIELD_NAME##_HIGHBITS = HIGHBITS;  \
static const uint32_t INSTNAME##_##FIELD_NAME##_LOWBITS = LOWBITS;    \
static const uint32_t INSTNAME##_##FIELD_NAME##_WIDTH = ((HIGHBITS - LOWBITS) + 1); \
static const uint32_t INSTNAME##_##FIELD_NAME##_MASK = (((1 &amp;lt;&amp;lt; INSTNAME##_##FIELD_NAME##_WIDTH) - 1) &amp;lt;&amp;lt; LOWBITS);

#define DECL_INSTRUCTION_FIELDS(V)  \
    COMMON_REGISTER_FIELD_LIST(V)   \
    LDP_AND_STP_FIELD_LIST(V)       \
    LDR_AND_STR_FIELD_LIST(V)       \
    MOV_WIDE_FIELD_LIST(V)          \
    BITWISE_OP_FIELD_LIST(V)        \
    ADD_SUB_FIELD_LIST(V)           \
    COMPARE_OP_FIELD_LIST(V)        \
    BRANCH_FIELD_LIST(V)            \
    BRK_FIELD_LIST(V)
    
#define COMMON_REGISTER_FIELD_LIST(V)   \
    V(COMMON_REG, Rd, 4, 0)             \
    V(COMMON_REG, Rn, 9, 5)             \
    V(COMMON_REG, Rm, 20, 16)           \
    V(COMMON_REG, Rt, 4, 0)             \
    V(COMMON_REG, Rt2, 14, 10)          \
    V(COMMON_REG, Sf, 31, 31)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Add &amp;amp;&amp;amp; Sub&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;aarch64&lt;/code&gt;中，&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;sub&lt;/code&gt;是一组几乎相同的指令。因此我们主要针对&lt;code&gt;add&lt;/code&gt;进行讲解。&lt;code&gt;add&lt;/code&gt;有两种类型：&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;adds&lt;/code&gt;(我们不考虑&lt;code&gt;addg&lt;/code&gt;的实现)。其中，&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;adds&lt;/code&gt;各有三种情况：&lt;code&gt;extend register&lt;/code&gt;、&lt;code&gt;immediate&lt;/code&gt;和&lt;code&gt;shifted register&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;add(extend register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Add(extended register)&lt;/code&gt;&lt;strong&gt;指令将一个寄存器值和一个符号或零扩展的寄存器值相加，并可以选择进行左移，然后将结果写入目标寄存器&lt;/strong&gt;。由&lt;code&gt;&amp;lt;Rm&amp;gt;&lt;/code&gt;寄存器扩展而来的参数可以是字节(byte)、半字(halfword)、字(word)或双字(doubleword)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041036158.png&quot; alt=&quot;add extended register&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从&lt;code&gt;sf&lt;/code&gt;位可以判断指令的数据位宽：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果$sf = 0$，则表示$32bits$:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ADD &amp;lt;Wd|WSP&amp;gt;, &amp;lt;Wn|WSP&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果$sf = 1$，则表示$64bits$:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ADD &amp;lt;Xd|SP&amp;gt;, &amp;lt;Xn|SP&amp;gt;, &amp;lt;R&amp;gt;&amp;lt;m&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;*d&lt;/code&gt;而言，则为作为目的寄存器，而&lt;code&gt;*n&lt;/code&gt;是第一个源寄存器。而&lt;code&gt;*m&lt;/code&gt;自然就是第二个源寄存器。对于在$64bits$下的指令格式而言，&lt;code&gt;&amp;lt;R&amp;gt;&lt;/code&gt;用于表示寄存器前缀，&lt;code&gt;&amp;lt;m&amp;gt;&lt;/code&gt;表示其寄存器号：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;&amp;lt;R&amp;gt;&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;option = 00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;option = 010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;option = x11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;option = 10x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;option = 110&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;而言，则是和&lt;code&gt;Extend&lt;/code&gt;这一实现有关，如果有需要请自行向上检索&lt;code&gt;Extend&lt;/code&gt;或查阅相关手册。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;Rd&lt;/code&gt;或&lt;code&gt;Rn&lt;/code&gt;寄存器为$11111$即&lt;code&gt;WSP&lt;/code&gt;时，当指令为$32bits$时，且$option = 010$，则以&lt;code&gt;LSL&lt;/code&gt;作为扩展方式；如果为$64bits$，且$option = 011$，则以&lt;code&gt;LSL&lt;/code&gt;作为扩展方式。但如果$imm3 = 000$时，其可以被省略，否则其他情况下&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;是必须的；且在$option = 010$时($32bits$)，还需以&lt;code&gt;UXTW&lt;/code&gt;方式扩展；在$option = 011$时(64bits)，以&lt;code&gt;UXTW&lt;/code&gt;方式扩展。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;与&lt;code&gt;imm3&lt;/code&gt;字段有关，其用于左移的适当的偏移量，范围在$0 - 4$中(默认为$0$)。如果&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;字段不存在，那么&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;也就不存在；如果&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;为&lt;code&gt;LSL&lt;/code&gt;，那么&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;就必须存在；如果&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;存在但不为&lt;code&gt;LSL&lt;/code&gt;，那么&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;就是一个可选择的选项。&lt;/p&gt;
&lt;h5&gt;add(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;add(immediate)&lt;/code&gt;添加一个寄存器值和一个可选择性位移的立即数值，并写入到目的寄存器中。该指令可以用于&lt;code&gt;MOV(to/from SP)&lt;/code&gt;的别名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADD &amp;lt;Wd|WSP&amp;gt;, &amp;lt;Wn|WSP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;} ; 32-bits
ADD &amp;lt;Xd|SP&amp;gt;, &amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;}   ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041138935.png&quot; alt=&quot;add immediate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$sh = 0$且$imm12 = 0$，以及$R_n = SP$和$R_d = SP$时，可以用作&lt;code&gt;MOV&lt;/code&gt;的一个别名：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041142177.png&quot; alt=&quot;add mov&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于之前相同的字段名，这里不做过多介绍。&lt;code&gt;imm12&lt;/code&gt;和&lt;code&gt;imm3&lt;/code&gt;类似，不过这里的&lt;code&gt;imm12&lt;/code&gt;是立即数数据，用于计算，其范围在$0 - 4095$之间。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;shift&amp;gt;&lt;/code&gt;字段，则是会默认对&lt;code&gt;&amp;lt;imm12&amp;gt;&lt;/code&gt;字段进行&lt;code&gt;LSL #0&lt;/code&gt;的操作。如果$sh = 1$，则会进行&lt;code&gt;LSL #12&lt;/code&gt;的操作(即符号扩展)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case sh of
  when &apos;0&apos; imm = ZeroExtend(imm12, datasize);
  when &apos;1&apos; imm = ZeroExtend(imm12:Zeros(12), datasize);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;add(shifted register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;add(shifted register)&lt;/code&gt;添加一个寄存器值和一个可选择性位移的寄存器值，并写入到目的寄存器中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADD &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 32-bits
ADD &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041156195.png&quot; alt=&quot;add(shifted register)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;shift&amp;gt;&lt;/code&gt;字段，其会对第二个源寄存器进行处理：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LSL&lt;/td&gt;
&lt;td&gt;shift = 00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LSR&lt;/td&gt;
&lt;td&gt;shift = 01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASR&lt;/td&gt;
&lt;td&gt;shift = 10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;而&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;字段则是与&lt;code&gt;imm6&lt;/code&gt;字段相关，用于位移的位宽，其范围为$0 - 31$，默认为$0$。&lt;/p&gt;
&lt;h5&gt;adds&lt;/h5&gt;
&lt;p&gt;对于&lt;code&gt;adds&lt;/code&gt;指令而言，也有三种情况：&lt;code&gt;adds(extended register)&lt;/code&gt;、&lt;code&gt;adds(immediate)&lt;/code&gt;和&lt;code&gt;adds(shifted register)&lt;/code&gt;。与上面的&lt;code&gt;add&lt;/code&gt;不同的只有其中的&lt;code&gt;&amp;lt;S&amp;gt;&lt;/code&gt;标志，&lt;strong&gt;以及执行加法操作，同时更新条件标志&lt;/strong&gt;($N$, $Z$, $C$, $V$)。其余均无变化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041216169.png&quot; alt=&quot;adds&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; adds(extend register)
ADDS &amp;lt;Wd&amp;gt;, &amp;lt;Wn|WSP&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}   ; 32-bits
ADDS &amp;lt;Xd&amp;gt;, &amp;lt;Xn|SP&amp;gt;, &amp;lt;R&amp;gt;&amp;lt;m&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}  ; 64-bits

; adds(immediate)
ADDS &amp;lt;Wd&amp;gt;, &amp;lt;Wn|WSP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;}
ADDS &amp;lt;Xd&amp;gt;, &amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;}

; adds(shifted register)
ADDS &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}
ADDS &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;condition&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;N(Negative)&lt;/td&gt;
&lt;td&gt;$result \lt 0$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z(Zero)&lt;/td&gt;
&lt;td&gt;$result = 0$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C(Carry)&lt;/td&gt;
&lt;td&gt;carry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V(Overflow)&lt;/td&gt;
&lt;td&gt;over flow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;还有一点差距的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;adds(extended register)&lt;/code&gt;可用作&lt;code&gt;CMN(extended register)&lt;/code&gt;的别名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adds(immediate)&lt;/code&gt;可用作&lt;code&gt;CMN(immediate)&lt;/code&gt;的别名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adds(shifted register)&lt;/code&gt;可用作&lt;code&gt;CMN(shifted register)&lt;/code&gt;的别名&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;add code&lt;/h5&gt;
&lt;p&gt;首先，我们通过上面所说的宏定义，给出&lt;code&gt;add&lt;/code&gt;指令集对应的字段和位宽。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define ADD_SUB_FIELD_LIST(V)           \
    V(ADD_SUB, S, 29, 29)               \
    V(ADD_SUB, Sh, 22, 22)              \
    V(ADD_SUB, Imm12, 21, 10)           \
    V(ADD_SUB, Shift, 23, 22)           \
    V(ADD_SUB, ShiftAmount, 15, 10)     \
    V(ADD_SUB, ExtendOption, 15, 13)    \
    V(ADD_SUB, ExtendShift, 12, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;add&lt;/code&gt;和&lt;code&gt;sub&lt;/code&gt;这两个指令集相差无几，所以我们将这两个指令集的字段和处理逻辑同放在&lt;code&gt;AddSubImm&lt;/code&gt;和&lt;code&gt;AddSubReg&lt;/code&gt;中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::AddSubImm(AddSubOpCode op, const Register &amp;amp;rd, const Register &amp;amp;rn, bool setFlags, uint64_t imm)
{
    ASSERT(IsAddSubImm(imm));
    uint32_t shift = 0;
    const uint64_t IMM12_MASK = (1 &amp;lt;&amp;lt; ADD_SUB_Imm12_WIDTH) - 1;
    uint64_t imm12 = imm &amp;amp; (~IMM12_MASK);
    if (imm12 != 0) {
        shift = 1;
    } else {
        imm12 = imm;
    }
    uint32_t flags_field = ((setFlags ? 1 : 0) &amp;lt;&amp;lt; ADD_SUB_S_LOWBITS) &amp;amp; ADD_SUB_S_MASK;
    uint32_t imm_field = (imm12 &amp;lt;&amp;lt; ADD_SUB_Imm12_LOWBITS) &amp;amp; ADD_SUB_Imm12_MASK;
    uint32_t shift_field = (shift &amp;lt;&amp;lt; ADD_SUB_Sh_LOWBITS) &amp;amp; ADD_SUB_Sh_MASK;
    uint32_t code = Sf(!rd.IsW()) | op | flags_field | shift_field | imm_field | Rd(rd.GetId()) | Rn(rn.GetId());
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;add(immediate)&lt;/code&gt;的处理而言，我们需要对是否需要进行扩展和立即数进行提取，然后移动到对应的位上即可。&lt;code&gt;setFlags&lt;/code&gt;用于判断是&lt;code&gt;add&lt;/code&gt;还是&lt;code&gt;adds&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::AddSubReg(AddSubOpCode op, const Register &amp;amp;rd, const Register &amp;amp;rn,
                                 bool setFlags, const Operand &amp;amp;operand)
{
    uint32_t flags_field = ((setFlags ? 1 : 0) &amp;lt;&amp;lt; ADD_SUB_S_LOWBITS) &amp;amp; ADD_SUB_S_MASK;
    uint32_t code = 0;
    if (operand.IsShifted()) {
        uint32_t shift_field = ((operand.GetShiftOption()) &amp;lt;&amp;lt; ADD_SUB_Shift_LOWBITS) &amp;amp; ADD_SUB_Shift_MASK;
        uint32_t shift_amount = ((operand.GetShiftAmount()) &amp;lt;&amp;lt; ADD_SUB_ShiftAmount_LOWBITS) &amp;amp; ADD_SUB_ShiftAmount_MASK;
        ASSERT((op == ADD_Shift) | (op == SUB_Shift));
        code = Sf(!rd.IsW()) | op | flags_field | shift_field | Rm(operand.Reg().GetId()) |
                  shift_amount | Rn(rn.GetId()) | Rd(rd.GetId());
    } else {
        ASSERT((op == ADD_Extend) | (op == SUB_Extend));
        uint32_t extend_field =
            (operand.GetExtendOption() &amp;lt;&amp;lt; ADD_SUB_ExtendOption_LOWBITS) &amp;amp; ADD_SUB_ExtendOption_MASK;
        uint32_t extend_shift = (operand.GetShiftAmount() &amp;lt;&amp;lt; ADD_SUB_ExtendShift_LOWBITS) &amp;amp; ADD_SUB_ExtendShift_MASK;
        code = Sf(!rd.IsW()) | op | flags_field | Rm(operand.Reg().GetId()) | extend_field |
                  extend_shift | Rn(rn.GetId()) | Rd(rd.GetId());
    }
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里同时处理了&lt;code&gt;add(extend register)&lt;/code&gt;和&lt;code&gt;add(shifted register)&lt;/code&gt;这两种情况，可以看见，实际上就是将对应字段数据处理到真实机器码字段的对应位置即可。&lt;/p&gt;
&lt;h4&gt;CMP&lt;/h4&gt;
&lt;h5&gt;cmp&lt;/h5&gt;
&lt;p&gt;在&lt;code&gt;aarch64&lt;/code&gt;中，&lt;code&gt;CMP&lt;/code&gt;有三种类型，其都可以与&lt;code&gt;SUBS&lt;/code&gt;等价。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041505225.png&quot; alt=&quot;cmp&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; cmp(extend register)
CMP &amp;lt;Wn|WSP&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}  ; 32-bits
  equals SUBS WZR, &amp;lt;Wn|WSP&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}
CMP &amp;lt;Xn|SP&amp;gt;, &amp;lt;R&amp;gt;&amp;lt;m&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}} ; 64-bits
  equals SUBS XZR, &amp;lt;Xn|SP&amp;gt;, &amp;lt;R&amp;gt;&amp;lt;m&amp;gt;{, &amp;lt;extend&amp;gt; {#&amp;lt;amount&amp;gt;}}
  
; cmp(immediate)
CMP &amp;lt;Wn|WSP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;} ; 32-bits
  equals SUBS WZR, &amp;lt;Wn|WSP&amp;gt;, #&amp;lt;imm&amp;gt; {, &amp;lt;shift&amp;gt;}
CMP &amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;{, &amp;lt;shift&amp;gt;}  ; 64-bits
  equals SUBS XZR, &amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt; {, &amp;lt;shift&amp;gt;}
  
; cmp(shifted register)
CMP &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 32-bits
  equals SUBS WZR, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt; {, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}
CMP &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 64-bits
  equals SUBS XZR, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt; {, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;CBZ&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;CBZ&lt;/code&gt;&lt;strong&gt;将寄存器中的值与零进行比较，如果比较结果相等，则条件跳转到一个与当前程序计数器(PC)相对偏移的标签处。该指令提示这不是一个子程序调用或返回。这条指令不会影响条件标志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041517189.png&quot; alt=&quot;cbz&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CBZ &amp;lt;Wt&amp;gt;, &amp;lt;label&amp;gt; ; 32-bits
CBZ &amp;lt;Xt&amp;gt;, &amp;lt;label&amp;gt; ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;CBZ&lt;/code&gt;而言，最简单的实现就是直接跳转到立即数地址处，但是我们为了更加方便，还增加了一个&lt;code&gt;Label&lt;/code&gt;的重载，通过&lt;code&gt;Label&lt;/code&gt;计算其偏移量，然后进行跳转:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
void AssemblerAarch64::Cbz(const Register &amp;amp;rt, Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    Cbz(rt, offsetImm);
}

void AssemblerAarch64::Cbz(const Register &amp;amp;rt, int32_t imm)
{
    uint32_t code = Sf(!rt.IsW()) | BranchOpCode::CBZ | BranchImm19(imm) | rt.GetId();
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;CBNZ&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;CBNZ&lt;/code&gt;&lt;strong&gt;将寄存器中的值与零进行比较，如果比较结果不相等，则条件跳转到一个与当前程序计数器(PC)相对偏移的标签处。该指令提示这不是一个子程序调用或返回。这条指令不会影响条件标志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041527765.png&quot; alt=&quot;cbnz&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CBNZ &amp;lt;Wt&amp;gt;, &amp;lt;label&amp;gt; ; 32-bits
CBNZ &amp;lt;Xt&amp;gt;, &amp;lt;label&amp;gt; ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CBNZ&lt;/code&gt;的代码实现和&lt;code&gt;CBZ&lt;/code&gt;几乎相同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Cbnz(const Register &amp;amp;rt, Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    Cbnz(rt, offsetImm);
}

void AssemblerAarch64::Cbnz(const Register &amp;amp;rt, int32_t imm)
{
    uint32_t code = Sf(!rt.IsW()) | BranchOpCode::CBNZ | BranchImm19(imm) | rt.GetId();
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Branch&lt;/h4&gt;
&lt;h5&gt;B&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;B&lt;/code&gt;会&lt;strong&gt;引起一个无条件跳转到一个与程序计数器(PC)相对偏移的标签，并提示这不是一个子程序调用或返回&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041544668.png&quot; alt=&quot;b&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了支持度更高，我们也和&lt;code&gt;CMP&lt;/code&gt;一样，针对&lt;code&gt;Label&lt;/code&gt;做了一个重载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::B(Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    B(offsetImm);
}

void AssemblerAarch64::B(int32_t imm)
{
    uint32_t code = BranchOpCode::Branch | ((imm &amp;lt;&amp;lt; BRANCH_Imm26_LOWBITS) &amp;amp; BRANCH_Imm26_MASK);
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;B.cond&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;B.cond&lt;/code&gt;&lt;strong&gt;根据条件跳转到一个与程序计数器(PC)相对偏移的标签，并提示这不是一个子程序调用或返回&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041552227.png&quot; alt=&quot;B.cond&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;cond&amp;gt;&lt;/code&gt;而言，可以使用标准的&lt;code&gt;conditions&lt;/code&gt;，可以参考上面所实现的&lt;code&gt;Condition&lt;/code&gt;或对应的参考手册。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;相对于该指令地址的偏移量在$±1MB$范围内。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::B(Condition cond, Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    B(cond, offsetImm);
}

void AssemblerAarch64::B(Condition cond, int32_t imm)
{
    uint32_t code = BranchOpCode::BranchCond | BranchImm19(imm) | cond;
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Br&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Br&lt;/code&gt;&lt;strong&gt;无条件地跳转到寄存器中的地址，并提示这不是一个子程序返回&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041605898.png&quot; alt=&quot;Br&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Br(const Register &amp;amp;rn)
{
    uint32_t code = BranchOpCode::BR | Rn(rn.GetId());
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Blr&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Blr&lt;/code&gt;&lt;strong&gt;调用寄存器中的地址处的子程序，并将寄存器&lt;code&gt;X30&lt;/code&gt;设置为&lt;/strong&gt;$PC+4$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041613453.png&quot; alt=&quot;blr&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Blr(const Register &amp;amp;rn)
{
    ASSERT(!rn.IsW());
    uint32_t code = CallOpCode::BLR | Rn(rn.GetId());
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Bl&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Bl&lt;/code&gt;&lt;strong&gt;跳转到一个与程序计数器(PC)相对偏移的位置，并将寄存器&lt;code&gt;X30&lt;/code&gt;设置为$PC+4$。它提示这是一个子程序调用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407041618598.png&quot; alt=&quot;Bl&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Bl(Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    Bl(offsetImm);
}

void AssemblerAarch64::Bl(int32_t imm)
{
    uint32_t code = CallOpCode::BL | ((imm &amp;lt;&amp;lt; BRANCH_Imm26_LOWBITS) &amp;amp; BRANCH_Imm26_MASK);
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;TB&lt;/h4&gt;
&lt;h5&gt;TBNZ&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;TBNZ&lt;/code&gt;&lt;strong&gt;将通用寄存器中的一个位与零进行比较，并在比较结果不等于零时，按PC相对偏移有条件地分支到一个标签。该指令提供一个提示，表示这不是子程序调用或返回。该指令不会影响条件标志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407051013408.png&quot; alt=&quot;TBNZ&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TBNZ &amp;lt;R&amp;gt;&amp;lt;t&amp;gt;, #&amp;lt;imm&amp;gt;, &amp;lt;label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里的&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;指的是第几位数据，其被$b5:b40$所共同组成。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;和&lt;code&gt;imm14&lt;/code&gt;相关，其范围为$+/-32KB$&lt;/p&gt;
&lt;p&gt;在实际的处理中，我们依旧提供了&lt;code&gt;imm&lt;/code&gt;和&lt;code&gt;Label&lt;/code&gt;的两个实现版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Tbnz(const Register &amp;amp;rt, int32_t bitPos, Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    Tbnz(rt, bitPos, offsetImm);
}

void AssemblerAarch64::Tbnz(const Register &amp;amp;rt, int32_t bitPos, int32_t imm)
{
    uint32_t b5 = (bitPos &amp;lt;&amp;lt; (BRANCH_B5_LOWBITS - 5)) &amp;amp; BRANCH_B5_MASK;
    uint32_t b40 = (bitPos &amp;lt;&amp;lt; BRANCH_B40_LOWBITS) &amp;amp; BRANCH_B40_MASK;
    uint32_t imm14 = (imm &amp;lt;&amp;lt;BRANCH_Imm14_LOWBITS) &amp;amp; BRANCH_Imm14_MASK;
    uint32_t code = b5 | BranchOpCode::TBNZ | b40 | imm14 | rt.GetId();
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;TBZ&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;TBZ&lt;/code&gt;&lt;strong&gt;将测试位的值与零进行比较，并在比较结果等于零时，按PC相对偏移有条件地分支到一个标签。该指令提供一个提示，表示这不是子程序调用或返回。该指令不会影响条件标志&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407051019765.png&quot; alt=&quot;TBZ&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TBZ &amp;lt;R&amp;gt;&amp;lt;t&amp;gt;, #&amp;lt;imm&amp;gt;, &amp;lt;label&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与&lt;code&gt;TBNZ&lt;/code&gt;的实际处理情况相同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Tbz(const Register &amp;amp;rt, int32_t bitPos, Label *label)
{
    int32_t offsetImm = LinkAndGetInstOffsetToLabel(label);
    // 2 : 2 means 4 bytes aligned.
    offsetImm &amp;gt;&amp;gt;= 2;
    Tbz(rt, bitPos, offsetImm);
}

void AssemblerAarch64::Tbz(const Register &amp;amp;rt, int32_t bitPos, int32_t imm)
{
    uint32_t b5 = (bitPos &amp;lt;&amp;lt; (BRANCH_B5_LOWBITS - 5)) &amp;amp; BRANCH_B5_MASK;
    uint32_t b40 = (bitPos &amp;lt;&amp;lt; BRANCH_B40_LOWBITS) &amp;amp; BRANCH_B40_MASK;
    uint32_t imm14 = (imm &amp;lt;&amp;lt; BRANCH_Imm14_LOWBITS) &amp;amp; BRANCH_Imm14_MASK;
    uint32_t code = b5 | BranchOpCode::TBZ | b40 | imm14 | rt.GetId();
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;TST&lt;/h4&gt;
&lt;h5&gt;TST(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;TST(immediate)&lt;/code&gt;&lt;strong&gt;指令的作用是对两个操作数执行按位与(AND)运算，但不保存结果。相反，它会根据运算结果设置或清除条件标志寄存器中的相关标志位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407051033270.png&quot; alt=&quot;TST(immediate)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;TST(immediate)&lt;/code&gt;的一个假名则是&lt;code&gt;ADDS(immediate)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; sf = 0 &amp;amp;&amp;amp; N = 0
TST &amp;lt;Wn&amp;gt;, #&amp;lt;imm&amp;gt;
  equals ANDS WZR, &amp;lt;Wn&amp;gt;, #&amp;lt;imm&amp;gt;
  
; sf = 1
TST &amp;lt;Xn&amp;gt;, #&amp;lt;imm&amp;gt;
  equals ANDS XZR, &amp;lt;Xn&amp;gt;, #&amp;lt;imm&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;需要注意：如果在$32-bits$下，其由$imms:immr$组成；在$64-bits$下由$N:imms:immr$组成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Tst(const Register &amp;amp;rn, const LogicalImmediate &amp;amp;imm)
{
    Ands(Register(Zero, rn.GetType()), rn, imm);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;TST(shifted register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;TST(shifted register)&lt;/code&gt;&lt;strong&gt;对寄存器值和可选移位的寄存器值执行按位与操作。它根据结果更新条件标志，并丢弃结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407051040715.png&quot; alt=&quot;TST(shifted register)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;TST(shifted register)&lt;/code&gt;的一个假名则是&lt;code&gt;ADDS(shifted register)&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TST &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 32-bits
  equals ANDS WZR, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}

TST &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 64-bits
  equals ANDS XZR, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;&amp;lt;shift&amp;gt;&lt;/code&gt;的可选项为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407051048196.png&quot; alt=&quot;shift&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Logic&lt;/h4&gt;
&lt;p&gt;对于&lt;code&gt;ArkTS&lt;/code&gt;中的&lt;code&gt;aarch64&lt;/code&gt;架构的逻辑部分的汇编实现而言，主要分为&lt;code&gt;Orr&lt;/code&gt;和&lt;code&gt;And&lt;/code&gt;两类。更重要的是，&lt;code&gt;Orr&lt;/code&gt;和&lt;code&gt;And&lt;/code&gt;的内部实现实际上是十分相似的，因此我们会使用&lt;code&gt;BitWiseOP&lt;/code&gt;这样的函数来统一实现其内部逻辑。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;Orr&lt;/code&gt;和&lt;code&gt;And&lt;/code&gt;的具体差异主要在&lt;code&gt;opcode&lt;/code&gt;上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum BitwiseOpCode {
    AND_Imm      = 0x12000000,
    AND_Shift    = 0x0a000000,
    ANDS_Imm     = 0x72000000,
    ANDS_Shift   = 0x6a000000,
    ORR_Imm      = 0x32000000,
    ORR_Shift    = 0x2a000000,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;immediate&lt;/code&gt;下的指令，我们只需要直接传入对应的&lt;code&gt;opcode&lt;/code&gt;即可，不需要做多余的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::BitWiseOpImm(BitwiseOpCode op, const Register &amp;amp;rd, const Register &amp;amp;rn, uint64_t imm)
{
    uint32_t code = Sf(!rd.IsW()) | op | imm | Rn(rn.GetId()) | Rd(rd.GetId());
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而对于&lt;code&gt;shifted&lt;/code&gt;的指令，我们就需要通过之前实现的&lt;code&gt;Operand&lt;/code&gt;对&lt;code&gt;shift&lt;/code&gt;操作进行具体的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
void AssemblerAarch64::BitWiseOpShift(BitwiseOpCode op, const Register &amp;amp;rd, const Register &amp;amp;rn, const Operand &amp;amp;operand)
{
    uint32_t shift_field = (operand.GetShiftOption() &amp;lt;&amp;lt; BITWISE_OP_Shift_LOWBITS) &amp;amp; BITWISE_OP_Shift_MASK;
    uint32_t shift_amount = (operand.GetShiftAmount() &amp;lt;&amp;lt; BITWISE_OP_ShiftAmount_LOWBITS) &amp;amp; BITWISE_OP_ShiftAmount_MASK;
    uint32_t code = Sf(!rd.IsW()) | op | shift_field | Rm(operand.Reg().GetId()) |
                       shift_amount | Rn(rn.GetId()) | Rd(rd.GetId());
    EmitU32(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的类似于&lt;code&gt;BITWISE_OP_Shift_LOWBITS&lt;/code&gt;是通过&lt;code&gt;BITWISE_OP_FIELD_LIST&lt;/code&gt;宏实现的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define BITWISE_OP_FIELD_LIST(V)            \
    V(BITWISE_OP, N, 22, 22)                \
    V(BITWISE_OP, Immr, 21, 16)             \
    V(BITWISE_OP, Shift, 23, 22)            \
    V(BITWISE_OP, Imms, 15, 10)             \
    V(BITWISE_OP, ShiftAmount, 15, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;ORR(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Orr immediate&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个立即数寄存器值进行按位或(包含或)操作，并将结果写入目标寄存器。该指令可用作别名指令&lt;/strong&gt;&lt;code&gt;MOV(bitmask immediate)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161552892.png&quot; alt=&quot;Orr immediate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$Rn = 11111$且&lt;code&gt;!MoveWidePreferred(sf, N, imms, immr)&lt;/code&gt;时，可以用作&lt;code&gt;Mov(bitmask immediate)&lt;/code&gt;的假名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ORR &amp;lt;Wd|WSP&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;imm&amp;gt;  ; 32-bits
ORR &amp;lt;Xd|SP&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;imm&amp;gt;   ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;而言，再$32bits$下由&lt;code&gt;imms:immr&lt;/code&gt;组成；而$64bits$由&lt;code&gt;N:imms:immr&lt;/code&gt;组成。&lt;/p&gt;
&lt;h5&gt;ORR(shifted register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Orr(shifted register)&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个可选择进行位移的寄存器值进行按位或(包含或)操作，并将结果写入目标寄存器。该指令可用作别名指令&lt;/strong&gt;&lt;code&gt;MOV(register)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161557139.png&quot; alt=&quot;orr shifted register&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$shite = 00$且$imm6 = 000000$且$Rn = 11111$时，可以用作&lt;code&gt;Mov(register)&lt;/code&gt;的假名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ORR &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 32-bits
ORR &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;AND(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;And(immediate)&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个立即数值进行按位与操作，并将结果写入目标寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161507612.png&quot; alt=&quot;and immediate&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AND &amp;lt;Wd|WSP&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;imm&amp;gt;  ; 32-bits
AND &amp;lt;Xd|SP&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;imm&amp;gt;   ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;来说，在$32bits$下由&lt;code&gt;imms:immr&lt;/code&gt;构成；而$64bits$下由&lt;code&gt;N:imms:immr&lt;/code&gt;构成。&lt;/p&gt;
&lt;h5&gt;AND(shifted register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;And(shifted register)&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个可选择进行位移的寄存器值进行按位与操作，并将结果写入目标寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161513255.png&quot; alt=&quot;and shifted&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AND &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 32-bits
AND &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;} ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;ANDS(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Ands immediate&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个立即数值进行按位与操作，并将结果写入目标寄存器。它根据结果更新条件标志位。该指令可用作别名指令&lt;/strong&gt;&lt;code&gt;TST(immediate)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161523841.png&quot; alt=&quot;ands immediate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$Rd = 11111$时，可以作为&lt;code&gt;TST(immediate)&lt;/code&gt;的别名使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANDS &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;imm&amp;gt; ; 32-bits
ANDS &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;imm&amp;gt; ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;而言，再$32bits$下由&lt;code&gt;imms:immr&lt;/code&gt;组成；而$64bits$由&lt;code&gt;N:imms:immr&lt;/code&gt;组成。&lt;/p&gt;
&lt;h5&gt;ANDS(shifted register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Ands shifted register&lt;/code&gt;&lt;strong&gt;对一个寄存器值和一个可选择进行位移的寄存器值进行按位与操作，并将结果写入目标寄存器。它根据结果更新条件标志位。该指令可用作别名指令&lt;/strong&gt;&lt;code&gt;TST(shifted register)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161536291.png&quot; alt=&quot;ands shifted&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$Rd = 11111$时，可以作为&lt;code&gt;TST(shifted register)&lt;/code&gt;的别名使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANDS &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}  ; 32-bits
ANDS &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;{, &amp;lt;shift&amp;gt; #&amp;lt;amount&amp;gt;}  ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Shift&lt;/h4&gt;
&lt;h5&gt;LSR(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LSR(immediate)&lt;/code&gt;&lt;strong&gt;将一个寄存器值向右移动固定的位数，移入零位，并将结果写入目标寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;该指令是&lt;code&gt;UBFM&lt;/code&gt;指令的别名。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在本描述中，编码的命名与&lt;code&gt;UBFM&lt;/code&gt;的编码相匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UBFM&lt;/code&gt;的描述提供了操作伪代码、任何&lt;code&gt;CONSTRAINED UNPREDICTABLE&lt;/code&gt;行为和该指令的操作信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161614990.png&quot; alt=&quot;lsr immediate&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LSR &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;shift&amp;gt; ; 32-bits
  equals UBFM &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;shift&amp;gt;, #31
  when sf == 0 &amp;amp;&amp;amp; N == 0 &amp;amp;&amp;amp; imms == 011111 
  
LSR &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;shift&amp;gt; ; 64-bits
  equals UBFM &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;shift&amp;gt;, #63
  when sf == 1 &amp;amp;&amp;amp; N == 1 &amp;amp;&amp;amp; imms == 111111 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LSR(register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LSR(register)&lt;/code&gt;&lt;strong&gt;根据一个可变的位数将一个寄存器值右移，移入零位，并将结果写入目标寄存器。第二个源寄存器除以数据大小所得的余数确定了将第一个源寄存器右移的位数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;该指令是&lt;code&gt;LSRV&lt;/code&gt;指令的别名。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在本描述中，编码的命名与&lt;code&gt;LSRV&lt;/code&gt;的编码相匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LSRV&lt;/code&gt;的描述提供了操作伪代码、任何&lt;code&gt;CONSTRAINED UNPREDICTABLE&lt;/code&gt;行为和该指令的操作信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161618114.png&quot; alt=&quot;lsr register&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LSR &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;  ; 32-bits
  equals LSRV &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;
  when sf == 0 
  
LSR &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;  ; 64-bits
  equals LSRV &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;
  when sf == 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LSL(register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LSL(register)&lt;/code&gt;&lt;strong&gt;根据一个可变的位数将一个寄存器值左移，移入零位，并将结果写入目标寄存器。第二个源寄存器除以数据大小所得的余数确定了将第一个源寄存器左移的位数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;该指令是&lt;code&gt;LSLV&lt;/code&gt;指令的别名。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在本描述中，编码的命名与&lt;code&gt;LSLV&lt;/code&gt;的编码相匹配。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LSLV&lt;/code&gt;的描述提供了操作伪代码、任何&lt;code&gt;CONSTRAINED UNPREDICTABLE&lt;/code&gt;行为和该指令的操作信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161622992.png&quot; alt=&quot;lsl register&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LSL &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;  ; 32-bits
  equals LSRV &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, &amp;lt;Wm&amp;gt;
  when sf == 0 
  
LSL &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;  ; 64-bits
  equals LSRV &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, &amp;lt;Xm&amp;gt;
  when sf == 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;UBFM&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;Unsigned Bitfield Move&lt;/code&gt;&lt;strong&gt;通常通过其别名访问，这些别名在反汇编时始终优先选择&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果$imms \ge immr$，则将源寄存器中从位&lt;code&gt;immr&lt;/code&gt;开始的长度为($imms - immr + 1$)位的位域复制到目标寄存器的最低有效位。&lt;/li&gt;
&lt;li&gt;如果$imms \lt immr$，则将源寄存器的最低有效位中长度为($imms + 1$)位的位域复制到目标寄存器的位位置($regsize - immr$)处，其中&lt;code&gt;regsize&lt;/code&gt;是目标寄存器的大小，可以是$32$位或$64$位。&lt;/li&gt;
&lt;li&gt;在这两种情况下，位域下方和上方的目标位都设置为零。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161610214.png&quot; alt=&quot;UBFM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UBFM&lt;/code&gt;的使用场景通常为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;位字段操作：从一个寄存器中提取特定的位字段，并将其复制到另一个寄存器中，以进行后续的处理或使用。&lt;/li&gt;
&lt;li&gt;数据解析：当处理二进制数据时，可以使用 UBFM 指令来提取特定的位字段，并将其转换为有意义的数值或状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该指令可用作别名指令&lt;code&gt;LSL(immediate)&lt;/code&gt;、&lt;code&gt;LSR(immediate)&lt;/code&gt;、&lt;code&gt;UBFIZ&lt;/code&gt;、&lt;code&gt;UBFX&lt;/code&gt;、&lt;code&gt;UXTB&lt;/code&gt;和&lt;code&gt;UXTH&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161612084.png&quot; alt=&quot;alias ubfm&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UBFM &amp;lt;Wd&amp;gt;, &amp;lt;Wn&amp;gt;, #&amp;lt;immr&amp;gt;, #&amp;lt;imms&amp;gt; ; 32-bits
UBFM &amp;lt;Xd&amp;gt;, &amp;lt;Xn&amp;gt;, #&amp;lt;immr&amp;gt;, #&amp;lt;imms&amp;gt; ; 64-bits
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Store and Load&lt;/h4&gt;
&lt;h5&gt;LDP&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDP&lt;/code&gt;&lt;strong&gt;指令通过基础寄存器值和立即偏移量计算地址，从内存中加载两个32位字或两个64位双字，并将它们写入两个寄存器中&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;AddrMode&lt;/code&gt;中我们提到，内存模型通常有三种：&lt;code&gt;Post&lt;/code&gt;、&lt;code&gt;Pre&lt;/code&gt;和&lt;code&gt;Signed offset&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161653917.png&quot; alt=&quot;memory mode&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
LDP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt; ; 32-bits, when opc == 00 
LDP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt; ; 64-bits, when opc == 10 

; Pre-index
LDP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]! ; 32-bits, when opc == 00 
LDP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]! ; 64-bits, when opc == 10 

; Signed offset
LDP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}] ; 32-bits, when opc == 00 
LDP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}] ; 64-bits, when opc == 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;有以下的解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于32位&lt;code&gt;Post-index&lt;/code&gt;和32位&lt;code&gt;Pre-index&lt;/code&gt;变体：有符号的立即字节偏移量在范围$-256 ~ 252$之间，是&lt;code&gt;4&lt;/code&gt;的倍数，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm/4$。&lt;/li&gt;
&lt;li&gt;对于32位&lt;code&gt;Signed offset&lt;/code&gt;变体：可选的有符号立即字节偏移量在范围$-256 ~ 252$之间，是&lt;code&gt;4&lt;/code&gt;的倍数，默认为&lt;code&gt;0&lt;/code&gt;，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm/4$。&lt;/li&gt;
&lt;li&gt;对于64位&lt;code&gt;Post-index&lt;/code&gt;和64位&lt;code&gt;Pre-index&lt;/code&gt;变体：有符号的立即字节偏移量在范围$-512 ~ 504$之间，是&lt;code&gt;8&lt;/code&gt;的倍数，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm/8$。&lt;/li&gt;
&lt;li&gt;对于64位&lt;code&gt;Signed offset&lt;/code&gt;变体：可选的有符号立即字节偏移量在范围$-512 ~ 504$之间，是&lt;code&gt;8&lt;/code&gt;的倍数，默认为&lt;code&gt;0&lt;/code&gt;，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm/8$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而在实际的代码处理中，也能够很好的体现上面所说的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum LoadStorePairOpCode {
    LDP_Post     = 0x28c00000,
    LDP_Pre      = 0x29c00000,
    LDP_Offset   = 0x29400000,
};

if (sf) {
    imm &amp;gt;&amp;gt;= 3;  // 3: 64 RegSise, imm/8 to remove trailing zeros
} else {
    imm &amp;gt;&amp;gt;= 2;  // 2: 32 RegSise, imm/4 to remove trailing zeros
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Vector LDP&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDP(SIMD&amp;amp;FP)&lt;/code&gt;&lt;strong&gt;指令从内存中加载一对&lt;code&gt;SIMD&amp;amp;FP&lt;/code&gt;寄存器。用于加载的地址是根据基础寄存器值和可选的立即偏移量计算得出的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根据&lt;code&gt;CPACR_EL1&lt;/code&gt;、&lt;code&gt;CPTR_EL2&lt;/code&gt;和&lt;code&gt;CPTR_EL3&lt;/code&gt;寄存器中的设置以及当前的安全状态和异常级别，执行该指令的尝试可能会被捕获。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161713110.png&quot; alt=&quot;ldp simd&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
LDP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 32-bits, when opc == 00(S)
LDP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 64-bits, when opc == 01(D)
LDP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 128-bits, when opc == 10(Q)

; Pre-index
LDP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!  ; 32-bits, when opc == 00(S)
LDP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!  ; 64-bits, when opc == 01(D)
LDP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!  ; 128-bits, when opc == 10(Q)

; Signed offset
LDP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}] ; 32-bits, when opc == 00(S)
LDP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}] ; 64-bits, when opc == 01(D)
LDP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}] ; 128-bits, when opc == 10(Q)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;&amp;lt;imm&amp;gt;&lt;/code&gt;有以下解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于32位&lt;code&gt;Post-index&lt;/code&gt;和32位&lt;code&gt;Pre-index&lt;/code&gt;变体：有符号的立即字节偏移量在范围$-256 ~ 252$之间，是&lt;code&gt;4&lt;/code&gt;的倍数，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 4$。&lt;/li&gt;
&lt;li&gt;对于32位&lt;code&gt;Signed offset&lt;/code&gt;变体：可选的有符号立即字节偏移量在范围$-256 ~ 252$之间，是&lt;code&gt;4&lt;/code&gt;的倍数，默认为&lt;code&gt;0&lt;/code&gt;，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 4$。&lt;/li&gt;
&lt;li&gt;对于64位&lt;code&gt;Post-index&lt;/code&gt;和64位&lt;code&gt;Pre-index&lt;/code&gt;变体：有符号的立即字节偏移量在范围$-512 ~ 504$之间，是&lt;code&gt;8&lt;/code&gt;的倍数，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 8$。&lt;/li&gt;
&lt;li&gt;对于64位&lt;code&gt;Signed offset&lt;/code&gt;变体：可选的有符号立即字节偏移量在范围$-512 ~ 504$之间，是&lt;code&gt;8&lt;/code&gt;的倍数，默认为&lt;code&gt;0&lt;/code&gt;，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 8$。&lt;/li&gt;
&lt;li&gt;对于128位&lt;code&gt;Post-index&lt;/code&gt;和128位&lt;code&gt;Pre-index&lt;/code&gt;变体：有符号的立即字节偏移量在范围$-1024 ~ 1008$之间，是&lt;code&gt;16&lt;/code&gt;的倍数，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 16$。&lt;/li&gt;
&lt;li&gt;对于128位&lt;code&gt;Signed offset&lt;/code&gt;变体：可选的有符号立即字节偏移量在范围$-1024 ~ 1008$之间，是&lt;code&gt;16&lt;/code&gt;的倍数，默认为&lt;code&gt;0&lt;/code&gt;，并在&lt;code&gt;imm7&lt;/code&gt;字段中编码为$imm / 16$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其具体视线中，也能够体现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch (vt.GetScale()) {
    case S:
        // 2 : 2 means remove trailing zeros
        imm &amp;gt;&amp;gt;= 2;
        break;
    case D:
        // 3 : 3 means remove trailing zeros
        imm &amp;gt;&amp;gt;= 3;
        break;
    case Q:
        // 4 : 4 means remove trailing zeros
        imm &amp;gt;&amp;gt;= 4;
        break;
    default:
        LOG_ECMA(FATAL) &amp;lt;&amp;lt; &quot;this branch is unreachable&quot;;
        UNREACHABLE();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDR(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDR(immediate)&lt;/code&gt;&lt;strong&gt;指令从内存中加载一个字或双字，并将其写入一个寄存器。用于加载的地址是根据基础寄存器和立即偏移量计算得出的。无符号偏移量变体会将立即偏移量的值按照所访问值的大小进行缩放，然后再将其加到基础寄存器值上&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161730888.png&quot; alt=&quot;ldr immediate&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
LDR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;    ; 32-bits, when size == 10 
LDR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;    ; 64-bits, when size == 11

; Pre-index
LDR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!   ; 32-bits, when size == 10
LDR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!   ; 64-bits, when size == 11

; Signed offset
LDR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]  ; 32-bits, when size == 10
LDR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]  ; 64-bits, when size == 11 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际中，我们在&lt;code&gt;Ldr&lt;/code&gt;函数中共同处理了两种情况，这里给出&lt;code&gt;ldr(immediate)&lt;/code&gt;的处理逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (operand.IsImmediateOffset()) {
    uint64_t imm = GetImmOfLdr(operand, scale, regX);
    bool isSigned = operand.GetAddrMode() != AddrMode::OFFSET;
    // 30: 30bit indicate the size of LDR Reg, and Ldrb and Ldrh do not need it
    uint32_t instructionCode = ((regX &amp;amp;&amp;amp; (scale == Scale::Q)) &amp;lt;&amp;lt; 30) | op | LoadAndStoreImm(imm, isSigned) |
                                Rn(operand.GetRegBase().GetId()) | Rt(rt.GetId());
    EmitU32(instructionCode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDR(register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDR(register)&lt;/code&gt;&lt;strong&gt;指令根据基础寄存器值和偏移寄存器值计算一个地址，从内存中加载一个字，并将其写入一个寄存器。偏移寄存器值可以选择进行位移和扩展操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161747916.png&quot; alt=&quot;ldr register&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, (&amp;lt;Wm&amp;gt;|&amp;lt;Xm&amp;gt;){, &amp;lt;extend&amp;gt; {&amp;lt;amount&amp;gt;}}] ; 32-bits, when size == 10 
LDR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, (&amp;lt;Wm&amp;gt;|&amp;lt;Xm&amp;gt;){, &amp;lt;extend&amp;gt; {&amp;lt;amount&amp;gt;}}] ; 64-bits, when size == 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;是索引扩展/位移说明符(&lt;code&gt;index extend/shift specifier&lt;/code&gt;），默认为&lt;code&gt;LSL(左移)&lt;/code&gt;。当&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;被省略时，必须为&lt;code&gt;LSL&lt;/code&gt;省略&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;。其解码为&lt;code&gt;option&lt;/code&gt;字段。它可以具有以下值:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UXTW    when option = 010
LSL     when option = 011
SXTW    when option = 110
SXTX    when option = 111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;amount&amp;gt;&lt;/code&gt;是唯一索引量，当&lt;code&gt;&amp;lt;extend&amp;gt;&lt;/code&gt;不为&lt;code&gt;LSL&lt;/code&gt;时可选。当允许被选中时，其默认值为&lt;code&gt;0&lt;/code&gt;，被解码为&lt;code&gt;S&lt;/code&gt;字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 32-bits
#0 when S = 0
#2 when S = 1

; 64-bits
#0 when S = 0
#3 when S = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDRB(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDRB(immediate)&lt;/code&gt;&lt;strong&gt;从内存中加载一个字节，对其进行零扩展，并将结果写入寄存器。用于加载的地址是从基寄存器和立即偏移量计算出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161800512.png&quot; alt=&quot;ldrb immediate&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
LDRB &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;

; Pre-index
LDRB &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!

; Signed offset
LDRB &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;LDRB&lt;/code&gt;的具体实现，直接通过&lt;code&gt;LDR&lt;/code&gt;进行实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Ldrb(const Register &amp;amp;rt, const MemoryOperand &amp;amp;operand)
{
    ASSERT(rt.IsW());
    Ldr(rt, operand, Scale::B);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDRB(register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDRB(register)&lt;/code&gt;&lt;strong&gt;从基寄存器值和偏移寄存器值计算一个地址，从内存中加载一个字节，对其进行零扩展，并将其写入寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161802805.png&quot; alt=&quot;ldrb regsiter&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Extended register 
LDRB &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, (&amp;lt;Wm&amp;gt;|&amp;lt;Xm&amp;gt;), &amp;lt;extend&amp;gt; {&amp;lt;amount&amp;gt;}]  ; when option != 011 .

; Shifted register
LDRB &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, &amp;lt;Xm&amp;gt;{, LSL &amp;lt;amount&amp;gt;}]  ; when option == 011 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDRH(immediate)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDRH(immediate)&lt;/code&gt;&lt;strong&gt;从内存中加载半字，对其进行零扩展，并将结果写入寄存器。用于加载的地址是从基寄存器和立即偏移量计算出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161805014.png&quot; alt=&quot;LDRH(immediate)&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
LDRH &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;

; Pre-index
LDRH &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!

; Unsigned offset
LDRH &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其内部实现由&lt;code&gt;LDR&lt;/code&gt;构成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Ldrh(const Register &amp;amp;rt, const MemoryOperand &amp;amp;operand)
{
    ASSERT(rt.IsW());
    Ldr(rt, operand, Scale::H);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDRH(register)&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDRH(register)&lt;/code&gt;&lt;strong&gt;从基寄存器值和偏移寄存器值计算地址，从内存中加载半字，对其进行零扩展，并将其写入寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161807499.png&quot; alt=&quot;LDRH register&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDRH &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, (&amp;lt;Wm&amp;gt;|&amp;lt;Xm&amp;gt;){, &amp;lt;extend&amp;gt; {&amp;lt;amount&amp;gt;}}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;LDUR&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;LDUR&lt;/code&gt;&lt;strong&gt;从基寄存器和直接偏移量中计算一个地址，从内存中加载一个32位字或64位双字，对其进行零扩展，并将其写入寄存器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161809487.png&quot; alt=&quot;ldur&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDUR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;simm&amp;gt;}]   ; 32-bits, when size == 10
LDUR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;simm&amp;gt;}]   ; 64-bits, when size == 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其具体实现为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Ldur(const Register &amp;amp;rt, const MemoryOperand &amp;amp;operand)
{
    bool regX = !rt.IsW();
    uint32_t op = LDUR_Offset;
    ASSERT(operand.IsImmediateOffset());
    uint64_t imm = static_cast&amp;lt;uint64_t&amp;gt;(operand.GetImmediate().Value());
    // 30: 30bit indicate the size of LDUR Reg
    uint32_t instructionCode = (regX &amp;lt;&amp;lt; 30) | op | LoadAndStoreImm(imm, true) |
                               Rn(operand.GetRegBase().GetId()) | Rt(rt.GetId());
    EmitU32(instructionCode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;STP&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;STP&lt;/code&gt;&lt;strong&gt;从基寄存器值和直接偏移量计算地址，并从两个寄存器中存储两个32位字或两个64位双字到计算的地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161818457.png&quot; alt=&quot;stp&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
STP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 32-bits, when opc == 00 
STP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 64-bits, when opc == 01

; Pre-index
STP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!   ; 32-bits, when opc == 00 
STP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!   ; 64-bits, when opc == 01

; Signed offset
STP &amp;lt;Wt1&amp;gt;, &amp;lt;Wt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}]   ; 32-bits, when opc == 00 
STP &amp;lt;Xt1&amp;gt;, &amp;lt;Xt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}]   ; 64-bits, when opc == 01
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;STP&lt;/code&gt;的实际实现，主要代码片段为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case OFFSET:
    op = LoadStorePairOpCode::STP_Offset;
    break;
case PREINDEX:
    op = LoadStorePairOpCode::STP_Pre;
    break;
case POSTINDEX:
    op = LoadStorePairOpCode::STP_Post;
    break;
    
if (sf) {
    imm &amp;gt;&amp;gt;= 3;  // 3: 64 RegSise, imm/8 to remove trailing zeros
} else {
    imm &amp;gt;&amp;gt;= 2;  // 2: 32 RegSise, imm/4 to remove trailing zeros
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Vector STP&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;STP(SIMD&amp;amp;FP)&lt;/code&gt;&lt;strong&gt;将一对&lt;code&gt;SIMD&amp;amp;FP&lt;/code&gt;寄存器存储到内存中。用于存储的地址是从基寄存器值和立即偏移量计算出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;根据&lt;code&gt;CPACR_EL1&lt;/code&gt;、&lt;code&gt;CPTR_EL2&lt;/code&gt;和&lt;code&gt;CPTR_EL3&lt;/code&gt;寄存器中的设置，以及当前的安全状态和Exception级别，执行指令的尝试可能会被捕获。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161822544.png&quot; alt=&quot;stp(simd&amp;amp;fp)&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
STP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 32-bits, when opc == 00(S)
STP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 64-bits, when opc == 01(D)
STP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;imm&amp;gt;   ; 128-bits, when opc == 10(Q)

; Pre-index
STP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!   ; 32-bits, when opc == 00(S)
STP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!   ; 64-bits, when opc == 01(D)
STP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;imm&amp;gt;]!   ; 128-bits, when opc == 10(Q)

; Signed offset
STP &amp;lt;St1&amp;gt;, &amp;lt;St2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}]   ; 32-bits, when opc == 00(S)
STP &amp;lt;Dt1&amp;gt;, &amp;lt;Dt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}]   ; 64-bits, when opc == 01(D)
STP &amp;lt;Qt1&amp;gt;, &amp;lt;Qt2&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;imm&amp;gt;}]   ; 128-bits, when opc == 10(Q)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其代码的主要部分为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case OFFSET:
    op = LoadStorePairOpCode::STP_V_Offset;
    break;
case PREINDEX:
    op = LoadStorePairOpCode::STP_V_Pre;
    break;
case POSTINDEX:
    op = LoadStorePairOpCode::STP_V_Post;
    break;
case S:
    // 2 : 2 means remove trailing zeros
    imm &amp;gt;&amp;gt;= 2;
    break;
case D:
    // 3 : 3 means remove trailing zeros
    imm &amp;gt;&amp;gt;= 3;
    break;
case Q:
    // 4 : 4 means remove trailing zeros
    imm &amp;gt;&amp;gt;= 4;
    break;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;STR&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;STR&lt;/code&gt;&lt;strong&gt;将一个字或双字从寄存器存储到存储器中。用于存储的地址是从基寄存器和立即偏移量计算出来的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161931064.png&quot; alt=&quot;str immediate&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Post-index
STR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;    ; 32-bits, when size == 10
STR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;], #&amp;lt;simm&amp;gt;    ; 64-bits, when size == 11

; Pre-index
STR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!   ; 32-bits, when size == 10
STR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;, #&amp;lt;simm&amp;gt;]!   ; 64-bits, when size == 11

; Unsigned offset
STR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]  ; 32-bits, when size == 10
STR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;pimm&amp;gt;}]  ; 64-bits, when size == 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;ArkTS&lt;/code&gt;中，主要实现了&lt;code&gt;str(immediate)&lt;/code&gt;其内部的主要实现为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case OFFSET:
    op = LoadStoreOpCode::STR_Offset;
    if (regX) {
        imm &amp;gt;&amp;gt;= 3;   // 3:  64 RegSise, imm/8 to remove trailing zeros
    } else {
        imm &amp;gt;&amp;gt;= 2;  // 2: 32 RegSise, imm/4 to remove trailing zeros
    }
    isSigned = false;
    break;
case PREINDEX:
    op = LoadStoreOpCode::STR_Pre;
    break;
case POSTINDEX:
    op = LoadStoreOpCode::STR_Post;
    break;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;STUR&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;STUR&lt;/code&gt;&lt;strong&gt;从基本寄存器值和直接偏移量计算一个地址，并从寄存器中存储一个32位字或64位双字到计算出的地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407161935126.png&quot; alt=&quot;stur&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;STUR &amp;lt;Wt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;simm&amp;gt;}]   ; 32-bits, when size == 10
STUR &amp;lt;Xt&amp;gt;, [&amp;lt;Xn|SP&amp;gt;{, #&amp;lt;simm&amp;gt;}]   ; 64-bits, when size == 11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其内部实现为:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void AssemblerAarch64::Ldur(const Register &amp;amp;rt, const MemoryOperand &amp;amp;operand)
{
    bool regX = !rt.IsW();
    uint32_t op = LDUR_Offset;
    ASSERT(operand.IsImmediateOffset());
    uint64_t imm = static_cast&amp;lt;uint64_t&amp;gt;(operand.GetImmediate().Value());
    // 30: 30bit indicate the size of LDUR Reg
    uint32_t instructionCode = (regX &amp;lt;&amp;lt; 30) | op | LoadAndStoreImm(imm, true) |
                               Rn(operand.GetRegBase().GetId()) | Rt(rt.GetId());
    EmitU32(instructionCode);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;至此，&lt;code&gt;ArkTS&lt;/code&gt;的&lt;code&gt;Aarch&lt;/code&gt;汇编的分析到此结束了。&lt;/p&gt;
</content:encoded></item><item><title>ArkTS: compile runtime_core and ets_*</title><link>https://chenmiaoi.github.io/2024/07/01/ArkTS-compile-runtime-core-and-ets/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/07/01/ArkTS-compile-runtime-core-and-ets/</guid><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这一节中，我会介绍&lt;code&gt;ArkTS&lt;/code&gt;的四大组件以及如何编译并成功运行&lt;code&gt;ArkTS&lt;/code&gt;的前端和运行时。&lt;/p&gt;
&lt;h2&gt;ArkTS Components&lt;/h2&gt;
&lt;p&gt;方舟编译器(ArkCompiler)是为支持多种编程语言、多种芯片平台的联合编译、运行而设计的统一编译运行时平台。它支持包括动态类型和静态类型语言在内的多种编程语言，如JS、TS、ArkTS；它是支撑OpenHarmony系统成为打通手机、PC、平板、电视、车机和智能穿戴等多种设备的操作系统的编译运行时底座。&lt;/p&gt;
&lt;p&gt;从结构上看，ArkCompiler主要分成两个部分：编译工具链与运行时。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407011731363.png&quot; alt=&quot;编译工具链架构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ArkCompiler的编译工具链以ArkTS/TS/JS源码作为输入，将其编译生成为abc(ArkCompiler Bytecode，即方舟字节码)文件。其主要是通过在编译中产生的二进制程序&lt;code&gt;es2abc&lt;/code&gt;进行工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;es2abc hello.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407011732850.png&quot; alt=&quot;运行时架构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ArkCompiler运行时直接运行字节码文件，实现对应语言规范的语义逻辑。&lt;/p&gt;
&lt;p&gt;主要由四个子系统组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Core Subsystem
&lt;ul&gt;
&lt;li&gt;Core Subsystem主要由与语言无关的基础运行库组成，包括承载字节码的File组件、支持Debugger的Tooling组件、负责适配系统调用的Base库组件等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Execution Subsystem
&lt;ul&gt;
&lt;li&gt;Execution Subsystem包含执行字节码的解释器、快速路径内联缓存、以及抓取运行时信息的Profiler。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Compiler Subsystem
&lt;ul&gt;
&lt;li&gt;Compiler Subsystem包含Stub编译器、基于IR的编译优化框架和代码生成器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Runtime subsystem
&lt;ul&gt;
&lt;li&gt;Runtime Subsystem包含了ArkTS/TS/JS运行相关的模块。&lt;/li&gt;
&lt;li&gt;内存管理：对象分配器与垃圾回收器(并发标记和部分内存压缩的CMS-GC和Partial-Compressing-GC)
&lt;ul&gt;
&lt;li&gt;分析工具：DFX工具、cpu和heap的profiling工具&lt;/li&gt;
&lt;li&gt;并发管理：actor并发模型中的abc文件管理器&lt;/li&gt;
&lt;li&gt;标准库：Ecmascript规范定义的标准库、高效的container容器库与对象模型&lt;/li&gt;
&lt;li&gt;其他：异步工作队列、TypeScript类型加载、跟C++交互的JSNAPI接口等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在本次任务中，我们着重关注&lt;code&gt;Compiler Subsystem&lt;/code&gt;中的&lt;code&gt;stub&lt;/code&gt;和&lt;code&gt;assember&lt;/code&gt;。以下给出了具体的一个工作目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/arkcompiler
├── ets_runtime       # ArkTS运行时组件
    ├── ecmascript
        ├── compiler  # Assember解析目录
        └── stubs     # stubs
├── runtime_core      # 运行时公共组件
├── ets_frontend      # ArkTS语言的前端工具
└── toolchain         # ArkTS工具链
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Compile ETS&lt;/h2&gt;
&lt;p&gt;本次编译过程中的项目组织结构为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── arkcompiler
│         ├── ets_frontend
│         ├── ets_runtime
│         ├── runtime_core
│         └── toolchain
├── ark.py -&amp;gt; arkcompiler/toolchain/build/compile_script/ark.py
├── build
│         ├── build_scripts
│         ├── config
│         ├── core
│         ├── docs
│         ├── ...
│         ├── gn_helpers.py
│         ├── ohos_var.gni
│         ├── prebuilts_download_config.json
│         ├── prebuilts_download.py
│         ├── prebuilts_download.sh
│         └── zip.py
├── developtools
├── download_packages
├── kernel
├── kernel_linux_patches -&amp;gt; kernel/linux/patches/
├── out
│         ├── install
│         ├── lib
│         ├── ... 
│         ├── ohos-riscv64
│         ├── ohos-riscv64-install
│         ├── riscv64.release
│         └── x64.release
├── packages
├── prebuilts
├── README.md
├── third_party
└── toolchain
    ├── lldb-mi
    └── llvm-project
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dependency &amp;amp;&amp;amp; Compile&lt;/h3&gt;
&lt;p&gt;在正式开始编译之前，我们首先需要解决依赖配置。目前，我所使用的环境为&lt;code&gt;Ubuntu 22.04 LTS&lt;/code&gt;发行版，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install git-lfs git bison flex gnupg build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses-dev x11proto-core-dev libx11-dev libc++1 lib32z1-dev ccache libgl1-mesa-dev libxml2-utils xsltproc unzip m4 libtinfo5 bc npm genext2fs liblz4-tool libssl-dev ruby openjdk-8-jre-headless gdb python3-pip libelf-dev libxcursor-dev libxrandr-dev libxinerama-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是参考了&lt;a href=&quot;https://gitee.com/openharmony/arkcompiler_ets_runtime/blob/master/docs/README_zh.md&quot;&gt;方舟运行时使用指南&lt;/a&gt;的依赖配置。&lt;/p&gt;
&lt;p&gt;需要注意的是，在&lt;code&gt;Ubuntu18.04&lt;/code&gt;或&lt;code&gt;Ubuntu20.04&lt;/code&gt;时，可能还存在&lt;code&gt;python&lt;/code&gt;(通常新式的系统都直接支持&lt;code&gt;python3&lt;/code&gt;)。因此需要通过符号链接生成一个&lt;code&gt;python&lt;/code&gt;可执行文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s python3 python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成这一步后，我们需要下载一些额外的依赖，这一点可以通过项目中的下载脚本自动配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;toolchain/llvm-project/llvm-build/env_prepare.sh
arkcompiler/toolchain/build/prebuilts_download/prebuilts_download.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载完毕后，就可以通过&lt;code&gt;build.py&lt;/code&gt;进行自动化构建&lt;code&gt;ArkTS&lt;/code&gt;编译所需要的&lt;code&gt;clang&lt;/code&gt;，在此处，我们需要的&lt;code&gt;x64&lt;/code&gt;和&lt;code&gt;riscv64&lt;/code&gt;架构的构建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 ./toolchain/llvm-project/llvm-build/build.py \
	--no-build-arm \
	--no-build-aarch64 \
	--no-build-mipsel \
	--no-build windows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我当前的配置为&lt;code&gt;11th Gen Intel i5-11400H (8) @ 2.688GHz&lt;/code&gt;，使用的虚拟机配置为八核16G内存，通常编译&lt;code&gt;clang&lt;/code&gt;的时长在三个半小时左右。编译完成后，需要将产出的&lt;code&gt;clang&lt;/code&gt;拷贝到&lt;code&gt;prebuilts&lt;/code&gt;文件夹中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mv prebuilts/clang/ohos/linux-x86_64/llvm prebuilts/clang/ohos/linux-x86_64/llvm.origin 
cp -r out/install/linux-x86_64/clang-dev/ prebuilts/clang/ohos/linux-x86_64/llvm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了构建在&lt;code&gt;riscv64&lt;/code&gt;架构下适配的&lt;code&gt;ArkTS&lt;/code&gt;，我们需要单独构建&lt;code&gt;riscv64&lt;/code&gt;架构的&lt;code&gt;LLVM lib&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 toolchain/llvm-project/llvm-build/build-ohos-riscv64.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;code&gt;build-ohos-riscv64.py&lt;/code&gt;通常需要半个小时左右，完成后，需要将&lt;code&gt;riscv64&lt;/code&gt;架构的&lt;code&gt;LLVM lib&lt;/code&gt;产出拷贝到&lt;code&gt;prebuilts&lt;/code&gt;目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/{build,llvm}
cp -r out/ohos-riscv64-install/include prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/llvm/
cp -r out/ohos-riscv64/{include,lib} prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/build/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，前期准备就完成了。现在，就可以开始执行&lt;code&gt;ark.py&lt;/code&gt;脚本来编译对应架构下的&lt;code&gt;ets_*&lt;/code&gt;工具链了。&lt;/p&gt;
&lt;p&gt;编译&lt;code&gt;x86_64&lt;/code&gt;的&lt;code&gt;ArkTS&lt;/code&gt;前端、运行时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 ./arkcompiler/toolchain/build/compile_script/ark.py x64.release --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译&lt;code&gt;riscv64&lt;/code&gt;的&lt;code&gt;ArkTS&lt;/code&gt;前端、运行时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 ./arkcompiler/toolchain/build/compile_script/ark.py riscv64.release --verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需要注意的是，在未修改的依赖项中&lt;/strong&gt;，&lt;code&gt;riscv64&lt;/code&gt;&lt;strong&gt;架构下编译不会对&lt;/strong&gt;&lt;code&gt;ets_frontend&lt;/code&gt;&lt;strong&gt;生效&lt;/strong&gt;。关于如何启用和修改&lt;code&gt;bug&lt;/code&gt;，将在后面进行介绍。&lt;/p&gt;
&lt;p&gt;然后你可以看到在&lt;code&gt;out&lt;/code&gt;目录中的一个目录树结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;out/
├── install
├── ...
├── ohos-riscv64
├── ohos-riscv64-install
├── riscv64.release
└── x64.release
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的目标生成文件位于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;out/x64.release/
├── args.gn
├── arkcompiler
│         ├── ets_frontend
│         ├── ets_runtime
│         ├── runtime_core
│         └── toolchain
├── ...
├── gen
│         ├── arkcompiler
│         ├── isa
│         └── libpandabase
└── toolchain.ninja

out/riscv64.release/
├── args.gn
├── arkcompiler
│         ├── ets_runtime
│         ├── runtime_core
│         └── toolchain
├── ...
├── gen
│         ├── arkcompiler
│         ├── isa
│         └── libpandabase
└── toolchain.ninja
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们也可以发现，&lt;code&gt;riscv64&lt;/code&gt;架构下的&lt;code&gt;arkcompiler&lt;/code&gt;目录中缺少了&lt;code&gt;ets_frontend&lt;/code&gt;的生成。&lt;/p&gt;
&lt;h2&gt;Dependency Detail &amp;amp;&amp;amp; Fix&lt;/h2&gt;
&lt;p&gt;在上面我们发现，我们实际真正需要的&lt;code&gt;riscv64&lt;/code&gt;的&lt;code&gt;ArkTS&lt;/code&gt;工具链中，&lt;code&gt;ets_frontend&lt;/code&gt;缺失，并且，关于&lt;code&gt;stub&lt;/code&gt;实际上也没有真正编译。在这一小节中，我会对&lt;code&gt;arkcompiler&lt;/code&gt;的&lt;code&gt;GN&lt;/code&gt;依赖进行分析，然后逐步修复错误使得&lt;code&gt;ets_frontend&lt;/code&gt;至少可用。&lt;/p&gt;
&lt;h3&gt;Dependency Detail&lt;/h3&gt;
&lt;p&gt;我们是通过&lt;code&gt;ark.py&lt;/code&gt;进行自动化构建的，现在对&lt;code&gt;ark.py&lt;/code&gt;进行溯源，其原始目录位于:&lt;code&gt;arkcompiler/toolchain/build/compile_script/&lt;/code&gt;，然后进入&lt;code&gt;ark.py&lt;/code&gt;中进行解析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def build_for_gn_target(self, out_path: str, gn_args: list, arg_list: list, log_file_name: str):
    # prepare log file
    build_log_path = os.path.join(out_path, log_file_name)
    str_to_build_log = &quot;================================\nbuild_time: {0}\nbuild_target: {1}\n\n&quot;.format(
        str_of_time_now(), &quot; &quot;.join(arg_list))
    _write(build_log_path, str_to_build_log, &quot;a&quot;)
    # gn command
    print(&quot;=== gn gen start ===&quot;)
    code = call_with_output(
        &quot;{0} gen {1} --args=\&quot;{2}\&quot;&quot;.format(
            self.gn_binary_path, out_path, &quot; &quot;.join(gn_args).replace(&quot;\&quot;&quot;, &quot;\\\&quot;&quot;)),
        build_log_path)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;build_for_gn_target&lt;/code&gt;是主要构建函数，通过&lt;code&gt;gn&lt;/code&gt;来进行构建，而&lt;code&gt;gn&lt;/code&gt;会通过同级目录下的&lt;code&gt;.gn&lt;/code&gt;文件进行配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# The location of the build configuration file.
buildconfig = &quot;//arkcompiler/toolchain/build/config/BUILDCONFIG.gn&quot;

# The source root location.
root = &quot;//arkcompiler/toolchain/build/core/gn&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们暂时不需要关注&lt;code&gt;buildconfig&lt;/code&gt;中的配置，我们主要的构建逻辑位于&lt;code&gt;root&lt;/code&gt;中。并且，在开启&lt;code&gt;riscv64&lt;/code&gt;后，我们需要首先知道这个信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;current_os=ohos,   current_cpu=riscv64
host_os=linux,      host_cpu=x64
target_os=ohos,    target_cpu=riscv64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先&lt;code&gt;//root&lt;/code&gt;设置了一个基本的默认构建逻辑，如果&lt;code&gt;host_os&lt;/code&gt;不在&lt;code&gt;MacOS&lt;/code&gt;上运行时，则对于&lt;code&gt;ArkTS&lt;/code&gt;的四个组件都应该构建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group(&quot;default&quot;) {
  if (host_os != &quot;mac&quot;) {
    deps = [
      &quot;:ets_frontend&quot;,
      &quot;:ets_runtime&quot;,
      &quot;:runtime_core&quot;,
      &quot;:toolchain&quot;,
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;ets_runtime&lt;/h4&gt;
&lt;p&gt;然后，我们来看一看&lt;code&gt;ets_runtime&lt;/code&gt;的构建逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group(&quot;ets_runtime&quot;) {
  deps = [
    &quot;$js_root:libark_jsruntime&quot;,
    &quot;$js_root/ecmascript/js_vm:ark_js_vm&quot;,
    &quot;$js_root/ecmascript/quick_fix:quick_fix&quot;,
  ]
  if ((target_os == &quot;linux&quot; &amp;amp;&amp;amp; target_cpu == &quot;x64&quot;) ||
      (target_cpu == &quot;arm64&quot; &amp;amp;&amp;amp; target_os == &quot;ohos&quot;)) {
    deps += [
      &quot;$js_root/ecmascript/compiler:ark_aot_compiler&quot;,
      &quot;$js_root/ecmascript/compiler:ark_stub_compiler&quot;,
      &quot;$js_root/ecmascript/pgo_profiler/prof_dump:profdump&quot;,
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，在以来中，我们会生成两个可执行文件&lt;code&gt;ark_js_vm&lt;/code&gt;和&lt;code&gt;quick_fix&lt;/code&gt;。我们主要关注&lt;code&gt;ark_js_vm&lt;/code&gt;的生成。在后面我们发现，这里只对于&lt;code&gt;x64&lt;/code&gt;架构的&lt;code&gt;ets_runtime&lt;/code&gt;提供了&lt;code&gt;ark_stub_compiler&lt;/code&gt;提供了构建依赖，而对于&lt;code&gt;riscv64&lt;/code&gt;并没有，因此，&lt;strong&gt;我猜测，在后续实现时，我们需要在这里添加对于&lt;/strong&gt;&lt;code&gt;riscv64-ohos&lt;/code&gt;&lt;strong&gt;架构的支持&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是目前而言，因为&lt;code&gt;riscv stub&lt;/code&gt;的实现暂时有些许问题，因此先忽视。在这里，&lt;code&gt;ets_runtime&lt;/code&gt;都是能够正常编译的。&lt;/p&gt;
&lt;h4&gt;ets_frontend&lt;/h4&gt;
&lt;p&gt;现在，我们来看一看&lt;code&gt;ets_frontend&lt;/code&gt;的构建逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group(&quot;ets_frontend&quot;) {
  if ((target_os == &quot;linux&quot; &amp;amp;&amp;amp; target_cpu == &quot;x64&quot;) || target_os == &quot;mingw&quot;) {
    deps = [
      &quot;$ets_frontend_root/es2panda:es2panda&quot;,
      &quot;$ets_frontend_root/merge_abc:merge_abc&quot;,
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们看到，&lt;code&gt;ets_frontend&lt;/code&gt;只支持了&lt;code&gt;x64&lt;/code&gt;的完整支持，因此需要做一定的修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group(&quot;ets_frontend&quot;) {
  # FIX: Add the ohos-riscv64 ets_frontend dependency
  if ((target_os == &quot;linux&quot; &amp;amp;&amp;amp; target_cpu == &quot;x64&quot;) || 
      (target_os == &quot;mingw&quot;) || 
      (target_os == &quot;ohos&quot; &amp;amp;&amp;amp; target_cpu == &quot;riscv64&quot;)) {
    deps = [
      &quot;$ets_frontend_root/es2panda:es2panda&quot;,
      &quot;$ets_frontend_root/merge_abc:merge_abc&quot;,
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后我们尝试运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERROR at //arkcompiler/toolchain/build/templates/cxx/cxx.gni:182:7: Script returned non-zero exit code.
      exec_script(&quot;$build_root/templates/cxx/external_deps_handler.py&quot;,
      ^----------
      
See //arkcompiler/toolchain/build/third_party_gn/protobuf/BUILD.gn:85:1: whence it was called.
ohos_static_library(&quot;protobuf_lite_static&quot;) {
^--------------------------------------------

See //arkcompiler/ets_frontend/merge_abc/BUILD.gn:148:5: which caused the file to be included.
    &quot;$ark_third_party_root/protobuf:protobuf_lite_static&quot;,
    ^----------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就会产生报错，通过查看错误日志后发现，这里是有一个依赖不能够被&lt;code&gt;external_deps_handler.py&lt;/code&gt;处理，该依赖是&lt;code&gt;[hilog:libhilog]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通过日志发现，最先出现的调用栈为：&lt;code&gt;arkcompiler/ets_frontend/merge_abc/BUILD.gn:148:5&lt;/code&gt;，因此可以查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Cause Proto -&amp;gt; hilog:libhilog
deps = [
    &quot;:arkcompiler_generate_proto&quot;,
    &quot;$ark_third_party_root/protobuf:protobuf_lite_static&quot;,
    &quot;$ark_third_party_root/protobuf:protobuf_static&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现是&lt;code&gt;protobuf_lite_static&lt;/code&gt;依赖出现问题，因此进入&lt;code&gt;arkcompiler/toolchain/build/third_party_gn/protobuf/BUILD.gn:85:1&lt;/code&gt;中进行查看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ohos_static_library(&quot;protobuf_lite_static&quot;) {
    ...
    if (!is_mingw) {
        if (current_toolchain != host_toolchain) {
            # target build, not host build
            defines = [ &quot;HAVE_HILOG&quot; ]
            external_deps = [ &quot;hilog:libhilog&quot; ]
        }
    } else {
        defines = [ &quot;_FILE_OFFSET_BITS_SET_LSEEK&quot; ]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现，在&lt;code&gt;ohos_static_library(&quot;protobuf_lite_static&quot;)&lt;/code&gt;逻辑中，因为这里只是简单的判断&lt;code&gt;current_toolchain != host_toolchain&lt;/code&gt;然后就添加了&lt;code&gt;external_deps = [ &quot;hilog:libhilog&quot; ]&lt;/code&gt;从而导致无法处理&lt;code&gt;[ hilog:libhilog ]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此，我们做出以下修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# To Append the `enable_hilog` identifier
import(&quot;//arkcompiler/runtime_core/ark_config.gni&quot;)

ohos_static_library(&quot;protobuf_lite_static&quot;) {
    ...
    if (!is_mingw) {
        if (enable_hilog &amp;amp;&amp;amp; current_toolchain != host_toolchain) {
            # target build, not host build
            defines = [ &quot;HAVE_HILOG&quot; ]
            external_deps = [ &quot;hilog:libhilog&quot; ]
        }
    } else {
        defines = [ &quot;_FILE_OFFSET_BITS_SET_LSEEK&quot; ]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在再次运行&lt;code&gt;riscv64&lt;/code&gt;的构建脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 ./arkcompiler/toolchain/build/compile_script/ark.py riscv64.release --verbose

...

Done. Made 3926 targets from 543 files in 661ms

=== gn gen success! ===
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;runtime_core &amp;amp;&amp;amp; toolchain&lt;/h4&gt;
&lt;p&gt;对于这两个结构而言，没有太多需要注意的地方，只有&lt;code&gt;runtime_core&lt;/code&gt;构建出的两个可执行文件在后续可能会用上&lt;code&gt;ark_asm&lt;/code&gt;和&lt;code&gt;ark_disasm&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group(&quot;runtime_core&quot;) {
  deps = [
    &quot;$ark_root/assembler:ark_asm&quot;,
    &quot;$ark_root/disassembler:ark_disasm&quot;,
  ]
}

group(&quot;toolchain&quot;) {
  if (target_cpu != &quot;mipsel&quot;) {
    deps = [
      &quot;$toolchain_root/inspector:ark_debugger&quot;,
      &quot;$toolchain_root/inspector:connectserver_debugger&quot;,
      &quot;$toolchain_root/tooling:libark_ecma_debugger&quot;,
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Compile Show&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202407011726816.png&quot; alt=&quot;ArkTS result&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>xv6: The Uart</title><link>https://chenmiaoi.github.io/2024/06/27/xv6-The-Uart/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/06/27/xv6-The-Uart/</guid><pubDate>Thu, 27 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上一节中，我们介绍了&lt;code&gt;xv6&lt;/code&gt;中的自旋锁。在这一节，我们将从&lt;code&gt;uart&lt;/code&gt;开始介绍，&lt;code&gt;uart&lt;/code&gt;是&lt;code&gt;xv6&lt;/code&gt;中&lt;code&gt;console&lt;/code&gt;和&lt;code&gt;printf&lt;/code&gt;的最基础的模块，用于串口通信。而对于&lt;code&gt;xv6-riscv&lt;/code&gt;的&lt;code&gt;uart&lt;/code&gt;模块的构成则是参考了&lt;a href=&quot;http://byterunner.com/16550.html&quot;&gt;16550a-uart&lt;/a&gt;或&lt;a href=&quot;https://caro.su/msx/ocm_de1/16550.pdf&quot;&gt;16550a-uart SIDSA&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406271027143.png&quot; alt=&quot;Uart&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Uart&lt;/h2&gt;
</content:encoded></item><item><title>xv6: The Spin Lock</title><link>https://chenmiaoi.github.io/2024/06/24/xv6-The-Spin-Lock/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/06/24/xv6-The-Spin-Lock/</guid><pubDate>Mon, 24 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一节中，我简单介绍了内核态中的入口函数&lt;code&gt;main()&lt;/code&gt;。如果按照入口函数中的初始化顺序，理应讲解&lt;code&gt;consoleinit()&lt;/code&gt;，但是位于&lt;code&gt;consoleinit&lt;/code&gt;中有涉及到其余部分的介绍，为了每个章节的独立性和可读性，并且为了避免一次性知识的复杂程度过高，这里选择率先讲解&lt;code&gt;spinlock&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在这一节中，我并不打算详细介绍什么是“锁”，只会简单的提一点最为基本的概念以及锁的种类。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;锁是一种用于控制多线程或多进程环境中资源访问的同步机制。它的主要作用是防止数据竞争和保持数据一致性。当多个线程或进程需要访问共享资源时，锁可以确保同一时间只有一个线程或进程可以访问该资源，从而避免并发操作带来的问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一般而言，常见的锁有：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;种类&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;互斥锁(Mutex Lock)&lt;/td&gt;
&lt;td&gt;适用于需要严格防止并发访问的场景，如更新共享变量或文件操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;读写锁 (Read-Write Lock)&lt;/td&gt;
&lt;td&gt;适用于读多写少的场景，例如缓存系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;递归锁 (Recursive Lock)&lt;/td&gt;
&lt;td&gt;适用于需要递归调用且每次调用都需要获取同一锁的场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自旋锁 (Spin Lock)&lt;/td&gt;
&lt;td&gt;适用于锁持有时间很短的场景，因为自旋锁会消耗 CPU 资源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;而在&lt;code&gt;xv6-riscv&lt;/code&gt;中，我们主要实现的锁便是自旋锁。&lt;code&gt;xv6&lt;/code&gt;的目标是尽可能简单地展示操作系统的基本原理。&lt;strong&gt;自旋锁的实现相对简单，不需要处理复杂的上下文切换或线程调度问题&lt;/strong&gt;。实现其他类型的锁（如互斥锁、读写锁）需要更复杂的调度机制和上下文切换管理，这会增加内核的复杂性。而自旋锁只需要简单的检查和自旋等待，不需要操作系统调度线程的切换，简化了内核的实现。&lt;/p&gt;
&lt;p&gt;同时，在自旋锁中，由于线程在等待锁的时候不会进行上下文切换，这在某些情况下反而更加高效，特别是在临界区执行时间极短的情况下。避免上下文切换的开销，可以提升系统性能。&lt;/p&gt;
&lt;h2&gt;Lock&lt;/h2&gt;
&lt;p&gt;多个$CPU$的&lt;code&gt;xv6&lt;/code&gt;共享物理内存，并且利用共享来维护所有$cpu$读取和写入的数据结构。而共享数据则是增加了一个$CPU$在读取数据的同时，另一个$CPU$正在更新该数据结构的可能性，甚至多个$CPU$同时更新。哪怕在单核中，内核也可能不断的在$CPU$中切换大量线程，使得它们交替执行。&lt;strong&gt;并发一词指的是由于多处理器并行性、线程切换或中断而导致多条指令流交错的情况&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;内核中总是充斥着许多并行数据，内核设计者们为了让内核使用并行以增加其性能和响应性，不得不允许了大量并发的存在。为了不造成严重错误，又设计出并发控制技术来解决这一问题。&lt;/p&gt;
&lt;p&gt;这一节主要涉及到并发控制及技术中最为广泛使用的一项技术：锁。锁提供了互斥性，确保了一次只能一个$CPU$享有锁。也就是说，如果一个数据结构使用锁来保护其数据，那么锁就使得一次只有一个$CPU$能够使用该数据。尽管锁是一种极其简单有效的机制，但是大量使用锁会使得性能下降，因为它使其变为了单独的序列化执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406251020809.png&quot; alt=&quot;Simplified SMP architecture&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Races&lt;/h2&gt;
&lt;p&gt;考虑一下这个例子：两个具有退出子进程的进程在两个不同的$CPU$上调用&lt;code&gt;wait&lt;/code&gt;，而&lt;code&gt;wait&lt;/code&gt;会释放这两个子进程的内存。因此，对于这两个$CPU$而言，内核会调用&lt;code&gt;kfree&lt;/code&gt;来回收这两个子进程的内存页。&lt;code&gt;xv6&lt;/code&gt;的内核分配器维护了一个链表&lt;code&gt;kalloc()&lt;/code&gt;，&lt;code&gt;kalloc&lt;/code&gt;会从空闲链表中弹出一页内存，而&lt;code&gt;kfree&lt;/code&gt;就会将这页内存给回收。&lt;/p&gt;
&lt;p&gt;下图展示了一个详细情况：空闲页面的链表位于两个$CPU$的共享内存中，它们使用&lt;code&gt;load&lt;/code&gt;和&lt;code&gt;store&lt;/code&gt;指令操作该链表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406251030024.png&quot; alt=&quot;Example race&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果不考虑并发情况，那么你可能会这样实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct element {
    int data;
    struct element* next;  
};

struct element* list = NULL;

void push(int data) {
    struct element* l;
    l = malloc( sizeof *l );
    l-&amp;gt;data = data;
    l-&amp;gt;next = list;
    list = l;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面我们说过，如果这段代码是单进程下，那么是正确的。如果两个$CPU$同时执行&lt;code&gt;push&lt;/code&gt;，因此这两个$CPU$都可能会在&lt;code&gt;list = l&lt;/code&gt;前执行&lt;code&gt;l-&amp;gt;next = list&lt;/code&gt;。那么就会导致上面图示的结果，两个元素确实插入到链表中，但是第一次对于链表的数据的赋值会发生在第二个节点上，并且第二次的赋值会覆盖掉第一次的赋值，并且第一个节点是没有值的。&lt;/p&gt;
&lt;p&gt;这就是竞争的一个简单例子。竞争是一种发生在一块内存被并发访问，并且其中至少有一个操作是写入时的一种状态。&lt;strong&gt;竞争的结果取决于编译器生成的机器码、所涉及的两个$cpu$的计时以及它们的内存操作如何由内存系统排序，这可能使竞争引起的错误难以重现和调试&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;避免竞争发生的常用方法就是锁。锁的存在保证了互斥性，以至于一次只有一个$cpu$能够执行一些敏感数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct element* list = NULL;
struct lock listlock;

void push(int data) {
    struct element* l;
    l = malloc( sizeof *l );
    l-&amp;gt;data = data;
    
    acquire(&amp;amp;listlock);
    l-&amp;gt;next = list;
    list = l;
    release(&amp;amp;listlock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;位于&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;之间的代码片段被称为&lt;code&gt;临界区(critical section)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当我们说锁保护数据时，通常而言是指锁保护应用于数据的一些不变量集合。不变量才是跨操作维护数据结构的属性。换言之，一个操作的是否正确取决于操作时的不变量是否正确。&lt;/p&gt;
&lt;p&gt;你可以将锁视为序列化并发临界区，以便它们一次运行一个，从而保留不变量。你还可以将由同一锁保护的临界区视为彼此之间的原子性，因此每个临界区都只能看到来自早期临界区的完整更改集，而永远不会看到部分完成的更新。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内核设计中的一个主要挑战是，在追求并行性时避免锁争用&lt;/strong&gt;。&lt;code&gt;xv6&lt;/code&gt;在这方面做得很少，但复杂的内核会专门组织数据结构和算法以避免锁争用。&lt;/p&gt;
&lt;h2&gt;Spin Lock&apos;s Code&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;xv6&lt;/code&gt;种实际上是存在两种锁的，一种则是&lt;code&gt;自旋锁(spin lock)&lt;/code&gt;，另一种是&lt;code&gt;睡眠锁(sleep lock)&lt;/code&gt;。我们首先从自旋锁开始介绍。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;xv6&lt;/code&gt;中，自旋锁的结构是在&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.h#L2&quot;&gt;kernel/spinlock.h:2&lt;/a&gt;中被定义的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Mutual exclusion lock.
struct spinlock {
  uint locked;      // Is the lock held?

  // For debugging:
  char *name;       // Name of lock.
  struct cpu *cpu;  // The cpu holding the lock.
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，最为主要的一个元素自然是&lt;code&gt;locked&lt;/code&gt;，这用于标识是否上锁：&lt;strong&gt;当锁可用时，其值为零；锁被拥有时，为非零&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然后我们可以在&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.c&quot;&gt;kernel/spinlock.c&lt;/a&gt;中查看其余函数的定义和实现。&lt;/p&gt;
&lt;h3&gt;initlock&lt;/h3&gt;
&lt;p&gt;不止在锁这一个模块中，其余每个模块基本上都会存在一个初始化函数，用于在内核进入时统一初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void initlock(struct spinlock *lk, char *name) {
  lk-&amp;gt;name = name;
  lk-&amp;gt;locked = 0;
  lk-&amp;gt;cpu = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于锁的初始化而言，没有过多讲解的必要。这里额外讲解一点$cpu$的结构信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Per-CPU state.
struct cpu {
  struct proc *proc;          // The process running on this cpu, or null.
  struct context context;     // swtch() here to enter scheduler().
  int noff;                   // Depth of push_off() nesting.
  int intena;                 // Were interrupts enabled before push_off()?
};
extern struct cpu cpus[NCPU];

// Return this CPU&apos;s cpu struct.
// Interrupts must be disabled.
struct cpu *mycpu(void) {
  int id = cpuid();
  struct cpu *c = &amp;amp;cpus[id];
  return c;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于&lt;code&gt;proc&lt;/code&gt;和&lt;code&gt;context&lt;/code&gt;这两个字段的含义，有些读者可能能够理解，但是目前暂时不需要过多的讲解，因为在此处不会有更多的使用。我们主要了解&lt;code&gt;noff&lt;/code&gt;和&lt;code&gt;intena&lt;/code&gt;，前者是关于锁嵌套的深度，而后者则是在获取锁之前，该$CPU$的中断使能状态信息。&lt;/p&gt;
&lt;p&gt;因此对于锁而言，&lt;strong&gt;我们在获取锁后为了避免死锁的发生，需要关闭中断使得获取锁后不会因为中断的发生而导致跳出锁的作用范围&lt;/strong&gt;。因此，我们就需要率先获取获取锁之前的状态，然后关闭中断使能，后续释放锁后，再根据获取的状态进行恢复使能状态。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;cpus&lt;/code&gt;和&lt;code&gt;mycpu()&lt;/code&gt;，则是针对于现代计算机多核的一种设计，我们在处理锁的时候，需要锁住当前$cpu$状态就需要使用。&lt;/p&gt;
&lt;h3&gt;push_off &amp;amp;&amp;amp; pop_off&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.c#L80&quot;&gt;push_off&lt;/a&gt;和&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.c#L89&quot;&gt;pop_off&lt;/a&gt;函数的设计和作用主要与中断的管理有关，以确保在持有自旋锁期间不发生上下文切换，从而避免死锁和竞争条件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;push_off&lt;/code&gt;函数的主要作用是&lt;strong&gt;禁用当前处理器的中断，并记录之前的中断状态。其设计目的是确保在持有自旋锁期间，中断不会打断当前的执行流，从而避免由于中断处理程序尝试获取相同自旋锁而导致的死锁&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void push_off(void) {
  int old = intr_get();

  intr_off();
  if (mycpu()-&amp;gt;noff == 0)
    mycpu()-&amp;gt;intena = old;
  mycpu()-&amp;gt;noff += 1;
}

static inline void intr_off() { w_sstatus(r_sstatus() &amp;amp; ~SSTATUS_SIE); }

// are device interrupts enabled?
static inline int intr_get() {
  uint64 x = r_sstatus();
  return (x &amp;amp; SSTATUS_SIE) != 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的实现中，&lt;code&gt;intr_get()&lt;/code&gt;会获取&lt;code&gt;mstatus&lt;/code&gt;寄存器，用于读取当前中断使能状态，然后通过&lt;code&gt;intr_off()&lt;/code&gt;关闭中断。当&lt;code&gt;cpu&lt;/code&gt;是第一次获取锁时，其中的&lt;code&gt;intena&lt;/code&gt;会保存之前的使能状态以供后续恢复。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pop_off&lt;/code&gt;函数的主要作用是&lt;strong&gt;恢复之前的中断状态。其设计目的是在完成对共享资源的访问后，重新启用中断&lt;/strong&gt;(&lt;strong&gt;如果在获取自旋锁之前中断是启用的&lt;/strong&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void pop_off(void) {
  struct cpu *c = mycpu();
  if (intr_get())
    panic(&quot;pop_off - interruptible&quot;);
  if (c-&amp;gt;noff &amp;lt; 1)
    panic(&quot;pop_off&quot;);
  c-&amp;gt;noff -= 1;
  if (c-&amp;gt;noff == 0 &amp;amp;&amp;amp; c-&amp;gt;intena)
    intr_on();
}

// enable device interrupts
static inline void intr_on() { w_sstatus(r_sstatus() | SSTATUS_SIE); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，解锁的过程中也是处于关中断状态的，因此我们需要判断此时的中断状态以及&lt;code&gt;noff&lt;/code&gt;字段。然后通过&lt;code&gt;intena&lt;/code&gt;判断是否使用&lt;code&gt;intr_on()&lt;/code&gt;恢复中断使能状态。&lt;/p&gt;
&lt;h3&gt;acquire &amp;amp;&amp;amp; release&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;xv6&lt;/code&gt;中，我们最为常用的两个自旋锁接口是&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.c#19&quot;&gt;acquire&lt;/a&gt;和&lt;a href=&quot;https://github.com/mit-pdos/xv6-riscv/blob/riscv//kernel/spinlock.c#42&quot;&gt;release&lt;/a&gt;。&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;函数用于管理自旋锁，它们分别用于获取和释放锁，从而确保对共享资源的互斥访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;acquire&lt;/code&gt;函数用于获取自旋锁。它的主要步骤包括禁用中断、检查当前锁状态、尝试获取锁以及记录当前持有锁的CPU。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void acquire(struct spinlock *lk) {
  push_off(); // disable interrupts to avoid deadlock.
  if (holding(lk))
    panic(&quot;acquire&quot;);

  while (__sync_lock_test_and_set(&amp;amp;lk-&amp;gt;locked, 1) != 0)
    ;
  __sync_synchronize();

  lk-&amp;gt;cpu = mycpu();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在上面知道，整个锁的环节都是需要在关中断的状态下完成的，因此关中断肯定是获取锁的第一步。然后我们需要判断该$cpu$是否以及获取过一次锁，如果以及获取锁，那么就会导致错误，因为两个锁的同时拥有会导致问题频发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int holding(struct spinlock *lk) {
  int r;
  r = (lk-&amp;gt;locked &amp;amp;&amp;amp; lk-&amp;gt;cpu == mycpu());
  return r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断有条件拥有锁后，我们就需要设置&lt;code&gt;locked&lt;/code&gt;字段，这里使用了$GCC$提供的内置的原子操作&lt;code&gt;__sync_lock_test_and_set&lt;/code&gt;，它是一个$atomic exchange operation$，这样就使得&lt;strong&gt;执行交换操作期间不会发生其他线程或进程的干扰，从而保证交换操作是原子的&lt;/strong&gt;。因此&lt;code&gt;locked&lt;/code&gt;就被置为了&lt;code&gt;1&lt;/code&gt;，并且其返回值是原先的旧值。因此，如果锁被其他$cpu$所持有，那么就会一直在&lt;code&gt;while&lt;/code&gt;中循环直到获取锁，因此被称为自旋锁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原子交换操作($atomic exchange operation$)
原子交换操作是一种并发编程中的操作，用于在多线程或多进程环境下实现原子性的值交换。它确保在执行交换操作期间不会发生其他线程或进程的干扰，从而保证交换操作是原子的。&lt;br /&gt;
在并发环境中，多个线程或进程可以同时访问和修改共享的变量。当多个线程或进程需要同时对某个变量进行读取和写入时，就可能发生竞态条件(race condition)，导致不确定的结果或数据不一致的情况。原子交换操作提供了一种解决竞态条件问题的机制。&lt;br /&gt;
原子交换操作通常包括两个步骤：读取变量的当前值并将新值写入变量。这两个步骤被视为一个原子操作，要么完全执行成功，要么完全不执行。原子交换操作通常使用底层的硬件指令或操作系统提供的原子操作函数来实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后通过&lt;code&gt;__sync_synchronize&lt;/code&gt;内置函数，用于提交一个&lt;code&gt;fence&lt;/code&gt;指令进行内存屏障操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; __sync_lock_test_and_set
; a5 = 1
; s1 = &amp;amp;lk-&amp;gt;locked

amoswap.w.aq a5, a5, (s1)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;AMOSWAP&lt;br /&gt;
AMOSWAP指令用于在多线程或多处理器环境下执行原子性的值交换操作。它允许程序原子地读取一个内存位置的当前值，并将一个新值写入该位置。该指令确保在执行交换操作期间不会发生其他线程或处理器的干扰，从而保证交换操作是原子的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406261048339.png&quot; alt=&quot;riscv Atomic Memory Operations&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，将当前$cpu$的结构信息保存在锁中，用于后续判断和调试。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;release&lt;/code&gt;函数用于释放自旋锁。它的主要步骤包括检查当前持有锁的$CPU$、清除持有锁的$CPU$信息、释放锁以及恢复中断状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void release(struct spinlock *lk) {
  if (!holding(lk))
    panic(&quot;release&quot;);

  lk-&amp;gt;cpu = 0;

  __sync_synchronize();

  __sync_lock_release(&amp;amp;lk-&amp;gt;locked);

  pop_off();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，首先需要判断的就是当前$cpu$是否含有锁，如果不持有锁，那么&lt;code&gt;release&lt;/code&gt;操作肯定就是有问题的。然后我们清空锁持有的$cpu$状态。&lt;/p&gt;
&lt;p&gt;在释放锁前，我们需要同步一下$cpu$的状态，因此使用&lt;code&gt;__sync_synchronize&lt;/code&gt;提交&lt;code&gt;fence&lt;/code&gt;指令。然后通过内建的原子操作&lt;code&gt;__sync_lock_release&lt;/code&gt;用于释放通过&lt;code&gt;__sync_lock_test_and_set&lt;/code&gt;的设置值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; sync_lock_release
; s1 = &amp;amp;lk-&amp;gt;locked
amoswap.w zero, zero, (s1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，通过&lt;code&gt;pop_off&lt;/code&gt;恢复$cpu$的中断使能状态。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于&lt;code&gt;xv6&lt;/code&gt;的自旋锁源码就解释完了。&lt;/p&gt;
&lt;h2&gt;Deadlock and Lock Ordering&lt;/h2&gt;
&lt;p&gt;如果内核中的某个代码路径必须同时持有多个锁，那么确保所有代码路径以相同的顺序获取这些锁是非常重要的。如果它们不按照相同的顺序获取锁，就会存在死锁的风险。&lt;/p&gt;
&lt;p&gt;假设在&lt;code&gt;xv6&lt;/code&gt;操作系统中有两个代码路径需要锁$A$和$B$，但是代码路径&lt;code&gt;1&lt;/code&gt;按照$A$然后$B$的顺序获取锁，而另一个代码路径按照$B$然后$A$的顺序获取锁。假设线程&lt;code&gt;T1&lt;/code&gt;执行代码路径&lt;code&gt;1&lt;/code&gt;并获取了锁$A$，线程&lt;code&gt;T2&lt;/code&gt;执行代码路径&lt;code&gt;2&lt;/code&gt;并获取了锁$B$。接下来，&lt;code&gt;T1&lt;/code&gt;将尝试获取锁$B$，而&lt;code&gt;T2&lt;/code&gt;将尝试获取锁$A$。由于在这两种情况下，另一个线程持有所需的锁，并且在其获取返回之前不会释放它，因此两个获取操作都将无限期地阻塞。为了避免这种死锁情况，&lt;strong&gt;所有代码路径必须按照相同的顺序获取锁。对于全局锁获取顺序的需求意味着锁实际上是每个函数规范的一部分：调用者必须以一种使得按照约定的顺序获取锁的方式调用函数&lt;/strong&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lock&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bcache.lock&lt;/td&gt;
&lt;td&gt;保存块缓冲条目的分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cons.lock&lt;/td&gt;
&lt;td&gt;序列化对终端硬件的访问，避免混合输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ftable.lock&lt;/td&gt;
&lt;td&gt;序列化文件表中结构文件的分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;itable.lock&lt;/td&gt;
&lt;td&gt;保护内存中inode条目的分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vdisk.lock&lt;/td&gt;
&lt;td&gt;序列化对磁盘硬件和DMA描述符队列的访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kmem.lock&lt;/td&gt;
&lt;td&gt;序列化内存分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log.lock&lt;/td&gt;
&lt;td&gt;序列化事务日志上的操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pipe&apos;s pi-&amp;gt;lock&lt;/td&gt;
&lt;td&gt;序列化每个管道上的操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pid_lock&lt;/td&gt;
&lt;td&gt;序列化next_pid的增量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;proc&apos;s p-&amp;gt;lock&lt;/td&gt;
&lt;td&gt;序列化进程状态的更改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wait_lock&lt;/td&gt;
&lt;td&gt;帮助等待避免丢失唤醒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tickslock&lt;/td&gt;
&lt;td&gt;序列化计时计数器上的操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inode&apos;s ip-&amp;gt;lock&lt;/td&gt;
&lt;td&gt;序列化每个索引节点及其内容上的操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buf&apos;s b-&amp;gt;lock&lt;/td&gt;
&lt;td&gt;序列化每个块缓冲区上的操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;出于这种情况，&lt;code&gt;xv6&lt;/code&gt;的&lt;code&gt;struct proc&lt;/code&gt;有一个&lt;code&gt;chain&lt;/code&gt;字段，用于&lt;code&gt;sleep&lt;/code&gt;和锁的协同作用。这样就避免了死锁的发生。例如：&lt;code&gt;consoleintr&lt;/code&gt;是处理键入字符的中断例程。当输入一个换行符时，任何正在等待控制台输入的进程都应该被唤醒。为了实现这一点，&lt;code&gt;consoleintr&lt;/code&gt;在调用&lt;code&gt;wakeup&lt;/code&gt;时持有&lt;code&gt;cons.lock&lt;/code&gt;，而&lt;code&gt;wakeup&lt;/code&gt;会获取等待进程的锁以唤醒它。因此，为了避免全局死锁，锁的获取顺序规则中包括了&lt;code&gt;cons.lock&lt;/code&gt;必须在任何进程锁之前获取的规定。文件系统代码包含了&lt;code&gt;xv6&lt;/code&gt;中最长的锁链。例如，创建一个文件需要同时持有目录的锁、新文件的&lt;code&gt;inode&lt;/code&gt;的锁、磁盘块缓冲区的锁、磁盘驱动器的&lt;code&gt;vdisk_lock&lt;/code&gt;以及调用进程的&lt;code&gt;p-&amp;gt;lock&lt;/code&gt;。为了避免死锁，文件系统代码总是按照前面提到的顺序获取锁。&lt;/p&gt;
</content:encoded></item><item><title>Concurrency in Action: Share data between threads</title><link>https://chenmiaoi.github.io/2024/06/15/Concurrency-in-Action-Share-data-between-threads/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/06/15/Concurrency-in-Action-Share-data-between-threads/</guid><pubDate>Sat, 15 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;使用线程来实现并发的一个潜在的好处就是能够很容易、直接地在它们之间使用共享数据&lt;/strong&gt;。因此，本篇笔记就是来讨论一些围绕共享数据的一些问题。&lt;/p&gt;
&lt;p&gt;如果你在线程之间共享了数据，你需要&lt;strong&gt;规定哪个线程可以在何时访问哪位数据，以及如何将任何更新传达给关心该数据的其他线程&lt;/strong&gt;。但是不正确的共享数据的使用却是线程导致$BUG$的最大的一个原因。&lt;/p&gt;
&lt;p&gt;本篇笔记是&lt;strong&gt;关于在$c++$中线程之间安全地共享数据，避免可能出现的潜在问题，并最大限度地提高收益&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Problems with sharing data between threads&lt;/h2&gt;
&lt;p&gt;当我们提到这个的时候，共享数据的问题全都是因为线程修改数据而导致的。&lt;strong&gt;如果所有的共享数据都是只读的，那么不会发生任何问题。因为一个线程读取数据不被会另一个读取相同数据的线程所影响&lt;/strong&gt;。但是如果一个或多个线程开始修改数据，就可能导致许多问题的产生。&lt;/p&gt;
&lt;p&gt;一个广泛的概念是：&lt;code&gt;invariant(不变量)&lt;/code&gt;————关于特定数据结构始终为真的陈述。这些不变量经常在更新过程中被破坏，特别是在数据结构非常复杂或更新需要修改多个值的情况下。&lt;/p&gt;
&lt;p&gt;现在考虑一个双向链表，其中每一个节点都含有一个指针，其指向链表中的下一个节点和上一个节点。其中的一个不变量为：如果你从一个节点($A$)通过&lt;code&gt;next&lt;/code&gt;指针跟随到下一个节点($B$)，那么就有节点($B$)的指针通过&lt;code&gt;prev&lt;/code&gt;指针指向上一个节点($A$)。如果我们想要删除一个节点，那么节点的两侧就应该相互指向对方；一旦一侧的指向发生了改变，那么不变性就被破坏了；直到另一侧也发生了该相应的改变，不变性重新被建立。&lt;/p&gt;
&lt;p&gt;这里给出下图中每一步的解释：&lt;/p&gt;
&lt;p&gt;a. 标识$N$节点会被删除
b. 更新$N$节点的上一个节点指向$N$节点的下一个节点
c. 更新$N$节点的下一个节点指向$N$节点的上一个节点
d. 删除$N$节点&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406161236186.png&quot; alt=&quot;Deleting a node from a doubly linked list&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看见，在$b$和$c$步骤之间，由于一个方向上的连接关系和另一个方向上的连接关系不一致，因此不变量被打破。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;修改线程间共享的数据最简单的潜在问题是破坏不变量&lt;/strong&gt;，如果一个线程正在读这个双向链表而另一个正在删除双向链表上的一个节点，那么读线程就很可能看见这正在被删除时的链表，因此不变量就被打破了。而根据读线程看到的删除状态的不同，其最终的结果也有所不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果读线程从左往右读取，那么它就可能直接跳过被删除的节点$N$&lt;/li&gt;
&lt;li&gt;如果删除节点的线程正好删除最右边的节点，那么就可能永久性的破坏链表的结构并且最终导致程序崩溃&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，又一个问题产生了，一个并发代码中最常见的$bug$原因之一：&lt;code&gt;race condition(竞争条件)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Race Condition&lt;/h3&gt;
&lt;p&gt;假设你想要在电影院购买一张电影票，并且有多个窗口办理购买流程。如果你现在已经对售票员述说了购买需求，而刚好有另一位人在另一个窗口和你买了同样的座位，那么你是否能够成功购买到你想要的座位就取决于你先订票还是他先订票。这就是一个&lt;code&gt;race condition(竞争条件)&lt;/code&gt;的一个例子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在并发中，竞争条件是指任何结果取决于两个或多个线程上操作执行的相对顺序的情况&lt;/strong&gt;;线程竞相执行各自的操作。在大多数情况下，竞争条件是良性的；但是，&lt;strong&gt;一旦竞争条件导致不变量被破坏，就会可能导致问题的发生&lt;/strong&gt;。在我们讨论竞争条件时，这个术语&lt;code&gt;race condition&lt;/code&gt;通常指的是&lt;code&gt;problematic race condition&lt;/code&gt;。在$c++$标准中，还特别使用&lt;code&gt;data race&lt;/code&gt;来特指因为并发修改了单个对象的数据导致的一种竞争条件类型。而&lt;code&gt;data race&lt;/code&gt;会产生未定义行为。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;problematic race condition&lt;/code&gt;通常&lt;strong&gt;发生在完成一个操作需要修改两个或多个不同的数据块时&lt;/strong&gt;。&lt;strong&gt;竞争条件通常是难以被发现和重现的，因为其出现的几率是很小的&lt;/strong&gt;。&lt;strong&gt;如果修改是作为连续的$CPU$指令进行的，即使数据结构正在被另一个线程并发访问，问题在任一运行中出现的概率也很小。随着系统负载的增加和操作执行次数的增加，问题出现的可能性也会增加。这种问题往往会在最不方便的时候出现，几乎是不可避免的。由于竞态条件通常是与时间有关的，当应用程序在调试器下运行时，它们往往会完全消失，因为调试器会影响程序的时间，即使只是稍微地&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，如何在多线程代码中避免竞争条件就是必须的事情。&lt;/p&gt;
&lt;h3&gt;Avoiding problematic race condition&lt;/h3&gt;
&lt;p&gt;这里有很多种方式来解决有问题的竞争条件。最简单的一种方式是&lt;strong&gt;用保护机制包装数据结构，以确保只有执行修改的线程才能看到不变量被破坏的中间状态&lt;/strong&gt;。从访问该数据结构的其他线程的角度来看：&lt;strong&gt;对于数据结构的修改要么还未开始，要么已经结束&lt;/strong&gt;。针对于这一点，$c++$标准库提供了许多机制来处理(将在本篇笔记中介绍)。&lt;/p&gt;
&lt;p&gt;另一种选择是&lt;strong&gt;修改数据结构及其不变性的设计，以便将修改作为一系列不可分割的更改进行，每个更改都保持不变性&lt;/strong&gt;。这种被称为&lt;code&gt;lock-free programming(无锁编程)&lt;/code&gt;，但这往往很难做到。如果你在这个层面上工作，&lt;strong&gt;内存模型的细微差别和确定哪些线程可能会看到哪些值的问题可能会变得复杂&lt;/strong&gt;。内存模型在第五篇笔记中有详细介绍，而无锁编程则在第七篇中进行了讨论。&lt;/p&gt;
&lt;p&gt;处理竞争条件的另一种方法是将对数据结构的更新作为&lt;code&gt;transaction(事务处理)&lt;/code&gt;，就像对数据库的更新在事务中完成一样。在其中，有一个术语叫做&lt;code&gt;STM(software transactional memory)&lt;/code&gt;，它是一种并发编程模型，&lt;strong&gt;旨在简化多线程程序中的共享内存访问和同步。它提供了一种类似于数据库事务的机制，使开发人员能够定义一系列原子操作，这些操作要么全部成功执行，要么全部回滚，就像一个单个不可分割的事务一样&lt;/strong&gt;。但是目前而言，在本系列笔记中不做过多介绍。&lt;/p&gt;
&lt;p&gt;而在$c++$标准库中的最常见的保护共享数据的方式是：&lt;code&gt;mutex(互斥锁)&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Protecting shared data with mutexes&lt;/h2&gt;
&lt;p&gt;如果你有一个共享的数据结构，就比如在前面所提到的双向链表。你想要保护它免受竞态条件和可能导致的破坏不变性的问题，那么将所有访问该数据结构的代码标记为&lt;code&gt;mutually exclusive(互斥)&lt;/code&gt;，这样就使得&lt;strong&gt;当一个线程执行时，任何试图访问该数据结构的其他线程都必须等待这个线程完成。这将使得除了进行修改的线程之外，其他线程无法看到破坏的不变性。这样做将确保线程在修改时才能看到破坏的不变性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，你可以使用$c++$所提供的同步原语&lt;code&gt;mutex&lt;/code&gt;。在你访问这个共享数据之前，你首先得将&lt;code&gt;mutex&lt;/code&gt;上锁，当你完成访问或其他操作后，再将&lt;code&gt;mutex&lt;/code&gt;解锁。然后，线程库确保一旦一个线程锁定了一个特定的互斥锁，所有其他试图锁定同一个互斥锁的线程都必须等待，直到成功锁定互斥锁的线程将其解锁。这就确保了所有线程所看到数据的一致性。&lt;/p&gt;
&lt;p&gt;当然，在大多数时候互斥锁的存在能够解决许多问题，但是这绝不是唯一一种，并且在少数极端情况下，互斥锁并不是灵丹妙药，其可能产生&lt;code&gt;dead lock(死锁)&lt;/code&gt;问题。&lt;/p&gt;
&lt;h3&gt;Using mutexes in C++&lt;/h3&gt;
&lt;p&gt;在$c++$中，你可以通过构造一个&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/mutex&quot;&gt;std::mutex&lt;/a&gt;的实例来创建一个互斥锁，使用&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/mutex/lock&quot;&gt;lock()&lt;/a&gt;上锁和&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/mutex/unlock&quot;&gt;unlock()&lt;/a&gt;来解锁。从实际上说，并不推荐手动的进行&lt;code&gt;lock()&lt;/code&gt;和&lt;code&gt;unlock()&lt;/code&gt;，在$c++$中还提供了实现了&lt;code&gt;RAII&lt;/code&gt;的&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/lock_guard&quot;&gt;std::lock_guard&lt;/a&gt;类模板，它在构造时锁定所提供的互斥锁，在销毁时解锁，确保被锁定的互斥锁总是被正确解锁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;list&amp;gt;
#include &amp;lt;mutex&amp;gt;

std::list&amp;lt; int32_t &amp;gt; some_list;
std::mutex some_mutex;
void add_to_list(int new_value) {
	std::lock_guard&amp;lt; std::mutex &amp;gt; guard( some_mutex );
	some_list.push_back( new_value );
}

bool list_contains(int value_to_find) {
	std::lock_guard&amp;lt; std::mutex &amp;gt; guard( some_mutex );
	return std::find(
		some_list.begin(),
		some_list.end(),
		value_to_find ) != some_list.end();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的示例中看出，这两个函数&lt;code&gt;add_to_list&lt;/code&gt;和&lt;code&gt;list_contains&lt;/code&gt;是互斥的，也就意味着&lt;code&gt;list_contains&lt;/code&gt;永远不会在&lt;code&gt;add_to_list&lt;/code&gt;修改链表的途中访问数据结构。 在$c++17$标准中，由于$CTAD$的存在，我们可以直接忽略到模板参数的显式声明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::lock_guard guard( some_mutex );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在$c++17$标准中，还增加了一个增强版本的&lt;code&gt;lock guard&lt;/code&gt;类：&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/scoped_lock&quot;&gt;std::scoped_lock&lt;/a&gt;，当然，它也可以通过$CTAD$来省略模板的声明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::scoped_lock guard( some_mutex );
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;CTAD, Class Template Argument Deduction&lt;br /&gt;
C++17引入的一个特性。它允许在使用类模板时省略模板参数，编译器会根据函数调用或对象创建的上下文自动推导出模板参数。CTAD的引入简化了模板的使用和书写，使代码更加简洁和易读。需要注意的是，&lt;strong&gt;$CTAD$仅适用于某些特定的上下文和初始化方式，而不是所有情况下都可以省略模板参数&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了笔记的简洁性和兼容性，这里依旧使用&lt;code&gt;std::lock_guard&lt;/code&gt;作为代码例子。&lt;/p&gt;
&lt;p&gt;尽管在某些情况下使用全局变量是合适的，但在大多数情况下，&lt;strong&gt;通常将互斥锁和受保护的数据组合在一个类中，而不是使用全局变量&lt;/strong&gt;。这符合$OOP$的设计规范。但是，有一些眼尖的人会发现，&lt;strong&gt;如果一个成员函数返回指向受保护数据的指针或引用，那么所有成员函数都以一种良好、有序的方式锁定互斥锁就无关紧要了，因为你已经在保护中弄出了一个大漏洞&lt;/strong&gt;。&lt;em&gt;&lt;strong&gt;具有对该指针或引用的访问权限的任何代码都可以在不锁定互斥量的情况下访问(并潜在地修改)受保护的数据&lt;/strong&gt;&lt;/em&gt;。因此，用互斥锁保护数据需要仔细的接口设计，以确保互斥锁在对受保护数据进行任何访问之前被锁定，并且没有后门。&lt;/p&gt;
&lt;h3&gt;Structuring code for protecting shared data&lt;/h3&gt;
&lt;p&gt;正如你所看到的，使用互斥锁保护共享数据并不是在函数中添加一个&lt;code&gt;std::lock_guard&lt;/code&gt;就能解决的简单的事情；&lt;strong&gt;一个偏离的指针或引用，就会导致所有的保护工作白费&lt;/strong&gt;。在一方面来说，检查出偏离的指针或引用是较为容易的；&lt;strong&gt;只需要对应的成员函数没有返回所保护数据的指针或引用，或没有通过&lt;/strong&gt;&lt;code&gt;out&lt;/code&gt;&lt;strong&gt;参数将其指针或引用传递出去即可&lt;/strong&gt;。如果你想的更深，就会知道没有什么事情是容易的。&lt;strong&gt;除了检查成员函数不会将指针或引用传递给调用者之外，同样重要的是检查它们不会将这些指针或引用传递给它们调用的、不受你控制的函数。因为这些函数可能不会立即使用这些受保护的数据，而是存储起来在没有互斥锁保护的地方去使用&lt;/strong&gt;。下面就给出了一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class some_data {
	int32_t _a;
	std::string _b;

public:
	void do_something() {}
};

class data_wrapper {
	some_data _data;
	std::mutex _mtx;

public:
	template &amp;lt; typename Function &amp;gt;
	void process_data(Function func) {
		std::lock_guard&amp;lt; std::mutex &amp;gt; lock( _mtx );
		func( _data );	// Pass “protected” data to user-supplied function
	}
};

some_data* unprotected;
void malicious_function(some_data&amp;amp; protect_data) {
	unprotected = &amp;amp;protect_data;
}

data_wrapper x;
void foo() {
	x.process_data(malicious_function );	// [Warning]: Pass in a malicious function
	unprotected-&amp;gt;do_something(); // [Warning]: Unprotected access to protected data
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;process_data&lt;/code&gt;看上去是足够安全的，并且很好地被&lt;code&gt;std::lock_guard&lt;/code&gt;所保护。但是，这里调用了一个用户提供的函数&lt;code&gt;func&lt;/code&gt;，这就意味着它会传递给&lt;code&gt;malicious_function&lt;/code&gt;，并且在没有任何保护措施的情况下调用&lt;code&gt;do_something()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从根本上说，这段代码的问题在于它没有完成你设定的任务：&lt;strong&gt;将访问数据结构的&lt;em&gt;所有代码&lt;/em&gt;块标记为互斥的&lt;/strong&gt;。也就是说，它在这个例子中忽略了&lt;code&gt;unprotected-&amp;gt;do_something()&lt;/code&gt;的保护，更糟糕的是，$c++$&lt;strong&gt;标准库对于这一行为并未提供任何保护机制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;不要在互斥锁的范围之外传递指向受保护数据的指针和引用，无论是通过从函数返回它们、将它们存储在外部可见的内存中，还是将它们作为参数传递给用户提供的函数&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Spotting race condition inherent in interface&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;你现在只是使用互斥锁来保护共享数据，这并不意味着你现在不受竞争条件的影响。你仍然需要确保数据是否受到保护&lt;/strong&gt;。就以先前的双向链表的删除操作为例，为了使一个线程安全的删除一个节点，我们尝试去将需要被删除的节点和其两边的节点都做出保护措施。但是，这仍旧会发生竞争条件，因为你只是单独的对每一个节点做出了保护，而不是针对于删除操作这整个流程做出保护。在这种情况下，最简单的方式就如之前所示的，使用单个互斥锁将整个链表给保护起来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但如果只是因为链表上的单个操作受到了保护，那么你还没有真正脱离危险，仍会发生竞争条件&lt;/strong&gt;。考虑一个栈的数据结构，比如&lt;code&gt;stack&lt;/code&gt;适配器。如果你修改了&lt;code&gt;top()&lt;/code&gt;，&lt;strong&gt;使它返回一个副本而不是一个引用，并且用互斥锁保护内部数据&lt;/strong&gt;，这个接口仍然会受到竞争条件的影响。这个问题并不是基于互斥锁的实现所独有的，这是一个接口问题，因此在无锁实现中仍然会出现竞争条件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt; typename T, typename Container = std::deque&amp;lt; T &amp;gt; &amp;gt;
struct stack {
public:
	explicit stack( const Container&amp;amp; );
	explicit stack( Container&amp;amp;&amp;amp; = Container() );

	template &amp;lt; typename Alloc &amp;gt; explicit stack( const Alloc&amp;amp; );
	template &amp;lt; typename Alloc &amp;gt; stack( const Container&amp;amp;, const Alloc&amp;amp; );
	template &amp;lt; typename Alloc &amp;gt; stack( Container&amp;amp;&amp;amp;, const Alloc&amp;amp; );
	template &amp;lt; typename Alloc &amp;gt; stack( stack&amp;amp;&amp;amp;, const Alloc&amp;amp; );

	bool 	    empty() const;
	size_t 		size() const;
	T&amp;amp; 			top();
	const T&amp;amp;	top() const;
	void 		push( const T&amp;amp; );
	void 		push( T&amp;amp;&amp;amp; );
	void 		swap( stack&amp;amp;&amp;amp; );

	template &amp;lt; typename ... Args &amp;gt; void emplace( Args&amp;amp;&amp;amp; ... args );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的问题主要是因为&lt;code&gt;empty()&lt;/code&gt;和&lt;code&gt;size()&lt;/code&gt;的结果不再可靠。尽管在调用时它们可能是正确的，但一旦它们返回，其他线程可以自由访问堆栈，并且在调用&lt;code&gt;empty()&lt;/code&gt;或&lt;code&gt;size()&lt;/code&gt;的线程使用该信息之前，可能会在堆栈上&lt;code&gt;push()&lt;/code&gt;新元素或&lt;code&gt;pop()&lt;/code&gt;现有元素。&lt;/p&gt;
&lt;p&gt;特别是，如果堆栈实例不是共享的，检查&lt;code&gt;empty()&lt;/code&gt;然后调用&lt;code&gt;top()&lt;/code&gt;来访问堆栈不为空的&lt;code&gt;top()&lt;/code&gt;元素是安全的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stack&amp;lt; int32_t &amp;gt; s;
if ( s.empty() ) {
    const int32_t value = s.top();
    s.pop();
    do_something( value ); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显而易见的，在单线程上这段代码始终是安全的。那么在多线程上，这段代码就不再安全，因为很有可能当前线程位于调用&lt;code&gt;empty()&lt;/code&gt;和&lt;code&gt;top()&lt;/code&gt;之间时，另一个线程调用了&lt;code&gt;pop()&lt;/code&gt;并且移除了栈中的最后一个元素，这将导致未定义行为的产生。因此，&lt;strong&gt;在内部使用互斥锁也不能保证行为的正确性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那么如何解决呢？在最简单的情况下，当栈中没有任何元素时，你可以在&lt;code&gt;top()&lt;/code&gt;的实现如果此时被调用，则内部抛出异常。虽然我们直接的解决了这个问题，但是我们又需要在外部捕获这个异常，这显然很冗余。那么针对于&lt;code&gt;empty()&lt;/code&gt;调用的优化就有必要了(当然，这并不是必须的)。&lt;/p&gt;
&lt;p&gt;如果你细心一点还会发现，在上面的代码片段中，位于&lt;code&gt;top()&lt;/code&gt;和&lt;code&gt;pop()&lt;/code&gt;之间也存在竞争条件。让我们来考虑这样一种情况，如果你有两个线程同时指向一个栈对象&lt;code&gt;s&lt;/code&gt;。假设栈上一开始有两个元素，那么你并不需要担心&lt;code&gt;empty()&lt;/code&gt;和&lt;code&gt;top()&lt;/code&gt;之间会出现问题。&lt;/p&gt;
&lt;p&gt;如果这个栈被互斥锁保护，一次只允许一个线程进入执行其内部函数，那么这些调用就能够正常的交替执行。但是，对于&lt;code&gt;do_something&lt;/code&gt;而言，则是并发的。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thread A&lt;/th&gt;
&lt;th&gt;Thread B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;if (!e.empty())&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;if (!e.empty())&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const int32_t value = s.top()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;const int32_t value = s.top()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;s.pop()&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;s.pop()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;do_something( value )&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;do_something( value )&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;就如你所看到的，如果只有两个线程运行，那么对于&lt;code&gt;top()&lt;/code&gt;的调用中间就不会有间隔，因此两个线程中的&lt;code&gt;value&lt;/code&gt;看到的是同一份数据。不仅如此，在这两次&lt;code&gt;top()&lt;/code&gt;的调用之间，也不会存在&lt;code&gt;pop()&lt;/code&gt;的调用。因此，&lt;strong&gt;一个值将会在没有被读取的情况下直接被丢弃，而另一个值则会被使用两次&lt;/strong&gt;。可以看见，这里就是另一个竞争条件存在的位置，并且远比&lt;code&gt;empty()&lt;/code&gt;和&lt;code&gt;top()&lt;/code&gt;导致的未定义行为阴险的多；更重要的是，这里不会有明显的错误，而且错误的后果和原因还可能取决于&lt;code&gt;do_something()&lt;/code&gt;的实际操作。&lt;/p&gt;
&lt;p&gt;这需要对接口进行更为激进的改变，即将对&lt;code&gt;top()&lt;/code&gt;和&lt;code&gt;pop()&lt;/code&gt;的调用在互斥锁的保护下组合起来。但是有人指出，&lt;strong&gt;如果栈上对象的复制构造函数可能抛出异常，组合调用可能会导致问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们现在通过&lt;code&gt;stack&amp;lt; vector&amp;lt; int32_t &amp;gt; &amp;gt;&lt;/code&gt;来解释这一问题。&lt;code&gt;vector&lt;/code&gt;是一个动态大小的容器，因此当你打算拷贝一份&lt;code&gt;vector&lt;/code&gt;时，标准库就需要在堆上申请空间以拷贝其数据。如果系统负载过重或存在明显的资源限制，这种内存分配可能失败，因此&lt;code&gt;vector&lt;/code&gt;的拷贝构造函数可能抛出&lt;code&gt;std::bad_alloc&lt;/code&gt;异常。如果&lt;code&gt;pop()&lt;/code&gt;函数的实现为返回被弹出的值，并将其从堆栈中移除时，你就会遇见一个潜在的问题：&lt;strong&gt;只有在栈已经被修改之后，弹出的值才会返回给调用者，但在将数据拷贝给调用者的过程中可能会抛出异常&lt;/strong&gt;。如果这种情况发生，就会导致这个值确实被弹出并移除，但其拷贝却失败了。因此，&lt;code&gt;stack&lt;/code&gt;设计该接口的时候，需要将其分为两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;获取栈顶元素(&lt;code&gt;top()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;移除该元素(&lt;code&gt;pop()&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，如果你不能安全的拷贝这份数据，那么该数据仍会留在栈中。但需要值得注意的是，如果你需要解决竞态的发生，就不应该出现这种分裂的情况。&lt;/p&gt;
&lt;h4&gt;Option1: Pass In A Reference&lt;/h4&gt;
&lt;p&gt;解决的第一种方法是：当你想得到弹出的值时，用一个引用作为调用&lt;code&gt;pop()&lt;/code&gt;的参数的接收值即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt; int32_t &amp;gt; result;
some_stack.pop( result );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这在许多情况下都很好用，但它有一个明显的缺点，&lt;strong&gt;即要求调用代码在调用前构造一个栈值类型的实例，以便将其作为目标传入&lt;/strong&gt;。对于某些类型来说，这是不切实际的，因为构造一个实例在时间或资源方面都非常昂贵。对于其他类型来说，这也并非总是可行的，因为构造函数需要的参数在代码的这一点上不一定能够获得。最后，它要求存储的类型是可赋值的。这是一个重要的限制：&lt;strong&gt;许多自定义的类型不支持赋值，尽管它们可能支持移动构造甚至拷贝构造(并允许通过值返回)&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Option2: Require A No-Throw Copy Constructor Or Move Constructor&lt;/h4&gt;
</content:encoded></item><item><title>Concurrency in Action: Managing threads</title><link>https://chenmiaoi.github.io/2024/06/13/Concurrency-in-Action-Managing-threads/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/06/13/Concurrency-in-Action-Managing-threads/</guid><pubDate>Thu, 13 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一篇笔记中，你已经了解了什么是线程，那么这一讲我们主要关注的重点就是：&lt;strong&gt;C++如何启动线程、等待线程结束以及如何管理线程&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Basic thread management&lt;/h2&gt;
&lt;p&gt;每一个$C++$的程序都至少有一个线程，其由$C++$运行时启动，用于运行&lt;code&gt;main()&lt;/code&gt;。你的程序可以启动额外的线程，并且其入口点可以自行决定。那么现在，我们来看看如何启动一个新线程。&lt;/p&gt;
&lt;h3&gt;Launching a thread&lt;/h3&gt;
&lt;p&gt;就如上一篇笔记所看到的那样，线程在&lt;code&gt;std::thread&lt;/code&gt;对象构造时开始，并且指定了需要运行的任务。在简单的例子中，任务通常是简单的、无参数也无返回值的，它会在新开的线程中自行运行，结束后线程便终止。但在一些极端情况下，这个任务可以是一个&lt;code&gt;函数对象&lt;/code&gt;，需要传入指定的额外参数并执行一系列独立的操作，这些操作是在运行时通过某些系统信息传递指定的，并且线程只有在收到信号时才会停止。&lt;/p&gt;
&lt;p&gt;不过，总而言之，&lt;strong&gt;对于$C++$标准库启动线程而言，总是从&lt;/strong&gt;&lt;code&gt;std::thread&lt;/code&gt;&lt;strong&gt;开始的&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_some_work();
std::thread my_thread( do_some_work );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，这只是一个极其简单的例子。对于&lt;code&gt;std::thread&lt;/code&gt;而言，&lt;strong&gt;它能够接收任何&lt;/strong&gt;&lt;code&gt;可调用对象类型&lt;/code&gt;，因此讲带有&lt;code&gt;operator()&lt;/code&gt;实现的类的实例传递给&lt;code&gt;std::thread&lt;/code&gt;是可行的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class background_task {
public:
	void operator() () const {
		do_something();
		do_something_else();
	}
};

background_task f;
std::thread my_thread( f );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，这个提供的函数对象被&lt;strong&gt;拷贝&lt;/strong&gt;到新创建的线程所属的内存中调用。因此，&lt;strong&gt;拷贝的行为必须与原始行为相同，否则结果可能不是预期的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有个点值得注意：当你传入一个函数对象，而这个函数对象是一个临时值时，那么$C++$会将其解释为临时变量，而非对象定义。如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread my_thread( background_task() ); // [Warning] Parentheses were disambiguated as a function declaration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会导致这样的提示：声明了一个单参数，返回一个&lt;code&gt;std::thread&lt;/code&gt;对象的函数，而非启动了一个新线程。因此，我们有两种方式解决这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread my_thread( (background_task()) );
std::thread my_thread{ background_task() };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在第一种方式中，我们通过额外的括号防止将&lt;code&gt;my_thread&lt;/code&gt;解释为函数声明，从而允许将&lt;code&gt;my_thread&lt;/code&gt;解释为函数声明，从而允许将&lt;code&gt;my_thread&lt;/code&gt;声明为&lt;code&gt;std::thread&lt;/code&gt;类型的变量。那么第二种方式则是使用了$C++11$标准中的，一致性初始化语法(个人倾向于这一种方式)。&lt;/p&gt;
&lt;p&gt;那么还有一种方式能够在使用函数对象时避免上面这一种问题：那就是使用&lt;code&gt;lambda expression&lt;/code&gt;。这是在$C++11$中新增的特性，能够允许你写出局部函数，并且能够捕获局部变量，这可以避免一些额外参数的传递。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread my_thread ( [] {
    do_something();
    do_something_else();
} );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;一旦你启动了你的线程，就必须显式的去决定是要等待这个线程结束(join)，还是让这个线程自行运行(detach)，如果你在该线程结束前都没有确定它运行的方式，那么你的程序会被强制终止&lt;/strong&gt;(&lt;code&gt;std::tread&lt;/code&gt;会调用&lt;code&gt;std::terminate()&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;如果你并不希望等待(join)线程完成，则&lt;strong&gt;需要确保线程访问的数据在线程完成之前是有效的&lt;/strong&gt;。这和单线程程序是一样的，如果你在一个对象销毁后继续访问，那么其结果是未定义的。&lt;/p&gt;
&lt;p&gt;你可能会遇到这样的问题：当你的线程函数包含了一个局部变量的指针或引用，并且当函数退出时，该线程仍未结束。下面给出一个例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct func {
	int&amp;amp; _i;

	func(int&amp;amp; i): _i( i ) {}

	void operator() () {
		for (uint32_t j = 0; j &amp;lt; 100&apos;0000; ++j) {
			do_something(_i);   // [Warning]: Potential access to dangling reference
		}
	}
};

void oops() {
	int some_local_state = 0;
	func my_func( some_local_state );
	std::thread my_thread( my_func );
	my_thread.detach();         // [Warning]: New thread might still be running
	                            // [Warning]: `some_local_state` destroyed
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，这一个例子中，&lt;code&gt;my_thread&lt;/code&gt;调用了&lt;code&gt;detach()&lt;/code&gt;，因此该新线程可以自由运行，但该线程的激活函数使用了局部变量&lt;code&gt;some_local_state&lt;/code&gt;的引用，当&lt;code&gt;my_thread&lt;/code&gt;运行出该作用域时，&lt;code&gt;some_local_state&lt;/code&gt;被销毁，因此线程中的&lt;code&gt;do_something(_i)&lt;/code&gt;就可能访问到未定义的数据。下面给出一个简单的表以便观察：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Main Thread&lt;/th&gt;
&lt;th&gt;New Thread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;用&lt;code&gt;some_local_state&lt;/code&gt;的引用来构造&lt;code&gt;my_func&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;创建&lt;code&gt;my_thread&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;开始运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;调用&lt;code&gt;func::operator()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分离&lt;code&gt;my_thread&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行&lt;code&gt;func::operator()&lt;/code&gt;；调用&lt;code&gt;do_something()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;销毁&lt;code&gt;some_local_state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仍旧运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;退出&lt;code&gt;oops&lt;/code&gt;函数&lt;/td&gt;
&lt;td&gt;仍旧运行&lt;code&gt;func::opeartor()&lt;/code&gt;；调用&lt;code&gt;do_something(some_local_state)&lt;/code&gt; $\Rightarrow$ 未定义行为&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;处理这种情况的最常见方法就是使线程函数自包含并且拷贝数据到线程中，而非使用共享数据。&lt;strong&gt;在函数中创建一个可以访问该函数中的局部变量的线程是一个不好的设计，除非线程能够保证在函数退出前完成&lt;/strong&gt;。或者，你可以使用&lt;code&gt;join()&lt;/code&gt;来确保线程函数在函数退出前完成。&lt;/p&gt;
&lt;h3&gt;Waiting for a thread to complete&lt;/h3&gt;
&lt;p&gt;如果你想等待一个线程的完成，那你就应该在对应的线程实例上调用&lt;code&gt;join()&lt;/code&gt;函数。就如同上面那个例子，如果你将&lt;code&gt;detach()&lt;/code&gt;替换为&lt;code&gt;join()&lt;/code&gt;，那么该线程就能够在函数退出前完成，因此&lt;code&gt;some_local_state&lt;/code&gt;局部变量就不会被销毁，因此是有意义的。显然的，在单独线程上运行函数没有什么意义，但在实际代码中，原始线程要么有工作处理，要么在等待所有线程完成之前启动几个线程来做有用的工作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;join()&lt;/code&gt;是一个简单粗暴的方案————你要么等待线程完成，要么不等待。如果你想进行一些更深入、详细的控制，比如检查线程是否完成、只等待一段时间等等，你可以使用我们将在第四篇笔记中讲到的&lt;code&gt;条件变量(condition variable)&lt;/code&gt;和&lt;code&gt;futures&lt;/code&gt;机制。调用&lt;code&gt;join()&lt;/code&gt;的行为也会&lt;strong&gt;清空该线程实例的任何内存空间&lt;/strong&gt;，因此这个线程不再与&lt;code&gt;std::thread&lt;/code&gt;的&lt;strong&gt;任何实例&lt;/strong&gt;相关联。这就意味着，&lt;strong&gt;对于每一个给定的线程实例，只能够调用一次&lt;/strong&gt;&lt;code&gt;join()&lt;/code&gt;，一旦你调用了&lt;code&gt;join()&lt;/code&gt;，那么该线程实例就不再是可连接的(joinable)，因此&lt;code&gt;joinable()&lt;/code&gt;就应该返回$false$。&lt;/p&gt;
&lt;h3&gt;Waiting in exceptional circumstances&lt;/h3&gt;
&lt;p&gt;在之前我们提到，你需要确保在&lt;code&gt;std::thread&lt;/code&gt;实例被销毁前调用&lt;code&gt;join()&lt;/code&gt;或&lt;code&gt;detach()&lt;/code&gt;。如果你想要分离线程，那么只需要在启动线程后直接调用&lt;code&gt;detach()&lt;/code&gt;即可，这并不会造成问题。但是，如果你想要等待线程，你就需要谨慎的选择调用&lt;code&gt;join()&lt;/code&gt;的位置。这就意味着：&lt;strong&gt;如果一个异常发生在线程启动之后&lt;/strong&gt;，&lt;code&gt;join()&lt;/code&gt;&lt;strong&gt;调用之前，那么&lt;/strong&gt;&lt;code&gt;join()&lt;/code&gt;&lt;strong&gt;的调用就可能被跳过从而导致程序异常终止&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通常情况下，如果你想避免上述情况的发生，但是你的预期又想在非异常的地方调用&lt;code&gt;join()&lt;/code&gt;，那么就应该在异常处理处也调用&lt;code&gt;join()&lt;/code&gt;，如以下方式实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct func;

void f() {
	int some_local_state = 0;
	func my_func( some_local_state );
	std::thread t( my_func );
	try {
		do_something_in_current_thread();
	} catch ( ... ) {
		t.join();
		throw ;
	}
	t.join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码使用了&lt;code&gt;try/catch&lt;/code&gt;块来保证访问本地状态的线程在函数退出前结束，无论是正常退出还是异常。不过，&lt;code&gt;try/catch&lt;/code&gt;&lt;strong&gt;块的使用是冗长的，并且容易造成作用域错误，因此这并不是我们常用的处理方式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果确保线程在函数退出之前完成这一目标是很重要的，并且无论它是因为局部变量的引用，还是其他任何原因，&lt;strong&gt;确保所有可能的退出路径对这一目标的实现是异常重要的&lt;/strong&gt;。因此，需要一个简单、简洁的机制来实现这一功能。&lt;/p&gt;
&lt;p&gt;一种实现方式是使用标准的&lt;code&gt;资源获取即初始化(Resource Acquisition Is Initialization，RAII)&lt;/code&gt;，提供一个在其析构函数中执行&lt;code&gt;join()&lt;/code&gt;的类，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class thread_guard {
    std::thread&amp;amp; _t;

public:
    explicit thread_guard(std::thread&amp;amp; t): _t( t ) {}
    ~thread_guard() {
        if (_t.joinable()) {
            _t.join();
	    }
    }

    thread_guard(const thread_guard&amp;amp;)               = delete;
    thread_guard&amp;amp; operator= (const thread_guard&amp;amp;)   = delete;
};

struct func;

void f() {
    int some_local_state = 0;
    func my_func( some_local_state );
    std::thread t( my_func );
    thread_guard g( t );
    do_something_in_current_thread();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当当前线程的执行到达&lt;code&gt;f()&lt;/code&gt;的末尾时，&lt;strong&gt;局部变量会按照与构造顺序相反的顺序进行销毁&lt;/strong&gt;。因此，&lt;code&gt;thread_guard&lt;/code&gt;的实例是最先销毁的，所以&lt;code&gt;g&lt;/code&gt;会调用其析构函数，并在内部进行&lt;code&gt;join()&lt;/code&gt;调用，即便是因为异常退出，也因为跳出函数作用域而进行析构函数的调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;thread_guard&lt;/code&gt;的析构函数会首先判断该线程实例是否能够等待(&lt;code&gt;joinable()&lt;/code&gt;)，又因为&lt;code&gt;join()&lt;/code&gt;自身只能够被调用一次，因此就能够判断防止重复&lt;code&gt;join()&lt;/code&gt;的情况。而拷贝构造和拷贝赋值则是显式的调用了&lt;code&gt;delete&lt;/code&gt;，防止编译器自动提供拷贝方式。&lt;strong&gt;如果允许拷贝的存在，那么就可能跳出该线程所应该允许的范围&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果不需要等待线程完成，可以通过分离线程来避免这种异常安全问题。这将打破线程与&lt;code&gt;std::thread&lt;/code&gt;对象的关联，并确保&lt;code&gt;std::terminate()&lt;/code&gt;在&lt;code&gt;std::thread&lt;/code&gt;对象被销毁时不会被调用，即使线程仍在后台运行。&lt;/p&gt;
&lt;h3&gt;Running threads in the background&lt;/h3&gt;
&lt;p&gt;在一个线程对象上调用&lt;code&gt;detach()&lt;/code&gt;会使得该线程在后台运行，在此之后没有直接的与之通信的方式；并且也不再等待该线程的完成，&lt;strong&gt;其所有权和控制权转交给$C++$运行时，以确保线程退出时能够正确回收与之相关的资源。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;与$Unix$的&lt;code&gt;守护进程(daemen process)&lt;/code&gt;对应的，分离的线程通常被称之为&lt;code&gt;守护线程(daemen thread)&lt;/code&gt;，在后台运行并且没有任何显式的用户界面接口。这些线程都是长时间运行的，它们几乎运行在应用程序的整个生命周期，执行一些后台任务，例如：监视文件系统、清除无用的缓存条目或者优化数据结构等等。&lt;/p&gt;
&lt;p&gt;在另一种极端情况下，使用分离线程是有意义的，因为有另一种机制来识别线程何时完成或线程用于&lt;code&gt;一次性任务(fire-and-forget task)&lt;/code&gt;的位置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fire-And-Forget task&lt;br /&gt;
这个术语通常用于描述一种任务执行的方式，尤其是在计算机科学和工程领域中。&quot;Fire-and-forget&quot; 意味着执行者启动任务后，不再需要关注任务的结果或状态，而可以继续执行其他任务。
在计算机编程中，&quot;fire-and-forget&quot; 可以用来描述一种异步操作的方式。当一个程序执行一个 &quot;fire-and-forget&quot; 任务时，它会触发一个操作，但不会等待该操作完成或返回结果。相反，程序会继续执行后续的代码，而不会被任务的执行阻塞。
这种方式常用于处理那些不需要立即获得结果或不需要对结果进行进一步处理的任务。例如，在发送电子邮件时，可以使用 &quot;fire-and-forget&quot; 模式，将电子邮件添加到发送队列中，然后立即返回用户界面，而不需要等待所有电子邮件都成功发送。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;就如你在之前见到的示例，分离线程后，该线程实例就不再会被等待。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread t( do_background_task );
t.detach();
assert( !t.joinable() );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要注意的是，&lt;strong&gt;你可以使用&lt;/strong&gt;&lt;code&gt;joinable()&lt;/code&gt;&lt;strong&gt;来检查该实例对象是否可以被&lt;/strong&gt;&lt;code&gt;join()&lt;/code&gt;&lt;strong&gt;或者&lt;/strong&gt;&lt;code&gt;detach()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在来考虑一个应用程序，比如一个可以同时编辑多个文档的文本编辑器。这里有很多种方式来实现，不论是从$UI$层面还是从内部。目前越来越普遍的一种方式是使用多个独立的顶级窗口，每个正在编辑的文档对应一个窗口。尽管这些窗口看上去完全独立，并且有用自己的菜单栏，但实际上它们是运行在应用程序的相同实例中。另一种处理方式就是每一个文档编辑窗口就是一个线程，每一个线程都运行同样的代码，只是正在编辑的文档和对应的窗口属性相关的数据不同。 打开一个新的文档就是创建了一个新进线程，处理请求的线程不会关心等待另一个线程完成，因为它正在处理一个不相关的文档，所以这使得它成为运行一个分离线程的主要候选者。&lt;/p&gt;
&lt;p&gt;这里是一个简单的实现大纲:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void edit_document( const std::string&amp;amp; filename ) {
    open_document_and_display_gui( filename );
    while ( !done_editing() ) {
        user_command cmd = get_user_input();
        if ( cmd.type == open_new_document ) {
            const std::string new_name = get_filename_from_user();
            std::thread t( edit_document, new_name );
            t.detach();
        } else {
            process_user_input( cmd );
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Passing arguments to a thread function&lt;/h2&gt;
&lt;p&gt;我们在上面的例子中看到，向&lt;code&gt;std::thread&lt;/code&gt;构造函数传递附加参数在本质上与向可调用对象或函数传递参数一样简单。但是最重要的一点是：&lt;strong&gt;默认情况下，参数会被拷贝进内部的内存空间(以右值的方式)，其能够被新线程的执行流所获取并使用和访问，并以&lt;/strong&gt;&lt;code&gt;右值(rvalue)&lt;/code&gt;&lt;strong&gt;的形式传递给可调用对象或函数作为参数&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f( int i, const std::string&amp;amp; s );
std::thread( f, 3, &quot;hello&quot; ); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指的注意的是，&lt;strong&gt;尽管&lt;/strong&gt;&lt;code&gt;f&lt;/code&gt;&lt;strong&gt;第二个参数接收的形式为&lt;/strong&gt;&lt;code&gt;const std::string&amp;amp;&lt;/code&gt;，&lt;strong&gt;但字符串字面量会被当作&lt;/strong&gt;&lt;code&gt;const char*&lt;/code&gt;&lt;strong&gt;传递，仅在新线程的上下文中被转换为&lt;/strong&gt;&lt;code&gt;std::string&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f( int i, const std::string&amp;amp; s );

void oops( int some_param ) {
	char buffer[1024];
	sprintf(buffer, &quot;%i&quot;, some_param);
	std::thread t( f, 3, buffer );  // [Warning]: it might be dangling pointer
	t.detach();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;buffer&lt;/code&gt;是指向局部变量的一个指针，其被传递给新线程。&lt;strong&gt;在新线程上将其转换为&lt;/strong&gt;&lt;code&gt;std::string&lt;/code&gt;&lt;strong&gt;之前&lt;/strong&gt;，&lt;code&gt;oops&lt;/code&gt;&lt;strong&gt;函数可能退出，从而导致未定义行为&lt;/strong&gt;。因此，应该在传入&lt;code&gt;buffer&lt;/code&gt;前，将其转换为&lt;code&gt;std::string&lt;/code&gt;就能够解决。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f( int i, const std::string&amp;amp; s );

void not_oops( int some_param ) {
	char buffer[1024];
	sprintf(buffer, &quot;%i&quot;, some_param);
	std::thread t( f, 3, std::string( buffer ) );   // Using std::string avoids dangling pointer
	                                                // [Warning]: convert too late
	t.detach();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，我们想要通过显式地提前转换&lt;code&gt;buffer&lt;/code&gt;为对应的类型，&lt;strong&gt;但是这个转换可能发生的太晚，从而导致没有产生出预期类型和结果&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而这种情况：参数是被拷贝的，而你想要一个&lt;code&gt;non-const&lt;/code&gt;的引用，这是不可能的，会导致编译错误。如下面所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void update_data_for_widget( widget_id w, widget_data&amp;amp; data );
void oops_again( widget_id w ) {
	widget_data data;
	// [Error]: std::thread arguments must be invocable after conversion to rvalues
	std::thread t ( update_data_for_widget, w, data );   
	display_status();
	t.join();
	process_widget_data( data ); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管&lt;code&gt;update_data_for_widget&lt;/code&gt;函数的第二个参数要求是&lt;code&gt;widget_data&lt;/code&gt;的引用类型，但是&lt;code&gt;std::thread&lt;/code&gt;的构造函数并不认识这个类型；&lt;strong&gt;因为它忽略了其所期待的参数类型，只是盲目的将参数给拷贝过去。并且，其内部实现也只是将其以右值的形式传递过去，因为整个&lt;/strong&gt;&lt;code&gt;std::thread&lt;/code&gt;&lt;strong&gt;的构造函数都被设置为是右值的&lt;/strong&gt;。这就导致了编译错误，因为你&lt;strong&gt;无法传递一个右值给一个&lt;/strong&gt;&lt;code&gt;non-const&lt;/code&gt;&lt;strong&gt;类型&lt;/strong&gt;。如果你熟悉&lt;code&gt;std::bind&lt;/code&gt;的话，那么很轻易就能够想到，我们需要将这个参数使用&lt;code&gt;std::ref&lt;/code&gt;进行封装成为引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread t ( update_data_for_widget, w, std::ref( data ) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改之后，就能够成功通过编译，并且传递一个引用的参数进入函数内部。&lt;/p&gt;
&lt;p&gt;如果你熟悉&lt;code&gt;std::bind&lt;/code&gt;，那么对于&lt;code&gt;std::thread&lt;/code&gt;的参数传递语义就不那么难以理解，因为&lt;code&gt;std::thread&lt;/code&gt;&lt;strong&gt;的构造器和&lt;/strong&gt;&lt;code&gt;std::bind&lt;/code&gt;&lt;strong&gt;的实现机制是相同的&lt;/strong&gt;。这就意味着，你可以传递成员函数指针作为函数，并提供一个合适的对象指针作为第一个参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class X {
public:
    void do_lengthy_work();
};

X my_x;
std::thread t ( &amp;amp;X::do_lengthy_work, &amp;amp;my_x );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码会在新线程中调用&lt;code&gt;my_x.do_lengthy_work()&lt;/code&gt;，因为&lt;code&gt;my_x&lt;/code&gt;的地址作为对象指针被提供给了线程的构造函数。还可以为成员函数提供参数，构造函数的第三个参数就会作为第一个参数，以此类推。&lt;/p&gt;
&lt;p&gt;另一个提供参数的有趣场景是：&lt;strong&gt;参数不能被拷贝，只能被&lt;/strong&gt;&lt;code&gt;move(移动)&lt;/code&gt;。这种类型的一个例子就是&lt;code&gt;std::unique&lt;/code&gt;，其为动态分配的对象提供自动内存管理。每次只有一个&lt;code&gt;std::unique&lt;/code&gt;实例指向所给定的对象，当该实例被销毁时，其指向的对象也会被删除。&lt;code&gt;move constructor&lt;/code&gt;和&lt;code&gt;move assignment operator&lt;/code&gt;允许在&lt;code&gt;std::unique&lt;/code&gt;实例之间转交对象的所有权。如果是临时对象，那么移动则会自动发生；如果是有名对象，那么就需要通过&lt;code&gt;std::move&lt;/code&gt;来显式地指定移动语义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void process_big_object( std::unique_ptr&amp;lt; big_object &amp;gt; );
std::unique_ptr&amp;lt; big_object &amp;gt; p ( new big_object );
p-&amp;gt;prepare_data( 42 );
std::thread t ( process_big_object, std::move(p) );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这种所有权可以在实例之间进行转移&lt;/strong&gt;，因为&lt;code&gt;std::thread&lt;/code&gt;的&lt;strong&gt;实例是可移动的，尽管它们不可复制。这确保在任何时候只有一个对象与特定的执行线程相关联，同时允许我们选择在对象之间转移该所有权&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Transferring ownership of a thread&lt;/h2&gt;
&lt;p&gt;假设你想编写一个函数，创建一个后台线程来运行，但是将新线程的所有权交回给调用函数，而不是等待它完成；或者你可能想要相反的情况：创建一个线程，并将所有权传递给某个函数，该函数应该等待它完成。在任何情况下，你都需要将所有权从一个地方转移到另一个地方。&lt;/p&gt;
&lt;p&gt;这就是&lt;code&gt;std::thread&lt;/code&gt;支持移动语义的所在之处。这就意味着一个执行线程的所有权是能够在&lt;code&gt;std::thread&lt;/code&gt;实例之间移动的，下面的例子展示了创建两个执行线程，并在三个&lt;code&gt;std::thread&lt;/code&gt;实例&lt;code&gt;t1&lt;/code&gt;、&lt;code&gt;t2&lt;/code&gt;和&lt;code&gt;t3&lt;/code&gt;之间转移这些线程的所有权：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void some_function();
void some_other_function();
std::thread t1( some_function );
std::thread t2 = std::move( t1 );
t1 = std::thread( some_other_function );
std::thread t3;
t3 = std::move( t2 );
t1 = std::move( t3 );   // [Warning]: This assignment will terminate the program!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;t1 = std::move( t3 )&lt;/code&gt;执行之前，我们可以看见线程实例的所有权在它们之中相互传递，这都是能够正常进行的。但是，当&lt;code&gt;t1 = std::move( t3 )&lt;/code&gt;执行时，我们可以发现，此时&lt;code&gt;t1&lt;/code&gt;本就关联了一个线程实例，当我们移动&lt;code&gt;t3&lt;/code&gt;的所有权给&lt;code&gt;t1&lt;/code&gt;时，&lt;code&gt;t1&lt;/code&gt;原本的资源便会在没有&lt;code&gt;join()&lt;/code&gt;或&lt;code&gt;detach()&lt;/code&gt;的情况下被遗失，因此，就会调用&lt;code&gt;std::terminate()&lt;/code&gt;。这样做是为了保证&lt;code&gt;std::thread&lt;/code&gt;析构的一致性：&lt;strong&gt;不能通过给管理线程的&lt;/strong&gt;&lt;code&gt;std::thread&lt;/code&gt;&lt;strong&gt;对象赋一个新值来删除线程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::thread&lt;/code&gt;支持移动语义就意味着&lt;strong&gt;所有权可以很容易地从函数中转移出来&lt;/strong&gt;，下面就是一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread f() {
	void some_function();
	return std::thread( some_function );
}

std::thread g() {
	void some_other_function( int );
	std::thread t( some_other_function, 42 );
	return t;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，如果所有权需要被转交给一个函数，它可以按值接收一个&lt;code&gt;std::thread&lt;/code&gt;的参数，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void f( std::thread t );
void g() {
	void some_function();
	f( std::thread( some_function ) );
	std::thread t( some_function );
	f( std::move( t ) );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;std::thread&lt;/code&gt;支持移动语义的一个好处就在于：：&lt;strong&gt;能够让一些需要的地方获取它的所有权&lt;/strong&gt;。就比如在先前我们所创建的&lt;code&gt;thread_guard&lt;/code&gt;类，我们就能够在此基础上进行修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class scoped_thread {
	std::thread _t;

public:
	explicit scoped_thread( std::thread t ): _t( std::move(t) ) {
		if ( !t.joinable() )
			throw std::logic_error( &quot;No thread&quot; );
	}
	~scoped_thread() {
		_t.join();
	}
	scoped_thread( const scoped_thread&amp;amp; ) = delete;
	scoped_thread&amp;amp; operator= ( const scoped_thread&amp;amp; ) = delete;
};

struct func;
void f() {
	int some_local_state;
	scoped_thread t { std::thread( func( some_local_state ) ) };
	do_something_in_current_thread();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个示例和&lt;code&gt;thread_guard&lt;/code&gt;类似，但是新线程直接被传递给&lt;code&gt;scoped_thread&lt;/code&gt;，不必创建一个单独的命名变量。同时，当&lt;code&gt;scoped_thread&lt;/code&gt;被销毁时，不需要判断线程是否可连接，直接调用&lt;code&gt;join()&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;C++17&lt;/code&gt;中有一个关于&lt;code&gt;joining_thread&lt;/code&gt;的草案，其与&lt;code&gt;std::thread&lt;/code&gt;类似，但会像&lt;code&gt;scoped_thread&lt;/code&gt;那样自动在析构函数中调用&lt;code&gt;join()&lt;/code&gt;。但该草案并没有在标准委员会中得到认可，反而是在&lt;code&gt;C++20&lt;/code&gt;中以&lt;code&gt;std::jthread&lt;/code&gt;的形式出现。下面给出了一个可能实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class joining_thread {
	std::thread _t;

public:
	joining_thread() noexcept = default;

	template &amp;lt; typename Callable, typename ... Args &amp;gt;
	explicit joining_thread(Callable&amp;amp;&amp;amp; func, Args&amp;amp;&amp;amp; ... args)
		: _t(std::forward&amp;lt; Callable &amp;gt;(func), std::forward&amp;lt; Args &amp;gt;(args)...) { }

	explicit joining_thread(std::thread t) noexcept
		: _t(std::move(t)) { }

	joining_thread(joining_thread&amp;amp;&amp;amp; rhs) noexcept
		: _t(std::move(rhs._t)) { }

	joining_thread&amp;amp; operator=(joining_thread&amp;amp;&amp;amp; rhs) noexcept {
		if ( joinable()) join();
		_t = std::move(rhs._t);
		return *this;
	}

	joining_thread&amp;amp; operator=(std::thread other) noexcept {
		if ( joinable()) join();
		_t = std::move(other);
		return *this;
	}

	~joining_thread() noexcept {
		if ( joinable()) join();
	}

public:
	void swap(joining_thread&amp;amp; other) noexcept { _t.swap(other._t); }
	bool joinable() const noexcept { return _t.joinable(); }
	void join() { _t.join(); }
	void detach() { _t.detach(); }
	std::thread&amp;amp; as_thread() noexcept { return _t; }
	const std::thread&amp;amp; as_thread() const noexcept { return _t; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;std::thread&lt;/code&gt;移动语义的支持还允许使用std::thread对象的容器，比如说更新后的&lt;code&gt;std::vector&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void do_work(int id);
void f() {
    std::vector&amp;lt;std::thread&amp;gt; threads;
    for (int i = 0; i &amp;lt; 20; i++) {
        threads.emplace_back(do_work, i);   // Spawns threads
    }
    for (auto&amp;amp; entry : threads) 
        entry.join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果线程被用来划分算法的工作，通常需要满足以下条件：&lt;strong&gt;在返回给调用者之前，所有线程必须都已完成&lt;/strong&gt;。上面的代码的简单结构意味着线程所执行的工作是自包含的，它们的操作结果仅仅是对共享数据的副作用。如果f&lt;code&gt;()&lt;/code&gt;&lt;strong&gt;函数需要向调用者返回一个依赖于这些线程操作结果的值，那么按照目前的写法，这个返回值必须通过在线程终止后检查共享数据来确定&lt;/strong&gt;。在后面的第四篇笔记中，我们将讨论在线程之间传递操作结果的其他方案。&lt;/p&gt;
&lt;p&gt;将&lt;code&gt;std::thread&lt;/code&gt;对象放在&lt;code&gt;std::vector&lt;/code&gt;中是朝着自动化管理这些线程迈出的一步:&lt;strong&gt;不必为这些线程创建单独的变量并直接与它们连接，而是可以将它们视为一个组&lt;/strong&gt;。你可以更进一步，在运行时创建动态线程数，而不是创建固定数量。&lt;/p&gt;
&lt;h2&gt;Choosing the number of threads at runtime&lt;/h2&gt;
&lt;p&gt;$C++$标准库中一个有用的功能就是：&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/thread/hardware_concurrency&quot;&gt;std::thread::hardware_concurrency()&lt;/a&gt;。这个函数返回一个指示，&lt;strong&gt;表明在给定的程序执行过程中可以真正并发运行的线程数&lt;/strong&gt;。在一个多核系统中，这个数量可能是$CPU$的核心数。&lt;/p&gt;
&lt;p&gt;下面是一个代码示例，展示了一个简单的&lt;a href=&quot;https://en.cppreference.com/w/cpp/algorithm/accumulate&quot;&gt;std::accumulate()&lt;/a&gt;的并行化版本的实现。在实际中，你可能还会想要使用并行化的&lt;code&gt;std::reduce&lt;/code&gt;算法而非自行实现，这将会在第十篇笔记中讲解。在下面的示例中，它将会划分每个线程的任务数量，使得每个线程都执行一个较少数量的任务，以避免额外过多开销。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt; typename Iterator, typename T &amp;gt;
struct accumulate_block {
	void operator() ( Iterator first, Iterator last, T&amp;amp; result ) {
		result = std::accumulate( first, last, result );
	}
};

template &amp;lt; typename Iterator, typename T &amp;gt;
T parallel_accumulate( Iterator first, Iterator last, T init ) {
	const uint64_t length = std::distance( first, last );
	if ( !length ) return init;
	const uint64_t min_per_thread = 25;
	const uint64_t max_threads = ( length + min_per_thread - 1 ) / min_per_thread;
	const uint64_t hardware_threads = std::thread::hardware_concurrency();
	const uint64_t num_threads = std::min(
		hardware_threads != 0 ? hardware_threads : 2,
		max_threads );
	const uint64_t block_size = length / num_threads;

	std::vector&amp;lt; T &amp;gt; results( num_threads );
	std::vector&amp;lt; std::thread &amp;gt; threads( num_threads - 1 );
	Iterator block_start = first;
	for (uint64_t i = 0; i &amp;lt; num_threads - 1; i++) {
		Iterator block_end = block_start;
		std::advance( block_end, block_size );
		threads[i] = std::thread{
			accumulate_block&amp;lt; Iterator, T &amp;gt; (),
			block_start, block_end, std::ref( results[i] )
		};
		block_start = block_end;
	}
	accumulate_block&amp;lt; Iterator, T &amp;gt;() (
		block_start, last, results[num_threads - 1]
	);
	for (auto&amp;amp; thread : threads) thread.join();

	return std::accumulate(results.begin(), results.end(), init);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尽管这个代码看上去比较长，但实际上简单易懂。假定你的机器有$32$个核心。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一步，确定任务所需的最大线程数
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果输入的范围为空，就应该返回作为&lt;/strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;strong&gt;参数的初始值作为返回值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;否则，只要有一个元素在&lt;code&gt;range&lt;/code&gt;中，你就能将要处理的元素数量除以&lt;code&gt;block_size&lt;/code&gt;，就得到所需要的最大的线程数量&lt;code&gt;max_threads&lt;/code&gt;(这一步是为了避免创建过多的线程资源)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第二步，计算真正运行的线程数
&lt;ul&gt;
&lt;li&gt;我们不希望运行超过硬件所能支持的线程数(这被称为&lt;code&gt;oversubscription(超额订阅)&lt;/code&gt;)，因为上下文切换将意味着更多的线程将降低性能。&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;std::thread::hardware_concurrency()&lt;/code&gt;返回了$0$，那么就用自己设置的值作为代替，这里的代替值是$2$&lt;/li&gt;
&lt;li&gt;否则，就用实际的硬件线程数量与我们所得到的最大线程数量取最小值，这样就得出真正运行的线程数量&lt;code&gt;num_threads&lt;/code&gt;(&lt;strong&gt;因为如果线程过多，就会导致单核上的性能下降；如果线程过少，又不能实现并行化&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第三步，计算每一个线程应该执行的任务数
&lt;ul&gt;
&lt;li&gt;每个线程要处理的条目数是该任务范围的长度&lt;code&gt;block_size&lt;/code&gt;除以真实运行的线程数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第四步，创建线程空间和结果保存的空间
&lt;ul&gt;
&lt;li&gt;现在你已经计算出真正线程运行的数量以及每个线程将要执行的任务数量，所以就应该给出对应的资源&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::vector&amp;lt; T &amp;gt; results&lt;/code&gt;作为每个线程运行结果存储的位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::vector&amp;lt; std::thread &amp;gt; threads&lt;/code&gt;作为每个线程运行的空间和管理的容器(&lt;strong&gt;你需要创建比我们计算出的真实运行的线程数少于一个的空间，因为运行时线程也能够参与计算&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第五步，划分每一个线程需要计算的任务块
&lt;ul&gt;
&lt;li&gt;通过一个简单的循环来划分每一个线程所执行的范围$[first, last)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::advance( block_end, block_size )&lt;/code&gt;和&lt;code&gt;block_start = block_end;&lt;/code&gt;是确定任务区间的主要逻辑；&lt;code&gt;accumulate_block&amp;lt; Iterator, T &amp;gt; ()&lt;/code&gt;则是线程运行的实际逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第六步，累加所有结果
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;results&lt;/code&gt;中存放了每一个线程所计算的对应任务区间的长度，通过&lt;code&gt;std::accumulate(results.begin(), results.end(), init)&lt;/code&gt;就能够得到其最终结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里还有一些点需要注意：&lt;strong&gt;迭代器至少是&lt;/strong&gt;&lt;code&gt;forward iterator(向前迭代器)&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;&lt;strong&gt;类型必须是可默认构造的，以便能够创建&lt;/strong&gt;&lt;code&gt;results&lt;/code&gt;。同时，&lt;strong&gt;我们并不能在线程中直接的返回一个值&lt;/strong&gt;，这在后续的第四篇笔记中通过&lt;code&gt;future&lt;/code&gt;解决；在第八篇笔记中，我会深入的解析并行算法；而在第十篇笔记，则会介绍一些$C++17$标准库中的并行算法。&lt;/p&gt;
&lt;p&gt;在这个例子中，我们所有的数据都是在线程初始化时传递的(包括计算结果所要存放的位置)。事实上，我们总会有一些数据是需要通过在运行中才能得出的，比如说我们需要定位某一个线程并获取其位于哪一层调用栈。因此在$C++$标准库的设计中，每一个线程都有一个独一无二的标识。&lt;/p&gt;
&lt;h2&gt;Identifying threads&lt;/h2&gt;
&lt;p&gt;线程标识符的类型为&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/thread/id&quot;&gt;std::thread::id&lt;/a&gt;，其有两种方式能够获取。第一种，线程标识符可以通过调用&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/thread/get_id&quot;&gt;get_id()&lt;/a&gt;从与之关联的线程对象中获取；另一种方式是通过&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/get_id&quot;&gt;std::this_thread::get_id()&lt;/a&gt;获取到&lt;strong&gt;当前线程&lt;/strong&gt;的线程标识符。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::thread::id&lt;/code&gt;类型的实例&lt;strong&gt;可以随意地拷贝和比较&lt;/strong&gt;，否则它就毫无意义。如果两个实例相等，那么就标识它们是同一个线程或都为&lt;code&gt;not any thread&lt;/code&gt;值。&lt;/p&gt;
&lt;p&gt;$C++$标准库&lt;strong&gt;不仅不限制您只能检查线程标识符是否相同，而且&lt;/strong&gt;&lt;code&gt;std::thread::id&lt;/code&gt;&lt;strong&gt;类型的对象提供了完整的比较运算符集合，为所有不同的值提供了完全的排序。这使得它们可以用作关联容器中的键，或进行排序，或以任何你认为合适的其他方式进行比较。比较运算符为所有非相等的&lt;/strong&gt;&lt;code&gt;std::thread::id&lt;/code&gt;&lt;strong&gt;值提供了一个完全的顺序&lt;/strong&gt;，因此它们的行为符合您的直觉期望：&lt;strong&gt;如果$a \lt b$且$b \lt c$，那么$a \lt c$，依此类推&lt;/strong&gt;。标准库还提供了&lt;a href=&quot;https://en.cppreference.com/w/cpp/thread/thread/id/hash&quot;&gt;std::hash&amp;lt; std::thread::id &amp;gt;&lt;/a&gt;，&lt;strong&gt;以便&lt;/strong&gt;&lt;code&gt;std::thread::id&lt;/code&gt;&lt;strong&gt;类型的值也可以用作新的无序关联容器中的键&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::thread::id&lt;/code&gt;&lt;strong&gt;的实例通常用于检查线程是否需要执行某些操作&lt;/strong&gt;，就如下面的实例所展示的一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::thread::id master_thread;
void some_core_part_of_algorithm() {
	if ( std::this_thread::get_id() == master_thread ) {
		do_master_thread_work();
	}
	do_common_thread_work();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，当前线程的&lt;code&gt;std::thread::id&lt;/code&gt;可以作为操作的一部分存储在数据结构中。然后，对同一数据结构的后续操作可以检查存储的线程$id$与执行操作的线程$id$之间的关系，以确定允许(或需要)执行的操作。&lt;/p&gt;
&lt;p&gt;类似地，&lt;strong&gt;线程$id$可以用作关联容器中的键，用于将特定数据与线程关联起来，但不适合使用其他机制，例如线程局部存储。这样的容器可以由控制线程使用，用于存储控制下的每个线程的信息，或者用于在线程之间传递信息&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>Concurrency in Action: Introduction</title><link>https://chenmiaoi.github.io/2024/06/12/Concurrency-in-Action-Introduction/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/06/12/Concurrency-in-Action-Introduction/</guid><pubDate>Wed, 12 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本系列笔记是关于如何使用C++写出多线程并发的应用以及关于C++对于多线程的特性、库工具做出解析。仍然需要注意的是，该系列的笔记&lt;strong&gt;只是个人理解下的观点，如有错误，请通过email发送信息给我&lt;/strong&gt;(&lt;a href=&quot;mailto:chenmiao.ku@gmail.com&quot;&gt;chenmiao.ku@gmail.com&lt;/a&gt;)。&lt;/p&gt;
&lt;h2&gt;What is concurrency?&lt;/h2&gt;
&lt;p&gt;在最简单、基本的感知上，&lt;strong&gt;并发是指两个或多个独立活动同时发生&lt;/strong&gt;。并发在我们的日常生活中的一部分：我们可以一边走路一边说话或者是用每只手执行多个不同的行动，亦或是我们个人独立的生活着......&lt;/p&gt;
&lt;h3&gt;Concurrency in computers systems&lt;/h3&gt;
&lt;p&gt;每当我们谈论计算机术语：并发时。我们指的是&lt;strong&gt;一个独立的系统&lt;/strong&gt;并行的&lt;strong&gt;执行多个独立的活动，而不是顺序或一个接着一个地执行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从历史上看，大多数台式计算机都有一个处理器，其中只有一个处理单元或核心(当然，现在大多数计算机应该都是双核或多核)。像&lt;strong&gt;这样的计算机(单核)一次只能处理一个任务，但可以通过在任务之间不断切换任务。一个任务做一点，另一个任务再做一点的方式，这就看起来像任务正在发生并行&lt;/strong&gt;。这被叫做&lt;code&gt;任务切换(task switching)&lt;/code&gt;。需要注意的是，&lt;strong&gt;由于任务切换的速度过快，你无法判断上一个任务切换到下一个任务时，任务会在哪一个点挂起&lt;/strong&gt;。同时，&lt;strong&gt;任务切换给了用户和应用一个&lt;em&gt;并发的假象&lt;/em&gt;，这就使得应用的行为可能与真实在能够进行并发的计算机上运行时的有细微的不同&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在以前，包含多个处理器的计算机通常被用于服务器或高性能计算任务；如今，拥有多核的台式电脑逐渐普遍。&lt;strong&gt;无论这些计算机拥有多个处理器还是一个处理器中有用多个处理核心，它们都能够真正的&lt;em&gt;并行&lt;/em&gt;运行这些任务&lt;/strong&gt;。我们称之为&lt;code&gt;硬件并发(hardware concurrency)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下图展示了一个理想情况下，一个计算机刚好有两个任务，并且该两个任务刚好被切分为十个大小相等的块。在一个双核机器上，每一个任务都能够执行在自己的核心上；但在单核机器上，就需要进行任务切换，并且每一个任务块是交替的。一个系统为了交替执行任务，就必须从一个任务切换到另一个任务时执行&lt;code&gt;上下文切换(context switching)&lt;/code&gt;，并花费一定的时间开销。&lt;/p&gt;
&lt;p&gt;为了执行一个上下文切换，&lt;strong&gt;系统不得不保存&lt;em&gt;当前正在运行&lt;/em&gt;的$CPU$状态和指令指针，计算出要切换到哪一个任务，并为正在切换的任务重新加载到$CPU$状态&lt;/strong&gt;。&lt;strong&gt;然后，$CPU$可能需要将新任务的指令和数据加载到缓存中，这可能阻止$CPU$执行任何指令，导致进一步的延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406121520627.png&quot; alt=&quot;Two approaches to concurrency: parallel execution on a dual-core machine versus task switching on a single-core machine&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当操作系统决定要切换到另一个任务时，CPU需要加载新任务的指令和数据到缓存中。这是因为缓存是CPU用于快速访问内存中的数据的临时存储器。如果新任务的指令和数据不在缓存中，CPU就必须从主内存中读取它们，这需要更多的时间和资源。
这个加载过程可能会导致CPU无法执行任何指令，因为它必须等待新任务的数据在缓存中准备好。这会导致进一步的延迟，因为CPU无法继续执行其他指令，直到新任务的数据准备就绪。
一旦新任务的数据加载到缓存中，CPU就可以开始执行新任务的指令。但在此过程中，由于加载数据和等待的延迟，总体上会出现额外的延迟。
这种延迟是任务切换过程中不可避免的，但可以通过优化缓存和内存访问策略来减少其影响，以提高系统的性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然硬件中的并发性在多处理器或多核系统中最为明显，但有些处理器可以在单核上执行多个线程。&lt;strong&gt;需要考虑的重要因素就是&lt;/strong&gt;&lt;code&gt;硬件线程(hardware thread)&lt;/code&gt;&lt;strong&gt;的数量，这是衡量硬件可以真正并发运行多少独立任务的指标&lt;/strong&gt;。即使系统具有真正的硬件并发性，任务数量也很容易超过硬件可以并行运行的数量，因此在这些情况下仍旧进行任务切换。如下图所示，四个任务的任务切换发生在一个双核计算机上，和先前理想的情况一样，每个任务都被均分为多个小块。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406121540787.png&quot; alt=&quot;Task switching of four tasks on two cores&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Approaches to concurrency&lt;/h3&gt;
&lt;p&gt;现在举一个例子，假设一对程序员在一起做一个软件项目。如果你的开发人员在不同的办公室，他们可以和平地工作，不会被彼此打扰，并且他们每个人都有自己的一套参考手册。但显而易见的，沟通并非直截了当的；他们需要使用电话或电子邮件，或者站起来走到对方的办公室。此外，你还需要管理两个办公室，需要购买多份参考手册。&lt;/p&gt;
&lt;p&gt;现在，假如你将他们放在同一个办公室，他们现在可以随意的交流或讨论应用的设计。你只需要管理一个办公室，购买一份参考手册；但是，他们可能会发现很难集中注意力，并且出现资源问题(一份参考手册)。&lt;/p&gt;
&lt;p&gt;上述的两种方式解释了两种基本的并发实现的方式。每一位开发者就对应了一个线程，办公室就表示了一个进程，而参考手册便是资源。&lt;/p&gt;
&lt;h4&gt;CONCURRENCY WITH MULTIPLE PROCESSORS&lt;/h4&gt;
&lt;p&gt;在应用程序中使用并发的第一种方法是&lt;strong&gt;将应用程序划分为同时运行的多个独立的单线程进程&lt;/strong&gt;。然后，这些独立的进程可以通过所有正常的进程间通信通道(&lt;code&gt;signals&lt;/code&gt;、&lt;code&gt;sockets&lt;/code&gt;、&lt;code&gt;files&lt;/code&gt;...)相互传递消息。&lt;/p&gt;
&lt;p&gt;通过这种方式实现的缺点是：&lt;strong&gt;进程间通信要么建立起来很复杂，要么很慢，或者两者皆有；同时，运行多个进程存在固有的开销，启动一个进程需要开销，操作系统必须投入内部资源来管理这些进程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当然，也并非全是坏处：&lt;strong&gt;操作系统通常在进程和高级通信机制之间提供了额外的保护，这意味着使用进程而不是线程更容易编写安全的并发代码；并且，你可以在通过网络连接的不同机器上运行单独的进程。虽然这增加了通信成本，但在精心设计的系统上，它可以是增加可用并行性和提高性能的一种经济有效的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将多个进程通过网络分布在多个主机上，这叫做分布式计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202406121631041.png&quot; alt=&quot;communicate&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;CONCURRENCY WITH MULTIPLE THREADS&lt;/h4&gt;
&lt;p&gt;并发的另一种方式是&lt;strong&gt;在单个进程中运行多个线程&lt;/strong&gt;。&lt;strong&gt;线程很像&lt;em&gt;轻量级进程&lt;/em&gt;：每个线程独立于其他线程运行，每个线程可以运行不同的指令序列&lt;/strong&gt;。但是，&lt;strong&gt;一个进程中的所有线程共享相同的地址空间，并且大多数数据可以从所有线程直接访问————全局变量在全局保持，指针、对象或数据的引用可以在线程之间传递&lt;/strong&gt;。尽管线程之间可以共享进程中的地址空间，但是它们的建立和管理依旧是很复杂的，&lt;strong&gt;因为相同数据的内存地址在不同的进程中不一定相同&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享的地址空间和线程之间缺乏数据保护使得使用多个线程的开销比使用多个进程的开销要小得多&lt;/strong&gt;。但是&lt;strong&gt;共享内存的灵活性也有代价：如果数据是由多个线程访问的，那么应用程序员必须&lt;em&gt;确认每个线程所看到的数据视图在访问数据时是一致的&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;与在多个进程之间启动和通信相比，&lt;strong&gt;在一个进程内的多个线程之间的启动和通信的开销较低，这意味这是主流语言中最受欢迎的并发方式，尽管共享内存会产生潜在的问题&lt;/strong&gt;。再者，&lt;strong&gt;C++标准并没有为进程之间的通信提供任何内在的支持，因此&lt;em&gt;使用多个进程的应用程序将不得不依赖特定平台的API来实现这一点。&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Concurrency vs. Parallelism&lt;/h3&gt;
&lt;p&gt;在多线程代码上，&lt;code&gt;并发(Concurrency)&lt;/code&gt;和&lt;code&gt;并行(Parallelism)&lt;/code&gt;在意义上有很大部分的重叠。实际上，大多数时候它们指的就是同一件事；其区别主要在于细微差别、关注点和意图。&lt;/p&gt;
&lt;p&gt;这两个术语都与利用可用的硬件同时运行多个任务有关，但&lt;strong&gt;并行性更加注重性能&lt;/strong&gt;。&lt;strong&gt;当人们主要关注利用可用的硬件来提高大规模数据处理的性能时，他们谈论的是并行性&lt;/strong&gt;；&lt;strong&gt;而当人们主要关注关注点分离或响应性时，他们谈论的是并发性&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并行性强调的是同时执行多个任务以提高整体性能。它关注的是如何充分利用多核处理器或分布式系统等硬件资源来同时处理多个任务，以实现更快的数据处理。并行性的目标是通过同时处理多个任务来加速计算过程。&lt;/li&gt;
&lt;li&gt;而并发性更注重任务的分离和响应性。它关注的是如何同时处理多个任务，使它们能够并发地执行，而不是简单地顺序执行。并发性的目标是实现任务的交替执行，使系统能够更好地响应用户的请求，提高系统的吞吐量和响应时间。&lt;/li&gt;
&lt;li&gt;总的来说，并行性更注重性能优化和数据处理速度，而并发性更注重任务的分离和系统的响应性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why use concurrency?&lt;/h2&gt;
&lt;p&gt;在应用程序中使用并发性有两个主要原因：&lt;code&gt;关注点分离(separation of concern)&lt;/code&gt;和&lt;code&gt;性能(performance)&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关注点分离
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发可以帮助将复杂的应用程序分解为更小、更独立的部分，每个部分专注于处理特定的任务或功能&lt;/strong&gt;。通过将应用程序分成多个并发执行的部分，可以更好地组织和管理代码，提高代码的可读性和可维护性。不同的任务可以在不同的线程或进程中执行，彼此之间相互独立，从而实现了关注点分离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在这种情况下，线程的数量与可用的CPU核心数量是独立的，因为线程的划分是基于概念设计，而不是为了增加吞吐量&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;code&gt;任务并行(task parallelism)&lt;/code&gt;。&lt;strong&gt;每个线程对数据的不同部分执行相同的操作&lt;/strong&gt;，这被称为&lt;code&gt;数据并行(data parallelism)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那些容易受到这种并行性影响的算法通常被称为&lt;code&gt;embarrassingly parallel(尴尬并行)&lt;/code&gt;。尽管这个说法可能暗示你可能会为代码如此容易并行化而感到尴尬，但这是一件好事。我还遇到过其他形容这类算法的术语，如&lt;code&gt;naturally parallel(自然并行)&lt;/code&gt;和&lt;code&gt;conveniently concurrent(方便并发)&lt;/code&gt;。&lt;code&gt;Embarrassingly parallel&lt;/code&gt;算法具有良好的可扩展性特性——随着可用的硬件线程数量增加，算法中的并行性可以增加以匹配。这样的算法完美地体现了一句格言：&quot;人多好办事&quot;。对于那些不是尴尬并行的算法部分，你可能可以将算法划分为固定(因此不可扩展)数量的并行任务。&lt;/p&gt;
&lt;h3&gt;When not to use concurrency&lt;/h3&gt;
&lt;p&gt;知道什么时候不能使用并发和知道什么时候能使用并发同样重要。从根本上说，&lt;strong&gt;唯一一个不使用并发的原因是收益不值得付出其代价&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;除非潜在的性能收益足够大或关注点的分离足够清晰，以便证明为了正确实现并行性所需的额外开发时间和维护多线程代码所需的额外成本是合理的，否则不要使用并发性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当然，使用并发的性能增益可能没有你想想中的那么大；启动线程有一个固有的开销，&lt;strong&gt;因为操作系统必须得去分配相关的内核资源和堆栈空间，然后将新线程添加到调度器中，所有这些都需要时间。如果在线程上运行的任务很快就完成了，那么与启动线程的开销相比，任务所花费的时间可能会相形见绌，这可能导致应用程序的整体性能比直接由生成线程执行任务时更差&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此外，线程是一种有限的资源。如果同时运行太多的线程，这会消耗操作系统资源，并可能使整个系统运行速度变慢。不仅如此，使用太多的线程还会耗尽进程的可用内存或地址空间，因为每个线程都需要单独的堆栈空间。&lt;/p&gt;
&lt;p&gt;最后，运行的线程越多，操作系统必须进行的上下文切换就越多。&lt;strong&gt;每次上下文切换都会占用一些本来可以用来做有用工作的时间，因此在某些时候，添加一个额外的线程将降低而不是提高应用程序的整体性能&lt;/strong&gt;。由于这个原因，如果您试图实现系统的最佳性能，就有必要调整正在运行的线程数量，以考虑到可用的硬件并发性(或缺乏并发性)。&lt;/p&gt;
&lt;p&gt;使用并发性提高性能与其他任何优化策略一样：&lt;strong&gt;它有可能极大地提高应用程序的性能，但也可能使代码复杂化，使其更难以理解，更容易出现bug&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Concurrency and multithreading in C++&lt;/h2&gt;
&lt;p&gt;通过多线程对并发性的标准化支持对于&lt;code&gt;c++&lt;/code&gt;来说是一个相对较新的事物。只有从&lt;code&gt;c++11&lt;/code&gt;标准开始，你才能够编写多线程代码，而不需要借助特定于平台的扩展。&lt;/p&gt;
&lt;h3&gt;Efficiency in the C++ Thread Library&lt;/h3&gt;
&lt;p&gt;如果你追求最高的性能，&lt;strong&gt;了解使用任何高级功能相对于直接使用底层低级功能所带来的实现成本是非常重要的&lt;/strong&gt;。这个成本就是&lt;code&gt;抽象代价(abstraction penalty)&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Abstraction Penalty&lt;br /&gt;
&lt;strong&gt;抽象代价是指使用高级功能相对于直接使用底层低级功能所带来的性能开销&lt;/strong&gt;。封装的高级功能提供了更方便的接口和抽象层，但在底层实现上可能引入了额外的开销。这些开销可能包括资源消耗、额外的函数调用、内存分配和释放等。因此，在追求极致性能时，需要权衡使用高级功能所带来的便利性和抽象层与底层实现的开销之间的权衡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;C++标准委员会&lt;/code&gt;在设计&lt;code&gt;C++标准库&lt;/code&gt;的整体以及特别是&lt;code&gt;标准C++线程库&lt;/code&gt;时已经意识到了这一点。其中一个设计目标是，&lt;strong&gt;在提供相同功能的情况下，直接使用低级$API$应该没有或者几乎没有任何好处&lt;/strong&gt;。因此，&lt;strong&gt;该库的设计旨在允许在大多数主要平台上进行高效实现&lt;/strong&gt;(&lt;strong&gt;具有较低的抽象代价&lt;/strong&gt;)。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;C++标准委员会&lt;/code&gt;的另一个目标是&lt;strong&gt;确保C++为那些希望在接近硬件层面上获得最佳性能的开发人员提供足够的低级功能&lt;/strong&gt;。为了实现这一目标，除了新的内存模型之外，还引入了一个全面的原子操作库，用于直接控制单个位和字节的操作，以及线程间同步和可见性的任何变化。&lt;strong&gt;这些原子类型和相应的操作现在可以用于许多开发人员之前可能选择使用特定平台汇编语言的地方&lt;/strong&gt;。使用新的标准类型和操作的代码更具可移植性和易于维护。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有时，使用这些低级功能可能会带来性能成本，因为需要执行额外的代码。但是，这种性能成本并不一定意味着更高的抽象代价&lt;/strong&gt;。如果你追求性能，而使用高级功能的成本过高，你可能更适合使用低级功能手工实现所需的功能。在绝大多数情况下，额外的复杂性和错误的机会远远超过了从微小的性能提升中可能获得的潜在好处。&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;我们从一个最基本的&quot;Hello World&quot;程序作为例子，下面是一个单线程的执行代码，这将作为我们后续修改为多线程的一个基准：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

int main() {
  std::cout &amp;lt;&amp;lt; &quot;Hello World\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面则是&quot;Hello World&quot;多线程的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;thread&amp;gt;

void hello() {
  std::cout &amp;lt;&amp;lt; &quot;Hello Concurrency World\n&quot;;
}

int main() {
  std::thread t { hello };
  t.join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先能够发现的是，多了一个头文件&lt;code&gt;&amp;lt;thread&amp;gt;&lt;/code&gt;，这个头文件是管理线程的函数和类声明的文件；其他保护共享数据的函数和类则声明在其他头文件中。&lt;/p&gt;
&lt;p&gt;然后，我们的打印语句则是放在了一个单独的函数&lt;code&gt;hello()&lt;/code&gt;中，这是因为&lt;strong&gt;每一个线程都必须有一个初始函数，线程从这个初始函数开始执行&lt;/strong&gt;。因此，这里的&lt;code&gt;hello()&lt;/code&gt;函数作为了线程&lt;code&gt;t&lt;/code&gt;的初始函数。&lt;/p&gt;
&lt;p&gt;当我们的新线程启动后，初始线程(main)会继续执行。如果它没有等待新线程完成，它将继续到&lt;code&gt;main()&lt;/code&gt;结束并结束程序，这会导致&lt;strong&gt;可能在新线程运行前，程序就已经结束&lt;/strong&gt;。因此我们在新线程的语句下面需要调用&lt;code&gt;join()&lt;/code&gt;(该函数将在第二篇笔记中解释)。&lt;/p&gt;
&lt;p&gt;至此，一个多线程的&quot;Hello World&quot;就完成了。&lt;/p&gt;
</content:encoded></item><item><title>xv6: The Kernel Env</title><link>https://chenmiaoi.github.io/2024/05/17/xv6-The-Kernel-Env/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/05/17/xv6-The-Kernel-Env/</guid><pubDate>Fri, 17 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一节中，我详细介绍了&lt;code&gt;xv6-riscv&lt;/code&gt;的&lt;code&gt;start&lt;/code&gt;函数，里面对&lt;code&gt;xv6-riscv&lt;/code&gt;的内核执行特权级环境、中断环境和定时器中断进行了一系列配置。现在，让我们来看看&lt;code&gt;xv6-riscv&lt;/code&gt;内核态中的具体配置和实现。&lt;/p&gt;
&lt;h2&gt;Main Function&lt;/h2&gt;
&lt;p&gt;在本节中，我们只会对内核态的配置做出大概介绍，具体的介绍在后续会为每一个模块做出详细章节来讲述。&lt;/p&gt;
&lt;p&gt;我们从&lt;code&gt;xv6-riscv&lt;/code&gt;的源码中也可以看出&lt;code&gt;main&lt;/code&gt;函数主要是对各种模块进行启用，然后调用&lt;code&gt;initcode&lt;/code&gt;进入到用户态中，完成整个操作系统的启动。&lt;/p&gt;
&lt;p&gt;需要完全的理解该代码，我们需要进行一些知识的准备。&lt;/p&gt;
&lt;h3&gt;volatile type&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Every access (both read and write) made through a lvalue expression of volatile-qualified type is considered an observable side effect for the purpose of optimization and is evaluated strictly according to the rules of the abstract machine (that is, all writes are completed at some time before the next sequence point). This means that within a single thread of execution, a volatile access cannot be optimized out or reordered relative to another visible side effect that is separated by a sequence point from the volatile access&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;volatile&lt;/code&gt;限定类型的左值表达式的每次访问(无论是读取还是写入)都被视为一个可观察的副作用，并且严格按照抽象机器的规则进行评估(也就是说，所有写操作都会在下一个序列点之前完成)。&lt;strong&gt;这意味着在单个线程的执行过程中，一个&lt;/strong&gt;&lt;code&gt;volatile&lt;/code&gt;&lt;strong&gt;访问不能被优化掉，也不能相对于另一个由序列点分隔的可见副作用进行重新排序&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通常，编译器会优化代码以提高执行效率。例如，如果一个变量在代码中没有被显式修改，编译器可能会将其值缓存到寄存器中，以避免多次访问内存。然而，对于某些变量(如硬件寄存器、共享内存、信号处理程序中的变量等)，它们的值可能在程序之外被修改。如果编译器对这些变量进行优化，可能会导致程序读取的值不是最新的，从而引发错误。&lt;code&gt;volatile&lt;/code&gt;&lt;strong&gt;关键字通知编译器不要对这些变量进行优化，每次都应从内存中读取它们的最新值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在多线程环境中，多个线程可能会访问和修改同一个变量。使用&lt;code&gt;volatile&lt;/code&gt;可以&lt;strong&gt;确保一个线程对该变量所做的修改能立即被其他线程看到&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;值得注意的：&lt;code&gt;volatile&lt;/code&gt;&lt;strong&gt;仅保证了变量的最新值读取，不提供任何同步机制。如果多个线程同时读写同一个变量，还需要使用适当的同步机制(如互斥锁)来确保操作的原子性和一致性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在某些体系结构和编译器实现中，即使使用了&lt;code&gt;volatile&lt;/code&gt;，依然可能需要额外的内存屏障(&lt;code&gt;memory barrier&lt;/code&gt;)指令来确保正确的内存访问顺序。&lt;/p&gt;
&lt;h3&gt;__sync_synchronize&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;__sync_synchronize&lt;/code&gt;是&lt;code&gt;GCC&lt;/code&gt;提供的一种内存屏障(&lt;code&gt;memory barrier&lt;/code&gt;或&lt;code&gt;memory fence&lt;/code&gt;)，&lt;strong&gt;用于确保在多处理器系统上进行的内存操作按照程序指定的顺序执行。它在涉及多线程编程时尤其有用，能够防止编译器和CPU对内存操作进行不正确的重排序&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;防止重排
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__sync_synchronize&lt;/code&gt;用于阻止编译器和&lt;code&gt;CPU&lt;/code&gt;对内存操作进行重排序。&lt;strong&gt;这样可以确保在它之前的内存操作在它之后的内存操作之前完成&lt;/strong&gt;。这对于多线程程序中的同步非常重要，能确保不同线程对共享数据的访问顺序正确&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;确保内存可见性
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在调用该函数之前进行的所有内存写操作在其他处理器和线程中变得可见&lt;/strong&gt;。因此，它可以用于实现内存可见性保证，确保一个线程所做的更改对其他线程可见&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;

volatile int flag = 0;
int data = 0;

void *thread1_func(void *arg) {
    data = 42;             // 写入数据
    __sync_synchronize();  // 内存屏障，确保写操作完成
    flag = 1;              // 设置标志，通知另一个线程
    return NULL;
}

void *thread2_func(void *arg) {
    while (flag == 0);     // 等待标志
    __sync_synchronize();  // 内存屏障，确保读取最新的data
    printf(&quot;Data: %d\n&quot;, data);  // 输出data
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    pthread_create(&amp;amp;thread1, NULL, thread1_func, NULL);
    pthread_create(&amp;amp;thread2, NULL, thread2_func, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程1在写入&lt;code&gt;data&lt;/code&gt;后调用&lt;code&gt;__sync_synchronize&lt;/code&gt;来确保在设置&lt;code&gt;flag&lt;/code&gt;之前，&lt;code&gt;data&lt;/code&gt;的写操作已经完成并且对其他线程可见。线程2在读取到&lt;code&gt;flag&lt;/code&gt;为&lt;code&gt;1&lt;/code&gt;后调用&lt;code&gt;__sync_synchronize&lt;/code&gt;来确保在读取&lt;code&gt;data&lt;/code&gt;之前，所有之前的内存操作(包括线程1对&lt;code&gt;data&lt;/code&gt;的写操作)都已经完成并且对当前线程可见。&lt;/p&gt;
&lt;p&gt;这样就使得，不论如何，线程2所看见的&lt;code&gt;data&lt;/code&gt;的数据总是线程1已经写入完毕后的&lt;code&gt;data&lt;/code&gt;数据，而不会发生线程1正在写的时候(并未写入)，线程2访问&lt;code&gt;data&lt;/code&gt;得到了一个错误的数据。&lt;/p&gt;
&lt;h3&gt;Main Code&lt;/h3&gt;
&lt;p&gt;在了解上面的前置知识后，我们就可以逐行解析&lt;code&gt;xv6-riscv&lt;/code&gt;内核的大致流程了:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void main() {
    if (cpuid() == 0) {
        consoleinit();
        printfinit();
        printf(&quot;\n&quot;);
        printf(&quot;xv6 kernel is booting\n&quot;);
        printf(&quot;\n&quot;);
        kinit();            // physical page allocator
        kvminit();          // create kernel page table
        kvminithart();      // turn on paging
        procinit();         // process table
        trapinit();         // trap vectors
        trapinithart();     // install kernel trap vector
        plicinit();         // set up interrupt controller
        plicinithart();     // ask PLIC for device interrupts
        binit();            // buffer cache
        iinit();            // inode table
        fileinit();         // file table
        virtio_disk_init(); // emulated hard disk
        userinit();         // first user process
        __sync_synchronize();
        started = 1;
    } else {
        while (started == 0)
            ;
        __sync_synchronize();
        printf(&quot;hart %d starting\n&quot;, cpuid());
        kvminithart();  // turn on paging
        trapinithart(); // install kernel trap vector
        plicinithart(); // ask PLIC for device interrupts
    }
    
    scheduler();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;volatile static int started = 0&lt;/code&gt;：定义一个静态变量&lt;code&gt;started&lt;/code&gt;，用来协调各个&lt;code&gt;hart&lt;/code&gt;的启动。&lt;code&gt;volatile&lt;/code&gt;关键字确保每次访问该变量时，都从内存中读取最新的值，而不是从寄存器中读取缓存值。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;xv6-riscv&lt;/code&gt;中，我们将$hartid = 0$的&lt;code&gt;hart&lt;/code&gt;标识为主要的执行流，主&lt;code&gt;hart&lt;/code&gt;执行一系列初始化函数，如&lt;code&gt;consoleinit()&lt;/code&gt;、&lt;code&gt;kinit()&lt;/code&gt;、&lt;code&gt;kvminit()&lt;/code&gt;等，这些函数负责&lt;strong&gt;初始化控制台&lt;/strong&gt;、&lt;strong&gt;物理内存分配器&lt;/strong&gt;、&lt;strong&gt;内核页表&lt;/strong&gt;、&lt;strong&gt;进程表&lt;/strong&gt;、&lt;strong&gt;trap vector&lt;/strong&gt;、&lt;strong&gt;中断控制器&lt;/strong&gt;、&lt;strong&gt;缓存&lt;/strong&gt;、&lt;strong&gt;inode表&lt;/strong&gt;、&lt;strong&gt;文件表&lt;/strong&gt;和&lt;strong&gt;虚拟磁盘&lt;/strong&gt;等。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;__sync_synchronize()&lt;/code&gt;在&lt;code&gt;started&lt;/code&gt;变量赋值之前调用内存屏障，确保所有的初始化操作都在赋值之前完成。这样就使得其他&lt;code&gt;hart&lt;/code&gt;流执行时，访问到的&lt;code&gt;started&lt;/code&gt;数据一定是$started == 1$的。&lt;/p&gt;
&lt;p&gt;其他的&lt;code&gt;hart&lt;/code&gt;流执行需要等待主要&lt;code&gt;hart&lt;/code&gt;流执行完毕，直到$started == 1$为止，&lt;code&gt;__sync_synchronize()&lt;/code&gt;使得&lt;code&gt;hart&lt;/code&gt;流能够看见主要&lt;code&gt;hart&lt;/code&gt;执行流的一切初始化操作，然后&lt;code&gt;hart&lt;/code&gt;执行自己的初始化函数，如&lt;code&gt;kvminithart()&lt;/code&gt;、&lt;code&gt;trapinithart()&lt;/code&gt;和&lt;code&gt;plicinithart()&lt;/code&gt;，这些函数负责启用分页、安装内核陷阱向量、请求设备中断等。&lt;/p&gt;
&lt;p&gt;最终，所有&lt;code&gt;hart&lt;/code&gt;(包括主&lt;code&gt;hart&lt;/code&gt;和从&lt;code&gt;hart&lt;/code&gt;)都进入调度器，以开始调度和执行任务。值得注意的是，第一个任务是&lt;strong&gt;跳转到用户态执行&lt;/strong&gt;&lt;code&gt;initcode&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里给出对应的部分逻辑(在此不做过多介绍)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
  ...
};

uvmfirst(p-&amp;gt;pagetable, initcode, sizeof(initcode));
p-&amp;gt;trapframe-&amp;gt;epc = 0;     // user program counter
p-&amp;gt;state = RUNNABLE;

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看见，&lt;code&gt;userinit&lt;/code&gt;会在内部调用&lt;code&gt;uvmfirst&lt;/code&gt;函数来设置第一个任务为&lt;code&gt;initcode&lt;/code&gt;，然后通过&lt;code&gt;scheduler&lt;/code&gt;函数跳转到&lt;code&gt;initcode.S&lt;/code&gt;中执行。而&lt;code&gt;initcode&lt;/code&gt;中通过&lt;code&gt;exec&lt;/code&gt;程序调用了用户态的&lt;code&gt;init&lt;/code&gt;程序，&lt;code&gt;init&lt;/code&gt;程序又会调用&lt;code&gt;sh&lt;/code&gt;执行&lt;code&gt;shell&lt;/code&gt;环境，然后就正式进入了用户态，并且终端上呈现出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init: starting sh
#  
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>xv6: The Starter</title><link>https://chenmiaoi.github.io/2024/05/16/xv6-The-Starter/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/05/16/xv6-The-Starter/</guid><pubDate>Thu, 16 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在上一节中，我们已经了解了&lt;code&gt;xv6-riscv&lt;/code&gt;是如何从&lt;code&gt;qemu&lt;/code&gt;中引导内核到指定入口点，并从该入口点进入转跳到特定&lt;code&gt;start&lt;/code&gt;函数的。那么现在，我们就来解析&lt;code&gt;start&lt;/code&gt;函数是如何工作的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Start Function&lt;/h2&gt;
&lt;p&gt;在开始之前，我们需要知道的是，在&lt;code&gt;start&lt;/code&gt;函数中，&lt;strong&gt;我们并未真正地进入内核&lt;/strong&gt;。这是什么意思呢？实际上就是，&lt;code&gt;start&lt;/code&gt;&lt;strong&gt;函数实际上还是属于引导程序的一部分，其最终的目的就是跳转到&lt;/strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;strong&gt;函数中，从而进入内核态执行&lt;/strong&gt;。同时，有一个最浅显的概念是：&lt;code&gt;xv6-riscv&lt;/code&gt;的内核态是运行在&lt;code&gt;S&lt;/code&gt;态的，而非&lt;code&gt;M&lt;/code&gt;态，因此，我们从入口点转跳到&lt;code&gt;start&lt;/code&gt;函数中时，其仍是&lt;code&gt;M&lt;/code&gt;态。也就是说，&lt;code&gt;start&lt;/code&gt;&lt;strong&gt;函数的另一个目的就是需要为进入&lt;/strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;strong&gt;函数设置合适的环境配置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，这里就很自然的引出了一个问题：&lt;strong&gt;如何从&lt;/strong&gt;&lt;code&gt;M&lt;/code&gt;&lt;strong&gt;态降入&lt;/strong&gt;&lt;code&gt;S&lt;/code&gt;**态呢？**现在，我们来了解一些关于&lt;code&gt;risc-v privilege&lt;/code&gt;的知识。&lt;/p&gt;
&lt;h3&gt;mstatus csr(Machine Status Register)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mstatus&lt;/code&gt;寄存器是一个&lt;code&gt;MXLEN&lt;/code&gt;位比特的可读写的寄存器，&lt;strong&gt;其跟踪并控制了&lt;/strong&gt;&lt;code&gt;hart&lt;/code&gt;&lt;strong&gt;的当前运行状态&lt;/strong&gt;。也就是说，我们可以通过修改&lt;code&gt;mstatus&lt;/code&gt;的一些特定位，来使得&lt;code&gt;hart&lt;/code&gt;在当前的运行状态发生变化。下面给出了&lt;code&gt;rv32 mstatus&lt;/code&gt;和&lt;code&gt;rv64 mstatus&lt;/code&gt;的格式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161531613.png&quot; alt=&quot;risc-v32 mstatus&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161533069.png&quot; alt=&quot;risc-v64 mstatus&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这里，我&lt;strong&gt;并不打算完整的将&lt;/strong&gt;&lt;code&gt;mstatus&lt;/code&gt;&lt;strong&gt;的所有标志位介绍完，只会介绍本节中我们将涉及到的标志位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;全局中断使能标志位：&lt;code&gt;MIE&lt;/code&gt;和&lt;code&gt;SIE&lt;/code&gt;，分别用于&lt;code&gt;M-mode&lt;/code&gt;和&lt;code&gt;S-mode&lt;/code&gt;。这些位主要被用于确保当前特权级模式下的中断处理程序的原子性，也就是说：&lt;strong&gt;设置&lt;/strong&gt;&lt;code&gt;xIE&lt;/code&gt;&lt;strong&gt;以处理&lt;/strong&gt;&lt;code&gt;M&lt;/code&gt;&lt;strong&gt;态或&lt;/strong&gt;&lt;code&gt;S&lt;/code&gt;&lt;strong&gt;态中断。并且，允许被一个单独的&lt;/strong&gt;&lt;code&gt;csr&lt;/code&gt;&lt;strong&gt;指令所设置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当一个&lt;code&gt;hart&lt;/code&gt;在$x$特权级下执行时，当$xIE = 1$时，全局启用中断；当$xIE = 0$时，全局关闭中断。对于低特权级的中断而言，如果$x \gt w$，那么不论低特权级设置了何种$wIE$，总是全局中断的；对于高特权级的中断而言，如果$y \ge x$，那么不论如何，总是全局开启的。如何理解呢：&lt;strong&gt;如果从一个高特权级的视角看待低特权级，那么低特权级的中断设置对于高特权级没有任何影响；如果从低特权级的视角看待高特权级，那么高特权级总是启用的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;也就是说，实际上的&lt;code&gt;riscv&lt;/code&gt;特权级有着以下规定：&lt;strong&gt;高特权级总是能够打断低特权级的&lt;/strong&gt;。同时，这里也给出了一个优势，&lt;strong&gt;高特权级的代码能够使用单独的每个中断使能来禁用选定的高特权级中断，然后再将控制权转交给低特权级&lt;/strong&gt;。而回忆一下我们的目标，我们需要从&lt;code&gt;M&lt;/code&gt;态转变到&lt;code&gt;S&lt;/code&gt;态，而内核态是常驻于&lt;code&gt;S&lt;/code&gt;态的，因此，&lt;strong&gt;我们并不希望&lt;/strong&gt;&lt;code&gt;M&lt;/code&gt;&lt;strong&gt;态的中断被经常触发从而导致内核的任务被强行打断，使得效率变低&lt;/strong&gt;，因此，我们需要对中断位进行设置(在后续介绍)。&lt;/p&gt;
&lt;p&gt;为了支持嵌套&lt;code&gt;trap&lt;/code&gt;，每一个能够响应中断的特权级模式$x$都有一个包含中断使能位和特权级模式的两级栈。$xPIE$&lt;strong&gt;保存了&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;发生之前激活的中断使能位的值，而$xPP$保存了上一个特权级模式&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Two-level Stack&lt;br /&gt;
两级栈指的是有两个层次的栈结构，用于存储中断使能位和特权模式。第一级存储当前的中断使能位和特权模式，第二级存储在处理中断时被保存的中断使能位和特权模式。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$xPP$字段最多只能包含$x$个特权级模式，因此，&lt;code&gt;MPP&lt;/code&gt;有两个比特的位宽，而&lt;code&gt;SPP&lt;/code&gt;只有一位。当&lt;code&gt;trap&lt;/code&gt;从特权级模式$y$进入到特权级$x$时，$xPIE$被设置为$xIE$的值，而$xIE$将会被置零，与此同时，$xPP = y$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于低特权级而言，任何&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;(&lt;strong&gt;同步或异步的&lt;/strong&gt;)&lt;strong&gt;通常在进入时以&lt;em&gt;中断禁用的状态&lt;/em&gt;进入更高特权级模式。高特权级的&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;处理程序将处理该&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;并使用堆栈信息返回，或者，如果不立即返回到中断的上下文，将在重新启动中断之前保存特权级堆栈，因此每个堆栈只需要一个入口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;到现在我们就还剩一个问题，高特权级是如何返回到低特权级的呢？&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MRET&lt;/code&gt;或&lt;code&gt;SRET&lt;/code&gt;指令解决了这一个疑问，&lt;code&gt;MRET&lt;/code&gt;或&lt;code&gt;SRET&lt;/code&gt;分别被用于从一个处于&lt;code&gt;M&lt;/code&gt;态或&lt;code&gt;S&lt;/code&gt;态的&lt;code&gt;trap&lt;/code&gt;中返回。假设$xPP = y，当执行一条$xRET$指令时，那么$xIE = xPIE$，特权级模式被设置为$y$，$xPIE = 1$，$xPP$将被设置为最低特权级模式(&lt;code&gt;U mode&lt;/code&gt;, 如果&lt;code&gt;U&lt;/code&gt;未被实现，则设置为&lt;code&gt;M&lt;/code&gt;)。并且，如果$y \ne M \rightarrow MPRV = 0$。&lt;/p&gt;
&lt;p&gt;这里简单的解释一下上面这句话的含义：当&lt;code&gt;xRET&lt;/code&gt;指令执行时，保存着上一特权级信息的&lt;code&gt;xPIE&lt;/code&gt;和&lt;code&gt;xPP&lt;/code&gt;就会起效，&lt;code&gt;xPIE&lt;/code&gt;在赋值完成后，将被设置为&lt;code&gt;1&lt;/code&gt;，这是因为&lt;strong&gt;高特权级的中断总是启用的&lt;/strong&gt;。而&lt;code&gt;MPRV&lt;/code&gt;表示内存访问的特权模式。如果返回的特权模式&lt;code&gt;y&lt;/code&gt;不是&lt;code&gt;M&lt;/code&gt;，则将&lt;code&gt;MPRV&lt;/code&gt;置为&lt;code&gt;0&lt;/code&gt;。这意味着内存访问将不再使用&lt;code&gt;M mode&lt;/code&gt;，而是使用当前的特权模式。&lt;/p&gt;
&lt;p&gt;比较难以理解的是，为什么&lt;code&gt;xPP&lt;/code&gt;会被设置为最低特权级模式。设置&lt;code&gt;xPP&lt;/code&gt;为最低特权级模式&lt;strong&gt;有助于识别在两级特权级栈管理中的软件错误&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;xPP&lt;/code&gt;字段是&lt;code&gt;WARL&lt;/code&gt;类型字段，&lt;strong&gt;其只能包含&lt;/strong&gt;$x$&lt;strong&gt;特权级模式或任何实现的低于&lt;/strong&gt;$x$&lt;strong&gt;的特权级模式&lt;/strong&gt;。&lt;strong&gt;如果特权级模式&lt;/strong&gt;$x$&lt;strong&gt;未被实现&lt;/strong&gt;，&lt;strong&gt;那么$xPP$字段必须被设置为只读的&lt;/strong&gt;$0$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;WARL(Write Any Values, Reads Legal Values)&lt;br /&gt;
某些读/写&lt;code&gt;CSR&lt;/code&gt;字段仅在特定的比特编码子集中定义，但允许写入任何值，同时保证在读取时返回一个合法值。假设写入&lt;code&gt;CSR&lt;/code&gt;没有其他副作用，可以通过尝试写入一个期望的设置，然后读取以查看该值是否被保留，从而确定支持的值范围。这些字段在寄存器描述中标记为&lt;code&gt;WARL(Write-Any Read-Legal)&lt;/code&gt;。&lt;br /&gt;
实现不会因为向&lt;code&gt;WARL&lt;/code&gt;字段写入不支持的值而引发异常。当读取一个&lt;code&gt;WARL&lt;/code&gt;字段时，如果上一次写入的是一个非法值，实现可以返回任何合法值，但返回的合法值应当与写入的非法值以及&lt;code&gt;hart&lt;/code&gt;的架构状态有确定性的关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;mepc csr(Machine Exception Program Counter)&lt;/h3&gt;
&lt;p&gt;在这里，我并不会详细介绍&lt;code&gt;mepc&lt;/code&gt;的所有用法，只会提及一点：&lt;strong&gt;当&lt;/strong&gt;&lt;code&gt;start&lt;/code&gt;&lt;strong&gt;函数调用&lt;/strong&gt;&lt;code&gt;mret&lt;/code&gt;&lt;strong&gt;时&lt;/strong&gt;, &lt;code&gt;mepc&lt;/code&gt;&lt;strong&gt;的值应该被设置为&lt;/strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;strong&gt;函数的地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里可以给出一个比较正式的用法：当&lt;code&gt;trap&lt;/code&gt;进入&lt;code&gt;M&lt;/code&gt;态时，&lt;code&gt;mepc&lt;/code&gt;会被写入引发中断或遇到异常的指令的虚拟地址。因此，在&lt;code&gt;mret&lt;/code&gt;调用时，会根据&lt;code&gt;mepc&lt;/code&gt;的值进行跳转。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161703495.png&quot; alt=&quot;mepc&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;satp(Supervisor Address Translation and Protection Register)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;satp&lt;/code&gt;寄存器是一个&lt;code&gt;SXLEN&lt;/code&gt;位宽的读写寄存器，其控制了&lt;code&gt;S&lt;/code&gt;模式下的地址转换和保护。&lt;code&gt;satp&lt;/code&gt;有两种形式，分别为$SXLEN = 31$或$SXLEN = 64$：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161825930.png&quot; alt=&quot;SXLEN = 32&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161826849.png&quot; alt=&quot;SXLEN = 64&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;satp&lt;/code&gt;寄存器包含一个根页表的物理页码(&lt;code&gt;PPN, Physical Page Number&lt;/code&gt;)，该物理页码是由&lt;code&gt;S mode&lt;/code&gt;的物理地址除以$4 KiB$而来；一个地址空间标识符(&lt;code&gt;ASID, Address Space Identify&lt;/code&gt;)，该标识符有助于在每个地址空间执行地址转换屏障(&lt;code&gt;fence&lt;/code&gt;)；以及一个模式(&lt;code&gt;MODE&lt;/code&gt;)字段，其决定了当前地址的转换方法。&lt;/p&gt;
&lt;p&gt;其中，对于&lt;code&gt;MODE&lt;/code&gt;字段我们需要详细介绍一下。当$MODE = Bare$时，&lt;code&gt;S&lt;/code&gt;特权级的虚拟地址直接等价于物理地址，除了物理内存保护方案外没有其他额外的内存保护。为了选择$MODE = Bare$，软件&lt;strong&gt;必须&lt;/strong&gt;将&lt;code&gt;satp&lt;/code&gt;的&lt;strong&gt;剩余字段置零&lt;/strong&gt;。&lt;strong&gt;试图在$MODE = Bare$的情况下使用一个非零的&lt;/strong&gt;&lt;code&gt;satp&lt;/code&gt;&lt;strong&gt;将对未置零的字段的值会产生一个&lt;/strong&gt;&lt;code&gt;UNSPECIFIED(未指明的)&lt;/code&gt;&lt;strong&gt;影响，并对地址转换和保护行为产生一个&lt;/strong&gt;&lt;code&gt;UNSPECIFIED&lt;/code&gt;&lt;strong&gt;影响&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于$SXLEN = 32$时，&lt;code&gt;MODE&lt;/code&gt;的唯一一个有效设置为&lt;code&gt;Sv32&lt;/code&gt;，一种虚拟内存分页策略。当$SXLEN = 64$时，一共有三种虚拟内存分页策略可供选择：&lt;code&gt;Sv39&lt;/code&gt;、&lt;code&gt;Sv48&lt;/code&gt;和&lt;code&gt;Sv57&lt;/code&gt;。其余的&lt;code&gt;MODE&lt;/code&gt;设置保留以供将来使用，并且可以定义&lt;code&gt;satp&lt;/code&gt;中其他字段的不同解释。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161850259.png&quot; alt=&quot;satp mode&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;satp&lt;/code&gt;&lt;strong&gt;寄存器在&lt;/strong&gt;&lt;code&gt;S mode&lt;/code&gt;和&lt;code&gt;U mode&lt;/code&gt;&lt;strong&gt;下才被认为是激活状态&lt;/strong&gt;。地址转换算法只可能在&lt;code&gt;satp&lt;/code&gt;被激活时使用一个给定的&lt;code&gt;satp&lt;/code&gt;值开始执行。&lt;/p&gt;
&lt;h3&gt;medeleg &amp;amp; mideleg csr(Machine Trap Delegation Registers)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;在默认情况下，任何特权级的所有&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;都是在&lt;/strong&gt;&lt;code&gt;M mode&lt;/code&gt;&lt;strong&gt;被处理的，不过&lt;/strong&gt;&lt;code&gt;M mode&lt;/code&gt;&lt;strong&gt;的处理程序可以使用&lt;/strong&gt;&lt;code&gt;MRET&lt;/code&gt;&lt;strong&gt;指令将&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;重定向到合适的特权级&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了提高性能，具体实现可以在&lt;code&gt;medeleg&lt;/code&gt;和&lt;code&gt;mideleg&lt;/code&gt;中通过提供单独的读写位来表明一些特定的异常和中断应该被低特权级直接处理。&lt;code&gt;medeleg(machine exception delegation)&lt;/code&gt;是一个$64$位宽的读写寄存器；而&lt;code&gt;mideleg(machine interrupt delegation)&lt;/code&gt;是一个&lt;code&gt;MXLEN&lt;/code&gt;位宽的读写寄存器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161903945.png&quot; alt=&quot;medeleg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161904480.png&quot; alt=&quot;mideleg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;S mode&lt;/code&gt;下的&lt;code&gt;hart&lt;/code&gt;中，&lt;code&gt;medeleg&lt;/code&gt;和&lt;code&gt;mideleg&lt;/code&gt;&lt;strong&gt;必须存在，并且在&lt;/strong&gt;&lt;code&gt;S mode&lt;/code&gt;或&lt;code&gt;U mode&lt;/code&gt;&lt;strong&gt;下发生对应的&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;时，在&lt;/strong&gt;&lt;code&gt;medeleg&lt;/code&gt;或&lt;code&gt;mideleg&lt;/code&gt;&lt;strong&gt;中设置一位把该&lt;/strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;委托给&lt;/strong&gt;&lt;code&gt;S mode&lt;/code&gt;&lt;strong&gt;下的&lt;/strong&gt;&lt;code&gt;trap handler&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当一个&lt;code&gt;trap&lt;/code&gt;被委托给&lt;code&gt;S mode&lt;/code&gt;时，会执行以下操作:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scause&lt;/code&gt;寄存器写入&lt;code&gt;trap&lt;/code&gt;的原因&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sepc&lt;/code&gt;寄存器写入引发&lt;code&gt;trap&lt;/code&gt;的指令的虚拟地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stval&lt;/code&gt;寄存器写入特定于异常的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mstatus&lt;/code&gt;寄存器中的&lt;code&gt;SPP&lt;/code&gt;字段写入发生&lt;code&gt;trap&lt;/code&gt;时激活的特权级模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mstatus&lt;/code&gt;寄存器中的&lt;code&gt;SPIE&lt;/code&gt;字段写入发生&lt;code&gt;trap&lt;/code&gt;时的&lt;code&gt;SIE&lt;/code&gt;字段的值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mstatus.SIE&lt;/code&gt;字段置零&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mcause&lt;/code&gt;、&lt;code&gt;mepc&lt;/code&gt;、&lt;code&gt;mtval&lt;/code&gt;以及&lt;code&gt;mstatus.MPP&lt;/code&gt;和&lt;code&gt;mstatus.MPIE&lt;/code&gt;字段不会被写入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得注意的是：&lt;code&gt;trap&lt;/code&gt;&lt;strong&gt;永远不会从更高特权级转换到更低特权级，这一情况不会发生&lt;/strong&gt;。只可能在水平上进行发生，也就是：如果&lt;code&gt;M mode&lt;/code&gt;委托了一个&lt;code&gt;trap&lt;/code&gt;到&lt;code&gt;S mode&lt;/code&gt;，那么引发异常的&lt;code&gt;trap&lt;/code&gt;能够在&lt;code&gt;S mode&lt;/code&gt;下进行处理。&lt;/p&gt;
&lt;h3&gt;Supervisor Interrupt Registers(sip and sie)&lt;/h3&gt;
&lt;p&gt;在本节中，并不会对&lt;code&gt;sip&lt;/code&gt;寄存器做出介绍，因为这里的重点是&lt;code&gt;sie&lt;/code&gt;寄存器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sie&lt;/code&gt;寄存器是一个$SXLEN$位宽的读写寄存器，其包含了中断使能位。中断原因号与&lt;code&gt;sie&lt;/code&gt;的位号相对应。比特&lt;code&gt;15:0&lt;/code&gt;只分配给标准中断原因，而比特&lt;code&gt;16&lt;/code&gt;及以上被指定为平台使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161929965.png&quot; alt=&quot;sie&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161929250.png&quot; alt=&quot;scause values&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161931972.png&quot; alt=&quot;sie portion&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sie.SSIE&lt;/code&gt;是用于&lt;code&gt;S mode&lt;/code&gt;外部中断的中断使能位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sie.STIP&lt;/code&gt;是用于&lt;code&gt;S mode&lt;/code&gt;时钟中断的中断使能位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sie.SEIE&lt;/code&gt;是用于&lt;code&gt;S mode&lt;/code&gt;软件中断的中断使能位&lt;/li&gt;
&lt;li&gt;如果实现了&lt;code&gt;Sscofpmf&lt;/code&gt;扩展，&lt;code&gt;sie.LCOFIE&lt;/code&gt;是本地计数器溢出中断的中断使能位。如果未实现该扩展，&lt;code&gt;sie.LCOFIE&lt;/code&gt;则是只读的&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Physical Memory Protection CSRs&lt;/h3&gt;
&lt;p&gt;在这里只介绍两个寄存器：&lt;code&gt;pmpcfg0&lt;/code&gt;和&lt;code&gt;pmpaddr0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PMP(物理内存保护)&lt;/code&gt;条目由一个&lt;code&gt;8&lt;/code&gt;位的配置寄存器和一个&lt;code&gt;MXLEN&lt;/code&gt;位的地址寄存器描述。一些&lt;code&gt;PMP&lt;/code&gt;设置还会使用与前一个&lt;code&gt;PMP&lt;/code&gt;条目相关联的地址寄存器。最多支持&lt;code&gt;64&lt;/code&gt;个&lt;code&gt;PMP&lt;/code&gt;条目。实现中可以实现&lt;code&gt;0&lt;/code&gt;个、&lt;code&gt;16&lt;/code&gt;个或&lt;code&gt;64&lt;/code&gt;个&lt;code&gt;PMP&lt;/code&gt;条目；&lt;strong&gt;必须首先实现编号最低的&lt;/strong&gt;&lt;code&gt;PMP&lt;/code&gt;&lt;strong&gt;条目&lt;/strong&gt;。所有&lt;code&gt;PMP CSR&lt;/code&gt;字段都是&lt;code&gt;WARL&lt;/code&gt;，并且可能是只读零。&lt;code&gt;PMP CSR&lt;/code&gt;&lt;strong&gt;仅在&lt;/strong&gt;&lt;code&gt;M mode&lt;/code&gt;&lt;strong&gt;下可访问&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PMP configuration&lt;/code&gt;寄存器被密集地打包到&lt;code&gt;CSRs&lt;/code&gt;中，&lt;strong&gt;以最小化上下文切换时间&lt;/strong&gt;。对于&lt;code&gt;RV32&lt;/code&gt;，有十六个&lt;code&gt;CSRs(pmpcfg0–pmpcfg15)&lt;/code&gt;，用于保存&lt;code&gt;64&lt;/code&gt;个&lt;code&gt;PMP&lt;/code&gt;条目的配置&lt;code&gt;(pmp0cfg–pmp63cfg)&lt;/code&gt;。对于&lt;code&gt;RV64&lt;/code&gt;，有八个偶数编号的&lt;code&gt;CSRs(pmpcfg0、pmpcfg2、……、pmpcfg14)&lt;/code&gt;，用于保存&lt;code&gt;64&lt;/code&gt;个&lt;code&gt;PMP&lt;/code&gt;条目的配置。对于&lt;code&gt;RV64&lt;/code&gt;，&lt;strong&gt;奇数编号的配置寄存器&lt;/strong&gt;&lt;code&gt;(pmpcfg1、pmpcfg3、……、pmpcfg15)&lt;/code&gt;&lt;strong&gt;是非法的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161949274.png&quot; alt=&quot;rv32 pmp configuration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161950438.png&quot; alt=&quot;rv64 pmp configuration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PMP address&lt;/code&gt;寄存器是命名为&lt;code&gt;pmpaddr0-pmpaddr63&lt;/code&gt;的&lt;code&gt;CSRs&lt;/code&gt;。每个&lt;code&gt;PMP address&lt;/code&gt;寄存器在&lt;code&gt;RV32&lt;/code&gt;中编码一个&lt;code&gt;34&lt;/code&gt;位物理地址的第&lt;code&gt;33&lt;/code&gt;到第&lt;code&gt;2&lt;/code&gt;位。对于&lt;code&gt;RV64&lt;/code&gt;，每个&lt;code&gt;PMP address&lt;/code&gt;寄存器编码一个&lt;code&gt;56&lt;/code&gt;位物理地址的第&lt;code&gt;55&lt;/code&gt;到第&lt;code&gt;2&lt;/code&gt;位。&lt;strong&gt;并非所有物理地址位都需要实现&lt;/strong&gt;，因此&lt;code&gt;pmpaddr&lt;/code&gt;寄存器是&lt;code&gt;WARL&lt;/code&gt;的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161954317.png&quot; alt=&quot;rv32 pmp address&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161954318.png&quot; alt=&quot;rv64 pmp address&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图显示了&lt;code&gt;PMP configuration&lt;/code&gt;寄存器的布局。&lt;code&gt;R&lt;/code&gt;、&lt;code&gt;W&lt;/code&gt;和&lt;code&gt;X&lt;/code&gt;位分别表示&lt;code&gt;PMP&lt;/code&gt;条目允许&lt;strong&gt;读&lt;/strong&gt;、&lt;strong&gt;写&lt;/strong&gt;和&lt;strong&gt;指令执行&lt;/strong&gt;。&lt;strong&gt;当其中某一位被清除时，相应的访问类型被拒绝&lt;/strong&gt;。&lt;code&gt;R&lt;/code&gt;、&lt;code&gt;W&lt;/code&gt;和&lt;code&gt;X&lt;/code&gt;字段组成一个集体的&lt;code&gt;WARL&lt;/code&gt;字段，其中$R = 0$和$W = 1$的组合是保留的。其余两个字段&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;L&lt;/code&gt;在后续章节中描述。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161957353.png&quot; alt=&quot;pmp configuration layout&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Start Code&lt;/h3&gt;
&lt;p&gt;至此，关于理解&lt;code&gt;start&lt;/code&gt;函数逻辑的前置知识就介绍完毕。现在就让我们来分析&lt;code&gt;start&lt;/code&gt;函数中的具体内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define MSTATUS_MPP_MASK (3L &amp;lt;&amp;lt; 11)
#define MSTATUS_MPP_S (1L &amp;lt;&amp;lt; 11)
r_mstatus =&amp;gt; asm volatile(&quot;csrr %0, mstatus&quot; : &quot;=r&quot;(x));
r_mstatus =&amp;gt; asm volatile(&quot;csrw mstatus, %0&quot; : : &quot;r&quot;(x));

unsigned long x = r_mstatus();
x &amp;amp;= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是从入口点&lt;code&gt;_entry&lt;/code&gt;到&lt;code&gt;start&lt;/code&gt;函数的第一个操作，我们在之前已经知道：&lt;code&gt;start&lt;/code&gt;函数最终目的是要转跳到&lt;code&gt;main&lt;/code&gt;函数中，并在此之前进行一系列配置，然后进入内核态。那么，&lt;code&gt;xv6-riscv&lt;/code&gt;的内核态是运行在&lt;code&gt;S mode&lt;/code&gt;的，而从&lt;code&gt;_entry&lt;/code&gt;进入到&lt;code&gt;start&lt;/code&gt;的此时，是处于&lt;code&gt;M mode&lt;/code&gt;的。因此，我们需要设置进入&lt;code&gt;main&lt;/code&gt;函数后的特权级。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;r_mstatus&lt;/code&gt;便是读取当前&lt;code&gt;hart&lt;/code&gt;的运行状态，而&lt;code&gt;x &amp;amp;= ~MSTATUS_MPP_MASK&lt;/code&gt;对&lt;code&gt;mstatus.MPP&lt;/code&gt;进行置零操作，然后通过&lt;code&gt;x |= MSTATUS_MPP_S&lt;/code&gt;设置&lt;code&gt;mstatus.MPP = S&lt;/code&gt;。这样，我们就能够在&lt;code&gt;mret&lt;/code&gt;指令后，将特权级设置为&lt;code&gt;S&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是，我们发现：&lt;strong&gt;我们现在只完成了设置特权级，但是并未设置到底要转跳到哪一个位置&lt;/strong&gt;。因此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;w_mepc((uint64)main);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面我们已经了解了，&lt;code&gt;mret&lt;/code&gt;的返回地址是根据&lt;code&gt;mepc&lt;/code&gt;寄存器而决定的，因此我们将&lt;code&gt;mepc&lt;/code&gt;的值设置为&lt;code&gt;main&lt;/code&gt;函数的地址，就能够在&lt;code&gt;mret&lt;/code&gt;执行的时候转跳到对应的&lt;code&gt;main&lt;/code&gt;函数从而进入内核态。&lt;/p&gt;
&lt;p&gt;这样，我们就完成了一个简易的内核态的入口，但是，对于一个&lt;code&gt;xcv6-riscv&lt;/code&gt;来说，还需要进行一些具体的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// disable paging for now.
w_satp(0);

// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);

// ask for clock interrupts.
timerinit();

// keep each CPU&apos;s hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们来逐行分析这些代码的含义：&lt;/p&gt;
&lt;p&gt;我们知道&lt;code&gt;satp&lt;/code&gt;寄存器用于管理页表基地址、地址空间标识符以及启动或禁用分页机制。简单思考一下，&lt;strong&gt;操作系统初始化早期，我们并不需要启动太多的事项，而且由于有一些需要直接访问物理内存进行配置，如果启动分页机制，就需要进行页表转换，那么我们在初始化阶段就需要浪费一定的资源来额外的进行初始化操作&lt;/strong&gt;。我们在这里设置$satp = 0$是为了简化内存管理，并且不需要过早的进行分页管理，等到进入内核态后，再启用分页机制支持虚拟内存。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;medeleg&lt;/code&gt;和&lt;code&gt;mideleg&lt;/code&gt;是为了提高效率，因为频繁的通过&lt;code&gt;M mode&lt;/code&gt;来处理&lt;code&gt;trap&lt;/code&gt;会极大的影响内核的运行。因此，在此处，我们直接将所有的异常和中断都委托给了&lt;code&gt;S mode&lt;/code&gt;来处理，也就是说，内核态对&lt;code&gt;trap&lt;/code&gt;具有全部的处理能力，这也符合一般认知，内核会处理用户的异常和中断，而不是交由&lt;code&gt;M mode&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在我们已经将&lt;code&gt;trap&lt;/code&gt;的处理权交付给了&lt;code&gt;S mode&lt;/code&gt;，但是我们需要显示开启&lt;code&gt;S&lt;/code&gt;特权级下的中断使能，因此&lt;code&gt;w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE)&lt;/code&gt;便是&lt;strong&gt;允许内核态对外部中断、时钟中断和软件中断的响应&lt;/strong&gt;。这也对应了上面&lt;code&gt;M mode&lt;/code&gt;将&lt;code&gt;trap&lt;/code&gt;的处理全权委托给了内核态。&lt;/p&gt;
&lt;p&gt;当我们有了处理权限后，我们就可以考虑内存访问了，在&lt;a href=&quot;https://chenmiaoi.github.io/2024/05/15/xv6-The-boot-loader/&quot;&gt;上一节&lt;/a&gt;中，我们了解到&lt;code&gt;xv6-riscv&lt;/code&gt;的内存布局，而其物理地址是$56$位宽的，因此&lt;code&gt;w_pmpaddr0(0x3fffffffffffffull)&lt;/code&gt;将&lt;code&gt;PMP&lt;/code&gt;的第一个条目的地址寄存器设置为&lt;code&gt;0x3fffffffffffff&lt;/code&gt;，刚好覆盖了整个物理地址空间，因此该条目可以匹配任何物理地址。而&lt;code&gt;w_pmpcfg0(0xf)&lt;/code&gt;则是对该条目进行具体配置，允许了&lt;code&gt;S mode&lt;/code&gt;&lt;strong&gt;对整个物理地址具有读、写和执行访问权限&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;timerinit()&lt;/code&gt;则是设置了时钟中断源，使得&lt;code&gt;xv6-riscv&lt;/code&gt;能够获得时间片。&lt;/p&gt;
&lt;p&gt;而最后一点，便是获取当前&lt;code&gt;hart&lt;/code&gt;的&lt;code&gt;ID&lt;/code&gt;，写入&lt;code&gt;tp&lt;/code&gt;寄存器。&lt;code&gt;tp&lt;/code&gt;寄存器用于存储线程指针，即当前执行流&lt;code&gt;ID&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Timer&lt;/h2&gt;
&lt;h3&gt;CLINT&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;risc-v&lt;/code&gt;中，&lt;code&gt;CLINT&lt;/code&gt;的定义是由平台具体实现的，而&lt;code&gt;qemu virt&lt;/code&gt;参考了&lt;code&gt;SIFIVE&lt;/code&gt;的&lt;code&gt;CLINT&lt;/code&gt;设计。因此，在这里我参考了&lt;code&gt;SiFive FE310-G000&lt;/code&gt;型号的开发板进行分析。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;xv6-riscv&lt;/code&gt;的&lt;code&gt;CLINT&lt;/code&gt;是根据&lt;a href=&quot;https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c&quot;&gt;qemu virt&lt;/a&gt;中的设置而来，因此可以看见&lt;code&gt;CLINT&lt;/code&gt;的基址位于&lt;code&gt;0x2000&apos;0000&lt;/code&gt;处。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static const MemMapEntry virt_memmap[] = {
    [VIRT_CLINT] =        {  0x2000000,       0x10000 },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;了解到&lt;code&gt;CLINT&lt;/code&gt;在实际物理地址中的基址后，我们就需要学习关于&lt;code&gt;CLINT&lt;/code&gt;的一些基本概念:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CLINT(Core Local Interruptor)&lt;/code&gt;是一个处理器内部模块，&lt;strong&gt;负责处理和管理核本地的中断和定时器功能&lt;/strong&gt;。&lt;code&gt;CLINT&lt;/code&gt;的主要功能包括:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;本地中断管理(Local Interrupt Management)&lt;/code&gt;：&lt;code&gt;CLINT&lt;/code&gt;处理核本地的中断请求，这些中断请求通常不需要通过全局中断控制器(如&lt;code&gt;PLIC，Platform-Level Interrupt Controller&lt;/code&gt;)进行处理。&lt;code&gt;CLINT&lt;/code&gt;&lt;strong&gt;管理的中断通常是核内的特殊事件&lt;/strong&gt;，例如软件中断和定时器中断。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;定时器功能(Timer Functionality)&lt;/code&gt;：&lt;code&gt;CLINT&lt;/code&gt;&lt;strong&gt;提供核本地的定时器功能，用于生成周期性中断&lt;/strong&gt;。每个处理器核都有一个独立的定时器，通过编程可以设置定时器的触发时间。&lt;strong&gt;当定时器到达设定时间时，会触发一个中断，处理器核可以用这个中断来执行周期性任务或进行时间管理&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在&lt;code&gt;risc-v&lt;/code&gt;中，操作&lt;code&gt;CLINT&lt;/code&gt;是有着专属寄存器的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;msip(Machine-mode Software Interrupt Pending Register)&lt;/code&gt;：用于管理软件中断。每个核都有各自的&lt;code&gt;msip&lt;/code&gt;寄存器。写入这个寄存器会触发相应核的机器模式软件中断(&lt;code&gt;Machine Software Interrupt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;`mtime(Machine Timer Register)：这是一个64位的计时器寄存器，&lt;strong&gt;用于跟踪时间&lt;/strong&gt;。&lt;strong&gt;它通常由一个全局的、统一递增的计时器硬件单元提供时间戳&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mtimecmp(Machine Timer Compare Register)&lt;/code&gt;：每个处理器核都有一个独立的$64$位&lt;code&gt;mtimecmp&lt;/code&gt;寄存器。处理器核会不断地比较&lt;code&gt;mtime&lt;/code&gt;和&lt;code&gt;mtimecmp&lt;/code&gt;的值，当&lt;code&gt;mtime&lt;/code&gt;达到或超过&lt;code&gt;mtimecmp&lt;/code&gt;的值时，会触发机器模式定时器中断(&lt;code&gt;Machine Timer Interrupt&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于&lt;code&gt;CLINT&lt;/code&gt;的专属寄存器，&lt;code&gt;risc-v&lt;/code&gt;手册中并未给出详细定义地址，因此，我们参考&lt;code&gt;SiFive FE310-G000&lt;/code&gt;能够得到其在物理地址中的映射地址：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405171437088.png&quot; alt=&quot;CLINT register remap&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// core local interruptor (CLINT), which contains the timer.
#define CLINT 0x2000000L
#define CLINT_MTIMECMP(hartid) (CLINT + 0x4000 + 8 * (hartid))
#define CLINT_MTIME (CLINT + 0xBFF8) // cycles since boot.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLINT&lt;/code&gt;：&lt;code&gt;CLINT&lt;/code&gt;的基址，即&lt;code&gt;CLINT&lt;/code&gt;寄存器的起始地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLINT_MTIMECMP(hartid)&lt;/code&gt;：用于计算给定&lt;code&gt;hartid&lt;/code&gt;的&lt;code&gt;MTIMECMP&lt;/code&gt;寄存器的地址。在&lt;code&gt;CLINT&lt;/code&gt;中，每个&lt;code&gt;hart&lt;/code&gt;都有一个&lt;code&gt;MTIMECMP&lt;/code&gt;寄存器，用于设置定时器中断触发的时间比较值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLINT_MTIME&lt;/code&gt;：用于访问&lt;code&gt;MTIME&lt;/code&gt;寄存器的地址。&lt;code&gt;MTIME&lt;/code&gt;寄存器用于跟踪自启动以来的时钟周期数，它通常用于实现定时器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在正式介绍&lt;code&gt;Timer&lt;/code&gt;的代码前，我们需要对一些寄存器做出了解。&lt;/p&gt;
&lt;h3&gt;mscratch csr(Machine Scratch Register)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mscratch&lt;/code&gt;寄存器是一个$MXLEN$位宽的读写寄存器，&lt;strong&gt;其只能被&lt;/strong&gt;&lt;code&gt;M mode&lt;/code&gt;&lt;strong&gt;所使用&lt;/strong&gt;。&lt;strong&gt;通常，它用于保存指向&lt;/strong&gt;&lt;code&gt;M mode&lt;/code&gt;的&lt;code&gt;hart-local&lt;/code&gt;&lt;strong&gt;上下文空间的指针，并在进入&lt;/strong&gt;&lt;code&gt;M mode trap handler&lt;/code&gt;&lt;strong&gt;时与用户寄存器交换&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当处理器进入机器模式处理中断或异常时，通常会使用&lt;code&gt;mscratch&lt;/code&gt;寄存器保存上下文信息，例如保存当前的寄存器状态、程序计数器等。这样可以在处理完中断或异常后恢复处理器状态。&lt;/p&gt;
&lt;h3&gt;mtvec csr(Machine Trap-Vector Base-Address Register)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mtvec&lt;/code&gt;寄存器是一个$MXLEN$位宽的读写寄存器，&lt;strong&gt;其保存了由一个向量基址&lt;/strong&gt;(&lt;code&gt;Vector Base Address&lt;/code&gt;)&lt;strong&gt;和向量模式&lt;/strong&gt;(&lt;code&gt;Vector Mode&lt;/code&gt;)&lt;strong&gt;组成的&lt;/strong&gt;&lt;code&gt;trap vector configuration&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405171455924.png&quot; alt=&quot;mtvec&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mtvec&lt;/code&gt;总是被实现的，至少会包含一个可读的值；如果&lt;code&gt;mtvec&lt;/code&gt;以可写的方式实现，那么&lt;code&gt;mtvec&lt;/code&gt;所保存的值的集合根据实现的不同而不同。&lt;code&gt;mtvec.BASE&lt;/code&gt;的值必须始终是四字节对齐的，而&lt;code&gt;mtvec.MODE&lt;/code&gt;设置的值可能会对&lt;code&gt;mtvec.BASE&lt;/code&gt;施加额外的对齐操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405171502892.png&quot; alt=&quot;Encoding of mtvec MODE field&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mtvec.MODE&lt;/code&gt;的编码如上所示。当$mtvec.MODE = Direct$时，所有进入到&lt;code&gt;M&lt;/code&gt;态的&lt;code&gt;trap&lt;/code&gt;都会导致&lt;code&gt;pc&lt;/code&gt;被设置为&lt;code&gt;mtvec.BASE&lt;/code&gt;字段中的值；当&lt;code&gt;mtvec.MODE = Vectored&lt;/code&gt;时，所有进入到&lt;code&gt;M&lt;/code&gt;态的&lt;strong&gt;同步异常&lt;/strong&gt;都会导致&lt;code&gt;pc&lt;/code&gt;被设置为&lt;code&gt;mtvec.BASE&lt;/code&gt;字段中的值，而所有进入到&lt;code&gt;M&lt;/code&gt;态的&lt;strong&gt;异步中断&lt;/strong&gt;都会导致&lt;code&gt;pc&lt;/code&gt;被设置为$mtvec.BASE + cause \times 4$。&lt;/p&gt;
&lt;h3&gt;Supervisor Interrupt Registers(sip and sie)&lt;/h3&gt;
&lt;p&gt;我们在介绍&lt;code&gt;start&lt;/code&gt;函数时，曾介绍过&lt;code&gt;sie&lt;/code&gt;寄存器。现在，我们对&lt;code&gt;sip&lt;/code&gt;寄存器做出介绍。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sip&lt;/code&gt;寄存器是一个$SXLEN$位宽的读写寄存器，&lt;strong&gt;其包含了挂起的中断的信息&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405171514872.png&quot; alt=&quot;sip&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405161929250.png&quot; alt=&quot;scause values&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405171519208.png&quot; alt=&quot;sip portion&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sip.SEIP&lt;/code&gt;用于&lt;code&gt;S&lt;/code&gt;态的外部中断的中断挂起，如果实现，&lt;code&gt;SEIP&lt;/code&gt;在&lt;code&gt;sip&lt;/code&gt;中是&lt;strong&gt;只读&lt;/strong&gt;的，并且由执行环境设置和清除，通常通过特定于平台的中断控件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sip.STIP&lt;/code&gt;用于&lt;code&gt;S&lt;/code&gt;态的定时器中断的中断挂起，如果实现，&lt;code&gt;STIP&lt;/code&gt;在&lt;code&gt;sip&lt;/code&gt;中是&lt;strong&gt;只读&lt;/strong&gt;的，并且由执行环境设置和清除&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sip.SSIP&lt;/code&gt;用于&lt;code&gt;S&lt;/code&gt;态的软件中断的中断挂起，如果实现，&lt;code&gt;SSIP&lt;/code&gt;在&lt;code&gt;sip&lt;/code&gt;中是&lt;strong&gt;可写&lt;/strong&gt;的，并且可能被平台特定的中断控制器置&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Timer Code&lt;/h3&gt;
&lt;h4&gt;Timer Init&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];

void timerinit() {
  // each CPU has a separate source of timer interrupts.
  int id = r_mhartid();

  // ask the CLINT for a timer interrupt.
  int interval = 1000000; // cycles; about 1/10th second in qemu.
  *(uint64 *)CLINT_MTIMECMP(id) = *(uint64 *)CLINT_MTIME + interval;

  // prepare information in scratch[] for timervec.
  // scratch[0..2] : space for timervec to save registers.
  // scratch[3] : address of CLINT MTIMECMP register.
  // scratch[4] : desired interval (in cycles) between timer interrupts.
  uint64 *scratch = &amp;amp;timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);
  scratch[4] = interval;
  w_mscratch((uint64)scratch);

  // set the machine-mode trap handler.
  w_mtvec((uint64)timervec);

  // enable machine-mode interrupts.
  w_mstatus(r_mstatus() | MSTATUS_MIE);

  // enable machine-mode timer interrupts.
  w_mie(r_mie() | MIE_MTIE);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们来逐行分析上述代码：首先，获取当前的&lt;code&gt;hartid&lt;/code&gt;，以便于后续计算&lt;code&gt;CLINT&lt;/code&gt;的相关信息；&lt;/p&gt;
&lt;p&gt;然后&lt;code&gt;*(uint64 *)CLINT_MTIMECMP(id) = *(uint64 *)CLINT_MTIME + interval&lt;/code&gt;: 在&lt;code&gt;CLINT&lt;/code&gt;中设置一个定时器中断。这行代码将&lt;code&gt;CLINT&lt;/code&gt;的&lt;code&gt;MTIMECMP&lt;/code&gt;寄存器(&lt;strong&gt;用于设置定时器中断触发的时间比较值&lt;/strong&gt;)的值设置为当前的&lt;code&gt;MTIME&lt;/code&gt;寄存器值加上一个指定的间隔。在这里，间隔为$100&apos;0000$个&lt;code&gt;CPU&lt;/code&gt;周期，大约相当于&lt;code&gt;qemu&lt;/code&gt;中的$1/10$秒。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uint64 *scratch = &amp;amp;timer_scratch[id][0]&lt;/code&gt;: 创建一个指向&lt;code&gt;timer_scratch&lt;/code&gt;数组的指针，并将其设置为当前&lt;code&gt;hart&lt;/code&gt;对应的上下文的地址。这个数组用于存储一些与定时器中断相关的信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scratch[3] = CLINT_MTIMECMP(id)&lt;/code&gt;: 将&lt;code&gt;CLINT&lt;/code&gt;的&lt;code&gt;MTIMECMP&lt;/code&gt;寄存器的地址存储在&lt;code&gt;scratch&lt;/code&gt;数组的第&lt;code&gt;3&lt;/code&gt;个条目中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scratch[4] = interval&lt;/code&gt;: 将定时器中断触发的时间间隔(以&lt;code&gt;CPU&lt;/code&gt;周期数表示)存储在&lt;code&gt;scratch&lt;/code&gt;数组的第&lt;code&gt;4&lt;/code&gt;个条目中&lt;/p&gt;
&lt;p&gt;&lt;code&gt;w_mscratch((uint64)scratch)&lt;/code&gt;: 将&lt;code&gt;scratch&lt;/code&gt;数组的地址存储在&lt;code&gt;MSRATCH&lt;/code&gt;寄存器中，以便后续的处理，这里实际上就是对定时器中断的信息进行了初始化，保存在了当前&lt;code&gt;hart&lt;/code&gt;的上下文中。&lt;/p&gt;
&lt;p&gt;也就是说，在&lt;code&gt;timer init&lt;/code&gt;中，我们创建了一个针对于每一个&lt;code&gt;hart&lt;/code&gt;单独的定时器的上下文配置，具体如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timer scratch[5] = {
    0,  reserve for parameters 
    8,  reserve for parameters 
    16, reserve for parameters 
    24, address of CLINT MTIMECMP register
    32, desired interval (in cycles) between timer interrupts
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;w_mtvec((uint64)timervec)&lt;/code&gt;: 将&lt;code&gt;M mode trap&lt;/code&gt;向量基址寄存器(&lt;code&gt;MTVEC&lt;/code&gt;)设置为&lt;code&gt;timervec&lt;/code&gt;函数的地址。这意味着当发生&lt;code&gt;M mode&lt;/code&gt;的&lt;code&gt;trap&lt;/code&gt;时，处理器将跳转到&lt;code&gt;timervec&lt;/code&gt;函数中执行相应的处理，而对于定时器中断，我们将其设置为&lt;code&gt;M mode trap handler&lt;/code&gt;，因此只要发生定时器中断，那么就可以跳转到&lt;code&gt;timervec&lt;/code&gt;中进行处理。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;w_mstatus(r_mstatus() | MSTATUS_MIE)&lt;/code&gt;: 使能&lt;code&gt;M mode&lt;/code&gt;中断(&lt;code&gt;MIE&lt;/code&gt;)。这行代码将&lt;code&gt;MSTATUS&lt;/code&gt;寄存器的&lt;code&gt;MIE&lt;/code&gt;位设置为&lt;code&gt;1&lt;/code&gt;，允许&lt;code&gt;M mode&lt;/code&gt;中断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;w_mie(r_mie() | MIE_MTIE)&lt;/code&gt;: 使能机器模式的定时器中断(&lt;code&gt;MTIE&lt;/code&gt;)。这行代码将&lt;code&gt;MIE&lt;/code&gt;寄存器的&lt;code&gt;MTIE&lt;/code&gt;位设置为&lt;code&gt;1&lt;/code&gt;，允许机器模式的定时器中断，这里就与上述代码对应了。&lt;/p&gt;
&lt;h4&gt;Time Interrupt Handler&lt;/h4&gt;
&lt;p&gt;实际上，我认为在这里将&lt;code&gt;Timer Trap Handler&lt;/code&gt;不是太合适，因此此处仅仅列出代码，并不做任何解释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.globl timervec
.align 4
timervec:
        csrrw a0, mscratch, a0
        sd a1, 0(a0)
        sd a2, 8(a0)
        sd a3, 16(a0)

        # schedule the next timer interrupt
        # by adding interval to mtimecmp.
        ld a1, 24(a0) # CLINT_MTIMECMP(hart)
        ld a2, 32(a0) # interval
        ld a3, 0(a1)
        add a3, a3, a2
        sd a3, 0(a1)

        # arrange for a supervisor software interrupt
        # after this handler returns.
        li a1, 2
        csrw sip, a1

        ld a3, 16(a0)
        ld a2, 8(a0)
        ld a1, 0(a0)
        csrrw a0, mscratch, a0

        mret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，所有的准备工作完成后，我们就能够正式进入内核态进行各种初始化配置和运行了。&lt;/p&gt;
</content:encoded></item><item><title>xv6: The boot loader</title><link>https://chenmiaoi.github.io/2024/05/15/xv6-The-boot-loader/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/05/15/xv6-The-boot-loader/</guid><pubDate>Wed, 15 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在开始讲述内容前，我必须声明的是：&lt;strong&gt;本系列笔记是针对于有一定操作系统基础的(包括不限于：写过简单的OS，学校课程所学的操作系统课等)，因此部分内容个人认为是读者应该知晓的知识，因此不会做过多赘述&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Specify Memory Layout&lt;/h2&gt;
&lt;p&gt;在我们正式介绍&lt;code&gt;xv6-riscv&lt;/code&gt;源码之前，我们首先得了解一个操作系统的内存布局。也就是，&lt;code&gt;xv6-riscv&lt;/code&gt;的入口点以及各个&lt;code&gt;section&lt;/code&gt;的布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405151629479.png&quot; alt=&quot;xv6 内存布局&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们可以看见，&lt;code&gt;xv6-riscv&lt;/code&gt;操作系统的入口点，也就是&lt;code&gt;KERNBASE&lt;/code&gt;位于物理内存的&lt;code&gt;0x8000&apos;0000&lt;/code&gt;处，并且结束于&lt;code&gt;PHYSTOP&lt;/code&gt;(也就是物理内存的&lt;code&gt;0x8640&apos;0000&lt;/code&gt;)处，这样我们就得到：&lt;/p&gt;
&lt;p&gt;$$
PhysicalMemory(ram) = 0x8640&apos;0000 - 0x8000&apos;0000 = 0x640&apos;0000
$$&lt;/p&gt;
&lt;p&gt;现在，现在让我们根据上面的内存布局图来对照对应的&lt;code&gt;link-script&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OUTPUT_ARCH( &quot;riscv&quot; )
ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu&apos;s -kernel jumps.
   */
  . = 0x80000000;

  .text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    _trampoline = .;
    *(trampsec)
    . = ALIGN(0x1000);
    ASSERT(. - _trampoline == 0x1000, &quot;error: trampoline larger than one page&quot;);
    PROVIDE(etext = .);
  }

  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }

  PROVIDE(end = .);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们逐一的来分析该&lt;code&gt;kernel.ld&lt;/code&gt;文件的含义。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;OUTPUT_ARCH( &quot;riscv&quot; )&lt;/code&gt;来说，是指明我们的生成架构是&lt;code&gt;risc-v&lt;/code&gt;，而&lt;code&gt;ENTRY( _entry )&lt;/code&gt;指定程序的入口点为 _entry。&lt;strong&gt;这是一个非常重要的配置，用于确定当程序开始执行时，处理器应该跳转到哪个地址开始执行代码。在操作系统内核和许多其他类型的程序中，这个地址通常是初始化代码的起始位置&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们在上文已经知道，我们设置程序的入口地址为&lt;code&gt;0x8000&apos;0000&lt;/code&gt;，也就是说，我们将&lt;code&gt;_entry&lt;/code&gt;的入口地址绑定为&lt;code&gt;0x8000&apos;0000&lt;/code&gt;，因此，可以看见这一语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;. = 0x80000000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，我们最终生成的镜像文件中的以下&lt;code&gt;section&lt;/code&gt;会从该地址处开始。同时，这也是&lt;code&gt;qemu-riscv&lt;/code&gt;默认加载内核的地址。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c&quot;&gt;qemu risc-v virt.c&lt;/a&gt;文件中，有该定义:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static const MemMapEntry virt_memmap[] = {
    [VIRT_DRAM] = { 0x80000000,           0x0 },
};

const MemMapEntry *memmap = virt_memmap;
MachineState *machine = MACHINE(s);
target_ulong start_addr = memmap[VIRT_DRAM].base;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体&lt;code&gt;qemu-riscv&lt;/code&gt;执行的语句为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;riscv64-linux-gnu-ld ... -T kernel.ld ... -o kernel ...
qemu-system-riscv64 -machine virt ... kernel -m 128M ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;介绍完入口点后，我们就应该介绍各个&lt;code&gt;section&lt;/code&gt;了，有关于&lt;code&gt;ELF Sections&lt;/code&gt;的介绍，请读者自行查阅维基百科或&lt;a href=&quot;https://refspecs.linuxbase.org/elf/elf.pdf&quot;&gt;elf file pdf&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;根据上方的内存映射，我们可以看见，第一个应该分配的便是&lt;code&gt;.text section&lt;/code&gt;，其中，在&lt;code&gt;Kernel text&lt;/code&gt;的映射中，&lt;code&gt;Trampoline&lt;/code&gt;被映射到&lt;code&gt;.text&lt;/code&gt;中(关于&lt;code&gt;trampoline&lt;/code&gt;在后续讲述，此处不做过多描述，只需要知道的是: &lt;code&gt;trampoline&lt;/code&gt;的大小被分配为一个页)，因此可以看见&lt;code&gt;kernel.ld&lt;/code&gt;中的语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    _trampoline = .;
    *(trampsec)
    . = ALIGN(0x1000);
    ASSERT(. - _trampoline == 0x1000, &quot;error: trampoline larger than one page&quot;);
    PROVIDE(etext = .);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;.text&lt;/code&gt;的含义，这里做出简要介绍：&lt;strong&gt;This section holds the &quot;text&quot;, or executable instructions, of a program&lt;/strong&gt;，也就是说，&lt;code&gt;.text&lt;/code&gt;段是代码文本和指令存放的区域。现在，让我们来逐语句解释上面的语句含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*(.text .text.*)&lt;/code&gt;: 这一行指定了在链接时将所有以&lt;code&gt;.text&lt;/code&gt;开头的段(section)和所有&lt;code&gt;.text.&lt;/code&gt;后跟任意字符的段都放置到&lt;code&gt;.text&lt;/code&gt;段中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;. = ALIGN(0x1000)&lt;/code&gt;: 这一行将当前位置(.)对齐到&lt;code&gt;0x1000&lt;/code&gt;(即$4MB/page$)字节的边界。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_trampoline = .&lt;/code&gt;: 这一行将当前位置(.)的值赋给名为&lt;code&gt;_trampoline&lt;/code&gt;的符号，这对应了&lt;code&gt;Trampoline&lt;/code&gt;的映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(trampsec)&lt;/code&gt;: 这一行将所有名为&lt;code&gt;trampsec&lt;/code&gt;的段(如果有的话)添加到&lt;code&gt;.text&lt;/code&gt;段中，其具体含义后续介绍&lt;/li&gt;
&lt;li&gt;&lt;code&gt;. = ALIGN(0x1000)&lt;/code&gt;: 再次将当前位置(.)对齐到&lt;code&gt;0x1000&lt;/code&gt;字节的边界，以确保&lt;code&gt;.text&lt;/code&gt;段的大小是页面大小的整数倍&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ASSERT(. - _trampoline == 0x1000, &quot;error: trampoline larger than one page&quot;)&lt;/code&gt;: 对&lt;code&gt;Trampoline&lt;/code&gt;的映射大小做出检测&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROVIDE(etext = .)&lt;/code&gt;: 这一行定义了一个名为&lt;code&gt;etext&lt;/code&gt;的符号，并将其值设置为当前位置(.)，这定义了一个类似于&lt;code&gt;end_text&lt;/code&gt;的符号标识&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二个映射的内存布局便是&lt;code&gt;Kernel data&lt;/code&gt;，一般而言，&lt;code&gt;data&lt;/code&gt;是有两种的，&lt;strong&gt;只读数据&lt;/strong&gt;以及&lt;strong&gt;可读可写数据&lt;/strong&gt;。在&lt;code&gt;ELF&lt;/code&gt;文件格式中，&lt;code&gt;.rodata&lt;/code&gt;通常是放于&lt;code&gt;.text&lt;/code&gt;之后，&lt;code&gt;.data&lt;/code&gt;之前的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;.rodata&lt;/code&gt;的含义：&lt;strong&gt;These sections hold read-only data that typically contribute to a non-writable segment in the process image&lt;/strong&gt;，也就是说，&lt;code&gt;.rodata&lt;/code&gt;段是由只读数据构成的。现在，让我们来逐语句解释上面的语句含义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;. = ALIGN(16)&lt;/code&gt;: 这行代码将当前的地址对齐到16字节边界。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(.srodata .srodata.*)&lt;/code&gt;: 表示将所有在链接时发现的&lt;code&gt;.srodata&lt;/code&gt;段和以&lt;code&gt;.srodata.&lt;/code&gt;开头的段放入当前段(即&lt;code&gt;.rodata&lt;/code&gt;段)。这包括类似&lt;code&gt;.srodata&lt;/code&gt;和&lt;code&gt;.srodata.foo&lt;/code&gt;这样的段。这些段通常包含只读数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*(.rodata .rodata.*)&lt;/code&gt;: 表示将所有在链接时发现的&lt;code&gt;.rodata&lt;/code&gt;段和以&lt;code&gt;.rodata.&lt;/code&gt;开头的段放入当前段(即&lt;code&gt;.rodata&lt;/code&gt;段)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;.data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
}

.bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
}

PROVIDE(end = .);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紧接着的分配的&lt;code&gt;.data&lt;/code&gt;和&lt;code&gt;.bss&lt;/code&gt;段，这里不再赘述，而&lt;code&gt;.data&lt;/code&gt;的含义为: &lt;strong&gt;These sections hold initialized data that contribute to the program&apos;s memory image&lt;/strong&gt;，也就是说，&lt;code&gt;.data&lt;/code&gt;段包含了初始化的数据；而&lt;code&gt;.bss&lt;/code&gt;段的含义为：&lt;strong&gt;This section holds uninitialized data that contribute to the program&apos;s memory image&lt;/strong&gt;，也就是说，&lt;code&gt;.bss&lt;/code&gt;包含了未初始化数据。&lt;/p&gt;
&lt;p&gt;至此，&lt;code&gt;xv6-riscv&lt;/code&gt;简单的内存布局便设置好了，现在，让我们把视角从内存布局处移到内核入口点。&lt;/p&gt;
&lt;h2&gt;Kernel Entry Point&lt;/h2&gt;
&lt;p&gt;不用多说，我们的内核环境肯定是&quot;裸机的&quot;，因此，我们需要根据上面的内存布局设置的入口点，设置我们的入口函数。而如何进入我们的入口函数，就需要通过汇编直接跳转到入口地址了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.section .text
.global _entry
_entry:
        # set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0
        li a0, 1024*4
        csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
        # jump to start() in start.c
        call start
spin:
        j spin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以看见一个很关键的地方：&lt;code&gt;.text&lt;/code&gt;段的声明，我们在上面一节中知道，&lt;code&gt;Kernel text&lt;/code&gt;也就是&lt;code&gt;.text&lt;/code&gt;是从&lt;code&gt;0x8000&apos;0000&lt;/code&gt;地址处开始的，并且，&lt;code&gt;.text&lt;/code&gt;是存储代码文本和程序指令的地方。因此，当内核启动时，会启动&lt;code&gt;_entry&lt;/code&gt;作为入口点，而该入口点，就进入了上面的这一块代码。&lt;/p&gt;
&lt;p&gt;现在，让我们来逐语句的解释其含义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define NCPU          8  // maximum number of CPUs
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
la sp, stack0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先要为每一个&lt;code&gt;hart&lt;/code&gt;分配对应的内核栈，而有一个比较有趣的现象是，&lt;code&gt;__attribute__ ((aligned (16)))&lt;/code&gt;，这与我们分配&lt;code&gt;.bss&lt;/code&gt;的时候一致，需要进行十六个字节的对齐。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;li a0, 1024*4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是设置了每一个&lt;code&gt;hart&lt;/code&gt;对应的内核栈大小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从&lt;code&gt;mhartid&lt;/code&gt;寄存器中读取硬件线程&lt;code&gt;ID&lt;/code&gt;，计算出每一个&lt;code&gt;hart&lt;/code&gt;自身对应的内核栈的偏移量，然后设置&lt;code&gt;sp&lt;/code&gt;指针指向当前&lt;code&gt;hart&lt;/code&gt;的专属内核栈位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;call start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终转跳到&lt;code&gt;start&lt;/code&gt;函数中，而&lt;code&gt;start&lt;/code&gt;函数，便是整个内核的开始位置。下面给出对应的实际汇编代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0000000080000000 &amp;lt;_entry&amp;gt;:
    80000000:	00009117          	auipc	sp,0x9
    80000004:	a1010113          	addi	sp,sp,-1520 # 80008a10 &amp;lt;stack0&amp;gt;
    80000008:	6505                	lui	a0,0x1
    8000000a:	f14025f3          	csrr	a1,mhartid
    8000000e:	0585                	addi	a1,a1,1
    80000010:	02b50533          	mul	a0,a0,a1
    80000014:	912a                	add	sp,sp,a0
    80000016:	078000ef          	jal	ra,8000008e &amp;lt;start&amp;gt;
    
000000008000008e &amp;lt;start&amp;gt;:
{
    8000008e:	1141                	addi	sp,sp,-16
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此，可以看见，&lt;code&gt;_entry&lt;/code&gt;入口点确实在&lt;code&gt;0x8000&apos;0000&lt;/code&gt;地址处，而最终也会正确的转跳到&lt;code&gt;start&lt;/code&gt;函数中。同时，在&lt;code&gt;start&lt;/code&gt;函数中，使用的&lt;code&gt;sp&lt;/code&gt;指针也是独属于当前&lt;code&gt;hart&lt;/code&gt;的内核栈。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;RISC-V Hart&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在&lt;code&gt;RISC-V&lt;/code&gt;架构中，&lt;code&gt;hart&lt;/code&gt;代表硬件线程(&lt;code&gt;hardware thread&lt;/code&gt;), 在一个多核处理器中，每个核心可以有一个或多个&lt;code&gt;hart&lt;/code&gt;。每个 hart 在系统中都有一个唯一的标识符，称为 hart ID。在&lt;code&gt;RISC-V&lt;/code&gt;架构中，可以通过读取特定的&lt;code&gt;CSR(Control and Status Register)&lt;/code&gt;来获取当前执行的&lt;code&gt;hart&lt;/code&gt;的&lt;code&gt;ID&lt;/code&gt;，通常是&lt;code&gt;mhartid&lt;/code&gt;寄存器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202405152046139.png&quot; alt=&quot;hart&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>rfc1951: DEFLATE Compressed Data Format Specification</title><link>https://chenmiaoi.github.io/2024/05/10/rfc1951-DEFLATE-Compressed-Data-Format-Specification/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/05/10/rfc1951-DEFLATE-Compressed-Data-Format-Specification/</guid><pubDate>Fri, 10 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Abstract&lt;/h2&gt;
&lt;p&gt;该规范定义了一种无损压缩数据格式，使用&lt;code&gt;LZ77&lt;/code&gt;算法和哈夫曼编码的组合压缩数据，效率可与当前可用的最佳通用压缩方法相当。即使是对于一个长度任意的顺序呈现的输入数据流，数据可以被生成或消耗，而只需要有限的中间存储空间。该格式可以轻松地以一种未被专利覆盖的方式实现。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;h3&gt;Purpose&lt;/h3&gt;
&lt;p&gt;该规范的目的是定义一种无损压缩的数据格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与&lt;code&gt;CPU&lt;/code&gt;类型、操作系统、文件系统和字符集相独立；因此可以互换使用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以压缩或解压缩一个数据流(与此相对的是随机访问介质文件)来产生另一种数据流，只使用预先限定的中间存储量，因此可以用于数据通信或类似结果&lt;/strong&gt;，比如&lt;code&gt;Unix过滤器&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据的压缩效率可以与当前最佳的通用压缩方法相媲美&lt;/strong&gt;，尤其是比&quot;compress&quot;程序更好;&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;code&gt;gzip&lt;/code&gt;&lt;strong&gt;实用程序生成的文件格式兼容，符合标准的解压器能够读取由现有&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;压缩器生成的数据&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;可以压缩或解压缩数据流：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将数据转化为一种更为紧凑的表示形式，以减少其占用空间；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;只实用有限量的中间存储空间：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也就是说，数据的大小在一开始就会被计算，不会随着数据流的大小变化而增长；&lt;strong&gt;使用缓冲区或许能够提升一定的性能&lt;/strong&gt;？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;专利不涵盖：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;该压缩格式没有与之相关的专利保护&lt;/strong&gt;。换句话说，使用该压缩格式不会侵犯其他人的专利权利，因为该格式的设计没有涉及到受专利保护的技术或方法。这意味着任何人都可以自由地实现、使用和分发这种压缩格式，而不必担心侵犯他人的知识产权。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;本规范定义的数据格式不会做出以下尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供对压缩数据的随机访问;&lt;/li&gt;
&lt;li&gt;压缩专用数据以及目前最好的专用算法;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这上诉的点上，和上一篇规范&lt;code&gt;GZIP&lt;/code&gt;一致。一个简单的计数论证表明：&lt;strong&gt;任何无损压缩算法都不能压缩每一个可能的输入数据集&lt;/strong&gt;。对于本文定义的格式，最坏情况下每$32K$字节块的扩展为$5$个字节，即对于大数据集来说，大小增加了$0.015%$。英文文本通常能够压缩为原来的$2.5 \sim 3$倍；可执行文件通常压缩得稍微少一些；类似于光栅图像这样的图形数据可能会压缩的更多。&lt;/p&gt;
&lt;h3&gt;Intended audience&lt;/h3&gt;
&lt;p&gt;本规范供软件实现者使用，&lt;strong&gt;用于将数据压缩为&lt;/strong&gt;&lt;code&gt;deflate&lt;/code&gt;&lt;strong&gt;格式和(或)从&lt;/strong&gt;&lt;code&gt;deflate&lt;/code&gt;格式中解压数据。&lt;/p&gt;
&lt;p&gt;本规范的文本假定读者具有编程方面的基础知识，能够理解位和其他基本数据表示级别的概念。熟悉哈夫曼编码技术是有帮助的，但这并非必须的。&lt;/p&gt;
&lt;h3&gt;Score&lt;/h3&gt;
&lt;p&gt;本规范规定了一种将字节序列表示为(通常较短的)比特序列的方法，以及将后面的比特序列打包为字节的方法。&lt;/p&gt;
&lt;h3&gt;Compliance&lt;/h3&gt;
&lt;p&gt;除非下文另有说明，&lt;strong&gt;符合规范的解压器必须能够接收并解压符合本文提出的所有规范的数据集；符合规范的压缩器必须生成符合此处提供的所有规范的数据集&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Definitions of terms and conventions used&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;byte&lt;/code&gt;: 以单位形式存储或传输的&lt;code&gt;8 bit&lt;/code&gt;。(对于此规范，一个字节恰好是8位，即使在将字节存储在与8位不同的位数的计算机也是如此。)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt;: 任意字节的序列&lt;/p&gt;
&lt;h3&gt;Changes from previous versions&lt;/h3&gt;
&lt;p&gt;自从这个规范的&lt;code&gt;1.1&lt;/code&gt;版本以来，&lt;code&gt;deflate&lt;/code&gt;格式没有发生任何技术变化。在&lt;code&gt;1.2&lt;/code&gt;版本中，一些术语被更改了。&lt;code&gt;1.3&lt;/code&gt;版本是将规范转换为&lt;code&gt;RFC&lt;/code&gt;风格的版本。&lt;/p&gt;
&lt;h2&gt;Compress representation overview&lt;/h2&gt;
&lt;p&gt;一个压缩数据集由一系列块组成，对应于输入数据的连续块。块的大小是任意的，除了不可压缩块被限制在$65535$字节之外。&lt;/p&gt;
&lt;p&gt;每个块都是用&lt;code&gt;LZ77&lt;/code&gt;算法和哈夫曼编码的组合进行压缩。每个块的哈夫曼树与前后块的哈夫曼树无关；&lt;code&gt;LZ77&lt;/code&gt;算法可以使用对前一个块中出现的重复字符串引用，最多可以回溯到$32K$个输入字节。&lt;/p&gt;
&lt;p&gt;每个块都由两部分组成：一对描述压缩数据部分表示的哈夫曼编码树，以及一个压缩数据部分。(哈夫曼树自身也会使用哈夫曼编码进行压缩。)压缩数据由两种类型的元素组成：&lt;code&gt;文字字节(literal bytes)&lt;/code&gt;(即在前$32K$个输入字节中未被检测为重复的字符串)和指向重复字符串的指针，其中指针表示为$&amp;lt;length, backward\  distance&amp;gt;$。&lt;code&gt;deflate&lt;/code&gt;格式中使用的表示方法将距离($distance$)限制为$32K$字节，长度($length$)限制为$258$字节，但不限制块的大小，除了上述已经注意到的不可压缩块。&lt;/p&gt;
&lt;p&gt;压缩数据中的每种值类型(&lt;code&gt;literal&lt;/code&gt;, &lt;code&gt;distance&lt;/code&gt;和&lt;code&gt;lengths&lt;/code&gt;)都是用哈夫曼编码表示，使用一个用于文字(&lt;code&gt;literal&lt;/code&gt;)和长度(&lt;code&gt;length&lt;/code&gt;)的编码树，以及一个用于距离(&lt;code&gt;distance&lt;/code&gt;)的单独编码树。每个块的编码树都以紧凑形式出现在该块的压缩数据之前。&lt;/p&gt;
&lt;h2&gt;Detailed specification&lt;/h2&gt;
&lt;h3&gt;Overall conventions&lt;/h3&gt;
&lt;p&gt;在下方的图示中，一个这样的&lt;code&gt;box&lt;/code&gt;表示一个字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+ 
|   | &amp;lt;-- the vertical bars might be missing 
+---+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而类似于这样的&lt;code&gt;box&lt;/code&gt;表示可变的字节数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+==============+ 
|              | 
+==============+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;存储在计算机中的字节不具有“位序”，因为它们总是被视作一个单位来处理&lt;/strong&gt;。然而，将字节视为介于$0 \sim 255$之间的整数确实具有最高有效位和最低有效位，因为我们总是以最高有效数字在左侧的方式书写数字，因此我们也以最高有效位在左侧的方式书写字节。&lt;/p&gt;
&lt;p&gt;在下方的图示中，我们对一个字节的比特进行编号，使得比特&lt;code&gt;0&lt;/code&gt;是最低有效位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+--------+ 
|76543210| 
+--------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在计算机中，一个数字可能占据多个字节。在这里描述的格式中，&lt;strong&gt;所有的多字节数字都是以最低有效位优先的方式存储&lt;/strong&gt;(&lt;strong&gt;即在较低的内存地址处&lt;/strong&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    0        1 
+--------+--------+ 
|00001000|00000010| 
+--------+--------+ 
ˆ         ˆ 
|         | 
|         + more significant byte = 2 x 256 
+ less significant byte = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Packing into bytes&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;由于本文描述的数据格式是面向字节的，而不是面向比特的；因此，本文没有解决字节的比特在比特序列介质上传输的顺序问题&lt;/strong&gt;。然而，&lt;strong&gt;我们将在下文中将压缩快格式描述为各种比特长度的数据元素的序列，而不是字节的序列。因此，我们必须指定如何将这些数据元素打包成字节，以形成最终的压缩字节序列&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据元素按照字节内位序递增的顺序打包到字节中，即从字节的最低有效位开始&lt;/li&gt;
&lt;li&gt;哈夫曼编码以外的数据元素从数据元素的最低有效位开始打包&lt;/li&gt;
&lt;li&gt;哈夫曼编码从编码的最高有效位开始打包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换句话说，如果将压缩数据按字节序列打印出来，从右边界开始的第一个字节，然后向左边进行，每个字节的最高有效位通常在左边，那么可以从右到左解析结果，固定宽度的元素按照正确的最高有效位到最低有效位的顺序排列，而哈夫曼编码则按位逆序排列（即，编码的第一位在相对的最低有效位位置）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;假设我们有一个字节序列，其中每个字节包含以下数据元素：
- 3位长度（LSB至MSB）
- 2位距离（LSB至MSB）
- 3位文字（LSB至MSB）
现在让我们考虑一个示例字节：10110010。

根据先前的说明，我们从右到左解析：
- 最右侧的位（最低有效位）表示文字的最低位。
- 接下来的2位表示距离。
- 接下来的3位表示长度。
- 最左侧的3位表示文字的最高位。
所以，10110010字节可以解析为：
- 文字：010
- 距离：01
- 长度：100
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compressed block format&lt;/h3&gt;
&lt;h4&gt;Synopsis of prefix and Huffman coding&lt;/h4&gt;
&lt;p&gt;前缀编码(prefix code)通过位序列(代码)来表示来自已知字母表的符号，每个符号对应一个代码，以一种方式进行，以便不同的符号可以由不同长度的位序列表示，但解析器始终可以逐个符号无歧义的解析编码的字符串。&lt;/p&gt;
&lt;p&gt;我们&lt;strong&gt;将前缀码定义为一种二叉树，其中每个非叶节点下降的两条边分别标记为&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;，&lt;strong&gt;叶节点与字母表中的符号一一对应（用标签标记）；然后，符号的编码是从根节点到标记为该符号的叶节点的边上的&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;序列&lt;/strong&gt;。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            /\          Symbol      Code 
           0  1         ------      ----
          /    \            A        00 
         /\     B           B         1 
        0  1                C       011 
       /    \               D       010 
      A     /\ 
           0  1 
          /    \ 
         D      C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析器可以通过从根节点开始沿着树向下移动，在每一步选择与下一个输入位对应的边来从编码的输入流中解码下一个符号。&lt;/p&gt;
&lt;p&gt;在已知符号频率的字母表中，哈夫曼算法允许构建最佳前缀码(使用最少位数表示具有该字母表符号频率的字符串的前缀码)。这样的代码称为哈夫曼码。(有关赫夫曼码的更多信息，请参见参考文献&amp;lt;&lt;a href=&quot;https://www.ias.ac.in/article/fulltext/reso/011/02/0091-0099&quot;&gt;Huffman, D. A., “A Method for the Construction of Minimum Redundancy Codes”, Proceedings of the Institute of Radio Engineers, September 1952, Volume 40, Number 9, pp. 1098-1101&lt;/a&gt;&amp;gt;)&lt;/p&gt;
&lt;p&gt;请注意，在&lt;code&gt;deflate&lt;/code&gt;格式中，&lt;strong&gt;各种字母表的哈夫曼码长度不能超过某些最大码长。这个约束条件使得从符号频率计算码长的算法变得更加复杂&lt;/strong&gt;。详细信息请参见上述的参考文献。&lt;/p&gt;
</content:encoded></item><item><title>rfc1952: GZIP file format specification</title><link>https://chenmiaoi.github.io/2024/05/09/rfc1952-GZIP-file-format-specification/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/05/09/rfc1952-GZIP-file-format-specification/</guid><pubDate>Thu, 09 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Abstract&lt;/h2&gt;
&lt;p&gt;本篇规范定义了一种无损压缩数据格式，与广泛使用的&lt;code&gt;GZIP&lt;/code&gt;实用程序兼容。该格式包括&lt;code&gt;循环冗余校验值(cyclic redundancy check, CRC)&lt;/code&gt;，用于检测数据损坏。该格式目前使用&lt;code&gt;DEFLATE&lt;/code&gt;压缩方法，但可以轻松扩展以使用其他压缩方法。该格式可以轻松地以未受专利覆盖的方式实现。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;h3&gt;Purpose&lt;/h3&gt;
&lt;p&gt;该规范的目的是定义一种无损压缩的数据格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与&lt;code&gt;CPU&lt;/code&gt;类型、操作系统、文件系统和字符集相独立；因此可以互换使用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以压缩或解压缩一个数据流(与此相对的是随机访问介质文件)来产生另一种数据流，只使用预先限定的中间存储量，因此可以用于数据通信或类似结果&lt;/strong&gt;，比如&lt;code&gt;Unix过滤器&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据的压缩效率可以与当前最佳的通用压缩方法相媲美&lt;/strong&gt;，尤其是比&quot;compress&quot;程序更好;&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;code&gt;gzip&lt;/code&gt;&lt;strong&gt;实用程序生成的文件格式兼容，符合标准的解压器能够读取由现有&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;压缩器生成的数据&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;可以压缩或解压缩数据流：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将数据转化为一种更为紧凑的表示形式，以减少其占用空间；&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;只实用有限量的中间存储空间：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也就是说，数据的大小在一开始就会被计算，不会随着数据流的大小变化而增长；&lt;strong&gt;使用缓冲区或许能够提升一定的性能&lt;/strong&gt;？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;专利不涵盖：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;该压缩格式没有与之相关的专利保护&lt;/strong&gt;。换句话说，使用该压缩格式不会侵犯其他人的专利权利，因为该格式的设计没有涉及到受专利保护的技术或方法。这意味着任何人都可以自由地实现、使用和分发这种压缩格式，而不必担心侵犯他人的知识产权。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;本规范定义的数据格式不会做出以下尝试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供对压缩数据的随机访问;&lt;/li&gt;
&lt;li&gt;压缩专用数据以及目前最好的专用算法;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Intended audience&lt;/h3&gt;
&lt;p&gt;本规范供软件实现者使用，&lt;strong&gt;用于将数据压缩为&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;格式和(或)从&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;格式中解压数据。&lt;/p&gt;
&lt;p&gt;本规范的文本假定读者具有编程方面的基础知识，能够理解位和其他基本数据表示级别的概念。&lt;/p&gt;
&lt;h3&gt;Scope&lt;/h3&gt;
&lt;p&gt;本规范指定了一种压缩方法和一个文件格式(&lt;strong&gt;后者仅假定文件可以存储在任意字节序列&lt;/strong&gt;)。它没有指定与文件系统的任何特定接口，也没有涉及字符集或编码的任何内容(除了文件名和注释，这些都是可选的)。&lt;/p&gt;
&lt;h3&gt;Compliance&lt;/h3&gt;
&lt;p&gt;除非下文另有说明，&lt;strong&gt;符合规范的解压器必须能够接收并解压符合此处提供的所有规范的任何文件；符合规范的压缩器必须生成符合此处提供的所有规范的文件&lt;/strong&gt;。附录中的材料不属于规范本身，与符合性无关。&lt;/p&gt;
&lt;h3&gt;Definitions of terms and conventions used&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;byte&lt;/code&gt;: 以单位形式存储或传输的&lt;code&gt;8 bit&lt;/code&gt;。(对于此规范，一个字节恰好是8位，即使在将字节存储在与8位不同的位数的计算机也是如此。)&lt;/p&gt;
&lt;h3&gt;Changes from previous versions&lt;/h3&gt;
&lt;p&gt;自本规范的&lt;code&gt;4.1&lt;/code&gt;版本起，未对&lt;code&gt;gzip&lt;/code&gt;格式进行任何技术变更。在&lt;code&gt;4.2&lt;/code&gt;版本中，更改了一些术语，并重写了示例&lt;code&gt;CRC&lt;/code&gt;代码，以提高清晰度并消除调用者进行前置和后置条件的要求。&lt;code&gt;4.3&lt;/code&gt;版本是将规范转为&lt;code&gt;RFC&lt;/code&gt;风格的过程。&lt;/p&gt;
&lt;h2&gt;Detailed specification&lt;/h2&gt;
&lt;h3&gt;Overall conventions&lt;/h3&gt;
&lt;p&gt;在下方的图示中，一个这样的&lt;code&gt;box&lt;/code&gt;表示一个字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+ 
|   | &amp;lt;-- the vertical bars might be missing 
+---+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而类似于这样的&lt;code&gt;box&lt;/code&gt;表示可变的字节数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+==============+ 
|              | 
+==============+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;存储在计算机中的字节不具有“位序”，因为它们总是被视作一个单位来处理&lt;/strong&gt;。然而，将字节视为介于$0 \sim 255$之间的整数确实具有最高有效位和最低有效位，因为我们总是以最高有效数字在左侧的方式书写数字，因此我们也以最高有效位在左侧的方式书写字节。&lt;/p&gt;
&lt;p&gt;在下方的图示中，我们对一个字节的比特进行编号，使得比特&lt;code&gt;0&lt;/code&gt;是最低有效位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+--------+ 
|76543210| 
+--------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;由于本文描述的数据格式是面向字节的，而不是面向比特的；因此，本文没有解决字节的比特在比特序列介质上传输的顺序问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在计算机中，一个数字可能占据多个字节。在这里描述的格式中，&lt;strong&gt;所有的多字节数字都是以最低有效位优先的方式存储&lt;/strong&gt;(&lt;strong&gt;即在较低的内存地址处&lt;/strong&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    0        1 
+--------+--------+ 
|00001000|00000010| 
+--------+--------+ 
ˆ         ˆ 
|         | 
|         + more significant byte = 2 x 256 
+ less significant byte = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;File format&lt;/h3&gt;
&lt;p&gt;一个&lt;code&gt;gzip&lt;/code&gt;文件由一系列&quot;成员&quot;(压缩数据集)组成。每个成员的格式在下面会详细说明。成员只是在文件中一个接一个地出现，在它们之前、之间或之后没有额外的信息。&lt;/p&gt;
&lt;h3&gt;Member format&lt;/h3&gt;
&lt;p&gt;每一个成员都有如下结构:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+---+---+---+---+---+---+---+---+---+ 
|ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more--&amp;gt;) 
+---+---+---+---+---+---+---+---+---+---+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;rfc1952-GZIP-file-format-specification.md&quot;&gt;rfc1952-GZIP-file-format-specification.md&lt;/a&gt;
如果&lt;code&gt;FLG::FEXTRA&lt;/code&gt;字段被设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+---+=================================+ 
| XLEN |...XLEN bytes of &quot;extra field&quot;...| (more--&amp;gt;) 
+---+---+=================================+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;FLG::FNAME&lt;/code&gt;字段被设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+=========================================+ 
|...original file name, zero-terminated...| (more--&amp;gt;) 
+=========================================+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;FLG::FCOMMENT&lt;/code&gt;字段被设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+===================================+ 
|...file comment, zero-terminated...| (more--&amp;gt;) 
+===================================+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;FLG::FHCRC&lt;/code&gt;字段被设置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+---+ 
| CRC16 | 
+---+---+

+=======================+ 
|...compressed blocks...| (more--&amp;gt;) 
+=======================+

  0   1   2   3   4   5   6   7 
+---+---+---+---+---+---+---+---+ 
|     CRC32     |     ISIZE     | 
+---+---+---+---+---+---+---+---+
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Member header and trailer&lt;/h4&gt;
&lt;h5&gt;ID1(IDentification 1)&lt;/h5&gt;
&lt;h5&gt;ID2(IDentification 2)&lt;/h5&gt;
&lt;p&gt;这两个字段是固定值：$ID1 = 31(0x1f, \037)，ID2 = 139(0x8b, \213)$，&lt;strong&gt;用于标识文件为&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;格式。&lt;/p&gt;
&lt;h5&gt;CM(Compression Method)&lt;/h5&gt;
&lt;p&gt;该字段确定了文件中使用的压缩方法。$CM = 0 - 7$被保留，$CM = 8$表示&lt;code&gt;deflate&lt;/code&gt;压缩方法，这是&lt;code&gt;gzip&lt;/code&gt;习惯使用的压缩方法，其他文档中有详细说明。&lt;/p&gt;
&lt;h5&gt;FLG(FLaGs)&lt;/h5&gt;
&lt;p&gt;该标志字段被划分为单个比特，如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bit&lt;/th&gt;
&lt;th&gt;位&lt;/th&gt;
&lt;th&gt;标识&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;FTEXT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;FHCRC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;FEXTRA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;FNAME&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;FCOMMENT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bit&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果&lt;code&gt;FTEXT&lt;/code&gt;被设置，该文件就很可能是&lt;code&gt;ASCII&lt;/code&gt;文本。这是一个可选的指示，&lt;strong&gt;压缩器可以通过减少少量的输入数据来查看是否存在任何非&lt;/strong&gt;&lt;code&gt;ASCII&lt;/code&gt;&lt;strong&gt;字符&lt;/strong&gt;。如果存疑，&lt;code&gt;FTEXT&lt;/code&gt;将会被重置，表示二进制数据。对于&lt;code&gt;ASCII&lt;/code&gt;文本和二进制数据格式不同的系统，解压器可以使用&lt;code&gt;FTEXT&lt;/code&gt;选项选择合适的文件格式。&lt;strong&gt;我们有意不指定此位的算法，因为压缩程序始终有可能将其清零，而解压程序始终有可能忽略它，让其他程序处理数据转换的问题&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FTEXT&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在压缩和解压缩过程中，如果&lt;code&gt;FTEXT&lt;/code&gt;字段被设置为&lt;code&gt;1&lt;/code&gt;，压缩器和解压器可以相应地进行处理，以确保数据的正确性和可读性。如果&lt;code&gt;FTEXT&lt;/code&gt;字段被设置为&lt;code&gt;0&lt;/code&gt;，压缩器和解压器可以将数据视为二进制数据处理，而不会进行额外的文本处理。这样设计是为了提高灵活性，使得&lt;code&gt;gzip&lt;/code&gt;格式可以适应不同类型的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果&lt;code&gt;FHCRC&lt;/code&gt;被设置，则在压缩数据的前面，&lt;code&gt;gzip&lt;/code&gt;头部存在一个&lt;code&gt;CRC16&lt;/code&gt;数据。&lt;code&gt;CRC16&lt;/code&gt;由&lt;code&gt;gzip&lt;/code&gt;头部中直到&lt;code&gt;CRC16&lt;/code&gt;之前的所有字节的&lt;code&gt;CRC32&lt;/code&gt;的最低有效字节组成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ID1 = 32
ID2 = 139
CM = 8
FLG = 2
CRC32 = 0x11223344

==&amp;gt; CRC16 = 0x3344
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果&lt;code&gt;FEXTRA&lt;/code&gt;被设置，则存在可选的额外字段(将会在后续介绍)。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;FNAME&lt;/code&gt;被设置，则存在一个原始文件名，&lt;strong&gt;以零字节终止&lt;/strong&gt;。名称必须以&lt;code&gt;ISO 8859-1(LATIN-1)&lt;/code&gt;字符组成;在使用&lt;code&gt;EBCDIC&lt;/code&gt;或任何其他字符集作为文件名的操作系统上，必须将文件名翻译为&lt;code&gt;ISO LATIN-1&lt;/code&gt;字符集。&lt;strong&gt;这是被压缩的文件的原始名称，其中已删除了任何目录组件，并且如果被压缩的文件位于不区分大小写的文件系统上，则将其强制转换为小写&lt;/strong&gt;。&lt;strong&gt;如果数据不是从具名文件而是从其他来源压缩的，则没有原始文件名&lt;/strong&gt;；例如，如果数据源是&lt;code&gt;Unix&lt;/code&gt;系统上的&lt;code&gt;stdin&lt;/code&gt;，则没有文件名。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[零字节终止文件名]&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原始文件名以零字节终止，这意味着在gzip文件格式中，原始文件名的末尾是一个零字节（ASCII码为0）。这个零字节用来表示文件名的结束，后面可能会跟随其他数据。这种方式使得解压缩程序可以轻松地确定原始文件名的结束位置，而不必依赖于固定长度或其他分隔符。&lt;br /&gt;
&quot;example.txt&quot; =&amp;gt; 65 78 61 6d 70 6c 65 2e 74 78 74 00&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cs.stanford.edu/people/miles/iso8859.html&quot;&gt;ISO LATIN-1字符集&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;它定义了用于拉丁字母语言的8位字符编码，覆盖了大多数西欧语言，如英语、法语、德语、西班牙语、意大利语等。&lt;code&gt;ISO LATIN-1&lt;/code&gt;编码的范围是&lt;code&gt;0x00&lt;/code&gt;到&lt;code&gt;0xFF&lt;/code&gt;，其中包含了基本拉丁字母、拉丁字母补充、数字、标点符号以及一些特殊字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ibm.com/docs/en/zos-basic-skills?topic=mainframe-ebcdic-character-set&quot;&gt;EBCDIC(Extended Binary Coded Decimal Interchange Code)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通常用于&lt;code&gt;IBM&lt;/code&gt;的大型机系统（如&lt;code&gt;System/360&lt;/code&gt;，&lt;code&gt;System/370&lt;/code&gt;和&lt;code&gt;zSeries&lt;/code&gt;）。与&lt;code&gt;ASCII&lt;/code&gt;和&lt;code&gt;ISO Latin-1&lt;/code&gt;不同，&lt;code&gt;EBCDIC&lt;/code&gt;编码使用8位编码，并且其编码值与&lt;code&gt;ASCII&lt;/code&gt;或&lt;code&gt;ISO Latin-1&lt;/code&gt;中的对应字符不同。&lt;code&gt;EBCDIC&lt;/code&gt;编码方案包含了数字、字母、标点符号和其他特殊字符。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果&lt;code&gt;FCOMMENT&lt;/code&gt;被设置，&lt;strong&gt;则存在一个以零字节终止的文件注释。此注释不会被解释；它仅供人类阅读&lt;/strong&gt;。注释必须由&lt;code&gt;ISO 8859-1(LATIN-1)&lt;/code&gt;字符组成。换行应该用单个换行字符(10进制)表示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预留的FLG位应该被置零&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;MTIME(Modification TIME)&lt;/h5&gt;
&lt;p&gt;该字段给出了源文件被压缩时的最近修改时间。这个时间是&lt;code&gt;Unix&lt;/code&gt;格式的(也就是格林威治时间)。(&lt;strong&gt;注意，这可能会给&lt;code&gt;MSDOS&lt;/code&gt;和其他使用本地时间而非全球时间的系统带来问题&lt;/strong&gt;)。如果压缩后的数据不是来自于文件，那么&lt;code&gt;MTIME&lt;/code&gt;则会被设置为压缩开始的时间。$MTIME = 0$表示没有可用的时间戳。&lt;/p&gt;
&lt;h5&gt;XFL(eXtra FLags)&lt;/h5&gt;
&lt;p&gt;这些标志位可以通过特定的压缩方法使用。&lt;code&gt;DEFLATE&lt;/code&gt;方法($CM = 8$)将这些标志位设置如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$XFL = 2$表示压缩器使用最大压缩、最慢算法&lt;/li&gt;
&lt;li&gt;$XFL = 4$表示压缩器使用最快算法&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;OS(Operating System)&lt;/h5&gt;
&lt;p&gt;该字段标识了发生压缩的文件系统类型，这对于确定文本文件的行尾约定可能是有用的。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bit&lt;/th&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;FAT filesystem(MS-DOS, OS/2, NT/Win32)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Amiga&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;VMS(or OpenVMS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Unix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;VM/CMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Atari TOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;HPFS filesystem(OS/2, NT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Macintosh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Z-System&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;CP/M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;TOPS-20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;NTFS filesystem(NT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;QDOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Acorn RISCOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;常见的行尾约定为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unix/Linux -&amp;gt; LF&lt;br /&gt;
Windows -&amp;gt; CRLF&lt;br /&gt;
macOS -&amp;gt; CR&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h5&gt;XLEN(eXtra LENgth)&lt;/h5&gt;
&lt;p&gt;如果&lt;code&gt;FLG::FEXYRA&lt;/code&gt;被设置，给出一个可选的额外字段的长度，详情见下文。&lt;/p&gt;
&lt;h5&gt;CRC32(CRC-32)&lt;/h5&gt;
&lt;p&gt;这个字段包含了未压缩数据的循环冗余校验值，根据ISO 3309标准和ITU-T推荐V.42中的CRC-32算法计算得出。你可以在 http://www.iso.ch 上订购ISO文件。你也可以在 gopher://info.itu.ch 上找到ITU-T V.42的在线版本。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cdn.standards.iteh.ai/samples/8561/ee3e6fc1cc8641fabff5257e9660cf07/ISO-IEC-3309-1993.pdf&quot;&gt;CRC-32 ISO3309&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;定义了一种循环冗余校验（CRC）算法，用于检测数据的传输错误。具体来说，ISO 3309标准描述了CRC-32算法，这是一种32位的循环冗余校验算法，常用于检测数据的完整性。&lt;br /&gt;
CRC-32算法通过对数据流进行多项式除法运算来计算校验值，然后将该校验值附加到数据流中。接收方可以使用相同的CRC-32算法来计算接收到的数据的校验值，并将其与发送方提供的校验值进行比较，从而检测数据是否在传输过程中发生了错误或损坏。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.itu.int/rec/T-REC-V.42/en&quot;&gt;ITU-T V.42&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体而言，ITU-T V.42标准涵盖了数据压缩、误码纠正和数据流控制等方面的内容。其中，压缩算法和误码纠正技术对于提高数据传输效率和减少传输错误非常重要。在ITU-T V.42中提到的CRC-32算法是用于数据传输中的校验和验证，以确保数据的完整性。&lt;br /&gt;
ITU-T V.42标准的发布旨在促进数据通信技术的发展和应用，为各种类型的数据通信提供了一个共同的技术框架和标准化的基础。这有助于不同厂商和组织之间的互操作性，并为用户提供了更可靠和高效的数据通信解决方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h5&gt;ISIZE(Input SIZE)&lt;/h5&gt;
&lt;p&gt;该字段包含了原始输入数据(未压缩数据)$mod 2^{32}$的大小&lt;/p&gt;
&lt;h5&gt;Extra field&lt;/h5&gt;
&lt;p&gt;如果&lt;code&gt;FLG::FEXTRA&lt;/code&gt;被设置，那么一个“额外字段”就会出现在头部中，总长度为&lt;code&gt;XLEN&lt;/code&gt;字节。它由一系列子字段组成，每一个子字段的形式都是如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---+---+---+---+==================================+ 
|SI1|SI2|  LEN  |... LEN bytes of subfield data ...| 
+---+---+---+---+==================================+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SI1&lt;/code&gt;和&lt;code&gt;SI2&lt;/code&gt;提供了一个子字段&lt;code&gt;ID&lt;/code&gt;，通常是两个具有一定助记符价值的&lt;code&gt;ASCII&lt;/code&gt;字母。[&lt;strong&gt;Jean-Loup Gailly&lt;/strong&gt;]&lt;a href=&quot;mailto:gzip@prep.ai.mit.edu&quot;&gt;gzip@prep.ai.mit.edu&lt;/a&gt;维护着一个子字段ID的注册表；请将您希望使用的任何子字段&lt;code&gt;ID&lt;/code&gt;发送给他。&lt;code&gt;SI2&lt;/code&gt;为&lt;code&gt;0&lt;/code&gt;的子字段&lt;code&gt;ID&lt;/code&gt;保留供将来使用。当前已定义的子字段&lt;code&gt;ID&lt;/code&gt;如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SI1         SI2         Data 
----------  ----------  ---
0x41 (&apos;A&apos;)  0x70 (&apos;P&apos;)  Apollo file type information
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;LEN&lt;/code&gt;字段给出了子字段数据的长度，不包括四个初始字节。&lt;/p&gt;
&lt;h5&gt;Compliance&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;符合规范的压缩器必须生成具有正确的&lt;/strong&gt;&lt;code&gt;ID1&lt;/code&gt;、&lt;code&gt;ID2&lt;/code&gt;、&lt;code&gt;CM&lt;/code&gt;、&lt;code&gt;CRC32&lt;/code&gt;和&lt;code&gt;ISIZE&lt;/code&gt;&lt;strong&gt;字段的文件，但可以将头部固定长度部分中的所有其他字段设置为默认值&lt;/strong&gt;(&lt;code&gt;OS&lt;/code&gt;字段为&lt;code&gt;255&lt;/code&gt;，其他字段为&lt;code&gt;0&lt;/code&gt;)。&lt;strong&gt;压缩器必须将所有保留位设置为零&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;符合规范的解压器必须检查&lt;/strong&gt;&lt;code&gt;ID1&lt;/code&gt;、&lt;code&gt;ID2&lt;/code&gt;和&lt;code&gt;CM&lt;/code&gt;，&lt;strong&gt;并在其中任何一个具有不正确的值时提供错误指示&lt;/strong&gt;。&lt;strong&gt;它必须至少检查&lt;/strong&gt;&lt;code&gt;FEXTRA/XLEN&lt;/code&gt;、&lt;code&gt;FNAME&lt;/code&gt;、&lt;code&gt;FCOMMENT&lt;/code&gt;和&lt;code&gt;FHCRC&lt;/code&gt;，&lt;strong&gt;以便在这些可选字段存在时可以跳过它们。它不需要检查头部或尾部的任何其他部分；特别是，解压器可以忽略&lt;/strong&gt;&lt;code&gt;FTEXT&lt;/code&gt;和&lt;code&gt;OS&lt;/code&gt;&lt;strong&gt;并始终生成二进制输出，仍然符合规范。如果任何保留位为非零，则符合规范的解压器必须提供错误指示，因为这样的位可能指示存在新字段，这将导致后续数据被错误解释&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Security Considerations&lt;/h2&gt;
&lt;p&gt;任何数据压缩方法都涉及减少数据中的冗余。因此，对数据的任何损坏可能会产生严重影响，并且很难纠正。另一方面，未压缩的文本可能仍然可读，尽管存在一些损坏的字节。&lt;/p&gt;
&lt;p&gt;建议使用这种数据格式的系统提供某种验证压缩数据完整性的手段，例如设置和检查CRC-32校验值。&lt;/p&gt;
&lt;h2&gt;Appendix: Jean-Loup Gailly&apos;s gzip utility&lt;/h2&gt;
&lt;p&gt;gzip压缩的最广泛使用的实现，以及这个规范的原始文档，是由[&lt;strong&gt;Jean-Loup Gailly&lt;/strong&gt;]&lt;a href=&quot;mailto:gzip@prep.ai.mit.edu&quot;&gt;gzip@prep.ai.mit.edu&lt;/a&gt;创建的。由于这个实现已成为事实上的标准，我们在这里提到了一些它的更多特性。&lt;strong&gt;再次强调，本节中的内容并不是规范的一部分，实现不必遵循它才能符合规范&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在压缩或解压缩文件时，&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;会保留本地文件系统上的保护、所有权和修改时间属性，因为&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;文件格式本身没有提供表示保护属性的方法。由于文件格式包含修改时间，&lt;/strong&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;strong&gt;解压缩器提供了一个命令行开关，将文件的修改时间分配给解压后的输出，而不是使用压缩输入的本地修改时间&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Appendix: Sample CRC Code&lt;/h2&gt;
&lt;p&gt;以下示例代码表示了&lt;code&gt;CRC(循环冗余校验)&lt;/code&gt;的实际实现。(也请参阅&lt;code&gt;ISO 3309&lt;/code&gt;和&lt;code&gt;ITU-T V.42&lt;/code&gt;进行正式规范。）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

// CRC table for fast computation
uint32_t crc_table[256];
int crc_table_computed = 0;

/**
 * Make the table for a fast CRC.
 */
void make_crc_table(void) {
    uint32_t c;
    int n, k;
    
    for (n = 0; n &amp;lt; 256; n++) {
        c = (uint32_t)n;
        for (k = 0; k &amp;lt; 8; k++) {
            if (c &amp;amp; 1) {
                c = 0xedb88320L ^ (c &amp;gt;&amp;gt; 1);
            } else {
                c = c &amp;gt;&amp;gt; 1;
            }
        }
        crc_table[n] = c;
    }
    crc_table_computed = 1;
}

/**
 * Update a running crc with the bytes buf[0..len-1] and return the updated crc. 
 * The crc should be initialized to zero. Pre- and post-conditioning (one’s complement) is performed 
 * within this function so it shouldn’t be done by the caller. Usage example:
 */
uint32_t update_crc(uint32_t crc, unsigned char* buf, int len) {
    uint32_t c = crc ^ 0xffffffffL;
    int n;
    
    if (!crc_table_computed) 
        make_crc_table();
    for (n = 0; n &amp;lt; len; n++) {
        c = crc_table[(c ^ buf[n]) &amp;amp; 0xff] ^ (c &amp;gt;&amp;gt; 8);
    }
    return c ^ 0xffffffffL;
}

/**
 * Return the CRC of the bytes buf[0..len-1].
 */
uint32_t crc(unsigned char* buf, int len) {
    return update_crc(0L, buf, len);
}

int main() {
    // Example usage:
    unsigned char buffer[] = &quot;Hello, world!&quot;;
    int length = sizeof(buffer) - 1;    // Exclude the null terminator
    uint32_t original_crc = 0x12345678; // Original CRC, for comparison

    // Calculate CRC
    uint32_t crc_value = crc(buffer, length);

    // Verify CRC
    if (crc_value != original_crc)
        printf(&quot;Error: CRC does not match!\n&quot;);
    else
        printf(&quot;CRC matches!\n&quot;);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>DDCA: The Chapter 7 Reading</title><link>https://chenmiaoi.github.io/2024/04/20/DDCA-The-Chapter-7-Reading/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/04/20/DDCA-The-Chapter-7-Reading/</guid><pubDate>Sat, 20 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在本篇笔记中，我们为&lt;code&gt;MIPS&lt;/code&gt;处理器架构开发了三种微体系结构：单周期、多周期和流水线。&lt;/p&gt;
&lt;h2&gt;Single-Cycle Processor&lt;/h2&gt;
&lt;p&gt;我们首先介绍一个在单周期内执行指令的&lt;code&gt;MIPS&lt;/code&gt;微体系结构。我们将下图中的状态元素与可执行各种指令的组合逻辑连接，开始构建数据通路。&lt;strong&gt;控制信号决定了数据通路在任意给定时刻执行何种特定指令；控制器包含组合逻辑，根据当前指令产生合适的控制信号&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201429220.png&quot; alt=&quot;State element of MIPS processor&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Single-Cycle Datapath&lt;/h3&gt;
&lt;p&gt;本节逐步地开发一个单周期数据通路，每次在上图的基础上在状态元素间添加一个小片段。新添加的以黑色(或蓝色)的标识以强调，而已经研究过的硬件部分以灰色部分展示。&lt;/p&gt;
&lt;p&gt;程序计数器寄存器(&lt;code&gt;PC&lt;/code&gt;)包含了将要执行指令的地址。第一步是从指令存储器中读出指令，下图简单地展示了&lt;code&gt;PC&lt;/code&gt;连接指令存储器的地址输入。指令存储器读出(或&lt;code&gt;抓取(fetch)&lt;/code&gt;)一条$32-bits$的指令，标记为&lt;code&gt;Instr&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201443500.png&quot; alt=&quot;fetch instruction from memory&quot; /&gt;&lt;/p&gt;
&lt;p&gt;处理器的行为取决于被获取的特殊指令。首先，我们将计算出&lt;code&gt;lw&lt;/code&gt;指令的数据通路；然后，我们就需要考虑如何泛化该数据通路到其余指令。&lt;/p&gt;
&lt;p&gt;对于一条&lt;code&gt;lw&lt;/code&gt;指令，下一步就是读取包含基址的源寄存器。这个寄存器在指令$Instr_{25:21}$处的&lt;code&gt;rs&lt;/code&gt;字段中指定。指令的这些位连接到其中一个&lt;code&gt;regfile&lt;/code&gt;模块中读取端口&lt;code&gt;A1&lt;/code&gt;的地址输入，如下图所示。$regfile$将寄存器的值读取到&lt;code&gt;rd1&lt;/code&gt;上&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201449462.png&quot; alt=&quot;regfile&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lw&lt;/code&gt;指令也要求一个&lt;code&gt;offset&lt;/code&gt;。这个偏移被存储在指令$Instr_{15:0}$的&lt;code&gt;immediate&lt;/code&gt;字段。因为$16-bits$的立即数可能是正数或负数，因此我们必须把其符号扩展到$32-bits$，如上图所示。$32-bits$符号扩展数被称为$SignImm$。&lt;/p&gt;
&lt;p&gt;处理器必须将基址和偏移地址相加才能得到真正从内存中读出的数据的地址。下图介绍了&lt;code&gt;ALU&lt;/code&gt;来执行这一操作。$ALU$接收两个操作数，$SrcA$和$SrcB$；$SrcA$从&lt;code&gt;refile&lt;/code&gt;中得到，而$SrcB$就是符号扩展的立即数。$3-bits \ ALUControl$信号代指了各种操作，而$ALU$会生成一个$32bits$的$ALUResult$和一个$Zero$标志。对于&lt;code&gt;lw&lt;/code&gt;指令来说，$ALUControl$信号应该被设置为&lt;code&gt;010&lt;/code&gt;来相加基址和偏移地址。$ALUResult$被发送到数据内存中，作为加载指令的地址，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201522987.png&quot; alt=&quot;compute memory address&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从数据存储器中读出的数据会被送往&lt;code&gt;ReadData&lt;/code&gt;总线中，然后在一个周期的最后回写到&lt;code&gt;regfile&lt;/code&gt;中的目的寄存器中，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201526440.png&quot; alt=&quot;write data back to register file&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lw&lt;/code&gt;指令的目的寄存器被特指为&lt;code&gt;rd&lt;/code&gt;字段，$Instr_{20:16}$被连接到&lt;code&gt;regfile&lt;/code&gt;的$A3$上。&lt;code&gt;ReadData&lt;/code&gt;总线被连接到&lt;code&gt;regfile&lt;/code&gt;上的$WD3$上。被称为&lt;code&gt;RegWrite&lt;/code&gt;的控制信号被连接到$WE3$上，同时其也是&lt;code&gt;lw&lt;/code&gt;指令是否导通的标志，使得数据能够被写入&lt;code&gt;regfile&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当指令被执行后，处理器必须计算下一条指令的地址$PC&apos;$。因为指令是$32bits = 4 bytes$的，因此下一条指令是$PC + 4$。下图中使用了另一个加法器来使得$PC$增加。新的地址在下一个上升沿上被写入到程序计数器中。至此，&lt;code&gt;lw&lt;/code&gt;的数据通路就完成了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201535839.png&quot; alt=&quot;determine address of next&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下一步，让我们来扩展这个数据通路以处理&lt;code&gt;sw&lt;/code&gt;指令。就像&lt;code&gt;lw&lt;/code&gt;一样，&lt;code&gt;sw&lt;/code&gt;指令读一个基址从&lt;code&gt;regfile&lt;/code&gt;和符号扩展的立即数中。 &lt;code&gt;sw&lt;/code&gt;指令也能读取第二个寄存器然后将其写入数据存储器中，如下图所示。该寄存器被称为&lt;code&gt;rt&lt;/code&gt;，$Instr_{20:16}$，其被连接到&lt;code&gt;regfile&lt;/code&gt;中的&lt;code&gt;A2&lt;/code&gt;上。寄存器值被读取到&lt;code&gt;RD2&lt;/code&gt;端口。然后连接到数据存储器上的&lt;code&gt;WD&lt;/code&gt;接口上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404201957750.png&quot; alt=&quot;write data to memory&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于数据存储器上，则多出一个&lt;code&gt;MemWrite&lt;/code&gt;信号控制端口，用来判断是否应该写入，而&lt;code&gt;ALUControl = 010&lt;/code&gt;则是执行基址与偏移地址相加的操作。现在，考虑扩展数据通路来处理&lt;code&gt;R-type&lt;/code&gt;指令&lt;code&gt;add&lt;/code&gt;、&lt;code&gt;sub&lt;/code&gt;、&lt;code&gt;and&lt;/code&gt;、&lt;code&gt;or&lt;/code&gt;和&lt;code&gt;slt&lt;/code&gt;。这些指令都是从&lt;code&gt;regfile&lt;/code&gt;中读取两个寄存器，对其进行相应的操作后写回第三个寄存器。因此，这些指令都可以用相同的硬件、不同的&lt;code&gt;ALUControl&lt;/code&gt;信号来处理。&lt;/p&gt;
&lt;p&gt;下图展示了扩展后的数据通路：&lt;code&gt;regfile&lt;/code&gt;读取两个寄存器，$ALU$执行不同的操作来进行处理。值得注意的是，之前的$SrcB$只接收一个$SignImm$作为输入，而现在应该需要使用多路选择器在$SignImm$和$RD2$中做出选择。同时，对于&lt;code&gt;lw&lt;/code&gt;和&lt;code&gt;sw&lt;/code&gt;而言，总是要经过数据存储器，但是对于&lt;code&gt;R-type&lt;/code&gt;来说，在&lt;code&gt;ALU&lt;/code&gt;得到的结果应该直接写回到&lt;code&gt;regfile&lt;/code&gt;中。因此，我们在$ReadData$和$ALUResult$之间添加了一个多路选择器，这个多路选择器被一个新信号$MemtoReg$控制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202005350.png&quot; alt=&quot;enhanced datapath&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在之前，&lt;code&gt;lw&lt;/code&gt;和&lt;code&gt;sw&lt;/code&gt;指令最终会写到&lt;code&gt;rt&lt;/code&gt;，也就是$Instr_{20:16}$字段上；然而，对于&lt;code&gt;R-type&lt;/code&gt;指令来说，应该写回&lt;code&gt;rd&lt;/code&gt;，即$Instr_{15:11}$字段上。因此，我们添加第三个多路选择器来选择最终需要写回的寄存器是哪一个。该选择器被$RegDst$信号所控制。&lt;/p&gt;
&lt;p&gt;最终，我们需要扩展数据通路来处理&lt;code&gt;beq&lt;/code&gt;指令。&lt;code&gt;beq&lt;/code&gt;比较两个寄存器是否相等，然后决定是否跳转。对于&lt;code&gt;beq&lt;/code&gt;来说，跳转地址仍旧需要进行符号扩展，而新的地址应该是$PC&apos; = PC + 4 + SignImm \times 4$。下图展示了数据通路的修改。我们新添一个$Branch$信号与$ALU$配合进行判断是否发生跳转，添加$PCBranch$来计算跳转的地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202016781.png&quot; alt=&quot;datapath for beq&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此，我们就完成了单周期&lt;code&gt;MIPS&lt;/code&gt;处理器的数据通路，下面就应该考虑如何计算控制信号来指导我们的数据通路的运行。&lt;/p&gt;
&lt;h3&gt;Single-Cycle Control&lt;/h3&gt;
&lt;p&gt;控制单元根据&lt;code&gt;opcode&lt;/code&gt;和&lt;code&gt;funct&lt;/code&gt;，即$Instr_{31:26}$和$Instr_{5:0}$来计算控制信号。下图展示了整个单周期&lt;code&gt;MIPS&lt;/code&gt;处理器的控制单元的连接情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202022145.png&quot; alt=&quot;complete processor&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;MIPS&lt;/code&gt;指令集，大多数控制信息来自于&lt;code&gt;opcode&lt;/code&gt;字段，但是&lt;code&gt;R-type&lt;/code&gt;需要协同使用&lt;code&gt;funct&lt;/code&gt;字段来决定&lt;code&gt;ALU&lt;/code&gt;的执行类型。因此，我们通过将控制单元分解成两个逻辑组合块来简化涉及，如下图所示，主译码器计算来自&lt;code&gt;opcode&lt;/code&gt;的大部分输出，还决定了一个$2bit$的&lt;code&gt;ALUOP&lt;/code&gt;信号。&lt;code&gt;ALU&lt;/code&gt;解码器根据&lt;code&gt;ALUOP&lt;/code&gt;信号结合&lt;code&gt;funct&lt;/code&gt;字段来计算&lt;code&gt;ALUControl&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202028296.png&quot; alt=&quot;control unit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下表是&lt;code&gt;ALU&lt;/code&gt;解码器的真值表以及主解码器的真值表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202031791.png&quot; alt=&quot;truth table&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Performance Analysis&lt;/h3&gt;
&lt;p&gt;单周期处理器中每条指令都需要一个时钟周期，因此$CPI = 1$。&lt;code&gt;lw&lt;/code&gt;指令的关键路径如下图所示，如蓝色虚线所示。其从&lt;code&gt;PC&lt;/code&gt;开始，在时钟上升沿加载一个新地址，指令存储器读取下一条指令，&lt;code&gt;regfile&lt;/code&gt;读取$SrcA$，并同时对立即数进行符号扩展，然后在多路选择器上进行选择，以确定$SrcB$。$ALU$计算这两个值以得到实际的有效地址，然后从数据存储器中的对应地址读出，$MemtoReg$多路复用器选择$ReadData$然后在下个周期上升沿之前写回&lt;code&gt;regfile&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;$$
T_c = t_{pcq_PC} + t_{mem} + max[t_{RFread}, t_{sext} + t_{mux}] + t_{ALU} + t_{mem} + t_{mux} + t_{RFsetup}
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202034921.png&quot; alt=&quot;critical path for lw&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在大多数实现中，&lt;code&gt;ALU&lt;/code&gt;、存储器和&lt;code&gt;regfile&lt;/code&gt;的访问速度明显慢于其余操作。因此，周期时间简化为：&lt;/p&gt;
&lt;p&gt;$$
T_c = t_{pcq_PC} + 2t_{mem} + t_{RFread} + t_{ALU} + t_{RFsetup}
$$&lt;/p&gt;
&lt;h2&gt;Multi-Cycle Processor&lt;/h2&gt;
&lt;p&gt;现在，我们以最开始的存储器和&lt;code&gt;regfile&lt;/code&gt;开始。在单周期设计中，我们使用了单独的指令、数据存储器来读写指令数据。现在，我们选择同时使用指令和数据结合的存储器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202053459.png&quot; alt=&quot;state element&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PC&lt;/code&gt;包含了执行指令的地址。第一步是从指令存储器中读取指令，下图展示了&lt;code&gt;PC&lt;/code&gt;与指令存储器之间的连接。被读取的指令会存储在一个新的&lt;code&gt;Instruction Register&lt;/code&gt;中，以便于其可以被未来的周期使用。这个指令寄存器接收一个使能信号$IRWrite$，当需要更新指令时，该信号导通。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202057254.png&quot; alt=&quot;fetch inst&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;lw&lt;/code&gt;指令来说，下一步就是去读取包含基址的源寄存器&lt;code&gt;rs&lt;/code&gt;，即$Instr+{25:21}$。然后该寄存器与&lt;code&gt;regfile&lt;/code&gt;中的&lt;code&gt;A1&lt;/code&gt;相连，读出对应数据后，通过&lt;code&gt;RD1&lt;/code&gt;输出到一个寄存器&lt;code&gt;A&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202103024.png&quot; alt=&quot;read source&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然，对于立即数，即$Instr_{15:0}$而言，仍旧需要进行符号扩展。因为立即数是指令的一个字段，在处理当前指令时指令自身不会发生变化，因此就不再需要额外的寄存器来保存立即数的值。&lt;/p&gt;
&lt;p&gt;需要加载的地址由&lt;code&gt;ALU&lt;/code&gt;计算得出，最终的结果会保存在名为&lt;code&gt;ALUOut&lt;/code&gt;的寄存器中，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202106374.png&quot; alt=&quot;Add base&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下一步就是根据计算的地址从存储器中取出数据。我们在存储器前添加一个多路选择器&lt;code&gt;Adr&lt;/code&gt;来选择存储地址，多路选择器的选择信号被称为&lt;code&gt;IorD&lt;/code&gt;，表明是指令地址还是数据地址。从存储器中读出的数据被存储在另一个寄存器&lt;code&gt;Data&lt;/code&gt;中。注意，&lt;strong&gt;复用器允许我们在&lt;/strong&gt;&lt;code&gt;lw&lt;/code&gt;&lt;strong&gt;指令期间能够重复使用内存：第一步，从$PC$中获取地址；然后地址取自&lt;/strong&gt;&lt;code&gt;ALUOut&lt;/code&gt;&lt;strong&gt;以加载数据&lt;/strong&gt;。因此，&lt;code&gt;IorD&lt;/code&gt;在不同步骤上就必然有不同的取值，在后续会介绍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202108650.png&quot; alt=&quot;load data&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终，数据被写回&lt;code&gt;regfile&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202114951.png&quot; alt=&quot;write data back&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当这些操作都发生后，处理器必须更新程序计数器。在单周期处理器中，单独的加法器足以完成。但在多周期处理器中，我们可以在上述步骤空闲时使用&lt;code&gt;ALU&lt;/code&gt;。因此，我们必须在&lt;code&gt;ALU&lt;/code&gt;的两个输入源处插入多路选择器，以使得$SrcA = PC, SrcB = 4$。而&lt;code&gt;PCWrite&lt;/code&gt;信号使得&lt;code&gt;PC&lt;/code&gt;寄存器仅在某些周期上被写入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202118050.png&quot; alt=&quot;increment PC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样，我们就完成了&lt;code&gt;lw&lt;/code&gt;指令的数据通路。现在，扩展数据通路以处理&lt;code&gt;sw&lt;/code&gt;指令。对于&lt;code&gt;sw&lt;/code&gt;来说，在电路中的变化就只是从&lt;code&gt;regfile&lt;/code&gt;读出时存入寄存器，然后从寄存器中写入内存即可。然后在存储器上新增一个控制信号&lt;code&gt;MemWrite&lt;/code&gt;，表示允许数据写入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202122377.png&quot; alt=&quot;datapath for sw&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;R-type&lt;/code&gt;而言，两个源寄存器的读取和上述一致，对于寄存器$B$的数据需要在$SrcB$时通过$ALUSrcB_{1:0}$进行选择，在回写阶段时，&lt;code&gt;ALUOut&lt;/code&gt;的值应该被&lt;code&gt;MemtoReg&lt;/code&gt;信号选择是&lt;code&gt;R-type&lt;/code&gt;还是&lt;code&gt;lw&lt;/code&gt;的数据，也要通过$RegDst$信号判断目的寄存器是&lt;code&gt;rd&lt;/code&gt;还是&lt;code&gt;rt&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202130883.png&quot; alt=&quot;for R-type&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;beq&lt;/code&gt;指令，在单周期时我们需要增加一个加法器和多路选择来进行计算；而在多周期，由于可以重复使用一个硬件资源，因此，我们重复利用&lt;code&gt;ALU&lt;/code&gt;即可得到跳转地址。在其中一个步骤中，&lt;code&gt;ALU&lt;/code&gt;会计算&lt;code&gt;PC + 4&lt;/code&gt;并将结果写回&lt;code&gt;PC&lt;/code&gt;寄存器中；因此，我们只需要在立即数到$SrcB$之间，对立即数进行左移操作，然后在将变化后的&lt;code&gt;PC&lt;/code&gt;值与变化后的立即数值相加，写回&lt;code&gt;PC&lt;/code&gt;寄存器即可得到跳转地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202136801.png&quot; alt=&quot;complete multi&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就完成了多周期&lt;code&gt;MIPS&lt;/code&gt;处理器数据通路的设计。设计过程与单周期处理器非常相似，即系统地连接状态元素之间的硬件以处理每条指令。主要区别在于指令在多个步骤中执行。非体系结构寄存器被插入以保存每个步骤的结果。通过这种方式，ALU可以被多次重用，节省了额外加法器的成本。同样地，指令和数据可以存储在一个共享内存中。在下一节中，我们将开发一个FSM控制器，以在每个指令的每个步骤中向数据通路传递适当的控制信号序列。&lt;/p&gt;
&lt;h3&gt;Multi-Cycle Control&lt;/h3&gt;
&lt;p&gt;和单周期一样，多周期也使用控制单元根据&lt;code&gt;opcode&lt;/code&gt;和&lt;code&gt;funct&lt;/code&gt;来计算控制信号，如下所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202150698.png&quot; alt=&quot;complete multi-cycle&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是，和单周期简单的控制不同，多周期的主控制器是一个状态机，其在适当的周期或步骤上应用适当的控制信号。控制信号的顺序取决于正在执行的指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202152218.png&quot; alt=&quot;control unit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主控制器产生出多路复用器和寄存器使能信号，其选择信号为：&lt;code&gt;MemtoReg&lt;/code&gt;、&lt;code&gt;RegDst&lt;/code&gt;、&lt;code&gt;IorD&lt;/code&gt;、&lt;code&gt;PCSrc&lt;/code&gt;、&lt;code&gt;ALUSrcB&lt;/code&gt;和&lt;code&gt;ALUSrcA&lt;/code&gt;；使能信号为&lt;code&gt;IRWrite&lt;/code&gt;、&lt;code&gt;MemWrite&lt;/code&gt;、&lt;code&gt;PCWrite&lt;/code&gt;、&lt;code&gt;Branch&lt;/code&gt;和&lt;code&gt;RegWrite&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了保障下面的状态转移图具有可队形，我们只列出相关的控制信号。选择信号只在其意义重大的地方列出，否则无需关心；而使能信号只有导通时才会被列出，否则为&lt;code&gt;0&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;任何指令的第一步都是从&lt;code&gt;PC&lt;/code&gt;上对应的存储器上的地址中获取指令开始的。状态机在复位时进入该状态，为了读取内存，$IorD = 0$，因此地址取自&lt;code&gt;PC&lt;/code&gt;。&lt;code&gt;IRWrite&lt;/code&gt;被导通是是将指令写入指令寄存器&lt;code&gt;IR&lt;/code&gt;中。同时，&lt;code&gt;PC&lt;/code&gt;应自增以指向下一条指令。因为当前状态&lt;code&gt;ALU&lt;/code&gt;并未被使用，因此处理器在取值的同时可以计算&lt;code&gt;PC + 4&lt;/code&gt;。&lt;code&gt;ALUSrcA = 0&lt;/code&gt;因此&lt;code&gt;SrcA&lt;/code&gt;来自于&lt;code&gt;PC&lt;/code&gt;；$ALUSrcB = 01$因此$SrcB = 4$；$ALUOp = 00$所以$ALU$解码器产生$ALUControl = 010$，使&lt;code&gt;ALU&lt;/code&gt;进行加法运算；为了使得&lt;code&gt;PC&lt;/code&gt;值更新，因此$PCSrc = 0, PCWrite = 1$。该状态如下图所示，蓝色虚线表示指令取值，&lt;code&gt;PC&lt;/code&gt;变化用灰色虚线表示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202205240.png&quot; alt=&quot;data flow during fetch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下一步使读取&lt;code&gt;regfile&lt;/code&gt;和解码指令。&lt;code&gt;regfile&lt;/code&gt;总是读取指令的&lt;code&gt;rs&lt;/code&gt;和&lt;code&gt;rt&lt;/code&gt;字段指定的两个源寄存器。同时，立即数被符号扩展。解码涉及到检查指令的&lt;code&gt;opcode&lt;/code&gt;来决定下一步做什么。解码指令不需要控制信号，但状态机必须等待一个周期以完成读取和解码，如下图所示，新状态以蓝色突出显示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202209814.png&quot; alt=&quot;data flow during decode&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在状态机根据&lt;code&gt;opcode&lt;/code&gt;进入几种可能的状态之一：&lt;/p&gt;
&lt;p&gt;如果指令是&lt;code&gt;lw&lt;/code&gt;或&lt;code&gt;sw&lt;/code&gt;，多周期处理器通过将基址和符号扩展的立即数相加，因此就需要$ALUSrcA = 1$来选择寄存器&lt;code&gt;A&lt;/code&gt;；$ALUSrcB = 10$来选择&lt;code&gt;SignImm&lt;/code&gt;；&lt;code&gt;ALUOp = 00&lt;/code&gt;使得&lt;code&gt;ALU&lt;/code&gt;相加。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202213649.png&quot; alt=&quot;data flow during memory&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果指令是&lt;code&gt;lw&lt;/code&gt;，多周期处理的下一步必须是从存储器中读取数据并写到&lt;code&gt;regfile&lt;/code&gt;中。为了读取内存，$IorD = 1$来选择刚刚计算并保存在&lt;code&gt;ALUOut&lt;/code&gt;中的内存地址。在步骤&lt;code&gt;S3&lt;/code&gt;中，该地址在内存中被读取，并保存在&lt;code&gt;Data&lt;/code&gt;寄存器中。在下一步&lt;code&gt;S4&lt;/code&gt;中，&lt;code&gt;Data&lt;/code&gt;被写入&lt;code&gt;refile&lt;/code&gt;：$MemtoReg = 1$来选择&lt;code&gt;Data&lt;/code&gt;，然后$RegDst = 0$来选择&lt;code&gt;rt&lt;/code&gt;字段作为目的寄存器。&lt;code&gt;RegWrite&lt;/code&gt;被导通使得允许写入，此时完成&lt;code&gt;lw&lt;/code&gt;指令。最终，状态机返回初始状态&lt;code&gt;S0&lt;/code&gt;来获取下一条指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202231451.png&quot; alt=&quot;data flow during write back&quot; /&gt;&lt;/p&gt;
&lt;p&gt;返回状态&lt;code&gt;S2&lt;/code&gt;，如果指令是&lt;code&gt;sw&lt;/code&gt;，那么从&lt;code&gt;regfile&lt;/code&gt;中的&lt;code&gt;RD2&lt;/code&gt;中读出数据然后写入内存。在&lt;code&gt;S3&lt;/code&gt;中，$IorD = 1$来选择在&lt;code&gt;S2&lt;/code&gt;中计算并保存在&lt;code&gt;ALUOut&lt;/code&gt;中的内存地址。&lt;code&gt;MemWrite&lt;/code&gt;被导通用以写入内存。然后，状态机回到&lt;code&gt;S0&lt;/code&gt;获取下一条指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202237344.png&quot; alt=&quot;data flow during write&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;opcode&lt;/code&gt;表示是&lt;code&gt;R-type&lt;/code&gt;指令，多周期处理器必须通过&lt;code&gt;ALU&lt;/code&gt;计算结果并且将其写回到&lt;code&gt;regfile&lt;/code&gt;。在&lt;code&gt;S6&lt;/code&gt;中，通过选择&lt;code&gt;A&lt;/code&gt;和&lt;code&gt;B&lt;/code&gt;寄存器($ALUSrcA = 1, ALUSrcB = 00$)执行指令中&lt;code&gt;funct&lt;/code&gt;字段指示的&lt;code&gt;ALU&lt;/code&gt;操作。对于所有的&lt;code&gt;R-type&lt;/code&gt;指令，$ALUOp = 10$。&lt;code&gt;ALUResult&lt;/code&gt;存储在&lt;code&gt;ALUOut&lt;/code&gt;中。在&lt;code&gt;S7&lt;/code&gt;中，&lt;code&gt;ALUOut&lt;/code&gt;被写入&lt;code&gt;regfile&lt;/code&gt;，$RegRst = 1$，因为此时的目的寄存器为&lt;code&gt;rd&lt;/code&gt;。$MemtoReg = 0$，因为写入数据&lt;code&gt;WD3&lt;/code&gt;来自于&lt;code&gt;ALUOut&lt;/code&gt;，&lt;code&gt;RegWrite&lt;/code&gt;被激活以写入&lt;code&gt;regfile&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202244633.png&quot; alt=&quot;r-type&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;beq&lt;/code&gt;指令，处理器必须计算目标地址并比较两个源寄存器，以确定是否应该执行分支。这需要使用&lt;code&gt;ALU&lt;/code&gt;两次，因此可能看起来需要两个新状态。&lt;strong&gt;然而，请注意，在读取寄存器时&lt;/strong&gt;&lt;code&gt;S1&lt;/code&gt;&lt;strong&gt;期间并未使用&lt;/strong&gt;&lt;code&gt;ALU&lt;/code&gt;。处理器可以在那个时候使用&lt;code&gt;ALU&lt;/code&gt;来计算目标地址，方法是将增加的&lt;code&gt;PC&apos;&lt;/code&gt;，即$PC + 4$，与$SignImm \times 4$相加。$ALUSrcA = 0$用于选择增加的&lt;code&gt;PC&apos;&lt;/code&gt;，$ALUSrcB = 11$用于选择$SignImm \times 4$，$ALUOp = 00$用于加法。目标地址存储在&lt;code&gt;ALUOut&lt;/code&gt;中。如果指令不是&lt;code&gt;beq&lt;/code&gt;，计算的地址将不会在后续周期中使用，但其计算是无害的。在&lt;code&gt;S8&lt;/code&gt;中，处理器通过对两个寄存器进行减法操作并检查结果是否为&lt;code&gt;0&lt;/code&gt;来比较它们。如果是，处理器将分支到刚刚计算的地址。$ALUSrcA = 1$用于选择寄存器&lt;code&gt;A&lt;/code&gt;；$ALUSrcB = 00$用于选择寄存器&lt;code&gt;B&lt;/code&gt;；$ALUOp = 01$用于减法；$PCSrc = 1$用于从&lt;code&gt;ALUOut&lt;/code&gt;中取目标地址，并且$Branch = 1$表示如果&lt;code&gt;ALU&lt;/code&gt;结果为&lt;code&gt;0&lt;/code&gt;，则更新&lt;code&gt;PC&lt;/code&gt;为此地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404202249653.png&quot; alt=&quot;branch state&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Performance Analysis&lt;/h3&gt;
&lt;p&gt;指令的执行时间取决于它使用的周期数和周期时间。而单周期处理器在一个周期内执行所有指令，多周期处理器则对各种指令使用不同数量的周期。然而，多周期处理器在单个周期内执行的工作量较少，因此具有较短的周期时间。多周期处理器需要对&lt;code&gt;beq&lt;/code&gt;和&lt;code&gt;j&lt;/code&gt;指令使用三个周期，对&lt;code&gt;sw&lt;/code&gt;、&lt;code&gt;addi&lt;/code&gt;和&lt;code&gt;R-type&lt;/code&gt;指令使用四个周期，对&lt;code&gt;lw&lt;/code&gt;指令使用五个周期。&lt;code&gt;CPI&lt;/code&gt;取决于每个指令被使用的相对概率。&lt;/p&gt;
&lt;p&gt;回顾我们设计的多周期处理器，每个周期都涉及一个ALU操作、存储器访问或寄存器文件访问。假设寄存器文件比存储器更快，写入存储器比读取存储器更快。检查数据通路揭示了两条可能限制周期时间的关键路径：&lt;/p&gt;
&lt;p&gt;$$
T_c = t_{pcq} + t_{mux} + max[t_{ALU} + t_{mux}, t_{mem}] + t_{setup}
$$&lt;/p&gt;
&lt;h2&gt;Pipelined Processor&lt;/h2&gt;
&lt;p&gt;流水线技术是提高数字系统吞吐量的强大方式。我们通过将单周期处理器细分为五个流水线阶段来设计流水线处理器，因此，五条指令可以同时执行，每个阶段执行一条指令。由于每个阶段只有整个逻辑的五分之一，时钟频率几乎快了五倍。因此，每条指令的延迟在理想情况下不变，但吞吐量理想情况下提高了五倍。微处理器每秒执行数百万到数十亿条指令，因此，吞吐量比延迟更重要。流水线技术引入了一些开销，因此吞吐量可能不会像我们理想地希望的那样高，但流水线技术为极少的成本提供了巨大优势，因此所有现代高性能微处理器都采用了流水线技术。&lt;/p&gt;
&lt;p&gt;读取和写入内存和&lt;code&gt;regfile&lt;/code&gt;，以及使用&lt;code&gt;ALU&lt;/code&gt;通常构成处理器中的最大延迟。我们选择了五个流水线阶段，以便每个阶段都包含其中一个较慢的步骤。具体来说，我们称这五个阶段为&lt;code&gt;取指(Fetch)&lt;/code&gt;、&lt;code&gt;解码(Decode)&lt;/code&gt;、&lt;code&gt;执行(Execute)&lt;/code&gt;、&lt;code&gt;存储器(Memory)&lt;/code&gt;和&lt;code&gt;写回(Writeback)&lt;/code&gt;。它们类似于多周期处理器用于执行&lt;code&gt;lw&lt;/code&gt;的五个步骤。在取指阶段，处理器从指令存储器中读取指令。在解码阶段，处理器从寄存器文件中读取源操作数，并解码指令以产生控制信号。在执行阶段，处理器使用&lt;code&gt;ALU&lt;/code&gt;进行计算。在存储器阶段，处理器读取或写入数据存储器。最后，在写回阶段，处理器在适用时将结果写入&lt;code&gt;regfile&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下图给出了单周期和流水线处理器的时序比图。时间在横轴上，指令在纵轴上。该图假定了逻辑单元延迟，忽略了多路选择器和寄存器的延迟。在图(a)中，第一条指令在&lt;code&gt;0&lt;/code&gt;时刻从存储器中读出；接下来从&lt;code&gt;regfile&lt;/code&gt;中读取操作数；然后&lt;code&gt;ALU&lt;/code&gt;执行必要的计算；再者，访问数据存储器，最终在$950ps$时刻时回写到&lt;code&gt;regfile&lt;/code&gt;。于是该单周期的延迟为$950ps$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404210836280.png&quot; alt=&quot;time diagrams&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图(b)展示了流水线处理器，其中最慢的一个阶段被设置为$250ps$，即取值或访存阶段中的读写内存操作。在&lt;code&gt;0&lt;/code&gt;时刻时，第一条指令从存储器中被读出；在$250ps$时刻，第一条指令进入译码阶段，第二条指令开始被读取；在$500ps$，第一条指令被执行，第二条指令进入译码阶段，第三条指令开始被加载；以此类推，直到所有指令被完成。此时的指令延迟为$5 \times 250 = 1250ps$，吞吐量为$250ps/inst$。&lt;strong&gt;由于各个阶段的逻辑量不完全平衡，流水线处理器的延迟略长于单周期处理器。同样地，对于五级流水线处理器，吞吐量并不完全是单周期处理器的五倍。然而，吞吐量的优势仍然是相当大的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图展示了一个运动中的流水线抽象视角，其中每个阶段都以图像的方式表示。每个流水线阶段都用其主要组成部分表示，指令存储器(&lt;code&gt;IM&lt;/code&gt;)、&lt;code&gt;regfile&lt;/code&gt;读(&lt;code&gt;RF&lt;/code&gt;)、&lt;code&gt;ALU&lt;/code&gt;执行、数据存储器(&lt;code&gt;DM&lt;/code&gt;)和&lt;code&gt;regfile&lt;/code&gt;写(&lt;code&gt;RW&lt;/code&gt;)。读取一行展示了一条指令在每一个阶段上的时钟周期。读取一列展示了多个流水线阶段在同一个时钟周期上执行。每个阶段用阴影部分表示正在被使用。在流水线处理器中，&lt;code&gt;regfile&lt;/code&gt;在一个周期的第一部分写入，第二个部分读取，通过这种方式，可以在单个周期内写入和读回数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404210851673.png&quot; alt=&quot;abstract view of pipeline&quot; /&gt;&lt;/p&gt;
&lt;p&gt;流水线系统中的一个核心挑战是处理当一个指令的结果在前一条指令完成之前就被后续指令所需要时发生的&lt;code&gt;冲突(hazards)&lt;/code&gt;。例如，如果上图中的加法指令使用的是&lt;code&gt;$s2&lt;/code&gt;而不是&lt;code&gt;$t2&lt;/code&gt;，那么会发生冲突，因为在加法指令读取&lt;code&gt;$s2&lt;/code&gt;之前，&lt;code&gt;lw&lt;/code&gt;指令尚未将数据写入&lt;code&gt;$s2&lt;/code&gt;寄存器。本节探讨了&lt;code&gt;转发(forwarding)&lt;/code&gt;、&lt;code&gt;停顿(stalls)&lt;/code&gt;和&lt;code&gt;清空(flushes)&lt;/code&gt;作为解决冲突的方法。最后，本节重新考虑了性能分析，考虑了序列化开销和冲突的影响。&lt;/p&gt;
&lt;h3&gt;Pipelined Datapath&lt;/h3&gt;
&lt;p&gt;流水线的数据通路是将单周期数据通路切割成由流水线寄存器间隔的五个阶段形成的。图(a)展示了单周期数据通路的延伸，其为流水线寄存器留下了足够的空间。图(b)展示了通过插入四个流水线寄存器形成的流水线数据通路，将数据通路分为五个阶段。每个阶段及其边界用蓝色表示。信号用一个后缀(&lt;code&gt;F&lt;/code&gt;、&lt;code&gt;D&lt;/code&gt;、&lt;code&gt;E&lt;/code&gt;、&lt;code&gt;M&lt;/code&gt;和&lt;code&gt;W&lt;/code&gt;)来表示它们所处的阶段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404210910816.png&quot; alt=&quot;five-stage pipeline data path&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;regfile&lt;/code&gt;是比较特殊的，因为它在译码阶段被读，回写阶段被写入。它是在译码阶段被绘制的，但写地址和数据来自于写回阶段，这种反馈将会导致流水线冲突。流水线处理器中的&lt;code&gt;regfile&lt;/code&gt;在&lt;code&gt;CLK&lt;/code&gt;的下降沿写入，此时&lt;code&gt;WD3&lt;/code&gt;是稳定的。&lt;/p&gt;
&lt;p&gt;流水线中一个微妙但关键的问题在于&lt;strong&gt;特定指令相关的所有信号必须一致地通过流水线推进&lt;/strong&gt;，但上图中的&lt;code&gt;regfile&lt;/code&gt;写逻辑中，即写回阶段操作，数据值来自于写回阶段的&lt;code&gt;ResultW&lt;/code&gt;信号，但是地址却来自于执行阶段的&lt;code&gt;WriteRegE&lt;/code&gt;信号。比如下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lw $s2, 40($0)
add $s3, $t1, $t2
sub $s4, $s1, $5 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当第一条指令&lt;code&gt;lw $s2, 40($0)&lt;/code&gt;处于第五个周期时，第三条指令恰好将&lt;code&gt;$s4&lt;/code&gt;译码为目的寄存器，因此，当第五个周期将第一条指令的&lt;code&gt;ResultW&lt;/code&gt;信号发出后，写入的实际上是&lt;code&gt;$s4&lt;/code&gt;而非第一条指令的&lt;code&gt;$s2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下图展示了正确的数据通路，&lt;code&gt;WriteReg&lt;/code&gt;信号现在在&lt;code&gt;Memory&lt;/code&gt;阶段和&lt;code&gt;Write Back&lt;/code&gt;阶段被流水线化，因此它与指令的其余部分保持同步。&lt;code&gt;WriteRegW&lt;/code&gt;和&lt;code&gt;ResultW&lt;/code&gt;信号被一起反馈到&lt;code&gt;Write Back&lt;/code&gt;阶段的&lt;code&gt;regfile&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221612944.png&quot; alt=&quot;corrected pipeline datapath&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一些敏锐的读者可能发现，&lt;code&gt;PC&lt;/code&gt;的逻辑实际上也是有一定问题的，因为它可能会用&lt;code&gt;Fetch&lt;/code&gt;或&lt;code&gt;Memory&lt;/code&gt;阶段的信号(&lt;code&gt;PCPlus4F&lt;/code&gt;或&lt;code&gt;PCBranchM&lt;/code&gt;)，情况我们在后面再讨论。&lt;/p&gt;
&lt;h3&gt;Pipelined Control&lt;/h3&gt;
&lt;p&gt;流水线处理器与单周期处理器使用着相同的信号，因此也就使用了相同的控制单元。不过，&lt;strong&gt;这些控制信号必须与数据一同流水线化，以便与指令保持同步&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图是带有控制的流水线处理器，&lt;code&gt;RegWrite&lt;/code&gt;在反馈到&lt;code&gt;regfile&lt;/code&gt;之前必须被流水线送入&lt;code&gt;Write Back&lt;/code&gt;阶段，正如同&lt;code&gt;WriteReg&lt;/code&gt;在之前那样。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221618134.png&quot; alt=&quot;pipeline processor with control&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Hazards&lt;/h3&gt;
&lt;p&gt;在一个流水线系统中，多条指令是被同时处理的。&lt;strong&gt;当一条指令依赖于另一个尚未完成的结果时，就会产生&lt;/strong&gt;&lt;code&gt;冲突(hazards)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;regfile&lt;/code&gt;能够在同一个周期内进行读写操作，但&lt;strong&gt;写操作发生在一个周期的前半部分，读操作发生在一个周期的后半部分，因此寄存器可以在同一个周期内写入和读回而不引入冲突&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221622059.png&quot; alt=&quot;abstract pipeline diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图图解了一个冲突发生在一条指令写入寄存器&lt;code&gt;$s0&lt;/code&gt;而随后一条指令将要读取寄存器&lt;code&gt;$s0&lt;/code&gt;的时候。这被称为&lt;code&gt;读后写入冲突(read after write, RAW hazard)&lt;/code&gt;。&lt;code&gt;add&lt;/code&gt;指令会在第五个周期的前半个周期将结果写入&lt;code&gt;$s0&lt;/code&gt;，然而，&lt;code&gt;and&lt;/code&gt;指令在第三个周期读取&lt;code&gt;$s0&lt;/code&gt;，因而得到了错误的值。&lt;code&gt;or&lt;/code&gt;指令在第四个周期读取&lt;code&gt;$s0&lt;/code&gt;，同样会得到错误的值。&lt;code&gt;sub&lt;/code&gt;指令在第五个周期的后半个周期读取&lt;code&gt;$s0&lt;/code&gt;，因此得到了在第五个周期的前半个周期写入的正确的值。从这以后，后面的指令都能读取到正确的&lt;code&gt;$s0&lt;/code&gt;的值。上图显示，&lt;strong&gt;当一条指令写入一个寄存器，并且后面的两条指令中的任何一条读到该寄存器时，该流水线就会发生冲突。如果不做任何特殊处理，流水线将会计算出错&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;然而，仔细观察可以得出，&lt;code&gt;add&lt;/code&gt;指令的计算结果总是在第三周期的&lt;code&gt;ALU&lt;/code&gt;计算得出，而直到在第四周期&lt;code&gt;ALU&lt;/code&gt;使用它时，才会在&lt;code&gt;and&lt;/code&gt;指令中严格要求。&lt;strong&gt;原则上，我们能够将结果从一条指令转发给下一条指令来解决&lt;/strong&gt;&lt;code&gt;RAW&lt;/code&gt;&lt;strong&gt;冲突&lt;/strong&gt;。在后续将要讨论的冲突中，可能还需要暂停流水线以为得出结果留出足够的时间。&lt;/p&gt;
&lt;p&gt;冲突通常被分为两类：&lt;code&gt;数据冲突(data hazard)&lt;/code&gt;和&lt;code&gt;信号冲突(control hazard)&lt;/code&gt;。&lt;strong&gt;数据冲突通常发生在一条指令尝试去读取一个上一条指令还未写回的寄存器时；而信号冲突则发生在取值后还未决定下一步取值的指令是什么时&lt;/strong&gt;。在后续的章节，我会介绍使用冲突单元来对流水线处理器进行增强，以检测冲突并进行适当的处理，使处理器正确地执行程序。&lt;/p&gt;
&lt;h4&gt;Solving Data Hazards with Forwarding&lt;/h4&gt;
&lt;p&gt;一些数据冲突能够被&lt;code&gt;转发(forwarding or bypassing)&lt;/code&gt;解决，也就是&lt;strong&gt;通过转发从&lt;code&gt;Memory&lt;/code&gt;或&lt;code&gt;Write Back&lt;/code&gt;阶段转发得到的结果到&lt;code&gt;Execute&lt;/code&gt;阶段所依赖的指令上&lt;/strong&gt;。这就需要在&lt;code&gt;ALU&lt;/code&gt;前添加多路选择器，以便从&lt;code&gt;regfile&lt;/code&gt;、&lt;code&gt;Memory&lt;/code&gt;或&lt;code&gt;Write Back&lt;/code&gt;阶段选择操作数。下图解释了这一原理。在周期四中，&lt;code&gt;$s0&lt;/code&gt;从&lt;code&gt;add&lt;/code&gt;指令的&lt;code&gt;Memory&lt;/code&gt;阶段转发到需要依赖结果的&lt;code&gt;and&lt;/code&gt;的&lt;code&gt;Execute&lt;/code&gt;阶段。而周期五中，&lt;code&gt;$s0&lt;/code&gt;从&lt;code&gt;add&lt;/code&gt;指令的&lt;code&gt;Write Back&lt;/code&gt;阶段转发到需要依赖结果的&lt;code&gt;or&lt;/code&gt;的&lt;code&gt;Execute&lt;/code&gt;阶段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221645749.png&quot; alt=&quot;abstract pipeline diagram forwarding&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当执行阶段的一条指令有一个源寄存器与上一条指令在&lt;code&gt;Memory&lt;/code&gt;或&lt;code&gt;Write Back&lt;/code&gt;阶段的目的寄存器匹配时，转发就是必要的&lt;/strong&gt;。下图修改了流水线处理器以支持转发，其新增了一个&lt;code&gt;冲突预测单元(hazard detection unit)&lt;/code&gt;和两个转发多路选择器。冲突预测单元接收两个处于&lt;code&gt;Execute&lt;/code&gt;阶段的源寄存器和处于&lt;code&gt;Memory&lt;/code&gt;或&lt;code&gt;Write Back&lt;/code&gt;阶段的目的寄存器，还接收来自&lt;code&gt;Memory&lt;/code&gt;和&lt;code&gt;Write Back&lt;/code&gt;阶段的&lt;code&gt;RegWrite&lt;/code&gt;信号以便于了解目的寄存器是否会被实际写入(因为类似&lt;code&gt;sw&lt;/code&gt;和&lt;code&gt;beq&lt;/code&gt;指令不将结果写入&lt;code&gt;regfile&lt;/code&gt;中，因此不需要转发)。值得注意的是，&lt;code&gt;RegWrite&lt;/code&gt;信号在图中是通过名字连接的，而非直接使用长导线来横贯图示以使得图示混乱。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221651697.png&quot; alt=&quot;pipeline processor with forwarding&quot; /&gt;&lt;/p&gt;
&lt;p&gt;冲突预测单元计算控制信号，用于转发多路复用器从&lt;code&gt;regfile&lt;/code&gt;中选择操作数，或从&lt;code&gt;Memory&lt;/code&gt;或&lt;code&gt;Write Back&lt;/code&gt;阶段的结果中选择操作数。如果一个阶段将要写入一个目的寄存器，且该目的寄存器和源寄存器匹配，则应该从该阶段向前转发。但是，在&lt;code&gt;MIPS&lt;/code&gt;中的&lt;code&gt;$0&lt;/code&gt;寄存器无需转发，因为其恒为&lt;code&gt;0&lt;/code&gt;值。如果&lt;code&gt;Memory&lt;/code&gt;和&lt;code&gt;Write Back&lt;/code&gt;阶段都包含匹配的目的寄存器，那么&lt;code&gt;Memory&lt;/code&gt;阶段应该具有优先性的，因为它包含了最近执行的指令。因此，对于$SrcA$给出如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如果rs1寄存器不为$0，rs1与Memory阶段的目的寄存器匹配，且写使能导通
if ((rsE != 0) and (rsE == WriteRegM) and RegWriteM) then 
    ForwardAE = 10      // 直接从ALU计算结果处开始转发
// 如果rs1寄存器不为$0，rs1与Write Back阶段的目的寄存器匹配，且写使能导通
else if ((rsE != 0) and (rsE == WriteRegW) and RegWriteW) then
    ForwardAE = 01      // 从写回阶段转发
// 没有发生数据冲突
else 
    ForwardAE = 00      // rs1可以直接进行计算
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里也给出对应$SrcB$的代码，和$SrcA$相同，只不过判断的寄存器从&lt;code&gt;rs&lt;/code&gt;变为&lt;code&gt;rt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if ((rtE != 0) and (rtE == WriteRegM) and RegWriteM) then
    ForwardBE = 10      
else if ((rtE != 0) and (rtE == WriteRegW) and RegWriteW) then
    ForwardBE = 01      
else 
    ForwardBE = 00      
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Solving Data Hazards with Stalls&lt;/h4&gt;
&lt;p&gt;当结果在指令的&lt;code&gt;Execute&lt;/code&gt;阶段被计算时，转发就足以处理&lt;code&gt;RAW&lt;/code&gt;数据冲突，因为其结果可以被转发给下一条指令的&lt;code&gt;Execute&lt;/code&gt;阶段。但是，对于&lt;code&gt;lw&lt;/code&gt;指令而言，其直到&lt;code&gt;Memory&lt;/code&gt;阶段结束才完成读取数据，因此其结果不能转发到下一条指令的&lt;code&gt;Execute&lt;/code&gt;阶段。因此，我们称&lt;code&gt;lw&lt;/code&gt;有两个周期的延迟，因为一个依赖指令不能使用它的结果，直到两个周期后才被允许。下图显示了这个问题，&lt;code&gt;lw&lt;/code&gt;指令在周期四结束时从&lt;code&gt;Memory&lt;/code&gt;中读取数据，但&lt;code&gt;and&lt;/code&gt;指令需要在第四个周期开始时将数据作为源操作数进行计算，因此这种情况下，转发就不再有效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221725010.png&quot; alt=&quot;abstract pipeline diagram trouble forwarding&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可供选择的解决方案是&lt;code&gt;暂停(stall)&lt;/code&gt;管道，停止运行直到数据可用为止。下图展示了在&lt;code&gt;Decode&lt;/code&gt;阶段暂停依赖指令&lt;code&gt;and&lt;/code&gt;。&lt;code&gt;and&lt;/code&gt;指令在第三周期进入&lt;code&gt;Decode&lt;/code&gt;阶段，然后暂停直到第四周期结束，后续的指令&lt;code&gt;or&lt;/code&gt;也必须保持在&lt;code&gt;Fetch&lt;/code&gt;阶段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221731679.png&quot; alt=&quot;abstract pipeline diagram stall&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在第五个周期，&lt;code&gt;lw&lt;/code&gt;的的结果可以从&lt;code&gt;lw&lt;/code&gt;的&lt;code&gt;Write Back&lt;/code&gt;阶段转发到&lt;code&gt;and&lt;/code&gt;的&lt;code&gt;Execute&lt;/code&gt;阶段，而此时的&lt;code&gt;or&lt;/code&gt;指令的$s0$并不需要转发，直接从&lt;code&gt;regfile&lt;/code&gt;读取即可，因为此时结果已经写回到&lt;code&gt;regfile&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;注意，&lt;code&gt;Execute&lt;/code&gt;阶段在周期四中并未被使用，同样&lt;code&gt;Memory&lt;/code&gt;在周期五以及&lt;code&gt;Write Back&lt;/code&gt;在周期六也是如此。这样通过流水线传播的未被使用的阶段被称为&lt;code&gt;气泡(bubble)&lt;/code&gt;，它的行为就像一个&lt;code&gt;nop&lt;/code&gt;指令。在&lt;code&gt;Decode&lt;/code&gt;阶段暂停期间，通过将&lt;code&gt;Execute&lt;/code&gt;阶段的控制信号置零来引入&lt;code&gt;bubble&lt;/code&gt;，使得气泡不执行任何动作，也不改变架构状态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总之，通过停用流水线寄存器来暂停流水线阶段的进行，使得其内容不发生变化。当一个阶段暂停时，之前的所有阶段都会因此停顿，以免后续指令丢失。为了防止伪信息向前传递，必须清空在该阶段之后的流水线。暂停会降低新能，因此只能在必要时使用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图是修改后的流水线处理器，为&lt;code&gt;lw&lt;/code&gt;指令的数据依赖添加暂停操作。冲突预测单元在&lt;code&gt;Execute&lt;/code&gt;阶段对指令进行检查：如果是&lt;code&gt;lw&lt;/code&gt;指令，其目的寄存器&lt;code&gt;rtE&lt;/code&gt;与&lt;code&gt;Decode&lt;/code&gt;阶段的&lt;code&gt;rsD&lt;/code&gt;或&lt;code&gt;rtD&lt;/code&gt;的任意源操作数匹配，则该指令就必须在&lt;code&gt;Decode&lt;/code&gt;阶段暂停，直到源操作数准备好。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221824653.png&quot; alt=&quot;pipeline for stall&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过在&lt;code&gt;Fetch&lt;/code&gt;和&lt;code&gt;Decode&lt;/code&gt;阶段的流水线寄存器中添加使能输入&lt;code&gt;EN&lt;/code&gt;和在&lt;code&gt;Execute&lt;/code&gt;阶段添加同步的复位/清除输入(&lt;code&gt;CLR&lt;/code&gt;)来支持暂停操作。当&lt;code&gt;lw&lt;/code&gt;指令暂停发生时，&lt;code&gt;StallD&lt;/code&gt;和&lt;code&gt;StallF&lt;/code&gt;被导通使得&lt;code&gt;Decode&lt;/code&gt;和&lt;code&gt;Fetch&lt;/code&gt;阶段的流水线寄存器保持其旧值。&lt;code&gt;FlushE&lt;/code&gt;导通，并且清空执行阶段流水线寄存器的值，引入一个&lt;code&gt;bubble&lt;/code&gt;。&lt;code&gt;MemtoReg&lt;/code&gt;信号被导通。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lwstall = ((rsD == rtE) or (rtD = rtE)) and MemtoRegE
StallF = StallD = FlushE = lwstall
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Solving Control Hazards&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;beq&lt;/code&gt;指令存在一个&lt;code&gt;控制冲突(control hazard)&lt;/code&gt;：&lt;strong&gt;流水线处理器不知道下一次取指什么指令，因为下一次取指时还没有做出分支决策&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理控制冲突的一种机制是将流水线暂停，直到分支决策确定。但是，由于决策是在&lt;/strong&gt;&lt;code&gt;Memory&lt;/code&gt;&lt;strong&gt;阶段决定的，因此每个分支，流水线都必须暂停三个周期，这是我们无法接受的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一种更好的方法是预测分支是否会被取走，并根据预测结果开始执行指令。一旦分支决策做出，并且预测其错误，处理器就会丢弃这些指令。尤其是，假设我们预测的分支不会被执行，然后按照顺序执行程序。如果分支本应该执行，则必须通过清空这些流水线寄存器来丢弃分支后的三条指令，这种浪费被称为&lt;/strong&gt;&lt;code&gt;分支预测错误惩罚(branch misprediction penalty)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下图便展示了这一机制，其中从地址&lt;code&gt;20&lt;/code&gt;到地址&lt;code&gt;64&lt;/code&gt;中取一个分支。该分支直到周期四才做出分支决策，此时地址&lt;code&gt;24&lt;/code&gt;、&lt;code&gt;28&lt;/code&gt;、&lt;code&gt;2C&lt;/code&gt;处的&lt;code&gt;and&lt;/code&gt;、&lt;code&gt;or&lt;/code&gt;、&lt;code&gt;sub&lt;/code&gt;指令已经被取出，因此这些指令必须被刷新，然后&lt;code&gt;slt&lt;/code&gt;指令被取出。这在一定程度上相较于暂停做出了改进，但是一旦进行分支决策时的指令数过多，全部冲洗掉就会降低性能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221839922.png&quot; alt=&quot;abstract pipeline diagram flushing when branch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果能够更早地做出决策，则可以减少分支预测错误惩罚。决策只需要比较两个寄存器的值即可。使用专用的比较器比执行减法和零检测要快得多。如果比较器的速度足够快，可用将其移回&lt;code&gt;Decode&lt;/code&gt;阶段，以便从&lt;code&gt;regfile&lt;/code&gt;中读取操作数并进行比较。&lt;/p&gt;
&lt;p&gt;下图展示了在第二个周期进行早期分支预测的流水线运行情况。在第三周期，刷新&lt;code&gt;and&lt;/code&gt;指令并取出&lt;code&gt;slt&lt;/code&gt;指令，现在分支预测错误惩罚被减少到只有一条指令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221846278.png&quot; alt=&quot;ealier branch decision&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图修改了流水线处理器，以便更早地移动分支决策和处理控制冲突。在&lt;code&gt;Decode&lt;/code&gt;阶段增加一个相等比较器，并提前移动了$PCSrc AND$门，这就使得&lt;code&gt;Decode&lt;/code&gt;阶段而非&lt;code&gt;Memory&lt;/code&gt;阶段确定$PCSrc$。&lt;code&gt;PCBranch&lt;/code&gt;加法器必须移入&lt;code&gt;Decode&lt;/code&gt;阶段以便于及时计算目的地址。在&lt;code&gt;Decode&lt;/code&gt;阶段的流水线寄存器中添加与$PCSrcD$相连的&lt;code&gt;CLR&lt;/code&gt;信号，以便在分支决策时能够对读取的指令进行刷新。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221848694.png&quot; alt=&quot;solving branch control&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不幸的是，过早的分支预测硬件引入了新的&lt;code&gt;RAW&lt;/code&gt;数据冲突。具体地说，&lt;strong&gt;如果分支的一个源操作数是通过先前的指令计算得到的，尚未写入&lt;/strong&gt;&lt;code&gt;regfile&lt;/code&gt;，&lt;strong&gt;则该分支将从&lt;/strong&gt;&lt;code&gt;regfile&lt;/code&gt;&lt;strong&gt;中读取到错误的操作数值&lt;/strong&gt;。因此，我们还需要通过转发正确的值来解决数据冲突，或许也可以通过暂停流水线以获得已准备的数据。&lt;/p&gt;
&lt;p&gt;下图给出了具体的流水线处理器的修改。如果一个结果处于&lt;code&gt;Write Back&lt;/code&gt;阶段，它将在周期的前半部分被写入，在后半部分被读出。因此不存在冲突；如果一个&lt;code&gt;ALU&lt;/code&gt;指令的结果处于&lt;code&gt;Memory&lt;/code&gt;阶段，则可用通过新增的两个多路选择器将其转发给相等比较器。如果&lt;code&gt;ALU&lt;/code&gt;指令的结果在&lt;code&gt;Execute&lt;/code&gt;阶段，或者&lt;code&gt;lw&lt;/code&gt;指令的结果在&lt;code&gt;Memory&lt;/code&gt;阶段，则必须在&lt;code&gt;Decode&lt;/code&gt;阶段暂停流水线，直到结果准备就绪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221903204.png&quot; alt=&quot;handing data&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里给出&lt;code&gt;Decode&lt;/code&gt;阶段的转发逻辑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 如果需要比较的源操作数是后续处于Memory阶段的值，就需要转发
ForwardAD = (rsD != 0) and (rsD == WriteRegM) and RegWriteM
ForwardBD = (rtD != 0) and (rtD == WriteRegM) and RegWriteM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面给出分支的暂停分支逻辑。处理器必须在&lt;code&gt;Decode&lt;/code&gt;阶段进行分支预测。如果分支的任何一个源操作数在&lt;code&gt;Execute&lt;/code&gt;阶段依赖于&lt;code&gt;ALU&lt;/code&gt;指令，或者在&lt;code&gt;Memory&lt;/code&gt;阶段依赖于&lt;code&gt;lw&lt;/code&gt;指令，则处理器就应该暂停，直到源操作数准备就绪。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;branchstall = 
    BranchD and RegWriteE and (WriteRegE == rsD or WriteRegE == rtD)
    or
    BranchD and MemtoRegM and (WriteRegM == rsD or WriteRegM == rtD)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就得出了完整的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StallF = StallD = FlushE = lwstall or branchstall
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Summary&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;RAW&lt;/code&gt;&lt;strong&gt;数据冲突发生在一条指令依赖于另一条指令的结果，而后者尚未写入寄存器文件时。如果结果能够很快计算出来，可以通过转发来解决数据冲突；否则，它们需要暂停流水线，直到结果可用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;控制冲突发生在到达下一条指令必须获取之时，决定应该获取哪条指令的决定尚未做出。通过预测应该获取哪条指令并且在后续确认预测错误时清空流水线来解决控制冲突。尽早做出决定可以最大程度地减少错误预测时清空的指令数量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;到目前为止，设计流水线处理器的挑战之一是了解所有指令之间可能存在的所有交互作用，并发现可能存在的所有冲突。下图展示了处理所有冲突的完整流水线处理器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404221913172.png&quot; alt=&quot;whole&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>CPU for MIPS: ORI&apos;s Five-Stage Pipeline</title><link>https://chenmiaoi.github.io/2024/04/18/CPU-for-MIPS-ORI-s-Five-Stage-Pipeline/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/04/18/CPU-for-MIPS-ORI-s-Five-Stage-Pipeline/</guid><pubDate>Thu, 18 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在CPU for MIPS系列的笔记中，我会详细介绍从一个最小的五级流水线开始不断的构建出一个完整的五级流水线(支持$MIPS ReleaseⅠ，从而将$ucos$Ⅱ进行搭载)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;ORI Instruction&lt;/h2&gt;
&lt;p&gt;下图是&lt;code&gt;ORI&lt;/code&gt;指令的类型格式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181017833.png&quot; alt=&quot;ori instruction type&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也就是说，在后续，我们需要通过&lt;code&gt;001_101&lt;/code&gt;这一编码来判断是否是&lt;code&gt;ori&lt;/code&gt;类型；同时，也需要注意的是：&lt;code&gt;ori&lt;/code&gt;是&lt;code&gt;I&lt;/code&gt;型指令，也就是有一个立即数&lt;code&gt;imm&lt;/code&gt;。在&lt;code&gt;MIPS&lt;/code&gt;中，&lt;code&gt;imm&lt;/code&gt;是无符号扩展的，这一点很重要。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; ori的用法
ori rs, rt, imm
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A Simple Five-Stage Pipeline&lt;/h2&gt;
&lt;p&gt;在我之间的文章中，已经详细介绍过流水线(&lt;a href=&quot;https://chenmiaoi.github.io/2024/04/13/DDCA-The-Chapter-3-Reading/#Parallelism&quot;&gt;DDCA The Chapter 3 Reading, Parallelism&lt;/a&gt;)的概念，因此就不再赘述。本节我主要讲述的是，关于&lt;code&gt;ori&lt;/code&gt;的完备的，简单的五级流水线系统。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181036641.png&quot; alt=&quot;origin data graph&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由上图可以看见，五级流水线实际上就是五个阶段过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;取指
&lt;ul&gt;
&lt;li&gt;取指阶段是从指令存储器中取出&lt;code&gt;pc&lt;/code&gt;所对应的指令数据，以供译指(译码)阶段使用，而后&lt;code&gt;pc&lt;/code&gt;自增。&lt;/li&gt;
&lt;li&gt;因此，这里涉及的两个元件是&lt;code&gt;pc&lt;/code&gt;、&lt;code&gt;inst_fetch_inst_decode&lt;/code&gt;和&lt;code&gt;inst_rom&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;译码
&lt;ul&gt;
&lt;li&gt;译码阶段是要将从取指阶段得到的指令翻译成&lt;code&gt;cpu&lt;/code&gt;能够识别的二进制数据。因此，在这一阶段，&lt;strong&gt;我们需要对指令进行解析&lt;/strong&gt;，比如：
&lt;ul&gt;
&lt;li&gt;指令的类型&lt;/li&gt;
&lt;li&gt;指令需要操作的寄存器(通常是读取源寄存器，写入目的寄存器)&lt;/li&gt;
&lt;li&gt;指令需要操作的寄存器中的数据&lt;/li&gt;
&lt;li&gt;指令中是否含有立即数&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;获得如上数据后，将这些数据送往执行阶段让&lt;code&gt;cpu&lt;/code&gt;进行实际的执行&lt;/li&gt;
&lt;li&gt;因此，这里涉及的元件有&lt;code&gt;regfile&lt;/code&gt;，&lt;code&gt;inst_decode&lt;/code&gt;和&lt;code&gt;inst_decode_execute&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行
&lt;ul&gt;
&lt;li&gt;执行阶段是根据从译码阶段得到的各种数据，进行真实的执行，比如：
&lt;ul&gt;
&lt;li&gt;从译码阶段获得的操作码判断操作类型，本次进行的是&lt;code&gt;ori&lt;/code&gt;操作，也就是或操作&lt;/li&gt;
&lt;li&gt;通过从译码阶段得到的源寄存器数据和目的寄存器数据，进行或操作&lt;/li&gt;
&lt;li&gt;然后指定将结果写入目的寄存器中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;下一步便是访存阶段&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;访存
&lt;ul&gt;
&lt;li&gt;对于这一阶段，通常是对内存进行读写操作的指令需要进行的。因此&lt;code&gt;ori&lt;/code&gt;在此阶段并没有操作，将执行阶段的数据向下传递到回写阶段即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回写
&lt;ul&gt;
&lt;li&gt;回写将执行的计算结果保存到目的寄存器中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我们主要实现的就是一个简单的针对&lt;code&gt;ori&lt;/code&gt;指令的五级流水线。&lt;/p&gt;
&lt;h2&gt;具体实现&lt;/h2&gt;
&lt;p&gt;上面我们已经介绍了五级流水线的基本模型，现在，就应该具体来实现每一个部分了。&lt;/p&gt;
&lt;h3&gt;取指阶段&lt;/h3&gt;
&lt;p&gt;取指阶段要求我们根据&lt;code&gt;pc&lt;/code&gt;从&lt;code&gt;inst_rom&lt;/code&gt;中取出对应的指令数据，并传递给译码阶段。因此，这一阶段我们需要实现三个模块：&lt;code&gt;pc&lt;/code&gt;、&lt;code&gt;inst_rom&lt;/code&gt;和&lt;code&gt;inst_fetch&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;PC模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pc&lt;/code&gt;的作用是给出当前指令地址，并准备下一条指令的执行。其接口对应的参数为:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pc_addr&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;要读取的指令地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inst_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;指令存储器使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module pc(
    input   wire            rst,        // reset signal
    input   wire            clk,        // clock signal
    output  reg             inst_en,    // instruction memory enable signal
    output  reg     [31:0]  pc_addr     // need to get the address of pc 
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;pc&lt;/code&gt;模块中，我们只需要做两个极其简单的判断(在时钟上升沿时)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果复位信号是开启的，那么就需要重置；反之，则给指令存储器使能信号&lt;/li&gt;
&lt;li&gt;如果指令存储器使能信号没有开启，就说明&lt;code&gt;pc&lt;/code&gt;无法使用，则给一个初始值；反之，则需要累加&lt;code&gt;4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (rst == `RST_ENABLE) ...
if (inst_en == `CHIP_DISABLE) ...
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：
为什么&lt;code&gt;pc&lt;/code&gt;要累加&lt;code&gt;4&lt;/code&gt;呢？因此在&lt;code&gt;risc&lt;/code&gt;指令集中，一条指令被强制规定为&lt;code&gt;4&lt;/code&gt;字节的，常见的有&lt;code&gt;arm&lt;/code&gt;、&lt;code&gt;mips&lt;/code&gt;和&lt;code&gt;risc-v&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时的电路图为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181121963.png&quot; alt=&quot;pc circuit&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Instruction ROM模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;inst_rom&lt;/code&gt;的作用是存储系统中需要执行的指令，因此，该模块只与&lt;code&gt;pc&lt;/code&gt;暂时相关联。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;i_pc_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;指令存储器使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_pc_addr&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;当前指令的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_pc_inst&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;当前指令地址对应的指令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;这里需要说明的是：
&lt;strong&gt;接口名的命名规范问题：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt;表示输入，&lt;code&gt;o&lt;/code&gt;表示输出&lt;br /&gt;
紧接着的&lt;code&gt;pc&lt;/code&gt;就是从什么地方输入过来的信号，需要输出到哪一个模块去&lt;br /&gt;
最后的是信息&lt;br /&gt;
也就是说，接口名具体意义是(以&lt;code&gt;i_pc_en&lt;/code&gt;为例)：从&lt;code&gt;pc&lt;/code&gt;过来的使能信号输入&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;module rom_memory(
    input   wire                    i_pc_en,        // 从pc输入的使能信号
    input   wire [`INST_ADDR_BUS]   i_pc_addr,      // 从pc输入的指令地址
    output  reg [`INST_BUS]         o_fetch_inst    // 输出到fetch模块的指令
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;inst_rom&lt;/code&gt;中，我们需要做的就是创建一块内存用于保存指令数据，然后根据对应的&lt;code&gt;pc&lt;/code&gt;地址和使能信号取出指令。因此，当前的电路为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181139643.png&quot; alt=&quot;inst_rom&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Fetch模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;inst_fetch&lt;/code&gt;模块的作用是暂时保存取指阶段的数据，并在下一个时钟传递给译码阶段。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_rom_pc&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;通过&lt;code&gt;pc&lt;/code&gt;从&lt;code&gt;inst_rom&lt;/code&gt;模块中获得的指令地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_rom_inst&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;通过&lt;code&gt;pc&lt;/code&gt;从&lt;code&gt;inst_rom&lt;/code&gt;模块中获得的指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_inst_decode_pc&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到译码阶段的指令地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_inst_decode_inst&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到译码阶段的指令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module inst_fetch(
    input   wire                            rst,
    input   wire                            clk,
    input   wire    [`INST_ADDR_BUS]        i_rom_pc,               // the addr of the instruction attached in instruction fetch 
    input   wire    [`INST_BUS]             i_rom_inst,             // the instruction attached in instruction fetch
    output  reg     [`INST_ADDR_BUS]        o_inst_decode_pc,       // the addr of the instruction for instruction decode
    output  reg     [`INST_BUS]             o_inst_decode_inst      // the instruction for instruction decode
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;inst_fetch&lt;/code&gt;模块中，我们不需要做任何处理，这就相当于一个中转站，将&lt;code&gt;pc&lt;/code&gt;和&lt;code&gt;inst&lt;/code&gt;数据发送给译码阶段的模块。&lt;/p&gt;
&lt;p&gt;如下就是整个&lt;code&gt;fetch&lt;/code&gt;模块的电路图:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181207668.png&quot; alt=&quot;fetch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;译码阶段&lt;/h3&gt;
&lt;p&gt;译码阶段需要将取指阶段的数据进行处理，然后输出到执行阶段，因此，译码阶段涉及&lt;code&gt;regfile&lt;/code&gt;、&lt;code&gt;inst_decode&lt;/code&gt;和&lt;code&gt;inst_decode_excute&lt;/code&gt;模块。&lt;/p&gt;
&lt;h4&gt;Regfile模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;regfile&lt;/code&gt;模块主要是用于寄存器表，也就是汇编中我们使用的寄存器，其可以同时支持两个寄存器的读操作和一个寄存器的写操作。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;需要写入的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;需要写入的寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;写使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_r_reg_addr1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;第一个需要读取的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_r_reg_addr2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;第二个需要读取的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_r_reg_en1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;第一个读使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_r_reg_en2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;第二个读使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_r_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;被读取的第一个寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_r_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;被读取的第二个寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module regfile(
    input   wire                        rst,
    input   wire                        clk,
    input   wire    [`REG_ADDR_BUS]     i_w_reg_addr,     // rd&apos;s addr
    input   wire    [`REG_BUS]          i_w_reg_data,     // rd&apos;s data
    input   wire    [`REG_ADDR_BUS]     i_r_reg_addr1,    // rs1&apos;s addr
    input   wire    [`REG_ADDR_BUS]     i_r_reg_addr2,    // rs2&apos;s addr
    input   wire                        i_w_reg_en,       // write rd enable
    input   wire                        i_r_reg_en1,      // read rs1 enable
    input   wire                        i_r_reg_en2,      // read rs2 enable
    output  reg     [`REG_BUS]          o_r_reg_data1,    // read rs1&apos;s data
    output  reg     [`REG_BUS]          o_r_reg_data2     // read rs2&apos;s data
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;regfile&lt;/code&gt;模块中，我们需要考虑两种情况，读和写：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写：
&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;i_w_reg_en&lt;/code&gt;是开启的，且&lt;code&gt;i_w_reg_addr&lt;/code&gt;不为&lt;code&gt;$0&lt;/code&gt;，那么就将对应的&lt;code&gt;i_w_reg_data&lt;/code&gt;写入到寄存器表中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;读：
&lt;ul&gt;
&lt;li&gt;读有四种情况：
&lt;ul&gt;
&lt;li&gt;第一种：如果&lt;code&gt;rs1&lt;/code&gt;(或&lt;code&gt;rs2&lt;/code&gt;)需要读的寄存器为&lt;code&gt;$0&lt;/code&gt;，那么直接赋值为&lt;code&gt;0&lt;/code&gt;进行输出&lt;/li&gt;
&lt;li&gt;第二种：如果&lt;code&gt;rs1&lt;/code&gt;(或&lt;code&gt;rs2&lt;/code&gt;)需要读的寄存器刚好是写寄存器，那么直接将写寄存器的值赋值进行输出&lt;/li&gt;
&lt;li&gt;第三种：如果&lt;code&gt;rs1&lt;/code&gt;(或&lt;code&gt;rs2&lt;/code&gt;)单纯的读，那么将在寄存器中&lt;code&gt;i_r_reg_addr&lt;/code&gt;对应的值赋值进行输出&lt;/li&gt;
&lt;li&gt;第四种：其他情况均赋值为&lt;code&gt;0&lt;/code&gt;进行输出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于&lt;code&gt;regfile&lt;/code&gt;类似于表模块，因此暂时不给出对应的电路。&lt;/p&gt;
&lt;h4&gt;inst_decode模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;inst_decode&lt;/code&gt;模块的作用是对指令进行译码，得到最终的运算类型、子类型、源操作数1、源操作数2和要写入的目的寄存器地址等信息。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_fetch_pc&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;fetch&lt;/code&gt;模块中得到的&lt;code&gt;pc&lt;/code&gt;地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_fetch_inst&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;fetch&lt;/code&gt;模块中得到的指令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_regfile_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;regfile&lt;/code&gt;模块中查询到的&lt;code&gt;reg1&lt;/code&gt;的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_regfile_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;regfile&lt;/code&gt;模块中查询到的&lt;code&gt;reg2&lt;/code&gt;的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_r_reg_en1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;regfile&lt;/code&gt;的&lt;code&gt;reg1&lt;/code&gt;的读使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_r_reg_en2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;regfile&lt;/code&gt;的&lt;code&gt;reg2&lt;/code&gt;的读使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_r_reg_addr1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;regfile&lt;/code&gt;的&lt;code&gt;reg1&lt;/code&gt;的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_r_reg_addr2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;regfile&lt;/code&gt;的&lt;code&gt;reg2&lt;/code&gt;的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_alu_op&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的指令的子类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_alu_sel&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的指令的类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的&lt;code&gt;reg1&lt;/code&gt;的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的&lt;code&gt;reg2&lt;/code&gt;的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的&lt;code&gt;rd&lt;/code&gt;的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到&lt;code&gt;execute&lt;/code&gt;阶段的&lt;code&gt;rd&lt;/code&gt;的写使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module inst_decode(
    input   wire                        rst,    
    input   wire    [`INST_ADDR_BUS]    i_fetch_pc,             //! the addr of the instruction whith need to be decoded
    input   wire    [`INST_BUS]         i_fetch_inst,           //! the instruction whith need to be decoded

    // read the regfile
    input   wire    [`REG_BUS]          i_regfile_reg_data1,    //! rs1&apos;s data
    input   wire    [`REG_BUS]          i_regfile_reg_data2,    //! rs2&apos;s data

    // output into regfile
    output  reg                         o_regfile_r_reg_en1,    //! the read enable signal for rs1 
    output  reg                         o_regfile_r_reg_en2,    //! the read enable signal for rs2
    output  reg     [`REG_ADDR_BUS]     o_regfile_r_reg_addr1,  //! the addr of rs1 which will be readed
    output  reg     [`REG_ADDR_BUS]     o_regfile_r_reg_addr2,  //! the addr of rs2 which will be readed

    // output into execution inst
    output  reg     [`ALU_OP_BUS]       o_execute_alu_op,       //! bit range operator code: logic、shift、algorithm
    output  reg     [`ALU_SEL_BUS]      o_execute_alu_sel,      //! small range operator code: and logic, or logic...
    output  reg     [`REG_BUS]          o_execute_reg_data1,    //! the rs1 which need to be calc
    output  reg     [`REG_BUS]          o_execute_reg_data2,    //! the rs2 which need to be calc
    output  reg     [`REG_ADDR_BUS]     o_execute_w_reg_addr,   //! the addr which need to be writen when calc done
    output  reg                         o_execute_w_reg_en      //! the write enable signal for rd
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，可以看见，&lt;code&gt;inst_decode&lt;/code&gt;会从&lt;code&gt;fetch&lt;/code&gt;模块中得到的指令和地址进行解析，然后通过访问&lt;code&gt;regfile&lt;/code&gt;查表操作，获得对应寄存器的操作数值作为输出；同时，在内部解析指令每一个字段含义。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 31       26 25       21 20       16 15                   0
// ----------------------------------------------------------
//     ORI    |    rs    |     rd    |          imme
// ----------------------------------------------------------
//   001101   |          |           |
// ----------------------------------------------------------

// 31       26 25       21 20       16 15       11 10       6 5         0
// ----------------------------------------------------------------------
//     op     |     rs    |     rt    |     rd    |     sa   |   func   
// ----------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上所示，第一个是&lt;code&gt;ori&lt;/code&gt;指令的格式，而下面的是常见的&lt;code&gt;R-type&lt;/code&gt;指令格式。因此，如果&lt;code&gt;rst&lt;/code&gt;是关闭的，我们的初始化应该需要根据每个字段进行初始化，我们默认&lt;code&gt;rs&lt;/code&gt;、&lt;code&gt;rt&lt;/code&gt;和&lt;code&gt;rd&lt;/code&gt;是存在的，也就是一般意义的&lt;code&gt;R-type&lt;/code&gt;指令。&lt;/p&gt;
&lt;p&gt;然后我们通过&lt;code&gt;i_fetch_inst[21:26]&lt;/code&gt;来截取最开始的操作码，目前我们只需要判断&lt;code&gt;ori&lt;/code&gt;指令，因此，一旦判断是&lt;code&gt;ori&lt;/code&gt;指令后，写使能开启，指令类型是&lt;code&gt;logic&lt;/code&gt;，子类型是&lt;code&gt;or logic&lt;/code&gt;；又从上面可以知道&lt;code&gt;ori&lt;/code&gt;是&lt;code&gt;I-type&lt;/code&gt;指令，因此我们假设&lt;code&gt;rt&lt;/code&gt;寄存器无效，&lt;code&gt;rs&lt;/code&gt;作为第一个操作数。而&lt;code&gt;imm&lt;/code&gt;直接由&lt;code&gt;i_fetch_inst[15:0]&lt;/code&gt;截取后进行无符号扩展(&lt;strong&gt;注意，这里是大端编码&lt;/strong&gt;)，然后还需要设置&lt;code&gt;rd&lt;/code&gt;寄存器地址，并指出该指令有效。&lt;/p&gt;
&lt;p&gt;初始化完成后，我们就需要去&lt;code&gt;regfile&lt;/code&gt;表中读取&lt;code&gt;rs1(或rs2，如果有效的话)&lt;/code&gt;作为&lt;code&gt;o_execute_reg_data&lt;/code&gt;的数据输出到&lt;code&gt;execute&lt;/code&gt;阶段，如果&lt;code&gt;rs1(或rs2)&lt;/code&gt;是无效的，那么对应的&lt;code&gt;o_execute_reg_data&lt;/code&gt;就应该是&lt;code&gt;imm&lt;/code&gt;的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181703512.png&quot; alt=&quot;inst_decode&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;inst_decode_execute模块&lt;/h4&gt;
&lt;p&gt;这个模块和&lt;code&gt;fetch&lt;/code&gt;模块类似，是作为&lt;code&gt;inst_decode&lt;/code&gt;模块的输出中转站，用以在下一个时钟送往&lt;code&gt;execute&lt;/code&gt;模块的。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_alu_sel&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的运算类型信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_alu_op&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的子运算类型信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的第一个源操作数的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的第二个源操作数的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段获取的目的寄存器写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_alu_sel&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的类型信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_alu_op&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的子类型信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的第一个操作数数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的第二个操作数数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的目的寄存器地址信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_execute_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;将要输出到&lt;code&gt;execute&lt;/code&gt;阶段的目的寄存器写使能信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module inst_decode_execute(
    input   wire                        rst,
    input   wire                        clk,

    input   wire    [`ALU_SEL_BUS]      i_inst_decode_alu_sel,
    input   wire    [`ALU_OP_BUS]       i_inst_decode_alu_op,
    input   wire    [`REG_BUS]          i_inst_decode_reg_data1,
    input   wire    [`REG_BUS]          i_inst_decode_reg_data2,
    input   wire    [`REG_ADDR_BUS]     i_inst_decode_w_reg_addr,
    input   wire                        i_inst_decode_w_reg_en,

    output  reg     [`ALU_SEL_BUS]      o_execute_alu_sel,
    output  reg     [`ALU_OP_BUS]       o_execute_alu_op,
    output  reg     [`REG_BUS]          o_execute_reg_data1,
    output  reg     [`REG_BUS]          o_execute_reg_data2,
    output  reg     [`REG_ADDR_BUS]     o_execute_w_reg_addr,
    output  reg                         o_execute_w_reg_en
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的逻辑极其简单，就是在下一个时钟上升沿时，将输入的输出转交给&lt;code&gt;execute&lt;/code&gt;模块即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404181724602.png&quot; alt=&quot;instruction decode&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;执行阶段&lt;/h3&gt;
&lt;p&gt;我们已经在译码阶段得到了第一个操作数数据、第二个操作数数据、指令类型、指令子类型、目的寄存器地址和目的寄存器的写使能。现在就需要通过获得的数据来进行具体的操作实现了。&lt;/p&gt;
&lt;h4&gt;execute模块&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;execute&lt;/code&gt;模块会根据译码阶段的信息，来实现对应类型的操作，然后得出应该存在目的寄存器中的数据。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输出/输入&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_alu_sel&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的计算子类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_alu_op&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的计算类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_reg_data1&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的第一个源操作数的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_reg_data2&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的第一个源操作数的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_inst_decode_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从译码阶段得到的目的寄存器写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;模块计算后需要写入的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;模块需要写入的目的寄存器的写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;&lt;code&gt;execute&lt;/code&gt;模块需要写入的目的寄存器的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module execute(
    input   wire                    rst,
    input   wire  [`ALU_SEL_BUS]    i_inst_decode_alu_sel,
    input   wire  [`ALU_OP_BUS]     i_inst_decode_alu_op,
    input   wire  [`REG_BUS]        i_inst_decode_reg_data1,
    input   wire  [`REG_BUS]        i_inst_decode_reg_data2,
    input   wire  [`REG_ADDR_BUS]   i_inst_decode_w_reg_addr,
    input   wire                    i_inst_decode_w_reg_en,
    output  reg                     o_w_reg_en,
    output  reg   [`REG_ADDR_BUS]   o_w_reg_addr,
    output  reg   [`REG_BUS]        o_w_reg_data
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;execute&lt;/code&gt;模块中，我们需要根据得到的子类型来计算对应的数据，然后判断&lt;code&gt;i_inst_decode_alu_sel&lt;/code&gt;后通过&lt;code&gt;o_w_reg_data&lt;/code&gt;进行输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case (i_inst_decode_alu_op)
  `EXE_OR_OP: begin
  end
  
case (i_inst_decode_alu_sel)
  `EXE_RES_LOGIC: begin
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182017748.png&quot; alt=&quot;execute&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;execute_memory模块&lt;/h4&gt;
&lt;p&gt;该模块是执行阶段和访存阶段之间的中转站，用于将&lt;code&gt;execute&lt;/code&gt;中的输出信息转交给&lt;code&gt;memory&lt;/code&gt;模块。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;execute&lt;/code&gt;模块得到的目的寄存器的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;execute&lt;/code&gt;模块得到的目的寄存器的写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从&lt;code&gt;execute&lt;/code&gt;中计算得到的目的寄存器的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_memory_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;访存阶段将要写入的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_memory_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;访存阶段的使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_memory_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;访存阶段将要写入的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module execute_memory(
    input   wire                        rst,
    input   wire                        clk,
    input   wire                        i_execute_w_reg_en,
    input   wire    [`REG_ADDR_BUS]     i_execute_w_reg_addr,
    input   wire    [`REG_BUS]          i_execute_w_reg_data,
    output  reg                         o_memory_w_reg_en,
    output  reg     [`REG_ADDR_BUS]     o_memory_w_reg_addr,
    output  reg     [`REG_BUS]          o_memory_w_reg_data
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访存阶段&lt;/h3&gt;
&lt;p&gt;当执行阶段结束后，就应该进入访存阶段了，但是，&lt;code&gt;ori&lt;/code&gt;指令并不涉及访存操作，因此我们不需要做任何事，直接将数据传给回写阶段即可。&lt;/p&gt;
&lt;h4&gt;memory模块&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口号&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从执行阶段得到的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从执行阶段得到的目的寄存器写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_execute_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从执行阶段得到的目的寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_write_back_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到写回阶段的目的寄存器的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_write_back_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到写回阶段的目的寄存器的写使能信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_write_back_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;输出到写回阶段的目的寄存器的值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在&lt;code&gt;ori&lt;/code&gt;中，没有任何操作。&lt;/p&gt;
&lt;h3&gt;回写阶段&lt;/h3&gt;
&lt;p&gt;回写阶段实际上是两部分组成，一个是&lt;code&gt;memory_write_back&lt;/code&gt;模块，一个就是通过该模块回写到&lt;code&gt;regfile&lt;/code&gt;中。&lt;/p&gt;
&lt;h4&gt;memory write back模块&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;接口名&lt;/th&gt;
&lt;th&gt;宽度&lt;/th&gt;
&lt;th&gt;输入/输出&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rst&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;复位信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clk&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;时钟信号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_memory_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从访存阶段得到的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_memory_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从访存阶段得到的目的寄存器写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i_memory_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;i&lt;/td&gt;
&lt;td&gt;从访存阶段得到的目的寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_w_reg_addr&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;回写到&lt;code&gt;regfile&lt;/code&gt;模块的目的寄存器地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_w_reg_en&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;回写到&lt;code&gt;regfile&lt;/code&gt;模块的目的寄存器写使能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_regfile_w_reg_data&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;o&lt;/td&gt;
&lt;td&gt;回写到&lt;code&gt;regfile&lt;/code&gt;模块的目的寄存器数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module memory_write_back(
    input   wire                        rst,
    input   wire                        clk,
    input   wire                        i_memory_w_reg_en,
    input   wire    [`REG_ADDR_BUS]     i_memory_w_reg_addr,
    input   wire    [`REG_BUS]          i_memory_w_reg_data,
    output  reg                         o_regfile_w_reg_en,
    output  reg     [`REG_ADDR_BUS]     o_regfile_w_reg_addr,
    output  reg     [`REG_BUS]          o_regfile_w_reg_data
    );
endmodule;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;顶层模块构建&lt;/h3&gt;
&lt;p&gt;目前，我们已经完成了&lt;code&gt;ori&lt;/code&gt;的五级流水线的五个部分，现在就应该通过顶层模块来布线了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182121621.png&quot; alt=&quot;top-module&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每一个部分分别为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182136329.png&quot; alt=&quot;fetch stage&quot; /&gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182136278.png&quot; alt=&quot;decode stage&quot; /&gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182137914.png&quot; alt=&quot;execute stage&quot; /&gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182137828.png&quot; alt=&quot;memory stage&quot; /&gt;
&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182137278.png&quot; alt=&quot;write back stage&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终我们得到如下电路图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404182134795.png&quot; alt=&quot;top-circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;......世纪长图&lt;/p&gt;
</content:encoded></item><item><title>DDCA: The Chapter 3 Reading</title><link>https://chenmiaoi.github.io/2024/04/13/DDCA-The-Chapter-3-Reading/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/04/13/DDCA-The-Chapter-3-Reading/</guid><pubDate>Sat, 13 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一篇笔记中，我们展示了如何去分析和设计组合逻辑点。&lt;strong&gt;组合逻辑电路的输出只取决于电路的输入值&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;本篇笔记，我们会分析和设计&lt;code&gt;时序逻辑(sequential logic)&lt;/code&gt;。&lt;strong&gt;时序逻辑的输出取决于当前和上一个状态的输入值，因此，时序逻辑具有记忆性&lt;/strong&gt;。时序逻辑可能明确地记录下上一个输入的准确值，或许将先前的输入值提炼成一个更小的信息量，其被称为系统的&lt;code&gt;状态(state)&lt;/code&gt;。&lt;strong&gt;数字时序电路的状态是一组被称为&lt;/strong&gt;&lt;code&gt;状态变量(state variables)&lt;/code&gt;&lt;strong&gt;的比特，其包含了过去的所有信息，这些信息是解释电路未来行为所必须的&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Latches and Flip-Flops&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;内存的最基本的构建块是&lt;/strong&gt;&lt;code&gt;双稳态(bistable)&lt;/code&gt;&lt;strong&gt;元件，一个有两个稳定状态的元件&lt;/strong&gt;。下图(a)表示了一个简单的由一对逆变器连接在一个回路中组成的双稳态，下图(b)表示了相同的重绘的电路以强调对称性。&lt;strong&gt;逆变器之间是&lt;/strong&gt;&lt;code&gt;交叉耦合(cross-coupled)&lt;/code&gt;的，&lt;strong&gt;即$I1$的输入应为$I2$的输出，反之亦然&lt;/strong&gt;。&lt;strong&gt;该电路没有没有输入，但有两个输出&lt;/strong&gt;$Q$和$\overline {Q}$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131905695.png&quot; alt=&quot;Cross-couple inverter pair&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分析该电路不同于分析组合逻辑电路，因为它是循环的：$Q$依赖于$\overline {Q}$，而$\overline {Q}$依赖于$Q$。&lt;/p&gt;
&lt;p&gt;考虑$Q$分别为&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;这两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当$Q = 0$时：
&lt;ul&gt;
&lt;li&gt;如下图(a)所示，$I2$接收一个$FALSE$输入$Q$，因此产生一个$TRUE$的输出$\overline {Q}$。于是$I1$接收一个$TRUE$的输入$\overline {Q}$，因此产生出一个$FALSE$的输出$Q$。&lt;strong&gt;这与原假设的$Q = 0$时一致的，因此称这种情况时稳定的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当$Q = 1$时
&lt;ul&gt;
&lt;li&gt;如下图(b)所示，同理，最终会得到一个为$TRUE$的输出$Q$，因此也是稳定的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;因为交叉耦合的逆变器由两个稳定状态，因此这个电路被称为双稳态&lt;/strong&gt;。不过，有一个微妙的观点是，&lt;strong&gt;电路有第三个可能的状态，两个输出大约在&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;之间，这被称为&lt;/strong&gt;&lt;code&gt;亚稳态(metastable state)&lt;/code&gt;，这将会在后续被讨论。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131916972.png&quot; alt=&quot;Bistable operation of cross-couple&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一个$N$个稳定状态的元件可以传递$\log_2N \ bits$的信息，因此&lt;strong&gt;一个双稳态存储一个比特&lt;/strong&gt;。&lt;strong&gt;交叉耦合逆变器的状态包含在一个二进制状态变量$Q$中，因此，$Q$的值告诉我们过去的一切，这对解释电路的未来行为是必要的&lt;/strong&gt;。这个电路确实有另一个节点$\overline {Q}$，但是$\overline {Q}$不会包含任何额外信息，因为$Q$如果是确认的，那么$\overline {Q}$也一定随之确认。另一方面，$\overline {Q}$也是双稳态的一个可以接受的选项。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当电源首次作用于时序电路时，其初始状态是未知的，通常是不可预测的。并且每次电路导通时的状态也可能不同&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;尽管交叉耦合逆变器能够存储一个信息比特，&lt;strong&gt;但是其并不实用，因为用户没有控制状态的输入&lt;/strong&gt;。而&lt;code&gt;锁存器(latches)&lt;/code&gt;和&lt;code&gt;触发器(flip-flops)&lt;/code&gt;提供了控制状态变量的输入，因为我们考虑这两个双稳态元件。&lt;/p&gt;
&lt;h3&gt;SR Latch&lt;/h3&gt;
&lt;p&gt;最简单的时序电路是&lt;code&gt;SR锁存器(SR Latch)&lt;/code&gt;，&lt;strong&gt;由两个交叉耦合的或非门组成&lt;/strong&gt;。如下图所示，锁存器由两个输入$S$和$R$以及两个输出$Q$和$\overline{Q}$。$SR$锁存器类似于交叉耦合逆变器，但其状态可以通过$S$和$R$输入来控制，$S$和$R$输入对输出$Q$进行&lt;code&gt;设置(set)&lt;/code&gt;和&lt;code&gt;复位(reset)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;理解一个不熟悉的电路的作用就是写出它的真值表。回忆一下，当或非门的任意一个输入为$TRUE$，产生出一个输出$FALSE$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当$R = 1, S = 0$时：
&lt;ul&gt;
&lt;li&gt;$N1$首先看见至少一个输出$R$为$TRUE$，因此其产生一个为$FALSE$的输出$Q$。$N2$发现$Q$和$S$都是$FALSE$，因此产出一个$TRUE$的输出$\overline {Q}$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当$R = 0, S = 1$时：
&lt;ul&gt;
&lt;li&gt;$N1$接受输入&lt;code&gt;0&lt;/code&gt;和$\overline {Q}$。因为我们不再了解$\overline {Q}$，我们不能决定输出$\overline {Q}$。$N2$接收到至少一个为$TRUE$的输入$S$，因此其产生一个为$FALSE$的输出$\overline {Q}$。现在我们重新回顾$N1$，其两个输出都是$FALSE$，因此其输出$Q$为$TRUE$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当$R = 1, S = 1$时：
&lt;ul&gt;
&lt;li&gt;$N1$和$N2$都能至少看见一个为$TRUE$的输入($R$或$S$)，所以其分别产出一个为$FALSE$的输出。因此$Q$和$\overline {Q}$都是$FALSE$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当$R = 0, S = 0$时：
&lt;ul&gt;
&lt;li&gt;$N1$接受输入&lt;code&gt;0&lt;/code&gt;和$\overline {Q}$。因为我们还不知道$\overline {Q}$，所以目前无法决定其输出。$N2$接受输入&lt;code&gt;0&lt;/code&gt;和$Q$，因为我们不知道$Q$，因此也无法决定其输出。现在我们就被困住了，但我们直到$Q$要么是&lt;code&gt;1&lt;/code&gt;，要么是&lt;code&gt;0&lt;/code&gt;。因此我们可以检查每个子例来解决：
&lt;ul&gt;
&lt;li&gt;当$Q = 0$时：
&lt;ul&gt;
&lt;li&gt;因为$S$和$Q$都是$FALSE$的，所以$N2$产生一个为$TRUE$的输出$\overline {Q}$。现在$N1$就接收一个为$TRUE$的输入$\overline {Q}$，因此产出一个为$FALSE$的$Q$。正如我们所假设的那样&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当$Q = 1$时：
&lt;ul&gt;
&lt;li&gt;因为$Q$是$TRUE$，因此$N2$产出一个为$FALSE$的输出$\overline{Q}$。现在$N1$接收两个$FALSE$的输入$R$和$\overline {Q}$，因此产生一个为$TRUE$的输出$Q$。正如我们所假设的那种&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上，在进入第四个情况之前，$Q$已经有了已知的先前值，被称为$Q_{prev}$。$Q_{prev}$是&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;，用于表示系统的状态。当$R$和$S$是&lt;code&gt;0&lt;/code&gt;时，$Q$就会记住旧的值$Q_{prev}$，而$\overline {Q}$就是其补值$\overline {Q_{prev}}$。因此这个电路具有记忆性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131949339.png&quot; alt=&quot;Bistable state of SR latch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图中的真值表总结了上述的四种情况。输入$S$和$R$分别表示$Set$和$Reset$。设置一个比特意味着使它变为$TRUE$，重置一个比特意味着使其为$FALSE$。当$R$导通时，$Q$被重置为&lt;code&gt;0&lt;/code&gt;而$\overline {Q}$与其相反。当$S$被导通时，$Q$被设置为&lt;code&gt;1&lt;/code&gt;而$\overline {Q}$与其相反。当两个输出都没有被导通时，$Q$则会记录旧值$O_{prev}$。同时导通$S$和$R$时没有意义的，因为这就意味着锁存器需要同时设置和充值，这是不可能的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404132030554.png&quot; alt=&quot;SR latch truth table and symbol&quot; /&gt;&lt;/p&gt;
&lt;p&gt;$SR$锁存器以上图中的右图的符号代替。和交叉耦合逆变器一样，$SR$锁存器是一个双稳态元件，一个比特的状态能够存储在$Q$中。&lt;strong&gt;无论过去发生了怎样的设置和重置，预测$SR$锁存器的未来行为所需要的仅仅只是最近一次设置或重置&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;D Latch&lt;/h3&gt;
&lt;p&gt;$SR$锁存器是比较尴尬的，因为其在$S$和$R$都被导通时的行为是奇怪的。此外，&lt;strong&gt;输入$S$和$R$混淆了何时以及何物的问题。置位其中一个输出不仅确定了状态应该是什么，还确定了状态何时应该变化&lt;/strong&gt;。当将这些因素分离开时，电路设计就会变得更加容易。下图显示的&lt;code&gt;D锁存器(D latch)&lt;/code&gt;解决了这个问题。其有两个输入，&lt;code&gt;数据(data)&lt;/code&gt;输入$D$控制下一个状态应该是什么和&lt;code&gt;时钟(clock)&lt;/code&gt;输入$CLK$控制状态何时变化。&lt;/p&gt;
&lt;p&gt;我们再一次分析该锁存器的真值表。为了方便起见，我们首先考虑内部节点$\overline {D}$、$S$和$R$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果$CLK= 0$，$S$和$R$的值都为$FALSE$，则忽视$D$的值&lt;/li&gt;
&lt;li&gt;如果$CLK = 1$，那么一个与门将产生$TRUE$，另一个则是$FALSE$，这取决于$D$的值。&lt;/li&gt;
&lt;li&gt;在$S$、$R$、$Q$和$\overline {Q}$都如同$SR$锁存器一样被确定时：
&lt;ul&gt;
&lt;li&gt;当$CLK = 0$时，$Q$会记录其旧值$Q_{prev}$&lt;/li&gt;
&lt;li&gt;当$CLK = 1$时，$Q = D$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，$D$锁存器避免了同时导通$S$和$R$的特殊情况。将所有内容整合在一起，我们看到时钟控制数据何时流过锁存器。当$CLK = 1$时，锁存器是&lt;code&gt;透明(transparent)&lt;/code&gt;的。$D$处的数据会像锁存器只是一个缓冲器一样流过到$Q$。当$CLK = 0$时，锁存器是不透明的。它阻止新数据流过到$Q$，而$Q$保留旧值。因此，$D$锁存器有时被称为&lt;code&gt;透明锁存器(transparent latch)&lt;/code&gt;或&lt;code&gt;电平敏感锁存器(level-sensitive latch)&lt;/code&gt;。$D$锁存器的符号如图(c)所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404132105532.png&quot; alt=&quot;D latch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$CLK = 1$时，$D$锁存器连续更新其状态。我们将在本章后面看到，仅在特定时间点更新状态是很有用的。下一节描述的$D$触发器就是这样做的。&lt;/p&gt;
&lt;h3&gt;D Flip-Flop&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;D触发器(D Flip-Flop)&lt;/code&gt;可以由两个互补的时钟信号$CLK$控制的$D$锁存器背靠背地组成，如下图所示。其中，第一个锁存器被称为$master$，第二个锁存器被称为$slave$。$D$触发器的符号如图(b)所示，如果输出$\overline{Q}$不是必须的，可以用图(c)表示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404140827764.png&quot; alt=&quot;D Flip-flop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当$CLK = 0$时，$master$锁存器是透明的(数据流通的)，而$slave$锁存器是不透明的(数据不流通的)。因此，&lt;strong&gt;无论此时$D$是什么值都会传输到&lt;/strong&gt;$N1$。当$CLK = 1$时，主锁存器变成不透明时，从锁存器变为透明的。在$N1$的值就可以传输到$Q$，但是$N1$被$D$截断。因此，在时钟从&lt;code&gt;0&lt;/code&gt;跃迁到&lt;code&gt;1&lt;/code&gt;之前，处于$D$的任何立即值都可以在时钟上升后复制到$Q$处。在其余的时刻，$Q$保存旧值，因为无论何时$D$和$Q$的路径之间都有一个不透明的锁存器块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;换句话说，$D$触发器在时钟上升沿时复制$D$到$Q$，并在其他时刻记住它的状态&lt;/strong&gt;。为了简洁起见，时钟的上升沿通常被称为&lt;code&gt;时钟边沿(clock edge)&lt;/code&gt;。&lt;strong&gt;输入$D$指定新状态将是什么，时钟边沿表示何时需要更新状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$D$触发器又被称为&lt;code&gt;主从触发器(master-slave flip-flop)&lt;/code&gt;、&lt;code&gt;边沿触发器(edge-triggered flip-flop)&lt;/code&gt;或&lt;code&gt;正边沿触发器(positive edge-triggered flip-flop)&lt;/code&gt;。符号中的三角形表示边沿触发的时钟输入。在不需要输出$\overline{Q}$的情况下，其经常被忽略。&lt;/p&gt;
&lt;h3&gt;Register&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一个$N-bit$寄存器是由$N$个触发器组成的触发器组，其共享一个公共的$CLK$输入，使得寄存器的所有位同时被更新和保存&lt;/strong&gt;。寄存器是绝大多数时序电路模块构成的关键，下图展示了一个四位寄存器的示意图和符号，输入$D_{3:0}$和输出$Q_{3:0}$都是四位总线的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404140858433.png&quot; alt=&quot;a 4-bit register&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Enabled Flip-Flop&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;使能触发器(enabled flip-flop)&lt;/code&gt;添加另一个被称为$EN$或$ENABLE$的输出，&lt;strong&gt;以确定数据是否在时钟边沿时加载&lt;/strong&gt;。当$EN$为$TRUE$时，使能触发器的行为就如同一个正常的$D$触发器；当$EN$是$FALSE$时，使能触发器忽略时钟输入并保存其原有状态。&lt;strong&gt;使能触发器在我们希望只在某些时刻加载一个新值，而非每一个边沿触发时是极其有用的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404140901786.png&quot; alt=&quot;Enabled Flip-Flop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图显示了两种构建使能触发器的方法。图(a)中的多路复用器选择了输入是否能够传递到$D$；图(b)中的时钟是一个与门，只有同时为真时才会使得触发器更新状态。&lt;strong&gt;注意，当$CLK = 1$时，$EN$不允许被改变，以免触发器看到时钟毛刺&lt;/strong&gt;。因此，&lt;strong&gt;在时钟上执行逻辑是一个不好的行为。时钟门会延迟时钟，并可能导致时序错误&lt;/strong&gt;。使能触发器的符号由图(c)所示。&lt;/p&gt;
&lt;h3&gt;Resettable Flip-Flop&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;可重置触发器(resettable flip-flop)&lt;/code&gt;添加了另一个被称为$RESET$的输入。当$RESET$是$FALSE$时，可重置触发器的行为和普通$D$触发器一样。当$RESET$是$TRUE$时，可重置触发器忽略$D$并重置输出为&lt;code&gt;0&lt;/code&gt;。&lt;strong&gt;可重置触发器在我们第一次打开系统时，想把一个已知的状态强加到系统中的所有触发器时是很有用的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404140918455.png&quot; alt=&quot;Resettable flip-flop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种触发器可以&lt;code&gt;同步(synchronously)&lt;/code&gt;或&lt;code&gt;异步(asynchronously)&lt;/code&gt;复位。同步可重置触发器仅在$CLK$上升沿复位，而异步可重置触发器在$RESET$跃迁至$TRUE$时立即复位，与$CLK$无关。&lt;/p&gt;
&lt;p&gt;上图中图(a)展示了如何从一个普通的$D$触发器和与门构建一个同步可重置触发器。当$\overline {RESET}$是$FALSE$时，与门就会强制输入一个&lt;code&gt;0&lt;/code&gt;作为触发器的输入。当$\overline {RESET}$为$TRUE$时，与门就能传递$D$作为输入。因此，图(a)的$\overline {RESET}$表示其是一个&lt;code&gt;低激活(active low)&lt;/code&gt;信号，而增加一个逆变器后，图(b)和图(c)展示了一个&lt;code&gt;高激活(active high)&lt;/code&gt;信号的可重置触发器符号。&lt;/p&gt;
&lt;h3&gt;Putting It All Together&lt;/h3&gt;
&lt;p&gt;锁存器和触发器是构建时序电路的基本构建块。&lt;strong&gt;请记住，$D$锁存器是电平敏感的，而$D$触发器是边沿触发的&lt;/strong&gt;。当$CLK = 1$时，$D$锁存器时透明的，允许$D$输入的数据流向输出$Q$。$D$触发器在$CLK$的上升沿时复制$D$的数据到$输出Q$。在其他时刻，锁存器和触发器都保持其状态。寄存器是一组多个$D$触发器构建的，其共用一个$CLK$信号。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404140943122.png&quot; alt=&quot;comparison waveforms&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图展示了锁存器和触发器的时序图，假设$Q$响应输入变化的延迟很小。箭头表示输出变化的原因。$Q$的初始值是为止的，可以是&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;，正如上图中的一组平行线所示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先考虑锁存器，在$CLK$的第一个上升沿，$D = 0$，因此$Q$也必然变为&lt;code&gt;0&lt;/code&gt;。每次改变$Q$，$Q$也随之变化。当$D$变化而$CLK = 0$时，忽略不计。&lt;/li&gt;
&lt;li&gt;现在考虑触发器，在$CLK$的每一个上升沿上，$D$被复制到$Q$上，其余时刻，$Q$保持其状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Synchronous Logic Design&lt;/h2&gt;
&lt;p&gt;一般来说，时序电路包含所有的非组合电路，也就是说，&lt;strong&gt;输出不能简单地通过查看当前输入来确定的电路&lt;/strong&gt;。本节会介绍&lt;strong&gt;同步时序电路的概念和动态规约&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Some Problematic Circuits&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;多谐振荡器(astable circuits)&lt;/code&gt;
现在我们有三个被误用的逆变器形成的一个回路。如下图所示，第三个逆变器的输出被反馈到第一个逆变器的输入。每个逆变器的传播延时为$1ns$，请确定一下该电路的作用是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141051188.png&quot; alt=&quot;ring oscillator waveforms&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设$X$节点的初始值是&lt;code&gt;0&lt;/code&gt;，那么$Y = 1, Z = 0$，从而又得出$X = 1$，因此与我们的原假设不符。这个电路没有稳定的状态，因此被称为&lt;code&gt;不稳定的(unstable or astable)&lt;/code&gt;。上图表示了这个电路的行为。&lt;/p&gt;
&lt;p&gt;如果$X$在&lt;code&gt;0&lt;/code&gt;时刻上升，$Y$就会在$1ns$时下降，而$Z$就会在$2ns$时上升，$X$会在$3ns$时下降。下一轮时，$Y$在$4ns$时上升，$Z$在$5ns$时下降...然后持续重复。每个节点在$0 \thicksim 1$之间震荡，周期(也就是重复时间)为$6ns$。这种电路被称为&lt;code&gt;环形振荡器(ring oscillator)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;环形振荡器的周期取决于每个逆变器的传播延时，而延时又取决于逆变器是如何制造的、电源电压甚至温度&lt;/strong&gt;。因此，环形振荡器的周期很难确定。&lt;strong&gt;总之，环形振荡器是一个具有零输入和一个周期变化的输出的时序电路&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;竞争条件(race condiyions)&lt;/code&gt;
这里我们设计了一个新的$D$锁存器，并宣称其比我们之前所介绍的锁存器更好，因为使用了更少的门。如下图所示。
请判断该锁存器是否工作正确？并且每个门是否独立延迟？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141101015.png&quot; alt=&quot;an improved? D latch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图展示了该电路在某些门的速度比其他门慢时，会存在竞争条件使其失败。假设$CLK = D = 1$时，该锁存器是透明的，并且传递$D$使得$Q = 1$。那么现在$CLK$下降，锁存器应该记住旧值$Q_{prev}$使得保持$Q = 1$。然而，假设通过逆变器从$CLK$到$\overline {CLK}$的延迟相对于与门和非门的延迟要长，此时，节点$N1$和$Q$在$CLK$上升之前都可能下降。在这种情况下，$N2$永远都不会上升，从而使得维持$Q = 0$这一状况。&lt;/p&gt;
&lt;p&gt;这是一个异步电路设计的例子，&lt;strong&gt;其中输出直接反馈到输入&lt;/strong&gt;。&lt;strong&gt;异步电路因存在竞争条件而臭名昭著，电路的行为取决于逻辑门中哪条路径是最快的。一个电路可能工作正常，而另一个看似相同但由具有稍有不同延迟的门构建的电路可能不工作。或者该电路可能仅在某些温度或电压下工作，而这些温度或电压下的延迟恰到好处。这些故障极其难以追踪&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Synchronous Sequential Circuits&lt;/h3&gt;
&lt;p&gt;上面的两个例子包含循环路径，其中输出直接反馈到输入。&lt;strong&gt;如果将一个输入应用到组合电路中，则在一个传播延时内，输出总会收敛到正确的值。然而，具有循环路径的时序电路可能存在不期望的竞争或不稳定的行为&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了避免这种情况，&lt;strong&gt;通常使用在路径的某处插入寄存器来打断循环路径&lt;/strong&gt;。这就将电路转化为一个组合逻辑和寄存器的集合。&lt;strong&gt;寄存器包含该系统的状态，并且只在边沿触发时改变，因此我们称其该状态与时钟&lt;/strong&gt;&lt;code&gt;同步(synchronized)&lt;/code&gt;。&lt;strong&gt;如果时钟速度足够慢，使得在下一个时钟边沿到来之前所有的寄存器的输入都已经稳定，那么所有的竞争条件便被消除。在循环路径中使用寄存器的这种规约，我们就得出了同步时序电路的正式定义&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时序电路有一个有限的离散状态集合$S_0, S_1, \cdots, S_{k - 1}$。同步时序电路有一个时钟输入，其上升沿指示状态转变发生的时间序列。我们通常使用输入当前状态和下一个状态来区分系统当前的状态和其将要进入的下一个时钟边沿的状态。时序电路的功能准则详细说明了对于每种可能的当前状态和输入值的组合的下一个状态和每一个输出的值。而时序规范包括时钟上升沿到输出变化的时间上界$t_{pcq}$和下界$t_{ccq}$，以及相对于时钟上升沿时输入必须稳定的设置和保持时间&lt;/strong&gt;$t_{setup}$和$t_{hold}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同步时序电路的构成规则告诉我们，如果一个电路是由相互连接的元件构成的，则称其为同步时序电路&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个电路元件要么是寄存器，要么是组合电路&lt;/li&gt;
&lt;li&gt;至少一个电路元件是寄存器&lt;/li&gt;
&lt;li&gt;所有的寄存器接收相同的时钟信号&lt;/li&gt;
&lt;li&gt;每一个循环路径包含至少一个寄存器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同步的时序电路被称为异步时序电路。&lt;/p&gt;
&lt;p&gt;触发器是一个最简单的同步时序电路。其有一个输入$D$，时钟$CLK$，输出$Q$和两个状态${0, 1}$。触发器的功能规范是下一个状态是$D$，而$Q$是当前状态。另外两个最常见的同步时序电路被称为有限状态机和流水线，这将在后续介绍。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;判断哪一个是同步时序电路&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141507671.png&quot; alt=&quot;example circuits&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图(a)是组合电路而非时序电路&lt;/li&gt;
&lt;li&gt;图(b)是一个无反馈的简单时序电路，但不同步&lt;/li&gt;
&lt;li&gt;图(c)既不是组合电路也不是同步时序电路，因为其有一个既不是组合电路又不是寄存器的锁存器元件&lt;/li&gt;
&lt;li&gt;图(d)、图(e)是同步时序电路，同时也是简单的有限状态机&lt;/li&gt;
&lt;li&gt;图(f)既不是组合电路也不是同步时序电路，因为在回路上没有寄存器&lt;/li&gt;
&lt;li&gt;图(g)是同步时序电路，也是流水线形式&lt;/li&gt;
&lt;li&gt;图(h)不是严格意义上的同步时序电路，因为第二个寄存器接收了一个不同的时钟信号(其延迟由两个逆变器决定)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Synchronous and Asynchronous Circuits&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;异步设计在理论上比同步设计更具有一般性，因为系统的时序不受时钟寄存器的限制&lt;/strong&gt;。就如同模拟电路比数字电路更具一般性。&lt;strong&gt;但是，同步电路已经被证明比异步电路更容易设计和使用，并且尽管对一部电路进行了数十年的研究，但几乎所有的数字系统本质上都是同步的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当然，异步电路在具有不同时钟的系统之间通信或者任意时间接收输入时偶尔是有必要的。&lt;/p&gt;
&lt;h2&gt;Finite State Machine&lt;/h2&gt;
&lt;p&gt;同步时序电路可以以一种叫做&lt;code&gt;有限状态机(finite state machine, FSM)&lt;/code&gt;的形式画出，如下图所示。&lt;strong&gt;其之所以被叫做有限状态机，是因为一个带有$k$个寄存器的电路可以处于有限数量$2^k$个唯一状态中的一个状态&lt;/strong&gt;。一个有限状态机有$M$个输入，$N$个输出以及$k$比特状态。其也接收一个时钟和重置(可选)信号。&lt;strong&gt;一个有限状态机由两个组合逻辑组成，即下一个状态逻辑和输出逻辑，以及一个存储状态的寄存器。在每个时钟边沿上，有限状态机就会进入下一个状态，该状态是由当前状态和输入计算出来的&lt;/strong&gt;。有两种常见的状态机类别，它们根据自身的功能准则来区分。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$Moore Machines$
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输出只取决于机器的当前状态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$Mealy Machines$
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输出取决于机器的当前状态以及当前输入&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在给定功能准则的前提下，有限状态机提供了一种系统化的方法来设计同步时序电路&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141521542.png&quot; alt=&quot;Finite state machines&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;FSM Design Example&lt;/h3&gt;
&lt;p&gt;为了解释有限状态机的设计，考虑在校园繁忙的路口发明一个交通控制灯的问题。&lt;/p&gt;
&lt;p&gt;我们决定使用有限状态机来实现这个问题，我们在十字交叉的两个道路上分别安装两个交通传感器$T_A$和$T_B$。如果检测到学生，那么每个传感器都表示为$TRUE$；反之则为$FALSE$。同时，再安装两个交通灯$L_A$和$L_B$来控制交通。每个灯接收数字输入来指定红黄绿三色。因此，我们的有限状态机有两个输入$T_A$和$T_B$，两个输出$L_A$和$L_B$。同时，我们还提供了一个周期为$5s$的时钟和复位按钮以置于已知的初始状态。如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141543278.png&quot; alt=&quot;fsm machines&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下一步我们就画出如上图所示的状态机草图，表示了系统中所有可能的状态以及这些状态之间的转变。当系统被重置时，$L_A$就应该时绿灯而$L_B$就是红灯。每五秒钟，控制器就会检查交通模式并决定下一步要做什么。一旦$Academic Ave$上有车流，那么这个信号灯就不应该被改变。当不再有车流时，那么就应该在其变黄五秒后变为红灯；而$Bravado Ave$的灯光就变成绿灯。类似的，如果$Bravado$上有车流，那么持续绿灯，如果没有，则就变为黄灯再变为红灯。&lt;/p&gt;
&lt;p&gt;在一个状态转移图中，圆圈表示状态，圆弧表示状态之间的转变。状态转换发生在时钟的上升沿。此外，时钟只简单的控制状态该何时发生，而状态图表示哪些转变发生。标记为$Reset$的圆弧从外部空间指向$S_0$，表示系统在复位时应进入该状态，不论其先前的状态是什么。如果一个状态有多个圆弧离开，需要在每个弧上标记每个转换是由哪个输入触发的。如果一个状态只有一个弧离开，且该弧为标记，就说明无论输入如何，该状态总是会转变到所指向的状态。&lt;/p&gt;
&lt;p&gt;我们将状态转移图重写为状态转移表，表示对于每个状态和输入。下一个状态$S&apos;$应该是什么。注意，&lt;strong&gt;当下一个状态不依赖于某个特定输入时，表中使用$Don&apos;t Cares$符号$X$来表示&lt;/strong&gt;。同时也应该注意，外来符号$Reset$应该省略。&lt;/p&gt;
&lt;p&gt;状态转移图是抽象的，其使用标记为${S_0, S_1, S_2, S_3}$的状态和标记为${red, yellow, green}$的输出。要建立一个实际的电路，&lt;strong&gt;必须对状态和输出进行二进制编码&lt;/strong&gt;。下图是对其编码的展示，每个状态和输出用两个比特编码：$S_{1:0}$，$L_{A1:0}$和$L_{B1:0}$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141600013.png&quot; alt=&quot;state tables&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们使用得出的编码更新状态转移表，如下图所示。修改后的状态转移表就是一个真值表，其规定了下一个状态的逻辑。它将下一个状态$S&apos;$定义为当前状态$S$和输入的函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141607142.png&quot; alt=&quot;state transition table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这个真值表中，以乘积和规范式读出下一个状态的布尔方程是很简单的：&lt;/p&gt;
&lt;p&gt;$$
S_1&apos; = \overline {S_1}S_0 + S_1\overline {S_0}\overline {T_B} + S_1\overline {S_0}T_B
$$&lt;/p&gt;
&lt;p&gt;$$
S_0&apos; = \overline {S_1}\overline {S_0}\overline {T_A} + S_1\overline {S_0}\overline {T_B}
$$&lt;/p&gt;
&lt;p&gt;上述等式可以通过卡诺图简化，但是这里我们直接观察可以看出：\overline {T_B}和$T_B$是可以简化的，并且$S_1&apos;$可以被归结为异或运算。因此：&lt;/p&gt;
&lt;p&gt;$$
S_1&apos; = S_1 \oplus S_2
$$&lt;/p&gt;
&lt;p&gt;$$
S_0&apos; = \overline {S_1}\overline {S_0}\overline {T_A} + S_1\overline {S_0}\overline {T_B}
$$&lt;/p&gt;
&lt;p&gt;和上述一样，我们以相同的方式写出一个输出的真值表，表明对于每个状态，该状态下的输出应该是什么。然后得出其布尔方程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141617387.png&quot; alt=&quot;output table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;$$
L_{A1} = S_1 \qquad L_{A0} = \overline {S_1}S_0
$$&lt;/p&gt;
&lt;p&gt;$$
L_{B1} = \overline {S_1} \qquad L_{B0} = S_1S_0
$$&lt;/p&gt;
&lt;p&gt;最终，我们可以画出一个$Moore$状态机。&lt;/p&gt;
&lt;p&gt;首先，我们先画出一个$2-bit$状态寄存器，如下图(a)所示。在每个时钟边上，状态寄存器复制下一个状态$S_{1:0}&apos;$成为$S_{1:0}$。状态寄存器启动时接收同步或异或重置以初始化状态机。&lt;/p&gt;
&lt;p&gt;然后，我们根据得出的$S_1&apos;$和$S_0&apos;$的布尔方程绘制状态逻辑，该逻辑从当前状态和输入中计算下一个状态，如图(b)所示。&lt;/p&gt;
&lt;p&gt;最后，我们根据得出的输出布尔方程绘制输出逻辑，该逻辑从当前状态计算输出，如图(c)所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141620067.png&quot; alt=&quot;state machine circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图展示了交通灯控制器经过状态序列的时序图。该图显示了$CLK$、$Reset$，输入$T_A$和$T_B$，下一个状态$S&apos;$，当前状态$S$以及输出$L_A$和$L_B$。箭头表示因果关系，虚线表示状态变化时的时钟上升沿。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141633186.png&quot; alt=&quot;timing diagram for traffic&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该时钟具有$5s$一个周期，因此交通灯最多$5s$换一次。当有限状态机首次开启时，其状态是未知的，因此需要对系统进行重置进入已知状态。在该时序图中，$S$会立即重置为$S_0$，表示正在使用异步可复位的触发器。在状态$S_0$中，$L_A$是绿色而$L_B$是红色。&lt;/p&gt;
&lt;h3&gt;State Encodings&lt;/h3&gt;
&lt;p&gt;在上一个例子中，输出和状态的编码是任意选择的。不同的选择会导致不同的电路。&lt;strong&gt;一个很自然的问题是：如何确定产生逻辑门最少或传播延迟最短的电路的编码&lt;/strong&gt;？不幸的是，找到最佳编码没有简单的方法，除了尝试所有可能性，当状态数量很大时，这是不可行的。然而，通常可以通过观察来选择一个好的编码，以便相关的状态或输出共享位。计算机辅助设计($CDA$)工具也擅长搜索可能的编码集并选择一个合理的编码。&lt;/p&gt;
&lt;p&gt;状态编码中的一个重要决策是在二进制编码和独热(复习：独热指的是一次只有一个输出为$TRUE$)编码之间进行选择。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用二进制编码，就像在交通灯控制器示例中使用的那样，每个状态都表示为一个二进制数。因为$K$个二进制数可以用$log_2K$位表示，所以具有$K$个状态的系统只需要$log_2K$位的状态。&lt;/li&gt;
&lt;li&gt;在独热编码中，每个状态都使用一个单独的状态位。它被称为&lt;code&gt;one-hot&lt;/code&gt;，因为任何时候只有一个状态位是&lt;code&gt;hot&lt;/code&gt;或$TRUE$。例如，一个具有三个状态的独热编码的$FSM$将具有状态编码$001$、$010$和$100$。每个状态位都存储在一个触发器中，因此独热编码需要比二进制编码更多的触发器。然而，使用独热编码时，下一个状态和输出逻辑通常更简单，因此需要更少的门。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，最佳编码的选择得根据具体的有限状态机来进行分析。&lt;/p&gt;
&lt;h3&gt;Moore and Mealy Machines&lt;/h3&gt;
&lt;p&gt;至今为止，我们已经展示了$Moore Machine$的例子，&lt;strong&gt;其输出仅依赖于系统的状态&lt;/strong&gt;。因此在$Moore Machine$的状态转移图中，输出被标注在圆圈中。$Mealy Machine$和$Moore Machine$很相似，不过&lt;strong&gt;其输出同时取决于系统状态和输入&lt;/strong&gt;。因此，$Mealy$的状态转移图中的输出被标记在圆弧上，而非圆圈中。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Moore Versus Mealy Machines&lt;br /&gt;
我们拥有一只带有$FSM$大脑的宠物机器蜗牛。这只蜗牛沿着一个包含一系列&lt;code&gt;1&lt;/code&gt;和&lt;code&gt;0&lt;/code&gt;的纸带从左到右爬行。在每个时钟周期，蜗牛爬到下一个位。当蜗牛爬过的最后两个位是从左到右为&lt;code&gt;01&lt;/code&gt;时，蜗牛会微笑。设计$FSM$来计算蜗牛何时应该微笑。输入$A$是蜗牛触角下方的位。当蜗牛微笑时，输出$Y$为$TRUE$。比较$Moore$和$Mealy$状态机设计。为每个机器草拟一个时序图，显示蜗牛沿着序列·0100110111·爬行时的输入、状态和输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$Moore Machine$需要三个状态，如下图中图(a)所示；相比而言，$Mealy Machine$只需要两个状态。如图(b)所示，每个弧段标记为$A/Y$，其中$A$为引起该转变的输入值，$Y$是对应的输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141929121.png&quot; alt=&quot;FSM state transition diagrams&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下表展示了$Moore Machine$的状态转换表和输出表。$Moore$至少需要两个比特的状态。考虑采用二进制状态编码：$S_0 = 00, S_1 = 01, S_2 = 10$，下图表示了使用编码重写后的状态转换表和输出表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141945332.png&quot; alt=&quot;Moore table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从这些表中，我们可以发现下一个状态和输出的布尔方程：&lt;/p&gt;
&lt;p&gt;$$
S_1&apos; = S_0A \qquad S_0&apos; = \overline {A}
$$&lt;/p&gt;
&lt;p&gt;$$
Y = S_1
$$&lt;/p&gt;
&lt;p&gt;下面则表示了$Mealy$的状态转换表和输出表。$Mealy$只需要一个比特的状态。考虑采用二进制编码：$S_0 = 0, S_1 = 1$。下图也以二进制编码重写了输出表和状态转换表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141956361.png&quot; alt=&quot;Mealy table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上述的表中，我们可以得出其下一个状态和输出的布尔方程：&lt;/p&gt;
&lt;p&gt;$$
S_0&apos; = \overline {A}
$$&lt;/p&gt;
&lt;p&gt;$$
Y = S_0A
$$&lt;/p&gt;
&lt;p&gt;$Moore$和$Mealy$的电路图(图(a)是$Moore$，图(b)是$Mealy$)和时序图下图所示。这两台机器遵循不同的状态序列。此外，$Mealy$机的输出会比较早一个周期上升，因为它对输入做出响应，而不是等待状态改变。如果通过一个触发器延迟$Mealy$输出，它将与$Moore$输出匹配。在选择$FSM$设计风格时，请考虑你希望输出何时响应。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404141958147.png&quot; alt=&quot;FSM schematics&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Factoring State Machine&lt;/h3&gt;
&lt;p&gt;如果一个状态机能够被分解成多个相互作用的简单状态机使得一些机器的输出是另一些机器的输入，那么设计复杂的状态机通常是容易的。这种层次化和模块化的应用被称为&lt;code&gt;状态机的分解(factoring of state machines)&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unfactored and factored state machines
修改我们在之前做的交通灯控制器，使其拥有一个游行模式，该模式保持$Bravado$的灯当观众和乐队以分散的团体向足球比赛进军时一直是绿灯。控制器接收两个新的输入：$P$和$R$，如果$P$导通至少一个周期进入巡游模式；而$R$导通至少一个周期结束巡游模式。在游行模式下，控制器按照正常的顺序进行，直到$L_B$变绿，然后保持在该状态下，直到游行模式结束。
首先，我们给出一个有限状态机的草图，如下所示；然后，给出两个相互作用的有限状态机的状态转换图。$Mode$状态机在导通时输出$M$；$Lights$状态机基于$M$和$T_A$、$T_B$来控制灯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404142017950.png&quot; alt=&quot;state controller FSM&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中图(a)的状态机图显示了单个状态机的设计。状态$S_0 \thicksim S_1$处理正常模式，状态$S_4 \thicksim S_7$处理游行模式。途中的这两部分几乎完全相同，但在游行模式下，状态机仍停留在$S_6$，因此$Bravado$上仍亮着绿灯。输入$P$和$R$控制这两部分的运动，状态机的设计较为凌乱繁琐。&lt;/p&gt;
&lt;p&gt;而图(b)显示了两种类型的状态机，$Mode$状态机用于跟踪灯光是否处于正常或游行模式；当$M$为$TRUE$时，$Lights$状态机被修改为保持在$S_2$状态。&lt;/p&gt;
&lt;h3&gt;Deriving an FSM from a Schematic&lt;/h3&gt;
&lt;p&gt;从示意图中导出状态转移图几乎是遵循状态机设计的逆过程。当在逆向别人的工程或查看一个不完善的文档时，这个过程可以是必要的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查电路，说明输入、输出和状态位&lt;/li&gt;
&lt;li&gt;写出下一个状态和输出的布尔等式&lt;/li&gt;
&lt;li&gt;创建出下一个状态和输出表&lt;/li&gt;
&lt;li&gt;缩减下一个状态表，消除不可达状态&lt;/li&gt;
&lt;li&gt;分配每一个有效状态位组合一个名字&lt;/li&gt;
&lt;li&gt;使用分配的状态名重写下一个状态和输出表&lt;/li&gt;
&lt;li&gt;画出状态转移图&lt;/li&gt;
&lt;li&gt;陈述状态机的每一步操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一步，必须要小心简洁的描述状态机的整体目的和功能，不要简单地重述状态转换图的每一个转换。&lt;/p&gt;
&lt;h2&gt;Timing of Sequential Logic&lt;/h2&gt;
&lt;p&gt;我们回忆一下，触发器在时钟上升沿时将输入$D$复制到输出$Q$上，这种过程被称为&lt;code&gt;采样(sampling)&lt;/code&gt;。如果$D$在时钟上升沿时稳定在&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;，则此行为的定义是明确的。那么，如果$D$在上升沿时发生了改变呢？&lt;/p&gt;
&lt;p&gt;这个问题就类似于相机在抓拍一张图片时面临的问题。试想一下，拍摄一只青蛙从荷叶上跳入水中的情况。如果你在跳跃前拍摄，你会看到荷叶上有一只青蛙；在跳跃后拍摄，则会看到水中的涟漪；如果你在青蛙正在跳跃时拍摄，你可能会看到一张模糊的图像。相机的一个特点就是有一个&lt;code&gt;光圈时间(aperture time)&lt;/code&gt;，在这个时间内，物体必须保持静止才能捕获到清晰的图像；&lt;strong&gt;同样，时序元件在时钟边缘附近也有一个光圈时间，在这个时间内，输入必须是稳定的才能使触发器产生良好定义的输出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;时序元件的光圈时间被时钟边缘前的&lt;code&gt;建立时间(setup time)&lt;/code&gt;和其后的&lt;code&gt;保持时间(hold time)&lt;/code&gt;所定义。就如同静态规约限制我们使用禁区之外的逻辑电平一样，&lt;strong&gt;动态规约限制我们使用在光圈时间外的发生变化的信号&lt;/strong&gt;。通过利用动态规约，&lt;strong&gt;我们可以把时间看作是一个离散单位，称为时钟周期&lt;/strong&gt;，就如同我们电平信号看作是离散的&lt;code&gt;1&lt;/code&gt;和&lt;code&gt;0&lt;/code&gt;一样。一个信号可能在一定时间内产生毛刺和大范围的振荡。在动态规约下，&lt;strong&gt;我们只关心在时钟周期结束时的最终值，即在信号到达一个稳定值时&lt;/strong&gt;。因此，我们可以简单的写为$A[n]$，表示信号$A$在第$n$个时钟周期结束时的值，其中$n$是整数；而不是用$A(t)$表示，其表示$A$信号在某一时刻$t$的值，其中$t$是任意实数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时钟周期必须足够长，使得所有的信号都能稳定&lt;/strong&gt;。这就对系统的速度提出了限制，&lt;strong&gt;在实际系统中，时钟并不能精确的同时到达所有触发器&lt;/strong&gt;。这种时间上的变化，称为&lt;code&gt;时钟偏差(clock skew)&lt;/code&gt;，进一步增加了必须要的时钟周期。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有时满足动态规约是不可能的，特别是在与现实世界进行接口时。例如，考虑一个电路，其输入来自按钮。一只猴子可能会在时钟上升时按下按钮。这可能导致一种称为&lt;/strong&gt;&lt;code&gt;亚稳态(metastability)&lt;/code&gt;&lt;strong&gt;的现象，其中触发器捕获了介于0和1之间的值，需要花费无限长的时间才能解析为一个良好的逻辑值。对于这样的异步输入，解决方案是使用一个同步器，它有很小的(但非零)概率产生非法逻辑值&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;The Dynamic Discipline&lt;/h3&gt;
&lt;p&gt;我们已经讨论了时序电路的功能准则，现在让我们来看一看时序电路的时许准则，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151517194.png&quot; alt=&quot;timing specification&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当时钟上升沿时，输出可能在时钟到输出$Q$之间的污染延迟$t_{ccq}$之后开始变化，并且在传播延时$T_{pcq}$内确定到稳定状态的最终值，污染延迟和传播延迟分别代表电路中的最快和最慢延迟。为了使电路正确采样其输入，&lt;strong&gt;输入必须在时钟上升沿之前必须稳定在至少一段建立时间$t_{setup}$和在时钟上升沿之后的一段保持时间$t_{hold}$。建立时间和保持时间之和被称为电路的光圈时间，因为这是输入必须保持稳定的总时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;动态规约指出，&lt;strong&gt;同步时序电路的输入必须在上升沿的光圈时间附近建立和保持一个稳定状态&lt;/strong&gt;。这一要求使得触发器采样信号不会被改变，并且由于我们只关心输入在采样时刻的最终值，因此&lt;strong&gt;我们可以将信号在时间和逻辑上都视为离散的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;System Timing&lt;/h3&gt;
&lt;p&gt;时钟周期或周期时间$T_{c}$为重复时钟信号上升沿之间的时间，其倒数被称为时钟频率$f_c = 1/T_c$。其他条件不变的情况下，&lt;strong&gt;增加时钟频率会增加数字系统每单位时间可以完成的工作量&lt;/strong&gt;，频率以赫兹$Hz$为单位进行测量，即每秒周期数：$1MHz = 10^6Hz$，$1GHz = 10^9Hz$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151535501.png&quot; alt=&quot;Path between registers and timing diagram&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图(a)说明了一个同步时序电路中的一般路径，其时钟周期是我们想要计算的。在上升沿上，寄存器$R1$输出$Q1$，信号进入逻辑组合块中，输出$D2$，作为输入进入$R2$。该时序图由图(b)所示，每个输出信号在其输入发生变化后可能会在污染延迟后开始变化，并在其输入稳定后的传播延迟内稳定最终值。灰色箭头表示通过$R1$和组合逻辑的污染延迟，蓝色表示通过$R1$和组合逻辑的传播延迟。我们针对第二个寄存器$R2$的建立时间和保持时间进行时序约束分析。&lt;/p&gt;
&lt;h4&gt;Setup Time Constraint&lt;/h4&gt;
&lt;p&gt;下图仅表示通过路径的最大延迟，用蓝色箭头表示。为了满足$R2$的建立时间，$D2$必须不晚于下一个时钟周期之前的建立时间，因此，我们找到一个关于最小时钟周期的方程：&lt;/p&gt;
&lt;p&gt;$$
T_c \ge t_{pcq} + t_{pd} + t_{setup}
$$&lt;/p&gt;
&lt;p&gt;触发器时钟到输出$Q$的传播延时$T_{pcq}$和建立时间$t_{setup}$由制造商指定。因此我们将上述的方程重新排列，以逻辑组合求解最大传播延迟，而组合逻辑通常是个体设计者控制下的唯一变量。&lt;/p&gt;
&lt;p&gt;$$
t_{pd} \le T_c - (t_{pcq} + t_{setup})
$$&lt;/p&gt;
&lt;p&gt;上述中括号里面的项$t_{pcq} + t_{setup}$被称为&lt;code&gt;时序开销(sequencing overhead)&lt;/code&gt;。理想情况下，整个周期时间$T_c$将可用于组合逻辑中的有用计算，即$T_{pd}$。然而，&lt;strong&gt;触发器的时序开销会占据这段时间&lt;/strong&gt;。方程$t_{pd} \le T_c - (t_{pcq} + t_{setup})$被称为&lt;code&gt;建立时间约束(setup time constrain)&lt;/code&gt;或&lt;code&gt;最大延迟约束(max-delay constrain)&lt;/code&gt;，&lt;strong&gt;因为它取决于建立时间并限制了组合逻辑中的最大延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151541424.png&quot; alt=&quot;Maximum delay for setup time constraint&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果通过组合逻辑的传播延迟太大，那么$D2$就可能在$R2$需要稳定值采样时还没达到其最终值。因此，$R2$可能会采样到一个错误的结果，甚至是处于禁区的非法的逻辑层次。&lt;strong&gt;该问题可以通过增加时钟周期或重新设计组合逻辑以获得更短的传播时延来解决&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Hold Time Constraint&lt;/h4&gt;
&lt;p&gt;在上面的例子中，$R2$寄存器也有一个&lt;code&gt;保持时间约束(hold time constraint)&lt;/code&gt;。输入$D2$不能被改变，直到时钟上升沿后的$t_{hold}$时间过后。如下图，$D2$可能在时钟上升沿后的$t_{ccq} + t_{cd}$后被改变。因此，&lt;/p&gt;
&lt;p&gt;$$
t_{ccq} + t_{cd} \ge t_{hold}
$$&lt;/p&gt;
&lt;p&gt;当然，$t_{ccq}$和$t_{hold}$同样也是触发器的特定，因此不再设计者的控制范围内。所以，我们需要对该式进行重排来求解最小污染延迟：&lt;/p&gt;
&lt;p&gt;$$
t_{cd} \ge t_{hold} - t_{ccq}
$$&lt;/p&gt;
&lt;p&gt;上式通常被称为保持时间约束或&lt;code&gt;最小延迟约束(min-delay constraint)&lt;/code&gt;，因为其限制了通过组合逻辑的最小延迟。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151604807.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们假设任何逻辑元件之间都可以互相连接，而不引入时序问题。尤其是，我们希望两个触发器直接级联，而不会引起保持时间问题。在这个例子中，$t_{cd} = 0$，因为触发器之间没有逻辑组合，因此：&lt;/p&gt;
&lt;p&gt;$$
t_{hold} \le t_{ccq}
$$&lt;/p&gt;
&lt;p&gt;换句话说，&lt;strong&gt;可靠的触发器必须有一个比污染延迟短的保持延时&lt;/strong&gt;。通常而言，触发器通常的设计为$t_{hold} = 0$，因此上式是始终成立的。除非另有说明，否则在笔记中中我们通常会做出这样的假设，并忽略保持时间约束。&lt;/p&gt;
&lt;p&gt;然而，保持时间约束非常重要。&lt;strong&gt;如果违反了这些约束，唯一的解决方案是增加逻辑中的污染延迟&lt;/strong&gt;，这需要重新设计电路。与建立时间约束不同，它们不能通过调整时钟周期来修复。在当今先进技术中，重新设计集成电路并制造修正设计需要数月时间和数百万美元的投资，因此必须非常严肃地对待保持时间违规行为。&lt;/p&gt;
&lt;h3&gt;Metastability&lt;/h3&gt;
&lt;p&gt;如先前所说，我们不能保证时序电路的输入在光圈时间内始终稳定，特别是当输入来自外部时。考虑一个按钮被连接到触发器的输入的情况，如下图所示。当按钮没有按下时，$D = 0$;反之，$D = 1$。一只猴子在相对于$CLK$上升沿的某个随机时间点按下按钮，我们想知道$CLK$上升沿之后的输出$Q$的值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151638458.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例一，当按钮在$CLK$之前被按下时，输出$Q = 1$&lt;/li&gt;
&lt;li&gt;例二，当按钮在$CLK$之后被按下时，输出$Q = 0$&lt;/li&gt;
&lt;li&gt;例三，当按钮在$CLK$的建立时间$t_{setup}$后，保持时间$t_{hold}$前按下时，输出此时不能确定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Metastable State&lt;/h4&gt;
&lt;p&gt;当触发器采样一个在其光圈时间内变化的输入时，输出$Q$可能会瞬间承受一个介于&lt;code&gt;0&lt;/code&gt;和$V_{DD}$之间的电压，而这个电压处于禁区之内，这被称为&lt;code&gt;亚稳态(metastable)&lt;/code&gt;。最终，触发器就会将输出解析为&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;的稳定状态。然而，&lt;strong&gt;达到稳定状态的解析时间是无界的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如下图所示，触发器的亚稳态类似于两个山谷之间的山峰上的一个球。&lt;strong&gt;这两个山谷是稳定状态，因为球在山谷里不受干扰的情况下会保持在那里。山顶上的平衡点称为亚稳态，因为如果球完美平衡，它会保持在那里。但由于没有什么是完美的，球最终会朝一个方向或另一个方向滚动。这种变化所需的时间取决于球最初的平衡程度。每个双稳态设备在两个稳定状态之间都有一个亚稳态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151650474.png&quot; alt=&quot;metastable&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Resolution Time&lt;/h4&gt;
&lt;p&gt;如果一个触发器的输入在时钟周期内随机变化，那么解析到稳定状态所需的解析时间$t_{res}$也是一个随机变量。如果输出在光圈时间外变化，则$t_{res} = t_{pcq}$。一旦输出在光圈时间内变化，那么$t_{res}$就会大幅度变长。解析时间$t_{res}$超过某个任意时间$t$的概率随着$t$的增加呈指数下降：&lt;/p&gt;
&lt;p&gt;$$
P(t_{res} \lt t) = \frac{T_0}{T_c}e^{-\frac{t}{\tau}}
$$&lt;/p&gt;
&lt;p&gt;其中，$T_c$是时钟周期，$T_0$和$\tau$是触发器的特性。该方程仅对$t$长于$t_{pcq}$的情况有效。&lt;/p&gt;
&lt;p&gt;直观地说，$T_0/T_c$描述了输入在不良时间(即在光圈时间内)发生变化的概率；这个概率随着周期时间$T_c$的增加而减少。$\tau$是一个时间常数，表示触发器从亚稳态移动出去的速度；它与触发器中交叉耦合门的延迟有关。&lt;/p&gt;
&lt;p&gt;总之，&lt;strong&gt;如果一个双稳态设备如触发器的输入在光圈时间内发生变化，输出可能在解析为稳定的&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;或&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;之前持续处于亚稳态值。解析所需的时间是不受限制的，因为对于任何有限时间$t$，触发器仍然处于亚稳态的概率是非零的。然而，随着$t$的增加，这个概率指数级下降。因此，如果我们等待的时间足够长，远远超过$t_{pcq}$，我们可以极高的概率期待触发器达到有效的逻辑电平&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Synchronizers&lt;/h3&gt;
&lt;p&gt;现实中，对数字系统的异步输入是不可避免的。例如人类的输入是异步的，如果这些输入处理不当就会导致系统内的亚稳态电压，从而引起难以跟踪和纠正的不稳定的系统故障。数字系统的设计者的目标应该是确保给定异步输入时，遇到亚稳态的几率足够小。“足够”需要取决于环境，对于手机来说，十年一次故障也许是能接受的；对于一个医疗设备来说，在宇宙的预期寿命中发生一次故障或许是一个更好的目标。&lt;strong&gt;为了保证良好的逻辑水平，所有的异步输入都应该被同步地传入&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如下图所示的&lt;code&gt;同步器(Synchronizers)&lt;/code&gt;，其可以接收一个异步输入$D$和时钟信号$CLK$。&lt;strong&gt;它在有限的时间内产生一个输出$Q$；并且该输出极高概率是一个有效的逻辑电平&lt;/strong&gt;。如果$D$在光圈时间内是稳定的，输出$Q$就应该和$D$的值一样；如果$D$不稳定，$Q$就可能是$HIGH$或$LOW$，但不一定是亚稳态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151731484.png&quot; alt=&quot;simple synchronizer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图也给出了一个简单的方式来构建一个同步器：用两个触发器。$F1$在$CLK$上升沿处采样$D$，如果$D$正在改变，那么输出$D2$就可能暂时处于亚稳态；如果时钟周期足够长，$D2$就有很高的概率在时钟周期结束前收敛到一个有效的逻辑电平。然后$F2$采集稳定状态下的$D2$，产生出一个良好输出$Q$。&lt;/p&gt;
&lt;p&gt;如果同步器的输出$Q$是亚稳态的，那么该同步器就是失效的。这发生在$F2$需要进入建立时间，$D2$还未被解析到一个有效值时，即如果$t_{res} \gt T_c - t_{setup}$时。通过之前我们得到的概率式($P(t_{res} \lt t) = \frac{T_0}{T_c}e^{-\frac{t}{\tau}}$)，单个输入在随机时刻失败的概率为：&lt;/p&gt;
&lt;p&gt;$$
P(failure) = \frac{T_0}{T_c}e^{-\frac{T_c - t_{setup}}{\tau}}
$$&lt;/p&gt;
&lt;p&gt;失败概率$P(failure)$是输出$Q$在单次$D$变化时处于亚稳态的概率。如果$D$每秒变化一次，每秒的失败概率就是$P(failure)$。但是，如果$D$每秒变化$N$次，则每秒发生故障的概率为$N$倍：&lt;/p&gt;
&lt;p&gt;$$
P(failure) / sec = N\frac{T_0}{T_c}e^{-\frac{T_c - t_{setup}}{\tau}}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统的可靠性通常用&lt;/strong&gt;&lt;code&gt;平均故障时间间隔(mean time between failures, MTBF)&lt;/code&gt;&lt;strong&gt;来衡量&lt;/strong&gt;。顾名思义，$MTBF$就是&lt;strong&gt;系统在任意给定秒内发生故障的概率的倒数&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;$$
MTBF = \frac{1}{P(failure) / sec} = \frac{T_ce^{-\frac{T_c - t_{setup}}{\tau}}}{NT_0}
$$&lt;/p&gt;
&lt;p&gt;上面的方程表明，&lt;strong&gt;随着同步器的等待时间$T_c$增加，$MBTF$呈指数增加&lt;/strong&gt;。对于绝大多数系统，&lt;strong&gt;等待一个时钟周期的同步器提供了一个安全的$MTBF$，在高速系统中，可能需要等待更多的周期&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Parallelism&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;系统的速度由信息在其中移动的延迟和吞吐量来表征&lt;/strong&gt;。我们将&lt;code&gt;令牌(token)&lt;/code&gt;定义为一组输入，经过处理后产生一组输出。&lt;strong&gt;系统的延迟是一个令牌从开始到结束通过系统所需的时间，吞吐量是单位时间内可以产生的令牌数量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以想象，通过同时处理多个令牌可以提高吞吐量。这被称为&lt;code&gt;并行(parallelism)&lt;/code&gt;，其有两种形式：空间的和时间的。利用&lt;code&gt;空间并行性(spatial parallelism)&lt;/code&gt;，提供多个硬件副本从而在同一时刻完成多个任务；利用&lt;code&gt;时间并行性(temporal parallelism)&lt;/code&gt;，任务被分成多个阶段，就像一条分配线。多任务可以跨阶段展开执行。尽管每个任务都需要经过所有阶段，但每个阶段的任意时刻都会有不同的任务，因此多个任务可以重叠。时间并行通常被称为&lt;code&gt;流水线(pipelining)&lt;/code&gt;。空间并行有时也被称为&lt;code&gt;并行(parallelism)&lt;/code&gt;，但是我们会避免这种叫法，因为有二义性。&lt;/p&gt;
&lt;p&gt;考虑一个具有延迟$L$的任务。在没有并行性的系统中，吞吐量为$1/L$。在一个具有$N$个硬件副本的空间并行系统中，吞吐量为$N/L$。在一个时间上并行的系统中，任务被理想地分成了$N$个相等长度的步骤或阶段。在这种情况下，吞吐量同样为$N/L$，并且只需要一份硬件副本。然而，&lt;strong&gt;找到$N$个相等长度的步骤通常是不切实际的。如果最长的步骤具有延迟$L1$，则流水线式的吞吐量为&lt;/strong&gt;$1/L_1$。&lt;/p&gt;
&lt;p&gt;流水线处理(时间上的并行性)特别吸引人，因为它加速了电路而无需复制硬件。相反，寄存器被放置在组合逻辑块之间，将逻辑划分为较短的阶段，这些阶段可以以更快的时钟运行。这些寄存器防止了一个流水线阶段中的令牌赶上并损坏下一个阶段中的令牌。&lt;/p&gt;
&lt;p&gt;下图展示了没有流水线处理的电路的例子。其在寄存器之间包含了四个逻辑块，关键路径通过了$2, 3, 4$。假设这个寄存器的时钟沿到$Q$的传播时延为$0.3ns$和设置时间是$0.2ns$，那么其周期时间为$T_c = t_{pcq} + t_{pd} + t_{setup} = 0.3 + 3 + 2 + 4 + 0.2 = 9.5ns$。因此，该电路的延迟为$9.5ns$，吞吐量为$1/9.5ns = 105MHz$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151938547.png&quot; alt=&quot;no pipelining&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下图的第一张图展示了一样的电路，不过有一个二级流水线，通过在第三和第四组合电路之间添加寄存器。第一部分的最小时钟周期为$T_{c1} = 0.3 + 3 + 2 + 0.2 = 5.5ns$，第二部分的最小时钟周期为$T_{c2} = 0.3 + 4 + 0.2 = 4.5$。时钟必须足够慢，使得所有阶段都能工作，因此$T_c = max{T_{c1}, T_{c2}} = 5.5ns$。其延迟为两部分之和，即$11ns$，吞吐量为$1/5.5ns = 182MHz$。这个例子表明，&lt;strong&gt;在一个真实的电路中，具有两个阶段的流水线几乎使吞吐量翻倍，并略微增加了延迟。相比之下，理想的流水线处理会在不增加延迟的情况下确实使吞吐量翻倍。这种差异是因为电路无法被精确地划分为两个完全相等的部分，以及因为寄存器引入了更多的顺序开销&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404151943893.png&quot; alt=&quot;has pipelining&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图的第二张图显示了一个三级流水线，&lt;strong&gt;值得注意的是，在第一个流水线阶段结束时，需要两个寄存器来存储块$1$和块$2$的结果&lt;/strong&gt;。可以看出，周期时间被第三阶段限制为$T_c = T_{c3} = 4.5ns$，延迟为$11.5ns$，吞吐量为$1/4.5=222MHz$。因此，&lt;strong&gt;增加一个流水线阶段以牺牲部分延迟的代价来提高吞吐量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;尽管这些技术非常强大，但并不适用于所有情况。&lt;strong&gt;并行性的祸根是依赖关系。如果当前任务依赖于先前任务的结果，而不仅仅是当前任务中的先前步骤，那么任务在先前任务完成之前就无法启动。例如，如果本想要在开始准备第二批饼干之前先检查第一批饼干的味道是否好，那么他就有了一个依赖关系，这将阻止流水线处理或并行操作。并行性是设计高性能数字系统的最重要技术之一&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>DDCA: The Chapter 2 Reading</title><link>https://chenmiaoi.github.io/2024/04/11/DDCA-The-Chapter-2-Reading/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/04/11/DDCA-The-Chapter-2-Reading/</guid><pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在上一篇笔记中，我们已经学习了关于数字系统的一些基本原理以及构建和理解数字系统的基本概念。现在本篇笔记将讲解&lt;code&gt;组合逻辑(Combinational Logic)&lt;/code&gt;，&lt;strong&gt;即输出只取决于当前的输入&lt;/strong&gt;。在上一篇笔记中的逻辑门介绍都是组合逻辑的例子，&lt;strong&gt;本篇中你将学习设计多个门电路，以实现由&lt;/strong&gt;&lt;code&gt;真值表(truth table)&lt;/code&gt;或&lt;code&gt;布尔方程(Boolean Equation)&lt;/code&gt;&lt;strong&gt;指定的输入和输出之间的关系&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;在一个数字电子学中，&lt;code&gt;电路(circuit)&lt;/code&gt;&lt;strong&gt;是处理离散变量的网格，其可以被看作一个黑箱&lt;/strong&gt;，如下图中左图所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个或多个离散值输入接口&lt;/li&gt;
&lt;li&gt;一个或多个离散值输出接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;描述输入和输出之间关系的&lt;/strong&gt;&lt;code&gt;功能规范(functional specification)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;描述输入变化和输出相应之间延迟的&lt;/strong&gt;&lt;code&gt;时许规范(timing specification)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404112329840.png&quot; alt=&quot;black box with circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;电路由&lt;code&gt;节点(node)&lt;/code&gt;和&lt;code&gt;元件(element)&lt;/code&gt;组成。&lt;strong&gt;一个元件本身就是一个具有输入输出和规范的电路；节点是导线，其电压传递一个离散值变量。节点一般分为输入、输出和内部节点&lt;/strong&gt;(&lt;strong&gt;也就是没有输入或输出的导线&lt;/strong&gt;)。上图中的左图解释了三个元件的电路，以及六个节点。&lt;/p&gt;
&lt;p&gt;数字电路被分为组合逻辑电路或&lt;code&gt;时序逻辑电路(sequential)&lt;/code&gt;。&lt;strong&gt;一个组合逻辑电路的输出只取决于输入的当前值；时序逻辑电路的输出则取决于输入的当前值和之前的值(也就是上一个状态)，换句话说，其取决于输入的序列。组合逻辑是&lt;/strong&gt;&lt;code&gt;无记忆性(memoryless)&lt;/code&gt;&lt;strong&gt;，而时序逻辑是具有&lt;/strong&gt;&lt;code&gt;记忆性(memory)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;组合电路的功能规范用电流输入值表示输出值，时序规范由输入到输出的上界和下界组成。我们首先关注的功能规范。&lt;/p&gt;
&lt;p&gt;下图展示了一个二输入单输出的组合电路，黑盒内部的符号℄表明它只用组合逻辑实现，在下图的例子中，特指功能$F$为$OR$：$Y = F(A, B) = A + B$。同时，我们也可以从右图中发现，&lt;strong&gt;对于单个功能的实现可能会存在多种形式，在给定可用的构建块和设计约束下，可以自由地选择如何设计&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121105178.png&quot; alt=&quot;combinational logic circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而下面一张图则表示了多输出的情况，该电路被称为&lt;code&gt;全加器(full adder)&lt;/code&gt;。为了简化图纸，我们通常使用一条带斜线的单线和旁边的一个数字来表示总线，即多个信号的约束。该数字规定了总线中有多少个信号。如下图中的左图所示。如果位数不重要或上下文中明显，就可能如图b一样只展示斜线，不表示数字，表示一个块的任意数量的输出作为第二个块的输入。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121113083.png&quot; alt=&quot;multiple-output&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;组合构图(combinational composition)&lt;/code&gt;的规则告诉我们如何从较小的组合逻辑电路元件构造一个大的组合电路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个电路元件本身就是组合的&lt;/li&gt;
&lt;li&gt;电路的每个节点要么被指定为电路的输入，要么恰好连接电路元件的一个输出端&lt;/li&gt;
&lt;li&gt;电路中不包含循环路径：&lt;strong&gt;每条经过电路的路径至多访问每个电路节点一次&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大型电路(比如微处理器)是非常复杂的，所以我们使用从上一篇笔记中所说的原则来管理复杂性。&lt;strong&gt;将电路看成一个具有良好定义的接口和功能的黑盒，是抽象化和模块化的运用；由小模块构建电路是层次化的应用；而组合构图则是规约的一种运用&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Boolean Equations&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;布尔方程(Boolean Equations)&lt;/code&gt;处理的变量要么为$TRUE$，要么为$FALSE$，因此它们非常适合描述数字逻辑。本节定义了布尔方程中常用的一些术语，然后展示了如何根据真值表为任何逻辑函数编写布尔方程。&lt;/p&gt;
&lt;h3&gt;Terminology&lt;/h3&gt;
&lt;p&gt;变量$A$的&lt;code&gt;补(complement)&lt;/code&gt;被称为$\overline {A}$。一个变量或自身的补被称为&lt;code&gt;字面量(literal)&lt;/code&gt;。我们也称$A$是变量的&lt;code&gt;真正形式(true form)&lt;/code&gt;，而$\overline {A}$是变量的&lt;code&gt;补充形式(complementary form)&lt;/code&gt;。&lt;strong&gt;真正形式并不意味着$A$就是$TRUE$，仅仅是说明$A$没有上划线&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个或多个字面量的$AND$运算被称为&lt;code&gt;乘积(product)&lt;/code&gt;或&lt;code&gt;蕴含项(implicant)&lt;/code&gt;。一个&lt;code&gt;最小项(minterm)&lt;/code&gt;是一个涉及到函数的所有输入的积；一个&lt;code&gt;最大项(maxterm)&lt;/code&gt;是一个涉及到函数的所有输入的和。&lt;/p&gt;
&lt;h3&gt;Sum-of-Products Form&lt;/h3&gt;
&lt;p&gt;一个$N$输入的真值表包含$2^N$行，真值表中的每一行都与一个最小项相关联，而该最小项就是该行的$TRUE$值。例如下图中的左图所示，第一行的最小项是$\overline {AB}$，因为当$A = 0, B = 0$时，$\overline {AB} = TRUE$。最小项的编号从0开始计算。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121212217.png&quot; alt=&quot;truth table and minterms&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于任意一个真值表，我们可以通过求和每一个最小项来写出一个布尔方程，其满足输出$Y = TRUE$即可&lt;/strong&gt;。比如上图中两表画蓝圈所表示的：$Y_1 = m_1 = \overline {A}B$，$Y_2 = m_1 + m_3 = \overline {A}B + AB$。&lt;/p&gt;
&lt;p&gt;如上述的表达式被称为函数的&lt;code&gt;乘积和规范式(sum-of-products canonical form)&lt;/code&gt;。&lt;strong&gt;其表明了，对于同一个真值表，我们总是能够写出相同的布尔表达式&lt;/strong&gt;(&lt;strong&gt;即真值表具有唯一性&lt;/strong&gt;)。乘积和规范式也可以使用求和符号$\sum$写成&lt;code&gt;sigma表示法(sigma notation)&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;$$
F(A, B) = \sum(m_1, m_3) or F(A, B) = \sum(1, 3)
$$&lt;/p&gt;
&lt;p&gt;同时，乘积和规范式为任意变量个数的真值表提供了一个布尔方程。&lt;strong&gt;但令人遗憾地是，乘积和规范式并不一定产生最简单的方程&lt;/strong&gt;。产生最简式的方法将在后续介绍。&lt;/p&gt;
&lt;h3&gt;Product-of-Sums Form&lt;/h3&gt;
&lt;p&gt;布尔表达式的另一种表示方式是&lt;code&gt;和积规范式(product-of-sums canonical form)&lt;/code&gt;。真值表中的每一行都与一个最大值相关联，而该最大值就是该行的$FALSE$值。&lt;strong&gt;对于任意一个真值表，我们可以通过乘积每一个最大项来写出一个布尔方程，其满足输出$Y = FALSE$即可&lt;/strong&gt;。和积规范式也可以使用乘积分号$\Pi$来表示&lt;code&gt;pi表示法(pi notation)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有当少数几行真值表上的输出为$TRUE$时，乘积和规范式产生一个较短的等式；仅当少数几行真值表上的输出为$FALSE$时，和积规范式更简单&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Boolean Algebra&lt;/h2&gt;
&lt;p&gt;为了产生最简式，我们可以通过&lt;code&gt;布尔代数(Boolean Algebra)&lt;/code&gt;来简化布尔方程。布尔代数的规则与普通代数的规则非常类似，并且在某些情况下更为简单。&lt;/p&gt;
&lt;p&gt;布尔代数的公理和定理都遵循&lt;code&gt;对偶原则(the principle of duality)&lt;/code&gt;。如果符号&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;以及$+$和$\cdot$可以互换，并且语句依旧正确，那么就可以使用$\prime$来标记一个语句的对偶。&lt;/p&gt;
&lt;h3&gt;Axioms&lt;/h3&gt;
&lt;p&gt;下表陈述了布尔代数的&lt;code&gt;公理(Axioms)&lt;/code&gt;。这五条公理和它们的对偶定义了布尔变量和$NOT$、$AND$和$OR$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121254979.png&quot; alt=&quot;Axioms of Boolean Algebra&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Theorems of One Variable&lt;/h3&gt;
&lt;p&gt;下表陈述了布尔代数的&lt;code&gt;定理(Theorems)&lt;/code&gt;。&lt;strong&gt;这五条定理描述了如何化简含有一个变量的方程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121657502.png&quot; alt=&quot;Boolean theorems of one variable&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Theorems of Several Variables&lt;/h3&gt;
&lt;p&gt;下表描述了如何简化多个布尔变量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121701668.png&quot; alt=&quot;Boolean theorems of several variables&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从德摩根律可以得出一些有趣的结论。假如我们把&lt;code&gt;反转圈(inversion circle)&lt;/code&gt;看作&lt;code&gt;气泡(bubble)&lt;/code&gt;，如果你把下图中在输入端的“气泡”推向输出端，那么输入端的“气泡”就会移动到输出端，然后门体就会发生翻转(比如从$OR$翻转为$AND$)，我们就得到两个不同的逻辑门表示同一个门功能。但是，“气泡”的推动是有一些依赖规则的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;向前或向后(输入到输入为向前)推动“气泡”会改变门体从$AND$到$OR$，反之亦然&lt;/li&gt;
&lt;li&gt;从输出端推回“气泡”会使得输入端的每个接口都会有一个“气泡”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保证输入端都有“气泡”的情况下，从输出入段推送“气泡”会使得输出端有一个“气泡”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404121706989.png&quot; alt=&quot;De Morgan equivalent&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Simplifying Equations&lt;/h3&gt;
&lt;p&gt;简化乘积和规范式的基本原理是利用关系式$PA + P\overline{A} = P$来组合项，其中$P$可以是任意蕴含项。&lt;strong&gt;我们定义一个乘积和规范式如果在尽可能少的使用蕴含项的情况下那么就是&lt;/strong&gt;&lt;code&gt;最小化方程(minimized)&lt;/code&gt;。如果有几个方程具有相同的蕴含项，则最小的则是具体字面量少的那个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果一个蕴含项不能与方程中的其他任何蕴含项结合起来，以较少的字面量构成一个新的蕴含项，就称其为&lt;/strong&gt;&lt;code&gt;素蕴含项(prime implicant)&lt;/code&gt;。&lt;strong&gt;最小化等式中的蕴含项一定都是素蕴含项&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122022122.png&quot; alt=&quot;improved equation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，虽然这有点反直觉($A\overline{B}C = A\overline{B}C + A\overline{B}\overline{C}$)，但扩展蕴含项有时在简化最小化方程时有妙用。这样做能够重复扩展的最小项中的一个与另外一个最小项进行组合。&lt;/p&gt;
&lt;p&gt;或许你已经注意到，使用布尔定理完全简化一个布尔代数方程可能会导致一些错误的(也就是求出来的最小化可能有误)。后续笔者会介绍另一个被称为卡诺图的方法，用于简化。&lt;/p&gt;
&lt;h2&gt;From Logic To Gates&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;原理图(schematic)&lt;/code&gt;是数字电路的示意图，显示了连接它们的元件和导线。比如该等式$Y = \overline{A}\overline{B}\overline{C} + A\overline{B}\overline{C} + A\overline{B}C$可能的硬件实现为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122029433.png&quot; alt=&quot;schematic&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过一致的方式绘制原理图使得它们更容易阅读和调试，以下是一些准则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原理图中的输入通常是在左边(或上方)&lt;/li&gt;
&lt;li&gt;原理图中的输出通常是在右边(或下方)&lt;/li&gt;
&lt;li&gt;在可能的情况下，门应该是从左到右的&lt;/li&gt;
&lt;li&gt;直线导线比有多个拐角的导线更好用(&lt;strong&gt;参差不齐的导线会浪费精力跟踪电线，而不是思考电路的功能&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;导线总是连接在$T$连结上&lt;/li&gt;
&lt;li&gt;导线交叉的点表示导线之间的连接(若无点，则说明不连接)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122036060.png&quot; alt=&quot;wires form&quot; /&gt;&lt;/p&gt;
&lt;p&gt;任何乘积和规范的布尔方程都能够用系统的方式绘制成原理图。&lt;code&gt;可编辑逻辑阵列(Programmable Logic Array，PLA)&lt;/code&gt;是一种数字电路，其中包含了一系列可编程的逻辑门，允许用户根据特定需求配置逻辑功能。$PLA$通常由一组可编程的$AND$门阵列和一组可编程的$OR$门阵列组成。通过编程这些门的连接方式和输入，可以实现各种逻辑功能，从而定制化地构建数字电路。&lt;/p&gt;
&lt;p&gt;我们可以利用非门来进一步减少门的数量，比如下图中$\overline{B}\overline{C}$是一个带反向输入的与门。我们回忆以下德摩根律，一个带反向输入的与门等价于一个或非门。根据实现的技术不同，使用最少的门或优先使用某些类型的门或许更便宜，这样我们就能够再次进行优化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122045848.png&quot; alt=&quot;optimization gate&quot; /&gt;&lt;/p&gt;
&lt;p&gt;许多电路具有多个输出，其每个输出都与单独的布尔函数输入相关。我们可以为每个输出写一个单独的真值表，但在单个真值表上写出所有的输出，并用所有的输出画出一个示意图往往是方便很多的。例如，四输入优先电路的绘制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当$A_3$是导通时，$Y_3$才能是$TRUE$，因此$Y_3 = A_3$&lt;/li&gt;
&lt;li&gt;当$A_2$是导通的，$A_3$断开时，$Y_2$才能是$TRUE$，因此$Y_2 = \overline{A_3}A_2$&lt;/li&gt;
&lt;li&gt;依次类推...$Y_1 = \overline{A_3}\overline{A_2}A_1$，$Y_0 = \overline{A_3}\overline{A_2}\overline{A_1}A_0$&lt;/li&gt;
&lt;li&gt;然后列出四个输出的真值表&lt;/li&gt;
&lt;li&gt;画出对应电路&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122100899.png&quot; alt=&quot;priority circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们需要注意的是，当$A_3$是导通时，&lt;strong&gt;该优先电路的输出不关心其他的输入是什么&lt;/strong&gt;，因此，我们使用$X$来描述不被输出关心的输入值。然后，我们可以简化该真值表，如上图所示。&lt;/p&gt;
&lt;h3&gt;Multilevel Combinational Logic&lt;/h3&gt;
&lt;p&gt;在乘积和规范式的逻辑被称为&lt;code&gt;二级逻辑(two-level logic)&lt;/code&gt;，因为它由字面量连接到一级与门和一级或门。通常的，我们使用多级组合逻辑来设计电路，因为其可能使用更少的元件。&lt;/p&gt;
&lt;h3&gt;Hardware Reduction&lt;/h3&gt;
&lt;p&gt;现在我们学过的异或门来看，假如我们想要通过二级逻辑来构建三输入的异或门。&lt;/p&gt;
&lt;p&gt;回忆$N$输入异或门：&lt;strong&gt;当有奇数个输入为$TRUE$时，异或门的输出是&lt;/strong&gt;$TRUE$。因此我们得到其表达式：$Y = \overline {A}\overline {B}C + \overline {A}B\overline {C} + A\overline {B}\overline {C} + ABC$。我们发现，&lt;strong&gt;该表达式已经没有办法再简化了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另一方面，$A \oplus B \oplus C = (A \oplus B) \oplus C$，因此，三输入异或可以由二输入的异或级联而成。&lt;/p&gt;
&lt;p&gt;依次推广，你会发现&lt;strong&gt;使用二输入的异或级联称为多输入的异或是极好的&lt;/strong&gt;，假如你需要一个个三输入异或，你就需要$128$个输入的或门和$128$个八输入与门级联。不过，&lt;strong&gt;选择一个特定逻辑功能的最佳多级实现不是一个简单的过程&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Bubble Pushing&lt;/h3&gt;
&lt;p&gt;$CMOS$更倾向于使用与非门和或非门(而非与门和或门)。但是，&lt;strong&gt;通过观察具有与非门和或非门的多级电路来构建方程是一件非常复杂的事&lt;/strong&gt;。所以“气泡”的推动有助于重绘这些电路，以便“气泡”消除和功能更容易确定。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从电路的输出端开始，向输入端工作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推回任何再最终输出上的“气泡”使得你可以根据输出读取一个方程&lt;/strong&gt;(&lt;strong&gt;也就是输出&lt;/strong&gt;$Y$&lt;strong&gt;而不是&lt;/strong&gt;$\overline {Y}$)&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122142430.png&quot; alt=&quot;bubble-pushed circuit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后需要强调的是，&lt;strong&gt;由于串联的气泡相互抵消&lt;/strong&gt;，我们可以忽略在中间门的输出和最右门的一个输入上的“气泡”，因此得到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122146980.png&quot; alt=&quot;Logically equivalent bubble-pushed circuit&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;X&apos;s and Z&apos;s, Oh My&lt;/h2&gt;
&lt;p&gt;虽然布尔代数限制在&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;之间，但是真实的电路会存在非法值和浮动值，使用$X$和$Z$分别代替。&lt;/p&gt;
&lt;h3&gt;Illegal Value: X&lt;/h3&gt;
&lt;p&gt;符号$X$表示电路节点存在&lt;code&gt;未知的(unknown)&lt;/code&gt;或&lt;code&gt;非法的(illegal)&lt;/code&gt;值，其通常发生在电路同时导通&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;，这种情况被称为&lt;code&gt;竞争(contention)&lt;/code&gt;。这种情况的发生是被错误的且必须避免的。发生竞争后，该电路节点的电压通常是在$0 \thicksim V_{DD}$的，并且绝大多数都是在禁区的。竞争会导致大量电能在冲突的门中流动，导致电路发热并损坏。&lt;/p&gt;
&lt;p&gt;$X$值也通常被用于电路模拟中的未初始化值。不过，回忆之前看的，我们在真值表中也会出现$X$，但是该$X$与这两种情况不同，仅仅是表示其值可以被忽略的。&lt;/p&gt;
&lt;h3&gt;Floating Value: Z&lt;/h3&gt;
&lt;p&gt;符号$Z$表示一个节点既不在高电压也不在低电压，因此被称为&lt;code&gt;浮动(floating)&lt;/code&gt;、&lt;code&gt;高阻抗(high impedance)&lt;/code&gt;或者&lt;code&gt;高Z(high Z)&lt;/code&gt;。&lt;strong&gt;一个经典错误是：浮动或非驱动的节点的逻辑值和&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;&lt;strong&gt;相同；实际上，浮动节点可能为&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;&lt;strong&gt;也可能为&lt;/strong&gt;&lt;code&gt;1&lt;/code&gt;，&lt;strong&gt;还可能为其中间的其他电压值，这取决于系统&lt;/strong&gt;。还需要注意的是：&lt;strong&gt;一个浮动的节点所表现的电路状态不总是错的，有可能也会正确的执行其功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;产生浮动节点的一种常见方式是：&lt;strong&gt;忘记将电压接到电源输入，或假设未连接的输入与取值为&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;&lt;strong&gt;的输入相同。这种错误就可能导致当浮动值在$0 \thicksim 1$随机变化时，电路表现出不规律&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122210693.png&quot; alt=&quot;tristate buffer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;三态缓冲器(tristate buffer)&lt;/code&gt;有三种可能的输出状态：&lt;code&gt;HIGH(1)&lt;/code&gt;、&lt;code&gt;LOW(0)&lt;/code&gt;和&lt;code&gt;floating(Z)&lt;/code&gt;。当使能$E$表现不同状态时，三态缓冲器的输出也会随之变化。三态缓冲器常用于在总线上连接多个芯片。&lt;/p&gt;
&lt;p&gt;比如说微处理器、视频控制器和以太网控制器都需要与计算机中的存储系统通信，每个芯片都可以通过使用三态缓冲器连接到一个共享内存总线上。同时，&lt;strong&gt;该共享总线一次性只能与一个控制器通信，因此其他控制器就必须进入浮动输出状态以免干扰通信中的控制器&lt;/strong&gt;。不过在现代计算机中，&lt;strong&gt;点对点链路可以实现更高的通信速度，芯片之间直接连通，不再通过总线共享&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Karnaugh Maps&lt;/h2&gt;
&lt;p&gt;在使用布尔代数对布尔方程进行多次最小化后，你会意识到，一旦你不小心，最后就会得到一个完全不同的方程，而不是一个简化的方程。因此，&lt;code&gt;卡诺图(Karnaugh maps, K-maps)&lt;/code&gt;时一种简化布尔方程的图解方法。&lt;strong&gt;卡诺图善于处理至多四个变量的情况&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;回想一下，逻辑最小化涉及到组合项：&lt;strong&gt;将包含一个蕴含项$P$的两项与某一变量$A$的真实形式和互补形式组合，以消除&lt;/strong&gt;$A$，即$PA + P\overline{A} = P$。卡诺图以图解的形式展示出来。&lt;/p&gt;
&lt;p&gt;下图展示了真值表和卡诺图。卡诺图的最上面的行给出了输入$AB$的四个可能值，左边的列给出了输入$C$的两个可能值。每一个正方形格都对应着真值表中的一行，并且包含该行的输出$Y$的值。图(c)显示了卡诺图中的每个正方形格对应的最小项。&lt;/p&gt;
&lt;p&gt;每个单元格(或最小项)因单个变量的变化而与相邻的单元格不同，&lt;strong&gt;意味着相邻的单元格共享除一个以外的所有相同的字面量(也就是$\overline{A}\overline{B}\overline{C} \longrightarrow \overline{A}B\overline{C}$, $\overline{A}\overline{B}\overline{C} \longrightarrow \overline{A}\overline{B}C$等等)&lt;/strong&gt;。你或许注意到，&lt;strong&gt;顶部$AB$的取值是特殊的：$00,\ 01,\ 11,\ 10$，这不同于正常顺序的二进制编码&lt;/strong&gt;，而是&lt;code&gt;格雷码(Gray code)&lt;/code&gt;。也就是说，&lt;strong&gt;相邻元素只有单个值发生变化，不会有多个值同时变化&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;卡诺图是“环绕的”：&lt;strong&gt;最右边的列与最左边的列是有效相邻的(变化同样是单元素变化)，也就是说，可以将卡诺图卷成一个圆柱体，端点连接成为圆环，保证性质不变&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122315476.png&quot; alt=&quot;Three-input function&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Circular Thinking&lt;/h3&gt;
&lt;p&gt;在上图中，只有两个最小项，而从卡诺图中读取最小项恰好等价于直接从真值表中读取乘积和规范式。&lt;/p&gt;
&lt;p&gt;在这之前，我们可以通过布尔代数得到最小化：&lt;/p&gt;
&lt;p&gt;$$
Y = \overline{A}\overline{B}\overline{C} + \overline{A}\overline{B}C = \overline{A}\overline{B}(\overline{C} + C) = \overline {AB}
$$&lt;/p&gt;
&lt;p&gt;现在，卡诺图可以通过圈住邻近值为&lt;code&gt;1&lt;/code&gt;的单元格帮助我们图像化地简化。&lt;/p&gt;
&lt;p&gt;对于每个圈，我们应该写出对应的蕴含项(重温一下，&lt;strong&gt;蕴含项是一个或多个字面量的组合&lt;/strong&gt;)。但是记住&lt;strong&gt;真值形式和互补形式都在圈内的变量将被排除在蕴含项之外&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404122337073.png&quot; alt=&quot;K-maps minization&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Logic Minimization with K-Maps&lt;/h3&gt;
&lt;p&gt;卡诺图提供了一种简易的可视化方法来得到最小化。只需要使用尽可能少的圆圈数，将地图中的表示&lt;code&gt;1&lt;/code&gt;值的所有单元格圈出即可。更为正式地说，&lt;strong&gt;当一个布尔方程被写成最少的素蕴含项的和时，是最小化的；因此卡诺图上的每个圆圈代表一个蕴含项，最大可能的圆圈为素蕴含项&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这里给出从卡诺图中找到最小化的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用最少且必要的圈来覆盖所有的&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;圈内的所有单元格都必须包含&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每个圈必须跨越一个矩形块(该矩形块必须是$1、2、4$个方格组成的)&lt;/li&gt;
&lt;li&gt;每个圆圈必须尽可能的大&lt;/li&gt;
&lt;li&gt;圆圈必须在卡诺图的边界内&lt;/li&gt;
&lt;li&gt;如果允许使用较少的圆，则卡诺图中的一个&lt;code&gt;1&lt;/code&gt;可以被循环多次包含&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一个七段数码管解码器接受一个四位数据输入$D_{3:0}$，并产生七个输出来控制发光二极管以显示从0到9的数字。这七个输出通常称为$a$到$g$段，或者$S_a–S_g$，如下图所定义。数字如下图所示。编写输出的真值表，并使用卡诺图找到$S_a$和$S_b$的布尔方程。假设非法的输入值$(10–15)$会产生一个空白输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404130024444.png&quot; alt=&quot;Seven-digit truth table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其$S_a - S_g$的真值表如上图所示，而$S_a$和$S_b$的卡诺图如下所示。然后我们将所有相邻的&lt;code&gt;1&lt;/code&gt;按照卡诺图的规则尽可能多的圈出来，就得到了最下方的卡诺图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404130024417.png&quot; alt=&quot;K-Map solution&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要注意：&lt;strong&gt;素蕴含项的最小化集合并不唯一&lt;/strong&gt;。比如说，$S_a$的卡诺图中的$0000$项与$1000$项可以共同产生出$\overline{D_2}\overline{D_1}\overline{D_0}$，可也能和$0010$项产生出$\overline{D_3}\overline{D_2}\overline{D_0}$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131255374.png&quot; alt=&quot;alternative K-maps for Sa&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中的右图需要注意：&lt;strong&gt;这里出现了一个常见的错误，即在左上角选择了一个非素蕴含项来覆盖&lt;/strong&gt;&lt;code&gt;1&lt;/code&gt;。这个最小项$\overline{D_3}\overline{D_2}\overline{D_1}\overline{D_0}$给出了一个不为最小化的乘积和规范式。该最小项可以和相邻的任意一个项组合形成一个更大的圆圈。&lt;/p&gt;
&lt;h3&gt;Don&apos;t Cares&lt;/h3&gt;
&lt;p&gt;回忆我们在之前学习的$X$，即真值表的输入不被输出所关心。这一概念其实也能出现在真值表中输出不被输入所关心。因此，&lt;strong&gt;在卡诺图中，$X$允许进一步逻辑最小化，如果被圈出的对圈没有任何帮助，那么是可以被忽略的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131307082.png&quot; alt=&quot;K-Maps with don&apos;t cares&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;The Big Picture&lt;/h3&gt;
&lt;p&gt;卡诺图和布尔代数都是为了&lt;strong&gt;寻找到一种低成本实现特定逻辑功能的方法&lt;/strong&gt;。但在现代工程实践中，&lt;strong&gt;通过逻辑综合器的计算机程序可以从逻辑函数的描述中生成最简电路，因此对于大型问题，使用逻辑综合器更佳&lt;/strong&gt;。我们将在后续进行学习。&lt;/p&gt;
&lt;h2&gt;Combinational Building Blocks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;组合逻辑通常被组合成一个更大块来构建更为复杂的系统。这是一种抽象原则的应用，隐藏了不必要的门级细节以强调组成块的功能&lt;/strong&gt;。本节中会介绍&lt;code&gt;多路复用器(multiplexer)&lt;/code&gt;和&lt;code&gt;解码器(decoders)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Multiplexer&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;多路复用器(Multiplexer)&lt;/code&gt;是最常用的组合电路。&lt;strong&gt;其根据一个选择信号的值从多个可能的输入中选择一个输出&lt;/strong&gt;。多路复用器又被称作&lt;code&gt;mux&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;2:1 Multiplexer&lt;/h4&gt;
&lt;p&gt;下图展示了&lt;code&gt;2:1多路复用器&lt;/code&gt;的原理图和真值表。其有两个输入$D_0$和$D_1$，一个选择输入$S$以及一个输出$Y$。其遵循：当$S=0$时，$Y=D_0$反之则$Y=D_1$。$S$也被称为&lt;code&gt;控制信号(control signal)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;其有图展示了&lt;code&gt;2:1多路复用器&lt;/code&gt;的卡诺图和逻辑电路。当然，我们还有另一种方式构建多路复用器，也就是通过三态缓冲器。&lt;strong&gt;三态缓冲器使得在任何时候，只有一个三态缓冲器是激活的，当$S = 0$时激活三态$T_0$，反之激活$T_1$，就能够实现同等功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131428215.png&quot; alt=&quot;2:1 Multiplexer&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Wider Multiplexer&lt;/h4&gt;
&lt;p&gt;下图展示了&lt;code&gt;4:1多路复用器&lt;/code&gt;，我们可以发现，多路复用器可以有三种构成方式：与门、三台缓冲器和多路复用器级联。我们也可以发现，如果在更大的多路复用器上，其控制信号的个数为$\log_2N$，并且依旧能使用这三种方式来构建。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131431612.png&quot; alt=&quot;4:1 Multiplexer&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Multiplexer Logic&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;多路复用器可以使用&lt;/strong&gt;&lt;code&gt;查找表(lookup tables)&lt;/code&gt;&lt;strong&gt;的方式来实现逻辑功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图使用了&lt;code&gt;4:1多路复用器&lt;/code&gt;来实现二输入与门的逻辑操作.&lt;strong&gt;实际上，对于任意一个$N$输入的逻辑函数，其都可以被$2^N$个输入的多路复用器来实现&lt;/strong&gt;。&lt;strong&gt;其方法是：写出逻辑函数的真值表，通过真值表的值对多路复用器的输入进行连接，而对应的输入作为多路复用器的控制信号用以查找表即可&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131443021.png&quot; alt=&quot;multiplexer logic implement&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Decoders&lt;/h3&gt;
&lt;p&gt;一个&lt;code&gt;解码器(decoder)&lt;/code&gt;有$N$个输入和$2^N$个输出。&lt;strong&gt;它的每一位输出都依赖于输入的组合&lt;/strong&gt;。下图是&lt;code&gt;2:4解码器&lt;/code&gt;的逻辑，其输出被称为&lt;code&gt;独热(one-hot)&lt;/code&gt;，因为在给定条件下一次只有一个输出为&lt;code&gt;hot(HIGH)&lt;/code&gt;状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;解码器和译码器这两个术语是等同的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;译码器的每一个输出都依赖于所有输入的真实形式或互补形式。总之，一个$N:2^N$译码器可以由$2^N$个输入与门通过接收所有输入的$TRUE$或$FALSE$的各种组合值构成&lt;/strong&gt;。&lt;strong&gt;译码器的每一个输出都表示了一个最小项&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131454667.png&quot; alt=&quot;Decoder implement&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Decoder Logic&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;译码器可以与或门一起来构建逻辑函数&lt;/strong&gt;。如上图所示，一个&lt;code&gt;2:4译码器&lt;/code&gt;与一个二输入或门构建了一个二输入同或门$Y = AB + \overline{AB} = \overline {A \oplus B}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用译码器来构建逻辑电路时，很容易将函数表示成真值表或乘积和规范式。一个在真值表中有&lt;/strong&gt;$M$个&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;的$N$输入函数能够被一个$N:2^N$译码器和一个$M$个输入的或门连接所有输出&lt;/strong&gt;&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;的最小项构成&lt;/strong&gt;。这在后面的只读存储器中会被使用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里看起来或许比较晦涩拗口，所以给出一个小例子：
假如你需要实现二输入的与门，我们已经知道其真值表中有一个输出为&lt;code&gt;1&lt;/code&gt;。因此选择&lt;code&gt;2:4译码器&lt;/code&gt;和一个一输入或门(也就是说，二输入或门的一个输入永远是&lt;code&gt;0&lt;/code&gt;)，然后根据二输入与门的真值表可知，只有两个输入同时为&lt;code&gt;1&lt;/code&gt;时，输出才为&lt;code&gt;1&lt;/code&gt;，那么我们选择&lt;code&gt;2:4译码器&lt;/code&gt;中的&lt;code&gt;Y_3&lt;/code&gt;，然后连接所有最小项，可以得到其方程$Y = Y_3 + 0 = AB + 0 = AB$
这样就得到了通过&lt;code&gt;2:4译码器&lt;/code&gt;和一个一输入或门实现二输入与门的实现&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Timing&lt;/h2&gt;
&lt;p&gt;在之前的几个小节中，我们主要关注&lt;strong&gt;理想状况下使用最少数量的门的电路工作情况&lt;/strong&gt;。实际上，&lt;strong&gt;电路设计中最具挑战的问题之一是时序问题：如何使电路运行地更快&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个输出需要时间来响应一个输入的变化&lt;/strong&gt;。下图展示了缓冲器的输入变化和后续输出变化之间的&lt;code&gt;延迟(delay)&lt;/code&gt;。这种图被称为$时序图(timing diagram)$；&lt;strong&gt;它刻画了当输入变化时缓冲电路的&lt;/strong&gt;&lt;code&gt;瞬态响应(transient response)&lt;/code&gt;。从$LOW$到$HIGH$的转变被称为&lt;code&gt;上升沿(rising edge)&lt;/code&gt;，当然，从$HIGH$到$LOW$的变化被称为$下降沿(falling edge)$。蓝色的箭头表示了$Y$的上升沿是由于$A$的上升沿引起的。&lt;strong&gt;我们在输入信号$A$的$50%$点与输出信号$Y$的$50%$点测量延迟&lt;/strong&gt;。&lt;code&gt;50%点(50% point)&lt;/code&gt;&lt;strong&gt;是信号在转变过程中电压处于高电平和低电平之间&lt;/strong&gt;&lt;code&gt;中间点(half-way)&lt;/code&gt;&lt;strong&gt;的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131523810.png&quot; alt=&quot;Circuit delay&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Propagation amd Contamination Delay&lt;/h3&gt;
&lt;p&gt;组合逻辑电路的时序特征是&lt;code&gt;传播延时(Propagation Delay)&lt;/code&gt;和&lt;code&gt;污染延时(Contamination Delay)&lt;/code&gt;。传播延时$t_{pd}$&lt;strong&gt;是指输入发生变化直到输出或输出到达到其最终值的最长时间&lt;/strong&gt;；而污染延时$t_{cd}$&lt;strong&gt;是指输入发生变化直到输出开始变化其值的最小时间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如上图所示，缓冲器的传播延时和污染延时分别由蓝色和灰色表示。上图表示了$A$的初态要么是$HIGH$要么是$LOW$，并在特定时间变化到另一种状态。我们并不关心其值，只对其发生了变化进行关注。$Y$在$A$改变一段时间后才会响应。弧线表示$Y$可能在$A$转变后$t_{cd}$时间后开始相应，在$t_{pd}$时间内会稳定到新值。&lt;/p&gt;
&lt;p&gt;电路延迟的根本原因包括&lt;strong&gt;电路中电容充电的所需时间和电信号以光速传播&lt;/strong&gt;，为此，$t_{cd}$和$t_{pd}$不同的可能原因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同的上升和下降延迟&lt;/li&gt;
&lt;li&gt;多个输入和输出之间的延迟可能不同&lt;/li&gt;
&lt;li&gt;电路在冷时加速，热时减速&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;想要详细准确地计算$t_{cd}$和$t_{pd}$需要深入到更低的抽象层次，这不在我们的研究范围。然而，&lt;strong&gt;制造商通常会提供数据表来指定每个门的延迟&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如前所述，&lt;strong&gt;传播延时和污染延时也和信号从输入到输出的路径决定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图展示了一个四输入逻辑电路。蓝色表示的是&lt;code&gt;关键路径(critical path)&lt;/code&gt;，&lt;strong&gt;其是最长的同时也是最慢的一条路径，因为输入通过了三个门才传输到输出。这条路径是关键路径的原因是因为它限制了电路的运行速度&lt;/strong&gt;。灰色表示的是&lt;code&gt;最短路径(short path)&lt;/code&gt;，&lt;strong&gt;其只从输入到输出经过一个门，所以是最短的路径也是最快的路径&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131633513.png&quot; alt=&quot;Critical and short path waveforms&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;组合逻辑电路上的传播时延是在关键路径上的每一个元件的传输延迟之和&lt;/strong&gt;。&lt;strong&gt;污染时延是最短路径上每个元件的最小延迟之和&lt;/strong&gt;。比如上图中的时延为：&lt;/p&gt;
&lt;p&gt;$$
t_{pd} = 2t_{pd_AND} + t_{pd_OR}
$$&lt;/p&gt;
&lt;p&gt;$$
t_{cd} = t_{cd_AND}
$$&lt;/p&gt;
&lt;h3&gt;Glitches&lt;/h3&gt;
&lt;p&gt;至今为止，我们只讨论了单输入转换导致单输出转换的情况。&lt;strong&gt;然而，单个输入转换可能导致多个输出变换&lt;/strong&gt;，这被称为&lt;code&gt;毛刺(glitches)&lt;/code&gt;或&lt;code&gt;危险(hazards)&lt;/code&gt;。&lt;strong&gt;尽管永昌毛刺不会造成问题，但是重要的是要意识到它们的存在，并在查看时序图时识别出它们&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131645691.png&quot; alt=&quot;Circuits with a glitch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图表示了一个带有毛刺电路的卡诺图和电路。最短路径(用灰色表示)穿过两个门，与门和或门。关键路径(用蓝色表示)穿过三个门，非门，与门和或门。当$B$从&lt;code&gt;1&lt;/code&gt;跃迁到&lt;code&gt;0&lt;/code&gt;时，$n2$(在短路径上)先于$n1$(在关键路径上)下降。直到$n1$上升前，或门的两个输出都是是&lt;code&gt;0&lt;/code&gt;，使得输出$Y$跌至&lt;code&gt;0&lt;/code&gt;。当$n1$最终上升后，$Y$才会回到&lt;code&gt;1&lt;/code&gt;。上图中也展示了该状况的时序图，$Y$从&lt;code&gt;1&lt;/code&gt;开始变化，以&lt;code&gt;1&lt;/code&gt;结束，但是在中间暂时产生了毛刺&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;只要我们在读取输出之前的等待时间和传输延迟一样长，毛刺的出现就不会造成问题，因为输出最终将稳定在正确的值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;添加另一个逻辑门可以避免这样的毛刺&lt;/strong&gt;。下图中的卡诺图可以解释这一问题，我们发现从$ABC = 001 \longrightarrow ABC = 011$的过程正好是上述$B$发生变换的过程，也就是其产生了一个毛刺。因此，&lt;strong&gt;卡诺图中跨越两个素蕴含项边界的跃迁预示着一个可能的毛刺&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由上图中的时序图可以看出，&lt;strong&gt;如果一个素蕴含项的电路在另一个素蕴含项的电路开启之前关闭，就会产生毛刺&lt;/strong&gt;。为了去除毛刺，&lt;strong&gt;可以增加一个新的覆盖该蕴含项边缘的圆圈，根据一致性定理，新增加的项$\overline{A}C$是一致的或冗余的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404131703043.png&quot; alt=&quot;Circuit without glitch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图就表示了一个无毛刺的电路。增加了一个与门(蓝色高亮部分)，此时，$B$的跃迁不再导致输出的毛刺产生，因为这个与门在整个转移过程中输出&lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总之，当单个变量的跃迁导致卡诺图中的两个素蕴含项之间的边界被跨越时，我们能够通过在卡诺图中增加多余的蕴含项来消除毛刺&lt;/strong&gt;。&lt;strong&gt;但这会带来额外的硬件成本代价&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;然而，多个输入同时发生转换也会导致毛刺。这些毛刺无法通过添加硬件来修复&lt;/strong&gt;。&lt;strong&gt;由于绝大多数“有趣的”系统在多个输入上同时(或接近同时)发生转换，&quot;因此在大多数电路中，毛刺是&apos;生活&apos;中的一部分&quot;&lt;/strong&gt;。虽然我们已经展示了如何消除一种类型的毛刺，&lt;strong&gt;但讨论毛刺的重点不是为了消除它们，而是意识到它们的存在。在查看模拟器或示波器上的时序图时，这一点尤为重要&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;数字电路是一个带离散电压值输入和输出的模块。它的规范描述了模块实现的功能和时序。这一章我们将重点放在组合电路上，其输出仅仅取决于当前的输入量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下一章将介绍时序电路，它的输出取决于先前的输入和当前的输入。换句话说，时序电路对过去的状态有记忆能力&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>DDCA: The Chapter 1 Reading</title><link>https://chenmiaoi.github.io/2024/04/09/DDCA-The-Chapter-1-Reading/</link><guid isPermaLink="true">https://chenmiaoi.github.io/2024/04/09/DDCA-The-Chapter-1-Reading/</guid><pubDate>Tue, 09 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;首先我必须声明的一点是：&lt;strong&gt;往后的所有文章、记录均非博客、知识讲述系列；仅仅用于我个人的学习记录和体会。因此&lt;em&gt;其中的一些知识并非全面且准确的。&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本学习笔记的起名&lt;code&gt;DDCA&lt;/code&gt;是有着一定含义的：其一，是&lt;code&gt;ETHz(苏黎世联邦理工学院)&lt;/code&gt;的一门课&lt;a href=&quot;https://safari.ethz.ch/digitaltechnik/spring2023/doku.php?id=schedule&quot;&gt;Digital Design and Computer Architecture&lt;/a&gt;； 同时也是一本入门书籍&lt;code&gt;Digital Design and Computer Architecture&lt;/code&gt;的名字。对于笔者而言，这也是我的第一门系统性的&lt;code&gt;Architecture&lt;/code&gt;课程和阅读书籍。&lt;/p&gt;
&lt;p&gt;笔者真心的希望，该阅读笔记能够对你有所帮助。同时，本笔记采用中英结合的方式，尽量的不会对读者对于原著的理解有过多偏差。&lt;/p&gt;
&lt;h2&gt;The GAME PLAN&lt;/h2&gt;
&lt;p&gt;本节讲述了全书的目标：&lt;strong&gt;如何设计和构建自己的微处理器(how to design and build your own microprocessor)&lt;/strong&gt;。该书致力于描述&lt;strong&gt;数字系统的设计，尤其是如何在&lt;code&gt;1&lt;/code&gt;，&lt;code&gt;0&lt;/code&gt;的机器上运行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;并且，本节也指出了数字系统的一个很大的优点就是&lt;strong&gt;足够简单&lt;/strong&gt;，只有&lt;code&gt;1&lt;/code&gt;和&lt;code&gt;0&lt;/code&gt;。当然，这和后面的数字系统中的二进制系统有很大关系，也涉及到了布尔代数(这一点我们后面再谈)。&lt;/p&gt;
&lt;h2&gt;The Art of Managing Complexity&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the characteristics that separates an engineer or computer scientist from a layperson is a systematic approach to managing complexity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;将工程师或计算机科学家从外行人中分离出来的特征之一是管理复杂性的系统方法&lt;/strong&gt;。也就是说，你需要学习去了解如何构建微处理器，而不会陷入其中细节的泥潭。&lt;/p&gt;
&lt;p&gt;显而易见的，这些概念在生活、学习和项目中是十分常见的。我们需要确定一个明确的目标，同时也需要一个良好的&lt;strong&gt;层次抽象&lt;/strong&gt;(&lt;strong&gt;Hierarchical Abstraction&lt;/strong&gt;)。&lt;/p&gt;
&lt;h3&gt;Abstraction&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The critical technique for managing complexity is abstraction: hiding details when they are not important.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;管理复杂性的关键技术在于&lt;code&gt;抽象(Abstraction)&lt;/code&gt;: 在不重要的时候隐藏细节&lt;/strong&gt;(&lt;strong&gt;或者说，隐藏一些不重要的细节&lt;/strong&gt;)。一个系统通常能从许多不同的抽象层次来看待：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;比如电子计算机系统的抽象层次，以及每个层次的经典构建(由上到下)：
&lt;ul&gt;
&lt;li&gt;Application Software, Programs&lt;/li&gt;
&lt;li&gt;Operator Systems, Device Drivers&lt;/li&gt;
&lt;li&gt;Architecture, Instructions Registers&lt;/li&gt;
&lt;li&gt;Micro-architecture, Datapaths Controllers&lt;/li&gt;
&lt;li&gt;Logic, Adders Memories&lt;/li&gt;
&lt;li&gt;Digital Circuits, AND Gates and NOT Gates&lt;/li&gt;
&lt;li&gt;Analog Circuits, Amplifiers Filters&lt;/li&gt;
&lt;li&gt;Devices, Transistors Diodes&lt;/li&gt;
&lt;li&gt;Physics, Electrons&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最底层的抽象是物理学，也就是电子的运动。电子的行为是由量子力学和麦克斯韦方程组描述(Quantum mechanics and Maxwell&apos;s equations)。我们的系统是由晶体管(或真空管(已经淘汰))等电子器件(electronic devices)构建的。&lt;/p&gt;
&lt;p&gt;再下一个抽象层是&lt;strong&gt;模拟电路(Analog Circuits)&lt;/strong&gt;，各种器件被组装以创建放大器等组件，&lt;strong&gt;模拟电路输入和输出一个连续的电压范围&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;数字电路(Digital circuits)&lt;/strong&gt; 将电压限制在离散范围内，我们将用它来表示&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;。在后续的逻辑层(&lt;code&gt;Logic&lt;/code&gt;)中，我们会从数字电路中构建更复杂的结构，如加法器或存储器。&lt;/p&gt;
&lt;p&gt;微架构(Micro-architecture)将抽象的逻辑层和架构层联系起来。&lt;strong&gt;架构层(Architecture)的抽象是从程序员的角度来描述一台计算的&lt;/strong&gt;。&lt;strong&gt;微架构涉及组合逻辑元素(combining logic elements)来执行架构定义的指令&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于软件层和操作系统层，这里就不再赘述。&lt;strong&gt;本书主要集中关注数字电路通过计算机体系结构的抽象层次&lt;/strong&gt;(&lt;strong&gt;focuses on the levels of abstraction from digital circuits through computer architecture.&lt;/strong&gt;)。&lt;/p&gt;
&lt;p&gt;在具体介绍更为详细的内容前，有一句话值得深思：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When you are working at one level of abstraction, it is good to know something about the levels of abstraction immediately above and below where you are working.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Discipline&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Discipline is the act of intentionally restricting your design choices so that you can work more productively at a higher level of abstraction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;规训(Discipline，其实笔者更愿意叫做规约)有意限制你的设计选择的行为，这样你就可以在更高的抽象层面上更有效地工作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这句话是较为难理解的，这句话强调了&lt;strong&gt;在设计和实现过程中要考虑到上层需求和可替代性，以便在提供抽象接口给上层时，能够让上层用户更加灵活、高效地使用。规约的设计和实现可以确保在更高的抽象层面上工作时不会出现不必要的复杂性和混乱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;结合上述，在本书的内容中，数字上的规约是十分重要的：我们考虑到数字电路上使用离散电压，而模拟电路使用连续电压。因此，数字电路是离散电路的一个子集。从某种意义上说，&lt;strong&gt;数字电路必须具备比更广泛的模拟电路类别更少的能力&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那么，我们就应该选择模拟电路作为规约了吗？&lt;strong&gt;不对，然而数字电路更更更容易去设计，对于数字电路，我们可以容易地将元件组合成复杂的系统，最终在许多应用中优于由模拟电路构建的系统。因此，回归主题，本书是以数字电路为载体研究计算机结构的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;The Three-Y&apos;s&lt;/h3&gt;
&lt;p&gt;除了&lt;strong&gt;抽象&lt;/strong&gt;和&lt;strong&gt;规约&lt;/strong&gt;，设计者们还会使用三个&lt;code&gt;y&apos;s&lt;/code&gt;来管理复杂性：&lt;code&gt;层次性(Hierarchy)&lt;/code&gt;、&lt;code&gt;模块性(Modularity)&lt;/code&gt;和&lt;code&gt;规律性(Regularity)&lt;/code&gt;：&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;blockquote&gt;
&lt;p&gt;我猜，肯定有人会不理解层次性居然是划分模块，而不是分层级。实际上，这里的模块指的就是一般意义上的层级，比如说&lt;code&gt;add&lt;/code&gt;，&lt;code&gt;sub&lt;/code&gt;就可以看作两个模块，而这两个模块同属于&lt;code&gt;function&lt;/code&gt;模块。如上所述，这样就分为了两个层级。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404101320181.png&quot; alt=&quot;Flintlock rifle with a close-up view of the lock&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是原书中的例子，这里做一个简要介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以层次性来说
&lt;ul&gt;
&lt;li&gt;来复枪分为三个组件：&lt;code&gt;Stock&lt;/code&gt;、&lt;code&gt;Lock&lt;/code&gt;、&lt;code&gt;Barrel&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Barrel&lt;/code&gt;为发射子弹的金属长管&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Lock&lt;/code&gt;可以分为：&lt;code&gt;Flint&lt;/code&gt;、&lt;code&gt;Cock&lt;/code&gt;、&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Sprint&lt;/code&gt;和&lt;code&gt;Pan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stock&lt;/code&gt;将零件固定在一起，提供安全抓力的木制本体&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;每个组件都可以被进一步详细地分层描述&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;以模块化来说
&lt;ul&gt;
&lt;li&gt;每个组件都应该具有明确定义的功能和接口
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Stock&lt;/code&gt;的功能是组装&lt;code&gt;Lock&lt;/code&gt;和&lt;code&gt;Barrel&lt;/code&gt;，其接口由长度和安装引脚位置组成&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Barrel&lt;/code&gt;的功能是给子弹施加旋转，使其更加精确地运动&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化要求不应该有副作用&lt;/strong&gt;：&lt;code&gt;Stock&lt;/code&gt;的设计不应该方案&lt;code&gt;Barrel&lt;/code&gt;的功能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;以规律性来说
&lt;ul&gt;
&lt;li&gt;一个损坏的&lt;code&gt;Barrel&lt;/code&gt;可以由一个相同的部分来代替，&lt;code&gt;Stock&lt;/code&gt;可以在装配线上高效地建造，而不是费力地手工&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Digital Abstraction&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Most physical variables are continuous. For example, the voltage on a wire, the frequency of an oscillation, or the position of a mass are all continuous quantities. Digital systems, on the other hand, represent information with discrete-valued variables—that is, variables with a finite number of distinct values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于数字系统(digital system)，其用离散值变量表示信息的——即具有有限个不同值的变量。&lt;/p&gt;
&lt;p&gt;而不同于&lt;code&gt;Babbage&apos;s machine&lt;/code&gt;，大多数电子计算机采用二进制(two-valued)表示，其中高电压表示&lt;code&gt;1&lt;/code&gt;，低电压表示&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404101756624.png&quot; alt=&quot;Babbage&apos;s machine&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设信息量为$D$(the amount of information D)，那么$N$个不同状态的离散值变量中的信息量$D$以比特(bit)为单位进行度量:&lt;/p&gt;
&lt;p&gt;$$
D = \log_2N \ bits
$$&lt;/p&gt;
&lt;p&gt;所以，一个二进制变量传递的信息量为$\log_22 = 1 \ bit$(事实上，&lt;code&gt;bit&lt;/code&gt;这一单位就是由&lt;code&gt;binary digit&lt;/code&gt;的缩写)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个连续的信号理论上包含无穷的信息量，因为它可以取无穷多个值&lt;/strong&gt;(&lt;strong&gt;A continuous signal theoretically contains an infinite amount of information because it can take on an infinite number of values.&lt;/strong&gt;)。 但在实际应用中，&lt;strong&gt;对于大多数连续信号，噪声和测量误差将信息限制在&lt;/strong&gt;$10 \thicksim 16 \ bits$。如果你想要快速传递信息，那么该信息量需要更低(比如说$8 \ bits$)。&lt;/p&gt;
&lt;p&gt;本书关注的是使用二进制变量：&lt;code&gt;1&lt;/code&gt;和&lt;code&gt;0&lt;/code&gt;的数字电路，而&lt;code&gt;Boole&lt;/code&gt;开发了一种对二进制变量进行操作的逻辑系统，被称为&lt;code&gt;布尔逻辑(Boolean Logic)&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每一个布尔变量都是&lt;code&gt;TRUE&lt;/code&gt;或者&lt;code&gt;FALSE&lt;/code&gt;。电子计算机通常使用正电压表示&lt;code&gt;1&lt;/code&gt;，零电压表示&lt;code&gt;0&lt;/code&gt;。因此，在本书以及本笔记中，我们会同义地使用术语&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;TRUE&lt;/code&gt;和&lt;code&gt;HIGH&lt;/code&gt;；当然&lt;code&gt;0&lt;/code&gt;、&lt;code&gt;FALSE&lt;/code&gt;和&lt;code&gt;LOW&lt;/code&gt;也是如此。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;程序员可以在不需要了解计算机硬件的详细细节的情况下进行工作。另一方面，了解硬件的细节可以让程序员更好地针对特定计算机进行软件优化&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Number Systems&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;对于大多数人工作在十进制系统的情况不同，我们通常会工作在十六进制或者二进制下。所以，不会有人还要被教吧？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Decimal Numbers&lt;/h3&gt;
&lt;p&gt;我们以$K$表示系数，$10$表示基数，$n$代表权重，那么就可以得出十进制的表示方法：&lt;/p&gt;
&lt;p&gt;$$
K_n10^n \cdot K_{n-1}10^{n-1} \cdot ... \cdot K_210^2 \cdot K_110^1 \cdot K_010^0 \cdot K_{-1}10^{-1} \cdot K_{-2}10^{-2} \cdot ... \cdot K_{-n+1}10^{-n+1} \cdot K_{-n}10^{-n}
$$&lt;/p&gt;
&lt;h3&gt;Binary Numbers&lt;/h3&gt;
&lt;p&gt;和十进制一样，不过我们需要把基数换为$2$：&lt;/p&gt;
&lt;p&gt;$$
K_n2^n \cdot K_{n-1}2^{n-1} \cdot ... \cdot K_22^2 \cdot K_12^1 \cdot K_02^0 \cdot K_{-1}2^{-1} \cdot K_{-2}2^{-2} \cdot ... \cdot K_{-n+1}2^{-n+1} \cdot K_{-n}2^{-n}
$$&lt;/p&gt;
&lt;h3&gt;Hexadecimal Numbers&lt;/h3&gt;
&lt;p&gt;不多赘述：&lt;/p&gt;
&lt;p&gt;$$
K_n16^n \cdot K_{n-1}16^{n-1} \cdot ... \cdot K_216^2 \cdot K_116^1 \cdot K_016^0 \cdot K_{-1}16^{-1} \cdot K_{-2}16^{-2} \cdot ... \cdot K_{-n+1}16^{-n+1} \cdot K_{-n}16^{-n}
$$&lt;/p&gt;
&lt;h3&gt;Bytes, Nibbles, and All That Jazz&lt;/h3&gt;
&lt;p&gt;虽然我们了解了数字系统，不过我们也应该了解一些在计算机数字系统中的一些术语。&lt;/p&gt;
&lt;p&gt;一个$8 \ bits$的组被称为$1 \ byte$。其表示范围为：$2^8 = 256$。&lt;strong&gt;存储在计算机内存中的对象大小通常以&lt;code&gt;byte&lt;/code&gt;而不是&lt;code&gt;bit&lt;/code&gt;来衡量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个$4 \ bits$的组，或者说半字节(half a byte)被称为$1 \ nibble$(这应该算是老古董的衡量单位了，现在已经不在使用)。&lt;/p&gt;
&lt;p&gt;微处理器以被称为&lt;code&gt;字(words)&lt;/code&gt;的块来处理数据，&lt;strong&gt;一个&lt;code&gt;word&lt;/code&gt;的大小取决于微处理器的体系结构&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;微处理器是建立在单芯片上的处理器。直到1970&apos;s为止，处理器过于复杂，不再适合单芯片上运行。
在本笔记中，我们将交替使用&lt;code&gt;微处理器&lt;/code&gt;和&lt;code&gt;处理器&lt;/code&gt;两个术语。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在一组&lt;code&gt;bits&lt;/code&gt;中，&lt;code&gt;1&lt;/code&gt;(这里指的是位数，而非是从右到左第一个为1的)所在列的比特位被称为&lt;code&gt;最小有效位数(the least significant bit, lsb)&lt;/code&gt;，而另一端的比特位被称为&lt;code&gt;最大有效位(the most significant bit, msb)&lt;/code&gt;；类似地，在一个&lt;code&gt;word&lt;/code&gt;中，也有&lt;code&gt;最低有效字节(the least significant byte, LSB)&lt;/code&gt;和&lt;code&gt;最高有效字节(the most significant byte, MSB)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404101921449.png&quot; alt=&quot;Least and most significant bits and bytes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;巧合的是，$2^{10} = 1024 \approx 10^3$。因此，术语&lt;code&gt;kilo&lt;/code&gt;表明$2^{10}$。与此类似的是：&lt;code&gt;MB&lt;/code&gt;、&lt;code&gt;Mb&lt;/code&gt;、&lt;code&gt;GB&lt;/code&gt;和&lt;code&gt;Gb&lt;/code&gt;分别表示$2^{20}bytes$、$2^{20}bits$、$2^{30}bytes$和$2^{30}bits$。&lt;/p&gt;
&lt;h3&gt;Binary Addition&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这一块没什么说的，就是正常的计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Signed Binary Numbers&lt;/h3&gt;
&lt;p&gt;我们有两种方法表示有符号二进制数：&lt;code&gt;Sign/Maganitude&lt;/code&gt;和&lt;code&gt;Two&apos;s complement&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;Sign/Magnitude Numbers&lt;/h4&gt;
&lt;p&gt;一个$N-bit$的&lt;code&gt;sign/magnitude&lt;/code&gt;数使用最高符号位作为符号位，剩余的$N-1 \ bits$作为幅度(也就是绝对值)。&lt;/p&gt;
&lt;p&gt;值得注意的是：&lt;strong&gt;正常的二进制加法在&lt;code&gt;sign/magnitude&lt;/code&gt;数中不起作用，同时具有正零和负零之分&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Two&apos;s Complement Numbers&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;二进制补码(Two&apos;s complement)&lt;/code&gt;数克服了&lt;code&gt;sign/magnitude&lt;/code&gt;数的问题，其表示的容量范围为：$[2^{N-1}, 2^{N-1} - 1]$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二进制补码的表示方法分为两类：
&lt;ul&gt;
&lt;li&gt;如果该二进制数是正数(包括0)，那么不变&lt;/li&gt;
&lt;li&gt;如果该二进制数是负数
&lt;ul&gt;
&lt;li&gt;使用1作为充当符号位的数&lt;/li&gt;
&lt;li&gt;其余位正常表示后，除符号位取反加一即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于补码来说，还可以用于检测加减法是否&lt;code&gt;溢出(overflow)&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时，补码还有另外一个特点：&lt;code&gt;符号扩展(signed extension)&lt;/code&gt;，&lt;strong&gt;当一个补码数扩展到更多位时，必须将符号位从当前位置依次复制到&lt;code&gt;msb&lt;/code&gt;上&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;Comparison of Number Systems&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404101943857.png&quot; alt=&quot;Comparison Numbers&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Logic Gates&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Now that we know how to use binary variables to represent information, we explore digital systems that perform operations on these binary variables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;逻辑门(Logic Gates)&lt;/code&gt;是取一个或多个二进制输入并产生一个二进制输出的简单数字电路。逻辑门是用符号表示输入和输出的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404101949233.png&quot; alt=&quot;Gates&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;NOT Gate&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;非门(NOT Gate)&lt;/code&gt;有一个输入$A$和一个输出$Y$。关于非门的描述不做过多介绍，&lt;strong&gt;非门也被称为&lt;/strong&gt;&lt;code&gt;逆变器(inverter)&lt;/code&gt;。本笔记中，我们使用$Y = \overline{A}$作为非门的记号。&lt;/p&gt;
&lt;h3&gt;Buffer&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;缓冲器(buffer)&lt;/code&gt;只是简单的将输入复制到输出。有的人可能会问，“这个有存在的必吗？”。当然的，存在就必有其价值：&lt;strong&gt;从模拟的角度上看，缓冲器可能具有理想的特性，例如能将大量的电流传递给电机等&lt;/strong&gt;。如果单单只是从逻辑上看，缓冲器与导线并没有什么区别。&lt;strong&gt;这就是为什么我们需要从多层次的了解系统的一个例子：数字抽象掩盖了缓冲器的实际目的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;AND Gate&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;与门(AND Gate)&lt;/code&gt;有两个输入和一个输出。我们通常使用$Y = AB$来表示。&lt;/p&gt;
&lt;h3&gt;OR Gate&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;或门(OR Gate)&lt;/code&gt;和与门一致，我们使用$Y = A + B$来表示。&lt;/p&gt;
&lt;h3&gt;Other Two Input Gates&lt;/h3&gt;
&lt;p&gt;至于下面的二输入逻辑门，不再过多赘述，请自行查阅。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102000169.png&quot; alt=&quot;Other Gates&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Beneath The Digital Abstraction&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;一个数字系统使用离散值变量，然而，变量使用连续的物理量来表示的&lt;/strong&gt;。这就导致一些问题：在真实的系统中存在许多噪声，如果假定$5V = 1$，那么$4.97V$也应该表示$A = 1$？$4.3V$呢？或者更一点的值呢？&lt;/p&gt;
&lt;h3&gt;Supply Voltage&lt;/h3&gt;
&lt;p&gt;假设系统中的最低电压为$0V$，其称为&lt;code&gt;groud&lt;/code&gt;或&lt;code&gt;GND&lt;/code&gt;；系统中的最高电压来自于电源，通常被称为$V_{DD}$。&lt;strong&gt;目前，电源电压的值随着时代的发展在逐步降低&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Logic Levels&lt;/h3&gt;
&lt;p&gt;连续变量到离散变量的二元映射是通过定义&lt;code&gt;逻辑层次(Logic Levels)&lt;/code&gt;来完成的。如下图所示，第一个门被称为&lt;code&gt;driver&lt;/code&gt;，而第二个门被称为&lt;code&gt;receiver&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;假设&lt;code&gt;driver&lt;/code&gt;的输出范围为$0 \thicksim V_{OL}$的电压就可以表示输出了&lt;code&gt;LOW(0)&lt;/code&gt;，输出范围为$V_{OH} \thicksim V_{DD}$的电压表示输出了&lt;code&gt;HIGH(1)&lt;/code&gt;;而&lt;code&gt;receiver&lt;/code&gt;接收到范围为$0 \thicksim V_{IL}$的电压表示输入&lt;code&gt;LOW(0)&lt;/code&gt;，接收到范围为$V_{IH} \thicksim V_{DD}$的电压表示输入&lt;code&gt;HIGH(1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于噪声或者故障原件等原因，接收器的输入应该落在范围为$V_{IL} \thicksim V_{IH}$的&lt;code&gt;禁区(forbid)&lt;/code&gt;内，表示门的行为此时是不可预测的&lt;/strong&gt;。现在我们称$V_{OL}$、$V_{OH}$、$V_{IL}$和$V_{IH}$分别为输出和输出的高、低逻辑电平。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102010401.png&quot; alt=&quot;Logic levels and noise margins&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Noise Margins&lt;/h3&gt;
&lt;p&gt;为了保障即使环境内存在故障或噪声，但是其影响不那么大时，逻辑门的功能仍然能够正常运行。我们应该有一个被称为&lt;code&gt;噪声容量(noise margin)&lt;/code&gt;的概念：&lt;strong&gt;在最坏的情况下的输出中可以添加的噪声量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如上图所示，当我们的输入在$V_{OL} \lt NM_{L}$和$V_{OH} \gt NM_{H}$时，接收器的输入仍然能够检测到正确的逻辑电平。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;$V_{DD}$表示金属氧化物半导体晶体管&lt;code&gt;漏极(drain)&lt;/code&gt;上的电压，用于构建大多数现代芯片，有时也被称为$V_{CC}$；
$GND$表示金属氧化物半导体晶体管&lt;code&gt;源极(source)&lt;/code&gt;上的电压，有时也被称为$V_{SS}$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;DC Transfer Characteristics&lt;/h3&gt;
&lt;p&gt;为了理解数字抽象的限度，就必须深入研究门的模拟行为。&lt;code&gt;栅极(gate)&lt;/code&gt;的&lt;code&gt;直流转移特性(direct current transfer characteristics)&lt;/code&gt;将输出电压描述为输入电压的函数，&lt;strong&gt;当输入足够慢时，其输出可以保持不点&lt;/strong&gt;。这一点描述了输入输出的特性，因此被称为特性转移。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102032728.png&quot; alt=&quot;DC transfer characteristics and logic levels&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也就是上图所示，在理想情况当$V(A) \lt V_{DD} / 2$时，$V(Y) = V_{DD}$。此时，$V_{IH} = V_{IL} = V_{DD} / 2$，$V_{OH} = V_{DD}$以及$V_{OL} = 0$。&lt;/p&gt;
&lt;p&gt;但实际上，由于各种原因，这些端点之间的转换是平滑的(也就是说，并非会直接以图a折线的方式转换)，可能并不完全以$V_{DD} / 2$为中心。&lt;/p&gt;
&lt;p&gt;选择逻辑电平的合理位置是传输特性$dV(Y)/dV(A)$的斜率为$-1$时，这两个点被称为&lt;code&gt;单位增益点(unity gain points)&lt;/code&gt;。在单位增益点处选择逻辑电平通常会使噪声容量最大化。&lt;/p&gt;
&lt;h3&gt;The Static Discipline&lt;/h3&gt;
&lt;p&gt;为了避免输入落入禁区，数字逻辑门在设计之初就有着一个准则：&lt;strong&gt;给定逻辑上有效的输入，那么每一个电路元件都会产生逻辑上有效的输出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;数字设计者通过遵循静态规训，牺牲了使用任意模拟电路元件的自由，以换取数字电路的简单性和鲁棒性。(By conforming to the static discipline, digital designers sacrifice the freedom of using arbitrary analog circuit elements in return for the simplicity and robustness of digital circuits.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;$V_{DD}$和逻辑电平的选择使任意的，但所有的通信门都必须具备兼容的逻辑电平&lt;/strong&gt;。因此，门被划分到&lt;code&gt;逻辑家族(logic families)&lt;/code&gt;中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102051690.png&quot; alt=&quot;Logic levels of 5 V and 3.3 V logic families&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;CMOS Transistors&lt;/h2&gt;
&lt;p&gt;现代计算机使用晶体管，因为它们便宜、小巧而且可靠。晶体管是在控制端施加电压或电流时，使开关导通或关闭的电控开关。&lt;/p&gt;
&lt;p&gt;常见的有两种主要类型的晶体管：&lt;code&gt;双极晶体管(bipolar junction transistors)&lt;/code&gt;和&lt;code&gt;金属氧化物半导体效应晶体管(metal-oxide-semiconductor field effect transistors)&lt;/code&gt;，后者一般被叫做&lt;code&gt;MOSFETs&lt;/code&gt;或&lt;code&gt;MOS transistors&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;本节，我们将在数字抽象的下方窥探逻辑门是如何从&lt;code&gt;MOS&lt;/code&gt;中构建出来的。&lt;/p&gt;
&lt;h3&gt;Semiconductors&lt;/h3&gt;
&lt;p&gt;MOS晶体管是由原子硅构成的，硅($Si$)是Ⅳ族原子，因此其价电子层有四个电子，并与相邻的四个原子形成键，从而形成晶格。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;就其本身而言，硅是一种不良导体，因为所有的电子都是以共价键结合在一起的。当仔细地添加少量的杂质，即掺杂原子时，它就会成为一个更好的导体&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果加入Ⅴ族掺杂剂(比如$As$)，掺杂剂原子有一个不参与成键的额外电子(free electron)。电子可以很容易的绕过晶格移动，留下一个电离的掺杂原子($As^{+}$)，如下图b所示。由于这个自由电子携带的是负电(Negative charge)，因此我们称为&lt;code&gt;n-type掺杂剂&lt;/code&gt;，而构建的半导体被称为&lt;code&gt;n-type半导体&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;另一方面，如果加入Ⅲ族掺杂剂(比如$B$)，掺杂剂原子缺少一个电子，这个缺失的电子被称为&lt;code&gt;空穴(hole)&lt;/code&gt;。一个来自邻近硅原子的电子可以移动过来填充缺失的键，形成一个离子化的掺杂原子($B^{-}$)，并在该邻近硅原子处留下一个空穴。空穴可以在晶格周围移动，并且，空穴是缺乏负电荷的，因此其就像一个带正电荷的粒子。因此，我们将该掺杂剂称为&lt;code&gt;p-type掺杂剂&lt;/code&gt;，而构建的半导体称为&lt;code&gt;p-type半导体&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;由于硅的电导率随着掺杂剂的浓度变化而在多个数量级上变化，因此硅被称为半导体&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102109913.png&quot; alt=&quot;Silicon lattice and dopant atoms&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Diodes&lt;/h3&gt;
&lt;p&gt;现在我们已经知道：$n-type$半导体中有游离的电子，而$p-type$半导体中有游离的空穴。&lt;/p&gt;
&lt;p&gt;当我们将$n-type$和$p-type$连接时，由于材料中载流子(空穴和电子)的浓度不同，会发生载流子的扩散。具体来说，$p-type$中的空穴会向$n-type$扩散，而$n-type$中的电子会向$p-type$扩散。这种扩散导致了在&lt;code&gt;PN结(PN junction)&lt;/code&gt;附近形成了一个带电荷的区域。&lt;/p&gt;
&lt;p&gt;在$PN$结形成的过程中，当$p-type$中的空穴与$n-type$中的电子重新结合时，会产生正负电荷相互抵消的情况，形成一个无载流子的区域，这被称为耗尽区。&lt;strong&gt;在耗尽区内部，形成了一个内建电场，内建电场阻止了进一步的载流子扩散，因此PN结具有一定的单向导电性质&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，只有当外加电压且$P$区加正电、$N$区加负电(外加电压大于内建电场电压)时，自由电子才会远远不断的流向$P$区，就产生了从$P \longrightarrow N$的电流。&lt;strong&gt;如果反向加电压，那么会给内建电压助力，如果反向电压过高，那么内场就会被击穿&lt;/strong&gt;。因此，二极管具有单向导电性。下图中的二极管图片很形象的描述了这一性质。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102130507.png&quot; alt=&quot;diodes&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102139188.png&quot; alt=&quot;p-n junction and capacitor&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Capacitors&lt;/h3&gt;
&lt;p&gt;电容器由绝缘体隔开的两个导体组成。当对其中一个导体施加电压$V$时，该导体积聚电荷$Q$，另一个导体积聚相反的电荷$-Q$。电容器的电容$C$为电荷与电压的比值：&lt;/p&gt;
&lt;p&gt;$$
C = \frac{Q}{V}
$$&lt;/p&gt;
&lt;p&gt;电容的符号如上图所示。&lt;strong&gt;电容很重要，因为对导体充电或放电需要时间和能量。更多的电容意味着电路将更慢，需要更多的能量来运行&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;nMOS and pMOS Transistors&lt;/h3&gt;
&lt;p&gt;我们先来了解一下$n-type$的制作过程和原理。&lt;/p&gt;
&lt;p&gt;如下图所示，$nMOS$的绝大部分以&lt;code&gt;p-type&lt;/code&gt;作为&lt;code&gt;底物(substrate)&lt;/code&gt;，在底物的两侧有两个&lt;code&gt;n-type&lt;/code&gt;，在底物的上方覆盖一层二氧化硅绝缘体，再在其上方填充一层&lt;code&gt;多晶硅(Polysilicon)&lt;/code&gt;。我们使用金属导体分别在两个&lt;code&gt;n-type&lt;/code&gt;和多晶硅上引出金属电极。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102152357.png&quot; alt=&quot;nMOS and pMOS transistors&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中一个被称为&lt;code&gt;源极(source)&lt;/code&gt;，另一个称为&lt;code&gt;漏极(drain)&lt;/code&gt;；由多晶硅引出的金属导体被称为&lt;code&gt;栅极(gate)&lt;/code&gt;。底物通常接地也就是&lt;code&gt;GND&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们发现，$nMOS$的两个&lt;code&gt;n-type&lt;/code&gt;和底物&lt;code&gt;p-type&lt;/code&gt;刚好形成两个方向相反二极管。这就导致了，&lt;strong&gt;当栅极接地时，其中一个二极管是导通的，另外一个二极管就是截止的。因此，源极和漏极不导通，无电源流通；当栅极连接电源时，由于栅极现在积攒正电荷，而中间有一层绝缘体，因此在下方的$p-type$底物上就会吸引负电荷累积将空穴赶走，当自由电子累积的足够多时，就形成了如下图b展示的&lt;code&gt;通道(channel)&lt;/code&gt;，这里是$n-channel$。现在就有了一条从$n-type$源极开始途径$n-channel$到$n-type$漏极的连续的通道，电流就可以从源极流向漏极，此时该$nMOS$就是导通的，也就是$ON$的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404102205244.png&quot; alt=&quot;nMOS transistor operation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而$pMOS$的制作和原理仅仅是$n-type$换为$p-type$即可，&lt;strong&gt;当栅极接地时，源极和漏极导通；当栅极连接电源时，源极和漏极截止&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，$MOS$晶体管的&lt;strong&gt;表现为电压控制开关，其中栅极电压产生电场，使源极和漏极之间的连接导通或关断。&lt;code&gt;效场应晶体管(field effect transistor)&lt;/code&gt;就来源于这一工作原理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;晶体管开启所需的栅极电压称为&lt;/strong&gt;&lt;code&gt;阈值电压(threshold voltage, $V_t$)&lt;/code&gt;，通常为$0.3 \thicksim 0.7V$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不幸地是&lt;/strong&gt;，&lt;code&gt;MOSFETs&lt;/code&gt;&lt;strong&gt;并非是完美的&lt;/strong&gt;&lt;code&gt;开关(switches)&lt;/code&gt;。以下是其的一些主要原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导通状态的抗阻问题：
&lt;ul&gt;
&lt;li&gt;在开关应用中，理想情况下希望开关关闭时有很高的阻抗以防止电流流动，而开关打开时有很低的阻抗以便电流通过。&lt;code&gt;MOSFETs&lt;/code&gt;在导通时有相对较低的导通阻抗，但它们在关闭时的阻抗可能相对较高，这可能会导致一些能量损失&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态功耗
&lt;ul&gt;
&lt;li&gt;即使&lt;code&gt;MOSFET&lt;/code&gt;处于关闭状态，仍然存在漏电流，这可能导致一些静态功耗。虽然这种功耗相对较小，但在某些应用中可能是一个问题，尤其是对于需要长时间保持关闭状态的应用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;体效应
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MOSFET&lt;/code&gt;的导通状态是通过施加栅极电压来控制的，这可能受到称为“体效应”的影响。当栅极电压较低时，由于电荷积累在栅极和沟道之间的绝缘层中，MOSFET的阻抗可能会增加，这可能会导致开关性能不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;电压和电流的关系
&lt;ul&gt;
&lt;li&gt;在某些情况下，&lt;code&gt;MOSFET&lt;/code&gt;的导通特性可能不完全符合理想的线性关系。这可能导致在特定电压下，电流并不严格地按照预期的方式变化，这可能会导致一些设计上的挑战&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了在同一芯片上同时构建两种类型的晶体管，制造过程通常以一个$pMOS$开始，然后在$pMOS$会途径的地区植入被称为&lt;code&gt;well&lt;/code&gt;(实际上我不知道该怎么翻译)的$nMOS$。&lt;strong&gt;这种同时提供两种不同类型的晶体管的这些工艺被称为&lt;/strong&gt;&lt;code&gt;互补金属氧化物半导体(Complementary MOS)&lt;/code&gt;或&lt;code&gt;CMOS&lt;/code&gt;。$CMOS$工艺被用于构建当今制造的所有晶体管中的绝大多数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404111050319.png&quot; alt=&quot;Switch moduels of MOSFETs&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;CMOS NOT Gate&lt;/h3&gt;
&lt;p&gt;由下图中的左图可以看出，如果$A = 0$，$N1$是断开的而$P1$是导通的，因此$Y$就会和$V_{DD}$导通呈现&lt;code&gt;上拉(pulled-up)&lt;/code&gt;逻辑&lt;code&gt;1&lt;/code&gt;；相反的，$A = 0$，$Y$就会与$GND$导通呈现&lt;code&gt;下拉(pulled-down)&lt;/code&gt;逻辑&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404111055909.png&quot; alt=&quot;CMOS Gates&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Other CMOS Logic Gates&lt;/h3&gt;
&lt;p&gt;上图中的中间一图展示了二输入的$NAND$门，但是我在这里就不再过多赘述。&lt;/p&gt;
&lt;p&gt;最右图展示了&lt;strong&gt;用于构建任意反转逻辑门的一般形式&lt;/strong&gt;(如$NOT$, $NAND$或$NOR$)。&lt;/p&gt;
&lt;p&gt;$nMOS$晶体管善于传递&lt;code&gt;0&lt;/code&gt;(这里是如何理解呢，是因为当输入为$A=1$时，$nMOS$中的$n-channel$形成，内部是传递&lt;code&gt;0&lt;/code&gt;的)，因此在输出$Y$和$GND$之间放置了一组$nMOS$晶体管的下拉网格，以将输出拉低为&lt;code&gt;0&lt;/code&gt;。$pMOS$晶体管善于传递&lt;code&gt;1&lt;/code&gt;，因此在输入$V_{DD}$和输出$Y$之间放置了一组$pMOS$晶体管的上拉网格，以将输出上拉为&lt;code&gt;1&lt;/code&gt;。&lt;strong&gt;网格可以由串联或并联的晶体管组成&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果上拉网格和下拉网格同时导通&lt;/strong&gt;，$V_{DD}$和$GND$之间将发生&lt;code&gt;短路(short circuit)&lt;/code&gt;。&lt;strong&gt;当然，栅极的输出可能处于禁区，那么晶体管造成大量功耗，可能足以烧毁&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另一方面，&lt;strong&gt;如果上拉网格和下拉网格同时断开，则输出既不连接&lt;/strong&gt;$V_{DD}$&lt;strong&gt;也不连接&lt;/strong&gt;$GND$。&lt;strong&gt;这时我们就说输出是&lt;/strong&gt;&lt;code&gt;浮动(floating)&lt;/code&gt;&lt;strong&gt;的，其值是未定义的&lt;/strong&gt;。浮动输出通常是不被希望的，但是在后面我们也会介绍一些关于浮动输出的特例(偶尔也可以作为优势，就比如你的bugs一样)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在一个正常工作的逻辑门中，对于任意给定的时刻，如果其中一个网络是&lt;/strong&gt;$ON$&lt;strong&gt;的，那么另一个网络就应该是&lt;/strong&gt;$OFF$&lt;strong&gt;的，这样输出就可以被拉高或拉低，而不是短路或浮动的&lt;/strong&gt;。我们可以通过&lt;code&gt;导通互补规则(the rule of conduction complements)&lt;/code&gt;来保证这一点：&lt;strong&gt;当$nMOS$晶体管串联时，$pMOS$晶体管必须并联；当$pMOS$晶体管并联时，$pMOS$晶体管必须串联&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;Transmission Gates&lt;/h3&gt;
&lt;p&gt;有时，设计者发现使用一个能够良好传递&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;的理想开关很方便。回顾一下，$nMOS$晶体管擅长传递&lt;code&gt;0&lt;/code&gt;，而$pMOS$晶体管擅长传递&lt;code&gt;1&lt;/code&gt;， 因此两者的并联组合可以很好地传递这两个值。下图中的左图显示了这样一个电路，称为&lt;code&gt;传输们(Transimission Gate)&lt;/code&gt;或&lt;code&gt;通门(pass gate)&lt;/code&gt;。 开关的两侧称为$A$和$B$，&lt;strong&gt;因为开关是双向的，没有首选输入或输出侧。控制信号称为&lt;/strong&gt;&lt;code&gt;使能(enables)&lt;/code&gt;，$EN$和$\overline{EN}$。&lt;strong&gt;当$EN = 0$且$\overline {EN} = 1$时，两个晶体管都关闭。因此，传输门处于关闭或禁用状态，因此$A$和$B$未连接。当$EN = 1$且$\overline{EN} = 0$时，传输门处于打开或启用状态，任何逻辑值都可以在$A$和$B$之间流动&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hexo-pirctures.oss-cn-chengdu.aliyuncs.com/imgs202404111151042.png&quot; alt=&quot;transimission gate and pseudo nMOS&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Pseudo-nMOS Logic&lt;/h3&gt;
&lt;p&gt;一个$N$输入的$CMOS$非门使用$N$个$nMOS$晶体管并联和$N$个$pMOS$晶体管串联。事实上，&lt;strong&gt;串联的晶体管的速度会比并联的晶体管速度慢，因为串联的电阻比并联的电阻阻值更大&lt;/strong&gt;。不仅如此，&lt;strong&gt;由于空穴并不能像自由电子一样快速地在硅晶格周围移动，因此$pMOS$的速度也比$nMOS$的速度慢&lt;/strong&gt;。这样下来，&lt;strong&gt;并联的$nMOS$是要比串联的$pMOS$速度更快的，尤其是当许多晶体管串联时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;因此，我们就引入了&lt;code&gt;伪-$nMOS$逻辑(pseudo-nMOS logic)&lt;/code&gt;，&lt;strong&gt;用一个始终处于开启状态的单个&lt;/strong&gt;&lt;code&gt;弱$pMOS$(week $pMOS$)&lt;/code&gt;&lt;strong&gt;晶体管来代替一组速度较慢的$pMOS$晶体管&lt;/strong&gt;，如上图中的中间图例所示。这个$pMOS$晶体管通常被称为&lt;code&gt;弱上拉(week pulled-up)&lt;/code&gt;。由于被选择的$pMOS$尺寸的物理特性，使得其微弱地将输出$Y$拉高，&lt;strong&gt;即仅当没有任何$nMOS$晶体管处于导通时，该弱上拉会使得输出$Y$产生逻辑&lt;/strong&gt;&lt;code&gt;1&lt;/code&gt;。&lt;strong&gt;但是，一旦有任何一个$nMOS$晶体管处于导通状态，其就会压倒弱上拉将输出$Y$拉低至产生逻辑&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;伪$nMOS$逻辑的优势在于&lt;strong&gt;可以用于构建多输入的快速异或门&lt;/strong&gt;，例如上图中的有图所示。更多的优势我们将在后续的笔记中描述。而其缺点是&lt;strong&gt;当输出为低电平时，$V_{DD}$和$GND$之间存在短路；弱$pMOS$和$nMOS$晶体管都会处于导通状态，这种短路会持续的吸收功耗，应当谨慎使用&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;值得注意的是：&lt;strong&gt;伪$nMOS$逻辑的出现主要是因为$nMOS$晶体管在早期的工艺中比$pMOS$晶体管易于实现和快速&lt;/strong&gt;。在早期的集成电路设计中，$nMOS$晶体管比$pMOS$晶体管更容易制造，并且具有更高的工作速度和更低的功耗。因此，工程师们采用了伪$nMOS$逻辑设计来构建数字逻辑电路。伪$nMOS$逻辑电路使用$nMOS$晶体管作为开关，而$pMOS$晶体管仅用于电路的拉高操作，从而提高了电路的速度和性能。
&lt;strong&gt;虽然$pMOS$晶体管的速度相对较慢是一种因素，但并非是伪$nMOS$逻辑出现的唯一原因&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Power Consumption&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;功耗(power consumption)&lt;/code&gt;&lt;strong&gt;是单位时间内所消耗的能源量&lt;/strong&gt;。&lt;strong&gt;在现代架构中，如何减少功耗是一个巨大的难题，同时也在数字系统中占据了非常重要的地位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;数字系统会同时消耗&lt;code&gt;动态功率(dynamic power)&lt;/code&gt;和&lt;code&gt;静态功率(static power)&lt;/code&gt;。&lt;strong&gt;动态功率是在信号&lt;/strong&gt;&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;&lt;strong&gt;之间变化时用于充电电容的功率；静态功率是即使在信号未发生变化或系统处于空闲状态时也会消耗的功率&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;逻辑门极其连接它们的导线具有电容。从电源抽出能量为电容$C$充电至电压$V_{DD}$的能量是$CV_{DD}^2$。如果电容器上的电压以频率$f$(即每秒$f$次)切换，则每秒它充电$f/2$次和放电$f/2$次。放电不会从电源中消耗能力，因此动态功耗为：&lt;/p&gt;
&lt;p&gt;$$
P_{dynamic} = \frac{1}{2}CV_{DD}^2f
$$&lt;/p&gt;
&lt;p&gt;电子系统即使在空闲时也会吸收一些电流。当晶体管关闭时，它们会泄漏小部分电流，比如上一节讨论的伪$nMOS$门，有一条从$V_{DD}$到$GND$的通路，通过该路径电流持续流动。总静态电流$I_{DD}$也被称为&lt;code&gt;泄漏电流(leakage current)&lt;/code&gt;或&lt;code&gt;静态电源电流(quiescent supply current)&lt;/code&gt;，其流动在$V_{DD}$和$GND$之间。静态功耗与这个静态电流成正比：&lt;/p&gt;
&lt;p&gt;$$
P_{static} = I_{DD}V_{DD}
$$&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;本篇笔记介绍了一些理解和设计复杂系统的基本原理，逻辑门通常由$CMOS$晶体管构成，其表现为电控开关。$nMOS$在栅极为&lt;code&gt;1&lt;/code&gt;时导通，而$pMOS$在栅极为&lt;code&gt;0&lt;/code&gt;时导通。&lt;/p&gt;
</content:encoded></item></channel></rss>