<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>The Siolin Memo</title><description>Built with Astro-Pure</description><link>https://siolin.me</link><item><title>秒杀解决方案</title><link>https://siolin.me/blog/backend/untitled</link><guid isPermaLink="true">https://siolin.me/blog/backend/untitled</guid><description>Write your description here.</description><pubDate>Sun, 28 Dec 2025 11:22:52 GMT</pubDate><content:encoded>&lt;p&gt;Write your content here.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>基于分布式锁解决缓存击穿</title><link>https://siolin.me/blog/backend/mutex</link><guid isPermaLink="true">https://siolin.me/blog/backend/mutex</guid><description>循环重试+TTL刷新</description><pubDate>Sun, 28 Dec 2025 11:12:34 GMT</pubDate><content:encoded>&lt;p&gt;核心思想：&lt;strong&gt;同一时间只允许一个线程去查询数据库&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存过期了，大家一起去抢锁&lt;/li&gt;
&lt;li&gt;抢到锁的线程去查询数据库&lt;/li&gt;
&lt;li&gt;没抢到锁的线程等待，然后重试&lt;/li&gt;
&lt;li&gt;抢到锁的线程写完缓存后，其他线程可以直接从缓存读&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;假如线程 1 先来访问，查询缓存没有命中，那么其会获取互斥锁，然后去执行查询数据库的逻辑&lt;/li&gt;
&lt;li&gt;线程 2 查询缓存同样没有命中，由于互斥锁已经被占用，所以其无法获取，只能执行 sleep 进行休眠&lt;/li&gt;
&lt;li&gt;等到线程 1 释放锁后，线程 2 会被唤醒并获取锁，但是其不是直接查询数据库，而是进行递归来查询缓存
&lt;img src=&quot;images/mutexeg.png&quot; alt=&quot;互斥锁举例&quot;&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;执行流程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1. 查询 Redis
   ├─ 存在且非空 → 刷新 TTL 后返回（热数据保活）
   ├─ 存在但为空 → 返回 null（空值缓存，不刷新 TTL）
   └─ 不存在 → 继续步骤 2

2. 循环尝试获取锁（最多 100 次）
   ├─ 获取成功
   │    ├─ Double Check：再查一次 Redis
   │    │    ├─ 缓存已存在 → 刷新 TTL 后返回
   │    │    └─ 缓存仍不存在 → 查数据库并写入缓存
   │    └─ 释放锁
   └─ 获取失败
        └─ 等待 50ms 后重试

3. 重试超限（100 次）→ 直接查数据库作为兜底
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码实现：
&lt;img src=&quot;images/mutex.png&quot; alt=&quot;image-20251228111727066&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>基于逻辑过期解决缓存击穿</title><link>https://siolin.me/blog/backend/logicalexpire</link><guid isPermaLink="true">https://siolin.me/blog/backend/logicalexpire</guid><description>异步重建+双重检查+TTL刷新。</description><pubDate>Sat, 27 Dec 2025 23:41:27 GMT</pubDate><content:encoded>&lt;p&gt;核心思想：&lt;strong&gt;缓存永不过期，但存储一个&quot;逻辑过期时间&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询时判断是否已逻辑过期&lt;/li&gt;
&lt;li&gt;如果未过期，直接返回缓存数据&lt;/li&gt;
&lt;li&gt;如果已过期，尝试获取锁，只让一个线程去重建缓存&lt;/li&gt;
&lt;li&gt;其他线程直接返回旧数据（不等待）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;执行流程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1. 查询 Redis
   └─ 不存在 → 返回 null（说明从来没有缓存过）
2. 存在，反序列化为 RedisData
3. 判断逻辑过期时间
   └─ 未过期 → 刷新逻辑过期时间后返回（热数据保活）
