<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>WillMo</title>
  
  
  <link href="https://blog.willmo.top/atom.xml" rel="self"/>
  
  <link href="https://blog.willmo.top/"/>
  <updated>2026-05-08T11:18:18.283Z</updated>
  <id>https://blog.willmo.top/</id>
  
  <author>
    <name>WillMo</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>RocketMQ消息队列</title>
    <link href="https://blog.willmo.top/posts/RocketMQ/"/>
    <id>https://blog.willmo.top/posts/RocketMQ/</id>
    <published>2026-05-08T07:09:18.421Z</published>
    <updated>2026-05-08T11:18:18.283Z</updated>
    
    <content type="html"><![CDATA[<h1 id="RocketMQ消息队列"><a href="#RocketMQ消息队列" class="headerlink" title="RocketMQ消息队列"></a>RocketMQ消息队列</h1><h2 id="消息队列"><a href="#消息队列" class="headerlink" title="消息队列"></a>消息队列</h2><p>MQ（全称Message Queue）是一种进程间通信或同一进程的不同线程间的通信方式，队列就是一个消息容器。</p><p>现实使用中，我们将消息队列称之为<strong>中间件</strong>，从它的名字就可以看出，消息队列不存储消息内容的本身，它只是消息的搬运工。</p><h3 id="RPC-通信模型"><a href="#RPC-通信模型" class="headerlink" title="RPC 通信模型"></a>RPC 通信模型</h3><p><img src="https://files.seeusercontent.com/2026/05/08/oj1O/image-20260504142359125.png" alt="image-20260504142359125"></p><ul><li>句柄：直接告知进程内存地址具有安全隐患，所以操作系统弄了一张表，把真实地址藏起来，只给你一个<strong>不能直接读写、只能用于系统调用的整数</strong>，这就是句柄。</li><li>序列化：将<strong>内存中的数据结构或对象</strong>，转换成<strong>可以存储或传输的字节流</strong>的过程</li></ul><p>RPC调用步骤<br>1、建立通信：消费者要想调用提供者的方法，首先要和提供者建立通信连接，主要是通过客户端和服务器之间建立TCP连接实现的<br>2、服务寻址：寻址实现的是确定提供者的IP、端口号以及方法的名称<br>3、网络传输：消费者发起一个RPC调用时，将调用方法和参数的数据进行序列化传输给提供者，提供者将接收到的参数进行反序列化操作后执行方法<br>4、服务调用：提供者进行本地调用后得到了返回值，提供者将返回值进行序列化操作后，再通过网络传输将二进制数据发送回给消费者<br><em><strong>从本质上看，RPC一般对于客户端来说是一种同步的远程服务调用技术，而一般来说MQ是异步的远程调用</strong></em></p><p>MQ的实质就是<strong>一发一存一消费</strong>，这也就是为什么叫做中间件。</p><p>MQ有多种模型：</p><ul><li>队列模型，点对点通信，单播：</li></ul><p><img src="https://files.seeusercontent.com/2026/05/08/1Kiq/image-20260504143309224.png" alt="image-20260504143309224"></p><ul><li><strong>发布订阅模式（PubSub）</strong>：一个生产者发送的每一个消息，都会发送到所有订阅了此队列的消费者，也就是<strong>广播</strong></li></ul><p><img src="https://files.seeusercontent.com/2026/05/08/5djG/image-20260504143331173.png" alt="image-20260504143331173"></p><p>存放消息的容器变成了主题（主题也是一个队列），订阅者在接收消息之前需要先订阅主题，最终，每个订阅者都可以收到同一个主题的全量消息，可以看到和队列模式唯一不同的就是，发布订阅模式中的<strong>一份消息数据可以被多次消费</strong></p><p>MQ被用于操作解耦，比如，如果没有MQ那就：</p><p><img src="https://files.seeusercontent.com/2026/05/08/jdQ7/image-20260504143533443.png" alt="image-20260504143533443"></p><p>只能一步步做的，但是引入MQ之后就可以：</p><p><img src="https://files.seeusercontent.com/2026/05/08/O9gj/image-20260504143552905.png" alt="image-20260504143552905"></p><p>那么这三个就能够摆脱低效的串行而转为异步并行。</p><p>还能做到应用解耦，如果没有MQ：</p><p><img src="https://files.seeusercontent.com/2026/05/08/Z5uv/image-20260504143847193.png" alt="image-20260504143847193"></p><p>任一挂了另一系统就无法工作，但是如果有MQ，只要MQ健在，MQ就能够接受和转发其他系统的信息。</p><h4 id="流量削峰"><a href="#流量削峰" class="headerlink" title="流量削峰"></a>流量削峰</h4><p><strong>场景</strong>： 秒杀或者团抢活动，一般因为流量过大，导致流量暴增，应用挂掉</p><p>MQ使得做奶茶的人和前台分离，使得前台一心应付顾客并生成单号，后台再根据单号稳定慢慢做奶茶。</p><h2 id="RocketMQ"><a href="#RocketMQ" class="headerlink" title="RocketMQ"></a>RocketMQ</h2><blockquote><p>2026年5月8日14:26:54，一直以来，我可能对消息队列RocketMQ类工具的学习认知产生了偏差，这不是开发的一部分，他是一个工具，和Docker一样的工具，也是一个基础设施，这和开发逻辑没有多大关系。所以，没必要在win+idea上反复纠缠，最好是在Docker中运行，因为最后这些代码是要放到Linux里面跑的，嗯，这是一次很重要的反思。但是不必担心，人类在曲折中前进。</p></blockquote><h3 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h3><ul><li><strong>Message（消息）</strong>：Message 是 <strong>RocketMQ</strong> 传输的基本单元，包含了具体的业务数据以及一些元数据（如消息 ID、主题、标签、发送时间等）。消息可以是文本、二进制数据或其他任何序列化后的对象形式。</li><li><strong>Topic（主题）</strong>：Topic 是一类消息的逻辑分类名，是 Apache RocketMQ  中消息传输和存储的顶层容器。类似于邮件系统中的邮箱地址或发布/订阅模式中的“频道”。生产者向特定的 Topic 发送消息，消费者则根据  Topic 订阅并接收消息。一个 Topic 可以被多个生产者写入，同时也能被多个消费者订阅。</li><li><strong>Queue（队列）</strong>：每个 Topic 被划分为多个 Queue（队列），或称 MessageQueue，这些队列用于存储消息。生产者发送到 Topic 的消息会被分配到其下的各个 Queue 中；消费者则是从这些 Queue 中拉取消息进行消费。</li><li><strong>Subscription（订阅）</strong>：Subscription 表示消费者对某个 Topic 消息的兴趣表达。订阅关系由消费者分组动态注册到服务端系统，并在消息传输中按照订阅关系定义的过滤规则进行消息匹配和消费进度的维护。</li><li><strong>Producer（生产者）</strong>：生产者是消息产生的源头，将消息发送到服务端指定 Topic。</li><li><strong>Consumer（消费者）</strong>：消费者负责从服务端中拉取消息并进行处理。</li><li><strong>ProducerGroup（生产者组）</strong>：ProducerGroup 是一组生产者的逻辑分组，共享同样的 Topic 发送配置，实现发送端的负载均衡和容错。如果组内某个生产者失败，其他生产者可以继续工作，保证消息发送的连续性。</li><li><strong>ConsumerGroup（消费者组）</strong>：消费者分组是 Apache RocketMQ  系统中承载多个消费行为一致的消费者的负载均衡分组。和消费者不同，消费者分组并不是运行实体，而是一个逻辑资源。分组中的消费者共同订阅同一个  Topic 并以某种策略（如广播、集群消费）消费消息。在 Apache RocketMQ  中，通过消费者分组内初始化多个消费者实现消费性能的水平扩展以及高可用容灾。</li></ul><h3 id="架构分析"><a href="#架构分析" class="headerlink" title="架构分析"></a>架构分析</h3><p><strong>Apache RocketMQ</strong> 服务端基础组件包括 <strong>NameServer，Broker，Proxy</strong>，推荐使用存储计算分离模式部署。</p><table><thead><tr><th>组件</th><th>核心职责</th><th>类比理解</th></tr></thead><tbody><tr><td><strong>NameServer</strong></td><td><strong>服务发现与路由中心</strong>。轻量级，管理 Broker 的元数据（IP、存活状态、Topic 队列信息）。客户端启动时先问它：“我想发消息，该找哪个 Broker？”</td><td>像<strong>导航地图</strong>。不运货，但告诉你货车（Broker）在哪里。</td></tr><tr><td><strong>Broker</strong></td><td><strong>消息存储与转发核心</strong>。真正负责消息的写入、读取、持久化、HA 主从同步。处理客户端具体的收发请求。</td><td>像<strong>快递中转仓</strong>。实际保管包裹（消息），处理存和取。</td></tr><tr><td><strong>Proxy</strong></td><td><strong>接入网关 / 协议适配层</strong>（<strong>RocketMQ 5.0</strong> 新增）。接收客户端请求后，转发给 Broker，可做协议转换（GRPC/HTTP）、负载均衡、无状态易扩展。</td><td>像<strong>快递柜或前台</strong>。客户不直接找仓库，而是通过统一入口交接。部分场景可代替直连 NameServer。</td></tr></tbody></table><p><img src="https://files.seeusercontent.com/2026/05/08/l0Tj/O1CN01aD4VOe1TB8xPi5xcg_60000000.jpg" alt="O1CN01aD4VOe1TB8xPi5xcg_!!6000000002343-0-tps-2084-766"></p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>将使用Docker模拟服务器环境安装，我不会再去找什么Win特供版本了。</p><p>创建一个<code>docker-compose.yml</code>文件，其中写入：</p><figure class="highlight dockerfile"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs dockerfile">version: <span class="hljs-string">'3.8'</span><br><br>services:<br>  <span class="hljs-comment"># NameServer 服务</span><br>  namesrv:<br>    image: apache/rocketmq:<span class="hljs-number">5.2</span>.<span class="hljs-number">0</span><br>    container_name: rocketmq-namesrv<br>    restart: always<br>    ports:<br>      - <span class="hljs-string">"9876:9876"</span><br>    environment:<br>      - JAVA_OPT_EXT=-server -Xms256m -Xmx256m -Xmn128m<br>    volumes:<br>      - ./data/namesrv/logs:/home/rocketmq/logs<br>      - ./data/namesrv/store:/home/rocketmq/store<br>    command: sh mqnamesrv<br><br>  <span class="hljs-comment"># Broker 服务</span><br>  broker:<br>    image: apache/rocketmq:<span class="hljs-number">5.2</span>.<span class="hljs-number">0</span><br>    container_name: rocketmq-broker<br>    restart: always<br>    hostname: broker                        <span class="hljs-comment"># 确保容器主机名就是 broker</span><br>    depends_on:<br>      - namesrv<br>    ports:<br>      - <span class="hljs-string">"10909:10909"</span>   <span class="hljs-comment"># SSL 端口（不强制启用，可保留）</span><br>      - <span class="hljs-string">"10911:10911"</span>   <span class="hljs-comment"># Broker 主端口</span><br>      - <span class="hljs-string">"10912:10912"</span>   <span class="hljs-comment"># HA 端口</span><br>    environment:<br>      - JAVA_OPT_EXT=-server -Xms256m -Xmx256m -Xmn128m<br>    volumes:<br>      - ./data/broker/logs:/home/rocketmq/logs<br>      - ./data/broker/store:/home/rocketmq/store<br>      - ./broker.conf:/opt/rocketmq/conf/broker.conf<br>    command: sh mqbroker -c /opt/rocketmq/conf/broker.conf<br><br>  <span class="hljs-comment"># RocketMQ 控制台</span><br>  console:<br>    image: apacherocketmq/rocketmq-dashboard:latest<br>    container_name: rocketmq-console<br>    restart: always<br>    depends_on:<br>      - namesrv<br>    ports:<br>      - <span class="hljs-string">"8080:8082"</span><br>    environment:<br>      - JAVA_OPTS=-Drocketmq.namesrv.addr=namesrv:<span class="hljs-number">9876</span><br></code></pre></td></tr></tbody></table></figure><p>再创建一个文件名为<code>broker.conf</code>配置broker的链接：</p><figure class="highlight yaml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-string">brokerClusterName</span> <span class="hljs-string">=</span> <span class="hljs-string">DefaultCluster</span><br><span class="hljs-string">brokerName</span> <span class="hljs-string">=</span> <span class="hljs-string">broker-a</span><br><span class="hljs-string">brokerId</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span><br><span class="hljs-string">deleteWhen</span> <span class="hljs-string">=</span> <span class="hljs-number">04</span><br><span class="hljs-string">fileReservedTime</span> <span class="hljs-string">=</span> <span class="hljs-number">48</span><br><span class="hljs-string">brokerRole</span> <span class="hljs-string">=</span> <span class="hljs-string">ASYNC_MASTER</span><br><span class="hljs-string">flushDiskType</span> <span class="hljs-string">=</span> <span class="hljs-string">ASYNC_FLUSH</span><br><br><span class="hljs-comment"># NameServer 地址（容器内部使用服务名）</span><br><span class="hljs-string">namesrvAddr</span> <span class="hljs-string">=</span> <span class="hljs-string">namesrv:9876</span><br><br><span class="hljs-comment"># 自动向 NameServer 注册的 IP，必须设为服务名或可被其他容器解析的地址</span><br><span class="hljs-string">brokerIP1</span> <span class="hljs-string">=</span> <span class="hljs-string">broker</span><br></code></pre></td></tr></tbody></table></figure><p>随后用<code>docker-compose up -d</code>自动拉取镜像和创建容器。</p><p>这会创建一个包含了<strong>Broker，NameServer和Proxy</strong>的<strong>MQ集群</strong>，你能够看到三个容器，这三个容器已经在运行中了，您不需要做任何操作。</p><p><img src="https://files.seeusercontent.com/2026/05/06/7pgL/image-20260506112757979.png" alt="image-20260506112757979"></p><p><strong>9876 端口（NameServer）</strong> 扮演着“中枢神经”的角色，负责存储和分发路由信息。</p><p><strong>Broker 需要上报路由信息</strong></p><p><strong>Console 需要获取集群状态</strong>所以会被这两个访问。</p><p>10911 端口：主通信与业务端口，负责在默认模式下承接Broker的大部分工作，包括生产者生产和消费者拉取。<strong>一个 TCP 端口完全可以展开双向工作，因为它背后建立的是一条“全双工”的通信通道</strong></p><p>console是控制台，能够通过访问<code>localhost:8080</code>来访问控制台：</p><p><img src="https://files.seeusercontent.com/2026/05/06/1rhK/image-20260506112822627.png" alt="image-20260506112822627"></p><h3 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h3><p>进入web端，在主题处添加新的：</p><p><img src="https://files.seeusercontent.com/2026/05/06/B8ix/image-20260506113128503.png" alt="image-20260506113128503"></p><p>随后发送一条消息：</p><p><img src="https://files.seeusercontent.com/2026/05/06/g3Ni/image-20260506113727513.png" alt="image-20260506113727513"></p><p>这就是发送成功了，而且也能够在<strong>消息详情</strong>中查询到：<img src="https://files.seeusercontent.com/2026/05/06/my8D/image-20260506114256318.png" alt="image-20260506114256318"></p><p>那么测试完毕，集群配置正确。</p><h3 id="Java接入RocketMQ"><a href="#Java接入RocketMQ" class="headerlink" title="Java接入RocketMQ"></a>Java接入RocketMQ</h3><p>RocketMQ，是一个<strong>独立设施</strong>，但是我们部署他是为了在开发中使用它以优化性能。</p><p>在<strong>Maven</strong>中引入RocketMQ依赖：</p><figure class="highlight xml"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">dependencies</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>org.apache.rocketmq<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>rocketmq-client<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>5.2.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">dependencies</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure><p>随后用JAVA编写生产者：</p><p>消息生产者分别通过三种方式发送消息：</p><ul><li>同步发送：等待消息返回后再继续进行下面的操作。</li><li>异步发送：不等待消息返回直接进入后续流程。broker将结果返回后调用callback函数，并使用CountDownLatch计数。</li><li>单向发送：只负责发送，不管消息是否发送成功。</li></ul><p>这里只编写同步发送，引入依赖后新建JAVA类Procduer:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 同步发送</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Producer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"SyncProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">2</span>; i++) {<br>            <span class="hljs-type">Message</span> <span class="hljs-variable">msg</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"test_topic"</span>, <span class="hljs-comment">//上面创建的测试主题</span><br>                    <span class="hljs-string">"TagA"</span>,  <span class="hljs-comment">//设置消息Tag，用于消费端根据指定Tag过滤消息。</span><br>                    <span class="hljs-string">"Simple-Sync"</span>.getBytes(StandardCharsets.UTF_8) <span class="hljs-comment">//消息体。</span><br>            );<br>            <span class="hljs-type">SendResult</span> <span class="hljs-variable">send</span> <span class="hljs-operator">=</span> producer.send(msg);<br>            System.out.printf(i + <span class="hljs-string">".发送消息成功：%s%n"</span>, send);<br>        }<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>注意，这里的端口是Docker对外映射的端口，IP就写本地127.0.0.1就可以了，因为容器和客户端都在一台PC上。</p><p>但是你仍有可能连接不上，因为Docker设置的<code>127.0.0.1</code> 是 <strong>回环地址</strong>，在宿主机上指向宿主机自己，在容器内部指向容器自己。在本例中，MQ运作流程是：</p><p><strong>连接 NameServer</strong> → 宿主机 <code>127.0.0.1:9876</code> 通过 Docker 端口映射，到达容器内的 NameServer。</p><p><strong>获取 Topic 路由</strong> → NameServer 返回 Broker 的地址，这个地址是 <strong>Broker 在启动时注册</strong> 的。</p><p><strong>连接 Broker</strong> → 客户端拿到的 Broker 地址去连接，发送消息。</p><p>但是请回忆，<strong>Broker.conf是在docker容器里面写就的</strong>，也就是说客户端拿到Broker这个字符串根本不知道它指向哪里所以无法连接。</p><p>如果使用局域网IP，那么因为局域网IP是容器和宿主机共用的，那么数据就能够在网卡中回环，宿主机就能够连接上容器，且同一局域网的机器也能够连接上Docker容器。</p><p>所以你如果遇到超时请在host文件里加一行<code>127.0.0.1   broker</code>，来让客户端认识他，知道Broker就是127.0.0.1，需要管理员权限。</p><p>或者修改Broker.conf，将IP地址定位<strong>局域网IP地址</strong>。</p><p>随后是消费者：</p><p>消费者消费消息分两种：</p><ul><li>拉模式：消费者主动去Broker上拉取消息。</li><li>推模式：消费者等待Broker把消息推送过来。</li></ul><p>随后新建Java类Consumer，这里只介绍推模式:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.MessageExt;<br><br><span class="hljs-keyword">import</span> java.util.List;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 推模式</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Consumer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException {<br>        <span class="hljs-type">DefaultMQPushConsumer</span> <span class="hljs-variable">pushConsumer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQPushConsumer</span>(<span class="hljs-string">"SimplePushConsumer"</span>);<br>        pushConsumer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        pushConsumer.subscribe(<span class="hljs-string">"test_topic"</span>,<span class="hljs-string">"*"</span>);<br>        pushConsumer.setMessageListener(<span class="hljs-keyword">new</span> <span class="hljs-title class_">MessageListenerConcurrently</span>() {<br>            <span class="hljs-meta">@Override</span><br>            <span class="hljs-keyword">public</span> ConsumeConcurrentlyStatus <span class="hljs-title function_">consumeMessage</span><span class="hljs-params">(List&lt;MessageExt&gt; list, ConsumeConcurrentlyContext consumeConcurrentlyContext)</span> {<br>                list.forEach( n-&gt;{<br>                    System.out.printf(<span class="hljs-string">"收到消息: %s%n"</span> , n);<br>                });<br>                <span class="hljs-keyword">return</span> ConsumeConcurrentlyStatus.CONSUME_SUCCESS;<br>            }<br>        });<br>        pushConsumer.start();<br>        System.out.printf(<span class="hljs-string">"Consumer Started.%n"</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>执行后能够返回：</p><p><img src="https://files.seeusercontent.com/2026/05/07/6Tbr/image-20260507110648368.png" alt="image-20260507110648368"></p><p><img src="https://files.seeusercontent.com/2026/05/07/tHn2/image-20260507110713611.png" alt="image-20260507110713611"></p><p>在控制台也能查到信息：</p><p><img src="https://files.seeusercontent.com/2026/05/07/t2Fe/image-20260507110826579.png" alt="image-20260507110826579"></p><p>那么本地的JAVA开发测试就完成了，下面介绍一下各种消息类型。</p><h2 id="顺序消息"><a href="#顺序消息" class="headerlink" title="顺序消息"></a>顺序消息</h2><p><strong>顺序消息（Orderly Message）</strong> 指的是消息的<strong>生产顺序</strong>与<strong>消费顺序</strong>保持一致。<br>它分为两种：</p><ul><li><strong>全局有序</strong>：所有消息严格按照发送顺序被消费，只能使用 <strong>一个队列</strong>（Topic 下只有一个 Queue），性能较低。</li><li><strong>分区有序（局部有序）</strong>：同一业务标识（如订单 ID）的消息<strong>在同一个队列内</strong>严格有序，但不同并行队列之间是并行无序的，这是最常用的方式。</li></ul><p>生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> Order;<br><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.MessageQueueSelector;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.MessageQueue;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><span class="hljs-keyword">import</span> java.util.List;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment">* 顺序消息生产者</span><br><span class="hljs-comment">*/</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OrderProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"OrderProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">j</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">5</span>; j++) {<br>            <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) {<br>                <span class="hljs-type">Message</span> <span class="hljs-variable">message</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"OrderTopic"</span>,<span class="hljs-string">"TagA"</span>,<br>                                              (<span class="hljs-string">"order_"</span> + j + <span class="hljs-string">"_step_"</span> + i).getBytes(StandardCharsets.UTF_8));<br>                <span class="hljs-type">SendResult</span> <span class="hljs-variable">sendResult</span> <span class="hljs-operator">=</span> producer.send(message, <span class="hljs-keyword">new</span> <span class="hljs-title class_">MessageQueueSelector</span>() {<br>                    <span class="hljs-meta">@Override</span><br>                    <span class="hljs-keyword">public</span> MessageQueue <span class="hljs-title function_">select</span><span class="hljs-params">(List&lt;MessageQueue&gt; list, Message message, Object o)</span> {<br>                        <span class="hljs-type">Integer</span> <span class="hljs-variable">id</span> <span class="hljs-operator">=</span> (Integer) o;<br>                        <span class="hljs-type">int</span> <span class="hljs-variable">index</span> <span class="hljs-operator">=</span> id % list.size();<br>                        <span class="hljs-keyword">return</span> list.get(index);<br>                    }<br>                }, j);<br>                System.out.printf(<span class="hljs-string">"%s%n"</span>, sendResult);<br>            }<br>        }<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>重点解释一下队列选择中的代码：双层循环，j是订单号，又名步骤标识，i是订单内的步骤号。</p><p>匿名函数的第三个参数 <code>j</code> 会作为 <code>Object o</code> 传入 <code>select</code> 方法，即<strong>业务标识</strong>。</p><p><strong><code>id % list.size()</code></strong> 是实现选择装入的核心：</p><ul><li>假设 <code>OrderTopic</code> 有 8 个队列（编号 0~7），<code>list.size()</code> 为 8。</li><li><code>order_0</code> → <code>j=0</code> → <code>0 % 8 = 0</code> → 始终选择队列 0。</li><li><code>order_1</code> → <code>j=1</code> → <code>1 % 8 = 1</code> → 始终选择队列 1。</li><li><code>order_2</code> → <code>2 % 8 = 2</code> → 队列 2，依此类推。</li></ul><p><strong>结果</strong>：同一订单的所有消息（<code>step_0</code> 到 <code>step_9</code>）都被路由到<strong>同一个固定的队列</strong>中，且单个订单的发送的顺序就是 <code>step_0</code>→<code>step_9</code>。<strong>同一个队列（Queue）内的消息是严格 FIFO（先入先出）的</strong>，无论是存储还是拉取，都保持写入顺序。</p><p>消费者:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> Order;<br><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.MessageExt;<br><br><span class="hljs-keyword">import</span> java.util.List;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 顺序消息消费者</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OrderConsumer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException {<br>        <span class="hljs-type">DefaultMQPushConsumer</span> <span class="hljs-variable">consumer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQPushConsumer</span>(<span class="hljs-string">"OrderConsumer"</span>);<br>        consumer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        consumer.subscribe(<span class="hljs-string">"OrderTopic"</span>,<span class="hljs-string">"*"</span>);<br>        consumer.registerMessageListener(<span class="hljs-keyword">new</span> <span class="hljs-title class_">MessageListenerOrderly</span>() {<br>            <span class="hljs-meta">@Override</span><br>            <span class="hljs-keyword">public</span> ConsumeOrderlyStatus <span class="hljs-title function_">consumeMessage</span><span class="hljs-params">(List&lt;MessageExt&gt; list, ConsumeOrderlyContext consumeOrderlyContext)</span> {<br>                list.forEach(n-&gt;{<br>                    System.out.println(<span class="hljs-string">"QueueId:"</span>+n.getQueueId() + <span class="hljs-string">"收到消息内容 "</span>+<span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>(n.getBody()));<br>                });<br>                <span class="hljs-keyword">return</span> ConsumeOrderlyStatus.SUCCESS;<br>            }<br>        });<br>        consumer.start();<br>        System.out.printf(<span class="hljs-string">"Consumer Started.%n"</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>消费者进程注册一个<strong>顺序消息监听器</strong> <code>MessageListenerOrderly</code>。这是实现顺序消费的关键。</p><p>与并发监听器 <code>MessageListenerConcurrently</code> 不同，顺序监听器<strong>保证同一队列的消息在同一时刻只被一个线程消费</strong>，且消费顺序与队列内存储顺序一致。</p><p>所以：</p><ul><li>生产者通过 <code>MessageQueueSelector</code> 将同业务键的消息固定到一个队列，队列内 FIFO 存储。</li><li>消费者使用 <code>MessageListenerOrderly</code>，<strong>对每个队列只会分配一个线程</strong>去顺序拉取和处理消息，严格保障消费顺序。</li></ul><h2 id="广播消息"><a href="#广播消息" class="headerlink" title="广播消息"></a>广播消息</h2><p><strong>广播消息（Broadcasting）</strong> 是 RocketMQ 的一种消费模式，与默认的<strong>集群模式（Clustering）</strong> 相对。</p><table><thead><tr><th>模式</th><th>消费行为</th><th>应用场景</th></tr></thead><tbody><tr><td><strong>集群模式</strong>（默认）</td><td>同组内<strong>所有消费者均摊</strong>队列，每条消息只被<strong>一个消费者</strong>处理</td><td>分布式任务处理，如订单履约</td></tr><tr><td><strong>广播模式</strong></td><td>同组内<strong>每个消费者都收到全部消息</strong>，相当于每个人都独立消费全量数据</td><td>推送配置更新、缓存刷新、通知所有人</td></tr></tbody></table><p>广播模式消费者JAVA写法：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> broadcast; <span class="hljs-comment">// 声明包名，通常用于组织代码</span><br><br><span class="hljs-comment">// 导入 RocketMQ 客户端所需的类</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; <span class="hljs-comment">// 推模式消费者</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; <span class="hljs-comment">// 并发消费上下文</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; <span class="hljs-comment">// 并发消费状态</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; <span class="hljs-comment">// 并发消息监听器</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException; <span class="hljs-comment">// 客户端异常</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.MessageExt; <span class="hljs-comment">// 消息扩展对象（包含消息详情）</span><br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; <span class="hljs-comment">// 消费模式枚举（集群/广播）</span><br><br><span class="hljs-keyword">import</span> java.util.List; <span class="hljs-comment">// Java 集合类，用于存储消息列表</span><br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 广播消息消费模式</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">BroadcastConsumer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException {<br>        <span class="hljs-comment">// 1. 创建推模式消费者，指定消费者组名为 "BroadCastConsumer"</span><br>        <span class="hljs-type">DefaultMQPushConsumer</span> <span class="hljs-variable">consumer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQPushConsumer</span>(<span class="hljs-string">"BroadCastConsumer"</span>);<br><br>        <span class="hljs-comment">// 2. 设置 NameServer 地址，用于发现 Broker 和路由信息</span><br>        consumer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br><br>        <span class="hljs-comment">// 3. 订阅主题 "simple"，"*" 表示接收该主题下的所有标签（Tag）消息</span><br>        consumer.subscribe(<span class="hljs-string">"simple"</span>, <span class="hljs-string">"*"</span>);<br><br>        <span class="hljs-comment">// 4. 设置消费模式为广播（默认是集群模式）</span><br>        consumer.setMessageModel(MessageModel.BROADCASTING); <span class="hljs-comment">// 广播模式</span><br>        <span class="hljs-comment">// consumer.setMessageModel(MessageModel.CLUSTERING); // 如果取消注释，则切换为集群模式</span><br><br>        <span class="hljs-comment">// 5. 注册并发消息监听器（广播模式下不能使用顺序监听器）</span><br>        consumer.registerMessageListener(<span class="hljs-keyword">new</span> <span class="hljs-title class_">MessageListenerConcurrently</span>() {<br>            <span class="hljs-meta">@Override</span><br>            <span class="hljs-keyword">public</span> ConsumeConcurrentlyStatus <span class="hljs-title function_">consumeMessage</span><span class="hljs-params">(List&lt;MessageExt&gt; list, ConsumeConcurrentlyContext consumeConcurrentlyContext)</span> {<br>                <span class="hljs-comment">// 遍历本次拉取到的消息列表</span><br>                list.forEach(n -&gt; {<br>                    <span class="hljs-comment">// 打印消息所在的队列 ID 和消息体内容</span><br>                    System.out.println(<span class="hljs-string">"QueueId:"</span> + n.getQueueId() + <span class="hljs-string">" 收到消息内容 "</span> + <span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>(n.getBody()));<br>                });<br>                <span class="hljs-comment">// 返回消费成功状态，Broker 会更新消费进度</span><br>                <span class="hljs-keyword">return</span> ConsumeConcurrentlyStatus.CONSUME_SUCCESS;<br>            }<br>        });<br><br>        <span class="hljs-comment">// 6. 启动消费者，开始监听并消费消息</span><br>        consumer.start();<br><br>        <span class="hljs-comment">// 控制台打印启动成功标识</span><br>        System.out.printf(<span class="hljs-string">"Broadcast Consumer Started.%n"</span>);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>该广播模式消费者组的每个实例都会将主题的每一条消息接收并处理，<strong>不会因为组里有示例已接收就放弃。</strong></p><p>而对于默认的集群模式，比如订单处理服务部署了 5 个实例，加入同一个消费者组 OrderConsumerGroup。 10 个订单队列均匀分配给 5 个实例，每条订单消息只被一个实例处理，不会出现多个实例重复发货的问题。实际中会运行五个甚至更多一模一样的程序来提升并发度从而提升运行效率。<strong>这是分布式系统里典型的方式(水平扩展方式)，目的就是为了提升并发度和整体吞吐量。</strong></p><h2 id="延迟消息"><a href="#延迟消息" class="headerlink" title="延迟消息"></a>延迟消息</h2><p><strong>延迟消息</strong>指的是生产者发送消息(立即传到Broker)后，<strong>Broker不会立即投递给消费者，而是按照预设的延迟时间，到了指定时间后才被消费者可见并消费</strong>。这是RocketMQ特有的一个功能。使用方法有两种：</p><ul><li>**message.setDelayTimeLevel(3)**：预定日常定时发送。1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h；可以在dashboard中broker配置查看。</li><li>**msg.setDelayTimeMs(10L)**：指定时间定时发送。默认支持最大延迟时间为3天，可以根据broker配置：timerMaxDelaySec修改。</li></ul><p>如预定日期生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><span class="hljs-keyword">import</span> java.time.LocalTime;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 预定日程定时发送</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">ScheduleProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"ScheduleProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">2</span>; i++) {<br>            <span class="hljs-type">Message</span> <span class="hljs-variable">msg</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"Schedule"</span>, <span class="hljs-comment">//主题</span><br>                    <span class="hljs-string">"TagA"</span>,  <span class="hljs-comment">//设置消息Tag，用于消费端根据指定Tag过滤消息。</span><br>                    <span class="hljs-string">"ScheduleProducer"</span>.getBytes(StandardCharsets.UTF_8) <span class="hljs-comment">//消息体。</span><br>            );<br>            <span class="hljs-comment">//1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h</span><br>            msg.setDelayTimeLevel(<span class="hljs-number">3</span>);<br>            producer.send(msg);<br>            System.out.printf(i + <span class="hljs-string">".发送消息成功：%s%n"</span>, LocalTime.now());<br>        }<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><h2 id="批量消息"><a href="#批量消息" class="headerlink" title="批量消息"></a>批量消息</h2><p><strong>批量消息（Batch Message）</strong> 是指生产者<strong>将多条消息合并为一个请求</strong>发送给 Broker，以减少网络往返次数，提升吞吐量。</p><p>批量消息的使用限制：</p><ul><li>消息大小不能超过4M，虽然源码注释不能超1M，但是实际使用不超过4M即可。平衡整体的性能，建议保持1M左右。</li><li>相同的Topic主题</li><li>相同的waitStoreMsgOK</li><li><ul><li><code>waitStoreMsgOK</code> 是 Apache RocketMQ 中一个核心的可靠性保障参数。它是一条指令，告诉 Broker（消息服务器）：<strong>在向生产者（Producer）返回“发送成功”的响应之前，是否必须等待消息被写入磁盘。</strong></li></ul></li><li>不能是延迟消息、事务消息等特殊信息。</li></ul><p>插播一条<code>waitStoreMsgOK</code>:</p><table><thead><tr><th>对比维度</th><th><code>waitStoreMsgOK = true</code> (默认)</th><th><code>waitStoreMsgOK = false</code></th></tr></thead><tbody><tr><td><strong>等待行为</strong></td><td>生产者线程会阻塞，等待写入完成</td><td>生产者发送后立即返回，不等待写入结果</td></tr><tr><td><strong>性能影响</strong></td><td>较低。每次发送都涉及磁盘I/O操作的等待时间</td><td>非常高。发送操作不被磁盘I/O阻塞</td></tr><tr><td><strong>数据可靠性</strong></td><td>极高。一旦确认，消息已持久化到磁盘，不易丢失</td><td>较低。若Broker在内存数据刷盘前宕机，消息会丢失</td></tr><tr><td><strong>组合使用</strong></td><td>Broker 的 <code>flushDiskType</code> 必须设置为 <code>SYNC_FLUSH</code></td><td>Broker 刷盘模式可以是 <code>SYNC_FLUSH</code> 或 <code>ASYNC_FLUSH</code></td></tr><tr><td><strong>适用场景</strong></td><td>金融交易、支付回调等绝不能丢失消息的场景</td><td>海量日志采集、非关键性监控数据上报等，允许少量丢失以换取高吞吐的场景</td></tr></tbody></table><p>如生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><span class="hljs-keyword">import</span> java.util.ArrayList;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 批量发送消息</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">BatchProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"BatchProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        ArrayList&lt;Message&gt; messages = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayList</span>&lt;&gt;();<br>        messages.add(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"simple"</span>,<span class="hljs-string">"TagA"</span>, <span class="hljs-string">"BatchProducer0"</span>.getBytes(StandardCharsets.UTF_8)));<br>        messages.add(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"simple"</span>,<span class="hljs-string">"TagA"</span>, <span class="hljs-string">"BatchProducer1"</span>.getBytes(StandardCharsets.UTF_8)));<br>        messages.add(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"simple"</span>,<span class="hljs-string">"TagA"</span>, <span class="hljs-string">"BatchProducer2"</span>.getBytes(StandardCharsets.UTF_8)));<br>        <span class="hljs-type">SendResult</span> <span class="hljs-variable">send</span> <span class="hljs-operator">=</span> producer.send(messages);<br>        System.out.printf(<span class="hljs-string">".发送消息成功：%s%n"</span>, send);<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>将信息都装入一个动态数组<code>ArrayList</code>中，在用send发出去就行。这貌似能够用直觉理解，至于这背后的源码怎么写，我们大多数人应该不在乎。</p><h2 id="消息过滤"><a href="#消息过滤" class="headerlink" title="消息过滤"></a>消息过滤</h2><h3 id="Tag过滤"><a href="#Tag过滤" class="headerlink" title="Tag过滤"></a>Tag过滤</h3><p>所有的消息都可以带Tag，生产者用Tag标记消息，消费者用Tag过滤消息：</p><p>如生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 过滤消息-tag过滤生产者</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TagFilterProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"SyncProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        String[] tags = <span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[] {<span class="hljs-string">"TagA"</span>,<span class="hljs-string">"TagB"</span>,<span class="hljs-string">"TagC"</span>};<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">15</span>; i++) {<br>            <span class="hljs-type">Message</span> <span class="hljs-variable">msg</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"FilterTopic"</span>, <span class="hljs-comment">//主题</span><br>                    tags[i % tags.length],  <span class="hljs-comment">//设置消息Tag，用于消费端根据指定Tag过滤消息。</span><br>                    (<span class="hljs-string">"TagFilterProducer_"</span>+tags[i % tags.length]).getBytes(StandardCharsets.UTF_8) <span class="hljs-comment">//消息体。</span><br>            );<br>            <span class="hljs-type">SendResult</span> <span class="hljs-variable">send</span> <span class="hljs-operator">=</span> producer.send(msg);<br>            System.out.printf(i + <span class="hljs-string">".发送消息成功：%s%n"</span>, send);<br>        }<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p><strong>Tag是RocketMQ中特有的一个消息属性。</strong></p><p>RocketMQ的最佳实践中就建议使用RocketMQ时，<strong>一个应用可以就用一个Topic</strong>，而应用中的不同业务就用Tag来区分。</p><h3 id="SQL过滤"><a href="#SQL过滤" class="headerlink" title="SQL过滤"></a>SQL过滤</h3><p>有时候现实情况很复杂，仅仅用 一个Tag来定义一个消息是不够的。所以我们用SQL来做进一步区分。</p><p>如生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> filter;<br><br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQBrokerException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.DefaultMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.SendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.remoting.exception.RemotingException;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment">* 过滤消息-SQL过滤生产者</span><br><span class="hljs-comment">*/</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SqlFilterProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, MQBrokerException, RemotingException, InterruptedException {<br>        <span class="hljs-type">DefaultMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultMQProducer</span>(<span class="hljs-string">"SyncProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        producer.start();<br>        String[] tags = <span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[] {<span class="hljs-string">"TagA"</span>,<span class="hljs-string">"TagB"</span>,<span class="hljs-string">"TagC"</span>};<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">15</span>; i++) {<br>            <span class="hljs-type">Message</span> <span class="hljs-variable">msg</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"FilterTopic"</span>, <span class="hljs-comment">//主题</span><br>                                      tags[i % tags.length],  <span class="hljs-comment">//设置消息Tag，用于消费端根据指定Tag过滤消息。</span><br>                                      (<span class="hljs-string">"TagFilterProducer_"</span>+tags[i % tags.length] +  <span class="hljs-string">"_i_"</span> + i).getBytes(StandardCharsets.UTF_8) <span class="hljs-comment">//消息体。</span><br>                                     );<br>            msg.putUserProperty(<span class="hljs-string">"baiLi"</span>, String.valueOf(i));<br>            <span class="hljs-type">SendResult</span> <span class="hljs-variable">send</span> <span class="hljs-operator">=</span> producer.send(msg);<br>            System.out.printf(i + <span class="hljs-string">".发送消息成功：%s%n"</span>, send);<br>        }<br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>我们重点看这句：<code>  msg.putUserProperty("baiLi", String.valueOf(i));</code></p><p><code>putUserProperty</code> 给消息附加了一个名为 <code>baiLi</code> 的属性，值为当前循环序号 <code>i</code>（字符串形式）。</p><p>后续消费者可以使用 SQL92 表达式进行过滤，例如：</p><p><code>consumer.subscribe("FilterTopic", MessageSelector.bySql("baiLi BETWEEN 0 AND 5"));</code></p><p>这样消费者只会收到 <code>baiLi</code> 属性在 0<del>5 之间的消息，即序号 0</del>5 的消息（共 6 条）。</p><p>SQL肯定是比Tag更灵活，但也更复杂。</p><h2 id="事务消息"><a href="#事务消息" class="headerlink" title="事务消息"></a>事务消息</h2><p>事务( Transaction)(消息是 RocketMQ 用来<strong>保证本地事务执行和消息发送原子性</strong>的一种高级消息类型，它基于 <strong>两阶段提交（2PC）</strong>和<strong>事务回查</strong>机制，确保两者在分布式环境下最终一致，要么一起成功，要么一起失败。</p><p><img src="https://files.seeusercontent.com/2026/05/07/Nth1/image.png" alt="image"></p><p><strong>第一阶段：发送“半消息”</strong><br>生产者向 Broker 发送一条消息。这条消息特殊之处在于，Broker 成功保存后，会向生产者确认，但<strong>暂时不对消费者投递</strong>。此时，这条消息处于“<strong>半消息</strong>”状态。</p><p><strong>第二阶段：执行本地事务</strong><br>生产者收到上一步的成功确认后，开始执行自身的本地业务逻辑，例如“将订单状态更新为‘已支付’”。</p><p><strong>第三阶段：提交或回滚</strong><br>生产者根据本地事务的执行结果，向 Broker 发送二次确认：</p><ul><li><strong>Commit</strong>：本地事务成功后发送。Broker 收到后将“半消息”标记为“可投递”，消费者就能消费了。</li><li><strong>Rollback</strong>：本地事务失败后发送。Broker 会将之前的“半消息”丢弃。</li></ul><p>保证原子性的最后一个方法是<strong>事务回查</strong></p><p>如果因网络问题或生产者宕机，Broker 迟迟收不到二次确认，它会启动<strong>事务回查</strong>：</p><ol><li><strong>主动询问最终状态</strong>：经过一段固定时间（例如，默认30秒），Broker 会向<strong>同一生产者组</strong>中的任意一个实例，发起回查请求，询问该消息关联的本地事务的最终状态。</li><li><strong>实现<code>checkLocalTransaction</code>方法</strong>：为了保护回查机制能正常工作，<strong>开发者必须实现<code>TransactionListener</code>接口中的<code>checkLocalTransaction</code>方法</strong>，Broker会主动调用这个方法来确定本地事务的最终状态。</li><li><strong>最终处理</strong>：生产者确认后，Broker 会再次根据 Commit 或 Rollback 的结果，对“半消息”进行最终处理。</li></ol><p>如事务消息生产者：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> org.apache.rocketmq.client.exception.MQClientException;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.TransactionListener;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.TransactionMQProducer;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.TransactionSendResult;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><br><span class="hljs-keyword">import</span> java.nio.charset.StandardCharsets;<br><span class="hljs-keyword">import</span> java.util.concurrent.*;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 事务消息生产者</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TransactionProducer</span> {<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> MQClientException, InterruptedException {<br>        <span class="hljs-type">TransactionMQProducer</span> <span class="hljs-variable">producer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">TransactionMQProducer</span>(<span class="hljs-string">"TransProducer"</span>);<br>        producer.setNamesrvAddr(<span class="hljs-string">"127.0.0.1:9876"</span>);<br>        <span class="hljs-comment">//使用executorService异步提交事务状态，从而提高系统的性能和可靠性</span><br>        <span class="hljs-type">ExecutorService</span> <span class="hljs-variable">executorService</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ThreadPoolExecutor</span>(<span class="hljs-number">2</span>, <span class="hljs-number">5</span>, <span class="hljs-number">100</span>, TimeUnit.SECONDS, <span class="hljs-keyword">new</span> <span class="hljs-title class_">ArrayBlockingQueue</span>&lt;Runnable&gt;(<span class="hljs-number">2000</span>), <span class="hljs-keyword">new</span> <span class="hljs-title class_">ThreadFactory</span>() {<br>            <span class="hljs-meta">@Override</span><br>            <span class="hljs-keyword">public</span> Thread <span class="hljs-title function_">newThread</span><span class="hljs-params">(Runnable r)</span> {<br>                <span class="hljs-type">Thread</span> <span class="hljs-variable">thread</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Thread</span>(r);<br>                thread.setName(<span class="hljs-string">"client-transaction-msg-check-thread"</span>);<br>                <span class="hljs-keyword">return</span> thread;<br>            }<br>        });<br>        producer.setExecutorService(executorService);<br><br>        <span class="hljs-comment">//本地事务监听器</span><br>        <span class="hljs-type">TransactionListener</span> <span class="hljs-variable">transactionListener</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">TransactionListenerImpl</span>();<br>        producer.setTransactionListener(transactionListener);<br><br>        producer.start();<br>        String[] tags = <span class="hljs-keyword">new</span> <span class="hljs-title class_">String</span>[] {<span class="hljs-string">"TagA"</span>,<span class="hljs-string">"TagB"</span>,<span class="hljs-string">"TagC"</span>,<span class="hljs-string">"TagD"</span>,<span class="hljs-string">"TagE"</span>};<br>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) {<br>            <span class="hljs-type">Message</span> <span class="hljs-variable">message</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Message</span>(<span class="hljs-string">"TransactionTopic"</span>,<br>                    tags[ i % tags.length],<br>                    (<span class="hljs-string">"Transaction_"</span>+ tags[ i % tags.length]).getBytes(StandardCharsets.UTF_8));<br>            <span class="hljs-type">TransactionSendResult</span> <span class="hljs-variable">transactionSendResult</span> <span class="hljs-operator">=</span> producer.sendMessageInTransaction(message, <span class="hljs-literal">null</span>);<br>            System.out.printf(<span class="hljs-string">"%s%n"</span>, transactionSendResult);<br><br>            Thread.sleep(<span class="hljs-number">10</span>); <span class="hljs-comment">//延迟10毫秒</span><br>        }<br><br>        Thread.sleep(<span class="hljs-number">100000</span>);<span class="hljs-comment">//等待broker端回调</span><br>        producer.shutdown();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p><strong><code>TransactionListenerImpl</code>监听器需要开发者自己写</strong>，因为RocketMQ无法预知你的回查逻辑：</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> transaction;<br><br><span class="hljs-keyword">import</span> org.apache.commons.lang3.StringUtils;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.LocalTransactionState;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.client.producer.TransactionListener;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.Message;<br><span class="hljs-keyword">import</span> org.apache.rocketmq.common.message.MessageExt;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 本地事务监听器</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TransactionListenerImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">TransactionListener</span> {<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-comment">/**</span><br><span class="hljs-comment">     * 在提交完事务消息后执行。</span><br><span class="hljs-comment">     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。</span><br><span class="hljs-comment">     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。</span><br><span class="hljs-comment">     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。</span><br><span class="hljs-comment">     */</span><br>    <span class="hljs-keyword">public</span> LocalTransactionState <span class="hljs-title function_">executeLocalTransaction</span><span class="hljs-params">(Message message, Object o)</span> {<br>        <span class="hljs-type">String</span> <span class="hljs-variable">tags</span> <span class="hljs-operator">=</span> message.getTags();<br>        <span class="hljs-comment">//TagA的消息会立即被消费者消费到</span><br>        <span class="hljs-keyword">if</span>(StringUtils.contains(tags,<span class="hljs-string">"TagA"</span>)){<br>            <span class="hljs-keyword">return</span> LocalTransactionState.COMMIT_MESSAGE;<br>            <span class="hljs-comment">//TagB的消息会被丢弃</span><br>        }<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(StringUtils.contains(tags,<span class="hljs-string">"TagB"</span>)){<br>            <span class="hljs-keyword">return</span> LocalTransactionState.ROLLBACK_MESSAGE;<br>            <span class="hljs-comment">//其他消息会等待Broker进行事务状态回查。</span><br>        }<span class="hljs-keyword">else</span>{<br>            <span class="hljs-keyword">return</span> LocalTransactionState.UNKNOW;<br>        }<br>    }<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-comment">/**</span><br><span class="hljs-comment">     * 在对UNKNOWN状态的消息进行状态回查时执行。</span><br><span class="hljs-comment">     * 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。</span><br><span class="hljs-comment">     * 返回ROLLBACK_MESSAGE状态的消息会被丢弃。</span><br><span class="hljs-comment">     * 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。</span><br><span class="hljs-comment">     */</span><br>    <span class="hljs-keyword">public</span> LocalTransactionState <span class="hljs-title function_">checkLocalTransaction</span><span class="hljs-params">(MessageExt messageExt)</span> {<br>        <span class="hljs-type">String</span> <span class="hljs-variable">tags</span> <span class="hljs-operator">=</span> messageExt.getTags();<br>        <span class="hljs-comment">//TagC的消息过一段时间会被消费者消费到</span><br>        <span class="hljs-keyword">if</span>(StringUtils.contains(tags,<span class="hljs-string">"TagC"</span>)){<br>            <span class="hljs-keyword">return</span> LocalTransactionState.COMMIT_MESSAGE;<br>            <span class="hljs-comment">//TagD的消息也会在状态回查时被丢弃掉</span><br>        }<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(StringUtils.contains(tags,<span class="hljs-string">"TagD"</span>)){<br>            <span class="hljs-keyword">return</span> LocalTransactionState.ROLLBACK_MESSAGE;<br>            <span class="hljs-comment">//剩下TagE的消息会在多次状态回查后最终丢弃</span><br>        }<span class="hljs-keyword">else</span>{<br>            <span class="hljs-keyword">return</span> LocalTransactionState.UNKNOW;<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>事务消息的使用限制</p><ul><li>事务消息不支持延迟消息和批量消息。</li><li>为了避免单个消息被检查太多次而导致半队列消息累积，我们默认将单个消息的检查次数限制为 15 次，但是用户可以通过 Broker 配置文件的transactionCheckMax参数来修改此限制。如果已经检查某条消息超过N次的话（N = transactionCheckMax）则 Broker 将丢弃此消息，并在默认情况下同时打印错误日志。可以通过重写AbstractTransactionCheckListener类来修改这个行为。</li><li>事务性消息可能不止一次被检查或消费。</li></ul><h3 id="RocketMQ如何保证数据不丢失"><a href="#RocketMQ如何保证数据不丢失" class="headerlink" title="RocketMQ如何保证数据不丢失"></a>RocketMQ如何保证数据不丢失</h3><blockquote><p>这是一个补充说明：</p></blockquote><p><img src="https://files.seeusercontent.com/2026/05/07/Msv8/image.png" alt="image"></p><p><img src="https://files.seeusercontent.com/2026/05/07/0Vct/image.png" alt="image"></p><blockquote><p>“刷盘 (Flush Disk)” 是消息队列（Message Queue）为保证消息可靠性的核心机制。简单来说，它指的是将暂存在内存（操作系统Page Cache）中的消息数据，强制写入到物理磁盘上的过程。</p></blockquote><p>我们将消息流程分为三大部分，每一部分都有可能会丢失数据。</p><ul><li>生产阶段：Producer通过网络将消息发送给Broker，这个发送可能会发生丢失。比如网络延迟不可达等。</li><li>存储阶段：Broker肯定是先把消息放到内存的，然后根据刷盘策略持久化到硬盘中。刚收到Producer的消息，放入内存，但是异常宕机了，导致消息丢失。</li><li>消费阶段：消费失败。比如先提交ack再消费，处理过程中出现异常，该消息就出现了丢失。</li></ul><p>解决方式：</p><ul><li>生产阶段：使用同步发送失败重试机制；异步发送重写回调方法检查发送结果；Ack确认机制。</li><li>存储阶段：同步刷盘机制；集群模式采用同步复制。</li><li>消费阶段：正常消费处理完成才提交ACK；如果处理异常返回重试标识。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;RocketMQ消息队列&quot;&gt;&lt;a href=&quot;#RocketMQ消息队列&quot; class=&quot;headerlink&quot; title=&quot;RocketMQ消息队列&quot;&gt;&lt;/a&gt;RocketMQ消息队列&lt;/h1&gt;&lt;h2 id=&quot;消息队列&quot;&gt;&lt;a href=&quot;#消息队列&quot; cla</summary>
      
    
    
    
    <category term="消息队列" scheme="https://blog.willmo.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
    
    <category term="消息队列" scheme="https://blog.willmo.top/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    
  </entry>
  
  <entry>
    <title>Claude Code基础使用手册</title>
    <link href="https://blog.willmo.top/posts/claude/"/>
    <id>https://blog.willmo.top/posts/claude/</id>
    <published>2026-05-02T06:38:50.101Z</published>
    <updated>2026-05-11T11:43:05.289Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Claude-Code基础使用手册"><a href="#Claude-Code基础使用手册" class="headerlink" title="Claude Code基础使用手册"></a>Claude Code基础使用手册</h1><blockquote><p>不要完全依赖AI，面对一个黑箱项目时的束手无策，源于人类最原始的傲慢。</p></blockquote><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>Claude Code 是 Anthropic 推出的面向开发者的 AI 编程协作工具，与在聊天窗口里写几段代码不同，Claude Code 的核心目标是理解你的整个项目，并参与到真实的编码、修改和重构过程中。</p><p>Claude Code不是chat，并不局限于聊天，Claude Code时Agent，是代理人，能够帮助用户直接编辑代码以完成项目。</p><h2 id="Claude-Code-CLI安装"><a href="#Claude-Code-CLI安装" class="headerlink" title="Claude Code CLI安装"></a>Claude Code CLI安装</h2><p>在Windows的Powershell安装：</p><figure class="highlight powershell"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs powershell"><span class="hljs-built_in">irm</span> https://claude.ai/install.ps1 | <span class="hljs-built_in">iex</span><br></code></pre></td></tr></tbody></table></figure><p>检验是否安装成功：</p><figure class="highlight ada"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ada">claude <span class="hljs-comment">--version</span><br></code></pre></td></tr></tbody></table></figure><p>如果出现版本号就是安装成功。</p><p>更新命令：</p><figure class="highlight cmake"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmake">claude <span class="hljs-keyword">install</span><br></code></pre></td></tr></tbody></table></figure><p>或者可以在<code>settings.json</code>中设置</p><figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">{</span><br>  <span class="hljs-attr">"autoUpdatesChannel"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"stable"</span>    <span class="hljs-comment">// 更新渠道：stable（稳定版，推荐）或 beta（测试版）</span><br><span class="hljs-punctuation">}</span><br><span class="hljs-punctuation">{</span><br>  <span class="hljs-attr">"env"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><br>    <span class="hljs-attr">"DISABLE_AUTOUPDATER"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"1"</span>    <span class="hljs-comment">// 设为 "1" 禁用自动更新，"0" 或删除该行则恢复自动更新</span><br>  <span class="hljs-punctuation">}</span><br><span class="hljs-punctuation">}</span><br></code></pre></td></tr></tbody></table></figure><p>删除指令：</p><figure class="highlight basic"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs basic"><span class="hljs-comment">Remove-Item -Path "$env:USERPROFILE\.local\bin\claude.exe" -Force</span><br><span class="hljs-comment">Remove-Item -Path "$env:USERPROFILE\.local\share\claude" -Recurse -Force</span><br></code></pre></td></tr></tbody></table></figure><p>安装过程中可能需要手动添加环境变量，这里不多赘述。</p><h4 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h4><figure class="highlight ebnf"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">claude</span><br></code></pre></td></tr></tbody></table></figure><p>首次启动后claude会引导你登录，但是登录似乎需要Max或者Pro。不登录也没关系。</p><h2 id="接入DeepSeek-API"><a href="#接入DeepSeek-API" class="headerlink" title="接入DeepSeek API"></a>接入DeepSeek API</h2><p>Deepseek是思考问题的大脑，Claude是大脑之下的身体而已，所以我们只需要付给Deepseek API费用就足够了。</p><p>你需要去在 <a href="https://platform.deepseek.com/api_keys">DeepSeek Platform</a> 注册账号并创建 API Key。</p><p>然后在Powershell中执行命令以设置环境变量：</p><figure class="highlight powershell"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs powershell"><span class="hljs-variable">$env:ANTHROPIC_BASE_URL</span>=<span class="hljs-string">"https://api.deepseek.com/anthropic"</span><br><span class="hljs-variable">$env:ANTHROPIC_AUTH_TOKEN</span>=<span class="hljs-string">"&lt;你的 DeepSeek API Key&gt;"</span><br><span class="hljs-variable">$env:ANTHROPIC_MODEL</span>=<span class="hljs-string">"deepseek-v4-pro[1m]"</span><br><span class="hljs-variable">$env:ANTHROPIC_DEFAULT_OPUS_MODEL</span>=<span class="hljs-string">"deepseek-v4-pro[1m]"</span><br><span class="hljs-variable">$env:ANTHROPIC_DEFAULT_SONNET_MODEL</span>=<span class="hljs-string">"deepseek-v4-pro[1m]"</span><br><span class="hljs-variable">$env:ANTHROPIC_DEFAULT_HAIKU_MODEL</span>=<span class="hljs-string">"deepseek-v4-flash"</span><br><span class="hljs-variable">$env:CLAUDE_CODE_SUBAGENT_MODEL</span>=<span class="hljs-string">"deepseek-v4-flash"</span><br><span class="hljs-variable">$env:CLAUDE_CODE_EFFORT_LEVEL</span>=<span class="hljs-string">"max"</span><br></code></pre></td></tr></tbody></table></figure><p>随后在任意项目地址使用<code>claude</code>启动：</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260502140258568.png" alt="image-20260502140258568"></p><p>可以看到deepseek-v4-pro的模型，提问会消耗api，所以如果要吹水建议去网页chat。</p><p>API是按量计费的，如果使用包月的Coding Plan 就能够按月计费。</p><p>随后你可以在IDE中安装claude的插件来在IDE中使用。</p><p>至此，你已经安装并配置完成了，在Token够用的情况下，你可以自由地使用Claude Code。</p><h2 id="项目初始化"><a href="#项目初始化" class="headerlink" title="项目初始化"></a>项目初始化</h2><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">/init<br></code></pre></td></tr></tbody></table></figure><p>Claude 会自动扫描你的代码库——读取 package.json、现有文档、配置文件以及代码结构，然后生成一份专属于你项目的 CLAUDE.md 文件。</p><p>Claude 整个过程无需手动操作，Claude 会自行分析并输出初始配置。</p><p>CLAUDE.md 是一个放在项目根目录的 Markdown 文件，Claude Code 在每次会话开始时都会自动读取。</p><p>CLAUDE.md 会成为 Claude 系统提示的一部分，使每次对话都能预先加载项目上下文，不再需要重复解释基本信息。</p><p>但是<strong>不要让 CLAUDE.md 替代 linter</strong>：在文件中写代码风格规范是最常见的误区之一。永远不要用 LLM 来做 linter 的工作——linter 更快、更便宜，而且是确定性的。代码风格约束只会让上下文窗口膨胀，降低 Claude 的指令遵从质量。</p><p>你可以使用<code>#</code>让模型记住什么事情，比如：</p><figure class="highlight 1c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs 1c"><span class="hljs-meta"># 强制使用shell以保证兼容性</span><br><span class="hljs-meta"># 最后项目需要经过编译测试</span><br></code></pre></td></tr></tbody></table></figure><h2 id="Claude-Code目录结构"><a href="#Claude-Code目录结构" class="headerlink" title="Claude Code目录结构"></a>Claude Code目录结构</h2><figure class="highlight stylus"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs stylus">your-project/<br>├── CLAUDE<span class="hljs-selector-class">.md</span>                    ← 团队共享指令，提交到 git<br>├── CLAUDE<span class="hljs-selector-class">.local</span><span class="hljs-selector-class">.md</span>              ← 个人覆盖，被 git 忽略<br>└── .claude/<br>    ├── settings<span class="hljs-selector-class">.json</span>            ← 权限 + 配置，提交到 git<br>    ├── settings<span class="hljs-selector-class">.local</span><span class="hljs-selector-class">.json</span>      ← 个人权限，被 git 忽略<br>    ├── commands/                ← 自定义斜杠命令<br>    │   ├── review<span class="hljs-selector-class">.md</span>            →  /project:review<br>    │   ├── fix-issue<span class="hljs-selector-class">.md</span>         →  /project:fix-issue<br>    │   └── deploy<span class="hljs-selector-class">.md</span>            →  /project:deploy<br>    ├── rules/                   ← 模块化指令文件（全局生效）<br>    │   ├── code-style<span class="hljs-selector-class">.md</span><br>    │   ├── testing<span class="hljs-selector-class">.md</span><br>    │   └── api-conventions<span class="hljs-selector-class">.md</span><br>    ├── skills/                  ← 自动调用的工作流<br>    │   ├── security-review/<br>    │   │   └── SKILL<span class="hljs-selector-class">.md</span><br>    │   └── deploy/<br>    │       └── SKILL<span class="hljs-selector-class">.md</span><br>    └── agents/                  ← 子代理角色定义<br>        ├── code-reviewer<span class="hljs-selector-class">.md</span><br>        └── security-auditor.md<br></code></pre></td></tr></tbody></table></figure><p>Claude的三种模式：</p><ul><li>Ask 模式：只看不动（只读分析）</li><li>Plan 模式：谋定而后动（只规划，不执行）</li><li>Edit 模式：直接执行（可写代码）</li></ul><h2 id="斜杠命令"><a href="#斜杠命令" class="headerlink" title="斜杠命令"></a>斜杠命令</h2><p>使用斜杠命令来控制Claude的行为：</p><p><img src="https://www.runoob.com/wp-content/uploads/2026/01/d29e985e-5cb1-43bc-b6fb-e3b290f83bf9.png" alt="img"></p><h2 id="CLAUDE-md"><a href="#CLAUDE-md" class="headerlink" title="CLAUDE.md"></a>CLAUDE.md</h2><p>Claude Code每次关闭就会忘记一切，所以需要Claude来让它记住重要的东西</p><p><code>CLAUDE.md</code> 是 Claude Code 中最重要的配置文件，用于向 Claude 传递项目级别的持久指令。每次启动 Claude Code 会话时，它都会自动读取并加载这个文件中的内容，作为系统级上下文融入每一次对话中。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Claude-Code基础使用手册&quot;&gt;&lt;a href=&quot;#Claude-Code基础使用手册&quot; class=&quot;headerlink&quot; title=&quot;Claude Code基础使用手册&quot;&gt;&lt;/a&gt;Claude Code基础使用手册&lt;/h1&gt;&lt;blockquote&gt;
</summary>
      
    
    
    
    <category term="Vide coding" scheme="https://blog.willmo.top/categories/Vide-coding/"/>
    
    
    <category term="Vibe coding" scheme="https://blog.willmo.top/tags/Vibe-coding/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第七章：网络安全</title>
    <link href="https://blog.willmo.top/posts/Chapter%207%20of%20Computer%20Networks:%20Network%20Security/"/>
    <id>https://blog.willmo.top/posts/Chapter%207%20of%20Computer%20Networks:%20Network%20Security/</id>
    <published>2026-04-20T11:49:59.570Z</published>
    <updated>2026-04-20T11:50:00.563Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第七章：网络安全"><a href="#第七章：网络安全" class="headerlink" title="第七章：网络安全"></a>第七章：网络安全</h1><p>分为三方面内容：</p><ul><li>安全威胁</li><li>安全服务</li><li>安全机制</li></ul><h4 id="安全威胁"><a href="#安全威胁" class="headerlink" title="安全威胁"></a>安全威胁</h4><ul><li>被动攻击</li><li><ul><li>攻击者通过窃听手段仅观察和分析网络中传输数据流中的敏感信息，而不对其进行干扰。</li></ul></li><li>主动攻击</li><li><ul><li>攻击者对网络中传输着的数据流进行各种处理，比如中断，篡改，伪造，恶意程序，拒绝服务等等。</li></ul></li></ul><p>**拒绝服务(Denial of Service,DoS)**攻击基于大量的恶意请求抢占服务器资源，使得服务器难以向正常用户提供服务。</p><p><img src="https://files.seeusercontent.com/2026/04/15/A6ce/image-20260415204651230.png" alt="image-20260415204651230"></p><p>攻击者能够自行租用黑服务器攻击，也能够使用病毒控制僵尸主机来攻击。</p><p>交换机中毒：攻击者大量伪造了不同源MAC地址的帧，填满转发表使其无法正常工作。</p><p><img src="https://files.seeusercontent.com/2026/04/15/U5zp/image-20260415204842166.png" alt="image-20260415204842166"></p><p>被动攻击不涉及对数据的更改，很难被发现。对付被动攻击主要采用各种数据加密技术进行预防而不是主动检测。</p><p>主动攻击容易检测。对付主动攻击除要采取数据加密技术、访问控制技术等预防措施，还需要采取各种检测技术及时发现并阻止攻击，同时还要对攻击源进行追踪，并利用法律手段对其进行打击。</p><p>为了防止上述各种安全威胁，一个安全的计算机网络应设法提供相应的安全服务</p><h3 id="安全服务"><a href="#安全服务" class="headerlink" title="安全服务"></a>安全服务</h3><p>包括六个服务：</p><ul><li>保密性</li><li><ul><li>确保网络中传输的信息只有其发送方和接收方才能懂得其含义，而信息的截获者则看不懂所截获的数据。显然，保密性是计算机网络中最基本的安全服务，也是对付被动攻击所必须具备的功能。</li></ul></li><li>报文完整性</li><li><ul><li>确保网络中传输的信息不被攻击者篡改或伪造，它在应对主动攻击时也是必不可少的。</li></ul></li><li>实体鉴别</li><li><ul><li>通信两端的实体能够相互验证对方的真实身份，确保不会与冒充者进行通信。目前频发的网络诈骗，多数情况都是由于在网络上不能鉴别出对方的真实身份。实体鉴别在对付主动攻击时是非常重要的。</li></ul></li><li>不可否认性</li><li><ul><li>用来防止发送方或接收方否认发送或接收过某信息。在电子商务中这是一种非常重要的安全服务。</li></ul></li><li>访问控制</li><li><ul><li>可以限制和控制不同实体对信息源或其他系统资源进行访问的能力。必须在鉴别实体身份的基础上对实体的访问权限进行控制。</li></ul></li><li>可用性</li><li><ul><li>确保授权用户能够正常访问系统信息和资源。很多攻击都会导致系统可用性的损失，拒绝服务DoS攻击就是可用性最直接的威胁。</li></ul></li></ul><p>实现以上服务的机制名为<strong>安全机制</strong>，将在接下来陆续介绍。</p><h3 id="密码学基本概念"><a href="#密码学基本概念" class="headerlink" title="密码学基本概念"></a>密码学基本概念</h3><p>将发送的数据变换成对任何不知道如何做逆变换的人都不可理解的形式，从而保证数据的机密性，这种变换称为加密(Encryption)。</p><ul><li>加密前的数据被称为明文(Plaintext)。</li><li>加密后的数据被称为密文(Ciphertext)。</li></ul><p>通过某种逆变换将密文重新变换回明文，这种逆变换称为解密(Decryption)。</p><p>加密和解密过程可以使用密钥(Key)作为参数。</p><ul><li>密钥必须保密，但加密和解密的过程可以公开。</li><li>只有知道密钥的人才能解密密文，否则即使知道加密或解密算法也无法解密密文。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/laA0/image-20260415205837470.png" alt="image-20260415205837470"></p><p>加密密钥和解密密钥可以相同，也可以不同（即使不同，这两个密钥也必然有某种相关性）这取决于采用的是对称密钥密码体制还是公开密钥（公钥）密码体制。</p><p>如果不论攻击者截获了多少密文，在密文中都没有足够的信息来唯一地确定出对应的明文，则这一密码体制称为无条件安全的，或称<br>为理论上是不可破的。</p><p>然而，在无任何条件限制下，目前几乎所有实用的密码体制均是可破的。因此，人们关心的是在计算上（而不是理论上）是不可破的密码体制如果一个密码体制中的密码不能在一定时间内被可以使用的计算资源破解，则这一密码体制称为在计算上是安全的。</p><p>比如，计算核弹发生密码所需要的时间是623年，那就没有意义了。</p><p>一般情况下，通过使用长的密钥可以有效增加破解密文的难度，但同时也使得加密方和解密方的计算量加大</p><h3 id="对称密钥密码体制"><a href="#对称密钥密码体制" class="headerlink" title="对称密钥密码体制"></a>对称密钥密码体制</h3><p>对称密钥密码体制是指加密密钥与解密密钥相同的密码体制。</p><p><img src="https://files.seeusercontent.com/2026/04/15/djT8/image-20260415210331618.png" alt="image-20260415210331618"></p><h4 id="数据加密标准DES"><a href="#数据加密标准DES" class="headerlink" title="数据加密标准DES"></a>数据加密标准DES</h4><p>由IBM公司研制并于1977年被美国定为联邦信息标准的数据加密标准(Data Encryption Standard,DES),就是对称密钥密码体制的典型代表。国际标准化组织曾把DES作为数据加密标准。</p><p><img src="https://files.seeusercontent.com/2026/04/15/7Xqm/image-20260415210447373.png" alt="image-20260415210447373"></p><p>DES的保密性仅取决于对密钥的保密和密钥的长度，而算法是公开的。</p><p>20世纪70年代设计的DES,经过了世界上无数优秀学者几十年的密码分析，除了其56比特密钥长度太短，没有发现其任何大的设计缺陷。<br>56比特长的密钥，可用密钥数量为25^6（约7.6×1016）。</p><p>现在对于56比特的DES密钥的搜索已成常态，<strong>56比特DES已不再被认为是安全的.</strong></p><h4 id="三重DES算法"><a href="#三重DES算法" class="headerlink" title="三重DES算法"></a>三重DES算法</h4><p>为了解决56l比特DES密钥太短的问题，学者们提出了三重DES(Triple DES,3DES)。3DES在1985年成为美国的一个商用加密标准RFC2420]。</p><ul><li>3DES使用3个密钥执行三次DES算法。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/6uzK/image-20260415210759842.png" alt="image-20260415210759842"></p><p>BM最初设计DES时主要考虑用硬件来实现，因此DES/3DES的软件实现较慢。3DES目前正在被2001年发布的高级加密标准AES所替代。</p><h4 id="高级加密标准AES"><a href="#高级加密标准AES" class="headerlink" title="高级加密标准AES"></a>高级加密标准AES</h4><p>高级加密标准(Advanced Encryption Standard,AES)支持128比特、192比特和256比特的密钥长度，用硬件和软件都可以快速实现。</p><p>AES不需要太多内存，因此适用于小型移动设备。</p><h3 id="公钥密码体制"><a href="#公钥密码体制" class="headerlink" title="公钥密码体制"></a>公钥密码体制</h3><p>在对称密钥密码体制中，加解密双方要共享同一个密钥。</p><p>但是这个约定实际上是很难完成的，如果：</p><ul><li>双方事先约定</li><li><ul><li>给密钥的管理和更换带来极大的不便</li></ul></li><li>用信使来传送密钥</li><li><ul><li>对于高度自动化的大型计算机网络，使用信使来传送密钥显然是不合适的</li></ul></li></ul><p>尽管使用复杂但高度安全的密钥分配中心(Key Distribution Center,KDC)可以解决该问题，但是采用公钥密码体制可以比较容易地解决该问题。</p><p><strong>公钥密码体制使用不同的加密密钥和解密密钥</strong>，<strong>这是非对称的密钥密码体制</strong>，其概念是由Stanford,大学的研究人员Diffie和Hellman于1976年提出的。</p><ul><li>加密密钥是向公众公开的，称为公钥(Public Key,PK)。</li><li>解密密钥是需要保密的，称为私钥或密钥(Secret Key,SK)。</li><li>加密算法E和解密算法D都是公开的。</li><li>尽管SK由PK决定，但却不能根据PK计算出SK。</li></ul><p>公钥密码体制的加密解密过程的一般表示式：</p><p>$$<br>\boldsymbol{D}<em>{S \boldsymbol{K}} \big( \boldsymbol{E}</em>{\boldsymbol{P} \boldsymbol{K}} ( \boldsymbol{X} ) \big)=\boldsymbol{X}<br>$$</p><p>加密和解密运算可以对调：</p><p>$$<br>\boldsymbol{E}<em>{\boldsymbol{P} K} \big( \boldsymbol{D}</em>{\boldsymbol{S} K} ( \boldsymbol{X} ) \big)=\boldsymbol{X}<br>$$</p><p>但是PK只能用来加密，而不能用来解密：</p><p>$$<br>D_{P K} ! \left( \boldsymbol{E}_{P K} ( \boldsymbol{X} ) \right) \neq\boldsymbol{X}<br>$$</p><p>由于从公钥PK不能推导出私钥SK,并且PK不能用来解密，因此PK可以是公开的。</p><p>显然，采用公钥密码体制更易解决密钥分发的问题。</p><p><img src="https://files.seeusercontent.com/2026/04/15/Nuf6/image-20260415211708141.png" alt="image-20260415211708141"></p><p>公钥密码体制不仅用于加密，还可以很方便地用于鉴别和数字签名。<br>目前的公钥密码算法比对称密钥密码算法慢好几个数量级。</p><ul><li>公钥密码体制通常用于会话密钥的建立；</li><li>对称密钥密码体制被用于其他大多数情况下的加密。</li></ul><p>来看看过程：</p><p><img src="https://files.seeusercontent.com/2026/04/15/jjT9/image-20260415212250650.png" alt="image-20260415212250650"></p><p>任何加密方法的安全性取决于密钥的长度和攻破密文所需的计算量，而不是简单地取决于加密体制。因此不能简单地判断，对称密钥密码体制与公钥密码体制相比，哪个密码体制的安全性更好。如果存在曼德尔超算，那算出密码的时间比密码失去作用的时间更短的话，那任何加密算法都不是安全的。</p><h3 id="报文摘要和报文鉴别码"><a href="#报文摘要和报文鉴别码" class="headerlink" title="报文摘要和报文鉴别码"></a>报文摘要和报文鉴别码</h3><p>如果报文被攻击者篡改或伪造，则报文就不具备完整性。</p><p>对报文进行完整性验证就是进行报文鉴别，也就是鉴别报文的真伪。</p><p>使用加密技术通常就可以达到报文鉴别的目的，因为被篡改的报文解密后一般不能得到可理解的内容。</p><ul><li>然而，对于不需要保密而只需要报文鉴别的网络应用（例如从因特网的某个网站下载一个应用软件，用户只关心该软件是否与官方发布的一致，以防范中间人安装包替换攻击)，对整个报文进行加密和解密，会使计算机花费相当多的CPU时间。</li><li>使用报文摘要(Message Digest,MD)进行报文鉴别是一种更有效的方法。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/i6Wz/image-20260415212705869.png" alt="image-20260415212705869"></p><p>使用报文摘要对报文进行鉴别的好处是：只对长度固定且比整个报文长度短得多的报文摘要H（m）进行加密，比对整个报文m进行加密要简单得多。</p><p>密钥K仅在通信双方之间共享，没有第三方能用伪造报文产生出使用密钥K加密的伪造报文摘要。</p><p>附加在报文后面用于报文鉴别的码串(即加密后的报文摘要)，称为报文鉴别码(Message Authentication Code,MAC)</p><h4 id="密码散列函数"><a href="#密码散列函数" class="headerlink" title="密码散列函数"></a>密码散列函数</h4><p>报文摘要MD实际上与之前介绍过的帧检验序列（第3章）、首部检验和（第4章）等都是散列函数(Hash Function)的一种应用，用于接收方对收到的数据进行检查以便发现是否有误码。</p><p>散列函数(哈希函数)具有以下特点:</p><ul><li>散列函数的输入长度是可变的，并且可以很长，但其输出长度是固定的，并且较短。散列函数的输出称为散列值，也可简称为散列。</li><li>不同的散列值对应不同的输入，但不同的输入却可能得出相同的散列值。换句话说，<strong>散列函数的输入和输出并非一一对应，而是多对一的。</strong></li></ul><p>为了满足安全性，报文摘要算法必须满足：</p><ul><li>对于任意给定的某个报文摘要值H(x),若想找到一个报文y使得H(y)=H(x),在计算上是不可行的。</li><li>若想找到任意两个报文x和y,使得H(y)=H(x),在计算上是不可行的。</li></ul><p>满足上述两个条件表明：对于发送方产生的报文和其相应的报文摘要H(x),攻击者不可能伪造出另一个报文y,使得y与x具有同样的报文摘要。</p><p>满足上述条件的散列函数就称为<strong>密码散列函数或安全散列函数。</strong></p><p>密码散列函数实际上是一种单向函数，由于无法通过报文摘要还原出原文，因此可把密码散列函数运算看作没有密钥的加密运算。</p><p>最有名的报文摘要算法（或称密码散列函数或散列算法）有MD5 (Message Digest,MD-5)和安全散列算法(Secure Hash Algorithm,SHA-1)</p><p>但是2004年，王小云证明能够用系统方法找出一对报文能够生成相同的md5摘要，仅需小于1小时的时间，这使得md5被安全散列算法SHA-1替代。</p><p>尽管SHA1比MD5安全，但后来也被证明其实际安全性并未达到设计目标，并且也曾被王小云的研究团队攻破。</p><p>尽管现在SHA-1仍在使用，但很快就会被SHA-2和SHA-3所替代。</p><p>利用密码散列函数的特殊性质，可以设计出无需对报文摘要进行加密的报文鉴别方法，只要通信双方共享一个称为<strong>鉴别密钥</strong>的秘密比特串即可。</p><p>直接使用密码散列函数实现报文鉴别的技术又称为散列报文鉴别码(Hashed MAC,HMAC)。</p><p><img src="https://files.seeusercontent.com/2026/04/15/lIn8/image-20260415213722117.png" alt="image-20260415213722117"></p><p>攻击者不知道仅由通信双方共享的鉴别密钥s,也不能从截获的散列报文鉴别码HMAC中计算出s,因此不能伪造报文m’产生（m’+s)的HMAC。</p><h3 id="数字签名"><a href="#数字签名" class="headerlink" title="数字签名"></a>数字签名</h3><p>我们日常生活中的书信或文件可以依据亲笔签名或印章来证明其真实来源。</p><p>类似地，在计算机网络中传送的报文可以使用数字签名(Digital Signature)来证明其真实来源。</p><p>数字签名必须保证以下三点：</p><ul><li>接收方能够核实发送方对报文的数字签名。</li><li>接收方在内的任何人都不能伪造对报文的数字签名。</li><li>发送方事后不能抵赖对报文的数字签名。</li></ul><p>对于事后追责，法律站在我们这边，但是法律发挥作用的前提是有不可磨灭的证据。</p><p><img src="https://files.seeusercontent.com/2026/04/16/L0rw/image-20260416103408861.png" alt="image-20260416103408861"></p><p>报文由A使用解密密钥SK对报文进行解密并生成数字签名，随后将密文发送给接收方B，B再根据自己的私钥将报文还原。</p><p>为什么报文没有经过加密就使用解密密钥SK解密呢？没有关系，加密解密的区分无非是顺序上的不同，实际上他们都是一个数学计算过程，都能够将初始数据转化为不可读取的杂乱无章的内容。</p><p>由于除A外的其他任何人都没有A的私钥SK,所以除A外没有其他任何人能产生带有数字签名的报文,而任何伪造的报文经过接收方用A的公钥运算后都不会得到可理解的内容。因此，B就通过这种方式核实了报文m的确是A签名发送的。</p><p>在非对称加密中，是<strong>他人用你的公钥加密，你用私钥解密</strong>；而在数字签名中，是<strong>你用私钥签名，他人用你的公钥验证</strong>。</p><p>如果A要抵赖曾发送报文m给B,B可以把报文m和带有数字签名的报文出具给第三方。第三方很容易用A的公钥PK,去证实A确实发送过报文给B。<br>反之，若B把报文m伪造成m’,则B不能在第三方前出示带有数字签名的报文,因为B不可能真有A的私钥SK。这样就证明B伪造了报文。</p><p>很显然，数字签名实现了对报文来源的鉴<br>别。</p><p>实际上，需要说明的是，由于公钥密码算法的计算代价非常高，对整个报文进行数字签名是件非常耗时的事情，更有效的方法是<strong>仅对报文摘要</strong>进行数字签名。</p><p>另外，上述过程只对报文进行了签名，但是没有实行加密。A的公钥PK是公开的，这意味着任何攻击者都能够在截获到报文后用公钥将其还原成有意义的字符：</p><p><img src="https://files.seeusercontent.com/2026/04/16/g5xC/image-20260416104524331.png" alt="image-20260416104524331"></p><p>所以我们需要带加密的数字签名</p><h4 id="具有保密性的数字签名"><a href="#具有保密性的数字签名" class="headerlink" title="具有保密性的数字签名"></a>具有保密性的数字签名</h4><p>实际上这并不难，与前面的非对称数字加密联合就可以。</p><p><img src="https://files.seeusercontent.com/2026/04/16/u0tP/image-20260416104755609.png" alt="image-20260416104755609"></p><p>由于其他人无法知道接收方B的私钥，所以截获了报文也无法将其解密。</p><h3 id="实体鉴别"><a href="#实体鉴别" class="headerlink" title="实体鉴别"></a>实体鉴别</h3><p>实体鉴别就是通信双方的一方验证另一方身份的技术，常简称为鉴别。</p><ul><li>实体可以是人、客户进程或服务器进程。</li></ul><p>下面仅介绍如何鉴别通信对端实体的身份，也就是验证通信的对方确实是所要通信的实体，而不是其他伪装者。</p><ul><li>通信实体的鉴别通常是两个通信实体之间传输实际数据之前或进行访问控制之前的必要过程，这是很多安全协议的重要组成部分。</li></ul><p>实体鉴别的最简单方法就是使用用户名和口令。为了应对用户名和口令被攻击者截获的安全威胁，需要对用户名和口令进行加密。但是这样会导致重放攻击(Replay Attack)：</p><p><img src="https://files.seeusercontent.com/2026/04/16/frW5/image-20260416105514125.png" alt="image-20260416105514125"></p><p>攻击者根本无须解密报文，而是能够直接将加密后的密文发送给B，随后B将C认为是A，C就能够伪装A的身份收到B发送的数据。</p><p>为了抵御重放攻击，可以用不重数(nonce)，这是一个不重复使用的大随机数。这被称为”挑战-响应协议”。</p><p><img src="https://files.seeusercontent.com/2026/04/16/H4rn/image-20260416110054001.png" alt="image-20260416110054001"></p><p>A随机生成一个不重数RA，随后将A的用户名和RA加密后传输到B，B接收到后再生成一个随机不重数RB，将RA，RB和数据封装在一个报文中传回A，A用共享密钥解出RA，这就证明了B的身份(只有成功解密的主机才能得知RA，才能在其他的报文中加入RA)。</p><p>随后A将RB和数据封装在报文中发送给B，B收到后用共享密钥解密，就验证了A的身份。之后就能够开始数据传输了。</p><p>由于每次会话必须使用不同的不重数，所以攻击者在进行重放攻击时无法重复使用所截获的不重数，因而也就无法伪装成A或B。</p><p>这个协议通过“<strong>谁能正确说出我刚生成的秘密随机数，谁就拥有共享密钥</strong>”的逻辑，实现了双向身份验证。</p><p>实际上使用公钥密码体制也可以实现实体鉴别。</p><p><img src="https://files.seeusercontent.com/2026/04/16/aQi1/image-20260416111611484.png" alt="image-20260416111611484"></p><p>A生成随机数RA，将其以<strong>明文</strong>的形式发给B，B接收到后，将其与自身的RB用私钥进行数字签名后发给A，A接收后发现<strong>能够使用公钥解密成有意义内容且其中含有RA</strong>这就能够确认B的身份。随后A用自己的私钥加密RB，B接收后用A的公钥解密发现是RB，这就确认了A的身份。随后即可进行数据传输。</p><p>这种基于不重数（随机挑战值）的公钥认证协议，其核心本质正是 <strong>“双方互相验证对方的数字签名”</strong>。</p><p>但是这么做依旧有被攻击的可能。</p><p>C是AB的中间人，AB之间的通信需要C的转发。</p><p><img src="https://files.seeusercontent.com/2026/04/16/h9tH/image-20260416113430524.png" alt="image-20260416113430524"></p><p>过程待补充</p><p>所以，公钥的分发以及公钥真实性的认证也是一个非常重要的问题，</p><h3 id="密钥分发"><a href="#密钥分发" class="headerlink" title="密钥分发"></a>密钥分发</h3><ul><li>在对称密钥密码体制中，通信双方共享的密钥是需要保密的。</li><li>在公钥密码体制中，公钥是公开的，但私钥仍然需要保密。</li><li>密钥分发是密钥系统中一个非常重要的问题。密钥必须通过安全的通路进行分发。</li></ul><p>密钥分早期使用信使来进行分发，但是现在规模大了，只能用网内密钥自动分发，就是通过网络自动分发秘钥。</p><p>对于对称密钥密码体制，目前常用的密钥分发方式是建立密钥分配中心(Key Distribution Center,KDC)。</p><p>KDC是一个公众都信任的机构，其任务就是给需要进行秘密通信的用户临时分配一个会话密钥。</p><p>【举例】假设用户A和B都是KDC的登记用户，他们在KDC登记时就分别拥有了与KDC通信的主密钥KA和KB。用户A和B通过KDC安全获得他们之间共享的、用于一次会话的密钥KAB的过程如下：</p><p><img src="https://files.seeusercontent.com/2026/04/16/X6bq/image-20260416114955852.png" alt="image-20260416114955852"></p><p>如图，A和B分别持有私钥：</p><p>A将想和B通信的请求以明文告知KDC，KDC返回一报文 ，该报文最外层用KA加密，内存KAB和一票据，票据用KB加密，内含KAB和A向B的通信请求。该报文发给A后，A通过私钥解密得到KAB，随后转发给B，B通过B私钥KB解密得到KAB，并得知A想和B通信。持有KAB的双方就能够开始通信。</p><p>注意：</p><ul><li>在网络上传送的密钥都是用于加密的密钥，并且需要加密后才能传送。解密用的密钥都不在网上传送。</li><li>KDC分配给用户的主密钥（例如上述例子中的KA和KB)应当定期更换以减少攻击者破译密钥的机会。</li><li>KDC可以在报文中加入时间戳，以防止报文的截获者利用之前已记录下的报文进行重放攻击。</li></ul><h4 id="公钥的分发"><a href="#公钥的分发" class="headerlink" title="公钥的分发"></a>公钥的分发</h4><p>公钥密码体制中的公钥是公开的，而私钥是用户私有的（保密的)</p><ul><li>如果每个用户都知道其他用户的公钥，用户之间就可以实现安全通信。</li><li>然而，如果通过网络来随意公布用户的公钥，会面临极大的安全风险。</li><li>因此，需要有一个值得信赖的机构将公钥与其对应的实体（人或机器）进行绑定(binding)。这种机构被称为认证中心(Certification Authority,CA),一般由政府出资建立。</li></ul><p>需要发布公钥的用户可以让CA为其公钥签发一个证书(Certification)，证书中包含有公钥及其拥有者的身份标识信息（人名、公司名或IP地址等）。</p><p>CA签发证书的过程如下：</p><ul><li>CA必须核实用户真实身份；</li><li>CA为用户产生公钥私钥对，并生成证书：</li><li>CA用自己的私钥对证书进行数字签名。</li><li>该证书就可以通过网络发送给任何希望与该证书拥有者通信的实体，也可将该证书存放在服务器由其他用户自由下载。</li><li>CA为用户产生的公钥私钥对中的私钥，必须由证书拥有者自己秘密保存。</li><li>任何人都可从可信的地方（例如代表政府的报纸）获取CA自身的公钥，并用这个公钥来验证某个证书是否是该C签发的真实证书。一旦证书被鉴别是真实的，则可以相信证书中的公钥确实属于证书中声称的用户。</li></ul><p>如果全世界仅使用一个CA来签发证书，则会出现负载过重和单点故障等问题。</p><p>因特网采用的是RFC5280](现在是建议标准)给出的、在全球范围内为所有因特网用户提供证书签发与认证服务的公钥基础结构(Public Key Infrastructure,PKI)</p><ul><li>下级CA的证书由上级CA签发和认证。</li><li>所有用户都信任顶级的根CA,但可以信任也可以不信任中间的CA。</li><li>用户可以在自己信任的CA获取个人证书，当要验证来自不信任CA签发的证书时，需要到上一级CA验证该证书的真伪，如果上一级CA也不可信则需要到更上一级CA进行验证，一直追朔到可信任的一级CA,这一过程最终有可能会追溯到顶级的根CA。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/kxB5/image-20260416120828045.png" alt="image-20260416120828045"></p><p>如win10可以运行<code>certmgr.msc</code>来查看证书：</p><p><img src="https://files.seeusercontent.com/2026/04/16/Gl9q/image-20260416121023167.png" alt="image-20260416121023167"></p><h3 id="访问控制"><a href="#访问控制" class="headerlink" title="访问控制"></a>访问控制</h3><p>访问控制是信息安全的核心机制，用于<strong>决定谁（用户或进程）在什么条件下可以访问哪些系统资源（如数据、文件、网络），以及能进行何种操作（如读、写、执行）</strong>。</p><p><img src="https://files.seeusercontent.com/2026/04/16/5Pmw/image-20260416151658839.png" alt="image-20260416151658839"></p><p>对合法用户访问权限的授予一般遵循最小特权原则。</p><ul><li>最小特权是指能够满足用户完成工作所需的权限，用户不会被赋予超出其实际需求的权限。</li><li>最小特权原则可以有效防范合法用户滥用权限所带来的安全风险。</li></ul><p>访问控制包含以下要素：</p><ul><li>主体：<strong>访问活动的发起者</strong>，可以是某个用户，也可以是代表用户执行操作的进程、服务和设备等。</li><li>客体：<strong>访问活动中被访问的对象</strong>，凡是可以被操作的信息、文件、设备、服务等资源都可以认为是客体。</li><li>访问：<strong>主体对客体的各种操作类型。</strong>例如创建、读取、修改、删除、执行、发送、接收等操作。不同的系统有不同的访问类型。</li><li>访问策略：<strong>访问控制的核心，访问控制根据访问策略限制主体对客体的访问。</strong>访问控制策略可用三元组(S、O、P)来描述，其中S表示主体，O表示客体，P表示许可(Permission)。许可P明确了允许主体S对客体0O所进行的访问类型。访问策略通常存储在系统的授权服务器中。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/9awW/image-20260416152005280.png" alt="image-20260416152005280"></p><p>如图，是著名的访问监控器（Reference Monitor）模型：</p><p>主体访问客体前，访问监视器会向授权服务器查询相应的访问策略并决定是否允许其访问。审计模块是该模型的必要补充，会记录访问主体，客体，类型，时间等信息留痕。系统管理员能够事后查看审计记录来定责。</p><p>管理员主要检查的信息有：</p><ul><li>哪些主体对哪些资源的访问请求被拒绝。</li><li>访问策略是否得到了严格执行。</li><li>提供访问活动的证据，为事后追查和追责提供依据。</li></ul><h4 id="三种典型的访问控制策略"><a href="#三种典型的访问控制策略" class="headerlink" title="三种典型的访问控制策略"></a>三种典型的访问控制策略</h4><ul><li>自主访问控制策略</li></ul><p>DAC策略中“自主”的意思是客体的拥有者可以自主地决定其他主体对其拥有的客体所进行访问的权限。</p><ul><li>优点：具有很强的灵活性</li><li>缺点：权限管理过于分散，容易出现漏洞，并且无法有效地控制被攻击主体破坏系统安全性的行为。</li></ul><p>木马程序利用自主访问控制策略的上述缺点，可以很容易地破坏系统的安全性。</p><p><img src="https://files.seeusercontent.com/2026/04/16/Ky8z/image-20260416152658787.png" alt="image-20260416152658787"></p><p>自主访问控制策略的最大特点是“自主”，也就是资源的拥有者对资源的访问策略具有决策权，因此是一种限制比较弱的访问控制策略。这种访问控制策略给用户带来灵活性的同时，也带来了安全隐患。</p><ul><li>强制访问控制策略</li></ul><p>强制访问控制策略与自主访问控制策略不同，它不允许一般的主体进行访问权限的设置。</p><p>在强制访问控制策略中，主体和客体被赋予一定的安全级别。通常只有系统的安全管理员可以进行安全级别的设定，而普通用户不能改变自己或任何客体的安全级别。</p><p>系统通过比较主体和客体的安全级别来决定某个主体是否能够访问某个客体，广泛使用以下两个原则：</p><ul><li>下读：主体的安全级别高于或等于客体才能读取客体，主体往下读取。</li><li>上写：客体安全级别高于或等于主体才能被写入，主体往上写入</li></ul><p>”下读”和“上写”原则限制了信息只能由低级别的对象流向高级别或同级别的对象，这样能够有效防止木马等恶意程序的窃密攻击。</p><p><img src="https://files.seeusercontent.com/2026/04/16/ekE6/image-20260416153312578.png" alt="image-20260416153312578"></p><p>攻击者B安全级别&lt;安全级别&lt;用户A的安全级别</p><p>病毒拥有用户A的安全级别，但是无法创建新的低级文件并写入，这就使得B无法通过木马读取到文件中的信息。</p><ul><li>基于角色的访问控制策略</li></ul><p>基于角色的访问控制策略的核心思想是：根据安全策略划分不同的角色，用户不再直接与许可关联，而是通过角色与许可关联。</p><ul><li>一个用户可以拥有多个角色，一个角色也可以赋予多个用户：</li><li>一个角色可以拥有多种许可，一种许可也可以分配给多个角色。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/g6Iu/image-20260416153512650.png" alt="image-20260416153512650"></p><p>角色与许可之间的关系比角色与用户的关系要更加稳定。</p><p>当一个用户的职责发生变化或需要为一个新的用户授权时，只要修改或设置用户的角色即可。</p><p>很显然，将用户和访问权限通过角色进行逻辑隔离能够减小授权的复杂性、增强权限的可管理性、减少因授权失误导致安全漏洞的风险。</p><h2 id="网络体系结构的各层采取的措施"><a href="#网络体系结构的各层采取的措施" class="headerlink" title="网络体系结构的各层采取的措施"></a>网络体系结构的各层采取的措施</h2><h3 id="物理层"><a href="#物理层" class="headerlink" title="物理层"></a>物理层</h3><p>在物理层实现通信数据的保密性和完整性的方法是对信道进行加密。</p><p>对信道进行加密需要使用信道加密机。</p><p><img src="https://files.seeusercontent.com/2026/04/16/6Mgg/image-20260416153929210.png" alt="image-20260416153929210"></p><p>使用信道加密技术可为通过链路的所有数据提供安全保护，并且对上层协议几乎没有任何影响，也就是具有很好的透明性。</p><ul><li>由于链路上传送的各种协议数据单元PDU的首部和数据载荷都被加密了，PDU的截获者无法从PDU中提取源地址和目的地址，因此可以防止各种形式的流量分析。</li></ul><p>由于信道加密机完全使用硬件加密技术，因此加密和解密速度快，并目无须传送额外的数据，采用该技术不会减少网络的有效带宽。</p><p>在网络互连的情况下，信道加密这种仅用于保护网络局部链路通信安全的技术不能确保端到端通信的安全，他的作用区域仅在一段链路，数据在路由器节点必须解密，这会导致风险</p><p>在实际应用中，通常只在容易被窃听的无线链路(例如卫星链路)上，或军用网络等专用网络的通信链路所在不安全区域的部分使用信道加密技术。</p><h3 id="数据链路层"><a href="#数据链路层" class="headerlink" title="数据链路层"></a>数据链路层</h3><p>随着802.11无线局域网(Wi-Fi)的广泛应用，其安全问题越来越受到关注。</p><p>在无线通信方式下，电磁波在自由空间辐射传播，任何无线终端都可在无线接入点的信号覆盖范围内接收其无线信号。</p><ul><li>若不采取相应安全措施，任何无线终端都可以接入到网络中，进而窃听网络通信或非法使用网络资源。</li></ul><p>因此802.11无线局域网的安全问题就显得尤为重要。802.11无线局域网主要在数据链路层为用户提供安全服务。有三个安全机制：</p><ul><li>SSID匹配机制</li><li><ul><li>用户给终端配置的SSID相同，才能接入，实际上没什么用。</li><li><img src="https://files.seeusercontent.com/2026/04/16/3yCy/image-20260416154537346.png" alt="image-20260416154537346"></li></ul></li><li>MAC地址过滤机制</li><li><ul><li>网络管理员能够在路由器配置允许接入网络的MAC地址列表，只有表中的MAC地址才能接入网络。一样没什么用。</li><li><img src="https://files.seeusercontent.com/2026/04/16/nC9j/image-20260416154658941.png" alt="image-20260416154658941"></li></ul></li><li>有线等效保密WEP机制</li><li><ul><li>网络管理员在AP上配置由AP和用户的无线终端共享的WEP密钥，显然是对称密码体制。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/3bTq/image-20260416154758234.png" alt="image-20260416154758234"></p><p>WEP并没有密钥分发机制，在鉴别过程和其后的数据通信过程中的所有通信都使用同一个共享密钥，并且所有接入到同一802.11无线局域网的无线终端都使用这同一个密钥。</p><p>另外，WEP采用的加密算法的强度较低。国内外众多研究人员已从理论和实际上都证明了WEP加密存在严重的安全隐患。</p><p>2004年，IEEE 802.11i提出，使用了更强的安全机制。</p><p><img src="https://files.seeusercontent.com/2026/04/16/cdK1/image-20260416154924585.png" alt="image-20260416154924585"></p><p><img src="https://files.seeusercontent.com/2026/04/16/8Viw/image-20260416155153992.png" alt="image-20260416155153992"></p><p>待补充</p><p>IEEE802.11提供了多种加密形式，其中包含WEP加密的强化版本临时密钥完整性协议(Temporal Key Integrity Protocol,TKIP)和基于高级加密标准AES的加密方案。</p><h3 id="网络层"><a href="#网络层" class="headerlink" title="网络层"></a>网络层</h3><p>IPSec是“IP Security”(IP安全)的缩写词，它是为因特网的网际层提供安全服务的协议族<br>(也有人不太严格地称其为”IPSect协议”)[RFC4301,RFC6071]。</p><p>IPSec并不限定用户使用何种特定的加密和鉴别算法，通信双方可以选择合适的算法和参数（例如，密钥长度)。</p><ul><li>为了保证互操作性，PSec也包含了一套加密算法，所有PSc的实现都必须实现这套加密算法。</li></ul><p>IPSec包含两种 不同的工作方式：</p><ul><li>运输方式</li><li>隧道方式</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/1xfF/image-20260416160112831.png" alt="image-20260416160112831"></p><p>​     无论使用上述哪种方式，所封装出的IP安全数据报的首部都是不加密的。这是为了使因<br>特网中的各路由器能够识别IP安全数据报首部中的相关信息，进而可以将P安全数据报在不<br>安全的因特网中从源点安全地转发到终点。</p><p>所谓”IP安全数据报”，是指数据报的数据载荷<br>是经过加密并能够被鉴别的。</p><p>在使用隧道方式传送P安全数据报之前，应当首先为通信双方建立一条网际层的逻辑连接（即安全隧道)，称为安全关联(Security Association,SA)。</p><p>这样，传统因特网无连接的网际层就变成了具有逻辑连接的一个层。</p><p>提供安全服务的安全关联SA是从源点到终点的单向连接。如果需要进行双向安全通信，则两个方向都需要建立SA。IP安全数据报就在这些SA中传送。</p><p><img src="https://files.seeusercontent.com/2026/04/16/7buI/image-20260416161010985.png" alt="image-20260416161010985"></p><p>如图，总部与分公司间建立起一道安全关联隧道SA。主机A的数据报经过R1被封装加密成IP安全数据报，经过SA运输到达R2，随后R2将其解密成原始IP数据报，并根据目的地址直接交付给B。</p><p><img src="https://files.seeusercontent.com/2026/04/16/Jx8a/image-20260416161434096.png" alt="image-20260416161434096"></p><p>建立安全关联SA的路由器或主机，需要维护这条SA的状态信息</p><ul><li>一个32位的连接标识符，称为安全参数索引(Security Parameter Index,SPI)</li><li>SA的源点和终点的P地址（图中路由器R1和主机B的P地址）</li><li>所使用的的加密类型（例如DES或AES)</li><li>加密的密钥</li><li>完整性检查的类型</li><li>鉴别使用的密钥</li></ul><h4 id="IP安全数据报格式"><a href="#IP安全数据报格式" class="headerlink" title="IP安全数据报格式"></a>IP安全数据报格式</h4><p>在PSec协议族中有两个主要的协议：鉴别首部<br>(Authentication Header,.AH)协议和封装安全有效载荷(Encapsulation Security Payload,ESP)协议。</p><ul><li>AH协议提供源点鉴别和数据完整性服务，但不能提供保密性服务。</li><li>ESP协议比AH协议复杂得多，它提供源点鉴别，数据完整性和保密性服务。</li></ul><p>IPSec既支持IPv4,也支持Pv6。在IPv6中，AH和ESP都是扩展首部的一部分。</p><p>由于AH协议的功能都已包含在ESP协议中，因此使用ESP协议就无须使用AH协议。下面不再介绍AH协议，而只介绍ESP协议的要点。</p><p>使用ESP或AH协议的IP数据报称为IP安全数据报（或IPSec数据报），它可以在两台主机之间、两台路由之间或一台主机和一台路由器之间的安全关联SA(安全隧道)中传送。</p><p><img src="https://files.seeusercontent.com/2026/04/16/r8eB/image-20260416162241381.png" alt="image-20260416162241381"></p><p><img src="https://files.seeusercontent.com/2026/04/16/D8cf/image-20260416162416479.png" alt="image-20260416162416479"></p><p>若某个IP安全数据报在因特网中被某个攻击者截获，只要攻击者不知道该P安全数据报的密码：</p><ul><li>攻击者就仅能知道该P安全数据报的首部信息（例如源P地址和目的P地址），但无法看懂其数据载荷的含义。</li><li>即使攻击者故意删除了该P安全数据报的数据载荷中的一些字节，由于接收端能够进行完整性验证，因此不会接受这种含有差错的数据报。</li><li>如果攻击者尝试重放攻击，但由于P安全数据报使用了有效的序号，因此使得重放攻击也不能成功。</li></ul><h4 id="IPSec的其他构件"><a href="#IPSec的其他构件" class="headerlink" title="IPSec的其他构件"></a>IPSec的其他构件</h4><table><thead><tr><th>安全关联数据库 （SecurityAssociationDatabase,SAD)</th><th>发送IP安全数据报的实体（路由器或主机）使用安全关联数据库 SAD来存储可能要用到的很多条安全关联SA。</th></tr></thead><tbody><tr><td>安全策略数据库 (Security Policy Database, SPD)</td><td>安全策略数据库SPD指明了什么样的IP数据报需要进行IPSec处理。</td></tr><tr><td>因特网密钥交换协议 (Internet Key Exchange, IKE)</td><td>因特网密钥交换IKE协议为IP安全数据报自动创建安全关联SA（而 无需人工配置）。</td></tr></tbody></table><h3 id="运输层"><a href="#运输层" class="headerlink" title="运输层"></a>运输层</h3><p>举一个最常见的例子，网上购物，计算机需要提供以下基本服务：</p><ul><li>顾客与销售商之间需要相互鉴别，以防止冒充者</li><li>要确保顾客与销售商之间的报文（例如购物清单和账单)在传输过程中没有被篡改。</li><li>要确保顾客与销售商之间的敏感信息<br>(例如顾客的信用卡号)不被冒充者窃听。</li></ul><p>要实现上述这些安全服务，就需要使用运输<br>层的安全协议，有以下两个：</p><ul><li>安全套接字层SSL</li><li>运输层安全TLS</li></ul><p>作用于应用层与运输层之间，可看作以下层次结构：</p><p><img src="https://files.seeusercontent.com/2026/04/16/zdA3/image-20260416163842665.png" alt="image-20260416163842665"></p><h4 id="安全套接字SSL"><a href="#安全套接字SSL" class="headerlink" title="安全套接字SSL"></a>安全套接字SSL</h4><p>SSL提供以下三个服务：</p><ul><li>SSL服务器鉴别</li><li><ul><li>支持SSL的客户端通过验证来自服务器的证书，来鉴别服务器的真实身份并获得服务器公钥</li></ul></li><li>SSL客户鉴别</li><li><ul><li>用于服务器证实客户身份，这是SSL的可选安全服务</li></ul></li><li>加密的SSL会话</li><li><ul><li>加密客户和服务器之间传送的所有报文，并检测报文是否被篡改。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/tI4g/image-20260416164954321.png" alt="image-20260416164954321"></p><h3 id="应用层"><a href="#应用层" class="headerlink" title="应用层"></a>应用层</h3><p>相较于计算机网络体系结构的其他各层，在应用层实现安全服务相对简单。</p><p>TCP体系结构的应用层中包含大量协议，本节课以电子邮件相关安全协议为例进行介绍。</p><p>PGP(Pretty Good Privacy）是已被广泛应用的，为电子邮件提供加密、鉴别、电子签名和压缩等技术的电子邮件安全软件包，它是Zimmermann于1995年开发的。</p><ul><li>PGP使用对称密钥和公钥的组合进行加密，为电子邮件提供保密性。</li><li>PGP通过报文摘要和数字签名技术为电子邮件提供完整性和不可否认性。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/7kYl/image-20260416165333584.png" alt="image-20260416165333584"></p><p>接收方：</p><p><img src="https://files.seeusercontent.com/2026/04/16/Hsf5/image-20260416165406243.png" alt="image-20260416165406243"></p><ul><li>邮件的收发双方获取对方公钥的最安全的方法是双方面对面直接交换公钥，但在大多数情况下这并不现实。</li><li>一般的做法是通过认证中心CA签发的证书来验证公钥持有者的合法身份。然而，PGP并没有要求使用CA，而允许使用一种第三方签署的方式来解决该问题。</li><li>需要说明的是，用户发布其公钥的最常见方式，还是通过电子邮件进行分发或把公钥发布在他们的个人网页上。具体采用哪种方式发布自己的公钥，取决于用户对安全性的要求。</li></ul><h3 id="防火墙访问控制和入侵监测系统"><a href="#防火墙访问控制和入侵监测系统" class="headerlink" title="防火墙访问控制和入侵监测系统"></a>防火墙访问控制和入侵监测系统</h3><p><img src="https://files.seeusercontent.com/2026/04/16/kzE3/image-20260416192350461.png" alt="image-20260416192350461"></p><p>上述说到的安全机制无法解决的问题是：</p><ul><li>非法用户利用系统漏洞进行未授权登录</li><li>授权用户非法获取更高级别的权限</li><li>通过网络传播病毒、蠕虫和特洛伊木马</li><li>阻止合法用户正常使用服务的拒绝服务攻击</li></ul><p>解决措施是防火墙和入侵监测系统。</p><h4 id="防火墙"><a href="#防火墙" class="headerlink" title="防火墙"></a>防火墙</h4><p>防火墙（Firewall）属于一种访问控制技术，具体实现为一种可编程的特殊路由器。</p><ul><li>防火墙把一个单位（机构或公司）的内部网络与其他网络（一般为因特网）进行安全隔离。</li><li>根据防火墙中配置的访问控制策略，某些分组允许通过防火墙，而某些分组则被禁止通过。</li><li>访问控制策略由使用防火墙的单位根据自己的安全需要自行制定。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/p6lI/image-20260416192611759.png" alt="image-20260416192611759"></p><p>实现防火墙的设备有2种：</p><ul><li>分组过滤路由器</li></ul><p>分组过滤路由器是一种具有分组过滤功能的路由器，它根据所配置的分组过滤规则对出入内部网络的分组执行转发或丢弃（即分组过滤）</p><p>分组过滤规则所采用的各参数来自分组的网际层和/或运输层首部中的某些字段的值。</p><ul><li>网际层：源IP地址和目的IP地址</li><li>运输层：源端口和目的端口、以及协议类型(TCP或UDP)</li></ul><p>如图所示：</p><p><img src="https://files.seeusercontent.com/2026/04/16/zH2y/image-20260416192909366.png" alt="image-20260416192909366"></p><p>FTP文件传输协议使用熟知端口号21来进行，防火墙拦截21端口后，内部主机将无法从不信任的外部FTP服务器中下载任何文件。</p><p>通常，分组过滤规则以访问控制列表(Access Control List,ACL)的形式存储在分组过滤路由器中。</p><ul><li>网络管理人员可以通过命令行或图形界面来配置ACL中的规则。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/Wb6s/image-20260416193142688.png" alt="image-20260416193142688"></p><p>分组过滤路由器简单高效，对用户是透明的（用户感觉不到分组过滤路由器的存在）。<br>然而，分组过滤路由器：</p><ul><li>不能对应用层数据进行过滤。</li><li>不能禁止某个用户对某个特定网络应用进行某个特定的操作</li><li>不支持应用层用户鉴别。</li></ul><p>这就需要应用网关来实现</p><hr><ul><li>应用网关</li></ul><p>应用网关又称为代理服务器(Proxy Server）),它可以实现应用层数据的过滤和高层用户的鉴别。</p><p>当用户通过应用网关访问内网或外网资源时，应用网关可以要求用户进行身份鉴别，然后根据用户身份对用户做出相应的访问控制。</p><p>应用网关也有一些局限：</p><ul><li>每种网络应用都需要一个专用的应用网关。当然，多个不同的应用网关可以运行在同一台主机上。</li><li>在应用层处理和转发报文，处理负担较重。</li><li>应用网关对应用程序是不透明的，用户需要在应用程序客户端指明应用网关（代理服务器）的地址。</li></ul><p>实际上，这两个常常被配合使用：</p><p><img src="https://files.seeusercontent.com/2026/04/16/Nmd6/image-20260416193741046.png" alt="image-20260416193741046"></p><p>防火墙亦有分类：</p><ul><li>网络防火墙</li><li><ul><li>保护内部网络安全，由网络管理员进行配置和使用</li></ul></li><li>个人防火墙</li><li>仅保护单台计算机，由用户为其配置分组过滤规则，操作系统自带功能较弱的防火墙。</li></ul><p>尽管防火墙提高了内部网络的安全防护程度，但仍存在以下局限：</p><ul><li><p>对防火墙的配置是否正确和完善，在很大程度上决定了防火墙可以发挥的安全防护作用。</p></li><li><p>防火墙对恶意代码（病毒、木马等）的查杀能力非常有限，因此不能有效地防止恶意代码通过网络的传播。</p><ul><li>由于查杀恶意代码的计算开销非常大，若提高防火墙的查杀力度，则会降低防火墙的处理速度，进而降低用户的网络带宽。</li></ul></li><li><p>防火墙对于一些利用系统漏洞或网络协议漏洞进行的攻击是无法防范的。</p></li><li><ul><li>攻击者通过分组过滤规则中允许的端口对某个服务器的漏洞进行攻击，这对于一般的分组过滤路<br>由器是无法防护的。</li><li>即使使用应用网关，也必须具有能够识别该特定漏洞的应用网关才能阻断攻击。</li></ul></li><li><p>防火墙技术自身存在不足。</p></li><li><ul><li>分组过滤路由器不能防止P地址和端口号欺骗。<ul><li>应用软件自身也可能有软件漏洞而存在被渗透攻击的风险。</li></ul></li></ul></li></ul><h4 id="入侵检测系统"><a href="#入侵检测系统" class="headerlink" title="入侵检测系统"></a>入侵检测系统</h4><p><img src="https://files.seeusercontent.com/2026/04/20/obD2/licensed-image.jpg"></p><p>在入侵已经开始但还未造成危害或在造成更大危害之前，及时检测到入侵并尽快阻止入侵，尽量把危害降到最小，就是非常有必要的。入侵检测系统(Intrusion Detection System,IDS)正是这样一种技术。</p><p>IDS对出入网络的分组执行深度检查，当检查到可疑分组时，会及时向网络管理员发出警报或进行阻断。</p><ul><li>由于IDS的误报率较高，因此一般情况下不建议对可疑分组进行自动阻断。</li></ul><p>IDS能够检测出：</p><ul><li>端口扫描</li><li>拒绝服务攻击</li><li>网络映射</li><li>恶意代码</li><li>系统漏洞</li></ul><p>入侵检测系统IDS分为两种：</p><ul><li>基于特征的入侵检测系统</li><li><ul><li>维护一个已知各类攻击的标志性特征的数据库</li><li>检测到与某种攻击特征匹配的分组或分组序列时，就判断可能出现了某种入侵行为。</li><li>标志性特征必须具有很好的区分度。</li><li>标志性特征一般由网络安全专家提供，由单位的网络管理员定制并将其加入到数据库中。</li></ul></li><li>基于异常的入侵检测系统</li><li><ul><li>通过观察正常运行的网络流量来学习正常网络流量的统计特性和规律</li><li>检测到网络流量的某种统计规律不符合正常情况时，则判断可能发生了入侵行为。</li><li>区分正常流量和统计异常流量是非常困难的。</li><li>现在很多研究致力于将机器学习方法应用于入侵检测，减少对网络安全专家的依赖</li></ul></li></ul><p>衡量入侵检测系统DS的重要依据:</p><ul><li>漏报率：未能检测出</li><li>误报率：对正常操作误报</li></ul><h3 id="常见网络攻击和防范"><a href="#常见网络攻击和防范" class="headerlink" title="常见网络攻击和防范"></a>常见网络攻击和防范</h3><h4 id="网络扫描"><a href="#网络扫描" class="headerlink" title="网络扫描"></a>网络扫描</h4><p>网络扫描是获取攻击目标信息的一种重要技术。</p><ul><li>攻击目标信息包括目的主机的P地址、操作系统、运行的程序及存在的漏洞等。</li><li>在进行网络攻击之前，对攻击目标的信息掌握得越全面和具体，就越能有效合理地制定出攻击策略和攻击方法，进而提高网络攻击的成功率。</li></ul><p>这是渗透测试的第一步，信息收集，网络扫描有四种类型：</p><ul><li>主机发现</li></ul><p>主机发现是指搜索要攻击的主机，这是对其进行攻击的前提。搜索要攻击的主机，实际上是要确定该目标主机的P地址。</p><p>进行主机发现的主要方法是利用网际控制报文协议ICMP。</p><p><img src="https://files.seeusercontent.com/2026/04/16/1orM/image-20260416195154497.png" alt="image-20260416195154497"></p><p>使用ping命令对某个lP地址范围内的所有P地址进行连通性测试，来发现正在运行的<br>目标主机。</p><p>为了防范，可以配置主机不对ICMP进行回复，或者配置防火墙阻断ICMP查询报文。</p><p>对策是，攻击者发送首部有误的IP数据报，在用户机引起错误之后让其传回ICMP差错报告报文，而ICMP差错报告报文一般是不会被防火墙拦截的，否则容易导致错误无法被报告和解决。</p><p><img src="https://files.seeusercontent.com/2026/04/16/bb7A/image-20260416195602338.png" alt="image-20260416195602338"></p><ul><li>端口扫描</li></ul><p>在进行主机发现确定了要攻击的目标主机后，攻击者可以进一步通过端口扫描来获取目标主机所有端口的工作状态，进而得出目标主机上开放了哪些网络服务。</p><p><img src="https://files.seeusercontent.com/2026/04/16/6Yzy/image-20260416195725053.png"></p><p><img src="https://files.seeusercontent.com/2026/04/16/f3Xs/image-20260416195745235.png" alt="image-20260416195745235"></p><ul><li>操作系统检测</li></ul><p>不同的操作系统所存在的安全漏洞可能有很多不同。要利用这些安全漏洞进行攻击，首先必须检测远程目标主机所使用的操作系统类型，通常有三种方法：</p><ul><li><ul><li>获取操作系统旗标信息：</li><li>在客户机与服务器建立连接<br>的过程中，服务器往往会返<br>回各种独特的欢迎信息。<br>攻击者可根据这些信息来推<br>断出服务器的操作系统类型</li></ul></li><li><ul><li>获取主机端口状态信息：</li><li>操作系统通常会默认开启<br>些常用的网络服务，这些服<br>务会打开各自特定的端口进<br>行网络监听。<br>攻击者可对目标主机进行端<br>口扫描，根据端口扫描结果<br>来推断目标主机的操作系统<br>类型</li></ul></li><li>获取TCP/IP协议栈指纹</li><li><ul><li>尽管RFC文档严格规定了各<br>种协议的三要素（语法、语<br>义和同步)，但并没有规定<br>各种协议的具体实现。<br>同一个协议在不同操作系统<br>中的实现细节可能会有所不<br>同，攻击者可据此推断目标<br>主机的操作系统类型。</li></ul></li></ul><table><thead><tr><th>操作系统</th><th>默认TTL值</th></tr></thead><tbody><tr><td>Linux (2.4 kernel)</td><td>255</td></tr><tr><td>Windows 10</td><td>128</td></tr><tr><td>MacOS X (10.5.6)</td><td>64</td></tr></tbody></table><table><thead><tr><th>操作系统</th><th>TCP窗口默认初始值</th></tr></thead><tbody><tr><td>Windows</td><td>8KB</td></tr><tr><td>Solaris</td><td>52KB</td></tr><tr><td>FreeBSD</td><td>发送=32KB，接收=56KB</td></tr></tbody></table><ul><li>漏洞扫描</li></ul><p>根据前面两个提供的信息，攻击者能够根据漏洞库查看该操作系统的主机和服务可能出现哪些漏洞来制定攻击方案。</p><h4 id="网络攻击防范"><a href="#网络攻击防范" class="headerlink" title="网络攻击防范"></a>网络攻击防范</h4><ul><li>仅打开确实需要使用的端口，关闭闲置和危险端口。</li><li>限制因特网（不可信网络）中的主机主动与内部网络(可信网络)中的主机进行通信。</li><li>设置防火墙，根据安全要求设置分组过滤策略（例如过滤不必要的CMP报文）</li><li>使用入侵检测系统IDS及时发现网络扫描行为和攻击者P地址，配置防火墙对来自该地址的分组进行阻断。<ul><li>网络扫描的行为特征是比较明显的，例如在短时间内对某一P地址范围内的每个地址和端口<br>发起连接等。</li><li>目前，大部分防火墙都具有识别简单的网络扫描行为的功能。</li><li>然而，很多攻击者也在研究如何隐蔽自己的网络攻击行为。例如利用虚假源地址、减缓扫描速度、动态调整扫描顺序、分布式扫描等。</li></ul></li></ul><h4 id="网络监听"><a href="#网络监听" class="headerlink" title="网络监听"></a>网络监听</h4><p>网络中传输的数据大部分都是明文形式。如果攻击者对网络进行监听并截获了包含有大量明文信息的一系列分组，则可从这些分组中直接分析出账号、密码等敏感信息。</p><p>分为三种：</p><ul><li>分组嗅探器</li></ul><p>分组嗅探器(Packet Sniffer)是一种网络监听工具软件，它运行在网络中的某台主机上，被动接收所有出入该主机的网络适配器（网卡）的数据链路层协议数据单元（即帧）</p><p>如果将网络适配器配置为混杂方式，网络适配器就会接收所有进入自己接口的MAC帧，而不管这些帧的目的MAC地址是否指向该网络适配器。</p><p><img src="https://files.seeusercontent.com/2026/04/16/f2xT/image-20260416200711824.png" alt="image-20260416200711824"></p><p>如此C就能够收到别人的数据包，但是交换器就不同了：</p><p><img src="https://files.seeusercontent.com/2026/04/16/6iqF/image-20260416200833667.png" alt="image-20260416200833667"></p><p>已经完成自学习的交换机不会将帧转发给C，这就需要交换机毒化攻击。</p><ul><li>交换机毒化攻击</li><li><img src="https://files.seeusercontent.com/2026/04/16/5But/image-20260416201156264.png" alt="image-20260416201156264"></li></ul><p>攻击者伪造大量MAC地址的帧发给以太网交换机，填满其转发表。让原来有效的转发表项被更新淘汰。</p><p>当有新的帧抵达时，在转发表中找不到该帧的地址对应的接口，交换机只能广播该单播帧，攻击机就能够获取到该帧。</p><ul><li>ARP欺骗</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/7oDr/image-20260416201340361.png" alt="image-20260416201340361"></p><p>如果网络中的所有主机都被攻击者进行了ARP欺骗，则攻击者就将自己伪装成了路由器，从而可以监听到网络中的所有通信，这对网络安全造成了极大的危害。</p><h4 id="网络监听防范措施"><a href="#网络监听防范措施" class="headerlink" title="网络监听防范措施"></a>网络监听防范措施</h4><ul><li>使用交换机替代集线器，这不但可以提高网络性能，还能使攻击者在交换机环境中更难实施监听。</li><li>禁用交换机的自学习功能，将P地址、MC地址与交换机的接口进行静态绑定，这样可以限制非<br>法主机的接入，使攻击者无法实施交换机毒化攻击，也使ARP欺骗难以实施。</li><li>对于ARP欺骗，主机或路由器可以仅使用静态ARP表，而不再依据ARP请求报文或响应报文动态更<br>新。</li><li>划分VLAN可以限制攻击者的监听范围，因为分组嗅探器只能在单个局域网范围内进行。</li><li>防范网络监听的最有效的方法是进行数据加密和实体鉴别技术。</li></ul><h4 id="拒绝服务攻击"><a href="#拒绝服务攻击" class="headerlink" title="拒绝服务攻击"></a>拒绝服务攻击</h4><p>拒绝服务DoS攻击是最容易实现却又最难防范的攻击手段。<br>类型有三种：</p><ul><li>基于漏洞的DoS攻击</li></ul><p>主要利用网络协议漏洞或操作系统漏洞。攻击者向目标系统发送一些特殊分组，使目标系统在处理这些分组时出现异常，甚至崩溃。这种攻击又称为剧毒包或杀手包(Killer Packet)攻击。</p><p><img src="https://files.seeusercontent.com/2026/04/16/ki2D/image-20260416201739644.png" alt="image-20260416201739644"></p><p>如，死亡之ping（Ping of Death），这是一种网络拒绝服务攻击，攻击者通过发送超过IP协议规定最大长度（65,535字节）的恶意ICMP数据包，导致目标系统因无法处理超大数据包而出现内存溢出、系统崩溃或重启等问题</p><ul><li>基于资源消耗的DoS攻击</li></ul><p>这种攻击是DoS攻击中采用最多的一种攻击。攻击者通过向目标系统发送大量的分组，从而耗尽目标系统的资源，致使目标系统崩溃而无法向正常用户提供服务。</p><p>如TCP SYN洪范攻击：</p><p><img src="https://files.seeusercontent.com/2026/04/16/nqH7/image-20260416202043203.png" alt="image-20260416202043203"></p><p>攻击者向目标服务器发送海量的TCP SYN包服务器收到SYN后，会为每个请求分配资源由于攻击者伪造了源IP地址，服务器发送的SYN-ACK包永远无法到达真实的客户端，也等不到最终的ACK回复。这些未完成的连接会长时间占用服务器的“半连接队列”，直到超时。当海量的伪造请求填满队列后，服务器就无法再处理任何新的合法连接请求，导致服务瘫痪。</p><p>当半连接的数量超过了系统允许的上限时，系统就不会再接受任何TCP连接请求，这将导致TCP服务器无法为正常客户提供服务。</p><p>还有另一种攻击方法：</p><p><img src="https://files.seeusercontent.com/2026/04/16/l9Uk/image-20260416202311441.png" alt="image-20260416202311441"></p><p>还有“反射攻击”</p><p>如Smurf攻击：</p><p><img src="https://files.seeusercontent.com/2026/04/16/Ve8q/image-20260416202407951.png" alt="image-20260416202407951"></p><p>主机A伪造主机C的地址，随后广播，将另外主机作为反射节点，引发其他主机对主机C群起而攻之，导致目标主机崩溃而无法对外提供服务。</p><p><img src="https://files.seeusercontent.com/2026/04/16/2nvB/image-20260416202532044.png" alt="image-20260416202532044"></p><p>反射攻击具有放大攻击流量的功能，网络中的主机越多，这种放大效果越好。</p><ul><li>分布式DoS攻击</li></ul><p><img src="https://files.seeusercontent.com/2026/04/16/4lhJ/image-20260416202649391.png" alt="image-20260416202649391"></p><h4 id="防范措施"><a href="#防范措施" class="headerlink" title="防范措施"></a>防范措施</h4><ul><li>利用防火墙对恶意分组进行过滤，例如过滤掉所有ICMP回送请求报文可以防范Smurf攻击。然而，对于像“TCP SYN洪泛”这类攻击，一般很难区分哪些TCP连接请求报文是恶意的。</li><li>使用支持源端过滤的路由器，尽量减少P地址伪造的现象。如果要通过源端过滤来防范DoS攻击，则必须使因特网上所有的路由器都具有源端过滤的功能。然而，目前源端过滤并不是路由器的强<br>制功能，支持源端过滤的路由器还是少数。</li><li>追溯DoS攻击源。路由器对通过自己的P数据报的首部进行标记，通过该标记可追潮到Do$S攻击源。这个过程通常很慢，并且需要人工干预，因此目前主要用于事后追查以及采取相应的法律手段提供依据。</li><li>进行DoS攻击检测，通过分析分组首部特征和流量特征来检测正在发生的DoS攻击并发出警报。</li></ul><p>上述措施并不能彻底防范DoS攻击，DoS攻击是目前最容易实现却又最难防范的攻击手段。</p><blockquote><p>恭喜，计算机网络的路你已经走完了。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第七章：网络安全&quot;&gt;&lt;a href=&quot;#第七章：网络安全&quot; class=&quot;headerlink&quot; title=&quot;第七章：网络安全&quot;&gt;&lt;/a&gt;第七章：网络安全&lt;/h1&gt;&lt;p&gt;分为三方面内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全威胁&lt;/li&gt;
&lt;li&gt;安全服务&lt;/li&gt;
</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第六章：应用层</title>
    <link href="https://blog.willmo.top/posts/Chapter%206%20of%20Computer%20Networks:%20Application%20Layer/"/>
    <id>https://blog.willmo.top/posts/Chapter%206%20of%20Computer%20Networks:%20Application%20Layer/</id>
    <published>2026-04-20T11:48:35.927Z</published>
    <updated>2026-04-20T11:48:36.703Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第六章：应用层"><a href="#第六章：应用层" class="headerlink" title="第六章：应用层"></a>第六章：应用层</h1><blockquote><p>恭喜，这里是计算机网络体系结构的顶层。</p></blockquote><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p><img src="https://files.seeusercontent.com/2026/04/15/Ecj6/image-20260415134634090.png" alt="image-20260415134634090"></p><p>应用层是计算机网络体系结构的最顶层，是设计和建立计算机网络的最终目的，也是计算机网络中发展最快的部分。</p><p>我们将介绍：</p><ul><li>万维网www</li><li>域名系统DNS</li><li>动态主机配置DHCP</li><li>电子邮件</li><li>文件传送FTP和P2P文件共享</li><li>多媒体网络应用</li></ul><h3 id="客户-x2F-服务器方式-C-x2F-S方式-和对等方式-P2P方式"><a href="#客户-x2F-服务器方式-C-x2F-S方式-和对等方式-P2P方式" class="headerlink" title="客户/服务器方式(C/S方式)和对等方式(P2P方式)"></a>客户/服务器方式(C/S方式)和对等方式(P2P方式)</h3><p>客户/服务器(Client/Server,C/S)方式</p><ul><li>客户和服务器是指通信中所涉及的两个应用进程。</li><li>客户/服务器方式所描述的是进程之间服务和被服务的关系。</li><li>客户是服务请求方，服务器是服务提供方。</li><li>服务器总是处于运行状态，并等待客户的服务请求。服务器具有固定端口号（例如HTTP服务器的默认端口号为80)，而运行服务器的主机也具有固定的P地址。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/3aeP/image-20260415141101123.png" alt="image-20260415141101123"></p><p>C/S方式是因特网上传统的、同时也是最成熟的方式，很多我们熟悉的网络应用采用的都是C/S方式，包括万维网WWW、电子邮件、文件传输FTP等。</p><p>基于C/S方式的应用服务通常是服务集中型的，即应用服务集中在网络中比客户计算机少得多的服务<br>器计算机上。</p><ul><li>由于一台服务器计算机要为多个客户机提供服务，在C/S应用中，常会出现服务器计算机跟不上众多客户机请求的情况。</li><li>为此，在C/S应用中，常用计算机群集<br>(或服务器场)构建一个强大的虚拟服务器。</li></ul><p>对等(Peer-to-Peer,P2P)方式</p><ul><li>在P2P方式中，没有固定的服务请求者和服务提供者，分布在网络边缘各端系统中的应用进程是对等的，被称为对等方。对等方相互之间直接通信，每个对等方既是服务的请求者，又是服务的提供者。</li><li>目前，在因特网上流行的P2P应用主要包括P2P文件共享、即时通信、P2P流媒体、分布式存储等。</li><li>基于P2P的应用是服务分散型的，因为服务不是集中在少数几个服务器计算机中，而是分散在大量对等计算机中，这些计算机并不为服务提供商所有，而是为个人控制的桌面计算机和笔记本电脑，它们通常位于住宅、校园和办公室中。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/9Dmo/image-20260415141406010.png" alt="image-20260415141406010"></p><ul><li>P2P方式的最突出特性之一就是它的可扩展性。因为系统每增加一个对等方，不仅增加的是服务的请求者，同时也增加了服务的提供者，系统性能不会因规模的增大而降低。</li><li>P2P方式具有成本上的优势，因为它通常不需要庞大的服务器设施和服务器带宽。为了降低成本，服务提供商对于将P2P方式用于应用的兴趣越来越大。</li></ul><h3 id="动态主机配置协议DHCP"><a href="#动态主机配置协议DHCP" class="headerlink" title="动态主机配置协议DHCP"></a>动态主机配置协议DHCP</h3><p>DHCP是一种网络管理协议，它能自动为网络中的设备分配IP地址、子网掩码、默认网关和DNS服务器等配置信息。</p><p>所谓分配，实际上是客户端向DHCP服务器租用某个IP地址，他免去了手工配置麻烦，易出错的痛点。</p><h4 id="DHCP工作原理"><a href="#DHCP工作原理" class="headerlink" title="DHCP工作原理"></a>DHCP工作原理</h4><p><img src="https://files.seeusercontent.com/2026/04/15/In3e/image-20260415143004390.png" alt="image-20260415143004390"></p><p>如图，起初，DHCP客户端广播寻找DHCP服务器，服务器接收后提供IP地址租用服务并从自己的地址池中选择一个可用的IP地址连通其他配置封装为数据报返回一个“DHCP OFFER”报文。客户端可能接收到多个DHCP报文，一般选择最先收到的那个，客户端处理报文但不会使用内部的IP地址，会回复 一个“DHCP请求”报文申请使用那个IP地址。DHCP服务器收到请求后传回ACK数据报。</p><p>接收到ACK确认后，客户端会用ARP检查IP地址是否被其他主机占用，如未占用，IP租用正式成立，DHCP客户端将IP地址等配置使用在自己的端口上。</p><p>租用期不是永久的，需要续约。</p><p>在租用期过半的时候，客户端会向服务器发送DHCP续约请求报文，若服务器同意传回ACK，那就得到新的租用期，若没有同意，那客户端必须停止使用原IP地址并重新申请IP地址。</p><p>若DHCP服务器仍不响应，客户端将在租用期到达0.875时重新申请。</p><p>若DHCP服务器还在装死，那租用期到后，客户端会重新申请IP地址。</p><p>另外，客户端可以随时向DHCP服务器终止IP租用合约，只需要向服务器发送释放请求即可。</p><h4 id="DHCP中继代理"><a href="#DHCP中继代理" class="headerlink" title="DHCP中继代理"></a>DHCP中继代理</h4><p><img src="https://files.seeusercontent.com/2026/04/15/e9mC/image-20260415144236124.png" alt="image-20260415144236124"></p><p>黄色的主机不可使用DHCP，因为广播报文不会被路由器转发，而是丢弃。</p><p>方法是给该路由器配置DHCP服务器的IP地址并使之成为 DHCP中继代理。</p><p>当路由器接收到DHCP请求后，会将其单播转发给DHCP服务器。</p><h3 id="域名系统DNS"><a href="#域名系统DNS" class="headerlink" title="域名系统DNS"></a>域名系统DNS</h3><p>我们在访问某个url来获取网站信息时，实际上计算机访问了对应服务器的IP地址，因为访问地址才符合计算机网络的设计。但是对于人类，域名显然比IP要好记得多，所以需要一个东西来实现域名到IP的转换。</p><p>DNS是域名系统（Domain Name System）的简称，它的核心作用是将我们容易记住的域名（例如 <code>www.qq.com</code>）翻译成计算机能识别的IP地址（例如 <code>14.18.240.1</code>）。</p><p>这种域名和ip键值对在本地会存有一部分，但是用户访问的网站是无限可能的，所以遇到本地查询不到的情况就需要询问DNS域名系统。</p><p>遗憾的是，DNS服务器不可只有一个，因为因特网过于庞大，任何计算机都难以承受，且中心化的代价就是如果该DNS中心发生停电，整个世界的网络都会瘫痪。</p><p>早在1983年，因特网就开始采用层次结构的命名树作为主机的名字(即域名)并使用分布式的域名系统DNS, DNS使大多数域名都在本地解析，仅少量解析需要在因特网上通信，因此系统效率很高。</p><p>由于DNS是分布式系统，即使单个计算机出了故障，也不会妨碍整个系统的正常运行</p><h4 id="分布式DNS"><a href="#分布式DNS" class="headerlink" title="分布式DNS"></a>分布式DNS</h4><p>因特网采用层次树状结构的域名结构。</p><p>域名的结构由若干个分量组成，各分量之间用“点”隔开，分别代表不同级别的域名。</p><table><thead><tr><th>..</th><th>四级域名</th><th>三级域名</th><th>二级域名</th><th>顶级域名</th></tr></thead></table><ul><li>每一级的域名都由英文字母和数字组成，不超过63个字符，不区分大小写字母。</li><li>级别最低的域名写在最左边，而级别最高的顶级域名写在最右边。</li><li>完整的域名不超过255个字符。</li><li>域名系统既不规定一个域名需要包含多少个下级域名，也不规定每一级的域名代表什么意思。</li><li>各级域名由其上一级的域名管理机构管理，而最高的顶级域名则由因特网名称与数字地址分配机构ICANN进行管理。</li></ul><p>如该博客：</p><p>blog.willmo.top</p><ul><li>top是顶级域名，是我向域名服务商namesilo购买的</li><li>willmo是我购买时填入的二级域名</li><li>blog是我在DNS中分配的三级域名</li></ul><p>顶级域名TLD(Top Level Domain)分为以下三类：</p><ul><li>国家顶级域名nTLD<br>采用S03166的规定。如cn表示中国，us表示美国，uk表示英国、等等。</li><li>通用顶级域名gTLD<br>最常见的通用顶级域名有七个，即：com(公司企业)、net(网络服务机<br>构)、org(非营利性组织)、int(国际组织)、edu(美国教育结构)、<br>gov(美国政府部门)、mil(美国军事部门)。</li><li>反向域arpa<br>用于反向域名解析，即IP地址反向解析为域名。</li></ul><p>在国家顶级域名下注册的二级域名均由该国家自行确定。例如，顶级域名为jp的日本，将其教育和企业机构的二级域名定为ac和co，而不用edu和com。</p><h4 id="因特网域名空间"><a href="#因特网域名空间" class="headerlink" title="因特网域名空间"></a>因特网域名空间</h4><p>像是一颗倒着生长的树：</p><p><img src="https://files.seeusercontent.com/2026/04/15/M1ma/image-20260415151254828.png" alt="image-20260415151254828"></p><h4 id="DNS域名分布式解析"><a href="#DNS域名分布式解析" class="headerlink" title="DNS域名分布式解析"></a>DNS域名分布式解析</h4><p><img src="https://files.seeusercontent.com/2026/04/15/f7Xd/image-20260415151440997.png" alt="image-20260415151440997"></p><blockquote><p>等我慢慢转成文字。</p></blockquote><p>查询方式有：</p><ul><li>递归查询</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/Pr2m/image-20260415151637265.png" alt="image-20260415151637265"></p><ul><li>迭代查询</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/lJ2c/image-20260415151714998.png" alt="image-20260415151714998"></p><p>由于递归查询对于被查询的域名服务器负担太大，通常采用以下模式：从请求主机到本地域名服务器的查询是递归查询，而其余的查询是迭代查询。</p><hr><p>为了提高DNS的查询效率，并减轻根域名服务器的负荷和减少因特网上的DNS查询报文数量，在域名服务器中广泛地使用了高速缓存。高速缓存用来存放最近查询过的域名以及从何处获得域名映射信息的记录。</p><p>由于域名到P地址的映射关系并不是永久不变，为保持高速缓存中的内容正确，域名服务器应为每项内容设置计时器并删除超过合理时间的项（例如，每个项目只存放两天）</p><p>不但在本地域名服务器中需要高速缓存，在用户主机中也很需要。许多用户主机在启动时从本地域名服务器下载域名和P地址的全部数据库，维护存放自己最近使用的域名的高速缓存，并且只在从缓存中找不到域名时才向域名服务器查询。同理，主机也需要保持高速缓存中内容的正确性。</p><p>DNS报文使用UDP进行封装。</p><h3 id="文件传送协议FTP"><a href="#文件传送协议FTP" class="headerlink" title="文件传送协议FTP"></a>文件传送协议FTP</h3><p>将某台计算机中的文件通过网络传送到可能相距很远的另一台计算机中，是一项基本的网络应用，即文件传送。<br>文件传送协议FTP(File Transfer Protocol)是因特网上使用得最广泛的文件传送协议。</p><ul><li>FTP提供交互式的访问，允许客户指明文件的类型与格式（如指明是否使用ASCII码），并允许文件具有存取权限（如访问文件的用户必须经过授权，并输入有效的口令）。</li><li>FTP屏蔽了各计算机系统的细节，因而适合于在异构网络中任意计算机之间传送文件。</li></ul><p>FTP基于某个服务器计算机，为了保证访问性和稳定性，应该采用大存储，高稳定性的服务器来搭建。但是 为了短暂测试也可以用个人PC搭建。</p><h4 id="FTP的工作原理"><a href="#FTP的工作原理" class="headerlink" title="FTP的工作原理"></a>FTP的工作原理</h4><p><img src="https://files.seeusercontent.com/2026/04/15/Zp0j/image-20260415163414973.png" alt="image-20260415163414973"></p><ul><li>主动模式</li></ul><p>FTP用户端向一直在监听熟知端口号21的服务器建立控制TCP链接，该链接用于传输命令，从始至终一直存在。</p><p>当有数据需要传输时，就建立另一个文件传输TCP链接，该链接用于传送文件，只有在传输文件时在存活。</p><ul><li>被动模式</li></ul><p>控制链接的建立与主动模式没什么不同，但是文件传输链接的建立有不同：</p><p>当需要传输数据时，客户端通过控制连接发送命令。服务器收到后，会<strong>随机开启一个高端端口（例如1024以上）并监听</strong>，然后将这个端口号通过控制连接告诉客户端。客户端<strong>主动向服务器的这个指定端口发起连接</strong>，从而建立数据通道。</p><h3 id="电子邮件"><a href="#电子邮件" class="headerlink" title="电子邮件"></a>电子邮件</h3><p>电子邮件(E-mail)是因特网上最早流行的一种应用，并且仍然是当今因特网上最重要、最实用的应用之一。</p><ul><li>发件人将邮件发送到自己使用的邮件服务器：</li><li>发件人的邮件服务器将收到的邮件按其</li><li>目的地址转发到收件人邮件服务器中的收件人邮箱：</li><li>收件人在方便的时候访问收件人邮件服务器中自己的邮箱，获取收到的电子邮件。</li><li>电子邮件使用方便、传递迅速而且费用低廉。它不仅可以传送文字信息，而且还可附上声音和图像。</li></ul><p>电子邮件系统采用客户/服务器方式。<br>电子邮件系统的三个主要组成构件：用户代理，邮件服务器，以及电子邮件所需的协议。</p><ul><li>用户代理是用户与电子邮件系统的接口，又称为电子邮件客户端软件。</li><li>邮件服务器是电子邮件系统的基础设施。因特网上所有的ISP都有邮件服务器，其功能是发送和接收邮件，同时还要负责维护用户的邮箱。</li><li>协议包括邮件发送协议（例如SMTP)和邮件读取协议（例如POP3,IMAP)</li></ul><p>下面解释过程:</p><p><img src="https://files.seeusercontent.com/2026/04/15/s7Ux/image-20260415191006173.png" alt="image-20260415191006173"></p><p>发送方使用SMTP协议与发送方服务器建立TCP链接并传输邮件，随后发送方服务器与接收方服务器建立链接后完成传输，随后接收方通过POP3协议从接收方服务器中读取邮件。分为接收方和发送方是因为实际过程中收发双方的邮件服务很可能不一样，比如QQ邮箱和谷歌邮箱。</p><h4 id="邮件发送SMTP工作原理"><a href="#邮件发送SMTP工作原理" class="headerlink" title="邮件发送SMTP工作原理"></a>邮件发送SMTP工作原理</h4><p><img src="https://files.seeusercontent.com/2026/04/15/z2Rn/image-20260415191810636.png" alt="image-20260415191810636"></p><p>多说无益，自己看吧。</p><h4 id="电子邮件的信息格式"><a href="#电子邮件的信息格式" class="headerlink" title="电子邮件的信息格式"></a>电子邮件的信息格式</h4><p>电子邮件的信息格式并不是由SMTP定义的，而是在RFC822中单独定义的。这个RFC文档已在2008年更新为RFC5322。一个电子邮件有<strong>信封</strong>和<strong>内容</strong>两部分。而内容又由<strong>首部</strong>和<strong>主体</strong>两部分构成。</p><p><img src="https://files.seeusercontent.com/2026/04/15/xBw4/image-20260415192129149.png" alt="image-20260415192129149"></p><p>补充：邮件的抄送人（CC， Carbon Copy）是指除了主要收件人（“收件人”栏）之外，发送方希望其知晓邮件内容、但通常不要求其直接回复或采取行动的人。</p><h4 id="SMTP的局限"><a href="#SMTP的局限" class="headerlink" title="SMTP的局限"></a>SMTP的局限</h4><ul><li>SMTP协议只能传送ASCI码文本数据，不能传送可执行文件或其他的二进制对象。</li><li>SMTP不能满足传送多媒体邮件（例如带有图片、音频或视频数据）的需要。并且许多其他非英语国家的文字<br>(例如中文、俄文、甚至带有重音符号的法文或德文)也无法用SMTP传送。</li><li>为解决SMTP传送非ASCII码文本的问题，提出了多用途因特网邮件扩展MIME(Multipurpose Internet Mail Extensions)</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/Ss3a/image-20260415192329529.png" alt="image-20260415192329529"></p><p>MINE做到了：</p><ul><li>增加了5个新的邮件首部字段，这些字段提供了有关邮件主体的信息。</li><li>定义了许多邮件内容的格式，对多媒体电子邮件的表示方法进行了标准化。</li><li>定义了传送编码，可对任何内容格式进行转换，而不会被邮件系统改变。</li></ul><p>实际上，MIME不仅仅用于SMTP,也用于后来的同样面向ASCII字符的HTTP。</p><h4 id="邮件读取POP和IMAP"><a href="#邮件读取POP和IMAP" class="headerlink" title="邮件读取POP和IMAP"></a>邮件读取POP和IMAP</h4><ul><li>邮局协议POP(Post Office Protocol),POP3是其第三个版本，是因特网正式标准非常简单、功能有限的邮件读取协议。</li><li><ul><li>用户只能以下载并删除方式或下载并保留方式从邮件服务器下载邮件到用户方计算机。不允许用户在邮件服务器上管理自己的邮件。（例如创建文件夹，对邮件进行分类管<br>理等)。</li></ul></li><li>因特网邮件访问协议IMAP(Internet Message Access Protocol)),IMAP4是其第四个版本，目前还只是因特网建议标准功能。</li><li><ul><li>比POP3强大的邮件读取协议，用户在自己的计算机上就可以操控邮件服务器中的邮箱，就像在本地<br>操控一样，因此IMAP是一个联机协议。</li></ul></li></ul><p>POP3和IMAP4都采用基于TCP连接的客户/服务器方式。POP3使用熟知端口110，IMAP4使用熟知端口143。</p><h4 id="基于万维网的电子邮件"><a href="#基于万维网的电子邮件" class="headerlink" title="基于万维网的电子邮件"></a>基于万维网的电子邮件</h4><p>通过浏览器登录（提供用户名和口令）邮件服务器万维网网站就可以撰写、收发、阅读和管理电子邮件。这种工作模式与MAP很类似，不同的是用户计算机无需安装专门的用户代理程序，只需要使用通用的万维网浏览器。<br>邮件服务器网站通常都提供非常强大和方便的邮件管理功能，用户可以在邮件服务器网站上管理和处理自己的邮件，而不需要将邮件下载到本地进行管理。</p><p><img src="https://files.seeusercontent.com/2026/04/15/L8fi/image-20260415192858696.png" alt="image-20260415192858696"></p><h3 id="万维网"><a href="#万维网" class="headerlink" title="万维网"></a>万维网</h3><p>万维网WWW(World Wide Web)并非某种特殊的计算机网络。它是一个大规模的、联机式的信息储藏所，是运行在因特网上的一个分布式应用。<br>万维网利用网页之间的超链接将不同网站的网页链接成一张逻辑上的信息网。</p><p>浏览器最重要的部分是渲染引擎，也就是浏览器内核。负责对网页内容进行解析和显示。</p><ul><li>不同的浏览器内核对网页内容的解析也有不同，因此同一网页在不同内核的浏览器里的显示效果可能不同：</li><li>网页编写者需要在不同内核的浏览器中测试网页显示效果。</li></ul><p>为了方便地访问在世界范围的文档，万维网使用统一资源定位符URL来指明因特网上任何种类“资源”的位置。<br>URL的一般形式由以下四个部分组成：</p><p><code>&lt;协议&gt;：//&lt;主机&gt;：&lt;端口&gt;/&lt;路径&gt;</code></p><p>而万维网的文档是html，css和javascript。</p><ul><li>超文本标记语言HTML(HyperText Markup Language)<br>使用多种“标签”来描述网页的结构和内容</li><li>层叠样式表CSS(Cascading Style Sheets)<br>从审美的角度来描述网页的样式</li><li>js，一种脚本语言（和Java没有任何关系）<br>控制网页的行为</li></ul><h4 id="超文本传输协议HTTP"><a href="#超文本传输协议HTTP" class="headerlink" title="超文本传输协议HTTP"></a>超文本传输协议HTTP</h4><p>HTTP定义了浏览器（即万维网客户进程）<br>怎样向万维网服务器请求万维网文档，以及万维网服务器怎样把万维网文档传送给浏览器。</p><p><img src="https://files.seeusercontent.com/2026/04/15/en9U/image-20260415193759372.png" alt="image-20260415193759372"></p><p>HTTP/1.0采用非持续连接方式。在该方式下，每次浏览器要请求一个文件都要与服务器建立TCP连接，当收到响应后就立即关闭连接。</p><ul><li>每请求一个文档就要有两倍的TT的开销。若一个网页上有很多引用对象（例如图片等），那么请求每一个对象都需要花费2RTT的时间。</li><li>为了减小时延，浏览器通常会建立多个并行的TCP连接同时请求多个对象。但是，这会大量占用万维网服务器的资源，特别是万维网服务器往往要同时服务于大量客户的请求，这会使其负担很重。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/Fx2b/image-20260415193953925.png" alt="image-20260415193953925"></p><p>HTTP/1.1采用持续连接方式。在该方式下，万维网服务器在发送响应后仍然保持这条连接，使同一个客户（浏览器）和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这并不局限于传送同一个页面上引用的对象，而是只要这些文档都在同一个服务器上就行。</p><p>为了进一步提高效率，HTTP/1.1的持续连接还可以使用流水线方式工作，即浏览器在收到HTTP的响应报文之前就能够连续发送多个请求报文。这样的一个接一个的请求报文到达服务器后，服务器就发回一个接一个的响应报文。这样就节省了很多个RTT时间，使TCP连接中的空闲时间减少，提高了下载文档的效率。</p><h4 id="HTTP的报文格式"><a href="#HTTP的报文格式" class="headerlink" title="HTTP的报文格式"></a>HTTP的报文格式</h4><p>HTTP是面向文本的，其报文中的每一个字段都是一些ASCII码串，并且每个字段的长度都是不确定的。</p><p><img src="https://files.seeusercontent.com/2026/04/15/Gxx2/image-20260415194319215.png" alt="image-20260415194319215"></p><p><img src="https://files.seeusercontent.com/2026/04/15/z9tU/image-20260415194426876.png" alt="image-20260415194426876"></p><h4 id="Cookie"><a href="#Cookie" class="headerlink" title="Cookie"></a>Cookie</h4><ul><li>使用Cookie在服务器上记录用户信息</li><li>现在，用户可以通过万维网实现各种复杂的应用，如网上购物、电子商务等。这些应用往往需要万维网服务<br>器能够识别用户。</li></ul><p>Cookie提供了一种机制使得万维网服务器能够“记住”用户，而无需用户主动提供用户标识信息。也就是说，Cookie是一种对无状态的HTTP进行状态化的技术。</p><p><img src="https://files.seeusercontent.com/2026/04/15/gIl8/image-20260415194706811.png" alt="image-20260415194706811"></p><h4 id="万维网缓存"><a href="#万维网缓存" class="headerlink" title="万维网缓存"></a>万维网缓存</h4><ul><li>在万维网中还可以使用缓存机制以提高万维网的效率。</li><li>万维网缓存又称为Web缓存(Web Cache),可位于客户机，也可位于中间系统上，位于中间系统上的<br>Web缓存又称为代理服务器(Proxy Server)。</li><li>Web缓存把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时，若发现这个请求与暂时存放的请求相同，就返回暂存的响应，而不需要按URL的地址再次去因特网访问该资源。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/q7Gq/image-20260415194900348.png" alt="image-20260415194900348"></p><p>代理服务器内的文件资源存在一个过期时间，若到了时间，就需要重新向原始服务器发送请求，通过提供修改日期，原始服务器查看代理服务器中的文件与自己的是否一致，如果一致就发回304响应，若不一致就将新文件传到代理服务器。这样代理服务器就完成了文档的更新。</p><p><img src="https://files.seeusercontent.com/2026/04/15/aXh9/image-20260415195304877.png" alt="image-20260415195304877"></p><p>至此，应用层完结</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第六章：应用层&quot;&gt;&lt;a href=&quot;#第六章：应用层&quot; class=&quot;headerlink&quot; title=&quot;第六章：应用层&quot;&gt;&lt;/a&gt;第六章：应用层&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;恭喜，这里是计算机网络体系结构的顶层。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第五章：运输层</title>
    <link href="https://blog.willmo.top/posts/Computer%20Networks%20Chapter%205:%20Transport%20Layer/"/>
    <id>https://blog.willmo.top/posts/Computer%20Networks%20Chapter%205:%20Transport%20Layer/</id>
    <published>2026-04-20T11:47:18.618Z</published>
    <updated>2026-04-20T11:47:19.263Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第五章：运输层"><a href="#第五章：运输层" class="headerlink" title="第五章：运输层"></a>第五章：运输层</h1><h4 id="进程间基于网络的通信"><a href="#进程间基于网络的通信" class="headerlink" title="进程间基于网络的通信"></a>进程间基于网络的通信</h4><p>第2~4章依次介绍了计算机网络体系结构中的物理层、数据链路层和网络层，它们共同解决了将主机通过异构网络互联起来所面临的问题，实现了主机到主机的通信。</p><p>然而在计算机网络中实际进行通信的真正实体，是位于通信两端主机中的进程。<br>如何为运行在不同主机上的应用进程提供直接的逻辑通信服务，就是运输层的主要任务。运输层协议称为端到端协议。</p><p><img src="https://files.seeusercontent.com/2026/04/14/xA3i/image-20260414102058876.png" alt="image-20260414102058876"></p><p>我们从计算机体系结构来看：</p><p><img src="https://files.seeusercontent.com/2026/04/14/vLi6/image-20260414102239039.png" alt="image-20260414102239039"></p><p>注意：运输层的端口不是物理端口，而是0-65535的端口。</p><p>运输层向应用层实体屏蔽了下面网络核心的细节（例如网络拓扑、所采用的路由选择协议等），它使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道。<br>根据应用需求的不同，因特网的运输层为应用层提供了两种不同的运输层协议，即<strong>面向连接的TCP</strong>和<strong>无连接的UDP</strong>,这两种协议就是本章要讨论的主要内容。</p><h3 id="TCP-x2F-IP体系结构运输层中最重要的两个协议"><a href="#TCP-x2F-IP体系结构运输层中最重要的两个协议" class="headerlink" title="TCP/IP体系结构运输层中最重要的两个协议"></a>TCP/IP体系结构运输层中最重要的两个协议</h3><p><img src="https://files.seeusercontent.com/2026/04/14/2Tvm/image-20260414103102501.png" alt="image-20260414103102501"></p><ul><li>传输控制协议(Transmission Control Protocol,TCP)为其上层提供的是面向连接的可靠的数据传输服务</li><li>使用TCP通信的双方，在传送数据之前必须首先建立TCP连接（逻辑连接，而非物理连接）。数据传输结束后必须要释放TCP连接，</li><li>TCP为了实现可靠传输，就必须使用很多措施，例如TCP连接管理、确认机制、{<br>超时重传、流量控制以及拥塞控制等</li><li>TCP的实现复杂，TCP报文段的首部比较大，占用处理机资源比较多</li></ul><hr><ul><li>用户数据报协议(User Datagram Protocol,UDP)其上层提供的是无连接的不可靠的数据传输服务。</li><li>使用UDP通信的双方，在传送数据之前不需要建立连接。</li><li>UDP不需要实现可靠传输，因此不需要使用实现可靠传输的各种机制。</li><li>UDP的实现简单，UDP用户数据报的首部比较小。</li></ul><h3 id="端口号，复用与分用的概念"><a href="#端口号，复用与分用的概念" class="headerlink" title="端口号，复用与分用的概念"></a>端口号，复用与分用的概念</h3><h4 id="运输层端口号"><a href="#运输层端口号" class="headerlink" title="运输层端口号"></a>运输层端口号</h4><p>运行在计算机上的进程是使用进程标识符(Process ldentification,PID)来标识的。</p><ul><li>然而，因特网上的计算机并不是使用统一的操作系统，而不同操作系统(Windows、.Linux、MacOS)又使用不同格式的进程标识符。</li><li>为了使运行不同操作系统的计算机的应用进程之间能够基于网络进行通信，就必须使用统一的方法对TCP体系的应用进程进行标识。</li></ul><p>TCP体系结构的运输层使用端口号来标识和区分应用层的不同应用进程。端口号的长度为16比特，取值范围是0-65535。</p><p>端口号分为两类：</p><ul><li>服务器端使用的端口号</li><li><ul><li>熟知端口号0-1023</li><li><ul><li>由IANA分配给TCP/IP体系结构应用层中最重要的一些应用协议。</li></ul></li><li>登记端口号1024-49151</li><li><ul><li>为没有熟知端口号的应用程序使用。要使用这类端口号，必须在IANA进行登记，以防止重复。例如，Microsoft RDP微软远程桌面应用程序使用的端口号是3389。</li></ul></li></ul></li><li>客户端使用端口号</li><li><ul><li>仅在客户端使用，由客户进程在运行时动态选择，通信结束后会被系统收回，以便给其他客户进程使用</li></ul></li></ul><p>端口号只具有本地意义，即端口号只是为了标识本计算机网络协议栈应用层中的各应用进程。在因特网中，不同计算机中的相同端口号是没有关系的，即相互独立。另外，TCP和UDP端口号之间也是没有关系的。</p><h4 id="发送方的复用和接收方分用"><a href="#发送方的复用和接收方分用" class="headerlink" title="发送方的复用和接收方分用"></a>发送方的复用和接收方分用</h4><p><img src="https://files.seeusercontent.com/2026/04/14/vkN0/image-20260414104202241.png" alt="image-20260414104202241"></p><p>运输层中的<strong>复用</strong>与<strong>分用</strong>是同一通信过程的两个方向，核心是借助<strong>端口号</strong>来区分不同应用程序的数据流。</p><ul><li><strong>复用</strong>：发送方主机的多个应用进程（如浏览器、邮件客户端）都可以使用同一个运输层协议（如TCP或UDP）来发送数据。运输层将这些来自不同应用的数据块封装上首部（包含目的端口号等信息），合并成同一个协议的数据流，交给网络层发送。</li><li><strong>分用</strong>：接收方主机的运输层从网络层收到数据后，根据数据首部中的目的端口号，将数据准确交付到正在该端口“监听”的对应应用进程。</li></ul><p>来看一个例子：</p><p><img src="https://files.seeusercontent.com/2026/04/14/qtI3/image-20260414105032977.png" alt="image-20260414105032977"></p><p>用户级PC向DNS服务器发送一个DNS查询请求报文，发送端口是49152，目的端口是DNS服务器的53号端口，DNS服务器查询到域名对应的IP后返回给49152端口，随后“根据域名查询IP地址”这一进程已结束，端口49152被收回</p><p><img src="https://files.seeusercontent.com/2026/04/14/t1cG/image-20260414105357378.png" alt="image-20260414105357378"></p><p>随后分配给浏览器使用，浏览器发送请求，请求查看Web服务器的首页内容，发送端口是49152，目的端口是Web服务器的80端口，随后Web返回首页内容，PC在显示器中显示。这就完成了对端口和协议的复用和分用。</p><h3 id="TCP和UDP对比"><a href="#TCP和UDP对比" class="headerlink" title="TCP和UDP对比"></a>TCP和UDP对比</h3><p>UDP使用的是无连接不可靠的传输服务，原理简单，在本节就能介绍完。</p><p><img src="https://files.seeusercontent.com/2026/04/14/h2sJ/image-20260414105834202.png" alt="image-20260414105834202"></p><p>TCP有建立连接和断开连接的过程，以保证可靠运输。</p><p><img src="https://files.seeusercontent.com/2026/04/14/on9J/image-20260414110214687.png" alt="image-20260414110214687"></p><p>UDP相对自由，支持单播多播和广播，但是TCP需要建立链接，这意味着只能进行单播通信。</p><p><img src="https://files.seeusercontent.com/2026/04/14/Co9w/image-20260414110552874.png" alt="image-20260414110552874"></p><p><strong>UDP面向应用报文</strong>：UDP对应用程序交下来的数据，<strong>原封不动地</strong>加上首部就发送出去。一个应用报文（比如一段文字“Hello”）就封装成一个UDP数据报，网络层一次发送一个完整的报文。接收方的UDP也一次交付一个完整的报文。因此，<strong>应用程序必须选择合适大小的报文</strong>，如果太大，IP层可能需要分片，降低效率；如果太小，则首部开销相对较大。</p><p><strong>TCP面向字节流</strong>：TCP把应用程序交下来的数据看成仅仅是<strong>一连串无结构的字节流</strong>。它不保证发送方应用程序发出的数据块和接收方应用程序收到的数据块具有对应的大小关系。TCP会根据网络情况和窗口大小，<strong>自主决定</strong>将多少字节流封装成一个TCP报文段发送出去。接收方也是将字节流按顺序重组后，源源不断地交付给应用程序。</p><p>TCP支持全双工通信。</p><p><img src="https://files.seeusercontent.com/2026/04/14/omU7/image-20260414111235348.png" alt="image-20260414111235348"></p><p>UDP发现误码后不会要求重传，而是直接丢弃，毕竟IP电话，视频会议的少量数据包无关痛痒。这能够有效保障传输速度。</p><p>即使网际层提供的也是无连接不可靠传输服务，但是位于传输层的TCP提供了面向连接的可靠传输服务支持，TCP建立的信道能够处理误码，丢失，乱序，重复等问题。</p><p><img src="https://files.seeusercontent.com/2026/04/14/d3oQ/image-20260414111619447.png" alt="image-20260414111619447"></p><p>TCP的传输机制比UDP复杂得多，所以首部被设计的很复杂才能适应需求。</p><h3 id="TCP报文段首部格式"><a href="#TCP报文段首部格式" class="headerlink" title="TCP报文段首部格式"></a>TCP报文段首部格式</h3><p>TCP的全部功能需要依靠其首部中的各字段来实现。</p><p><img src="https://files.seeusercontent.com/2026/04/14/M5ve/image-20260414122030259.png" alt="image-20260414122030259"></p><p>端口号就不在赘述了。</p><ul><li>序号：占32比特，取值范围0~232.1。当序号增加到最后一个时，下一个序号又回到0。用来指出本TCP报文段数据载荷的第一个字节的序号。</li><li>确认号：占32比特，取值范围0~232.1。当确认号增加到最后一个时，下一个确认号又回到0。用来指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号，同时也是对之前收到的所有数据的确认。</li><li>确认标志位ACK：<strong>表示该报文段是一个确认报文</strong>。只有当ACK取值为1时，确认号字段才有效。ACK取值为0时，确认号字段无效，这不是一个确认报文。 TCP规定：在TCP连接建立后，所有传送的TCP报文段都必须把ACK置1。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/14/ev5D/image-20260414122827114.png" alt="image-20260414122827114"></p><p>如图：客户端向服务器发送TCP数据报，该数据表示：</p><ul><li>已确认此前发送的800字节的数据没有问题，现在需要800往后的数据</li><li>ACK=1，该报文有效，没有问题，意义很灵活，需要结合情景理解。</li><li>序号=201，这是客户端向主机传送的以第201字节为开头的数据。</li></ul><p>服务器传回也差不多。</p><p>放个例题：</p><p><img src="https://files.seeusercontent.com/2026/04/14/un4Z/image-20260414123310496.png" alt="image-20260414123310496"></p><p>继续看字段：</p><ul><li>数据偏移：占4比特，该字段的取值以4字节为单位。指出TCP报文段的数据载荷部分的起始处距离TCP报文段的起始处有多远，这实际上指出了TCP报文段的首部长度。该字段的存在是因为扩展首部的存在使得首部的长度不是固定的。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/14/ld6H/image-20260414124058947.png"></p><ul><li>保留：占6比特，保留为以后使用，目前置为0</li><li>窗口：占16比特，该字段的取值以字节为单位。指出发送本报文段的一方的接收窗口的大小，即接收缓存的可用空间大小，这用来表征接收方的接收能力。在计算机网络中，经常用接收方的接收能力的大小来控制发送方的数据发送量，这就是所谓的流量控制(后面会介绍)。</li><li>检验和：占16比特，用来检查整个TCP报文在传输过程中是否出现了误码。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/14/Iru3/image-20260414124956506.png" alt="image-20260414124956506"></p><ul><li>同步标志位SYN：用于TCP“三报文握手”建立连接，当SYN=1且ACK=0时，表明这是一个TCP连接请求报文段。对方若同意建立连接，则应在响应的TCP报文段的首部中使SYN=1且ACK=1。综上所述，SYN为1的TCP报文段要么是一个连接请求报文段，要么是一个连接响应报文段。</li><li>终止标志位FIN：用于TCP“四报文挥手”释放连接。当FIN=1时，表明此TCP报文段的发送方已经将全部数据发送完毕，现在要求释放TCP连接</li><li>复位标志位RST：用于复位TCP连接。当RST=1时，表明TCP连接中出现严重差错，必须释放连接，然后再重新建立连接。 RST置1还用来拒绝一个非法的TCP报文段或拒绝打开一个TCP连接。</li><li>推送标志位PSH：发送方TCP把PSH置1，并立即创建一个TCP报文段发送出去，而不需要积累到足够多的数据再发送。接收方TCP收到PSH为1的TCP报文段，就尽快地交付给应用进程而不再等到接收到足够多的数据才向上交付。</li></ul><blockquote><p>出于效率的考虑，TCP的发送方可能会延迟发送数据，而TCP的接收方可能会延迟向应用进程交付数据。这样可以一次处理更多的数据。但是当两个应用进程进行交互式通信时，有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下，应用进程可以通知TCP使用推送(PUSH)操作。</p></blockquote><ul><li>紧急指针字段URG：占16比特，以字节为单位，用来指明紧急数据的长度。当发送方有紧急数据时，可将紧急数据“插队”到发送缓存的最前面，并立刻封装至一个TCP报文段中进行发送。紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据，紧急数据之后是普通数据。接收方收到紧急标志位为1的TCP报文段，会按照紧急指针字段的值从报文段数据载中取出紧急数据并直接上交应用进程，而不必在接收缓存中排队。当URG=1时，紧急指针字段有效。当URG=0时，紧急指针字段无效。</li><li>选项：长度可变，最大40字节。</li><li><ul><li>最大报文段长度MSS选项：指出TCP报文段数据载荷部分的最大长度，而不是整个TCP报文段的长度。</li><li>窗口扩大选项：用来扩大窗口，提高吞吐率。</li><li>时间戳选项：·用于计算往返时间RTT·用于处理序号超范围的情况，又称为防止序号绕回PAWS。</li><li>选择确认选项：用来实现选择确认功能。</li></ul></li><li>填充：若选项字段的长度加上20字节固定首部的长度不能被4字节整除时，需要填充相应数量的比特 0以确保首部长度能被4字节整除。IPv4数据报中也是这么干的。</li></ul><h3 id="TCP三报文握手建立链接"><a href="#TCP三报文握手建立链接" class="headerlink" title="TCP三报文握手建立链接"></a>TCP三报文握手建立链接</h3><blockquote><p>这两个过程中序号和数据携带的关系没写出来。</p></blockquote><p>TCP运输连接有以下三个阶段：</p><ul><li>通过“三报文握手”来建立TCP连接。</li><li>基于已建立的TCP连接进行可靠的数据传输。</li><li>在数据传输结束后，还要通过“四报文挥手”来释放TCP连接。</li></ul><p>建立链接是为了解决以下问题：</p><ul><li>使TCP双方能够确知对方的存在。</li><li>使TCP双方能够协商一些参数（例如最大报文段长度、最大窗口大小、时间戳选项等）。</li><li>使TCP双方能够对运输实体资源进行分配和初始化。运输实体资源包括缓存大小、各状态变量、连接表中的项目等。</li></ul><p>来看看链接过程：</p><p><img src="https://files.seeusercontent.com/2026/04/14/6vqP/image-20260414192524603.png" alt="image-20260414192524603"></p><p>初始状态下，双方的TCP监听都是关闭状态，首先，服务器的TCP进程创建传输控制块TCB,随后进入TCP监听状态。</p><p>随后用户端主机向服务器发送TCP请求链接报文，该报文SYN=1，seq为序号，被设置为X用于作为初始序号。随后服务器接收TCP请求并处理，同意后返回TCP请求接收报文，该报文SYN=1，ACK=1，seq=y，ack=x+1.seq是服务器选择的初始序号。</p><p>用户端主机接收到确认报文之后会发回一个确认TCP报文，ack=1，seq=x+1，ack=y+1.随后进入链接已建立状态。</p><blockquote><p>TCP规定普通的TCP确认报文段可以携带数据，但如果不携带数据，则不消耗序号。<br>如果该报文段不携带数据，则TCP客户进程要发送的下一个数据报文段的序号仍为x+1。</p></blockquote><p>服务器收到后进入链接已建立的状态。</p><p>思考最后用户端发送的TCP报文是否多余？</p><p>不多余，来看下面两报文握手的情况：</p><p><img src="https://files.seeusercontent.com/2026/04/14/cqH5/image-20260414193436857.png" alt="image-20260414193436857"></p><p>第一次传的TCP请求链接报文在网络链路中迷失，随后触发超时重传，TCP客户机和服务器正常握手，传输数据后正常关闭。</p><p>这是，此前迷失的TCP请求链接报文抵达服务器，服务器照例进入链接已建立的状态并传回确认TCP报文，但是用户机已没有传输请求所以不理睬，这会导致服务器一直等待用户机的数据，这会导致资源浪费。</p><h3 id="TCP四报文挥手释放链接"><a href="#TCP四报文挥手释放链接" class="headerlink" title="TCP四报文挥手释放链接"></a>TCP四报文挥手释放链接</h3><p>来看看过程：</p><p><img src="https://files.seeusercontent.com/2026/04/14/lX7u/image-20260414194820074.png" alt="image-20260414194820074"></p><p>TCP客户机准备关闭TCP链接，向TCP服务器发送释放报文段，FIN=1，ACK=1，<strong>seq=u</strong>,ack</p><p>=v,seq等于最后一个数据的字节序号+1，ack等于<strong>TCP客户进程之前已收到的数据的</strong>最后一个字节的序号加1。</p><p>随后服务器接收并处理报文，进入关闭等待状态后发送回一个TCP确认终止报文段，ACK=1，seq=v，<strong>ack=u+1</strong>，此时，从TCP客户进程到TCP服务器进程这个方向的连接就释放了，此时的TCP链接处于半关闭状态，TCP客户进程已经没有数据要发送了。</p><p>随后服务器准备关闭连接，进入最后确认状态并发送报文，FIN=1，ACK=1，seq=w,ack=u+1.seq定义为w是因为半关闭状态下服务器可能发送了一些数据。</p><p>随后用户机发回确认报文，ACK=1，seq=u+1，ack=w+1.随后服务器进入关闭状态。用户机需要经过2倍的MSL等待时间后才能进入关闭状态，撤销TCB控制块。</p><blockquote><p>MSL是最长报文段寿命(Maximum Segment Lifetime):的英文缩写词，[RFC793]建议为2分钟。也就是说，TCP客户进程进入时间等待(TIME WAIT)状态后，还要经过4分钟才能进入关闭(CLOSED)状态。实际上MSL在现实中被设置为更短</p></blockquote><p>还是一样的，用户机最后的等待是很有必要的。</p><p><img src="https://files.seeusercontent.com/2026/04/14/7vxT/image-20260414195943855.png" alt="image-20260414195943855"></p><p>如果最后用户机发送的报文丢失，服务器触发超时重传重新发送的报文无法被已经关闭的用户机接收，这会使得服务器永远无法进入关闭状态。</p><p>而MSL就是报文在链路中的最大寿命，超出这个时间的报文将永远不会抵达目的地。</p><h4 id="TCP保活计时器"><a href="#TCP保活计时器" class="headerlink" title="TCP保活计时器"></a>TCP保活计时器</h4><p>TCP保活计时器是TCP协议中的一个<strong>可选机制</strong>，用于检测一个空闲的TCP连接是否仍然有效。</p><p>如果链接建立后用户机停电关闭，服务器就不可能将数据送达，就可能陷入到无限制的超时重传中，保活计时器就是为了解决这个问题：</p><p><img src="https://files.seeusercontent.com/2026/04/14/eJ5r/image-20260414200542907.png" alt="image-20260414200542907"></p><h3 id="TCP的流量控制"><a href="#TCP的流量控制" class="headerlink" title="TCP的流量控制"></a>TCP的流量控制</h3><p>如图：</p><p><img src="https://files.seeusercontent.com/2026/04/14/1Inu/image-20260414200915718.png" alt="image-20260414200915718"></p><p>如果服务器未能及时取走缓存区内的数据，而用户机又在一昧发送，会导致缓存充满后溢出，数据丢失。</p><p>TCP为应用程序提供了流量控制(Flow Control)机制，以解决因发送方发送数据太快而导致接收方来不及接收，造成接收方的接收缓存溢出的问题。<br>流量控制的基本方法：接收方根据自己的接收能力（接收缓存的可用空间大小）控制发送方的速率，如果接收方缓存已用尽，发送方就等待。</p><blockquote><p>rwnd字段是 接收窗口 的缩写，其作用是由接收方明确告知发送方：自己当前还有多少空闲的缓冲区容量可以接收数据。</p></blockquote><p><img src="https://files.seeusercontent.com/2026/04/14/qBc8/image-20260414202112548.png" alt="image-20260414202112548"></p><p>如图，主机B的接收缓存是400字节，主机A发送了3个分组，但是最后一个分组(201)丢失，随后主机B传回累积确认分组，表示“前201数据已接收，我的接收缓存还剩下300字节”随后主机A将调整发送窗口大小，并将发送窗口向右滑动，将已被接受的数据溢出窗口外，开始传输201~500的数据。此时，主机B已经实现了对主机A的第一次流量控制。</p><p>这种窗口不断滑动又动态调整大小的机制，就是大名鼎鼎的<strong>滑动窗口机制</strong></p><p>但是还是存在一个问题，那就是带有rwnd字段的报文丢失会导致死锁问题:</p><p><img src="https://files.seeusercontent.com/2026/04/14/5Jxl/image-20260414202817201.png" alt="image-20260414202817201"></p><p>所以需要一个计时器来解决：</p><p>TCP为每一个连接都设有一个持续计时器，只要TCP连接的一方收到对方的零窗口通知，就启动持续计时器。当持续计时器超时时，就发送一个零窗口探测报文段，仅携带1字节的数据。对方在确认这个零窗口探测报文段时，给出自己现在的接收窗口值。如果接收窗口值仍然是0，那么收到这个报文段的一方就重新启动持续计时器，如果接收窗口值不是0，那么死锁的局面就可以被打破了。</p><p><img src="https://files.seeusercontent.com/2026/04/14/gdX3/image-20260414203226032.png" alt="image-20260414203226032"></p><p><strong>实际上TCP规定：即使接收窗口值为0，也必须接受零窗口探测报文段、确认报文段以及携带有紧急数据的报文段。</strong></p><p>且因为零窗口探测报文段也有重传计时器，当重传计时器超时后，零窗口探测报文段会被重传。</p><h3 id="拥塞控制"><a href="#拥塞控制" class="headerlink" title="拥塞控制"></a>拥塞控制</h3><p>在某段时间，若对网络中某一资源的需求超过了该资源所能提供的可用部分，网络性能就要变坏，这种情况就叫作拥塞(congestion)。</p><ul><li>计算机网络中的链路容量（带宽）、交换节点中的缓存和处理机等都是网络的资源。</li></ul><p>若出现拥塞而不进行控制，整个网络的吞吐量将随输入负荷的增大而下降。更严重的是，如果输入负载超过了拥塞控制的能力范围，就会导致死锁。</p><p><img src="https://files.seeusercontent.com/2026/04/14/h7sB/image-20260414210205080.png" alt="image-20260414210205080"></p><p>所以，拥塞控制的目的是防止过多的数据注入到网络中，使网络能够承受现有的网络负荷</p><h4 id="与流量控制的区别"><a href="#与流量控制的区别" class="headerlink" title="与流量控制的区别"></a>与流量控制的区别</h4><p>流量控制：</p><ul><li>以接收方的接收能力控制发送方（源点）<br>的发送速率</li><li>只与特定的点对点通信的发送方和接收方之间的流量有关</li></ul><p>拥塞控制：</p><ul><li>源点根据各方面因素，按拥塞控制算法自行控制发送速率。</li><li>全局性问题，涉及网络中所有的主机，路由器等</li></ul><h4 id="拥塞控制的基本方法"><a href="#拥塞控制的基本方法" class="headerlink" title="拥塞控制的基本方法"></a>拥塞控制的基本方法</h4><p>开环控制：</p><ul><li>试图用良好的设计来解决问题</li><li>确保从一开始就保证问题不出现</li><li>一旦运行起来就不需要中途修正</li><li>当网络的流量特征可以精准规定且性能要求能够事先获得时适用</li></ul><p>闭环控制：</p><ul><li>检测网络拥塞在何时何地发生</li><li>把控制发生的相关信息传送到可以采取行动的地方</li><li>调整网络的运行以解决拥塞问题</li><li>目前因特网广泛采用闭环控制方法，在此我们只讨论闭环的拥塞控制算法。</li></ul><h4 id="衡量网络拥塞的指标"><a href="#衡量网络拥塞的指标" class="headerlink" title="衡量网络拥塞的指标"></a>衡量网络拥塞的指标</h4><ul><li>由于缓存溢出而丢弃的分组的百分比</li><li>路由器的平均队列长度</li><li>超时重传的分组数量</li><li>平均分组时延和分组时延的标准差</li></ul><p>根据拥塞信息的反馈形式，可将闭环拥塞控制算法分为</p><ul><li>显式反馈算法</li><li><ul><li>从拥塞节点(即路由器)向源点提供关于<br>网络中拥塞状态的显式反馈信息。</li></ul></li><li>隐式反馈算法</li><li><ul><li>源点自身通过对网络行为的观察（例如超时重传或往返时间RTT)来推断网络是否发生了拥塞。TCP采用的就是隐式反馈算法。</li></ul></li></ul><p>注意，拥塞控制并不仅仅是运输层要考虑的问题。显式反馈算法就必须涉及网络层。虽然一些网络体系结构（如ATM网络）主要在网络层实现拥塞控制，但因特网主要利用隐式反馈在运输层实现拥塞控制。</p><p>且进行拥塞控制是需要代价的，可能需要在节点之间交换信息和各种命令，以便选择拥塞控制的策略并实施控制，这样会产生额外开销。<br>还可能需要预留一些资源用于特殊用户或特殊情况，这样就降低了网络资源的共享程度。</p><h4 id="拥塞控制的4种方法"><a href="#拥塞控制的4种方法" class="headerlink" title="拥塞控制的4种方法"></a>拥塞控制的4种方法</h4><p>为了集中精力讨论拥塞控制算法的基本原理，假定如下条件</p><ul><li>数据是单方向传送的，而另一个方向只传送确认。</li><li>接收方总是有足够大的接收缓存空间，因而发送方的发送窗口的大小仅由网络的拥塞程度来决定，也就是不考虑接收方对发送方的流量控制。</li><li>以TCP最大报文段MSS(即TCP报文段的数据载荷部分)的个数作为讨论问题的单位，而不是以字节为单位(尽管TCP是面向字节流的)。</li></ul><p>所谓的4种方法是：</p><ul><li>慢开始算法</li><li>拥塞避免算法</li><li>快重传算法</li><li>快恢复算法</li></ul><p>我们必须清楚为了实现控制，收发双方需要维护的字段：</p><p><img src="https://files.seeusercontent.com/2026/04/15/Y1zf/image-20260415103315955.png" alt="image-20260415103315955"></p><p>请回忆流量控制时讲到的窗口：发送方需要维护两个状态变量，发送窗口swnd和拥塞窗口cwnd，接收方需要维护接收窗口rwnd。</p><p>发送窗口的大小等于接收窗口和拥塞窗口的最小值，保证同时满足拥塞控制和流量控制。</p><p>cwnd的维护原则：只要网络没有出现拥塞<br>拥塞窗口就再增大一些，但只要网络出现拥<br>塞，拥塞窗口就减少一些<br>判断网络出现拥塞的依据：没有按时收到应<br>当到达的TCP确认报文段而产生了超时重传。</p><p>接下来看过程：</p><p>发送方需要维护一个变量叫慢开始门限ssthresh，控制TCP从“慢开始”阶段切换到“拥塞避免”阶段的时机。</p><p><strong>当cwnd&lt;ssthresh时，使用慢开始算法。**<br>**当cwnd&gt;ssthresh时，停止使用慢开始算法而改用拥塞避免算法。</strong><br><strong>当cwnd=ssthresh时，既可使用慢开始算法，也可使用拥塞避免算法：</strong></p><p>慢开始算法：</p><p><img src="https://files.seeusercontent.com/2026/04/15/1zjE/image-20260415103938542.png" alt="image-20260415103938542"></p><p>起始，拥塞窗口为1，慢开始门限为16，发送窗口等于拥塞窗口，每完成一次“传输轮次”(一次发送分组和一次确认分组)拥塞窗口的值就变为两倍，呈指数式增长，这就是慢开始算法。</p><p>直到拥塞窗口的大小抵达慢开始门限，就要改用拥塞避免算法：</p><p><img src="https://files.seeusercontent.com/2026/04/15/k4Bq/image-20260415104248506.png" alt="image-20260415104248506"></p><p>改用拥塞避免算法后，完成传输轮次后拥塞窗口不再指数式增长，而是逐次+1增长，直到出现报文丢失。</p><p><img src="https://files.seeusercontent.com/2026/04/15/zTe2/image-20260415104426924.png" alt="image-20260415104426924"></p><p>出现报文丢失后，发送方记录此时的拥塞窗口大小，记下“拥塞窗口达到该值时，会出现拥塞”，并将该值的一半赋予慢开始门限，将拥塞窗口重置为1,重新开始慢开始算法。</p><p>随后达到门限后又改用拥塞避免算法，循环往复。</p><p><img src="https://files.seeusercontent.com/2026/04/15/tXa0/image-20260415104700582.png" alt="image-20260415104700582"></p><p>需要注意的是：“慢开始”是指一开始向网络注入的报文段少，而并不是指拥塞窗口CWD的值增长速度慢。</p><p>“拥塞避免”也并非指完全能够避免拥塞，而是指在拥塞避免阶段将cwd值控制为按线性规<br>律增长，使网络比较不容易出现拥塞。</p><p>这两种算法在1988年提出，那时候的TCP属于Tahoe版本</p><hr><p>后来1990年增加了两个新的拥塞控制算法，分别是：</p><ul><li>快重传</li><li>快恢复</li></ul><p>实际上，网络中的报文出现超时重传并不都是因为网络拥塞。还有可能因为高延迟，设置错误，生存时间过长等问题。但是发送方总是都认为是网络拥塞而启动慢开始算法，会导致传输效率下降。</p><p>采用快重传算法可以让发送方尽早知道发生了个别TCP报文段的丢失：</p><ul><li>“快重传”是指使发送方尽快（尽早）进行重传，而不是等重传计时器超时再重传。</li><li>这就要求接收方不要等待自己发送数据时才进行捎带确认，而是要立即发送确认，即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。</li><li>发送方一旦收到3个连续的重复确认，就将相应的报文段立即重传，而不是等该报文段的重传计时器超时再重传。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/Z9md/image-20260415105942820.png" alt="image-20260415105942820"></p><p>如图，发送方每次都在接收方的ACK报文抵达之前发送。当M3报文丢失，发送方发送M4时,接收方会发现接收到的报文段不是按序抵达的，就会重复发送M2报文段，重复该过程3次后发送方会发现M3丢失，立即重传M3.这样就能够在超时重传触发之前重传M3.</p><p>快重传通过重复确认降低了超时重传的可能性</p><p>对于个别丢失的报文段，发送方不会出现超时重传，也就不会误认为出现了拥塞而错误地把拥塞窗口cwnd的值减为1。实践证明，使用快重传可以使整个网络的吞吐量提高约20%。</p><p>与快重传算法配合使用的是快恢复算法，发送方一旦收到3个重复确认，就知道现在只是丢失了个别的报文段，于是不启动慢开始算法，而是执行快恢复算法。</p><p>发送方将慢开始门限ssthresh的值和拥塞窗口cwnd的值都调整为当前cwnd值的一半(而非置为1)，并开始执行拥塞避免算法。<br>也有的快恢复实现是把快恢复开始时的cwnd值再增大一些，即cwnd=新ssthresh+3。</p><ul><li>既然发送方收到了3个重复的确认，就表明有3个数据报文段已经离开了网络。</li><li>这3个报文段不再消耗网络资源而是停留在接收方的接收缓存中。</li><li>可见现在网络中不是堆积了报文段而是减少了3个报文段，因此可以适当把cwnd值增大一些。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/s4yT/image-20260415110834686.png" alt="image-20260415110834686"></p><p>拥塞控制的流程图如下：</p><p><img src="https://files.seeusercontent.com/2026/04/15/hNv7/image-20260415110920300.png" alt="image-20260415110920300"></p><p>需要说明的是，TCP拥塞控制仍然是计算机网络中的一个研究热点 TCP拥塞控制算法也还在不断地发展和变化。在这里仅讨论了其基本原理和要点，并没给出更多的细节。拥塞控制算法的设计和优化应该是因特网工程任务组那群书写规则和标准的人考虑的。</p><h3 id="TCP拥塞控制和网际层拥塞控制的关系"><a href="#TCP拥塞控制和网际层拥塞控制的关系" class="headerlink" title="TCP拥塞控制和网际层拥塞控制的关系"></a>TCP拥塞控制和网际层拥塞控制的关系</h3><p>这与网际层对IP数据报的丢弃策略紧密相关：</p><p>路由器的输入缓存（可看作缓存队列，以下简称为队列）通常都按照“先进先出FFO”的规则来处理到达的P数据报。由于队列长度总是有限的，因此当队列已满时，之后再到达的所有IP数据报都将被丢弃，这就叫作尾部丢弃策略.</p><p><img src="https://files.seeusercontent.com/2026/04/15/jh9X/image-20260415111336190.png" alt="image-20260415111336190"></p><p>为了避免网络中出现全局同步问题，在1998年提出了主动队列管理(Active Queue Management,AQM)。</p><ul><li>所谓“主动”，就是在路由器的队列长度达到某个阈值但还未满时就主动丢弃P数据报，而不是要等到路由器的队列已满时才不得不丢弃后面到达的P数据报，这样就太被动了。</li><li>应当在路由器队列长度达到某个值得警惕的数值时，也就是网络出现了某些拥塞征兆时，就主动丢弃到达的P数据报来造成发送方的超时重传，进而降低发送方的发送速率，因而有可能减轻网络的拥塞程度，甚至不出现网络拥塞。</li></ul><p>AQM的具体实现算法是RED，但是经过实践证明，IETF认为效果太有限了，所以不建议使用，新的算法仍在设计。</p><h3 id="TCP可靠传输的实现"><a href="#TCP可靠传输的实现" class="headerlink" title="TCP可靠传输的实现"></a>TCP可靠传输的实现</h3><p>这个小节，我不知道怎么写。</p><blockquote><p>注意：ack在选择重传协议与TCP协议中并不完全相同。<br>在选择重传协议中，ack表明序号到n为止的数据已正确接收，现在期望收到序号为n+1的数据。<br>在TCP协议中，ack表明序号到n-1为止的数据已正确接收，现在期望收到序号为n的数据。但是这种事情无关痛痒，不影响我们理解整体流程。</p></blockquote><p>以下过程不考虑拥塞问题：</p><p><img src="https://files.seeusercontent.com/2026/04/15/jx9P/image-20260415114853128.png" alt="image-20260415114853128"></p><p>发送方在没有收到接收方确认的情况下，可以把序号落入发送窗口内的数据依次全部发送出去，</p><p>凡是已经发送过的数据，在未收到确认之前都必须暂时保留，以便在超时重传时使用。</p><p>发送窗口具有前沿和后沿，为了标记前沿和后沿之内和之外的不同数据，需要使用三个指针来区分，如上图。</p><p>而接收窗口也具有前沿和后沿，且接收方只能对按序收到的数据中的最高序号给出确认。</p><p><img src="https://files.seeusercontent.com/2026/04/15/rtV7/image-20260415115732318.png" alt="image-20260415115732318"></p><p>如图，31号数据报文在链路中迷失，接收方只能给出“前30号数据已被接受”的确认。</p><p><img src="https://files.seeusercontent.com/2026/04/15/pHh9/image-20260415120002530.png" alt="image-20260415120002530"></p><h4 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h4><p>虽然发送方的发送窗口是根据接收方的接收窗口设置的，但在同一时刻，发送方的发送窗口并不总是和接收方的<br>接收窗口一样大，这是因为：</p><ul><li>网络传送窗口值需要经历一定的时间滞后，并且这个时间还是不确定的。</li><li>发送方还可能根据网络当时的拥塞情况适当减小自己的发送窗口尺寸。</li></ul><p>对于不按序到达的数据应如何处理，TCP并无明确规定。</p><ul><li>如果接收方把不按序到达的数据一律丢弃，那么接收窗口的管理将会比较简单，但这样做对网络资源的利用不利，因为发送方会重复传送较多的数据。</li><li>TCP通常对不按序到达的数据先临时存放在接收窗口中，等到字节流中所缺少的字节收到后，再按序交付上层的应用进程。<br>TCP要求接收方必须有累积确认（这一点与选择重传协议不同）和捎带确认机制。这样可以减小传输开销。接收方可以在合适的时候发送确认，也可以在自己有数据要发送时把确认信息顺便捎带上。</li><li>接收方不应过分延迟发送确认，否则会导致发送方不必要的超时重传，这反而浪费了网络资源。TCP标准规定确认推迟的时间不应超过0.5秒。若收到一连串具有最大长度的报文段，则必须每隔一个报文段就发送一个确认[RFC1122]</li><li>捎带确认实际上并不经常发生，因为大多数应用程序很少同时在两个方向上发送数据。<br>TCP的通信是全双工通信。通信中的每一方都在发送和接收报文段。因此，每一方都有自己的发送窗口和接收窗口。在谈到这些窗口时，一定要弄清楚是哪一方的窗口。</li></ul><h3 id="TCP超时重传的选择"><a href="#TCP超时重传的选择" class="headerlink" title="TCP超时重传的选择"></a>TCP超时重传的选择</h3><p>TCP超时重传时间RTO的选择是TCP最复杂的问题之一。</p><p><img src="https://files.seeusercontent.com/2026/04/15/p7uK/image-20260415131841478.png" alt="image-20260415131841478"></p><p><img src="https://files.seeusercontent.com/2026/04/15/Yzi9/image-20260415131851543.png" alt="image-20260415131851543"></p><p>RTO设置的短了，就会在ACK到达之前触发，引发不必要的重传。如果长了，会导致网络空闲时间增大，降低了传输效率。</p><p>所以RTO应该设置为略大于往返时间RTT。</p><p>但是问题是：</p><p>TCP下层是复杂的因特网环境：</p><ul><li>主机A所发送的报文段可能只经过一个高速率的局域网</li><li>也可能经过多个低速率的网络</li><li>并且每个IP数据报的转发路由还可能不同</li></ul><p>这就意味着RTO必须是动态的。</p><ul><li>不能直接使用略大于某次测量得到的往返时间RTT样本的值作为超时重传时间RTO。</li><li>但是，可以利用每次测量得到的RTT样本计算加权平均往返时间RTTs,这样可以得到比较平滑的往返时间。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/15/5Nfg/image-20260415132808384.png" alt="image-20260415132808384"></p><p>显然，超时重传时间RTO的值应略大于加权平均往返时间RTTs的值(而不是某个RTT样本的值)</p><p><img src="https://files.seeusercontent.com/2026/04/15/Mj3n/image-20260415133034320.png" alt="image-20260415133034320"></p><p>更让人抓狂的是：RTT的计算严重依赖RTT的测量。如果所测量到的RTT样本不正确，那么所计算出的RTTs和RTT,自然就不正确，进而所计算出的RTO也就不正确。<br>然而，RTT的测量确实是比较复杂的。</p><p><img src="https://files.seeusercontent.com/2026/04/15/mp9G/image-20260415133312788.png" alt="image-20260415133312788"></p><p>发送方出现超时重传时，难以判断RTT属于第一次传输还是重传数据报。</p><h4 id="Karn算法"><a href="#Karn算法" class="headerlink" title="Karn算法"></a>Karn算法</h4><p>在计算加权平均TTs时，只要报文段重传了，就不采用其RTT样本。换句话说，出现重传时，不重新计算 RTTs,进而RTO也不会重新计算。</p><p>但是会引发新问题：</p><p>设想出现这样的情况：报文段的时延突然增大很多并且之后很长一段时间都会保持这种时延（这可能是因为网络拓扑发生了变化)。因此在原来得出的RTO内不会收到确认报文段，于是就重传报文段。但根据Karn算法，不考虑重传的报文段的RTT样本，因此RTO就无法更新，这会导致报文段反复被重传</p><p>所以需要修正：</p><p>报文段每重传一次，就把 RTO增大一些。典型的做法是将新RTO的值取为旧RTO的2倍，</p><h3 id="TCP的选择确认"><a href="#TCP的选择确认" class="headerlink" title="TCP的选择确认"></a>TCP的选择确认</h3><p>在之前介绍TCP的快重传和可靠传输时，TCP接收方只能对按序收到的数据中的最高序号给出确认。当发送方超时重传时，接收方之前已收到的未按序到达的数据也会被重传。</p><p>但是能否设法只传送缺少的数据而不重传已经正确到达，只是未按序到达的数据呢？</p><p>可以，这就是TCP的选择确认功能。</p><p><img src="https://files.seeusercontent.com/2026/04/15/sT3e/image-20260415133929105.png" alt="image-20260415133929105"></p><p>通过指明缺少的字段序号就能够让发送方发送指定的字段。</p><p>但是TCP首部没有预留字段来实现选择确认功能，所以只能使用扩展首部来完成。</p><ul><li>允许SACK选项(1字节)</li><li>SACK选项长度(1字节)</li><li>指明一个边界需要4字节(因为序号为4字节)</li><li>一个字节块需要两个边界，即8字节。<br>因此，最多指明4个字节块的信息。</li></ul><p>但实际上这种选择确认因为实现复杂，且SACK相关文档并没有指明发送方应当怎样响应SACK。最后不了了之，实际上大多数的TCP实现还是重传所有未被确认的数据块。</p><p>第五章结束。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第五章：运输层&quot;&gt;&lt;a href=&quot;#第五章：运输层&quot; class=&quot;headerlink&quot; title=&quot;第五章：运输层&quot;&gt;&lt;/a&gt;第五章：运输层&lt;/h1&gt;&lt;h4 id=&quot;进程间基于网络的通信&quot;&gt;&lt;a href=&quot;#进程间基于网络的通信&quot; class=&quot;head</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第四章：网络层</title>
    <link href="https://blog.willmo.top/posts/Computer%20Networks%20Chapter%204:%20Network%20Layer/"/>
    <id>https://blog.willmo.top/posts/Computer%20Networks%20Chapter%204:%20Network%20Layer/</id>
    <published>2026-04-20T11:44:25.175Z</published>
    <updated>2026-04-20T11:46:08.451Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第四章：网络层"><a href="#第四章：网络层" class="headerlink" title="第四章：网络层"></a>第四章：网络层</h1><blockquote><p>网际层和网络层本质上并无不同，只不过属于不同模型。</p></blockquote><h2 id="分组转发和路由选择"><a href="#分组转发和路由选择" class="headerlink" title="分组转发和路由选择"></a>分组转发和路由选择</h2><p>网络层的主要任务就是<strong>将分组从源主机经过多个网络和多段链路传输到目的主机</strong>，可以将该任务划分为分组转发和路由选择两种重要的功能。</p><p><strong>转发表</strong>通常指数据链路层（如交换机）使用的表，它根据MAC地址决定将数据帧从哪个物理端口转发出去，实现同一网络内的设备间通信。</p><p><strong>路由表</strong>是网络层（如路由器）使用的表，它根据目标IP地址和路由算法，决定数据包应该发往哪个“下一跳”路由器或接口，从而实现不同网络之间的互联。</p><p>主机A若要传输信息到主机B，就需要将数据包发到路由器R1，R1通过转发表来决定转发到何处。转发表由上层路由表决定。在情况简单时可以手动配置路由表。</p><p><img src="https://files.seeusercontent.com/2026/04/08/wWh8/image-20260408131848487.png" alt="image-20260408131848487"></p><h3 id="网络层往上层提供的服务"><a href="#网络层往上层提供的服务" class="headerlink" title="网络层往上层提供的服务"></a>网络层往上层提供的服务</h3><h4 id="面向连接的虚电路服务"><a href="#面向连接的虚电路服务" class="headerlink" title="面向连接的虚电路服务"></a>面向连接的虚电路服务</h4><p>核心思想是“可靠通信应由网络自身来保证”。</p><p>必须首先建立网络层连接一虚电路(Virtual Circuit,VC),以保证通信双方所需的一切网络资源。</p><p>随即通信双方沿着已建立的虚电路发送分组。</p><p>通信结束后，需要释放之前建立的虚电路。</p><p><img src="https://files.seeusercontent.com/2026/04/08/6Mri/image-20260408132125135.png" alt="image-20260408132125135"></p><p>虚电路是逻辑上的链接，不是物理上的链接，这和电话通信是不同的。</p><p>这种通信方式如果再使用可靠传输的网络协议，就可使所发送的分组最终正确（无差错按序到达、不丢失、不重复)到达接收方。</p><h4 id="无连接数据报服务"><a href="#无连接数据报服务" class="headerlink" title="无连接数据报服务"></a>无连接数据报服务</h4><p>核心思想是“可靠通信应由用户主机来保证”。<br>不需要建立网络层连接。<br>每个分组可走不同的路径。因此，每个分组的首部都必须携带目的主机的完整地址。</p><p>通信结束后，没有需要释放的链路。</p><p><img src="https://files.seeusercontent.com/2026/04/08/x0Mu/image-20260408132457062.png" alt="image-20260408132457062"></p><p>这种方式可能导致误码丢失重复失序。</p><p>该方式将相对简单的尽最大努力(即不可靠)的分组交付功能置于因特网核心，将复杂的用户处理功能至于因特网边缘(用户主机和运输层)</p><p>由于网络自身不提供端到端的可靠传输服务，这就使得网络中的路由器可以做得比较简单，大大降低了网络造价。</p><p>这种设计思想的运行方式灵活，适应性强。</p><p>所以，网络层想上层提供的主要是无连接的数据报服务。</p><h3 id="网际协议IP"><a href="#网际协议IP" class="headerlink" title="网际协议IP"></a>网际协议IP</h3><p>网际协议(Internet Protocol,,IP)是TCP体系结构网际层中的核心协议，承上启下地链接了运输层和网络接口层的工作。</p><p><img src="https://files.seeusercontent.com/2026/04/08/7wPh/image-20260408134035769.png" alt="image-20260408134035769"></p><p>因此，网络层又名网际层或IP层。</p><h3 id="异构网络互联"><a href="#异构网络互联" class="headerlink" title="异构网络互联"></a>异构网络互联</h3><p>因特网是由全球数以百万计的网络通过路由器相连，其拓扑，性能，协议不相同，需要在路由器作为统一通信节点。</p><p>如果他们都使用IP协议，当IP网上的主机进行通信时就好像在一个单个网络上通信一样，它们看不见互连的各网络的具体异构细节。</p><p><img src="https://files.seeusercontent.com/2026/04/08/7slY/image-20260408134316188.png" alt="image-20260408134316188"></p><h3 id="IPv4"><a href="#IPv4" class="headerlink" title="IPv4"></a>IPv4</h3><p>IPv4地址是给因特网(Internet)上的每一个主机（或路由器）的每一个接口分配的一个在全世界范围内唯一的32比特的标识符。</p><p><img src="https://files.seeusercontent.com/2026/04/08/xX9k/image-20260408134624741.png" alt="image-20260408134624741"></p><p>IPv4地址由因特网名字和数字分配机构(Internet CoRIPoration for Assigned Names and Numbers,ICANN)进行分配，不接受个人申请，且IPv4地址已经被分配完毕，现在是IPv6时代。</p><p>IPv4编址经历了三个阶段：</p><p><img src="https://files.seeusercontent.com/2026/04/08/iCg4/image-20260408135030516.png" alt="image-20260408135030516"></p><p>IPv4地址采用点分十进制表示方法以方便用户使用。</p><p><img src="https://files.seeusercontent.com/2026/04/08/0Gie/image-20260408135229433.png" alt="image-20260408135229433"></p><p>小学生都会做的进制转换就不赘述了。</p><h4 id="IPv4分类编址方法"><a href="#IPv4分类编址方法" class="headerlink" title="IPv4分类编址方法"></a>IPv4分类编址方法</h4><p>他将32比特的IPv4地址分为两部分</p><ul><li>网络号</li><li><ul><li>标志主机(或路由器)的接口所连接到的网络，同一个网络中，不同主机（或路由器）的接口的IPv4地址的网络号必须相同，表示它们属于同一个网络</li></ul></li><li>主机号</li><li><ul><li>标志主机(或路由器)的接口，同一个网络中，不同主机（或路由器）的接口的IPv4地址的主机号必须各不相同，以便区分各主机（或路由器）的接口。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/08/wJo1/image-20260408135845379.png" alt="image-20260408135845379">关于前几位是网络号，后几位是主机号的问题，分类编制有几类编制方式：</p><p><img src="https://files.seeusercontent.com/2026/04/08/0mAs/image-20260408140027328.png" alt="image-20260408140027328"></p><p>A类、B类和C类地址都是单播地址，只有单播地址可以分配给网络中的主机（或路由器）的各接口。<br>最小主机号为“全0”的地址是网络地址，不能分配给主机（或路由器）的各接口。</p><p>最大主机号为“全1”的地址是广播地址，不能分配给主机（或路由器）的各接口。</p><p><strong>一般地，我们将最小地址分配作为网络地址，将最大地址分配作为广播地址。</strong></p><h4 id="A类地址"><a href="#A类地址" class="headerlink" title="A类地址"></a>A类地址</h4><p><img src="https://files.seeusercontent.com/2026/04/08/Nc6x/image-20260408140348969.png" alt="image-20260408140348969"></p><h4 id="B类地址"><a href="#B类地址" class="headerlink" title="B类地址"></a>B类地址</h4><p><img src="https://files.seeusercontent.com/2026/04/08/uf8B/image-20260408140953604.png"></p><h4 id="C类地址"><a href="#C类地址" class="headerlink" title="C类地址"></a>C类地址</h4><p><img src="https://files.seeusercontent.com/2026/04/08/vVc0/image-20260408141132434.png" alt="image-20260408141132434"></p><p>横向对比一下：</p><p><img src="https://files.seeusercontent.com/2026/04/08/Sk7x/image-20260408141151629.png"></p><h4 id="IPv4子网编址方法"><a href="#IPv4子网编址方法" class="headerlink" title="IPv4子网编址方法"></a>IPv4子网编址方法</h4><p>分类编址方法不够灵活且容易造成大量地址浪费，划分子网编址方法对其进行改进。</p><p><img src="https://files.seeusercontent.com/2026/04/08/tZ4l/image-20260408142605131.png" alt="image-20260408142605131"></p><p>如果可以从IPv4地址的主机号部分借用一些比特作为子网号来区分不同的子网，就可以利用原有网络中剩余的大量IPv4地址，而不用申请新的网络地址了。</p><p>我们或计算机又如何知道在分类地址中，主机号有多少比特被借用作为子网号了呢？这就需要子网掩码。</p><h4 id="子网掩码"><a href="#子网掩码" class="headerlink" title="子网掩码"></a>子网掩码</h4><p>子网掩码可以表明分类IPv4地址的主机号部分被借用了几个比特作为子网号。<br>与IPv4地址类似，子网掩码也是由32比特构成的。<br>■用左起多个连续的比特1对应IPv4地址中的网络号和子网号：<br>☐之后的多个连续的比特0对应IPv4地址中的主机号。</p><p><img src="https://files.seeusercontent.com/2026/04/08/Cn6z/image-20260408142914898.png" alt="image-20260408142914898"></p><p>将划分子网的IPv4地址与相应的子网掩码进行逐比特的逻辑与运算，就可得到该IPv4地址所在子网的网络地址。</p><p>来看一个例子：</p><p><img src="https://files.seeusercontent.com/2026/04/08/yOb9/image-20260408143247535.png" alt="image-20260408143247535"></p><p>若依靠子网掩码，从主机号借用几个比特给网络号，会导致主机号可分配范围减小而网络号可分配范围加大。</p><p><img src="https://files.seeusercontent.com/2026/04/08/jbB6/image-20260408143733546.png" alt="image-20260408143733546"></p><p>如图，可分配给单网络的主机号从C类地址的254个削减到到126个。</p><h5 id="默认子网掩码"><a href="#默认子网掩码" class="headerlink" title="默认子网掩码"></a>默认子网掩码</h5><p>默认子网掩码是指在未划分子网的情况下使用的子网掩码。</p><p><img src="https://files.seeusercontent.com/2026/04/08/7gbJ/image-20260408144022671.png" alt="image-20260408144022671"></p><h4 id="IPv4无分类编址方法"><a href="#IPv4无分类编址方法" class="headerlink" title="IPv4无分类编址方法"></a>IPv4无分类编址方法</h4><p>这是为了缓解IPv4地址紧张的问题。</p><p><img src="https://files.seeusercontent.com/2026/04/08/cp7N/image-20260408144548218.png" alt="image-20260408144548218"></p><h4 id="地址掩码"><a href="#地址掩码" class="headerlink" title="地址掩码"></a>地址掩码</h4><p>有32比特构成，和子网掩码很相似。</p><ul><li>用左起多个连续的比特1对应IPv4地址中的网络前缀：</li><li>之后的多个连续的比特0对应IPv4地址中的主机号。</li></ul><p>为了简便起见，可以不明确给出配套的地址掩码的点分十进制形式，而是在无分类编址的IPV4地址后面加上斜线“/”，在斜线之后写上网络前缀所占的比特数量（也就是地址掩码中左起连续比特1的数量),这种记法称为斜线记法。</p><p>如128.14.35.7 / 20，像这样一个就被称为一个CIDR地址块。</p><p>只要知道CIDR地址块中的任何一个地址，就可以知道该地址块的以下全部细节：</p><ul><li>地址块中的最小地址</li><li>地址块中的最大地址</li><li>地址块中的地址数量</li><li>地址块中聚合某类网络(A类、B类、C类)的数量</li><li>地址掩码</li></ul><p><img src="https://files.seeusercontent.com/2026/04/08/gIq6/image-20260408152106431.png" alt="image-20260408152106431"></p><p>使用CDR的一个好处是，可以根据客户的需要分配适当大小的CDR地址块，因此可以更加有效地分配 IPv4的地址空间。</p><p><img src="https://files.seeusercontent.com/2026/04/08/7Uuu/image-20260408152501837.png" alt="image-20260408152501837"></p><p>CIDR的另一个好处是路由聚合，又名构造超网。</p><p>R1链接了五个网络，路由聚合就是找这五个地址的共同前缀，并生成一个新的CIDR块来描述或定义这五个网络聚成的单网络。</p><p><img src="https://files.seeusercontent.com/2026/04/08/h0Hx/image-20260408153006740.png" alt="image-20260408153006740"></p><p>所以，R1可以只发给这一条<code>172.1.4.9 / 22</code>发给R2.</p><h4 id="IPv4地址的应用规划"><a href="#IPv4地址的应用规划" class="headerlink" title="IPv4地址的应用规划"></a>IPv4地址的应用规划</h4><p>IPv4地址的应用规划是指将给定的IPv4地址块（或分类网络）划分成若干个更小的地址块（或子网），并将这些地址块（或子网）分配给互联网中的不同网络，进而可以给各网络中的主机和路由器的接口分配 IPv4地址。</p><p>子网掩码分为两种：</p><ul><li>定长的子网掩码</li><li><ul><li>所划分出的每一个子网都使用同一个子网掩码。</li><li>每个子网所分配的IP地址数量相同，容易造成地址资源的浪费。</li></ul></li><li>变长的子网掩码</li><li><ul><li><strong>所划分出的每一个子网能够使用不同的子网掩码</strong>。</li><li>每个子网分配的IP地址数量可以不同，尽可能减少对地址的浪费。</li></ul></li></ul><p>举个定长子网掩码的例子：</p><p><img src="https://files.seeusercontent.com/2026/04/08/3vOn/image-20260408193202685.png" alt="image-20260408193202685"></p><p>网络1需要P地址数量为9，网络2需要P地址数量为28，网络3需要P地址数量为15，网络4需要P地址数量为13，网络5需要P地址数量为4。一共有5个网络，需要子网数量需要达到5以上，所以借用主机号3个比特位。</p><p><img src="https://files.seeusercontent.com/2026/04/08/4Vrn/image-20260408193826681.png" alt="image-20260408193826681"></p><p>答案就是：</p><p><img src="https://files.seeusercontent.com/2026/04/08/D6oq/image-20260408193915631.png" alt="image-20260408193915631"></p><p>你看，本来一共是8位比特，分走3位后，剩下5位，那就是2^5 =32个地址，但是网络5只需要4个网络地址，这就浪费了28个地址无法被使用，在某些例子中，这种浪费更严重。</p><p>再来看变长的例子：</p><p><img src="https://files.seeusercontent.com/2026/04/08/Dy5s/image-20260408194646377.png" alt="image-20260408194646377"></p><p>因为允许给每个子网都使用不用的子网掩码，所以能够针对每个子网不同的设备数量来选择借用多少主机位。</p><p><img src="https://files.seeusercontent.com/2026/04/08/3Vif/image-20260408200644441.png" alt="image-20260408200644441"></p><h3 id="IPv4地址和Mac地址关系"><a href="#IPv4地址和Mac地址关系" class="headerlink" title="IPv4地址和Mac地址关系"></a>IPv4地址和Mac地址关系</h3><p><img src="https://files.seeusercontent.com/2026/04/08/nF6f/image-20260408201636792.png" alt="image-20260408201636792"></p><p>接下来是数据包在传输过程中，IPV4和Mac地址的变化情况。</p><p><img src="https://files.seeusercontent.com/2026/04/08/rCm5/image-20260408202605333.png" alt="image-20260408202605333"></p><p>在数据包的传送过程中，数据包的源P地址和目的IP地址保持不变；<br>在数据包的传送过程中，数据包的源MAC地址和目的MAC地址逐链路（或逐网络）改变。</p><p>那么，因特网为什么要使用IP地址和MAC地址这两种类型的地址来共同完成寻址工作，仅使用MAC地址进行通信不可以吗？</p><p>不行，因为：</p><ul><li>因特网中的每台路由器的路由表中就必须记录因特网上所有主机和路由器各接口的MAC地址。</li><li>手工给各路由器配置路由表几乎是不可能完成的任务，即使使用路由协议让路由器通过相互交换路由信息来自动构建路由表，也会因为路由信息需要包含海量的MAC地址信息而严重占用通信资源。</li><li>包含海量MAC地址的路由信息需要路由器具备极大的存储空间，并且会给分组的查表转发带来非常大的时延。</li></ul><p>因特网的网际层使用IP地址进行寻址，就可使因特网中各路由器的路由表中的路由记录的数量大大减少，因为只需记录部分网络的网络地址，而不是记录每个网络中各通信设备的各接口的MAC地址。</p><p>查表转发的结果可以指明P数据报的下一跳路由器的IP地址，但无法指明该P地址所对应的 MAC地址。因此，在数据链路层封装该P数据报成为帧时，帧首部中的目的MAC地址字段就无法填写，该问题需要使用网际层中的地址解析协议ARIP来解决。</p><h3 id="地址解析协议ARIP"><a href="#地址解析协议ARIP" class="headerlink" title="地址解析协议ARIP"></a>地址解析协议ARIP</h3><p>地址解析协议（ARIP）是用于将网络层的IP地址解析为数据链路层的物理地址（MAC地址）的协议。</p><p>如果没有ARIP，数据链路层在封装MAC帧时就不知道应该给数据分组加上什么MAC地址。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260409123726901.png" alt="image-20260409123726901"></p><p>实际上，每台主机都会维护一个ARIP高速缓存表，内存有MAC地址和IP地址的对应关系，在封装时能够根据IP找到对应的MAC地址。</p><table><thead><tr><th>IP地址</th><th>MAC地址</th><th>类型</th></tr></thead><tbody><tr><td>192.168.0.3</td><td>00-0C-CF-B8-4A-82</td><td>动态</td></tr><tr><td>192.168.0.4</td><td>00-01-C7-D3-B2-B5</td><td>静态</td></tr><tr><td>………..</td><td>………</td><td>…..</td></tr></tbody></table><p>如果没有找到，那主机A就会发送ARIP请求报文，这是一条广播报文，需要封装在目的MAC地址为FF-FF-FF-FF-FF-FF的广播帧中发送。该广播帧会抵达每个主机。其他主机接受到后会将帧中的IP地址与自己对比，如果不对就不理会，如果正确就发回携带自身MAC地址的ARIP响应报文。此时主机A的ARIP表会自更，这样下次就能直接在表中找到对应的MAC地址。</p><p><img src="https://files.seeusercontent.com/2026/04/09/Ary8/image-20260409193538289.png" alt="image-20260409193538289"></p><p>随即将ARIP缓存表更新为：</p><table><thead><tr><th>IP地址</th><th>MAC地址</th><th>类型</th></tr></thead><tbody><tr><td>192.168.0.3</td><td>00-0C-CF-B8-4A-82</td><td>动态</td></tr><tr><td>192.168.0.4</td><td>00-01-C7-D3-B2-B5</td><td>静态</td></tr><tr><td><strong>192.168.0.2</strong></td><td><strong>00-E0-F9-A3-43-77 7</strong></td><td><strong>动态</strong></td></tr><tr><td>……</td><td>…..</td><td>….</td></tr></tbody></table><p>需要额外说明的是：IP-MAC的键值对往往分为静态和动态：</p><ul><li>动态：通过ARIP自动获取，生命周期默认两分钟</li><li>静态：手工配置，不同操作系统生命周期不同，例如系统重启后不存在或重启后依旧有效</li></ul><p>IP-MAC的对应关系不是永久不变的，比如主板更换了新的网卡，所以是有生命周期的。</p><p>注意，ARIP不能跨网络使用，只能在局域网中 逐层寻找目标主机并等待响应。</p><p>注意：</p><ul><li>由于ARIP协议的主要用途是从网际层使用的P地址解析出在数据链路层使用的MAC地址。因此，有的教材将ARIP协议划归在网际层，而有的教材将ARIP协议划归在数据链路层。这两种做法都是可以的。</li><li>除了本节课介绍的ARIP请求报文和响应报文，ARIP协议还有其他类型的报文，例如用于检查P地址冲突的“无故ARIP”(Gratuitous ARIP)</li><li>由于ARIP协议很早就制定出来了(1982年11月)<br>,当时并没有考虑网络安全问题。因此，ARIP协议没<br>有安全验证机制，存在<strong>ARIP欺骗和攻击</strong>等问题。</li></ul><h4 id="IP数据报的发送和转发"><a href="#IP数据报的发送和转发" class="headerlink" title="IP数据报的发送和转发"></a>IP数据报的发送和转发</h4><p>包含两个过程：</p><ul><li>主机发送IP数据报</li><li>路由器转发IP数据报</li></ul><p>为了集中讲解，我们忽略使用ARIP协议获取MAC地址的过程和以太网交换机自学习和转发帧的过程。</p><p>我们都直到，通过交换机而不通过路由器，在同一个网络中通信属于“直接交付”</p><p>通过路由器在不同网络中通信属于“间接交付”</p><p>那么，主机如何判断目标主机是否与自己属于同一网络呢？</p><ul><li>主机会调出目的主机的IP地址并根据地址掩码得出网络号，随后与自己的IP地址比对，如果相同就是同一个网络</li></ul><p>那么，主机如何判断应该借助哪个路由器来转发呢？</p><ul><li>实际上，每个主机都默认指认一个路由器，该地址被称为默认网关，主机默认将需要间接交互的数据分组转发到默认网关的位置。</li></ul><p>基于IP数据报首部中的目的P地址在路由表中进行查询：</p><ul><li>若找到匹配的路由条目，则按该路由条目的指示进行转发；</li><li>否则丢弃该P数据报，并向发送该IP数据报的源主机发送差错报告。</li></ul><p>例如：主机A准备给主机D发送数据分组：</p><p><img src="https://files.seeusercontent.com/2026/04/09/gyV2/image-20260409201130825.png" alt="image-20260409201130825"></p><p>路由表中的每一项叫做路由条目，路由条目可以是自学习，也可以是用户或管理员自行配置。</p><p>路由器接收到数据报后，将其中的目的地址结合地址掩码解析出网络地址后，根据路由表找到对应的端口号，将其转发出去，抵达主机D。</p><p>在路由器的实际研发过程中，需要设计很好的数据结构(甚至是专用硬件)以便提高查找速度。</p><p>补充一下路由器对广播数据报的处理：</p><p>主机A发出广播帧，交换机将广播帧转发到除了主机A外的每一个端口，而路由器默认不转发广播IP数据报，即路由器隔离广播域，如果因特网中数量巨大的路由器收到广播IP数据报后都进行转发，则会造成巨大的广播风暴，严重浪费因特网资源。</p><h4 id="IPv4数据报的首部格式"><a href="#IPv4数据报的首部格式" class="headerlink" title="IPv4数据报的首部格式"></a>IPv4数据报的首部格式</h4><ul><li>IPv4数据报的首部格式及其内容是实现IPv4协议各种功能的基础。</li><li>在TCP/IP标准中，各种数据格式常常以32比特（即4字节）为单位来描述。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/10/7xtL/image-20260410142445416.png" alt="image-20260410142445416"></p><ul><li>固定部分是指每个IPv4数据报都必须要包含的部分。</li><li>某些IPv4数据报的首部，除了包含20字节的固定部分，还包含一些可选的字段来增加IPV4数据报的功能</li></ul><p>下面来介绍各个字段的作用:</p><ul><li><p>版本:长度为4个比特，用来表示IP协议的版本。<br>通信双方使用的P协议的版本必须一致。目前广泛使用的P协议的版本号为4（即IPV4)</p></li><li><p>首部长度<br>长度为4个比特，该字段的取值以4字节为单位，用来表示IPV4数据报的首部长度。<br>最小取值为二进制的0101，即十进制的5，再乘以4字节单位，表示IPv4数据报首部只有20字节固定部分。<br>最大取值为二进制的1111，即十进制的15，再乘以4字节单位，表示IPv4数据报首部包含20字节固定部分和最大40字节可变部分。</p></li><li><p>可选字段<br>长度从1字节到40字节不等，用来支持排错、测量以及安全措施等功能。这同时让首部长度可变，增加了路由器处理开销</p></li><li><p>填充<br>用来确保<strong>IPv4数据报的首部长度是4字节的整数倍</strong>(因为首部长度是4字节的整数倍)，使用全0进行填充。</p></li><li><p>区分服务<br>长度为8个比特，用来获得更好的服务。<br>该字段在旧标准中叫作服务类型，但实际上一直没有被使用过。<br>1998年，因特网工程任务组IETF把这个字段改名为区分服务。</p><p>利用该字段的不同取值可提供不同等级的服务质量。<br>只有在使用区分服务时该字段才起作用，一般情况下都不使用该字段。</p></li><li><p>总长度<br>长度为16个比特，该字段的取值以字节为单位，用来表示IPv4数据报的长度（首部长度+数据载荷长度）最大取值为2进制的16个比特1，即十进制的65535。配合首部长度能够计算出数据载荷长度</p></li></ul><p>在此需要讲一下“IPv4数据报分片的概念：</p><p>当IPv4数据报长度超过MTU时，无法封装成帧，就需要将IPV4数据报分为若干个更小的数据报。</p><ul><li>标识<br>长度为16个比特，属于同一个IPv4数据报的各分片数据报应该具有相同的标识。<br>IP软件会维持一个计数器，每产生一个IPv4数据报，计数器值就加1，并将此值赋给标识字段。</li><li>标志<br>最低位(More Fragment,MF)<br>MF=1表示本分片后面还有分片<br>MF=0表示本分片后面没有分片<br>中间位(Don’t Fragment,DF)<br>DF=1表示不允许分片<br>DF=0表示允许分片<br>最高位为保留位，必须设置为0</li><li>片偏移<br>长度为13个比特，该字段的取值<br>以8字节为单位，用来指出分片<br>IPv4数据报的数据载荷偏移其在<br>原IPv4数据报的位置有多远。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/10/uG1n/image-20260410145733549.png" alt="image-20260410145733549"></p><p>如果分片2经过某个网络时还需要进行分配，那就根据MTU再分片：</p><p><img src="https://files.seeusercontent.com/2026/04/10/vc5T/image-20260410145831803.png" alt="image-20260410145831803"></p><ul><li><p>生存时间字段(TTL)</p><p>长度为8个比特，最大取值为二进制的11111111，即十进制的255。该字段的取值最初以秒为单位。因此，IPv4数<br>据报的最大生存时间最初为255秒。路由器转发IPV4数据报时，将其首部中该字段的值减去该数据报在路由器上所耗费的时间，若结果不为0就转发，否则就丢弃。生存时间字段后来改为以“跳数”为单位，路由器收到待转发的IPV4数据报时，将其首部中的该字段的值减1，若结果不为0就转发，否则就丢弃。</p></li></ul><p>生存时间是为了防止被错误路由的IPv4数据报无限制地在因特网兜圈。</p><p><img src="https://files.seeusercontent.com/2026/04/10/Nut4/image-20260410150509965.png" alt="image-20260410150509965">如果没有TTL，数据报就会在R1和R2的闭合环路中永久兜圈。</p><ul><li><p>协议</p><p>长度为8个比特，用来指明IPV4数据报的数据载荷是何种协议数据单元PDU。</p><table><thead><tr><th>协议名称</th><th>ICMP</th><th>IGMP</th><th>TCP</th><th>UDP</th><th>IPv6</th><th>OSPF</th></tr></thead><tbody><tr><td>协议字段值</td><td>1</td><td>2</td><td>6</td><td>17</td><td>41</td><td>89</td></tr></tbody></table></li><li><p>首部检验和</p><p>长度为16个比特，用于检测IPv4数据报在传输过程中其首部是否出现了差错。 IPv4数据报每经过一个路由器，其首部中的某些字段的值（例如生存时间TTL、标志以及片偏移等）都可能发生变化，因此路由器都要重新计算一下首部检验和。</p></li><li><ul><li>检验和的计算方法不仅用于IP协议，还用于运输层的用户数据报协议UDP和传输控制协议TCP,常被称为因特网检验和(Internet Checksum)。这种检验和的检错性能虽然不如CRC,但更易用软件实现。</li></ul></li></ul><blockquote><p>由于网际层并不向其高层提供可靠传输的服务，并且计算首部检验和是一项耗时的操作,因此在Pv6中，路由器不再计算首部检验和，从而更快转发P数据报。</p></blockquote><h3 id="静态路由配置"><a href="#静态路由配置" class="headerlink" title="静态路由配置"></a>静态路由配置</h3><p>静态路由配置是指用户或网络运维人员使用路由器的相关命令给路由器人工配置路由表。</p><p>人工配置方式简单、开销小、但不能及时适应网络状态（流量、拓扑等）的变化，一般只在小规模网络中采用。</p><p><img src="https://files.seeusercontent.com/2026/04/10/4evD/image-20260410152215417.png" alt="image-20260410152215417"></p><p>直连路由条目是路由器自行得出的。</p><p><strong>图中的静态路由条目是人工手动配置的</strong>。</p><h4 id="默认路由"><a href="#默认路由" class="headerlink" title="默认路由"></a>默认路由</h4><p>将所有路由都加进路由表是一件荒诞的事情，这会让路由表的大小难以估量，并降低查表转发的速度。所以给其他网络都设立一个共同出口，就是默认路由，换句话说：</p><p><strong>默认路由是一种特殊的静态路由，可以理解为网络中的“万能备用出口”或“最后选择的路径”。</strong></p><p>默认路由条目中的目的网络0.0.0.0/0，其中<br>0.0.0.0表示任意网络，而网络前缀<br>“/01”(相应的地址掩码为0.0.0.0)是最短的网络前缀.<br>路由器在查找转发表转发P数据报时，遵循<br>“最长前缀匹配”的原则，因此默认路由条目的匹配优先级最低。</p><table><thead><tr><th>目的网络</th><th>下一跳</th><th>类型</th></tr></thead><tbody><tr><td>192.168.1.0/24</td><td>接口0</td><td>直连</td></tr><tr><td>10.0.0.0/30</td><td>接口1</td><td>直连</td></tr><tr><td>….</td><td>……</td><td>……</td></tr><tr><td>0.0.0.0/0</td><td>10.0.0.2</td><td>静态</td></tr></tbody></table><p>这么做极大缩短了查表时间，减少了路由表占用空间。</p><h4 id="特定主机路由"><a href="#特定主机路由" class="headerlink" title="特定主机路由"></a>特定主机路由</h4><p>出于某种安全问题的考虑，同时为了使网络运维人员更方便地控制网络和测试网络，特别是在对网络的连接或路由表进行排错时，指明到某一台主机的特定主机路由是十分有用的。</p><p><img src="https://files.seeusercontent.com/2026/04/10/Ej4r/image-20260410154208590.png" alt="image-20260410154208590"></p><p>特定主机路由条目中的目的网络192.168.2.1/32，其中192.168.2.1是特<br>定主机的P地址，而网络前缀“/32“(相应地址掩码为255.255.255.255)是最长的网络前缀</p><p>路由器在查找转发表转发P数据报时，遵循“最长前缀匹配”的原则，因此特定主机路由条目的匹配优先级最高。</p><table><thead><tr><th>目的网络</th><th>下一跳</th><th>类型</th></tr></thead><tbody><tr><td>192.168.1.0/24</td><td>接口0</td><td>直连</td></tr><tr><td>10.0.0.0/30</td><td>接口1</td><td>直连</td></tr><tr><td>192.168.2.0/24</td><td>10.0.0.2</td><td>静态</td></tr><tr><td>192.168.2.1/32</td><td>10.0.0.2</td><td>静态</td></tr></tbody></table><p>在查表转发去往192.168.2.1这台特定主机的P数据报时,这两个路由条目（/24和/32）都可以匹配，遵循“最长前缀匹配”的原则，按照匹配优先级最高的特定主机路由条目进行转发。</p><p>进行静态路由配置需要认真考虑和谨慎操作，否则可能出现以下问题：</p><ul><li>路由条目配置有问题，出现路由环路</li><li>引入不存在的网络</li></ul><h3 id="因特网的路由选择协议"><a href="#因特网的路由选择协议" class="headerlink" title="因特网的路由选择协议"></a>因特网的路由选择协议</h3><p><strong>路由选择</strong>是网络设备（主要是路由器）根据路由表，为数据包选择最佳传输路径(更稳定更高效)的过程。它决定了数据从源地址到目的地址的流向，是互联网能够互联互通的核心机制。</p><p>分为两种：</p><ul><li>静态路由选择</li><li><ul><li>采用人工配置的方式给路由器添加网络路由、默认路由和特定主机路由等路由条目。<br>静态路由选择简单、开销小，但不能及时适应网络状态（流量、拓扑等）的变化，一般只在小规模网络中采用。</li></ul></li><li>动态路由选择</li><li><ul><li>路由器通过路由选择协议自动获取路由信息。<br>动态路由选择比较复杂、开销比较大，但能较好地适应网络状态的变化，适用于大规模网络，</li></ul></li></ul><p>路由选择协议必须有以下特点：</p><ul><li>自适应：因特网采用动态路由选择，能较好地适应网络状态的变化。</li><li>分布式：因特网中的各路由器通过相互间的信息交互，共同完成路由信息的获取和更新。</li><li>分层次：将整个因特网划分为许多较小的自治系统<br>(AutonomousSystem, AS)。在自治系统内部和外部采用不同类别的路由选择协议，分别进行路由选择。</li></ul><p>如：<img src="https://files.seeusercontent.com/2026/04/10/9yoH/image-20260410161610093.png" alt="image-20260410161610093"></p><p>系统间使用的是外部网关协议EGP</p><p>系统内使用的是内部网关协议IGP</p><p>注意：</p><ul><li>外部网关协议EGP和内部网关协议IGP只是路由选择协议的分类名称，而不是具体的路由选择</li><li>外部网关协议和内部网关协议名称中使用的是“网关”这个名词，是因为在因特网早期的RFC文档中，没有使用“路由器”而使用的是“网关”这一名词。</li></ul><h3 id="路由信息网关RIP协议"><a href="#路由信息网关RIP协议" class="headerlink" title="路由信息网关RIP协议"></a>路由信息网关RIP协议</h3><p>路由信息协议(Routing Information Protocol,RIP)是内部网关协议IGP中最先得到广泛使用的协议之一</p><p>RIP要求自治系统AS内的每一个路由器，都要维护从它自己到AS内其他每一个网络的距离记录。这是一组距离，称为距离向量(Distance Vector,D-V)</p><p>RIP使用<strong>跳数</strong>(Hop Count)作为度量(Metric)来衡量到达目的网络的距离。</p><ul><li>RIP将路由器到直连网络的距离定义为1。</li><li>RIP将路由器到非直连网络的距离定义为所经过的路由器数加1。</li><li>RIP允许一条路径最多只能包含15个路由器，距离等于16时相当于不可达。因此RIP只适用于小型互联网。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/11/4uwF/image-20260411193434810.png" alt="image-20260411193434810"></p><p>但有些厂商的路由器没有严格按照标准文档来实现RIP，但是不影响运行。</p><ul><li>RIP认为好的路由就是“距离短”的路由，也就是所通过路由器数量最少的路由。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/11/W4fx/image-20260411193544151.png" alt="image-20260411193544151"></p><p>这里的好路由是R1-&gt;R4-&gt;R5</p><ul><li>当到达同一目的网络有多条RIP距离相等的路由时，可以进行等价负载均衡，也就是将通信量均衡地分布到多条等价的路径上。</li><li>RIP具有第三个特点：</li><li><ul><li>仅和相邻路由器交换信息</li><li>交换路由表的相关信息</li><li>周期性交换</li></ul></li></ul><p>我们将展示RIP的基本工作原理，如下图：</p><p><img src="https://files.seeusercontent.com/2026/04/11/n9cV/image-20260411194612931.png" alt="image-20260411194612931"></p><ul><li>路由器刚开始工作时，只知道自己到直连网络的RIP距离为1。</li><li>每个路由器仅和相邻路由器周期性地交换并更新路由信息。</li><li>若干次交换和更新后，每个路由器都知道到达本自治系统内各网络的最短距离和下一跳路由器，称为收敛。</li></ul><p>那么RIP的距离向量是如何计算的呢？</p><p>路由器C将自己的路由表封装为RIP报文发送，收到的路由表将下一跳改为C，并将RIP距离+1</p><p><img src="https://files.seeusercontent.com/2026/04/11/Kad0/image-20260411195615000.png" alt="image-20260411195615000"></p><p>C告诉D：”我可以到达这些网络，以及到达这些网络的RIP最短距离分别是….<br>D作为C的相邻路由器，当然也就可以通过C来到达这些网络，只是比C到达这些网络的RIP距离大1.</p><p>但是如果网络拓扑结构发生了变化，C到N2的距离变为2，那么抵达更新周期后，D就会接收到C发来的RIP报文，更新最新距离为2+1=3.</p><p>更新理由可以是：</p><ul><li>更新此前未发现的网络，如N3</li><li>相同下一跳的路由器的RIP距离变化</li><li>到达目的网络的不同下一跳，新路由具有路径优势，(更新为达到指定网络的最短路径)</li><li>通过不同下一跳到达目的网络，RIP距离相等，可做等价负载均衡，添加新路径</li></ul><p>除了上述RIP路由条目更新规则，在RIP的距离向量算法中还包含以下一些时间参数：</p><ul><li>路由器每隔大约30秒向其所有相邻路由器发送路由更新报文。</li><li>若180秒（默认）没有收到某条路由条目的更新报文，则把该路由条目标记为无效（即把P距离设置为16，表示不可达)，若再过一段时间（如120秒），还没有收到该路由条目的更新报文，则将该路由条目从路由表中删除。</li></ul><p>但是RIP也是存在问题的，那就是坏消息传播的很慢。</p><p><img src="https://files.seeusercontent.com/2026/04/11/Oi2k/image-20260411201538637.png" alt="image-20260411201538637"></p><p>假设R1到N1的链路出现故障，R1就会将自身路由表的N1的RIP长度改成16不可达，但是如果在R1发送路由更新报文之前，R2的更新周期到了，那么R2传过来的更新报文里包含了“抵达N1的RIP长度为2”。R1就误认为能够通过R2抵达网络N1，就会将自身的N1的RIP长度设置为3，随后周期到了又会发送给R2.</p><p>R2接收后又将N1的RIP长度加一，两者反复，直到几分钟后R1和R2将RIP长度加到16才知道R1已经不可达。这就是为什么坏消息传递慢。如果没有16为上限，那就形成无限路由环路。</p><p>“坏消息传播得慢”的问题又被称为路由环路或P距离无穷计数问题。这是距离向量算法的一个固有问题。可以采取以下多种措施减少出现该问题的概率或减小该问题带来的危害：</p><ul><li>限制最大RIP距离为15(16表示不可达)。</li><li>当路由表发生变化时就立即发送路由更新报文（即“触发更新”），而不仅是周期性发送。</li><li>让路由器记录收到某个特定路由信息的接口，而不让同一路由信息再通过此接口向反方向传送（即‘水平分割”)。</li></ul><p>但是这无法彻底解决问题，因为在距离向量算法中，每个路由器都缺少到目的网络的整个路径的完整信息，他们只知道下一跳，无法判断路由是否出现了环路。</p><ul><li>RIP相关报文使用运输层的用户数据报协议UDP进行封装，使用的UDP端口号为520。</li><li>从RIP报文封装的角度看，RIP属于TCP体系结构的应用层。</li></ul><h4 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h4><p><img src="https://files.seeusercontent.com/2026/04/11/9Bym/image-20260411202559376.png" alt="image-20260411202559376"></p><h3 id="开放最短路径优先OSPF"><a href="#开放最短路径优先OSPF" class="headerlink" title="开放最短路径优先OSPF"></a>开放最短路径优先OSPF</h3><p>开放最短路径优先(Open Shortest Path First,OSPF)协议是为了克服路由信息协议RIP的缺点在1989年开发出来的。</p><ul><li>“开放”表明OSPF协议不是受某一厂商控制，而是公开发表的。</li><li>“最短路径优先”是因为使用了Dijkstra提出的最短路径算法(Shortest Path First,.SPF)。</li><li>OSPF是基于链路状态的，而不像RIP是基于距离向量的。</li><li>OSPF基于链路状态并采用最短路径算法计算路由，从算法上保证了不会产生路由环路。</li><li>OSPF不限制网络规模，更新效率高，收敛速度快。</li></ul><h4 id="链路状态"><a href="#链路状态" class="headerlink" title="链路状态"></a>链路状态</h4><p>链路状态(Link State,LS)是指本路由器都和哪些路由器相邻，以及相应链路的“代价(cos)”</p><p>“代价”用来表示费用、距离、时延和带宽等，这些都由网络管理人员来决定。</p><p><img src="https://files.seeusercontent.com/2026/04/11/H5hh/image-20260411203925319.png" alt="image-20260411203925319"></p><p>OSPF相邻路由器之间通过交互问候(Hello)分组来建立和维护邻居关系。</p><ul><li>问候(Helo)分组封装在P数据报中，发往组播地址224.0.0.5。P数据报首部中的协议号字段的取值为89，表明IP数据报的数据载荷为OSPF分组。</li></ul><table><thead><tr><th>目的IP地址 224.0.0.5</th><th>…</th><th>协议号 89</th><th>…</th><th>OSPF问候分组</th></tr></thead></table><blockquote><p>注意：OSPF分组直接使用网际层的RIP数据报进行封装，而不像RIP报文需要使用运输层用户数据报协议UDP封装。从数据包按网络体系结构逐层封装的角度看，OSPF属于网际层协议，而RIP属于应用层协议(但其核心功能是路由选择，属于网际层)。</p></blockquote><ul><li>问候(Hello)分组的发送周期为10秒。</li><li>若40秒未收到来自邻居路由器的问候(Hello)分组,则认为邻居路由器不可达。</li><li>每个路由器都会建立一张邻居表。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/11/Si9q/image-20260411210549432.png" alt="image-20260411210549432"></p><h4 id="链路状态通告LSA"><a href="#链路状态通告LSA" class="headerlink" title="链路状态通告LSA"></a>链路状态通告LSA</h4><p>使用OSPF的每个路由器都会产生链路状态通告(Link State Advertisement,LSA)，用于在OSPF区域内或区域间传递路由器和网络的链路状态信息。LSA通过泛洪的方式发送后，所有路由器收集到这些LSA，就能拼凑出一张完整的、一致的网络拓扑图（即链路状态数据库LSDB）。</p><p>LSA中包含以下两类链路状态信息：</p><ul><li>直连网络的链路状态信息</li><li>邻居路由器的链路状态信息</li></ul><p><img src="https://files.seeusercontent.com/2026/04/11/6Abx/image-20260411210840609.png" alt="image-20260411210840609"></p><h4 id="链路状态更新分组LSU"><a href="#链路状态更新分组LSU" class="headerlink" title="链路状态更新分组LSU"></a>链路状态更新分组LSU</h4><ul><li>链路状态通告LSA被封装在链路状态更新(Link State Update,LSU)分组中，采用可靠的洪泛法(Flooding)进行发送。</li><li>洪泛法的要点是路由器向自己所有的邻居路由器发送链路状态更新分组，收到该分组的各路由器又将该分组转发给自己所有的邻居路由器（但其上游路由器除外），以此类推。</li><li>可靠是指收到链路状态更新分组后要发送确认，收到重复的更新分组无需再次转发，但要发送一次确认。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/Ngc8/image-20260412122452135.png" alt="image-20260412122452135"></p><h4 id="链路状态数据库LSDB"><a href="#链路状态数据库LSDB" class="headerlink" title="链路状态数据库LSDB"></a>链路状态数据库LSDB</h4><ul><li>使用OSPF的每一个路由器都有一个链路状态数据库(Link State Database,LSDB),用于存储链路状态通告LSA。</li><li>通过各路由器洪泛发送封装有各自链路状态通告LSU的链路状态更新分组LSU,各路由器的链路状态数据库LSDB最终将达到一致。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/sH5t/image-20260412122724611.png" alt="image-20260412122724611"></p><p><strong>LSDB（链路状态数据库）存储的正是整个OSPF区域的完整、详细的网络拓扑结构。</strong>所以能够根据LSDB使用最短路径算法推导出最短路径。</p><p><img src="https://files.seeusercontent.com/2026/04/12/9cnW/image-20260412122945717.png" alt="image-20260412122945717"></p><h4 id="OSPF的五种分组类型"><a href="#OSPF的五种分组类型" class="headerlink" title="OSPF的五种分组类型"></a>OSPF的五种分组类型</h4><table><thead><tr><th>问候 (Hello)</th><th>用来发现和维护邻居路由器的可达性。</th></tr></thead><tbody><tr><td>数据库描述 (Database Description)</td><td>用来向邻居路由器给出自己的链路状态数据库中的所有链路状态项目的摘要信息。</td></tr><tr><td>链路状态请求 (Link State Request)</td><td>用来向邻居路由器请求发送某些链路状态项目的详细信息。</td></tr><tr><td>链路状态更新 (LinkState Update)</td><td>路由器使用链路状态更新分组将其链路状态信息进行洪泛发送，即用 洪泛法对整个系统更新链路状态。</td></tr><tr><td>链路状态确认 (Link State Acknowledgement)</td><td>对链路状态更新分组的确认分组。</td></tr></tbody></table><p>OSPF 的基本工作过程如下：</p><p><img src="https://files.seeusercontent.com/2026/04/12/9Ycs/image-20260412123955162.png" alt="image-20260412123955162"></p><h4 id="多点接入网络中的OSPF路由器"><a href="#多点接入网络中的OSPF路由器" class="headerlink" title="多点接入网络中的OSPF路由器"></a>多点接入网络中的OSPF路由器</h4><p>在多点接入网络中OSPF路由器邻居关系的建立：</p><p><img src="https://files.seeusercontent.com/2026/04/12/6xqO/image-20260412124154494.png" alt="image-20260412124154494"></p><p>为了减少所发送问候分组和链路状态更新分组的数量，OSPF采用以下措施：<br>选举指定路由器(Designated Router,DR)<br>和备用的指定路由器(Backup Designated<br>Router,BDR)</p><p>并规定：所有的非DR/BDR只与DR/BDR建立邻居关系，且非DR/BDR之间通过DR/BDR交换信息。</p><p><img src="https://files.seeusercontent.com/2026/04/12/3Pxu/image-20260412124320170.png" alt="image-20260412124320170"></p><p>为了使OSPF协议能够用于规模很大的网络，OSPF把一个自治系统AS再划分为若干个更小的范围，称为区域(area)。</p><p><img src="https://files.seeusercontent.com/2026/04/12/vx7O/image-20260412124528017.png" alt="image-20260412124528017"></p><p>划分区域的好处就是把利用洪泛法交换链路状态信息的范围局限于每一个区域，而不是整个自治系统AS,这样就减少了整个网络上的通信量。使得OSPF协议能用于大规模网络。</p><p>在OSPF中，路由器也有分类。</p><h3 id="边界网关协议BGP"><a href="#边界网关协议BGP" class="headerlink" title="边界网关协议BGP"></a>边界网关协议BGP</h3><ul><li>边界网关协议(Border Gateway Protocol,BGP)属于外部网关协议EGP这个类别，用于自治系统AS之间的路由选择协议。</li><li>由于在不同AS内度量路由的“代价”（距离、带宽、费用等）可能不同，因此对于AS之间的路由选择，使用统一的“代价”作为度量来寻找最佳路由是不行的。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/0Dkh/image-20260412125926759.png" alt="image-20260412125926759"></p><p>AS之间的路由选择需要考虑相关策略，如政治经济安全等。</p><p>自治系统之间的路由选择协议应当允许使用多种路由选择策略。这些策略包括政治、经济、安全等，它们都是由网络管理人员对每一个路由器进行设置的。但这些策略并不是自治系统之间的路由选择协议本身。</p><p>寻找最佳路由的代价有时候大于收益，BGP只能是力求寻找一条能够到达目的网络且比较好的路由（即不能兜圈子），而并非要寻找一条最佳路由。</p><p>在配置BGP时，每个AS的管理员要选择至少一个路由器作为该AS的“BGP发言人”。<br>一般来说，两个BGP发言人都是通过一个共享网络连接在一起的，而BGP发言人往往就是BGP边界路由器。</p><p>使用TCP连接交换路由信息的两个BGP发言人，彼此称为对方的邻站(neighbor)或对等站(peer)。<br>BGP发言人除了运行BGP协议外，还必须运行自己所在AS所使用的内部网关协议IGP,例如RIP可或OSPF。</p><p>BGP发言人交换网络可达性的信息，也就是要到达某个网络所要经过的一系列自治系统。</p><p>当BGP发言人相互交换了网络可达性的信息后，各BGP发言人就根据所采用的策略，从收到的路由信息中找出到达各自治系统的较好的路由，也就是构造出树形结构且不存在环路的自治系统连通图。</p><p><img src="https://files.seeusercontent.com/2026/04/12/lhI3/image-20260412131041173.png" alt="image-20260412131041173"></p><p><img src="https://files.seeusercontent.com/2026/04/12/z2eF/image-20260412131409972.png" alt="image-20260412131409972"></p><h4 id="BGP-4中的四种报文"><a href="#BGP-4中的四种报文" class="headerlink" title="BGP-4中的四种报文"></a>BGP-4中的四种报文</h4><table><thead><tr><th>打开 OPEN</th><th>用来与相邻的另一个BGP发言人建立关系，使通信初始化。</th></tr></thead><tbody><tr><td>保活 KEEPALIVE</td><td>用来周期性地证实邻站的连通性。</td></tr><tr><td>更新 UPDATE</td><td>用来通告某一条路由的信息，以及列出要撤销的多条路由。</td></tr><tr><td>通知 NOTIFICATION</td><td>用来发送检测到的差错。</td></tr></tbody></table><h3 id="路由器的基本工作原理"><a href="#路由器的基本工作原理" class="headerlink" title="路由器的基本工作原理"></a>路由器的基本工作原理</h3><p>路由器是一种具有多个输入端口和输出端口的专用计算机，其任务是转发分组。</p><p><img src="https://files.seeusercontent.com/2026/04/12/g3Vq/image-20260412131806813.png" alt="image-20260412131806813"></p><p>交换结构的速率对于路由器的性能是至关重要的。因此，人们对交换结构进行了大量研究，以提高路由器的转发速率。</p><p>实现交换结构的三种基本方式是：通过存储器、通过总线以及通过互连网络。这三种交换结构可实现的路由器转发速率依次提高。</p><h3 id="网际控制报文ICMP"><a href="#网际控制报文ICMP" class="headerlink" title="网际控制报文ICMP"></a>网际控制报文ICMP</h3><ul><li>为了更有效地转发P数据报以及提高P数据报交付成功的机会，TCP体系结构的网际层使用了网际控制报文协议(Internet Control Message Protocol,ICMP)[RFC792]。</li><li>主机或路由器使用ICMP来发送差错报告报文和询问报文。</li><li>1CMP报文被封装在P数据报中发送。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/5Vwq/image-20260412140536691.png" alt="image-20260412140536691"></p><p>分为两大类：</p><ul><li>差错报告报文：用来向主机或路由器报告差错情况</li><li>询问报文：用来向主机或路由器询问情况。</li></ul><h4 id="差错报告报文"><a href="#差错报告报文" class="headerlink" title="差错报告报文"></a>差错报告报文</h4><p>负责五种差错报告：</p><ul><li>终点不可达：</li><li><ul><li>当路由器或主机不能交付P数据报时，就向源点发送终点可达报文。具体可再根据ICMP的代码字段细分为目的网络不可达、目的主机不可达、目的协议不可达、目的端口不可达、目的网络未知、目的主机未知等13种。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/c6dP/image-20260412142319526.png" alt="image-20260412142319526"></p><ul><li><p>源点抑制</p></li><li><ul><li>当路由器或主机由于拥塞而丢弃P数据报时，就向发送该P数据报的源点发送源点抑制报文，使源点知道应当把IP数据报的发送速率放慢</li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/9lPm/image-20260412142553616.png" alt="image-20260412142553616"></p></li><li><p>时间超过(超时)</p></li><li><ul><li>当路由器收到一个目的IP地址不是自己的IP数据报时，会将其首部中生存时间TTL字段的值减1。若结果不为0，则路由器将该数据报转发出去；若结果为0，路由器不但要丢弃该数据报，还要向发送该P数据报的源点发送时间超过（超时）报文。</li><li>另外，当终点在预先规定的时间内未能收到一个数据报的全部数据报分片时，就把已收到的数据报片都丢弃，也会向源点发送时间超过（超时）报文。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/2Dlo/image-20260412142758362.png" alt="image-20260412142758362"></p><ul><li>参数问题</li><li><ul><li>当路由器或目的主机收到IP数据报后发现其首部中有不正确的字段值时，就丢弃该数据报，并向发送该数据报的源点发送参数问题报文。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/s6vW/image-20260412143218289.png" alt="image-20260412143218289"></p><ul><li>改变路由</li><li><ul><li>路由器把改变路由报文发送给主机，让主机知道下次应将P数据报发送给另外的路由器，这样可以通过更好的路由到达目的主机。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/12/b1Zv/image-20260412143332787.png" alt="image-20260412143332787"></p><h4 id="以下情况不发送ICMP差错报告报文"><a href="#以下情况不发送ICMP差错报告报文" class="headerlink" title="以下情况不发送ICMP差错报告报文:"></a>以下情况不发送ICMP差错报告报文:</h4><ul><li>对ICMP差错报告报文不再发送ICMP差错报告报文。</li><li>对第一个分片的P数据报片的所有后续数据报片都不发送CMP差错报告报文。</li><li>对具有多播地址的P数据报都不发送CMP差错报告报文。</li><li>对具有特殊地址（例如127.0.0.0或0.0.0.0）的IP数据报不发送CMP差错报告报文。</li></ul><h4 id="询问报文"><a href="#询问报文" class="headerlink" title="询问报文"></a>询问报文</h4><p>两种：</p><ul><li>会送请求和回答</li><li><ul><li>由主机或路由器向一个特定的目的主机或路由器发出。<br>收到此报文的主机或路由器必须给发送该报文的源主机或路由器发送<br>ICMP回送回答报文。<br>这种询问报文用来测试目的站是否可达以及了解其有关状态。</li></ul></li><li>时间戳请求和回答</li><li><ul><li>用来请求某个主机或路由器回答当前的日期和时间。<br>在CMP时间戳回答报文中有一个32比特的字段，其中写入的整数代表<br>从1900年1月1日起到当前时刻一共有多少秒。<br>这种询问报文用来进行时钟同步和测量时间。</li></ul></li></ul><h4 id="ICMP应用"><a href="#ICMP应用" class="headerlink" title="ICMP应用"></a>ICMP应用</h4><ul><li>分组网间探测</li></ul><p>分组网间探测PING用来测试主机或路由器之间的连通性。<br>PING是TCP/IP体系结构的应用层直接使用网际层ICMP的一个例子，它并不使用运输层的TCP或UDP。<br>PING应用所使用的ICMP报文类型为回送请求和回答。</p><p><img src="https://files.seeusercontent.com/2026/04/12/7wVp/image-20260412150250448.png" alt="image-20260412150250448"></p><p>然而，某些主机回或服务器为了防止恶意攻击，并不会理睬外界发来的ICMP回送请求报文。</p><ul><li>跟踪路由</li></ul><p>跟踪路由应用traceroute,用于探测IP数据报从源主机到达目的主机要经过哪些路由器。</p><p>在不同操作系统中，traceroute应用的命令和实现机制有所不同：</p><ul><li>在UNIX版本中，具体命令为“traceroute’”,其在运输层使用UDP协议，在网络层使用ICMP报文类型只<br>有差错报告报文。</li><li>在Windows版本中，具体命令为”tracert”,其应用层直接使用网际层的ICMP协议，所使用的ICMP报<br>文类型有回送请求和回答报文以及差错报告报文。<br><img src="https://files.seeusercontent.com/2026/04/12/Zi3x/image-20260412151109583.png" alt="image-20260412151109583"></li></ul><p>实现过程如下：</p><p><strong>发送探测包</strong>： <code>tracert</code>首先向目标主机（如 <code>blog.willmo.top</code>）发送一个 <strong>ICMP 回显请求（Echo Request）</strong> 数据包，并将该数据包的 <strong>TTL（生存时间）</strong> 值设置为 <strong>1</strong>。</p><p><strong>触发ICMP超时</strong>： 第一个路由器（跃点1）收到数据包后，会将 TTL 减 1。TTL 变为 0，于是该路由器丢弃数据包，并向您的电脑发送一个 <strong>ICMP 超时（Time Exceeded）</strong> 消息。<code>tracert</code>由此获得了第一个路由器的地址和往返时间。</p><p><strong>递增TTL，重复过程</strong>： 接着，<code>tracert</code>发送一个 TTL=2 的数据包。它经过第一个路由器（TTL减为1），到达第二个路由器（TTL再减1变为0）。第二个路由器同样丢弃包并返回 ICMP 超时消息。如此循环，每次 TTL 加 1。</p><p><strong>到达目标</strong>： 当 TTL 足够大，数据包最终到达目标主机。目标主机则会返回一个 <strong>ICMP 回显应答（Echo Reply）</strong> 消息（对于 Windows <code>tracert</code>的 ICMP 探测）。收到此应答后，追踪完成。</p><p><img src="https://files.seeusercontent.com/2026/04/12/B1fj/image-20260412151024696.png" alt="image-20260412151024696"></p><h3 id="虚拟专用网VPN"><a href="#虚拟专用网VPN" class="headerlink" title="虚拟专用网VPN"></a>虚拟专用网VPN</h3><blockquote><p>我知道你看到这三个字母会很兴奋，但是请冷静。</p></blockquote><p>虚拟专用网(Virtual Private Network,VPN):利用公用的因特网作为本机构各专用网之间的通信载体，这样形成的网络又称为虚拟专用网。</p><p><img src="https://files.seeusercontent.com/2026/04/12/5dFu/image-20260412152212524.png" alt="image-20260412152212524"></p><p>位于两个不同地方的主机，将其用因特网链接起来，但是出于安全不能将其暴露在公网上，所以，给专用网内各主机配置的P地址应使各主机在专用网内可以相互通信，而不能直接与公用的因特网通信。</p><p>给专用网内各主机配置的P地址应该是该专用网所在机构可以自行分配的P地址，这类P地址仅在机构内<br>部有效，称为专用地址(Private Address),不需要向因特网的管理机构申请。</p><p>[RFC1918]规定了以下三个CDR地址块中的地址作为专用地址:<br>10.0.0.0<del>10.255.255.255<br>(CDR地址块10/8)<br>172.16.0.0</del>172.31.255.255<br>(ClDR地址块172.16/12)<br>192.168.0.0~192.168.255.255<br>(CDR地址块192.168/16)</p><p>很显然，全世界可能有很多不同机构的专用网具有相同的专用P地址，但这并不会引起麻烦，因为这些专用地址仅在机构内部使用。</p><p>因特网中的所有路由器，对目的地址是专用地址的P数据报一律不进行转发，这需要由因特网服务提供者1SP对其拥有的因特网路由器进行设置来实现。</p><p>两个网络仍需要两个公网IP来进行公网通信，此时配备公网IP的路由器需要进行VPN设置。</p><p><img src="https://files.seeusercontent.com/2026/04/12/jhY4/image-20260412153633535.png" alt="image-20260412153633535"></p><p>主机H1封装 IP数据报，内含H1和H2的专用 地址，随后交于路由器R1，R1将数据报加密，随后添加目的网络路由器的IP地址。</p><p>R2接收后解密，再将IP数据报发给H2。</p><p>虽然两个专用网内的主机间发送的数据报是通过公用的因特网传送的，但从效果上就好像是本机构的专用网上传送一样，这也是虚拟专用网中“虚拟”的含义。</p><p><img src="https://files.seeusercontent.com/2026/04/12/s7jL/image-20260412153714580.png"></p><p>P数据报在因特网中可能要经过多个网络和路由器，但从逻辑上看，路由器R1和R2之间好像是条直通的点对点链路，因此也被称为P隧道技术。</p><p>本例所示的是同一机构内不同部门的内部网络所构成的VPN,又称为内联网VPN。<br>有时，一个机构的虚拟专用网VP需要某些外部机构（通常是合作伙伴）参加进来，这样的VPN就称为外联网VPN。</p><p>在外地工作的员工需要访问公司内部的专用网时，只要在任何地点接入因特网，运行驻留在员工PC中的 VPN软件，在员工的PC和公司的主机之间建立VPN隧道，就可以访问专用网中的资源，这种虚拟专用网又称为远程接入VPN。</p><h3 id="网络地址转换"><a href="#网络地址转换" class="headerlink" title="网络地址转换"></a>网络地址转换</h3><p>网络地址转换(Network Address Translation,NAT)技术于1994年被提出，用来缓解IPv4地址空间即将耗尽<br>的问题。</p><ul><li>NAT能使大量使用内部专用地址的专用网络用户共享少量外部全球地址来访问因特网上的主机和资源。</li><li>这种方法需要在专用网络连接到因特网的路由器上安装NAT软件。装有NAT软件的路由器称为NAT路由器，它至少要有一个有效的公网IP。这样，所有使用内部专用地址的主机在和外部因特网通信时，都要在NAT路由器上将其内部专用地址转换成Pc。</li></ul><h4 id="最基本的NAT方法"><a href="#最基本的NAT方法" class="headerlink" title="最基本的NAT方法"></a>最基本的NAT方法</h4><p><img src="https://files.seeusercontent.com/2026/04/12/k8cO/image-20260412154537827.png" alt="image-20260412154537827"></p><p><img src="https://files.seeusercontent.com/2026/04/12/3Veu/image-20260412154546878.png" alt="image-20260412154546878"></p><p>这样的方法的缺点是：</p><p>基础 NAT（不带端口号转换的那种）的缺点是：<strong>外网 IP 地址利用率极低</strong>。如果有 100 个内网用户想同时上网，你就必须拥有 100 个公网 IP。</p><p>这根本没有解决 IPv4 枯竭的问题，所以现在几乎见不到纯 NAT，全是 <strong>NAPT</strong></p><hr><p>由于目前绝大多数基于TCP协议栈的网络应用，都使用运输层的传输控制协议TCP或用户数据报协议UDP,，为了更加有效地利用NAT路由器中的全球P地址，现在常将NAT转换和运输层端口号结合使用。</p><ul><li>这样就可以使内部专用网中使用专用地址的大量主机，共用NAT路由器上的1个全球P地址，因而可以同时与因特网中的不同主机进行通信。</li></ul><p>将NAT和运输层端口号结合使用，称为网络地址与端口号转换(Network Address and Port Translation, NAPT)</p><ul><li>现在很多家用路由器将家中各种智能设备（手机、平板、笔记本电脑、台式电脑、物联网设备等）接入因特网，这种路由器实际上就是一个NAPT路由器，但往往并不运行路由选择协议。</li></ul><h4 id="NAPT协议路由器基本工作原理"><a href="#NAPT协议路由器基本工作原理" class="headerlink" title="NAPT协议路由器基本工作原理"></a>NAPT协议路由器基本工作原理</h4><p><img src="https://files.seeusercontent.com/2026/04/12/Zww3/image-20260412155545811.png" alt="image-20260412155545811"></p><p>与主机A选择的源端口号相同，这纯属巧合(端口号仅在本主机中才有意义)。特意这样举例，就是为了能更好地说明 NAPT路由器还会对源端口号重新动态分配。</p><p>尽管NAT(和NAPT)的出现在很大程度上缓解了IPV4地址资源紧张的局面，但NAT(和NAPT)对网络应用并不完全透明，会对某些网络应用产生影响。</p><p>NAT(和NAPT)的一个重要特点就是通信必须由专用网内部发起，因此拥有内部专用地址的主机不能直接充当因特网中的服务器。</p><p>对于目前P2P这类需要外网主机主动与内网主机进行通信的网络应用，在通过NAT时会遇到问题，需要网络应用自身使用一些特殊的NAT穿透技术来解决。</p><h3 id="IP多播技术"><a href="#IP多播技术" class="headerlink" title="IP多播技术"></a>IP多播技术</h3><p>多播(Multicast,也称为组播)是一种实现“一对多”通信的技术，与传统单播“一对一”通信相l比，多播可以极大地节省网络资源。</p><p>在因特网上进行的多播，称为IP多播。</p><p><img src="https://files.seeusercontent.com/2026/04/12/Yl4j/image-20260412203322345.png" alt="image-20260412203322345"></p><p>使用多播技术，视频服务器只需要发送一个视频文件即可，该视频文件会在路由器被复制为多份以保证送达每个主机。</p><h4 id="IP多播地址和多播组"><a href="#IP多播地址和多播组" class="headerlink" title="IP多播地址和多播组"></a>IP多播地址和多播组</h4><p>在IPv4中，D类地址被作为多播地址。</p><p>多播地址只能用作目的地址，而不能用作源地址。<br>用每一个D类地址来标识一个多播组，使用同一个P多播地址接收P多播数据报的所有主机就构成了一个多播组。</p><ul><li>每个多播组的成员是可以随时变动的，一台主机可以随时加入或离开多播组。</li><li>多播组成员的数量和所在的地理位置也不受限制，一台主机可以属于几个多播组。</li></ul><p>非多播组成员也可以向多播组发送P多播数据报。<br>与IP数据报相同，IP多播数据报也是“尽最大努力交付”，不保证一定能够交付给多播组内的所有成员。</p><p><img src="https://files.seeusercontent.com/2026/04/12/Frc8/image-20260412203912555.png" alt="image-20260412203912555"></p><p>IPV4多播地址又可分为预留的多播地址（永久多播地址）、全球范围可用的多播地址以及本地管理的多播地址[RFC3330]。</p><p><img src="https://files.seeusercontent.com/2026/04/13/o4Mz/image-20260413120828946.png" alt="image-20260413120828946"></p><p>IP多播可以分为以下两种：</p><ul><li>只在本局域网上进行的硬件多播。</li><li>在因特网上进行的多播。</li></ul><p>目前大部分主机都是通过局域网接入因特网的。因此，在因特网上进行多播的最后阶段，还是要把P多播数据报在局域网上用硬件多播交付给多播组的所有成员。</p><h3 id="在局域网中进行硬件多播"><a href="#在局域网中进行硬件多播" class="headerlink" title="在局域网中进行硬件多播"></a>在局域网中进行硬件多播</h3><p>由于MAC地址（也称为硬件地址）有多播MAC地址这种类型，因此只要把IPv4多播地址映射成多播MAC地址，即可将IP多播数据报封装在局域网的MAC帧中，而MAC帧首部中的目的MAC地址字段的值，就设置为由IPv4多播地址映射成的多播MAC地址。这样，可以很方便地利用硬件多播来实现局域网内的IP多播。</p><p><img src="https://files.seeusercontent.com/2026/04/13/d2Xq/image-20260413122023943.png" alt="image-20260413122023943"></p><p>当给某个多播组的成员主机配置其所属多播组的P多播地址时，系统就会根据映射规则从该IP多播地址生成相应的局域网多播MAC地址。</p><p>所以，因特网号码指派管理局IANA,将自己从IEEE注册管理机构申请到的以太网MAC地址块中从01-00-5E-00-00- 00到01-00-5E-7F-FF-FF的多播MAC地址，用于映射lIPv4多播地址。</p><p>这些多播MAC地址的左起前25个比特都是相同的，剩余23个比特可以任意变化，因此共有2^23个。</p><p><img src="https://files.seeusercontent.com/2026/04/13/Gnk3/image-20260413122310336.png" alt="image-20260413122310336"></p><p>那么地址映射是如何实现的呢？</p><p>来看看两个多播地址的结构：</p><p><img src="https://files.seeusercontent.com/2026/04/13/5uZk/image-20260413122424418.png" alt="image-20260413122424418"></p><p>可以看到有5个比特处于空闲位，这会导致映射关系不唯一，比如：</p><p><img src="https://files.seeusercontent.com/2026/04/13/t2sE/image-20260413122537748.png" alt="image-20260413122537748"></p><p>可见，如果多个IP地址仅在那五个不可映射的比特中出现不同，那么这些IP地址会映射出同一个多播MAC地址。</p><p>因此收到IP多播数据报的主机还要在网际层利用软件进行过滤,把不是主机要接收的P多播数据报丢弃。</p><p><img src="https://files.seeusercontent.com/2026/04/13/q5Xs/image-20260413123445865.png" alt="image-20260413123445865"></p><p>如图，主机接收到多播MAC帧后，将目的MAC地址与自身MAC地址比对，不匹配就不接收。发现MAC地址在前两个主机匹配之后接收转交到网际层处理。随后网际层将MAC多播帧中的IP地址与自身作比较，匹配的接收，不匹配的丢弃。</p><h3 id="在因特网上进行IP多播需要的两种协议"><a href="#在因特网上进行IP多播需要的两种协议" class="headerlink" title="在因特网上进行IP多播需要的两种协议"></a>在因特网上进行IP多播需要的两种协议</h3><p>要在因特网上进行P多播，就必须要考虑P多播数据报经过多个多播路由器进行转发的问题。</p><ul><li>多播路由器必须根据IP多播数据报首部中的IP多播地址，将其转发到有该多播组成员的局域网。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/13/zq7U/image-20260413124021222.png" alt="image-20260413124021222">如图，如果路由器收到一多播数据报，如何得知自己的局域网中有没有目的多播组的成员？</p><p>这需要用到<strong>IGMP协议。</strong></p><p>网际组管理协议(Internet Group Management Protocol,IGMP)是TCP/IP体系结构网际层中的协议，其作用是让连接在本地局域网上的多播路由器知道本局域网上是否有主机（实际上是主机中的某个进程）加入或退出了某个多播组。</p><p>IGMP仅在本网络有效，使用IGMP并不能知道多播组所包含的成员数量，也不能知道多播组的成员都分布在哪些网络中。</p><p>但是仅使用IGMP并不能在因特网上进行P多播。连接在局域网上的多播路由器还必须和因特网上的其他多播路由器协同工作，以便把P多播数据报用最小的代价传送给所有的多播组成员，这就需要使用<strong>多播路由选择协议。</strong></p><p>多播路由选择协议的主要任务是：在多播路由器之间为每个多播组建立一个多播转发树。</p><ul><li>多播转发树连接多播源和所有拥有该多播组成员的路由器。</li><li>P多播数据报只要沿着多播转发树进行洪泛，就能被传送到所有拥有该多播组成员的多播路由器。</li><li>之后，在多播路由器所直连的局域网内，多播路由器通过硬件多播，将P多播数据报发送给该多播组的所有成员。</li></ul><p>那么总结一下，将每个网络成为节点，IGMP负责提供节点内出于多播组的主机的信息，多播路由选择协议负责连接节点构建多播转发树。随后IP数据报在转发树中进行转发即可实现多播。</p><p><img src="https://files.seeusercontent.com/2026/04/13/kJp8/image-20260413124836312.png" alt="image-20260413124836312"></p><p>针对不同的多播组需要维护不同的多播转发树，而目必须动态地适应多播组成员的变化，但此时网络拓扑并不一定发生变化，因此多播路由选择协议要比单播路由选择协议（例如RIP、OSPF等）复杂得多。</p><ul><li>即使某个主机不是任何多播组的成员，它也可以向任何多播组发送多播数据报。</li><li>为了覆盖多播组的所有成员，多播转发树可能要经过一些没有多播组成员的路由器（例如上图中的R2）</li></ul><h3 id="网际管理层IGMP"><a href="#网际管理层IGMP" class="headerlink" title="网际管理层IGMP"></a>网际管理层IGMP</h3><p>网际组管理协议IGMP目前的最新版本是2002年10月公布的IGMPv3[RFC3376]。</p><p>有三种报文类型：</p><ul><li>成员报告报文</li><li>成员查询报文</li><li>离开组报文</li></ul><p>IGMP报文被封装在IP数据报中传送</p><p><img src="https://files.seeusercontent.com/2026/04/13/Me0y/image-20260413125533532.png" alt="image-20260413125533532"></p><h4 id="加入多播组"><a href="#加入多播组" class="headerlink" title="加入多播组"></a>加入多播组</h4><p><img src="https://files.seeusercontent.com/2026/04/13/Hmo9/image-20260413130436635.png" alt="image-20260413130436635"></p><p>主机A主机B希望加入多播组226.0.9.26。</p><p>主机B发送IGMP成员报告报文，随后局域网内的所有主机都会受到该多播帧，但是D不属于任何多播组，检查MAC地址不符，所以会将其丢弃。C发现自己的MAC地址相符但是经过网际层判断后IP不符，也会将其丢弃。主机A发现其MAC的地址相符接收后，检查IP地址发现与自己想加入的多播地址相同，于是接收该数据报，交付IGMP协议解析，得知该数据报来自于同一多播组的主机。</p><p>此时A会取消发送IGMP成员数据报。</p><p>为什么要取消？因为路由器只在乎自己的网络中有哪些多播组存在而不关心成员的IP地址，路由器只需要在接收到多播数据报并发现自己的网络中有该多播组的主机是将多播数据报根据“多播地址”和“物理端口”的键值对来转发。</p><p>随后，路由器R1接收并解析IGMP报告 报文，将新的多播地址添加到自己的多播组列表中，证明“我管辖的网络中有主机属于这些多播组”。</p><h4 id="查询多播组"><a href="#查询多播组" class="headerlink" title="查询多播组"></a>查询多播组</h4><p><img src="https://files.seeusercontent.com/2026/04/13/e1Wv/image-20260413132245097.png" alt="image-20260413132245097"></p><p>每隔一段时间，路由器R1就会发送IGMP成员查询报文来查询多播组情况。发送的报文中带有一个特殊的多播地址<code>224.0.0.1</code>，这会让 所有参与多播的主机都接收并处理该报文。随后每个接收并处理该报文的主机将在随机延迟后向路由器返回IGMP成员报告报文。</p><p>为了避免重复响应，每个多播组只需要一个主机响应即可。</p><p>主机B延时抵达，将IGMP成员报告报文广播，主机D和主机C分别在MAC层和网际层丢弃，而主机A则接收并 解析，取消发送自身的IGMP报告报文。随后R1接收到报告报文，得知多播组<code>226.0.9.26</code>在自己的网络中仍有成员，就更新其计时。</p><p><img src="https://files.seeusercontent.com/2026/04/13/K0qg/image-20260413133515955.png" alt="image-20260413133515955"></p><p>随后主机C响应，流程与B类似。</p><p>但是如果主机C此时已经不是多播组的成员了，那么主机C就会丢弃来自于R1的MAC帧。路由器R1长期没有接收到来自<code>226.128.9.26</code>的响应报文就会认为“我管理的网络中没有多播组<code>226.128.9.26</code>的成员”，就将其从多播组列表中删除。</p><p>补充一点：</p><ul><li>同一网络中的多播路由器可能不止一个，但没有必要每个多播路由器都周期性地发送GMP成员查询报文。</li><li>只要在这些多播路由器中选择一个作为查询路由器，由查询路由器发送GMP成员查询报文，而其他的多播路由<br>器仅被动接收响应并更新自己的多播组列表即可。</li><li>选择查询路由器的方法：<ul><li>每个多播路由器若监听到源P地址比自己的P地址小的GMP成员查询报文则退出选举。</li><li>最后，网络中只有P地址最小的多播路由器成为查询路由器。</li></ul></li></ul><h4 id="退出多播组"><a href="#退出多播组" class="headerlink" title="退出多播组"></a>退出多播组</h4><p>IGMPv2在GMPv1的基础上增加了一个可选项：当主机要退出某个多播组时，可主动发送一个离开组报文而不必等待多播路由器的查询。这样可使多播路由器能够更快地发现某个组有成员离开。</p><p><img src="https://files.seeusercontent.com/2026/04/13/R0xk/image-20260413134358919.png" alt="image-20260413134358919"></p><p>如图，主机C准备离开了多播组，向局域网中广播IGMP离开报文，其他主机都会将其丢弃。但是R1接收后会立即发送查询报文，查看是否有其他组成员，如果有响应就不删除，如果没有就将其删除。</p><h3 id="多播路由选择协议"><a href="#多播路由选择协议" class="headerlink" title="多播路由选择协议"></a>多播路由选择协议</h3><p>多播路由选择协议的主要任务是：在多播路由器之间为每个多播组建立一个多播转发树。而多播转发树链接多播源和拥有该多播组成员的路由器。</p><p>有两种办法构造多播转发树：</p><ul><li>基于源树多播路由选择</li><li>组共享树多播路由选择</li></ul><h4 id="基于源树多播路由选择"><a href="#基于源树多播路由选择" class="headerlink" title="基于源树多播路由选择"></a>基于源树多播路由选择</h4><p>基于源树的多播路由选择的最典型算法是反向路径多播(Reverse Path Multicasting,RIP)算法。</p><p>RIPM算法包含以下两个步骤：</p><ul><li>利用反向路径广播(Reverse Path Broadcasting,RIPB)算法建立一个广播转发树。</li><li>利用剪枝(Pruning)算法，剪除广播转发树中的下游非成员路由器，获得一个多播转发树。</li></ul><p>要建立广播转发树，可以使用洪泛法。</p><p><img src="https://files.seeusercontent.com/2026/04/13/h5lZ/image-20260413135646021.png" alt="image-20260413135646021"></p><p>但是如果遇到环路，就会出现堵塞。</p><p>但是使用RIPB算法就不需要考虑环路。</p><p>RIPB算法的要点是：<strong>每一台路由器在收到一个广播分组时，先检查该广播分组是否是从源点经最短路径传送来的</strong>。</p><ul><li>若是，本路由器就从自己除刚才接收该广播分组的接口的所有其他接口转发该广播分组。</li><li>否则，丢弃该广播分组。</li><li>如果本路由器有好几个邻居路由器都处在到源点的最短路径上，也就是存在好几条同样长度的最短路径，那么只能选取一条最短路径。选取的规则是这几条最短路径中的邻居路由器的IP地址最小的那条最短路径。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/13/Wy6i/image-20260413140446653.png" alt="image-20260413140446653"></p><p>但是这只是实现了广播而非多播，这就需要剪枝算法。</p><p>剪枝算法是为了减掉那些“内部没有多播组成员”的路由器。</p><p>但是如果减除了这种路由器通过IGMP发现了新的多播组成员，就需要嫁接算法重新接到树中。</p><h4 id="组共享树多播路由选择"><a href="#组共享树多播路由选择" class="headerlink" title="组共享树多播路由选择"></a>组共享树多播路由选择</h4><p>组共享树多播路由选择采用基于核心的分布式生成树算法来建立共享树。</p><ul><li>该方法在每个多播组中指定一个核心(co)路由器，以该路由器为根，建立一棵连接多播组的所有成员路由器的生成树，作为多播转发树。</li></ul><p>每个多播组中除了核心路由器，其他所有成员路由器都会向自己多播组中的核心路由器单播加入报文。</p><ul><li>加入报文通过单播朝着核心路由器转发，直到它到达已经属于该多播生成树的某个节点或者直接到达该核心路由器。</li><li>加入报文所经过的路径，就确定了一条从单播该报文的边缘节点到核心路由器之间的分支，而这个新分支就被嫁接到现有的多播转发树上。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/13/Hi0x/image-20260413142002787.png" alt="image-20260413142002787"></p><p>如图，定义R5为核心路由器，那么具有多播组成员的路由器就会试图和R5组成生成树。</p><p>若源主机需要发送数据，将该多播分组封装到目的地址为核心路由器R5的单播分组中，将该单播分组发送给R5。R5将被封装在单播分组中的多播分组解封出来，在多播转发树上洪泛转发。</p><h4 id="在因特网上的多播路由选择协议"><a href="#在因特网上的多播路由选择协议" class="headerlink" title="在因特网上的多播路由选择协议"></a>在因特网上的多播路由选择协议</h4><p>目前还没有在整个因特网范围使用的多播路由选择协议。下面是一些建议使用的多播路由选择协议：</p><ul><li>距离向量多播路由选择协议(Distance Vector Multicast Routing Protocol,.DVMRIP)[RFC1075]。</li><li>开放最短路径优先的多播扩展(Multicast Extensions to OSPF,MOSPF)RFC1585]。</li><li>协议无关多播-稀疏方式(Protocol Independent Multicast–Sparse Mode,PIM-SW)[RFC2362]。</li><li>协议无关多播-密集方式(Protocol Independent Multicast-.Dense Mode,PIM-DM)[RFC3973]。</li><li>基于核心的转发树(Core Based Tree,CBT)[RFC2189,RFC2201].</li></ul><p>尽管因特网工程任务组IETF努力推动着因特网上的全球多播主干网(Multicast Backbone On the Internet, MBONE)的建设，但至今在因特网上的P多播还没有得到大规模的应用。<br>主要原因是：</p><ul><li>改变一个已成功运行且广泛部署的网络层协议是一件极其困难的事情。</li><li>目前IP多播主要应用在一些局部的园区网络、专用网络或者虚拟专用网中。</li></ul><p>另外，P2P技术的广泛应用推动了应用层多播技术的发展，许多视频流公司和内容分发公司通过构建自己的应用层多播覆盖网络来分发它们的内容。但上述多播路由选择协议的算法思想在应用层多播中依然适用。</p><h3 id="移动IP技术"><a href="#移动IP技术" class="headerlink" title="移动IP技术"></a>移动IP技术</h3><p>来看三个应用场景：</p><p><img src="https://files.seeusercontent.com/2026/04/13/B3od/image-20260413142928099.png" alt="image-20260413142928099"></p><p>为了解决应用场景3，因特网工程任务组开发了移动IP技术，使得移动主机在各网络中漫游时，保持IP不变。</p><p>我们需要了解以下概念:</p><ul><li>每个移动主机都有一个默认连接的网络或初始申请接入的网络，称为归属网络(Home Network)</li><li>移动主机在归属网络中的P地址在其整个移动通信过程中是始终不变的，因此称为永久地址(Permanent Address)或归属地址(Home Address)</li><li>在归属网络中，代表移动主机执行移动管理功能的实体称为归属代理(Home<br>Agent)。归属代理通常就是连接在归属网络上的路由器，然而它作为代理的特定功能则是在网络层完成的。</li><li>移动主机当前漫游所在的网络称为外地网络(Foreign Network)或被访网络(Visited Network)</li><li>在外地网络中，帮助移动主机执行移动管理功能的实体称为外地代理(Foreign Agent)</li><li>外地代理通常就是连接在外地网络上的路由器。外地代理会为移动主机提供一个临时使用的属于外地网络的转交地址(Care-of Address)</li></ul><p>搁置，移动IP技术未看完。</p><hr><h3 id="IPv6"><a href="#IPv6" class="headerlink" title="IPv6"></a>IPv6</h3><p>诞生背景就是IPv4不够用，而NAT等技术只是延长寿命而不能从根本上解决问题。</p><p>解决IPV4地址耗尽的根本措施就是采用具有<br>更大地址空间(IP地址的长度为128比特)的新版本P,即IPv6。</p><p><strong>IPv6 是已经成熟并大规模部署的国际标准</strong></p><p>IPv6有很多好处：</p><p><img src="https://files.seeusercontent.com/2026/04/13/6Xmd/image-20260413144518600.png" alt="image-20260413144518600"></p><h3 id="IPv6的数据报的基本首部"><a href="#IPv6的数据报的基本首部" class="headerlink" title="IPv6的数据报的基本首部"></a>IPv6的数据报的基本首部</h3><p><img src="https://files.seeusercontent.com/2026/04/13/g6gX/image-20260413144942246.png" alt="image-20260413144942246"></p><p>所有的扩展首部并不属于Pv6数据报的首部，它们与其后面的数据部分合起来构成有效载荷(payload,也称为净负荷)。</p><p>Pv6将IPv4数据报首部中不必要的功能取消了，这使得P6数据报基本首部中的字段数量减少到只有8个。</p><ul><li>但由于Pv6地址的长度扩展到了128比特，因此使得Pv6数据报基本首部的长度反而增大到了40字节，</li><li>比IPv4数据报首部固定部分的长度(20字节)增大了20字节。</li></ul><hr><p>ipv6后面再倒过来看，累了。</p><h3 id="软件定义网络"><a href="#软件定义网络" class="headerlink" title="软件定义网络"></a>软件定义网络</h3><p>软件定义网络(Software Defined Network,SDN)的概念最早由斯坦福大学的Nick McKeown教授于2009年提出。</p><p>SDN是当前网络领域最热门和最具发展前途的技术之一，成为近年来的研究热点。</p><p>请回忆普通路由器的功能：</p><ul><li>为主机间的通信提供转发服务</li><li>路由选择</li></ul><p>路由器的信息种类：</p><ul><li>主机之间的分组</li><li>路由信息</li></ul><p>此前，路由器被分为控制层面和数据层面：</p><p><img src="https://files.seeusercontent.com/2026/04/13/pXy7/image-20260413153511938.png" alt="image-20260413153511938"></p><p>在SDN体系结构中，路由器中的路由软件都不存在了。因此，路由器之间不再交换路由信息。</p><ul><li>在控制层面中，有一个在逻辑上集中的远程控制器。</li><li>逻辑上集中的远程控制器在物理上可由不同地点的多个服务器组成。</li><li>远程控制器掌握各主机和整个网络的状态。</li><li>远程控制器能够为每一个分组计算出最佳的路由。</li><li>远程控制器为每一个路由器生成其正确的转发表。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/13/Oev0/image-20260413153616774.png" alt="image-20260413153616774"></p><p>那么，路由器就能专注于查表转发这一个业务。</p><p>SD这种新型网络体系结构的核心思想：把网络的控制层面和数据层面分离，而让控制层面利用软件来控制数据层面中的许多设备。</p><ul><li>OpenFlow协议是一个得到高度认可的标准，在讨论SDN时往往与OpenFlow-一起讨论。</li><li>OpenFlow协议可被看成是SDN体系结构中控制层面与数据层面之间的通信接口。</li><li>OpenFlow协议使得控制层面的控制器可以对数据层面中的物理设备进行直接访问和控制。</li></ul><h4 id="数据层面"><a href="#数据层面" class="headerlink" title="数据层面"></a>数据层面</h4><p>传统意义上的数据层面的任务：根据转发表转发分组。</p><ul><li>进行“匹配”：查找转发表中的网络前缀，进行最长前缀匹配。</li><li>执行“动作”：把分组从匹配结果指明的接口转发出去。</li></ul><p>SDN中的广义转发分为以下两个步骤：</p><ul><li>进行“匹配”：能够对网络体系结构中各层（数据链路层、网络层、运输层）首部中的字段进行匹配。</li><li>执行“动作”：不仅转发分组，还可以负载均衡、重写IP首部（类似NAT路由器中的地址转换）人为地阻挡或丢弃一些分组（类似防火墙一样）</li></ul><p>在SDN的广义转发中，完成“匹配+动作”的设备并不局限在网络层工作，因此不再称为路由器，而称为“OpenFlow交换机”或”分组交换机”，或更简单地称为“交换机”</p><p>相应的，在SDN中取代传统路由器中转发表的是“流表(Flow Table)“。</p><ul><li>一个流就是穿过网络的一种分组序列，而在此序列中的每个分组都共享分组首部某些字段的值。例如，某个流可以是具有相同源P地址和目的P地址的一<br>连串分组。</li><li>OpenFlow交换机中的流表是由SDN远程控制器来管理的。SDN远程控制器通过一个安全信道，使用OpenFlow协议来管理OpenFlow交换机中的流表。</li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第四章：网络层&quot;&gt;&lt;a href=&quot;#第四章：网络层&quot; class=&quot;headerlink&quot; title=&quot;第四章：网络层&quot;&gt;&lt;/a&gt;第四章：网络层&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;网际层和网络层本质上并无不同，只不过属于不同模型。&lt;/p&gt;
&lt;/block</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第三章：数据链路层</title>
    <link href="https://blog.willmo.top/posts/Computer%20Networks%20Chapter%203:%20Data%20Link%20Layer/"/>
    <id>https://blog.willmo.top/posts/Computer%20Networks%20Chapter%203:%20Data%20Link%20Layer/</id>
    <published>2026-04-20T11:42:30.635Z</published>
    <updated>2026-04-20T11:42:31.555Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第三章：数据链路层"><a href="#第三章：数据链路层" class="headerlink" title="第三章：数据链路层"></a>第三章：数据链路层</h1><h2 id="一些概念"><a href="#一些概念" class="headerlink" title="一些概念"></a>一些概念</h2><p><img src="https://s2.loli.net/2026/02/03/wAK1f6B5zepasqd.png" alt="image-20260203214001521"></p><p>因为物理层不理解也无须理解帧的结构，我们可以看作是信息在链路层的水平传输。注意，这里的链路层协议不同是<strong>因为已经不再是直接相连的两个节点</strong>，不要认为对等实体的性质是错误的。</p><ul><li><strong>链路</strong>：指从一个节点到相邻节点的一段物理线路（有线或无线），而中间没有任何其他的交换节点。图中有三段链路，而H1-R2叫<strong>通信路径</strong></li><li><strong>数据链路</strong>：基于链路，当在一条链路上传送数据时，除需要链路本身，还需要一些必要的通信协议来控制这些数据的传输，把<strong>实现这些协议的硬件和软件加到链路上</strong>，就构成了数据链路。</li><li><ul><li>网卡和对应的软件驱动已经实现了这些协议，网卡包含了物理层和数据链路层这两层的功能。</li></ul></li><li><strong>帧</strong>：是数据链路层对等实体之间在水平方向进行逻辑通信的协议数据单元<strong>PDU</strong>。</li></ul><h2 id="封装成帧"><a href="#封装成帧" class="headerlink" title="封装成帧"></a>封装成帧</h2><p><img src="https://s2.loli.net/2026/02/03/8Al6EyRoJq1B9ju.png" alt="image-20260203215808316"></p><p>如图，帧是数据链路层将下放的数据单元加上链路层首部和尾部形成的。</p><p>所以我们就得到了定义。</p><p><strong>封装成帧</strong>是指数据链路层给上层交付下来的协议数据单元PDU添加一个首部和一个尾部，使之成为<strong>帧</strong>。</p><p>帧的首部和尾部包含重要的控制信息，比如帧定界标识，标识某一段是帧(但并不是每一种协议都有帧定界标志，如以太网V2的MAC帧)</p><p>以太网V2的MAC帧会在物理层加上前导码，前导码会同步接收方的CLK，这使得帧能够被时间自动分组。</p><p>为了提高数据链路层传输帧的效率，应当使帧的<strong>数据载荷的长度尽可能地大于首部和尾部的长度</strong>。</p><p>但是考虑到对缓存空间的需求以及差错控制等诸多因素，每一种数据链路层协议都规定了帧的数据载荷的长度上限，即最大传送单元(Maximum Transfer Unit,MTU)。例如，以太网的MTU为1500个字节。</p><h3 id="透明传输"><a href="#透明传输" class="headerlink" title="透明传输"></a>透明传输</h3><p><img src="https://s2.loli.net/2026/02/03/ZgOS28MKozxqwTt.png" alt="image-20260203222501511"></p><p>如果数据载荷中出现了和帧定界相同的字段，可能导致帧被错误识别，对此我们需要透明传输。</p><p><strong>透明传输</strong>是指数据链路层对上层交付下来的协议数据单元PDU没有任何限制，就好像数据链路层不存在一样。</p><p>实现透明传输有很多方法：</p><ul><li><strong>字节填充</strong>：对于面向字节的物理链路，发送前扫描帧的数据载荷，如果有和帧定界相同的字段就在前加上转义符，接收方读取到转义符就能够正确识别帧</li><li><strong>比特填充</strong>：对于面向比特的物理链路就是用比特填充，大同小异</li></ul><p><img src="https://s2.loli.net/2026/02/03/YFwSaUbKVpOJ5ux.png" alt="image-20260203224415198"></p><h3 id="差错传输"><a href="#差错传输" class="headerlink" title="差错传输"></a>差错传输</h3><p><img src="https://s2.loli.net/2026/02/03/MAmGJuga3lYz72t.png" alt="image-20260203220526390"></p><p>由于物理等因素，数据在传输的过程中往往不可避免地会出现错误，数据域中可能出现失真，二进制位取反等问题，这被称为<strong>比特差错</strong></p><p>在一段时间内，传输错误的比特数量占所传输比特总数的比率称为误码率(Bit Error Rate,BER)。</p><p>提高链路的信噪比可以降低误码率。<br>但在实际的通信链路上，不可能使误码率下降为零。因此使用差错检测技术是必要手段。</p><p>发送方根据数据载荷算出检错码放在帧尾部，接收方再提取帧尾部，用与发送方相同的计算方式验证传输是否有误。检错码被称为<strong>帧检验序列FCS</strong></p><h4 id="奇偶检验"><a href="#奇偶检验" class="headerlink" title="奇偶检验"></a>奇偶检验</h4><p><strong>奇校验</strong>是在待发送的数据后面添加1个校验位，使得<strong>添加该校验位后的整个数据</strong>中比特1的个数为奇数。<br><strong>偶校验</strong>是在待发送的数据后面添加1个校验位，使得<strong>添加该校验位后的整个数据</strong>中比特1的个数为偶数。</p><p><img src="https://s2.loli.net/2026/02/04/uJshaxAy1GfPz6i.png" alt="image-20260204154418422"></p><p>如果产生了两位误码，导致比特1奇性不改变，就无法检测出错误。这就被称为漏检。</p><h4 id="循环冗余校验"><a href="#循环冗余校验" class="headerlink" title="循环冗余校验"></a>循环冗余校验</h4><p>数据链路层广泛使用漏检率极低的**循环冗余校验(Cyclic Redundancy Check,CRC)**检错技术。</p><h4 id="基本思想"><a href="#基本思想" class="headerlink" title="基本思想"></a>基本思想</h4><ul><li>收发双方约定好一个生成多项式G(X)。</li><li>发送方基于待发送的数据和生成多项式G(X),计算出差错检测码（冗余码），将冗余码添加到待发送数据的后面一起传输。</li><li>接收方收到数据和冗余码后，通过生成多项式G(X)来计算收到的数据和冗余码是否产生了误码。</li></ul><p><img src="https://s2.loli.net/2026/02/04/SXdcqNB3LEYTHiQ.png" alt="image-20260204155411807"></p><p><img src="https://s2.loli.net/2026/02/04/qdcYiLaNuo5U1mS.png" alt="image-20260204155435579"></p><p><strong>缺一条冗余码计算</strong></p><h3 id="可靠传输与不可靠运输"><a href="#可靠传输与不可靠运输" class="headerlink" title="可靠传输与不可靠运输"></a>可靠传输与不可靠运输</h3><ul><li>可靠传输服务</li></ul><p>如果是可靠，接收方检验出错误包的时候会发送信息让发送方重传，确保每个包都能无误送到。</p><ul><li>不可靠传输服务</li></ul><p>如果是不可靠，接收方在检验出有分组的包会直接丢弃而不会采取任何补救措施。</p><p>在一般情况下，有线链路的误码率比较低。为了减小开销，并不要求数据链路层向其上层提供可靠传输服务。即使出现了误码，可靠传输的问题由其上层处理。</p><p>无线链路易受干扰，误码率比较高，因此要求数据链路层必须向其上层提供可靠传输服务。</p><p>值得一提的是，误码只是传输差错中的一种，差错类型还有分组丢失，分组失序，分组重复，这三种情况发生在<strong>数据链路层的上层</strong></p><p>可靠传输服务并不局限于数据链路层，其他各层均可选择实现可靠传输。可靠传输的实现比较复杂，开销比较大，是否使用可靠传输取决于应用需求。</p><p>下面将介绍三种可靠传输的协议，这三种协议并不局限于数据链路层，也可以用在数据链路层的上层。</p><h4 id="停止-等待协议"><a href="#停止-等待协议" class="headerlink" title="停止-等待协议"></a>停止-等待协议</h4><p><img src="https://s2.loli.net/2026/02/04/1TfLbpWAlBQSYNU.png" alt="image-20260204222238920"></p><p>发送方在发送一个分组之后，不会将该分组从发送缓存中立即删除，而是会等待接收方传回ACK确认分组才会删除。接收方在确认传输无误的情况下会返回确认分组。</p><p>接收方在发现收到的分组出错之后，向发送方发送否认分组NAK，发送方察觉到否认分组之后重发该分组，直到接收方确认传输无误传回ACK。</p><p>但是这种机制，如果出现了分组丢失情况，接收方不会发回确认分组。所以，发送方会内置一个计时器RTO，如果在应当传回ACK的时间内没有接到回复，就会重发分组，直到接收方回复ACK确认分组</p><blockquote><p>否认机制是为了可以使发送方在超时计时器超时前就尽快重传，保证传输效率</p></blockquote><p><img src="https://s2.loli.net/2026/02/04/ZNkideB7hqnLmoj.png" alt="image-20260204222805497"></p><p>但是如果ACK确认分组在返回的过程中丢失，发送方会重发一次分组，这就造成了分组重复。需要再加一个分组编号机制。</p><p>只要保证每发送一个新的数据分组，其序号与上次发送的数据分组的序号不同就可以了，因此用一个比特来编号就够了，序号有0和1这两个。</p><p><img src="https://s2.loli.net/2026/02/04/gPKJduVi59QSIzL.png" alt="image-20260204223037459"></p><p>同理，确认分组也是需要编号的，不然如果ACK迟到的情况：</p><p><img src="https://s2.loli.net/2026/02/04/9aY2jpHqtJmVRSv.png" alt="image-20260204223913821"></p><p>这就导致的对第二个分组的传输情况忽视了，所以需要编号。</p><blockquote><p>实际上数据链路层极少出现ACK迟到的情况，可以不给确认分组编号</p></blockquote><p><img src="https://s2.loli.net/2026/02/04/FUgLDufW1QHIoMG.png" alt="image-20260204224054592"></p><p>至此，使用停止-等待协议就能够在不可靠信道上实现可靠传输了。</p><blockquote><p><strong>注意：停止-等待协议属于自动请求重传(Automatic Repeat reQuest,ARQ)协议。即重传的请求是发送方自动进行的，而不是接收方请求发送方重传某个误码的数据分组。</strong></p></blockquote><h4 id="信道利用率"><a href="#信道利用率" class="headerlink" title="信道利用率"></a>信道利用率</h4><p><img src="https://s2.loli.net/2026/02/04/lyJmWFRnzHeBKu1.png" alt="image-20260204224952452"></p><p>所以，当RTT远大于发送时间TD时(如卫星链路)，会导致信道利用率很低。如果是超时重传，还要更低。</p><p>反之如果在无线局域网的情况下，那信道利用率还是挺高的。</p><p>所以这种协议不是很实用，往往使用其他的两个协议。</p><h3 id="停止-等待协议的局限"><a href="#停止-等待协议的局限" class="headerlink" title="停止-等待协议的局限"></a>停止-等待协议的局限</h3><p>停止-等待协议的<strong>等待接收方传回确认分组</strong>的时间属于<strong>死时间</strong>，使用流水线运输可以优化，即发送方连续发送数据分组。</p><p><img src="https://s2.loli.net/2026/02/06/e9a3JoS6KMZuzIV.png" alt="image-20260206123603293"></p><p>但是在使用流水线传输方式时，发送方不能无限制地连续发送数据分组，否则可能会导致网络中的路由器或接收方来不及处理这些数据分组，进而导致数据分组的丢失，这实际上是对网络资源的浪费。因此，必须采取措施来限制发送方连续发送数据分组的数量。</p><p>回退N帧协议采用流水线传输方式，并且利用发送窗口来限制发送方连续发送数据分组的数量，这属于<strong>连续ARQ协议</strong>。</p><h3 id="回退N帧协议"><a href="#回退N帧协议" class="headerlink" title="回退N帧协议"></a>回退N帧协议</h3><p>采用n个比特给分组编序号，序号范围是0～（2n－1）。本例假设采用3个比特给分组编序号则序号范围是0~7。</p><p>发送方需要维护一个发送窗口Wt，在未收到接收方确认分组的情况下，发送方可将序号落入Wt内的所有数据分组连续发送出去。<br>采用n个比特给分组编序号，则Wt的取值范围是1&lt;Wt≤(2的n次方-1)。本例假设采用3个<br>比特给分组编序号，则W的取值范围是2~7，本例取Wt=5。</p><p>同样地，接收方需要维护一个接收窗口WR,只有正确到达接收方（无误码）且序号落入WR内的数据分组才被接收方接收。<br>WR的取值只能是1，这一点与停止等待协议是相同的。</p><p><img src="https://s2.loli.net/2026/02/06/lV8YP7a2tQCm9Up.png" alt="image-20260206153907750"></p><p>接收方每正收到一个序号落入接收窗口的数据分组，就将接收窗口向前滑动一个位置，<br>这样就有一个新的序号落入接收窗口。与此同时，接收方还要给发送方发送针对该数据分组的确认分组。</p><p>发送方每收到一个按序确认的确认分组，就将发送窗口向前滑动一个位置，这样就有一个新的序号落入发送窗口，序号落入发送窗口内的数据分组可继续被发送。</p><p>在回退帧协议的工作过程中，发送方的发送窗口和接收方的接收窗口按上述规则不断向前滑动。因此，这类协议又称为<strong>滑动窗口协议</strong>。</p><p>看一个无传输差错的例子：</p><p><img src="https://s2.loli.net/2026/02/06/2Qw3gpaMAxhXTfL.png" alt="image-20260206154208297"></p><p>发送方将0-4划分为发送窗口，将0-4连续发送出去，分组经过链路到达接收端，接收方确认传输过程中没有误码失序等情况后就将确认分组ACK传回，每传回一个ACK，接收窗口就向前移动一个分组，发送方每接收一个确认分组，就向前移动一个位置，已发送且不在窗口内的分组就可以从发送分组中删除了。</p><p>在无传输差错的情况下，回退N帧协议的信道利用率比停止.等待协议的信道利用率有显著提高。提高的程度取决于发送窗口的大小。</p><p>如果出现超时重传：</p><p><img src="https://s2.loli.net/2026/02/06/FZJB2ImjE83XrlO.png" alt="image-20260206184707489"></p><p>如图，2号分组出现传输错误，接收方将损坏分组二号丢弃，为了保证接受顺序，无论三号和四号是否损坏都会被丢弃。此处，接收窗口只会移动两次到2的位置就停止。随即，在照例发送完0号和1号的ACK分组之后，接收方会<strong>发送针对最近已按序接收的数据分组的确认分组</strong>，在这里是发送一号分组。发送方接收到发送的0和1分组后将发送窗口后移两位。随即发送方两个1号的ACK的重复确认。随即在新的发送窗口中发送5号和6号分组。接收方接到后因为2号缺失导致接收窗口无法移动，又会发送两次1号分组。随即发送方知道<strong>有四个分组(3,4,5,6)未能正确传输</strong>，那么发送方可立即重发或者等到计时器超时再重发发送窗口内的所有分组(在这里是2,3,4,5,6)，即使3,4,5,6此前未出现错误，但是2的缺位导致无法被正确接收。</p><p>这就是回退N帧的由来，一旦出错就要回退N个帧。所以，一个数据分组的差错就可能呙引起大量数据分组的重传在信道质量较差(容易出现误码)的情况下，<strong>回退N帧协议的信道利用率并不比停止-等待协议的信道利用率高。</strong></p><p>值得一提的是，回退N帧协议的接收方采取积累确认的方式。</p><ul><li>接收方不必对收到的每一个数据分组都发送一个确认分组，而是可以在收到几个序号连续的数据分组后，对按序到达的最后一个数据分组发送确认分组。</li><li>接收方何时发送累积确认分组，由具体实现决定。</li><li>确认分组ACKn表明序号为n及之前的所有数据分组都已正确接收。比如，ACK4表示0-4的数据分组都已被正确接收。</li></ul><p>累积确认能够减少向网络中注入确认分组的数量，且即使确认分组丢失，也可能不必重传数据分组。但是不能向发送方及时准确地反映出接收方已正确接收的所有数据分组的数量。</p><p>我们来看一个例子，如果发送窗口的取指大于比特位的范围。</p><p><img src="https://s2.loli.net/2026/02/07/ELqzcxwROSmotvu.png" alt="image-20260207154918735"></p><p>发送方连续发送0-7分组，接收方接收并将接收窗口向后移动8个单位，移动到0的位置，传回ACK确认分组。但是ACK在传输中丢失，发送方重传计时器超时重传0-7分组，接收方根据分组号认定为是0号数据可以接受。这就重复传输了8个数据分组造成传输错误。</p><p>所以，如果发送窗口大小超过了比特位的范围会导致接收方无法分辨出新旧分组而导致传输错误。</p><p>回退N帧协议对出错分组之后的重传依旧会浪费通信资源，是一个可以优化的点。</p><p>我们希望发送方能够仅仅重发出现发错的分组，节省信道资源。</p><h3 id="选择-重传协议"><a href="#选择-重传协议" class="headerlink" title="选择-重传协议"></a>选择-重传协议</h3><p><img src="https://s2.loli.net/2026/02/07/xXaAFoQM41CG3su.png" alt="image-20260207161725552"></p><p>为了使发送方仅重传出现差错的数据分组接收方<strong>不再采用累积确认</strong>，而需要对每一个正确接收的数据分组进行逐一确认。</p><p><img src="https://s2.loli.net/2026/02/07/aJFBgpZsiGv8hKW.png" alt="image-20260207162929705"></p><p>如图，发送方连续发送0-3分组，但是0号数据分组在传输过程中出错，接收方接收到0-3分组，并检查0号分组出错将其丢弃，并将1-3分组缓存(不交付给上层)，接收窗口无法向前滑动 。随即，接收方发回1-3号ACK，发送方收到后无法滑动发送窗口，开始确认1-3号分组已被接受，开始重传0号数据分组。接收方收到后传回ACK0，双方滑动窗口开始下一阶段。</p><p><img src="https://s2.loli.net/2026/02/07/XHSjsiPFxAbVlu9.png" alt="image-20260207164034661"></p><p>如果是接收窗口小于发送窗口，就会出现接收方无法分辨新旧数据分组的错误。</p><p>发送方一次性连续发送0-4，被正确传输并接受，接收方滑动窗口并传回0-4号ACK但是0号丢失。随即发送方的0号重传计时器超时，重传0号，接收方发现收到的0号分组处于滑动窗口内，开始接受不属于这里的0号数据分组，这就乱套了。</p><h3 id="点对点协议PPP"><a href="#点对点协议PPP" class="headerlink" title="点对点协议PPP"></a>点对点协议PPP</h3><p>点对点协议(Point-to-Point Protocol,PPP)是目前使用最广泛的<strong>点对点数据链路层协议</strong>。</p><p>PPPoE协议是基于PPP开发的针对以太网的传输协议。</p><p>PPP协议包含了：</p><ul><li>一套网络控制协议NCPs</li><li>一个网络层PDU封装到串行链路的方法</li><li>一个链路控制协议LCP</li></ul><h4 id="PPP帧结构"><a href="#PPP帧结构" class="headerlink" title="PPP帧结构"></a>PPP帧结构</h4><p><img src="https://s2.loli.net/2026/02/07/Sn5xgPrHBCw3I2l.png" alt="image-20260207185539431"></p><h4 id="PPP协议的透明传输"><a href="#PPP协议的透明传输" class="headerlink" title="PPP协议的透明传输"></a>PPP协议的透明传输</h4><p><img src="https://s2.loli.net/2026/02/09/xXmhcoUGFkl3PHK.png" alt="image-20260209163946147"></p><p><img src="https://s2.loli.net/2026/02/09/Djc1CteUolA2Yaq.png" alt="image-20260209164115812"></p><h3 id="差错检测"><a href="#差错检测" class="headerlink" title="差错检测"></a>差错检测</h3><p><img src="https://s2.loli.net/2026/02/09/nGLpVwSbE58BYAz.png" alt="image-20260209164212920"></p><p>帧检验序列FCS字段：其值是使用循环冗余校验CRC计算出的检错码。CRC采用生成的多项式为</p><p>$$<br>CRC-CCITT=X^{16}+X^{12}+X^{5}+1<br>$$</p><p>接收方每收到一个PPP帧，就进行CRC检验。<br>若CRC检验正确，就收下这个帧；否则，就丢弃这个帧。<br>使用PPP的数据链路层，向上提供的是不可靠<br>数据传输服务</p><h3 id="PPP的工作状态"><a href="#PPP的工作状态" class="headerlink" title="PPP的工作状态"></a>PPP的工作状态</h3><p>以用户主机拨号接入因特网服务提供者ISP的拨号服务器的过程为例</p><p><img src="https://s2.loli.net/2026/02/09/WyCR4Jbc9Stlhs6.png" alt="image-20260209164645605"></p><h3 id="共享式以太网"><a href="#共享式以太网" class="headerlink" title="共享式以太网"></a>共享式以太网</h3><p>以太网(Ethernet)以曾经被假想的电磁波传播介质一以太(Ether）来命名。<br>以太网最初采用无源电缆（不包含电源线）作为共享总线来传输帧，属于基带总<br>线局域网，传输速率为2.94Mb/s。</p><p>以太网目前已经从传统的共享式以太网发展到交换式以太网，传输速率已经从10Mb/s提高到100Mb/s、1Gb/s甚至10Gb/s。<br>我们会首先介绍最早流行的传输速率<br>为10Mb/s的共享式以太网的相关知识。</p><h3 id="网络适配器和MAC地址"><a href="#网络适配器和MAC地址" class="headerlink" title="网络适配器和MAC地址"></a>网络适配器和MAC地址</h3><p>要将计算机连接到以太网，需要使用相应的网络适配器(Adapter),网络适配器一般简称为“网卡”。</p><p><img src="https://s2.loli.net/2026/02/09/4cjLYJgDsF3ux8M.png" alt="image-20260209185930441"></p><p>在计算机内部，网卡与CPU之间的通信，一般是通过计算机主板上的IO总线以并行传输方式进行。<br>网卡与外部以太网（局域网）之间的通信，一般是通过传输媒体（同轴电缆、双绞线电缆、光纤）以<strong>串行方式</strong>进行的。<br>网卡除要实现物理层和数据链路层功能，其另外一个重要功能就是要进行<strong>并行传输和串行传输</strong>的转换。<br>由于网络的传输速率和计算机内部总线上的传输速率并不相同，因此在网卡的核心芯片中都会包含用于缓存数据的存储器。</p><h4 id="MAC地址"><a href="#MAC地址" class="headerlink" title="MAC地址"></a>MAC地址</h4><p>在以太网集群中，多个主机连接在同一个广播信道上，要想实现两个主机之间的通信，则每个主机都必须有一个唯一的标识，即一个数据链路层地址。</p><p>所以，在每个主机发送的帧的首部中，都携带有发送主机（源主机）和接收主机（目的主机）的数据链路层地址。由于这类地址是用于媒体接入控制(Medium Access Control,MAC)的，因此被称为MAC地址。</p><p>MAC地址一般被固化在网卡的电可擦可编程只读存储器EEPROM中，因此MAC地址也被称为硬件地址或物理地址</p><p>一般情况下，普通用户计算机中往往会包含两块网卡：</p><ul><li>一块是用于接入有线局域网的以太网卡</li><li>一块是用于接入无线局域网的Wi-Fi网卡</li></ul><p>每块网卡都有一个全球唯一的MAC地址。<br>某些服务器,交换机或者路由器内置多个网卡，有多个MAC地址。<br>所以，严格来说，MAC地址是对网络上<strong>各接口</strong>的唯一标识，而不是对网络上各设备的唯一标识。</p><p>在IEEE 802局域网中，MAC地址格式如下：</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260209220030889.png" alt="image-20260209220030889"></p><p>我们能够根据OUI查询到该网卡所属的厂商。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260209221026856.png" alt="image-20260209221026856"></p><p><strong>单播</strong>：<strong>一对一</strong>通信，信息从一台设备准确发送到另一台<strong>指定的</strong>设备。</p><p><strong>多播</strong>：<strong>一对多</strong>通信，信息从一台设备发送给<strong>一组有共同需求的</strong>设备。</p><p>如果将本地多播地址设置为<strong>除B0B1以外全1时，就是广播地址</strong></p><p>在发送顺序中：</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260209221326998.png" alt="image-20260209221326998"></p><h4 id="单播MAC地址使用"><a href="#单播MAC地址使用" class="headerlink" title="单播MAC地址使用"></a>单播MAC地址使用</h4><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260209221829801.png" alt="image-20260209221829801"></p><p>主机B填写目的地址和源地址后，发送到当前以太网中的每一个设备，主机A发现收到的帧的目的地址和自己并不匹配(这里不用相同这个词，是要考虑广播这种情况)，所以丢弃该帧。主机B收到后发现匹配，就在检查传输正确与否后上交到网络层等处理。</p><p>若主机B的目的地址填写<code>FF-FF-FF-FF-FF-FF</code>就是广播，以太网中的所有设备都会接收并检查是否有传输错误。</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>网卡从网络上每收到一个无误码的帧，就检查帧首部中的目的MAC地址，按以下情况处理：</p><ul><li>如果目的MAC地址是广播地址(FF-FF-FF-FF-FF-FF),则接受该帧。</li><li>如果目的MAC地址与网卡上固化的全球单播MAC地址相同，则接受该帧。</li><li>如果目的MAC地址是网卡支持的多播地址，则接受该帧。</li><li>除上述(1)、(2)和(3)情况外，丢弃该帧。</li></ul><p>网卡还可被设置为一种特殊的工作方式：**混杂方式(Promiscuous Mode)**。工作在混杂方式的网卡只要收到共享媒体上传来的帧就会收下，而不管帧的目的MAC地址是什么。</p><p>全球单播MAC地址就如同身份证上的身份证号码，具有唯一性，它往往与用户个人信<br>息绑定在一起。因此，用户应尽量确保自己拥有的全球单播MAC地址不被泄露。<br>为了避免用户设备连接Wi-F热点时MAC地址泄露的安全问题，目前大多数移动设备都已经采用了随机MAC地址技术。</p><h3 id="CSMA-x2F-CD协议的基本原理"><a href="#CSMA-x2F-CD协议的基本原理" class="headerlink" title="CSMA/CD协议的基本原理"></a>CSMA/CD协议的基本原理</h3><p>在共享总线以太网中，多个站点连接在同一总线上，在使用总线时会发生信道碰撞等问题。CSMA/CD协议用于解决信道的调度问题。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260210210518335.png" alt="image-20260210210518335"></p><p>以太网还有一个强化碰撞的机制：发送帧的站点一旦检测到碰撞，除了立即停止发送帧外，还要再继续发送32比特或48比特的人为干扰信号(Jamming Signal),以便有足够多的碰撞信号使所有站点都能检测出碰撞。</p><p>该协议只能尽量避免碰撞，且不可能进行全双工通信，而只能进行半双工通信（双向交替通信）。</p><h3 id="共享式以太网的争用期"><a href="#共享式以太网的争用期" class="headerlink" title="共享式以太网的争用期"></a>共享式以太网的争用期</h3><p>3.4的两个章节后面再看</p><h3 id="使用集线器的共享式以太网"><a href="#使用集线器的共享式以太网" class="headerlink" title="使用集线器的共享式以太网"></a>使用集线器的共享式以太网</h3><p>无源电缆接触点多，依赖元件多，并不可靠，替代他的是更便宜更灵活的集线器。</p><p>集线器具有以下特点：</p><ul><li>使用集线器的以太网虽然物理拓扑是星型的，但在逻辑上仍然是一个总线网。总线上的各站点共享总线资源，使用的还是CSMA/CD协议。</li><li>集线器只工作在物理层，它的每个接口仅简单地转发比特，并不进行碰撞检测。碰撞检测的任务由各站点中的网卡负责。</li><li>集线器一般都有少量的容错能力和网络管理功能，能够修复网络连接中的小问题。</li></ul><h3 id="在物理层扩大以太网"><a href="#在物理层扩大以太网" class="headerlink" title="在物理层扩大以太网"></a>在物理层扩大以太网</h3><p>可以直接使用一个更大的集线器将两个以太网集群相连，但是那么做会形成更大碰撞域，导致碰撞的概率增加。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260211000717786.png" alt="image-20260211000717786"></p><h3 id="在数据链路层扩大以太网"><a href="#在数据链路层扩大以太网" class="headerlink" title="在数据链路层扩大以太网"></a>在数据链路层扩大以太网</h3><p>可以使用网桥来扩大以太网</p><p>网桥可以识别帧的结构，且能根据目的地址或自身转发表转发帧。</p><p>网桥不会形成更大碰撞域，不会增加碰撞概率。</p><p><img src="https://files.seeusercontent.com/2026/02/16/A9es/image-20260211001036357.png" alt="image-20260211001036357"></p><h4 id="自学习和转发帧流程"><a href="#自学习和转发帧流程" class="headerlink" title="自学习和转发帧流程"></a>自学习和转发帧流程</h4><p><img src="https://files.seeusercontent.com/2026/02/16/g9nQ/image-20260211002234919.png" alt="image-20260211002234919"></p><p>记得这个吗？</p><p>网桥采用<strong>自学习算法</strong>建立转发表。</p><p>初始时，网桥的转发表是空的，直到接收到第一个广播的帧(A-&gt;B)时，将帧的来源和网桥接口作为键值对登记起来。因为是第一个键值对，没有关于B的接口信息，所以将帧盲目转发给所有接口。当D-&gt;A的帧抵达网桥时，网桥记录D位于接口2并将帧发送到接口1，抵达主机A。剩下的流程不解释。</p><p><img src="https://files.seeusercontent.com/2026/02/16/b2Ab/image-20260211003606249.png" alt="image-20260211003606249"></p><p>所以：</p><ul><li>网桥收到帧后进行登记（即自学习），登记的内容为帧的源MAC地址和进入网桥的接口号。</li><li>网桥根据帧的目的MAC地址和网桥的转发表对帧进行转发，包含以下三种情况：</li><li><ul><li>·明确转发：网桥知道应当从哪个接口转发帧。</li><li>·盲目转发：网桥不知道应当从哪个接口转发帧，只能将其通过除进网桥的接口外的其他所有接口<br>转发。</li><li>丢弃：网桥知道不应该转发该帧，将其丢弃。</li></ul></li></ul><h3 id="透明网桥的生成树协议STP"><a href="#透明网桥的生成树协议STP" class="headerlink" title="透明网桥的生成树协议STP"></a>透明网桥的生成树协议STP</h3><p>为了提高以太网的可靠性，有时需要在两个以太网之间使用多个透明网桥来提供冗余链路。</p><p>但是如果网络链路形成了闭合环路，那会导致帧在闭合环路中永久兜圈，浪费通信资源。</p><p>所以我们会生成一个连接所有节点的最小生成树，帧的传输就在该生成树中进行。</p><p>生成树：使得每个节点之间都有通路的路径。</p><p><img src="https://files.seeusercontent.com/2026/02/16/Fg3k/image-20260211004742372.png" alt="image-20260211004742372"></p><h3 id="交换式以太网"><a href="#交换式以太网" class="headerlink" title="交换式以太网"></a>交换式以太网</h3><p>网桥接口很少，只用来连接不同的网段。</p><p>交换式集线器是具有多个接口的网桥，被称为以太网交换机或二层交换机。</p><p>仅使用交换机（而不使用集线器）的以太网就是交换式以太网。</p><p>交换式以太网使用的核心还是网桥，在交换式以太网中进行扩展，<strong>不会</strong>形成一个更大的<strong>冲突域</strong>。</p><h3 id="以太网交换机"><a href="#以太网交换机" class="headerlink" title="以太网交换机"></a>以太网交换机</h3><p>交换机的每个接口可以连接计算机，也可以连接集线器或另一个交换机。</p><p>当交换机的接口与计算机或交换机连接时，可以工作在<strong>全双工</strong>方式，并能在自身内部同时连通多对接口，使每一对相互通信的计算机都能像独占传输媒体那样，无碰撞地传输数据，这样就不需要使用CSMA/CD协议了。</p><p>一般的交换机都采用“存储转发”方式，为了减小交换机的转发时延，某些交换机采用了直通(Cut-Through)交换方式。<br>采用直通交换方式的交换机，在接收帧的同时就立即按帧的目的MAC地址决定该帧的转发接口，然后通过其内部基于<strong>硬件的交叉矩阵</strong>进行转发，而不必把整个帧先缓存后再进行处理。</p><p>(交叉矩阵的图在我看的那本书上有，等我有时间再拍下来。)</p><p>直通交换时延很小，但是不会检查差错，可能将一些无效帧转发给其他主机。</p><h3 id="共享式和交换式对比"><a href="#共享式和交换式对比" class="headerlink" title="共享式和交换式对比"></a>共享式和交换式对比</h3><p>明确，共享式是在物理层扩展，交换式是在数据链路层扩展。</p><p>3.5.2，待补全</p><h3 id="以太网的MAC帧格式"><a href="#以太网的MAC帧格式" class="headerlink" title="以太网的MAC帧格式"></a>以太网的MAC帧格式</h3><p>分为</p><ul><li>DIX Ethernet V2的帧格式</li><li>IEEE 802.3的帧格式</li></ul><p>两者差别很小，第一章较流行，我们介绍第一种。</p><p><img src="https://files.seeusercontent.com/2026/02/16/eT4e/image-20260213220419309.png" alt="image-20260213220419309"></p><p>目的地址和源地址字段用于指明发送方和接收方。</p><p>类型字段用来指明数据载荷中的内容是由上一层的哪个协议封装的，以便将收到的MAC帧的数据载荷上交给上一层的这个协议。</p><p>FCS是使用CRC生成的帧检验序列FCS,接收方的网卡通过FCS的内容就可检测出帧在传输过程中是否产生了误码。</p><p>数据载荷最小为46，最大为1500字节，过小会填充，过大会分组。</p><p>在以太网中，物理层会给帧加上前导码，以便接收方时钟同步。</p><p>接收方可能收到的无效MAC帧包括以下几种：</p><ul><li>MAC帧的长度不是整数个字节</li><li>通过MAC帧的FCS字段的值检测出帧有误码</li><li>MAC帧的长度不在64~1518字节之间</li></ul><h3 id="虚拟局域网VLAN"><a href="#虚拟局域网VLAN" class="headerlink" title="虚拟局域网VLAN"></a>虚拟局域网VLAN</h3><p>将多个站点通过一个或多个以太网交换机连接起来就构建出了交换式以太网。<br>交换式以太网中的所有站点都属于同一个广播域。随着交换式以太网规摸的扩大，广播域也相应扩大。</p><p><img src="https://files.seeusercontent.com/2026/02/16/9Yli/image-20260213224848285.png" alt="image-20260213224848285">但是，巨大的广播域会带来一系列问题：</p><ul><li>广播风暴</li><li><ul><li>某大型以太网中，主机A要给主机B发送帧，但是A不知道B的地址，A发送一个ARP广播请求，广播中的所有主机都会收到并查看该请求。这会浪费网络资源和CPU资源。</li></ul></li></ul><p><strong>使用路由器能够分割广播域，因为路由器默认不会对分组进行转发</strong>。但是路由器成本高，大量使用路由器分割是不现实的，我们会使用虚拟局域网来进行逻辑分割。</p><p>虚拟局域网(Virtual Local Area Network,VLAN)是一种将局域网内的站点分成与物理位置无关的逻辑组的技术，一个逻辑组就是一个VLAN,VLAN中的各站点具有某些共同的应用需求。</p><p>属于同一VLAN的站点之间可以直接进行通信，而不同VLAN中的站点之间<strong>不能直接通信</strong>。<br>网络管理员可对局域网中的各交换机进行配置来建立多个逻辑上独立的VLAN。<br>连接在同一交换机上的多个站点可以属于不同的VLAN,而属于同一VLAN的多个站点可以连接在不同的交换机上。</p><p><img src="https://files.seeusercontent.com/2026/02/16/Q0su/image-20260213225647003.png" alt="image-20260213225647003"></p><p>两个VLAN之间不可直接通信，不会被广播风暴影响。</p><h3 id="VLAN的实现机制"><a href="#VLAN的实现机制" class="headerlink" title="VLAN的实现机制"></a>VLAN的实现机制</h3><p>虚拟局域网VLAN有多种实现技术，最常见的就是基于以太网交换机的接口来实现VLAN。这就需要以太网交换机能够实现以下两个功能：</p><ul><li>能够处理带有VLAN标记的帧，也就是IEEE802.1Q帧。</li><li>交换机的各接口可以支持不同的接口类型，不同接口类型的接口对帧的处理方式有所不同。</li></ul><p>IEEE 802.1Q帧也称为Dot One QI帧，它对以太网V2的MAC帧格式进行了扩展：在源地址字段和类型字段之间插入了4字节的<strong>VLAN标签(tag)字段</strong>，由<strong>标签协议标识符TPID,优先级PRI,规范格式指示符CFI,虚拟局域网标识符VID</strong>组成。</p><ul><li>TPID：长度为16比特，其值固定为0x8100,表示该帧是EEE802.1Q帧，使用对应的协议处理。</li><li>PRI:长度为3比特，取值范围是0~7，值越大优先级越高。当网络阻塞时，设备优先发送优先级高的802.1Q帧。</li><li>CFI:长度为1比特，取值为0表示MAC地址以规范格式封装，取值为1表示MAC地址以非规范式封装。对于以太网，CF的取值为0。</li><li>VID：全写VLAN ID，长度为12比特，取值范围是0~4095，其中0和4095保留不使用。VID是802.1Q帧所属的VLAN编号，设备依赖VID来识别帧所属的VLAN。<strong>广播一般只在同一VLAN转发，限制广播域</strong></li></ul><p>帧是由以太网交换机处理的。</p><p>当交换机收到普通的以太网MAC帧时，会给其插入4字节的VLAN标签使之成为802.1Q帧，该处理简称为“打标签”。<br>当交换机转发802.1Q帧时，可能会删除其4字节的VLAN标签使之成为普通的以太网MAC帧，该处理简称为“去标签”。</p><p>交换机转发802.1Q帧时也有可能不进行“去标签”处理，是否进行“去标签”处理取决于交换机的接口类型。</p><p>根据接受和发送对帧的处理方式不同和接口连接对象不同，能够将以太网交换机接口类型分为Access和Trunk两种。</p><p>PVID（Port VLAN ID）是<strong>端口默认VLAN ID</strong>，指交换机端口为接收到的<strong>未打标签的数据帧</strong>所添加的VLAN标签。在下文中称作“缺省VLAN ID”</p><table><thead><tr><th align="center">特性</th><th align="left">PVID (Port VLAN ID)</th><th>VLAN ID</th></tr></thead><tbody><tr><td align="center"><strong>作用对象</strong></td><td align="left"><strong>交换机端口</strong>的属性</td><td><strong>VLAN本身</strong>的标识</td></tr><tr><td align="center"><strong>主要功能</strong></td><td align="left">定义端口对<strong>入向未标记帧</strong>的处理规则</td><td>在交换机内部逻辑上划分广播域</td></tr><tr><td align="center"><strong>配置位置</strong></td><td align="left">在<strong>每个端口</strong>下进行配置</td><td>在全局或VLAN数据库中创建</td></tr><tr><td align="center"><strong>典型场景</strong></td><td align="left">连接PC、服务器等不支持VLAN的终端设备时，端口的PVID通常等于该终端所属的VLAN ID。</td><td>规划网络时，为不同部门（如财务部VLAN 10、技术部VLAN 20）分配不同的ID。</td></tr></tbody></table><ul><li><strong>Access端口</strong>：用于连接<strong>不支持VLAN的终端设备</strong>（如PC、服务器、打印机），实现“<strong>一个端口，一个VLAN</strong>”。</li><li><strong>Trunk端口</strong>：用于<strong>交换机之间或交换机与支持VLAN的网络设备（如路由器、另一台交换机）之间的互联</strong>，实现“<strong>一个端口，多个VLAN</strong>”。</li><li><strong>原生VLAN是Trunk端口上一个特殊的VLAN，其数据帧在Trunk链路上以未标记的形式传输。</strong></li><li><strong>标记VLAN是指在Trunk链路上，其数据帧必须携带802.1Q VLAN标签进行传输的VLAN。</strong></li></ul><table><thead><tr><th>特性</th><th>Access端口</th><th>Trunk端口</th></tr></thead><tbody><tr><td><strong>设计目的</strong></td><td>接入终端设备</td><td>设备间互联，承载多个VLAN流量</td></tr><tr><td><strong>可承载VLAN数量</strong></td><td><strong>仅1个</strong></td><td><strong>多个</strong>（一个原生VLAN + 多个标记VLAN）</td></tr><tr><td><strong>数据处理（入向）</strong></td><td>接收<strong>未标记</strong>帧，为其打上<strong>端口的PVID</strong>标签。</td><td>接收<strong>标记</strong>帧，根据标签放入对应VLAN；接收<strong>未标记</strong>帧，则打上<strong>原生VLAN（Native VLAN）</strong> 的标签。</td></tr><tr><td><strong>数据处理（出向）</strong></td><td>发送前<strong>剥离VLAN标签</strong>，始终发出<strong>未标记</strong>帧。</td><td>对于<strong>标记VLAN</strong>，保留标签发出；对于<strong>原生VLAN</strong>，<strong>剥离标签</strong>后作为未标记帧发出。</td></tr><tr><td><strong>典型连接对象</strong></td><td>计算机、服务器、IP电话、打印机</td><td>另一台交换机、路由器、支持VLAN的服务器</td></tr><tr><td><strong>与PVID的关系</strong></td><td><strong>端口的PVID就是它所属的VLAN ID</strong>，配置Access端口时，本质上就是在设置它的PVID。</td><td>端口有PVID（即其<strong>原生VLAN的ID</strong>），用于处理未标记帧。Trunk口允许的VLAN列表中通常包含其原生VLAN。</td></tr></tbody></table><p>当以太网交换机启动后，若之前未对其各接口进行过VLAN的相关设置，则各接口的接口类型默为Access,并且各接口的缺省VLAN ID:为1，即各接口默认属于VLAN1。</p><p>对于华为交换机，接口的缺省VLAN ID(VID)称为端口VLAN ID(Port VLAN ID),简记为PVID。对别的交换机则不是这样。<br>交换机的每个接口有且仅有一个PVID。</p><p>看一个例子：</p><p><img src="https://files.seeusercontent.com/2026/02/16/xa6F/image-20260214222338597.png" alt="image-20260214222338597"></p><p>本例中，主机A发送一个未标识的帧，交换机的一号接口的类型为Access，就将帧打上默认标签VID=1，因为其他四个接口的VID都等于1，所以交换机会将该帧转发到2,3,4号接口。</p><p><img src="https://files.seeusercontent.com/2026/02/16/cWy1/image-20260214223259123.png" alt="image-20260214223259123"></p><p>本例中，我们可以手动配置以太网交换机，使得1号和2号接口为VLAN2，3号和4号接口为VLAN3。主机C发送一个未标识的帧，交换机将3号端口的PVID作为标签附加到帧中，随后转发到所有PVID为3的端口。</p><p><img src="https://files.seeusercontent.com/2026/02/16/jlK8/image-20260214230507223.png" alt="image-20260214230507223">如图，主机A发送一个未标识的帧，因为端口1的类型是Access，交换机对帧进行贴标签处理，随后发送到交换机1所有PVID为1的接口。发送到5号Trunk接口后，会去除帧的标签发送给交换机2。交换机2将接收到的未标识帧贴上标签，转发给交换机2所有PVID=2的端口，从这些Access端口去标签发送给主机。</p><p><img src="https://files.seeusercontent.com/2026/02/16/Ol0m/image-20260215105048325.png" alt="image-20260215105048325"></p><p>如图，主机D发送一个未标识帧，因为端口3的类型是Access，交换机对帧进行贴标签处理，随后发送到交换机1所有PVID=2的接口也就是4接口。因为<strong>5号接口为Trunk类型且帧的VID与5号接口的PVID不相同</strong>，所以带标签的帧会被直接转发到交换机2。在交换机2中，帧被转发到所有PVID=3的接口中。</p><p>总结一下两种接口类型：</p><p><img src="https://files.seeusercontent.com/2026/02/16/5tiZ/image-20260215105948724.png" alt="image-20260215105948724"></p><p>Hybrid接口是华为交换机私有的接口类型。Hybrid接口既可以用于交换机之间的互连（与Truk接口相同），也可用于交换机与用户计算机之间的互连<br>(与Access接口相同)。<br>除此之外，Hybrid接口的绝大部分功能与Trunk:接口相同。不同点在于Hybrid接口会查看帧的VID值是否在接口的“去标签’列表中，若存在则“去标签”后转发，若不存在则直接转发。</p><h3 id="以太网"><a href="#以太网" class="headerlink" title="以太网"></a>以太网</h3><p>以太网被物理电缆束缚，那么就必须拥有远超无线网络的传输速率。</p><p>3.8未看</p><h3 id="无线局域网"><a href="#无线局域网" class="headerlink" title="无线局域网"></a>无线局域网</h3><p>802.11无线局域网可分为以下两类：</p><ul><li>有固定基础设施的</li><li><ul><li>指的是预先建立的，能够覆盖一定地理范围的，多个固定的通信基站。</li></ul></li><li>无固定基础设施的</li></ul><h4 id="有固定基础设施的802-11无线局域网"><a href="#有固定基础设施的802-11无线局域网" class="headerlink" title="有固定基础设施的802.11无线局域网"></a>有固定基础设施的802.11无线局域网</h4><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260215210154737.png" alt="image-20260215210154737"></p><p>采用星型拓扑结构，其中心基站是网络接入点AP，图中结构名为基本服务集BSS。BSS由一个接入点AP和若干个移动站组成，移动站的网络流量都需要经过接入点AP，覆盖地理范围一般小于100米。管理员需要为AP分配一个最大32字节的服务集标识符SSID(也就是WIFI名)和一个无线通信信道。</p><p>能够使用分配系统扩展服务集：</p><p><img src="https://files.seeusercontent.com/2026/02/16/Pa8s/image-20260215212121725.png" alt="image-20260215212121725"></p><p>如果主机A发送某个数据给主机B，那么数据包依次经过AP1，DS，AP2到达主机B。</p><p>漫游：指的是移动站从一个无线接入点（AP）的信号范围，切换到另一个更强或更合适的接入点保持网络不断的过程。</p><p>在图中表示为主机A到主机A’的过程。</p><p>但是802.11标准并没有定义实现漫游的具体方法，但是定义了以下基本服务：</p><ul><li>关联服务：移动站与接入点AP建立关联的方法有以下两种：</li><li><ul><li>被动扫描：AP周期性发出包含重要参数的信标帧，移动端被动接受</li><li>主动扫描：移动站发出探测请求帧，AP发回探测响应帧。</li></ul></li><li>重建关联服务和分离服务</li><li><ul><li>如果一个移动站要把与某个接入点AP的关联转移到另一个AP,就可以使用重建关联服务；若要终止关联服务，就应使用分离服务。</li></ul></li></ul><h4 id="无固定基础设施的802-11无线局域网"><a href="#无固定基础设施的802-11无线局域网" class="headerlink" title="无固定基础设施的802.11无线局域网"></a>无固定基础设施的802.11无线局域网</h4><p>又名自组织网络，即ad hoc Network网络。</p><p>这种网络没有中心基站AP，具有去中心化的特点</p><p><img src="https://files.seeusercontent.com/2026/02/16/waP1/image-20260215222406062.png" alt="image-20260215222406062"></p><p>比如，源站A到达目的站E需要移动站BCD作为转发站，显然，转发站需要具有路由功能。</p><p>这种网络需要经过网关才能连接上因特网。</p><p>但802.11无线局域网的ad hoc模式允许网络中的各站点在其通信范围内直接通信，也就是支持站点间的单跳通信，而标准中并没有包括多跳路由功能。因此，802.11无线局域网的ad hoc模式应用较少。</p><h3 id="无线局域网的物理层"><a href="#无线局域网的物理层" class="headerlink" title="无线局域网的物理层"></a>无线局域网的物理层</h3><p>802.11无线局域网的物理层非常复杂，依据工作频段、调制方式、传输速率等，可将其分为多种物理层标准。</p><p><img src="https://files.seeusercontent.com/2026/02/16/Pk7p/image-20260215224228628.png" alt="image-20260215224228628"></p><p>所以，无线网卡一般会做成多模的，以适应不同的技术标准</p><p><img src="https://files.seeusercontent.com/2026/02/16/eCx5/image-20260215224304325.png" alt="image-20260215224304325"></p><p>最近又出现新的物理层标准</p><p><img src="https://files.seeusercontent.com/2026/02/16/Jp7y/image-20260215224558785.png" alt="image-20260215224558785"></p><h3 id="无线局域网的数据链路层"><a href="#无线局域网的数据链路层" class="headerlink" title="无线局域网的数据链路层"></a>无线局域网的数据链路层</h3><p>无线局域网同样使用广播信道，但是和共享总线以太网使用有线传输介质不同，不能直接照搬以太网的CSMA/CA协议。</p><p>802.11无线局域网采用了另一种称为<strong>CSMA/CA</strong>的协议，称为<strong>载波监听多址接入/碰撞避免</strong>的协议。CSMA部分是相同的，但是碰撞检测CD改为了碰撞避免CA。</p><p>原因如下：</p><ul><li>由于无线信道的传输环境复杂且信号强度的动态范围非常大，在802.11无线网卡上接收到的信号强度一般都远远小于发送信号的强度，信号强度甚至相差百万倍。因此，如果要在802.11无线网卡上实现碰撞检测，对硬件的要求非常高。</li><li>即使能够在硬件上实现碰撞检测功能，但由于无线电波传播的特殊性（存在隐蔽站问题），还会出现无法检测到碰撞的情况，因此实现碰撞检测并没有意义。</li></ul><p>隐蔽站问题：</p><p><img src="https://files.seeusercontent.com/2026/02/17/dgU4/image-20260217160940077.png" alt="image-20260217160940077"></p><p>也就是主机A的信号范围内不可见主机B，主机B的信号范围不可见主机A，如果两边同时向AP发送数据就会发生信号碰撞。且碰撞信号强度较弱，即使传播到主机AB也难以检测到。所以不能使用碰撞检测CD，要使用碰撞避免CA协议。</p><h3 id="CSMA-x2F-CA协议"><a href="#CSMA-x2F-CA协议" class="headerlink" title="CSMA/CA协议"></a>CSMA/CA协议</h3><p>很多时候我们获取的原理不是知识，原理只占百分之十。原理只需要三十几个小时的网课，但是知识需要数万小时的调试。</p><h4 id="虚拟载波监听机制"><a href="#虚拟载波监听机制" class="headerlink" title="虚拟载波监听机制"></a>虚拟载波监听机制</h4><p><img src="https://files.seeusercontent.com/2026/02/20/dfP0/image-20260220163832118.png" alt="image-20260220163832118"></p><p>DIFS是<strong>DCF帧间间隔</strong>，长度为128us,在DCF方式中，DIFS用来发送数据帧和管理帧，<br>DCF是分布式协调功能(Distributed Coordination Function,DCF)的英文缩写词。在DCF方式下，没有中心控制站点，每个站点使用CSMA/CA协议通过争用信道来获取发送权。DCF方式是802.11定义的默认方式（必须实现）。</p><p>确认信道空闲之后等待DIFS在发送是<strong>考虑到可能其他站有更高优先级的帧需要发送。</strong></p><p>帧首部中的“持续时间”字段的值指出了源站要占用信道的时间（包括目的站发回确认帧所需的时间)。</p><p>短帧间间隔(Short Interframe Space,SIFS)的长度为28us,它是最短的帧间间隔，用来分隔开属于一次对话的各帧。一个站点应当能够在这段时间内从发送方式切换到接收方式。使用SIFS的帧类型有ACK帧、<strong>CTS帧</strong>等。</p><p>因为无线误码率高，所以CSMA/CA协议要配合停止-等待协议来确保可靠传输，与CSMA/CD协议的共享式以太网不同。</p><p>当某个站检测到正在信道中传送的帧首部中的“持续时间”字段时，就调整自己的网络分配向量(Network Allocation Vector,NAV)。NAV指出了完成这次帧的传送且信道转入空闲状态所需的时间。</p><p>随机等待时间的作用在于：<strong>当某个站在发送帧时，很可能有多个站都在监听信道并等待发送帧，一旦信道空闲，这些站几乎同时发送帧而产生碰撞。<br>为了避免上述情况，所有要发送帧的站检测到信道从忙转为空闲后，都要执行退避算法。这样不仅可以减少发生碰撞的概率，还可避免某个站长时间占用无线信道。</strong></p><p>只有在在<strong>检测到信道空闲且该数据帧不是成功发送完上一个数据帧之后立即连续发送的数据帧</strong>时，不使用退避算法。具体就是以下三种情况。</p><ul><li>在发送帧之前检测到信道处于忙态</li><li>在每一次重传一个帧时</li><li>在每一次成功发送帧后要连续发送下一个帧时</li></ul><h4 id="CSMA-x2F-CA协议的退避算法"><a href="#CSMA-x2F-CA协议的退避算法" class="headerlink" title="CSMA/CA协议的退避算法"></a>CSMA/CA协议的退避算法</h4><p>在执行退避算法时，站点为退避计时器设置一个随机的退避时间：</p><ul><li>当退避计时器的时间减小到零时，就开始发送数据</li><li>当退避计时器的时间还未减小到零时而信道又转变为忙状态，这时就冻结退避计时器的数值，重新等待信道变为空闲，再经过帧间间隔DIFS后，继续启动退避计时器。</li><li>在进行第i次退避时，退避时间在时隙编号中随机选择一个，然后乘以基本退避<br>时间（也就是一个时隙的长度）就可以得到随机的退避时间。当时隙编号达到255时（对应于第6次退避）就不再增加了。</li></ul><p><img src="https://files.seeusercontent.com/2026/02/20/7kmB/image-20260220165853983.png" alt="image-20260220165853983"></p><p>同时为了进一步降低发生碰撞的概率，802.11无线局域网允许源站对信道进行预约。</p><p>RTS(Request To Send)帧是短的控制帧，<br>它包括源地址、目的地址和本次通信（包括目的站发回确认帧所需的时间）所需的持续时间。</p><p>CTS (Clear To Send)帧是短的响应控制帧，<br>它也包括本次通信所需的持续时间（从RTS帧中将此持续时间复制到CTS帧中）。</p><p>使用RTS帧和CTS帧进行信道预约会带来额外开销，但是代价很小能带来稳定的低碰撞率，这是值得的。实际上用户可以选择三种模式：</p><ul><li>使用RTS帧和CTS帧。</li><li>只有当数据帧的长度超过某个数值时才使用RTS帧和CTS帧。</li><li>不使用RTS帧和CTS帧。</li></ul><p>信道预约也属于虚拟载波监听机制。</p><h3 id="无线局域网的MAC帧"><a href="#无线局域网的MAC帧" class="headerlink" title="无线局域网的MAC帧"></a>无线局域网的MAC帧</h3><p>MAC帧分为三种：</p><p><img src="https://files.seeusercontent.com/2026/02/20/yDx9/image-20260221001932333.png" alt="image-20260221001932333"></p><p>在这里我们只介绍数据帧，其格式为：</p><p><img src="https://files.seeusercontent.com/2026/03/26/ux2M/image-20260326201756873.png" alt="image-20260326201756873"></p><p>其中帧控制字段很复杂。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第三章：数据链路层&quot;&gt;&lt;a href=&quot;#第三章：数据链路层&quot; class=&quot;headerlink&quot; title=&quot;第三章：数据链路层&quot;&gt;&lt;/a&gt;第三章：数据链路层&lt;/h1&gt;&lt;h2 id=&quot;一些概念&quot;&gt;&lt;a href=&quot;#一些概念&quot; class=&quot;headerli</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第二章：物理层</title>
    <link href="https://blog.willmo.top/posts/Computer%20Networks%20Chapter%202:%20Physical%20Layer/"/>
    <id>https://blog.willmo.top/posts/Computer%20Networks%20Chapter%202:%20Physical%20Layer/</id>
    <published>2026-04-20T11:28:00.299Z</published>
    <updated>2026-04-20T11:28:01.280Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第二章：物理层"><a href="#第二章：物理层" class="headerlink" title="第二章：物理层"></a>第二章：物理层</h1><h2 id="物理层功能"><a href="#物理层功能" class="headerlink" title="物理层功能"></a>物理层功能</h2><p>物理层的功能就是要传输比特流。</p><p>物理层向数据链路层提供服务，且因为物理层的透明性，屏蔽掉了各种传输媒体和通讯手段的差异，使得链路层能够专注于层内的服务。</p><h2 id="物理层接口特性"><a href="#物理层接口特性" class="headerlink" title="物理层接口特性"></a>物理层接口特性</h2><ul><li>机械特性：形状特性，引脚数目，锁定装置等，参考RJ45型网口</li><li>电气特性：信号电压范围，阻抗情况，传输速率，距离限制</li><li>功能特性：规定各条信号线作用，即不同颜色的线的作用</li><li>过程特性：规定信号线上比特流的操作过程，包括信号之间的时序关系</li></ul><h2 id="传输媒体的分类"><a href="#传输媒体的分类" class="headerlink" title="传输媒体的分类"></a>传输媒体的分类</h2><p><strong>传输媒体</strong>是计算机网络设备之间的物理通路，也称为传输介质或传输媒介。<br>传输媒体并<strong>不</strong>包含在计算机网络体系结构中。</p><p><img src="https://s2.loli.net/2026/02/03/ploUnPvEO8MaTCV.png" alt="image-20260203114135731"></p><h3 id="导向性传输媒体"><a href="#导向性传输媒体" class="headerlink" title="导向性传输媒体"></a>导向性传输媒体</h3><h4 id="同轴电缆"><a href="#同轴电缆" class="headerlink" title="同轴电缆"></a>同轴电缆</h4><p><img src="https://s2.loli.net/2026/02/03/WQmhobSqRDY6Et2.png" alt="image-20260203114251599"></p><p>横切面是同心圆，所以才叫同轴电缆。</p><p>分为基带同轴电缆和宽带同轴电缆。</p><ul><li>基带：用于数字传输，在早期互联网中广泛使用</li><li>宽带：用于模拟传输，用于有线电视的入户线</li></ul><p>但是同轴电缆价格贵，布线不够灵活，不够方便。</p><h4 id="双绞线"><a href="#双绞线" class="headerlink" title="双绞线"></a>双绞线</h4><p><img src="https://s2.loli.net/2026/02/03/OYPErS4nygu6VGT.png" alt="image-20260203114932916"></p><p>双绞合结构能够抵御来自内部和外部的电磁干扰。</p><p>双绞线有质量级别，6A类能够满足万兆局域网的需求。</p><p><img src="https://s2.loli.net/2026/02/03/H5Mh8sL27RV6Kar.png" alt="image-20260203115011370"></p><h4 id="光纤通信"><a href="#光纤通信" class="headerlink" title="光纤通信"></a>光纤通信</h4><p><img src="https://s2.loli.net/2026/02/03/E8nwKfIHgpkrNUa.png" alt="image-20260203115156582"></p><p>光纤通信利用光脉冲在光纤中的传递来进行通信。由于可见光的频率非常高（约为10^8MHz量级），因此一个光纤通信系统的传输带宽远大于目前其他各种传输媒体的带宽。</p><p>光纤，即光导纤维，由石英管制成，利用光在其中发生全反射，信息随光不断传输下去。</p><p><img src="https://s2.loli.net/2026/02/03/LcKpFzswEj7oh5a.png" alt="image-20260203115318366"></p><p>光纤分为多模光纤和单模光纤</p><p><img src="https://s2.loli.net/2026/02/03/oqkmnahevBLyiUr.png" alt="image-20260203115551309"></p><p>单模光纤的制造成本较高，但是不会因为光的色散导致信号失真。</p><p>光纤所用的光波长是</p><ul><li>850nm 衰减大但是其他特性较好</li><li>1300nm 衰减小</li><li>1550nm 衰减小</li></ul><p>以上三种能达到25000~30000GHz的带宽，光纤的通信容量很大。</p><p>光纤很细，为了保证结构强度必须做成光缆。</p><p><img src="https://s2.loli.net/2026/02/03/aUYCjWL1FDAiguK.png" alt="image-20260203154709216"></p><h3 id="非导向性传输媒体"><a href="#非导向性传输媒体" class="headerlink" title="非导向性传输媒体"></a>非导向性传输媒体</h3><p>即无线通信，使用电磁波进行。</p><p>只能使用一定范围的电磁波来通信，其他波段往往穿墙能力弱，对生物有害，不适用。</p><p><img src="https://s2.loli.net/2026/02/03/WvthdPUJRwugpAZ.png" alt="image-20260203155614906"></p><h4 id="无线电波"><a href="#无线电波" class="headerlink" title="无线电波"></a>无线电波</h4><p>传播距离很远，在LF-MF波段，地面传播，但在HF-VHF波段，则地面波会被吸收，主要依靠在电离层反射来传播。</p><h4 id="微波通信"><a href="#微波通信" class="headerlink" title="微波通信"></a>微波通信</h4><p>在空间内直线传播，所以传播距离只有50km左右，用于接力通信。</p><p>微波能够穿过电离层，用于卫星通信</p><h4 id="红外线"><a href="#红外线" class="headerlink" title="红外线"></a>红外线</h4><p>点对点无线传输，传播距离短，穿墙能力弱，用于遥控家电设备</p><h4 id="激光"><a href="#激光" class="headerlink" title="激光"></a>激光</h4><p>分为光纤通信和大气激光通信，大气激光通信主要用于地面与卫星之间的通信。</p><h4 id="可见光"><a href="#可见光" class="headerlink" title="可见光"></a>可见光</h4><p>某种新兴方式，不写了。</p><h2 id="传输方式"><a href="#传输方式" class="headerlink" title="传输方式"></a>传输方式</h2><h3 id="串行传输和并行传输"><a href="#串行传输和并行传输" class="headerlink" title="串行传输和并行传输"></a>串行传输和并行传输</h3><p>串行：比特流在一条数据传输线路上传输</p><p>并行：比特率在N条数据传输线路上传输，速度是串行的N倍，但并行成本高还可能因为到达时延导致信息失真。</p><p>并行只用于短途传输，比如计算机内部件。</p><p><img src="https://s2.loli.net/2026/02/03/bQD1YSHGhmWlzqd.png" alt="image-20260203160801089"></p><h3 id="同步传输和异步传输"><a href="#同步传输和异步传输" class="headerlink" title="同步传输和异步传输"></a>同步传输和异步传输</h3><p>同步强制要求两方使用相同的时钟频率，同步方式详见计算机组成原理第二章总线结构。</p><p><img src="https://s2.loli.net/2026/02/03/9GyvrK2SfD6cmW8.png" alt="image-20260203160919381"></p><p>异步：每个数据单元（如一个字节）都带有自己的起始位和停止位，字节之间的时间间隔不固定，但是<strong>字节内每个比特持续时间</strong>必须是相同的，这是受双方约定的波特率约束的。</p><p><img src="https://s2.loli.net/2026/02/03/9RweIT8QhPxiaBL.png" alt="image-20260203161101491"></p><h4 id="单向通信，双向交替通信，双向同时通信"><a href="#单向通信，双向交替通信，双向同时通信" class="headerlink" title="单向通信，双向交替通信，双向同时通信"></a>单向通信，双向交替通信，双向同时通信</h4><p><img src="https://s2.loli.net/2026/02/03/E6qFWhfurw4975v.png" alt="image-20260203161215528"></p><h3 id="编码和调制"><a href="#编码和调制" class="headerlink" title="编码和调制"></a>编码和调制</h3><p>待补充，也许某天会回来看，也许不会</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第二章：物理层&quot;&gt;&lt;a href=&quot;#第二章：物理层&quot; class=&quot;headerlink&quot; title=&quot;第二章：物理层&quot;&gt;&lt;/a&gt;第二章：物理层&lt;/h1&gt;&lt;h2 id=&quot;物理层功能&quot;&gt;&lt;a href=&quot;#物理层功能&quot; class=&quot;headerlink&quot; ti</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>计算机网络第一章：概述</title>
    <link href="https://blog.willmo.top/posts/Chapter%201%20of%20Computer%20Networks:%20Overview/"/>
    <id>https://blog.willmo.top/posts/Chapter%201%20of%20Computer%20Networks:%20Overview/</id>
    <published>2026-04-20T11:18:23.296Z</published>
    <updated>2026-04-20T11:53:36.885Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第一章：计算机网络概述"><a href="#第一章：计算机网络概述" class="headerlink" title="第一章：计算机网络概述"></a>第一章：计算机网络概述</h1><h2 id="三种交换方式"><a href="#三种交换方式" class="headerlink" title="三种交换方式"></a>三种交换方式</h2><h3 id="电路交换"><a href="#电路交换" class="headerlink" title="电路交换"></a>电路交换</h3><p><img src="https://s2.loli.net/2026/01/31/bhx3IOtuWY9E5zo.png" alt="image-20260131205058223"></p><p>A发出通话请求，E接到响铃后接听开始通话，通话过程中将一直占用通讯线路，直到挂断线路才会被交换机释放给其他使用。</p><p>计算机之间的数据传送是突发式的，当使用<strong>电路交换</strong>来传送计算机数据时，其线路的传输效率一般都会很低，线路上真正用来传送数据的时间往往不到10%甚至1%。</p><h3 id="分组交换"><a href="#分组交换" class="headerlink" title="分组交换"></a>分组交换</h3><p><img src="https://s2.loli.net/2026/01/31/n1FXU3hSdGr8sNx.png" alt="image-20260131210058995"></p><p>计算机之间通信是交换一个二进制串，名为<strong>报文</strong>。报文有时候很长，往往超过了交换节点的缓存大小，所以就被设计为分组交换。发送机H1发送的报文被分为若干组，每个组回加一个首部来区分报文分组情况。接收机H3根据首部将报文组合成完整报文。</p><p>分组交换有以下优点：</p><ul><li>没有建立连接和释放连接的过程</li><li>分组传输过程中逐段占用通信链路，有较高的通信线路利用率。</li><li>交换节点可以为每一个分组独立选择转发路由，使得网络有很好的生存性。</li></ul><p>这使得分组交换适合用于计算机的数据交换。</p><p>但是也有缺点：</p><ul><li>分组首部会带来额外的传输开销</li><li>交换节点存储转发会带来一定的时延</li><li>通信量大时会造成网络堵塞</li></ul><h3 id="报文交换"><a href="#报文交换" class="headerlink" title="报文交换"></a>报文交换</h3><p>报文交换是分组的前身，在这里报文被整个发送而不是分组，所以在交换节点接收到完整报文才会转发到下一个节点。这样的交换方式对交换节点的缓存空间要求很高而且会造成很大的传输时延。</p><h3 id="横向对比"><a href="#横向对比" class="headerlink" title="横向对比"></a>横向对比</h3><p><img src="https://s2.loli.net/2026/01/31/rzFgHk4PnusiCJm.png" alt="image-20260131210929913"></p><p>如果需要连续传输大量数据，适用电路交换，然而计算机的传输都是突发式的。</p><p>报文交换和分组交换不需要建立连接，即不需要预先分配通信资源，能够提升链路利用率。</p><p>将报文构造成若干个更小的分组进行分组交换，比将整个报文进行报文交换的时延要小，并且还可以避免太长的报文长时间占用链路，有利于差错控制，同时具有更好的灵活性。</p><h2 id="计算机网络的性能指标"><a href="#计算机网络的性能指标" class="headerlink" title="计算机网络的性能指标"></a>计算机网络的性能指标</h2><h3 id="速率"><a href="#速率" class="headerlink" title="速率"></a>速率</h3><p>比特(bit,记为小写b)是计算机中数据量的基本单位，一个比特就是二进制数字中的一个1或0。<br>数据量的常用单位有字节(byte,记为大写B)、千字节(KB)、兆字节(MB)、吉字节(GB)以及太字节(TB)。</p><p><strong>注意：在这里的数据量的进率不是1024而是1000</strong></p><p><strong>速率：</strong>数据传输的速率，即每秒传输多少个比特，也被称为数据率或比特率。</p><p>在此基础上可以延伸为千比特/秒(kb/s)和兆比特/秒(Mb/s)等。</p><h3 id="带宽"><a href="#带宽" class="headerlink" title="带宽"></a>带宽</h3><p>带宽在计算机网络中用来表示网络的通信线路所能传送数据的能力，即在单位时间内从网<br>络中的某一点到另一点所能通过的最高数据率。（也就是最高速率）<br>单位：b/s(kb/s,Mb/s,Gb/s,Tb/s)</p><p>但是数据传送速率=min[主机接口速率，线路带宽，交换机或路由器的接口速率]</p><p>因为发送过程中存在因速率不同带来的等待问题，所以我们只能取最小者，<strong>类似于短板效应</strong>。</p><h3 id="吞吐量"><a href="#吞吐量" class="headerlink" title="吞吐量"></a>吞吐量</h3><p>吞吐量是指在单位时间内通过某个网络或接口的实际数据量。吞吐量常被用于对实际网络的测量，以便获知到底有多少数据量通过了网络。</p><p>吞吐量受网络宽带的限制。</p><p>吞吐量等于下载速率和上传速率的总和。</p><h3 id="时延"><a href="#时延" class="headerlink" title="时延"></a>时延</h3><p>时延是指数据从网络的一端传送到另一端所耗费的时间，也称为延迟或迟延。数据可由一个或多分组、甚至是一个比特构成。</p><p>时延分为发送时延，传播时延，排队时延等</p><p>值得一提的是，接收时延往往和发送时延同时进行，所以不单独计算。</p><p><img src="https://s2.loli.net/2026/02/02/R9HZYiQESshJuLt.png" alt="image-20260202111345454"></p><p>如果将每个分组发送后，等到该分组被完全接收再发送下一分组会导致在传输时延过程中发送方处于空窗期，造成效率低下。所以这里只要分组一的第一个字节到达路由器，路由器就能够接收并处理它，链路不再被分组一占用，发送方就能够将分组二推到链路上传输。</p><p><img src="https://s2.loli.net/2026/02/02/1DwS5s9GXhO8FYy.png" alt="image-20260202111605744"></p><p>例题：</p><p><img src="https://s2.loli.net/2026/02/02/2YiUVdQ18bDNEZj.png" alt="image-20260202160852845"></p><p>这就涉及到一个问题，时延是发送时延主导还是传播时延主导。</p><h3 id="时延带宽积"><a href="#时延带宽积" class="headerlink" title="时延带宽积"></a>时延带宽积</h3><p>时延带宽积是传播时延和带宽的乘积。</p><p>可将其视为一个管道，管道的长度为传播时延，管道半径为带宽。</p><p><img src="https://s2.loli.net/2026/02/02/uCA3spgLQxhJjVD.png" alt="image-20260202112107494"></p><p><img src="https://s2.loli.net/2026/02/02/TOvsUca35j6Lo8C.png" alt="image-20260202161255480"></p><p>所以，发送的第一个比特到达主机B时，说明计算机已经发送了<strong>时延带宽积</strong>个比特。</p><h3 id="往返时间"><a href="#往返时间" class="headerlink" title="往返时间"></a>往返时间</h3><p>往返时间(Round-Trip Time,RTT)是指从发送端发送数据分组开始，到发送端收到接收端发来的相应确认分组为止，总共耗费的时间。</p><p>你可以使用<code>ping www.bing.com</code>来测试你与Bing网站的RTT往返时间。</p><p><img src="https://s2.loli.net/2026/02/02/9dC3IVTRUSXvuOe.png" alt="image-20260202162143602"></p><h3 id="利用率"><a href="#利用率" class="headerlink" title="利用率"></a>利用率</h3><p>分为链路利用率和网络利用率。</p><ul><li>链路利用率是指某条链路有百分之几的时间是被利用的（即有数据通过）。完全空闲的链路的利用率为零。</li><li>网络利用率是指网络中所有链路的链路利用率的加权平均。</li></ul><p>根据排队论可知，当某链路的利用率增大时，该链路引起的时延就会迅速增加。网络也是如此，通信量增大会导致排队时延增大。</p><p>令D0表示网络空闲时的时延，D表示网络当前的时延，那么在理想的假定条件下，可用下式来表示D、D0和网络利用率U之间的关系。</p><p>$$<br>D=\frac{D_0}{1-U}<br>$$</p><p>所以，过高的利用率会使得时延急剧增大。</p><h3 id="丢包率"><a href="#丢包率" class="headerlink" title="丢包率"></a>丢包率</h3><p>丢包率是指在一定的时间范围内，传输过程中丢失的分组数量与总分组数量的比率。</p><p>可分为接口丢包率，网络丢包率，路径丢包率，节点丢包率，链路丢包率等。</p><p>分组丢失主要有以下两种情况：</p><ul><li>分组在传输过程中出现误码，被传输路径中的节点交换机（例如路由器）或目的主机检测出误码而丢弃。</li><li>节点交换机根据丢弃策略主动丢弃分组。</li></ul><p>丢包率能够反应网络拥塞情况，无拥塞时丢包率为0，轻度为1%-4%，重度为5%-15%。</p><h2 id="三种计算机网络体系结构"><a href="#三种计算机网络体系结构" class="headerlink" title="三种计算机网络体系结构"></a>三种计算机网络体系结构</h2><p>国际标准化组织成立了一个专业组织提出了一个标准化模型，名为OSI参考模型，将网络的标准有力地统一起来，这是法律上国际标准。</p><p>但是OSI提出之时因特网已经颇具规模，所以其使用的TCP/IP参考模型也是体系之一，作为事实标准存在。</p><p>但是能够占领市场的才是标准，OSI的背后没有商业驱动力，而且协议复杂，层次划分不大合理。</p><p><img src="https://s2.loli.net/2026/02/02/4fGZRBCsY2OVAah.png" alt="image-20260202164409860"></p><p>每个层之间相互协作。</p><p><img src="https://s2.loli.net/2026/02/02/hWlVnec9NmbdBPR.png" alt="image-20260202165400216"></p><p>计算机网络技术是一个很抽象且复杂的问题，所以分层解决各个层面的问题是很有必要的。考虑到OSI和TCP两个模型的冲突，我们各取一部分设计一个参考模型来讲解计算机网络技术，模型如下：</p><p><img src="https://s2.loli.net/2026/02/02/z1AbmcXBQF9pHN7.png" alt="image-20260202190700133"></p><h3 id="分层应用实例"><a href="#分层应用实例" class="headerlink" title="分层应用实例"></a>分层应用实例</h3><p><img src="https://s2.loli.net/2026/02/02/NR9wiO12jDovmZL.png" alt="image-20260202204630143">PC和服务器的通讯本质上是应用间基于网络的通信，这里是谷歌浏览器和Nginx应用的通信。</p><p>如上图，应用层浏览器生成HTTP请求报文并下放，运输层给HTTP请求报文加上TCP报文段，用于区分应用进程保证传输可靠性，下放，网络层将接受的TCP报文段添加一个IP首部，用于IP寻址和路由，下放，链路层将IP数据报添加一个首部和尾部，作用是MAC寻址和帧校验，就此封装称为帧。物理层不理解帧的结构，将帧视为比特流并转化为电信号通过网线转移到路由器（如果是以太网还需要加<strong>前导码</strong>首部来控制时钟信号）。</p><p>路由器接收到电信号后去掉前导码(如有)，转化为帧上交，链路层去掉首部和尾部，上交。网络层根据IP数据包的首部中的目标IP地址在内置转发表中查找，决定从哪个接口转发数据表，并对某些数值(如生存时间TTL)修改，再下放，链路层和物理层分别加首尾和转化为电信号发送到Web服务器。</p><p>Web服务器接收到后，自下而上的顺序逐层解开数据包，得到HTTP报文后做出处理，生成对应的相应报文，随后层层封装发送回个人PC。</p><p>我希望能够记住这个过程，以后的章节都会根据这个流程展开。</p><h2 id="计网专业术语"><a href="#计网专业术语" class="headerlink" title="计网专业术语"></a>计网专业术语</h2><p><strong>实体</strong>：指任何可发送或接收信息的硬件或软件进程。</p><p><strong>对等实体</strong>：通信双方相同层次中的实体。</p><p>对等实体之间使用相同的协议。</p><p><img src="https://s2.loli.net/2026/02/02/l5Da74V9evEW86J.png" alt="image-20260202215148766"></p><p><strong>协议</strong>：控制两个对等实体在“水平方向”进行“逻辑通信”的规则的集合。</p><p>协议有三个要素，语法，语义，同步。</p><ul><li>语法：定义交换信息的格式</li><li>语义：定义通信双方要完成的操作</li><li>同步：定义通信双方的时序关系</li></ul><p><img src="https://s2.loli.net/2026/02/02/dbweUT7HrWJMIfO.png" alt="image-20260202215213740"></p><p><strong>服务</strong>：在协议的控制下，两个对等实体在水平方向的逻辑通信使得本层能够向上一层提供服务。</p><ul><li>在同一系统中相邻两层的实体交换信息的逻辑接口称为服务访问点SAP,它被用于区分不同的服务类型。</li><li>帧的“类型”字段、IP数据报的“协议”字段，TCP报文段或UDP用户数据报的“端口号”字段都是SAP。</li></ul><p><img src="https://s2.loli.net/2026/02/02/4UPGdtcOnxJyzsE.png" alt="image-20260202215245279"></p><blockquote><p>最后给用户提供服务。</p></blockquote><p>实体看得见下层提供的服务，但并不知道实现该服务的具体协议。下层的协议对上层的实体是“透明”的。所谓透明，即玻璃，就是上层能透过它(协议)享受服务，但不会注意到玻璃(协议)本身的存在。</p><p>上层要使用下层所提供的服务，必须通过与下层交换一些命令，这些命令称为<strong>服务原语</strong>。</p><p>对等层次之间传送的数据包称为该层的<strong>协议数据单元</strong>(Protocol Data Unit,PDU)<br>同一系统内层与层之间交换的数据包称为<strong>服务数据单元</strong>(Service Data Unit,SDU)。</p><p><img src="https://s2.loli.net/2026/02/02/GAHfsdqQ5xRcN3B.png" alt="image-20260202220108746"></p><p>如果一时间没办法理解，没关系，放过去，这里只是概论课而已。</p><p>第一章：概论结束。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第一章：计算机网络概述&quot;&gt;&lt;a href=&quot;#第一章：计算机网络概述&quot; class=&quot;headerlink&quot; title=&quot;第一章：计算机网络概述&quot;&gt;&lt;/a&gt;第一章：计算机网络概述&lt;/h1&gt;&lt;h2 id=&quot;三种交换方式&quot;&gt;&lt;a href=&quot;#三种交换方式&quot; cla</summary>
      
    
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
    
    <category term="计算机网络" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>操作系统第五章：IO设备</title>
    <link href="https://blog.willmo.top/posts/Operating%20System%20Chapter%205:%20IO%20Devices/"/>
    <id>https://blog.willmo.top/posts/Operating%20System%20Chapter%205:%20IO%20Devices/</id>
    <published>2026-04-20T07:17:37.517Z</published>
    <updated>2026-04-20T07:17:38.698Z</updated>
    
    <content type="html"><![CDATA[<h1 id="操作系统：第五章"><a href="#操作系统：第五章" class="headerlink" title="操作系统：第五章"></a>操作系统：第五章</h1><blockquote><p>这是最后一章了，这和计算机组成原理比较相似。</p></blockquote><h3 id="IO设备基本概念"><a href="#IO设备基本概念" class="headerlink" title="IO设备基本概念"></a>IO设备基本概念</h3><p>操作系统作为系统资源的管理者，也需要对外部设备进行管理。</p><p>“I/O”就是“输入/输出”(Input/,Output)<br>I/O设备就是可以将数据输入到计算机，或者可以接收计算机输出数据的外部设备，属于计算机中的硬件部件。</p><p>UNIX系统将外部设备抽象为一种特殊的文件，用户可以使用与文件操作相同的方式对外部设备进行操作。</p><p>按使用特性分类可以分为</p><ul><li>人机交互类外部设备</li><li>存储设备</li><li>网络通信设备</li></ul><p>按传输速率分类</p><ul><li>低速设备</li><li>中速设备</li><li>高速设备</li></ul><p>按信息交换单位</p><ul><li>块设备</li><li>字符设备</li></ul><h3 id="IO控制器"><a href="#IO控制器" class="headerlink" title="IO控制器"></a>IO控制器</h3><p>I/O设备的机械部件主要用来执行具体I/O操作。<br>如我们看得见摸得着的鼠标/键盘的按钮；显示器的LED屏；移动硬盘的磁臂、磁盘盘面。<br>I/O设备的电子部件通常是一块插入主板扩充槽的印刷电路板。</p><p>但是CPU无法直接控制I/O设备的机械部件，因此I/O设备还要有一个电子部件作为CPU和I/O设备机械部件之间的“中介”，用于实现CPU对设备的控制，安装在主板上。<br>这个电子部件就是I/O控制器，又称设备控制器。CPU可控制I/O控制器，又由I/O控制器来控制设备的机械件。</p><p>IO控制器的功能是</p><ul><li>接收和识别CPU发出的命令</li><li><ul><li>如CPU发来的read/write命令，lI/O控制器中会有相应的控制寄存器来存放命令和参数。</li></ul></li><li>向CPU报告设备状态并中断</li><li><ul><li>IO控制器中会有相应的状态控制器，用于记录IO设备的当前状态。</li></ul></li><li>数据交换</li><li><ul><li>IO控制器中会设置相应的数据寄存器。输出时，数据寄存器用于暂存CU发来的数据，之后再由控制器传送设备。输入时，数据寄存器用于暂存设备发来的数据，之后CPU从数据寄存器中取走数据</li></ul></li><li>地址识别</li><li><ul><li>类似于内存的地址，为了区分设备控制器中的各个寄存器，也需要给各个寄存器设置一个特定的“地址”。I/O控制器通过CPU提供的“地址”来判断CPU要读/写的是哪个寄存器</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/04/yK3a/image-20260404211133406.png" alt="image-20260404211133406"></p><p>IO控制器的组成部分是：</p><ul><li>控制器与设备的接口</li><li><ul><li>控制器与设备的接口用于实现控制器与设备之间的通信</li></ul></li><li>IO逻辑</li><li><ul><li>I/O逻辑负责接收和识别CPU的各种命令（如地址译码)，并负责对设备发出命令。</li></ul></li><li>CPU与控制器接口</li><li><ul><li>用于实现CPU与控制器之间的通信。CPU通过控制线发出命令：通过地址线指明要操作的设备：通过数据线来取出（输入)数据，或放入（输出）数据</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/04/mRt5/image-20260404211335635.png"></p><p>值得注意的小细节：</p><p>①一个I/O控制器可能会对应多个设备：<br>②数据寄存器、控制寄存器、状态寄存器可能有多个（如：每个控制/状态寄存器对应一个具体的设备)，且这些寄存器都要有相应的地址，才能方便CPU操作。有的计算机会让这些寄存器占用内存地址的一部分，称为内存映像I/O:另一些计算机则采用I/O专用地址，即寄存器独立编址。</p><h4 id="内存映像和寄存器独立编制"><a href="#内存映像和寄存器独立编制" class="headerlink" title="内存映像和寄存器独立编制"></a>内存映像和寄存器独立编制</h4><p><img src="https://files.seeusercontent.com/2026/04/04/4Kne/image-20260404211600976.png" alt="image-20260404211600976"></p><h3 id="IO控制方式"><a href="#IO控制方式" class="headerlink" title="IO控制方式"></a>IO控制方式</h3><blockquote><p>这里和计算机组成原理可能比较像</p></blockquote><p>我们需要注意5点：</p><ul><li>完成一次读写的流程</li><li>CPU干预的频率</li><li>数据传送的单位</li><li>数据的流向</li><li>主要优点和缺点</li></ul><h4 id="程序直接控制方式"><a href="#程序直接控制方式" class="headerlink" title="程序直接控制方式"></a>程序直接控制方式</h4><p>轮询：CPU发出读命令后不断读取状态寄存器，如果状态为已就绪，就将数据寄存器的内容转移到CPU寄存器，再从CPU寄存器转存到内存当中。</p><p><img src="https://files.seeusercontent.com/2026/04/05/f4Ko/image-20260405103505338.png" alt="image-20260405103505338"></p><p>这种方式称为“轮询”。</p><p><img src="https://files.seeusercontent.com/2026/04/05/lyX9/image-20260405103804492.png" alt="image-20260405103804492"></p><p>CPU干预频率：极度频繁。</p><p>IO操作开始之前、完成之后需要CPU介入，并且在等待IO完成的过程中CPU需要不断地轮询检查。</p><p>数据传送的单位：每次读/写一个字</p><p>数据流向：</p><p>读操作（数据输入）：IO设备→CPU→内存<br>写操作（数据输出）：内存→CPU→IO设备<br>每个字的读/写都需要CPU的帮助。</p><p>主要缺点和主要优点<br>优点：实现简单。在读/写指令之后，加上实现循环检查的一系列指令即可（因此才称为“程序直接控制方式”）<br>缺点：CPU和I/O设备只能串行工作，CPU需要一直轮询检查，长期处于“忙等”状态，CPU利用率低。</p><h4 id="中断驱动方式"><a href="#中断驱动方式" class="headerlink" title="中断驱动方式"></a>中断驱动方式</h4><p>引入中断机制。由于I/O设备速度很慢，因此在CPU发出读/写命令后，可将等待I/O的进程阻塞，先切换到别的进程执行。当I/O完成后，控制器会向CPU发出一个中断信号，CPU检测到中断信号后，会保存当前进程的运行环境信息，转去执行中断处理程序处理该中断。处理中断的过程中，CPU从I/O控制器读一个字的数据传送到cPU寄存器，再写入主存。接着，CPU恢复等待I/O的进程（或其他进程）的运行环境，然后继续执行。</p><p><img src="https://files.seeusercontent.com/2026/04/05/9qwS/image-20260405105059859.png" alt="image-20260405105059859"></p><p>①CPU会在每个指令周期的末尾检查中断：<br>②中断处理过程中需要保存、恢复进程的运行环境，这个过程是需要一定时间开销的。可见，如果中断发生的频率太高，也会降低系统性能。</p><p>CPU干预频率：较低</p><p>每次Io操作开始之前、完成之后需要CPU介入。<br>等待I/O完成的过程中CPU可以切换到别的进程执行。</p><p>数据传输单位：每次一个字</p><p>数据流向：</p><p>读操作（数据输入）：I/O设备→CPU→内存<br>写操作（数据输出）：内存→CPU→I/O设备</p><p>优点：与“程序直接控制方式”相比，在“中断驱动方式”中，I/O控制器会通过中断信号主动报告I/O己完成，CU不再需要不停地轮询。 CPU和I/O设备可并行工作，CPU利用率得到明显提升。</p><p>缺点：每个字在I/O设备与内存之间的传输，都需要经过CPU。而频繁的中断处理会消耗较多的CPU时间。</p><h4 id="DMA方式"><a href="#DMA方式" class="headerlink" title="DMA方式"></a>DMA方式</h4><p>与“中断驱动方式”相比，DMA方式(Direct Memory Access,直接存储器存取。主要用于块设备的<br>I/O控制)有这样几个改进：<br>①数据的传送单位是“块”。不再是一个字、一个字的传送：<br>②数据的流向是从设备直接放入内存，或者从内存直接到设备。不再需要CPU作为“中间商”。<br>③仅在传送一个或多个数据块的开始和结束时，才需要CPU干预。</p><p><img src="https://files.seeusercontent.com/2026/04/05/J9bq/image-20260405105805164.png" alt="image-20260405105805164"></p><p>CPU给IO模块发出读命令，随后做其他事。DMA控制器会根据CPU命令参数来完成读写工作，完成后向CPU发送中断信号，CPU再来处理这个中断。</p><p><img src="https://files.seeusercontent.com/2026/04/05/ay2Z/image-20260405110012696.png" alt="image-20260405110012696"></p><h4 id="DMA控制器"><a href="#DMA控制器" class="headerlink" title="DMA控制器"></a>DMA控制器</h4><p><img src="https://files.seeusercontent.com/2026/04/05/4lrX/image-20260405115957112.png" alt="image-20260405115957112"></p><p>DR(Data Register,数据寄存器)：暂存从设备到内存，或从内存到设备的数据。<br>MAR(Memory Address Register,内存地址寄存器)：在输入时，MAR表示数据应放到内存中的什么位置；输出时MAR表示要输出的数据放在内存中的什么位置。<br>DC(Data Counter,.数据计数器)：表示剩余要读/写的字节数/块数。<br>CR(Command Register,.命令/状态寄存器)：用于存放CPU发来的lI/O命令，或设备的状态信息。</p><p>CPU干预频率：极低，仅在传传输一个或者多个数据库的开始和结束时，才需要CPU介入。</p><p>数据传送的单位每次读/写一个或多个块（注意：每次读写的只能是连续的多个块，且这些块读入内存后在内存中也必须是连续的)</p><p>4.数据的流向（不再需要经过CPU)<br>读操作（数据输入）：IO设备→内存<br>写操作（数据输出）：内存→Io设备</p><p>5.主要缺点和主要优点<br>优点：数据传输以“块”为单位，CPU介入频率进一步降低。数据的传输不再需要先经过CPU再写入内存，数据传输效率进一步增加。CPU和IO设备的并行性得到提升。<br>缺点：CPU每发出一条I/O指令，只能读/写一个或多个连续的数据块。</p><p>如果要读/写多个离散存储的数据块，戍者要将数据分别写到不同的内存区域时，CPU要分别发出多条IO指令，进行多次中断处理才能完成。</p><h4 id="通道控制方式"><a href="#通道控制方式" class="headerlink" title="通道控制方式"></a>通道控制方式</h4><p>通道：一种硬件，可以理解为是“性能更低的CPU”。通道可以识别并执行一系列通道指令。</p><p>通道相对于DMA控制器更智能，但是对于CPU，可以执行的指令很单一，而且通道程序是放在主机内存中的，也就是说通道和CPU一起共享内存。</p><p><img src="https://files.seeusercontent.com/2026/04/05/bQ9z/image-20260405120712341.png" alt="image-20260405120712341"></p><p>CPU的干预频率极低，通道会根据CPU的指示执行相应的通道程序，只有完成一组数据块的读/写后才需要发出中断信号，请求CPU干预。</p><p>数据传输单位：每次读写一组数据块</p><p>数据的流向：</p><p>读操作（数据输入）：I/O设备→内存<br>写操作（数据输出）：内存→I/O设备</p><p>缺点：实现复杂，需要专门的通道硬件支持<br>优点：CPU、通道、I/O设备可并行工作，资源利用率很高。</p><p><img src="https://files.seeusercontent.com/2026/04/05/chU4/image-20260405121430993.png" alt="image-20260405121430993"></p><h3 id="IO软件层次结构"><a href="#IO软件层次结构" class="headerlink" title="IO软件层次结构"></a>IO软件层次结构</h3><p><img src="https://files.seeusercontent.com/2026/04/05/2Dox/image-20260405121808394.png" alt="image-20260405121808394"></p><p>每一层会利用其下层提供的服务，实现某些功能，并屏蔽实现的具体细节，向高层提供服务(“封装思想”)</p><p>我们来具体看一下每个层的实例。</p><h4 id="用户层软件"><a href="#用户层软件" class="headerlink" title="用户层软件"></a>用户层软件</h4><p><img src="https://files.seeusercontent.com/2026/04/05/sK1y/image-20260405122112110.png" alt="image-20260405122112110"></p><h4 id="设备独立性软件"><a href="#设备独立性软件" class="headerlink" title="设备独立性软件"></a>设备独立性软件</h4><p>设备独立性软件，又称设备无关性软件。与设备的硬件特性无关的功能几乎都在这一层实现。</p><ul><li>向上层提供统一的调用接口(R/W)</li><li>设备的保护(访问控制等)</li><li>差错处理</li><li>设备的分配与回收(设备是临界资源)</li><li>数据缓冲区管理(可以通过缓冲技术屏蔽设备之间数据交换单位大小和传输速度的差异)</li><li>建立<strong>逻辑设备名</strong>到<strong>物理设备名</strong>的映射关系；根据设备类型选择调用相应的驱动程序</li></ul><p>详细解释一下逻辑设备名和物理设备名</p><p><img src="https://files.seeusercontent.com/2026/04/05/rx6A/image-20260405122955932.png" alt="image-20260405122955932"></p><p>逻辑设备名是给用户看的，比如打印机1，打印机2等</p><p>物理设备名 就如物理地址，是给操作系统看的。所以建立二者的映射关系是必要的。</p><p>操作系统系统可以采用两种方式管理逻辑设备表(LUT):<br>第一种方式，整个系统只设置一张LUT,这就意味着所有用户不能使用相同的逻辑设备名，因此这种方式只适用于单用户操作系统。<br>第二种方式，为每个用户设置一张LUT,各个用户使用的逻辑设备名可以重复，适用于多用户操作系统。系统会在用户登录时为其建立一个用户管理进程，而LUT就存放在用户管理进程的PCB中。适用于多用户系统。</p><p>不同厂家的外设的内部电子结构是不同的，所以需要不同的驱动程序。这个驱动程序是由厂家编写的，且一般以独立进程的方式存在。</p><h4 id="中断处理程序"><a href="#中断处理程序" class="headerlink" title="中断处理程序"></a>中断处理程序</h4><p>当I/O任务完成时，I/O控制器会发送一个中断信号，系统会根<br>据中断信号类型找到相应的中断处理程序并执行。</p><p>中断处理程序的处理流程如下：</p><p><img src="https://files.seeusercontent.com/2026/04/05/Dkm3/image-20260405124353300.png" alt="image-20260405124353300"></p><p>所以中断处理程序也需要直接和硬件接触。</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p><img src="https://files.seeusercontent.com/2026/04/05/sg6S/image-20260405124453075.png" alt="image-20260405124453075"></p><h3 id="输入输出应用程序接口"><a href="#输入输出应用程序接口" class="headerlink" title="输入输出应用程序接口"></a>输入输出应用程序接口</h3><p>不同的设备具有不同的特性，不能用统一的系统调用接口来完成所有类型设备的IO需求，所以操作系统必须向上层提供多种IO应用程序接口。</p><p><img src="https://files.seeusercontent.com/2026/04/05/qT8b/image-20260405132827087.png" alt="image-20260405132827087"></p><p>字符设备和块设备我们已经足够熟悉了，重点是网络设备。</p><p>网络设备的通信是通过socket协议进行的。</p><p><img src="https://files.seeusercontent.com/2026/04/05/V3xz/image-20260405132941635.png" alt="image-20260405132941635"></p><h4 id="阻塞IO和非阻塞IO"><a href="#阻塞IO和非阻塞IO" class="headerlink" title="阻塞IO和非阻塞IO"></a>阻塞IO和非阻塞IO</h4><p>阻塞I/O:应用程序发出I/O系统调，进程需转为阻塞态等待。<br>eg:字符设备接口一一从键盘读一个字符get<br>非阻塞I/O:应用程序发出I/O系统调用，系统调用可迅速返回，进程无需阻塞等待。<br>eg:块设备接口一一往磁盘写数据write</p><h4 id="设备驱动程序接口"><a href="#设备驱动程序接口" class="headerlink" title="设备驱动程序接口"></a>设备驱动程序接口</h4><p>若各公司开发的设备驱动程序接口不统一，则操作系统很难调用设备驱动程序。</p><p><img src="https://files.seeusercontent.com/2026/04/05/ne9U/image-20260405133607725.png" alt="image-20260405133607725"></p><p>所以需要统一标准的设备驱动程序接口。</p><p><img src="https://files.seeusercontent.com/2026/04/05/iD9h/image-20260405133640923.png" alt="image-20260405133640923"></p><p>厂商必须根据操作系统规定的接口来编写驱动程序。</p><h3 id="假脱机技术"><a href="#假脱机技术" class="headerlink" title="假脱机技术"></a>假脱机技术</h3><p><img src="https://files.seeusercontent.com/2026/04/05/7mkI/image-20260405143114406.png" alt="image-20260405143114406"></p><p>脱机技术是早期计算机系统中使用的一种I/O操作方式。它的核心思想是让输入/输出操作在<strong>脱离主机（CPU）控制</strong>的情况下，由专门的卫星机或外围机独立完成。</p><p>“假脱机技术”，又称“SPOOLing技术”是用软件的方式模拟脱机技术。</p><p>SPOOLing系统组成如下：</p><p><img src="https://files.seeusercontent.com/2026/04/05/hGj1/image-20260405143316346.png" alt="image-20260405143316346"></p><p>在磁盘上开辟出两个存储区域一一“输入井”和“输出井”</p><p>“输入井”模拟脱机输入时的磁带，用于收容/0设备输入的数据</p><p>输出井”模拟脱机输出时的磁带，用于收容用户进程输出的数据</p><p>输入进程模拟脱机输入时的外围控制机</p><p>输出进程模拟脱机输出时的外围控制机</p><p>要实现SPOOLing技术，就需要多个进程支持。</p><p>在输入进程的控制下，“输入缓冲区”用于暂存从输入设备输入的数据，之后再转存到输入井中</p><p>在输出进程的控制下，“输出缓冲区”用于暂存从输出井送来的数据，之后再传送到输出设备上。</p><p>看一个共享打印机的应用。</p><p>独占式设备一只允许各个进程串行使用的设备。一段时间内只能满足一个进程的请求。</p><p>共享设备一一允许多个进程“同时”使用的设备（宏观上同时使用，微观上可能是交替使<br>用)。可以同时满足多个进程的使用请求。</p><p>打印机是独占式设备，但是可以用SPOOLing技术改造成共享设备。</p><p><img src="https://files.seeusercontent.com/2026/04/05/bk2C/image-20260405144605901.png" alt="image-20260405144605901"></p><p>当多个用户进程提出输出打印的请求时，系统会答应它们的请求，但是并不是真正把打印机分配给他们而是由假脱机管理进程为每个进程做两件事：</p><ul><li>在磁盘输出井中为进程申请一个空闲缓冲区（也就是说，这个缓冲区是在磁盘上的)，并将要打印的数据送入其中：</li><li>为用户进程申请一张空白的打印请求表，并将用户的打印请求填入表中（其实就是用来说明用户的打印数据存放位置等信息的)，再将该表挂到假脱机文件队列上。</li></ul><p>当打印机空闲时，输出进程会从文件队列的队头取出一张打印请求表，并根据表中的要求将要打印的据从输出井传送到输出缓冲区，再输出到打印机进行打印。用这种方式可依次处理完全部的打印任务</p><p>虽然系统中只有一个台打印机，但每个进程提出打印请求时，系统都会为在输出井中为其分配一个存储区（相当于分配了一个逻辑设备），使每个用户进程都觉得自己在独占一台打印机，从而实现对打印机的共享。</p><p>SPOOLing技术可以把一台物理设备虚拟成逻辑上的多台设备，可将独占式设备改造成共享设备。</p><p>未采取假脱机技术，用户进程发现打印机处于忙碌，只能进入阻塞态等待，这会导致时间的浪费。但是引入假脱机技术，用户进程就能够将需要打印的文件交付给输出缓冲区后继续去执行其他的事情。</p><p>这就是打印机和打印店的区别，后者有店员来接收并处理进程提交的打印请求。</p><p>假脱机技术通过<strong>引入磁盘队列作为中间层</strong>，实现了<strong>用户进程与慢速物理设备的“时间解耦”</strong>。用户进程只与高速的磁盘交换数据，从而从对慢速设备的等待中彻底脱离出来，极大地提高了进程的响应速度和整个系统的吞吐率。</p><h3 id="设备的分配与回收"><a href="#设备的分配与回收" class="headerlink" title="设备的分配与回收"></a>设备的分配与回收</h3><h4 id="设备分配时考虑的因素"><a href="#设备分配时考虑的因素" class="headerlink" title="设备分配时考虑的因素"></a>设备分配时考虑的因素</h4><ul><li>设备固有属性</li><li><ul><li>独占设备，共享设备和虚拟设备</li></ul></li><li>设备分配算法</li><li><ul><li>先来先服务等算法</li></ul></li><li>设备分配中的安全性</li><li><ul><li>安全分配方式</li></ul></li></ul><p>安全分配方式：为进程分配一个设备后就将进程阻塞，本次I/O完成后才将进程唤醒。(eg:考虑进程请求打印机打印输出的例子)</p><p>一个时段内每个进程只能使用一个设备<br>优点：破坏了“请求和保持”条件，不会死锁<br>缺点：对于一个进程来说，CPU和I/O设备只能串行工作。</p><ul><li><ul><li>不安全分配方式</li></ul></li></ul><p>不安全分配方式：进程发出I/O请求后，系统为其分配I/O设备，进程可继续执行，之后还可以发出新的I/O请求。只有某个I/O请求得不到满足时才将进程阻塞。</p><p>一个进程可以同时使用多个设备<br>优点：进程的计算任务和I/O任务可以并行处理，使进程迅速推进。<br>缺点：有可能发生死锁（死锁避免、死锁的检测和解除）。</p><p>静态分配：进程运行前为其分配全部所需资源，运行结束后归还资源。</p><p>动态分配方式：进程运行过程中动态申请设备资源</p><h4 id="设备分配管理的数据结构"><a href="#设备分配管理的数据结构" class="headerlink" title="设备分配管理的数据结构"></a>设备分配管理的数据结构</h4><p><img src="https://files.seeusercontent.com/2026/04/05/heB9/image-20260405151051178.png" alt="image-20260405151051178"></p><p>一个通道可控制多个控制器，每个控制器可控制多个设备。</p><p>设备控制表(DCT)：系统为每个设备配置一张DCT,用于记录设备情况。</p><p><img src="https://files.seeusercontent.com/2026/04/05/2zvS/image-20260405151700409.png" alt="image-20260405151700409"></p><p>注：“进程管理”章节中曾经提到过“系统会根据阻塞原因不同，将进程PCB挂到不同的阻塞队列中”</p><p>控制器控制表(COCT)：每个设备控制器都会对应一张COCT。操作系统根据COCT的信息对控制器进行操作和管理。</p><p><img src="https://files.seeusercontent.com/2026/04/05/2dGg/image-20260405152040878.png" alt="image-20260405152040878"></p><p>通道控制表(CHCT):每个通道都会对应一张CHCT，根据CHCT对通道进行操作和管理。</p><p><img src="https://files.seeusercontent.com/2026/04/05/3Aqo/image-20260405152223765.png" alt="image-20260405152223765"></p><p>系统设备表(SDT)：记录了系统中全部设备的情况，每个设备对应一个表目。</p><p><img src="https://files.seeusercontent.com/2026/04/05/Fqe1/image-20260405152325200.png" alt="image-20260405152325200"></p><h4 id="设备分配的步骤"><a href="#设备分配的步骤" class="headerlink" title="设备分配的步骤"></a>设备分配的步骤</h4><ul><li>根据进程请求的物理设备名查找SDT(注：物理设备名是进程请求分配设备时提供的参数)</li><li>根据SDT找到DCT,若设备忙碌则将进程PCB挂到设备等待队列中，不忙碌则将设备分配给进程。</li><li>根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中，不忙碌则将控制器分配<br>给进程。</li><li>根据COcT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中，不忙碌则将通道分配给进程。</li></ul><p>缺点很明显：<br>①用户编程时必须使用“物理设备名”，底层细节对用户不透明，不方便编程<br>②若换了一个物理设备，则程序无法运行<br>③若进程请求的物理设备正在忙碌，则即使系统中还有同类型的设备，进程也必须阻塞等待。</p><p>改进方法：<strong>建立逻辑设备名与物理设备名的映射机制，用户编程时只需提供逻辑设备名。</strong></p><p><strong>改进后的步骤如下：</strong></p><ul><li>根据进程请求的逻辑设备名查找SDT(注：用户编程时提供的逻辑设备名其实就是“设备类型”)</li><li>查找SDT,找到用户进程指定类型的、并且空闲的设备，将其分配给该进程。操作系统在逻辑设<br>备表(LUT)中新增一个表项。</li><li>根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中，不忙碌则将控制器分配<br>给进程。</li><li>根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中，不忙碌则将通道分配给进程。</li></ul><h4 id="缓冲区管理"><a href="#缓冲区管理" class="headerlink" title="缓冲区管理"></a>缓冲区管理</h4><p>缓冲区是一个存储区域，可以由专门的硬件寄存器组成，也可利用内存作为缓冲区。</p><p>使用硬件作为缓冲区的成本较高，容量也较小，一般仅用在对速度要求非常高的场合（如存储器管理中所用的联想寄存器，由于对页表的访问频率极高，因此使用速度很快的联想寄存器来存放页表项的副本)</p><p>一般情况下，更多的是利用内存作为缓冲区，“设备独立性软件”的缓冲区管理就是要组织管理好这些缓冲区。</p><p><img src="https://files.seeusercontent.com/2026/04/05/4Dzg/image-20260405153855113.png" alt="image-20260405153855113"></p><p>使用缓冲区能够解决高速设备和低速设备之间的矛盾。</p><h4 id="单缓冲"><a href="#单缓冲" class="headerlink" title="单缓冲"></a>单缓冲</h4><p>假设某用户进程请求某种块设备读入若干块的数据。若采用单缓冲的策略，操作系统会在主存中为其分配一个缓冲区（若题目中没有特别说明，一个缓冲区的大小就是一个块）。</p><p>注意：当缓冲区数据非空时，不能往缓冲区充入数据，只能从缓冲区把数据传出；当缓冲区为空时，可以往缓冲区充入数据，但必须把缓冲区充满以后，才能从缓冲区把数据传出。</p><p><img src="https://files.seeusercontent.com/2026/04/06/Dt7c/image-20260406121605608.png" alt="image-20260406121605608"></p><p>第二种情况：</p><p><img src="https://files.seeusercontent.com/2026/04/06/sa7S/image-20260406121934068.png" alt="image-20260406121934068"></p><h4 id="双缓冲"><a href="#双缓冲" class="headerlink" title="双缓冲"></a>双缓冲</h4><p>假设某用户进程请求某种块设备读入若干块的数据。若采用双缓冲的策略，操作系统会在主存中为其分配两个缓冲区（若题目中没有特别说明，一个缓冲区的大小就是一个块）<br>双缓冲题目中，假设初始状态为：工作区空，其中一个缓冲区满，另一个缓冲区空<br>假设T&gt;C+M</p><p><img src="https://files.seeusercontent.com/2026/04/06/O0vq/image-20260406122347313.png" alt="image-20260406122347313"></p><p>第二种情况：</p><p><img src="https://files.seeusercontent.com/2026/04/06/ySp3/image-20260406122641637.png" alt="image-20260406122641637"></p><p>采用双缓冲策略，处理一个数据块的平均耗时为Max(T,C+M)。</p><h4 id="使用单-x2F-双缓冲在通信时的区别"><a href="#使用单-x2F-双缓冲在通信时的区别" class="headerlink" title="使用单/双缓冲在通信时的区别"></a>使用单/双缓冲在通信时的区别</h4><p><img src="https://files.seeusercontent.com/2026/04/06/g8dO/image-20260406122907422.png" alt="image-20260406122907422"></p><p>显然，若两个相互通信的机器只设置单缓冲区，在任一时刻只能实现数据的单向传输。</p><p><img src="https://files.seeusercontent.com/2026/04/06/a9Fx/image-20260406123000752.png" alt="image-20260406123000752"></p><p>若两个相互通信的机器设置双缓冲区，则同一时刻可以实现双向的数据传输。</p><h4 id="循环缓冲区"><a href="#循环缓冲区" class="headerlink" title="循环缓冲区"></a>循环缓冲区</h4><p>将多个大小相等的缓冲区连接成一个循环队列。</p><p>注：以下图示中，橙色表示己充满数据的缓冲区，绿色表示空缓冲区。</p><p><img src="https://files.seeusercontent.com/2026/04/06/mF3r/image-20260406123127321.png" alt="image-20260406123127321"></p><h4 id="缓冲池"><a href="#缓冲池" class="headerlink" title="缓冲池"></a>缓冲池</h4><p>缓冲池由系统中共用的缓冲区组成。这些缓冲区按使用状况可以分为：空缓冲队列、装满输入数据的缓冲队列（输入队列）、装满输出数据的缓冲队列（输出队列）。</p><p>另外，根据一个缓冲区在实际运算中扮演的功能不同，又设置了四种工作缓冲区：用于收容输入数据的工作缓冲区(hin)、用于提取输入数据的工作缓冲区(sin)、用于收容输出数据的工作缓冲区(hout)、用于提取输出数据的工作缓冲区(sout)</p><p><img src="https://files.seeusercontent.com/2026/04/06/sXh4/image-20260406123254670.png" alt="image-20260406123254670"></p><p><img src="https://files.seeusercontent.com/2026/04/06/vt0C/image-20260406123312856.png" alt="image-20260406123312856"></p><p>分情况使用方法大概如下：</p><ul><li>输入进程请求输入数据，取下一块空缓冲区，放入hin充满之后挂到输入队列的队尾。</li><li>计算进程想要取得一块输入数据，取下一个输入队列缓冲区放入sin，被取空后放回到空缓冲队列的队尾。</li><li>计算进程需要将准备好的数据充入缓冲区，取下一个空缓冲区放入hout，充满后挂到输出队列的队尾</li><li>输出进程请求输出数据时，取下一块缓冲区放在sout，输出完毕后为空，挂回到空缓冲队列的队尾。</li></ul><h3 id="磁盘"><a href="#磁盘" class="headerlink" title="磁盘"></a>磁盘</h3><p>又名机械硬盘，在<a href="https://blog.willmo.top/posts/chapter_7_i/o_systems/#%E6%9C%BA%E6%A2%B0%E7%A1%AC%E7%9B%98%EF%BC%88%E7%A3%81%E7%9B%98%EF%BC%89">计算机组成原理</a>中已经有写到这一小节的大部分内容。</p><p>在此对重复的部分仅仅贴出PPT，而对计算机组成原理未涉及的部分将展开讲解。</p><p><img src="https://files.seeusercontent.com/2026/04/06/2Vpq/image-20260406124731760.png" alt="image-20260406124731760"></p><p><img src="https://files.seeusercontent.com/2026/04/06/e1Up/image-20260406124753957.png" alt="image-20260406124753957"></p><h4 id="磁盘的物理地址"><a href="#磁盘的物理地址" class="headerlink" title="磁盘的物理地址"></a>磁盘的物理地址</h4><p><img src="https://files.seeusercontent.com/2026/04/06/gAj1/image-20260406125055356.png" alt="image-20260406125055356"></p><p>所以我们可用（柱面号，盘面号，扇区号）来定位任意一个“磁盘块”。在“文件的物理结构”小节中，我们经常提到文件数据存放在外存中的几号块，这个块号就可以转换成（柱面号，盘面号，扇区号）的地址形式。</p><p>可根据该地址读取一个“块”<br>①根据“柱面号”移动磁臂，让磁头指向指定柱面；<br>②激活指定盘面对应的磁头；<br>③磁盘旋转的过程中，指定的扇区会从<br>磁头下面划过，这样就完成了对指定扇<br>区的读/写。</p><h4 id="磁盘的分类"><a href="#磁盘的分类" class="headerlink" title="磁盘的分类"></a>磁盘的分类</h4><p>根据磁头可否移动可分为两种磁盘：</p><ul><li>活动头磁盘</li><li>固定头磁盘</li></ul><p>根据盘片是否可以更换可以分为：</p><ul><li>可换盘磁盘</li><li>固定盘磁盘</li></ul><h3 id="磁盘调度算法"><a href="#磁盘调度算法" class="headerlink" title="磁盘调度算法"></a>磁盘调度算法</h3><blockquote><p>这一章和处理机调度是比较像的</p></blockquote><p>磁盘的存取时间在计算机组成原理中已提到。</p><p>由于机械硬盘的磁头移动会带来额外的等待时间，所以需要调度算法来减少平均寻道时间。</p><h4 id="先到先服务-FCFS"><a href="#先到先服务-FCFS" class="headerlink" title="先到先服务(FCFS)"></a>先到先服务(FCFS)</h4><p>根据进程请求访问磁盘的先后顺序进行调度。</p><p>假设磁头的初始位置是100号磁道，有多个进程先后陆续地请求访问55、58、39、18、90、160、 150、38、184号磁道</p><p>按照FCFS的规则，按照请求到达的顺序，磁头需要依次移动到55、58、39、18、90、160、150、 38、184号磁道</p><p><img src="https://files.seeusercontent.com/2026/04/06/c0kP/image-20260406130931799.png" alt="image-20260406130931799"></p><p>磁头总共移动了45+3+19+21+72+70+10+112+146=498个磁道<br>响应一个请求平均需要移动498/9=55.3个磁道（平均寻找长度）</p><p>优点：公平；如果请求访问的磁道比较集中的话，算法性能还算过的去<br>缺点：如果有大量进程竞争使用磁盘，请求访问的磁道很分散，则FCFS在性能上很差，寻道时间长。趋近于随机调度算法，因为寻道请求是随机提交的。</p><h4 id="最短寻找时间优先-SSTF"><a href="#最短寻找时间优先-SSTF" class="headerlink" title="最短寻找时间优先(SSTF)"></a>最短寻找时间优先(SSTF)</h4><p>SSTF算法会优先处理的磁道是与当前磁头最近的磁道。可以保证每次的寻道时间最短，但是并不能保证总的寻道时间最短。（其实就是贪心算法的思想，只是选择眼前最优，但是总体未必最优）<br>假设磁头的初始位置是100号磁道，有多个进程先后陆续地请求访问55、58、39、18、90、160、150、38、184号磁道。</p><p><img src="https://files.seeusercontent.com/2026/04/06/Rhj0/image-20260406131221373.png" alt="image-20260406131221373"></p><p>磁头总共移动了（100-18)+(184-18)=248个磁道<br>响应一个请求平均需要移动248/9=27.5个磁道（平均寻找长度)<br>优点：性能较好，平均寻道时间短<br>缺点：可能产生“饥饿”现象</p><p>产生饥饿的原因在于：磁头在一个小区域内来回来去地移动。</p><h4 id="扫描算法-SCAN"><a href="#扫描算法-SCAN" class="headerlink" title="扫描算法(SCAN)"></a>扫描算法(SCAN)</h4><p>SSTF算法会产生饥饿的原因在于：磁头有可能在一个小区域内来回来去地移动。为了防止这个问题，可以规定，<strong>只有磁头移动到最外侧磁道的时候才能往内移动，移动到最内侧磁道的时候才能往外移动。</strong>这就是扫描算法(SCAN)的思想。由于磁头移动的方式很像电梯，因此也叫电梯算法。<br>假设某磁盘的磁道为0~200号，磁头的初始位置是100号磁道，且此时磁头正在往磁道号增大的方向移动，有多个进程先后陆续地请求访问55、58、39、18、90、160、150、38、184号磁道。</p><p><img src="https://files.seeusercontent.com/2026/04/06/6uTo/image-20260406131559593.png" alt="image-20260406131559593"><br>磁头总共移动了(200-100)+(200-18)=282个磁道<br>响应一个请求平均需要移动282/9=31.3个磁道（平均寻找长度)<br>优点：性能较好，平均寻道时间较短，不会产生饥饿现象<br>缺点：</p><ul><li>只有到达最边上的磁道时才能改变磁头移动方向，事实上，处理了184号的访问请求之后就不需要再往右移动磁头了。</li><li>SCAN算法对于各个位置磁道的响应频率不平均（如：假设此时磁头正在往右移动，且刚处理过 90号磁道，那么下次处理90号磁道的请求就需要等磁头移动很长一段距离；而响应了184号磁道的请求之后，很快又可以再次响应184号磁道的请求了)</li></ul><h4 id="LOOK调度算法"><a href="#LOOK调度算法" class="headerlink" title="LOOK调度算法"></a>LOOK调度算法</h4><p>扫描算法(SCAN)中，只有到达最边上的磁道时才能改变磁头移动方向，事实上，处理了184号磁道的访问请求之后就不需要再往右移动磁头了。LOOK调度算法就是为了解决这个问题，如果在磁头移动方向上已经没有别的请求，就可以立即改变磁头移动方向。（边移动边观察，因此叫LOOK）</p><p>假设某磁盘的磁道为0~200号，磁头的初始位置是100号磁道，且此时磁头正在往磁道号增大的方向移动，有多个进程先后陆续地请求访问55、58、39、18、90、160、150、38、184号磁道</p><p><img src="https://files.seeusercontent.com/2026/04/06/Xh7z/image-20260406132019078.png" alt="image-20260406132019078"></p><p>磁头总共移动了(184-100)+(184-18)=250个磁道<br>响应一个请求平均需要移动250/9=27.5个磁道（平均寻找长度)<br>优点：比起SCAN算法来，不需要每次都移动到最外侧或最内侧才改变磁头方向，使寻道时间进一步缩短</p><h4 id="循环扫描算法-C-SCAN"><a href="#循环扫描算法-C-SCAN" class="headerlink" title="循环扫描算法(C-SCAN)"></a>循环扫描算法(C-SCAN)</h4><p>SCAN算法对于各个位置磁道的响应频率不平均，而C-SCAN算法就是为了解决这个问题。规定<strong>只有磁头朝某个特定方向移动时才处理磁道访问请求，而返回时直接快速移动至起始端而不处理任何请求</strong>。<br>假设某磁盘的磁道为0~200号，磁头的初始位置是100号磁道，且此时磁头正在往磁道号增大的方向移动，有多个进程先后陆续地请求访问55、58、39、18、90、160、150、38、184号磁道。</p><p><img src="https://files.seeusercontent.com/2026/04/06/o3Tq/image-20260406132315314.png" alt="image-20260406132315314"></p><p>磁头总共移动了(200-100)+(200-0)+(90-0)=390个磁道</p><p>响应一个请求平均需要移动390/9=43.3个磁道（平均寻找长度）</p><p>优点：比起SCAN来，对于各个位置磁道的响应频率很平均。</p><p>缺点：只有到达最边上的磁道时才能改变磁头移动方向，事实上，处理了184号磁道的访问请求之后就不需要再往右移动磁头了；并且，磁头返回时其实只需要返回到18号磁道即可，不需要返回到最边缘的磁道。另外，比起SCAN算法来，平均寻道时间更长。</p><h4 id="C-LOOK调度算法"><a href="#C-LOOK调度算法" class="headerlink" title="C-LOOK调度算法"></a>C-LOOK调度算法</h4><p>C-SCAN算法的主要缺点是只有到达最边上的磁道时才能改变磁头移动方向，并且磁头返回时不一定需要返回到最边缘的磁道上。C-LOOK算法就是为了解决这个问题。<strong>如果磁头移动的方向上己经没有磁道访问请求了，就可以立即让磁头返回，并且磁头只需要返回到有磁道访问请求的位置即可。</strong></p><p>假设某磁盘的磁道为0~200号，磁头的初始位置是100号磁道，且此时磁头正在往磁道号增大的方向移动，有多个进程先后陆续地请求访问55、58、39、18、90、160、150、38、184号磁道</p><p><img src="https://files.seeusercontent.com/2026/04/06/Xq1z/image-20260406132558192.png" alt="image-20260406132558192"></p><p>磁头总共移动了（184-100)+(184-18)+(90-18)=322个磁道<br>响应一个请求平均需要移动322/9=35.8个磁道（平均寻找长度）<br>优点：比起C-SCAN算法来，不需要每次都移动到最外侧或最内侧才改变磁头方向，使寻道时间进一步缩短。</p><h3 id="减少磁盘延迟时间的办法"><a href="#减少磁盘延迟时间的办法" class="headerlink" title="减少磁盘延迟时间的办法"></a>减少磁盘延迟时间的办法</h3><p>延迟时间：将目标扇区转到磁头下面所花的时间。</p><p><img src="https://files.seeusercontent.com/2026/04/06/z4rN/image-20260406133239663.png" alt="image-20260406133239663"></p><p>磁头读入一个扇区数据后需要一小段时间处理，遗憾的是，这段处理时间通常没有一个单一、全球统一的专有名词，但在磁盘技术和性能分析的语境中，它最常被描述为 <strong>控制器处理时间</strong> 或 <strong>命令开销</strong>，下称命令开销。</p><p>这命令开销的阻隔造成了读取完某一扇区后的真空期，意味着连续读取都需要等待磁盘转一圈，这会使得延迟时间特别长。</p><h4 id="交替编号"><a href="#交替编号" class="headerlink" title="交替编号"></a>交替编号</h4><p>若采用交替编号的策略，即让逻辑上相邻的扇区在物理上有一定的间隔，可以使读取连续的逻辑扇区所需要的延迟时间更小。</p><p><img src="https://files.seeusercontent.com/2026/04/06/e4If/image-20260406133531462.png" alt="image-20260406133531462"></p><p>读取2号扇区后，因为相隔的6号扇区的存在，磁头能够在挂到3号扇区之前就做好读取数据的准备。</p><h4 id="磁盘地址结构的设计"><a href="#磁盘地址结构的设计" class="headerlink" title="磁盘地址结构的设计"></a>磁盘地址结构的设计</h4><p>磁盘的物理地址是（柱面号，盘面号，扇区号），为什么不是（盘面号，柱面号，扇区号）？</p><p>若物理地址结构是（盘面号，柱面号，扇区号），且需要连续读取物理地址(00,000,000)~(00,001,111)的扇区：</p><p>(00,000,000)~(00,000,111)转两圈可读完</p><p>之后再读取物理地址相邻的区域，即<br>(00,001,000)~（00,001,111),需要启动磁头臂，将磁头移动到下一个磁道。</p><hr><p>若物理地址结构是（柱面号，盘面号，扇区号），且需要连续读取物理地址(000,00,000)~(000,01,111)的扇区：</p><p>(000,00,000)~（000,00,111)由盘面0的磁头读入数据</p><p>之后再读取物理地址相邻的区域，即<br>(000,01,000)~（000,01,111),由于柱面号/磁道号相同，只是盘面号不同，因此不需要移动磁头臂。只需要激活相邻盘面的磁头即可</p><p>所以，使用（柱面号，盘面号，扇区号）的地址结构能够有效减少磁头移动消耗的时间。</p><h4 id="错位命名"><a href="#错位命名" class="headerlink" title="错位命名"></a>错位命名</h4><p>更准确的命名是<strong>柱面偏移</strong> 或 <strong>磁道偏移</strong></p><p><strong>在多盘片硬盘中，将不同盘面上同一柱面的各个磁道的起始扇区位置，在圆周方向上故意错开一定角度的技术。</strong></p><p><img src="https://files.seeusercontent.com/2026/04/06/y5nG/image-20260406135451876.png" alt="image-20260406135451876"></p><p>读取完第一个盘面后想读取第二个盘面，会因为命令开销的存在而不得不等待再转一圈才能读取。</p><p>通过这种方式：</p><p><img src="https://files.seeusercontent.com/2026/04/06/Gjy9/image-20260406140412950.png" alt="image-20260406140412950"></p><h3 id="磁盘管理"><a href="#磁盘管理" class="headerlink" title="磁盘管理"></a>磁盘管理</h3><p>三部分：</p><ul><li>磁盘初始化</li><li>引导块</li><li>坏块的管理</li></ul><h4 id="磁盘初始化"><a href="#磁盘初始化" class="headerlink" title="磁盘初始化"></a>磁盘初始化</h4><p>磁盘初始化：</p><ul><li>Step1:进行低级格式化（物理格式化），将磁盘的各个磁道划分为扇区。一个扇区通常可分为头、数据区域（如512B大小)、尾三个部分组成。管理扇区所需要的各种数据结构一般<br>存放在头、尾两个部分，包括扇区校验码（奇偶校验、CC循环冗余校验码等，校验码用于校验扇区中的数据是否发生错误)</li><li>Step2:将磁盘分区，每个分区由若干柱面组成（即分为我们熟悉的C盘、D盘、E盘)</li><li>Step3:进行逻辑格式化，创建文件系统。包括创建文件系统的根目录、初始化存储空间管理所用的数据结构（如位示图、空闲分区表)</li></ul><p><img src="https://files.seeusercontent.com/2026/04/06/ju1F/image-20260406141513786.png" alt="image-20260406141513786"></p><h4 id="引导块"><a href="#引导块" class="headerlink" title="引导块"></a>引导块</h4><p>计算机开机时需要进行一系列初始化的工作，这些初始化工作是通过执行初始化程序（自举程序)完成的。</p><p>在电路元件中，ROM又名“只读存储器”，出厂写入且不可更改，集成在主板上。</p><p>初始化程序程序（自举程序）如果放在ROM中，万一需要更新自举程序、将会很不方便，因为 ROM中的数据无法更改。</p><p>所以，将完整的自举程序放在磁盘的启动块（即引导块/启动分区)上，启动块位于磁盘的固定位置。</p><p>ROM中只存放很小的“自举装入程序”。开机时计算机先运行“自举装入程序”，通过执行该程序就可找到引导块，并将完整的“自举程序”读入内存，完成初始化。</p><p>拥有启动分区的磁盘称为启动磁盘或系统磁盘(C:盘）</p><h4 id="坏块的管理"><a href="#坏块的管理" class="headerlink" title="坏块的管理"></a>坏块的管理</h4><p>坏块是存储设备中因物理损伤或老化而无法正常读写数据的微小区域。这属于硬件故障，操作系统是无法修复的。应该将坏块标记出来，以免错误地使用到它。</p><p>对于简单的磁盘，可以在逻辑格式化时（建立文件系统时)对整个磁盘进行坏块检查，标明哪些扇区是坏扇区，比如：在FAT表上标明。（在这种方式中，坏块对操作系统不透明)</p><p>对于复杂的磁盘，磁盘控制器（磁盘设备内部的一个硬件部件)会维护一个坏块链表。</p><p>在磁盘出厂前进行低级格式化（物理格式化）时就将坏块链进行初始化会保留一些“备用扇区”，用于替换坏块。这种方案称为扇区备用。且这种处理方式中，坏块对操作系统透明。</p><h3 id="固态硬盘SSD"><a href="#固态硬盘SSD" class="headerlink" title="固态硬盘SSD"></a>固态硬盘SSD</h3><p>固态硬盘SSD基于闪存技术Flash Memory,属于电可擦除ROM,即EEPROM。</p><p><img src="https://files.seeusercontent.com/2026/04/06/xeG3/image-20260406143351851.png" alt="image-20260406143351851"></p><h4 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h4><p><img src="https://files.seeusercontent.com/2026/04/06/8Aty/image-20260406144803280.png" alt="image-20260406144803280"></p><ul><li>闪存翻译层</li><li><ul><li>负责翻译逻辑块号，找到对应页(Page)</li></ul></li><li>存储介质：</li><li><ul><li>多个闪存芯片(Flash Chip)(黑色的就是)</li><li>每个芯片包含多个块(block)</li><li>每个块包含多个页(page)</li></ul></li></ul><h4 id="读写"><a href="#读写" class="headerlink" title="读写"></a>读写</h4><ul><li>以页(page)为单位读/写，相当于磁盘的”扇区”</li></ul><p>注意，逻辑地址的一个块等同于磁盘的一个块，<strong>等于固态硬盘的一个页</strong>而不是块。</p><p>SSD的读写是以页为单位的。</p><ul><li>以块(block)为单位”擦除”，擦干净的块，其中的每页都可以写一次，读无限次</li></ul><p>如果需要对某块内的某一页进行擦除，就需要把块内待擦除页之外所有页转移到其他块中，并<strong>修改闪存翻译层中的地址</strong>，再将原块擦除。</p><ul><li>支持随机访问，系统给定一个逻辑地址，闪存翻译层可通过电路迅速定位到对应的物理地址</li><li>读快、写慢。要写的页如果有数据，则不能写入，需要将块内其他页全部复制到一个新的（擦除过的）块中，再写入新的页</li></ul><h4 id="特点"><a href="#特点" class="headerlink" title="特点"></a>特点</h4><ul><li>SSD读写速度快，随机访问性能高，用电路控制访问位置；机械硬盘通过移动磁臂旋转磁盘控制访问位置，有寻道时间和旋转延迟</li><li>SSD安静无噪音、耐摔抗震、能耗低、但是造价更贵</li><li>SSD的一个”块”被擦除次数过多（重复写同一个块）可能会坏掉，而机械硬盘的扇区不会因为写的次数太多而坏掉</li></ul><h3 id="磨损均衡技术"><a href="#磨损均衡技术" class="headerlink" title="磨损均衡技术"></a>磨损均衡技术</h3><p>因为SSD的一个”块”被擦除次数过多容易报废，所以需要将将“擦除”平均分布在各个块上，以提升使用寿命。</p><p>这被称为磨损均衡，两种：</p><ul><li><strong>动态磨损均衡：写入数据时，自动选择较新的闪存块。</strong></li><li><strong>静态磨损均衡：就算没有数据写入，SSD也会检测并自动进行数据分配，让老的闪存块承担无须写数据的存储任务，同时让新的闪存块腾出空间，承担平常的写操作。</strong></li></ul><blockquote><p>这里就是终点了，谢谢你读到这里。为了你，也为了我。</p></blockquote>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;操作系统：第五章&quot;&gt;&lt;a href=&quot;#操作系统：第五章&quot; class=&quot;headerlink&quot; title=&quot;操作系统：第五章&quot;&gt;&lt;/a&gt;操作系统：第五章&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;这是最后一章了，这和计算机组成原理比较相似。&lt;/p&gt;
&lt;/bloc</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>操作系统第四章：文件</title>
    <link href="https://blog.willmo.top/posts/Operating%20System%20Chapter%204:%20Files/"/>
    <id>https://blog.willmo.top/posts/Operating%20System%20Chapter%204:%20Files/</id>
    <published>2026-04-20T07:15:21.371Z</published>
    <updated>2026-04-20T07:15:22.538Z</updated>
    
    <content type="html"><![CDATA[<h1 id="操作系统：第四章"><a href="#操作系统：第四章" class="headerlink" title="操作系统：第四章"></a>操作系统：第四章</h1><h3 id="初识文件管理"><a href="#初识文件管理" class="headerlink" title="初识文件管理"></a>初识文件管理</h3><blockquote><p>这是一个非常难的章节，所以我们需要先从整体的角度来看。</p></blockquote><p>操作系统是计算机资源的管理者，包括文件管理。</p><p>所谓文件，就是一组有意义的信息组合。</p><p>操作系统需要解决几个问题：</p><ul><li>一个文件有哪些属性</li><li>文件内部的数据应该如何组织起来</li><li>文件之间又应该怎么组织起来</li><li>在计算机层级中，OS应该向上级提供哪些功能</li><li>文件如何存放在外存中</li></ul><h4 id="文件属性"><a href="#文件属性" class="headerlink" title="文件属性"></a>文件属性</h4><ul><li>文件名</li></ul><p>文件名：由创建文件的用户决定文件名，主要是为了方便用户找到文件，同一目录下不允许有重名文件。</p><ul><li>标识符</li></ul><p>标识符：一个系统内的各文件标识符唯一，对用户来说毫无可读性，因此标识符只是操作系统用于区分各个文件的一种内部名称。</p><ul><li>类型</li></ul><p>指明文件的类型</p><ul><li>位置</li></ul><p>位置：文件存放的路径（让用户使用)、在外存中的地址（操作系统使用，对用户不可见)</p><ul><li>大小</li></ul><p>指明文件的大小</p><ul><li>保护信息</li></ul><p>对文件进行保护的访问控制信息</p><p><img src="https://files.seeusercontent.com/2026/03/31/ycV6/image-20260331110433126.png" alt="image-20260331110433126"></p><ul><li>创建时间，修改时间，所有者信息等</li></ul><h4 id="内部的数据应该如何组织起来"><a href="#内部的数据应该如何组织起来" class="headerlink" title="内部的数据应该如何组织起来"></a>内部的数据应该如何组织起来</h4><p>无结构文件（如文本文件)一一由一些二进制或字符流组成，又称“流式文件”。</p><p>有结构文件（如数据库表，Excel表）一一由一组相似的记录组成，又称“记录式文件”</p><p>记录是一组相关数据项的集合。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260331110829238.png" alt="image-20260331110829238"></p><p>这些是之后需要讨论的“文件的逻辑结构”的问题。</p><h4 id="文件之间如何组织"><a href="#文件之间如何组织" class="headerlink" title="文件之间如何组织"></a>文件之间如何组织</h4><p>使用树状结构联系起来。</p><p><img src="https://files.seeusercontent.com/2026/03/31/fF8a/image-20260331202801267.png" alt="image-20260331202801267"></p><h4 id="向上层提供什么功能"><a href="#向上层提供什么功能" class="headerlink" title="向上层提供什么功能"></a>向上层提供什么功能</h4><p><img src="https://files.seeusercontent.com/2026/03/31/t1Fb/image-20260331203202851.png" alt="image-20260331203202851"></p><p><img src="https://files.seeusercontent.com/2026/03/31/vm7C/image-20260331203229816.png" alt="image-20260331203229816"></p><h4 id="如何放在外存中？"><a href="#如何放在外存中？" class="headerlink" title="如何放在外存中？"></a>如何放在外存中？</h4><p><img src="https://files.seeusercontent.com/2026/03/31/eJ4c/image-20260331204104894.png" alt="image-20260331204104894"></p><p>那么文件比较大，就需要存放在多个磁盘块当中。而操作系统有需要管理空闲磁盘块。</p><p><img src="https://files.seeusercontent.com/2026/03/31/r5Qd/image-20260331204253625.png" alt="image-20260331204253625"></p><h3 id="文件的逻辑结构"><a href="#文件的逻辑结构" class="headerlink" title="文件的逻辑结构"></a>文件的逻辑结构</h3><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260401102240072.png" alt="image-20260401102240072"></p><p>所谓的“逻辑结构”，就是指在用户看来文件内部的数据应该是如何组织起来的。而物理结构”指的是在操作系统看来，文件的数据是如何存放在外存中的。</p><p>类似于数据结构的“逻辑结构”和“物理结构”。<br>如“线性表”就是一种逻辑结构，在用户角度看来，线性表就是一组有先后关系的元素序列。<br>“线性表”这种逻辑结构可以用不同的物理结构实现，如：顺序表/链表。顺序表的各个元素在逻辑上相邻，在物理上也相邻；而链表的各个元素在物理上可以是不相邻的。因此，顺序表可以实现“随机访问”，而“链表”无法实现随机访问。</p><h4 id="无结构文件"><a href="#无结构文件" class="headerlink" title="无结构文件"></a>无结构文件</h4><p>无结构文件：文件内部的数据就是一系列二进制流或字符流组成。又称“流式文件”。如：Windows操作系统中的.txt文件。</p><h4 id="有结构文件"><a href="#有结构文件" class="headerlink" title="有结构文件"></a>有结构文件</h4><p>有结构文件：由一组相似的记录组成，又称“记录式文件”每条记录又若干个数据项组成。如：数据库表文件。一般来说，每条记录有一个数据项可作为关键字（作为识别不同记录的ID)。根据各条记录的长度是否相等，可分为定长记录和可变长记录两种。</p><p>定长记录如下：</p><p><img src="https://files.seeusercontent.com/2026/04/01/kC0f/image-20260401102721165.png"></p><p>但可变长记录才是最常用的：</p><p><img src="https://files.seeusercontent.com/2026/04/01/c1Ok/image-20260401102800578.png" alt="image-20260401102800578"></p><p>记录的逻辑结构分为：</p><ul><li>顺序文件</li><li>索引文件</li><li>索引顺序文件</li></ul><h4 id="顺序文件"><a href="#顺序文件" class="headerlink" title="顺序文件"></a>顺序文件</h4><p>顺序文件：文件中的记录一个接一个地顺序排列（逻辑上），记录可以是定长的或可变长的。各个记录在物理上可以顺序存储或链式存储。</p><p><img src="https://files.seeusercontent.com/2026/04/01/fTu0/image-20260401103146232.png" alt="image-20260401103146232"></p><p>那么就能够分为</p><ul><li>串结构</li><li><ul><li>记录之间的顺序性与关键字无关</li></ul></li><li>顺序结构</li><li><ul><li>记录之间的顺序按关键字顺序排列</li></ul></li></ul><p>结构决定功能，具有以下特点：</p><p><img src="https://files.seeusercontent.com/2026/04/01/T5ve/image-20260401103613773.png" alt="image-20260401103613773"></p><p>基于索引的顺序结构能够通过首地址加随机偏移量来实现<strong>随机访问</strong>，但是存储地址不连续的链式存储结构无法这么做。</p><p>特殊地，顺序存储的可变长记录因为字段区间不固定，所以也无法进行随机存取。</p><p>结论：定长记录的顺序文件，若物理上采用顺序存储，则可实现随机存取；若能再保证记录的顺序结构，则可实现快速检索(如二分查找法)(即根据关键字快速找到对应记录)。</p><h4 id="索引文件"><a href="#索引文件" class="headerlink" title="索引文件"></a>索引文件</h4><p>对于可变长记录文件，要找到第ⅰ个记录，必须先顺序第查找前i -1个记录，但是很多应用场景中又必须使用可变长记录。如何解决这个问题？</p><p>我们建立一张索引表，每条记录对应一个索引项。而这些记录在物理上可以离散存放。</p><p>索引表本身是定长记录的顺序文件。因此可以快速找到第ⅰ个记录对应的索引项。</p><p><img src="https://files.seeusercontent.com/2026/04/01/Idq7/image-20260401105917904.png" alt="image-20260401105917904"></p><p>可将关键字作为索引号内容，若按关键字顺序排列，则还可以支持按照关键字折半查找。</p><p>每当要增加/删除一个记录时，需要对索引表进行修改。由于索引文件有很快的检索速度，因此主要用于对信息处理的及时性要求比较高的场合。</p><p>另外，可以用不同的数据项建立多个索表。如：</p><p>学生信息表中，可用关键字“学号”建立一张索引表。也可用“姓名”建立一张索引表。这样就可以根据“姓名”快速地检索文件了。</p><h4 id="索引顺序文件"><a href="#索引顺序文件" class="headerlink" title="索引顺序文件"></a>索引顺序文件</h4><p>思考索引文件的缺点：每个记录对应一个索引表项，因此索引表可能会很大比如：文件的每个记录平均只占8B,而每个索引表项占32个字节，那么索引表都要比文件内容本身大4倍，这样对存储空间的利用率就太低了。</p><p>索引顺序文件是索引文件和顺序文件思想的结合。索引顺序文件中，同样会为文件建立一张索引表，但不同的是：并不是每个记录对应一个索引表项，而是一组记录对应一个索引表项。</p><p>听起来很抽象，请结合以下来理解：</p><p><strong>一本完全索引的字典（对应“索引文件”）</strong>：目录里列出<strong>每一个字</strong>和它的页码。查找极快，但目录本身非常厚。</p><p><strong>一本没有索引的字典（对应“顺序文件”）</strong>：所有字按顺序排列，但没有目录。查找时只能一页一页翻。</p><p><strong>一本标准的字典（这就是“索引顺序文件”）</strong>：目录（索引表）只列出 <strong>“A部、C部、F部……”</strong> 这些<strong>章节组</strong>的起始页码。要查“苹果”，你先在目录里找到“P部”的起始页，然后翻到P部那个章节，再在这个章节内部按顺序找到“苹果”。</p><p><img src="https://files.seeusercontent.com/2026/04/01/k3lL/image-20260401110913372.png" alt="image-20260401110913372"></p><p>这么做带来的收益是很高的：</p><p>若一个顺序文件有10000个记录，则根据关键字检索文件，只能从头开始顺序查找（这里指的并不是定长记录、顺序结构的顺序文件)，平均须查找5000个记录。若采用索引顺序文件结构，可把10000个记录分为V10000=100组，每组100个记录。则需要先顺序查找索引表找到分组（共100个分组，因此索引表长度为100，平均需要查50次），找到分组后，再在分组中顺序查找记录（每个分组100个记录，因此平均需要查50次）。可见，采用索引顺序文件结构后，平均查找次数减少为50+50=100次。</p><p>但是如果数据量很庞大，那就需要使用多级索引来优化性能。</p><p>为了进一步提高检索效率，可以为顺序文件建立多级索引表。例如，对于一个含10^6个记录的文件，可先为该文件建立一张低级索引表，每100个记录为一组，故低级索引表中共有10000个表项（即10000个定长记录)，再把这10000个定长记录分组，每组100个，为其建立顶级索引表，故顶级索引表中共有100个表项。</p><p><img src="https://files.seeusercontent.com/2026/04/01/fFf9/image-20260401111314691.png" alt="image-20260401111314691"></p><h3 id="文件目录"><a href="#文件目录" class="headerlink" title="文件目录"></a>文件目录</h3><p>计算机中的目录是树状结构，组织结构清晰，易于查找和访问。那么这种结构在操作系统如何达成呢？</p><p><img src="https://files.seeusercontent.com/2026/04/01/pcR1/image-20260401114133824.png" alt="image-20260401114133824"></p><p>实际上目录本身就是一种有结构文件，由一条条记录组成，类似一张“表格”。每条记录对应一个在该放在该目录下的文件。而目录项就是表格中的每一条记录。</p><p>注意到记录也存放了物理地址，所以，当我们双击“照片”后，操作系统会在这个目录表中找到关键字“照片”对应的目录项（也就是记录），然后从外存中将“照片”目录的信息读入内存，于是，“照片”目录中的内容就可以显示出来了。</p><p>目录文件中一条记录就是一个“<strong>文件控制块</strong>FCB”。FCB的有序集合称为“文件目录”，一个FCB就是一个文件目录项。 FCB中包含了文件的基本信息（文件名、物理地址、逻辑结构、物理结构等)，存取控制信息（是否可读/何写、禁止访问的用户名单等)，使用信息（如文件的建立时间、修改时间等）</p><p>不过最重要，最基本的还是<strong>文件名，文件存放的物理地址。</strong></p><p>FCB最重要的职能就是实现了文件名和文件之间的映射关系。使得用户能够“按名存取”。</p><p>目录需要完成以下操作：</p><ul><li>搜索：当用户要使用一个文件时，系统要根据文件名搜索目录，找到该文件对应的目录项</li><li>创建文件：创建一个新文件时，需要在其所属的目录中增加一个目录项</li><li>删除文件：当删除一个文件时，需要在目录中删除相应的目录项</li><li>显示目录：用户可以请求显示目录的内容，如显示该目录中的所有文件及相应属性</li><li>修改目录：某些文件属性保存在目录中，因此属性变化时需要修改对应目录项。</li></ul><h4 id="早期单机目录结构"><a href="#早期单机目录结构" class="headerlink" title="早期单机目录结构"></a>早期单机目录结构</h4><p>早期操作系统并不支持多级目录，整个系统中只建立一张目录表，每个文件占一个目录项。</p><p>单级目录实现了“按名存取”，但是不允许文件重名。<br>在创建一个文件时，需要先检查目录表中有没有重名文件，确定不重名后才能允许建立文件，并将新文件对应的目录项插入目录表中。<br>显然，单级目录结构不适用于多用户操作系统。</p><h4 id="两级目录结构"><a href="#两级目录结构" class="headerlink" title="两级目录结构"></a>两级目录结构</h4><p>早期的多用户操作系统，采用两级目录结构。分为主文件目录(MFD,Master File Directory)和用户文件目录(UFD,User Flie Directory)。</p><p><img src="https://files.seeusercontent.com/2026/04/01/ge8X/image-20260401132057014.png" alt="image-20260401132057014"></p><h4 id="多级目录结构"><a href="#多级目录结构" class="headerlink" title="多级目录结构"></a>多级目录结构</h4><p>又名树级目录结构或簇级目录结构。</p><p><img src="https://files.seeusercontent.com/2026/04/01/ryS3/image-20260401132217902.png" alt="image-20260401132217902"></p><p>用户（或用户进程）要访问某个文件时要用文件路径名标识文件，文件路径名是个字符串。各级目录之间用“/”隔开。从根目录出发的路径称为绝对路径。例如：自拍.jPg的绝对路径是“/照片/2015-08/自拍，jpg”</p><p>系统根据绝对路径一层一层地找到下一级目录。刚开始从外存读入根目录的目录表；找到“照片”目录的存放位置后，从外存读入对应的目录表；再找到“2015-08”目录的存放位置，再从外存读入对应目录表：最后才找到文件“自拍jPg”的存放位置。整个过程需要3次读磁盘I/O操作。</p><p>但是很多时候，用户会连续访问同一目录内的多个文件（比如：接连查看“2015-08”目录内的多个照片文件)显然，每次都从根目录开始查找，是很低效的。因此可以设置一个“当前目录”。</p><p>例如，此时已经打开了“照片”的目录文件，也就是说，这张目录表己调入内存，那么可以把它设置为“当前目录”。当用户想要访问某个文件时，可以使用从当前目录出发的“相对路径”<br>在Linux中，“.”表示当前目录，因此如果“照片”是当前目录，则”自拍jpg”的相对路径为：<br>“/2015-08/自拍jpg”。从当前路径出发，只需要查询内存中的“照片”目录表，即可知道”2015-08”目录表的存放位置，从外存调入该目录，即可知道“自拍jpg”存放的位置了。</p><p>但是树形目录不是万能的，它不便于实现用户间的文件共享。所以有了无环图目录结构。</p><p><img src="https://files.seeusercontent.com/2026/04/01/cAc6/image-20260401133928766.png" alt="image-20260401133928766"></p><p>这意味着文件的删除不能再像以前一样这么简单。</p><p>可以用不同的文件名指向同一个文件，甚至可以指向同一个目录（共享同一目录下的所有内容）。<br>需要为每个共享结点设置一个共享计数器，用于记录此时有多少个地方在共享该结点。用户提出删除结点的请求时，只是删除该用户的FCB、并使共享计数器减1，并不会直接删除共享结点。共享文件不同于复制文件，在共享文件中，由于用户指向同一个文件因此只要其中一个用户修改了，另一边也会发觉文件变化。</p><h4 id="索引节点-FCB的改进"><a href="#索引节点-FCB的改进" class="headerlink" title="索引节点(FCB的改进)"></a>索引节点(FCB的改进)</h4><p>其实在查找各级目录的过程中只需要用到“文件名”这个信息，只有文件名匹配时，才需要读出文件的其他信息。因此可以考虑让目录表“瘦身”来提升效率。</p><p>那么访问流程就是：目录项就是键值对，文件名对索引节点编号(内有除了文件名之外的所有信息)，然后如果需要访问某个文件就根据编号打开索引节点。那这个索引节点无非就是将目录项的“文件详细信息”那一栏给封装起来了。</p><p><img src="https://files.seeusercontent.com/2026/04/01/1Yss/image-20260401135150141.png" alt="image-20260401135150141"></p><p>假设一个FCB是64B,磁盘块的大小为1KB,则每个盘块中只能存放 16个FCB。若一个文件目录中共有 640个目录项，则共需要占用 640/16=40个盘块。因此按照某文件名检索该目录，平均需要查询320个目录项，平均需要启动磁盘20次（每次磁盘/O读入一块）。</p><p>若使用索引结点机制，文件名占14B,索引结点指针占2B,则每个盘块可存放64个目录项，那么按文件名检索目录平均只需要读入320/64=5个磁盘块。显然，这将大大提升文件检索速度。</p><p>当找到文件名对应的目录项时，才需要将索引结点调入内存，索引结点中记录了文件的各种信息，包括文件在外存中的存放位置，根据“存放位置”即可找到文件。</p><p>存放在外存中的索引结点称为“磁盘索引结点”，当索引结点放入内存后称为“内存索引结点”。相比之下内存索引结点中需要增加一些信息，比如：文件是否被修改、此时有几个进程正在访问该文件等。</p><h3 id="文件的物理结构"><a href="#文件的物理结构" class="headerlink" title="文件的物理结构"></a>文件的物理结构</h3><p>又名文件分配方式，这涉及到对非空闲磁盘块的管理。</p><p>分为以下结构：</p><ul><li>连续分配</li><li>链接分配</li><li><ul><li>隐式链接</li><li>显式链接</li></ul></li><li>索引分配</li></ul><p>补充几个知识点：</p><p>类似以于内存分配，磁盘中的存储单元也会被分为一个个“块/磁盘块/物理块”。很多操作系统中，磁盘块的大小与内存块、页面的大小相同，这样进行数据交换会很方便。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260401142323473.png" alt="image-20260401142323473"></p><p>在内存管理中，进程的逻辑地址空间被分为一个一个页面</p><p>同样的，在外存管理中，为了方便对文件数据的管理，文件的逻辑地址空间也被分为了一个一个的文件“块”</p><p>于是文件的逻辑地址也可以表示为（逻辑块号，块内地址）的形式。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260401142540114.png" alt="image-20260401142540114"></p><p>操作系统为文件分配存储空间都是以块为单位的。</p><p>用户通过逻辑地址来操作自己的文件，操作系统要负责实现从逻辑地址到物理地址的映射。</p><h4 id="连续分配"><a href="#连续分配" class="headerlink" title="连续分配"></a>连续分配</h4><p>连续分配方式要求每个文件在磁盘上占有一组连续的块。</p><p><img src="https://files.seeusercontent.com/2026/04/02/Lf8g/image-20260402140140838.png" alt="image-20260402140140838"></p><p>如这里，文件aaa就是保存到了物理块4，5，6当中。</p><p>用户通过逻辑地址来操作自己的文件，操作系统如何实现从逻辑地址到物理地址的映射？</p><p>（逻辑块号，块内地址)→物理块号，块内地址)。只需转换块号就行，块内地址保持不变。</p><p>在文件目录中记录存放的起始块号和长度(一共占用几个块)。</p><table><thead><tr><th>文件名</th><th>······</th><th>起始块号</th><th>长度</th></tr></thead><tbody><tr><td>aaa</td><td>······</td><td>4</td><td>3</td></tr><tr><td>bbb</td><td>······</td><td>10</td><td>4</td></tr><tr><td>·····</td><td>·····</td><td>····</td><td>···</td></tr></tbody></table><p>用户给出要访问的逻辑块号，操作系统<br>找到该文件对应的目录项(FCB)物理块号=起始块号+逻辑块号<br>当然，还需要检查用户提供的逻辑块号是否合法（逻辑块号≥长度就不合法）可以直接算出逻辑块号对应的物理块号，因此连续分配支持顺序访问和直接访问（即随机访问）。</p><p>值得一提的是，对于磁盘，也就是机械硬盘，读取某个磁盘块时，需要移动磁头。访问的<strong>两个磁盘块相隔越远，移动磁头所需时间就越长。</strong>所以，连续分配的文件在顺序读写的时候最快。</p><p>缺点是：</p><ul><li>物理上采用连续分配的文件不方便拓展。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/02/hvI8/image-20260402141303146.png" alt="image-20260402141303146"></p><p>若此时文件A要拓展，需要再增加一个磁盘块（总共需要连续的4个磁盘块)。由于采用连续结构，因此文件A占用的磁盘块必须是连续的。因此只能将文件A全部“迁移”到绿色区域。</p><ul><li>物理上采用连续分配，存储空间利用率低，会产生难以利用的磁盘碎片可以用紧凑来处理碎片，但是需要耗费很大的时间代价。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/02/2Qvv/image-20260402141436150.png" alt="image-20260402141436150"></p><h4 id="链接分配"><a href="#链接分配" class="headerlink" title="链接分配"></a>链接分配</h4><p>链接分配采取离散分配的方式，可以为文件分配离散的磁盘块。</p><p>分为</p><ul><li>隐式链接</li><li>显式链接</li></ul><p>可以参考链表来理解过程：</p><ul><li>隐式链接</li></ul><p><img src="https://files.seeusercontent.com/2026/04/02/qJ4y/image-20260402142342249.png" alt="image-20260402142342249"></p><p>如何实现文件的逻辑块号到物理块号的转变？</p><p>用户给出要访问的逻辑块号i，操作系统找到该文件对应的目录项(FCB).</p><p>从目录项中找到起始块号（即0号块），将0号逻辑块读入内存，由此知道1号逻辑块存放的物理块号，于是读入1号逻辑块，再找到2号逻辑块的存放位置…以此类推。</p><p>结论：采用链式分配（隐式链接）方式的文件，只支持顺序访问，不支持随机访问，查找效率低。另外，指向下一个盘块的指针也需要耗费少量的存储空间。</p><ul><li>显式链接</li></ul><p>把用于链接文件各物理块的指针显式地存放在一张表中。即文件分配(FAT,File Allocation Table)</p><p><img src="https://files.seeusercontent.com/2026/04/02/3ypT/image-20260402143132406.png" alt="image-20260402143132406"></p><p>就是通过一张表，用索引来代替指针。</p><p>注意：一个磁盘仅设置一张FAT。开机时，将FAT读入内存，并常驻内存。FAT的各个表项在物理上连续存储，且每一个表项长度相同，因此“物理块号”字段可以是隐含的。</p><p>那么如何实现逻辑块号到物理块号的转变呢？</p><p>用户给出要访问的逻辑块号ⅰ，操作系统找到该文件对应的目录项(FCB)<br>从目录项中找到起始块号，若i&gt;0,则查询内存中的文件分配表FAT(该表常驻内存),往后找到ⅰ号逻辑块对应的物理块号。逻辑块号转换成物理块号的过程不需要读磁盘操作。</p><p>结论：采用链式分配（显式链接）方式的文件，支持顺序访问，也支持随机访问（想访问ⅰ号逻辑块时，并不需要依次访问之前的0~-1号逻辑块)，由于块号转换的过程不需要访问磁盘，因此相比于隐式链接来说，访问速度快很多。</p><p>总结一下：</p><p>链接分配采取离散分配的方式，可以为文件分配离散的磁盘块。分为隐式链接和显式链接两种。<br>隐式链接一一除文件的最后一个盘块之外，每个盘块中都存有指向下一个盘块的指针。文件目录<br>包括文件第一块的指针和最后一块的指针。<br>优点：很方便文件拓展，不会有碎片问题，外存利用率高。<br>缺点：只支持顺序访问，不支持随机访问，查找效率低，指向下一个盘块的指针也需要耗费少量<br>的存储空间。<br>显式链接一一把用于链接文件各物理块的指针显式地存放在一张表中，即文件分配表(FAT,File<br>Allocation Table)。一个磁盘只会建立一张文件分配表。开机时文件分配表放入内存，并常驻内<br>存。<br>优点：很方便文件拓展，不会有碎片问题，外存利用率高，并且支持随机访问。相比于隐式链接<br>来说，地址转换时不需要访问磁盘，因此文件的访问效率更高。<br>缺点：文件分配表的需要占用一定的存储空间。</p><h4 id="索引分配"><a href="#索引分配" class="headerlink" title="索引分配"></a>索引分配</h4><p>索引分配允许文件离散地分配在各个磁盘块中，系统会为每个文件建立一张索引表，索引表中记录了文件的各个逻辑块对应的物理块（索引表的功能类似于内存管理中的页表一一建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为<strong>索引块</strong>。文件数据存放的磁盘块称为<strong>数据块</strong>。</p><p><img src="https://files.seeusercontent.com/2026/04/02/hRq3/image-20260402144542919.png" alt="image-20260402144542919"></p><p>索引块就是索引表所在位置，其包含了该文件所在的磁盘块和存放顺序（访问该文件的磁盘块访问顺序）</p><p>注：在显式链接的链式分配方式中，文件分配表FAT是一个磁盘对应一张。而索引分配方式中，索引表是一个文件对应一张</p><p>可以用固定的长度表示物理块号（如：假设磁盘总容量为1TB=2^40B,磁盘块大小为1KB,则共有30个磁盘块，则可用 4B表示磁盘块号)，因此，索引表中的“逻辑块号”可以是隐含的。</p><p>如何实现文件的逻辑块号到物理块号的转换？</p><p>用户给出要访问的逻辑块号，操作系统找<br>到该文件对应的目录项(FCB)<br>从目录项中可知索引表存放位置，将索引表<br>从外存读入内存，并查找索引表即可只ⅰ号<br>逻辑块在外存中的存放位置。</p><p>可见，索引分配方式可以支持随机访问。<br>文件拓展也很容易实现（只需要给文件分配<br>一个空闲块，并增加一个索引表项即可)</p><p>但是索引表也需要占用空间。</p><p>所以，</p><p>若每个磁盘块1KB,一个索引表项4B,则一个磁盘块只能存放256个索引项。</p><p>如果一个文件的大小超过了256块，那么一个磁盘块是装不下文件的整张索引表的，如何解决这个问题？</p><ul><li>链接方案</li></ul><p>如果索引表太大，一个索引块装不下，那么可以将多个索引块链接起来存放。</p><p><img src="https://files.seeusercontent.com/2026/04/02/gNh1/image-20260402150037727.png" alt="image-20260402150037727"></p><p>这是很低效的。</p><ul><li>多层索引</li></ul><p>类似于多级页表：使第一层索引块指向第二层索引块，还可根据文件大小的要求在建立第三层，第四层索引块。</p><p><img src="https://files.seeusercontent.com/2026/04/02/xa1L/image-20260402150353879.png" alt="image-20260402150353879"></p><p>若某文件采用两层索引，则该文件的最大长度可以到<br>256<em>256</em>1KB=65,536KB=64MB<br>可根据逻辑块号算出应该查找索引表中的哪个表项。如：要访问1026号逻辑块，则<br>1026/256=4,1026%256=2<br>因此可以先将一级索引表调入内存，查询4号表项，将其对应的二级索引表调入内存，再查询二级索引表的2号表项即可知道1026号逻辑块存放的磁盘块号了。<br>访问目标数据块，需要3次磁盘/O</p><p>若采用三层索引，则文件的最大长度为<br>256<em>256</em>256*1K8=16GB，代价是访问一次需要4次IO读磁盘操作。</p><ul><li>混合索引</li></ul><p>多种索引分配方式的结合。例如，一个文件的顶级索引表中，既包含直接地址索引（直接指向数据块)，又包含一级间接索引（指向单层索引表）、还包含两级间接索引（指向两层索引表）。</p><p><img src="https://files.seeusercontent.com/2026/04/02/ggO0/image-20260402151139813.png" alt="image-20260402151139813"></p><p>这种结构的索引支持最大长度是65800KB。</p><p>若顶级索引表还没读入内存访问0<del>7号逻辑块：两次读磁盘；访问8</del>263：三次读磁盘；访问264~65799：四次读磁盘</p><p><img src="https://files.seeusercontent.com/2026/04/02/9wGk/image-20260402151738933.png" alt="image-20260402151738933"></p><h3 id="逻辑结构和物理结构"><a href="#逻辑结构和物理结构" class="headerlink" title="逻辑结构和物理结构"></a>逻辑结构和物理结构</h3><p><img src="https://files.seeusercontent.com/2026/04/02/vyG1/image-20260402151952620.png" alt="image-20260402151952620"></p><p>还没么看</p><h3 id="文件存储空间管理"><a href="#文件存储空间管理" class="headerlink" title="文件存储空间管理"></a>文件存储空间管理</h3><p>这一章节需要考虑三个问题：</p><ul><li>如何记录空闲磁盘块的情况</li><li>如何分配空间</li><li>如何回收空间</li></ul><h4 id="存储空间的划分和初始化"><a href="#存储空间的划分和初始化" class="headerlink" title="存储空间的划分和初始化"></a>存储空间的划分和初始化</h4><p>装系统的时候，一个必须做的步骤是给硬盘分区，每个区都分为目录区和文件区。</p><p><img src="https://files.seeusercontent.com/2026/04/02/tl4K/image-20260402153241661.png" alt="image-20260402153241661"></p><h4 id="空闲表法"><a href="#空闲表法" class="headerlink" title="空闲表法"></a>空闲表法</h4><p>用一个表来记录空闲区间的开始地址和空闲块长度。</p><p><img src="https://files.seeusercontent.com/2026/04/02/x2qP/image-20260402153540993.png" alt="image-20260402153540993"></p><p>如何分配磁盘块：与内存管理中的动态分区分配很类似，为一个文件分配连续的存储空间。同样可采用<strong>首次适应、最佳适应、最坏适应</strong>等算法来决定要为文件分配哪个区间。</p><p>请回忆一下，都大同小异的。</p><p>如何回收磁盘块：与内存管理中的动态分区分配很类似，当回收某个存储区时需要有四种情况一一①回收区的前后都没有相邻空闲区；②回收区的前后都是空闲区；③回收区前面是空闲区；④回收区后面是空闲区。总之，回收时需要注意表项的合并问题。每次回收都需要更新空闲表。</p><h4 id="空闲链表法"><a href="#空闲链表法" class="headerlink" title="空闲链表法"></a>空闲链表法</h4><p>分为</p><ul><li>空闲盘块连链</li><li><ul><li>以盘块为单位组成一条空闲链</li></ul></li><li>空闲盘区链</li><li><ul><li>以盘区为单位组成一条空闲链</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/04/02/7jpY/image-20260402155110603.png" alt="image-20260402155110603"></p><p>对于空闲盘块链：</p><p>操作系统保存着链头、链尾指针。<br>如何分配：若某文件申请K个盘块，则从链头开始依次摘下K个盘块分配，并修改空闲链的链头指针。<br>如何回收：回收的盘块依次挂到链尾，并修改空闲链的链尾指针。</p><p>所以适用于离散分配的物理结构，为文件分配多个盘块可能需要多次操作。</p><p>对于空闲盘区链：</p><p><img src="https://files.seeusercontent.com/2026/04/02/q5As/image-20260402160001452.png" alt="image-20260402160001452"></p><p>操作系统保存着链头、链尾指针。</p><p>如何分配：若某文件申请K个盘块，则可以采用首次适应、最佳适应等算法，从链头开始检索按照算法规则找到一个大小符合要求的空闲盘区分配给文件。若没有合适的连续空闲块，也可以将不同盘区的盘块同时分配给一个文件，注意分配后可能要修改相应的链指针、盘区大小等数据。</p><p>如何回收：若回收区和某个空闲盘区相邻，则需要将回收区合并到空闲盘区中。若回收区没有和任何空闲区相邻，将回收区作为单独的一个空闲盘区挂到链尾。</p><p>离散分配和连续分配都适用。为一个文件分配多个盘块时效率更高。</p><h4 id="位示图法"><a href="#位示图法" class="headerlink" title="位示图法"></a>位示图法</h4><p>位示图：每个二进制位对应一个盘块。在本例中，“0”代表盘块空闲，“1”代表盘块已分配。位示图一般用连续的“字”来表示，如本例中一个字的字长是16位，字中的每一位对应一个盘块。因此可以用（字号，位号)对应一个盘块号。当然有的题目中也描述为（行号，列号）</p><p><img src="https://files.seeusercontent.com/2026/04/02/yvY5/image-20260402160625582.png" alt="image-20260402160625582"></p><p>我们需要了解盘块号与(字号，位号)之间的转换方法。</p><p>(字号，位号)=(i，j)的二进制位对应的盘块号b=ni+j<br>b号盘块对应的字号i=b/n,位号j=b%n</p><p>如何分配：若文件需要K个块，①顺序扫描位示图，找到K个相邻或不相邻的“0”；②根据字号、位号算出对应的盘块号，将相应盘块分配给文件：③将相应位设置为“1”。</p><p>如何回收：①根据回收的盘块号计算出对应的字号、位号；②将相应二进制位设为“0”</p><h4 id="成组链接法"><a href="#成组链接法" class="headerlink" title="成组链接法"></a>成组链接法</h4><p>空闲表法、空闲链表法不适用于大型文件系统，因为空闲表或空闲链表可能过大。UNIX系统中采用了成组链接法对磁盘空闲块进行管理。<br>文件卷的目录区中专门用一个磁盘块作为“超级块”，当系统启动时需要将超级块读入内存。并且要保证内存与外存中的“超级块”数据一致。</p><p><img src="https://files.seeusercontent.com/2026/04/02/9jIj/image-20260402161717016.png" alt="image-20260402161717016"></p><p>超级块有什么作用呢？</p><p>超级块用来记录下一组空闲盘块数量和空闲块号。</p><p>它的核心作用是<strong>将空闲磁盘块分组管理，通过极少的磁盘I/O操作就能完成空闲块的分配与回收，大幅提升效率</strong>。</p><p><strong>基本原理如下：</strong></p><ol><li><strong>分组存储</strong>：将所有空闲磁盘块分成若干组，比如每100个块为一组。</li><li><strong>栈式管理</strong>：每一组的第一个空闲块中，不存储用户数据，而是存储本组其余99个空闲块的块号，以及下一组第一个空闲块的块号（相当于指向下一组的指针）。</li><li><strong>超级块常驻内存</strong>：系统在内存的超级块中，只维护一个“当前组”的空闲块号栈。这个栈通常包含：<ul><li>本组空闲块的数量。</li><li>本组所有空闲块的块号列表。</li></ul></li></ol><p><img src="https://files.seeusercontent.com/2026/04/03/6oWd/image-20260403145338253.png" alt="image-20260403145338253"></p><p>那么如何分配呢？</p><p>假如现在需要100个空闲块，</p><ul><li>检查第一个分组块数是否足够，100=100，足够</li><li>分配第一个分组中的100个空闲块，但是由于300号块内存放了再下一组的信息，因此300号块的数据需要复制到超级块当中(修改指针)。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/03/uE9h/image-20260403145854513.png" alt="image-20260403145854513"></p><p>那么300号盘块就从空闲体系当中剥离，变成下图的：</p><p><img src="https://files.seeusercontent.com/2026/04/03/eK4c/image-20260403145935339.png" alt="image-20260403145935339"></p><p>如何回收？</p><p>假设每个分组最多为100个空闲块，此时第一个分组已有100个块，还要再回收一块。需要将超级块中的数据复制到新回收的块中，并修改超级块的内容，让新回收的块成为第一个分组。</p><p><img src="https://files.seeusercontent.com/2026/04/03/r9mQ/image-20260403150241791.png" alt="image-20260403150241791"></p><p>这样做读写速度很快，分配与回收只需要在内存中进行。</p><h3 id="文件的基本操作"><a href="#文件的基本操作" class="headerlink" title="文件的基本操作"></a>文件的基本操作</h3><h4 id="创建文件"><a href="#创建文件" class="headerlink" title="创建文件"></a>创建文件</h4><p>当我们选择创建文件时，图形化交互进程在背后调用了“create系统调用”。</p><p>使用create系统调用需要提供几个主要参数：</p><ul><li>所需的外存空间大小</li><li>文件存放路径</li><li>文件名</li></ul><p>操作系统需要完成：</p><ul><li>在外存中找到文件所需的空间。</li><li>根据文件存放路径的信息找到该目录对应的目录文件，在目录中创建该文件对应的目录项。目录项中包含了文件名、文件在外存中的存放位置等信息。</li></ul><h4 id="删除文件"><a href="#删除文件" class="headerlink" title="删除文件"></a>删除文件</h4><p>相反地，当我们选择删除文件时，图形化交互进程在背后调用了“Delete系统调用”</p><p>需要提供几个参数</p><ul><li>文件存放路径</li><li>文件名</li></ul><p>操作系统需要完成：</p><ul><li>根据文件存放路径找到相应的目录文件，从目<br>录中找到文件名对应的目录项</li><li>根据该目录项记录的文件在外存的存放位置、<br>文件大小等信息，回收文件占用的磁盘块<br>(回收磁盘块时，根据空闲表法、空闲链表法、位图法等管理策略的不同，需要做不同的处理</li><li>从目录表中删除文件对应的目录项</li></ul><h4 id="打开文件"><a href="#打开文件" class="headerlink" title="打开文件"></a>打开文件</h4><p>在很多操作系统中，在对文件进行操作之前，要求用户先使用open系统调用“打开文件”，需要提供的几个主要参数：</p><ul><li>文件存放路径</li><li>文件名</li><li>对文件的操作类型(只读，读写等。这里受用户权限限制。)</li></ul><p>操作系统在处理open系统调用时，主要做了几件事：</p><ul><li>根据文件存放路径找到相应的目录文件，从目录中找到文件名对应的的目录项，并检查该用户是否有指定的操作权限。</li><li>将目录项复制到内存中的“<strong>打开文件表</strong>”中。并将对应表目的编号返回给用户。之后用户使用打开文件表的编号来指明要操作的文件。</li></ul><p><img src="https://files.seeusercontent.com/2026/04/03/v3zC/image-20260403151731464.png" alt="image-20260403151731464"></p><p>值得一提的是，打开文件表有两种</p><ul><li>系统的打开文件表，只有一张</li><li>进程的打开文件表，每个进程都有。</li></ul><p>这两张表存在映射关系。</p><p><img src="https://files.seeusercontent.com/2026/04/03/pA4e/image-20260403151946586.png" alt="image-20260403151946586"></p><p>这么做能够很方便地实现同步，权限检查的操作。</p><h4 id="关闭文件"><a href="#关闭文件" class="headerlink" title="关闭文件"></a>关闭文件</h4><p>进程使用完文件后，要“关闭文件”<br>操作系统在处理Close系统调用时，主要做了几件事：</p><ul><li>将进程的打开文件表相应表项删除</li><li>回收分配给该文件的内存空间等资源</li><li>系统打开文件表的打开计数器count减1，若count=0,则删除对应表项。</li></ul><h4 id="读文件"><a href="#读文件" class="headerlink" title="读文件"></a>读文件</h4><p>可以“读文件”，将文件数据读入内存，才能让CPU处理（双击后，“记事本”应用程序通过操作系统提供的“读文件”功能，即read系统调用，将文件数据从外存读入内存，并显示在屏幕上)。</p><p><img src="https://files.seeusercontent.com/2026/04/03/Wfh7/image-20260403153128457.png" alt="image-20260403153128457"></p><p>进程使用read系统调用完成写操作。需要指明是哪个文件（在支持“打开文件”操作的系统中，只需要提供文件在打开文件表中的索引号即可)，还需要指明要读入多少数据（如：读入1KB)、指明读入的数据要放在内存中的什么位置。</p><p>操作系统在处理read系统调用时，会从读指针指向的外存中，将用户指定大小的数据读入用户指定的内存区域中。</p><h4 id="写文件"><a href="#写文件" class="headerlink" title="写文件"></a>写文件</h4><p>可以“写文件”，将更改过的文件数据写回外存（我们在“记事本”应用程序中编辑文件内容，点击“保存”后，“记事本”应用程序通过操作系统提供的“写文件”功能，即write系统调用，将文件数据从内存写回外存)</p><hr><p>进程使用write系统调用完成写操作，需要指明是哪个文件（在支持“打开文件”操作的系统中，只需要提供文件在打开文件表中的索引号即可)，还需要指明要写出多少数据（如：写出1KB)、写回外存的数据放在内存中的什么位置</p><p>操作系统在处理write系统调用时，会从用户指定的内存区域中，将指定大小的数据写回写指针指向的外存。</p><h3 id="文件共享"><a href="#文件共享" class="headerlink" title="文件共享"></a>文件共享</h3><p>操作系统为用户提供文件共享功能，可以让多个用户共享地使用同一个文件。</p><p>两种办法：</p><ul><li>基于索引结点的共享方式(硬链接)</li><li>基于符号链的共享方式(软链接)</li></ul><p>注意：多个用户共享同一个文件，意味着系统中只有“一份”文件数据。并且只要某个用户修改了该文件的数据，其他用户也可以看到文件数据的变化。如果是多个用户都“复制”了同一个文件，那么系统中会有“好几份”文件数据。其中一个用户修改了自己的那份文件数据，对其他用户的文件数据并没有影响。</p><h4 id="基于索引结点的共享方式"><a href="#基于索引结点的共享方式" class="headerlink" title="基于索引结点的共享方式"></a>基于索引结点的共享方式</h4><p>回忆，索引结点，是一种文件目录瘦身策略。由于检索文件时只需用到文件名，因此可以将除了文件名之外的其他信息放到索引结点中。这样目录项就只需要包含文件名、索引结点指针。</p><p><img src="https://files.seeusercontent.com/2026/04/03/cjK5/image-20260403154453756.png" alt="image-20260403154453756"></p><p>索引结点中设置一个链接计数变量count,用于表示链接到本索引结点上的用户目录项数。<br>若count=2,说明此时有两个用户目录项链接到该索引结点上，或赌说是有两个用户在共享此文件。<br>若某个用户决定“删除”该文件，则只是要把用户目录中与该文件对应的目录项删除，且索引结点的<br>count值减1。</p><p>若count&gt;0,说明还有别的用户要使用该文件，暂时不能把文件数据删除，否则会导致指针悬空。<br>当count=0时，也就是没有指针指向他时，系统负责删除文件。</p><h4 id="基于符号链的共享方式"><a href="#基于符号链的共享方式" class="headerlink" title="基于符号链的共享方式"></a>基于符号链的共享方式</h4><p><img src="https://files.seeusercontent.com/2026/04/03/kCb1/image-20260403154933085.png" alt="image-20260403154933085"></p><p>当User3访问“ccc”时，操作系统判断文件“ccc”属于Link类型文件，于是会根据其中记录的路径层层查找目录，最终找到User1的目录表中的“aaa”表项，于是就找到了文件i的索引结点。</p><p><img src="https://files.seeusercontent.com/2026/04/03/aa1Y/image-20260403155246376.png" alt="image-20260403155246376"></p><p>如果实际上指向的exe文件已经被删除，link型文件依旧存在，但是执行时会找不到目标文件。</p><p>由于用软链接的方式访问共享文件时要查询多级目录，会有多次磁盘I/O,因此用软链接访问</p><h3 id="文件保护"><a href="#文件保护" class="headerlink" title="文件保护"></a>文件保护</h3><p>三种：</p><ul><li>口令保护</li><li>加密保护</li><li>访问控制</li></ul><h4 id="口令保护"><a href="#口令保护" class="headerlink" title="口令保护"></a>口令保护</h4><p>为文件设置一个“口令”（如：bc112233),用户请求访问该文件时必须提供“口令”。</p><p>口令一般存放在文件对应的FCB或索引结点中。用户访问文件前需要先输入“口令”，操作系统会将用户提供的口令与FCB中存储的口令进行对比，如果正确，则允许该用户访问文件。</p><p>优点：保存口令的空间开销不多，验证口令的时间开销也很小。</p><p>缺点：正确的“口令”存放在系统内部，不够安全。</p><h4 id="加密保护"><a href="#加密保护" class="headerlink" title="加密保护"></a>加密保护</h4><p>使用某个“密码”对文件进行加密，在访问文件时需要提供正确的“密码”才能对文件进行正确的解密。</p><p>举个例子：使用异或加密：</p><p>假设用于加密/解密的密码是 “01001”。</p><p>使用”01001”逐个计算异或，就能够得到加密结果</p><p><img src="https://files.seeusercontent.com/2026/04/03/fY2c/image-20260403160511611.png" alt="image-20260403160511611"></p><p>解密也是一样的，在此不赘述了。</p><p>如果用户使用错误的密码是无法计算出有意义的文件数据的。</p><p>优点：保密性强，不需要在系统中存储“密码”</p><p>缺点：编码/译码，或者说加密/解密要花费一定时间。</p><h4 id="访问控制"><a href="#访问控制" class="headerlink" title="访问控制"></a>访问控制</h4><p>在每个文件的FCB(或索引结点)中增加一个访问控制列表(Access-Control List,ACL),该表中记录了各个用户可以对该文件执行哪些操作。</p><p><img src="https://files.seeusercontent.com/2026/04/03/2mrW/image-20260403160935805.png" alt="image-20260403160935805"></p><p>有的计算机可能会有很多个用户，因此访问控制列表可能会很大，可以用精简的访问列表解决这个问题</p><p>精简的访问列表：以“组”为单位，标记各“组”用户可以对文件执行哪些操作。如：分为系统管理员、文件主、文件主的伙伴、其他用户几个分组。当某用户想要访问文件时，系统会检查该用户所属的分组是否有相应的访问权限。</p><p><img src="https://files.seeusercontent.com/2026/04/03/tT2c/image-20260403161008866.png" alt="image-20260403161008866"></p><p>用户组的具体操作不在此赘述。</p><h3 id="文件系统的层次结构"><a href="#文件系统的层次结构" class="headerlink" title="文件系统的层次结构"></a>文件系统的层次结构</h3><p><img src="https://files.seeusercontent.com/2026/04/03/vm6G/image-20260403162100304.png" alt="image-20260403162100304"></p><p>用一个例子来辅助记忆文件系统的层次结构：<br>假设某用户请求删除文件“D:/工作目录/学生信息.xsx”的最后100条记录。<br>1.用户需要通过操作系统提供的接口发出上述请求一一用户接口<br>2.由于用户提供的是文件的存放路径，因此需要操作系统一层一层地查找目录，找到对应的目录<br>项一一文件目录系统<br>3.不同的用户对文件有不同的操作权限，因此为了保证安全，需要检查用户是否有访问权限一一<br>存取控制模块（存取控制验证层）<br>4.验证了用户的访问权限之后，需要把用户提供的“记录号”转变为对应的逻辑地址一一逻辑文<br>件系统与文件信息缓冲区<br>5.知道了目标记录对应的逻辑地址后，还需要转换成实际的物理地址一一物理文件系统<br>6.要删除这条记录，必定要对磁盘设备发出请求一一设备管理程序模块<br>7.删除这些记录后，会有一些盘块空闲，因此要将这些空闲盘块回收一一辅助分配模块</p><h3 id="文件系统布局"><a href="#文件系统布局" class="headerlink" title="文件系统布局"></a>文件系统布局</h3><h4 id="在外存的结构"><a href="#在外存的结构" class="headerlink" title="在外存的结构"></a>在外存的结构</h4><p>原始磁盘刚被生产出来时，内部没有被划分扇区。</p><p>第一步要做的事情是物理格式化：</p><p><img src="https://files.seeusercontent.com/2026/04/03/s5kS/image-20260403163452708.png" alt="image-20260403163452708"></p><p>又名低级格式化，划分扇区，检测坏扇区，并用备用扇区替换坏扇区。这一步对操作系统也是透明的。</p><p>随后是逻辑格式化，又名高级格式化。</p><p><img src="https://files.seeusercontent.com/2026/04/03/Rkn6/image-20260403163533989.png" alt="image-20260403163533989"></p><p>将磁人为地分为一个个分区，又名分卷，完成各分区的文件系统初始化。</p><p>注意：逻辑格式化后，灰色部分就有实际数据了，白色部分还没有数据。</p><p>位示图的作用用来表示空闲，但是超级块更多用来分配和回收。</p><p>i结点区，可以理解为一个超大数组，数组元素就是索引结点。</p><h4 id="在内存的结构"><a href="#在内存的结构" class="headerlink" title="在内存的结构"></a>在内存的结构</h4><p><img src="https://files.seeusercontent.com/2026/04/03/Psw5/image-20260403164507065.png" alt="image-20260403164507065"></p><blockquote><p>文件描述符和文件句柄都是操作系统用于管理和访问文件等I/O资源的抽象标识。</p><p><strong>文件描述符</strong> 是Unix/Linux等类Unix操作系统中的概念。它是一个非负整数（如0, 1, 2），是进程打开文件表的一个索引。标准输入、输出、错误对应的描述符分别是0、1、2。</p></blockquote><ul><li>目录的缓存</li></ul><p>注：近期访问过的目录文件会缓存在内存中，不用每次都从磁盘读入，这样可以加快目录检索速度</p><ul><li>打开文件表</li><li>①open(./M/A,只读)；根据路径一级一级读入目录</li><li>②找到目标文件的FCB，,复制到系统打开文件表</li><li>③在进程打开文件表中新建一个条目并返回<strong>文件描述符</strong></li><li>操作系统根据文件描述符，从进程打开文件表当中查找系统打开文件表的索引，然后再在系统打开文件表当中的FCB从而得知其位置。</li></ul><h3 id="虚拟文件系统"><a href="#虚拟文件系统" class="headerlink" title="虚拟文件系统"></a>虚拟文件系统</h3><p>电脑并不是只有一种文件系统，U盘，固态，磁盘可能都不是统一的文件系统。不一致的文件系统就意味着需要不同的系统调用，这是一件很麻烦的事情。</p><p><img src="https://files.seeusercontent.com/2026/04/04/s1Lt/image-20260404200748902.png" alt="image-20260404200748902"></p><p>这是，统一文件操作接口的工作就十分重要。这就催生了虚拟文件系统 。</p><p><img src="https://files.seeusercontent.com/2026/04/04/h3Bv/image-20260404200826126.png" alt="image-20260404200826126"></p><p>虚拟文件系统的特点：</p><p>①向上层用户进程提供统一标准的系统调用接口，屏蔽底层具体文件系统的实现差异</p><p>②VFS要求下层的文件系统必须实现某些规定的函数功能，如：open/read/write。一个新的文件系统想要在某操作系统上被使用，就必须满足该操作系统VFS的要求</p><p>③每打开一个文件，VFS就在主存中新建一个<strong>vnode (Virtual Node)</strong>,用统一的数据结构表示文件，无论该文件存储在哪个文件系统。</p><p>举个例子，UFS文件系统和FAT文件系统的目录项差别是很大的。他们表示文件的数据结构不同，那么在主存的表示就不一样 。</p><p>在虚拟文件系统中，会有一个新的，统一记录文件的数据结构，不管访问的是什么文件系统，都能够将其信息填入统一的数据结构，实现统一访问。</p><p><img src="https://files.seeusercontent.com/2026/04/04/yA9c/image-20260404201802865.png" alt="image-20260404201802865">其中的“函数功能指针”就是指向对应文件系统的功能函数。</p><p><img src="https://files.seeusercontent.com/2026/04/04/8Amh/image-20260404202515925.png" alt="image-20260404202515925"></p><h3 id="文件系统挂载"><a href="#文件系统挂载" class="headerlink" title="文件系统挂载"></a>文件系统挂载</h3><p>文件系统挂载(mounting)，即文件系统安装/装载。</p><p>文件系统挂载要做的事：</p><ul><li>在VFS中注册新挂载的文件系统。内存中的挂载表(mount table)包含每个文件系统的相关信息，包括文件系统类型、容量大小等。</li><li>新挂载的文件系统，要向VFS提供一个函数地址列表</li><li>将新文件系统加到挂载点(mount point),也就是将新文件系统挂载在某个父目录下。</li></ul><p>第四章到此结束</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;操作系统：第四章&quot;&gt;&lt;a href=&quot;#操作系统：第四章&quot; class=&quot;headerlink&quot; title=&quot;操作系统：第四章&quot;&gt;&lt;/a&gt;操作系统：第四章&lt;/h1&gt;&lt;h3 id=&quot;初识文件管理&quot;&gt;&lt;a href=&quot;#初识文件管理&quot; class=&quot;headerli</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>操作系统第三章：内存</title>
    <link href="https://blog.willmo.top/posts/Operating%20System%20Chapter%203:%20Memory/"/>
    <id>https://blog.willmo.top/posts/Operating%20System%20Chapter%203:%20Memory/</id>
    <published>2026-04-20T07:12:32.226Z</published>
    <updated>2026-04-20T07:12:33.409Z</updated>
    
    <content type="html"><![CDATA[<h1 id="操作系统第三章"><a href="#操作系统第三章" class="headerlink" title="操作系统第三章"></a>操作系统第三章</h1><blockquote><p>恭喜你已经度过了漫长的进程，但这个章节更加抽象。</p></blockquote><h3 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h3><p>内存是计算机中用于临时存储正在运行的程序和所需数据的硬件部件。详细解释在计算机组成原理的笔记中。</p><p>内存可存放数据。程序执行前需要先放到内存中才能被CPU处理，以缓和CPU与硬盘之间的速度矛盾。</p><p>我们需要区分各个程序的数据是放在什么地方，所以就需要给内存的存储单元编地址。</p><p>关于按字节编址和按字编址：</p><p><img src="https://files.seeusercontent.com/2026/03/24/Ibb6/image-20260324152529861.png" alt="image-20260324152529861"></p><p>$$<br>{2^{10}=1K}\\ {2^{20}=1M}\ {2^{30}=1G}\<br>$$</p><p>所以，一台手机/电脑有4GB内存，是指该内存中可以存放4 * 2^30个字节。如果是按字节编址的话，也就是有4*2^30=2^32个“小房间，也就是<strong>42 亿个字节单元</strong>。</p><p>编址是很重要的，增删改查都是通过地址进行的。</p><p><img src="https://files.seeusercontent.com/2026/03/24/x3Io/image-20260324153738790.png" alt="image-20260324153738790"></p><p><img src="https://files.seeusercontent.com/2026/03/24/Dv5o/image-20260324154100675.png" alt="image-20260324154100675"></p><p>如果是绝对寻址，写入到79的操作是危险的，因为79很可能是分配给其他进程使用的，这会干扰其他进程的正常运行。</p><p>在内存管理这个章节中，我们需要了解三种装入方式：</p><ul><li>绝对装入</li><li>可重定位装入</li><li>动态运行时装入</li></ul><h4 id="绝对装入"><a href="#绝对装入" class="headerlink" title="绝对装入"></a>绝对装入</h4><blockquote><p>单道程序阶段，操作系统未诞生阶段。</p></blockquote><p>在编译时，如果知道程序将放到内存中的哪个位置，编译程序将产生<strong>正确的</strong>绝对地址的目标代码。装入程序按照装入模块中的地址，将程序和数据装入内存。</p><p>灵活性很差，只适用于单道程序环境。</p><p><img src="https://files.seeusercontent.com/2026/03/24/x0Fh/image-20260324154928062.png" alt="image-20260324154928062"></p><h4 id="可重定位装入"><a href="#可重定位装入" class="headerlink" title="可重定位装入"></a>可重定位装入</h4><blockquote><p>用于早期多道批处理操作系统</p></blockquote><p>静态重定位：又称可重定位装入。编译、链接后的装入模块的地址都是从0开始的，指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。可根据内存的当前情况，将装入模块装入到内存的适当位置。装入时对地址进行“重定位”，将逻辑地址变换为物理地址（地址变换是在装入时一次完成的)。</p><p><img src="https://files.seeusercontent.com/2026/03/24/xX8o/image-20260324155012698.png" alt="image-20260324155012698"></p><p>静态重定位的特点是在一个作业装入内存时，必须分配其要求的全部内存空间，如果没有足够的内存，就不能装入该作业。</p><p>理解这一点需要虚拟化技术，不用急。</p><h4 id="动态运行时装入"><a href="#动态运行时装入" class="headerlink" title="动态运行时装入"></a>动态运行时装入</h4><blockquote><p>灵活性高，用于现代操作系统</p></blockquote><p>动态重定位：又称动态运行时装入。编译、链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后，并不会立即把逻辑地址转换为物理地址，而是把地址转换推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持。</p><p><img src="https://files.seeusercontent.com/2026/03/24/9raI/image-20260324155406352.png" alt="image-20260324155406352"></p><p>动态重定位允许程序在内存中发生移动并且可将程序分配到不连续的存储区中；在程序运行前只需装入它的部分代码即可投入运行，然后在程序运行期间，根据需要动态申请分配内存；便于程序段的共享，可以向用户提供一个比存储空间大得多的地址空间。</p><h4 id="从程序编写到程序运行"><a href="#从程序编写到程序运行" class="headerlink" title="从程序编写到程序运行"></a>从程序编写到程序运行</h4><p><img src="https://files.seeusercontent.com/2026/03/24/Axp7/image-20260324160030595.png" alt="image-20260324160030595"></p><p>编译：由编译程序将用户源代码编译成若干个目标模块（编译就是把高级语言翻译为机器语言）</p><p>链接：由链接程序将编译后形成的一组目标模块，以及所需库函数链接在一起，形成一个完整的装入模块</p><p>装入（装载）：由装入程序将装入模块装入内存运行</p><p>我们需要三种链接方式：</p><ul><li>静态链接：在程序运行之前，先将各目标模块及它们所需的库函数连接成一个完整的可执行文件（装入模块），之后不再拆开。</li><li>装入时动态链接：将各目标模块装入内存时，边装入边链接的链接方式。</li><li>运行时动态链接：在程序执行中需要该目标模块时，才对它进行链接。其优点是便于修改和更新，便于实现对目标模块的共享。</li></ul><p>最后一种，如果全程用不到某个模块就不会被链接装入，灵活性更高，对内存的利用 也更充分。</p><h3 id="内存管理"><a href="#内存管理" class="headerlink" title="内存管理"></a>内存管理</h3><p>操作系统作为系统资源的管理者，也必须对内存进行管理。分为以下几个方面：</p><ul><li>操作系统负责内存空间的分配与回收</li><li>操作系统需要提供某种技术从逻辑上对内存空间进行扩充(虚拟化)</li><li>操作系统需要提供地址转换功能，负责程序的逻辑地址与物理地址的转换(装入)</li><li>操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行，互不千扰</li></ul><h4 id="内存保护"><a href="#内存保护" class="headerlink" title="内存保护"></a>内存保护</h4><p>指的是限制某个进程只能在自己的划分空间内操作，而无法访问或者是修改操作系统内核等高权限空间和其他进程的无关空间。</p><p><img src="https://files.seeusercontent.com/2026/03/24/xv7B/image-20260324192945344.png" alt="image-20260324192945344"></p><p>实现内存保护可以有以下：</p><ul><li>上下限寄存器法：在CPU中设置一对上、下限寄存器，存放进程的上、下限地址。进程的指令要访问某个地址时，CPU检查是否越界。</li><li>双寄存器法：采用重定位寄存器（又称基址寄存器）和界地址寄存器（又称限长寄存器）进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址。</li></ul><h3 id="进程的内存映像"><a href="#进程的内存映像" class="headerlink" title="进程的内存映像"></a>进程的内存映像</h3><p>我们结合一段C语言代码来理解。</p><p><img src="https://files.seeusercontent.com/2026/03/24/5jRq/image-20260324195501943.png" alt="image-20260324195501943"></p><p>值得一提的是，宏定义的量X不会被专门分配到存储空间，而是在预编译阶段被替换为指定值1024，相当于隐含在程序指令当中。换句话说，X不作为变量存在，他只是1024的别称。</p><p><img src="https://files.seeusercontent.com/2026/03/24/hEd9/image-20260324195522812.png" alt="image-20260324195522812"></p><h3 id="覆盖与交换"><a href="#覆盖与交换" class="headerlink" title="覆盖与交换"></a>覆盖与交换</h3><blockquote><p>这两个技术已经进入历史了。</p></blockquote><h4 id="覆盖"><a href="#覆盖" class="headerlink" title="覆盖"></a>覆盖</h4><p>早期的计算机内存很小，比如BM推出的第一台PC机最大只支持1MB大小的内存。因此经常会出现内存大小不够的情况。对此，人们引入了覆盖技术。</p><p>核心思想:覆盖技术的思想：将程序分为多个段（多个模块）。常用的段常驻内存，不常用的段在需要时调入内存。</p><p>所以，内存中分为一个“固定区”和若干个“覆盖区”。需要常驻内存的段放在“固定区”中，调入后就不再调出（除非运行结束）<br>不常用的段放在’覆盖区”，需要用到时调入内存，用不到时调出内存。</p><p><img src="https://files.seeusercontent.com/2026/03/25/d4Zq/image-20260325123743674.png" alt="image-20260325123743674"></p><p>明显的缺点就是：程序的调用结构必须由程序员声明，这很麻烦。</p><h4 id="交换"><a href="#交换" class="headerlink" title="交换"></a>交换</h4><p>交换（对换）技术的设计思想：内存空间紧张时，系统将内存中某些进程暂时换出外存(挂起)，把外存中某些已经具备运行条件的进程换入内存（进程在内存与磁盘间动态调度）。</p><p>本质上就是中级调度(内存调度)。</p><p><img src="https://files.seeusercontent.com/2026/03/25/Hmc7/image-20260325124220213.png" alt="image-20260325124220213"></p><p>被换下的进程需要放入外存，那么对磁盘就需要分区：</p><p><img src="https://files.seeusercontent.com/2026/03/25/9tuN/image-20260325124720668.png" alt="image-20260325124720668"></p><p>具有对换功能的操作系统中，通常把磁盘空间分为文件区和对换区两部分。</p><p>文件区主要用于存放文件，主要追求存储空间的利用率，因此对文件区空间的管理采用离散分配方式；</p><p>对换区空间只占磁盘空间的小部分，被换出的进程数据就存放在对换区。</p><p>由于对换的速度直接影响到系统的整体速度，因此对换区空间的管理主要追求换入换出速度，因此通常对换区采用连续分配方式（学过文件管理章节后即可理解)。总之，对换区的I/O速度比文件区的更快。</p><p>交换通常在许多进程运行且内存吃紧时进行，而系统负荷降低就暂停。例如：在发现许多进程运行时经常发生缺页，就说明内存紧张，此时可以换出一些进程；如果缺页率明显下降，就可以暂停换出。</p><p>在实际运用中，可优先换出阻塞进程；可换出优先级低的进程；为了防止优先级低的进程在被调入内存后很快又被换出，有的系统还会考虑进程往内存的驻留时间。</p><p>但是PCB处于内核层，不会被换出外存。</p><h3 id="内存空间的分配与回收"><a href="#内存空间的分配与回收" class="headerlink" title="内存空间的分配与回收"></a>内存空间的分配与回收</h3><h4 id="连续分配管理方式"><a href="#连续分配管理方式" class="headerlink" title="连续分配管理方式"></a>连续分配管理方式</h4><ul><li><strong>单一连续分配</strong></li></ul><p>在单一连续分配方式中，内存被分为系统区和用户区。系统区通常位于内存的低地址部分，用于存放操作系统相关数据；用户区用于存放用户进程相关数据。内存中只能有一道用户程序，<strong>连并发都不支持</strong>，用户程序独占整个用户区空间。</p><p><img src="https://files.seeusercontent.com/2026/03/24/hf3C/image-20260324200741336.png" alt="image-20260324200741336"></p><p>优点：实现简单；无外部碎片；可以采用覆盖技术扩充内存；不一定需要采取内存保护(eg:早期的Pc操作系统MS-DOS)</p><p>缺点：不支持并发，且有一些分配给用户进程但是没有利用上的“内部碎片”被浪费掉了，存储器利用率很低。</p><ul><li><strong>固定分区分配</strong></li></ul><p>将整个用户空间划分为若干个固定大小或者不等的分区，在每个分区中只装入一道作业，这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式。</p><p><img src="https://files.seeusercontent.com/2026/03/24/k4Hx/image-20260324201136659.png" alt="image-20260324201136659"></p><p>分区大小相等：缺乏灵活性，但是很适合用于用一台计算机控制多个相同对象的场合（比如：钢铁厂有个相同的炼钢炉，就可把内存分为个大小相等的区域存n个炼钢炉控制程序)<br>分区大小不等：增加了灵活性，可以满足不同大小的程需求。根据常在系统中运行的作业大小情况进行划分(比如：划分多个小分区、适量中等分区、少量大分区)</p><p>如果是不等分区大小，就需要在内核区建立一个数据结构“分区说明表”，用来记录分区的大小，起始地址和状态等信息。</p><p><img src="https://files.seeusercontent.com/2026/03/24/Yql5/image-20260324201806905.png" alt="image-20260324201806905"></p><p>优点：实现简单，没有外部碎片</p><p>缺点：当程序太大就只能使用覆盖技术来解决，且会产生内部碎片，利用率不是特别高</p><ul><li><strong>动态分区分配</strong></li></ul><p>动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区，而是在进程装入内存时，根据进程的大小动态地建立分区，并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。(eg:假设某计算机内存大小为64MB,系统区8MB,用户区共56MB.)</p><p><img src="https://files.seeusercontent.com/2026/03/24/Xp2s/image-20260324202747572.png" alt="image-20260324202747572"></p><p>对此，操作系统会使用空闲分区表或者空闲分区链来记录内存使用情况。</p><p><img src="https://files.seeusercontent.com/2026/03/24/5Ccp/image-20260324202901094.png" alt="image-20260324202901094"></p><p>把一个新作业装入内存时，需要解决“多个 分区都能满足要求时，应该选择哪个分区来满足进程需求？”的问题。</p><p>所以须按照一定的动态分区分配算法，从空闲分区表（或空闲分区链）中选出一个分区分配给该作业。分配算法对系统性能有很大的影响。下一节我们会讲到。</p><p>算法需要将待分配的空闲分区分割，回收到的空闲分区自动合并，并在内存分区数据结构中更改。</p><p>动态分区分配没有内部碎片，但是有外部碎片。</p><ul><li>内部碎片，分配给某进程的内存区域中，如果有些部分没有用上。</li><li>外部碎片，是指内存中的某些空闲分区由于太小而难以利用。</li></ul><p>如果算法划分出4个12M的内存空间，但是这个破碎的内存空间无法被任何软件利用，那就是浪费了。</p><p><img src="https://files.seeusercontent.com/2026/03/25/Uo1t/image-20260325130237756.png" alt="image-20260325130237756"></p><p>但是我们能够通过紧凑技术将碎片拼凑起来，来减少外部碎片。</p><h3 id="动态分区分配算法"><a href="#动态分区分配算法" class="headerlink" title="动态分区分配算法"></a>动态分区分配算法</h3><p><strong>在动态分区分配方式中，当很多个空闲分区都能满足需求时，应该选择哪个分区进行分配？</strong>解决方式有：</p><ul><li>首次适应算法</li></ul><p><strong>每次都从低地址开始查找，找到第一个能满足大小的空闲分区。</strong></p><p>也就是空闲分区以<strong>地址递增</strong>的次序排列。每次分配内存时顺序查找空闲分区链（或空闲分区表)，找到大小能满足要求的第一个空闲分区。</p><p>新进程需要9M，根据地址从小到大有1M ,60M，9M，那就近原则优先拆解60M的空间为9M和51M，给进程利用。</p><ul><li>最佳适应算法</li></ul><p><strong>由于动态分区分配是一种连续分配方式，为各进程分配的空间必须是连续的一整片区域。因此为了保证当“大进程”到来时能有连续的大片空间，可以尽可能多地留下大片的空闲区即，优先使用更小的空闲区。</strong></p><p>也就是空闲分区按<strong>容量递增</strong>次序链接。每次分配内存时顺序查找空闲分区链（或空闲分区表)，找到大小能满足要求的第一个空闲分区。</p><p>新进程需要9M，空闲有10M ,60M 79M，那就会优先拆解10M的空间为9M和1M，给进程利用。</p><p>缺点是：每次都选最小的分区进行分配，会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。</p><ul><li>最坏适应算法，上者的反面</li></ul><p>又称最大适应算法。为了解决最佳适应算法的问题一一即留下太多难以利用的小碎片，可以在每次分配时优先使用最大的连续空闲区，这样分配后剩余的空闲区就不会太小，更方便使用。</p><p>具体做法是：空闲分区按容量递减次序链接。每次分配内存时顺序查找空闲分区链（或空闲分区表)，找到大小能满足要求的第一个空闲分区。</p><p>新进程需要9M，空闲有10M ,60M 79M，那就会优先拆解79M的空间为9M和70M，给进程利用。</p><p>缺点：每次都选最大的分区进行分配，虽然可以让分配后留下的空闲区更大，更可用，但是这种方式会导致较大的连续空闲区被迅速用完。如果之后有“大进程”到达，就没有内存分区可用了。</p><ul><li>邻近适应算法</li></ul><p>思想：首次适应算法每次都从链头开始查找的。这可能会导致低地址部分出现很多小的空闲分区，而每次分配查找时，都要经过这些分区，因此也增加了查找的开销。如果每次都从上次查找结束的位置开始检索，就能解决上述问题。</p><p>如何实现：空闲分区以地址递增的顺序排列（可排成一个循环链表）。每次分配内存时从上次查找结束的位置开始查找空闲分区链（或空闲分区表），找到大小能满足要求的第一个空闲分区。</p><p><img src="https://files.seeusercontent.com/2026/03/25/ha0T/image-20260325133220030.png" alt="image-20260325133220030"></p><p>这是一个折中的算法：</p><p>首次适应算法每次都要从头查找，每次都需要检索低地址的小分区。但是这种规则也决定了当低地址部分有更小的分区可以满足需求时，会更有可能用到低地址部分的小分区，也会更有可能把高地址部分的大分区保留下来（最佳适应算法的优点）邻近适应算法的规则可能会导致无论低地址、高地址部分的空闲分区都有相同的概率被使用，也就导致了高地址部分的大分区更可能被使用，划分为小分区，最后导致无大分区可用（最大适应算法的缺点）</p><p>做一个表格横向对比：</p><table><thead><tr><th>算法</th><th>算法思想</th><th>分区排列顺序</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>首次适应</td><td>从头到尾找适合的 分区</td><td>空闲分区以地址 递增次序排列</td><td>综合看性能最好。算 法开销小，回收分区 后一般不需要对空闲 分区队列重新排序</td><td></td></tr><tr><td>最佳适应</td><td>优先使用更小的分 区，以保留更多大 分区</td><td>空闲分区以容量 递增次序排列</td><td>会有更多的大分区被 保留下来，更能满足 大进程需求</td><td>会产生很多太小的、难以 利用的碎片；算法开销大， 回收分区后可能需要对空 闲分区队列重新排序</td></tr><tr><td>最坏适应</td><td>优先使用更大的分 区，以防止产生太 小的不可用的碎片</td><td>空闲分区以容量 递减次序排列</td><td>可以减少难以利用的 小碎片</td><td>大分区容易被用完，不利 于大进程；算法开销大 （原因同上）</td></tr><tr><td>邻近适应</td><td>由首次适应演变而 来，每次从上次查 找结束位置开始查 找</td><td>空闲分区以地址 递增次序排列 （可排列成循环 链表）</td><td>不用每次都从低地址 的小分区开始检索。 算法开销小（原因同 首次适应算法)</td><td>会使高地址的大分区也被 用完</td></tr></tbody></table><h3 id="基本分页存储管理"><a href="#基本分页存储管理" class="headerlink" title="基本分页存储管理"></a>基本分页存储管理</h3><blockquote><p>属于非连续分配管理方式，这是难点。</p></blockquote><h4 id="分页存储"><a href="#分页存储" class="headerlink" title="分页存储"></a>分页存储</h4><p>将<strong>内存空间</strong>分为一个个大小相等的分区（比如：每个分区 4KB),每个分区就是一个“页框”（页框=页帧=内存块=物理块=物理页面)。每个页框有一个编号，即“页框号（页框号=页帧号=内存块号=物理块号=物理页号)，页框号从0开始。</p><p>将<strong>进程的逻辑地址空间</strong>也分为与页框大小相等的一个个部分，每个部分称为一个“页”或“页面”。每个页面也有一个编号，即“页号”，页号也是从0开始。</p><blockquote><p><strong>页面</strong> 是<strong>虚拟内存</strong>中的固定大小块，是进程视角看到的“数据单元”。</p></blockquote><blockquote><p><strong>页框</strong> 是<strong>物理内存</strong>中的固定大小块，是实际存放数据的“物理容器”。</p></blockquote><p>就好像将作家写的文章变成书一样，需要书页来分割。</p><p>我们需要明确二者的关系：<strong>逻辑地址(又称虚拟地址)是程序使用的地址，由操作系统通过内存管理单元（MMU）和页表等机制，动态映射到实际的物理内存地址上。</strong></p><p>初学易混一一页、页面vs页框、页帧、物理页页号、页面号和页框号、页帧号、物理页号。请了解完毕再前进。</p><p><img src="https://files.seeusercontent.com/2026/03/25/3Eum/image-20260325134455727.png" alt="image-20260325134455727"></p><p>操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说，进程的页面与内存的页框有一一对应的关系。</p><p>一样地，划分操作也需要使用数据结构“页表”。</p><p><img src="https://files.seeusercontent.com/2026/03/25/s0Qt/image-20260325134923512.png" alt="image-20260325134923512"></p><p>值得一提的是，内存块就是页框，页框也是英文书里的直译。</p><p>这种结构实际上表达了页框与页面的映射对应关系。</p><p>Eg:假设某系统物理内存大小为4GB,页面大小为4KB,则每个页表项至少应该为多少字节？</p><p>内存块大小=页面大小=4KB。</p><p>那么4GB/4KB就是2^20个 内存块</p><p>那内存块号应该就是0-2^(20-1)</p><p>至少需要20bit表示，也就是3B。</p><p>但是页号是不需要占内存空间的，因为其是连续的。</p><p><img src="https://files.seeusercontent.com/2026/03/25/9viL/image-20260325150059775.png" alt="image-20260325150059775"></p><p>就好像数组的下标是不需要空间来存放一样。</p><p>但请记住，页表记录的只是内存块号，而不是内存块的起始地址，j号内存块的起始地址是j*内存块大小。</p><h4 id="地址转换"><a href="#地址转换" class="headerlink" title="地址转换"></a>地址转换</h4><p>进程在内存中连续存放时，操作系统是如何实现逻辑地址到物理地址的转换的？</p><p><img src="https://files.seeusercontent.com/2026/03/25/zaO6/image-20260325150416243.png" alt="image-20260325150416243"></p><p>那么进程地址空间分页之后，操作系统又如何实现地址转换呢？</p><p>特点：虽然进程的各个页面是离散存放的，但是页面内部是连续存放的</p><p>如果要访问逻辑地址A,则</p><ul><li>确定逻辑地址A对应的“页号”P</li><li>找到P号页面在内存中的起始地址（需要查页表)</li><li>确定逻辑地址A的“页内偏移量”W</li></ul><p>逻辑地址A对应的物理地址=P号页面在内存中的起始地址+页内偏移量W。</p><p>我们来讲解如何获取页号P，起始地址和页内偏移量W。</p><p>Eg：在某计算机系统中，页面大小是50B。某进程逻辑地址空间大小为200B，则逻辑地址110 对应的页号、页内偏移量是多少？</p><p>进程将被分为4个页面。</p><p>页号=逻辑地址/页面长度（取除法的整数部分）。<br>页内偏移量=逻辑地址%页面长度（取除法的余数部分）。</p><p>起始地址存在于页表中，只需要查页表即可。</p><p>所以，页号=110/50=2<br>页内偏移量=110%50=10</p><p>因为页面的地址是连续存放的，那么相除就能够表示所在区间，取余就能够表示偏移量。</p><p>以上是人工计算的办法，计算机内部需要进制计算，那样快很多：</p><p>假设计算机用32个二进制位表示逻辑地址，页面大小为4KB  = 2^12B = 4096B</p><p>请回忆计算机组成原理的指令设计章节。</p><p><img src="https://files.seeusercontent.com/2026/03/27/k0cE/image-20260327142718612.png" alt="image-20260327142718612"></p><p>Eg:逻辑地址2，用二进制表示应该是00000000000000000000000000000010<br>页号=2/4096=0=00000000000000000000，页内偏移量=2%4096=2=000000000010</p><p>Eg:逻辑地址4097，用二进制表示应该是00000000000000000001000000000001<br>页号=4097/4096=1=00000000000000000001，页内偏移量=4097%4096=1=000000000001</p><p>所以，如果每个界面大小为2^kB，用二进制数表示逻辑地址，则末尾K位即为页内偏移量，其余就是页号。将逻辑地址拆分就能快速确定页号P和页内偏移量W。</p><p>但是请注意：</p><p><img src="https://files.seeusercontent.com/2026/03/27/8Mqo/image-20260327143405813.png" alt="image-20260327143405813"></p><p>根据页号可以查询页表，而页表中记录的<br>只是内存块号，而不是内存块的起始地址。<br>J号内存块的起始地址=J*内存块大小</p><p>所以，页面大小刚好是2的整数次幂有很多好处：</p><ul><li>逻辑地址的拆分更加迅速一一如果每个页面大小为2^KB,用二进制数表示逻辑地址，则末尾K位即为页内偏移量，其余部分就是页号。因此，如果让每个页面的大小为2的整数幂，计算机硬件就可以很方便地得出一个逻辑地址对应的页号和页内偏移量，而无需进行除法运算，从而提升了运行速度。</li><li>物理地址的计算更加迅速一一根据逻辑地址得到页号，根据页号查询页表从而找到页面存放的内存块号，将二进制表示的内存块号和页内偏移量拼接起来，就可以得到最终的物理地址。</li></ul><p>也许需要看看计组才能理解这一块的本质。</p><p><img src="https://files.seeusercontent.com/2026/03/27/cNe3/image-20260327143710448.png" alt="image-20260327143710448"></p><h3 id="基本地址变换机构"><a href="#基本地址变换机构" class="headerlink" title="基本地址变换机构"></a>基本地址变换机构</h3><p>基本地址变化机构就是用于<strong>实现逻辑地址到物理地址转换</strong>的一组硬件机构，能够借助进程的页表将逻辑地址转换为物理地址。</p><p>通常会在系统中设置一个页表寄存器(PTR),存放页表在内存中的起始地址F和页表长度M。<br>进程未执行时，页表的始址和页表长度放在进程控制块(PCB)中，当进程被调度时，操作系统内核会把它们放到页表寄存器中。</p><p>设页面大小为L，逻辑地址A到物理地址E的变换过程如下：</p><p><img src="https://files.seeusercontent.com/2026/03/27/7Ccq/image-20260327145421158.png" alt="image-20260327145421158"></p><p>首先，内存中被划分为系统区和用户区，页表寄存器中记录页表初始地址F(在物理内存中的起始地址)和页表长度M(M是页表中所包含的<strong>页表项</strong>总数，表示该进程有多少逻辑页面)。<strong>页表项是页表中的一个基本单元，它记录了操作系统为进程的一个逻辑页面分配的物理内存块的信息。页表项数量通常就等于进程的逻辑页面数</strong></p><p>如果想访问的页号超出了M，那就是非法越界访问，那就会触发内部中断。</p><p>那么通过页表知道了页号和内存块号的键值对，就能够知道物理地址E,那就能够顺利访问物理内存了。</p><p>所以整个过程是5件事：</p><ul><li>根据逻辑地址计算出页号，页内偏移量</li><li>判断是否越界</li><li>查询页表，找到对应的页表项，确定页面存放的内存块号</li><li>用内存块号和页内偏移量得到物理地址</li><li>访问目标内存单元</li></ul><p>使用文字重复一遍这个过程：</p><p>①计算页号P和页内偏移量W(如果用十进制数手算，则P=A/L,W=A%L;但是在计算机实际<br>运行时，逻辑地址结构是固定不变的，因此计算机硬件可以更快地得到二进制表示的页号、页内偏移量)<br>②比较页号P和页表长度M,若P≥M,则产生越界中断，否则继续执行。（注意：页号是从0开<br>始的，而页表长度至少是1，因此P=M时也会越界)<br>③页表中页号P对应的页表项地址=页表起始地址F+页号P<em>页表项长度，取出该页表项内容b,<br>即为内存块号。（注意区分页表项长度、页表长度、页面大小的区别。页表长度指的是这个页表中总共有几个页表项，即总共有几个页：页表项长度指的是每个页表项占多大的存储空间；页面大小指的是个页面占多大的存储空间)<br>④计算E=b</em>L+W,用得到的物理地址E去访存。（如果内存块号、页面偏移量是用二进制表<br>示的，那么把二者拼接起来就是最终的物理地址了)</p><blockquote><p>也许我应该加一个折叠的。</p></blockquote><p>这里有一道题，没写上来。。</p><h4 id="对页表项大小的进一步探讨"><a href="#对页表项大小的进一步探讨" class="headerlink" title="对页表项大小的进一步探讨"></a>对页表项大小的进一步探讨</h4><p>每个页表项的长度是相同的，页号是“隐含”的。</p><p><img src="https://files.seeusercontent.com/2026/03/27/ueR9/image-20260327152106299.png" alt="image-20260327152106299"></p><h3 id="具有快表的地址变换机构"><a href="#具有快表的地址变换机构" class="headerlink" title="具有快表的地址变换机构"></a>具有快表的地址变换机构</h3><p>基本地址变换机构的改进版本，能够让转换的过程变快。</p><p>快表，又称联想寄存器(TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存(TLB不是内存！)，用来存放最近访问的页表项的副本，可以加速地址变换的速度。与此对应，内存中的页表常称为慢表。</p><p>回顾一下存储金字塔：</p><p><img src="https://files.seeusercontent.com/2026/03/27/6Ktf/image-20260327153338077.png" alt="image-20260327153338077"></p><p>回忆缓存的工作原理。</p><p><img src="https://files.seeusercontent.com/2026/03/27/lx3Z/image-20260327154855134.png" alt="image-20260327154855134"></p><p>进程第一次上处理机运行时，快表是空的，执行第一条指令时，判断没有越界，后检查空快表，自然不会命中，随即用内存中的页表计算出内存地址后访问内存，然后将访问的那一页放入快表。要访问下一个地址时，判断没有越界后，发现快表命中就能够直接在快表中计算出内存地址并访问，这就实现了基于快表的加速。请回忆局部性原理。</p><p>分步重复这个过程就是：</p><p>①CPU给出逻辑地址，由某个硬件算得页号、页内偏移量，将页号与快表中的所有页号进行比较。<br>②如果找到匹配的页号，说明要访问的页表项在快表中有副本，则直接从中取出该页对应的内存块<br>号，再将内存块号与页内偏移量拼接形成物理地址，最后，访问该物理地址对应的内存元。因此，若快表命中，则访问某个逻辑地址仅需一次访存即可。<br>③如果没有找到匹配的页号，则需要访问内存中的页表，找到对应页表项，得到页面存放的内存块号，再将内存块号与页内偏移量拼接形成物理地址，最后，访问该物理地址对应的内存单元。因此，若快表未命中，则访问某个逻辑地址需要两次访存（注意：在找到页表项后，应同时将其存入快表，<br>以便后面可能的再次访问。但若快表己满，则必须按照一定的算法对旧的页表项进行替换)</p><p>有的系统可能支持快表和慢表同时查找，这样在快表未命中的时候能够节省一个遍历快表的时间。<img src="https://files.seeusercontent.com/2026/03/27/gDs9/image-20260327160207339.png" alt="image-20260327160207339"></p><h4 id="局部性原理"><a href="#局部性原理" class="headerlink" title="局部性原理"></a>局部性原理</h4><p>分为时间局部性和空间局部性，请前去查看计算机组成原理的笔记。</p><h3 id="二级页表和多级页表"><a href="#二级页表和多级页表" class="headerlink" title="二级页表和多级页表"></a>二级页表和多级页表</h3><p><img src="https://files.seeusercontent.com/2026/03/27/e8gY/image-20260327161852354.png" alt="image-20260327161852354"></p><p>单级页表的问题在于：</p><ul><li>页表必须连续存放，当页表很大时，就需要占用多个连续的页框</li><li>没有必要让整个页表都常驻内存，进程在一段时间内只需要访问某几个页面就可以正常运行了</li><li>在64位系统中完全不可行</li></ul><p>解决方式就是将长长的页表进行分组，使每个内存块刚好可以放入一个分组（比如上个例子中，页面大小4KB,每个页表项4B,每个页面可存放1K个页表项，因此每1K个连续的页表项为一组，每组刚好占一个内存块，再将各组离散地放到各个内存块中)。另外，要为离散分配的页表再建立一张页表，称为页目录表，或称外层页表，或称顶层页表。</p><p>二级页表非但不能节省空间，反而因为多了一张“页目录表”而<strong>多占了空间</strong>。它的真正价值在于：<strong>它不需要页表在内存中连续存放</strong>，且可以<strong>按需调入</strong>（不在内存里的页表项可以暂时放在磁盘）。</p><p>听起来很抽象，没关系。</p><p>32位逻辑地址空间，页表项大小为4B,页面大小为4KB,则页内地址占12位，页号就占20位，那么进程最多有2^20个页面。</p><p><img src="https://files.seeusercontent.com/2026/03/27/tj8B/image-20260327193215805.png" alt="image-20260327193215805"></p><p>使用一个新的表来记录X#页表，记录第X页表和所在内存块号的映射关系，就是：</p><p><img src="https://files.seeusercontent.com/2026/03/27/aa3B/image-20260327193507633.png" alt="image-20260327193507633"></p><p>看起来很像二级指针，那么如何才能实现地址转换呢？</p><p>就是根据顶级页表逐层查询内存块号。</p><p><img src="https://files.seeusercontent.com/2026/03/27/3qSs/image-20260327194455204.png" alt="image-20260327194455204"></p><ul><li>①按照地址结构将逻辑地址拆分成三部分</li><li>②从PCB中读出页目录表始址，再根据一级页号查页目录表，找到下一级页表在内存中的存放位置</li><li>③根据二级页号查表，找到最终想访问的内存块号</li><li>④结合页内偏移量得到物理地址</li></ul><p>这就解决了第一个问题：页表必须连续存放，当页表很大时，就需要占用多个连续的页框。</p><p>至于第二个问题，可以给每个页表项增加一个标志位，记录它是否已经调入内存，若想访问的页面不在内存中，就触发缺页中断，随后将页面从外存调入内存。这个技术叫虚拟化技术，后面会详细介绍。</p><p><img src="https://files.seeusercontent.com/2026/03/27/m8aG/image-20260327194801323.png" alt="image-20260327194801323"></p><p>同理，也可以使用多级页表，但是那可能会因为地址转换需要的访存次数变多而导致效率下降。</p><p><img src="https://files.seeusercontent.com/2026/03/27/e2Pm/image-20260327195314827.png" alt="image-20260327195314827"></p><h3 id="基本分段存储"><a href="#基本分段存储" class="headerlink" title="基本分段存储"></a>基本分段存储</h3><p>这是与分页管理平行的一个管理方式，最大的区别就是离散分配时所分配地址空间的基本单位不同。</p><p>进程的地址空间：按照程序自身的逻辑关系划分为若干个段，每个段都有一个段名（在低级语言中，程序员使用段名来编程)，每段从0开始编址。</p><p><img src="https://files.seeusercontent.com/2026/03/27/4jXv/image-20260327200942742.png" alt="image-20260327200942742"></p><p>编译程序会将段名转换为相应的段号。</p><p><img src="https://files.seeusercontent.com/2026/03/27/d4Zr/image-20260327201218517.png" alt="image-20260327201218517"></p><p>段号的位数决定了每个进程最多可以分几个段，而段内地址位数决定了每个段的最大长度是多少。</p><h4 id="段表"><a href="#段表" class="headerlink" title="段表"></a>段表</h4><blockquote><p>务必记住，段表和<strong>单级页表</strong>都是目录，是为了快速找到目标内存地址，所以这一定是在空间上连续存放的。</p><p>你可以这么理解，段表是数组，段表项是数组元素。</p></blockquote><p>程序分多个段，各段离散地装入内存，为了保证程序能正常运行，就必须能从物理内存中找到各个逻辑段的存放位置。为此，需为每个进程建立一张段映射表，简称“段表”</p><p><img src="https://files.seeusercontent.com/2026/03/29/9aoF/image-20260329102931793.png" alt="image-20260329102931793"></p><ul><li>每个段对应一个段表项，其中记录了该段在内存中的起始位置（又称“基址”)和段的长度。</li><li>各个段表项的长度是相同的。例如：某系统按字节寻址，采用分段存储管理，逻辑地址结构为（段号16位，段内地址16位），因此用16位即可表示最大段长。物理内存大小为4GB(可用32位表示整个物理内存地址空间)。因此，可以让每个段表项占16+32=48位，即6B。由于段表项长度相同，因此段号可以是隐含的，不占存储空间。若段表存放的起始地址为M,则K号段对应的段表项存放的地址为M+K*6</li></ul><h3 id="地址变换"><a href="#地址变换" class="headerlink" title="地址变换"></a>地址变换</h3><p><img src="https://files.seeusercontent.com/2026/03/29/9sLs/image-20260329104717600.png" alt="image-20260329104717600"></p><p><img src="https://files.seeusercontent.com/2026/03/29/4Pvv/image-20260329105228875.png" alt="image-20260329105228875"></p><p>和分页存储还是有一点不一样的。</p><p>首先，根据逻辑地址得到段号和段内地址，随后检查段号是否越界，如不越界就继续执行。随后查询段表，找到对应的段表项，找到其存放地址。</p><p>随后<strong>检查段内地址是否越界</strong>，随后计算物理地址，访问即可。</p><p>关于访存次数：</p><p>分段：第一次访存一一查内存中的段表，第二次访存一一访问目标内存单元。总共两次访存与分页系统类似，分段系统中也可以引入快表机构，将近期访问过的段表项放到快表中，这样可以少一次访问，加快地址变换速度。</p><h4 id="分段和分页的横向对比"><a href="#分段和分页的横向对比" class="headerlink" title="分段和分页的横向对比"></a>分段和分页的横向对比</h4><p>页是信息的物理单位。分页的主要目的是为了实现离散分配，提高内存利用率。分页仅仅是系统管理上的需要，完全是系统行为，对用户是不可见的。</p><p>段是信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的，用户编程时需要显式地给出段名。</p><p>页的大小固定且由系统决定。段的长度却不固定，决定于用户编写的程序。</p><p>分页的用户进程地址空间是一维的，程序员只需给出一个记忆符即可表示一个地址。</p><p>分段的用户进程地址空间是二维的，程序员在标识一个地址时，既要给出段名，也要给出段内地址。</p><p><img src="https://files.seeusercontent.com/2026/03/29/M1wz/image-20260329111212786.png" alt="image-20260329111212786"></p><p>在好处方面：</p><p>分段比分页更容易实现信息的共享与保护。</p><p>不能被修改的代码称为纯代码或可重入代码（不属于临界资源），这样的代码是可以共享的。可修改的代码是不能共享的（比如，有一个代码段中有很多变量，各进程并发地同时访问可能造成数据不一致)</p><p>如生产者和消费者问题，两个进程之间可以使用一个共有的缓冲区段，具有相同的段长和基址，就能实现共享。</p><p><img src="https://files.seeusercontent.com/2026/03/29/zSp4/image-20260329112258752.png" alt="image-20260329112258752"></p><p>如果使用分页来实现共享：</p><p><img src="https://files.seeusercontent.com/2026/03/29/B1ir/image-20260329112608552.png" alt="image-20260329112608552"></p><p>对于段表和页表的设计：</p><p><img src="https://files.seeusercontent.com/2026/03/29/T8uo/image-20260329112846337.png" alt="image-20260329112846337"></p><p>所以，因为分页是按照空间来划分而不是功能划分的，所以实现共享方面自然有缺陷。</p><h3 id="段页式管理方式"><a href="#段页式管理方式" class="headerlink" title="段页式管理方式"></a>段页式管理方式</h3><blockquote><p>分段和分页的管理方式</p></blockquote><table><thead><tr><th></th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>分页管理</td><td>内存空间利用率高，不会产生外部碎片，只会有少量的页内碎片</td><td>不方便按照逻辑模块实现信息的共享和保护</td></tr><tr><td>分段管理</td><td>很方便按照逻辑模块实现信息的共享和保护</td><td>如果段长过大，为其分配很大的连续空间会很不方便。另外，段式管理会产生外部碎片</td></tr></tbody></table><p>分段管理中产生的外部碎片也可以用“凑”来解决，只是需要付出较大的时间代价。</p><p>段页式采用“先分段再分页”的模式。</p><p><img src="https://files.seeusercontent.com/2026/03/29/x1Of/image-20260329114031884.png" alt="image-20260329114031884"></p><p>所以，逻辑地址组成就需要变化。</p><p><img src="https://files.seeusercontent.com/2026/03/29/3lDh/image-20260329114105731.png" alt="image-20260329114105731"></p><p>实际上是将段再按页拆分的结果。</p><p>段号的位数决定了每个进程最多可以分几个段。<br>页号位数决定了每个段最大有多少页。<br>页内偏移量决定了页面大小、内存块大小是多少。</p><p>“分段”对用户是可见的，程序员编程时需要显式地给出段号、段内地址。而将各段“分页”对用户是不可见的。系统会根据段内地址自动划分页号和页内偏移量因此段页式管理的地址结构是二维的。</p><p><img src="https://files.seeusercontent.com/2026/03/29/N3bd/image-20260329114923467.png" alt="image-20260329114923467"></p><p>那么就涉及到数据结构。每个段对应一个段表项，每个段表项由段号、页表长度、页表存放块号（页表起始地址)组成。每个段表项长度相等，段号是隐含的。而每个页面对应一个页表项，每个页表项由页号、页面存放的内存块号组成。每个页表项长度相等，页号是隐含的。</p><h4 id="地址转换-1"><a href="#地址转换-1" class="headerlink" title="地址转换"></a>地址转换</h4><p><img src="https://files.seeusercontent.com/2026/03/29/pt5M/image-20260329115253169.png" alt="image-20260329115253169"></p><p>所以有三次访存：</p><ul><li>第一次访问内存当中的段表</li><li>第二次访问页表</li><li>第三次访问目标内存单元</li></ul><h3 id="虚拟内存技术"><a href="#虚拟内存技术" class="headerlink" title="虚拟内存技术"></a>虚拟内存技术</h3><p>这属于内存空间的扩充，终于来到了这里。</p><p>传统的存储管理方案有很多缺点：</p><p><img src="https://files.seeusercontent.com/2026/03/29/imU6/image-20260329131612130.png" alt="image-20260329131612130"></p><p>一次性：作业必须一次性全部装入内存后才能开始运行。这会造成两个问题：</p><ul><li>作业很大时，不能全部装入内存，导致大作业无法运行；</li><li>当大量作业要求运行时，由于内存无法容纳所有作业，因此只有少量作业能运行，导致多道程序并发度下降。</li></ul><p>驻留性：一旦作业被装入内存，就会一直驻留在内存中，直至作业运行结束。事实上，在一个时间段内，只需要访问作业的一小部分数据即可正常运行，这就导致了内存中会驻留大量的、暂时用不到的数据，浪费了宝贵的内存资源。</p><p>以上问题就能用虚拟技术来完成，虚拟技术是使用时间局部性和空间局部性来完成的。</p><p>时间局部性：如果执行了程序中的某条指令，那么不久后这条指令很有可能再次执行；如果某个数据被访问过，不久之后该数据很可能再次被访问。（因为程序中存在大量的循环)。<br>空间局部性：一旦程序访问了某个存储单元，在不久之后，其附近的储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的，并且程序的指令也是顺序地在内存中存放的)。</p><p>所以，在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序<br>若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。</p><p>在操作系统的管理下，在用户看来似乎有一个比实际内存大得多的内存，这就是虚拟内存</p><p>所以，虚拟内存具有以下特征。</p><ul><li>多次性：无需在作业运行时一次性全部装入内存，而是允许被分成多次调入内存。</li><li>对换性：在作业运行时无需一直常驻内存，而是允许在作业运行过程中，将作业换入、换出。</li><li>虚拟性：从逻辑上扩充了内存的容量，使用户看到的内存容量，远大于实际的容量。</li></ul><h4 id="虚拟内存技术的实现"><a href="#虚拟内存技术的实现" class="headerlink" title="虚拟内存技术的实现"></a>虚拟内存技术的实现</h4><p>虚拟内存技术，允许一个作业分多次调入内存。如果采用连续分配方式，会不方便实现。因此，虚拟内存的实现需要建立在离散分配(分页分段和段页式)的内存管理方式基础上，就有了请求式管理方式(请求分页，请求分段，请求段页式)。</p><p>其主要区别是：在程序执行过程中，当所访问的信息不在内存时，由操作系统负责将所需信息从外存调入内存，然后继续执行程序。若内存空间不够，由操作系统负责将内存中暂时用不到的信息换出到外存。</p><p>因为需要将信息在外存和内存中交换，所以操作系统需要提供调页和段置换功能。</p><h3 id="页表机制"><a href="#页表机制" class="headerlink" title="页表机制"></a>页表机制</h3><p>与基本分页管理相比，请求分页管理中，为了实现“请求调页”，操作系统需要知道每个页面是否已经调入内存：如果还没调入，那么也需要知道该页面在外存中存放的位置。这就需要将信息结构化地记录在页表当中。这就需要设计新的请求分页存储管理的页表：</p><p><img src="https://files.seeusercontent.com/2026/03/29/5njA/image-20260329133606469.png" alt="image-20260329133606469"></p><p>请求页表新增状态位，访问字段，修改位和外存地址。</p><h4 id="缺页中断机构"><a href="#缺页中断机构" class="headerlink" title="缺页中断机构"></a>缺页中断机构</h4><p><img src="https://files.seeusercontent.com/2026/03/29/Q2hf/image-20260329134150906.png" alt="image-20260329134150906"></p><p>假设此时要访问逻辑地址=（页号，页内偏移量）=(0,1024)<br>在请求分页系统中，每当要访问的页面不在内存时，便产生一个缺页中断，然后由操作系统的<strong>缺页中断处理程序</strong>处理中断。</p><p>此时缺页的进程阻塞，放入阻塞队列，调页完成后再将其唤醒，放回就绪队列。</p><p>如果内存中有空闲块，则为进程分配一个空闲块，将所缺页面装入该块，并修改页表中相应的页表项。<br>如果内存中没有空闲块，则由页面置换算法选择一个页面淘汰，若该页面在内存期间被修改过，则要将其写回外存。未修改过的页面不用写回外存。</p><p>缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的，因此属于内中断。且它是由错误条件引起的，有可能被系统自行修复，所以属于“故障”。</p><h4 id="地址变换机构"><a href="#地址变换机构" class="headerlink" title="地址变换机构"></a>地址变换机构</h4><p>当发生页面调入或者调出时，需要对新来的页面进行地址补充，这就是地址变换。</p><p>要做的事情有三个：</p><ul><li>请求调页（查到页表项时进行判断</li><li>页面置换（需要调入页面，但没有空闲内存块时进行）</li><li>需要修改请求页表中新增的表项</li></ul><p>与基本分页管理差不多：</p><p><img src="https://files.seeusercontent.com/2026/03/29/Yrr2/image-20260329135013058.png" alt="image-20260329135013058"></p><p><img src="https://files.seeusercontent.com/2026/03/29/my8D/image-20260329135037794.png" alt="image-20260329135037794"></p><p>红框就是请求式相对于基本分页增加的内容</p><p>补充点细节：</p><p>①只有“写指令”才需要修改“修改位”。并且，一般来说只需修改快表中的数据，只有要将<br>快表项删除时才需要写回内存中的慢表。这样可以减少访存次数。<br>②和普通的中断处理一样，缺页中断处理依然需要保留CPU现场。<br>③需要用某种“页面置换算法”来决定一个换出页面（下节内容）<br>④换入/换出页面都需要启动慢速的I/O操作，可见，如果换入I/O换出太频繁，会有很大的开销。<br>⑤页面调入内存后，需要修改慢表，同时也需要将表项复制到快表中。在具有快表机构的请求分页系统中，访问一个逻辑地址时，若发生缺页，则地址变换步骤是：</p><ul><li>查快表（未命中）</li><li>查慢表（发现未调入内存）</li><li>调页（调入的页面对应的表项会直接加入快表)</li><li>查快表（命中)</li><li>访问目标内存单元。</li></ul><h3 id="页面置换算法"><a href="#页面置换算法" class="headerlink" title="页面置换算法"></a>页面置换算法</h3><p>页面置换算法用于实现将需要的页面调入内存和选择将哪个“不需要的页面”换到外存。</p><p>页面的换入、换出需要磁盘I/O,会有较大的开销，因此好的页面置换算法应该追求更少的缺页率。</p><h4 id="最佳置换算法OPT"><a href="#最佳置换算法OPT" class="headerlink" title="最佳置换算法OPT"></a>最佳置换算法OPT</h4><p>最佳置换算法(OPT,Optimal)：每次选择淘汰的页面将是以后永不使用，或者在最长时间内不再被访问的页面，这样可以保证最低的缺页率。</p><p><img src="https://files.seeusercontent.com/2026/03/29/Rh3q/image-20260329143402115.png" alt="image-20260329143402115"></p><p>最佳置换算法可以保证最低的缺页率，但实际上，只有在进程执行的过程中才能知道接下来会访问到的是哪个页面。操作系统无法提前预判页面访问序列。因此，理想化的最佳置换算法是无法实现的。</p><h4 id="先进先出置换算法-FIFO"><a href="#先进先出置换算法-FIFO" class="headerlink" title="先进先出置换算法(FIFO)"></a>先进先出置换算法(FIFO)</h4><p>先进先出置换算法(FIFO)：每次选择淘汰的页面是最早进入内存的页面。</p><p>实现方法：把调入内存的页面根据调入的先后顺序排成一个队列，需要换出页面时选择队头页面即可。队列的最大长度取决于系统为进程分配了多少个内存块。</p><p><img src="https://files.seeusercontent.com/2026/03/29/Iz7z/image-20260329145149047.png" alt="image-20260329145149047"></p><p>如果是有4个内存块：</p><p><img src="https://files.seeusercontent.com/2026/03/29/uZk8/image-20260329145310550.png" alt="image-20260329145310550"></p><p>发现不对劲了吧：内存块增加了，但是缺页次数反而增多了，这被称为贝拉迪异常。这是因为这种算法几乎是完全不尊重局部性原则。</p><p>只有FIFO算法会产生Belady异常。另外，FIFO算法虽然实现简单，但是该算法与进程实际运行时的规律不适应，因为先进入的页面也有可能最经常被访问。因此，算法性能差。</p><h4 id="最近最久未使用置换算法-LRU"><a href="#最近最久未使用置换算法-LRU" class="headerlink" title="最近最久未使用置换算法(LRU)"></a>最近最久未使用置换算法(LRU)</h4><p>最近最久未使用置换算法(LRU,least recently used)：每次淘汰的页面是最近最久未使用的页面。</p><p>实现方法：赋予每个页面对应的页表项中，用访问字段记录该页面自上次被访问以来所经历的时间。当需要淘汰一个页面时，选择现有页面中t值最大的，即最近最久未使用的页面。</p><p><img src="https://files.seeusercontent.com/2026/03/29/jx0D/image-20260329145741047.png" alt="image-20260329145741047"></p><p>该算法的实现需要专门的硬件支持，虽然算法性能好，但是实现困难，开销大</p><h4 id="时钟置换算法-CLOCK"><a href="#时钟置换算法-CLOCK" class="headerlink" title="时钟置换算法(CLOCK)"></a>时钟置换算法(CLOCK)</h4><blockquote><p>最佳置换算法性能最好，但无法实现；先进先出置换算法实现简单，但算法性能差；最近最久未使用置换算法性能好，是最接近OPT算法性能的，但是实现起来需要专门的硬件支持，算法开销大。</p></blockquote><p>时钟置换算法是一种性能和开销较均衡的算法，又称CLOCK算法，或最近未用算法<br>(NRU,Not Recently Used)<br>简单的CLOCK算法实现方法：为每个页面设置一个访问位，再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时，其访问位置为1。当需要淘汰一个页面时，只需检查页的访问位。<br>如果是0，就选择该页换出：如果是1，则将它置为0，暂不换出，继续检查下一个页面，若第一轮扫描中所有页面都是1，则将这些页面的访问位依次置为0后，再进行第二轮扫描（第二轮扫描中一定会有访问位为0的页面，因此简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描)。</p><p><img src="https://files.seeusercontent.com/2026/03/29/Xe1g/image-20260329150524108.png" alt="image-20260329150524108"></p><p>如图，头1,3,4,2,5后他们组成了一个时钟，经过两轮扫描，会将1号置换为6号，并将6号设置为1，随后按次序访问4号后发现没有7号而发生缺页中断，然后扫描到2号为0，随后将2号置换为7号页面。</p><h4 id="改进型时钟置换算法"><a href="#改进型时钟置换算法" class="headerlink" title="改进型时钟置换算法"></a>改进型时钟置换算法</h4><p>简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上，如果被淘汰的页面没有被修改过，就不需要执行IO操作写回外存。只有被淘汰的页面被修改过时，才需要写回外存。</p><p>因此，除了考虑一个页面最近有没有被访问过之外，操作系统还应考虑页面有没有被修改过。在其他条件都相同时，应优先淘汰没有修改过的页面，避免I/O操作。这就是改进型的时钟置换算法的思想。</p><p>修改位=0，表示页面没有被修改过；修改位=1，表示页面被修改过。</p><p>为方便讨论，用（访问位，修改位）的形式表示各页面状态。如(1,1)表示一个页面近期被访问过，且被修改过。</p><p>那么算法规则就是：</p><p>将所有可能被置换的页面排成一个循环队列</p><ul><li>第一轮：从当前位置开始扫描到第一个(0,0)的帧用于替换。本轮扫描<br>不修改任何标志位</li><li>第二轮：若第一轮扫描失败，则重新扫描，查找第一个(0,1)的帧用于替换。本轮将所有扫描过的帧访问位设为0</li><li>第三轮：若第二轮扫描失败，则重新扫描，查找第一个(0,0)的帧用于替换。本轮扫描不修改任何标志位</li><li>第四轮：若第三轮扫描失败，则重新扫描，查找第一个(0,1)的帧用于<br>替换。</li></ul><p>由于第二轮已将所有帧的访问位设为0，因此经过第三轮、第四轮扫描一定会有一个帧被选中，因此改进型CLOCK置换算法选择一个淘汰页面最多会进行四轮扫描</p><p><img src="https://files.seeusercontent.com/2026/03/30/zo0C/image-20260330140947195.png" alt="image-20260330140947195"></p><p>这就是一个需要经过四轮扫描的例子。</p><p>这种算法，优先级如下：</p><ul><li>第一优先级：最近没访问且没修改的页面</li><li>第二优先级：最近没访问但修改过的页面</li><li>第三优先级：最近访问过但没修改的页面</li><li>第四优先级：最近访问过且修改过的页面</li></ul><h3 id="页面分配策略"><a href="#页面分配策略" class="headerlink" title="页面分配策略"></a>页面分配策略</h3><p>驻留集：请求分页存储管理中给进程分配的物理块的集合。</p><p>在采用了虚拟存储技术的系统中，驻留集大小一般小于进程的总大小。那我们来考虑一个极端情况，若某进程共有100个页面，则该进程的驻留集大小为100时进程可以全部放入内存，运行期间不可能再发生缺页。若驻留集大小为1，则进程运行期间必定会极频繁地缺页。</p><p>所以，若驻留集太小，会导致缺页频繁，系统要花大量的时间来处理缺页，实际用于进程推进的时间很少；驻留集太大，又会导致多道程序并发度下降，资源利用率降低。所以应该选择一个合适的驻留集大小。</p><p>人们想出了两种分配方式：</p><p>固定分配：操作系统为每个进程分配一组固定数目的物理块，在进程运行期间不再改变。即，驻留集大小不变<br>可变分配：先为每个进程分配一定数目的物理块，在进程运行期间，可根据情况做适当的增加或减少。即，驻留集大小可变</p><p>那么针对页面置换的范围，又提出两种策略。</p><p>局部置换：发生缺页时只能选进程自己的物理块进行置换。<br>全局置换：可以将操作系统保留的空闲物理块分配给缺页进程，也可以将别的进程持有的物理块置换到外存，再分配给缺页进程。</p><table><thead><tr><th></th><th>局部置换</th><th>全局置换</th></tr></thead><tbody><tr><td>固定分配</td><td>V</td><td>/</td></tr><tr><td>可变分配</td><td>V</td><td>V</td></tr></tbody></table><p>全局置换意味着一个进程拥有的物理块数量必然会改变，因此不可能是固定分配。</p><h4 id="固定分配局部置换"><a href="#固定分配局部置换" class="headerlink" title="固定分配局部置换"></a>固定分配局部置换</h4><p>固定分配局部置换：系统为每个进程分配一定数量的物理块，在整个运行期间都不改变。若进程在运行中发生缺页，则只能从该进程在内存中的页面中选出一页换出，然后再调入需要的页面。</p><p>这种策略的缺点是：很难在刚开始就确定应为每个进程分配多少个物理块才算合理。（采用这种策略的系统可以根据进程大小、优先级、或是根据程序员给出的参数来确定为一个进程分配的内存块数)</p><h3 id="可变分配全局置换"><a href="#可变分配全局置换" class="headerlink" title="可变分配全局置换"></a>可变分配全局置换</h3><p>可变分配全局置换：刚开始会为每个进程分配一定数量的物理块。操作系统会保持一个空闲物理块队列。当某进程发生缺页时，从空闲物理块中取出一块分配给该进程：若己无空闲物理块，则可选择一个**未锁定的页面(部分锁定的内核界面是不能被换出外存的)**换出外存，再将该物理块分配给缺页的进程。采用这种策略时，只要某进程发生缺页，都将获得新的物理块，仅当空闲物理块用完时，系统才选择一个未锁定的页面调出。</p><p>被选择调出的页可能是系统中任何一个进程中的页，因此这个被选中的进程拥有的物理块会减少，缺页率会增加。</p><h4 id="可变分配局部置换"><a href="#可变分配局部置换" class="headerlink" title="可变分配局部置换"></a>可变分配局部置换</h4><p>可变分配局部置换：刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页时，只允许从该进程自己的物理块中选出一个进行换出外存。如果进程在运行中频繁地缺页，系统会为该进程多分配几个物理块，直至该进程缺页率趋势适当程度；反之，如果进程在运行中缺页率特别低，则可适当减少分配给该进程的物理块。</p><p>注意：</p><p>可变分配全局置换：要缺页就给分配新物理块</p><p>可变分配局部置换：要根据发生缺页的频率来动态地增加或减少进程的物理块</p><p>对于何时调入页面的问题，人们提出了几种策略。</p><ul><li>预调页策略：根据局部性原理，一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入的页面中大多数都没被访问过，则又是低效的。因此可以预测不久之后可能访问到的页面，将它们预先调入内存，但目前预测成功率只有50%左右。故这种策略<strong>主要用于进程的首次调入</strong>，由程序员指出应该先调入哪些部分。</li><li>请求调页策略：进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到，但由于每次只能调入一页，而每次调页都要磁盘I/O操作，因此I/O开销较大。主要用于运行时调入。</li></ul><p>对于从何处调入页面：</p><p>外存具有<strong>读/写速度更快，采用连续分配方式的</strong>对换区和<strong>读写更慢，采用离散分配的方式的</strong>文件区两个区。</p><p>置换方式有三种 ：</p><ul><li><strong>系统拥有足够的对换区空间</strong>：页面的调入、调出都是在内存与对换区之间进行，这样可以保证页面的调入、调出速度很快。在进程运行前，需将进程相关的数据从文件区复制到对换区。</li></ul><p><img src="https://files.seeusercontent.com/2026/03/30/Zly3/image-20260330144421716.png" alt="image-20260330144421716"></p><ul><li><p>系统缺少足够的对换区空间：凡是不会被修改的数据都直接从文件区调入，由于这些页面不会被修改，因此换出时不必写回磁盘，下次需要时再从文件区调入即可。对于可能被修改的部分，换出时需写回磁盘对换区，下次需要时再从对换区调入。</p><p><img src="https://files.seeusercontent.com/2026/03/30/qz9P/image-20260330144506873.png" alt="image-20260330144506873"></p></li><li><p>UNIX方式：运行之前进程有关的数据全部放在文件区，故未使用过的页面，都可从文件区调入。若被使用过的页面需要换出，则写回对换区，下次需要时从对换区调入。</p></li></ul><p><img src="https://files.seeusercontent.com/2026/03/30/jV4v/image-20260330144543858.png" alt="image-20260330144543858"></p><h4 id="抖动现象"><a href="#抖动现象" class="headerlink" title="抖动现象"></a>抖动现象</h4><p>刚刚换出的页面马上又要换入内存，刚刚换入的页面马上又要换出外存，这种频繁的页面调度行为称为抖动，或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数（分配给进程的物理块不够)</p><p>这会导致大量的时间用于换入换出而不是执行，整体效率下降。</p><p>而分配物理块数又是一个问题，所以为了搞清楚进程需要多少物理块，需要引入工作集概念。</p><h3 id="工作集"><a href="#工作集" class="headerlink" title="工作集"></a>工作集</h3><p>驻留集：指请求分页存储管理中给进程分配的内存块的集合。</p><p>工作集：指在某段时间间隔里，进程实际访问页面的集合。</p><p>操作系统会根据“窗口尺寸”来算出工作集。例：某进程的页面访问序列如下，窗口尺寸为4，各时刻的工作集为？</p><p><img src="https://files.seeusercontent.com/2026/03/30/kZ5f/image-20260330145048910.png" alt="image-20260330145048910"></p><p>注意，这里具有集合的属性，不统计一样的。</p><p>工作集大小可能小于窗口尺寸，实际应用中，操作系统可以统计进程的工作集大小，根据工作集大小给进程分配若干内存块。如：窗口尺寸为5，经过一段时间的监测发现某进程的工作集最大为3，那么说明该进程有很好的局部性，可以给这个进程分配3个以上的内存块即可满足进程的运行需要。一般来说，驻留集大小不能小于工作集大小，否则进程运行过程中将频繁缺页。</p><h3 id="内存映射文件"><a href="#内存映射文件" class="headerlink" title="内存映射文件"></a>内存映射文件</h3><p>内存映射文件一一操作系统向上层程序员提供的功能（系统调用）</p><p>通过该功能，能够做到：</p><ul><li>方便程序员访问文件数据</li><li>方便多个进程共享同一个文件</li></ul><p>现在来看看第一个作用：</p><h4 id="传统访问方式"><a href="#传统访问方式" class="headerlink" title="传统访问方式"></a>传统访问方式</h4><p><img src="https://files.seeusercontent.com/2026/03/30/9rXb/image-20260330145818757.png" alt="image-20260330145818757"></p><p>听起来很麻烦对吧。</p><p>如果使用内存映射文件就不用这么麻烦。</p><p><img src="https://files.seeusercontent.com/2026/03/30/mYq0/image-20260330150217174.png" alt="image-20260330150217174"></p><p>手动读写的步骤被省略，将由操作系统自动完成。</p><p>然后看第二个作用：</p><p>两个进程会分别将文件从磁盘映射自己的虚拟地址空间中，而操作系统会修改页表将他们放到一起便于实现共享。</p><p><img src="https://files.seeusercontent.com/2026/03/30/sEh6/image-20260330150452317.png" alt="image-20260330150452317"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;操作系统第三章&quot;&gt;&lt;a href=&quot;#操作系统第三章&quot; class=&quot;headerlink&quot; title=&quot;操作系统第三章&quot;&gt;&lt;/a&gt;操作系统第三章&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;恭喜你已经度过了漫长的进程，但这个章节更加抽象。&lt;/p&gt;
&lt;/blockq</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>操作系统第二章：进程</title>
    <link href="https://blog.willmo.top/posts/Operating%20System%20Chapter%202:%20Processes/"/>
    <id>https://blog.willmo.top/posts/Operating%20System%20Chapter%202:%20Processes/</id>
    <published>2026-04-20T07:09:54.144Z</published>
    <updated>2026-04-20T07:11:29.763Z</updated>
    
    <content type="html"><![CDATA[<h1 id="操作系统-第二章"><a href="#操作系统-第二章" class="headerlink" title="操作系统 第二章"></a>操作系统 第二章</h1><blockquote><p>这一章会非常非常长，写完的时候统计数据是25036个词。</p></blockquote><h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><p>进程和程序不是一回事。</p><p>程序：<strong>是静态的，就是个存放在磁盘里的可执行文件，就是一系列的指令集合</strong>。</p><p>进程(Process):<strong>是动态的，是程序的一次执行过程。同一个程序的多次执行对应多个进程。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/12/h9Uy/image-20260312154539723.png" alt="image-20260312154539723"></p><p>我们启动了三个TIM，任务管理器中就出现了三个TIM进程。</p><p>当进程被创建时，操作系统会为该进程分配一个唯一的、不重复的“身份证号”一一<strong>PID(Process,进程ID)</strong></p><p><img src="https://files.seeusercontent.com/2026/03/12/e7Yp/image-20260312154949635.png" alt="image-20260312154949635"></p><p>任务管理器中可见进程的<strong>PID和启动用户名</strong>等信息。这些信息被放在一个数据结构**PCB(Process Control Block)**中，即进程控制块，操作系统需要对各个并发运行的进程进行管理，但凡管理时所需要的信息，都会被放在PCB中。</p><p>因此PCB是一个很重要的数据结构。</p><p><img src="https://files.seeusercontent.com/2026/03/12/ekC9/image-20260312155757926.png" alt="image-20260312155757926"></p><p>进程实体是进程在某一时刻的快照，显示出在某一时刻的状态。</p><p>进程是进程实体的运行过程，是<strong>系统进行资源分配和调度的一个独立单位。</strong></p><blockquote><p>调度：系统决定让该进程在CPU上运行，先记住这个不大准确的概念。</p></blockquote><p>在上面的例子中，三个进程的PCB，数据段是不同的，但是程序段是相同的，因为<strong>它们都运行着相同的TIM程序。</strong></p><p>进程具有以下特征：</p><ul><li><strong>动态性</strong>：程序执行过程</li><li><strong>并发性</strong>：各进程能够并发执行</li><li><strong>独立性</strong>：进程能独立运行和获取资源</li><li><strong>异步性</strong>：进程各自独立，不可知的速度推进</li><li><strong>结构性</strong>：每个进程都会提供一个结构化PCB</li></ul><h2 id="进程的状态和转换"><a href="#进程的状态和转换" class="headerlink" title="进程的状态和转换"></a>进程的状态和转换</h2><p>在PCB中<strong>有一个变量叫做state,用于记录进程的状态。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/12/X3gr/image-20260312162001095.png" alt="image-20260312162001095"></p><p>进程一开始是<strong>创建态</strong>，随后操作系统开始为其创建PCB，划分数据段，完成后进入<strong>就绪态</strong>，此时进程<strong>已经具备除了处理机之外的所有运行条件。</strong></p><p>CPU处于空闲时，进程被调度(被放在CPU上运行)，成为<strong>运行态</strong>，若进程需要使用打印机，但打印机处于忙状态，进程必须等待，进入<strong>阻塞态</strong>，此时进程不具备处理组件和其他任何运行条件。随后打印机进入空闲，进程申请的打印机资源被分配，进程进入<strong>就绪态</strong>，再调度进入<strong>运行态</strong>，运行完毕或遇到不可修复的错误后进入<strong>终止态</strong>。</p><p>例外的是：如果CPU在运行某个进程时接收到外部的CLK时钟中断，进程会从<strong>运行态退回到就绪态</strong>。且如果是多核CPU，会有多个进程处于运行态。</p><h3 id="进程的组织方式"><a href="#进程的组织方式" class="headerlink" title="进程的组织方式"></a>进程的组织方式</h3><p>进程很多，需要用某种方式标记才能管理和调度。</p><p>使用链接方式就类似于<strong>链表</strong>，也是大多数操作系统使用的方式。</p><p><img src="https://files.seeusercontent.com/2026/03/12/Zq8w/image-20260312163449378.png" alt="image-20260312163449378"></p><p>使用索引方式就类似于指针数组。</p><p><img src="https://files.seeusercontent.com/2026/03/12/0Irn/image-20260312163525011.png" alt="image-20260312163525011"></p><h3 id="进程控制"><a href="#进程控制" class="headerlink" title="进程控制"></a>进程控制</h3><p>进程控制的主要功能是<strong>对系统中的所有进程实施有效的管理，它具有创建新进程、撤销已有进程、实现进程状态转换等功能。</strong></p><p>记得原语吗？<strong>不可中断的原子程序</strong>。操作系统就是用原语来实现进程控制的。</p><p>PCB中的变量<strong>State用来表示进程当前所处的状态</strong>，1表示就绪态，2表示阻塞态。那么用链接方式来表示就绪队列和阻塞队列：</p><p><img src="https://files.seeusercontent.com/2026/03/12/H6sf/image-20260312191340179.png" alt="image-20260312191340179"></p><p>假设此时进程2等待的事件发生(进程2的处理件已就绪)，则操作系统中，负责进程控制的内核程序至少需要做这样两件事：</p><ul><li><strong>将PCB2的state设为1</strong></li><li><strong>将PCB2从阻塞队列放到就绪队列</strong></li></ul><p>这两件事是必须一次性完成的，否则系统很容易出错。</p><p>我们使用<strong>开/关中断指令</strong>来实现原语的原子性。</p><p><img src="https://files.seeusercontent.com/2026/03/12/M4gv/image-20260312192050833.png" alt="image-20260312192050833"></p><p>CPU在执行一条命令完成后，就会检查一次外部有没有输入中断信号，如果有，就中断当前程序去执行中断处理程序，处理完成后才会返回继续处理之前未完成的程序。</p><p>这一部分在我的<strong>计算机组成原理</strong>笔记上有提到。</p><p>而关中断指令，能够让CPU不再检查中断信号，直到执行开中断指令才会恢复检查。因此，指令a和指令b是无法中断的，操作是原子性的，这就实现了程序的原子性。</p><p>很显然，开中断和关中断是<strong>特权指令</strong>，在绝大部分情况下都不应该让应用程序有权执行。</p><p>创建原语的工作是：</p><ul><li><strong>申请空白PCB</strong></li><li><strong>为新进程分配资源</strong></li><li><strong>初始化PCB</strong></li><li><strong>将PCB插入就绪队列</strong></li></ul><p>反过来，撤销原语的工作是：</p><ul><li><strong>找到目标PCB</strong></li><li><strong>若进程在运行，剥夺CPU，将CPU分配给其他进程</strong></li><li><strong>终止其所有子进程</strong></li><li><strong>将进程资源返还给父进程或操作系统</strong></li><li><strong>删除PCB</strong></li></ul><p>但是进程的终止不一定就是撤销原语的成果，当任务正常结束，或者非法使用特权指令，或者是用户选择杀死进程，就不是撤销原语的结果。</p><p>存疑，网课和ai各执一词。</p><p>除了增删，进程控制还需要能将进程在阻塞态和就绪态中切换。</p><p>原语还有阻塞原语和唤醒原语以及切换原语。</p><h2 id="进程通信"><a href="#进程通信" class="headerlink" title="进程通信"></a>进程通信</h2><p>进程间通信(Inter-Process Communication,IPC)是<strong>指两个进程之间产生数据交互。</strong></p><p>某天你在视频平台上发现了一个极佳的<strong>数字泔水</strong>，然后你果断选择分享发送给好友，那么抖音和微信两个进程之间就发生了进程通信。</p><p>进程通信的过程是<strong>需要操作系统支持</strong>的，因为进程是分配系统资源的单位（包括内存地址空间），因此各进程拥有的内存地址空间相互独立。出于安全考虑，进程是<strong>不能访问其他进程的地址空间</strong>的，所以必须又操作系统支持才能完成进程通信，目前有三种方法，<strong>共享存储，消息传递，管道通信。</strong></p><h4 id="共享存储"><a href="#共享存储" class="headerlink" title="共享存储"></a>共享存储</h4><p>操作系统给需要通信的两个进程划分一片共享的存储空间，<strong>提供信息的进程将信息写入共享存储区，另一进程再读取共享存储区</strong>，就能完成通信。</p><p>然而如果是多个进程需要通信，那么同时往一个共享存储区内写入可能会导致冲突，这就需要<strong>保证对共享空间的访问应该是互斥的</strong>，各个进程可使用操作系统内核提供的同步互斥工具(P,V操作)来实现互斥访问。</p><p>共享存储一样具有两个方式：</p><p>基于存储区共享方式的灵活性很高，数据的形式，存放位置是由进程控制的。</p><p>基于数据结构的共享灵活性差，被局限在某一固定的结构，速度很慢</p><h4 id="消息传递"><a href="#消息传递" class="headerlink" title="消息传递"></a>消息传递</h4><p>进程间的数据交换以<strong>格式化的消息（Message）为单位</strong>。进程通过操作系统提供的“<strong>发送消息/接收消息</strong>”两个原语进行数据交换。</p><p>类似于计算机网络中的数据包，消息由<strong>消息头和消息体</strong>两部分组成。消息头由发送进程ID和接受进程ID，消息长度等格式化信息组成。消息传递的方式有两种，<strong>直接和间接</strong>。</p><p>消息传递的直接通信方式如下所示：</p><p><img src="https://files.seeusercontent.com/2026/03/13/v1Fx/image-20260313192826804.png" alt="image-20260313192826804"></p><p>进程P封装一个消息，使用<strong>发送原语</strong>来指出接收方，操作系统受发送原语召唤会将消息复制到内核空间，挂到进程Q的消息队列(位于进程Q的PCB中)中去，随后如果进程Q使用接收原语来接收进程P的消息，随后操作系统将内核空间里的消息发送到进程Q的内存空间。注意，进程会主动执行接收原语，就好像你出门之前会查看信箱。</p><p>这种方式是点名道姓的，所以是<strong>直接通信</strong>方式。</p><p>消息传递的间接通信方式如下：</p><p><img src="https://files.seeusercontent.com/2026/03/13/Ww8s/image-20260313193845935.png" alt="image-20260313193845935"></p><p>首先，进程P在自己的地址空间内完成消息体的填充与封装，随后使用发送原语指明要发送到哪个信箱。随后进程Q执行接收原语，从A信箱接搜信息，这就完成了通信。</p><p>这种方式借用了“信箱”作为中间实体来进行通信。</p><p>间接消息传递和共享存储确实在“<strong>通过一个中间实体进行通信</strong>”这一点上非常相似，但它们的设计哲学、实现方式和核心特性有本质区别。</p><h4 id="管道通信"><a href="#管道通信" class="headerlink" title="管道通信"></a>管道通信</h4><p>进程通信还有管道通信这一方式：</p><p><img src="https://files.seeusercontent.com/2026/03/13/Ico7/image-20260313195741451.png" alt="image-20260313195741451"></p><p>进程P和进程Q之间建立一个管道，<strong>管道内的数据流向是单向的</strong>，就像水管中的水流一样，随后进程P能够向管道写入数据，进程Q能够向管道读出数据。注意，“管道”是一个特殊的共享文件，<strong>又名pipe文件</strong>。其实就是在内存中开辟一个大小固定的内存缓冲区。</p><p>你可能会觉得这和共享存储很像，但共享存储是一片自由的空间，发送和接受的顺序没有先后关系，也允许双向通信。而管道内置一个<strong>字节流队列</strong>，先进先出，进程P先发送的数据必须是第一个被进程Q接收的。</p><p>管道实际上是循环队列，解决普通队列中需要移动数据的问题，<strong>提升传输效率。</strong></p><p>管道只能采用<strong>半双工通信，</strong>同一时间内只能实现单向传输，如需双向就需要两个管道。</p><p><img src="https://files.seeusercontent.com/2026/03/13/1cdL/image-20260313195759615.png" alt="image-20260313195759615"></p><p>当管道写满时，写进程将阻塞，直到读进程将管道中的数据取走，即可唤醒写进程。<br>当管道读空时，读进程将阻塞，直到写进程往管道中写入数据，即可唤醒读进程。</p><p>管道中的数据一旦被读出，就彻底消失。因此，当多个进程读同一个管道时，可能会错乱。对此，通常有两种解决方案：</p><ul><li>一个管道允许多个写进程，一个读进程；</li><li>允许有多个写进程，多个读进程，但系统会让各个读进程轮流从管道中读数据，这也是Linux的解决方案。</li></ul><h3 id="信号"><a href="#信号" class="headerlink" title="信号"></a>信号</h3><p>信号(signal)：用于<strong>通知进程某个特定事件已经发生</strong>.（进程收到一个信号后，对该信号进行处理。</p><p>Linux操作系统定义30种信号类型如下：</p><p><img src="https://files.seeusercontent.com/2026/03/13/sa0A/image-20260313201938753.png" alt="image-20260313201938753"></p><p>每个信号都有对应的信号处理程序，比如信号9 <strong>SIGKILL</strong>的信号处理程序就是<strong>杀死当前进程。</strong></p><p>信号的发送与保存如下：</p><p><img src="https://files.seeusercontent.com/2026/03/13/Tfk5/image-20260313202833348.png" alt="image-20260313202833348"></p><p>进程1的PCB中有两个数组，一个是<strong>待处理信号pending</strong>，一个是<strong>被阻塞信号blocked</strong>，他们的索引值就是<strong>信号类型</strong>。被阻塞信号中值为1的索引(这里是7和8)，待处理信号中这个索引的值将会被忽略。此外两个数组是位向量，每个位对应一个元素是否存在，而无关数量和执行次数，存在就是1，不存在就是0。</p><p>图中，进程2,3和OS内核进程调用了函数kill，那么进程1的PCB的待处理信号的索引1和8的值就被设置为1。进程1无法找出信号发送方。</p><p><img src="https://files.seeusercontent.com/2026/03/13/id0S/image-20260313203414818.png" alt="image-20260313203414818"></p><p>用户进程之间可以发送信号，但是有限制的。</p><p>内核具有最高权限，能够给任何进程发送信号。</p><p><strong>当进程从内核态转为用户态时，例行检查是否有待处理信号，如果有就处理信号。</strong></p><p>检查步骤是：<strong>将被阻塞信号全部取反，在将待处理信号和被阻塞信号逐位做与运算</strong>。这样依赖，索引8是被忽视，不被处理的。</p><p>接下来要运行信号1处理程序，那运行流程：</p><p><img src="https://files.seeusercontent.com/2026/03/14/4Iss/image-20260314122246388.png" alt="image-20260314122246388"></p><p>进程P1在接收到信号后并不会立即响应，<strong>只有执行指令k(如系统调用)从内核态转化到用户态的时候</strong>，会检测待处理信号并运行信号处理程序，最后返回去执行下一条指令(除非信号直接阻塞或者杀死了进程)。<strong>处理后会将PCB中的待处理信号的对应值归零.</strong></p><p>如果指令k只是一条普通的指令，不导致进程在用户态和内核态之间的切换(如循环计算指令)，那么即使信号抵达，它也无法被处理。这时<strong>操作系统内置的定时器计时结束</strong>会强制将CPU的控制权夺回给内核检查一次指令，检查到IO中断后就会中断当前进程。(存疑，可能跟网课说的不一样。)</p><p>之前说过，操作系统会给每一类信号准备一个默认的信号处理程序，<strong>实际上用户(进程)也能够设置自定义信号处理程序，这会覆盖默认的信号处理程序，且自定义的处理程序只作用于自身进程。</strong></p><p><strong>但有的至高权限的信号无法被自定义覆写，也无法被阻塞。</strong></p><p>比如，序号为28的窗口大小变化，变化时操作系统什么都不用做，但是应用程序内的UI必须随窗口而改变，这个改变就是<strong>自定义信号处理程序</strong>。</p><p>请回看PCB中的两个数组，当收到多个不同类的信号是，通常优先处理序号更小的信号。</p><h4 id="信号与异常"><a href="#信号与异常" class="headerlink" title="信号与异常"></a>信号与异常</h4><p>“信号”可以作为“异常”的配套机制，让进程对操作系统的异常处理进行补充。<br>在进程运行过程中，某些特殊事件可能引发“异常”，操作系统内核负责捕获并处理异常</p><ul><li>有些异常可以由内核完成全部处理（如：缺页异常），此时就不必再使用信号机制。</li><li>有些异常无法由内核完成全部处理，可能还需要用户进程配合，此时就可以用“信号机制”与“异常机制”相互配合（如：在Linux中，发出除以0异常时，内核的异常处理程序会向用户进程发送SIGFPE信号。SIGFPE信号的默认处理程序会将进程终止并转储内存；当然进程可以自定义SIGFPE信号处理程序)。</li></ul><p>如果开发者不修改，那么计算器检测到除以0的时候会自动闪退，这对用户体验很不友好。这时候就需要自定义信号的处理程序来平滑地处理异常。</p><h3 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h3><p>在传统的进程机制中，<strong>进程是程序执行流的最小单位。</strong></p><p>有的进程可能需要“同时”做很多事，而传统的进程只能串行地执行一系列程序。为此，引入了“线程”，来增加并发度。</p><p>就好比<strong>Telegram</strong>这个进程，需要同时发送文件，聊天，下载视频等。</p><p>引入线程之后，<strong>线程就是程序执行流的最小单位</strong>。每个进程有不同的线程，线程能够被CPU并发处理。从这个角度看，能够把线程理解是轻量级进程。</p><p>引入线程之后，不仅是进程之间可以并发，进程内的各线程之间也可以并发，从而进一步提升了系统的并发度，使得一个进程内也可以并发处理各种任务（如QQ视频、文字聊天、传文件)</p><p>引入线程后，<strong>进程只作为除CPU之外的系统资源的分配单元</strong>（如打印机、内存地址空间等都是分配给进程的)。</p><p>换句话说，引入之前，<strong>进程是资源分配、调度的基本单位，引入线程后，进程是资源分配的基本单位，而线程是调度的基本单位。</strong></p><p>因为传统的进程间并发，需要切换进程的运行环境，系统开销很大，如果试试线程并发，那就不需要切换进程环境，系统开销小。<strong>所以，引入线程能够有效减少并发带来的系统开销。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/14/k4Dk/image-20260314140516885.png" alt="image-20260314140516885"></p><h3 id="线程的多模型实现方式"><a href="#线程的多模型实现方式" class="headerlink" title="线程的多模型实现方式"></a>线程的多模型实现方式</h3><h4 id="用户级线程"><a href="#用户级线程" class="headerlink" title="用户级线程"></a>用户级线程</h4><p><img src="https://files.seeusercontent.com/2026/03/14/cm1U/image-20260314142057540.png" alt="image-20260314142057540"></p><p>早期操作系统只支持进程，不支持线程，当时的线程是使用<strong>线程库</strong>来实现的。</p><p>如果一个通讯软件需要并发运行三个任务，实际上是通过一段很丑陋且低效的代码逻辑来实现的。</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">()</span>{<br><span class="hljs-type">int</span> i=<span class="hljs-number">0</span>;<br><span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>)  {<br>  <span class="hljs-keyword">if</span>(i==<span class="hljs-number">0</span>){处理视频聊天的代码；}<br>  <span class="hljs-keyword">if</span>（i==<span class="hljs-number">1</span>){处理文字聊天的代码；}<br>  <span class="hljs-keyword">if</span>(i==<span class="hljs-number">2</span>){处理文件传输的代码；}<br>    i=(i+<span class="hljs-number">1</span>)%<span class="hljs-number">3</span>；<span class="hljs-comment">//i的0,1,2,0,1,2.·.</span><br>  }<br>}<br></code></pre></td></tr></tbody></table></figure><p>实际上很多的编程语言都提供了线程库，可以实现线程的创建，销毁，调度等功能，这比上面的循环要复杂的多。</p><p>在用户级线程中，线程管理工作是由应用进程来完成的，线程切换的工作也不需要CPU转换形态，且<strong>操作系统也无法意识到线程的存在</strong>。</p><p>优点：</p><ul><li>用户级线程的切换在用户空间即可完成，不需要切换到核心态，线程管理的系统开销更小。</li></ul><p>缺点：</p><ul><li>如果一个用户级线程被堵塞之后，整个进程都会被堵塞，并发性不高，且因为进程是调度的基本单位，所以无法在多核机器上并行运行。</li></ul><p>看会上面的循环，如果i==0，执行视频聊天代码时，如摄像头调用失败，那么就会一直等待，剩下的文字聊天和文件传输线程都无法运行</p><p>在 Linux 中，内核并不真正区分进程和线程，它把它们都视为 <strong>Task</strong>。线程在 Linux 内核里是通过 <code>clone()</code> 系统调用创建的，只是它和父进程共享了地址空间（内存）。</p><h4 id="内核级线程"><a href="#内核级线程" class="headerlink" title="内核级线程"></a>内核级线程</h4><p>大多数现代操作系统都实现了内核级线程，如Windows、Linux。</p><p><img src="https://files.seeusercontent.com/2026/03/14/s8Pf/image-20260314142126158.png" alt="image-20260314142126158"></p><p>内核级线程中，<strong>线程管理工作是由操作系统完成</strong>，线程切换需要CPU切换形态，操作系统能够察觉到线程的存在。</p><p>优点：</p><ul><li>当某个线程被堵塞，其他线程可以继续执行，并发能力强，多线程可在多核处理机上并发执行。</li><li>用户进程会占用多个内核级线程，线程切换由操作系统内核完成，需要切换状态，线程管理成本高，开销大。</li></ul><p>优缺点似乎是互补的，有没有办法将他们结合起来呢？</p><h3 id="多线程模型"><a href="#多线程模型" class="headerlink" title="多线程模型"></a>多线程模型</h3><p>根据用户级线程和内核级线程的映射关系就能够划分为几种多线程模型。</p><p><strong>一对一模型就是上面解释的内核级线程。</strong></p><p><strong>多对一模型就是上面解释的用户级线程</strong>。</p><p>多对多模型就是将n个用户及线程映射到m个内核级线程(n&gt;=m)。每个用户进程对应m个内核级线程。</p><p><img src="https://files.seeusercontent.com/2026/03/14/Rut4/image-20260314143540049.png" alt="image-20260314143540049"></p><p>多对多模型能够同时克服两个的缺点，且具有很高的自由度。</p><p>操作系统在运行和调度的时候，是以内核级线程作为基本单位的，所以，上面的模式因为只有2个内核级线程只能被分配进两个核。</p><h3 id="线程的状态与转换"><a href="#线程的状态与转换" class="headerlink" title="线程的状态与转换"></a>线程的状态与转换</h3><p>和进程很相像，在这不过多赘述。</p><p><img src="https://files.seeusercontent.com/2026/03/14/Ve5s/image-20260314145344466.png" alt="image-20260314145344466"></p><h3 id="线程的组织与控制"><a href="#线程的组织与控制" class="headerlink" title="线程的组织与控制"></a>线程的组织与控制</h3><p>类似于PCB，线程也有对应的数据结构和表。</p><p><img src="https://files.seeusercontent.com/2026/03/14/zIo8/image-20260314145419081.png" alt="image-20260314145419081"></p><h3 id="调度"><a href="#调度" class="headerlink" title="调度"></a>调度</h3><p>调度是<strong>指在资源有限的情况下，根据一定的规则或策略，对任务、进程或资源进行安排和分配的过程。</strong>其核心目标是优化系统整体效率，确保任务有序、高效地完成。</p><h3 id="高级调度-作业调度"><a href="#高级调度-作业调度" class="headerlink" title="高级调度(作业调度)"></a>高级调度(作业调度)</h3><p>作业就是一个具体的任务。</p><p>用户向系统提交一个作业就是<strong>用户让操作系统启动一个程序。</strong></p><p>高级调度（作业调度）。按一定的原则从外存的作业后备队列中挑选一个作业调入内存，成列入“就绪队列”，并创建进程。每个作业只调入一次，调出一次。作业调入时会建立PCB,调出时才撤销PCB。</p><p><img src="C:\Users\willm\AppData\Roaming\Typora\typora-user-images\image-20260315121319810.png" alt="image-20260315121319810"></p><p>为什么需要挑选？因为<strong>内存空间有限，有时无法将用户提交的作业全部放入内存。</strong></p><h3 id="低级调度-进程调度"><a href="#低级调度-进程调度" class="headerlink" title="低级调度(进程调度)"></a>低级调度(进程调度)</h3><p>低级调度：按照某中策略从就绪队列中选取一个<strong>进程</strong>，将处理机分配给他。</p><p>低级调度是实现并发的一个关键机制，只有高频执行才能让用户认为这是同步发生的。</p><p><img src="https://files.seeusercontent.com/2026/03/15/teE4/image-20260315121435981.png" alt="image-20260315121435981"></p><p>进程调度是操作系统中最基本的一种调度，执行频率很高，几十毫秒一次。</p><h3 id="中级调度-内存调度"><a href="#中级调度-内存调度" class="headerlink" title="中级调度(内存调度)"></a>中级调度(内存调度)</h3><p>内存不足时，<strong>可以将某些进程的数据调出外存，等到内存空闲时在重新调入内存。</strong></p><p>暂时调到外存等待的进程状态为挂起状态，被挂起的进程PCB会被组织成挂起队列。</p><p>中级调度（内存调度）一一按照某种策略决定将哪个处于挂起状态的进程重新调入内存。</p><p><img src="https://files.seeusercontent.com/2026/03/15/Zwr5/image-20260315121837447.png" alt="image-20260315121837447"></p><p>进程可能会被多次调出多次调入，所以中级调度发生的频率要比高级调度更高。</p><p>有了调度的知识，我们就可以扩充之前的进程五态模型成为七状态模型。</p><p><img src="https://files.seeusercontent.com/2026/03/15/s2mR/image-20260315122947664.png" alt="image-20260315122947664"></p><p>挂起和阻塞的区别：<strong>挂起时进程在外存，阻塞时进程还在内存当中。</strong></p><p>你会看到很多箭头，因为操作系统不同，对挂起的方式是不同的。</p><table><thead><tr><th></th><th>要做什么</th><th>调度发生在..</th><th>发生频 率</th><th>对进程状态的影响</th></tr></thead><tbody><tr><td>高级调度 （作业调度）</td><td>按照某种规则，从后备队列 中选择合适的作业将其调入 内存，并为其创建进程</td><td>外存→内存 (面向作业)</td><td>最低</td><td>无→创建态→就绪态</td></tr><tr><td>中级调度 (内存调度)</td><td>按照某种规则，从挂起队列 中选择合适的进程将其数据 调回内存</td><td>外存→内存 (面向进程)</td><td>中等</td><td>挂起态→就绪态 (阻塞挂起→阻塞态）</td></tr><tr><td>低级调度 (进程调度）</td><td>按照某种规则，从就绪队列 中选择一个进程为其分配处 理机</td><td>内存→CPU</td><td>最高</td><td>就绪态→运行态</td></tr></tbody></table><blockquote><p><strong>在现代操作系统中，低级调度（CPU调度）的实际执行单位是线程，而不是整个进程。</strong> 但当人们说“调度进程”时，通常指的是调度一个<strong>只有单个线程的进程</strong>，或者是以该进程的<strong>主线程</strong>为代表。</p></blockquote><h3 id="进程调度的时机"><a href="#进程调度的时机" class="headerlink" title="进程调度的时机"></a>进程调度的时机</h3><p>先来看看需要进行进程调度与切换的情况</p><ul><li>当前运行的进程主动放弃处理机</li><li><ul><li>进程正常终止</li><li>运行过程中发生异常而终止</li><li>进程主动请求阻塞</li></ul></li><li>当前运行的进程被动放弃处理机</li><li><ul><li>分给进程的时间片用完</li><li>有更紧急的事情处理如IO中断</li><li>有更高优先级的进程进入就绪队列</li></ul></li></ul><p>还有些时候不能进程调度：</p><ul><li>处理中断时，这段时间与硬件密切相关，难以进行进程切换。</li><li>进程在操作系统内核程序临界区中</li><li>原语操作是不可中断的。</li></ul><p>我们详细解释第二条，一部分资源被称为<strong>临界资源</strong>，一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源。</p><p>临界区：<strong>访问临界资源的代码</strong></p><p>内核程序临界区一般是用来访问某种内核数据结构的，比如进程的就绪队列（由各就绪进程的PCB组成）</p><p><img src="https://files.seeusercontent.com/2026/03/15/fo6I/image-20260315130009657.png" alt="image-20260315130009657"></p><p>记得Java的同步锁吗？进程访问就绪队列时，操作系统会给<strong>就绪队列</strong>上锁，使之在这段时间内只能被一个进程访问，<strong>在程序退出临界区之前同步锁不会解开</strong>。因为进行进程调度是需要访问就绪队列的，但是这里被同步锁拒绝访问了，所以无法进行进程调度。</p><p>如果进程正在访问普通的临界资源，比如打印机，<strong>那么打印机会被上锁</strong>，在同一时间内只允许一个进程访问。但是打印机是慢速设备，打印完成之前不会解锁，那这段时间就只能等待，这会导致CPU效率严重下滑。</p><p>所以，普通临界区访问的临界资源不会直接影响操作系统的内核管理，这里可以进行进程调度。</p><blockquote><p>有的操作系统只允许进程主动放弃处理机。有的则两个都支持。</p></blockquote><p>就此，我们能够分为两种调度方式。</p><ul><li><strong>非剥夺调度方式，又称非抢占方式</strong>。即，只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达，当前进程依然会继续使用处理机，直到该进程终止或主动要求进入阻塞态。</li><li><ul><li>实现简单，系统开销小，但是无法处理紧急任务，只用于早期系统</li></ul></li><li><strong>剥夺调度方式，又称抢占方式</strong>。当一个进程正在处理机上执行时，如果有一个更重要或更紧迫的进程需要使用处理机，则立即暂停正在执行的进程，将处理机分配给更重要紧迫的那个进程。</li><li><ul><li>可以优先处理更紧急进程，被用于分时操作系统和实时操作系统。</li></ul></li></ul><h4 id="进程切换与调度"><a href="#进程切换与调度" class="headerlink" title="进程切换与调度"></a>进程切换与调度</h4><p>“狭义的进程调度”与“进程切换”的区别：<br>狭义的进程调度指的是从就绪队列中选中一个要运行的进程。（这个进程可以是刚刚被暂停执行的进程，也可能是另一个进程，后一种情况就需要进程切换)进程切换是指一个进程让出处理机，由另一个进程占用处理机的过程。<br>广义的进程调度包含了选择一个进程和进程切换两个步骤。</p><p>而进程切换完成了两件事：</p><ul><li>保存原来运行的进程的各种数据</li><li>对新的进程的各种数据的恢复</li></ul><p>注意：<strong>进程切换是有代价的</strong>，因此如果过于频繁的进行进程调度、切换，必然会使整个系统的效率降低，使系统大部分时间都花在了进程切换上，而真正用于执行进程的时间减少。</p><h3 id="调度器-x2F-调度程序"><a href="#调度器-x2F-调度程序" class="headerlink" title="调度器/调度程序"></a>调度器/调度程序</h3><p>调度程序是计算机系统中<strong>负责管理和分配资源的核心组件</strong>，它的任务有两条：</p><ul><li>让谁运行 — 调度算法</li><li>运行多长时间  —时间片大小</li></ul><p>而调度时机，就是什么事件会触发“调度程序”？</p><ul><li>创建新进程</li><li>进程退出</li><li>运行进程出现阻塞</li><li>IO中断发生</li><li>抢占式调度策略，每个时钟中断会触发调度程序工作</li><li>非抢占式调度策略，只有运行的进程阻塞或退出才触发调度程序工作</li></ul><p><img src="https://files.seeusercontent.com/2026/03/16/haV3/image-20260316105542528.png" alt="image-20260316105542528"></p><h3 id="闲逛进程"><a href="#闲逛进程" class="headerlink" title="闲逛进程"></a>闲逛进程</h3><p>调度程序永远的备胎，没有其他就绪进程时，运行**闲逛进程(**idle)，具有以下特点：</p><ul><li>优先级最低</li><li>可以是零地址指令，如nop指令</li><li>能耗低</li></ul><p>就像你闲着没事会抖腿一样，<strong>CPU实际上永远都不会有空闲期。</strong></p><h3 id="调度算法的评价指标"><a href="#调度算法的评价指标" class="headerlink" title="调度算法的评价指标"></a>调度算法的评价指标</h3><p>CPU的造价是极其昂贵的，人类需要让CPU尽可能工作</p><p>CPU利用率指的是<strong>忙碌的时间/总时间</strong></p><p>系统吞吐量指的是<strong>单位时间内完成作业的数量，也就是完成作业的数量/花的时间</strong></p><p>而周转时间，就是从作业被提交给系统开始，到作业被完成的时间间隔。他包含四个时间：</p><ul><li><strong>作业在外存中等待高级调度的时间</strong></li><li><strong>进程在就绪队列上等待低级调度的时间</strong></li><li><strong>在CPU上执行的时间</strong></li><li><strong>进程等待IO操作完成的时间</strong></li></ul><p>所以，作业的周转时间等于作业的完成时间-作业的提交时间。</p><p>平均周转时间就是取平均数。</p><p>带权周转时间=周转时间/实际运行时间，这个值越小越好。</p><p>等待时间：进程/作业处在后备队列中等待的时间，等待时间越长，用户越难受。</p><p><img src="https://files.seeusercontent.com/2026/03/16/sjV7/image-20260316120508720.png" alt="image-20260316120508720"></p><p>响应时间，<strong>指从用户提交请求到首次产生响应所用的时间。</strong></p><h3 id="调度算法"><a href="#调度算法" class="headerlink" title="调度算法"></a>调度算法</h3><p>在此之前，先明确进程与作业的区别。</p><table><thead><tr><th>对比维度</th><th>作业</th><th>进程</th></tr></thead><tbody><tr><td><strong>定义与视角</strong></td><td>用户向系统提交的一个任务（程序+数据+说明书）。</td><td>程序在内存中的一次执行过程，是系统进行资源分配和调度的基本单位。</td></tr><tr><td><strong>状态</strong></td><td>静态的。存在于外存（如磁盘）中，等待被调度进入内存执行。</td><td>动态的。存在于内存中，具有创建、就绪、运行、阻塞、终止等多种状态。</td></tr><tr><td><strong>生命周期</strong></td><td>从提交开始，到完成结束。</td><td>从被创建（通常由作业调度产生）开始，到被撤销结束。</td></tr><tr><td><strong>主要关系</strong></td><td>一个作业通常至少对应一个进程。一个大型作业（如编译任务）可能被拆分为多个进程来协作完成。</td><td>进程是作业的执行体现。一个进程必然来源于某个作业（或用户交互命令）。</td></tr></tbody></table><p>你可以把“作业”想象成厨师收到的一张<strong>订单</strong>（上面写着要做哪道菜以及要求），而“进程”就是厨师在厨房里<strong>实际烹饪</strong>这道菜的过程。</p><p>我们会结构化地分析算法：</p><ul><li>算法思想</li><li>算法规则</li><li>用于作业调度还是进程调度(高级调度还是低级调度)</li><li>抢占式？非抢占式？</li><li>优点和缺点</li><li>是否会导致饥饿(进程/作业长时间等待)。</li></ul><h4 id="先来先服务FCFS"><a href="#先来先服务FCFS" class="headerlink" title="先来先服务FCFS"></a>先来先服务FCFS</h4><p>先来先服务调度算法：<strong>按照到达的先后顺序调度，事实上就是等待时间越久的越优先得到服务。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/17/lk1T/image-20260317111726085.png" alt="image-20260317111726085"></p><table><thead><tr><th>算法思想</th><th>主要从“公平”的角度考虑（类似于我们生活中排队买东 西的例子）</th></tr></thead><tbody><tr><td>算法规则</td><td>按照作业/进程到达的先后顺序进行服务</td></tr><tr><td>用于作业/进程调度</td><td>用于作业调度时，考虑的是哪个作业先到达后备队列；用 于进程调度时，考虑的是哪个进程先到达就绪队列</td></tr><tr><td>是否可抢占？</td><td>非抢占式的算法</td></tr><tr><td>优缺点</td><td>优点：公平、算法实现简单 缺点：排在长作业（进程）后面的短作业需要等待很长时 间，带权周转时间很大，对短作业来说用户体验不好。即，FCFS算法对长作业有利，对短作业不利（Eg：排队买奶茶…）</td></tr><tr><td>是否会导致饥饿</td><td>不会，总会有轮到进程的时候</td></tr></tbody></table><p>FCFS不是很优秀。</p><h4 id="短作业优先SJF"><a href="#短作业优先SJF" class="headerlink" title="短作业优先SJF"></a>短作业优先SJF</h4><blockquote><p>作业调度是SJF，进程调度是SPF，也就是中间的单词不一样 ，一个是Job作业，一个是Progress进程</p></blockquote><p>短作业/进程优先调度算法：<strong>每次调度时选择当前已到达且运行时间最短的作业/进程。</strong></p><p>很多情况下只有非抢占式的，但是也有抢占式的。</p><p>非抢占式短作业优先算法：</p><p><img src="https://files.seeusercontent.com/2026/03/17/biF5/image-20260317112705426.png" alt="image-20260317112705426"></p><p>抢占式，又名最短剩余时间优先算法：</p><p>最短剩余时间优先算法：每当有进程加入就绪队列改变时就需要调度，如果新到达的进程剩余时间比当前运行的进程剩余时间更短，则由新进程抢占处理机，当前运行进程重新回到就绪队列。另外，当一个进程完成时也需要调度。</p><p>这种算法如果遇见了更短的进程抵达，会将正在运行的进程从处理机上拉下来，自己上位处运行。被拉下来的进程能够断点续传，已经运行的时间不会重置</p><p><img src="https://files.seeusercontent.com/2026/03/17/Pv6u/image-20260317113457792.png" alt="image-20260317113457792"></p><table><thead><tr><th>算法思想</th><th>追求最少的平均等待时间，最少的平均周转时间、最少的平均平均带权周转时间</th></tr></thead><tbody><tr><td>算法规则</td><td>最短的作业/进程优先得到服务（所谓“最短”，是指要求服务时间最短)</td></tr><tr><td>用于作业和进程调度</td><td>即可用于作业调度，也可用于进程调度。用于进程调度时称为“短进程优先(SPE,Shortest Process First)算法”</td></tr><tr><td>抢占或非抢占</td><td>SF和SPF是非抢占式的算法。但是也有抢占式的版本一一最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)</td></tr><tr><td>优缺点</td><td>优点：“最短的”平均等待时间、平均周转时间缺点：不公平。对短作业有利，对长作业不利。可能产生饥饿现象。另外，作业/进程的运行时间是由用户提供的，并不一定真实，不一定能做到真正的短作业优先</td></tr><tr><td>饥饿？</td><td>会。如果源源不断地有短作业/进程到来，可能使长作业/进程长时间得不到服务，产生“饥饿”现象。如果一直得不到服务，则称为“饿死”</td></tr></tbody></table><p>在<strong>所有进程同时到达</strong>或者<strong>非抢占式</strong>的前提下，SJF 的平均等待时间/周转时间是最优的。而在抢占式环境下，<strong>SRTN（最短剩余时间优先）</strong> 才是理论上的最优。</p><p>还是一样的，取两者精华，就有了新的算法。</p><h3 id="高响应比优先HRRN"><a href="#高响应比优先HRRN" class="headerlink" title="高响应比优先HRRN"></a>高响应比优先HRRN</h3><p>$$<br>\text{响应比}=\frac{\text{等待时间}+\text{要求服务时间}}{\text{要求服务时间}}<br>$$</p><p><img src="https://files.seeusercontent.com/2026/03/17/2dOz/image-20260317160828345.png" alt="image-20260317160828345"></p><table><thead><tr><th>算法思想</th><th>要综合考虑作业/进程的等待时间和要求服务的时间</th></tr></thead><tbody><tr><td>算法规则</td><td>在每次调度时先计算各个作业/进程的响应比，选择响应比最高的为其服务</td></tr><tr><td>用于作业/进程调度</td><td>即可用于作业调度，也可用于进程调度</td></tr><tr><td>是否可抢占？</td><td>非抢占式的算法。因此只有当前运行的作业/进程主动放弃 处理机时，才需要调度，才需要计算响应比</td></tr><tr><td>优缺点</td><td>综合考虑了等待时间和运行时间 (要求服务时间) 等待时间相同时，要求服务时间短的优先（SJF的优点） 要求服务时间相同时，等待时间长的优先（FCFS的优点）</td></tr><tr><td>是否会导致饥饿</td><td>不会， 对于长作业来说，随着等待时间越来越久，其响应比也会 越来越大，从而避免了长作业饥饿的问题</td></tr></tbody></table><p>注：这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标，但是不关心“响应时间”，也并不区分任务的紧急程度，因此对于用户来说，交互性很糟糕，用户无法手动参与调度，很多时候我们需要首先处理某个任务，但你无法让CPU这么做。所以，以上三种只用于批处理系统。</p><h3 id="时间片轮转调度RR"><a href="#时间片轮转调度RR" class="headerlink" title="时间片轮转调度RR"></a>时间片轮转调度RR</h3><p>提倡进程公平性的算法，注意时间片只能影响进程而不是作业。</p><p><strong>最常用于分时操作系统，最注重响应时间，所以不计算周转时间。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/17/i7Ka/image-20260317164324514.png" alt="image-20260317164324514"></p><p><img src="https://files.seeusercontent.com/2026/03/17/4zEg/image-20260317164656124.png" alt="image-20260317164656124"></p><p>这个算法类似于<strong>循环队列</strong>，时间片的设定对性能的影响是非常显著的。如果时间片太大，使得每个进程都可以在一个时间片内就完成，则时间片轮转调度算法退化为先来先服务调度算法，并且会增大进程响应时间。因此时间片不能太大。反之如果太小，进程的切换就会过于频繁，那花在切换上的时间代价就很大，这也会导致性能的下降 。</p><p>一般来说，设计时间片时要让切换进程的开销占比不超过1%。</p><table><thead><tr><th>算法思想</th><th>公平地、轮流地为各个进程服务，让每个进程在一定时间 间隔内都可以得到响应</th></tr></thead><tbody><tr><td>算法规则</td><td>按照各进程到达就绪队列的顺序，轮流让各个进程执行一 个时间片（如100ms）。若进程未在一个时间片内执行完， 则剥夺处理机，将进程重新放到就绪队列队尾重新排队。</td></tr><tr><td>用于作业/进程调度</td><td>用于进程调度（只有作业放入内存建立了相应的进程后， 才能被分配处理机时间片）</td></tr><tr><td>是否可抢占？</td><td>若进程未能在时间片内运行完，将被强行剥夺处理机使用 权，因此时间片轮转调度算法属于抢占式的算法。由时钟 装置发出时钟中断来通知CPU时间片已到</td></tr><tr><td>优缺点</td><td>优点：公平；响应快，适用于分时操作系统； 缺点：由于高频率的进程切换，因此有一定开销；不区分任务紧急程度</td></tr><tr><td>饥饿?</td><td>不会</td></tr></tbody></table><h3 id="优先级调度算法"><a href="#优先级调度算法" class="headerlink" title="优先级调度算法"></a>优先级调度算法</h3><p>这个算法极强依赖用户自主设计的优先级，相当于把调度问题抛给了用户。这个算法我觉得很好理解。</p><p><img src="https://files.seeusercontent.com/2026/03/17/r6Rw/image-20260317171025306.png" alt="image-20260317171025306"></p><p>就绪队列未必只有一个，可以按照不同优先级来组织。另外，也可以把优先级高的进程排在更靠近队头的位置。</p><p>而且，根据优先级确定后是否能够改变，能够分为静态优先级和动态优先级。</p><p>系统进程优先级高于用户进程<br>前台进程优先级高于后台进程<br>操作系统更偏好I/O型进程，如果让IO设备优先运行，能够让它尽早投入工作，使得CPU的利用率，吞吐量能够提升。</p><p>动态优先级则更灵活。可以从追求公平、提升资源利用率等角度考虑如果某进程在就绪队列中等待了很长时间，则可以适当提升其优先级。如果某进程占用处理机运行了很长时间，则可适当降低其优先级。</p><p>这么来看，高响应比也算动态优先级调度算法。</p><table><thead><tr><th>算法思想</th><th>随着计算机的发展，特别是实时操作系统的出现，越来越 多的应用场景需要根据任务的紧急程度来决定处理顺序</th></tr></thead><tbody><tr><td>算法规则</td><td>调度时选择优先级最高的作业/进程</td></tr><tr><td>用于作业/进程调度</td><td>既可用于作业调度，也可用于进程调度。甚至，还会用于 在之后会学习的I/O调度中</td></tr><tr><td>是否可抢占?</td><td>抢占式、非抢占式都有。做题时的区别在于：非抢占式只 需在进程主动放弃处理机时进行调度即可，而抢占式还需 在就绪队列变化时，检查是否会发生抢占。</td></tr><tr><td>优缺点</td><td>优点：用优先级区分紧急程度、重要程度，适用于实时操 作系统。可灵活地调整对各种作业/进程的偏好程度。 缺点：若源源不断地有高优先级进程到来，则可能导致饥饿</td></tr><tr><td>饥饿？</td><td>会</td></tr></tbody></table><p>一样的，我们希望钝化以上算法尖锐的地方，取得一个折中的方案。</p><h3 id="多级反馈队列调度算法"><a href="#多级反馈队列调度算法" class="headerlink" title="多级反馈队列调度算法"></a>多级反馈队列调度算法</h3><blockquote><p>这是最后一个，也是最难理解的算法了。</p></blockquote><p>设置多级就绪队列，各级队列优先级从高到低，时间片从小到大<br>新进程到达时先进入第1级队列，按FCFS原则排队等待被分配时间片。若用完时间片进程还未结束，则进程进入下一级队列队尾。如果此时己经在最下级的队列，则重新放回最下级队列队尾<br>只有第k级队列为空时，才会为k+1级队头的进程分配时间片<br>被抢占处理机的进程重新放回原队列队尾。</p><blockquote><p>但如果进程是由于<strong>发生 I/O 阻塞</strong>主动放弃 CPU，通常在再次就绪时，它会回到<strong>原来的那一级队列</strong>（甚至有的算法会提升它的优先级）。</p></blockquote><p><img src="https://files.seeusercontent.com/2026/03/17/1ywH/image-20260317194047613.png" alt="image-20260317194047613"></p><p>复杂的好处就是足够高效。</p><table><thead><tr><th>算法思想</th><th>其他调度算法的折中</th></tr></thead><tbody><tr><td>算法规则</td><td>1.设置多级就绪队列，各级队列优先级从高到低，时间片从小到大2.新进程到达时先进入第1级队列，按FCFS原则排队等待被分配时间片，若用完时间片进程还未结束，则进程进入下一级队列队尾如果此时己经是在最下级的队列，则重新放回该队列队尾3.只有第k级队列为空时，才会为k+1级队头的进程分配时间片</td></tr><tr><td>作业/进程调度</td><td>进程调度</td></tr><tr><td>抢占式/非抢占式</td><td>抢占式的算法。在k级队列的进程运行过程中，若更上级的队列(1k-1级)中进入了一个新进程，则由于新进程处于优先级更高的队列中，因此新进程会抢占处理机，原来运行的进程放回k级队列队尾。</td></tr><tr><td>优缺点</td><td>对各类型进程相对公平(FCFS的优点)；每个新到达的进程都可以很快就得到响应(RR的优点)；短进程只用较少的时间就可完成(SPF的优点)；不必实现估计进程的运行时间（避免用户作假）：可灵活地调整对各类进程的偏好程度，比如CPU密集型进程、I/O密集型进程（拓展：可以将因I/O而阻塞的进程重新放回原队列，这样I/O型进程就可以保持较高优先级)</td></tr><tr><td>饥饿？</td><td>会</td></tr></tbody></table><h3 id="多级队列调度算法"><a href="#多级队列调度算法" class="headerlink" title="多级队列调度算法"></a>多级队列调度算法</h3><blockquote><p>这是针对就绪队列的算法，之前说的是“单一调度策略”，而这个是一个调度框架或者模型，它首先<strong>改变了就绪队列的结构</strong>（分成多个队列），然后<strong>可以</strong>在每个子队列中<strong>采用</strong> FCFS、RR 等不同的单一策略。</p></blockquote><p>系统中按进程类型设置多个队列，进程创建成功后插入某个队列。</p><p><img src="https://files.seeusercontent.com/2026/03/17/1wjF/image-20260317195509306.png" alt="image-20260317195509306"></p><p>队列之间可采取固定优先级，或时间片划分固定优先级：高优先级空时低优先级进程才能被调度时间片划分：如三个队列分配时间50%、40%、10%。</p><p>各队列可采用不同的调度策略，如系统进程队列采用优先级调度交互式队列采用RR批处理队列采用FCFS。</p><h3 id="多处理机调度"><a href="#多处理机调度" class="headerlink" title="多处理机调度"></a>多处理机调度</h3><p>CPU是多核的，这也需要调度。</p><p>单处理机调度只需要决定让哪个就绪进程优先上处理机即可。</p><p>而多处理机调度，还需决定被调度的进程上哪个处理机。</p><p>在多处理机调度中，应该力求做到：</p><ul><li><strong>负载均衡</strong>：尽可能让每个CPU都同等忙碌，避免出现一核工作，八核围观的情况。</li><li><strong>处理器亲和性</strong>：尽量让一个进程调度到同一个CPU上运行，以发挥CPU的缓存作用</li></ul><h4 id="公共就绪队列"><a href="#公共就绪队列" class="headerlink" title="公共就绪队列"></a>公共就绪队列</h4><p><img src="https://files.seeusercontent.com/2026/03/17/Uax7/image-20260317204936311.png" alt="image-20260317204936311"></p><ul><li><strong>所有CPU共享同一个就绪进程队列</strong></li><li><strong>每个CPU运行调度程序，从公共队列中选择一个运行。</strong></li><li><strong>每个CPU访问公共就绪队列时需要上锁保证互斥访问。</strong></li></ul><p>优点：能够天然实现负载均衡。</p><p>缺点：各个进程频繁切换CPU，亲和性差。</p><p>为了提升处理机亲和性：<br>软亲和：由进程调度程序尽量保证“亲和性”<br>硬亲和：由用户进程通过系统调用，主动要求操作系统分配固定的CPU,确保“亲和性”</p><h4 id="私有就绪队列"><a href="#私有就绪队列" class="headerlink" title="私有就绪队列"></a>私有就绪队列</h4><ul><li>每个CPU都有一个私有就绪队列</li><li>CPU空闲时运行调度程序，从私有就绪队列中选择一个进程运行</li></ul><p>私有队列天然实现了亲和性。</p><p>私有队列需要思考如何实现负载均衡的问题：</p><p>推迁移(Push)策略：一个特定的系统程序周期性检查每个处理器的负载，如果负载不平衡，就从忙碌CPU的就绪队列中“推”一些就绪进程到空闲CPU的就绪队列。包工头重新分配任务。</p><p>拉迁移(pull)策略：每个CPU运行调度程序时，周期性检查自身负载与其他CPU负载。如果一个CPU负载很低，就从其他高负载CPU的就绪队列中“拉”一些就绪进程到自己的就绪队列。自己空闲就主动揽活干。</p><h3 id="进程同步和互斥"><a href="#进程同步和互斥" class="headerlink" title="进程同步和互斥"></a>进程同步和互斥</h3><blockquote><p>我们之前说过，进程具有异步性的特征，并发执行的进程以各自独立的不可预知的速度前进。</p></blockquote><p>有的时候<strong>异步</strong>会带来不必要的等待，所以异步性是需要解决的问题，保证其不会触发不必要的等待。</p><p><img src="https://files.seeusercontent.com/2026/03/19/lX7f/image-20260319122202392.png" alt="image-20260319122202392"></p><p>比如在管道通信中：<strong>读进程和写进程</strong>并发地运行，由于并发必然导致异步性，因此“写数据”和“读数据”两个操作执行的先后顺序是不确定的。而实际应用中，又必须按照“写数据→读数据”的顺序来执行的。<br>如何解决这种异步问题，就是“进程同步”所讨论的内容。</p><p>所以同步亦称<strong>直接制约关系</strong>，它是指为完成某种任务而建立的两个或多个进程，这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。</p><p>进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源（比如内存，又比如打印机、摄像头这样的I/O设备），而共享又分为互斥共享和同时共享。</p><p>我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备（比如摄像头、打印机）都属于临界资源。此外还有许多变量、数据、内存缓冲区等部属于临界资源。对这些临界资源的访问必须互斥进行。</p><p>对临界资源的互斥访问，能够认定为以下代码逻辑：</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">do</span> {<br>entry section;  <span class="hljs-comment">//进入区</span><br>critical section; <span class="hljs-comment">//临界区,又名临界段</span><br><span class="hljs-built_in">exit</span> section;<span class="hljs-comment">//退出区</span><br>remainder section;<span class="hljs-comment">//剩余区</span><br>}<span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>)<br></code></pre></td></tr></tbody></table></figure><p>进入区检查是否可进入临界区，若可进入，会设置“正在访问临界资源”标志（上同步锁），阻止其他同时进如临界区。临界区是访问临界资源的那段代码，退出区负责解除“正在访问临界资源”标志(解同步锁),剩余区负责做其他处理。所以临界区是进程中访问临界资源的代码段而进入区和退出区是负责实现互斥的代码段。</p><p>为了实现对临界资源的互斥访问，同时保证系统整体性能，需要遵循以下原则：</p><ul><li><strong>空闲让进</strong>：临界区空闲时，可以允许一个请求进入临界区的进程立即进入临界区；</li><li><strong>忙则等待</strong>：当己有进程进入临界区时，其他试图进入临界区的进程必须等待：</li><li><strong>有限等待</strong>：对请求访问的进程，应保证能在有限时间内进入临界区（保证不会饥饿）。</li><li><strong>让权等待</strong>：当进程不能进入临界区时，应立即释放处理机，防止进程忙等待。也就是进程需要的资源处于繁忙时，他需要进入阻塞态而不是占着处理机不放。</li></ul><h3 id="进程互斥的软件实现方法"><a href="#进程互斥的软件实现方法" class="headerlink" title="进程互斥的软件实现方法"></a>进程互斥的软件实现方法</h3><h3 id="单标志法"><a href="#单标志法" class="headerlink" title="单标志法"></a>单标志法</h3><p>算法思想：<strong>两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。</strong></p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">int</span> turn = <span class="hljs-number">0</span>;<span class="hljs-comment">//可认为是谦让变量</span><br><span class="hljs-comment">//P0进程：</span><br><span class="hljs-keyword">while</span> (turn != <span class="hljs-number">0</span>);    <span class="hljs-number">1</span><br>critical section;     <span class="hljs-number">2</span><br>turn = <span class="hljs-number">1</span>;             <span class="hljs-number">3</span><br>remainder section;    <span class="hljs-number">4</span><br><span class="hljs-comment">//P1进程：</span><br><span class="hljs-keyword">while</span> (turn != <span class="hljs-number">1</span>);    <span class="hljs-number">5</span><br>critical section;     <span class="hljs-number">6</span><br>turn = <span class="hljs-number">0</span>;             <span class="hljs-number">7</span><br>remainder section;    <span class="hljs-number">8</span><br></code></pre></td></tr></tbody></table></figure><p>turn的初值为0，即刚开始只允许0号进程进入临界区。若P1先上处理机运行，则会一直卡在⑤。直到P1的时间片用完，发生调度，切换P0上处理机运行。代码①不会卡住P0,P0可以正常访问临界区，在P0访问临界区期间即时切换回P1,P1依然会卡在⑤，随后发生调度，P0重新上处理机。只有P0在退出区将turn改为1后，P1才能进入临界区。</p><p>每个进程的流程都是：</p><ul><li>检查资源是否轮到自己用</li><li>访问临界资源</li><li>将谦让变量设置为除自己以外的数值</li><li>做其他事情</li></ul><p>这种算法有一个问题：临界资源只能轮流使用，且如果P1进程下线，那么P0进程将turn设置为1后P0就再也无法使用资源。这就违反了<strong>空闲让进</strong>的原则。</p><h3 id="双标志先检查法"><a href="#双标志先检查法" class="headerlink" title="双标志先检查法"></a>双标志先检查法</h3><p>算法思想：设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿，比如“flag[0]=ture”意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区，如果没有，则把自身对应的标志flag[i]设为true,之后开始访问临界区。</p><p><img src="https://files.seeusercontent.com/2026/03/19/w4oX/image-20260319163005241.png" alt="image-20260319163005241"></p><p>每个进程的流程是：</p><ul><li>检查对方(除自己之外的其他人)是否想要使用</li><li>表达想用的意愿，上锁</li><li>访问临界资源</li><li>退出，解锁。</li></ul><p>这个算法同样是有问题的， 如果P0P1两个进程是并发进行的话，那么P0上处理机执行，发现第1条指令符合，准备执行第二条指令时发生切换，P1上处理机执行第5条指令，随后执行第6条设置为true后切换，P0执行第2条指令也设置为true，这会导致双true同时使用临界资源。所以该算法违反了<strong>“忙且等待”</strong>原则。</p><p>原因是检查和上锁这两个步骤不具备原语的连贯性。</p><h3 id="双标志后检查法"><a href="#双标志后检查法" class="headerlink" title="双标志后检查法"></a>双标志后检查法</h3><p>算法思想：双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”，但是这两个操作又无法一气呵成，因此导致了两个进程同时进入临界区的问题。因此，人们又想到先“上锁”后“检查”的方法，来避免上述问题。</p><p><img src="https://files.seeusercontent.com/2026/03/19/3kbI/image-20260319164437728.png" alt="image-20260319164437728">但是如果按照1，5,2,6的顺序执行，会出现P0P1都无法进入临界区的现象。</p><p>因此，双标志后检查法虽然解决了“忙则等待”的问题，但是又违背了“空闲让进”和“有限等待”原则，会因各进程都长期无法访问临界资源而产生“饥饿”现象。</p><h3 id="Peterson皮特森算法："><a href="#Peterson皮特森算法：" class="headerlink" title="Peterson皮特森算法："></a>Peterson皮特森算法：</h3><p>算法思想：结合双标志法、单标志法的思想。如果双方都争着想进入临界区，那可以让进程尝试“孔融让梨”（谦让)。做一个有礼貌的进程。</p><p><img src="https://files.seeusercontent.com/2026/03/19/2Uay/image-20260319170704422.png" alt="image-20260319170704422"></p><p>谁最后表达谦让，谁就失去了行动的优先权。</p><p>Peterson算法用软件方法解决了进程互斥问题，遵循了空闲让进、忙则等待、有限等待三个原则，但是依然未遵循<strong>让权等待</strong>的原则。</p><p>该算法在无法进入临界区时会卡在循环而不是让出处理机。</p><h3 id="进程互斥的硬件实现方法"><a href="#进程互斥的硬件实现方法" class="headerlink" title="进程互斥的硬件实现方法"></a>进程互斥的硬件实现方法</h3><blockquote><p>这里可能依赖计算机组成原理的知识。</p></blockquote><h3 id="中断屏蔽法"><a href="#中断屏蔽法" class="headerlink" title="中断屏蔽法"></a>中断屏蔽法</h3><p>利用“开/关中断指令”实现（与原语的实现思想相同，即在某进程开始访问临界区到结束访问为止都不允许被中断，也就不能发生进程切换，因此也不可能发生两个同时访问临界区的情况)</p><p>优点是简单高效，缺点是不适用于多处理机：只适用于操作系统内核进程，不适用于用户进程（因为开/关中断指令只能运行在内核态，这组指令如果能让用户随意使用会很危险)</p><h3 id="TestAndSet指令"><a href="#TestAndSet指令" class="headerlink" title="TestAndSet指令"></a>TestAndSet指令</h3><p>又名TS指令，TestAndSetLock指令或者TSL指令。</p><p>TSL指令是用硬件实现的，执行的过程不允许被中断，只能一气呵成</p><p><img src="https://files.seeusercontent.com/2026/03/19/2nqA/image-20260319192526553.png" alt="image-20260319192526553"></p><p>若刚开始lock是false,则TSL返回的old值为false,while循环条件不满足，直接跳过循环，进入临界区。若刚开始lock是true,则执行TLS后old返回的值为true,while循环条件满足，会一直循环，直到当前访问临界区的进程在退出区进行“解锁”相比软件实现方法，TSL指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。</p><p>优点：实现简单，无需像软件实现方法那样严格检查是否会有逻辑漏洞；适用于多处理机环境</p><p>缺点：不满足“让权等待”原则，暂时无法进入临界区内的进程会占用CPU并循环执行TSL，造成忙等。</p><h3 id="Swap指令"><a href="#Swap指令" class="headerlink" title="Swap指令"></a>Swap指令</h3><p>别名Exchange指令，简称XCHG指令。</p><p>用硬件实现，执行过程中不允许中断。</p><img src="https://files.seeusercontent.com/2026/03/19/mtH0/image-20260319193502058.png" alt="image-20260319193502058" style="zoom:150%;"><p>逻辑上来看Swap和TSL并无太大区别，都是先记录下此时临界区是否已经被上锁（记录在old变量上)，再将上锁标记lock设置为true,最后检查old,如果old为false则说明之前没有别的进程对临界区上锁，则可跳出循环，进入临界区。</p><p>优点和缺点和TSL指令是一样的。</p><h3 id="互斥锁"><a href="#互斥锁" class="headerlink" title="互斥锁"></a>互斥锁</h3><p>解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁；在退出临界区时释放锁。函数acquire()获得锁，而函数release()释放锁。每个互斥锁有一个布尔变量available,表示锁是否可用。如果锁是可用的，调用acquire()会成功，锁不再可用。当一个进程试图获取不可用的锁时，会被阻塞，直到锁被释放。</p><figure class="highlight arduino"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs arduino"><span class="hljs-built_in">acquire</span> (){<br> <span class="hljs-keyword">while</span> (!available);<br> available = <span class="hljs-literal">false</span>;<br>}<br><span class="hljs-built_in">release</span>(){<br> available = <span class="hljs-literal">true</span>;<br>}<br></code></pre></td></tr></tbody></table></figure><p>acquire()或release()的执行必须是原子操作，因此互斥锁通常采用硬件机制来实现。</p><p>互斥锁的主要缺点是忙等待，当有一个进程在临界区中，任何其他进程在进入临界区时必须连续循环调用acquire()，这种不断循环检查该条件的行为并没有释放CPU资源或进入休眠状态。当多个进程共享同一CPU时，就浪费了CPU周期。因此，互斥锁通常用于多处理器系统，一个线程可以在一个处理器上等待，不影响其他线程的执行。</p><p>需要连续循环忙等的互斥锁，都可称为自旋锁(spin lock)，如TSL指令、swap指令、单标志法</p><p>虽然需忙等，进程时间片用完才下处理机，违反“让权等待”的原则，但是优点明显：</p><ul><li><p>等待期间不用切换进程上下文，多处理器系统中，若上锁的时间短，则等待代价很低</p></li><li><p>常用于多处理器系统，一个核忙等，其他核照常工作，并快速释放临界区</p><p>但是不太适用于单处理机系统，忙等的过程中不可能解锁</p></li></ul><p><img src="https://files.seeusercontent.com/2026/03/21/0rHe/image-20260321133820934.png" alt="image-20260321133820934"></p><h3 id="信号量机制"><a href="#信号量机制" class="headerlink" title="信号量机制"></a>信号量机制</h3><p>之前学习的实现方式都无法实现“让权等待”原则，最短路径提出者迪杰斯特拉提出了实现进程互斥,同步的方法——<strong>信号量机制。</strong></p><p>用户进程可以通过使用操作系统提供的一对<strong>原语</strong>来对信号量进行操作，从而很方便的实现了进程互斥、进程同步。</p><p>而信号量其实就是一个变量（可以是一个整数，也可以是更复杂的记录型变量），可以用一个信号量来表示系统中某种资源的数量，比如：系统中只有一台打印机，就可以设置一个初值为1的信号量。</p><p>使用<strong>原语</strong>能够解决软件解决方案的主要问题“进入区的各种操作无法一气呵成”，这里使用的原语主要是wait(S)原语和signal(S)原语，可以把原语理解为我们自己写的函数，函数名分别为wait和signal,.括号里的信号量S其实就是函数调用时传入的一个参，分别简称为P(S)，V(S)。</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-type">int</span> S = <span class="hljs-number">1</span>; <span class="hljs-comment">//初始化整型信号量S，表示当前系统中的资源数</span><br><span class="hljs-type">void</span> <span class="hljs-title function_">wait</span> <span class="hljs-params">(<span class="hljs-type">int</span> S)</span>  <span class="hljs-comment">//原语，相当于进入区</span><br>{<br>    <span class="hljs-keyword">while</span> (S&lt;=<span class="hljs-number">0</span>);  <span class="hljs-comment">//如果资源数不够就循环等待</span><br>    S=S<span class="hljs-number">-1</span>;   <span class="hljs-comment">//如果够用，就占用一个位置。</span><br>}<br><span class="hljs-type">void</span> <span class="hljs-title function_">signal</span> <span class="hljs-params">(<span class="hljs-type">int</span> S)</span>  <span class="hljs-comment">//原语，退出区</span><br>{<br>    S=S+<span class="hljs-number">1</span>;<span class="hljs-comment">//使用后释放资源</span><br>}<br></code></pre></td></tr></tbody></table></figure><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs C">进程P0:<br>.....  <br>wait(S);  <span class="hljs-comment">//进入区，申请资源</span><br>使用资源.... <span class="hljs-comment">//临界区，访问资源</span><br>signal(S); <span class="hljs-comment">//退出区，释放资源</span><br>....<br></code></pre></td></tr></tbody></table></figure><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs c">进程P1:<br>.....  <br>wait(S);  <br>使用资源.... <br>signal(S); <br>....<br></code></pre></td></tr></tbody></table></figure><p>因为是随时用原语来完成的，所以“检查”和“上锁”两步是一气呵成的，不会导致并发，异步导致的问题。</p><p>但是如果系统资源不够，进程依旧会不停循环检查系统资源数，依旧存在忙等的问题，也不满足“让权等待”原则。</p><p>整型信号量的缺陷是存在“<strong>忙等</strong>”问题，因此人们又提出了“<strong>记录型信号量</strong>”，即用记录型数据结构表示的信号量。</p><h3 id="记录型信号量"><a href="#记录型信号量" class="headerlink" title="记录型信号量"></a>记录型信号量</h3><p>记录型信号量，当某一进程发现没有系统资源可以分配时，就自动将该进程移动到阻塞队列，防止循环检查占用资源，直到被另一个释放资源的进程使用原语唤醒，这么做就避免了忙等的问题。</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> {</span><br>    <span class="hljs-type">int</span> value;<br>    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">process</span> *<span class="hljs-title">L</span>;</span><br>} semaphore;<br><span class="hljs-type">void</span> <span class="hljs-title function_">wait</span> <span class="hljs-params">(semaphore S)</span><br>{<br>    S.value--;<br>    <span class="hljs-keyword">if</span> (S.value &lt; <span class="hljs-number">0</span>)<br>    {<br>        block (S.L);<br>    }<br>}<br><span class="hljs-type">void</span> <span class="hljs-title function_">signal</span> <span class="hljs-params">(semaphore S)</span><br>{<br>    S.value++;<br>    <span class="hljs-keyword">if</span> (S.value &lt;= <span class="hljs-number">0</span>)<br>    {<br>        wakeup(S.L);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure><p>结合上面的函数体来看下面的流程：</p><p><img src="https://files.seeusercontent.com/2026/03/21/dZ2t/image-20260321144031873.png" alt="image-20260321144031873"></p><p>总结一下：<br>S.value的初值表示系统中某种资源的数目。<br>对信号量S的一次P操作意味着进程请求一个单位的该类资源，因此需要执行S.value-,表示资源数减1，当S.value&lt;0时表示该类资源己分配完毕，因此进程应调用block原语进行自我阻塞（当前运行的进程从运行态→阻塞态)，主动放弃处理机，并插入该类资源的等待队列S.L中。可见，该机制遵循了“让权等待”原则，不会出现“忙等”现象。</p><h3 id="信号量机制实现互斥同步和前驱"><a href="#信号量机制实现互斥同步和前驱" class="headerlink" title="信号量机制实现互斥同步和前驱"></a>信号量机制实现互斥同步和前驱</h3><blockquote><p>该机制将持续贯穿第二章剩余部分，如果忘了就回来看看。</p></blockquote><h4 id="互斥"><a href="#互斥" class="headerlink" title="互斥"></a>互斥</h4><p><img src="https://files.seeusercontent.com/2026/03/21/O3by/image-20260321151235729.png" alt="image-20260321151235729"></p><p>互斥是对同一个进程进行一对PV操作。</p><h4 id="同步"><a href="#同步" class="headerlink" title="同步"></a>同步</h4><p>落实同步性是为了抵抗程序异步执行带来的一系列不稳定的问题,大致步骤是：</p><ul><li><p>分析在什么地方需要同步关系，需要保证一前一后的操作顺序</p></li><li><p>设置同步信号量S，初始为0</p></li><li><p>在“前操作”之后执行V(S)</p></li><li><p>在”后操作”之前执行P(S)</p><p>如果我们需要让代码4在代码2之后 执行，就需要引入同步信号量</p></li></ul><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs c">semaphore S = <span class="hljs-number">0</span>;<span class="hljs-comment">//初始化同步信号量</span><br>p1(){<br>    代码<span class="hljs-number">1</span>;<br>    代码<span class="hljs-number">2</span>;<br>    V(S);<br>    代码<span class="hljs-number">3</span>;<br>}<br>p2(){<br>    P(S);<br>    代码<span class="hljs-number">4</span>;<br>    代码<span class="hljs-number">5</span>;<br>    代码<span class="hljs-number">6</span>;<br>}<br></code></pre></td></tr></tbody></table></figure><p>若先执行到 V(S)操作，则 S++后 S=1。之后当执行到P(S)操作时，由于S=1，表示有可用资源，会执行S–，S的值变回0，P2进程不会执行block原语，而是继续往下执行代码4。</p><p>若先执行到P(S)操作，由于S=0,S-后S=-1,表示此时没有可用资源，因此P操作中会执行block原语，主动请求阻塞。之后当执行完代码2，继而执行V(S)操作，S+,使S变回0，由于此时有进程在该信号量对应的阻塞队列中，因此会在V操作中执行wakeup原语，唤醒P2进程。这样P2就可以继续执行代码4了.</p><p>所以无论谁先被调度，代码4一定是在代码2后执行的。请记住这个做法，这很重要。</p><h4 id="前驱关系"><a href="#前驱关系" class="headerlink" title="前驱关系"></a>前驱关系</h4><p>前驱关系就是保证了代码之间的拓扑关系，保证一定的前后执行顺序。</p><p><img src="https://files.seeusercontent.com/2026/03/21/xE5c/image-20260321152939757.png" alt="image-20260321152939757"></p><p>实际上，每一对前驱关系都是一个进程同步问题，需要保证一前一后的执行顺序，那么：</p><ul><li>给每一对前驱关系各设置一个同步信号量</li><li>在“前操作”之后对相应的同步信号量执行V操作</li><li>在“后操作”之前对相应的同步信号量执行P操作</li></ul><p>那么就能够使用 PV原语作用于每一个同步关系就行：</p><p><img src="https://files.seeusercontent.com/2026/03/21/L2lf/image-20260321153131104.png" alt="image-20260321153131104"></p><p>将一个一个同步的思想集合起来就能够满足前驱的拓扑关系。</p><h3 id="生产者消费者的问题"><a href="#生产者消费者的问题" class="headerlink" title="生产者消费者的问题"></a>生产者消费者的问题</h3><p><img src="https://files.seeusercontent.com/2026/03/21/Idg5/image-20260321155823492.png" alt="image-20260321155823492"></p><p>系统中有<strong>一组生产者进程和一组消费者</strong>进程，生产者进程每次生产一个产品放入缓冲区，消费者进程每次从缓冲区中取出一个产品并使用。（注：这里的“产品”理解为某种数据）生产者、消费者共享一个初始为空、大小为n的缓冲区。只有缓冲区没满时，生产者才能把产品放入缓冲区，否则必须等待.</p><p>想起来了吗，缓冲区没满-&gt;生产者生产，缓冲区不空-&gt;消费者消费，否则必须等待，这是一个同步问题。缓冲区是临界资源，所以这也是一个互斥问题。</p><p><img src="https://files.seeusercontent.com/2026/03/21/dmE2/image-20260321162256140.png" alt="image-20260321162256140"></p><p>所以我们需要针对互斥，空闲，满载三种状态设定三个信号量，分别是mutex,empty,full。</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs c">semaphore mutex=<span class="hljs-number">1</span>;<span class="hljs-comment">//互斥信号量，实现对缓冲区的互斥访问 </span><br>semaphore empty=n;<span class="hljs-comment">//同步信号量，表示空闲缓冲区的数量 </span><br>semaphore full=<span class="hljs-number">0</span>;<span class="hljs-comment">//同步信号量，表示产品的数量，也即非空缓冲区的数量</span><br></code></pre></td></tr></tbody></table></figure><p>因此，生产者和消费者的进程的设定应该是：</p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs c">producer(){<br>   <span class="hljs-keyword">while</span>(<span class="hljs-number">1</span>){<br>   P(empty);<span class="hljs-comment">//消耗一个缓冲区</span><br>   P(mutex);<br>   把产品放入缓冲区;<br>   V(mutex);<span class="hljs-comment">//互斥是在同一进程中执行PV</span><br>   V(full); <span class="hljs-comment">//增加一个产品</span><br>   }<br>}<br>consumer(){<br>   <span class="hljs-keyword">while</span>(<span class="hljs-number">1</span>){<br>   P(full);<span class="hljs-comment">//消耗一个产品</span><br>   P(mutex);<br>   从缓冲区取出一个产品;<br>   V(mutex);<span class="hljs-comment">//互斥是在同一进程中执行PV</span><br>   V(empty); <span class="hljs-comment">//增加一个缓冲区</span><br>   使用产品;<br>   }<br>}<br></code></pre></td></tr></tbody></table></figure><p>有没有想过，<strong>改变相邻P，V操作的顺序？</strong></p><p><img src="https://files.seeusercontent.com/2026/03/21/i3mG/image-20260321163110858.png" alt="image-20260321163110858"></p><p>若此时缓冲区内已经放满产品，则empty=0,full=n。则生产者进程执行①使mutex变为0，再执行②，由于已没有空闲缓冲区，因此生产者被阻塞。由于生产者阻塞，因此切换回消费者进程。消费者进程执行③，由于mutex为0，即生产者还没释放对临界资源的“锁”，因此消费者也被阻塞。</p><p>这就造成了生产者等待消费者释放空闲缓冲区，而消费者又等待生产者释放临界区的情况，生产者和消费者循环等待被对方唤醒，出现“死锁”。</p><p>同样的，若缓冲区中没有产品，即full=0,empty=n。按③④①的顺序执行就会发生死锁。因此，实现互斥的P操作一定要在实现同步的P操作之后。</p><p>但是，V操作不会导致进程阻塞，因此两个V操作顺序可以交换:)</p><p>将业务代码(中文写的)放在一起不会造成严重后果，但是会导致上锁的时间变长，会导致运行效率下降。</p><h3 id="多生产者—多消费者的问题"><a href="#多生产者—多消费者的问题" class="headerlink" title="多生产者—多消费者的问题"></a>多生产者—多消费者的问题</h3><p><strong>多类型</strong>生产者和<strong>多类型</strong>消费者共用一个缓冲区。</p><p><img src="https://files.seeusercontent.com/2026/03/21/q9kX/image-20260321165705176.png" alt="image-20260321165705176"></p><ul><li>对盘子的访问需要互斥的进行。</li><li>父亲将苹果放入盘子后，女儿才能取苹果</li><li>母亲将橘子放入盘子后，儿子才能取橘子</li><li>只有盘子为空时，父亲或母亲才能放入水果</li><li>“盘子为空”这个事件可以由儿子或女儿触发，事件发生后才允许父亲或母亲放水果</li></ul><p>互斥很简单，临界区前后分别PV,不需要考虑拓扑结构，所以可以放在最后考虑</p><h4 id="如何实现"><a href="#如何实现" class="headerlink" title="如何实现"></a>如何实现</h4><p><img src="https://files.seeusercontent.com/2026/03/22/Srh0/image-20260322112721991.png" alt="image-20260322112721991"></p><figure class="highlight c"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c">semaphore mutex =<span class="hljs-number">1</span>; <span class="hljs-comment">//实现互斥访问盘子（缓冲区）</span><br>semaphore apple=<span class="hljs-number">0</span>; <span class="hljs-comment">//盘子中有几个苹果</span><br>semaphore orange=<span class="hljs-number">0</span>; <span class="hljs-comment">//盘子中有几个橘子</span><br>semaphore plate=<span class="hljs-number">1</span>; <span class="hljs-comment">//盘子中还可以放多少个水果</span><br></code></pre></td></tr></tbody></table></figure><p><img src="https://files.seeusercontent.com/2026/03/22/9lEz/image-20260322113152960.png" alt="image-20260322113152960"></p><p>其实删除互斥信号量，这个系统也不会出现同时访问临界资源的情况。这是因为缓冲区只有1，在任意时刻信号量最多只有一个的值为1。</p><p>如果缓冲区为2或者更多，不设置互斥信号量会导致写入覆盖的问题。</p><p>这种问题应该优先考虑进程执行的前后关系来思考，得出拓扑结构后就知道如何设计流程。</p><h3 id="读者-写者问题"><a href="#读者-写者问题" class="headerlink" title="读者-写者问题"></a>读者-写者问题</h3><p>有读者和写者两组并发进程，共享一个文件，当两个或两个以上的读进程同时访问共享数据时不会产生副作用，但若某个写进程和其他进程（读进程或写进程）同时访问共享数据时则可能导致数据不一致的错误。</p><p>因此要求：</p><p>①允许多个读者可以同时对文件执行读操作；</p><p>②只允许一个写者往文件中写信息；</p><p>③任一写者在完成写操作之前不允许其他读者或写者工作；</p><p>④写者执行写操前，应让己有的读者和写者全部退出。</p><p>与消费者进程不同，读者进想在读数据后并不会将数据清空，并不会改变数据。因此多个读者可同时访问共享数据</p><p><img src="https://files.seeusercontent.com/2026/03/22/0yQa/image-20260322122112555.png" alt="image-20260322122112555"></p><p>分析进程关系：</p><p>互斥关系：写进程一写进程、写进程一读进程。读进程与读进程不存在互斥问题。</p><figure class="highlight abnf"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs abnf">semaphore rw <span class="hljs-operator">=</span> <span class="hljs-number">1</span><span class="hljs-comment">; //对共享文件的互斥访问</span><br></code></pre></td></tr></tbody></table></figure><p>读者也需要加锁解锁，但是这么做会导致读者进程不能同时读取，所以需要优化。</p><p><code>int count = 0;</code></p><p>如果读者发现自己是第一个读者，那么就上锁，如果读完后发现自己是最后一个读完的人，就解锁，让权给写者工作。</p><p><img src="https://files.seeusercontent.com/2026/03/22/vuD9/image-20260322134048315.png" alt="image-20260322134048315"></p><p>该方案存在一个问题，还是判断-执行这两步操作不是原语导致的。如果读者一判断后发生切换，读者二判断后加锁，随后切换回读者1的加锁操作(P)会发现<code>S.value &lt; 0 </code>随后执行原语进入阻塞阶段，这就造成了读者之间无法同时读取。</p><p>解决方案：</p><ul><li>给count单独加一个互斥信号量，读者1执行P(mutex)之后即使切换，读者2发现其他读者没有完成对count的访问，就会等待而无法介入，等待切回读者1之后正常运行。在这个算法中，读者是具有高优先级的，在最后一个读者读完之前，缓存池都不会被解锁，写者可能长期饥饿甚至饿死。</li><li>使用原语将判断-上锁两步操作绑定，不可中断。理论学习中第一种能够帮助你理解整个流程，但是第二种在工程中效率更高。</li></ul><p>如果要实现写者优先，那就要再加一个互斥写者信号量。</p><p><img src="https://files.seeusercontent.com/2026/03/22/gv5O/image-20260322135820815.png" alt="image-20260322135820815"></p><p>这个问题后半段我没看，心累。</p><h3 id="哲学家进餐问题"><a href="#哲学家进餐问题" class="headerlink" title="哲学家进餐问题"></a>哲学家进餐问题</h3><p>一张圆桌上坐着5名哲学家，每两个哲学家之间的桌上摆一根筷子，桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐，哲学家在思考时，并不影响他人。只有当哲学家饥饿时，才试图拿起左、右两根筷子（一根一根地拿起）。如果筷子己在他人手上，则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐，当进餐完毕后，放下筷子继续思考。</p><p><img src="https://files.seeusercontent.com/2026/03/22/f1Ji/image-20260322143827041.png" alt="image-20260322143827041"></p><p>这个问题不存在同步关系，只有互斥关系。</p><p>如资源分配不当，可能导致死锁的问题。</p><p>信号量需要一个数组：<code>chopstick[5]={1,1,1,1,1}</code>实现对筷子的互斥访问，哲学家也需要编号。哲学家i左边的筷子编号为i，右边的筷子编号为(i+1)%5.</p><p>看上去解决方案就是：</p><p><img src="https://files.seeusercontent.com/2026/03/22/m5bG/image-20260322144752820.png" alt="image-20260322144752820"></p><p>如果5个哲学家都拿到了自己左边的筷子，然后试图拿取右边的筷子时发现已经被夺取，然后5个人就会一直等待对方放下筷子，显然谁也等不到。这就造成了死锁。</p><p>所以这个解决方案是不合理的。</p><p><img src="https://files.seeusercontent.com/2026/03/22/Vim2/image-20260322145001676.png" alt="image-20260322145001676"></p><p>我们可以对哲学家进程施加一些限制条件，比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。且定义奇数位哲学家先抓左边再抓右边，而偶数位相反，这就让相邻的两个人之间至少有一个人能够拥有一双筷子。</p><p>又或者，定义只有左右筷子都可用是才允许他抓起筷子，这样就能减少只有一根筷子的情况。</p><p><img src="https://files.seeusercontent.com/2026/03/22/zSi0/image-20260322151519917.png" alt="image-20260322151519917"></p><p>但是这么做也会有问题，0号取走两边的筷子，1号因为缺乏左筷子而阻塞，2号因为mutex(取筷子的动作也是互斥的)也会阻塞，所以2号有获取一双筷子的能力但是却不能这么做。</p><p><img src="https://files.seeusercontent.com/2026/03/22/wKh0/image-20260322151725771.png" alt="image-20260322151725771"></p><p>同样地，如果1号拿起一双，4号会取得左筷子随后在右手发生阻塞。</p><p><img src="https://files.seeusercontent.com/2026/03/22/vBl6/image-20260322152040052.png" alt="image-20260322152040052"></p><p>所以算法的表述不大严谨，更准确的说法应该是：各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞，也不会有别的哲学家会继续尝试拿筷子。这样的话，当前正在吃饭的哲学家放下筷子后，被阻塞的哲学家就可以获得等待的筷子了。</p><p>这么做使用全局锁杜绝了死锁，但是如果同时进餐，某个哲学家可能长期得不到筷子而饥饿。</p><p>如果遇到了一个进程需要同时持有多个临界资源的情况，应该参考哲学家问题的思想。</p><h3 id="管程"><a href="#管程" class="headerlink" title="管程"></a>管程</h3><p>信号量机制存在的问题是：<strong>编写程序困难，容易出错，甚至连操作顺序错了也会导致严重后果。</strong></p><p>1973年，Brinch Hansen首次在程序设计语言(Pascal)中引入了“管程”成分，一种高级同步机制</p><p>管程是一种特殊的软件模块，有这些部分组成：</p><p>1.局部于管程的共享数据结构说明；</p><p>2.对该数据结构进行操作的一组过程(函数)；</p><p>3.对局部于管程的共享数据设置初始值的语句</p><p>4.管程有一个名字。</p><p>听起来很像编程语言中的类，对吧？</p><p>管程的基本特征：<br>1.局部于管程的数据只能被局部于管程的过程所访问：<br>2.一个进程只有通过调用管程内的过程才能进入管程访问共享数据：<br>3.每次仅允许一个进程在管程内执行某个内部过程。</p><p>就好比，类的私有变量只能用类的公共方法访问修改。管程能够保证互斥访问。</p><p><img src="https://files.seeusercontent.com/2026/03/23/0iAw/image-20260323153359725.png" alt="image-20260323153359725"></p><p>缓冲区满时，生产者需要等待，full是满载队列，生产者进程需要在此处等待，直到被signal原语唤醒。</p><p>缓冲区空时，消费者需要等待，empty是空载队列，消费者进程需要在此处等待，直到被signal原语唤醒。</p><p>wait函数做的事情是：</p><ul><li><strong>让调用线程进入睡眠状态</strong>，释放CPU。</li><li><strong>释放管程的互斥锁</strong>，让其他线程可以进入</li><li><strong>将该线程放入条件变量的等待队列</strong>。</li><li><strong>在将来被唤醒时，重新获取互斥锁</strong>后再返回。</li></ul><p>换管程设定完成后，就由编译器负责实现各进程互斥地进入管程中的过程。编译器每次仅允许一个进程在管程内执行某个内部过程。</p><p>如果两个生产者并发执行，依次调用了insert，那么第二个会被暂时阻塞，出于等待状态，直到第一个执行完insert后他才能唤醒执行insert。消费者也是一样的，编译器能够保证互斥，而不需要进程来关心，进程就能够专注于业务逻辑。。</p><ul><li>需要在管程中定义共享数据（如生产者消费者问题的缓冲区）</li><li>需要在管程中定义用于访问这些共享数据的“入口”一一其实就是一些函数（如生产者消费者问题中，可以定义一个函数用于将产品放入缓冲区，再定义一个函数用于从缓冲区取出产品)</li><li>只有通过这些特定的“入口”才能访问共享数据</li><li>管程中有很多“入口”，但是每次只能开放其中一个“入口”，并且只能让一个进程或线程进入（如生产者消费者问题中，各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意：这种互斥特性是由编译器负责实现的，程序员不用关心)</li><li>可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变重上等待（此时，该进程应先释放管程的使用权，也就是让出“入口”）；可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。</li></ul><p>程序员能够用特殊的语法定义一个管程，之后其他人就能够使用管程提供入口实现进程的同步和互斥。</p><p>如JAVA中能够使用synchronized关键字来描述一个同步函数，该函数在同一时间内只能被一个线程调用。</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">insert</span> <span class="hljs-params">(Item item)</span><br></code></pre></td></tr></tbody></table></figure><p>管程不过是为了解决信号量机制编程麻烦、易出错的问题，其目的也是实现互斥和同步。</p><h3 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h3><p>死锁：各进程互相等待对方手里的资源，导致各进程都阻塞，无法向前推进的现象。</p><p>参考前面说过的哲学家问题。</p><p>饥饿：由于长期得不到想要的资源，某进程无法向前推进的现象。比如：在短进程优先(SPF)算法中，若有源源不断的短进程到来，则长进程将一直得不到处理机，从而发生长进程“饥饿”。</p><p>死循环：某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑bug导致的，有时是程序员故意设计的。如for(int i = 0,i&gt;=0,i++)。</p><p><img src="https://files.seeusercontent.com/2026/03/23/Qdw5/image-20260323160636869.png" alt="image-20260323160636869"></p><p>产生死锁必须同时满足一下四个条件，只要其中任一条件不成立，死锁就不会发生。</p><ul><li>互斥条件：只有对必须互斥使用的资源的争抢才会导致死锁（如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的（因为进程不用阻塞等待这种资源)</li><li>不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。</li><li>请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己己有的资源保持不放</li><li>循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程己获得的资源同时被下一个进程所请求</li></ul><p>注意，发生死锁一定有循环等待，但是发生循环等待不一定就代表死锁。所以，循环等待是死锁的必要不充分条件。因为<strong>资源类的实例数量（Instance）</strong>可能大于 1。</p><p>如果同类资源数大于1，则即使有循环等待，也未必发生死锁。但如果系统中每类资源都只有一个，那循环等待就是死锁的充分必要条件了。</p><p>发生死锁的常见情况：</p><ul><li>对系统资源的竞争。各进程对不可剥夺的资源（如打印机）的竞争可能引起死锁，对可剥夺的资源(CPU)的竞争是不会引起死锁的。</li><li>进程推进顺序非法。请求和释放资源的顺序不当，也同样会导致死锁。例如，并发执行的进程P1、P2分别申清并占有了资源R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞，从而发生死锁。</li><li>信号量的使用不当也会造成死锁。如生产者-消费者问题中，如果实现互斥的P操作在实现同步的P操作之前，就有可能导致死锁。（可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)</li></ul><p>总之，对不可剥夺的资源的不合理分配，就很可能导致死锁。</p><h3 id="预防死锁"><a href="#预防死锁" class="headerlink" title="预防死锁"></a>预防死锁</h3><p>破坏造成死锁的4个条件即可。</p><h4 id="互斥条件"><a href="#互斥条件" class="headerlink" title="互斥条件"></a>互斥条件</h4><p>只有对必须互斥使用的资源的争抢才会导致死锁。</p><p>所以把这类资源改成能够共享使用的资源，就能够从根本上解决死锁问题。如SPOOLing技术。</p><p>实际上，SPOOLing 并没有改变打印机作为“临界资源”的物理属性，它只是引入了一个<strong>中间代理进程</strong>（守护进程）。</p><p>比如打印机能新开一个进程，接收来自于不同进程的文件和请求，并形成队列，慢慢使用打印机打印，在进程1和2眼中，这就成了一个共享资源。</p><p><img src="https://files.seeusercontent.com/2026/03/23/au3T/image-20260323163518805.png" alt="image-20260323163518805"></p><p>但是很多资源都是不能被改造成可共享资源的，这个策略还是具有很大局限性的。</p><h4 id="不可剥夺条件"><a href="#不可剥夺条件" class="headerlink" title="不可剥夺条件"></a>不可剥夺条件</h4><p>破坏不剥夺条件：</p><ul><li>方案一：当某个进程请求新的资源得不到满足时，它必须立即释放保持的所有资源，待以后需要时再重新申请。也就是说，即使某些资源尚未使用完，也需要主动释放，从而破坏了不可剥夺条件。</li><li>方案二：当某个进程需要的资源被其他进程所占有的时候，可以由操作系统协助，将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级（比如：剥夺调度方式，就是将处理机资源强行剥夺给优先级更高的进程使形)</li></ul><p>该策略的缺点：</p><ul><li>实现起来比较复杂。</li><li>释放己获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源，如CPU。</li><li>反复申请和释放资源会增加系统开销</li><li>方案一只要得不到所有资源就要放弃已占有的所有资源，这会导致饥饿问题。</li></ul><h3 id="请求和保持条件"><a href="#请求和保持条件" class="headerlink" title="请求和保持条件"></a>请求和保持条件</h3><p>进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己己有的资源保持不放</p><p>静态配置方法：进程在运行之前一次申请完需要的所有资源，在满足之前不会运行，一旦开始运行，就不会申请其他任何资源。</p><p>实现起来简单，但是缺点明显：</p><ul><li>有些资源可能只需要用很短的时间，因此如果进程的整个运行期间都一直保持着所有资源，就会造成严重的资源浪费，资源利用率极低。另外，该策略也有可能导致某些进程饥饿。</li><li><ul><li>资源浪费，例如，一个进程可能需要先使用键盘输入数据，然后进行长时间计算，最后才用打印机输出结果。但按照静态分配，进程一开始就占用了键盘和打印机，导致打印机在计算阶段（可能长达数小时）完全闲置，其他进程也无法使用，造成资源浪费。</li></ul></li></ul><p><img src="https://files.seeusercontent.com/2026/03/23/uhE6/image-20260323164715132.png" alt="image-20260323164715132"></p><p>如果系统源源不断涌入AB两类进程，那C类进程就会饥饿。</p><h4 id="循环等待条件"><a href="#循环等待条件" class="headerlink" title="循环等待条件"></a>循环等待条件</h4><p>循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程已获得的资源同时被下一个进程所请求。</p><p>可采用顺序资源分配法。首先给系统中的资源编号，规定每个进程必须按编号递增的顺序请求资源，同类资源（即编号相同的资源）一次申请完。那么在任何一个时刻，<strong>总有一个进程拥有的资源编号是最大的，那这个进程申请之后的资源必然畅通无阻。</strong>因此，不可能出现所有进程都阻塞的死锁现象。(哲学家中只要有一个人有权夺取筷子，就不会出现死锁)</p><p>一样是有缺点的：</p><ul><li>不方便增加新的设备，需要重新编号</li><li>实际使用顺序可能和编号递增顺序不一致，会导致系统资源浪费</li><li>用户编程很麻烦。</li></ul><h3 id="死锁的避免"><a href="#死锁的避免" class="headerlink" title="死锁的避免"></a>死锁的避免</h3><p>这与前几个静态策略不一样，这是动态策略。</p><h4 id="安全序列"><a href="#安全序列" class="headerlink" title="安全序列"></a>安全序列</h4><p>考虑一个问题：</p><p>你是一位成功的银行家，手里掌握着100个亿的资金<br>有三个企业想找你贷款，分别是企业B、企业A、企业T,为描述方便，简称BAT。<br>B表示：“大哥，我最多会跟你借70亿…”<br>A表示：“大哥，我最多会跟你借40亿.”<br>T表示：“大哥，我最多会跟你借50亿”<br>然而…江湖中有个不成文的规矩：如果你借给企业的钱总数达不到企业提出的最大要求，那么不管你之前给企业借了多少钱，那些钱都拿不回来了。</p><p>刚开始，BAT三个企业分别从你这儿借了20、10、30亿，你手里里还剩下40亿，这时B还想借30亿，刚借吗？</p><p>如果你答应了，那就变成如下的关系：</p><p><img src="https://files.seeusercontent.com/2026/03/23/qO1y/image-20260323193754593.png" alt="image-20260323193754593"></p><p>你手里只剩下10亿，不足以支付任何一家企业的全部借款，于是你损失了90亿。</p><p>所以这是一个需要慎重思考的问题。</p><p>如果A想借20亿，你答应了A的请求，那就是：</p><p><img src="https://files.seeusercontent.com/2026/03/23/vkY1/image-20260323194058402.png" alt="image-20260323194058402"></p><p>你还剩下20亿，那么之后可以借给T，等T归还之后再用50亿借给B，最后在借给A收回本金。</p><p>又可以借给A，再借给T，最后借给B，也可以收回成本。所以你借给A20亿的决定是绝对安全的，是能够收回本金的。</p><p>所以有些请求时不能答应的，所谓安全序列，就是指如果系统按照这种序列分配资源，则每个进程都能顺利完成。只要能找出一个安全序列(如借给A20亿之后的ATB或者TAB)，系统就是安全状态。当然，安全序列可能有多个。</p><p>如果分配了资源之后，系统中找不出任何一个安全序列，系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然，如果有进程提前归还了一些资源，那系统也有可能重新回到安全状态，不过我们在分配资源之前总是要考虑到最坏的情况，不能将希望寄托于进程提前归还资源。</p><p>如果系统处于安全状态，就一定不会发生死锁。如果系统进入不安全状态，就可能发生死锁（处于不安全状态未必就是发生了死锁，但发生死锁时一定是在不安全状态)。</p><p>我们需要在分配之前考虑分配会不会导致系统进入不安全状态，这就是“银行家算法”的核心思想。</p><p>银行家算法是荷兰学者Dijkstra(没错又是他)为银行系统设计的，以确保银行在发放现金贷款时，不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中，用于避免死锁。</p><p><img src="https://files.seeusercontent.com/2026/03/23/z2Ji/image-20260323195542052.png" alt="image-20260323195542052"></p><p>资源总数(10,5,7)，剩余可用资源(5,3,2)，思考安全序列。</p><p>此时系统是否处于安全状态？<br>思路：尝试找出一个安全序列.P1,P3}<br>依次检查剩余可用资源(3,3,2)是否能满足各进程的需求<br>可满足P1需求，将P1加入安全序列，并更新剩余可用资源值为(5,3,2)<br>依次检查剩余可用资源(5,3,2)是否能满足剩余进程（不包括已加入安全序列的进程）的需求<br>可满足P3需求，将P3加入安全序列，并更新剩余可用资源值为(7,4,3)<br>依次检查剩余可用资源(7,4,3)是否能满足剩余进程（不包括已加入安全序列的进程）的需求<br>以此类推，共五次循环检查即可将5个进程都加入安全序列中，最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程，每一轮检查都从编号较小的进程开始检查。</p><p>有时候找不到安全序列，就说明已经处于不安全状态。</p><h4 id="算法实现"><a href="#算法实现" class="headerlink" title="算法实现"></a>算法实现</h4><p><img src="https://files.seeusercontent.com/2026/03/23/nz1U/image-20260323200537888.png" alt="image-20260323200537888"></p><p><img src="https://files.seeusercontent.com/2026/03/23/1tzM/image-20260323200635298.png" alt="image-20260323200635298"></p><p>记住这几个矩阵的 LaTeX 关系式，逻辑就通了：</p><p>$$<br>Need[i,j]=Max[i,j]−Allocation[i,j]<br>$$</p><ul><li><strong>Max：</strong> 最多要多少。</li><li><strong>Allocation：</strong> 已经给了多少。</li><li><strong>Need：</strong> 还要多少。</li><li><strong>Available：</strong> 银行手里还剩多少。</li></ul><p><strong>安全性检查的核心：</strong> 只有当 Available≥Need[i] 时，才敢把资源分给进程 i。</p><h3 id="死锁的检测与解除"><a href="#死锁的检测与解除" class="headerlink" title="死锁的检测与解除"></a>死锁的检测与解除</h3><p>即使已经有两种办法避免死锁的可能，但是如果死锁真的发生也需要方法去检测和解除。</p><p>为了能对系统是否已发生了死锁进行检测，必须：</p><ul><li>用某种数据结构来保存资源的请求和分配信息：</li><li>提供一种算法，利用上述信息来检测系统是否已进入死锁状态。</li></ul><p>请求边：起始于进程，箭头指向资源，一般用矩形表示资源结点，矩形中的小圆代表该类资源的数量。</p><p>分配边：起始于资源，箭头指向进程 ，表示资源分配给进程的量。</p><p><img src="https://files.seeusercontent.com/2026/03/23/0Loa/image-20260323201934238.png" alt="image-20260323201934238"></p><p>如果系统中剩余的可用资源数足够满足进程的需求，那么这个进程暂时是不会阻塞的，可以顺利地执行下去。如果这个进程执行结束了把资源归还系统，就可能使某些正在等待资源的进程被激活，并顺利地执行下去。</p><p>进程执行完毕后归还资源就能够消除对应的边，如果最后边都能消除，那我们就称图“可简化”，那就一定没有发生死锁。</p><p><img src="https://files.seeusercontent.com/2026/03/23/2Bne/image-20260323202930673.png" alt="image-20260323202930673"></p><p>即使P3执行完归还资源，P1和P2仍会因为资源不足而阻塞，这就发生了死锁。</p><p>算法核心就是：</p><p>1)在资源分配图中，找出既不阻塞又不是孤点的进程Pⅰ（即找出一条有向边与它相连，且该有向边对应资源的申请数量小于等于系统中己有空闲资源数量。如下图中，R1没有空闲资源，R2有一个空闲资源。若所有的连接该进程的边均满足上述条件，则这个进程能继续运行直至完成，然后释放它所占有的所有资源)。消去它所有的请求边和分配变，使之称为孤立的结点。在下图中， P1是满足这一条件的进程结点，于是将P1的所有边消去。</p><p>2)进程P所释放的资源，可以唤醒某些因等待这些资源而阻塞的进程，原来的阻塞进程可能变为非阻塞进程。在下图中，P2就满足这样的条件。根据1)中的方法进行一系列简化后，若能消去途中所有的边，则称该图是可完全简化的。</p><p><img src="https://files.seeusercontent.com/2026/03/23/Zr4f/image-20260323203341715.png" alt="image-20260323203341715"></p><p>就像之前的P1P2P3一样，不是所有的进程都是死锁的。</p><h4 id="死锁的解除"><a href="#死锁的解除" class="headerlink" title="死锁的解除"></a>死锁的解除</h4><p>解除死锁的主要方法有：</p><p>1.资源剥夺法。挂起（暂时放到外存上）某些死锁进程，并抢占它的资源，将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。</p><p>2.撤销进程法（或称终止进程法）。强制撤销部分、甚至全部死锁进程，并剥夺这些进程的资源。这种方式的优点是实现简单，但所付出的代价可能会很大。因为有些进程可能己经运行了很长时间，已经接近结束了，一旦被终止可谓功亏一篑，以后还得从头再来。</p><p>3.进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息，设置还原点，但这很难实现。</p><p>解除死锁毕竟是需要付出代价的，为了让代价最小化，可以根据以下指标决定对哪个进程动手：</p><ul><li>进程优先级</li><li>已执行时间</li><li>剩余完成时间</li><li>已经占用资源</li></ul><blockquote><p>实际上，银行家算法在实际操作系统中几乎是隐形人，它存在技术上的瓶颈和性能瓶颈，现代操作系统多使用“鸵鸟策略 (Ostrich Algorithm)”，像鸵鸟一样把头埋进沙子里，假装死锁不存在，如果真的发生了，让用户来兜底。因为死锁的概率足够低。</p></blockquote><p>操作系统第二章到此结束</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;操作系统-第二章&quot;&gt;&lt;a href=&quot;#操作系统-第二章&quot; class=&quot;headerlink&quot; title=&quot;操作系统 第二章&quot;&gt;&lt;/a&gt;操作系统 第二章&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;这一章会非常非常长，写完的时候统计数据是25036个词。&lt;/p&gt;
</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>操作系统第一章：概述</title>
    <link href="https://blog.willmo.top/posts/Chapter%201%20of%20Operating%20Systems:%20Overview/"/>
    <id>https://blog.willmo.top/posts/Chapter%201%20of%20Operating%20Systems:%20Overview/</id>
    <published>2026-04-20T06:06:20.549Z</published>
    <updated>2026-04-20T06:59:33.828Z</updated>
    
    <content type="html"><![CDATA[<h1 id="操作系统的概念和功能"><a href="#操作系统的概念和功能" class="headerlink" title="操作系统的概念和功能"></a>操作系统的概念和功能</h1><p>操作系统(Operating System,OS)是指</p><ul><li><strong>控制和管理</strong>整个计算机系统的硬件和软件资源</li><li>并合理地<strong>组织调度</strong>计算机的工作和资源的分配</li><li>以<strong>提供给用户和其他软件方便的接口和环境</strong></li><li>是计算机系统中<strong>最基本的系统软件</strong>。</li></ul><p>类似于计算机网络，操作系统必须向上层提供方便易用<strong>且透明</strong>的服务，而上层用户无须理解底层硬件的原理。比如GUI图形界面。</p><p>在早期或现代特化操作系统中，只有交互式命令接口，用户通过输入命令与计算机交互，你可以使用Windows的cmd命令行模式体验。这种说<strong>一句做一句</strong>的模式叫做<strong>联机命令接口。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/11/o9Rl/image-20260311093445299.png" alt="image-20260311093445299"></p><p>脱机命令接口，就是将所有需要执行的命令写在一个<strong>bat文件</strong>内，运行该文件会逐行执行内部的命令。<strong>用户输入一堆，计算机处理一堆。</strong></p><p>程序接口：可以在程序中进行<strong>系统调用</strong>来使用程序接口。普通用户不能直接使用程序接口，只能通过程序代码间接使用。</p><p>比如我们写一个HelloWorld，那就是通过程序代码(C语言)去调用了<strong>程序接口</strong>使在显示器上显示。</p><p>操作系统需要实现对硬件机器的扩展。没有任何软件的计算机叫裸机，安装操作系统后用户能够更方便地使用软件，计算机的功能也就更强大。</p><h2 id="操作系统的特征"><a href="#操作系统的特征" class="headerlink" title="操作系统的特征"></a>操作系统的特征</h2><ul><li><strong>并发</strong></li><li><strong>共享</strong></li><li><strong>虚拟</strong></li><li><strong>异步</strong></li></ul><h3 id="并发"><a href="#并发" class="headerlink" title="并发"></a>并发</h3><p>并发：指两个或多个事件<strong>在同一时间间隔内</strong>发生。</p><p>这些事件宏观上是同时发生的，<strong>但微观上是交替发生的</strong>。单核CPU在不同任务中快速切换。</p><p>并行：两个或者多个事件在同一时刻**同时发生。多核CPU同时处理多个任务。</p><p>操作系统的并发性指计算机系统中“同时”运行着多个程序，这些程序宏观上看是同时运行着的，而微观上看是交替运行的。</p><p>CPU的核心数很难超过实际中的进程数量，所以为了保证宏观上的同时发生，并发是很重要的</p><h3 id="共享"><a href="#共享" class="headerlink" title="共享"></a>共享</h3><ul><li><strong>互斥共享方式</strong></li></ul><p>系统中的某些资源，称为<strong>临界资源</strong>，虽然可以提供给多个进程使用，但一个时间段内只允许一个进程访问该资源。比如摄像头在同一时间内只能分配给一个进程。</p><ul><li><strong>同时共享方式</strong></li></ul><p>系统中的某些资源，允许一个时间段内由多个进程“同时”对它们进行访问。比如两个通信进程传输两个不同的文件，宏观上看好像他们都在使用硬盘，微观上看，两个进程是<strong>交替访问硬盘</strong>的。</p><blockquote><p>共享性和并发性是唇亡齿寒的关系，是互为存在条件。缺失任何一方，另一方也没有意义。</p></blockquote><h3 id="虚拟"><a href="#虚拟" class="headerlink" title="虚拟"></a>虚拟</h3><p>虚拟是指把一个物理上的实体变为若干个<strong>逻辑上</strong>的对应物。</p><p>物理实体（前者）是实际存在的，而逻辑上对应物（后者）是用户感受到的。</p><p>能够使得实际上很小的内存在宏观上看起来很大。</p><p>分为<strong>虚拟存储器技术</strong>和<strong>空分复用技术</strong>。</p><p>在第三章会统一讲解。</p><p>如果没有并发性，那<strong>虚拟性</strong>没有意义。</p><h3 id="异步"><a href="#异步" class="headerlink" title="异步"></a>异步</h3><p>异步是指在多道程序环境下，允许多个程序并发执行，但由于资源有限，<strong>进程的执行不是一贯到底的，而是走走停停，以不可预知的速度向前推进，</strong>这就是进程的异步性。</p><p>只有并发性，才有可能导致异步性。</p><p>所以<strong>并发和共享</strong>是操作系统最基本的两个特征。</p><h3 id="操作系统的运行机制"><a href="#操作系统的运行机制" class="headerlink" title="操作系统的运行机制"></a>操作系统的运行机制</h3><p>高级语言编译成机器指令，CPU识别并执行机器指令。</p><p>注意：这里的指令是0-1机器码，不是操作系统的交互式命令。</p><p>普通程序员写的是应用程序，是运行在操作系统之上的。</p><p>但是操作系统程序员写的是<strong>内核程序</strong>，运行在操作系统内核，又名**内核(Kernel)**。</p><p>内核是操作系统中最核心的部分，在大部分情况下，一个操作系统中只要有内核就够了。</p><p>内核程序具有<strong>更高的运行权限</strong>，能够执行可能干扰正常运行的特权指令，<strong>腾讯ACE反作弊系统就是内核级别程序</strong>，能够有效监管应用层的作弊软件。</p><p>CPU不具有识别指令类型的功能，所以CPU有两种状态，<strong>一个是内核态（管态）和用户态（目态）</strong>，由内部的一个寄存器来标识状态。</p><p>在启动相关程序之前，CPU会转变为对应类型，如果程序出现跨权限执行特权指令，CPU会发现“用户态”和指令的不一致并引发中断信号，CPU切换为内核态关闭当前应用程序。</p><h2 id="中断和异常"><a href="#中断和异常" class="headerlink" title="中断和异常"></a>中断和异常</h2><p>中断：<strong>CPU停止执行下一条指令，去执行其他操作。</strong></p><p>中断是实现并发的关键，没有中断就没有并发。</p><p>中断能够实现用户态和内核态的过渡。</p><p>中断分为</p><ul><li>内中断（异常，例外）</li><li><ul><li>与当前执行指令<strong>有关</strong>，中断信号来自CPU内部</li></ul></li><li>外中断（又名中断）</li><li><ul><li>与当前执行指令<strong>无关</strong>，中断信号来自CPU外部</li></ul></li></ul><p>内中断就是上面举过的例子，越权执行指令，除数为零等操作。</p><p>有时候应用程序请求操作系统内核服务，就会执行<strong>陷入指令</strong>，陷入指令在用户态执行，它的出现意味着应用程序<strong>将控制权交给内核</strong>，这同样会导致中断，但不是特权指令。</p><hr><p>而外中断就是来自于外部的CLK时钟信号等。如果CPU接到来自外部的时钟中断信号，就会自行中断，去执行处理时钟中断的内核程序。</p><p><img src="https://files.seeusercontent.com/2026/03/11/wNo5/image-20260311152139906.png" alt="image-20260311152139906"></p><p>外中断还可以来自打印机打印完毕，U盘接口松动等<strong>IO中断请求</strong>。</p><h3 id="系统调用"><a href="#系统调用" class="headerlink" title="系统调用"></a>系统调用</h3><p>“系统调用”是<strong>操作系统提供给应用程序（程序员/编程人员）使用的接口</strong>，可以理解为一种可供应用程序调用的特殊函数，应用程序可以通过系统调用来<strong>请求获得操作系统内核的服务。</strong></p><p><img src="https://files.seeusercontent.com/2026/03/11/5uDk/image-20260311154915374.png" alt="image-20260311154915374"></p><table><thead><tr><th>普通应用程序</th><th>可直接进行系统调用，也可使用库函数。 有的库函数涉及系统调用，有的不涉及</th></tr></thead><tbody><tr><td>编程语言</td><td>向上提供库函数。有时会将系统调用封装 成库函数，以隐藏系统调用的一些细节， 使程序员编程更加方便。</td></tr><tr><td>操作系统</td><td>向上提供系统调用，使得上层程序能请求 内核的服务</td></tr><tr><td>裸机</td><td>提供硬件支持</td></tr></tbody></table><p>不涉及系统调用的<strong>库函数</strong>：如的“取绝对值”的函数<br>涉及系统调用的<strong>库函数</strong>：如“创建一个新文件”的函数</p><p>系统调用是必须的，用户只能通过系统调用来使用共享资源，<strong>由操作系统来统一调度和分配</strong>，保证系统的稳定性和安全性。</p><p>应用程序一般会执行数个传参指令，用于确认系统调用的类型，随后执行陷入指令，CPU开始执行对应的<strong>系统调用入口程序</strong>，开始判断程序需要哪些内核服务，随后执行所需的内核服务</p><p><img src="https://files.seeusercontent.com/2026/03/11/2Kro/image-20260311160254282.png" alt="image-20260311160254282"></p><p><img src="https://files.seeusercontent.com/2026/03/11/r2gE/image-20260311160530423.png" alt="image-20260311160530423"></p><h3 id="操作系统体系结构"><a href="#操作系统体系结构" class="headerlink" title="操作系统体系结构"></a>操作系统体系结构</h3><p><img src="https://files.seeusercontent.com/2026/03/11/Qy1b/image-20260311161756247.png" alt="image-20260311161756247"></p><p>操作系统内部能够细分，分为<strong>内核功能和非内核功能</strong>，内核功能最重要，而乌班图等操作系统开发者往往实现<strong>非内核功能。</strong></p><p>原语是原子程序，执行必须一步到位，不能被中断。</p><p><img src="https://files.seeusercontent.com/2026/03/11/Zlh9/image-20260311162004741.png" alt="image-20260311162004741"></p><p>内核同样可以再分，分为<strong>大内核和微内核。</strong></p><p>微内核只保留与硬件关系最紧密的部分，这会对性能造成影响。</p><p><img src="https://files.seeusercontent.com/2026/03/11/Gn0s/image-20260311162333001.png" alt="image-20260311162333001"></p><p>所以大内核有更高的性能。</p><p>但是微内核将大部分操作隔绝在用户态，更安全，且体积小，维护轻松。</p><p>Linux, Unix, Windows内核都是大内核。</p><p>操作系统也具有<strong>分层结构</strong>，每一层能够调用更低一层提供的接口，而本层必须<strong>向更高一层</strong>提供接口和服务。</p><p><img src="https://files.seeusercontent.com/2026/04/19/Gk1j/image-20260419130323856.png" alt="image-20260419130323856"></p><p>这样的分层结构<strong>便于验证和扩展</strong>，但是每层只能调用相邻层的接口，不能跨层调用这意味着0和2层<strong>不能直接通信</strong>，所以效率偏低。</p><p>操作系统有模块化的特点，内核由<strong>主模块</strong>和<strong>可加载内核模块</strong>组成，主模块是管理进程和内存的核心，可加载内核模块是锦上添花的部分。</p><p>模块化便于维护，且模块之间能够直接调用，效率高，但是因为模块之间的相互依赖，调试上可能出现困难。<strong>大内核采用了模块化设计</strong>，上图中的进程管理和存储管理就是不同的模块，之间能够相互调用。</p><p>操作系统还有一个组件叫做<strong>外核</strong>。内核负责进程调度、进程通信等功能，<strong>外核负责为用户进程分配未经抽象的硬件资源</strong>，且由外核负责保证资源使用安全。内核为了统一管理和简化使用会采取抽象的方式，将硬盘上的离散空间抽象成连续空间。但外核则强调减少抽象，赋予程序更大的控制权和灵活性。</p><p>举个例子，虚拟硬件资源是映射的结果，提供给应用程序的“连续”的地址可能不是在物理意义上连续的，如果某个应用程序知道自己的某一部分数据将在未来被高频访问，那么就能够向外核申请一片连续的硬盘空间存放数据以显著提升缓存命中率。</p><p>外核降低了系统的一致性，且会使得系统变得更复杂。</p><h3 id="操作系统的引导"><a href="#操作系统的引导" class="headerlink" title="操作系统的引导"></a>操作系统的引导</h3><p>Boot，英文语义为靴子，鞋带，鞋带是便于穿靴子的工具。</p><p>所以<strong>Boot是计算机操作系统的引导程序，也就是运行操作系统的工具。</strong></p><p>我们每个人都应该有能力将一个裸机安装操作系统。</p><p><img src="https://files.seeusercontent.com/2026/04/19/8kAf/image-20260419130605772.png" alt="image-20260419130605772"></p><p>操作系统引导：</p><ul><li>CPU从一个特定主存地址开始，取指令，执行ROM中的引导程序（先进行硬件自检，再开机）</li><li>将磁盘的第一块——主引导记录 读入内存，执行磁盘引导程序，扫描分区表</li><li>从活动分区（又称主分区，即安装了操作系统的分区）读入分区引导记录，执行其中的程序</li><li>从根目录下找到完整的操作系统初始化程序（即 启动管理器）并执行，完成“开机”的一系列动作</li></ul><h3 id="操作系统的虚拟机"><a href="#操作系统的虚拟机" class="headerlink" title="操作系统的虚拟机"></a>操作系统的虚拟机</h3><p>传统计算机中，一个物理机器上只能运行一个操作系统，操作系统中有很多进程。但是因为只能运行一个操作系统，存在<strong>利用效率低下的问题和争抢资源的问题</strong>。</p><p>在这个问题上，虚拟机应运而生。</p><p>虚拟机：<strong>使用虚拟化技术，将一台物理机器虚拟化为多台虚拟机器(Virtual Machine,VM),每个虚拟机器都可以独立运行一个操作系统。</strong></p><p>同义术语：虚拟机管理程序/虚拟机监控程序Virtual Machine Monitor/Hypervisor</p><p>虚拟机的实现原理有两类，分别是<strong>直接运行在硬件上</strong>和<strong>运行在宿主操作系统上</strong>。</p><p><img src="https://files.seeusercontent.com/2026/03/12/Txi9/image-20260312120948139.png" alt="image-20260312120948139"></p><h4 id="第一类"><a href="#第一类" class="headerlink" title="第一类"></a>第一类</h4><p>第一类直接运行在硬件上，由运行在内核层的第一类虚拟机管理程序管理和分配。虚拟机管理程序会将总的硬件资源通过虚拟化划分为多个部分，这可以通过将CPU划分为不同的时间片再分配给虚拟机来做到。</p><p>这就有一个问题，运行在<strong>操作系统中的应用程序</strong>(全程处于用户态)会执行特权指令，这时第一类虚拟机管理程序会将提交的<strong>特权指令请求</strong>做等价交换来执行。</p><p>注意，<strong>双系统不属于第一类，第一类虚拟机管理程序是一个极简的操作系统。</strong></p><h4 id="第二类"><a href="#第二类" class="headerlink" title="第二类"></a>第二类</h4><p>第二类运行在一个宿主操作系统Host OS上，并通过<strong>VMware,VirtualBox</strong>等虚拟机管理软件在宿主上安装并运行客户操作系统(Guest OS)。在软件中你能够自主分配给机器的运存和硬盘空间。</p><table><thead><tr><th></th><th>虚拟机种类</th><th></th></tr></thead><tbody><tr><td></td><td>第一类VMM</td><td>第二类VMM</td></tr><tr><td>对物理资源的控制权</td><td>直接运行在硬件之上，能直接控制和分配物理资 源</td><td>运行在Host OS之上，依赖于Host OS为其分配物 理资源</td></tr><tr><td>资源分配方式</td><td>在安装Guest OS时，VMM要在原本的硬盘上自 行分配存储空间，类似于”外核”的分配方式，分 配未经抽象的物理硬件</td><td>GuestOS 拥有自己的虚拟磁盘，该盘实际上是 Host OS 文件系统中的一个大文件。GuestOS分 配到的内存是虚拟内存</td></tr><tr><td>性能</td><td>性能更好</td><td>性能更差，需要HostOS作为”中介”</td></tr><tr><td>可支持的虚拟机数量</td><td>更多，不需要和 Host OS 竞争资源，相同的硬件 资源可以支持更多的虚拟机</td><td>更少，Host OS 本身需要使用物理资源，Host OS上运行的其他进程也需要物理资源</td></tr><tr><td>虚拟机的可迁移性</td><td>更差</td><td>更好，只需导出虚拟机镜像文件即可迁移到另一 台HostOS 上，商业化应用更广泛</td></tr><tr><td>运行模式</td><td>第一类VMM运行在最高特权级（Ring 0），可以 执行最高特权的指令。</td><td>第二类VMM部分运行在用户态、<strong>部分</strong>运行在内核 态。GuestOS 发出的系统调用会被 VMM 截获， 并转化为VMM 对HostOS 的系统调用</td></tr></tbody></table><p>后来的CPU将特权指令划分为更细致的特权级别，绝大多数现代 CPU（尤其是 x86 架构）采用的是 <strong>4 个特权级：Ring 0、Ring 1、Ring 2 和 Ring 3</strong>，这么做能够提升系统运行效率。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;操作系统的概念和功能&quot;&gt;&lt;a href=&quot;#操作系统的概念和功能&quot; class=&quot;headerlink&quot; title=&quot;操作系统的概念和功能&quot;&gt;&lt;/a&gt;操作系统的概念和功能&lt;/h1&gt;&lt;p&gt;操作系统(Operating System,OS)是指&lt;/p&gt;
&lt;ul&gt;
&lt;</summary>
      
    
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
    
    <category term="操作系统" scheme="https://blog.willmo.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
    
  </entry>
  
  <entry>
    <title>第七章：输入输出系统</title>
    <link href="https://blog.willmo.top/posts/Chapter_7_I/O_Systems/"/>
    <id>https://blog.willmo.top/posts/Chapter_7_I/O_Systems/</id>
    <published>2026-01-25T04:29:05.813Z</published>
    <updated>2026-01-25T04:29:06.537Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第七章：输入输出系统"><a href="#第七章：输入输出系统" class="headerlink" title="第七章：输入输出系统"></a>第七章：输入输出系统</h1><p><strong>除了CPU和主存之外的设备都是外围设备，简称外设。</strong></p><p><strong>每个外设都有各自的控制器，也可以称为I/O接口。</strong></p><p><img src="https://s2.loli.net/2026/01/03/w5n4WmeQM8ukvhS.png" alt="image-20260103171311870"></p><p><img src="https://s2.loli.net/2026/01/03/sxUPBrVwR2jHkt3.png" alt="image-20260103171324998"></p><h3 id="机械硬盘（磁盘）"><a href="#机械硬盘（磁盘）" class="headerlink" title="机械硬盘（磁盘）"></a>机械硬盘（磁盘）</h3><p><strong>主存的存储速度很快，但是属于易失性存储器，断电后容易丢失，而且每个sRAM造价很高。</strong></p><p><strong>所以我们需要外存来长期存取数据。</strong></p><p><img src="https://s2.loli.net/2026/01/03/MLmA3YdI9th1zaQ.png" alt="image-20260103171657999"></p><p><strong>机械硬盘的构造如下：</strong></p><p><img src="https://s2.loli.net/2026/01/03/DyFe5Efni2PtKXm.png" alt="image-20260103172127430"></p><p><img src="https://s2.loli.net/2026/01/03/FiuyprHkNgE5Ic6.png" alt="image-20260103172144628"></p><p><strong>位密度：单位长度内存放的二进制数目。</strong></p><p><strong>扇区：就是数学意义上的扇形。</strong></p><table><thead><tr><th><strong>对比维度</strong></th><th><strong>低密度存储（恒定扇区数）</strong></th><th><strong>高密度存储（恒定位密度）</strong></th></tr></thead><tbody><tr><td><strong>分区方式</strong></td><td><strong>将盘面划分为数量相等</strong>的扇形区域</td><td><strong>将盘面划分为弧长相等</strong>的扇形区域</td></tr><tr><td><strong>位密度特性</strong></td><td><strong>内圈位密度高，外圈位密度低磁盘整体位密度不均</strong></td><td><strong>内外圈位密度保持一致磁盘整体位密度恒定</strong></td></tr><tr><td><strong>扇区数量</strong></td><td><strong>每个磁道上的扇区数量相同</strong></td><td><strong>外圈磁道的扇区数量远多于</strong>内圈磁道</td></tr><tr><td><strong>优点</strong></td><td><strong>控制逻辑相对简单（早期技术）</strong></td><td><strong>大幅提升存储容量</strong>，更好地利用外圈空间</td></tr><tr><td><strong>缺点</strong></td><td><strong>存储空间浪费严重（外圈位密度低）</strong></td><td><strong>需要更复杂的磁头和控制算法来读写不同数量的扇区</strong></td></tr></tbody></table><p><img src="https://s2.loli.net/2026/01/03/FYM7WGjiQDgo16B.png" alt="image-20260103172530927"></p><p><img src="https://s2.loli.net/2026/01/03/CWQpsovG29xJTi1.png" alt="image-20260103172922303"></p><h3 id="磁盘的平均存取时间"><a href="#磁盘的平均存取时间" class="headerlink" title="磁盘的平均存取时间"></a>磁盘的平均存取时间</h3><p><strong>一次存取的时间由三部分时间构成：</strong></p><ul><li><strong>寻道时间：磁头从当前位置移动到目标数据所在磁道</strong>所需的时间</li><li><strong>旋转等待时间：磁头到达目标磁道后，等待磁盘旋转，使目标扇区转到磁头下方</strong>所需的时间</li><li><strong>数据传输时间：目标扇区位于磁头下方后，实际读取或写入数据</strong>所花费的时间</li></ul><p><strong>以上三种时间都有最好和最坏情况，只能取平均，作为评判磁盘性能的指标给到消费者。</strong></p><p><img src="https://s2.loli.net/2026/01/03/ov7DYAjzFEtgJfN.png" alt="image-20260103181015576"></p><p><strong>看一道例题：</strong></p><p><img src="https://s2.loli.net/2026/01/03/6JodCpTe7qyB1KW.png" alt="image-20260103181213967"></p><p><img src="https://s2.loli.net/2026/01/03/TGatJglLS5mE1ob.png" alt="image-20260103181739046"></p><h3 id="磁盘驱动器和控制器"><a href="#磁盘驱动器和控制器" class="headerlink" title="磁盘驱动器和控制器"></a>磁盘驱动器和控制器</h3><p><strong>CPU的寄存器很小，需要配合主存来控制磁盘。</strong></p><p><img src="https://s2.loli.net/2026/01/03/UOHNn3wzV6x97vs.png" alt="image-20260103183142943"></p><p><strong>CPU的机器字长是4个字节，那么一共512B的数据就需要传128次（我指的是128个周期），数据经过CPU后放到主存这种传输效率是比较慢的。</strong></p><p><strong>位于主机和磁盘之间的是磁盘控制器，能够翻译地址。</strong></p><p><img src="https://s2.loli.net/2026/01/03/fKF5axiTOsUHJRW.png" alt="image-20260103183533070"></p><p><strong>具体流程如下：</strong></p><p><img src="https://s2.loli.net/2026/01/03/wsuWtLRXF583EQV.png" alt="image-20260103184002764"></p><p><strong>首先，主机通过总线将逻辑块号发送到磁盘控制器翻译地址，同时将主存中的数据的头32位数据传到数据缓冲寄存器。磁盘控制器的电路发出写控制信号，地址翻译模块将翻译后的地址发送到磁盘驱动器进行寻道，完毕后发回结束信号，随即磁头开始定位并匹配，匹配后发回信号，控制器发送写命令，开始读写数据。数据缓冲寄存器的数据送到磁头，主句再将后续的数据经过数据缓冲寄存器写到磁盘。</strong></p><p><strong>总结一下：</strong></p><p><img src="https://s2.loli.net/2026/01/03/NQOYBethqoXvaVZ.png" alt="image-20260103184937070"></p><h3 id="固态硬盘（SSD）"><a href="#固态硬盘（SSD）" class="headerlink" title="固态硬盘（SSD）"></a>固态硬盘（SSD）</h3><p><strong>比机械硬盘更快，但是很贵。</strong></p><p><strong>固态硬盘使用半导体材料制成，是高密度非易失。型存储。但是读取能够马上读取，写需要先擦除再写入，写速度和磁盘差不多，而且擦写是有寿命的</strong></p><p><strong>固态硬盘是用页和块来分区的，CPU通过硬盘控制器来实现对硬盘的管理。</strong></p><p><img src="https://s2.loli.net/2026/01/03/z8bUte5pBSKvXL3.png" alt="image-20260103185343981"></p><p><strong>为了解决擦写寿命短的问题，硬盘控制器引入了磨损均衡，分为两种</strong></p><ul><li><strong>动态磨损均衡：写入数据时，自动选择较新的闪存块。</strong></li><li><strong>静态磨损均衡：就算没有数据写入，SSD也会检测并自动进行数据分配，让老的闪存块承担无须写数据的存储任务，同时让新的闪存块腾出空间，承担平常的写操作。</strong></li></ul><h3 id="IO模块的通用接口"><a href="#IO模块的通用接口" class="headerlink" title="IO模块的通用接口"></a>IO模块的通用接口</h3><p><strong>无论是磁盘还是硬盘以及其他的外设，我们都可以把他们的控制器称作是IO接口。</strong></p><p><strong>他们具有一些通用特性，请回忆计网那本书的内容。</strong></p><p><strong>主机想要访问某一设备时，IO桥会将目标设备号广播，直到目标外设回应</strong></p><p><img src="https://s2.loli.net/2026/01/03/zJmniI1E8oPtMGp.png" alt="image-20260103200325016"></p><p><strong>这意味着IO接口要和主机通讯，IO接口中有数据缓冲寄存器，设备状态寄存器，命令控制寄存器等寄存器用数据总线与主机相连。还有设备选择电路用地址总线与主机相连，还有IO控制电路用控制总线与主机相连。</strong></p><p><img src="https://s2.loli.net/2026/01/03/ApQm3jRyLU67Y9F.png" alt="image-20260103200929408"></p><p><strong>比如打印机的IO运行模式：</strong></p><p><img src="https://s2.loli.net/2026/01/03/BO1XKYaembcAzqr.png" alt="image-20260103201032975"></p><p><strong>现在需要把主存中的ABC三个字符打印出来。</strong></p><p><strong>CPU发送启动命令，打印机命令译码器分析，启动打印机，状态寄存器设置为就绪，随即主存的A暂存在eax寄存器，读出打印机为就绪状态，A和打印命令通过数据总线传送到打印机，译码器分析后打印机开始工作，状态设置为busy，CPU将B传到eax寄存器之后不断检查打印机状态，如果为就绪就继续下一个字母。</strong></p><p><strong>循环这个流程，直到完全打印完毕。</strong></p><p><strong>IO端口的编址有独立编址和统一编址。</strong></p><p><strong>独立编址就是基于总线并独立于主存地址，给每个外设分配一个地址，操作时加上OUT或INSERT等关键词来表示对IO设备的操作。这种方式因为IO设备数量不多，寻址快，但是只有输入输出，灵活性差。</strong></p><p><strong>统一编址就是将主存的一部分地址拿出来分配给IO设备，常被精简指令集采用，不需要设置专门的IO指令，只需要经典的访存指令就能满足需求，CPU会自动根据地址的区间来判断具体的操作设备。</strong></p><h3 id="IO程序查询方式"><a href="#IO程序查询方式" class="headerlink" title="IO程序查询方式"></a>IO程序查询方式</h3><p><strong>我们上个例子说到，打印机忙碌时，CPU不断检查打印机状态，这就是一种独占查询方式，CPU必须干等着，利用率很低，即使实时性无可比拟。</strong></p><p><strong>定时查询方式</strong>：CPU启动I/O后，<strong>返回执行正常程序</strong>。同时，一个定时器被设定，每隔固定时间（如每毫秒）产生一次中断，CPU在中断处理程序中查询一次设备状态。利用率较高，但是存在延迟。</p><p><strong>我们以鼠标为例子：</strong></p><p><img src="https://s2.loli.net/2026/01/04/oe9k4mbB37gYTD6.png" alt="image-20260104075518671"></p><p><strong>再以硬盘为例子：</strong></p><p><img src="https://s2.loli.net/2026/01/04/rqW4KEkgCPU78ab.png" alt="image-20260104075649177"></p><p><strong>所以：</strong></p><p><img src="https://s2.loli.net/2026/01/04/tBURdVDMc4f2N7r.png" alt="image-20260104075718827"></p><p><strong>但是我们希望时外设自己执行完命令就主动通知CPU，而不是CPU是不是就要去看一样，那样就可以实现并行运行。</strong></p><p><del>就像我希望老师可以在讲完后主动提醒我这节课签到而不是我一直盯着智慧课堂看他签不签到。</del></p><h3 id="IO程序中断控制方式"><a href="#IO程序中断控制方式" class="headerlink" title="IO程序中断控制方式"></a>IO程序中断控制方式</h3><p><strong>I/O（输入/输出）程序中断控制方式是计算机管理中一种关键的数据交换机制。它的核心思想是变被动等待为主动通知</strong>：CPU启动I/O设备后便可继续执行原有任务，当设备完成操作时主动“打断”CPU，CPU再转去处理这次I/O请求，从而极大提升了工作效率。</p><p><strong>初始化与并行工作</strong>：CPU执行一条I/O指令，启动外部设备（如磁盘读取）。之后，CPU<strong>不再等待</strong>，而是立即返回执行其主程序。此时，<strong>CPU和设备处于并行工作状态</strong>。</p><p><strong>中断请求与响应</strong>：当设备完成数据准备后，它会通过中断请求线向CPU发出一个<strong>中断请求信号</strong>。CPU并非随时响应，而是在<strong>当前指令执行周期结束时</strong>，才会检查是否有中断请求。如果请求有效且未被屏蔽，CPU则会响应中断。</p><p><strong>核心处理与数据传送</strong>：响应中断后，CPU通过<strong>中断向量表</strong>自动定位并执行对应的<strong>中断服务程序</strong>。ISR的核心任务包括从I/O设备的数据寄存器中读取一个字（或字节）的数据，并最终将其存入内存。</p><p><strong>收尾与返回</strong>：数据传送完成后，ISR<strong>中断服务例程</strong>会执行<strong>中断返回指令</strong>。该指令能自动恢复之前保存的现场，使CPU跳回主程序的断点处继续执行</p><p><strong>这一次我们以键盘为例</strong></p><p><img src="https://s2.loli.net/2026/01/05/UprKDSndwVE39Xx.png" alt="image-20260105151315875"></p><p><strong>假设当前CPU正在执行某一程序的某条指令D，下一条指令程序的地址是0x1234，也就是说PC里的地址是0x1234。这时你在键盘上输入了一个字母a，字母a被送到数据寄存器中，同时IO控制电路发送高电平刀中断触发器中，随后CPU在执行完毕指令D之后给中断触发器发送一个中断查询信号，看看有没有外设向CPU发送中断请求。随机中断触发器通过控制总线发送中断信号发送到CPU的中断系统，随即CPU知道键盘有新数据产生，现在需要优先处理键盘数据，所以CPU会执行键盘中断程序。将PC中的值0x1234移动到栈内存中，然后将PC的值设置为键盘中断程序的首条指令的地址0x8888，开始执行程序，键盘中断程序将输入的a移动到主存并将其显示到显示器当中。当键盘中断程序结束后，PC再取回存在栈内存中的0x1234继续执行原程序。</strong></p><blockquote><p><strong>网课还讲了打印机和鼠标的例子，但是没有补充</strong></p></blockquote><p><strong>这种方式一些问题，一个是中断触发器分布在多个IO接口中难以管理，而且cpu一次只能响应一次中断请求，这就产生了类似总线总裁的问题。</strong></p><p><img src="https://s2.loli.net/2026/01/05/q985RLPj6hondiv.png" alt="image-20260105154448281"></p><p><strong>为了解决这个问题，就有了中断控制器，内置介个中断请求寄存器，接到每个外设的中断触发器中。控制器在接收到CPU的中断查询信号时，会将已接收的中断请求通过中断判优电路排序优先级，再通过设备编码器返回编码后的向量地址（存储地址）。</strong></p><p><strong>插播一下跳转，每个子程序的头指令的地址都会放在主存的某个地方并且有一个相应的地址与之对应，比如上图中0x6666对应地址0xCCCC，对应程序打印机中断服务程序。</strong></p><p><strong>比如现在鼠标发出一个中断信号，经过判别电路和编码器生成的中断向量地址0x665F，随后PC值会被转移到栈内存，PC值更新为0x665F，执行，跳转到0x8888，执行鼠标中断服务程序。</strong></p><h3 id="中断控制的条件和步骤"><a href="#中断控制的条件和步骤" class="headerlink" title="中断控制的条件和步骤"></a>中断控制的条件和步骤</h3><p><img src="https://s2.loli.net/2026/01/05/zIursPeDJ6x5fhE.png" alt="image-20260105162424392"></p><p><strong>整个过程分为三部</strong></p><ul><li><strong>中断识别和判优</strong></li><li><strong>中断响应</strong></li><li><strong>中断处理</strong></li></ul><p><strong>CPU响应中断是有条件的</strong></p><ul><li><strong>至少有一个中断请求</strong></li><li><strong>当前指令刚刚执行完</strong></li><li><strong>CPU处于开中断状态</strong></li></ul><p><strong>指令执行完CPU才会发送中断查询信号。</strong></p><h4 id="中断识别与判优"><a href="#中断识别与判优" class="headerlink" title="中断识别与判优"></a>中断识别与判优</h4><p><strong>CPU执行某一条指令完毕时，中断控制器接收到CPU发送的中断查询请求，内部集合，判优，编码中断请求，并发回到CPU中去。</strong></p><h4 id="中断响应"><a href="#中断响应" class="headerlink" title="中断响应"></a>中断响应</h4><p><strong>通过中断隐指令完成，中断隐指令不是一个真正的指令，而是CPU一个自动完成的流程。</strong></p><p><strong>中断隐指令：</strong></p><ul><li><strong>关中断</strong></li><li><ul><li><strong>一个标志，因为CPU一时间只能处理一个请求，当关中断尾关时，CPU不再接收新的请求。</strong></li></ul></li><li><strong>断点与程序状态</strong></li><li><ul><li><strong>断点其实就是当前PC的值，处理中断的时候断点会被放到EPC寄存器（MIPS）或者栈内存（x86）中。程序状态将存放在CPU的标志寄存器中去</strong></li></ul></li><li><strong>寻找中断服务程序入口地址并跳转</strong></li><li><ul><li><strong>就是根据中断控制器传入的向量地址找到中断程序的首条指令的地址</strong></li></ul></li></ul><h4 id="中断处理阶段"><a href="#中断处理阶段" class="headerlink" title="中断处理阶段"></a>中断处理阶段</h4><p><strong>中断处理涉及到控制流程的转移，那么就需要关注原来执行的程序A。</strong></p><p><img src="https://s2.loli.net/2026/01/05/zCwQLdKkjgh8e73.png" alt="image-20260105162726978"></p><p><strong>鼠标中断服务程序执行完了CPU要回到程序A继续执行，就分为以下几个流程。</strong></p><ul><li><strong>保护现场</strong></li><li><ul><li><strong>在刚进入中断服务程序时，通过软件（如PUSH指令）将当前所有通用寄存器（如EAX, EBX等）和程序状态字的值压入系统堆栈保存 。</strong></li></ul></li><li><strong>恢复现场</strong></li><li><ul><li><strong>在中断服务程序结束前，通过软件（如POP指令）将之前压栈的寄存器值按相反顺序弹出到原来的寄存器中 。</strong></li></ul></li><li><strong>开中断</strong></li><li><ul><li><strong>在恢复现场操作之后、执行中断返回指令之前，使用STI等指令将中断标志位重新置位。这通常在中断服务程序的最后阶段进行。</strong></li></ul></li></ul><p><strong>整体流程：</strong></p><p><img src="https://s2.loli.net/2026/01/05/EkAob6QTqgP4uD2.png" alt="image-20260105163544780"></p><p><strong>以上描述的是单中断，以下我们探讨多重中断（嵌套中断）</strong></p><h3 id="多重中断和中断屏蔽字"><a href="#多重中断和中断屏蔽字" class="headerlink" title="多重中断和中断屏蔽字"></a>多重中断和中断屏蔽字</h3><p><strong>多重中断：是指当CPU正在执行某个中断服务程序时，另一个中断源又提出了新的中断请求，而CPU又响应了这个请求，暂时停止正在的中断服务程序，转去执行新的中断服务程序。</strong></p><p><strong>多重中断的</strong>“关中断”的状态并不是整个中断处理过程中不可改变的，操作系统可以通过软件指令在关键时刻“开中断”，从而允许更高优先级的中断嵌入。</p><p><strong>比如，现在是CPU正在运行鼠标中断服务程序，CPU为了能够接收更高级的中断请求将关中断设置为开</strong>，随即将鼠标中断服务程序的断电保存到其他地方去，执行键盘中断程序，这就实现了多重中断。</p><p><img src="https://s2.loli.net/2026/01/05/3SHTqmkvrKGo9l8.png" alt="image-20260105201502482"></p><p><strong>但是这么做就需要保证：只有优先级更高的中断源请求才可以中断比其级别低的中断服务程序，反之则不然。</strong></p><p><strong>要完成这个逻辑就需要引入中断屏蔽字。</strong></p><p><strong>中断屏蔽字：一组二进制代码，用来控制CPU是否响应某个中断源发出的请求，从而实现对中断处理顺序的精细管理。</strong></p><p><strong>比如有以下四个中断源，那就需要完成这样的逻辑</strong></p><p><img src="https://s2.loli.net/2026/01/05/dJNaEMf5gPS1ryw.png" alt="image-20260105202252241"></p><p><strong>我们就需要这样的中断屏蔽字来实现，在这里，1就是不接受对应外设的中断请求。</strong></p><p><img src="https://s2.loli.net/2026/01/05/JQUXfpA1uZPc8Te.png" alt="image-20260105202941209"></p><p><strong>那么硬件具体如何实现？</strong></p><p><img src="https://s2.loli.net/2026/01/05/GldteyJAUcrowDp.png" alt="image-20260105203324449"></p><p><strong>如图，中断控制器内置一个中断屏蔽字寄存器，现在CPU正在执行C的中断服务程序，当B传出中断请求，控制器的判优电路认为B优先级低，CPU不会响应B的指令。如果是A发送请求，那根据判优电路，A的优先级高，C的断电被保存，控制流程转移，开始执行A的中断服务程序。A的中断服务程序执行完后，开始执行C的中断服务程序。</strong></p><h3 id="磁盘的中断控制"><a href="#磁盘的中断控制" class="headerlink" title="磁盘的中断控制"></a>磁盘的中断控制</h3><p><img src="https://s2.loli.net/2026/01/05/9tFe58ShHjLykBC.png" alt="image-20260105204530798"></p><h2 id="DMA控制方式"><a href="#DMA控制方式" class="headerlink" title="DMA控制方式"></a>DMA控制方式</h2><p><strong>DMA（Direct Memory Access，直接内存访问）是一项重要的计算机技术，它允许外部设备（如硬盘、网卡）与计算机内存之间直接进行数据交换，而无需中央处理器（CPU）频繁介入。</strong></p><p><strong>DMA接口可以位于任何外设当中，比如有能力绕过CPU将数据从磁盘直接移动到主存中，从而减轻CPU开销。</strong></p><p><strong>DMA接口又名DMA控制器，内部构成是：</strong></p><ul><li><strong>AR：Address Register，主存地址寄存器</strong></li><li><strong>WC：Word Count，字计数器</strong></li><li><strong>BR：Buffer Register，数据缓冲寄存器</strong></li><li><strong>DAR：Device Address Register，设备地址寄存器</strong></li><li><strong>其他控制逻辑</strong></li></ul><p><img src="https://s2.loli.net/2026/01/05/JfW9bdP6VX7NeBw.png" alt="image-20260105211356026"></p><p><strong>如图CPU将操作参数传至DMA控制器中后，磁盘准备好一个字的数据传到DMA的BR中，同时发送DMA请求，DMA控制逻辑发送总线申请，主存控制器允许使用总线，DMA发送主存地址和主存读写命令，一个字的数据被传到主存，最后DMA控制逻辑授予磁盘一个DMA周期，主存地址AR和WC也要相应变化，为下一个字做准备。最后发送DMA中断请求，CPU接受后执行DMA中断服务，开始检查传输是否有误等后续工作。</strong></p><p><strong>如果有时候DMA准备使用主存时，CPU已经使用主存。这就产生了冲突。</strong></p><p><strong>解决的第一种方法就是DMA夺权：DMA就会向CPU提出申请让他取消使用主存让出总线控制权，DMA获取控制器完成数据传输后会将控制权还给CPU。但是这种方法会导致CPU在DMA使用总线时进入什么都不做的空窗期。</strong></p><p><strong>第二种是周期挪用：IO设备挪用或者窃取总线占用权一个或几个贮存周期。DMA仅在需要传输数据时</strong>“借用”一个或几个总线周期。若CPU不使用总线，则无冲突；若冲突，则DMA优先（这个时候BR已经是满的，IO设备再等会触发BR覆写，原有数据丢失会导致整体传输失败）</p><p><strong>第三种是DMA和CPU交替访问：将CPU的工作周期强制分时</strong>，一部分专供DMA访存，另一部分专供CPU访存，这样会扩大存储周期</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第七章：输入输出系统&quot;&gt;&lt;a href=&quot;#第七章：输入输出系统&quot; class=&quot;headerlink&quot; title=&quot;第七章：输入输出系统&quot;&gt;&lt;/a&gt;第七章：输入输出系统&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;除了CPU和主存之外的设备都是外围设备，简称外设。&lt;/str</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>第六章：中央处理器</title>
    <link href="https://blog.willmo.top/posts/Chapter_6_CPU/"/>
    <id>https://blog.willmo.top/posts/Chapter_6_CPU/</id>
    <published>2026-01-25T04:27:07.602Z</published>
    <updated>2026-01-25T04:27:08.486Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第六章：中央处理器"><a href="#第六章：中央处理器" class="headerlink" title="第六章：中央处理器"></a>第六章：中央处理器</h1><p><strong>全章节学习路线：</strong></p><p><img src="https://s2.loli.net/2025/12/05/51ySOEgGCs2YwmW.png" alt="image-20251205193311196"></p><h2 id="数电基础"><a href="#数电基础" class="headerlink" title="数电基础"></a>数电基础</h2><p><strong>组合逻辑电路在任何时刻的输出信号的稳定值，仅仅与该时刻的输入信号有关，而与该时刻以前的输入信号无关。</strong></p><p><strong>但时序电路的输出不仅取决于当前的输入，还取决于电路原来的状态</strong>。</p><p><strong>CPU是边沿反应的，这是为了阻止输出信号震荡的问题。</strong></p><h3 id="中央从处理器"><a href="#中央从处理器" class="headerlink" title="中央从处理器"></a>中央从处理器</h3><p><strong>CPU由数据通路和控制单元组成。</strong></p><p><strong>CPU内部集成了一根内部总线，但是存在信号碰撞的问题，也就是在同一时刻只能传输一个数据。</strong></p><p><img src="https://s2.loli.net/2025/12/05/84oU9hWR6kTLVDy.png" alt="image-20251205200944438"></p><p><strong>元件是否进入总线是由三态门把关的，三态门接收到使能信号就允许通过，反之成立，每个三态门都与控制单元连接。控制单元还通过使能信号端来控制每个元件。</strong></p><p><strong>数据通路指的是：指令执行过程中数据经过的路径，包括路径上的部件，称为数据通路。</strong></p><p><strong>控制单元根据每条指令的功能不同生成对数据通路的控制信号，并正确控制指令的执行流程。</strong></p><h3 id="取指阶段"><a href="#取指阶段" class="headerlink" title="取指阶段"></a>取指阶段</h3><p><strong>需要执行的指令存在主存中，那么将指令cong主存读取到CPU的过程就是取指阶段。</strong></p><p><strong>取指阶段由六个微操作组成。</strong></p><p><strong>第一步，CU向pc和mar发送信号，PC的主存地址会通过总线传输到MAR中，MAR将通过总线送到主存中</strong></p><p><strong>第二步：CU向主存发送信号</strong></p><p><strong>第三步：主存返回指令存放到MDR</strong></p><p><strong>第四步：向IR和MDR发送信号，将 MDR发送到IR</strong></p><p><strong>第五步：将IR发送到CU进行译码分析</strong></p><p><strong>第六步：译码分析并执行完毕后，CU向pC发送信号使PC自增1</strong></p><blockquote><p><strong>为了加速单次取指的时间，我们需要注意：部分微操作的次序不可改变。</strong></p><p><strong>被控制对象不同的微操作应尽可能放在同一个时钟周期节省时间。</strong></p><p><strong>占用时间不长的微操作应该安排到同一个时钟周期内完成，允许其有不同的次序</strong></p></blockquote><p><strong>注意看，第一步和第二步的被操作对象不同，那么我们就能够将他们放在同一个时钟周期内</strong></p><p><strong>第三第四第五步有先后顺序，不能改变。</strong></p><p><strong>但是第六步自增可以和第三步安排在同一时钟周期。</strong></p><p><strong>只要这两个微操作不冲突（不使用同一总线或功能部件），就能够放到同一周期以优化性能。</strong></p><p><strong>所以，加速后的操作次序就是</strong></p><p><strong>T1:PC -&gt; MAR,CU-&gt;Read</strong></p><p><strong>T2:M[MAR]-&gt;MDR,PC++</strong></p><p><strong>T3:MDR-&gt;IR,OP[IR]-&gt;CU</strong></p><p><strong>执行一次取指需要三个时钟周期，</strong></p><p><img src="https://s2.loli.net/2025/12/06/5y7Ghq2dc9RDoTj.png" alt="image-20251206143343069"></p><h3 id="执行阶段"><a href="#执行阶段" class="headerlink" title="执行阶段"></a>执行阶段</h3><p><strong>假设这是一个CPU的指令集：</strong></p><p><img src="https://s2.loli.net/2025/12/06/Oij6Cy3IERNJHKo.png" alt="image-20251206143638747"></p><p><strong>第一条是将制定主存地址的数据加载到寄存器R0中。</strong></p><p><strong>执行起来有三步。</strong></p><p><strong>T1:Ad[IR]-&gt;MAR,1-&gt;Read 将IR中的地址经过MAR发送到主存，CU发送控制读信号。</strong></p><p><strong>T2:M[MAR]-&gt;MDR 将主存中对应地址的数据取出到MDR</strong></p><p><strong>T3:MDR-&gt;R0 将MDR的数据存入到寄存器R0中。</strong></p><p><strong>再比如，倒数第三条指令，是将R1和R0做加法存到R1</strong></p><p><img src="https://s2.loli.net/2025/12/06/emEpPUHBwkAgMS8.png" alt="image-20251206145008437"></p><p><strong>T1:R1-&gt;Y将R1的 值赋给Y</strong></p><p><strong>T2:[R0]+[Y]-&gt;Z 这一步需要CU发送00指令</strong></p><p><strong>T3:Z-&gt;R1</strong></p><p><strong>这三部用的都是同一个对象，不可再做任何优化</strong></p><p><strong>再看JUMP无条件跳转指令</strong></p><p><strong>这个很简单了，就是</strong></p><p><strong>T1:Ad[IR]-&gt;PC</strong></p><p><strong>指令周期：取出并执行一条指令所需的全部时间</strong></p><p><strong>指令周期可以看作是取指周期（Fetch）和执行周期（STORE）组成</strong></p><p><strong>取指周期完成取指令和分析指令的操作</strong></p><p><strong>执行周期是完成执行指令的操作</strong></p><h2 id="控制单元是如何实现的"><a href="#控制单元是如何实现的" class="headerlink" title="控制单元是如何实现的"></a>控制单元是如何实现的</h2><p><strong>控制单元的职能是译码分析，执行指令，发送控制信号等</strong></p><h3 id="组合逻辑控制单元"><a href="#组合逻辑控制单元" class="headerlink" title="组合逻辑控制单元"></a>组合逻辑控制单元</h3><p><img src="https://s2.loli.net/2025/12/06/Iegf72LGSdJVs4H.png" alt="image-20251206151741768"></p><p><strong>使用门电路实现，又名硬部件控制单元，适用于早期计算机。</strong></p><p><strong>其执行速度非常快，原子速度接近光速，但是结果复杂，实现困难，不容易扩充和修改指令，可塑性非常差只适合RISC指令系统，但在追求极致速度的场景下也会使用。</strong></p><h3 id="微程序控制单元"><a href="#微程序控制单元" class="headerlink" title="微程序控制单元"></a>微程序控制单元</h3><p><strong>微程序控制采用”存储器替代逻辑”的思想，将机器指令的执行转化为微程序的执行。</strong></p><p><strong>微程序是一个微指令，一条伪指令包括若干微命令，一条机器指令功能由一个微程序完成。</strong></p><p><strong>你记不记得我们说过，CU能够给每个元件发送控制信号？</strong></p><p><strong>那么请回忆计网那本书的内容。CU能够将对所有的元件发送的信号组合成一个长二进制（也就是一条微指令），每一位都对应不同的元件，随后将该二进制在一个时钟周期内广播，就完成了发送控制信号的工作。</strong></p><p><img src="https://s2.loli.net/2025/12/06/S2WXF6zVIPMDiuo.png" alt="image-20251206201231806"></p><p><strong>那么取指阶段的三个微操作就对应了CU发送的三条微指令，执行ADD命令也是如此。</strong></p><p><img src="https://s2.loli.net/2025/12/06/siwLObeNoXRHYnZ.png" alt="image-20251206201509433"></p><p><strong>这六个微指令合在一起就是微程序，一个微程序就完成一条机器指令的功能，因为取指是公共操作，所以我们会将取指的三个微指令作为一个独立的微程序。</strong></p><p><img src="https://s2.loli.net/2025/12/06/xyDk7lr1F526mQR.png" alt="image-20251206203036427"></p><p><strong>这些微程序存放在CU的控制存储器(Control Storage)当中，是一块只读ROM芯片，简称CS。每一条微指令都有对应的微地址。</strong></p><p><strong>还有一个起始和转移地址发生器</strong>，能够生成下一条微指令的微地址，并存入uPC中（与CPU中的PC功能类似）。指令寄存器uIR会根据uPC的地址获取对应的微指令，控制单元开始译码uIR中的指令并对相应的元件发出相应的控制信号到数据通路中，最后在时钟信号的控制下uPC自动加1.</p><p><strong>微程序控制单元中的转移控制模块能够洁厕出下一条指令的微地址</strong></p><h3 id="下一条指令地址的决策"><a href="#下一条指令地址的决策" class="headerlink" title="下一条指令地址的决策"></a>下一条指令地址的决策</h3><p><strong>执行完一条指令后，下一条指令地址有三种方法得到：机器指令操作码，uPC++，控制转移字段。</strong></p><p><strong>CS中，每个指令末尾都带两位二进制作为控制转移字段。控制转移字段能决定下一条指令的地址选取，就是图中的MUX多路选择器的输入。</strong></p><p><img src="https://s2.loli.net/2025/12/06/jaFVSG2XfT4vuWh.png" alt="image-20251206210151664"></p><p>这种三合一的计数方法，因为大多数是时候都是程序顺序执行，所以在大部分时间都是uPComing增量+1操作，所以我们称其为“<strong>增量计数法</strong>”，这样的计数法要求微指令必须是顺序存放的。</p><p><strong>针对不是顺序存放的我们使用“断定法“。</strong></p><p><img src="https://s2.loli.net/2026/01/03/qJ3I7Rx8Oh2WlHi.png" alt="image-20260103160903297"></p><p><strong>下址就是下一条指令的微地址。</strong></p><p><strong>这样虽然空间利用率低，但是确实能够解决非顺序存放的问题。</strong></p><h3 id="微命令编码问题"><a href="#微命令编码问题" class="headerlink" title="微命令编码问题"></a>微命令编码问题</h3><p><strong>比如这个过程：</strong></p><p><img src="https://s2.loli.net/2026/01/03/fAWMec8TzvGiXDJ.png" alt="image-20260103161808572"></p><p><strong>这么长一个微指令只有三个1信号，这就显得空间利用率低。另外，因为1-&gt;Read，PC-&gt;MAR，MDR-&gt;IR都需要占用总线，这意味着他们是互斥微命令，不能同时执行。这意味着所有相同后缀的互斥信号都因为互斥能够统一管理和编码。</strong></p><p><strong>所以，我们可以将相容的输出信号放到一起，统一编码来优化空间利用率。</strong></p><p><img src="https://s2.loli.net/2026/01/03/MkU356exLwoysmt.png" alt="image-20260103162448299">这样空间利用率就得到极大优化，这样的编码法叫做<strong>字段编码法</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第六章：中央处理器&quot;&gt;&lt;a href=&quot;#第六章：中央处理器&quot; class=&quot;headerlink&quot; title=&quot;第六章：中央处理器&quot;&gt;&lt;/a&gt;第六章：中央处理器&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;全章节学习路线：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>第五章：指令系统</title>
    <link href="https://blog.willmo.top/posts/Chapter_5_Command_System/"/>
    <id>https://blog.willmo.top/posts/Chapter_5_Command_System/</id>
    <published>2026-01-25T04:22:28.892Z</published>
    <updated>2026-01-25T04:22:29.687Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第五章：指令系统"><a href="#第五章：指令系统" class="headerlink" title="第五章：指令系统"></a>第五章：指令系统</h1><p><strong>记得在第一章写的层次结构吗？</strong></p><p><img src="https://s2.loli.net/2026/01/03/nwjRpAyZNLtHbU7.png" alt="image-20251117153817438"></p><p><strong>ISA是一种规定和结构规范，是软件和硬件之间的接口。</strong></p><p><strong>目前分为Intel x86（支配着计算机的PC时代）</strong></p><p><strong>arm（最流行的嵌入式设备指令系统）</strong></p><p><strong>MIPS和RISC-V（新兴的，能够使得计算机系统构建变得简单）</strong></p><p>指令集又分为<strong>复杂指令集</strong>和<strong>精简指令集</strong>，Intel x86属于复杂指令集，其他属于后者。</p><p>MIPS和X86两种<strong>ISA</strong>，都有32位和64位两种版本，也就是说寄存器可以是32位和64位</p><p><img src="https://s2.loli.net/2025/11/26/1WlxsjguXh69D3c.png" alt="image-20251125221854356"></p><p><strong>本章的学习流程会配合C语言，需要你根据一段C语言代码找到对应的MIPS代码。使用</strong><a href="godbolt.org">GodBolt</a>能够将C语音代码转化为其他指令系统的代码。</p><p><img src="https://s2.loli.net/2025/11/26/pM2jP7c6E3HQDrg.png" alt="image-20251126111028860"></p><h2 id="MIPS"><a href="#MIPS" class="headerlink" title="MIPS"></a>MIPS</h2><p><strong>MIPS是“Microprocessor without Interlocked Pipeline Stages”的缩写，即“无互锁流水级的微处理器”</strong></p><p><strong>MIPS规定了在CPU内部有32个通用寄存器，每个寄存器的大小是32位，32位为一个字，也就是一个字等于四个字节。</strong></p><p><strong>MIPS是精简指令集</strong></p><p><img src="https://s2.loli.net/2025/12/01/swF4vS9DKLiyGJc.png" alt="image-20251201144921739"></p><h3 id="寄存器寻址"><a href="#寄存器寻址" class="headerlink" title="寄存器寻址"></a>寄存器寻址</h3><p><img src="https://s2.loli.net/2025/12/01/KgYmB9EbJ2OFder.png" alt="image-20251201193928997"></p><p><strong>注意看，addu是无符号加指令，subu是无符号减指令（上面没有） ，li是加载指令，jal是程序跳转指令，该指令意味着程序需要暂时离开当前位置去执行一个子任务（函数或者子程序）</strong></p><p><strong>首先，将25加载到7号寄存器，将12加载到6号寄存器，直到将-10加载到4号寄存器，jal指令跳转到函数add中继续执行，将4号和5号相加，结果存到4号寄存器，随即将4和6相加放到4号，再将4和7相加，结果放到2号寄存器返回给res</strong></p><h3 id="基址寻址"><a href="#基址寻址" class="headerlink" title="基址寻址"></a>基址寻址</h3><p><img src="https://s2.loli.net/2025/12/01/JkT86s2EhwF5y7X.png" alt="image-20251201200613540"></p><p><strong>栈指针是29号寄存器，里面存有一个存储地址这里是偏移值8，sw是存数指令，将一个字的数据存到指定的内存单元中。</strong></p><p><strong>这里将1存入2号寄存器，随后将2号寄存器的值存入到24+$sp(8) = 32号主存中，循环往复直到主存被写入A数组的6个元素，随后给5号寄存器写入10，addiu执行将8+24=32存放到4号寄存器中，随后jal跳转到add_3</strong></p><p><strong>add_3中有三条指令</strong></p><figure class="highlight apache"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">lw</span> $<span class="hljs-number">2</span>,<span class="hljs-number">12</span>($<span class="hljs-number">4</span>)<br><span class="hljs-attribute">nop</span><br><span class="hljs-attribute">addu</span> $<span class="hljs-number">2</span>,$<span class="hljs-number">2</span>,$<span class="hljs-number">5</span><br></code></pre></td></tr></tbody></table></figure><p><strong>lw是取数，从指定内存地址取得一个字，这是取得4号寄存器的值32+12（索引是3，一个字是4，那就是3*4 = 12）= 44存入2号寄存器中，nop指令是空操作指令，addu将2和5相加，结果存入2号寄存器</strong></p><p><strong>数据传送指令：在主存储器和寄存器之间传送数据指令，比如sw，lw。</strong></p><p><strong>$sp也可以叫基址寄存器,又名栈指针寄存器，写在前面的值是偏移值offset，目标地址为$sp+offset。</strong></p><p><strong>CPU寄存器是主体，主存是客体</strong></p><blockquote><p><strong>初次之外还有立即寻址，也就是操作数就在指令内，这样的操作数称为立即数。</strong></p></blockquote><h3 id="整体指令执行流程"><a href="#整体指令执行流程" class="headerlink" title="整体指令执行流程"></a>整体指令执行流程</h3><p><img src="https://s2.loli.net/2025/12/02/6OqkJhHj18dESfi.png" alt="image-20251202144247923">两种寻址方式讲的有点细，我们需要跳出来看整体的执行流程。指令也一样是存在主存中的，一开始PC是0，CU向PC和MAR发送信号，将PC的值发送到MAR，通过地址总线读取0号内存的指令，CU通过控制总线发送信号，主存将指令发送到CPU的MDR中，PC自动加4(每一条指令4个字节)，MDR中的指令会移交到IR，CU对IR做译码操作，ALU将4号寄存器里的值和偏移值12做加法，得到44送到MAR，CU,MAR配合读出44号内存的数字存到寄存器中。</p><h3 id="R型指令格式"><a href="#R型指令格式" class="headerlink" title="R型指令格式"></a>R型指令格式</h3><blockquote><p><strong>R型指令可以表示算数，逻辑，移位运算指令。</strong></p></blockquote><p><strong>一条指令对应的二进制位长度就是指令字长。</strong></p><p><strong>精简指令集比如RISC或者MIPS是定长的。</strong></p><p><strong>复杂指令集比如CISC是变长的。</strong></p><p><strong>MIPS中所有的指令都是32位的，32位分布结构如下：</strong></p><p><img src="https://s2.loli.net/2025/12/02/Suv75hNLOE91Hd2.png" alt="image-20251202145924526"></p><p><strong>前六位op是用来区分指令格式的，可以分为R型（寄存器型），I型（立即数型），J型（跳转型）等等。</strong></p><p><strong>比如指令</strong><code>add $2,$4,$6</code>$4是rs（第一源）,$6是rt（第二源）,$2是rd（目的寄存器）。</p><p><strong>指令送入IR中，译码器根据前6位操作码确定格式，CU再根据最后6位功能码进行译码分析其操作，转交ALU或者其他模块执行。</strong></p><p><strong>寄存器地址是5位，因为2^5=32，正好能够表示32个寄存器。</strong></p><p>而位移量是5位，<strong>5位二进制数恰好可以表示0到31（2^5=32个值），而这足以覆盖一个32位寄存器所有有意义的移位操作。</strong></p><blockquote><p><strong>你只需要知道原理，不需要知道具体怎么写，我们不可能用汇编语言或者机器语言来写程序。</strong></p></blockquote><h3 id="乘除指令"><a href="#乘除指令" class="headerlink" title="乘除指令"></a>乘除指令</h3><p><strong>记得之前说过的吗？有两个32位的乘商寄存器：Hi和Lo是专用寄存器。</strong></p><p><img src="https://s2.loli.net/2025/12/02/PTivDrAa95IERWZ.png" alt="image-20251202160008452"></p><p><code>mult</code>是将两个操作数存到专用寄存器中，<code>mflo</code>再将结果存到2号寄存器中</p><p><strong>除法也是差不多，在这里就不赘述了。</strong></p><h3 id="R型指令格式的总结"><a href="#R型指令格式的总结" class="headerlink" title="R型指令格式的总结"></a>R型指令格式的总结</h3><p><img src="https://s2.loli.net/2025/12/02/Pi4mMGZjEaK2zOJ.png" alt="image-20251202160632925"></p><p><strong>因为指令字长是一定的，所以即使nop只有一个操作字段，也不得不用32位来表示，虽然这样看起来确实很规整。</strong></p><h3 id="I型指令格式"><a href="#I型指令格式" class="headerlink" title="I型指令格式"></a>I型指令格式</h3><p><img src="https://s2.loli.net/2025/12/02/QLKOVoDb9CZ3gd5.png" alt="image-20251202192910943"></p><p><strong>其中rs是源操作数寄存器，rt是目标操作数寄存器，im是立即数，就是包含在指令中的操作数，范围是-32768到+32767。</strong></p><p><strong>I型指令没有独立的功能码，因为从根本上就不需要。</strong></p><p><strong>比如指令</strong><code>lw $2,1200($4)</code>，所以$4是rs，1200是立即数,$2是目标操作数寄存器。</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p><img src="https://s2.loli.net/2025/12/02/HWkfZ6rehotV1zl.png" alt="image-20251202193834837"></p><h3 id="MIPS执行if语句"><a href="#MIPS执行if语句" class="headerlink" title="MIPS执行if语句"></a>MIPS执行if语句</h3><p><img src="https://s2.loli.net/2025/12/02/h4dDRG5BuM6nAmf.png" alt="image-20251202195009184"></p><p><img src="https://s2.loli.net/2025/12/02/Zu1CFIVeifPpDxG.png" alt="image-20251202202342495"></p><p><img src="https://s2.loli.net/2025/12/02/RG3XgTUisn7trZH.png" alt="image-20251202202356984"></p><h3 id="MIPS中执行循环结构"><a href="#MIPS中执行循环结构" class="headerlink" title="MIPS中执行循环结构"></a>MIPS中执行循环结构</h3><p><img src="https://s2.loli.net/2025/12/03/vJOia81wg9jmHk5.png" alt="image-20251203134516104"></p><p>将同一段高级语言代码能够转化为不同的汇编语言代码，不同汇编代码之间性能差别很大，<strong>挑选并采用最佳性能的过程叫优化。</strong></p><p><img src="https://s2.loli.net/2025/12/03/ISRH4uOvjGCo6L8.png" alt="image-20251203135415382"></p><p><img src="https://s2.loli.net/2025/12/03/XG3h82RgSm5VbMY.png" alt="image-20251203135444575"></p><h3 id="指令设计"><a href="#指令设计" class="headerlink" title="指令设计"></a>指令设计</h3><p><strong>指令长度：字面意思，就是指令的长度，就是指令的二进制位数。</strong></p><p><strong>越短越好，降低空间开销。</strong></p><p>可以设置为**定长指令集(MIPS)，或者是变长指令集(x86)**。</p><h3 id="指令格式："><a href="#指令格式：" class="headerlink" title="指令格式："></a>指令格式：</h3><p><strong>一般指令都需要两个部分，一个是操作码一个是地址码，地址码可以是立即数或者寄存器编号，也可以有多个地址码。</strong></p><p><img src="https://s2.loli.net/2025/12/09/H8b9FdJBQZ5Onk3.png" alt="image-20251209143216922"></p><p><strong>但是我们会使用PC存放下一个指令地址</strong></p><p><img src="https://s2.loli.net/2025/12/09/H1gvw2dne76DOsN.png" alt="image-20251209142906162"></p><p><strong>有些操作不需要输出到某个寄存器</strong></p><p><img src="https://s2.loli.net/2025/12/09/TuLdPKcIsvZEb8e.png" alt="image-20251209143256102"></p><p><strong>部分操作是单目的。</strong></p><p><img src="https://s2.loli.net/2025/12/09/pTVN47uFxRrMCDj.png" alt="image-20251209143311733"></p><p><strong>随地址码位数扩大，那么能表示的地址范围就越来越大。</strong></p><h3 id="指令寻址"><a href="#指令寻址" class="headerlink" title="指令寻址"></a>指令寻址</h3><p>分为<strong>立即数寻址，直接寻址，间接寻址，相对寻址，基址寻址，变址寻址，隐含寻址</strong>。</p><p><strong>其中基址寻址，变址寻址，隐含寻址统称偏移寻址。</strong></p><p><strong>立即数寻址就是在指令中直接给出操作数本身。</strong></p><p><strong>直接寻址分为寄存器直接寻址和主存直接寻址。</strong></p><p><strong>寄存器直接寻址就是操作数存放在CPU寄存器中，主存直接寻址就是操作数放在主存中，指令给出寄存器编号或者主存的有效地址来寻找并调用操作数。</strong></p><blockquote><p><strong>因为寄存器数量远小于主存单元数，所以寄存器编号比主存短很多，所以寄存器寻址方式的指令比较短，访问也比主存更快。</strong></p></blockquote><p><strong>间接寻址也分为寄存器间接寻址和主存间接寻址。</strong></p><p><strong>寄存器间接寻址就是：指令中的地址码是一个寄存器编号，寄存器中存放的是操作数的有效地址，常用于指针。</strong></p><p><img src="https://s2.loli.net/2025/12/09/3lIyrOtL1ZKTVF6.png" alt="image-20251209144854536"></p><p><strong>主存间接寻址就是：在指令中给出的地址码是存放操作数有效地址的主存单元地址，用的比较少。</strong></p><p><img src="https://s2.loli.net/2025/12/09/1QSNf32ktWDFAPr.png" alt="image-20251209145031647"></p><p><strong>基址寻址：指令中给出形式地址R和偏移量offset</strong></p><p><strong>操作数的有效地址等于给出的形式地址R加上某个偏移量offset</strong></p><p><img src="https://s2.loli.net/2025/12/09/fd485eyTRPvsLpV.png" alt="image-20251209145309835"></p><p><strong>相对寻址：操作数有效地址等于PC寄存器的值加上给出的偏移值offset</strong></p><p><img src="https://s2.loli.net/2025/12/09/wq8kG9bS3pliHeU.png" alt="image-20251209145521340"></p><p><strong>变址寻址：</strong></p><p><strong>操作数地址等于一个基地址和一个动态可变偏移量相加。</strong></p><p><strong>可变偏移量往往是一个内容可改变的寄存器</strong></p><p><img src="https://s2.loli.net/2025/12/09/oFHG2a1TvrcMXY8.png" alt="image-20251209145947932"></p><p><strong>隐含寻址是一种特别的指令设计技巧，它的核心在于指令中并不直接写明某个操作数的地址，而是通过操作码或约定俗成的规则隐含地指定</strong></p><p><strong>为了让CPU知道寻址方式，指令往往是这种结构：</strong></p><p><img src="https://s2.loli.net/2025/12/09/yxq59cMszYOQgt7.png" alt="image-20251209150127712"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第五章：指令系统&quot;&gt;&lt;a href=&quot;#第五章：指令系统&quot; class=&quot;headerlink&quot; title=&quot;第五章：指令系统&quot;&gt;&lt;/a&gt;第五章：指令系统&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;记得在第一章写的层次结构吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img sr</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>第四章：数据的表示与运算</title>
    <link href="https://blog.willmo.top/posts/Chapter_4_Representation_and_operation_of_data/"/>
    <id>https://blog.willmo.top/posts/Chapter_4_Representation_and_operation_of_data/</id>
    <published>2026-01-25T04:18:52.219Z</published>
    <updated>2026-01-25T04:18:52.908Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第四章：数据的表示与运算"><a href="#第四章：数据的表示与运算" class="headerlink" title="第四章：数据的表示与运算"></a>第四章：数据的表示与运算</h1><p><strong>计算机的世界中，只有零和一两种表示。</strong></p><p><strong>将各种表示用0和1的二进制来表示的过程，就是编码。</strong></p><p><strong>广义上的定义是：</strong></p><p><strong>编码，就是用少量简单的基本符号对大量复杂多样的信息进行一定规则的组合</strong></p><p><strong>本章思维导图：</strong></p><p><img src="https://s2.loli.net/2025/11/25/mfSUFa49goRzvLJ.png" alt="image-20251125213805365"></p><p><strong>进制转换技巧</strong></p><p><strong>非数值数据的编码</strong></p><p><strong>无符号整数的编码</strong></p><blockquote><p><strong>八个二进制位那么表示的无符号范围就是0-255，因为255对应的是八位全一，在加一就是256但是这种情况下发生了溢出，所以只能取到255</strong></p></blockquote><p><strong>有符号整数的原码</strong></p><blockquote><p><strong>第一位就是符号位，后面都是数值位</strong></p></blockquote><p><strong>正数的原码反码补码都相同</strong></p><p><strong>负数的反码等于原码的符号位不变其余位取反，补码等于反码+1</strong></p><p><strong>补码（负数的补码）数值位取反+1就是原码</strong></p><p><strong>补码的存在有很强的进步性。</strong></p><p><img src="https://s2.loli.net/2025/11/26/SpARUgaxh9NOnu6.png" alt="image-20251126140928451"></p><p>如果我们要使用无符号数表示符号数，对于0-499，我们可以直接表示，但是对于-500-0，我们的方法是求其补数（<strong>补数是一个在特定计数系统（模运算）下，能够“互补”使得结果达到某个“上限”的数。</strong>），也就是用500-999来表示，这么做很有利于计算。</p><p><strong>比如，我们要计算176-253，那就是176+（-253），那–253对应747，那就是176+747就是923对应-77。我们也知道176-253就是-77，前后对得上，这就完成了计算。</strong></p><p><strong>补码中+0和-0是同一个表示</strong></p><p><strong>注意，如果说在二进制比较中1比0大，那么会出现****负数大于整数的情况</strong></p><p><strong>我们用移码来解决这个问题：</strong></p><p><img src="https://s2.loli.net/2025/11/26/ayhLqUlVvDYQ5iX.png" alt="image-20251126200129240"></p><p><strong>移码就是将数往某个方向移动一段距离从而统一符号。[X]移 = 2 ^ (n-1) + x，可表示的范围是-2^(n-1)到2^(n-1)-1,和补码的范围是一样的。</strong></p><h3 id="有符号数和无符号数的转换"><a href="#有符号数和无符号数的转换" class="headerlink" title="有符号数和无符号数的转换"></a>有符号数和无符号数的转换</h3><p><strong>无符号编码如果超过了正数范围转化后会变为负数。转换的C语音语句大概是</strong></p><p><code>unsigned short uv =(unsigned short)v; </code></p><h2 id="零扩展和位截断"><a href="#零扩展和位截断" class="headerlink" title="零扩展和位截断"></a>零扩展和位截断</h2><p><strong>如果将一个无符号数转换成一个更大的无符号类型的话，就需要执行零扩展（位扩展）。这就是扩大了数据的最大位数。</strong></p><p><strong>顺序是先进行零扩展在进行符号转换</strong></p><p><strong>反过来，如果需要将long转化为int或者short，就需要位截断</strong></p><h2 id="浮点数编码"><a href="#浮点数编码" class="headerlink" title="浮点数编码"></a>浮点数编码</h2><p><strong>浮点数编码是使用科学计数法的方式来表示的，根据需要分为符号位S，指数域E（标定数的范围），尾数域M（决定数的精度）。</strong></p><p>让我们以数字<code>-12.375</code>的单精度（32位）编码过程为例，来具体感受一下</p><p><strong>处理符号</strong>：这是一个负数，所以**符号位 <code>S</code>设为 <code>1</code>**。</p><p><strong>转换为二进制科学计数法</strong>： 先将 <code>12.375</code>转换为二进制。整数部分 <code>12</code>是 <code>1100</code>，小数部分 <code>0.375</code>是 <code>0.011</code>（因为 0.375=0.25+0.125=2−2+2−3）。 所以 <code>12.375</code>的二进制表示是 <code>1100.011</code>。</p><p><strong>将其规格化：</strong><code>1100.011 = 1.100011 × 2^3</code>。这里，**底数（尾数）是 <code>1.100011</code><strong>，</strong>实际指数是 <code>3</code>**。</p><p><strong>编码数</strong>：精度的偏移值是127。所以，存入指数域的值是 <code>3 + 127 = 130</code>。<code>130</code>的二进制是 <code>10000010</code>。所以**指数域 <code>E</code>为 <code>10000010</code>**。</p><p><strong>编码尾数</strong>：从规格化后的尾数 <code>1.100011</code>中，取出小数部分 <code>100011</code>，然后在其<strong>右边补0，直到凑满23位</strong>。所以**尾数域 <code>M</code>为 <code>100 0110 0000 0000 0000 0000</code>**。</p><p><strong>最终，</strong><code>-12.375</code>的单精度浮点数编码结果就是将这三部分拼接起来：**<code>1 10000010 10001100000000000000000</code>**（为了方便阅读，这里用空格分开了符号位、指数域和尾数域）。</p><p><img src="https://s2.loli.net/2025/11/27/i17eElwBnSLsgzA.png" alt="image-20251127105936336"></p><p><strong>标准化的原因是尾数部分我们尽可能不让0占用精度，会移动小数点位置，位数部分往左移叫做左规，右移叫右规。</strong></p><p><strong>规格化浮点数的标志是尾数部分的最高位是1，但在IEEE 754标准中，这个1是隐含的。</strong></p><h3 id="IEEE-754浮点数规范"><a href="#IEEE-754浮点数规范" class="headerlink" title="IEEE 754浮点数规范"></a>IEEE 754浮点数规范</h3><p><strong>该规范解决了不同计算机数据不互通需要额外转化数据的问题。</strong></p><p><img src="https://s2.loli.net/2025/11/27/Wvcqm3jw6D9o21H.png" alt="image-20251127112240360"></p><p><img src="https://s2.loli.net/2025/11/27/pHAduK6Gg2xRZTm.png" alt="image-20251127193931090"></p><p><img src="https://s2.loli.net/2025/11/27/KVwcEXHrtqv36Lj.png" alt="image-20251127195520234"></p><p><strong>其他区间不能使用浮点数表示，属于溢出数，根据溢出的不同分为负上溢，负下溢，正上溢，正下溢。</strong></p><blockquote><p><strong>数据一旦产生上溢，计算机必须中断运算操作，进行溢出处理。</strong> <strong>而数据下溢时，浮点数值趋于零，计算机将其当作机器零处理。</strong></p></blockquote><p><strong>非规格化数的特点：</strong></p><ul><li><strong>阶码为0</strong></li><li><strong>尾数不为零</strong></li><li><strong>指数是-126</strong></li><li><strong>隐藏位是0，位数变化范围是0.0000····1 ~0.111····1</strong></li></ul><p><strong>六十四位类似。</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第四章：数据的表示与运算&quot;&gt;&lt;a href=&quot;#第四章：数据的表示与运算&quot; class=&quot;headerlink&quot; title=&quot;第四章：数据的表示与运算&quot;&gt;&lt;/a&gt;第四章：数据的表示与运算&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;计算机的世界中，只有零和一两种表示。&lt;/st</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>第三章：主存储器</title>
    <link href="https://blog.willmo.top/posts/Chapter_3_Main_Memory/"/>
    <id>https://blog.willmo.top/posts/Chapter_3_Main_Memory/</id>
    <published>2026-01-25T04:13:07.204Z</published>
    <updated>2026-01-25T04:13:08.033Z</updated>
    
    <content type="html"><![CDATA[<h1 id="第三章：主存储器"><a href="#第三章：主存储器" class="headerlink" title="第三章：主存储器"></a>第三章：主存储器</h1><blockquote><p><strong>思维导图</strong></p></blockquote><p><img src="https://s2.loli.net/2025/11/21/f7iHGPU1R2rqdWI.png" alt="image-20251121200709266"></p><p><strong>数电内容在此略过</strong></p><p><strong>寄存器就是能够存储一定位数的二进制位的电子元件</strong></p><p><strong>机器字长:CPU一次能够处理的数据的位数，与CPU的寄存器位数有关</strong></p><p><img src="https://s2.loli.net/2025/11/21/BDoebIykMr1qTwn.png" alt="image-20251121203419264"></p><p><strong>bus是读口，RA和RB是输入要读的寄存器的编号，busW是写口，配合CLK时钟信号和WE使能端，RW写入寄存器地址，往寄存器内写入信息。</strong></p><h2 id="主存储器存储原理"><a href="#主存储器存储原理" class="headerlink" title="主存储器存储原理"></a>主存储器存储原理</h2><p><strong>一个字节等于8位二进制</strong>，这是计算机普遍适用的规律</p><p><strong>也就是1B = 8Bit</strong></p><p><strong>所以有 1024B = 1KB</strong></p><p><strong>1024KB = 1MB</strong></p><p><strong>在计算机内部是1024为进率</strong></p><p><strong>所以4GB = 4 * 1024 * 1024* 1024 /8B</strong></p><p><strong>= 2 ^ 29个存储字</strong></p><p><strong>平时下载速度不是波特率而是比特率。</strong></p><h3 id="ROM"><a href="#ROM" class="headerlink" title="ROM"></a>ROM</h3><blockquote><p><strong>只读，正常工作时只能读出数据，切断电源信息也不会消失，存储不经常改变的信息</strong></p></blockquote><p><img src="https://s2.loli.net/2025/11/21/LO6qcgJ5PReWCm2.png" alt="image-20251121204505080"></p><p><strong>掩膜只读寄存器（Mask ROM）</strong></p><p><strong>掩膜ROM由地址译码器和存储都阵列两部分组成，结构如下：</strong></p><p><img src="https://s2.loli.net/2025/11/21/KI98ynX1V7te5Ez.png" alt="image-20251121205143797"></p><p><strong>可以看到，先是一个3-8译码器直接连接CPU，并且四根位线与8根字字选择线相交，并且在某些交点用二极管链接，那么输入地址和数据输出就是以下关系：</strong></p><table><thead><tr><th><strong>A2</strong></th><th><strong>A1</strong></th><th><strong>A0</strong></th><th><strong>F0</strong></th><th><strong>F1</strong></th><th><strong>F2</strong></th><th><strong>F3</strong></th></tr></thead><tbody><tr><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td></tr><tr><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td></tr><tr><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>1</strong></td></tr><tr><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td></tr><tr><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>0</strong></td></tr><tr><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td></tr><tr><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td></tr><tr><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>1</strong></td><td><strong>0</strong></td><td><strong>0</strong></td></tr></tbody></table><p><strong>3根地址线，有2^3=8个地址，每个地址存</strong> <strong>放4位二进制，总容量8*4=32bit</strong></p><p><strong>一次性读出来4位二进制称作一个存储字（存储单元），存储字长就是4，</strong></p><p><strong>存储元：存储一个位的电路</strong></p><p><strong>如果有10根地址线，8根位线，那就是2^10 * 8 = 1K * 8 = 1KB</strong></p><p><strong>存储信息只读，这就是为什么使用二极管链接。</strong></p><h3 id="可编程ROM"><a href="#可编程ROM" class="headerlink" title="可编程ROM"></a>可编程ROM</h3><p><strong>可编程只读存储器（PROM）</strong></p><p><img src="https://s2.loli.net/2025/11/21/2EfpSO3lKcWazxL.png" alt="image-20251121210807957"></p><p><strong>存储元改为使用熔丝链接，熔丝只能允许一次数据交换，之后便会自行熔断，所以只能写入一次。</strong></p><p><strong>可擦除可编程只读存储器（EPROM）</strong></p><p><img src="https://s2.loli.net/2025/11/21/iX741OuhPScknBl.png" alt="image-20251121210949401"></p><p><strong>能够反复写入擦除读取，不过擦除需要专门工具，而且是MOS工艺，速度比较慢，而且一旦擦除整个数据都会消失</strong></p><p><strong>电擦除电改写只读存储器（EEPROM）</strong></p><p><img src="https://s2.loli.net/2025/11/21/fO73lIPeqBYnLmX.png" alt="image-20251121211137338"></p><p><strong>使用电来擦除</strong></p><p><strong>快闪只读存储器（FM）</strong></p><h3 id="RAM"><a href="#RAM" class="headerlink" title="RAM"></a>RAM</h3><blockquote><p><strong>随机存取存储器</strong></p></blockquote><p><strong>随机：按地址访问存储单元，因为每个地址译码时间是一样的，所以每个单元的访问时间是一个常数，与地址无关。</strong></p><p><img src="https://s2.loli.net/2025/11/23/MIlCZJPioXjvNzn.png" alt="image-20251123144024546"></p><p><strong>唯一需要运算的地方就是地址译码器。</strong></p><p><strong>这样的译码方式叫一维译码，只适用于小容量。</strong></p><p><strong>二维译码(重合法，双译码法)：使用两个译码器。</strong></p><p><img src="https://s2.loli.net/2025/11/23/JKZmYPkT7SLONg8.png" alt="image-20251123144606317"></p><p><strong>一个黑块就是一个存储单元，那么两个译码器就构成了64个存储单元，这样的结构称为存储阵列</strong>。</p><p><strong>但是如果XY</strong>(也就是行地址译码器和列地址译码器)分别只输入了一位，那意味着能够直接读取一行。</p><p><img src="https://s2.loli.net/2025/11/23/v58mtDBJLxciPeI.png" alt="image-20251123145359317"></p><h4 id="静态SRAM"><a href="#静态SRAM" class="headerlink" title="静态SRAM"></a>静态SRAM</h4><p><strong>SRAM存储元一开始保存的是0还是1是随机的。</strong></p><p><img src="https://s2.loli.net/2025/11/23/cWVkK6gdj4FhGuT.png" alt="image-20251123150450133"></p><p><strong>存储阵列示意图：</strong></p><p><img src="https://s2.loli.net/2025/11/23/9Rlt7Efu8v3SXMB.png" alt="image-20251123150652294"></p><p><strong>可以看到每个列地址使用了6个MOS管来构成双稳态触发器，访问速度非常快而且通电时数据就永久保持。</strong></p><p><strong>X地址选择5，Y选择3，那么Y=3的线连接两个MOS管导通，WE为低电平，将数据线传输的数据写到存储单元(5,3)中。</strong></p><h4 id="动态DRAM"><a href="#动态DRAM" class="headerlink" title="动态DRAM"></a>动态DRAM</h4><p><img src="https://s2.loli.net/2025/11/23/AlFELiKHSmZDnCj.png" alt="image-20251123151659551"></p><p><img src="https://s2.loli.net/2025/11/23/berNZ2A6hyv4HWR.png" alt="image-20251123152038530"></p><p><strong>可以看到每个列地址是一个MOS管加一个电容来连接。电容会漏电，数据会丢失，访问慢。</strong></p><p><strong>所以，读取数据的同时，会清空存储电荷的电容，导致原始数据被破坏。因此，必须在读取后立即执行一次“再生”操作，将数据重新写回。</strong></p><h3 id="存储器的存储周期"><a href="#存储器的存储周期" class="headerlink" title="存储器的存储周期"></a>存储器的存储周期</h3><p><strong>读出时间:指从存储器接收到读命令开始，到读取出信息所需时间</strong></p><p><strong>写入时间:指从存储器接收到写命令开始，至信息被写入存储器所需的时间</strong></p><p><strong>存储周期：进行一次操作需要的时间，也就是存\取时间+恢复时间。</strong></p><h3 id="DRAM刷新"><a href="#DRAM刷新" class="headerlink" title="DRAM刷新"></a>DRAM刷新</h3><p><strong>DRAM的电容会缓慢放电，放电过多会导致信息丢失，破坏性读取后也会直接清空电容电量，所以必须定时给电容充电，这个过程称为刷新或者读后再生。</strong></p><p><strong>刷新是以行为单位进行的，是由刷新计数器和刷新控制器来进行的</strong></p><p>刷新周期定义为：<strong>上次对整个存储器刷新结束时作为开始点，到下次对整个存储器全部刷新一遍为止的时间间隔。</strong></p><p><strong>和总线的仲裁类型差不多，这里有刷新也有刷新策略：</strong></p><ul><li><p><strong>集中刷新</strong></p><ul><li><strong>在规定的一个刷新周期内，对全部存储元集中一段时间进行逐行刷新</strong></li><li><strong>存储阵列式128*128，刷新周期是2ms，存储周期是0.5us，刷新单行的时间可以看作是存储（读取）单行的时间，那就是128 * 0.5us=64us，那么刷新周期为2ms，也就是2000us，我们就划分为前1936us来进行读写操作，后64us刷新，刷新时间为死时间，在这个时间内无法进行读写。</strong></li></ul><p><img src="https://s2.loli.net/2025/11/23/nIAoDMfO4s8hVCL.png" alt="image-20251123155833655"></p></li><li><p><strong>分散刷新</strong></p><ul><li><strong>对每行存储单元的刷新分散到每个存储周期内完成。将存储周期分为两段，前段存取，后半段刷新。</strong></li><li><strong>存储阵列式128*128，刷新周期是2ms，存储周期是0.5us，Tm是存取，Tr是刷新操作，Tc就是读取并刷新一行的时间。这样没有死时间但是会延长存储周期一倍或者更多。到点就强制刷新，这可能会干扰CPU正在进行的操作。</strong></li></ul><p><img src="https://s2.loli.net/2025/11/23/woJ7A4KBqZUxiyh.png" alt="image-20251123160507879"></p></li><li><p><strong>异步刷新</strong></p><ul><li><strong>前两种方式的结合，能够缩短死时间，又可以充分利用最大刷新间隔为2ms的特点。</strong></li><li><strong>存储阵列式128*128，刷新周期是2ms，存储周期是0.5us，将刷新周期平均分配到每一行上，那么刷新间隔是2ms/128行 = 15.6us，那么在存取一行后就会刷新一次。</strong></li></ul><p><img src="https://s2.loli.net/2025/11/23/sP89MuNlHexAco1.png" alt="image-20251123164942461"></p></li></ul><h3 id="DRAM和SRAM"><a href="#DRAM和SRAM" class="headerlink" title="DRAM和SRAM"></a>DRAM和SRAM</h3><p><strong>SRAM是“快而贵”的缓存，DRAM是“大而便宜”的主存</strong></p><table><thead><tr><th><strong>特性维度</strong></th><th><strong>SRAM（静态随机存储器）</strong></th><th><strong>DRAM（动态随机存储器）</strong></th></tr></thead><tbody><tr><td><strong>基本存储单元</strong></td><td><strong>6个晶体管（MOS管）构成一个****双稳态触发器</strong></td><td><strong>1个晶体管（MOS管） + 1个****电容</strong></td></tr><tr><td><strong>数据保持原理</strong></td><td><strong>只要通电，数据就****永久保持</strong>（静态）</td><td><strong>电容会漏电，数据****会丢失</strong>，需要定期<strong>刷新</strong>（动态）</td></tr><tr><td><strong>速度</strong></td><td><strong>非常快</strong>（访问周期约10ns级别）</td><td><strong>较慢</strong>（访问周期约50ns级别，含刷新开销）</td></tr><tr><td><strong>集成度/容量</strong></td><td><strong>低（一个单元要6个晶体管）</strong></td><td><strong>高（一个单元只需1个晶体管1个电容）</strong></td></tr><tr><td><strong>功耗</strong></td><td><strong>较高（持续通电）</strong></td><td><strong>较低（待机时功耗低，但刷新会增加总功耗）</strong></td></tr><tr><td><strong>成本</strong></td><td><strong>高</strong>（芯片面积大，制造复杂）</td><td><strong>低</strong>（芯片面积小，结构简单）</td></tr><tr><td><strong>主要应用</strong></td><td><strong>CPU高速缓存</strong>（L1, L2, L3 Cache）</td><td><strong>主存储器</strong>（内存条/运行内存）</td></tr><tr><td><strong>核心挑战</strong></td><td><strong>面积大，成本高</strong></td><td><strong>需要刷新电路</strong>，设计更复杂</td></tr></tbody></table><h3 id="主存如何与CPU连接"><a href="#主存如何与CPU连接" class="headerlink" title="主存如何与CPU连接"></a>主存如何与CPU连接</h3><p><strong>我们将之前讲过的逻辑电路看成一块集成电路IC，称作存储芯片</strong></p><p><img src="https://s2.loli.net/2025/11/23/nX3hrsaWFTMUOQR.png" alt="image-20251123165558704"></p><p><strong>单个存储芯片不可能做得很大，为我们需要扩展存储芯片的容量大小。</strong></p><p><img src="https://s2.loli.net/2025/11/23/Qe5yvgLnR2VwANE.png" alt="image-20251123165832393"></p><p><strong>CPU通过主存控制器</strong>与存储芯片链接。</p><p><img src="https://s2.loli.net/2025/11/23/eQpqL6cfrK4IaNW.png" alt="image-20251123192730840"></p><p><strong>现代计算机中，主存控制器已被集成到CPU中，所以有些图不会画出主存控制器。</strong></p><p><strong>m是MDR的位数，也就是存储字长，n是MAR的位数，2^n是存储字数量。</strong></p><p><strong>建议配合上图理解。</strong></p><p><img src="https://s2.loli.net/2025/11/23/YRuL5Oiqj63D8rW.png" alt="image-20251123193908944"></p><h3 id="主存的扩展"><a href="#主存的扩展" class="headerlink" title="主存的扩展"></a>主存的扩展</h3><p><strong>如果说一个主存是1K * 4Bit，说明存储字数量为1K，存储字长为4位，主存容量就是4096位，根据一个字节8位那就是512B（这里的B是字节Byte），那么也就是一共有1024个存储单元，每个存储单元能够存储4位二进制，芯片通过4根数据线换数据。</strong></p><ul><li><strong>位扩展：增大存储字</strong><ul><li><strong>我们希望有8根数据线进行是数据交换，字长就被扩张到8位</strong></li></ul></li></ul><p><img src="https://s2.loli.net/2025/11/24/7KdEso8rfPnl6xC.png" alt="image-20251124135041016"></p><p><strong>注意看，第一芯片链接D0-D3，第二芯片链接D4-D7，那就是一共有8个数据线来交换数据，这就增加了存储字长。</strong></p><ul><li><strong>字扩展：增加存储字数量</strong><ul><li><strong>我们通过加一个芯片，扩大地址数，也就是扩大存储字数量。</strong></li></ul></li></ul><p><img src="https://s2.loli.net/2025/11/24/kSBEbAxYytjIHCF.png" alt="image-20251124140100173"></p><p><strong>注意看，我们用两个芯片并联了地址信号并用一个非门将两芯片的片选信号连接起来，这就使得地址数扩大了一倍，那就是扩大了存储字数量。</strong></p><ul><li><strong>字,位扩展</strong><ul><li><strong>两个方法的结合，能够同时扩大存储字长和存储字数量。</strong></li></ul></li></ul><p><img src="https://s2.loli.net/2025/11/24/S6L8CQPYsAe71pq.png" alt="image-20251124140535880"></p><h2 id="CPU和主存链接的例题"><a href="#CPU和主存链接的例题" class="headerlink" title="CPU和主存链接的例题"></a>CPU和主存链接的例题</h2><p><img src="https://s2.loli.net/2025/11/24/yTmsIQjKADr1qMl.png" alt="image-20251124142411532"></p><p><a href="https://www.bilibili.com/video/BV1G29JYQE8b?spm_id_from=333.788.player.switch&amp;vd_source=cb9728ea927f0db8dc6b78c5ae60b004&amp;p=37">讲解视频</a></p><h3 id="同步主存储器SDRAM（DRAM）"><a href="#同步主存储器SDRAM（DRAM）" class="headerlink" title="同步主存储器SDRAM（DRAM）"></a>同步主存储器SDRAM（DRAM）</h3><p><strong>MFC异步通信很大程度拖慢了CPU时间。</strong></p><h4 id="行缓冲器"><a href="#行缓冲器" class="headerlink" title="行缓冲器"></a>行缓冲器</h4><p><strong>SDRAM的行缓冲器</strong>是集成在SDRAM芯片内部的一个<strong>高速静态存储器（SRAM）缓存</strong>。它的作用是：当CPU要访问SDRAM时，<strong>并不直接去庞大的存储单元阵列中读取数据，而是先把整个一行数据（通常几千位）一次性从慢速的DRAM阵列预取到这个高速的行缓冲器中</strong>，后续的读写操作都在这个缓冲器上完成，从而极大提升访问速度。</p><p><img src="https://s2.loli.net/2025/11/24/THjrhz9g3Gk6LNp.png" alt="image-20251124144736726"></p><p><strong>如果地址线输入01，那就是选中了第二行，行缓存器会提取所有第二行的数据并保存，</strong></p><p><strong>随即传入列地址00,就能够从缓冲器中读取第一列的八位数据。基于行缓冲器SRAM的高速率，这个过程是非常快的。</strong></p><h5 id="突发传输"><a href="#突发传输" class="headerlink" title="突发传输"></a>突发传输</h5><p><img src="https://s2.loli.net/2025/11/24/XeJ4HIuYKsDVPci.png" alt="image-20251124145154160"></p><p><strong>行缓冲器中有两个寄存器，一个是BL，保存突发长度，第二个是CL，CAS（列地址信号）潜伏期</strong></p><h5 id="DDR（Double-Data-Rate）-SDRAM"><a href="#DDR（Double-Data-Rate）-SDRAM" class="headerlink" title="DDR（Double Data Rate） SDRAM"></a>DDR（Double Data Rate） SDRAM</h5><p><img src="https://s2.loli.net/2025/11/24/pxwkXvnqRPM4u8z.png" alt="image-20251124150106165"></p><p><strong>DDR技术也在不断更新</strong></p><p><img src="https://s2.loli.net/2025/11/24/StTVLBfarmeyA5z.png" alt="image-20251124150137825"></p><h3 id="按字节寻址"><a href="#按字节寻址" class="headerlink" title="按字节寻址"></a>按字节寻址</h3><p><strong>温习一下主存结构：</strong></p><p><img src="https://s2.loli.net/2025/11/24/naKxmH8oZVMPvkY.png" alt="image-20251124152421868"></p><p><strong>八根数据线，那每次能够读写一个字节。</strong></p><p><strong>有32个地址线，使用二进制表示地址太长了，所以我们往往使用十六进制来表示，例如</strong><code>0x00000004</code>等。</p><p><strong>所以每个地址对应一个字节，根据地址来找到地址就是根据字节寻址。</strong></p><p><strong>如果每个地址存储的字节数 &lt;=1的话，那就只能使用按字节寻址。</strong></p><h3 id="按字寻址"><a href="#按字寻址" class="headerlink" title="按字寻址"></a>按字寻址</h3><p><strong>如果每个地址存放的字节数多比如32位，那因为一个字节等于八个二进制位（这是计算机的普遍标准），我们就可以定义四个字节为一个字，那么每个地址对应1个字，我们就能够通过查找字来确定要查找的数据的位置，也就是按字寻址。</strong></p><table><thead><tr><th><strong>特性</strong></th><th><strong>按字节寻址</strong></th><th><strong>按字寻址</strong></th></tr></thead><tbody><tr><td><strong>基本寻址单位</strong></td><td><strong>字节</strong></td><td><strong>字</strong></td></tr><tr><td><strong>地址对应数据量</strong></td><td><strong>1个字节</strong></td><td><strong>1个字（如32位系统下为4个字节）</strong></td></tr><tr><td><strong>灵活性</strong></td><td><strong>高</strong>，可精确访问任意字节</td><td><strong>低</strong>，以字为单位访问</td></tr><tr><td><strong>地址空间利用</strong></td><td><strong>地址线与字节地址对应</strong></td><td><strong>地址线与字地址对应，相同地址线可管理更大物理内存</strong></td></tr><tr><td><strong>常见应用</strong></td><td><strong>现代通用计算机（如x86架构）</strong></td><td><strong>某些早期或特定领域的硬件系统</strong></td></tr></tbody></table><p><strong>事实上按字寻址</strong>凭借<strong>相同地址线能够管理更大内存</strong>，是对早期计算机制造瓶颈的妥协。</p><p><strong>现在不存在技术瓶颈，使用字节寻址是现代计算机的绝对主流</strong>。</p><h3 id="高速缓存Cache"><a href="#高速缓存Cache" class="headerlink" title="高速缓存Cache"></a>高速缓存Cache</h3><p>时间局限性：<strong>被访问的某个存储单元带一个叫短时间间隔内可能又被访问</strong>，比如for循环等</p><p>空间局限性：<strong>被访问的某个存储单元的邻近单元在一个较短时间间隔内也可能被访问。比如顺序执行的指令，遍历数组等</strong></p><p><strong>而Cache的设计，就是****预测</strong>程序的行为会遵循局部性原理。一方面他会保存最近使用的数据等待再次访问，另一方面他会预取相邻的数据等待CPU访问。如果下一次CPU要访问的数据刚好在Cache中，那么就能利用Cache的高速访问来缩短时间，这样的情况我们称之为”<strong>命中（Hit）</strong>“。这样的机制能够使速度提升很多倍。</p><p><img src="https://s2.loli.net/2025/11/25/I1WFykcP5fMdQTZ.png" alt="image-20251125135735079"></p><p><strong>这个代码的作用是求和数组，函数中有for循环，这符合时间局部性，而且数组在内存中是顺序排列的，这就是符合空间局部性。</strong></p><p><img src="https://s2.loli.net/2025/11/25/dqBWOlh3a9fDVwM.png" alt="image-20251125140609316"></p><p><strong>注意看，C语言中，数组是按行存储的，程序一的按行访问，正好对应了顺序访问的局部性原则，程序二却是按列访问，这意味着访问完</strong><code>A[0][0]</code>之后会直接跳转到<code>A[0][2047]</code>这会大大降低Cache命中的概率，所以空间局部性很差。</p><h3 id="Cache基本工作原理"><a href="#Cache基本工作原理" class="headerlink" title="Cache基本工作原理"></a>Cache基本工作原理</h3><p><strong>Cache往往集成到CPU中，并于主存控制器，ALU等连接到一起，在CPU正式操作前，他会预取一部分的数据和指令保存，供CPU使用</strong></p><p><img src="https://s2.loli.net/2025/11/25/SjXLR7gotIrQHiq.png" alt="image-20251125144701973"></p><p><strong>总结一下</strong></p><p><img src="https://s2.loli.net/2025/11/25/WGOyM2dAvenHr9D.png" alt="image-20251125144947176"></p><p><strong>如果CPU访问主存单元所在的主存块在Cache中，则称为Cache命中(hit)。</strong> <strong>命中的概率称为命中率（hitrate），它等于命中次数与访问总次数之比。</strong> <strong>如果CPU访间主存单元所在的主存块不在Cache 中，则称为Cache 不命中（miss)。</strong> <strong>不命中的概率称为缺失率（missrate)，它等于不命中次数与访问总次数之比。</strong></p><h3 id="主存块的划分和相关地址的计算"><a href="#主存块的划分和相关地址的计算" class="headerlink" title="主存块的划分和相关地址的计算"></a>主存块的划分和相关地址的计算</h3><p><img src="https://s2.loli.net/2025/11/25/yQSV3GBad7LvNEO.png" alt="image-20251125145824600"></p><p><strong>Cache和主存块的三种映射方式是直接映射，组相联映射，全相联映射</strong>。</p><p><strong>抱歉，这三个映射方式我没看，也许有时间会补上。</strong></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;第三章：主存储器&quot;&gt;&lt;a href=&quot;#第三章：主存储器&quot; class=&quot;headerlink&quot; title=&quot;第三章：主存储器&quot;&gt;&lt;/a&gt;第三章：主存储器&lt;/h1&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;思维导图&lt;/strong&gt;&lt;/p&gt;
&lt;/block</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>第二章：总线结构</title>
    <link href="https://blog.willmo.top/posts/Chapter_2_Bus_Structure/"/>
    <id>https://blog.willmo.top/posts/Chapter_2_Bus_Structure/</id>
    <published>2026-01-25T04:09:15.658Z</published>
    <updated>2026-01-25T04:09:16.662Z</updated>
    
    <content type="html"><![CDATA[<h1 id="总线结构"><a href="#总线结构" class="headerlink" title="总线结构"></a>总线结构</h1><p><strong>总线</strong>是一组<strong>为多个部件服务的公共信息传输线路</strong>，是计算机内部各个功能部件（如CPU、内存、I/O设备）之间进行数据、地址和控制信息传输的<strong>共享通道</strong>。</p><p><strong>所以，总线是一个公用的通道，他需要一些通用属性：****带宽，仲裁逻辑，总线事务，总线定时，总线标准。</strong></p><p><img src="https://s2.loli.net/2026/01/25/Ux8gfDKJr6iM1bT.png" alt="image-20251118141640690"></p><p><strong>总线在同一时刻在发送相同信息会导致信息冲突，形成通信瓶颈。所以中大型计算机会使用多个总线。</strong></p><p><img src="https://s2.loli.net/2026/01/25/pX6cDu3l7x28Yih.png" alt="image-20251118141856685"></p><h1 id="通用属性"><a href="#通用属性" class="headerlink" title="通用属性"></a>通用属性</h1><h2 id="通信知识"><a href="#通信知识" class="headerlink" title="通信知识"></a>通信知识</h2><p><strong>通信的目的是为了传递数据，信号就是传输载体。</strong></p><p><strong>数据又分为连续数据和离散数据，在信号中，连续数据用模拟信号表示，离散数据用数字信号表示。</strong></p><p><strong>CPU内置一个发射器，能够将二进制码转化为电信号，让别的设备接受，但是这种信号不适用于远距离，需要调制为模拟型号</strong></p><p><img src="https://s2.loli.net/2026/01/25/W3a8Yr1CGtOFBcV.png" alt="image-20251118143043278"></p><p><strong>但是，读取的时间是需要控制的，这就由发射器和****接收器中的时钟</strong>完成，时钟会发出具有时钟周期的脉冲信号。接收方在接到信号后就知道在一个脉冲信号的周期截获并解析一个二进制码，这种方法称为<strong>外同步法</strong>。</p><p><img src="https://s2.loli.net/2026/01/25/TyWkPDMGRX6s7LK.png" alt="image-20251118143611473"></p><p><img src="https://s2.loli.net/2026/01/25/Of2eFSmYlazC4Au.png" alt="image-20251118144031603"></p><p><strong>自同步码后面会提到。</strong></p><p><strong>同理，总线也有一个总线控制器，里面有总线时钟来负责同步工作。</strong></p><h2 id="带宽"><a href="#带宽" class="headerlink" title="带宽"></a>带宽</h2><p><img src="https://s2.loli.net/2026/01/25/udzVSmayZ2whH4L.png" alt="image-20251118144940589"></p><p><strong>并行运输有缺点，30米以上的长度会导致不同信道的信号到达时间不同，会导致****数据错误</strong></p><p><strong>然而总线中有不同类型的支线，支线内部也是****并行传输</strong>的。</p><p><img src="https://s2.loli.net/2026/01/25/8cfn2VdaLPyRDXt.png" alt="image-20251118145035698"></p><p><strong>每一个总线有自己的时钟频率，****单位是赫兹hz</strong>，100MHz表示每秒传输100M次数据。</p><p><strong>所以总线带宽表示总线的最大数据传输率，即****单位时间没最多可以传输的数据量。</strong></p><p>$$<br>\text{总线带宽}=\text{总线宽度}\times\frac{1s}{\text{总线时钟周期}}=\text{总线宽度}\times\text{总线时钟频率}<br>$$</p><p><strong>总线时钟频率是每秒可以发送多少次数据，总线宽度就是每次你能够发送多少位数据。</strong></p><p>$$<br>\text{总线带宽}=32\mathrm{~bit~*~33MHz=(32/8)~B~*~33MHz=~132~MB/s}\quad(1\mathrm{M=10^{6})}<br>$$</p><p><strong>但是现在有的总线在一个总线时钟周期内能够发送2次甚至是4次数据，这样的技术名为DDR技术。</strong></p><p><strong>这时候，总线的工作频率就是总线时钟频率的2倍或者4倍。</strong></p><p><strong>这时：****总线带宽=总线宽度×总线工作频率=总线宽度×总线时钟频率×数据传输倍数</strong></p><h2 id="总线仲裁"><a href="#总线仲裁" class="headerlink" title="总线仲裁"></a>总线仲裁</h2><p><strong>主设备</strong>指的是在总线中发射信号的设备，<strong>具有总线控制权</strong>，<strong>从设备</strong>是在总线中接收信号的设备，<strong>没有总线控制权</strong>。</p><p><strong>比如，存储器总线中，CPU是主设备，主存是从设备。</strong></p><p><strong>主设备想控制从设备需要先向总线控制器发送信号，得到同意信号后才能拿到控制权，才能通过总线通信。如果多个主设备申请，总线需要仲裁需要确定同意哪些申请。</strong></p><p><strong>仲裁分为****集中式仲裁</strong>和分布式仲裁。</p><p><strong>集中式仲裁又分为****链式查询方式，计时器查询方式和独立请求方式。</strong></p><p><strong>链式查询方式:</strong></p><p><img src="https://s2.loli.net/2026/01/25/gDRMboc4lkZ7eCv.png" alt="image-20251118202635094"></p><p><strong>设备4和设备n发送使用申请，随即总线授权线会逐个检查设备是否发送过申请，如果发送过就同意申请。这样的方式使用几根线就能实现一定优先次序的总线仲裁，容易扩充设备。但是对电路敏感很故障，而且查询链的次序很固定。</strong></p><p><strong>计数器定时查询：</strong></p><p><img src="https://s2.loli.net/2026/01/25/dtLCFIw1uvkmQWf.png" alt="image-20251118203113491"></p><p><strong>总线控制器内置一个计数器，初始为1，如果设备2发送了总线请求，总线控制器会将1这个数值通过设备号线发送到每个设备（也就是广播），随即检查设备号与发送的数值相等的设备有没有发送过请求，如果有就同意，如果没有就让计数器自增然后向所有设备广播2,这个数值，检查到设备2并同意之后，总线忙，该阶段计数器不会变化。总线被设备2解除占用后，计数器恢复自增功能，变为3继续检查。</strong></p><p><strong>优点是优先级是相同的，计数器初始值可以通过程序设置，但是控制线数量很多，如果有15个设备，至少需要4位二进制，2^4 = 16。n台设备需要log2(n)条数据线</strong></p><p><strong>独立请求方式（使用较多）：</strong></p><p><img src="https://s2.loli.net/2026/01/25/pqTGKujH9NLyXcO.png" alt="image-20251118204629549"></p><p><strong>每个设备都有独立的申请线路，总线控制器内部有一个排队电路，先接收到申请就先通过，先来后到，忙完下一个。</strong></p><p><strong>优点是响应速度很快，次序控制灵活，但是控制线数量很多，行线复杂，n台设备需要2n条控制线</strong></p><h2 id="总线事务周期"><a href="#总线事务周期" class="headerlink" title="总线事务周期"></a>总线事务周期</h2><p><img src="https://s2.loli.net/2026/01/25/zI8sOVqpBH7rYXg.png" alt="image-20251119141554016"></p><p><strong>数据线是双向的，用于传输数据，地址线传输数据对应的地址，控制线传输控制信息，比如时钟信号等，控制线整体上是双向的。</strong></p><p><strong>总线周期就是完成一次总线操作的时间。</strong></p><p><strong>一个周期包括四个阶段。</strong></p><p><strong>1.申请分配阶段</strong></p><p><strong>主设备向总线控制器发送申请，总线仲裁器将总线控制权移交到主设备。</strong></p><p><strong>2.寻址阶段</strong></p><p><strong>主设备通过总线发送从设备地址，启动从设备</strong></p><p><strong>3.数据传输阶段</strong></p><p><strong>主设备与从设备进行数据交换</strong></p><p><strong>4.结束</strong></p><p><strong>主设备的信息从总线中撤除，让出总线控制权</strong></p><h2 id="总线定时"><a href="#总线定时" class="headerlink" title="总线定时"></a>总线定时</h2><p><strong>总线周期内有很多操作时间：****发送地址，发送命令，提供数据，撤销命令，撤销数据。</strong></p><p><strong>总线定时是为了解决这些操作事件在一个周期内的先后次序关系。</strong></p><p><strong>定时方式有好几种，分为同步定时和异步定时。</strong></p><h2 id="同步定时："><a href="#同步定时：" class="headerlink" title="同步定时："></a>同步定时：</h2><p><strong>时钟信号线会发送4个时钟信号</strong></p><p><strong>以主设备读取从设备数据为例</strong></p><p><strong>T1：主设备发送从设备地址</strong></p><p><strong>T2：主设备发送读取从设备的命令</strong></p><p><strong>T3：从设备提供数据</strong></p><p><strong>T4：主从设备撤销命令，撤销数据</strong></p><blockquote><p><strong>这里没有申请分配阶段</strong></p></blockquote><p><strong>这样的定时方式有很高的传输频率但属于强制性同步，如果有读写很慢的部件他会拖慢整个读写速度。</strong></p><p><strong>适用于总线长度短，各设备存取时间差不多的情况。</strong></p><h2 id="异步定时"><a href="#异步定时" class="headerlink" title="异步定时"></a>异步定时</h2><p><strong>异步定时允许总线上设备存储时间不一致</strong></p><p><img src="https://s2.loli.net/2025/11/20/HX6q3jdTLMQkcFv.png" alt="image-20251120005210471"></p><p><strong>数据交换的过程是</strong></p><p><strong>主设备将地址发送到地址线等地址信号稳定后，通过控制线发送读命令到从设备，从设备将数据放到数据线，等到数据线信号稳定后，从设备使用一个确认线告知主设备连接安全，数据交换结束后，从设备撤销数据线，主设备撤销地址线</strong></p><p><strong>可以看出：后一个操作事件出现在总线上的时刻取决于前一个操作事件出现的时刻</strong></p><p><strong>异步请求有三种</strong></p><ul><li><strong>异步定时（不互锁）</strong></li><li><strong>主设备向从设备发送请求，等待一段时间后，不管有没有应答，撤销请求信号，就执行其他任务，从设备接收到请求，将数据放到数据线上，将确认通知发送到主设备，等待一段时间，撤销通知信号，然后执行其他任务。</strong></li><li><strong>异步定时（半互锁）</strong></li><li><strong>主设备发送请求，等待从设备应答，从设备应答，从设备将数据放到数据线中，从设备等待一定事件后离开并执行其他任务</strong></li><li><strong>异步定时（全互锁）</strong></li><li><strong>主从设备发出的信息必须等待回复，否则不能撤销信号，这就是二者的互相锁定</strong></li></ul><p><img src="https://s2.loli.net/2025/11/20/1DaWOVjrZXAiuL6.png" alt="image-20251120005455811"></p><p><strong>CPU和主存同步和异步都可以，有些读写速度差别过大的只能用异步，用同步会严重拖慢CPU速度。</strong></p><h1 id="总线数据传输方式"><a href="#总线数据传输方式" class="headerlink" title="总线数据传输方式"></a>总线数据传输方式</h1><p><strong>分为串行和并行，串行就是一次只能传输一位二进制信息，并行就是一次能够传输多为二进制信息，这和同步异步一起可以理解为4种场景。</strong></p><p><strong>也就是：</strong></p><ul><li><strong>同步串行通信</strong></li><li><strong>异步串行通信</strong></li><li><strong>同步并行通信</strong></li><li><strong>异步并行通信</strong></li></ul><h3 id="同步串行通信"><a href="#同步串行通信" class="headerlink" title="同步串行通信"></a>同步串行通信</h3><p><img src="https://s2.loli.net/2025/11/20/9DFa6uYwvlHOEhA.png" alt="image-20251120193110587"><strong>外同步法</strong>：每个时钟周期发送一位二进制</p><p><strong>自同步法</strong>：在每个数据元的头加上同步字符来同步</p><h3 id="异步串行通信（这很重要）"><a href="#异步串行通信（这很重要）" class="headerlink" title="异步串行通信（这很重要）"></a>异步串行通信（这很重要）</h3><p><strong>现代大多数总线都是这种通信模式，这很重要。</strong></p><p><strong>不需要同步时钟，以字符为单位传输</strong></p><p><strong>字符结构示意图：</strong></p><p><img src="https://s2.loli.net/2025/11/20/Lf1QeXgWJFIiuMk.png" alt="image-20251120193629922"></p><p><strong>奇偶校验是为了检查传输是否有错误，分为奇校验和偶校验。</strong></p><p><strong>奇偶校验位是一个附加位，其值（0或1）使得包括校验位在内的整个数据单元中“1”的个数为奇数（奇校验）或偶数（偶校验）</strong>。</p><p><strong>比如奇校验数据1011001****1</strong>，加粗是校验位可以看到，为了使包括校验位在内的1的数量为奇数，校验位只能是1。如果传输中出错，某一个1变为0，那就统计的数量时不再是奇数。</p><blockquote><p><strong>但是如果出错两个，那就检查不出来，更别提纠正。</strong></p></blockquote><p><img src="https://s2.loli.net/2025/11/20/Z2KNXIzeq4Q5tVY.png" alt="image-20251120194726429"></p><p><strong>数据传输速率用波特率来衡量。</strong></p><p><strong>波特率：单位时间内传输的二进制数据位数，单位bps(位/秒)</strong></p><h2 id="突发传输"><a href="#突发传输" class="headerlink" title="突发传输"></a><strong>突发传输</strong></h2><p><img src="https://s2.loli.net/2025/11/20/ScQElq7rMJuVFts.png" alt="image-20251120200403727"></p><p><strong>如果是非突发传输：命令周期为T</strong></p><p><strong>T1：传起始地址</strong></p><p><strong>T2：传读命令</strong></p><p><strong>T3：读取4地址的数据</strong></p><p><strong>T4：撤销命令和地址</strong></p><p><strong>以后每一位数据都要这么干，要花4个周期。</strong></p><p><strong>如果是突发传输</strong></p><p><strong>T1：传地址</strong></p><p><strong>T2：传控制命令</strong></p><p><strong>T3：传一个数据</strong></p><p><strong>T4T5T6：传剩下三个数据</strong></p><p><strong>T7：撤销命令，结束传输</strong></p><p><strong>这么来就少了很多个周期，传输效率很高。</strong></p><h2 id="传统计算机的总线结构"><a href="#传统计算机的总线结构" class="headerlink" title="传统计算机的总线结构"></a>传统计算机的总线结构</h2><p><img src="https://s2.loli.net/2025/11/20/IhkDXuwfrsNvBGU.png" alt="image-20251120202945181"></p><p><img src="https://s2.loli.net/2025/11/20/FqGm6P3HXEwWgAL.png" alt="image-20251120203003896"></p><h2 id="现代计算机的总线结构"><a href="#现代计算机的总线结构" class="headerlink" title="现代计算机的总线结构"></a>现代计算机的总线结构</h2><p><strong>可以看到本来CPU之间是共享链接，在新的结构中是独立链接</strong></p><p><strong>PCle总线是一个串行的点对点总线，全双工。</strong></p><p><strong>PCIe的“全双工”是指****每个链路（Lane）由独立的发送和接收通道组成，支持同时双向传输</strong></p><p><img src="https://s2.loli.net/2025/11/20/Pbg1aiJus2FxhIr.png" alt="image-20251120204326662"></p><p><strong>总体结构</strong></p><p><img src="https://s2.loli.net/2025/11/20/89owjlUPDudyBZ6.png" alt="image-20251120204614104"></p><p><img src="https://s2.loli.net/2025/11/20/FufBCi6GaAYU314.png" alt="image-20251120204623170"></p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;总线结构&quot;&gt;&lt;a href=&quot;#总线结构&quot; class=&quot;headerlink&quot; title=&quot;总线结构&quot;&gt;&lt;/a&gt;总线结构&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;总线&lt;/strong&gt;是一组&lt;strong&gt;为多个部件服务的公共信息传输线路&lt;/strong&gt;，是计算机内部</summary>
      
    
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/categories/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
    
    <category term="计算机组成原理" scheme="https://blog.willmo.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BB%84%E6%88%90%E5%8E%9F%E7%90%86/"/>
    
  </entry>
  
</feed>
