<?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>Yutong&apos;s Site</title><description>My compass is curiosity.</description><link>https://www.lyt0112.com</link><item><title>Running Into Doctor Who in Balboa Park</title><link>https://www.lyt0112.com/blog/dw_worlds_of_wonder-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw_worlds_of_wonder-en</guid><description>The plan was just to visit the San Diego Museum of Art, then a glance up while passing the Comic-Con Museum changed the day.</description><pubDate>Mon, 16 Feb 2026 16:30:00 GMT</pubDate><content:encoded>&lt;p&gt;This day was really fun.&lt;/p&gt;
&lt;p&gt;The original plan was just to visit the San Diego Museum of Art. The sky was a little gray, the ground was still wet, and everything felt quiet. Then while passing the Comic-Con Museum, there was a glimpse of some very familiar blue boxes, with &lt;code&gt;Doctor Who Worlds of Wonder&lt;/code&gt; across them.&lt;/p&gt;
&lt;p&gt;There had been absolutely no idea this exhibition was here. It was not the reason for the trip, and it was not something checked in advance either. It just appeared out of nowhere while walking by.&lt;/p&gt;
&lt;p&gt;Getting closer, it turned out to be real.&lt;/p&gt;
&lt;p&gt;Tickets were about $30 for adults and $20 for students. At the time, signing up on the official site also got a $15 discounted ticket.&lt;/p&gt;
&lt;h2&gt;That Blue Door&lt;/h2&gt;
&lt;p&gt;The entrance itself was simple, but once that blue door showed up, the whole place already felt very Doctor Who.&lt;/p&gt;
&lt;p&gt;The first thing there was the costume wall.&lt;/p&gt;
&lt;p&gt;Nine, Ten, Eleven, Twelve, Thirteen. For anyone who has been following the show for years, it still feels special. Twelve especially brought back 2014, when Doctor Who first became a real part of everyday life.&lt;/p&gt;
&lt;h2&gt;Props&lt;/h2&gt;
&lt;p&gt;Further in were the console and the sonic screwdrivers[^1]. Even got to touch the console once.&lt;/p&gt;
&lt;p&gt;In the nearby case were some smaller props that do not show up as often, including Twelve&apos;s sonic shades[^2] and the little device used in the 2013 50th Anniversary Special[^3].&lt;/p&gt;
&lt;p&gt;There was also Handles[^4][^5], the old Cyberman head that stayed with Eleven to the very end.&lt;/p&gt;
&lt;p&gt;After the outfit area, the first thing to see was a row of three spacesuits. The one on the left is River Song&apos;s Apollo-style suit from the &lt;code&gt;S06&lt;/code&gt; story arc[^6]. The middle one is from &lt;code&gt;S04E08-S04E09&lt;/code&gt;[^7][^8], and it immediately brings back &lt;code&gt;Hey, who turned out the lights?&lt;/code&gt;. The orange one on the right is that classic Doctor Who spacesuit that appears again and again, including Clara&apos;s in &lt;code&gt;S08E07 Kill the Moon&lt;/code&gt;[^9].&lt;/p&gt;
&lt;p&gt;The Weeping Angel on the left is from &lt;code&gt;S03E10&lt;/code&gt;[^10], and the fluffy Meep on the right is from the 2023 60th anniversary &lt;code&gt;Special 1&lt;/code&gt;[^11].&lt;/p&gt;
&lt;p&gt;Then came the villain lineup: Daleks[^12], Cybermen[^13], and Sontarans[^14].&lt;/p&gt;
&lt;p&gt;Up close, the Dalek eyestalk still feels a little intimidating.&lt;/p&gt;
&lt;p&gt;It was the first time seeing the words on the TARDIS door from this close.&lt;/p&gt;
&lt;p&gt;There was also the Teller from &lt;code&gt;S08E05 Time Heist&lt;/code&gt;[^15], or &lt;code&gt;Brain Soup&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Face of Boe / Captain Jack Harkness[^16][^17]. Torchwood is pretty good too[^18].&lt;/p&gt;
&lt;p&gt;Nearby was the cat nun from &lt;code&gt;S02E01&lt;/code&gt;[^19].&lt;/p&gt;
&lt;p&gt;Silence will fall![^20]&lt;/p&gt;
&lt;h2&gt;Whiteboard and screening area&lt;/h2&gt;
&lt;p&gt;Left a quick message on the whiteboard.&lt;/p&gt;
&lt;p&gt;There was also a screening area nearby. When this visit happened, it was introducing the 2023 60th anniversary &lt;code&gt;Special 1&lt;/code&gt;[^11].&lt;/p&gt;
&lt;p&gt;This timeline wall runs from 1963 to now. Doctor Who has already been going for a very, very long time.&lt;/p&gt;
&lt;h2&gt;Gift shop&lt;/h2&gt;
&lt;p&gt;T-shirts, tote bags, Dalek-pattern bags, and so on.&lt;/p&gt;
&lt;p&gt;Doctor Who novels, comics, and spin-offs.&lt;/p&gt;
&lt;h2&gt;After leaving&lt;/h2&gt;
&lt;p&gt;It still was not dark in Balboa Park when leaving the exhibition. The ground was still wet, and the light was slowly starting to come back.&lt;/p&gt;
&lt;p&gt;Later there was also a walk through the Japanese garden nearby. Daleks and Cybermen first, then water and stones.&lt;/p&gt;
&lt;p&gt;At the end, there was a sunset after the rain.&lt;/p&gt;
&lt;p&gt;The best part was this kind of completely unplanned surprise.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: What is the TARDIS? Doctor Who. https://www.doctorwho.tv/news-and-features/what-is-the-tardis&lt;/p&gt;
&lt;p&gt;[^2]: Sonic shades. Tardis Wiki. https://tardis.wiki/wiki/Sonic_shades&lt;/p&gt;
&lt;p&gt;[^3]: The Day of the Doctor. Doctor Who. https://www.doctorwho.tv/stories/the-day-of-the-doctor&lt;/p&gt;
&lt;p&gt;[^4]: Handles. Tardis Wiki. https://tardis.wiki/wiki/Handles&lt;/p&gt;
&lt;p&gt;[^5]: The Time of the Doctor. Doctor Who. https://www.doctorwho.tv/stories/the-time-of-the-doctor&lt;/p&gt;
&lt;p&gt;[^6]: River Song. Tardis Wiki. https://tardis.wiki/wiki/River_Song&lt;/p&gt;
&lt;p&gt;[^7]: Silence in the Library. Doctor Who. https://www.doctorwho.tv/stories/silence-in-the-library&lt;/p&gt;
&lt;p&gt;[^8]: Forest of the Dead. Doctor Who. https://www.doctorwho.tv/stories/forest-of-the-dead&lt;/p&gt;
&lt;p&gt;[^9]: Kill the Moon. Doctor Who. https://www.doctorwho.tv/stories/kill-the-moon&lt;/p&gt;
&lt;p&gt;[^10]: Blink. Doctor Who. https://www.doctorwho.tv/stories/blink&lt;/p&gt;
&lt;p&gt;[^11]: The Star Beast. Doctor Who. https://www.doctorwho.tv/stories/the-star-beast&lt;/p&gt;
&lt;p&gt;[^12]: Daleks. Doctor Who. https://www.doctorwho.tv/characters/daleks&lt;/p&gt;
&lt;p&gt;[^13]: Cybermen. Doctor Who. https://www.doctorwho.tv/characters/cybermen&lt;/p&gt;
&lt;p&gt;[^14]: Sontarans. Doctor Who. https://www.doctorwho.tv/characters/sontarans&lt;/p&gt;
&lt;p&gt;[^15]: Time Heist. Doctor Who. https://www.doctorwho.tv/stories/time-heist&lt;/p&gt;
&lt;p&gt;[^16]: Face of Boe. Tardis Wiki. https://tardis.wiki/wiki/Face_of_Boe&lt;/p&gt;
&lt;p&gt;[^17]: Captain Jack Harkness. Tardis Wiki. https://tardis.wiki/wiki/Captain_Jack_Harkness&lt;/p&gt;
&lt;p&gt;[^18]: Torchwood (TV series). Tardis Wiki. https://tardis.wiki/wiki/Torchwood_(TV_series)&lt;/p&gt;
&lt;p&gt;[^19]: New Earth. Doctor Who. https://www.doctorwho.tv/stories/new-earth&lt;/p&gt;
&lt;p&gt;[^20]: The Silence. Doctor Who. https://www.doctorwho.tv/characters/the-silence&lt;/p&gt;</content:encoded><h:img src="/_astro/dw_worlds_of_wonder.Do0a2RVI.jpeg"/><enclosure url="/_astro/dw_worlds_of_wonder.Do0a2RVI.jpeg"/></item><item><title>在 Balboa Park 偶遇 Doctor Who</title><link>https://www.lyt0112.com/blog/dw_worlds_of_wonder-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw_worlds_of_wonder-zh</guid><description>本来只是去圣地亚哥美术馆, 结果路过 Comic-Con Museum 时抬头一看.</description><pubDate>Mon, 16 Feb 2026 16:30:00 GMT</pubDate><content:encoded>&lt;p&gt;这天真是太有趣了.&lt;/p&gt;
&lt;p&gt;原计划其实只是去 San Diego Museum of Art 逛逛. 天有点阴, 地上还是湿的, 非常宁静的氛围. 结果路过 Comic-Con Museum, 偶然瞥见了一些熟悉的蓝盒子, 标题是 &lt;code&gt;Doctor Who Worlds of Wonder&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;之前完全不知道这个展, 不是专门来参观, 也不是提前查好的行程, 非常非常凑巧路过的时候突然看见了.&lt;/p&gt;
&lt;p&gt;走近之后确认一下居然是真的.&lt;/p&gt;
&lt;p&gt;票价大概是成人 30 美元, 学生 20 美元. 当时如果在官网注册过活动信息, 还有 15 美元优惠票.&lt;/p&gt;
&lt;h2&gt;That Blue Door&lt;/h2&gt;
&lt;p&gt;展览入口很简单, 但是蓝门一出来, 整个环境已经非常 Doctor Who 了.&lt;/p&gt;
&lt;p&gt;第一眼就是服装墙.&lt;/p&gt;
&lt;p&gt;九任, 十任, 十一任, 十二任, 十三任. 对追了很多年的人来说, 这种感觉还是很特别. 尤其是十二任那套, 看到的时候想起 2014 年刚开始追剧的那段时间.&lt;/p&gt;
&lt;h2&gt;道具区&lt;/h2&gt;
&lt;p&gt;再往里走就是控制台和 sonic screwdrivers[^1]. 还上手摸了一把控制台.&lt;/p&gt;
&lt;p&gt;旁边展柜的各种出现次数较少的道具, 有十二叔的音速眼镜[^2], 还有五十周年纪念集中用来探查 Zygon Queen Elizabeth 的小道具[^3].&lt;/p&gt;
&lt;p&gt;还有 &lt;code&gt;The Time of the Doctor&lt;/code&gt; 里的 Handles[^4][^5], 陪 11 任走完最后一段的老朋友.&lt;/p&gt;
&lt;p&gt;经过了 outfit 区域先看到的是三套宇航服. 左边这套是 River Song[^6] 在 &lt;code&gt;S06&lt;/code&gt; 故事线里的 Apollo 风宇航服. 中间这套来自图书馆两集 &lt;code&gt;S04E08-S04E09&lt;/code&gt;[^7][^8], 一看到就会想起那句 &quot;Hey, who turned out the lights?&quot;. 右边这套橙色宇航服则是出现在很多集里的经典宇航服, 比如 Clara 在 &lt;code&gt;S08E07 Kill the Moon&lt;/code&gt; 穿过的那一套[^9].&lt;/p&gt;
&lt;p&gt;左边的 Weeping Angel 来自 &lt;code&gt;S03E10&lt;/code&gt;[^10], 右边这只毛茸茸的 Meep 来自 2023 60 周年 &lt;code&gt;Special 1&lt;/code&gt;[^11].&lt;/p&gt;
&lt;p&gt;反派大合集[^12][^13][^14].&lt;/p&gt;
&lt;p&gt;Dalek 的眼柄近看还是会有一点压迫感.&lt;/p&gt;
&lt;p&gt;TARDIS 门上的字第一次距离这么近看.&lt;/p&gt;
&lt;p&gt;还有 &lt;code&gt;S08E05 Time Heist&lt;/code&gt; 的 &lt;code&gt;Brain Soup&lt;/code&gt;[^15].&lt;/p&gt;
&lt;p&gt;Face of Boe / Captain Jack Harkness[^16][^17]. Torchwood 还蛮好看的嘞[^18].&lt;/p&gt;
&lt;p&gt;旁边还有 &lt;code&gt;S02E01&lt;/code&gt; 的猫修女[^19].&lt;/p&gt;
&lt;p&gt;Silence will fall![^20]&lt;/p&gt;
&lt;h2&gt;留言板和放映区&lt;/h2&gt;
&lt;p&gt;在白板上到此一游一下.&lt;/p&gt;
&lt;p&gt;旁边还有放映区, 去的时候正在介绍 2023 60 周年 &lt;code&gt;Special 1&lt;/code&gt;[^11].&lt;/p&gt;
&lt;p&gt;这面时间线墙, 从 1963 年到现在, Doctor Who 已经走了很久很久了.&lt;/p&gt;
&lt;h2&gt;纪念品区&lt;/h2&gt;
&lt;p&gt;T 恤, tote bag, Dalek 图案的包等等.&lt;/p&gt;
&lt;p&gt;DW 的周边小说漫画衍生剧.&lt;/p&gt;
&lt;h2&gt;之后&lt;/h2&gt;
&lt;p&gt;离开展馆时, Balboa Park 的天还没有黑. 地面还是湿的, 光线开始慢慢出来.&lt;/p&gt;
&lt;p&gt;后来又去旁边的日本庭园转了转. 前面看 Dalek 和 Cybermen, 后面看水和石头.&lt;/p&gt;
&lt;p&gt;最后看到了雨后的晚霞.&lt;/p&gt;
&lt;p&gt;最喜欢的还是这种完全没计划好的惊喜.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: What is the TARDIS? Doctor Who. https://www.doctorwho.tv/news-and-features/what-is-the-tardis&lt;/p&gt;
&lt;p&gt;[^2]: Sonic shades. Tardis Wiki. https://tardis.wiki/wiki/Sonic_shades&lt;/p&gt;
&lt;p&gt;[^3]: The Day of the Doctor. Doctor Who. https://www.doctorwho.tv/stories/the-day-of-the-doctor&lt;/p&gt;
&lt;p&gt;[^4]: Handles. Tardis Wiki. https://tardis.wiki/wiki/Handles&lt;/p&gt;
&lt;p&gt;[^5]: The Time of the Doctor. Doctor Who. https://www.doctorwho.tv/stories/the-time-of-the-doctor&lt;/p&gt;
&lt;p&gt;[^6]: River Song. Tardis Wiki. https://tardis.wiki/wiki/River_Song&lt;/p&gt;
&lt;p&gt;[^7]: Silence in the Library. Doctor Who. https://www.doctorwho.tv/stories/silence-in-the-library&lt;/p&gt;
&lt;p&gt;[^8]: Forest of the Dead. Doctor Who. https://www.doctorwho.tv/stories/forest-of-the-dead&lt;/p&gt;
&lt;p&gt;[^9]: Kill the Moon. Doctor Who. https://www.doctorwho.tv/stories/kill-the-moon&lt;/p&gt;
&lt;p&gt;[^10]: Blink. Doctor Who. https://www.doctorwho.tv/stories/blink&lt;/p&gt;
&lt;p&gt;[^11]: The Star Beast. Doctor Who. https://www.doctorwho.tv/stories/the-star-beast&lt;/p&gt;
&lt;p&gt;[^12]: Daleks. Doctor Who. https://www.doctorwho.tv/characters/daleks&lt;/p&gt;
&lt;p&gt;[^13]: Cybermen. Doctor Who. https://www.doctorwho.tv/characters/cybermen&lt;/p&gt;
&lt;p&gt;[^14]: Sontarans. Doctor Who. https://www.doctorwho.tv/characters/sontarans&lt;/p&gt;
&lt;p&gt;[^15]: Time Heist. Doctor Who. https://www.doctorwho.tv/stories/time-heist&lt;/p&gt;
&lt;p&gt;[^16]: Face of Boe. Tardis Wiki. https://tardis.wiki/wiki/Face_of_Boe&lt;/p&gt;
&lt;p&gt;[^17]: Captain Jack Harkness. Tardis Wiki. https://tardis.wiki/wiki/Captain_Jack_Harkness&lt;/p&gt;
&lt;p&gt;[^18]: Torchwood (TV series). Tardis Wiki. https://tardis.wiki/wiki/Torchwood_(TV_series)&lt;/p&gt;
&lt;p&gt;[^19]: New Earth. Doctor Who. https://www.doctorwho.tv/stories/new-earth&lt;/p&gt;
&lt;p&gt;[^20]: The Silence. Doctor Who. https://www.doctorwho.tv/characters/the-silence&lt;/p&gt;</content:encoded><h:img src="/_astro/dw_worlds_of_wonder.Do0a2RVI.jpeg"/><enclosure url="/_astro/dw_worlds_of_wonder.Do0a2RVI.jpeg"/></item><item><title>Real-World RL: From The Matrix to Planet Earth</title><link>https://www.lyt0112.com/blog/real_world_rl-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/real_world_rl-en</guid><description>How real-world RL pulls robots out of virtual worlds and lets them learn in the only environment that matters.</description><pubDate>Mon, 17 Nov 2025 23:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gpt-5.2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to polish the writing.&lt;/p&gt;
&lt;p&gt;Physical Intelligence released the technical report for $\pi_{0.6}^*$ [^7]. A key point is that it uses real-world RL on physical robots to improve performance. I had been following real-world RL for a while, so I used this as a chance to review the line of work and sketch a few possible directions.&lt;/p&gt;
&lt;h2&gt;Background: What is Real-World RL?&lt;/h2&gt;
&lt;p&gt;For a long time, &lt;code&gt;robots + reinforcement learning&lt;/code&gt; almost meant collecting experience inside a physics simulator. We put virtual robots into environments and frameworks such as MuJoCo, Isaac Gym, Isaac Lab, and ManiSkill, let them run day and night, and after hundreds of millions of actions we find a policy that maximizes return. Then we try to use domain randomization, sim-to-real, or residual actions to make up for the dynamics gap between the simulator and the real world. This gap is usually called the &lt;code&gt;Sim2Real Gap&lt;/code&gt;, and it becomes more obvious as tasks become more dynamic and longer-horizon.&lt;/p&gt;
&lt;p&gt;Real-world RL takes a more direct path: let real-robot data carry most of the learning, use simulation only as a rough warm-up (if at all), and improve performance through repeated real interaction. The payoff is that training happens under the deployment dynamics, with no simulator bias.&lt;/p&gt;
&lt;p&gt;Compared with classic sim-to-real, there are two key differences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The trajectories for RL come from the real world, so there is no Sim2Real Gap&lt;/li&gt;
&lt;li&gt;Algorithms and systems must face real-world constraints seriously: hardware breaks, objects get lost, environments are messy, and human patience is limited&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why DayDreamer [^1] emphasizes from the start: no simulation, no human demonstrations, only online RL on real robots. It takes the Dreamer world model and puts it on real platforms, and shows that a world model can also learn behaviors efficiently in the physical world.&lt;/p&gt;
&lt;p&gt;Later, A Walk in the Park [^2] lowered the bar even further: no world model, no complex pretraining. As long as you polish a model-free RL algorithm and the controller and engineering details, a Unitree A1 quadruped can learn to walk stably on various outdoor terrains in about 20 minutes of real-world interaction.&lt;/p&gt;
&lt;p&gt;From these works on, Real-World RL stopped being just a vision and became a technical route that people could discuss seriously.&lt;/p&gt;
&lt;h2&gt;First Stage: From Learning in Simulation to Learning in Reality&lt;/h2&gt;
&lt;p&gt;If you only read the titles, it is easy to treat DayDreamer and A Walk in the Park as two separate lines: one from the world-model side, the other from the model-free side.
But if you stretch out the timeline, they actually work together to show one thing: doing RL in the real world is not a myth, but an engineering problem that can be solved.&lt;/p&gt;
&lt;h3&gt;DayDreamer: Not in World, in World Model&lt;/h3&gt;
&lt;p&gt;In short, DayDreamer moves the Dreamer world model to real robots and tests whether it can still learn. It evaluates on several different platforms: for example, training robots in the real world to navigate, to balance, and to do locomotion control, without relying on simulation or human teleoperation. Data collection and training run online in parallel. [^1]&lt;/p&gt;
&lt;p&gt;In this setup the world model is important because it gives real-world RL a virtual environment and increases the efficiency of using real data: the robot first learns a policy in the learned world model, which greatly reduces the number of real interactions needed. This makes online RL in the real world feasible for the first time.&lt;/p&gt;
&lt;p&gt;DayDreamer&apos;s main result is proof that online RL with a learned world model is workable on real robots, turning the path from theory into something you can walk.&lt;/p&gt;
&lt;h3&gt;A Walk in the Park: Algorithm or Engineering?&lt;/h3&gt;
&lt;p&gt;A year later, A Walk in the Park reported that carefully tuned SAC-style control and low-level controllers let an A1 learn outdoors in ~20 minutes without a world model. The takeaway is that the win came from task/MDP and controller engineering, not a new RL trick, reinforcing that system and controller engineering (not the loss function) is the bottleneck. [^2]&lt;/p&gt;
&lt;h3&gt;RoboCat: Self-generated Data&lt;/h3&gt;
&lt;p&gt;In 2023, DeepMind released RoboCat, which takes a different path. It is not a system designed specifically for Real-World RL, but it looks like a kind of rehearsal for real-world RL. [^6]&lt;/p&gt;
&lt;p&gt;RoboCat builds on a Gato-style vision-based decision Transformer. It trains a generalist agent from demonstration data covering many robots and many tasks, and then runs a self-improvement loop: humans provide 100-1000 demonstrations for a new task, the model is fine-tuned, then it practices about ten thousand times in simulation or in the real world, and the generated data is fed back into the training set to produce a new, stronger version.&lt;/p&gt;
&lt;p&gt;Although RoboCat does not emphasize online RL as strongly as DayDreamer or A Walk in the Park, it clearly brings out another important idea:&lt;/p&gt;
&lt;p&gt;A generalist robot policy can keep getting stronger by generating its own data and improving itself.&lt;/p&gt;
&lt;p&gt;This idea will later become a core theme in $\pi_{0.6}^*$.&lt;/p&gt;
&lt;h2&gt;Second Stage: From Can Learn to Learns Well&lt;/h2&gt;
&lt;p&gt;The first stage showed that real-world RL can learn. But when people try to deploy these systems, they care about two other questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can the agent learn in a way that is stable enough, with success rates close to 100%?&lt;/li&gt;
&lt;li&gt;Can it keep running for a long time, without needing a human to rescue it every half hour?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A series of works between 2023 and 2025 can be seen as systematic answers to these questions.&lt;/p&gt;
&lt;h3&gt;HIL-SERL: Data + Human Corrections + RL&lt;/h3&gt;
&lt;p&gt;HIL-SERL (Human-in-the-Loop Sample-Efficient RL) comes from Berkeley RAIL and appeared in Science Robotics 2025. It targets a set of tasks that are much harder than learning to walk: dynamic shaking to pull out a block, precise assembly, dual-arm collaboration, pan tossing for cooking, and other real manipulation tasks. [^3]&lt;/p&gt;
&lt;p&gt;The training procedure of HIL-SERL is simple but effective: first they collect good and bad trajectories using teleoperation, and train a binary reward model that judges success or failure; then they use a small number of demonstrations to initialize the policy; finally they run online RL on real robots, where humans step in to correct the robot at key moments. RL then improves the policy using this correction data and the learned reward.&lt;/p&gt;
&lt;p&gt;The results are very direct: on a set of complex manipulation tasks, HIL-SERL can push the success rate of vision-based policies close to 100% in about 1-2.5 hours of interaction, and the final execution speed is even faster than human teleoperation.&lt;/p&gt;
&lt;p&gt;This work makes two points that strongly influence later research:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-world RL should not start from random exploration, but should stand on top of demonstrations&lt;/li&gt;
&lt;li&gt;Human interventions are not noise; they are the key component that makes RL both safe and efficient&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can view it as an upgrade of what DayDreamer and A Walk in the Park did: from can learn to can learn well, and learn fast.&lt;/p&gt;
&lt;h3&gt;RL-100: Systematizing the Pipeline&lt;/h3&gt;
&lt;p&gt;If HIL-SERL is still a method, RL-100 has already grown into an engineering system.&lt;/p&gt;
&lt;p&gt;RL-100 proposes a three-stage pipeline: first, use imitation learning to inject human experience into a diffusion policy; second, run offline RL with offline policy evaluation (OPE) to obtain conservative policy improvement; finally, run a short period of online RL on real robots to clean up the remaining failure modes. [^4]&lt;/p&gt;
&lt;p&gt;They validate the system on seven real-robot tasks, including cloth folding, pouring fluids and granular materials, dynamic pushing, dexterous nut tightening, multi-stage orange juicing, and so on. In 900 evaluations they achieve 900/900 successes, and some tasks can run 250 times in a row without failure.&lt;/p&gt;
&lt;p&gt;Technically, RL-100 and HIL-SERL share the same spirit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Both rely on demonstrations and offline data to ensure a good starting point&lt;/li&gt;
&lt;li&gt;All exploration stays within safety boundaries monitored by OPE or humans&lt;/li&gt;
&lt;li&gt;The role of RL is to fix long-tail failures, not to invent motions from scratch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But RL-100 does one extra important thing: it turns the whole pipeline into a framework that is relatively agnostic to tasks, robot platforms, and sensing modalities. This is a step from paper demo toward a reusable system.&lt;/p&gt;
&lt;h3&gt;Contact-Rich Sim-to-Real: A Compromise Route&lt;/h3&gt;
&lt;p&gt;For assembly and tight insertion tasks where contact mechanics are very sensitive, learning entirely in the real world is still too risky. For such settings, work from Tomizuka&apos;s group proposes a hybrid idea: learn trajectories and compliance parameters with RL in simulation, then, in the real world, only do online fine-tuning of a small admittance residual. [^5]&lt;/p&gt;
&lt;p&gt;This style of method may not be as eye-catching as HIL-SERL or RL-100, but it is very practical in industrial scenarios: most of the risk is handled in simulation, and real-world RL only applies a small residual update.&lt;/p&gt;
&lt;p&gt;You can view it as an important side branch in the second stage: Real-World RL is not always the main actor, but can serve as the final adaptation layer in sim-to-real.&lt;/p&gt;
&lt;h2&gt;Third Stage: From Task-Specific to General Policies&lt;/h2&gt;
&lt;p&gt;So far, most work still focuses on letting the robot learn a single task. $\pi_{0.6}^*$ does something slightly counter-intuitive: it changes the object of RL training from a task to a general policy.&lt;/p&gt;
&lt;h3&gt;A Good-Enough General VLA&lt;/h3&gt;
&lt;p&gt;Physical Intelligence released $\pi_{0}$ in 2024. This model is essentially a vision-language-action (VLA) foundation model: it uses internet-scale vision-language pretraining plus large-scale robot data to train a model that can zero-shot and few-shot generalize across robots and tasks. [^8]&lt;/p&gt;
&lt;p&gt;$\pi_{0.5}$ and $\pi_{0.6}$ then increase model size, training data, and architectural capacity, forming a large-policy model that can  &quot;basically get the job done&quot;  on many household and simple industrial tasks. But, just like all the previous systems we have discussed, it hits the familiar problem: success rates are passable, but still not high enough for real use.&lt;/p&gt;
&lt;p&gt;This is the background for $\pi_{0.6}^*$.&lt;/p&gt;
&lt;h3&gt;RL with Experience &amp;#x26; Corrections&lt;/h3&gt;
&lt;p&gt;The technical report of $\pi_{0.6}^*$ describes a staged recipe: offline pretraining, supervised fine-tuning, and online correction-driven RL. [^7]&lt;/p&gt;
&lt;p&gt;What is interesting is the shape of the learning signal. In practice, Recap (RL with Experience &amp;#x26; Corrections via Advantage-conditioned Policies) is closer to supervised regression with a preference signal than to classic RL. The value function is trained by regressing to returns on collected trajectories. The policy is trained by regressing to actions in the dataset, but conditioned on advantage, so the model learns to bias toward higher-value choices. The key addition beyond plain imitation is that failures are &lt;strong&gt;no longer just &quot;bad demos&quot; to be discarded&lt;/strong&gt;. They become explicit negative labels, so the policy gets told what to avoid. [^9]&lt;/p&gt;
&lt;p&gt;Concretely, Recap looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First they run offline RL on $\pi_{0.6}$ to shape action preferences from offline data, i.e. training a value function on the model&apos;s own trajectories, regress it to returns, compute an advantage signal that scores actions as better or worse than average, and feed that advantage into the VLA as a condition&lt;/li&gt;
&lt;li&gt;For each concrete task, they then run a round of supervised / imitation learning fine-tuning from human demonstrations, so the model has a decent starting point&lt;/li&gt;
&lt;li&gt;Next they deploy the model on real robots and let it run the task by itself. Humans only step in when there are clear mistakes; these corrections are logged as supervision in the failure states&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This may sound abstract, but a simpler description is: $\pi_{0.6}^*$ uses interaction to expose real errors, turns those failures into training signal, and then uses advantage-conditioning to spread the fix to nearby situations instead of patching only the exact state you corrected.&lt;/p&gt;
&lt;p&gt;What about results? The report lists concrete numbers and case studies: on complex tasks like making espresso, assembling cardboard boxes, and folding different types of clothes, Recap doubles the throughput of $\pi_{0.6}^*$ (the number of tasks finished per unit time), and cuts failure rates to half or even less. The team runs robots from 5:30 in the morning to 11:30 at night making coffee, folding 50 unseen garments in a stranger&apos;s house, or assembling 59 real boxes on a factory line, without any run ending early because of model errors.&lt;/p&gt;
&lt;p&gt;If you zoom out on the timeline, it is natural to see $\pi_{0.6}^*$ standing on the shoulders of the previous work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Like HIL-SERL, it uses the trio of demonstrations + human corrections + RL to solve long-tail failures&lt;/li&gt;
&lt;li&gt;Like RL-100, it treats RL as a final repair layer that upgrades performance from sometimes wrong to rarely wrong&lt;/li&gt;
&lt;li&gt;But it also goes further: it is not optimizing a policy for a single task, but fine-tuning a large, general model&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the level of $\pi_{0.6}^*$, Real-World RL changes its role from a skill learning algorithm to a last-mile training tool for a general policy.&lt;/p&gt;
&lt;h2&gt;Summary and Outlook: Where Might Real-World RL Go Next?&lt;/h2&gt;
&lt;p&gt;DayDreamer and A Walk in the Park show that real-world RL can learn. HIL-SERL and RL-100 show that it can learn stably and for a long time. $\pi_{0.6}^*$ shows that it can become the last step for training general robot policies.&lt;/p&gt;
&lt;p&gt;From the point of view of research methodology, Real-World RL has quietly shifted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From we need new RL algorithms to we need reliable system engineering and training pipelines&lt;/li&gt;
&lt;li&gt;From letting RL learn one skill in the real world to letting RL fix the corners that a VLA cannot handle in the real world&lt;/li&gt;
&lt;li&gt;From simulation is the main work and the real world is only for evaluation to real experience is a necessary stage, and simulation is just a warm-up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Looking forward, some promising directions might cluster around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Larger-scale real-world data: trying robots generate their own training data across many tasks at the same time&lt;/li&gt;
&lt;li&gt;More automated and cheaper human intervention and safety mechanisms: for example, better semi-automatic correction, batch annotation tools, and more autonomous recovery, instead of requiring an engineer to stand by with an emergency stop&lt;/li&gt;
&lt;li&gt;More dexterous motion: overcoming the large Sim2Real gap in dexterous manipulation or high-dynamics manipulation, so that Real-World RL can learn in-hand manipulation beyond simple pick-and-place (for example, one-handed Rubik&apos;s Cube rotations, or using chopsticks)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From your perspective, if you are doing research or products in robot learning, the most practical value of Real-World RL today may not be inventing an even fancier RL algorithm, but carefully answering two very concrete questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which parts of your system should be handled by demonstrations and offline training, so that the model becomes smart enough to not self-destruct easily?&lt;/li&gt;
&lt;li&gt;And then, where must RL touch the real world, and learn from real failures and long tails?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$\pi_{0.6}^*$ gives the following answer:&lt;/p&gt;
&lt;p&gt;Demonstrations and pretraining are responsible for getting the success rate above zero, and Real-World RL is responsible for addressing real-world failure cases and closing the remaining gaps, until the robot can operate reliably in the physical world.&lt;/p&gt;
&lt;p&gt;That is probably the most attractive part of Real-World RL: it is not meant to replace everything else, but to make the whole robot system work reliably in the real world.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: DayDreamer: World Models for Physical Robot Learning. CoRL 2022. https://danijar.com/project/daydreamer/
[^2]: Laura Smith et al. A Walk in the Park: Learning to Walk in 20 Minutes With Model Free Reinforcement Learning. RSS Demo Track 2023. https://arxiv.org/abs/2208.07860
[^3]: HIL-SERL: Precise and Dexterous Robotic Manipulation via Human-in-the-Loop Sample-Efficient Robotic Reinforcement Learning. Science Robotics, 2025. https://hil-serl.github.io/
[^4]: Kun Lei et al. RL-100: Performant Robotic Manipulation with Real-World Reinforcement Learning. arXiv:2510.14830, 2025. https://arxiv.org/abs/2510.14830
[^5]: Xiang Zhang et al. Efficient Sim-to-real Transfer of Contact-Rich Manipulation Skills with Online Admittance Residual Learning. CoRL 2023. https://arxiv.org/abs/2310.10509
[^6]: RoboCat: A Self-Improving Generalist Agent for Robotic Manipulation. DeepMind, 2023. https://arxiv.org/abs/2306.11706
[^7]: $\pi_{0.6}^*$: A VLA that Learns from Experience. Physical Intelligence Blog, 2025-11-17. https://www.pi.website/blog/pistar06
[^8]: $\pi_{0}$: A Vision-Language-Action Flow Model for General Robot Control. Physical Intelligence Blog, 2024-10-31. https://www.physicalintelligence.company/blog/pi0
[^9]: Pi 0.6 : 披着Reinforcement Learning 外衣的 Supervised Learning, 2026-01-13. https://mp.weixin.qq.com/s/O7QOFeyjMDlg8Y5xDVbJNA&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail-test-2.DzZDiYKA.jpg"/><enclosure url="/_astro/thumbnail-test-2.DzZDiYKA.jpg"/></item><item><title>Real-World RL: From The Matrix to Planet Earth</title><link>https://www.lyt0112.com/blog/real_world_rl-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/real_world_rl-zh</guid><description>How real-world RL pulls robots out of virtual worlds and makes them to learn in the only environment that counts.</description><pubDate>Mon, 17 Nov 2025 23:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gpt-5.2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to polish the writing.&lt;/p&gt;
&lt;p&gt;今天 Physical Intelligence 发布了 $\pi_{0.6}^*$ 的技术报告 [^7], 其中一个关键点是它在真实机器人上使用了 Real-World RL 来提升性能. 之前也读过一些 Real-World RL 相关的工作, 借此机会调研一下已有的相关工作并总结一下来龙去脉, 以及未来可能的发展方向.&lt;/p&gt;
&lt;h2&gt;背景: 什么是 Real-World RL?&lt;/h2&gt;
&lt;p&gt;在很长一段时间里, 机器人 + 强化学习几乎等同于在物理仿真器里刷经验. 我们在 MuJoCo, Isaac Gym, Isaac Lab, ManiSkill 之类的环境/框架里, 让虚拟机器人日夜练习, 在数亿次动作之后找到最大化 return 的 policy, 再想办法用 domain randomization, sim-to-real 或者 residual action 等方法去弥补物理仿真器和现实的动力学差异, 这个差异被称作 &lt;code&gt;Sim2Real Gap&lt;/code&gt;, 并且在越动态, 越长的 Task 上越明显.&lt;/p&gt;
&lt;p&gt;Real-World RL 这个词, 指的是另一条更直接的路: 让真实世界的数据在真实机器人上承担主要的学习责任, 完全不依赖模拟, 或者模拟只是粗略起步, 最终通过现实里的反复交互迭代提升性能. 这是一个很有吸引力的目标, 因为现实世界的分布和动力学才是最终部署环境.&lt;/p&gt;
&lt;p&gt;它和 sim-to-real 有两个重要区别:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RL Trajectory 的来源是现实, 没有 Sim2Real Gap&lt;/li&gt;
&lt;li&gt;算法和系统必须严肃面对现实层面的约束: 硬件会坏, 物体会丢, 环境不可控, 人的耐心有限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是为什么 DayDreamer [^1] 会刻意强调: 不用模拟, 没有人类示教, 只靠 online RL 训练现实中的机器人, 把 Dreamer 世界模型搬到真实平台上, 证明 world model 在物理世界中也能高效学习行为.&lt;/p&gt;
&lt;p&gt;而后来的 A Walk in the Park [^2] 又把门槛再往下压了一点: 不用世界模型, 不用复杂预训练, 只要把 Model Free RL 算法和控制器以及工程实现打磨好, 一台 A1 四足机器人就可以在 20 分钟左右的现实交互里学会在多种户外地形上稳定行走.&lt;/p&gt;
&lt;p&gt;从这些工作开始, Real-World RL 不再只是一个愿景, 而变成了可以被认真讨论的技术路线.&lt;/p&gt;
&lt;h2&gt;第一阶段: 从在虚拟里学习到在现实里学习&lt;/h2&gt;
&lt;p&gt;如果只看论文标题, 很容易把 DayDreamer 和 A Walk in the Park 当成两条平行线: 一个是 World Model 派, 一个是 Model Free 派.
但如果把时间线拉长, 它们其实一起完成了同一件事: 证明在现实世界里做 RL 不是神话, 而是可以解决的工程问题.&lt;/p&gt;
&lt;h3&gt;DayDreamer: Not in World, in World Model&lt;/h3&gt;
&lt;p&gt;DayDreamer 做的事概括成一下: 把 Dreamer 世界模型整个搬到真实机器人上, 看它能不能学出来. 它在多个不同平台上测试: 例如让机器人在现实中学导航, 学平衡和运动控制, 完全没有依赖模拟或人工示教, 数据收集和训练在线并行进行. [^1]&lt;/p&gt;
&lt;p&gt;在这里世界模型真正重要的是, 它给现实 RL 提供了一种虚拟的世界, 提高真实数据的应用效率: 机器人先在 learned world model 里学习 policy, 大幅减少真实交互次数, 这使得在现实中做 online RL 第一次变得可行.&lt;/p&gt;
&lt;p&gt;DayDreamer 的结论很直接: 世界模型配合在线 RL 在真实机器人上可行, 路已经能走.&lt;/p&gt;
&lt;h3&gt;A Walk in the Park: 算法 OR 工程&lt;/h3&gt;
&lt;p&gt;一年后, A Walk in the Park 给出了一个精炼结果: 不用世界模型, 只要把 SAC 一类的 model-free 算法与低层控制器打磨到位, A1 在户外 20 分钟左右也能学会走路. 关键信息是: 成功更多来自 MDP/控制/工程细节, 而不是新奇的算法, 难点更多在系统与控制, 而非损失函数.&lt;/p&gt;
&lt;h3&gt;RoboCat: 自生成数据&lt;/h3&gt;
&lt;p&gt;2023 年, DeepMind 发布的 RoboCat 走的是另一条路线: 它不是专门为 Real-World RL 设计的系统, 但可以视为现实 RL 的一种预演. [^6]&lt;/p&gt;
&lt;p&gt;RoboCat 基于 Gato 式的视觉决策 Transformer, 通过汇集多种机器人, 多任务的示教数据训练出一个 generalist agent, 然后进入自我改进循环: 人给 100~1000 条新任务示范, 模型微调后在仿真或现实中自我练习约一万次, 再把生成的数据回灌进训练集, 得到新的, 更强版本.&lt;/p&gt;
&lt;p&gt;它虽然不像 DayDreamer, A Walk in the Park 那样强调在线 RL, 但它清楚地提出了另一个重要思想:&lt;/p&gt;
&lt;p&gt;通用机器人策略可以通过自我产生数据与自我改进来变得越来越强.&lt;/p&gt;
&lt;p&gt;这个思想会在 $\pi_{0.6}^*$ 里被放大成一个核心主题.&lt;/p&gt;
&lt;h2&gt;第二阶段: 从能学到学得好&lt;/h2&gt;
&lt;p&gt;第一阶段证明了现实 RL 可以学会, 然而实际部署时, 人们更关心的是另外两个问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能不能学得足够稳, 成功率接近 100%?&lt;/li&gt;
&lt;li&gt;能不能学得足够久, 不要每半小时就需要人来救场?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2023 - 2025 年间的一系列工作, 基本都可以看作是围绕这两个问题的系统化解答.&lt;/p&gt;
&lt;h3&gt;HIL-SERL: Data + 人类纠正 + RL&lt;/h3&gt;
&lt;p&gt;HIL-SERL (Human-in-the-Loop Sample-Efficient RL) 出自 Berkeley RAIL, 发表在 2025 年的 Science Robotics. 它面对的是一组比学走路难得多的任务: 动态抖动抽积木, 精密装配, 双臂协同, 颠锅做饭之类的真实 manipulation. [^3]&lt;/p&gt;
&lt;p&gt;HIL-SERL 的训练流程非常朴素但有效: 先用遥操作采集好坏样本, 训练一个二分类 reward 模型判断是否成功; 再用少量示范初始化策略; 最后在现实中做 online RL, 人类在关键时刻介入纠正, RL 在这些纠正数据和奖励上不断改进策略.&lt;/p&gt;
&lt;p&gt;实验结果非常直接: 在一系列复杂操作任务上, HIL-SERL 能在大约 1~2.5 小时的交互里, 把视觉策略的成功率推到接近 100%, 并且执行速度还比人类遥操作更快.&lt;/p&gt;
&lt;p&gt;这篇工作有两个对后续研究影响很大的观点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;现实 RL 不应该从零随机探索, 而应该站在示教的起点上&lt;/li&gt;
&lt;li&gt;人类的介入不是噪声, 而是让 RL 既安全又高效的关键构件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以把它看成是 DayDreamer / A Walk in the Park 做的那件事的升级版: 从能学会升级成能学好, 而且学得很快.&lt;/p&gt;
&lt;h3&gt;RL-100: Systematizing the Pipeline&lt;/h3&gt;
&lt;p&gt;如果说 HIL-SERL 还是一个方法, RL-100 就已经长成了一个工程系统.&lt;/p&gt;
&lt;p&gt;RL-100 提出了一个三阶段 pipeline: 先用 imitation learning 把人类经验注入到扩散策略里, 再用带离线策略评估 (OPE) 的离线 RL 做保守的策略改进, 最后用一小段在线 RL 在真实机器人上清除残余的失败模式. [^4]&lt;/p&gt;
&lt;p&gt;他们在七个真实机器人任务上做了系统验证:
包括布料折叠, 流体/颗粒物倾倒, 动态推杆, 灵巧手拧螺帽, 多阶段榨橙汁等, 最终在 900 次评估里做到了 900/900 成功, 有的任务甚至能连续 250 次不出错.&lt;/p&gt;
&lt;p&gt;从技术角度看, RL-100 和 HIL-SERL 的精神是一致的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;都依赖示教和离线数据来保证起点不错&lt;/li&gt;
&lt;li&gt;所有探索都在被 OPE 或人类监控的安全边界内进行&lt;/li&gt;
&lt;li&gt;RL 的职责是修补长尾失败情况, 而不是从头发明动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但 RL-100 做了一件非常重要的额外工作: 它把整条链路做成了一个对任务, 机器人平台, 感知模态相对 agnostic 的框架, 这是从论文 demo 迈向可复用系统的一步.&lt;/p&gt;
&lt;h3&gt;Contact-Rich Sim-to-Real: 折中路线&lt;/h3&gt;
&lt;p&gt;在装配, 精密插接这种接触力学极其敏感的任务上, 完全在现实里学依然太危险. 针对这类任务, Tomizuka 等人的工作提出了一个混合思路: 在模拟中用 RL 学到轨迹和顺应控制参数, 到了现实世界只在线微调一个小小的 admittance residual. [^5]&lt;/p&gt;
&lt;p&gt;这类方法不一定像 HIL-SERL, RL-100 那样显眼, 但它们在工业场景里非常实用: 大部分风险在模拟里解决, 现实中的 RL 只做小幅度的残差微调.&lt;/p&gt;
&lt;p&gt;可以把它视为第二阶段中的一个重要支线: Real-World RL 不一定总是主角, 但可以作为 sim-to-real 的最后一层自适应.&lt;/p&gt;
&lt;h2&gt;第三阶段: 从任务级 RL 到通用策略&lt;/h2&gt;
&lt;p&gt;前面所有工作, 多少还是以让机器人学会某个任务为主角.
$\pi_{0.6}^*$ 做了一件略微反常识的事: 它把 RL 用来训练的对象, 从某个任务换成了一个通用策略.&lt;/p&gt;
&lt;h3&gt;足够好的通用 VLA&lt;/h3&gt;
&lt;p&gt;Physical Intelligence 在 2024 年公开了 $\pi_{0}$, 这个模型本质上是一个 vision-language-action (VLA) 基础模型: 用互联网规模的视觉-语言预训练加上大规模机器人数据, 让一个模型在多机器人, 多任务上具备零样本和小样本泛化能力. [^8]&lt;/p&gt;
&lt;p&gt;之后的 $\pi_{0.5}$, $\pi_{0.6}$ 继续在模型规模, 训练数据和架构上做增强, 形成了一个在很多家务和简单工业任务上基本能干活的大模型策略. 但和前面提到的所有工作一样, 它也遇到了那个熟悉的问题: 成功率可以过得去, 但离真正可用还差一些.&lt;/p&gt;
&lt;p&gt;这就是 $\pi_{0.6}^*$ 登场的背景.&lt;/p&gt;
&lt;h3&gt;RL with Experience &amp;#x26; Corrections&lt;/h3&gt;
&lt;p&gt;$\pi_{0.6}^*$ 的技术报告里, 描述了一个分阶段的训练流程: 离线预训练, 监督微调, 以及基于纠正信号的在线 RL. [^7]&lt;/p&gt;
&lt;p&gt;但如果把它拆开看, Recap (RL with Experience &amp;#x26; Corrections via Advantage-conditioned Policies) 更像是带偏好条件的监督回归. Value function 是对回报做回归训练, policy 是对动作做回归训练, 只是把 advantage 当作条件输入, 让模型倾向于更高 value 的选择. 和传统 imitation learning 最大的差别, 是&lt;strong&gt;失败不再被当作噪声丢掉&lt;/strong&gt;, 而是被标注成负信号, 变成模型要主动避免的东西. [^9]&lt;/p&gt;
&lt;p&gt;具体流程可以写成这样:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先用离线 RL 在 $\pi_{0.6}$ 上做一次预训练, 让模型在离线数据上学会区分好动作和坏动作, 方法是让模型基于自己的执行轨迹学习一个 value function, 对 sparse 的 return 做回归, 计算 advantage 信号, 并把这个 advantage 作为条件输入, 让 VLA 学会偏好高 advantage 的行为&lt;/li&gt;
&lt;li&gt;对于每个具体任务, 再用人类示教做一轮微调, 让模型在这个任务上有个不错的起点&lt;/li&gt;
&lt;li&gt;然后在真实机器人上放开模型, 让它自己做任务, 人类只在明显错误时介入纠正, 这些纠正会被作为失败状态下的纠正样本, 再重复第一步的 RL 流程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换个角度说, 这里的 RL 负责暴露真实错误, 把失败变成训练信号, advantage-conditioned policy 则让修正从一个具体状态扩展到一类相近情境.&lt;/p&gt;
&lt;p&gt;结果如何? 报告里给出了一些非常具体的数字和案例: 在制作意式咖啡, 组装纸箱, 折叠各类衣物这些复杂任务上, 引入 Recap 后, $\pi_{0.6}^*$ 的吞吐量 (单位时间内成功完成的任务数) 可以翻倍, 失败率下降到原来的一半甚至更低. 团队让机器人从早上 5:30 到晚上 11:30 一直做咖啡, 或者在陌生家里连续折 50 件没见过的衣服, 或者在真实工厂里装 59 个真正用于包装的纸箱, 都没有因为模型错误而中断.&lt;/p&gt;
&lt;p&gt;如果把时间线拉远一点, 你会发现 $\pi_{0.6}^*$ 非常自然地站在前面几篇工作的肩膀上:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它和 HIL-SERL 一样, 用示教 + 人类纠正 + RL 的三段式套路解决长尾失败问题&lt;/li&gt;
&lt;li&gt;它和 RL-100 一样, 把 RL 放在最后的修补层, 只负责把成功率从偶尔错打磨到尽量不错&lt;/li&gt;
&lt;li&gt;但它又走得更远: 它不是在优化一个具体任务的策略, 而是在优化一个通用的大模型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 $\pi_{0.6}^*$ 这里, Real-World RL 的角色从技能学习算法变成了通用策略的最后一公里训练工具.&lt;/p&gt;
&lt;h2&gt;小结与展望: Real-World RL 之后会走向哪里?&lt;/h2&gt;
&lt;p&gt;把上面的故事压缩成一句话的话, 大概是这样:&lt;/p&gt;
&lt;p&gt;DayDreamer 和 A Walk in the Park 证明了现实世界 RL 可以学会, HIL-SERL 和 RL-100 证明了它可以学得稳, 学得久, $\pi_{0.6}^*$ 则展示了它可以变成通用机器人策略的最后一步.&lt;/p&gt;
&lt;p&gt;从研究范式的角度看, Real-World RL 已经完成了几次观念的转变:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从我们需要新的 RL 算法变成我们需要靠谱的系统工程和训练流程&lt;/li&gt;
&lt;li&gt;从让 RL 在现实中学会一个技能变成让 RL 在现实中修好 VLA 学不到的角落&lt;/li&gt;
&lt;li&gt;从模拟是主力, 现实只是验证变成现实经验是必经之路, 模拟只是热身&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;未来比较值得期待的一些方向, 大概会围绕下面几件事展开:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更大规模的真实世界数据: 在大量的 Task 上同时尝试机器人自己产生训练数据&lt;/li&gt;
&lt;li&gt;更自动化, 更便宜的人类介入和安全机制: 比如更好的半自动纠正, 批量标注工具, 以及更强的自主恢复能力, 而不是依赖工程师随时介入&lt;/li&gt;
&lt;li&gt;更灵巧的动作: 在 Dexterous Manipulation 或者 High Dynamics Manipulation 这类更复杂的任务上克服传统 Sim2Real 的巨大 SimReal Gap, 让 Real-World RL 学到更复杂的 In-Hand Manipulation, 而不是简单的 Pick and Place 组合, 比如单手拧魔方或用筷子夹取物体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从你的角度, 如果你在做机器人学习相关的研究或产品, Real-World RL 在今天最现实的价值可能不是发明一个更 fancy 的 RL 算法, 而是认真回答两个非常具体的问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的系统里, 哪些部分应该交给示教和离线训练, 让模型先足够聪明, 不太容易自杀&lt;/li&gt;
&lt;li&gt;然后, 在哪些地方必须让 RL 接触真实世界, 从真正的错误和长尾里学到修正信号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$\pi_{0.6}^*$ 给出的答案是:&lt;/p&gt;
&lt;p&gt;示教和预训练负责把动作练到成功率不为 0, Real-World RL 则负责在现实世界里覆盖失败场景并逐步缩小剩余 gap, 直到系统可以稳定运行.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: DayDreamer: World Models for Physical Robot Learning. CoRL 2022. https://danijar.com/project/daydreamer/
[^2]: Laura Smith et al. A Walk in the Park: Learning to Walk in 20 Minutes With Model Free Reinforcement Learning. RSS Demo Track 2023. https://arxiv.org/abs/2208.07860
[^3]: HIL-SERL: Precise and Dexterous Robotic Manipulation via Human-in-the-Loop Sample-Efficient Robotic Reinforcement Learning. Science Robotics, 2025. https://hil-serl.github.io/
[^4]: Kun Lei et al. RL-100: Performant Robotic Manipulation with Real-World Reinforcement Learning. arXiv:2510.14830, 2025. https://arxiv.org/abs/2510.14830
[^5]: Xiang Zhang et al. Efficient Sim-to-real Transfer of Contact-Rich Manipulation Skills with Online Admittance Residual Learning. CoRL 2023. https://arxiv.org/abs/2310.10509
[^6]: RoboCat: A Self-Improving Generalist Agent for Robotic Manipulation. DeepMind, 2023. https://arxiv.org/abs/2306.11706
[^7]: $\pi_{0.6}^*$: A VLA that Learns from Experience. Physical Intelligence Blog, 2025-11-17. https://www.pi.website/blog/pistar06
[^8]: $\pi_{0}$: A Vision-Language-Action Flow Model for General Robot Control. Physical Intelligence Blog, 2024-10-31. https://www.physicalintelligence.company/blog/pi0
[^9]: Pi 0.6 : 披着Reinforcement Learning 外衣的 Supervised Learning, 2026-01-13. https://mp.weixin.qq.com/s/O7QOFeyjMDlg8Y5xDVbJNA&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail-test-2.DzZDiYKA.jpg"/><enclosure url="/_astro/thumbnail-test-2.DzZDiYKA.jpg"/></item><item><title>Hand Motion Retargeting</title><link>https://www.lyt0112.com/blog/retargeting-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/retargeting-zh</guid><description>Interaction-aware hand motion retargeting spanning geometry, force, and self-supervision.</description><pubDate>Mon, 29 Sep 2025 17:13:04 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gpt-5.2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to polish the writing.&lt;/p&gt;
&lt;h2&gt;背景与定义&lt;/h2&gt;
&lt;p&gt;运动重定向 (Motion Retargeting) 是角色动画/机器人领域的一个概念, 指在把个体的关节状态 qpos 转换成另一个类似的个体的关节状态.&lt;/p&gt;
&lt;p&gt;具体来说, 在人手上是指把人手或者机械手的动作转移到另一个手, 同时尽量保持对同一物体的操作一致性. 比如给定人手拿起一个木块的动作, 能够通过 retargeting 将动作转移给机械手, 最终让它也拿起这个木块. 所以这项能力是 teleoperation, imitation learning 和数据扩增的关键环节, 因为它让已有的人类示教得以跨本体复用.&lt;/p&gt;
&lt;p&gt;如果是同样的自由度, 那么最直接的想法是逐关节复制旋转角度, 但当两只手在自由度, 连杆长度和关节约束上不一致时, 这个 naive 方式马上失效, 物体接触的位置也会漂移. 人手动作重定向 (Hand Motion Retargeting) 真正的挑战在于人手与机器人手存在系统性的形态差异, 同时还要面对与物体的复杂接触. 纯几何角度映射难以保留交互语义和任务目标, 于是研究逐渐从几何匹配走向交互感知, 把物体形状, 受力, 触觉和动作意图纳入同一个学习闭环, 并积极采用自监督和无配对数据.&lt;/p&gt;
&lt;h2&gt;几何重定向&lt;/h2&gt;
&lt;p&gt;早期路线聚焦于几何一致性: 对齐关键点, 缩放轨迹, 用优化吸收残差. AnyTeleop [^1] 把手腕到指尖的向量误差纳入目标并施加平滑正则, DexH2R [^2] 则缩放人手轨迹后求解非线性优化, 为机器人手输出关节序列. 这条路径提供了直接的几何直觉, 但缺乏对物体语义的建模, 一旦任务或接触面发生变化就容易失稳.&lt;/p&gt;
&lt;h2&gt;对象形状为条件的重定向&lt;/h2&gt;
&lt;p&gt;人手面对不同形状的物体时, 关节角度和接触分布会系统性重排. 如果仍然把人手姿态硬映射到机器人手, 接触点会错位, 抓握力会失衡, 姿态也会显得不自然. 因此研究开始把物体几何引入条件, 先对齐物体再推断手部姿态, 以恢复高层的交互直觉.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FunGrasp (2024) [^3]&lt;/strong&gt;: 该系统提出了一个三阶段管线, 首先利用单张 RGB-D 图像估计功能性人手姿态, 并通过在物体坐标系中对齐手部链节方向及优化保持精确接触点和人手姿态, 将人类功能性抓握重定向到不同机器人手. 随后结合视觉和触觉的动态强化学习策略, 在保持接触参考的基础上让机器人手适应不同形状和未见物体, 并通过特权学习和系统辨识提升仿真到真实的迁移.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DexFlow (2025) [^4]&lt;/strong&gt;: 该方法构建了一个分层优化管线, 先进行全局姿态搜索匹配人手与机器人手, 再在局部阶段用能量函数优化接触, 使机器人手自然贴合物体表面. 同时通过双阈值检测与时间平滑的时序接触处理流程提取稳定接触, 并发布包含 29.2 万个抓取帧的跨手拓扑数据集支持这一流程.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kinematic Motion Retargeting for Contact-Rich Manipulations (2024) [^5]&lt;/strong&gt;: 该工作将重定向视为非等距形状匹配问题, 利用表面接触区域与标记点数据, 通过逆运动学逐步估计并优化目标手轨迹. 其核心贡献是局部形状匹配算法和多阶段优化管线, 可在整个操作序列中保持接触分布一致, 并支持对象替换与跨手泛化.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Learning Cross-hand Policies of High-DOF Reaching and Grasping (2024) [^6]&lt;/strong&gt;: 作者提出手形无关的状态-动作表示和二阶段框架, 先用统一策略预测抓取关键点位移, 再由手型特定的适配器转化为各手的关节控制, 从而实现高自由度抓取策略的跨手迁移. 策略输入由语义关键点和交互中垂面 (IBS) 组成, 借助 Transformer 网络学习手指间关系, 从而对不同手型和物体组合具备泛化能力.&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;&lt;strong&gt;Feel the Force: Contact-Driven Learning from Humans (2025) [^7]&lt;/strong&gt;: 使用带触觉传感器的手套记录人手接触力与关键点坐标, 预测机器人轨迹和期望抓握力, 执行时以 PD 控制调整夹爪以贴合触觉示范, 但 pipeline 中夹杂了大量手工设置, 可迁移性有限.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DexMachina (2025) [^8]&lt;/strong&gt;: 强化学习阶段引入强度衰减的虚拟物体控制器, 叠加接触奖励和任务奖励, 但是这其实应该被认为是 RL Tracking 而不是 Retargeting.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cross-embodiment 与自监督学习&lt;/h2&gt;
&lt;p&gt;这一方向试图摆脱人工配对数据, 直接从动作准则中学习跨手映射.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Geometric Retargeting (2025) [^12]&lt;/strong&gt;: 以指尖速度一致性等动作准则作为自监督信号, 学习跨本体的无配对映射, 即便在尺度与关节差异下仍维持接触语义与运动稳定性, 并已作为几何先验集成进 Dexterity Gen [^13].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning to Transfer Human Hand Skills for Robot Manipulations (2025) [^10]&lt;/strong&gt;: 拟合人手, 机器人动作与物体运动的共享流形, 利用合成配对三元组训练模型, 避免真实人机配对数据的高昂成本.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;几何映射难以覆盖复杂物体交互与任务约束, 因此近年来的工作都在利用视觉和触觉线索来追求更自然的接触以及更强的泛化. 然而, 除去简单的 pick and place, 多数任务仍难以可靠迁移. 直觉上, 缺口主要在两点: 对物体的理解与对受力的一致性. 其一, 面对不同形状或功能的物体, 即使人手的动作相同, 期望的机器人关节配置并不相同, 因此需要以物体几何或功能为条件; 其二, 即便是同一物体, 不同的人手力分布也应映射到不同的机器人 target qpos, 因此需要以接触力或目标力为条件.&lt;/p&gt;
&lt;p&gt;从另一条路径看, 若未来的强化学习学得足够 dexterous 的原生策略, 只需把人手动作作为额外的输入进行对齐就能端到端完成 retargeting. 但是以当下的能力而言, 这样的控制器尚未出现. 因此, 以物体与受力为条件的交互感知重定向仍是最务实且可扩展的路线.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: AnyTeleop: A General Vision-Based Dexterous Robot Arm-Hand Teleoperation System. https://arxiv.org/abs/2307.04577v3
[^2]: DexH2R. https://arxiv.org/abs/2411.04428.pdf
[^3]: FunGrasp: Functional Grasping for Diverse Dexterous Hands. https://arxiv.org/abs/2411.16755v1
[^4]: DexFlow: A Unified Approach for Dexterous Hand Pose Retargeting and Interaction. https://arxiv.org/abs/2505.01083v1
[^5]: Kinematic Motion Retargeting for Contact-Rich Anthropomorphic Manipulations. https://arxiv.org/abs/2402.04820.pdf
[^6]: Learning Cross-hand Policies of High-DOF Reaching and Grasping. https://arxiv.org/abs/2404.09150
[^7]: Feel the Force: Contact-Driven Learning from Humans. https://arxiv.org/abs/2506.01944.pdf
[^8]: DexMachina. https://arxiv.org/abs/2505.24853.pdf
[^9]: ManipTrans: Efficient Dexterous Bimanual Manipulation Transfer via Residual Learning. https://arxiv.org/abs/2503.21860
[^10]: Learning to Transfer Human Hand Skills for Robot Manipulations. https://arxiv.org/abs/2501.04169v1
[^11]: Cross-Embodiment Dexterous Grasping with Reinforcement Learning. https://arxiv.org/abs/2410.02479v1
[^12]: Geometric Retargeting. https://arxiv.org/abs/2503.07541
[^13]: Dexterity Gen. https://zhaohengyin.github.io/dexteritygen/&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail-test-3.1GZ294Dz.jpg"/><enclosure url="/_astro/thumbnail-test-3.1GZ294Dz.jpg"/></item><item><title>Hand Motion Retargeting</title><link>https://www.lyt0112.com/blog/retargeting-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/retargeting-en</guid><description>Interaction-aware hand motion retargeting spanning geometry, force, and self-supervision.</description><pubDate>Mon, 29 Sep 2025 17:13:03 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gpt-5.2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to polish the writing.&lt;/p&gt;
&lt;h2&gt;Background and Definition&lt;/h2&gt;
&lt;p&gt;Motion Retargeting is a concept in character animation and robotics that refers to converting one individual&apos;s joint state (qpos) to another similar individual&apos;s joint state.&lt;/p&gt;
&lt;p&gt;For hands, it means transferring the motion of a human hand or robotic hand to another hand while maintaining the ability to manipulate the same objects consistently. For example, given a human hand motion of picking up a block, retargeting can transfer this motion to a robotic hand, ultimately enabling it to pick up the same block. This capability is a key hub for teleoperation, imitation learning, and data augmentation, because it allows existing human demonstrations to be reused across embodiments.&lt;/p&gt;
&lt;p&gt;If the degrees of freedom are identical, the most straightforward idea is to copy joint rotation angles one by one. When two hands differ in degrees of freedom, link lengths, or joint constraints, this naive approach fails and the contact positions drift. The real challenge of hand motion retargeting lies in the systematic morphological differences between human and robot hands, combined with complex contacts with objects. Pure geometric mappings struggle to preserve interaction semantics and task objectives, so research has gradually shifted from geometric matching to interaction-aware methods that bring object shape, force, touch, and action intent into the learning loop, often via self-supervision and unpaired data.&lt;/p&gt;
&lt;h2&gt;Geometric Retargeting&lt;/h2&gt;
&lt;p&gt;Early approaches focused on geometric consistency: align keypoints, scale trajectories, and absorb residuals via optimization. AnyTeleop [^1] includes the vector error from wrist to fingertip in the objective and applies smooth regularization. DexH2R [^2] scales human-hand trajectories and then solves a nonlinear optimization to produce a joint sequence for the robot hand. This path provides straightforward geometric intuition but lacks modeling of object semantics, so it often becomes unstable when the task or contact surface changes.&lt;/p&gt;
&lt;h2&gt;Object-conditioned Retargeting&lt;/h2&gt;
&lt;p&gt;When the hand interacts with objects of different shapes, joint angles and contact distributions rearrange systematically. If we continue to hard-map human poses to a robot hand, contact points will misalign, grip forces will become unbalanced, and the resulting pose will look unnatural. Consequently, recent work conditions on object geometry: first align the object, then infer the hand pose to recover high-level interaction intent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FunGrasp (2024) [^3]&lt;/strong&gt;: Proposes a three-stage pipeline. It first estimates a functional human-hand pose from a single RGB-D image. In the object frame, it aligns hand link directions and optimizes to preserve precise contact points and human-hand pose, retargeting functional grasps to different robot hands. It then trains a vision-and-touch DRL policy that adapts the robot hand to different shapes and unseen objects while respecting contact references, and improves sim-to-real transfer via privileged learning and system identification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DexFlow (2025) [^4]&lt;/strong&gt;: Builds a hierarchical optimization pipeline. It performs a global pose search to match human and robot hands, then locally optimizes contacts with an energy function so the robot hand naturally conforms to the object surface. It also extracts stable contacts via dual-threshold detection with temporal smoothing, and releases a cross-hand-topology dataset containing 292k grasp frames to support this pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kinematic Motion Retargeting for Contact-Rich Manipulations (2024) [^5]&lt;/strong&gt;: Treats retargeting as a non-isometric shape matching problem. Using surface contact regions and marker data, it incrementally estimates and optimizes target-hand trajectories via inverse kinematics. The core contributions are a local shape-matching algorithm and a multi-stage optimization pipeline that maintains consistent contact distributions over full manipulation sequences, and supports object replacement and cross-hand generalization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning Cross-hand Policies of High-DOF Reaching and Grasping (2024) [^6]&lt;/strong&gt;: Proposes a hand-shape-agnostic state-action representation and a two-stage framework. A unified policy predicts displacements of grasp keypoints, then hand-specific adapters convert them to each hand&apos;s joint controls, enabling cross-hand transfer of high-DOF grasping. Inputs are semantic keypoints and the interaction bisector surface (IBS); a Transformer learns relations among fingers, yielding generalization over different hands and objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Force-conditioned Retargeting&lt;/h2&gt;
&lt;p&gt;In practice, force placement decides grasp stability; even with the same object, different force profiles demand different target poses, so force must be an explicit condition.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feel the Force: Contact-Driven Learning from Humans (2025) [^7]&lt;/strong&gt;: Uses a tactile glove to record human contact forces and keypoint coordinates, predicts robot trajectories and desired grasp forces, and at execution time adjusts the gripper with PD control to track tactile demonstrations. However, the pipeline involves many hand-tuned components and has limited transferability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DexMachina (2025) [^8]&lt;/strong&gt;: Introduces a fading virtual-object controller during RL and adds contact and task rewards, but this should be considered RL tracking rather than true retargeting.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cross-embodiment and Self-supervision&lt;/h2&gt;
&lt;p&gt;This direction aims to avoid manually paired data and to learn cross-hand mappings directly from action principles.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Geometric Retargeting (2025) [^12]&lt;/strong&gt;: Uses action principles such as fingertip-velocity consistency as self-supervised signals to learn unpaired, cross-embodiment mappings that preserve contact semantics and motion stability despite scale and joint differences, and has been integrated as a geometric prior into Dexterity Gen [^13].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning to Transfer Human Hand Skills for Robot Manipulations (2025) [^10]&lt;/strong&gt;: Fits a shared manifold of human-hand motion, robot actions, and object motion; trains on synthetic paired triplets to avoid the high cost of real human-robot pairs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Purely geometric retargeting cannot cover complex object interactions and task constraints, so recent work leverages visual and tactile cues to pursue more natural contacts and stronger generalization. Yet, beyond simple pick-and-place, most tasks still fail to transfer reliably. In practice, the main gaps are twofold: understanding objects and achieving force consistency. First, for objects with different shapes or functions, even identical human motions should map to different robot joint configurations; therefore conditioning on object geometry or functionality is required. Second, even for the same object, different human force distributions should map to different robot target qpos; therefore conditioning on contact or target forces is necessary.&lt;/p&gt;
&lt;p&gt;From another angle, if future reinforcement learning yields sufficiently dexterous native policies, we could complete retargeting end-to-end by aligning human motion as an additional input. At present, such controllers do not exist. Thus, interaction-aware retargeting conditioned on objects and forces remains the most pragmatic and scalable path.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: AnyTeleop: A General Vision-Based Dexterous Robot Arm-Hand Teleoperation System. https://arxiv.org/abs/2307.04577v3
[^2]: DexH2R. https://arxiv.org/abs/2411.04428.pdf
[^3]: FunGrasp: Functional Grasping for Diverse Dexterous Hands. https://arxiv.org/abs/2411.16755v1
[^4]: DexFlow: A Unified Approach for Dexterous Hand Pose Retargeting and Interaction. https://arxiv.org/abs/2505.01083v1
[^5]: Kinematic Motion Retargeting for Contact-Rich Anthropomorphic Manipulations. https://arxiv.org/abs/2402.04820.pdf
[^6]: Learning Cross-hand Policies of High-DOF Reaching and Grasping. https://arxiv.org/abs/2404.09150
[^7]: Feel the Force: Contact-Driven Learning from Humans. https://arxiv.org/abs/2506.01944.pdf
[^8]: DexMachina. https://arxiv.org/abs/2505.24853.pdf
[^9]: ManipTrans: Efficient Dexterous Bimanual Manipulation Transfer via Residual Learning. https://arxiv.org/abs/2503.21860
[^10]: Learning to Transfer Human Hand Skills for Robot Manipulations. https://arxiv.org/abs/2501.04169v1
[^11]: Cross-Embodiment Dexterous Grasping with Reinforcement Learning. https://arxiv.org/abs/2410.02479v1
[^12]: Geometric Retargeting. https://arxiv.org/abs/2503.07541
[^13]: Dexterity Gen. https://zhaohengyin.github.io/dexteritygen/&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail-test-3.1GZ294Dz.jpg"/><enclosure url="/_astro/thumbnail-test-3.1GZ294Dz.jpg"/></item><item><title>燕园未竟书</title><link>https://www.lyt0112.com/blog/this_is_pku-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/this_is_pku-zh</guid><description>欲买桂花同载酒，终不似，少年游。</description><pubDate>Thu, 03 Jul 2025 02:58:21 GMT</pubDate><content:encoded>&lt;h2&gt;燕园未竟书&lt;/h2&gt;
&lt;p&gt;作为本科生在燕园的最后一个白日，竟与四年前踏入此地的那个清晨，有着相似的温度。只是彼时的凉爽里浸润着对未来的迷茫热望，此刻的暑气中却弥漫着一丝曲终人散的惘然。&lt;/p&gt;
&lt;p&gt;此刻七月三日深夜提笔时，才发觉曾经汹涌的万语千言，此刻都已碎成沙。毕业，原来不是一场盛大的落幕，而是一次无声的离席。我没能为这四年找到一个足够定义自己的标签，它像一部还没来得及起书名的手稿，在对生活意义的寻寻觅觅中，便仓促地迎来了终章。&lt;/p&gt;
&lt;p&gt;四年很长，长到足够我吃下几千块钱的勺园黄焖鸡；四年也很短，短到我还没来得及尝遍家园与学一之外的早餐，没来得及去老馆的旧时光里坐上一坐，然后在自习的间隙睡上一觉。我总以为来日方长，却被时间悄悄地甩在了身后。这些未竟的小事，像书页里忘记折角的遗憾，最终都成了故事的一部分，提醒我，人生本就是一场充满留白的旅行。&lt;/p&gt;
&lt;p&gt;入学那天，我在邱德拔那条曲折的坡道前遇见两位学长，一句&quot;欢迎来到北大&quot;，轻描淡写，却成为我与这座园子最初的约定。坡道蜿蜒而上，正如我这四年虽曲折却不断向前的旅程。我在入学前曾用不容置疑的声音告诉自己，要坦然接受自己的普通，也要学会与期待和解，我努力选择倾听内心真正的声音，但是终究被绩点裹挟内耗。燕园很大，大到可以包容无数种梦想，绩点只是其中一种，但绝不是唯一的衡量。生命的价值，绝不应只用一份成绩单来定义。更重要的是，找到那件能让你眼里闪光、能让你被世界听见、被世界需要的事——让自己的声音被世界感知，让自己的努力回应世界的渴望，这远比在一条拥挤的赛道上身心俱疲更有意义。&lt;/p&gt;
&lt;p&gt;作为一个小小的尝试，从大二起，我开始整理我的&lt;a href=&quot;https://www.lyt0112.com/blog/course_review-zh&quot;&gt;课程资料&lt;/a&gt;。我并非想立一座不朽的丰碑，只是想在身后留下一串脚印，为后来者点一盏小小的灯。如果有人能因此少走一段弯路，多看一程风景，那便是我能想到的，最浪漫的告别。或许终有一天，我会从所有人的记忆里淡出，但若曾有某位同学，在某个深夜，对着屏幕上的资料，轻声念出标题，那一刻，我的青春便与他同在，我的思考便在这座园子里，获得了永恒。这份坚持，也离不开挚友 &lt;a href=&quot;https://arthals.ink&quot;&gt;Arthals 同学&lt;/a&gt; 的启发，是他让我看到，知识的分享本身，就是一种创造。&lt;/p&gt;
&lt;p&gt;告别的时刻终将到来。我们转身，将燕园的四季与晨昏装进行囊，走向一个没有围墙的远方。我们中的大多数人，都将从宏大的叙事中淡出，在日复一日里，活成一个面目模糊却无比真实的普通人。&lt;/p&gt;
&lt;p&gt;但这并非故事的结尾。请记得那个曾在四十五乙楼仰望星空的少年，请守护好那份与世界交手时，不与世俗和解的倔强。那才是我们行囊里最宝贵的财富。它会在未来的风雨里提醒我们：即便走在平凡的路上，也要走出属于自己的回响。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;命运似乎总爱在不经意处埋下伏笔。作为信息科学技术学院最早报到的学生之一，当年那个最早踏入四十五乙楼的我，在时光流转中送走了一任又一任的楼长之后，竟阴差阳错地递补接过了三、四楼微信群主的&quot;权柄&quot;。这件略带荒诞色彩的小事，成了一段意想不到的插曲，或许也是时光以一种奇特的方式，为我四年前那个启程的清晨，留下的一枚最特别的印记。&lt;/p&gt;
&lt;p&gt;如果你恰好看到并用到了我的&lt;a href=&quot;https://www.lyt0112.com/blog/course_review-zh&quot;&gt;课程资料&lt;/a&gt;，特别欢迎你在那个页面下方留下一个评论，让我知道这份微小的努力曾经被你看见。&lt;/p&gt;
&lt;p&gt;树洞 #7427133, #7393484, #7309965, #7285599, #7113770 的作者是我。&lt;/p&gt;
&lt;p&gt;树洞 #5836142 的作者是 &lt;a href=&quot;https://arthals.ink&quot;&gt;Arthals 同学&lt;/a&gt;，他是一个非常好的 ICS 助教。&lt;/p&gt;</content:encoded><h:img src="/_astro/qdb.CVzIg79R.jpeg"/><enclosure url="/_astro/qdb.CVzIg79R.jpeg"/></item><item><title>An Unfinished Story</title><link>https://www.lyt0112.com/blog/this_is_pku-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/this_is_pku-en</guid><description>So we beat on, boats against the current, borne back ceaselessly into the past.</description><pubDate>Thu, 03 Jul 2025 02:58:20 GMT</pubDate><content:encoded>&lt;h2&gt;An Unfinished Story&lt;/h2&gt;
&lt;p&gt;My last day as an undergraduate at Peking University felt just like the morning I first arrived four years ago. The air then was cool and filled with a vague but passionate hope for the future; today, the warmth is tinged with the sadness of parting.&lt;/p&gt;
&lt;p&gt;As I start to write, I realize the torrent of thoughts that once surged within me has crumbled into fragments. Graduation, it turns out, isn&apos;t a grand finale but a quiet exit. I never found a single label for these four years. They feel like an untitled manuscript, a hurried ending to a quest for meaning that was just beginning.&lt;/p&gt;
&lt;p&gt;Four years was long enough to eat my weight in braised chicken from the Shaoyuan canteen. Yet, it was short enough that I never managed to try breakfast anywhere but the Jiayuan and Xueyi canteens, or find time to just sit in the old library and nap between study sessions. I always thought I had all the time in the world, only to watch it slip away. These small, unfinished tasks, like un-dog-eared pages in a book, have become part of the story, reminding me that life is a journey with its share of blank spaces.&lt;/p&gt;
&lt;p&gt;On my first day, I met two upperclassmen on the winding path in front of the Qiu Deba Sports Hall. Their simple &quot;Welcome to PKU&quot; became my first real connection to this campus. The path sloped upwards, much like my own winding but ever-forward journey. Before I even started, I told myself to accept my own ordinariness and make peace with my expectations. I tried to listen to my inner voice, but was ultimately consumed by the pressure of my GPA. Peking University is vast enough to hold countless dreams; grades are just one measure of success, and by no means the only one. A person&apos;s worth should never be defined by a transcript. What matters more is finding what makes your eyes light up, what allows your voice to be heard and your presence to be felt. Making your voice heard and your efforts count is far more meaningful than exhausting yourself on a crowded track.&lt;/p&gt;
&lt;p&gt;As a small experiment in my sophomore year, I began organizing my &lt;a href=&quot;https://www.lyt0112.com/blog/course_review-zh&quot;&gt;course materials&lt;/a&gt;. I wasn&apos;t trying to build a monument, just leave a trail of footprints—a small light for those who would come after me. If it helped someone avoid a few detours on their own path, that would be the best farewell I could imagine. Maybe one day I&apos;ll fade from everyone&apos;s memory. But if, late one night, a student looks at my notes and softly reads the title, in that moment, my youth will be with them. My thoughts will have found a small piece of eternity on this campus. This endeavor was also inspired by my dear friend &lt;a href=&quot;https://arthals.ink&quot;&gt;Arthals&lt;/a&gt;, who showed me that sharing knowledge is an act of creation in itself.&lt;/p&gt;
&lt;p&gt;The time to say goodbye has come. We turn, packing the seasons of Peking University into our bags, and head toward a future without walls. Most of us will fade from the grand narratives and become ordinary people, our faces perhaps indistinct in a crowd, but our lives incredibly real.&lt;/p&gt;
&lt;p&gt;But this isn&apos;t the end of the story. Remember the young man who once gazed at the stars from Building 45B. Hold on to that stubborn refusal to compromise with the world. That is the most precious treasure in our bags. It will remind us in the storms to come that even on an ordinary path, we must create our own echo.&lt;/p&gt;
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;Destiny, it seems, has a habit of leaving clues where you least expect them. As one of the first students from the School of Information Science and Technology to check in, I was the first to step into Building 45B. After watching several floor managers come and go, by a strange twist of fate, I inherited the &quot;mantle&quot; of being the WeChat group admin for the third and fourth floors. This absurd little detail became an unexpected chapter in my story—perhaps time&apos;s own quirky way of commemorating that first morning my journey began.&lt;/p&gt;</content:encoded><h:img src="/_astro/qdb.CVzIg79R.jpeg"/><enclosure url="/_astro/qdb.CVzIg79R.jpeg"/></item><item><title>Rubik&apos;s Cube Blindfold Method and Improvement</title><link>https://www.lyt0112.com/blog/blindfold-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/blindfold-zh</guid><description>三阶魔方彳亍法盲拧逻辑详解以及一套更符合直觉, 更易于记忆的三阶魔方盲拧编码方案. </description><pubDate>Mon, 30 Jun 2025 00:13:04 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;盲拧, 作为一项复原手段, 与我们熟知的速拧有着本质的不同. 速拧依赖于观察魔方状态的实时反馈, 是一个&quot;闭环控制&quot;过程; 而盲拧则是在蒙上双眼后, 仅凭记忆执行一系列操作, 是一个&quot;开环控制&quot;过程. 这两个系统虽然逻辑迥异, 但在一些技巧上却有共通之处.&lt;/p&gt;
&lt;p&gt;盲拧入门最大的挑战之一, 就是如何将魔方的状态编码并牢记. 目前流行的&quot;彳亍法&quot;&lt;a href=&quot;%5B%E4%B8%89%E5%BE%AA%E7%8E%AF%E5%90%8C%E6%97%B6%E8%A7%A3%E5%86%B3%E4%BD%8D%E7%BD%AE%E5%92%8C%E6%96%B9%E5%90%91%EF%BC%88%E6%80%9D%E8%B7%AF%EF%BC%89%E2%80%94%E2%80%94%E5%BD%B3%E4%BA%8D%5D(http://www.mf8-china.com/forum.php?mod=viewthread&amp;#x26;tid=2101)&quot;&gt;^8&lt;/a&gt;最初是由 &lt;code&gt;彳亍&lt;/code&gt; 于 2006 年在 mf8 论坛上提出的, 其作为三循环二步法虽然强大, 但其编码对于新手而言记忆熟练负担较重. 为了解决这一痛点, 本文在现代&quot;彳亍法&quot;教程[^1]&lt;a href=&quot;%5B%E5%BD%B3%E4%BA%8D%E6%B3%95(%E4%B8%89%E5%BE%AA%E7%8E%AF%E4%BA%8C%E6%AD%A5%E7%9B%B2%E6%8B%A7%E6%B3%95)%E5%85%A5%E9%97%A8%E7%AE%80%E6%98%8E%E6%95%99%E7%A8%8B%E2%80%94%E2%80%94%E4%B8%80%E5%8F%B6%E7%9F%A5%E7%A7%8B%5D(http://bbs.mf8-china.com/forum.php?mod=viewthread&amp;#x26;tid=13404)&quot;&gt;^5&lt;/a&gt;的基础上, 设计了一套更符合直觉, 更易于记忆的编码图, 希望能为广大魔方爱好者打开盲拧世界的大门.&lt;/p&gt;
&lt;p&gt;彳亍法盲拧的基本思路可以简要概括为: 首先, 将打乱后的魔方角块和棱块的位置, 分别按照特定规则编码成两个字母字符串. 每个字符串大约包含 20 个英文字母. 记住这两个字符串后, 在蒙眼复原时, 依次按照三循环公式, 将字符串中的每一对字母所对应的块复原到正确位置. 当这两个字符串全部执行完毕, 魔方的角块和棱块也就全部复原了.&lt;/p&gt;
&lt;p&gt;因此, 与速拧相比, 学习盲拧彳亍法时主要需要掌握以下四大板块:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;熟记魔方上每个面所对应的字母编码.&lt;/li&gt;
&lt;li&gt;新的复原流程.&lt;/li&gt;
&lt;li&gt;掌握应用在字母编码上的三循环公式:
&lt;ol&gt;
&lt;li&gt;简单版本中, 我们只考虑缓冲块与顶层非对角的两个块的三循环公式, 角块需要 8 个公式, 棱块需要 18 个公式;&lt;/li&gt;
&lt;li&gt;复杂版本中, 我们考虑任意两个块和缓冲块之间的三循环公式, 角块公式多达 440 个, 棱块公式有 378 个, 总计 818 个公式, 这也是&quot;盲拧彳亍 818&quot;名称的由来.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;高效记忆这两个总长约 20 个字母的字符串, 方法如下:
&lt;ol&gt;
&lt;li&gt;将每两个字母&lt;strong&gt;联想&lt;/strong&gt;成一个词语进行记忆;&lt;/li&gt;
&lt;li&gt;用拼音（或双拼）来记忆两个字母;&lt;/li&gt;
&lt;li&gt;像记电话号码, 车牌号一样, 直接顺序顺口读下来;&lt;/li&gt;
&lt;li&gt;对于初学者, 建议全部采用联想记忆法. 当盲拧速度提升到两分钟以内时, 可以尝试&quot;记角读棱法&quot;: 用方法 2 记忆较短的角块编码, 用方法 3 记忆较长的棱块编码, 复原时优先还原棱块, 以防遗忘棱块编码.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;编码&lt;/h2&gt;
&lt;h3&gt;设计理念&lt;/h3&gt;
&lt;p&gt;参考[^2]和[^3], 我设计了一套新的编码图, 旨在融合 &lt;code&gt;Speffz&lt;/code&gt; 编码的逻辑性和中文编码的顺口性, 为初学者提供一套更易上手的盲拧记忆方案.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对比彳亍编码&lt;/strong&gt;: 传统的彳亍编码记忆点较为零散, 新手容易&quot;从入门到放弃&quot;. 我的设计更注重字母排布的规律, 降低了记忆门槛.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对比 Speffz 编码&lt;/strong&gt;: Speffz 作为国际标准, 是由 &lt;a href=&quot;https://www.speedsolving.com/wiki/index.php?title=Ville_Sepp%C3%A4nen&quot;&gt;Ville Seppänen&lt;/a&gt; 和 &lt;a href=&quot;https://www.speedsolving.com/wiki/index.php?title=Rob_Holt&quot;&gt;Rob Holt&lt;/a&gt; 于 2010 年为了方便交流提出的标准化方案, 逻辑性极佳, 但字母本身难以与中文的形象或拼音关联, 不便于中文母语者通过组词等方式记忆.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;编码详解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顶层 (U Face)&lt;/strong&gt;: 完全遵循 &lt;code&gt;Speffz&lt;/code&gt; 编码的 &lt;code&gt;A, B, C, D&lt;/code&gt; 顺序, 一组连续字母, 方便整体记忆.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前层 (F Face)&lt;/strong&gt;: 字母为 &lt;code&gt;E, F, G, X&lt;/code&gt;. 可以联想为这个面正对着&quot;我 (Me)&quot;, 所以以 &lt;code&gt;E&lt;/code&gt; 开头. &lt;code&gt;E, F, G&lt;/code&gt; 字母连续, 朗朗上口. &lt;code&gt;X&lt;/code&gt; 则作为一个特殊字母, 与右, 后, 左面的 &lt;code&gt;Y, Z, W&lt;/code&gt; 形成一组.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;右, 后, 左层 (R, B, L Faces)&lt;/strong&gt;: 首字母分别为 &lt;code&gt;R, O, L&lt;/code&gt;, 可记为 &lt;code&gt;Right&lt;/code&gt; (右), &lt;code&gt;Opposite&lt;/code&gt; (前层的对面), &lt;code&gt;Left&lt;/code&gt; (左). 每个面同样搭配了易于记诵的连续字母.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底层 (D Face)&lt;/strong&gt;: 字母为 &lt;code&gt;H, I, J, K&lt;/code&gt;, 一组连续字母, 方便整体记忆.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;编码与复原流程&lt;/h2&gt;
&lt;p&gt;前文已经简要介绍了盲拧的基本思路, 下面将详细梳理整个复原流程, 帮助大家更清晰地理解每一步的操作.&lt;/p&gt;
&lt;p&gt;由于实际操作中举例讲解非常重要, 因此强烈建议大家配合 &lt;a href=&quot;https://space.bilibili.com/243671525/lists/4331101&quot;&gt;彳亍法盲拧教程&lt;/a&gt; 的视频学习, 视频中的演示和讲解非常直观易懂.&lt;/p&gt;
&lt;h3&gt;编码——顺序&lt;/h3&gt;
&lt;p&gt;编码的起点是缓冲块的上方: 对于棱块来说, 是 &lt;code&gt;UF&lt;/code&gt; 棱块的 &lt;code&gt;U&lt;/code&gt; 面; 对于角块, 则是 &lt;code&gt;UFR&lt;/code&gt; 角块的 &lt;code&gt;U&lt;/code&gt; 面. 我们通常先对棱块进行编码, 角块的编码原理与棱块类似, 这里以棱块为例进行说明.&lt;/p&gt;
&lt;p&gt;假设当前 &lt;code&gt;UL&lt;/code&gt; 棱块的 &lt;code&gt;L&lt;/code&gt; 面正好位于缓冲块 &lt;code&gt;UF&lt;/code&gt; 的 &lt;code&gt;U&lt;/code&gt; 面上, 那么第一个编码就是 &lt;code&gt;L&lt;/code&gt;. 这是因为在我设计的编码方案中, &lt;code&gt;UL&lt;/code&gt; 棱块的 &lt;code&gt;L&lt;/code&gt; 面对应字母 &lt;code&gt;L&lt;/code&gt;. 这个编码的含义是: 现在处于 &lt;code&gt;C&lt;/code&gt; 位置的棱块, 现在应该被送往 &lt;code&gt;L&lt;/code&gt; 位置.&lt;/p&gt;
&lt;p&gt;需要注意的是, 字母编码是固定的, 不会随着魔方的旋转而变化, 可以理解为一个静止的坐标系. 比如“&lt;code&gt;C&lt;/code&gt; 这个字母前往 &lt;code&gt;L&lt;/code&gt;”, 就好比在坐标系中, 一个标注了 0 的小球应该从位置 5 移动到位置 0, 复原这个小球的编码就是 0.&lt;/p&gt;
&lt;p&gt;接下来, 观察 &lt;code&gt;L&lt;/code&gt; 位置上的棱块颜色, 判断它应该去往哪个位置. 如果它应该前往 &lt;code&gt;B&lt;/code&gt; 位置, 那么第二个编码就是 &lt;code&gt;B&lt;/code&gt;. 依此类推, 直到所有棱块都被编码完毕.&lt;/p&gt;
&lt;h3&gt;编码——奇偶校验 (Parity Check)&lt;/h3&gt;
&lt;p&gt;当编码长度为奇数时 (棱块和角块编码长度必然同奇偶), 需要进行奇偶校验. 方法很简单: 只需在棱块和角块编码后各加一个 &lt;code&gt;B&lt;/code&gt;, 并在复原最后执行一次 &lt;code&gt;Jb Perm&lt;/code&gt; 公式即可.&lt;/p&gt;
&lt;h3&gt;编码——翻色&lt;/h3&gt;
&lt;p&gt;如果打乱后有某个棱块或角块虽然位置正确但方向错误, 那么就需要进行翻色操作.&lt;/p&gt;
&lt;p&gt;如果需要翻色的块数为偶数, 则这些块一起翻色; 如果为奇数, 则这些块和缓冲块一起翻色.&lt;/p&gt;
&lt;h3&gt;编码——小循环&lt;/h3&gt;
&lt;p&gt;小循环的概念相对抽象, 建议大家先观看&lt;a href=&quot;https://www.bilibili.com/video/BV1TE41187AG&quot;&gt;实例视频&lt;/a&gt;，或亲自尝试构造一个顺序编码提前结束的小循环案例, 以便更好地理解.&lt;/p&gt;
&lt;p&gt;在实际编码过程中, 我们常常会遇到这样一种情况: 顺序编码时, 某个块需要回到缓冲块的位置 (即 &lt;code&gt;C&lt;/code&gt; 位置或 &lt;code&gt;E&lt;/code&gt; 位置), 但此时还有其他块尚未被编码. 此时, 顺序编码会提前&quot;断开&quot;, 形成一个小循环. 为了继续完成编码, 我们需要主动选择一个尚未被编码的块, 作为新的&quot;起点&quot;, 继续顺序编码, 直到所有块都被编码为止.&lt;/p&gt;
&lt;p&gt;色相: &lt;code&gt;UD&lt;/code&gt; 面为高级色相, &lt;code&gt;FR&lt;/code&gt; 面为中级色相, &lt;code&gt;LB&lt;/code&gt; 面为低级色相. 每个棱块都位于两个不同级别的面上, 因此每个块都拥有一个高级色相和一个低级色相.&lt;/p&gt;
&lt;p&gt;色相相同: 指的是两个面同为高级色相或同为低级色相.&lt;/p&gt;
&lt;p&gt;如何选择新的起点 (即如何打破小循环) 是许多初学者常见的疑惑, 我们给出选择原则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;棱块: 选择与当前缓冲块&lt;strong&gt;色相相同&lt;/strong&gt;的块作为下一个编码目标.&lt;/li&gt;
&lt;li&gt;角块: 可以任意选择一个未编码的位置继续编码, 无需遵守色相一致原则.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体操作时, 如果顺序编码遇到某个块应回到缓冲块的 &lt;code&gt;C&lt;/code&gt; 位置, 应优先选择一个尚未编码、拥有高级色相的块作为下一个编码目标; 如果遇到应回到 &lt;code&gt;E&lt;/code&gt; 位置, 则优先选择拥有低级色相的块. 这样做可以避免在有奇偶校验且编码以 &lt;code&gt;UR&lt;/code&gt; 块结尾时, 出现额外的翻色情况.&lt;/p&gt;
&lt;p&gt;大多数情况下, 借位色相是否一致并不会影响结果, 只有在编码为奇数个 (即需要奇偶校验) 且棱块最后一个编码落在 &lt;code&gt;B&lt;/code&gt; 或 &lt;code&gt;R&lt;/code&gt; 时才会有区别.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果此时其余棱块没有翻色或是偶数个翻色, 按色相借位, 最后的编码是 &lt;code&gt;B&lt;/code&gt;, 不按色相借位则是 &lt;code&gt;R&lt;/code&gt;. 这样会导致缓冲块和 &lt;code&gt;UR&lt;/code&gt; 块&lt;strong&gt;额外&lt;/strong&gt;产生一次翻色, 这不是打乱本身造成的, 而是因为没有遵守色相一致原则导致的, 造成了额外的工作量, 这是我们希望避免的.&lt;/li&gt;
&lt;li&gt;如果是奇数个棱块需要翻色, 按色相借位同样会使最后一个编码落到 &lt;code&gt;R&lt;/code&gt;, 同样会有翻色, 但此时是缓冲块和另外奇数个翻色块共同翻色. 由于额外翻色为奇数, 无论如何都得进行翻色, 这是打乱本身造成的, 没有造成额外的工作量, 可以接受.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举例说明:&lt;/p&gt;
&lt;p&gt;红顶绿前打乱: &lt;code&gt;R U R&apos; F&apos; R U R&apos; U&apos; R&apos; F R2 U&apos; R&apos; U&apos;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个打乱的盲拧编码长度为奇数, 存在奇偶校验, 并且最后一个编码必然为 &lt;code&gt;B&lt;/code&gt; 或 &lt;code&gt;R&lt;/code&gt;, 满足出现特殊情况的条件.&lt;/p&gt;
&lt;p&gt;如果遵守色相一致原则, 棱块盲拧编码为: &lt;code&gt;BZ B&lt;/code&gt;, 最后一个 &lt;code&gt;B&lt;/code&gt; 和奇偶校验加入的额外 &lt;code&gt;B&lt;/code&gt; 互相抵消, 因此只需做一个 &lt;code&gt;BZ&lt;/code&gt; 公式.&lt;/p&gt;
&lt;p&gt;如果不遵守色相一致原则, 棱块盲拧编码为: &lt;code&gt;RS R&lt;/code&gt;, 最后一个 &lt;code&gt;R&lt;/code&gt; 和奇偶校验加入的额外 &lt;code&gt;B&lt;/code&gt; 互相抵消, 因此只需做一个 &lt;code&gt;RS&lt;/code&gt; 公式.&lt;/p&gt;
&lt;p&gt;从做完公式的结果可以看到, 遵守色相一致原则不会产生额外翻色, 而不遵守则会多一次翻色.&lt;/p&gt;
&lt;h3&gt;复原——应用公式&lt;/h3&gt;
&lt;p&gt;编码完成后, 就可以开始应用公式进行复原.&lt;/p&gt;
&lt;p&gt;例如, 若两个编码为 &lt;code&gt;DB&lt;/code&gt;, 则直接使用 &lt;code&gt;M2 U&apos; M U2 M&apos; U&apos; M2&lt;/code&gt; 公式进行复原.&lt;/p&gt;
&lt;h3&gt;复原——Set Up and Reverse&lt;/h3&gt;
&lt;p&gt;如果没有记住任意两块之间的复原公式 (共 818 条), 可以只记 26 条基础公式, 配合 Set Up and Reverse 技巧完成复原.&lt;/p&gt;
&lt;p&gt;Set Up and Reverse 的原理是: 先将目标两块通过预处理 (Set Up) 转移到 &lt;code&gt;UL&lt;/code&gt; 和 &lt;code&gt;UR&lt;/code&gt; 位置, 应用已记住的公式进行复原, 最后再按相反步骤 (Reverse) 将两块归位. 这个过程类似于&quot;入栈&quot;和&quot;出栈&quot;.&lt;/p&gt;
&lt;p&gt;有两个必须遵守的规则和一个可以不遵守的建议:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set Up 过程中, 不能在转动某个面时同时移动两个需要被 Set Up 的块, 否则可能导致额外的翻色.&lt;/li&gt;
&lt;li&gt;Set Up 过程中, 不能移动缓冲块, 否则公式就不再针对当前缓冲块 (除非你清楚缓冲块的新位置并能应用相应公式).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;建议先 Set Up 一对编码中的第一个块, 再 Set Up 第二个块. 这样更有逻辑性, 也更不容易出错.&lt;/p&gt;
&lt;h2&gt;三循环公式&lt;/h2&gt;
&lt;p&gt;顾名思义, 三循环公式就是调换任意三个块 (棱块或角块) 而不影响其他块的公式.&lt;/p&gt;
&lt;p&gt;参考&lt;a href=&quot;%5B%E5%BD%B3%E4%BA%8D%E6%B3%95%E7%9B%B2%E6%8B%A7%E6%95%99%E7%A8%8B(10)%E2%80%94%E2%80%94%E5%9F%BA%E7%A1%80%E5%85%AC%E5%BC%8F%E2%80%94%E2%80%94%E4%BE%9D%E6%97%A7%E6%98%AF%E4%B8%80%E4%B9%9D%E5%9B%9B%5D(https://www.bilibili.com/video/BV1TE411b7Xe)&quot;&gt;^7&lt;/a&gt;, 这里给出简单版本的公式, 只包含 &lt;code&gt;UL&lt;/code&gt;,&lt;code&gt;UF&lt;/code&gt;,&lt;code&gt;UR&lt;/code&gt; 三个棱块互换和 &lt;code&gt;UFL&lt;/code&gt;,&lt;code&gt;UFR&lt;/code&gt;,&lt;code&gt;UBR&lt;/code&gt; 三个角块互换的公式. 复杂版本的公式可以参考&lt;a href=&quot;https://www.bilibili.com/opus/355029676093901363&quot;&gt;一九四的三盲818公式&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;E 层&lt;/strong&gt;: U 层和 D 层所夹的中层, 转动方向同 D 层.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S 层&lt;/strong&gt;: F 层和 B 层所夹的中层, 转动方向同 F 层.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;M 层&lt;/strong&gt;: L 层和 R 层所夹的中层, 转动方向同 L 层.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;棱块公式&lt;/h3&gt;
&lt;p&gt;缓冲块为 &lt;code&gt;UF&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;其中的备选公式 1 比较适合初学者, 备选公式 2 比较进阶.&lt;/p&gt;
&lt;p&gt;| 目标位置 | 备选公式 1 (初学) | 备选公式 2 (进阶) |
| :---: | :--- | :--- |
| DB | &lt;code&gt;R2 U R U R&apos; U&apos; R&apos; U&apos; R&apos; U R&apos;&lt;/code&gt; | &lt;code&gt;M2 U&apos; M U2 M&apos; U&apos; M2&lt;/code&gt; |
| BD | &lt;code&gt;R U&apos; R U R U R U&apos; R&apos; U&apos; R2&lt;/code&gt; | &lt;code&gt;M2 U M U2 M&apos; U M2&lt;/code&gt; |
| LB | &lt;code&gt;U&apos; r U R&apos; U&apos; r&apos; R U R U&apos; R&apos; U&lt;/code&gt; | &lt;code&gt;L F&apos; L&apos; S&apos; L F L&apos; S&lt;/code&gt; |
| BL | &lt;code&gt;U&apos; R U R&apos; U&apos; M&apos; U R U&apos; r&apos; U&lt;/code&gt; | &lt;code&gt;S&apos; L F&apos; L&apos; S L F L&apos;&lt;/code&gt; |
| LR | &lt;code&gt;M U M&apos; U2 M U M&apos;&lt;/code&gt; | |
| RL | &lt;code&gt;M U&apos; M&apos; U2 M U&apos; M&apos;&lt;/code&gt; | |
| DR | &lt;code&gt;U&apos; R&apos; U&apos; R U M U&apos; R&apos; U r&lt;/code&gt; | &lt;code&gt;S R&apos; F R S&apos; R&apos; F&apos; R&lt;/code&gt; |
| RD | &lt;code&gt;U&apos; r&apos; U&apos; R U M&apos; U&apos; R&apos; U R&lt;/code&gt; | &lt;code&gt;R&apos; F R S R&apos; F&apos; R S&apos;&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;对棱翻色公式: &lt;code&gt;M&apos; U M&apos; U M&apos; U2 M U M U M U2&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;角块公式&lt;/h3&gt;
&lt;p&gt;缓冲块为 &lt;code&gt;UFR&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;| 目标位置 | 公式 |
| :---: | :--- |
| DB | &lt;code&gt;x&apos; R2 D2 R&apos; U&apos; R D2 R&apos; U R&apos; x&lt;/code&gt; |
| BD | &lt;code&gt;x&apos; R U&apos; R D2 R&apos; U R D2 R2 x&lt;/code&gt; |
| DO | &lt;code&gt;R&apos; U&apos; D&apos; R&apos; D R U R&apos; D&apos; R D R&lt;/code&gt; |
| OD | &lt;code&gt;R&apos; D&apos; R&apos; D R U&apos; R&apos; D&apos; R U D R&lt;/code&gt; |
| DS | &lt;code&gt;U&apos; R&apos; U2 R&apos; D&apos; R U2 R&apos; D R2 U&lt;/code&gt; |
| SD | &lt;code&gt;U&apos; R2 D&apos; R U2 R&apos; D R U2 R U&lt;/code&gt; |
| EB | &lt;code&gt;R2 D R&apos; U2 R D&apos; R&apos; U2 R&apos;&lt;/code&gt; |
| BE | &lt;code&gt;R U2 R D R&apos; U2 R D&apos; R2&lt;/code&gt; |
| EO | &lt;code&gt;R U D&apos; R&apos; D&apos; R U2 R&apos; D R D U R&apos;&lt;/code&gt; |
| OE | &lt;code&gt;R U&apos; D&apos; R&apos; D&apos; R U2 R&apos; D R D U&apos; R&apos;&lt;/code&gt; |
| ES | &lt;code&gt;R&apos; U&apos; R2 D&apos; R2 D R2 U R2 D&apos; R2 D R&apos;&lt;/code&gt; |
| SE | &lt;code&gt;R D&apos; R2 D R2 U&apos; R2 D&apos; R2 D R2 U R&lt;/code&gt; |
| MB | &lt;code&gt;x&apos; R U R&apos; D R U&apos; R&apos; D&apos; x&lt;/code&gt; |
| BM | &lt;code&gt;x&apos; D R U R&apos; D&apos; R U&apos; R&apos; x&lt;/code&gt; |
| MO | &lt;code&gt;R&apos; U L U&apos; R U L&apos; U&apos;&lt;/code&gt; |
| OM | &lt;code&gt;U L U&apos; R&apos; U L&apos; U&apos; R&lt;/code&gt; |
| MS | &lt;code&gt;F&apos; U R&apos; D R U2 R&apos; D&apos; R U F&lt;/code&gt; |
| SM | &lt;code&gt;F&apos; U&apos; R&apos; D R U2 R&apos; D&apos; R U&apos; F&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;角块翻色采用 &lt;code&gt;(R U R&apos; U&apos;) D (U R U&apos; R&apos;) D&apos;&lt;/code&gt; 等类似的转换机 (Commutator) 翻色法.&lt;/p&gt;
&lt;p&gt;详情可参考视频: &lt;a href=&quot;https://www.bilibili.com/video/BV1RE411t7zr/&quot;&gt;彳亍法教程——关于翻色&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;奇偶校验&lt;/h3&gt;
&lt;p&gt;当编码中出现奇数个字母时, 说明出现了奇偶校验问题, 需要额外执行下述的其中一个公式来修正 (哪一个都行).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;棱块缓冲与B交换, 角块缓冲与B交换&lt;/strong&gt;: &lt;code&gt;R U R&apos; F&apos; R U R&apos; U&apos; R&apos; F R2 U&apos; R&apos; U&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;棱块缓冲与A交换, 角块缓冲与D交换&lt;/strong&gt;: &lt;code&gt;U&apos; R U R&apos; U&apos; R&apos; F R2 U&apos; R&apos; U&apos; R U R&apos; F&apos; U&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;记忆方法&lt;/h2&gt;
&lt;p&gt;我采用的是小鹤双拼记忆法[^4], 这种方法与双拼输入法的原理类似, 即用一个汉字来表示两个字母. 例如, 如果你的盲拧编码只有棱块的 &lt;code&gt;ID&lt;/code&gt;, 在双拼输入法中可以对应为 &lt;code&gt;拆&lt;/code&gt; 或 &lt;code&gt;柴&lt;/code&gt; 字, 因此你只需要记住这个汉字即可.&lt;/p&gt;
&lt;p&gt;当然, 有些字母组合可能没有对应的汉字, 这时我们可以用其他拼音方式来替代. 例如, 编码 &lt;code&gt;C&lt;/code&gt; 代表缓冲块, 不会被编码, 所以在没有对应汉字的情况下, 可以将双拼的第一个或第二个音节替换为 &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;具体的设计细节可以参考 &lt;a href=&quot;https://www.cuberoot.me/cuberoot-letter-scheme-and-syllable/&quot;&gt;CubeRoot 双拼记忆法&lt;/a&gt;, 这里不再赘述.&lt;/p&gt;
&lt;p&gt;双拼记忆法的优点在于, 它将记忆量减半, 相比拼音法更加高效. 不过, 这种方法更适合平时就习惯使用双拼输入法的人. 如果你平时并不熟悉双拼, 可能需要花费较多时间先掌握双拼编码. 因此, 如果你不常用双拼输入法, 我更推荐你采用联想法或拼音法进行记忆.&lt;/p&gt;
&lt;p&gt;如果想了解更多相关教程, 可以在 B 站搜索 &lt;code&gt;盲拧记忆&lt;/code&gt; 等关键词进行查找.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^2]: &lt;a href=&quot;https://www.bilibili.com/video/BV1TUKszEEVT/&quot;&gt;我发明了一种新的盲拧编码图, 或许是最符合中文思路的盲拧编码图——依旧是一九四&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^3]: &lt;a href=&quot;https://www.bilibili.com/video/BV1NaK5zJEQV/&quot;&gt;一种新的盲拧编码方式分享, 或许是容易记忆的盲拧编码图（回应194大神的编码视频）——亢泽超kzc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^4]: &lt;a href=&quot;https://www.cuberoot.me/cuberoot-letter-scheme-and-syllable/&quot;&gt;CubeRoot Scheme, Syllable——CubeRoot&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^5]: &lt;a href=&quot;https://www.bilibili.com/read/cv17648560/&quot;&gt;盲拧魔方彳亍法入门 文字版教程——天方魔&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/blindfold.Dd68ErZI.png"/><enclosure url="/_astro/blindfold.Dd68ErZI.png"/></item><item><title>Course Review for PKU Students</title><link>https://www.lyt0112.com/blog/course_review-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/course_review-zh</guid><description>Helpful Course Information for PKU Students</description><pubDate>Sun, 22 Jun 2025 12:17:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import GithubCard from &apos;@/components/advanced/GithubCard.astro&apos;&lt;/p&gt;
&lt;p&gt;欢迎来到博主的课程评测页面!&lt;/p&gt;
&lt;p&gt;使用电脑等宽屏设备浏览, 可以在右侧看到详细目录; 使用手机等移动设备, 点击右下角按钮同样可以方便地展开目录.&lt;/p&gt;
&lt;p&gt;每门课程的资料链接都放在对应的介绍后面, 欢迎根据需要查阅.&lt;/p&gt;
&lt;p&gt;如果这些内容对你有所帮助, 非常欢迎在下方评论区留下你的反馈和建议, 这不仅能帮助到我, 也能帮助到更多同学. 同时也欢迎给我的 GitHub 仓库点个 star 🌟, 你的支持是我持续更新的动力!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;祝大家选课顺利, 学业进步, 收获满满!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Update on 2025.12.29:&lt;/p&gt;
&lt;p&gt;又到了一年一度的 ICS 期末季, 选择这门课的同学们真的辛苦了. 想在这里分享一些感想.&lt;/p&gt;
&lt;p&gt;从现在的视角来看, 很多课程资料, 尤其是 Lab 代码, 已经不再具有强的指导意义, 因为各种 coding agent 已能高效完成定义明确的课程 Lab 编程任务. 比如, &lt;a href=&quot;https://github.com/ICUlizhi/VibePkuMinic&quot;&gt;Vibe Coding: PKU Compiler Principles Lab&lt;/a&gt; 就展示了, 即使是难度最高的编译原理 Lab, 也可以完全交给 AI 来完成. 这与我在科研中的实践也相互印证.&lt;/p&gt;
&lt;p&gt;因此, 最简单的, &lt;strong&gt;一定要学会使用 coding agent 写代码&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;其次, 在这样的环境下, 我们如何保持竞争力？这并不是几句话能说清的. 但正如那篇文章的作者所言：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当 Implementation 的边际成本趋近于零, Engineering 的价值便会从 &quot;How to build&quot; 向 &quot;What to build&quot; 剧烈迁移.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;希望大家能在这样的环境中找到自己真正喜欢的事情, 并全身心投入. 这才是大学阶段最值得去做的事.&lt;/p&gt;
&lt;p&gt;Update on 2025.07.03:&lt;/p&gt;
&lt;p&gt;我在 &lt;a href=&quot;this_is_pku-zh&quot;&gt;这篇文章&lt;/a&gt; 中分享了整理课程资料的心路历程，欢迎感兴趣的同学阅读！&lt;/p&gt;
&lt;p&gt;Update on 2025.06.22:&lt;/p&gt;
&lt;p&gt;最后一科分析性英语写作 (一门非常好的不用上课的 C 级英语课) 卡着毕业生出分 DDL 出分了, 本科 150 学分课程全部完成, 毕业快乐!&lt;/p&gt;
&lt;p&gt;Update on 2025.04.18:&lt;/p&gt;
&lt;p&gt;这里有一个北大同学开发的课程资料网站: &lt;a href=&quot;https://pkuhub.cn&quot;&gt;PKUHUB&lt;/a&gt;, 在开发团队的邀请下我上传了部分资料, 也可以看到别的同学上传的资料, 非常好的项目!&lt;/p&gt;
&lt;h2&gt;春秋学期均开设的课程&lt;/h2&gt;
&lt;h3&gt;编译原理&lt;/h3&gt;
&lt;p&gt;刘先华, 王迪, 张路 2024秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大四了就去过三次课, 但是我感觉讲的还挺清楚的, 但是如果只看 slides 可能会很迷惑, 尤其是语法分析和 SDT 部分, 所以建议要么看书作为补充, 要么看看下面资料中的笔记.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;十五次作业, 九个 lab. 有的时候一周两次作业, 有的时候一周没有作业.&lt;/p&gt;
&lt;p&gt;建议在没有绩点压力的大四选这门课, 就可以不写 Lv9 了, 因为 Lv9 非常浪费时间并且占所有 Lab 的 27% 的分数, 也就是总评的 8 分, 如果在绩点压力下完成所有 Lab 会浪费很多时间.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最后一节课有三十分钟的 Lab 小测, 开卷可以查阅任何资料, 我们的小测问题是:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;目前完成的程度及功能得分.&lt;/li&gt;
&lt;li&gt;简介你的编译器是如何处理符号表及嵌套作用域的.&lt;/li&gt;
&lt;li&gt;简介你的寄存器分配方案的设计和实现.&lt;/li&gt;
&lt;li&gt;SysY 文法是否有二义性问题, 你的编译器是如何解决的.&lt;/li&gt;
&lt;li&gt;简述 KoopalR (或你所使用的 IR) 的主要特点.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;期中期末都有考试, 期中之前好好学习了, 每周看 slides ; 期中之后没看过, 期末考试前两天速成, 对往年题做 few-shot learning 可以做到期中 90 分, 期末也差不多, 同样写写往年题就行了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Compiler_Principles-2024Fall-PKU&quot;&gt;编译原理资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Compiler_Principles_Lab-2024Fall-PKU&quot;&gt;编译原理 Lab 代码&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/compiler_principles_lab_note-zh&quot;&gt;编译原理 Lab 笔记&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://arthals.ink/tags/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86&quot;&gt;课程知识笔记和 Lab 注意事项 from Arthals&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2025.01.26: 81&lt;/p&gt;
&lt;p&gt;大四满足了 🎊&lt;/p&gt;
&lt;h3&gt;操作系统&lt;/h3&gt;
&lt;p&gt;陈向群 2025春&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;去了 0 次 + 最后一节课的 10 分钟.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;xv6 实验&lt;/li&gt;
&lt;li&gt;四次书面作业&lt;/li&gt;
&lt;li&gt;二十几次上课小测&lt;/li&gt;
&lt;li&gt;一次期中考试 (半开卷, 可以带五张正反面 A4 纸)&lt;/li&gt;
&lt;li&gt;一次期末考试&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中考试:&lt;/p&gt;
&lt;p&gt;考到第六章虚拟内存技术.&lt;/p&gt;
&lt;p&gt;非常抽象的事情是考试方式阴晴不定, 我了解到的信息 (大概是这样):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2023年春季学期没有期中考试&lt;/li&gt;
&lt;li&gt;2023年秋季学期期中考试闭卷&lt;/li&gt;
&lt;li&gt;2024年春季学期期中考试闭卷&lt;/li&gt;
&lt;li&gt;2024年秋季学期期中考试完全开卷, 可以带任意多资料&lt;/li&gt;
&lt;li&gt;2025年春季学期期中考试部分开卷, 可以带 5 张 A4 纸&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;期末考试:&lt;/p&gt;
&lt;p&gt;这两年期末老师提到必考&lt;a href=&quot;https://github.com/EmptyBlueBox/Operating_Systems-2025Spring-PKU/blob/main/Resource/%E6%80%9D%E8%80%83%E9%A2%98.md#02-unix%E5%92%8Cfat16%E5%B8%83%E5%B1%80%E6%A8%A1%E6%8B%9F&quot;&gt;UNIX和FAT16布局模拟&lt;/a&gt;, 上课也仔细讲了这道题, 如果没时间可以看看最后两次课的内容, &lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_10-zh&quot;&gt;经典的同步问题&lt;/a&gt; 和 &lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_11-zh&quot;&gt;死锁&lt;/a&gt; 的内容.&lt;/p&gt;
&lt;p&gt;期末考试题目回忆:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;举一个使用了 &lt;code&gt;mmap&lt;/code&gt; 的例子, 并解释这样做有什么好处, 使用了什么数据结构&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Operating_Systems-2025Spring-PKU/blob/main/Resource/%E8%AF%BE%E5%A0%82%E5%B0%8F%E6%B5%8B-%E6%9C%9F%E4%B8%AD%E5%89%8D.md&quot;&gt;期中前课堂小测&lt;/a&gt;的最后一题
&lt;ol&gt;
&lt;li&gt;询问什么时候会把页面从工作集中置换到空闲页面链表&lt;/li&gt;
&lt;li&gt;把修改的页面先放到修改页面链表再写回 (延迟写) 有什么好处&lt;/li&gt;
&lt;li&gt;什么是软缺页异常&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;就是上面这个 UNIX 文件系统布局模拟, 但是用了两级目录, 然后提问某个文件读取其中一段访问了几个数据块&lt;/li&gt;
&lt;li&gt;死锁
&lt;ol&gt;
&lt;li&gt;有哪些方法避免死锁&lt;/li&gt;
&lt;li&gt;哲学家就餐问题怎么使用有序资源申请解决&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;按下键盘上的 &lt;code&gt;Ctrl+C&lt;/code&gt; 后发生了什么, 给了大约20个选项排序, 包括陷入内核的时候要并发调度到别的进程&lt;/li&gt;
&lt;li&gt;同步问题
&lt;ol&gt;
&lt;li&gt;用信号量解决三峡大坝船闸问题&lt;/li&gt;
&lt;li&gt;用管程解决生产者-消费者问题, 并说明你用的是 Hoare 管程还是 MESA 管程&lt;/li&gt;
&lt;li&gt;用信号量管理共享资源, 询问 &quot;I&apos;ll Do it for You&quot; 是否是正确解法, 然后给 &quot;Pass the Baton&quot; 解法填空&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;xv6 代码题 (主要是跟作业相关的)
&lt;ol&gt;
&lt;li&gt;解释 &lt;code&gt;uvmalloc&lt;/code&gt; 怎么实现的, 主要是解释其中的 &lt;code&gt;for&lt;/code&gt; 循环在做什么&lt;/li&gt;
&lt;li&gt;解释为什么要获取 &lt;code&gt;bcache&lt;/code&gt; 的锁, 然后解释磁盘块高速缓存怎么实现 &lt;code&gt;LRU&lt;/code&gt; 算法的&lt;/li&gt;
&lt;li&gt;从管道写入的函数, 具体记不清了&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_01-zh&quot;&gt;01. 操作系统概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_02-zh&quot;&gt;02. 中断异常机制&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_03-zh&quot;&gt;03. 进程线程模型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_04-zh&quot;&gt;04. 进程线程调度&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_05-zh&quot;&gt;05. 内存管理概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_06-zh&quot;&gt;06. 虚拟内存技术&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_07-zh&quot;&gt;07. 文件系统 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_08-zh&quot;&gt;08. 文件系统 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_09-zh&quot;&gt;09. 并发机制&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_10-zh&quot;&gt;10. 经典的同步问题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/operating_systems_note_11-zh&quot;&gt;11. 死锁&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Operating_Systems-2025Spring-PKU&quot;&gt;操作系统资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;课程笔记&lt;/li&gt;
&lt;li&gt;课程作业&lt;/li&gt;
&lt;li&gt;Lab 代码 (下面这个仓库作为 submodule)&lt;/li&gt;
&lt;li&gt;考试往年题以及解答&lt;/li&gt;
&lt;li&gt;课堂小测以及解答&lt;/li&gt;
&lt;li&gt;思考题以及解答&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Operating_Systems_Lab-2025Spring-PKU&quot;&gt;操作系统 Lab 代码仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Lab 代码&lt;/li&gt;
&lt;li&gt;Lab 实验文档&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;80&lt;/p&gt;
&lt;p&gt;终究还是给了 80 而不是 80- 吗, 哈基cnn, 你这家伙......&lt;/p&gt;
&lt;h3&gt;射箭&lt;/h3&gt;
&lt;p&gt;张冰 2024秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;超难选超好玩给分超好体育课.&lt;/p&gt;
&lt;p&gt; 这个博主射箭超厉害 &lt;/p&gt;
&lt;p&gt; 最后一节课的有趣射箭小游戏, 第一名有奖牌 &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只有期末理论方面的一篇手写论文, 和射箭相关写多少字都行, 我就写了两段话半张 A4 纸.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期末会有两节课的射箭排位赛, 具体方式就是每节课每人射 8 组箭, 前两组用作热身不算成绩, 每组箭 6 枝, 两节课总共算 72 枝箭的成绩, 最后全班按照总环数排名.&lt;/p&gt;
&lt;p&gt;基本上 650 环可以拿到第一名, 博主 610 环第四名.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;无&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2024.12.30: 97&lt;/p&gt;
&lt;p&gt;疑似体测扣了 3 分, 射箭本体满分...🌚...&lt;/p&gt;
&lt;h2&gt;秋季学期课程&lt;/h2&gt;
&lt;h3&gt;信息安全引论&lt;/h3&gt;
&lt;p&gt;王昭 2024秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大四专业选修凑学分课, 疑似没听过. Slides 也非常非常极尽迷惑之事, 根本看不明白, 感觉不如看课本, 但是课本太多了, 所以建议前几节课学学后面的就不用学了, 尤其是期中之后的知识考试涉及不多.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三次书面作业, 六次小测, 两次作业小测, 四次课堂小测, 但是课堂小测可以下来问同学密码是多少然后补测.&lt;/p&gt;
&lt;p&gt;一次写代码的 Lab, 若干次在一个在线平台上的 Lab, 第一个 Lab 需要费点时间, 后面的 Lab 比较简单, 一个小时就能做一个.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有期中考试, 期末考试 60% 为作业题, 大四同学好好复习作业题和小测题即可, 在仓库里有详细解析的作业题和小测题自取~&lt;/p&gt;
&lt;p&gt;期末题型为填空, 判断, 选择, 简答, 大题, 体感来讲如果好好复习了作业题和小测题确实至少能拿 60 分, 博主体感正经写了 80 分左右的题目, 剩下 20 分就瞎写了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Information_Security-2024Fall-PKU&quot;&gt;信息安全引论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作业及解答&lt;/li&gt;
&lt;li&gt;期末复习选择题总结&lt;/li&gt;
&lt;li&gt;Lab 解答&lt;/li&gt;
&lt;li&gt;小测题, 不过已经总结进期末复习选择题总结里了&lt;/li&gt;
&lt;li&gt;课本&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2025.01.16: 84&lt;/p&gt;
&lt;p&gt;大四满足了 🎊&lt;/p&gt;
&lt;h3&gt;强化学习&lt;/h3&gt;
&lt;p&gt;李文新 2024秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;大四就去过两次课, 感觉不如看 slides, 但是上课回答问题可以加分, 最高加三分. 同时时不时就有传纸条签到的环节, 但是完全可以让同学帮忙写姓名学号.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;八次作业, 一次大作业, 一次课程大作业 Presentation.&lt;/p&gt;
&lt;p&gt;每次小作业差不多花一两个小时, 就是复现一些经典的 RL 算法, 有的调参还挺烦的. 其中最后一个作业是在开悟平台上的控制 agent 走迷宫, 只需要调整 reward 然后续训就能达到 1430 左右的分数.&lt;/p&gt;
&lt;p&gt;大作业是在四个项目里选择, 分别是开悟平台王者1v1对战, 国标麻将, 双升, 和掼蛋的强化学习模型训练, 每个大作业按照跟同学的对战胜负排名天梯给分. 建议选择王者1v1对战, 因为可以复用最后一次小作业的经验, 并且不用修改代码就能跑起来, 最后只需要调整 reward 和模型架构即可.&lt;/p&gt;
&lt;p&gt;大作业 Presentation 就是每组上台汇报自己的 RL agent 训练经验, 汇报持续两周, 第一周汇报的同学因为训练时间更少所以有一分加分.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有期中考试, 期末考试 60 道选择题. 拟合小测题和往年题即可.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Reinforcement_Learning-2024Fall-PKU&quot;&gt;强化学习资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;期末复习总结&lt;/li&gt;
&lt;li&gt;课程作业及解答&lt;/li&gt;
&lt;li&gt;小测题&lt;/li&gt;
&lt;li&gt;课本&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;89&lt;/p&gt;
&lt;p&gt;给分吹爆, 没有任何额外加分都能给大四这么高, 优秀率 60%.&lt;/p&gt;
&lt;p&gt;来自树洞 7073734:&lt;/p&gt;
&lt;p&gt;学长学长, 你的地概确实很强, 但还是太吃优秀率限制了, 有没有什么更加简单又好拿分的课程推荐一下吗？&lt;/p&gt;
&lt;p&gt;有的兄弟！有的！像这么好的课程北大当然是不止一个了, 一共有九个, 当时当前学期t0.5的优质课程, 随便选上一个都能够轻松彩虹, 如果九门都选上那你的院系排名将会冲顶, 如果九门课都像学长一样拿到满分的水平, 那我可以说你的成绩可以直接冲五四奖学金都是没有问题的了.&lt;/p&gt;
&lt;p&gt;2025年, zkc教员宣告水课地概已不复存在, 考试重点不再勾画, 优秀率也不再给满, 新形势下, 老九门课程讲何去何从？我们还有没有机会重铸彩虹荣光？&lt;/p&gt;
&lt;p&gt;有的兄弟！有的！我推荐你来选嘻嘻科学技术学院课程：强化学习. 本课程由李文新教授讲授, 课堂幽默风趣互动良多愉快有加分, 且加分有上限无内卷, 作业两周一个轻松容易, 不考勤有慕课复习无压力, 考核内容有趣不紧张, 助教贴心全程陪学有问必答答必解惑, lab框架清晰同学交流密切, 更有前沿讲座各界大佬分享技术交流思想, 考题不超纲全是单选且与教学内容绝对平行不正交, 有往年题提供开源自信从不隐瞒, 大作业选题宽泛麻将掼蛋王者荣耀应有尽有, 即使你对强化学习算法一窍不通也可以简单多次训练拿到很好的表现加强对强化学习的了解与认知.&lt;/p&gt;
&lt;p&gt;选择强化学习, 重铸彩虹荣光！&lt;/p&gt;
&lt;p&gt;兄弟！有没有心动！&lt;/p&gt;
&lt;p&gt;兄弟！有的！&lt;/p&gt;
&lt;h3&gt;智能机器人概论&lt;/h3&gt;
&lt;p&gt;赵卉菁 2023秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;听感一般, 老师讲课很认真, 但是三教的教室实在是太温暖了, 让人不得不睡觉...&lt;/p&gt;
&lt;p&gt;最后来的人越来越少, 老师就会以前一节课的 slides 内容作为小测内容, 在下一节课小测, 但是准确率不重要, 是用作签到的.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;三次写代码的小作业, 每次也就几十行代码, 很快就写完了, 但是要写实验报告, 大概是使用课上讲过的算法来构建地图这样的简单任务.&lt;/p&gt;
&lt;p&gt;期中期末两次大作业.&lt;/p&gt;
&lt;p&gt;期中是绕行理科二号楼一圈, 今年额外加了最后的绕桶项目, 难度较大, 前面的拐弯使用启发式算法即可.&lt;/p&gt;
&lt;p&gt;期末是自选题目, 最重要的事情是选择一个有趣的题目, 强烈建议借助小车完成期末作业, 然后可以做一个视频讲一个故事, 越有趣越好, 代码难度没有什么影响.&lt;/p&gt;
&lt;p&gt;附上我们组的期末大作业视频以供参考: &lt;a href=&quot;https://b23.tv/gBXGwpG&quot;&gt;Bilibili&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;无&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Intelligent_Robots-zhj-2023Fall-PKU&quot;&gt;智能机器人概论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;三次作业的代码&lt;/li&gt;
&lt;li&gt;期中大作业的代码和评分标准&lt;/li&gt;
&lt;li&gt;期末大作业的视频链接&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;93&lt;/p&gt;
&lt;p&gt;感觉是大作业的 idea 让老师感觉很有趣.&lt;/p&gt;
&lt;h3&gt;计算机网络&lt;/h3&gt;
&lt;p&gt;黄群 2023秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;黄群老师讲课是自顶向下的, 和同一个学期的计网实验班是相反的顺序.&lt;/p&gt;
&lt;p&gt;老师讲得挺好的, 可以把 slides 和口述部分连接起来形成逻辑链条, 课程值得一听.&lt;/p&gt;
&lt;p&gt;讲课的时候老师还喜欢加入长者的梗, +1s&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有平时作业.&lt;/p&gt;
&lt;p&gt;四个 lab 选三个最高分, 我选择的是 1, 2, 4, 首先第四个 lab 半个小时就能写完, 一定要选; 第一个 lab 也不难, 而且是刚开学时间充裕, 建议选; 最后我还建议选第二个 lab, 因为第三个 lab 可能和期末考试时间有冲突, 难度也不低.&lt;/p&gt;
&lt;p&gt;MOOC 水过, ddl 是期末之后, 及格就行.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中考试无, 期末考试会给往年题和考试范围.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Computer_Networks-hq-2023Fall-PKU&quot;&gt;计算机网络资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我的期末复习笔记.&lt;/li&gt;
&lt;li&gt;Labs , 但是第三个 Router Lab 仍然有 bug , 但是由于只需要完成四个 lab 中的三个即可, 所以这个 lab 就没有彻底完成.&lt;/li&gt;
&lt;li&gt;一些相关资源, 包括一些往年题.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;88&lt;/p&gt;
&lt;p&gt;疑似期末考得一般.&lt;/p&gt;
&lt;h3&gt;博弈论&lt;/h3&gt;
&lt;p&gt;刘霖 2023秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是政府学院的刘霖教授开设的三学分博弈论课程, 课程分为四个部分介绍了不同种类的博弈论, 知识内容还是比较丰富的, 老师上课也会积极与同学互动, 叫同学上台回答问题.&lt;/p&gt;
&lt;p&gt;但是刘霖教授的讲课方式注定了这门课需要下课自己好好思考学习. 因为老师的 slides 只有大纲, 具体的知识点都是口述, 所以上课是必要的, 但是又由于老师讲课有的时候比较模糊, 所以需要下课复习回放. 在复习回放的基础上, 没有必要看其他的补充教材.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作业次数不多, 但是每次作业都有较难的题, 需要多花一些时间认真思考.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老师的考试方式也是很有特点, 考试类似简单的数学课考试, 所以吸引了很多数学, 物理, 信科学院的同学选修.&lt;/p&gt;
&lt;p&gt;不过如果对考试非常有信心, 是可以冲击彩虹的, 我仅使用了自己记的笔记就拿到了彩虹, 没有使用其它的参考资料, 欢迎参考!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Game_Theory-ll-2023Fall-PKU&quot;&gt;博弈论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;课件&lt;/li&gt;
&lt;li&gt;带有笔记的课件 (我的主要贡献是给老师较为简陋的 slides 上记录了非常详尽的笔记, 基本上都是根据老师口述所整理的, 感觉对这门课的学习会有非常大的帮助)&lt;/li&gt;
&lt;li&gt;课程作业&lt;/li&gt;
&lt;li&gt;一些相关资源 (包括往年的考试和往年带有答案的作业题)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;100&lt;/p&gt;
&lt;h3&gt;魅力化学&lt;/h3&gt;
&lt;p&gt;黄建滨 2022秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老师风趣幽默, 每节课会请不同方向的教授来做讲座, 每节课课前会发一个有两三道题的小测, 听完讲座交上去, 用作签到.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一篇期末论文&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有考试&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.lyt0112.com/blog/crystal-zh&quot;&gt;我的魅力化学课程论文&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;97&lt;/p&gt;
&lt;p&gt;唯一一个上 90 的论文课... 想必是论文写出了真情实感吧🥹&lt;/p&gt;
&lt;h3&gt;计算机系统导论&lt;/h3&gt;
&lt;p&gt;管雪涛 2022秋&lt;/p&gt;
&lt;p&gt;2024.09.05 Update: 兹喜闻zzy同学当选为24届27班ICS助教, 特此表示热烈祝贺!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这门课的课程测评很多了, 就不重复说了.&lt;/p&gt;
&lt;p&gt;在此推荐唯一真神: &lt;a href=&quot;https://arthals.ink/blog/data-lab&quot;&gt;更适合北大宝宝体质的 XXX Lab 踩坑记&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Computer_System_ICS-gxt-2022Fall-PKU&quot;&gt;计算机系统导论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所有lab的满分解答 (针对2022年版本的labs, 由于包含面向样例优化, 之后的年份可能由于测试样例的改变导致性能急剧下降, 比如下降到80分左右)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;94&lt;/p&gt;
&lt;p&gt;历史成绩不具备现实意义.&lt;/p&gt;
&lt;h3&gt;数据结构与算法 (A)&lt;/h3&gt;
&lt;p&gt;邹磊 2022秋&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;中规中矩, 感觉看看 slides 就可以了. 对于学习巩固算法知识还是有用的.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;中规中矩, 每一章有一些算法题和几道书面算法题, 尽量熟练使用 STL 可以提高编程效率.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中考试:&lt;/p&gt;
&lt;p&gt;期中是线下考试, 大部分就是正常的算法模拟题, 拟合一下往年题就好了&lt;/p&gt;
&lt;p&gt;期末考试:&lt;/p&gt;
&lt;p&gt;线上考试, 不具有参考意义. 但是是根据样例通过个数给分, 与之前的程设和计算概论都不同.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Data_Structures_And_Algorithms_A-zl-2022Fall-PKU/&quot;&gt;数据结构与算法资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;上机作业题&lt;/li&gt;
&lt;li&gt;书面作业题&lt;/li&gt;
&lt;li&gt;往年考试题&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;91&lt;/p&gt;
&lt;p&gt;历史成绩不具备现实意义.&lt;/p&gt;
&lt;h3&gt;中级微观经济学 (Legacy)&lt;/h3&gt;
&lt;p&gt;秦晋 2023秋&lt;/p&gt;
&lt;p&gt;Update 2024.09.25: 看老师在朋友圈发了一暑假照片哈哈哈&lt;/p&gt;
&lt;p&gt;Update 2024.06.06: 听说老师要走了, 真的很可惜, 虽然在考试和难度方面有争议, 但是老师讲的还是很认真的, 也对教书这件事情很热情. 希望老师能找到可以实现理想的地方.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;非常不错, Dr.J 讲课条理清晰, 用填空的形式带领补全思维链条, 微观经济学本身就有清晰的理论依据, 这更使得这门课听起来简洁明了. 如果学过吴泽南的经济学原理, 那么这门课的内容大部分是在经济学原理中已经讲授过的, 所以给我一种在复习经济学原理的错觉(x&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作业不难, 可能偶尔有一两道比较tricky的问题&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;出分之后在树洞上引起了不小的讨论, 主要是在讨论这样给分是否合理. 由于调分力度很大, 树洞里大量的彩虹和 84.5 样本说明成绩分布有极端化的趋势, 我觉得在调分的时候适当引入一些梯度可以更好评价每一位同学的付出程度. 当然无论如何调分力度都很大, 期末是助教出题, 题目较难, 但是最后的成绩很满意!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Intermediate_Microeconomics-qj-2023Fall-PKU&quot;&gt;中级微观经济学资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;填补完成的课程笔记&lt;/li&gt;
&lt;li&gt;课程作业&lt;/li&gt;
&lt;li&gt;一些期末模拟题以供训练&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;100&lt;/p&gt;
&lt;p&gt;历史成绩不具备现实意义.&lt;/p&gt;
&lt;h2&gt;春季学期课程&lt;/h2&gt;
&lt;h3&gt;计算机视觉导论&lt;/h3&gt;
&lt;p&gt;王鹤 2024春&lt;/p&gt;
&lt;p&gt;2024.07.12 Update: 出分哩, 大三结束!&lt;/p&gt;
&lt;p&gt;2024.06.18 Update: PDF 格式的笔记在 GitHub 上作为 &lt;code&gt;Releases&lt;/code&gt; 发布&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Github 仓库所有笔记更新完毕, 完结撒花!!!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2024.06.08 Update: Embodied AI 相关笔记&lt;/p&gt;
&lt;p&gt;2024.06.01 Update: Generative Model 相关笔记&lt;/p&gt;
&lt;p&gt;2024.05.25 Update: Detection and Instance Segmentation 相关笔记&lt;/p&gt;
&lt;p&gt;2024.05.18 Update: Transformer 相关笔记&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(以下是在课程笔记前写的前言)&lt;/p&gt;
&lt;p&gt;作为北京大学信息科学技术学院的学生, 长期以来饱受糟糕课程质量, 糟糕课程作业, 糟糕考试难度的折磨.  比如算法设计与分析的等课程的教学质量极低, 教考分离, ICS 考试一面黑板的考试错误题目订正等等.  在这样的环境下,  遇到了王鹤老师开设的计算机视觉导论课程, 这门课程的内容丰富, 作业质量高, 考试难度适中, 称得上是精品课程 (与算分这种国家精品课程相区别) .&lt;/p&gt;
&lt;p&gt;王鹤老师将计算机视觉的发展脉络呈现给大家, 在这个深度学习时代, 老师并没有完全忽视传统 CV 的方法, 而是挑选了其中具有代表性的工作, 这些工作为深度学习时代的 CV 打下了良好的基础, 提供了许多基础工具和数据集的构建方式. 同时老师也更加注重深度学习的基础知识, 如 BatchNorm 的特性和与其他 Norm 的区别, 许多人仅仅只是会 PyTorch 的积木搭建, 但是对于这些基础知识的原理和性质却不甚了解, 导致在实际使用中遇到问题时无法解决, 王老师在这方面往往提出 intuitive 的问题, 引人深思.&lt;/p&gt;
&lt;p&gt;我是在大三下学期选修了这门课程, 即使我已经具有了一定的深度学习基础, 但是我仍然很享受上课 (看回放) 的过程, 因为对于许多已经了解的知识, 王老师会再度给出解释, 总是让我在同一个地方有不同的收获.&lt;/p&gt;
&lt;p&gt;我在本学期期中考试之前偶然了解到曾经有学长撰写了一本笔记, 但是许多内容已经进行了 更新或者删改, 因此我联系上林晓疏 (笔名) 学长, 获取了这份笔记的源代码, 并在此基础上进行更新, 以飨后人.&lt;/p&gt;
&lt;p&gt;该笔记按照讲授先后顺序进行排列, 但是章节编排按照知识结构划分, 因此章节划分可能与课程进度有所不同. 同时本笔记不能替代课程, 只是对这部分知识的总结和思考, 建议与课程回放配合食用.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作业是四次 lab , 没有其它作业, 对我来讲周更笔记算是比较有趣的事情, 一边看回放一边记记笔记需要两三个小时, 预计总时长会在25小时左右 (更新完之后发现总时长在35小时左右) .&lt;/p&gt;
&lt;p&gt;Lab1&lt;/p&gt;
&lt;p&gt;都不能使用 for 循环&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现卷积, 包括 pedding , 使用 Toeplitz Matrix 实现卷积, 使用 Sliding Window 实现卷积&lt;/li&gt;
&lt;li&gt;实现 Canny Edge Detector, 包括包括 NMS 和 Edge Linking with Hysteresis Threshold&lt;/li&gt;
&lt;li&gt;实现 Harris Corner Detector&lt;/li&gt;
&lt;li&gt;使用 RANSAC 进行平面 fitting&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lab2&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;手工实现反向传播 (不是很要求矩阵求导, 考试也不考察矩阵求导)&lt;/li&gt;
&lt;li&gt;手工实现 Batch Norm&lt;/li&gt;
&lt;li&gt;使用前两问的函数在 Cifar-10 上训练一个 CNN&lt;/li&gt;
&lt;li&gt;使用 PyTorch 实现一个 CNN&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lab3&lt;/p&gt;
&lt;p&gt;除了 Marching Cube 都不能使用 for 循环&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现相机校准&lt;/li&gt;
&lt;li&gt;从 depth image 重建点云&lt;/li&gt;
&lt;li&gt;从 mesh sample 点云, 使用两种 metric 计算点云距离&lt;/li&gt;
&lt;li&gt;使用 Marching Cube 从 SDF 重建 mesh&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Lab4&lt;/p&gt;
&lt;p&gt;可以使用 for 循环&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实现 Point Net&lt;/li&gt;
&lt;li&gt;Mask RCNN&lt;/li&gt;
&lt;li&gt;RNN&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时间: 3-4个小时&lt;/p&gt;
&lt;p&gt;lab质量较前三个有所下降, 大概是因为这几个网络复杂度提高了太多了, 想要好好写任务量太大了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中考试&lt;/p&gt;
&lt;p&gt;还是有一些难度的, cheatsheet实际上用处不大, 记录一些公式或者课上老师口述的例子就够了, 我考试的时候就看了两三次, 还是确认一下我记的名词是不是对的&lt;/p&gt;
&lt;p&gt;需要对CV的知识和老师上课讲的 insight 比较熟悉才能拿到90+的分数&lt;/p&gt;
&lt;p&gt;期末考试&lt;/p&gt;
&lt;p&gt;几个记忆比较深刻的地方&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于 RCNN, Fast RCNN, Faster RCNN 的算法流程要熟悉, 比如具体为什么 Fast RCNN 比 RCNN 快, 快在哪里了&lt;/li&gt;
&lt;li&gt;Embodied AI 和一些相关知识点我的笔记都有提到&lt;/li&gt;
&lt;li&gt;SDF 哪边是正数, 因为我有个同学记错了🙂‍↔️&lt;/li&gt;
&lt;li&gt;Object detection 的 IoU, PR曲线, AP计算模拟&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不过大三大四应该是不调分的, 我的原始成绩98.15, 最后还向下取整了🥹&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Computer_Vision-wh-2024Spring-PKU&quot;&gt;计算机视觉导论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Latex 格式的课程笔记, 编译好的 PDF 格式笔记可以在 &lt;code&gt;Releases&lt;/code&gt; 中下载&lt;/li&gt;
&lt;li&gt;Lab 及其解答&lt;/li&gt;
&lt;li&gt;一些相关 Cheatsheet&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;98&lt;/p&gt;
&lt;h3&gt;计算机组织与体系结构&lt;/h3&gt;
&lt;p&gt;陆俊林 2024春&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老师讲的挺好的, 讲课水平算信科中上等, 并且3学分的课按理来说有一天是奇偶周上课, 但是这一天不用上课, 老师安排自己看MOOC, 所以上起来就像两学分的课.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一次 Report on the EDVAC&lt;/li&gt;
&lt;li&gt;每周MOOC课后题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;考试考的是老师上课和MOOC的并集, 所以既需要上课也需要看MOOC, 很抽象.&lt;/p&gt;
&lt;p&gt;期中考试比较简单, 拟合往年题.&lt;/p&gt;
&lt;p&gt;期末考试: 我在写这段话的时候刚考完期末, 15道不定项选择题.&lt;/p&gt;
&lt;p&gt;大题:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读一段小汇编代码&lt;/li&gt;
&lt;li&gt;流水线分析, 数加入气泡之后总共几个周期, 有点像 ICS 的考试题&lt;/li&gt;
&lt;li&gt;外部中断的流程填空, 串行和并行数据传输的优缺点, MSI的优缺点&lt;/li&gt;
&lt;li&gt;经典Burst传输图, 背slides上的图就可以了&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;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Computer_Architectures-ljl-2024Spring-PKU&quot;&gt;计算机组织与体系结构资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;课程讲义 (not available, copyright belongs to Prof. Junlin Lu)&lt;/li&gt;
&lt;li&gt;MOOC笔记&lt;/li&gt;
&lt;li&gt;课程作业&lt;/li&gt;
&lt;li&gt;一些其它资料 (往年考题和考试提纲)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;85&lt;/p&gt;
&lt;h3&gt;数据库概论&lt;/h3&gt;
&lt;p&gt;陈立军 2024春&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;听感一般, 老师讲课还可以, 但是上课人数是真的越来越少, 我基本上不上课就看回放, 这个学期好像也没有签到.&lt;/p&gt;
&lt;p&gt;大家都喜欢入主中南海😍🥰&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;六次作业, 五次实习, 虽然每次都借助 ChatGPT 完成, 用不了半天时间, 但是东西还挺多的, 主要是 ChatGPT 面对这种问题答案往往是不对的.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;没有期中考试, 这是好的.&lt;/p&gt;
&lt;p&gt;期末考试听说会考很多 sides 上面的东西, 过拟合一下可怜的一点点往年题好了.&lt;/p&gt;
&lt;p&gt;考完之后发现需要好好拟合往年题, 不论是期中还是期末.&lt;/p&gt;
&lt;p&gt;首先是15道填空, 一个一分, 考的很细致, 比如如何使用关系代数判定函数依赖和多值依赖&lt;/p&gt;
&lt;p&gt;大题:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;五个简答题, 元组关系演算, 期中往年题计算一个SQL查询的结果, 另外的记不清了&lt;/li&gt;
&lt;li&gt;期中往年题, 关系代数计算, 比如左外链接, 计算最小值, 基础关系运算表示除法, 四个小题还有一个忘了&lt;/li&gt;
&lt;li&gt;期中往年题, 股票SQL查询&lt;/li&gt;
&lt;li&gt;第一小题是计算一个比较复杂的函数依赖的候选码, 判断是哪个范式; 第二小题是期末往年题, 给定关系模式 R 和函数依赖集 F, 画出合适的 E-R 图 (提示: 先给出保持函数依赖的分解, 再观察各关系主外码的关系)&lt;/li&gt;
&lt;li&gt;期中往年题, 篮球比赛ER图&lt;/li&gt;
&lt;li&gt;视图可串行化+三个并发协议, 看看 slides 上面的例题即可&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;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Database_Systems-clj-2024Spring-PKU&quot;&gt;数据库概论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;课程讲义&lt;/li&gt;
&lt;li&gt;笔记 (基本上包含了老师上课口述的问题和知识, 从 &lt;code&gt;Releases&lt;/code&gt; 下载)&lt;/li&gt;
&lt;li&gt;课程作业 (包含六次作业和五次实习)&lt;/li&gt;
&lt;li&gt;一些其它资料 (往年考题和考试提纲)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;89.5&lt;/p&gt;
&lt;h3&gt;经济学原理 (Ⅱ)&lt;/h3&gt;
&lt;p&gt;庄晨 2023春&lt;/p&gt;
&lt;p&gt;Update June 5th, 2024: 祝愿带了我两个学期的经济学原理的李亦丁助教学长在美国的博士研究顺利!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这门课程是庄晨教授在北大第一次授课, 课程听起来难以通过 slides 和老师的口述建立起连贯的知识体系, 或许这也是宏观经济学本身的问题, 不像微观经济学那样有明确的唯一的数学模型.&lt;/p&gt;
&lt;p&gt;感觉课程难度偏难, 这是因为老师强调经济学直觉, 对于一些问题没有拿出数学工具进行分析, 这就导致在一些宏观问题上有些迷惑, 一知半解.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;较少, 作业比课程 slides 清楚&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中期末老师都给出了样例, 可以缓解题目参考较少的焦虑, 助教哥哥姐姐也在考试之前开腾讯会议带大家复习, 讲解考试样例🥰.&lt;/p&gt;
&lt;p&gt;另外其实可以找找庄晨教授在国外开课的课程讲义等资料作为参考复习.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Principle_of_Economics_II-zc-2023Spring-PKU&quot;&gt;经济学原理 (Ⅱ) 资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自己总结的课程笔记, 在一些数学推导上下了一些功夫总结, 可供参考&lt;/li&gt;
&lt;li&gt;隔壁李博班的讲义&lt;/li&gt;
&lt;li&gt;期中期末的样例题目&lt;/li&gt;
&lt;li&gt;课程作业 (还被李亦丁学长发到小红书上去了)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;96&lt;/p&gt;
&lt;h3&gt;AI引论&lt;/h3&gt;
&lt;p&gt;刘利斌 2023春&lt;/p&gt;
&lt;p&gt;Update 2025.02.20: 我来当助教啦&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这门课程是在2023年课程改革后的第一年, 比之前的AI引论好了不少 (之前的AI引论纯 flybitch), 在课程结构上更加符合认知规律了. 但是对于比较重要的 deep learning 部分, 和隔壁的 AI 基础相比讲授还是偏少, 期中之前的搜索讲的太多太细致了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作业不多, 但是lab想要满分还是需要一些时间的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;期中期末可以携带一张手写的 cheatsheet, 并且 cheatsheet 会被收走 (猜想可能是为了不让祖传cheatsheet) , 有难度, cheatsheet也用不上. 在此感慨隔壁 AI 基础考试半个小时拿到满分的 &lt;a href=&quot;https://arthals.ink&quot;&gt;卓宝&lt;/a&gt; 同学真是太强了...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Introduction_to_Artificial_Intelligence-llb-2023Spring-PKU&quot;&gt;AI引论资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;自己总结的课程笔记 (较为粗糙, 仅供参考)&lt;/li&gt;
&lt;li&gt;课程lab解答&lt;/li&gt;
&lt;li&gt;一些python练习 (许多同学没有python基础)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;94&lt;/p&gt;
&lt;h3&gt;程序设计实习&lt;/h3&gt;
&lt;p&gt;刘家瑛 2022春&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;课程听感&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这门课程的教学质量算是信科中上水平的, 尤其是刘家瑛老师讲课还是很有趣可爱的, 有的时候还会讲一些关于保研出国的建议.&lt;/p&gt;
&lt;p&gt;这门课程前半学期是 c++ 类和对象的语法部分, 跟着 openjudge 上面的题目和课件正常学习就好了, 后半学期是算法部分, 对于非竞赛生可能需要多努力一些, 并且学着多使用 STL 会提高不少编程效率.&lt;/p&gt;
&lt;p&gt;因为疫情原因, 数学考试延期至下学期开学进行, 所以我有充足的时间练习算法题, 这就是仓库里为什么会有那么多算法题的解答.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作业/任务量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作业就是每周写语法, 算法题, 平时任务量不大, 但是著名的魔兽世界还是需要一些时间完成的, 笔者当时是五一之前上午期中考完试, 下午写的终极版魔兽世界, 对上一版魔兽世界做了非常多的修改, 之后又零零碎碎花了一些时间对拍 debug , 魔兽世界对拍 debug 真是非常好的方法.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考试&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为是疫情期间线上期末考试, 不太具有参考价值.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;相关资料&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Practice_of_Programming_in_C_and_CPP_cxsjsx-ljy-2022Spring-PKU&quot;&gt;程序设计实习资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这个仓库包括:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;作业&lt;/li&gt;
&lt;li&gt;课下练习&lt;/li&gt;
&lt;li&gt;期中整理好的题目, 答案以及一些手写笔记&lt;/li&gt;
&lt;li&gt;期末考题&lt;/li&gt;
&lt;li&gt;算法模板整理&lt;/li&gt;
&lt;li&gt;Qt 大作业&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;成绩&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;91&lt;/p&gt;</content:encoded><h:img src="/_astro/pku_scene.dwn-_9Tu.jpeg"/><enclosure url="/_astro/pku_scene.dwn-_9Tu.jpeg"/></item><item><title>Operating Systems Notes 11: Deadlock</title><link>https://www.lyt0112.com/blog/operating_systems_note_11-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_11-zh</guid><description>Operating Systems Notes 11: 死锁</description><pubDate>Fri, 06 Jun 2025 15:40:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-06-05&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 核心概念&lt;/h2&gt;
&lt;p&gt;我们首先要对死锁以及与它相关的几个概念有一个清晰的认识。&lt;/p&gt;
&lt;h3&gt;1.1 并发编程的问题&lt;/h3&gt;
&lt;p&gt;在复杂的并发程序 (如数据库MySQL、Web服务器Apache等) 中，开发者会遇到各种各样的缺陷。研究表明，这些缺陷主要分为两大类：死锁缺陷和非死锁缺陷 (如原子性违背、顺序错误等) 。其中，非死锁的缺陷占了绝大多数 (约97%) ，但这并不意味着死锁不重要。一旦发生死锁，其后果往往是灾难性的，可能导致整个系统挂起。&lt;/p&gt;
&lt;h3&gt;1.2 死锁 (Deadlock)&lt;/h3&gt;
&lt;h4&gt;1.2.1 定义&lt;/h4&gt;
&lt;p&gt;我们来看两个定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个进程如果正在等待一个永远也不可能为真的条件，那么这个进程就陷入了死锁。&lt;/li&gt;
&lt;li&gt;在一组进程中，每个进程都在无限期地等待该组中另一个进程所占有的资源，因而永远无法获取到自己所需的资源，这种现象称为&lt;strong&gt;进程死锁&lt;/strong&gt;。这组进程就称为&lt;strong&gt;死锁进程&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简单来说，就是两个或多个进程形成了一个&quot;僵局&quot;，比如进程A等着进程B手里的资源，而进程B又恰好在等进程A的资源，谁也不肯先放手，就这么永远地等下去了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键结论&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有参与死锁的进程都处于&lt;strong&gt;等待&lt;/strong&gt;状态。&lt;/li&gt;
&lt;li&gt;死锁的进程只是当前系统中所有进程的一个子集。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2.2 为什么会发生死锁？&lt;/h4&gt;
&lt;p&gt;死锁的根源在于&lt;strong&gt;资源&lt;/strong&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;可重用资源 (Reusable Resource)&lt;/strong&gt;: 可被多个进程多次使用，不会被消耗掉。例如：CPU、内存、磁盘、文件、数据库、信号量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可消耗资源 (Consumable Resource)&lt;/strong&gt;: 只能使用一次，可以被创建和销毁。例如：信号、中断、消息。&lt;/li&gt;
&lt;li&gt;资源的&lt;strong&gt;数量是有限的&lt;/strong&gt;，如果无限，自然就没有争抢了。&lt;/li&gt;
&lt;li&gt;资源的使用模式通常是&quot;&lt;strong&gt;申请-分配-使用-释放&lt;/strong&gt;&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;编程实践中的原因&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复杂的依赖关系&lt;/strong&gt;: 在大型软件中，不同模块间的调用关系复杂，可能在不经意间形成一个锁的循环依赖，导致死锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装的副作用&lt;/strong&gt;: 模块化编程隐藏了实现细节，这本是好事。但调用者可能不知道一个看似简单的接口内部会去获取某个锁，当多个这样的接口被组合调用时，就可能意外地触发死锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.3 活锁 (Livelock)&lt;/h3&gt;
&lt;p&gt;活锁很有意思，它和死锁很像，但又有所不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;: 进程没有被阻塞 (Block) ，它们的状态在持续改变，看起来很&quot;活跃&quot;，但却无法向前推进任何有效的工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;: 想象两个人 (进程) 在一条狭窄的走廊相遇，都想给对方让路。A向左让，B也向左让，结果又堵住了；A向右让，B也向右让，又堵住了。两者都在不停地&quot;尝试&quot;和&quot;让步&quot; (状态在改变) ，但就是过不去 (没有进展) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在代码中，活锁通常表现为进程在循环中不断尝试获取锁，但因为某些条件 (例如礼让策略) 总是失败。&lt;/p&gt;
&lt;h3&gt;1.4 饥饿 (Starvation)&lt;/h3&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;: 在一个优先级调度系统中，如果总有高优先级的任务到来，那么低优先级的任务可能永远也得不到CPU时间，这就&quot;饿&quot;死了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.5 死锁、活锁与饥饿的区别&lt;/h3&gt;
&lt;p&gt;| 特征         | 死锁 (Deadlock)        | 活锁 (Livelock)       | 饥饿 (Starvation)          |
| :----------- | :--------------------- | :-------------------- | :------------------------- |
| &lt;strong&gt;进程状态&lt;/strong&gt; | 阻塞 (Waiting/Blocked) | 运行 (Running/Active) | 阻塞或就绪 (Blocked/Ready) |
| &lt;strong&gt;系统进展&lt;/strong&gt; | 无进展                 | 无进展                | 可能有其他进程在进展       |
| &lt;strong&gt;原因&lt;/strong&gt;     | 循环等待资源且不释放   | 错误的重试/礼让逻辑   | 不公平的资源分配策略       |&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 死锁的产生&lt;/h2&gt;
&lt;p&gt;一个系统中要发生死锁，必须&lt;strong&gt;同时&lt;/strong&gt;满足以下四个必要条件。只要能破坏其中任意一个，死锁就不会发生。&lt;/p&gt;
&lt;h3&gt;2.1 四大必要条件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;互斥使用 (Mutual Exclusion)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;: 一个资源在同一时刻只能被一个进程使用。如果其他进程请求该资源，则必须等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质&lt;/strong&gt;: 这是资源本身的固有属性，比如打印机，不可能两个进程同时打印。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;占有且等待 (Hold and Wait)&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;: 进程P1占有了资源R1，然后又去请求资源R2，但R2被P2占有。此时P1就处于&quot;占有R1且等待R2&quot;的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不可抢占 (No Preemption)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;: 资源不能被强制地从占有它的进程中夺走，只能由占有者在使用完毕后自愿释放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;: 一个进程打开了一个文件并持有文件锁，操作系统不能在它主动关闭文件前把这个锁抢走给别人。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;循环等待 (Circular Wait)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;描述&lt;/strong&gt;: 存在一个进程-资源的循环链，P1在等P2的资源，P2在等P3的资源，...，Pn在等P1的资源，形成一个闭环。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数学表示&lt;/strong&gt;: 存在一个进程集合 ${P_0, P_1, ..., P_n}$，使得 $P_0$ 等待 $P_1$ 持有的资源，$P_1$ 等待 $P_2$ 持有的资源，...，$P_n$ 等待 $P_0$ 持有的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;3. 资源分配图 (Resource Allocation Graph - RAG)&lt;/h2&gt;
&lt;p&gt;为了更直观地描述和分析死锁，我们引入了资源分配图。&lt;/p&gt;
&lt;h3&gt;3.1 RAG的构成&lt;/h3&gt;
&lt;p&gt;RAG是一个有向图 $G=(V, E)$，包含两类节点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程节点 (P)&lt;/strong&gt;: 用圆形表示，如 $P_1, P_2$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源节点 (R)&lt;/strong&gt;: 用方形表示。方框内的点表示该类资源的&lt;strong&gt;实例&lt;/strong&gt;数量。例如，一个方框代表&quot;打印机&quot;这类资源，里面的两个点代表有两台打印机。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图中包含两类有向边：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;申请边 (Request Edge)&lt;/strong&gt;: 从进程指向资源类，表示该进程正在申请该类资源的一个实例。$P_i \rightarrow R_j$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配边 (Assignment Edge)&lt;/strong&gt;: 从资源实例指向进程，表示该资源实例已经被分配给了这个进程。$R_j \rightarrow P_i$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 RAG 图例&lt;/h3&gt;
&lt;p&gt;下面是一些RAG的例子，我们可以用Mermaid流程图来表示它们。&lt;/p&gt;
&lt;h4&gt;3.2.1 交通路口死锁&lt;/h4&gt;
&lt;p&gt;十字路口的四个区域可以看作四个资源，四辆车看作四个进程。每辆车都占用了自己当前的路口，同时又想进入前方的路口。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1(P1) --&gt; Ra[Ra]
    P2(P2) --&gt; Rb[Rb]
    P3(P3) --&gt; Rc[Rc]
    P4(P4) --&gt; Rd[Rd]
    Ra --&gt; P2
    Rb --&gt; P3
    Rc --&gt; P4
    Rd --&gt; P1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就形成了一个 &lt;code&gt;P1 -&gt; R_a -&gt; P2 -&gt; R_b -&gt; P3 -&gt; R_c -&gt; P4 -&gt; R_d -&gt; P1&lt;/code&gt; 的循环等待，导致死锁。&lt;/p&gt;
&lt;h3&gt;3.3 死锁定理&lt;/h3&gt;
&lt;p&gt;RAG和死锁之间存在一个重要的关系，我们称之为死锁定理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果RAG中&lt;strong&gt;没有环路&lt;/strong&gt;，则系统中&lt;strong&gt;一定没有&lt;/strong&gt;死锁。&lt;/li&gt;
&lt;li&gt;如果RAG中&lt;strong&gt;存在环路&lt;/strong&gt;，则系统中&lt;strong&gt;可能存在&lt;/strong&gt;死锁。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特殊情况&lt;/strong&gt;：如果每个资源类都&lt;strong&gt;只有一个实例&lt;/strong&gt;，那么环路是死锁存在的&lt;strong&gt;充分必要条件&lt;/strong&gt; (即有环必死锁) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.3.1 有环但无死锁的例子&lt;/h4&gt;
&lt;p&gt;为什么有环路却&quot;可能&quot;没有死锁呢？这通常用来阐明在每类资源拥有多个实例的情况下，&lt;strong&gt;环路是死锁的必要不充分条件&lt;/strong&gt;。看下面这个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    R1[&quot;R1 (2)&quot;]
    R2[&quot;R2 (2)&quot;]
    P1(P1)
    P2(P2)
    P3(P3)
    P4(P4)

    R2 --&quot;持有&quot;--&gt; P1
    R1 --&quot;持有&quot;--&gt; P2
    R1 --&quot;持有&quot;--&gt; P3
    R2 --&quot;持有&quot;--&gt; P4

    P1 --&quot;请求&quot;--&gt; R1
    P3 --&quot;请求&quot;--&gt; R2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个图中，存在环路 &lt;code&gt;P1 → R1 → P3 → R2 → P1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;虽然图中存在一个 &lt;code&gt;P1 → R1 → P3 → R2 → P1&lt;/code&gt; 的循环等待，但我们可以通过一个简单的推演 (这其实就是资源图化简法的思路) 发现，系统仍然可以找到一个让所有进程都完成的执行序列，因此没有死锁：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;我们观察到进程 &lt;code&gt;P2&lt;/code&gt; 和 &lt;code&gt;P4&lt;/code&gt;。它们虽然持有资源，但&lt;strong&gt;没有请求任何新资源&lt;/strong&gt; (即没有从它们出发的指向资源框的请求边) 。这意味着它们不处于等待状态，可以顺利执行完毕并释放资源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;P2 (或 P4) 先完成&lt;/strong&gt;：假设 &lt;code&gt;P2&lt;/code&gt; 先执行完毕，它会释放它持有的1个 &lt;code&gt;R1&lt;/code&gt; 资源实例。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;此时，系统可用的 &lt;code&gt;R1&lt;/code&gt; 资源变为1个 (&lt;code&gt;Available R1 = 1&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;P1的请求被满足&lt;/strong&gt;：&lt;code&gt;P1&lt;/code&gt; 一直在等待 &lt;code&gt;R1&lt;/code&gt;。现在系统有可用的 &lt;code&gt;R1&lt;/code&gt; 了，于是就可以把这个 &lt;code&gt;R1&lt;/code&gt; 实例分配给 &lt;code&gt;P1&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P1&lt;/code&gt; 获得了所需的全部资源 (它原来持有的&lt;code&gt;R2&lt;/code&gt;和新获得的&lt;code&gt;R1&lt;/code&gt;) ，可以继续执行直到完成。僵局被打破。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;P1完成并释放资源&lt;/strong&gt;：&lt;code&gt;P1&lt;/code&gt; 完成后，会释放它持有的所有资源：1个 &lt;code&gt;R1&lt;/code&gt; 实例和1个 &lt;code&gt;R2&lt;/code&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;&lt;strong&gt;其他进程相继完成&lt;/strong&gt;：&lt;code&gt;P1&lt;/code&gt; 释放的 &lt;code&gt;R2&lt;/code&gt; 可以满足 &lt;code&gt;P3&lt;/code&gt; 的请求，&lt;code&gt;P3&lt;/code&gt; 也能完成。&lt;code&gt;P4&lt;/code&gt; 本来就可以自己完成。最终，所有进程都能顺利结束。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为存在一个可以让所有进程都完成的执行序列 (例如 &lt;code&gt;&amp;#x3C;P2, P1, P4, P3&gt;&lt;/code&gt;) ，所以系统&lt;strong&gt;没有发生死锁&lt;/strong&gt;。这个例子完美地说明了：当每个资源类有多个实例时，&lt;strong&gt;资源分配图中存在环路是死锁的必要条件，但不是充分条件&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;3.4 资源分配图化简&lt;/h3&gt;
&lt;p&gt;我们可以通过一种称为&quot;图化简&quot;的方法来检测死锁。基本思想是：模拟系统资源的分配和释放过程，看看是否能满足所有进程的需求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;化简步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在图中寻找一个既不阻塞 (没有申请边) 也非孤立的进程节点 $P_i$。&lt;/li&gt;
&lt;li&gt;释放该进程 $P_i$ 所占有的所有资源，抹去所有与它相连的分配边，使其变为孤立节点。这些释放的资源现在可用于满足其他进程的请求。&lt;/li&gt;
&lt;li&gt;检查这些新释放的资源是否能满足某个正在等待的进程 $P_j$ 的请求。如果可以，将 $P_j$ 的申请边变为分配边。现在 $P_j$ 获得了所需资源，可以继续运行，未来它也会释放所有资源。我们可以把它也看作一个可以被&quot;消去&quot;的节点。&lt;/li&gt;
&lt;li&gt;重复以上步骤，直到无法再找到可化简的进程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;: 如果最终图中所有的进程都被化简掉 (成为孤立节点) ，则称该图是&lt;strong&gt;可完全化简的&lt;/strong&gt;，系统&lt;strong&gt;没有死锁&lt;/strong&gt;。反之，如果最后还剩下无法化简的进程节点，则系统存在死锁。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 解决死锁的方法&lt;/h2&gt;
&lt;p&gt;面对死锁，主要有四大类处理策略：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**忽略该问题 (鸵鸟算法) **: 就像鸵鸟把头埋进沙子里一样，假装问题不存在。适用于发生概率极低，且处理代价远高于其危害的系统。很多主流操作系统 (如Windows, UNIX) 都采用此策略，它们假设死锁不会发生，将处理死锁的责任留给了程序员。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁预防 (Deadlock Prevention)&lt;/strong&gt;: 在系统设计时，通过施加某些限制，从根本上破坏死锁产生的四个必要条件之一。这是一种&lt;strong&gt;静态&lt;/strong&gt;策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁避免 (Deadlock Avoidance)&lt;/strong&gt;: 在系统运行时，动态地跟踪资源分配，每当有资源请求时，先判断分配后是否会导致系统进入&quot;不安全状态&quot;。如果会，则拒绝分配。这是一种&lt;strong&gt;动态&lt;/strong&gt;策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁检测与解除 (Deadlock Detection and Recovery)&lt;/strong&gt;: 允许死锁发生，但系统有能力检测到它，并采取措施 (如剥夺资源、终止进程) 来解除死锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4.1 死锁预防&lt;/h3&gt;
&lt;p&gt;这是最直接的思路，即破坏四个必要条件中的一个。&lt;/p&gt;
&lt;h4&gt;4.1.1 破坏&quot;互斥使用&quot;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 让资源变得可共享，而不是独占。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法&lt;/strong&gt;: &lt;strong&gt;资源转换技术&lt;/strong&gt;。一个典型的例子是 &lt;strong&gt;SPOOLing 技术&lt;/strong&gt; (Simultaneous Peripheral Operations On-Line)。以打印机为例，它本身是独占设备。但系统可以设立一个打印机守护进程 (daemon) ，所有进程不直接访问打印机，而是将打印内容发送给这个 daemon 进程，由 daemon 统一管理打印队列。这样，对于进程来说，它们访问的&quot;打印服务&quot;是共享的，不会被阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.1.2 破坏&quot;占有且等待&quot;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 不允许进程在占有资源的同时去等待新资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案1&lt;/strong&gt;: &lt;strong&gt;一次性申请&lt;/strong&gt;。进程在开始运行前，必须一次性地申请它在整个运行过程中所需要的所有资源。只有系统能满足其全部请求时，才分配给它。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 严重降低资源利用率 (一个进程可能很早就申请了某个资源，但很晚才用) ；可能导致&quot;饥饿&quot; (一个需要很多资源的进程可能永远也等不到所有资源都空闲的时刻) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案2&lt;/strong&gt;: &lt;strong&gt;释放已有资源&lt;/strong&gt;。允许进程动态申请资源，但当它申请的新资源无法被满足时，它必须释放掉已经占有的所有资源。等以后需要时再重新申请。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 大大增加系统开销，进程可能要反复申请和释放资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.1.3 破坏&quot;不可抢占&quot;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 允许系统强行剥夺已被占有的资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法&lt;/strong&gt;: 当一个高优先级的进程请求一个被低优先级进程占有的资源时，操作系统可以抢占这个资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局限性&lt;/strong&gt;: 这种方法只适用于那些状态容易保存和恢复的资源，如CPU、内存。对于像打印机这样的设备，你总不能抢占一个打印到一半的任务吧。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.1.4 破坏&quot;循环等待&quot;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 避免形成等待环路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法&lt;/strong&gt;: &lt;strong&gt;资源有序分配法 (Resource Ordering)&lt;/strong&gt;。将系统中所有的资源类型进行线性排序 (例如，R1=1, R2=2, R3=3...) 。然后强制规定：任何进程在申请资源时，都必须严格按照资源编号的&lt;strong&gt;递增顺序&lt;/strong&gt;进行申请。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么能行？&lt;/strong&gt;: 假设一个进程已经申请了资源 $R_i$，它接下来只能申请编号大于 $i$ 的资源 $R_j$ ($j &gt; i$)。这样就不可能出现一个进程占有 $R_j$ 反过来去申请 $R_i$ 的情况，从而保证了资源分配图不会出现环路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 资源编号需要精心设计，可能会给编程带来不便。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 死锁避免&lt;/h3&gt;
&lt;p&gt;死锁避免不像预防那样施加严格的限制，而是更加灵活。它允许前三个条件的存在，但在分配资源时会小心翼翼，确保不会踏入死胡同。这里的核心概念是&lt;strong&gt;安全状态&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;4.2.1 安全状态 (Safe State)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全状态&lt;/strong&gt;: 如果系统能找到一个&lt;strong&gt;安全序列&lt;/strong&gt;，那么系统就处于安全状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全序列&lt;/strong&gt;: 一个进程序列 &lt;code&gt;$&amp;#x3C;P_1, P_2, ..., P_n&gt;$&lt;/code&gt; 是安全的，指的是对于序列中的每一个进程 $P_i$，它未来所需要的最大资源量，可以被&lt;strong&gt;系统当前剩余的资源&lt;/strong&gt;加上&lt;strong&gt;所有排在它前面的进程 ($P_j, j\le i$) 所持有的资源&lt;/strong&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;strong&gt;安全状态&lt;/strong&gt;和&lt;strong&gt;不安全状态&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁状态&lt;/strong&gt; 是一种&lt;strong&gt;不安全状态&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全状态&lt;/strong&gt;一定&lt;strong&gt;没有死锁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不安全状态&lt;/strong&gt; &lt;strong&gt;不一定&lt;/strong&gt;会&lt;strong&gt;导致死锁&lt;/strong&gt;，只是系统无法再保证能按某种顺序为所有进程分配资源，存在进入死锁的可能性。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[系统状态] --&gt; B(安全状态)
    A --&gt; C(不安全状态)
    C --&gt; D(死锁状态)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;4.2.2 银行家算法 (Banker&apos;s Algorithm)&lt;/h4&gt;
&lt;p&gt;就是资源分配图化简法，但是考虑了进程的&lt;strong&gt;最大需求&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这是由Dijkstra提出的最著名的死锁避免算法。它模拟银行家审批贷款的过程：银行家 (操作系统) 在批出贷款 (分配资源) 前，要确保即使这笔钱放出去了，自己手头的资金 (剩余资源) 依然能满足其他客户 (进程) 后续的最大需求，从而保证银行不会倒闭 (系统不会死锁) 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法所需的数据结构&lt;/strong&gt;:
假设有n个进程和m类资源。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Available[m]&lt;/code&gt;: 一个向量，表示每类资源当前还&lt;strong&gt;可用&lt;/strong&gt;的实例数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Max[n][m]&lt;/code&gt;: 一个矩阵，表示每个进程对每类资源&lt;strong&gt;最大&lt;/strong&gt;的需求量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Allocation[n][m]&lt;/code&gt;: 一个矩阵，表示每个进程当前已&lt;strong&gt;分配&lt;/strong&gt;到的每类资源的实例数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Need[n][m]&lt;/code&gt;: 一个矩阵，表示每个进程&lt;strong&gt;还需&lt;/strong&gt;要的每类资源的实例数量。
&lt;ul&gt;
&lt;li&gt;这个矩阵是推算出来的：&lt;code&gt;Need[i][j] = Max[i][j] - Allocation[i][j]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;算法核心&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;资源请求&lt;/strong&gt;: 当进程 $P_i$ 请求资源 &lt;code&gt;Request[i]&lt;/code&gt; 时：
a. 检查请求是否合法：&lt;code&gt;Request[i] &amp;#x3C;= Need[i]&lt;/code&gt;？如果一个进程请求的超过了它声明的最大需求，则为非法请求。
b. 检查系统是否有足够资源：&lt;code&gt;Request[i] &amp;#x3C;= Available&lt;/code&gt;？如果没有，进程必须等待。
c. &lt;strong&gt;假装分配&lt;/strong&gt;: 如果前两步都通过，系统&lt;strong&gt;暂时&lt;/strong&gt;将资源分配给它，并更新数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Available = Available - Request[i]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Allocation[i] = Allocation[i] + Request[i]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Need[i] = Need[i] - Request[i]&lt;/code&gt;
d. &lt;strong&gt;安全检查&lt;/strong&gt;: 调用&lt;strong&gt;安全性算法&lt;/strong&gt;，检查分配后的新状态是否安全。&lt;/li&gt;
&lt;li&gt;如果安全，则正式完成分配。&lt;/li&gt;
&lt;li&gt;如果不安全，则撤销刚才的&quot;假装分配&quot;，恢复到原来的状态，让进程 $P_i$ 等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安全性算法&lt;/strong&gt;:
a. 初始化工作向量 &lt;code&gt;Work = Available&lt;/code&gt;，和完成标记 &lt;code&gt;Finish[n]&lt;/code&gt; (所有元素为&lt;code&gt;false&lt;/code&gt;) 。
b. 从进程中寻找一个满足以下两个条件的进程 $P_i$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Finish[i] == false&lt;/code&gt; (还没执行完)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Need[i] &amp;#x3C;= Work&lt;/code&gt; (它需要的资源小于等于系统当前可用的资源)
c. 如果找不到这样的进程，直接跳到第 e 步。
d. 如果找到了 $P_i$，则假设它执行完毕并释放所有资源。更新 &lt;code&gt;Work&lt;/code&gt; 和 &lt;code&gt;Finish&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Work = Work + Allocation[i]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish[i] = true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;返回第 b 步，继续寻找下一个可以满足的进程。
e. 检查 &lt;code&gt;Finish&lt;/code&gt; 数组，如果所有元素都为 &lt;code&gt;true&lt;/code&gt;，则说明系统处于&lt;strong&gt;安全状态&lt;/strong&gt;。否则，为&lt;strong&gt;不安全状态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4.2.3 银行家算法应用示例1&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态&lt;/strong&gt;: 1类资源，总量12。P1, P2, P3。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 进程 | 目前占有量 | 最大需求量 | 尚需要量 (Need) |
| :--- | :--------: | :--------: | :-------------: |
| P1   |     1      |     4      |        3        |
| P2   |     4      |     6      |        2        |
| P3   |     5      |     8      |        3        |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;系统剩余量 (Available)&lt;/strong&gt;: &lt;code&gt;12 - (1+4+5) = 2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;安全性检查&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Work = 2&lt;/code&gt;, &lt;code&gt;Finish = [F, F, F]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;找 $P_i$ s.t. $Need[i] \leq Work$&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P1: &lt;code&gt;Need=3 &gt; Work=2&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P2: &lt;code&gt;Need=2 &amp;#x3C;= Work=2&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;P3: &lt;code&gt;Need=3 &gt; Work=2&lt;/code&gt; (不行)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P2执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = Work + Allocation[P2] = 2 + 4 = 6&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [F, T, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回到第2步，用新的Work=6找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P1: &lt;code&gt;Need=3 &amp;#x3C;= Work=6&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P1执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = Work + Allocation[P1] = 6 + 1 = 7&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [T, T, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回到第2步，用新的Work=7找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P3: &lt;code&gt;Need=3 &amp;#x3C;= Work=7&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P3执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = Work + Allocation[P3] = 7 + 5 = 12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [T, T, T]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;: 由于所有进程的 &lt;code&gt;Finish&lt;/code&gt; 标记都为 &lt;code&gt;true&lt;/code&gt;，所以&lt;strong&gt;当前状态是安全的&lt;/strong&gt;。一个安全序列是 &lt;code&gt;&amp;#x3C;P2, P1, P3&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;4.2.4 银行家算法应用示例2&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态&lt;/strong&gt;: 5个进程 (P1-P5)，3类资源 (A,B,C)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;|        | 已分配 (Allocation) | 最大需求 (Max) | 尚需要 (Need) |
| :----- | :-----------------: | :------------: | :-----------: |
|        |        A B C        |     A B C      |     A B C     |
| &lt;strong&gt;P1&lt;/strong&gt; |        0 1 0        |     7 5 3      |     7 4 3     |
| &lt;strong&gt;P2&lt;/strong&gt; |        2 0 0        |     3 2 2      |     1 2 2     |
| &lt;strong&gt;P3&lt;/strong&gt; |        3 0 2        |     9 0 2      |     6 0 0     |
| &lt;strong&gt;P4&lt;/strong&gt; |        2 1 1        |     2 2 2      |     0 1 1     |
| &lt;strong&gt;P5&lt;/strong&gt; |        0 0 2        |     4 3 3      |     4 3 1     |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;剩余资源 (Available)&lt;/strong&gt;: A=3, B=3, C=2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题1：此状态是否安全？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Work = [3, 3, 2]&lt;/code&gt;, &lt;code&gt;Finish = [F, F, F, F, F]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;找 $P_i$ s.t. $Need[i] \leq Work$&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P1: &lt;code&gt;[7,4,3] &gt; [3,3,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P2: &lt;code&gt;[1,2,2] &amp;#x3C;= [3,3,2]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P2。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P2执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [3,3,2] + [2,0,0] = [5,3,2]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [F, T, F, F, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[5,3,2]找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P1: &lt;code&gt;[7,4,3] &gt; [5,3,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P3: &lt;code&gt;[6,0,0] &gt; [5,3,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P4: &lt;code&gt;[0,1,1] &amp;#x3C;= [5,3,2]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P4。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P4执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [5,3,2] + [2,1,1] = [7,4,3]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [F, T, F, T, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[7,4,3]找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P1: &lt;code&gt;[7,4,3] &amp;#x3C;= [7,4,3]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P1执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [7,4,3] + [0,1,0] = [7,5,3]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [T, T, F, T, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[7,5,3]找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P3: &lt;code&gt;[6,0,0] &amp;#x3C;= [7,5,3]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P3。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P3执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [7,5,3] + [3,0,2] = [10,5,5]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [T, T, T, T, F]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[10,5,5]找&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;P5: &lt;code&gt;[4,3,1] &amp;#x3C;= [10,5,5]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P5。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟P5执行&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [10,5,5] + [0,0,2] = [10,5,7]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [T, T, T, T, T]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;: 是安全状态。一个安全序列为 &lt;code&gt;&amp;#x3C;P2, P4, P1, P3, P5&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题2：P1申请(0,2,0)能否分配？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;检查请求&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Request_P1 = [0,2,0] &amp;#x3C;= Need_P1 = [7,4,3]&lt;/code&gt; (合法)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Request_P1 = [0,2,0] &amp;#x3C;= Available = [3,3,2]&lt;/code&gt; (有资源)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;假装分配&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Available = [3,3,2] - [0,2,0] = [3,1,2]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Allocation_P1 = [0,1,0] + [0,2,0] = [0,3,0]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Need_P1 = [7,4,3] - [0,2,0] = [7,2,3]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新状态进行安全检查&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Work = [3,1,2]&lt;/code&gt;, &lt;code&gt;Finish = [F,F,F,F,F]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;P1: &lt;code&gt;[7,2,3] &gt; [3,1,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P2: &lt;code&gt;[1,2,2] &gt; [3,1,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P3: &lt;code&gt;[6,0,0] &gt; [3,1,2]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P4: &lt;code&gt;[0,1,1] &amp;#x3C;= [3,1,2]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P4&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Work = [3,1,2] + [2,1,1] = [5,2,3]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [F,F,F,T,F]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[5,2,3]找&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;P1: &lt;code&gt;[7,2,3] &gt; [5,2,3]&lt;/code&gt; (不行)&lt;/li&gt;
&lt;li&gt;P2: &lt;code&gt;[1,2,2] &amp;#x3C;= [5,2,3]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Work = [5,2,3] + [2,0,0] = [7,2,3]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Finish = [F,T,F,T,F]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用新的Work=[7,2,3]找&lt;/strong&gt;:&lt;/li&gt;
&lt;li&gt;P1: &lt;code&gt;[7,2,3] &amp;#x3C;= [7,2,3]&lt;/code&gt; (&lt;strong&gt;可以！&lt;/strong&gt;) -&gt; 找到 P1&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;h3&gt;4.3 死锁检测与解除&lt;/h3&gt;
&lt;p&gt;这种策略不预防也不避免，而是采取&quot;亡羊补牢&quot;的方式。&lt;/p&gt;
&lt;h4&gt;4.3.1 死锁检测&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;何时检测&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;当有进程因资源请求失败而阻塞时。 (开销大)&lt;/li&gt;
&lt;li&gt;定时检测，比如每隔一段时间。&lt;/li&gt;
&lt;li&gt;当系统资源利用率明显下降时，可能是死锁的征兆。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测算法&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;最常用的方法是维护一个&lt;strong&gt;等待图 (Wait-for Graph)&lt;/strong&gt;，它是资源分配图的简化版，只包含进程节点。&lt;/li&gt;
&lt;li&gt;如果 $P_i$ 正在等待 $P_j$ 持有的资源，就在图中画一条从 $P_i$ 到 $P_j$ 的边。&lt;/li&gt;
&lt;li&gt;然后，在等待图中运行&lt;strong&gt;环路检测算法&lt;/strong&gt;。如果发现环路，就意味着系统存在死锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.3.2 死锁解除 (恢复)&lt;/h4&gt;
&lt;p&gt;一旦检测到死锁，就需要采取措施打破僵局。代价越小越好。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;撤销所有死锁进程&lt;/strong&gt;: 最简单粗暴，但代价巨大，所有进程的工作成果都丢失了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程回退 (Rollback)&lt;/strong&gt;: 利用系统的检查点 (Checkpoint) 机制，让死锁的进程回退到某个未发生死锁的先前状态，然后重新执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐一撤销死锁进程&lt;/strong&gt;: 按照某种原则 (如进程优先级、已执行时间、占有资源多少等) 选择一个&quot;牺牲品&quot;进程并终止它，释放其资源。然后再次检测死锁，如果还存在，再撤销下一个，直到死锁解除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逐一抢占资源&lt;/strong&gt;: 同样按某种原则，从一个死锁进程中抢占部分资源分配给其他进程，以打破环路。被抢占资源的进程必须回退到安全状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;5. 经典问题：哲学家就餐问题&lt;/h2&gt;
&lt;p&gt;这个问题是Dijkstra提出的一个经典的并发同步问题，用以说明死锁。&lt;/p&gt;
&lt;h3&gt;5.1 问题描述&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;五个哲学家围坐在一张圆桌旁。&lt;/li&gt;
&lt;li&gt;每人面前一盘意面，每两人之间放着一只筷子，共五只。&lt;/li&gt;
&lt;li&gt;哲学家的动作是：思考 -&gt; 感到饥饿 -&gt; 拿筷子吃饭 -&gt; 放下筷子 -&gt; 继续思考。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则&lt;/strong&gt;: 必须同时拿到左右两边的两只筷子才能吃饭。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 解决方案分析&lt;/h3&gt;
&lt;h4&gt;方案1：朴素的信号量解法&lt;/h4&gt;
&lt;p&gt;每个筷子是一个信号量。哲学家&lt;code&gt;i&lt;/code&gt;先拿左边的筷子&lt;code&gt;fork[i]&lt;/code&gt;，再拿右边的&lt;code&gt;fork[(i+1)%5]&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 伪代码
void philosopher(int i) {
    while(true) {
        think();
        P(fork[i]);              // 拿左筷子
        P(fork[(i+1) % 5]);      // 拿右筷子
        eat();
        V(fork[(i+1) % 5]);      // 放右筷子
        V(fork[i]);              // 放左筷子
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;: &lt;strong&gt;会发生死锁&lt;/strong&gt;。如果五个哲学家同时感到饥饿，并同时拿起自己左手边的筷子，那么他们每个人都将永远等待自己右手边的筷子，而那只筷子正被邻座拿着。这就形成了循环等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方案2：使用管程 (Monitor)&lt;/h4&gt;
&lt;p&gt;将获取和释放筷子的操作封装在管程内部。但幻灯片中给出的&lt;code&gt;get_forks&lt;/code&gt;实现是有问题的，它依然是顺序地申请左右筷子，如果获取左筷子后在等待右筷子时被阻塞，同样会造成死锁，并没有解决根本问题。一个正确的管程实现需要能&lt;strong&gt;原子地&lt;/strong&gt;检查并获取两只筷子。&lt;/p&gt;
&lt;h4&gt;方案3：状态数组法 (推荐)&lt;/h4&gt;
&lt;p&gt;这是 Tanenbaum 提出的一个经典解法，可以避免死锁和饥饿。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;: 引入&lt;code&gt;state&lt;/code&gt;数组记录每个哲学家的状态 (思考、饥饿、进食) 。一个哲学家只有在他&lt;strong&gt;左右两边的邻居都没有在进食&lt;/strong&gt;时，才允许他从&quot;饥饿&quot;状态转为&quot;进食&quot;状态。这个检查和状态转换是&lt;strong&gt;原子&lt;/strong&gt;的 (通过&lt;code&gt;mutex&lt;/code&gt;互斥锁保护) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 伪代码
#define N 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2
int state[N];
semaphore mutex = 1; // 保护对state数组的访问
semaphore s[N] = {0}; // 每个哲学家一个信号量，用于阻塞

void philosopher(int i) {
    while(true) {
        think();
        take_forks(i); // 核心逻辑
        eat();
        put_forks(i);  // 核心逻辑
    }
}

void take_forks(int i) {
    P(mutex); // 进入临界区
    state[i] = HUNGRY;
    test(i); // 尝试获取筷子
    V(mutex); // 退出临界区
    P(s[i]); // 如果不能吃，则在此阻塞
}

void put_forks(int i) {
    P(mutex); // 进入临界区
    state[i] = THINKING;
    test((i+N-1) % N); // 检查左邻居能否开吃
    test((i+1) % N); // 检查右邻居能否开吃
    V(mutex); // 退出临界区
}

void test(int i) {
    // 如果哲学家i饿了，并且左右邻居都没在吃
    if (state[i] == HUNGRY &amp;#x26;&amp;#x26;
        state[(i+N-1) % N] != EATING &amp;#x26;&amp;#x26;
        state[(i+1) % N] != EATING)
    {
        state[i] = EATING;
        V(s[i]); // 唤醒哲学家i (如果他被阻塞了) 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 这个方案不会死锁，因为获取筷子的条件是两只都可用。同时，通过&lt;code&gt;test&lt;/code&gt;邻居，它也能保证一定的公平性，避免饥饿。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方案4：限制就餐人数 (破坏循环等待的变种)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想&lt;/strong&gt;: 最多只允许 N-1 (即4个) 哲学家&lt;strong&gt;同时&lt;/strong&gt;进入餐厅 (尝试拿筷子) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现&lt;/strong&gt;: 引入一个计数信号量 &lt;code&gt;room&lt;/code&gt;，并初始化为4。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 伪代码
semaphore room = 4; // 餐厅最多坐4人
void philosopher(int i) {
    while(true) {
        think();
        P(room); // 进入餐厅
        P(fork[i]);
        P(fork[(i+1) % 5]);
        eat();
        V(fork[(i+1) % 5]);
        V(fork[i]);
        V(room); // 离开餐厅
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为什么能行？&lt;/strong&gt;: 因为桌上总共5只筷子，4个哲学家最多拿走4x2=8只 (理论上) ，但实际上他们最多一人拿一只，也就是4只筷子。总有1只筷子是空闲的。这意味着，在最坏情况下 (4个哲学家都拿了左手筷子) ，那个没能进入餐厅的第5位哲学家的邻座，总能拿到他右手的筷子，从而吃上饭，吃完释放两只筷子，打破僵局。这实际上破坏了循环等待。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. 思考题解答&lt;/h2&gt;
&lt;h3&gt;思考题1：画出5个进程陷入死锁的所有非同构模型&lt;/h3&gt;
&lt;p&gt;这实际上是寻找5个节点的所有强连通有向图的非同构类。我按最小边数和结构复杂度系统分析：&lt;/p&gt;
&lt;h4&gt;第1类：最小强连通图 (5条边)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;模型1: 单个5环&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第2类：6条边的强连通图&lt;/h4&gt;
&lt;p&gt;**模型2: 5环+1条弦 (跨2个节点) **&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
    P1 --&gt; P3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型3: 4环+分支链&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P1
    P5 --&gt; P2 --&gt; P5
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第3类：7条边的强连通图&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;模型4: 5环+2条相邻弦&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
    P1 --&gt; P3
    P2 --&gt; P4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型5: 5环+2条对称弦&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
    P1 --&gt; P3
    P3 --&gt; P5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**模型6: 3环+2环 (共享1个节点) **&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P1
    P1 --&gt; P4 --&gt; P5 --&gt; P1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型7: 5环+2条非相邻、非对称弦&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
    P1 --&gt; P3
    P1 --&gt; P4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**模型8: 双4环结构 (共享1条边) **&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P1
    P2 --&gt; P4 --&gt; P5 --&gt; P2
    P1 --&gt; P4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第4类：8条边的强连通图&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;模型9: 3环+双分支汇聚&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P1
    P4 --&gt; P1
    P5 --&gt; P1
    P1 --&gt; P4
    P2 --&gt; P5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型10: 中心星型+反向环&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2
    P1 --&gt; P3
    P1 --&gt; P4
    P1 --&gt; P5
    P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型11: 双3环交叉结构&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P1
    P3 --&gt; P4 --&gt; P5 --&gt; P3
    P2 --&gt; P4
    P5 --&gt; P1
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;第5类：9条边及以上的复杂结构&lt;/h4&gt;
&lt;p&gt;**模型12: 准完全图 (缺少部分边的强连通图) **&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P1
    P2 --&gt; P3 --&gt; P2
    P3 --&gt; P4 --&gt; P3
    P4 --&gt; P5 --&gt; P4
    P5 --&gt; P1 --&gt; P5
    P1 --&gt; P3
    P2 --&gt; P4
    P3 --&gt; P5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;模型13: 高度连接的复杂图&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    P1 --&gt; P2 --&gt; P3 --&gt; P4 --&gt; P5 --&gt; P1
    P1 --&gt; P4
    P2 --&gt; P5
    P3 --&gt; P1
    P4 --&gt; P2
    P5 --&gt; P3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;根据图论分析，5个节点的强连通有向图共有&lt;strong&gt;13种&lt;/strong&gt;主要的非同构类。这些结构可以按边数分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;5条边&lt;/strong&gt;: 1种 (单5环)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;6条边&lt;/strong&gt;: 2种&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7条边&lt;/strong&gt;: 5种&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8条边&lt;/strong&gt;: 3种&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9条边及以上&lt;/strong&gt;: 2种典型结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每种结构都代表了一种可能的5进程死锁模式，死锁问题的复杂性远超简单的环形等待。&lt;/p&gt;
&lt;h3&gt;思考题2：学生用&quot;超时&quot;方法解决死锁，该给多少分？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;: 进程请求资源时启动计时器。如果因阻塞超时，就&quot;释放&quot;该进程并让它重新执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我的评价&lt;/strong&gt;:
作为老师，我会给这位同学一个&lt;strong&gt;及格分，但不会给高分&lt;/strong&gt;。比如 60-70分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点 (为什么给分)&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;抓住了问题&lt;/strong&gt;: 学生意识到了死锁是进程无限等待，并试图通过打破&quot;无限&quot;这个条件来解决问题，思路是对的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提出了一种可行的恢复策略&lt;/strong&gt;: 这本质上是一种&lt;strong&gt;死锁解除&lt;/strong&gt;机制，通过强制终止 (或重启) 进程来打破等待。在某些实时系统或健壮性要求高的系统中，类似&quot;看门狗&quot;(Watchdog Timer)的机制确实在被使用，防止任务卡死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简单易行&lt;/strong&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;strong&gt;治标不治本&lt;/strong&gt;: 它没有解决死锁产生的根本原因，只是在死锁发生后进行粗暴的干预。死锁问题依然会反复出现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时阈值难以确定&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;如果时间设得太&lt;strong&gt;短&lt;/strong&gt;，一个正常的、只是计算时间或I/O时间稍长的进程可能会被无辜&quot;杀死&quot;。&lt;/li&gt;
&lt;li&gt;如果时间设得太&lt;strong&gt;长&lt;/strong&gt;，系统可能已经在死锁状态下浪费了大量时间，才被发现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可能导致活锁&lt;/strong&gt;: 想象两个进程总是以同样的时序去申请同样的两个资源，它们可能都会因为超时而被反复重启，然后又在同一点上卡住，再次超时、重启……系统在不停地忙碌，但没有做任何有效的工作，这就是一种活锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据一致性问题&lt;/strong&gt;: 强行&quot;释放&quot;或重启一个进程，可能会导致它操作的数据处于不一致或损坏的状态。例如，一个事务只完成了一半。这就需要非常复杂的事务和回滚机制来配合，学生的方法里没有提到这一点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效率低下&lt;/strong&gt;: 反复重启进程的代价非常高。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 10: Classic Synchronization Problems</title><link>https://www.lyt0112.com/blog/operating_systems_note_10-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_10-zh</guid><description>Operating Systems Notes 10: 经典同步问题</description><pubDate>Fri, 06 Jun 2025 15:39:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-06-05&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;p&gt;在多道程序环境下, 多个进程并发执行, 它们之间或多或少会存在一些依赖关系. 为了协调这些进程的执行, 确保程序的正确性, 我们需要一套机制来让它们进行通信和同步, 这就是所谓的IPC (Inter-Process Communication) . 本讲聚焦于几种经典的同步问题, 并要求使用信号量 (Semaphore) 的P、V操作或管程 (Monitor) 来解决.&lt;/p&gt;
&lt;p&gt;本课程我们主要关注以下几个问题：另类PV操作问题、食品供应问题、三峡大坝问题、狒狒过峡谷问题、睡眠理发师问题, 以及最后的一个资源管理问题.&lt;/p&gt;
&lt;h2&gt;1 解决同步问题的系统方法论&lt;/h2&gt;
&lt;p&gt;无论是用信号量还是管程, 解决问题的第一步都是相同的：&lt;strong&gt;分析问题本身&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;1.1 分析模型 (与工具无关)&lt;/h3&gt;
&lt;p&gt;在写任何一行代码之前, 先用自然语言清晰地定义以下三个要素：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;识别&quot;角色&quot;(Actors) 和&quot;资源&quot;(Resources)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;角色&lt;/strong&gt;: 系统中有哪些并发执行的实体？(例如：生产者、消费者、读者、写者、上行船只、东行狒狒)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源&lt;/strong&gt;: 它们需要共享和访问什么？(例如：缓冲区、共享数据、船闸、绳索)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;定义&quot;规则&quot;, 即约束条件 (Constraints)&lt;/strong&gt;
这是最关键的一步. 规则分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥 (Mutual Exclusion)&lt;/strong&gt;: &quot;在任何时候, 只有&lt;strong&gt;一个&lt;/strong&gt;角色能做某件事&quot;. 这通常是为了保护共享资源的完整性.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子&lt;/em&gt;: 任何时候只能有一个理发师在修改 &lt;code&gt;waiting_chairs&lt;/code&gt; 计数器. 任何时候只能有一艘船在使用某一级船闸 &lt;code&gt;gate[i]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步 (Synchronization / Condition Synchronization)&lt;/strong&gt;: &quot;一个角色必须&lt;strong&gt;等待&lt;/strong&gt;某个条件为真才能继续&quot;. 这是角色之间的协作和时序关系.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子&lt;/em&gt;: 消费者必须&lt;strong&gt;等待&lt;/strong&gt;缓冲区里有产品. 理发师必须&lt;strong&gt;等待&lt;/strong&gt;有顾客到来. 船只要想上行, 必须&lt;strong&gt;等待&lt;/strong&gt;航向没有被下行船只占用.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一旦用自然语言把所有&quot;互斥&quot;和&quot;同步&quot;的规则都列出来, 问题就解决了一半. 下一步才是选择工具, 将这些规则&quot;翻译&quot;成代码.&lt;/p&gt;
&lt;h3&gt;1.2 一个重要的问题&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;读者可能会提出两个个非常好的问题, 这两个问题意义相反, 但是背后的原因是相同的&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在睡眠理发师问题中, &lt;code&gt;customers&lt;/code&gt;  (现在等待的顾客数) 不也是一个共享变量吗？为什么它被直接实现为一个信号量, 而不是一个受&lt;code&gt;mutex&lt;/code&gt;保护的整数？&lt;/li&gt;
&lt;li&gt;为什么在读者-写者问题中, &lt;code&gt;read_count&lt;/code&gt; (正在读取的读者数) 被实现为一个普通整型变量, 而不是一个信号量？&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是一个非常深刻的观察, 它揭示了信号量更强大的本质：&lt;strong&gt;信号量是一种封装了&quot;计数&quot;和&quot;等待&quot;的智能变量.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们选择用信号量直接实现&lt;code&gt;customers&lt;/code&gt;, 是因为理发师的同步规则 (&quot;若无顾客则等待&quot;) 与信号量的P操作完美匹配. &lt;code&gt;P(customers)&lt;/code&gt; 这一个动作就完成了&quot;检查顾客数, 如果为零则睡眠&quot;的全部逻辑.&lt;/p&gt;
&lt;p&gt;这是一种设计上的抽象和优化. 我们本可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 复杂的、非必要的实现
int customers_count = 0;
semaphore mutex = 1;
semaphore barber_is_sleeping = 0; // 单独的等待工具

// 理发师
P(mutex);
if (customers_count == 0) {
    V(mutex);
    P(barber_is_sleeping); // 在独立的信号量上等待
} else { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这种实现更繁琐, 引入了更多的变量和复杂性. 直接使用 &lt;code&gt;semaphore customers = 0;&lt;/code&gt; 更清晰、更优雅.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更精确的指导原则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当你的同步规则是简单的 &lt;strong&gt;&quot;等待, 直到某个资源的计数 &gt; 0&quot;&lt;/strong&gt; 时, 直接用一个信号量来代表这个资源是最佳实践. 例如：等待顾客(&lt;code&gt;customers&lt;/code&gt;)、等待产品(&lt;code&gt;full&lt;/code&gt;)、等待空位(&lt;code&gt;empty&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;当你的同步规则&lt;strong&gt;更复杂&lt;/strong&gt;, 需要读取一个计数值并基于它的值执行不同的逻辑分支时 (例如读者-写者问题中的&lt;code&gt;read_count&lt;/code&gt;, 你需要在第一个读者进来的时候锁住写者不让他进来, 而这个操作信号量是不支持的) , 你应该使用一个普通整型变量, 并用一个互斥锁来保护它的读写操作.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 选择工具并进行&quot;翻译&quot;&lt;/h3&gt;
&lt;h4&gt;1.3.1 方法论一：使用信号量 (P/V 操作)&lt;/h4&gt;
&lt;p&gt;信号量的核心思想是 &lt;strong&gt;&quot;计数&quot;&lt;/strong&gt;. 你把每个约束条件看作一种或多种需要计数的&quot;资源&quot;.&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;: 为每一个需要互斥访问的共享资源 (或代码段) 定义一个&lt;strong&gt;二元信号量 (mutex)&lt;/strong&gt;, 初值为 &lt;strong&gt;1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模式&lt;/strong&gt;: 在临界区代码前 &lt;code&gt;P(mutex)&lt;/code&gt;, 在之后 &lt;code&gt;V(mutex)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思考&lt;/strong&gt;: &quot;这个信号量代表了进入临界区的&apos;许可&apos;, 初始时有1个许可. &quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;翻译「同步」规则&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方法&lt;/strong&gt;: 为每一个&quot;等待条件&quot;定义一个&lt;strong&gt;通用信号量&lt;/strong&gt;, 并问自己：&lt;strong&gt;&quot;这个信号量在计数什么？&quot;&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;这个计数代表了角色正在等待的&quot;资源&quot;或&quot;事件&quot;的数量.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定初值&lt;/strong&gt;: 信号量的初值等于系统启动时, 它所计数的资源的&lt;strong&gt;初始数量&lt;/strong&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子 (睡眠理发师)&lt;/em&gt;: 顾客等待&quot;空闲的理发师&quot;. 系统开始时, 理发师都在睡觉 (不空闲) , 所以 &lt;code&gt;barbers&lt;/code&gt; 信号量初值为 &lt;strong&gt;0&lt;/strong&gt;. 理发师等待&quot;已到店的顾客&quot;. 系统开始时, 没有顾客, 所以 &lt;code&gt;customers&lt;/code&gt; 信号量初值为 &lt;strong&gt;0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;例子 (食品供应)&lt;/em&gt;: 供应者A等待&quot;A食品的空货架&quot;. 初始时货架全空, 所以 &lt;code&gt;emptyA&lt;/code&gt; 信号量初值为 &lt;strong&gt;m&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安排 P/V 操作&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P 操作&lt;/strong&gt;: 当一个角色&lt;strong&gt;需要/消耗&lt;/strong&gt;一个资源时, 它就执行 &lt;code&gt;P&lt;/code&gt; 操作. 如果资源数 (信号量的值) 为0, 它就自动等待.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子&lt;/em&gt;: 理发师需要一个顾客, 所以他 &lt;code&gt;P(customers)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V 操作&lt;/strong&gt;: 当一个角色&lt;strong&gt;生产/释放&lt;/strong&gt;一个资源时, 它就执行 &lt;code&gt;V&lt;/code&gt; 操作, 通知等待者资源增加了.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子&lt;/em&gt;: 顾客到店了, 他&quot;生产&quot;了一个&quot;理发请求&quot;, 所以他 &lt;code&gt;V(customers)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;1.3.2 方法论二：使用管程 (Monitor)&lt;/h4&gt;
&lt;p&gt;管程的核心思想是 &lt;strong&gt;&quot;封装和等待条件&quot;&lt;/strong&gt;. 它将共享的所有东西都锁在一个&quot;房间&quot;里, 角色按规则排队进入.&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;: 将所有共享变量 (如 &lt;code&gt;countA&lt;/code&gt;, &lt;code&gt;up_count&lt;/code&gt;, &lt;code&gt;writer_active&lt;/code&gt; 等) &lt;strong&gt;全部封装&lt;/strong&gt;在一个管程对象内部.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思考&lt;/strong&gt;: 管程自动保证了所有调用其方法 (过程) 的线程都是互斥的. 你&lt;strong&gt;不再需要&lt;/strong&gt;手动创建 &lt;code&gt;mutex&lt;/code&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;&lt;strong&gt;方法&lt;/strong&gt;: 为每一个&quot;等待原因&quot;定义一个&lt;strong&gt;条件变量 (Condition Variable)&lt;/strong&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名技巧&lt;/strong&gt;: 给条件变量起一个能反映&quot;等待者期望什么变为真&quot;的名字. 例如, &lt;code&gt;can_go_east&lt;/code&gt; (东行狒狒等待这个条件为真), &lt;code&gt;barber_available&lt;/code&gt; (理发师等待这个条件为真).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现管程过程 (经典模式: &lt;code&gt;while-wait-signal&lt;/code&gt;)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;while (condition_is_not_met)&lt;/code&gt;&lt;/strong&gt;: 在管程方法内部, &lt;strong&gt;永远使用 &lt;code&gt;while&lt;/code&gt; 循环&lt;/strong&gt;来检查等待条件, 而不是 &lt;code&gt;if&lt;/code&gt;. 这是因为当你被唤醒时, 你期望的条件可能已经被另一个刚被唤醒的进程改变了. &lt;code&gt;while&lt;/code&gt; 确保你继续执行时, 条件一定是满足的.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;wait(condition_variable)&lt;/code&gt;&lt;/strong&gt;: 在 &lt;code&gt;while&lt;/code&gt; 循环中, 如果条件不满足, 就调用 &lt;code&gt;wait&lt;/code&gt;. 这会&lt;strong&gt;原子地&lt;/strong&gt;做两件事：1) 释放管程的锁；2) 在这个条件变量上睡眠.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;signal(c)&lt;/code&gt; / &lt;code&gt;broadcast(c)&lt;/code&gt;&lt;/strong&gt;: 当你修改了共享状态后, 问自己：&quot;我的修改是否使得某个等待队列的条件成真了？&quot; 如果是, 就调用 &lt;code&gt;signal&lt;/code&gt; (唤醒一个) 或 &lt;code&gt;broadcast&lt;/code&gt; (唤醒所有) 来通知等待者.
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;例子 (狒狒过峡谷)&lt;/em&gt;: 当最后一个东行狒狒离开后 (&lt;code&gt;east_count--&lt;/code&gt; 变为 0), 它使得 &lt;code&gt;west_count &gt; 0&lt;/code&gt; 这个条件对西行狒狒不再成立, 所以它应该 &lt;code&gt;broadcast(can_go_west)&lt;/code&gt; 来唤醒所有等待的西行狒狒, 让它们重新检查.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.4 总结与最佳实践&lt;/h3&gt;
&lt;p&gt;| 特性         | 信号量 (P/V)                        | 管程 (Monitor)                            |
| :----------- | :---------------------------------- | :---------------------------------------- |
| &lt;strong&gt;核心思想&lt;/strong&gt; | 计数资源/事件                       | 封装状态, 等待条件                        |
| &lt;strong&gt;互斥实现&lt;/strong&gt; | &lt;strong&gt;手动&lt;/strong&gt; (使用&lt;code&gt;mutex&lt;/code&gt;信号量)        | &lt;strong&gt;自动&lt;/strong&gt; (由管程结构保证)                 |
| &lt;strong&gt;同步实现&lt;/strong&gt; | &lt;strong&gt;隐式&lt;/strong&gt; (通过信号量的值)           | &lt;strong&gt;显式&lt;/strong&gt; (通过条件变量&lt;code&gt;wait/signal&lt;/code&gt;)      |
| &lt;strong&gt;易错点&lt;/strong&gt;   | 忘记&lt;code&gt;P/V&lt;/code&gt;操作, 死锁, 信号量初值设错 | &lt;code&gt;wait&lt;/code&gt;条件用&lt;code&gt;if&lt;/code&gt;不用&lt;code&gt;while&lt;/code&gt;, 忘记&lt;code&gt;signal&lt;/code&gt; |
| &lt;strong&gt;抽象层级&lt;/strong&gt; | 较低, 更接近硬件                    | 较高, 更结构化, 易于管理复杂状态          |&lt;/p&gt;
&lt;h2&gt;2 纯同步问题&lt;/h2&gt;
&lt;p&gt;纯同步问题是指进程之间仅仅存在执行顺序上的依赖关系, 而没有互斥访问共享资源的需求. 这种依赖关系通常可以用前驱图 (Precedence Graph) 来描述.&lt;/p&gt;
&lt;h3&gt;2.1 例子：前驱图&lt;/h3&gt;
&lt;p&gt;下面是前驱图的例子, 箭头代表了进程 (或代码段) 执行的先后顺序. 例如, 从 S 指向 p1 的箭头表示 p1 必须在 S 完成后才能开始.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通用前驱图 (General precedence)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    S --&gt; p1
    p1 --&gt; p2
    p1 --&gt; p3
    p2 --&gt; p4
    p3 --&gt; p4
    p3 --&gt; p5
    p4 --&gt; p6
    p5 --&gt; p6
    p6 --&gt; p7
    p6 --&gt; p8
    p7 --&gt; F
    p8 --&gt; F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解法思路&lt;/strong&gt;:
解决这类问题的通用方法是, 为每一个前驱关系 (图中的每一条边) 设置一个信号量.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;信号量设置&lt;/strong&gt;: 对于从进程 $P_i$ 指向 $P_j$ 的每一条边, 都设置一个初始值为0的信号量 $S_{ij}$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程 $P_i$&lt;/strong&gt;: 在 $P_i$ 执行完毕后, 对所有从 $P_i$ 发出的边所对应的信号量执行 &lt;code&gt;V&lt;/code&gt; 操作. 例如, 如果 $P_i$ 是 $P_j$ 和 $P_k$ 的前驱, 则 $P_i$ 结尾处需要执行 $V(S_{ij})$ 和 $V(S_{ik})$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程 $P_j$&lt;/strong&gt;: 在 $P_j$ 开始执行前, 对所有指向 $P_j$ 的边所对应的信号量执行 &lt;code&gt;P&lt;/code&gt; 操作. 例如, 如果 $P_j$ 的前驱是 $P_i$ 和 $P_k$, 则 $P_j$ 开始处需要执行 $P(S_{ij})$ 和 $P(S_{kj})$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这种方式, 可以保证图中所定义的所有前驱关系都能得到满足.&lt;/p&gt;
&lt;h2&gt;3 另类P、V操作问题&lt;/h2&gt;
&lt;h3&gt;3.1 问题描述&lt;/h3&gt;
&lt;p&gt;有一个系统, 对P、V操作的定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P(s)&lt;/strong&gt;:
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;s.count --;
if (s.count &amp;#x3C; 0) {
    将本进程插入相应队列末尾等待;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V(s)&lt;/strong&gt;:
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;s.count ++;
if (s.count &amp;#x3C;= 0) {
    从相应等待队列队尾唤醒一个进程, 将其插入就绪队列;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;思考并回答&lt;/strong&gt;:
a. 这样定义P、V操作是否有问题？
b. 用这样的P、V操作实现N个进程竞争使用某一共享变量的互斥机制.
c. 对于b的解法, 有无效率更高的方法. 如有, 试问降低了多少复杂性？&lt;/p&gt;
&lt;h3&gt;3.2 使用P、V操作的问题解答&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;a. 这样定义P、V操作是否有问题？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;: 有问题. 关键在于&lt;code&gt;V(s)&lt;/code&gt;操作. 标准的&lt;code&gt;V&lt;/code&gt;操作在唤醒进程时, 是从等待队列的&lt;strong&gt;队头&lt;/strong&gt;唤醒一个进程, 这遵循了&quot;先来先服务&quot; (FCFS) 的原则, 可以保证等待的公平性, 避免某些进程饥饿 (长时间得不到执行) . 而题目中定义的&lt;code&gt;V&lt;/code&gt;操作是从&lt;strong&gt;队尾&lt;/strong&gt;唤醒进程, 这是一种&quot;后来先服务&quot; (LIFS) 的策略. 这会导致等待时间最长的进程可能一直得不到唤醒, 从而产生&lt;strong&gt;饥饿&lt;/strong&gt;现象. 因此, 这种定义是不公平的, 在实际系统中可能会导致严重问题.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;b. 用这样的P、V操作实现N个进程竞争共享变量的互斥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;: 尽管存在公平性问题, 但我们仍然可以用它来实现互斥. 实现互斥的经典方法是使用一个初值为1的信号量.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 信号量定义
semaphore mutex;
mutex.count = 1;

// N个进程的执行代码
Process_i() {
    while(true) {
        P(mutex);
        // --- 临界区开始 ---
        // 访问共享变量
        // --- 临界区结束 ---
        V(mutex);

        // 非临界区代码
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工作原理分析&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 信号量的初值为1, 表示允许一个进程进入临界区.&lt;/li&gt;
&lt;li&gt;第一个进程调用 &lt;code&gt;P(mutex)&lt;/code&gt;, &lt;code&gt;mutex.count&lt;/code&gt; 变为0, 进程进入临界区.&lt;/li&gt;
&lt;li&gt;此时若有第二个进程调用 &lt;code&gt;P(mutex)&lt;/code&gt;, &lt;code&gt;mutex.count&lt;/code&gt; 变为-1, 该进程被阻塞, 并加入等待队列. 之后再来的进程也同样被阻塞.&lt;/li&gt;
&lt;li&gt;第一个进程完成临界区操作后, 调用 &lt;code&gt;V(mutex)&lt;/code&gt;, &lt;code&gt;mutex.count&lt;/code&gt; 变为0 (&lt;code&gt;-1+1=0&lt;/code&gt;). 因为 &lt;code&gt;mutex.count &amp;#x3C;= 0&lt;/code&gt;, 所以会从等待队列的队尾唤醒一个进程.&lt;/li&gt;
&lt;li&gt;被唤醒的进程进入临界区, 其他进程继续等待. 这样就保证了任意时刻只有一个进程在临界区内, 实现了互斥.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;c. 效率和复杂性分析&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;: 这里的&quot;效率&quot;可以从两个角度理解：一是算法的执行效率, 二是系统的公平性.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;执行效率&lt;/strong&gt;: 对于单个P或V操作来说, 无论是从队头还是队尾操作, 其时间复杂度通常都是 $O(1)$. 所以单次操作的执行效率没有太大差别.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;公平性与系统整体效率&lt;/strong&gt;: 标准的队头唤醒 (FIFO) 方式, 其公平性好. 而题目中的队尾唤醒 (LIFO) 方式, 公平性差, 可能导致饥饿. 从系统整体来看, 饥饿现象会降低系统的效率和响应性.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;是否有更高效率的方法？&lt;/strong&gt;
如果指的是解决互斥问题的效率, 使用信号量本身就是一种非常高效的方法. 关键在于&lt;code&gt;V&lt;/code&gt;操作的实现. 标准的、从队头唤醒进程的&lt;code&gt;V&lt;/code&gt;操作是更优的实现, 因为它保证了公平性. 所以, &lt;strong&gt;&quot;更高效率&quot;的方法就是使用标准定义的V操作&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;降低了多少复杂性？&lt;/strong&gt;
从实现上讲, 将队尾唤醒改为队头唤醒, 并没有增加或降低算法本身的实现复杂性. 但是, 它&lt;strong&gt;降低了系统行为的复杂性&lt;/strong&gt;. 使用标准的FIFO策略, 我们更容易预测和分析系统的行为, 可以保证进程的等待时间是有限的, 从而避免了饥饿这个复杂且难以处理的问题. 可以说, 它通过保证公平性, 大大降低了系统出现异常行为的风险.&lt;/p&gt;
&lt;h2&gt;4 食品供货问题&lt;/h2&gt;
&lt;h3&gt;4.1 问题描述&lt;/h3&gt;
&lt;p&gt;某商店有两种食品A和B, 最大容量各为 &lt;code&gt;m&lt;/code&gt; 个. 商店将A、B两种食品搭配出售 (每次各取一个) . 为保证新鲜, 遵循&quot;先到食品先出售&quot;原则. 有两个食品公司分别不断地供应A和B. 为保证正常销售, 当某种食品的数量比另一种的数量超过 &lt;code&gt;k&lt;/code&gt; ($k &amp;#x3C; m$) 个时, 暂停对数量大的食品进货. 试用P、V操作解决同步和互斥关系.&lt;/p&gt;
&lt;h3&gt;4.2 使用P、V操作的问题解答&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 关系分析&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥关系&lt;/strong&gt;: 商店的库存 (食品A和B的数量) 是共享变量, 所有进程 (供应A、供应B、销售) 在修改库存数量时都必须互斥. 我们可以用一个信号量 &lt;code&gt;mutex&lt;/code&gt; 来实现.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步关系&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;销售员&lt;/strong&gt;: 必须等到商店里同时有食品A和食品B时才能进行销售.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;供应A&lt;/strong&gt;: 当A的数量已满(&lt;code&gt;m&lt;/code&gt;个)时, 或当A比B多&lt;code&gt;k&lt;/code&gt;个时, 需要暂停供应.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;供应B&lt;/strong&gt;: 当B的数量已满(&lt;code&gt;m&lt;/code&gt;个)时, 或当B比A多&lt;code&gt;k&lt;/code&gt;个时, 需要暂停供应.&lt;/li&gt;
&lt;/ul&gt;
&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;&lt;code&gt;mutex&lt;/code&gt;: 互斥信号量, 保护对库存数量的访问, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emptyA&lt;/code&gt;, &lt;code&gt;emptyB&lt;/code&gt;: 表示A和B的空闲货架数, 初值均为 &lt;code&gt;m&lt;/code&gt;. 用于控制库存上限.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;foodA&lt;/code&gt;, &lt;code&gt;foodB&lt;/code&gt;: 表示已有的A和B食品数量, 初值均为 0. 用于销售员判断是否可以销售.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;limitA&lt;/code&gt;: 控制&quot;A比B数量多k个&quot;的限制. 当A比B多&lt;code&gt;k&lt;/code&gt;个时, A的供应者需要等待. 可以看作是A相对于B的&quot;空闲容量&quot;. 初值为 &lt;code&gt;k&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;limitB&lt;/code&gt;: 控制&quot;B比A数量多k个&quot;的限制. 同理, 初值为 &lt;code&gt;k&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 共享变量
int countA = 0, countB = 0; // 食品数量

// 信号量
semaphore mutex = 1;
semaphore emptyA = m, emptyB = m;
semaphore foodA = 0, foodB = 0;
semaphore limitA = k, limitB = k; // A比B, B比A的富余量

// 供应A进程
ProviderA() {
    while(true) {
        P(emptyA);       // 是否还有空货架放A?
        P(limitB);       // B比A的数量是否已经多了k个？
                         // P(limitB)意味着消耗一个B对A的富余名额
        P(mutex);
        // 生产一个A并放入
        countA++;
        V(mutex);

        V(limitA);       // 增加一个A对B的富余名额
        V(foodA);        // 增加一个食品A
    }
}

// 供应B进程
ProviderB() {
    while(true) {
        P(emptyB);       // 是否还有空货架放B?
        P(limitA);       // A比B的数量是否已经多了k个？
                         // P(limitA)意味着消耗一个A对B的富余名额
        P(mutex);
        // 生产一个B并放入
        countB++;
        V(mutex);

        V(limitB);       // 增加一个B对A的富余名额
        V(foodB);        // 增加一个食品B
    }
}

// 销售员进程
Seller() {
    while(true) {
        P(foodA); // 是否有食品A?
        P(foodB); // 是否有食品B?

        P(mutex);
        // 取走A和B
        countA--;
        countB--;
        V(mutex);

        V(emptyA); // 货架空出
        V(emptyB); // 货架空出
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 使用管程的解决方案&lt;/h3&gt;
&lt;p&gt;使用管程可以更结构化地解决此问题. 我们将商店的库存和操作封装在一个管程内.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;countA&lt;/code&gt;, &lt;code&gt;countB&lt;/code&gt;: 两种食品的当前数量.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;canSupplyA&lt;/code&gt;: 当A食品无法供应时 (已满或比B多k个), 供应者A在此等待.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canSupplyB&lt;/code&gt;: 当B食品无法供应时, 供应者B在此等待.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canSell&lt;/code&gt;: 当A或B食品缺货时, 销售员在此等待.&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;SupplyA(item)&lt;/code&gt;: 供货商A调用.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SupplyB(item)&lt;/code&gt;: 供货商B调用.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sell()&lt;/code&gt;: 销售员调用.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class FoodStore {
    int countA = 0, countB = 0;
    condition canSupplyA, canSupplyB, canSell;

    // 供应A食品
    public void SupplyA() {
        while (countA &gt;= m || countA - countB &gt;= k) {
            wait(canSupplyA);
        }
        // 放入食品A
        countA++;
        // 唤醒可能在等待的销售员和供应者B
        notify(canSell);
        if (countB - countA &amp;#x3C; k) { // 检查是否可以解除对B的限制
             broadcast(canSupplyB); // 使用broadcast更安全, 唤醒所有等待的B供应者
        }
    }

    // 供应B食品
    public void SupplyB() {
        while (countB &gt;= m || countB - countA &gt;= k) {
            wait(canSupplyB);
        }
        // 放入食品B
        countB++;
        // 唤醒可能在等待的销售员和供应者A
        notify(canSell);
        if (countA - countB &amp;#x3C; k) { // 检查是否可以解除对A的限制
            broadcast(canSupplyA);
        }
    }

    // 销售
    public void Sell() {
        while (countA &amp;#x3C; 1 || countB &amp;#x3C; 1) {
            wait(canSell);
        }
        // 取走食品
        countA--;
        countB--;
        // 唤醒可能在等待的供应者
        broadcast(canSupplyA);
        broadcast(canSupplyB);
    }
}

// 进程调用
ProviderA() { while(true) { FoodStore.SupplyA(); } }
ProviderB() { while(true) { FoodStore.SupplyB(); } }
Seller() { while(true) { FoodStore.Sell(); } }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;封装与互斥&lt;/strong&gt;: 管程 &lt;code&gt;FoodStore&lt;/code&gt; 封装了所有共享状态 (&lt;code&gt;countA&lt;/code&gt;, &lt;code&gt;countB&lt;/code&gt;), 并自动保证了对这些状态的所有操作 (&lt;code&gt;SupplyA&lt;/code&gt;, &lt;code&gt;SupplyB&lt;/code&gt;, &lt;code&gt;Sell&lt;/code&gt;) 都是互斥的, 无需手动使用 &lt;code&gt;mutex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件等待&lt;/strong&gt;: 每个进程在进入管程后, 使用 &lt;code&gt;while&lt;/code&gt; 循环检查其执行条件. 如果条件不满足, 则调用 &lt;code&gt;wait()&lt;/code&gt; 在相应的条件变量上等待, 这会自动释放管程的锁.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;唤醒&lt;/strong&gt;: 当一个进程改变了系统状态后 (如增加或减少了食品), 它会调用 &lt;code&gt;notify()&lt;/code&gt; 或 &lt;code&gt;broadcast()&lt;/code&gt; 来唤醒可能在等待的其它进程. 例如, &lt;code&gt;Sell&lt;/code&gt; 过程完成后, 货架空出, 两种食品的数量关系也可能改变, 因此它 &lt;code&gt;broadcast&lt;/code&gt; 唤醒所有可能在等待的供应者, 让它们重新检查条件. 使用 &lt;code&gt;while&lt;/code&gt; 而不是 &lt;code&gt;if&lt;/code&gt; 来检查等待条件, 确保了被唤醒的进程在继续执行前, 其条件一定是满足的, 这是一种健壮的并发编程模式.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5 三峡大坝船闸调度问题&lt;/h2&gt;
&lt;h3&gt;5.1 问题描述&lt;/h3&gt;
&lt;p&gt;三峡大坝有五级船闸 $T_1 \sim T_5$. 上游船只依次通过 $T_1 \to T_2 \to T_3 \to T_4 \to T_5$ 到达下游, 下游船只依次通过 $T_5 \to T_4 \to T_3 \to T_2 \to T_1$ 到达上游. 假设船闸为单向通行, 即在同一时间段内, 所有五级船闸要么都给上游船只使用, 要么都给下游船只使用. 试用P、V操作解决该调度问题.&lt;/p&gt;
&lt;h3&gt;5.2 使用P、V操作的问题解答&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 关系分析&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥关系&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;船闸互斥&lt;/strong&gt;: 每一级船闸 ($T_1$ 到 $T_5$) 在同一时刻只能被一艘船使用.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方向互斥&lt;/strong&gt;: 整个船闸系统在同一时间只能有一个通行方向. 这是一个典型的&quot;读者-写者问题&quot;的变体. 我们可以将上行和下行的船队看作两类&quot;读者&quot;, 但这两类&quot;读者&quot;是互斥的.&lt;/li&gt;
&lt;/ol&gt;
&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;&lt;code&gt;gate[5]&lt;/code&gt;: 一个信号量数组, &lt;code&gt;gate[i]&lt;/code&gt; 代表第 $i+1$ 级船闸, 初值均为 1, 用于保证每级船闸的互斥使用.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up_mutex&lt;/code&gt;, &lt;code&gt;down_mutex&lt;/code&gt;: 两个互斥信号量, 初值为 1, 用于保护上行船只计数器 &lt;code&gt;up_count&lt;/code&gt; 和下行船只计数器 &lt;code&gt;down_count&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;up_count&lt;/code&gt;, &lt;code&gt;down_count&lt;/code&gt;: 两个整型变量, 初值为 0, 记录正在船闸系统中航行的上行和下行船只数量.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;direction_mutex&lt;/code&gt;: 关键的&quot;方向&quot;信号量, 初值为 1. 用于保证同一时间只有一个方向的船只可以通过. 第一个进入系统的船 (无论是上行还是下行) 将持有这个信号量, 最后一个离开系统的船将释放它.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 信号量和计数器
semaphore gate[5] = {1, 1, 1, 1, 1}; // 5个船闸
semaphore up_mutex = 1, down_mutex = 1;
semaphore direction_mutex = 1;
int up_count = 0, down_count = 0;

// 上行船只进程 (下游 -&gt; 上游: T5 -&gt; T1)
Upward_Ship() {
    // ---- 进入船闸系统 ----
    P(up_mutex);
    if (up_count == 0) { // 如果是第一艘上行船
        P(direction_mutex); // 尝试获取航行方向控制权
    }
    up_count++;
    V(up_mutex);

    // ---- 依次通过船闸 ----
    P(gate[4]); // 过 T5
    // 正在过 T5 ...
    V(gate[4]);

    P(gate[3]); // 过 T4
    // 正在过 T4 ...
    V(gate[3]);

    P(gate[2]); // 过 T3
    // 正在过 T3 ...
    V(gate[2]);

    P(gate[1]); // 过 T2
    // 正在过 T2 ...
    V(gate[1]);

    P(gate[0]); // 过 T1
    // 正在过 T1 ...
    V(gate[0]);

    // ---- 离开船闸系统 ----
    P(up_mutex);
    up_count--;
    if (up_count == 0) { // 如果是最后一艘上行船
        V(direction_mutex); // 释放航行方向控制权
    }
    V(up_mutex);
}

// 下行船只进程 (上游 -&gt; 下游: T1 -&gt; T5)
Downward_Ship() {
    // ---- 进入船闸系统 ----
    P(down_mutex);
    if (down_count == 0) { // 如果是第一艘下行船
        P(direction_mutex); // 尝试获取航行方向控制权
    }
    down_count++;
    V(down_mutex);

    // ---- 依次通过船闸 ----
    P(gate[0]); // 过 T1
    // 正在过 T1 ...
    V(gate[0]);
    
    P(gate[1]); // 过 T2
    // 正在过 T2 ...
    V(gate[1]);

    P(gate[2]); // 过 T3
    // 正在过 T3 ...
    V(gate[2]);

    P(gate[3]); // 过 T4
    // 正在过 T4 ...
    V(gate[3]);

    P(gate[4]); // 过 T5
    // 正在过 T5 ...
    V(gate[4]);

    // ---- 离开船闸系统 ----
    P(down_mutex);
    down_count--;
    if (down_count == 0) { // 如果是最后一艘下行船
        V(direction_mutex); // 释放航行方向控制权
    }
    V(down_mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gate[i]&lt;/code&gt; 保证了每个船闸的互斥使用.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;direction_mutex&lt;/code&gt; 是解决问题的核心. 当一个方向 (例如上行) 的第一艘船到达时, 它会执行 &lt;code&gt;P(direction_mutex)&lt;/code&gt;, 从而锁定了航行方向. 在此期间, 任何试图从相反方向 (下行) 进入的第一艘船都会因为 &lt;code&gt;P(direction_mutex)&lt;/code&gt; 而被阻塞.&lt;/li&gt;
&lt;li&gt;只有当该方向的所有船只都通过并离开后 (&lt;code&gt;up_count&lt;/code&gt; 减为 0) , 最后一艘船才会执行 &lt;code&gt;V(direction_mutex)&lt;/code&gt;, 从而允许相反方向的船只开始通行. 这完美地实现了&quot;单向通行&quot;的要求.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 使用管程的解决方案&lt;/h3&gt;
&lt;p&gt;此问题本质上是&quot;两类读者互斥&quot;的读者-写者问题变体. 管程非常适合解决此类问题. 我们将整个船闸系统的调度逻辑封装在一个管程中.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;up_count&lt;/code&gt;, &lt;code&gt;down_count&lt;/code&gt;: 记录正在系统中的上行和下行船只数.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;direction&lt;/code&gt;: 表示当前航行方向 (&lt;code&gt;UP&lt;/code&gt;, &lt;code&gt;DOWN&lt;/code&gt;, 或 &lt;code&gt;NONE&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;up_q&lt;/code&gt;: 上行船只在此等待.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;down_q&lt;/code&gt;: 下行船只在此等待.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RequestUpstreamPass()&lt;/code&gt;: 上行船只请求进入.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FinishUpstreamPass()&lt;/code&gt;: 上行船只完成通过.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RequestDownstreamPass()&lt;/code&gt;: 下行船只请求进入.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FinishDownstreamPass()&lt;/code&gt;: 下行船只完成通过.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class DamLockControl {
    int up_count = 0, down_count = 0;
    // 使用一个枚举或整数表示方向: 0-NONE, 1-UP, 2-DOWN
    int current_direction = 0; 
    condition up_q, down_q;

    // 上行船只请求进入
    public void RequestUpstreamPass() {
        // 如果当前是下行方向, 则等待
        while (current_direction == 2) {
            wait(up_q);
        }
        up_count++;
        current_direction = 1; // 占定方向为上行
    }

    // 上行船只完成通过
    public void FinishUpstreamPass() {
        up_count--;
        if (up_count == 0) {
            current_direction = 0; // 释放方向
            broadcast(down_q);     // 通知所有等待的下行船只
        }
    }

    // 下行船只请求进入
    public void RequestDownstreamPass() {
        // 如果当前是上行方向, 则等待
        while (current_direction == 1) {
            wait(down_q);
        }
        down_count++;
        current_direction = 2; // 占定方向为下行
    }

    // 下行船只完成通过
    public void FinishDownstreamPass() {
        down_count--;
        if (down_count == 0) {
            current_direction = 0; // 释放方向
            broadcast(up_q);       // 通知所有等待的上行船只
        }
    }
}

// 船只进程
Upward_Ship() {
    DamLockControl.RequestUpstreamPass();
    // 依次通过T5 -&gt; T1... (这部分互斥可以用信号量或更细粒度的管程)
    DamLockControl.FinishUpstreamPass();
}

Downward_Ship() {
    DamLockControl.RequestDownstreamPass();
    // 依次通过T1 -&gt; T5...
    DamLockControl.FinishDownstreamPass();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方向控制&lt;/strong&gt;: 管程通过 &lt;code&gt;current_direction&lt;/code&gt; 变量显式地管理航行方向. 当一个方向的船只请求进入时, 它会检查 &lt;code&gt;current_direction&lt;/code&gt;. 如果方向冲突, 它就在相应的条件变量上等待.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进入与离开&lt;/strong&gt;: 第一个进入的船只 (任一方向) 会设定 &lt;code&gt;current_direction&lt;/code&gt;, 从而阻止相反方向的船只. 最后一个离开的船只负责重置 &lt;code&gt;current_direction&lt;/code&gt; 并用 &lt;code&gt;broadcast&lt;/code&gt; 唤醒所有等待的对向狒狒, 把通行权交给它们.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化&lt;/strong&gt;: 此管程方案优雅地解决了核心的&quot;方向互斥&quot;问题. 对于&quot;每级船闸只能有一艘船&quot;的次级互斥问题, 可以使用单独的信号量数组 (如原解法) , 或者为每级船闸设计一个简单的互斥管程. 将两者结合即可得到完整解法.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6 狒狒过峡谷问题&lt;/h2&gt;
&lt;p&gt;这个问题实际上是&quot;读者-写者问题&quot;的一个非常形象的变体.&lt;/p&gt;
&lt;h3&gt;6.1 问题概述&lt;/h3&gt;
&lt;p&gt;一根绳索横跨峡谷, 狒狒可以沿绳索过峡谷. 只要方向相同, 可以有多只狒狒同时在绳索上. 但如果不同方向的狒狒同时上绳, 就会在中间相遇, 产生死锁. 编写一个避免死锁的程序.&lt;/p&gt;
&lt;p&gt;不考虑饥饿问题或者考虑饥饿问题.&lt;/p&gt;
&lt;h3&gt;6.2 使用P、V操作的解答 (不考虑饥饿问题)&lt;/h3&gt;
&lt;p&gt;这个问题和三峡大坝问题非常相似. 我们可以把向东的狒狒看作一类&quot;读者&quot;, 向西的狒狒看作另一类&quot;读者&quot;, 这两类读者是互斥的.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 信号量设置&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt;: 互斥信号量, 保护对东行和西行计数器的访问, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;east_count&lt;/code&gt;, &lt;code&gt;west_count&lt;/code&gt;: 计数器, 记录正在绳索上向东和向西的狒狒数量, 初值为 0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rope&lt;/code&gt;: 代表绳索使用权的信号量, 初值为 1. 第一个要上绳的狒狒群体 (无论是东行还是西行) 获取它, 最后一个离开的狒狒释放它.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 信号量和计数器
semaphore mutex = 1;
semaphore rope = 1;
int east_count = 0, west_count = 0; // 可以合并为一个方向计数器和一个方向变量

// 向东走的狒狒
Eastward_Baboon() {
    P(mutex);
    if (east_count == 0) { // 如果是第一只向东的狒狒
        P(rope);           // 尝试获取绳索
    }
    east_count++;
    V(mutex);

    // --- 过峡谷 ---
    // crossing the rope...
    // --- 过完峡谷 ---

    P(mutex);
    east_count--;
    if (east_count == 0) { // 如果是最后一只向东的狒狒
        V(rope);           // 释放绳索
    }
    V(mutex);
}

// 向西走的狒狒
Westward_Baboon() {
    P(mutex);
    // 这里我们用 west_count 来实现, 逻辑完全对称
    if (west_count == 0) { // 如果是第一只向西的狒狒
        P(rope);           // 尝试获取绳索
    }
    west_count++;
    V(mutex);

    // --- 过峡谷 ---
    // crossing the rope...
    // --- 过完峡谷 ---

    P(mutex);
    west_count--;
    if (west_count == 0) { // 如果是最后一只向西的狒狒
        V(rope);           // 释放绳索
    }
    V(mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;:
这个解法是&quot;读者优先&quot;的变体. 例如, 如果有一批狒狒正在向东走, &lt;code&gt;rope&lt;/code&gt; 信号量被持有. 此时, 任何想向西走的狒狒都会在 &lt;code&gt;P(rope)&lt;/code&gt; 处被阻塞. 而新来的向东的狒狒则可以不受阻碍地直接上绳. 这可能导致西行的狒狒饥饿.&lt;/p&gt;
&lt;h3&gt;6.3 使用管程的解答 (不考虑饥饿问题)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个管程 &lt;code&gt;RopeBridge&lt;/code&gt; 将封装所有共享状态和同步逻辑.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;east_count&lt;/code&gt;: 记录正在向东走的狒狒数量.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;west_count&lt;/code&gt;: 记录正在向西走的狒狒数量.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;can_go_east&lt;/code&gt;: 准备向东走的狒狒在此条件上等待.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;can_go_west&lt;/code&gt;: 准备向西走的狒狒在此条件上等待.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程 (Procedures)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ArriveEast()&lt;/code&gt; / &lt;code&gt;LeaveEast()&lt;/code&gt;: 东行狒狒调用的进入和离开方法.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArriveWest()&lt;/code&gt; / &lt;code&gt;LeaveWest()&lt;/code&gt;: 西行狒狒调用的进入和离开方法.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class RopeBridge {
    int east_count = 0;
    int west_count = 0;
    condition can_go_east, can_go_west;

    // 狒狒准备向东走时调用
    public void ArriveEast() {
        // 如果有任何狒狒正在向西走, 则必须等待
        while (west_count &gt; 0) {
            wait(can_go_east);
        }
        // 允许通行, 东行计数加一
        east_count++;
    }

    // 狒狒向东走完后调用
    public void LeaveEast() {
        // 东行计数减一
        east_count--;
        // 如果我是最后一个东行的狒狒, 就唤醒所有等待西行的狒狒
        if (east_count == 0) {
            broadcast(can_go_west);
        }
    }

    // 狒狒准备向西走时调用
    public void ArriveWest() {
        // 如果有任何狒狒正在向东走, 则必须等待
        while (east_count &gt; 0) {
            wait(can_go_west);
        }
        // 允许通行, 西行计数加一
        west_count++;
    }

    // 狒狒向西走完后调用
    public void LeaveWest() {
        // 西行计数减一
        west_count--;
        // 如果我是最后一个西行的狒狒, 就唤醒所有等待东行的狒狒
        if (west_count == 0) {
            broadcast(can_go_east);
        }
    }
}

// 外部调用逻辑
// 向东走的狒狒
Eastward_Baboon() {
    RopeBridge.ArriveEast();
    // --- 正在过峡谷 ---
    RopeBridge.LeaveEast();
}

// 向西走的狒狒
Westward_Baboon() {
    RopeBridge.ArriveWest();
    // --- 正在过峡谷 ---
    RopeBridge.LeaveWest();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;
这个解法是&quot;读者优先&quot;模式的管程实现. 例如, 当有一批狒狒正在向东走时 (&lt;code&gt;east_count &gt; 0&lt;/code&gt;), 任何新到达的、想向东走的狒狒都可以立即通过 &lt;code&gt;ArriveEast&lt;/code&gt; 方法并上绳. 然而, 所有想向西走的狒狒都会在 &lt;code&gt;ArriveWest&lt;/code&gt; 中的 &lt;code&gt;wait(can_go_west)&lt;/code&gt; 处被阻塞. 只有当最后一个东行狒狒调用 &lt;code&gt;LeaveEast&lt;/code&gt; 并执行 &lt;code&gt;broadcast(can_go_west)&lt;/code&gt; 后, 西行狒狒才可能被唤醒.&lt;/p&gt;
&lt;p&gt;这种策略的缺点是, 如果东行狒狒源源不断地到来, 那么西行狒狒可能会永远等待下去, 导致&lt;strong&gt;饥饿&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;6.4 使用P、V操作的解答 (考虑饥饿问题)&lt;/h3&gt;
&lt;p&gt;在6.2节的解法中, 一个方向的狒狒流可能会导致另一个方向的狒狒永久等待, 产生饥饿. 为了解决这个问题, 我们可以增加一个&quot;旋转门&quot; (turnstile) 信号量, 来保证对绳索的请求是公平的, 实现先到先服务 (FCFS).&lt;/p&gt;
&lt;p&gt;我觉得这是一个很巧妙的算法!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 信号量设置&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mutex_e&lt;/code&gt;, &lt;code&gt;mutex_w&lt;/code&gt;: 分别保护 &lt;code&gt;east_count&lt;/code&gt; 和 &lt;code&gt;west_count&lt;/code&gt; 的互斥锁, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;east_count&lt;/code&gt;, &lt;code&gt;west_count&lt;/code&gt;: 计数器, 记录正在绳索上的狒狒数量, 初值为 0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rope&lt;/code&gt;: 代表绳索的使用权, 保证方向的互斥, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;turnstile&lt;/code&gt;: 新增的&quot;旋转门&quot;或&quot;门卫&quot;信号量, 所有狒狒在尝试获取绳索前都必须先通过它. 这确保了对&lt;code&gt;rope&lt;/code&gt;信号量的竞争是公平的. 初值为 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 信号量和计数器
semaphore mutex_e = 1, mutex_w = 1;
semaphore rope = 1;
semaphore turnstile = 1; // 保证公平的旋转门
int east_count = 0, west_count = 0;

// 向东走的狒狒
Eastward_Baboon() {
    P(turnstile); // 在旋转门前排队
    P(mutex_e);
    east_count++;
    if (east_count == 1) { // 如果是第一只东行狒狒
        P(rope);           // 尝试为东行方向获取绳索
    }
    V(mutex_e);
    V(turnstile); // 通过旋转门, 让下一个人/狒狒可以排队

    // --- 过峡谷 ---

    P(mutex_e);
    east_count--;
    if (east_count == 0) { // 如果是最后一只东行狒狒
        V(rope);           // 释放绳索
    }
    V(mutex_e);
}

// 向西走的狒狒
Westward_Baboon() {
    P(turnstile); // 在旋转门前排队
    P(mutex_w);
    west_count++;
    if (west_count == 1) { // 如果是第一只西行狒狒
        P(rope);           // 尝试为西行方向获取绳索
    }
    V(mutex_w);
    V(turnstile); // 通过旋转门

    // --- 过峡谷 ---

    P(mutex_w);
    west_count--;
    if (west_count == 0) { // 如果是最后一只西行狒狒
        V(rope);           // 释放绳索
    }
    V(mutex_w);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公平性保证&lt;/strong&gt;: &lt;code&gt;turnstile&lt;/code&gt; 是解决饥饿问题的关键. 假设有一队东行狒狒正在过桥, &lt;code&gt;rope&lt;/code&gt; 信号量被东行者持有. 此时, 一只西行狒狒 &lt;code&gt;W1&lt;/code&gt; 到达, 它会通过 &lt;code&gt;turnstile&lt;/code&gt; 并阻塞在 &lt;code&gt;P(rope)&lt;/code&gt; 上. 紧接着, 一只新的东行狒狒 &lt;code&gt;E_new&lt;/code&gt; 到达. &lt;code&gt;E_new&lt;/code&gt; 会被 &lt;code&gt;P(turnstile)&lt;/code&gt; 阻塞, 因为 &lt;code&gt;W1&lt;/code&gt; 还没有执行 &lt;code&gt;V(turnstile)&lt;/code&gt;. 这样, &lt;code&gt;E_new&lt;/code&gt; 就必须排在 &lt;code&gt;W1&lt;/code&gt; 之后. 当东行队伍全部过完并释放 &lt;code&gt;rope&lt;/code&gt; 时, &lt;code&gt;W1&lt;/code&gt; 就能获得绳索, &lt;code&gt;E_new&lt;/code&gt; 则需等待 &lt;code&gt;W1&lt;/code&gt; 的队伍过完. 这就避免了西行狒狒的饥饿.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.5 使用管程的解答 (考虑饥饿问题)&lt;/h3&gt;
&lt;p&gt;由于管程调度器往往是先到先服务, 是公平的, 所以我们只要保证在管程中, 所有等待的狒狒都在同一个条件变量上排队, 就可以保证公平性.&lt;/p&gt;
&lt;p&gt;但是这样会导致狒狒只能一个一个过, 即使只有同一个方向的也会在一个过去之后再过下一个, 我还没想到更好的算法, 如果有人有更好的算法, 欢迎在评论区提出!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;east_count&lt;/code&gt;, &lt;code&gt;west_count&lt;/code&gt;: 记录正在绳索上的狒狒数量.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rope_is_busy&lt;/code&gt;: 一个布尔标志, 表示绳索是否被任何一个方向占用.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;can_try&lt;/code&gt;: 一个单一的条件变量, 所有等待的狒狒都在此排队.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Arrive(direction)&lt;/code&gt;: 狒狒到达时调用, 传入其方向.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Leave(direction)&lt;/code&gt;: 狒狒离开时调用.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class FairRopeBridge {
    int east_count = 0;
    int west_count = 0;
    // 使用一个单一的条件变量来模拟公平的等待队列
    condition can_try;

    // 狒狒到达, 准备上绳
    public void Arrive(direction) {
        // 检查是否可以上绳.
        // 如果当前是相反方向, 则必须等待.
        if (direction == EAST) {
            while (west_count &gt; 0) {
                wait(can_try);
            }
            east_count++;
        } else { // direction == WEST
            while (east_count &gt; 0) {
                wait(can_try);
            }
            west_count++;
        }
    }

    // 狒狒离开
    public void Leave(direction) {
        if (direction == EAST) {
            east_count--;
            // 如果我是最后一个, 唤醒所有等待者, 让他们重新竞争
            if (east_count == 0) {
                broadcast(can_try);
            }
        } else { // direction == WEST
            west_count--;
            // 如果我是最后一个, 唤醒所有等待者
            if (west_count == 0) {
                broadcast(can_try);
            }
        }
    }
}

// 外部调用逻辑
Eastward_Baboon() {
    FairRopeBridge.Arrive(EAST);
    // --- 正在过峡谷 ---
    FairRopeBridge.Leave(EAST);
}

Westward_Baboon() {
    FairRopeBridge.Arrive(WEST);
    // --- 正在过峡谷 ---
    FairRopeBridge.Leave(WEST);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公平调度&lt;/strong&gt;: 此管程模型通过将所有等待的狒狒（无论方向）都放在同一个条件变量 &lt;code&gt;can_try&lt;/code&gt; 上, 实现了公平性. 当一批狒狒过完峡谷, 最后一个离开的狒狒会调用 &lt;code&gt;broadcast(can_try)&lt;/code&gt;. 这会唤醒所有在 &lt;code&gt;wait(can_try)&lt;/code&gt; 上睡眠的进程.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;竞争与 &lt;code&gt;while&lt;/code&gt; 循环&lt;/strong&gt;: 被唤醒的狒狒们会重新开始竞争进入管程. &lt;strong&gt;由于调度器通常是公平的&lt;/strong&gt;, 等待时间最长的狒狒有很大概率先被调度. 它会重新检查 &lt;code&gt;while&lt;/code&gt; 循环的条件 (&lt;code&gt;west_count &gt; 0&lt;/code&gt; 或 &lt;code&gt;east_count &gt; 0&lt;/code&gt;). 如果绳索对它来说是空闲的, 它就能成功跳出循环并上绳. 其他被唤醒的、方向相反的狒狒会发现条件不满足, 重新进入 &lt;code&gt;wait&lt;/code&gt; 状态. 这种 &lt;code&gt;while-wait-broadcast&lt;/code&gt; 的模式是实现公平调度的经典方法.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7 睡眠理发师问题&lt;/h2&gt;
&lt;h3&gt;7.1 问题描述&lt;/h3&gt;
&lt;p&gt;理发店有一位理发师、一把理发椅和N把供顾客等候的椅子.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有顾客, 理发师就在理发椅上睡觉.&lt;/li&gt;
&lt;li&gt;当一个顾客到来时, 他必须唤醒理发师.&lt;/li&gt;
&lt;li&gt;如果理发师正在理发, 新来的顾客会看是否有空椅子. 如果有, 就坐下等待；如果没有, 就离开.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.2 使用P、V操作的问题解答 (一位理发师)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 关系分析&lt;/strong&gt;
这是一个生产者-消费者模型. 顾客是&quot;生产者&quot; (生产理发请求) , 理发师是&quot;消费者&quot; (消费理发请求) , 椅子是缓冲区. 但是和生产者-消费者模型不同的是, 不能在顾客坐下 (生产理发请求) 之后立刻结束, 必须等理发师理发结束 (消费理发请求) 之后才能结束.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同步关系&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;理发师需要等待顾客的到来才能开始工作.&lt;/li&gt;
&lt;li&gt;顾客需要等待理发师空闲才能理发.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互斥关系&lt;/strong&gt;: 对等候椅数量 &lt;code&gt;waiting_chairs&lt;/code&gt; 的修改需要互斥.&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;&lt;code&gt;customers&lt;/code&gt;: 信号量, 表示等待理发的顾客数量. 理发师通过 &lt;code&gt;P(customers)&lt;/code&gt; 来检查是否有顾客, 如果没有就会睡眠. 初值为 0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;barbers&lt;/code&gt;: 信号量, 表示空闲的理发师数量. 顾客通过 &lt;code&gt;P(barbers)&lt;/code&gt; 来等待理发师空闲. 初值为 0 (或 1, 取决于初始状态理发师是否在睡觉). 我们设为0, 表示初始没有空闲理发师, 需要顾客唤醒.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt;: 互斥信号量, 用于保护对 &lt;code&gt;waiting_chairs&lt;/code&gt; 计数器的访问, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;waiting_chairs&lt;/code&gt;: 一个整型变量, 记录可用的等候椅数量, 初值为 N.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 常量和变量
#define N 5 // 假设有5把椅子
int waiting_chairs = N; // 可用的等候椅数量, 就是生产者-消费者模型中的 empty 信号量

// 信号量
semaphore customers = 0; // 等待的顾客数, 就是生产者-消费者模型中的 full 信号量
semaphore barbers = 0;   // 空闲的理发师数, 加入的新信号量, 因为必须理完发才能走, 所以必须找到一个空闲的理发师
semaphore mutex = 1;     // 保护 waiting_chairs, 就是生产者-消费者模型中的 mutex 信号量

// 顾客进程-生产者
Customer() {
    P(mutex);
    if (waiting_chairs &gt; 0) {
        waiting_chairs--; // 坐下一把椅子
        V(mutex);         // 释放锁
        V(customers);     // 通知理发师, 我来了

        // 如果是生产者-消费者模型, 这里就可以结束了, 因为进入就可以结束了, 但是理发坐下之后还得等

        P(barbers);       // 等待理发师准备好 (即理发师被叫醒给自己理发)
        
        // --- 正在理发 ---
        // get_haircut();
    } else {      // 没有空椅子了
        V(mutex); // 必须释放锁再离开
        
        // --- 顾客离开 ---
        // leave_shop();
    }
}

// 理发师进程-消费者
Barber() {
    while(true) {
        P(customers); // 等待顾客到来. 如果没有顾客, 则在椅子上睡眠. 

        // 下述三行其实就是生产者-消费者模型中的 V(empty) 操作, 因为顾客坐下之后, 等待用的椅子就多了一把, 所以 empty 信号量加一
        P(mutex);
        waiting_chairs++; // 一个顾客从等待区坐到理发椅上, 空出一把椅子
        V(mutex);

        // 如果是生产者-消费者模型, 这里就可以结束了, 因为使用完物品就可以结束了

        V(barbers); // 我这个理发师被叫醒了, 可以开始理发了

        // --- 理发进行中 ---
        // cut_hair();
        // --- 理发结束 ---
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能出现饥饿问题, 也就是第一个顾客叫醒的理发师, 可能被第二个顾客抢走.&lt;/p&gt;
&lt;h3&gt;7.3 使用管程的解决方案 (一位理发师)&lt;/h3&gt;
&lt;p&gt;管程将理发店的状态 (等待的顾客) 和操作 (理发、等待) 封装起来, 使逻辑更清晰.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;: &lt;code&gt;waiting_count&lt;/code&gt;, 记录在等候椅上的人数.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customer_is_waiting&lt;/code&gt;: 理发师在此等待顾客来.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;barber_is_ready&lt;/code&gt;: 顾客在此等待理发师睡醒.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetHaircut()&lt;/code&gt;: 由顾客调用.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NextCustomer()&lt;/code&gt;: 由理发师调用.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class BarberShop {
    int waiting_count = 0;
    final int N = 5; // 椅子数量
    condition customer_is_waiting, barber_is_ready;

    // 顾客调用的过程
    public function GetHaircut() returns bool {
        if (waiting_count == N) {
            return false; // 没有椅子, 理发失败
        }
        waiting_count++;
        // 唤醒理发师
        notify(customer_is_waiting);
        
        // 等待理发师为我服务, 如果是生产者-消费者模型, 这里就可以结束了
        wait(barber_is_ready);
        return true; // 理发成功
    }

    // 理发师调用的过程
    public void NextCustomer() {
        // 如果没有等待的顾客, 就睡觉
        while (waiting_count == 0) {
            wait(customer_is_waiting);
        }
        waiting_count--;
        
        // 叫醒一个顾客, 如果是生产者-消费者模型, 这里就可以结束了
        notify(barber_is_ready);
    }
}

// 进程
Customer() {
    if (BarberShop.GetHaircut()) {
        // 正在理发...
    } else {
        // 离开...
    }
}

Barber() {
    while(true) {
        BarberShop.NextCustomer();
        // cut_hair()...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;
顾客调用 &lt;code&gt;GetHaircut&lt;/code&gt;, 如果成功进入等待队列, 就 &lt;code&gt;notify&lt;/code&gt; 理发师并 &lt;code&gt;wait&lt;/code&gt; 在自己的条件上. 理发师调用 &lt;code&gt;NextCustomer&lt;/code&gt;,  &lt;code&gt;wait&lt;/code&gt; 等待顾客, 被唤醒后 &lt;code&gt;notify&lt;/code&gt; 一个顾客来理发. 这种客户和理发师之间的&quot;握手&quot;非常清晰.&lt;/p&gt;
&lt;h3&gt;7.4 问题解答 (M个理发师)&lt;/h3&gt;
&lt;p&gt;如果理发店有 M 个理发师, 如何修改？&lt;/p&gt;
&lt;p&gt;如果有多个消费者, 不需要修改代码, 因为本身生产者-消费者模型中, 消费者是多个的, 所以不需要修改.&lt;/p&gt;
&lt;h2&gt;8 第二类读者写者问题 (写者优先)&lt;/h2&gt;
&lt;h3&gt;8.1 问题描述&lt;/h3&gt;
&lt;p&gt;要求：
a. 多个读者可以同时进行读.
b. 写者必须互斥 (一次只有一个写者, 且读写不能同时) .
c. 写者优先于读者 (一旦有写者在等待, 后续到达的读者必须等待, 直到所有等待的写者都完成后才能读) .&lt;/p&gt;
&lt;h3&gt;8.2 使用P、V操作的问题解答&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 关系分析&lt;/strong&gt;
写者优先的核心在于：当一个写者希望写入时, 它应该尽快获得访问权, 甚至要插队到已经在等待的读者前面.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 信号量设置&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read_count&lt;/code&gt;, &lt;code&gt;write_count&lt;/code&gt;: 读者和写者的计数器, 初值为 0.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex_r&lt;/code&gt;, &lt;code&gt;mutex_w&lt;/code&gt;: 分别保护 &lt;code&gt;read_count&lt;/code&gt; 和 &lt;code&gt;write_count&lt;/code&gt; 的互斥锁, 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r_gate&lt;/code&gt;: 一个&quot;读者大门&quot;信号量. 当有写者在等待时, 此门关闭, 阻止新读者进入. 初值为 1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w_resource&lt;/code&gt;: 代表共享资源的&quot;写者锁&quot;, 也用于实现读者和写者的互斥. 初值为 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;int read_count = 0, write_count = 0;
semaphore mutex_r = 1, mutex_w = 1; // 保护计数器的锁
semaphore r_gate = 1;               // 读者进入的大门
semaphore w_resource = 1;           // 资源本身的锁, 也是写者锁

// 读者进程
Reader() {
    P(r_gate);        // 尝试通过读者大门
    P(mutex_r);
    read_count++;
    if (read_count == 1) { // 如果是第一个读者
        P(w_resource); // 锁住资源, 阻止写者
    }
    V(mutex_r);
    V(r_gate);        // 通过后, 立刻让其他读者也通过大门

    // --- 读操作 ---
    // reading...
    
    P(mutex_r);
    read_count--;
    if (read_count == 0) { // 如果是最后一个读者
        V(w_resource); // 释放资源, 允许写者进入
    }
    V(mutex_r);
}

// 写者进程
Writer() {
    P(mutex_w);
    write_count++;
    if (write_count == 1) { // 如果是第一个写者
        P(r_gate);        // 关上读者大门, 阻止新读者进入
    }
    V(mutex_w);

    P(w_resource);      // 获取资源的独占访问权 (写者锁) 
    
    // --- 写操作 ---
    // writing...

    V(w_resource);      // 释放资源

    P(mutex_w);
    write_count--;
    if (write_count == 0) { // 如果是最后一个写者
        V(r_gate);        // 打开读者大门, 允许读者进入
    }
    V(mutex_w);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写者如何优先&lt;/strong&gt;: 当第一个写者到达时 (&lt;code&gt;write_count&lt;/code&gt; 变为 1), 它会立即 &lt;code&gt;P(r_gate)&lt;/code&gt;. 这会导致所有后续到来的读者全部被阻塞在 &lt;code&gt;P(r_gate)&lt;/code&gt; 处. 它们甚至没有机会去修改 &lt;code&gt;read_count&lt;/code&gt; 或尝试获取 &lt;code&gt;w_resource&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写者执行&lt;/strong&gt;: 写者进程会继续执行 &lt;code&gt;P(w_resource)&lt;/code&gt;. 如果当前有读者正在读, 它会等待这些读者全部结束 (最后一个读者会 &lt;code&gt;V(w_resource)&lt;/code&gt;). 一旦 &lt;code&gt;w_resource&lt;/code&gt; 被释放, 等待的写者就能立刻获得它, 开始写入.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写者离开&lt;/strong&gt;: 当最后一个等待的写者完成工作后 (&lt;code&gt;write_count&lt;/code&gt; 变为 0), 它会 &lt;code&gt;V(r_gate)&lt;/code&gt;, 把读者大门打开, 此时在门外等候的读者们才能进入.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种机制确保了只要有写者在等待, 就不会有新的读者开始读取, 体现了写者优先.&lt;/p&gt;
&lt;h3&gt;8.3 使用管程的解决方案&lt;/h3&gt;
&lt;p&gt;写者优先的逻辑在管程中可以通过增加一个写者等待计数器来实现.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read_count&lt;/code&gt;: 正在读的读者数.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write_waiting_count&lt;/code&gt;: 正在等待的写者数.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writer_active&lt;/code&gt;: 是否有写者正在写.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;: &lt;code&gt;can_read&lt;/code&gt;, &lt;code&gt;can_write&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程&lt;/strong&gt;: &lt;code&gt;StartRead&lt;/code&gt;, &lt;code&gt;EndRead&lt;/code&gt;, &lt;code&gt;StartWrite&lt;/code&gt;, &lt;code&gt;EndWrite&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class WriterPriorityRW {
    int read_count = 0;
    int write_waiting_count = 0;
    bool writer_active = false;
    condition can_read, can_write;

    public void StartWrite() {
        write_waiting_count++;
        // 如果有读者在读, 或者有写者在写, 则等待
        while (read_count &gt; 0 || writer_active) {
            wait(can_write);
        }
        write_waiting_count--;
        writer_active = true;
    }

    public void EndWrite() {
        writer_active = false;
        // 优先唤醒等待的写者
        if (write_waiting_count &gt; 0) {
            notify(can_write);
        } else { // 否则唤醒所有等待的读者
            broadcast(can_read);
        }
    }

    public void StartRead() {
        // 如果有写者在写, 或者有写者在等待, 则等待
        while (writer_active || write_waiting_count &gt; 0) {
            wait(can_read);
        }
        read_count++;
    }

    public void EndRead() {
        read_count--;
        // 如果是最后一个读者, 则唤醒一个写者
        if (read_count == 0) {
            notify(can_write);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写者优先的实现&lt;/strong&gt;: 关键在于 &lt;code&gt;StartRead&lt;/code&gt; 和 &lt;code&gt;StartWrite&lt;/code&gt; 中的 &lt;code&gt;write_waiting_count&lt;/code&gt; 计数器.
&lt;ul&gt;
&lt;li&gt;一个写者到达时, 先将 &lt;code&gt;write_waiting_count&lt;/code&gt; 加一, 表明&quot;有写者驾到&quot;.&lt;/li&gt;
&lt;li&gt;一个读者准备读之前, 必须检查 &lt;code&gt;writer_active&lt;/code&gt; 和 &lt;code&gt;write_waiting_count&lt;/code&gt;. 只要 &lt;code&gt;write_waiting_count &gt; 0&lt;/code&gt;, 即使当前没有写者在写, 读者也必须等待. 这就赋予了写者优先权.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;唤醒逻辑&lt;/strong&gt;: 在 &lt;code&gt;EndWrite&lt;/code&gt; 中, 优先检查并唤醒其他等待的写者. 只有在没有写者等待时, 才会唤醒读者. 在 &lt;code&gt;EndRead&lt;/code&gt; 中, 最后一个读者离开时, 需要检查是否有写者在等待, 并负责唤醒它们.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9 利用信号量管理共享资源&lt;/h2&gt;
&lt;p&gt;这是一个复杂的问题, 通过分析一个有问题的代码, 逐步引出两种经典的并发编程模式.&lt;/p&gt;
&lt;h3&gt;9.1 问题描述&lt;/h3&gt;
&lt;p&gt;一个共享资源有如下特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当使用者少于 3 个时, 新进程可以立刻获得资源.&lt;/li&gt;
&lt;li&gt;当 3 个资源都被占用后, 新进程必须等待. 直到&lt;strong&gt;当前使用资源的 3 个进程都释放完资源&lt;/strong&gt;后, 其他等待的进程才能获得资源 (一批一批地进入) .&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;9.2 初始的错误程序分析&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;程序代码 (简化版)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 申请资源
P(mutex);
if (must_wait) { // must_wait is (active == 3)
    waiting++;
    V(mutex);
    P(block);
    P(mutex); // 再次获取锁, 修改waiting
    waiting--;
}
active++;
must_wait = (active == 3);
V(mutex);

// 释放资源
P(mutex);
active--;
if (active == 0) {
    int n = min(waiting, 3);
    for (i=0; i&amp;#x3C;n; i++) {
        V(block);
    }
    must_wait = false;
}
V(mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;a. 解释其出错的位置&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;: 错误发生在被 &lt;code&gt;P(block)&lt;/code&gt; 唤醒之后.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;假设进程$P_4$, $P_5$, $P_6$... 都在 &lt;code&gt;P(block)&lt;/code&gt; 处等待.&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;active&lt;/code&gt; 变为 0 时, 最后一个离开的进程 (比如$P_3$) 会执行 &lt;code&gt;V(block)&lt;/code&gt; 三次, 唤醒 $P_4$, $P_5$, $P_6$.&lt;/li&gt;
&lt;li&gt;这三个进程被唤醒后, 会&lt;strong&gt;依次&lt;/strong&gt;尝试获取 &lt;code&gt;mutex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;假设 $P_4$ 第一个获取 &lt;code&gt;mutex&lt;/code&gt;. 它执行 &lt;code&gt;waiting--&lt;/code&gt;, 然后 &lt;code&gt;active++&lt;/code&gt; (此时 &lt;code&gt;active&lt;/code&gt;=1), &lt;code&gt;must_wait&lt;/code&gt; 仍然是 &lt;code&gt;false&lt;/code&gt;, 然后释放 &lt;code&gt;mutex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;现在轮到 $P_5$. 它获取 &lt;code&gt;mutex&lt;/code&gt;, 执行 &lt;code&gt;waiting--&lt;/code&gt;, &lt;code&gt;active++&lt;/code&gt; (此时 &lt;code&gt;active&lt;/code&gt;=2), &lt;code&gt;must_wait&lt;/code&gt; 仍然是 &lt;code&gt;false&lt;/code&gt;, 释放 &lt;code&gt;mutex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题来了&lt;/strong&gt;: 此时, 如果一个&lt;strong&gt;新进程&lt;/strong&gt; $P_{10}$ 到达, 它执行申请资源的代码. 它会发现 &lt;code&gt;must_wait&lt;/code&gt; 是 &lt;code&gt;false&lt;/code&gt;！于是它不会去 &lt;code&gt;P(block)&lt;/code&gt; 等待, 而是直接 &lt;code&gt;active++&lt;/code&gt; (此时 &lt;code&gt;active&lt;/code&gt;=3), 并将 &lt;code&gt;must_wait&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;. $P_{10}$&quot;插队&quot;成功了.&lt;/li&gt;
&lt;li&gt;之后, 之前被唤醒的 $P_6$ 才能获取 &lt;code&gt;mutex&lt;/code&gt;, 它执行时 &lt;code&gt;active&lt;/code&gt; 已经变成3, &lt;code&gt;must_wait&lt;/code&gt; 已经为 &lt;code&gt;true&lt;/code&gt;, 但它还是会执行 &lt;code&gt;active++&lt;/code&gt;, 导致 &lt;code&gt;active&lt;/code&gt; 变成4, 这违反了最多3个进程使用的规则.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;根本原因&lt;/strong&gt;: 进程被唤醒后, 它所处的&quot;环境&quot; (即 &lt;code&gt;must_wait&lt;/code&gt; 的状态) 可能已经被其他进程改变了.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;b. &lt;code&gt;if&lt;/code&gt; 换成 &lt;code&gt;while&lt;/code&gt; 是否解决问题？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;: 将 &lt;code&gt;if(must_wait)&lt;/code&gt; 换成 &lt;code&gt;while(must_wait)&lt;/code&gt; &lt;strong&gt;可以解决&quot;插队&quot;和 &lt;code&gt;active&lt;/code&gt; 超出3的问题&lt;/strong&gt;, 但会引入&lt;strong&gt;新的问题&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;如何解决&lt;/strong&gt;: 如果P10插队并设置了 &lt;code&gt;must_wait = true&lt;/code&gt;, 那么当P6被唤醒并再次检查 &lt;code&gt;while(must_wait)&lt;/code&gt; 条件时, 它会发现条件成立, 于是重新执行 &lt;code&gt;V(mutex)&lt;/code&gt; 和 &lt;code&gt;P(block)&lt;/code&gt;, 再次进入等待状态. 这避免了 &lt;code&gt;active &gt; 3&lt;/code&gt; 的情况. 这种&quot;&lt;strong&gt;循环检查条件&lt;/strong&gt;&quot;是并发编程的一个重要原则.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;有什么难点仍然存在？&lt;/strong&gt;: &lt;strong&gt;死锁&lt;/strong&gt;.
在上面的场景中, $P_6$被唤醒后, 发现条件不满足, 又回去睡觉了. 但是谁来唤醒它呢？最后一个离开者 ($P_3$) 已经完成了它的唤醒任务 (&lt;code&gt;V(block)&lt;/code&gt;了三次) . 现在 &lt;code&gt;active&lt;/code&gt; 是3 ($P_4$, $P_5$, $P_{10}$), 在它们全部离开之前, 不会有新的 &lt;code&gt;V(block)&lt;/code&gt; 操作. 而 $P_6$ 却在 &lt;code&gt;P(block)&lt;/code&gt; 上永久地等待下去. 这就造成了&lt;strong&gt;死锁&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.3 正确解法一: &quot;I&apos;ll Do It for You&quot; 模式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;程序代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// 申请资源 (Acquire)
P(mutex);
if (must_wait) {
    waiting++;
    V(mutex);
    P(block); // 等待被别人处理
} else {
    active++;
    must_wait = (active == 3);
    V(mutex);
}

// 释放资源 (Release)
P(mutex);
active--;
if (active == 0) {
    int n = min(waiting, 3);
    waiting -= n;
    active = n;
    for (i=0; i&amp;#x3C;n; i++) {
        V(block);
    }
    must_wait = (active == 3);
}
V(mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;a. 解释工作方式和正确性&lt;/strong&gt;
这种模式被称为 &quot;I&apos;ll Do It for You&quot; (我来帮你做).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;: 申请资源的进程如果需要等待, 它什么都不做, 只是排队 (&lt;code&gt;P(block)&lt;/code&gt;). 唤醒它的进程 (即释放资源的进程) 会&lt;strong&gt;替它完成&lt;/strong&gt;所有状态变量的修改.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;申请&lt;/strong&gt;: 进程检查 &lt;code&gt;must_wait&lt;/code&gt;. 如果为 &lt;code&gt;true&lt;/code&gt;, 则 &lt;code&gt;waiting++&lt;/code&gt; 然后就去 &lt;code&gt;P(block)&lt;/code&gt; 睡觉. 它&lt;strong&gt;不自己&lt;/strong&gt;去 &lt;code&gt;active++&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放&lt;/strong&gt;: 当最后一个使用者 (&lt;code&gt;active&lt;/code&gt;变为0) 释放资源时, 它扮演了&quot;组织者&quot;的角色. 它查看有多少进程在等待 (&lt;code&gt;waiting&lt;/code&gt;), 决定唤醒 &lt;code&gt;n&lt;/code&gt; 个 (最多3个) . &lt;strong&gt;关键在于&lt;/strong&gt;：它在唤醒它们之前, 就&lt;strong&gt;预先&lt;/strong&gt;把 &lt;code&gt;active&lt;/code&gt; 设置为 &lt;code&gt;n&lt;/code&gt;, 把 &lt;code&gt;waiting&lt;/code&gt; 减去 &lt;code&gt;n&lt;/code&gt;, 并更新好 &lt;code&gt;must_wait&lt;/code&gt; 状态.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;唤醒后&lt;/strong&gt;: 被唤醒的进程从 &lt;code&gt;P(block)&lt;/code&gt; 返回后, &lt;strong&gt;什么也不用做&lt;/strong&gt;, 它已经被&quot;记作&quot;是 &lt;code&gt;active&lt;/code&gt; 状态了, 可以直接去使用资源.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正确性&lt;/strong&gt;: 这种方法是正确的, 因为所有状态变量的修改都在一个单一的临界区内 (释放者的临界区) 完成. 这避免了多个被唤醒进程之间以及新来进程之间的竞争和状态不一致问题.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;b. 给出新进程插队的例子&lt;/strong&gt;
这个程序&lt;strong&gt;不能完全避免&lt;/strong&gt;新进程插队.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;假设 &lt;code&gt;active=2&lt;/code&gt;, &lt;code&gt;must_wait=false&lt;/code&gt;, &lt;code&gt;waiting=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;进程 $P_{new}$ 到达, 执行申请代码. 它通过了 &lt;code&gt;if(must_wait)&lt;/code&gt; 检查, 即将执行 &lt;code&gt;active++&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;在 $P_{new}$ 执行 &lt;code&gt;active++&lt;/code&gt; 之前, 发生上下文切换. 一个正在使用资源的进程 $P_{old}$ 执行释放代码. 它将 &lt;code&gt;active&lt;/code&gt; 减为 1.&lt;/li&gt;
&lt;li&gt;此时, 另一个等待了很久的进程 $P_{waiter}$ 正在 &lt;code&gt;P(block)&lt;/code&gt; 等待 (假设之前有等待队列) .&lt;/li&gt;
&lt;li&gt;现在切换回 $P_{new}$. 它继续执行, &lt;code&gt;active&lt;/code&gt; 变成 2. 它没有插在 $P_{waiter}$ 前面, 因为 $P_{waiter}$ 的唤醒条件 (&lt;code&gt;active==0&lt;/code&gt;) 还没满足.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重新审视问题&lt;/strong&gt;: &quot;这个程序不能完全避免新到达的进程插到已有等待进程前得到资源&quot;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;场景&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;active=2&lt;/code&gt;, &lt;code&gt;waiting=5&lt;/code&gt;, &lt;code&gt;must_wait=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;$P_1$, $P_2$ 正在使用资源. $P_3$-$P_7$ 在 &lt;code&gt;P(block)&lt;/code&gt; 等待.&lt;/li&gt;
&lt;li&gt;$P_1$ 释放资源, &lt;code&gt;active&lt;/code&gt; 变为 1. &lt;code&gt;must_wait&lt;/code&gt; 仍然是 &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;此时, 一个新进程 $P_8$ 到达. 它执行申请代码, 看到 &lt;code&gt;must_wait&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;, 于是 &lt;code&gt;waiting++&lt;/code&gt; (变为6), 然后去 &lt;code&gt;P(block)&lt;/code&gt; 等待.&lt;/li&gt;
&lt;li&gt;$P_2$ 释放资源, &lt;code&gt;active&lt;/code&gt; 变为 0. 它看到 &lt;code&gt;waiting=6&lt;/code&gt;, 于是唤醒3个进程 ($P_3$,$P_4$,$P_5$). 它将 &lt;code&gt;active&lt;/code&gt; 设为 3, &lt;code&gt;waiting&lt;/code&gt; 设为 3, &lt;code&gt;must_wait&lt;/code&gt; 设&lt;/li&gt;
&lt;li&gt;现在 $P_3$, $P_4$, $P_5$ 在使用资源. $P_6$, $P_7$, $P_8$ 在等待.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;此时, 如果一个新进程 $P_9$ 到达, 它会发现 &lt;code&gt;must_wait&lt;/code&gt; 是 &lt;code&gt;false&lt;/code&gt; 吗？&lt;/strong&gt; 不会, 因为 &lt;code&gt;must_wait&lt;/code&gt; 已经被设为 &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;真正的插队机会&lt;/strong&gt;:
插队发生于&lt;strong&gt;信号量本身&lt;/strong&gt;的等待队列和我们自己维护的 &lt;code&gt;waiting&lt;/code&gt; 计数器之间.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;active=2&lt;/code&gt;, &lt;code&gt;must_wait=false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;进程 $P_{waiter}$ 到达, 执行 &lt;code&gt;P(mutex)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;切换到新进程 $P_{new}$, 它也执行 &lt;code&gt;P(mutex)&lt;/code&gt;, 于是被阻塞在 &lt;code&gt;mutex&lt;/code&gt; 的等待队列里.&lt;/li&gt;
&lt;li&gt;切换回 $P_{waiter}$. 它执行 &lt;code&gt;active++&lt;/code&gt; 使 &lt;code&gt;active=3&lt;/code&gt;, 设置 &lt;code&gt;must_wait=true&lt;/code&gt;, 然后 &lt;code&gt;V(mutex)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V(mutex)&lt;/code&gt; 唤醒了 $P_{new}$. $P_{new}$ 看到 &lt;code&gt;must_wait=true&lt;/code&gt;, 于是去 &lt;code&gt;P(block)&lt;/code&gt; 等待.
在这个场景里, 先到达的 $P_{waiter}$ 获得了资源.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个模式下的插队问题非常微妙, 通常与&quot;Thundering Herd Problem&quot;有关, 即多个进程被唤醒去竞争同一个锁. 这里的模型简化了这个问题. 它的主要优点是逻辑清晰, 由一个进程负责管理状态.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;c. 解释 &quot;I&apos;ll Do It for You&quot; 模式&lt;/strong&gt;
该模式的核心是, 当一个进程需要等待某个条件满足时, 它不主动在唤醒后重新检查条件或修改状态. 它仅仅是作为一个被动的等待者. 而负责满足这个条件的进程 (通常是释放资源的进程) , 会主动地、代替那些等待者完成所有的状态转换. 就像一个秘书, 把所有事情都安排好了, 然后通知老板们：&quot;你们现在可以开始工作了&quot;, 老板们 (被唤醒的进程) 无需再关心任何准备工作.&lt;/p&gt;
&lt;h3&gt;9.4 正确解法二: &quot;Pass the Baton&quot; 模式&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;程序代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;semaphore mutex = 1, block = 0;
int active = 0, waiting = 0;
bool must_wait = false;

void Acquire() {
    // 申请资源 (Acquire)
    P(mutex);
    if (must_wait) {
        waiting++;
        V(mutex);
        P(block); // 等待接力棒, 第四个进程开始需要等候, 因为 block 初始为 0
        waiting--;
        // 唤醒后, 我获得了mutex的所有权 (接力棒) 
    }
    active++;
    must_wait = (active == 3);
    if (waiting &gt; 0 &amp;#x26;&amp;#x26; !must_wait) {
        V(block); // 传递接力棒
    } else {
        V(mutex); // 释放锁
    }
}

void Release() {
    // 释放资源 (Release)
    P(mutex);
    active--;
    if (active == 0) {
        must_wait = false;
    }
    if (waiting &gt; 0 &amp;#x26;&amp;#x26; !must_wait) {
        V(block); // 开始传递接力棒
    } else {
        V(mutex); // 释放锁
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;a. 解释工作方式和正确性&lt;/strong&gt;
该模式被称为 &quot;Pass the Baton&quot; (传递接力棒).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;: 互斥锁 &lt;code&gt;mutex&lt;/code&gt; 在被唤醒的进程间直接传递, 就像接力赛中的&quot;接力棒&quot;. 一个进程完成自己的临界区后, 不直接释放锁给公众, 而是直接将&quot;锁的所有权&quot;传递给下一个需要进入的、正在等待的进程.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;申请&lt;/strong&gt;: 如果需要等待 (&lt;code&gt;must_wait&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;), 进程释放 &lt;code&gt;mutex&lt;/code&gt; 并 &lt;code&gt;P(block)&lt;/code&gt; 等待.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放/传递&lt;/strong&gt;: 当一个进程 (无论是释放者还是刚进入的申请者) 完成操作后, 它会检查是否还有进程在等待 (&lt;code&gt;waiting &gt; 0&lt;/code&gt;) 并且进入条件满足 (&lt;code&gt;!must_wait&lt;/code&gt;). 如果满足, 它不执行 &lt;code&gt;V(mutex)&lt;/code&gt;, 而是执行 &lt;code&gt;V(block)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接力棒传递&lt;/strong&gt;: &lt;code&gt;V(block)&lt;/code&gt; 唤醒一个在 &lt;code&gt;P(block)&lt;/code&gt; 处等待的进程. 这个被唤醒的进程在返回时, &lt;strong&gt;它就拥有了互斥锁 &lt;code&gt;mutex&lt;/code&gt; 的所有权&lt;/strong&gt;, 因为它没有通过 &lt;code&gt;P(mutex)&lt;/code&gt; 来获得, 而是由上一个进程&quot;递给&quot;它的. 然后它继续执行临界区代码 (&lt;code&gt;active++&lt;/code&gt; 等) .&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后一人&lt;/strong&gt;: 当最后一个需要被唤醒的进程完成操作后, 它检查条件, 发现不再需要唤醒别人了, 于是它执行 &lt;code&gt;V(mutex)&lt;/code&gt;, 将锁&quot;放回&quot;架子上, 供所有新来的进程竞争.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正确性&lt;/strong&gt;: 这种方式保证了被唤醒的进程可以立即执行, 无需再次竞争 &lt;code&gt;mutex&lt;/code&gt;, 从而避免了&quot;唤醒后发现条件又变了&quot;的问题. 它形成了一个&quot;唤醒链&quot;, 效率很高.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;b. 与上一解法在唤醒进程个数上的不同&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;I&apos;ll Do It for You&quot;&lt;/strong&gt;: 是一次性唤醒一批进程. 释放者在一个循环里执行 &lt;code&gt;V(block)&lt;/code&gt; &lt;code&gt;n&lt;/code&gt; 次, &lt;strong&gt;同时&lt;/strong&gt;唤醒 &lt;code&gt;n&lt;/code&gt; 个进程. 这 &lt;code&gt;n&lt;/code&gt; 个进程随后会并发执行.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Pass the Baton&quot;&lt;/strong&gt;: 是一次只唤醒一个进程. 一个进程 &lt;code&gt;V(block)&lt;/code&gt; 唤醒下一个, 下一个再唤醒下下一个, 形成一个&lt;strong&gt;串行&lt;/strong&gt;的唤醒链. 每次只有一个进程在临界区内活动.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;c. 解释 &quot;Pass the Baton&quot; 模式&lt;/strong&gt;
这个模式的精髓在于&lt;strong&gt;将锁的所有权和唤醒信号绑定&lt;/strong&gt;. 执行 &lt;code&gt;V(block)&lt;/code&gt; 的进程不仅是在唤醒一个等待者, 更是在对它说：&quot;我把临界区的钥匙 (mutex) 现在直接交给你了, 你继续&quot;. 这避免了被唤醒的进程再去和外面那些新来的进程一起竞争锁, 保证了等待队列的公平性和执行的有序性, 是一种非常高效和优雅的并发控制模式.&lt;/p&gt;
&lt;h3&gt;9.5 使用管程的解法&lt;/h3&gt;
&lt;p&gt;下述算法的 &lt;code&gt;active_count&lt;/code&gt; 没有被保护, 所以可能出现竞争问题, 但是我还没想出更好的算法, 如果有人有更好的算法, 欢迎在评论区提出!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 管程设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;共享变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;active_count&lt;/code&gt;: 当前正在使用资源的进程数.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;must_wait&lt;/code&gt;: 一个布尔标志, 当资源满员后设置为 &lt;code&gt;true&lt;/code&gt;, 阻止新进程进入, 直到下一批开始.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;next_batch_q&lt;/code&gt;: 等待下一批的进程在此睡眠.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管程过程&lt;/strong&gt;: &lt;code&gt;Acquire()&lt;/code&gt; 和 &lt;code&gt;Release()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 伪代码实现&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;class ResourceManager {
    int active_count = 0;
    bool must_wait = false;
    condition next_batch_q;

    // 申请资源
    public void Acquire() {
        // 如果上一批已满员, 则必须等待
        while (must_wait) {
            wait(next_batch_q);
        }

        active_count++;
        // 如果我是本批的第3个, 则关上大门
        if (active_count == 3) {
            must_wait = true;
        }
    }

    // 释放资源
    public void Release() {
        active_count--;
        // 如果我是本批最后一个释放资源的
        if (active_count == 0) {
            // 打开大门, 并通知所有等待者
            must_wait = false;
            broadcast(next_batch_q);
        }
    }
}

// 进程代码
Process_i() {
    while(true) {
        ResourceManager.Acquire();
        // --- 使用资源 ---
        ResourceManager.Release();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 说明&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批次控制&lt;/strong&gt;: &lt;code&gt;must_wait&lt;/code&gt; 标志是实现&quot;批处理&quot;的关键. 当第三个进程获得资源时, 它将 &lt;code&gt;must_wait&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;, 这就像关上了一扇大门, 所有后续到来的进程都会在 &lt;code&gt;wait(next_batch_q)&lt;/code&gt; 处被阻塞.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批次切换&lt;/strong&gt;: 只有当 &lt;code&gt;active_count&lt;/code&gt; 降为0时 (即上一批的所有3个进程都已释放资源), 最后一个离开的进程才会将 &lt;code&gt;must_wait&lt;/code&gt; 设回 &lt;code&gt;false&lt;/code&gt; 并 &lt;code&gt;broadcast&lt;/code&gt; 唤醒所有等待的进程.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;健壮性&lt;/strong&gt;: 被 &lt;code&gt;broadcast&lt;/code&gt; 唤醒的进程会重新检查 &lt;code&gt;while (must_wait)&lt;/code&gt;. 虽然所有等待者都被唤醒, 但只有前3个能够成功通过循环并获得资源. 第4个及以后的进程会发现 &lt;code&gt;active_count&lt;/code&gt; 又变成了3, &lt;code&gt;must_wait&lt;/code&gt; 再次变为 &lt;code&gt;true&lt;/code&gt;, 于是它们会继续 &lt;code&gt;wait&lt;/code&gt;, 等待再下一批. 这种 &lt;code&gt;while&lt;/code&gt; 循环检查机制是管程编程的黄金法则, 确保了逻辑的正确性.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 09: Concurrency Mechanism</title><link>https://www.lyt0112.com/blog/operating_systems_note_09-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_09-zh</guid><description>Operating Systems Notes 09: 并发机制</description><pubDate>Fri, 06 Jun 2025 15:38:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-06-05&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 并发的特征与挑战&lt;/h2&gt;
&lt;p&gt;并发是操作系统设计的基石。当多个执行单元 (进程/线程) 同时存在于一个系统中时，它们的执行具有以下特征，并带来相应的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;间断性&lt;/strong&gt;: 执行过程不是连续的，会被操作系统调度中断，切换到其他执行单元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享&lt;/strong&gt;: 多个执行单元可能会访问和操作共同的资源或数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可预测性&lt;/strong&gt;: 各个执行单元的相对执行速度无法预知，取决于调度策略、系统负载等多种因素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不确定性&lt;/strong&gt;: 由于执行速度的不可预测性，程序的执行结果可能是不确定的，这会导致与时间有关的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.1. 案例分析：与时间有关的错误&lt;/h3&gt;
&lt;h4&gt;1.1.1. 银行账户取款问题&lt;/h4&gt;
&lt;p&gt;假设一个银行账户有5000元，两个ATM机 (T1和T2) 同时对该账户进行取款操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T1: 取款1000元&lt;/li&gt;
&lt;li&gt;T2: 取款2000元&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个可能的错误执行序列如下：&lt;/p&gt;
&lt;p&gt;| 步骤 | ATM T1 操作     | ATM T2 操作     | 账户余额 (x) | 说明                        |
| :--- | :-------------- | :-------------- | :----------- | :-------------------------- |
| 1    | &lt;code&gt;read(x)&lt;/code&gt;       |                 | 5000         | T1 读取余额 5000            |
| 2    |                 | &lt;code&gt;read(x)&lt;/code&gt;       | 5000         | T2 也读取余额 5000          |
| 3    | &lt;code&gt;x := x - 1000&lt;/code&gt; |                 | 4000         | T1 计算新余额               |
| 4    | &lt;code&gt;write(x)&lt;/code&gt;      |                 | 4000         | T1 将 4000 写回             |
| 5    |                 | &lt;code&gt;x := x - 2000&lt;/code&gt; | 3000         | T2 基于它&lt;strong&gt;旧的&lt;/strong&gt;读取值计算 |
| 6    |                 | &lt;code&gt;write(x)&lt;/code&gt;      | 3000         | T2 将 3000 写回             |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终结果&lt;/strong&gt;: 账户余额为3000元，但正确结果应为 5000 - 1000 - 2000 = 2000元。银行损失了1000元。这个问题的根源在于T1和T2的操作发生了&lt;strong&gt;交叉&lt;/strong&gt;，导致T2的操作覆盖了T1的操作结果。&lt;/p&gt;
&lt;h4&gt;1.1.2. 共享计数器问题 (C代码及汇编)&lt;/h4&gt;
&lt;p&gt;一个常见的并发问题是多个线程同时对一个共享变量进行自增操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C 代码:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;#x3C; niters; i++)
    cnt++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对应的汇编代码 (以一个线程为例):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;L3:
    movq cnt(%rip), %rdx   ; (Li) Load: 将 cnt 的值加载到寄存器 %rdx
    addq $1, %rdx          ; (Ui) Update: 将寄存器 %rdx 的值加 1
    movq %rdx, cnt(%rip)   ; (Si) Store: 将寄存器 %rdx 的值写回内存中的 cnt
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;L&lt;/code&gt;, &lt;code&gt;U&lt;/code&gt;, &lt;code&gt;S&lt;/code&gt; 这三个步骤合起来才能完成一次 &lt;code&gt;cnt++&lt;/code&gt;。在并发环境下，这三个步骤并非原子操作 (不可分割) ，可能被中断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;错误的执行交错:&lt;/strong&gt;
假设 &lt;code&gt;cnt&lt;/code&gt; 初始值为 0，两个线程都想执行 &lt;code&gt;cnt++&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| 线程 | 指令          | %rdx1 | %rdx2 | cnt  | 说明                |
| :--- | :------------ | :---- | :---- | :--- | :------------------ |
| 1    | &lt;code&gt;L1&lt;/code&gt; (Load)   | 0     | -     | 0    | 线程1加载 cnt=0     |
| 2    | &lt;code&gt;L2&lt;/code&gt; (Load)   | 0     | 0     | 0    | 线程2也加载 cnt=0   |
| 1    | &lt;code&gt;U1&lt;/code&gt; (Update) | 1     | 0     | 0    | 线程1计算结果为 1   |
| 2    | &lt;code&gt;U2&lt;/code&gt; (Update) | 1     | 1     | 0    | 线程2计算结果也为 1 |
| 1    | &lt;code&gt;S1&lt;/code&gt; (Store)  | 1     | 1     | 1    | 线程1将 1 写回      |
| 2    | &lt;code&gt;S2&lt;/code&gt; (Store)  | 1     | 1     | 1    | 线程2将 1 写回      |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最终结果&lt;/strong&gt;: &lt;code&gt;cnt&lt;/code&gt; 的值为1，而不是预期的2。这就是&lt;strong&gt;竞争条件 (Race Condition)&lt;/strong&gt; 的典型例子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;竞争条件 (Race Condition) 定义&lt;/strong&gt;: 两个或多个进程/线程读写某些共享数据，而最终的结果取决于它们运⾏的精确时序。&lt;/p&gt;
&lt;h4&gt;1.1.3. 线程参数传递问题&lt;/h4&gt;
&lt;p&gt;在循环中创建线程并传递循环变量的地址是一个非常经典的错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;错误代码示例:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Main Thread
void *thread(void *vargp);

int main() {
    pthread_t tid;
    int i;
    for (i = 0; i &amp;#x3C; 100; i++) {
        // 传递 i 的地址
        pthread_create(&amp;#x26;tid, NULL, thread, &amp;#x26;i);
    }
    ...
}

// Peer Thread
void *thread(void *vargp) {
    // 解引用指针获取值
    int i = *((int *)vargp);
    ...
    // 保存 i 的值用于后续分析
    save_value(i);
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题分析&lt;/strong&gt;:
所有新创建的线程都共享同一个变量 &lt;code&gt;i&lt;/code&gt; 的内存地址。主线程的&lt;code&gt;for&lt;/code&gt;循环可能很快就执行完毕，&lt;code&gt;i&lt;/code&gt; 的值变成了100。而新创建的线程在这之后才开始执行，当它们去解引用&lt;code&gt;vargp&lt;/code&gt;时，访问到的都是主线程栈上那个已经变成100 (或某个中间值) 的&lt;code&gt;i&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验结果&lt;/strong&gt;: 实验表明，在多核服务器上，很多线程获取到的 &lt;code&gt;i&lt;/code&gt; 值是重复的，甚至是混乱的，很少有线程能拿到从0到99的唯一值。这证明了竞争条件确实发生了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正确做法&lt;/strong&gt;:
应该为每个线程传递一个独一无二的内存地址。通常的做法是在主线程中为每个线程动态分配内存 (&lt;code&gt;malloc&lt;/code&gt;) ，或者传递值本身 (如果值的大小不超过&lt;code&gt;void*&lt;/code&gt;) 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// 修正后的主线程循环
for (i = 0; i &amp;#x3C; 100; i++) {
    int *arg = malloc(sizeof(int));
    *arg = i;
    pthread_create(&amp;#x26;tid, NULL, thread, arg);
}

// 修正后的子线程
void *thread(void *vargp) {
    int i = *((int *)vargp);
    free(vargp); // 释放内存
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 进程/线程间的制约关系&lt;/h2&gt;
&lt;p&gt;在并发环境中，进程/线程之间主要存在两种制约关系。&lt;/p&gt;
&lt;h3&gt;2.1. 互斥 (Mutual Exclusion)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因&lt;/strong&gt;: 多个进程/线程需要排他性地使用某个共享资源 (如打印机、共享变量) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;: 当一个进程在使用共享资源时，其他试图使用该资源的进程必须等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临界资源 (Critical Resource)&lt;/strong&gt;: 一次只允许一个进程/线程访问的资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临界区 (Critical Section)&lt;/strong&gt;: 程序中访问临界资源的代码片段。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;临界区使用原则&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;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;: 任何进程请求进入临界区，应在有限时间内得到满足，防止&quot;饿死&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让权等待&lt;/strong&gt;: 进程在等待进入临界区时，应放弃CPU，进入阻塞状态，避免&quot;忙等待&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2. 同步 (Synchronization)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原因&lt;/strong&gt;: 多个进程/线程为了合作完成一个共同任务，需要按照一定的时序关系来执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;: 一个进程的执行需要依赖另一个进程的消息或信号。在收到信号前，该进程会阻塞等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子&lt;/strong&gt;: 生产者-消费者问题。消费者必须等待生产者生产了产品后才能消费。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;用前驱图表示同步关系&lt;/strong&gt;:
前驱图是一个有向无环图，用于描述进程间的执行顺序。箭头 &lt;code&gt;p1 -&gt; p2&lt;/code&gt; 表示 &lt;code&gt;p1&lt;/code&gt; 必须在 &lt;code&gt;p2&lt;/code&gt; 之前完成。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前驱图 (Precedence Graph)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    S --&gt; p1
    p1 --&gt; p2
    p1 --&gt; p5
    p2 --&gt; p3
    p3 --&gt; p4
    p4 --&gt; p5
    p5 --&gt; p6
    p3 --&gt; p7
    p7 --&gt; p6
    p6 --&gt; p8
    p8 --&gt; F
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. 解决互斥问题的方案&lt;/h2&gt;
&lt;h3&gt;3.1. 软件方案&lt;/h3&gt;
&lt;p&gt;软件方案通过巧妙的算法逻辑来确保互斥，但通常较为复杂且效率不高。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解法1 (单一标志法)&lt;/strong&gt;: 使用一个&lt;code&gt;bool free&lt;/code&gt;变量。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;: 在&lt;code&gt;while(free);&lt;/code&gt;和&lt;code&gt;free=true;&lt;/code&gt;之间可能发生上下文切换，两个进程都可能通过&lt;code&gt;while&lt;/code&gt;检查，然后都进入临界区。违反&quot;无空等待&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法2 (轮转法)&lt;/strong&gt;: 使用一个&lt;code&gt;turn&lt;/code&gt;变量，&lt;code&gt;turn=true&lt;/code&gt;允许P进，&lt;code&gt;turn=false&lt;/code&gt;允许Q进。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;: 必须严格轮流进入临界区。如果P不想进入，但&lt;code&gt;turn&lt;/code&gt;标志归它，Q也无法进入。违反&quot;有空让进&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解法3 (双标志法)&lt;/strong&gt;: 每个进程有自己的&lt;code&gt;pturn&lt;/code&gt;, &lt;code&gt;qturn&lt;/code&gt;标志，表示自己想进入。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;: 两个进程可能同时设置自己的标志为&lt;code&gt;true&lt;/code&gt;，然后都在&lt;code&gt;while&lt;/code&gt;循环中等待对方，导致&lt;strong&gt;死锁&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dekker算法 (1965)&lt;/strong&gt;: 结合解法2和3，引入&lt;code&gt;turn&lt;/code&gt;变量来解决死锁。是第一个被证明正确的纯软件解法，但实现复杂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peterson算法 (1981)&lt;/strong&gt;: 一个更简洁优美的软件解法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Peterson 算法详解&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define N 2 // 进程数量
int turn; // 轮到谁
int interested[N]; // 兴趣数组，初始为FALSE

void enter_region(int process) {
    int other = 1 - process; // 另一个进程

    interested[process] = TRUE; // 1. 表示自己感兴趣
    turn = process;             // 2. 将优先权让给后一个运行这行代码的进程 (即可能是对方，如果是自己，那自己就进入临界区) 

    // 3. 当对方感兴趣且优先权在对方时，循环等待
    while (turn == process &amp;#x26;&amp;#x26; interested[other] == TRUE) {
        // busy waiting
    }
}

void leave_region(int process) {
    interested[process] = FALSE; // 离开临界区，不再感兴趣
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;互斥&lt;/strong&gt;: 如果两个进程同时调用&lt;code&gt;enter_region&lt;/code&gt;，它们都会设置自己的&lt;code&gt;interested&lt;/code&gt;为&lt;code&gt;TRUE&lt;/code&gt;。但&lt;code&gt;turn&lt;/code&gt;变量的写入是原子性的，后写入的会覆盖先写入的。假设进程1后写，则&lt;code&gt;turn=1&lt;/code&gt;。此时进程0在&lt;code&gt;while&lt;/code&gt;中会发现&lt;code&gt;turn==0 &amp;#x26;&amp;#x26; interested[1]==TRUE&lt;/code&gt;不成立，跳出循环进入临界区。而进程1会发现&lt;code&gt;turn==1 &amp;#x26;&amp;#x26; interested[0]==TRUE&lt;/code&gt;成立，陷入等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有空让进&lt;/strong&gt;: 如果进程1不感兴趣 (&lt;code&gt;interested[1]==FALSE&lt;/code&gt;)，进程0的&lt;code&gt;while&lt;/code&gt;条件不成立，直接进入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有限等待&lt;/strong&gt;: 不会死锁。即使一个进程在&lt;code&gt;while&lt;/code&gt;中等待，一旦另一个进程离开临界区 (设置&lt;code&gt;interested&lt;/code&gt;为&lt;code&gt;FALSE&lt;/code&gt;) ，等待的进程就能立即进入。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2. 硬件方案&lt;/h3&gt;
&lt;p&gt;硬件方案利用特殊的CPU指令来提供原子操作，实现起来更简单高效。&lt;/p&gt;
&lt;h4&gt;3.2.1. 关闭中断 (Disable Interrupts)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (true) {
    disable_interrupts();
    // critical section
    enable_interrupts();
    // remainder section
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;: 在单处理器系统中，关闭中断可以阻止操作系统进行上下文切换，从而保证临界区代码的连续执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 简单、高效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代价高&lt;/strong&gt;: 长期关闭中断会影响系统响应其他事件的能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不适用于多处理器&lt;/strong&gt;: 在多核CPU上，一个核心关闭中断并不能阻止其他核心并发访问共享内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滥用风险&lt;/strong&gt;: 如果把这个权力交给用户进程，一个恶意或有bug的程序可能会导致整个系统瘫痪。因此，它只适用于操作系统内核自身。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考题提问&lt;/strong&gt;：单处理器下，关闭中断是否符合临界区的使用原则 (有空让进、无空等待、有限等待) ？
&lt;strong&gt;思考题解答&lt;/strong&gt;：在单处理器系统中，关闭中断的方式&lt;strong&gt;符合&lt;/strong&gt;&quot;有空让进&quot;和&quot;无空等待&quot;原则。因为只有一个处理器，关闭中断后不会被其他进程抢占，能保证互斥和不会出现空等。但它&lt;strong&gt;不一定符合&lt;/strong&gt;&quot;有限等待&quot;原则：如果某个进程长时间占用临界区，其他进程会一直等待，甚至可能被饿死。此外，这种方式&lt;strong&gt;不符合&lt;/strong&gt;&quot;让权等待&quot;原则，因为等待的进程无法主动让出CPU，整个系统调度被阻塞，属于一种特殊的忙等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;3.2.2. 测试并加锁 (Test-and-Set Lock, TSL) 指令&lt;/h4&gt;
&lt;p&gt;TSL是一条&lt;strong&gt;原子指令&lt;/strong&gt;，它执行两个操作：读取一个内存位置 (锁变量) 的值到寄存器，然后将该内存位置设置为1。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;enter_region:
    TSL REGISTER, LOCK   ; 原子操作: 将LOCK的值复制到REGISTER, 并将LOCK设为1
    CMP REGISTER, #0     ; 比较之前LOCK的值是否为0
    JNE enter_region     ; 如果不为0 (锁已被占用), 则循环等待
    RET                  ; 如果为0 (成功获取锁), 返回并进入临界区

leave_region:
    MOVE LOCK, #0        ; 将LOCK的值设为0, 释放锁
    RET
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;: &lt;code&gt;TSL&lt;/code&gt;指令的原子性保证了在检查锁和设置锁之间不会有上下文切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忙等待 (Busy Waiting) / 自旋锁 (Spin Lock)&lt;/strong&gt;: 这种实现方式会让等待的进程在一个循环中持续消耗CPU时间，因此也称为自旋锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级反转&lt;/strong&gt;: 如果一个低优先级进程持有锁，而一个高优先级进程在自旋等待，就会发生优先级反转。高优先级进程无法执行，而低优先级进程可能得不到CPU时间来释放锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考题提问&lt;/strong&gt;: TSL指令对多处理器系统&lt;strong&gt;有效&lt;/strong&gt;吗？为什么？
&lt;strong&gt;思考题解答&lt;/strong&gt;: TSL指令对多处理器系统&lt;strong&gt;有效&lt;/strong&gt;。因为&lt;code&gt;TSL&lt;/code&gt;指令会锁定内存总线，确保在它执行期间，其他CPU核心无法访问同一个内存地址&lt;code&gt;LOCK&lt;/code&gt;。这保证了操作的原子性是跨核心的。&lt;code&gt;XCHG&lt;/code&gt; (原子交换) 指令也能达到同样的效果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. 高级同步机制&lt;/h2&gt;
&lt;p&gt;软件和硬件方案都存在问题 (复杂性、忙等待等) 。为了解决这些问题，操作系统和编程语言提供了更高级、更易用的同步机制。&lt;/p&gt;
&lt;h3&gt;4.1. 信号量 (Semaphore) 及 P/V 操作&lt;/h3&gt;
&lt;p&gt;由Dijkstra在1965年提出，是一种功能强大且经典的同步工具。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;: 信号量是一个特殊的整数变量，只能通过两个原子操作来访问：&lt;code&gt;P&lt;/code&gt;和&lt;code&gt;V&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; (Proberen, test): 检查并尝试减少信号量的值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt; (Verhogen, increment): 增加信号量的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;本课程定义的 P/V 操作&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct semaphore {
    int count;          // 计数值
    queueType queue;    // 等待队列
};

// P(s) 操作 - 申请资源
P(s) {
    s.count--;
    if (s.count &amp;#x3C; 0) {
        // 资源不足，阻塞当前进程
        block_process();
        add_to_queue(s.queue);
        reschedule();
    }
}

// V(s) 操作 - 释放资源
V(s) {
    s.count++;
    if (s.count &amp;#x3C;= 0) {
        // 有进程在等待，唤醒一个
        wakeup_process_from_queue(s.queue);
        move_to_ready_queue();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;s.count&lt;/code&gt;的含义&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s.count &gt;= 0&lt;/code&gt;: 表示可用资源的数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s.count &amp;#x3C; 0&lt;/code&gt;: 其绝对值表示正在等待该资源的进程数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P/V操作是原语 (Atomic Action)&lt;/strong&gt;: 它们的执行不可分割，通常由操作系统通过关闭中断或硬件指令来保证。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;问题解答&lt;/strong&gt;: 信号量及PV操作&lt;strong&gt;既可以解决互斥问题，也可以解决同步问题&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解决互斥&lt;/strong&gt;: 使用一个&lt;strong&gt;二元信号量&lt;/strong&gt; (也叫互斥锁 &lt;code&gt;mutex&lt;/code&gt;) ，初值为1。在临界区前后分别调用 &lt;code&gt;P(mutex)&lt;/code&gt; 和 &lt;code&gt;V(mutex)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决同步&lt;/strong&gt;: 使用一个&lt;strong&gt;计数信号量&lt;/strong&gt;，其初值表示可用资源的数量。例如，同步某个事件的发生，初值为0。等待事件的进程执行 &lt;code&gt;P&lt;/code&gt; 操作被阻塞，触发事件的进程执行 &lt;code&gt;V&lt;/code&gt; 操作来唤醒等待者。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4.1.1. 案例：生产者-消费者问题&lt;/h4&gt;
&lt;p&gt;这是同步问题的经典模型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题描述&lt;/strong&gt;: 生产者进程向一个有界缓冲区中放入产品，消费者进程从中取出产品。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同步要求&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;缓冲区满时，生产者必须停止生产并等待。&lt;/li&gt;
&lt;li&gt;缓冲区空时，消费者必须停止消费并等待。&lt;/li&gt;
&lt;li&gt;对缓冲区的访问是临界区，必须互斥。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;信号量解决方案&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define N 100 // 缓冲区大小
semaphore mutex = 1;      // 互斥信号量，保护缓冲区访问，初值为1
semaphore empty = N;      // 同步信号量，记录空闲缓冲区数量，初值为N
semaphore full = 0;       // 同步信号量，记录产品数量，初值为0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;生产者代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void producer() {
    while (TRUE) {
        item = produce_item();
        P(&amp;#x26;empty); // 1. 申请一个空缓冲区 (若无则等待)
        P(&amp;#x26;mutex); // 2. 锁住缓冲区
        insert_item(item);
        V(&amp;#x26;mutex); // 3. 解锁缓冲区
        V(&amp;#x26;full);  // 4. 通知有一个产品可用
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;消费者代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void consumer() {
    while (TRUE) {
        P(&amp;#x26;full);  // 1. 申请一个产品 (若无则等待)
        P(&amp;#x26;mutex); // 2. 锁住缓冲区
        item = remove_item();
        V(&amp;#x26;mutex); // 3. 解锁缓冲区
        V(&amp;#x26;empty); // 4. 通知有一个空缓冲区可用
        consume_item(item);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考题解答&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;颠倒两个P操作的顺序会怎样?&lt;/strong&gt;
如果生产者代码变为 &lt;code&gt;P(&amp;#x26;mutex); P(&amp;#x26;empty);&lt;/code&gt;。当缓冲区满时 (&lt;code&gt;empty=0&lt;/code&gt;)，生产者会先成功执行&lt;code&gt;P(&amp;#x26;mutex)&lt;/code&gt;获得互斥锁，然后执行&lt;code&gt;P(&amp;#x26;empty)&lt;/code&gt;被阻塞。但因为它持有&lt;code&gt;mutex&lt;/code&gt;锁，消费者即使消费了产品想执行&lt;code&gt;V(&amp;#x26;empty)&lt;/code&gt;，也无法进入临界区 (被&lt;code&gt;P(&amp;#x26;mutex)&lt;/code&gt;阻塞) ，也就无法唤醒生产者。系统发生&lt;strong&gt;死锁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;颠倒两个V操作的顺序会怎样?&lt;/strong&gt;
&lt;code&gt;V(&amp;#x26;full); V(&amp;#x26;mutex);&lt;/code&gt; 颠倒为 &lt;code&gt;V(&amp;#x26;mutex); V(&amp;#x26;full);&lt;/code&gt;。这&lt;strong&gt;不会&lt;/strong&gt;导致逻辑错误或死锁。释放互斥锁和通知产品可用是两个独立的操作，它们的顺序不影响正确性。但通常建议先&lt;code&gt;V(mutex)&lt;/code&gt;再&lt;code&gt;V(full)&lt;/code&gt;，这样可以尽快释放临界区，让其他进程进入，提高并发度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4.1.2. 案例：第一类读者-写者问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题描述&lt;/strong&gt;: 多个进程共享数据，分为读者和写者。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要求&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;允许多个读者同时读。&lt;/li&gt;
&lt;li&gt;只允许一个写者写。&lt;/li&gt;
&lt;li&gt;读者和写者不能同时访问。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;采用读者优先解法:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在这种策略下，只要有读者在读，后续的读者都可以直接进入，写者必须等待所有读者结束后才能进入。可能会导致写者&quot;饿死&quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信号量和变量&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;semaphore w = 1;      // 控制写的互斥信号量
semaphore mutex = 1;  // 用于保护对 read_count 的修改
int read_count = 0;   // 记录当前正在读的读者数量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;写者代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void writer() {
    while (TRUE) {
        P(w); // 申请写权限
        // 写操作
        V(w); // 释放写权限
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;读者代码&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void reader() {
    while (TRUE) {
        P(mutex); // 锁住 read_count
        read_count++;
        if (read_count == 1) { // 第一个读者
            P(w); // 阻止写者进入
        }
        V(mutex); // 解锁 read_count

        // 读操作

        P(mutex); // 锁住 read_count
        read_count--;
        if (read_count == 0) { // 最后一个读者
            V(w); // 允许写者进入
        }
        V(mutex); // 解锁 read_count
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2. 管程 (Monitor)&lt;/h3&gt;
&lt;p&gt;信号量虽然强大，但使用时容易出错 (比如P/V顺序写反导致死锁) 。管程是一种更高级的、语言层面的同步机制，旨在简化并发编程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;: 管程是一个程序模块，它包含：
&lt;ol&gt;
&lt;li&gt;一组共享数据结构。&lt;/li&gt;
&lt;li&gt;一组操作这些数据的过程 (方法) 。&lt;/li&gt;
&lt;li&gt;一个初始化部分。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心特性&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动互斥&lt;/strong&gt;: 编译器保证在任何时刻，最多只有一个进程能在管程内部执行其过程。进程调用管程过程时，会自动获得互斥锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件变量 (Condition Variable)&lt;/strong&gt;: 用于解决同步问题。它不是一个值，而是一个等待队列。提供&lt;code&gt;wait&lt;/code&gt;和&lt;code&gt;signal&lt;/code&gt;操作。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;c.wait()&lt;/code&gt;: 调用此操作的进程会被阻塞，并&lt;strong&gt;释放管程的互斥锁&lt;/strong&gt;，然后进入条件变量&lt;code&gt;c&lt;/code&gt;的等待队列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c.signal()&lt;/code&gt;: 如果有进程在条件变量&lt;code&gt;c&lt;/code&gt;上等待，则唤醒其中一个。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.2.1. Hoare 管程 vs. Mesa 管程&lt;/h4&gt;
&lt;p&gt;当一个在管程中的进程P执行 &lt;code&gt;c.signal()&lt;/code&gt; 唤醒了另一个等待的进程Q时，管程中就有了两个活跃进程P和Q。如何处理？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hoare 管程 (P waits for Q)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规则&lt;/strong&gt;: P立刻挂起，Q马上执行。P进入一个&quot;紧急等待队列&quot;，优先级高于管程入口的等待队列。当Q执行完毕或再次&lt;code&gt;wait&lt;/code&gt;时，P才能恢复执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: &lt;code&gt;signal&lt;/code&gt;之后，被唤醒的进程Q可以确定它等待的条件现在是满足的。可以用&lt;code&gt;if&lt;/code&gt;来判断条件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 实现复杂，需要多次上下文切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mesa 管程 (P continues, Q waits)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规则&lt;/strong&gt;: &lt;code&gt;signal&lt;/code&gt;操作 (在Mesa中称为&lt;code&gt;notify&lt;/code&gt;) 只是一个&quot;提示&quot;。P继续执行，Q从等待队列移到就绪队列，但不会立即执行。Q何时执行取决于调度器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 实现简单，上下文切换少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;: 当Q最终被调度执行时，它等待的条件可能已经被其他进程改变了。因此，Q被唤醒后必须&lt;strong&gt;重新检查条件&lt;/strong&gt;。这导致了著名的&lt;strong&gt;while循环&lt;/strong&gt;范式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Hoare管程 - 生产者消费者&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-pascal&quot;&gt;monitor ProducerConsumer
    condition full, empty;
    integer count;

    procedure insert(item: integer);
    begin
        // Hoare管程, signal后条件确定为真, 用if即可
        if count == N then wait(full);
        ...
        if count == 1 then signal(empty);
    end;
    ...
end;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Mesa管程 - 生产者消费者&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;核心思想是，被唤醒的线程 (无论是生产者还是消费者) 必须用&lt;code&gt;while&lt;/code&gt;循环重新检查条件。这是因为从&lt;code&gt;c.notify&lt;/code&gt; (通知) 发生，到等待的线程被唤醒并重新获得管程锁的这段时间内，共享状态 (如缓冲区是满是空) 可能已经被其他线程改变了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;append&lt;/code&gt;和&lt;code&gt;take&lt;/code&gt;函数的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Producer Code
void append(char x) {
    // 在Mesa管程中，唤醒的线程必须重新检查条件，因为从notify()发生到线程重新获得管程锁的这段时间内，共享状态 (如缓冲区是满是空) 可能已经被其他线程改变了。
    // 这就是为什么必须使用while循环，而不是if语句。
    while (count == N) {
        c.wait(notfull); // 如果缓冲区满了，则等待
    }

    // 添加项目到缓冲区
    buffer[nextin] = x;
    nextin = (nextin + 1) % N;
    count++;

    c.notify(notempty);  // 通知任何等待的消费者。
}

// Consumer Code
// 注意：函数签名从 `take(char x)` 改为 `take(char *x)`
// 为了通过输出参数返回一个值，使其在C中有效。
void take(char *x) {
    while (count == 0) {
        c.wait(notempty); // 如果缓冲区为空，则等待
    }

    // 从缓冲区中移除项目
    *x = buffer[nextout];
    nextout = (nextout + 1) % N;
    count--;

    c.notify(notfull);    // 通知任何等待的生产者。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Mesa管程的改进&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;broadcast&lt;/code&gt;: 唤醒在某个条件变量上等待的&lt;strong&gt;所有&lt;/strong&gt;进程。当不确定哪个进程能满足条件时非常有用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时 &lt;code&gt;wait&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;wait&lt;/code&gt;操作可以带一个超时参数，防止因信号丢失而导致的永久等待 (饥饿) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.2.2. Java 中的管程&lt;/h4&gt;
&lt;p&gt;Java的 &lt;code&gt;synchronized&lt;/code&gt; 关键字和 &lt;code&gt;Object&lt;/code&gt; 类的 &lt;code&gt;wait()&lt;/code&gt;, &lt;code&gt;notify()&lt;/code&gt;, &lt;code&gt;notifyAll()&lt;/code&gt; 方法是Mesa管程模型的一种实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;synchronized&lt;/code&gt;&lt;/strong&gt;: 任何对象都可以作为锁。当一个线程进入一个对象的&lt;code&gt;synchronized&lt;/code&gt;方法或代码块时，它就获得了该对象的锁。这实现了自动互斥。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;wait()&lt;/code&gt;, &lt;code&gt;notify()&lt;/code&gt;, &lt;code&gt;notifyAll()&lt;/code&gt;&lt;/strong&gt;: 相当于Mesa管程的 &lt;code&gt;cwait&lt;/code&gt;, &lt;code&gt;cnotify&lt;/code&gt;, &lt;code&gt;cbroadcast&lt;/code&gt;。它们必须在&lt;code&gt;synchronized&lt;/code&gt;块中调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Java 实现生产者消费者&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// our_monitor 类的 insert 方法
public synchronized void insert(int val) {
    // 必须使用 while 循环检查条件
    while (count == N) {
        try {
            wait(); // 释放锁并等待
        } catch (InterruptedException e) {}
    }
    // ... 插入数据 ...
    count++;
    // 唤醒一个等待的消费者
    notifyAll(); // 使用notifyAll更健壮，防止信号丢失
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3. Pthreads 同步机制&lt;/h3&gt;
&lt;p&gt;Pthreads (POSIX Threads) 是一套标准的线程API，它提供了两种主要的同步工具：互斥锁和条件变量。这套API非常接近Mesa管程模型。&lt;/p&gt;
&lt;h4&gt;4.3.1. 互斥锁 (Mutex)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_t&lt;/code&gt;: 互斥锁变量类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_init()&lt;/code&gt;: 初始化互斥锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_lock()&lt;/code&gt;: 加锁。如果锁已被占用，线程阻塞。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_unlock()&lt;/code&gt;: 解锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_destroy()&lt;/code&gt;: 销毁互斥锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.3.2. 条件变量 (Condition Variable)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_t&lt;/code&gt;: 条件变量类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_init()&lt;/code&gt;: 初始化条件变量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_wait(cond, mutex)&lt;/code&gt;:
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;原子地&lt;/strong&gt;解锁 &lt;code&gt;mutex&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;使线程阻塞在 &lt;code&gt;cond&lt;/code&gt; 上。&lt;/li&gt;
&lt;li&gt;当被唤醒后，&lt;strong&gt;重新自动地&lt;/strong&gt;锁上 &lt;code&gt;mutex&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_signal(cond)&lt;/code&gt;: 唤醒至少一个在&lt;code&gt;cond&lt;/code&gt;上等待的线程 (类似&lt;code&gt;notify&lt;/code&gt;) 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_broadcast(cond)&lt;/code&gt;: 唤醒所有在&lt;code&gt;cond&lt;/code&gt;上等待的线程 (类似&lt;code&gt;broadcast&lt;/code&gt;) 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_destroy()&lt;/code&gt;: 销毁条件变量。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3.3. Pthreads 解决生产者-消费者问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;代码框架&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;pthread_mutex_t the_mutex;
pthread_cond_t condc, condp; // c:消费者等待条件, p:生产者等待条件
int buffer = 0;

void *producer(void *ptr) {
    for (...) {
        pthread_mutex_lock(&amp;#x26;the_mutex); // 加锁
        // 缓冲区不为空(已被填充), 生产者等待
        while (buffer != 0) {
            pthread_cond_wait(&amp;#x26;condp, &amp;#x26;the_mutex);
        }
        buffer = i; // 生产
        pthread_cond_signal(&amp;#x26;condc); // 唤醒消费者
        pthread_mutex_unlock(&amp;#x26;the_mutex); // 解锁
    }
}

void *consumer(void *ptr) {
    for (...) {
        pthread_mutex_lock(&amp;#x26;the_mutex); // 加锁
        // 缓冲区为空, 消费者等待
        while (buffer == 0) {
            pthread_cond_wait(&amp;#x26;condc, &amp;#x26;the_mutex);
        }
        buffer = 0; // 消费
        pthread_cond_signal(&amp;#x26;condp); // 唤醒生产者
        pthread_mutex_unlock(&amp;#x26;the_mutex); // 解锁
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重点讨论&lt;/strong&gt;: 为什么 &lt;code&gt;pthread_cond_wait&lt;/code&gt; 必须用 &lt;code&gt;while&lt;/code&gt; 循环包裹？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;虚假唤醒 (Spurious Wakeup)&lt;/strong&gt;: 在某些系统实现中，线程可能在没有&lt;code&gt;signal&lt;/code&gt;或&lt;code&gt;broadcast&lt;/code&gt;的情况下被唤醒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mesa模型特性&lt;/strong&gt;: 正如之前讨论的，&lt;code&gt;signal&lt;/code&gt;只是一个提示。从线程被唤醒到它实际运行并重新获得锁之间，它等待的条件可能已经被其他线程改变了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果使用 &lt;code&gt;if&lt;/code&gt;，线程被唤醒后会盲目地继续执行，可能会操作一个不满足条件的共享状态，导致错误。&lt;code&gt;while&lt;/code&gt;循环确保线程在继续执行前，必须重新验证条件是否为真。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;5. 锁的实现与现代硬件&lt;/h2&gt;
&lt;h3&gt;5.1. 锁 (Mutex) 的实现&lt;/h3&gt;
&lt;p&gt;锁是实现其他高级同步机制的基础。&lt;/p&gt;
&lt;h4&gt;5.1.1. 基于开关中断的实现&lt;/h4&gt;
&lt;p&gt;这种方法有严重缺陷。
&lt;strong&gt;错误实现&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;lock() {
    disable_interrupts();
    while (value != FREE); // 忙等待，且中断被关闭
    value = BUSY;
    enable_interrupts();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;: 如果锁被占用，&lt;code&gt;while&lt;/code&gt;循环会一直执行，而中断是关闭的，持有锁的线程永远无法被调度来释放锁，导致系统死锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;稍好的实现&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;lock() {
    disable_interrupts();
    while (value != FREE) {
        enable_interrupts();  // 循环时临时开中断
        // some delay...
        disable_interrupts(); // 再次检查前关中断
    }
    value = BUSY;
    enable_interrupts();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;: 在 &lt;code&gt;enable_interrupts()&lt;/code&gt; 和下一次 &lt;code&gt;disable_interrupts()&lt;/code&gt; 之间，可能会一直被新的 &lt;code&gt;lock&lt;/code&gt; 调用阻塞导致饥饿。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正确的无忙等待实现 (OS内核级)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;lock() {
    disable_interrupts();
    if (value == FREE) {
        value = BUSY;
    } else {
        // (1)
        add_thread_to_queue; 
        // (2)
        switch_to_next_thread; 
        // (3)
    }
    enable_interrupts();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思考题解答&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;这段代码展示了操作系统内核在获取锁失败时，如何让线程休眠等待。为防止竞态条件，从关中断到线程安全地休眠或成功获取锁的整个过程，其原子性至关重要。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在位置 (1) 开中断（入队前）：不可行&lt;/strong&gt; ❌
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;: &lt;strong&gt;&quot;丢失唤醒&quot; (Lost Wakeup)&lt;/strong&gt;。如果在此处开启中断，持有锁的线程可能会立即运行并释放锁。&lt;code&gt;unlock&lt;/code&gt; 操作会检查等待队列，但因为当前线程还未入队，所以它不会被唤醒。当中断返回后，当前线程会继续执行，将自己加入一个空队列并进入休眠，从而永久错过了已经发生的唤醒信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;: 检查锁的状态和将线程加入等待队列必须合并为一个原子操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在位置 (2) 开中断（休眠前）：不可行&lt;/strong&gt; ❌
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;: &lt;strong&gt;同样是&quot;丢失唤醒&quot;&lt;/strong&gt;。线程已在队列中，但尚未调用 &lt;code&gt;switch_to_next_thread&lt;/code&gt; 进入休眠状态。如果此时被唤醒（例如，状态被置为 &lt;code&gt;READY&lt;/code&gt;），它紧接着会继续执行 &lt;code&gt;switch_to_next_thread&lt;/code&gt;，该操作会将其状态置为 &lt;code&gt;BLOCKED&lt;/code&gt; 并让出CPU。结果是，线程刚被唤醒，马上又睡着了，导致唤醒操作被无效化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;: 将线程加入队列和使其休眠也必须是一个原子操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在位置 (3) 开中断（唤醒后）：可行且必要&lt;/strong&gt; ✅
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态&lt;/strong&gt;: 线程执行到这里，意味着它已经被其他线程（通过 &lt;code&gt;unlock&lt;/code&gt;）唤醒，并且调度器已选择它重新运行。此时，该线程已经&quot;获得&quot;了锁的所有权，但CPU仍处于之前 &lt;code&gt;lock&lt;/code&gt; 操作时关闭中断的状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作&lt;/strong&gt;: 在 &lt;code&gt;lock()&lt;/code&gt; 函数返回、进入临界区代码之前，&lt;strong&gt;必须&lt;/strong&gt;重新开启中断，以保证系统的正常响应。代码中，执行路径会自然地落到最后的 &lt;code&gt;enable_interrupts()&lt;/code&gt;，这在逻辑上是完全正确的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;5.1.2. 基于 TSL 指令的实现 (带让出CPU)&lt;/h4&gt;
&lt;p&gt;为了避免纯粹的自旋消耗CPU，可以在尝试获取锁失败后，主动让出CPU。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;mutex_lock:
    TSL REGISTER, MUTEX   ; 尝试获取锁
    CMP REGISTER, #0      ; 检查之前是否锁着
    JZE ok                ; 成功获取，跳转到ok
    CALL thread_yield     ; 失败，调用线程调度，让出CPU
    JMP mutex_lock        ; 被唤醒后，重新尝试
ok:
    RET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式结合了硬件原子指令和操作系统的调度，比纯自旋锁要好，但仍然有尝试-失败的开销。&lt;/p&gt;
&lt;h3&gt;5.2. 现代并发机制拓展&lt;/h3&gt;
&lt;h4&gt;5.2.1. Futex (Fast Userspace Mutex)&lt;/h4&gt;
&lt;p&gt;Futex是Linux中一种高效的锁机制，它结合了用户空间和内核空间的优点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速路径 (Fast Path)&lt;/strong&gt;: 在无竞争的情况下，锁的获取和释放完全在用户空间通过原子操作完成，无需进入内核，开销极小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢速路径 (Slow Path)&lt;/strong&gt;: 当发生锁竞争时，线程通过系统调用进入内核，将自己置于等待队列并睡眠，由内核负责唤醒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两阶段锁&lt;/strong&gt;: 这种思想可以演变为两阶段锁：第一阶段在用户态自旋一小段时间 (期望锁很快被释放) ，如果失败，第二阶段再进入内核休眠。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5.2.2. 现代硬件支持&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指令重排 (Instruction Reordering)&lt;/strong&gt;: 为了优化性能，CPU和编译器可能会改变指令的执行顺序。这在单线程下不影响结果，但在多线程下可能破坏程序员预期的同步逻辑。需要使用&lt;strong&gt;内存屏障 (Memory Fence/Barrier)&lt;/strong&gt; 来阻止重排。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更多原子指令&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CAS (Compare-and-Swap)&lt;/strong&gt;: &lt;code&gt;CAS(addr, old, new)&lt;/code&gt; 原子地检查 &lt;code&gt;*addr&lt;/code&gt; 是否等于 &lt;code&gt;old&lt;/code&gt;，如果是，就把它更新为 &lt;code&gt;new&lt;/code&gt;。这是实现&lt;strong&gt;无锁数据结构&lt;/strong&gt; (lock-free) 和乐观锁的基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fetch-and-Add&lt;/strong&gt;: 原子地读取一个值并给它加上一个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSP (Communicating Sequential Processes) 模型&lt;/strong&gt;: 与通过共享内存和锁进行同步不同，CSP模型强调进程之间通过显式的&lt;strong&gt;通信通道 (Channel)&lt;/strong&gt; 来传递消息和同步。Go语言的&lt;code&gt;channel&lt;/code&gt;就是基于此模型，它能有效避免竞争条件和死锁问题。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 08: File System 2</title><link>https://www.lyt0112.com/blog/operating_systems_note_08-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_08-zh</guid><description>Operating Systems Notes 08: 文件系统 2</description><pubDate>Sun, 18 May 2025 19:17:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-06-05&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 文件系统的管理&lt;/h2&gt;
&lt;p&gt;文件系统的管理主要涵盖三个方面：&lt;strong&gt;可靠性&lt;/strong&gt;、&lt;strong&gt;一致性&lt;/strong&gt;和&lt;strong&gt;安全性&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.1 文件系统的可靠性&lt;/h3&gt;
&lt;p&gt;可靠性指的是文件系统抵御和预防各种物理性或人为性破坏的能力。&lt;/p&gt;
&lt;h4&gt;1.1.1 坏块问题&lt;/h4&gt;
&lt;p&gt;磁盘上可能会出现物理损坏的块，称为坏块。文件系统需要能够识别、标记并隔离这些坏块，避免将数据写入其中。&lt;/p&gt;
&lt;h4&gt;1.1.2 备份 (Backup)&lt;/h4&gt;
&lt;p&gt;备份是通过转储操作，为文件或整个文件系统创建多个副本，以便在发生数据丢失或损坏时进行恢复。主要有以下几种备份方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全量转储 (Full Dump)&lt;/strong&gt;: 定期将&lt;strong&gt;所有&lt;/strong&gt;文件完整地拷贝到备份存储器（如磁带、另一块硬盘）上。这种方式最简单，但如果数据量大且变化少，会非常耗时且浪费存储空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增量转储 (Incremental Dump)&lt;/strong&gt;: 只转储自上次备份以来被&lt;strong&gt;修改过&lt;/strong&gt;的文件。这种方式可以显著减少备份所需的时间和空间开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理转储 (Physical Dump)&lt;/strong&gt;: 从磁盘的第一个物理块（0号块）开始，按顺序将所有磁盘块的内容原封不动地输出到备份介质。这种方式不关心文件系统的逻辑结构，备份和恢复速度快，但不够灵活。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑转储 (Logical Dump)&lt;/strong&gt;: 从一个或多个指定的目录开始，递归地遍历文件树，只转储那些自某个给定日期之后发生过修改的文件和目录。这种方式更灵活，可以支持更复杂的需求，例如恢复单个文件或目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 文件系统的一致性&lt;/h3&gt;
&lt;h4&gt;1.2.1 问题来源&lt;/h4&gt;
&lt;p&gt;文件系统的操作通常不是原子性的。例如，更新一个文件可能需要修改数据块、i-node（索引节点）和空闲块列表。这些修改通常先在内存的缓冲区中进行，然后再一次性或分批写回磁盘。如果在数据完全写回磁盘之前系统发生崩溃（例如突然断电），磁盘上的文件系统状态就可能处于一种中间状态，导致不一致。&lt;/p&gt;
&lt;p&gt;例如，一个数据块可能既被一个文件引用，又同时存在于空闲块列表中。&lt;/p&gt;
&lt;h4&gt;1.2.2 解决方案&lt;/h4&gt;
&lt;h5&gt;1.2.2.1 文件系统检查工具 (fsck)&lt;/h5&gt;
&lt;p&gt;为了解决不一致问题，可以设计一个实用程序（在UNIX/Linux中通常是 &lt;code&gt;fsck&lt;/code&gt;），在系统启动时运行，检查并修复文件系统的逻辑结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;UNIX &lt;code&gt;fsck&lt;/code&gt; 的工作过程示例&lt;/strong&gt;:
&lt;code&gt;fsck&lt;/code&gt; 会使用两张表来辅助检查，表的条目数与磁盘块数相同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;表1&lt;/strong&gt;: 用于记录每个磁盘块在&lt;strong&gt;文件中&lt;/strong&gt;被引用的次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表2&lt;/strong&gt;: 用于记录每个磁盘块在&lt;strong&gt;空闲块列表&lt;/strong&gt;中出现的次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;fsck&lt;/code&gt; 会遍历整个文件系统的元数据（如i-node和间接块），填充这两张表。理想情况下，对于任何一个磁盘块，它要么在表1中的计数为1，表2中的计数为0（已被分配）；要么在表1中的计数为0，表2中的计数为1（是空闲的）。&lt;/p&gt;
&lt;p&gt;下面是几种不一致的情况及其修复方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;情况A：一个块在空闲列表中，但未被任何文件使用。&lt;/strong&gt; 这是正常状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情况B：一个块在空闲列表中，同时也被文件使用（表1计数&gt;0，表2计数&gt;0）。&lt;/strong&gt; 这是严重错误。&lt;code&gt;fsck&lt;/code&gt; 会将其从空闲列表中移除来解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情况C：一个块既不在空闲列表，也未被任何文件使用（表1计数=0，表2计数=0）。&lt;/strong&gt; 这会导致块资源泄露。&lt;code&gt;fsck&lt;/code&gt; 会将其添加到空闲列表中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;情况D：一个块被两个或多个文件同时使用（表1计数&gt;1）。&lt;/strong&gt; 这也是严重错误。&lt;code&gt;fsck&lt;/code&gt; 通常会复制该块，让每个文件拥有自己的副本。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[开始检查] --&gt; B{遍历所有i-node};
    B --&gt; C{填充&apos;文件引用计数表&apos;};
    A --&gt; D{扫描空闲块列表};
    D --&gt; E{填充&apos;空闲块计数表&apos;};
    F{分析两张表};
    C --&gt; F;
    E --&gt; F;
    F --&gt; G{不一致?};
    G -- 是 --&gt; H[根据规则修复];
    G -- 否 --&gt; I[结束];
    H --&gt; I;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1.2.2.2 文件系统写入方式&lt;/h5&gt;
&lt;p&gt;为了在性能和一致性之间取得平衡，发展出了不同的写入策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通写 (Write-through)&lt;/strong&gt;: 内存缓冲区的修改&lt;strong&gt;立即&lt;/strong&gt;写回磁盘。这种方式能最大程度保证数据一致性，但因为频繁的磁盘I/O，性能非常差。FAT文件系统是典型的例子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟写 (Lazy-write)&lt;/strong&gt;: 修改只在内存缓冲区中进行，由操作系统决定何时将这些&quot;脏&quot;的缓冲区（write-back cache）写回磁盘。这种方式性能很好，但如果系统在写回前崩溃，数据丢失的风险很高，可恢复性差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可恢复写 (Journaling / Transaction Log)&lt;/strong&gt;: 这是在安全性和性能之间的一个优秀折中。在对文件系统的元数据进行实际修改之前，先把要做的&lt;strong&gt;操作步骤&lt;/strong&gt;（事务日志，Journal/Log）记录下来并写入磁盘。如果系统在修改过程中崩溃，重启后只需读取日志，就能重做（redo）未完成的操作，或撤销（undo）不完整的操作，从而快速恢复到一致状态。NTFS和现代Linux文件系统（如ext4）都采用了这种技术。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 文件系统的安全性&lt;/h3&gt;
&lt;p&gt;安全性旨在确保数据不会丢失或被未经授权的用户访问。&lt;/p&gt;
&lt;h4&gt;1.3.1 威胁来源&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据丢失&lt;/strong&gt;: 可能源于硬件或软件故障、自然灾害，甚至是人为失误。&lt;strong&gt;备份&lt;/strong&gt;是应对这类威胁的主要手段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;入侵者&lt;/strong&gt;:
&lt;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;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.3.2 文件保护机制&lt;/h4&gt;
&lt;h5&gt;1.3.2.1 用户身份验证&lt;/h5&gt;
&lt;p&gt;这是保护的第一道防线，用于在用户登录时验证其身份。常见方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户知道什么&lt;/strong&gt;: 口令（Password）、密码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户拥有什么&lt;/strong&gt;: 磁卡、智能卡、USB Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户是什么 (生物特征)&lt;/strong&gt;: 指纹、虹膜、人脸识别、声纹等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CAPTCHA测试&lt;/strong&gt;: 用于区分人类和计算机程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1.3.2.2 访问控制&lt;/h5&gt;
&lt;p&gt;当用户身份被确认后，系统需要决定该用户能对哪些文件进行哪些操作。主要有两种实现模型：&lt;/p&gt;
&lt;p&gt;| 特性         | 访问控制表 (ACL - Access Control List) | 能力表 (Capability List)               |
| :----------- | :------------------------------------- | :------------------------------------- |
| &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;查询方式&lt;/strong&gt; | &quot;谁能访问我？&quot;                         | &quot;我能访问谁？&quot;                         |
| &lt;strong&gt;存放位置&lt;/strong&gt; | 内核空间                               | 内核空间                               |
| &lt;strong&gt;常见场景&lt;/strong&gt; | 文件系统（如Unix权限、NTFS ACL）       | Kerberos, OAuth (能力凭证)             |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思考：这两种访问控制方式的优缺点是什么？&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ACL的优点&lt;/strong&gt;：管理特定资源的权限非常直观和方便。很容易回答&quot;谁能访问这个文件？&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACL的缺点&lt;/strong&gt;：当需要查看某个用户的所有权限时，必须遍历系统中所有文件的ACL，效率低下。权限的撤销也很直接（从ACL中移除即可）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能力表的优点&lt;/strong&gt;：查看某个用户的所有权限非常高效。用户在访问时主动出示&quot;能力凭证&quot;，验证逻辑简单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能力表的缺点&lt;/strong&gt;：权限的撤销比较困难，因为能力凭证已经被分发出去，系统需要有机制让它们失效。同时，很难回答&quot;谁能访问这个文件？&quot;，需要检查所有用户的能力表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1.3.2.3 拓展访问控制模型&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RBAC (Role-Based Access Control, 基于角色的访问控制)&lt;/strong&gt;: 在用户和权限之间引入&quot;角色&quot;层。权限被赋予角色，而用户被分配角色。当需要修改一组用户的权限时，只需修改角色的权限即可，大大简化了管理。例如，公司里的&quot;财务&quot;角色拥有访问财务报表的权限，&quot;人事&quot;角色拥有访问员工档案的权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ABAC (Attribute-Based Access Control, 基于属性的访问控制)&lt;/strong&gt;: 这是最灵活的模型。访问决策基于用户、资源、环境等多种&lt;strong&gt;属性&lt;/strong&gt;的组合。例如，可以制定一个规则：&quot;&apos;医生&apos;角色的用户，在&apos;工作时间&apos;内，从&apos;医院内网&apos;访问其&apos;自己病人&apos;的&apos;病历&apos;，可以被允许&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;1.3.2.4 UNIX的文件保护机制&lt;/h5&gt;
&lt;p&gt;UNIX采用了一种简化的、两级访问控制机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第一级：对访问者的识别&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件主 (owner)&lt;/strong&gt;: 创建文件的用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件主的同组用户 (group)&lt;/strong&gt;: 与文件主在同一个用户组的用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他用户 (other)&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;&lt;strong&gt;读操作 (r)&lt;/strong&gt;: read&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写操作 (w)&lt;/strong&gt;: write&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行操作 (x)&lt;/strong&gt;: execute&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两种分类组合成了经典的 &lt;code&gt;rwxrwxrwx&lt;/code&gt; 9-bit 权限模式。例如：&lt;code&gt;rwx r-x r--&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rwx&lt;/code&gt; (二进制111，八进制7): 文件主拥有读、写、执行权限。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r-x&lt;/code&gt; (二进制101，八进制5): 同组用户拥有读、执行权限。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r--&lt;/code&gt; (二进制100，八进制4): 其他用户仅拥有读权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;chmod 755 file&lt;/code&gt; 命令就是将文件的权限设置为 &lt;code&gt;rwxr-xr-x&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1.4 数据恢复技术&lt;/h3&gt;
&lt;p&gt;数据恢复的原理在于，当文件被删除或分区被格式化时，存储介质上的数据通常没有被立即清除，只是文件系统用于索引这些数据的元信息（如目录项、FAT表、i-node）被修改或清除了。只要这些数据没有被新的数据覆盖，就有可能被恢复。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 文件系统的性能&lt;/h2&gt;
&lt;p&gt;磁盘I/O是计算机系统中最慢的环节之一，因此提高文件系统性能的核心在于&lt;strong&gt;减少磁盘访问次数&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2.1 磁盘高速缓存 (Disk Cache / Buffer Cache)&lt;/h3&gt;
&lt;p&gt;这是最重要和最有效的性能提升手段。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原理&lt;/strong&gt;: 在内存中开辟一块区域，用于缓存从磁盘读出的数据块的副本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;当进程请求读取某个磁盘块时，系统首先检查该块是否已在缓存中。&lt;/li&gt;
&lt;li&gt;如果在（称为&lt;strong&gt;缓存命中，Cache Hit&lt;/strong&gt;），则直接从内存中读取数据，无需访问磁盘。&lt;/li&gt;
&lt;li&gt;如果不在（称为&lt;strong&gt;缓存未命中，Cache Miss&lt;/strong&gt;），系统会从磁盘读取该块，存入缓存，然后再提供给进程。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部性原理&lt;/strong&gt;: 由于程序的访问具有&lt;strong&gt;时间局部性&lt;/strong&gt;（一个数据被访问后，短时间内可能再次被访问）和&lt;strong&gt;空间局部性&lt;/strong&gt;（一个数据被访问后，其附近的数据也可能很快被访问），缓存的命中率通常很高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存置换算法&lt;/strong&gt;: 当缓存满了之后，需要选择一个块来替换出去。常用算法是 &lt;strong&gt;LRU (Least Recently Used, 最近最少使用)&lt;/strong&gt;，即替换掉最长时间没有被访问过的块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性问题&lt;/strong&gt;: 延迟写（write-back）策略会带来一致性问题。如果一个被修改的缓存块（脏块）在写回磁盘前系统崩溃，修改就会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &quot;操作系统内核 (Kernel Level)&quot;
        A[文件子系统]
        B[缓冲区缓存 Buffer Cache]
        C[设备驱动]
        A &amp;#x3C;--&gt; B
        B &amp;#x3C;--&gt; C
    end
    subgraph &quot;用户态 (User Level)&quot;
        D[用户程序]
    end
    subgraph &quot;硬件 (Hardware Level)&quot;
        E[磁盘]
    end
    D -- &quot;系统调用&quot; --&gt; A
    C &amp;#x3C;--&gt; E
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 提前读取 (Prefetch / Read-ahead)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 基于&lt;strong&gt;空间局部性原理&lt;/strong&gt;，当系统读取一个块时，它会猜测程序接下来可能会访问相邻的块，于是&quot;提前&quot;将这些块也读入缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开销&lt;/strong&gt;: 额外开销很小，因为磁盘寻道和旋转定位的时间已经付出，多读几个连续的块只增加了少量数据传输时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 合理分配磁盘空间&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思路&lt;/strong&gt;: 将逻辑上连续的文件块，在物理上也存放在相邻的位置，最好是在同一个柱面上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;: 减少磁盘臂的移动（寻道时间）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子：柱面组 (Cylinder Group)&lt;/strong&gt;: 像Berkeley FFS这样的文件系统会将磁盘划分为多个柱面组。每个柱面组都有自己的i-node区和数据块区。当创建一个新文件时，系统会尽量将其i-node和数据块都分配在同一个柱面组内，从而将访问局部化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 磁盘调度&lt;/h3&gt;
&lt;p&gt;当有多个磁盘I/O请求在队列中等待时，调度算法决定了为哪个请求服务的顺序。
一次访盘时间 = 寻道时间 + 旋转延迟时间 + 传输时间。
调度的主要目标是&lt;strong&gt;减少总的寻道时间&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题示例&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;磁盘请求队列 (按到达顺序): 98, 183, 37, 122, 14, 124, 65, 67&lt;/li&gt;
&lt;li&gt;磁头初始位置: 53&lt;/li&gt;
&lt;li&gt;磁盘柱面范围: 0-199 (假设)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.1 先来先服务 (FCFS - First-Come, First-Served)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;: 按请求到达的顺序处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务序列&lt;/strong&gt;: 53 -&gt; 98 -&gt; 183 -&gt; 37 -&gt; 122 -&gt; 14 -&gt; 124 -&gt; 65 -&gt; 67&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁头移动距离&lt;/strong&gt;: (98-53) + (183-98) + (183-37) + (122-37) + (122-14) + (124-14) + (124-65) + (67-65) = 45 + 85 + 146 + 85 + 108 + 110 + 59 + 2 = &lt;strong&gt;640&lt;/strong&gt; 个磁道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价&lt;/strong&gt;: 公平，但效率低下，磁头可能在磁盘两端来回移动。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.2 最短寻道时间优先 (SSTF - Shortest Seek Time First)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;: 优先服务与当前磁头位置最近的请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务序列&lt;/strong&gt;: 53 -&gt; 65 -&gt; 67 -&gt; 37 -&gt; 14 -&gt; 98 -&gt; 122 -&gt; 124 -&gt; 183&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁头移动距离&lt;/strong&gt;: (65-53) + (67-65) + (67-37) + (37-14) + (98-14) + (122-98) + (124-122) + (183-124) = 12 + 2 + 30 + 23 + 84 + 24 + 2 + 59 = &lt;strong&gt;236&lt;/strong&gt; 个磁道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价&lt;/strong&gt;: 性能比FCFS好很多，但可能导致&quot;饥饿&quot;现象，即离磁头较远的请求可能长时间得不到服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.3 扫描算法 (SCAN / Elevator Algorithm)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;: 磁头在一个方向上移动，服务所有沿途的请求，直到到达磁盘末端，然后反转方向继续。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务序列 (假设初始向磁道号减小的方向移动)&lt;/strong&gt;: 53 -&gt; 37 -&gt; 14 -&gt; (到达0) -&gt; 65 -&gt; 67 -&gt; 98 -&gt; 122 -&gt; 124 -&gt; 183&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁头移动距离&lt;/strong&gt;: (53-0) + (183-0) = 53 + 183 = &lt;strong&gt;236&lt;/strong&gt; 个磁道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价&lt;/strong&gt;: 性能较好，且避免了饥饿。但对两端的请求不公平，中间区域的请求被服务得更频繁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.4 单向扫描算法 (C-SCAN - Circular SCAN)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;: 磁头只在一个方向上服务请求（例如从0到199）。到达末端后，立即返回到起始端（0号），不服务任何请求，然后开始下一轮扫描。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务序列 (假设初始向磁道号增加的方向移动)&lt;/strong&gt;: 53 -&gt; 65 -&gt; 67 -&gt; 98 -&gt; 122 -&gt; 124 -&gt; 183 -&gt; (到199) -&gt; (跳回0) -&gt; 14 -&gt; 37&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;评价&lt;/strong&gt;: 提供了更均匀的等待时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.5 N-Step-SCAN 和 FSCAN&lt;/h4&gt;
&lt;p&gt;这两种是SCAN的变种，旨在解决&quot;磁头臂粘性&quot;问题（即在处理当前队列时，新来的请求如果恰好在磁头附近，会插队，导致磁头长时间停留在一个区域）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N-Step-SCAN&lt;/strong&gt;: 将请求队列分成长度为N的子队列，按顺序处理。在处理一个子队列时，新来的请求放入其他队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FSCAN&lt;/strong&gt;: 使用两个队列。扫描开始时，所有请求在一个队列，另一个为空。扫描期间，新请求放入空队列，直到当前队列处理完毕再切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.6 练习题解答&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;练习1&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求列表 (柱面, 磁头, 扇区): (5,4,1), (5,1,5), (5,4,5), (5,2,8)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析&lt;/strong&gt;: 所有请求都在同一个柱面（5号）。因此，寻道时间为0。我们需要优化旋转延迟。假设磁头从0扇区开始旋转，服务顺序应按扇区号从小到大排列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最优次序&lt;/strong&gt;: (5,4,1) -&gt; (5,1,5) -&gt; (5,4,5) -&gt; (5,2,8)。注意，(5,1,5)和(5,4,5)扇区号相同，可以在同一旋转位置被不同磁头服务，它们的先后顺序不影响时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;练习2&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求列表 (柱面, 磁头, 扇区): ①(9,6,3), ②(7,5,6), ③(15,20,6), ④(9,4,4), ⑤(20,9,5), ⑥(7,15,2)&lt;/li&gt;
&lt;li&gt;磁头初始位置: 8号柱面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析&lt;/strong&gt;: 这是一个综合优化问题，需要结合寻道和旋转。我们采用SSTF策略来决定柱面访问顺序。
&lt;ol&gt;
&lt;li&gt;当前在柱面8。最近的请求是柱面7和9。&lt;/li&gt;
&lt;li&gt;访问柱面9的请求①和④。移动1个柱面。&lt;/li&gt;
&lt;li&gt;访问柱面7的请求②和⑥。移动2个柱面。&lt;/li&gt;
&lt;li&gt;访问柱面15的请求③。移动8个柱面。&lt;/li&gt;
&lt;li&gt;访问柱面20的请求⑤。移动5个柱面。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最优柱面访问次序&lt;/strong&gt;: 8 -&gt; 9 -&gt; 7 -&gt; 15 -&gt; 20。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终请求服务次序&lt;/strong&gt;: 先后访问柱面9上的请求（①和④），再访问柱面7上的请求（⑥和②），然后是柱面15（③），最后是柱面20（⑤）。在同一柱面内，按扇区号优化。所以响应次序是 &lt;strong&gt;④ -&gt; ① -&gt; ⑥ -&gt; ② -&gt; ③ -&gt; ⑤&lt;/strong&gt; (假设磁头从0扇区开始转，先遇到4扇区再遇到3扇区，先遇到2扇区再遇到6扇区)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 信息的优化分布 (Optimized Information Layout on Disk)&lt;/h3&gt;
&lt;ul&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;ul&gt;
&lt;li&gt;一个处理程序要求按顺序处理文件中的8个逻辑记录。&lt;/li&gt;
&lt;li&gt;磁盘旋转一周为20毫秒 (假设每转处理完所有可见记录)。&lt;/li&gt;
&lt;li&gt;处理器处理一个记录需要5毫秒。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;图示分析 (Interleaving - 交叉因子)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无交叉 (Sequential Layout)&lt;/strong&gt;: 逻辑记录1, 2, 3, ... 存放在物理扇区 1, 2, 3, ...
&lt;ul&gt;
&lt;li&gt;读取记录1 (假设1ms传输) -&gt; 处理记录1 (5ms)。&lt;/li&gt;
&lt;li&gt;在处理记录1的5ms期间，磁盘继续旋转。当处理完毕准备读取记录2时，记录2所在的扇区可能已经转过磁头，需要等待几乎一整圈 (接近20ms) 才能再读到记录2。&lt;/li&gt;
&lt;li&gt;总时间 = 8 * (1ms读 + 5ms处理 + ~19ms等待) = 8 * 25ms = 200ms (估算)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有交叉 (Interleaved Layout)&lt;/strong&gt;: 为了匹配处理速度和磁盘旋转速度，逻辑记录的存放可以跳过一些物理扇区。
&lt;ul&gt;
&lt;li&gt;例如，如果每处理一个记录磁盘转过N个扇区，那么逻辑记录i和逻辑记录i+1之间应该间隔N-1个物理扇区。&lt;/li&gt;
&lt;li&gt;假设5ms处理时间对应磁盘转过1/4圈 (5ms/20ms)。如果一圈有8个扇区 (简化) ，则转过2个扇区。那么交叉因子可以是2。&lt;/li&gt;
&lt;li&gt;记录1在扇区1。读1，处理1。此时磁盘可能到了扇区3附近。如果记录2放在扇区3，则可以立即读取。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理扇区应该如何安排&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;逻辑记录: 1  2  3  4  5  6  7  8&lt;/li&gt;
&lt;li&gt;物理扇区: 1  4  7  2  5  8  3  6 (假设交叉因子为2，即跳过2个物理扇区)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标&lt;/strong&gt;: 当CPU处理完当前记录后，下一个逻辑记录所在的物理扇区正好旋转到磁头下，从而避免或减少旋转等待时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.6 RAID技术 (Redundant Arrays of Independent Disks)&lt;/h3&gt;
&lt;p&gt;RAID通过将多块独立的磁盘组合成一个逻辑单元，来提供比单个磁盘更高的性能、更大的容量和更好的容错能力。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据分条 (Striping)&lt;/strong&gt;: 将数据分成小块（条带），并行地写入/读出多块磁盘，以提高吞吐率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冗余 (Redundancy)&lt;/strong&gt;: 通过镜像或校验码来提供容错能力。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;主要RAID级别&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAID 0 (条带化)&lt;/strong&gt;: 纯粹的数据分条，没有冗余。读写性能最佳，但任何一块磁盘损坏都会导致所有数据丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    D0[&quot;Disk 0&quot;]
    D1[&quot;Disk 1&quot;]
    D2[&quot;Disk 2&quot;]
    D3[&quot;Disk 3&quot;]
    subgraph &quot;Logic Data&quot;
        S0[Stripe 0] --&gt; D0;
        S1[Stripe 1] --&gt; D1;
        S2[Stripe 2] --&gt; D2;
        S3[Stripe 3] --&gt; D3;
        S4[Stripe 4] --&gt; D0;
        S5[Stripe 5] --&gt; D1;
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAID 1 (镜像)&lt;/strong&gt;: 将数据完全复制到另一块磁盘。提供最高的数据安全性，但磁盘利用率只有50%。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    D0[&quot;Disk 0 (Primary)&quot;]
    D1[&quot;Disk 1 (Mirror)&quot;]
    subgraph &quot;Logic Data&quot;
        S0[Stripe 0] --&gt; D0;
        S0_M[Stripe 0] --&gt; D1;
        S1[Stripe 1] --&gt; D0;
        S1_M[Stripe 1] --&gt; D1;
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAID 3 (字节级带奇偶校验的条带化)&lt;/strong&gt;: 数据以字节为单位分条，并使用一块专用磁盘存储奇偶校验码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 4 (块级带奇偶校验的条带化)&lt;/strong&gt;: 与RAID 3类似，但以块为单位。校验盘成为写入瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 5 (块级带分布式奇偶校验的条带化)&lt;/strong&gt;: 将校验块分布到阵列中的所有磁盘上，消除了RAID 4的校验盘瓶颈。是性能、成本和可靠性之间的一个良好平衡。`&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 6 (双重分布式奇偶校验)&lt;/strong&gt;: 在RAID 5的基础上增加了第二个独立的校验块。可以容忍两块磁盘同时损坏，可靠性更高，但写入性能和成本代价也更大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 1+0 (或RAID 10)&lt;/strong&gt;: 先做镜像，再做条带化。性能和可靠性都很好，但成本高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAID 0+1&lt;/strong&gt;: 先做条带化，再做镜像。容错性不如RAID 10。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 文件系统的结构&lt;/h2&gt;
&lt;h3&gt;3.1 文件系统通用层次模型&lt;/h3&gt;
&lt;p&gt;一个典型的文件系统可以被看作是分层的结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[应用程序] --&gt; B[逻辑文件系统层];
    B --&gt; C[文件组织模块层];
    C --&gt; D[基本文件系统层];
    D --&gt; E[基本I/O控制层];
    E --&gt; F[物理磁盘];
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑文件系统层&lt;/strong&gt;: 管理元数据，如目录结构、文件属性。负责权限检查（安全）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件组织模块层&lt;/strong&gt;: 知道文件和它们的逻辑块是如何映射到磁盘上的物理块的。负责逻辑块号到物理块号的转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本文件系统层&lt;/strong&gt;: 负责向设备驱动程序发出具体的读/写物理块的命令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本I/O控制层&lt;/strong&gt;: 设备驱动程序和中断处理程序，直接与硬件交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 虚拟文件系统 (VFS - Virtual File System)&lt;/h3&gt;
&lt;p&gt;VFS是内核中的一个抽象层，它为用户态程序提供了一个统一的文件系统接口，从而屏蔽了底层不同类型文件系统的差异。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[&quot;用户程序 (open, read, write)&quot;] --&gt; B[&quot;系统调用接口 (sys_open, sys_read)&quot;];
    subgraph &quot;内核空间&quot;
        B --&gt; C[&quot;虚拟文件系统 (VFS)&quot;];
        C --&gt; D[&quot;ext4 模块&quot;];
        C --&gt; E[&quot;NTFS 模块&quot;];
        C --&gt; F[&quot;NFS 模块&quot;];
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了VFS，你的程序无需关心它操作的文件是在ext4分区、NTFS分区还是在一个网络文件系统（NFS）上，可以使用同样的方式来访问。&lt;/p&gt;
&lt;h3&gt;3.3 日志结构文件系统 (LFS - Log-structured File System)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想&lt;/strong&gt;: 为了将所有随机写操作转换为顺序写，从而极大地提高写入性能，LFS将整个磁盘看作一个巨大的、只追加的日志（Log）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作方式&lt;/strong&gt;: 任何修改（包括数据、i-node、目录等）都会被打包成一个段（segment），然后顺序地写入到日志的末尾。一个后台的清理线程会周期性地扫描日志，回收旧的、不再有效的空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 日志文件系统 (Journaling File System)&lt;/h3&gt;
&lt;p&gt;这是从LFS借鉴了&quot;日志&quot;思想并加以简化和实用化的产物。它不把所有数据都写入日志，通常&lt;strong&gt;只记录元数据的变更&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;典型删除操作步骤&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写日志&lt;/strong&gt;: 向日志区写入一个条目，记录&quot;我将要：1.从目录中删除文件X；2.释放i-node Y；3.归还数据块Z到空闲池&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交日志&lt;/strong&gt;: 确保日志条目已安全写入磁盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行操作&lt;/strong&gt;: 实际地去修改目录、i-node和空闲块列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清除日志&lt;/strong&gt;: 操作完成后，标记日志条目为已完成（或直接擦除）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果在第3步时系统崩溃，重启后只需检查日志，发现有一个未完成的事务，然后重新执行这三步操作即可，保证了文件系统的一致性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 分布式文件系统&lt;/h2&gt;
&lt;p&gt;分布式文件系统允许多台计算机通过网络共享文件，并且对用户来说，远程文件仿佛就在本地一样（透明性）。&lt;/p&gt;
&lt;h3&gt;4.1 HDFS (Hadoop Distributed File System) 概述&lt;/h3&gt;
&lt;p&gt;HDFS是专为在大规模廉价硬件集群上存储和处理海量数据而设计的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HDFS架构&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NameNode (主节点)&lt;/strong&gt;: 负责管理文件系统的命名空间（目录树）和所有文件的元数据（文件由哪些块组成，这些块存储在哪些DataNode上）。它是系统的&lt;strong&gt;单点故障&lt;/strong&gt;来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DataNode (从节点)&lt;/strong&gt;: 负责存储实际的数据块，并响应来自客户端或NameNode的读写请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Client_Read[客户端 Read]
    Client_Write[客户端 Write]

    NameNode
    
    subgraph &quot;机架1&quot;
        DN11[DataNode]
        DN12[DataNode]
    end
    
    subgraph &quot;机架2&quot;
        DN21[DataNode]
        DN22[DataNode]
    end

    Client_Read -- &quot;1. 请求元数据&quot; --&gt; NameNode
    NameNode -- &quot;2. 返回块位置&quot; --&gt; Client_Read
    Client_Read -- &quot;3. 直接读取数据&quot; --&gt; DN11
    Client_Read -- &quot;3. 直接读取数据&quot; --&gt; DN21

    Client_Write -- &quot;1. 请求写入&quot; --&gt; NameNode
    NameNode -- &quot;2. 分配块位置&quot; --&gt; Client_Write
    Client_Write -- &quot;3. 写入数据管道&quot; --&gt; DN12
    DN12 -- &quot;4. 复制&quot; --&gt; DN22
    DN22 -- &quot;5. 复制&quot; --&gt; DN11
&lt;/code&gt;&lt;/pre&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;: 文件被切分成巨大的块（默认128MB或256MB），以块为单位存储和复制。这减少了元数据的规模。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一次写入，多次读取&lt;/strong&gt;: 为流式数据访问优化，吞吐量高。支持文件追加，但不支持任意位置修改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据冗余和机架感知&lt;/strong&gt;: 每个块默认有3个副本。HDFS会尽量将副本分散到不同的机架上（例如，1个在本地机架，2个在远程机架），以防止整个机架掉电或网络故障导致数据不可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用 (High Availability)&lt;/strong&gt;: 为了解决NameNode的单点故障问题，引入了&lt;strong&gt;Active-Standby NameNode&lt;/strong&gt;模式。一个Active NameNode对外服务，一个Standby NameNode作为热备份，它们通过一个共享存储（如NFS或JournalNode集群）来同步元数据。如果Active节点宕机，Standby节点可以迅速接管。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有两道期末考试常考的练习题, 可以参考我写的&lt;a href=&quot;https://github.com/EmptyBlueBox/Operating_Systems-2025Spring-PKU/blob/main/Resource/%E6%80%9D%E8%80%83%E9%A2%98.md&quot;&gt;思考题答案&lt;/a&gt;中的解答.&lt;/p&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 07: File System 1</title><link>https://www.lyt0112.com/blog/operating_systems_note_07-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_07-zh</guid><description>Operating Systems Notes 07: 文件系统 1</description><pubDate>Wed, 23 Apr 2025 02:09:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-06-05&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 文件系统的基本概念&lt;/h2&gt;
&lt;h3&gt;1.1 问题导引&lt;/h3&gt;
&lt;p&gt;在我们开始之前，请大家先思考几个问题，这些问题将贯穿我们整个章节的学习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;怎样理解“文件系统是对磁盘的抽象”这句话?&lt;/li&gt;
&lt;li&gt;从用户角度看，文件系统的功能是什么？我们如何建立、删除、打开、关闭、读写文件？如何确定和修改文件的权限？&lt;/li&gt;
&lt;li&gt;从系统角度看，文件系统是如何实现的？它如何管理每个文件？如何设计并实现文件目录？如何管理磁盘空间？&lt;/li&gt;
&lt;li&gt;文件系统还需要考虑哪些其他需求？比如性能和安全。&lt;/li&gt;
&lt;li&gt;文件系统与其他操作系统功能（如I/O系统）的接口是怎样的？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;带着这些问题，我们来逐一揭开文件系统的神秘面纱。&lt;/p&gt;
&lt;h3&gt;1.2 文件 (File)&lt;/h3&gt;
&lt;h4&gt;1.2.1 什么是文件？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;“文件是对磁盘的抽象”&lt;/strong&gt;。这是一个非常重要的概念。磁盘的物理结构是磁道、扇区，它只能通过低级的指令（如“读写第X磁道第Y扇区”）来访问，这非常不便。文件系统将这些底层细节隐藏起来，提供给我们一个更高层次、更易于理解和操作的视图。&lt;/p&gt;
&lt;p&gt;在逻辑上，&lt;strong&gt;文件&lt;/strong&gt;是指一组带标识（标识即为文件名）的、有完整意义的信息项的序列。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信息项&lt;/strong&gt;: 构成文件内容的基本单位，可以是一个字节，也可以是多个字节。这些信息项之间具有顺序关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件内容的意义&lt;/strong&gt;: 由创建和使用文件的用户或程序来解释。操作系统本身不关心文件内容是文本、图像还是可执行程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个文件可以看作是信息项的线性序列，通常会有一个&lt;strong&gt;读写指针&lt;/strong&gt;来标识当前访问的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 信息项0 | 信息项1 | ... | 信息项i | ... | 信息项n-1 |
                      ^
                      |
                    读写指针
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在UNIX/Linux系统中，这个抽象被推向了极致，也就是著名的**“万物皆文件” (Everything is a file)**。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;普通文件 (如 &lt;code&gt;my_document.txt&lt;/code&gt;) 是文件。&lt;/li&gt;
&lt;li&gt;硬件设备 (如磁盘分区 &lt;code&gt;/dev/sda2&lt;/code&gt;，终端 &lt;code&gt;/dev/tty2&lt;/code&gt;) 也是文件。&lt;/li&gt;
&lt;li&gt;甚至内核的各种数据结构也通过 &lt;code&gt;/proc&lt;/code&gt; 目录以文件的形式暴露出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这极大地简化了应用程序与系统各种资源的交互方式，因为它们都可以使用同一套文件操作API（&lt;code&gt;open&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;）来访问。&lt;/p&gt;
&lt;h4&gt;1.2.2 文件的分类&lt;/h4&gt;
&lt;p&gt;根据文件的性质和用途，特别是在UNIX系统中，文件可以分为以下几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;普通文件 (Regular File)&lt;/strong&gt;: 包含用户的信息，内容通常是ASCII文本或二进制数据。这是我们最常见的文件类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录文件 (Directory File)&lt;/strong&gt;: 用于管理文件系统的系统文件。它的内容是该目录下包含的文件和子目录的列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特殊文件 (Special File)&lt;/strong&gt;: 用于I/O。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字符设备文件&lt;/strong&gt;: 用于模拟串行I/O设备，如终端、打印机。数据以字符为单位进行传输。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块设备文件&lt;/strong&gt;: 用于模拟磁盘等块设备。数据以数据块为单位进行传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管道文件 (Pipe)&lt;/strong&gt;: 用于进程间通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;套接字 (Socket)&lt;/strong&gt;: 用于不同机器间的网络通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;符号链接文件 (Symbolic Link)&lt;/strong&gt;: 其内容是到另一个文件的路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.2.3 文件的逻辑结构&lt;/h4&gt;
&lt;p&gt;文件的逻辑结构是指在用户看来，文件内部是如何组织的。这由用户的访问方式决定，主要有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流式文件 (Stream File)&lt;/strong&gt;: 文件被看作一个无结构的字节序列（如UNIX中的文件）。这是最简单、最常用的一种结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记录式文件 (Record File)&lt;/strong&gt;: 文件由一组定长或变长的记录构成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树形结构&lt;/strong&gt;: 文件由记录的树状结构组织而成，便于按键值查找。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    subgraph &quot;文件的逻辑结构&quot;
        A[&quot;字节序列 (流式文件)&quot;]
        B[&quot;记录序列 (记录式文件)&quot;]
        C[&quot;树形结构&quot;]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，还有一些更复杂的结构，如索引结构、索引顺序结构、散列结构等，它们通常用于数据库系统中，以优化数据检索效率。&lt;/p&gt;
&lt;h4&gt;1.2.4 文件的访问方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序存取 (Sequential Access)&lt;/strong&gt;: 从文件开头按字节或记录的顺序依次读写。这是最基本的方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机存取 (Random Access)&lt;/strong&gt;: 可以从文件中的任意位置开始读写。这需要操作系统提供&lt;code&gt;seek&lt;/code&gt;之类的操作来移动读写指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 文件属性与文件控制块 (FCB)&lt;/h3&gt;
&lt;p&gt;操作系统为了管理文件，需要记录每个文件的相关信息。这些信息被称为&lt;strong&gt;文件属性 (File Attributes)&lt;/strong&gt; 或 &lt;strong&gt;元数据 (Metadata)&lt;/strong&gt;。它们存放在一个称为&lt;strong&gt;文件控制块 (File Control Block, FCB)&lt;/strong&gt; 的数据结构中。&lt;/p&gt;
&lt;p&gt;一个FCB通常包含以下属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;标识信息&lt;/strong&gt;: 文件名，文件系统内部唯一的文件号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型信息&lt;/strong&gt;: 文件类型（普通、目录、特殊等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理位置信息&lt;/strong&gt;: 文件在磁盘上的存放地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大小信息&lt;/strong&gt;: 文件当前的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限保护信息&lt;/strong&gt;: 谁可以读、写、执行该文件（创建者、拥有者、访问控制列表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理信息&lt;/strong&gt;: 创建时间、最后修改时间、最后访问时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他&lt;/strong&gt;: 共享计数、只读/隐藏/系统等标志位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4 文件操作&lt;/h3&gt;
&lt;p&gt;操作系统提供了一系列系统调用来支持对文件的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create&lt;/code&gt;: 创建一个新文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete&lt;/code&gt;: 删除一个文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open&lt;/code&gt;: 打开一个已存在的文件，为后续的读写操作做准备。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;close&lt;/code&gt;: 关闭一个已打开的文件，释放相关资源。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read&lt;/code&gt;: 从文件中读取数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write&lt;/code&gt;: 向文件中写入数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;append&lt;/code&gt;: 在文件末尾追加数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seek&lt;/code&gt;: 移动文件读写指针到指定位置（用于随机存取）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get attributes&lt;/code&gt; / &lt;code&gt;set attributes&lt;/code&gt;: 获取或设置文件属性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rename&lt;/code&gt;: 重命名文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个典型的文件访问模式是：先 &lt;code&gt;open&lt;/code&gt;，然后进行若干次 &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;，最后 &lt;code&gt;close&lt;/code&gt;。
&lt;code&gt;f = open(name, flag);&lt;/code&gt;
&lt;code&gt;...&lt;/code&gt;
&lt;code&gt;read(f, ...);&lt;/code&gt;
&lt;code&gt;...&lt;/code&gt;
&lt;code&gt;close(f);&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;1.5 文件目录 (Directory)&lt;/h3&gt;
&lt;h4&gt;1.5.1 基本概念&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件目录 (File Directory)&lt;/strong&gt;: 为了统一管理所有文件的FCB，我们将它们组织起来，形成的这个集合就是文件目录。所以，&lt;strong&gt;文件目录是文件控制块的有序集合&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录项 (Directory Entry)&lt;/strong&gt;: 构成文件目录的基本单元，每个目录项对应一个文件或子目录。在简单系统中，目录项就是FCB。在复杂系统中（如UNIX），目录项可能只包含文件名和指向一个更完整数据结构（如i-node）的指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录文件 (Directory File)&lt;/strong&gt;: 为了持久化存储，文件目录通常会以一种特殊文件的形式存放在磁盘上，这个文件就叫目录文件。其内容由一系列目录项组成。为保证文件系统的一致性，目录文件通常只允许内核进行修改，应用程序通过系统调用来访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.5.2 目录结构&lt;/h4&gt;
&lt;p&gt;最常用的是&lt;strong&gt;树形目录结构 (Tree-structured Directory)&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;根目录 (Root Directory)&lt;/strong&gt;: 整个文件系统的起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子目录 (Subdirectory)&lt;/strong&gt;: 允许用户将文件分组，形成层次化结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径名 (Pathname)&lt;/strong&gt;: 唯一标识一个文件或目录在树中的位置。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;绝对路径名&lt;/strong&gt;: 从根目录开始的路径，例如 &lt;code&gt;/User_B/Draw/ABC&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相对路径名&lt;/strong&gt;: 从当前目录开始的路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当前目录 (Current/Working Directory)&lt;/strong&gt;: 每个进程都有一个当前目录。这使得用户可以使用更短的相对路径名来访问文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Root(Root directory) --&gt; A(User A)
    Root --&gt; B(User B)
    Root --&gt; C(User C)

    A --&gt; A_file(File A)

    B --&gt; B_subdir1(Dir B)
    B --&gt; B_subdir2(Dir B)
    B_subdir1 --&gt; B_file(File B)

    C --&gt; C_subdir1(Dir C)
    C --&gt; C_file1(File C)
    C_subdir1 --&gt; C_subdir2(Dir C)
    C_subdir1 --&gt; C_file2(File C)
    C_subdir2 --&gt; C_file3(File C)
    C_subdir2 --&gt; C_file4(File C)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.5.3 目录操作&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create&lt;/code&gt; / &lt;code&gt;delete&lt;/code&gt;: 创建或删除一个目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opendir&lt;/code&gt; / &lt;code&gt;closedir&lt;/code&gt;: 打开和关闭目录以读取其内容。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readdir&lt;/code&gt;: 读取目录中的下一个目录项。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rename&lt;/code&gt;: 重命名目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;link&lt;/code&gt; / &lt;code&gt;unlink&lt;/code&gt;: 创建或删除文件的链接（别名）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1.5.4 思考题：文件名解析&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;: 观察下图，试画出对应的树形目录结构，并给出解析文件名 &lt;code&gt;/User_B/Draw/ABC&lt;/code&gt; 的步骤。
&lt;strong&gt;图示&lt;/strong&gt;: (根据幻灯片 Page 25 的图)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Master Directory (Root)
&lt;ul&gt;
&lt;li&gt;User_A (Dir) -&gt; ...&lt;/li&gt;
&lt;li&gt;User_B (Dir)
&lt;ul&gt;
&lt;li&gt;Word (Dir) -&gt; ...&lt;/li&gt;
&lt;li&gt;Draw (Dir) -&gt; ABC (File)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;User_C (Dir) -&gt; ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;树形结构&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Root(&quot;Master Directory&quot;) --&gt; UserA(&quot;User_A&quot;)
    Root --&gt; UserB(&quot;User_B&quot;)
    Root --&gt; UserC(&quot;User_C&quot;)

    UserB --&gt; Word(&quot;Word&quot;)
    UserB --&gt; Draw(&quot;Draw&quot;)

    Draw --&gt; ABC(&quot;File ABC&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;文件名解析步骤 (/User_B/Draw/ABC)&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从根目录（Master Directory）开始查找。&lt;/li&gt;
&lt;li&gt;在根目录中，查找名为 &lt;code&gt;User_B&lt;/code&gt; 的目录项。找到后，获取该目录文件的位置信息。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;User_B&lt;/code&gt; 目录文件的内容，在其中查找名为 &lt;code&gt;Draw&lt;/code&gt; 的目录项。找到后，获取该目录文件的位置信息。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;Draw&lt;/code&gt; 目录文件的内容，在其中查找名为 &lt;code&gt;ABC&lt;/code&gt; 的目录项。&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;ABC&lt;/code&gt; 的目录项，从中获取文件的FCB信息（如物理地址、大小、权限等），解析完成。此时，文件就可以被打开和访问了。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.6 文件系统 (File System)&lt;/h3&gt;
&lt;p&gt;文件系统是操作系统中负责&lt;strong&gt;管理持久性数据&lt;/strong&gt;的部分。它不仅管理文件，还负责它们底层的存储。&lt;/p&gt;
&lt;p&gt;其主要功能可以总结为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实现文件的按名存取&lt;/strong&gt;: 用户通过文件名来访问数据，而无需关心物理地址。文件系统负责完成“名字空间”到“磁盘空间”的映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一管理磁盘空间&lt;/strong&gt;: 实施磁盘空间的分配与回收，跟踪哪些块是空闲的，哪些块已被占用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件信息的共享与保护&lt;/strong&gt;: 提供数据可靠性和安全保障机制，如访问控制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供便捷的接口&lt;/strong&gt;: 向用户提供一套方便使用的文件和目录操作命令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高性能&lt;/strong&gt;: 通过各种技术（如缓存、优化的布局策略）来提高文件系统的访问速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与I/O系统的统一接口&lt;/strong&gt;: 为上层应用提供与具体设备无关的I/O操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 文件系统的实现&lt;/h2&gt;
&lt;p&gt;现在我们从系统内部视角，看看文件系统是如何被设计和实现的。&lt;/p&gt;
&lt;h3&gt;2.1 存储介质与物理块&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;存储介质&lt;/strong&gt;: 文件通常存储在持久性存储介质上，如磁盘（HDD）、固态硬盘（SSD）、U盘等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抽象&lt;/strong&gt;: 操作系统将磁盘抽象为一维的逻辑块（Block）数组，从 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;n-1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据块 (Block)&lt;/strong&gt;: 存储设备被划分为大小相等的块，块是信息存储、传输和分配的基本单位。Windows中称为&lt;strong&gt;簇 (Cluster)&lt;/strong&gt;。一个块通常由若干个连续的扇区组成。扇区是物理存储单元，而块是逻辑存储单元。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;HDD vs SSD 寻址&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LBA (Logical Block Addressing)&lt;/strong&gt;: 操作系统使用的是逻辑块地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PBA (Physical Block Address)&lt;/strong&gt;: 存储设备内部的实际物理地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;在HDD中，LBA到PBA的映射相对固定，由硬盘固件管理。物理地址通常由柱面号、磁头号、扇区号组成。&lt;/li&gt;
&lt;li&gt;在SSD中，由于闪存的写入限制（磨损均衡），LBA到PBA的映射是动态的，由FTL（Flash Translation Layer）管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次典型的HDD访盘请求包括三个主要时间开销：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;寻道时间&lt;/strong&gt;: 移动磁臂到指定磁道。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;旋转延迟时间&lt;/strong&gt;: 等待目标扇区旋转到磁头下方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据传输时间&lt;/strong&gt;: 实际读写数据的时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 文件系统布局&lt;/h3&gt;
&lt;p&gt;在实现文件系统时，首先要考虑的是它在磁盘上和内存中的内容布局。&lt;/p&gt;
&lt;h4&gt;2.2.1 相关术语&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;磁盘分区 (Partition)&lt;/strong&gt;: 把一个物理磁盘的存储空间划分为几个相互独立的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件卷 (Volume)&lt;/strong&gt;: 一个逻辑分区，可以是一个分区，也可以跨越多个磁盘（如RAID）。同一个文件卷使用同一份管理数据（元数据）来进行文件分配和空间管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;格式化 (Format)&lt;/strong&gt;: 在一个文件卷上建立文件系统的过程。这包括创建和初始化用于文件分配和磁盘空间管理的元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2.2 磁盘上的内容&lt;/h4&gt;
&lt;p&gt;一个格式化好的文件卷通常包含以下几个部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;引导区 (Boot Block / Boot Sector)&lt;/strong&gt;: 包含从该卷引导操作系统所需的信息。通常是分区的第一个扇区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;卷管理信息 (Superblock)&lt;/strong&gt;: 描述整个文件卷的信息，如块大小、块总数、空闲块数量、i-node数量等。这是文件系统的“元数据中的元数据”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空闲空间管理区&lt;/strong&gt;: 用于记录哪些磁盘块是空闲的数据结构（如位图或空闲块链表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件元数据区&lt;/strong&gt;: 存放文件控制块（如UNIX中的i-node区）的区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据区&lt;/strong&gt;: 存放实际的用户文件和目录文件内容的区域。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;UNIX文件系统布局 (分区内)&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 引导记录 | 超级块 | 空闲区管理 | i-node区 | 根目录及数据区 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;FAT文件系统布局&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 引导区 | 文件分配表1 (FAT1) | 文件分配表2 (FAT2) | 根目录 | 其他目录和文件 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 文件的物理结构&lt;/h3&gt;
&lt;p&gt;这是文件系统实现的核心之一，决定了文件在物理介质上的存放方式。&lt;/p&gt;
&lt;h4&gt;2.3.1 连续结构 (Contiguous Allocation)&lt;/h4&gt;
&lt;p&gt;文件的信息存放在若干&lt;strong&gt;连续&lt;/strong&gt;的物理块中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCB中如何记录?&lt;/strong&gt; 只需记录起始块号和文件长度（总块数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;实现简单，读写效率高，因为磁头移动最少。&lt;/li&gt;
&lt;li&gt;支持顺序存取和随机存取。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;:
&lt;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;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3.2 链接结构 (Linked Allocation)&lt;/h4&gt;
&lt;p&gt;文件的信息存放在若干&lt;strong&gt;不连续&lt;/strong&gt;的物理块中，各块之间通过指针连接。每个块中都包含指向下一个块的指针。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCB中如何记录?&lt;/strong&gt; 只需记录起始块号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;有效利用磁盘空间，没有外部碎片问题。&lt;/li&gt;
&lt;li&gt;文件可以动态增长。&lt;/li&gt;
&lt;li&gt;有利于文件内容的插入删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不支持高效的随机存取&lt;/strong&gt;: 要访问第 N 块，必须从头开始沿着指针链顺序访问前 N-1 块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠性问题&lt;/strong&gt;: 指针丢失或损坏会导致文件数据丢失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指针占用空间&lt;/strong&gt;: 每个块都需要一部分空间来存储指针。&lt;/li&gt;
&lt;li&gt;寻道次数多，存取速度慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;变形：文件分配表 (File Allocation Table, FAT)&lt;/strong&gt;
为了解决链接结构的缺点，FAT文件系统将所有块的链接指针集中存放在磁盘开头的一个特殊区域——&lt;strong&gt;文件分配表 (FAT)&lt;/strong&gt; 中。FAT本身是一个大数组，数组的下标对应磁盘块号，数组元素的值表示下一个块的块号。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCB/目录项中如何记录?&lt;/strong&gt; 仍然是记录起始块号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;: 整个FAT可以被读入内存，使得随机访问变得高效。要找第 N 块，只需在内存的FAT数组中查找 N 次即可，无需访问磁盘数据区。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3.3 索引结构 (Indexed Allocation)&lt;/h4&gt;
&lt;p&gt;为每个文件建立一个专用的&lt;strong&gt;索引表 (Index Block)&lt;/strong&gt;，这个表中存放了文件所有数据块的块号。索引表本身也需要一个磁盘块来存储。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCB中如何记录?&lt;/strong&gt; 记录该文件的索引表的地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;保留了链接结构的优点（无外部碎片，动态增长）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持高效的随机存取&lt;/strong&gt;: 要访问第 N 块，只需先读入索引表，然后在索引表中直接找到第 N 项，就能得到目标块的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;索引表本身带来了系统开销（占用磁盘空间和内存）。&lt;/li&gt;
&lt;li&gt;对于小文件，索引表的开销可能显得过大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;问题：索引表很大，一个块放不下怎么办？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;链接方式&lt;/strong&gt;: 将多个索引表块链接起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级索引&lt;/strong&gt;: 用一个顶级索引表来索引次级索引表，形成层次结构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;综合模式&lt;/strong&gt;: 将直接地址和间接（多级）索引结合起来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;UNIX 多级索引结构 (综合模式)&lt;/strong&gt;
UNIX的i-node中通常包含一个包含15个地址项的数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前12项 (直接寻址)&lt;/strong&gt;: 直接指向文件的前12个数据块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第13项 (一级间接)&lt;/strong&gt;: 指向一个索引块，该索引块里存放了更多数据块的地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第14项 (二级间接)&lt;/strong&gt;: 指向一个二级索引块，该块中的每一项都指向一个一级索引块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第15项 (三级间接)&lt;/strong&gt;: 指向一个三级索引块。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Inode[&quot;i-node&quot;] --&gt; D1(Direct Block 1)
    Inode --&gt; D2(Direct Block 2)
    Inode --&gt; D_etc(...)
    Inode --&gt; D12(Direct Block 12)

    Inode -- &quot;Single Indirect&quot; --&gt; S_Indirect(一级索引块)
    S_Indirect --&gt; DataBlock1
    S_Indirect --&gt; DataBlock2
    S_Indirect --&gt; DataBlock_etc(...)

    Inode -- &quot;Double Indirect&quot; --&gt; D_Indirect(二级索引块)
    D_Indirect --&gt; S_Indirect_1(一级索引块)
    D_Indirect --&gt; S_Indirect_2(一级索引块)
    S_Indirect_1 --&gt; DataBlock_A
    S_Indirect_2 --&gt; DataBlock_B

    Inode -- &quot;Triple Indirect&quot; --&gt; T_Indirect(三级索引块)
    T_Indirect --&gt; D_Indirect_A(二级索引块)
    T_Indirect --&gt; D_Indirect_B(二级索引块)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;思考题&lt;/strong&gt;: UNIX中采用了三级索引结构后, 文件最大可达到多少个物理块? 假设扇区大小为512字节, 物理块等于扇区块大小, 一级索引表可以存放256个物理块号(一个块号占2字节)。
&lt;strong&gt;回答&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接块: 12 块&lt;/li&gt;
&lt;li&gt;一级间接: 256 块&lt;/li&gt;
&lt;li&gt;二级间接: $256 \times 256 = 65,536$ 块&lt;/li&gt;
&lt;li&gt;三级间接: $256 \times 256 \times 256 = 16,777,216$ 块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总计&lt;/strong&gt;: $12 + 256 + 65,536 + 16,777,216 = 16,843,020$ 个物理块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大文件大小&lt;/strong&gt;: $16,843,020 \times 512$ 字节 $\approx$ 8 GB。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 目录文件的实现&lt;/h3&gt;
&lt;h4&gt;2.4.1 目录项组织方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序表&lt;/strong&gt;: 最简单的方式，将目录项一个接一个地存放。查找文件时需要线性扫描，效率低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;散列表 (Hash Table)&lt;/strong&gt;: 根据文件名计算散列值，快速定位到目录项。解决了查找速度问题，但需要处理冲突。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B+树&lt;/strong&gt;: 很多现代文件系统（如NTFS）使用B+树来组织目录项。B+树是一种平衡树，能保证目录的增、删、查操作都有稳定且高效的性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4.2 目录检索的加速&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;: 如何加快目录检索？一个很大的目录文件，如果线性查找所有FCB会很慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：目录项分解法 (UNIX i-node)&lt;/strong&gt;
将FCB拆分为两部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;目录项&lt;/strong&gt;: 只包含&lt;strong&gt;文件名&lt;/strong&gt;和指向文件元数据的&lt;strong&gt;文件号 (i-node number)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;i-node (索引节点)&lt;/strong&gt;: 包含文件名之外的所有其他元数据（权限、大小、物理地址等）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所有i-node集中存放在磁盘的&lt;strong&gt;i-node区&lt;/strong&gt;。目录文件变得更小，因为每个条目只占几个字节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考题&lt;/strong&gt;: 假设一个FCB有48字节，物理块大小512字节。一个目录文件有128个目录项。分解前，符号目录项占8字节（文件名6B，文件号2B），基本目录项占42字节。计算查找一个文件的平均访盘次数。
&lt;strong&gt;回答&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分解前&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;一个块能放 $\frac{512}{48} \approx 10$ 个FCB。&lt;/li&gt;
&lt;li&gt;128个目录项需要 $\frac{128}{10} \approx 13$ 个块。&lt;/li&gt;
&lt;li&gt;假设目录文件是连续存放的，平均查找的期望为，$\sum_{i=1}^{13} \frac{i}{13} = \frac{13 \times (13 + 1)}{2 \times 13} = 7$ 次访盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分解后&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;目录文件只存符号目录项。一个块能放 $\frac{512}{8} = 64$ 个符号目录项。&lt;/li&gt;
&lt;li&gt;128个目录项需要 $\frac{128}{64} = 2$ 个块。&lt;/li&gt;
&lt;li&gt;i-node区存放基本目录项。一个块能放 $\frac{512}{42} \approx 12$ 个i-node。&lt;/li&gt;
&lt;li&gt;128个i-node需要 $\frac{128}{12} \approx 11$ 个块。&lt;/li&gt;
&lt;li&gt;查找过程：
&lt;ol&gt;
&lt;li&gt;读目录文件，找到文件名对应的i-node号。平均需要 $\frac{1}{2}+\frac{2}{2}=1.5$ 次访盘。&lt;/li&gt;
&lt;li&gt;根据i-node号去i-node区读取i-node信息。需要1次访盘。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;总平均访盘次数：$1.5 + 1 = 2.5$ 次。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结论&lt;/strong&gt;: 改进后，通过先在小型的目录文件中快速定位，再精确访问i-node，显著减少了磁盘访问次数，提高了检索速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 磁盘空间管理&lt;/h3&gt;
&lt;p&gt;文件系统需要跟踪所有空闲的磁盘块。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;位图法 (Bitmap)&lt;/strong&gt;: 用一个二进制位串来表示所有磁盘块。&lt;code&gt;1&lt;/code&gt;代表空闲，&lt;code&gt;0&lt;/code&gt;代表已分配（或反之）。
&lt;ul&gt;
&lt;li&gt;优点: 简单直观，很容易找到连续的空闲块。&lt;/li&gt;
&lt;li&gt;缺点: 对于大磁盘，位图本身可能很大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;计算公式&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;已知块号&lt;code&gt;b&lt;/code&gt;，字长&lt;code&gt;w&lt;/code&gt;：位图中的字号 $i = b / w$，位号 $j = b \pmod w$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空闲块链表&lt;/strong&gt;: 把所有空闲块用指针链接起来。
&lt;ul&gt;
&lt;li&gt;优点: 实现简单。&lt;/li&gt;
&lt;li&gt;缺点: 申请/归还多个块时效率低，因为要多次读写链表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成组链接法 (UNIX)&lt;/strong&gt;: 空闲链表的改进版。将空闲块分组，每组的第一个块记录了该组其他块的块号以及下一组的地址。
&lt;ul&gt;
&lt;li&gt;分配时，从当前组取出一个块。如果当前组用完，则从下一组中读入新的空闲块信息。&lt;/li&gt;
&lt;li&gt;回收时，将块加入当前组。如果当前组满了，就将当前组的信息写入回收的块中，使其成为新的“信息块”，并链接到空闲链表的头部。&lt;/li&gt;
&lt;li&gt;这种方法在分配和回收大量块时效率很高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.6 内存中的数据结构 (UNIX)&lt;/h3&gt;
&lt;p&gt;为了提高效率，操作系统会在内存中维护一些与文件系统相关的数据结构。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;系统级打开文件表 (System-wide Open File Table)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;整个系统只有一张。&lt;/li&gt;
&lt;li&gt;当任何进程第一次打开一个文件时，该文件的i-node被从磁盘读入内存，并在此表中创建一个条目。&lt;/li&gt;
&lt;li&gt;条目内容包括i-node的拷贝、引用计数（记录有多少个进程打开了此文件）等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程级打开文件表 (Per-process Open File Table)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;每个进程都有一张。&lt;/li&gt;
&lt;li&gt;当进程打开一个文件时，会在自己的这张表中创建一个条目。这个条目被称为&lt;strong&gt;文件描述符 (File Descriptor)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;条目内容包括打开方式（读/写）、当前的读写指针（&lt;strong&gt;重要：读写指针是每个打开实例私有的&lt;/strong&gt;）、以及一个指向系统级打开文件表对应条目的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;文件共享与fork&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个进程&lt;code&gt;fork&lt;/code&gt;一个子进程时，子进程会继承父进程的打开文件表。&lt;/li&gt;
&lt;li&gt;父子进程的文件描述符将指向&lt;strong&gt;同一个&lt;/strong&gt;系统级打开文件表条目。&lt;/li&gt;
&lt;li&gt;这意味着它们共享同一个读写指针。如果一个进程移动了指针，另一个进程也会看到变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;I/O重定向 (&lt;code&gt;dup2&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dup2(oldfd, newfd)&lt;/code&gt;系统调用可以使&lt;code&gt;newfd&lt;/code&gt;指向&lt;code&gt;oldfd&lt;/code&gt;所指向的同一个系统级文件表条目。&lt;/li&gt;
&lt;li&gt;这使得我们可以将标准输出（&lt;code&gt;fd=1&lt;/code&gt;）重定向到一个文件，从而实现命令输出到文件的功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    direction LR
    subgraph &quot;进程A&quot;
        A_Tbl[&quot;描述符表&quot;]
        A_Tbl -- &quot;fd 0 (stdin)&quot; --&gt; OF_Term
        A_Tbl -- &quot;fd 1 (stdout)&quot; --&gt; OF_Term
        A_Tbl -- &quot;fd 4&quot; --&gt; OF_FileB
    end

    subgraph &quot;内核&quot;
        OF_Term[&quot;系统打开文件表项 (终端) \n pos, refcnt=2&quot;]
        OF_FileB[&quot;系统打开文件表项 (文件B) \n pos, refcnt=1&quot;]

        OF_Term --&gt; V_Term[&quot;v-node表项 (终端信息)&quot;]
        OF_FileB --&gt; V_FileB[&quot;v-node表项 (文件B信息)&quot;]
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.7 文件操作的实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;create(文件名, ...)&lt;/code&gt;&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;在目录中查找，确保没有重名文件。&lt;/li&gt;
&lt;li&gt;申请一个新的i-node。&lt;/li&gt;
&lt;li&gt;在目录文件中增加一个新条目（文件名, i-node号）。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;open(文件名, ...)&lt;/code&gt;&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;解析路径名，找到文件的i-node号。&lt;/li&gt;
&lt;li&gt;检查系统打开文件表，看文件是否已被其他进程打开。
&lt;ul&gt;
&lt;li&gt;是：引用计数加1。&lt;/li&gt;
&lt;li&gt;否：从磁盘读入i-node到系统打开文件表的新条目中，引用计数置为1。&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;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;read(fd, ...)&lt;/code&gt;&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;fd&lt;/code&gt;在进程和系统打开文件表中找到文件的i-node。&lt;/li&gt;
&lt;li&gt;根据文件描述符中的读写指针和i-node中的物理地址信息，计算出要读取的物理块号。&lt;/li&gt;
&lt;li&gt;启动磁盘I/O操作，将数据从磁盘读入内核缓冲区，再从内核缓冲区复制到用户指定的内存地址。&lt;/li&gt;
&lt;li&gt;更新读写指针。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;seek(fd, ...)&lt;/code&gt;&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;fd&lt;/code&gt;找到进程打开文件表中的条目。&lt;/li&gt;
&lt;li&gt;修改其中的读写指针值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.8 文件共享&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬链接 (Hard Link)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;多个目录项（可以在不同目录，有不同文件名）指向&lt;strong&gt;同一个i-node&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;i-node中有一个&lt;strong&gt;链接计数 (link count)&lt;/strong&gt;，记录有多少个目录项指向它。&lt;/li&gt;
&lt;li&gt;创建链接时，计数加1。&lt;/li&gt;
&lt;li&gt;删除文件（即删除一个目录项）时，计数减1。&lt;/li&gt;
&lt;li&gt;只有当链接计数减为0时，操作系统才会真正删除i-node并回收数据块。&lt;/li&gt;
&lt;li&gt;所有硬链接是平等的，没有主次之分。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软链接/符号链接 (Soft/Symbolic Link)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;创建一个特殊类型的文件，其内容是&lt;strong&gt;另一个文件的路径名&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;访问软链接时，系统会读取其内容（路径名），然后去访问真正的文件。&lt;/li&gt;
&lt;li&gt;优点: 可以跨文件系统、可以链接到目录。&lt;/li&gt;
&lt;li&gt;缺点: 如果原文件被删除，软链接会失效（悬空指针）；解析需要额外的开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 文件系统实例&lt;/h2&gt;
&lt;h3&gt;3.1 UNIX 文件系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心&lt;/strong&gt;: i-node。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构&lt;/strong&gt;: 目录项（文件名+i-node号）和i-node（元数据）分离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找示例 &lt;code&gt;/usr/ast/mbox&lt;/code&gt;&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;内核从根目录（i-node号已知，通常是2）开始。&lt;/li&gt;
&lt;li&gt;读取根目录的数据块，查找名为 &lt;code&gt;usr&lt;/code&gt; 的条目，得到其i-node号（例如 7）。&lt;/li&gt;
&lt;li&gt;读取i-node 7，找到 &lt;code&gt;usr&lt;/code&gt; 目录的数据块地址。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;usr&lt;/code&gt; 目录的数据块，查找名为 &lt;code&gt;ast&lt;/code&gt; 的条目，得到其i-node号（例如 62）。&lt;/li&gt;
&lt;li&gt;读取i-node 62，找到 &lt;code&gt;ast&lt;/code&gt; 目录的数据块地址。&lt;/li&gt;
&lt;li&gt;读取 &lt;code&gt;ast&lt;/code&gt; 目录的数据块，查找名为 &lt;code&gt;mbox&lt;/code&gt; 的条目，得到其i-node号（例如 80）。&lt;/li&gt;
&lt;li&gt;现在内核有了 &lt;code&gt;mbox&lt;/code&gt; 文件的i-node 80，就可以进行读写等操作了。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 FAT (File Allocation Table) 文件系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心&lt;/strong&gt;: 文件分配表 (FAT)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;磁盘布局&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DBR (DOS Boot Record)&lt;/strong&gt;: 引导扇区，包含BPB（BIOS参数块），描述了文件系统的各种参数（每扇区字节数、每簇扇区数等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FAT表&lt;/strong&gt;: 紧跟在DBR之后。通常有两份作为备份。它是一个大数组，记录了簇（块）的链接关系和状态（未使用、坏簇、文件结束标志等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根目录区&lt;/strong&gt;: 在FAT12/16中，根目录大小和位置是固定的。在FAT32中，根目录是可扩展的，像普通文件一样存储。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据区&lt;/strong&gt;: 存放文件和子目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录项&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;32字节定长结构。&lt;/li&gt;
&lt;li&gt;包含文件名（8.3格式）、属性、创建/修改时间、文件大小和&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;ol&gt;
&lt;li&gt;&lt;strong&gt;定位目录项&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;如果文件在根目录，直接在根目录区中查找对应的目录项。&lt;/li&gt;
&lt;li&gt;如果文件在子目录中，需要先通过路径逐级查找：从根目录开始，找到第一级子目录的目录项，获取其起始簇号；读取该子目录的数据簇，在其中查找下一级目录或文件的目录项；重复此过程直到找到目标文件的目录项。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取起始簇号&lt;/strong&gt;: 从目录项中获取文件的起始簇号。目录项是32字节的定长结构，其中第26-27字节（低16位）和第20-21字节（高16位，仅FAT32）存储了文件的起始簇号。对于FAT12/16，只使用低16位；对于FAT32，需要组合高低16位得到完整的32位簇号。&lt;/li&gt;
&lt;li&gt;以该簇号为索引，在内存中的FAT表副本中查找，得到下一簇的簇号。&lt;/li&gt;
&lt;li&gt;重复此过程，直到遇到文件结束标志。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长文件名 (FAT32)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;采用了一种巧妙的兼容方案。对于长文件名，会创建多个特殊的&lt;strong&gt;长文件名目录项 (LFN entry)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每个LFN项存储长文件名的一部分（Unicode编码）。&lt;/li&gt;
&lt;li&gt;这些LFN项紧挨在真正的8.3格式短文件名目录项之前。&lt;/li&gt;
&lt;li&gt;不识别长文件名的旧系统会忽略这些LFEN项（因为它们的属性被设为卷标、系统、隐藏、只读的组合，旧系统不认识），只看到短文件名项。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 NTFS (New Technology File System)&lt;/h3&gt;
&lt;p&gt;Windows NT/XP及之后版本使用的文件系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设计目标&lt;/strong&gt;: 可靠性、高效性、安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心&lt;/strong&gt;: &lt;strong&gt;主控文件表 (Master File Table, MFT)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MFT&lt;/strong&gt;: MFT本身也是一个文件。它是一个由&lt;strong&gt;文件记录 (File Record)&lt;/strong&gt; 组成的数组。卷上的每一个文件或目录都至少对应MFT中的一个记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;万物皆属性&lt;/strong&gt;: 在NTFS中，所有数据都被看作是文件的&lt;strong&gt;属性 (Attribute)&lt;/strong&gt;。我们通常所说的文件内容，只是一个未命名的“数据属性流”。文件名、时间戳、权限等都是属性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件记录&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常驻属性 (Resident Attribute)&lt;/strong&gt;: 对于小文件或小属性，其内容直接存储在MFT记录中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非常驻属性 (Non-resident Attribute)&lt;/strong&gt;: 对于大文件或大属性，其内容存储在MFT之外的数据区。MFT记录中只存放指向这些数据块的指针，称为&lt;strong&gt;run&lt;/strong&gt;或&lt;strong&gt;extent&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录&lt;/strong&gt;: 目录也是一个文件，其“数据属性”是一个文件名索引。为了提高效率，这个索引通常用&lt;strong&gt;B+树&lt;/strong&gt;来组织。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠性&lt;/strong&gt;: 通过&lt;strong&gt;日志记录 (Logging)&lt;/strong&gt; 实现。对文件系统的所有修改操作，在实际写入磁盘前，会先在日志文件（&lt;code&gt;$LogFile&lt;/code&gt;）中记录。如果系统崩溃，重启后可以通过重做或撤销日志中的事务来恢复文件系统的一致性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;元数据文件&lt;/strong&gt;: MFT的前16个记录被保留给系统元数据文件使用，如&lt;code&gt;$Mft&lt;/code&gt;(MFT自身)、&lt;code&gt;$MftMirr&lt;/code&gt;(MFT镜像)、&lt;code&gt;$LogFile&lt;/code&gt;(日志文件)、&lt;code&gt;$Volume&lt;/code&gt;(卷信息)、&lt;code&gt;$.&lt;/code&gt;(根目录)等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 其他文件系统和技术&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ext4&lt;/strong&gt;: Linux默认的文件系统，是ext3的改进。使用&lt;strong&gt;extents&lt;/strong&gt;（连续块区段）代替传统的块映射，减少大文件碎片；支持&lt;strong&gt;延迟分配&lt;/strong&gt;，优化写入性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VFS (Virtual File System)&lt;/strong&gt;: 一个内核抽象层，为用户程序提供统一的文件系统接口（如&lt;code&gt;open&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;）。VFS可以将这些通用请求转换为底层具体文件系统（如ext4, FAT, NTFS）的特定操作。这使得Linux可以同时支持多种不同的文件系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NFS (Network File System)&lt;/strong&gt;: 一种分布式文件系统协议，允许客户端计算机像访问本地文件一样通过网络访问远程服务器上的文件。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 06: Virtual Memory Technology</title><link>https://www.lyt0112.com/blog/operating_systems_note_06-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_06-zh</guid><description>Operating Systems Notes 06: 虚拟内存技术</description><pubDate>Fri, 11 Apr 2025 01:36:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-03-25&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 虚拟内存基础概念&lt;/h2&gt;
&lt;h3&gt;1.1 虚拟地址空间 (Virtual Address Space)&lt;/h3&gt;
&lt;ul&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;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;
&lt;p&gt;&lt;strong&gt;提问：CPU取到的地址是什么地址？物理地址还是虚拟地址？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答:&lt;/strong&gt; CPU发出的地址通常是 &lt;strong&gt;虚拟地址&lt;/strong&gt; (Virtual Address)。这个虚拟地址随后会被 &lt;strong&gt;内存管理单元 (MMU)&lt;/strong&gt; 转换为物理地址 (Physical Address)。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    CPU -- Virtual Address --&gt; MMU;
    MMU -- Physical Address --&gt; PhysicalMemory[物理内存];
    MMU -- Page Fault --&gt; OS[操作系统];
    OS -- Data from Disk --&gt; PhysicalMemory;
    PhysicalMemory &amp;#x3C;--&gt; Disk[磁盘];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 虚拟内存管理的目标&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;透明性 (Transparency):&lt;/strong&gt; 运行的程序不应感知到虚拟内存机制的存在。程序员可以像操作一个巨大的连续内存一样编程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效率 (Efficiency):&lt;/strong&gt; 地址转换和页面调度应尽可能快，减少性能开销。需要 &lt;strong&gt;硬件支持&lt;/strong&gt; (如MMU, TLB)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护 (Protection):&lt;/strong&gt; 确保进程之间、进程与操作系统之间相互隔离，互不干扰。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.3 存储体系 (Memory Hierarchy)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;结构:&lt;/strong&gt; 寄存器 -&gt; Cache -&gt; 内存 (RAM) -&gt; 磁盘 (Disk)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统角色:&lt;/strong&gt; 协调各级存储器的使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 结合速度快但容量小的存储 (如Cache, RAM) 和速度慢但容量大的存储 (如磁盘) ，为用户提供一个既“快”又“大”的逻辑内存 (虚存) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4 相关术语辨识&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟内存 (Virtual Memory):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解释:&lt;/strong&gt; 将物理内存与磁盘结合使用，为程序提供一个容量远大于物理内存的逻辑存储空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键:&lt;/strong&gt; 程序引用的地址 (虚拟地址) 与物理内存地址不同，由系统自动转换。虚存大小受限于计算机寻址能力和可用磁盘空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟地址空间 (Virtual Address Space):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解释:&lt;/strong&gt; 分配给一个进程的逻辑地址范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟地址 (Virtual Address):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解释:&lt;/strong&gt; 虚拟地址空间中的某个地址。进程通过虚拟地址访问数据，仿佛它就在内存中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟存储技术 (Virtual Memory Technology):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解释:&lt;/strong&gt; 一种内存管理技术。程序运行时，只将其一部分装入内存，其余部分留在磁盘。当需要访问不在内存中的部分时，操作系统自动将其从磁盘调入内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 虚拟页式存储管理 (Paged Virtual Memory)&lt;/h2&gt;
&lt;h3&gt;2.1 基本思想&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按需加载:&lt;/strong&gt; 装载程序时，只装入部分 (甚至零个) 页面到物理内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态调页:&lt;/strong&gt; 当进程执行需要访问不在内存中的页面时，产生 &lt;strong&gt;页错误 (Page Fault)&lt;/strong&gt;，操作系统负责将所需页面从磁盘动态调入内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面换出:&lt;/strong&gt; 当内存不足时，将内存中暂时不用的页面交换 (写回) 到磁盘，以腾出空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现方式:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求调页 (Demand Paging):&lt;/strong&gt; 只有当页面被访问时才调入。 (最常用)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预先调页 (Prepaging):&lt;/strong&gt; 预测进程可能需要的页面并提前调入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质:&lt;/strong&gt; 资源转换技术，用CPU时间和磁盘空间换取 (看似无限的) 物理内存空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 核心策略 (Coffman &amp;#x26; Denning)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;取页策略 (Fetch Policy):&lt;/strong&gt; 决定何时将页面从磁盘调入内存。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求调页:&lt;/strong&gt; 发生缺页时才调入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预调页:&lt;/strong&gt; 预测并提前调入。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;放置策略 (Placement Policy):&lt;/strong&gt; 决定将调入的页面放置在物理内存的哪个 &lt;strong&gt;页框 (Page Frame)&lt;/strong&gt; 中。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解释:&lt;/strong&gt; 在分页系统中，任何空闲页框都可以存放任何页面，所以此策略相对简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;置换策略 (Replacement Policy):&lt;/strong&gt; 当内存没有空闲页框时，决定选择哪个页框中的页面换出到磁盘。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.3 设计与实现问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;页表表项 (PTE) 的设计。&lt;/li&gt;
&lt;li&gt;如何处理页表过大的问题 (如多级页表) 。&lt;/li&gt;
&lt;li&gt;地址重定位与快表 (TLB)。&lt;/li&gt;
&lt;li&gt;缺页异常 (Page Fault) 的处理机制。&lt;/li&gt;
&lt;li&gt;驻留集 (Resident Set) 管理。&lt;/li&gt;
&lt;li&gt;置换策略 (Replacement Algorithms)。&lt;/li&gt;
&lt;li&gt;清除策略 (Cleaning Policy)。&lt;/li&gt;
&lt;li&gt;加载控制 (Load Control)。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 硬件支持与核心机制&lt;/h2&gt;
&lt;h3&gt;3.1 页表表项 (Page Table Entry - PTE) 设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关键字段:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页框号 (Page Frame Number - PFN):&lt;/strong&gt; 该虚拟页对应的物理内存块号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;有效位/驻留位 (Valid/Present Bit - P):&lt;/strong&gt; 标记该页是否在物理内存中 (1=在内存, 0=不在内存/在磁盘)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问位/引用位 (Accessed/Referenced Bit - A/R):&lt;/strong&gt; 标记该页近期是否被访问过 (硬件在访问时设置，OS定期清零)。用于置换算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改位/脏位 (Dirty/Modified Bit - D/M):&lt;/strong&gt; 标记该页在内存中是否被修改过 (硬件在写入时设置)。如果为1，换出时必须写回磁盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护位 (Protection Bits - R/W/X):&lt;/strong&gt; 控制对该页的访问权限 (读/写/执行) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;i386 PDE/PTE 示例:&lt;/strong&gt; (展示了具体位域)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; (Present), &lt;code&gt;A&lt;/code&gt; (Accessed), &lt;code&gt;D&lt;/code&gt; (Dirty), &lt;code&gt;R/W&lt;/code&gt; (Read/Write), &lt;code&gt;U/S&lt;/code&gt; (User/Supervisor), &lt;code&gt;PWT&lt;/code&gt; (Page Write Through), &lt;code&gt;PCD&lt;/code&gt; (Page Cache Disable), &lt;code&gt;PS&lt;/code&gt; (Page Size - for large pages).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 处理页表过大的问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;32位地址空间 (4KB页面, 4B PTE): 页表本身占用内存 &lt;code&gt;2^20 个 PTE * 4B = 4MB = 2^22 = 1024 个 4KB 页面&lt;/code&gt; 的空间。&lt;/li&gt;
&lt;li&gt;若用户拥有 &lt;code&gt;2G = 2^31 = 2^19 个 4KB 页面&lt;/code&gt; 的物理空间，索引这块内存的**有效 (有效位 P=1)**的页表就占 512 页 (&lt;code&gt;2^19 * 4B / 4KB = 512&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;64位地址空间: 页表大小会变得极其巨大 (理论上 &lt;code&gt;2^52 * 8B&lt;/code&gt;，不可行)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;多级页表 (Multi-Level Page Tables):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 将巨大的线性页表变成树形结构。外层页表 (页目录) 的条目指向内层页表。只有被用到的内层页表才需要分配内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二级页表示例:&lt;/strong&gt; 虚拟地址分为 &lt;code&gt;页目录偏移 | 页表偏移 | 页内偏移&lt;/code&gt;。CR3寄存器指向页目录基址 -&gt; 查页目录得页表基址 -&gt; 查页表得页框号 -&gt; 拼接页内偏移得物理地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core i7 示例 (四级页表):&lt;/strong&gt; 48位虚拟地址，分为 &lt;code&gt;9 | 9 | 9 | 9 | 12&lt;/code&gt; 位，对应四级页表的索引和页内偏移。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 节省空间，只有实际使用的页表部分才需载入内存。虽然理论上总页表项数量不变，但实际上大多数进程只使用地址空间的一小部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间节省示例:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在32位系统中 (4GB地址空间) ，使用4KB页面，线性页表需要1M个页表项 (32位虚拟地址空间都需要对应的页表项) 。&lt;/li&gt;
&lt;li&gt;假设一个进程只使用了4MB的连续内存 (位于0x80000000-0x80400000) ，在二级页表中：
&lt;ul&gt;
&lt;li&gt;需要1个完整的页目录 (1024项，4KB)&lt;/li&gt;
&lt;li&gt;只需要1个二级页表 (1024项，4KB，对应使用的4MB区域)&lt;/li&gt;
&lt;li&gt;其余1023个二级页表 (对应未使用的地址空间) 根本不需要创建&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;总计只需8KB内存，而不是线性页表的4MB，节省了约99.8%的空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 每次地址翻译需要多次内存访问 (可通过TLB缓解) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反转页表 (Inverted Page Table):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 不再为每个进程维护一个页表，而是为整个物理内存建立一个全局页表。页表项 &lt;code&gt;i&lt;/code&gt; 对应物理页框 &lt;code&gt;i&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容:&lt;/strong&gt; 每个页表项记录 &lt;code&gt;(进程ID, 虚拟页号)&lt;/code&gt;，表示哪个进程的哪个虚拟页映射到了这个物理页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换:&lt;/strong&gt; 给定 &lt;code&gt;(进程ID, 虚拟页号)&lt;/code&gt;，需要搜索整个反转页表找到匹配项，得到其索引 (即物理页框号) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化:&lt;/strong&gt; 使用哈希表 (Hash Table) 加速查找。将 &lt;code&gt;(进程ID, 虚拟页号)&lt;/code&gt; 哈希到一个索引，指向反转页表中的一个桶 (可能需要链表解决冲突) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 页表大小与物理内存大小成正比，与虚拟地址空间大小和进程数量无关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 查找可能较慢 (即使有哈希) ，实现共享比较困难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用:&lt;/strong&gt; PowerPC, UltraSPARC, IA-64 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 内存管理单元 (MMU)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; CPU中的硬件单元，负责将虚拟地址转换为物理地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过程:&lt;/strong&gt; 接收CPU发出的虚拟地址，查询页表 (优先查TLB) ，生成物理地址或触发缺页异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 地址转换 (Address Translation)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件机制:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;CPU发出虚拟地址。&lt;/li&gt;
&lt;li&gt;MMU 从虚拟地址中提取 &lt;strong&gt;虚拟页号 (VPN)&lt;/strong&gt; 和 &lt;strong&gt;页内偏移 (Offset)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;MMU 使用 VPN (可能结合多级页表结构) 查找页表 (先查TLB) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查 PTE:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Case 1: PTE 有效 (Valid/Present bit = 1) 且权限允许:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;从 PTE 中获取 &lt;strong&gt;页框号 (PFN)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;将 PFN 与页内偏移拼接，形成 &lt;strong&gt;物理地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;访问内存。&lt;/li&gt;
&lt;li&gt;硬件根据访问类型 (读/写) 可能设置 &lt;strong&gt;访问位 (A)&lt;/strong&gt; 或 &lt;strong&gt;修改位 (D)&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Case 2: PTE 无效 (Valid/Present bit = 0) 或权限不足:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;MMU 产生 &lt;strong&gt;页错误 (Page Fault)&lt;/strong&gt; 异常，将控制权交给操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页错误处理 (OS):&lt;/strong&gt; (详见 3.6)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.5 快表 (Translation Look-aside Buffer - TLB)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt; 多级页表导致每次地址翻译需要多次内存访问，显著降低性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原理:&lt;/strong&gt; 利用 &lt;strong&gt;程序访问的局部性原理&lt;/strong&gt; (Locality of Reference)。最近访问过的页面很可能再次被访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;什么是TLB:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一种高速的、容量小的 &lt;strong&gt;相联存储器 (Associative Memory)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;特点：按 &lt;strong&gt;内容&lt;/strong&gt; 并行查找，速度极快。&lt;/li&gt;
&lt;li&gt;存储内容：缓存近期使用过的 &lt;strong&gt;虚拟页号 (VPN) 到 页框号 (PFN) 的映射&lt;/strong&gt; (即部分活跃的页表项)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流程:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;MMU 收到虚拟地址后，&lt;strong&gt;首先并行查找 TLB&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB Hit (命中):&lt;/strong&gt; 如果在 TLB 中找到匹配的 VPN，直接获取 PFN，快速完成地址转换。跳过页表查找。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB Miss (未命中):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;MMU 需要访问内存中的页表进行查找。&lt;/li&gt;
&lt;li&gt;找到 PFN 后，&lt;strong&gt;将 (VPN -&gt; PFN) 的映射关系装入 TLB&lt;/strong&gt; (可能需要替换掉 TLB 中的一个旧条目，使用LRU等策略)。&lt;/li&gt;
&lt;li&gt;完成地址转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB 刷新问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt; 进程切换时，原进程的 TLB 条目对新进程无效，需要刷新 TLB，导致新进程初期 TLB Miss 增多，性能下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PCID (Process Context Identifier) / ASID (Address Space Identifier):&lt;/strong&gt; 给 TLB 条目打上进程标识符。切换进程时，只需加载新进程的 PCID/ASID，TLB 中带有不同 ID 的条目不会被匹配，无需完全刷新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键参数:&lt;/strong&gt; TLB 的大小、位置 (通常集成在MMU或CPU核心内) 、替换策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.6 缺页异常 (Page Fault) 处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;触发:&lt;/strong&gt; 地址转换过程中，MMU 发现所需页面的 PTE 无效 (P=0) 或访问权限不足。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本质:&lt;/strong&gt; 一种硬件中断/异常，将控制权交给操作系统内核的 &lt;strong&gt;缺页异常处理程序 (Page Fault Handler)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理流程 (典型情况 - 页面不在内存):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;保存现场:&lt;/strong&gt; 保存用户进程的状态 (PC, 寄存器等) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确定原因:&lt;/strong&gt; 操作系统分析是真缺页 (P=0) ，还是保护性错误 (权限不足) 。如果是后者，可能终止进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位磁盘地址:&lt;/strong&gt; 如果是真缺页，查找该虚拟页在磁盘 (交换空间或文件) 上的位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找空闲页框:&lt;/strong&gt; 在物理内存中寻找一个空闲的页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理无空闲页框:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;若无空闲页框:&lt;/strong&gt; 执行 &lt;strong&gt;页面置换算法&lt;/strong&gt;，选择一个牺牲页框 (Victim Frame)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写回脏页:&lt;/strong&gt; 如果牺牲页框中的页面是 &quot;脏&quot; 的 (D=1)，则需要将其内容 &lt;strong&gt;写回磁盘&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调入页面:&lt;/strong&gt; 启动磁盘 I/O 操作，将所需的页面从磁盘读入选定的 (空闲或牺牲) 页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新页表:&lt;/strong&gt; 页面调入完成后，修改该虚拟页对应的 PTE (Page Table Entry，页表项)：设置 P=1，填入 PFN，清除 D 位，可能设置 A 位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复现场:&lt;/strong&gt; 恢复用户进程的状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新执行指令:&lt;/strong&gt; 重新执行导致缺页异常的指令。此时地址转换可以成功。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.7 驻留集管理 (Resident Set Management)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;驻留集 (Resident Set):&lt;/strong&gt; 进程当前在物理内存中的页面集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驻留集大小管理:&lt;/strong&gt; 决定给每个进程分配多少页框。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定分配策略 (Fixed Allocation):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&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;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 简单但缺乏灵活性，无法适应进程工作集大小的动态变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可变分配策略 (Variable Allocation):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;根据进程的 &lt;strong&gt;缺页率&lt;/strong&gt; 动态评估其局部性表现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整机制:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;缺页率高 → 增加页框数 (扩大驻留集) 。&lt;/li&gt;
&lt;li&gt;缺页率低 → 减少页框数 (缩小驻留集) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 能够适应程序局部性的变化，提高内存利用率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 实现复杂，需要监控缺页率，调整策略可能引入系统开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统开销考量:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;驻留集管理本身会消耗CPU时间和内存资源。&lt;/li&gt;
&lt;li&gt;过于频繁的调整可能导致系统开销超过收益。&lt;/li&gt;
&lt;li&gt;需要在响应性和开销之间找到平衡点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 页面置换算法 (Page Replacement Algorithms)&lt;/h2&gt;
&lt;h3&gt;4.1 置换问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;背景:&lt;/strong&gt; 当发生缺页异常且没有空闲物理页框时，需要选择一个当前在内存中的页面换出，为新页面腾出空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 选择一个 &lt;strong&gt;最近最不可能被访问&lt;/strong&gt; 的页面进行置换，以最小化未来的缺页次数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束:&lt;/strong&gt; 不能置换被 &lt;strong&gt;锁定 (Locked/Pinned)&lt;/strong&gt; 的页框 (如内核代码、I/O 缓冲区等) 。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页框锁定:&lt;/strong&gt; 通过 PTE 中的锁定位或特殊机制，防止 OS 将某些关键页面换出内存，避免 I/O 操作期间页面被换出导致错误，或保证实时任务的响应时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 置换范围&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;局部置换策略 (Local Replacement):&lt;/strong&gt; 仅在引发缺页的那个进程自己的 &lt;strong&gt;驻留集 (Resident Set)&lt;/strong&gt; 中选择牺牲页。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 进程间的隔离性好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 可能无法利用系统中其他进程不活跃的页框。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局置换策略 (Global Replacement):&lt;/strong&gt; 可以在内存中所有未锁定的页框中选择牺牲页，无论它属于哪个进程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 更灵活，可能提高系统整体吞吐率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 一个行为不良的进程可能挤占其他进程的页框；进程的缺页率受其他进程影响，难以控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与分配策略的关系:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;固定分配通常配合局部置换。&lt;/li&gt;
&lt;li&gt;可变分配可以配合局部或全局置换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 典型置换算法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最优置换算法 (Optimal - OPT / MIN):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 置换 &lt;strong&gt;未来最长时间内不会被访问&lt;/strong&gt; 的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 无法实现，因为需要预知未来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt; 作为性能比较的 &lt;strong&gt;基准 (Benchmark)&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先进先出算法 (First-In, First-Out - FIFO):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 置换 &lt;strong&gt;在内存中驻留时间最长&lt;/strong&gt; 的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 维护一个页面进入内存的队列，替换队首页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现简单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 性能较差，可能换出常用页面。存在 &lt;strong&gt;Belady 异常&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二次机会算法 (Second Chance - SCR):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; FIFO 的改进。检查队首页面的 &lt;strong&gt;访问位 (A/R)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;检查队首页面 PTE 的 A 位。&lt;/li&gt;
&lt;li&gt;如果 A=0，置换该页。&lt;/li&gt;
&lt;li&gt;如果 A=1，给它 &quot;第二次机会&quot;：将 A 位清零，并将该页移到队尾，然后检查下一个队首页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 比 FIFO 好，避免了立即换出刚被访问过的页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;时钟算法 (Clock):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; Second Chance Replacement (SCR，第二次机会算法) 的高效实现，避免了频繁移动页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 将所有物理页框组织成一个 &lt;strong&gt;循环链表 (缓冲区)&lt;/strong&gt;，用一个指针指向下一个要检查的候选页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;发生缺页时，从指针当前位置开始扫描。&lt;/li&gt;
&lt;li&gt;检查当前页框 PTE 的 A 位。&lt;/li&gt;
&lt;li&gt;如果 A=0，选择该页框进行置换，将新页面放入，指针前移。&lt;/li&gt;
&lt;li&gt;如果 A=1，将 A 位清零，指针前移，继续检查下一个页框。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现相对简单，性能优于 FIFO，接近 LRU。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最近未使用算法 (Not Recently Used - NRU):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 优先淘汰近期 &lt;strong&gt;既未被访问 (A=0) 也未被修改 (D=0)&lt;/strong&gt; 的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 利用 PTE 中的 &lt;strong&gt;访问位 (A)&lt;/strong&gt; 和 &lt;strong&gt;修改位 (D)&lt;/strong&gt; 将页面分为四类：
&lt;ul&gt;
&lt;li&gt;第 0 类: (A=0, D=0) - 未访问，未修改&lt;/li&gt;
&lt;li&gt;第 1 类: (A=0, D=1) - 未访问，已修改&lt;/li&gt;
&lt;li&gt;第 2 类: (A=1, D=0) - 已访问，未修改&lt;/li&gt;
&lt;li&gt;第 3 类: (A=1, D=1) - 已访问，已修改&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程:&lt;/strong&gt; OS 定期将所有页面的 A 位清零。发生缺页时，&lt;strong&gt;随机&lt;/strong&gt; 从编号最小的非空类别中选择一个页面进行置换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现简单，性能尚可。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NRU 的时钟实现:&lt;/strong&gt; (一种变体)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;扫描 1:&lt;/strong&gt; 找第一个 (A=0, D=0) 的页框，找到即置换。此过程不清 A 位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扫描 2 (若扫描 1 失败):&lt;/strong&gt; 重新扫描，找第一个 (A=0, D=1) 的页框。此过程中，&lt;strong&gt;跳过的页框 (A=1) 的 A 位被清零&lt;/strong&gt;。找到即置换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扫描 3 (若扫描 2 失败):&lt;/strong&gt; 此时所有页框 A 位都为 0。重复扫描 1 (必然能找到 A=0, D=0 或 A=0, D=1) ，然后如有必要重复扫描 2。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 优先换出干净页 (D=0)，节省写回磁盘的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最近最少使用算法 (Least Recently Used - LRU):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 置换 &lt;strong&gt;过去最长时间未被访问&lt;/strong&gt; 的页面。基于局部性原理，认为最久未用的页面，近期也最不可能被使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间戳法:&lt;/strong&gt; 每个 PTE 记录上次访问时间，置换时间戳最小的。 (硬件开销大)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈/链表法:&lt;/strong&gt; 维护一个按访问时间排序的页面栈/链表，每次访问将页面移到栈顶/链表头，置换栈底/链表尾的页面。 (软件开销大)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 性能非常好，接近 OPT。&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;最不经常使用算法 (Not Frequently Used - NFU):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 置换 &lt;strong&gt;过去访问次数最少&lt;/strong&gt; 的页面。LRU 的一种软件近似。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 每个 PTE 关联一个软件计数器，初值为 0。每次时钟中断，检查 A 位，若 A=1，则对应计数器加 1，并将 A 位清零。缺页时置换计数值最小的页面。&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;老化算法 (Aging):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 模拟 LRU。改进 NFU，使计数器能反映访问的时间远近。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 每个 PTE 关联一个多位计数器 (e.g., 8-bit)。每次时钟中断：
&lt;ol&gt;
&lt;li&gt;将每个计数器 &lt;strong&gt;右移 1 位&lt;/strong&gt; (模拟时间流逝，旧的访问权重降低)。&lt;/li&gt;
&lt;li&gt;将当前 PTE 的 &lt;strong&gt;A 位&lt;/strong&gt; 加到计数器的 &lt;strong&gt;最左边 (最高位)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;将 A 位清零。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;缺页时，置换计数值最小的页面。计数值小的页面表示近期访问较少或很久未访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 较好地模拟了 LRU，实现开销适中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与LRU的区别:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;精确度:&lt;/strong&gt; LRU 精确记录每次访问的时间顺序，而 Aging 只能近似反映访问频率和时间远近。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现开销:&lt;/strong&gt; LRU 需要在每次内存访问时更新数据结构，开销大；Aging 只在时钟中断时更新计数器，开销小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;历史长度:&lt;/strong&gt; LRU 可以无限追溯历史访问记录；Aging 受计数器位数限制 (如8位只能记录最近8个时间窗口的访问情况) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件支持:&lt;/strong&gt; LRU 需要专门硬件支持才能高效实现；Aging 只需要访问位支持，更易于实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4.4 算法示例与现象&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FIFO, LRU, OPT 缺页次数计算:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;例子:&lt;/strong&gt; 页面访问序列 &lt;code&gt;2 3 2 1 5 2 4 5 3 2 5 2&lt;/code&gt;，分配 3 个页框。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FIFO 算法过程:&lt;/strong&gt;
| 访问页面 | 2   | 3   | 2   | 1   | 5   | 2   | 4   | 5   | 3   | 2   | 5   | 2   |
| -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 页框1    | 2   | 2   | 2   | 2   | 5   | 5   | 5   | 5   | 3   | 3   | 3   | 3   |
| 页框2    | -   | 3   | 3   | 3   | 3   | 2   | 2   | 2   | 2   | 2   | 5   | 5   |
| 页框3    | -   | -   | -   | 1   | 1   | 1   | 4   | 4   | 4   | 4   | 4   | 2   |
| 缺页     | √   | √   | ×   | √   | √   | √   | √   | ×   | √   | ×   | √   | √   |&lt;/p&gt;
&lt;p&gt;总计9次缺页&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LRU 算法过程:&lt;/strong&gt;
| 访问页面 | 2   | 3   | 2   | 1   | 5   | 2   | 4   | 5   | 3   | 2   | 5   | 2   |
| -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 页框1    | 2   | 2   | 2   | 2   | 2   | 2   | 2   | 2   | 3   | 3   | 3   | 3   |
| 页框2    | -   | 3   | 3   | 3   | 5   | 5   | 5   | 5   | 5   | 5   | 5   | 5   |
| 页框3    | -   | -   | -   | 1   | 1   | 1   | 4   | 4   | 4   | 2   | 2   | 2   |
| 缺页     | √   | √   | ×   | √   | √   | ×   | √   | ×   | √   | √   | ×   | ×   |&lt;/p&gt;
&lt;p&gt;总计7次缺页&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OPT 算法过程:&lt;/strong&gt;
| 访问页面 | 2   | 3   | 2   | 1   | 5   | 2   | 4   | 5   | 3   | 2   | 5   | 2   |
| -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 页框1    | 2   | 2   | 2   | 2   | 2   | 2   | 4   | 4   | 4   | 2   | 2   | 2   |
| 页框2    | -   | 3   | 3   | 3   | 3   | 3   | 3   | 3   | 3   | 3   | 3   | 3   |
| 页框3    | -   | -   | -   | 1   | 5   | 5   | 5   | 5   | 5   | 5   | 5   | 5   |
| 缺页     | √   | √   | ×   | √   | √   | ×   | √   | ×   | ×   | √   | ×   | ×   |&lt;/p&gt;
&lt;p&gt;总计6次缺页, 可以发现重点是第五次访问要把 1 换出去, 因为我们知道未来信息 1 不再被用到了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Belady 异常 (Belady&apos;s Anomaly):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;现象:&lt;/strong&gt; 对于某些置换算法 (如 FIFO) ，增加分配给进程的物理页框数，缺页次数 &lt;strong&gt;反而增加&lt;/strong&gt; 的反常现象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; 序列 &lt;code&gt;1 2 3 4 1 2 5 1 2 3 4 5&lt;/code&gt;，FIFO算法。
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;m=3 时，缺页过程:
| 访问页面 | 1   | 2   | 3   | 4   | 1   | 2   | 5   | 1   | 2   | 3   | 4   | 5   |
| -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 页框1    | 1   | 1   | 1   | 4   | 4   | 4   | 5   | 5   | 5   | 5   | 5   | 5   |
| 页框2    | -   | 2   | 2   | 2   | 1   | 1   | 1   | 1   | 1   | 3   | 3   | 3   |
| 页框3    | -   | -   | 3   | 3   | 3   | 2   | 2   | 2   | 2   | 2   | 4   | 4   |
| 缺页     | √   | √   | √   | √   | √   | √   | √   | ×   | ×   | √   | √   | ×   |&lt;/p&gt;
&lt;p&gt;总计9次缺页&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;m=4 时，缺页过程:
| 访问页面 | 1   | 2   | 3   | 4   | 1   | 2   | 5   | 1   | 2   | 3   | 4   | 5   |
| -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 页框1    | 1   | 1   | 1   | 1   | 1   | 1   | 5   | 5   | 5   | 5   | 4   | 4   |
| 页框2    | -   | 2   | 2   | 2   | 2   | 2   | 2   | 1   | 1   | 1   | 1   | 5   |
| 页框3    | -   | -   | 3   | 3   | 3   | 3   | 3   | 3   | 2   | 2   | 2   | 2   |
| 页框4    | -   | -   | -   | 4   | 4   | 4   | 4   | 4   | 4   | 3   | 3   | 3   |
| 缺页     | √   | √   | √   | √   | ×   | ×   | √   | √   | √   | √   | √   | √   |&lt;/p&gt;
&lt;p&gt;总计10次缺页&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因:&lt;/strong&gt; FIFO 只考虑进来的时间，不考虑进来之后的访问情况。增加页框可能导致一个&quot;坏&quot;的页面 (未来会用到) 驻留更久，从而在后面挤掉了更有用的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LRU 和 OPT 不存在 Belady 异常:&lt;/strong&gt; 因为它们满足 &lt;strong&gt;栈属性 (Stack Property)&lt;/strong&gt;：即 &lt;code&gt;m&lt;/code&gt; 个页框时的内存内容总是 &lt;code&gt;m+1&lt;/code&gt; 个页框时内存内容的子集。增加页框只会包含更多有用的页，不会导致缺页增加。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.5 影响缺页次数的因素&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面置换算法:&lt;/strong&gt; 好算法 ≈ 少缺页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配给进程的物理页框数:&lt;/strong&gt; 太少会导致频繁缺页，过多则浪费内存。存在一个最佳范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面尺寸问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;确定页面大小对于分页的硬件设计非常重要，而对于操作系统是个可选的参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要考虑的因素:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内部碎片:&lt;/strong&gt; 页面越大，内部碎片越多；页面越小，内部碎片越少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表长度:&lt;/strong&gt; 页面越大，页表越小；页面越小，页表越大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;辅存的物理特性:&lt;/strong&gt; 磁盘传输特性影响最佳页面大小选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB 覆盖范围:&lt;/strong&gt; 影响地址转换性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小页面优缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 减少内部碎片，更适合程序局部性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 页表变大，TLB 效率可能降低，磁盘 I/O 效率低。&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; 页表小，TLB 覆盖范围大，磁盘 I/O 效率高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 内部碎片增加，可能不适合小局部性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最优页面大小:&lt;/strong&gt; 理论上可以用公式 P = √(2se) 来计算，其中 $s$ 是页表项的大小 (表示页表开销) ，$e$ 是平均程序段大小 (表示内部碎片开销) 。这个公式平衡了页表大小和内部碎片之间的权衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实际实现:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Intel 80x86/Pentium:&lt;/strong&gt; 支持 4KB 或 4MB 页面大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;现代系统:&lt;/strong&gt; 通常支持多种页面大小 (如 4KB, 2MB, 1GB) ，为有效使用TLB带来灵活性，但给操作系统带来复杂性。&lt;/li&gt;
&lt;li&gt;OS 和应用可根据需求灵活选择不同页面大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序的编制方法:&lt;/strong&gt; 访问模式影响局部性。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; 只分配了&lt;strong&gt;一个 4KB页框&lt;/strong&gt;，访问按行存储的二维数组 &lt;code&gt;A[1024][1024]&lt;/code&gt; (4KB页面)。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按行访问 (方法2):&lt;/strong&gt; &lt;code&gt;for(i)... for(j)... A[i][j]&lt;/code&gt;。空间局部性好，每次访问都在同一页或下一页，缺页少 ( 1024 次，每行开始时缺页) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按列访问 (方法1):&lt;/strong&gt; &lt;code&gt;for(j)... for(i)... A[i][j]&lt;/code&gt;。空间局部性差，每次访问 &lt;code&gt;A[i][j]&lt;/code&gt; 和 &lt;code&gt;A[i+1][j]&lt;/code&gt; 会跨越多个页面 (1024*4 bytes ≈ 1 page) ，导致大量缺页 (1024 * 1024 次) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;颠簸/抖动 (Thrashing):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 当系统内存严重不足，分配给进程的页框远小于其活跃页面所需时，进程会不断地发生缺页，大部分时间都用于页面换入换出，而不是真正执行计算。导致系统效率急剧下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因:&lt;/strong&gt; 并发度过高，或进程所需工作集大于可用内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表现:&lt;/strong&gt; CPU 利用率很低，但磁盘 I/O 非常繁忙。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. 高级内存管理策略&lt;/h2&gt;
&lt;h3&gt;5.1 工作集模型 (Working Set Model)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提出者:&lt;/strong&gt; Denning (1968)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本思想:&lt;/strong&gt; 基于 &lt;strong&gt;程序访问的局部性原理&lt;/strong&gt;。一个进程在任何时刻都倾向于访问一个相对较小的页面集合，称为 &lt;strong&gt;活跃页面 (Active Pages)&lt;/strong&gt;。如果能将这些活跃页面都保留在内存中，就能显著减少缺页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作集 (Working Set) W(t, Δ):&lt;/strong&gt; 在当前时间 &lt;code&gt;t&lt;/code&gt; 之前的 &lt;strong&gt;时间窗口 Δ&lt;/strong&gt; 内，进程实际访问过的 &lt;strong&gt;虚拟页面&lt;/strong&gt; 的集合。
&lt;ul&gt;
&lt;li&gt;Δ: 工作集窗口大小，是一个关键参数。&lt;/li&gt;
&lt;li&gt;工作集大小 &lt;code&gt;|W(t, Δ)|&lt;/code&gt; 随时间动态变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与驻留集的关系:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;驻留集 (Resident Set):&lt;/strong&gt; 当前时刻，进程 &lt;strong&gt;实际&lt;/strong&gt; 驻留在物理内存中的页面集合。由 OS 分配策略和置换算法决定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理想状态:&lt;/strong&gt; 进程的驻留集应包含其当前的工作集 (&lt;code&gt;Resident Set &gt;= Working Set&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作集策略应用:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;置换:&lt;/strong&gt; 换出不在当前工作集中的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载控制:&lt;/strong&gt; 只有当一个进程的工作集能够被完全调入内存时，才激活该进程运行，否则挂起。防止 Thrashing。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 工作集算法 (实现工作集置换)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基本思路:&lt;/strong&gt; 识别并换出不在当前工作集 (W(t, Δ)) 中的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一种实现:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PTE 增强:&lt;/strong&gt; 每个 PTE 增加一个字段，记录该页面的 &lt;strong&gt;最后访问时间 (Last Use Time)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数:&lt;/strong&gt; 设置一个时间阈值 &lt;code&gt;T&lt;/code&gt; (近似 Δ)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扫描过程 (类似时钟):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;定期或缺页时扫描页框。&lt;/li&gt;
&lt;li&gt;检查 PTE 的 A 位：
&lt;ul&gt;
&lt;li&gt;若 A=1: 表示在当前时钟滴答内被访问。记录 &lt;strong&gt;当前虚拟时间&lt;/strong&gt; 到 PTE 的 &quot;最后访问时间&quot; 字段，并将 A 位清零。&lt;/li&gt;
&lt;li&gt;若 A=0: 表示在当前滴答内未被访问。计算 &lt;strong&gt;页面年龄 (Age) = 当前虚拟时间 - 最后访问时间&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;如果 Age &gt; T (页面“老”，不在工作集内)：
&lt;ul&gt;
&lt;li&gt;如果页面是干净的 (D=0)，则该页面是 &lt;strong&gt;最佳&lt;/strong&gt; 牺牲页，直接置换。&lt;/li&gt;
&lt;li&gt;如果页面是脏的 (D=1)，先记录下来，继续扫描，希望能找到一个干净的老页面。如果找不到干净的老页面，最后回来置换这个脏的老页面 (需要写回磁盘) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 Age ≤ T (页面“年轻”，在工作集内)：保留该页面，继续扫描。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;讨论:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;精确实现工作集算法开销较大 (需要记录和比较时间) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺页率算法 (Page Fault Frequency - PFF):&lt;/strong&gt; 一种近似方法。通过监控进程的缺页率来动态调整其驻留集大小。
&lt;ul&gt;
&lt;li&gt;设置缺页率上限和下限。&lt;/li&gt;
&lt;li&gt;缺页率 &gt; 上限: 增加进程的页框数。&lt;/li&gt;
&lt;li&gt;缺页率 &amp;#x3C; 下限: 减少进程的页框数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 清除策略 (Cleaning Policy)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt; 当需要空闲页框时，如果选中的牺牲页是 &quot;脏&quot; 的，需要先写回磁盘，增加了缺页处理时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 保持一定数量的 &lt;strong&gt;干净 (Clean)&lt;/strong&gt; 空闲页框可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 使用 &lt;strong&gt;分页守护进程 (Paging Daemon)&lt;/strong&gt; (如 &lt;code&gt;kswapd&lt;/code&gt; in Linux)。
&lt;ul&gt;
&lt;li&gt;该进程周期性 (或在内存不足时) 被唤醒。&lt;/li&gt;
&lt;li&gt;检查内存状态，如果空闲页框低于某个阈值。&lt;/li&gt;
&lt;li&gt;使用页面置换算法 (如 Clock 或 LRU 近似) 选择一些页面。&lt;/li&gt;
&lt;li&gt;如果选中的页面是脏页，则启动 I/O 将其 &lt;strong&gt;提前写回 (Write Back)&lt;/strong&gt; 磁盘，并将其标记为干净。&lt;/li&gt;
&lt;li&gt;这样，未来需要空闲页框时，可以直接使用这些已变干净的页框，或者快速换出它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双指针时钟 (Two-Handed Clock):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前指针 (Cleaning Hand):&lt;/strong&gt; 由分页守护进程控制。扫描页框，遇到脏页就启动写回，然后将其标记为干净；遇到干净页则跳过。前指针不断“清洁”页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后指针 (Eviction Hand):&lt;/strong&gt; 由缺页处理程序控制。用于实际选择牺牲页。由于前指针的工作，后指针更有可能遇到干净页面，从而加速缺页处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 页缓冲技术 (Page Buffering)&lt;/h3&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;ul&gt;
&lt;li&gt;被置换出的页面 &lt;strong&gt;不立即&lt;/strong&gt; 丢弃或覆盖。&lt;/li&gt;
&lt;li&gt;维护两个链表：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;空闲页链表 (Free Page List):&lt;/strong&gt; 存放被置换出的 &lt;strong&gt;干净&lt;/strong&gt; 页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改页链表 (Modified Page List):&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;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速回收 (Soft Fault):&lt;/strong&gt; 如果进程很快又要访问刚被“置换”到这两个链表中的页面，可以直接将其重新链回进程的驻留集，无需磁盘 I/O。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;簇写回 (Cluster Write):&lt;/strong&gt; 修改页链表中的脏页可以累积起来，&lt;strong&gt;成簇地 (in clusters)&lt;/strong&gt; 写回磁盘，而不是一次只写一页，提高了磁盘 I/O 效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page Cache (如 OSTEP 23章 提及):&lt;/strong&gt; 现代 OS 中广泛使用的技术，用于缓存文件数据和匿名页 (包括上述缓冲的页面) 。
&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;ul&gt;
&lt;li&gt;当进程读取文件时，数据首先从磁盘加载到 page cache，然后再传递给进程。&lt;/li&gt;
&lt;li&gt;当进程写入文件时，数据先写入 page cache，标记为&quot;脏&quot;，稍后由后台进程 (如 pdflush/flush/kswapd) 异步写回磁盘。&lt;/li&gt;
&lt;li&gt;后续对相同文件数据的访问可直接从 page cache 获取，避免磁盘 I/O。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理策略:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;使用类似 LRU 的替换算法 (如 Linux 的 2Q) 决定哪些页面保留在缓存中。&lt;/li&gt;
&lt;li&gt;通过 readahead 机制预读文件数据，提高顺序访问性能。&lt;/li&gt;
&lt;li&gt;支持 write-back (延迟写) 和 write-through (直接写) 两种写入策略。&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;减少磁盘 I/O:&lt;/strong&gt; 大幅降低文件操作的延迟，提高系统整体性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一缓存:&lt;/strong&gt; 在现代系统中，page cache 通常与 buffer cache 统一，形成统一缓存管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存利用:&lt;/strong&gt; 未使用的物理内存自动用于缓存，提高内存利用率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2Q 算法:&lt;/strong&gt; 一种近似 LRU 的页面缓存替换算法，但开销更低且能应对 LRU 不擅长的场景。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基本结构:&lt;/strong&gt; 维护两个队列:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非活跃队列 (A1):&lt;/strong&gt; 存放首次访问的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;活跃队列 (Am):&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;当页面第一次被访问时，放入非活跃队列 A1 的头部。&lt;/li&gt;
&lt;li&gt;当页面在 A1 中再次被访问时，将其从 A1 移除并放入活跃队列 Am 的头部。&lt;/li&gt;
&lt;li&gt;当页面在 Am 中被访问时，将其移到 Am 的头部 (类似 LRU) 。&lt;/li&gt;
&lt;li&gt;需要置换页面时，总是从非活跃队列 A1 的尾部选择牺牲页。&lt;/li&gt;
&lt;li&gt;定期将活跃队列 Am 尾部的页面移回非活跃队列 A1，以保持整个缓存中约 2/3 的页面在活跃队列中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优势:&lt;/strong&gt; 能有效应对扫描型工作负载 (如顺序读取大文件) 导致的频繁页面互换问题，这类场景下传统 LRU 表现不佳。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.5 加载控制 (Load Control)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt; 系统中并发运行的进程过多，总内存需求超过物理内存容量，导致 Thrashing。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 控制系统的 &lt;strong&gt;并发度 (Multiprogramming Level)&lt;/strong&gt;，即同时驻留在内存中 (活跃) 的进程数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程挂起/交换 (Process Suspension/Swapping):&lt;/strong&gt; 当系统负载过高 (如通过高缺页率或低 CPU 利用率检测到 Thrashing) 时，选择一个或多个进程，将其 &lt;strong&gt;所有页面&lt;/strong&gt; 换出到磁盘 (交换区) ，并将其置于挂起状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择标准:&lt;/strong&gt; 选择哪些进程挂起？通常选择低优先级进程、长时间阻塞的进程，或者导致最多缺页的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效果:&lt;/strong&gt; 释放大量内存，降低活跃进程的内存竞争，使剩余进程能够获得足够的工作集空间，恢复系统效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. 内存映射文件 (Memory-Mapped Files)&lt;/h2&gt;
&lt;h3&gt;6.1 基本思想&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;允许进程将一个 &lt;strong&gt;文件&lt;/strong&gt; 或文件的一部分直接 &lt;strong&gt;映射&lt;/strong&gt; 到其 &lt;strong&gt;虚拟地址空间&lt;/strong&gt; 的一个区域。&lt;/li&gt;
&lt;li&gt;映射后，进程可以像访问普通内存 (如数组) 一样 &lt;strong&gt;通过内存读写指令&lt;/strong&gt; 来访问文件内容，而无需使用 &lt;code&gt;read()&lt;/code&gt; / &lt;code&gt;write()&lt;/code&gt; 等系统调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 工作机制&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;系统调用:&lt;/strong&gt; 如 POSIX 的 &lt;code&gt;mmap()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射建立:&lt;/strong&gt; &lt;code&gt;mmap()&lt;/code&gt; 调用并不立即读取文件内容。它只是在进程的虚拟地址空间中建立一个区域 (&lt;code&gt;vm_area_struct&lt;/code&gt; in Linux)，并设置相应的页表项指向 &lt;strong&gt;文件&lt;/strong&gt; 作为后备存储 (Backing Store)。PTE 初始标记为无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按需调页:&lt;/strong&gt; 当进程 &lt;strong&gt;首次访问&lt;/strong&gt; 映射区域中的某个地址时，会触发 &lt;strong&gt;缺页异常&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺页处理:&lt;/strong&gt; OS 识别出这是一个映射文件的缺页，计算出该虚拟地址对应文件中的偏移量，然后从 &lt;strong&gt;磁盘文件&lt;/strong&gt; 读取相应的 &lt;strong&gt;数据块 (页)&lt;/strong&gt; 到一个物理页框，并更新 PTE 使其有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写回:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果映射是 &lt;strong&gt;共享的 (MAP_SHARED)&lt;/strong&gt;，对内存区域的修改 &lt;strong&gt;最终会写回&lt;/strong&gt; 磁盘上的原始文件 (通常在页面换出时、或调用 &lt;code&gt;msync()&lt;/code&gt;、或解除映射 &lt;code&gt;munmap()&lt;/code&gt; 时) 。其他映射同一文件的进程也能看到修改。&lt;/li&gt;
&lt;li&gt;如果映射是 &lt;strong&gt;私有的 (MAP_PRIVATE)&lt;/strong&gt;，使用 &lt;strong&gt;写时复制 (Copy-on-Write)&lt;/strong&gt;。首次写入时，会为该进程创建一个私有的页面副本，后续修改只影响此副本，不影响原始文件或其他进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一些问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核空间共享:&lt;/strong&gt; 在现代操作系统中，内核空间通常在所有进程间共享
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实现方式:&lt;/strong&gt; 每个进程的页表中，映射到内核空间的部分是相同的，指向同一组物理页框&lt;/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;简化内核访问：系统调用时可以直接访问内核数据结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux实现:&lt;/strong&gt; 通过将所有进程的高地址部分映射到相同的内核物理页面实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减轻页表增长压力的方式:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;稀疏地址空间处理:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多级页表 (Multi-level Page Tables):&lt;/strong&gt; 将页表分为多级，只为实际使用的地址区域分配页表项，避免为整个虚拟地址空间分配连续页表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;倒排页表 (Inverted Page Tables):&lt;/strong&gt; 以物理页框为索引建立表项，每个表项记录映射到该物理页的虚拟页信息，页表大小与物理内存成正比而非虚拟地址空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大页模式 (Huge Pages/Large Pages):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解决的问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;减少TLB缺失:&lt;/strong&gt; 使用大页可以增加TLB覆盖范围，单个TLB表项可以映射更大内存区域(如2MB或1GB而非4KB)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少页表层级:&lt;/strong&gt; 减少地址转换时的页表遍历层数，降低内存访问延迟&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少页表大小:&lt;/strong&gt; 相同大小的内存区域需要更少的页表项，节省页表空间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高内存密集型应用性能:&lt;/strong&gt; 数据库、科学计算等应用可显著受益&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内部碎片增加:&lt;/strong&gt; 如果应用只使用大页的一小部分，会造成内存浪费&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存分配挑战:&lt;/strong&gt; 需要连续的物理内存块，在系统运行一段时间后可能难以满足&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面换出复杂化:&lt;/strong&gt; 换出一个大页需要更多I/O操作，可能增加延迟&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;细粒度保护受限:&lt;/strong&gt; 无法为大页内的不同区域设置不同的访问权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理复杂性增加:&lt;/strong&gt; 系统需要同时管理标准页和大页，增加内存管理复杂度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 &lt;code&gt;mmap()&lt;/code&gt; 函数 (POSIX 示例)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start&lt;/code&gt;: 建议的映射起始虚拟地址 (通常设为 NULL，由内核选择)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;length&lt;/code&gt;: 映射的字节数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prot&lt;/code&gt;: 内存保护标志 (指定映射区域的访问权限)。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PROT_READ&lt;/code&gt;: 可读。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROT_WRITE&lt;/code&gt;: 可写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROT_EXEC&lt;/code&gt;: 可执行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROT_NONE&lt;/code&gt;: 不可访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;: 映射类型和选项。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MAP_SHARED&lt;/code&gt;: 共享映射，修改会写回文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAP_PRIVATE&lt;/code&gt;: 私有写时复制映射。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAP_ANONYMOUS&lt;/code&gt; (或 &lt;code&gt;MAP_ANON&lt;/code&gt;): 匿名映射，不关联任何文件，用于分配内存 (类似 &lt;code&gt;malloc&lt;/code&gt;) 。常与 &lt;code&gt;MAP_PRIVATE&lt;/code&gt; 结合用于进程的堆、栈、BSS段。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fd&lt;/code&gt;: 要映射的文件描述符。对于匿名映射，此参数忽略 (通常设为 -1) 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset&lt;/code&gt;: 文件内的映射起始偏移量 (必须是页面大小的倍数) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 &lt;code&gt;mmap&lt;/code&gt; 与 &lt;code&gt;shm&lt;/code&gt; (共享内存) 对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mmap&lt;/code&gt; (基于文件):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通信方式:&lt;/strong&gt; 通过映射 &lt;strong&gt;同一个磁盘文件&lt;/strong&gt; 到不同进程的地址空间实现共享。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性:&lt;/strong&gt; 共享内容与磁盘文件关联，可以是持久的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用场景:&lt;/strong&gt; 共享大文件，IPC，加载动态库，程序加载器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;shm&lt;/code&gt; (System V Shared Memory):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通信方式:&lt;/strong&gt; 使用 &lt;code&gt;shmget()&lt;/code&gt; 创建一个内核管理的 &lt;strong&gt;纯内存&lt;/strong&gt; 共享区域，然后用 &lt;code&gt;shmat()&lt;/code&gt; 将其附加到进程地址空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性:&lt;/strong&gt; 通常是临时的，与进程生命周期或显式删除 (&lt;code&gt;shmctl&lt;/code&gt; with &lt;code&gt;IPC_RMID&lt;/code&gt;) 相关，不直接关联磁盘文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能:&lt;/strong&gt; 可能比基于文件的 &lt;code&gt;mmap&lt;/code&gt; 更快，因为它不涉及文件系统开销 (除非发生交换) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大小限制:&lt;/strong&gt; 受可用物理内存/交换空间限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.5 &lt;code&gt;mmap&lt;/code&gt; 相关思考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mmap&lt;/code&gt; 比物理内存+Swap空间大，是否有问题?&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答:&lt;/strong&gt; &lt;code&gt;mmap&lt;/code&gt; 本身 &lt;strong&gt;可以&lt;/strong&gt; 映射比物理内存+Swap 大得多的文件。&lt;code&gt;mmap&lt;/code&gt; 只是建立了虚拟地址到文件内容的 &lt;strong&gt;潜在&lt;/strong&gt; 映射。只有当进程 &lt;strong&gt;实际访问&lt;/strong&gt; 映射区域的页面时，才需要将其调入物理内存。这里需要区分两种情况：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件映射:&lt;/strong&gt; 对于映射到文件的页面，文件本身就是这些页面的&quot;后备存储&quot;。当内存不足时，如果这些页面没有被修改过(非脏页)，可以直接丢弃，需要时再从文件重新读取；如果被修改过(脏页)，则需要先写回文件再释放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;匿名映射:&lt;/strong&gt; 没有关联文件的映射区域(如堆)，必须使用Swap空间作为后备存储。Swap是专门用于存储从内存中换出的页面的磁盘区域。&lt;/li&gt;
&lt;/ul&gt;
因此，文件映射可以超过物理内存+Swap的总大小，因为它使用原始文件作为后备存储。但如果工作集(实际访问的页面总数)超过了物理内存+Swap，系统会发生&lt;strong&gt;颠簸(Thrashing)&lt;/strong&gt;，性能急剧下降，尽管映射本身是合法的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 &lt;code&gt;mmap&lt;/code&gt; 代替 &lt;code&gt;read/write&lt;/code&gt; 进行文件读写的优势?&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;减少数据拷贝:&lt;/strong&gt; &lt;code&gt;read/write&lt;/code&gt; 通常涉及数据在内核缓冲区和用户缓冲区之间的拷贝。&lt;code&gt;mmap&lt;/code&gt; 允许进程直接访问内核的页缓存 (或直接从磁盘调页) ，&lt;strong&gt;避免了这次拷贝&lt;/strong&gt;，提高效率，尤其对于大文件或频繁读写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化随机访问:&lt;/strong&gt; 对于需要频繁在文件中随机定位读写的场景，&lt;code&gt;mmap&lt;/code&gt; 将文件视为内存数组，可以通过指针运算直接访问任意位置，代码更简洁，无需管理文件指针和复杂的 &lt;code&gt;lseek&lt;/code&gt; 调用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核优化:&lt;/strong&gt; 内核可以更有效地管理 &lt;code&gt;mmap&lt;/code&gt; 区域的页面缓存和预读。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.6 内存映射文件应用示例&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序加载:&lt;/strong&gt; 加载可执行文件和动态链接库 (DLLs/SOs) 时，代码段和只读数据段通常通过私有映射 (&lt;code&gt;MAP_PRIVATE&lt;/code&gt;) 加载，数据段通过写时复制加载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程间通信 (IPC):&lt;/strong&gt; 通过共享映射 (&lt;code&gt;MAP_SHARED&lt;/code&gt;) 同一个文件 (或匿名映射) ，实现高效的数据共享。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库:&lt;/strong&gt; 像 LMDB (Lightning Memory-Mapped Database) 这样的内存映射数据库，将整个数据库文件映射到内存，利用 OS 的虚拟内存管理进行数据缓存和访问，简化了缓冲管理，提高了 I/O 性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. 虚拟内存管理全貌 (结合进程结构)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程控制块 (PCB / &lt;code&gt;task_struct&lt;/code&gt; in Linux):&lt;/strong&gt; 包含指向内存描述符 (&lt;code&gt;mm_struct&lt;/code&gt;) 的指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存描述符 (&lt;code&gt;mm_struct&lt;/code&gt;):&lt;/strong&gt; 描述进程的整个虚拟地址空间，包含指向页表的指针 (如 CR3 指向的页目录物理地址) 和指向虚拟内存区域链表/树 (&lt;code&gt;vm_area_struct&lt;/code&gt; list/tree) 的指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟内存区域 (&lt;code&gt;vm_area_struct&lt;/code&gt; - VMA):&lt;/strong&gt; 描述进程地址空间中一段 &lt;strong&gt;连续&lt;/strong&gt; 的、具有 &lt;strong&gt;相同属性&lt;/strong&gt; (如权限、映射文件) 的虚拟内存区域。例如，代码段、数据段、堆、栈、每个内存映射文件、每个共享库都对应一个或多个 VMA。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关键字段:&lt;/strong&gt; 起始地址 (&lt;code&gt;vm_start&lt;/code&gt;), 结束地址 (&lt;code&gt;vm_end&lt;/code&gt;), 访问权限 (&lt;code&gt;vm_prot&lt;/code&gt;), 标志 (&lt;code&gt;vm_flags&lt;/code&gt;, 如 &lt;code&gt;VM_READ&lt;/code&gt;, &lt;code&gt;VM_WRITE&lt;/code&gt;, &lt;code&gt;VM_EXEC&lt;/code&gt;, &lt;code&gt;VM_SHARED&lt;/code&gt;), 指向映射文件信息的指针 (&lt;code&gt;vm_file&lt;/code&gt;) 等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表 (Page Tables):&lt;/strong&gt; 将 VMA 内的虚拟页号映射到物理页框号或标记为不在内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理内存 (Page Frames):&lt;/strong&gt; 实际存储数据的内存块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后备存储 (Backing Store):&lt;/strong&gt; 磁盘上的文件 (可执行文件、库、数据文件) 或交换空间 (Swap Area)，用于存放不在物理内存中的页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;交互过程:&lt;/strong&gt; 访问虚拟地址 -&gt; 查找 VMA -&gt; 查找页表 (TLB first) -&gt; 访问物理内存 / Page Fault -&gt; (缺页处理) -&gt; 访问后备存储。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. 写时复制 (Copy-on-Write - COW)&lt;/h2&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;ul&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt; 系统调用创建子进程。&lt;/li&gt;
&lt;li&gt;私有内存映射 (&lt;code&gt;MAP_PRIVATE&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机制:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;共享初始副本:&lt;/strong&gt; 当创建副本时 (如 &lt;code&gt;fork()&lt;/code&gt; 创建子进程) ，并不立即复制父进程的物理内存页面。而是让子进程的页表项指向与父进程 &lt;strong&gt;相同&lt;/strong&gt; 的物理页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标记为只读:&lt;/strong&gt; 同时，将这些共享页框在 &lt;strong&gt;父子进程的页表项中都标记为只读 (Read-Only)&lt;/strong&gt;，即使它们原本是可写的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写操作触发异常:&lt;/strong&gt; 如果任何一个进程 (父或子) 尝试 &lt;strong&gt;写入&lt;/strong&gt; 这些共享的页面，会触发一个 &lt;strong&gt;保护性页错误 (Protection Fault)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真正复制:&lt;/strong&gt; 操作系统捕获此异常，识别出是 COW 机制。此时，内核会：
&lt;ul&gt;
&lt;li&gt;分配一个新的物理页框。&lt;/li&gt;
&lt;li&gt;将原始页框的内容 &lt;strong&gt;复制&lt;/strong&gt; 到新页框。&lt;/li&gt;
&lt;li&gt;修改 &lt;strong&gt;触发写入操作的那个进程&lt;/strong&gt; 的页表项，使其指向 &lt;strong&gt;新复制的页框&lt;/strong&gt;，并将该页表项的权限 &lt;strong&gt;恢复为可写 (Read-Write)&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复执行:&lt;/strong&gt; 进程继续执行写操作，现在写入的是它自己的私有副本。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fork()&lt;/code&gt; 效率高:&lt;/strong&gt; 如果子进程立刻调用 &lt;code&gt;exec()&lt;/code&gt; 加载新程序，那么之前的大部分复制就白费了。COW 避免了这种不必要的开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节省内存:&lt;/strong&gt; 只要页面不被修改，父子进程可以一直共享同一物理副本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;9. Windows 虚拟内存管理 (概述)&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;(注: 以下内容可能为对 Windows (可能较早版本如 NT/XP/7) 的描述) , 不重要&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;9.1 Intel x86 虚拟内存机制回顾&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保护模式寻址:&lt;/strong&gt; 使用 &lt;strong&gt;段选择符 (Segment Selector)&lt;/strong&gt; + &lt;strong&gt;偏移量 (Offset)&lt;/strong&gt; 形成逻辑地址。段选择符指向 &lt;strong&gt;段描述符 (Segment Descriptor)&lt;/strong&gt; (在 GDT/LDT 中)，包含段基址、限长、权限等。逻辑地址 -&gt; &lt;strong&gt;线性地址 (Linear Address)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分页机制:&lt;/strong&gt; 如果 CR0 寄存器的 PG 位开启，线性地址会被页式机制进一步转换为 &lt;strong&gt;物理地址 (Physical Address)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绕过分段:&lt;/strong&gt; 可以设置段描述符使段基址为 0，限长为 4GB，从而让线性地址等于逻辑地址的偏移量部分，达到“平坦内存模型”的效果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表结构:&lt;/strong&gt; x86 支持多级页表 (早期 2 级，x86-64 支持 4 级)。CR3 寄存器指向最高级页表的物理基地址。PTE/PDE 结构包含 PFN, P, A, D, R/W, U/S, PCD, PWT 等位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.2 Windows 内存管理器 (Memory Manager)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;位置:&lt;/strong&gt; 位于内核执行体 (&lt;code&gt;Ntoskrnl.exe&lt;/code&gt;) 中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主要组成:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;执行体系统服务:&lt;/strong&gt; 提供 API (&lt;code&gt;VirtualAlloc&lt;/code&gt;, &lt;code&gt;MapViewOfFile&lt;/code&gt;, &lt;code&gt;HeapAlloc&lt;/code&gt; 等) 用于虚存分配、回收和管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面错误陷阱处理程序 (&lt;code&gt;MmAccessFault&lt;/code&gt;):&lt;/strong&gt; 处理 MMU 检测到的内存管理异常 (缺页、权限错误)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后台线程 (关键组件):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工作集管理器 (&lt;code&gt;MmWorkingSetManager&lt;/code&gt;):&lt;/strong&gt; 负责调整进程工作集大小 (修整) 、老化页面、启动脏页写出等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程/栈交换器 (&lt;code&gt;KeSwapProcessOrStack&lt;/code&gt;):&lt;/strong&gt; 负责整个进程或内核线程栈的换入换出 (用于挂起/恢复进程)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改页面写出器 (&lt;code&gt;MiModifiedPageWriter&lt;/code&gt;):&lt;/strong&gt; 将“脏”的匿名页面 (来自页文件) 写回到页文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射页面写出器 (&lt;code&gt;MiMappedPageWriter&lt;/code&gt;):&lt;/strong&gt; 将内存映射文件中的“脏”页写回磁盘文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零页线程 (&lt;code&gt;MmZeroPageThread&lt;/code&gt;):&lt;/strong&gt; 将空闲页框清零，为按需零页 (Demand-Zero) 提供准备好的页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.3 Windows 地址空间布局 (32-bit 示例)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;总览:&lt;/strong&gt; 4GB 虚拟地址空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户空间 (通常 0x00000000 - 0x7FFFFFFF, 2GB):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;应用程序代码 (EXE)&lt;/li&gt;
&lt;li&gt;动态链接库 (DLL) 代码和数据&lt;/li&gt;
&lt;li&gt;进程堆 (Heap)&lt;/li&gt;
&lt;li&gt;线程栈 (Stack)&lt;/li&gt;
&lt;li&gt;进程环境块 (PEB), 线程环境块 (TEB)&lt;/li&gt;
&lt;li&gt;内存映射文件区域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统空间 (通常 0x80000000 - 0xFFFFFFFF, 2GB):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;内核代码 (&lt;code&gt;Ntoskrnl.exe&lt;/code&gt;), HAL (&lt;code&gt;hal.dll&lt;/code&gt;), 内核驱动&lt;/li&gt;
&lt;li&gt;页表本身 (自映射区域)&lt;/li&gt;
&lt;li&gt;系统缓存 (File Cache)&lt;/li&gt;
&lt;li&gt;分页缓冲池 (Paged Pool): 可被换出的内核内存&lt;/li&gt;
&lt;li&gt;非分页缓冲池 (Non-Paged Pool): 不能被换出的内核内存 (用于中断处理等)&lt;/li&gt;
&lt;li&gt;超空间 (Hyperspace): 临时映射进程页表等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/3GB 启动选项:&lt;/strong&gt; 可以修改用户/系统空间划分为 3GB/1GB，让用户进程获得更大地址空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自映射机制:&lt;/strong&gt; Windows 将当前进程的 &lt;strong&gt;页目录&lt;/strong&gt; 和 &lt;strong&gt;所有页表&lt;/strong&gt; 映射到系统空间的一个固定虚拟地址范围 (如 0xC0000000 开始)，使得内核可以通过虚拟地址方便地访问任何进程的 PTE/PDE，而无需切换 CR3 寄存器。
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MiGetPdeAddress(va)&lt;/code&gt; 和 &lt;code&gt;MiGetPteAddress(va)&lt;/code&gt; 宏利用此机制快速计算给定虚拟地址 &lt;code&gt;va&lt;/code&gt; 对应的 PDE 和 PTE 的 &lt;strong&gt;虚拟地址&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.4 Windows 缺页处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;流程:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;CPU 访问无效 PTE 地址，触发 0xE 号中断 (Page Fault)。&lt;/li&gt;
&lt;li&gt;CPU 将出错的虚拟地址存入 CR2 寄存器。&lt;/li&gt;
&lt;li&gt;CPU 跳转到内核的缺页中断处理例程 &lt;code&gt;KiTrap0E&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KiTrap0E&lt;/code&gt; 调用核心处理函数 &lt;code&gt;MmAccessFault&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MmAccessFault&lt;/code&gt; 读取 CR2，找到对应的 PTE。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分析 PTE 内容 (即使 P=0，其他位仍有 OS 定义的含义):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;页面从未被提交 (Committed): 可能是非法访问。&lt;/li&gt;
&lt;li&gt;访问违反权限 (Protection Violation): 如写入只读页。&lt;/li&gt;
&lt;li&gt;写时复制 (Copy-on-Write): 执行 COW 操作。&lt;/li&gt;
&lt;li&gt;栈扩展 (Stack Expansion): 自动分配并映射新的栈页面。&lt;/li&gt;
&lt;li&gt;页面在转换状态 (Transition): 页面正在被 I/O (读入/写出) ，需等待 I/O 完成。&lt;/li&gt;
&lt;li&gt;页面在页文件/映射文件中 (Paged Out): 需要从磁盘调入。&lt;/li&gt;
&lt;li&gt;请求零页面 (Demand Zero): 分配一个已清零的物理页框。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行相应操作:&lt;/strong&gt; 分配页框、启动磁盘 I/O、修改 PTE、完成 COW 等。&lt;/li&gt;
&lt;li&gt;返回用户模式，重新执行指令。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.5 Windows 工作集 (Working Set)&lt;/h3&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;ul&gt;
&lt;li&gt;&lt;strong&gt;进程工作集:&lt;/strong&gt; 每个进程私有的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统工作集:&lt;/strong&gt; 内核自身代码和数据 (可分页部分) 所占用的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态调整:&lt;/strong&gt; 工作集大小是动态变化的，有最小值和最大值限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作集修整 (Trimming):&lt;/strong&gt; 当系统物理内存紧张时，&lt;strong&gt;工作集管理器&lt;/strong&gt; 会减少某些进程的工作集大小 (通常使用 Clock 类似算法移除“最老”的页面) ，将移除的页面放入 Standby 或 Modified 链表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动增长:&lt;/strong&gt; 进程发生缺页时，如果其工作集大小未达到最大值，且系统有空闲内存，则调入页面会使其工作集增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9.6 用户空间内存分配方式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;以页为单位的虚拟内存分配 (&lt;code&gt;VirtualAlloc&lt;/code&gt;, &lt;code&gt;VirtualFree&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两阶段:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保留 (Reserve):&lt;/strong&gt; 在进程虚拟地址空间预留一段范围，不分配物理内存或页文件空间。只是标记地址范围不可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交 (Commit):&lt;/strong&gt; 为保留的地址空间实际分配物理内存 (或页文件支持) 。页面首次访问时才会真正调入物理内存 (Demand Zero 或从页文件加载) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟地址描述符 (VAD - Virtual Address Descriptor):&lt;/strong&gt; 内核为每个进程维护一棵 &lt;strong&gt;自平衡二叉树 (VAD Tree)&lt;/strong&gt;，每个节点描述一段连续的、属性相同的虚拟地址空间 (已保留或已提交) 。用于快速查找、分配、释放虚拟地址范围。&lt;code&gt;EPROCESS&lt;/code&gt; 结构包含指向 VAD 树根的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存映射文件 (&lt;code&gt;CreateFileMapping&lt;/code&gt;, &lt;code&gt;MapViewOfFile&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;strong&gt;区域对象 (Section Object)&lt;/strong&gt; (Win32 API 称之为 File Mapping Object) 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CreateFileMapping&lt;/code&gt; 创建一个区域对象，可以基于磁盘文件或页文件 (用于匿名共享内存) 。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapViewOfFile&lt;/code&gt; 将区域对象的一部分或全部映射到进程的虚拟地址空间，得到一个 &lt;strong&gt;视图 (View)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;通过映射同一区域对象，实现进程间共享内存或文件访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存堆 (Heap) (&lt;code&gt;HeapCreate&lt;/code&gt;, &lt;code&gt;HeapAlloc&lt;/code&gt;, &lt;code&gt;HeapFree&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用途:&lt;/strong&gt; 管理 &lt;strong&gt;大量、小块&lt;/strong&gt; 的内存分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机制:&lt;/strong&gt; 进程首先用 &lt;code&gt;VirtualAlloc&lt;/code&gt; (通常在进程启动时由系统自动完成，创建默认堆) 分配一大块虚拟内存作为堆区域。然后，&lt;strong&gt;堆管理器&lt;/strong&gt; (用户模式库或内核函数集) 在这个区域内进一步细分和管理小块内存的分配与释放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型:&lt;/strong&gt; 每个进程有一个 &lt;strong&gt;默认进程堆&lt;/strong&gt; (&lt;code&gt;GetProcessHeap&lt;/code&gt;)。也可以创建额外的 &lt;strong&gt;私有堆&lt;/strong&gt; (&lt;code&gt;HeapCreate&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;9.7 Windows 物理内存管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页框号数据库 (PFN Database):&lt;/strong&gt; 一个 &lt;strong&gt;全局数组&lt;/strong&gt; (&lt;code&gt;MmPfnDatabase&lt;/code&gt;)，数组的每个元素是一个 &lt;code&gt;MMPFN&lt;/code&gt; 结构，对应一个物理内存页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MMPFN&lt;/code&gt; 结构:&lt;/strong&gt; 包含该物理页框的所有状态信息，如：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;状态 (State):&lt;/strong&gt; Active, Standby, Modified, Transition, Free, Zeroed, Bad。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链接指针:&lt;/strong&gt; 用于将处于相同状态的页框链接起来 (形成链表) 。&lt;/li&gt;
&lt;li&gt;指向 PTE 的指针 (如果页面是 Active/Standby/Modified)。&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;strong&gt;页框状态与链表:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Active/Valid:&lt;/strong&gt; 页面在某个工作集中，PTE有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transition:&lt;/strong&gt; 页面正在进行 I/O。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standby:&lt;/strong&gt; 页面刚被从工作集移除，内容 &lt;strong&gt;干净&lt;/strong&gt;，PTE 无效但指向此 PFN。可被快速重用 (Soft Fault)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modified:&lt;/strong&gt; 页面刚被从工作集移除，内容 &lt;strong&gt;脏&lt;/strong&gt;，PTE 无效但指向此 PFN。重用前需写回磁盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free:&lt;/strong&gt; 页框空闲，内容无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zeroed:&lt;/strong&gt; 页框空闲，且已清零。可立即用于 Demand Zero 页。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bad:&lt;/strong&gt; 页框硬件损坏，不可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链表:&lt;/strong&gt; 内核维护 &lt;code&gt;FreePageList&lt;/code&gt;, &lt;code&gt;ZeroedPageList&lt;/code&gt;, &lt;code&gt;StandbyPageList&lt;/code&gt;, &lt;code&gt;ModifiedPageList&lt;/code&gt; 等链表，通过 &lt;code&gt;MMPFN&lt;/code&gt; 中的指针将对应状态的页框链接起来，方便快速查找和管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态转换图 (示意):&lt;/strong&gt; 页面在不同链表和工作集之间根据缺页、写回、清零、置换等操作进行转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;10. 重点小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心概念:&lt;/strong&gt; 虚拟内存是对物理内存的抽象，提供更大、受保护的地址空间。通过页表和 MMU 实现虚拟到物理地址的转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键机制:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分页 (Paging):&lt;/strong&gt; 将地址空间划分为固定大小的页/页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表 (Page Table):&lt;/strong&gt; 存储 VPN 到 PFN 的映射及状态位 (P, A, D, Protection)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级页表/反转页表:&lt;/strong&gt; 解决页表过大问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB (快表):&lt;/strong&gt; 加速地址转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺页异常 (Page Fault):&lt;/strong&gt; 处理页面不在内存的情况，实现按需调页。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重要策略:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;置换策略 (Replacement):&lt;/strong&gt; FIFO, LRU, Clock, Working Set 等，决定牺牲哪个页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;驻留集管理:&lt;/strong&gt; 控制进程占用多少物理内存 (Fixed vs. Variable, Working Set)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清除策略 (Cleaning):&lt;/strong&gt; 提前写回脏页，保持空闲页框干净。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载控制:&lt;/strong&gt; 防止 Thrashing。&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;内存映射文件 (mmap):&lt;/strong&gt; 高效文件 I/O 和 IPC 机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写时复制 (COW):&lt;/strong&gt; 优化 &lt;code&gt;fork()&lt;/code&gt; 和私有映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows 特点:&lt;/strong&gt; VAD 树管理虚拟地址空间，PFN 数据库管理物理页框，多种页面状态链表，工作集模型，多种内存分配 API。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 05: Memory Management Overview</title><link>https://www.lyt0112.com/blog/operating_systems_note_05-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_05-zh</guid><description>Operating Systems Notes 05: 内存管理概述</description><pubDate>Fri, 28 Mar 2025 16:14:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-03-25&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 重要概念&lt;/h2&gt;
&lt;h3&gt;1.1 存储体系 (Memory Hierarchy)&lt;/h3&gt;
&lt;p&gt;计算机存储器按照速度、容量和成本排列成层次结构。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;寄存器 (Registers):&lt;/strong&gt; 最快，容量最小，成本最高，直接由 CPU 访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高速缓存 (Cache - L1, L2, L3):&lt;/strong&gt; 速度快，容量较小，成本高，用于缓解 CPU 和主存之间的速度差异。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存 (Main Memory / RAM):&lt;/strong&gt; 速度、容量和成本居中，是程序运行时代码和数据的主要存储区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地磁盘 (Local Disk / Secondary Storage):&lt;/strong&gt; 速度慢，容量大，成本低，用于持久化存储，如硬盘 (HDD)、固态硬盘 (SSD)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;远程磁盘 (Remote Storage):&lt;/strong&gt; 通过网络访问，速度最慢，容量可以很大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;内存管理主要关注的是主存 (内存) 的管理。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;1.2 地址空间 (Address Space)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 操作系统为每个进程分配的一个独立的逻辑地址范围。进程所能“看到”和访问的地址集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立性:&lt;/strong&gt; 每个进程拥有自己独立的地址空间，一个进程默认不能访问另一个进程的地址空间，这是实现&lt;strong&gt;存储保护&lt;/strong&gt;的基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理:&lt;/strong&gt; 操作系统需要管理地址空间的分配 (放置 placement) 、回收、分割与合并。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 逻辑地址 vs 物理地址&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑地址 (Logical Address):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;也称为&lt;strong&gt;相对地址 (Relative Address)&lt;/strong&gt; 或 &lt;strong&gt;虚拟地址 (Virtual Address)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;是用户程序 (或 CPU 发出) 使用的地址。&lt;/li&gt;
&lt;li&gt;通常从 0 开始编址，地址是相对于进程自身的地址空间。&lt;/li&gt;
&lt;li&gt;编译、汇编后生成的目标代码通常使用逻辑地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能&lt;/strong&gt;直接用于在物理内存中寻址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理地址 (Physical Address):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;也称为&lt;strong&gt;绝对地址 (Absolute Address)&lt;/strong&gt; 或 &lt;strong&gt;实地址 (Real Address)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;是内存存储单元的实际地址，硬件内存总线可以直接访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4 地址重定位 (Address Relocation)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 将用户程序中的&lt;strong&gt;逻辑地址&lt;/strong&gt;转换为运行时可由机器直接寻址的&lt;strong&gt;物理地址&lt;/strong&gt;的过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别名:&lt;/strong&gt; 也常被称为&lt;strong&gt;地址转换 (Address Translation)&lt;/strong&gt;、&lt;strong&gt;地址变换 (Address Transformation)&lt;/strong&gt; 或 &lt;strong&gt;地址映射 (Address Mapping)&lt;/strong&gt;。这些术语基本同义，都指代从逻辑地址到物理地址的转换过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 保证 CPU 执行指令时能够正确访问到物理内存单元。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么需要?&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;多道程序环境下，内存中有多个进程。&lt;/li&gt;
&lt;li&gt;程序加载到内存的位置通常在运行时才能确定，无法在编译时预知其物理地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址绑定时机 (Binding Time):&lt;/strong&gt; 指令和数据绑定到内存地址的时间点：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编译时 (Compile Time):&lt;/strong&gt; 如果编译时就知道程序将驻留在内存的哪个位置，编译器可以直接生成绝对代码。但如果加载位置改变，程序就必须重新编译。很少用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载时 (Load Time):&lt;/strong&gt; 如果编译时不知道加载位置，编译器生成&lt;strong&gt;可重定位代码 (Relocatable Code)&lt;/strong&gt;。加载器 (Loader) 在将程序加载到内存时，根据实际加载的起始地址，一次性地将所有逻辑地址转换为物理地址。这称为&lt;strong&gt;静态地址重定位&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时 (Run Time):&lt;/strong&gt; 地址转换延迟到程序运行时才进行。CPU 每次访问内存 (取指或访存) 时，都会将逻辑地址转换为物理地址。这需要硬件支持 (如 MMU) ，称为&lt;strong&gt;动态地址重定位&lt;/strong&gt;。现代操作系统普遍采用此方式，因为它提供了最大的灵活性 (如进程可以在内存中移动) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;静态地址重定位 (Static Relocation)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过程:&lt;/strong&gt; 在程序被加载进内存时，由加载器一次性完成逻辑地址到物理地址的转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 通常由软件 (加载器) 完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例:&lt;/strong&gt;
&lt;pre&gt;&lt;code&gt;// 源代码         // 目标代码 (逻辑地址)        // 装载模块 (磁盘, 逻辑)   // 装载模块 (内存, 物理 @ 1000)
i = ...;        store 20;                 store 120;               store 1120;
f();            branch f;                 branch 100;              branch 1100; // 假设 f 在 100
...             ...                       ...                      ...
f: ...          f: ...                    f: ... (在 100)          f: ... (在 1100)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 程序加载后不能在内存中移动；不灵活。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;动态地址重定位 (Dynamic Relocation)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过程:&lt;/strong&gt; 在进程执行过程中，每次访问内存地址时进行转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 需要硬件支持，通常是 &lt;strong&gt;内存管理单元 (MMU - Memory Management Unit)&lt;/strong&gt;。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MMU:&lt;/strong&gt; 一个硬件设备，负责将 CPU 发出的逻辑地址实时转换为物理地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现方式 (简单示例):&lt;/strong&gt; 使用&lt;strong&gt;基址寄存器 (Base Register)&lt;/strong&gt; 和&lt;strong&gt;重定位寄存器 (Relocation Register)&lt;/strong&gt;。逻辑地址加上重定位寄存器的值得到物理地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 进程可以在内存中移动 (例如，为了内存紧凑) ；支持更高级的内存管理技术 (如虚拟内存) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.5 存储保护 (Memory Protection)&lt;/h3&gt;
&lt;ul&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;防止进程执行不适当的操作 (如写入只读代码段) 。&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;基地址寄存器 (Base Register):&lt;/strong&gt; 存放进程在物理内存中的起始地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;界限寄存器 (Limit Register):&lt;/strong&gt; 存放进程的逻辑地址空间的大小 (或最大合法逻辑地址) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查过程:&lt;/strong&gt; CPU 产生的每个逻辑地址 &lt;code&gt;addr&lt;/code&gt; 必须满足 &lt;code&gt;0 &amp;#x3C;= addr &amp;#x3C; Limit Register&lt;/code&gt;。转换后的物理地址 &lt;code&gt;Base Register + addr&lt;/code&gt; 也必须在分配给该进程的物理内存范围内 (有时 limit check 可以直接在物理地址上做，&lt;code&gt;Base &amp;#x3C;= Physical Address &amp;#x3C; Base + Limit&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载:&lt;/strong&gt; 基址和界限寄存器的值由操作系统通过&lt;strong&gt;特权指令 (Privileged Instructions)&lt;/strong&gt; 加载，用户程序无法修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他机制:&lt;/strong&gt; 页表/段表中的保护位 (读/写/执行权限) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.6 存储共享 (Memory Sharing)&lt;/h3&gt;
&lt;p&gt;允许多个进程安全地共享同一段物理内存区域。例如，共享库 (如 C 库) 的代码段通常可以在多个进程间共享，只需在物理内存中保留一份副本，节省内存。实现通常依赖于分页或分段机制。&lt;/p&gt;
&lt;h3&gt;1.7 局部性原理 (Principle of Locality)&lt;/h3&gt;
&lt;p&gt;程序在执行过程中的一个普遍倾向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间局部性 (Temporal Locality):&lt;/strong&gt; 如果一个内存位置被访问，那么它在不久的将来很可能再次被访问 (如循环中的指令、变量) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间局部性 (Spatial Locality):&lt;/strong&gt; 如果一个内存位置被访问，那么它附近的内存位置也很可能在不久的将来被访问 (如顺序执行的代码、数组元素) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;意义:&lt;/strong&gt; 存储体系 (特别是 Cache) 和虚拟内存管理 (如页面置换算法) 都依赖局部性原理来提高性能。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 内存管理的目标与功能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 有效、安全地管理计算机主存资源，提升系统性能和易用性。主要追求：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;透明性 (Transparency):&lt;/strong&gt; 内存管理对用户程序应该是透明的，程序员不需要关心物理内存的细节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;效率 (Efficiency):&lt;/strong&gt; 最小化内存访问时间，高效利用内存资源 (减少浪费) ，降低管理开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护 (Protection):&lt;/strong&gt; 确保进程间地址空间隔离，保护操作系统自身。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基本功能:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存分配与回收:&lt;/strong&gt; 为进程按需分配内存空间，并在进程结束或不再需要时回收。管理空闲内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址映射/转换:&lt;/strong&gt; 实现逻辑地址到物理地址的转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存保护:&lt;/strong&gt; 提供机制防止非法内存访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存共享:&lt;/strong&gt; 支持多个进程共享部分内存区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存扩充:&lt;/strong&gt; 通过覆盖、交换、虚拟内存等技术，在有限的物理内存上运行更大的程序或更多的进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;多道程序设计对内存管理提出的挑战:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多个进程同时在内存中，需要管理它们各自的空间。&lt;/li&gt;
&lt;li&gt;需要支持地址重定位 (程序加载位置不确定) 。&lt;/li&gt;
&lt;li&gt;需要支持地址保护 (进程间隔离) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 进程地址空间&lt;/h2&gt;
&lt;h3&gt;3.1 地址空间布局 (典型 Linux 布局)&lt;/h3&gt;
&lt;p&gt;进程的逻辑地址空间通常划分为几个标准段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    高地址 0xFFFFFFFF  +-----------------------+
                      | 内核地址空间 (Kernel)    | --&gt; 供操作系统使用，用户模式不可访问
          0xC0000000  +-----------------------+
                      | 用户栈 (Stack)          | --&gt; 函数调用、局部变量 (向下增长)         |
                      | ----------------------- |
                      |                         |
                      | 内存映射区域            | --&gt; 共享库、内存映射文件                  |
                      | (Memory Mapped Region)  |
                      |                         |
                      | ----------------------- |
                      | 堆 (Heap)               | --&gt; 动态内存分配 (malloc, new) (向上增长) |
                      | ----------------------- |
                      | BSS 段 (Uninit. Data)   | --&gt; 未初始化全局/静态变量                 |
                      | ----------------------- |
                      | 数据段 (Data Segment)   | --&gt; 已初始化全局/静态变量                 |
                      | 0x08048000              | -----------------------                   |
                      | 代码段 (Text Segment)   | --&gt; 程序指令 (只读)                       |
    低地址  0x00000000 +-----------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加载来源:&lt;/strong&gt; 代码段、数据段、BSS 段通常从可执行文件中加载。栈和堆是在运行时动态创建和增长的。共享库在运行时动态链接和映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PC (Program Counter):&lt;/strong&gt; 指向当前执行的指令 (在代码段内) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SP (Stack Pointer):&lt;/strong&gt; 指向栈顶。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 XV6 示例 (地址空间)&lt;/h3&gt;
&lt;p&gt;XV6 是一个教学用的简单操作系统，其地址空间布局相对简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址从低到高依次为：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码 (text):&lt;/strong&gt; 程序指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据 (data) &amp;#x26; BSS:&lt;/strong&gt; 初始化和未初始化的全局/静态变量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈 (stack):&lt;/strong&gt; 函数调用栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆 (heap):&lt;/strong&gt; 动态分配区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;高地址处有 &lt;strong&gt;Trampoline&lt;/strong&gt; 和 &lt;strong&gt;Trapframe&lt;/strong&gt; 区域，用于用户态和内核态之间的切换，并且在两种模式下都映射。(&lt;code&gt;kernel/memlayout.h&lt;/code&gt; 中定义)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 相关概念解释 (常用于虚拟内存)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;活跃页面 (Active Page):&lt;/strong&gt; 指当前正在被进程频繁访问的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作集 (Working Set):&lt;/strong&gt; 一个进程在最近一段时间 &lt;code&gt;Δ&lt;/code&gt; 内所访问到的页面集合。这是衡量进程当前运行所需内存大小的一个动态指标，反映了程序的局部性。如果一个进程的工作集能完全驻留在内存中，那么它就能高效运行，很少发生缺页中断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常驻集 (Resident Set):&lt;/strong&gt; 指一个进程当前时刻实际驻留在物理内存中的页面集合。常驻集的大小受操作系统分配策略的影响。理想情况下，常驻集应该包含进程的工作集。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 物理内存管理 (空闲空间管理)&lt;/h2&gt;
&lt;p&gt;操作系统需要跟踪哪些物理内存是空闲的，哪些已被分配。&lt;/p&gt;
&lt;h3&gt;4.1 管理数据结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;位图 (Bitmap / Bit Vector):&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;/li&gt;
&lt;li&gt;位的值表示单元状态：&lt;code&gt;0&lt;/code&gt; 表示空闲，&lt;code&gt;1&lt;/code&gt; 表示已分配 (或相反) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 简单，易于快速找到连续的空闲块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 位图本身需要占用内存空间 (空间开销与内存总量成正比) 。查找指定大小的空闲块可能需要扫描整个位图。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;空闲区表/链表 (Free List):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;维护一个包含所有空闲内存块 (区) 信息的数据结构。&lt;/li&gt;
&lt;li&gt;每个节点/表项记录一个空闲区的&lt;strong&gt;起始地址 (Start Address)&lt;/strong&gt; 和 &lt;strong&gt;长度 (Length)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;可以组织成&lt;strong&gt;表 (Array)&lt;/strong&gt; 或&lt;strong&gt;链表 (Linked List)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;链表类型:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐式空闲链表 (Implicit Free List):&lt;/strong&gt; 空闲块和已分配块都存储在内存中，通过块头部信息 (大小、是否空闲) 来遍历。分配和回收时需要查找。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式空闲链表 (Explicit Free List):&lt;/strong&gt; 只将&lt;strong&gt;空闲块&lt;/strong&gt;链接起来。头部包含指向下一个 (有时还有上一个) 空闲块的指针。查找空闲块更快，但维护链表指针有开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离空闲链表 (Segregated Free List):&lt;/strong&gt; 维护多个空闲链表，每个链表负责特定大小范围的空闲块。分配时，根据请求大小直接去对应链表查找。可以加快分配速度，减少碎片。伙伴系统和 SLAB 分配器属于此类。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 空间开销只与空闲块数量有关，不一定与内存总量成正比 (对于大块空闲区更有效) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 分配和回收时可能需要遍历链表/表，查找合适空闲块。可能会产生外部碎片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 衡量指标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内存资源利用率:&lt;/strong&gt; 目标是减少浪费。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内碎片 (Internal Fragmentation):&lt;/strong&gt; 分配给进程的内存块&lt;strong&gt;大于&lt;/strong&gt;进程实际请求的大小，块&lt;strong&gt;内部&lt;/strong&gt;未被使用的部分。常见于固定分区、页式管理和固定大小分配策略 (如伙伴系统分配的块可能大于请求) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外碎片 (External Fragmentation):&lt;/strong&gt; 内存中存在&lt;strong&gt;足够多&lt;/strong&gt;的空闲空间总和来满足一个请求，但这些空间&lt;strong&gt;不连续&lt;/strong&gt;，而是散布在已分配块之间的小块。导致无法分配较大的连续内存块。常见于可变分区、段式管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能:&lt;/strong&gt; 分配和回收内存操作的速度，以及这些操作占用的 CPU 时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 内存分配算法 (针对空闲区表/链表)&lt;/h3&gt;
&lt;p&gt;当需要为进程分配长度为 &lt;code&gt;s&lt;/code&gt; 的内存时，如何在空闲区列表中选择一个合适的空闲块？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;首次适配 (First Fit):&lt;/strong&gt; 从链表/表的开头开始查找，选择&lt;strong&gt;第一个&lt;/strong&gt;找到的大小 &lt;code&gt;&gt;= s&lt;/code&gt; 的空闲块。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 算法简单，速度较快。倾向于在低地址区域留下碎片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 可能产生较多的小碎片，且每次查找都从头开始。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下次适配 (Next Fit):&lt;/strong&gt; 从&lt;strong&gt;上次&lt;/strong&gt;分配操作结束的位置开始查找，选择第一个找到的大小 &lt;code&gt;&gt;= s&lt;/code&gt; 的空闲块。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 避免了每次都从头查找，空闲块的使用更均匀分布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 可能导致大的空闲块很快被分割完。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最佳适配 (Best Fit):&lt;/strong&gt; 遍历&lt;strong&gt;整个&lt;/strong&gt;空闲链表/表，找到大小 &lt;code&gt;&gt;= s&lt;/code&gt; 且&lt;strong&gt;最小&lt;/strong&gt;的那个空闲块 (即差值 &lt;code&gt;size - s&lt;/code&gt; 最小) 。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 试图留下最大的可用空闲块，减少大碎片的产生。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 速度最慢 (需要全表扫描) ，容易产生大量非常小而难以利用的碎片。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最差适配 (Worst Fit):&lt;/strong&gt; 遍历&lt;strong&gt;整个&lt;/strong&gt;空闲链表/表，找到大小 &lt;code&gt;&gt;= s&lt;/code&gt; 且&lt;strong&gt;最大&lt;/strong&gt;的那个空闲块。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 试图将剩余部分保持为较大的可用块，避免产生过多小碎片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 速度慢 (需要全表扫描) ，可能导致大的空闲块很快被消耗掉，无法满足后续对大内存的需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;分配过程:&lt;/strong&gt; 选定一个空闲块后，如果其大小 &lt;code&gt;Size&lt;/code&gt; 远大于请求大小 &lt;code&gt;s&lt;/code&gt;，通常会将其&lt;strong&gt;分割&lt;/strong&gt;为两部分：一部分 (大小为 &lt;code&gt;s&lt;/code&gt;) 分配给进程，另一部分 (大小为 &lt;code&gt;Size - s&lt;/code&gt;) 变回一个新的、更小的空闲块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例:&lt;/strong&gt;
假设空闲区为 [15K, 23K], [48K, 20K], [80K, 30K]。进程 P5 请求 5K，进程 P6 请求 13K。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First Fit:&lt;/strong&gt; P5 分配到 [15K, 5K]，剩余 [20K, 18K]。P6 分配到 [20K, 13K]，剩余 [33K, 5K]。空闲区变为 [33K, 5K], [48K, 20K], [80K, 30K]。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best Fit:&lt;/strong&gt; P5 分配到 [48K, 5K]，剩余 [53K, 15K]。P6 分配到 [53K, 13K]，剩余 [66K, 2K]。空闲区变为 [15K, 23K], [66K, 2K], [80K, 30K]。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worst Fit:&lt;/strong&gt; P5 分配到 [80K, 5K]，剩余 [85K, 25K]。P6 分配到 [85K, 13K]，剩余 [98K, 12K]。空闲区变为 [15K, 23K], [48K, 20K], [98K, 12K]。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 内存回收 (针对空闲区表/链表)&lt;/h3&gt;
&lt;p&gt;当一个进程释放内存块时，需要将其归还给空闲列表。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;合并 (Coalescing):&lt;/strong&gt; 为了减少碎片，回收时需要检查该块是否与物理上相邻的空闲块接壤。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上相邻:&lt;/strong&gt; 与前面的空闲块合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下相邻:&lt;/strong&gt; 与后面的空闲块合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下都相邻:&lt;/strong&gt; 与前后两个空闲块合并成一个大空闲块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下都不相邻:&lt;/strong&gt; 直接将回收块作为一个新的独立空闲块添加到链表/表中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新数据结构:&lt;/strong&gt; 相应地修改空闲区表/链表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.5 特定分配策略&lt;/h3&gt;
&lt;h4&gt;伙伴系统 (Buddy System)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 一种&lt;strong&gt;特殊的“分离适配”算法&lt;/strong&gt;，用于管理大小为 2 的幂次的内存块。Linux 内核底层内存管理曾采用 (现在也部分采用) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结构:&lt;/strong&gt; 将整个可用内存 (假设大小为 2U) 看作一个块。维护 &lt;code&gt;U+1&lt;/code&gt; 个空闲链表，分别管理大小为 20, 21, ..., 2U 的空闲块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配过程 (请求大小为 &lt;code&gt;s&lt;/code&gt;):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;计算满足需求的最小 2 的幂次 &lt;code&gt;k&lt;/code&gt;，使得 2k-1 &amp;#x3C; &lt;code&gt;s&lt;/code&gt; ≤ 2k。&lt;/li&gt;
&lt;li&gt;查找大小为 2k 的空闲链表。&lt;/li&gt;
&lt;li&gt;如果找到，分配该块。&lt;/li&gt;
&lt;li&gt;如果没找到，则查找更大的块 (2k+1)。找到后，将其&lt;strong&gt;分裂 (Split)&lt;/strong&gt; 成两个大小相等的&lt;strong&gt;伙伴 (Buddies)&lt;/strong&gt; (均为 2k)。一个用于分配，另一个放入 2k 的空闲链表。&lt;/li&gt;
&lt;li&gt;如果 2k+1 也没有，继续向上查找并递归分裂，直到找到可分配的块。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回收过程 (释放大小为 2k 的块 &lt;code&gt;B&lt;/code&gt;):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;查找块 &lt;code&gt;B&lt;/code&gt; 的&lt;strong&gt;伙伴&lt;/strong&gt; &lt;code&gt;B&apos;&lt;/code&gt; (地址可以通过异或运算计算得到)。&lt;/li&gt;
&lt;li&gt;检查伙伴 &lt;code&gt;B&apos;&lt;/code&gt; 是否也空闲且大小相同 (2k)。&lt;/li&gt;
&lt;li&gt;如果&lt;strong&gt;是&lt;/strong&gt;，则将 &lt;code&gt;B&lt;/code&gt; 和 &lt;code&gt;B&apos;&lt;/code&gt; &lt;strong&gt;合并 (Merge)&lt;/strong&gt; 成一个更大的块 (2k+1)，并递归尝试与新块的伙伴合并。&lt;/li&gt;
&lt;li&gt;如果&lt;strong&gt;否&lt;/strong&gt;，则将 &lt;code&gt;B&lt;/code&gt; 加入 2k 的空闲链表。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 分裂和合并相对高效 (伙伴地址计算快) ，能较好地控制外部碎片 (合并机制) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 存在&lt;strong&gt;内碎片&lt;/strong&gt; (分配的块大小必须是 2 的幂，可能大于实际需求) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;伙伴系统示例 (1MB 内存):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始:&lt;/strong&gt; Free list for 1M: [Block 1M]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A 申请 100K:&lt;/strong&gt; 需要 128K (27)。分裂 1M -&gt; 512K + 512K; 分裂 512K -&gt; 256K + 256K; 分裂 256K -&gt; 128K + 128K。分配一个 128K 给 A。
&lt;ul&gt;
&lt;li&gt;Memory: [A=128K][Free 128K][Free 256K][Free 512K]&lt;/li&gt;
&lt;li&gt;Free lists: 128K:[1 block], 256K:[1 block], 512K:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B 申请 240K:&lt;/strong&gt; 需要 256K (28)。分配链表中的 256K 给 B。
&lt;ul&gt;
&lt;li&gt;Memory: [A=128K][Free 128K][B=256K][Free 512K]&lt;/li&gt;
&lt;li&gt;Free lists: 128K:[1 block], 512K:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C 申请 64K:&lt;/strong&gt; 需要 64K (26)。分裂 128K -&gt; 64K + 64K。分配一个 64K 给 C。
&lt;ul&gt;
&lt;li&gt;Memory: [A=128K][C=64K][Free 64K][B=256K][Free 512K]&lt;/li&gt;
&lt;li&gt;Free lists: 64K:[1 block], 512K:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D 申请 256K:&lt;/strong&gt; 需要 256K (28)。分裂 512K -&gt; 256K + 256K。分配一个 256K 给 D。
&lt;ul&gt;
&lt;li&gt;Memory: [A=128K][C=64K][Free 64K][B=256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 64K:[1 block], 256K:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放 B (256K):&lt;/strong&gt; 它的伙伴 D=256K 未释放。将 B 加入 256K 链表。
&lt;ul&gt;
&lt;li&gt;Memory: [A=128K][C=64K][Free 64K][Free 256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 64K:[1 block], 256K:[2 blocks]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放 A (128K):&lt;/strong&gt; 它的伙伴 C=64K+Free 64K 中的 Free 64K 是否是伙伴？需要看具体地址。假设 A 的伙伴是 [C=64K][Free 64K] 中的 Free 64K 之前的那个 128K 块，则 A 不能立即合并。将 A 加入 128K 链表。 (&lt;em&gt;原始图示似乎假设 A 的伙伴是 Free 128K，那释放 A 时应该合并成 256K&lt;/em&gt;。我们按原始图示逻辑继续)
&lt;ul&gt;
&lt;li&gt;按图示：释放 A(128K) -&gt; 与其伙伴(Free 128K)合并 -&gt; 256K。&lt;/li&gt;
&lt;li&gt;Memory: [Free 256K][C=64K][Free 64K][B=256K][D=256K][Free 256K] (B 已经释放) -&gt; [Free 256K][C=64K][Free 64K][Free 256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 64K:[1 block], 256K:[3 blocks]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E 申请 75K:&lt;/strong&gt; 需要 128K。从 Free 256K 中分裂 -&gt; 128K+128K。分配一个 128K 给 E。
&lt;ul&gt;
&lt;li&gt;Memory: [E=128K][Free 128K][C=64K][Free 64K][Free 256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 64K:[1 block], 128K:[1 block], 256K:[2 blocks]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放 C (64K):&lt;/strong&gt; 伙伴是 Free 64K。合并 -&gt; 128K。这个 128K 的伙伴是 E=128K (未释放)。将这个新 128K 加入链表。
&lt;ul&gt;
&lt;li&gt;Memory: [E=128K][Free 128K][Free 128K][Free 256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 128K:[2 blocks], 256K:[2 blocks]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放 E (128K):&lt;/strong&gt; 伙伴是 Free 128K (来自 C 的合并)。合并 -&gt; 256K。这个 256K 的伙伴是另一个 Free 128K  (原始 A 分裂剩下的) ？地址决定。按图示，它与另一个 128K 合并成 256K，再与相邻的 Free 256K 合并成 512K。
&lt;ul&gt;
&lt;li&gt;Memory: [Free 512K][Free 256K][D=256K][Free 256K]&lt;/li&gt;
&lt;li&gt;Free lists: 256K:[2 blocks], 512K:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;释放 D (256K):&lt;/strong&gt; 伙伴是 Free 256K。合并 -&gt; 512K。这个 512K 的伙伴是 Free 512K。合并 -&gt; 1M。
&lt;ul&gt;
&lt;li&gt;Memory: [Free 1M]&lt;/li&gt;
&lt;li&gt;Free lists: 1M:[1 block]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;SLAB/SLUB/SLOB 分配器&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;目的:&lt;/strong&gt; 高效管理内核中&lt;strong&gt;频繁分配和释放的小内存对象&lt;/strong&gt; (如 inode、task_struct 等) 。伙伴系统分配的最小块可能仍太大，导致内碎片。&lt;/p&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;将伙伴系统分配的大块内存 (称为 &quot;slab&quot;) 进一步细分成多个&lt;strong&gt;固定大小的小对象 (object)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;为&lt;strong&gt;每种类型&lt;/strong&gt;的对象维护一个或多个 slab 缓存 (cache)。&lt;/li&gt;
&lt;li&gt;分配对象时，从对应的 cache 中快速获取一个空闲对象。&lt;/li&gt;
&lt;li&gt;释放对象时，将其放回原 cache，通常无需立即归还给伙伴系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优点:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少内碎片 (对象大小精确匹配) 。&lt;/li&gt;
&lt;li&gt;分配和释放速度快 (对象通常已初始化，且无需查找) 。&lt;/li&gt;
&lt;li&gt;利用缓存局部性 (对象在 L1/L2 cache 中可能仍然有效) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SLAB 分配器 (原始, Jeff Bonwick, Solaris -&gt; Linux):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为每种对象类型维护一个 &lt;code&gt;kmem_cache&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;每个 cache 包含多个 slab (通常是 1 或多个物理页)。&lt;/li&gt;
&lt;li&gt;Slab 内包含对象和元数据。&lt;/li&gt;
&lt;li&gt;Slab 分为：全满 (full)、部分空闲 (partial)、全空 (empty) 三种链表。分配优先从 partial slab 获取。&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;SLUB 分配器 (改进, Pekka Enberg, Linux 2.6.22+):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 简化设计，提高性能和可伸缩性。是当前 Linux &lt;strong&gt;默认&lt;/strong&gt;的分配器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化:&lt;/strong&gt; 去除了复杂的 slab 链表管理，主要将 page (物理页) 作为 slab 进行管理。元数据存储开销更小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能:&lt;/strong&gt; 减少锁竞争，对 NUMA (Non-Uniform Memory Access) 架构和多核系统优化更好 (利用 per-CPU 缓存) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SLOB 分配器 (简单):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 极简、紧凑，适用于代码大小和内存开销受限的&lt;strong&gt;嵌入式系统&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 使用简单的首次适配算法在小的内存块 (slab) 内分配。不适合高性能、大内存系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看 Slab 信息 (Linux):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /proc/slabinfo
# 或者使用 slabtop 工具
slabtop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;slabinfo&lt;/code&gt; 提供详细的 cache 列表，包括对象大小、活动对象数、总对象数、slab 数等信息。)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. 基本内存管理方案&lt;/h2&gt;
&lt;p&gt;不同的策略将进程的逻辑地址空间映射到物理内存。&lt;/p&gt;
&lt;p&gt;| 方案                    | 加载单位 | 内存划分           | 碎片类型                  | 地址转换                   | 特点                                           |
| ----------------------- | -------- | ------------------ | ------------------------- | -------------------------- | ---------------------------------------------- |
| &lt;strong&gt;单一连续区&lt;/strong&gt;          | 进程     | 不划分 (除 OS 外)  | 无 (低利用率)             | 简单基址 (或固定地址)      | 最简单，同一时间只有一个用户进程，内存利用率低 |
| &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;页式 (Paging)&lt;/strong&gt;       | 页       | 固定大小的页框     | &lt;strong&gt;内碎片&lt;/strong&gt; (最后一页)     | 页表 (MMU硬件查表)         | 消除外碎片，分配管理简单，不要求连续，&lt;strong&gt;常用&lt;/strong&gt; |
| &lt;strong&gt;段式 (Segmentation)&lt;/strong&gt; | 段       | 动态按需划分的段   | &lt;strong&gt;外碎片&lt;/strong&gt;                | 段表 (MMU硬件查表)         | 符合程序逻辑，易于共享和保护，但产生外碎片     |
| &lt;strong&gt;段页式&lt;/strong&gt;              | 页       | 固定大小的页框     | &lt;strong&gt;内碎片&lt;/strong&gt; (段内最后一页) | 段表 + 页表 (MMU硬件查表)  | 结合二者优点，管理复杂，开销大                 |&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5.1 单一连续区 (Single Contiguous Allocation)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 内存除操作系统区域外，全部由当前运行的&lt;strong&gt;一个&lt;/strong&gt;用户程序独占。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 程序总是加载到同一个内存地址 (或通过一个简单的基址寄存器) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 非常简单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 内存利用率极低，无法支持多道程序设计。适用于非常早期的或简单的嵌入式系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 固定分区 (Fixed Partitioning)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 内存被预先划分成若干个&lt;strong&gt;大小固定&lt;/strong&gt;的分区。分区大小可以相同也可以不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配:&lt;/strong&gt; 每个分区装入一个进程。当进程需要内存时，操作系统寻找一个足够大且空闲的分区分配给它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现简单，支持了多道程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内碎片:&lt;/strong&gt; 分配给进程的分区可能大于进程实际需要的大小。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不灵活:&lt;/strong&gt; 分区大小固定，大进程可能无处容身，小进程占用大分区造成浪费。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 可变分区 (Variable Partitioning / Dynamic Partitioning)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特点:&lt;/strong&gt; 内存不预先划分。根据进程的实际需求，从**空闲内存 (洞 Hole) ** 中动态地分割出一个分区分配给它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分配:&lt;/strong&gt; 使用 First Fit, Best Fit, Worst Fit 等算法在空闲区列表中查找并分配。&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;ul&gt;
&lt;li&gt;&lt;strong&gt;外碎片:&lt;/strong&gt; 随着进程的分配和回收，内存中会产生许多不连续的小空闲区，即使总空闲量足够，也可能无法满足新的较大内存请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理复杂:&lt;/strong&gt; 需要维护空闲区列表，分配和回收时涉及查找、分割和合并。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;外碎片解决方案:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;紧缩技术 (Compaction / Memory Compaction):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 通过移动内存中的进程，将所有小的空闲区合并成一个或几个大的连续空闲区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 需要&lt;strong&gt;动态重定位&lt;/strong&gt;支持 (因为进程物理地址改变了) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开销大:&lt;/strong&gt; 移动内存内容非常耗时，期间系统性能会下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动时机:&lt;/strong&gt; 何时进行紧缩？ (例如，当分配失败且有足够总空闲空间时，或定时进行) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 页式管理 (Paging)&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;strong&gt;页 (Page)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理内存空间:&lt;/strong&gt; 划分为与页大小相同的块，称为 &lt;strong&gt;页框 (Page Frame)&lt;/strong&gt; 或物理页面、内存块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配:&lt;/strong&gt; 以&lt;strong&gt;页&lt;/strong&gt;为单位进行。进程需要的页可以加载到&lt;strong&gt;任意&lt;/strong&gt;空闲的页框中。逻辑上相邻的页在物理上&lt;strong&gt;不必&lt;/strong&gt;相邻。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑地址结构:&lt;/strong&gt; &lt;code&gt;逻辑地址 = 页号 (Page Number) + 页内偏移 (Offset)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;例如，32位地址，页面大小 4KB (212 B):
&lt;ul&gt;
&lt;li&gt;高 20位 (31-12) 是页号。&lt;/li&gt;
&lt;li&gt;低 12位 (11-0) 是页内偏移。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据结构:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页表 (Page Table):&lt;/strong&gt; 每个进程都有一个页表。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能:&lt;/strong&gt; 记录逻辑页号到物理页框号的映射关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表项 (Page Table Entry - PTE):&lt;/strong&gt; 至少包含 &lt;code&gt;页框号 (Frame Number)&lt;/code&gt;。通常还包含其他控制位：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;有效位/驻留位 (Valid/Present Bit):&lt;/strong&gt; 标记该页是否在物理内存中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护位 (Protection Bits):&lt;/strong&gt; 控制读/写/执行权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改位 (Modified/Dirty Bit):&lt;/strong&gt; 标记该页加载到内存后是否被修改过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问位 (Accessed/Referenced Bit):&lt;/strong&gt; 标记该页是否被访问过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储:&lt;/strong&gt; 页表本身也存储在内存中。操作系统通过&lt;strong&gt;页表基址寄存器 (Page Table Base Register - PTBR)&lt;/strong&gt; (如 x86 的 CR3 寄存器) 指向当前进程的页表起始地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空闲页框列表:&lt;/strong&gt; 操作系统需要维护一个数据结构 (如位图或链表) 来跟踪哪些物理页框是空闲的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换过程 (硬件 MMU):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;CPU 发出逻辑地址。&lt;/li&gt;
&lt;li&gt;MMU 将逻辑地址分解为&lt;strong&gt;页号 &lt;code&gt;p&lt;/code&gt;&lt;/strong&gt; 和&lt;strong&gt;页内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;页号 &lt;code&gt;p&lt;/code&gt;&lt;/strong&gt; 作为索引，访问当前进程的&lt;strong&gt;页表&lt;/strong&gt; (基址在 PTBR)。&lt;/li&gt;
&lt;li&gt;找到对应的&lt;strong&gt;页表项 (PTE)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;检查 PTE 中的&lt;strong&gt;有效位&lt;/strong&gt;和&lt;strong&gt;保护位&lt;/strong&gt;。如果无效或权限不足，则产生&lt;strong&gt;缺页异常 (Page Fault)&lt;/strong&gt; 或&lt;strong&gt;保护异常&lt;/strong&gt;，陷入操作系统处理。&lt;/li&gt;
&lt;li&gt;如果有效且权限允许，从 PTE 中取出&lt;strong&gt;页框号 &lt;code&gt;f&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;将&lt;strong&gt;页框号 &lt;code&gt;f&lt;/code&gt;&lt;/strong&gt; 与&lt;strong&gt;页内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt; 拼接 (或 &lt;code&gt;f * PageSize + d&lt;/code&gt;) 得到最终的&lt;strong&gt;物理地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;访问物理内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无外碎片:&lt;/strong&gt; 以固定大小的页框为单位分配，总能利用空闲页框。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存利用率高:&lt;/strong&gt; 物理内存不必连续。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易于实现共享:&lt;/strong&gt; 让多个进程的页表项指向同一个物理页框即可共享页面 (如共享库代码) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持虚拟内存的基础。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内碎片:&lt;/strong&gt; 进程的最后一页通常不会完全占满，导致该页框内产生少量内碎片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表开销:&lt;/strong&gt; 页表本身需要占用内存空间。对于大地址空间和小编页面，页表可能非常大 (需要多级页表等技术解决) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换开销:&lt;/strong&gt; 每次访存理论上需要两次内存访问 (一次查页表，一次访问数据) 。实际使用 &lt;strong&gt;TLB (Translation Lookaside Buffer)&lt;/strong&gt;，一种页表项的高速缓存，来加速转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.5 段式管理 (Segmentation)&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;strong&gt;逻辑结构&lt;/strong&gt;划分为多个&lt;strong&gt;段 (Segment)&lt;/strong&gt;，如代码段、数据段、栈段等。每个段有自己的名字 (通常用段号代替) 和长度。&lt;strong&gt;段的长度可以不同&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理内存空间:&lt;/strong&gt; 仍然是线性地址空间，但分配时按&lt;strong&gt;整个段&lt;/strong&gt;分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配:&lt;/strong&gt; 以&lt;strong&gt;段&lt;/strong&gt;为单位。每个段需要分配一块&lt;strong&gt;连续&lt;/strong&gt;的物理内存空间，但不同段之间可以不相邻。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑地址结构:&lt;/strong&gt; &lt;code&gt;逻辑地址 = 段号 (Segment Number) + 段内偏移 (Offset within Segment)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据结构:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;段表 (Segment Table):&lt;/strong&gt; 每个进程一个段表。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能:&lt;/strong&gt; 记录逻辑段号到物理内存信息的映射。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;段表项 (Segment Table Entry - STE):&lt;/strong&gt; 通常包含：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;段基址 (Segment Base):&lt;/strong&gt; 该段在物理内存中的起始地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;段限长 (Segment Limit):&lt;/strong&gt; 该段的长度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护位 (Protection Bits):&lt;/strong&gt; 如读/写/执行权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储:&lt;/strong&gt; 段表本身也存储在内存中。操作系统通过&lt;strong&gt;段表基址寄存器 (Segment Table Base Register - STBR)&lt;/strong&gt; 指向当前进程的段表。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物理内存管理:&lt;/strong&gt; 类似于&lt;strong&gt;可变分区&lt;/strong&gt;管理，需要维护空闲区列表，使用 First Fit 等算法分配连续空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换过程 (硬件 MMU):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;CPU 发出逻辑地址。&lt;/li&gt;
&lt;li&gt;MMU 将逻辑地址分解为&lt;strong&gt;段号 &lt;code&gt;s&lt;/code&gt;&lt;/strong&gt; 和&lt;strong&gt;段内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;段号 &lt;code&gt;s&lt;/code&gt;&lt;/strong&gt; 作为索引，访问当前进程的&lt;strong&gt;段表&lt;/strong&gt; (基址在 STBR)。&lt;/li&gt;
&lt;li&gt;找到对应的&lt;strong&gt;段表项 (STE)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;段号 &lt;code&gt;s&lt;/code&gt; 是否合法 (在段表范围内) 。&lt;/li&gt;
&lt;li&gt;段内偏移 &lt;code&gt;d&lt;/code&gt; 是否小于段限长 &lt;code&gt;Limit&lt;/code&gt; (&lt;code&gt;0 &amp;#x3C;= d &amp;#x3C; Limit&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果检查通过，取出&lt;strong&gt;段基址 &lt;code&gt;Base&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;计算物理地址: &lt;strong&gt;物理地址 = 段基址 &lt;code&gt;Base&lt;/code&gt; + 段内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;访问物理内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;符合程序逻辑:&lt;/strong&gt; 分段是用户可见的，便于程序员组织代码和数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易于共享和保护:&lt;/strong&gt; 可以方便地对整个逻辑段 (如代码段) 进行共享或设置保护属性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;外碎片:&lt;/strong&gt; 段的长度可变，分配和回收类似于可变分区，会产生外碎片。需要紧缩技术。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存分配复杂:&lt;/strong&gt; 需要找到足够大的连续空闲块。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.6 段页式管理 (Segmented Paging)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想:&lt;/strong&gt; 结合&lt;strong&gt;段式&lt;/strong&gt;和&lt;strong&gt;页式&lt;/strong&gt;的优点。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户视角 / 逻辑地址空间:&lt;/strong&gt; 仍然按&lt;strong&gt;段&lt;/strong&gt;划分 (用户可见)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理 / 物理内存:&lt;/strong&gt; 按&lt;strong&gt;页框&lt;/strong&gt;划分和分配 (系统底层)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 每个&lt;strong&gt;逻辑段&lt;/strong&gt;内部再进一步划分为&lt;strong&gt;固定大小的页&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑地址结构:&lt;/strong&gt; &lt;code&gt;逻辑地址 = 段号 s + 段内页号 p + 页内偏移 d&lt;/code&gt;
(或者看作 &lt;code&gt;段号 s + 段内偏移 offset&lt;/code&gt;，其中 &lt;code&gt;offset&lt;/code&gt; 再被解释为 &lt;code&gt;页号 p + 页内偏移 d&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据结构:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;段表 (Segment Table):&lt;/strong&gt; 每个进程一个。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;段表项:&lt;/strong&gt; 不再直接指向物理基址，而是指向该&lt;strong&gt;段对应的页表&lt;/strong&gt;的基址，并包含页表的长度 (或段的页数) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表 (Page Table):&lt;/strong&gt; &lt;strong&gt;每个段&lt;/strong&gt;拥有一个页表。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页表项:&lt;/strong&gt; 记录段内的逻辑页号到物理页框号的映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换过程 (硬件 MMU):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;CPU 发出逻辑地址 &lt;code&gt;(s, offset)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;段号 &lt;code&gt;s&lt;/code&gt;&lt;/strong&gt; 查&lt;strong&gt;段表&lt;/strong&gt;，找到对应段的&lt;strong&gt;页表基址&lt;/strong&gt;和&lt;strong&gt;段限长&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;检查&lt;strong&gt;段内偏移 &lt;code&gt;offset&lt;/code&gt;&lt;/strong&gt; 是否小于&lt;strong&gt;段限长&lt;/strong&gt;。如果超出，则地址越界。&lt;/li&gt;
&lt;li&gt;将&lt;strong&gt;段内偏移 &lt;code&gt;offset&lt;/code&gt;&lt;/strong&gt; 分解为&lt;strong&gt;段内页号 &lt;code&gt;p&lt;/code&gt;&lt;/strong&gt; 和&lt;strong&gt;页内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;使用&lt;strong&gt;段内页号 &lt;code&gt;p&lt;/code&gt;&lt;/strong&gt; 作为索引，访问&lt;strong&gt;该段的页表&lt;/strong&gt; (基址来自段表项) ，找到对应的&lt;strong&gt;页表项 (PTE)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;检查 PTE 的有效位和保护位。&lt;/li&gt;
&lt;li&gt;从 PTE 中取出&lt;strong&gt;页框号 &lt;code&gt;f&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;计算物理地址: &lt;strong&gt;物理地址 = 页框号 &lt;code&gt;f&lt;/code&gt; * PageSize + 页内偏移 &lt;code&gt;d&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;访问物理内存。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;结合了段式的逻辑清晰、易于共享保护和页式的内存利用率高、无外碎片的优点。&lt;/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;/li&gt;
&lt;li&gt;&lt;strong&gt;地址转换更复杂:&lt;/strong&gt; 需要多次内存访问 (查段表 -&gt; 查页表 -&gt; 访问数据) 。同样需要 TLB 来加速。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. 内存 &quot;扩充&quot; 技术&lt;/h2&gt;
&lt;p&gt;在物理内存不足时，让系统能运行更大程序或更多进程的技术。&lt;/p&gt;
&lt;h3&gt;6.1 覆盖技术 (Overlaying)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 在&lt;strong&gt;物理内存小于程序总大小&lt;/strong&gt;的情况下运行程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 程序的&lt;strong&gt;不同模块&lt;/strong&gt; (覆盖段) 按照它们的&lt;strong&gt;调用关系&lt;/strong&gt;在&lt;strong&gt;同一块内存区域&lt;/strong&gt;中&lt;strong&gt;相互替换&lt;/strong&gt;。只有当前需要的模块和常驻模块保留在内存中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序员负责:&lt;/strong&gt; 需要程序员手动划分程序模块，并指定它们之间的&lt;strong&gt;覆盖结构 (Overlay Structure)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;操作系统提供加载覆盖模块的机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例:&lt;/strong&gt; 程序 A 调用 B 或 C；B 调用 D 或 E；C 调用 F。
&lt;ul&gt;
&lt;li&gt;常驻区: A (8K)&lt;/li&gt;
&lt;li&gt;覆盖区 0: B (8K) 或 C (10K) -&gt; 需要 10K&lt;/li&gt;
&lt;li&gt;覆盖区 1: D (12K) 或 E (4K) (当B在内存时) 或 F (10K) (当C在内存时) -&gt; 需要 12K&lt;/li&gt;
&lt;li&gt;总需内存 = 常驻区 + Max(覆盖区0) + Max(覆盖区1) = 8K + 10K + 12K = 30K (远小于原始总和 54K)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 能够在小内存上运行大程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对用户不透明:&lt;/strong&gt; 增加了程序员的负担，编程复杂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行时间增加:&lt;/strong&gt; 需要从外存动态加载覆盖模块，属于“时间换空间”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用:&lt;/strong&gt; 主要用于&lt;strong&gt;早期&lt;/strong&gt;内存极其有限的操作系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 交换技术 (Swapping)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 提高内存利用率和系统吞吐量，允许运行的进程总大小超过物理内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思想:&lt;/strong&gt; 将&lt;strong&gt;暂时不运行&lt;/strong&gt;的进程&lt;strong&gt;完整地&lt;/strong&gt;从&lt;strong&gt;内存&lt;/strong&gt;移动到&lt;strong&gt;外存 (交换区 Swap Space) &lt;strong&gt;，称为&lt;/strong&gt;换出 (Swap Out / Roll Out)&lt;/strong&gt;。当需要再次运行时，再从外存将其&lt;strong&gt;换回 (Swap In / Roll In)&lt;/strong&gt; 到内存中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Swapper (交换程序):&lt;/strong&gt; 操作系统中负责执行交换操作的模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换区 (Swap Space):&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;strong&gt;交换内容:&lt;/strong&gt; 进程的哪些部分需要交换？通常是进程的整个用户地址空间 (代码、数据、堆、栈等运行时状态) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换位置:&lt;/strong&gt; 被换出的进程保存在磁盘的交换区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交换时机:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;内存空间不足时触发换出。&lt;/li&gt;
&lt;li&gt;进程长时间阻塞或优先级低时可能被换出。&lt;/li&gt;
&lt;li&gt;与&lt;strong&gt;调度器&lt;/strong&gt;结合，选择合适的进程换入换出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换出进程选择:&lt;/strong&gt; 考虑进程状态 (不应换出等待 I/O 的进程) 、优先级、在内存驻留时间等因素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换入位置:&lt;/strong&gt; 换回内存时不一定回到原来的物理地址。需要&lt;strong&gt;动态地址重定位&lt;/strong&gt;支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程空间增长:&lt;/strong&gt; 如果进程在内存中时其地址空间增长 (如堆或栈扩展) ，分配可能需要更多内存。如果此时内存不足，可能需要换出其他进程。如果进程在交换区时需要增长，则处理更复杂，通常不允许或有预留机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 提高了内存利用率，支持运行比物理内存更大的进程集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开销大:&lt;/strong&gt; 整个进程映像的磁盘 I/O 非常耗时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可能导致抖动 (Thrashing):&lt;/strong&gt; 如果内存严重不足，系统可能花费大量时间在换入换出进程上，而实际执行用户代码的时间很少。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用:&lt;/strong&gt; 曾用于分时系统，现代系统中的&lt;strong&gt;虚拟内存&lt;/strong&gt;可以看作是更精细化的交换技术 (以页为单位) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 虚拟内存技术 (Virtual Memory)&lt;/h3&gt;
&lt;p&gt;(本讲义中提及，但未详细展开，通常是后续章节内容)
结合了&lt;strong&gt;请求调页 (Demand Paging)&lt;/strong&gt; 或&lt;strong&gt;请求分段 (Demand Segmentation)&lt;/strong&gt; 与&lt;strong&gt;交换技术&lt;/strong&gt;的思想。允许程序只加载部分页面/段到内存即可运行，其余部分在需要时才从磁盘加载。这是现代操作系统普遍采用的核心内存管理技术。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 重点小结&lt;/h2&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;ul&gt;
&lt;li&gt;数据结构：位示图、空闲区表/链表 (隐式/显式/分离) 。&lt;/li&gt;
&lt;li&gt;分配算法：首次/下次/最佳/最差适配。&lt;/li&gt;
&lt;li&gt;回收与合并。&lt;/li&gt;
&lt;li&gt;碎片问题：内碎片、外碎片。&lt;/li&gt;
&lt;li&gt;特定策略：伙伴系统、SLAB/SLUB/SLOB 分配器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理方案:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;单一连续区、固定分区、可变分区 (+紧缩) 、页式、段式、段页式。&lt;/li&gt;
&lt;li&gt;每种方案的特点、优缺点、地址转换机制、相关数据结构 (页表/段表) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存扩充技术:&lt;/strong&gt; 覆盖技术、交换技术 (虚拟内存是更高级形式) 。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 04: Process and Thread Scheduling</title><link>https://www.lyt0112.com/blog/operating_systems_note_04-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_04-zh</guid><description>Operating Systems Notes 04: 进程线程调度</description><pubDate>Fri, 28 Mar 2025 04:15:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-03-25&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 进程/线程调度问题分析&lt;/h2&gt;
&lt;h3&gt;1.1 When and How (调度时机与原因)&lt;/h3&gt;
&lt;h4&gt;调度这件事儿什么时候做？做的理由有哪些？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;调度时机 (When):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;操作系统需要在特定事件发生时决定哪个进程接下来应该占用CPU。这些时机主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;进程创建 (Process Creation):&lt;/strong&gt; 当一个新进程被创建时 (例如，通过&lt;code&gt;fork()&lt;/code&gt;系统调用) ，需要决定是运行父进程还是子进程，或者其他进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程终止 (Process Termination):&lt;/strong&gt; 当一个进程执行完毕或被终止时 (例如，调用&lt;code&gt;exit()&lt;/code&gt;) ，它占用的CPU必须分配给其他就绪的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程阻塞 (Process Blocking):&lt;/strong&gt; 当一个进程因等待某个事件 (如I/O操作完成、等待信号量、等待用户输入等) 而无法继续执行时，它会进入阻塞状态，此时调度器需要选择另一个进程来运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O中断发生 (I/O Interrupt):&lt;/strong&gt; 当一个I/O操作完成，产生中断时，原先等待该I/O的进程可能会从阻塞态变为就绪态。这时，调度器可能需要重新评估，决定是继续运行当前进程，还是切换到刚刚变为就绪的、可能优先级更高的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟中断发生 (Clock Interrupt):&lt;/strong&gt; 在分时系统中，为了防止某个进程长时间独占CPU，操作系统会设置一个定时器。当定时器中断发生时，当前运行进程的时间片 (Time Slice/Quantum) 可能已用完，调度器会介入，决定是继续运行该进程 (如果时间片未用完或没有其他就绪进程) ，还是切换到另一个就绪进程 (抢占式调度) 。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;调度的理由 (Why):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;调度的根本目的是有效、公平地管理和分配有限的CPU资源给多个并发执行的进程。具体理由包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提高CPU利用率:&lt;/strong&gt; 尽量让CPU保持忙碌状态，减少空闲时间。当一个进程等待I/O时，可以让其他就绪进程使用CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高系统吞吐量 (Throughput):&lt;/strong&gt; 单位时间内完成的进程数量。好的调度算法可以在满足其他目标的同时，尽可能多地完成任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少周转时间 (Turnaround Time):&lt;/strong&gt; 指一个进程从提交到完成所花费的总时间 (等待进入内存、在就绪队列等待、CPU执行、I/O执行的总和) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少等待时间 (Waiting Time):&lt;/strong&gt; 指进程在就绪队列中等待CPU所花费的总时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少响应时间 (Response Time):&lt;/strong&gt; 对于交互式系统尤其重要，指从用户发出请求到系统首次产生响应 (而非完成任务) 所花费的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确保公平性 (Fairness):&lt;/strong&gt; 保证每个进程都能获得合理的CPU时间份额，防止某些进程被饿死 (Starvation) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;满足实时性要求 (Real-time Constraints):&lt;/strong&gt; 对于实时系统，调度必须保证关键任务在它们的截止时间 (Deadline) 之前完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;如果没有可被调度的进程，系统做什么呢？&lt;/h4&gt;
&lt;p&gt;如果当前没有用户进程或系统核心任务进程处于就绪状态 (Ready State) ，CPU不能完全停止。操作系统通常会执行一个特殊的&lt;strong&gt;空闲进程 (Idle Process)&lt;/strong&gt; 或称为&lt;strong&gt;系统空闲任务 (System Idle Task)&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt; 这个进程拥有最低的优先级。当没有其他任何事情可做时，调度器就会选择它来运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行为:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;它通常执行一个无限循环。&lt;/li&gt;
&lt;li&gt;在这个循环中，它可以执行一些低优先级的系统维护任务。&lt;/li&gt;
&lt;li&gt;更重要的是，在许多架构上 (如x86) ，它可以执行一个特殊的指令 (如&lt;code&gt;HLT&lt;/code&gt; - Halt) ，使CPU进入低功耗状态，直到下一个中断 (如时钟中断、I/O中断) 唤醒CPU。这有助于节能和降低温度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 确保CPU总是有事可做 (即使是“等待”) ，并提供一个合法的状态供调度器切换，同时优化能源使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;上下文切换的过程？有哪些开销？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;上下文切换 (Context Switch)&lt;/strong&gt; 是指操作系统保存当前正在运行进程的状态 (上下文) ，并加载另一个进程的状态，以便让后者开始或继续运行的过程。这是实现多任务处理的基础。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;过程:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断/系统调用触发:&lt;/strong&gt; 调度发生 (如时间片用完、进程阻塞等) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存当前进程上下文:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;保存程序计数器 (Program Counter, PC) 和其他CPU寄存器 (通用寄存器、状态寄存器等) 的值。这些值通常保存在该进程的&lt;strong&gt;进程控制块 (Process Control Block, PCB)&lt;/strong&gt; 中。&lt;/li&gt;
&lt;li&gt;保存当前进程的栈指针。&lt;/li&gt;
&lt;li&gt;更新进程状态 (例如，从 &quot;Running&quot; 变为 &quot;Ready&quot; 或 &quot;Blocked&quot;) 。&lt;/li&gt;
&lt;li&gt;可能需要保存内存管理相关信息 (如页表基址寄存器) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行调度算法:&lt;/strong&gt; 操作系统调度器代码运行，根据调度策略选择下一个要运行的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载新进程上下文:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;从选定进程的PCB中恢复其状态。&lt;/li&gt;
&lt;li&gt;加载新进程的程序计数器和CPU寄存器。&lt;/li&gt;
&lt;li&gt;恢复新进程的栈指针。&lt;/li&gt;
&lt;li&gt;更新新进程的状态 (通常是从 &quot;Ready&quot; 变为 &quot;Running&quot;) 。&lt;/li&gt;
&lt;li&gt;恢复内存管理信息 (可能需要刷新TLB - Translation Lookaside Buffer) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳转执行:&lt;/strong&gt; CPU跳转到新进程被中断时的下一条指令地址 (或其入口点，如果是首次运行) 开始执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;开销:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;上下文切换本身并不执行任何有用的用户工作，它是一种&lt;strong&gt;纯粹的开销 (Overhead)&lt;/strong&gt;。开销主要包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;直接开销 (Direct Costs):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;保存和加载寄存器:&lt;/strong&gt; CPU需要时间来读写寄存器和PCB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行调度器代码:&lt;/strong&gt; 选择下一个进程也需要CPU时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新PCB和其他数据结构:&lt;/strong&gt; 维护进程队列等操作需要时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MMU操作:&lt;/strong&gt; 可能需要加载新的页表基址，这可能导致TLB被刷新 (TLB flush) ，增加后续内存访问的延迟。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;间接开销 (Indirect Costs):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存污染 (Cache Pollution):&lt;/strong&gt; 当新进程开始运行时，CPU缓存 (L1, L2, L3 Cache) 中很可能包含的是前一个进程的数据和指令。新进程运行时会发生大量的缓存未命中 (Cache Miss) ，需要从内存中重新加载数据，这会显著降低执行速度，直到新进程的“工作集” (Working Set) 被加载到缓存中。这是上下文切换最主要的性能影响之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU流水线冲刷:&lt;/strong&gt; 切换可能导致CPU的指令流水线被清空和重建。&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;h4&gt;关于调度算法，我们都关心什么？&lt;/h4&gt;
&lt;p&gt;我们在评估和选择调度算法时，主要关心以下几个&lt;strong&gt;性能指标 (Performance Metrics)&lt;/strong&gt; 或&lt;strong&gt;目标 (Goals)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 利用率 (CPU Utilization):&lt;/strong&gt; CPU处于忙碌状态的时间百分比。越高越好，但100%可能意味着没有冗余，响应性可能变差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统吞吐量 (Throughput):&lt;/strong&gt; 单位时间内完成的进程 (或作业) 数量。越高越好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;周转时间 (Turnaround Time):&lt;/strong&gt; 从进程提交到完成的总时间。越短越好 (平均周转时间、最差周转时间) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待时间 (Waiting Time):&lt;/strong&gt; 进程在就绪队列中等待CPU的总时间。越短越好 (平均等待时间、最差等待时间) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应时间 (Response Time):&lt;/strong&gt; 从提交请求到产生第一个响应的时间 (交互式系统关键) 。越短且越稳定越好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平性 (Fairness):&lt;/strong&gt; 每个进程获得合理的CPU份额，防止饿死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可预测性 (Predictability):&lt;/strong&gt; 对于实时系统，执行时间的可预测性比平均性能更重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;满足截止时间 (Meeting Deadlines):&lt;/strong&gt; 对于实时系统，这是硬性或软性要求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级处理 (Priority Handling):&lt;/strong&gt; 系统能否有效处理不同优先级的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源平衡 (Resource Balance):&lt;/strong&gt; 尽量保持所有资源 (CPU, I/O设备) 都处于忙碌状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;不同类型的操作系统都适用同一种调度算法吗？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;不适用。&lt;/strong&gt; 不同类型的操作系统有着不同的设计目标和用户需求，因此需要采用不同的调度策略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批处理系统 (Batch Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;目标：最大化吞吐量和CPU利用率，减少平均周转时间。用户通常不直接与系统交互。&lt;/li&gt;
&lt;li&gt;适用算法：先来先服务 (FCFS)、最短作业优先 (SJF)、最高响应比优先 (HRRN)。公平性和响应时间相对不重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式系统 (Interactive Systems / Time-Sharing Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;目标：最小化响应时间，提供良好的用户体验，兼顾公平性。&lt;/li&gt;
&lt;li&gt;适用算法：轮转法 (Round Robin, RR)、优先级调度、多级队列调度 (Multi-level Queue)、多级反馈队列调度 (Multi-level Feedback Queue, MLFQ)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时系统 (Real-Time Systems, RTOS):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;目标：满足任务的截止时间要求，可预测性至关重要。分为硬实时 (必须满足) 和软实时 (尽量满足) 。&lt;/li&gt;
&lt;li&gt;适用算法：速率单调调度 (Rate Monotonic Scheduling, RMS - 用于静态优先级)、最早截止时间优先 (Earliest Deadline First, EDF - 用于动态优先级)、优先级调度 (配合精确的优先级分配) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;对于一个调度算法，应该追求什么样的目标？&lt;/h4&gt;
&lt;p&gt;一个调度算法应该追求的目标组合取决于具体的系统类型和应用场景。通常需要在多个 (有时是相互冲突的) 目标之间进行&lt;strong&gt;权衡 (Trade-off)&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通用目标 (All Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公平性:&lt;/strong&gt; 防止饿死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略强制:&lt;/strong&gt; 确保系统设定的策略 (如优先级) 得到执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平衡:&lt;/strong&gt; 保持系统的各个部分都处于活动状态 (例如，CPU密集型和I/O密集型进程交替运行) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批处理系统目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;高吞吐量&lt;/li&gt;
&lt;li&gt;低周转时间&lt;/li&gt;
&lt;li&gt;高CPU利用率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式系统目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;快速响应时间&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;strong&gt;实时系统目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;满足截止时间&lt;/li&gt;
&lt;li&gt;高可预测性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最佳实践:&lt;/strong&gt; 没有“万能”的调度算法。选择或设计算法时，必须明确系统的主要目标，并接受在其他方面可能存在的不足。例如，追求极低响应时间可能会牺牲一些吞吐量。&lt;/p&gt;
&lt;h4&gt;选进程时都考虑了哪些点？单一因素还是多因素？&lt;/h4&gt;
&lt;p&gt;选择下一个要运行的进程时，调度算法可能考虑&lt;strong&gt;单一因素&lt;/strong&gt;或&lt;strong&gt;多个因素&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;单一因素:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FCFS:&lt;/strong&gt; 只考虑进程到达就绪队列的时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SJF (非抢占式):&lt;/strong&gt; 只考虑预估的下一个CPU脉冲 (burst) 长度。&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;&lt;strong&gt;优先级调度 (带老化):&lt;/strong&gt; 考虑静态优先级，但也考虑进程等待时间 (老化机制，aging) ，提高等待过久进程的优先级以防饿死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RR:&lt;/strong&gt; 考虑到达时间和时间片轮转。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MLFQ:&lt;/strong&gt; 考虑优先级、进程行为 (CPU密集型 vs I/O密集型) 、等待时间等，动态调整进程在不同队列间的移动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HRRN:&lt;/strong&gt; 考虑等待时间 (W) 和服务时间/脉冲长度 (S)，计算响应比 &lt;code&gt;(W+S)/S&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux CFS:&lt;/strong&gt; 考虑进程的虚拟运行时间 (virtual runtime)，旨在给每个进程公平的CPU时间比例。它间接考虑了进程的等待时间和已运行时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows调度:&lt;/strong&gt; 考虑基础优先级、动态优先级提升 (如完成I/O、处于前台窗口) 、时间片消耗情况等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;结论:&lt;/strong&gt; 简单算法可能只关注单一因素，但为了在复杂环境中平衡多个目标 (如响应时间、公平性、吞吐量) ，现代通用操作系统广泛使用&lt;strong&gt;多因素调度算法&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;1.2 How (调度算法详解)&lt;/h3&gt;
&lt;h4&gt;适用批处理系统的调度算法有哪些？&lt;/h4&gt;
&lt;p&gt;主要目标是效率 (吞吐量、CPU利用率) 和整体完成时间 (周转时间) 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先来先服务 (First-Come, First-Served, FCFS):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;实现：按进程到达就绪队列的顺序进行调度。使用FIFO队列。&lt;/li&gt;
&lt;li&gt;优点：简单，易于实现，公平 (按到达顺序) 。&lt;/li&gt;
&lt;li&gt;缺点：平均等待时间可能很长，尤其当短进程排在长进程之后时 (&lt;strong&gt;护航效应 Convoy Effect&lt;/strong&gt;) 。不适合交互式系统。是非抢占式的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最短作业优先 (Shortest Job First, SJF):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;实现：选择预计CPU执行时间 (下一个CPU burst) 最短的进程。可以是抢占式 (SRTF - Shortest Remaining Time First) 或非抢占式。&lt;/li&gt;
&lt;li&gt;优点：理论上具有最优的平均等待时间和平均周转时间。&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;需要预测下一个CPU burst长度，这很难精确做到 (通常基于历史数据估计) 。&lt;/li&gt;
&lt;li&gt;可能导致长作业&lt;strong&gt;饿死 (Starvation)&lt;/strong&gt;，即长时间得不到CPU。&lt;/li&gt;
&lt;li&gt;非抢占式SJF不适合交互系统。抢占式SRTF开销较大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最高响应比优先 (Highest Response Ratio Next, HRRN):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;实现：非抢占式。计算每个进程的响应比 &lt;code&gt;R = (等待时间 W + 服务时间 S) / 服务时间 S&lt;/code&gt;，选择R最高的进程。&lt;/li&gt;
&lt;li&gt;优点：结合了FCFS和SJF的优点。短作业容易被选中 (S小，R大) 。同时，等待时间长的进程其响应比也会增加，避免了饿死。&lt;/li&gt;
&lt;li&gt;缺点：仍需要预测服务时间S。计算响应比有额外开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;适用交互式系统的调度算法有哪些？&lt;/h4&gt;
&lt;p&gt;主要目标是提供快速响应和用户满意度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;轮转法 (Round Robin, RR):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现：类似于FCFS，但增加了时间片和抢占。每个进程被分配一个固定的时间片 (Quantum) ，运行时间超出时间片后会被强制切换 (抢占) ，放回就绪队列尾部。&lt;/li&gt;
&lt;li&gt;优点：公平，响应时间相对较短 (特别是对于短请求) ，简单。&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;性能对时间片大小非常敏感。太小则上下文切换频繁，开销大；太大则退化为FCFS，响应时间变长。&lt;/li&gt;
&lt;li&gt;平均周转时间通常比SJF长。&lt;/li&gt;
&lt;li&gt;没有考虑优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最佳实践:&lt;/strong&gt; 时间片大小通常选择比平均交互响应所需时间稍长，但足够短以保证多个交互进程能快速轮换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优先级调度 (Priority Scheduling):&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;ul&gt;
&lt;li&gt;可能导致低优先级进程&lt;strong&gt;饿死&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;优先级的确定可能是个问题 (静态 vs 动态) 。&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;老化 (Aging):&lt;/strong&gt; 随时间增加等待进程的优先级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态优先级:&lt;/strong&gt; 根据进程行为 (如I/O等待) 调整优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多级队列调度 (Multi-level Queue Scheduling):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现：将就绪队列划分为多个独立的队列，每个队列有自己的优先级和调度算法 (如：前台交互队列用RR，后台批处理队列用FCFS) 。进程被永久分配到一个队列。&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;p&gt;&lt;strong&gt;多级反馈队列调度 (Multi-level Feedback Queue, MLFQ):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&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;li&gt;I/O密集型进程 (经常阻塞放弃CPU) 通常会停留在较高优先级队列，保证响应性。CPU密集型进程会逐渐下降到较低优先级队列。&lt;/li&gt;
&lt;/ul&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;h4&gt;适用实时系统的调度算法有哪些？&lt;/h4&gt;
&lt;p&gt;主要目标是满足时间约束 (截止时间) 。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;速率单调调度 (Rate Monotonic Scheduling, RMS):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;类型：静态优先级，抢占式。&lt;/li&gt;
&lt;li&gt;原理：周期性任务的优先级与其执行频率 (速率) 成正比。周期越短 (频率越高) ，优先级越高。&lt;/li&gt;
&lt;li&gt;优点：简单，易于实现，是最佳的静态优先级调度算法 (如果任务集可调度，RMS就能找到调度方案) 。可进行理论上的可调度性分析 (例如，利用率测试) 。&lt;/li&gt;
&lt;li&gt;缺点：只适用于周期性任务；对任务特性有较强假设；CPU利用率上限不如动态优先级算法高。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最早截止时间优先 (Earliest Deadline First, EDF):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;类型：动态优先级，抢占式。&lt;/li&gt;
&lt;li&gt;原理：当前就绪任务中，绝对截止时间最早的任务拥有最高优先级。&lt;/li&gt;
&lt;li&gt;优点：理论上是最优的动态优先级调度算法。只要任务集的总CPU利用率不超过100%，EDF就能找到调度方案 (对于可抢占、独立任务等理想情况) 。可以处理周期性和非周期性任务。&lt;/li&gt;
&lt;li&gt;缺点：实现比RMS复杂；可能出现多米诺骨牌效应 (一个任务错过截止时间可能导致后续任务也错过) ；动态优先级变化导致上下文切换可能更频繁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于优先级的抢占式调度:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;通用方法：给实时任务分配高优先级，使用标准优先级调度器。可以通过精心设置优先级来模拟RMS或EDF的行为。常用于软实时系统或硬实时系统中与其他任务共存的情况。&lt;/li&gt;
&lt;li&gt;需要确保高优先级任务能抢占低优先级任务，并且优先级反转 (Priority Inversion) 问题得到处理 (如使用优先级继承 Priority Inheritance 或优先级天花板 Priority Ceiling Protocol) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;怎样理解抢占式和非抢占式？&lt;/h4&gt;
&lt;p&gt;这是调度器决定何时进行调度的两种基本模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;非抢占式调度 (Non-preemptive / Cooperative Scheduling):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 一旦CPU分配给某个进程，该进程将一直运行，直到它主动放弃CPU (完成任务、阻塞等待I/O、或显式调用yield) 。调度器不能在进程运行中途强制剥夺其CPU使用权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现简单；上下文切换只在进程自愿放弃CPU时发生，开销相对较小；不会有并发访问内核数据结构的竞争问题 (在单处理器上) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 一个长时间运行或行为不当的进程可以独占CPU，导致其他进程 (特别是需要快速响应的交互式进程) 长时间等待，响应性差；不适合分时和实时系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; 早期的Windows (如Windows 3.1), 早期的Mac OS, 某些简单的嵌入式系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;抢占式调度 (Preemptive Scheduling):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 操作系统可以强制暂停当前正在运行的进程 (即使它并未主动放弃CPU) ，并将CPU分配给另一个进程。这种抢占通常发生在时钟中断 (时间片用完) 或更高优先级进程变为就绪时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 能够保证CPU在进程间公平分配；提供更好的响应时间；可以有效处理优先级，防止低优先级任务阻塞高优先级任务 (前提是优先级设置合理) ；是现代多任务操作系统的标准做法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 实现更复杂；上下文切换更频繁，开销更大；需要处理内核数据结构的并发访问问题 (需要锁或其他同步机制) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; Unix/Linux, Windows NT及后续版本, macOS, 大多数现代RTOS。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;从哪几方面对调度算法进行比较？&lt;/h4&gt;
&lt;p&gt;主要从前面提到的&lt;strong&gt;性能指标/目标&lt;/strong&gt;来进行比较和评估：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CPU利用率:&lt;/strong&gt; 哪个算法更能让CPU保持忙碌？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;吞吐量:&lt;/strong&gt; 哪个算法单位时间内能完成更多任务？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;周转时间 (平均/最坏):&lt;/strong&gt; 哪个算法下进程从提交到完成更快？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待时间 (平均/最坏):&lt;/strong&gt; 哪个算法下进程在就绪队列中等待的时间更短？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应时间 (平均/方差):&lt;/strong&gt; 哪个算法对交互式请求的响应更快、更稳定？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平性:&lt;/strong&gt; 哪个算法更能保证所有进程获得合理的CPU时间，避免饿死？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可预测性/满足截止时间:&lt;/strong&gt; 对于实时系统，哪个算法更能保证任务按时完成？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法开销:&lt;/strong&gt; 算法本身的计算复杂度以及它导致的上下文切换频率和开销如何？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现复杂度:&lt;/strong&gt; 算法是否容易实现和调试？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对参数的敏感性:&lt;/strong&gt; 算法性能是否严重依赖于某些参数 (如时间片大小、优先级设置) ？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;比较方法:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;确定性建模 (Deterministic Modeling):&lt;/strong&gt; 给定一组特定的进程及其属性 (到达时间、CPU burst) ，模拟运行不同算法，计算性能指标。简单但只反映特定场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排队论建模 (Queueing Models):&lt;/strong&gt; 使用数学方法 (基于概率分布描述进程到达和CPU burst) 来分析平均性能。能提供理论洞察但模型可能简化现实。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟 (Simulation):&lt;/strong&gt; 编写程序模拟操作系统调度行为，使用随机生成的进程或真实系统负载的轨迹 (trace) 作为输入。灵活且能反映动态行为，是常用的评估方法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实际系统测量 (Implementation &amp;#x26; Measurement):&lt;/strong&gt; 在真实操作系统中实现算法，运行基准测试 (Benchmark) 或实际负载进行测量。最准确但成本最高。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;机制和策略分离的原则在调度算法中的应用&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;机制与策略分离 (Separation of Mechanism and Policy)&lt;/strong&gt; 是一个重要的操作系统设计原则，也适用于调度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;机制 (Mechanism):&lt;/strong&gt; 提供&lt;strong&gt;如何做 (How)&lt;/strong&gt; 的基础能力或工具。在调度中，机制包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上下文切换的代码 (保存/加载寄存器、切换页表) 。&lt;/li&gt;
&lt;li&gt;维护进程状态 (就绪、运行、阻塞) 和PCB的数据结构。&lt;/li&gt;
&lt;li&gt;管理就绪队列 (如链表、优先级队列、红黑树) 。&lt;/li&gt;
&lt;li&gt;时钟中断处理程序。&lt;/li&gt;
&lt;li&gt;提供设置和读取进程优先级的接口。&lt;/li&gt;
&lt;li&gt;进程挂起和唤醒的原子操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;策略 (Policy):&lt;/strong&gt; 决定&lt;strong&gt;做什么 (What)&lt;/strong&gt; 或&lt;strong&gt;何时做 (When)&lt;/strong&gt;。在调度中，策略是指具体的调度算法逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何选择下一个运行的进程 (FCFS规则？SJF规则？RR规则？优先级规则？) 。&lt;/li&gt;
&lt;li&gt;时间片长度是多少？&lt;/li&gt;
&lt;li&gt;优先级如何确定？是静态还是动态调整？如何调整？&lt;/li&gt;
&lt;li&gt;何时进行抢占？&lt;/li&gt;
&lt;li&gt;如何处理不同类型的进程 (前台/后台，实时/普通) ？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;应用与好处:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;灵活性与可扩展性:&lt;/strong&gt; 将调度算法 (策略) 与底层的上下文切换等 (机制) 分开，使得修改或更换调度策略更加容易，而无需改变底层的核心机制代码。例如，Linux内核允许通过&lt;code&gt;sched_setscheduler()&lt;/code&gt;系统调用为进程选择不同的调度策略 (如&lt;code&gt;SCHED_FIFO&lt;/code&gt;, &lt;code&gt;SCHED_RR&lt;/code&gt;, &lt;code&gt;SCHED_NORMAL/CFS&lt;/code&gt;) ，但它们都使用相同的底层上下文切换机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化:&lt;/strong&gt; 代码结构更清晰，职责分明。调度策略模块可以独立开发、测试和更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可定制性:&lt;/strong&gt; 用户或管理员可以更容易地根据特定需求调整调度策略参数，甚至在某些系统中插入自定义的调度模块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;例子:&lt;/strong&gt;
调度器的主循环可能是一个通用的框架 (机制) ，它调用一个函数 (策略) 来选择下一个进程。不同的调度算法可以通过实现这个选择函数来插入。上下文切换函数是另一个独立的机制。优先级队列的实现 (如使用堆或链表) 是机制，而如何利用这个队列 (是按优先级取还是按FIFO取) 是策略。&lt;/p&gt;
&lt;h4&gt;实例操作系统的调度算法都是什么？&lt;/h4&gt;
&lt;p&gt;现代通用操作系统通常采用复杂且混合的调度策略，以平衡各种需求。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主要调度器 (针对普通进程): 完全公平调度器 (Completely Fair Scheduler, CFS)&lt;/strong&gt;，自内核2.6.23起引入。
&lt;ul&gt;
&lt;li&gt;目标：为所有运行中的任务提供尽可能公平的CPU时间份额。&lt;/li&gt;
&lt;li&gt;机制：不再基于固定时间片，而是维护每个任务的&lt;strong&gt;虚拟运行时间 (vruntime)&lt;/strong&gt;。总是选择vruntime最小的任务来运行。任务运行会增加其vruntime。I/O等待的任务vruntime增长慢，因此返回时更容易被选中。&lt;/li&gt;
&lt;li&gt;实现：使用红黑树来高效地找到vruntime最小的任务。&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;SCHED_FIFO&lt;/code&gt;: 静态优先级的先来先服务 (非时间片轮转) 。相同优先级的任务按到达顺序执行，直到阻塞、退出或被更高优先级抢占。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SCHED_RR&lt;/code&gt;: 静态优先级的轮转法。同&lt;code&gt;SCHED_FIFO&lt;/code&gt;，但增加了时间片，同一优先级任务轮流运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优先级：实时任务优先级高于普通任务。普通任务也有优先级 (nice值) ，但CFS主要通过vruntime实现公平性，nice值影响vruntime增长的速度。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Windows (NT内核及以后):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采用&lt;strong&gt;基于优先级&lt;/strong&gt;的&lt;strong&gt;抢占式&lt;/strong&gt;调度算法，结合了&lt;strong&gt;多级反馈队列&lt;/strong&gt;的思想。&lt;/li&gt;
&lt;li&gt;优先级：分为32个优先级。0为系统空闲线程，1-15为&lt;strong&gt;可变优先级类&lt;/strong&gt; (Variable Priority Classes) ，16-31为&lt;strong&gt;实时优先级类&lt;/strong&gt; (Real-time Priority Classes) 。内核线程可能使用更高的内部优先级。&lt;/li&gt;
&lt;li&gt;动态调整：
&lt;ul&gt;
&lt;li&gt;对于可变优先级类，系统会&lt;strong&gt;动态提升&lt;/strong&gt;线程的优先级 (Priority Boost) ，例如：
&lt;ul&gt;
&lt;li&gt;当线程完成I/O操作时。&lt;/li&gt;
&lt;li&gt;当等待事件/信号量被满足时。&lt;/li&gt;
&lt;li&gt;前台窗口的线程优先级通常会被提升，以改善交互响应。&lt;/li&gt;
&lt;li&gt;短时间片用完后，优先级可能会暂时降低。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;长时间消耗CPU的线程其动态优先级会逐渐衰减回基础优先级。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;时间片：不同优先级的线程可能有不同的时间片长度。前台进程的时间片通常比后台进程长且可变。&lt;/li&gt;
&lt;li&gt;Quantum：Windows中称为Quantum，不是固定的，可以根据系统设置和前后台状态调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于XNU内核 (混合了Mach微内核和BSD Unix) 。&lt;/li&gt;
&lt;li&gt;调度也采用&lt;strong&gt;基于优先级&lt;/strong&gt;的&lt;strong&gt;抢占式&lt;/strong&gt;模型，具有&lt;strong&gt;多级反馈&lt;/strong&gt;特性。&lt;/li&gt;
&lt;li&gt;线程优先级分为几个主要波段 (bands)：内核模式、系统高优先级、用户交互 (UI响应关键) 、用户启动、后台任务等。&lt;/li&gt;
&lt;li&gt;动态调整：系统会根据线程的行为 (如CPU使用情况、是否阻塞等待I/O、是否与用户界面交互) 动态调整其优先级，类似于Windows。&lt;/li&gt;
&lt;li&gt;也使用了类似Mach的线程调度原语和概念，例如时间片捐赠 (Time-sharing donation) 等机制来优化性能。&lt;/li&gt;
&lt;li&gt;近年来引入了&lt;strong&gt;服务质量 (Quality of Service, QoS)&lt;/strong&gt; 类的概念，让开发者可以指定任务的意图 (如用户交互、后台数据处理、维护任务) ，系统据此进行更智能的资源 (包括CPU调度) 管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;总结:&lt;/strong&gt; 现代主流操作系统都使用抢占式、基于优先级的调度框架，并结合动态优先级调整、多级队列/反馈机制，以及针对公平性 (如Linux CFS) 或服务质量 (如macOS QoS) 的特定优化，以适应通用计算环境中复杂多变的需求。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 处理器调度的基本概念&lt;/h2&gt;
&lt;h3&gt;2.1 调度的三个层次&lt;/h3&gt;
&lt;p&gt;操作系统中的调度可以发生在不同层面，对应不同的资源管理和时间尺度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;长程调度 (Long-term Scheduling / 作业调度):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时机:&lt;/strong&gt; 创建新进程时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;决策:&lt;/strong&gt; 决定是否将新创建的进程纳入当前活跃进程集合 (即是否允许进入内存和就绪队列) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 控制系统的并发度 (道数) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中程调度 (Medium-term Scheduling / 内存调度):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时机:&lt;/strong&gt; 内存资源紧张或需要优化内存使用时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;决策:&lt;/strong&gt; 决定哪些进程的部分或全部从内存换出到外存 (挂起) ，以及何时将挂起的进程换回内存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 提高内存利用率和系统吞吐量，通过交换 (Swapping) 技术实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短程调度 (Short-term Scheduling / CPU调度 / 微观调度):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时机:&lt;/strong&gt; 发生特定事件 (如中断、系统调用、进程阻塞/唤醒、时间片用完等) 后，需要选择下一个占用CPU的进程时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;决策:&lt;/strong&gt; 从就绪队列中选择一个进程/线程，将CPU的使用权分配给它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;频率:&lt;/strong&gt; 非常频繁，通常在毫秒级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要求:&lt;/strong&gt; 实现必须高效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;联系:&lt;/strong&gt; 这三个层次的调度相互关联，共同管理进程从创建到完成的整个生命周期及其资源使用。&lt;/p&gt;
&lt;h3&gt;2.2 处理器调度的定义与场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 控制和协调多个进程对CPU资源的竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;场景:&lt;/strong&gt; 系统中有 N 个进程处于就绪状态，等待在 M 个CPU (M ≥ 1) 上运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务:&lt;/strong&gt; 调度程序 (内核函数) 根据特定的调度算法，从就绪队列中选择一个进程，并将CPU使用权交给它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idle进程:&lt;/strong&gt; 如果就绪队列为空 (没有可运行的用户或系统进程) ，系统会调度一个特殊的&lt;strong&gt;空闲进程 (idle process)&lt;/strong&gt; 来运行，它通常执行一些低优先级任务 (如系统监控、节能) 或简单地循环等待中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 调度需要解决的核心问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;调度时机 (When):&lt;/strong&gt; 何时进行处理器分配决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度算法 (What):&lt;/strong&gt; 依据何种原则挑选进程/线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度过程 (How):&lt;/strong&gt; 如何完成CPU的分配，即上下文切换。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.4 CPU调度的时机&lt;/h3&gt;
&lt;p&gt;调度通常在以下事件发生后，内核处理完相应事件并准备返回用户态之前的最后时刻进行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程生命周期变化:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;进程执行完毕并退出 (&lt;code&gt;exit()&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;进程因错误或异常而终止 (&lt;code&gt;abort&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;创建新进程 (&lt;code&gt;fork()&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程状态转换:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;运行进程因等待I/O或资源而进入阻塞态 (&lt;code&gt;wait()&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;阻塞进程被唤醒，回到就绪态 (I/O完成中断)。&lt;/li&gt;
&lt;li&gt;运行进程用完分配的时间片，回到就绪态 (时钟中断)。&lt;/li&gt;
&lt;li&gt;运行进程主动放弃 CPU (&lt;code&gt;yield()&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;I/O 中断。&lt;/li&gt;
&lt;li&gt;时钟中断 (用于时间片、计时器)。&lt;/li&gt;
&lt;li&gt;系统调用返回前。&lt;/li&gt;
&lt;li&gt;异常处理后。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;流程:&lt;/strong&gt; 事件发生 → 暂停当前进程 → 硬件响应 → 进入内核处理事件 → 事件处理结束 (可能导致进程状态变化、就绪队列调整) → 执行进程调度 → 选择新进程运行。&lt;/p&gt;
&lt;h3&gt;2.5 调度过程：上下文切换 (Context Switching)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 将CPU的控制权从一个进程 (或线程) 转移给另一个进程 (或线程) 的过程。这涉及到保存当前进程的状态并加载新进程的状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文 (Context):&lt;/strong&gt; 进程运行时，其执行状态 (硬件上下文) 保存在CPU的寄存器中 (如程序计数器PC, 程序状态字PSW, 栈指针SP, 通用寄存器等) 。进程不运行时，这些信息保存在其&lt;strong&gt;进程控制块 (PCB)&lt;/strong&gt; 中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主要工作:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;切换地址空间:&lt;/strong&gt; 修改页目录寄存器 (如CR3 on x86) 以指向新进程的页表，加载新的虚拟地址空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切换内核栈和硬件上下文:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;保存当前进程的寄存器值到其PCB或内核栈。&lt;/li&gt;
&lt;li&gt;从新进程的PCB或内核栈中恢复其寄存器值到CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核栈 (Kernel Stack):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;每个进程都有自己的内核栈，用于在进程执行内核代码时存储函数调用、局部变量和上下文信息。&lt;/li&gt;
&lt;li&gt;当进程从用户态切换到内核态 (如系统调用、中断) 时，CPU会自动切换到该进程的内核栈。&lt;/li&gt;
&lt;li&gt;内核栈位于内核地址空间，对用户程序不可见，大小通常是固定的 (如 Linux 中为 8KB 或 16KB) 。&lt;/li&gt;
&lt;li&gt;内核栈的地址通常保存在进程的 PCB 中，在上下文切换时需要更新相关寄存器 (如栈指针) 指向新进程的内核栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体步骤 (进程A切换到进程B):&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;保存进程A的硬件上下文 (寄存器值) 。&lt;/li&gt;
&lt;li&gt;更新进程A的PCB (如状态改为就绪或阻塞，记录PC等) 。&lt;/li&gt;
&lt;li&gt;将进程A移入相应的队列 (就绪队列、等待队列) 。&lt;/li&gt;
&lt;li&gt;选择进程B作为下一个运行进程。&lt;/li&gt;
&lt;li&gt;更新进程B的PCB (状态改为运行) 。&lt;/li&gt;
&lt;li&gt;加载进程B的上下文 (恢复寄存器值，切换地址空间) 。&lt;/li&gt;
&lt;li&gt;开始执行进程B。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XV6 Context Switch Example (&lt;code&gt;swtch.S&lt;/code&gt;):&lt;/strong&gt; (此处展示汇编代码逻辑，具体代码略)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;swtch&lt;/code&gt; 函数接受两个参数：旧进程上下文指针 (&lt;code&gt;old&lt;/code&gt;) 和新进程上下文指针 (&lt;code&gt;new&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;它负责保存 &lt;code&gt;old&lt;/code&gt; 进程的callee-saved寄存器到其上下文结构中。&lt;/li&gt;
&lt;li&gt;然后，从 &lt;code&gt;new&lt;/code&gt; 进程的上下文结构中恢复callee-saved寄存器。&lt;/li&gt;
&lt;li&gt;最后，通过 &lt;code&gt;ret&lt;/code&gt; 指令返回，此时CPU将跳转到 &lt;code&gt;new&lt;/code&gt; 进程之前保存的PC地址， effectively switching execution flow. The key is switching the stack pointer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文切换开销 (Cost):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接开销:&lt;/strong&gt; 内核执行切换操作所花费的CPU时间。
&lt;ul&gt;
&lt;li&gt;保存和恢复寄存器。&lt;/li&gt;
&lt;li&gt;切换地址空间 (TLB Flush相关指令通常较昂贵) 。&lt;/li&gt;
&lt;li&gt;执行调度算法本身的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;间接开销:&lt;/strong&gt; 切换导致缓存性能下降。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU Cache 失效:&lt;/strong&gt; 新进程的代码和数据不在缓存中，需要从内存加载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLB (Translation Lookaside Buffer) 失效:&lt;/strong&gt; 地址翻译缓存失效，需要重新查询页表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓冲区缓存 (Buffer Cache) 可能失效:&lt;/strong&gt; 文件系统相关的缓存可能对新进程无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 处理器调度算法的设计&lt;/h2&gt;
&lt;h3&gt;3.1 不同操作系统类型的调度目标&lt;/h3&gt;
&lt;p&gt;调度算法的选择与操作系统的主要应用场景和目标密切相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;批处理系统 (Batch Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 通常运行长任务，无需用户交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高吞吐量 (Throughput):&lt;/strong&gt; 单位时间内完成的作业数量最大化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短周转时间 (Turnaround Time):&lt;/strong&gt; 作业从提交到完成的总时间最小化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高CPU利用率 (CPU Utilization):&lt;/strong&gt; 让CPU尽可能处于忙碌状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式系统 (Interactive Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 需要频繁与用户交互，用户等待输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;快速响应时间 (Response Time):&lt;/strong&gt; 从用户输入命令到系统首次给出反馈的时间要短 (通常要求低于50-150ms) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;均衡性 (Proportionality):&lt;/strong&gt; 用户感觉系统性能稳定，符合预期。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时系统 (Real-time Systems):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 任务有严格的时间限制 (截止时间, Deadline) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;满足最后期限 (Meeting Deadlines):&lt;/strong&gt; 关键任务必须在规定时间内完成 (硬实时) 或尽可能满足 (软实时) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可预测性 (Predictability):&lt;/strong&gt; 系统行为在时间上是确定的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 调度算法的设计考量&lt;/h3&gt;
&lt;p&gt;设计调度算法时，需在多个目标之间进行权衡 (Trade-off) ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户角度 (User-oriented):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;周转时间 (Turnaround Time):&lt;/strong&gt; T(completion) - T(arrival)。进程从进入系统到完成的总时间。&lt;strong&gt;目标：最小化平均周转时间。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应时间 (Response Time):&lt;/strong&gt; 从请求发出到第一次产生响应的时间。&lt;strong&gt;目标：最小化响应时间 (对交互式系统尤为重要) 。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后期限 (Deadline):&lt;/strong&gt; 实时任务必须在规定时间前完成。&lt;strong&gt;目标：确保满足所有 (硬实时) 或重要 (软实时) 的截止时间。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可预测性 (Predictability):&lt;/strong&gt; 任务运行时间稳定，尤其对实时系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统角度 (System-oriented):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;吞吐量 (Throughput):&lt;/strong&gt; 单位时间内完成的进程数量。&lt;strong&gt;目标：最大化吞吐量。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU 利用率 (CPU Utilization):&lt;/strong&gt; CPU忙于执行有效工作的时间百分比。&lt;strong&gt;目标：最大化CPU利用率。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平性 (Fairness):&lt;/strong&gt; 各进程获得合理的CPU时间份额，防止饥饿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;均衡性 (Balance):&lt;/strong&gt; 系统资源 (CPU, I/O设备等) 应保持忙碌，充分利用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强制优先级 (Enforcing Priorities):&lt;/strong&gt; 确保高优先级进程优先获得服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 调度算法的关键决策点&lt;/h3&gt;
&lt;p&gt;设计或选择调度算法时，需要考虑以下几个方面：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程优先级 (Priority):&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;静态优先级 (Static Priority):&lt;/strong&gt; 进程创建时指定，运行期间不变。简单，但可能不适应进程行为变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态优先级 (Dynamic Priority):&lt;/strong&gt; 进程优先级在运行过程中可以调整。例如，可以提升长时间等待的进程的优先级 (老化, Aging) ，或降低长时间占用CPU进程的优先级。更能适应系统变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PCB记录:&lt;/strong&gt; PCB中需要包含优先级信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;就绪队列组织 (Ready Queue Organization):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单一队列:&lt;/strong&gt; 所有就绪进程放在一个队列中，按某种顺序 (如FCFS、优先级) 排列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级队列 (Multiple Queues):&lt;/strong&gt; 按进程属性 (如优先级、类型) 划分多个队列。不同队列可采用不同调度策略。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按优先级排队:&lt;/strong&gt; 每个优先级一个队列。调度器先服务高优先级队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按类型排队:&lt;/strong&gt; 如前台 (交互) 进程队列、后台 (批处理) 进程队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;抢占 vs. 非抢占 (Preemptive vs. Non-preemptive):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非抢占式 (Non-preemptive / 不可剥夺):&lt;/strong&gt; 一旦进程获得CPU，它将一直运行，直到它自愿放弃 (完成、阻塞、&lt;code&gt;yield&lt;/code&gt;) 。适用于批处理，简单，但响应性差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抢占式 (Preemptive / 可剥夺):&lt;/strong&gt; 当前运行的进程可以被更高优先级的就绪进程或时钟中断强制中断，CPU被分配给新进程。适用于交互式和实时系统，响应性好，但有上下文切换开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;I/O密集型 vs. CPU密集型进程 (I/O-bound vs. CPU-bound):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I/O密集型:&lt;/strong&gt; 进程大部分时间在等待I/O操作完成，CPU计算时间短。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU密集型 (计算密集型):&lt;/strong&gt; 进程大部分时间在进行CPU计算，很少I/O操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度倾向:&lt;/strong&gt; 现代系统通常倾向于优先调度I/O密集型进程，以保持I/O设备忙碌，提高系统整体吞吐量和响应性。让I/O进程尽快发出下一个I/O请求，然后在其等待时运行CPU密集型进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;时间片 (Time Slice / Quantum):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 在抢占式调度 (特别是轮转RR) 中，分配给进程一次连续运行的最大CPU时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择:&lt;/strong&gt; 时间片大小的选择是一个重要的权衡：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;太长:&lt;/strong&gt; 接近非抢占，长任务会阻塞短任务，交互式响应变慢。退化为FCFS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;太短:&lt;/strong&gt; 频繁发生上下文切换，系统开销增大，有效工作比例下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合适的大小:&lt;/strong&gt; 通常需要在几十到几百毫秒之间，取决于系统负载、CPU速度、上下文切换开销和对响应时间的要求。应略大于典型的一次交互所需CPU时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;固定 vs. 可变:&lt;/strong&gt; 时间片可以是固定的，也可以根据进程优先级或行为动态调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;4. 典型的处理器调度算法&lt;/h2&gt;
&lt;h3&gt;4.1 批处理系统调度算法&lt;/h3&gt;
&lt;p&gt;主要目标：高吞吐量、低周转时间、高CPU利用率。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;先来先服务 (FCFS - First Come First Serve):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 按进程到达就绪队列的顺序进行调度。非抢占式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 公平 (按到达顺序) 、简单易实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;平均周转时间和平均等待时间可能很长，特别是当短进程排在长进程之后时 (护航效应, Convoy Effect) 。&lt;/li&gt;
&lt;li&gt;不利于I/O密集型进程 (长CPU进程运行时，I/O进程等待；I/O进程运行时，CPU空闲) 。&lt;/li&gt;
&lt;li&gt;对交互式用户不友好。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; 进程P1(24s), P2(3s), P3(3s) 按 P1, P2, P3 顺序到达。
&lt;ul&gt;
&lt;li&gt;执行顺序: P1 -&gt; P2 -&gt; P3&lt;/li&gt;
&lt;li&gt;完成时间: P1(24), P2(27), P3(30)&lt;/li&gt;
&lt;li&gt;周转时间: P1(24), P2(27), P3(30) -&gt; 平均 27s&lt;/li&gt;
&lt;li&gt;若按 P2, P3, P1 顺序调度:
&lt;ul&gt;
&lt;li&gt;执行顺序: P2 -&gt; P3 -&gt; P1&lt;/li&gt;
&lt;li&gt;完成时间: P2(3), P3(6), P1(30)&lt;/li&gt;
&lt;li&gt;周转时间: P2(3), P3(6), P1(30) -&gt; 平均 13s (显著改善)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最短作业优先 (SJF - Shortest Job First):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 选择预计运行时间最短的进程投入运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非抢占式 SJF:&lt;/strong&gt; 当前进程一直运行直到结束或阻塞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抢占式 SJF (最短剩余时间优先, SRTN - Shortest Remaining Time Next):&lt;/strong&gt; 当一个新进程到达，其预计总运行时间比当前进程的 &lt;em&gt;剩余&lt;/em&gt; 运行时间还短时，抢占当前进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 理论上可证明，在所有进程同时到达时，SJF(非抢占)具有最低的平均周转时间。SRTN通常比非抢占SJF的平均周转时间更短。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;需要预测未来:&lt;/strong&gt; 如何准确知道进程的运行时间？通常基于历史数据进行估计，可能不准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;饥饿 (Starvation):&lt;/strong&gt; 长进程可能永远得不到CPU，如果总有短进程到来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不公平:&lt;/strong&gt; 明显偏袒短进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子 (SRTN):&lt;/strong&gt;
| 进程 | 到达时刻 | 运行时间 |
| :--- | :------- | :------- |
| P1   | 0        | 7        |
| P2   | 2        | 4        |
| P3   | 4        | 1        |
| P4   | 5        | 4        |
&lt;ul&gt;
&lt;li&gt;0: P1 运行 (剩余 7)&lt;/li&gt;
&lt;li&gt;2: P2 到达 (剩余 4) &amp;#x3C; P1 (剩余 5)，P2 抢占 P1。P2 运行 (剩余 4)&lt;/li&gt;
&lt;li&gt;4: P3 到达 (剩余 1) &amp;#x3C; P2 (剩余 2)，P3 抢占 P2。P3 运行 (剩余 1)&lt;/li&gt;
&lt;li&gt;5: P3 完成。P4 到达 (剩余 4)。比较 P1(剩余 5), P2(剩余 2), P4(剩余 4)。P2 剩余时间最短，P2 运行 (剩余 2)&lt;/li&gt;
&lt;li&gt;7: P2 完成。比较 P1(剩余 5), P4(剩余 4)。P4 剩余时间最短，P4 运行 (剩余 4)&lt;/li&gt;
&lt;li&gt;11: P4 完成。只剩 P1，P1 运行 (剩余 5)&lt;/li&gt;
&lt;li&gt;16: P1 完成。&lt;/li&gt;
&lt;li&gt;执行序列: P1(0-2) -&gt; P2(2-4) -&gt; P3(4-5) -&gt; P2(5-7) -&gt; P4(7-11) -&gt; P1(11-16)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最高响应比优先 (HRRN - Highest Response Ratio Next):&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;响应比 R = (等待时间 + 预计运行时间) / 预计运行时间 = 1 + (等待时间 / 预计运行时间)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;试图在SJF和FCFS之间取得平衡。&lt;/li&gt;
&lt;li&gt;短进程：预计运行时间小，响应比增长快，容易被选中 (类似SJF) 。&lt;/li&gt;
&lt;li&gt;长进程：等待时间足够长后，响应比会提高，最终能获得CPU，避免了饥饿。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 仍需预测运行时间。计算响应比有额外开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抢占式HRRN?&lt;/strong&gt; 理论上可以，但每次事件 (如新进程到达) 都需要重新计算所有就绪进程的响应比并排序，开销较大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 交互式系统调度算法&lt;/h3&gt;
&lt;p&gt;主要目标：快速响应时间、均衡性、公平性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;时间片轮转 (RR - Round Robin):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 将所有就绪进程按FCFS排成队列。调度器选择队首进程，分配一个时间片 (quantum) 。进程用完时间片后，若未完成或阻塞，则移到队尾。抢占式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;公平：每个进程都能获得运行机会。&lt;/li&gt;
&lt;li&gt;响应时间快：短进程能较快完成或得到响应。非常适合分时系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;上下文切换开销：时间片过短会导致开销过大。&lt;/li&gt;
&lt;li&gt;性能与时间片长度密切相关。&lt;/li&gt;
&lt;li&gt;对周转时间不一定最优。对于运行时间相近的进程，RR的平均周转时间可能比FCFS差。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子 (时间片 q=20):&lt;/strong&gt; P1(53), P2(8), P3(68), P4(24)
&lt;ul&gt;
&lt;li&gt;执行序列: P1(0-20) -&gt; P2(20-28) -&gt; P3(28-48) -&gt; P4(48-68) -&gt; P1(68-88) -&gt; P3(88-108) -&gt; P4(108-112) -&gt; P1(112-125) -&gt; P3(125-145) -&gt; P3(145-153)&lt;/li&gt;
&lt;li&gt;平均等待时间 = ( (68-20)+(112-88) + (20-0) + (28-0)+(88-48)+(125-108) + (48-0)+(108-68) ) / 4 = (72 + 20 + 85 + 88) / 4 = 66.25 ms (假设到达时间为0)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;虚拟轮转 (Virtual RR - VRR):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动机:&lt;/strong&gt; RR对I/O密集型进程可能不公平。I/O进程经常在时间片未用完时就阻塞，返回就绪队列时排在队尾，下次获得CPU可能要等很久。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 维护一个辅助就绪队列 (如AUX队列) 。当一个进程因I/O阻塞完成而返回时，不放入主RR队列尾部，而是放入AUX队列头部。调度器优先检查AUX队列，若非空则调度AUX队首进程，给其一个&lt;strong&gt;较短&lt;/strong&gt;的时间片 (通常是其上次阻塞时剩余的时间片) ；若AUX队列为空，则按标准RR调度主队列。从AUX队列运行完时间片的进程回到主RR队列尾部。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 给I/O密集型进程更多机会运行，提高I/O设备利用率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;优先级调度 (Priority Scheduling):&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; 实现简单，能满足不同进程的紧急程度需求 (如系统进程&gt;用户进程，前台&gt;后台，I/O型&gt;CPU型) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;饥饿:&lt;/strong&gt; 低优先级进程可能永远无法运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级反转 (Priority Inversion):&lt;/strong&gt; 一个低优先级进程持有高优先级进程所需的资源 (如锁) ，导致高优先级进程被迫等待低优先级进程。更糟的是，如果此时有一个中等优先级的CPU密集型进程就绪，它会抢占低优先级进程，使得高优先级进程的等待时间变得更长甚至不可预测。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级反转解决方案:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优先级继承 (Priority Inheritance):&lt;/strong&gt; 当高优先级进程等待低优先级进程持有的资源时，暂时将低优先级进程的优先级提升到与高优先级进程相同，使其能尽快运行并释放资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先级天花板协议 (Priority Ceiling Protocol):&lt;/strong&gt; 给每个资源预设一个优先级上限 (等于可能使用该资源的所有进程中的最高优先级) 。当一个进程获得资源时，将其优先级提升到该资源的优先级上限。这能预防死锁并限制阻塞时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断禁止:&lt;/strong&gt; 在临界区执行期间禁止中断 (简单粗暴，在通用操作系统中通常不可取，但用于某些嵌入式或实时内核) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多级队列调度 (Multilevel Queue Scheduling):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 将就绪队列划分为多个独立的队列，每个队列有自己的调度算法和优先级。例如：
&lt;ul&gt;
&lt;li&gt;系统进程队列 (最高优先级, RR 或 FCFS)&lt;/li&gt;
&lt;li&gt;交互式进程队列 (中优先级, RR)&lt;/li&gt;
&lt;li&gt;批处理进程队列 (最低优先级, FCFS)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;调度器首先处理高优先级队列中的所有进程，然后才处理次高优先级队列，以此类推。队列之间通常是抢占式的 (高优先级队列进程可抢占低优先级队列进程) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 灵活性高，可以为不同类型的进程定制调度策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 进程通常被固定分配到一个队列，缺乏灵活性；低优先级队列可能饥饿。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多级反馈队列调度 (Multilevel Feedback Queue Scheduling - MFQ):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 结合了多级队列和动态优先级调整。进程可以在不同队列之间移动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型实现:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;设置多个优先级队列 (Q0, Q1, ..., Qn)，优先级 Q0 &gt; Q1 &gt; ... &gt; Qn。&lt;/li&gt;
&lt;li&gt;不同队列分配不同的时间片长度，优先级越高的队列时间片越短 (如 Q0=q, Q1=2q, Q2=4q...) 。&lt;/li&gt;
&lt;li&gt;新进程进入最高优先级队列 Q0。&lt;/li&gt;
&lt;li&gt;调度器总是先运行最高非空队列中的进程，同队列内通常用RR。&lt;/li&gt;
&lt;li&gt;如果进程在一个队列中用完了其时间片但未完成，它会被 &lt;strong&gt;降级&lt;/strong&gt; 到下一个较低优先级队列。&lt;/li&gt;
&lt;li&gt;如果进程在时间片未用完前因 &lt;strong&gt;阻塞&lt;/strong&gt; (如等待I/O) 而放弃CPU，当它再次就绪时，通常会回到 &lt;strong&gt;原来的&lt;/strong&gt; 队列 (或有时提升一级) ，以优待I/O密集型进程。(&lt;strong&gt;讨论点：&lt;/strong&gt; 回到原队列还是队首/队尾？提升吗？具体策略不同系统可能不同。)&lt;/li&gt;
&lt;li&gt;最低优先级队列通常采用FCFS或很长的时间片RR。&lt;/li&gt;
&lt;li&gt;(可选) 可以加入 &lt;strong&gt;老化 (Aging)&lt;/strong&gt; 机制：在低优先级队列等待过久的进程可以被提升到较高优先级队列，防止饥饿。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 非常灵活，能同时满足交互式 (响应快) 和批处理 (吞吐量) 的需求，能自动适应进程行为，是最常用的调度算法之一。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 设计和调优 (队列数量、时间片大小、升级降级策略) 比较复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其他交互式算法 (简述):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公平共享调度 (Fair-share Scheduling):&lt;/strong&gt; 不仅考虑单个进程，还考虑进程所属的用户或用户组，确保CPU时间在用户/组之间公平分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保证调度 (Guaranteed Scheduling):&lt;/strong&gt; 向用户承诺每个进程将获得 CPU 时间的 1/n (如果有n个进程) ，并跟踪进程实际获得的CPU时间，优先运行获得时间最少的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;彩票调度 (Lottery Scheduling):&lt;/strong&gt; 给每个进程分配一定数量的“彩票”，调度器随机抽取一张彩票，持有该彩票的进程获得CPU。进程持有的彩票越多，获得CPU的机会越大。优先级可以通过分配不同数量的彩票来体现。简单，易实现概率公平。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 实时系统调度算法&lt;/h3&gt;
&lt;p&gt;主要目标：满足任务截止时间、可预测性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可调度性分析:&lt;/strong&gt; 对于周期性实时任务，需要判断系统是否能在所有任务的截止时间内完成它们。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若有 m 个周期任务，任务 i 的周期为 Pi，每次执行需 Ci 的CPU时间，则一个简单的 (充分非必要) 可调度条件是：
&lt;strong&gt;Σ (Ci / Pi) ≤ 1&lt;/strong&gt; (CPU利用率不超过100%)&lt;/li&gt;
&lt;li&gt;更精确的条件取决于具体算法 (如RM, EDF) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;速率单调调度 (Rate-Monotonic Scheduling - RM):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型:&lt;/strong&gt; 静态优先级，抢占式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 任务的优先级根据其 &lt;strong&gt;周期&lt;/strong&gt; (Rate) 设定：周期越短 (频率越高) ，优先级越高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适用:&lt;/strong&gt; 周期性实时任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 简单，理论成熟，可进行精确的可调度性分析 (Liu &amp;#x26; Layland 条件：Σ(Ci/Pi) ≤ n(2^(1/n)-1))。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 仅适用于周期任务，对任务集利用率上限有要求 (不是100%)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;最早截止时间优先 (Earliest Deadline First - EDF):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类型:&lt;/strong&gt; 动态优先级，抢占式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略:&lt;/strong&gt; 调度器在每次调度时，选择就绪队列中 &lt;strong&gt;绝对截止时间 (Deadline)&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; 理论上是最优的动态优先级算法，只要系统总利用率 ≤ 1，EDF就能找到一个可行的调度 (如果存在的话) 。CPU利用率上限可达100%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 实现比RM复杂 (需要跟踪每个任务的截止时间) ，可能出现瞬时过载导致多米诺骨牌效应 (一个任务错过deadline可能导致后续任务都错过) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 各种调度算法比较总结&lt;/h3&gt;
&lt;p&gt;| 调度算法     | 选择依据       | 决策模式     | 吞吐量    | 响应时间                    | 开销     | 对进程影响                         | 饥饿问题      |
| :----------- | :------------- | :----------- | :-------- | :-------------------------- | :------- | :--------------------------------- | :------------ |
| &lt;strong&gt;FCFS&lt;/strong&gt;     | &lt;code&gt;max[w]&lt;/code&gt;       | 非抢占       | 不强调    | 可能很差 (长作业阻塞短作业) | 最小     | 对短进程/IO密集型不利              | 无            |
| &lt;strong&gt;RR&lt;/strong&gt;       | 固定时间片     | 抢占(时间片) | q过小则低 | 短进程好                    | 较小     | 公平                               | 无            |
| &lt;strong&gt;SJF&lt;/strong&gt;      | &lt;code&gt;min[s]&lt;/code&gt;       | 非抢占       | 高        | 短进程好                    | 可能较高 | 对长进程不利                       | 可能          |
| &lt;strong&gt;SRTN&lt;/strong&gt;     | &lt;code&gt;min[s-e]&lt;/code&gt;     | 抢占(到达时) | 高        | 好                          | 可能较高 | 对长进程不利                       | 可能          |
| &lt;strong&gt;HRRN&lt;/strong&gt;     | &lt;code&gt;max[(w+s)/s]&lt;/code&gt; | 非抢占       | 高        | 较好                        | 可能较高 | 平衡                               | 无            |
| &lt;strong&gt;Feedback&lt;/strong&gt; | 见算法思想     | 抢占(时间片) | 不强调    | 较好 (可调优)               | 可能较高 | 可优待IO密集型，可能对某些进程不利 | 可能 (需老化) |&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(表中 &lt;code&gt;w&lt;/code&gt;: 等待时间, &lt;code&gt;s&lt;/code&gt;: 总服务时间, &lt;code&gt;e&lt;/code&gt;: 已执行时间)&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. 调度中的重要原则与实践&lt;/h2&gt;
&lt;h3&gt;5.1 机制与策略分离 (Mechanism vs. Policy Separation)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;原则:&lt;/strong&gt; 将调度的具体实现 (&lt;strong&gt;机制&lt;/strong&gt;，如何进行上下文切换、如何管理队列等) 与调度的决策逻辑 (&lt;strong&gt;策略&lt;/strong&gt;，选择哪个进程运行、优先级如何确定等) 分离开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么?&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;灵活性:&lt;/strong&gt; 更容易修改或替换调度策略，而无需改变底层机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性:&lt;/strong&gt; 方便添加新的调度策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块化:&lt;/strong&gt; 代码结构更清晰，易于理解和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么做?&lt;/strong&gt; 操作系统内核提供通用的调度框架 (机制，如优先级队列、上下文切换函数) ，而具体的调度算法 (策略) 作为可配置或可插拔的模块实现。例如，Linux 的 &lt;code&gt;sched_class&lt;/code&gt; 结构就体现了这种思想。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 线程调度 (Thread Scheduling)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;背景:&lt;/strong&gt; 现代操作系统多数支持内核级线程。调度单元从进程变为线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户级线程 vs. 内核级线程:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户级线程:&lt;/strong&gt; 调度由用户空间的线程库管理，内核只看到一个进程。切换快，但一个线程阻塞会导致整个进程阻塞。无法利用多核。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核级线程:&lt;/strong&gt; 调度由内核管理，每个线程有自己的上下文。切换开销比用户级线程大，但比进程切换小。一个线程阻塞不影响其他线程。可以并发运行在多核上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度对象:&lt;/strong&gt; 内核调度器直接调度内核级线程。对于用户级线程，内核调度的是其所属的进程 (或承载用户线程的内核线程LWP) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. 实例：操作系统调度算法&lt;/h2&gt;
&lt;h3&gt;6.1 典型系统采用的算法概览&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UNIX (早期):&lt;/strong&gt; 动态优先级，基于nice值和CPU使用情况调整。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5.3BSD:&lt;/strong&gt; 多级反馈队列算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows:&lt;/strong&gt; 基于优先级的抢占式多任务调度 (细节见下) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux:&lt;/strong&gt; 抢占式调度，主要使用CFS (普通进程) 和实时调度策略 (实时进程)  (细节见下) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solaris:&lt;/strong&gt; 综合调度算法，支持多种调度类 (实时、分时、交互、系统等) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 Windows 线程调度&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调度单位:&lt;/strong&gt; 线程 (内核级线程)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心算法:&lt;/strong&gt; 基于 &lt;strong&gt;动态优先级&lt;/strong&gt; 的 &lt;strong&gt;抢占式&lt;/strong&gt; 调度，结合 &lt;strong&gt;时间配额 (Quantum)&lt;/strong&gt; 调整。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪队列:&lt;/strong&gt; 维护多个优先级队列 (0-31) 。系统总是选择当前最高非空优先级队列中的线程运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同优先级调度:&lt;/strong&gt; 同一优先级队列内部，线程按 &lt;strong&gt;时间片轮转 (RR)&lt;/strong&gt; 方式调度。&lt;/li&gt;
&lt;li&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;线程状态改变 (运行-&gt;阻塞, 阻塞-&gt;就绪, 运行-&gt;就绪) 。&lt;/li&gt;
&lt;li&gt;线程优先级改变。&lt;/li&gt;
&lt;li&gt;线程改变其处理器亲和性 (Affinity)。&lt;/li&gt;
&lt;li&gt;时间片用完。&lt;/li&gt;
&lt;li&gt;主动放弃 (&lt;code&gt;yield&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程优先级:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;共32个优先级级别 (0-31)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时优先级 (16-31):&lt;/strong&gt; 优先级固定不变。用于需要紧急响应的任务。最高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可变优先级 (1-15):&lt;/strong&gt; 线程有一个&lt;strong&gt;基本优先级 (Base Priority)&lt;/strong&gt;，其&lt;strong&gt;当前优先级 (Current Priority)&lt;/strong&gt; 可以在此基础上动态调整 (提升或降低) 。用于普通用户和系统线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统线程 (1-15中的一部分):&lt;/strong&gt; 用于操作系统内部任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零页线程 (0):&lt;/strong&gt; 特殊线程，优先级最低，用于在系统空闲时将物理内存页清零。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间配额 (Quantum):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;不是绝对时间值，而是以 &lt;strong&gt;配额单位 (quantum unit)&lt;/strong&gt; 的整数表示。系统时钟中断时递减。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Quantum&lt;/code&gt; 和 &lt;code&gt;QuantumReset&lt;/code&gt; 记录在 &lt;code&gt;KTHREAD&lt;/code&gt; 结构中。&lt;/li&gt;
&lt;li&gt;当线程用完时间配额：
&lt;ul&gt;
&lt;li&gt;如果 &lt;strong&gt;没有&lt;/strong&gt; 其他同优先级或更高优先级的线程就绪，Windows会 &lt;strong&gt;重新分配&lt;/strong&gt; 一个新的时间配额给该线程，让它继续运行 (避免不必要的切换) 。&lt;/li&gt;
&lt;li&gt;如果 &lt;strong&gt;有&lt;/strong&gt; 其他同优先级线程就绪，该线程移到其优先级队列的末尾，调度器选择下一个线程。&lt;/li&gt;
&lt;li&gt;如果用完时间配额 &lt;strong&gt;且&lt;/strong&gt; 优先级被降低，则会被抢占。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt; 调整时间配额 (而非仅优先级) 可以影响进程获得CPU时间的比例，而不会完全饿死其他进程。例如，给前台游戏进程更大配额，使其运行更流畅，同时后台计算任务也能获得一些CPU时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度数据结构:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;每个进程有默认优先级、亲和性、时间配额。&lt;/li&gt;
&lt;li&gt;每个线程有基本优先级、当前优先级、亲和性、时间配额。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dispatcher Ready List:&lt;/strong&gt; 包含32个就绪线程队列的数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KiDispatcherReadyListHead:&lt;/strong&gt; 指向就绪队列的指针数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ready Summary (就绪位图):&lt;/strong&gt; 一个32位掩码，每一位对应一个优先级队列，指示该队列是否为空。调度器通过查找第一个置位的位 (Find First Set bit, FFS) 快速找到最高优先级的非空队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idle Summary (空闲位图):&lt;/strong&gt; (多处理器) 位图，指示哪些处理器当前处于空闲状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度策略细节:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主动切换:&lt;/strong&gt; 进程自愿放弃CPU (阻塞、Yield等) 。&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;被抢占线程放回其 &lt;strong&gt;原&lt;/strong&gt; 优先级就绪队列的 &lt;strong&gt;队首&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;实时优先级线程被抢占，下次运行时获得完整时间配额。&lt;/li&gt;
&lt;li&gt;可变优先级线程被抢占，下次运行时继续执行剩余时间配额。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间配额用完:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;优先级不降低：若队列无其他线程则重置配额继续，否则移到队尾。&lt;/li&gt;
&lt;li&gt;优先级降低：移到新优先级的队列，可能被抢占。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程优先级提升 (Priority Boost):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 改善响应性、解决饥饿、提高吞吐量。仅针对可变优先级线程 (1-15)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发情况:&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;I/O操作完成:&lt;/strong&gt; 临时提升等待该I/O的线程优先级，幅度由设备驱动程序建议 (与设备响应要求相关) ，使其能快速处理数据。提升后时间配额会减1 (避免不公平利用I/O提升) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待事件或信号量结束:&lt;/strong&gt; 线程优先级提升1级 (不超过15) ，以补偿其等待时间。完成提升后的运行后，优先级会逐渐衰减回基本优先级。时间配额减1。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前台进程中的线程&lt;/strong&gt; 完成等待操作。&lt;/li&gt;
&lt;li&gt;因 &lt;strong&gt;窗口消息&lt;/strong&gt; (GUI活动) 而唤醒的线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;反饥饿:&lt;/strong&gt; 系统线程&quot;平衡集管理器&quot; (Balance Set Manager) 定期扫描，将等待过久 (如 &gt; 300时钟中断) 的线程优先级提升到15，并给予4倍时间配额。用完后优先级立即恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空闲线程 (Idle Thread):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;每个处理器核都有一个对应的空闲线程。优先级为0。&lt;/li&gt;
&lt;li&gt;当没有其他可运行线程时，调度器调度空闲线程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能:&lt;/strong&gt; 循环检测是否有工作要做：处理挂起的中断(DPCs)、检查是否有新就绪线程、调用HAL执行电源管理 (如让CPU进入低功耗状态) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 多处理器调度 (Multiprocessor Scheduling)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;特点:&lt;/strong&gt; 系统包含多个CPU (核) ，可共享负载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对称多处理 (SMP - Symmetric Multiprocessing):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;所有CPU地位平等，都可以运行内核代码和用户进程。&lt;/li&gt;
&lt;li&gt;每个CPU通常有自己的调度器实例。&lt;/li&gt;
&lt;li&gt;调度器访问共享数据结构 (如就绪队列) 需要同步 (锁、原子操作) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计挑战:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程/线程分配:&lt;/strong&gt; 决定哪个任务在哪个CPU上运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载均衡 (Load Balancing):&lt;/strong&gt; 使各CPU负载大致均匀，避免某些CPU过载而其他CPU空闲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理器亲和性 (Processor Affinity):&lt;/strong&gt; 尽量让一个进程/线程连续在同一个CPU上运行，以利用CPU缓存 (L1/L2 cache) 中已加载的数据和TLB条目，减少缓存失效带来的开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存一致性 (Cache Coherence):&lt;/strong&gt; 硬件机制 (如MESI协议) 确保多个CPU缓存中共享数据的副本是一致的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程分配策略:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态进程分配 (Static Assignment):&lt;/strong&gt; 进程从创建到结束都绑定在某个特定CPU上。每个CPU有自己的私有就绪队列。调度开销小，易于维护亲和性，但可能导致负载不均。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态进程分配 (Dynamic Assignment):&lt;/strong&gt; 进程可以在不同CPU之间迁移。通常有一个全局共享就绪队列，或各CPU有私有队列但允许任务迁移。负载均衡好，但调度开销大 (需要同步、迁移成本) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多核处理器问题:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存一致性:&lt;/strong&gt; 如上所述，硬件解决。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存亲和性:&lt;/strong&gt; 调度器需要考虑。让任务倾向于留在上次运行的核上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核间数据共享:&lt;/strong&gt; 需要高效的同步机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载均衡:&lt;/strong&gt; 需要策略在CPU间迁移任务。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;缓存亲和性 vs. 负载均衡:&lt;/strong&gt; 这是个权衡。过于强调亲和性可能导致负载失衡；过于频繁地迁移以追求负载均衡则会破坏缓存亲和性，增加开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并行计算/渲染:&lt;/strong&gt; 任务与数据绑定到核心可利用缓存，但计算量不均时需迁移任务以平衡负载，导致缓存失效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN:&lt;/strong&gt; 内容按地理位置缓存 (亲和性) ，但负载高时请求可能路由到其他节点 (破坏亲和性) 以均衡负载。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作窃取 (Work Stealing):&lt;/strong&gt; 一种常见的负载均衡策略。每个CPU维护一个本地任务队列 (通常是双端队列) 。CPU优先执行自己队列的任务。当一个CPU空闲时，它会随机选择另一个CPU，并从其任务队列的 &lt;strong&gt;尾部&lt;/strong&gt; “窃取” 一个任务来执行。 (被窃取的CPU从头部获取任务) 。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 实现了负载均衡，同时本地任务优先执行保证了一定的缓存亲和性，分布式决策减少了中心瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 仍有通信和同步开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实例:&lt;/strong&gt; Go语言的GMP调度器，Java的ForkJoinPool，Hadoop YARN。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 实时调度 (Real-time Scheduling) (回顾)&lt;/h3&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; 硬实时 (必须满足) vs. 软实时 (尽量满足)。周期性 vs. 偶发性 vs. 非周期性任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键参数:&lt;/strong&gt; 时间 (周期、执行时间、截止时间) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法:&lt;/strong&gt; RM (静态优先级, 周期短优先), EDF (动态优先级, 截止时间早优先)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.5 OpenEuler 多核调度技术 (简述)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础:&lt;/strong&gt; CPU调度是为保证并发性，通过调度程序(Scheduler)按调度策略(Policy)选择进程占用CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算法:&lt;/strong&gt; 结合使用 FIFO, RR, 优先级调度 (用于实时进程) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;普通进程:&lt;/strong&gt; 主要采用 &lt;strong&gt;CFS (Completely Fair Scheduler)&lt;/strong&gt; 算法，追求公平性，基于虚拟运行时间 (vruntime) 按优先级比例分配CPU时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多核调度:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;早期单队列问题:&lt;/strong&gt; 所有CPU共享一个队列。
&lt;ul&gt;
&lt;li&gt;策略一 (简单RR) ：进程在CPU间频繁迁移，破坏缓存亲和性。&lt;/li&gt;
&lt;li&gt;策略二 (带亲和性) ：尽量让进程留在一个CPU，但可能牺牲某些进程 (如E) 的公平性或导致负载失衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多队列调度:&lt;/strong&gt; 每个CPU维护自己的就绪队列 (如Q0 for CPU0, Q1 for CPU1) 。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 提高缓存亲和性，减少锁竞争。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;问题:&lt;/strong&gt; 可能导致负载失衡 (如一个队列空了，另一个还很忙) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移线程 (Migration Thread):&lt;/strong&gt; OpenEuler 使用迁移线程解决负载不均衡。每个CPU有一个 &lt;code&gt;migration/CPUID&lt;/code&gt; 内核线程。当检测到负载不均时 (如CPU0空闲，CPU1忙) ，CPU0可以向CPU1的停机工作队列 (stop machine workqueue) 添加一个任务，唤醒CPU1的迁移线程。该线程优先级很高，会立即执行迁移任务 (如将进程D从CPU1迁移到CPU0) ，从而实现负载均衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.6 Linux 进程调度&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;调度单位:&lt;/strong&gt; 线程 (内核级线程，Linux中称为&apos;进程&apos;或&apos;任务&apos;) 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程分类与调度策略:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实时进程 (Real-time Processes):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;要求：调度延迟最低，立即响应。&lt;/li&gt;
&lt;li&gt;策略：&lt;code&gt;SCHED_FIFO&lt;/code&gt; (静态优先级，非抢占式，除非更高优先级到达或阻塞)、&lt;code&gt;SCHED_RR&lt;/code&gt; (静态优先级，抢占式，带时间片轮转)。优先级范围 1-99。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;普通进程 (Normal Processes):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;包括交互式进程 (需要快速响应) 和批处理进程 (后台运行，容忍延迟) 。&lt;/li&gt;
&lt;li&gt;策略：&lt;code&gt;SCHED_NORMAL&lt;/code&gt; (也叫 &lt;code&gt;SCHED_OTHER&lt;/code&gt;), &lt;code&gt;SCHED_BATCH&lt;/code&gt;, &lt;code&gt;SCHED_IDLE&lt;/code&gt;。主要由 &lt;strong&gt;CFS (Completely Fair Scheduler)&lt;/strong&gt; 算法管理。优先级范围 100-139 (对应nice值 -20 到 +19)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Linux调度算法演化:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux 2.4:&lt;/strong&gt; 简单 O(n) 调度器。基于优先级和时间片。遍历整个运行队列找最高优先级进程。所有进程时间片用完后统一重新计算。对交互式进程通过剩余时间片补偿来提升优先级。&lt;strong&gt;缺点:&lt;/strong&gt; 扩展性差 (高负载时慢) ，交互性优化不完善，非抢占内核。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux 2.6 (早期): O(1) 调度器 (by Ingo Molnar):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;引入 active/expired 两个优先级数组队列。调度只需 O(1) 时间找到最高优先级非空队列。&lt;/li&gt;
&lt;li&gt;动态优先级基于静态优先级(nice值)和平均睡眠时间 bonus 计算，试图区分交互式/批处理。&lt;/li&gt;
&lt;li&gt;进程时间片用完后移入 expired 队列 (除非是特殊情况) 。active 队列空后，交换 active 和 expired 指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 区分交互式的启发式规则复杂难懂且易失效，代码难维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux 2.6 (中期): SD (Staircase Scheduler by Con Kolivas) / RSDL (Rotating Staircase Deadline Scheduler):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;追求公平，抛弃复杂动态优先级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SD:&lt;/strong&gt; 进程用完时间片后优先级降低一级 (下楼梯) ，到底后回到较高层并获更多时间片。交互进程睡眠时停留在高层，唤醒后响应快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSDL:&lt;/strong&gt; 引入 group quota (Tg) 和 expired 数组。高优先级组用完 Tg 后整体降级 (minor rotation) ，保证低优先级任务的可预测等待时间。时间片用完进 expired 队列。active 队列空或到底后触发 major rotation (交换 active/expired)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;影响:&lt;/strong&gt; 启发了CFS的公平思想。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux 2.6.23 至今: CFS (Completely Fair Scheduler by Ingo Molnar):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心思想:&lt;/strong&gt; 完全公平。理想情况下，每个进程获得 1/n 的CPU时间。不再区分交互式/批处理，不再使用固定时间片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟运行时间 (vruntime):&lt;/strong&gt; &lt;code&gt;vruntime&lt;/code&gt; 记录进程的加权运行时间。&lt;code&gt;vruntime&lt;/code&gt; 增长速度与实际运行时间成正比，与进程权重 (优先级) 成反比。
&lt;code&gt;vruntime ≈ 实际运行时间 * (NICE_0_LOAD / 进程权重)&lt;/code&gt;
(NICE_0_LOAD 是 nice=0 进程的权重)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度决策:&lt;/strong&gt; 总是选择就绪队列中 &lt;strong&gt;&lt;code&gt;vruntime&lt;/code&gt; 最小&lt;/strong&gt; 的进程运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据结构:&lt;/strong&gt; 使用 &lt;strong&gt;红黑树 (Red-Black Tree)&lt;/strong&gt; 存储就绪进程，按 &lt;code&gt;vruntime&lt;/code&gt; 排序。插入、删除、查找最小节点都是 O(log n) 时间。调度器取最左节点运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平性实现:&lt;/strong&gt; 优先级高的进程权重高，&lt;code&gt;vruntime&lt;/code&gt; 增长慢，更容易被选中；优先级低的进程权重低，&lt;code&gt;vruntime&lt;/code&gt; 增长快。最终达到按权重比例分配CPU时间的效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux 6.6+ (实验性/可选): EEVDF (Earliest Eligible Virtual Deadline First):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;对CFS的改进，旨在解决CFS在极短任务和延迟敏感任务上的一些问题，进一步改善延迟和公平性。它结合了虚拟时间和截止时间的概念。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CFS 调度器详解:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;task_struct&lt;/code&gt;:&lt;/strong&gt; Linux 进程/任务描述符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sched_entity&lt;/code&gt;:&lt;/strong&gt; 调度实体，嵌入 &lt;code&gt;task_struct&lt;/code&gt; 中，包含CFS调度所需信息 (如 &lt;code&gt;load_weight&lt;/code&gt; 权重, &lt;code&gt;rb_node&lt;/code&gt; 红黑树节点, &lt;code&gt;vruntime&lt;/code&gt; 等) 。一个 &lt;code&gt;sched_entity&lt;/code&gt; 可以代表一个任务或一个任务组 (用于组调度) 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;sched_class&lt;/code&gt;:&lt;/strong&gt; 调度类结构体，定义了一套调度器操作函数接口 (如 &lt;code&gt;enqueue_task&lt;/code&gt;, &lt;code&gt;dequeue_task&lt;/code&gt;, &lt;code&gt;pick_next_task&lt;/code&gt;) 。CFS, RT(FIFO/RR), Idle 都有自己的 &lt;code&gt;sched_class&lt;/code&gt; 实现。内核按优先级顺序查询 &lt;code&gt;sched_class&lt;/code&gt; 来决定使用哪个调度器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;cfs_rq&lt;/code&gt;:&lt;/strong&gt; CFS 运行队列，每个CPU有一个。包含红黑树 &lt;code&gt;tasks_timeline&lt;/code&gt; 和 &lt;code&gt;min_vruntime&lt;/code&gt; 等信息。&lt;code&gt;min_vruntime&lt;/code&gt; 记录该队列中所有进程的最小 &lt;code&gt;vruntime&lt;/code&gt;，作为新进程/唤醒进程 &lt;code&gt;vruntime&lt;/code&gt; 计算的基准。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;红黑树 (&lt;code&gt;rb_node&lt;/code&gt;, &lt;code&gt;rb_root tasks_timeline&lt;/code&gt;):&lt;/strong&gt; 按 &lt;code&gt;vruntime&lt;/code&gt; 组织就绪的 &lt;code&gt;sched_entity&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CFS 关键情景:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新进程创建 (&lt;code&gt;fork()&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vruntime&lt;/code&gt; 初始值通常设为当前 &lt;code&gt;cfs_rq-&gt;min_vruntime&lt;/code&gt; (或略大) ，确保新进程不会立即获得过多优势。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;父子 &lt;code&gt;vruntime&lt;/code&gt; 交换?&lt;/strong&gt; 如果设置了&lt;code&gt;sysctl_sched_child_runs_first&lt;/code&gt;，且父子在同CPU，父&lt;code&gt;vruntime&lt;/code&gt; &amp;#x3C; 子&lt;code&gt;vruntime&lt;/code&gt;，则交换，让子进程优先运行。&lt;/li&gt;
&lt;li&gt;插入红黑树。&lt;/li&gt;
&lt;li&gt;检查是否需要抢占当前进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程唤醒 (&lt;code&gt;wake_up_process()&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;调整 &lt;code&gt;vruntime&lt;/code&gt;：通常设为 &lt;code&gt;max(waker-&gt;vruntime, cfs_rq-&gt;min_vruntime - delta)&lt;/code&gt;，其中 &lt;code&gt;delta&lt;/code&gt; 是一个小的补偿值。确保进程不会因睡眠获得不公平优势，但也给予一定补偿使其尽快运行。&lt;/li&gt;
&lt;li&gt;插入红黑树。&lt;/li&gt;
&lt;li&gt;检查是否需要抢占当前进程 (如果唤醒进程 &lt;code&gt;vruntime&lt;/code&gt; 足够小) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟中断 (&lt;code&gt;scheduler_tick()&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;更新当前运行进程的 &lt;code&gt;vruntime&lt;/code&gt; (&lt;code&gt;actual_runtime * NICE_0_LOAD / weight&lt;/code&gt;) 。&lt;/li&gt;
&lt;li&gt;更新 &lt;code&gt;cfs_rq-&gt;min_vruntime&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检查当前进程是否已运行超过其“理想运行时间” (基于调度周期和权重计算得出) 。如果是，则设置抢占标记 (&lt;code&gt;TIF_NEED_RESCHED&lt;/code&gt;)，在中断返回前会调用 &lt;code&gt;schedule()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动调度 (&lt;code&gt;schedule()&lt;/code&gt;):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当前进程阻塞、&lt;code&gt;yield&lt;/code&gt; 或被标记抢占时调用。&lt;/li&gt;
&lt;li&gt;更新当前进程 &lt;code&gt;vruntime&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果当前进程仍是就绪态，将其重新插入红黑树。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;pick_next_task()&lt;/code&gt; 选择下一个运行进程：
&lt;ul&gt;
&lt;li&gt;按优先级查询 &lt;code&gt;sched_class&lt;/code&gt; (RT -&gt; CFS -&gt; Idle)。&lt;/li&gt;
&lt;li&gt;CFS 中，通常选择红黑树最左节点 (&lt;code&gt;vruntime&lt;/code&gt; 最小者) 。&lt;/li&gt;
&lt;li&gt;特殊情况：考虑 &lt;code&gt;cfs_rq-&gt;next&lt;/code&gt; (上次被抢占者) 和 &lt;code&gt;cfs_rq-&gt;last&lt;/code&gt; (刚运行完者) 的缓存亲和性，可能优先选择它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从红黑树中移除被选中进程的 &lt;code&gt;sched_entity&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;执行上下文切换 (&lt;code&gt;context_switch()&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CFS 与进程状态转换图示:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    New -- fork() --&gt; Ready(Ready State: In Red-Black Tree);
    Ready -- schedule() selects --&gt; Running(Running State);
    Running -- Block (I/O, wait) --&gt; Blocked(Blocked State);
    Blocked -- Wakeup --&gt; Ready;
    Running -- Timeslice Check in Tick / Preempted --&gt; Ready;
    Running -- exit() --&gt; Terminated(Terminated State);

    subgraph CFS Logic
        direction LR
        Ready -- select min vruntime --&gt; Running;
        Running -- update vruntime &amp;#x26; re-insert --&gt; Ready;
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. 重点小结&lt;/h2&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; I/O密集型 vs. CPU密集型。&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;ul&gt;
&lt;li&gt;批处理: FCFS, SJF/SRTN, HRRN。&lt;/li&gt;
&lt;li&gt;交互式: RR, Priority (含反转问题), 多级队列, 多级反馈队列。&lt;/li&gt;
&lt;li&gt;实时: RM, EDF。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键设计点:&lt;/strong&gt; 优先级 (静/动) 、队列组织、抢占、时间片。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心原则:&lt;/strong&gt; 机制与策略分离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实例分析:&lt;/strong&gt; Windows 线程调度 (优先级、时间配额、提升机制) 、Linux 进程调度 (演化、CFS核心思想、vruntime、红黑树) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多处理器调度:&lt;/strong&gt; SMP、负载均衡、缓存亲和性、工作窃取。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 03: Process and Thread Model</title><link>https://www.lyt0112.com/blog/operating_systems_note_03-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_03-zh</guid><description>Operating Systems Notes 03: 进程线程模型</description><pubDate>Thu, 27 Mar 2025 00:13:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;gemini-2.5-pro-preview-03-25&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 核心问题解答&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;怎样理解“进程是对CPU的抽象”这句话？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 物理CPU只有一个 (或有限个) ，但通过多道程序设计和操作系统的进程调度，可以让多个程序在宏观上“同时”运行。操作系统为每个运行的程序创建一个进程，并管理它们对CPU的使用 (分时复用) 。这使得每个进程都感觉自己仿佛独占了一个CPU (或一个虚拟CPU) 来执行自己的指令序列。因此，进程机制将一个或多个物理CPU虚拟化成了多个虚拟CPU，供多个程序并发执行，这是对CPU计算能力的抽象。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;何谓进程映像？进程有实体吗？在哪里？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; &lt;strong&gt;进程映像 (Process Image)&lt;/strong&gt; 是指进程在执行时的完整状态描述，是进程实体的静态体现。它包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序代码 (Code Segment):&lt;/strong&gt; 进程要执行的指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序数据 (Data Segment):&lt;/strong&gt; 程序使用的全局变量、静态变量等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序堆栈 (Stack):&lt;/strong&gt; 用于函数调用、局部变量存储。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆 (Heap):&lt;/strong&gt; 动态分配内存的区域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程控制块 (PCB):&lt;/strong&gt; 包含进程的所有管理信息 (状态、ID、寄存器值、资源列表等) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程是有实体的&lt;/strong&gt;。它的实体就是进程映像所包含的内存区域 (代码、数据、堆栈、堆) 以及在操作系统内核中的数据结构 (PCB) 。这些实体主要存在于 &lt;strong&gt;内存&lt;/strong&gt; 中 (代码、数据、堆栈、堆) 和 &lt;strong&gt;操作系统内核空间&lt;/strong&gt; (PCB及其相关数据结构) 。当进程被挂起时，部分映像可能被交换到 **磁盘 (交换空间) ** 上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;怎样描述进程？一个进程都有什么 (组成要素) ？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 描述一个进程主要通过其 &lt;strong&gt;进程控制块 (PCB)&lt;/strong&gt;。PCB是操作系统感知进程存在的唯一标志，包含了描述和控制进程运行所需的所有信息。&lt;/li&gt;
&lt;li&gt;一个进程的组成要素 (即进程映像) 包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序代码&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据集合&lt;/strong&gt; (全局变量、静态变量、动态分配的内存)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行上下文&lt;/strong&gt; (CPU寄存器值、程序计数器PC、程序状态字PSW、栈指针等)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程控制块 (PCB)&lt;/strong&gt; (包含进程标识符、状态、优先级、资源列表等)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建进程主要完成哪些工作？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 创建一个进程 (例如通过 &lt;code&gt;fork()&lt;/code&gt; 或 &lt;code&gt;CreateProcess&lt;/code&gt;) 主要包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分配进程标识符 (PID):&lt;/strong&gt; 给新进程一个唯一的ID。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;创建和初始化进程控制块 (PCB):&lt;/strong&gt; 分配PCB结构，并填入初始信息 (如PID、父进程ID、初始状态设为New或Ready、优先级等) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配地址空间:&lt;/strong&gt; 为进程分配独立的虚拟内存空间 (可能通过复制父进程空间或加载新程序) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载程序和数据:&lt;/strong&gt; 将可执行文件的代码和数据加载到进程的地址空间中 (&lt;code&gt;exec&lt;/code&gt; 的工作) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;初始化执行上下文:&lt;/strong&gt; 设置PC指向程序入口，初始化栈指针和寄存器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分配资源:&lt;/strong&gt; 分配进程所需的其他资源 (如文件描述符，继承自父进程或新创建) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态设置与调度:&lt;/strong&gt; 将进程状态设置为就绪态 (Ready)，并将其链入就绪队列，等待调度器分配CPU。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程的生命周期内都会经历哪些变化？怎样表示这些变化？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 进程在其生命周期中会经历状态的转换。基本的状态包括：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建态 (New):&lt;/strong&gt; 进程正在被创建。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪态 (Ready):&lt;/strong&gt; 具备运行条件，等待CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行态 (Running):&lt;/strong&gt; 正在CPU上执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待态/阻塞态 (Waiting/Blocked):&lt;/strong&gt; 等待某个事件 (如I/O完成) 而暂停执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;终止态 (Terminated):&lt;/strong&gt; 进程执行完毕或被终止，等待系统回收资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这些变化通常用 &lt;strong&gt;进程状态转换图&lt;/strong&gt; 来表示，图中的节点代表状态，有向边代表状态之间的转换及其触发条件 (如调度、等待事件、事件完成等) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**进程有哪些状态？进程状态之间的转换 (条件？操作？) **&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基本状态：&lt;/strong&gt; 运行态 (Running)、就绪态 (Ready)、等待态 (Waiting/Blocked)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他状态：&lt;/strong&gt; 创建态 (New)、终止态 (Terminated)。还可能引入挂起态 (Suspended Ready, Suspended Blocked)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见转换及条件/操作：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;New -&gt; Ready&lt;/code&gt;: OS完成进程创建的必要工作，资源基本到位，允许参与调度。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ready -&gt; Running&lt;/code&gt;: 进程被调度器 (Scheduler) 选中，获得CPU使用权。&lt;strong&gt;操作：&lt;/strong&gt; 上下文切换，恢复进程现场。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Running -&gt; Ready&lt;/code&gt;: 时间片用完；或被更高优先级的进程抢占。&lt;strong&gt;操作：&lt;/strong&gt; 上下文切换，保存进程现场。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Running -&gt; Waiting&lt;/code&gt;: 进程请求I/O操作或等待某一资源/事件。&lt;strong&gt;操作：&lt;/strong&gt; 进程主动调用阻塞原语 (e.g., &lt;code&gt;wait()&lt;/code&gt;)，保存现场，移入等待队列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Waiting -&gt; Ready&lt;/code&gt;: 进程等待的事件发生或资源可用 (如I/O完成) 。&lt;strong&gt;操作：&lt;/strong&gt; 中断处理程序或相关内核线程执行唤醒原语 (e.g., &lt;code&gt;wakeup()&lt;/code&gt;)，将进程移入就绪队列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Running -&gt; Terminated&lt;/code&gt;: 进程正常执行完毕或出错退出。&lt;strong&gt;操作：&lt;/strong&gt; 进程调用退出原语 (e.g., &lt;code&gt;exit()&lt;/code&gt;)，进入终止态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Terminated -&gt; Gone&lt;/code&gt;: OS回收进程所占资源 (PCB、内存等) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程状态转换的发生，是否一定导致另一个转换发生？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; &lt;strong&gt;是的，通常是这样。&lt;/strong&gt; 操作系统是一个动态系统，进程状态转换往往是相互关联的。
&lt;ul&gt;
&lt;li&gt;例如，一个进程从 &lt;strong&gt;Running -&gt; Waiting&lt;/strong&gt;，会释放CPU，这使得调度器可以选择另一个处于 &lt;strong&gt;Ready&lt;/strong&gt; 状态的进程，使其发生 &lt;strong&gt;Ready -&gt; Running&lt;/strong&gt; 的转换。&lt;/li&gt;
&lt;li&gt;一个进程从 &lt;strong&gt;Waiting -&gt; Ready&lt;/strong&gt; (如I/O完成)，它进入就绪队列，可能在未来某个时刻引发 &lt;strong&gt;Ready -&gt; Running&lt;/strong&gt; 的转换 (当它被调度时) 。&lt;/li&gt;
&lt;li&gt;一个进程 &lt;strong&gt;Running -&gt; Terminated&lt;/strong&gt;，会释放它占有的资源，这可能使得另一个 &lt;strong&gt;Waiting&lt;/strong&gt; (等待该资源) 的进程变为 &lt;strong&gt;Ready&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;操作系统给进程提供内存空间，该空间的地址是虚拟地址还是物理地址？为什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 操作系统提供给进程的地址空间是 &lt;strong&gt;虚拟地址空间 (Virtual Address Space)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原因：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隔离与保护:&lt;/strong&gt; 每个进程拥有独立的虚拟地址空间，一个进程无法直接访问另一个进程的内存，提供了安全保护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;地址空间扩展:&lt;/strong&gt; 虚拟地址空间可以大于物理内存，借助内存管理单元(MMU)和磁盘交换空间，给进程提供更大的可用地址范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存管理简化:&lt;/strong&gt; 操作系统可以更灵活地管理物理内存，例如将非连续的物理内存页映射到连续的虚拟地址空间，简化了内存分配和程序加载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序加载和链接简化:&lt;/strong&gt; 程序可以在编译链接时确定其在虚拟地址空间的布局，而无需关心实际加载到物理内存的哪个位置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&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; 操作系统内核通常使用特定的数据结构来描述进程的地址空间。例如：
&lt;ul&gt;
&lt;li&gt;在 &lt;strong&gt;Linux&lt;/strong&gt; 中，使用 &lt;code&gt;mm_struct&lt;/code&gt; 结构来表示一个进程的整个地址空间。&lt;code&gt;mm_struct&lt;/code&gt; 内部包含一个 &lt;code&gt;vm_area_struct&lt;/code&gt; (VMA) 的链表或树，每个 VMA 描述了虚拟地址空间中的一个连续区域 (段) ，包括其起止地址、访问权限 (读/写/执行) 、映射的文件 (如果有) 等信息。&lt;/li&gt;
&lt;li&gt;通过 &lt;strong&gt;页表 (Page Tables)&lt;/strong&gt; 或 &lt;strong&gt;段表 (Segment Tables)&lt;/strong&gt; 将虚拟地址映射到物理地址。这些表由硬件 (MMU) 使用，操作系统负责维护。&lt;/li&gt;
&lt;li&gt;可以通过 &lt;code&gt;cat /proc/&amp;#x3C;PID&gt;/maps&lt;/code&gt; 命令查看一个进程的虚拟地址空间布局和 VMA 信息。需要将 &lt;code&gt;&amp;#x3C;PID&gt;&lt;/code&gt; 替换为实际的进程 ID (可通过 &lt;code&gt;ps&lt;/code&gt; 命令查找)，否则会提示文件或目录不存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;为什么有了进程后又引入线程？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 引入线程主要是为了解决进程的以下不足：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发应用需求:&lt;/strong&gt; 许多应用内部包含多个并发执行的任务 (如Web服务器处理多个请求，GUI程序响应用户输入同时后台处理) 。用多进程实现这些任务，开销较大且通信复杂。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开销问题:&lt;/strong&gt; 创建进程、撤销进程、以及在进程间切换 (上下文切换) 都需要较大的时间和系统资源开销。线程作为“轻量级进程”，创建、销毁和切换的开销小得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信效率:&lt;/strong&gt; 同一进程内的线程共享地址空间和大部分资源，它们之间的通信 (通过共享内存) 非常高效，无需内核介入。进程间通信 (IPC) 通常需要内核的协调，更复杂且效率较低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能提升:&lt;/strong&gt; 在多核处理器上，同一进程的多个线程可以真正并行执行在不同的核心上，提高应用程序的吞吐量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;怎样实现线程机制？为什么有各种支持线程的方式？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 线程机制主要有三种实现方式：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户级线程 (User-Level Threads, ULT):&lt;/strong&gt; 线程的管理 (创建、调度、同步) 完全在用户空间由一个线程库来完成。内核对线程无感知，只管理进程。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 切换快 (不需内核模式) ，可自定义调度算法，可移植性好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 一个线程阻塞 (如系统调用) ，整个进程会阻塞；无法利用多核并行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心级线程 (Kernel-Level Threads, KLT):&lt;/strong&gt; 线程的管理由操作系统内核完成。内核知道每个线程的存在，并进行调度。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt; 一个线程阻塞不影响其他线程；可以利用多核并行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt; 线程创建、销毁、切换需要进入内核，开销比ULT大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混合实现 (Hybrid Implementation):&lt;/strong&gt; 结合了ULT和KLT。内核管理KLT，用户空间线程库将多个ULT映射到一个或多个KLT上。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 兼具两者的优点，但实现复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存在多种方式的原因：&lt;/strong&gt; 是在性能、并发能力、实现复杂度、系统资源消耗之间进行权衡的结果。
&lt;ul&gt;
&lt;li&gt;ULT优先考虑低开销和灵活性。&lt;/li&gt;
&lt;li&gt;KLT优先考虑真正的并发和对阻塞系统调用的处理。&lt;/li&gt;
&lt;li&gt;混合模型试图找到一个平衡点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程包Pthreads中相关的函数的功能？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答:&lt;/strong&gt; Pthreads (POSIX Threads) 是一个线程API标准，提供了一系列函数来管理线程：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pthread_create()&lt;/code&gt;: 创建一个新的线程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_exit()&lt;/code&gt;: 终止调用该函数的线程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_join()&lt;/code&gt;: 等待指定的线程终止。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_yield()&lt;/code&gt;: 主动让出CPU，让其他线程运行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_self()&lt;/code&gt;: 获取调用线程自身的线程ID。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_mutex_init()&lt;/code&gt;, &lt;code&gt;pthread_mutex_lock()&lt;/code&gt;, &lt;code&gt;pthread_mutex_unlock()&lt;/code&gt;, &lt;code&gt;pthread_mutex_destroy()&lt;/code&gt;: 互斥锁相关操作，用于保护临界区，实现线程互斥。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pthread_cond_init()&lt;/code&gt;, &lt;code&gt;pthread_cond_wait()&lt;/code&gt;, &lt;code&gt;pthread_cond_signal()&lt;/code&gt;, &lt;code&gt;pthread_cond_broadcast()&lt;/code&gt;, &lt;code&gt;pthread_cond_destroy()&lt;/code&gt;: 条件变量相关操作，用于线程间的同步 (等待某个条件满足) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中断/异常机制与进程线程模型的关联？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 中断和异常是操作系统得以实现进程/线程调度和管理的关键机制。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上下文切换触发:&lt;/strong&gt; 时钟中断 (Timer Interrupt) 使得操作系统可以剥夺当前运行进程/线程的CPU使用权 (时间片用完) ，进行调度，切换到其他就绪的进程/线程。这是实现分时复用的基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态转换:&lt;/strong&gt; I/O完成中断会通知操作系统，操作系统可以将等待该I/O的进程/线程从等待态转换为就绪态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用:&lt;/strong&gt; 进程通过执行特定的指令 (如 &lt;code&gt;syscall&lt;/code&gt; 或 &lt;code&gt;int 0x80&lt;/code&gt;) 产生异常 (陷阱 Trap) ，主动陷入内核态，请求操作系统服务 (如创建进程、读写文件、阻塞等待) 。内核处理完请求后，可能会进行调度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误处理:&lt;/strong&gt; 异常 (如除零、缺页故障 Page Fault) 也需要内核介入处理。缺页故障处理是虚拟内存管理的核心部分，可能导致进程阻塞 (等待页面从磁盘调入) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保存与恢复现场:&lt;/strong&gt; 发生中断/异常时，硬件和操作系统内核协作，必须保存当前进程/线程的执行上下文 (寄存器、PC、状态等) ，处理事件后，再恢复某个进程/线程 (可能是同一个，也可能是不同的) 的上下文继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;机制和策略分离的原则在进程线程模型中的体现？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有点像操作系统提供的 API 和调用这些 API 的策略之间的关系&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt; 机制与策略分离 (Separation of Mechanism and Policy) 是操作系统设计的重要原则，意指提供实现某种功能的基础能力 (机制) ，与决定何时、如何使用这些能力的决策逻辑 (策略) 分开。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程/线程状态管理 (机制)  vs. 调度算法 (策略) ：&lt;/strong&gt; 操作系统提供了进程/线程状态 (就绪、运行、等待等) 以及在它们之间转换的机制 (如阻塞/唤醒原语、上下文切换) 。但是，&lt;strong&gt;选择&lt;/strong&gt; 哪个就绪进程/线程投入运行，则是调度算法 (策略) 决定的 (如FIFO、轮转、优先级调度等) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂起/激活 (机制)  vs. 负载调节 (策略) ：&lt;/strong&gt; 操作系统提供将进程换出到磁盘 (挂起) 和换回内存 (激活) 的机制。但是，&lt;strong&gt;决定&lt;/strong&gt; 何时挂起哪个进程 (例如，为了降低内存压力或提高系统吞吐量) ，则是系统负载调节策略的一部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程实现 (机制)  vs. 应用并发模型 (策略) ：&lt;/strong&gt; 用户级线程库提供创建和管理线程的机制。应用程序&lt;strong&gt;如何&lt;/strong&gt; 利用这些线程来构建并发逻辑 (例如，线程池大小、任务分配方式) 则是应用层面的策略。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;协程是什么？为什么引入协程？协程怎么用？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解答：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;是什么：&lt;/strong&gt; &lt;strong&gt;协程 (Coroutine)&lt;/strong&gt; 是一种比线程更轻量级的用户态并发 (或协作式多任务) 实现方式。它们是可以在特定点暂停执行，并在稍后从同一点恢复执行的计算过程。协程之间的切换由程序员 (或协程库/语言运行时) 显式控制，通常发生在用户态，不需要内核介入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么引入：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极低的切换开销:&lt;/strong&gt; 协程切换完全在用户态进行，避免了内核态和用户态之间的切换以及内核调度，开销远小于线程切换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高并发能力:&lt;/strong&gt; 单个线程可以管理成千上万个协程，特别适合处理大量并发连接 (如网络服务器) 或I/O密集型任务，能有效减少线程数量和内存消耗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;简化异步编程:&lt;/strong&gt; 允许使用看似同步的代码风格来编写异步逻辑 (例如，使用 &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; 关键字) ，从而避免 &lt;strong&gt;回调地狱 (Callback Hell)&lt;/strong&gt;。回调地狱是指在传统异步编程中，当一个操作依赖于另一个异步操作的结果时，需要将后续操作放在前一个操作的回调函数中，如果存在多层依赖，就会形成层层嵌套的回调函数结构，导致代码难以阅读、理解和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么用：&lt;/strong&gt; 通常通过编程语言或第三方库提供的支持来使用。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;语言原生支持:&lt;/strong&gt; 如 Python (&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;), Go (&lt;code&gt;goroutine&lt;/code&gt;), C++20 (&lt;code&gt;co_await&lt;/code&gt;, &lt;code&gt;co_yield&lt;/code&gt;, &lt;code&gt;co_return&lt;/code&gt;), Rust (&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;库支持:&lt;/strong&gt; 通过特定的协程库在不支持原生协程的语言中使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用法:&lt;/strong&gt; 开发者定义协程函数，在需要等待的操作 (通常是I/O) 前使用特定关键字 (如 &lt;code&gt;await&lt;/code&gt; 或 &lt;code&gt;yield&lt;/code&gt;) 暂停当前协程，让出执行权给其他协程或事件循环，当操作完成后，协程从暂停点恢复执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;2. 进程模型 (Process Model)&lt;/h2&gt;
&lt;h3&gt;2.1 基本概念&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;顺序程序与顺序环境 (Sequential Program &amp;#x26; Environment)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;程序 (Program):&lt;/strong&gt; 指令或语句的序列，体现某种算法，是静态的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;顺序环境:&lt;/strong&gt; 系统中只有一个程序在运行，独占所有资源，执行不受外界干扰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序性:&lt;/strong&gt; 指令严格按程序规定顺序执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封闭性:&lt;/strong&gt; 程序运行时独占资源，不受外界干扰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可再现性:&lt;/strong&gt; 只要输入相同，程序执行结果总是相同，与速度无关。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多道程序设计 (Multiprogramming)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;允许多个程序 &lt;strong&gt;同时&lt;/strong&gt; 进入内存并 &lt;strong&gt;交替&lt;/strong&gt; 运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的:&lt;/strong&gt; 提高CPU利用率和系统整体效率。当一个程序等待I/O时，CPU可以切换去执行另一个程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;并发环境与并发程序 (Concurrent Environment &amp;#x26; Program)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发环境:&lt;/strong&gt; 一段时间间隔内，单处理器上有两个或以上程序&lt;strong&gt;同时处于开始运行但尚未结束的状态&lt;/strong&gt;，并且执行次序不确定。宏观上并行，微观上串行 (在单核CPU上) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发程序:&lt;/strong&gt; 在并发环境中执行的程序。&lt;/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;执行 -&gt; 停 -&gt; 执行&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源共享:&lt;/strong&gt; 多个程序可能共享系统资源 (CPU、内存、I/O设备) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可再现性:&lt;/strong&gt; 由于执行走停的时机和顺序不确定，以及共享资源可能被修改，程序执行结果可能与执行速度有关，变得不可再现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立性与制约性:&lt;/strong&gt; 程序各自独立运行，但也可能因共享资源或需要协作而相互制约。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序与计算不再一一对应:&lt;/strong&gt; 一个程序可能对应多次执行 (多个进程) ，一次执行也可能断续完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程 (Process)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;程序的一次执行过程。&lt;/li&gt;
&lt;li&gt;正在运行程序的抽象。&lt;/li&gt;
&lt;li&gt;操作系统进行 &lt;strong&gt;资源分配&lt;/strong&gt; 和 &lt;strong&gt;调度&lt;/strong&gt; 的 &lt;strong&gt;独立单位&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;具有独立功能的程序在某个数据集合上的一次运行活动。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程是对CPU的抽象:&lt;/strong&gt; 如前所述，它将物理CPU虚拟化为多个逻辑CPU。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源分配单位:&lt;/strong&gt; 系统资源 (如内存、文件句柄) 以进程为单位进行分配。每个进程通常拥有独立的 &lt;strong&gt;地址空间&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度单位:&lt;/strong&gt; 操作系统将CPU时间片调度给进程 (或进程中的线程) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程与程序的区别:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态 vs. 静态:&lt;/strong&gt; 进程是动态的 (有生命周期) ，程序是静态的 (文件) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并发描述:&lt;/strong&gt; 进程是描述并发的基本单位，程序不能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生命周期:&lt;/strong&gt; 进程是暂时的 (创建、运行、消亡) ，程序是相对长久的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对应关系:&lt;/strong&gt; 一个程序可以对应多个进程实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 进程模型详解&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程状态 (Process States)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三种基本状态:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运行态 (Running):&lt;/strong&gt; 进程占有CPU，并在CPU上运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪态 (Ready):&lt;/strong&gt; 进程已具备运行条件 (资源到位) ，但因无空闲CPU而等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待态 (Waiting/Blocked):&lt;/strong&gt; 进程因等待某一事件 (如I/O完成、信号量) 而暂时不能运行。&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;创建态 (New):&lt;/strong&gt; 进程正在被创建，OS已分配PCB，但尚未完成所有初始化或未被批准执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;终止态 (Terminated):&lt;/strong&gt; 进程已停止执行，等待OS回收资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;挂起态 (Suspended):&lt;/strong&gt; 进程映像被从内存移到外存 (磁盘) ，用于调节系统负载或用户请求。可以有 &lt;strong&gt;挂起就绪 (Suspended Ready)&lt;/strong&gt; 和 &lt;strong&gt;挂起阻塞 (Suspended Blocked)&lt;/strong&gt; 两种状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程状态转换模型 (State Transition Models)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;三状态模型:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    Ready(就绪) --&gt; |调度|Running(运行)
    Running --&gt; |时间片到/高优先级进程抢占|Ready
    Running --&gt; |等待事件|Waiting(阻塞)
    Waiting --&gt; |事件发生|Ready
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;五状态模型:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    New(创建) --&gt; |提交|Ready(就绪)
    Ready --&gt; |调度|Running(运行)
    Running --&gt; |时间片到/高优先级进程抢占|Ready
    Running --&gt; |等待事件|Waiting(阻塞)
    Waiting --&gt; |事件发生|Ready
    Running --&gt; |完成|Terminated(终止)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;七状态模型:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    New(创建) --&gt; |提交|Ready(就绪)
    Ready --&gt; |调度|Running(运行)
    Running --&gt; |时间片到/高优先级进程抢占|Ready
    Running --&gt; |等待事件|Waiting(阻塞)
    Waiting --&gt; |事件发生|Ready
    Running --&gt; |完成|Terminated(终止)
    Ready --&gt; |挂起Suspend|SReady(就绪挂起)
    SReady --&gt; |激活Activate|Ready
    Waiting --&gt; |挂起Suspend|SWaiting(阻塞挂起)
    SWaiting --&gt; |事件发生|SReady
    SWaiting --&gt; |激活Activate|Waiting
    New --&gt; |提交|SReady
    Running --&gt; |挂起Suspend|SReady
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Linux 进程状态:&lt;/strong&gt; 包括 &lt;code&gt;R (TASK_RUNNING)&lt;/code&gt; (运行或就绪), &lt;code&gt;S (TASK_INTERRUPTIBLE)&lt;/code&gt; (可中断睡眠), &lt;code&gt;D (TASK_UNINTERRUPTIBLE)&lt;/code&gt; (不可中断睡眠), &lt;code&gt;T (TASK_STOPPED)&lt;/code&gt; (停止), &lt;code&gt;Z (TASK_DEAD - ZOMBIE)&lt;/code&gt; (僵尸) 等。其状态模型与理论模型有所差异，更贴近实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;XV6 进程状态:&lt;/strong&gt; &lt;code&gt;UNUSED&lt;/code&gt;, &lt;code&gt;USED&lt;/code&gt;, &lt;code&gt;SLEEPING&lt;/code&gt;, &lt;code&gt;RUNNABLE&lt;/code&gt;, &lt;code&gt;RUNNING&lt;/code&gt;, &lt;code&gt;ZOMBIE&lt;/code&gt;。这是一个简化的教学模型。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不同模型的意义:&lt;/strong&gt; 体现了 &lt;strong&gt;机制和策略分离&lt;/strong&gt;，基础状态转换是机制，增加挂起等状态是为了实现更复杂的内存管理和负载均衡策略。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程控制块 (Process Control Block, PCB)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义:&lt;/strong&gt; 操作系统用于管理进程的核心数据结构，是进程存在的唯一标志。也称进程描述符。所有进程的PCB集合构成进程表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作用:&lt;/strong&gt; 保存进程状态、资源、上下文等信息，供OS进行调度和管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主要内容:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程描述信息:&lt;/strong&gt; PID (唯一标识), 进程名, 用户ID (UID), 进程组关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程控制信息:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;当前状态 (State)。&lt;/li&gt;
&lt;li&gt;优先级 (Priority)。&lt;/li&gt;
&lt;li&gt;CPU现场信息 (Context): 程序计数器 (PC), 各种CPU寄存器, 程序状态字 (PSW), 栈指针 (SP)。&lt;strong&gt;这是进程切换时需要保存和恢复的关键信息。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;调度相关信息 (如等待事件、时间片等)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所拥有的资源和使用情况:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;虚拟地址空间描述 (指向页表/段表的指针)。&lt;/li&gt;
&lt;li&gt;打开文件列表。&lt;/li&gt;
&lt;li&gt;I/O设备信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程间通信与同步信息:&lt;/strong&gt; 消息队列指针, 信号量等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记账信息:&lt;/strong&gt; CPU使用时间, 内存使用量等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具体实现:&lt;/strong&gt; 不同OS有不同结构，如 Linux 的 &lt;code&gt;task_struct&lt;/code&gt;, Windows 的 &lt;code&gt;EPROCESS&lt;/code&gt;/&lt;code&gt;KPROCESS&lt;/code&gt;/&lt;code&gt;PEB&lt;/code&gt;, Solaris 的 &lt;code&gt;proc_t&lt;/code&gt;。真实系统中的PCB结构非常庞大复杂。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程地址空间 (Process Address Space)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;概念:&lt;/strong&gt; 操作系统为每个进程分配的、独立的 &lt;strong&gt;虚拟内存&lt;/strong&gt; 范围。是对内存的抽象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型布局 (从低地址到高地址):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;代码段 (.text):&lt;/strong&gt; 存放程序指令，通常只读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据段 (.data, .bss):&lt;/strong&gt; 存放已初始化的全局/静态变量 (.data) 和未初始化的全局/静态变量 (.bss)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;堆 (Heap):&lt;/strong&gt; 动态内存分配区域 (&lt;code&gt;malloc&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt;)，向上增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;(文件映射区/共享库):&lt;/strong&gt; 加载动态链接库、内存映射文件等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈 (Stack):&lt;/strong&gt; 存放函数参数、局部变量、返回地址等，向下增长。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核空间:&lt;/strong&gt; 每个进程地址空间的高地址部分映射到操作系统的内核空间，供系统调用和中断处理使用 (用户态不可直接访问) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立性来源:&lt;/strong&gt; 每个进程有自己的页表/段表，将相同的虚拟地址映射到不同的物理内存页 (或相同的只读页，如共享库代码) 。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写时复制 (Copy-on-Write, COW):&lt;/strong&gt; &lt;code&gt;fork()&lt;/code&gt; 创建子进程时，并不立即复制整个地址空间，而是让父子进程共享物理页面，并将页面标记为只读。当任何一方尝试写入时，触发异常，内核才真正复制该页面，使其私有化。这极大地优化了 &lt;code&gt;fork()&lt;/code&gt; 的效率，特别是 &lt;code&gt;fork()&lt;/code&gt; 后立即 &lt;code&gt;exec()&lt;/code&gt; 的情况，因为&lt;code&gt;exec()&lt;/code&gt;会替换整个地址空间，使得大部分共享页面在被写入前就已被丢弃，从而避免了不必要的复制开销。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;COW异常的详细处理流程：&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写操作触发页面故障：&lt;/strong&gt; 当父进程或子进程尝试写入共享的只读页面时，CPU检测到违反内存保护，触发页面故障异常(page fault)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进入内核态：&lt;/strong&gt; CPU立即切换到内核态，保存当前上下文，并跳转到页面故障处理程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理：&lt;/strong&gt; 内核的页面故障处理程序检查故障原因，发现是COW页面的写操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面复制：&lt;/strong&gt; 内核为写操作进程分配一个新的物理页框，将原共享页面的内容完整复制到新页框中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表更新：&lt;/strong&gt; 修改发起写操作的进程的页表，将相关虚拟地址映射到新分配的物理页框，并设置为可写权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复执行：&lt;/strong&gt; 内核返回用户态，恢复被中断的进程执行，此时写操作可以正常进行，且不会影响另一进程的内存视图。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查看:&lt;/strong&gt; &lt;code&gt;cat /proc/&amp;#x3C;PID&gt;/maps&lt;/code&gt; (Linux) 可以查看进程的虚拟内存区域布局。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;进程队列 (Process Queues)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统通常根据进程状态将PCB组织在不同的队列中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;就绪队列 (Ready Queue):&lt;/strong&gt; 存放所有处于就绪态的进程PCB。调度器从中选择下一个要运行的进程。可能按优先级组织成多个队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;等待队列 (Waiting Queues):&lt;/strong&gt; 可能有多个，每个队列对应一个特定的等待事件 (如等待磁盘I/O、等待键盘输入、等待某个信号量) 。当进程等待某事件时，其PCB被移入相应的等待队列。&lt;/li&gt;
&lt;li&gt;进程状态的改变伴随着其PCB在不同队列间的移动。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.3 进程控制&lt;/h3&gt;
&lt;h4&gt;进程控制原语&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;原语 (Primitive) &lt;strong&gt;是完成某种特定功能的一段程序，具有&lt;/strong&gt;不可分割性&lt;/strong&gt;或&lt;strong&gt;不可中断性&lt;/strong&gt;，即原语的执行必须是连续的，在执行过程中不允许被中断，也称为**原子操作 (Atomic) **。&lt;/p&gt;
&lt;p&gt;进程控制操作完成进程各状态之间的转换，由具有特定功能的原语完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程创建原语&lt;/li&gt;
&lt;li&gt;进程撤销原语&lt;/li&gt;
&lt;li&gt;阻塞原语&lt;/li&gt;
&lt;li&gt;唤醒原语&lt;/li&gt;
&lt;li&gt;挂起原语&lt;/li&gt;
&lt;li&gt;激活 (解挂) 原语&lt;/li&gt;
&lt;li&gt;改变进程优先级原语&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;进程的生命周期&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;进程创建的时机：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统初始化时&lt;/li&gt;
&lt;li&gt;操作系统提供的服务&lt;/li&gt;
&lt;li&gt;交互用户登录系统&lt;/li&gt;
&lt;li&gt;由现有的进程派生出一个新进程&lt;/li&gt;
&lt;li&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;/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;ul&gt;
&lt;li&gt;正常结束&lt;/li&gt;
&lt;li&gt;给定时限到&lt;/li&gt;
&lt;li&gt;缺少内存&lt;/li&gt;
&lt;li&gt;存储器出界&lt;/li&gt;
&lt;li&gt;保护性出错 (写只读文件)&lt;/li&gt;
&lt;li&gt;算术错误&lt;/li&gt;
&lt;li&gt;超出时间 (进程等待超过对某事件的最大值)&lt;/li&gt;
&lt;li&gt;I/O 失败&lt;/li&gt;
&lt;li&gt;无效指令 (如试图执行数据)&lt;/li&gt;
&lt;li&gt;特权指令&lt;/li&gt;
&lt;li&gt;操作系统干预 (如当死锁发生时)&lt;/li&gt;
&lt;li&gt;父进程请求中止某一子进程&lt;/li&gt;
&lt;li&gt;父进程中止 (子进程也中止)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;进程控制操作&lt;/h4&gt;
&lt;h5&gt;进程的创建&lt;/h5&gt;
&lt;p&gt;进程创建的主要步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给新进程分配一个唯一标识(pid)以及进程控制块(PCB)&lt;/li&gt;
&lt;li&gt;为进程分配地址空间&lt;/li&gt;
&lt;li&gt;初始化进程控制块
&lt;ul&gt;
&lt;li&gt;设置默认值 (如：状态为 New，...)&lt;/li&gt;
&lt;li&gt;设置相应的队列指针 (如：把新进程加到就绪队列的链表中)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;创建或扩充其他数据结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同操作系统的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNIX：fork/exec&lt;/li&gt;
&lt;li&gt;WINDOWS：CreateProcess&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;进程的撤销&lt;/h5&gt;
&lt;p&gt;进程撤销的主要步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&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;li&gt;撤销该进程的PCB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同操作系统的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNIX：exit&lt;/li&gt;
&lt;li&gt;WINDOWS：ExitProcess&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;进程阻塞和进程唤醒&lt;/h5&gt;
&lt;p&gt;处于运行状态的进程，在其运行过程中期待某一事件发生 (如等待键盘输入、等待磁盘数据传输完成、等待其它进程发送消息) ，当被等待的事件未发生时，由进程自己执行阻塞原语，使自己由运行态变为阻塞态。&lt;/p&gt;
&lt;p&gt;不同操作系统的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UNIX：wait&lt;/li&gt;
&lt;li&gt;WINDOWS：WaitForSingleObject&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;UNIX系统设计的进程控制操作&lt;/h5&gt;
&lt;p&gt;UNIX系统提供了一系列系统调用来实现进程控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;fork()&lt;/strong&gt;: 通过复制调用进程来建立新的进程，是最基本的进程建立过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exec()&lt;/strong&gt;: 包括一系列系统调用，它们都是通过用一段新的代码覆盖原来的内存空间，实现进程执行代码的转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wait()&lt;/strong&gt;: 提供初级的进程同步措施，能使一个进程等待，直到另外一个进程结束为止&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exit()&lt;/strong&gt;: 用来终止一个进程的运行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些系统调用之间的关联 (shell、fork()、exec()、wait()) 体现了UNIX进程管理的设计哲学，通过简单而正交的原语组合实现复杂功能。&lt;/p&gt;
&lt;h4&gt;UNIX的fork()实现及优化&lt;/h4&gt;
&lt;p&gt;UNIX的fork()实现步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为子进程分配一个空闲的进程描述符 (proc结构)&lt;/li&gt;
&lt;li&gt;分配给子进程唯一标识pid&lt;/li&gt;
&lt;li&gt;以一次一页的方式复制父进程地址空间&lt;/li&gt;
&lt;li&gt;从父进程处继承共享资源，如打开的文件和当前工作目录等&lt;/li&gt;
&lt;li&gt;将子进程的状态设为就绪，插入到就绪队列&lt;/li&gt;
&lt;li&gt;对子进程返回标识符0&lt;/li&gt;
&lt;li&gt;对父进程返回子进程的pid&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;优化方案：&lt;/strong&gt;
Linux的解决方案是利用存储管理模块中的&quot;写时复制技术&quot;COW (Copy-On-Write) 对fork()进行了优化。&lt;/p&gt;
&lt;h4&gt;写时复制 (Copy-on-Write, COW) 技术&lt;/h4&gt;
&lt;p&gt;重新审视fork函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;虚拟内存和内存映射解释了fork如何为每个进程提供私有地址空间
&lt;ul&gt;
&lt;li&gt;fork()后跟exec()的常见情况的完美方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为新进程创建虚拟地址空间的步骤：
&lt;ul&gt;
&lt;li&gt;创建新的进程mm_struct、vm_area_struct、页表的精确副本&lt;/li&gt;
&lt;li&gt;将两个进程中的每个页面标记为只读&lt;/li&gt;
&lt;li&gt;将两个进程中的每个vm_area_struct标记为私有COW&lt;/li&gt;
&lt;li&gt;返回时，每个进程都有虚拟内存的精确副本&lt;/li&gt;
&lt;li&gt;后续写入使用COW机制创建新页面&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 线程模型 (Thread Model)&lt;/h2&gt;
&lt;h3&gt;3.1 线程的引入&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;为什么引入线程？&lt;/strong&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;/li&gt;
&lt;li&gt;&lt;strong&gt;Web服务器:&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;/li&gt;
&lt;li&gt;线程是轻量级的，其创建、销毁、切换开销小得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能的考虑:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;通信效率:&lt;/strong&gt; 同一进程的线程共享地址空间和资源，通信 (共享内存) 非常高效，无需内核干预。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行计算:&lt;/strong&gt; 在多核CPU上，同一进程的多个线程可以真正并行执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.2 线程的基本概念&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;线程 (Thread)&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;CPU调度&lt;/strong&gt; 的基本单位。&lt;/li&gt;
&lt;li&gt;有时称为 &lt;strong&gt;轻量级进程 (Lightweight Process, LWP)&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;&lt;strong&gt;拥有独立的状态:&lt;/strong&gt; (Running, Ready, Blocked等)，需要进行状态转换管理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥有独立的执行上下文:&lt;/strong&gt; 程序计数器 (PC), 寄存器集合, 栈 (Stack) 和栈指针 (SP)。&lt;strong&gt;线程切换时保存/恢复的是这部分私有上下文。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享所在进程的资源:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;地址空间 (代码段、数据段、堆) 。&lt;/li&gt;
&lt;li&gt;打开的文件。&lt;/li&gt;
&lt;li&gt;全局变量。&lt;/li&gt;
&lt;li&gt;信号处理器等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以创建、撤销、同步其他线程。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多线程进程模型:&lt;/strong&gt; 一个进程包含一个PCB和&lt;strong&gt;多个线程控制块 (Thread Control Block, TCB)&lt;/strong&gt;。所有TCB共享进程的地址空间和资源，但每个TCB有自己独立的PC、寄存器和栈。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.3 线程的实现&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用户级线程 (User-Level Threads, ULT)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 在用户空间通过线程库实现，内核对线程无感知。线程调度由库函数完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;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;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阻塞问题:&lt;/strong&gt; 如果一个用户级线程执行了阻塞式系统调用，整个进程都会被内核阻塞，即使其他线程是就绪的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多核利用问题:&lt;/strong&gt; 内核只把CPU分配给进程，所以一个进程中的多个ULT不能在多核上并行执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻塞处理:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;使用非阻塞系统调用。&lt;/li&gt;
&lt;li&gt;使用 &quot;Jacketing&quot; / &quot;Wrapper&quot; 技术：库函数在调用可能阻塞的系统调用前检查，如果会阻塞，则不调用，而是切换到另一个用户线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;核心级线程 (Kernel-Level Threads, KLT)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 线程的管理 (创建、调度、同步) 由操作系统内核完成。内核维护每个线程的TCB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一个线程阻塞不影响进程内其他线程的执行。&lt;/li&gt;
&lt;li&gt;内核可以直接调度线程，可以在多核CPU上实现真正的并行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;线程的创建、销毁、切换都需要进入内核态，开销比ULT大 (但仍远小于进程切换) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; Windows 线程, Linux 的 NPTL (Native POSIX Thread Library，实际上是KLT)。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;混合模型 (Hybrid Implementation)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实现:&lt;/strong&gt; 内核支持KLT，用户空间线程库将多个ULT映射到少量KLT上 (M:N模型) 。线程创建在用户态快，调度利用内核。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; 早期的 Solaris。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目标:&lt;/strong&gt; 试图结合ULT的低开销和KLT的并发优势，但实现复杂，现在较少见，Linux和Windows都主要采用KLT模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;4. 协程 (Coroutine)&lt;/h2&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;为了在单线程内实现更高效率的并发，尤其是针对 I/O 密集型任务和需要管理大量连接的场景。&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;p&gt;&lt;strong&gt;协程是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种 &lt;strong&gt;用户态的、协作式&lt;/strong&gt; 的多任务实现。&lt;/li&gt;
&lt;li&gt;可以看作是比线程更轻量级的执行单元，由 &lt;strong&gt;程序员/运行时&lt;/strong&gt; 在 &lt;strong&gt;用户态&lt;/strong&gt; 控制切换。&lt;/li&gt;
&lt;li&gt;协程可以在执行过程中的特定点 &lt;strong&gt;暂停 (yield)&lt;/strong&gt;，然后在未来从同一点 &lt;strong&gt;恢复 (resume)&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;依赖于 &lt;strong&gt;编程语言或库&lt;/strong&gt; 的支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常见模式:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;定义协程函数 (如 Python 的 &lt;code&gt;async def&lt;/code&gt;) 。&lt;/li&gt;
&lt;li&gt;在协程函数内部，遇到需要等待的操作 (如异步I/O) 时，使用特定关键字 (如 &lt;code&gt;await&lt;/code&gt;, &lt;code&gt;yield&lt;/code&gt;) 主动让出控制权。&lt;/li&gt;
&lt;li&gt;一个事件循环 (Event Loop) 或调度器负责管理协程的暂停和恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;例子:&lt;/strong&gt; Python &lt;code&gt;asyncio&lt;/code&gt;, Go &lt;code&gt;goroutine&lt;/code&gt;, C++20 &lt;code&gt;coroutine&lt;/code&gt;, Rust &lt;code&gt;async/await&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;纤程 (Fiber)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 操作系统提供的一种类似协程的机制，也是用户态调度的轻量级执行单元，一个线程内可以包含多个纤程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;5. 重点小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进程 (Process):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;并发执行程序的实例，动态产生和消亡。&lt;/li&gt;
&lt;li&gt;OS &lt;strong&gt;资源分配&lt;/strong&gt; 的独立单位，拥有独立地址空间。&lt;/li&gt;
&lt;li&gt;基本特征：并发性、动态性、独立性、制约性、异步性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程映像&lt;/strong&gt; = 代码 + 数据 + 栈 + 堆 + PCB。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;线程 (Thread):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;进程内的一个执行流，&lt;strong&gt;CPU调度&lt;/strong&gt; 的基本单位。&lt;/li&gt;
&lt;li&gt;拥有私有执行上下文 (PC, Regs, Stack) ，共享进程资源 (地址空间, 文件) 。&lt;/li&gt;
&lt;li&gt;引入原因：应用并发需求、低开销、高性能 (通信、并行) 。&lt;/li&gt;
&lt;li&gt;实现方式：用户级 (ULT)、核心级 (KLT)、混合。各有优劣。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协程 (Coroutine):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用户态协作式多任务单元，比线程更轻量。&lt;/li&gt;
&lt;li&gt;切换开销极低，适合高并发I/O密集型任务。&lt;/li&gt;
&lt;li&gt;通过语言/库支持，用同步方式写异步代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可再入程序 (Reentrant Program):&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;可被多个进程同时调用的程序。&lt;/li&gt;
&lt;li&gt;特点：纯代码 (执行中不修改自身) ，不使用静态/全局变量存储可变状态 (或通过参数传入/线程本地存储) 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 02: Interrupt and Exception Mechanism</title><link>https://www.lyt0112.com/blog/operating_systems_note_02-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_02-zh</guid><description>Operating Systems Notes 02: 中断异常机制</description><pubDate>Thu, 13 Mar 2025 04:20:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;claude-3-7-sonnet-20250219&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 核心问题解答&lt;/h2&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;怎样理解“操作系统是由中断/异常/事件驱动的“这句话？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这句话的意思是操作系统的运行依赖于中断、异常和事件的触发。中断和异常是硬件或软件产生的信号，通知操作系统需要处理的事件。操作系统通过响应这些信号来管理系统资源和执行任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中断/异常的来源有什么不同？处理方式是一样的吗？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断通常由外部设备 (如键盘、鼠标、网络接口等) 产生，而异常通常由CPU在执行指令时检测到的错误 (如除零错误、非法指令等) 产生。处理方式有所不同，中断处理程序通常较为简单，主要负责响应外部设备的请求，而异常处理程序则需要更复杂的错误处理机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;回顾一下：ICS对异常的描述及分类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ICS (计算机系统结构) 将异常分为四类：陷入 (Trap) 、故障 (Fault) 、终止 (Abort) 和中断 (Interrupt) 。陷入是由用户程序主动发起的系统调用，故障是可恢复的错误，终止是不可恢复的错误，中断是由外部设备发起的请求。&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;/li&gt;
&lt;/ul&gt;
&lt;/li&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;操作系统初始化与中断/异常有哪些关联？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统初始化时会设置中断向量表、初始化中断控制器、注册中断/异常处理程序等。中断/异常机制是操作系统正常运行的重要保障。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;什么是软件异常？它是如何工作的？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;软件异常是由软件引发的异常情况，例如非法内存访问、除零错误等。软件异常通过硬件检测并触发相应的异常处理程序，操作系统负责处理这些异常并采取相应的措施，如终止进程、生成错误报告等。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;X86有哪些控制和状态寄存器？所起的作用是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X86处理器有多个控制和状态寄存器，包括CR0-CR4 (控制寄存器) 、EFLAGS (状态寄存器) 、GDTR/IDTR (全局/中断描述符表寄存器) 等。控制寄存器用于控制处理器的操作模式，状态寄存器保存处理器的状态标志，描述符表寄存器用于存储全局和中断描述符表的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;X86在PentiumII 300之后提供了sysenter/sysexit指令，为什么？与int 0x80/iret有什么不同？X86-64提供的系统调用指令是什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sysenter/sysexit指令提供了更高效的系统调用机制，减少了系统调用的开销。与int 0x80/iret相比，sysenter/sysexit指令不需要保存和恢复中断标志，减少了上下文切换的开销。X86-64提供的系统调用指令是syscall/sysret。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于基于x86体系结构的Linux的系统调用实现：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;系统调用入口程序system_call()与中断描述符表是什么关系？与系统调用表是什么关系？
&lt;ul&gt;
&lt;li&gt;system_call()是系统调用的入口程序，通过中断描述符表 (IDT) 中的中断向量指向。系统调用表 (sys_call_table) 存储了所有系统调用的地址，system_call()根据系统调用号查找并调用相应的系统调用处理程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统调用处理结束后，处理器转去执行哪个模块？
&lt;ul&gt;
&lt;li&gt;系统调用处理结束后，处理器会返回到用户态，继续执行被中断的用户程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;12&quot;&gt;
&lt;li&gt;系统调用与函数/过程调用的区别是什么？系统调用与C函数调用的区别？系统调用与API的关系？
&lt;ul&gt;
&lt;li&gt;系统调用是操作系统提供的接口，用于应用程序请求操作系统服务。函数/过程调用是程序内部的调用机制。系统调用与C函数调用的区别在于系统调用需要从用户态切换到内核态，而C函数调用在用户态内执行。API (应用程序编程接口) 是应用程序与操作系统或库函数之间的接口，系统调用是API的一部分，提供底层操作系统服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;关键核心：ECF——异常控制流&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;理解ECF (异常控制流) 是深入理解计算机系统和操作系统交互的关键。&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;ECF描述了应用程序在运行过程中如何通过系统调用、中断和异常与操作系统进行交互。通过理解ECF，可以更好地理解操作系统如何管理硬件资源和提供服务。&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;通过掌握ECF的原理，开发者可以编写更高效、更可靠的应用程序。理解系统调用和异常处理机制，可以帮助开发者优化程序性能，并处理各种异常情况。&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;ECF在并发编程中起着重要作用。通过理解中断和异常的处理流程，可以更好地设计和实现多线程、多进程的并发程序，确保程序的正确性和高效性。&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;软件异常是ECF的重要组成部分。通过理解软件异常的触发和处理机制，可以更好地调试和维护程序，提升程序的稳定性和安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;2. 中央处理器(CPU)&lt;/h2&gt;
&lt;h3&gt;2.1 关于寄存器&lt;/h3&gt;
&lt;p&gt;处理器由运算器、控制器、寄存器及高速缓存构成：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用户可见寄存器&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;机器语言可以直接访问&lt;/li&gt;
&lt;li&gt;数据寄存器(通用寄存器)&lt;/li&gt;
&lt;li&gt;地址寄存器&lt;/li&gt;
&lt;li&gt;条件码寄存器：保存CPU操作结果的标记位&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;/li&gt;
&lt;li&gt;程序计数器(PC, Program Counter)&lt;/li&gt;
&lt;li&gt;指令寄存器(IR, Instruction Register)&lt;/li&gt;
&lt;li&gt;程序状态字(PSW, Program Status Word)：存储处理器当前运行状态的关键寄存器，包含多种重要信息：
&lt;ul&gt;
&lt;li&gt;条件码标志位：如零标志(Zero)、进位标志(Carry)、溢出标志(Overflow)等，反映算术和逻辑运算的结果&lt;/li&gt;
&lt;li&gt;中断控制位：控制处理器对中断的响应方式，如中断使能/禁止标志&lt;/li&gt;
&lt;li&gt;处理器模式位：指示当前CPU运行在什么特权级别(如用户态/内核态)&lt;/li&gt;
&lt;li&gt;内存管理相关标志：如分页模式、虚拟内存使能等
PSW在进程上下文切换和中断处理过程中会被保存和恢复，是操作系统实现特权保护和进程隔离的核心机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 操作系统的需求之一 —— 保护&lt;/h3&gt;
&lt;h4&gt;从操作系统的特征考虑&lt;/h4&gt;
&lt;p&gt;操作系统需要处理并发和共享资源的问题，这就提出了对系统进行保护与控制的要求。为了实现这一点，操作系统依赖于硬件机制来隔离操作系统和用户程序。&lt;/p&gt;
&lt;h4&gt;硬件机制的支持&lt;/h4&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;/li&gt;
&lt;li&gt;通过特权级别，处理器可以区分内核态和用户态，从而控制哪些指令可以在特定模式下执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特权级别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特权级别决定了处理器可以执行哪些指令以及访问哪些资源。&lt;/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;h3&gt;2.3 处理器的状态(模式)&lt;/h3&gt;
&lt;p&gt;现代处理器通常将CPU状态划分为两种、三种或四种，在程序状态字寄存器PSW中设置位，根据运行程序对资源和指令的使用权限设置不同的CPU状态。&lt;/p&gt;
&lt;p&gt;例如X86架构中的EFLAGS寄存器，RISC-V的三种特权模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;机器模式(M模式)&lt;/li&gt;
&lt;li&gt;用户模式(U模式)&lt;/li&gt;
&lt;li&gt;监管模式(S模式)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.4 特权指令和非特权指令&lt;/h3&gt;
&lt;p&gt;操作系统需要两种CPU状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核态(Kernel Mode)&lt;/strong&gt;：运行操作系统程序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户态(User Mode)&lt;/strong&gt;：运行用户程序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;特权指令&lt;/strong&gt;：只能由操作系统使用、用户程序不能使用的指令
&lt;strong&gt;非特权指令&lt;/strong&gt;：用户程序可以使用的指令&lt;/p&gt;
&lt;p&gt;X86支持4个处理器特权级别(特权环 Ring)：R0、R1、R2和R3&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;R0相当于内核态，特权能力最高&lt;/li&gt;
&lt;li&gt;R3相当于用户态，特权能力最低&lt;/li&gt;
&lt;li&gt;目前大多数基于x86处理器的操作系统只用了R0和R3两个特权级别&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.5 CPU状态之间的转换&lt;/h3&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;：通过设置程序状态字PSW&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;陷入指令(访管指令, supervisor call)：提供给用户程序的接口，用于调用操作系统功能
例如：int, trap, syscall, sysenter/sysexit, ecall&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 中断机制&lt;/h2&gt;
&lt;p&gt;中断对于操作系统的重要性就如同汽车发动机、飞机引擎的作用，操作系统是由&quot;中断驱动&quot;或&quot;事件驱动&quot;的。&lt;/p&gt;
&lt;p&gt;主要作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;及时处理设备发来的中断请求&lt;/li&gt;
&lt;li&gt;捕获用户程序提出的服务请求&lt;/li&gt;
&lt;li&gt;防止用户程序执行过程中的破坏性活动&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.1 中断/异常的概念&lt;/h3&gt;
&lt;p&gt;CPU对系统发生的某个事件作出的一种反应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU暂停正在执行的程序&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;ul&gt;
&lt;li&gt;是随机发生的&lt;/li&gt;
&lt;li&gt;是自动处理的&lt;/li&gt;
&lt;li&gt;是可恢复的&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;中断/异常的来源及处理方式&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;中断的来源&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;外部设备：如键盘、鼠标、网络接口卡等外设发出的中断信号&lt;/li&gt;
&lt;li&gt;定时器：系统定时器发出的中断信号&lt;/li&gt;
&lt;li&gt;其他硬件部件：如硬盘、打印机等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;异常的来源&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序错误：如除零错误、非法指令、页面错误等&lt;/li&gt;
&lt;li&gt;系统调用：用户程序通过系统调用引发的陷入&lt;/li&gt;
&lt;li&gt;其他内部事件：如调试事件、断点等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;处理方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断处理：中断处理程序通常是操作系统的一部分，负责响应外部设备的请求。处理过程包括保存当前CPU状态、执行中断处理程序、恢复CPU状态并返回被中断的程序。&lt;/li&gt;
&lt;li&gt;异常处理：异常处理程序也由操作系统提供，负责处理程序运行过程中出现的错误或特殊事件。处理过程包括识别异常类型、执行相应的处理程序、根据异常类型决定是否返回被中断的程序或终止程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;术语演化的历史背景&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;中断的引入&lt;/strong&gt;：为了支持CPU和设备之间的并行操作&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当CPU启动设备进行输入/输出后，设备便可以独立工作，CPU转去处理与此次输入/输出不相关的事情；当设备完成输入/输出后，通过向CPU发中断报告此次输入/输出的结果，让CPU决定如何处理以后的事情&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;异常的引入&lt;/strong&gt;：表示CPU执行指令时本身出现的问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如算术溢出、除零、取数时的奇偶错，访存地址时越界或执行了“陷入指令”等，这时硬件改变了CPU当前的执行流程，转到相应的错误处理程序或异常处理程序或执行系统调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 事件&lt;/h3&gt;
&lt;p&gt;事件可分为中断(外中断)和异常(内中断, 即下面三个表项)：&lt;/p&gt;
&lt;p&gt;| 类别            | 原因                      | 异步/同步 | 返回行为             |
| --------------- | ------------------------- | --------- | -------------------- |
| 中断(Interrupt) | 来自I/O设备、其他硬件部件 | 异步      | 总是返回到下一条指令 |
| 陷入(Trap)      | 有意识安排的              | 同步      | 返回到下一条指令     |
| 故障(Fault)     | 可恢复的错误              | 同步      | 返回到当前指令       |
| 终止(Abort)     | 不可恢复的错误            | 同步      | 不会返回             |&lt;/p&gt;
&lt;h3&gt;3.3 中断/异常机制工作原理&lt;/h3&gt;
&lt;p&gt;中断/异常机制是现代计算机系统的核心机制之一，通过硬件和软件相互配合，使计算机系统得以充分发挥能力：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬件工作&lt;/strong&gt;：中断/异常响应&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;捕获中断源发出的中断/异常请求&lt;/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;ul&gt;
&lt;li&gt;识别中断/异常类型&lt;/li&gt;
&lt;li&gt;完成相应的处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在每条指令执行周期的最后时刻扫描中断寄存器，查看是否有中断信号。
若无中断信号，继续执行下一条指令。
若有中断，中断硬件将该中断触发器内容按规定编码送入PSW的相应位，称为中断码，通过交换中断向量引出中断处理程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬件——中断响应过程示意&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在每条指令执行周期的最后时刻，扫描中断寄存器。&lt;/li&gt;
&lt;li&gt;检查是否有中断信号。
&lt;ul&gt;
&lt;li&gt;若无中断信号，继续执行下一条指令。&lt;/li&gt;
&lt;li&gt;若有中断信号，中断硬件将中断触发器内容按规定编码送入PSW的相应位，称为中断码。&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;/li&gt;
&lt;li&gt;中断处理完成后，恢复CPU状态并返回被中断的程序。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.4 软硬协同——中断向量表&lt;/h3&gt;
&lt;p&gt;中断向量：一个内存单元，存放中断处理程序入口地址和程序运行所需的处理机状态字&lt;/p&gt;
&lt;p&gt;硬件执行流程按中断号/异常类型的不同，通过中断向量表转移控制权给中断处理程序&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux中的中断向量(X86)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0～19：不可屏蔽中断和异常
&lt;ul&gt;
&lt;li&gt;0: 除法错误 #DE&lt;/li&gt;
&lt;li&gt;1: 调试异常 #DB&lt;/li&gt;
&lt;li&gt;2: NMI中断&lt;/li&gt;
&lt;li&gt;3: 断点异常 #BP&lt;/li&gt;
&lt;li&gt;4: 溢出异常 #OF&lt;/li&gt;
&lt;li&gt;5: 边界检查异常 #BR&lt;/li&gt;
&lt;li&gt;6: 无效操作码异常 #UD&lt;/li&gt;
&lt;li&gt;7: 设备不可用异常 #NM&lt;/li&gt;
&lt;li&gt;8: 双重故障异常 #DF&lt;/li&gt;
&lt;li&gt;9: 协处理器段越界异常&lt;/li&gt;
&lt;li&gt;10: 无效TSS异常 #TS&lt;/li&gt;
&lt;li&gt;11: 段不存在异常 #NP&lt;/li&gt;
&lt;li&gt;12: 栈段错误 #SS&lt;/li&gt;
&lt;li&gt;13: 通用保护异常 #GP&lt;/li&gt;
&lt;li&gt;14: &lt;strong&gt;页错误异常 #PF&lt;/strong&gt; (包含COW写时复制机制的页错误)&lt;/li&gt;
&lt;li&gt;15: 保留&lt;/li&gt;
&lt;li&gt;16: 浮点异常 #MF&lt;/li&gt;
&lt;li&gt;17: 对齐检查异常 #AC&lt;/li&gt;
&lt;li&gt;18: 机器检查异常 #MC&lt;/li&gt;
&lt;li&gt;19: SIMD浮点异常 #XF&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;20～31：Intel保留&lt;/li&gt;
&lt;li&gt;32～127：外部中断(IRQ)&lt;/li&gt;
&lt;li&gt;128(0x80)：用于系统调用的可编程异常&lt;/li&gt;
&lt;li&gt;129～238：外部中断&lt;/li&gt;
&lt;li&gt;239：本地APIC时钟中断&lt;/li&gt;
&lt;li&gt;240：本地APIC高温中断&lt;/li&gt;
&lt;li&gt;241～250：Linux保留&lt;/li&gt;
&lt;li&gt;251～253：处理器间中断&lt;/li&gt;
&lt;li&gt;254：本地APIC错误中断&lt;/li&gt;
&lt;li&gt;255：本地APIC伪中断&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.5 中断响应流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;设备发中断信号&lt;/li&gt;
&lt;li&gt;硬件保存现场&lt;/li&gt;
&lt;li&gt;根据中断码查表&lt;/li&gt;
&lt;li&gt;把中断处理程序入口地址等推送到相应的寄存器&lt;/li&gt;
&lt;li&gt;执行中断处理程序&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.6 上半部和下半部处理&lt;/h3&gt;
&lt;p&gt;在 Linux 系统中，中断处理程序应该尽量短且快，以减少对正常进程调度的影响。然而，中断处理程序可能会暂时关闭中断，如果执行时间过长，可能会丢失其他设备的中断请求。为了解决这个问题，Linux 将中断过程分为上半部和下半部。&lt;/p&gt;
&lt;p&gt;上半部用于快速处理中断，通常会暂时关闭中断请求，主要负责处理与硬件紧密相关或时间敏感的任务。下半部用于延迟处理上半部未完成的工作，一般以内核线程的方式运行。&lt;/p&gt;
&lt;p&gt;**上半部 (Top Half) **：&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;**下半部 (Bottom Half) **：&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;例如，当网卡收到网络包后，通过 DMA 将数据写入内存，并通过硬件中断通知内核有新数据到达。内核调用中断处理程序，分为上半部和下半部。上半部会先禁止网卡中断，避免频繁硬中断降低内核效率，然后触发软中断，将耗时且复杂的任务交给软中断处理程序 (下半部) 处理，如解析网络数据并将其传递给应用程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么引入上半部和下半部处理？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;提高响应速度&lt;/strong&gt;：上半部只执行最紧急的操作，尽量缩短中断处理时间，使系统能够快速响应其他中断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少中断禁用时间&lt;/strong&gt;：上半部运行在中断上下文中，系统在处理上半部时会禁用中断。通过将复杂操作移到下半部，可以减少中断禁用时间，提高系统的并发性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分离紧急和非紧急任务&lt;/strong&gt;：将紧急任务放在上半部，非紧急任务放在下半部，有助于合理分配系统资源，提高系统的整体性能和稳定性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以，中断处理程序的上半部和下半部可以理解为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上半部直接处理硬件请求，也就是硬中断，主要是负责耗时短的工作，特点是快速执行；&lt;/li&gt;
&lt;li&gt;下半部是由内核触发，也就是软中断，主要是负责上半部未完成的工作，通常都是耗时比较长的事情，特点是延迟执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个区别，硬中断 (上半部) 是会打断 CPU 正在执行的任务，然后立即执行中断处理程序，而软中断 (下半部) 是以内核线程的方式执行，并且每一个 CPU 都对应一个软中断内核线程，名字通常为「ksoftirqd/CPU 编号」，比如 0 号 CPU 对应的软中断内核线程的名字是 ksoftirqd/0。&lt;/p&gt;
&lt;p&gt;Ref: &lt;a href=&quot;https://xiaolincoding.com/os/1_hardware/soft_interrupt.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%BD%AF%E4%B8%AD%E6%96%AD&quot;&gt;软中断&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3.7 软件——中断处理程序&lt;/h3&gt;
&lt;p&gt;设计操作系统时，为每一类中断/异常事件编好相应的处理程序，并设置好中断向量表。系统运行时若响应中断，中断硬件部件将CPU控制权转给中断处理程序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存相关寄存器信息&lt;/li&gt;
&lt;li&gt;分析中断/异常的具体原因&lt;/li&gt;
&lt;li&gt;执行对应的处理功能&lt;/li&gt;
&lt;li&gt;恢复现场，返回被事件打断的程序&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;3.8 中断/异常机制小结&lt;/h3&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;ul&gt;
&lt;li&gt;处理器状态被切换到内核态&lt;/li&gt;
&lt;li&gt;在系统栈中保存被中断程序的重要上下文环境，主要是程序计数器PC、程序状态字PSW&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CPU根据中断码查中断向量表，获得与该中断相关的处理程序的入口地址，并将PC设置成该地址，新的指令周期开始时，CPU控制转移到中断处理程序&lt;/li&gt;
&lt;li&gt;中断处理程序开始工作
&lt;ul&gt;
&lt;li&gt;在系统栈中保存现场信息&lt;/li&gt;
&lt;li&gt;检查I/O设备的状态信息，操纵I/O设备或者在设备和内存之间传送数据等等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;中断处理结束时，CPU检测到中断返回指令，从系统栈中恢复被中断程序的上下文环境，CPU状态恢复成原来的状态，PSW和PC恢复成中断前的值，CPU开始一个新的指令周期&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. IA32 体系结构对中断的支持&lt;/h2&gt;
&lt;h3&gt;4.1 基本概念——X86处理器&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;中断&lt;/strong&gt;：由硬件信号引发的，分为可屏蔽和不可屏蔽中断&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;异常&lt;/strong&gt;：由指令执行引发的，比如除零异常&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;80x86处理器发布了大约20种不同的异常&lt;/li&gt;
&lt;li&gt;对于某些异常，CPU会在执行异常处理程序之前产生硬件出错码，并压入内核态堆栈&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;系统调用&lt;/strong&gt;：异常的一种，用户态到系统态的唯一入口&lt;/p&gt;
&lt;h3&gt;4.2 IA32体系结构对中断的支持&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;中断控制器(PIC或APIC)&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;负责将硬件的中断信号转换为中断向量，引发CPU中断&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实模式&lt;/strong&gt;：中断向量表(Interrupt Vector)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;存放中断服务程序的入口地址&lt;/li&gt;
&lt;li&gt;不支持CPU运行状态切换&lt;/li&gt;
&lt;li&gt;中断处理与一般的过程调用相似&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;保护模式&lt;/strong&gt;：中断描述符表(Interrupt Descriptor table)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;采用门(gate)描述符数据结构描述中断向量&lt;/li&gt;
&lt;li&gt;表项包含四种类型门描述符：
&lt;ul&gt;
&lt;li&gt;任务门(Task Gate)&lt;/li&gt;
&lt;li&gt;中断门(Interrupt Gate)&lt;/li&gt;
&lt;li&gt;陷阱门(Trap Gate)&lt;/li&gt;
&lt;li&gt;调用门(Call Gate)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;中断向量表/中断描述符表&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表项包含四种类型门描述符：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任务门(Task Gate)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;中断发生时，必须取代当前进程的那个进程的TSS选择符存放在任务门中 (Linux没有使用任务门)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断门(Interrupt Gate)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;给出段选择符 (Segment Selector)、中断/异常程序的段内偏移量 (Offset)&lt;/li&gt;
&lt;li&gt;通过中断门后系统会自动禁止中断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;陷阱门(Trap Gate)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;与中断门类似，但通过陷阱门后系统不会自动关中断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用门(Call Gate)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;中断/异常的硬件处理过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定与中断或异常关联的向量i&lt;/li&gt;
&lt;li&gt;通过IDTR寄存器找到IDT表，获得中断描述符 (表中的第i个表项)&lt;/li&gt;
&lt;li&gt;从GDTR寄存器获得GDT的地址，结合中断描述符中的段选择符，在GDT表获取对应的段描述符&lt;/li&gt;
&lt;li&gt;特权级检查&lt;/li&gt;
&lt;li&gt;检查是否发生了特权级的变化，如需要则进行堆栈切换&lt;/li&gt;
&lt;li&gt;硬件压栈，保存上下文环境&lt;/li&gt;
&lt;li&gt;如果是中断，清IF位&lt;/li&gt;
&lt;li&gt;通过中断描述符中的段内偏移量和段描述符中的基地址，找到中断/异常处理程序的入口地址，执行其第一条指令&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;5. 系统调用(System call)&lt;/h2&gt;
&lt;p&gt;系统调用是用户在编程时可以调用的操作系统功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统调用是操作系统提供给编程人员的唯一接口&lt;/li&gt;
&lt;li&gt;使CPU状态从用户态陷入内核态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个操作系统都提供几百种系统调用，包括进程控制、进程通信、文件使用、目录操作、设备管理、信息维护等。&lt;/p&gt;
&lt;h4&gt;经典问题：系统调用与C函数调用的区别？&lt;/h4&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;：系统调用是操作系统提供给用户程序的接口，用于执行特权操作，如文件操作、进程控制、内存管理等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行环境&lt;/strong&gt;：系统调用会导致CPU从用户态切换到内核态，执行内核中的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现方式&lt;/strong&gt;：通过特定的陷入指令 (如&lt;code&gt;int 0x80&lt;/code&gt;) 触发中断或异常，进入内核态执行相应的系统调用服务例程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开销&lt;/strong&gt;：由于涉及用户态到内核态的切换，系统调用的开销较大。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;C函数调用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义&lt;/strong&gt;：C函数调用是程序内部的函数调用，用于实现特定的功能或算法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行环境&lt;/strong&gt;：C函数调用在用户态执行，不涉及特权操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现方式&lt;/strong&gt;：通过函数调用指令 (如&lt;code&gt;call&lt;/code&gt;) 在程序内部跳转到函数的入口地址执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开销&lt;/strong&gt;：C函数调用的开销较小，因为不涉及用户态和内核态的切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，&lt;code&gt;printf&lt;/code&gt;函数是一个C库函数，它最终会调用系统调用&lt;code&gt;write&lt;/code&gt;来将数据输出到终端。&lt;code&gt;printf&lt;/code&gt;函数本身在用户态执行，而&lt;code&gt;write&lt;/code&gt;系统调用会切换到内核态执行实际的输出操作。&lt;/p&gt;
&lt;h3&gt;5.1 静态：系统调用机制的设计&lt;/h3&gt;
&lt;p&gt;机制与策略分离原则指导下的系统调用设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断/异常机制&lt;/strong&gt;：支持系统调用服务的实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;陷入指令&lt;/strong&gt;：引发异常，完成用户态到内核态的切换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用号和参数&lt;/strong&gt;：每个系统调用都事先给定一个编号(功能号)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用表&lt;/strong&gt;：存放系统调用服务例程的入口地址&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 静态：参数传递过程问题&lt;/h3&gt;
&lt;p&gt;怎样实现用户程序的参数传递给内核？常用的3种实现方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由陷入指令自带参数：陷入指令的长度有限，只能自带有限的参数&lt;/li&gt;
&lt;li&gt;通过通用寄存器传递参数：寄存器的个数会限制传递参数的数量&lt;/li&gt;
&lt;li&gt;在内存中开辟专用堆栈区来传递参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;数据段部分&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;section .data
output:
    ascii &quot;Hello!\n&quot;
output_end:
equ len, output_end - output
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;section .data&lt;/code&gt;：定义数据段，用于存放程序中的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output:&lt;/code&gt;：定义一个标签，表示数据的起始位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ascii &quot;Hello!\n&quot;&lt;/code&gt;：定义一个ASCII字符串&quot;Hello!&quot;，后跟换行符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output_end:&lt;/code&gt;：定义另一个标签，表示数据的结束位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;equ len, output_end - output&lt;/code&gt;：定义一个常量&lt;code&gt;len&lt;/code&gt;，其值为&lt;code&gt;output_end&lt;/code&gt;和&lt;code&gt;output&lt;/code&gt;之间的字节数，即字符串的长度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;代码段部分&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;section .text
globl _start
_start:
    movl $4, %eax     #eax存放系统调用号
    movl $1, %ebx
    movl $output, %ecx
    movl $len, %edx
    int $0x80         #引发一次系统调用
end:
    movl $1, %eax     #1这个系统调用的作用？
    movl $0, %ebx
    int $0x80
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;section .text&lt;/code&gt;：定义代码段，用于存放程序的指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;globl _start&lt;/code&gt;：声明&lt;code&gt;_start&lt;/code&gt;标签为全局的，使链接器能够找到程序的入口点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_start:&lt;/code&gt;：程序的入口点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $4, %eax&lt;/code&gt;：将4放入eax寄存器，4是Linux系统调用表中&lt;code&gt;write&lt;/code&gt;函数的调用号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $1, %ebx&lt;/code&gt;：将1放入ebx寄存器，1代表标准输出文件描述符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $output, %ecx&lt;/code&gt;：将output字符串的地址放入ecx寄存器，作为要输出的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $len, %edx&lt;/code&gt;：将len (字符串长度) 放入edx寄存器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int $0x80&lt;/code&gt;：触发中断0x80，执行系统调用，这里执行的是write(1, &quot;Hello!\n&quot;, 7)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $1, %eax&lt;/code&gt;：将1放入eax寄存器，1是Linux系统调用表中&lt;code&gt;exit&lt;/code&gt;函数的调用号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movl $0, %ebx&lt;/code&gt;：将0放入ebx寄存器，作为exit()的参数，表示程序正常退出 (返回值为0)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int $0x80&lt;/code&gt;：再次触发中断0x80，执行系统调用exit(0)，终止程序&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.3 动态：系统调用的执行过程&lt;/h3&gt;
&lt;p&gt;当CPU执行到特殊的陷入指令时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;中断/异常机制&lt;/strong&gt;：硬件保护现场；通过查中断向量表把控制权转给系统调用总入口程序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;系统调用总入口程序&lt;/strong&gt;：保存现场；将参数保存在内核堆栈里；通过查系统调用表把控制权转给相应的系统调用处理例程或内核函数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行系统调用例程&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复现场，返回用户程序&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;6. Linux系统调用实现&lt;/h2&gt;
&lt;p&gt;基于x86体系结构的Linux系统调用实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;陷入指令选择&lt;/strong&gt;：&lt;code&gt;int $0x80&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;门描述符&lt;/strong&gt;：系统初始化时对IDT表中的第128号门初始化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;门类型&lt;/strong&gt;：15，陷阱门：陷阱门不会自动屏蔽中断，允许在处理系统调用时继续响应其他中断，提高系统的并发性和响应速度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DPL&lt;/strong&gt;：3，与用户级别相同，允许用户进程使用该门描述符&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.1 系统调用号示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;# define __NR_exit 1
# define __NR_fork 2
# define __NR_read 3
# define __NR_write 4
# define __NR_open 5
# define __NR_close 6
# define __NR_waitpid 7
# define __NR_creat 8
# define __NR_link 9
# define __NR_unlink 10
# define __NR_execve 11
# define __NR_chdir 12
# define __NR_time 13
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 系统执行 &lt;code&gt;int $0x80&lt;/code&gt; 指令&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特权级的改变&lt;/strong&gt;：由于从用户态切换到内核态，CPU需要切换栈。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户栈切换到内核栈：CPU从任务状态段 (TSS) 中装入新的栈指针 (SS:ESP) ，指向内核栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;保存用户态信息&lt;/strong&gt;：用户栈的信息 (SS:ESP) 、EFLAGS、用户态CS、EIP寄存器的内容会被压栈，以便返回时使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将EFLAGS压栈后，复位TF (陷阱标志) ，IF (中断标志) 位保持不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查找IDT&lt;/strong&gt;：使用128在中断描述符表 (IDT) 中找到对应的门描述符，从中找出段选择符装入代码段寄存器CS。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码段描述符中的基地址加上陷阱门描述符中的偏移量，定位到system_call的入口地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;特权级检查&lt;/strong&gt;：代码只能访问相同或较低特权级的数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确保系统调用在内核态执行，防止用户态代码直接访问内核数据。&lt;/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;系统调用号：通过EAX寄存器传递。&lt;/li&gt;
&lt;li&gt;系统调用参数：通过EBX、ECX、EDX、ESI、EDI寄存器传递。&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;处理完成后，将结果放入EAX寄存器，并通过ret_from_sys_call例程返回用户态程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.3 Linux系统调用执行流程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;应用程序 → 封装例程 → 陷入处理 → 内核函数
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;用户态下调用C库的库函数，比如write()&lt;/li&gt;
&lt;li&gt;封装后的write()先做好参数传递工作，然后使用int 0x80指令产生一次异常&lt;/li&gt;
&lt;li&gt;CPU通过0x80号在IDT中找到对应的服务例程system_call()，并调用之&lt;/li&gt;
&lt;li&gt;system_call()将参数保存在内核栈；根据系统调用号索引系统调用表，找到系统调用程序入口，比如sys_write()&lt;/li&gt;
&lt;li&gt;sys_write()执行完后，经过ret_from_sys_call()例程返回用户程序&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.4 示例：系统调用的参数传递&lt;/h3&gt;
&lt;p&gt;系统调用使用寄存器传递参数，要传递的参数包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统调用号&lt;/li&gt;
&lt;li&gt;系统调用所需的参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用于传递参数的寄存器有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;eax用于保存系统调用号和系统调用返回值&lt;/li&gt;
&lt;li&gt;系统调用参数保存在ebx, ecx, edx, esi和edi中，参数个数不超过6个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进入内核态后，system_call再将这些参数保存在内核堆栈中。&lt;/p&gt;
&lt;p&gt;假如C库中封装的系统调用号3的函数原型如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;movl 0x8(%esp), %ecx  # 将用户态堆栈中的para2放入ecx
movl 0x4(%esp), %ebx  # 将用户态堆栈中的para1放入ebx
movl $0x3, %eax       # 系统调用号保存在eax中
int $0x80             # 引发系统调用
movl %eax, errno      # 将结果存入全局变量errno中
movl $-1, %eax        # eax置为-1，表示出错
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则调用时，参数传递如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;eax = 3&lt;/li&gt;
&lt;li&gt;ebx = para1&lt;/li&gt;
&lt;li&gt;ecx = para2&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. 系统调用小结&lt;/h2&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;CPU状态从目态转入管态&lt;/li&gt;
&lt;li&gt;利用系统调用，可以动态请求和释放系统资源，完成与硬件相关的工作以及控制程序的执行等&lt;/li&gt;
&lt;li&gt;每个操作系统都提供几百种系统调用 (POSIX标准)&lt;/li&gt;
&lt;li&gt;系统调用与C函数调用的区别？&lt;/li&gt;
&lt;li&gt;完成系统调用机制的运行需要什么条件 (准备工作) ？
&lt;ul&gt;
&lt;li&gt;静态 和 动态&lt;/li&gt;
&lt;li&gt;封装内核函数 - 库函数 (API) ；访管指令与陷入机制；编译器；操作系统 (初始化、系统调用编号及参数；系统调用表)&lt;/li&gt;
&lt;li&gt;陷入内核，总入口程序，保存现场 (压栈) ，查表分派，执行返回&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>Operating Systems Notes 01: Overview of Operating Systems</title><link>https://www.lyt0112.com/blog/operating_systems_note_01-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/operating_systems_note_01-zh</guid><description>Operating Systems Notes 01: 操作系统概述</description><pubDate>Tue, 11 Mar 2025 22:14:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;Model: &lt;code&gt;claude-3-7-sonnet-20250219&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This article uses LLM to improve efficiency, which can make mistakes. I have tried my best to check and proofread, but still cannot guarantee complete accuracy.&lt;/p&gt;
&lt;p&gt;For personal use, may be nagging for you.&lt;/p&gt;
&lt;h2&gt;1. 操作系统是什么？&lt;/h2&gt;
&lt;h3&gt;1.1 操作系统的定义&lt;/h3&gt;
&lt;p&gt;操作系统是计算机系统中的一个系统软件，是一些程序模块的集合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;资源的管理者&lt;/strong&gt;：能以尽量有效、合理的方式组织和管理计算机的软硬件资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务的提供者&lt;/strong&gt;：合理地组织计算机的工作流程，控制程序的执行并向用户提供各种服务功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;机器能力的扩展&lt;/strong&gt;：使得用户能够灵活、方便地使用计算机，使整个计算机系统能高效地运行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.2 操作系统的作用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;虚拟机&lt;/strong&gt;：将物理资源(处理器、内存、磁盘)转换成更通用、更强大、更易用的虚拟形式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIs&lt;/strong&gt;：提供可供用户调用的接口，提供应用程序的标准库&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源管理器&lt;/strong&gt;：允许多程序运行(共享CPU)，允许多程序并发访问内存，允许多程序访问设备&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.3 设计与实现目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;抽象&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;模块化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用高级语言&lt;/strong&gt;(C)而非汇编&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;(最小化开销)
&lt;ul&gt;
&lt;li&gt;最小化额外的时间(指令)&lt;/li&gt;
&lt;li&gt;最小化额外的空间(内存/磁盘)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保护&lt;/strong&gt;：应用之间、操作系统与应用之间的隔离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节能&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：防止恶意应用的入侵&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动性&lt;/strong&gt;：能够运行在越来越小的设备上&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 从不同角度认知操作系统&lt;/h2&gt;
&lt;h3&gt;2.1 资源管理的观点&lt;/h3&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;：CPU，内存，设备(I/O设备、磁盘、时钟、网络接口等)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软件资源&lt;/strong&gt;：磁盘上的文件、信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源管理目的&lt;/strong&gt;：实现资源共享、提高资源利用率&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复用方式&lt;/strong&gt;：时间及空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;怎样管理资源？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据结构与算法&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;跟踪记录资源使用状况&lt;/li&gt;
&lt;li&gt;分配和回收资源(资源分配策略与算法)
&lt;ul&gt;
&lt;li&gt;静态分配策略&lt;/li&gt;
&lt;li&gt;动态分配策略 ✓&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/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;协调多个进程对资源请求的冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 进程的观点&lt;/h3&gt;
&lt;p&gt;从操作系统运行的角度动态观察操作系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统是由一些可同时、独立运行的进程和一个对这些进程进行协调的核心组成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进程&lt;/strong&gt;：完成某一特定功能的程序，是程序的一次执行过程，动态的、有生命的，有诞生/消亡&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 虚机器观点&lt;/h3&gt;
&lt;p&gt;从操作系统内部结构来看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把操作系统分成若干层&lt;/li&gt;
&lt;li&gt;每一层完成其特定功能，构成一个虚机器，并对上一层提供支持&lt;/li&gt;
&lt;li&gt;通过逐层功能扩充，最终完成整个操作系统虚机器&lt;/li&gt;
&lt;li&gt;操作系统虚机器向用户提供各种功能，完成用户请求&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 操作系统的特征&lt;/h2&gt;
&lt;h3&gt;3.1 并发(concurrency)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;处理多个同时性活动的能力&lt;/li&gt;
&lt;li&gt;计算机系统中同时运行多个程序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宏观上&lt;/strong&gt;：这些程序同时在执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微观上&lt;/strong&gt;：单CPU情况下，任何时刻只有一个程序在执行，即这些程序在CPU上轮流执行&lt;/li&gt;
&lt;li&gt;由并发引起的问题：活动切换、保护、相互依赖的活动间的同步&lt;/li&gt;
&lt;li&gt;**并行(parallel)**与&quot;并发&quot;的区别&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 共享(sharing)&lt;/h3&gt;
&lt;p&gt;操作系统与多个用户程序共同使用计算机系统中的资源&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享有限的系统资源&lt;/li&gt;
&lt;li&gt;操作系统要对系统资源进行合理分配和使用&lt;/li&gt;
&lt;li&gt;资源在一个时间段内交替被多个进程所用
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥共享&lt;/strong&gt;(如打印机)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同时共享(访问)&lt;/strong&gt;(如可重入代码，磁盘文件)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由于共享引发的问题：资源分配难以达到最优化，资源使用时需要保护&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 虚拟(Virtual)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个物理实体映射为若干个对应的逻辑实体(分时或分空间)&lt;/li&gt;
&lt;li&gt;虚拟技术是操作系统管理系统资源的重要手段，可提高资源利用率
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU&lt;/strong&gt;：每个用户(进程)的&quot;虚处理器&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储器&lt;/strong&gt;：每个进程都占有的地址空间(代码＋数据＋堆、栈)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示设备&lt;/strong&gt;：多窗口或虚拟终端&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.4 随机性(不确定性)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;操作系统必须随时对以不可预测的次序发生的事件进行响应&lt;/li&gt;
&lt;li&gt;进程的运行速度不可预知：多个进程并发执行，&quot;走走停停&quot;，无法预知每个进程的运行推进快慢&lt;/li&gt;
&lt;li&gt;难以重现系统在某个时刻的状态(包括重现运行中的错误)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 操作系统发展历史&lt;/h2&gt;
&lt;h3&gt;技术变化与概念重用&lt;/h3&gt;
&lt;p&gt;技术变化导致某些思想过时并迅速消失，但技术的另一种变化还可能使它们复活。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;磁盘上文件分配—连续文件(CD-ROM文件系统)&lt;/li&gt;
&lt;li&gt;硬件保护&lt;/li&gt;
&lt;li&gt;动态链接(MULTICS首先提出)&lt;/li&gt;
&lt;li&gt;计算服务(MULTICS，以大量的、附有相对简单用户机器的、集中式Internet服务器形式回归) → 云计算&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 操作系统发展阶段&lt;/h3&gt;
&lt;h4&gt;第1阶段(1948-1970)：硬件昂贵，人工便宜&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;控制台&lt;/strong&gt;：一次一个用户(独占资源)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;批处理&lt;/strong&gt;：装入程序 → 运行 → 打印输出结果(无保护)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多道程序设计&lt;/strong&gt;：多个程序同时运行，多个用户共享系统(需要存储保护)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SPOOLing技术&lt;/strong&gt;：批处理作业处理流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第2阶段(1970-1985)：硬件便宜，人工昂贵&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;交互、分时&lt;/strong&gt;：多个用户同时与系统交互&lt;/li&gt;
&lt;li&gt;用户可以在线工作：开发、调试、编辑等&lt;/li&gt;
&lt;li&gt;问题：增加用户时 → 系统性能降低(响应时间、抖动)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;第一个分时操作系统CTSS&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1959年在MIT提出分时系统思想&lt;/li&gt;
&lt;li&gt;每个用户有一个联机终端&lt;/li&gt;
&lt;li&gt;计算机能够为许多用户提供交互式、快速服务，同时在CPU空闲时还能在后台运行大作业&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重要历史事件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS/360&lt;/strong&gt;：IBM发布时带着已知的1000个错误&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multics&lt;/strong&gt;：1963年开始，1969年才发布&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UNIX&lt;/strong&gt;：一群计算机迷在贝尔实验室开发，初衷是在DEC PDP-7小型计算机上玩星际探险游戏&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第3阶段(1981-)：硬件非常便宜，人工非常昂贵&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;个人计算时代&lt;/li&gt;
&lt;li&gt;开始PC硬件资源有限，一次运行一个程序，OS是一个例程库，回归简单&lt;/li&gt;
&lt;li&gt;逐渐PC资源丰富，OS又成为一个庞然大物(大型OS)，存储保护、多道程序设计再次出现&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第4阶段(1981-)：分布式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;网络：允许不同机器很容易地相互共享资源(打印机、文件服务器、Web服务器)&lt;/li&gt;
&lt;li&gt;解决问题：共享，安全&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第5阶段(1995-)：移动计算时代&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;各种移动终端的出现(笔记本、平板、手机、机顶盒、可穿戴设备等)&lt;/li&gt;
&lt;li&gt;特点：小型、移动、便宜，但能力有限&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第6阶段(2006-)：云计算时代&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;提供可无限扩展的、可随时获取的、按需使用、按使用付费的资源&lt;/li&gt;
&lt;li&gt;云计算操作系统：云计算后台数据中心的整体管理运营系统&lt;/li&gt;
&lt;li&gt;作用：
&lt;ul&gt;
&lt;li&gt;管理和驱动海量服务器、存储等基础硬件&lt;/li&gt;
&lt;li&gt;为云应用软件提供统一、标准的接口&lt;/li&gt;
&lt;li&gt;管理海量的计算任务以及资源调配&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;第7阶段(200?-)：泛在计算/普适计算/物联网&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;许多联网设备为许多人提供个性化的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. 操作系统分类&lt;/h2&gt;
&lt;h3&gt;5.1 传统分类&lt;/h3&gt;
&lt;h4&gt;1. 批处理操作系统&lt;/h4&gt;
&lt;ul&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;ol&gt;
&lt;li&gt;用户将作业交给系统操作员&lt;/li&gt;
&lt;li&gt;系统操作员将许多用户的作业组成一批作业，输入到计算机系统中&lt;/li&gt;
&lt;li&gt;系统操作员启动操作系统&lt;/li&gt;
&lt;li&gt;系统自动、依次执行每个作业&lt;/li&gt;
&lt;li&gt;由系统操作员将作业结果交给用户&lt;/li&gt;
&lt;/ol&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;ul&gt;
&lt;li&gt;&lt;strong&gt;单道批处理系统&lt;/strong&gt;(simple batch processing, uni-programming)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多道批处理系统&lt;/strong&gt;(multiprogramming system)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SPOOLing系统(技术)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1961年，英国曼彻斯特大学，Atalas机&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simultaneous Peripheral Operation On-Line&lt;/strong&gt;(同时的外围设备联机操作)--假脱机技术&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;思想&lt;/strong&gt;：利用磁盘作缓冲，将输入、计算、输出分别组织成独立的任务流，使I/O和计算真正并行&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;作业运行的结果输出到磁盘上的输出井&lt;/li&gt;
&lt;li&gt;结果从磁盘上的输出井送到打印机&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主要特点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;提高I/O速度&lt;/li&gt;
&lt;li&gt;将独占设备改造为共享设备&lt;/li&gt;
&lt;li&gt;实现虚拟设备功能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 分时操作系统(time-sharing system)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间片&lt;/strong&gt;(time slice)：操作系统将CPU的时间划分成若干个片段&lt;/li&gt;
&lt;li&gt;操作系统以时间片为单位，轮流为每个终端用户服务, 每次服务一个时间片 (其特点是利用人的错觉，使人感觉不到)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追求目标&lt;/strong&gt;：及时响应(依据是响应时间)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应时间&lt;/strong&gt;：从终端发出命令到系统给予回答所经历的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 实时操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;使计算机能及时响应外部事件的请求，在规定的严格时间内完成对该事件的处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分类&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实时过程控制&lt;/strong&gt;：工业控制，军事控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时通信(信息)处理&lt;/strong&gt;：电讯(自动交换)，银行，飞机订票，股市行情&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追求目标&lt;/strong&gt;：对外部请求在严格时间范围内作出反应，高可靠性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征&lt;/strong&gt;：关键参数是时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬实时系统&lt;/strong&gt;：某个动作绝对必须在规定的时刻或时间范围完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软实时系统&lt;/strong&gt;：接受偶尔违反最终时限&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 个人计算机操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;计算机在某一时间内为单用户服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追求目标&lt;/strong&gt;：界面友好，使用方便，丰富的应用软件&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. 网络操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;基于计算机网络，在各种计算机操作系统上按网络体系结构协议标准开发的软件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能&lt;/strong&gt;：网络管理，通信，安全，资源共享和各种网络应用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追求目标&lt;/strong&gt;：相互通信，资源共享&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;6. 分布式操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分布式系统&lt;/strong&gt;：处理和控制的分散(相对于集中式系统)&lt;/li&gt;
&lt;li&gt;以计算机网络为基础，基本特征是处理的分布(功能和任务的分布)&lt;/li&gt;
&lt;li&gt;所有系统任务可在系统中任何处理机上运行，自动实现全系统范围内的任务分配&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特征&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;是一个统一的操作系统&lt;/li&gt;
&lt;li&gt;资源进一步共享&lt;/li&gt;
&lt;li&gt;透明性: 资源共享，分布对用户来讲是不知道的&lt;/li&gt;
&lt;li&gt;自治性: 处于分布式系统的多个主机处于平等地位，无主从关系&lt;/li&gt;
&lt;li&gt;处理能力增强、速度更快、可靠性增强&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;7. 嵌入式操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;嵌入式系统&lt;/strong&gt;：在各种设备、装置或系统中，完成特定功能的软硬件系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;嵌入式操作系统&lt;/strong&gt;：运行在嵌入式系统环境中，对整个嵌入式系统及其所操作、控制的各种部件装置等资源进行统一协调、调度、指挥和控制的系统软件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 Tanenbaum分类&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;大型机操作系统&lt;/li&gt;
&lt;li&gt;服务器操作系统&lt;/li&gt;
&lt;li&gt;多处理机操作系统&lt;/li&gt;
&lt;li&gt;个人计算机操作系统&lt;/li&gt;
&lt;li&gt;掌上计算机操作系统(移动计算机操作系统)&lt;/li&gt;
&lt;li&gt;嵌入式操作系统&lt;/li&gt;
&lt;li&gt;传感器节点操作系统&lt;/li&gt;
&lt;li&gt;实时操作系统&lt;/li&gt;
&lt;li&gt;智能卡操作系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;智能卡操作系统&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;智能卡&lt;/strong&gt;：一种包含有一块CPU芯片的信用卡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;非常严格的运行能耗和存储空间的限制&lt;/li&gt;
&lt;li&gt;有些智能卡只有单项功能，如电子支付&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;专用的操作系统&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;有些智能卡是面向Java的，ROM中有Java虚拟机解释器&lt;/li&gt;
&lt;li&gt;Java小程序被下载到卡中并由JVM解释器解释&lt;/li&gt;
&lt;li&gt;有些卡可以同时处理多个Java小程序，需要多道程序调度&lt;/li&gt;
&lt;li&gt;资源管理和保护由卡上的操作系统处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信方式&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;在读写器与智能卡之间通过&quot;命令-响应对&quot;方式进行通信和控制&lt;/li&gt;
&lt;li&gt;读写器发出操作命令，智能卡接收命令&lt;/li&gt;
&lt;li&gt;操作系统对命令解释，完成命令的解密与校验&lt;/li&gt;
&lt;li&gt;操作系统调用相应程序进行数据处理，产生应答信息，加密后送给读写器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. 重点小结&lt;/h2&gt;
&lt;h3&gt;6.1 典型的国产操作系统&lt;/h3&gt;
&lt;p&gt;麒麟、鸿蒙、Openeuler、统信等&lt;/p&gt;
&lt;h3&gt;6.2 核心概念&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;操作系统的概念&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解操作系统的不同角度&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统的主要特征&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;典型的、历史上/当前有重要意义的操作系统&lt;/strong&gt;&lt;/li&gt;
&lt;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;通道&lt;/li&gt;
&lt;li&gt;SPOOLing技术&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作系统的分类&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.3 重要历史操作系统&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS/360&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MULTICS&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.4 操作系统特征&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机性&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;7. 常见问题解答&lt;/h2&gt;
&lt;h3&gt;7.1 Buffer Cache的作用和工作原理&lt;/h3&gt;
&lt;p&gt;在UNIX操作系统中，文件子系统与块设备之间的Buffer Cache：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;减少对物理设备的访问次数，提高I/O性能&lt;/li&gt;
&lt;li&gt;协调CPU与I/O设备之间的速度差异&lt;/li&gt;
&lt;li&gt;支持数据的读写缓冲，实现数据共享&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;当进程请求读取数据时，系统首先检查Buffer Cache中是否有所需数据&lt;/li&gt;
&lt;li&gt;如果有 (命中) ，直接从Buffer Cache返回数据&lt;/li&gt;
&lt;li&gt;如果没有 (未命中) ，从物理设备读取数据到Buffer Cache，再返回给进程&lt;/li&gt;
&lt;li&gt;写操作时，数据先写入Buffer Cache，再由系统决定何时写回物理设备&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.2 UNIX的名字来历 (猜测)&lt;/h3&gt;
&lt;p&gt;UNIX名字可能来源于MULTICS (Multiplexed Information and Computing Service) 的简化和双关语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MULTICS是一个复杂的分时系统&lt;/li&gt;
&lt;li&gt;UNIX (Uniplexed Information and Computing Service) 表示简化版的MULTICS&lt;/li&gt;
&lt;li&gt;也有说法是&quot;UNIX&quot;是&quot;eUNuchs&quot; (阉割版的MULTICS) 的变体&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.3 批处理操作系统的区别&lt;/h3&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;：内存中同时存放多个作业，CPU在这些作业之间切换执行，提高了CPU利用率和系统吞吐量&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.4 SPOOLing技术的现状&lt;/h3&gt;
&lt;p&gt;SPOOLing技术并未过时，在现代计算机系统中仍有广泛应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;打印系统&lt;/strong&gt;：现代操作系统的打印队列管理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件系统&lt;/strong&gt;：电子邮件的发送和接收过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后台作业处理&lt;/strong&gt;：批处理任务的调度和执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库系统&lt;/strong&gt;：事务处理和日志管理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.5 传统分时系统在今天的意义&lt;/h3&gt;
&lt;p&gt;传统分时系统在今天仍有重要意义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多用户支持&lt;/strong&gt;：现代操作系统继承了分时系统的多用户并发访问能力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源共享&lt;/strong&gt;：分时思想是云计算、虚拟化等现代技术的基础&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互式计算&lt;/strong&gt;：分时系统建立的交互式计算模式仍是现代系统的核心特性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;公平调度&lt;/strong&gt;：分时系统的时间片轮转调度思想仍被广泛应用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.6 操作系统的主要作用和典型特征&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;操作系统的主要作用&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;管理计算机硬件和软件资源&lt;/li&gt;
&lt;li&gt;为应用程序提供统一的服务接口&lt;/li&gt;
&lt;li&gt;实现人机交互，提供用户界面&lt;/li&gt;
&lt;li&gt;提高系统资源利用率&lt;/li&gt;
&lt;li&gt;保护系统安全和数据完整性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;操作系统的典型特征&lt;/strong&gt; (与其他软件相比) ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并发性&lt;/strong&gt;：能够同时运行多个程序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享性&lt;/strong&gt;：多个用户/程序共享系统资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;虚拟性&lt;/strong&gt;：将物理资源抽象为逻辑资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步性&lt;/strong&gt;：程序执行的结果与时间有关&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久性&lt;/strong&gt;：操作系统常驻内存&lt;/li&gt;
&lt;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;</content:encoded><h:img src="/_astro/operating_systems.Dib9zy_r.jpeg"/><enclosure url="/_astro/operating_systems.Dib9zy_r.jpeg"/></item><item><title>How to Make a Beautiful Crystal at Home</title><link>https://www.lyt0112.com/blog/crystal-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/crystal-zh</guid><description>其实是魅力化学的课程论文</description><pubDate>Tue, 28 Jan 2025 15:07:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;&lt;/p&gt;
&lt;p&gt;本文为博主 2022 年秋季学期的 &lt;a href=&quot;https://www.lyt0112.com/blog/course_review-zh#%E9%AD%85%E5%8A%9B%E5%8C%96%E5%AD%A6&quot;&gt;魅力化学课程&lt;/a&gt; 论文, 感觉很有纪念意义故发布.&lt;/p&gt;
&lt;h2&gt;一、前言&lt;/h2&gt;
&lt;p&gt;苏霍姆林斯基说：&quot;在人的心灵深处，都有一种根深蒂固的需要，就是希望自己是一个发现者， 研究者和探究者。&quot; 每个人都有对新奇事物的求知欲，而作为一门历史悠久的学科， 化学以其百变的现象、 剔透的晶体、 丰富的物质等一系列独特而又直观的魅力吸引着一代又一代的学生。&lt;/p&gt;
&lt;p&gt;作为一门典型的实验学科，中学生们却只能在习题中做实验，进入实验室的机会可以说是少之又少， 而在对实验的兴趣和沉没题海的枯燥之间的矛盾便是中学家庭实验党产生的最好土壤。&lt;/p&gt;
&lt;p&gt;幸运又不幸，我曾是一名家庭实验党。在自己动手实现一些实验时，我满足了对化学的好奇心。例如亚甲基蓝和葡萄糖在空气作用下的还原氧化循环（蓝瓶子实验）或制作纯净物的晶体（本文主要介绍内容），这些实践让我对化学产生了更深的理解。&lt;/p&gt;
&lt;p&gt;而遗憾的是，首先实验区与生活区不能完全分离、废物处理不当、在缺乏适当安全防护的情况下进行危险实验（比如大剂量铝热实验、没有通风橱的情况下制取氯气），这些都存在潜在危险。同时也常常疏于记录实验过程，这也不是一个好习惯。更重要的是，关于家庭实验的意义，在现在对化学实验有了更深认识的当下，我或许会产生一些不同的看法。&lt;/p&gt;
&lt;p&gt;那段时间我主要进行纯净物晶体的制备，从最基础的五水合硫酸铜、十二水合硫酸铝钾，到硫酸铬钾、氯化钠、氯化钾、硫酸亚铁，再到铁氰化钾、硫酸镍铵、硫酸锰。随着经验的积累，我对溶液的析出结晶细节愈发熟悉，能够获得更为规则、晶莹剔透的晶体。&lt;/p&gt;
&lt;p&gt; 十二水合硫酸铝钾包裹的硫酸铬钾 &lt;/p&gt;
&lt;p&gt;最后由于时间久远，本文中的图片没有完全使用我自己拍摄的图片，使用了一些网络图片作为补充（将标明）。&lt;/p&gt;
&lt;h2&gt;二、一般的制作流程&lt;/h2&gt;
&lt;p&gt;制作晶体的方法多种多样，大体上分为两种，分别是液相结晶和熔融凝固结晶法。&lt;/p&gt;
&lt;p&gt;在家中进行晶体制作的条件比较简陋，因此一般采取水液相结晶的方法。&lt;/p&gt;
&lt;p&gt;熔融凝固结晶法一般用来制作金属晶体，比如铋和镓的晶体，需要较高的温度，并且某些物质还会受热分解，因此难度较高。&lt;/p&gt;
&lt;p&gt;其它溶剂液相结晶法可以用来制作在水中溶解度低的物质的晶体， 比如使用四氯化碳制作硫晶体、 无水乙醇制作碘晶体， 但是由于有机溶剂一般具有较大的毒性且挥发性较强，往往不采用这个方法制作。&lt;/p&gt;
&lt;h3&gt;（一）晶体类型选择&lt;/h3&gt;
&lt;p&gt;第一次制备晶体时的最佳选择是五水合硫酸铜（胆矾）。硫酸铜晶体析出的稳定性极强，制备难度极低。 硫酸铜溶解度高， 溶液稍有杂质或者过饱和也能长出明显的平行四边形晶体， 颜色亮蓝色鲜艳好看， 且毒性低。 同时它的晶体容易悬吊，适合使用悬吊法制做出极大的单晶。&lt;/p&gt;
&lt;p&gt;其次就是十二水合硫酸铝钾（明矾）和十二水合硫酸铬钾（铬矾），因为它们是类质同晶的， 也就是可以做它们的不同配比的混晶， 做出来的晶体为正六面体或者正八面体，形状规则好看。&lt;/p&gt;
&lt;p&gt; 体积较大的分层明矾铬钾 &lt;/p&gt;
&lt;p&gt;稍有难度的是氯化钠的晶体，虽然常用的食盐中绝大部分都是它，但是想要做一个大且透明的单晶是难上加难， 制作的时候一定不要使用食盐， 因为杂质与添加剂太多， 不会形成单晶。 氯化钠晶体的制作难度首先是它的溶解度较低， 想要冷却热饱和溶液制备晶种几乎不可能， 其次是容易产生碎晶， 也很难做出晶莹剔透的晶体。&lt;/p&gt;
&lt;p&gt;一般在制备的时候都会添加一些甘氨酸[^1] （8g/L） 作为添加剂以提高氯化钠晶体的透明度， 如果想要进一步提高晶体的透明度可以加入少量的硝酸铅[^2]（0.1g/L）。&lt;/p&gt;
&lt;p&gt; 氯化钠单晶 &lt;/p&gt;
&lt;h3&gt;（二）配置母液&lt;/h3&gt;
&lt;p&gt;首先应该配置饱和溶液。一般采用母液法，虽然理论上可以通过查询溶解度表， 再计算溶质与结晶水的质量来使溶液恰好饱和， 但是考虑到晶体制作的流水线连续性、 溶液复用性、 室温的波动以及溶解度表的不准确性， 使用一个烧杯存放母液作为缓冲往往可以得到更好的效果。&lt;/p&gt;
&lt;p&gt;首先需要准备一个容量较大的烧杯作为母液烧杯， 根据溶解度加入大致质量的溶质并等待一段时间， 如果烧杯底部仍有未溶解的溶质 （因为溶解结晶是动态平衡，因此多余的试剂往往会变成晶体&quot;大饼&quot;），就说明这杯溶液饱和了，我们将其作为母液，此时母液的上层清液便是饱和溶液。&lt;/p&gt;
&lt;p&gt; 烧杯底部的晶体&quot;大饼&quot;[^2] &lt;/p&gt;
&lt;h3&gt;（三）析晶方法&lt;/h3&gt;
&lt;p&gt;现在我们有了饱和溶液， 那么具体如何操作才能使晶体从水中析出呢？一般有两种方法，分别是冷却热饱和溶液法和常温蒸发结晶法。&lt;/p&gt;
&lt;p&gt;冷却热饱和溶液法适用于溶解度对温度变化敏感的溶质，比如硫酸铜。具体操作是对每 100 克水加入比当前温度溶解度多 20 克左右的溶质（加多了容易析出大饼） ， 搅拌加热至完全溶解， 将溶液立刻倒入结晶皿中并等待结晶，此时要注意在结晶皿上盖一张滤纸以防止灰尘落入结晶皿， 灰尘可能会造成溶液迅速析出大量碎晶；待温度下降溶液结晶后用镊子夹出品相较好的小晶体作为晶核/晶种， 注意一旦镊子进入溶液也会造成溶液迅速析出大量碎晶。 在拿出心仪的小晶种之后，将剩余液体与碎晶倒回母液烧杯。&lt;/p&gt;
&lt;p&gt;冷却热饱和溶液法由于溶解度下降的速率较快，往往用来制作大量小晶种，也就是以量取胜，难以制作单个的形状规则的大晶体。&lt;/p&gt;
&lt;p&gt; 冷却热硫酸铜溶液析晶[^3] &lt;/p&gt;
&lt;p&gt;常温蒸发结晶法适用于任何种类的溶质，只要取母液的上层清液，倒入结晶皿，放入小晶核（不放其实也可以），静置析晶即可。&lt;/p&gt;
&lt;p&gt;常温蒸发结晶法除了可以将晶种放在结晶皿底部，也可以用透明细鱼线（或许头发也可以） 将晶种悬挂在饱和溶液中， 这个方法的好处在于晶体底部不会被结晶皿底部限制而产生不自然的凹面，但是坏处是鱼线会留在晶体内部。&lt;/p&gt;
&lt;p&gt; 栓绳的胆矾晶体与悬吊法生长的明矾 &lt;/p&gt;
&lt;p&gt;最后在从溶液中拿出晶体后，一定要注意立刻将晶体表面的溶液擦干，因为表面的溶液由于于空气接触面积极大， 会迅速风干， 在晶体表面留下许多细小的杂晶，导致一段时间过后晶体表面变得粗糙。&lt;/p&gt;
&lt;h3&gt;（四）保存方法&lt;/h3&gt;
&lt;p&gt;在得到了这些漂亮的晶体之后我们显然不会把它们又放回母液中溶掉， 而是想把它们保存下来， 那么现在就要考虑如何保存晶体才能让保存时间长久还能好好地把玩晶体。&lt;/p&gt;
&lt;p&gt;另外如果晶体因为保存不慎表面被风化了一些， 可以通过将晶体放回饱和溶液几秒钟，直到表面的脱水白色部分变回到正常含结晶水的颜色。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拍照保存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;拍照保存实在是保存时间最久的方法， 下图中的硫酸亚铁早已因为保存不当而丢失结晶水风化为白色粉末了，但是它的遗照仍然存放在我的电脑中。&lt;/p&gt;
&lt;p&gt; 硫酸亚铁（N 年之前） &lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;使用矿物标本盒&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顾名思义，这个方法往往用来展示固体标本而不是长时间存放，如果是易风化（含有结晶水的任何物质）、光解（三草酸合铁酸钾）的晶体不要使用这样的方法展示。&lt;/p&gt;
&lt;p&gt; 矿标盒里的铝钾铬钾混晶[^4] &lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;使用密封袋&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;好处在于价格亲民，保存非常方便，对于普通的晶体如氯化钠，可以做到长期的保存。但如果稍微有一点难度，如会风化（硫酸铜）、吸湿（氢氧化钠）、光解（三草酸合铁酸钾）、热分解（高锰酸钾）、氧化（硫酸亚铁），只用密封袋保存的话，它们会迅速变质，因此往往将晶体用清漆包裹一层再放入密封袋。&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;西林瓶+液体石蜡（石蜡油）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;能够较为长久地保存晶体，但是坏处在于石蜡油极其难以清除，如果需要使用晶体或者用其他方法保存晶体，表面的石蜡油就很难清理干净了。&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;环氧树脂&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个方法可以非常长久的保存晶体 （除了光解、 热分解的晶体） ， 方便把玩，不怕摔碎， 实在是保存晶体的上佳方式。 但是不建议与还原性物质一起使用， 可能会造成晶体的氧化变质，比如硫酸亚铁可能会氧化发黑[^5]。&lt;/p&gt;
&lt;p&gt; 环氧树脂里的铁氰化钾[^6] &lt;/p&gt;
&lt;h2&gt;三、 实验安全&lt;/h2&gt;
&lt;h3&gt;（一）选材安全&lt;/h3&gt;
&lt;p&gt;应当遵守法律法规，遵守《危险化学品安全管理条例》、《中华人民共和国治安管理处罚法》、 《中华人民共和国消防法》，不使用有毒有机溶剂结晶、不使用有毒性、致癌的药品，如重铬酸钾。&lt;/p&gt;
&lt;h3&gt;（二）过程安全&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;生活区与实验区必须完全分离， 生活区禁止存放化学试剂， 特别是禁止在冰箱中同时存放食物和化学试剂；试验区不得进食或使用厨具。&lt;/li&gt;
&lt;li&gt;戴手套， 尤其是使用重金属盐时， 不戴手套最常见的问题是镍离子导致皮肤过敏、重金属盐（如银离子）导致手掌蛋白质变色。&lt;/li&gt;
&lt;li&gt;不使用不规范、有风险的操作，如：直接加热烧杯、重复使用过滤纸，纱布及一切滤纸代用品，并且应逐步取消明火加热。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四、 环保要求&lt;/h2&gt;
&lt;p&gt;尤其需要注意的是废水排放需要遵循《污水综合排放标准》，排入下水道应遵循《污水排入城市下水道水质标准》[^7]，以铜为例，排入下水道的废液中总铜含量应小于 2mg/L。&lt;/p&gt;
&lt;p&gt;以硫酸铜为例给出晶体废液的可能处理方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;储存或放回母液中&lt;/li&gt;
&lt;li&gt;用于制备其他试剂，如斐林试剂&lt;/li&gt;
&lt;li&gt;蒸干回收硫酸铜&lt;/li&gt;
&lt;li&gt;硫化钠沉淀铜离子&lt;/li&gt;
&lt;li&gt;碱沉淀铜离子&lt;/li&gt;
&lt;li&gt;活泼金属置换铜&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;五、 总结&lt;/h2&gt;
&lt;p&gt;家庭制作纯净物晶体的出发点是为了满足对化学实验的好奇、 满足对美丽的化学晶体的喜爱。 仍然记得我做出来的第一个晶体是硫酸铜， 当我把热饱和溶液倒入结晶皿的时候内心非常激动， 甚至盖好了滤纸 （防止灰尘进入） 还是忍不住每隔一小会就揭开看一眼， 最后当我看到了清澈的蓝色溶液中出现了小小的， 但是清晰可见的平行四边形晶体的时候，内心的激动真的是无以言表。&lt;/p&gt;
&lt;p&gt;回到最开始提出的问题，家庭实验是否是有意义的，如果是在化学领域深耕多年的相关从业者， 他可能会说家庭实验毫无意义， 与学习相比是舍本逐末； 而如果是刚开始学习的小白（比如当年的我）可能会说这是兴趣与知识的桥梁。&lt;/p&gt;
&lt;p&gt;如果要由现在的我来评价，在对化学实验有了更理性的认识，收起所有仪器之后，我会说家庭实验可能并不是最理想的学习方式。如果我反思当时的经历，会发现实际上自己完全没有实验规划，只是看到了有趣的实验现象，就会想去尝试复现，完成后又想尝试下一个，因此实验的重点在于观察现象而不是实验前后的思考，这就使实验失去了其教育意义。&lt;/p&gt;
&lt;p&gt;有观点认为，几百年前的化学家通过随机实验发现了许多有意义的反应。然而，这种说法并不准确。事实上，化学家们是基于已有反应构建模型，通过模型预测其他反应的结果，再进行实验验证。这种假说演绎法具有科学思维的意义，与单纯追求实验效果的做法有本质区别。&lt;/p&gt;
&lt;p&gt;本文所述晶体均为亲身制备经历。限于篇幅，未能详述其他晶体的制备过程，如淡粉色的硫酸锰、淡黄色的亚铁氰化钾。正是这些晶体的美感促使我研究单晶的制备技术。时至今日，每一个制备的晶体仍历历在目，这或许正是化学的魅力所在。&lt;/p&gt;
&lt;p&gt;谨以此文总结我在中学阶段与化学的故事和对化学的热爱。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;[^1]: 翁贤芬. 大颗粒氯化钠的制备研究[J]. 盐业与化工, 2009.&lt;/p&gt;
&lt;p&gt;[^2]: 双色大饼，你值得拥有[EB/OL]. 百度贴吧. https://tieba.baidu.com/p/7821134717&lt;/p&gt;
&lt;p&gt;[^3]: 从零开始的晶体教程（3）[EB/OL]. 科创网. https://www.kechuang.org/t/88078&lt;/p&gt;
&lt;p&gt;[^4]: 百度贴吧[EB/OL]. https://tieba.baidu.com/p/7714718573&lt;/p&gt;
&lt;p&gt;[^5]: 从零开始的晶体教程（3.5）[EB/OL]. 科创网. https://www.kechuang.org/t/88081&lt;/p&gt;
&lt;p&gt;[^6]: 百度贴吧[EB/OL]. https://tieba.baidu.com/p/7714718573&lt;/p&gt;
&lt;p&gt;[^7]: GB/T 31962-2015, 污水排入城镇下水道水质标准[S]. http://www.wxbh.gov.cn/doc/2022/09/01/3743836.shtml&lt;/p&gt;</content:encoded><h:img src="/_astro/crystal.qzggYUtQ.png"/><enclosure url="/_astro/crystal.qzggYUtQ.png"/></item><item><title>Compiler Principles Lab Notes</title><link>https://www.lyt0112.com/blog/compiler_principles_lab_note-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/compiler_principles_lab_note-zh</guid><description>Implement a compiler that compiles SysY language to Koopa IR, finally to RISC-V assembly</description><pubDate>Sun, 12 Jan 2025 02:15:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/components/user&apos;
import GithubCard from &apos;@/components/advanced/GithubCard.astro&apos;&lt;/p&gt;
&lt;h2&gt;Related Links&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/EmptyBlueBox/Compiler_Principles-2024Fall-PKU&quot;&gt;编译原理资料仓库&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;本文档中所有代码均在 &lt;a href=&quot;https://github.com/EmptyBlueBox/Compiler_Principles_Lab-2024Fall-PKU&quot;&gt;我的编译原理 Lab 仓库&lt;/a&gt; 中开源, 欢迎大家参考.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://pku-minic.github.io/online-doc/#/&quot;&gt;实验文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://course.educg.net/&quot;&gt;实验评测平台&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.eduxiji.net/&quot;&gt;实验代码上传平台: GitLab&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://arthals.ink/tags/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86&quot;&gt;Arthals 的编译原理资料&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;记录编程过程中的思考和编码规范, 通过制定明确的原则来提高代码的可读性和可维护性, 例如寄存器和栈的管理策略.&lt;/li&gt;
&lt;li&gt;通过具体示例展示一些最佳实践, 致力于&lt;strong&gt;在代码抽象程度和可读性之间寻求平衡&lt;/strong&gt;, 以实现逻辑简洁性和代码可理解性的统一.&lt;/li&gt;
&lt;li&gt;补充实验文档中未详细说明的问题及其解决方案, 为后续实验者提供参考.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下是一些小建议~&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个较容易的完成 Lab 的方法是在写每一个 Lab &lt;code&gt;x&lt;/code&gt; 的时候, 将我的 (或者别人的) Lab &lt;code&gt;x-1&lt;/code&gt; 和 Lab &lt;code&gt;x&lt;/code&gt; 的代码进行对比, 比如 &lt;code&gt;VSCode&lt;/code&gt; 的 &lt;code&gt;compare selected&lt;/code&gt; 功能, 然后在自己的 Lab &lt;code&gt;x-1&lt;/code&gt; 的基础上写 Lab &lt;code&gt;x&lt;/code&gt; 的代码 (而不是抄袭别人的 Lab &lt;code&gt;x&lt;/code&gt; 的代码) , 这样能保证考虑到所有的逻辑, 大幅减少 debug 的时间, 还能锻炼自己阅读代码的能力 🌚.&lt;/li&gt;
&lt;li&gt;每一个 Level 的测试点在你实现正确的情况下都是可以全部通过的, 如果你没有通过说明你的逻辑存在问题, 不存在需要后面的特性才能解决前面测试点的情况.&lt;/li&gt;
&lt;li&gt;强烈建议大家在大四没有绩点压力的时候选这门课!
&lt;ul&gt;
&lt;li&gt;这样最后可以放掉 Lv9 不完成, 因为 Lv9 非常浪费时间并且占所有 Lab 的 &lt;code&gt;27%&lt;/code&gt; 的分数, 也就是总评的 8 分, 如果你在绩点压力下完成所有 Lab 会浪费很多时间.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Lv 0&lt;/h2&gt;
&lt;h3&gt;Prepare Environment&lt;/h3&gt;
&lt;p&gt;首先安装并运行 &lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt;拉取镜像:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull maxxing/compiler-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用镜像创建容器:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -it --name compiler -v &amp;#x3C;compiler_lab_path&gt;:/root/compiler maxxing/compiler-dev bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中参数的含义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-it&lt;/code&gt; 表示以交互模式运行容器.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--name compiler&lt;/code&gt; 表示将容器重命名为 &lt;code&gt;compiler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v &amp;#x3C;compiler_lab_path&gt;:/root/compiler&lt;/code&gt; 表示将宿主机的编译 Lab 项目文件夹挂载到容器中.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxxing/compiler-dev&lt;/code&gt; 表示使用 &lt;code&gt;maxxing&lt;/code&gt; 提供的 &lt;code&gt;compiler-dev&lt;/code&gt; 镜像.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bash&lt;/code&gt; 表示以 &lt;code&gt;bash&lt;/code&gt; 为启动命令.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;博主的编译项目结构类似这样, 其中 &lt;code&gt;~/Documents/xxx/Lv7&lt;/code&gt; 就是你的宿主机编译 Lab 项目文件夹:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Mac OS 1926-08-17 12:00:00
emptyblue ~/Documents/xxx/Lv7
❯ tree 

.
├── CMakeLists.txt
└── src
    ├── include
    │   ├── koopa.h
    │   ├── koopa.hpp
    │   ├── koopa_util.hpp
    │   ├── riscv.hpp
    │   └── riscv_util.hpp
    ├── koopa.cpp
    ├── koopa_util.cpp
    ├── main.cpp
    ├── riscv.cpp
    ├── riscv_util.cpp
    ├── sysy.l
    └── sysy.y

3 directories, 13 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果非 MacOS, 可以加入 &lt;code&gt;--cap-add=SYS_PTRACE --security-opt seccomp=unconfined&lt;/code&gt; 表示添加 &lt;code&gt;SYS_PTRACE&lt;/code&gt; 权限, 取消 seccomp 限制, 以方便使用 &lt;code&gt;LLDB&lt;/code&gt; 在 Docker 中调试.&lt;/p&gt;
&lt;p&gt;但是我没有找到方法在 MacOS 上的 Docker 中运行 &lt;code&gt;LLDB&lt;/code&gt;, 如果你找到了可以解决这个问题的办法, 可以在下面写一个评论!&lt;/p&gt;
&lt;p&gt;2024-12-23 Update:&lt;/p&gt;
&lt;p&gt;似乎助教的实验文档提及了如何调试: &lt;a href=&quot;https://pku-minic.github.io/online-doc/#/misc-app-ref/environment?id=%E8%B0%83%E8%AF%95-risc-v-%E7%A8%8B%E5%BA%8F&quot;&gt;调试 risc-v 程序&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;每次需要进入容器时, 先启动容器, 再进入:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker start compiler
docker exec -it compiler bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;退出容器 (Control + D):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看所有容器:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止所有容器:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker stop $(docker ps -aq)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除 &lt;code&gt;compiler&lt;/code&gt; 容器:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm -f compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除所有容器:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm $(docker ps -aq)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;请仔细阅读 &lt;a href=&quot;https://pku-minic.github.io/online-doc/#/misc-app-ref/environment&quot;&gt;实验环境使用说明&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;cmake&lt;/code&gt; 生成 Makefile 文件, 指定编译类型为 &lt;code&gt;Debug&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake -DCMAKE_BUILD_TYPE=Debug -B build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;cmake&lt;/code&gt; 生成 Makefile 文件, 不指定编译类型:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake -B build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;cmake&lt;/code&gt; 编译:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake --build build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv&amp;#x3C;lv_number&gt; /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv&amp;#x3C;lv_number&gt; /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果本地测试不通过, 可以把 &lt;code&gt;/opt/bin/testcases&lt;/code&gt; 中的测试用例复制到当前路径进行查看调试.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp -r /opt/bin/testcases .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想测试自己的测试用例, 可以将自己的测试用例所在的目录传给 &lt;code&gt;autotest&lt;/code&gt; 命令, 比如:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -t &amp;#x3C;test_case_dir&gt; /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意每一个 Test Case 包含两个名称相同的 &lt;code&gt;.c&lt;/code&gt; 和 &lt;code&gt;.out&lt;/code&gt; 文件, 其中 &lt;code&gt;.c&lt;/code&gt; 是输入给编译器的代码, &lt;code&gt;.out&lt;/code&gt; 是你的编译器应该返回的结果.&lt;/p&gt;
&lt;h2&gt;Lv 1&lt;/h2&gt;
&lt;p&gt;Lab 的第一个 checkpoint 要求大家完成编译器的基础逻辑结构搭建, 包括:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;词法分析器的逻辑&lt;/li&gt;
&lt;li&gt;语法分析器的逻辑&lt;/li&gt;
&lt;li&gt;简单的中间代码 &lt;code&gt;Koopa&lt;/code&gt; 生成器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我使用的是 CMake 作为构建系统, 采用了 &lt;a href=&quot;https://github.com/pku-minic/sysy-cmake-template&quot;&gt;基于 CMake 的 SysY 编译器项目模板&lt;/a&gt; 的结构.&lt;/p&gt;
&lt;p&gt;可以先把&lt;a href=&quot;https://pku-minic.github.io/online-doc/#/&quot;&gt;实验文档&lt;/a&gt;中 &lt;code&gt;Lv 1&lt;/code&gt; 中给出的代码复制下来作为 &lt;code&gt;codebase&lt;/code&gt; .&lt;/p&gt;
&lt;h3&gt;Lexer&lt;/h3&gt;
&lt;p&gt;我们只需要在 &lt;code&gt;src/sysy.l&lt;/code&gt; 中写一段代码说明如何定义 Token 类型, 如何把读到的字符串转化为整数或者浮点数, 然后就可以使用 &lt;code&gt;flex&lt;/code&gt; 读入 &lt;code&gt;src/sysy.l&lt;/code&gt; 来生成词法分析器, 所以实际上不用自己写一个词法分析器.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex&lt;/code&gt; 的输入文件的语法规则可以参考&lt;a href=&quot;https://pku-minic.github.io/online-doc/#/lv1-main/lexer-parser&quot;&gt;实验文档对应章节&lt;/a&gt; .&lt;/p&gt;
&lt;p&gt;示例:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;&quot;int&quot;           { return INT; }
&quot;return&quot;        { return RETURN; }
{Identifier}    { yylval.str_val = new string(yytext); return IDENT; }
{Decimal}       { yylval.int_val = strtol(yytext, nullptr, 0); return INT_CONST; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;INT&lt;/code&gt;, &lt;code&gt;RETURN&lt;/code&gt;, &lt;code&gt;IDENT&lt;/code&gt; 等返回值, 其实是 Bison 生成的固定枚举类型值, 就是一个整数.&lt;/p&gt;
&lt;p&gt;所以这段代码代表的含义为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&quot;int&quot;&lt;/code&gt; 和 &lt;code&gt;&quot;return&quot;&lt;/code&gt; 是正则表达式, 这就是告诉当匹配到这些字符串时, 返回给语法分析器 &lt;code&gt;INT&lt;/code&gt; 和 &lt;code&gt;RETURN&lt;/code&gt; 类型, 告诉语法分析器这是一个保留字.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{Identifier}&lt;/code&gt; 是上面定义好的正则表达式, 当这个表达式匹配到某个字符串时, 将这个字符串赋值给语法分析器定义的 &lt;code&gt;yylval.str_val&lt;/code&gt; 变量, 然后返回给语法分析器 &lt;code&gt;IDENT&lt;/code&gt; 类型, 告诉语法分析器这是一个标识符, 比如函数名, 语法分析器知道现在读取到了一个标识符, 就从 &lt;code&gt;yylval.str_val&lt;/code&gt; 中取出这个字符串.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{Decimal}&lt;/code&gt; 是上面定义好的正则表达式, 当这个表达式匹配到某个字符串时, 将这个字符串赋值给语法分析器定义的 &lt;code&gt;yylval.int_val&lt;/code&gt; 变量, 然后返回给语法分析器 &lt;code&gt;INT_CONST&lt;/code&gt; 类型, 告诉语法分析器这是一个整数常量, 语法分析器知道现在读取到了一个整数常量, 就从 &lt;code&gt;yylval.int_val&lt;/code&gt; 中取出这个整数.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Parser&lt;/h3&gt;
&lt;p&gt;我们只需要在 &lt;code&gt;src/sysy.y&lt;/code&gt; 中写一段代码说明如何定义语法规则, 比如一个函数是如何由函数类型, 函数名, 变量声明列表和函数体组成的, 然后就可以使用 &lt;code&gt;bison&lt;/code&gt; 读入 &lt;code&gt;src/sysy.y&lt;/code&gt; 来生成语法分析器.&lt;/p&gt;
&lt;p&gt;规约示例:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;FuncDef
  : FuncType IDENT &apos;(&apos; &apos;)&apos; Block {
    auto ast = new FuncDefAST();
    ast-&gt;func_type = unique_ptr&amp;#x3C;BaseAST&gt;($1);
    ast-&gt;ident = *unique_ptr&amp;#x3C;string&gt;($2);
    ast-&gt;block = unique_ptr&amp;#x3C;BaseAST&gt;($5);
    $$ = ast;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其中 &lt;code&gt;FuncDefAST&lt;/code&gt; 等类是 &lt;code&gt;include/ast.hpp&lt;/code&gt; 中由你定义好的, 其中包括了每个语法规则包含的语法单元, 比如 &lt;code&gt;FuncDef&lt;/code&gt; 语法规则包含 &lt;code&gt;FuncType&lt;/code&gt;, &lt;code&gt;IDENT&lt;/code&gt;, &lt;code&gt;Block&lt;/code&gt; 等语法单元, 语法分析器现在使用这些类来构造抽象语法树.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$1&lt;/code&gt; 是代表子语法单元的变量, 比如 &lt;code&gt;FuncDef&lt;/code&gt; 语法规则的第一个子语法单元是 &lt;code&gt;FuncType&lt;/code&gt;, 那么 &lt;code&gt;$1&lt;/code&gt; 就代表 &lt;code&gt;FuncType&lt;/code&gt;, 语法分析器递归地从 &lt;code&gt;FuncType&lt;/code&gt; 继续规约, 注意这里的变量标号是从 1 开始的.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$$ = ast;&lt;/code&gt; 是告诉语法分析器, &lt;code&gt;FuncDef&lt;/code&gt; 语法规则规约的结果是一个 &lt;code&gt;FuncDefAST&lt;/code&gt; 类, 语法分析器现在知道 &lt;code&gt;FuncDef&lt;/code&gt; 语法规则规约的结果 &lt;code&gt;FuncDefAST&lt;/code&gt; 类中都是什么数据了.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;Koopa IR&lt;/code&gt; Generation&lt;/h3&gt;
&lt;p&gt;当语法分析器规约出抽象语法树后, 我们就可以遍历抽象语法树, 遇到叶子节点就 &lt;code&gt;print&lt;/code&gt; , 遍历结束就生成了中间代码 &lt;code&gt;Koopa&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;main.cpp&lt;/code&gt; 中定义编译器本身的 &lt;code&gt;main&lt;/code&gt; 函数, 读入需要编译的源文件, 调用词法分析器和语法分析器, 得到放好数据的抽象语法树, 然后调用语法树类中定义好的 &lt;code&gt;print&lt;/code&gt; 函数生成中间代码 &lt;code&gt;Koopa&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;我的具体结构安排是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;main.cpp&lt;/code&gt; 中定义编译器本身的 &lt;code&gt;main&lt;/code&gt; 函数, 读入需要编译的源文件, 调用词法分析器和语法分析器, 得到放好数据的抽象语法树, 然后调用语法树类中定义好的 &lt;code&gt;print&lt;/code&gt; 函数生成中间代码 &lt;code&gt;Koopa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;include/ast.hpp&lt;/code&gt; 中定义抽象语法树的节点类, 每个节点类中定义一个 &lt;code&gt;print&lt;/code&gt; 函数, 用于生成中间代码 &lt;code&gt;Koopa&lt;/code&gt; 或 Debug 信息.&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;ast.cpp&lt;/code&gt; 中定义抽象语法树的各个节点类, 并实现 &lt;code&gt;print&lt;/code&gt; 函数.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Incomplete Parts of the Codebase&lt;/h3&gt;
&lt;p&gt;在 handout 给出的 codebase 中, 词法分析器和语法分析器大部分已经写好了, 但是还有一些需要修改的地方:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;src/sysy.y&lt;/code&gt; 中, 需要加入 &lt;code&gt;include/ast.hpp&lt;/code&gt; 的引用, 否则语法分析器会找不到你定义的抽象语法树的节点类, 就不能把数据写到你定义的抽象语法树中.&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;include/ast.hpp&lt;/code&gt; 中, 需要定义示例代码中没有给出的抽象语法树的节点类, 并定义 &lt;code&gt;print&lt;/code&gt; 函数.&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;ast.cpp&lt;/code&gt; 中, 需要实现示例代码中没有实现的抽象语法树的各个节点类, 并实现 &lt;code&gt;print&lt;/code&gt; 函数.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确保在 &lt;code&gt;src/sysy.l&lt;/code&gt; 中定义正确的 BlockComment 的正则表达式, 否则有一些测试点不通过.&lt;/p&gt;
&lt;p&gt;好的, 现在已经完成了编译器的基础逻辑结构搭建!&lt;/p&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;以下均默认你已经进入容器, 并且当前目录为编译 Lab 的一级目录.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;cmake&lt;/code&gt; 生成 Makefile 文件:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake -DCMAKE_BUILD_TYPE=Debug -B build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;cmake&lt;/code&gt; 编译:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cmake --build build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv1 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果本地测试不通过, 可以把 &lt;code&gt;/opt/bin/testcases&lt;/code&gt; 中的测试用例复制到当前路径进行查看调试.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp -r /opt/bin/testcases .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv 2&lt;/h2&gt;
&lt;p&gt;首先回顾一下编译器的三层结构:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端&lt;/strong&gt;: 通过词法分析和语法分析, 将源代码解析成抽象语法树 (abstract syntax tree, AST). 通过语义分析, 扫描抽象语法树, 检查其是否存在语义错误.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中端&lt;/strong&gt;: 将抽象语法树转换为中间表示 (intermediate representation, IR), 并在此基础上完成一些机器无关优化.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端&lt;/strong&gt;: 将中间表示转换为目标平台的汇编代码, 并在此基础上完成一些机器相关优化.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Lv 1 中已经完成了前端和中端, 现在来完成后端.&lt;/p&gt;
&lt;p&gt;虽说是完成后端, 但是实际上助教团队已经帮助实现好了能够处理 Koopa IR 的库, 我们只需要调用他们提供的库 (即调用 &lt;code&gt;koopa.h&lt;/code&gt; 中定义的函数) 就可以完成后端的大部分实现了, 我们只需要自己实现 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码的输出就可以了.&lt;/p&gt;
&lt;p&gt;具体如何调用, 参阅 &lt;a href=&quot;https://pku-minic.github.io/online-doc/#/lv2-code-gen/processing-ir&quot;&gt;实验文档&lt;/a&gt; 把代码复制下来即可.&lt;/p&gt;
&lt;p&gt;最后的后端代码入口应该类似:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &quot;include/backend.hpp&quot;

int backend(const char *koopa_str)
{
    // 解析字符串 str, 得到 Koopa IR 程序
    koopa_program_t program;
    koopa_error_code_t ret = koopa_parse_from_string(koopa_str, &amp;#x26;program);
    assert(ret == KOOPA_EC_SUCCESS); // 确保解析时没有出错
    // 创建一个 raw program builder, 用来构建 raw program
    koopa_raw_program_builder_t builder = koopa_new_raw_program_builder();
    // 将 Koopa IR 程序转换为 raw program
    koopa_raw_program_t raw = koopa_build_raw_program(builder, program);
    // 释放 Koopa IR 程序占用的内存
    koopa_delete_program(program);

    // 处理 raw program
    visit(raw);

    // 处理完成, 释放 raw program builder 占用的内存
    // 注意, raw program 中所有的指针指向的内存均为 raw program builder 的内存
    // 所以不要在 raw program 处理完毕之前释放 builder
    koopa_delete_raw_program_builder(builder);

    return 0;
}

void visit(const koopa_raw_slice_t &amp;#x26;slice)
{
  // ...
}

void visit(const koopa_raw_program_t &amp;#x26;program)
{
  // ...
}

// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Traverse the Abstract Syntax Tree&lt;/h3&gt;
&lt;p&gt;调用库之后我们就得到了以抽象语法树形式表示的 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码, 现在需要 DFS 遍历这棵抽象语法树, 将其转换为字符串并输出, 我们选择使用函数递归的方式来遍历这棵抽象语法树, 具体如何遍历可以参考实验文档, 这里不再赘述, 但是有一些 high level 的 idea 可以帮助你理解这颗树上的各种助教定义的 type.&lt;/p&gt;
&lt;p&gt;这颗语法树的节点大致是 &lt;code&gt;program&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;basic_block&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, 这些节点很多都包含同类型的东西, 比如一个程序有很多的函数, 一个函数有很多的基本块, 一个基本块有很多指令, 那么比如 &lt;code&gt;program&lt;/code&gt; 这个节点的这一堆函数就会存在&lt;strong&gt;一个&lt;/strong&gt; &lt;code&gt;koopa_raw_slice_t&lt;/code&gt; 类型中, 所以对于一个 &lt;code&gt;program&lt;/code&gt; 节点中的所有函数, 只需要对包含这些函数的这一个 &lt;code&gt;koopa_raw_slice_t&lt;/code&gt; 调用一次 &lt;code&gt;visit&lt;/code&gt; 函数即可, 如下所示, &lt;code&gt;visit&lt;/code&gt; 函数会把这一堆东西逐个帮你访问.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void visit(const koopa_raw_slice_t &amp;#x26;slice)
{
    for (size_t i = 0; i &amp;#x3C; slice.len; ++i)
    {
        auto ptr = slice.buffer[i];
        // 根据 slice 的 kind 决定将 ptr 视作何种元素
        switch (slice.kind)
        {
        case KOOPA_RSIK_FUNCTION:
            // 访问函数
            visit(reinterpret_cast&amp;#x3C;koopa_raw_function_t&gt;(ptr));
            break;
        case KOOPA_RSIK_BASIC_BLOCK:
            // 访问基本块
            visit(reinterpret_cast&amp;#x3C;koopa_raw_basic_block_t&gt;(ptr));
            break;
        case KOOPA_RSIK_VALUE:
            // 访问指令
            visit(reinterpret_cast&amp;#x3C;koopa_raw_value_t&gt;(ptr));
            break;
        default:
            // 我们暂时不会遇到其他内容, 于是不对其做任何处理
            assert(false);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 类型的函数大致如下所示, &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 类型是一个指针, 这个指针可以指向 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码的一个值 (一条指令的结果可以代表这个指令, 所以一条指令也算一个值) .&lt;/p&gt;
&lt;p&gt;如果碰上代表一个指令, 就可能重复调用 &lt;code&gt;visit koopa_raw_value_t&lt;/code&gt; 两次, 因为指令中包含值, 比如调用 &lt;code&gt;visit koopa_raw_value_t&lt;/code&gt; 时发现这是一个返回指令, 就调用 &lt;code&gt;visit koopa_raw_return_t&lt;/code&gt; , 返回值就会需要再调用一次 &lt;code&gt;visit koopa_raw_value_t&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void visit(const koopa_raw_value_t &amp;#x26;value)
{
    // 根据指令类型判断后续需要如何访问
    const auto &amp;#x26;kind = value-&gt;kind;
    switch (kind.tag)
    {
    case KOOPA_RVT_RETURN:
        // 访问 return 指令
        visit(kind.data.ret);
        break;
    case KOOPA_RVT_INTEGER:
        // 访问 integer 指令
        visit(kind.data.integer, value);
        break;
    // ...
    default:
        // 其他类型暂时遇不到
        throw std::runtime_error(&quot;visit: invalid instruction&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Link Middleend and Backend&lt;/h3&gt;
&lt;p&gt;与此同时, 你还需要将你在 Lv 1 中输出的 &lt;code&gt;Koopa IR&lt;/code&gt; 输入给后端, 在这里我推荐使用 &lt;code&gt;std::stringstream&lt;/code&gt; 类型来存储 &lt;code&gt;Koopa IR&lt;/code&gt;, 然后交给后端即可, 实现代码可以类似:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::ostringstream koopa_ir; // 用于存储 Koopa IR 的 stringstream
// ...
ast-&gt;print(&amp;#x26;koopa_ir);
  // ...
  // inside print(): 你的中端代码将 Koopa IR 输出到 koopa_ir 中
  // ...
freopen(output, &quot;w&quot;, stdout); // 将 stdout 重定向到 output 文件, output 是你的输出文件路径
backend(koopa_ir.str().c_str()); // 将 stringstream 转换为 C style string 后交给后端
  // ...
  // inside backend(): 后端代码将 RISC-V 汇编代码输出到 stdout 中
  // ...
fclose(stdout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写的优点包括:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现简洁明了.&lt;/li&gt;
&lt;li&gt;无需进行硬盘交互 (把 Koopa IR 输出到文件系统再从硬盘读取, 再交给后端), 这样实现之后 Koopa IR 的数据保存在内存中, 后端直接从内存中读取即可.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv1 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv 3&lt;/h2&gt;
&lt;p&gt;本章将在上一章的基础上, 实现一个能够处理表达式 (一元/二元) 的编译器.&lt;/p&gt;
&lt;p&gt;需要完成对抽象语法树和 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码输出这两部分的修改.&lt;/p&gt;
&lt;p&gt;你的编译器将可以处理如下的 SysY 程序:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  return 1 + 2 * -3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要完成对抽象语法树和 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码输出这两部分的修改.&lt;/p&gt;
&lt;h3&gt;Lexer&lt;/h3&gt;
&lt;p&gt;修改 &lt;code&gt;src/sysy.l&lt;/code&gt; 文件, 添加对运算符的识别, 比如 &lt;code&gt;!&lt;/code&gt; 和 &lt;code&gt;-&lt;/code&gt; 运算符.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;/* 运算符 */
ExclusiveUnaryOp       &quot;!&quot;
MulOp         [\*/%]
AddOp         [\+\-]
RelOp         (&quot;&amp;#x3C;&quot;|&quot;&gt;&quot;|&quot;&amp;#x3C;=&quot;|&quot;&gt;=&quot;)
EqOp          (&quot;==&quot;|&quot;!=&quot;)
AndOp         &quot;&amp;#x26;&amp;#x26;&quot;
OrOp          &quot;||&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我的逻辑是对每一种运算都定义一个正则表达式, 用来表达这个运算会使用的所有字符, 这样就不用在语法分析器中对每一个运算符都写一个规约规则了.&lt;/p&gt;
&lt;p&gt;但是有一个问题是单元运算符和二元加减运算符有两个符号是重叠的, 所以我选择使用 &lt;code&gt;ExclusiveUnaryOp&lt;/code&gt; 来表示只有单元运算符使用的符号, &lt;code&gt;AddOp&lt;/code&gt; 就代表着单元运算和二元加减运算共同使用的符号了.&lt;/p&gt;
&lt;p&gt;另外还要加入识别 token 之后如何返回给语法分析器, 类似 &lt;code&gt;Identifier&lt;/code&gt; 和 &lt;code&gt;Decimal&lt;/code&gt; 那样, 使用 &lt;code&gt;yylval&lt;/code&gt; 来返回字符串给语法分析器.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;{ExclusiveUnaryOp}      { yylval.str_val = new string(yytext); return EXCLUSIVE_UNARY_OP; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Parser&lt;/h3&gt;
&lt;p&gt;修改 &lt;code&gt;src/sysy.y&lt;/code&gt; 文件和 &lt;code&gt;include/ast.hpp&lt;/code&gt; 文件, 添加对新的语法规则的规约.&lt;/p&gt;
&lt;p&gt;举例说明:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;UnaryExp
  : PrimaryExp {
    auto ast = new UnaryExpAST();
    ast-&gt;primary_exp = unique_ptr&amp;#x3C;BaseAST&gt;($1);
    $$ = ast;
  }
  | EXCLUSIVE_UNARY_OP UnaryExp {
    auto ast = new UnaryExpAST();
    ast-&gt;op = *unique_ptr&amp;#x3C;string&gt;($1);
    ast-&gt;unary_exp = unique_ptr&amp;#x3C;BaseAST&gt;($2);
    $$ = ast;
  }
  | ADD_OP UnaryExp {
    auto ast = new UnaryExpAST();
    ast-&gt;op = *unique_ptr&amp;#x3C;string&gt;($1);
    ast-&gt;unary_exp = unique_ptr&amp;#x3C;BaseAST&gt;($2);
    $$ = ast;
  }
  ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应如下的抽象语法树类:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * @brief 一元表达式抽象语法树类. 
 */
class UnaryExpAST : public BaseAST
{
public:
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; primary_exp; // 可选的基本表达式
    std::optional&amp;#x3C;std::string&gt; op;                       // 可选的操作符 (&quot;+&quot;, &quot;-&quot;, &quot;!&quot;)
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; unary_exp;   // 可选的一元表达式

    /**
     * @brief 打印抽象语法树. 
     * @param[in] output_stream 输出流. 
     * @return 打印操作的结果
     */
    Result print(std::stringstream &amp;#x26;output_stream) const override; // 打印抽象语法树, 稍后解释这个返回类型的用处
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中有两个实现细节:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;std::optional&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;是 &lt;strong&gt;C++17&lt;/strong&gt; 引入的类型, 你可能需要修改 &lt;code&gt;VSCode&lt;/code&gt; 的编译器版本, 否则无法正常高亮显示.&lt;/li&gt;
&lt;li&gt;它用于表示一个可能存在也可能不存在的值, 如果值存在, 则可以使用 &lt;code&gt;value()&lt;/code&gt; 方法获取该值, 如果值不存在, 则可以使用 &lt;code&gt;has_value()&lt;/code&gt; 方法判断是否存在, 或者使用 &lt;code&gt;operator*&lt;/code&gt; 获取该值, 用来判断在多个规约规则中具体选了那个规约规则.&lt;/li&gt;
&lt;li&gt;比如下面的规约规则有两个选择, 分别是 &lt;code&gt;PrimaryExp&lt;/code&gt; 和 &lt;code&gt;UnaryOp UnaryExp&lt;/code&gt;, 如果选择了 &lt;code&gt;PrimaryExp&lt;/code&gt; 那么 &lt;code&gt;primary_exp&lt;/code&gt; 就会存在, &lt;code&gt;op&lt;/code&gt; 和 &lt;code&gt;unary_exp&lt;/code&gt; 就不存在, 反之亦然.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;print&lt;/code&gt; 函数返回一个 &lt;code&gt;Result&lt;/code&gt; 类型的变量, 稍后解释这个返回类型的用处.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上的代码代表着如下的规约规则:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;UnaryExp    ::= PrimaryExp | UnaryOp UnaryExp;
UnaryOp     ::= &quot;+&quot; | &quot;-&quot; | &quot;!&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中使用了我们在对词法分析器作修改的时候定义的新 token 种类 &lt;code&gt;EXCLUSIVE_UNARY_OP&lt;/code&gt; 和 &lt;code&gt;ADD_OP&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;当然别忘了要在 &lt;code&gt;src/sysy.y&lt;/code&gt; 文件中定义新的终结符和非终结符的类型.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;// lexer 返回的所有 token 种类的声明, 终结符的类型为 str_val 和 int_val
%token INT RETURN
%token &amp;#x3C;str_val&gt; IDENT
%token &amp;#x3C;int_val&gt; INT_CONST
%token &amp;#x3C;str_val&gt; EXCLUSIVE_UNARY_OP MUL_OP ADD_OP REL_OP EQ_OP AND_OP OR_OP // Operators

// 非终结符的类型定义
%type &amp;#x3C;ast_val&gt; FuncDef FuncType Block Stmt Exp UnaryExp PrimaryExp MulExp AddExp LOrExp LAndExp RelExp EqExp
%type &amp;#x3C;int_val&gt; Number
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;Koopa IR&lt;/code&gt; Generation&lt;/h3&gt;
&lt;p&gt;修改 &lt;code&gt;ast.cpp&lt;/code&gt; 文件, 添加对新的语法规则的 &lt;code&gt;print&lt;/code&gt; 函数.&lt;/p&gt;
&lt;p&gt;在实现之前我们先来思考一个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  return 6;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是之前的编译器可以处理的代码, 我们在调用 &lt;code&gt;RetAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数时, 先输出 &lt;code&gt;ret &lt;/code&gt; 然后调用 &lt;code&gt;NumberExpAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数, 输出 &lt;code&gt;6&lt;/code&gt;, 最后回到 &lt;code&gt;RetAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数输出 &lt;code&gt;\n&lt;/code&gt;, 就可以得到如下的 &lt;code&gt;Koopa IR&lt;/code&gt; 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
  ret 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们考虑这个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  return -6;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果还是按照之前的逻辑, 先输出 &lt;code&gt;ret &lt;/code&gt; 然后调用 &lt;code&gt;ExpAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数, 就会出现问题, 因为我们还需要一个 &lt;code&gt;sub&lt;/code&gt; 指令才能计算出 &lt;code&gt;-6&lt;/code&gt;, 但是此时 &lt;code&gt;Koopa IR&lt;/code&gt; 已经输出到文件中了, 我们无法在 &lt;code&gt;ret&lt;/code&gt; 指令后面继续输出 &lt;code&gt;sub&lt;/code&gt; 指令了.&lt;/p&gt;
&lt;p&gt;这是我们期望得到的 &lt;code&gt;Koopa IR&lt;/code&gt; 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
  %0 = sub 0, 6
  ret %0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们在进入任何一个 &lt;code&gt;print&lt;/code&gt; 函数时, 不能先入为主地输出任何 &lt;code&gt;Koopa IR&lt;/code&gt; 指令, 需要先调用这个抽象语法树的所有子变量的 &lt;code&gt;print&lt;/code&gt; 函数, 等到它们把类似上文的 &lt;code&gt;sub&lt;/code&gt; 指令输出完成之后再输出当前的 &lt;code&gt;Koopa IR&lt;/code&gt; 指令.&lt;/p&gt;
&lt;p&gt;但是如果子变量的所有 &lt;code&gt;print&lt;/code&gt; 函数都没有返回任何信息, 那么我们怎么知道这些子变量把计算结果储存到哪里了呢?&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;RetAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数, 当它调用完成 &lt;code&gt;ExpAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数之后, 它怎么知道 &lt;code&gt;ExpAST&lt;/code&gt; 的计算结果是储存在 &lt;code&gt;%0&lt;/code&gt; 这个寄存器中了而不是 &lt;code&gt;%1&lt;/code&gt; 或者其他寄存器中呢?&lt;/p&gt;
&lt;p&gt;我们不希望使用全局变量解决任何问题, 这样非常 dirty, 所以我们需要每一个 &lt;code&gt;print&lt;/code&gt; 函数返回一个 &lt;code&gt;Result&lt;/code&gt; 类型的变量, 告诉父变量这个子变量的计算结果储存在哪里, 以便父变量决定如何输出当前的 &lt;code&gt;Koopa IR&lt;/code&gt; 指令.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * @brief 用于存储计算结果的类, 可以是符号或立即数. 
 * @note 如果当前函数会产生一个计算结果, 那么这个计算结果会存储在返回的 `Result` 类型的变量中
 * @note 比如 `PrimaryExpAST` 的 `print` 函数, 当它是从数字规约而来时, 它的 `Result` 变量会被初始化为立即数, 返回 `Result(Result::Type::IMM, *number)` 这样一个变量
 * @note 如果当前函数不会产生计算结果, 那么返回的 `Result` 变量会被初始化为立即数 0
 * @date 2024-11-27
 */
class Result
{
public:
    /**
     * @brief 当前计算值, 存储在 `%current_value_symbol_index` 符号中. 
     * @date 2024-11-27
     */
    static int current_symbol_index;

    enum class Type
    {
        IMM, // 立即数
        REG  // 寄存器
    };
    Type type; // 结果的类型
    int val;   // 结果的值

    // 默认构造函数, 初始化为立即数 0, 没有用到它的地方
    Result() : type(Type::IMM), val(0) {}

    // 带有指定类型的构造函数, 主要用来初始化寄存器
    Result(Type type) : type(type), val(0)
    {
        if (type == Type::REG)
        {
            val = ++current_symbol_index;
        }
    }

    // 带有指定类型和值的构造函数, 主要用来初始化立即数
    Result(Type type, int val) : type(type), val(val)
    {
        if (type == Type::REG)
        {
            val = ++current_symbol_index;
        }
    }

    // 重载 &amp;#x3C;&amp;#x3C;
    friend std::ostream &amp;#x26;operator&amp;#x3C;&amp;#x3C;(std::ostream &amp;#x26;os, const Result &amp;#x26;result)
    {
        os &amp;#x3C;&amp;#x3C; (result.type == Result::Type::REG ? &quot;%&quot; : &quot;&quot;) &amp;#x3C;&amp;#x3C; result.val;
        return os;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Result&lt;/code&gt; 类中有一个静态变量 &lt;code&gt;current_symbol_index&lt;/code&gt;, 这个变量用于给每一个计算结果分配一个唯一的寄存器. 当 &lt;code&gt;Result&lt;/code&gt; 类被初始化为立即数时, 这个变量不会被用到, 而当 &lt;code&gt;Result&lt;/code&gt; 类被初始化为寄存器时, 这个变量会被用来给计算结果分配一个唯一的寄存器, 然后这个 &lt;code&gt;current_symbol_index&lt;/code&gt; 的值会加一.&lt;/p&gt;
&lt;p&gt;同时为了方便输出寄存器和立即数, 我们重载了 &lt;code&gt;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 操作符.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Result UnaryExpAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (primary_exp &amp;#x26;&amp;#x26; !op &amp;#x26;&amp;#x26; !unary_exp)
    {
        return (*primary_exp)-&gt;print(output_stream);
    }
    else if (!primary_exp &amp;#x26;&amp;#x26; op &amp;#x26;&amp;#x26; unary_exp)
    {
        Result unary_result = (*unary_exp)-&gt;print(output_stream);
        Result result = Result(Result::Type::REG);
        if (*op == &quot;+&quot;)
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = add 0, &quot; &amp;#x3C;&amp;#x3C; unary_result &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
        }
        else if (*op == &quot;-&quot;)
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = sub 0, &quot; &amp;#x3C;&amp;#x3C; unary_result &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
        }
        else if (*op == &quot;!&quot;)
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = eq 0, &quot; &amp;#x3C;&amp;#x3C; unary_result &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
        }
        else
        {
            throw std::runtime_error(&quot;UnaryExpAST::print: invalid unary operator&quot;);
        }
        return result;
    }
    else
    {
        throw std::runtime_error(&quot;UnaryExpAST::print: invalid unary expression&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样看起来就很清晰了.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当 &lt;code&gt;UnaryExpAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数被调用时, 如果它选择了 &lt;code&gt;UnaryExp ::= PrimaryExp&lt;/code&gt; 这条规约规则, 那么它就会调用 &lt;code&gt;PrimaryExpAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数&lt;/li&gt;
&lt;li&gt;此时 &lt;code&gt;UnaryExpAST&lt;/code&gt; 并没有做任何计算, 所以直接返回 &lt;code&gt;PrimaryExpAST&lt;/code&gt; 的计算结果即可.&lt;/li&gt;
&lt;li&gt;如果它选择了 &lt;code&gt;UnaryExp ::= UnaryOp UnaryExp&lt;/code&gt; 这条规约规则, 就需要根据 &lt;code&gt;UnaryOp&lt;/code&gt; 的值输出相应的 &lt;code&gt;Koopa IR&lt;/code&gt; 指令, 运算的结果需要使用一个新的寄存器来储存, 所以构造一个新的 &lt;code&gt;Result(Result::Type::REG)&lt;/code&gt; 变量, 并返回这个变量.&lt;/li&gt;
&lt;li&gt;最后如果出现任何例外情况, 直接抛出异常, 这样是很好的防御型编程操作实践.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;code&gt;RISC-V&lt;/code&gt; Assembly Code Generation&lt;/h3&gt;
&lt;p&gt;在这一部分你需要修改 &lt;code&gt;include/backend.hpp&lt;/code&gt; 和 &lt;code&gt;src/backend.cpp&lt;/code&gt; 文件, 完成新的语法规则的 &lt;code&gt;print&lt;/code&gt; 函数来输出 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码.&lt;/p&gt;
&lt;p&gt;这部分的难点在于如何分配寄存器, 储存在内存中的 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码是没有进行寄存器分配的.&lt;/p&gt;
&lt;p&gt;比如一个加法运算, 你只知道左操作数和右操作数是两个表达式, 但是你并不知道这两个表达式的结果分别在哪个寄存器当中, 内存中的汇编代码也不提供具体的寄存器编号, 所以你需要在输出的同时为每一行运算都分配一个寄存器来保存运算的结果, 并且不能覆盖之前刚计算完还没用过的寄存器.&lt;/p&gt;
&lt;p&gt;寄存器分配问题是一个 NPC 问题, 但是很好的一点是 Lv3 的测试样例中不会出现需要寄存器复用的情况, 所以我们可以使用贪心算法来解决这个问题.&lt;/p&gt;
&lt;p&gt;首先仔细观察一个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  return 1 + 2 * -3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以得到如下 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;	.text
	.globl main
main:
  li  t0, 2
  li  t1, 3
  mul t1, t0, t1
  li  t2, 1
  add t2, t1, t2
  mv a0, t2
  ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现当前&lt;strong&gt;每一行汇编代码的计算结果都只会被使用一次&lt;/strong&gt;, 如果把这个汇编代码修改为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;	.text
	.globl main
main:
  li  t0, 2
  li  t1, 3
  mul t0, t0, t1
  li  t1, 1
  add t0, t0, t1
  mv a0, t0
  ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不会有任何问题.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;RISC-V&lt;/code&gt; Register Manager&lt;/h3&gt;
&lt;p&gt;为了方便管理寄存器, 我设计了一个 &lt;code&gt;RegisterManager&lt;/code&gt; 类, 这个类可以设置一个寄存器为可以覆盖, 判断一个值是否占用了一个寄存器, 给一个值分配一个寄存器, 输出某个值对应的寄存器名称.&lt;/p&gt;
&lt;p&gt;所有计算结果, 包括一个表达式, 一个立即数, 都是跟一个 &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 类型的指针一一对应的, 所以我们可以使用 &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 类型的指针来作为寄存器管理器中的键值, 这样就可以方便地找到一个值对应的寄存器名称.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * @brief 寄存器管理器, 可以设置一个寄存器为可以覆盖, 判断一个值是否占用了一个寄存器, 给一个值分配一个寄存器, 输出某个值对应的寄存器名称
 */
class RegisterManager
{
private:
    // 值到寄存器名称的映射
    std::unordered_map&amp;#x3C;koopa_raw_value_t, std::string&gt; _value_to_reg_string;
    // 存储当前所有寄存器是否可能会再次被利用, 比如将立即数转移给 a0 寄存器, 我们现在就认为 a0 寄存器被占用了, 但是如果 a0 寄存器之后被调用了, 这个立即数被使用过了, 那么 a0 寄存器就会被标记为不被占用, 因为到目前为止我们认为每一个结果只被使用一次
    std::unordered_map&amp;#x3C;std::string, bool&gt; _reg_is_used;
    /**
     * @brief 设置一个值对应哪个寄存器, 内部函数不被外部调用
     * @param[in] value 值
     * @param[in] reg_string 寄存器名称
     */
    void _set_value_to_reg_string(const koopa_raw_value_t &amp;#x26;value, const std::string &amp;#x26;reg_string);

public:
    /**
     * @brief 构造函数, 初始化所有寄存器为未占用
     */
    RegisterManager()
    {
        // 初始化所有寄存器为未占用
        for (int i = 0; i &amp;#x3C;= 6; ++i)
        {
            _reg_is_used[&quot;t&quot; + std::to_string(i)] = false;
        }
        for (int i = 0; i &amp;#x3C;= 7; ++i)
        {
            _reg_is_used[&quot;a&quot; + std::to_string(i)] = false;
        }
    }

    /**
     * @brief 设置一个值对应的寄存器为未占用, 当一个值被使用过之后, 我们将它占用的寄存器设置为未占用, 因为我们认为每一个结果只被使用一次
     * @param[in] value 值
     */
    void set_reg_free(const koopa_raw_value_t &amp;#x26;value);

    /**
     * @brief 判断一个值是否已经分配了寄存器
     * @param[in] value 值
     * @return 是否已经分配了寄存器
     */
    bool exist(const koopa_raw_value_t &amp;#x26;value);

    /**
     * @brief 给一个值分配一个寄存器, 自动选择一个未被占用的寄存器
     * @note x0 是一个特殊的寄存器, 它的值恒为 0, 且向它写入的任何数据都会被丢弃, t0 到 t6 寄存器, 以及 a0 到 a7 寄存器可以用来存放临时值
     * @param[in] value 值
     * @param[in] is_zero 如果是立即数, 那么是否是立即数 0
     */
    void allocate_reg(const koopa_raw_value_t &amp;#x26;value, bool is_zero = false);

    /**
     * @brief 找出这个值占用哪个寄存器, 用于输出 RISC-V 汇编代码
     * @param[in] value 值
     * @return 寄存器名称
     */
    std::string value_to_reg_string(const koopa_raw_value_t &amp;#x26;value);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;RISC-V&lt;/code&gt; Assembly Code Output&lt;/h3&gt;
&lt;p&gt;为了方便维护 &lt;code&gt;RegisterManager&lt;/code&gt; 类, 我选择将 &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 类型的指针也传给 &lt;code&gt;koopa_raw_integer_t&lt;/code&gt; 和 &lt;code&gt;koopa_raw_binary_t&lt;/code&gt; 的 &lt;code&gt;visit&lt;/code&gt; 函数, 这样在这两个函数中就可以调用 &lt;code&gt;RegisterManager&lt;/code&gt; 类的方法来管理寄存器了.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 访问 koopa_raw_value_t
void visit(const koopa_raw_value_t &amp;#x26;value)
{
    const auto &amp;#x26;kind = value-&gt;kind;
    switch (kind.tag)
    {
    case KOOPA_RVT_RETURN:
        visit(kind.data.ret);
        break;
    case KOOPA_RVT_INTEGER:
        visit(kind.data.integer, value);
        break;
    case KOOPA_RVT_BINARY:
        visit(kind.data.binary, value);
        break;
    default:
        throw std::runtime_error(&quot;visit: invalid instruction&quot;);
    }
}

// 访问 return 指令
void visit(const koopa_raw_return_t &amp;#x26;ret)
{
    // 根据 ret 的 value 类型判断后续需要如何访问
    if (ret.value)
    {
        // 特判如果是立即数, 则直接赋值给 a0 寄存器, 跳过访问 value 的过程
        if (ret.value-&gt;kind.tag == KOOPA_RVT_INTEGER)
        {
            std::cout &amp;#x3C;&amp;#x3C; &quot;\tli a0, &quot; &amp;#x3C;&amp;#x3C; ret.value-&gt;kind.data.integer.value &amp;#x3C;&amp;#x3C; std::endl;
        }
        // 否则, 访问这个值, 然后把这个值存储在的寄存器名称移动给 a0 寄存器, 注意不是 li
        else
        {
            bool is_allocated = register_manager.exist(ret.value);
            if (!is_allocated)
            {
                visit(ret.value);
            }
            std::cout &amp;#x3C;&amp;#x3C; &quot;\tmv a0, &quot; &amp;#x3C;&amp;#x3C; register_manager.value_to_reg_string(ret.value) &amp;#x3C;&amp;#x3C; std::endl;
        }
    }
    // 如果 ret 的 value 为空, 则直接赋值 0 给 a0 寄存器, 然后返回
    else
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tli a0, 0&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    std::cout &amp;#x3C;&amp;#x3C; &quot;\tret&quot; &amp;#x3C;&amp;#x3C; std::endl;
}

// 访问 integer
void visit(const koopa_raw_integer_t &amp;#x26;integer, const koopa_raw_value_t &amp;#x26;value)
{
    if (integer.value == 0)
    {
        register_manager.allocate_reg(value, true);
    }
    else
    {
        register_manager.allocate_reg(value);
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tli &quot; &amp;#x3C;&amp;#x3C; register_manager.value_to_reg_string(value) &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; integer.value &amp;#x3C;&amp;#x3C; std::endl;
    }
}

// 访问 binary 指令
void visit(const koopa_raw_binary_t &amp;#x26;binary, const koopa_raw_value_t &amp;#x26;value)
{
    // lhs 和 rhs 是否已经分配过寄存器, 如果没分配过, 则需要先访问 lhs 和 rhs, 访问过程中会分配寄存器, 注意 ricsv 不能直接操作立即数, 必须先加载到寄存器中!
    bool lhs_is_allocated = register_manager.exist(binary.lhs);
    if (!lhs_is_allocated)
    {
        visit(binary.lhs);
    }
    bool rhs_is_allocated = register_manager.exist(binary.rhs);
    if (!rhs_is_allocated)
    {
        visit(binary.rhs);
    }
    // 我们认为每个结果仅使用一次, 所以可以设置两个子结果的寄存器可以被覆盖了.
    // 比如将立即数转移给 a0 寄存器, 我们现在就认为 a0 寄存器被占用了, 但是如果 a0 寄存器之后被调用了, 这个立即数被使用过了, 那么 a0 寄存器就会被标记为不被占用, 因为到目前为止我们认为每一个结果只被使用一次
    register_manager.set_reg_free(binary.lhs);
    register_manager.set_reg_free(binary.rhs);
    register_manager.allocate_reg(value);

    // 获取当前结果, lhs 和 rhs 对应的寄存器名称
    std::string cur = register_manager.value_to_reg_string(value);
    std::string lhs = register_manager.value_to_reg_string(binary.lhs);
    std::string rhs = register_manager.value_to_reg_string(binary.rhs);

    // 根据二元运算符的类型进行处理
    switch (binary.op)
    {
    case KOOPA_RBO_EQ:
        std::cout &amp;#x3C;&amp;#x3C; &quot;\txor &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; lhs &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; rhs &amp;#x3C;&amp;#x3C; std::endl;
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tseqz &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; std::endl;
        break;
    case KOOPA_RBO_NOT_EQ:
        std::cout &amp;#x3C;&amp;#x3C; &quot;\txor &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; lhs &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; rhs &amp;#x3C;&amp;#x3C; std::endl;
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tsnez &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; cur &amp;#x3C;&amp;#x3C; std::endl;
        break;
    // ...
    default:
        throw std::runtime_error(&quot;visit: invalid binary operator&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果无法通过全部 Lv3 的测试样例, 可以检查如下几个问题:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把数据从寄存器移动到寄存器时, 使用的是 &lt;code&gt;mv&lt;/code&gt; 指令, 而不是 &lt;code&gt;li&lt;/code&gt; 指令.&lt;/li&gt;
&lt;li&gt;访问 &lt;code&gt;return&lt;/code&gt; 指令时, 在访问 &lt;code&gt;ret.value&lt;/code&gt; 之前有没有检查它是否已经分配了寄存器, 否则可能造成同一个运算被输出多次.&lt;/li&gt;
&lt;li&gt;还有一个测试样例, 可以检查你的寄存器是否会溢出:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  return 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0 + 0 * 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv3 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv3 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv 4&lt;/h2&gt;
&lt;p&gt;本节需要让你的编译器可以处理变量的声明和定义, 用例如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
  const int x = 233 * 4;
  int y = 10;
  y = y + x / 2;
  return y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体来讲, 有如下几个重点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;koopa&lt;/code&gt; 部分需要增加符号表来管理变量.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;koopa&lt;/code&gt; 部分需要完成 &lt;code&gt;const&lt;/code&gt; 变量的编译期求值.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;riscv&lt;/code&gt; 部分在处理不同条 &lt;code&gt;koopa&lt;/code&gt; 指令间, 只需要维护栈帧.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;riscv&lt;/code&gt; 部分在处理同一条 &lt;code&gt;koopa&lt;/code&gt; 指令时, 需要维护寄存器.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;code&gt;Koopa&lt;/code&gt; Symbol Table&lt;/h3&gt;
&lt;p&gt;如果是&lt;strong&gt;常量定义&lt;/strong&gt;, 比如 &lt;code&gt;int x = 233;&lt;/code&gt;, 它不需要 &lt;code&gt;koopa&lt;/code&gt; 指令来完成.&lt;/p&gt;
&lt;p&gt;你只需要在符号表中记录 &lt;code&gt;@x&lt;/code&gt; 这个变量和 &lt;code&gt;233&lt;/code&gt; 这个立即数, 当其他命令调用变量 &lt;code&gt;x&lt;/code&gt; 时, 比如 &lt;code&gt;y = x;&lt;/code&gt;, 从符号表中找到 &lt;code&gt;@x&lt;/code&gt; 这个变量对应的数值, 然后直接使用 &lt;code&gt;store 233, @y&lt;/code&gt; 指令把 &lt;code&gt;@x&lt;/code&gt; 这个变量的值加载到寄存器中给这个指令用.&lt;/p&gt;
&lt;p&gt;如果是&lt;strong&gt;变量定义&lt;/strong&gt;, 比如 &lt;code&gt;int y = x + 1;&lt;/code&gt;, 它需要若干 &lt;code&gt;koopa&lt;/code&gt; 指令来完成 (假设 &lt;code&gt;x&lt;/code&gt; 不是常量, 需要从内存中加载):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;%0 = load @x
add %0, 1
@y = alloc i32
store %0, @y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你需要在符号表中记录 &lt;code&gt;@y&lt;/code&gt; 这个变量, 当其他命令调用变量 &lt;code&gt;y&lt;/code&gt; 时, 检查符号表中是否有 &lt;code&gt;@y&lt;/code&gt; 这个变量, 如果有, 则使用 &lt;code&gt;%0 = load @y&lt;/code&gt; 指令把 &lt;code&gt;@y&lt;/code&gt; 这个变量的值加载到寄存器中给这个指令用.&lt;/p&gt;
&lt;p&gt;综上所述:&lt;/p&gt;
&lt;p&gt;符号表的 &lt;code&gt;key&lt;/code&gt; 就是 &lt;code&gt;@&lt;/code&gt; 开头的内存名, &lt;code&gt;value&lt;/code&gt; 是 &lt;code&gt;i32&lt;/code&gt; 类型的立即数 (常量定义) 或什么都没有 (变量定义, 但是如果你想保持一致性也可以把 &lt;code&gt;%&lt;/code&gt; 开头的寄存器名存下来, 在上面这个例子中是 &lt;code&gt;%0&lt;/code&gt; ; 不过在多层嵌套的块中, 我们可以把这个变量处在的定义域的层级用 &lt;code&gt;value&lt;/code&gt; 来传递, 具体可以参考 Lv5 的实现).&lt;/p&gt;
&lt;p&gt;符号和符号表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * @brief 符号
 * @date 2024-12-22
 */
class Symbol
{
public:
    enum class Type
    {
        VAR,
        VAL
    };
    Type type;
    int val;
    Symbol() : type(Type::VAL), val(0) {}
    Symbol(Type type, int val) : type(type), val(val) {}
};

/**
 * @brief 符号表
 * @date 2024-12-22
 */
class SymbolTable
{
private:
    std::unordered_map&amp;#x3C;std::string, Symbol&gt; symbol_table;
    bool is_returned = false;

public:
    void create(const std::string &amp;#x26;name, Symbol symbol);
    bool exist(const std::string &amp;#x26;name);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在常量的定义中维护符号表:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Result ConstDefAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (symbol_table.exist(const_symbol)) // 如果符号表中已经存在这个符号, 则抛出错误
    {
        throw std::runtime_error(&quot;ConstDefAST::print: const identifier already exists&quot;);
    }
    Result value_result = const_init_val-&gt;print(output_stream); // 计算常量表达式的值
    symbol_table.create(const_symbol, Symbol(Symbol::Type::VAL, value_result.val)); // 将常量表达式的值存入符号表
    return Result(); // 返回空结果, 为什么不返回调用 print 的返回值? 因为我们的先验知识 (语义规范) 告诉我们, 声明和定义语句不会返回任何值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;Koopa&lt;/code&gt; Const Variable Compile-time Computation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() {
  const int x = 1 + 1;
  return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一个返回常量的代码, 我们直接返回 &lt;code&gt;2&lt;/code&gt; 即可:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
  ret 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 &lt;code&gt;1 + 1&lt;/code&gt; 在编译期就被求值为 &lt;code&gt;2&lt;/code&gt; 了, 那么如何完成 &lt;code&gt;const&lt;/code&gt; 变量的编译期求值呢?&lt;/p&gt;
&lt;p&gt;我们只需要在访问每一个计算节点, 比如 &lt;code&gt;AddExpAST::print&lt;/code&gt; 的时候判断它的左右操作数是不是都是立即数, 如果是立即数就返回计算完的立即数, 如果不是立即数才需要 &lt;code&gt;%1 = add %0, 1&lt;/code&gt; 这样的计算指令.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Result AddExpAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (!add_exp &amp;#x26;&amp;#x26; !op &amp;#x26;&amp;#x26; mul_exp)
    {
        return (*mul_exp)-&gt;print(output_stream);
    }
    else if (add_exp &amp;#x26;&amp;#x26; op &amp;#x26;&amp;#x26; mul_exp)
    {
        Result result_left = (*add_exp)-&gt;print(output_stream);
        Result result_right = (*mul_exp)-&gt;print(output_stream);
        if (result_left.type == Result::Type::IMM &amp;#x26;&amp;#x26; result_right.type == Result::Type::IMM)
        {
            if (*op == &quot;+&quot;)
            {
                return Result(Result::Type::IMM, result_left.val + result_right.val);
            }
            else if (*op == &quot;-&quot;)
            {
                return Result(Result::Type::IMM, result_left.val - result_right.val);
            }
            else
            {
                throw std::runtime_error(&quot;AddExpAST::print: invalid add operator when both operands are immediate&quot;);
            }
        }
        else
        {
            Result result = Result(Result::Type::REG);
            if (*op == &quot;+&quot;)
            {
                output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = add &quot; &amp;#x3C;&amp;#x3C; result_left &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; result_right &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            }
            else if (*op == &quot;-&quot;)
            {
                output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = sub &quot; &amp;#x3C;&amp;#x3C; result_left &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; result_right &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            }
            else
            {
                throw std::runtime_error(&quot;AddExpAST::print: invalid add operator when one of the operands is not immediate&quot;);
            }
            return result;
        }
    }
    else
    {
        throw std::runtime_error(&quot;AddExpAST::print: invalid add expression&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出, 如果 &lt;code&gt;AddExpAST::print&lt;/code&gt; 的左右操作数都是立即数, 那么 &lt;code&gt;AddExpAST::print&lt;/code&gt; 的返回值就是立即数, 否则就是寄存器.&lt;/p&gt;
&lt;p&gt;这样我们避免了已经知道左右操作数的真实数值的情况下依然生成 &lt;code&gt;add&lt;/code&gt; 指令, 从而实现了编译期求值, 同时这是一个递归的过程, 所以仅需要修改很少的代码就可以实现.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;RISC-V&lt;/code&gt; Stack Frame&lt;/h3&gt;
&lt;p&gt;在处理不同条 &lt;code&gt;koopa&lt;/code&gt; 指令间只需要维护栈帧而不需要维护寄存器, 具体来讲, 在使用到 &lt;code&gt;@x&lt;/code&gt; 或 &lt;code&gt;%1&lt;/code&gt; 等&lt;strong&gt;所有&lt;/strong&gt; &lt;code&gt;koopa&lt;/code&gt; 变量和内存时维护栈帧.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@x&lt;/code&gt; 存在栈帧上是没有问题的, 毕竟 &lt;code&gt;koopa&lt;/code&gt; 就是这么做的.&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;%1&lt;/code&gt; 这样的 &lt;code&gt;koopa&lt;/code&gt; 寄存器应该存在哪里呢? 我们为了简化寄存器分配, 因此将 &lt;code&gt;koopa&lt;/code&gt; 的所有寄存器计算出来之后, 也存在 &lt;code&gt;riscv&lt;/code&gt; 的栈帧上, 如果这个 &lt;code&gt;%1&lt;/code&gt; 之后被使用了, 就从栈帧中找到 &lt;code&gt;%1&lt;/code&gt; 对应的内存即可.&lt;/p&gt;
&lt;p&gt;可以看下面的例子, &lt;code&gt;%2 = load @y&lt;/code&gt; 计算出来了 &lt;code&gt;%2&lt;/code&gt; 的值, 我们的操作是把它存在了 &lt;code&gt;sp + 12&lt;/code&gt; 这个位置, 然后 &lt;code&gt;ret %2&lt;/code&gt; 的时候从 &lt;code&gt;sp + 12&lt;/code&gt; 这个位置取出 &lt;code&gt;%2&lt;/code&gt; 的值, 从而在不同 &lt;code&gt;koopa&lt;/code&gt; 指令间通过栈帧传递信息, 完全不使用寄存器.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;	.text
	.globl main
main:
	addi sp, sp, -16

  # store 10, @y
	li t0, 10
	sw t0, 0(sp)

  # %0 = load @y
	lw t0, 0(sp)
	sw t0, 4(sp) 

  # %1 = add %0, 466
	lw t0, 4(sp)
	li t1, 466
	add t0, t0, t1
	sw t0, 8(sp)

  # store %1, @y
	lw t0, 8(sp)
	sw t0, 0(sp)

  # %2 = load @y
	lw t0, 0(sp)
	sw t0, 12(sp)

  # ret %2
	lw a0, 12(sp)
	addi sp, sp, 16
    ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我使用了 &lt;code&gt;ContextManager&lt;/code&gt; 来管理栈帧和寄存器, 这是从 &lt;code&gt;Lv3&lt;/code&gt; 的 &lt;code&gt;RegisterManager&lt;/code&gt; 加入了栈帧的管理器得到的.&lt;/p&gt;
&lt;p&gt;所有代码共用一个 &lt;code&gt;ContextManager&lt;/code&gt;, 每一个函数在进入的时候单开一个 &lt;code&gt;StackManager&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;具体来讲:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/**
 * @brief 寄存器和所有函数的栈管理器, 是全局共用的, 可以维护值和寄存器的关系, 可以维护一个值和这个值对应的函数的栈信息
 * @author Yutong Liang
 * @date 2024-11-28
 */
class ContextManager
{
private:
    // 值到寄存器名称的映射
    std::unordered_map&amp;#x3C;koopa_raw_value_t, std::string&gt; _value_to_reg_string;

    // 存储当前所有寄存器是否有不能被覆盖的值
    std::unordered_map&amp;#x3C;std::string, bool&gt; _reg_is_used;

    /**
     * @brief 设置一个值对应哪个寄存器, 内部函数不被外部调用
     * @param[in] value 值
     * @param[in] reg_string 寄存器名称
     * @author Yutong Liang
     * @date 2024-11-28
     */
    void _set_value_to_reg_string(const koopa_raw_value_t &amp;#x26;value, const std::string &amp;#x26;reg_string);

    // 函数名到这个函数的 StackManager 的映射
    std::unordered_map&amp;#x3C;std::string, StackManager&gt; _function_name_to_stack_manager;

    // 当前正在处理的函数的函数名
    std::string current_function_name;
};

/**
 * @brief 单个函数的栈管理器, 是一个函数使用的, 可以维护值 (比如 `@x`, `%1`) 和栈地址的关系
 * @author Yutong Liang
 * @date 2024-12-22
 */
class StackManager
{
private:
    // 栈帧大小, 初始化的时候确定的, 单位是字节
    int stack_size;

    // 栈帧当前使用情况, 初始化时0, 直到增长为 stack_size 为止, 单位是字节
    int stack_used_byte;

    // 值到栈地址的映射, 栈地址的表示方法是 &quot;sp + offset&quot; 中的 int offset
    std::unordered_map&amp;#x3C;koopa_raw_value_t, int&gt; value_to_stack_offset;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;RISC-V&lt;/code&gt; Register Allocation&lt;/h3&gt;
&lt;p&gt;不同 &lt;code&gt;koopa&lt;/code&gt; 代码之间共享栈帧但是寄存器不互相影响, 同一行 &lt;code&gt;koopa&lt;/code&gt; 代码之间才可能产生寄存器的影响, 每一条 &lt;code&gt;koopa&lt;/code&gt; 指令使用自己的寄存器然后释放自己的寄存器.&lt;/p&gt;
&lt;p&gt;这是因为每一行 &lt;code&gt;koopa&lt;/code&gt; 代码只会使用 &lt;code&gt;@x&lt;/code&gt;, &lt;code&gt;%1&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt; 这样的值, 而这些值在 RISC-V 中要么在内存中, 要么就是立即数, 所以&lt;strong&gt;任意两个 &lt;code&gt;koopa&lt;/code&gt; 指令之间是不会产生寄存器复用的&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;有了这个原则, 我们在实现的时候就可以安心地使用寄存器了.&lt;/p&gt;
&lt;p&gt;在访问 &lt;code&gt;koopa_raw_value_t&lt;/code&gt; 的时候, 只访问对应 &lt;code&gt;koopa&lt;/code&gt; 指令的抽象语法树, 不对应的不访问, 比如立即数就不要访问, 因为这样安排逻辑清晰, 保证了寄存器使用的解耦, 两个 &lt;code&gt;visit&lt;/code&gt; 函数之间没有寄存器依赖关系, 同时 &lt;code&gt;switch&lt;/code&gt; 中每一个 &lt;code&gt;visit&lt;/code&gt; 函数都会使用后释放自己的寄存器.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 访问指令
void visit(const koopa_raw_value_t &amp;#x26;value)
{
    const auto &amp;#x26;kind = value-&gt;kind;
    switch (kind.tag)
    {
    case KOOPA_RVT_RETURN:
        // 访问 return 指令
        visit(kind.data.ret);
        break;
    case KOOPA_RVT_BINARY:
        // 访问 binary 计算指令
        visit(kind.data.binary, value);
        break;
    case KOOPA_RVT_ALLOC:
        // 访问 alloc 指令, 分配内存是实际存在的指令, 但是汇编语言的内存分配是直接用栈指针管理的, 所以不需要显式的内存分配, 可以忽略 koopa 的 alloc 指令
        // 比如 @x = alloc i32 这样一个指令, 如果只有这个指令本身, 并不需要做任何操作也能保证 RISC-V 的正确性
        // 只有在使用了 @x 的时候, 比如 store 10, @x 这样的指令, 才需要设定 @x 的栈地址, 所以 alloc 指令可以忽略
        break;
    case KOOPA_RVT_LOAD:
        // 访问 load 指令
        visit(kind.data.load, value);
        break;
    case KOOPA_RVT_STORE:
        // 访问 store 指令
        visit(kind.data.store, value);
        break;
    default:
        // 其他类型暂时遇不到
        throw std::runtime_error(&quot;visit: invalid instruction&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如 &lt;code&gt;%1 = add %0, 1&lt;/code&gt; 这样的指令, 只访问 &lt;code&gt;add&lt;/code&gt; 指令, 在访问 &lt;code&gt;add&lt;/code&gt; 的时候不调用 &lt;code&gt;visit( 立即数 1 )&lt;/code&gt; , 参考如下 7-17 行代码, 而是直接将 &lt;code&gt;1&lt;/code&gt; 加载到寄存器中, 因为立即数不是 &lt;code&gt;koopa&lt;/code&gt; 指令, 如果访问了就违反了我们最开始的原则了.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 访问 binary 指令
void visit(const koopa_raw_binary_t &amp;#x26;binary, const koopa_raw_value_t &amp;#x26;value)
{
    // 判断 lhs 是立即数还是内存, 如果是立即数就 li, 否则就 lw
    context_manager.allocate_reg(binary.lhs);
    std::string lhs = context_manager.value_to_reg_string(binary.lhs);
    if (binary.lhs-&gt;kind.tag == KOOPA_RVT_INTEGER) // 这里不要调用 visit( 立即数 1 ), 因为这不是一个 koopa 指令
    {
        riscv_printer.li(lhs, binary.lhs-&gt;kind.data.integer.value);
    }
    else
    {
        // 当前函数的 StackManager
        StackManager &amp;#x26;stack_manager = context_manager.get_current_function_stack_manager();
        // 从栈中加载数据到寄存器
        riscv_printer.lw(lhs, &quot;sp&quot;, stack_manager.get_value_stack_offset(binary.lhs), context_manager);
    }
    // 判断 rhs 是立即数还是内存, 如果是立即数就 li, 否则就 lw
    context_manager.allocate_reg(binary.rhs);
    std::string rhs = context_manager.value_to_reg_string(binary.rhs);
    if (binary.rhs-&gt;kind.tag == KOOPA_RVT_INTEGER)
    {
        riscv_printer.li(rhs, binary.rhs-&gt;kind.data.integer.value);
    }
    else
    {
        // 当前函数的 StackManager
        StackManager &amp;#x26;stack_manager = context_manager.get_current_function_stack_manager();
        // 从栈中加载数据到寄存器
        riscv_printer.lw(rhs, &quot;sp&quot;, stack_manager.get_value_stack_offset(binary.rhs), context_manager);
    }

    // 给结果分配一个寄存器, 分配之前可以先释放掉 lhs 和 rhs 对应的寄存器, 因为他们相当于已经加载进来了, 一会使用的时候可以覆盖, 比如 add t0, t0, t1
    context_manager.set_reg_free(binary.lhs);
    context_manager.set_reg_free(binary.rhs);
    context_manager.allocate_reg(value);
    std::string cur = context_manager.value_to_reg_string(value);

    // 根据二元运算符的类型进行处理
    switch (binary.op)
    {
    case KOOPA_RBO_EQ:
        riscv_printer.xor_(cur, lhs, rhs);
        riscv_printer.seqz(cur, cur);
        break;
    // ...
    default:
        throw std::runtime_error(&quot;visit: invalid binary operator&quot;);
    }
    // 当前函数的 StackManager
    StackManager &amp;#x26;stack_manager = context_manager.get_current_function_stack_manager();
    // 把结果存回栈中
    stack_manager.save_value_to_stack(value);
    riscv_printer.sw(cur, &quot;sp&quot;, stack_manager.get_value_stack_offset(value), context_manager);
    // 当前结果所在的寄存器已经被使用过了, 释放
    context_manager.set_reg_free(value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv4 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv4 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv5&lt;/h2&gt;
&lt;p&gt;这是一个非常简单的 Level 呀, 只需要在 Lv4 的基础上将一个 &lt;code&gt;SymbolTable&lt;/code&gt; 变为多个就可以了.&lt;/p&gt;
&lt;h3&gt;Multiple Symbol Table&lt;/h3&gt;
&lt;p&gt;Lv4 中的 &lt;code&gt;SymbolTable&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class SymbolTable
{
private:
    std::unordered_map&amp;#x3C;std::string, Symbol&gt; symbol_table;
    bool is_returned = false;

public:
    // CRUD ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lv5 中的 &lt;code&gt;SymbolTable&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class SymbolTable
{
private:
    std::vector&amp;#x3C;std::unordered_map&amp;#x3C;std::string, Symbol&gt;&gt; symbol_table; // 每进入一个块, 就创建一个新的符号表, 块包括函数的大括号和语句块的大括号
    bool is_returned = false;

public:
    // 每进入一个块, 就创建一个新的符号表, 块包括函数的大括号和语句块的大括号
    void new_symbol_table_hierarchy();
    // 每离开一个块, 就删除一个符号表
    void delete_symbol_table_hierarchy();
    // CRUD ...
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别在于我们把 &lt;code&gt;SymbolTable&lt;/code&gt; 从单个变为多个, 构建为一个栈, 每进入一个块, 就创建一个新的符号表, 每离开一个块, 就删除一个符号表.&lt;/p&gt;
&lt;p&gt;同时我们需要修改内存的存储位置, 因为不同层的定义域可能使用同一个变量名 (例子如下), 此时应该从内向外找到最近的变量名然后使用这一层的内存地址.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
  @a_1 = alloc i32
  store 1, @a_1
  store 2, @a_1
  @a_2 = alloc i32
  store 3, @a_2
  %0 = load @a_1
  ret %0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Naming Same Symbol From Different Scope&lt;/h3&gt;
&lt;p&gt;那么具体应该如何命名呢? 我们恰巧可以使用这个变量处于的 &lt;code&gt;vector&lt;/code&gt; 的 &lt;code&gt;index&lt;/code&gt; 来命名, 比如第一个 &lt;code&gt;@a&lt;/code&gt; 处于 &lt;code&gt;symbol_table[0]&lt;/code&gt; 中, 那么我们就把访问第一层中的 &lt;code&gt;@a&lt;/code&gt; 的内存地址表示为 &lt;code&gt;@a_1&lt;/code&gt;, 第二个 &lt;code&gt;@a&lt;/code&gt; 处于 &lt;code&gt;symbol_table[1]&lt;/code&gt; 中, 那么我们就把访问第二层中的 &lt;code&gt;@a&lt;/code&gt; 的内存地址表示为 &lt;code&gt;@a_2&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;具体实现可以参考:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Symbol
{
public:
    enum class Type
    {
        VAR,
        VAL
    };
    Type type;
    int val; // 如果 type 是 VAL, 那么 val 是立即数的数值; 如果 type 是 VAR, 那么 val 是变量的层级, 比如 `a = 2;` 如果在符号表中在层级 1 找到这个符号, 那么就会返回 1, 得到 @a_1
    Symbol() : type(Type::VAL), val(0) {}
    Symbol(Type type, int val) : type(type), val(val) {}
};

Symbol SymbolTable::read(const std::string &amp;#x26;name)
{
    for (int i = symbol_table.size() - 1; i &gt;= 0; --i)
    {
        if (symbol_table[i].find(name) != symbol_table[i].end())
        {
            Symbol symbol = symbol_table[i].at(name);
            if (symbol.type == Symbol::Type::VAL)
            {
                return symbol; // 如果是常量, 直接返回常量的值
            }
            else if (symbol.type == Symbol::Type::VAR)
            {
                return Symbol(Symbol::Type::VAR, i + 1); // 如果是变量, 返回变量所在的 SymbolTable 的 index
            }
            else
            {
                throw std::runtime_error(&quot;SymbolTable::read: invalid symbol type&quot;);
            }
        }
    }
    throw std::runtime_error(&quot;SymbolTable::read: identifier does not exist&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是 &lt;code&gt;Symbol::Type::VAR&lt;/code&gt; , 我们可以正好使用 &lt;code&gt;Symbol::val&lt;/code&gt; 来表示它处于的 &lt;code&gt;symbol_table&lt;/code&gt; 的 &lt;code&gt;index&lt;/code&gt; , 毕竟这个 &lt;code&gt;Symbol&lt;/code&gt; 初始化的时候 &lt;code&gt;Symbol::val&lt;/code&gt; 就是没用的, 可以参考如下的初始化代码和它的 Context:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Result VarDefAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (var_init_val)
    {
        Result value_result = (*var_init_val)-&gt;print(output_stream);
        symbol_table.insert_symbol(var_symbol, Symbol(Symbol::Type::VAR, value_result.val));
        std::string symbol_name = var_symbol;
        std::string suffix = std::to_string(symbol_table.read(symbol_name).val);
        std::string symbol_name_with_suffix = symbol_name + &quot;_&quot; + suffix;
        output_stream &amp;#x3C;&amp;#x3C; &quot;\t@&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot; = alloc i32\n&quot;;
        output_stream &amp;#x3C;&amp;#x3C; &quot;\tstore &quot; &amp;#x3C;&amp;#x3C; value_result &amp;#x3C;&amp;#x3C; &quot;, @&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    }
    else
    {
        symbol_table.insert_symbol(var_symbol, Symbol(Symbol::Type::VAR, 0));
        std::string symbol_name = var_symbol;
        std::string suffix = std::to_string(symbol_table.read(symbol_name).val);
        std::string symbol_name_with_suffix = symbol_name + &quot;_&quot; + suffix;
        output_stream &amp;#x3C;&amp;#x3C; &quot;\t@&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot; = alloc i32\n&quot;;
    }
    return Result();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中第六行中用 &lt;code&gt;value_result.val&lt;/code&gt; 初始化了这个 &lt;code&gt;Symbol&lt;/code&gt; , 但是实际上这个值是 &lt;code&gt;koopa&lt;/code&gt; 寄存器的名称, 我们并不需要保存这个某个内存和它用了某个寄存器来初始化的关系, 所以我们可以用 &lt;code&gt;symbol_table&lt;/code&gt; 的 &lt;code&gt;index&lt;/code&gt; 来覆盖这个值, 没有影响.&lt;/p&gt;
&lt;p&gt;最后 &lt;code&gt;RISC-V&lt;/code&gt; 部分不需要修改, 因为 &lt;code&gt;koopa&lt;/code&gt; 代码的可用指令没有修改.&lt;/p&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv5 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv5 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv6&lt;/h2&gt;
&lt;p&gt;这部分我们需要完成 &lt;code&gt;if&lt;/code&gt; 语句的编译, 示例如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  int a = 1;
  if (a == 2 || a == 3) 
  {
    return 0;
  } 
  else 
  {
    return a + 1;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有如下几个重点:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成 &lt;code&gt;Koopa&lt;/code&gt; 中间代码时解决分支语句在语法分析的时候产生的移入/归约冲突.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;Koopa&lt;/code&gt; 中间代码时多 &lt;code&gt;if&lt;/code&gt; 的编号问题.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;Koopa&lt;/code&gt; 中间代码时解决控制流提前结束的问题.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;Koopa&lt;/code&gt; 中间代码时解决同一层级多次分配相同名称的内存的问题.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;Koopa&lt;/code&gt; 中间代码时逻辑运算短路求值的特性.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码时添加两条 &lt;code&gt;RISC-V&lt;/code&gt; 指令.&lt;/li&gt;
&lt;li&gt;生成 &lt;code&gt;RISC-V&lt;/code&gt; 汇编代码时容易出现的 bug 和解决方法, 主要是 12 位立即数溢出的问题.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Shift/Reduce Conflict&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;If&lt;/code&gt; 相关的语法规则如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stmt ::= &quot;if&quot; &quot;(&quot; Exp &quot;)&quot; Stmt [&quot;else&quot; Stmt]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于移入/归约冲突的原因分析可以参考 &lt;a href=&quot;https://pku-minic.github.io/online-doc/#/lv6-if/if-else&quot;&gt;Lv6 的 Lab 文档&lt;/a&gt;, 这里不再赘述.&lt;/p&gt;
&lt;p&gt;为了避免这样的问题, &lt;code&gt;SysY&lt;/code&gt; 的语义规定了 &lt;code&gt;else&lt;/code&gt; 必须和最近的 &lt;code&gt;if&lt;/code&gt; 进行匹配, 助教在这里提示拆分可以解决问题, 那么具体怎么做呢?&lt;/p&gt;
&lt;p&gt;一个重要的观察是, 如果一个 &lt;code&gt;if ...&lt;/code&gt; 语句在语法分析后跟随了一个 &lt;code&gt;else ...&lt;/code&gt; 语句, 那么这个 &lt;code&gt;if ...&lt;/code&gt; 语句内部中所有可能出现的 &lt;code&gt;if ...&lt;/code&gt; 语句都必须是跟随 &lt;code&gt;else ...&lt;/code&gt; 语句的, 否则就和 &lt;code&gt;SysY&lt;/code&gt; 的语义规定冲突了.&lt;/p&gt;
&lt;p&gt;因此我们可以将原有的语法规则修改为:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stmt ::= &quot;if&quot; &quot;(&quot; Exp &quot;)&quot; Stmt
       | &quot;if&quot; &quot;(&quot; Exp &quot;)&quot; StmtWithElse &quot;else&quot; Stmt

StmtWithElse ::= &quot;if&quot; &quot;(&quot; Exp &quot;)&quot; StmtWithElse &quot;else&quot; StmtWithElse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以解决移入/归约冲突的问题了.&lt;/p&gt;
&lt;p&gt;在 Parser 中的参考实现:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* ast.hpp */

class StmtAST : public BaseAST
{
public:
    enum class StmtType
    {
        Assign,
        Expression,
        Block,
        Return,
        If
    };
    StmtType stmt_type;
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; lval;             // 语句中的左值
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; exp;              // 语句中的表达式
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; block;            // 语句中的基本块, 其实是另一个用大括号包裹的语句块
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; inside_if_stmt;   // if ... 中的语句块
    std::optional&amp;#x3C;std::unique_ptr&amp;#x3C;BaseAST&gt;&gt; inside_else_stmt; // else ... 中的语句块

    Result print(std::stringstream &amp;#x26;output_stream) const override;
};

/* sysy.y */

Stmt
  // Assign, Expression, Block, Return ...
  | IF &apos;(&apos; Exp &apos;)&apos; Stmt {
    // ...
  }
  | IF &apos;(&apos; Exp &apos;)&apos; StmtWithElse ELSE Stmt {
    // ...
  }
  ;

StmtWithElse
  // Assign, Expression, Block, Return ...
  | IF &apos;(&apos; Exp &apos;)&apos; StmtWithElse ELSE StmtWithElse {
    // ...
  }
  ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么我把 &lt;code&gt;StmtWithElse&lt;/code&gt; 和 &lt;code&gt;Stmt&lt;/code&gt; 的语法规则分开写, 重复写了一遍 &lt;code&gt;Stmt&lt;/code&gt; 中的其它内容呢?&lt;/p&gt;
&lt;p&gt;因为对于 &lt;code&gt;Stmt&lt;/code&gt; 来说, 无法在规约的时候传递一个参数说明这个 &lt;code&gt;Stmt&lt;/code&gt; 是有 &lt;code&gt;else ...&lt;/code&gt; 语句的, 还是没有 &lt;code&gt;else ...&lt;/code&gt; 语句的, 所以需要新开一个语法规则. 同时我又不想大幅修改 &lt;code&gt;Stmt&lt;/code&gt; 原始的语法规则, 想要保持前后的一致性, 所以只能复制一遍 &lt;code&gt;Stmt&lt;/code&gt; 中的其它语法规则到 &lt;code&gt;StmtWithElse&lt;/code&gt; 中了.&lt;/p&gt;
&lt;h3&gt;Multiple If Statement&lt;/h3&gt;
&lt;p&gt;不同的 &lt;code&gt;if&lt;/code&gt; 语句都有自己的 &lt;code&gt;%then&lt;/code&gt;, &lt;code&gt;%else&lt;/code&gt; 和 &lt;code&gt;%end&lt;/code&gt; 标签, 所以需要一个计数器来区分不同的 &lt;code&gt;if&lt;/code&gt; 语句, 这个标签只要遇见一次 &lt;code&gt;if ...&lt;/code&gt; 语句就加一, 这样就可以区分不同的 &lt;code&gt;if&lt;/code&gt; 语句了.&lt;/p&gt;
&lt;p&gt;具体的输出代码可以参考:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa.cpp */

koopa_context_manager.total_if_else_statement_count++; // 每遇见一次 if ... 语句, 就加一
std::string then_label = &quot;%then_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
std::string else_label = &quot;%else_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
std::string end_label = &quot;%end_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);

Result exp_result = (*exp)-&gt;print(output_stream);

output_stream &amp;#x3C;&amp;#x3C; &quot;\tbr &quot; &amp;#x3C;&amp;#x3C; exp_result &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; then_label &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; (inside_else_stmt ? else_label : end_label) &amp;#x3C;&amp;#x3C; std::endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handle Control Flow Early End&lt;/h3&gt;
&lt;p&gt;先来看一个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
  if (0) 
  {
    return 1;
  } 
  else 
  {
    return 2;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们 naive 地实现 &lt;code&gt;if&lt;/code&gt; 语句的 &lt;code&gt;koopa&lt;/code&gt; 输出, 比如这样:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa.cpp */

Result StmtAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (stmt_type == StmtType::Assign)
    {
        // ...
    }
    else if (stmt_type == StmtType::Return)
    {
        // ...
    }
    else if (stmt_type == StmtType::Expression)
    {
        // ...
    }
    else if (stmt_type == StmtType::Block)
    {
        // ...
    }
    else if (stmt_type == StmtType::If)
    {
        koopa_context_manager.total_if_else_statement_count++;
        std::string then_label = &quot;%then_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
        std::string else_label = &quot;%else_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
        std::string end_label = &quot;%end_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);

        // 计算表达式, 根据表达式结果跳转到不同的分支
        Result exp_result = (*exp)-&gt;print(output_stream);
        output_stream &amp;#x3C;&amp;#x3C; &quot;\tbr &quot; &amp;#x3C;&amp;#x3C; exp_result &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; then_label &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; (inside_else_stmt ? else_label : end_label) &amp;#x3C;&amp;#x3C; std::endl;

        // 进入 if 语句块
        output_stream &amp;#x3C;&amp;#x3C; then_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;
        Result result_if = (*inside_if_stmt)-&gt;print(output_stream);
        output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; end_label &amp;#x3C;&amp;#x3C; std::endl;

        // else 语句块
        Result result_else = Result();
        if (inside_else_stmt)
        {
            output_stream &amp;#x3C;&amp;#x3C; else_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;
            result_else = (*inside_else_stmt)-&gt;print(output_stream);
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; end_label &amp;#x3C;&amp;#x3C; std::endl;
        }

        Result result = Result();
        return result;
    }
    else
    {
        throw std::runtime_error(&quot;StmtAST::print: invalid statement&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先回忆基本块的定义:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;基本块 (basic block) 是编译领域的一个很常见的概念, 它指的是一系列指令的集合, 基本块满足:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只有一个入口点: 所有基本块中的指令如果要执行跳转, 只能跳到某个基本块的开头, 而不能跳到中间.&lt;/li&gt;
&lt;li&gt;只有一个出口点: 基本块中, 只有最后一条指令能进行控制流的转移, 也就是跳到其他基本块, 或者从函数中返回 (执行 return 操作).
基本块的存在可以简化很多编译过程中需要进行的分析, 所以 Koopa IR 要求函数中的指令必须预先按照基本块分类. 同时, Koopa IR 约定, 函数的第一个基本块为函数的入口基本块, 也就是执行函数时, 首先会执行第一个基本块中的指令.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么我们可能会得到这样的 &lt;code&gt;koopa&lt;/code&gt; 代码, 注意其中的 &lt;code&gt;%then_1&lt;/code&gt; 和 &lt;code&gt;%else_1&lt;/code&gt; 在返回后存在跳转到 &lt;code&gt;%end_1&lt;/code&gt; 的指令, 这和基本块的定义 (只有最后一条指令能进行控制流的转移) 冲突了, 从而会造成编译器后端的错误.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
    br 0, %then_1, %else_1
%then_1:
    ret 1
    jump %end_1
%else_1:
    ret 2
    jump %end_1
%end_1:
ret 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们需要在输出 &lt;code&gt;if&lt;/code&gt; 内部的 &lt;code&gt;jump&lt;/code&gt; 前判断改控制流是否提前被 &lt;code&gt;ret&lt;/code&gt; 打断了, 如果被打断了就不输出 &lt;code&gt;jump&lt;/code&gt; 了.&lt;/p&gt;
&lt;p&gt;一个非常直观的想法是在每一个 &lt;code&gt;print&lt;/code&gt; 函数的输出 &lt;code&gt;Result&lt;/code&gt; 中加入一个 &lt;code&gt;bool&lt;/code&gt; 变量, 表示这个语句以及其嵌套的语句内是否被 &lt;code&gt;ret&lt;/code&gt; 语句显式地返回了, 然后根据这个变量来决定是否输出 &lt;code&gt;jump&lt;/code&gt; 指令, 同时还可以提前停止输出, 提高代码运行效率.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa.cpp */

Result StmtAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (stmt_type == StmtType::Assign)
    {
        // ...
    }
    else if (stmt_type == StmtType::Return)
    {
        if (!lval &amp;#x26;&amp;#x26; exp &amp;#x26;&amp;#x26; !block)
        {
            Result result = (*exp)-&gt;print(output_stream);
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tret &quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            result.control_flow_returned = true; // 如果语句返回了, 就设置该控制流返回
            return result;
        }
        else if (!lval &amp;#x26;&amp;#x26; !exp &amp;#x26;&amp;#x26; !block)
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tret\n&quot;;
            Result result = Result();
            result.control_flow_returned = true; // 如果语句返回了, 就设置该控制流返回
            return result;
        }
        else
        {
            throw std::runtime_error(&quot;StmtAST::print: invalid return statement&quot;);
        }
    }
    else if (stmt_type == StmtType::Expression)
    {
        // ...
    }
    else if (stmt_type == StmtType::Block)
    {
        if (!lval &amp;#x26;&amp;#x26; !exp &amp;#x26;&amp;#x26; block)
        {
            Result result = (*block)-&gt;print(output_stream);
            return result;
        }
        else
        {
            throw std::runtime_error(&quot;StmtAST::print: invalid block statement&quot;);
        }
    }
    else if (stmt_type == StmtType::If)
    {
        koopa_context_manager.total_if_else_statement_count++;
        std::string then_label = &quot;%then_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
        std::string else_label = &quot;%else_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);
        std::string end_label = &quot;%end_&quot; + std::to_string(koopa_context_manager.total_if_else_statement_count);

        Result exp_result = (*exp)-&gt;print(output_stream);
        if (!inside_if_stmt &amp;#x26;&amp;#x26; !inside_else_stmt)
        {
            throw std::runtime_error(&quot;StmtAST::print: invalid if statement, there&apos;s no if&quot;);
        }

        output_stream &amp;#x3C;&amp;#x3C; &quot;\tbr &quot; &amp;#x3C;&amp;#x3C; exp_result &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; then_label &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; (inside_else_stmt ? else_label : end_label) &amp;#x3C;&amp;#x3C; std::endl;

        // if 语句块
        output_stream &amp;#x3C;&amp;#x3C; then_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;
        Result result_if = (*inside_if_stmt)-&gt;print(output_stream);

        // 如果 if 语句块显式的返回了, 就不要跳转了, 否则输出这样的 koopa 代码是错误的:
        // fun @main(): i32 {
        // %entry:
        //     br 0, %then_1, %else_1
        // %then_1:
        //     ret 1
        //     jump %end_1
        // %else_1:
        //     ret 2
        //     jump %end_1
        // %end_1:
        // }
        if (!result_if.control_flow_returned)
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; end_label &amp;#x3C;&amp;#x3C; std::endl;
        }

        // else 语句块
        Result result_else = Result();
        if (inside_else_stmt)
        {
            output_stream &amp;#x3C;&amp;#x3C; else_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;

            result_else = (*inside_else_stmt)-&gt;print(output_stream);

            // 如果 else 语句块显式的返回了, 就不要跳转了
            if (!result_else.control_flow_returned)
            {
                output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; end_label &amp;#x3C;&amp;#x3C; std::endl;
            }
        }

        // 如果 if 语句块和 else 语句块都返回了, 则注明整个 if ... else ... 语句块返回了
        // 但是为了避免这样的空 %end , 如果已经结束了就不输出 %end 了
        // fun @main(): i32 {
        // %entry:
        // 	   br 0, %then_1, %else_1
        // %then_1:
        //     ret 1
        // %else_1:
        //     ret 2
        // %end_1:
        // }
        Result result = Result();
        if (!result_if.control_flow_returned || !result_else.control_flow_returned)
        {
            output_stream &amp;#x3C;&amp;#x3C; end_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;
        }
        else
        {
            result.control_flow_returned = true; // 如果是 if ... else ... 语句, 则 if ... 和 else ... 语句块都返回了才设置整体函数返回
        }
        return result;
    }
    else
    {
        throw std::runtime_error(&quot;StmtAST::print: invalid statement&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后给 &lt;code&gt;FuncDefAST&lt;/code&gt; 的 &lt;code&gt;print&lt;/code&gt; 函数加上控制流返回的判断, 如果 &lt;code&gt;block&lt;/code&gt; 没有显式的 &lt;code&gt;ret&lt;/code&gt; 指令, 则补上一个 &lt;code&gt;ret 0&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa.cpp */

Result FuncDefAST::print(std::stringstream &amp;#x26;output_stream) const
{
    output_stream &amp;#x3C;&amp;#x3C; &quot;fun @&quot; &amp;#x3C;&amp;#x3C; ident &amp;#x3C;&amp;#x3C; &quot;(): &quot;;
    func_type-&gt;print(output_stream);
    output_stream &amp;#x3C;&amp;#x3C; &quot; {&quot; &amp;#x3C;&amp;#x3C; std::endl;
    output_stream &amp;#x3C;&amp;#x3C; &quot;%entry:&quot; &amp;#x3C;&amp;#x3C; std::endl;
    Result result = block-&gt;print(output_stream);
    // 如果 block 没有显式的 ret 指令, 则补上一个 ret 0
    if (!result.control_flow_returned)
    {
        output_stream &amp;#x3C;&amp;#x3C; &quot;\tret 0&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    output_stream &amp;#x3C;&amp;#x3C; &quot;}&quot; &amp;#x3C;&amp;#x3C; std::endl;
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handle Same Variable Name in Same Level&lt;/h3&gt;
&lt;p&gt;一个例子:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main()
{
    {
        int a = 2;
    }
    {
        int a = 3;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果按照我们之前的处理方式, 会得出这样的 &lt;code&gt;koopa&lt;/code&gt; 代码:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
    @a_2 = alloc i32
    store 2, @a_2
    @a_2 = alloc i32
    sstore 3, @a_2
    ret 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下就出问题了, 因为 &lt;code&gt;@a_2&lt;/code&gt; 被分配了两次. 但是这件事情很好处理, 只要维护一个 &lt;code&gt;std::map&amp;#x3C;std::pair&amp;#x3C;std::string, int&gt;, int&gt;&lt;/code&gt; 来记录每个变量名在每个层级中是否已经分配了, 然后每次分配变量的时候, 先检查这个变量名是否已经存在, 如果存在就使用这个变量的地址, 否则就分配一个新的地址.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa_util.hpp */

class KoopaContextManager
{
private:
    // ...
    // 用于判断当前符号是否在当前下标被分配, 比如 @a_1 在 symbol_tables[0] 中被分配, 那么 is_symbol_allocated_in_this_level[std::make_pair(&quot;a&quot;, 1)] == true
    std::map&amp;#x3C;std::pair&amp;#x3C;std::string, int&gt;, bool&gt; _is_symbol_allocated_in_this_level;

public:
    // ...
};

/* koopa.cpp */

Result VarDefAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (var_init_val)
    {
        Result value_result = (*var_init_val)-&gt;print(output_stream);
        koopa_context_manager.insert_symbol(var_symbol, Symbol(Symbol::Type::VAR, value_result.val));
        std::string symbol_name = var_symbol;
        std::string suffix = std::to_string(koopa_context_manager.name_to_symbol(symbol_name).val);
        std::string symbol_name_with_suffix = symbol_name + &quot;_&quot; + suffix;
        // 如果这个变量名在当前层级中没有被分配过, 则分配一个新的地址
        if (!koopa_context_manager.is_symbol_allocated_in_this_level(symbol_name))
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t@&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot; = alloc i32\n&quot;;
        }
        koopa_context_manager.set_symbol_allocated_in_this_level(symbol_name);
        output_stream &amp;#x3C;&amp;#x3C; &quot;\tstore &quot; &amp;#x3C;&amp;#x3C; value_result &amp;#x3C;&amp;#x3C; &quot;, @&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    }
    else
    {
        koopa_context_manager.insert_symbol(var_symbol, Symbol(Symbol::Type::VAR, 0));
        std::string symbol_name = var_symbol;
        std::string suffix = std::to_string(koopa_context_manager.name_to_symbol(symbol_name).val);
        std::string symbol_name_with_suffix = symbol_name + &quot;_&quot; + suffix;
        // 如果这个变量名在当前层级中没有被分配过, 则分配一个新的地址
        if (!koopa_context_manager.is_symbol_allocated_in_this_level(symbol_name))
        {
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t@&quot; &amp;#x3C;&amp;#x3C; symbol_name_with_suffix &amp;#x3C;&amp;#x3C; &quot; = alloc i32\n&quot;;
        }
        koopa_context_manager.set_symbol_allocated_in_this_level(symbol_name);
    }
    return Result();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Short-circuit Evaluation of Logical Expressions&lt;/h3&gt;
&lt;p&gt;编译器对逻辑运算, 比如 &lt;code&gt;||&lt;/code&gt; 实际上是做了如下操作:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int result = 1;

if (lhs == 0) 
{
  result = rhs != 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样当 &lt;code&gt;lhs == 1&lt;/code&gt; 时, 编译器会直接返回 &lt;code&gt;result&lt;/code&gt;, 而不会计算 &lt;code&gt;rhs&lt;/code&gt; 的值.&lt;/p&gt;
&lt;p&gt;在具体实现的时候你可以对 &lt;code&gt;lhs&lt;/code&gt; 的返回值进行判断, 如果是立即数就不要使用跳转指令, 这样可能会造成常量表达式的求值失败.&lt;/p&gt;
&lt;p&gt;如果编译期无法确定是否可以短路求值, 我们需要使用内存来保存逻辑表达式的结果. 假设第一个操作数存在了 &lt;code&gt;%1&lt;/code&gt; 这个寄存器中, 编译期我们不知道第二个操作数 &lt;code&gt;%2&lt;/code&gt; 是否存在, 所以无法返回 &lt;code&gt;or&lt;/code&gt; 表达式整体的答案存在哪里了, 所以需要结果存在内存中以保证可以修改.&lt;/p&gt;
&lt;p&gt;一个短路求值示例如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main() 
{
    int x = 1;
    int y = 0;
    return x || y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的 &lt;code&gt;koopa&lt;/code&gt; 代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;fun @main(): i32 {
%entry:
	@x_1 = alloc i32
	store 1, @x_1
	@y_1 = alloc i32
	store 0, @y_1
	%0 = load @x_1
	%1 = ne %0, 0
	@or_result_in_memory_1 = alloc i32
	store %1, @or_result_in_memory_1
	br %1, %or_end_1, %or_second_operator_1
%or_second_operator_1:
	%2 = load @y_1
	%3 = ne %2, 0
	%4 = or %1, %3
	store %4, @or_result_in_memory_1
	jump %or_end_1
%or_end_1:
	%5 = load @or_result_in_memory_1
	ret %5
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中我们可以看到最后返回的 &lt;code&gt;%5&lt;/code&gt; 是 &lt;code&gt;@or_result_in_memory_1&lt;/code&gt; 这个内存地址中的值, 这个内存地址中保存的值有可能来源于 &lt;code&gt;%1&lt;/code&gt; 这个寄存器, 对应 &lt;code&gt;lhs&lt;/code&gt; 的值, 也有可能来源于 &lt;code&gt;%4&lt;/code&gt; 这个寄存器, 对应 &lt;code&gt;rhs&lt;/code&gt; 的值. 为了避免不知道返回 &lt;code&gt;%1&lt;/code&gt; 还是 &lt;code&gt;%4&lt;/code&gt; 的情况, 我们使用内存来保存结果, 最后从内存中读取结果到 &lt;code&gt;%5&lt;/code&gt; 中即可.&lt;/p&gt;
&lt;p&gt;一个可能的实现方式如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* koopa.cpp */

Result LOrExpAST::print(std::stringstream &amp;#x26;output_stream) const
{
    if (!left_or_exp &amp;#x26;&amp;#x26; !op &amp;#x26;&amp;#x26; left_and_exp)
    {
        return (*left_and_exp)-&gt;print(output_stream);
    }
    else if (left_or_exp &amp;#x26;&amp;#x26; op &amp;#x26;&amp;#x26; left_and_exp)
    {
        Result result_left = (*left_or_exp)-&gt;print(output_stream);

        if (result_left.type == Result::Type::IMM &amp;#x26;&amp;#x26; result_left.val != 0) // 立即数非 0
        {
            return Result(Result::Type::IMM, 1);
        }
        else if (result_left.type == Result::Type::IMM &amp;#x26;&amp;#x26; result_left.val == 0) // 立即数 0
        {
            Result result_right = (*left_and_exp)-&gt;print(output_stream);
            if (result_right.type == Result::Type::IMM)
            {
                return Result(Result::Type::IMM, 0 || result_right.val);
            }
            else
            {
                Result temp = Result(Result::Type::REG);
                output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; temp &amp;#x3C;&amp;#x3C; &quot; = ne &quot; &amp;#x3C;&amp;#x3C; result_right &amp;#x3C;&amp;#x3C; &quot;, 0\n&quot;;
                return temp;
            }
        }
        else if (result_left.type == Result::Type::REG) // 如果是寄存器, 不能在编译期完成短路求值, 就需要跳转来完成短路求值, 如果判断寄存器是 0 直接跳转到 or_end_label
        {
            // 每进入一个需要用分支跳转语句达成短路求值的 || 语句, 就设置一个跳转标签
            koopa_context_manager.total_or_statement_count++;

            // 设置跳转标签
            std::string or_second_operator_label = &quot;%or_second_operator_&quot; + std::to_string(koopa_context_manager.total_or_statement_count);
            std::string or_end_label = &quot;%or_end_&quot; + std::to_string(koopa_context_manager.total_or_statement_count);

            // 假设第一个操作数存在了 %1 这个寄存器中, 编译期不知道第二个操作数 %2 是否存在, 所以无法返回 or 表达式整体的答案存在哪里了, 所以需要结果存在内存中以保证可以修改
            std::string or_result_in_memory = &quot;@or_result_in_memory_&quot; + std::to_string(koopa_context_manager.total_or_statement_count);

            // 如果第一个操作数是 0, 则跳转到 or_second_operator_label 看看第二个操作数是否是 0, 否则跳转到 or_end_label
            Result temp_1 = Result(Result::Type::REG);
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; temp_1 &amp;#x3C;&amp;#x3C; &quot; = ne &quot; &amp;#x3C;&amp;#x3C; result_left &amp;#x3C;&amp;#x3C; &quot;, 0\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; or_result_in_memory &amp;#x3C;&amp;#x3C; &quot; = alloc i32\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tstore &quot; &amp;#x3C;&amp;#x3C; temp_1 &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; or_result_in_memory &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tbr &quot; &amp;#x3C;&amp;#x3C; temp_1 &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; or_end_label &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; or_second_operator_label &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

            // 输出没有短路求值的控制流 label
            output_stream &amp;#x3C;&amp;#x3C; or_second_operator_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;

            // 计算第二个操作数
            Result result_right = (*left_and_exp)-&gt;print(output_stream);
            Result temp_2 = Result(Result::Type::REG);
            Result temp_3 = Result(Result::Type::REG);
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; temp_2 &amp;#x3C;&amp;#x3C; &quot; = ne &quot; &amp;#x3C;&amp;#x3C; result_right &amp;#x3C;&amp;#x3C; &quot;, 0\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; temp_3 &amp;#x3C;&amp;#x3C; &quot; = or &quot; &amp;#x3C;&amp;#x3C; temp_1 &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; temp_2 &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tstore &quot; &amp;#x3C;&amp;#x3C; temp_3 &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; or_result_in_memory &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; or_end_label &amp;#x3C;&amp;#x3C; &quot;\n&quot;;

            // 输出短路求值之后的控制流合并 label
            output_stream &amp;#x3C;&amp;#x3C; or_end_label &amp;#x3C;&amp;#x3C; &quot;:&quot; &amp;#x3C;&amp;#x3C; std::endl;

            // 把结果从内存中读取到寄存器中
            Result result = Result(Result::Type::REG);
            output_stream &amp;#x3C;&amp;#x3C; &quot;\t&quot; &amp;#x3C;&amp;#x3C; result &amp;#x3C;&amp;#x3C; &quot; = load &quot; &amp;#x3C;&amp;#x3C; or_result_in_memory &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
            return result;
        }
        else
        {
            throw std::runtime_error(&quot;LOrExpAST::print: invalid first operand of logical OR expression&quot;);
        }
    }
    else
    {
        throw std::runtime_error(&quot;LOrExpAST::print: invalid logical OR expression&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate RISC-V Branch Code&lt;/h3&gt;
&lt;p&gt;在完成了 &lt;code&gt;koopa&lt;/code&gt; 的生成后, 我们就可以开始生成 &lt;code&gt;RISC-V&lt;/code&gt; 的汇编代码了.&lt;/p&gt;
&lt;p&gt;具体来讲只需要实现 &lt;code&gt;bnez&lt;/code&gt; 和 &lt;code&gt;j&lt;/code&gt; 这两个 &lt;code&gt;RISC-V&lt;/code&gt; 指令的生成以满足 &lt;code&gt;br&lt;/code&gt; 和 &lt;code&gt;jump&lt;/code&gt; 这两个 &lt;code&gt;koopa&lt;/code&gt; 指令的生成即可, 难度不大.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;/* riscv.cpp */

// 访问 branch 指令, 这个指令的输入是立即数或内存, 所以需要判断 branch.cond-&gt;kind.tag
void visit(const koopa_raw_branch_t &amp;#x26;branch, const koopa_raw_value_t &amp;#x26;value)
{
    // 当前函数的 StackManager
    StackManager &amp;#x26;stack_manager = riscv_context_manager.get_current_function_stack_manager();
    // 给中间结果分配一个寄存器
    riscv_context_manager.allocate_reg(value);
    std::string temp_reg_name = riscv_context_manager.value_to_reg_string(value);
    // 使用立即数或从栈中加载数据到寄存器
    if (branch.cond-&gt;kind.tag == KOOPA_RVT_INTEGER)
    {
        riscv_printer.li(temp_reg_name, branch.cond-&gt;kind.data.integer.value);
    }
    else
    {
        riscv_printer.lw(temp_reg_name, &quot;sp&quot;, stack_manager.get_value_stack_offset(branch.cond), riscv_context_manager);
    }
    // 访问 branch 指令
    riscv_printer.bnez(temp_reg_name, branch.true_bb-&gt;name + 1);
    riscv_printer.jump(branch.false_bb-&gt;name + 1);
    // 当前操作数所在的寄存器已经被使用过了, 释放
    riscv_context_manager.set_reg_free(value);
}

// 访问 jump 指令
void visit(const koopa_raw_jump_t &amp;#x26;jump)
{
    // 访问 jump 指令
    riscv_printer.jump(jump.target-&gt;name + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Handle Immediate Number Overflow&lt;/h3&gt;
&lt;p&gt;但是在这里, &lt;code&gt;Lv6&lt;/code&gt; 的 &lt;code&gt;RISC-V&lt;/code&gt; 的所有测试点中有一个叫做 &lt;code&gt;logical1&lt;/code&gt; 的测试点比较特殊, 它测试了 &lt;code&gt;lw&lt;/code&gt;, &lt;code&gt;sw&lt;/code&gt; 和 &lt;code&gt;addi&lt;/code&gt; 这三个指令中立即数的范围.&lt;/p&gt;
&lt;p&gt;非常奇怪的是这个立即数溢出的问题在 &lt;a href=&quot;https://pku-minic.github.io/online-doc/#/lv4-const-n-var/var-n-assign&quot;&gt;Lv4.2. 变量和赋值&lt;/a&gt; 中第一次提到, 但是到了 &lt;code&gt;Lv6&lt;/code&gt; 的 &lt;code&gt;RISC-V&lt;/code&gt; 生成部分才第一次被测试, 并且这个测试点的名字非常具有误导性, 让人以为是逻辑运算的测试点, 实际上错误出在 &lt;code&gt;RISC-V&lt;/code&gt; 的立即数范围上...&lt;/p&gt;
&lt;p&gt;回忆 &lt;code&gt;RISC-V&lt;/code&gt; 的指令格式, 立即数范围为十二位整数, 即 &lt;code&gt;-2048&lt;/code&gt; 到 &lt;code&gt;2047&lt;/code&gt;, 所以这三个指令一旦遇到立即数超过这个范围的指令就需要进行额外的处理, 以 &lt;code&gt;lw&lt;/code&gt; 为例:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void RISCVPrinter::lw(const std::string &amp;#x26;rd, const std::string &amp;#x26;base, const int &amp;#x26;bias, RISCVContextManager &amp;#x26;context_manager)
{
    // 检查偏移量是否在 12 位立即数范围内
    if (bias &gt;= -2048 &amp;#x26;&amp;#x26; bias &amp;#x3C; 2048)
    {
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tlw &quot; &amp;#x3C;&amp;#x3C; rd &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; bias &amp;#x3C;&amp;#x3C; &quot;(&quot; &amp;#x3C;&amp;#x3C; base &amp;#x3C;&amp;#x3C; &quot;)&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
    else
    {
        std::string reg = context_manager.new_temp_reg();
        li(reg, bias);
        add(reg, reg, base);
        std::cout &amp;#x3C;&amp;#x3C; &quot;\tlw &quot; &amp;#x3C;&amp;#x3C; rd &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; &quot;(&quot; &amp;#x3C;&amp;#x3C; reg &amp;#x3C;&amp;#x3C; &quot;)&quot; &amp;#x3C;&amp;#x3C; std::endl;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且注意指令格式是 &lt;code&gt;lw rd, bias(base)&lt;/code&gt;, 其中 &lt;code&gt;bias&lt;/code&gt; 是&lt;strong&gt;立即数不是寄存器&lt;/strong&gt;, 所以 &lt;code&gt;%0 = load @x&lt;/code&gt; 指令 (其中 &lt;code&gt;@x&lt;/code&gt; 在栈偏移量为 2048 的地方) 应该写成:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;li t1, 2048
add t1, t1, sp
lw t0, (t1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;li t1, 2048
lw t0, t1(sp)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv6 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv6 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv7&lt;/h2&gt;
&lt;p&gt;这个 Level 需要实现 &lt;code&gt;while&lt;/code&gt; 和它配套的 &lt;code&gt;break&lt;/code&gt; 和 &lt;code&gt;continue&lt;/code&gt; 语句.&lt;/p&gt;
&lt;p&gt;这个 Level 的测试点中我有两个 WA 的测试点, 分别是 &lt;code&gt;12_break1&lt;/code&gt; 和 &lt;code&gt;14_summary1&lt;/code&gt; . 如果您有关于 &lt;code&gt;Lv7&lt;/code&gt; 的任何 corner case 的测试点, 欢迎和我交流!&lt;/p&gt;
&lt;p&gt;Update 2025-01-06: 问题出在控制流分析出错. 我之前认为如果在 &lt;code&gt;while&lt;/code&gt; 语句块中有 &lt;code&gt;return&lt;/code&gt; 语句, 那么 &lt;code&gt;while&lt;/code&gt; 语句也告诉上一层控制流返回了, 但是实际上这个 &lt;code&gt;while&lt;/code&gt; 语句可能根本不会进去, 是否进入 &lt;code&gt;while&lt;/code&gt; 语句块取决于运行时条件, 所以无论 &lt;code&gt;while&lt;/code&gt; 语句块中是否存在 &lt;code&gt;return&lt;/code&gt; 语句, 都应该告诉上一层控制流还没有返回.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;While&lt;/code&gt; Statement&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;while&lt;/code&gt; 语句的语法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stmt ::= &quot;while&quot; &quot;(&quot; Exp &quot;)&quot; Stmt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 &lt;code&gt;if&lt;/code&gt; 语句类似, 不再赘述.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Break&lt;/code&gt; and &lt;code&gt;Continue&lt;/code&gt; Statement&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;break&lt;/code&gt; 和 &lt;code&gt;continue&lt;/code&gt; 语句的语法如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Stmt ::= &quot;break;&quot;;
       | &quot;continue;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要的难点在于如何正确获取跳转的目标, 因为跳转的目标标签是在访问 &lt;code&gt;while&lt;/code&gt; 语句时定义的, 当你访问 &lt;code&gt;break&lt;/code&gt; 或 &lt;code&gt;continue&lt;/code&gt; 语句时, 需要获取 &lt;code&gt;while&lt;/code&gt; 语句的跳转目标标签.&lt;/p&gt;
&lt;p&gt;我们采用栈的方式获取跳转的目标标签, 当进入 &lt;code&gt;while&lt;/code&gt; 语句时, 把当前的 &lt;code&gt;while&lt;/code&gt; 语句的序号压入栈中, 当访问 &lt;code&gt;break&lt;/code&gt; 或 &lt;code&gt;continue&lt;/code&gt; 语句时, 从栈中读取当前的栈顶序号, 然后生成跳转指令.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;else if (stmt_type == StmtType::Break)
{
    if (koopa_context_manager.while_statement_stack.empty())
    {
        throw std::runtime_error(&quot;StmtAST::print: invalid break statement, not in a while statement&quot;);
    }
    int current_while_statement_count = koopa_context_manager.while_statement_stack.top();
    std::string while_end_label = &quot;%while_end_&quot; + std::to_string(current_while_statement_count);
    output_stream &amp;#x3C;&amp;#x3C; &quot;\tjump &quot; &amp;#x3C;&amp;#x3C; while_end_label &amp;#x3C;&amp;#x3C; &quot;\n&quot;;
    Result result = Result();
    result.control_flow_while_interrupted = true;
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv7 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv7 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv8&lt;/h2&gt;
&lt;p&gt;这一章节需要实现一个能够处理函数 (包括 SysY 库函数) 和全局变量的编译器.&lt;/p&gt;
&lt;p&gt;示例函数如下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;
int var;

int func(int x) 
{
  var = var + x;
  return var;
}

int main() 
{
  // putint 和 putch 都是 SysY 库函数
  // SysY 要求库函数不声明就可以使用
  putint(func(1));
  var = var * 10;
  putint(func(2));
  putch(10);
  return var;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Some Simple Advice&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;中间代码生成部分只需要注意重新 &lt;code&gt;load&lt;/code&gt; 函数参数, 这样可以为目标代码的生成省一些事, 具体来讲, 如果这样做了之后只有 &lt;code&gt;load&lt;/code&gt; 指令可能收到 &lt;code&gt;KOOPA_RVT_GLOBAL_ALLOC&lt;/code&gt; 这个代表函数参数的 &lt;code&gt;tag&lt;/code&gt;, 其他函数不用修改.&lt;/li&gt;
&lt;li&gt;在进入一个新函数后, 要在顶部保存 &lt;code&gt;ra&lt;/code&gt; 寄存器, 在退出函数后恢复 &lt;code&gt;ra&lt;/code&gt; 寄存器, 所以栈大小要多加一, 多分配一条 store 指令来存储 ra 寄存器, ra 是调用者保存寄存器, 调用者把它的 ra 存在每个栈帧的最上面, 调用函数之前修改这个寄存器为 call 的下一条指令, 然后进入下一个函数, 代表调用者的下一条指令.&lt;/li&gt;
&lt;li&gt;在栈上存变量的时候, 要注意把栈顶的留给函数参数的位置空出来, 不要存储局部变量, 否则可能造成函数参数对局部变量的覆盖.&lt;/li&gt;
&lt;li&gt;在调用完函数后, 需要检测 &lt;code&gt;value-&gt;ty-&gt;tag == KOOPA_RTT_UNIT&lt;/code&gt; 这个条件, 如果为真, 则函数没有返回值, 不需要生成 &lt;code&gt;return&lt;/code&gt; 指令, 否则需要.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv8 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv8 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lv9&lt;/h2&gt;
&lt;p&gt;终于知道为什么说编译原理比 ICS 的任务量多了...&lt;/p&gt;
&lt;p&gt;建议在没有绩点压力的大四选这门课就可以不用写 Lv9 了, 因为 Lv9 非常浪费时间并且占所有 Lab 的 27% 的分数, 也就是总评的 8 分, 如果在绩点压力下完成所有 Lab 会浪费很多时间.&lt;/p&gt;
&lt;p&gt;引流到 &lt;a href=&quot;https://arthals.ink/tags/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86/&quot;&gt;Arthals 的编译原理相关博客&lt;/a&gt; , 里面有更多关于编译原理课程和 Lab 的笔记, 相信他会把 Lv9 的 Lab 讲的更清楚.&lt;/p&gt;
&lt;h3&gt;Compile and Test&lt;/h3&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -koopa debug/hello.c -o debug/hello.koopa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行编译器, 把 &lt;code&gt;debug/hello.c&lt;/code&gt; 编译为 &lt;code&gt;debug/hello.S&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./build/compiler -riscv debug/hello.c -o debug/hello.S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;koopa&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -koopa -s lv9 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地自动评测 &lt;code&gt;riscv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;autotest -riscv -s lv9 /root/compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;这门课的 Lab 任务量很大, 但是思维难度实际上不大, 助教哥哥的文档写的还是比较清楚的, 只建议大四同学选这门课, 否则完成 Lab 的压力会很大.&lt;/p&gt;
&lt;p&gt;谢谢大家!&lt;/p&gt;
&lt;p&gt;Yutong Liang&lt;/p&gt;
&lt;p&gt;2025-01-12&lt;/p&gt;</content:encoded><h:img src="/_astro/how_compilers_work.CwrE3bLr.png"/><enclosure url="/_astro/how_compilers_work.CwrE3bLr.png"/></item><item><title>La La Land 2024</title><link>https://www.lyt0112.com/blog/lalaland_competition-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/lalaland_competition-zh</guid><description>WCA Competition @ Los Angeles, 一些新朋友, 个人 WCA 记录</description><pubDate>Sun, 01 Sep 2024 20:00:01 GMT</pubDate><content:encoded>&lt;p&gt;记录一下 La La Land 2024 的比赛经历&lt;/p&gt;
&lt;p&gt;上次比赛是五年前的天津赛, 这次正好在斯坦福做暑研, 也想去洛杉矶玩, 顺路比赛&lt;/p&gt;
&lt;h2&gt;出发&lt;/h2&gt;
&lt;p&gt;早上十点半就前往比赛场馆, 准备签到 (还得是希尔顿, 报名费 $70 也太贵了)&lt;/p&gt;
&lt;p&gt; 签到处 &lt;/p&gt;
&lt;p&gt;然后我们在休息区等待 WCA Delegate 讲解比赛事项和如何当裁判&lt;/p&gt;
&lt;p&gt; 准备开始比赛! &lt;/p&gt;
&lt;p&gt;甚至还有人在卖自己烤的有魔方图案的饼干, 坐在我旁边的大哥 &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2019HOIN01&quot;&gt;Preston Hoing (2019HOIN01)&lt;/a&gt; 买了两个送了我一个, 保存下来留作纪念&lt;/p&gt;
&lt;p&gt; 你是哪块儿小饼干 &lt;/p&gt;
&lt;h2&gt;比赛过程&lt;/h2&gt;
&lt;p&gt;这是一次 24 小时比赛, 也就是说 24 小时比赛不间断, 如果参加的比赛比较多 (比如我), 那就需要通宵, 事实上我也确实通宵了&lt;/p&gt;
&lt;p&gt;对这边的空调温度表示尊敬, 下次再也不穿短裤了...&lt;/p&gt;
&lt;p&gt;半夜冻的全身发抖, 每一把结束都需要去卫生间用热水洗手保持良好的状态&lt;/p&gt;
&lt;p&gt; 时间表 &lt;/p&gt;
&lt;p&gt;这次比赛总共参加了九个项目, 选两个有趣的项目讲讲&lt;/p&gt;
&lt;h3&gt;3x3&lt;/h3&gt;
&lt;p&gt;入魔 7 年, 前 3 年进步 5 秒, 后 4 年进步 5 秒...&lt;/p&gt;
&lt;p&gt;这次比赛刷新了单次和平均的个人记录, 单次还进了 10s, 平时练习的时候可能 20 把能有一把 sub10, 有点小激动&lt;/p&gt;
&lt;p&gt; SUB 10 &lt;/p&gt;
&lt;p&gt;下次比赛一定要做到平均 SUB 10 !&lt;/p&gt;
&lt;h3&gt;Pyraminx&lt;/h3&gt;
&lt;p&gt;不知道为什么我用的普通四棱往往会取得较高的名次, 这次在第二轮手感比较好, 刷新了单次和平均的个人纪录, 同时还进入了决赛&lt;/p&gt;
&lt;p&gt; Pyraminx Final &lt;/p&gt;
&lt;p&gt;可能是因为通宵太累了, 决赛发挥不好, 但是还是很开心能进决赛&lt;/p&gt;
&lt;p&gt; Pyraminx Grades &lt;/p&gt;
&lt;h2&gt;Interesting Things&lt;/h2&gt;
&lt;h3&gt;Clock WR&lt;/h3&gt;
&lt;p&gt;第一次见证世界纪录的诞生, &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2021DUNA01&quot;&gt;Brendyn Dunagan (2021DUNA01)&lt;/a&gt; 以 1.973s 刷新了 Clock 的单次世界纪录&lt;/p&gt;
&lt;p&gt; Clock WR &lt;/p&gt;
&lt;p&gt;还跟他合影留念了, 祝贺他 Nice Record!&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;国内外比赛区别&lt;/h3&gt;
&lt;p&gt;最开始我以为国外比赛和中国比赛流程是一样的, 但是实际上还是有比较多区别的&lt;/p&gt;
&lt;p&gt;中国比赛: Judge 负责拿走你的魔方, 在打乱区让打乱员打乱, 然后拿回到刚才的比赛台上, 每个人的五把结束之前是不会离开座位的&lt;/p&gt;
&lt;p&gt;国外比赛: 先提交魔方, 打乱完成后 Runner 把你的魔方放到一个有裁判的闲置比赛台上, 叫你的名字, 去这个比赛台完成复原, Judge 记录成绩, 结束之后你需要离开比赛台回到备赛区, Runner 把魔方送回打乱区, 等待下一次在另一个比赛台叫你的名字, 所以你需要在五个比赛台上完成五把&lt;/p&gt;
&lt;p&gt; 提交魔方 &lt;/p&gt;
&lt;p&gt;国外这样效率比较高, 因为不存在闲置的比赛台, 选手等待打乱的时候不会占用比赛台&lt;/p&gt;
&lt;p&gt;另外国内外裁判员和打乱员不同, 国外的裁判员都是选手, 每个人在一个比赛组的同时也会被分配到一个裁判组, 此时你需要找到一个空闲的裁判位置去当裁判&lt;/p&gt;
&lt;p&gt; 我在当裁判 &lt;/p&gt;
&lt;p&gt;当然裁判员的数量是过饱和的, 因为一个项目你只需要比 5 把, 但是需要裁判所有来到这个比赛桌的人, 大概 50 把, 所以也可以选择在轮到你的时候摆烂不去当裁判&lt;/p&gt;
&lt;p&gt;但是如果有很多人都摆烂, 不去当裁判, 就会造成缺少裁判的情况, WCA Delegate 就会在麦克风大叫, 找人来当裁判, 尤其是半夜的时候很多小朋友都去睡觉了, 甚至只有一个人来当裁判&lt;/p&gt;
&lt;p&gt; 裁判的码表⏱️ &lt;/p&gt;
&lt;p&gt;然而中国的裁判都是问卷报名制, 比赛之前从各种群发问卷招募裁判, 这样就能保证裁判是充足的, 但是坏处就是需要花精力去招募&lt;/p&gt;
&lt;h2&gt;People &amp;#x26; Friends&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.worldcubeassociation.org/persons/2019HOIN01&quot;&gt;Preston Hoing (2019HOIN01)&lt;/a&gt; 是我到了比赛场地第一个遇见的人, 他有点话唠, 跟我很热情地聊天, 还讲到了他上次魔表遇到的一个很好的打乱.&lt;/p&gt;
&lt;p&gt;他是这次比赛全项目的选手, 24小时的全项目对他来说真的累, 但是他还是坚持到了最后, 完成了他的第一次五盲, 还进了三盲决赛, 佩服. 不过忘了和他合影了, 有点遗憾.&lt;/p&gt;
&lt;p&gt;然后我坐到了一个小朋友和他爸爸旁边, 从巴西来的很可爱的小朋友: &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2024REIN02&quot;&gt;Benjamin Dias Reino (2024REIN02)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; With Benjamin and His Dad &lt;/p&gt;
&lt;p&gt;然后来了另两个小朋友, 是 Benjamin 在上一场比赛认识的, 两兄弟中的弟弟参加比赛 &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2024SEMI01&quot;&gt;Ren Davis Semien (2024SEMI01)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;后来这个桌子上又来了一个洛杉矶本地高中生 &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2022LEAV02&quot;&gt;Kierran Leavitt (2022LEAV02)&lt;/a&gt;, 他自述在学习开飞机, 包括滑翔机和有动力飞机, 还挺好玩&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;在三阶第一轮最后一把遇见了一个中国裁判员, &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2023SHAO01&quot;&gt;Tianshuo Shao (2023SHAO01)&lt;/a&gt; 如果不是他我可能需要说一天英语了, 脑子要烧了&lt;/p&gt;
&lt;p&gt;后来在比赛间隙他开车带我在洛杉矶游览, 晚上还去了 Santa Monica 海滩和码头, 好多大麻味&lt;/p&gt;
&lt;p&gt; Santa Monica Beach &lt;/p&gt;
&lt;p&gt;忘了合照, 过两天补上&lt;/p&gt;
&lt;p&gt;Update on Oct 28, 2024: 才想起来补合影&lt;/p&gt;
&lt;p&gt; 和 Tianshuo 在 SacCubing XVIII 2024 @ Sacremento &lt;/p&gt;
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;随便放一些图片&lt;/p&gt;
&lt;p&gt; 比赛之前 &lt;/p&gt;
&lt;p&gt; 在阳台练习的人 &lt;/p&gt;
&lt;p&gt; 酒店大阳台的景色, 对面停车场花了我和 Tianshuo $30&lt;/p&gt;
&lt;p&gt; 开进死胡同 &lt;/p&gt;
&lt;p&gt; 又开进死胡同 &lt;/p&gt;
&lt;p&gt; 半夜大家在庆祝 12 点 &lt;/p&gt;
&lt;p&gt; 希尔顿酒店的泳池 &lt;/p&gt;
&lt;p&gt; With Benjamin &lt;/p&gt;
&lt;p&gt; Santa Monica 码头 &lt;/p&gt;
&lt;p&gt; 在 Santa Monica 码头上钓鱼的人&lt;/p&gt;</content:encoded><h:img src="/_astro/la_cubing.DGDROiHK.jpeg"/><enclosure url="/_astro/la_cubing.DGDROiHK.jpeg"/></item><item><title>La La Land 2024</title><link>https://www.lyt0112.com/blog/lalaland_competition-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/lalaland_competition-en</guid><description>WCA Competition @ Los Angeles, met new friends and broke my personal WCA record.</description><pubDate>Sun, 01 Sep 2024 20:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The competition experience of La La Land 2024&lt;/p&gt;
&lt;p&gt;The last competition was the Tianjin competition five years ago.&lt;/p&gt;
&lt;h2&gt;Set out&lt;/h2&gt;
&lt;p&gt;Heading to the competition venue at 10:30 in the morning to sign in (So Hilton, the registration fee of $70 is too expensive).&lt;/p&gt;
&lt;p&gt; Sign-in desk &lt;/p&gt;
&lt;p&gt;Then we waited in the rest area for the WCA Delegate to explain the competition matters and how to be a judge.&lt;/p&gt;
&lt;p&gt; Get ready to start the competition! &lt;/p&gt;
&lt;p&gt;Some people were even selling cookies they baked with Rubik&apos;s Cube patterns. The guy sitting next to me, &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2019HOIN01&quot;&gt;Preston Hoing (2019HOIN01)&lt;/a&gt;, bought two and gave me one as a souvenir.&lt;/p&gt;
&lt;p&gt; Little Biscuit &lt;/p&gt;
&lt;h2&gt;Comp process&lt;/h2&gt;
&lt;p&gt;This is a 24-hour race, which means the race is continuous for 24 hours. If you participate in many events (like me), you will need to stay up all night. In fact, I did stay up all night.&lt;/p&gt;
&lt;p&gt;Respect the air conditioning temperature here, I will never wear shorts in a 24h comp again...&lt;/p&gt;
&lt;p&gt;Shivering all over in the mid-night, I need to go to the bathroom to wash my hands with hot water to maintain a good condition.&lt;/p&gt;
&lt;p&gt; Timetable &lt;/p&gt;
&lt;p&gt;A total of nine events participated in this competition, choose two interesting events to talk about.&lt;/p&gt;
&lt;h3&gt;3x3&lt;/h3&gt;
&lt;p&gt;Cubing for 7 years, improved by 5 seconds in the first 3 years, improved by 5 seconds in the last 4 years...&lt;/p&gt;
&lt;p&gt;This competition broke the single and average personal records, with a single time Sub10. During regular practice, there might be one sub-10 out of 20 attempts, so I&apos;m a bit excited.&lt;/p&gt;
&lt;p&gt; SUB 10 &lt;/p&gt;
&lt;p&gt;Next time, make a resolution to average SUB 10!&lt;/p&gt;
&lt;h3&gt;Pyraminx&lt;/h3&gt;
&lt;p&gt;I don&apos;t know why I often achieve higher rankings with the regular 4-sided, this time I felt good in the second round, broke my personal records for single and average, and also made it to the finals.&lt;/p&gt;
&lt;p&gt; Pyraminx Final &lt;/p&gt;
&lt;p&gt;It might be because I was too tired from staying up all night, I didn&apos;t perform well in the finals, but I&apos;m still very happy to have made it to the finals.&lt;/p&gt;
&lt;p&gt; Pyraminx Grades &lt;/p&gt;
&lt;h2&gt;Interesting Things&lt;/h2&gt;
&lt;h3&gt;Clock WR&lt;/h3&gt;
&lt;p&gt;Witnessing the birth of a world record for the first time, &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2021DUNA01&quot;&gt;Brendyn Dunagan (2021DUNA01)&lt;/a&gt; set a new single world record for Clock with a time of 1.973 seconds.&lt;/p&gt;
&lt;p&gt; Clock WR: 1.973s &lt;/p&gt;
&lt;p&gt;Took a photo with him, congratulated him Nice Record!&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;Differences between China and US competitions&lt;/h3&gt;
&lt;p&gt;At first, I thought that the process of foreign competitions was the same as that of Chinese competitions, but actually, there are quite a few differences.&lt;/p&gt;
&lt;p&gt;In Chinese competitions, the Judge is responsible for taking your cube, having it scrambled by the scrambler in the scrambling area, and then bringing it back to the competition table. Each person will not leave their seat until all five attempts are finished.&lt;/p&gt;
&lt;p&gt;In US competitions, you first submit your cube. After scrambling is completed, a Runner will place your cube on an idle competition table and call your name. You go to this competition table to complete the solve. The Judge records your result. After finishing one solve, you need to leave the competition table and return to the waiting area. The Runner will take your cube back to the scrambling area to wait for the next time your name is called at another competition table. So, you need to complete five solves on five different competition tables.&lt;/p&gt;
&lt;p&gt; Submit Rubik&apos;s Cube &lt;/p&gt;
&lt;p&gt;It seems that the efficiency is higher abroad because there are no idle competition tables, and the tables are not occupied during shuffling.&lt;/p&gt;
&lt;p&gt;In addition, judges and scramblers are different. US judges are all players. When each person is assigned to a competition group, they are also assigned to another judge group. During the competition of this group, you need to find a free judge position to act as a judge.&lt;/p&gt;
&lt;p&gt; I&apos;m a Judge &lt;/p&gt;
&lt;p&gt;Of course, the number of judges is oversaturated, because for one event you only need to judge 5 matches, but you need to judge all the people who come to this competition table, which may require judging about 50 matches, so you can also choose to slack off and not go when it&apos;s your turn to judge.&lt;/p&gt;
&lt;p&gt;However, many people are unwilling to be a judge, which often results in a lack of referees. The WCA Delegate has to shout into the microphone to find people to be judges, especially late at night when many kids have gone to bed, and sometimes there&apos;s only one person acting as a judge.&lt;/p&gt;
&lt;p&gt; The judge&apos;s stopwatch ⏱️ &lt;/p&gt;
&lt;p&gt;However, judges in China are recruited through a questionnaire registration system. Before the competition, referees are recruited by sending out questionnaires in various groups. This ensures that there are enough referees, but the downside is that it requires effort to recruit them.&lt;/p&gt;
&lt;h2&gt;People &amp;#x26; Friends&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.worldcubeassociation.org/persons/2019HOIN01&quot;&gt;Preston Hoing (2019HOIN01)&lt;/a&gt; was the first person I met at the competition venue. He is a bit of a chatterbox and chatted with me very enthusiastically, even mentioning a good scramble he encountered last time with the magic clock.&lt;/p&gt;
&lt;p&gt;He is a participant in all events of this competition. The 24-hour all-event challenge was really exhausting for him, but he still persevered until the end, completed his first five-blind, and even made it to the three-blind finals. Impressive. However, I forgot to take a photo with him, which is a bit regrettable.&lt;/p&gt;
&lt;p&gt;Then I sat next to a little boy and his father, a very cute little boy from Brazil: &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2024REIN02&quot;&gt;Benjamin Dias Reino (2024REIN02)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; With Benjamin and His Dad &lt;/p&gt;
&lt;p&gt;Then two other kids came, whom Benjamin met in the last competition. The younger brother of the two brothers participated in the competition &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2024SEMI01&quot;&gt;Ren Davis Semien (2024SEMI01)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;Later, a local high school student from Los Angeles &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2022LEAV02&quot;&gt;Kierran Leavitt (2022LEAV02)&lt;/a&gt; joined the table. He mentioned that he is learning to fly planes, including gliders and powered aircraft, which is quite fun.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;p&gt;In the last match of the first round of the third stage, I met a Chinese judge, &lt;a href=&quot;https://www.worldcubeassociation.org/persons/2023SHAO01&quot;&gt;Tianshuo Shao (2023SHAO01)&lt;/a&gt;. If it weren&apos;t for him, I might have had to speak English all day, my brain would have burned out.&lt;/p&gt;
&lt;p&gt;Later, during the break in the game, he drove me around Los Angeles. In the evening, we went to Santa Monica Beach and Pier, and there was a strong smell of marijuana.&lt;/p&gt;
&lt;p&gt; Santa Monica Beach &lt;/p&gt;
&lt;p&gt;Forgot to take a group photo, will make up for it in a couple of days.&lt;/p&gt;
&lt;p&gt;Update on Oct 28, 2024: Just remembered to put the photo here&lt;/p&gt;
&lt;p&gt; With Tianshuo at SacCubing XVIII 2024 @ Sacremento &lt;/p&gt;
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;Put some pictures&lt;/p&gt;
&lt;p&gt; Before the match &lt;/p&gt;
&lt;p&gt; The person practicing on the balcony &lt;/p&gt;
&lt;p&gt; The view from the hotel&apos;s large balcony, the parking lot across the street cost Tianshuo and me $30 &lt;/p&gt;
&lt;p&gt; Drive into a dead end &lt;/p&gt;
&lt;p&gt; Drove into a dead end again &lt;/p&gt;
&lt;p&gt; Everyone was celebrating at midnight &lt;/p&gt;
&lt;p&gt; The pool at the Hilton Hotel &lt;/p&gt;
&lt;p&gt; With Benjamin &lt;/p&gt;
&lt;p&gt; Santa Monica Pier &lt;/p&gt;
&lt;p&gt; People fishing on the Santa Monica Pier &lt;/p&gt;</content:encoded><h:img src="/_astro/la_cubing.DGDROiHK.jpeg"/><enclosure url="/_astro/la_cubing.DGDROiHK.jpeg"/></item><item><title>Ten Years with DW</title><link>https://www.lyt0112.com/blog/dw3-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw3-zh</guid><description>We are all different people all through our lives and that&apos;s okay, you&apos;ve got to keep moving so long as you remember all the people that you used to be.</description><pubDate>Fri, 23 Aug 2024 20:00:01 GMT</pubDate><content:encoded>&lt;h2&gt;Start of the Journey&lt;/h2&gt;
&lt;p&gt;依然清晰的记得 2014 年夏天的那天晚上, &lt;a href=&quot;https://tieba.baidu.com/p/3250946750&quot;&gt;贴吧的置顶帖&lt;/a&gt;里我看到了从来没见过的一集 Doctor Who, 一个从未见过的眉毛特别浓密的老头和 Clara 从 TARDIS 里出来, 我还以为是 Jenna Coleman 出演的另一部电视剧 (当时还没看到博士之日里面的眉毛或者博士之时里面的重生), 后来发现那是刚刚首播的 S08E01.&lt;/p&gt;
&lt;p&gt;最初了解到 Doctor Who 是因为这是老师上课放的众多科幻剧集中的一个 (BTW 我现在和他依然有联系, 他说现在上课放神秘小镇了, DW 有的时候比较恐怖), 后来在他的帮助下我找到了贴吧上面的资源追剧, 那一天我注册了贴吧账号, 第八季也恰巧在那一天开播, 如果点进我的&lt;a href=&quot;https://tieba.baidu.com/home/main?id=tb.1.f7667468.skxWSalCb6g2De4ISKeFxw&amp;#x26;fr=userbar&quot;&gt;贴吧主页&lt;/a&gt;会发现我恰好是十年吧龄.&lt;/p&gt;
&lt;h2&gt;Back to the Beginning&lt;/h2&gt;
&lt;p&gt;S08E01 首播十年的晚上重温了这一集, 每一句台词都是那么的熟悉, 好像一位老朋友再次和我娓娓道来我们刚认识时候他讲过的故事, 我知道他的会说的每一句话, 每一个梗和脸上的每一个表情, 甚至有的时候会突然有一种既视感, 让我感觉好像想起来了十年之前的情感. 比如 Clara 在餐厅地下被博士 &apos;抛弃&apos; 的时候, 一种陌生和绝望的感觉突然涌上心头, 似乎和曾经的我共享了同一个感情... 虽然是一样的剧集, 但是切身体会到 &lt;code&gt;所以兴怀，其致一也&lt;/code&gt; 的感觉是真的非常奇妙.&lt;/p&gt;
&lt;p&gt;P.S. 这一集中 Vastra 夫人说的 &quot;Well then, here we go again.&quot; 是 Brigadier 曾经在第三任博士重生为第四任博士的时候说过的话, 拥有这样长时间尺度的回忆和致敬也是我喜欢 DW 的一个非常重要的原因, 因为我同样是一个不喜欢结束的人, 就好像撕掉书的最后一页.&lt;/p&gt;
&lt;p&gt;这一集真的很好地刻画了博士重生之后的迷茫和脆弱, 他是一个充满活力的人, 但也感到害怕和孤独. 12 在翻译恐龙的咆哮声的时候或许真的是在表达自己的恐惧, 在这样的借物喻人之下, 博士对恐龙的安全保证是那么的脆弱, 让他那么的愧疚, 这和小 10 在庞贝火山爆发的时候有一些相似性, 但是不同却在于 12 最终没有救下恐龙但是 10 却救下了庞贝商人.&lt;/p&gt;
&lt;p&gt; You will be home again! &lt;/p&gt;
&lt;p&gt;Clara 对刚刚重生的 12 比较疏离却又非常担心他, 她还没有准备好接受博士突然变成了一个看起来完全陌生的形象, 以至于用一种非常有攻击性的语气和 Vastra 夫人对线, 但是最终还是在小 11 的最后电话之后慢慢接受了 12, 但是 12 对自己身份的追寻依然刚刚开始.&lt;/p&gt;
&lt;p&gt; How dare you! &lt;/p&gt;
&lt;h2&gt;How does it feel?&lt;/h2&gt;
&lt;p&gt;最开始我非常向往 Doctor 的旅行, 作为 Doctor 或者 Companion 是宇宙中最酷的工作了, 我想几乎每一个 Whovian 都曾经幻想过和 Doctor 一起旅行会是什么样的, 也正是这样的好奇心驱使着我们对 DW 的热爱.&lt;/p&gt;
&lt;p&gt;后来渐渐长大, 我逐渐理解了剧集中人物的感情和 Doctor 的价值观, 那样的好奇心和责任, &lt;code&gt;Never cruel, never cowardly; never give up, never give in&lt;/code&gt;, 我一直非常喜欢这句话, 希望每一个人都能够坚持自己的信念.&lt;/p&gt;
&lt;p&gt;当然, 作为一个设定上寿命极长的角色, 唯一的 Time Lord, 拥有这样强大的能力和智慧, 如何约束自己的行为, 我想这个命题在 Clara 离去的三集中做出了充分的探讨.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Face the Raven&lt;/code&gt;, &lt;code&gt;Heaven Sent&lt;/code&gt; 和 &lt;code&gt;Hell Bent&lt;/code&gt; 这三集作为季末的整体, 讲述当 Doctor 失去了至亲之人后会做出怎样的选择, 甚至会为了复仇而放弃自己的原则, 选择修改时间, 但是最后依然不能阻止事情的发生, 或许对于每一个人也是一样的, 当一个人面对极端的情况, 往往会下意识地否认, 试图扭转状况, 只不过这里的 Doctor 可以操纵时间, 但是依然无法让 Clara 逃避自己的命运.&lt;/p&gt;
&lt;p&gt; 听说这个画的背面也有写字 &quot;I&apos;m in 12&quot;, 但是 Doctor 不忍心把它转过来... &lt;/p&gt;
&lt;p&gt;如果说学校的教育是知识的传授, 那么我相信 DW 一定在我的价值观中占有一席之地, 好奇心和冒险精神, 如何面对困难, 以及对于人性的探讨, 在这些普世价值和命题上的探讨导致 Doctor Who 对我的影响真的很大, 也让我想去亲自 &lt;code&gt;see the world&lt;/code&gt;, 并且很多时候问一下自己: &lt;code&gt;What would The Doctor do?&lt;/code&gt; 或许就能得到一个很好的答案.&lt;/p&gt;
&lt;h2&gt;Words&lt;/h2&gt;
&lt;p&gt;DW 里面真的有很多我很喜欢的台词, 有一些非常有名, 这里列举一些我比较喜欢的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One day, I shall come back. Yes, I shall come back. Until then, there must be no regrets, no tears, no anxieties. Just go forward in all your beliefs, and prove to me that I am not mistaken in mine.&lt;/li&gt;
&lt;li&gt;I burned up a sun just to say goodbye.&lt;/li&gt;
&lt;li&gt;Never cruel, never cowardly; never give up, never give in.&lt;/li&gt;
&lt;li&gt;Goodness is not goodness that seeks advantage. Good is good in the final hour, in the deepest pit without hope, without witness, without reward. Virtue is only virtue in extremis.&lt;/li&gt;
&lt;li&gt;It all just disappears doesn&apos;t it? Everything you are, gone in a moment like breath on a mirror. Any moment now, he’s a coming, (Clara: Who&apos;s coming?) The Doctor. (Clara: But you are, you are the doctor.) And I always will be. But times change and so must I. We all change, when you think about it. We are all different people all through our lives and that&apos;s okay, that&apos;s good, you&apos;ve got to keep moving so long as you remember all the people that you used to be. I will not forget one line of this, not one day, I swear. I will always remember when The Doctor was me.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐几个粉丝混剪:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Tx411P746&quot;&gt;B站 - Are You The One&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;这首歌绝了, 完美契合 Doctor 的形象, 我通过这个混剪爱上了这首歌, 剪辑也非常到位.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7li5eZ3JZbA?si=TDZun38jWCOf7z9Q&quot;&gt;YouTube - The First Question - 50th Anniversary Trailer&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;50 周年的混剪, 12 年之前的视频了, 但是到现在也不过时, 尤其是台词的选取和配合, 以及音乐的选择都非常到位.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/-CdJUYWkrR0?si=bmuiay9TYl3zI1zF&quot;&gt;YouTube - The First Question - 60th Anniversary Edition&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;60 周年的混剪, 上一个视频的十年后重制版, 也是非常的精彩.&lt;/li&gt;
&lt;li&gt;这个视频 B 站还没有搬运, 因此我联系上作者取得了转载授权.&lt;/li&gt;
&lt;li&gt;B 站搬运: &lt;a href=&quot;https://www.bilibili.com/video/BV1unW1e8EwR/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;B站 - The First Question - 60th Anniversary Edition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;用这篇文章简单聊了聊和 DW 的故事, 但是有更多的 delicate 的情感很难用文字表达出来, 很多剧集真的是百看不厌, 最喜欢的一集还是 &lt;code&gt;Heaven Sent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;但是我真的很希望下一季的 DW 能够在剧情上多下下功夫, S14/新新版S1 的最后一集有点难绷... 相关吐槽欢迎前往 &lt;a href=&quot;https://www.lyt0112.com/blog/dw2&quot;&gt;[DW] 幽默RTD, 幽默S14E08&lt;/a&gt; 观看.&lt;/p&gt;
&lt;p&gt;每个博士重生都不只是博士追寻新身份的过程，也是观众接受新博士的过程, 12 的首集通过女伴的感情变化来慢慢表达了这个过程. 其实观众接纳新博士本身也是接纳新自己的过程, 在现实中同样代表了我们愿不愿意勇敢地开始一个新的人生阶段, 愿不愿意在这个过程中舍弃原先拥有东西, 即便这个东西是好的, 我们珍爱的.&lt;/p&gt;
&lt;p&gt;就像这句话一样: &lt;code&gt;So long as you remember all the people that you used to be&lt;/code&gt; 在人生前进的路上只要我们记得曾经的那些美好, 就没有浪费那些时光. 十年之前的我又怎能想象到现在的我是什么样的呢, 但是只要记住这十年来 DW 带来的感动 (或者 CC 的垃圾剧情) 就足够了.&lt;/p&gt;
&lt;p&gt; The Doctors &lt;/p&gt;
&lt;p&gt;我和 DW 的下个十年会怎么样呢? Nobody knows, 但是我会一直期待下一集 Doctor Who 的!&lt;/p&gt;
&lt;p&gt;如果你也是 Whovian, 欢迎在评论区分享你和 DW 的故事或者你最喜欢的台词 / 剧集.&lt;/p&gt;
&lt;p&gt;After all, that&apos;s how the all started!&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.facebook.com/DoctorWho/posts/pfbid04btj2pTSrWnJXfoMMJQ8rWiHKuz9H9P21QN7Hamwei3sNv6HkL7V8tiS3v8UetFXl&quot;&gt;Doctor Who Facebook - Flashpoint, final episode of the Dalek Invasion of Earth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/YWfpn6k79pQ?si=eBJH5Zyb2dzMLj_F&quot;&gt;Doctor Who Clips - The Five Doctors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Doctor_Who_series_8&quot;&gt;Wiki - DW S8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Doctor_Who_series_9&quot;&gt;Wiki - DW S9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7L9cUG3BxN0?si=3b19mIGhbglNDfWz&quot;&gt;YouTube - The Subtle Brilliance Of Doctor Who&apos;s Most Divisive Finale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7li5eZ3JZbA?si=TDZun38jWCOf7z9Q&quot;&gt;YouTube - Doctor Who: The First Question - 50th Anniversary Trailer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/-CdJUYWkrR0?si=bmuiay9TYl3zI1zF&quot;&gt;YouTube - Doctor Who: The First Question - 60th Anniversary Edition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Tx411P746&quot;&gt;Bilibili - Are You The One&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/dw-s08e01.DivpxHP8.png"/><enclosure url="/_astro/dw-s08e01.DivpxHP8.png"/></item><item><title>Decade with DW</title><link>https://www.lyt0112.com/blog/dw3-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw3-en</guid><description>We are all different people all through our lives and that&apos;s okay, you&apos;ve got to keep moving so long as you remember all the people that you used to be.</description><pubDate>Fri, 23 Aug 2024 20:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Start of the Journey&lt;/h2&gt;
&lt;p&gt;I still clearly remember that night in the summer of 2014, in the &lt;a href=&quot;https://tieba.baidu.com/p/3250946750&quot;&gt;pinned post&lt;/a&gt;, I saw an episode of Doctor Who that I had never seen before. An old man with particularly thick eyebrows and Clara came out of the TARDIS. I thought it was another TV series starring Jenna Coleman (at that time, I hadn&apos;t seen the eyebrows in &quot;The Day of the Doctor&quot; or the regeneration in &quot;The Time of the Doctor&quot;). Later, I realized it was the just-premiered S08E01.&lt;/p&gt;
&lt;p&gt;The first time I learned about Doctor Who was because it was one of the many sci-fi series that the teacher played in class. Later, with his help, I found resources to follow the series.&lt;/p&gt;
&lt;h2&gt;Back to the Beginning&lt;/h2&gt;
&lt;p&gt;Rewatched this S08E01 episode on the night of its ten-year premiere, every line of dialogue is so familiar, like an old friend recounting the stories he told me when we first met. I know every word he will say, every joke, and every expression on his face.&lt;/p&gt;
&lt;p&gt;Sometimes, I even get a sense of déjà vu, making me feel like I remember the emotions from ten years ago. For example, when Clara was &apos;abandoned&apos; by the Doctor in the restaurant basement, a feeling of unfamiliarity and despair suddenly surged in my heart, as if I shared the same emotion with my past self... Although it&apos;s the same episode, the personal experience of &quot;so the feelings are the same&quot; is truly very wonderful.&lt;/p&gt;
&lt;p&gt;P.S. In this episode, what Mrs. Vastra said, &quot;Well then, here we go again,&quot; was something Brigadier once said when the Third Doctor regenerated into the Fourth Doctor. Having such long-term memories and tributes is one of the very important reasons why I like DW, because I am also someone who doesn&apos;t like endings, just like tearing out the last page of a book.&lt;/p&gt;
&lt;p&gt;This episode really well portrays the Doctor&apos;s confusion and vulnerability after his regeneration. He is a person full of energy, but also feels fear and loneliness. When translating the dinosaur&apos;s roar, he might really be expressing his own fear. Under such a metaphor, the Doctor&apos;s assurance of the dinosaur&apos;s safety is so fragile, making him feel so guilty. This is somewhat similar to the situation with the Tenth Doctor during the eruption of Mount Vesuvius, but the difference is that the Twelfth Doctor ultimately did not save the dinosaur, whereas the Tenth Doctor saved the Pompeii merchant.&lt;/p&gt;
&lt;p&gt; You will be home again! &lt;/p&gt;
&lt;p&gt;Clara is somewhat distant from the newly regenerated 12 but is very worried about him. She is not yet ready to accept that the Doctor has suddenly turned into a completely unfamiliar appearance, to the point of using a very aggressive tone with Madam Vastra. However, she eventually starts to accept 12 after the final call from 11, but 12&apos;s quest for his own identity has just begun.&lt;/p&gt;
&lt;p&gt; How dare you! &lt;/p&gt;
&lt;h2&gt;How does it feel?&lt;/h2&gt;
&lt;p&gt;At first, I was very eager for the Doctor&apos;s travels. Being a Doctor or a Companion is the coolest job in the universe. I think almost every Whovian has fantasized about what it would be like to travel with the Doctor, and it is this curiosity that drives our love for DW.&lt;/p&gt;
&lt;p&gt;As I grew up, I gradually understood the emotions of the characters in the series and the values of the Doctor, such curiosity and responsibility, &quot;Never cruel, never cowardly; never give up, never give in.&quot; I have always liked this phrase very much and hope that everyone can stick to their beliefs.&lt;/p&gt;
&lt;p&gt;Of course, as a character with an extremely long lifespan, the only Time Lord, possessing such powerful abilities and wisdom, how to restrain one&apos;s behavior, I think this proposition was fully explored in the three episodes where Clara left.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Face the Raven&lt;/code&gt;, &lt;code&gt;Heaven Sent&lt;/code&gt;, and &lt;code&gt;Hell Bent&lt;/code&gt; as a whole for the season finale, tell the story of what choices the Doctor will make after losing a loved one, even to the point of abandoning his principles for revenge and choosing to alter time. However, in the end, he still cannot prevent things from happening. Perhaps it is the same for everyone; when faced with extreme situations, people often subconsciously deny and try to reverse the situation. But here, even though the Doctor can manipulate time, he still cannot let Clara escape her fate.&lt;/p&gt;
&lt;p&gt; I heard that there is also writing on the back of this painting saying &quot;I&apos;m in 12&quot;, but the Doctor couldn&apos;t bear to turn it over... &lt;/p&gt;
&lt;p&gt;If the education at school is the transmission of knowledge, then I believe DW definitely holds a place in my values. Curiosity and adventurous spirit, how to face difficulties, and the exploration of human nature. The discussion on these values and propositions has led Doctor Who to have a significant impact on me, making me want to personally &lt;code&gt;see the world&lt;/code&gt;, and often ask myself: &lt;code&gt;What would The Doctor do?&lt;/code&gt; Perhaps I can get a very good answer.&lt;/p&gt;
&lt;h2&gt;Words&lt;/h2&gt;
&lt;p&gt;There are really many lines in DW that I like, some of which are very famous. Here are some that I particularly like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One day, I shall come back. Yes, I shall come back. Until then, there must be no regrets, no tears, no anxieties. Just go forward in all your beliefs, and prove to me that I am not mistaken in mine.&lt;/li&gt;
&lt;li&gt;I burned up a sun just to say goodbye.&lt;/li&gt;
&lt;li&gt;Never cruel, never cowardly; never give up, never give in.&lt;/li&gt;
&lt;li&gt;Goodness is not goodness that seeks advantage. Good is good in the final hour, in the deepest pit without hope, without witness, without reward. Virtue is only virtue in extremis.&lt;/li&gt;
&lt;li&gt;It all just disappears doesn&apos;t it? Everything you are, gone in a moment like breath on a mirror. Any moment now, he’s a coming, (Clara: Who&apos;s coming?) The Doctor. (Clara: But you are, you are the doctor.) And I always will be. But times change and so must I. We all change, when you think about it. We are all different people all through our lives and that&apos;s okay, that&apos;s good, you&apos;ve got to keep moving so long as you remember all the people that you used to be. I will not forget one line of this, not one day, I swear. I will always remember when The Doctor was me.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Recommend some videos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Tx411P746&quot;&gt;Bilibili - Are You The One&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;This song is amazing, perfectly fitting the image of the Doctor. I fell in love with this song through this mashup, and the editing is also very well done.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7li5eZ3JZbA?si=TDZun38jWCOf7z9Q&quot;&gt;YouTube - The First Question - 50th Anniversary Trailer&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;The 50th anniversary mashup, a video from 12 years ago, but still not outdated, especially the selection and coordination of lines, as well as the choice of music, are all very well done.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/-CdJUYWkrR0?si=bmuiay9TYl3zI1zF&quot;&gt;YouTube - The First Question - 60th Anniversary Edition&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;A 60th-anniversary mashup, a remastered version of the previous video from ten years ago, is also very exciting.&lt;/li&gt;
&lt;li&gt;This video has not been uploaded to Bilibili yet, so I contacted the author and obtained permission to repost it.&lt;/li&gt;
&lt;li&gt;Bilibili repost: &lt;a href=&quot;https://www.bilibili.com/video/BV1unW1e8EwR/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;Bilibili - The First Question - 60th Anniversary Edition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;This article briefly talks about the story with DW, but there are more delicate emotions that are hard to express in words. Many episodes are really worth watching repeatedly, and my favorite episode is still &lt;code&gt;Heaven Sent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;However, I really hope that the next season of DW can put more effort into the plot. The last episode of S14/New Version S1 is a bit hard to endure... Related discussions are welcome to visit &lt;a href=&quot;https://www.lyt0112.com/blog/dw2&quot;&gt;[DW] 幽默RTD, 幽默S14E08&lt;/a&gt; to watch.&lt;/p&gt;
&lt;p&gt;Every Doctor&apos;s regeneration is not just a process of the Doctor seeking a new identity, but also a process for the audience to accept the new Doctor. The first episode of 12 slowly expresses this process through the emotional changes of the companion. In fact, the audience&apos;s acceptance of the new Doctor is also a process of accepting a new self, which in reality represents whether we are willing to bravely start a new stage of life, and whether we are willing to give up things we previously had, even if those things are good and cherished by us.&lt;/p&gt;
&lt;p&gt;Just like this sentence: &lt;code&gt;So long as you remember all the people that you used to be&lt;/code&gt; As long as we remember those beautiful moments on the road of life, we haven&apos;t wasted that time. How could I have imagined what I would be like ten years ago, but as long as I remember the emotions brought by DW (or the bad plot from CC) over the past ten years, it&apos;s enough.&lt;/p&gt;
&lt;p&gt; The Doctors &lt;/p&gt;
&lt;p&gt;What will the next decade be like for me and DW? Nobody knows, but I will always look forward to the next episode of Doctor Who!&lt;/p&gt;
&lt;p&gt;If you are also a Whovian, feel free to share your stories with DW or your favorite lines/episodes in the comments.&lt;/p&gt;
&lt;p&gt;After all, that&apos;s how the all started!&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.facebook.com/DoctorWho/posts/pfbid04btj2pTSrWnJXfoMMJQ8rWiHKuz9H9P21QN7Hamwei3sNv6HkL7V8tiS3v8UetFXl&quot;&gt;Doctor Who Facebook - Flashpoint, final episode of the Dalek Invasion of Earth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/YWfpn6k79pQ?si=eBJH5Zyb2dzMLj_F&quot;&gt;Doctor Who Clips - The Five Doctors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Doctor_Who_series_8&quot;&gt;Wiki - DW S8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Doctor_Who_series_9&quot;&gt;Wiki - DW S9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7L9cUG3BxN0?si=3b19mIGhbglNDfWz&quot;&gt;YouTube - The Subtle Brilliance Of Doctor Who&apos;s Most Divisive Finale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/7li5eZ3JZbA?si=TDZun38jWCOf7z9Q&quot;&gt;YouTube - Doctor Who: The First Question - 50th Anniversary Trailer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/-CdJUYWkrR0?si=bmuiay9TYl3zI1zF&quot;&gt;YouTube - Doctor Who: The First Question - 60th Anniversary Edition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1Tx411P746&quot;&gt;Bilibili - Are You The One&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/dw-s08e01.DivpxHP8.png"/><enclosure url="/_astro/dw-s08e01.DivpxHP8.png"/></item><item><title>SQ-1: Basic Lars Method</title><link>https://www.lyt0112.com/blog/sq1-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/sq1-zh</guid><description>SQ-1 魔方的最初级解法, 即使比较初级, 仍然较为复杂</description><pubDate>Tue, 06 Aug 2024 00:23:21 GMT</pubDate><content:encoded>&lt;h2&gt;魔方介绍&lt;/h2&gt;
&lt;p&gt;Square-1 (以前称为Cube 21和Back to Square One) 是一个两极类可变形魔方. 它的解法非常独特, 因为上下两层的块是可以通过上下层的转换变换顺序的, 所以不存在角块和棱块之说, 上层可以有10个块, 而底层只有6个块.&lt;/p&gt;
&lt;p&gt;该魔方由 Karel Hršel 和 Vojtech Kopský 于1990年发明.&lt;/p&gt;
&lt;p&gt;它是WCA官方项目，最快解法由来自美国的 Ryan Pilat 保持 (3.41秒).&lt;/p&gt;
&lt;h2&gt;记号表示&lt;/h2&gt;
&lt;p&gt;顶（底）层在图片的左（右）侧, 所有图均为俯视图.&lt;/p&gt;
&lt;p&gt;拿 SQ1 让赤道（中层）的前面的左侧为短边. 不要整体旋转 SQ1, 左手始终握住赤道的短边.&lt;/p&gt;
&lt;p&gt;| 记号               | 含义                                                  |
| ------------------ | ----------------------------------------------------- |
| $\pm x \pm y$      | 转 U 层 $(\pm 30x)^\circ$ 并转 D 层 $(\pm 30y)^\circ$ |
| $n, \underline{n}$ | $n0, 0n$                                              |
| $U, D$             | $3, \underline{3}$                                    |
| $M2$               | 1/-1-1/01                                             |&lt;/p&gt;
&lt;h2&gt;复原步骤&lt;/h2&gt;
&lt;h3&gt;复原形状&lt;/h3&gt;
&lt;p&gt;所有复形的最后都是要转化成风筝-风筝, 可以做 4 个连角后化成扇贝-风筝；也可以先做 3 个连角并放在 DL，再做 3 个并放在 DR 后化为 8-星形或 71-星形.&lt;/p&gt;
&lt;p&gt; 复原形状 &lt;/p&gt;
&lt;p&gt;推荐初学者采用后一种方式, 先把所有 60 度角块放在底层组成一个星星, 就可以套用公式啦. 当你理解了公式的原理后, 你可以尝试脱离公式, 用自己的思考解决, 类似 F2L.&lt;/p&gt;
&lt;p&gt; 六角星公式 &lt;/p&gt;
&lt;h3&gt;角块色相&lt;/h3&gt;
&lt;p&gt;这一步比较简单, 逻辑上类似四阶魔方的中心块复原.&lt;/p&gt;
&lt;p&gt; 角块色相 &lt;/p&gt;
&lt;h3&gt;棱块色相&lt;/h3&gt;
&lt;p&gt;单棱交换就是背公式了, 但是双棱交换可以试试这个公式然后理解这个转换机的工作流程.&lt;/p&gt;
&lt;p&gt; 棱块色相 &lt;/p&gt;
&lt;h3&gt;角块顺序&lt;/h3&gt;
&lt;p&gt;两个比较简单可以理解的公式.&lt;/p&gt;
&lt;p&gt; 角块顺序 &lt;/p&gt;
&lt;p&gt;其他情况: 顶层和底层肯定有颜色在同一个面上相同的角块, 把这个面放在左或右侧后做 /U&apos;/UD/D&apos;/ , 变成上图中第一种情况.&lt;/p&gt;
&lt;h3&gt;棱块顺序&lt;/h3&gt;
&lt;p&gt;这里的相邻棱交换也是需要背公式, 但是对棱交换也可以试试这个公式然后理解这个转换机的工作流程.&lt;/p&gt;
&lt;p&gt; 棱块顺序 &lt;/p&gt;
&lt;h3&gt;奇偶校验&lt;/h3&gt;
&lt;p&gt;最逆天的公式...&lt;/p&gt;
&lt;p&gt; 奇偶校验 &lt;/p&gt;
&lt;h3&gt;赤道翻转&lt;/h3&gt;
&lt;p&gt;如果最后出现中层位置错误的情况, 可以用这个公式解决.&lt;/p&gt;
&lt;p&gt; 赤道翻转 &lt;/p&gt;
&lt;h2&gt;Credit&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://ruwix.com/twisty-puzzles/square-1-back-to-square-one/&quot;&gt;Ruwix: Twisty Puzzle Wiki - Square-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cuberoot.me/sq-beginner-lars/&quot;&gt;Cube Root - Square-1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/sq1_cover.B1UPAu-D.jpg"/><enclosure url="/_astro/sq1_cover.B1UPAu-D.jpg"/></item><item><title>SQ-1: Basic Lars Method</title><link>https://www.lyt0112.com/blog/sq1-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/sq1-en</guid><description>SQ-1 cube&apos;s most basic solution, even though relatively basic, is still quite complicated.</description><pubDate>Tue, 06 Aug 2024 00:23:20 GMT</pubDate><content:encoded>&lt;h2&gt;Intoduction to SQ-1&lt;/h2&gt;
&lt;p&gt;The Square-1 (previously called as Cube 21 and Back to Square One) is a shape-shifting three-layered twisty puzzle. Its solution is very unique because the kite-shaped corners and the triangular edges are indistinguishable to the puzzle&apos;s inner mechanism, meaning that corners can be swapped with edges and therefore it&apos;s possible to have 10 pieces in the upper layer while only 6 in the bottom.&lt;/p&gt;
&lt;p&gt;The puzzle was invented in 1990 by Karel Hršel and Vojtech Kopský.&lt;/p&gt;
&lt;p&gt;It&apos;s an official WCA competition event, the fastest solution being held by Ryan Pilat from USA (3.41 seconds).&lt;/p&gt;
&lt;h2&gt;Notation&lt;/h2&gt;
&lt;p&gt;The top (bottom) layer is on the left (right) side of the image, all images are top views.&lt;/p&gt;
&lt;p&gt;Take SQ1 and let the left side of the front of the equator (middle layer) be the short edge. Do not rotate SQ1 as a whole, always hold the short edge of the equator with your left hand.&lt;/p&gt;
&lt;p&gt;| Notation           | Meaning                                                                        |
| ------------------ | ------------------------------------------------------------------------------ |
| $\pm x \pm y$      | Rotate U layer for $(\pm 30x)^\circ$  AND rotate D layer for $(\pm 30y)^\circ$ |
| $n, \underline{n}$ | $n0, 0n$                                                                       |
| $U, D$             | $3, \underline{3}$                                                             |
| $M2$               | 1/-1-1/01                                                                      |&lt;/p&gt;
&lt;h2&gt;Steps&lt;/h2&gt;
&lt;h3&gt;Cubeshape&lt;/h3&gt;
&lt;p&gt;The end of any cubeshape is to transform into Kite-Kite. One can make 4 paired corners then transform to Scallop-Kite. Or make and place 3 paired corners in DL, then 3 more in DR, and then transform into 8-Star or 71-Star.&lt;/p&gt;
&lt;p&gt; Cubeshape &lt;/p&gt;
&lt;p&gt;It is recommended for beginners to use the latter method. First, place all the 60-degree angle pieces on the bottom layer to form a star, then you can apply the formula. Once you understand the principle of the formula, you can try to solve it without the formula, using your own thinking, similar to F2L.&lt;/p&gt;
&lt;p&gt; Hexagram formula &lt;/p&gt;
&lt;h3&gt;Corner Orientation&lt;/h3&gt;
&lt;p&gt;This step is relatively simple, logically similar to the restoration of the center piece of a 4x4 Rubik&apos;s Cube.&lt;/p&gt;
&lt;p&gt; Corner Orientation &lt;/p&gt;
&lt;h3&gt;Edge Orientation&lt;/h3&gt;
&lt;p&gt;Single edge exchange is about memorizing the formula, but for double edge exchange, you can try this formula and understand the workflow of this commutator.&lt;/p&gt;
&lt;p&gt; Edge Orientation &lt;/p&gt;
&lt;h3&gt;Corner Permutation&lt;/h3&gt;
&lt;p&gt;Two relatively simple and understandable formulas.&lt;/p&gt;
&lt;p&gt; Corner Permutation &lt;/p&gt;
&lt;p&gt;Other situations: There are definitely corner pieces with the same color on the same face for the top and bottom layer. After placing this face on the left or right side, perform /U&apos;/UD/D&apos;/ to turn it into the first situation in the above diagram.&lt;/p&gt;
&lt;h3&gt;Edge Permutation&lt;/h3&gt;
&lt;p&gt;The adjacent edge swap here also requires memorizing the formula, but for opposite edge swaps, you can also try this formula and understand the workflow of this commutator.&lt;/p&gt;
&lt;p&gt; Edge Permutation &lt;/p&gt;
&lt;h3&gt;Parity&lt;/h3&gt;
&lt;p&gt;The most complicated formula...&lt;/p&gt;
&lt;p&gt; Parity &lt;/p&gt;
&lt;h3&gt;Equator Flip&lt;/h3&gt;
&lt;p&gt;If there is an error in the equator (middle layer) position at the end, you can use this formula to solve it.&lt;/p&gt;
&lt;p&gt; Equator Flip &lt;/p&gt;
&lt;h2&gt;Credit&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://ruwix.com/twisty-puzzles/square-1-back-to-square-one/&quot;&gt;Ruwix: Twisty Puzzle Wiki - Square-1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cuberoot.me/sq-beginner-lars/&quot;&gt;Cube Root - Square-1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/sq1_cover.B1UPAu-D.jpg"/><enclosure url="/_astro/sq1_cover.B1UPAu-D.jpg"/></item><item><title>San Francisco Trip</title><link>https://www.lyt0112.com/blog/sf_trip</link><guid isPermaLink="true">https://www.lyt0112.com/blog/sf_trip</guid><description>First Caltrain Journey, Berkeley, San Francisco.</description><pubDate>Mon, 22 Jul 2024 17:44:14 GMT</pubDate><content:encoded>&lt;h2&gt;First Caltrain Journey&lt;/h2&gt;
&lt;p&gt;I was quite curious before boarding the Caltrain for my first experience with public transportation in the US. Purchasing a ticket or acquiring a Clipper card turned out to be straightforward - you can buy a ticket at any station or download a digital card via the Clipper App.&lt;/p&gt;
&lt;p&gt;Upon arriving at the Caltrain station, I immediately noticed the contrast with Chinese high-speed railway stations. Caltrain stations are considerably more open; you don&apos;t have to swipe your Clipper card or show your train ticket to access the platform. Although it&apos;s possible to board the train without paying, conductors periodically check for valid tickets.&lt;/p&gt;
&lt;p&gt;Caltrain features double-decker trains equipped with one or two bike cars. The first level is dedicated to cyclists, making it extremely bike-friendly. As for seating, the second level offers two configurations: a single-seat arrangement with an open view to the lower level, and standard seats accompanied by tables.&lt;/p&gt;
&lt;p&gt;I took the Caltrain from California Ave Station to SF Station and back, departing at 7:48AM and returning at 8:57PM on Saturday.&lt;/p&gt;
&lt;p&gt;I generally love its convenience, but it&apos;s a bit slow, taking more than 1 hour for a 60km trip.&lt;/p&gt;
&lt;h2&gt;Berkeley&lt;/h2&gt;
&lt;p&gt;I took an Uber to Berkeley and enjoyed a brief hour-long tour, wandering around.&lt;/p&gt;
&lt;h2&gt;San Francisco&lt;/h2&gt;
&lt;h3&gt;Lunch&lt;/h3&gt;
&lt;p&gt;In-N-Out Burger!&lt;/p&gt;
&lt;h3&gt;Exploring and Vintage Shops&lt;/h3&gt;
&lt;p&gt;Be cautious of coyotes!&lt;/p&gt;
&lt;p&gt;Enjoy exploring the beautiful mountainous terrain and have fun.&lt;/p&gt;
&lt;p&gt;{/* In a perfume shop we met a active puppy.&lt;/p&gt;
&lt;p&gt;Also, purchased chocolates worth $130.&lt;/p&gt;
&lt;p&gt;A book from 1911&lt;/p&gt;
&lt;h3&gt;Dinner&lt;/h3&gt;
&lt;p&gt;Very good location and scene! It&apos;s called &lt;code&gt;Crab House&lt;/code&gt; at Pier 39.&lt;/p&gt;
&lt;p&gt;There were people celebrating a birthday there, and we encountered two birthday parties!&lt;/p&gt;
&lt;p&gt;{/*  */}&lt;/p&gt;
&lt;h3&gt;Golden Gate Bridge&lt;/h3&gt;
&lt;p&gt;I took some fantastic photos!&lt;/p&gt;
&lt;p&gt;It was extremely windy and cold, so please dress more warmly!&lt;/p&gt;
&lt;h2&gt;Home&lt;/h2&gt;
&lt;p&gt;It was very cold at the SF Caltrain Station before I boarded the train at 9 PM.&lt;/p&gt;
&lt;p&gt;After arriving at California Ave Station at 10PM, I found it convenient that I had parked my bike next to the platform that morning.&lt;/p&gt;</content:encoded><h:img src="/_astro/golden_gate_bridge.bM31Ko2T.jpeg"/><enclosure url="/_astro/golden_gate_bridge.bM31Ko2T.jpeg"/></item><item><title>Rubik&apos;s Clock: 7simul Flip Method</title><link>https://www.lyt0112.com/blog/clock-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/clock-zh</guid><description>7simul flip Method for Rubik&apos;s Clock</description><pubDate>Tue, 25 Jun 2024 08:15:04 GMT</pubDate><content:encoded>&lt;h2&gt;基础介绍和还原&lt;/h2&gt;
&lt;p&gt;这个视频说得又短又清楚: &lt;a href=&quot;https://www.bilibili.com/video/BV1pb4y177r4/?p=2&amp;#x26;share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;魔表基础还原教程&lt;/a&gt; .&lt;/p&gt;
&lt;h2&gt;7simul flip&lt;/h2&gt;
&lt;p&gt;其实本来是想写 &lt;a href=&quot;https://www.bilibili.com/video/BV13f421D7uw/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;Tommy&apos;s 7simul Method&lt;/a&gt; 的, 但是正当我要写的时候发现了同一个 UP 刚刚发布了更好的新方法.&lt;/p&gt;
&lt;h3&gt;观察记录&lt;/h3&gt;
&lt;p&gt;如果是 Tommy&apos;s 7simul Method , 可以将每一个表盘看作一个数字, 每一个箭头看作两个数字之间的向量, 这样更加直观.&lt;/p&gt;
&lt;p&gt;以如下打乱为例:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UR3- DR4- DL1- UL2- U1+ R4+ D2- L3- ALL1+ y2 U3- R1- D4- L4+ ALL1+ DR DL UL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;假设面对着你的那一面用大写字母, 背面用小写字母, 读码的时候翻面都使用 &lt;code&gt;x2&lt;/code&gt; 上下翻面, 而不是打乱时候的左右翻面, 数字字母编码可以用自己喜好的方式.&lt;/p&gt;
&lt;p&gt;这个例子使用打乱的时候朝上的那一面作为正面&lt;/p&gt;
&lt;p&gt;| 步骤 | 描述                                 | 数值大小 | 编码 |
| ---- | ------------------------------------ | -------- | ---- |
| 1    | $(R\rightarrow D)+(l\rightarrow ul)$ | 6        | 6    |
| 2    | $u\rightarrow c$                     | -3       | C    |
| 3    | $l\rightarrow u$                     | -5       | E    |
| 4    | $(r\rightarrow d)+(L\rightarrow UL)$ | -3       | C    |
| 5    | $U\rightarrow C$                     | 5        | 5    |
| 6    | $L\rightarrow U$                     | 4        | 4    |&lt;/p&gt;
&lt;h3&gt;操作&lt;/h3&gt;
&lt;p&gt;假设面向你的四根立柱分别是 &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;UR&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt; , 以下说明的都是哪些立柱突出.&lt;/p&gt;
&lt;p&gt;| 步骤 | 立柱突出         | &lt;code&gt;UL&lt;/code&gt; 轮拧多少                           | &lt;code&gt;UR&lt;/code&gt; 轮拧多少             |
| ---- | ---------------- | --------------------------------------- | ------------------------- |
| 1    | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt; | 步骤 1 的编码                           | 步骤 2 的编码             |
| 2    | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;       | 对齐 &lt;code&gt;D&lt;/code&gt; 和 &lt;code&gt;R&lt;/code&gt; 表盘                    | 步骤 3 的编码             |
| 3    | &lt;code&gt;UL&lt;/code&gt;             | 对齐 &lt;code&gt;C&lt;/code&gt; 和 &lt;code&gt;D&amp;#x26;R&lt;/code&gt; 表盘                  | 对齐 &lt;code&gt;DR&lt;/code&gt; 和 &lt;code&gt;D&amp;#x26;R&lt;/code&gt; 表盘   |
| 4    | &lt;code&gt;x2&lt;/code&gt; 翻面        | &lt;code&gt;x2&lt;/code&gt; 翻面                               | &lt;code&gt;x2&lt;/code&gt; 翻面                 |
| 5    | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt; | 步骤 4 的编码                           | 步骤 5 的编码             |
| 6    | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;       | 对齐 &lt;code&gt;D&lt;/code&gt; 和 &lt;code&gt;R&lt;/code&gt; 表盘                    | 步骤 6 的编码             |
| 7    | &lt;code&gt;UL&lt;/code&gt;             | 对齐左上的四个表盘和 &lt;code&gt;D&amp;#x26;R&lt;/code&gt; 表盘         | 对齐 &lt;code&gt;DR&lt;/code&gt; 和 &lt;code&gt;D&amp;#x26;R&lt;/code&gt; 表盘   |
| 8    | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;       | 除了  &lt;code&gt;DL&lt;/code&gt;, &lt;code&gt;UR&lt;/code&gt; , 让其余表盘对齐十二点 | &lt;code&gt;DL&lt;/code&gt;, &lt;code&gt;UR&lt;/code&gt; 表盘对齐十二点 |&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV17S421d7K2/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;不喜欢D轮？来学7simul flip吧&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV13f421D7uw/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;零基础带你学会魔表7simul法（Tommy版）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1pb4y177r4/?p=2&amp;#x26;share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;魔表基础还原教程&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/clock_cover.zli7n4gz.png"/><enclosure url="/_astro/clock_cover.zli7n4gz.png"/></item><item><title>Rubik&apos;s Clock: 7simul Flip Method</title><link>https://www.lyt0112.com/blog/clock-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/clock-en</guid><description>7simul flip Method for Rubik&apos;s Clock</description><pubDate>Tue, 25 Jun 2024 08:15:03 GMT</pubDate><content:encoded>&lt;h2&gt;7simul flip&lt;/h2&gt;
&lt;h3&gt;Observation&lt;/h3&gt;
&lt;p&gt;For example, with the following shuffle:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UR3- DR4- DL1- UL2- U1+ R4+ D2- L3- ALL1+ y2 U3- R1- D4- L4+ ALL1+ DR DL UL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Assume the side facing you uses uppercase letters, the back side uses lowercase letters, and when reading the code, flip it using &lt;code&gt;x2&lt;/code&gt; (flip up and down), not the left-right flip used during scrambling. You can use your preferred method for number and letter encoding.&lt;/p&gt;
&lt;p&gt;This example uses the side facing up during scrambling as the front.&lt;/p&gt;
&lt;p&gt;| Steps | Description                          | Numerical value | Encoding |
| ----- | ------------------------------------ | --------------- | -------- |
| 1     | $(R\rightarrow D)+(l\rightarrow ul)$ | 6               | 6        |
| 2     | $u\rightarrow c$                     | -3              | C        |
| 3     | $l\rightarrow u$                     | -5              | E        |
| 4     | $(r\rightarrow d)+(L\rightarrow UL)$ | -3              | C        |
| 5     | $U\rightarrow C$                     | 5               | 5        |
| 6     | $L\rightarrow U$                     | 4               | 4        |&lt;/p&gt;
&lt;h3&gt;Operation&lt;/h3&gt;
&lt;p&gt;Assuming the four pillars facing you are &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;UR&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;, the following descriptions indicate which pillars are protruding.&lt;/p&gt;
&lt;p&gt;| Steps | Protruding column | How many steps &lt;code&gt;UL&lt;/code&gt; turns                                              | How many steps &lt;code&gt;UR&lt;/code&gt; turns                 |
| ----- | ----------------- | ---------------------------------------------------------------------- | ----------------------------------------- |
| 1     | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;  | Encoding of Step 1                                                     | Encoding of step 2                        |
| 2     | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;        | Align &lt;code&gt;D&lt;/code&gt; and &lt;code&gt;R&lt;/code&gt; dials                                                | Encoding of Step 3                        |
| 3     | &lt;code&gt;UL&lt;/code&gt;              | Align &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;D&amp;#x26;R&lt;/code&gt; dials                                              | Align &lt;code&gt;DR&lt;/code&gt; and &lt;code&gt;D&amp;#x26;R&lt;/code&gt; dials                |
| 4     | &lt;code&gt;x2&lt;/code&gt; Flip over    | &lt;code&gt;x2&lt;/code&gt; Flip over                                                         | &lt;code&gt;x2&lt;/code&gt; Flip over                            |
| 5     | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;  | Encoding of step 4                                                     | Encoding of step 5                        |
| 6     | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DL&lt;/code&gt;        | Align &lt;code&gt;D&lt;/code&gt; and &lt;code&gt;R&lt;/code&gt; dials                                                | Encoding of step 6                        |
| 7     | &lt;code&gt;UL&lt;/code&gt;              | Align the top-left four dials and the &lt;code&gt;D&amp;#x26;R&lt;/code&gt; dial.                      | Align &lt;code&gt;DR&lt;/code&gt; and &lt;code&gt;D&amp;#x26;R&lt;/code&gt; dials                |
| 8     | &lt;code&gt;UL&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;        | Except for &lt;code&gt;DL&lt;/code&gt; and &lt;code&gt;UR&lt;/code&gt;, align the remaining dials to twelve o&apos;clock. | &lt;code&gt;DL&lt;/code&gt;, &lt;code&gt;UR&lt;/code&gt; dial aligned to twelve o&apos;clock |&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV17S421d7K2/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;不喜欢D轮？来学7simul flip吧&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV13f421D7uw/?share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;零基础带你学会魔表7simul法（Tommy版）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1pb4y177r4/?p=2&amp;#x26;share_source=copy_web&amp;#x26;vd_source=715b7965ee40cd347a349e6161f34dfc&quot;&gt;魔表基础还原教程&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/clock_cover.zli7n4gz.png"/><enclosure url="/_astro/clock_cover.zli7n4gz.png"/></item><item><title>Skewb: Optimized LBL Method</title><link>https://www.lyt0112.com/blog/skewb-zh</link><guid isPermaLink="true">https://www.lyt0112.com/blog/skewb-zh</guid><description>斜转魔方控心法教程, 包括FL, LL, 侧面中心还原</description><pubDate>Sun, 23 Jun 2024 08:20:21 GMT</pubDate><content:encoded>&lt;h2&gt;斜转复原方法整体概览&lt;/h2&gt;
&lt;h3&gt;普通SH叠加法 (LBL Method)&lt;/h3&gt;
&lt;p&gt;这部分的详细教程可以参考 &lt;a href=&quot;https://www.gancube.com/zh/gancube-tutorials-skewb/&quot;&gt;Gan&apos;s Skewb tutorial&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;控心法 (Optimized LBL Method)&lt;/h3&gt;
&lt;p&gt;顶面控心: 在普通SH叠加法的基础上, 完成顶角的同时把顶面中心归位.&lt;/p&gt;
&lt;p&gt;多向控心: 在顶面空心的基础上, 避免出现四心换, 或者跳过三心换. 其实多向控心并不一定要控顶面中心, 当你可以两个对位侧面中心归位时, 剩下的顶多也就是一个包含顶面的三心换.&lt;/p&gt;
&lt;h3&gt;二步法 (L2L Method)&lt;/h3&gt;
&lt;p&gt;二步法是更高级的方法, 但是有些case的解法不如叠加, 比如 zbll 中有的小鱼情况的 zbll 还不如直接做OP.&lt;/p&gt;
&lt;p&gt;叠加二步法 (L2L, Last Two Layers): 完成底面后, 至多需要五个S或H即可还原斜转魔方, 记住每种情况所对应的SH叠加和其中的转体, 一气呵成还原魔方. 总共 134 个公式, 可以参考 &lt;a href=&quot;https://www.cuberoot.me/skewb-l2l-by-learning-order/&quot;&gt;Skewb L2L Method&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NS法: 对叠加二步法的补充, 叠加二步法中, 当出现需要四个或者五个SH叠加才能完成的情况时, 继续使用叠加二步法步骤太多, 耗时太长, 于是产生NS法对叠加二步法中繁琐的情况发明了新的公式进行补充.&lt;/p&gt;
&lt;p&gt;KK法: 和NS法一样, 不过不好用, 渐渐被淘汰了.&lt;/p&gt;
&lt;h3&gt;全预判&lt;/h3&gt;
&lt;p&gt;推演出还原一面后出现的情况, 然后通过二步法还原斜转魔方, 中间不需要任何停顿和思考.&lt;/p&gt;
&lt;h2&gt;记号说明&lt;/h2&gt;
&lt;h3&gt;Basic notations&lt;/h3&gt;
&lt;p&gt;转动方式和三阶相同, 看向哪个面的时候对那个面做顺时针旋转就是那个角的记号 (没有撇) .&lt;/p&gt;
&lt;h3&gt;H&amp;#x26;S notations&lt;/h3&gt;
&lt;p&gt;S (Sledge) 和 H (Hedge) 的名字最开始起源于三阶的术语, 但是在三阶中逐渐淘汰却被发现在 Skewb 上很合适, 于是被发扬光大&lt;/p&gt;
&lt;h2&gt;控心法教程&lt;/h2&gt;
&lt;h3&gt;FL (First Layer)&lt;/h3&gt;
&lt;p&gt;这一步需要复原底层角块和底层中心.&lt;/p&gt;
&lt;p&gt;总的来说跟三阶的底层十字一样, 需要多多练习, 在你更熟悉Skewb是如何旋转后, 这一步将轻易完成.&lt;/p&gt;
&lt;p&gt;不过在最初几次复原中, 对于最后一个角你或许会碰上麻烦. 很有可能的是你为了做这个角却破坏了一个已经好了的, 这时有两种情况可用公式处理&lt;/p&gt;
&lt;h3&gt;LL (Last Layer)&lt;/h3&gt;
&lt;p&gt;这一步需要同时复原顶层角块和顶层中心.&lt;/p&gt;
&lt;p&gt;总计十种小情况, 公式如下.&lt;/p&gt;
&lt;h3&gt;Side Centers&lt;/h3&gt;
&lt;p&gt;这一步需要复原侧面的中心块, 和初级方法相同.&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cuberoot.me/&quot;&gt;Cube Root&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;李宗阳 &lt;a href=&quot;https://www.bilibili.com/video/BV1Tp4y1e74q/&quot;&gt;控心法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gancube.com/zh/gancube-tutorials-skewb/&quot;&gt;Gan&apos;s Skewb tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/421944291&quot;&gt;斜转魔方: Skewb速拧法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/skewb_cover.DfR1oFly.png"/><enclosure url="/_astro/skewb_cover.DfR1oFly.png"/></item><item><title>Skewb: Optimized LBL Method</title><link>https://www.lyt0112.com/blog/skewb-en</link><guid isPermaLink="true">https://www.lyt0112.com/blog/skewb-en</guid><description>Skewb Optimized LBL Method, including FL, LL, side center restoration</description><pubDate>Sun, 23 Jun 2024 08:20:20 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;h3&gt;LBL Method&lt;/h3&gt;
&lt;p&gt;The detailed tutorial for this part can be referred to &lt;a href=&quot;https://www.gancube.com/zh/gancube-tutorials-skewb/&quot;&gt;Gan&apos;s Skewb tutorial&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Optimized LBL Method&lt;/h3&gt;
&lt;p&gt;Optimized LBL Method: Based on the LBL Method, complete the top corners while returning the top face center to its position.&lt;/p&gt;
&lt;p&gt;Multi-directional LBL Method: Based on Optimized LBL Method, avoid four-central-block swaps or skip three-central-block swaps. In fact, Multi-directional LBL Method does not necessarily require controlling the top surface center. When you can align two side centers, the remaining will at most be a three-central-block swap involving the top surface.&lt;/p&gt;
&lt;h3&gt;L2L Method&lt;/h3&gt;
&lt;p&gt;The two-step method is a more advanced method, but in some cases, the solution is not as good as LBL Method. For example, in some OLL cases in ZBLL, it is better to directly do OLL itself.&lt;/p&gt;
&lt;p&gt;L2L, Last Two Layers: After completing the bottom face, at most five S or H moves are needed to restore the skewb. Remember the SH combinations and the rotations corresponding to each situation, and restore the cube in one go.
A total of 134 formulas can be referenced. &lt;a href=&quot;https://www.cuberoot.me/skewb-l2l-by-learning-order/&quot;&gt;Skewb L2L Method&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;NS Method: A supplement to the L2L Method. In the L2L Method, when it requires four or five SH superpositions to complete, continuing to use the L2L Method involves too many steps and takes too long. Therefore, the NS Method was created to supplement the cumbersome situations in the L2L Method with new formulas.&lt;/p&gt;
&lt;p&gt;KK method: Same as the NS method, but not very useful, gradually phased out.&lt;/p&gt;
&lt;h3&gt;Full prediction&lt;/h3&gt;
&lt;p&gt;Deduce the situation after restoring one side, then restore the skewb cube using the two-step method without any pauses or thinking in between.&lt;/p&gt;
&lt;h2&gt;Symbol Description&lt;/h2&gt;
&lt;h3&gt;Basic notations&lt;/h3&gt;
&lt;p&gt;The rotation method is the same as the third order, when looking at a face, a clockwise rotation on that face is the notation for that corner (without an apostrophe).&lt;/p&gt;
&lt;h3&gt;H&amp;#x26;S notations&lt;/h3&gt;
&lt;p&gt;The names S (Sledge) and H (Hedge) originally came from the terminology of the 3x3 cube, but as they were gradually phased out in the 3x3, they were found to be very suitable for the Skewb, and thus were popularized.&lt;/p&gt;
&lt;h2&gt;Optimized LBL Method&lt;/h2&gt;
&lt;h3&gt;FL (First Layer)&lt;/h3&gt;
&lt;p&gt;This step requires restoring the bottom corner pieces and the bottom center.&lt;/p&gt;
&lt;p&gt;Overall, it is similar to the bottom cross of a 3x3 cube, requiring a lot of practice. Once you become more familiar with how the Skewb rotates, this step will be easily completed.&lt;/p&gt;
&lt;p&gt;However, during the first few solves, you might encounter trouble with the last corner. It is very likely that while trying to solve this corner, you might disrupt an already solved one. At this point, there are two situations that can be handled with algorithms.&lt;/p&gt;
&lt;h3&gt;LL (Last Layer)&lt;/h3&gt;
&lt;p&gt;This step requires simultaneously restoring the top layer corner pieces and the top layer center.&lt;/p&gt;
&lt;p&gt;There are a total of ten small cases, the formulas are as follows.&lt;/p&gt;
&lt;h3&gt;Side Centers&lt;/h3&gt;
&lt;p&gt;This step requires restoring the center pieces on the sides, similar to the beginner&apos;s method.&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cuberoot.me/&quot;&gt;Cube Root&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;李宗阳 &lt;a href=&quot;https://www.bilibili.com/video/BV1Tp4y1e74q/&quot;&gt;控心法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gancube.com/zh/gancube-tutorials-skewb/&quot;&gt;Gan&apos;s Skewb tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/421944291&quot;&gt;斜转魔方: Skewb速拧法&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/skewb_cover.DfR1oFly.png"/><enclosure url="/_astro/skewb_cover.DfR1oFly.png"/></item><item><title>I don&apos;t like S14E08</title><link>https://www.lyt0112.com/blog/dw2</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw2</guid><description>有钱是真有钱了, 但是 RTD 有点像在逗我</description><pubDate>Sat, 22 Jun 2024 20:16:05 GMT</pubDate><content:encoded>&lt;p&gt;RTD 写的过于唯心主义了&lt;/p&gt;
&lt;p&gt;看了看 &lt;a href=&quot;https://tieba.baidu.com/p/9061254728&quot;&gt;讨论贴&lt;/a&gt; 基本上贬大于褒, 参考一些讨论总结一下&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;这一季 Kate 的存在感足够多, 很帅很😍.&lt;/li&gt;
&lt;li&gt;还有 Sarah Jane 的一个反派 Trickster出现, 有点唤起了六年之前的暑假的回忆.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;像遛狗一样就遛死 Sutekh 了??? 有点草率了感觉.&lt;/li&gt;
&lt;li&gt;Ruby 是一个 human 也不是不行, 试图跳出俗套也挺好的, 那凭什么就下雪了❄️还有圣诞颂歌, 毫无逻辑, 真就是因为 “We invest things with significance.” 呗.&lt;/li&gt;
&lt;li&gt;为什么 Goblin 会管 Ruby 叫 “The Beast” , 难绷.&lt;/li&gt;
&lt;li&gt;之前Ruby寻亲的时候没找到亲生父母还可以勉强解释成他父母的基因都没入寻亲机构的基因库. 但是既然在2046年战争首相就强制采集DNA了, 那在遥远的未来, 救护机为什么检测不到Ruby亲生父母的信息呢.&lt;/li&gt;
&lt;li&gt;那个勺子桥段也太离谱了, 不懂意义在哪里, 摸不着头脑, 如果是为了搞笑为啥那么严肃... 看看之前的勺子都是怎么做的, 比如 12 的 Robin hood 那一集还挺好玩的.&lt;/li&gt;
&lt;/ol&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;73 yard 明明是 Moffat 在桥边为了让人恰好看不清脸随便找的一个距离, 怎么就恰巧跟T娘的设定一样了.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Wait and see&lt;/h2&gt;
&lt;p&gt;尚未揭晓的伏笔还很多&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mrs.Flood是谁? 能打破次元壁与观众互动、知道T娘、知道Ruby会是博士的同伴、知道苏泰克, 或许还有其他神、衣着模仿克拉拉&amp;#x26;Romana I, 还会说“clever boy”, 所以Mrs. Flood到底是谁? 从本集她谈论造物主和结尾的那段话, 感觉不太像是克拉拉、Susan和Romana (她们仨应该不会用讲故事的语气和表情说15的结局会很惨吧) ... 会是Missy吗? 还是Rani?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在翻贴吧的时候发现 Romana II 美美美&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;gravity变成mavity, 60周年特辑的这个伏笔到现在也没有任何揭晓的迹象. 考虑到“ma-”这个字头和&quot;magic&quot;, &quot;marvel&quot;相似，而且牛顿在物理学的奠基地位, 这个伏笔似乎是在说世界由科学转向魔法. 这是个最基础的伏笔,至少要到第三季结尾,才会真正扭转过来, 考虑到 RTD 确实喜欢埋多季的伏笔, 这样也不是不行.&lt;/li&gt;
&lt;li&gt;撒盐对世界的影响，这和第一条是类似的, 都加重了这个世界的魔法性.&lt;/li&gt;
&lt;li&gt;Ruby的超能力: 下雪, Maestro说这个能力来源于她内心深处隐藏的一首歌, 而Maestro无法将其吸出, 所以Ruby的谜题并未全部揭晓.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Next&lt;/h2&gt;
&lt;p&gt;魔法特将于2024年圣诞特辑《普世欢腾》(Joy to the World）回归:&lt;/p&gt;
&lt;p&gt;“我不能拒绝, 我喜欢写圣诞特辑, 我想过 ‘写过《Boom》了, 不需要再写了’ 然后RTD给我发邮件 ‘你想要写圣诞特辑吗?’ 我回他‘好的, 必须写’ 每写一个故事都可能是我最后一个故事, 我总是想发掘一些东西, 有时圣诞特辑似乎比其它常规集更以博士为核心. 我真的不能多说, 圣诞礼物要留在圣诞节拆开.”&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.digitalspy.com/tv/a60835258/doctor-who-varada-sethu-surprise-appearance&quot;&gt;Doctor Who&apos;s Steven Moffat addresses surprise early Varada Sethu appearance&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://screenrant.com/doctor-who-season-14-ncuti-gatwa-modern-doctors-interaction-steven-moffat/&quot;&gt;How The New Doctor Would Interact With Other Modern Doctors Revealed By Past Doctor Who Showrunner&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://screenrant.com/doctor-who-season-14-steven-moffat-interview/&quot;&gt;Steven Moffat Explains What Makes &quot;Boom&quot; A Defining Doctor Who Episode For Ncuti Gatwa&apos;s Doctor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.tvinsider.com/1136487/doctor-who-boom-varada-sethu-mundy-susan-twist-characters-steven-moffat&quot;&gt;‘Doctor Who’: Steven Moffat Talks Introducing Spoiler &amp;#x26; What Wasn’t in First Draft of ‘Boom’ Script&lt;/a&gt;&lt;/p&gt;</content:encoded><h:img src="/_astro/dw-s14.Dt39ouu8.jpg"/><enclosure url="/_astro/dw-s14.Dt39ouu8.jpg"/></item><item><title>Doctor Who S14 24hrs to go!</title><link>https://www.lyt0112.com/blog/dw1</link><guid isPermaLink="true">https://www.lyt0112.com/blog/dw1</guid><description>Woohoo! Here we go!</description><pubDate>Thu, 02 May 2024 20:15:26 GMT</pubDate><content:encoded>&lt;p&gt;Ready to rock through time!&lt;/p&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;p&gt;本季是自1963年老版开播至今的第40季，是2005年新版复播软重启至今的第14季，也是BBC和Disney+在2023年四部特辑再度软重启全新开启的第1季。日常怎么称呼都可以（比如把1季称呼为14季），只需了解BBC官方定义和运作人RTD专访上《神秘博士》电视剧历史阶段划分为三个时代。综合BBC官方、运作人RTD、外网胡粉、国内胡粉对三个时代的称谓，总结如下（为了避免大家问名单里譬如“怎么没有战争博士和逃犯博士”这种问题，我在其他博士写明他们，只出现在官方杂志、或运作人采访譬如月亮博士、以及老版的养蜂人，因未在银幕中证明身份的，只是运作人嘴上说说，却没在剧里呈现的，我就不加入里面了）&lt;/p&gt;
&lt;h3&gt;一、经典系列老版（BBC）&lt;/h3&gt;
&lt;p&gt;1963年至1996年
（1963年至1989年共26季和1996年电影）&lt;/p&gt;
&lt;p&gt;主演:  1任至8任博士
其他博士: 莫比乌斯博士（1任之前）、Valeyard（无法重生后博士具象化黑暗面）、梅林（未来博士）&lt;/p&gt;
&lt;h3&gt;二、复活系列05新版（BBC）&lt;/h3&gt;
&lt;p&gt;2005年至2022年（共13季）&lt;/p&gt;
&lt;p&gt;主演: 9任至13任博士
其他博士: 战争博士（8任和9任之间时间大战时期）、馆长/策展人(博士退休后可自由更改过去最爱面孔的老年形态)、逃犯博士（1任之前）&lt;/p&gt;
&lt;h3&gt;三、复活系列23新版（BBC &amp;#x26; Disney+）&lt;/h3&gt;
&lt;p&gt;2023年至今&lt;/p&gt;
&lt;p&gt;主演: 14任（结局为博士退休）/15任博士至今
其他博士: 老年5任博士（双重生存活至今），老年6任博士（双重生存活至今），老年7任博士（双重生存活至今）&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;p&gt;https://tieba.baidu.com/p/9010746968&lt;/p&gt;</content:encoded><h:img src="/_astro/tardis-clara.DKqFUOna.jpeg"/><enclosure url="/_astro/tardis-clara.DKqFUOna.jpeg"/></item><item><title>Hello World!</title><link>https://www.lyt0112.com/blog/hello_world</link><guid isPermaLink="true">https://www.lyt0112.com/blog/hello_world</guid><description>Hello dear someone in the world, nice to meet you from the beginning of the story!</description><pubDate>Wed, 01 May 2024 09:13:43 GMT</pubDate><content:encoded>&lt;h2&gt;Hi there!&lt;/h2&gt;
&lt;p&gt;Hello dear someone in the world, hello from the beginning of the story!&lt;/p&gt;
&lt;h2&gt;My blogs&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;My first website theme was created on May 1, 2024, followed the style of &lt;a href=&quot;https://github.com/shiro/blog&quot;&gt;Shiro&lt;/a&gt;, and was stopped at July 28, 2024.&lt;/li&gt;
&lt;li&gt;The second website theme was created on July 28, 2024, and is still in use.&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/thumbnail-test-4.H3t_xmcX.jpg"/><enclosure url="/_astro/thumbnail-test-4.H3t_xmcX.jpg"/></item></channel></rss>