4. 已过期，尝试获取锁
   ├─ 获取锁失败 → 直接返回旧数据（不等待）
   └─ 获取锁成功 → Double Check 后异步重建
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!question] 为什么缓存不存在时不去查数据库，而是直接返回 null？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为 &lt;code&gt;queryWithLogicalExpire&lt;/code&gt; &lt;strong&gt;这个方法是专为“热点 Key”设计的，它的前提假设是缓存已经“预热”过了&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!question] 为什么要进行 Double Check（双重检查）？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;可能有其他线程已经重建了缓存&lt;/li&gt;
&lt;li&gt;避免重复查询数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!question] 为什么返回旧数据？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;用户体验：比返回错误强&lt;/li&gt;
&lt;li&gt;数据一致性：热点数据变化不会太频繁&lt;/li&gt;
&lt;li&gt;异步更新：后台线程很快会更新缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/logicalExpire.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：375. 猜数字大小 II</title><link>https://siolin.me/blog/dsa/375</link><guid isPermaLink="true">https://siolin.me/blog/dsa/375</guid><description>解题思路与思考。</description><pubDate>Wed, 24 Dec 2025 12:12:59 GMT</pubDate><content:encoded>&lt;p&gt;如何理解“确保获胜的最小现金”？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;最坏情况&lt;/strong&gt;：当猜数字 $k$ 时，如果猜错了，那么目标数字可能在左边，也可能在右边。为了确保能赢，需要准备应对更费钱的那一边&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最优策略&lt;/strong&gt;：虽然需要应对最坏情况，但是可以通过选择「第一次、第二次...猜哪个数字」，使得该「最坏情况下的开销」尽可能小&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;状态定义&lt;/h2&gt;
&lt;p&gt;设 $dp[i][j]$ 为：从 $i$ 到 $j$ 这个范围内，确保能赢所需要准备的最少钱数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标：求 $dp[1][n]$&lt;/li&gt;
&lt;li&gt;初始化：当 $i \ge j$ 时，$dp[i][j] = 0$，因为只有一个数字（一下子就能猜对）或者没有数字了，不用付钱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;状态转移方程&lt;/h2&gt;
&lt;p&gt;假设在区间 $[i, j]$ 猜数字 $k$（其中 $i \le k \le j$）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果猜 $k$ 猜错了，需要支付 $k$ 元&lt;/li&gt;
&lt;li&gt;接下来，目标数字要么在左区间 $[i, k - 1]$，要么在右区间 $[k + 1, j]$&lt;/li&gt;
&lt;li&gt;为了确保能赢，至少需要准备 $k + max(dp[i][k - 1], dp[k + 1][j])$&lt;/li&gt;
&lt;li&gt;同时为求最优策略，我们需要枚举所有的 $k$，取其中的最小值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;状态转移公式：
$$dp[i][j] = \min_{i \le k \le j} { k + \max(dp[i][k-1], dp[k+1][j]) }$$&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int getMoneyAmount(int n) {
	// dp[i][j] 表示从 i 到 j 确保获胜的最小金额
	// n + 2是为了防止 k + 1溢出
	int[][] dp = new int[n + 2][n + 2];

	// 枚举区间长度 len，从长度 2 开始（长度 1 的开销是 0）
	for (int len = 2; len &amp;#x3C;= n; len++) {
		// 枚举左端点 i
		for (int i = 1; i &amp;#x3C;= n - len + 1; i++) {
			int j = i + len - 1; // 右端点
			
			dp[i][j] = Integer.MAX_VALUE;

			// 枚举在该区间内第一次猜哪个数字 k
			for (int k = i; k &amp;#x3C;= j; k++) {
				int res = k + Math.max(dp[i][k - 1], dp[k + 1][j]);
				dp[i][j] = Math.min(dp[i][j], res);
			}
		}
	}
	return dp[1][n];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：132. 分割回文串 II</title><link>https://siolin.me/blog/dsa/132</link><guid isPermaLink="true">https://siolin.me/blog/dsa/132</guid><description>解题思路与思考。</description><pubDate>Wed, 24 Dec 2025 12:10:04 GMT</pubDate><content:encoded>&lt;h2&gt;状态定义&lt;/h2&gt;
&lt;p&gt;题目要求将字符串 $s[0 \dots i]$ 切成若干段，使得每一段都是回文。&lt;/p&gt;
&lt;p&gt;无论怎么切，最后一段 $s[j+1 \dots i]$ &lt;strong&gt;必须是一个回文串&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果我们枚举所有可能的 $j$，使得 $s[j+1 \dots i]$ 是回文。&lt;/li&gt;
&lt;li&gt;那么剩下的问题就变成了：如何用最少的次数切割前面的部分 $s[0 \dots j]$。&lt;/li&gt;
&lt;li&gt;而“切割 $s[0 \dots j]$ 的最少次数”正是我们之前已经计算出来的子问题 $dp[j]$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此设 $dp[i]$ 为：字符串前缀 $s[0...i]$ 的最少切割次数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标：$dp[n - 1]$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;状态转移方程&lt;/h2&gt;
&lt;p&gt;假设正在处理字符串 $s[0...i]$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $s[0...i]$ 本身就是回文，那么 $dp[i] = 0$，不需要切割&lt;/li&gt;
&lt;li&gt;否则，尝试在中间某处 $j$ 切一刀。如果 $s[j+1...i]$ 是回文，那么：
$$dp[i] = \min(dp[i], dp[j] + 1)$$&lt;/li&gt;
&lt;li&gt;如果 $s[j + 1...i]$ 不是回文，那么无需搭理，其不是合法方案&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int minCut(String str) {
	char[] s = str.toCharArray();
	int n = s.length;

	// 预处理回文数组
	boolean[][] isPal = new boolean[n][n];
	for (int j = 0; j &amp;#x3C; n; ++j) {
		for (int i = 0; i &amp;#x3C;= j; ++i) {
			// 两端相等，且内部也是回文（或者内部为空）
			if (s[i] == s[j] &amp;#x26;&amp;#x26; (j - i &amp;#x3C;= 2 || isPal[i + 1][j - 1])) {
				isPal[i][j] = true;
			}
		}
	}

	int[] dp = new int[n];
	for (int i = 0; i &amp;#x3C; n; ++i) {
		// 如果 0...i 本身就是回文串，不需要处理
		if (isPal[0][i]) {
			continue;
		}

		// 最坏情况下，前 i + 1 个字符需要切割 i 次
		dp[i] = i;
		for (int j = 0; j &amp;#x3C; i; ++j) {
			// 如果 j+1...i 是回文串，尝试在 j 后面切一刀
			if (isPal[j + 1][i]) {
				dp[i] = Math.min(dp[i], dp[j] + 1);
			}
		}
	}

	return dp[n - 1];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>短信登陆</title><link>https://siolin.me/blog/backend/login</link><guid isPermaLink="true">https://siolin.me/blog/backend/login</guid><description>使用Redis实现用户短信登陆。</description><pubDate>Mon, 22 Dec 2025 22:55:58 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;images/login.png&quot; alt=&quot;短信登陆流程图&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1 技术选型&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么使用 Redis 来代替 Session？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;集群挑战&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Session 数据存储在 &lt;strong&gt;JVM 的堆内存&lt;/strong&gt;中，在单机环境下没问题。但是在生产环境的&lt;strong&gt;集群部署&lt;/strong&gt;（多台服务器跑同一个项目）下，负载均衡器（比如 Nginx）会将请求分发到不同的服务器。&lt;/li&gt;
&lt;li&gt;如果用户在服务器 A 登陆，Session 存在 A 的内存里。该用户的下一次请求被分发到了服务器 B，B 内存中没有其 Session，那么就会认证失败&lt;/li&gt;
&lt;li&gt;Redis 是&lt;strong&gt;分布式缓存系统&lt;/strong&gt;，所有的服务器可以去同一个 Redis 集群读写数据&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;数据可靠性&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Session 的生命周期&lt;strong&gt;依赖于进程&lt;/strong&gt;，一旦后端程序崩溃或重启，那么所有用户的登录状态都会消失，那么用户的体验感极差&lt;/li&gt;
&lt;li&gt;Redis 虽然也是基于内存，但是其运行在独立的进程中&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么使用 Hash 存储用户信息，而不是 String？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内存效率&lt;/strong&gt;：Redis 的 Hash 结构在字段较少时使用 &lt;code&gt;ziplist&lt;/code&gt; 存储，内存占用极其紧凑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作粒度&lt;/strong&gt;：可以利用 &lt;code&gt;HSET&lt;/code&gt; 或 &lt;code&gt;HGET&lt;/code&gt; 针对单个属性（如更新昵称）进行操作，而 String 则需要进行全序列化和反序列化&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;2 系统设计与架构&lt;/h2&gt;
&lt;h3&gt;2.1 Redis 数据模型设计&lt;/h3&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;String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt;: &lt;code&gt;login:code:{phone}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;: 2 分钟&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;Hash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt;: &lt;code&gt;login:token:{token}&lt;/code&gt; (Token 采用随机 UUID)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt;: 30 分钟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 核心业务流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;发送验证码&lt;/strong&gt;：校验手机号 -&gt; 生成验证码 -&gt; 存入 Redis -&gt; 发送短信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;登录/注册&lt;/strong&gt;：校验验证码 -&gt; 数据库查/增用户 -&gt; &lt;strong&gt;生成随机 Token&lt;/strong&gt; -&gt; 脱敏处理 (UserDTO) -&gt; 存入 Redis 并返回 Token。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;3 核心代码实现&lt;/h2&gt;
&lt;p&gt;实现时要注意 &lt;code&gt;StringRedisTemplate&lt;/code&gt;对值类型的要求。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 核心逻辑：用户信息序列化与存储
public String login(LoginFormDTO loginForm) {
    // ... 校验逻辑 ...
    
    // 1. 生成唯一凭证（Token）
    String token = UUID.randomUUID().toString(true);
    
    // 2. 对象脱敏与类型转换
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    
    // 3. 将 Bean 转为 Map，并强制将所有字段转为 String
    Map&amp;#x3C;String, Object&gt; userMap = BeanUtil.beanToMap(userDTO, new HashMap&amp;#x3C;&gt;(),
        CopyOptions.create()
            .setIgnoreNullValue(true)
            .setFieldValueEditor((fieldName, fieldValue) -&gt; {
                if (fieldValue == null) return null;
                return fieldValue.toString();
            }));
            
    // 4. 写入 Redis 并设置有效期
    String tokenKey = LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    
    return token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4 滚动过期&lt;/h2&gt;
&lt;p&gt;为了提升用户体验，需要实现 &lt;strong&gt;“滚动过期”&lt;/strong&gt; 机制：用户在活跃期间，Token 有效期应自动续期，只有长时间无操作才会过期。&lt;/p&gt;
&lt;h3&gt;4.1 单拦截器方案的缺陷&lt;/h3&gt;
&lt;p&gt;如果仅在 &lt;code&gt;LoginInterceptor&lt;/code&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;，拦截器不会执行，Token 将在 30 分钟后过期，导致用户在进行需要登录的操作时被意外踢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 解决方案：双拦截器架构&lt;/h3&gt;
&lt;p&gt;引入两个拦截器，职责分离，解决上述问题：&lt;/p&gt;
&lt;p&gt;| &lt;strong&gt;拦截器&lt;/strong&gt;                  | &lt;strong&gt;拦截范围&lt;/strong&gt;         | &lt;strong&gt;核心职责&lt;/strong&gt;                                                 | &lt;strong&gt;执行顺序&lt;/strong&gt; |
| :-------------------------- | :------------------- | :----------------------------------------------------------- | :----------- |
| &lt;strong&gt;RefreshTokenInterceptor&lt;/strong&gt; | &lt;strong&gt;所有请求&lt;/strong&gt; (&lt;code&gt;/**&lt;/code&gt;) | 1. 尝试获取请求头中的 Token。  2. 若 Token 有效，则&lt;strong&gt;刷新其在 Redis 中的有效期&lt;/strong&gt;。  3. 将用户信息存入 &lt;code&gt;ThreadLocal&lt;/code&gt;，供后续流程使用。  4. 无论是否成功，&lt;strong&gt;均放行&lt;/strong&gt;。 | 第一         |
| &lt;strong&gt;LoginInterceptor&lt;/strong&gt;        | &lt;strong&gt;需要登录的路径&lt;/strong&gt;   | 1. 检查 &lt;code&gt;ThreadLocal&lt;/code&gt; 中是否存在用户信息。  2. 若存在，说明已登录，放行。  3. 若不存在，则拦截并返回“未登录”状态码（401）。 | 第二         |&lt;/p&gt;
&lt;p&gt;拦截器流程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/interceptor.png&quot; alt=&quot;拦截器流程图&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RefreshTokenInterceptor&lt;/code&gt; 核心逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean preHandle(HttpServletRequest request, ...) {
    // 1. 获取请求头中的 token
    String token = request.getHeader(&quot;authorization&quot;);
    if (StrUtil.isBlank(token)) {
        // 无 token，直接放行，由 LoginInterceptor 决定是否拦截
        return true;
    }
    
    // 2. 基于 token 从 Redis 获取用户信息
    String tokenKey = getTokenCacheKey(token);
    Map&amp;#x3C;Object, Object&gt; userMap = redisTemplate.opsForHash().entries(tokenKey);
    if (userMap.isEmpty()) {
        // token 无效，直接放行
        return true;
    }
    
    // 3. 将 Hash 数据转换回 UserDTO 对象
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    // 4. 保存用户信息到 ThreadLocal
    UserHolder.saveUser(userDTO);
    // 5. 刷新 token 有效期（实现滚动过期）
    redisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]&lt;/p&gt;
&lt;p&gt;在拦截器的 &lt;code&gt;afterCompletion&lt;/code&gt;方法中，必须调用 &lt;code&gt;UserHolder.removeUser()&lt;/code&gt;。这是因为 Tomcat 线程池会复用线程，如果不手动清理，会导致 ThreadLocal 中的数据被错误带入下一个请求，并造成内存泄漏。&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：1039.多边形三角剖分的最低得分</title><link>https://siolin.me/blog/dsa/1039</link><guid isPermaLink="true">https://siolin.me/blog/dsa/1039</guid><description>解题思路与思考。</description><pubDate>Mon, 22 Dec 2025 12:30:37 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://leetcode.cn/problems/minimum-score-triangulation-of-polygon/description/&quot;&gt;&lt;strong&gt;1039.多边形三角剖分的最低得分&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;核心逻辑：从一条边开始“切分”&lt;/h2&gt;
&lt;p&gt;假设有一个凸 $n$ 边形，顶点数值存在数组 $A$ 中。我们的目标是将它剖分成 $n-2$ 个三角形，使得所有三角形顶点的乘积之和最小。&lt;/p&gt;
&lt;p&gt;可以想象手中拿着一个凸多边形，每次切去一个角（一个三角形），直到最后只剩一个三角形。因此与其纠结「第一次切去哪个三角形」，不如考虑「&lt;strong&gt;最后保留哪个三角形&lt;/strong&gt;」。&lt;/p&gt;
&lt;p&gt;对于任何一个由顶点 $i$ 到顶点 $j$ 构成的多边形（记为区间 $[i, j]$），我们可以固定&lt;strong&gt;底边&lt;/strong&gt;（连接顶点 $i$ 和 $j$ 的边）。在最终的剖分方案中，&lt;strong&gt;这条底边一定属于某一个三角形&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假设这个三角形的第三个顶点是 $k$（其中 $k$ 在 $i$ 和 $j$ 之间），那么这个三角形 $(i, k, j)$ 就把原来的多边形切成了三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;左边：&lt;/strong&gt; 由顶点 $i$ 到 $k$ 构成的多边形。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中间：&lt;/strong&gt; 三角形 $(i, k, j)$ 本身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右边：&lt;/strong&gt; 由顶点 $k$ 到 $j$ 构成的多边形。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;定义状态与转移方程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态定义&lt;/strong&gt;：$dp[i][j]$ 表示从顶点 $i$ 到顶点 $j$ 连成的&lt;strong&gt;子多边形&lt;/strong&gt;进行三角剖分后的最低得分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转移方程&lt;/strong&gt;：
我们需要枚举 $i$ 和 $j$ 之间所有的可能顶点 $k$：    $$dp[i][j] = \min_{i &amp;#x3C; k &amp;#x3C; j} { dp[i][k] + dp[k][j] + A[i] \times A[k] \times A[j] }$$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界条件&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;如果 $i$ 和 $j$ 之间没有顶点（即 $j - i &amp;#x3C; 2$），无法形成三角形，$dp[i][j] = 0$。&lt;/li&gt;
&lt;li&gt;当 $j - i = 2$ 时，$dp[i][j]$ 就是唯一的那个三角形 $A[i] \times A[i+1] \times A[j]$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;遍历顺序&lt;/h2&gt;
&lt;p&gt;观察方程，$dp[i][j]$ 依赖于 $dp[i][k]$ 和 $dp[k][j]$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$dp[i][k]$：在 $dp[i][j]$ 的左侧（同一行）。&lt;/li&gt;
&lt;li&gt;$dp[k][j]$：在 $dp[i][j]$ 的下方（不同行，$k &gt; i$）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，遍历顺序与&lt;a href=&quot;516&quot;&gt;&lt;strong&gt;516.最长回文子序列&lt;/strong&gt;&lt;/a&gt;一致：&lt;strong&gt;$i$ 从大到小（从下往上），$j$ 从小到大（从左往右）&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int minScoreTriangulation(int[] values) {
    int n = values.length;
    int[][] dp = new int[n][n];

    // i 从下往上遍历
    for (int i = n - 3; i &gt;= 0; i--) {
        // j 从左往右遍历，且 j 与 i 之间至少要隔一个点
        for (int j = i + 2; j &amp;#x3C; n; j++) {
            // 初始化为一个较大值
            int minRes = Integer.MAX_VALUE;
            // 枚举中间顶点 k
            for (int k = i + 1; k &amp;#x3C; j; k++) {
                int score = dp[i][k] + dp[k][j] + values[i] * values[k] * values[j];
                minRes = Math.min(minRes, score);
            }
            dp[i][j] = minRes;
        }
    }
    return dp[0][n - 1];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：516.最长回文子序列</title><link>https://siolin.me/blog/dsa/516</link><guid isPermaLink="true">https://siolin.me/blog/dsa/516</guid><description>解题思路与思考。</description><pubDate>Mon, 22 Dec 2025 12:28:05 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://leetcode.cn/problems/longest-palindromic-subsequence/description/&quot;&gt;516. 最长回文子序列&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1 核心逻辑&lt;/h2&gt;
&lt;p&gt;回文序列的定义是正读反读都一样。对于一个子串 $s[i \dots j]$，我们要找它的最长回文子序列，关键看它的两个端点字符 $s[i]$ 和 $s[j]$：&lt;/p&gt;
&lt;p&gt;情况 A：$s[i] == s[j]$，如果首尾字符相等，那么这两个字符一定可以作为回文序列的最外层。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转移&lt;/strong&gt;：我们只需要知道中间部分 $s[i+1 \dots j-1]$ 的最长回文长度，然后加上这两个字符。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方程&lt;/strong&gt;：$dp[i][j] = dp[i+1][j-1] + 2$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;情况 B：$s[i] \ne s[j]$，如果首尾不相等，说明它们两个不可能同时出现在同一个回文子序列的最外层。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转移&lt;/strong&gt;：我们要么放弃 $s[i]$，看 $s[i+1 \dots j]$ 的结果；要么放弃 $s[j]$，看 $s[i \dots j-1]$ 的结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方程&lt;/strong&gt;：$dp[i][j] = \max(dp[i+1][j], dp[i][j-1])$&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2 状态定义与边界&lt;/h2&gt;
&lt;p&gt;$dp[i][j]$ 表示字符串 $s$ 从下标 $i$ 到下标 $j$ 范围内的最长回文子序列的长度。&lt;/p&gt;
&lt;p&gt;基础边界 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 $i == j$ 时，单个字符本身就是回文，长度为 $1$。即 $dp[i][i] = 1$。&lt;/li&gt;
&lt;li&gt;当 $i &gt; j$ 时，区间不存在，长度为 $0$。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3 遍历顺序&lt;/h2&gt;
&lt;p&gt;遍历顺序：观察状态转移方程，可以看到 $dp[i][j]$ 依赖于&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左下方： $dp[i + 1][j - 1]$&lt;/li&gt;
&lt;li&gt;下方：$dp[i + 1][j]$&lt;/li&gt;
&lt;li&gt;左方：$dp[i][j + 1]$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此遍历顺序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$i$ 从大到小（从 $n - 1$ 倒退到 $0$）&lt;/li&gt;
&lt;li&gt;$j$ 从小到大（从 $i + 1$ 前进到 $n - 1$）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4 代码实现&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int longestPalindromeSubseq(String s) {
	char[] str = s.toCharArray();
	int n = str.length;
	int[][] dp = new int[n][n];
	
	// 初始化：单个字符都是回文串
	for (int i = 0; i &amp;#x3C; n; ++i) {
		dp[i][i] = 1;
	}
	
	// 从下往上遍历i
	for (int i = n - 1; i &gt;= 0; --i) {
		// 从左往右遍历j
		for (int j = i + 1; j &amp;#x3C; n; ++j) {
			if (str[i] == str[j]) {
				// 首尾相同
				dp[i][j] = dp[i + 1][j - 1] + 2;
			} else {
				// 首尾不同
				dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
			}
		}
	}

	return dp[0][n - 1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5 空间优化&lt;/h2&gt;
&lt;p&gt;可以看到，$dp[i][j]$ 只依赖于「本行左侧」和「下一行」的值，因此二维矩阵可以压缩成一维数组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int longestPalindromeSubseq(String s) {
	char[] str = s.toCharArray();
	int n = str.length;
	int[] dp = new int[n];
	Arrays.fill(dp, 1);

	for (int i = n - 1; i &gt;= 0; --i) {
		int pre = 0; // 相当于dp[i + 1][j - 1]
		for (int j = i + 1; j &amp;#x3C; n; ++j) {
			int tmp = dp[j];
			if (str[i] == str[j]) {
				dp[j] = pre + 2;
			} else {
				dp[j] = Math.max(dp[j - 1], dp[j]);
			}
			pre = tmp;
		}
	}

	return dp[n - 1];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：3573.买卖股票的最佳时机 V</title><link>https://siolin.me/blog/dsa/3573</link><guid isPermaLink="true">https://siolin.me/blog/dsa/3573</guid><description>解题思路与思考。</description><pubDate>Sun, 21 Dec 2025 17:51:57 GMT</pubDate><content:encoded>&lt;h2&gt;状态机建模&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;188&quot;&gt;188.买卖股票的最佳时机 IV&lt;/a&gt; 的 $k$ 次交易中，我们每一轮交易只有“持有”和“不持有”两种状态。但在这一题里，当我们正处于一笔交易中时，身份有两种可能：&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;：手里既没股票也没欠钱，准备开始第 $j$ 次交易。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，对于第 $j$ 次交易（$1 \le j \le k$），我们定义三个变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;buy[j]&lt;/code&gt;：第 $j$ 次交易中，处于&lt;strong&gt;买入后&lt;/strong&gt;的状态（正向持股）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shorting[j]&lt;/code&gt;：第 $j$ 次交易中，处于&lt;strong&gt;卖出后&lt;/strong&gt;的状态（反向做空）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sell[j]&lt;/code&gt;：第 $j$ 次交易&lt;strong&gt;已完成&lt;/strong&gt;的状态（无论是普通还是做空）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;状态转移方程&lt;/h2&gt;
&lt;p&gt;当处于第 $j$ 次交易中，当天的股票价格为 $P$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正向持有（&lt;code&gt;buy[j]&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;来源 A：昨天就正向持有&lt;/li&gt;
&lt;li&gt;来源 B：昨天&lt;strong&gt;结束时&lt;/strong&gt;处于空仓状态，今天买入&lt;/li&gt;
&lt;li&gt;$buy[j] = max(buy[j], sell[j = 1] - p)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;反向持有（&lt;code&gt;shorting[j]&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;来源 A：昨天就反向持有&lt;/li&gt;
&lt;li&gt;来源 B：昨天 &lt;strong&gt;结束时&lt;/strong&gt;处于空仓状态，今天卖出&lt;/li&gt;
&lt;li&gt;$shorting[j] = max(shorting[j], sell[j - 1] + p)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空仓（&lt;code&gt;sell[j]&lt;/code&gt;）
&lt;ul&gt;
&lt;li&gt;来源 A：昨天 &lt;strong&gt;结束时&lt;/strong&gt;处于空仓&lt;/li&gt;
&lt;li&gt;来源 B：昨天正向持有，今天卖出&lt;/li&gt;
&lt;li&gt;来源 C：昨天反向持有，今天买入&lt;/li&gt;
&lt;li&gt;$sell[j] = max(sell[j], buy[j] + p, shorting[j] - p)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;因为题目中规定“你不能在已经进行买入或卖出操作的同一天再次进行买入或卖出操作”，所以这里使用&lt;strong&gt;倒序遍历&lt;/strong&gt;会更方便。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long maximumProfit(int[] prices, int k) {
	long[] buy = new long[k + 1];
	long[] shorting = new long[k + 1];
	long[] sell = new long[k + 1];

	Arrays.fill(buy, Long.MIN_VALUE / 2);
	Arrays.fill(shorting, Long.MIN_VALUE / 2);

	for (int p : prices) {
		for (int j = k; j &gt;= 1; j--) {
			// 此时的 buy[j] 和 shorting[j] 还是昨天的状态
			sell[j] = Math.max(sell[j], Math.max(buy[j] + p, shorting[j] - p));

			// 因为是倒序，此时的 sell[j-1] 还没有被今天的新价格更新过
			buy[j] = Math.max(buy[j], sell[j - 1] - p);
			shorting[j] = Math.max(shorting[j], sell[j - 1] + p);
		}
	}
	return sell[k];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：309.买卖股票的最佳时机含冷冻期</title><link>https://siolin.me/blog/dsa/309</link><guid isPermaLink="true">https://siolin.me/blog/dsa/309</guid><description>解题思路和思考。</description><pubDate>Sun, 21 Dec 2025 15:41:16 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/&quot;&gt;309. 买卖股票的最佳时机含冷冻期&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;状态机建模&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;188&quot;&gt;188.买卖股票的最佳时机 IV&lt;/a&gt;中，我们只有两个大类：买（持有）和卖（不持有）。&lt;/p&gt;
&lt;p&gt;但在这一题中，由于冷冻期的存在，“不持有”被分成了两种完全不同的情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态 0：持有股票&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态 1：刚刚卖出，处于冷冻期&lt;/strong&gt;
&lt;ul&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;状态转移方程&lt;/h2&gt;
&lt;p&gt;设 $f[i][s]$ 为第 $i$ 天&lt;strong&gt;结束时&lt;/strong&gt;，处于状态 $s$ 的最大利润。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态 0：今天结束后我“手里有货”&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;昨天就有货，今天歇着：$f[i-1][0]$&lt;/li&gt;
&lt;li&gt;昨天没货且不在冷冻期（状态 2），今天刚买入：$f[i-1][2] - price$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方程：&lt;/strong&gt; $f[i][0] = \max(f[i-1][0], f[i-1][2] - price)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;状态 1：今天结束后我“刚刚卖出”&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一来源：昨天我手里有货（状态 0），今天我把它卖了：$f[i-1][0] + price$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方程：&lt;/strong&gt; $f[i][1] = f[i-1][0] + price$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;状态 2：今天结束后我“两手空空且能买”&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;昨天就是这个状态，今天继续歇着：$f[i-1][2]$&lt;/li&gt;
&lt;li&gt;昨天我是刚卖完的冷冻期（状态 1），今天冷冻期解除了：$f[i-1][1]$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方程：&lt;/strong&gt; $f[i][2] = \max(f[i-1][2], f[i-1][1])$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Question：「刚刚卖出」和「处于冷冻期」不应该是两种状态吗？即昨天刚刚卖出，今天处于冷冻期&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里对状态的定义为：第 $i$ 天&lt;strong&gt;结束后&lt;/strong&gt;所处的状态，而不是所谓的“第 $i$ 天所处的状态”。&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;我们可以发现，第 $i$ 天的状态只取决于第 $i - 1$ 天，因此可以直接用三个变量来维护。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int maxProfit(int[] prices) {	
	int hold = -prices[0];  // 状态0
	int sold = 0;           // 状态1
	int rest = 0;           // 状态2

	for (int i = 1; i &amp;#x3C; prices.length; ++i) {
		int nextHold = Math.max(hold, rest - prices[i]);
		int nextSold = hold + prices[i];
		int nextRest = Math.max(rest, sold);

		hold = nextHold;
		sold = nextSold;
		rest = nextRest;
	}

	// 最大利润一定处于“手里没货”的状态
	return Math.max(sold, rest);
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LeetCode：188.买卖股票的最佳时机 IV</title><link>https://siolin.me/blog/dsa/188</link><guid isPermaLink="true">https://siolin.me/blog/dsa/188</guid><description>解题思路和思考。</description><pubDate>Sun, 21 Dec 2025 14:47:02 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/&quot;&gt;188.买卖股票的最佳时机 IV&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;状态机建模&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;./2786&quot;&gt;2786.访问数组中的位置使分数最大&lt;/a&gt;中，只有奇/偶两种状态；而在这一题中，我们最多允许 $k$ 次交易，那么在&lt;strong&gt;任意一天&lt;/strong&gt;，我们可能处于的状态有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 1 次持有&lt;/strong&gt;（Buy 1）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 1 次卖出&lt;/strong&gt;（Sell 1）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 2 次持有&lt;/strong&gt;（Buy 2）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 2 次卖出&lt;/strong&gt;（Sell 2）&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 $k$ 次持有&lt;/strong&gt;（Buy $k$）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 $k$ 次卖出&lt;/strong&gt;（Sell $k$）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总共有 $2k$ 个状态。&lt;/p&gt;
&lt;h2&gt;状态转移方程&lt;/h2&gt;
&lt;p&gt;设 $buy[j]$ 表示第 $j$ 次&lt;strong&gt;持有股票时&lt;/strong&gt;的最大利润，$sell[j]$ 表示第 $j$ 次&lt;strong&gt;卖出股票后&lt;/strong&gt;的最大利润。&lt;/p&gt;
&lt;p&gt;对于当天的股价 $P$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第 $j$ 次持有（$buy[i]$）
我可能今天&lt;strong&gt;继续持有&lt;/strong&gt;昨天的股票，或者今天&lt;strong&gt;刚刚买入&lt;/strong&gt;（前提是第 $j - 1$ 次交易已经卖出）：
$$buy[j] = \max(buy[j], sell[j-1] - P)$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第 $j$ 次卖 （$sell[j]$）
我可能今天&lt;strong&gt;继续空仓&lt;/strong&gt;，或者今天&lt;strong&gt;刚刚卖出&lt;/strong&gt;（前提是第 $j$ 次买入的股票还在手里）：
$$sell[j] = \max(sell[j], buy[j] + P)$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$buy$ 数组应该初始化最小值，因为都没开盘就持有股票，这是非法的，需要过滤掉&lt;/li&gt;
&lt;li&gt;$sell$ 数组应该初始化为 0，因为还没开始交易时利润为 0&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int maxProfit(int k, int[] prices) {
	if (prices.length == 0) return 0;

	// buy[j] 表示第 j + 1 次买入后的最大利润
	int[] buy = new int[k];
	// sell[j] 表示第 j + 1 次售出后的最大利润
	int[] sell = new int[k];

	Arrays.fill(buy, Integer.MIN_VALUE);

	for (int p : prices) {
		for (int j = 0; j &amp;#x3C; k; ++j) {
			int preSell = j == 0 ? 0 : sell[j - 1];
			buy[j] = Math.max(buy[j], preSell - p);
			sell[j] = Math.max(sell[j], buy[j] + p);
		}
	}

	return sell[k - 1];
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Leetcode：2786.访问数组中的位置使分数最大</title><link>https://siolin.me/blog/dsa/2786</link><guid isPermaLink="true">https://siolin.me/blog/dsa/2786</guid><description>解题思路和思考。</description><pubDate>Sun, 21 Dec 2025 12:52:00 GMT</pubDate><content:encoded>&lt;p&gt;题目：&lt;a href=&quot;https://leetcode.cn/problems/visit-array-positions-to-maximize-score/&quot;&gt;2786.访问数组中的位置使分数最大&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;记忆化搜索&lt;/h2&gt;
&lt;p&gt;$dfs(i, j)$表示：当前考虑到下标 $i$，且&lt;strong&gt;上一个选中的数&lt;/strong&gt;奇偶性为 $j$ 时，从 $i$ 到 $n - 1$ 能获得的最大额外分数。&lt;/p&gt;
&lt;p&gt;此时，对于 $v = nums[i]$，它的奇偶性为 $curr = v \bmod 2$：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$curr == j$（&lt;strong&gt;奇偶性相同&lt;/strong&gt;）
&lt;ul&gt;
&lt;li&gt;选：既然奇偶性相同，不需要减 $x$。那么&lt;strong&gt;选了肯定比不选好&lt;/strong&gt;，因为 $v &gt; 0$ 且没有改变后续的奇偶性&lt;/li&gt;
&lt;li&gt;决策：$v + dfs(i + 1, j)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$curr \ne j$（&lt;strong&gt;奇偶性不同&lt;/strong&gt;）
&lt;ul&gt;
&lt;li&gt;不选：维持现状，继续往后看。结果为 $dfs(i + 1, j)$&lt;/li&gt;
&lt;li&gt;选：获得了 $v$ 的分数，但是要扣除 $x$，且&lt;strong&gt;状态改变了&lt;/strong&gt;，从此往后&quot;上一个数&quot;的奇偶性变成了 $curr$（即 $j \oplus 1$）&lt;/li&gt;
&lt;li&gt;决策：$v - x + dfs(i + 1, j \oplus 1)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以整体的递归思路应该是：&lt;/p&gt;
&lt;p&gt;$$dfs(i, j) = \begin{cases} v + dfs(i+1, j) &amp;#x26; \text{if } (v \bmod 2 == j) \ \max(dfs(i+1, j), v - x + dfs(i+1, j \oplus 1)) &amp;#x26; \text{if } (v \bmod 2 \ne j) \end{cases}$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[] nums;
long[][] memo;
int x;
public long maxScore(int[] nums, int x) {
	this.nums = nums;
	this.x = x;
	int n = nums.length;
	memo = new long[n][2];
	for (long[] row : memo) {
		Arrays.fill(row, -1);
	}
	return dfs(0, nums[0] % 2);
}

private long dfs(int i, int j) {
	if (i == nums.length) {
		return 0;
	}
	if (memo[i][j] != -1) {
		return memo[i][j];
	}

	int curr = nums[i] &amp;#x26; 1;
	if (curr == j) {
		// 奇偶性相同必选
		return memo[i][j] = dfs(i + 1, j) + nums[i];
	} else {
		// 奇偶性不同，选或不选
		return memo[i][j] = Math.max(dfs(i + 1, j),  nums[i] - x + dfs(i + 1, j ^ 1));
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;递推&lt;/h2&gt;
&lt;p&gt;理解了上面的状态转移后，递推就非常简单了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f[i][0]$ 代表在 $[i, n - 1]$ 中以偶数开头的子序列的最大得分；&lt;/li&gt;
&lt;li&gt;$f[i][1]$ 代表在 $[i, n - 1]$ 中以奇数开头的子序列的最大得分。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long maxScore(int[] nums, int x) {
	int n = nums.length;
	long[][] f = new long[n + 1][2];

	for (int i = n - 1; i &gt;= 0; --i) {
		int v = nums[i];
		int r = v &amp;#x26; 1;
		// 相同必选
		f[i][r] = v + f[i + 1][r];
		// 不同，选或不选
		f[i][r ^ 1] = Math.max(f[i + 1][r ^ 1], f[i + 1][r] + v - x);
    }

	return f[0][nums[0] % 2];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;迭代 DP&lt;/h2&gt;
&lt;h3&gt;倒推&lt;/h3&gt;
&lt;p&gt;在递推中，我们只会访问 $f[i + 1]$ 的值，所以没有必要全部维护。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long maxScore(int[] nums, int x) {
	long[] f = new long[2];
	for (int i = nums.length - 1; i &gt;= 0; --i) {
		int v = nums[i];
		int r = v &amp;#x26; 1;
		// 不同，选或不选
		f[r ^ 1] = Math.max(f[r ^ 1], f[r] + v - x);
		// 相同必选
		f[r] = v + f[r];
	}
	return f[nums[0] % 2];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：这里的&lt;strong&gt;更新顺序不能改变&lt;/strong&gt;，即必须先更新 $f[r \oplus 1]$ 后更新 $f[r]$（也可以用一个临时变量 $tmp$ 先占位 $f[r]$）。&lt;/p&gt;
&lt;h3&gt;正推&lt;/h3&gt;
&lt;p&gt;由于第一个数必须选，因此正推的逻辑可能会更清晰。&lt;/p&gt;
&lt;p&gt;正推逻辑：当前数 $v$ 的奇偶性为 $r$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$f[0]$ 代表以偶数&lt;strong&gt;结尾&lt;/strong&gt;的子序列最大得分，$f[1]$ 代表以奇数&lt;strong&gt;结尾&lt;/strong&gt;的子序列最大得分&lt;/li&gt;
&lt;li&gt;只能更新 $f[r]$，因为选了数 $v$ 后，结尾奇偶性一定为 $r$&lt;/li&gt;
&lt;li&gt;$f[r \oplus 1]$ 保持不变，因为当前数 $v$ &lt;strong&gt;无法改变以异号结尾&lt;/strong&gt;的子序列的最大分数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;状态转移方程：
$$f[r] = Math.max(f[r] + v, f[r \oplus 1] + v - x)$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public long maxScore(int[] nums, int x) {
	long[] f = new long[2];
	Arrays.fill(f, Long.MIN_VALUE / 2); // 防止减x溢出
	f[nums[0] &amp;#x26; 1] = nums[0];
	for (int i = 1; i &amp;#x3C; nums.length; ++i) {
		int v = nums[i];
		int r = v &amp;#x26; 1;
		f[r] = Math.max(f[r] + v, f[r ^ 1] - x + v);
	}
	return Math.max(f[0], f[1]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/visit-array-positions-to-maximize-score/solutions/2810386/jiao-ni-yi-bu-bu-si-kao-dpcong-ji-yi-hua-jhvr/&quot;&gt;灵茶山艾府：教你一步步思考 DP：从记忆化搜索到递推到空间优化！（Python/Java/C++/Go）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>英语学习：Anki+查拉查词+HypperTTS</title><link>https://siolin.me/blog/share/anki</link><guid isPermaLink="true">https://siolin.me/blog/share/anki</guid><description>学习英语的配置。</description><pubDate>Fri, 19 Dec 2025 23:37:44 GMT</pubDate><content:encoded>&lt;h2&gt;传统App痛点&lt;/h2&gt;
&lt;p&gt;因为存在学习英语的需求，于是去体验了一些市面上传统的背单词APP。不可否认，诸如百词斩、墨墨背单词等App都存在各自的优点，但是长期使用后，发现存在一些实在难以忍受的痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;脱离语境&lt;/strong&gt;：App里的例句往往是预设好的，但是在阅读技术文档或看美剧时遇到的词，那种“当下”的语境感是App无法提供的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;被动输入&lt;/strong&gt;：这种学习更像是完成任务，而不是为了解决问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;算法的“黑盒”&lt;/strong&gt;：对于那些没有记忆而知识简单重复的复习策略，无法掌控真正的复习节奏。有些简单或偏僻的词反复出现浪费时间，而真正优美和常用的词却无法通过自定义权重来加练。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新方案&lt;/h2&gt;
&lt;h3&gt;沙拉查词&lt;/h3&gt;
&lt;p&gt;经过一段时间的探索后，总结出了一套&lt;strong&gt;Anki+沙拉查词+HyperTTS&lt;/strong&gt;的学习流。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://saladict.crimx.com/&quot;&gt;沙拉查词&lt;/a&gt;是一个浏览器插件，当阅读网页或PDF时，用来查询指定的单词。&lt;/p&gt;
&lt;p&gt;沙拉查词集成了&lt;a href=&quot;https://saladict.crimx.com/anki&quot;&gt;Anki Connect自动制卡&lt;/a&gt;，它可以直接抓取&lt;strong&gt;当前的上下文（Context）&lt;/strong&gt;。这个单词在什么文章里、哪一句话出现的，会被原封不动地带入卡片。这种“强关联”是记忆的捷径。&lt;/p&gt;
&lt;h3&gt;Anki&lt;/h3&gt;
&lt;p&gt;将卡片导入Anki后，我们可以在其中高度自定义自己的卡片模版和样式。&lt;/p&gt;
&lt;p&gt;以下是卡片展示效果：&lt;/p&gt;
&lt;p&gt;卡片正面：红色的为我们要记忆的生词，可以播放该单词的读音&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/front-side.png&quot; alt=&quot;卡片正面&quot;&gt;&lt;/p&gt;
&lt;p&gt;卡片背面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/back-side.jpeg&quot; alt=&quot;卡片背面&quot;&gt;&lt;/p&gt;
&lt;p&gt;以下是卡片模版（天高任鸟飞，可以高度定制）：&lt;/p&gt;
&lt;p&gt;正面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;card-container&quot;&gt;
    &amp;#x3C;div class=&quot;context-sentence&quot;&gt;{{Context}}&amp;#x3C;/div&gt;
    
    {{#Audio}}{{Audio}}{{/Audio}}
&amp;#x3C;/div&gt;

&amp;#x3C;script&gt;
    // 获取上下文和单词
    var contextDiv = document.querySelector(&apos;.context-sentence&apos;);
    var targetWord = &quot;{{Text}}&quot;.trim();

    if (contextDiv &amp;#x26;&amp;#x26; targetWord) {
        var text = contextDiv.innerHTML;
        // 创建正则，忽略大小写 (gi)
        var regex = new RegExp(&quot;(&quot; + targetWord + &quot;)&quot;, &quot;gi&quot;);
        // 替换为带高亮的 span
        var newText = text.replace(regex, &apos;&amp;#x3C;span class=&quot;highlight-word&quot;&gt;$1&amp;#x3C;/span&gt;&apos;);
        contextDiv.innerHTML = newText;
    }
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;背面：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;{{FrontSide}}

&amp;#x3C;hr id=&quot;answer&quot;&gt;

&amp;#x3C;div class=&quot;back-container&quot;&gt;
    
    {{#Translation}}
    &amp;#x3C;div class=&quot;sentence-translation&quot;&gt;{{Translation}}&amp;#x3C;/div&gt;
    {{/Translation}}

    &amp;#x3C;div class=&quot;target-word-header&quot;&gt;{{Text}}&amp;#x3C;/div&gt;

    {{#Note}}
    &amp;#x3C;div class=&quot;note-section&quot;&gt;
        {{Note}}
    &amp;#x3C;/div&gt;
    {{/Note}}

    {{#Title}}
    &amp;#x3C;div class=&quot;source-section&quot;&gt;
        {{#Favicon}}&amp;#x3C;img src=&quot;{{Favicon}}&quot; class=&quot;favicon&quot;/&gt;{{/Favicon}}
        &amp;#x3C;a href=&quot;{{Url}}&quot;&gt;{{Title}}&amp;#x3C;/a&gt;
    &amp;#x3C;/div&gt;
    {{/Title}}

&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;样式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card {
  font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto, Helvetica, Arial, sans-serif;
  font-size: 20px;
  line-height: 1.6;
  color: #333;
  background-color: #f7f7f7;
  text-align: left;
  padding: 20px;
}

/* 正面：上下文英文句子 */
.context-sentence {
  font-size: 1.2em;
  color: #2c3e50;
  font-weight: 500;
  margin-bottom: 5px;
}

/* 分割线 */
hr {
  border: 0;
  height: 1px;
  background: #ddd;
  margin: 15px 0;
}

/* 背面：句子的中文翻译 */
.sentence-translation {
  font-size: 1em;
  color: #666;
  font-style: italic; /* 用斜体区分翻译 */
  margin-bottom: 25px;
  padding-bottom: 15px;
  border-bottom: 1px dashed #e0e0e0;
}

/* 背面：目标单词标题 */
.target-word-header {
  font-size: 1.5em;
  font-weight: bold;
  color: #e67e22; /* 醒目的橙色 */
  margin-bottom: 10px;
}

/* 背面：笔记区域 (释义与用法) */
.note-section {
  font-size: 1em;
  color: #333;
  background-color: #fff;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* 轻微阴影，像一张卡片 */
  border-left: 5px solid #5caf9e; /* 左侧绿色装饰条 */
}

/* 底部来源信息 */
.source-section {
  margin-top: 30px;
  font-size: 0.75em;
  text-align: right;
  opacity: 0.6;
}

.source-section a {
  color: #7f8c8d;
  text-decoration: none;
}

.favicon {
  height: 16px;
  width: 16px;
  vertical-align: middle;
  margin-right: 5px;
}

/* 移动端适配 */
@media (max-width: 600px) {
    .card { padding: 15px; font-size: 18px; }
    .target-word-header { font-size: 1.3em; }
}

.highlight-word {
    color: #c0392b; /* 红色 */
    font-weight: bold;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HyperTTS&lt;/h3&gt;
&lt;p&gt;没有声音的单词是没有灵魂的，而在Anki的广袤插件市场中，&lt;a href=&quot;https://ankiweb.net/shared/info/111623432&quot;&gt;HyperTTS&lt;/a&gt;可以弥补这一点。&lt;/p&gt;
&lt;p&gt;HyperTTS可以调用Azure和Google等神经网络自动生成语音，但是免费的一些官方语音如剑桥也够用了。&lt;/p&gt;
&lt;p&gt;以下是一些配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Source/Source Field&lt;/code&gt;：Text （要生成语音的字段）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Target/Target Field&lt;/code&gt;：Audio（语音文件的目标字段）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Voice Selection/Voice&lt;/code&gt;：Cambridge（可以自由更换）&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>阿里云OSS</title><link>https://siolin.me/blog/backend/oss</link><guid isPermaLink="true">https://siolin.me/blog/backend/oss</guid><description>将文件上传到阿里云OSS的流程。</description><pubDate>Fri, 19 Dec 2025 22:57:55 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;images/aliOss.png&quot; alt=&quot;将文件上传到阿里云OSS&quot;&gt;&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>WebSocket</title><link>https://siolin.me/blog/backend/websocket</link><guid isPermaLink="true">https://siolin.me/blog/backend/websocket</guid><description>WebSocket的介绍。</description><pubDate>Fri, 19 Dec 2025 22:50:10 GMT</pubDate><content:encoded>&lt;h2&gt;HTTP 缺陷&lt;/h2&gt;
&lt;p&gt;在 WebSocket 出现之前，Web 世界主要靠 HTTP 协议。HTTP 有一个致命的性格缺陷：&lt;strong&gt;“被动”&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTP 的规则&lt;/strong&gt;：请求 -&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;blockquote&gt;
&lt;p&gt;假如没有 WebSocket，怎么实现“新订单提醒”？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;只能使用笨办法——&lt;strong&gt;轮询 (Polling)&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;短轮询 (Short Polling)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商家后台每隔 2 秒发一个 HTTP 请求问后端：“有新订单吗？”&lt;/li&gt;
&lt;li&gt;后端：“没有。”&lt;/li&gt;
&lt;li&gt;2秒后：“有新订单吗？”&lt;/li&gt;
&lt;li&gt;后端：“没有。”&lt;/li&gt;
&lt;li&gt;&lt;em&gt;缺点&lt;/em&gt;：99% 的请求都是废话，浪费流量，浪费服务器资源，而且有延迟（运气不好要等2秒）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;长轮询 (Long Polling)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商家后台问：“有新订单吗？”&lt;/li&gt;
&lt;li&gt;后端&lt;strong&gt;不立即回复&lt;/strong&gt;，而是把请求“挂起”（hold住）。哪怕等 20 秒，一旦有新订单，立刻返回；或者超时了再返回“没有”。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;缺点&lt;/em&gt;：虽然比短轮询好，但依然建立在 HTTP 之上，连接频繁断开重连，Header 头部信息冗余大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而 WebSocket 实现了 &lt;strong&gt;全双工通信&lt;/strong&gt; ——服务端可以 &lt;strong&gt;主动&lt;/strong&gt; 给客户端发消息。&lt;/p&gt;
&lt;h2&gt;WebSocket 工作机制&lt;/h2&gt;
&lt;p&gt;WebSocket 并不是完全脱离 HTTP 的，它更像是 HTTP 的一种“升级”。&lt;/p&gt;
&lt;h3&gt;握手&lt;/h3&gt;
&lt;p&gt;WebSocket 的连接建立，必须依靠 HTTP 来开路。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;客户端发起请求&lt;/strong&gt;：看起来像普通的 HTTP GET 请求，但 Header 里带了特殊的暗号：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /ws/clientId HTTP/1.1
Connection: Upgrade
Upgrade: websocket
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;翻译&lt;/strong&gt;：“大哥（服务器），我想把协议升级一下，咱们别用 HTTP 了，改用 WebSocket 吧？”&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;服务器响应&lt;/strong&gt;：如果服务器支持，会返回状态码 &lt;strong&gt;101&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;翻译&lt;/strong&gt;：“准了！以后这条连接就是 WebSocket 的天下了。”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;全双工通信&lt;/h3&gt;
&lt;p&gt;一旦握手成功，HTTP 协议就退场了。这条 TCP 连接&lt;strong&gt;不会断开&lt;/strong&gt;，双方可以通过这条“专线”自由地互相发送数据帧。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端有新订单 -&gt; 直接推给前端。&lt;/li&gt;
&lt;li&gt;前端有操作 -&gt; 直接推给后端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低开销&lt;/strong&gt;：不需要像 HTTP 那样每次都带一大堆 Header（Cookie, User-Agent等），数据包很轻量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;心跳保活&lt;/h3&gt;
&lt;p&gt;网络环境是很复杂的（中间有 Nginx、防火墙、路由器）。如果一条连接很久没数据传输，这些中间设备可能会以为连接“死”了，强行切断它。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ping/Pong&lt;/strong&gt;：客户端或服务端会定时发一个很小的数据包（Ping），另一方回复（Pong），以此证明“我还活着，别断我网”。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MD5加密</title><link>https://siolin.me/blog/backend/md5</link><guid isPermaLink="true">https://siolin.me/blog/backend/md5</guid><description>MD5加密的使用与优化。</description><pubDate>Fri, 19 Dec 2025 22:47:40 GMT</pubDate><content:encoded>&lt;h2&gt;核心特性&lt;/h2&gt;
&lt;p&gt;MD5 核心特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;压缩性&lt;/strong&gt;：无论明文长度是多少，输出的 MD5 值长度永远固定（32 位十六进制字符串）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可逆性&lt;/strong&gt;：可以通过明文算出 MD5，但是无法从 MD5 反向推导出明文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抗碰撞性&lt;/strong&gt;：不同的明文生成的 MD5 绝不相同。只要是原始数据改动了一个字节，生成的 MD5 就会天差地别（雪崩效应）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;应用场景&lt;/h2&gt;
&lt;p&gt;在「苍穹外卖」中，MD5 主要用于保护&lt;strong&gt;密码安全&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;用户注册/新增员工：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将前端传过来的明文密码，经过 MD5 加密后再存储到数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户登陆&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将前端传过来的明文密码进行 MD5 加密&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将加密后的结果与数据库对比&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;缺陷&lt;/h2&gt;
&lt;p&gt;单纯的MD5 已经不再安全，虽然其不可逆，但是黑客可以使用 &lt;strong&gt;彩虹表&lt;/strong&gt;进行暴力碰撞。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;彩虹表&lt;/strong&gt;：预先计算好常见密码的 MD5 值并存成一张大表&lt;/li&gt;
&lt;li&gt;拿到数据库里的 MD5 后，通过查表就可以反推出明文（&lt;strong&gt;利用「抗碰撞性」&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优化&lt;/h2&gt;
&lt;p&gt;为了破解彩虹表，可以给密码“&lt;strong&gt;加盐&lt;/strong&gt;”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户密码是 &lt;code&gt;123456&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;系统随机生成一个字符串（盐值，比如 &lt;code&gt;&amp;#x26;*%#_!22&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;123456 + &amp;#x26;*%#_!22&lt;/code&gt; 的组合进行 MD5 加密。&lt;/li&gt;
&lt;li&gt;即使两个用户的密码都是 &lt;code&gt;123456&lt;/code&gt;，因为盐值不同，数据库里的乱码也完全不同。这让彩虹表彻底失效&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Nginx</title><link>https://siolin.me/blog/backend/nginx</link><guid isPermaLink="true">https://siolin.me/blog/backend/nginx</guid><description>Nginx的介绍与应用。</description><pubDate>Fri, 19 Dec 2025 22:28:27 GMT</pubDate><content:encoded>&lt;h2&gt;基础命令&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-zsh&quot;&gt;# 启动nginx（使用默认的配置文件）
nginx

# 使用指定项目的配置文件和目录
# 都是相对于当前项目根目录的
nginx -p $(pwd) -c conf/nginx.conf
# -p 静态文件目录
# -c 配置文件

# 查看当前的nginx进程
ps -ef|grep nginx

# 查看80端口的占用情况
lsof -i:80

# 停止或重启
nginx -s [signal]
# quit: 优雅停止
# stop：立即停止
# reload: 重载配置文件
# reopen: 重新打开日志文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;p&gt;nginx的进程模型：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/nginx-model.png&quot; alt=&quot;nginx进程模型&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;master 进程只有一个，负责读取和验证配置文件，以及管理 worker 进程&lt;/li&gt;
&lt;li&gt;worker 进程就是工作进程，负责处理具体的请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;课程资料中给的是以 &lt;code&gt;nginx.exe&lt;/code&gt;的形式给出的，其配置目录为conf，里面有默认的静态站点页面：&lt;img src=&quot;images/nginx-directory.png&quot; alt=&quot;ngixn资料&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Mac上可以通过Window Stable来运行&lt;code&gt;.exe&lt;/code&gt;文件，也可以使用 &lt;code&gt;nginx -p $(pwd) -c conf/nginx.conf&lt;/code&gt;命令，用自己的nginx来运行该配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/nginx-run.png&quot; alt=&quot;image-20251219223840947&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，此时nginx的master进程，加载的就是资料中给的配置文件。&lt;/p&gt;
&lt;h2&gt;反向代理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;场景：
&lt;ul&gt;
&lt;li&gt;前端网页运行在浏览器里，访问的是 &lt;code&gt;http://localhost/api/employee/login&lt;/code&gt;（默认80端口）。&lt;/li&gt;
&lt;li&gt;后端代码运行在 IDEA 中，监听的是 &lt;code&gt;8080&lt;/code&gt; 端口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;问题：前端发送的请求，后端是如何接收到的？&lt;/li&gt;
&lt;li&gt;Nginx：将对请求地址的访问转发给指定地址（后端服务器）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;server {
	listen       80;
	server_name  localhost;

	# 反向代理,处理管理端发送的请求
	location /api/ {
		proxy_pass   http://localhost:8080/admin/;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置详解（&lt;code&gt;nginx.conf&lt;/code&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监听（&lt;code&gt;listen 80&lt;/code&gt;）：nginx 监听访问地址为 &lt;code&gt;localhost&lt;/code&gt; 的 &lt;code&gt;80&lt;/code&gt; 端口（浏览器的请求）&lt;/li&gt;
&lt;li&gt;反向代理（&lt;code&gt;prxy_pass&lt;/code&gt;）：将监听到以 &lt;code&gt;/api/&lt;/code&gt; 开头的请求路径后，转发给指定的地址 &lt;code&gt;http://localhost:8080/admin/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反向代理的好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提高访问速度：nginx 本身可以&lt;strong&gt;缓存数据&lt;/strong&gt;，如果访问同一接口，nginx 可以直接返回已缓存的数据，不需要再去访问服务端&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;负载均衡&lt;/h2&gt;
&lt;p&gt;如果服务器以集群的方式进行部署，那么 nginx 在转发请求到服务器时需要进行&lt;strong&gt;负载均衡&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;upstream&lt;/code&gt; 来配置后端服务器组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://webservers/admin;#负载均衡
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;负载均衡有很多策略，但是对于只有一个服务器的该项目，默认的「轮询」已经足够。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Nginx学习：&lt;a href=&quot;https://www.bilibili.com/video/BV1mz4y1n7PQ/?vd_source=74ec4f72a4bfcd8ce6aa18434e22e349&quot;&gt;【GeekHour】30分钟Nginx入门教程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>JWT</title><link>https://siolin.me/blog/backend/jwt</link><guid isPermaLink="true">https://siolin.me/blog/backend/jwt</guid><description>JWT的使用和优化。</description><pubDate>Fri, 19 Dec 2025 22:21:15 GMT</pubDate><content:encoded>&lt;h2&gt;应用场景&lt;/h2&gt;
&lt;p&gt;互联网服务中最常见的功能便是用户认证，比如登陆了某个网站后下次就可以自动登录。&lt;/p&gt;
&lt;p&gt;由于 HTTP 是&lt;strong&gt;无状态协议&lt;/strong&gt;，因此使用 cookie 来进行用户认证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1、用户向服务器发送用户名和密码。

2、服务器验证通过后，在当前对话（session）里面保存相关数据，比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id，写入用户的 Cookie。

4、用户随后的每一次请求，都会通过 Cookie，将 session_id 传回服务器。

5、服务器收到 session_id，找到前期保存的数据，由此得知用户的身份。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是问题在于，这里的 session 信息保存在哪里？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;保存在持久层（数据库），服务收到请求后，向持久层请求数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺点：工程量大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存在客户端，每次请求时发给 session&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;JWT，即 Json Web Token 就是第二种方案。&lt;/p&gt;
&lt;h2&gt;现实应用&lt;/h2&gt;
&lt;p&gt;传统的导入和配置不再赘述，主要讲一些扩展。&lt;/p&gt;
&lt;h3&gt;缺陷&lt;/h3&gt;
&lt;p&gt;如果黑客截获了 JWT，那么其就可以&lt;strong&gt;在令牌过期前冒充用户，进行一切合法的操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这也是所有基于 Token 机制所面临的共同安全挑战。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么 JWT 泄漏后可以被冒充？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无状态性&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT 被生成后，后端服务器不会在内存或 Redis 中存储其状态&lt;/li&gt;
&lt;li&gt;后端只通过 Token 的签名来验证其是否有效、过期，而无法知道其是否泄漏或被调用&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;JWT 内部包含了用户信息（比如苍穹外卖中的 &lt;code&gt;user_id&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;黑客只需要将 JWT 包含在 HTTP 请求头中，拦截器看到 Token 是合法的，就会认为这个请求是用户 &lt;code&gt;user_id&lt;/code&gt; 发出的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于基于 Session 的认证，服务器会&lt;strong&gt;在内存或 Redis 中存储 Session ID&lt;/strong&gt;。如果用户密码泄漏或检测到异地登录，服务器就可以直接删除这个 Session ID，用户就会立即掉线。&lt;/p&gt;
&lt;p&gt;但 JWT 是无状态的，即：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无法注销&lt;/strong&gt;：JWT 的注销只是前端删除了本地存储的 Token。Token 本身依然有效，直到过期&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无法吊销&lt;/strong&gt;：在 Token 有效期内，即使服务器发现该 Token 被盗，也无法立刻使其实效&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 补救&lt;/h3&gt;
&lt;p&gt;对于 Web 应用，可以采取以下几种措施来降低 JWT 泄漏的风险：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;设置极短的有效期&lt;/strong&gt;：如果用户体验要求高，可以引入 &lt;strong&gt;Refresh Token&lt;/strong&gt; 机制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Access Token 设置 1 小时有效&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;额外签发一个 Refresh Token，设置 7 天有效，用于在 Access Token 过期后安静的换取新的 Access Token，减少用户登录的次数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;实现 Token 黑名单机制&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;做法：将指定用户的 JWT ID 存入 &lt;strong&gt;Redis 黑名单&lt;/strong&gt;中，并设置黑名单过期时间与 JWT 有效期一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;校验流程：拦截器在校验 JWT 签名和有效期时，会额外查询其 ID 是否包含在 Redis 黑名单中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 Token 被加入黑名单，其就无法在请求服务&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;增强传输安全性（HTTPS）&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用更安全的 HTTPS 进行加密传输，降低了 JWT 被截获的风险&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>微信小程序登陆</title><link>https://siolin.me/blog/backend/wechat-login</link><guid isPermaLink="true">https://siolin.me/blog/backend/wechat-login</guid><description>微信小程序登陆的流程。</description><pubDate>Fri, 19 Dec 2025 22:11:13 GMT</pubDate><content:encoded>&lt;h2&gt;code（临时登陆凭证）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么需要 code？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为前端是不可信的，其可能伪造大量的假用户来占用服务端资源。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;code 是如何生成的？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;当用户进入小程序后并点击登陆后，小程序会调用 &lt;code&gt;wx.login()&lt;/code&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;code&lt;/strong&gt;（包含了加密信息和时效性）&lt;/li&gt;
&lt;li&gt;微信客户端将 code 返回给小程序，小程序才能去请求服务端&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;登陆凭证校验&lt;/h2&gt;
&lt;p&gt;后端收到 login 请求并拿到 code 后，其会调用 &lt;code&gt;auth.code2Session&lt;/code&gt; 接口去请求微信接口服务，返回 &lt;code&gt;session_key&lt;/code&gt; 和 &lt;code&gt;openid&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;session_key&lt;/code&gt;：&lt;strong&gt;会话密钥&lt;/strong&gt;。如果后续需要用户的其他信息（比如电话号码），微信会加密传输，后端需要用该密钥解密&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openid&lt;/code&gt;：&lt;strong&gt;用户的唯一身份证&lt;/strong&gt;。只要是同一个微信号，那么后端从微信接口服务获得的 openid 都会是同一个，因此后续登陆时可以直接依靠 openid 来匹配数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;自定义登陆态&lt;/h2&gt;
&lt;p&gt;自定义登录态是指开发者根据业务需求，自行设计并管理的一套身份凭证机制。&lt;/p&gt;
&lt;p&gt;在用户登录后，服务器端生成一个标识用户身份的凭证，并将其发送到客户端进行存储，客户端在后续每次请求时携带此凭证，服务器端据此&lt;strong&gt;识别用户身份并维持会话&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在苍穹外卖中，这里的「自定义登录态」就是指JWT。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么不直接把 openid 返回给前端？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为 openid 和 session_key 代表了用户的登录态数据，其一旦泄漏，黑客就可以直接冒充用户为所欲为。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>「苍穹外卖」缓存优化</title><link>https://siolin.me/blog/backend/redis-cache</link><guid isPermaLink="true">https://siolin.me/blog/backend/redis-cache</guid><description>Redis缓存在「苍穹外卖」中的进一步拓展。</description><pubDate>Fri, 19 Dec 2025 21:23:05 GMT</pubDate><content:encoded>&lt;p&gt;在原版教程中，存在两处缓存的应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;套餐分类：使用Spring Cache进行缓存，底层实现为Redis&lt;/li&gt;
&lt;li&gt;店铺状态：使用Redis的String类型缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该项目还有一些优化点。&lt;/p&gt;
&lt;p&gt;| 数据类型 | 缓存策略 | 理由               |
| -------- | -------- | ------------------ |
| 购物车   | Hash     | 高频读写           |
| 菜品分类 | Hash     | 高频查询           |
| 套餐分类 | Hash     | 高频查询           |
| 店铺状态 | String   | 高频查询           |
| 分类列表 | Hash     | 极少变更，高频查询 |
| 套餐详情 | Hash     | 包含关联数据       |
| 地址薄   | Hash     | 用户独立数据       |&lt;/p&gt;
&lt;h2&gt;购物车&lt;/h2&gt;
&lt;p&gt;购物车属于典型的**“高频读写、临时性强“**的数据，其临时性很强，非常适合迁移到Redis中而不是数据库表里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结构设计：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash
Key格式: shoppingCart_{userId}
Field格式: dish_{dishId}_{flavor} 或 setmeal_{setmealId}
Value: ShoppingCart对象
过期时间: 1小时
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么使用Redis而不是Spring Cache？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;与Spring Cache相比，Redis的显著优势在于其对粒度的精细控制，这也是购物车缓存不使用Spring Cache的&lt;strong&gt;核心原因&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Cahce的工作模式是&lt;strong&gt;全量/粗粒度&lt;/strong&gt;的，如果我仅仅在购物车中增加一份米饭，那么整个购物车的缓存都会失效。也就是说，为了修改一个小数据，浪费了其他未改动数据的序列化和传输开销&lt;/li&gt;
&lt;li&gt;Redis的工作模式是&lt;strong&gt;增量/细粒度&lt;/strong&gt;的，同样是在购物车汇总增加一份米饭，在使用&lt;strong&gt;Redis Hash&lt;/strong&gt;结构的情况下，我只需要清除该米饭的缓存（在查询时懒加载），而其他商品的缓存依然继续使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后面的缓存基本上都是使用&lt;strong&gt;Redis Hash&lt;/strong&gt;，主要是因为其可以对单一字段进行操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;既然已经在数据改动时清除了缓存，为什么还要再设置缓存过期时间？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;主要有两方面考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;容错机制&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极端情况&lt;/strong&gt;：如果在执行删除操作时，数据库（MySQL）执行成功了，但是 Redis 在执行删除代码时，突然因为某个原因导致服务器宕机或 Redis 挂了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后果&lt;/strong&gt;：如果没有过期时间，那么这份&lt;strong&gt;脏数据将永久驻留在 Redis 中&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;p&gt;&lt;strong&gt;内存管理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis是&lt;strong&gt;内存数据库&lt;/strong&gt;，其所有数据都是存储在内存的；而我们又都知道，内存是及其昂贵的资源&lt;/li&gt;
&lt;li&gt;缓存的目标是&lt;strong&gt;热数据&lt;/strong&gt;，即经常被访问的数据；如果没有设置过期时间，那么Redis可能会被冷数据填满（比如10年前用户的购物车）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;菜品分类查询&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;缓存策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash
Key格式: dish_category_{categoryId}  
Field格式: dish_{dishId}  
Value: Dish 对象
过期时间: 1小时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当更新菜品时，其分类是否更新是&lt;strong&gt;不确定&lt;/strong&gt;的。如果更新了分类，那么需要把旧分类和新分类的缓存&lt;strong&gt;全都清除&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;另外一点需要注意的是，DishServiceImpl 中存在两个菜品分类查询的接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getByCategoryId&lt;/code&gt;：返回 Dish，被管理端调用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getWithFlavorByCategoryId&lt;/code&gt;：返回 DishVO，被用户端调用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果全都实现缓存，那么需要使用两个不同的 Key；但是由于管理端访问频率较低，所以这里只实现用户端的接口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;为什么不能使用同一个 HashKey？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为二者返回的数据结构不同，会导致类型转换异常和相互覆盖。&lt;/p&gt;
&lt;h2&gt;套餐分类查询&lt;/h2&gt;
&lt;p&gt;与「菜品分类查询」类似。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存策略&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash  
Key格式: setmeal_category_{categoryId}  
Field格式: setmeal_{setmealId}  
Value: Setmeal 对象
过期时间: 1小时
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当更新套餐时，其分类是否更新是&lt;strong&gt;不确定&lt;/strong&gt;的。如果更新了分类，那么需要把旧分类和新分类的缓存&lt;strong&gt;全都清除&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;套餐详情查询&lt;/h2&gt;
&lt;p&gt;用户查看套餐详情时，服务端涉及 setmeal + setmeal_dish 的&lt;strong&gt;联表查询&lt;/strong&gt;，且套餐的修改频率较低，因此同样可以用缓存来提高效率。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash
Key: setmeal_detail_{setmealId}
Field: dish_{index}_{name}
Value: DishItemVO 对象
过期时间: 2小时
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于 &lt;code&gt;DishItemVO&lt;/code&gt; 中没有 &lt;code&gt;DishId&lt;/code&gt;，为了确保唯一性，所以使用「索引+名称」作为 field&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;分类列表&lt;/h2&gt;
&lt;p&gt;用户打开小程序首页时必查分类，而且分类是基础数据，变更极少，因此非常推荐缓存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash
Key: category_type_{type}
Field: category_{categoryId}
Value: Category 对象
过期时间：24小时
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里采用&lt;strong&gt;分类型缓存&lt;/strong&gt;，type：1-菜品分类，2-套餐分类&lt;/li&gt;
&lt;li&gt;分类的变更频率极低，因此过期时间可以适当延长&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于新增的分类，其状态默认为 0（禁用 ），因此可以不必立即清理缓存，可以等到启用的时候再清理。&lt;/p&gt;
&lt;h2&gt;店铺营业状态&lt;/h2&gt;
&lt;p&gt;店铺营业状态应该是读取频率最高的了，而且其实现也很简单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存策略：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: String
Key: SHOP_STATUS
Value: Integer (0-停业, 1-营业)
过期时间: 永久（手动更新）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用户地址簿&lt;/h2&gt;
&lt;p&gt;用户地址簿可能变更相对频繁，之所以将其缓存是因为其查询次数较多，且数据是按用户隔离的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;缓存策略&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数据结构: Hash
Key: address_book_{userId}
Field: address_{addressId}
Value: addressBook对象
过期时间: 30分钟（会话期间）
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>TCP拥塞控制：快速恢复算法</title><link>https://siolin.me/blog/share/fast_recovery</link><guid isPermaLink="true">https://siolin.me/blog/share/fast_recovery</guid><description>记录自己关于TCP拥塞控制中快速恢复算法的理解。</description><pubDate>Sun, 26 Oct 2025 21:18:39 GMT</pubDate><content:encoded>&lt;p&gt;当网络出现拥塞，TCP会进行数据段重传。存在两种重场景：超时重传和快速重传。&lt;/p&gt;
&lt;p&gt;不同的重传机制使用不同的拥塞发送算法。当发生快速重传时，TCP使用&lt;strong&gt;快速恢复算法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;当发生快速重传时，这种情况并不及超时重传严重（只丢失了一部分数据段），因此并不需要重新进入慢启动状态。 &lt;code&gt;ssthresh&lt;/code&gt; 和 &lt;code&gt;cwnd&lt;/code&gt; 变化如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cwnd = cwnd / 2&lt;/code&gt; ，即设置为原来的一半&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ssthresh = cwnd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;进入&lt;strong&gt;快速恢复算法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;快速恢复算法&lt;/strong&gt;操作如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拥塞窗口 &lt;code&gt;cwnd = ssthresh + 3&lt;/code&gt; （ 说明至少有 3 个分组离开了网络）&lt;/li&gt;
&lt;li&gt;重传丢失的数据包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每收到一个新的重复 ACK&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cwnd&lt;/code&gt; + 1（有一个分组离开了网络）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收到新数据的 ACK 时&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;说明丢失的数据已经被成功收到&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;cwnd&lt;/code&gt; 直接减为 &lt;code&gt;ssthresh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;退出快速恢复，进入**拥塞避免算法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图展示了 TCP Reno 的拥塞控制算法（来源于小林coding），包括慢启动、拥塞避免、快速重传和快速恢复：
&lt;img src=&quot;./images/tcp_reno.png&quot; alt=&quot;示意图&quot;&gt;&lt;/p&gt;
&lt;p&gt;存在两点疑问：&lt;/p&gt;
&lt;h2&gt;1. 为什么收到重复 ACK 时，cwnd 增加 1？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./images/tcp_window.png&quot; alt=&quot;TCP发送窗口&quot;&gt;&lt;/p&gt;
&lt;p&gt;该机制被称为&lt;strong&gt;窗口膨胀&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重复 ACK 的含义：&lt;/strong&gt; 接收方能够发出重复 ACK，就证明它&lt;strong&gt;已经接收并缓存了&lt;/strong&gt;一个新的乱序报文段，这意味着&lt;strong&gt;网络中有一个报文段（可能是乱序到达的那个，也可能是更早发送的）已经安全到达了接收端&lt;/strong&gt;，并离开了传输中的“拥塞管道”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;窗口补偿&lt;/strong&gt;：由于丢失的报文段未被确认，&lt;code&gt;snd_una&lt;/code&gt;（最早未被确认的报文段）无法推进，发送窗口（&lt;code&gt;rwnd&lt;/code&gt; 和 &lt;code&gt;cwnd&lt;/code&gt; 的最小值）因此&lt;strong&gt;不能滑动&lt;/strong&gt;。为了继续向网络注入数据流，避免“管道”变空，TCP 人为地将 &lt;code&gt;cwnd&lt;/code&gt; 增加 $1 \times MSS$，以补偿已离开网络、但尚未被最终确认的报文段。&lt;/p&gt;
&lt;h2&gt;2. 为什么收到新的 ACK 后还要恢复到 sstresh？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;新的 ACK 的含义&lt;/strong&gt;：收到确认新数据的 ACK，表明接收方已经收到并正确组装了之前丢失的所有报文段，因此发送窗口可以大幅向前滑动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;撤销膨胀&lt;/strong&gt;：既然发生了快速重传，说明此时的网络或多或少还是有些拥塞的，而之前增加 &lt;code&gt;cwnd&lt;/code&gt; 只是一种&lt;strong&gt;补偿机制&lt;/strong&gt;，即&lt;strong&gt;补偿窗口不能滑动使得无法发送新的包&lt;/strong&gt;。既然收到了新的 ACK 后窗口可以滑动了，那也就不需要继续补偿了，反而因为当下的拥塞状态缩小窗口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;恢复到拥塞避免&lt;/strong&gt;：将 &lt;code&gt;cwnd&lt;/code&gt; 设为 &lt;code&gt;ssthresh&lt;/code&gt; 的目的，就是&lt;strong&gt;退出快速恢复阶段&lt;/strong&gt;，进入&lt;strong&gt;拥塞避免阶段&lt;/strong&gt;，即开始“小心”地增长，避免发生拥塞。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Obsidian：RSS阅读与管理</title><link>https://siolin.me/blog/share/obsidian_rss</link><guid isPermaLink="true">https://siolin.me/blog/share/obsidian_rss</guid><description>在Obsidian中集成RSS功能，实现RSS的集中管理和阅读。</description><pubDate>Sun, 26 Oct 2025 20:34:54 GMT</pubDate><content:encoded>&lt;p&gt;尝试了很多RSS阅读器后，发现Obsidian存在插件——RSS Dashboard，可以实现RSS的集中管理和阅读，上手后觉得效果还不错，作此分享。&lt;/p&gt;
&lt;h2&gt;插件安装&lt;/h2&gt;
&lt;p&gt;项目地址：[https://github.com/amatya-aditya/obsidian-rss-dashboard]。&lt;/p&gt;
&lt;p&gt;里面的安装说明写得很详细，照着做就行。&lt;/p&gt;
&lt;h2&gt;插件使用&lt;/h2&gt;
&lt;p&gt;完成插件安装并启用后，左侧边栏会多出一个RSS Dashboard的图标，点击它即可打开RSS阅读界面。&lt;/p&gt;
&lt;p&gt;下面是我的使用界面。&lt;/p&gt;
&lt;p&gt;在Dashboard界面，可以看到已订阅的RSS源列表，以及各个源的最新文章预览。
&lt;img src=&quot;./images/rss_dashboard.png&quot; alt=&quot;RSS Dashboard 界面&quot;&gt;&lt;/p&gt;
&lt;p&gt;在Discover界面，可以搜索一些已有的RSS源，方便添加订阅。
&lt;img src=&quot;./images/rss_discover.png&quot; alt=&quot;RSS Discover 界面&quot;&gt;&lt;/p&gt;
&lt;h2&gt;RSS获取&lt;/h2&gt;
&lt;p&gt;插件本身并不提供RSS源，需要用户自行添加RSS源地址。推荐在&lt;a href=&quot;https://docs.rsshub.app/zh/guide/&quot;&gt;RSShub&lt;/a&gt;获取各种网站的RSS源地址，RSShub支持生成大量网站的RSS源，非常实用。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/kBI1odmnKDG1n8BjjRzXWw&quot;&gt;插件分享 RSS Dashboard 现代的RSS管理和消费体验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/659275676&quot;&gt;一文搞定RSS！从搭建、使用到自建订阅源。&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/amatya-aditya/obsidian-rss-dashboard&quot;&gt;obsidian-rss-dashboard
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rsshub.app/zh/guide/&quot;&gt;RSSHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CS144 Checkpoint2</title><link>https://siolin.me/blog/cs/checkpoint2</link><guid isPermaLink="true">https://siolin.me/blog/cs/checkpoint2</guid><description>实现序号转换以及接收端的receive/send函数。</description><pubDate>Tue, 16 Sep 2025 11:23:11 GMT</pubDate><content:encoded>&lt;p&gt;整个 cs144 的实验结构层次图如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;应用层程序
   │
[ TCPSocket ]   ← 提供 connect/read/write 接口
   │
[ TCPConnection ] ← 整体状态机，协调发送方和接收方
   ├─ [ TCPSender ]   ← 分片、发送、重传
   └─ [ TCPReceiver ] ← 重排、确认、窗口
        │
   [ Reassembler ]   ← 拼接乱序片段
        │
   [ ByteStream ]    ← 有限容量的字节缓冲
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Checkpoint2 中，我们需要实现一个 &lt;strong&gt;TCP 接收器（TCPReceiver）&lt;/strong&gt;。该模块主要任务如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;接收发送方的报文（Message），并且使用之前实现的 Reassembler 将其中的数据段组装成 ByteStream&lt;/li&gt;
&lt;li&gt;向发送方回复报文，其中包含 ACK number（ackno）以及当前接收窗口的空闲空间大小（用于流量控制）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;wrap/unwrap&lt;/h2&gt;
&lt;p&gt;在 Reassembler 中每个字节的序号由 64 位表示，并且序号从 0 开始，称为&lt;strong&gt;绝对序号（absolute seqno）&lt;/strong&gt;。但是在 TCP 首部中要尽可能的压缩空间，于是使用 32 位来表示，称为&lt;strong&gt;序号（seqno）&lt;/strong&gt;。这新增了以下机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;循环（wrap）&lt;/strong&gt;：相比 64 位，32 位能表示的范围非常小，如果超过最大值则进行循环处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机初始序号（ISN）&lt;/strong&gt;：为了防止旧报文干扰新连接，采取随机初始序号的方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标志位&lt;/strong&gt;：TCP 首部的 SYN 标志位表示“请求建立连接”，而 FIN 标志位表示“请求断开连接”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![[Pasted image 20250915163109.png]]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zero_point = 2^32 - 2&lt;/code&gt; ，代表逻辑零点，对应 TCP 的 SYN（因为 seqno 使用随机 ISN）&lt;/li&gt;
&lt;li&gt;stream index 才是传入重组器的参数，因为我们之前的实现没有考虑标志位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;已经提前声明了 &lt;code&gt;Wrap32&lt;/code&gt;（wrapping_integers.hh） 来表示报文中的序号，其中使用 &lt;code&gt;uint32_t&lt;/code&gt; 来存储数据。&lt;/p&gt;
&lt;p&gt;这里需要实现绝对序号和序号之间的转换，以便后面将其发送给 Reassembler 进行拼接。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wrap()&lt;/code&gt;：Absolute seqno -&gt; seqno&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Absolute seqno -&gt; seqno
Wrap32 Wrap32::wrap(uint64_t n, Wrap32 zero_point)
{
  constexpr uint64_t MOD = 1ull &amp;#x3C;&amp;#x3C; 32;
  // (isn + absolute_seqno) % 2^32
  const uint64_t sum = n + static_cast&amp;#x3C;uint64_t&gt;(zero_point.raw_value_);
  return Wrap32(static_cast&amp;#x3C;uint32_t&gt;(sum % MOD));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;unwrap()&lt;/code&gt;：序号 -&gt; 绝对序号&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// seqno -&gt; Absolute seqno
uint64_t Wrap32::unwrap(Wrap32 zero_point, uint64_t checkpoint) const
{
  constexpr uint64_t MOD = 1ull &amp;#x3C;&amp;#x3C; 32;

  // 计算 32 位偏移量
  const uint32_t off = this-&gt;raw_value_ - zero_point.raw_value_;
  // 对齐高 32 位
  uint64_t candidate = (checkpoint &amp;#x26; ~(MOD - 1)) + static_cast&amp;#x3C;uint64_t&gt;(off);

  // 判断与中点的相对位置
  if (candidate + (MOD &gt;&gt; 1) &amp;#x3C;= checkpoint) {
    candidate += MOD; // 靠左 -&gt; 下一圈
  } else if (candidate &gt; checkpoint + (MOD &gt;&gt; 1)) {
    if (candidate &gt;= MOD) candidate -= MOD; // 靠右 -&gt; 上一圈（前提是不会下溢）
    // 否则就保持当前不变
  }

  return candidate;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于是从大范围映射到小范围（64 位 -&gt; 32 位），因此会存在映射冲突&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkpoint&lt;/code&gt; 为已知的最后一个绝对序号（相当于 &lt;code&gt;bytes_pushed_&lt;/code&gt;），因此最接近 &lt;code&gt;checkpoint&lt;/code&gt; 才是正确的绝对序号&lt;/li&gt;
&lt;li&gt;如果直接减去 &lt;code&gt;MOD&lt;/code&gt; 有可能会发生下溢，反而距离更远；加一圈并不会造成上溢&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;![[unwrap.excalidraw 1.png]]&lt;/p&gt;
&lt;p&gt;![[unwrap.excalidraw]]&lt;/p&gt;
&lt;h2&gt;TCPreceiver&lt;/h2&gt;
&lt;p&gt;报文数据结构已经提前声明，分别是 &lt;code&gt;TCPSenderMessage&lt;/code&gt; 和 &lt;code&gt;TCPReceiverMessage&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;recive&lt;/h3&gt;
&lt;p&gt;首先如果收到 &lt;code&gt;RST&lt;/code&gt; 报文，那么直接设置出错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (message.RST) {
    reassembler_.output_.set_error();
    return;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果还未收到 &lt;code&gt;SYN&lt;/code&gt; 报文，那么应该忽略所有报文，因为此时连接还未建立，无法接收数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (!has_syn_) {
    // 还未收到 SYN 报文
    if (!message.SYN) return; // 忽略所有非 SYN 报文
    has_syn_ = true;
    zero_point_ = message.seqno;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就是要计算 &lt;code&gt;checkpoint&lt;/code&gt;，这样才能算出段首的绝对序号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  // checkpoint = 1(SYN) + bytes_pushed + (如果已经结束，再+1(FIN))
  const uint64_t bytes_pushed = reassembler_.output_.writer().bytes_pushed();
  const bool ended = reassembler_.output_.reader.is_finished();
  const uint64_t checkpoint = 1 + bytes_pushed + (ended ? 1 : 0);
  
  // payload 的 absolute seqno
  const uint64_t abs_seqno = message.seqno.unwrap(zero_point_, checkpoint);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;知道了绝对序号后，我们要将其转换为子字符串也就是 &lt;code&gt;payload&lt;/code&gt; 的字节流序号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  // payload 的 stream index
  const uint64_t stream_index = abs_seqno - 1 + (message.SYN ? 1 : 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![[seqno2index.excalidraw.png]]&lt;/p&gt;
&lt;p&gt;最后便是将数据推给 Reassembler，即重组器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  // 推给 BReassembler
  const std::string data = message.payload;
  reassembler_.insert(stream_index, data, message.FIN);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;send&lt;/h3&gt;
&lt;p&gt;首先是设置 RST 标志位：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  TCPReceiverMessage out;

  // 如果收到RST报文或底层字节流出错，要将其反映在发送消息中
  const bool stream_error = reader().has_error();
  out.RST = stream_error || rst_;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次是计算确认号，与先前计算 &lt;code&gt;checkpoint&lt;/code&gt; 逻辑一致：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  // ackno
  if (syn_) {
    const uint64_t bytes_pushed = writer().bytes_pushed();
    const bool ended = writer().is_closed();
    uint64_t ack_abs_seqno = 1 + bytes_pushed + (ended ? 1 : 0);
    out.ackno = Wrap32::wrap(ack_abs_seqno, zero_point_);
  } else {
    out.ackno = nullopt;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后是计算窗口大小：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;  // window_size
  const size_t win = static_cast&amp;#x3C;uint64_t&gt;(writer().available_capacity());
  out.window_size = static_cast&amp;#x3C;uint16_t&gt;(min&amp;#x3C;size_t&gt;(win, std::numeric_limits&amp;#x3C;uint16_t&gt;::max()));
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CS144 Checkpoint1</title><link>https://siolin.me/blog/cs/checkpoint1</link><guid isPermaLink="true">https://siolin.me/blog/cs/checkpoint1</guid><description>实现TCP层次结构中的Reassembler（重组器），用于将分组拼接成字节流。</description><pubDate>Sun, 07 Sep 2025 00:06:45 GMT</pubDate><content:encoded>&lt;p&gt;整个 cs144 的实验结构层次图如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;应用层程序
   │
[ TCPSocket ]   ← 提供 connect/read/write 接口
   │
[ TCPConnection ] ← 整体状态机，协调发送方和接收方
   ├─ [ TCPSender ]   ← 分片、发送、重传
   └─ [ TCPReceiver ] ← 重排、确认、窗口
        │
   [ Reassembler ]   ← 拼接乱序片段
        │
   [ ByteStream ]    ← 有限容量的字节缓冲
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Checkpoint1 中，我们需要实现一个 &lt;strong&gt;TCP 重组器（Reassembler）&lt;/strong&gt;。这个模块的主要任务，就是把可能乱序到达的分段（segment）拼接成一个连续的字节流，最终交给 &lt;code&gt;ByteStream&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;简单来说，TCP 的世界里：&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;/li&gt;
&lt;li&gt;所以接收方必须 &lt;strong&gt;缓存未到位的分段&lt;/strong&gt;，并在合适的时候写入字节流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;类的设计&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Reassembler&lt;/code&gt; 内部的几个重要成员：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;ByteStream output_;                     // 真正的字节流 
std::map&amp;#x3C;uint64_t, std::string&gt; segs_;  // 缓存未组装的分段 
uint64_t unassembled_;                  // segs_ 里累计的字节数 
std::optional&amp;#x3C;uint64_t&gt; eof_index_;     // FIN 报文对应的 EOF 位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的核心就是一个 &lt;code&gt;map&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;key 是分段的起始索引（first_index），&lt;/li&gt;
&lt;li&gt;value 是分段的字符串内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;利用 &lt;code&gt;map&lt;/code&gt; 的有序性，可以方便地处理乱序和重叠。&lt;/p&gt;
&lt;h2&gt;insert 的主逻辑&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;insert&lt;/code&gt; 方法接收三个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;first_index&lt;/code&gt;：子串的起始位置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;：子串内容；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_last_substring&lt;/code&gt;：是否是 TCP FIN 报文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码整体分为几个阶段：&lt;/p&gt;
&lt;h3&gt;1. 确定接收窗口&lt;/h3&gt;
&lt;p&gt;TCP 缓冲区是有限的，所以要限制接收窗口范围内的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const uint64_t next_index = output_.writer().bytes_pushed();
const uint64_t win_left = next_index;
const uint64_t win_right = next_index + output_.writer().available_capacity();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后根据窗口对 &lt;code&gt;data&lt;/code&gt; 作裁剪：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;窗口左边的丢掉（已经被写过的字节）；&lt;/li&gt;
&lt;li&gt;窗口右边的丢掉（超过缓存能力的部分）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 记录 EOF&lt;/h3&gt;
&lt;p&gt;TCP 里的 FIN 报文表示“数据结束”，这里用 eof 模拟。由于可能重传，&lt;code&gt;eof_index_&lt;/code&gt; 只需要记录一次。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (is_last_substring) {
    const uint64_t logical_eof = first_index + data.size();
    if (!eof_index_.has_value()) {
        eof_index_ = logical_eof;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 处理分段重叠&lt;/h3&gt;
&lt;p&gt;这是实现的核心难点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先找到第一个可能与新区间重叠的旧分段：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;auto it = segs_.lower_bound(start);
if (it != segs_.begin()) {
    auto prev_it = std::prev(it);
    if (prev_it-&gt;first + prev_it-&gt;second.size() &gt; start) {
        it = prev_it;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;然后从左到右遍历，处理和已有分段的覆盖、缺口情况：
&lt;ul&gt;
&lt;li&gt;如果旧分段完全在左边，跳过；&lt;/li&gt;
&lt;li&gt;如果有 gap，把缺口部分切出来放进缓存；&lt;/li&gt;
&lt;li&gt;如果被覆盖了，就移动到下一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;uint64_t pos = start;
while (pos &amp;#x3C; end &amp;#x26;&amp;#x26; it != segs_.end()) {
    uint64_t L = it-&gt;first;
    uint64_t R = it-&gt;first + static_cast&amp;#x3C;uint64_t&gt;(it-&gt;second.size());
  
    // 判断每个分段与当前分段的重叠情况
    // 1. R &amp;#x3C;= pos ：直接跳过，不用考虑
    if (R &amp;#x3C;= pos) {
      it++;
      continue;
    }

    // 2. L &gt; pos：考虑 clipped 的右端是否被覆盖
    if (L &gt; pos) {
      const uint64_t gap_end = std::min&amp;#x3C;uint64_t&gt;(end, L);
      // 从 clipped 中裁剪出 gap
      const size_t off = static_cast&amp;#x3C;size_t&gt;(pos - start);
      const size_t len = static_cast&amp;#x3C;size_t&gt;(gap_end - pos);
      std::string gap = clipped.substr(off, len);
  
      // 入库
      if (!gap.empty()) {
        segs_.emplace(pos, std::move(gap));
        unassembled_ += len;
      }
      pos = gap_end;
      if (pos == end) // 整个分段被处理完成
        break;
    }

    // 3. L &amp;#x3C;= pos &amp;#x3C; R，左端被覆盖
    pos = R;
    ++it;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以避免重复存储字节。&lt;/p&gt;
&lt;h3&gt;4. 处理尾部缺口&lt;/h3&gt;
&lt;p&gt;可能存在新分段超出已有缓存范围的情况，需要补上尾部的 gap。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (pos &amp;#x3C; end) {
    std::string last_gap = clipped.substr(pos - start, end - pos);
    segs_.emplace(pos, std::move(last_gap));
    unassembled_ += end - pos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. push 到 ByteStream&lt;/h3&gt;
&lt;p&gt;最后一步，就是把已经连续的部分从缓存里推送到字节流：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;while (true) {
    const uint64_t next = output_.writer().bytes_pushed();
    auto hit = segs_.find(next);
    if (hit == segs_.end()) break;

    output_.writer().push(hit-&gt;second);
    unassembled_ -= hit-&gt;second.size();
    segs_.erase(hit);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，&lt;code&gt;ByteStream&lt;/code&gt; 就始终保持尽可能完整的前缀字节流。&lt;/p&gt;
&lt;h3&gt;6. 收尾关闭&lt;/h3&gt;
&lt;p&gt;当所有字节都被写入，且 &lt;code&gt;bytes_pushed == eof_index_&lt;/code&gt;，就可以关闭流：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (eof_index_.has_value() &amp;#x26;&amp;#x26; output_.writer().bytes_pushed() == *eof_index_) {
    output_.writer().close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;整体代码&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;reassembler.hh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Reassembler
{
public:
  // Construct Reassembler to write into given ByteStream.
  // 维护一个字节流
  explicit Reassembler(ByteStream&amp;#x26;&amp;#x26; output)
    : output_(std::move(output)), segs_(), unassembled_(0), eof_index_(std::nullopt)
  {}
  
  // ...

private:
  ByteStream output_;
  std::map&amp;#x3C;uint64_t, std::string&gt; segs_; // 未进入字节流（已接收但不连续）
  uint64_t unassembled_;                 // segs中的字节数
  std::optional&amp;#x3C;uint64_t&gt; eof_index_;    // eof字符串索引
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reassembler.cc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 接受一个子字符串，其 first_index 代表该子串头部字节在整个字节流中的序号（这里规定序号从 0 开始）
// data 单纯代表数据，不包含头部
// is_last_substring 模拟的是 TCP FIN 报文
void Reassembler::insert(uint64_t first_index, string data, bool is_last_substring)
{
  const uint64_t next_index = output_.writer().bytes_pushed(); // 下一个要写入的索引
  // 划定接收窗口，即缓存中未被占用的部分 [win_left, win_right)
  const uint64_t win_left = next_index;
  const uint64_t win_right = next_index + output_.writer().available_capacity();
  
  // 记录eof指针位置
  // FIN报文只有一个，但是由于网络重传，其可能会被多次发送，因此这里只需记录一次
  if (is_last_substring) {
    const uint64_t logical_eof = first_index + static_cast&amp;#x3C;uint64_t&gt;(data.size());
    if (!eof_index_.has_value()) {
      eof_index_ = logical_eof; // 只记录一次
    }
  }

  // 确定data在窗口中的位置，溢出窗口的部分直接丢弃
  // [start, end)
  uint64_t start = max&amp;#x3C;uint64_t&gt;(first_index, win_left);
  const uint64_t end = min&amp;#x3C;uint64_t&gt;(first_index + static_cast&amp;#x3C;uint64_t&gt;(data.size()), win_right);
  if (start &gt;= end) {
    /*
     * 三种情况：
     * 1. 子串在窗口左边（冗余序列）
     * 2. 子串在窗口右边（溢出序列）
     * 3. 子串为空
     * 这些情况没有字节可以接收，之所以要单独讨论是因为其可能为is_last_substring，触发收尾
     */
    if (eof_index_.has_value() &amp;#x26;&amp;#x26; output_.writer().bytes_pushed() == *eof_index_) {
      output_.writer().close();
    }
    return;
  }

  // 裁剪字符串 [start - first_index, end - start)
  std::string clipped = data.substr(static_cast&amp;#x3C;uint64_t&gt;(start - first_index), static_cast&amp;#x3C;uint64_t&gt;(end - start));

  // 若存在重叠，则获取第一个与clipped重叠的分段
  // 若不存在，则默认获取后面一个分段
  auto it = segs_.lower_bound(start); // key&gt;=start
  // 有可能被前面分段覆盖
  if (it != segs_.begin()) {
    auto prev_it = std::prev(it);
    if (prev_it-&gt;first + static_cast&amp;#x3C;uint64_t&gt;(prev_it-&gt;second.size()) &gt; start) {
      it = prev_it;
    }
  }

  // 可能与多个分段存在重叠，因此需要从最早的那个开始遍历
  uint64_t pos = start;
  while (pos &amp;#x3C; end &amp;#x26;&amp;#x26; it != segs_.end()) {
    uint64_t L = it-&gt;first;
    uint64_t R = it-&gt;first + static_cast&amp;#x3C;uint64_t&gt;(it-&gt;second.size());

    // 判断每个分段与当前分段的重叠情况
    // 1. R &amp;#x3C;= pos ：直接跳过，不用考虑
    if (R &amp;#x3C;= pos) {
      it++;
      continue;
    }

    // 2. L &gt; pos：考虑 clipped 的右端是否被覆盖
    if (L &gt; pos) {
      const uint64_t gap_end = std::min&amp;#x3C;uint64_t&gt;(end, L);
      // 从 clipped 中裁剪出 gap
      const size_t off = static_cast&amp;#x3C;size_t&gt;(pos - start);
      const size_t len = static_cast&amp;#x3C;size_t&gt;(gap_end - pos);
      std::string gap = clipped.substr(off, len);

      // 入库
      if (!gap.empty()) {
        segs_.emplace(pos, std::move(gap));
        unassembled_ += len;
      }
      pos = gap_end;
      if (pos == end) // 整个分段被处理完成
        break;
    }

    // 3. L &amp;#x3C;= pos &amp;#x3C; R，左端被覆盖
    pos = R;
    ++it;
  }

  // 有可能clipped超出了segs现在的范围，导致其还剩余一个后置gap
  // 比如 clipped: [6, 10), segs最后一段: [7, 9), 导致 [9, 10)需要添加在最后
  if (pos &amp;#x3C; end) {
    const size_t off = static_cast&amp;#x3C;size_t&gt;(pos - start);
    const size_t len = static_cast&amp;#x3C;size_t&gt;(end - pos);
    std::string last_gap = clipped.substr(off, len);
    if (!last_gap.empty()) {
      segs_.emplace(pos, std::move(last_gap));
      unassembled_ += len;
    }
  }

  // 开始push连续的分段
  while (true) {
    // bytes_pushed_会一直更新，因此每次都要重新获取
    const uint64_t next = output_.writer().bytes_pushed();
    auto hit = segs_.find(next);
    if (hit == segs_.end())
      break;

    output_.writer().push(hit-&gt;second);
    unassembled_ -= hit-&gt;second.size();
    segs_.erase(hit);
  }

  // 最后只差eof时(bytes_pushed_ == *eof_index_)，可以开始关闭接收端口
  if (eof_index_.has_value() &amp;#x26;&amp;#x26; output_.writer().bytes_pushed() == *eof_index_) {
    output_.writer().close();
  }
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CS144 Checkpoint0</title><link>https://siolin.me/blog/cs/checkpoint0</link><guid isPermaLink="true">https://siolin.me/blog/cs/checkpoint0</guid><description>实现get_URL请求Web数据，和TCP层次结构中的ByteStream。</description><pubDate>Fri, 05 Sep 2025 17:51:12 GMT</pubDate><content:encoded>&lt;p&gt;整个 cs144 的实验结构层次图如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;应用层程序
   │
[ TCPSocket ]   ← 提供 connect/read/write 接口
   │
[ TCPConnection ] ← 整体状态机，协调发送方和接收方
   ├─ [ TCPSender ]   ← 分片、发送、重传
   └─ [ TCPReceiver ] ← 重排、确认、窗口
        │
   [ Reassembler ]   ← 拼接乱序片段
        │
   [ ByteStream ]    ← 有限容量的字节缓冲
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实验的顺序为层次图从低到高，本实验中需要实现 &lt;code&gt;ByteStream&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里其实有个小疑问：既然 &lt;code&gt;Socket&lt;/code&gt; 继承于 &lt;code&gt;FileDescritpion&lt;/code&gt;，其中已经实现了文件的读写，为什么还要在底层实验 &lt;code&gt;ByteStream&lt;/code&gt;，而不是直接在 &lt;code&gt;Socket&lt;/code&gt; 中封装对应 API 和状态？
其实是因为 CS144 为了教学目的，将这些功能拎出来进行封装，使得层次更加清晰；而在真实的操作系统中，以上功能是封装在一起的，也不需要增加一个 &lt;code&gt;ByteStream&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在 Linux 内核中的层次结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-perl&quot;&gt;┌───────────────────────────────┐
│        应用层程序 (用户态)      │
│  read/write, send/recv, etc.  │
└───────────────┬───────────────┘
                │
        系统调用接口 (syscall)
                │
┌───────────────▼────────────────┐
│     Socket 内核对象 (黑盒)       │  ← Linux 内核实现
│  struct socket / tcp_sock       │
│                                 │
│  - API 封装 (send/recv)         │
│  - TCP 状态机 (ESTABLISHED...)  │
│  - 序号空间 (SND.NXT/RCV.NXT)   │
│  - 定时器、RTT 估算             │
│  - 发送缓冲区 sndbuf            │
│  - 接收缓冲区 rcvbuf            │
│  - 乱序重组、ACK、重传          │
└───────────────┬────────────────┘
                │
                ▼
        TCP/IP 协议栈 (内核实现)
                │
                ▼
         网络接口/驱动/硬件

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;webget&lt;/h2&gt;
&lt;p&gt;首先来看文件 &lt;code&gt;file_descriptor.hh&lt;/code&gt;，其中使用 &lt;code&gt;FDWrapper&lt;/code&gt; 来保存fd及其状态信息，而 &lt;code&gt;FileDescriptor&lt;/code&gt; 提供了对外的操作接口并通过 &lt;code&gt;shared_ptr&lt;/code&gt; 来管理 &lt;code&gt;FDWrapper&lt;/code&gt;，多个 &lt;code&gt;FileDescriptor&lt;/code&gt; 可共享同一个fd(&lt;code&gt;FDWrapper&lt;/code&gt;)，内部增加其引用计数。&lt;/p&gt;
&lt;p&gt;在 socket.hh 中，&lt;code&gt;Socket&lt;/code&gt; 继承了 &lt;code&gt;FileDescriptor&lt;/code&gt;，证明 &lt;code&gt;Socket&lt;/code&gt; 本身就是一个文件描述符 fd，使用 &lt;code&gt;FileDescriptor&lt;/code&gt; 管理fd声明周期，并在 &lt;code&gt;Socket&lt;/code&gt; 中封装了 socket 相关的操作。
该文件中声明了许多类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;DatagramSocket&lt;/code&gt;：一个抽象层，封装了面向数据报（与之相反的是面向连接的 TCP）的 socket 操作，因为最后几个 class 都是面向数据报的，所以在这里添加一个抽象层用于继承&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UDPSocket&lt;/code&gt;：UPD socket，直接继承的 &lt;code&gt;DatagramSocket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TCPSocket&lt;/code&gt;：TCP socket，继承于 &lt;code&gt;Socket&lt;/code&gt;，并提供一些面向连接的 API，如 &lt;code&gt;listen()&lt;/code&gt; 和 &lt;code&gt;accept()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一些继承于 &lt;code&gt;DatagramSocket&lt;/code&gt; 的 socket&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;webget.cc&lt;/code&gt; 中 &lt;code&gt;get_URL()&lt;/code&gt; 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 该文件的目的是使用TCP套接字连接到Web服务器并获取一个URL。

void get_URL(const string&amp;#x26; host, const string&amp;#x26; path)

{
  // cerr &amp;#x3C;&amp;#x3C; &quot;Function called: get_URL(&quot; &amp;#x3C;&amp;#x3C; host &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; path &amp;#x3C;&amp;#x3C; &quot;)\n&quot;;
  // cerr &amp;#x3C;&amp;#x3C; &quot;Warning: get_URL() has not been implemented yet.\n&quot;;

  // 1. 与web建立连接
  // 这里并没有调用bind()来绑定本地地址，因为客户端的内核会自动进行隐式绑定
  TCPSocket client;
  client.connect(Address(host, &quot;http&quot;));

  // 2. 组装请求报文
  string msg;
  msg += &quot;GET &quot; + path + &quot; HTTP/1.1\r\n&quot;;
  msg += &quot;Host: &quot; + host + &quot;\r\n&quot;;
  msg += &quot;Connection: close\r\n&quot;; // 非持续连接
  msg += &quot;\r\n&quot;;                  // 报文要以\r\n结尾

  // 3. 发送请求报文
  client.write(msg);

  // 4. 循环获取响应报文，直到找到EOF
  string resp;
  while (!client.eof()) {
    resp.clear();
    client.read(resp);
    cout &amp;#x3C;&amp;#x3C; resp;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ByteStream&lt;/h2&gt;
&lt;p&gt;这个实现也很简单，就是设立缓冲区，实现 &lt;code&gt;ByteStream&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;byte_stream.hh&lt;/code&gt; 中添加维护字段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;protected:
  // Please add any additional state to the ByteStream here, and not to the Writer and Reader interfaces.
  uint64_t capacity_;
  bool error_ {};
  std::string buffer_;
  int bytes_pushed_;
  int bytes_popped_;
  bool closed_;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;byte_stream.cc 中实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;byte_stream.hh&quot;
#include &amp;#x3C;cstdint&gt;
#include &amp;#x3C;string_view&gt;

using namespace std;

ByteStream::ByteStream(uint64_t capacity)
  : capacity_(capacity), buffer_(), bytes_pushed_(0), bytes_popped_(0), closed_(false)
{}

void Writer::push(string data)
{
  if (closed_) {
    return;
  }
  
  uint64_t available = available_capacity();
  if (data.size() &gt; available) {
    data.resize(available);
  }

  buffer_.append(data);
  bytes_pushed_ += data.size(); // 这里不能加available，因为data长度可能没有溢出
}

void Writer::close()
{
  closed_ = true;
}

bool Writer::is_closed() const
{
  return closed_;
}

  

uint64_t Writer::available_capacity() const
{
  return capacity_ - buffer_.size();
}

uint64_t Writer::bytes_pushed() const
{
  return bytes_pushed_;
}

string_view Reader::peek() const
{
  return string_view(buffer_);
}

void Reader::pop(uint64_t len)
{
  if (len &gt; buffer_.size()) {
    set_error();
  }
  
  buffer_.erase(0, len);
  bytes_popped_ += len;
}

bool Reader::is_finished() const
{
  return closed_ &amp;#x26;&amp;#x26; buffer_.size() == 0;
}
  
uint64_t Reader::bytes_buffered() const
{
  return buffer_.size();
}

uint64_t Reader::bytes_popped() const
{
  return bytes_popped_;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025-08-31</title><link>https://siolin.me/blog/phase_recap/tophalf_daer</link><guid isPermaLink="true">https://siolin.me/blog/phase_recap/tophalf_daer</guid><description>暑假结束，大二上新学期伊始。</description><pubDate>Sun, 31 Aug 2025 10:56:32 GMT</pubDate><content:encoded>&lt;p&gt;暑假学习了操作系统的课程—mit6.s081，但是似乎仅限于此。对比放假前立下的目标——力扣竞赛分数、编程语言的深度学习，多多少少还有一段距离。&lt;/p&gt;
&lt;p&gt;暑假的颓废然我想起了很早之间刷到的一个油管视频，里面讲解了极度专注+短暂放松的学习方法，并建议在特定的场所学习，在自己的房间或宿舍的话就会不由自主的做一些无关紧要的事情（行动成本更低）。&lt;/p&gt;
&lt;p&gt;眼看与同龄人的差距越来越大，我必须加快自己的学习进度，这学期的时间应该大部分都要在图书馆度过。对于塞满日程的专业（水）课，能逃就逃，逃不掉就看专业书。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab mmap</title><link>https://siolin.me/blog/cs/10_mmap</link><guid isPermaLink="true">https://siolin.me/blog/cs/10_mmap</guid><description>实现mmap和munmap功能。</description><pubDate>Mon, 18 Aug 2025 22:18:13 GMT</pubDate><content:encoded>&lt;p&gt;参考博客：&lt;a href=&quot;https://fanxiao.tech/posts/2021-03-02-mit-6s081-notes/#144-lab-10-mmap&quot;&gt;Xiao Fan（樊潇）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;实验目的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现一个功能稍微简略的 &lt;code&gt;mmap()&lt;/code&gt;，&lt;code&gt;addr&lt;/code&gt; 始终为零，即由内核决定映射文件的虚拟地址&lt;/li&gt;
&lt;li&gt;实现 &lt;code&gt;munmap()&lt;/code&gt;，移除指定地址范围内的内存映射。如果进程已修改该内存且将其映射 &lt;code&gt;MAP_SHARED&lt;/code&gt;，则应现将修改内容写入文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先是将 &lt;code&gt;$U/_mmaptest\&lt;/code&gt; 添加到 Makefile，然后添加 &lt;code&gt;mmap()&lt;/code&gt; 和 &lt;code&gt;munmap()&lt;/code&gt; 系统调用，这里不再赘述。&lt;/p&gt;
&lt;p&gt;在 proc.h 中添加对 &lt;code&gt;struct vma&lt;/code&gt; 的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct vma {
  int valid;
  uint64 addr;
  int length;
  int prot;
  int flags;
  struct file *mapfile;
};

// Per-process state
struct proc {
  // ...
  struct vma vmas[NVMA];       // Process vmas
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于默认 offset 为零，因此这里不需要声明该字段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 param.h 中添加 &lt;code&gt;NVMA&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define NVMA 16 // number of process vmas
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 sysfile.c 中添加 &lt;code&gt;sys_mmap()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64 sys_mmap(void) {
  int length, prot, flags, fd;
  struct proc *p = myproc();
  struct file *mapfile;

  // get argument
  if (argint(1, &amp;#x26;length) &amp;#x3C; 0 || argint(2, &amp;#x26;prot) &amp;#x3C; 0 ||
      argint(3, &amp;#x26;flags) &amp;#x3C; 0 || argfd(4, &amp;#x26;fd, &amp;#x26;mapfile) &amp;#x3C; 0)
    return -1;

  // check
  length = PGROUNDDOWN(length);
  if(MAXVA - length &amp;#x3C; p-&gt;sz)
    return -1;
  if (!mapfile-&gt;readable &amp;#x26;&amp;#x26; (prot &amp;#x26; PROT_READ))
    return -1;
  if (!mapfile-&gt;writable &amp;#x26;&amp;#x26; (prot &amp;#x26; PROT_WRITE) &amp;#x26;&amp;#x26; (flags &amp;#x26; MAP_SHARED))
    return -1;

  // find a free vma and contain it
  for (int i = 0; i &amp;#x3C; NVMA; ++i) {
    struct vma *curvma = &amp;#x26;p-&gt;vmas[i];
    if (!curvma-&gt;valid) {
      curvma-&gt;valid = 1;
      curvma-&gt;addr = p-&gt;sz;
      p-&gt;sz += length;
      curvma-&gt;length = length;
      curvma-&gt;flags = flags;
      curvma-&gt;prot = prot;
      curvma-&gt;mapfile = mapfile;
      filedup(mapfile);
      return curvma-&gt;addr;
    }
  }

  // no free vma
  return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里固定 &lt;code&gt;addr&lt;/code&gt; 为 0，因此不需要获取&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pvma[i].addr = p-&gt;sz&lt;/code&gt;：将新映射的内存区域放在堆的栈顶，紧接在现有地址空间之后&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pvma[i].valid_len = pvma[i].len&lt;/code&gt;：延迟加载，初始时未分配任何物理页，但是要将其表示为已占用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来在 &lt;code&gt;usertrap()&lt;/code&gt; 中添加对页错误的处理，实现延迟加载：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;} else if (r_scause() == 13 || r_scause() == 15) { // Page Fault
    uint64 va = r_stval();
    struct proc *p = myproc();
    struct vma *vmas = p-&gt;vmas;

    // check va safe
    if (va &gt; MAXVA || va &gt;= p-&gt;sz)
      goto exception;

    // lazy allocation
    for (int i = 0; i &amp;#x3C; NVMA; ++i) {
      struct vma *curvma = &amp;#x26;vmas[i];
      if (curvma-&gt;valid &amp;#x26;&amp;#x26; va &gt;= curvma-&gt;addr &amp;#x26;&amp;#x26;
          va &amp;#x3C; curvma-&gt;addr + curvma-&gt;length) {
        va = PGROUNDDOWN(va);
        uint64 pa = (uint64)kalloc();
        if (pa == 0)
          goto exception;
        memset((void *)pa, 0, PGSIZE);
        ilock(curvma-&gt;mapfile-&gt;ip);
        if (readi(curvma-&gt;mapfile-&gt;ip, 0, pa, va - curvma-&gt;addr, PGSIZE) &amp;#x3C; 0) {
          iunlock(curvma-&gt;mapfile-&gt;ip);
          break;
        }
        iunlock(curvma-&gt;mapfile-&gt;ip);
        int flag = (curvma-&gt;prot &amp;#x3C;&amp;#x3C; 1) | PTE_V | PTE_U;
        if (mappages(p-&gt;pagetable, va, PGSIZE, pa, flag) &amp;#x3C; 0) {
          kfree((void*)pa);
          break;
        }
        break;
      }
    } 
  } else {
      exception:
      printf(&quot;usertrap(): unexpected scause %p pid=%d\n&quot;, r_scause(), p-&gt;pid);
      printf(&quot;            sepc=%p stval=%p\n&quot;, r_sepc(), r_stval());
      p-&gt;killed = 1;
    }

    if (p-&gt;killed)
      exit(-1);

    // give up the CPU if this is a timer interrupt.
    if (which_dev == 2)
      yield();

    usertrapret();
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int flag = (pvma[i].prot &amp;#x3C;&amp;#x3C; 1) | PTE_U | PTE_V&lt;/code&gt;：这里需要将 &lt;code&gt;prot&lt;/code&gt; 转换为 PTE 的权限位
&lt;ul&gt;
&lt;li&gt;在 PTE 中 &lt;code&gt;PTE_R (1L &amp;#x3C;&amp;#x3C; 1)、PTE_W (1L &amp;#x3C;&amp;#x3C; 2)、PTE_X (1L &amp;#x3C;&amp;#x3C; 3)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;prot&lt;/code&gt; 中 &lt;code&gt;PROT_READ 0x1、PROT_WRITE 0x2、PROT_EXEC 0x4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;也就是说 &lt;code&gt;prot&lt;/code&gt; 向左移动一位正好匹配 PTE 的标志位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来实现 &lt;code&gt;munmap()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64 sys_munmap(void) {
  uint64 addr;
  int length;
  struct proc *p = myproc();
  
  // get argument
  if (argaddr(0, &amp;#x26;addr) &amp;#x3C; 0 || argint(1, &amp;#x26;length) &amp;#x3C; 0)
    return -1;

  // look for vma
  struct vma *vma = 0;
  int found = 0;
  for (int i = 0; i &amp;#x3C; NVMA; ++i) {
    vma = &amp;#x26;p-&gt;vmas[i];
    if (vma-&gt;valid &amp;#x26;&amp;#x26; addr &gt;= vma-&gt;addr &amp;#x26;&amp;#x26; addr &amp;#x3C; vma-&gt;addr + vma-&gt;length) {
      found = 1;
      break;
    }
  }
  
  // not found
  if (!found)
    return -1;

  addr = PGROUNDDOWN(addr);
  length = PGROUNDDOWN(length);
  if (vma-&gt;flags &amp;#x26; MAP_SHARED) {
    // if MAP_SHARED then write back first
    if (filewrite(vma-&gt;mapfile, addr, length) &amp;#x3C; 0)
      printf(&quot;munmap: filewrite &amp;#x3C; 0\n&quot;);
  }

  // unmapped
  uvmunmap(p-&gt;pagetable, addr, length / PGSIZE, 1);

  if (addr == vma-&gt;addr) {
    if (length == vma-&gt;length) {
      // unmapped whole vma
      fileclose(vma-&gt;mapfile);
      vma-&gt;valid = 0;
      p-&gt;sz -= length;
    } else {
      // unmapped from start to middle
      vma-&gt;addr += length;
      vma-&gt;length -= length;
    } 
  } else if (addr + length == vma-&gt;addr + vma-&gt;length) {
    // unmapped from middle to end
    vma-&gt;length -= length;
  } else {
    return -1;
  }

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果进程已修改该内存且将其映射 &lt;code&gt;MAP_SHARED&lt;/code&gt; ，则应先将修改内容写入文件&lt;/li&gt;
&lt;li&gt;分情况讨论取消映射的范围，要么从起始处开始，要么一直到末尾，而不会在中间打洞&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvmunmap()&lt;/code&gt; 取消映射并释放先前分配的物理内存，最后一个参数为 &lt;code&gt;1&lt;/code&gt;（为 &lt;code&gt;0&lt;/code&gt; 则不释放物理内存）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p-&gt;sz -= length&lt;/code&gt;：既然是取消映射了整个 vma，这里应当更新 &lt;code&gt;p-&gt;sz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更新 &lt;code&gt;fork()&lt;/code&gt; 和 &lt;code&gt;exit()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

	// Copy vma from parent to child
  for (int i = 0; i &amp;#x3C; NVMA; i++) {
    if (p-&gt;vmas[i].valid) {
      memmove(&amp;#x26;np-&gt;vmas[i], &amp;#x26;p-&gt;vmas[i], sizeof(struct vma));
      filedup(np-&gt;vmas[i].mapfile);
    }
  }

  // ...
}

// Exit the current process.  Does not return.
// An exited process remains in the zombie state
// until its parent calls wait().
void exit(int status)
{
  struct proc *p = myproc();

  if(p == initproc)
    panic(&quot;init exiting&quot;);

  // Close all open files.
  for(int fd = 0; fd &amp;#x3C; NOFILE; fd++){
    if(p-&gt;ofile[fd]){
      struct file *f = p-&gt;ofile[fd];
      fileclose(f);
      p-&gt;ofile[fd] = 0;
    }
  }

  // unmap all vma
  for (int i = 0; i &amp;#x3C; NVMA; ++i) {
    if (p-&gt;vmas[i].valid) {
      if (p-&gt;vmas[i].flags &amp;#x26; MAP_SHARED) {
        filewrite(p-&gt;vmas[i].mapfile, p-&gt;vmas[i].addr, p-&gt;vmas[i].length);
      }
      fileclose(p-&gt;vmas[i].mapfile);
      uvmunmap(p-&gt;pagetable, p-&gt;vmas[i].addr, p-&gt;vmas[i].length / PGSIZE, 1);
      p-&gt;vmas[i].valid = 0;
    }
  }

  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里如果是 &lt;code&gt;MAP_SHARED&lt;/code&gt; 则同样需要先写入&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;fileclose()&lt;/code&gt; 减少引用&lt;/li&gt;
&lt;li&gt;依旧使用 &lt;code&gt;uvmunmap()&lt;/code&gt; 取消映射并释放物理内存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这一步依然存在许多 bug，导致无法通过测试。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;munmap()&lt;/code&gt; 中存在 &lt;code&gt;unmap from start to middle&lt;/code&gt; 和 &lt;code&gt;unmap from middle to end&lt;/code&gt; 的情况，这就导致在 &lt;code&gt;p-&gt;sz&lt;/code&gt; 以内的内存并不一定都有映射，因此可能会造成 &lt;code&gt;uvmunmap()&lt;/code&gt; 和 &lt;code&gt;uvmcopy()&lt;/code&gt; 的 &lt;code&gt;panic&lt;/code&gt;，需要作以下修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// uvmcopy
if((*pte &amp;#x26; PTE_V) == 0)
     // panic(&quot;uvmcopy: page not present&quot;);
     continue;
  
// uvmunmap  
// if((*pte &amp;#x26; PTE_V) == 0)
//   panic(&quot;uvmunmap: not mapped&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外 &lt;code&gt;kfree&lt;/code&gt; 会试图解放 0 这个物理内存，参考博客的作者没有给出原因，我在上一个 bug 耗费了太多时间和精力，因此也没有弄明白，直接作修改吧：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
kfree(void *pa)
{
  struct run *r;
  if (pa == 0)
    return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个 lab 的逻辑并不是非常难（默认 &lt;code&gt;addr&lt;/code&gt; 和 &lt;code&gt;offset&lt;/code&gt; 为 0，简化了逻辑）&lt;/li&gt;
&lt;li&gt;后边所说的这些 bug 实属可恶，只能使用 &lt;code&gt;printf&lt;/code&gt; 慢慢找错误点&lt;/li&gt;
&lt;li&gt;查看了许多博客的实现，但是好多都没有涉及上述 bug 的解决，不知道是哪里有差异或是我使用的 21 年版本新出现的bug&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>XV6 Trap机制（1）：硬件支持</title><link>https://siolin.me/blog/cs/trap%E6%9C%BA%E5%88%B61%E7%A1%AC%E4%BB%B6%E6%94%AF%E6%8C%81</link><guid isPermaLink="true">https://siolin.me/blog/cs/trap%E6%9C%BA%E5%88%B61%E7%A1%AC%E4%BB%B6%E6%94%AF%E6%8C%81</guid><description>介绍XV6中Trap机制的RISC-V硬件支持部分。</description><pubDate>Mon, 18 Aug 2025 22:18:13 GMT</pubDate><content:encoded>&lt;p&gt;每个 RISC-V CPU 都有一组&lt;strong&gt;特权寄存器&lt;/strong&gt;，内核写入这些控制寄存器来告诉 CPU 如何处理 trap，并且内核可以读取这些寄存器来找出已发生的 trap（在 &lt;code&gt;kernel/riscv.h&lt;/code&gt; 中定义）。&lt;/p&gt;
&lt;p&gt;重要特权寄存器概述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stevc&lt;/code&gt;：&lt;strong&gt;trap handler 的地址&lt;/strong&gt;，由内核写入，告诉&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sepc&lt;/code&gt;：保存 trap 发生时的&lt;strong&gt;程序寄存器&lt;/strong&gt;（因为 &lt;code&gt;pc&lt;/code&gt; 随后会被 &lt;code&gt;stvec&lt;/code&gt; 中的值覆盖）。&lt;code&gt;sret&lt;/code&gt;（从 trap 返回）指令将 &lt;code&gt;sepc&lt;/code&gt; 复制到 &lt;code&gt;pc&lt;/code&gt;，可以通过编写 &lt;code&gt;sepc&lt;/code&gt; 来控制 &lt;code&gt;sret&lt;/code&gt; 的去向&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scause&lt;/code&gt;：放置一个数字来描述 &lt;strong&gt;trap 的原因&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sscratch&lt;/code&gt;：放在 trap handler 的最开始处，防止在保存用户寄存器之前覆盖它们&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sstatus&lt;/code&gt;：其中的 &lt;strong&gt;SIE 位控制是否启用设备中断&lt;/strong&gt;。如果内核清除 SIE，RISC-V 将推迟设备中断，直到内核设置 SIE。SPP 位指示 trap 是&lt;strong&gt;来自用户模式还是管理模式&lt;/strong&gt;，并控制 &lt;code&gt;sret&lt;/code&gt; 返回的模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;satp&lt;/code&gt;：&lt;strong&gt;当前页表的根地址&lt;/strong&gt;
上述寄存器与内核模式下处理的 trap 相关，并且&lt;strong&gt;不能在用户模式下读取或写入&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RISC-V 硬件会对所有 trap 类型（定时器中断除外）执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断屏蔽检查&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果 trap 是由&lt;strong&gt;设备中断&lt;/strong&gt;引发的，且 &lt;code&gt;sstatus.SIE=0&lt;/code&gt;，则处理器会暂存该中断，暂缓执行&lt;/li&gt;
&lt;li&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;ul&gt;
&lt;li&gt;设置 &lt;code&gt;sstatus.SIE=0&lt;/code&gt;：防止 trap 处理期间被其他中断嵌套&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;sepc&lt;/code&gt;：保存当前 &lt;code&gt;pc&lt;/code&gt;，以便 trap 返回时恢复执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sstatus.SPP&lt;/code&gt;：保存当前特权模式（0=user, 1=kernel）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置 trap 原因&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scause&lt;/code&gt;&lt;/strong&gt;：记录 trap 类型（中断或异常）和具体原因（如中断号或异常码）。&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;Supervisor Mode&lt;/strong&gt;，以便执行内核中的 trap handler。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转到 trap handler&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;stvec&lt;/code&gt; 的地址加载到 &lt;code&gt;pc&lt;/code&gt; 。
以上步骤为硬件操作，在发生 trap 后自动执行，并没有显式代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;中断屏蔽检查中为什么区别对待？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&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;li&gt;异常和系统调用的同步性
&lt;ul&gt;
&lt;li&gt;异常和系统调用是由当前指令直接触发的，是同步事件。&lt;/li&gt;
&lt;li&gt;异常通常表示必须立即处理的错误或特殊情况，如果不处理，程序无法正确执行。&lt;/li&gt;
&lt;li&gt;系统调用是程序主动请求内核服务，如果不处理，程序会一直等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;未完成的步骤（需软件处理）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切换页表：CPU 不会自动切换页表&lt;/li&gt;
&lt;li&gt;切换栈指针：CPU 不会自动切换栈&lt;/li&gt;
&lt;li&gt;保存寄存器：通用寄存器需由软件保存
CPU 保留上述步骤交给软件处理是为软件提供灵活性，比如某些操作系统在某些情况下会省略页表切换，&lt;strong&gt;硬件仅提供最小必要的支持&lt;/strong&gt;。而这些步骤都将在 &lt;code&gt;trapline&lt;/code&gt; 页（&lt;code&gt;uservec&lt;/code&gt;）中执行。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab fs</title><link>https://siolin.me/blog/cs/9_fs</link><guid isPermaLink="true">https://siolin.me/blog/cs/9_fs</guid><description>对文件系统的inode扩容，并实现软链接功能。</description><pubDate>Fri, 15 Aug 2025 17:58:31 GMT</pubDate><content:encoded>&lt;h2&gt;Large files&lt;/h2&gt;
&lt;p&gt;实验目的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;扩充 inode 的 &lt;code&gt;addrs&lt;/code&gt; 数组，为其减少一个直接块，增加一个二级间接块，其存储一级间接块的地址&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;bmap()&lt;/code&gt;，使得其能定位二级间接块里的数据块&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;itrunc()&lt;/code&gt;，使其能释放二级间接块及其中的所有块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;addrs&lt;/code&gt; 数组的结构：
&lt;img src=&quot;./images/dinode.png&quot; alt=&quot;image-20250731142016639&quot;&gt;&lt;/p&gt;
&lt;p&gt;首先修改全局变量以及 &lt;code&gt;struct inode/dinode&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT)

// On-disk inode structure
struct dinode {
	short type; // File type
	short major; // Major device number (T_DEVICE only)
	short minor; // Minor device number (T_DEVICE only)
	short nlink; // Number of links to inode in file system
	uint size; // Size of file (bytes)
	uint addrs[NDIRECT+2]; // Data block addresses
};

// in-memory copy of an inode
struct inode {
	uint dev; // Device number
	uint inum; // Inode number
	int ref; // Reference count
	struct sleeplock lock; // protects everything below here
	int valid; // inode has been read from disk?
	  
	short type; // file or directory
	short major;
	short minor;
	short nlink;
	uint size;
	uint addrs[NDIRECT+2];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着为 &lt;code&gt;bmap()&lt;/code&gt; 增加索引二级间接块的逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static uint
bmap(struct inode *ip, uint bn)
{
	uint addr, *a;
	struct buf *bp;
	  
	if(bn &amp;#x3C; NDIRECT){
		if((addr = ip-&gt;addrs[bn]) == 0)
			ip-&gt;addrs[bn] = addr = balloc(ip-&gt;dev);
		return addr;
	}
	bn -= NDIRECT;
	
	if(bn &amp;#x3C; NINDIRECT){
		// Load indirect block, allocating if necessary.
		if((addr = ip-&gt;addrs[NDIRECT]) == 0)
			ip-&gt;addrs[NDIRECT] = addr = balloc(ip-&gt;dev);
		bp = bread(ip-&gt;dev, addr);
		a = (uint*)bp-&gt;data;
		if((addr = a[bn]) == 0){
			a[bn] = addr = balloc(ip-&gt;dev);
			log_write(bp);
		}
		brelse(bp);
		return addr;
	}
	bn -= NINDIRECT;
	  
	if (bn &amp;#x3C; NINDIRECT * NINDIRECT) {
		int id = bn / NINDIRECT;
		int off = bn % NINDIRECT;
		if ((addr = ip-&gt;addrs[NDIRECT + 1]) == 0) // 先检查二级间接块是否存在
			ip-&gt;addrs[NDIRECT + 1] = addr = balloc(ip-&gt;dev);
		bp = bread(ip-&gt;dev, addr);
		a = (uint *)bp-&gt;data;
		if ((addr = a[id]) == 0) { // 检查其中的一级间接块是否存在
			a[id] = addr = balloc(ip-&gt;dev);
			log_write(bp); // 记录修改
		}
		brelse(bp); // 释放二级间接块
	  
		bp = bread(ip-&gt;dev, addr); // 读取一级间接块
		a = (uint *)bp-&gt;data;
		if ((addr = a[off]) == 0) {
			a[off] = addr = balloc(ip-&gt;dev);
			log_write(bp);
		}
		brelse(bp);
		return addr;
	}
	
	panic(&quot;bmap: out of range&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;itrunc()&lt;/code&gt; 的逻辑，使其能够释放二级间接块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
itrunc(struct inode *ip)
{
	int i, j;
	struct buf *bp;
	uint *a;
	
	for(i = 0; i &amp;#x3C; NDIRECT; i++){
		if(ip-&gt;addrs[i]){
			bfree(ip-&gt;dev, ip-&gt;addrs[i]);
			ip-&gt;addrs[i] = 0;
		}
	}
	  
	if(ip-&gt;addrs[NDIRECT]){
		bp = bread(ip-&gt;dev, ip-&gt;addrs[NDIRECT]);
		a = (uint*)bp-&gt;data;
		for(j = 0; j &amp;#x3C; NINDIRECT; j++){
			if(a[j])
				bfree(ip-&gt;dev, a[j]);
		}
		brelse(bp);
		bfree(ip-&gt;dev, ip-&gt;addrs[NDIRECT]);
		ip-&gt;addrs[NDIRECT] = 0;
	}
	
	// 释放二级间接块
	if (ip-&gt;addrs[NDIRECT + 1]) {
		bp = bread(ip-&gt;dev, ip-&gt;addrs[NDIRECT + 1]);
		a = (uint *)bp-&gt;data;
		
		struct buf *bps;
		uint *b;
		for (j = 0; j &amp;#x3C; NDIRECT; ++j) {
			if (a[j]) { // 一级间接块存在，则需要先释放其中的数据块
				bps = bread(ip-&gt;dev, a[j]);
				b = (uint *)bps-&gt;data;
				for (int i = 0; i &amp;#x3C; NDIRECT; ++i) {
					if (b[i])
						bfree(ip-&gt;dev, b[i]);
				}
				brelse(bps);
				bfree(ip-&gt;dev, a[j]); // 释放一级间接块
			}
		}
		brelse(bp);
		bfree(ip-&gt;dev, ip-&gt;addrs[NDIRECT + 1]);
		ip-&gt;addrs[NDIRECT + 1] = 0;
	}
	
	ip-&gt;size = 0;
	iupdate(ip);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbolic links&lt;/h2&gt;
&lt;p&gt;硬链接是同一个文件的多个目录入口，指向相同的 inode；而软链接则是一个独立的文件，存储的是目标文件的路径。&lt;/p&gt;
&lt;p&gt;实验目的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加并实现 &lt;code&gt;symlink(char *target, char *path)&lt;/code&gt; 系统调用，使得为 &lt;code&gt;target&lt;/code&gt; 创建 &lt;code&gt;path&lt;/code&gt; 软链接&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;open()&lt;/code&gt;，添加对软链接的处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 &lt;code&gt;symlink()&lt;/code&gt; 系统调用的添加不再赘述。&lt;/p&gt;
&lt;p&gt;在 fcntl. h 中添加 &lt;code&gt;O_NOFOLLOW&lt;/code&gt;，由于不能与已有标志重叠，所以设置为 &lt;code&gt;0x800&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define O_RDONLY 0x000
#define O_WRONLY 0x001
#define O_RDWR 0x002
#define O_CREATE 0x200
#define O_TRUNC 0x400
#define O_NOFOLLOW 0x800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;symlink()&lt;/code&gt; 的实现：创建一个 inode，设置类型为 &lt;code&gt;T_SYMLINK&lt;/code&gt;，然后向 inode 中写入 &lt;code&gt;path&lt;/code&gt; 即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64
sys_symlink(void)
{
  char target[MAXPATH];
  memset(target, 0, sizeof(target));
  char path[MAXPATH];
  if(argstr(0, target, MAXPATH) &amp;#x3C; 0 || argstr(1, path, MAXPATH) &amp;#x3C; 0){
    return -1;
  }
  
  struct inode *ip;

  begin_op();
  if((ip = create(path, T_SYMLINK, 0, 0)) == 0){
    end_op();
    return -1;
  }

  if(writei(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){
    // panic(&quot;symlink write failed&quot;);
    return -1;
  }

  iunlockput(ip);
  end_op();
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;sys_open&lt;/code&gt; 中添加对符号链接的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64
sys_open(void)
{
  ...
	if(ip-&gt;type == T_DEVICE &amp;#x26;&amp;#x26; (ip-&gt;major &amp;#x3C; 0 || ip-&gt;major &gt;= NDEV)){
		...
	}

  if(ip-&gt;type == T_SYMLINK){
    if(!(omode &amp;#x26; O_NOFOLLOW)){ // 检查是否要求不解析链接
      int cycle = 0;
      char target[MAXPATH];
      while(ip-&gt;type == T_SYMLINK){
        if(cycle == 10){ // 最大递归深度10
          iunlockput(ip);
          end_op();
          return -1; // max cycle
        }
        cycle++;
        // 读取目标路径
        memset(target, 0, sizeof(target));
        readi(ip, 0, (uint64)target, 0, MAXPATH);
        iunlockput(ip);
        // 根据目标路径获取新的inode
        if((ip = namei(target)) == 0){
          end_op();
          return -1; // target not exist
        }
        ilock(ip);
      }
    }
  }

  if((f = filealloc()) == 0 || (fd = fdalloc(f)) &amp;#x3C; 0){
		...
	}
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;文件路径指向一个软链接时，系统需要递归地解析链接目标，直到找到最终的非链接文件或达到最大递归深度&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab lock</title><link>https://siolin.me/blog/cs/8_lock</link><guid isPermaLink="true">https://siolin.me/blog/cs/8_lock</guid><description>通过更细致的数据结构和锁划分来减少锁的争用，从而提升执行效率。</description><pubDate>Fri, 15 Aug 2025 04:19:20 GMT</pubDate><content:encoded>&lt;h2&gt;Memory allocator&lt;/h2&gt;
&lt;p&gt;实验要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为每个 CPU 维护一个空闲链表，每个链表配备自己的锁&lt;/li&gt;
&lt;li&gt;如果某个 CPU 的空闲链表为空，另一个 CPU 的链表仍有空闲内存，则该 CPU “窃取”其他 CPU 的空闲页&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先是为每个 CPU 维护一个空闲链表，并配备锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct {
	struct spinlock lock;
	struct run *freelist;
} kmem[NCPU];

void kinit() {
	for (int i = 0; i &amp;#x3C; NCPU; ++i) {
		initlock(&amp;#x26;kmem[i].lock, &quot;kmem&quot;);
	}
	freerange(end, (void *)PHYSTOP);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里将每个锁都命名为 &lt;code&gt;keme&lt;/code&gt; 也是没问题的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修改 &lt;code&gt;kfree()&lt;/code&gt;，使其将空闲内存分配给当前 CPU 的空闲链表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void kfree(void *pa) {
	struct run *r;
	
	if (((uint64)pa % PGSIZE) != 0 || (char *)pa &amp;#x3C; end || (uint64)pa &gt;= PHYSTOP)
		panic(&quot;kfree&quot;);
	
	// Fill with junk to catch dangling refs.
	memset(pa, 1, PGSIZE);
	
	r = (struct run *)pa;
	
	push_off();
	int icpu = cpuid();
	pop_off();
	
	acquire(&amp;#x26;kmem[icpu].lock);
	r-&gt;next = kmem[icpu].freelist;
	kmem[icpu].freelist = r;
	release(&amp;#x26;kmem[icpu].lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;cpuid()&lt;/code&gt; 时需要禁用中断，这样才能保证其结果准确&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修改 &lt;code&gt;kalloc()&lt;/code&gt;，使其能够在链表为空时“窃取”别的 CPU 的内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *kalloc(void) {
	struct run *r;
	
	push_off();
	int icpu = cpuid();
	pop_off();  
	
	acquire(&amp;#x26;kmem[icpu].lock);
	r = kmem[icpu].freelist;
	if (r)
		kmem[icpu].freelist = r-&gt;next;
	if (!r) {
		for (int i = 0; i &amp;#x3C; NCPU; ++i) {
			if (i == icpu) // 当前cpu
				continue;
			acquire(&amp;#x26;kmem[i].lock);
			r = kmem[i].freelist;
			if (r) {
				kmem[i].freelist = r-&gt;next;
				release(&amp;#x26;kmem[i].lock);
				break;
			}
			release(&amp;#x26;kmem[i].lock);
		}
	}
	release(&amp;#x26;kmem[icpu].lock);
	  
	if (r)
		memset((char *)r, 5, PGSIZE); // fill with junk
	return (void *)r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;!r&lt;/code&gt; 代表当前 CPU 链表已空，需要窃取内存&lt;/li&gt;
&lt;li&gt;遍历其他 CPU 获取空闲内存&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Buffer cache&lt;/h2&gt;
&lt;p&gt;实验要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用哈希表代替 LRU 双向链表，为每个桶分配锁，从而减少对整体锁的争用&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;ticks&lt;/code&gt; 来寻找 LRU &lt;code&gt;buf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先修改 &lt;code&gt;struct buf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct buf {
	int valid; // has data been read from disk?
	int disk; // does disk &quot;own&quot; buf?
	uint dev;
	uint blockno;
	struct sleeplock lock;
	uint refcnt;
	// struct buf *prev; // LRU cache list
	struct buf *next;
	uchar data[BSIZE];
	  
	uint timestamp;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不需要使用 &lt;code&gt;prev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;增加 &lt;code&gt;timestamp&lt;/code&gt; 来表示其最近被使用的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;声明变量和数据结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;extern uint ticks;
  
#define NBUCKET 13
#define NBUF (NBUCKET * 3)
  
struct {
	struct spinlock lock;
	struct buf buf[NBUF];
} bcache;
 
struct bucket {
	struct spinlock lock;
	struct buf head;
} hashtable[NBUCKET];

uint hash(uint blockno) { return blockno % NBUCKET; }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里放弃了原来声明的 &lt;code&gt;NBUF&lt;/code&gt;，这样改可以平均桶的分配&lt;/li&gt;
&lt;li&gt;全局锁依然有存在的必要，比如保护 &lt;code&gt;buf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着在 &lt;code&gt;binit()&lt;/code&gt; 中所有的锁以及初始化哈希表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void binit(void) {
	struct buf *b;
	  
	initlock(&amp;#x26;bcache.lock, &quot;bcache&quot;);
	
	for (b = bcache.buf; b &amp;#x3C; bcache.buf + NBUF; b++) {
		initsleeplock(&amp;#x26;b-&gt;lock, &quot;buffer&quot;);
	}
	
	b = bcache.buf;
	for (int i = 0; i &amp;#x3C; NBUCKET; i++) {
		initlock(&amp;#x26;hashtable[i].lock, &quot;bcache_bucket&quot;);
		for (int j = 0; j &amp;#x3C; NBUF / NBUCKET; j++) {
			b-&gt;blockno = i; // hash(b) should equal to i
			b-&gt;next = hashtable[i].head.next;
			hashtable[i].head.next = b;
			b++;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;这里将所有 &lt;code&gt;buf&lt;/code&gt; 平均分配到每个桶&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后是核心函数 &lt;code&gt;bget()&lt;/code&gt;，需要在哈希表中找到目标 &lt;code&gt;buf&lt;/code&gt;，如果没有缓存的话需要分配 LRU &lt;code&gt;buf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static struct buf *bget(uint dev, uint blockno) {
	// printf(&quot;dev: %d blockno: %d Status: &quot;, dev, blockno);
	struct buf *b;
	
	int idx = hash(blockno);
	struct bucket *bucket = hashtable + idx;
	acquire(&amp;#x26;bucket-&gt;lock);
	
	// Is the block already cached?
	for (b = bucket-&gt;head.next; b != 0; b = b-&gt;next) {
		if (b-&gt;dev == dev &amp;#x26;&amp;#x26; b-&gt;blockno == blockno) {
			b-&gt;refcnt++;
			b-&gt;timestamp = ticks;
			release(&amp;#x26;bucket-&gt;lock);
			acquiresleep(&amp;#x26;b-&gt;lock);
			return b;
		}
	}
	  
	// Not cached.
	// Look for LRU buf in current bucket
	uint min_time = __UINT32_MAX__;
	struct buf *replace_buf = 0;
	for (b = bucket-&gt;head.next; b != 0; b = b-&gt;next) {
		if (b-&gt;refcnt == 0 &amp;#x26;&amp;#x26; b-&gt;timestamp &amp;#x3C; min_time) {
			replace_buf = b;
			min_time = b-&gt;timestamp;
		}
	}
	if (replace_buf) {
		goto find;
	}
	
	// Try to find in other bucket.
	acquire(&amp;#x26;bcache.lock);
	refind:
	for (b = bcache.buf; b &amp;#x3C; bcache.buf + NBUF; b++) {
		if (b-&gt;refcnt == 0 &amp;#x26;&amp;#x26; b-&gt;timestamp &amp;#x3C; min_time) {
			replace_buf = b;
			min_time = b-&gt;timestamp;
		}
	}
	if (replace_buf) {
		// remove from old bucket
		int ridx = hash(replace_buf-&gt;blockno);
		acquire(&amp;#x26;hashtable[ridx].lock);
		if (replace_buf-&gt;refcnt != 1) // be used in another bucket&apos;s local find between finded and acquire
		{
			release(&amp;#x26;hashtable[ridx].lock);
			goto refind;
		}
		struct buf *pre = &amp;#x26;hashtable[ridx].head;
		struct buf *p = hashtable[ridx].head.next;
		while (p != replace_buf) {
			pre = pre-&gt;next;
			p = p-&gt;next;
		}
		pre-&gt;next = p-&gt;next;
		release(&amp;#x26;hashtable[ridx].lock);
		// add to current bucket
		replace_buf-&gt;next = hashtable[idx].head.next;
		hashtable[idx].head.next = replace_buf;
		release(&amp;#x26;bcache.lock);
		goto find;
	} else {
		panic(&quot;bget: no buffers&quot;);
	}
	
	find:
	replace_buf-&gt;dev = dev;
	replace_buf-&gt;blockno = blockno;
	replace_buf-&gt;valid = 0;
	replace_buf-&gt;refcnt = 1;
	release(&amp;#x26;bucket-&gt;lock);
	acquiresleep(&amp;#x26;replace_buf-&gt;lock);
	return replace_buf;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里便可以看出对哈希表和 &lt;code&gt;buf&lt;/code&gt; 链表分别上锁的好处：可以直接遍历 &lt;code&gt;buf&lt;/code&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;brelse()&lt;/code&gt;，减少计数，如果引用为零的话表示空闲，更新其时间戳：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void brelse(struct buf *b) {
	if (!holdingsleep(&amp;#x26;b-&gt;lock))
		panic(&quot;brelse&quot;);
	
	releasesleep(&amp;#x26;b-&gt;lock);
	  
	int idx = hash(b-&gt;blockno);
	
	acquire(&amp;#x26;hashtable[idx].lock);
	b-&gt;refcnt--;
	if (b-&gt;refcnt == 0) {
		// no one is waiting for it.
		b-&gt;timestamp = ticks;
	}
	
	release(&amp;#x26;hashtable[idx].lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩余的 &lt;code&gt;bpin()&lt;/code&gt; / &lt;code&gt;bunpin()&lt;/code&gt; 只需更新锁的获取就行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void bpin(struct buf *b) {
	int idx = hash(b-&gt;blockno);
	acquire(&amp;#x26;hashtable[idx].lock);
	b-&gt;refcnt++;
	release(&amp;#x26;hashtable[idx].lock);
}

void bunpin(struct buf *b) {
	int idx = hash( b-&gt;blockno);
	acquire(&amp;#x26;hashtable[idx].lock);
	b-&gt;refcnt--;
	release(&amp;#x26;hashtable[idx].lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ bcachetest
start test0
test0 results:
--- lock kmem/bcache stats
lock: kmem: #test-and-set 0 #acquire() 32928
lock: kmem: #test-and-set 0 #acquire() 129
lock: kmem: #test-and-set 0 #acquire() 22
lock: bcache_bucket: #test-and-set 0 #acquire() 6176
lock: bcache_bucket: #test-and-set 0 #acquire() 6186
lock: bcache_bucket: #test-and-set 0 #acquire() 6324
lock: bcache_bucket: #test-and-set 0 #acquire() 6320
lock: bcache_bucket: #test-and-set 0 #acquire() 6320
lock: bcache_bucket: #test-and-set 0 #acquire() 6310
lock: bcache_bucket: #test-and-set 0 #acquire() 4532
lock: bcache_bucket: #test-and-set 0 #acquire() 5300
lock: bcache_bucket: #test-and-set 0 #acquire() 2112
lock: bcache_bucket: #test-and-set 0 #acquire() 4118
lock: bcache_bucket: #test-and-set 0 #acquire() 2120
lock: bcache_bucket: #test-and-set 0 #acquire() 4122
lock: bcache_bucket: #test-and-set 0 #acquire() 4170
--- top 5 contended locks:
lock: virtio_disk: #test-and-set 1007951 #acquire() 1068
lock: proc: #test-and-set 56089 #acquire() 404689
lock: proc: #test-and-set 43046 #acquire() 384260
lock: proc: #test-and-set 33896 #acquire() 384248
lock: proc: #test-and-set 32820 #acquire() 384266
tot= 0
test0: OK
start test1
test1 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;刚开始做 Buffer cache 时，思路就是将哈希表集成在 &lt;code&gt;bcache&lt;/code&gt; 中，并在 &lt;code&gt;bget()&lt;/code&gt; 中遍历哈希表来获取 LRU &lt;code&gt;buf&lt;/code&gt;。这样做不仅复杂度提高，还经常出现让人摸不得头脑的死锁和 bug，从中午写到半夜也没有通过全部测试。看了博主&lt;a href=&quot;https://www.cnblogs.com/weijunji/p/xv6-study-12.html&quot;&gt;星见遥&lt;/a&gt;的实现后感觉非常巧妙，邃借鉴并在此说明。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab thread</title><link>https://siolin.me/blog/cs/6_thread</link><guid isPermaLink="true">https://siolin.me/blog/cs/6_thread</guid><description>实现线程切换，使用多线程加速程序运行，并实现一个屏障(barrier)。</description><pubDate>Sat, 09 Aug 2025 14:25:15 GMT</pubDate><content:encoded>&lt;h2&gt;Uthread: switching between threads&lt;/h2&gt;
&lt;p&gt;目的是实现线程的创建和切换。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;uthread_switch.S&lt;/code&gt;中保存和恢复上下文（仿照 &lt;code&gt;swtch.S&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;thread_switch:

/* YOUR CODE HERE */
	sd ra, 0(a0)
	sd sp, 8(a0)
	sd s0, 16(a0)
	sd s1, 24(a0)
	sd s2, 32(a0)
	sd s3, 40(a0)
	sd s4, 48(a0)
	sd s5, 56(a0)	
	sd s6, 64(a0)	
	sd s7, 72(a0)
	sd s8, 80(a0)
	sd s9, 88(a0)
	sd s10, 96(a0)
	sd s11, 104(a0)
	
	ld ra, 0(a1)
	ld sp, 8(a1)
	ld s0, 16(a1)
	ld s1, 24(a1)
	ld s2, 32(a1)
	ld s3, 40(a1)
	ld s4, 48(a1)
	ld s5, 56(a1)
	ld s6, 64(a1)
	ld s7, 72(a1)
	ld s8, 80(a1)
	ld s9, 88(a1)
	ld s10, 96(a1)
	ld s11, 104(a1)
	
	ret /* return to ra */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;uthread.c&lt;/code&gt; 的 &lt;code&gt;struct thread&lt;/code&gt; 中添加 &lt;code&gt;struct context&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct thread {
	char stack[STACK_SIZE]; /* the thread&apos;s stack */
	int state; /* FREE, RUNNING, RUNNABLE */
	
	struct context context;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;thread_create()&lt;/code&gt; 中初始化线程的栈和返回地址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void thread_create(void (*func)()) {
	struct thread *t;
	
	for (t = all_thread; t &amp;#x3C; all_thread + MAX_THREAD; t++) {
		if (t-&gt;state == FREE)
			break;
	}
	
	t-&gt;state = RUNNABLE;
	// YOUR CODE HERE
	t-&gt;context.ra = (uint64) func;
	t-&gt;context.sp = (uint64) t-&gt;stack + STACK_SIZE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里实际上是在该进程的内存空间（xv6 中一个进程中运行一个线程）中&lt;strong&gt;显式声明了一块内存区域作为该线程的栈&lt;/strong&gt;。在 &lt;code&gt;thread_create()&lt;/code&gt; 中进行初始化后，之后运行 &lt;code&gt;thread_switch()&lt;/code&gt; 会恢复上下文，从而达到在指定栈运行线程函数的效果。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;thread_schedule&lt;/code&gt; 中调用 &lt;code&gt;thread_switch()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;thread_switch((uint64) &amp;#x26;t-&gt;context, (uint64) &amp;#x26;next_thread-&gt;context);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using threads&lt;/h2&gt;
&lt;p&gt;目的是使用锁来解决 &lt;code&gt;put&lt;/code&gt; 中存在的竞争条件。&lt;/p&gt;
&lt;p&gt;如果两个线程同时 &lt;code&gt;put()&lt;/code&gt; 同一个桶，那么可能会出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程 A 检查 &lt;code&gt;table[i]&lt;/code&gt;，发现 &lt;code&gt;key&lt;/code&gt; 不存在，准备插入&lt;/li&gt;
&lt;li&gt;线程 B 检查 &lt;code&gt;table[i]&lt;/code&gt;，发现 &lt;code&gt;key&lt;/code&gt; 不存在，也准备插入&lt;/li&gt;
&lt;li&gt;线程 A 执行 &lt;code&gt;insert()&lt;/code&gt; ，新节点被插入到桶链表头部&lt;/li&gt;
&lt;li&gt;线程 B 执行 &lt;code&gt;insert()&lt;/code&gt;，如果此时线程 A 还未更新链表头，那么线程 A 的节点将被线程 B 覆盖
这导致对应的 key 并没有被插入，被 &lt;code&gt;get()&lt;/code&gt; 归为 &lt;code&gt;missing&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么为什么不可能是 &lt;code&gt;put()&lt;/code&gt; 和 &lt;code&gt;get()&lt;/code&gt; 发生竞争条件？
因为在 &lt;code&gt;main()&lt;/code&gt; 中 &lt;code&gt;put()&lt;/code&gt; 和 &lt;code&gt;get()&lt;/code&gt; 的执行是&lt;strong&gt;严格分离的两个阶段&lt;/strong&gt;，只有在执行完 &lt;code&gt;put()&lt;/code&gt; 后才会执行 &lt;code&gt;get()&lt;/code&gt;，因此这两个函数不会发生竞争条件。&lt;/p&gt;
&lt;p&gt;通过对 &lt;code&gt;put()&lt;/code&gt; 加锁来实现其原子性：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_t locks[NBUCKET]; // 为每个桶增加锁

// 在 main() 中初始化锁
for (int i = 0; i &amp;#x3C; NBUCKET; ++i) {
	pthread_mutex_init(&amp;#x26;locks[i], NULL);
}

// 在 put() 中使用锁
static void put(int key, int value) {
	int i = key % NBUCKET;
	
	// is the key already present?
	struct entry* e = 0;
	pthread_mutex_lock(&amp;#x26;locks[i]);
	for (e = table[i]; e != 0; e = e-&gt;next) {
		if (e-&gt;key == key)
			break;
	}
	if (e) {
		// update the existing key.
		e-&gt;value = value;
	} else {
		// the new is new.
		insert(key, value, &amp;#x26;table[i], table[i]);
	}
	pthread_mutex_unlock(&amp;#x26;locks[i]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;检查和插入之间同样存在竞争，因此临界区必须将其全部覆盖&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于 &lt;code&gt;main()&lt;/code&gt; 中的顺序执行，因此不需要为 &lt;code&gt;get()&lt;/code&gt; 加锁。&lt;/p&gt;
&lt;h2&gt;Barrier&lt;/h2&gt;
&lt;p&gt;目的是实现 &lt;code&gt;barrier()&lt;/code&gt;，用于同步所有线程。&lt;/p&gt;
&lt;p&gt;感觉目的很明确，逻辑也比前两题简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static void barrier() {
	// YOUR CODE HERE
	//
	// Block until all threads have called barrier() and
	// then increment bstate.round.
	//
	pthread_mutex_lock(&amp;#x26;bstate.barrier_mutex);
	bstate.nthread++;
	if (bstate.nthread == nthread) {
		bstate.round++;
		bstate.nthread = 0;
		pthread_cond_broadcast(&amp;#x26;bstate.barrier_cond);
	} else {
		pthread_cond_wait(&amp;#x26;bstate.barrier_cond, &amp;#x26;bstate.barrier_mutex);
	}
	pthread_mutex_unlock(&amp;#x26;bstate.barrier_mutex);
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab cow</title><link>https://siolin.me/blog/cs/5_cow</link><guid isPermaLink="true">https://siolin.me/blog/cs/5_cow</guid><description>通过Page Fault 来实现 Copy-On-Write Fork机制。</description><pubDate>Thu, 31 Jul 2025 14:18:43 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;参考博客：&lt;a href=&quot;https://fanxiao.tech/posts/2021-03-02-mit-6s081-notes/#85-lab-6-copy-on-write-fork&quot;&gt;Xiao Fan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://staff.ustc.edu.cn/~llxx/cod/reference_books/RISC-V-Reader-Chinese-v2p12017.pdf&quot;&gt;RISC-V手册&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Copy-on-Write Fork 介绍&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;基本流程&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始状态（&lt;code&gt;fork()&lt;/code&gt; 刚完成）
&lt;ul&gt;
&lt;li&gt;父进程与子进程共享所有的物理页，但它们的 PTE 标记为只读（&lt;code&gt;PTE_W=0&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;任何写入尝试都会触发存储页错误&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;写入触发存储页错误（trap handler 介入）
&lt;ul&gt;
&lt;li&gt;检查该页的引用计数：
&lt;ul&gt;
&lt;li&gt;如果&lt;strong&gt;仅当前进程引用该页&lt;/strong&gt;（无其他共享者），则直接恢复 &lt;code&gt;PTE_W&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;/li&gt;
&lt;li&gt;修改当前进程的 PTE，使其指向新页，并设置 &lt;code&gt;PTE_W=1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;恢复执行：重新执行触发页错误的执行，此时写入会成功&lt;/li&gt;
&lt;/ul&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;ul&gt;
&lt;li&gt;每个物理页维护一个&lt;strong&gt;引用计数&lt;/strong&gt;，记录有多少进程的 PTE 指向它&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt; 时，所有共享页的引用计数&lt;code&gt;+1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;COW 复制后：
&lt;ul&gt;
&lt;li&gt;原页的引用计数&lt;code&gt;-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新页的引用计数&lt;code&gt;=1&lt;/code&gt;（&lt;strong&gt;仅当前进程使用&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;释放内存时：只有当引用计数 &lt;code&gt;=0&lt;/code&gt; 时，才真正释放物理页&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储页错误的优化
&lt;ul&gt;
&lt;li&gt;如果仅当前进程引用该页（引用计数&lt;code&gt;=1&lt;/code&gt;），则无需复制，直接恢复 &lt;code&gt;PTE_W=1&lt;/code&gt; 即可
&lt;ul&gt;
&lt;li&gt;例如：父进程 &lt;code&gt;fork()&lt;/code&gt; 后，子进程 &lt;code&gt;exec()&lt;/code&gt; 丢弃了大部分内存，此时父进程写入自己的内存时可能无需复制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;uvmcopy()&lt;/h2&gt;
&lt;p&gt;在 kernel/vm.c 的 &lt;code&gt;uvmcopy()&lt;/code&gt;函数中，需要进行以下修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将父进程的物理页映射到子进程，而不是分配新页面；&lt;/li&gt;
&lt;li&gt;清除父进程和子进程的 &lt;code&gt;PTE_W&lt;/code&gt; 位&lt;/li&gt;
&lt;li&gt;设置新添加的 &lt;code&gt;PTE_COW&lt;/code&gt;位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中，在 PTE 的 RSW 处可以设置为我们的 &lt;code&gt;PTE_COW&lt;/code&gt; 位，以表明该物理页是COW Fork机制。&lt;/p&gt;
&lt;p&gt;在 riscv.h 中添加 &lt;code&gt;PTE_COW&lt;/code&gt; 位：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define PTE_COW   (1L &amp;#x3C;&amp;#x3C; 8)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;uvmcopy()&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;

  for(i = 0; i &amp;#x3C; sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      panic(&quot;uvmcopy: pte should exist&quot;);
    if((*pte &amp;#x26; PTE_V) == 0)
      panic(&quot;uvmcopy: page not present&quot;);

    pa = PTE2PA(*pte);
    *pte = (*pte &amp;#x26; ~PTE_W) | PTE_COW; // 设置父页flags
    if(mappages(new, i, PGSIZE, (uint64)pa, PTE_FLAGS(*pte)) != 0){ // 设置映射和子页flags
      goto err;
    }
    refcnt_add(pa); // 增加引用计数
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;usertrap()&lt;/h2&gt;
&lt;p&gt;接下来在 kernel/trap.c 的 &lt;code&gt;usertrap()&lt;/code&gt; 函数中添加对存储页错误的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;} else if (r_scause() == 15) { // 存储页错误
    uint64 va = r_stval();
    
    if (va &gt;= MAXVA || (va &amp;#x3C;= PGROUNDDOWN(p-&gt;trapframe-&gt;sp) &amp;#x26;&amp;#x26; va &gt;= PGROUNDDOWN(p-&gt;trapframe-&gt;sp) - PGSIZE))
      p-&gt;killed = 1;
    else if (refcnt_new(va, p-&gt;pagetable) == -1) // 空闲内存不足，终止进程
      p-&gt;killed = 1;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里需要检查虚拟地址是否越界，或者处于 guard page 当中，否则 usertests 无法通过。&lt;/p&gt;
&lt;h2&gt;kalloc.c&lt;/h2&gt;
&lt;p&gt;按照 COW 的逻辑，我们需要维护每一个物理页的引用计数 &lt;code&gt;refcnt&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;kalloc.c&lt;/code&gt; 中声明数据结构和辅助函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct {
  struct spinlock lock;
  uint cnt[(PHYSTOP - KERNBASE) / PGSIZE]; // 引用计数数组
} refcnt;

#define PA2IDX(pa) (((uint64)pa - KERNBASE) / PGSIZE) // 索引计算逻辑

// add cnt
void refcnt_add(uint64 pa) {
  acquire(&amp;#x26;refcnt.lock);
  refcnt.cnt[PA2IDX(pa)]++;
  release(&amp;#x26;refcnt.lock);
}

// set cnt
void refcnt_setter(uint64 pa, uint n) {
  refcnt.cnt[PA2IDX(pa)] = n;
}

// get cnt
uint refcnt_getter(uint64 pa) {
  return refcnt.cnt[PA2IDX(pa)];
}

// kalloc() without lock
void *
kalloc_nolock(void)
{
  struct run *r;

  acquire(&amp;#x26;kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r-&gt;next;
  release(&amp;#x26;kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  
  if (r)
    refcnt.cnt[PA2IDX((uint64)r)]++;
  return (void*)r;
}

// create new physical page
int refcnt_new(uint64 va, pagetable_t pagetable) {
  pte_t *pte;
  uint64 pa;
  uint flags, cnt;

  va = PGROUNDDOWN(va);
  pte = walk(pagetable, va, 0);
  pa = PTE2PA(*pte);
  flags = PTE_FLAGS(*pte);

  if (!(flags &amp;#x26; PTE_COW)) // 非COW页，不予处理
    return -2;

  acquire(&amp;#x26;refcnt.lock);
  cnt = refcnt_getter(pa);
  if (cnt &gt; 1) { // 多页则需要创建新页
    char *mem = kalloc_nolock();
    if (mem == 0) // 空闲内存不足
      goto bad;
    memmove(mem, (char *)pa, PGSIZE); // 复制旧页到新页
    uvmunmap(pagetable, va, 1, 0); // 需要旧页原有的映射
    if (mappages(pagetable, va, PGSIZE, (uint64)mem, (flags &amp;#x26; ~PTE_COW) | PTE_W) != 0) { // 设置新映射
      kfree(mem);
      goto bad;
    }
    refcnt_setter(pa, cnt - 1); // 旧页引用计数-1
  } else { // 单页直接写入
    *pte = (*pte &amp;#x26; ~PTE_COW) | PTE_W;
  }
  release(&amp;#x26;refcnt.lock);
  return 0;

  bad:
    release(&amp;#x26;refcnt.lock);
    return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;refcnt_new()&lt;/code&gt; 中只能使用 &lt;code&gt;kalloc_nolock()&lt;/code&gt;，因为其已经声明 &lt;code&gt;acquire(&amp;#x26;refcnt.lock)&lt;/code&gt;，如果直接使用 &lt;code&gt;kalloc()&lt;/code&gt;，里面会再一次声明，便会触发 &lt;code&gt;panic(&quot;acquire&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kinit()&lt;/code&gt; 中初始化 &lt;code&gt;refcnt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
kinit()
{
  initlock(&amp;#x26;kmem.lock, &quot;kmem&quot;);
  initlock(&amp;#x26;refcnt.lock, &quot;refcnt&quot;);
  memset(refcnt.cnt, 0, sizeof(refcnt.cnt)); // 数组初始化为0
  freerange(end, (void*)PHYSTOP);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;kalloc()&lt;/code&gt; 初始化引用计数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *kalloc(void)
{
  struct run *r;

  acquire(&amp;#x26;kmem.lock);
  r = kmem.freelist;
  if(r) {
    kmem.freelist = r-&gt;next;
    acquire(&amp;#x26;refcount.lock);
    refcount.count[PA2IDX((uint64) r)] = 1; // 初始设置为1
    release(&amp;#x26;refcount.lock);
  }
  release(&amp;#x26;kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;kfree()&lt;/code&gt; 的逻辑，只有引用计数为 0 时才释放物理内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
kfree(void *pa)
{
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa &amp;#x3C; end || (uint64)pa &gt;= PHYSTOP)
    panic(&quot;kfree&quot;);

  acquire(&amp;#x26;refcnt.lock);
  int cnt = refcnt_getter((uint64)pa);
  if (cnt &gt; 1) { // 存在多个引用，不释放内存
    refcnt_setter((uint64)pa, cnt - 1);
    release(&amp;#x26;refcnt.lock);
    return;
  }

  // 清零计数
  refcnt_setter((uint64)pa, 0);
  release(&amp;#x26;refcnt.lock);

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&amp;#x26;kmem.lock);
  r-&gt;next = kmem.freelist;
  kmem.freelist = r;
  release(&amp;#x26;kmem.lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新 &lt;code&gt;kalloc()&lt;/code&gt; 函数，使其分配内存时初始化计数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void *kalloc(void)
{
  struct run *r;

  acquire(&amp;#x26;kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r-&gt;next;
  release(&amp;#x26;kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  
  if (r)
    refcnt_add((uint64)r);
  return (void*)r;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;copyout ()&lt;/h2&gt;
&lt;p&gt;最后需要修改 &lt;code&gt;copyout()&lt;/code&gt;，使其当目标页为 COW 页时，分配一个新的物理页：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;
  pte_t *pte;

  while(len &gt; 0){
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;

    pte = walk(pagetable, va0, 0);
    if (*pte &amp;#x26; PTE_COW) {
      refcnt_new(va0, pagetable);
      pa0 = PTE2PA(*pte); // 需要更新pa0，否则还是写入原页
    }

    n = PGSIZE - (dstva - va0);
    if(n &gt; len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab traps</title><link>https://siolin.me/blog/cs/4_traps</link><guid isPermaLink="true">https://siolin.me/blog/cs/4_traps</guid><description>实现系统调用和中断处理，分析 trapframe 和上下文切换。</description><pubDate>Fri, 25 Jul 2025 18:52:32 GMT</pubDate><content:encoded>&lt;h2&gt;Backtrace&lt;/h2&gt;
&lt;p&gt;题目要求：编译器会在每个栈帧中放置一个帧指针，该指针保存着调用者帧指针的地址。您的 backtrace 应利用这些帧指针遍历堆栈，并打印每个栈帧中保存的返回地址。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;backtrace()&lt;/code&gt; 原型添加到 &lt;code&gt;defs.h&lt;/code&gt; 中。
在 &lt;code&gt;kernel/riscv.h&lt;/code&gt; 中添加函数，以获取帧指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static inline uint64
r_fp()
{
  uint64 x;
  asm volatile(&quot;mv %0, s0&quot; : &quot;=r&quot; (x) );
  return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;kernel/printf.c&lt;/code&gt; 中添加 &lt;code&gt;backtrace&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
backtrace(void) {
  printf(&quot;backtrace:\n&quot;);
  uint64 fp = r_fp();
  uint64 top = PGROUNDUP(fp);

  while (fp &amp;#x3C; top) {
    uint64 ra = *(uint64*)(fp - 8);
    printf(&quot;%p\n&quot;, &amp;#x26;ra);
    fp = *(uint64*)(fp - 16);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;由于是从低地址向高地址遍历栈帧，因此只需检查 &lt;code&gt;PGROUNDUP(fp)&lt;/code&gt; 边界即可；&lt;/li&gt;
&lt;li&gt;需要注意返回地址位于 &lt;code&gt;*(fp-8)&lt;/code&gt; 处，帧指针位于 &lt;code&gt;*(fp-16)&lt;/code&gt; 处。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;printf.c&lt;/code&gt; 的 &lt;code&gt;panic()&lt;/code&gt; 中添加 &lt;code&gt;backtrace()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void
panic(char *s)
{
  pr.locking = 0;
  printf(&quot;panic: &quot;);
  printf(s);
  printf(&quot;\n&quot;);
  backtrace();
  panicked = 1; // freeze uart output from other CPUs
  for(;;)
    ;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alarm&lt;/h2&gt;
&lt;p&gt;功能概述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sigalarm(n, fn)&lt;/code&gt; ：
&lt;ul&gt;
&lt;li&gt;设置每隔 &lt;code&gt;n&lt;/code&gt; 个 CPU 时间 ticks 调用一次 &lt;code&gt;fn&lt;/code&gt; 函数&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;fn&lt;/code&gt; 返回后，程序从被中断的地方继续执行&lt;/li&gt;
&lt;li&gt;如果调用 &lt;code&gt;sigalarm(0, 0)&lt;/code&gt;，则停止警报调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sigreturn()&lt;/code&gt; ：
&lt;ul&gt;
&lt;li&gt;由警报处理函数调用，用于恢复被中断的上下文&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先需要理解整个系统的调用流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;alarmtest&lt;/code&gt; 中初始化对 &lt;code&gt;sigalarm(2, periodic)&lt;/code&gt; 的调用，内核会在 &lt;code&gt;proc&lt;/code&gt; 中记录这些参数；&lt;/li&gt;
&lt;li&gt;每个时钟中断(tick)发生时：
&lt;ul&gt;
&lt;li&gt;硬件触发中断-&gt; 执行 &lt;code&gt;usertrap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;根据条件 &lt;code&gt;which_dev == 2&lt;/code&gt; 判断时钟中断&lt;/li&gt;
&lt;li&gt;只有 &lt;code&gt;ticks&lt;/code&gt; 计数器等于初始化设置的 &lt;code&gt;interval&lt;/code&gt; 时才调用 &lt;code&gt;periodic&lt;/code&gt; 处理函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由于我们将 &lt;code&gt;usertrap&lt;/code&gt; 的下一步变成了执行 &lt;code&gt;periodic&lt;/code&gt; 处理函数而不是 &lt;code&gt;usertrapret&lt;/code&gt;，因此需要在 &lt;code&gt;periodic&lt;/code&gt; 中调用 &lt;code&gt;sigreturn()&lt;/code&gt; 函数，从而进入恢复阶段&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;sigreturn()&lt;/code&gt; 中我们需要将保存的上下文恢复和重置一些状态&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;一般方案&lt;/h3&gt;
&lt;p&gt;初始设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;MAKEFILE&lt;/code&gt; 的添加 &lt;code&gt;alarmtest.c&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;user/user.h&lt;/code&gt; 中添加函数声明：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更新 &lt;code&gt;user/usys.pl&lt;/code&gt;、&lt;code&gt;kernel/syscall.h&lt;/code&gt; 和 &lt;code&gt;kernel/syscall.c&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;proc&lt;/code&gt; 添加变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt; int interval;             // 警报间隔
 void (*handler)();        // 处理函数指针，无返回值和参数传入
 int ticks;                // 距离上次警报的ticks数
 int in_handler;           // 是否在处理函数中
 struct trapframe *alarm_trapframe; // 保存原始的trapframe
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;in_handler&lt;/code&gt; 防止处理程序被重复调用&lt;/li&gt;
&lt;li&gt;这里使用 &lt;code&gt;alarm_trapframe&lt;/code&gt; 来避免了冗长的手动保存寄存器，保持代码整洁且符合原有的 xv6 风格，但是缺点是增加了内存占用以及性能开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;proc.c&lt;/code&gt; 中添加对 &lt;code&gt;alarm_trapframe&lt;/code&gt; 的分配和释放：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static struct proc*
allocproc(void)
{
	... // 其它代码
	
	// Allocate a trapframe page.
	if(((p-&gt;trapframe = (struct trapframe *)kalloc()) == 0) 
  || (p-&gt;alarm_trapframe = (struct trapframe *)kalloc()) == 0) {
    freeproc(p);
    release(&amp;#x26;p-&gt;lock);
    return 0;
  }
  
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;usertrap&lt;/code&gt; 中添加对时钟中断的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;} else if ((which_dev = devintr()) != 0) {
    // ok
    if (which_dev == 2 &amp;#x26;&amp;#x26; p-&gt;in_handler == 0) {
      p-&gt;ticks++;
      if ((p-&gt;ticks == p-&gt;interval) &amp;#x26;&amp;#x26; (p-&gt;interval != 0)) {
        p-&gt;in_handler = 1; // 设置为在处理函数中
        p-&gt;ticks = 0;      // 重置ticks计数
        p-&gt;alarm_trapframe = memmove(p-&gt;alarm_trapframe, p-&gt;trapframe, sizeof(*(p-&gt;trapframe)));
        p-&gt;trapframe-&gt;epc = (uint64)p-&gt;handler;
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;handler&lt;/code&gt; 写入 &lt;code&gt;p-&gt;trapframe-&gt;epc&lt;/code&gt; ，使得从 &lt;code&gt;usertrap&lt;/code&gt; 返回时开始执行 &lt;code&gt;handler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将整个 &lt;code&gt;trapframe&lt;/code&gt; 保存至 &lt;code&gt;alarm_trapframe&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;kernel/sysproc.c&lt;/code&gt; 中添加 &lt;code&gt;sigalarm&lt;/code&gt; 和 &lt;code&gt;sigreturn&lt;/code&gt; 的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64 sys_sigalarm(void) {
  int ticks;
  uint64 handler;
  if (argint(0, &amp;#x26;ticks) &amp;#x3C; 0 || argaddr(1, &amp;#x26;handler) &amp;#x3C; 0)
    return -1;
  struct proc *p = myproc();
  if (ticks &amp;#x3C; 0)
    return -1;
  p-&gt;interval = ticks;              // 设置警报间隔
  p-&gt;handler = (void (*)())handler; // 设置警报处理函数
  return 0;
}

uint64 sys_sigretrun(void) {
  struct proc *p = myproc();
  memmove(p-&gt;trapframe, p-&gt;alarm_trapframe, sizeof(*p-&gt;alarm_trapframe));
  p-&gt;in_handler = 0; // 重置为不在处理函数中
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;优化&lt;/h3&gt;
&lt;p&gt;由于处理函数 &lt;code&gt;periodic&lt;/code&gt; 的逻辑非常简答，不会修改其它的用户寄存器，因此不需要保存全部的用户寄存器，而是仅保存几个重要的寄存器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int interval;             // 警报间隔
void (*handler)();        // 处理函数指针，无返回值和参数传入
int ticks;                // 距离上次警报的ticks数
int in_handler;           // 是否在处理函数中
uint64 alarm_epc;         // 保存用户程序的epc
uint64 alarm_sp;          // 保存sp
uint64 alarm_ra;          // 返回地址
uint64 alarm_a0;          // 参数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;usertrap&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;} else if ((which_dev = devintr()) != 0) {
    // ok
    if (which_dev == 2 &amp;#x26;&amp;#x26; p-&gt;in_handler == 0) {
      p-&gt;ticks++;
      if ((p-&gt;ticks == p-&gt;interval) &amp;#x26;&amp;#x26; (p-&gt;interval != 0)) {
        p-&gt;in_handler = 1; // 设置为在处理函数中
        p-&gt;ticks = 0;      // 重置ticks计数
        p-&gt;alarm_epc = p-&gt;trapframe-&gt;epc;
        p-&gt;alarm_sp = p-&gt;trapframe-&gt;sp;
        p-&gt;alarm_a0 = p-&gt;trapframe-&gt;a0;
        p-&gt;alarm_ra = p-&gt;trapframe-&gt;ra;
        p-&gt;trapframe-&gt;epc = (uint64)p-&gt;handler;
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sysproc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64 sys_sigalarm(void) {
  int ticks;
  uint64 handler;
  if (argint(0, &amp;#x26;ticks) &amp;#x3C; 0 || argaddr(1, &amp;#x26;handler) &amp;#x3C; 0)
    return -1;
  struct proc *p = myproc();
  if (ticks &amp;#x3C; 0)
    return -1;
  p-&gt;interval = ticks;              // 设置警报间隔
  p-&gt;handler = (void (*)())handler; // 设置警报处理函数
  return 0;
}

uint64 sys_sigretrun(void) {
  struct proc *p = myproc();
  p-&gt;trapframe-&gt;epc = p-&gt;alarm_epc;
  p-&gt;trapframe-&gt;sp = p-&gt;alarm_sp;
  p-&gt;trapframe-&gt;ra = p-&gt;alarm_ra;
  p-&gt;trapframe-&gt;a0 = p-&gt;alarm_a0;
  p-&gt;in_handler = 0; // 重置为不在处理函数中
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://fanxiao.tech/posts/2021-03-02-mit-6s081-notes/#55-lab-4-traps&quot;&gt;Xiao Fan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab pgtbl</title><link>https://siolin.me/blog/cs/3_pgtbl</link><guid isPermaLink="true">https://siolin.me/blog/cs/3_pgtbl</guid><description>探索 Xv6 页表机制，修改页表映射并打印信息。</description><pubDate>Fri, 25 Jul 2025 18:48:14 GMT</pubDate><content:encoded>&lt;h2&gt;Speed up system calls&lt;/h2&gt;
&lt;p&gt;需要在创建进程是在 USYSCALL 处映射一个只读页面，在改位置存储一个 &lt;code&gt;struct usyscall&lt;/code&gt;，并初始化为当前进程的 PID。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define USYSCALL (TRAPFRAME - PGSIZE)

struct usyscall {
  int pid;  // Process ID
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先是需要在 &lt;code&gt;proc.c&lt;/code&gt; 的 &lt;code&gt;struct proc&lt;/code&gt; 中添加 &lt;code&gt;usyscall&lt;/code&gt; 变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct usyscall *usyscall;   // Usyscall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;allocproc()&lt;/code&gt; 中为其分配物理内存，并初始化数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Allocate a usyscall page.
  if ((p-&gt;usyscall = (struct usyscall *)kalloc()) == 0) {
    freeproc(p);
    release(&amp;#x26;p-&gt;lock);
    return 0;
  }
  p-&gt;usyscall-&gt;pid = p-&gt;pid;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;proc_pagetable()&lt;/code&gt; 中调用 &lt;code&gt;mappages()&lt;/code&gt; 插入映射：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// map the usyscall at USYSCALL
  if (mappages(pagetable, USYSCALL, PGSIZE, 
              (uint64)(p-&gt;usyscall), PTE_R | PTE_U) &amp;#x3C; 0) {
    uvmunmap(pagetable, USYSCALL, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注意这里需要添加 &lt;code&gt;PTE_U&lt;/code&gt;，使得用户能够访问&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 &lt;code&gt;freeproc&lt;/code&gt; 中释放物理页：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (p-&gt;usyscall)
    kfree((void*)p-&gt;usyscall);
  p-&gt;usyscall = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;proc_freepagetable&lt;/code&gt; 中解除页表映射：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmfree(pagetable, sz);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Print a page table&lt;/h2&gt;
&lt;p&gt;定义一个 &lt;code&gt;vmprint()&lt;/code&gt; 的函数，接受一个 &lt;code&gt;pagetable_t&lt;/code&gt; 参数，并以指定格式打印该页表。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;exec.c&lt;/code&gt; 添加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (p-&gt;pid == 1)
   vmprint(p-&gt;pagetable);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;kernel/def.h&lt;/code&gt; 中添加 &lt;code&gt;vmprint()&lt;/code&gt; 的原型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void            vmprint(pagetable_t);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用递归来实现，但是需要根据是否是最底层页表来判断是否继续向下递归，这里就需要一层判断：
如果 PTE 没有 R/W/X 权限，说明它是一个中间页表项（指向下一层页表）；反之则是最底层页表（指向实际的物理页）。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;vm.c&lt;/code&gt; 中实现 &lt;code&gt;vmprint()&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void vmprint_helper(pagetable_t pagetable, int level) {
  for (int i = 0; i &amp;#x3C; 512; ++i) {
    pte_t pte = pagetable[i];
    if ((pte &amp;#x26; PTE_V) &amp;#x26;&amp;#x26; (pte &amp;#x26; (PTE_R | PTE_W | PTE_X)) == 0) { // 中间页表项
      uint64 child = PTE2PA(pte); // 下一级页表的物理地址
      for (int j = 0; j &amp;#x3C;= level; ++j) { // 根据当前level打印缩进
        printf(&quot;..&quot;);
        if (j + 1 &amp;#x3C;= level)
          printf(&quot; &quot;);
      }
      printf(&quot;%d: pte %p pa %p\n&quot;, i, pte, child);
      vmprint_helper((pagetable_t)child, level + 1); // 递归处理下一级页表
    } else if (pte &amp;#x26; PTE_V) { // 指向实际的物理页（最底层页表）
      uint64 child = PTE2PA(pte);
      printf(&quot;.. .. ..%d: pte %p pa %p\n&quot;, i, pte, child);
    }
  }
}

void vmprint(pagetable_t pagetable) {
  printf(&quot;page table %p\n&quot;, pagetable);
  vmprint_helper(pagetable, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PTE2PA&lt;/code&gt; 在 &lt;code&gt;riscv.h&lt;/code&gt; 中定义，用于物理地址（PA）和页表项（PTE）之间的转换。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) &gt;&gt; 12) &amp;#x3C;&amp;#x3C; 10)
#define PTE2PA(pte) (((pte) &gt;&gt; 10) &amp;#x3C;&amp;#x3C; 12)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Detecting which pages have been accessed&lt;/h2&gt;
&lt;p&gt;首先需要在 kernel/riscv. h 中定义 PTE_A。根据 RISC-V 手册，PTE_A 是第 6 位：
![[Pasted image 20250722062258.png]]&lt;/p&gt;
&lt;p&gt;因此代码为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define PTE_A (1L &amp;#x3C;&amp;#x3C; 6) // 访问位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次是在 kernel/sysproc. c 中实现 &lt;code&gt;sys_pgaccess()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;uint64 sys_pageccess(void) {
  uint64 start; // 起始虚拟地址
  int len;   // 页面数量
  uint64 mask;// 位掩码缓冲区地址

  // 获取参数
  if (argaddr(0, &amp;#x26;start) &amp;#x3C; 0 || argint(1, &amp;#x26;len) &amp;#x3C; 0 
    || argaddr(2, &amp;#x26;mask) &amp;#x3C; 0)
    return -1;
  
  // 对应掩码缓冲区长度
  if (len &amp;#x3C; 1 || len &gt; 64)
    return -1;

  struct proc *p = myproc();
  pagetable_t pagetable = p-&gt;pagetable;

  // 在内核中创建临时缓冲区存储结果
  uint64 abits = 0;

  for (int i = 0; i &amp;#x3C; len; ++i) {
    uint64 va = start + i * PGSIZE;
    pte_t *pte = walk(pagetable, va, 0); // 获取对应的PTE
    
    if (pte == 0) continue; // PTE不存在

    if (*pte &amp;#x26; PTE_A) {
      abits |= (1 &amp;#x3C;&amp;#x3C; i);
      *pte &amp;#x26;= ~PTE_A; // 清除
    }
  }
  
  // 复制到用户空间
  if (copyout(pagetable, mask, (char *)&amp;#x26;abits, sizeof(abits)) &amp;#x3C; 0)
    return -1;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab syscall</title><link>https://siolin.me/blog/cs/2_syscall</link><guid isPermaLink="true">https://siolin.me/blog/cs/2_syscall</guid><description>尝试在 Xv6 中添加系统调用，理解用户态与内核态的交互。</description><pubDate>Mon, 21 Jul 2025 17:26:24 GMT</pubDate><content:encoded>&lt;p&gt;系统调用流程概述&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户程序调用用户空间包装函数（位于 &lt;code&gt;user.h&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;包装函数通过汇编指令触发软中断&lt;/li&gt;
&lt;li&gt;内核终端处理程序根据系统调用号（位于 &lt;code&gt;syscall.h&lt;/code&gt;）分派到正确的系统调用实现（位于 &lt;code&gt;syscall.c&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;结果返回给用户程序&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中，&lt;code&gt;usys.pl&lt;/code&gt; 为每个系统调用生成统一的汇编代码模板，处理系统调用号和参数传递，触发软中断进入内核态。&lt;/p&gt;
&lt;h2&gt;Sysetm call tracing&lt;/h2&gt;
&lt;p&gt;要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新增 &lt;code&gt;trace&lt;/code&gt; 系统调用
&lt;ul&gt;
&lt;li&gt;接受一个整数参数 &lt;code&gt;mask&lt;/code&gt;，二进制位用于指定要追踪的系统调用。&lt;/li&gt;
&lt;li&gt;例如：&lt;code&gt;trace(1 &amp;#x3C;&amp;#x3C; SYS_fork)&lt;/code&gt; 表示追踪 &lt;code&gt;fork&lt;/code&gt; 系统调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;修改内核以输出追踪信息
&lt;ul&gt;
&lt;li&gt;当被追踪的系统调用即将返回时，内核需打印一行信息，包括：
&lt;ul&gt;
&lt;li&gt;进程 ID&lt;/li&gt;
&lt;li&gt;系统调用名称&lt;/li&gt;
&lt;li&gt;返回值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;仅当系统调用编号在 &lt;code&gt;mask&lt;/code&gt; 中对应的位被设置时，才输出信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;追踪的继承性
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trace&lt;/code&gt; 调用后，当前进程及后续通过 &lt;code&gt;fork&lt;/code&gt; 创建的子进程均启用追踪，但不得影响其它无关进程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先是添加系统调用的声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// user.h
int trace(int);

// usys.S
entry(&quot;trace&quot;);

// syscall.h
#define SYS_trace  22
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其次是实现内核调用 &lt;code&gt;sys_trace&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 需要在proc.h为struct proc添加新变量
struct proc {
	...
	int trace_mask; // 进程要追踪的掩码
}

// 在sysproc.c中添加sys_trace()函数
// trace the system call from user space
uint64
sys_trace(void) {
  int mask;

  if (argint(0, &amp;#x26;mask) &amp;#x3C; 0) // 获取mask
    return -1;
  myproc()-&gt;trace_mask = mask;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;kernel/proc.c&lt;/code&gt; 中修改 &lt;code&gt;fork()&lt;/code&gt;，使得子进程能够继承父进程的跟踪掩码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int fork(void) {
	...
	np-&gt;trace_mask = p-&gt;trace_mask;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在 &lt;code&gt;kernel/syscall.c&lt;/code&gt; 中修改 &lt;code&gt;syscall()&lt;/code&gt; 函数，在系统调用执行完成后检查 &lt;code&gt;trace_mask&lt;/code&gt;，若当前系统调用编号被设置，则打印追踪信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void syscall(void)
{
  int num;
  struct proc *p = myproc();
  // 系统调用名称数组，用于索引
  char* syscall_name[22] = {&quot;fork&quot;, &quot;exit&quot;, &quot;wait&quot;, &quot;pipe&quot;, &quot;read&quot;, 
  &quot;kill&quot;, &quot;exec&quot;, &quot;fstat&quot;, &quot;chdir&quot;, &quot;dup&quot;, &quot;getpid&quot;, &quot;sbrk&quot;, &quot;sleep&quot;, 
  &quot;uptime&quot;, &quot;open&quot;, &quot;write&quot;, &quot;mknod&quot;, &quot;unlink&quot;, &quot;link&quot;, &quot;mkdir&quot;, &quot;close&quot;, 
  &quot;trace&quot;};

  num = p-&gt;trapframe-&gt;a7;
  if(num &gt; 0 &amp;#x26;&amp;#x26; num &amp;#x3C; NELEM(syscalls) &amp;#x26;&amp;#x26; syscalls[num]) {
    p-&gt;trapframe-&gt;a0 = syscalls[num]();
    if ((1 &amp;#x3C;&amp;#x3C; num) &amp;#x26; (p-&gt;trace_mask)) // 检查当前调用是否被追踪
      printf(&quot;%d: syscall %s -&gt; %d\n&quot;, p-&gt;pid, syscall_name[num - 1], p-&gt;trapframe-&gt;a0);
  } else {
    printf(&quot;%d %s: unknown sys call %d\n&quot;,
            p-&gt;pid, p-&gt;name, num);
    p-&gt;trapframe-&gt;a0 = -1;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sysinfo&lt;/h2&gt;
&lt;h3&gt;系统调用声明&lt;/h3&gt;
&lt;p&gt;要求：新增 &lt;code&gt;sysinfo&lt;/code&gt; 的系统调用，其参数为 &lt;code&gt;struct sysinfo*&lt;/code&gt;（声明在 &lt;code&gt;kernel/sysinfo.h&lt;/code&gt;），要求填充该结构体。&lt;/p&gt;
&lt;p&gt;按照之前的步骤添加声明，唯一不同的是在 &lt;code&gt;user/user.h&lt;/code&gt; 中需要声明 &lt;code&gt;struct sysinfo&lt;/code&gt; 的存在：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct sysinfo;
...
int sysinfo(struct sysinfo*);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;收集空闲内存量&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;kernel/kalloc.c&lt;/code&gt; 中存在以下声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于物理内存中被分成了页进行管理，这里实际上是用链表来存储空闲的内存页。其中 &lt;code&gt;freelist&lt;/code&gt; 为头节点，而 &lt;code&gt;struct run&lt;/code&gt; 定义了一个链表节点结构，这里实现为单链表。
于是我们可以据此计算出空闲空间的大小：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// get the free memory size of user space
uint64 freememSize(void) {
  struct run *r = kmem.freelist;
  uint64 i = 0; // 空闲页的数量
  while (r) {
    i++;
    r = r-&gt;next;
  }
  return i * PGSIZE;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;收集进程数量&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;struct sysinfo&lt;/code&gt; 中的 &lt;code&gt;nproc&lt;/code&gt; 设置为 &lt;code&gt;state&lt;/code&gt; 不为 UNUSED 的进程数量。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;kernel/proc.c&lt;/code&gt; 的头部声明了 &lt;code&gt;struct proc proc[NPROC]&lt;/code&gt;，这相当于是进程数组，因此我们只需要遍历数组即可：`&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// get the num of procs that aren&apos;t UNUSED.
uint64 nproc_active(void) {
  int i = 0;
  uint64 n = 0;
  while (i &amp;#x3C; NPROC) {
    if (proc[i].state != UNUSED) 
      n++;
    i++;
  }
  return n;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现 sysinfo&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;kernel/sysproc.c&lt;/code&gt; 中添加 &lt;code&gt;sys_sysinfo&lt;/code&gt; 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// collects information about the running system.
uint64
sys_sysinfo(void) {
  uint64 st; // 指向 struct sysinfo 的指针
  struct sysinfo sf;

  if (argaddr(0, &amp;#x26;st) &amp;#x3C; 0) // 获取用户空间的目标虚拟地址
    return -1;
  sf.freemem = freememSize();
  sf.nproc = nproc_active();
  if (copyout(myproc()-&gt;pagetable, st, (char *)&amp;#x26;sf, sizeof(sf)) &amp;#x3C; 0)
    return -1;
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之所以需要用到 &lt;code&gt;copyout&lt;/code&gt; ，是因为我们传递给 sysinfo 函数的参数是一个用户空间的指针，而我们的 sys_sysinfo 函数是内核函数，其运行在内核空间，在其内填充的 &lt;code&gt;struct sysinfo&lt;/code&gt; 也位于内核空间，用户空间的指针无法直接访问。而 &lt;code&gt;copyout&lt;/code&gt; 函数的作用就是&lt;strong&gt;将内核空间的数据复制到用户空间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;copyout&lt;/code&gt; 函数的用法（可以参考 &lt;code&gt;kernel/sysfile.c&lt;/code&gt; 的 &lt;code&gt;sys_fstat()&lt;/code&gt; 和 &lt;code&gt;kernel/file.c&lt;/code&gt; 的 &lt;code&gt;filestat()&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pagetable&lt;/code&gt;：目标进程的页表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dstva&lt;/code&gt;：用户空间的目标虚拟地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt;：内核空间的元数据地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;len&lt;/code&gt;：要复制的字节数
成功返回 0，失败返回 -1（如用户地址非法或不可写）。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 Lab util</title><link>https://siolin.me/blog/cs/1_util</link><guid isPermaLink="true">https://siolin.me/blog/cs/1_util</guid><description>添加一些简单的系统调用，主要涉及进程和文件描述符的知识点。</description><pubDate>Thu, 17 Jul 2025 17:52:52 GMT</pubDate><content:encoded>&lt;h2&gt;sleep&lt;/h2&gt;
&lt;p&gt;暂停用户指定的时钟周期数。&lt;/p&gt;
&lt;p&gt;首先是获取命令行参数（可以参考 &lt;em&gt;rm.c&lt;/em&gt;），将其转化成 &lt;code&gt;int&lt;/code&gt; 类型后再进行系统调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;

int main(int argc, char *argv[]) {
  // 参数少于两个，需要报错
  if (argc &amp;#x3C; 2) {
    fprintf(2, &quot;Usage: sleep seconds\n&quot;);
    exit(1);
  }
  
  // 系统调用，但是需要将char *转化成int
  sleep(atoi(argv[1]));
  exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pingpong&lt;/h2&gt;
&lt;p&gt;在两个进程之间传递一个字节。&lt;/p&gt;
&lt;p&gt;关于管道的两个端口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;p[0]：0为标准输出（把0想象成Output），因此此端为输出端口&lt;/li&gt;
&lt;li&gt;p[1]：1为标准输入（把1想象成Input），因此此端为输入端口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目的是通过管道实现进程之间的通信。创建两个管道，分别实现父对子通信和子对父通信，注意需要将用不到的管道关闭。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;

#define READEND 0
#define WRITEEDN 1

int main() {
  int p1[2]; // 父对子
  int p2[2]; // 子对父
  char buf[1]; // 用于临时存放进程间通信的一个字节

  pipe(p1);
  pipe(p2);
  
  if (fork() == 0) {
    // child progress
    close(p1[WRITEEDN]);
    close(p2[READEND]);
    read(p1[READEND], buf, 1);
    printf(&quot;%d: received ping\n&quot;, getpid());
    write(p2[WRITEEDN], &quot; &quot;, 1);
    close(p1[READEND]);
    close(p2[WRITEEDN]);
  } else {
    // parent progress
    close(p1[READEND]);
    close(p2[WRITEEDN]);
    write(p1[WRITEEDN], &quot; &quot;, 1);
    read(p2[READEND], buf, 1);
    printf(&quot;%d: received pong\n&quot;, getpid());
    close(p1[WRITEEDN]);
    close(p2[READEND]);
  }
  exit(0);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;子进程需要从p1[0]端读取数据，并从p2[1]端发送数据&lt;/li&gt;
&lt;li&gt;父进程需要从p1[1]端输入数据，并从p2[0]端读取数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中需要注意的是两个进程中 &lt;code&gt;read&lt;/code&gt; 和 &lt;code&gt;write&lt;/code&gt; 的顺序：必须存在一个进程的 &lt;code&gt;write&lt;/code&gt; 在 &lt;code&gt;read&lt;/code&gt; 之前，否则会导致进程阻塞或死锁。原因如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两个进程都是 &lt;code&gt;read&lt;/code&gt; 在前，那么由于谁都没有发送数据，双方都会卡在 &lt;code&gt;read&lt;/code&gt; 这一步；&lt;/li&gt;
&lt;li&gt;在其它情况下，即使进程之间的执行顺序无从得知，但无论如何都会有一个或两个进程写入了数据，最终可以读取到。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;primes&lt;/h2&gt;
&lt;p&gt;通过创建子进程和管道来筛选素数，就像一个筛网一样层层筛选：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://swtch.com/~rsc/thread/sieve.gif&quot; alt=&quot;img&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于xv6的文件描述符和进程数量有限，所以进程的最大数量为35，同时还要及时关闭用不到的文件描述符。&lt;/p&gt;
&lt;p&gt;采取递归的方式来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;

#define READEND 0
#define WRITEEND 1
#define MAXFEEDS 35

void child(int *pl); 

int main() {
  int p[2];
  pipe(p);

  if (fork() == 0) {
    child(p);
  } else {
    close(p[READEND]); // 关闭输出端口
    for (int i = 2; i &amp;#x3C;= MAXFEEDS; ++i) {
      write(p[WRITEEND], &amp;#x26;i, sizeof(int));
    }
    close(p[WRITEEND]);
    wait((int *) 0);
  }
  exit(0);
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored &quot;-Winfinite-recursion&quot;

void child(int *pl) {
  int pr[2];
  int n;
  close(pl[WRITEEND]);
  
  // 输入端口关闭时，read返回0，这代表当前处于最后一层递归
  int ret = read(pl[READEND], &amp;#x26;n, sizeof(int));
  if (ret == 0) {
    exit(0);
  }

  pipe(pr);

  if (fork() == 0) {
    child(pr);
  } else {
    close(pr[READEND]);
    printf(&quot;prime %d\n&quot;, n);
    int prime = n;
    while (read(pl[READEND], &amp;#x26;n, sizeof(int)) != 0) {
      if (n % prime != 0) {
        write(pr[WRITEEND], &amp;#x26;n, sizeof(int));
      }
    }
    close(pr[WRITEEND]);
    wait((int *) 0);
  }
  exit(0);
}

#pragma GCC diagnostic pop

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第17行需要关闭用不到的输出端口，但是为什么不在一开始即 &lt;code&gt;if&lt;/code&gt; 之前关？因为子进程会复制父进程的文件描述符，如果一开始就关那么子进程将无法读取数据；&lt;/li&gt;
&lt;li&gt;由于递归的终止条件是&lt;strong&gt;管道读取完毕&lt;/strong&gt;(&lt;code&gt;read&lt;/code&gt;返回&lt;code&gt;0&lt;/code&gt;)，但这是运行时行为，编译器无法预知，于是其会认为此程序无限递归从而会引发报错，所以需要在 &lt;code&gt;child&lt;/code&gt;  函数的前后加上编译指示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;find&lt;/h2&gt;
&lt;p&gt;在目录树中查找所有具有特定名称的文件，将其打印出来。此函数重点考查文件系统。&lt;/p&gt;
&lt;p&gt;位于 &lt;code&gt;fs.h&lt;/code&gt; 中的 &lt;code&gt;struct dirent&lt;/code&gt;，用来描述目录条目：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 文件名的最大长度
#define DIRSIZ 14

struct dirent {
  ushort inum; // 文件的i节点号，用于唯一标识文件。inum=0为空闲条目
  char name[DIRSIZ]; // 文件名
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;位于 &lt;code&gt;stat.h&lt;/code&gt; 中的 &lt;code&gt;struct stat&lt;/code&gt;，用来描述文件元数据：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define T_DIR     1   // Directory
#define T_FILE    2   // File
#define T_DEVICE  3   // Device

struct stat {
  int dev;     // File system&apos;s disk device
  uint ino;    // Inode number
  short type;  // Type of file
  short nlink; // Number of links to file
  uint64 size; // Size of file in bytes
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依旧是通过递归实现，代码与 &lt;code&gt;ls.c&lt;/code&gt; 存在大量重叠：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &quot;kernel/types.h&quot;
#include &quot;kernel/stat.h&quot;
#include &quot;user/user.h&quot;
#include &quot;kernel/fs.h&quot;

void find(char *path, char *file);

int main(int argc, char *argv[]) {
  if (argc != 3) { // 只能为3个参数
    fprintf(2, &quot;ERROR: need to pass only 2 arguments\n&quot;);
    exit(1);
  }

  find(argv[1], argv[2]);
  exit(0);
}

void find(char *path, char *file) {
  char buf[512], *p;
  int fd;
  struct dirent de;
  struct stat st;

  // 打开现有文件（只读）
  if ((fd = open(path, 0)) &amp;#x3C; 0) {
    fprintf(2, &quot;find: cannot open %s\n&quot;, path);
    return;
  }

  // 将fd指定的文件元数据存储到st中
  if (fstat(fd, &amp;#x26;st) &amp;#x3C; 0) {
    fprintf(2, &quot;find: cannot stat %s\n&quot;, path);
    close(fd);
    return;
  }
  
  switch (st.type) {
    case T_FILE: // 如果是文件，直接打印名称
      if (strcmp(path + strlen(path) - strlen(file), file) == 0) {
        printf(&quot;%s\n&quot;, path);
      }
      break;
    case T_DIR: // 如果是目录，需要继续递归寻找，同时维护当前路径名
      if (strlen(path) + 1 + DIRSIZ + 1 &gt; sizeof buf) {
        printf(&quot;find: path too long\n&quot;);
        break;
      } 
      strcpy(buf, path);
      p = buf + strlen(buf);
      *p++ = &apos;/&apos;;
      while (read(fd, &amp;#x26;de, sizeof(de)) == sizeof(de)) {
        if (de.inum == 0) // 空闲条目
          continue;
        memmove(p, de.name, DIRSIZ);
        p[DIRSIZ] = 0;
        if (strcmp(de.name, &quot;.&quot;) != 0 &amp;#x26;&amp;#x26; strcmp(de.name, &quot;..&quot;) != 0) {
          // 子目录递归寻找
          find(buf, file);
        }
      }
      break;
  }
  close(fd); // 当前递归结束前记得要关闭文件描述符
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;xargs&lt;/h2&gt;
&lt;p&gt;从标准输入读取行数据，并为每行数据执行指定命令，将该行内容作为命令参数传入。&lt;/p&gt;
&lt;p&gt;例如命令 &lt;code&gt;echo hello too | xargs echo bye&lt;/code&gt;，由于使用了管道符，因此 xargs 的标准输入为 &lt;code&gt;hello too&lt;/code&gt;，而指定命令则是 &lt;code&gt;echo bye&lt;/code&gt;，组合时候也就是 &lt;code&gt;echo bye hello too&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是需要注意这里的命令参数是以行为单位（上面的例子只有一行标准输入），因此执行 &lt;code&gt;echo &quot;1\n2&quot; | xargs -n 1 echo line&lt;/code&gt; 实际上是执行 &lt;code&gt;echo line 1&lt;/code&gt; 和 &lt;code&gt;echo line 2&lt;/code&gt;（其中 &lt;code&gt;-n 1&lt;/code&gt;意为只传入一个命令）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#include &quot;kernel/types.h&quot;
#include &quot;user/user.h&quot;
#include &quot;kernel/param.h&quot;

#define MAXLEN 100 // 参数的最大长度

int main(int argc, char *argv[]) {
  char *command = argv[1]; // 命令
  char bf;
  char paramv[MAXARG][MAXLEN]; // 存储行数据
  char *para[MAXARG]; 

  while (1) {
    int cnt = argc - 1;
    memset(paramv, 0, MAXARG * MAXLEN);
    // argv的第一个参数为程序本身，第二个才是命令
    for (int i = 1; i &amp;#x3C; argc - 1; ++i) {
      strcpy(paramv[i - 1], argv[i + 1]);
    }
    
    int ret;
    int cursor = 0;
    int flag = 0; // 标志位，为0时表示一个参数读取完毕

    while ((ret = read(0, &amp;#x26;bf, 1)) &gt; 0 &amp;#x26;&amp;#x26; bf != &apos;\n&apos;) {
      if (bf != &apos; &apos;) {
        paramv[cnt][cursor++] = bf;
        flag = 1;
      } else if (bf == &apos; &apos; &amp;#x26;&amp;#x26; flag == 1) {
        cnt++;
        cursor = 0;
        flag = 0;
      }
    }

    if (ret &amp;#x3C;= 0) { // 标准输入全部读取完
      break;
    }
    
    // 当前行参数已经读取完成
    for (int i = 0; i &amp;#x3C; MAXARG - 1; ++i) {
      para[i] = paramv[i];
    }
    para[MAXARG - 1] = 0;

    if (fork() == 0) {
      exec(command, para);
      exit(0);
    } else {
      wait((int *) 0);
    }
  }
  exit(0);
}


&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>MIT6.S081 环境配置及踩坑</title><link>https://siolin.me/blog/cs/0_setting</link><guid isPermaLink="true">https://siolin.me/blog/cs/0_setting</guid><description>MIT6.S081环境配置以及踩坑补充。</description><pubDate>Fri, 11 Jul 2025 04:38:10 GMT</pubDate><content:encoded>&lt;p&gt;记录在配置MIT6.S081时所踩的坑。&lt;/p&gt;
&lt;p&gt;系统为Archlinux物理机，环境如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/neofetch.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;学习的课程版本为&lt;a href=&quot;https://pdos.csail.mit.edu/6.828/2021/schedule.html&quot;&gt;Fall 2021&lt;/a&gt;，因为这一版本兼容新版本的 &lt;code&gt;qemu&lt;/code&gt;，而2020版本不兼容，需要额外降级。&lt;/p&gt;
&lt;p&gt;至于前置操作，已经有博主给出了详细的指导：&lt;a href=&quot;https://acmicpc.top/2024/02/08/MIT-6.S081-lab0-%E9%85%8D%E7%8E%AF%E5%A2%83/#%E9%85%8D%E7%BD%AEVScode%E5%92%8Cclangd&quot;&gt;MIT 6.S081 lab0：配置xv6环境+vscode调试&lt;/a&gt;，这里不再赘述。&lt;/p&gt;
&lt;p&gt;但是我按照其步骤配置时，另外发现一个坑。即修改完 &lt;code&gt;runcmd&lt;/code&gt;函数时再次运行 &lt;code&gt;make qemu&lt;/code&gt;，出现以下报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/error.png&quot; alt=&quot;bug&quot;&gt;&lt;/p&gt;
&lt;p&gt;解决方法是修改 &lt;code&gt;user/usertests.c&lt;/code&gt;文件中的 &lt;code&gt;rwsbrk()&lt;/code&gt;函数声明：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/rwsbrk.png&quot; alt=&quot;rwsbrk&quot;&gt;&lt;/p&gt;
&lt;p&gt;之后便可编译成功！&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025-06-27</title><link>https://siolin.me/blog/phase_recap/exam_week_end</link><guid isPermaLink="true">https://siolin.me/blog/phase_recap/exam_week_end</guid><description>期末周的总结与假期的展望。</description><pubDate>Fri, 27 Jun 2025 17:39:51 GMT</pubDate><content:encoded>&lt;p&gt;今天是心心念念的期末考试结束日，经过了许多的折磨和挣扎后终于熬过了这段时光，理所应当值得庆祝。但是考试周暴露出来的很多问题也值得总结和反思。&lt;/p&gt;
&lt;h2&gt;堆积课程带来的高压&lt;/h2&gt;
&lt;p&gt;​	学校课程堆积太多导致考试周持续高压：本学期有很多基础课程如“高等数学”、“大学物理”、“离散结构”和“线性代数”，知识点较为繁杂，这导致速通的效果并不显著，还容易把自己搞得身心俱疲。因此在之后的期末考试准备中可以将战线拉的长一点，给自己留下更多的时间准备。&lt;/p&gt;
&lt;h2&gt;高压带来的反弹放纵&lt;/h2&gt;
&lt;p&gt;​	白天的高强度速通令人疲惫，这成为了晚上打游戏的理由。劳役结合是好事，但是在打游戏中我并没有得到真正的娱乐和放松，反而给我带来虚度光阴的愧疚：一是因为我并不是真的喜欢玩这些游戏，只是将其当作消磨时光的一种途径，并没有起到真正的放松效果；二是想到自己与同龄人之间逐渐拉开的的差距以及对前程的不确定性。&lt;/p&gt;
&lt;p&gt;​	《认知觉醒》中提到人在在疲惫时，最容易松懈自己的元认知（监督自己）能力，进而会抵不住一些诱惑或做出错误的事情。考试周打乱了我的整体节奏，速通期末的疲惫也使得我无心过问CS的内容。因此在考试周结束的暑假中，要尽快重新找到自己的节奏，用充实的生活代替现在的荒废。&lt;/p&gt;
&lt;h2&gt;知识管理的再认知&lt;/h2&gt;
&lt;p&gt;​	在整理笔记中无意接触到 &lt;a href=&quot;https://pkmer.cn/&quot;&gt;PKMer&lt;/a&gt; 这个网站，里面除了介绍一些工作流之外，还详细讲解了“知识管理”这一专题。我一直想找到一套适合自己学习流，包括知识的摄入、组织和回顾，于是便据此完善我的知识管理体系。&lt;/p&gt;
&lt;h2&gt;大一阶段的总结&lt;/h2&gt;
&lt;p&gt;​	总的来说，整个大一阶段还是处于摸索，但是却极大的拓宽了我的眼界，使我能够较早地开始为未来作准备。期间接触到很多厉害的人，也感谢这些前辈的无私和开源精神，使得我少走了许多弯路。经过了一年的曲折，我的认知和能力得到了极大的提升，希望接下来的时间能继续坚持之前的优良习惯，减少之前的不良行为。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>2025-06-08</title><link>https://siolin.me/blog/phase_recap/exam_week_begin</link><guid isPermaLink="true">https://siolin.me/blog/phase_recap/exam_week_begin</guid><description>开始准备期末考试，总结一下此前的学习。</description><pubDate>Sun, 08 Jun 2025 21:18:41 GMT</pubDate><content:encoded>&lt;p&gt;由于考试周的即将到来，不得不放下专业课的学习，转而开始备考。&lt;/p&gt;
&lt;h2&gt;回顾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;完成了CSAPP的第一部分（1至6章），虽然经历了许多曲折，比如个别高难度的lab和一些基层的概念，但是也收获颇丰。对于我这种代码能力较差的人来说，能够独立完成这些lab无疑是巨大的挑战，只能在借助资料的情况下尽可能有自己的理解。&lt;/li&gt;
&lt;li&gt;搭建了自己的博客，于是也算在这广袤天地有了属于自己的精神净土。搭建博客有如下几方面目的：
&lt;ul&gt;
&lt;li&gt;博客的输出可以检验并加深我对知识点的理解，毕竟学习需要输入与输出结合，而写博客本身就是一种输出；&lt;/li&gt;
&lt;li&gt;可以进行总结和规划，将自己的行为和规划清晰化、透明化，不仅起到监督和勉励自己的作用，还可以消除心中对行动模糊的恐惧；&lt;/li&gt;
&lt;li&gt;发表自己的想法，也算是在赛博世界有了额外的精神寄托。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;完成了一些学校事务，如体育考试和英语的Presentation。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;规划&lt;/h2&gt;
&lt;p&gt;许多学校课程的结课以及考试近在咫尺，随之而来的是各种大作业以及课程复习（对于我来说更应该是开始学习）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全面准备期末考试，尽量的做到不挂科&lt;/li&gt;
&lt;li&gt;沉淀之前的CS所学，如数据结构与算法、JavaSE以及学过的部分《CSAPP》（实在做不到完全不碰CS）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对学校的各种繁杂琐碎始终反感，但是转念一想，生活中很少存在完全顺心顺意的时刻，就像“Life is like a box of chocolates.”所言，这一切构成了整个人生经历。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>