<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>快乐的春饼</title><link>https://konglils.org/</link><description>春饼的最近更新</description><generator>Hugo</generator><language>zh-CN</language><copyright>© 2026 快乐的春饼</copyright><lastBuildDate>Thu, 22 Oct 2026 15:34:18 +0800</lastBuildDate><atom:link href="https://konglils.org/index.xml" rel="self" type="application/rss+xml"/><item><title>xv6 学习笔记：Lab Traps</title><link>https://konglils.org/posts/xv6-lab-traps/</link><pubDate>Wed, 21 Oct 2026 15:34:18 +0800</pubDate><guid>https://konglils.org/posts/xv6-lab-traps/</guid><description>MIT 6.1810 xv6 Lab Traps 学习笔记：学习用户程序陷入内核的方式，操作系统如何处理中断，如何将 RISC-V 汇编嵌入 C 语言中，分析了系统调用的过程，分享了练习的思路和代码。</description><content:encoded>&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/labs/traps.html" target="_blank"&gt;Lab Traps&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="学习"&gt;学习&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;xv6 book 第 4 章&lt;/li&gt;
&lt;li&gt;kernel/trap.c, kernel/trampline.S&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="riscv"&gt;riscv&lt;/h2&gt;
&lt;p&gt;在 &lt;a href="https://konglils.org/posts/xv6-lab-syscall"&gt;xv6 学习笔记：Lab System calls&lt;/a&gt; 中介绍了几个寄存器。要处理 trap 的话，还要使用以下寄存器：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;stvec&lt;/strong&gt; (Supervisor Trap Vector Base Address Register) 寄存器被内核写入 trap 处理程序的地址；RISC-V跳转到这里来处理trap。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sscratch&lt;/strong&gt; (Supervisor Scratch Register) 寄存器被内核放置了一个值，在trap处理程序开始时可以方便地使用。&lt;/p&gt;
&lt;h2 id="riscv-寄存器"&gt;riscv 寄存器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;reg | name | saver | description
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-------+-------+--------+------------
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x0 | zero | | hardwired zero
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x1 | ra | caller | return address
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x2 | sp | callee | stack pointer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x3 | gp | | global pointer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x4 | tp | | thread pointer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x5-7 | t0-2 | caller | temporary registers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x8 | s0/fp | callee | saved register / frame pointer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x9 | s1 | callee | saved register
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x10-11 | a0-1 | caller | function arguments / return values
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x12-17 | a2-7 | caller | function arguments
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x18-27 | s2-11 | callee | saved registers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x28-31 | t3-6 | caller | temporary registers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pc | | | program counter
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="系统调用过程"&gt;系统调用过程&lt;/h2&gt;
&lt;p&gt;以 exec 系统调用为例，进程调用 exec 函数，首先会在 usys.S 中找到 exec 对应的代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;.global&lt;/span&gt; &lt;span class="no"&gt;exec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;exec:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;li&lt;/span&gt; &lt;span class="no"&gt;a7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SYS_exec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;ecall&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;ret&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;SYS_exec&lt;/code&gt;是 exec 的系统调用号，它被加载到 a7 寄存器中。然后调用 ecall。&lt;/p&gt;
&lt;p&gt;ecall 会读取 stvec 寄存器并将 pc 设为其值。而 stvec 在之前就已经被设置成了 uservec 的地址。所以 ecall 后，就会执行 uservec 函数。同时，ecall 也使得机器从 U-mode 转为 S-mode。ecall 触发 trap，RISC-V 总是在开始使用 trap 时通过清除 sstatus 的 SIE 位来禁用中断。&lt;/p&gt;
&lt;p&gt;uservec 定义在 trampoline.S 中。uservec 首先把各个寄存器的值存到 TRAPFRAME 页中。注意此时虽然以 S-mode 运行，但仍然使用进程页表而非内核页表。然后，uservec 把内核栈指针、hartid、trap 处理程序 usertrap 的地址、内核页表地址（在 TRAPFRAME 保存的 satp 中）加载到寄存器里。然后刷新 TLB 缓存，写入装入了内核页表地址的 satp，切换到内核页表。然后跳转到 usertrap。&lt;/p&gt;
&lt;p&gt;usertrap 只处理 U-mode 的 trap，所以开头会检查 sstatus 的 SPP 位，确保 trap 来自 U-mode。因为现在已经在内核中了，所以把 stvec 改为 kernelvec 的地址，用来处理内核的中断或异常。并保存进程的程序计数器。然后检查 scause 是否为 8 (Environment call from U-mode)，即系统调用（执行 ecall）产生了这个 trap。如果是，则调用 syscall 函数，执行具体的系统调用代码。执行完系统调用后，再依次反向做上面的动作，包括写 stvec 为 uservec 的地址，写 trapframe 的内核相关字段，把 sstatus 的 SPP 为设为 0（这样 sret 的时候就一定返回到 U-mode），然后把 sepc 设为 trapframe 里保存的 epc，最后返回装入了用户页表地址的新 satp（将其保存在 a0 寄存器中）。&lt;/p&gt;
&lt;p&gt;注意 uservec 最后一条指令&lt;code&gt;jalr t0&lt;/code&gt;，这条指令不仅调用了 usertrap，还使得 usertrap 返回后会跳到 jalr 的下一条指令，即 userret 处执行。usertrap 返回后切换到进程页表，把 trapframe 里的字段都取回各个寄存器。最后通过 sret 返回到 sepc 指定的地址，并切回 U-mode。&lt;/p&gt;
&lt;h2 id="练习"&gt;练习&lt;/h2&gt;
&lt;h3 id="backtrace"&gt;Backtrace&lt;/h3&gt;
&lt;p&gt;栈帧的布局：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Stack
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +-&amp;gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | +-----------------+ |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | return address | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | previous fp ------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | saved registers |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | local variables |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | ... | &amp;lt;-+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | +-----------------+ |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | return address | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +------ previous fp | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | saved registers | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | local variables | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +-&amp;gt; | ... | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | +-----------------+ |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | return address | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | previous fp ------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | saved registers |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | local variables |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | ... | &amp;lt;-+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | +-----------------+ |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | | return address | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +------ previous fp | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | saved registers | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | local variables | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $fp --&amp;gt; | ... | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +-----------------+ |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | return address | |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | previous fp ------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | saved registers |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $sp --&amp;gt; | local variables |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +-----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意指针类型的加减法和整数类型的不一样。下面第二种写法是错的。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;uint64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="alarm"&gt;Alarm&lt;/h3&gt;
&lt;p&gt;理清思路很重要，代码可以不用很多。&lt;/p&gt;
&lt;p&gt;sepc 是关键的寄存器。修改它就能控制中断返回之后从哪里开始执行代码。&lt;/p&gt;
&lt;p&gt;alarm 的作用是在一个进程使用一定的 CPU 时间后，调用进程指定的 handler 发出警报。这意味着，不论这一进程是否获得了充足的 CPU 时间，都以它获得的实际 CPU 时间计算是否发出警报。clockintr 表明每隔约 0.1s，ticks 就会+1，并触发时钟中断。所以，每个进程每次进入时钟中断就意味着该进程又使用了 1 tick 的 CPU 时间。&lt;/p&gt;
&lt;p&gt;alarm handler 运行时，如果运行时间长，可能会再次触发 alarm，这种情况不能出现。&lt;/p&gt;
&lt;p&gt;执行 alarm handler 时，handler 也有自己的栈帧，会修改寄存器的值。特别是，handler 运行的时候也有可能陷入中断，uservec 会无条件把 handler 相关的寄存器保存到 trapframe 中，从而导致原有 trapframe 丢失。所以，要将发生 alarm 时的 trapframe 另外保存下来。同样可以保存在进程的 TRAMPLINE 页中，那里还有很多空间未使用。当 handler 调用 sigreturn 时，把原来的 trapframe 复制回原处。&lt;/p&gt;
&lt;h2 id="我的代码"&gt;&lt;a href="https://github.com/konglils/xv6-labs-2026/tree/traps" target="_blank"&gt;我的代码&lt;/a&gt;&lt;/h2&gt;</content:encoded></item><item><title>xv6 学习笔记：Lab Page tables</title><link>https://konglils.org/posts/xv6-lab-pgtbl/</link><pubDate>Mon, 19 Oct 2026 10:35:36 +0800</pubDate><guid>https://konglils.org/posts/xv6-lab-pgtbl/</guid><description>MIT 6.1810 xv6 Lab Page tables 实验笔记：学习页表的作用和运行方式，介绍使用页表的关键寄存器 satp 的用途，附带了 xv6 book 中有关地址转换的插图，分享了练习的思路和代码。</description><content:encoded>&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/labs/pgtbl.html" target="_blank"&gt;Lab Page tables&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="学习"&gt;学习&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;xv6 book 第 3 章&lt;/li&gt;
&lt;li&gt;kernel/vm.c, kernel/exec.c&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf" target="_blank"&gt;RISC-V privileged instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://drive.google.com/file/d/17GeetSnT5wW3xNuAHI95-SI1gPGd5sJ_/view?usp=drive_link" target="_blank"&gt;RISC-V privileged architecture manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="页表"&gt;页表&lt;/h2&gt;
&lt;p&gt;页表是现代操作系统用来管理内存的方式。通过页表，操作系统可以给每个进程独立的虚拟地址空间，并映射到物理内存上。这意味着同一个虚拟地址，对不同的进程，映射到的物理地址可能是不同的。&lt;/p&gt;
&lt;p&gt;页是操作系统分配内存的基本单位，一页的大小为 4 KB，即 4096 字节。&lt;/p&gt;
&lt;p&gt;每个进程有独立的虚拟地址空间，意味着每个进程要独立地保存一组虚拟地址到物理地址的映射表。这个映射表大小也刚好是一页，叫作页表。作为页表的页称为页表页。每个页表由 512 个 8 字节的 PTE （Page Table Element，页表项）组成，每个 PTE 包含了一个 PPN（Physical Page Number，物理页号，指向一个物理页），以及一些控制位（比如这一页是否有效、是否可读、是否可写）。如果读取 PTE 的有效位的时候发现该页无效，就不再读取物理页了，引发缺页异常。&lt;/p&gt;
&lt;p&gt;到目前为止，已存在页表的情况下，进程要想通过一个虚拟地址访问精确到字节的内存数据，需要把虚拟地址分成两部分，一是 PTE 的索引，二是页内偏移量。每个页表都有 512 个 PTE，意味着可以用 log&lt;sub&gt;2&lt;/sub&gt; 512 = 9 位表示使用哪个 PTE。使用 log&lt;sub&gt;2&lt;/sub&gt; 4096 = 12 位表示页内偏移量。所以现在一个虚拟地址由 9 + 12 = 21 位组成，可以索引 2&lt;sup&gt;21&lt;/sup&gt; = 2 MB 内存。&lt;/p&gt;
&lt;p&gt;2 MB 对现代程序来说可能太小了。要增加程序可索引内存大小，只需要再增加一级页表。也就是说，原来程序拥有的那个页表，不再直接映射到实际存放数据的页，而是映射到另一些页表，这些页表才映射到存放数据的页。这样一来，虚拟地址就要增加 9 位用来索引中间这一层 PTE，于是程序就可以索引 2&lt;sup&gt;30&lt;/sup&gt; = 1 GB 内存了。以此类推，存在更多级的页表：三级页表需要 39 位虚拟地址，可以索引 512 GB 内存；四级页表 48 位，256 TB；五级页表 57 位，128 PB。&lt;/p&gt;
&lt;h2 id="satp"&gt;satp&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;satp&lt;/strong&gt; (supervisor address translation and protection) 寄存器，控制 S-mode 下的地址转换和保护。该寄存器存放了根页表的 PPN。MODE 指定了是否启用地址转换。启用地址转换前，内核代码访问的地址都是物理地址；启用地址转换后，不论是内核还是用户程序，访问的地址都是虚拟地址，如何映射由页表决定。xv6 在启用地址转换前会先运行一小段初始化代码，然后才启用地址转换。在 S-mode 和 U-mode 中 satp 才会激活。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/satp-layout.png" alt="satp 布局" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/satp-mode.png" alt="satp mode" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;h2 id="地址转换"&gt;地址转换&lt;/h2&gt;
&lt;p&gt;xv6 运行在 Sv39 RISC-V 上（Sv 指 Supervisor-level virtual-memory），意味着每个虚拟地址只用到 39 位，使用 9 * 3 = 27 位索引三级 PTE，使用 12 位索引页内偏移量。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/page-table.png" alt="页表" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/addr-trans.png" alt="RISC-V 地址转换" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;xv6 为每个进程维护一个用于描述进程的用户地址空间的页表，外加一个单独的描述内核地址空间的页表。&lt;/p&gt;
&lt;p&gt;xv6 内核的大部分地址是直接映射，除了 TRAMPLINE 页和内核栈页。&lt;/p&gt;
&lt;p&gt;内核地址映射：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/kernel-addr.png" alt="内核地址空间映射" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;用户程序地址映射：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/user-addr.png" alt="用户程序地址映射" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;h2 id="练习"&gt;练习&lt;/h2&gt;
&lt;h3 id="walk-and-print-a-two-level-page-table"&gt;Walk and print a two-level page table&lt;/h3&gt;
&lt;p&gt;用递归深入每一层页表。使用 riscv.h 文件结尾定义的宏和常量。位运算的时候注意整数的类型。&lt;/p&gt;
&lt;h3 id="detect-which-pages-have-been-accessed"&gt;Detect which pages have been accessed&lt;/h3&gt;
&lt;p&gt;需要定义一个缓冲区来存放结果，然后 copyout 到用户程序的第三个参数。检查 PTE_A 是否已设置后，要清除 PTE_A。否则，将无法确定自上次调用 pgaccess() 以来是否访问了页面（即，该位将被永久设置）。&lt;/p&gt;
&lt;h3 id="speed-up-the-getpid-system-call"&gt;Speed up the getpid system call&lt;/h3&gt;
&lt;p&gt;模仿 TRAPFRAME 页是如何被创建和回收的。在程序分配了 pid 后，把 pid 也复制到 USYSCALL 页里。&lt;/p&gt;
&lt;h3 id="use-superpages"&gt;Use superpages&lt;/h3&gt;
&lt;p&gt;查阅 RISC-V privileged architecture manual 第 12.3.2 节（图片是 Sv32，不过 Sv39 也大同小异）：&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-pgtbl/va-trans.png" alt="虚拟地址转换" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;可以发现，大页（2M页）相比小页最主要的区别就是 pte 在 level=1 的时候就是叶子 pte 了。并且叶子 pte 的 ppn 的低 9 位必须是 0，意味着大页的物理地址必须 2M 对齐，否则会 page fault。&lt;/p&gt;
&lt;p&gt;这个练习让我们把 kvmmake 函数里的映射都改成尽量使用大页。要映射大页，虚拟页和物理页都要 2M 对齐。具体方法是，不论虚拟地址还是物理地址，都按 2M 对齐的方式找到一块最大的连续地址空间，然后取二者中的较小的大小，就得到了最多多少字节能使用大页。再按 2M 对齐计算哪些地址用大页。其他地址用小页。&lt;/p&gt;
&lt;p&gt;如果不正确设置内核页表，会发生十分有趣的现象。我之前没注意到设置大页时，物理页也要 2M 对齐，否则使用页表进行地址转换的时候，会引发 page fault。关键在于，page fault 发生在哪里？我写的第一版代码运行后会卡在“xv6 kernel is booting”之后。我在每两条语句之间插入了 printk，这样我就知道具体是哪里卡住了。我注意到 kvminithart 函数调用 w_satp 之前的 printk 都打印出来了，后面的 printk 都没打印。我知道是我的代码引发了问题，但是很疑惑为什么会卡在写 satp 这条汇编语句上（甚至一度以为 RISC-V 在开启地址转换之后会检查页表是否正确，事实证明一条汇编语句做不了那么多事情）。后来发现原来是 printk 本身也被错误的页表影响了。printk 在启用地址转换之前，就已经可以通过物理地址访问硬件终端，从而打印内核消息了。启用地址转换之后，printk 访问的地址都是虚拟地址了。地址转换后运行的第一段代码就是 printk，所以 page fault 发生在 printk 内部！调试的工具本身被 bug 影响了。&lt;/p&gt;
&lt;h2 id="我的代码"&gt;&lt;a href="https://github.com/konglils/xv6-labs-2026/tree/pgtbl" target="_blank"&gt;我的代码&lt;/a&gt;&lt;/h2&gt;</content:encoded></item><item><title>xv6 学习笔记：Lab System calls</title><link>https://konglils.org/posts/xv6-lab-syscall/</link><pubDate>Mon, 12 Oct 2026 22:03:18 +0800</pubDate><guid>https://konglils.org/posts/xv6-lab-syscall/</guid><description>MIT 6.1810 xv6 Lab System calls 实验笔记：记录了 gdb 调试方法、riscv 的三种模式和若干寄存器解释、kernel panic 原因分析与定位。分享了练习思路，附代码仓库。</description><content:encoded>&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/labs/syscall.html" target="_blank"&gt;Lab System calls&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个 lab 与上一个正好互补。上一个 lab 中，我们通过预定义的几个系统调用编写了一些用户态程序，在这个 lab 中，我们要编写一个系统调用。&lt;/p&gt;
&lt;h2 id="学习"&gt;学习&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;xv6 book 第 2 章，以及第 4 章的 4.3 和 4.4 节，第 3 章也先看一下&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf" target="_blank"&gt;RISC-V privileged instructions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="gdb"&gt;GDB&lt;/h2&gt;
&lt;p&gt;做操作系统实验需要用 gdb 调试程序。需要调试程序的时候，打开一个终端窗口，在 xv6 目录下输入&lt;code&gt;make qemu-gdb&lt;/code&gt;。然后打开另一个终端窗口，在 xv6 目录下，输入&lt;code&gt;gdb&lt;/code&gt;。gdb 窗口可能会给出这样的警告：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⚠️ warning: File &amp;#34;/home/xxx/project/xv6-labs-2026/.gdbinit&amp;#34; auto-loading has been declined by your `auto-load safe-path&amp;#39; set to &amp;#34;$debugdir:$datadir/auto-load&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To enable execution of this file add
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; add-auto-load-safe-path /home/xxx/project/xv6-labs-2026/.gdbinit
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;line to your configuration file &amp;#34;/home/xxx/.config/gdb/gdbinit&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是 gdb 的安全警告，只需要在&lt;code&gt;/home/xxx/.config/gdb/gdbinit&lt;/code&gt;中写入&lt;code&gt;add-auto-load-safe-path /home/xxx/project/xv6-labs-2026/.gdbinit&lt;/code&gt;，然后重新打开 gdb 即可。&lt;/p&gt;
&lt;p&gt;gdb 常用命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;layout src # c源代码视图
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;layout asm # 汇编视图
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;layout split # c 和汇编视图
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backtrace # 打印函数调用链
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;c # continue, 继续运行程序直到下一个断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;n # next, 单步执行，不进入函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;s # step, 单步执行并进入函数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ni # 单步执行一条汇编指令
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;finish # 执行到函数返回点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b *0x80001d20 # 在虚拟地址 0x80001d20 处断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b syscall # 在 syscall 函数开始处断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b syscall.c:146 # 在 syscall.c 的第 146 行处断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;b &amp;lt;位置&amp;gt; if &amp;lt;条件表达式&amp;gt; # 满足条件表达式时才停下来
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info break # 列出所有断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info registers # 查看寄存器的值
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;delete 断点标号 # 删除断点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 查看变量
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;p xxx # 查看变量值
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;p /x xxx # 以十六进制查看变量值
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;p /t xxx # 以二进制查看变量值
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;p $rrr # 查看 rrr 寄存器的值
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;默认调试的是内核。如果要调试用户程序，执行&lt;code&gt;file user/_xxx&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="riscv"&gt;riscv&lt;/h2&gt;
&lt;p&gt;riscv 有三种权限等级: machine-mode (M-mode), supervisor-mode (S-mode), user-mode (U-mode)。M-mode 有最高权限。S-mode 通常用于运行操作系统，能访问物理内存地址和处理设备中断。U-mode 权限最低，用来运行用户程序。&lt;/p&gt;
&lt;p&gt;riscv 机器开机时，用 M-mode 运行。然后运行操作系统代码，切换成 S-mode。此后操作系统就一直在 S-mode 和 U-mode 中交替运行。&lt;/p&gt;
&lt;p&gt;常用的几个寄存器：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sstatus&lt;/strong&gt; (supervisor status) 寄存器记录了处理器当前的操作状态。SPP 位指示了这个 hart (hardware thread，逻辑线程) 在进入 S-mode 之前的特权模式。当 trap 发生的时候，如果 trap 来自 U-mode，SPP 设成 0，否则为 1，并控制 sret 指令返回到什么模式。SIE 位控制设备中断是否被启用。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-syscall/sstatus-layout.png" alt="sstatus 布局" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sepc&lt;/strong&gt; (supervisor exception program counter) 寄存器记录了陷入 S-mode 时发生异常的指令所处的虚拟地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;scause&lt;/strong&gt; (supervisor cause) 寄存器记录了陷入 S-mode 的原因。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-syscall/scause-layout.png" alt="scause 布局" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-syscall/scause-value.png" alt="scause 值" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;stval&lt;/strong&gt; (supervisor trap value) 寄存器提供了特定于异常的信息来辅助软件处理 S-mode 陷入。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://konglils.org/posts/xv6-lab-syscall/stval-layout.png" alt="stval 布局" loading="lazy" decoding="async" /&gt;
&lt;/p&gt;
&lt;h2 id="kernel-panic"&gt;kernel panic&lt;/h2&gt;
&lt;p&gt;把&lt;code&gt;kernel/syscall.c&lt;/code&gt;中函数&lt;code&gt;syscall&lt;/code&gt;的&lt;code&gt;num = p-&amp;gt;trapframe-&amp;gt;a7&lt;/code&gt;替换成&lt;code&gt;num = * (int *) 0&lt;/code&gt;，然后运行 xv6，会发现 xv6 报错，发生了 kerneltrap，并给出了 3 个寄存器的值：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scause=0xd sepc=0x80001d20 stval=0x0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;panic: kerneltrap
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;通过上面的 scause 表格，可以发现发生 panic 的原因是 load page fault。没有额外的异常信息。产生异常的指令位于虚拟地址 0x80001d20。可以用 gdb 打断点定位到相关代码处，也可以通过下面的方式：&lt;/p&gt;
&lt;p&gt;kernel/kernel.asm 是通过 objdump -S kernel/kernel 反汇编出来的内核代码。拿到上面的 sepc 寄存器值后，可以在 kernel.asm 里搜索这个地址，定位到相关代码。&lt;/p&gt;
&lt;h2 id="练习"&gt;练习&lt;/h2&gt;
&lt;h3 id="sandbox-a-command"&gt;Sandbox a command&lt;/h3&gt;
&lt;p&gt;编写一个系统调用 interpose。按照提示，并且参考其他系统调用的实现即可。&lt;/p&gt;
&lt;p&gt;值得注意的是，由于我们在编写操作系统代码，而且写的是沙箱，目的是阻止进程执行相应的系统调用。所以这个沙箱不能让进程通过某种方式又能执行被阻止的系统调用。为此，一旦把一个进程的某个系统调用的 mask 位设成了 1，这个进程（以及所有子进程）以后都不能再把这个 mask 位设置成 0。不然的话，恭喜你发明了一个 CVE。&lt;/p&gt;
&lt;h3 id="sandbox-with-allowed-pathnames"&gt;Sandbox with allowed pathnames&lt;/h3&gt;
&lt;p&gt;和上一个练习类似，提示也很仔细。同样类似的，已经设置过 pathname 就不能再被设置。&lt;/p&gt;
&lt;h3 id="attack-xv6"&gt;Attack xv6&lt;/h3&gt;
&lt;p&gt;这个练习字很多，并且要阅读很多代码。但是有简单的解法：按照和 secret 程序相同的方式多增长点堆内存，然后暴力搜索字符串即可。&lt;/p&gt;
&lt;h2 id="我的代码"&gt;&lt;a href="https://github.com/konglils/xv6-labs-2026/tree/syscall" target="_blank"&gt;我的代码&lt;/a&gt;&lt;/h2&gt;</content:encoded></item><item><title>xv6 学习笔记：Lab Utilities</title><link>https://konglils.org/posts/xv6-lab-util/</link><pubDate>Mon, 12 Oct 2026 13:20:50 +0800</pubDate><guid>https://konglils.org/posts/xv6-lab-util/</guid><description>MIT 6.1810 xv6 Lab Utilities 实验笔记：介绍了如何克隆并启动 xv6、在 user/ 目录添加自定义程序、运行 make grade 测试与格式化代码。附个人代码仓库。</description><content:encoded>&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/labs/util.html" target="_blank"&gt;Lab Utilities&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="启动-xv6"&gt;启动 xv6&lt;/h2&gt;
&lt;p&gt;克隆仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git://g.csail.mit.edu/xv6-labs-2026
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以每个练习 git commit 一次。&lt;/p&gt;
&lt;p&gt;运行 xv6：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make qemu
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 xv6 里可以通过&lt;code&gt;ls&lt;/code&gt;命令查看当前目录文件。xv6 没有&lt;code&gt;ps&lt;/code&gt;命令，但是可以在 shell 中输入&lt;code&gt;Ctrl-p&lt;/code&gt;查看进程。&lt;/p&gt;
&lt;p&gt;退出 qemu：&lt;code&gt;Ctrl-a x&lt;/code&gt;（同时按&lt;code&gt;Ctrl&lt;/code&gt;和&lt;code&gt;a&lt;/code&gt;，再按&lt;code&gt;x&lt;/code&gt;）&lt;/p&gt;
&lt;h2 id="添加程序"&gt;添加程序&lt;/h2&gt;
&lt;p&gt;这个 lab 的主要内容是用系统调用写一些工具程序，难度不大。&lt;/p&gt;
&lt;p&gt;要在 xv6 里添加程序的话，先在&lt;code&gt;user/&lt;/code&gt;目录里写好&lt;code&gt;xxx.c&lt;/code&gt;源代码。然后修改 Makefile，给&lt;code&gt;UPROGS&lt;/code&gt;加一行&lt;code&gt;$U/_xxx\&lt;/code&gt;，这样下次使用&lt;code&gt;make qemu&lt;/code&gt;的时候就会编译程序并放入镜像。&lt;/p&gt;
&lt;h2 id="测试"&gt;测试&lt;/h2&gt;
&lt;p&gt;运行&lt;code&gt;make grade&lt;/code&gt;会运行所有测试用例。运行&lt;code&gt;./grade-lab-util xxx&lt;/code&gt;可以只运行&lt;code&gt;xxx&lt;/code&gt;这一个练习的测试用例。&lt;/p&gt;
&lt;p&gt;测试通过代表你的代码正确完成了这个练习。&lt;/p&gt;
&lt;h2 id="格式化代码"&gt;格式化代码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;make fmt&lt;/code&gt;，commit 之前先运行。&lt;/p&gt;
&lt;h2 id="我的代码"&gt;&lt;a href="https://github.com/konglils/xv6-labs-2026/tree/util" target="_blank"&gt;我的代码&lt;/a&gt;&lt;/h2&gt;</content:encoded></item><item><title>xv6 学习笔记：开始</title><link>https://konglils.org/posts/xv6-lab-start/</link><pubDate>Fri, 09 Oct 2026 14:28:58 +0800</pubDate><guid>https://konglils.org/posts/xv6-lab-start/</guid><description>跟随 MIT 6.1810 课程系统学习操作系统：介绍了 xv6 实验的学习方法、中文资料与 AI 使用建议，以及 Fedora 下 RISC-V QEMU、GDB 等工具的安装步骤。</description><content:encoded>&lt;p&gt;从这篇文章开始，让我们跟随 MIT 著名课程 &lt;a href="https://pdos.csail.mit.edu/6.828/2026/index.html" target="_blank"&gt;6.1810: Operating System Engineering&lt;/a&gt;，系统性学习操作系统的知识。&lt;/p&gt;
&lt;p&gt;6.1810 的主要内容，就是在 &lt;a href="https://pdos.csail.mit.edu/6.828/2026/xv6.html" target="_blank"&gt;xv6&lt;/a&gt; 这个小巧精致的 Unix v6 操作系统的现代 RISC-V 实现上，完成一系列 lab，从而深入学习、理解并掌握现代操作系统的基本组成部分以及运行机制。&lt;/p&gt;
&lt;h2 id="学习方法"&gt;学习方法&lt;/h2&gt;
&lt;p&gt;我推荐搭配&lt;a href="https://github.com/iTanken/ostep-chinese" target="_blank"&gt;《操作系统导论》&lt;/a&gt;这本书一起学习。这本书内容详实，有很多插图，与 xv6 联系密切。&lt;/p&gt;
&lt;p&gt;诚然，不能让英语成为我们学习的障碍。之前我在想，学习这些优质英语课程的时候，直接看英文原版材料，这样既能学习课程本身，又能巩固英语。但是我错了，结果是课程本身学起来很吃力，英语水平也没见长进。所以最好有中文版就直接看中文版内容。少量缺乏翻译的英语内容可以试着自己阅读，或者用 AI 翻译。&lt;/p&gt;
&lt;p&gt;说到 AI，课程的态度是：“你可以使用 AI 工具来帮你理解操作系统概念、阅读材料以及 xv6 代码。但是不要使用 AI 工具写 lab 代码或调试代码。你只有通过自己的努力去设计、实现和调试你的软件，才能从 lab 中取得收获。”我们在做 lab 的过程中也要贯彻这一原则。&lt;/p&gt;
&lt;p&gt;不要熬夜做 lab，把 lab 分在多天完成更高效。操作系统内核中出现 bug 可能令人困惑，可能需要经过大量思考和仔细的调试才能理解和修复。&lt;/p&gt;
&lt;p&gt;建议做 lab 的时候把课程网页调成 150% 缩放，保护视力。&lt;/p&gt;
&lt;h2 id="安装工具"&gt;安装工具&lt;/h2&gt;
&lt;p&gt;需要安装 RISC-V 版的 QEMU 7.2+, GDB 8.3+, GCC, Binutils。&lt;/p&gt;
&lt;p&gt;我使用 fedora，键入以下命令安装：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dnf install git @development-tools gdb qemu-system-riscv gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;测试安装是否成功：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;qemu-system-riscv64 --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;riscv64-linux-gnu-gcc --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="目录"&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://konglils.org/posts/xv6-lab-util"&gt;Lab Utilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://konglils.org/posts/xv6-lab-syscall"&gt;Lab System calls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://konglils.org/posts/xv6-lab-pgtbl"&gt;Lab Page tables&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/overview.html" target="_blank"&gt;6.1810: Learning by doing&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/tools.html" target="_blank"&gt;Tools Used in 6.1810&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/labs/guidance.html" target="_blank"&gt;Lab guidance&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://pdos.csail.mit.edu/6.828/2026/reference.html" target="_blank"&gt;Reading materials&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://mit-pdos.github.io/xv6-riscv-book/" target="_blank"&gt;xv6: A simple, Unix-like teaching operating system&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/zhenyu-zang/xv6-riscv-book-Chinese" target="_blank"&gt;xv6 book 中文翻译&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/playlist?list=PLTsf9UeqkReZHXWY9yJvTwLJWYYPcKEqK" target="_blank"&gt;MIT 6.S081&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/iTanken/ostep-chinese" target="_blank"&gt;操作系统导论（中文版）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>测量 Linux 系统调用和进程切换开销</title><link>https://konglils.org/posts/syscall-spend/</link><pubDate>Mon, 07 Sep 2026 18:38:47 +0800</pubDate><guid>https://konglils.org/posts/syscall-spend/</guid><description>本文编写代码测量了 Linux 上的系统调用和进程切换的开销。通过多次测量，计算平均消耗时间。使用 lmbench 基准测试较为准确地测量了进程切换的时间。并分析了若干条件对结果的影响。</description><content:encoded>&lt;h2 id="系统调用开销"&gt;系统调用开销&lt;/h2&gt;
&lt;p&gt;操作系统中存在各类系统调用，用于让用户态的程序陷入内核态，从而执行特权指令，操作硬件、读取系统信息和创建进程等。如何测量它们的开销呢？&lt;/p&gt;
&lt;p&gt;一个简单的办法是，只要执行足够多次系统调用，再用总消耗时间除以调用次数，就可以得到一个系统调用的平均消耗时间。&lt;/p&gt;
&lt;p&gt;当然，这一开销在不同硬件和操作系统上会有所区别，不过可以大致估计一下其数量级。&lt;/p&gt;
&lt;h3 id="代码"&gt;代码&lt;/h3&gt;
&lt;p&gt;以下代码给出了 Linux 上的一些系统调用的平均开销时间测量方法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;err.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;fcntl.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/stat.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;time.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define MEASURE(tip, n, call) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;do { \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; struct timespec ts1; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; struct timespec ts2; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; clock_gettime(CLOCK_MONOTONIC, &amp;amp;ts1); \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; for (int i = 0; i &amp;lt; (n); i++) { \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; call; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; } \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; clock_gettime(CLOCK_MONOTONIC, &amp;amp;ts2); \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; double average = (double) ((ts2.tv_sec - ts1.tv_sec) * 1000000000 + \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; ts2.tv_nsec - ts1.tv_nsec) / (n); \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; printf(&amp;#34;%s: %f ns\n&amp;#34;, (tip), average); \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;} while (0)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;errx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;invalid argument&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 获取当前进程 ID
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;MEASURE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;getpid&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 0 字节读取
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;qsyscall.c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;O_RDONLY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;open&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;MEASURE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;read 0 byte&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 获取文件状态信息
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;MEASURE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lstat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;lstat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;qsyscall.c&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我写了个宏方便使用。用 &lt;code&gt;do { ... } while (0)&lt;/code&gt; 块包裹一个块，让内部变量有独立作用域。注意多行的宏，每行结尾都要有反斜杠，用来向编译器说明这是一个多行宏。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;clock_gettime&lt;/code&gt; 精度可达纳秒。使用 &lt;code&gt;CLOCK_MONOTONIC&lt;/code&gt; 作为时钟，测量结果为自系统开机开始到当前的时间，而且不受系统时间改变影响。&lt;/p&gt;
&lt;h3 id="结果示例"&gt;结果示例&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ gcc -o measure measure.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ./measure &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getpid: 146.415000 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; byte: 164.010000 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lstat: 221.023000 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ./measure &lt;span class="m"&gt;1000000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getpid: 60.134272 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; byte: 75.495297 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lstat: 200.536937 ns
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意到，当调用次数上升之后，各个系统调用的消耗时间会降低，这可能是 CPU 分支预测和缓存等机制作用的结果。&lt;/p&gt;
&lt;h2 id="进程切换开销"&gt;进程切换开销&lt;/h2&gt;
&lt;p&gt;在同一个 CPU 核心上，同一时间只能有一个进程在运行，当其执行了 IO 操作或者时间片用完后，操作系统会根据调度策略，执行其他进程。而这需要操作系统保存当前进程的状态到内存，切换另一个进程并运行。进程切换过程也有开销。如何测量进程切换的开销呢？&lt;/p&gt;
&lt;p&gt;我们可以使用 lmbench 基准测试：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;lmbench 基准测试的实现方法，是在单个 CPU 上运行两个进程并在它们之间设置两个 UNIX 管道。管道只是 UNIX 系统中的进程可以相互通信的许多方式之一。第一个进程向第一个管道写入数据，然后等待第二个数据的读取。由于看到第一个进程等待从第二个管道读取内容，OS 将第一个进程置于阻塞状态，并切换到另一个进程，该进程从第一个管道读取数据，然后写入第二个管道。当第二个进程再次尝试从第一个管道读取时，它会阻塞，从而继续进行通信的往返循环。通过反复测量这种通信的成本，lmbench 可以很好地估计上下文切换的成本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="代码-1"&gt;代码&lt;/h3&gt;
&lt;p&gt;在 Linux 上，我们可以使用这样的代码测量进程切换的开销：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define _GNU_SOURCE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;err.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sched.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;time.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nloops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 绑定进程到指定 CPU 核心
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;cpu_set_t&lt;/span&gt; &lt;span class="n"&gt;cpu_set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;CPU_ZERO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cpu_set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;CPU_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cpu_set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sched_setaffinity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu_set&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cpu_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;sched_setaffinity&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 创建管道，pipefd[0] 是读端，pipefd[1] 是写端
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pipefd1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pipefd2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;pipefd1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;pipefd2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 读写缓冲区顺便作为计数器
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cpid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;fork&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 子进程
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nloops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;child read %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;child write %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 父进程
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="n"&gt;ts1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clock_gettime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLOCK_MONOTONIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ts1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;clock_gettime at start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nloops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd1&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;parent write %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipefd2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;parent read %d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;clock_gettime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLOCK_MONOTONIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;clock_gettime at end&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tv_sec&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tv_sec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000000000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ts2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tv_nsec&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tv_nsec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;nloops&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;switch time: %f ns&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;average&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;fork 出的子进程会继承和父进程同样的 CPU 绑定状态，所以只需要设置一次。&lt;/p&gt;
&lt;p&gt;必须确保两个进程运行在同一个 CPU 核心上，不然结果会偏大。我猜测是因为两个进程不在同一 CPU 核心上的话，当父进程执行到 read，等待子进程 write 时，运行子进程的另一个 CPU 核心可能在运行其他进程，不能马上切换到子进程，反过来也是同理。&lt;/p&gt;
&lt;p&gt;必须创建两个管道，确保每个进程都只能在一个管道写，在另一个管道读。如果只有一个管道，一个进程在同一管道的两端读写的话，父进程因为先 write，所以 read 的时候不会阻塞，从而父进程自己会跑完所有循环，抢占所有 CPU 时间，就没有进程来回切换的时间了，导致结果偏小。&lt;/p&gt;
&lt;h3 id="结果示例-1"&gt;结果示例&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ gcc -o swtch swtch.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ./swtch &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;switch time: 906.945000 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ./swtch &lt;span class="m"&gt;31&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;switch time: 1709.777000 ns
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ ./swtch &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;switch time: 887.263488 ns
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我的 CPU 是 13 代酷睿，有大小核的区分。可以看到，不同性能的 CPU 核心对进程切换时间影响较大。&lt;/p&gt;
&lt;p&gt;调用次数越多，运行结果也会被分支预测和缓存等机制优化，不过相对来说没有系统调用那么明显的优化。&lt;/p&gt;
&lt;p&gt;使用管道应该也有额外开销，不过这应该接近真实消耗时间了。我还写了个用 Unix socket 测量的版本，消耗时间稍微多点。&lt;/p&gt;
&lt;h2 id="所有代码片段"&gt;&lt;a href="https://github.com/konglils/ostep-practice/tree/main/chapter06" target="_blank"&gt;所有代码片段&lt;/a&gt;&lt;/h2&gt;</content:encoded></item><item><title>使用阿里云 SDK 自动更新 DNS 记录</title><link>https://konglils.org/posts/certbot-esa/</link><pubDate>Fri, 15 May 2026 15:00:05 +0800</pubDate><guid>https://konglils.org/posts/certbot-esa/</guid><description>Certbot 提供了 DNS-01 的验证和清理钩子，以便能自动续签 Let's Encrypt 的免费 DV 证书。我通过编写 Python 脚本，实现了阿里云 ESA 上的 DNS 记录自动更新。DNS 安全十分重要，我们要限制相关权限，做到最小化权限原则，使得网站服务器能安全有序地运行。阿里云提供了精细的权限控制，通过合理的授权，可以把安全风险尽量压低。</description><content:encoded>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;
&lt;p&gt;在&lt;a href="https://konglils.org/posts/letsencrypt-https"&gt;HTTPS 启动指南&lt;/a&gt;中，我提到了 ACME 协议的 DNS-01 验证方式，因为各家 DNS 服务商提供的 API 不同，所以没有通用的方式做自动 DNS-01 验证。但好消息是，使用 Certbot 的验证钩子和清理钩子，可以编写自动化证书验证和清理脚本。本站使用了阿里云的免费版 ESA（原 DCDN），通过 NS 方式接入，我需要查找阿里云和 ESA 的相关文档，来编写验证和清理脚本。&lt;/p&gt;
&lt;h2 id="安全第一"&gt;安全第一&lt;/h2&gt;
&lt;p&gt;想要通过 API 请求修改 DNS 记录，就必须在请求中加上身份证明，通常被称作 API 凭据。如果要长期通过 DNS-01 方式自动续签 TLS 证书，就需要在服务器上长期存储 API 凭据，用以修改 DNS 记录。这可能存在安全隐患。假设 API 凭据的权限过大，能任意增删改查 DNS 记录，一旦服务器被攻击导致 API 凭据泄露，会造成十分危险的后果，比如恶意攻击者可以将你的主域名解析到它们的服务器上，然后申请一张完全合法的证书，就可以以你的名义做他们想做的事。&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt 和 Certbot 的文档都提到了在服务器上存储 API 凭据的危险性。原因是，部分 DNS 商提供的权限粒度过粗，导致用户没办法细致地只授予 DNS-01 验证所需的权限。如果 DNS 商提供的 API 可以做到细粒度的授权，我们就应该用最小权限授权，否则就是主动开放后门。&lt;/p&gt;
&lt;h2 id="esa"&gt;ESA&lt;/h2&gt;
&lt;p&gt;阿里云的权限设置还是比较精细的。对于 DNS-01 验证来说，我们只需要一个 API：UpdateRecord。它只能通过 RecordId 来修改记录内容。给 API 凭据限制权限，让它只能使用 UpdateRecord，然后手动获取 DNS-01 验证所需的 RecordId（可以通过 API 调试功能调用 ListRecords），填入续签脚本中。就算有恶意攻击者拿到了 API 凭据，也只能用来修改这些指定的以 _acme-challenge 开头的记录。当然，攻击者能据此向 Let&amp;rsquo;s Encrypt 证明他控制了该子域名，从而为该子域名申请证书，并且私钥在他手中。不过，他仍没办法把该子域名的 DNS 解析到其他地方，他申请的证书实际上使用不了。&lt;/p&gt;
&lt;p&gt;在验证脚本中，我们可以把记录的值更新为 Certbot 验证码。等待一段时间，确保 DNS 记录生效后，在清理脚本中，我们可以把记录的值更新为 0 或其他不重要的值（TXT 记录不能为空字符串）。&lt;/p&gt;
&lt;p&gt;我之前想到的办法是使用两个 API，一个是 CreateRecord，另一个是 DeleteRecord。验证脚本中先创建记录，清理脚本中再删除。DeleteRecord 也只能通过 RecordId 来删除记录，没有列举记录的权限时，其实只能拿到 CreateRecord 后返回的 RecordId，并删除对应记录。虽然这种方式也不能修改现有记录，但是恶意攻击者拿到 API 凭据后可以添加很多垃圾记录或申请很多证书，不确定的风险更多。&lt;/p&gt;
&lt;h2 id="我的实际情况"&gt;我的实际情况&lt;/h2&gt;
&lt;p&gt;我的 ESA 在账号 A 上，ECS 在账号 B 上。阿里云可以跨账号授权。&lt;/p&gt;
&lt;p&gt;首先在账号 A 上创建一个 RAM 角色，信任主体类型为云账号，信任主体名称设置为其他云账号，也就是账号 B。为该 RAM 角色新增一个自定义权限策略，该策略只包含 UpdateRecord。然后给账号 B 也添加一个 RAM 角色，信任主体类型为云服务，然后指定 ECS，这样确保指定的 ECS 才能使用该角色，进一步降低了 API 凭据泄露的风险。给账号 B 的 RAM 角色新增授权，策略为 AliyunSTSAssumeRoleAccess，这意味着账号 B 的 RAM 角色可以模拟账号 A 的 RAM 角色，从而调用账号 A 的 UpdateRecord。&lt;/p&gt;
&lt;h2 id="脚本编写"&gt;脚本编写&lt;/h2&gt;
&lt;p&gt;Certbot 给脚本提供了一些环境变量，其中必须用到的是&lt;code&gt;CERTBOT_DOMAIN&lt;/code&gt;和&lt;code&gt;CERTBOT_VALIDATION&lt;/code&gt;，分别是要验证的域名和 TXT 记录的值。这个 &lt;code&gt;CERTBOT_DOMAIN&lt;/code&gt;是 Certbot 参数中的原始域名，不包含&lt;code&gt;_acme-challenge&lt;/code&gt;前缀。我们需要自行判断和添加。对于清理脚本，Certbot 还提供了一个&lt;code&gt;CERTBOT_AUTH_OUTPUT&lt;/code&gt;环境变量，内容是验证脚本的 STDOUT 输出。&lt;/p&gt;
&lt;p&gt;相关脚本在&lt;a href="https://github.com/konglils/certbot-hook" target="_blank"&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="使用"&gt;使用&lt;/h2&gt;
&lt;p&gt;可以像这样给&lt;code&gt;www.example.com&lt;/code&gt;申请证书：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot certonly &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--preferred-challenges&lt;span class="o"&gt;=&lt;/span&gt;dns --manual &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-auth-hook /opt/aliyun/acme_authenticator.py &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-cleanup-hook /opt/aliyun/acme_cleanup.py &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--deploy-hook acme_deploy.sh &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	-d www.example.cn
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也可以像这样给&lt;code&gt;*.example.com&lt;/code&gt;更换验证方式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--cert-name example.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--preferred-challenges&lt;span class="o"&gt;=&lt;/span&gt;dns --manual &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-auth-hook /opt/aliyun/acme_authenticator.py &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-cleanup-hook /opt/aliyun/acme_cleanup.py &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--deploy-hook acme_deploy.sh &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--force-renewal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然 DNS-01 不太通用，但是 Certbot 提供了手动验证方式，以及钩子和相关环境变量，这使得我们能自行编写任意逻辑。Git 也有类似的钩子，在执行了某个操作之后可以运行一段可执行程序。&lt;/p&gt;
&lt;p&gt;阿里云的 API 文档很详细，虽然也有一些美中不足的地方，碰到了一些问题，稍稍地绕了些弯路，但无伤大雅。&lt;/p&gt;
&lt;p&gt;阿里云集成的 AI 助理很不错，在碰到问题的时候，询问它比询问那些通用的 AI 能得到更具体的解答，而且回答的时候有文档链接，确保回答内容不太容易胡编乱造。&lt;/p&gt;
&lt;p&gt;阿里云的 &lt;a href="https://api.aliyun.com/api" target="_blank"&gt;API 调试&lt;/a&gt; 功能十分强大。有些参数在浏览器界面里设置不了，必须通过 API 才能设置，使用 API 调试功能，就可以免去手写一些账号验证的代码。最让我惊讶的是，它们的 SDK 都是通过 &lt;a href="https://github.com/aliyun/darabonba" target="_blank"&gt;Darabonba&lt;/a&gt; 自动生成的，定义统一的 API，然后就可以生成不同语言的 SDK，还有代码示例。代码示例尤其有用，省去了很多查询文档的时间。非常值得学习。&lt;/p&gt;
&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html#pre-and-post-validation-hooks" target="_blank"&gt;User Guide — Certbot 5.7.0.dev0 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-updaterecord" target="_blank"&gt;调用UpdateRecord接口更新DNS解析记录并配置源站鉴权-边缘安全加速-阿里云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-createrecord" target="_blank"&gt;调用CreateRecord创建DNS记录-边缘安全加速-阿里云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.aliyun.com/zh/edge-security-acceleration/esa/api-esa-2024-09-10-deleterecord" target="_blank"&gt;调用DeleteRecord接口通过RecordId删除单条DNS解析记录-边缘安全加速-阿里云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.aliyun.com/zh/sdk/developer-reference/v2-manage-python-access-credentials#8c7f3f7cb5i0n" target="_blank"&gt;Python SDK使用Credentials工具配置访问凭据与默认凭据链-阿里云SDK-阿里云&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.aliyun.com/zh/ecs/user-guide/attach-an-instance-ram-role-to-an-ecs-instance" target="_blank"&gt;给ECS实例授予RAM角色-云服务器 ECS(ECS)-阿里云帮助中心&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HTTPS 启动指南 (Let's Encrypt)</title><link>https://konglils.org/posts/letsencrypt-https/</link><pubDate>Wed, 13 May 2026 23:18:47 +0800</pubDate><guid>https://konglils.org/posts/letsencrypt-https/</guid><description>使用 HTTPS 可以有效抵御中间人攻击，杜绝信息在网络传输过程中被监视和篡改的可能。HTTPS 是 HTTP 加了一层 TLS，要开启 HTTPS 就必须申请 SSL/TLS 证书。Let's Encrypt 提供了免费的 DV 证书颁发服务。本文介绍了 ACME 协议以及域名验证的原理，还介绍了使用 Certbot 用来申请免费 DV 证书，并自动续签，以长期使用 HTTPS。使用 Nginx 可以方便地配置证书相关服务。</description><content:encoded>&lt;p&gt;HTTPS (Hypertext Transfer Protocol Secure, 超文本传输安全协议)，可以有效抵御中间人攻击，杜绝信息在网络传输过程中被监视和篡改的可能。其实 HTTPS 就是 HTTP 协议套了一层 TLS 协议，用来加密 HTTP 数据报。关于 TLS 协议的具体内容，这里就不细致展开了。这篇文章主要聊聊免费 HTTPS 的相关概念以及如何给自己的网站和服务器申请并应用。&lt;/p&gt;
&lt;h2 id="基本概念"&gt;基本概念&lt;/h2&gt;
&lt;h3 id="ssltls"&gt;SSL/TLS&lt;/h3&gt;
&lt;p&gt;TLS (Transport Layer Security) 协议的前身是 SSL (Secure Sockets Layer) 协议，由于 SSL 不安全，后面才有了 TLS 协议。最新且主流的 TLS 版本是 TLS 1.3，部分较老的客户端可能还在使用 TLS 1.2。TLS 1.1 及以下的版本不安全，网站没必要支持它们。&lt;/p&gt;
&lt;h3 id="公钥私钥"&gt;公钥/私钥&lt;/h3&gt;
&lt;p&gt;TLS 加密的原理是非对称加密，简单来说就是使用公钥加密的内容，只有私钥才能解开。公钥会包含在证书里，任何人都可以查看。私钥必须存储在服务器上，不能泄露。&lt;/p&gt;
&lt;h3 id="证书"&gt;证书&lt;/h3&gt;
&lt;p&gt;历史原因，SSL 证书，TLS 证书，SSL/TLS 证书等等说法其实都是同一概念，为了避免混淆，以下统一简称证书。证书是由 CA 颁发的，相当于给网站的身份证明。TLS 连接的时候会用到证书，浏览器会根据证书检查网站的身份。在大多数浏览器中，网页地址栏旁边有个锁标志，点击其中可以查看网站的证书及颁发它的 CA。申请证书需要域名（还有 IP 证书，不了解，下面也不介绍）。证书具有有效期，在有效期内，证书才能被浏览器信任。过了有效期需要续签证书。有了证书才能使用 TLS 加密 HTTP 内容，进行 HTTPS 通信。&lt;/p&gt;
&lt;h3 id="ca-certificate-authority-证书颁发机构"&gt;CA (Certificate Authority, 证书颁发机构)&lt;/h3&gt;
&lt;p&gt;颁发证书的权威机构，有根 CA 和中间 CA。浏览器访问某个网站时，会向 CA 检查网站的证书是否有效。操作系统的安装镜像和浏览器的安装包里会存放一些根 CA 的证书。根 CA 被无条件信任。这样，浏览器访问网站的时候，信任链就是：根 CA -&amp;gt; 中间 CA -&amp;gt; 网站。其中有一个环节出错，都会导致浏览器发出不安全警告。&lt;/p&gt;
&lt;h3 id="dvovev"&gt;DV/OV/EV&lt;/h3&gt;
&lt;p&gt;Domain Validation (DV), Organization Validation (OV), Extended Validation (EV)。证书的信任等级。申请 DV 证书只需要验证申请者拥有该域名。而申请 OV 证书和 EV 证书需要验证申请者的组织，这可以提高客户的信任程度（得加钱！）。免费证书只能是 DV 了。&lt;/p&gt;
&lt;h3 id="acme-automatic-certificate-management-environment-自动证书管理环境"&gt;ACME (Automatic Certificate Management Environment, 自动证书管理环境)&lt;/h3&gt;
&lt;p&gt;ACME 协议是 DV 证书得以免费的关键。因为证书能自动颁发、续签、吊销，没有人工成本。&lt;/p&gt;
&lt;h3 id="lets-encrypt"&gt;Let&amp;rsquo;s Encrypt&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt 是今天的头号主角，维基百科是这么说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt是由网际网路安全研究小组（ISRG）运营的非营利性证书颁发机构，免费提供传输层安全性协定（TLS）加密的X.509证书。它是世界上最大的证书颁发机构，超过3亿个网站使用其服务，其目标是确保所有网站的安全并使用HTTPS。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s Encrypt 使用 ACME 协议颁发证书。&lt;/p&gt;
&lt;h3 id="certbot"&gt;Certbot&lt;/h3&gt;
&lt;p&gt;主要向 Let&amp;rsquo;s Encrypt 自动申请和续签 DV 证书的工具，使用 ACME 协议，用 Python 编写。&lt;/p&gt;
&lt;h2 id="申请原理"&gt;申请原理&lt;/h2&gt;
&lt;p&gt;详细申请原理可以看&lt;a href="https://letsencrypt.org/zh-cn/docs/challenge-types/" target="_blank"&gt;这里&lt;/a&gt;。Let&amp;rsquo;s Encrypt 只能颁发 DV 证书，所以只要能验证你拥有某个域名，就可以给你颁发证书了。验证方式主要有两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP-01：向网站某目录下放置一个指定名字和内容的文件，然后 Let&amp;rsquo;s Encrypt 检查该网址能够访问，并且内容也正确，就认为你拥有该域名。&lt;/li&gt;
&lt;li&gt;DNS-01：给域名添加一条指定名字和内容的 TXT 记录，然后 Let&amp;rsquo;s Encrypt 检查该 DNS 记录存在且内容正确，就认为你拥有该域名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="自动化"&gt;自动化&lt;/h2&gt;
&lt;p&gt;证书的续签必须能做到自动化，即无需人工操作。这么做的原因有很多，主要有三点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;省去了人工操作的麻烦，如果每隔一段时间就要手动连接服务器并输入续签命令，会带来心理负担。&lt;/li&gt;
&lt;li&gt;一旦有一段时间忘记了手动续签证书，网站就无法使用 HTTPS 连接，风险大大增加。&lt;/li&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt 的证书有效期比较短，只有90天。而且到了 2028 年 Let&amp;rsquo;s Encrypt 会把这一时间&lt;a href="https://letsencrypt.org/2025/12/02/from-90-to-45" target="_blank"&gt;缩短到 45 天&lt;/a&gt;，手动续签更麻烦了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不少云服务商或 CDN 商集成了 Let&amp;rsquo;s Encrypt 的证书申请和自动续签流程，使用这些服务可能会更方便。&lt;/p&gt;
&lt;p&gt;注意两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;证书的私钥存放在哪取决于谁申请了证书。如果是云服务商或 CDN 商申请了证书，私钥就由它们保管。如果是你自己申请的证书，私钥就由你自己保管。&lt;/li&gt;
&lt;li&gt;同一个子域名可以被多张证书包含。浏览器访问网站时，服务器可以决定这次连接使用哪张证书。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面开始介绍在服务器上自行申请证书。&lt;/p&gt;
&lt;h2 id="安装-certbot"&gt;安装 Certbot&lt;/h2&gt;
&lt;p&gt;在网站服务器上，Debian 等 Linux 发行版可以使用以下命令安装 Certbot：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install certbot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用系统包管理器安装 Certbot 比较方便，因为会自动配置 Systemd 的定时任务。在 Debian 12 上，Certbot 定时任务每天运行两次，检查当前时间是否满足续签条件，满足了就会续签一张新的证书。&lt;/p&gt;
&lt;h2 id="域名验证方式http-01"&gt;域名验证方式：HTTP-01&lt;/h2&gt;
&lt;p&gt;通常来说，向网站某目录放置一个文件是很容易的。比如，在 Nginx 上，可以添加如下配置（参考&lt;a href="https://github.com/konglils/nginx-conf/" target="_blank"&gt;这里&lt;/a&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 直接在 server 块里加上这个 location 块
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 这样配置的好处是已有的配置不需要调整
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 注意 WAF 有没有拦截这种 . 开头的路径
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/.well-known/acme-challenge/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# /path/to/acme-root 可以是任意文件夹，但不要轻易变化
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;alias&lt;/span&gt; &lt;span class="s"&gt;/path/to/acme-root/.well-known/acme-challenge/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后，运行以下命令为&lt;code&gt;www.example.com&lt;/code&gt;申请一张证书。可以添加多个&lt;code&gt;-d&lt;/code&gt;，实现一张证书包含多个具体子域名。&lt;code&gt;--deploy-hook&lt;/code&gt;指定一个可执行程序，只在成功获取证书后运行，可以用来重新加载 WEB 服务器（如&lt;code&gt;systemctl reload nginx&lt;/code&gt;），以便读取新证书，或者运行证书格式转换等。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot certonly &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--webroot -w /path/to/acme-root &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--deploy-hook /path/to/deploy-hook &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	-d www.example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一次运行的话，Certbot 会要求输入一个邮箱（似乎可以随便填写）。这个邮箱之前会用来收到证书临期提醒，2025 年开始&lt;a href="https://letsencrypt.org/2025/01/22/ending-expiration-emails" target="_blank"&gt;不会了&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果一切正常，Certbot 会和 Let&amp;rsquo;s Encrypt 通信，在&lt;code&gt;/path/to/acme-root&lt;/code&gt;下创建&lt;code&gt;.well-known/acme-challenge/&lt;/code&gt;文件夹和指定名字和内容的文件，Let&amp;rsquo;s Encrypt 会访问&lt;code&gt;http://&amp;lt;域名&amp;gt;/.well-known/acme-challenge/&amp;lt;文件&amp;gt;&lt;/code&gt;验证其内容，之后文件夹会被删除，证书就申请完毕了。&lt;/p&gt;
&lt;p&gt;申请完成后，Certbot 会提示证书存放位置，通常在&lt;code&gt;/etc/letsencrypt/live/&lt;/code&gt;下。&lt;/p&gt;
&lt;h2 id="域名验证方式dns-01"&gt;域名验证方式：DNS-01&lt;/h2&gt;
&lt;p&gt;修改域名的 DNS 记录没有太通用的方法，因为不同的 DNS 服务商有不同的 API。某些 DNS 服务商有对应的 Certbot 插件，可以方便地使用，参考 Certbot 的&lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html#dns-plugins" target="_blank"&gt;文档&lt;/a&gt;。使用 DNS-01 验证的参数是&lt;code&gt;--preferred-challenges=dns&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;比如，给&lt;code&gt;www.example.com&lt;/code&gt;通过 DNS-01 方式申请证书，需要添加一条 TXT 类型的 DNS 记录，记录名是&lt;code&gt;_acme-challenge.www.example.com&lt;/code&gt;，值是 Let&amp;rsquo;s Encrypt 生成的一串值。Let&amp;rsquo;s Encrypt 会验证该记录是否存在，以及值是否正确。之后可以删除该记录。&lt;/p&gt;
&lt;p&gt;虽然 DNS-01 不太方便，但是有些情况下必须使用。比如对于 DNS 服务商来说，它们给客户提供 Let&amp;rsquo;s Encrypt 的自动申请与续签，只能使用 DNS-01 来验证。再如，通配符域名证书如&lt;code&gt;*.example.com&lt;/code&gt;必须使用 DNS-01 的方式获取，因为通配符域名不对应一个具体的主机名，不存在相应网址。&lt;/p&gt;
&lt;p&gt;Certbot 提供了&lt;code&gt;--manual&lt;/code&gt;参数，可以手动添加续签证书所需要的 DNS 记录，验证完成后清理记录。但是，为了追求我们的自动化目标，我们还要使用另外两个参数，&lt;code&gt;--manual-auth-hook&lt;/code&gt;和&lt;code&gt;--manual-cleanup-hook&lt;/code&gt;。它们指定两个可执行文件，分别为创建记录的代码和清理记录的代码。Certbot 会在申请证书时和申请完成时分别执行两个可执行文件并传入相应的环境变量，实现 DNS-01 验证的自动化。&lt;/p&gt;
&lt;p&gt;我之后会发一篇&lt;a href="https://konglils.org/posts/certbot-esa/"&gt;文章&lt;/a&gt;，记录我编写 Python 脚本自动修改阿里云 ESA 的 DNS，完成 DNS-01 验证的过程。&lt;/p&gt;
&lt;h2 id="查看证书"&gt;查看证书&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot certificates
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以上命令会列出本机申请的所有证书，每张证书类似于：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Certificate Name: www.example.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Serial Number: 5*********************************4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Key Type: ECDSA
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Domains: www.example.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Expiry Date: 2026-08-10 10:50:56+00:00 &lt;span class="o"&gt;(&lt;/span&gt;VALID: &lt;span class="m"&gt;88&lt;/span&gt; days&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Certificate Path: /etc/letsencrypt/live/www.example.com/fullchain.pem
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Private Key Path: /etc/letsencrypt/live/www.example.com/privkey.pem
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Certbot 有些子命令可以使用&lt;code&gt;--cert-name&lt;/code&gt;参数，值就是上面的 Certificate Name。&lt;/p&gt;
&lt;h2 id="续签证书"&gt;续签证书&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 尝试续签本机所有证书&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 续签指定证书&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew --cert-name &amp;lt;证书名&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 模拟一遍证书续签流程，此时颁发的证书无效，并且也不会存到磁盘中&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 可以用来验证新的续签方法和参数是否有效，有效后再删掉该参数正式申请&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew --cert-name &amp;lt;证书名&amp;gt; --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 强制续签指定证书&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Let&amp;#39;s Encrypt 的速率限制挺宽松的，只要不是频繁续签证书，它不会限制&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew --cert-name &amp;lt;证书名&amp;gt; --force-renewal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 未指定验证方法和相关参数时，证书续签时的验证方式和申请时一样。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 如果指定了新的验证方式或者参数，并且强制续签证书，&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Certbot 就会使用新方法和参数续签新证书，以后续签也用新方法：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot renew &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--cert-name www.example.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--preferred-challenges&lt;span class="o"&gt;=&lt;/span&gt;dns --manual &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-auth-hook /path/to/authenticator &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--manual-cleanup-hook /path/to/cleanup &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--deploy-hook /path/to/deploy-hook &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	--force-renewal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;之所以有个强制选项，是因为即使使用了&lt;code&gt;renew&lt;/code&gt;子命令，Certbot 也还是会检查当前时间和证书有效期，当前时间超过阈值后，才会真正执行更新逻辑，具体阈值看&lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html#renewing-certificates" target="_blank"&gt;文档&lt;/a&gt;，Certbot 不同版本的阈值不一样，对于 Debian 12 的 Certbot 来说，这个阈值是固定的，证书失效前 30 天。加上强制选项可以马上续签证书。&lt;/p&gt;
&lt;h2 id="吊销证书"&gt;吊销证书&lt;/h2&gt;
&lt;p&gt;如果一张证书已经不再使用，可以吊销它。Certbot 会询问是否要删除证书，如果不删除，下次&lt;code&gt;renew&lt;/code&gt;的时候 Certbot 仍然会尝试续签该证书。吊销前确保证书已经不在任何地方使用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo certbot revoke --cert-name www.example.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="开启-https"&gt;开启 HTTPS&lt;/h2&gt;
&lt;p&gt;当使用 Nginx 时，在 server 块中添加或修改如下配置项，然后重新加载 Nginx，即可为网站开启 HTTPS：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 监听 HTTPS 的 443 端口，在该端口上启用 SSL（其实是 TLS）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 下面指定的证书需要包含这个子域名，否则客户端会认为连接不安全
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;www.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 包含网站证书、中间 CA 的证书
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ssl_certificate&lt;/span&gt; &lt;span class="s"&gt;/etc/letsencrypt/live/www.example.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 证书对应的私钥
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="s"&gt;/etc/letsencrypt/live/www.example.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 中间 CA 的证书
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Certbot 的文档提到如果 Nginx 使用了 OCSP stapling，该配置项必须为 chain.pem，
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 用来验证 OCSP 的响应
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ssl_trusted_certificate&lt;/span&gt; &lt;span class="s"&gt;/etc/letsencrypt/live/www.example.com/chain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="参考"&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/zh-cn/how-it-works/" target="_blank"&gt;Let&amp;rsquo;s Encrypt 的运作方式 - Let&amp;rsquo;s Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/zh-cn/docs/challenge-types/" target="_blank"&gt;验证方式 - Let&amp;rsquo;s Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/zh-cn/docs/faq/" target="_blank"&gt;常见问题 - Let&amp;rsquo;s Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html" target="_blank"&gt;User Guide — Certbot 5.7.0.dev0 documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Virtual Judge 自动抓取 OJ Cookie</title><link>https://konglils.org/posts/vjudge-cookie-filler/</link><pubDate>Thu, 22 May 2025 22:08:28 +0800</pubDate><guid>https://konglils.org/posts/vjudge-cookie-filler/</guid><description>在 Virtual Judge 通过自己账号交题经常要手动复制 OJ（比如 UVA）的 Cookie 粘贴到 VJ 里，这种重复劳动做多了会打断思路。我写了个油猴脚本，能自动化这个过程。本文顺便拆解了 VJ 的 Cookie 验证接口，分析了它的工作方式。内附脚本源码，希望能帮大家省下这些枯燥的复制时间。</description><content:encoded>&lt;h2 id="问题引入"&gt;问题引入&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://vjudge.net/" target="_blank"&gt;Virtual Judge&lt;/a&gt;（简称VJ）可以向其他 Online Judge（简称 OJ）提交题目，并在 VJ 上留下记录。&lt;/p&gt;
&lt;p&gt;VJ 有其中一些 OJ 的机器人账号，如果提交的题目是这些 OJ 上的，VJ 直接用机器人账号交题。&lt;/p&gt;
&lt;p&gt;对于另一些 OJ，因为验证机制比较复杂，机器人难以通过，所以 VJ 不支持直接用机器人账号向它们提交题目。VJ 的办法是：只要用户提供了那个 OJ 的 Cookie，VJ 就可以伪装成用户的账号，利用用户自己的账号交题。另外有时候即使机器人账号可用，我们还是想在自己账号上留下提交记录，也能选择这种方法。&lt;/p&gt;
&lt;p&gt;在 VJ 的提交页面，有“My Account”或者“Archive”选项，前者向用户账号提交一份新的代码，后者从用户账号中抓取之前提交过的代码。这两个操作都要用到 Cookie。在“Account”一栏中，有个“Update”选项，用户可以手动从浏览器的 Cookie 管理页面复制 OJ 的 Cookie 并填入。然后就可以在 VJ 上用自己的账号交题了。&lt;/p&gt;
&lt;p&gt;问题在于，手动复制太麻烦了。有些 OJ（比如 UVA），几乎每天都要更新 Cookie，手动复制 Cookie 实在繁琐。那为什么 VJ 不能直接获取其他 OJ 的 Cookie 呢？&lt;/p&gt;
&lt;h2 id="浏览器限制"&gt;浏览器限制&lt;/h2&gt;
&lt;p&gt;询问 AI：为什么网站不允许访问其他网站的 Cookie？&lt;/p&gt;
&lt;h3 id="1-保护用户隐私"&gt;1. 保护用户隐私&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cookie 中常包含用户的登录信息、偏好设置、会话 ID 等敏感信息。&lt;/li&gt;
&lt;li&gt;如果任意网站都可以读取其他网站的 Cookie，那用户在其他站点的私人数据将被泄露，严重威胁隐私。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-防止跨站攻击xsscsrf"&gt;2. 防止跨站攻击（XSS、CSRF）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果 A 网站能读取 B 网站的 Cookie，攻击者可以通过 A 网站诱导用户访问，并窃取 B 网站的 Cookie，从而劫持会话。&lt;/li&gt;
&lt;li&gt;这种能力会让 &lt;strong&gt;跨站请求伪造（CSRF）&lt;/strong&gt; 和 &lt;strong&gt;跨站脚本攻击（XSS）&lt;/strong&gt; 更容易实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-遵循同源策略"&gt;3. 遵循同源策略&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同源策略&lt;/strong&gt;是浏览器最基本的安全机制之一，限制网页能访问的资源范围。&lt;/li&gt;
&lt;li&gt;两个网站必须“同源”（协议、域名、端口相同）才能访问彼此的数据（包括 Cookie、localStorage、DOM 等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="初步解决"&gt;初步解决&lt;/h2&gt;
&lt;p&gt;除了用户自己，还有没有什么方式可以拿到 Cookie 呢？使用&lt;strong&gt;用户脚本&lt;/strong&gt;。编写一个在 &lt;a href="https://www.tampermonkey.net/" target="_blank"&gt;Tampermonkey（油猴）&lt;/a&gt;等用户脚本管理器上运行的 JavaScript 程序，就可以自动获取 Cookie 并发送给某个网站。&lt;del&gt;实话实说，这种工作方式很像木马。&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;我们只需要让程序模拟人工操作即可。通过浏览器开发人员工具对 VJ 抓包，我发现了以下关键特征。&lt;/p&gt;
&lt;h2 id="原理"&gt;原理&lt;/h2&gt;
&lt;h3 id="http-请求方式"&gt;HTTP 请求方式&lt;/h3&gt;
&lt;p&gt;VJ用&lt;code&gt;XMLHttpRequest&lt;/code&gt;发送 HTTP 请求，而非更现代的&lt;code&gt;fetch&lt;/code&gt;，它们的 API 有较大差别。&lt;/p&gt;
&lt;h3 id="验证账号及-cookie"&gt;验证账号及 Cookie&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;以下所有操作的前提是 VJ 账号已登录，即所有对 VJ 的请求都带了 VJ 的 Cookie&lt;/li&gt;
&lt;li&gt;在 VJ 中打开 Submit 界面的 My Account 或 Archive 时&lt;/li&gt;
&lt;li&gt;浏览器请求&lt;code&gt;GET /user/verifiedAccount?oj=xxx&lt;/code&gt;，VJ 检测之前是否验证过 xxx 的 Cookie，验证过则返回 &lt;code&gt;{&amp;quot;accountDisplay&amp;quot;: &amp;quot;&amp;lt;a href=&amp;quot;xxx的用户主页&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;用户名&amp;lt;/a&amp;gt;&amp;quot;}&lt;/code&gt;，将&lt;code&gt;class&lt;/code&gt;为&lt;code&gt;my-account&lt;/code&gt;的&lt;code&gt;span&lt;/code&gt;的内容由&lt;code&gt;(Not set)&lt;/code&gt;替换为刚才返回的内容；否则返回&lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果3成功，请求 &lt;code&gt;GET /user/checkAccount?oj=xxx&lt;/code&gt;，VJ 检测 xxx 的 Cookie 是否仍然有效，有效则返回&lt;code&gt;{&amp;quot;result&amp;quot;:&amp;quot;success&amp;quot;}&lt;/code&gt;；否则返回&lt;code&gt;{}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="update-cookie"&gt;Update Cookie&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 Update 页面点击 Confirm 后&lt;/li&gt;
&lt;li&gt;请求&lt;code&gt;POST /user/verifyAccount&lt;/code&gt;，负载为用户账号的 Cookie：&lt;code&gt;{&amp;quot;oj&amp;quot;:&amp;quot;xxx&amp;quot;,&amp;quot;proof&amp;quot;:[{&amp;quot;name&amp;quot;: &amp;quot;...&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;...&amp;quot;},...]}&lt;/code&gt;，响应为&lt;code&gt;{&amp;quot;success&amp;quot;: true, &amp;quot;accountDisplay&amp;quot;: &amp;quot;&amp;lt;a href=&amp;quot;xxx的用户主页&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;用户名&amp;lt;/a&amp;gt;&amp;quot;}&lt;/code&gt;，或&lt;code&gt;{&amp;quot;success&amp;quot;:false,&amp;quot;error&amp;quot;:&amp;quot;Verify failed!&amp;quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果2成功，再请求一次&lt;code&gt;GET /user/checkAccount?oj=xxx&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="脚本详细步骤"&gt;脚本详细步骤&lt;/h2&gt;
&lt;p&gt;理顺了以上流程，我们就可以设计一个油猴脚本了。具体操作如下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;：一定要在油猴的设置里把“安全”中的“允许脚本访问 Cookie”设置为 All，否则获取不到有些 OJ 的 Cookie。因为那些 OJ 把关键 Cookie 设置成了 Http-Only，禁止 JavaScript 访问。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果当前页面为 VJ，并检测到浏览器发起了请求&lt;code&gt;GET /user/verifiedAccount?oj=xxx&lt;/code&gt;或&lt;code&gt;GET /user/checkAccount?oj=xxx&lt;/code&gt;，如果有一个响应失败，则&lt;/li&gt;
&lt;li&gt;获取 xxx 的相应 Cookie&lt;/li&gt;
&lt;li&gt;如果2没有获取到相应的 Cookie，则原因应该为用户未登录到相应 OJ，提示用户登录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /user/verifyAccount&lt;/code&gt;，将负载设为获取到的 Cookie&lt;/li&gt;
&lt;li&gt;如果4成功，则更新提示信息；否则可能是用户 Cookie 过期，提示用户登录&lt;/li&gt;
&lt;li&gt;如果有哪步失败了，加个 Retry 按钮&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="代码"&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/konglils/vjudge-cookie-filler" target="_blank"&gt;Github&lt;/a&gt; | &lt;a href="https://greasyfork.org/zh-CN/scripts/536701-vjudge-cookie-filler" target="_blank"&gt;油叉&lt;/a&gt; | &lt;a href="https://scriptcat.org/zh-CN/script-show-page/3459/" target="_blank"&gt;脚本猫&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这个问题出发点很好 &lt;del&gt;，属于没有困难制造困难也要上&lt;/del&gt;，思路很清晰，以上所述基本就是我的思考过程。因为我对 JavaScript 和油猴脚本都不太熟，所以代码的细节是一点一点和 AI 交流得来的。这个问题能成功解决也证明了 AI 在编程领域的重要作用。程序员不用再死扣细节，重要的是对程序整体架构上的把握，发挥创造能力。&lt;/p&gt;</content:encoded></item><item><title>常见正则表达式的规则及使用</title><link>https://konglils.org/posts/python-re/</link><pubDate>Sat, 01 Mar 2025 22:32:33 +0800</pubDate><guid>https://konglils.org/posts/python-re/</guid><description>本文介绍了常见的正则表达式规则，并带有简要的说明及示例，同时解释了正则表达式捕获组的概念和阻止贪心的基本规则。本文还介绍了 Python re 库的常用函数、方法及特殊规则等。</description><content:encoded>&lt;h2 id="regex101"&gt;&lt;a href="https://regex101.com/" target="_blank"&gt;regex101&lt;/a&gt;&lt;/h2&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;h2 id="正则表达式规则"&gt;正则表达式规则&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th style="text-align: center"&gt;元字符&lt;/th&gt;
					&lt;th style="text-align: center"&gt;说明&lt;/th&gt;
					&lt;th style="text-align: center"&gt;示例&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;匹配数字 (0-9)&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d&lt;/code&gt; → &amp;ldquo;5&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\w&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;字母/数字/下划线 (a-z,0-9,_)&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\w+&lt;/code&gt; → &amp;ldquo;Hello&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\b&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;单词的开始或结束&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\bHello\b&lt;/code&gt; → &amp;quot; Hello &amp;quot;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;任意单个字符（换行除外）&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;h.&lt;/code&gt; → &amp;ldquo;ha&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;0次或多次重复&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;a*&lt;/code&gt; → &amp;ldquo;aa&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;1次或多次重复&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d+&lt;/code&gt; → &amp;ldquo;123&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;0次或1次重复&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;a?&lt;/code&gt; → &amp;ldquo;a&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;{n}&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;精确匹配n次&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d{3}&lt;/code&gt; → &amp;ldquo;123&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;{n,}&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;匹配n次或更多次&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d{3,}&lt;/code&gt; → &amp;ldquo;123&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;{n,m}&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;匹配n到m次&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d{3,4}&lt;/code&gt; → &amp;ldquo;123&amp;rdquo;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;[ ]&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;字符集合&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;[a-c]&lt;/code&gt; → a/b/c&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;开头匹配 / 取反&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;^\d&lt;/code&gt; 开头为数字&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;结尾匹配&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;\d$&lt;/code&gt; 结尾为数字&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;|&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;或&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;a|b&lt;/code&gt; → a 或 b&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;[^x]&lt;/code&gt;&lt;/td&gt;
					&lt;td style="text-align: center"&gt;除了x以外的字符&lt;/td&gt;
					&lt;td style="text-align: center"&gt;&lt;code&gt;[^a]&lt;/code&gt; 除了a的字符&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="捕获组"&gt;捕获组&lt;/h3&gt;
&lt;p&gt;用圆括号括起来的部分就是捕获组，方便进一步获取内容。&lt;/p&gt;
&lt;p&gt;在正则表达式内部或者正则替换时，可以用&lt;code&gt;\k&lt;/code&gt;的方式来引用第&lt;code&gt;k&lt;/code&gt;个捕获组。&lt;/p&gt;
&lt;p&gt;不论捕获组是否嵌套，左括号更前，编号就更前。&lt;/p&gt;
&lt;p&gt;捕获组索引从 1 开始，可以用&lt;code&gt;(?P&amp;lt;name&amp;gt;...)&lt;/code&gt;的方式给捕获组命名。在正则表达式内部通过&lt;code&gt;(?P=name)&lt;/code&gt;来引用命名捕获组；正则替换时用&lt;code&gt;\g&amp;lt;name&lt;/code&gt;来引用对应的捕获组。&lt;/p&gt;
&lt;h3 id="阻止贪心"&gt;阻止贪心&lt;/h3&gt;
&lt;p&gt;正则表达式默认贪心匹配，即匹配最多的字符。比如&lt;code&gt;.*&lt;/code&gt;会匹配尽量多的字符直到换行。&lt;/p&gt;
&lt;p&gt;对此，加上一个&lt;code&gt;?&lt;/code&gt;即可阻止贪心。例如对&lt;code&gt;abc 123&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;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;(.*)\s*[0-9]*&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;abc 123&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;(.*?)\s*[0-9]*&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;abc&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="python-re-使用"&gt;Python re 使用&lt;/h2&gt;
&lt;h3 id="compile"&gt;compile&lt;/h3&gt;
&lt;p&gt;编译正则表达式，加速后续运算：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="match"&gt;match&lt;/h3&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;&lt;code&gt;pattern.match(string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;字符串开头是否匹配模式&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;pattern.fullmatch(string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;字符串全部是否匹配模式&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="search"&gt;search&lt;/h3&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;&lt;code&gt;result = pattern.search(string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;查找匹配模式的第一个子串（找不到则为&lt;code&gt;None&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;result.group()&lt;/code&gt; &lt;code&gt;result.group(0)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;匹配到的子串&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;result.group(k)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;返回第&lt;code&gt;k&lt;/code&gt;个捕获组或者命名捕获组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;result.groups()&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;所有捕获组的元组&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;result.start()&lt;/code&gt; &lt;code&gt;result.end()&lt;/code&gt; &lt;code&gt;result.span()&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;匹配的起止位置&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="其他"&gt;其他&lt;/h3&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;&lt;code&gt;pattern.findall(string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;返回所有匹配结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;pattern.split(string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;按模式分割字符串&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;pattern.sub(sub, string)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;正则替换&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;re.DOTALL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;点号匹配换行符&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;</content:encoded></item><item><title>浮点数和端序</title><link>https://konglils.org/posts/float-and-endian/</link><pubDate>Wed, 18 Dec 2024 22:42:15 +0800</pubDate><guid>https://konglils.org/posts/float-and-endian/</guid><description>本文开头介绍了浮点数的结构（符号、阶码、尾数），以及一些特殊的浮点数（Inf, NaN）。同时，为了更好地验证浮点数的结构，我对端序也探索了一番，写了两个函数，能以二进制形式分别打印大端序和小端序的基本数据类型。我还写了一个 my_float 函数，能通过二进制反向构造内存中的浮点数。</description><content:encoded>&lt;h2 id="float-和-double"&gt;float 和 double&lt;/h2&gt;
&lt;p&gt;计算机中通常用浮点数来表示小数。在 IEEE 754 标准中，规定了两种常用的浮点类型：float和double，分别为单精度和双精度浮点类型。float采用4个字节存储，double采用8个字节存储。浮点类型类似于科学计数法的结构。float的结构是，从高位向低位，依次是1比特符号、8比特阶码、23比特尾数。double的结构是，从高位向低位，依次是1比特符号、11比特阶码、52比特尾数。比如-123.25的float表示是&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1 10000101 11101101000000000000000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - -------- -----------------------
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;符号 阶码 尾数
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;符号位为0表示正数，为1表示负数。阶码部分采用偏移量形式，即实际指数加上&lt;code&gt;0b01111111&lt;/code&gt;（127）。二进制尾数（除了0）的第一个比特必然是1，因此省略这一位以节约空间。double类似，不过偏移量是&lt;code&gt;0b01111111111&lt;/code&gt;（1023）。&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;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;0.0&lt;/td&gt;
					&lt;td&gt;0或1（正0.0等于负0.0）&lt;/td&gt;
					&lt;td&gt;全为0&lt;/td&gt;
					&lt;td&gt;全为0&lt;/td&gt;
					&lt;td&gt;浮点数0.0，或者根据阶码的意义认为是无穷小&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正无穷&lt;/td&gt;
					&lt;td&gt;0&lt;/td&gt;
					&lt;td&gt;全为1&lt;/td&gt;
					&lt;td&gt;全为0&lt;/td&gt;
					&lt;td&gt;浮点数正无穷&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;负无穷&lt;/td&gt;
					&lt;td&gt;1&lt;/td&gt;
					&lt;td&gt;全为1&lt;/td&gt;
					&lt;td&gt;全为0&lt;/td&gt;
					&lt;td&gt;浮点数负无穷&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;NaN&lt;/td&gt;
					&lt;td&gt;0或1&lt;/td&gt;
					&lt;td&gt;全为1&lt;/td&gt;
					&lt;td&gt;非全0&lt;/td&gt;
					&lt;td&gt;Not a Number，非数字，比如C语言里&lt;code&gt;0.0/0.0&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="端序"&gt;端序&lt;/h2&gt;
&lt;p&gt;计算机内存可以看作是一维线性空间（虚拟地址），内存地址用一个无符号整数就能表示。把数据存入内存时，会遇到一个问题，究竟是把数据的高位放在内存的高地址，低位放在低地址，还是把数据的高位放在内存的低地址，低位放在高地址。前者叫小端序（Little-Endian），后者叫大端序（Big-Endian）。争论哪种方式更好确实是个无聊的问题（出自《格列佛游记》，小人国为应该从小端打蛋还是从大端打蛋争论不休）。端序指的仅仅是字节间的关系，字节内部的顺序叫字节序，和现在讨论的内容关系不大。&lt;/p&gt;
&lt;p&gt;存储基本数据类型（整型、浮点型等）时，因为CPU中有相应计算单元，所以此时端序和CPU架构有关，如x86架构采用小端序。当数据结构较为复杂时，端序规则则不太统一，如网络传输中用大端序，JPEG用大端序，BMP用小端序。&lt;/p&gt;
&lt;h2 id="判断端序"&gt;判断端序&lt;/h2&gt;
&lt;p&gt;我们可以逐比特打印数据来判断端序，并按照人类阅读习惯，数据高位在前，低位在后。C代码如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;print_little_endian_binary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr_small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 数据以小端序存储时，打印正确结果
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ptr_small&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;1&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;print_big_endian_binary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr_small&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 数据以大端序存储时，打印正确结果
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;ptr_small&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;1&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;putchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文章开头数据可以通过在x86计算机上执行以下代码得到&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;123.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;print_little_endian_binary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="my_float"&gt;my_float&lt;/h2&gt;
&lt;p&gt;有意思的是，根据C语言标准，结构体内各字段的存储方式总是先定义的字段在低地址，后定义的字段在高地址，而且结构体还支持位字段，每个基本数据类型字段的端序仍和CPU架构相关。所以我们可以在x86计算机上以如下方式定义一个my_float：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;my_float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nl"&gt;tail&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nl"&gt;expo&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nl"&gt;sign&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nf"&gt;build_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;my_float&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 也可使用共用体
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以&lt;code&gt;build_float(1, 6, 0b11101101000000000000000)&lt;/code&gt;可得float类型的-123.25。&lt;/p&gt;</content:encoded></item><item><title>一种开方算法</title><link>https://konglils.org/posts/square-root-algorithm/</link><pubDate>Sat, 27 May 2023 12:43:57 +0800</pubDate><guid>https://konglils.org/posts/square-root-algorithm/</guid><description>一种化简双重根号式子的方法，涵盖证明与思考过程。</description><content:encoded>&lt;h2 id="介绍"&gt;介绍&lt;/h2&gt;
&lt;p&gt;这个方法是我之前做数学题的时候，苦于没有一个好的方法可以化简有双重根号的式子，而费了一番功夫想出来的。如下所示：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp; 若存在 x, y, a, b \in R，且 x &amp;gt; y &amp;gt; 0, a &amp;gt; b &amp;gt; 0，使得 x + y = \sqrt{a+b}或x - y = \sqrt{a-b}，\\
&amp;amp;则 x = \sqrt{\frac {a + \sqrt{a^2-b^2}} 2}, y = \sqrt{\frac {a - \sqrt{a^2-b^2}} 2} \end{aligned}
$$&lt;/p&gt;
&lt;h2 id="证明"&gt;证明&lt;/h2&gt;
&lt;p&gt;证明非常简单，把上面几个式子代换一下，就可以得到关于a和b的两个恒等式。&lt;/p&gt;
&lt;h2 id="思考过程"&gt;思考过程&lt;/h2&gt;
&lt;p&gt;我认为想法比结论更重要。因为有时结论似乎挺显而易见的。但是为了得到结论，思考过程通常不是一帆风顺的，而是包含了许多不成熟的想法，认清了它们之后，就要从思维的岔路中折转回去。最终，我们会得到想要的答案。而且，我们不仅收获了结论，还领悟到了思考过程中那些从前不为我们所知的东西。&lt;/p&gt;
&lt;p&gt;我整理了一下我的思考过程，发现这种方法有点类似向量。&lt;/p&gt;
&lt;p&gt;根据条件，结论应该是由正面推出来的。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp; 已知 x+y = \sqrt{a+b} \\
&amp;amp; 两边平方，得 x^2 + y^2 + 2xy = a + b \quad (1) \\
&amp;amp; 令 2xy = b，则 y = \frac b {2x} \quad (2)，此时 (1) 变为 x^2 + y^2 = a \\
&amp;amp; 将(2)代入(1)，得 x^2 + \frac {b^2} {4x^2} = a，即 x^4 - ax^2 + \frac {b^2} 4 = 0 \\
&amp;amp; 解得x^2 = \frac {a \pm \sqrt {a^2-b^2}} 2，再由(2)可得y^2 = \frac {a \pm \sqrt {a^2-b^2}} 2。显然 a - \sqrt {a^2-b^2} &amp;gt; 0 \\
&amp;amp; 又x &amp;gt; y &amp;gt; 0，所以 x = \sqrt{\frac {a + \sqrt{a^2-b^2}} 2}, y = \sqrt{\frac {a - \sqrt{a^2-b^2}} 2} \\
&amp;amp; 同理，可算出 x-y = \sqrt{a-b} 时x与y的值仍与上述结果相同。
\end{aligned}
$$&lt;/p&gt;</content:encoded></item><item><title>斐波那契数列计算</title><link>https://konglils.org/posts/fibonacci-sequence/</link><pubDate>Sun, 16 Apr 2023 12:35:57 +0800</pubDate><guid>https://konglils.org/posts/fibonacci-sequence/</guid><description>解决 SICP 练习1.13，得到了斐波那契数列的精确数学表达式。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;练习1.13 证明 $Fib(n)$ 是最接近 $\phi ^ n / \sqrt {5}$ 的整数，其中 $\phi = (1 + \sqrt {5}) / 2$。提示：利用归纳法和斐波那契数列的定义，证明 $Fib(n) = (\phi ^ n - \gamma ^ n) / \sqrt {5}$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;斐波那契数列定义如下：&lt;/p&gt;
&lt;p&gt;$$
Fib(n) =
\begin{cases}
0 &amp;amp; 如果n=0 \\
1 &amp;amp; 如果n=1 \\
Fib(n-1)+Fib(n-2) &amp;amp; 否则
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;这是&lt;a href="https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/full-text/book/book.html" target="_blank"&gt;SICP&lt;/a&gt;里的一个小练习。一种证明方法如下：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp; 若a_n为斐波那契数列，则a_{n+2}=a_{n+1}+a_{n}，a_1=1,a_2=1，n \in N_+。\\
&amp;amp; 设 \lambda \in R，则a_{n+2}+ \lambda a_{n+1} = (1+ \lambda) a_{n+1} + a_{n} \\
&amp;amp; 即 \frac {a_{n+2}+ \lambda a_{n+1}} {a_{n+1} + \frac {1} {1+ \lambda} a_{n}} = 1 + \lambda \\
&amp;amp; 令 \lambda = \frac {1} {1+ \lambda}，解得\lambda_1=\frac {-1-\sqrt {5}} {2}，\lambda_2=\frac {-1+\sqrt {5}} {2} \\
&amp;amp; 此时a_{n+1}+ \lambda a_n为等比数列，即 \\
&amp;amp; a_{n+1}+ \lambda_1 a_n = (1+\lambda_1)(1+\lambda_1)^{n-1} = (1+\lambda_1)^n \quad (1) \\
&amp;amp; a_{n+1}+ \lambda_2 a_n = (1+\lambda_2)(1+\lambda_2)^{n-1} = (1+\lambda_2)^n \quad (2) \\
&amp;amp; (2)-(1)，得 \sqrt5 a_n = (\frac {1+\sqrt5} 2) ^ n - (\frac {1-\sqrt5} 2) ^ n \\
&amp;amp; 所以 a_n = \frac {(\frac {1+\sqrt5} 2) ^ n - (\frac {1-\sqrt5} 2) ^ n } {\sqrt5} \\
&amp;amp; n=0时可得a_0=0，符合定义。所以a_n即为斐波那契数列的通项公式。\\
&amp;amp; 因为-1 &amp;lt; \frac {1-\sqrt5} 2 &amp;lt; 0，所以当n越来越大时，(\frac {1-\sqrt5} 2) ^ n会趋近于0，且正负符号交替出现。\\
&amp;amp; 因此可以说Fib(n)是最接近 \phi ^ n / \sqrt {5} 的整数。
\end{aligned}
$$&lt;/p&gt;</content:encoded></item><item><title>二项分布均值与方差计算</title><link>https://konglils.org/posts/binomial-distribution/</link><pubDate>Sat, 15 Apr 2023 12:12:44 +0800</pubDate><guid>https://konglils.org/posts/binomial-distribution/</guid><description>计算二项分布均值和方差的完整公式。</description><content:encoded>&lt;p&gt;对于 $X∼B(n,p)$ 的第k项，其概率为 $P(X=k) = C_n^k p^k(1-p)^(n-k)$。&lt;/p&gt;
&lt;p&gt;下面的计算中我用了几个变形技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;易证当存在 $n, k \in N, 且 n \ge k 时，有 kC_n^k = nC_{n-1}^{k-1}$&lt;/li&gt;
&lt;li&gt;二项展开式 $(a + b)^n = \sum_{k=0}^{n} C_n^k a^{n-k} b^k$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;则&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
EX &amp;amp; = \sum_{k=0}^{n}kC_{n}^{k}p^{k}(1-p)^{n-k} \\
&amp;amp; = \sum_{k=1}^{n}kC_{n}^{k}p^{k}(1-p)^{n-k} \\
&amp;amp; = \sum_{k=1}^{n}nC_{n-1}^{k-1}p^{k}(1-p)^{n-k} \\
&amp;amp; = np\sum_{k=1}^{n}C_{n-1}^{k-1}p^{k-1}(1-p)^{n-k} \\
&amp;amp; = np(p+1-p)^{n-1} \\
&amp;amp; = np
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
DX &amp;amp; = \sum_{k=0}^{n}(k-EX)^2P(X=k) \\
&amp;amp; = \sum_{k=0}^{n}(k-np)^2C_{n}^{k}p^{k}(1-p)^{n-k} \\
&amp;amp; = \sum_{k=0}^{n}(k^2-2knp+n^2p^2)C_{n}^{k}p^{k}(1-p)^{n-k} \\
&amp;amp; = \sum_{k=0}^{n}k^2C_{n}^{k}p^{k}(1-p)^{n-k} -2np\sum_{k=0}^{n}kC_{n}^{k}p^{k}(1-p)^{n-k} + n^2p^2\sum_{k=0}^{n}C_{n}^{k}p^{k}(1-p)^{n-k} \\
&amp;amp; = \sum_{k=0}^{n}k^2C_{n}^{k}p^{k}(1-p)^{n-k} - 2np \cdot np + n^2p^2(p+1-p)^n \\
&amp;amp; = \sum_{k=1}^{n}k \cdot kC_{n}^{k}p^{k}(1-p)^{n-k} - n^2p^2 \\
&amp;amp; = \sum_{k=1}^{n}k \cdot nC_{n-1}^{k-1}p^{k}(1-p)^{n-k} - n^2p^2 \\
&amp;amp; = np\sum_{k=1}^{n}kC_{n-1}^{k-1}p^{k-1}(1-p)^{n-k} - n^2p^2 \\
&amp;amp; = np[\sum_{k=1}^{n}(k-1)C_{n-1}^{k-1}p^{k-1}(1-p)^{n-k} + \sum_{k=1}^{n}C_{n-1}^{k-1}p^{k-1}(1-p)^{n-k}] - n^2p^2 \\
&amp;amp; = np[(n-1)p + (p+1-p)^{n-1}] - n^2p^2 \\
&amp;amp; = np(1-p)
\end{aligned}
$$&lt;/p&gt;</content:encoded></item><item><title>常用科学常数</title><link>https://konglils.org/posts/common-constant/</link><pubDate>Sat, 14 Jan 2023 12:03:54 +0800</pubDate><guid>https://konglils.org/posts/common-constant/</guid><description>&lt;ul&gt;
&lt;li&gt;圆周率 $\pi = 3.14159\ 26535\ 89793$&lt;/li&gt;
&lt;li&gt;自然底数 $e = 2.71828\ 18284\ 59045$&lt;/li&gt;
&lt;li&gt;光速 $c = 299792458\ m/s$&lt;/li&gt;
&lt;li&gt;基本电荷 $e = 1.602176634 \times 10^{-19}\ C$&lt;/li&gt;
&lt;li&gt;普朗克常数 $h = 6.62607015 \times 10^{-34}\ J \cdot s$&lt;/li&gt;
&lt;li&gt;阿伏伽德罗常数 $N_A = 6.02214076 \times 10^{23}\ mol^{-1}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;部分常数的位数做了取舍。物理相关常数摘自中国计量科学研究院提供的&lt;a href="https://www.nim.ac.cn/sites/default/files/inline-files/1%20%E7%AC%AC26%E5%B1%8A%E5%9B%BD%E9%99%85%E8%AE%A1%E9%87%8F%E5%A4%A7%E4%BC%9A%EF%BC%88CGPM%EF%BC%89%E5%86%B3%E8%AE%AE-%20%E5%9B%BD%E9%99%85%E5%8D%95%E4%BD%8D%E5%88%B6%E4%BF%AE%E8%AE%A2%EF%BC%88%E4%B8%AD%E6%96%87%E7%89%88%EF%BC%89.pdf" target="_blank"&gt;第26届国际计量大会（CGPM）决议&lt;/a&gt;。&lt;/p&gt;</description><content:encoded>&lt;ul&gt;
&lt;li&gt;圆周率 $\pi = 3.14159\ 26535\ 89793$&lt;/li&gt;
&lt;li&gt;自然底数 $e = 2.71828\ 18284\ 59045$&lt;/li&gt;
&lt;li&gt;光速 $c = 299792458\ m/s$&lt;/li&gt;
&lt;li&gt;基本电荷 $e = 1.602176634 \times 10^{-19}\ C$&lt;/li&gt;
&lt;li&gt;普朗克常数 $h = 6.62607015 \times 10^{-34}\ J \cdot s$&lt;/li&gt;
&lt;li&gt;阿伏伽德罗常数 $N_A = 6.02214076 \times 10^{23}\ mol^{-1}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;部分常数的位数做了取舍。物理相关常数摘自中国计量科学研究院提供的&lt;a href="https://www.nim.ac.cn/sites/default/files/inline-files/1%20%E7%AC%AC26%E5%B1%8A%E5%9B%BD%E9%99%85%E8%AE%A1%E9%87%8F%E5%A4%A7%E4%BC%9A%EF%BC%88CGPM%EF%BC%89%E5%86%B3%E8%AE%AE-%20%E5%9B%BD%E9%99%85%E5%8D%95%E4%BD%8D%E5%88%B6%E4%BF%AE%E8%AE%A2%EF%BC%88%E4%B8%AD%E6%96%87%E7%89%88%EF%BC%89.pdf" target="_blank"&gt;第26届国际计量大会（CGPM）决议&lt;/a&gt;。&lt;/p&gt;</content:encoded></item></channel></rss>