├── 书籍 ├── 计算机原理专栏 │ ├── 浮点数和定点数(下):深入理解浮点数到底有什么用?.md │ ├── 程序装载:“640K内存”真的不够用么?.md │ ├── 浮点数和定点数(上):怎么用有限的Bit表示尽可能多的信息.md │ ├── 理解电路:从电报机到门电路,我们如何做到“千里传信”?.md │ ├── 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”.md │ ├── 给你一张知识地图,计算机组成原理应该这么学.md │ ├── 加法器:如何像搭乐高一样搭电路(上).md │ ├── 通过你的CPU主频,我们来谈谈“性能”究竟是什么?.md │ ├── 乘法器:如何像搭乐高一样搭电路(下).md │ ├── 穿越功耗墙,我们该从哪些方面提升“性能”?.md │ ├── 指令跳转:原来if...else就是goto.md │ ├── 动态链接:程序内部的“共享单车”.md │ ├── 计算机指令:让我们试试用纸带编程.md │ └── 冯.若依曼体系结构:计算机组成原理的金字塔.md └── hello算法 │ └── hello算法.md ├── 计算机网络 ├── 王道笔记 │ ├── 数据链路层 │ │ ├── 数据链路层的功能.md │ │ ├── 令牌传递协议(轮询访问).md │ │ ├── 组帧.md │ │ ├── VLAN(虚拟局域网技术).md │ │ ├── 信道划分介质访问控制.md │ │ ├── 检错和纠错编码.md │ │ ├── 局域网与IEEE 802.md │ │ ├── 随机访问介质访问控制.md │ │ └── 流量控制、可靠传输、滑动窗口机制.md │ ├── 网络层 │ │ ├── 地址解析协议(ARP).md │ │ ├── 网络层的功能.md │ │ ├── 网络地址转换(NAT).md │ │ ├── 路由聚合.md │ │ ├── 子网划分和子网掩码.md │ │ ├── ICMP协议.md │ │ ├── 网络层设备.md │ │ ├── 动态主机配置协议(DHCP).md │ │ ├── BGP协议(AS之间使用的协议)了解.md │ │ ├── 移动IP.md │ │ ├── IP地址.md │ │ ├── 无分类编址(CIDR).md │ │ ├── IP组播.md │ │ ├── IPv6.md │ │ ├── IPv4.md │ │ └── 路由算法和路由协议.md │ ├── 物理层 │ │ ├── 通信基础的基本概念.md │ │ ├── 物理层设备.md │ │ ├── 信道的极限容量.md │ │ ├── 传输介质.md │ │ └── 编码与调制.md │ ├── 传输层 │ │ ├── 传输层概述.md │ │ ├── UDP协议.md │ │ └── TCP协议.md │ ├── 应用层 │ │ ├── 应用层模型.md │ │ ├── 文件传输协议FTP.md │ │ ├── DNS(域名系统).md │ │ ├── 电子邮件.md │ │ └── 万维网和HTTP协议.md │ └── 计算机网络的概念 │ │ ├── 计算机网络的分类.md │ │ ├── 计算机网络分层结构.md │ │ ├── 计算机网络的组成和功能.md │ │ ├── OSI参考模型和TCP_IP模型.md │ │ ├── 三种交换方式.md │ │ └── 计算机网络的性能指标.md └── 小林coding │ ├── 初始计算机网络.md │ └── 将数据包发送到网络世界的流程.md ├── 数据结构 ├── 王道笔记 │ ├── README.md │ ├── string.md │ └── time-and-space-complexity.md ├── Ebook │ ├── LongestPalindrome.c │ ├── Palindrome.c │ ├── CharacterToInt.c │ ├── CharacterQuestions.c │ └── CharacterContains.c ├── 算法题目 │ ├── 2024.10.12.md │ └── 2024.10.14.md └── 数据结构实现和拓展 │ ├── LinkList.c │ └── Array.c ├── 计算机组成原理 ├── 王道笔记 │ └── 计算机性能指标.md └── Exercise │ └── TwoChapter.c └── README.md /书籍/计算机原理专栏/浮点数和定点数(下):深入理解浮点数到底有什么用?.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /书籍/hello算法/hello算法.md: -------------------------------------------------------------------------------- 1 | https://www.hello-algo.com/ -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/数据链路层的功能.md: -------------------------------------------------------------------------------- 1 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735717476365-8710df94-909a-4e1f-8dde-f5fd4ac9cea6.jpeg) 2 | 3 | 数据链路层使用物理层提供的“比特传输服务” 4 | 5 | 数据链路层为网络层提供服务,将网络层的 IP 数据报(分组)封装成帧传输给下一个相邻结点 6 | 7 | **物理链路:传输介质(第 0 层)+物理层(第一层)实现了相邻结点的“物理链路”** 8 | 9 | **逻辑链路:数据链路层需要基于“物理链路”,实现相邻结点之间逻辑上无差错的数据链路(逻辑链路)** 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/地址解析协议(ARP).md: -------------------------------------------------------------------------------- 1 | 就是用于查询同一个网络中 IP 和 MAC 地址之间的映射关系的。 2 | 3 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1739281885253-8233679d-9412-4cb5-b649-9e3a653d7893.jpeg) 4 | 5 | 会在 IP 数据报中标明自己的 IP 地址和目的的 IP 地址,然后封装为 MAC 帧(也会把自己的 MAC 地址放进去)时会在帧的目的地址设为全 1 表示广播。目的地址知道后会返回一个单播帧告诉它自己的 MAC 帧。目的地址主机也会将这个帧中的发送请求的主句的 IP 和 MAC 地址关系存储起来,以便下次使用。 6 | 7 | 这就是广播去找,单播回答。 8 | 9 | 注意:如果响应时的单播帧经过集线器转发,那么还是会进行广播,这个帧只要到达了交换机那么就只会发送给对应的主机了。 10 | 11 | ARP 的请求分组大小是 28B 12 | 13 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/网络层的功能.md: -------------------------------------------------------------------------------- 1 | # 网络层的地位 2 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738240145325-7f7bbddf-3abf-41b2-9c83-a3e875e8e7b6.png) 3 | 4 | 首先先回顾一下之前层次数据的名称 5 | 在数据传输中网络层为传输层提供服务,将传输层的数据封装为 ip 数据报。网络中的路由器根据 ip 数据报首部中的源 ip 地址和目的 ip 地址进行分组转发。因此网络层实现了“主机到主机”之间的传输。 6 | 7 | 数据链路层为网络层的 ip 数据报(分组)封装成帧,传输给下一个相邻结点 8 | 9 | 通俗来讲就是:“ip 层决定总体走向,mac 层实现具体的小步骤,一步一步的实现 ip 层规定的走向” 10 | 11 | # 网络层的功能概图 12 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738242851058-e305812c-2ccc-424a-b674-d0f6003e9e7c.jpeg) 13 | 14 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/令牌传递协议(轮询访问).md: -------------------------------------------------------------------------------- 1 | 核心特点:环形拓扑结构,各节点轮询访问信道,不会发生信道冲突。 2 | 3 | 在这个时期 CSMA/CD 被发明解决以太网(总线型)的信道争用问题,但在交换机(星型)出现后,因为彻底解决了争用问题所以就慢慢淘汰了。 4 | 5 | 6 | 7 | 在一个环形网络中的主机需要获得令牌才能发送数据,这个令牌就是令牌帧,在需要发送数据时需要将令牌帧转换为一个数据帧,当然其中包含令牌号、源地址、目的地址、数据、是否接受标识。将一个数据帧按照一个顺序进行转发,如果是目的地址则在检测之后将数据复制一份并将是否接受标识修改为 true,继续进行转发直到源地址,源主机知道数据正确接受后就将令牌帧释放了并生成一个新的令牌号为下一个主机标识的令牌帧并传递到下一个主机中。 8 | 9 | 10 | 11 | **MAU--令牌环网的集中控制站** 12 | 13 | MAU:多站接入单元,用于集中控制“令牌环网” 14 | 15 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1737715103976-3a42dfe4-b501-4291-9404-27681714f60b.png) 16 | 17 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1737721451934-3a5649ad-123d-4981-b7c1-0cd1d31ea7b8.jpeg) 18 | 19 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/网络地址转换(NAT).md: -------------------------------------------------------------------------------- 1 | 还是 IP 地址消耗的问题越来越严重,在提出 CIDR 后依然存在,所以又出现了 NAT 技术,在现如今的大部分局域网中都使用了这种技术。 2 | 3 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1739276917882-43e222d9-3f4c-40be-ab37-68c546910623.jpeg) 4 | 5 | # 端口号的概念 6 | 其实很简单,网络层实现了主机与主机之间的数据传输,但是主机上会有许许多多的进程,这些传输的数据需要知道是哪个进程需要撒,这个就是传输层需要做的事情,实现进程与进程之间的通信。每个主机对于进程的分配是独立的。传输层需要在 TCP(UDP)报文段的首部指明源端口号和目的端口号 7 | 8 | 9 | 10 | # NAT 的执行过程 11 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739280912319-03b1d2c2-583d-4e39-8ee5-de076a133c44.png) 12 | 13 | 这个的原理和上面的端口号类似。使用对应的原理。 14 | 15 | 并且这个路由器实现了 NAT 功能,也就是维护了一个数据结构。外网 IP + 端口号 对应着 内网 IP + 端口号 16 | 17 | 外网是全球唯一的,内网可以在不同局域网中复用。所以需要实现复用的功能就必须使用一个唯一的 IP 进行对应,故使用外网与内网进行对应,并且内网中的主机有不同的进程所以 NAT 数据结构中还需要有个端口号。 18 | 19 | 数据需要传输到外网中,所以在经过 NAT 路由器需要对 IP 和端口号进行替换,出去是替换源,进入是替换目的,替换都是根据 NAT 路由器维护的一个表结构。 20 | 21 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/路由聚合.md: -------------------------------------------------------------------------------- 1 | **路由聚合:对于一个路由转发表,如果几条路由表项的转发接口相同,部分网络前缀也相同,那么可以将这几条路由表项聚合为一条。这种地址的聚合称为路由聚合,也称为构成超网。** 2 | 3 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739020579231-7936f0be-94a8-48ce-9a16-303f4950fc4d.png) 4 | 5 | 比如上面的例子: 6 | 7 | 对于县里的路由转发表 1、2、3 的前 27 位网络前缀是一致的并且都是从 G1 发送帧,所以可以将这 3 个子网聚合为一个存储到路由表中。 8 | 9 | 好处: 10 | 11 | 使得路由转发表更小,更不占内存,查询的更快 12 | 13 | 坏处: 14 | 15 | 可能会引入一些额外的多余地址,但是这个是不会影响路由表的正常工作的 16 | 17 | # 最长前缀匹配原则 18 | IP 数据报在经过路由器的转发过程中可能有多个表项可以与之匹配,此时按照怎样的原则进行匹配呢? 19 | 20 | 答案就是按子网掩码的长度进行匹配,越长的越先。 21 | 22 | 23 | 24 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738937808135-9ad2a39c-0fc7-4eda-9589-3cd1fe437e38.jpeg) 25 | 26 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739275910976-79f3dc6d-718e-4871-ae7f-6f7da64afc5f.png) 27 | 28 | 在路由聚合的情况下如果有多个相同的表项就采用最长前缀原则 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/子网划分和子网掩码.md: -------------------------------------------------------------------------------- 1 | # 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738933117438-df5c98fd-d833-429f-a19a-ff4e9d3fd22d.jpeg) 3 | 4 | 在主机间进行通信时首先需要判断两者是否是处于同一个子网中,在传统的网络分类中可以简单的比较 IP 地址的前 n 位是否相等,而在可以进行子网划分的网络中就不能这样比较了,需要给不同子网中的主机分配子网掩码,这样进行判断的时候使用主机号和子网掩码作与运算得出来的结果才是实际的网络前缀,才能判断是否处于同一个子网中。 5 | 6 | # 流程 7 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738937808135-9ad2a39c-0fc7-4eda-9589-3cd1fe437e38.jpeg) 8 | 9 | 主机在发生帧之前先需要判断是否在同一个子网当中,若在,则不用将帧发送给默认路由,否则将帧发送给默认路由并将其传递给网络层,网络层让目的 IP 地址与路由表中的子网掩码依次与运算,如果算出来的结果与网络前缀对的起,就将该帧发送到对应的接口中,帧就发送到目的地址中了,途中可以会有多次路由器但原理都是这样的。还有一种情况就是目的 IP 地址所处的子网中还是采用的传统子网划分,看是 A、B、C 哪一类,采用的是默认子网掩码,有多少位网络号对应的掩码就是多少位 1。这样就可以使用同一种算法进行判断发送了。当然如果所有的子网掩码和目的网络号都无法匹配就轮到默认路由出马了,此时会匹配到子网掩码全为 0,目的网络号也全为 0,的接口,该帧就会从这个接口中传递出去。 10 | 11 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738977179425-58b92cc5-a5ba-4485-9320-c6792a378c4a.jpeg) 12 | 13 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/物理层/通信基础的基本概念.md: -------------------------------------------------------------------------------- 1 | ## 大致了解 2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735439287959-31a2d720-d657-4b22-a026-68e89790a10d.jpeg) 3 | 4 | ### 信源 5 | 信号的来源(数据的发送方) 6 | 7 | ### 信宿 8 | 信号的归宿(数据的接收方) 9 | 10 | ### 数据 11 | 信息的实体(图片、音频、文字等)在计算机中通常使用 0、1 表示 12 | 13 | ### 信号 14 | 数据的载体,用来表达传输数据的介质 15 | 16 | ### 信道 17 | 信号的通道,一条网线有传输和接收通道 18 | 19 | ### 数字信号 20 | 信号是离散的。信号值 y={1, -1},这里 1 代表 1,-1 代表 0 21 | 22 | ### 模拟信号 23 | 信号值是连续的。信号值 y∈[-1, 1],不同的表达式表示不同的值 24 | 25 | 在这个地方只有两种信号表达的方式,所以只能表示 0、1。如果可以有 4 种或者更多的表达方式那是不是就可以表示更多的数据量了。由此引出码元 26 | 27 | ### 码元 28 | 每一个信号就是一个码元。可以把信号周期称为码元宽度 29 | 30 | 比如:1V 代表 1,-1V 代表 0,此时就有两种码元,每次发出的信号不是 0 就是 1 嘛 31 | 32 | 如果一个码元有 4 中状态,那么称其为 4 进制码元(一个码元携带 2 个 bit)为什么成为 4 进制码元呢?因为它最多由 4 中状态,是 4 进制中的进位数 33 | 优点:每个信号周期可以传输更多的信息。也就是一个码元可以传输更多的信息 34 | 35 | 代价:需要加强信号功率,对信道的要求更高了 36 | 37 | ### 速率 38 | 传输速率不仅仅可以使用 bit/s(比特率) 表示还可以使用码元/s(波特率) 表示 39 | 40 | 如果是使用波特的话想要知道对应的 bit/s 就需要知道一个码元携带多少个 bit 了 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/组帧.md: -------------------------------------------------------------------------------- 1 | ## 总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735731489356-bb8630c7-575a-41aa-952f-57688920abd3.jpeg) 3 | 4 | ## 字符计数法 5 | 6 | ## 原理:在每个帧开头用一个定长计数字段表示帧长 7 | 8 | 缺点:只要有一个计数字段发生错误,以后的全部都会错误 9 | 10 | 举个例子:一个定长计数字段为一个字节,然后真正的数据有 6 个字节,那么这个计数字段就会用二进制表示出一个 7(字节)。因为这个定长字段长度也会算入帧长 11 | 12 | ## 字节填充法 13 | 14 | ## 约定特殊的字符,如:SOH(帧开始)、EOT(帧结束)、ESC(转义字符) 15 | 16 | 这样进行声明一个帧的开始结束。转义字符的作用是如果在帧的数据部分出现了代表特殊字符的数据格式(并不要表示为特殊字符)让其可以正常传输为数据而不是特殊字符。接收方会在识别到一个转义字符后不检查后面一个数据,这样就可以实现数据的正常传输了 17 | 18 | 其中这个转义字符是在发送方发现帧的数据部分存在特殊字符后添加上的 19 | 20 | ## 零比特填充法 21 | 22 | 使用“特殊比特串”(01111110)表示一个帧的开始和结束,但是这样的方法会有和字节填充法同样的问题,就是说在真正传输的数据中也包含了这个“特殊比特串”,解决方案很简单。就是只要遇到连续 5 个 1 的情况就在后面添加一个 0,然后接受方去掉连续 5 个 1 之后的一个 0 就可以了。看这样就只有“特殊比特串”里会有 6 个连续的 1,这个问题也就迎刃而解了。 23 | 24 | 在数据链路层中的 HDLC 和 PPP 协议就是使用的零比特填充法进行组帧的 25 | 26 | ## 违规编码法 27 | 28 | 看名字就知道这个是要通过“违规”进行帧的判定了 29 | 30 | 如:我们知道曼彻斯特编码是上跳 0 下跳 1,反正是要跳的。正因为这样的特点,我们就可以插入一些不跳的信号作为帧的开始和结束撒。数据链路层直接将帧数据传输给物理层,物理层在帧开始前插入一个不跳的信号,在帧结束后也插入一个不跳的信号,这样就实现了帧的界定了。 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/传输层/传输层概述.md: -------------------------------------------------------------------------------- 1 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740102497208-eab78bab-55ec-4d8e-a921-734f2c7a2f4c.jpeg) 2 | 3 | # 传输层的功能 4 | 1. 传输层提供进程到进程之间的逻辑通信 5 | 2. 复用和分用 6 | 3. 传输层对收到的报文进行差错检验 7 | 4. 传输层的两种协议(TCP、UDP) 8 | 9 | 复用与分用就是在一个发送主机上有两个进程都需要使用传输层的 TCP 协议进行处理数据(复用),接受的主机接受的数据需要分到对应的两个进程上(分用)。 10 | 11 | # TCP 和 UDP 的特点 12 | TCP: 13 | + 面向连接 14 | + 可靠 15 | + 时延大 16 | + 适用于大文件 17 | 18 | UDP: 19 | 20 | + 不可靠 21 | + 无连接 22 | + 时延小 23 | + 适用于小文件 24 | 25 | # 传输层的寻址与端口 26 | 端口是传输层的 SAP(服务访问点),是一个逻辑端口,是主机系统动态分配的。 27 | 28 | 端口号只有在本机中才有意义,任意两个主机中的端口号是没有任何联系的。 29 | 30 | 端口号长度为 16bit,可以表示 65536 个端口。 31 | 32 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740104464873-78c71ed5-0619-4248-b957-bd01e2f132d6.jpeg) 33 | 34 | # 常用的应用程序与对应端口号 35 | | 应用程序 | FTP | TELNET | SMTP | DNS | TFTP | HTTP | SNMP | 36 | | --- | --- | --- | --- | --- | --- | --- | --- | 37 | | 端口号 | 21 | 23 | 25 | 53 | 69 | 80 | 161 | 38 | 39 | 40 | 在网络中采用发送方和接受方的套接字组合来识别端点。 41 | 42 | 套接字唯一标识了网络中的一个主机和它上面的一个进程。 43 | 44 | Socket = (主机地址 + 端口号),所以可以唯一标识 45 | 46 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/ICMP协议.md: -------------------------------------------------------------------------------- 1 | 在数据传输的过程中难免会出错,网络层发现帧出错后会直接丢弃,然后还会给发送的主机返回一个 ICMP 报文告诉它出错了。 2 | 3 | 4 | 5 | 组成:虽然是将 ICMP 报文放入 IP 数据报中但是其仍是网络层的协议 6 | 7 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739450559376-535f1849-d591-4fbc-a8fb-b4010dab29a5.png) 8 | 9 | 10 | 11 | # ICMP 差错报告报文(5 种) 12 | ## 1、终点不可达 13 | 当路由器或主机不能交付数据报时就向源节点发送终点不可达报文 14 | 15 | ## 2、源点抑制 16 | 如果路由器或者主机太拥塞就会给源节点返回源点抑制报文 17 | 18 | ## 3、时间超过 19 | 当路由器收到生存时间 TTL=0 的数据报时,除丢弃该数据报外,还要向源点发送时间超过报文。当终点在预先规定的时间内不能收到一个数据报的全部数据报文片时,就把已收到的数据报文片全部丢弃,并返回这个报文。 20 | 21 | ## 4、参数问题 22 | 数据报的首部中的字段值有不正确时,就丢弃该数据报,并向源点发送参数问题报文。 23 | 24 | ## 5、改变路由(重定向) 25 | 路由发现自己不是最佳发送的会给源节点发送改变路由报文,让源节点换一个路由 26 | 27 | # 不发送 ICMP 差错报文的情况 28 | ## 1、对 ICMP 差错报告报文不再发送 ICMP 差错报告报文 29 | ## 2、对第一个分片的数据报文的所有后续数据报片都不发送 ICMP 差错报告报文 30 | ## 3、对具有组播地址的数据报都不发送 ICMP 差错报告报文 31 | ## 4、对具有特殊地址(如 127.0.0.0 或 0.0.0.0)的数据报不发送 ICMP 差错报告报文 32 | 33 | 34 | # ICMP 询问报文 35 | ## 1、回送请求和回答报文 36 | 主机或路由器向特定目的主机发出的询问,收到此报文的主机必须给源主机或路由器发送 ICMP 回答此报文。 37 | 38 | 测试目的站是否可达以及了解其相关状态 39 | 40 | ## 2、时间戳请求和回答报文 41 | 请某个主机或路由器回答当前的时间和日期。 42 | 43 | 用于进行时钟同步和测量时间 44 | 45 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/网络层设备.md: -------------------------------------------------------------------------------- 1 | # 隔离冲突域和广播域 2 | 第二层设备(交换机、网桥)可以隔离冲突域 3 | 4 | 第三层设备(路由器)可以隔离广播域 5 | 6 | # 路由器的组成和功能 7 | 当源主机向目的主机发送数据报时,路由器会判断这两个主机是不是在同一个网络中的,如果在的话,就不需要通过路由器转发,直接交付就行了。如果不在就需要路由器通过路由转发表(这个是通过路由表得出的,路由表是通过静态分配或者 IGP 得出的)进行转发了。 8 | 9 | 路由器可以连接协议不相同的两个网络,所以说是隔离了广播域。 10 | 11 | ## 从结构上看 12 | 路由器是由路由选择和分组转发两部分构成的 13 | 路由选择:这个又称为控制部分,核心构件是路由选择处理机,其任务是根据选定的路由协议构造出路由表,同时经常或定期地和相邻路由器交换路由信息而不断更新和维护路由表 14 | 15 | 分组转发:由三部分组成: 16 | 17 | + 交换结构 18 | + 一组输入端口 19 | + 一组输出端口 20 | 21 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740054682290-8a3ab6bb-c686-4e26-840c-7e7d47f2bd1b.jpeg) 22 | 23 | ## 从模型上看 24 | 是一个网络层的设备,它实现了下三层的功能。 25 | 26 | 27 | 28 | 路由器端口在网络层的处理模块中都设有一个缓冲队列,用来暂存等待处理或已处理完毕待发送的分组,还可以用来进行必要的差错检测。如果分组处理的速率赶不上分组进入队列的速率,就会将没有进入缓冲区(满了)的分组丢弃,所以说路由器中的输入或输出队列产生溢出是造成分组丢失的重要原因。 29 | 30 | # 路由表与分组转发 31 | 路由表是通过路由选择算法得出的,主要用途是路由选择。 32 | 33 | 路由表的四个项目:目的网络 IP 地址、子网掩码、下一跳 IP 地址、接口(用于发送) 34 | 35 | 转发表是通过路由表得出来的,转发表的表项与路由表表项有着直接的对应关系,但是格式有所不同。 36 | 37 | 转发表中有一个目的网路和下一跳地址(MAC) 38 | 39 | 路由表总是通过软件实现的,转发表可以用软件实现,也可以使用特殊的硬件实现。 40 | 41 | # 路由表与转发表的不同 42 | 路由表是由复杂的算法通过很多路由器得出的,而转发表仅涉及一个路由器。 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/传输层/UDP协议.md: -------------------------------------------------------------------------------- 1 | # 用户数据报协议 UDP 概述 2 | UDP 只在 IP 数据报服务之上增加了很少的功能,即复用分用和差错检测功能。 3 | 4 | UDP 的主要特点: 5 | 6 | + UDP 是无连接的,减少开销和发送数据之前的时延 7 | + UDP 使用最大努力交付,即不保证可靠交付 8 | + UDP 是面向报文的,适合一次性传输少量数据的网络应用 9 | + UDP 无拥塞控制,适合很多实时应用 10 | + 首部开销小,只有 8B 而 TCP 是 20B 11 | 12 | 面向报文怎样理解呢,就是说应用层给的报文,在 UDP 这里直接发送完整的报文不会进行分段。所以不应该让 UDP 协议传输大的报文,因为这不仅会增加出错的可能性还会增加网络层的负担。 13 | 14 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740105938167-ae3a979e-8b17-44f0-9641-5945e8cb9b3d.png) 15 | 16 | # UDP 首部格式 17 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740106225399-adf82b31-582b-4613-b614-63632f4dc6d9.png) 18 | 19 | 数据部分是可有可无的,所以 UDP 报文最小可以为 8B。在分用时,如果找不到对应的目的端口号,就会丢弃该报文,并向源主机发送一个 ICMP 报文(端口不可达)。 20 | 21 | # UDP 校验 22 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740107978303-b627b2d4-fc9c-426b-a250-0931e5cf9168.png) 23 | 24 | 在计算检验字段之前需要先添加一个伪首部。这个伪首部不会向上和向下传递。这个伪首部这是用来帮助我们计算校验和的。类型字段使用 17 表示 UDP 协议。 25 | 26 | 怎样进行计算呢? 27 | 28 | 将伪首部和 UDP 数据报排在一起形成一个更大的数据,然后将其分为 2B 的小部分数据,如果这个大的数据包含的字节数是奇数,那么就需要在最后添加一个全为 0 的字节构成一个大的偶数字节的数据块。 29 | 30 | 将上面所有的 2B 数据进行相加,然后如果最高位需要进位就在结果的基础上+1(这个也称为反卷) 31 | 32 | 最后得出的结果取反码就是最后的校验和了。 33 | 34 | 这个算法是比较简单的,但是速度很快,所以适用于 UDP。 35 | 36 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/应用层/应用层模型.md: -------------------------------------------------------------------------------- 1 | # 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740276477499-fbca87d2-d19e-4520-8e20-31ae6d5ae5ff.jpeg) 3 | 4 | 应用层对应用程序的通信提供服务 5 | 6 | # 应用层协议定义 7 | 应用进程交换的报文类型,请求还是响应? 8 | 9 | 各种报文类型的语法,如报文中的各个字段及其详细描述 10 | 11 | 字段的语义,即包含在字段中信息的含义 12 | 13 | 进程何时、如何发送报文,以及对报文进行响应的规则 14 | 15 | # 应用层的功能 16 | | 应用层的功能 | 应用层重要的协议 | 17 | | --- | --- | 18 | | 文件传输、访问和管理 | FTP | 19 | | 电子邮件 | SMTP、POP3 | 20 | | 虚拟终端 | HTTP | 21 | | 查询服务和远程作业 | DNS | 22 | 23 | 24 | # 网络应用模型 25 | ## C/S 模型 26 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740277112815-d70e5036-f0a5-426a-9ce1-8ac7780ba53e.png) 27 | 28 | 特点: 29 | 30 | 服务器:提供计算服务的设备 31 | 32 | + 永久提供服务 33 | + 永久性访问地址 34 | + 网络中的各计算机地位不平等,服务器可以通过对用户权限的限制来达到管理客户机的目的 35 | + 可扩展性不好。受到服务器硬件和网络带宽的限制,服务器支持的客户机数量有限 36 | 37 | 客户机:请求计算服务的主机 38 | 39 | + 与服务器通信,使用服务器提供的服务 40 | + 间歇性接入网络 41 | + 可能使用动态 IP 地址 42 | + 不与其他客户机直接通信 43 | 44 | ## P2P 模型 45 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740277341575-db5487e2-68c8-4994-94e1-f7a5aae26fb7.png) 46 | 47 | 特点: 48 | 49 | + 不存在永远在线的服务器 50 | + 每个主机既可以提供服务,也可以请求服务 51 | + 任意端系统/节点之间可以直接通讯 52 | + 节点间歇性接入网络 53 | + 节点可能改变 IP 地址 54 | + 可扩展性强 55 | + 网络健壮性好 56 | 57 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/VLAN(虚拟局域网技术).md: -------------------------------------------------------------------------------- 1 | # 引入面临的问题 2 | 在一个局域网中(比如校园网),因为节点还是比较多的,所以可能会有许多节点发送广播帧,这些广播帧会被发送到每个节点,这样很可能会引发广播风暴。 3 | 4 | 而且在局域网中可能会存在高敏感的结点(如数据库、服务器),会导致不安全。 5 | 6 | # 虚拟局域网 7 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738208344606-abf01f25-70e8-42a9-aefc-2acfd238b894.png) 8 | 9 | 可以将一个大型的局域网划分为多个虚拟局域网(VLAN),每个 VLAN 是一个广播域 10 | 11 | 每个 VLAN 对应一个 VID,就是 VLAN-10 后面的 10 12 | 13 | 但是这个需要一个支持 VLAN 技术的交换机才行 14 | 15 | ## VLAN 划分方式 16 | ### 1、按接口划分 17 | 假设一个两个支持 VLAN 的交换机有 1~10 接口,管理员可以将偶数划为 VLAN-1,奇数划为 VLAN-2 18 | 19 | ### 2、按 mac 地址划分 20 | 就是将不同的 mac 地址绑定到 VLAN 中 21 | 22 | ### 3.按 ip 地址划分 23 | 这个可以跨多个局域网,将不同局域网中的结点划分到同一个 VLAN 中,这个需要网络层的支持所以这个交换机需要实现部分网络层的功能,使得其会更加复杂 24 | 25 | # 怎样识别在一个 VLAN 中不同交换机中的传输的主机有哪些 26 | 在两个交换机之下会有多个节点,可能被分到不同的 VLAN 中,一个数据帧在跨越交换机时怎样可以让交换机知道可以传输到的主机有哪些呢? 27 | 28 | 这里就需要理解一个地方了,在主机与交换机之间是以标准以太网帧,交换机和交换机之间是使用 802.1Q 帧的。 29 | 30 | 然后这个 802.1Q 帧是会包括 VID 的,这样就知道了要传给哪个节点了。 31 | 32 | ## 802.1Q 帧的结构 33 | 与标准的以太网帧相比就是对了一个 4 字节的 VLAN 标签,在源地址后 34 | 35 | 6 6 4 2 N 4 这样的结构 36 | 37 | # 总结 38 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738239904612-5c2d9162-aa57-4066-8d90-7a3bc86aa665.jpeg) 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/物理层/物理层设备.md: -------------------------------------------------------------------------------- 1 | ## 中继器 2 | 这个是第一层设备。在信号的传输过程中必然会失真,如果距离太远可能信号的差距相差十万八千里,所有在传输途中可以安设一些中继器将没有失真太过分的信号整形一下再发送,这样到达目的的信号就不会失真太严重。 3 | 4 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735699767173-c9656335-a12b-4b45-8f9e-c4dc8592eba5.jpeg) 5 | 6 | ## 集线器 7 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735700571170-609917b6-99aa-4c57-b82c-235c82e67f78.jpeg) 8 | 9 | 冲突域:如果两台主机同时发送数据会导致冲突,则这两台主机就处于同一个冲突域中。处于同一个冲突域中的主机在发送数据前需要进行**信道争用** 10 | 11 | 只要使用集线器连接的主机就会处于同一冲突域中,也就是说集线器不能隔离冲突域 12 | 13 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735701151823-4e522660-43de-4f06-98ec-61376661a36e.png) 14 | 答案:C 15 | 16 | 以太网交换机可以隔离冲突域(如果将上面的以太网交换机换为集线器则就只有 2 个冲突域了,但是是交换机的话就有 4 个,这个就叫隔离,不会让其合在一起) 17 | 18 | ## 一些其他特性(用于了解就行,不必过于深究) 19 | 1、 集线器和中继器是不能无限串联的。如:10Base5 的 “543 原则” 20 | 21 | 使用集线器和中继器连接 10Base5 网段时,最多只能串联 5 个网段,使用 4 台集线器(或中继器),只有三个网段可以挂在计算机 22 | 23 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735702619014-8885143a-c90d-4b05-9de6-66a0c33a6c9e.png) 24 | 25 | 2、集线器连接的各网端是共享带宽的 26 | 27 | 举个例子:带宽为 10Mbps 的集线器连接 8 台主机的话,每个主机平均就只有 1.25Mbps 的带宽了 28 | 29 | 3、集线器是可以连接不同的传输介质的,因此两网段的物理层接口特性可以不同(就是说两网段使用的物理层协议可以不同)。若集线器连接了不同速率的网段,所有的网段速率向下兼容,就是向速率低的看齐。 30 | 31 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/动态主机配置协议(DHCP).md: -------------------------------------------------------------------------------- 1 | # 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1739411088293-35c1276f-661e-4816-8b6a-7d53fe369342.jpeg) 3 | 4 | # 各层协议的关系 5 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739369813025-7253f834-99ab-4a41-b255-be020ad99076.png) 6 | 7 | 这个 DHCP 协议是应用层的协议但是这里放到网络层来学习是因为这个是为网络层提供参数服务的。 8 | 9 | 并且会使用到传输层的 UDP 协议和网络层的 IP 协议,最后封装为 MAC 帧。 10 | 11 | 12 | 13 | 1、 刚刚开始的时候新的主机没有自己的 IP 地址也不知道 DHCP 服务器的 IP 地址所以需要将目的 IP 地址设为广播地址,源 IP 地址为全 0,目的 MAC 地址全 1(广播帧),源 MAC 地址为自己的,这样才能将帧发送到 DHCP 服务器上。这就封装完第一个步骤需要的帧。 14 | 这个是一个广播帧意思是所有在这个局域网中的主机都会进行接收,那么处理 DHCP 服务器之外的主机是怎样处理的呢,其他主机在接收到这个广播帧后会一层一层的拆分,到了传输层数据时发现目的端口是 67,该主机就知道不是自己的了,因为这种专门的服务使用的端口是确定,其余的进程是不能分配的。 15 | 16 | 2、DHCP 服务器接收到了请求的报文后会进行返回,因为可以从请求的报文中得到客户主机的 MAC 地址,所以在 MAC 帧那里可以是有确定的目的 MAC 地址的,但是 IP 分组那里就没有确定的 IP 地址而是使用的广播帧,最重要的就是应用层的 DHCP 提供报文,给客户主机返回了租用 IP 地址、子网掩码、默认网关、租用时间等数据。 17 | 18 | 3、客户主机会向局域网中进行广播(不管是 IP 层还是 MAC 层)说明自己选择的 IP 地址是什么。 19 | 这里选择广播的原因是因为:在一个局域网中可能不止有一个 DHCP 服务器,所以在第一步后可能有多个 DHCP 服务器为其分配 IP,所以返回确认时需要进行广播告诉所有的 DHCP 服务器自己选择的 IP 是什么。 20 | 21 | 此时的客户还是没有 IP 的状态。 22 | 23 | 4、DHCP 服务器再进行一次 IP 层的广播,MAC 的单播(目的 MAC 地址为客户的 MAC 地址)还有 DHCP 确认报文,也就是之前的提供报文的内容。 24 | 25 | 26 | 27 | 经过上面的 4 个步骤就可以给客户主机分配一个 IP 地址了。 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/BGP协议(AS之间使用的协议)了解.md: -------------------------------------------------------------------------------- 1 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739972354542-4d4274dc-be21-4a42-96fc-9882d89beed2.png) 2 | 3 | 在 BGP 协议中每个 AS 会确定一个 BGP 发言人,通常来说就是边界路由器。 4 | 5 | BGP 发言人会与其相邻的 BGP 发言人交换信息; 6 | 7 | 交换的是网络可达性的信息,就是要到达某个网络所要经历的一系列 AS 8 | 9 | 在发生变化时更新发生变化的部分 10 | 11 | # BGP 交换信息的过程 12 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739973080512-1b8d67eb-ac10-4362-82f2-5559523cb6b4.png) 13 | 14 | BGP 发言人交换路径向量。 15 | 16 | 主干网还可以发出通知:要到达 N1、N2、N3、N4 可以经过 AS1 、AS2(也就是上面说的“一系列 AS”) 17 | 18 | # BGP 协议报文格式 19 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740013225798-60c4f85c-b49a-4ef1-8c72-9b51ed1239b1.png) 20 | 21 | 通过建立 TCP 连接以达到交换 BGP 报文建立 BGP 会话,然后通过 BGP 会话进行路由信息交换。 22 | 23 | 故 BGP 是应用层协议 24 | 25 | # BGP 的特点 26 | + 支持 CIDR,所以 BGP 路由器应该包含目的网络前缀、下一跳路由器地址,和要到达该网络的 AS 序列 27 | + 在 BGP 刚开始运行时,BGP 邻站交换的是整个 BGP 路由表,但在以后只需要在发生变化时更新变化的部分则可。 28 | 29 | # BGP 的四种报文 30 | 1. OPEN 报文:用来于相邻的另一个 BGP 发言人建立关系,并认证发送方 31 | 2. UPDATE 报文:通告新路径或撤销原路径 32 | 3. KEEPALIVE 报文:在没有 UPDATE 报文时,周期性证实邻站的连通性;也作为 OPEN 报文的确认 33 | 4. NOTIFICATION 报文:报告先前报文的差错;也被用于关闭连接 34 | 35 | # 三种协议的比较 36 | 直接上图 37 | 38 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740014187809-1a3de0c2-c6d4-481b-a907-36df4bdb65ea.png) 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/应用层/文件传输协议FTP.md: -------------------------------------------------------------------------------- 1 | # FTP 的工作原理 2 | FTP 是因特网上使用得最广泛的文件传输协议。FTP 是提供交互式的访问,允许客户指明文件的类型和格式,并允许文件具有存取权限。它屏蔽了各种操作系统之间的不同,因此适用于异构网络中的任意计算机之间的传输文件。 3 | 4 | FTP 提供以下功能: 5 | 6 | + 提供不同种类主机系统 (硬、软件体系等都可以不同)之间的文件传输能力 7 | + 以用户权限管理的方式提供用户对远程 FTP 服务器上的文件管理能力 8 | + 以匿名 FTP 的方式提供公用文件共享能力 9 | 10 | 其是采用的 C/S 工作方式,一个 FTP 进程可以同时为多个客户进程提供服务。并且 FTP 的服务器进程是由两大部分组成的。 11 | 一是:一个主进程,负责接收新的请求 12 | 13 | 二是:若干从属进程,负责处理单个请求 14 | 15 | FTP 服务器必须在整个会话期间保留用户的状态信息 16 | 17 | # 控制连接与数据连接 18 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740314016032-adaeec38-8dbe-4bb2-b336-420f01c93c74.jpeg?x-oss-process=image/auto-orient,1) 19 | 20 | FTP 在工作时使用两个并行的 TCP 连接:一个是控制连接(服务器端口号 21),一个数据连接(服务器端口号 20)。使用两个不同的端口号可以使协议更容易实现。 21 | 22 | ## 控制连接 23 | 服务器监听 21 号端口,等待客户进行连接,建立在这个端口上的连接称为控制连接,用来传输控制信息,不用来传输文件。因为在传输文件时还可以使用控制连接,所以在整个连接的会话期间要一直保持着打开状态。 24 | 25 | ## 数据连接 26 | 服务器端的控制进程在接收到 FTP 客户发来的文件传输请求后,就创建“数据传送进程”和“数据连接”。“数据连接”用来连接客户端和服务端的数据传送进程,数据传送进程实际完成文件的传送,在传送完成后关闭“数据传送连接”。 27 | 28 | 数据传送连接有两种传输模式:主动模式(PORT)和被动模式(PASV) 29 | 30 | 采用哪一种传输模式是由客户端决定。简单的来说,PORT 就是传输数据时服务器连接到客户端的端口,PASV 是相反的。 31 | 32 | 在使用 FTP 修改服务器上的文件时,需要先将服务器上的文件全部传送本机,然后在本机修改,最后将修改后的文件副本发送到服务器上,来回传送数据消费很多时间。而网络文件系统(NFS)采用的是一种更好的方式,它允许用户进程打开一个远程文件,并可以在该文件的某个特定位置开始读写数据,这样 NFS 可以让用户复制一个大文件中的一个很小的片段,而不需要复制整个大文件。 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/移动IP.md: -------------------------------------------------------------------------------- 1 | # 相关概念 2 | 移动 IP 技术移动节点(计算机/服务器等)以固定的网络 IP 地址,实现跨越不同网段的漫游功能,并保证了基于网络 IP 的网络权限在漫游过程中不发生任何改变。 3 | 4 | ## 移动节点 5 | 具有永久 IP 地址的移动设备,可以跑到多个网络中去。 6 | 7 | ## 归属代理(本地代理) 8 | 移动站原始连接的网络称为归属网络 9 | 10 | 一个移动结点的永久“居所”称为归属网络,在归属网络中代表移动结点执行移动管理功能的实体称为归属代理 11 | 12 | 就是归属网络上的边界路由器 13 | 14 | ## 永久地址(归属地址/主地址) 15 | 移动站点在归属网络中的原始地址 16 | 17 | ## 外部代理(外地代理) 18 | 在外部网络中帮助移动节点进行数据转发的路由器 19 | 20 | ## 转交地址 21 | 外部代理给在被访问网络中的主机分配的一个用于转发数据报的地址,动态分配的。 22 | 23 | 24 | 25 | 过程: 26 | 一个主机 B 需要向主机 A 发送数据报嘛,如果主机 A 在归属网络中则按照传统的 TCP/IP 方式进行通信。 27 | 28 | 如果主机 A 跑到另一个网络中(被访问网络)并且使用了移动 IP 技术,则开始一段奇妙之旅。首先需要向外地代理进行登记,获得一个临时的转交地址(根据我们所学习的知识,这一步应该是通过 DHCP 协议进行的) 29 | 30 | 然后向 A 的归属代理登记 A 的转交地址,归属代理知道了转交地址后会建立一条通向转交地址的隧道,用于截获发送给 A 的 IP 分组并进行再封装,并通过隧道发送给被访问网络的外地代理。外地代理将封装后的 IP 进行拆封为原始的 IP 分组后发送给移动站 A。 31 | 32 | 上述是移动站 A 接受 IP 数据报的过程,那么移动站如果要发送 IP 数据报呢? 33 | 34 | 发送时使用自己的永久地址作为 IP 分组的原地址,并使用外部代理进行转发。 35 | 36 | 最后如果移动站 A 回到了归属网络中,A 要向归属代理注销转发地址。 37 | 38 | # 网络层应该增加的功能 39 | 为了支持移动性,在网络层还应该增加一些功能: 40 | 41 | + 移动站到外地代理的登记协议 42 | + 外地代理到对归属代理的登记协议 43 | + 归属代理数据报封装协议 44 | + 外地代理拆封协议 45 | 46 | # 举个栗子 47 | 这个在书上有个非常生动的例子。 48 | 49 | 用一个通俗的例子来描述移动IP的通信原理。例如,在以前科技不那么发达的年代,本科毕业时都将走向各自的工作岗位。因为事先并不知道自己未来的准确通信地址,所以怎样继续和同学们保持联系呢?实际上也很简单。彼此留下各自的家庭地址(永久地址)。毕业后若要和某同学联系,只要写信寄到该同学的永久地址,再请其家长把信件转交即可。 50 | 51 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/应用层/DNS(域名系统).md: -------------------------------------------------------------------------------- 1 | 由之前的学习我们知道两个主机之间的通信是通过 ip 地址的,但是人们都不喜欢很长的数字,所以使用一个特定的字符串来对应 ip 地址,这个字符串就称为域名,比如 www.baidu.com。 2 | 3 | DNS 是运行在 UDP 之上的,使用 53 端口。 4 | 5 | 从概念上可以将 DNS 分为 3 个部分:层次域名空间、域名服务器、解析器 6 | 7 | # 层次域名空间 8 | 直接举例子吧。 9 | 10 | 如:www.cskaoyan.com,这个 com 是顶级域名,cskaoyan 是二级域名,www 是三级域名,意思就是域名的等级是从左到右增加的。有个根域名,就是一个"."。 11 | 顶级域名分为三类: 12 | 13 | 1. 国家(地区)顶级域名。如“.cn”表示中国,“.uk”表示英国 14 | 2. 通用顶级域名。如“.com”表公司,“.org”表示非盈利机构 15 | 3. 基础结构域名(arpa)。用于将 ip 地址解析为域名 16 | 17 | 命名方式是采用层次树状结构的。如下: 18 | 19 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740296495034-5ccf8ee1-30ad-460e-8d90-1f9e8410e01c.jpeg) 20 | 21 | # 域名服务器 22 | DNS 使用了大量的域名服务器,但是没有一台 DNS 服务器有所有的主机映射,这些映射是分布在所有的域名服务器上,一共存在着 4 种域名服务器。 23 | 24 | 1. 根域名服务器:这是最高层次的域名服务器,所有的根服务器都知道所有的顶级域名服务器的域名和 ip 地址。根域名服务器是最重要的域名服务器,因为不管是因特网上哪一个本地域名服务器对域名无法解析后就首先要求助根服务器。根服务器不会直接告诉客户端要问的域名 ip,而是告诉它应该去找哪一个顶级域名服务器。 25 | 2. 顶级域名服务器:这些域名服务器负责管理在该顶级域名服务器注册的所有二级域名。收到 DNS 查询请求时,就给出相应的回答。 26 | 3. 权限域名服务器(授权域名服务器):每台需要访问的主机都需要在权威域名服务器这里进行登记。 27 | 4. 本地域名服务器:当一台主机发送一个 DNS 查询请求时,这个请求报文就会发送给该主机的本地域名服务器。这个通常来说是通过 DHCP 协议从 DHCP 服务器那获取的,当然也可以自己手动填写。 28 | 29 | # 域名解析 30 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740279609768-49b3a31b-2455-45e3-ab3c-6b6f11fb706e.png) 31 | 32 | 主机到本地 DNS 服务器都是采用的递归查询,本地 DNS 服务器到其他 DNS 服务器采用递归或者迭代查询。 33 | 34 | 通过上面的图示很轻松地就知道这些是怎么回事,就不再赘言了。 35 | 36 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/IP地址.md: -------------------------------------------------------------------------------- 1 | 刚开始的使用是 IP 地址然后因为地址不够用所以引入了子网划分,发展之后又不够用了,采用 CIDR 进行优化,然后又采用 NAT 优化。 2 | 3 | 在 ipv4 被发明时具有时代局限性,所有的 ip 只有 42 亿左右。 4 | 5 | # 最初的 IP 分类方案 6 | **IP 地址 = 网络号 + 地址号** 7 | 8 | **** 9 | 10 | 将所有的 ip 地址分为 A、B、C、D、E 五类 11 | 12 | 其中 A、B、C 是单播地址,D 为多播地址,E 留到以后再使用 13 | 14 | + A 类地址网络号为 8 位且以 0 开头 15 | + B 类地址网络号为 16 位且以 10 开头 16 | + C 类地址网络号为 24 位且以 110 开头 17 | + D 类地址以 1110 开头 18 | + E 类地址以 1111 开头 19 | 20 | 在刚刚开始的那个年代,要求所有的每台主机、每个路由器接口被分配的 IP 地址都是全球唯一的。 21 | 22 | 路由器和路由器之间的接口可以不分配 ip,但是与其他节点连接的接口必须分配一个 ip 23 | 24 | 在一个网络中的所有主机的网络号必须是相同的,主机号可以自由分配 25 | 26 | 当一台新的主机接入该网络中时,需要给他分配一个 ip 并配置默认网关(就是接入互联网的路由器) 27 | 28 | # 特殊的 ip 地址 29 | 这些特殊的 ip 地址都不能指派给一台主机或者路由器私用 30 | 31 | | 网络号 | 主机号 | 作为分组源地址 | 作为分组目的地址 | 代表含义 | 32 | | --- | --- | --- | --- | --- | 33 | | 确定 | 全 0 | 不能 | 不能 | 表示整个网络(用于路由表、转发表) | 34 | | 确定 | 全 1 | 不能 | 可以 | 向确定的网络号的网络广播 IP 分组 | 35 | | 全 0 | 确定 | 可以 | 不能 | 表示本网络中的确定主机号的主机 | 36 | | 全 0 | 全 0 | 可以 | 不能 | 表示本网络的本主机 | 37 | | 全 1 | 全 1 | 不能 | 可以 | 向本网络广播 IP 分组 | 38 | | 127 | 非全 0 或 1 | 可以 | 可以 | 环回自检地址。表示一台主机本身,用于本地软件环回检测 | 39 | 40 | 41 | 由上面的 1,2 条可以知道一个主机号位数为 n 位的网络主机数不能超过 2^n - 2 台。因为有两种特殊的不能用于分配主机。 42 | 43 | 3,4 条的作用是:在一个网络中新增一个主机时,因为此时主机没有分配到 ip 地址,没有代表自己的 ip,也不知道这个网络中的其他主机 ip,所以此时需要封装一个 DHCP 帧(该帧使用全 0 代表源地址 ip,使用全 1 代表目的地址)发送给 DHCP 服务器,然后 DHCP 服务器就可以给这个发送该帧的主机返回一个 DHCP 帧告诉它给它分配的 ip 地址。 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /数据结构/王道笔记/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 引言 3 | icon: "star-of-life" 4 | categories: 5 | - "408" 6 | - "数据结构" 7 | - "引言" 8 | --- 9 | ## 啊,多么美妙(复杂)的数据结构啊! 10 | ::: tip 悄悄告诉你 11 | 括号里的话才是我想说的 12 | ::: 13 | 我们对于数据结构的认识就是复杂,难,不好思考。但是日常生活中却遍布着数据结构的思想。(只是需要一双善于观察的眼睛:eyes:) 14 | 15 | - 我们在排队等待“叫号”时,不就是一个**队列➡️**吗? 16 | 17 | - 我们存在不同人的QQ列表里,不就是一个**图👤**吗? 18 | 19 | - 我们打牌时情不自禁地将牌从小到大的排序,不就是**选择排序**吗? 20 | 21 | - ........ 22 | 23 | 可以说生活中的例子是数不胜数,所以将理论知识和现实生活中的事物做一个映射,不就变得容易起来了吗? 24 | ## 什么是数据结构 25 | 在王道书中定义的是:**相互之间存在一种或多种特定关系的数据元素的集合。其包含“逻辑结构”、“物理结构”、“数据的运算”**。 26 | 27 | 一个想法是不是总是先在脑海中出现才会实践到现实中。上述的三种组成也是这样,逻辑结构就是事先在脑海中出现的,我们可以使用物理结构去实现它,而运算就是我们实现它的目的。 28 | 29 | 准确的来讲: 30 | 31 | - **逻辑结构**是数据元素中间存在的关系,这个关系可以是线性的,也可以是非线性的。 32 | - **物理结构**是具体在计算机上实现的方式,存在顺序存储、链式存储、散列存储等等。 33 | - **数据的运算**是对数据的操作,怎样操作,操作的结果又是什么,常见的就是插入、删除、查找、更新。 34 | 35 | 数据结构的不同实现方式就是“权衡利弊”的艺术。对于数组和链表而言,前者追求读取的速度,后者追求插入、删除的方便。 36 | 37 | ## 什么是算法 38 | 常常将算法和数据结构联想在一起,甚至以为这两者是相同的东西。实际上并不是的,它们之间虽然有着千丝万缕的联系,但却并不是相同的东西。 39 | 40 | 首先我们需要知道算法的定义:**是对特定问题求解步骤的一种描述,是指令的有限序列**。 41 | 并且算法还具有以下特性: 42 | - **有穷性**:得到最终结果的代价是有具体值的。 43 | - **可行性**:操作过程中的所有步骤都能在有限次执行后成功。 44 | - **确定性**:在相同的输入条件下必然是相同的输出。 45 | 46 | 由此可见,算法是需要数据结构的支撑的,数据结构为算法提供结构化的数据,算法又为数据结构注入强大的生命力。就像在生活中的炒菜:蔬菜,肉类为我们提供材料,需要我们按照放油、放肉、放菜、起锅。才能做出一道香气喷喷的菜肴。 47 | 48 | 算法和数据结构是相互交织,紧密相连的。 49 | 50 | ## 解释 51 | 本文中出现的[ ],只要不是特殊说明都表示向下取整。 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /计算机组成原理/王道笔记/计算机性能指标.md: -------------------------------------------------------------------------------- 1 | 计算机性能包括软件和硬件,这里讨论硬件 2 | 3 | ### **1.首先先了解一下机器字长** 4 | 5 | 就是指的CPU一次能处理数据的位数,我们所说的32位、64位机器就是指的机器字长,对应的就是一次可以计算32bit(64bit)的数据。这里可以看到字长越长,那么处理信息的能力就越大,当然表示的数的范围也就越大,表示浮点数也就越精确,但是也不是越大越好,因为还要对考虑硬件的影响,机器字长的大小会直接影响到ALU、数据总线和存储字长的位数。这里引入了指令字长和存储字长 6 | 7 | - **指令字长:**一条指令中包含的二进制代码的位数 8 | - **存储字长:**一个存储单元中存储的二进制代码的长度 9 | 10 | 其实这个就是字面意思 11 | 12 | 我们需要注意的是,指令和存储字长都必须是字节的整数倍。而指令字长通常是存储字长的整数倍,是几倍,那么取指令字长就需要几个**机器周期(CPU做一次计算的时间)**,是一样就需要一个机器周期。 13 | 14 | ### 2.数据通路带宽 15 | 16 | 指的是数据总线一次能并行传送信息的位数,直接关系到数据的传送能力,这里的数据通路带宽是指外部数据总线的宽度,它与CPU内部的**数据总线宽度(机器字长)**可能不同。 17 | 18 | ### 3.存储容量 19 | 20 | 内存和外存容量 21 | 22 | 存储容量 = 存储单元个数 x 存储字长 23 | 24 | 在主存中,MAR的位数反映了存储单元的个数,MDR的位数反映了存储字的长度。 25 | 26 | 举个例子:MAR为16位,MDR为32位。则在存储体中有 27 | $$ 28 | 2^{16}个存储单元 29 | $$ 30 | 其中每个存储单元有 31 | $$ 32 | 2^{32}个bit 33 | $$ 34 | 所以总共的大小为两个大小相乘,共2M(bit) 35 | 36 | ### 4.运算速度 37 | 38 | 这个能影响计算机的因素有许多:计算机的主频、CPU的结构、执行什么样的操作、主存本身的运行速度等等 39 | 40 | ### 专业名词术语解释: 41 | 42 | 1.吞吐量:系统在单位时间内处理请求的数量,主要取决于主存的存取周期 43 | 44 | 2.响应时间:从用户发出一个请求,到系统做出响应并获得结果的时间。包括CPU时间、等待时间 45 | 46 | 3.主频(CPU时钟频率):机器内部主时钟的频率,是衡量机器速度的重要参数。同一类型计算机,主频越高,完成指令的一个步骤所用时间越短,执行指令的速度越快 47 | 48 | 4.CPU时钟周期:节拍脉冲的宽度或周期。也就是主频的倒数,它是CPU中最小的时间单位 49 | 50 | 5.CPI:执行一条指令所需要的时钟周期数。通常来说,指的是平均时钟周期数 51 | 52 | 6.CPU执行时间:运行一个程序所花费的时间 53 | 54 | ​ CPU执行时间 = (指令数 x CPI)/ 主频 55 | 56 | 7.MIPS:每秒执行百万条指令的数目 57 | 58 | 8.FLOPS:每秒执行浮点运算的数目 59 | 60 | 9.MFLOPS:每秒百万次浮点运算数目 -------------------------------------------------------------------------------- /计算机网络/王道笔记/物理层/信道的极限容量.md: -------------------------------------------------------------------------------- 1 | ## 大致概念 2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735539389530-33dcb377-043a-4cd5-8237-1c97a712eabb.jpeg) 3 | 4 | ## 奈奎斯特定理 5 | 对于一个理想低通信道(没有噪声,带宽有限的信道) 6 | 7 | ![image](https://cdn.nlark.com/yuque/__latex/446d977d8d038b86e1a5452069f4d82c.svg) 8 | 9 | 如果需要转换为 bit 的话就需要知道一个码元中有几个 bit,相乘就可以了。(也可以知道有 K 种信号, 10 | 11 | bit 数 = 以 2 为底 K 的对数) 12 | 13 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735540038016-e6dc7ae0-1332-4f4a-bee1-dc1fabb7eae4.png) 14 | 题目说了无噪声,使用奈氏定理。 15 | 16 | 因为码元率=2W=400。有 4 中信号则一个码元可以传输 2 个 bit 17 | 18 | 故传输的 bit 率为 400x2 = 800kbps 19 | 20 | 答案为 C 21 | 22 | ## 香农定理 23 | 对于一个有噪声,带宽有限的信道 24 | 25 | ![image](https://cdn.nlark.com/yuque/__latex/1c2d4c0da8bb469ada8bcb9726f5d607.svg) 26 | 27 | ![image](https://cdn.nlark.com/yuque/__latex/a67150240148c91e9edf36b1abf2c6c5.svg) 28 | 29 | 在实际应用中往往信道功率比噪声功率大得多,得出的 S/N 也很大,所以引出一个分贝的概念更好表示信噪比 30 | 31 | ![image](https://cdn.nlark.com/yuque/__latex/4ff88f34a0f68e0564d477a870546e9a.svg) 32 | 33 | 香农定理中的 S/N 是没有单位的那一种,如果算出来是分贝需要进行转换 34 | 35 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735540965460-235cf0d6-b87c-4460-9d75-a6ce9f23b759.png) 36 | 现将 dB 转换为没有单位的记法:1x10^3 37 | 38 | 代入香农公式可以得出极限约为 80kbps 39 | 40 | 又因为实际的传输速率只有极限的一半故为 40kbps 41 | 42 | 答案为:C 43 | 44 | ## 总结 45 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735541803503-7b117f0b-2288-4104-a079-6b23df1db4bf.jpeg) 46 | 47 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/无分类编址(CIDR).md: -------------------------------------------------------------------------------- 1 | # 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1738977317318-f00f925b-9ec6-456e-b5ac-9a27e645ec8d.jpeg) 3 | 4 | 当时互联网在民用领域大放异彩,因为每台主机都至少需要一个 IP,所以 IP 地址数量告急。 5 | 6 | 传统的分类有个痛点:如果需要给一个机构分配一个子网并且该机构有 2000 台主机,因为分类的限制所以只能分配 B 类,但是 B 类的一个子网就可以有 6 万多个 IP,这就造成了资源分配不合理的情况,加速了 IP 的消耗。所以提出 CIDR 分类的方式替代传统的分类方式。 7 | 8 | # 无分类编址 9 | 还是以上面的为例:某机构需要 2000 个 IP,此时采用 CIDR 就可以分配一个网络前缀有 21 位的 CIDR 地址块,此时主机号有 11 为,可以分配给 2048 台主机,已经够用了。 10 | 11 | 12 | 13 | 在某个机构获得了 CIDR 地址块后还可以对其进行划分为多个子网。 14 | 15 | ## 定长子网划分 16 | 在一个 CIDR 地址块中,把主机号前 k bit 抠出来作为定长子网,这样就能划分出 2^k 个子网(每个子网包含的 IP 地址块相等) 17 | 18 | 也就是说和之前的传统子网划分的原理一样,只不过是在 CIDR 地址块中进行的。 19 | 20 | 缺点:每个子网分配的 IP 地址数相同,不够灵活,还是容易造成浪费。 21 | 22 | ## 变长子网划分 23 | 在一个 CIDR 地址块中,划分子网时,子网号长度不固定(每个子网包含的 IP 地址块大小不同) 24 | CIDR 地址块的子网划分技巧:可以利用类似于“从根到叶构造二叉哈夫曼树”的技巧 25 | 26 | + 原始 CIDR 地址块作为根节点(假设可以自由分配的主机号占 n bit) 27 | + 每个分支节点必须同时拥有左右孩子,左 0,右 1(反过来也行) 28 | + 每个叶子结点对应一个子网,根据根节点到达叶子结点的路径来分析子网对应的 IP 地址块范围 29 | + 整棵树的高度不能超过 n - 1,因为主机号的位数必须大于 1(主机号不能全 0 或 1) 30 | 31 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738980994705-84a13592-d761-4219-ab52-da24fcd589d1.png) 32 | 33 | 如:给旺财的分配的网络前缀为:根节点的网络号 + 根节点到旺财节点路径上的值(1 0) 34 | 35 | 这就可以快速地获得某个子网的网络号了。 36 | 37 | 38 | 39 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738981176757-e46b57cd-a988-4b9d-87b6-3c15f370124b.png) 40 | 选 C 41 | 42 | 因为需要有 128 个子网所以需要分配给子网网络号的位数要有 7 位,所以对应的主机号就是 9 位,总共 512 个 IP,但是全 0 或全 1 不能使用,所以可以分配的就为 510 个。 43 | 44 | -------------------------------------------------------------------------------- /数据结构/Ebook/LongestPalindrome.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /** 4 | * 最长回文子串 5 | * 题目描述 6 | * 给定一个字符串,求它的最长回文子串的长度。 7 | * 8 | */ 9 | 10 | int getLongByOdd(const char * str, const int len) { 11 | int max = 0, c = 0; 12 | for (int i = 0; i < len; i++) { 13 | //该字符串个数为奇数时:adada 14 | for (int j = 0; (i - j) >= 0 && (i + j < len); j++) { 15 | if (str[i - j] != str[i + j]) 16 | break; 17 | c = j * 2 + 1; 18 | } 19 | if (c > max) 20 | max = c; 21 | } 22 | return max; 23 | } 24 | 25 | int getLongByEven(const char * str, const int len) { 26 | int max = 0, c = 0; 27 | for (int i = 0; i < len; i++) { 28 | //该字符串个数为偶数时:abaaba 29 | for (int j = 0; (i - j) >= 0 && (i + j + 1 < len); j++) { 30 | if (str[i - j] != str[i + j + 1]) 31 | break; 32 | c = j * 2 + 2; 33 | if (c > max) 34 | max = c; 35 | } 36 | } 37 | return max; 38 | } 39 | 40 | //思路一:一个回文串的前缀和后缀都是一致的,所以进行枚举中心点并更新最大长度则可 41 | //因为奇数和偶数的中心点在不同位置,所以需要进行不同的处理 42 | int longestPalindrome(const char *string, const int len) { 43 | //需要合法的输入数据 44 | if (string == 0 || len < 0) 45 | return 0; 46 | int max = 0; 47 | if (len % 2 == 1){ 48 | max = getLongByOdd(string, len); 49 | }else { 50 | max = getLongByEven(string, len); 51 | } 52 | return max; 53 | } 54 | int main(){ 55 | printf("%d", longestPalindrome("aaddaa", 6)); 56 | return 0; 57 | } -------------------------------------------------------------------------------- /数据结构/算法题目/2024.10.12.md: -------------------------------------------------------------------------------- 1 | #include 2 | /* 3 | 题目描述 4 | 路飞买了一堆桃子不知道个数,第一天吃了一半的桃子,还不过瘾,又多吃了一个。以后他每天吃剩下的桃子的一半还多一个,到 n 天只剩下一个桃子了。路飞想知道一开始买了多少桃子。 5 | 6 | 输入 7 | 输入一个整数 n(2≤n≤30)。 8 | 9 | 输出 10 | 输出买的桃子的数量。 11 | */ 12 | int num(int n){ 13 | if(n == 2){ 14 | return 4; 15 | } 16 | return 2*(num(n - 1) + 1); 17 | } 18 | /*题目描述 19 | 有一个小球掉落在一串连续的弹簧板上,小球落到某一个弹簧板后,会被弹到某一个地点,直到小球被弹到弹簧板以外的地方。 20 | 21 | 假设有 n 个连续的弹簧板,每个弹簧板占一个单位距离,a[i] 代表代表第 i 个弹簧板会把小球向前弹 a[i] 个距离。比如位置 1 的弹簧能让小球前进 2 个距离到达位置 3 。如果小球落到某个弹簧板后,经过一系列弹跳会被弹出弹簧板,那么小球就能从这个弹簧板弹出来。 22 | 23 | 现在小球掉到了1 号弹簧板上面,那么这个小球会被弹起多少次,才会弹出弹簧板。 1号弹簧板也算一次。 24 | 25 | 输入 26 | 第一个行输入一个 n 代表一共有 n(1≤n≤100000)个弹簧板。 27 | 28 | 第二行输入 n? 个数字,中间用空格分开。第 i? 个数字 a[i](0= n){ 35 | return 0; 36 | } 37 | return num(arr[i], n) + 1; 38 | } 39 | /*题目描述 40 | 41 | 从 1−*n* 这 *n* 个整数中随机选取任意多个,每种方案里的数从小到大排列,按字典序输出所有可能的选择方案。 42 | 43 | #### 输入 44 | 45 | 输入一个整数 *n*。(1≤*n*≤10) 46 | 47 | #### 输出 48 | 49 | 每行一组方案,每组方案中两个数之间用空格分隔。 50 | 51 | 注意每行最后一个数后没有空格。*/ 52 | 53 | /*f(int i, int j, int n)代表从i位置开始后面的最小可以为j,最大可以为n 54 | 55 | */ 56 | 57 | ```c 58 | void printf_one_result(int n){ 59 | for(int i = 0; i < n; i++){ 60 | if(i) printf(" "); 61 | printf("%d", arr[i]); 62 | } 63 | printf("\n") 64 | return ; 65 | } 66 | int arr[10]; 67 | void f(int i, int j, int n){ 68 | //边界条件 69 | if(j > n){ 70 | return ; 71 | } 72 | for(int k = j; k <= n; k++){ 73 | arr[i] = k; 74 | printf_one_result(i) 75 | f(i + 1, k + 1, n); 76 | } 77 | return ; 78 | } 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/信道划分介质访问控制.md: -------------------------------------------------------------------------------- 1 | 介质访问控制(MAC):多个节点共享同一个总线型广播信道时,可能发生信号冲突。这个就是要解决这个问题的技术。 2 | 3 | ## 知识总览 4 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736153062886-4a0d9006-1572-44b6-b396-3e50d3c6ebed.jpeg) 5 | 6 | ## 时分复用 7 | 将时间分为等长的 TDM 帧,每个 TDM 帧又分为等长的 m 个时隙,将 m 个时隙分配给 m 个用户(节点)使用 8 | 9 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1736153381449-5b80f2a1-321f-4318-956f-86f7c91290d1.png) 10 | 11 | 12 | TDM 有着明显的缺点: 13 | 14 | + 每个节点最多只能分配到信道总宽带的 1/m 15 | + 如果某节点暂不发送数据,会导致分配的时隙闲置,信道利用率低 16 | 17 | 因为一个节点发送的时间之占 1/m 所以其带宽只有总的 1/m 18 | 19 | 20 | 21 | 为了解决上面的问题就引入了统计时分复用(STDM) 22 | 23 | 在 TDM 的基础上增加动态按需分配时隙的功能,需要的多就给多点,这样的设计可以让信道闲置的几率下降 24 | 25 | **STDM 的优点:** 26 | 27 | + 如果需要,一个节点可以在一段时间内获得所有的信道带宽资源 28 | + 如果某节点暂不发送数据,可以不分配时隙,信道利用率更高 29 | 30 | ## 频分复用 31 | 将信道的总频带划分为多个子频带,每个子频带作为一个子信道,每队用户使用一个子信道进行通信 32 | 33 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1736154264659-2d95925a-e499-4f7a-8d6d-ac3dfa74deda.png) 34 | 35 | 这里的复用器是将各节点发出的信号复合后传输到共享信道上的,分用器是将子频带信号分离出来 36 | 37 | 38 | FDM 的优缺点: 39 | 40 | + 优点:各节点可以同时发送信号,充分利用了信道带宽 41 | + 缺点:只能用于模拟信号的发送 42 | 43 | 44 | 45 | 这里提一下波分复用:对光的频分复用 46 | 47 | ## 码分复用 48 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736159135081-de4436ca-8b9f-47a3-96cd-db55656d536b.jpeg) 49 | 50 | 码分序列是分配给节点的,在通信的系统中的码分序列是相互正交的。接收方多数收到的是叠加后的信号,需要分离出每个节点给它发送的信号,就需要用规格化内积来获取。上面的图中已经写好了。 51 | 52 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1736158574236-4ec1460c-9b1c-4b22-ac85-9324e041128c.png) 53 | 54 | 55 | 答案:B 56 | 57 | 十分简单:C 收到了 12 个,码分序列是 4 个,所以有 A 向 C 发送了 3 个 bit 的数据。 58 | 59 | 计算的话就是将 A 的(1,1,1,1)去和对应的 C 接收到 4 个作规格化内积就行 60 | 61 | 62 | 63 | ## 总结 64 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736159831227-d80498de-1f34-4178-bb04-b2ed88988cf9.jpeg) 65 | 66 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/计算机网络的分类.md: -------------------------------------------------------------------------------- 1 |

大体认识

2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735133945240-d539ef48-cdc2-47d7-b726-53fcf57234b5.jpeg) 3 | 4 | 现如今局域网几乎都是采用的“以太网技术”组建的,所以“以太网”成为了局域网的代名词。 5 | 6 | 因为城域网也是采用的“以太网”技术,一般将城域网归为局域网讨论。而广域网采用的技术与其有所不同。 7 | 8 | **局域网是通过路由器连入广域网的**:比如在家中使用的路由器是有以太网交换机和路由器组成,在家用路由器上会有 LAN 和 WAN 两种口子,在家中的电视、电脑等就是连接的 LAN,通过运营商提供的网线连接到 WAN 上接入到广域网中。 9 | 10 | 还有个域网:这个一般通过无线技术连接。如蓝牙、zigbee 等。使用场景是智能家居之类的,在智能家居中通常有一个网关(主设备)和许多从设备(智能电视、智能门锁等),它们在一起组成一个个域网然后主设备会链接到路由器从而接入到互联网中然后就可以通过手机进行远程控制了。 11 | 12 |

传输技术

13 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735134251212-11276fac-7e3d-40e5-8950-5e820c47527a.jpeg) 14 | 15 | 所有的无线网络都是广播式的;通过路由器转发的是点对点式的。 16 | 17 |

拓扑结构

18 |

总线形结构

19 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735213916912-12d6402a-0872-4566-aedb-7ded9905f08c.jpeg) 20 | 21 |

环形结构

22 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735214274819-d6f279ff-6639-45fe-8210-21f8664085e4.jpeg) 23 | 24 |

星形结构

25 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735214514796-d558d754-8601-4d44-929c-eb2ff363ae0c.jpeg) 26 | 27 |

网状结构

28 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735214602403-82ef9558-75d3-4581-a017-0d99ca5de6c6.jpeg) 29 | 30 | 网状型的结构使其灵活性非常的高(可靠性高),同时也导致了其线路十分的复杂,所以维护成本相应地变高了 31 | 32 |

使用者

33 |

公用网

34 | 就是给钱就可以使用的网络,对公众开放。如宽带、交话费就可以使用的互联网 35 | 36 |

专用网

37 | 就是给钱都不能用的,需要特定身份才能使用的网络。如国家安全机构的网络、银行网络等 38 | 39 |

总结

40 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735215080853-0a9b24a8-9bea-4a96-942c-08269401c36d.jpeg) 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/物理层/传输介质.md: -------------------------------------------------------------------------------- 1 | ## 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735608628944-f5989225-bc86-4339-bb1b-2f690edf7eeb.jpeg) 3 | 4 | **注意:我们讨论的传输介质是在第 0 层(传输媒体层),物理成接口是物理层和传输媒体层直接的接口部分** 5 | 6 | ## 双绞线 7 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735608960130-01f5b2a6-0ff9-4fd6-80e5-bf7dc3428ee6.jpeg) 8 | 9 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735609142954-869f2ce0-930b-448c-ab68-113070661b27.png) 10 | 11 | ## 同轴电缆 12 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735609452179-52b25983-39b0-4d61-bee4-84ca9f62edb0.jpeg) 13 | 14 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735609473030-04a1eac7-fa3f-4ce2-bf25-8a7c69831a67.png) 15 | 16 | 内导体的粗度越大,导线的电阻越小,相对应的传输能力越强 17 | 18 | ## 光纤 19 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735638426324-8ab1d581-56a5-4798-9b50-aa6591fb961e.jpeg) 20 | 21 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735638467567-08d83f5c-d074-4053-b1ac-f19c3a8d5a3e.png) 22 | 单模光纤和多模光纤的特点 23 | 24 | 单模光纤:纤芯更细,直线小于一个波长,只能传输一条光线,信号传输损耗低,适合远距离传输 25 | 26 | 多模光纤:纤芯更粗,可同时传输多条光线,信号传输损耗更高,适合近距离传输 27 | 28 | ## 以太网对有线传输介质的命名规则 29 | 速度+Base(基带传输,传输数字信号采用曼彻斯特编码)+介质信息 30 | 31 | 常见的有: 32 | 10Base5:使用同轴电缆并且传输速率为 10Mbps 且最远可以传输 500m 33 | 34 | 10Base2:使用同轴电缆并且传输速率为 10Mbps 且最远可以传输 200m 35 | 36 | 10BaseF*:使用光纤并且传输速率为 10Mbps。这个*可以为任意其他东西,但只要前面是 F 就代表光纤 37 | 38 | 10BaseT*:使用双绞线并且传输速率为 10Mbps。这个*可以为任意其他东西,但只要前面是 T 就代表双绞线 39 | 40 | ## 无线传输介质 41 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735639681319-017af725-69a6-48bd-8fdb-0775990eddf7.jpeg) 42 | 根据光的波长可以得出一些结论: 43 | 44 | 长波更适合长距离,非直线通信。短波更适合短距离,高速通信,如果短波用于长距离通信就需要建立中继站,短波信号指向性强,要求信号接收器对准信号源(以前家中看电视的“锅盖”就是这样的) 45 | 46 | ## 物理层接口的特性 47 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735640559332-4b160698-71e4-42af-9a19-805cdfae2329.jpeg) 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /数据结构/Ebook/Palindrome.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /** 6 | * 题目描述 7 | * 回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如madam、我爱我,这样的短句在智力性、 8 | * 趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗。 9 | * 那么,我们的第一个问题就是:判断一个字串是否是回文? 10 | */ 11 | 12 | //思路一:从两边向中间进行判断 13 | /*** 14 | * @param string 需要判断的字符串 15 | * @return 是否为回文数 16 | */ 17 | bool palindrome(const char *string) { 18 | if (string == NULL) { 19 | return false; 20 | } 21 | int end = (int) strlen(string) - 1; 22 | int start = 0; 23 | while (start < end) { 24 | if (string[start] != string[end]) { 25 | return false; 26 | } 27 | start++; 28 | end--; 29 | } 30 | return true; 31 | } 32 | //思路二:可以从中间向两边进行判断 33 | bool palindromeFromMid(const char *string, const int len) { 34 | //获取中间值:采用位运算获取 35 | /*** 36 | * 偶数个数: a b b a 37 | * ^ ^ 38 | * 奇数个数: a b c d e 39 | * ^ ^ 40 | * 箭头就是需要指向的位置,然后从这个位置向两侧进行判断则可 41 | */ 42 | const int mid = ((len >> 1) - 1) >= 0 ? len - 1 : 0; 43 | //从左侧加了一个mid与之对称的是从右侧加一个mid,这样就是关于中心对称了 44 | const char *left = string + mid; 45 | const char *right = string + len - 1 - mid; 46 | while (left >= string) { 47 | if (*left != *right) { 48 | return false; 49 | } 50 | left--; 51 | right++; 52 | } 53 | return true; 54 | } 55 | int main(){ 56 | const char arr[1000]; 57 | printf("please input a string :"); 58 | // &取地址符 59 | scanf("%s", &arr); 60 | printf("first thinking: "); 61 | if (palindrome(arr)) { 62 | printf("is a palindrome"); 63 | }else { 64 | printf("isn't a palindrome"); 65 | } 66 | printf("\n"); 67 | 68 | printf("second thinking: "); 69 | if (palindromeFromMid(arr, (int)strlen(arr))) { 70 | printf("is a palindrome"); 71 | }else { 72 | printf("isn't a palindrome"); 73 | } 74 | return 0; 75 | } -------------------------------------------------------------------------------- /计算机网络/王道笔记/物理层/编码与调制.md: -------------------------------------------------------------------------------- 1 | ## 大致了解 2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735542388626-484ec9cc-90f8-4a91-927b-afb3982d17c1.jpeg) 3 | 4 | ## 编码(解码)和调制(解调) 5 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735542811584-05b85537-20bb-4a0d-8e44-31b7aa173ce3.jpeg) 6 | 7 | ### 常见的编码方式 8 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735543508953-16b34a63-0201-4226-9ec3-acf440f4d41b.png) 9 | 10 | 根据图示易理解每个编码的方式: 11 | 12 | NRZ:不归零就已经说明其是不会回到 0 处的,并且高就是 1 ,低就是 0 13 | 14 | RZ:归零,就是说无论代表高或低都会回到 0 处,从高回到 0 为 1,从低到 0 就是 0 15 | 16 | NRZI:反向非归零编码,这个需要看每个周期的开头部分,如果和前一个周期的末尾的高度不变就为 1,否则就为 0,并且中间部分是不会改变的(跳 0 不跳 1,中不变) 17 | 18 | 19 | 20 | 曼彻斯特编码:这个通常的看法就是一个周期内从高变为低就为 1,从低变为高为 0 21 | 22 | 差分曼特斯特编码:一个周期内必变,但是需要看一个周期中的开头部分,如果和前一个周期的结尾高度相同就代表为 1,不同就为 0 23 | 24 | **自同步能力:信源和信宿可以根据信号完成节奏同步,无需时钟信号** 25 | 26 | 除了不归零编码其余的编码方式都有。但是除了不归零编码不浪费带宽其余的都会浪费(反向非归零编码需要增加冗余位来实现这个自同步功能)。除了连个曼彻斯特编码抗干扰能力强之外,其余的都弱。 27 | 28 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735545023649-d444f2c2-41bf-4d4d-b5b3-4391520024b3.png) 29 | 根据编码规范这道题答案为:A 30 | 31 | ### 常见的调制方法 32 | **基带信号:来自信源的数字信号,需调制后才能在某些信道(有些信道只能传输模拟信号,有些数字、模拟信号都可以传输)上。** 33 | 34 | 在长距离的信号传输中通常使用调制,因为模拟信号的抗干扰能力强而且在太空中只有模拟信号能传输 35 | 36 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735605740663-159dd6b2-95d9-493e-9108-8e4c53ef67b6.png) 37 | 38 | 除了上面的 3 种还有一个正交幅度调制:使用调幅和调相相结合的方式(QAM) 39 | 40 | AM、FM 都好理解,这里只用说明一下调相:如 y=sin(x)和 y=sin(x + π)这两个就是所谓的调相 41 | 42 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735607705513-41e25085-e03f-48d0-b4ce-785a1d6dc9a1.png) 43 | 这个需要注意的是 QAM-16 后面的数字表示什么意思。 44 | 45 | 表示的是有几种信号。16 就表示有 16 种信号,一个码元可以携带 4 个 bit。其余的同理 46 | 47 | 这道题可以知道波特为 8M 然后极端出一个波特有 6 个 bit 48 | 49 | 所以可以有 64 种信号所以答案为:C 50 | 51 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735607890824-ab340c62-41d6-4676-9a06-89b031b4ed26.png) 52 | 这个题口算则可:可以知道波特率为 400k 然后一个码元有 2bit 53 | 54 | 所以答案选:C 55 | 56 | **注意:以太网默认使用的就是曼彻斯特编码** 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/IP组播.md: -------------------------------------------------------------------------------- 1 | # IP 数据报的三种传输方式 2 | ## 单播 3 | 单播用于发送数据包到单个目的地,且每发送一份单播报文都使用一个单播 IP 地址作为目的地址。是一种点对点传输方式。 4 | 5 | ## 广播 6 | 广播是指发送数据包到同一广播域或子网内所有设备的一种数据传输方式,是一种点对多传输方式。 7 | 8 | ## 组播 9 | 当网络中的某些用户需要特定数据时,组播数据发送者仅发送一次数据,借助组播路由协议为组播数据包建立组播分发树,被传递的数据到达距离用户端尽可能近的节点才开始复制和分发,是一种点对多的传输方式。 10 | 11 | 12 | 13 | 使用组播时发送数据的服务器只需要发送一份数据则可,只有在数据遇到岔路口的时候才会进行数据的复制,链路上都只会有一份数据。如果采用的是单播的话,有多少个主机需要接收,服务器就要准备多少份数据,并且在链路上大概率会存在多个相同的数据。 14 | 15 | 故: 16 | 17 | **组播提高了数据传输效率,减少了主干网出现拥塞的可能性。组播组中的主机可以是在同一个物理网络,也可以是来自不同的物理网络(如果有 组播路由器-运行组播协议 的支持)** 18 | 19 | # IP 组播地址 20 | 因为要实现一个源地址发送数据,然后多个特定的主机接受,肯定不能再使用它们原本的 IP 地址了,不然就是单播了。所以我们需要给在同一个组播的主机分配一个组播 IP 地址,使用的就是之前说过的 D 类地址,此地址是只能用于目的地址的。 21 | 22 | 23 | 24 | 关于 IP 组播需要注意的是: 25 | 26 | 1. 组播数据也是“尽最大努力交付”,不提供可靠交付,应用于 UDP 27 | 2. 对组播数据报不产生 ICMP 差错报文 28 | 3. 并非所有 D类地址都可以作为组播地址 29 | 30 | 组播可以分为在因特网范围内组播和硬件组播。就是前者在路由器上和路由器之间进行组播,后者在局域网内进行组播。 31 | 32 | ## 硬件组播 33 | 这个地方提一下则可: 34 | 35 | 因为是使用的组播 IP,所以需要在局域网中映射为该组播地址对应的组播 MAC 地址。 36 | 37 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740038949511-7057e19e-cf44-4d47-aed1-4350cecc0645.png) 38 | 39 | 在组播 MAC 地址中开头的必须是:01-00-5E,并且后面的 23 为是从组播地址的后 23 为来的,因为 D 类地址前 4 为定位 1110,所以还存在 5 为自由变化的 bit,所以可能存在有多个组播地址最后映射为相同的组播 MAC 地址了,故主机在收到多播数据报后还要在 IP 层利用软件进行过滤,把不是本机需要的数据进行过滤。 40 | 41 | # IGMP 协议和组播路由选择协议 42 | ## 国际组管理协议 IGMP 43 | IGMP 协议让路由器知道本局域网上是否有主机(的进程)参加或退出了某个组播组。将其视为整个网际协议 IP 的一个组成部分。 44 | 45 | IGMP 工作的两个阶段: 46 | 47 | 1. 当某台主机加入新的多播组时,该主机向多播组的多播地址发送一个 IGMP 报文,表示自己要成为该组的成员。本地的多播路由器接收到后,还要利用多播路由选择协议,把这种组成员关系转发给互联网上的其他多播路由器。 48 | 2. 组成员是动态的,为了知道局域网上的主机还是不是组的成员,多播路由器需要周期性地探询。如果一个多播组中一个主机都没有回应,那么就认为该组的所有成员都已经离开了,也就不再把这个组的成员关系转发给其他的多播路由器了;只要该组中有一个主机回应,就认为这个组是活跃的。 49 | 50 | ## 组播路由选择协议 51 | 连接到局域网上的多播路由器还必须和互联网上的其他多播路由器协同工作,以便把多播数据报用最小的代价传送给所有组成员。这就叫同心协力。 52 | 53 | 组播路由选择协议最重要的就是要生成一颗组播转发树。发出数据的节点作为根节点,路由器为树上的其他节点。 54 | 55 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740041854457-3d71e854-6d09-43e0-ad40-3c6c42477bee.png) 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/IPv6.md: -------------------------------------------------------------------------------- 1 | # IPv6 的数据格式 2 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739495131781-4ccbcc8c-9507-40ce-990f-eea5b84a2fba.png) 3 | 用以解释一下上面的字段含义: 4 | 5 | + 版本:始终为 6 6 | + 优先级:在网络上从特定源地址到目的地址的一系列数据报,所有属于同一个流的数据报都具有同样的流标签 7 | + 有效载荷长度:真正的数据部分长度 8 | + 下一个首部:表示扩展首部或者上层协议首部 9 | + 跳数限制:相当于 IPv4 中的 TTL(能通过路由器的个数) 10 | 11 | # IPv4 和 IPv6 12 | + IPv6 将地址从 32 位扩大到了 128 位,可以表示更多的 IP 地址 13 | + IPv6 将 IPv4 的检验和字段彻底移除,减少了路由器上的处理时间 14 | + IPv6 将 IPv4 的可选字段移除首部,变为了扩展首部,成为灵活的首部格式,路由器通常不对扩展首部进行检查,大大提高了路由器的处理效率 15 | + IPv6 支持即插即用(自动配置),不需要 DHCP 协议给其分配 IP 地址 16 | + IPv6 只能在主机处分片,IPv4 可以在路由器和主机处分片 17 | + IPv6 在 ICMP 协议上有个“分组过大”的报文类型 18 | + IPv6 支持资源的预分配,支持实时视像等要求。保证一定的带宽和时延的应用 19 | + IPv6 取消了协议字段,改成了下一个首部字段 20 | + IPv6 取消了总长度字段,改用有效载荷长度字段 21 | + IPv6 取消了服务类型字段 22 | 23 | # IPv6 表示形式 24 | ## 一般形式 25 | 冒号十六进制记法:如下 26 | 27 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739791072281-43dd4f50-9f17-400f-8a39-7d8aa0406dff.png) 28 | 29 | IPv6 使用 128 位表示一个地址并使用 16 位一组。 30 | 31 | ## 压缩形式 32 | 如果有多个连续的 0 在一起可以删除,但是每个分组中都至少需要一位数据 33 | 34 | 如: 35 | 36 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739791214118-6e7b32db-8ba4-4a25-9045-c96857c7b1bc.png) 37 | 38 | 除此之外还有个零压缩的形式:一连串连续的 0 可以使用一对冒号取代。如下: 39 | 40 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739791327019-7c966ab0-6b74-44d2-a4fd-62df332af694.png) 41 | 42 | 需要注意的是一个地址中的一对冒号只能出现一次,不能出现多次。 43 | 44 | # IPv6 基本地址类型 45 | ## 单播 46 | 一对一通信。可以作为源地址或者目的地址 47 | 48 | ## 组播 49 | 一对多通信。只能作为目的地址。不同的目的主机可以加入同一个组播组,这样就可以将数据报同时发送给在同一个组播组中的每一个主机了。 50 | 51 | ## 任播 52 | 实质是一对一,形式是一对多。和一个组播组中的一台主机进行通信。 53 | 54 | # IPv4 向 IPv6 的转换 55 | ## 双栈协议 56 | 就是说在一台设备上同时启用 IPv4 协议栈和 IPv6 协议栈。这样的话,这台设备技能和 IPv4 网络通信又能和 IPv6 网络通信。如果这台设备是一个路由器,那么这台路由器的不同接口上,分别配置了 IPv4 地址和 IPv6 地址,并很可能分别连接了 IPv4 网络和 IPv6 网络。如果这台设备是一个计算机,那么它将同事拥有 IPv4 地址和 IPv6 地址,并具备同时处理这两个协议地址的功能。 57 | 58 | ## 隧道技术 59 | 就是对数据进行再次包装。 60 | 61 | 通过使用互联网络的基础设施在网络之间传递数据的方式,使用隧道传递的数据可以是不同协议的数据帧或包。隧道协议将其他协议的数据帧或包重新封装然后通过隧道发送。 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /数据结构/Ebook/CharacterToInt.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /** 6 | * 7 | * @param num 幂数 8 | * @return 10的幂指数结果 9 | */ 10 | int powerIndex(const int num) { 11 | int result = 1; 12 | for (int i = 0; i < num; i++) { 13 | result *= 10; 14 | } 15 | return result; 16 | } 17 | 18 | /** 19 | * 20 | * @param str 需要转换的字符串 21 | * @return 转换后的整数 22 | */ 23 | //此时的代码健壮性不好,需要重构一下 24 | int strToInt(const char *str) { 25 | // 1234 26 | int num = 0; 27 | const int len = (int)strlen(str); 28 | for (int i = 0; i < len; i++) { 29 | num = num * 10 + str[i] - '0'; 30 | } 31 | return num; 32 | // const int len = (int) strlen(str); 33 | // int num = 0; 34 | // for (int i = 0; i < len; i++) { 35 | // num += (str[i] - '1' + 1) * powerIndex(len - 1 - i); 36 | // } 37 | // return num; 38 | } 39 | 40 | /** 41 | * 优化重构后的实现 42 | * @param str 需要转换的字符串 43 | * @return 转换后的整数 44 | */ 45 | int betterStrToInt(const char *str) { 46 | static const int MAX_INT = (int)((unsigned)~0 >> 1); 47 | static const int MIN_INT = -(int)((unsigned)~0 >> 1) - 1; 48 | int n = 0; 49 | //判断是否输入为空 50 | if (str == 0) 51 | { 52 | return 0; 53 | } 54 | 55 | //处理空格 56 | while (isspace(*str)) 57 | ++str; 58 | 59 | //处理正负 60 | int sign = 1; 61 | if (*str == '+' || *str == '-') 62 | { 63 | if (*str == '-') 64 | sign = -1; 65 | ++str; 66 | } 67 | //确定是数字后才执行循环 68 | while (isdigit(*str)) 69 | { 70 | //处理溢出 71 | int c = *str - '0'; 72 | if (sign > 0 && (n > MAX_INT / 10 || (n == MAX_INT / 10 && c > MAX_INT % 10))) 73 | { 74 | n = MAX_INT; 75 | break; 76 | } 77 | else if (sign < 0 && (n >(unsigned)MIN_INT / 10 || (n == (unsigned)MIN_INT / 10 && c > (unsigned)MIN_INT % 10))) 78 | { 79 | n = MIN_INT; 80 | break; 81 | } 82 | //把之前得到的数字乘以10,再加上当前字符表示的数字。 83 | n = n * 10 + c; 84 | ++str; 85 | } 86 | return sign > 0 ? n : -n; 87 | } 88 | int main(void){ 89 | printf("%d", strToInt("4922")); 90 | return 0; 91 | } 92 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/IPv4.md: -------------------------------------------------------------------------------- 1 | # 各种协议之间的服务关系 2 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738308290432-c7a33dee-161e-4656-a504-6881c095a22e.png) 3 | 4 | ip 协议在整个网络中处于核心地位。ip 分组可以接受上层的 TCP、UDP 协议数据,同层的 ICMP、IGMP 协议也需要用到 ip 分组。ip 层又可以调用下层工作。 5 | 6 | 1. IP 协议:是互联网的核心 7 | 2. ARP 协议:用于查询同一网络中 ip 和 mac 地址之间的映射关系 8 | 3. ICMP 协议:用于网络层实体之间相互通知“异常事件” 9 | 4. IGMP 协议:用于实现 ip 组播 10 | 11 | # ip 数据报的格式 12 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738313478420-a3b903ad-5911-408a-ada9-29398005e8c0.png) 13 | 14 | ip 分组是由首部和数据部分构成的。 15 | 16 | 在首部中有:固定部分(20B)和可变部分(0~40B);在固定部分中首先是版本表示是使用 ipv4 还是 ipv6,然后是首部长度,这个表示出的数字要乘以 4 才是真正的首部长度字节,因为其只有 4bit,所以最多表示 15,所以首部长度最长为 4 x 15 = 60B,因此可变部分才是 0~40B(因为固定长度为 20B 嘛) ,这个首部检验和只是检验首部的,数据部分需要交给传输层进行检验,然后是可变部分的填充部分需要说明一下,这个填充是为了使得首部数据字节为 4 的倍数,这个是因为这个首部长度乘以 4 造成的印象。 17 | 18 | **将 ip 数据报交给下层传输时,实际上会受到下一段链路的最短帧长和最长帧长的影响。** 19 | 20 | # ip 数据报的分片问题 21 | **重要概念:MTU:一个链路层数据帧能承载的最大数据量成为最大传送单元。如:以太网的 MTU 为 1500B 22 | **如果一个 ip 数据报的总长度超过了下一个链路 MTU,就需要进行分片 23 | 24 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738327153956-8b323f62-fa85-4cf1-8a11-109d1a765eff.png) 25 | 26 | 这个标识是标明这个分片是属于哪个 ip 数据报的,同一个 ip 数据报经过分片后的各个片的标识是一样的 27 | 28 | 然后标志从低位到高位分别是 MF、DF、无所谓。MF 为 1 就表示后面还有分片,为 0 后面就没有分片了;DF 表示这个 ip 数据报能不能进行分片,为 1 表示该 ip 数据报不能被分片,为 0 就表示可以被分片。 29 | 30 | 如果有个 ip 数据报的 DF 为 1,且下一个链路的 MTU 小于该 ip 数据报的话,路由器会将该 ip 数据报丢弃并返回一个 ICMP 报文表示异常情况 31 | 32 | 片偏移代表分片后数据开始的字节在源 ip 数据报中的字节位置,需要将其乘以 8 33 | 34 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738327811170-5b93de5e-0c53-404f-8330-52031216ccbb.png) 35 | 36 | 这幅图详细地说明了 ip 数据报的分片过程。ip 数据报的分片不仅可以发生在源主机还可以发生在传输过程中的任意一个路由器中。 37 | 38 | # ip 数据报的生存时间等 39 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738328195186-9273f676-680c-439f-83b4-d9fd29259b57.png) 40 | 41 | TTL:是 ip 数据报的存活时间,就是可以跑的路由器个数,如果为 0 还没有达到目的节点则丢弃并返回一个 ICMP 报文通知源主机出错了。 42 | 43 | 协议:这个字段表示该 ip 数据报是采用的什么协议是 TCP 协议吗还是 UDP 协议,这样目的节点接收后才知道交给哪一个协议进行处理。 44 | 45 | 首部校验和:这个只用校验 ip 数据报的首部,数据部分不用校验,也不是 ip 层的工作。 46 | 47 | 48 | 49 | 最后的源地址和目的地址都是 32bit 50 | 51 | # IPv4 表示形式 52 | 点分十进制。如:255.255.13.43 53 | 54 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/计算机网络分层结构.md: -------------------------------------------------------------------------------- 1 | ## 大致了解 2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735286966202-4cc42d6b-ca6c-4202-b88a-58cc8a4b282e.jpeg) 3 | 4 | **分层思想:将一个庞大复杂的问题转换为若干较小的局部问题(化繁为简)** 5 | 6 | **分层结构的设计并不唯一,可以合理地根据实际需求添加或减少层次** 7 | 8 | **同一个功能可以在不同层次件重复出现** 9 | 10 | ## 三种常见的计算机网络分层 11 | ### OSI 参考模型 12 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735288134229-266f759e-810b-4c35-86b0-6ed7379dc346.jpeg) 13 | 14 | ### TCP/IP 模型 15 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735288214417-94d66fc7-2e0e-441e-b646-3b1611ed6c44.jpeg) 16 | 17 | ### 五层模型 18 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735290244879-bd037e3a-9215-435d-ba0c-f50448b50127.jpeg) 19 | 20 | ## 网络的体系结构 21 | 网络体系结构是计算机网络的各层及其协议的集合,就是这个计算机网络及其构件所应完成的功能的精确定义(不包含实现)。这个聊的是抽象的体系,实现是具体的软硬件。 22 | 23 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735304972370-d5d2b0d8-5607-43bb-98fd-611035c79381.jpeg) 24 | **实体**:在计算机网络分层结构中,第 n 层的活动元素(硬件和软件)通常称为第 n 层实体。 25 | 26 | **协议**:就是网络协议,是控制对等实体之间进行通信的规则的集合,是水平的。比如 A、B 传输层之间的通信,就需要协议,这样双方才能知道想表达的是什么意思。 27 | 28 | 不同机器上的同一层称为对等层,同一层的实体称为对等实体。 29 | 30 | **接口**:即同一节点内相邻两层的实体交换信息的逻辑接口,又称服务访问点 31 | 32 | **服务**:服务是指下层为紧邻(挨着的)上层提供功能调用,它是垂直的。 33 | 34 | **数据应用层之上传输到应用层,应用层会在传入的数据基础之上添加控制信息:应用层控制信息 + 数据** 35 | 36 | **应用层将这个数据传递到传输层,传输层又会在此基础上添加传输层的控制信息:传输层控制信息 + 应用层控制信息 + 数据。以此类推,将数据传递到一层,该层就会添加上对应的控制信息。接收主机接收到最终的数据后会根据不同层次进行数据的解析(拆除对应层次的控制信息)因为同一层次采用的协议是统一的,所以可以明白对方的意思。** 37 | 38 | 这里就可以引出几个概念: 39 | 40 | + 协议数据单元(PDU):对等层次之间传送的数据单位。第 n 层的 PDU 称为 n-PDU 41 | + 服务数据单元(SDU):为完成上一层实体所要求的功能而传输的数据。的 n 层的 SDU 称为 n-SDU 42 | + 协议控制信息(PCI):控制协议操作的信息。第 n 层 PCI 称为 n-PCI 43 | 44 | **协议数据单元是由服务数据单元和协议控制信息组成的。** 45 | 46 | ## 协议 47 | **协议的三要素:协议是由语法、语义和同步三部分组成的(三要素)** 48 | 49 | + **语法:**数据与控制信息的格式。例如:协议控制信息首部占几个字节,每个字节什么含义,协议的数据部分最多由几个字节构成 50 | + **语义:**即需要发出何种控制信息,完成何种动作以及做出何种应答。例如:协议中需要明确规定:发送方发完数据之后接收方是否需要应答,以及应答种类有哪些 51 | + **同步(时序):**执行各种操作的条件、时序关系等,即事件实现顺序的详细说明。例如:发送方发完数据后,接收方需要立即应答。如果发送方在 10 秒内未收到应答成功的信息,则发送发会再次发送 52 | 53 | **这个语法就是数据和控制信息的格式,语义就是规定详细的控制信息和发出什么动作以及具体的应答,同步就是操作的条件和时序关系等。** 54 | 55 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/计算机网络的组成和功能.md: -------------------------------------------------------------------------------- 1 | ## 计算机网络的概念 2 | 就是通过计算机网络可以将分散的、自洽的计算机系统由通信设备和线路连接起来并由功能完善的软件实现资源共享和信息传递。它是一个系统。 3 | 4 | 不同的设备在不同的情况下接入网络的方式可以不同: 5 | 6 | 1、笔记本电脑通过 wifi 7 | 8 | 2、手机在户外通过 5G 基站 9 | 10 | 3、台式机通过网线接入路由器 11 | 12 | 等等。 13 | 14 | 分散指的是可以在世界上任意一个地方、自洽是指一个设备损坏的情况下不会影响到整体。其中通信设备包括路由器、交换机等等。 15 | 可以通过微信、百度网盘等软件进行资源共享和信息传递等功能 16 | 17 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734761528039-923d2891-9da4-47ee-8ea1-5b3d552df013.jpeg) 18 | 19 | ## 计算机网络、互联网、互连网 20 | **计算机网络有若干个结点和连接这些结点的链路构成。** 21 | 22 | 场景再现: 23 | 我和室友想要一起联机打游戏,但是在宿舍已经断网了,现在可以使用一条网线将我的电脑和舍友的电脑进行连接。这样我们又可以快乐的打游戏了,这就叫网线直连。其他舍友看着断网了还能一起开黑很是羡慕,于是乎也想加入进来。但是没有多余的插口了,现在就可以请出我们的集线器(Hub)了,集线器可以将多台设备连接到同一个网络中来。但是我们的体验不好,这是因为集线器在使用中只能一台设备发送消息,其余的想要发送消息则要等待。 24 | 25 | 因为不好用现在几乎已经没有了,构建网络也不再使用集线器,而是使用交换机(Switch),和集线器长得挺像的,也有许多的插口,但是多台设备在发送消息时不会发生冲突。故现在想要构建一个局域网都会用到交换机。 26 | 27 | 路由器可以将这样由交换机组成的多个局域网连接起来组成更大的网络。 28 | 29 | 需要注意的是:家用的路由器包含了路由器和交换机和一些其他的功能。 30 | 31 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734762734314-096090cc-8823-415a-8f43-3fb133a2e6aa.jpeg) 32 | 33 | **路由器将两个或更多的计算机网络连接起来的更大的网络可以成为互连网。** 34 | 35 | **互联网是有 ISP(互联网服务提供商:电信、移动等公司) 和各大国际机构组建的覆盖全球范围内的互连网** 36 | 37 | 在互联网中规定了必须使用 TCP/IP 协议进行数据传输,而在互连网中可以使用任意协议 38 | 39 | 40 | 41 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734765028141-db1e8e0e-32a5-4c6a-80d7-44287ede3214.jpeg) 42 | 43 | ## 计算机网络的组成 44 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734764090113-69ffb4ba-72ca-4ab8-a372-9449ea147f3c.jpeg) 45 | 46 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734764497071-18749fbc-177f-4087-8090-b1cdd306acd2.jpeg) 47 | 48 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734764965107-2eb94bb9-cd94-476c-86d1-f423b1a99243.jpeg) 49 | 50 | ## 计算机网络的功能 51 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734766228870-31b7aaa3-b0e6-4dbe-bd66-f78348feed5e.jpeg) 52 | 53 | 以前不理解怎样共享硬件,现在明白了。比如智能音响在接受到主人的指令后并不是在本地处理的,而是将数据给云端服务器,通过服务器的强大算力得到结果返回,这里就使用了服务器的硬件资源。 54 | 55 | 存储文件在网盘上,服务器会有备份,当一个服务器出现故障会有备份,不至于自己的数据真正的消失。 56 | 57 | -------------------------------------------------------------------------------- /计算机网络/小林coding/初始计算机网络.md: -------------------------------------------------------------------------------- 1 | ​ 关于学习小林coding的图解计算机网络的一些笔记和见解。 2 | 3 | # 大体认识 4 | 5 | ​ 关于计算机网络的最重要的网络模型的层次:在小林coding中写的是应用层、传输层、网络层、网络接口层 6 | 7 | (tcp/ip四层模型)。在学校的学习中是将网络接口层细分为数据链路层和物理层(OSI七层模型),当然怎样分 8 | 9 | 都是有一定道理的,不用过度纠结。 10 | 11 | 重要的是这种分层的思想,在对应的层只实现这一层的功能,下一层总是为上一层提供必要的服务。 12 | 13 | ​ 关于每层的**功能和作用:** 14 | 15 | ​ **应用层:**在平常生活中使用的所有应用都是在这一层中存在,这一层是专心为用户提供功能:包括 16 | 17 | http(浏览器和服务器传输数据的协议)、ftp(传输文件)、dns(通过域名获取ip)、smtp(发邮箱)等。 18 | 19 | 如果不同的应用想要交互信息数据就要将数据传递到下一层。应用层是在OS的用户态工作,其余的层都是在OS的 20 | 21 | 内核态工作的。这里提到了用户态和内核态,他们的区别也很简单。在用户态状态下计算机不能直接访问硬件资源 22 | 23 | 而且不能执行特权指令,收到了OS的权限约束。而在内核态就是可以直接访问硬件资源而且可以执行特权指令。 24 | 25 | 这样的设计可以更好的保护我们的系统和提高性能。 26 | 27 | ​ **传输层:**上一层产生的数据想要传输就需要传输层的服务,也是为应用层提供网络支持(负责向两台终 28 | 29 | 端设备进程间的通信提供通用的数据传输服务)。在这一层有两个重要的协议,就是tcp和udp协议。这也是贯穿 30 | 31 | 整个计算机网络的两个词汇。他们有啥区别呢?tcp是可靠的连 32 | 33 | 接,具有重传机制,校验机制、拥塞控制等保障数据正确的协议,而udp协议就不用管这么多了,只要把数据发出 34 | 35 | 去就不管了。说到这里是不是觉得udp协议实在是太不负责任了,可以一个事物的存在必然有他的合理性,虽然说 36 | 37 | 在数据保障这里是它的弱势,但是正是由于没有了那么多的“包袱”所以在传输速率上非常快,因为这个特性可以在 38 | 39 | 一些需要高实时性的地方使用udp协议,比如视频通话。而且在tcp拥有的哪些机制,可以在使用udp的同时在应 40 | 41 | 用层实现数据保障的机制,只是说需要一定的技术水平,哈哈。还需要说到的是传输层在传输应用层传来的数据时 42 | 43 | 如果超过了MSS(tcp最大报文长度)就需要分块,如果在传输过程中丢失了块也就只需要重新传输丢失的块而不 44 | 45 | 用将全部的数据进行重传了。这里说到了在不同的应用间进行传输数据,在一台设备上往往会有多个应用,我们需 46 | 47 | 要怎样才能将传输的数据传给正确的应用呢?在这里我们就需要端口号了,通过端口号分别不同的应用。常见的有 48 | 49 | web服务器的80。 50 | 51 | ​ **网络层:**负责为分组交换网上的不同主机提供通信服务。在这一层使用的通常是ip协议,它会在传输层 52 | 53 | 的报文前面再加上ip头形成ip报文,如果这个长度超过了MTU(以太网中通常为1500字节)就会进行分片,然后 54 | 55 | 就可以将数据发往网络世界中了。我们需要知道,网络世界是很大的,机器也是非常多的,那它怎样知道需要将 56 | 57 | 数据传到哪里呢。在网络的世界里,每个机器都是有自己的标号的,采用的是ipv4协议,32为二进制组成的一串 58 | 59 | 数字每8位为一小组。虽然是有编号了,但是肯定不是一个一个去判断每一个机器是不是我需要传输的对象撒,不 60 | 61 | 然的话今天发的消息,可能明天才能收得到了。这里使用了一个方法,就是先找到机器所处的子网然后再将数据传 62 | 63 | 输到该子网中进行判断,这样的效率要高许多。说到这里就要引出**网络号和主机号**的概念了。网络号是区别这个主 64 | 65 | 机在哪一个子网直接锁定到一个小的区域再根据主机号确定数据要发送到的主机。举个例子:比如10.24.123.1/24 66 | 67 | 后面的24就是子网掩码,也可以是255.255.255.0这样表示子网掩码。网络号通过这个ip和子网掩码做与运算就可 68 | 69 | 以得到,主机号就是将网络号取反就可以了。ip协议有个很重要的功能就是路由,它可以使用它的算法去找到下一 70 | 71 | 个要发送的路由器是哪一个。 72 | 73 | ​ **网络接口层:**上一层的网络层将数据传递下来后,这一层会在数据前面再添一个mac帧头和帧尾。这个 74 | 75 | 的作用和ip是很相似的也是去找对应的机器,因为根据ip去找是在网络层的实现,在这一层都是与硬件相关的。在 76 | 77 | 以太网中就不适用ip了,所以需要使用mac去匹配机器,可以使用arp协议映射ip和mac的值,这样就可以将数据 78 | 79 | 发向正确的机器了。 -------------------------------------------------------------------------------- /计算机网络/王道笔记/应用层/电子邮件.md: -------------------------------------------------------------------------------- 1 | # 电子邮件系统的组成结构 2 | 电子邮件是一种异步通信的方式,是不需要双方同时在场的。 3 | 4 | 一个电子邮件系统至少具备三个最主要的组成构件: 5 | 6 | + 用户代理 7 | + 邮件服务器 8 | + 电子邮件使用的协议(SMTP、POP3 、IMAP 等) 9 | 10 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740355629686-3f47b8b5-e13d-4290-a1c7-dab3088e23af.jpeg?x-oss-process=image/auto-orient,1) 11 | 12 | **用户代理(UA)**:用户与电子邮件系统的接口。用户代理向用户提供一个很友好的接口来发送和接受邮件,用户代理至少应当具有撰写、显示和邮件处理的功能。就是一个在客户端运行的软件,如 OutLook,Foxmail 等等。 13 | 14 | **邮件服务器**:这个的功能是发送和接受邮件,同时还需要向发件人报告邮件传送的情况(已交付、被拒绝、丢失等)。它是采用 C/S 模式工作的,可以同时充当客户和服务器。也就是可以提供服务和请求服务,上面的图也可以看出来。 15 | 16 | **邮件发送协议和读取协议**:邮箱在发送和接受时采用的是不同的协议,发送采用 SMTP 协议,接收采用 POP3 协议。这两种协议,SMTP 是使用的推(PUSH)的通信方式,你看邮箱是被 SMTP 客户推着到发送方服务器的,然后又被推到了接收方服务器,POP3 使用的拉(PULL)的通信方式,POP3 客户端在读取邮箱时会发送一个请求将在接收方邮件服务器上的邮件拉取下来。 17 | 18 | 需要注意的是发送的邮件只会在发送方服务器和接收方服务器上,不会出现在中间服务器上的。有点像点对点发送。 19 | 20 | # 电子邮件格式和 MIME 21 | ## 电子邮件格式 22 | 有首部和主体部分组成。 23 | 24 | 在首部中有两个必填的关键字:To、From 25 | 26 | 还有个重要的是:Subject 27 | 28 | 这些在 Java 中都有具体的实现 api,这里就不需要多言了。直接上一张图吧。 29 | 30 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740356946494-23a9799f-0996-4fb5-b24e-3bf141b22fb4.jpeg) 31 | 32 | ## 多用途因特网邮件扩展(MIME) 33 | 其实 SMTP 协议只能传输 7 位的 ASCII 码文本邮件,所以其他国家的各种符号,文字都无法进行传输,所以由于现实生活中存在的问题,就提出了 MIME。 34 | 35 | MIME 并没有改动或取代 SMTP,而是相当于一个中间层,将邮件中的 ASCII 码进行转换。 36 | 37 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740357389555-684af833-e243-4709-a344-7570975e662a.jpeg) 38 | MIME 主要包括一下三部分内容: 39 | 40 | 1. 5 个新的邮件首部,包括 MIME 版本、内容描述、内容标识、传送编码和内容类型 41 | 2. 定义了许多邮件内容格式,对多媒体电子邮件的表示方法进行了标准化 42 | 3. 定义了传送编码,可对任何内容格式进行转换,而不会被邮件系统改变 43 | 44 | # SMTP 和 POP3 45 | ## 简单邮件传输协议( SMTP) 46 | 该协议是一种可靠且有效的电子邮件传输的协议,它控制两个相互通信的 SMTP 进程交换信息。SMTP 使用的是 TCP 连接,端口号为 25。 47 | 48 | SMTP 通信的过程有三个部分: 49 | 50 | + 连接建立 51 | + 邮件传递 52 | + 连接释放 53 | 54 | ## 邮局协议( POP3) 和因特网报文存取协议( IMAP) 55 | 两者都是读取邮件的协议,只是前者 IMAP 是简单且功能有限的,现在使用的版本是 POP3,在传输层上使用的 TCP,端口号是 110。 56 | 57 | 其接收方的用户代理上要运行 POP 客户程序,在接收方的邮件服务器中需要运行 POP 服务器程序。 58 | 59 | 它提供两种工作方式:下载并保留(用户读取了邮件,邮件还会在邮箱上)、下载并删除(用户读取了邮件,接收方服务器就会将该邮件进行删除) 60 | 61 | 62 | 63 | 后者提供许多联机命令,因此 IMAP 服务器需要维护会话用户的状态信息。并且其还可以允许用户代理只获取报文的某些部分(和文件下载那里的 NFS 相似哦)。 64 | 65 | 66 | 67 | 此外需要补充的是,随着万维网的流行,许多在此基础上建立的电子邮件也出现了。如:Hotmail、Gmail 等等。 68 | 69 | 此类电子邮件的特点是:用户浏览器与邮件服务器之间邮件的发送和接受使用的是 HTTP,在不同邮件服务器之间传送邮件时才使用 SMTP。 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/检错和纠错编码.md: -------------------------------------------------------------------------------- 1 | ## 差错控制功能 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735790456195-ff63bd61-b010-4d87-af95-821b3e0f8f38.jpeg) 3 | 4 | ## 检错编码 5 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735800354720-ea941a0c-66bf-4238-bb93-80f226eac160.png) 6 | 7 | ### 奇偶校验码 8 | #### 奇校验码 9 | 整个校验码(校验位+有效信息位)中 1 的个数是奇数 10 | 11 | #### 偶校验码 12 | 个校验码(校验位+有效信息位)中 1 的个数是偶数 13 | 14 | 15 | 16 | **在日常使用中更常用的是偶校验,因为偶检验只需要将所有信息位全部进行异或运算,结果为 0 就是没有错误,否则就是有错误的。** 17 | 18 | 但是这个奇偶校验只能校验奇数个错误,如果发生偶数个错误是无法检测到的并且只有检错功能没有纠错功能 19 | 20 | ### CRC 校验码(循环冗余校验码) 21 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735802911744-d79f7292-be42-45b7-94d7-3b136f6b1677.jpeg) 22 | 23 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735803307408-e7ad170a-a2ea-432f-a1be-4ad90223cfa4.png) 24 | 25 | #### 基本思想 26 | 发送方和接受方约定一个除数,接收方在接收到信息后使用这个除数,用信息(信息位+校验位)除以这个除数,如果结果为 0 就表示没有错误,否则就有错误。 27 | 28 | 发送方在发送的时候需要保证添加的校验位和信息位除以这个除数是为 0 的。 29 | 30 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735804147393-71082caf-decd-4040-af00-b92fe2ccb76b.png) 31 | 32 | 在这里举个例子:会给出生成多项式,这个包含除数信息和校验码的位数信息(最高幂次),然后根据校验码的位数往后添相应个数的 0,使用得到的二进制数除以除数,得到的余数就是校验位。从而就地道了 CRC 码了。(注意:在进行二进制的除法时只需要看最高位,与除数的最高位相同就商 1,否则商 0,每次保留比除数少一位的结果就行,这样最后的余数也只会比除数少一位) 33 | CRC 在 2^R>=K+R+1 的情况下可以有纠错 1 位 bit 的功能,如果信息位太多了就不能发现出错的在什么地方了,如果出错的在第一位接收方除后的余数对应的十进制为 1(这个是在2^R>=K+R+1 这个条件下的)以此类推。但是实际应用中是不会拿来纠错的,因为信息位的个数对于检验位可以表示的个数大的太多了 34 | 35 | CRC 校验码的能力: 36 | 37 | + 可以检测出所有奇数个错误 38 | + 可以检测出所有双比特的错误 39 | + 可以检测出所有小于等于校验位长度的连续错误 40 | + 若满足 2^R>=K+R+1 则可以纠正单比特错误 41 | 42 | ## 纠错校验码 43 | ### 海明校验码 44 | 设计思路:将信息位分组进行偶校验。所需要的条件还是 2^R>=R+K+1 45 | 46 | 设海明码是 Hn+kHn-1Hn-2....H1,信息位是 DnDn-1Dn-2...D1,校验位为 PkPk-1Pk-2...P1 47 | 48 | 校验码的分布不再像是上面的那样的全部位于信息位的前或后方,而是分布在各处,需要的规则是: 49 | 50 | Pi 处于海明码的 H(2^(i-1))处,P1 在 H1 处,P2 在 H2 处,P3 在 H4 处以此类推。信息位就从小位到大位依次排好就行。 51 | 52 | 那怎样确定这个校验码为多少呢? 53 | 54 | 这样的,将信息位所处的海明码 Hi 这个 i 写成二进制的形式(位数是和校验码的总数一样的),Pi 这个 i 代表的是权重,看所有信息位对应的权重上为 1 的信息位作异或操作得到的值就是对应权重的校验位的值 55 | 56 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735813513926-c92de591-0931-4bdd-8c5b-a60b2f1cfb0d.png) 57 | 58 | 重要的纠错步骤: 59 | 60 | 在接收方接收到这个信号后进行检验: 61 | 62 | 有多少个校验位就会有多少个 S,根据之前分好的组进行检验。如上: 63 | 64 | P1、D1、D2、D4 在一组,进行异或操作,同理将其余的分组也进行异或操作,将得出的数据按 SnSn-1...S1 的循序排好,将其转换为十进制,若为 0 则表示无差错,否则是几就是在传输的海明码第几位发生错误。 65 | 66 | **在实际应用中还会有一个全校验位(对后面整体进行偶校验得出的值),因为不使用这个全校验位无法区分是一位出错还是两位出错。如果是两位出错的话,海明校验码是无法进行纠错的,只有进行重传才行。** 67 | 68 | **** 69 | 70 | **** 71 | 72 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/OSI参考模型和TCP_IP模型.md: -------------------------------------------------------------------------------- 1 |

总览

2 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735385049745-255848bc-a423-43d6-a3bb-cd3a28897cb3.jpeg) 3 | 4 |

常见网络设备的功能层次

5 | 日常生活中常见的有网线、路由器、交换机等等,那他们的功能层次是在哪一层呢?(注意在功能层次的一层就代表在此层之下的所有层都实现了) 6 | 物理层:集线器 7 | 8 | 数据链路层:交换机 9 | 10 | 网络层:路由器 11 | 12 | 第零层:网线、双绞线等传输媒体 13 | 14 |

OSI 参考模型

15 |

物理层

16 | 实现两个节点之间数据的 bit(0、1) 传输 17 | 18 | + 需定义电路接口参数(如形状、大小、引脚数等) 19 | + 需定义传输信号的含义、电器特征(如 5V 表示 1,1V 表示 0;每比特信号持续时间 0.1ms) 20 | 21 | 这里使用 bit 传输信号的话,可以由于外界的干扰会导致表示的 1 到接收方那里变为了 0。所以为了确保正确性就引入了数据链路层 22 | 23 |

数据链路层

24 | 确保相邻节点之间的链路逻辑上无差错 25 | 26 | + 差错控制:检错+纠错或者检错+丢弃+重传 27 | + 流量控制:协调两个节点之间发送帧的速率 28 | 29 | 数据链路层会一次性传递多个 bit 并加上用于差错检验的 bit 而形成的帧。接受方在接收到这个帧后会检查是否出差错,没有则去掉多余的 bit 并将数据往上层传输 30 | 31 | 在数据的发送过程中往往是需要在多个节点之间进行转发的所以寻找下一跳的功能就放到了网络层中 32 | 33 |

网络层

34 | 把分组(数据报)从源节点转发到目的节点 35 | 36 | + 路由选择:构造并维护路由表,决定分组到达目的节点的最佳路径 37 | + 分组转发:将分组从合适的端口转发出去 38 | + 拥塞控制:发现网络拥塞,并采取措施缓解拥塞 39 | + 网际互联:实现异构网络互联(不在乎局域网中的实现方式) 40 | + 其他功能:差错控制、流量控制、建立连接与释放、可靠传输管理(接收方需要返回分组确认消息) 41 | 42 | 网络层的差错控制是以分组为单位的,数据链路层的是以帧为单位的。数据链路层保障了局部的正确性,而网络层要保障全局的正确性。同理流量控制也是这样的。建立连接和释放是说在数据传输前先建立一条虚电路,可以保障分组有序、不重复的到达,结束后释放虚电路。 43 | 44 | 到这一步就已经完成主机到主机的分组传输了,但是一个主机上有不同的进程,到底这个数据是哪一个进程的就需要传输层的帮忙了。 45 | 46 |

传输层

47 | 实现端口到端口的通信(进程到进程的通信) 48 | 49 | 这一层传输的数据称为报文段 50 | 51 | + 复用和分用:发送端几个高层实体复用一条底层的连接,接收端进行分用(发送方几个实体都向一个连接里发送数据,接收方几个实体都从这个连接中获取数据) 52 | + 其他功能:差错控制、流量控制、连接的建立与释放、可靠传输的管理 53 | 54 | 这个其他功能与上面的本质一样只不过对象变为了报文段而已,就不赘言了。 55 | 56 |

会话层

57 | 管理进程间会话 58 | 59 | + 会话管理:采用检查点机制,当通信失效时从检查点继续恢复通信 60 | 61 | 就是断点续传的意思。比如:两个人之间使用微信传输文件,如果传输到某处断了(检查点),等恢复连接后会直接从检查点处继续传输数据 62 | 63 |

表示层

64 | 解决不同主机上信息表示不一致的问题 65 | 66 | 比如:两台主机对数据编码格式不一样 67 | 68 | + 数据格式转化:编码转化、压缩/解压、加密/解密 69 | 70 |

应用层

71 | 实现特定的网络应用(传输的数据称为报文) 72 | 73 | 功能繁多根据应用需求进行设计 74 | 75 |

TCP/IP 模型

76 | 在网络应用中并不是所有的应用都需要会话层和表示层故在此模型中将其砍掉了,如果需要有特定的功能实现就使用应用层的特定协议。将这些不是必要的层次中的功能放在应用层进行实现是非常灵活的,应用需要就加不需要就不加,这样更加合理。 77 | 78 | 在 OSI 模型中物理层会规定具体的网线接口等硬件设备,但是 TCP/IP 模型的理念是网络硬件设备的种类很多不应该有过多的限制,基于这样的理念,TCP/IP 模型将网络层一下的都归为网络接口层。 79 | 80 |

网络接口层

81 | 实现相邻节点之间的数据传输(为网络层传输分组),但具体怎样传输不作规定 82 | 83 |

网络层

84 |

传输层

85 |

应用层

86 | 与 OSI 模型大体一样,在这里说明不一样的地方: 87 | 如果像 OSI 模型一样在每一层进行数据的差错控制等操作会让网络的核心部分变得复杂,路由器的压力会较大,造价也会提高。所以 TCP/IP 模型中网络层就不用管差错控制、连接的建立和释放、可靠传输管理,只需要尽自己最大努力将数据交付给下一跳,并且差错控制等功能放在了传输层,这样的设计使得网络核心部分的压力变小了,造价也降低了(数据的传输更快了),这样将压力给到了网络边缘部分的主机,也就是我们的电脑(需要差错检验等功能),如果采用的是 TCP/IP 协议同样是可以保证数据的正确性的,但采用 UDP 的话就不能保证了。 88 | 89 | OSI 模型的网络层向上层不仅提供了有连接可靠的服务(虚电路)有提供了无连接不可靠的服务(数据报) 90 | 91 | TCP/IP 模型的网络层向上层只能提供无连接不可靠的服务(数据报) 92 | 93 | -------------------------------------------------------------------------------- /数据结构/Ebook/CharacterQuestions.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | /** 5 | * 题目描述: 6 | * 给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部, 7 | * 如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部, 8 | * 使得原字符串变成字符串“cdefab”。请写一个函数完成此功能, 9 | * 要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。 10 | * 11 | */ 12 | 13 | //第一种思路 14 | //根据其有的性质进行移动 15 | 16 | /** 17 | * 第一种思路解 18 | * @param string 字符串 19 | * @param num 反转的字符个数 20 | */ 21 | void moveTargetNumToRight(char *string, const int num){ 22 | const unsigned int len = strlen(string); 23 | // 分配一个temp,存放更改后的字符串 24 | char *temp = malloc(sizeof(char) * num); 25 | // 将前num个字符移动temp后面 26 | for(int i = 0; i < num; i++){ 27 | temp[len - num + i] = string[i]; 28 | } 29 | for (int i = 0; i < len - num; i++) { 30 | temp[i] = string[num + i]; 31 | } 32 | // 将temp中的数据移到string中 33 | for (int i = 0; i < len; i++) { 34 | string[i] = temp[i]; 35 | } 36 | free(temp); 37 | } 38 | 39 | //第二种思路 40 | //反转:将整个字符串分为两个部分,各自反转后再整体反转就可以实现要求 41 | /** 42 | * 反转字符串 43 | * @param string 需要反转的字符串 44 | * @param start 开始的索引 45 | * @param end 结束的索引 46 | */ 47 | void reverse(char *string, unsigned int start, unsigned int end) { 48 | while (start < end) { 49 | const char temp = string[start]; 50 | string[start] = string[end]; 51 | string[end] = temp; 52 | start++; 53 | end--; 54 | } 55 | } 56 | 57 | /** 58 | * 使用reverse函数进行实现 59 | * @param string 修改的字符串 60 | * @param num 移动的个数 61 | */ 62 | void leftNumToRight(char *string, const int num) { 63 | const unsigned int len = strlen(string); 64 | reverse(string, 0, num - 1); 65 | reverse(string, num, len - 1); 66 | reverse(string, 0, len - 1); 67 | } 68 | 69 | //第三种思路 70 | //暴力解法 71 | /** 72 | * 将字符串的第一个字符移动到末尾 73 | * @param string 字符串 74 | * @param len 字符串长度 75 | */ 76 | void removeToRight(char *string, unsigned const int len) { 77 | const char temp = string[0]; 78 | for (int i = 0; i < len - 1; i++) { 79 | string[i] = string[i + 1]; 80 | } 81 | string[len - 1] = temp; 82 | } 83 | 84 | /** 85 | * 使用removeToRight实现 86 | * @param string 需要修改的字符串 87 | * @param num 移动的个数 88 | */ 89 | void violenceMove(char *string, int num) { 90 | while (num--) { 91 | removeToRight(string, strlen(string)); 92 | } 93 | } 94 | int main(void){ 95 | //测试第一种思路 96 | char arr[] = "abcde"; 97 | moveTargetNumToRight(arr, 2); 98 | printf("first thinking: "); 99 | for (int i = 0; i < strlen(arr); i++) { 100 | printf("%c", arr[i]); 101 | } 102 | printf("\n"); 103 | //测试第二种思路 104 | char brr[] = "abcdefg"; 105 | printf("second thinking: "); 106 | leftNumToRight(brr, 2); 107 | for (int i = 0; i < strlen(brr); i++) { 108 | printf("%c", brr[i]); 109 | } 110 | //测试第三种思路 111 | printf("\n"); 112 | printf("third thinking: "); 113 | char crr[] = "abcdefgh"; 114 | violenceMove(crr, 5); 115 | for (int i = 0; i < strlen(crr); i++) { 116 | printf("%c", crr[i]); 117 | } 118 | return 0; 119 | } 120 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/三种交换方式.md: -------------------------------------------------------------------------------- 1 | ARPAnet 是现在计算机网络的前身。当时的科学家需要解决让分布在各地的计算机互联互通。 2 | 3 | 其实在计算机网络被发明之前就已经存在各式各样的网络了。 4 | 5 | 公元前 5 世纪:邮政网络 6 | 7 | 1830~1960:电报网络 8 | 9 | 1870~1960:电话网络 10 | 11 | 1960:研发计算机网络 12 | 13 | 科学家们在解决这个问题时会考虑到之前的网络是怎样实现从而借鉴解决。 14 | 15 | ## 电路交换 16 | 17 | 通过物理线路的连接动态的分配传输线路资源。当时有接线员后来实现电气化,通过将电话交换机的插孔拨到相应的线路就可以实现通话。 18 | 19 | 电路交换的过程: 20 | 21 | 1、建立连接(试图占用通信资源) 22 | 23 | 2、通信(一直占用通信资源) 24 | 25 | 3、释放连接(归还通信资源) 26 | 27 | **优点:** 28 | 29 | **通信前从主叫端到被叫端建立一条专用的物理通路,在通信时间内两个用户占用这条通路的全部资源,数据直送,数据传输速率高** 30 | 31 | **缺点:** 32 | 33 | **建立和释放连接需要额外的时间** 34 | 35 | **线路被通信双发独占,利用率很低** 36 | 37 | **如果某个线路出现问题,无法灵活的重新分配线路** 38 | 39 | **交换节点并不支持差错控制(无法知道在传输过程中数据是否出现差错)** 40 | 41 | **基于这样的特性,电路交换适合低频次,大量数据的传输。** 42 | 43 | **但是计算机网络上往往是突发式、高频次、少数据量的,所以电路交换不是很适合计算机网络的使用。** 44 | 45 | ## 报文交换 46 | 47 | 存储转发的思想:把传送的数据单元先存储到中间节点再根据目的地址转发到下一节点。 48 | 49 | ![img](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1734770437306-2fcee300-2728-4400-a489-a163372d165f.jpeg) 50 | 51 | **优点:** 52 | 53 | **通信前无需建立连接** 54 | 55 | **数据以报文为单位,被交换节点间存储转发,通信线路分配较灵活** 56 | 57 | **在通信时间内,两个用户无需独占一条物理线路。相比与电路交换,利用率高** 58 | 59 | **交换节点支持差错控制(通过校验技术)** 60 | 61 | **缺点:** 62 | 63 | **报文不定长,不方便存储转发管理** 64 | 65 | **长报文的存储转发时间开销大,缓存开销大** 66 | 67 | **长报文容易出错,重传代价大** 68 | 69 | ## 分组交换 70 | 71 | 和报文交换很类似。将报文的数据分为定长的组,每个组都有头信息(源地址、目的地址、分组号) 72 | 73 | **路由器就是一种分组交换机** 74 | 75 | **优点:** 76 | 77 | **通信前无需建立连接** 78 | 79 | **数据以分组为单位,被交换节点间存储转发,通信线路分配较灵活** 80 | 81 | **在通信时间内,两个用户无需独占一条物理线路。相比与电路交换,利用率高** 82 | 83 | **交换节点支持差错控制(通过校验技术)** 84 | 85 | **除此之外还优化了报文交换中的诸多问题,转发节点需要的时间开销和缓存开销都变小了、重传的代价变小了,分组定长便于存储转发管理** 86 | 87 | **缺点:** 88 | 89 | **相比如报文交换,控制信息变得更多了** 90 | 91 | **相比与电路交换存在存储转发时延** 92 | 93 | **报文被拆分为多个组,在传输过程中可能出现失序,丢失等问题,增加了处理的复杂度** 94 | 95 | ## 虚电路交换技术 96 | 97 | 引入了电路交换的思想和分组交换的思想。 98 | 99 | 在发送数据之前先建立一条虚拟电路,在发送分组数据时按序发送。但是这个不会独占通路 100 | 101 | 但是最终并没有使用这个技术的原因是可以靠终端的强大算力解决分组交换中的各种问题。如果将这些问题给核心部分处理反而得不偿失了。 102 | 103 | ## 三种交换的性能分析 104 | 105 | ### 电路交换性能 106 | 107 | ![img](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735131023292-ce71e98e-df9e-4d6f-aa9d-92a58eadb0da.jpeg) 108 | 109 | 110 | 111 | ### 报文交换性能 112 | 113 | ![img](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735131664482-0d1b710a-076c-456f-af06-187d9577b042.jpeg) 114 | 115 | ### 分组交换性能 116 | 117 | ![img](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735132063754-793fdc18-ff72-43a1-969b-e25c881afcbb.png) 118 | 119 | ## 总结 120 | 121 | | | 电路交换 | 报文交换 | 分组交换 | 122 | | ---------------------- | ------------------------ | -------- | -------- | 123 | | 完成传输所需时间 | 最少(排除建立释放连接) | 最多 | 较少 | 124 | | 存储转发时延 | 无 | 较高 | 较低 | 125 | | 通信前是否建立连接 | 需要 | 不需要 | 不需要 | 126 | | 缓存开销 | 无 | 高 | 低 | 127 | | 是否支持差错控制 | 不支持 | 支持 | 支持 | 128 | | 报文数据是否有序到达 | 是 | 是 | 否 | 129 | | 是否需要额外的控制信息 | 否 | 是 | 是 | 130 | | 线路分配灵活性 | 最低 | 较高 | 最高 | 131 | | 线路利用率 | 低 | 较高 | 最高 | 132 | -------------------------------------------------------------------------------- /数据结构/Ebook/CharacterContains.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | /** 6 | *题目描述 7 | *给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里? 8 | *为了简单起见,我们规定输入的字符串只包含大写英文字母,请实现函数bool StringContains(string &A, string &B) 9 | *比如,如果是下面两个字符串: 10 | *String 1:ABCD 11 | *String 2:BAD 12 | *答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集。 13 | *如果是下面两个字符串: 14 | *String 1:ABCD 15 | *String 2:BCE 16 | *答案是false,因为字符串String2里的E字母不在字符串String1里。 17 | *同时,如果string1:ABCD,string 2:AA,同样返回true。 18 | */ 19 | //第一种思路 20 | //可以维护一个长为26的数组,然后初始化为0,只要String2有的对应ASCII码就为1,就容易判断了 21 | /** 22 | * 维护一个哈希表进行处理 23 | * @param A 长字符串 24 | * @param B 短字符串 25 | * @return A是否包含B 26 | */ 27 | bool StringContains(const char *A, const char *B) { 28 | int letter[26] = {0}; 29 | //得到最终的letter 30 | for (int i = 0; i < strlen(A); i++) { 31 | letter[A[i] - 'A'] = 1; 32 | } 33 | //查询letter 34 | for (int i = 0; i < strlen(B); i++) { 35 | if (letter[B[i] - 'A'] == 0) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | //第一种思路的优化:使用计算机中的bit代替数组 43 | bool StringContainBetter(const char *A, const char *B){ 44 | int hash = 0; 45 | for (int i = 0; i < strlen(A); ++i) 46 | { 47 | hash |= (1 << (A[i] - 'A')); 48 | } 49 | for (int i = 0; i < strlen(B); ++i) 50 | { 51 | if ((hash & (1 << (B[i] - 'A'))) == 0) 52 | { 53 | return false; 54 | } 55 | } 56 | return true; 57 | } 58 | //第二种思路 59 | //将A、B两个字符串中的字母进行排序然后进行依次比较,但是这种方法显然是没有上面的好的 60 | // 比较函数,用于 qsort 61 | int compareChars(const void *a, const void *b) { 62 | return (*(char *)a - *(char *)b); 63 | } 64 | // 排序函数 65 | void sort(char *string) { 66 | const size_t len = strlen(string); 67 | qsort(string, len, sizeof(char), compareChars); 68 | } 69 | bool sortStringContains(const char *A, const char *B) { 70 | char *sortedA = strdup(A); 71 | char *sortedB = strdup(B); 72 | 73 | if (sortedA == NULL || sortedB == NULL) { 74 | // 处理内存分配失败 75 | free(sortedA); 76 | free(sortedB); 77 | return false; 78 | } 79 | 80 | // 对字符串 A 和 B 进行排序 81 | sort(sortedA); 82 | sort(sortedB); 83 | 84 | // 使用两个指针进行比较 85 | int i = 0, j = 0; 86 | 87 | while (i < strlen(sortedA) && j < strlen(sortedB)) { 88 | if (sortedA[i] == sortedB[j]) { 89 | j++; // 找到 B 中的字符,移动 B 的指针 90 | }else { 91 | i++; 92 | } 93 | } 94 | 95 | // 如果 j 达到 sortedB 的长度,说明 B 中的所有字符都在 A 中 96 | const bool result = (j == strlen(sortedB)); 97 | 98 | // 释放内存 99 | free(sortedA); 100 | free(sortedB); 101 | return result; 102 | } 103 | 104 | //第三种思路 105 | //采用暴力破解:循环遍历每一个字符并进行判断 106 | 107 | int main(void){ 108 | //测试第一种思路 109 | const char A[] = "ABCDE"; 110 | const char B[] = "CEF"; 111 | printf("first thinking: "); 112 | if (StringContains(A, B)) { 113 | printf("A contains B"); 114 | }else { 115 | printf("A don't contains B"); 116 | } 117 | 118 | printf("\n"); 119 | //测试第二种思路 120 | printf("second thinking: "); 121 | const char C[] = "ABCDE"; 122 | const char D[] = "CE"; 123 | if (sortStringContains(C, D)) { 124 | printf("C contains D"); 125 | }else { 126 | printf("C don't contains D"); 127 | } 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /计算机组成原理/Exercise/TwoChapter.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 王道课后习题 3 | *第二章:线性表 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | /** 10 | * 第一题 11 | */ 12 | int findMin(int* arr, int *len) { 13 | if (arr == NULL) { 14 | fprintf(stderr, "arr is the NULL"); 15 | exit(1); 16 | } 17 | int min = arr[0]; 18 | int index = 0; 19 | for (int i = 0; i < *len; i++) { 20 | if (arr[i] < min) { 21 | index = i; 22 | min = arr[i]; 23 | } 24 | } 25 | for (int i = index; i < *len; i++) { 26 | arr[i] = arr[i + 1]; 27 | } 28 | (*len)--; 29 | return min; 30 | } 31 | void minePrintf(const char *info) { 32 | printf(info); 33 | printf("=========================================================="); 34 | printf("\n"); 35 | } 36 | 37 | /** 38 | * 第二题 39 | */ 40 | void reserver(int *arr, const int len) { 41 | int first = 0; 42 | int end = len - 1; 43 | while (first < end) { 44 | const int temp = arr[first]; 45 | arr[first] = arr[end]; 46 | arr[end] = temp; 47 | first++; 48 | end--; 49 | } 50 | } 51 | 52 | /** 53 | * 第三题 54 | */ 55 | void deleteSample(int *arr, int *len, const int x) { 56 | int k = 0; 57 | for (int i = 0; i < *len; i++) { 58 | if (arr[i] != x) { 59 | arr[k++] = arr[i]; 60 | } 61 | } 62 | (*len) = k; 63 | } 64 | 65 | /** 66 | * 第四题 67 | */ 68 | void deleteRange(int *arr, const int t, const int s, int *len) { 69 | if (arr == NULL) { 70 | fprintf(stderr, "arr is the NULL"); 71 | exit(1); 72 | } 73 | if (t >= s) { 74 | fprintf(stderr, "s must bigger than t"); 75 | exit(1); 76 | } 77 | int k = 0; 78 | for (int i = 0; i < (*len); i++) { 79 | if (arr[i] > s || arr[i] < t) { 80 | arr[k++] = arr[i]; 81 | } 82 | } 83 | (*len) = k; 84 | } 85 | int main(void){ 86 | //第一题测试 87 | int arr[5] = {3, 1, 4, 5, 7}; 88 | int len = 5; 89 | minePrintf("fist test:"); 90 | printf("the min number is %d\nthe arr is :", findMin(arr, &len)); 91 | for (int i = 0; i < len; i++) { 92 | printf("%d ", arr[i]); 93 | } 94 | printf("\n"); 95 | //第二题测试 96 | minePrintf("second test:"); 97 | int brr[5] = {3, 1, 4, 5, 7}; 98 | printf("the origin brr is : 3, 1, 4, 5, 7\n"); 99 | reserver(brr, 5); 100 | printf("the result brr is : "); 101 | for (int i = 0; i < 5; i++) { 102 | printf("%d, ", brr[i]); 103 | } 104 | printf("\n"); 105 | minePrintf("third test: "); 106 | int crr[5] = {12, 4 , 4, 21, 4}; 107 | int len2 = 5; 108 | printf("the origin crr is : 12, 4 , 4, 21, 4\n"); 109 | deleteSample(crr, &len2, 4); 110 | printf("the result brr is : "); 111 | for (int i = 0; i < len2; i++) { 112 | printf("%d, ", crr[i]); 113 | } 114 | printf("\n"); 115 | minePrintf("fourth test: "); 116 | int drr[5] = {12, 22, 23, 44, 56}; 117 | int len3 = 5; 118 | printf("the origin drr is : 12, 22, 23, 44, 56\n"); 119 | deleteRange(drr, 22, 44, &len3); 120 | printf("the result is : "); 121 | for (int i = 0; i < len3; i++) { 122 | printf("%d, ", drr[i]); 123 | } 124 | printf("\n"); 125 | return 0; 126 | } -------------------------------------------------------------------------------- /计算机网络/王道笔记/应用层/万维网和HTTP协议.md: -------------------------------------------------------------------------------- 1 | # WWW 的概念与组成 2 | 万维网(WWW)是一个分布式,联机的信息存储空间。在这个空间中: 3 | 一样有用的事物称为一样资源,并由一个全域统一资源定位符 URL 标识。这些资源通过超文本传输协议(HTTP)传输给使用者,而后者通过单击链接来获取资源。 4 | 5 | 由此可以知道的是,万维网使用链接的方法让用户可以很方便地从一个站点访问另一个站点,从而主动地获取到丰富的信息。展示内容页面的话,使用的是超文本标记语言(HTML)。 6 | 7 | 万维网的内核部分是由三个标准构成的: 8 | 9 | + 统一资源定位符(URL):负责标识万维网上的各种文档,并使每个文档在整个万维网上的范围内具有唯一的标识符 URL 10 | + 超文本传输协议(HTTP):一个应用层的协议,它使用 TCP 连接进行可靠的传输,HTTP 是万维网客户程序和服务器程序之间交互必须遵守的协议 11 | + 超文本标记语言(HTML):一种文档结构的标记语言,它使用一些约定的标记对页面上的各种信息、格式进行描述 12 | 13 | URL 的格式就是浏览器上搜索的网址:具体格式是: 14 | 15 | <协议>://<主机>:<端口>/<路径>。如我的这篇文档的 URL 为: 16 | 17 | > [https://www.yuque.com/yuqueyonghu71tocz/kmswnx/gprx7cqauvpg6bw2](https://www.yuque.com/yuqueyonghu71tocz/kmswnx/gprx7cqauvpg6bw2) 18 | > 19 | 20 | WWW 也是采用的 C/S 模式进行工作,我们使用的浏览器就是运行在本地的万维网客户程序,而万维网文档所在的主机则运行服务端程序。 21 | 22 | 工作流程如下:我们在 Java 那里使用的 thy 技术就是这样的撒 23 | 24 | 1. Web 用户使用浏览器(指定 URL)与 Web 服务器建立连接,并发送浏览请求 25 | 2. Web 服务器把 URL 转换为文件路径,并返回信息给 Web 浏览器 26 | 3. 通信完成,关闭连接 27 | 28 | 万维网是无数个网络站点和网页的集合,他们在一起构成了因特网最主要的部分(因特网还包括电子邮件、Usenet 和新闻组)。 29 | 30 | # 超文本传输协议(HTTP) 31 | HTTP 定义了浏览器(万维网客户进程)怎样向万维网服务器请求万维网文档,以及服务器怎样把文档传送给浏览器。从层次的角度看,HTTP 是面向事务的应用层协议,其规定了在浏览器和服务器之间的请求和响应的格式与规则,是万维网能够可靠地交换文件的重要基础。对于我们 IT 工作人员,在日常开发和作业时也少不了和 HTTP 打交道,所以还是非常清楚的。 32 | 33 | ## HTTP 的操作过程 34 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740378158903-4d13e485-1b5e-4456-969e-27806471abda.jpeg) 35 | 36 | 在服务器端有个服务器进程会持续的监听 TCP 的 80 端口(HTTP 协议的默认端口),当监听到连接请求后便于浏览器建立 TCP 连接。然后,浏览器会向服务器发送请求以获取某个 Web 页面。服务器接收到请求后,将构建所请求 Web 页面的必要信息,并通过 HTTP 响应返回给浏览器。浏览器经过解析就会展示给用户看。最后,TCP 连接释放。 37 | 38 | ## HTTP 的特点 39 | HTTP 建立在 TCP 协议之上,保证了数据传输的可靠性。HTTP 不需要考虑数据在传输过程中被丢弃后又怎样被重传,TCP 会实现的。 40 | 41 | + HTTP 是无连接的:这个是说在交换 HTTP 报文前不需要建立 HTTP 连接。虽然使用了 TCP 连接。 42 | + HTTP 是无状态的:就是说服务器不会记住来连接的客户,不管如何访问都是和第一次访问时的页面相同。 43 | 44 | 但是这个和我们熟知的那些网站有些不同呀,使用的京东呀,淘宝呀都知道我们买了什么商品,浏览了什么商品呀,这不是有记忆吗?这些是通过一些技术手段实现的,原理是使用一个唯一标识标识用户,然后将用户的行为通过后端记录到数据库中,下次来访问我的服务时,我就可以通过唯一标识来知道你干过什么啦,这里面比较出名的技术就是 Cookie 技术。 45 | 46 | 47 | HTTP 协议既可以使用非持续连接(HTTP/1.0)又可以使用持续连接(HTTP/1.1)。 48 | 49 | 对于非持续连接,每个网页元素对象(如图片、Flash)的传输都需要单独建立一个 TCP 连接。这样的机制会导致服务器压力暴增,所以浏览器通常会建立多个并行的 TCP 以同时请求多个对象。 50 | 51 | 52 | 53 | 对于持续连接,是指万维网服务器在发送响应后仍然保持这条连接,使同一个客户和该服务器可以继续在这条 TCP 连接上传送后续的 HTTP 请求报文和响应报文。 54 | 55 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740379635895-deb59013-e9a6-4299-80ce-f1c49efd13bb.jpeg) 56 | 57 | 可以看到这个持续连接展示的是非流水线的,就是说虽然可以使用这条 TCP,但是呢,只是说不需要连接了,该请求还是得请求,因此这个还是有点浪费服务器资源,因为需要等待下一个请求的到来。 58 | 59 | 当然咯,有非流水线的就肯定有流水线的,而流水线的就解决了这个问题。流水线的允许客户一次性发送多个请求,“ 把你要的全部告诉我,我直接全部给你就完了“,但是因为下面有个 TCP,所以每个 RTT 内传送的数据量还要收到 TCP 发送窗口的限制,也就不能向理想那样为所欲为了。 60 | 61 | 62 | 63 | ## HTTP 的报文结构 64 | HTTP 是面向文本,因此报文中的每个字段都是一些 ASCII 码串,并且每个字段的长度是不确定的。HTTP 报文类型一共分为两种: 65 | 66 | + 请求报文:从客户向服务器发送的请求报文 67 | + 响应报文:从服务器到客户的回答报文 68 | 69 | ![](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740380099927-b58d1f4a-0c35-4967-87cf-8f2325aaef16.jpeg) 70 | 71 | 了解一下就可以了,也可以自己到浏览器上去请求一下,使用控制台观察观察🤔[此处为语雀卡片,点击链接查看](https://www.yuque.com/docs/207110514#aF8Ym) 72 | 73 | 74 | 75 | ## HTTP 请求报文举例 76 | 在以太网数据帧中,各个部分代表的意义: 77 | 78 | + 第 1~6 字节是目的 MAC 地址(默认网关地址) 79 | + 第 7~12 字节是本机的 MAC 地址 80 | + 第 13~14 字节是类型字段 81 | + 第 15~34 字节是 IP 数据报的首部 82 | + 第 27~30 字节是源 IP 地址 83 | + 第 31~34 字节是目的 IP 地址 84 | + 第 35~54 字节是 TCP 报文段的首部 85 | 86 | 以下是从图片中提取并整理的文本信息: 87 | 88 | # 常见的应用层协议 89 | | 应用程序 | FTP 数据连接 | FTP 控制连接 | TELNET | SMTP | DNS | TFTP | HTTP | POP3 | SNMP | 90 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | 91 | | 使用协议 | TCP | TCP | TCP | TCP | UDP | UDP | TCP | TCP | UDP | 92 | | 熟知端口号 | 20 | 21 | 23 | 25 | 53 | 69 | 80 | 110 | 161 | 93 | 94 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/局域网与IEEE 802.md: -------------------------------------------------------------------------------- 1 | **IEEE **是电气电子工程师学会,是一个国际组织。 2 | 3 | 在它下面派分了 IEEE802 委员会 4 | 5 | 在 IEEE802 委员会之下还存在着许多工作组,包括 802.3 工作组(搞以太网技术的)、802.11 工作组(搞 WiFi 技术的) 6 | 7 | 8 | 9 | ## 局域网 10 | 特点: 11 | 12 | + 覆盖较小的物理范围 13 | + 较低的时延和误码率 14 | + 局域网内的各节点之间是以帧为单位进行传输 15 | + 支持单播、多播、广播 16 | 17 | 怎样确定目标地址呢?这个就需要使用到 mac 地址,每个节点都会拥有自己的 mac 地址全球唯一,48bit 18 | 19 | 对于局域网可以分为有线局域网和无线局域网 20 | 21 | 对于局域网中需要关注的要点有三条: 22 | 23 | 24 | + 拓扑结构 25 | + 传输介质 26 | + 介质访问控制方式(采用什么协议) 27 | 28 | 29 | 30 | ## 有线局域网 31 | ### 令牌环网 32 | 它的三个要点分别是 33 | 34 | 35 | + 环形结构 36 | + 同轴电缆或双绞线 37 | + 令牌传递协议 38 | 39 | 40 | 41 | ### 以太网 42 | 这个是有 IEEE 委员会下的 802.3 工作组在搞的,物理层采用曼彻斯特编码 43 | 44 | #### 同轴电缆以太网 45 | 它的三个要点分别是 46 | 47 | 48 | + 总线型 49 | + 同轴电缆(可使用中继器连接多个同轴电缆的网段) 50 | + CSMA/CD 协议 51 | 52 | 53 | 54 | #### 双绞线以太网 55 | 这个又分化为两种方向一个是使用集线器连接另一个是使用交换机连接 56 | 57 | 一根双绞线只能连接两个节点 58 | 59 | ##### 集线器连接 60 | 它的三个要点分别是 61 | 62 | + 物理上星型,逻辑上总线型 63 | + 双绞线 64 | + CSMA/CD 协议 65 | 66 | 这个只能是半双工形式的 67 | 68 | ##### 交换机连接 69 | + 物理上和逻辑上都是采用的星型结构 70 | + 双绞线 71 | + CSMA/NULL 72 | 73 | 这个在半双工形式下是采用全双工形式的,全双工形式下是采用 NULL 的 74 | 75 | #### 光纤以太网 76 | 它的三个要点分别是 77 | 78 | + 点对点(用于中继器、集线器、交换机之间的传输,也就是说通常不会直接连接终端节点) 79 | + 光纤 80 | + NULL(用两条光纤实现全双工通信) 81 | 82 | ## 无线局域网 83 | 它的三个要点分别是 84 | 85 | + IEEE 802.11 定义为星形(1 个 AP + 多台移动设备) 86 | + 无线 87 | + CSMA/CA 协议 88 | 89 | ## 网络适配器 90 | 网络适配器中存在一个 ROM 和一个 RAM 91 | 92 | 其中 ROM 中刻有全球唯一的物理地址 93 | 94 | RAM 用于缓冲 95 | 96 | 有网络适配器和网线适配器,有些电脑支持直接插入网线就是插入的网线适配器。有些电脑不能插入,就要通过一个转换器转换,我的电脑就是这样的,一个转换器中也有一个 ROM 和 RAM 97 | 98 | 需要完成的功能有: 99 | 100 | + 将帧发送到局域网上 101 | + 将局域网上的帧进行接受 102 | + 需要根据接入的局域网类型,按照标准实现 数据链路层 + 物理层 103 | + 需要完成数据的串/并行转换 104 | + 需要支持帧缓冲 105 | 106 | ## 以太网和 802.3 107 | 高速以太网:速率大于 100Mbps 108 | 109 | 需要注意是全双工还是半双工 110 | 111 | 只要使用 **同轴电缆 **就只能是半双工模式 112 | 113 | 使用 **双绞线 **的话在速率小于 2.5Gbps 就可以支持全双工或者半双工(这个需要看连接的是集线器还是交换机了,前者只能是半双工,后者可以根据连接的机器进行协商),但速率只要大于等于 2.5Gbps 的话就只能支持全双工了 114 | 115 | **光纤** 就只支持全双工 116 | 117 | 118 | 119 | ### V2 标准的以太网 MAC 帧 120 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1737989414238-f1f66b66-f65f-4dd9-ae6e-77b95131ecc6.jpeg) 121 | 122 | 上面的图中说明网络层会将 ip 数据报交给 mac 层让其封装成帧再交给物理层传输给下一个节点。 123 | 124 | 然后在数据部分最少要 46 字节如果少了需要进行填充,如果大了需要进行分片 125 | 126 | **所以这个 MAC 帧的最小长度为 64 字节最长为 1518 字节** 127 | 128 | 这个同步码就是 7 字节的 1010101...,为了给要发送的目的节点一个节奏,然后帧开始定界符在末尾是连续的两个 1 表示要开始传输数据了。 129 | 130 | 然后在一个帧的最后采用违规编码并会在下一个帧传输前预留一段时间 131 | 132 | ### 802.3 标准 133 | 字节的个数和上面的一致只是说 2 字节的不再表示协议类型而是数据长度 134 | 135 | ### 单播、广播的传播 136 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1738059018993-f4c6b607-ecc2-4705-9221-83fe09cee867.png) 137 | 首先需要知道一些知识: 138 | 139 | 1、 集线器是在物理层的没有 mac 地址的概念。 140 | 141 | 2、交换机是在物理层和数据链路层的存在 mac 地址的概念 142 | 143 | 3、路由器是在 ip 层及下层存在 mac 地址和 ip 地址 144 | 145 | 4、集线器在转发时会将数据转发到所有端口上 146 | 147 | 5、交换机聪明一点知道转发到的端口 148 | 149 | 如果在目的地址中全填 1 则代表广播帧会将其传播给局域网中的所有主机。 150 | 151 | **使用上面的例图来说明一下:** 152 | 153 | + **单播帧:** 154 | 155 | **1.A->C:只有 C 会收到并接受帧** 156 | 157 | **2.A->F:E、F、G 都会收到数据帧但只有 F 会接受** 158 | 159 | **3.E->A:F、G、A 都会收到数据帧但只有 A 会接受** 160 | 161 | **4.F->E:E、G 都会收到数据帧但只有 E 会接受** 162 | 163 | + **广播帧:** 164 | 165 | **1.局域网内任意一个节点发出广播帧其余节点都会接受** 166 | 167 | **需要说明的是广播帧只在一个局域网内,路由器并不会将其转发到其他网络中,所以只有一个局域网才属于一个广播域** 168 | 169 | 这里可以提一下之前的那个题:冲突域和广播域个数的题 170 | 171 | 判断冲突域(两个节点发送数据是否会冲突)的个数就是看集线器的个数,因为交换机可以隔离冲突域。 172 | 173 | 判断广播域就是看路由器,路由器两侧的网络属于不同的局域网。 174 | 175 | -------------------------------------------------------------------------------- /计算机网络/小林coding/将数据包发送到网络世界的流程.md: -------------------------------------------------------------------------------- 1 | # 浏览器中输入网址后到点击回车后其中发生的事情 2 | 3 | 首先浏览器做的事情就是解析这个url网址,采用的协议名称、服务器名称、端口号、访问资源在主机的具体位置 (可以为空, 4 | 这样服务器会默认index.html或default.html)。解析后会生成请求消息,包括请求方法(Get、Post等)、ip、 5 | 版本和携带的数据,但是url里放入的是域名,所以需要DNS的帮助,这个可以根据 域名去查询对应的ip地址,具体的实现方法也十分 6 | 的简单:先查看浏览器自己有没有这个url的缓存,没有的话就去问OS有没有,还没有的话还要再去host文件里看 看有没有缓存。 7 | 这样的设计可以让我们避免每次访问url都去询问DNS服务器,提高 效率。那如果上述的地方都没有域名对应的ip怎么办呢,这就需要 8 | DNS服务器了,用户的Socket库中的方法向电脑中配置的DNS服务器发送请求:问DNS服务器有没 有这个域名的ip,有的话就直接返回给 9 | 你的电脑了,没有的话就要向根DNS服务器 (. 就是这个,一个小点点),根DNS服务器会告诉你去哪里问可以得到结果,就是顶级DNS 10 | 服务器,于是你的电脑又发个请求给顶级DNS服务器,然后顶级服务器 又会告诉你到对应的权威DNS服务器中去找,于是你的电脑又发送 11 | 一个请求到权威DNS服务器,这下终于找到了,权威DNS服务器告诉你域名对应的ip。 12 | 13 | # 协议栈 14 | 这下准备工作终于做完了。然后浏览器就会委托OS发送数据,调用Socket库去使用协议栈进行数据的处理 和发送。协议栈有两部分: 15 | 上部分是接受浏览器传过来的数据(TCP、UDP),下部分是进行数据的发送(IP)。经过协议栈后让网卡驱动程序加上mac帧就可以通过 16 | 网卡发送到以太网中了。在数据的发送(IP)中有两个协议:ARP协议和ICMP协议,前者已经说过了,后者是告知数据包在传输过程中的 17 | 错误和控制信息的。 18 | 19 | # TCP协议 20 | HTTP是基于TCP协议的协议,那我们先来看看TCP是怎么回事吧。不外乎就是在要发送的数据前面加上了TCP头,那这个头里具体有什么呢? 21 | 首先是源端口号和目标端口号,需要知道是那两个应用要进行数据交互吧。还有序号,这个是防止包乱序的情况发生,还有确认号,这是判 22 | 断数据包是不是被接受了的,没有正确地接受就可以进行重传,防止了包丢失的问题,还有一些状态位如SYN、ACK、RST、FIN,TCP是面 23 | 向连接的,发送的状态位发生变化会导致连接状态发生变化。还有个重要的概念就是滑动窗口,控制流量的,不要在一段时间内发送的数据 24 | 太多了,也不要太少了。还可以进行拥塞控制。在使用HTTP发送数据前会先进行TCP的三次握手,确保双方都能进行正确的数据传输。 25 | **具体的过程是:** 26 | 开始的时候服务器端和客户端都是处理关闭(close)状态的,然后需要服务器监听一个端口等待着客户端(此时服务器处于listen状态) 27 | 在服务器监听的时候客户端发起连接请求SYN就处于SYN_SEND状态了,服务器收到这个请求后,就向客户端发送一个SYN包和一个ACK包 28 | 此时服务器处于SYN_RECV状态,前者用于确认连接,后者用于确认服务器收到请求。然后客户端收到这个包后,就向服务器发送一个ACK包, 29 | 然后处于ESTABLISHED状态,服务器收到客户端发送的ACK包后也处于ESTABLISHED状态,此时客户端就可以发送数据给服务器了。 30 | 通过这样的方式可以让客户端和服务器端都知道对方是可以发送和接受数据的。 31 | 32 | # IP协议 33 | TCP层所进行的操作都需要委托IP层将数据封装为网络包并传输给通信对象。IP协议中必然是有源IP地址和目的IP地址的,还有协议号,因为 34 | HTTP协议是基于TCP协议,所以协议号就是TCP协议(06)。如果一台机器有多个网卡,那怎样确定源IP地址呢?就需要将目标IP地址和网卡 35 | 的子网掩码作与操作,如果结果和Destination的IP地址相同,那就确定是这个网卡了。如果所有的网卡都匹配不上的话会最终匹配到默认网关(0.0.0.0)。 36 | 这个的子网掩码和Destination都是括号中的值,网关IP就是gateway的值。然后就可以将网络包发送到网关了。 37 | · 38 | # 两点传输 MAC 39 | 在网络层下,还会在IP协议上再添加一个MAC头部。包含了源MAC地址、目的MAC地址、协议类型(0x0800,表示IP协议)发送方的MAC地址容易得到 40 | 是在生产时写在网卡的ROM中的,那接收方的MAC地址应该从何而知呢。这是就需要ARP协议了,通过IP地址知道要发送的数据是到某个子网中,通过 41 | 广播的形式去询问这个IP的主机的MAC地址是什么,是这个IP的主机就会回答的自己的MAC地址,这样就可以放入目的MAC地址中了。但是想一想每次 42 | 这样询问MAC地址浪费的资源和时间真的有点多了,特别对于我这种有强迫症的人来说,这样的设计也令人难受。其实这里是有MAC缓存的,在得到了MAC 43 | 地址后,OS会将该IP地址和对应的MAC地址缓存起来,下次再遇到这个IP地址的时候,缓存中有的话直接从缓存中取出对应的MAC地址,不用再向网关发送 44 | 请求了。不存在的话再进行ARP请求。这样的设计合理太多了,在SpringBoot的项目中也常常用到这种思想。 45 | 46 | # 网卡 47 | 通过上面各层的努力,终于到达了出口了。数据包在内存中是以0、1的形式存在的,要发送的话是需要先将其转换为二进制数据(电信号),然后再发送。 48 | 这些操作是需要网卡来操作的,而要控制网卡是需要网卡驱动程序的。在网卡驱动获取到数据包后,会将其放入到网卡的缓存区中,然后会在包头加上报头 49 | 和起始帧分界符,在包尾加上帧校验帧序列(FCS 用于检测数据包是否损坏)。最后网卡将其转换为电信号并发送给网线。 50 | 51 | # 交换机 52 | 数据包通过网卡的发送会来到交换机这里,交换机的设计是为了将网络包原样转发到目的地。因为交换机工作在MAC层,因此又称其为二层网络设备。 53 | 那交换机具体是怎样工作的呢?首先,电信号到达了网线接口,交换机里的模块进行接受,然后交换机会将电信号转换为数字信号,这样才能识别信息呀。 54 | 再根据FCS校验,看看包在传输过程中有没有问题,没有的话就将其放入到缓冲区里。注意交换机是不会核对MAC地址的,所以不管是不是需要交换机接受的 55 | 只要到了这里就跑不了了,统统接收。由此可知,交换机的端口是没有MAC地址的。 56 | 将包放入到缓存区中后,就需要检查需要发送的目的MAC地址是不是已经存在MAC地址表中,交换机的MAC地址表主要是设备的MAC地址和端口(发送数据)。 57 | 如果在MAC地址表中找不到的话,交换机就会将数据发往除了源端口之外的所有端口,在目标设备做出响应后就将其存到MAC地址表中。除了这种情况外,如果 58 | 接收方的MAC地址是一个广播地址的话,也会这样发送包。(MAC:FF:FF:FF:FF:FF:FF,IP:255.255.255.255) 59 | 60 | # 路由器 61 | 数据包表示终于到了边境了。在经过交换机发送后到达了路由器,并在这里将其转发到下一个路由器或者设备。这个地方和交换机是相似的,也是通过查表进行 62 | 转发的,只不过在具体实现的操作中存在区别。 63 | 因为路由器是基于IP设计的,故又称为三层网络设备,路由器的各个端口都具有MAC地址和IP地址。而交换机是基于以太网设计的,故又称为二层网络设备并且 64 | 交换机的端口是不具备MAC地址的。因为路由器具有MAC地址,所以它可以成为以太网的发送方和接受方;同时具备IP地址,从这里来看挺像网卡呢。 65 | **当转发包时**:路由器会先接受发给自己的以太网包,然后通过路由表进行查询转发目标,再由相应的端口作为发送端口将以太网包发送出去。 66 | **当接受包时**:首先电信号到达网线接口,路由器中的模块会将电信号转换为数字信号,然后根据FCS进行校验,如果没有错误就检查MAC头部中的目表MAC地址 67 | ,确认是不是自己应该接受的包,是的话就将其放入到缓冲区中,否则就就丢弃这个包。在完成包的接受操作后,路由器就会去掉包开头的MAC头部(因为之前携带 68 | MAC地址的作用就是为了找到这个路由器),路由器会根据包携带的IP进行包的转发操作,这个跟交换机的实现有点像:使用目标IP和路由器中的子网掩码做与运算 69 | 得到的结构和Destination作比较如果相等就可以将其作为转发目标,如果是在是找不到会有个默认路由。 70 | OK,现在找到了可以发送的目标了,进行发送阶段。首先我们需要根据路由表的网关列判断对方的地址,如果网关是一个IP地址,则这个IP地址就是我们需要转发的 71 | 目标地址,仍没有到达终点还需要进行转发。如果网关为空,则说明IP头部中的接受方IP地址就是要转发的目标地址,也就是最终点。 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/随机访问介质访问控制.md: -------------------------------------------------------------------------------- 1 | ## 知识总览 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736750446436-bf37d68b-545b-4477-9062-bb7df4cad3ac.jpeg) 3 | 4 | 随机访问介质的意思是在信道的上的各个节点可以随机的发送数据 5 | 6 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736689533041-f8dca0ae-b58a-42a7-9871-d02109c21852.jpeg) 7 | 8 | ## ALOHA 协议(在夏威夷语中有 爱、同情等意思) 9 | ### 纯 ALOHA 协议 10 | 只要节点准备好数据帧就可以直接发送。 11 | 12 | 因为在数据传输的过程可能受到其他节点的干扰所以有一个收到 ACK的机制:一个节点在发送一个数据帧后会等待接收放返回一个 ACK,如果超时没有返回的话就会重新传输,出现冲突的话就会随机等待一个时间再发送数据帧,否则继续准备下一个数据帧。 13 | 14 | 为什么会使用随机时间呢?因为如果是固定的时间的话,在一次冲突后后面的必然是冲突。 15 | 16 | ### 时隙 ALOHA 协议 17 | 时隙 ALOHA 协议的时隙大小固定为传输一个数据帧的时间大小,只有在每个时隙开始时才发送数据帧 18 | 19 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1736690416902-414527d0-75ee-4ba1-bb47-190936512b84.png) 20 | 21 | 这样的话会降低发生冲突的概率,避免了用户的随意性,提高了信道的利用率。上面的纯 ALOHA 协议只要准备好就发送太容易碰在一起了。当然也是可能发生冲突的,解决方案跟上面的一样,发生冲突各个节点随机等待一段时间再发送就可以了 22 | 23 | ## CSMA 协议(载波监听多路访问协议) 24 | 是对 ALOHA 协议的升级,每次在发送之前先监听信道是否空闲,只有在信道是空闲的时候才进行发送。 25 | 26 | ### 1-坚持 CSMA 协议 27 | A 节点在发送数据,B 节点的数据帧已经准备好了,B 会一直监听这个信道是否有数据传输。一旦监听到没有则发送自己的数据帧。这就叫“1-坚持 CSMA 协议”,这里的“坚持”就体现出来了。当然还是可能发送冲突的,解决方法还是等待随机时间再重新发送 28 | 29 | 这个的优点就是信道利用率高,信道一旦空闲就马上有下一个节点继续使用,当然缺点也是非常明显的,就是如果有多个节点准备好数据的话,发生冲突的概率很大。为了解决这个问题就提出了非坚持 CSMA 协议 30 | 31 | ### 非坚持 CSMA 协议 32 | A 节点在发送数据帧,然后 B、C 节点准备好了数据帧准备发送,先监听信道上有没有数据,有的话就不再监听并在随机时间后再次监听信道,没有数据传输才进行发送,这样的做法将各个节点发送数据帧错开了,降低了发生冲突的概率了。 33 | 34 | 那么优点就是当多个节点的数据都已经准备好时,如果信道不空闲,则各节点会随机推迟一段时间再尝试监听,从而使各节点错开发送时机,降低冲突概率。缺点就是在信道刚恢复空闲时,可能不会立即利用,导致信道利用率低。 35 | 36 | ### p-坚持 CSMA 协议 37 | 这个呐就是前两个的折中表现,A 节点发送数据帧,B、C 节点准备好了数据帧然后监听信道,如果有数据,就一直监听,直到没有数据此时各节点发送数据帧的概率为 p,就是说有 p 的概率直接就发送了,还有 1-p 的概率推迟随机时间后再发送数据帧。这样相较于非坚持 CSMA 协议提高了信道利用率,相较于 1-坚持 CSMA 协议降低了冲突概率。 38 | 39 | ### CSMA/CD 协议(难点且重点) 40 | 用于早期的有线以太网,在 CSMA 监听的特性上再加上冲突检测的特性 41 | 42 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736906633470-92849964-663b-44a6-8505-520f068544af.jpeg) 43 | 44 | A 节点发送数据帧,B 节点准备好了数据帧,但是此时信道上存在数据,所以会坚持监听,直到没有数据在信道上了后立即发送数据帧并且会边发送边监听信道上有没有其他节点发送数据,如果有的话 B 节点会停止发送数据帧,随机等待时间后再次发送。如果没有检测到其他节点发送数据则认为没有冲突为发送成功。 45 | 46 | **现在需要重点了解的是如果发送冲突应该怎样解决:** 47 | 48 | 在 B 节点发送数据帧后,C 节点的数据帧也准备好了数据帧,此时 C 节点检测信道,因为 B 发送的数据还没有到达 C 节点,所以 C 节点认为此时信道上是没有数据的,也发送了数据帧,但是发送了一小会儿 B 发送的数据到达了 C 节点,因为 C 节点会一直监听信道,所以在 C 节点检测到了 B 节点发送的数据后马上停止发送数据帧并检测是第几次发送冲突,设发生冲突的次数为 k,若 k<16 就在 [0,2^k - 1] 中随机选择一个值 r 并乘以争用期就可以得到一个随机等待的时间,在这里 k=10 是一个分水岭,k 大于 10 的话 k 还是以 k=10 选取随机数 r。若 k>16 则报告上级表示实在是太拥堵了。 49 | 50 | #### 争用期 51 | **争用期=2 x 最大单向传播时延** 52 | 53 | 在争用期内没有检测到冲突则表明这个节点不会再遇到冲突了,那么这是为什么呢? 54 | 55 | 假设: 56 | 57 | A 在最左侧,B 在最右侧,在 A 发送数据帧后还差一点点时间就到了 B 但总归还差了点,就在这一点点时间里 B 节点发送了数据帧,马上 B 节点就接收到了 A 节点传输过来的数据,此时 B 就不用再传输数据了。但是 A 节点此时并不知道发生冲突了,因为 B 节点传输的数据还需要一个单向传播时延,这种情况是最极限的情况了,除此之外的所有情况检测到冲突的时间都比这个少。而且只要 A 节点的数据帧到达了 B 节点时信道就已经被 A 节点的数据占满了,其他节点在监听时也会知道不能发送数据帧,所以说在争用期内没有发生冲突就不会再发生冲突了。 58 | 59 | #### 最短帧长 60 | **最短帧长=2 x 最大单向传播时延 x 信道带宽;若收到的帧小于最短帧长则视为无效帧** 61 | 62 | 这个是为了避免节点没有检测到冲突但实际上发生了冲突的情况 63 | 64 | 假设: 65 | 66 | A、B 位置如上假设 67 | 68 | A 节点发送的数据帧长度比最短帧长还短的话,A 发送的数据帧到达 B 节点需要 30us,在 29us 时 B 节点发送数据帧,那么 A 发送数据帧发送完了,B 的数据才到 A 节点,这样的话 A 会认为是没有冲突的但是实际上已经发生冲突了。 69 | 70 | #### 最长帧长 71 | 规定最长帧长可以防止某些节点一直占用信道。防止某个节点构造一个超级长的数据帧,这样这个信道就会被这个节点一直占用了。 72 | 73 | ### CSMA/CA 协议(难点但不是重点) 74 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1736943970017-3458bfae-3f28-4ede-a245-22ecb3d89432.jpeg) 75 | 76 | 这个重点是避免冲突但不会完全避免。 77 | 78 | DIFS(分布式协调 IFS):最长的帧间间隔 79 | 80 | SIFS:最短的帧间间隔,预留 SIFS 用于处理收到的帧(这个是 AP(发送信号灯的器件)进行预留的时间,比如用于差错控制等) 81 | 82 | RTS:控制帧(相当于发送这个帧的节点要预约信道),其包括源地址,目的地址和这次通信所需的时间 83 | 84 | CTS:控制帧(AP 同意预约给节点返回的帧)同时也广播出去让其他节点知道 AP 已经有约了 85 | 86 | 87 | 88 | 节点发送数据帧时先监听信道上有没有数据在发送,如果有则进行进行随机退避,有个专门的算法确定出应该退避的算法,而这个节点的退避时间只有信道是空闲才减少,如果没有再监听一个 DIFS 时间若信道上还是没有数据则进行发送数据帧,AP 收到后使用 SIFS 的时间进行处理一下。如果处理后发现有错误就不会返回 ACK,节点在长时间没有接收到 ACK 则会按上述的。 89 | 90 | 还有个预约机制,就是节点给 AP 发送一个 RTS 帧,如果 AP 同意预约就返回一个 CTS 并将其广播给其他节点,让其他节点不干扰这个预约了的节点发送数据,这样也解决了隐蔽站的问题。这个功能可以开启和关闭,一般来说是根据数据帧的长度来判断是否开启,短的就不用开启,因为重传的代价小,长的就开启因为 RTS 帧重传的代价小。 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/网络层/路由算法和路由协议.md: -------------------------------------------------------------------------------- 1 | # 路由算法 2 | **最佳路由:“最佳”只能是相对于某一特定要求下得出的较为合理的选择而已** 3 | 4 | ## 分层次的路由选择协议 5 | 为什么要进行分层次呢? 6 | 7 | 因为: 8 | 9 | + 整个网络世界的规模非常庞大,不可能让一个路由器知道每个 ip 地址 10 | + 一些单位不想让外部知道自己的路由选择协议,但还想连入因特网 11 | 12 | 整个网络可以被分为一个一个的自治系统(AS),路由算法可以被分为 AS 内的和 AS 之间的。 13 | 14 | 首先我们需要知道什么是自治系统: 15 | 16 | 在单一的技术管理下的一组路由器,而这些路由器使用一种 AS 内部的路由选择协议和共同的度量以确定分组在该 AS 内的路由(就是有一组路由器在 AS 内),同时还使用一种 AS 之间的路由协议以确定在 AS 之间的路由(在 AS 之间的路由器)。 17 | 18 | 在一个 AS 内的所有网络都属于一个行政单位来管辖,一个自治系统的所有路由器在本自治系统内部都必须连通。 19 | 20 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1739880594590-02f1c064-3603-4dc3-ace0-3de7520d411d.jpeg) 21 | 22 | 23 | 24 | ## 静态路由算法(非自适应路由算法) 25 | 需要管理员手工配置路由信息 26 | 27 | 其简便、可靠,在负荷稳定、拓扑变化不大的网络中运行效果很好,广泛用于高度安全性的军事网络和较小的商业网络中。缺点就是路由更新慢,不适用与大型网络。 28 | 29 | ## 动态路由算法(自适应路由算法) 30 | 让路由器之间彼此交换信息,按照路由算法优化出路由表项 31 | 32 | 其更新快,使用于大型网络,及时响应网路拓扑变化。缺点是算法复杂,增加网络负担。 33 | 34 | ### 全局性(知道全局路由状态) 35 | 典型代表就是 OSPF(链路状态路由算法) 36 | 37 | 所有路由器掌握完整的网络拓扑和链路费用信息(是指用于评估网络中各个链路(或连接)质量的数值。这些费用信息帮助路由协议决定数据包在网络中传输的最佳路径) 38 | 39 | ### 分散性(只知道自己附近的路由状态) 40 | # RIP 协议与距离向量算法 41 | 典型代表就是 RIP(距离向量路由算法:这个名字的由来:距离是指离目的路由的跳数,向量指的是一条数据由多个数据项构成) 42 | 43 | RIP 是一种分布式的基于距离向量的路由器选择协议,是因特网的协议标准,最大优点是简单。 44 | RIP 协议要求网络中的每一个路由器都维护从它自己到其他每一个目的网络的唯一最佳距离记录。 45 | 46 | 距离:就是跳数,从源端口到目的端口所经历的路由器的个数(包括端口处的路由器),经过一个路由器,跳数就+1。RIP 允许一条路由器最多包含 15 个路由器,因此距离为 16 表示该网络不可达。基于它的跳数限制其只适用于小型网络。 47 | 48 | 我们需要知道的是在一个自治系统中使用 RIP 协议,怎样让每个路由器知道该 AS 中的 IP 信息呢。 49 | 50 | 那么它是采用的: 51 | 52 | + 仅和相邻的路由器交换信息 53 | + 路由器交换的信息是自己的路由表 54 | + 每 30 秒进行一次更新,然后路由器根据更新后的信息再次更新自己的信息。如果超过 180 秒没有收到相邻路由器的信息就会认为邻居没有了,此时会将自己路由表上关于没有的路由器的距离设置为 16,表示不可达。 55 | 56 | ## 怎样更新路由表的呢? 57 | 相邻两个路由器之间交换各自的路由信息是通过 RIP 报文进行交换的。 58 | 59 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739964901075-9203647f-1c93-443b-a96b-3debc55a988d.png) 60 | 61 | 进行更新的步骤: 62 | 63 | + 1、 修改相邻路由器发来的 RIP 报文中所有表项;因为是相邻的,其距离就不可能相同,还有下一跳也不同,所以需要修改所有的表项。怎样修改呢?假设有个路由器其地址为 X,有个 R1 路由器挨着地址为 X 的路由器,X 发送 RIP 报文,R1 接受到后需要将所有表项的“距离”字段+1,下一跳的地址修改为 X 64 | + 2、 对 RIP 修改后需要将该 RIP 报文中的数据进行更新到路由器中。此时需要进行下列步骤: 65 | 66 | 子步骤: 67 | 68 | 1. 若 R1 路由器中没有 Net3,则直接将 RIP 报文中的项目填入 R1 路由器 69 | 2. 若 R1 路由器中有 Net3,那么就需要查看路由器中下一跳的地址。若路由器中下一跳的地址为 X 那么就直接更新(因为需要保持实时状态撒);若路由器中下一跳不是 X 则需要比较两者之间的距离了,将路由器中的表项更新为距离短的 70 | + 3、若 180s 后还没有收到相邻路由器 X 的更新路由表,则 R1 将 X 记为不可达的路由器(将距离设置为 16) 71 | 72 | ## RIP 协议的报文格式 73 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739965727793-e3982b14-e163-40cd-a60c-b73851e36035.png) 74 | 75 | 需要注意的是: 76 | 77 | RIP 协议是在应用层的 78 | 79 | 每个 RIP 协议中最多只能包含 25 个路由,如果超过的话就必须再发送一个 RIP 报文 80 | 81 | 82 | 83 | RIP 对于“坏”消息传递的较慢。举个例子: 84 | 85 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1739966194917-709304d4-d0cb-4cea-a128-d08ddede2d69.jpeg) 86 | 87 | 路由器 R1 左侧的路由器损坏了,R1 在 180s 内没有收到左侧路由发送的 RIP 报文,所以将对应的表项的距离设置为 16 了,但是还没有到 30s,R1 没有通知 R2,但是此时 R2 恰好到了更新的时候并且在 R2 中存在通向已经损坏的路由器的转发表项,给了 R1 后,R1 就会进行更新存储,然后 R1 又给 R2,往互循环直到“距离”加到 16 才可以直到损坏的路由器不能到达。 88 | 89 | # OSPF 协议与链路状态算法 90 | 这个协议叫做:开放最短路径优先协议:“开放”标明 OSPF 协议不是受某一家厂商控制,而是公开发表的;“最短路径优先”是因为使用了 Dijkstra 提出的最短路径算法 SPF 91 | 92 | OSPF 最主要的特征就是使用分布式的链路状态协议 93 | 94 | OSPF 的特点: 95 | 96 | + 使用洪泛法向自治系统内所有路由器发送信息,即路由器通过输出端口向所有相邻的路由器发送信息,然后每个相邻路由器又再次将此信息发往其所有的相邻路由器。(类似于广播) 97 | + 发送的信息就是与本路由器相邻的所有路由器的链路状态(本路由器和哪些路由器相邻,以及该链路的度量/代价-费用、距离、时延、带宽等) 98 | + 只有当链路状态发生变化时,路由器才向所有路由器洪泛发送此信息 99 | 100 | 通过上述的过程最终所有的路由器都能建立一个链路状态数据库,即全网拓扑图。 101 | 102 | ## OSPF 算法 103 | 通过上面的操作可以让每个路由器得到整体的信息,而且只要有一个路由器链路发生变化就会从点到面的进行更新。 104 | 105 | ## OSPF 的分区 106 | 为了使 OSPF 能够用于规模很大的网络,OSPF 将一个自治系统再划分为若干个更小的范围,叫做区域。 107 | 108 | 每一个区域都有一个 32 位的区域标识符(用点分十进制表示),区域也不能太大,在一个区域内的路由器最好不超过 200 个。 109 | 110 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739968195975-c8d06c81-abd4-45d0-9590-a293d07950f2.png) 111 | 112 | ## OSPF 的数据分组 113 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1739968491843-1817ad42-1ed7-4406-8ea9-6bdeff9a0e4e.png) 114 | 115 | 考纲说 OSPF 是网络层的协议,但有些人又说因为将 OSPF 数据放入 IP 数据分组中,所以说其是网络层协议。 116 | 117 | 因为我们是考研所以还是以考纲为准,认为其是网络层协议。 118 | 119 | ## OSPF 的其他特点 120 | 1. 每隔 30 分钟需要进行刷新一次 121 | 2. 由于一个路由器的链路状态只涉及到与相邻路由器的连通状态,因而与整个互联网的规模并无直接关系,因此当互联网规模很大时,OSPF 协议要比距离向量协议 RIP 好得多 122 | 3. OSPF 不存在坏消息传的慢的问题,它的收敛速度很快 123 | 124 | -------------------------------------------------------------------------------- /计算机网络/王道笔记/计算机网络的概念/计算机网络的性能指标.md: -------------------------------------------------------------------------------- 1 |

基本了解

2 | 在使用计算机网络时我们会感受到流畅或者卡顿,只是因为什么呢?在这里就会理解到评判网络性能的依据是什么。 3 | 4 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735215496800-9cfedd53-03cc-4796-b423-27374287c19e.jpeg) 5 | 6 | 总的来说是通过这三个大的方面进行评判的,接下来继续学习吧!深入理解这些概念到底是什么 7 | 8 |

信道

9 | 表示某一方向传输信息的通道(信道不是通信线路),一条通信线路在逻辑对应着一条发送信道和一条接受信道 10 | 11 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735215825811-2899260f-874a-46da-9451-217b0d6f0426.jpeg) 12 | 13 |

速率

14 | 速率:是指连接到网络上的节点在信道上传输数据的速率。也称数据率或比特率、**数据传输速率(常用)** 15 | 16 | 速率单位:bit/s,b/s,bps。需要注意的是 B/s 和 b/s 是不同的含义,B 对应的是 byte 表示 8 个 bit 17 | 18 | **单位换算:1T = 10^3G、1G = 10^3M、1M = 10^3k。这个是在计算机网络中的换算规则,而在机组和 OS 中并不是这样。而是以 2^10 换算的。** 19 | 20 |

带宽

21 | 某信道所能传输的最高数据率。在办理宽带的时候会有这个指标 22 | 23 | 在《通信原理》中提到:带宽是某信道上允许通过的信号频带范围。单位是 Hz;可以认为信号频带范围越大所能携带的数据量也就越大。这个也只有在香农公式和奈氏准则那里会用到。 24 | 25 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735216946809-681b2793-7b24-49c3-8c52-96757dcd5650.jpeg) 26 | 27 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735217081483-edf1c4d1-eda2-49b6-9ccf-d82ab235b144.png) 28 | 29 | 这道题选择 B;通过这道题我们可以知道的是:**节点间通信实际能到达的最高速率是由带宽、节点性能共同限制的** 30 | 31 | 因为 B 能接收的最高速率就只有 10Mbps,A 不管发的再快 B 也接收不到。也就是说最低速率起决定性的作用。 32 | 33 | 34 | 35 |

吞吐量

36 | 指单位时间内通过某个网络(或信道、接口)的实际数据量。吞吐量受带宽限制、受复杂的网络负载影响。 37 | 38 | 也就是相应的所有速率之和(实际的综合数据率)。 39 | 40 | 比如:信道 1 的吞吐量是 10Mbps 41 | 42 | 信道 2 的吞吐量是 40Mbps 43 | 44 | 这条网线(包含信道 1 和信道 2)的吞吐量就是 (10+40)Mbps 咯 45 | 46 | 所以说什么什么吞吐量时,要将其上的所有吞吐量相加。 47 | 48 |

时延

49 | 指数据(一个报文、分组甚至比特)从网络(或链路)的一端传输到另一端所需的时间。有时又称延迟、迟延。 50 | 51 | **总时延 = 发送时延(传输时间)+ 传播时延 + 处理时延 + 排队时延** 52 | 53 | ![画板](https://cdn.nlark.com/yuque/0/2024/jpeg/48073730/1735282388333-24f507b5-287d-4957-82c8-1b5e7381fe54.jpeg) 54 | 55 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735282517232-0f9134b5-2f00-4602-9733-4a81624d5868.png) 56 | 57 | :::info 58 | 这是一道十分简单的题: 59 | 60 | 首先统一单位:只传输一个分组,且大小为 1000B。给出了 A、B 的发送数据速率 61 | 62 | 所以在 A、B 上的传输时延为 1000x8b/1x10^8,1000x8b/8x10^7 单位为 s 63 | 64 | 然后和传播时延相加可得答案 0.24ms 65 | 66 | **在这里可以给出计算公式:** 67 | 68 | ![image](https://cdn.nlark.com/yuque/__latex/4671e24f34121c6f9c057ca7d141a84d.svg) 69 | 70 | ![image](https://cdn.nlark.com/yuque/__latex/cc077a7ee40d10b406db8ec27c50bf11.svg) 71 | 72 | 而在试题中若题目中不强调处理时延和排队时延则不用考虑。 73 | 74 |

时延带宽积

75 | 时延带宽积 = 传播时延 x 带宽 76 | 77 | 我们知道在发送端发送一个 bit 后还会接着继续发送后面的 bit。在第一个 bit 到达接收方时,可能发送方法 bit 已经发完了,也有可能还没有发完。 78 | 79 | 那这个时延带宽积的含义是什么呢?根据公式可以知道是一条链路上从发送端发出但仍没有到达接收方的最大 bit 数。如果将链路比作一个装水的桶,那么可以用时延比作桶的高,带宽比作桶的底面积,时延带宽积比作这个水桶能装的水的体积。 80 | 81 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735283606653-bd3645f5-78c5-4c4d-89ed-63f89cc157f3.png) 82 | 这是一道考研真题:选 D 83 | 84 | 基础题型: 85 | 86 | 首先计算总共有多少个组,显然是 1MB/1000B = 1000 组 87 | 88 | 在第一个分组从 H1 发送并完全到达路由器后,路由器又会再次发送到 H2 89 | 90 | 题目中给出了带宽和时延带宽积就可以计算出相应的传播时延和发送时延。 91 | 92 | 因为发送时延为 0.08ms,考虑路由器到 H2 上有 1000 个这样的发送并且需要加上 H1 到路由器发送的第一个分组的一个传播时延和一个发送时延,还有一个路由器到 H2 的一个传播时延。这个通过画一个图非常好理解。 93 | 94 |

往返时延(RTT)

95 | 表示从发送方发送完数据到发送方接收到接收方返回的确认数据的总时间 96 | 97 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735220918922-4785f975-e13f-45f0-a473-5dc64f5370e4.png) 98 | 往返时延 RTT = t₂ + t₃ + t₄ + t₅ 99 | 100 | 其中: 101 | 102 | t₂: "数据"的单向传播时延 103 | 104 | t₃: 接收方收到数据后的处理时延 105 | 106 | t₄: "确认"的发送时延 107 | 108 | t₅: "确认"的传播时延(通常t₅与t₂相等) 109 | 110 | t₁: 发送方发送数据的发送时延(不计入RTT) 111 | 112 |

信道利用率

113 | 某个信道有百分之多少的时间是有数据通过的 114 | 115 | ![image](https://cdn.nlark.com/yuque/__latex/4bc75ab78cfb9a92a9cc66a3da56a0de.svg) 116 | 117 | **信道利用率太低会浪费资源,利用率太高很有可能发生信道阻塞** 118 | 119 | ![](https://cdn.nlark.com/yuque/0/2024/png/48073730/1735286542625-03b8a9a1-d516-48c7-b210-757a681e00fd.png) 120 | 根据上面的公式易得: 121 | 122 | 利用率 = (10 x 8 x 10^6 ÷ 8 x 10^7 x 20) / 1 x 60 = 33.3% 123 | 124 | -------------------------------------------------------------------------------- /数据结构/王道笔记/string.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 串 3 | categories: 4 | - "408" 5 | - "数据结构" 6 | - "串" 7 | icon: paper-plane 8 | --- 9 | ## 串的定义 10 | > 定义:由零个或多个字符组成的有限序列。 11 | 12 | 记为:S = '$a_1a_2...a_n$' ($n \geq 1$) 13 | 14 | S是串的名称,其中字符个数称为串的长度。当n = 0时,称其为**空串**。 15 | 16 | 串中任意多个连续的子序列称为该串的**子串**。包含这个子串的字符串称为**主串**。子串在主串中的位置是子串与主串相同时子串第一个字符在主串中的位置。 17 | 18 | 如:主串"I AM A PIG"中有个子串"PIG",子串的位置是8,空格也是需要加入计算的。 19 | 20 | 通常来说串的基本操作是以子串作为操作对象的,这一点和线性表有着不同。 21 | ## 串的基本操作 22 | - strCopy(char *string, char *data):将data的字符串复制给string 23 | - subString(char *string, char *data, int start, int len):使用string返回data中从start开始长度为len的子串 24 | - strLength(char *string):返回string的长度 25 | - index(char *s, char *t):定位,若s中存在t则返回t的位置,否则返回0 26 | - contact(char *s, char *t, char *m):连接,将t和m连接起来使用s返回 27 | - strCompare(char *s, char *t):比较字符串,s大于t则返回1,s=t返回0,s小于t返回-1 28 | - clear(char *s):将s中的字符全部清除,使其变为空串 29 | 30 | 根据上面的基本操作可以构建更复杂更强大的功能。 31 | ## 串的存储结构 32 | ### 顺序存储结构 33 | C语言结构体为: 34 | ```c 35 | typedef struct String{ 36 | char data[MAX_SIZE]; 37 | int length; 38 | } 39 | ``` 40 | 使用这种需要额外添加一个length,用于存储字符串的长度用以避免使用循环获取,其长度是固定的在声明之后无法进行修改长度。 41 | 42 | 如果添加的字符串长度大于MAX_SIZE就会发生**截断**。并且串长可以有两种表示方式,一就是上面的那种使用length记录,二是在字符串最后添加一个'\0',这个是不计入串长的,所以导致获取长度时要遍历。 43 | ### 堆分配存储结构 44 | 和之前学习的的链表一样,为了解决“固定”的问题。在上述的顺序结构中一旦初始化,其长度就不能变化了,但是在应用中我们一般不能预见到底需要初始化多长的结构,故采用这种堆分配的结构。 45 | 46 | 在C语言中,有一个可以自由分配的区域,称其为**堆区**,使用malloc()和free()进行动态的分配。在之前的代码中也使用了,需要注意的是使用malloc()获取的地址在使用后需要手动调用free()释放。 47 | ### 块链存储结构 48 | ![块链存储示意图](https://camo.githubusercontent.com/f8aff397da09a4481fafc8b6a694254acee573318a5f7f7af52be7833c9a448e/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f79757175652f302f323032352f706e672f34383037333733302f313734313537323136313939372d31336662666530652d356364322d346234622d626131352d3135623837306334336338652e706e67) 49 | 50 | 也是使用链式存储不过每个节点可以存放多个字符。如上:每个节点存储的字符数为4,如果一个节点中的字符不足就使用 **#** 填充。 51 | 52 | 这样呢,让每个节点的存储密度上升了。 53 | ## 串的模式匹配 54 | 我们需要知道: 55 | - 模式串:要进行匹配的字符串 56 | - 模式匹配:在主串中找到和模式串相同的子串,并返回对应的位置 57 | ### 简单模式匹配 58 | 采用的存储结构为第一种,但第一个字符从索引为1开始。简单模式匹配本质就是暴力破解,一次一次遍历主串和模式串,直到匹配到的子串和模式串相等。 59 | 60 | 显然此种算法的时间复杂度为:O(nm)(n位主串长度,m为模式串长度),在一些情况下有着大量地不必要比较。 61 | 62 | C语言表示算法: 63 | ```c 64 | int index(String string, String t){ 65 | int i = 1, j = 1; 66 | while(i <= string.length && j <= t.length){ 67 | if(j.data[i] == t.data[j]){ 68 | i++; 69 | j++; 70 | }else{ 71 | i = i - j + 2; 72 | j = 1; 73 | } 74 | } 75 | if(j > t.length){ 76 | return i - t.length; 77 | } 78 | return 0; 79 | } 80 | ``` 81 | ### KMP算法 82 | 为了优化上面暴力破解时一些没有必要的比较,于是就诞生了KMP算法。 83 | 84 | 使用KMP算法时,主串上的i是不会回退的,要么前进,要么不动。 85 | 86 | 在学习KMP算法之前我们需要知道一些概念: 87 | 88 | - 前缀:除最后一个字符外,字符串的所有头部子串 89 | - 后缀:除第一个字符外,字符串的所有尾部子串 90 | - 部分匹配值:字符串的前缀和后缀的**最长相等**前后缀长度 91 | 92 | 看着这些概念是模糊的,我们来举个例子:使用字符串 "aabbaa" 93 | 94 | 子串"a"的前缀和后缀都没有所以最长相等前后缀长度为0 95 | 96 | 子串"aa"的前缀为{a}后缀为{a}所以最长相等前后缀长度为1 97 | 98 | 子串"aab"的前缀为{a, aa}后缀为{b, ab}所以最长相等前后缀长度为0 99 | 100 | 子串"aabb"的前缀为{a, aa, aab}后缀为{b, bb, abb}所以最长相等前后缀长度为0 101 | 102 | 子串"aabba"的前缀为{a, aa, aab, aabb}后缀为{a, ba, bba, abba}所以最长相等前后缀长度为1 103 | 104 | 子串"aabbaa"的前缀为{a, aa, aab, aabb, aabba}后缀为{a, aa, baa, bbaa, abbaa}所以最长相等前后缀长度为2 105 | 106 | 所以字符串的部分匹配值为:0 1 0 0 1 2 107 | 108 | 我们想一下这个最长相等前后缀长度有什么意义呢? 109 | 110 | 知道字符串的最长前后缀长度,就知道了这个字符串前面和后面最多有几个字符相等了。如果在匹配过程中遇到不匹配时就可以查询这个部分匹配值表来确定移动的位数,就不用一位一位地移动了,知道这个原理就行了。 111 | #### 维护next数组 112 | 在匹配中,如果模式串的第j个字符失配了就跳到next[j]位置上继续比较,这个next数组就是由上面的部分匹配值来的。 113 | 114 | 在网络上有着多种的next表示的形式,但是究其本质是一样的。具体使用哪一种需要根据题目中的信息来判断,在这里我给出具体的计算方法: 115 | 1. 形如[0, 1, 2, 1, 1, 2];开头为0的next数组。 116 | 这个是怎样来的呢?将上面得出来的字符串的部分匹配值右移一位,第一位补0,其余为全部加一。 117 | 2. 形如[-1, 0, 1, 0, 0, 1]:开头为-1的next数组。 118 | 这个是将第1种的next数组每一位减1得到的。 119 | 120 | 为什么会有这两种呢?因为是字符串的起始位置定义不同。第1种的起始位置视为1,第2种起始位置视为0。 121 | ### KMP算法的优化 122 | 在使用next数组时其实还是有着缺陷。在某些时候还是会有多余地比较,所有在这里继续对其优化,得到nextval数组。 123 | 124 | 优化方法:首先得到next数组,nextval数组的第一位是和next数组中的第一位一样的,然后比较**next[i]对着的模式串字符**和**模式串[next[i]]**,若两者相等则nextval[i]就为模式串[next[i]]对应的nextval值,否则nextval[i]就为next[i]。 125 | 126 | 下面举个例子: 127 | 128 | 字符串:"aabbaa" 129 | 1. 采用开头为0的next数组 130 | 部分匹配值为:[0, 1, 0, 0, 1, 2] 131 | next数组:[0, 1, 2, 1, 1, 2] (右移并加1 补0) 132 | nextval数组:[0, 0, 2, 1, 0, 0] (进行比较) 133 | 2. 采用开头为-1的next数组 134 | next数组为:[-1, 0, 1, 0, 0, 1] (每位减1) 135 | nextval数组为:[-1, -1, 1, 0, -1, -1] (进行比较) 136 | 137 | **这一部分最重要的就是KMP算法,出题为算next数组、nextval数组、比较次数、滑动距离。** 138 | -------------------------------------------------------------------------------- /书籍/计算机原理专栏/程序装载:“640K内存”真的不够用么?.md: -------------------------------------------------------------------------------- 1 | # 09 程序装载:“640K内存”真的不够用么? 2 | 3 | 计算机这个行业的历史上有过很多成功的预言,最著名的自然是“摩尔定律”。当然免不了的也有很多“失败”的预测,其中一个最著名的就是,比尔·盖茨在上世纪 80 年代说的“640K ought to be enough for anyone”,也就是“640K 内存对哪个人来说都够用了”。 4 | 5 | 那个年代,微软开发的还是 DOS 操作系统,程序员们还在绞尽脑汁,想要用好这极为有限的 640K 内存。而现在,我手头的开发机已经是 16G 内存了,上升了一万倍还不止。那比尔·盖茨这句话在当时也是完全的无稽之谈么?有没有哪怕一点点的道理呢?这一讲里,我就和你一起来看一看。 6 | 7 | ## 程序装载面临的挑战 8 | 9 | 上一讲,我们看到了如何通过链接器,把多个文件合并成一个最终可执行文件。在运行这些可执行文件的时候,我们其实是通过一个装载器,解析 ELF 或者 PE 格式的可执行文件。装载器会把对应的指令和数据加载到内存里面来,让 CPU 去执行。 10 | 11 | 说起来只是装载到内存里面这一句话的事儿,实际上装载器需要满足两个要求。 12 | 13 | **第一,可执行程序加载后占用的内存空间应该是连续的**。我们在[第 6 讲]讲过,执行指令的时候,程序计数器是顺序地一条一条指令执行下去。这也就意味着,这一条条指令需要连续地存储在一起。 14 | 15 | **第二,我们需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置。**虽然编译出来的指令里已经有了对应的各种各样的内存地址,但是实际加载的时候,我们其实没有办法确保,这个程序一定加载在哪一段内存地址上。因为我们现在的计算机通常会同时运行很多个程序,可能你想要的内存地址已经被其他加载了的程序占用了。 16 | 17 | 要满足这两个基本的要求,我们很容易想到一个办法。那就是我们可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。 18 | 19 | 我们把指令里用到的内存地址叫作**虚拟内存地址**(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫**物理内存地址**(Physical Memory Address)**。** 20 | 21 | 程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。对于任何一个程序来说,它看到的都是同样的内存地址。我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。因为是连续的内存地址空间,所以我们只需要维护映射关系的起始地址和对应的空间大小就可以了。 22 | 23 | ## 内存分段 24 | 25 | 这种找出一段连续的物理内存和虚拟内存地址进行映射的方法,我们叫**分段**(Segmentation)**。**这里的段,就是指系统分配出来的那个连续的内存空间。 26 | 27 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/24596e1e66d88c5d077b4c957d0d7f18.png) 28 | 29 | 分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处,第一个就是**内存碎片**(Memory Fragmentation)的问题。 30 | 31 | 我们来看这样一个例子。我现在手头的这台电脑,有 1GB 的内存。我们先启动一个图形渲染程序,占用了 512MB 的内存,接着启动一个 Chrome 浏览器,占用了 128MB 内存,再启动一个 Python 程序,占用了 256MB 内存。这个时候,我们关掉 Chrome,于是空闲内存还有 1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个 200MB 的程序。但是,这 256MB 的内存空间不是连续的,而是被分成了两段 128MB 的内存。因此,实际情况是,我们的程序没办法加载进来。 32 | 33 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/57211af3053ed621aeb903433c6c10d1.png) 34 | 35 | 当然,这个我们也有办法解决。解决的办法叫**内存交换**(Memory Swapping)。 36 | 37 | 我们可以把 Python 程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里面。不过读回来的时候,我们不再把它加载到原来的位置,而是紧紧跟在那已经被占用了的 512MB 内存后面。这样,我们就有了连续的 256MB 内存空间,就可以去加载一个新的 200MB 的程序。如果你自己安装过 Linux 操作系统,你应该遇到过分配一个 swap 硬盘分区的问题。这块分出来的磁盘空间,其实就是专门给 Linux 操作系统进行内存交换用的。 38 | 39 | 虚拟内存、分段,再加上内存交换,看起来似乎已经解决了计算机同时装载运行很多个程序的问题。不过,你千万不要大意,这三者的组合仍然会遇到一个性能瓶颈。硬盘的访问速度要比内存慢很多,而每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是一个很占内存空间的程序,这样整个机器都会显得卡顿。 40 | 41 | ## 内存分页 42 | 43 | 既然问题出在内存碎片和内存交换的空间太大上,那么解决问题的办法就是,少出现一些内存碎片。另外,当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决这个问题。这个办法,在现在计算机的内存管理里面,就叫作**内存分页**(Paging)。 44 | 45 | **和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小**。而对应的程序所需要占用的虚拟内存空间,也会同样切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫**页**(Page)。从虚拟内存到物理内存的映射,不再是拿整段连续的内存的物理地址,而是按照一个一个页来的。页的尺寸一般远远小于整个程序的大小。在 Linux 下,我们通常只设置成 4KB。你可以通过命令看看你手头的 Linux 系统设置的页的大小。 46 | 47 | ```ruby 48 | $ getconf PAGE_SIZE 49 | 复制代码 50 | ``` 51 | 52 | 由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多 4KB 的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。 53 | 54 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/0cf2f08e1ceda473df71189334857cf0.png) 55 | 56 | 更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。 57 | 58 | 实际上,我们的操作系统,的确是这么做的。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的**缺页错误**(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。同时,这样一来,任何程序都不需要一次性加载完所有指令和数据,只需要加载当前需要用到就行了。 59 | 60 | 通过虚拟内存、内存交换和内存分页这三个技术的组合,我们最终得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。这些技术和方法,对于我们程序的编写、编译和链接过程都是透明的。这也是我们在计算机的软硬件开发中常用的一种方法,就是**加入一个间接层**。 61 | 62 | 通过引入虚拟内存、页映射和内存交换,我们的程序本身,就不再需要考虑对应的真实的内存地址、程序加载、内存管理等问题了。任何一个程序,都只需要把内存当成是一块完整而连续的空间来直接使用。 63 | 64 | ## 总结延伸 65 | 66 | 现在回到开头我问你的问题,我们的电脑只要 640K 内存就够了吗?很显然,现在来看,比尔·盖茨的这个判断是不合理的,那为什么他会这么认为呢?因为他也是一个很优秀的程序员啊! 67 | 68 | 在虚拟内存、内存交换和内存分页这三者结合之下,你会发现,其实要运行一个程序,“必需”的内存是很少的。CPU 只需要执行当前的指令,极限情况下,内存也只需要加载一页就好了。再大的程序,也可以分成一页。每次,只在需要用到对应的数据和指令的时候,从硬盘上交换到内存里面来就好了。以我们现在 4K 内存一页的大小,640K 内存也能放下足足 160 页呢,也无怪乎在比尔·盖茨会说出“640K ought to be enough for anyone”这样的话。 69 | 70 | 不过呢,硬盘的访问速度比内存慢很多,所以我们现在的计算机,没有个几 G 的内存都不好意思和人打招呼。 71 | 72 | 那么,除了程序分页装载这种方式之外,我们还有其他优化内存使用的方式么?下一讲,我们就一起来看看“动态装载”,学习一下让两个不同的应用程序,共用一个共享程序库的办法。 73 | 74 | ## 推荐阅读 75 | 76 | 想要更深入地了解代码装载的详细过程,推荐你阅读《程序员的自我修养——链接、装载和库》的第 1 章和第 6 章。 -------------------------------------------------------------------------------- /数据结构/王道笔记/time-and-space-complexity.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 评判算法好坏的标准 3 | icon: "bugs" 4 | categories: 5 | - "408" 6 | - "数据结构" 7 | tags: 8 | - "O(n)~" 9 | --- 10 | ## 算法的好坏评价标准 11 | 我们说这是一个优秀的算法,那是一个糟糕的算法,不是看是谁写的,也不是看是谁先提出来的,而是根据该算法的**时间复杂度**和**空间复杂度**来决定的。 12 | 13 | 那怎样进行判断呢? 14 | 15 | 我们就不搞数学那一套了,直接使用更为简单易懂的形式进行理解。 16 | 17 | ## 时间复杂度 18 | 第一眼看到这个名词的时候还以为是让一段程序在电脑上运行一下同时统计运行使用的时间嘞,但回头一想,这样好像不太科学吧! 19 | 20 | 是的,如果只是简单的采用在电脑上运行的时间作为评判,那就滑天下之大稽了。思考一下,你的20000元的电脑和我的3000元的电脑玩原神的流畅度,肯定是不能比的。在这里也是同理,不同的电脑对于同一段程序跑出来的时耗也是不同的,甚至是天差地别! 21 | 22 | 再退一步说,如果不论什么算法都让它跑一下,对于计算机资源也是一种浪费呀。我们可是**勤俭节约**的人呀!可不能这样干。 23 | 24 | 那这个问题怎样解决呢? 25 | 26 | 既然在不同的物理机上是存在差别的,那我们就将其统一嘛,认为每行代码执行的时间是相同的,这样对于输入规模为n的真实时间与使用时间复杂度估计的时间只会相差一个常量系数,而在n很大时这个值是可以忽略的。 27 | 28 | 此时记T(n) = O(f(n)),这个f(n)是每行代码执行之和,于是我们就用这个方法来表示算法的时间复杂度(也叫大O表示法) 29 | ### 各种时间复杂度 30 | 我们可以来举几个例子,举例子是最容易让人明白的手段: 31 | - 时间复杂度为O(1) 32 | ```c 33 | int sum(int n){ 34 | return n; 35 | } 36 | ``` 37 | 可以看到这个函数无论n为多少,函数内的代码只会执行与n无关的次数,也就是$O(n^0)$,故其时间复杂度为O(1) 38 | :::tip 请注意 39 | 不要认为里面的代码行数只有一行其时间复杂度为O(1)。就算里面有100000000....行,只要其与n无关都是O(1)的时间复杂度。 40 | ::: 41 | - 时间复杂度为O(n) 42 | ```c 43 | int sum(int n){ 44 | int result = 0; 45 | for(int i = 0; i < n; i++){ 46 | result += i; 47 | } 48 | return n; 49 | } 50 | ``` 51 | 此时总的执行次数为for循环中的n次加上开头和结尾分别一次,共n + 2次,T(n) = O(n + 2)。此时需要注意:我们在写这个T(n)时只需要保留n的最高次就行。 52 | 53 | 故T(n) = O(n) 54 | 55 | :::tip 请注意 56 | 上面说的保留最高次的意思是不管最高次前的系数,统一为1。 57 | 比如:所有代码运行了5n + 20次,此时的T(n)仍为O(n) 58 | ::: 59 | - 时间复杂度为$O(n^2)$ 60 | ```c 61 | int double(int n){ 62 | int result = 0; 63 | for(int i = 0; i < n; i++){ 64 | for(int j = 0; j < n; j++){ 65 | result += j; 66 | } 67 | } 68 | return result; 69 | } 70 | ``` 71 | 显然的,此时代码运行次数为$n ^ 2 + 2$,使用大O表示法可知时间复杂度为:$O(n^2)$。现在看来就简单了吧! 72 | - 时间复杂度为O($\log_2 n$) 73 | ```c 74 | int logarithmic(int n) { 75 | int count = 0; 76 | while (n > 1) { 77 | n = n / 2; 78 | count++; 79 | } 80 | return count; 81 | } 82 | ``` 83 | 我们还是进行分析代码运行的次数,显然有必然的两次(在以后的分析中就不用再考虑这些“无关紧要的次数了”),重要的中间的while循环。 84 | 85 | 进入循环时是n,经过一次之后为$\frac{n}{2}$,经历2次是$\frac{n}{2^2}$,依次类推,设运行了t次,那么有$\frac{n}{2^t} = 1$,显然得t=$\log_2 n$。此时时间复杂度T(n) = O($\log_2 n$)。 86 | 87 | - 时间复杂度为O($n \log_2 n$) 88 | ```c 89 | int linearLogRecur(int n) { 90 | if (n <= 1) 91 | return 1; 92 | int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); 93 | for (int i = 0; i < n; i++) { 94 | count++; 95 | } 96 | return count; 97 | } 98 | ``` 99 | 这个采用了递归,一共拆分为了$\log_2 n$层,每层下面有一个for循环则时间复杂度就为O(n$\log_2 n$)。 100 | - 时间复杂度为O($2^n$) 101 | ```c 102 | int exponential(int n) { 103 | int count = 0; 104 | int bas = 1; 105 | for (int i = 0; i < n; i++) { 106 | for (int j = 0; j < bas; j++) { 107 | count++; 108 | } 109 | bas *= 2; 110 | } 111 | // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 112 | return count; 113 | } 114 | ``` 115 | 和之前的思考方式一样,去算一算代码总共的运行的次数吧! 116 | - 时间复杂度为O(n!) 117 | ```c 118 | int factorialRecur(int n) { 119 | if (n == 0) 120 | return 1; 121 | int count = 0; 122 | for (int i = 0; i < n; i++) { 123 | count += factorialRecur(n - 1); 124 | } 125 | return count; 126 | } 127 | ``` 128 | 可以看到for循环中套了一个递归,最外层运行n次,进入之后为n-1,类推下去直到n = 0。容易看出运行的次数需要将每层循环的次数乘起来,故时间复杂度为O(n!)。 129 | ### 最好时间复杂度、最坏时间复杂度、平均时间复杂度 130 | 在一个算法中运行的时间可能并入我们所料的那么长,也可能没有我们预想的那么短。 131 | 132 | 举个例子: 133 | 在一个数组中查找置为12的索引,该数组为:[12, 23, 34, 22],显然第一个就是它,此时的最佳复杂度为Ω(1)。 134 | 如果我们要查找的不是12而是22,那么此时需要在最后一次才能找到它,也就是最坏的情况,此时的最坏时间复杂度为O(n)。 135 | 通常来说,有最佳,有最坏,那么肯定有个折中的,在这里就是平均时间复杂度,显然是O($\frac{n}{2}$),也就是O(n)。 136 | 137 | 值得说明的是:在使用时间复杂度时总是使用最坏的或者平均的,而不会使用最好的,因为我们总要把**最坏**的情况考虑到而不会是最好的情况,这样才能知道自己是否能接受这个结果从而进行取舍。 138 | ### 常见时间复杂度排序 139 | 我们来给常见的时间复杂度从小到大排个序: 140 | **O(1) < O($\log_2 n$) < O(n) < O(n * $\log_2 n$) < O($2^n$) < O(n!)** 141 | :::tip 注意 142 | O($2^n$) < O(n!)是n在大于4的情况下! 143 | ::: 144 | ## 空间复杂度 145 | 空间复杂度,顾名思义是在程序运行时需要额外空间与输入规模之间的关系,这个概念是和时间复杂度非常类似。 146 | 147 | - 常数阶O(1) 148 | ```c 149 | int func() { 150 | return 0; 151 | } 152 | void constant(int n) { 153 | const int a = 0; 154 | int b = 0; 155 | int nums[1000]; 156 | ListNode *node = newListNode(0); 157 | free(node); 158 | for (int i = 0; i < n; i++) { 159 | int c = 0; 160 | } 161 | for (int i = 0; i < n; i++) { 162 | func(); 163 | } 164 | } 165 | ``` 166 | 可以看到这个使用的空间与n的输入大小无关,故空间复杂度为O(1)。 167 | 168 | 其余的就不在这里举例了。如果有想了解其他空间复杂度的同学可以访问[hello算法](https://www.hello-algo.com/chapter_computational_complexity/space_complexity/#3-on2) 169 | 170 | ## 总结 171 | 在计算时间复杂度时要紧紧抓住运算代码的次数,这是最重要的地方! 172 | 173 | 174 | -------------------------------------------------------------------------------- /书籍/计算机原理专栏/浮点数和定点数(上):怎么用有限的Bit表示尽可能多的信息.md: -------------------------------------------------------------------------------- 1 | # 15 浮点数和定点数(上):怎么用有限的Bit表示尽可能多的信息? 2 | 3 | 在我们日常的程序开发中,不只会用到整数。更多情况下,我们用到的都是实数。比如,我们开发一个电商 App,商品的价格常常会是 9 块 9;再比如,现在流行的深度学习算法,对应的机器学习里的模型里的各个权重也都是 1.23 这样的数。可以说,在实际的应用过程中,这些有零有整的实数,是和整数同样常用的数据类型,我们也需要考虑到。 4 | 5 | ## 浮点数的不精确性 6 | 7 | 那么,我们能不能用二进制表示所有的实数,然后在二进制下计算它的加减乘除呢?先不着急,我们从一个有意思的小案例来看。 8 | 9 | 你可以在 Linux 下打开 Python 的命令行 Console,也可以在 Chrome 浏览器里面通过开发者工具,打开浏览器里的 Console,在里面输入“0.3 + 0.6”,然后看看你会得到一个什么样的结果。 10 | 11 | ```python-repl 12 | >>> 0.3 + 0.6 13 | 0.8999999999999999 14 | ``` 15 | 16 | 不知道你有没有大吃一惊,这么简单的一个加法,无论是在 Python 还是在 JavaScript 里面,算出来的结果居然不是准确的 0.9,而是 0.8999999999999999 这么个结果。这是为什么呢? 17 | 18 | 在回答为什么之前,我们先来想一个更抽象的问题。通过前面的这么多讲,你应该知道我们现在用的计算机通常用 16⁄32 个比特(bit)来表示一个数。那我问你,我们用 32 个比特,能够表示所有实数吗? 19 | 20 | 答案很显然是不能。32 个比特,只能表示 2 的 32 次方个不同的数,差不多是 40 亿个。如果表示的数要超过这个数,就会有两个不同的数的二进制表示是一样的。那计算机可就会一筹莫展,不知道这个数到底是多少。 21 | 22 | 40 亿个数看似已经很多了,但是比起无限多的实数集合却只是沧海一粟。所以,这个时候,计算机的设计者们,就要面临一个问题了:我到底应该让这 40 亿个数映射到实数集合上的哪些数,在实际应用中才能最划得来呢? 23 | 24 | ## 定点数的表示 25 | 26 | 有一个很直观的想法,就是我们用 4 个比特来表示 0~9 的整数,那么 32 个比特就可以表示 8 个这样的整数。然后我们把最右边的 2 个 0~9 的整数,当成小数部分;把左边 6 个 0~9 的整数,当成整数部分。这样,我们就可以用 32 个比特,来表示从 0 到 999999.99 这样 1 亿个实数了。 27 | 28 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/f5a0b0f2188ebe0d18f4424578a588b3.jpg) 29 | 30 | 这种用二进制来表示十进制的编码方式,叫作[**BCD 编码**](https://zh.wikipedia.org/wiki/二進碼十進數)(Binary-Coded Decimal)。其实它的运用非常广泛,最常用的是在超市、银行这样需要用小数记录金额的情况里。在超市里面,我们的小数最多也就到分。这样的表示方式,比较直观清楚,也满足了小数部分的计算。 31 | 32 | 不过,这样的表示方式也有几个缺点。 33 | 34 | **第一,这样的表示方式有点“浪费”。**本来 32 个比特我们可以表示 40 亿个不同的数,但是在 BCD 编码下,只能表示 1 亿个数,如果我们要精确到分的话,那么能够表示的最大金额也就是到 100 万。如果我们的货币单位是人民币或者美元还好,如果我们的货币单位变成了津巴布韦币,这个数量就不太够用了。 35 | 36 | **第二,这样的表示方式没办法同时表示很大的数字和很小的数字。**我们在写程序的时候,实数的用途可能是多种多样的。有时候我们想要表示商品的金额,关心的是 9.99 这样小的数字;有时候,我们又要进行物理学的运算,需要表示光速,也就是 3×1083×108 这样很大的数字。那么,我们有没有一个办法,既能够表示很小的数,又能表示很大的数呢? 37 | 38 | ## 浮点数的表示 39 | 40 | 答案当然是有的,就是你可能经常听说过的**浮点数**(Floating Point),也就是**float 类型**。 41 | 42 | 我们先来想一想。如果我们想在一张便签纸上,用一行来写一个十进制数,能够写下多大范围的数?因为我们要让人能够看清楚,所以字最小也有一个限制。你会发现一个和上面我们用 BCD 编码表示数一样的问题,就是纸张的宽度限制了我们能够表示的数的大小。如果宽度只放得下 8 个数字,那么我们还是只能写下最大到 99999999 这样的数字。 43 | 44 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/c321a0b9d95ba475439f9fbdff07bf56.png) 45 | 46 | 有限宽度的便签,只能写下有限大小的数字 47 | 48 | 其实,这里的纸张宽度,就和我们 32 个比特一样,是在空间层面的限制。那么,在现实生活中,我们是怎么表示一个很大的数的呢?比如说,我们想要在一本科普书里,写一下宇宙内原子的数量,莫非是用一页纸,用好多行写下很多个 0 么? 49 | 50 | 当然不是了,我们会用科学计数法来表示这个数字。宇宙内的原子的数量,大概在 10 的 82 次方左右,我们就用 1.0×10821.0×1082 这样的形式来表示这个数值,不需要写下 82 个 0。 51 | 52 | 在计算机里,我们也可以用一样的办法,用科学计数法来表示实数。浮点数的科学计数法的表示,有一个**IEEE**的标准,它定义了两个基本的格式。一个是用 32 比特表示单精度的浮点数,也就是我们常常说的 float 或者 float32 类型。另外一个是用 64 比特表示双精度的浮点数,也就是我们平时说的 double 或者 float64 类型。 53 | 54 | 双精度类型和单精度类型差不多,这里,我们来看单精度类型,双精度你自然也就明白了。 55 | 56 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/914b71bf1d85fb6ed76e1135f39b6941.jpg) 57 | 58 | 单精度的 32 个比特可以分成三部分。 59 | 60 | 第一部分是一个**符号位**,用来表示是正数还是负数。我们一般用**s**来表示。在浮点数里,我们不像正数分符号数还是无符号数,所有的浮点数都是有符号的。 61 | 62 | 接下来是一个 8 个比特组成的**指数位**。我们一般用**e**来表示。8 个比特能够表示的整数空间,就是 0~255。我们在这里用 1~254 映射到 -126~127 这 254 个有正有负的数上。因为我们的浮点数,不仅仅想要表示很大的数,还希望能够表示很小的数,所以指数位也会有负数。 63 | 64 | 你发现没,我们没有用到 0 和 255。没错,这里的 0(也就是 8 个比特全部为 0) 和 255 (也就是 8 个比特全部为 1)另有它用,我们等一下再讲。 65 | 66 | 最后,是一个 23 个比特组成的**有效数位**。我们用**f**来表示。综合科学计数法,我们的浮点数就可以表示成下面这样: 67 | 68 | (−1)s×1.f×2e(−1)s×1.f×2e 69 | 70 | 你会发现,这里的浮点数,没有办法表示 0。的确,要表示 0 和一些特殊的数,我们就要用上在 e 里面留下的 0 和 255 这两个表示,这两个表示其实是两个标记位。在 e 为 0 且 f 为 0 的时候,我们就把这个浮点数认为是 0。至于其它的 e 是 0 或者 255 的特殊情况,你可以看下面这个表格,分别可以表示出无穷大、无穷小、NAN 以及一个特殊的不规范数。 71 | 72 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/f922249a89667c4d10239eb8840dc94c.jpg) 73 | 74 | 我们可以以 0.5 为例子。0.5 的符号为 s 应该是 0,f 应该是 0,而 e 应该是 -1,也就是 75 | 76 | 0.5=(−1)0×1.0×2−1=0.50.5=(−1)0×1.0×2−1=0.5,对应的浮点数表示,就是 32 个比特。 77 | 78 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/5168fce3f313f4fc0b600ce5d1805c50.jpeg) 79 | 80 | s=0,e=2−1s=0,e=2−1,需要注意,e 表示从 -126 到 127 个,-1 是其中的第 126 个数,这里的 e 如果用整数表示,就是 26+25+24+23+22+21=12626+25+24+23+22+21=126,1.f=1.01.f=1.0。 81 | 82 | 在这样的浮点数表示下,不考虑符号的话,浮点数能够表示的最小的数和最大的数,差不多是 1.17×10−381.17×10−38 和 3.40×10383.40×1038。比前面的 BCD 编码能够表示的范围大多了。 83 | 84 | ## 总结延伸 85 | 86 | 你会看到,在这样的表示方式下,浮点数能够表示的数据范围一下子大了很多。正是因为这个数对应的小数点的位置是“浮动”的,它才被称为浮点数。随着指数位 e 的值的不同,小数点的位置也在变动。对应的,前面的 BCD 编码的实数,就是小数点固定在某一位的方式,我们也就把它称为**定点数**。 87 | 88 | 回到我们最开头,为什么我们用 0.3 + 0.6 不能得到 0.9 呢?这是因为,浮点数没有办法精确表示 0.3、0.6 和 0.9。事实上,我们拿出 0.1~0.9 这 9 个数,其中只有 0.5 能够被精确地表示成二进制的浮点数,也就是 s = 0、e = -1、f = 0 这样的情况。 89 | 90 | 而 0.3、0.6 乃至我们希望的 0.9,都只是一个近似的表达。这个也为我们带来了一个挑战,就是浮点数无论是表示还是计算其实都是近似计算。那么,在使用过程中,我们该怎么来使用浮点数,以及使用浮点数会遇到些什么问题呢?下一讲,我会用更多的实际代码案例,来带你看看浮点数计算中的各种“坑”。 91 | 92 | ## 推荐阅读 93 | 94 | 如果对浮点数的表示还不是很清楚,你可以仔细阅读一下《计算机组成与设计:硬件 / 软件接口》的 3.5.1 节。 -------------------------------------------------------------------------------- /计算机网络/王道笔记/数据链路层/流量控制、可靠传输、滑动窗口机制.md: -------------------------------------------------------------------------------- 1 | ## 流量控制和可靠传输 2 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735894104572-84b1ee16-8f3d-4062-be60-800a9a77d9b9.jpeg) 3 | 4 | 思考一下为什么一定需要给帧进行编号呢? 5 | 6 | **我们想象一个场景:如果发送方在发送了一个帧给接收方后,接收方的确认帧在传输给发送方的过程中丢失,此时接收方的滑动窗口已经准备接收新的帧了,但是发送方还没有接收到确认帧,在超时之后重传,然后接收方需要知道这个重传的帧到底是接收方想要的新的帧还是之前的帧,此时就需要帧的编号了。换句话说就是帧编号可以让接收方判断这个帧是不是重复帧。不是接收方想要的新的帧就一定是之前的重复帧。** 7 | 8 | **** 9 | 10 | 这个第 i 号帧并不是普通的增加就完了,而是一种循环,不然根本就没有那么多表示位数的 bit 位给它用。 11 | 例子:使用 S-W 协议(这里贴出图示下面就不再贴了) 12 | 13 | 将要发送的帧全部编号 0 1 0 1 0 1...,那么接收方要接受的帧也为 0 1 0 1 0 1... 14 | 15 | 这样就可以表示所有的帧了,因为需要接收方确认了之后发送方才能继续向后发送则肯定可以保证如果发生错误需要重传的是窗口中的哪一个帧。 16 | 17 | 这里有几个专业名词: 0 1 0 1 0 1...这个称为帧序号 18 | 19 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735871919949-44425f9d-9a72-4f7f-9e4a-5eb2c280db7b.png) 20 | 21 | ## 滑动窗口机制 22 | 在发送方有个发送窗口,在接收方有个接收窗口。发送方一次只能传输发送窗口内的帧,接收方只能接收接收窗口大小的帧,如果发送窗口的帧多于接收窗口的帧则多余的帧直接丢弃。在接收方接收完成后就会使用确认机制向发送方说可以继续了,发送方的滑动窗口就向后滑动和接收方滑动窗口同样大小的帧。按照这种模式依次发送 23 | 24 | ## 停止等待协议 25 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735892638300-1bcc03b9-df38-44cc-97f9-281c0c631434.jpeg) 26 | 27 | S-W 协议中不存在数据帧失序的问题,因为帧是一个一个的发送和接收的。其余的操作和上面的一致 28 | 29 | ## 后退 N 帧协议 30 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740216116076-c5105509-7ce6-4f75-8883-e156143e37d5.jpeg) 31 | 32 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735894343428-20a67220-09d7-47aa-ba69-075949667a37.png) 33 | 34 | 35 | 过程:发送方的发送窗口是 3 个帧(这里举例实际中可以是其他个数),接收方接收窗口是 1 个帧。(这样的安排就可以确定是按 0、1、2、3 的顺序进行排列了,因为那个不等式得出 n 为 2) 36 | 37 | 发送方在发送了 3 个帧后,接收方进行接收,一个一个的接收,在最后一个帧接收完成后,返回一个 ACK2,表示这个帧以前的我都接收成功了,然后发送窗口向后滑动。这个是正常的情况,下面说一下不正常的情况。 38 | 39 | **数据帧丢失:**以第二次发送窗口中的 E 丢失为例,D 在正确接收后,接收窗口到了 E,但是发送的 E 丢失了,接收窗口接收到了 F 对应的 1,但是现在是要接收 E 对应的 0,所以直接将不是想要的帧丢弃,并返回最后接收到帧 D 的 ACK3,然后发送窗口向后滑动,因为发送窗口中的 0 迟迟没有收到对应的 ACK, 由于超时重传机制,所以将其以及以后的滑动窗口中的所有帧进行再一次的发送。 40 | 41 | **确认帧丢失:**发送方发送 A、B、C 后,接收方到 C 已经全部接收成功,所以返回一个 ACK2 并将滑动窗口向后移动一帧,但是这个确认帧在返回过程中丢失,那么 A 在超时后会将这个滑动窗口内的所有帧进行重传,显然是落不到接收方的滑动窗口中的,所以接收方直接丢弃并返回最近的一个成功接收帧的 ACK,也就是 ACK2,发送方在接收后知道了,从而将滑动窗口向后移动。 42 | 43 | 44 | 45 | ## 选择重传协议 46 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1740189524253-3ffc4822-dfce-45f9-bb13-6eeff2f5eebb.jpeg) 47 | 48 | 在实际应用中发送窗口和接收窗口的大小通常是相同的。为什么 Wr<=Wt 呢?简单的来理解就是要保证接收方窗口的利用率高,如果接收方窗口大于发送方的话有些位置会长时间为空。 49 | 50 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735908058508-5eae9a1b-05ef-4852-a42c-a987a93ce3d8.png) 51 | 52 | 53 | 过程: 54 | 55 | 发送方在发送一个发送窗口中的帧后,等待接收接收方返回的 ACKi 如果整个发送窗口中的第一个帧(下界)收到对应的 ACKi 就可以将发送窗口向后滑动,接收窗口在 ACKi 发送出去后就向后滑动了。 56 | 57 | **数据帧丢失:**如果在发送方的 F(5 号帧) 在传输过程中丢失,那么接收窗口只会向后滑动一格,因为 F 没有收到,此时发送方只接收到了 ACK4,所以发送方的滑动窗口也只能向后滑动一格,发送方里新进入的 0 号帧会进行传输, 5 号帧会因为超时重传机制而重新传输,接收方在正确收到后返回对应的 ACKi 并将接收窗口向后滑动,发送方在接收到 ACKi 后滑动窗口也对应着向后滑动。**(在发送方发送了一个帧之后因为一些原因并没有到达接收方,那么接收方会先将这个帧之前和之后成功接收的帧的 ACKi 返回,接收窗口会向后滑动,发送方的发送窗口也会向后滑动(没有接收到 ACKi 的帧作为发送窗口的第一个帧),但是发送方丢失的帧并不会收到 ACKi,超时之后就会重传。)** 58 | 59 | **数据帧因为差错而被丢失:**接收方在接收了一个帧之后经检查发现这个帧有差错,会将这个帧进行丢弃的同时返回一个 NAKi 表示这个帧出现问题,需要重新传输 60 | 61 | **确认帧丢失:**意思就是接收窗口正常向后滑动了,但是发送窗口由于没有接收到对应帧的 ACKi,所以只能移动一部分,那些没有接收到 ACKi 的帧会重传,新进入的帧也会传输,接收方在接收到不是这个窗口的帧时(就是没有收到 ACKi 的帧)会直接认为是正确收到的返回对应的 ACKi 62 | 63 | 64 | 65 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740215467520-e031cc0b-c561-4e25-8790-a4c4f14f5998.png) 66 | 67 | ## 三种协议的信道利用率 68 | ### 知识总览 69 | ![画板](https://cdn.nlark.com/yuque/0/2025/jpeg/48073730/1735998136909-d560b2c5-29c8-407e-97ce-448026c7a1c2.jpeg) 70 | 71 | ### S-W 协议信道利用率 72 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735970667843-a007179c-97f8-4966-b912-e53cbbc6ac96.png) 73 | 74 | 因为确认 ACKi 传输时延很小所以在实际中都是不加入计算的。比较简单,利用画图进行理解 75 | 76 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735970837950-e67c5c1f-e250-4b09-9399-1a0bdf5822f4.png) 77 | 78 | 79 | 答案:D 80 | 81 | 设数据的传输时延为 xms,因为传播时延为 200ms 所以 RTT 为 400ms,根据公式可以得出 82 | 83 | ![image](https://cdn.nlark.com/yuque/__latex/8d99ade9640588d4c4ec2909b277ff8b.svg) 84 | 85 | 这样可以解出传输时延,然后根据数据传输速率就可以算出数据帧为 800bit 86 | 87 | 88 | 89 | ### GBN、SR 协议信道利用率 90 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735971341867-d2cc38f0-f146-42c5-9c4a-ae81130d291d.png) 91 | 92 | 根据图片非常好理解。如果在 ACKi 返回时发送方还在发送那么信道利用率为 1 93 | 94 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735971604329-53dc5fe6-932e-4e46-a230-7f41cb1b39ed.png) 95 | 96 | 97 | 答案:C 98 | 99 | 这道题我觉得可以使用两种想法来解决,一种是计算信道利用率,然后乘以信道带宽。一种是直接使用数据量除以传输时间。 100 | 101 | 1、一个帧长为 1000B,信道带宽 1000Mbps,所以可以得出一个帧的传输时延为 0.8ms,然后有 1000 个帧 102 | 103 | 所以全部的传输需要 80ms,给出了一个单向传播时延为 50ms,所以两个 100ms>80ms,传输都是在传播里面完成的,计算的话就是 80/(100+0.8)=0.8,这里需要加上刚开始的一个 0.8ms 的传输时延。0.8*100=80Mbps 104 | 105 | 2、根据 1 的计算可以得出 1000B*1000/(100ms + 0.8ms) = 80Mbps 106 | 107 | 108 | 109 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1735997182515-cee824df-b05d-484c-9c31-222d4f55445d.png) 110 | 111 | 112 | 答案:B 113 | 114 | **提到使用滑动窗口协议是指 GBN 或 SR 协议。** 115 | 116 | 这里进行说明:在理想情况下 GBN 和 SR 协议的信道利用率的方法是一样的 117 | 118 | 可以计算出一个帧的传输时延为 62.5ms,设发送窗口大小为 Wt 则根据公式 62.5xWt/500+62.5>=0.8 119 | 120 | 算出来 Wt>=7.2,设帧序号的比特数为 m,发送窗口和接收窗口需要满足 Wt+Wr<=2^m,GBN 的 Wr=1,所以此时 Wt+Wr=8.2 至少需要 4个比特数,SR 的 Wr>1,至少为 2,此时 Wt+Wr=9.2 至少需要 4 个比特数,所以 m 至少要为 4 121 | 122 | -------------------------------------------------------------------------------- /数据结构/数据结构实现和拓展/LinkList.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /** 6 | * 定义链表节点 7 | */ 8 | typedef struct Node { 9 | int data; 10 | struct Node *next; 11 | } Node; 12 | 13 | /** 14 | * 创建一个链表头结点 15 | * @return 头节点 16 | */ 17 | Node *createHead() { 18 | Node *head = (Node *) malloc(sizeof(Node)); 19 | if (head == NULL) { 20 | fprintf(stderr, "Memory allocation failed\n"); 21 | exit(EXIT_FAILURE); 22 | } 23 | head->data = 0; 24 | head->next = NULL; 25 | return head; 26 | } 27 | 28 | /** 29 | * 获取链表长度 30 | * @param head 链表头 31 | * @return 链表长度 32 | */ 33 | int getLinkLen(const Node *head) { 34 | return head->data; 35 | } 36 | 37 | /** 38 | * 创建一个节点 39 | * @param data 节点的值 40 | * @return 新节点 41 | */ 42 | Node *createNode(const int data) { 43 | Node *node = (Node *)malloc(sizeof(Node)); 44 | if (node == NULL) { 45 | fprintf(stderr, "Memory allocation failed\n"); 46 | exit(EXIT_FAILURE); 47 | } 48 | node->data = data; 49 | node->next = NULL; 50 | return node; 51 | } 52 | 53 | /** 54 | * 头插法 55 | * @param head 插入的链表头结点地址 56 | * @param data 插入的数据 57 | */ 58 | void insertHead(Node *head, const int data) { 59 | if (head == NULL) { 60 | printf("the head is the NULL\n"); 61 | return; 62 | } 63 | Node *newNode = createNode(data); 64 | newNode->next = head->next; 65 | head->next = newNode; 66 | head->data++; 67 | } 68 | 69 | /** 70 | * 71 | * @param head 链表头 72 | * @param data 元素值 73 | * @param location 插入的位置 74 | */ 75 | void insertLocation(Node *head, const int data, const int location) { 76 | if (head == NULL) { 77 | return; 78 | } 79 | head->data++; 80 | Node *node = createNode(data); 81 | Node *p = head; 82 | for (int i = 0; i < location - 1; i++) { 83 | p = p->next; 84 | } 85 | const Node *q = p->next; 86 | p->next = node; 87 | node->next = q->next; 88 | } 89 | 90 | /** 91 | * 尾差法 92 | * @param head 链表头 93 | * @param data 插入数值 94 | */ 95 | void insetEnd(Node *head, const int data) { 96 | if (head == NULL) { 97 | return; 98 | } 99 | head->data++; 100 | Node *p = head; 101 | //遍历到最后一个节点 102 | while (p->next) { 103 | p = p->next; 104 | } 105 | Node * node = createNode(data); 106 | p->next = node; 107 | } 108 | 109 | /** 110 | * 遍历链表 111 | * @param head 需要遍历的头节点 112 | */ 113 | void traversalLinkList(const Node *head){ 114 | if (head == NULL || head->next == NULL) { 115 | printf("the head is the NULL or empty list\n"); 116 | return; 117 | } 118 | const Node *node = head->next; 119 | printf("linkList has %d nodes: ", head->data); 120 | while (node != NULL) { 121 | printf("%d ", node->data); 122 | node = node->next; 123 | } 124 | printf("\n"); 125 | } 126 | 127 | /** 128 | * 129 | * @param head 查询链表头节点 130 | * @param num 查找的数值 131 | * @return 数值的位置 132 | */ 133 | int findElement(const Node *head, const int num) { 134 | if (head == NULL) { 135 | return -1; 136 | } 137 | int index = 1; 138 | for (const Node *node = head->next; node; node = node->next) { 139 | if (node->data == num) { 140 | return index; 141 | } 142 | index++; 143 | } 144 | return index; 145 | } 146 | 147 | /** 148 | * 149 | * @param head 链表的头节点 150 | * @param index 删除的位置 151 | */ 152 | void deletedElement(Node *head, const int index) { 153 | if (head == NULL || head->next == NULL) { 154 | return; 155 | } 156 | head->data--; 157 | Node *p = head; 158 | //遍历到要删除的节点之前 159 | for (int i = 0; i < index - 1; i++) { 160 | p = p->next; 161 | } 162 | //删除并释放节点 163 | Node *q = p->next; 164 | p->next = q->next; 165 | free(q); 166 | } 167 | 168 | /** 169 | * 释放链表节点 170 | * @param head 需要释放的头节点 171 | */ 172 | void freeNode(Node *head) { 173 | if (head == NULL) { 174 | return; 175 | } 176 | Node *p = head; 177 | while (p) { 178 | Node *q = p->next; 179 | free(p); 180 | p = q; 181 | } 182 | } 183 | 184 | /** 185 | * 2009年题 186 | * 已知一个带有表头结点的单链表,结点结构为 187 | * data link 188 | * 假设该链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表中倒数第k 189 | * 个位置上的结点(k 190 | * 为正整数)。若查找成功,算法输出该结点的data域的值,并返回1;否则,只返回0 191 | */ 192 | //思路:双指针法,快的先走k步,然后慢的和快的一起走,快的到了最后,慢的就是倒数的第k个了 193 | int findKBackWord(const Node *head, const int k) { 194 | if (head == NULL || k <= 0) { 195 | return 0; 196 | } 197 | const Node *fast = head; 198 | const Node *slow = head; 199 | for (int i = 0; i < k; i++) { 200 | fast = fast->next; 201 | } 202 | if (!fast) { 203 | return 0; 204 | } 205 | while (fast) { 206 | slow = slow->next; 207 | fast = fast->next; 208 | } 209 | printf("the back element %d is %d \n", k, slow->data); 210 | return 1; 211 | } 212 | 213 | int main(void){ 214 | Node *head = createHead(); 215 | for (int i = 0; i < 10; i++) { 216 | insertHead(head, i); 217 | } 218 | traversalLinkList(head); 219 | printf("the index of 5 is %d \n", findElement(head, 5)); 220 | printf("delete 5 location from the linkList .\n"); 221 | deletedElement(head, 5); 222 | traversalLinkList(head); 223 | printf("insert 10 in the linkList .\n"); 224 | insertHead(head, 10); 225 | traversalLinkList(head); 226 | // TODO: 测试2009年数据结构题 227 | printf("the result is %d \n", findKBackWord(head, 1)); 228 | freeNode(head); 229 | return 0; 230 | } 231 | -------------------------------------------------------------------------------- /书籍/计算机原理专栏/理解电路:从电报机到门电路,我们如何做到“千里传信”?.md: -------------------------------------------------------------------------------- 1 | # 12 理解电路:从电报机到门电路,我们如何做到“千里传信”? 2 | 3 | 我们前面讲过机器指令,你应该知道,所有最终执行的程序其实都是使用“0”和“1”这样的二进制代码来表示的。上一讲里,我也向你展示了,对应的整数和字符串,其实也是用“0”和“1”这样的二进制代码来表示的。 4 | 5 | 那么你可能要问了,我知道了这个有什么用呢?毕竟我们人用纸和笔来做运算,都是用十进制,直接用十进制和我们最熟悉的符号不是最简单么?为什么计算机里我们最终要选择二进制呢? 6 | 7 | 这一讲,我和你一起来看看,计算机在硬件层面究竟是怎么表示二进制的,以此你就会明白,为什么计算机会选择二进制。 8 | 9 | ## 从信使到电报,我们怎么做到“千里传书”? 10 | 11 | 马拉松的故事相信你听说过。公元前 490 年,在雅典附近的马拉松海边,发生了波斯和希腊之间的希波战争。雅典和斯巴达领导的希腊联军胜利之后,雅典飞毛腿菲迪皮德斯跑了历史上第一个马拉松,回雅典报喜。这个时候,人们在远距离报信的时候,采用的是派人跑腿,传口信或者送信的方式。 12 | 13 | 但是,这样靠人传口信或者送信的方式,实在是太慢了。在军事用途中,信息能否更早更准确地传递出去经常是事关成败的大事。所以我们看到中国古代的军队有“击鼓进军”和“鸣金收兵”,通过打鼓和敲钲发出不同的声音,来传递军队的号令。 14 | 15 | 如果我们把军队当成一台计算机,那“金”和“鼓”就是这台计算机的“1”和“0”。我们可以通过不同的编码方式,来指挥这支军队前进、后退、转向、追击等等。 16 | 17 | “金”和“鼓”比起跑腿传口信,固然效率更高了,但是能够传递的范围还是非常有限,超出个几公里恐怕就听不见了。于是,人们发明了更多能够往更远距离传信的方式,比如海上的灯塔、长城上的烽火台。因为光速比声速更快,传的距离也可以更远。 18 | 19 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/486201eca454fbda5b3a77ef29d27bf9.png) 20 | 21 | [图片来源](https://commons.wikimedia.org/wiki/File:PHAROS2006.jpg) 22 | 23 | 亚历山大港外的法罗斯灯塔,位列世界七大奇迹之一,可惜现在只剩下遗迹了。可见人类社会很早就学会使用类似二进制信号的方式来传输信息 24 | 25 | 但是,这些传递信息的方式都面临一个问题,就是受限于只有“1”和“0”这两种信号,不能传递太复杂的信息,那电报的发明就解决了这个问题。 26 | 27 | 从信息编码的角度来说,金、鼓、灯塔、烽火台类似电报的二进制编码。电报传输的信号有两种,一种是短促的**点信号**(dot 信号),一种是长一点的**划信号**(dash 信号)。我们把“点”当成“1”,把“划”当成“0”。这样一来,我们的电报信号就是另一种特殊的二进制编码了。电影里最常见的电报信号是“SOS”,这个信号表示出来就是 “点点点划划划点点点”。 28 | 29 | 比起灯塔和烽火台这样的设备,电报信号有两个明显的优势。第一,信号的传输距离迅速增加。因为电报本质上是通过电信号来进行传播的,所以从输入信号到输出信号基本上没有延时。第二,输入信号的速度加快了很多。电报机只有一个按钮,按下就是输入信号,按的时间短一点,就是发出了一个“点”信号;按的时间长一些,就是一个“划”信号。只要一个手指,就能快速发送电报。 30 | 31 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/5da409e31bd130129a5d669b143fa1a4.jpg) 32 | 33 | [图片来源](https://commons.wikimedia.org/wiki/File:Morsetaste.jpg) 34 | 35 | 一个摩尔斯电码的电报机 36 | 37 | 而且,制造一台电报机也非常容易。电报机本质上就是一个“**蜂鸣器 + 长长的电线 + 按钮开关**”。蜂鸣器装在接收方手里,开关留在发送方手里。双方用长长的电线连在一起。当按钮开关按下的时候,电线的电路接通了,蜂鸣器就会响。短促地按下,就是一个短促的点信号;按的时间稍微长一些,就是一个稍长的划信号。 38 | 39 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/283742f3a72eba22f6b4ae97e21c4112.jpg) 40 | 41 | 有了电池开关和铃铛,你就有了最简单的摩尔斯电码发报机 42 | 43 | ## 理解继电器,给跑不动的信号续一秒 44 | 45 | 有了电报机,只要铺设好电报线路,就可以传输我们需要的讯息了。但是这里面又出现了一个新的挑战,就是随着电线的线路越长,电线的电阻就越大。当电阻很大,而电压不够的时候,即使你按下开关,蜂鸣器也不会响。 46 | 47 | 你可能要说了,我们可以提高电压或者用更粗的电线,使得电阻更小,这样就可以让整个线路铺得更长一些。但是这个再长,也没办法从北京铺设到上海吧。要想从北京把电报发到上海,我们还得想些别的办法。 48 | 49 | 对于电报来说,电线太长了,使得线路接通也没有办法让蜂鸣器响起来。那么,我们就不要一次铺太长的线路,而把一小段距离当成一个线路,也和驿站建立一个小电报站。我们在小电报站里面安排一个电报员,他听到上一个小电报站发来的信息,然后原样输入,发到下一个电报站去。这样,我们的信号就可以一段段传输下去,而不会因为距离太长,导致电阻太大,没有办法成功传输信号。为了能够实现这样**接力传输信号**,在电路里面,工程师们造了一个叫作**继电器**(Relay)的设备。 50 | 51 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/1186a10341202ea36df27cba95f1cbea.jpg) 52 | 53 | 中继,其实就是不断地通过新的电源重新放大已经开始衰减的原有信号 54 | 55 | 事实上,这个过程中,我们需要在每一阶段**原样传输信号**,所以你可以想想,我们是不是可以设计一个设备来代替这个电报员?相比使用人工听蜂鸣器的声音,来重复输入信号,利用电磁效应和磁铁,来实现这个事情会更容易。 56 | 57 | 我们把原先用来输出声音的蜂鸣器,换成一段环形的螺旋线圈,让电路封闭通上电。因为电磁效应,这段螺旋线圈会产生一个带有磁性的电磁场。我们原本需要输入的按钮开关,就可以用一块磁力稍弱的磁铁把它设在“关”的状态。这样,按下上一个电报站的开关,螺旋线圈通电产生了磁场之后,磁力就会把开关“吸”下来,接通到下一个电报站的电路。 58 | 59 | 如果我们在中间所有小电报站都用这个“**螺旋线圈 + 磁性开关**”的方式,来替代蜂鸣器和普通开关,而只在电报的始发和终点用普通的开关和蜂鸣器,我们就有了一个拆成一段一段的电报线路,接力传输电报信号。这样,我们就不需要中间安排人力来听打电报内容,也不需要解决因为线缆太长导致的电阻太大或者电压不足的问题了。我们只要在终点站安排电报员,听写最终的电报内容就可以了。这样是不是比之前更省事了? 60 | 61 | 事实上,继电器还有一个名字就叫作**电驿**,这个“驿”就是驿站的驿,可以说非常形象了。这个接力的策略不仅可以用在电报中,在通信类的科技产品中其实都可以用到。 62 | 63 | 比如说,你在家里用 WiFi,如果你的屋子比较大,可能某些房间的信号就不好。你可以选用支持“中继”的 WiFi 路由器,在信号衰减的地方,增加一个 WiFi 设备,接收原来的 WiFi 信号,再重新从当前节点传输出去。这种中继对应的英文名词和继电器是一样的,也叫 Relay。 64 | 65 | 再比如说,我们现在互联网使用的光缆,是用光信号来传输数据。随着距离的增长、反射次数的增加,信号也会有所衰减,我们同样要每隔一段距离,来增加一个用来重新放大信号的中继。 66 | 67 | 有了继电器之后,我们不仅有了一个能够接力传输信号的方式,更重要的是,和输入端通过开关的“开”和“关”来表示“1”和“0”一样,我们在输出端也能表示“1”和“0”了。 68 | 69 | 输出端的作用,不仅仅是通过一个蜂鸣器或者灯泡,提供一个供人观察的输出信号,通过“螺旋线圈 + 磁性开关”,使得我们有“开”和“关”这两种状态,这个“开”和“关”表示的“1”和“0”,还可以作为后续线路的输入信号,让我们开始可以通过最简单的电路,来组合形成我们需要的逻辑。 70 | 71 | 通过这些线圈和开关,我们也可以很容易地创建出 “与(AND)”“或(OR)”“非(NOT)”这样的逻辑。我们在输入端的电路上,提供串联的两个开关,只有两个开关都打开,电路才接通,输出的开关也才能接通,这其实就是模拟了计算机里面的“与”操作。 72 | 73 | 我们在输入端的电路,提供两条独立的线路到输出端,两条线路上各有一个开关,那么任何一个开关打开了,到输出端的电路都是接通的,这其实就是模拟了计算机中的“或”操作。 74 | 75 | 当我们把输出端的“螺旋线圈 + 磁性开关”的组合,从默认关掉,只有通电有了磁场之后打开,换成默认是打开通电的,只有通电之后才关闭,我们就得到了一个计算机中的“非”操作。输出端开和关正好和输入端相反。这个在数字电路中,也叫作**反向器**(Inverter)。 76 | 77 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/977b09f3a334304c2861c6b420217b5e.jpg) 78 | 79 | 反向器的电路,其实就是开关从默认关闭变成默认开启而已 80 | 81 | 与、或、非的电路都非常简单,要想做稍微复杂一点的工作,我们需要很多电路的组合。不过,这也彰显了现代计算机体系中一个重要的思想,就是通过分层和组合,逐步搭建起更加强大的功能。 82 | 83 | 回到我们前面看的电报机原型,虽然一个按钮开关的电报机很“容易”操作,但是却不“方便”操作。因为电报员要熟记每一个字母对应的摩尔斯电码,并且需要快速按键来进行输入。一旦输错很难纠正。但是,因为电路之间可以通过与、或、非组合完成更复杂的功能,我们完全可以设计一个和打字机一样的电报机,每按下一个字母按钮,就会接通一部分电路,然后把这个字母的摩尔斯电码输出出去。 84 | 85 | 虽然在电报机时代,我们没有这么做,但是在计算机时代,我们其实就是这样做的。我们不再是给计算机“0”和“1”,而是通过千万个晶体管组合在一起,最终使得我们可以用“高级语言”,指挥计算机去干什么。 86 | 87 | ## 总结延伸 88 | 89 | 可以说,电报是现代计算机的一个最简单的原型。它和我们现在使用的现代计算机有很多相似之处。我们通过电路的“开”和“关”,来表示“1”和“0”。就像晶体管在不同的情况下,表现为导电的“1”和绝缘的“0”的状态。 90 | 91 | 我们通过电报机这个设备,看到了如何通过“螺旋线圈 + 开关”,来构造基本的逻辑电路,我们也叫门电路。一方面,我们可以通过继电器或者中继,进行长距离的信号传输。另一方面,我们也可以通过设置不同的线路和开关状态,实现更多不同的信号表示和处理方式,这些线路的连接方式其实就是我们在数字电路中所说的门电路。而这些门电路,也是我们创建 CPU 和内存的基本逻辑单元。我们的各种对于计算机二进制的“0”和“1”的操作,其实就是来自于门电路,叫作组合逻辑电路。 92 | 93 | ## 推荐阅读 94 | 95 | 《编码:隐匿在计算机软硬件背后的语言》的第 6~11 章,是一个很好的入门材料,可以帮助你深入理解数字电路,值得你花时间好好读一读。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”.md: -------------------------------------------------------------------------------- 1 | # 11 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”? 2 | 3 | 上算法和数据结构课的时候,老师们都会和你说,程序 = 算法 + 数据结构。如果对应到组成原理或者说硬件层面,算法就是我们前面讲的各种计算机指令,数据结构就对应我们接下来要讲的二进制数据。 4 | 5 | 众所周知,现代计算机都是用 0 和 1 组成的二进制,来表示所有的信息。前面几讲的程序指令用到的机器码,也是使用二进制表示的;我们存储在内存里面的字符串、整数、浮点数也都是用二进制表示的。万事万物在计算机里都是 0 和 1,所以呢,搞清楚各种数据在二进制层面是怎么表示的,是我们必备的一课。 6 | 7 | 大部分教科书都会详细地从整数的二进制表示讲起,相信你在各种地方都能看到对应的材料,所以我就不再啰啰嗦嗦地讲这个了,只会快速地浏览一遍整数的二进制表示。 8 | 9 | 然后呢,我们重点来看一看,大家在实际应用中最常遇到的问题,也就是文本字符串是怎么表示成二进制的,特别是我们会遇到的乱码究竟是怎么回事儿。我们平时在开发的时候,所说的 Unicode 和 UTF-8 之间有什么关系。理解了这些,相信以后遇到任何乱码问题,你都能手到擒来了。 10 | 11 | ## 理解二进制的“逢二进一” 12 | 13 | 二进制和我们平时用的十进制,其实并没有什么本质区别,只是平时我们是“逢十进一”,这里变成了“逢二进一”而已。每一位,相比于十进制下的 0~9 这十个数字,我们只能用 0 和 1 这两个数字。 14 | 15 | 任何一个十进制的整数,都能通过二进制表示出来。把一个二进制数,对应到十进制,非常简单,就是把从右到左的第 N 位,乘上一个 2 的 N 次方,然后加起来,就变成了一个十进制数。当然,既然二进制是一个面向程序员的“语言”,这个从右到左的位置,自然是从 0 开始的。 16 | 17 | 比如 0011 这个二进制数,对应的十进制表示,就是 0×23+0×22+1×21+1×200×23+0×22+1×21+1×20 =3=3,代表十进制的 3。 18 | 19 | 对应地,如果我们想要把一个十进制的数,转化成二进制,使用**短除法**就可以了。也就是,把十进制数除以 2 的余数,作为最右边的一位。然后用商继续除以 2,把对应的余数紧靠着刚才余数的右侧,这样递归迭代,直到商为 0 就可以了。 20 | 21 | 比如,我们想把 13 这个十进制数,用短除法转化成二进制,需要经历以下几个步骤: 22 | 23 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/a2b6f2a92bcf99e9f96367bbb90383d8.jpg) 24 | 25 | 因此,对应的二进制数,就是 1101。 26 | 27 | 刚才我们举的例子都是正数,对于负数来说,情况也是一样的吗?我们可以把一个数最左侧的一位,当成是对应的正负号,比如 0 为正数,1 为负数,这样来进行标记。 28 | 29 | 这样,一个 4 位的二进制数, 0011 就表示为 +3。而 1011 最左侧的第一位是 1,所以它就表示 -3。这个其实就是整数的**原码表示法**。原码表示法有一个很直观的缺点就是,0 可以用两个不同的编码来表示,1000 代表 0, 0000 也代表 0。习惯万事一一对应的程序员看到这种情况,必然会被“逼死”。 30 | 31 | 于是,我们就有了另一种表示方法。我们仍然通过最左侧第一位的 0 和 1,来判断这个数的正负。但是,我们不再把这一位当成单独的符号位,在剩下几位计算出的十进制前加上正负号,而是在计算整个二进制值的时候,在左侧最高位前面加个负号。 32 | 33 | 比如,一个 4 位的二进制补码数值 1011,转换成十进制,就是 −1×23+0×22+1×21+1×20−1×23+0×22+1×21+1×20 =−5=−5。如果最高位是 1,这个数必然是负数;最高位是 0,必然是正数。并且,只有 0000 表示 0,1000 在这样的情况下表示 -8。一个 4 位的二进制数,可以表示从 -8 到 7 这 16 个整数,不会白白浪费一位。 34 | 35 | 当然更重要的一点是,用补码来表示负数,使得我们的整数相加变得很容易,不需要做任何特殊处理,只是把它当成普通的二进制相加,就能得到正确的结果。 36 | 37 | 我们简单一点,拿一个 4 位的整数来算一下,比如 -5 + 1 = -4,-5 + 6 = 1。我们各自把它们转换成二进制来看一看。如果它们和无符号的二进制整数的加法用的是同样的计算方式,这也就意味着它们是同样的电路。 38 | 39 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/27fbe9a3f05b84480384fc18d74e3316.jpg) 40 | 41 | ## 字符串的表示,从编码到数字 42 | 43 | 不仅数值可以用二进制表示,字符乃至更多的信息都能用二进制表示。最典型的例子就是**字符串**(Character String)。最早计算机只需要使用英文字符,加上数字和一些特殊符号,然后用 8 位的二进制,就能表示我们日常需要的所有字符了,这个就是我们常常说的**ASCII 码**(American Standard Code for Information Interchange,美国信息交换标准代码)。 44 | 45 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/bee81480de3f6e7181cb7bb5f55cc805.png) 46 | 47 | [图片来源](https://en.wikipedia.org/wiki/ASCII) 48 | 49 | ASCII 码就好比一个字典,用 8 位二进制中的 128 个不同的数,映射到 128 个不同的字符里。比如,小写字母 a 在 ASCII 里面,就是第 97 个,也就是二进制的 0110 0001,对应的十六进制表示就是 61。而大写字母 A,就是第 65 个,也就是二进制的 0100 0001,对应的十六进制表示就是 41。 50 | 51 | 在 ASCII 码里面,数字 9 不再像整数表示法里一样,用 0000 1001 来表示,而是用 0011 1001 来表示。字符串 15 也不是用 0000 1111 这 8 位来表示,而是变成两个字符 1 和 5 连续放在一起,也就是 0011 0001 和 0011 0101,需要用两个 8 位来表示。 52 | 53 | 我们可以看到,最大的 32 位整数,就是 2147483647。如果用整数表示法,只需要 32 位就能表示了。但是如果用字符串来表示,一共有 10 个字符,每个字符用 8 位的话,需要整整 80 位。比起整数表示法,要多占很多空间。 54 | 55 | 这也是为什么,很多时候我们在存储数据的时候,要采用二进制序列化这样的方式,而不是简单地把数据通过 CSV 或者 JSON,这样的文本格式存储来进行序列化。**不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。** 56 | 57 | ASCII 码只表示了 128 个字符,一开始倒也堪用,毕竟计算机是在美国发明的。然而随着越来越多的不同国家的人都用上了计算机,想要表示譬如中文这样的文字,128 个字符显然是不太够用的。于是,计算机工程师们开始各显神通,给自己国家的语言创建了对应的**字符集**(Charset)和**字符编码**(Character Encoding)。 58 | 59 | 字符集,表示的可以是字符的一个集合。比如“中文”就是一个字符集,不过这样描述一个字符集并不准确。想要更精确一点,我们可以说,“第一版《新华字典》里面出现的所有汉字”,这是一个字符集。这样,我们才能明确知道,一个字符在不在这个集合里面。比如,我们日常说的 Unicode,其实就是一个字符集,包含了 150 种语言的 14 万个不同的字符。 60 | 61 | 而字符编码则是对于字符集里的这些字符,怎么一一用二进制表示出来的一个字典。我们上面说的 Unicode,就可以用 UTF-8、UTF-16,乃至 UTF-32 来进行编码,存储成二进制。所以,有了 Unicode,其实我们可以用不止 UTF-8 一种编码形式,我们也可以自己发明一套 GT-32 编码,比如就叫作 Geek Time 32 好了。只要别人知道这套编码规则,就可以正常传输、显示这段代码。 62 | 63 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/9911c58d79e8a1f106d48a83457d193e.jpg) 64 | 65 | 同样的文本,采用不同的编码存储下来。如果另外一个程序,用一种不同的编码方式来进行解码和展示,就会出现乱码。这就好像两个军队用密语通信,如果用错了密码本,那看到的消息就会不知所云。在中文世界里,最典型的就是“手持两把锟斤拷,口中疾呼烫烫烫”的典故。 66 | 67 | 我曾经听说过这么一个笑话,没有经验的同学,在看到程序输出“烫烫烫”的时候,以为是程序让 CPU 过热发出报警,于是尝试给 CPU 降频来解决问题。 68 | 69 | 既然今天要彻底搞清楚编码知识,我们就来弄清楚“锟斤拷”和“烫烫烫”的来龙去脉。 70 | 71 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/5c6e03705f50c250ccb5300849c281fd.png) 72 | 73 | 搜索了一下我自己的个人邮件历史记录,不出意外, 里面出现了各种“锟斤拷” 74 | 75 | 首先,“锟斤拷”的来源是这样的。如果我们想要用 Unicode 编码记录一些文本,特别是一些遗留的老字符集内的文本,但是这些字符在 Unicode 中可能并不存在。于是,Unicode 会统一把这些字符记录为 U+FFFD 这个编码。如果用 UTF-8 的格式存储下来,就是\xef\xbf\xbd。如果连续两个这样的字符放在一起,\xef\xbf\xbd\xef\xbf\xbd,这个时候,如果程序把这个字符,用 GB2312 的方式进行 decode,就会变成“锟斤拷”。这就好比我们用 GB2312 这本密码本,去解密别人用 UTF-8 加密的信息,自然没办法读出有用的信息。 76 | 77 | 而“烫烫烫”,则是因为如果你用了 Visual Studio 的调试器,默认使用 MBCS 字符集。“烫”在里面是由 0xCCCC 来表示的,而 0xCC 又恰好是未初始化的内存的赋值。于是,在读到没有赋值的内存地址或者变量的时候,电脑就开始大叫“烫烫烫”了。 78 | 79 | 了解了这些原理,相信你未来在遇到中文的编码问题的时候,可以做到“手中有粮,心中不慌”了。 80 | 81 | ## 总结延伸 82 | 83 | 到这里,相信你发现,我们可以用二进制编码的方式,表示任意的信息。只要建立起字符集和字符编码,并且得到大家的认同,我们就可以在计算机里面表示这样的信息了。所以说,如果你有心,要发明一门自己的克林贡语并不是什么难事。 84 | 85 | 不过,光是明白怎么把数值和字符在逻辑层面用二进制表示是不够的。我们在计算机组成里面,关心的不只是数值和字符的逻辑表示,更要弄明白,在硬件层面,这些数值和我们一直提的晶体管和电路有什么关系。下一讲,我就会为你揭开神秘的面纱。我会从时钟和 D 触发器讲起,最终让你明白,计算机里的加法,是如何通过电路来实现的。 86 | 87 | ## 推荐阅读 88 | 89 | 关于二进制和编码,我推荐你读一读《编码:隐匿在计算机软硬件背后的语言》。从电报机到计算机,这本书讲述了很多计算设备的历史故事,当然,也包含了二进制及其背后对应的电路原理。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/给你一张知识地图,计算机组成原理应该这么学.md: -------------------------------------------------------------------------------- 1 | 了解了现代计算机的基本硬件组成和背后最基本的冯·诺依曼体系结构,我们就可以正式进入计算机组成原理的学习了。在学习一个一个零散的知识点之前,我整理了一份学习地图,好让你对将要学习的内容有一个总纲层面的了解。 2 | 3 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/12bc980053ea355a201e2b529048e2ff.jpg) 4 | 5 | 建议保存后查看大图 6 | 7 | 从这张图可以看出来,**整个计算机组成原理,就是围绕着计算机是如何组织运作展开的**。 8 | 9 | ## 计算机组成原理知识地图 10 | 11 | 计算机组成原理的英文叫 Computer Organization。这里的 Organization 是“组织机构”的意思。计算机由很多个不同的部件放在一起,变成了一个“组织机构”。这个组织机构最终能够进行各种计算、控制、读取输入,进行输出,达成各种强大的功能。 12 | 13 | 在这张图里面,我们把整个计算机组成原理的知识点拆分成了四大部分,分别是计算机的基本组成、计算机的指令和计算、处理器设计,以及存储器和 I/O 设备。 14 | 15 | 首先,我们来看**计算机的基本组成**。 16 | 17 | 这一部分,你需要学习计算机是由哪些硬件组成的。这些硬件,又是怎么对应到经典的冯·诺依曼体系结构中的,也就是运算器、控制器、存储器、输入设备和输出设备这五大基本组件。除此之外,你还需要了解计算机的两个核心指标,性能和功耗。性能和功耗也是我们在应用和设计五大基本组件中需要重点考虑的因素。 18 | 19 | 了解了组成部分,接下来你需要掌握**计算机的指令和计算**。 20 | 21 | 在计算机指令部分,你需要搞明白,我们每天撰写的一行行 C、Java、PHP 程序,是怎么在计算机里面跑起来的。这里面,你既需要了解我们的程序是怎么通过编译器和汇编器,变成一条条机器指令这样的编译过程(如果把编译过程展开的话,可以变成一门完整的编译原理课程),还需要知道我们的操作系统是怎么链接、装载、执行这些程序的(这部分知识如果再深入学习,又可以变成一门操作系统课程)。而这一条条指令执行的控制过程,就是由计算机五大组件之一的**控制器**来控制的。 22 | 23 | 在计算机的计算部分,你要从二进制和编码开始,理解我们的数据在计算机里的表示,以及我们是怎么从数字电路层面,实现加法、乘法这些基本的运算功能的。实现这些运算功能的 ALU(Arithmetic Logic Unit/ALU),也就是算术逻辑单元,其实就是我们计算机五大组件之一的**运算器**。 24 | 25 | 这里面有一个在今天看起来特别重要的知识点,就是浮点数(Floating Point)。浮点数是我们在日常运用中非常容易用错的一种数据表示形式。掌握浮点数能让你对数据的编码、存储和计算能够有一个从表到里的深入理解。尤其在 AI 火热的今天,浮点数是机器学习中重度使用的数据表示形式,掌握它更是非常有必要。 26 | 27 | 明白计算机指令和计算是如何运转的,我们就可以深入到**CPU 的设计**中去一探究竟了。 28 | 29 | CPU 时钟可以用来构造寄存器和内存的锁存器和触发器,因此,CPU 时钟应该是我们学习 CPU 的前导知识。搞明白我们为什么需要 CPU 时钟(CPU Clock),以及寄存器和内存是用什么样的硬件组成的之后,我们可以再来看看,整个计算机的数据通路是如何构造出来的。 30 | 31 | 数据通路,其实就是连接了整个运算器和控制器,并最终组成了 CPU。而出于对于性能和功耗的考虑,你要进一步理解和掌握面向流水线设计的 CPU、数据和控制冒险,以及分支预测的相关技术。 32 | 33 | 既然 CPU 作为控制器要和输入输出设备通信,那么我们就要知道异常和中断发生的机制。在 CPU 设计部分的最后,我会讲一讲指令的并行执行,看看如何直接在 CPU 层面,通过 SIMD 来支持并行计算。 34 | 35 | 最后,我们需要看一看,计算机五大组成部分之一,**存储器的原理**。通过存储器的层次结构作为基础的框架引导,你需要掌握从上到下的 CPU 高速缓存、内存、SSD 硬盘和机械硬盘的工作原理,它们之间的性能差异,以及实际应用中利用这些设备会遇到的挑战。存储器其实很多时候又扮演了输入输出设备的角色,所以你需要进一步了解,CPU 和这些存储器之间是如何进行通信的,以及我们最重视的性能问题是怎么一回事;理解什么是 IO_WAIT,如何通过 DMA 来提升程序性能。 36 | 37 | 对于存储器,我们不仅需要它们能够正常工作,还要确保里面的数据不能丢失。于是你要掌握我们是如何通过 RAID、Erasure Code、ECC 以及分布式 HDFS,这些不同的技术,来确保数据的完整性和访问性能。 38 | 39 | ## 学习计算机组成原理,究竟有没有好办法? 40 | 41 | 相信这个学习地图,应该让你对计算机组成这门课要学些什么,有了一些了解。不过这个地图上的知识点繁多,应该也给你带来了不小的挑战。 42 | 43 | 我上一节也说过,相较于整个计算机科学中的其他科目,计算机组成原理更像是整个计算机学科里的“纲要”。这门课里任何一个知识点深入挖下去,都可以变成计算机科学里的一门核心课程。 44 | 45 | 比如说,程序怎样从高级代码变成指令在计算机里面运行,对应着“编译原理”和“操作系统”这两门课程;计算实现背后则是“数字电路”;如果要深入 CPU 和存储器系统的优化,必然要深入了解“计算机体系结构”。 46 | 47 | 因此,为了帮你更快更好地学计算机组成,我为你总结了三个学习方法,帮你更好地掌握这些知识点,并且能够学为所用,让你在工作中能够用得上。 48 | 49 | 首先,**学会提问自己来串联知识点**。学完一个知识点之后,你可以从下面两个方面,问一下自己。 50 | 51 | - 我写的程序,是怎样从输入的代码,变成运行的程序,并得到最终结果的? 52 | - 整个过程中,计算器层面到底经历了哪些步骤,有哪些地方是可以优化的? 53 | 54 | 无论是程序的编译、链接、装载和执行,以及计算时需要用到的逻辑电路、ALU,乃至 CPU 自发为你做的流水线、指令级并行和分支预测,还有对应访问到的硬盘、内存,以及加载到高速缓存中的数据,这些都对应着我们学习中的一个个知识点。建议你自己脑子里过一遍,最好时口头表述一遍或者写下来,这样对你彻底掌握这些知识点都会非常有帮助。 55 | 56 | 其次,**写一些示例程序来验证知识点。**计算机科学是一门实践的学科。计算机组成中的大量原理和设计,都对应着“性能”这个词。因此,通过把对应的知识点,变成一个个性能对比的示例代码程序记录下来,是把这些知识点融汇贯通的好方法。因为,相比于强记硬背知识点,一个有着明确性能对比的示例程序,会在你脑海里留下更深刻的印象。当你想要回顾这些知识点的时候,一个程序也更容易提示你把它从脑海深处里面找出来。 57 | 58 | 最后,**通过和计算机硬件发展的历史做对照**。计算机的发展并不是一蹴而就的。从第一台电子计算机 ENIAC(Electronic Numerical Integrator And Computer,电子数值积分计算机)的发明到现在,已经有 70 多年了。现代计算机用的各个技术,都是跟随实际应用中遇到的挑战,一个个发明、打磨,最后保留下来的。这当中不仅仅有学术层面的碰撞,更有大量商业层面的交锋。通过了解充满戏剧性和故事性的计算机硬件发展史,让你更容易理解计算机组成中各种原理的由来。 59 | 60 | 比如说,奔腾 4 和 SPARC 的失败,以及 ARM 的成功,能让我们记住 CPU 指令集的繁与简、权衡性能和功耗的重要性,而现今高速发展的机器学习和边缘计算,又给计算机硬件设计带来了新的挑战。 61 | 62 | ## 给松鼠症患者的学习资料 63 | 64 | 学习总是要花点笨功夫的。最有效的办法还是“读书百遍,其义自见”。对于不够明白的知识点,多搜索,多看不同来源的资料,多和朋友、同事、老师一起交流,一定能够帮你掌握好想要学习的知识点。 65 | 66 | 在这个专栏之前,计算机组成原理,已经有很多优秀的图书和课程珠玉在前了。为了覆盖更多知识点的细节,这些书通常都有点厚,课程都会有点长。不过作为专栏的补充阅读材料,却是最合适不过了。 67 | 68 | 因此,每一讲里,我都会留下一些“**补充阅读**”的材料。如果你想更进一步理解更多深入的计算机组成原理的知识,乃至更多相关的其他核心课程的知识,多用一些业余时间来看一看,读一读这些“补充阅读”也一定不会让你对花在上面的时间后悔的。 69 | 70 | 下面给你推荐一些我自己看过、读过的内容。我在之后的文章里推荐的“补充阅读”,大部分都是来自这些资料。你可以根据自己的情况来选择学习。 71 | 72 | ### 入门书籍 73 | 74 | 我知道,订阅这个专栏的同学,有很多是非计算机科班出身,我建议你先对计算机组成原理这门课有个基本概念。建立这个概念,有两种方法,第一,你可以把我上面那张地图的核心内容记下来,对这些内容之间的关系先有个大致的了解。 75 | 76 | 第二,我推荐你阅读两本书,准确地说,这其实是两本小册子,因为它们非常轻薄、好读,而且图文并茂,非常适合初学者和想要入门组成原理的同学。一本是《计算机是怎样跑起来的》,另一本是《程序是怎样跑起来的》。我要特别说一下后面这本,它可以说是一个入门微缩版本的“计算机组成原理”。 77 | 78 | 除此之外,计算机组成中,硬件层面的基础实现,比如寄存器、ALU 这些电路是怎么回事,你可以去看一看 Coursera 上的北京大学免费公开课[《](https://www.coursera.org/learn/jisuanji-zucheng)[Computer Organization](https://www.coursera.org/learn/jisuanji-zucheng)[》](https://www.coursera.org/learn/jisuanji-zucheng)。这个视频课程的视频部分也就 10 多个小时。在学习专栏相应章节的前后去浏览一遍,相信对你了解程序在电路层面会变成什么样子有所帮助。 79 | 80 | ### 深入学习书籍 81 | 82 | 对于想要深入掌握计算机组成的同学,我推荐你去读一读《计算机组成与设计:硬件 / 软件接口》和经典的《深入理解计算机系统》这两本书。后面这本被称为 CSAPP 的经典教材,网上也有配套的视频课程。我在这里给你推荐两个不同版本的链接([B](https://www.bilibili.com/video/av24540152/)[ilibili 版](https://www.bilibili.com/video/av24540152/)和[Y](https://www.youtube.com/playlist?list=PLmBgoRqEQCWy58EIwLSWwMPfkwLOLRM5R)[outube 版](https://www.youtube.com/playlist?list=PLmBgoRqEQCWy58EIwLSWwMPfkwLOLRM5R) )。不过这两本都在 500 页以上,坚持啃下来需要不少实践经验。 83 | 84 | 计算机组成原理还有一本的经典教材,就是来自操作系统大神塔能鲍姆(Andrew S. Tanenbaum)的《计算机组成:结构化方法》。这本书的组织结构和其他教材都不太一样,适合作为一个辅助的参考书来使用。 85 | 86 | 如果在学习这个专栏的过程中,引发了你对于计算机体系结构的兴趣,你还可以深入读一读《计算机体系结构:量化研究方法》。 87 | 88 | ### 课外阅读 89 | 90 | 在上面这些教材之外,对于资深程序员来说,来自 Redhat 的**What Every Programmer Should Know About Memory**是写出高性能程序不可不读的经典材料。而 LMAX 开源的 Disruptor,则是通过实际应用程序,来理解计算机组成原理中各个知识点的最好范例了。 91 | 92 | 《编码:隐匿在计算机软硬件背后的语言》和《程序员的自我修养:链接、装载和库》是理解计算机硬件和操作系统层面代码执行的优秀阅读材料。 93 | 94 | ## 总结延伸 95 | 96 | 学习不是死记硬背,学习材料也不是越多越好。到了这里,希望你不要因为我给出了太多可以学习的材料,结果成了“松鼠症”患者,光囤积材料,却没有花足够多的时间去学习这些知识。 97 | 98 | 我工作之后一直在持续学习,在这个过程中,我发现最有效的办法,**不是短时间冲刺,而是有节奏地坚持,希望你能够和专栏的发布节奏同步推进,做好思考题,并且多在留言区和其他朋友一起交流**,就更容易能够“积小步而至千里”,在程序员这个职业上有更长足的发展。 99 | 100 | 好了,对于学习资料的介绍就到这里了。希望在接下来的几个月里,你能和我一起走完这趟“计算机组成”之旅,从中收获到知识和成长。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/加法器:如何像搭乐高一样搭电路(上).md: -------------------------------------------------------------------------------- 1 | # 13 加法器:如何像搭乐高一样搭电路(上)? 2 | 3 | 上一讲,我们看到了如何通过电路,在计算机硬件层面设计最基本的单元,门电路。我给你看的门电路非常简单,只能做简单的 “与(AND)”“或(OR)”“NOT(非)”和“异或(XOR)”,这样最基本的单比特逻辑运算。下面这些门电路的标识,你需要非常熟悉,后续的电路都是由这些门电路组合起来的。 4 | 5 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/94194480bcfd3b5366e4649ee80de4f6.jpg) 6 | 7 | 这些基本的门电路,是我们计算机硬件端的最基本的“积木”,就好像乐高积木里面最简单的小方块。看似不起眼,但是把它们组合起来,最终可以搭出一个星球大战里面千年隼这样的大玩意儿。我们今天包含十亿级别晶体管的现代 CPU,都是由这样一个一个的门电路组合而成的。 8 | 9 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/2f20b26b1ed7f9d26c5a0858ad6770b7.jpg) 10 | 11 | [图片来源](https://www.flickr.com/photos/stickkim/7053151615/in/photolist-bKgffk-ogjPUr-bK5EB2-9KVuH1-cTubW1-fmT46W-fmCXpM-q4xNPg-ASbuvs-cTubiG-dzY1Ge-i9gZiN-cTuciQ-ijVpAw-aAnA68-fmCZvg-yfnA5X-zobNFw-jt28Zq-afa117-Av96ec-ntmgkW-rMD4KE-CgYrKU-L6YMgi-KgSyBJ-81yeEt-2s3w16-ReD2-VWSj-46LiG-cgy2zY-hLG2X1-aZZ6Rc-ac5vyy-21LNDAq-21vQ14P-46KYN-22NLSaf-q6QoLS-4BNrBP-4jY2Bj-nD232N-aYaGWX-XwJrFZ-569dUN-wYEBV5-cpHkWN-bazBbP-4BSGGJ) 12 | 13 | ## 异或门和半加器 14 | 15 | 我们看到的基础门电路,输入都是两个单独的 bit,输出是一个单独的 bit。如果我们要对 2 个 8 位(bit)的数,计算与、或、非这样的简单逻辑运算,其实很容易。只要连续摆放 8 个开关,来代表一个 8 位数。这样的两组开关,从左到右,上下单个的位开关之间,都统一用“与门”或者“或门”连起来,就是两个 8 位数的 AND 或者 OR 的运算了。 16 | 17 | 比起 AND 或者 OR 这样的电路外,要想实现整数的加法,就需要组建稍微复杂一点儿的电路了。 18 | 19 | 我们先回归一个最简单的 8 位的无符号整数的加法。这里的“无符号”,表示我们并不需要使用补码来表示负数。无论高位是“0”还是“1”,这个整数都是一个正数。 20 | 21 | 我们很直观就可以想到,要表示一个 8 位数的整数,简单地用 8 个 bit,也就是 8 个像上一讲的电路开关就好了。那 2 个 8 位整数的加法,就是 2 排 8 个开关。加法得到的结果也是一个 8 位的整数,所以又需要 1 排 8 位的开关。要想实现加法,我们就要看一下,通过什么样的门电路,能够连接起加数和被加数,得到最后期望的和。 22 | 23 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/281879883d285478b7771f576f4b3066.jpg) 24 | 25 | 其实加法器就是想一个办法把这三排开关电路连起来 26 | 27 | 要做到这一点,我们先来看看,我们人在计算加法的时候一般会怎么操作。二进制的加法和十进制没什么区别,所以我们一样可以用**列竖式**来计算。我们仍然是从左到右,一位一位进行计算,只是把从逢 10 进 1 变成逢 2 进 1。 28 | 29 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/1854b98fcac2c6bf4949ac5e2247d9d1.jpg) 30 | 31 | 你会发现,其实计算一位数的加法很简单。我们先就看最简单的个位数。输入一共是 4 种组合,00、01、10、11。得到的结果,也不复杂。 32 | 33 | 一方面,我们需要知道,加法计算之后的个位是什么,在输入的两位是 00 和 11 的情况下,对应的输出都应该是 0;在输入的两位是 10 和 01 的情况下,输出都是 1。结果你会发现,这个输入和输出的对应关系,其实就是我在上一讲留给你的思考题里面的“异或门(XOR)”。 34 | 35 | 讲与、或、非门的时候,我们很容易就能和程序里面的“AND(通常是 & 符号)”“ OR(通常是 | 符号)”和“ NOT(通常是 ! 符号)”对应起来。可能你没有想过,为什么我们会需要“异或(XOR)”,这样一个在逻辑运算里面没有出现的形式,作为一个基本电路。**其实,异或门就是一个最简单的整数加法,所需要使用的基本门电路**。 36 | 37 | 算完个位的输出还不算完,输入的两位都是 11 的时候,我们还需要向更左侧的一位进行进位。那这个就对应一个与门,也就是有且只有在加数和被加数都是 1 的时候,我们的进位才会是 1。 38 | 39 | 所以,通过一个异或门计算出个位,通过一个与门计算出是否进位,我们就通过电路算出了一个一位数的加法。于是,**我们把两个门电路打包,给它取一个名字,就叫作半加器**(Half Adder)。 40 | 41 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/5860fd8c4ace079b40e66b9568d2b81e.jpg) 42 | 43 | 半加器的电路演示 44 | 45 | ## 全加器 46 | 47 | 你肯定很奇怪,为什么我们给这样的电路组合,取名叫半加器(Half Adder)?莫非还有一个全加器(Full Adder)么?你猜得没错。半加器可以解决个位的加法问题,但是如果放到二位上来说,就不够用了。我们这里的竖式是个二进制的加法,所以如果从右往左数,第二列不是十位,我称之为“二位”。对应的再往左,就应该分别是四位、八位。 48 | 49 | 二位用一个半加器不能计算完成的原因也很简单。因为二位除了一个加数和被加数之外,还需要加上来自个位的进位信号,一共需要三个数进行相加,才能得到结果。但是我们目前用到的,无论是最简单的门电路,还是用两个门电路组合而成的半加器,输入都只能是两个 bit,也就是两个开关。那我们该怎么办呢? 50 | 51 | 实际上,解决方案也并不复杂。**我们用两个半加器和一个或门,就能组合成一个全加器**。第一个半加器,我们用和个位的加法一样的方式,得到是否进位 X 和对应的二个数加和后的结果 Y,这样两个输出。然后,我们把这个加和后的结果 Y,和个位数相加后输出的进位信息 U,再连接到一个半加器上,就会再拿到一个是否进位的信号 V 和对应的加和后的结果 W。 52 | 53 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/3f11f278ba8f24209a56fb3ee1ca9e2a.jpg) 54 | 55 | 全加器就是两个半加器加上一个或门 56 | 57 | 这个 W 就是我们在二位上留下的结果。我们把两个半加器的进位输出,作为一个或门的输入连接起来,只要两次加法中任何一次需要进位,那么在二位上,我们就会向左侧的四位进一位。因为一共只有三个 bit 相加,即使 3 个 bit 都是 1,也最多会进一位。 58 | 59 | 这样,通过两个半加器和一个或门,我们就得到了一个,能够接受进位信号、加数和被加数,这样三个数组成的加法。这就是我们需要的全加器。 60 | 61 | 有了全加器,我们要进行对应的两个 8 bit 数的加法就很容易了。我们只要把 8 个全加器串联起来就好了。个位的全加器的进位信号作为二位全加器的输入信号,二位全加器的进位信号再作为四位的全加器的进位信号。这样一层层串接八层,我们就得到了一个支持 8 位数加法的算术单元。如果要扩展到 16 位、32 位,乃至 64 位,都只需要多串联几个输入位和全加器就好了。 62 | 63 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/68cd38910f526c149d232720b82b6ca1.jpeg) 64 | 65 | 8 位加法器可以由 8 个全加器串联而成 66 | 67 | 唯一需要注意的是,对于这个全加器,在个位,我们只需要用一个半加器,或者让全加器的进位输入始终是 0。因为个位没有来自更右侧的进位。而最左侧的一位输出的进位信号,表示的并不是再进一位,而是表示我们的加法是否溢出了。 68 | 69 | 这也是很有意思的一点。以前我自己在了解二进制加法的时候,一直有这么个疑问,既然 int 这样的 16 位的整数加法,结果也是 16 位数,那我们怎么知道加法最终是否溢出了呢?因为结果也只存得下加法结果的 16 位数。我们并没有留下一个第 17 位,来记录这个加法的结果是否溢出。 70 | 71 | 看到全加器的电路设计,相信你应该明白,在整个加法器的结果中,我们其实有一个电路的信号,会标识出加法的结果是否溢出。我们可以把这个对应的信号,输出给到硬件中其他标志位里,让我们的计算机知道计算的结果是否溢出。而现代计算机也正是这样做的。这就是为什么你在撰写程序的时候,能够知道你的计算结果是否溢出在硬件层面得到的支持。 72 | 73 | ## 总结延伸 74 | 75 | 相信到这里,你应该已经体会到了,通过门电路来搭建算术计算的一个小功能,就好像搭乐高积木一样。 76 | 77 | 我们用两个门电路,搭出一个半加器,就好像我们拿两块乐高,叠在一起,变成一个长方形的乐高,这样我们就有了一个新的积木组件,柱子。我们再用两个柱子和一个长条的积木组合一下,就变成一个积木桥。然后几个积木桥串接在一起,又成了积木楼梯。 78 | 79 | 当我们想要搭建一个摩天大楼,我们需要很多很多楼梯。但是这个时候,我们已经不再关注最基础的一节楼梯是怎么用一块块积木搭建起来的。这其实就是计算机中,无论软件还是硬件中一个很重要的设计思想,**分层**。 80 | 81 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/8a7740f698236fda4e5f900d88fdf194.jpg) 82 | 83 | 从简单到复杂,我们一层层搭出了拥有更强能力的功能组件。在上面的一层,我们只需要考虑怎么用下一层的组件搭建出自己的功能,而不需要下沉到更低层的其他组件。就像你之前并没有深入学习过计算机组成原理,一样可以直接通过高级语言撰写代码,实现功能。 84 | 85 | 在硬件层面,我们通过门电路、半加器、全加器一层层搭出了加法器这样的功能组件。我们把这些用来做算术逻辑计算的组件叫作 ALU,也就是算术逻辑单元。当进一步打造强大的 CPU 时,我们不会再去关注最细颗粒的门电路,只需要把门电路组合而成的 ALU,当成一个能够完成基础计算的黑盒子就可以了。 86 | 87 | 以此类推,后面我们讲解 CPU 的设计和数据通路的时候,我们以 ALU 为一个基础单元来解释问题,也就够了。 88 | 89 | ## 补充阅读 90 | 91 | 出于性能考虑,实际 CPU 里面使用的加法器,比起我们今天讲解的电路还有些差别,会更复杂一些。真实的加法器,使用的是一种叫作**超前进位加法器**的东西。你可以找到北京大学在 Coursera 上开设的《计算机组成》课程中的 Video-306 “加法器优化”一节,了解一下超前进位加法器的实现原理,以及我们为什么要使用它。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/通过你的CPU主频,我们来谈谈“性能”究竟是什么?.md: -------------------------------------------------------------------------------- 1 | # 03 通过你的CPU主频,我们来谈谈“性能”究竟是什么? 2 | 3 | “性能”这个词,不管是在日常生活还是写程序的时候,都经常被提到。比方说,买新电脑的时候,我们会说“原来的电脑性能跟不上了”;写程序的时候,我们会说,“这个程序性能需要优化一下”。那么,你有没有想过,我们常常挂在嘴边的“性能”到底指的是什么呢?我们能不能给性能下一个明确的定义,然后来进行准确的比较呢? 4 | 5 | 在计算机组成原理乃至体系结构中,“性能”都是最重要的一个主题。我在前面说过,学习和研究计算机组成原理,就是在理解计算机是怎么运作的,以及为什么要这么运作。“为什么”所要解决的事情,很多时候就是提升“性能”。 6 | 7 | ## 什么是性能?时间的倒数 8 | 9 | 计算机的性能,其实和我们干体力劳动很像,好比是我们要搬东西。对于计算机的性能,我们需要有个标准来衡量。这个标准中主要有两个指标。 10 | 11 | 第一个是**响应时间**(Response time)或者叫执行时间(Execution time)。想要提升响应时间这个性能指标,你可以理解为让计算机“跑得更快”。 12 | 13 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/4c87a1851aeb6857a323064859da6396.png) 14 | 15 | 图中是我们实际系统里性能监测工具 NewRelic 中的响应时间,代表了每个外部的 Web 请求的执行时间 16 | 17 | 第二个是**吞吐率**(Throughput)或者带宽(Bandwidth),想要提升这个指标,你可以理解为让计算机“搬得更多”。 18 | 19 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/27cab77c0eec95ec29792e6c3d093d27.png) 20 | 21 | 服务器使用的网络带宽,通常就是一个吞吐率性能指标 22 | 23 | 所以说,响应时间指的就是,我们执行一个程序,到底需要花多少时间。花的时间越少,自然性能就越好。 24 | 25 | 而吞吐率是指我们在一定的时间范围内,到底能处理多少事情。这里的“事情”,在计算机里就是处理的数据或者执行的程序指令。 26 | 27 | 和搬东西来做对比,如果我们的响应时间短,跑得快,我们可以来回多跑几趟多搬几趟。所以说,缩短程序的响应时间,一般来说都会提升吞吐率。 28 | 29 | 除了缩短响应时间,我们还有别的方法吗?当然有,比如说,我们还可以多找几个人一起来搬,这就类似现代的服务器都是 8 核、16 核的。人多力量大,同时处理数据,在单位时间内就可以处理更多数据,吞吐率自然也就上去了。 30 | 31 | 提升吞吐率的办法有很多。大部分时候,我们只要多加一些机器,多堆一些硬件就好了。但是响应时间的提升却没有那么容易,因为 CPU 的性能提升其实在 10 年前就处于“挤牙膏”的状态了,所以我们得慎重地来分析对待。下面我们具体来看。 32 | 33 | 我们一般把性能,定义成响应时间的倒数,也就是: 34 | 35 | 性能 = 1/ 响应时间 36 | 37 | 这样一来,响应时间越短,性能的数值就越大。同样一个程序,在 Intel 最新的 CPU Coffee Lake 上,只需要 30s 就能运行完成,而在 5 年前 CPU Sandy Bridge 上,需要 1min 才能完成。那么我们自然可以算出来,Coffee Lake 的性能是 1/30,Sandy Bridge 的性能是 1/60,两个的性能比为 2。于是,我们就可以说,Coffee Lake 的性能是 Sandy Bridge 的 2 倍。 38 | 39 | 过去几年流行的手机跑分软件,就是把多个预设好的程序在手机上运行,然后根据运行需要的时间,算出一个分数来给出手机的性能评估。而在业界,各大 CPU 和服务器厂商组织了一个叫作**SPEC**(Standard Performance Evaluation Corporation)的第三方机构,专门用来指定各种“跑分”的规则。 40 | 41 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/a50a6cb9d3df027aeda5ee8e53b75422.png) 42 | 43 | 一份 SPEC 报告通常包含了大量不同测试的评分 44 | 45 | SPEC 提供的 CPU 基准测试程序,就好像 CPU 届的“高考”,通过数十个不同的计算程序,对于 CPU 的性能给出一个最终评分。这些程序丰富多彩,有编译器、解释器、视频压缩、人工智能国际象棋等等,涵盖了方方面面的应用场景。感兴趣的话,你可以点击[这个链接](https://www.spec.org/cpu2017/results/cpu2017.html)看看。 46 | 47 | ## 计算机的计时单位:CPU 时钟 48 | 49 | 虽然时间是一个很自然的用来衡量性能的指标,但是用时间来衡量时,有两个问题。 50 | 51 | **第一个就是时间不“准”**。如果用你自己随便写的一个程序,来统计程序运行的时间,每一次统计结果不会完全一样。有可能这一次花了 45ms,下一次变成了 53ms。 52 | 53 | 为什么会不准呢?这里面有好几个原因。首先,我们统计时间是用类似于“掐秒表”一样,记录程序运行结束的时间减去程序开始运行的时间。这个时间也叫 Wall Clock Time 或者 Elapsed Time,就是在运行程序期间,挂在墙上的钟走掉的时间。 54 | 55 | 但是,计算机可能同时运行着好多个程序,CPU 实际上不停地在各个程序之间进行切换。在这些走掉的时间里面,很可能 CPU 切换去运行别的程序了。而且,有些程序在运行的时候,可能要从网络、硬盘去读取数据,要等网络和硬盘把数据读出来,给到内存和 CPU。所以说,**要想准确统计某个程序运行时间,进而去比较两个程序的实际性能,我们得把这些时间给刨除掉**。 56 | 57 | 那这件事怎么实现呢?Linux 下有一个叫 time 的命令,可以帮我们统计出来,同样的 Wall Clock Time 下,程序实际在 CPU 上到底花了多少时间。 58 | 59 | 我们简单运行一下 time 命令。它会返回三个值,第一个是**real time**,也就是我们说的 Wall Clock Time,也就是运行程序整个过程中流逝掉的时间;第二个是**user time**,也就是 CPU 在运行你的程序,在用户态运行指令的时间;第三个是**sys time**,是 CPU 在运行你的程序,在操作系统内核里运行指令的时间。而**程序实际花费的 CPU 执行时间(CPU Time),就是 user time 加上 sys time**。 60 | 61 | ```shell 62 | $ time seq 1000000 | wc -l 63 | 1000000 64 | 65 | 66 | real 0m0.101s 67 | user 0m0.031s 68 | sys 0m0.016s 69 | ``` 70 | 71 | 在我给的这个例子里,你可以看到,实际上程序用了 0.101s,但是 CPU time 只有 0.031+0.016 = 0.047s。运行程序的时间里,只有不到一半是实际花在这个程序上的。 72 | 73 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/0b340db019d7e389a2bde4c237ee4700.jpg) 74 | 75 | 程序实际占用的 CPU 时间一般比 Elapsed Time 要少不少 76 | 77 | **其次,即使我们已经拿到了 CPU 时间,我们也不一定可以直接“比较”出两个程序的性能差异**。即使在同一台计算机上,CPU 可能满载运行也可能降频运行,降频运行的时候自然花的时间会多一些。 78 | 79 | 除了 CPU 之外,时间这个性能指标还会受到主板、内存这些其他相关硬件的影响。所以,我们需要对“时间”这个我们可以感知的指标进行拆解,把程序的 CPU 执行时间变成 CPU 时钟周期数(CPU Cycles)和 时钟周期时间(Clock Cycle)的乘积。 80 | 81 | 程序的 CPU 执行时间 =CPU 时钟周期数×时钟周期时间 82 | 83 | 我们先来理解一下什么是时钟周期时间。你在买电脑的时候,一定关注过 CPU 的主频。比如我手头的这台电脑就是 Intel Core-i7-7700HQ 2.8GHz,这里的 2.8GHz 就是电脑的主频(Frequency/Clock Rate)。这个 2.8GHz,我们可以先粗浅地认为,CPU 在 1 秒时间内,可以执行的简单指令的数量是 2.8G 条。 84 | 85 | 如果想要更准确一点描述,这个 2.8GHz 就代表,我们 CPU 的一个“钟表”能够识别出来的最小的时间间隔。就像我们挂在墙上的挂钟,都是“滴答滴答”一秒一秒地走,所以通过墙上的挂钟能够识别出来的最小时间单位就是秒。 86 | 87 | 而在 CPU 内部,和我们平时戴的电子石英表类似,有一个叫晶体振荡器(Oscillator Crystal)的东西,简称为晶振。我们把晶振当成 CPU 内部的电子表来使用。晶振带来的每一次“滴答”,就是时钟周期时间。 88 | 89 | 在我这个 2.8GHz 的 CPU 上,这个时钟周期时间,就是 1⁄2.8G。我们的 CPU,是按照这个“时钟”提示的时间来进行自己的操作。主频越高,意味着这个表走得越快,我们的 CPU 也就“被逼”着走得越快。 90 | 91 | 如果你自己组装过台式机的话,可能听说过“超频”这个概念,这说的其实就相当于把买回来的 CPU 内部的钟给调快了,于是 CPU 的计算跟着这个时钟的节奏,也就自然变快了。当然这个快不是没有代价的,CPU 跑得越快,散热的压力也就越大。就和人一样,超过生理极限,CPU 就会崩溃了。 92 | 93 | 我们现在回到上面程序 CPU 执行时间的公式。 94 | 95 | 程序的 CPU 执行时间 =CPU 时钟周期数×时钟周期时间 96 | 97 | 最简单的提升性能方案,自然缩短时钟周期时间,也就是提升主频。换句话说,就是换一块好一点的 CPU。不过,这个是我们这些软件工程师控制不了的事情,所以我们就把目光挪到了乘法的另一个因子——CPU 时钟周期数上。如果能够减少程序需要的 CPU 时钟周期数量,一样能够提升程序性能。 98 | 99 | 对于 CPU 时钟周期数,我们可以再做一个分解,把它变成“指令数×**每条指令的平均时钟周期数**(Cycles Per Instruction,简称 CPI)”。不同的指令需要的 Cycles 是不同的,加法和乘法都对应着一条 CPU 指令,但是乘法需要的 Cycles 就比加法要多,自然也就慢。在这样拆分了之后,我们的程序的 CPU 执行时间就可以变成这样三个部分的乘积。 100 | 101 | 程序的 CPU 执行时间 = 指令数×CPI×Clock Cycle Time 102 | 103 | 因此,如果我们想要解决性能问题,其实就是要优化这三者。 104 | 105 | 1. 时钟周期时间,就是计算机主频,这个取决于计算机硬件。我们所熟知的[摩尔定律](https://zh.wikipedia.org/wiki/摩尔定律)就一直在不停地提高我们计算机的主频。比如说,我最早使用的 80386 主频只有 33MHz,现在手头的笔记本电脑就有 2.8GHz,在主频层面,就提升了将近 100 倍。 106 | 2. 每条指令的平均时钟周期数 CPI,就是一条指令到底需要多少 CPU Cycle。在后面讲解 CPU 结构的时候,我们会看到,现代的 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU Cycle 尽可能地少。因此,对于 CPI 的优化,也是计算机组成和体系结构中的重要一环。 107 | 3. 指令数,代表执行我们的程序到底需要多少条指令、用哪些指令。这个很多时候就把挑战交给了编译器。同样的代码,编译成计算机指令时候,就有各种不同的表示方式。 108 | 109 | 我们可以把自己想象成一个 CPU,坐在那里写程序。计算机主频就好像是你的打字速度,打字越快,你自然可以多写一点程序。CPI 相当于你在写程序的时候,熟悉各种快捷键,越是打同样的内容,需要敲击键盘的次数就越少。指令数相当于你的程序设计得够合理,同样的程序要写的代码行数就少。如果三者皆能实现,你自然可以很快地写出一个优秀的程序,你的“性能”从外面来看就是好的。 110 | 111 | ## 总结延伸 112 | 113 | 好了,学完这一讲,对“性能”这个名词,你应该有了更清晰的认识。我主要对于“响应时间”这个性能指标进行抽丝剥茧,拆解成了计算机时钟周期、CPI 以及指令数这三个独立的指标的乘积,并且为你指明了优化计算机性能的三条康庄大道。也就是,提升计算机主频,优化 CPU 设计使得在单个时钟周期内能够执行更多指令,以及通过编译器来减少需要的指令数。 114 | 115 | 在后面的几讲里面,我会为你讲解,具体怎么在电路硬件、CPU 设计,乃至指令设计层面,提升计算机的性能。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/乘法器:如何像搭乐高一样搭电路(下).md: -------------------------------------------------------------------------------- 1 | # 14 乘法器:如何像搭乐高一样搭电路(下)? 2 | 3 | 和学习小学数学一样,学完了加法之后,我们自然而然就要来学习乘法。既然是退回到小学,我们就把问题搞得简单一点,先来看两个 4 位数的乘法。这里的 4 位数,当然还是一个二进制数。我们是人类而不是电路,自然还是用列竖式的方式来进行计算。 4 | 5 | 十进制中的 13 乘以 9,计算的结果应该是 117。我们通过转换成二进制,然后列竖式的办法,来看看整个计算的过程是怎样的。 6 | 7 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/498fdfa2dc95631068d65e0ff5769c4b.jpg) 8 | 9 | ## 顺序乘法的实现过程 10 | 11 | 从列出竖式的过程中,你会发现,二进制的乘法有个很大的优点,就是这个过程你不需要背九九乘法口诀表了。因为单个位置上,乘数只能是 0 或者 1,所以实际的乘法,就退化成了位移和加法。 12 | 13 | 在 13×9 这个例子里面,被乘数 13 表示成二进制是 1101,乘数 9 在二进制里面是 1001。最右边的个位是 1,所以个位乘以被乘数,就是把被乘数 1101 复制下来。因为二位和四位都是 0,所以乘以被乘数都是 0,那么保留下来的都是 0000。乘数的八位是 1,我们仍然需要把被乘数 1101 复制下来。不过这里和个位位置的单纯复制有一点小小的差别,那就是要把复制好的结果向左侧移三位,然后把四位单独进行乘法加位移的结果,再加起来,我们就得到了最终的计算结果。 14 | 15 | 对应到我们之前讲的数字电路和 ALU,你可以看到,最后一步的加法,我们可以用上一讲的加法器来实现。乘法因为只有“0”和“1”两种情况,所以可以做成输入输出都是 4 个开关,中间用 1 个开关,同时来控制这 8 个开关的方式,这就实现了二进制下的单位的乘法。 16 | 17 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/02ae32716bc3bf165d177dfe80d2c09c.jpg) 18 | 19 | 我们可以用一个开关来决定,下面的输出是完全复制输入,还是将输出全部设置为 0 20 | 21 | 至于位移也不麻烦,我们只要不是直接连线,把正对着的开关之间进行接通,而是斜着错开位置去接就好了。如果要左移一位,就错开一位接线;如果要左移两位,就错开两位接线。 22 | 23 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/e4c7ddb75731030930d38adf967b2d95.jpg) 24 | 25 | 把对应的线路错位连接,就可以起到位移的作用 26 | 27 | 这样,你会发现,我们并不需要引入任何新的、更复杂的电路,仍然用最基础的电路,只要用不同的接线方式,就能够实现一个“列竖式”的乘法。而且,因为二进制下,只有 0 和 1,也就是开关的开和闭这两种情况,所以我们的计算机也不需要去“背诵”九九乘法口诀表,不需要单独实现一个更复杂的电路,就能够实现乘法。 28 | 29 | 为了节约一点开关,也就是晶体管的数量。实际上,像 13×9 这样两个四位数的乘法,我们不需要把四次单位乘法的结果,用四组独立的开关单独都记录下来,然后再把这四个数加起来。因为这样做,需要很多组开关,如果我们计算一个 32 位的整数乘法,就要 32 组开关,太浪费晶体管了。如果我们顺序地来计算,只需要一组开关就好了。 30 | 31 | 我们先拿乘数最右侧的个位乘以被乘数,然后把结果写入用来存放计算结果的开关里面,然后,把被乘数左移一位,把乘数右移一位,仍然用乘数去乘以被乘数,然后把结果加到刚才的结果上。反复重复这一步骤,直到不能再左移和右移位置。这样,乘数和被乘数就像两列相向而驶的列车,仅仅需要简单的加法器、一个可以左移一位的电路和一个右移一位的电路,就能完成整个乘法。 32 | 33 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/cb809de19088d08767279715f07482e9.jpg) 34 | 35 | 乘法器硬件结构示意图 36 | 37 | 你看这里画的乘法器硬件结构示意图。这里的控制测试,其实就是通过一个时钟信号,来控制左移、右移以及重新计算乘法和加法的时机。我们还是以计算 13×9,也就是二进制的 1101×1001 来具体看。 38 | 39 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/0615e5e4406617ee6584adbb929f9571.jpeg) 40 | 41 | 这个计算方式虽然节约电路了,但是也有一个很大的缺点,那就是慢。 42 | 43 | 你应该很容易就能发现,在这个乘法器的实现过程里,我们其实就是把乘法展开,变成了“**加法 + 位移**”来实现。我们用的是 4 位数,所以要进行 4 组“位移 + 加法”的操作。而且这 4 组操作还不能同时进行。因为**下一组的加法要依赖上一组的加法后的计算结果,下一组的位移也要依赖上一组的位移的结果。这样,整个算法是“顺序”的,每一组加法或者位移的运算都需要一定的时间**。 44 | 45 | 所以,最终这个乘法的计算速度,其实和我们要计算的数的位数有关。比如,这里的 4 位,就需要 4 次加法。而我们的现代 CPU 常常要用 32 位或者是 64 位来表示整数,那么对应就需要 32 次或者 64 次加法。比起 4 位数,要多花上 8 倍乃至 16 倍的时间。 46 | 47 | 换个我们在算法和数据结构中的术语来说就是,这样的一个顺序乘法器硬件进行计算的时间复杂度是 O(N)。这里的 N,就是乘法的数里面的**位数**。 48 | 49 | ## 并行加速方法 50 | 51 | 那么,我们有没有办法,把时间复杂度上降下来呢?研究数据结构和算法的时候,我们总是希望能够把 O(N) 的时间复杂度,降低到 O(logN)。办法还真的有。和软件开发里面改算法一样,在涉及 CPU 和电路的时候,我们可以改电路。 52 | 53 | 32 位数虽然是 32 次加法,但是我们可以让很多加法同时进行。回到这一讲开始,我们把位移和乘法的计算结果加到中间结果里的方法,32 位整数的乘法,其实就变成了 32 个整数相加。 54 | 55 | 前面顺序乘法器硬件的实现办法,就好像体育比赛里面的**单败淘汰赛**。只有一个擂台会存下最新的计算结果。每一场新的比赛就来一个新的选手,实现一次加法,实现完了剩下的还是原来那个守擂的,直到其余 31 个选手都上来比过一场。如果一场比赛需要一天,那么一共要比 31 场,也就是 31 天。 56 | 57 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/07f7b0eedbf1a00fc72be7e2bd0d96ef.jpg) 58 | 59 | 目前的乘法实现就像是单败淘汰赛 60 | 61 | 加速的办法,就是把比赛变成像世界杯足球赛那样的淘汰赛,32 个球队捉对厮杀,同时开赛。这样一天一下子就淘汰了 16 支队,也就是说,32 个数两两相加后,你可以得到 16 个结果。后面的比赛也是一样同时开赛捉对厮杀。只需要 5 天,也就是 O(log2N) 的时间,就能得到计算的结果。但是这种方式要求我们得有 16 个球场。因为在淘汰赛的第一轮,我们需要 16 场比赛同时进行。对应到我们 CPU 的硬件上,就是需要更多的晶体管开关,来放下中间计算结果。 62 | 63 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/6646b90ea563c6b87dc20bbd81c54b98.jpeg) 64 | 65 | 通过并联更多的 ALU,加上更多的寄存器,我们也能加速乘法 66 | 67 | ## 电路并行 68 | 69 | 上面我们说的并行加速的办法,看起来还是有点儿笨。我们回头来做一个抽象的思考。之所以我们的计算会慢,核心原因其实是“顺序”计算,也就是说,要等前面的计算结果完成之后,我们才能得到后面的计算结果。 70 | 71 | 最典型的例子就是我们上一讲讲的加法器。每一个全加器,都要等待上一个全加器,把对应的进入输入结果算出来,才能算下一位的输出。位数越多,越往高位走,等待前面的步骤就越多,这个等待的时间有个专门的名词,叫作**门延迟**(Gate Delay)。 72 | 73 | 每通过一个门电路,我们就要等待门电路的计算结果,就是一层的门电路延迟,我们一般给它取一个“T”作为符号。一个全加器,其实就已经有了 3T 的延迟(进位需要经过 3 个门电路)。而 4 位整数,最高位的计算需要等待前面三个全加器的进位结果,也就是要等 9T 的延迟。如果是 64 位整数,那就要变成 63×3=189T 的延迟。这可不是个小数字啊! 74 | 75 | 除了门延迟之外,还有一个问题就是**时钟频率**。在上面的顺序乘法计算里面,如果我们想要用更少的电路,计算的中间结果需要保存在寄存器里面,然后等待下一个时钟周期的到来,控制测试信号才能进行下一次移位和加法,这个延迟比上面的门延迟更可观。 76 | 77 | 那么,我们有什么办法可以解决这个问题呢?实际上,在我们进行加法的时候,如果相加的两个数是确定的,那高位是否会进位其实也是确定的。对于我们人来说,我们本身去做计算都是顺序执行的,所以要一步一步计算进位。但是,计算机是连结的各种线路。我们不用让计算机模拟人脑的思考方式,来连结线路。 78 | 79 | 那怎么才能把线路连结得复杂一点,让高位和低位的计算同时出结果呢?怎样才能让高位不需要等待低位的进位结果,而是把低位的所有输入信号都放进来,直接计算出高位的计算结果和进位结果呢? 80 | 81 | 我们只要把进位部分的电路完全展开就好了。我们的半加器到全加器,再到加法器,都是用最基础的门电路组合而成的。门电路的计算逻辑,可以像我们做数学里面的多项式乘法一样完全展开。在展开之后呢,我们可以把原来需要较少的,但是有较多层前后计算依赖关系的门电路,展开成需要较多的,但是依赖关系更少的门电路。 82 | 83 | 我在这里画了一个示意图,展示了一下我们加法器。如果我们完全展开电路,高位的进位和计算结果,可以和低位的计算结果同时获得。这个的核心原因是电路是天然并行的,一个输入信号,可以同时传播到所有接通的线路当中。 84 | 85 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/10688d1659fe29f0f71a45d3c284a0df.jpeg) 86 | 87 | C4 是前 4 位的计算结果是否进位的门电路表示 88 | 89 | 如果一个 4 位整数最高位是否进位,展开门电路图,你会发现,我们只需要 3T 的延迟就可以拿到是否进位的计算结果。而对于 64 位的整数,也不会增加门延迟,只是从上往下复制这个电路,接入更多的信号而已。看到没?我们通过把电路变复杂,就解决了延迟的问题。 90 | 91 | 这个优化,本质上是利用了电路天然的并行性。电路只要接通,输入的信号自动传播到了所有接通的线路里面,这其实也是硬件和软件最大的不同。 92 | 93 | 无论是这里把对应的门电路逻辑进行完全展开以减少门延迟,还是上面的乘法通过并行计算多个位的乘法,都是把我们完成一个计算的电路变复杂了。而电路变复杂了,也就意味着晶体管变多了。 94 | 95 | 之前很多同学在我们讨论计算机的性能问题的时候,都提到,为什么晶体管的数量增加可以优化计算机的计算性能。实际上,这里的门电路展开和上面的并行计算乘法都是很好的例子。我们通过更多的晶体管,就可以拿到更低的门延迟,以及用更少的时钟周期完成一个计算指令。 96 | 97 | ## 总结延伸 98 | 99 | 讲到这里,相信你已经发现,我们通过之前两讲的 ALU 和门电路,搭建出来了乘法器。如果愿意的话,我们可以把很多在生活中不得不顺序执行的事情,通过简单地连结一下线路,就变成并行执行了。这是因为,硬件电路有一个很大的特点,那就是信号都是实时传输的。 100 | 101 | 我们也看到了,通过精巧地设计电路,用较少的门电路和寄存器,就能够计算完成乘法这样相对复杂的运算。是用更少更简单的电路,但是需要更长的门延迟和时钟周期;还是用更复杂的电路,但是更短的门延迟和时钟周期来计算一个复杂的指令,这之间的权衡,其实就是计算机体系结构中 RISC 和 CISC 的经典历史路线之争。 102 | 103 | ## 推荐阅读 104 | 105 | 如果还有什么细节你觉得还没有彻底弄明白,我推荐你看一看《计算机组成与设计:硬件 / 软件接口》的 3.3 节。 -------------------------------------------------------------------------------- /数据结构/算法题目/2024.10.14.md: -------------------------------------------------------------------------------- 1 | ```c 2 | /* 3 | 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 4 | 5 | 示例 1: 6 | 7 | 输入:head = [1,2,3,4,5] 8 | 输出:[5,4,3,2,1] 9 | 示例 2:= 10 | 11 | 输入:head = [1,2] 12 | 输出:[2,1] 13 | 示例 3: 14 | 15 | 输入:head = [] 16 | 输出:[] 17 | 提示: 18 | 链表中节点的数目范围是 [0, 5000] 19 | -5000 <= Node.val <= 5000 20 | 进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题? 21 | */ 22 | /** 23 | * Definition for singly-linked list. 24 | * struct ListNode { 25 | * int val; 26 | * struct ListNode *next; 27 | * }; 28 | */ 29 | //常规解法:头插法,先插入的反而在后面,就可以遍历并头插到另一个链表中则可 30 | struct ListNode* reverseList(struct ListNode* head) { 31 | //new_head为头指针,在new_head后面进行插入 32 | Node *new_head = (Node *)malloc(sizeof(Node)); 33 | Node *p = head; 34 | while(p){ 35 | Node *node = (Node *)malloc(sizeof(Node)); 36 | node->val = p->val; 37 | node->next = new_head->next; 38 | new_head->next = node; 39 | p = p -> next; 40 | } 41 | head = new_head->next; 42 | return head; 43 | } 44 | //进阶做法:利用递归思想 45 | struct ListNode* reverseList(struct ListNode* head){ 46 | //确定边界条件:考虑最简单的两种情况 47 | if(head == NULL || head->next == NULL){ 48 | return head; 49 | } 50 | //将翻转后的末节点前的节点获得 51 | ListNode *tail = head->next; 52 | //将翻转后的节点进行重定位 53 | Node *new_head = reverseList(head->next); 54 | head->next = tail->next; 55 | tail->next = head; 56 | return new_head; 57 | } 58 | /* 59 | 给你一个链表的头节点 head ,判断链表中是否有环。 60 | 61 | 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。 62 | 63 | 如果链表中存在环 ,则返回 true 。 否则,返回 false 。 64 | 示例 1: 65 | 输入:head = [3,2,0,-4], pos = 1 66 | 输出:true 67 | 解释:链表中有一个环,其尾部连接到第二个节点。 68 | 示例 2: 69 | 输入:head = [1,2], pos = 0 70 | 输出:true 71 | 解释:链表中有一个环,其尾部连接到第一个节点。 72 | 示例 3: 73 | 输入:head = [1], pos = -1 74 | 输出:false 75 | 解释:链表中没有环。 76 | 77 | 提示: 78 | 79 | 链表中节点的数目范围是 [0, 104] 80 | -105 <= Node.val <= 105 81 | pos 为 -1 或者链表中的一个 有效索引 。 82 | 83 | 84 | 进阶:你能用 O(1)(即,常量)内存解决此问题吗? 85 | */ 86 | /** 87 | * Definition for singly-linked list. 88 | * struct ListNode { 89 | * int val; 90 | * struct ListNode *next; 91 | * }; 92 | */ 93 | //解题思路:环形链表上如果分别有一个快指针和慢指针则一定会相遇 94 | bool hasCycle(struct ListNode *head) { 95 | ListNode *low = head; 96 | ListNode *fast = head; 97 | while(fast && fast->next->next){ 98 | fast = fast->next->next; 99 | low = low->next; 100 | if(fast == low){ 101 | return true; 102 | } 103 | } 104 | return false; 105 | } 106 | /* 107 | 编写一个算法来判断一个数 n 是不是快乐数。 108 | 「快乐数」 定义为: 109 | 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 110 | 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。 111 | 如果这个过程 结果为 1,那么这个数就是快乐数。 112 | 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。 113 | 示例 1: 114 | 输入:n = 19 115 | 输出:true 116 | 解释: 117 | 12 + 92 = 82 118 | 82 + 22 = 68 119 | 62 + 82 = 100 120 | 12 + 02 + 02 = 1 121 | 示例 2: 122 | 输入:n = 2 123 | 输出:false 124 | 提示: 125 | 126 | 1 <= n <= 231 - 1 127 | */ 128 | //解题思路:在胡船长的课程中学到一个好的方法,将这个看做成一个链表,一个数的后面跟着一个确定的数,如果在最后能为1则有出口,否则就是循环了,也就变为判断链表是否有环的问题了 129 | int getNext(int n){ 130 | int res = 0; 131 | while(n){ 132 | int d = n % 10; 133 | res += d * d; 134 | n /= 10; 135 | } 136 | return res; 137 | } 138 | bool isHappy(int n) { 139 | int fast = n; 140 | int low = n; 141 | while(fast != 1 && getNext(getNext(fast)) != 1){ 142 | fast = getNext(getNext(fast)); 143 | low = getNext(low); 144 | if(fast == low) return false; 145 | } 146 | return true; 147 | } 148 | /* 149 | 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。 150 | 示例 1: 151 | 输入:head = [1,2,3,4,5], k = 2 152 | 输出:[4,5,1,2,3] 153 | 示例 2: 154 | 输入:head = [0,1,2], k = 4 155 | 输出:[2,0,1] 156 | 提示: 157 | 链表中节点的数目在范围 [0, 500] 内 158 | -100 <= Node.val <= 100 159 | 0 <= k <= 2 * 109 160 | */ 161 | /** 162 | * Definition for singly-linked list. 163 | * struct ListNode { 164 | * int val; 165 | * struct ListNode *next; 166 | * }; 167 | */ 168 | //解题思路:首先若k的值很大,我们需要得到有效的大小,通过取余完成 169 | //然后右移k位,只用找到倒数k+1位,将其作为最后的结点,后面k位结点移到前面则可,这里就需要解决怎样找到倒数k+1位 170 | //用两个指针,让一个指针先走k位,然后再让两个指针一起走,直到先走的指针到达最后一个节点,后走的指向的就是需要的结点 171 | struct ListNode* rotateRight(struct ListNode* head, int k) { 172 | if(head == NULL || head->next == NULL){ 173 | return head; 174 | } 175 | int count = 0; 176 | for(struct ListNode *p = head; p; p = p->next) count++; 177 | k = k % count; 178 | if(k == 0) return head; 179 | struct ListNode *fast = head; 180 | struct ListNode *low = head; 181 | for(int i = 0; i < k; i++){ 182 | fast = fast->next; 183 | } 184 | while(fast->next){ 185 | fast = fast->next; 186 | low = low->next; 187 | } 188 | struct ListNode *new_head = low->next; 189 | low->next = fast->next; 190 | fast->next = head; 191 | return new_head; 192 | } 193 | /* 194 | 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 195 | 示例 1: 196 | 输入:head = [1,2,3,4,5], n = 2 197 | 输出:[1,2,3,5] 198 | 示例 2: 199 | 输入:head = [1], n = 1 200 | 输出:[] 201 | 示例 3: 202 | 输入:head = [1,2], n = 1 203 | 输出:[1] 204 | 提示: 205 | 链表中结点的数目为 sz 206 | 1 <= sz <= 30 207 | 0 <= Node.val <= 100 208 | 1 <= n <= sz 209 | 进阶:你能尝试使用一趟扫描实现吗? 210 | */ 211 | /** 212 | * Definition for singly-linked list. 213 | * struct ListNode { 214 | * int val; 215 | * struct ListNode *next; 216 | * }; 217 | */ 218 | struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { 219 | if (head == NULL || n <= 0) { 220 | return head; // 处理空链表和 n <= 0 的情况 221 | } 222 | struct ListNode *fast = head; 223 | struct ListNode *low = head; 224 | // 移动 fast 指针 n 步 225 | for (int i = 0; i < n; i++) { 226 | if (fast) { 227 | fast = fast->next; 228 | } else { 229 | return head; // 如果 n 大于链表长度,直接返回头节点 230 | } 231 | } 232 | // 如果 fast 为空,说明要删除的是头节点,无法向后移动 233 | if (fast == NULL) { 234 | struct ListNode *temp = head; 235 | head = head->next; // 更新头节点 236 | free(temp); // 释放原头节点 237 | return head; 238 | } 239 | // 移动 fast 和 low 指针,直到 fast 到达链表的最后一个节点 240 | while (fast->next) { 241 | fast = fast->next; 242 | low = low->next; 243 | } 244 | // 删除目标节点 245 | struct ListNode *p = low->next; 246 | low->next = low->next->next; 247 | free(p); 248 | return head; // 返回头节点 249 | } 250 | ``` 251 | 252 | -------------------------------------------------------------------------------- /书籍/计算机原理专栏/穿越功耗墙,我们该从哪些方面提升“性能”?.md: -------------------------------------------------------------------------------- 1 | # 04 穿越功耗墙,我们该从哪些方面提升“性能”? 2 | 3 | 上一讲,在讲 CPU 的性能时,我们提到了这样一个公式: 4 | 5 | 程序的 CPU 执行时间 = 指令数×CPI×Clock Cycle Time 6 | 7 | 这么来看,如果要提升计算机的性能,我们可以从指令数、CPI 以及 CPU 主频这三个地方入手。要搞定指令数或者 CPI,乍一看都不太容易。于是,研发 CPU 的硬件工程师们,从 80 年代开始,就挑上了 CPU 这个“软柿子”。在 CPU 上多放一点晶体管,不断提升 CPU 的时钟频率,这样就能让 CPU 变得更快,程序的执行时间就会缩短。 8 | 9 | 于是,从 1978 年 Intel 发布的 8086 CPU 开始,计算机的主频从 5MHz 开始,不断提升。1980 年代中期的 80386 能够跑到 40MHz,1989 年的 486 能够跑到 100MHz,直到 2000 年的奔腾 4 处理器,主频已经到达了 1.4GHz。而消费者也在这 20 年里养成了“看主频”买电脑的习惯。当时已经基本垄断了桌面 CPU 市场的 Intel 更是夸下了海口,表示奔腾 4 所使用的 CPU 结构可以做到 10GHz,颇有一点“大力出奇迹”的意思。 10 | 11 | ## 功耗:CPU 的“人体极限” 12 | 13 | 然而,计算机科学界从来不相信“大力出奇迹”。奔腾 4 的 CPU 主频从来没有达到过 10GHz,最终它的主频上限定格在 3.8GHz。这还不是最糟的,更糟糕的事情是,大家发现,奔腾 4 的主频虽然高,但是它的实际性能却配不上同样的主频。想要用在笔记本上的奔腾 4 2.4GHz 处理器,其性能只和基于奔腾 3 架构的奔腾 M 1.6GHz 处理器差不多。 14 | 15 | 于是,这一次的“大力出悲剧”,不仅让 Intel 的对手 AMD 获得了喘息之机,更是代表着“主频时代”的终结。后面几代 Intel CPU 主频不但没有上升,反而下降了。到如今,2019 年的最高配置 Intel i9 CPU,主频也只不过是 5GHz 而已。相较于 1978 年到 2000 年,这 20 年里 300 倍的主频提升,从 2000 年到现在的这 19 年,CPU 的主频大概提高了 3 倍。 16 | 17 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/1826102a89e4cdd31f7573db53dd9280.png) 18 | 19 | CPU 的主频变化,在奔腾 4 时代进入了瓶颈期,[图片来源](https://en.wikipedia.org/wiki/File:Clock_CPU_Scaling.jpg) 20 | 21 | 奔腾 4 的主频为什么没能超过 3.8GHz 的障碍呢?答案就是功耗问题。什么是功耗问题呢?我们先看一个直观的例子。 22 | 23 | 一个 3.8GHz 的奔腾 4 处理器,满载功率是 130 瓦。这个 130 瓦是什么概念呢?机场允许带上飞机的充电宝的容量上限是 100 瓦时。如果我们把这个 CPU 安在手机里面,不考虑屏幕内存之类的耗电,这个 CPU 满载运行 45 分钟,充电宝里面就没电了。而 iPhone X 使用 ARM 架构的 CPU,功率则只有 4.5 瓦左右。 24 | 25 | 我们的 CPU,一般都被叫作**超大规模集成电路**(Very-Large-Scale Integration,VLSI)。这些电路,实际上都是一个个晶体管组合而成的。CPU 在计算,其实就是让晶体管里面的“开关”不断地去“打开”和“关闭”,来组合完成各种运算和功能。 26 | 27 | 想要计算得快,一方面,我们要在 CPU 里,同样的面积里面,多放一些晶体管,也就是**增加密度**;另一方面,我们要让晶体管“打开”和“关闭”得更快一点,也就是**提升主频**。而这两者,都会增加功耗,带来耗电和散热的问题。 28 | 29 | 这么说可能还是有点抽象,我还是给你举一个例子。你可以把一个计算机 CPU 想象成一个巨大的工厂,里面有很多工人,相当于 CPU 上面的晶体管,互相之间协同工作。 30 | 31 | 为了工作得快一点,我们要在工厂里多塞一点人。你可能会问,为什么不把工厂造得大一点呢?这是因为,人和人之间如果离得远了,互相之间走过去需要花的时间就会变长,这也会导致性能下降。这就好像如果 CPU 的面积大,晶体管之间的距离变大,电信号传输的时间就会变长,运算速度自然就慢了。 32 | 33 | 除了多塞一点人,我们还希望每个人的动作都快一点,这样同样的时间里就可以多干一点活儿了。这就相当于提升 CPU 主频,但是动作快,每个人就要出汗散热。要是太热了,对工厂里面的人来说会中暑生病,对 CPU 来说就会崩溃出错。 34 | 35 | 我们会在 CPU 上面抹硅脂、装风扇,乃至用上水冷或者其他更好的散热设备,就好像在工厂里面装风扇、空调,发冷饮一样。但是同样的空间下,装上风扇空调能够带来的散热效果也是有极限的。 36 | 37 | 因此,在 CPU 里面,能够放下的晶体管数量和晶体管的“开关”频率也都是有限的。一个 CPU 的功率,可以用这样一个公式来表示: 38 | 39 | 功耗 ~= 1⁄2 ×负载电容×电压的平方×开关频率×晶体管数量 40 | 41 | 那么,为了要提升性能,我们需要不断地增加晶体管数量。同样的面积下,我们想要多放一点晶体管,就要把晶体管造得小一点。这个就是平时我们所说的提升“制程”。从 28nm 到 7nm,相当于晶体管本身变成了原来的 1⁄4 大小。这个就相当于我们在工厂里,同样的活儿,我们要找瘦小一点的工人,这样一个工厂里面就可以多一些人。我们还要提升主频,让开关的频率变快,也就是要找手脚更快的工人。 42 | 43 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/f59f2f33e308000cb5d2ad017f2ff8ed.jpeg) 44 | 45 | 但是,功耗增加太多,就会导致 CPU 散热跟不上,这时,我们就需要降低电压。这里有一点非常关键,在整个功耗的公式里面,功耗和电压的平方是成正比的。这意味着电压下降到原来的 1/5,整个的功耗会变成原来的 1/25。 46 | 47 | 事实上,从 5MHz 主频的 8086 到 5GHz 主频的 Intel i9,CPU 的电压已经从 5V 左右下降到了 1V 左右。这也是为什么我们 CPU 的主频提升了 1000 倍,但是功耗只增长了 40 倍。比如说,我写这篇文章用的是 Surface Go,在这样的轻薄笔记本上,微软就是选择了把电压下降到 0.25V 的低电压 CPU,使得笔记本能有更长的续航时间。 48 | 49 | ## 并行优化,理解阿姆达尔定律 50 | 51 | 虽然制程的优化和电压的下降,在过去的 20 年里,让我们的 CPU 性能有所提升。但是从上世纪九十年代到本世纪初,软件工程师们所用的“面向摩尔定律编程”的套路越来越用不下去了。“写程序不考虑性能,等明年 CPU 性能提升一倍,到时候性能自然就不成问题了”,这种想法已经不可行了。 52 | 53 | 于是,从奔腾 4 开始,Intel 意识到通过提升主频比较“难”去实现性能提升,边开始推出 Core Duo 这样的多核 CPU,通过提升“吞吐率”而不是“响应时间”,来达到目的。 54 | 55 | 提升响应时间,就好比提升你用的交通工具的速度,比如原本你是开汽车,现在变成了火车乃至飞机。本来开车从上海到北京要 20 个小时,换成飞机就只要 2 个小时了,但是,在此之上,再想要提升速度就不太容易了。我们的 CPU 在奔腾 4 的年代,就好比已经到了飞机这个速度极限。 56 | 57 | 那你可能要问了,接下来该怎么办呢?相比于给飞机提速,工程师们又想到了新的办法,可以一次同时开 2 架、4 架乃至 8 架飞机,这就好像我们现在用的 2 核、4 核,乃至 8 核的 CPU。 58 | 59 | 虽然从上海到北京的时间没有变,但是一次飞 8 架飞机能够运的东西自然就变多了,也就是所谓的“吞吐率”变大了。所以,不管你有没有需要,现在 CPU 的性能就是提升了 2 倍乃至 8 倍、16 倍。这也是一个最常见的提升性能的方式,**通过并行提高性能**。 60 | 61 | 这个思想在很多地方都可以使用。举个例子,我们做机器学习程序的时候,需要计算向量的点积,比如向量 W=[W0,W1,W2,…,W15]W=[W0,W1,W2,…,W15] 和向量 X=[X0,X1,X2,…,X15]X=[X0,X1,X2,…,X15],W⋅X=W0∗X0+W1∗X1+W·X=W0∗X0+W1∗X1+ W2∗X2+…+W15∗X15W2∗X2+…+W15∗X15。这些式子由 16 个乘法和 1 个连加组成。如果你自己一个人用笔来算的话,需要一步一步算 16 次乘法和 15 次加法。如果这个时候我们把这个人物分配给 4 个人,同时去算 W0~W3W0~W3, W4~W7W4~W7, W8~W11W8~W11, W12~W15W12~W15 这样四个部分的结果,再由一个人进行汇总,需要的时间就会缩短。 62 | 63 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/64d6957ecaa696edcf79dc1d5511269d.jpeg) 64 | 65 | 但是,并不是所有问题,都可以通过并行提高性能来解决。如果想要使用这种思想,需要满足这样几个条件。 66 | 67 | 第一,需要进行的计算,本身可以分解成几个可以并行的任务。好比上面的乘法和加法计算,几个人可以同时进行,不会影响最后的结果。 68 | 69 | 第二,需要能够分解好问题,并确保几个人的结果能够汇总到一起。 70 | 71 | 第三,在“汇总”这个阶段,是没有办法并行进行的,还是得顺序执行,一步一步来。 72 | 73 | 这就引出了我们在进行性能优化中,常常用到的一个经验定律,**阿姆达尔定律**(Amdahl’s Law)。这个定律说的就是,对于一个程序进行优化之后,处理器并行运算之后效率提升的情况。具体可以用这样一个公式来表示: 74 | 75 | 优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间 76 | 77 | 在刚刚的向量点积例子里,4 个人同时计算向量的一小段点积,就是通过并行提高了这部分的计算性能。但是,这 4 个人的计算结果,最终还是要在一个人那里进行汇总相加。这部分汇总相加的时间,是不能通过并行来优化的,也就是上面的公式里面**不受影响的执行时间**这一部分。 78 | 79 | 比如上面的各个向量的一小段的点积,需要 100ns,加法需要 20ns,总共需要 120ns。这里通过并行 4 个 CPU 有了 4 倍的加速度。那么最终优化后,就有了 100⁄4+20=45ns。即使我们增加更多的并行度来提供加速倍数,比如有 100 个 CPU,整个时间也需要 100⁄100+20=21ns。 80 | 81 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/f1d05ec439e6377803df741bc07b09e5.jpeg) 82 | 83 | ## 总结延伸 84 | 85 | 我们可以看到,无论是简单地通过提升主频,还是增加更多的 CPU 核心数量,通过并行来提升性能,都会遇到相应的瓶颈。仅仅简单地通过“堆硬件”的方式,在今天已经不能很好地满足我们对于程序性能的期望了。于是,工程师们需要从其他方面开始下功夫了。 86 | 87 | 在“摩尔定律”和“并行计算”之外,在整个计算机组成层面,还有这样几个原则性的性能提升方法。 88 | 89 | 1.**加速大概率事件**。最典型的就是,过去几年流行的深度学习,整个计算过程中,99% 都是向量和矩阵计算,于是,工程师们通过用 GPU 替代 CPU,大幅度提升了深度学习的模型训练过程。本来一个 CPU 需要跑几小时甚至几天的程序,GPU 只需要几分钟就好了。Google 更是不满足于 GPU 的性能,进一步地推出了 TPU。后面的文章,我也会为你讲解 GPU 和 TPU 的基本构造和原理。 90 | 91 | 2.**通过流水线提高性能**。现代的工厂里的生产线叫“流水线”。我们可以把装配 iPhone 这样的任务拆分成一个个细分的任务,让每个人都只需要处理一道工序,最大化整个工厂的生产效率。类似的,我们的 CPU 其实就是一个“运算工厂”。我们把 CPU 指令执行的过程进行拆分,细化运行,也是现代 CPU 在主频没有办法提升那么多的情况下,性能仍然可以得到提升的重要原因之一。我们在后面也会讲到,现代 CPU 里是如何通过流水线来提升性能的,以及反面的,过长的流水线会带来什么新的功耗和效率上的负面影响。 92 | 93 | 3.**通过预测提高性能**。通过预先猜测下一步该干什么,而不是等上一步运行的结果,提前进行运算,也是让程序跑得更快一点的办法。典型的例子就是在一个循环访问数组的时候,凭经验,你也会猜到下一步我们会访问数组的下一项。后面要讲的“分支和冒险”、“局部性原理”这些 CPU 和存储系统设计方法,其实都是在利用我们对于未来的“预测”,提前进行相应的操作,来提升我们的程序性能。 94 | 95 | 好了,到这里,我们讲完了计算机组成原理这门课的“前情提要”。一方面,整个组成乃至体系结构,都是基于冯·诺依曼架构组成的软硬件一体的解决方案。另一方面,你需要明白的就是,这里面的方方面面的设计和考虑,除了体系结构层面的抽象和通用性之外,核心需要考虑的是“性能”问题。 96 | 97 | 接下来,我们就要开始深入组成原理,从一个程序的运行讲起,开始我们的“机器指令”之旅。 98 | 99 | ## 补充阅读 100 | 101 | 如果你学有余力,关于本节内容,推荐你阅读下面两本书的对应章节,深入研读。 102 | 103 | 1. 《计算机组成与设计:软 / 硬件接口》(第 5 版)的 1.7 和 1.10 节,也简单介绍了功耗墙和阿姆达尔定律,你可以拿来细细阅读。 104 | 2. 如果你想对阿姆达尔定律有个更细致的了解,《深入理解计算机系统》(第 3 版)的 1.9 节不容错过。 -------------------------------------------------------------------------------- /计算机网络/王道笔记/传输层/TCP协议.md: -------------------------------------------------------------------------------- 1 | # TCP 的特点 2 | 1. TCP 是面向连接(虚连接)的传输层协议 3 | 2. 每一条 TCP 连接只能有两个端点,意思就是说 TCP 只能是点对点的 4 | 3. TCP 提供可靠交付的服务,无差错、不丢失、不重复、按序到达。可靠有序,不丢不重 5 | 4. TCP 提供全双工通信。在发送、接收端分别设置有缓存 6 | 5. TCP 面向字节流。TCP 把应用程序交下来的数据看成仅仅是一连串的无结构的字节流 7 | 8 | 发送文件的大致流程:每次携带一串字节 9 | 10 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740188513249-a946a17c-3f01-4720-a98d-d27929d324cf.png) 11 | 12 | # TCP 报文段首部格式 13 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740188636321-4c02b8d8-dd19-478d-9468-1e11e19c1d4e.png) 14 | 15 | TCP 首部存在 20B 固定长。目的端口和源端口就不再说了。 16 | **序号(seq)**:在一个 TCP 连接中传送的字节流中的每个字节都按顺序编号,本字段表示本报文段所发送数据的第一个字节的序号。如上面发送一个 TCP 头和 1、2、3 字节,首部对应的序号就是第一个 1。 17 | 18 | **确认号(ack)**:期望收到下一个报文段的第一个数据字节的序号。若确认号为 N,则证明到序号 N-1 为止的所有数据都已经正确收到了。如上面第一个 TCP 已经带着 1、2、3 字节了,希望跟着他的下一个字节是 4,所以对应的确认号字段为 4. 19 | 20 | **数据偏移(首部长度)**:TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远,以 4B 为单位的,就是最后的结果需要乘以 4 才是真实的结果。如上面一个 TCP 报文段的数据偏移字段为 15,则真实的长度为 60B,所以除了固定长度 20B 外,还有选项字段加填充字段 40B。 21 | 22 | **URG**:这个是一个紧急位,表示该报文段有紧急数据需要传送,不需要在发送缓存中排队,配置紧急指针字段使用。 23 | 24 | **ACK**:确认位,ACK=1 时确认号有效,在连接建立后所有传送的报文段都必须把 ACK 置为 1. 25 | 26 | **PSH**:推送位,和 URG 有点类似,但是是在接收方的,该字段为 1 的话,接收方需要尽快将该报文段交付给应用程序,不需要在接收缓存中排队。 27 | 28 | **RST**:复位位, RST 为 1 时,表示 TCP 连接中出现严重差错,必须释放连接,然后重新建立传输连接。 29 | 30 | **SYN**:同步位,SYN 为 1 时,表示一个连接请求或者接收报文,在建立连接需要使用。 31 | 32 | **FIN**:终止位,FIN 为 1 时,表示此报文发送方的数据已经发送完毕了,要求释放连接。 33 | 34 | **窗口**:指的是发送本报文段的一方的接收窗口,即现在允许对方发送的数据量。告诉对方下次你可以给我发送多少数据量。 35 | 36 | **检验和**:和 UDP 一样需要检验首部+数据部分。检验时要加上 12B 的伪首部,伪首部中的类型字段使用 6 来表示 TCP 协议。 37 | 38 | **紧急指针**:URG 为 1 时才有意义,所以说 URG 要配置紧急指针使用,指出本报文段中的紧急数据的字节数。 39 | 40 | **选项**:最大报文段长度 MSS(用于传输层规定的最大长度)、窗口扩大,时间错等等。 41 | 42 | **填充**:让 TCP 首部字节数为 4B 整数倍,需要的话就填充 0 字节。 43 | 44 | # TCP 的连接管理 45 | 这个的存在是为了运输连接的建立和释放都能正常进行。 46 | 47 | ## TCP 连接传输的三个过程(三次握手) 48 | 1. 建立连接 49 | 2. 传输数据 50 | 3. 释放连接 51 | 52 | TCP 连接的建立是采用的“客户服务器”的方式进行的,主动发起连接的主机称为客户,被动接收请求的主机称为服务器。 53 | 54 | 建立连接的示意图: 55 | 56 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740192726790-7040ae61-51d0-4353-8b44-48de6b9a00b4.png) 57 | 过程:写最为重要的部分。需要注意的是 SYN=1 的报文是不能携带数据的。 58 | 59 | + 客户需要发起一个请求连接的报文段,此报文的 SYN=1,并随机生成一个序号 seq=x。 60 | + 服务器接收到后,如果同意连接就返回一个报文段,该报文段的 SYN=ACK=1,也随机生成一个序号 seq=y,并且返回确认号 ack=x+1(表明我下一个接收的序号为 x+1) 61 | + 到达客户后,客户再发送一个 SYN=0,ACK=1,seq=x+1,ack=y+1 的报文段给服务器。此时这个报 62 | + 文段是带着数据的,如果不带数据 seq 就仍为 x(不消耗序号),下一个报文段再设置为 x+1 63 | 64 | ## SYN 泛洪攻击 65 | 该攻击发生在 OSI 层的第四层(传输层),这种方式利用 TCP 协议的特性,也就是三次握手,攻击者发送大量的三次握手中的第一次请求报文段,而当服务器返回一个 ACK 报文段后就不再发送确认报文段,这样这些 TCP 连接就会处于挂起状态,服务器收不到确认的话,就会重复发送 ACK 给攻击者。这样就会浪费服务器的资源,这些大量的处于挂起状态的 TCP 连接会消耗服务器的 CPU 和内存,最后服务器可能死机,就无法为正常用户提供服务了。 66 | 67 | ## TCP 连接释放的过程(四次挥手) 68 | 在参与 TCP 连接的两个进程,其中任意一个进行都能终止该连接。 69 | 70 | 这个关闭可以想象为一条 TCP 连接的两条数据链路都需要关闭。(TCP 全双工嘛) 71 | 72 | 每关闭一条都需要请求和确认两次发送。 73 | 74 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740212488999-526e7f16-c2a7-4f06-b56b-6aba8c449ef3.png) 75 | 76 | + 客户端需要关闭连接,会发送一个 FIN 为 1,seq 为 u(u 为上一个报文的最后一个字节序号+1) 的报文段,并停止发送数据。此时客户端就处于 FIN-WAIT-1 状态。此时客户端就已经不能再主动发送数据了,但是服务器还可以发送数据。 77 | + 服务器会接收到客户端发送过来的 FIN=1 的报文,此时服务器会返回一个 ACK=1 ,ack=u+1,seq=v 的报文段(因为之前的客户端报文 seq=u,所以服务器需要下一个报文的开头为 u+1。v 是服务器最后一个报文段中最后一个字节序号+1)。发送后服务器进入 CLOSE-WAIT 状态。到这里,客户端到服务器的连接就释放了,但是服务器到客户端的连接还没有关闭。因为至始至终都只是客户端的“要求”罢了。 78 | + 如果服务器没有再需要发送的数据了,就可以向客户端发送释放报文了,该报文的 FIN、ACK 都为 1,序号 seq=w(服务器可能还发送了一些数据),还得重复发送 ack=u+1。发送后服务器处于 LAST-ACK 状态。 79 | + 客户端收到了服务器发送的释放报文段,会被动的返回一个确认报文段。该报文段的 ACK=1,seq=u+1,ack=w+1。服务器收到该确认报文后就处于 CLOSED 状态了,客户端还要在 TIME-WAIT 状态,直到时间等待计时器设置的 2MSL(最长报文段寿命)结束,才进入 CLOSED 状态。 80 | 81 | 82 | 83 | # TCP 的可靠传输 84 | 可靠传输就是要保证接收方进程从缓存区读出的字节流与发送方发出的字节流完全一样。 85 | 86 | ## TCP 实现可靠传输的机制 87 | 1. 校验 88 | 2. 序号 89 | 3. 确认 90 | 4. 重传 91 | 92 | **校验**和 UDP 协议的校验是一样的,使用伪首部进行辅助计算 93 | 94 | **序号**就是对发送的字节进行编号 0、1、2...等等 95 | 96 | **确认**采用的是累加确认,接收方返回自己没有收到的序号的确认号,比如,接收方收到了 0~2, 5~8 字节的数据,显然还没有收到 3~7 的数据,此时返回报文段的确认号为 3 97 | 98 | **重传**有超时重传和 ACK 冗余重传。超时的意思就是发送方长时间没有收到 ACK,从而进行重传,但是这样的话有个弊端就是等待时间会比较长,所以使用冗余重传来解决这个问题。举个例子:发送方有 0 1 2 3 4 5 6 7 8 9 10 需要发送的字节序号,接收方收到 0 1 2 后返回确认报文段的 ack 为 3,发送方发送 3 4 5 但是因为一些原因丢失了,接着发送 6 7 8,9 10,接收方在接收后两个 TCP 报文段后都会返回一个 ack 为 3 的报文段,这就冗余了,发送方就会知道原来 ack 为 3 的 TCP 报文段发送失败了,就会进行重传。 99 | 100 | # TCP的流量控制 101 | 流量控制就是控制发送方的速率不至于太快,要让接收方来得及接收 102 | 103 | TCP 协议使用滑动窗口机制实现流量控制 104 | 105 | 在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,即接收方窗口 rwnd(接收方通过设置确认报文段中的窗口字段来实现),发送方的发送窗口取接收窗口 rwnd 和拥塞窗口 rwnd 的最小值。 106 | 107 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740225694572-f7f8a93c-f951-4c5f-8f10-bab703a2fe49.png) 108 | 109 | 这幅图形象的展示了 TCP 的“累积确认” 110 | 111 | 在最后的时候,B 返回的确认字段 rwnd 为 0,这表示 B 中的接收窗口已经没有空余的了。 112 | 113 | 这种情况需要 B 将接收窗口中的数据提交给上层然后 B 向 A 发送一个新的窗口值为止。 114 | 115 | TCP 在这里有个探测机制:TCP 会为每个连接设置一个持续计时器,只要发送方收到一个零窗口通知,就会启动该持续计时器,到时间了就给接收方发送一个探测报文段,接收方就会返回一个包含自己接收窗口大小的确认报文段,如果还是为 0 就重置持续计时器,否则就可以继续传输数据了。 116 | 117 | # TCP 的拥塞控制 118 | 首先我们来想一想出现拥塞的情况是什么,显然就是资源不够用了。(所需的资源>资源总和) 119 | 120 | 网络中资源出现供应不足时会导致网络性能变差,网络的吞吐量也会随着负荷的增加而下降。 121 | 122 | 拥塞控制就是防止过多的数据注入到网络中去,是一个全局性的问题。虽然和流量控制都是控制数据的传输,但是两者的作用范围是不同的。 123 | 124 | ## 拥塞控制的四种算法 125 | 因为发送端需要动态的变化自己发送的数据以避免网络发生拥塞,所以发送方需要维护一个拥塞窗口 cwnd 126 | 127 | 怎样控制的呢?只要网络没有出现拥塞则拥塞窗口就可以变的更大些,一旦网络发生拥塞则就将拥塞窗口变小些。 128 | 129 | 原理的就是这么一个原理。 130 | 131 | 132 | 133 | 因为发送方的发送窗口大小是需要取 min{接收窗口,拥塞窗口}的,其中的接收窗口可以让接收方发送 TCP 报文段来知道,所以发送方就需要维护 cwnd,就会使用慢开始和拥塞避免算法来维护。 134 | 135 | ### 慢开始和拥塞避免 136 | 在研究慢开始和拥塞避免算法时,为了便于理解,我们假设: 137 | 数据是单方向传送,对方只传送确认报文 138 | 139 | 接收方总是有足够大的缓存空间,因此发送方窗口的大小由网络的拥塞程度控制 140 | 141 | 采用最大报文长度 MSS (最大报文段长度)作为拥塞窗口大小的单位 142 | 143 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740271491341-0473b6b4-1cf2-485c-997b-4b1f9c5621f1.png) 144 | 145 | --- 146 | 147 | > 过程:刚开始使用慢开始算法,就是首先令 cwnd=1, 发送一个报文段进行探测,如果成功接收到接收方返回的确认报文,则将 cwnd 乘以 2,接收方成功返回两个确认报文后,由将 cwnd 乘以 2,这样的增长速率会很快,所以我们需要设置一个慢开始门限 ssthresh,在 cwnd=ssthresh 时就采用拥塞避免算法进行处理,每次接收方成功返回一个确认报文后不是将 cwnd 乘以 2 了,而是进行加 1,知道遇到网络拥塞,就将 ssthresh 重新设置为原来的二分之一,然后重新开始,cwnd=1,巴拉巴拉。 148 | > 149 | 150 | 需要注意的是:在 cwnd 增加后如果超过了 ssthresh,则将 cwnd 设置为 ssthresh 的大小,就如上面图片的第二次增长,8 后面应该是 16,但是由于 ssthresh 是 12 所以下一次的 cwnd 设置的是 12,而且 ssthresh 要大于等于 2 才行。 151 | 152 | ### 快重传和快恢复 153 | 在数据传输过程中可以某个报文段会丢失,发送方由于迟迟收不到确认报文,会错误的认为发生了网络拥塞,从而减少自己的 cwnd 和重新开始慢开始算法,导致利用率下降,使用快重传算法可以让发送方尽快的知道发生了报文段的丢失,从而减少“误导”时间。 154 | 155 | ![](https://cdn.nlark.com/yuque/0/2025/png/48073730/1740275163177-c2f46883-2fd0-48b0-b4de-a1d3bc395a70.png) 156 | 157 | > 快重传要要求接收方不能在自己发送数据时捎带确认,而要立即发送确认,因为 TCP 采用累积确认,如果发送方发送 1 2 3 报文段,且 2 丢失,那么接收方会一直重复确认 2 号报文段,这样发送方就会收到多个重复的报文段,当为连续 3 个时,就要立即重传丢失的报文段。快恢复算法就是如果发送方接受到 3 个连续的报文段时,就将 ssthresh 设置为此时的 cwnd 的一半并且将 cwnd 也设为此时 cwnd 的一半,然后线性增加。 158 | > 159 | 160 | 这样做主要是因为害怕网络拥塞,但是因为可以收到连续的报文段可能并不是那么拥塞,就直接从一半开始增加吧,就不从 1 开始了。 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /书籍/计算机原理专栏/指令跳转:原来if...else就是goto.md: -------------------------------------------------------------------------------- 1 | # 06 指令跳转:原来if...else就是goto 2 | 3 | 上一讲,我们讲解了一行代码是怎么变成计算机指令的。你平时写的程序中,肯定不只有 int a = 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和 for 这样的循环语句,还有函数或者过程调用。 4 | 5 | 对应的,CPU 执行的也不只是一条指令,一般一个程序包含很多条指令。因为有 if…else、for 这样的条件和循环存在,这些指令也不会一路平铺直叙地执行下去。 6 | 7 | 今天我们就在上一节的基础上来看看,一个计算机程序是怎么被分解成一条条指令来执行的。 8 | 9 | ## CPU 是如何执行指令的? 10 | 11 | 拿我们用的 Intel CPU 来说,里面差不多有几百亿个晶体管。实际上,一条条计算机指令执行起来非常复杂。好在 CPU 在软件层面已经为我们做好了封装。对于我们这些做软件的程序员来说,我们只要知道,写好的代码变成了指令之后,是一条一条**顺序**执行的就可以了。 12 | 13 | 我们先不管几百亿的晶体管的背后是怎么通过电路运转起来的,逻辑上,我们可以认为,CPU 其实就是由一堆寄存器组成的。而寄存器就是 CPU 内部,由多个触发器(Flip-Flop)或者锁存器(Latches)组成的简单电路。 14 | 15 | 触发器和锁存器,其实就是两种不同原理的数字电路组成的逻辑门。这块内容并不是我们这节课的重点,所以你只要了解就好。如果想要深入学习的话,你可以学习数字电路的相关课程,这里我们不深入探讨。 16 | 17 | 好了,现在我们接着前面说。N 个触发器或者锁存器,就可以组成一个 N 位(Bit)的寄存器,能够保存 N 位的数据。比方说,我们用的 64 位 Intel 服务器,寄存器就是 64 位的。 18 | 19 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/cdba5c17a04f0dd5ef05b70368b9a96f.jpg) 20 | 21 | 一个 CPU 里面会有很多种不同功能的寄存器。我这里给你介绍三种比较特殊的。 22 | 23 | 一个是**PC 寄存器**(Program Counter Register),我们也叫**指令地址寄存器**(Instruction Address Register)。顾名思义,它就是用来存放下一条需要执行的计算机指令的内存地址。 24 | 25 | 第二个是**指令寄存器**(Instruction Register),用来存放当前正在执行的指令。 26 | 27 | 第三个是**条件码寄存器**(Status Register),用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果。 28 | 29 | 除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。 30 | 31 | 实际上,一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。 32 | 33 | 而有些特殊指令,比如上一讲我们讲到 J 类指令,也就是跳转指令,会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。事实上,这些跳转指令的存在,也是我们可以在写程序的时候,使用 if…else 条件语句和 while/for 循环语句的原因。 34 | 35 | ## 从 if…else 来看程序的执行和跳转 36 | 37 | 我们现在就来看一个包含 if…else 的简单程序。 38 | 39 | ```cpp 40 | // test.c 41 | 42 | 43 | #include 44 | #include 45 | 46 | 47 | int main() 48 | { 49 | srand(time(NULL)); 50 | int r = rand() % 2; 51 | int a = 10; 52 | if (r == 0) 53 | { 54 | a = 1; 55 | } else { 56 | a = 2; 57 | } 58 | ``` 59 | 60 | 我们用 rand 生成了一个随机数 r,r 要么是 0,要么是 1。当 r 是 0 的时候,我们把之前定义的变量 a 设成 1,不然就设成 2。 61 | 62 | ```ruby 63 | $ gcc -g -c test.c 64 | $ objdump -d -M intel -S test.o 65 | ``` 66 | 67 | 我们把这个程序编译成汇编代码。你可以忽略前后无关的代码,只关注于这里的 if…else 条件判断语句。对应的汇编代码是这样的: 68 | 69 | ```yaml 70 | if (r == 0) 71 | 3b: 83 7d fc 00 cmp DWORD PTR [rbp-0x4],0x0 72 | 3f: 75 09 jne 4a 73 | { 74 | a = 1; 75 | 41: c7 45 f8 01 00 00 00 mov DWORD PTR [rbp-0x8],0x1 76 | 48: eb 07 jmp 51 77 | } 78 | else 79 | { 80 | a = 2; 81 | 4a: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 82 | 51: b8 00 00 00 00 mov eax,0x0 83 | } 84 | ``` 85 | 86 | 可以看到,这里对于 r == 0 的条件判断,被编译成了 cmp 和 jne 这两条指令。 87 | 88 | cmp 指令比较了前后两个操作数的值,这里的 DWORD PTR 代表操作的数据类型是 32 位的整数,而 [rbp-0x4] 则是一个寄存器的地址。所以,第一个操作数就是从寄存器里拿到的变量 r 的值。第二个操作数 0x0 就是我们设定的常量 0 的 16 进制表示。cmp 指令的比较结果,会存入到**条件码寄存器**当中去。 89 | 90 | 在这里,如果比较的结果是 True,也就是 r == 0,就把**零标志条件码**(对应的条件码是 ZF,Zero Flag)设置为 1。除了零标志之外,Intel 的 CPU 下还有**进位标志**(CF,Carry Flag)、**符号标志**(SF,Sign Flag)以及**溢出标志**(OF,Overflow Flag),用在不同的判断条件下。 91 | 92 | cmp 指令执行完成之后,PC 寄存器会自动自增,开始执行下一条 jne 的指令。 93 | 94 | 跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 4a 的位置。这个 4a,对应这里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。这个时候,CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。 95 | 96 | 跳转到执行地址为 4a 的指令,实际是一条 mov 指令,第一个操作数和前面的 cmp 指令一样,是另一个 32 位整型的寄存器地址,以及对应的 2 的 16 进制值 0x2。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。 97 | 98 | 这条 mov 指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,它的作用是一个占位符。我们回过头去看前面的 if 条件,如果满足的话,在赋值的 mov 指令执行完成之后,有一个 jmp 的无条件跳转指令。跳转的地址就是这一行的地址 51。我们的 main 函数没有设定返回值,而 mov eax, 0x0 其实就是给 main 函数生成了一个默认的为 0 的返回值到累加器里面。if 条件里面的内容执行完成之后也会跳转到这里,和 else 里的内容结束之后的位置是一样的。 99 | 100 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/b439cebb2d85496ad6eef2f61071aefa.jpeg) 101 | 102 | 上一讲我们讲打孔卡的时候说到,读取打孔卡的机器会顺序地一段一段地读取指令,然后执行。执行完一条指令,它会自动地顺序读取下一条指令。如果执行的当前指令带有跳转的地址,比如往后跳 10 个指令,那么机器会自动将卡片带往后移动 10 个指令的位置,再来执行指令。同样的,机器也能向前移动,去读取之前已经执行过的指令。这也就是我们的 while/for 循环实现的原理。 103 | 104 | ## 如何通过 if…else 和 goto 来实现循环? 105 | 106 | ```csharp 107 | int main() 108 | { 109 | int a = 0; 110 | for (int i = 0; i < 3; i++) 111 | { 112 | a += i; 113 | } 114 | } 115 | ``` 116 | 117 | 我们再看一段简单的利用 for 循环的程序。我们循环自增变量 i 三次,三次之后,i>=3,就会跳出循环。整个程序,对应的 Intel 汇编代码就是这样的: 118 | 119 | ```yaml 120 | for (int i = 0; i < 3; i++) 121 | b: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0 122 | 12: eb 0a jmp 1e 123 | { 124 | a += i; 125 | 14: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 126 | 17: 01 45 fc add DWORD PTR [rbp-0x4],eax 127 | for (int i = 0; i < 3; i++) 128 | 1a: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x1 129 | 1e: 83 7d f8 02 cmp DWORD PTR [rbp-0x8],0x2 130 | 22: 7e f0 jle 14 131 | 24: b8 00 00 00 00 mov eax,0x0 132 | } 133 | ``` 134 | 135 | 可以看到,对应的循环也是用 1e 这个地址上的 cmp 比较指令,和紧接着的 jle 条件跳转指令来实现的。主要的差别在于,这里的 jle 跳转的地址,在这条指令之前的地址 14,而非 if…else 编译出来的跳转指令之后。往前跳转使得条件满足的时候,PC 寄存器会把指令地址设置到之前执行过的指令位置,重新执行之前执行过的指令,直到条件不满足,顺序往下执行 jle 之后的指令,整个循环才结束。 136 | 137 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/7ca2eaf0df251e2ad16c412f7e625e30.jpeg) 138 | 139 | 如果你看一长条打孔卡的话,就会看到卡片往后移动一段,执行了之后,又反向移动,去重新执行前面的指令。 140 | 141 | 其实,你有没有觉得,jle 和 jmp 指令,有点像程序语言里面的 goto 命令,直接指定了一个特定条件下的跳转位置。虽然我们在用高级语言开发程序的时候反对使用 goto,但是实际在机器指令层面,无论是 if…else…也好,还是 for/while 也好,都是用和 goto 相同的跳转到特定指令位置的方式来实现的。 142 | 143 | ## 总结延伸 144 | 145 | 这一节,我们在单条指令的基础上,学习了程序里的多条指令,究竟是怎么样一条一条被执行的。除了简单地通过 PC 寄存器自增的方式顺序执行外,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。 146 | 147 | 你会发现,虽然我们可以用高级语言,可以用不同的语法,比如 if…else 这样的条件分支,或者 while/for 这样的循环方式,来实现不用的程序运行流程,但是回归到计算机可以识别的机器指令级别,其实都只是一个简单的地址跳转而已,也就是一个类似于 goto 的语句。 148 | 149 | 想要在硬件层面实现这个 goto 语句,除了本身需要用来保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器外,我们只需要再增加一个条件码寄存器,来保留条件判断的状态。这样简简单单的三个寄存器,就可以实现条件判断和循环重复执行代码的功能。 150 | 151 | 下一节,我们会进一步讲解,如果程序中出现函数或者过程这样可以复用的代码模块,对应的指令是怎么样执行的,会和我们这里的 if…else 有什么不同。 152 | 153 | ## 推荐阅读 154 | 155 | 《深入理解计算机系统》的第 3 章,详细讲解了 C 语言和 Intel CPU 的汇编语言以及指令的对应关系,以及 Intel CPU 的各种寄存器和指令集。 156 | 157 | Intel 指令集相对于之前的 MIPS 指令集要复杂一些,一方面,所有的指令是变长的,从 1 个字节到 15 个字节不等;另一方面,即使是汇编代码,还有很多针对操作数据的长度不同有不同的后缀。我在这里没有详细解释各个指令的含义,如果你对用 C/C++ 做 Linux 系统层面开发感兴趣,建议你一定好好读一读这一章节。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Typing SVG 5 | 6 |
7 |
8 | 9 | 10 |   11 | 12 | 13 |   14 | 15 | 16 |   17 | 访问量统计 18 |
19 |
20 |
21 |

📚针对考研党的一份较为全面的408笔记

22 |

个人博客(Java咖啡馆)已经搭建好了,在博客中会有更好的体验哟

23 |

24 | 25 | Java咖啡馆 26 | 27 |

28 |
除此之外,在每个目录下面都有对应的【语雀】版笔记,点击就可查看!
29 |
30 |
31 |

🔥 课程概览

32 | 33 | 34 | 39 | 44 | 45 | 46 | 51 | 56 | 57 |
35 | 计算机网络 36 |
37 | TCP/IP协议栈 | OSI、TCP/IP模型 | 应用层协议 38 |
40 | 操作系统 41 |
42 | 进程管理 | 内存管理 | 文件系统 43 |
47 | 数据结构 48 |
49 | 线性结构 | 树形结构 | 图论算法 50 |
52 | 计算机组成原理 53 |
54 | CPU设计 | 存储系统 | 指令系统 55 |
58 |
59 |
60 |
61 |

📚 本仓库包含408统考的四门专业课程的详细笔记

62 |
63 |
64 |

📝 特色说明

65 |

这是一份纯手敲的笔记,计网已经完成(4.6w字,后期大概率还会进行增加)。其余的科目正在制作中。因为本人是26考研,所以使用这个仓库进行对知识点的记录和做题的总结。因为我也是一位普通的学生,所以难免会出现一些错误,如果读者发现还请谅解。当然,你也可以通过提交issue或者通过上面的联系方式联系我!!!

66 |

✨ 知识点系统化整理

67 |
    68 |
  • 计算机网络
  • 69 |

    🎉每章尽量详细地介绍了要学习的内容,从概念出发并使用通俗易懂的语言进行解释。部分章节穿插着考研真题进行讲解,目的就是为了更好地理解并运用到解题中。

    70 | 语雀版-计算机网络 71 |
    72 |
  • 数据结构
  • 73 |

    📊数据结构比仅仅需要概念地理解,还需要上机进行“敲击”。所以除了具体的定义我还专门创建了目录为了代码的练习。其中包括每种数据结构的基本定义、常见的练习、进阶的练习题和考研真题,在一些较为复杂的题目下我都留下来具体的思路,便于理解。我们需要知道ds不仅仅需要思维还需要记忆,所以希望大家在理解的基础记忆住解题的思路,这样我们的思维才能越来越高。

    74 | 语雀版-数据结构笔记 75 |
    76 |
  • 计算机组成原理
  • 77 |

    78 | 待更新... 79 |

    80 |
  • 操作系统
  • 81 |

    82 | 待更新... 83 |

    84 |
85 |

整体目录

86 |

计算机网络

87 | 88 | 计算机网络的概念
89 | 90 | 物理层相关知识点
91 | 92 | 数据链路层相关知识点
93 | 94 | 网络层相关知识点
95 | 96 | 传输层相关知识点 97 |
98 | 99 | 应用层相关知识点
100 | 101 |

计算机网络更新完成!!!

102 | 103 |

数据结构

104 | 105 | 引言
106 | 107 | 时间、空间复杂度
108 | 109 | 线性表相关知识点
110 | 111 | 字符串相关知识点
112 | 113 | 栈、队列相关知识点
114 | 115 | 树相关知识点
116 | 117 | 图相关知识点
118 | 119 | 查找算法
120 | 121 | 排序算法
122 | 123 |

数据结构更新完成!!!

124 | 125 |

📖 重难点详细解析

126 |

待更新...

127 |

📈 历年真题分析

128 |

待更新...

129 |

🎯 复习重点指南

130 |

待更新...

131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /数据结构/数据结构实现和拓展/Array.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /** 6 | * 插入操作 7 | * @param arr 要插入的数组 8 | * @param index 插入的索引 9 | * @param val 插入的值 10 | * @param len 数组的长度 11 | */ 12 | void insert(int **arr, int *len, const int index, const int val) { 13 | // 判断数据是否合理 14 | if (arr == NULL || *arr == NULL || index < 0 || index > *len) { 15 | printf("invalid parameters\n"); 16 | return; 17 | } 18 | // 扩展数组的长度 19 | (*len)++; 20 | *arr = realloc(*arr, (*len) * sizeof(int)); 21 | if (*arr == NULL) { 22 | printf("Memory allocation failed\n"); 23 | return; 24 | } 25 | 26 | // 移动元素 27 | for (int i = *len - 1; i > index; i--) { 28 | (*arr)[i] = (*arr)[i - 1]; 29 | } 30 | (*arr)[index] = val; 31 | } 32 | 33 | /** 34 | * 35 | * @param arr 要删除的数组 36 | * @param index 删除元素的索引 37 | * @param len 数组长度 38 | * @return 是否成功删除 39 | */ 40 | bool removeIndex(int *arr, const int index, int *len) { 41 | if (arr == NULL || index < 0 || index >= *len) { 42 | printf("invalid parameters\n"); 43 | return false; 44 | } 45 | for (int i = index; i < *len - 1; i++) { 46 | arr[i] = arr[i + 1]; 47 | } 48 | // 更新长度,这样就不用处理剩余那个元素了,因为已经不会再遍历到它了 49 | (*len)--; 50 | return true; 51 | } 52 | 53 | /** 54 | * 55 | * @param arr 数组 56 | * @param num 要查找的元素 57 | * @param len 数组长度 58 | * @return 元素索引 59 | */ 60 | int findElement(const int *arr, const int num, const int len) { 61 | if (arr == NULL) { 62 | printf("the array is the null"); 63 | return -1; 64 | } 65 | for (int i = 0; i < len; i++) { 66 | if (arr[i] == num) { 67 | return i; 68 | } 69 | } 70 | return -1; 71 | } 72 | 73 | /** 74 | * 冒泡排序 75 | * @param arr 要排序的数组 76 | * @param len 数组长度 77 | */ 78 | void bubbleSort(int *arr, const int len) { 79 | for (int i = 0; i < len; i++) { 80 | for (int j = 0; j < len - i - 1; j++) { 81 | if (arr[j] > arr[j + 1]) { 82 | const int temp = arr[j]; 83 | arr[j] = arr[j + 1]; 84 | arr[j + 1] = temp; 85 | } 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 折半查找(二分查找) 92 | * @param arr 折半排序的数组 93 | * @param len 数组长度 94 | * @param num 要查找的元素 95 | */ 96 | int halfFoldFind(int *arr, const int len, const int num) { 97 | //先将其排序 98 | bubbleSort(arr, len); 99 | int start = 0; 100 | int end = len - 1; 101 | //使用双指针,每查找一次就会缩短一半 102 | while (start <= end) { 103 | const int mid = start + (end - start) / 2; 104 | if (num == arr[mid]) { 105 | return mid; 106 | }else { 107 | if (num < arr[mid]) { 108 | end = mid - 1; 109 | } 110 | else { 111 | start = mid + 1; 112 | } 113 | } 114 | } 115 | return -1; 116 | } 117 | 118 | /** 119 | * 反转数组 120 | * @param arr 要反转的数组 121 | * @param len 数组长度 122 | */ 123 | void reverse(int *arr, const int len) { 124 | // 1 2 3 4 5 125 | // 1 2 3 4 126 | // 最前面的和最后面的元素进行交换则可 127 | for (int i = 0; i < len / 2; i++) { 128 | const int temp = arr[i]; 129 | arr[i] = arr[len - i - 1]; 130 | arr[len - i - 1] = temp; 131 | } 132 | } 133 | 134 | /** 135 | * 给定一个含n(n>=1) 136 | * 个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{−5,3,2,3} 137 | * 中未出现的最小正整数是1;数组{1,2,3} 138 | * 中未出现的最小正整数是4 139 | */ 140 | int findMinPositive(int *arr, const int len) { 141 | //进行排序 142 | bubbleSort(arr, len); 143 | int index = 1; 144 | for (int i = 0; i < len; i++) { 145 | if (arr[i] <= 0) { 146 | continue; 147 | } 148 | if (arr[i] != index) { 149 | return index; 150 | } 151 | index++; 152 | } 153 | return 0; 154 | } 155 | 156 | /** 157 | * 2010年考题 158 | * 设将n(n>1) 159 | * 个整数存放到一维数组R中。设计一个在时间和空间两方面都尽可能高效的算法,将R中保存的序列循环左移p(0=1)的升序序列S,处在第[L/2]个位置的数称为S的中位数。例如,若序列S1=(11,13,15,17,19)则S1的中位数是15 193 | *,两个序列的中位数是它们所有元素的升序序列的中位数。例如,若S2=(2,4,6,8,20) 194 | *,则S1和S2的中位数是11。现在有两个等长升序序列A和B,设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。 195 | */ 196 | //肯定需要将两个数组进行排序,然后找到L/2位置的元素 197 | int findMidNumber(const int *arr, const int *brr, const int len) { 198 | //声明两个指针,用于两个数组元素的比较大小 199 | int a = 0; 200 | int b = 0; 201 | int *crr = (int *)malloc(sizeof(int) * (len + len)); 202 | while (a < len || b < len) { 203 | if (arr[a] <= brr[b] && a < len) { 204 | crr[a + b] = arr[a]; 205 | a++; 206 | }else if (arr[a] > brr[b] && b < len) { 207 | crr[a + b] = brr[b]; 208 | b++; 209 | } 210 | } 211 | const int result = crr[(len + len) / 2]; 212 | printf("traversal the array: "); 213 | for (int i = 0; i < 2 * len; i++) { 214 | printf("%d ", crr[i]); 215 | } 216 | free(crr); 217 | return result; 218 | } 219 | 220 | int main(void) { 221 | // 动态分配数组 222 | int *arr = (int *)malloc(5 * sizeof(int)); 223 | if (arr == NULL) { 224 | printf("Memory allocation failed\n"); 225 | return 1; 226 | } 227 | arr[0] = 13; 228 | arr[1] = 32; 229 | arr[2] = 133; 230 | arr[3] = 83; 231 | arr[4] = 21; 232 | int len = 5; // 当前数组长度 233 | 234 | // TODO: 测试插入操作 235 | insert(&arr, &len, 3, 5); 236 | printf("New length after insertion: %d\n", len); 237 | for (int i = 0; i < len; i++) { 238 | printf("%d ", arr[i]); 239 | } 240 | printf("\n"); 241 | 242 | // TODO: 测试移除操作 243 | removeIndex(arr, 3, &len); 244 | printf("New length after removal: %d\n", len); 245 | for (int i = 0; i < len; i++) { 246 | printf("%d ", arr[i]); 247 | } 248 | printf("\n"); 249 | printf("traversal find 133, it's index is: %d", findElement(arr, 133, len)); 250 | printf("\n"); 251 | // TODO: 测试冒泡排序 252 | printf("bubble sort result: "); 253 | bubbleSort(arr, len); 254 | for (int i = 0; i < len; i++) { 255 | printf("%d ", arr[i]); 256 | } 257 | printf("\n"); 258 | // TODO: 测试折半查找 259 | printf("half fold sort 133 index: %d", halfFoldFind(arr, len, 133)); 260 | printf("\n"); 261 | // TODO: 测试反转数组 262 | reverse(arr, len); 263 | printf("reserver the array: "); 264 | for (int i = 0; i < len; i++) { 265 | printf("%d ", arr[i]); 266 | } 267 | // 释放内存 268 | free(arr); 269 | printf("\n"); 270 | // TODO: 测试2018年考试题 271 | int brr[] = {-1, 1, 2, 4, 12, 23}; 272 | printf("the positive min is %d \n", findMinPositive(brr, 6)); 273 | // TODO: 测试2011年考题 274 | const int crr[5] = {11,13,15,17,19}; 275 | const int drr[5] = {2,4,6,8,20}; 276 | printf("\nthe 2011 year's result is %d\n", findMidNumber(crr, drr, 5)); 277 | // TODO: 测试2010年考试题 278 | int err[6] = {12, 34, 44, 54, 67, 99}; 279 | moveLeft(err, 3, 6); 280 | printf("test 2010's result: "); 281 | for (int i = 0; i < 6; i++) { 282 | printf("%d ", err[i]); 283 | } 284 | return 0; 285 | } -------------------------------------------------------------------------------- /书籍/计算机原理专栏/动态链接:程序内部的“共享单车”.md: -------------------------------------------------------------------------------- 1 | 我们之前讲过,程序的链接,是把对应的不同文件内的代码段,合并到一起,成为最后的可执行文件。这个链接的方式,让我们在写代码的时候做到了“复用”。同样的功能代码只要写一次,然后提供给很多不同的程序进行链接就行了。 2 | 3 | 这么说来,“链接”其实有点儿像我们日常生活中的**标准化、模块化**生产。我们有一个可以生产标准螺帽的生产线,就可以生产很多个不同的螺帽。只要需要螺帽,我们都可以通过**链接**的方式,去**复制**一个出来,放到需要的地方去,大到汽车,小到信箱。 4 | 5 | 但是,如果我们有很多个程序都要通过装载器装载到内存里面,那里面链接好的同样的功能代码,也都需要再装载一遍,再占一遍内存空间。这就好比,假设每个人都有骑自行车的需要,那我们给每个人都生产一辆自行车带在身边,固然大家都有自行车用了,但是马路上肯定会特别拥挤。 6 | 7 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/092dfd81e3cc45ea237bb85557bbfa51.jpg) 8 | 9 | ## 链接可以分动、静,共享运行省内存 10 | 11 | 我们上一节解决程序装载到内存的时候,讲了很多方法。说起来,最根本的问题其实就是**内存空间不够用**。如果我们能够让同样功能的代码,在不同的程序里面,不需要各占一份内存空间,那该有多好啊!就好比,现在马路上的共享单车,我们并不需要给每个人都造一辆自行车,只要马路上有这些单车,谁需要的时候,直接通过手机扫码,都可以解锁骑行。 12 | 13 | 这个思路就引入一种新的链接方法,叫作**动态链接**(Dynamic Link)。相应的,我们之前说的合并代码段的方法,就是**静态链接**(Static Link)。 14 | 15 | 在动态链接的过程中,我们想要“链接”的,不是存储在硬盘上的目标文件代码,而是加载到内存中的**共享库**(Shared Libraries)。顾名思义,这里的共享库重在“共享“这两个字。 16 | 17 | 这个加载到内存中的共享库会被很多个程序的指令调用到。在 Windows 下,这些共享库文件就是.dll 文件,也就是 Dynamic-Link Libary(DLL,动态链接库)。在 Linux 下,这些共享库文件就是.so 文件,也就是 Shared Object(一般我们也称之为动态链接库)。这两大操作系统下的文件名后缀,一个用了“动态链接”的意思,另一个用了“共享”的意思,正好覆盖了两方面的含义。 18 | 19 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/2980d241d3c7cbfa3724cb79b801d160.jpg) 20 | 21 | ## 地址无关很重要,相对地址解烦恼 22 | 23 | 不过,要想要在程序运行的时候共享代码,也有一定的要求,就是这些机器码必须是“**地址无关**”的。也就是说,我们编译出来的共享库文件的指令代码,是地址无关码(Position-Independent Code)。换句话说就是,这段代码,无论加载在哪个内存地址,都能够正常执行。如果不是这样的代码,就是地址相关的代码。 24 | 25 | 如果还不明白,我给你举一个生活中的例子。如果我们有一个骑自行车的程序,要“前进 500 米,左转进入天安门广场,再前进 500 米”。它在 500 米之后要到天安门广场了,这就是地址相关的。如果程序是“前进 500 米,左转,再前进 500 米”,无论你在哪里都可以骑车走这 1000 米,没有具体地点的限制,这就是地址无关的。 26 | 27 | 你可以想想,大部分函数库其实都可以做到地址无关,因为它们都接受特定的输入,进行确定的操作,然后给出返回结果就好了。无论是实现一个向量加法,还是实现一个打印的函数,这些代码逻辑和输入的数据在内存里面的位置并不重要。 28 | 29 | 而常见的地址相关的代码,比如绝对地址代码(Absolute Code)、利用重定位表的代码等等,都是地址相关的代码。你回想一下我们之前讲过的重定位表。在程序链接的时候,我们就把函数调用后要跳转访问的地址确定下来了,这意味着,如果这个函数加载到一个不同的内存地址,跳转就会失败。 30 | 31 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/8cab516a92fd3d7e951887808597094a.jpg) 32 | 33 | 对于所有动态链接共享库的程序来讲,虽然我们的共享库用的都是同一段物理内存地址,但是在不同的应用程序里,它所在的虚拟内存地址是不同的。我们没办法、也不应该要求动态链接同一个共享库的不同程序,必须把这个共享库所使用的虚拟内存地址变成一致。如果这样的话,我们写的程序就必须明确地知道内部的内存地址分配。 34 | 35 | 那么问题来了,我们要怎么样才能做到,动态共享库编译出来的代码指令,都是地址无关码呢? 36 | 37 | 动态代码库内部的变量和函数调用都很容易解决,我们只需要使用**相对地址**(Relative Address)就好了。各种指令中使用到的内存地址,给出的不是一个绝对的地址空间,而是一个相对于当前指令偏移量的内存地址。因为整个共享库是放在一段连续的虚拟内存地址中的,无论装载到哪一段地址,不同指令之间的相对地址都是不变的。 38 | 39 | ## PLT 和 GOT,动态链接的解决方案 40 | 41 | 要实现动态链接共享库,也并不困难,和前面的静态链接里的符号表和重定向表类似,还是和前面一样,我们还是拿出一小段代码来看一看。 42 | 43 | 首先,lib.h 定义了动态链接库的一个函数 show_me_the_money。 44 | 45 | ```cpp 46 | // lib.h 47 | #ifndef LIB_H 48 | #define LIB_H 49 | 50 | void show_me_the_money(int money); 51 | 52 | #endif 53 | ``` 54 | 55 | lib.c 包含了 lib.h 的实际实现。 56 | 57 | ```cpp 58 | // lib.c 59 | #include 60 | 61 | 62 | void show_me_the_money(int money) 63 | { 64 | printf("Show me USD %d from lib.c \n", money); 65 | } 66 | ``` 67 | 68 | 然后,show_me_poor.c 调用了 lib 里面的函数。 69 | 70 | ```cpp 71 | // show_me_poor.c 72 | #include "lib.h" 73 | int main() 74 | { 75 | int money = 5; 76 | show_me_the_money(money); 77 | } 78 | ``` 79 | 80 | 最后,我们把 lib.c 编译成了一个动态链接库,也就是 .so 文件。 81 | 82 | ```shell 83 | $ gcc lib.c -fPIC -shared -o lib.so 84 | $ gcc -o show_me_poor show_me_poor.c ./lib.so 85 | ``` 86 | 87 | 你可以看到,在编译的过程中,我们指定了一个 **-fPIC** 的参数。这个参数其实就是 Position Independent Code 的意思,也就是我们要把这个编译成一个地址无关代码。 88 | 89 | 然后,我们再通过 gcc 编译 show_me_poor 动态链接了 lib.so 的可执行文件。在这些操作都完成了之后,我们把 show_me_poor 这个文件通过 objdump 出来看一下。 90 | 91 | ```yaml 92 | $ objdump -d -M intel -S show_me_poor 93 | 复制代码 94 | …… 95 | 0000000000400540 : 96 | 400540: ff 35 12 05 20 00 push QWORD PTR [rip+0x200512] # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8> 97 | 400546: ff 25 14 05 20 00 jmp QWORD PTR [rip+0x200514] # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10> 98 | 40054c: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 99 | 100 | 0000000000400550 : 101 | 400550: ff 25 12 05 20 00 jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18> 102 | 400556: 68 00 00 00 00 push 0x0 103 | 40055b: e9 e0 ff ff ff jmp 400540 <_init+0x28> 104 | …… 105 | 0000000000400676
: 106 | 400676: 55 push rbp 107 | 400677: 48 89 e5 mov rbp,rsp 108 | 40067a: 48 83 ec 10 sub rsp,0x10 109 | 40067e: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5 110 | 400685: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 111 | 400688: 89 c7 mov edi,eax 112 | 40068a: e8 c1 fe ff ff call 400550 113 | 40068f: c9 leave 114 | 400690: c3 ret 115 | 400691: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 116 | 400698: 00 00 00 117 | 40069b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] 118 | …… 119 | ``` 120 | 121 | 我们还是只关心整个可执行文件中的一小部分内容。你应该可以看到,在 main 函数调用 show_me_the_money 的函数的时候,对应的代码是这样的: 122 | 123 | ```sql 124 | call 400550 125 | 复制代码 126 | ``` 127 | 128 | 这里后面有一个 @plt 的关键字,代表了我们需要从 PLT,也就是**程序链接表**(Procedure Link Table)里面找要调用的函数。对应的地址呢,则是 400550 这个地址。 129 | 130 | 那当我们把目光挪到上面的 400550 这个地址,你又会看到里面进行了一次跳转,这个跳转指定的跳转地址,你可以在后面的注释里面可以看到,GLOBAL_OFFSET_TABLE+0x18。这里的 GLOBAL_OFFSET_TABLE,就是我接下来要说的全局偏移表。 131 | 132 | ```yaml 133 | 400550: ff 25 12 05 20 00 jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18> 134 | 复制代码 135 | ``` 136 | 137 | 在动态链接对应的共享库,我们在共享库的 data section 里面,保存了一张**全局偏移表**(GOT,Global Offset Table)。**虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。**所有需要引用当前共享库外部的地址的指令,都会查询 GOT,来找到当前运行程序的虚拟内存里的对应位置。而 GOT 表里的数据,则是在我们加载一个个共享库的时候写进去的。 138 | 139 | 不同的进程,调用同样的 lib.so,各自 GOT 里面指向最终加载的动态链接库里面的虚拟内存地址是不同的。 140 | 141 | 这样,虽然不同的程序调用的同样的动态库,各自的内存地址是独立的,调用的又都是同一个动态库,但是不需要去修改动态库里面的代码所使用的地址,而是各个程序各自维护好自己的 GOT,能够找到对应的动态库就好了。 142 | 143 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/1144d3a2d4f3f4f87c349a93429805c8.jpg) 144 | 145 | 我们的 GOT 表位于共享库自己的数据段里。GOT 表在内存里和对应的代码段位置之间的偏移量,始终是确定的。这样,我们的共享库就是地址无关的代码,对应的各个程序只需要在物理内存里面加载同一份代码。而我们又要通过各个可执行程序在加载时,生成的各不相同的 GOT 表,来找到它需要调用到的外部变量和函数的地址。 146 | 147 | 这是一个典型的、不修改代码,而是通过修改“**地址数据**”来进行关联的办法。它有点像我们在 C 语言里面用函数指针来调用对应的函数,并不是通过预先已经确定好的函数名称来调用,而是利用当时它在内存里面的动态地址来调用。 148 | 149 | ## 总结延伸 150 | 151 | 这一讲,我们终于在静态链接和程序装载之后,利用动态链接把我们的内存利用到了极致。同样功能的代码生成的共享库,我们只要在内存里面保留一份就好了。这样,我们不仅能够做到代码在开发阶段的复用,也能做到代码在运行阶段的复用。 152 | 153 | 实际上,在进行 Linux 下的程序开发的时候,我们一直会用到各种各样的动态链接库。C 语言的标准库就在 1MB 以上。我们撰写任何一个程序可能都需要用到这个库,常见的 Linux 服务器里,/usr/bin 下面就有上千个可执行文件。如果每一个都把标准库静态链接进来的,几 GB 乃至几十 GB 的磁盘空间一下子就用出去了。如果我们服务端的多进程应用要开上千个进程,几 GB 的内存空间也会一下子就用出去了。这个问题在过去计算机的内存较少的时候更加显著。 154 | 155 | 通过动态链接这个方式,可以说彻底解决了这个问题。就像共享单车一样,如果仔细经营,是一个很有社会价值的事情,但是如果粗暴地把它变成无限制地复制生产,给每个人造一辆,只会在系统内制造大量无用的垃圾。 156 | 157 | 过去的 05~09 这五讲里,我们已经把程序怎么从源代码变成指令、数据,并装载到内存里面,由 CPU 一条条执行下去的过程讲完了。希望你能有所收获,对于一个程序是怎么跑起来的,有了一个初步的认识。 158 | 159 | ## 推荐阅读 160 | 161 | 想要更加深入地了解动态链接,我推荐你可以读一读《程序员的自我修养:链接、装载和库》的第 7 章,里面深入地讲解了,动态链接里程序内的数据布局和对应数据的加载关系。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/计算机指令:让我们试试用纸带编程.md: -------------------------------------------------------------------------------- 1 | # 05 计算机指令:让我们试试用纸带编程 2 | 3 | 你在学写程序的时候,有没有想过,古老年代的计算机程序是怎么写出来的? 4 | 5 | 上大学的时候,我们系里教 C 语言程序设计的老师说,他们当年学写程序的时候,不像现在这样,都是用一种古老的物理设备,叫作“打孔卡(Punched Card)”。用这种设备写程序,可没法像今天这样,掏出键盘就能打字,而是要先在脑海里或者在纸上写出程序,然后在纸带或者卡片上打洞。这样,要写的程序、要处理的数据,就变成一条条纸带或者一张张卡片,之后再交给当时的计算机去处理。 6 | 7 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/5d407c051e261902ad9a216c66de3fd7.jpg) 8 | 9 | 上世纪 60 年代晚期或 70 年代初期,Arnold Reinold 拍摄的 FORTRAN 计算程序的穿孔卡照片,[图片来源](https://commons.wikimedia.org/w/index.php?curid=775153) 10 | 11 | 你看这个穿孔纸带是不是有点儿像我们现在考试用的答题卡?那个时候,人们在特定的位置上打洞或者不打洞,来代表“0”或者“1”。 12 | 13 | 为什么早期的计算机程序要使用打孔卡,而不能像我们现在一样,用 C 或者 Python 这样的高级语言来写呢?原因很简单,因为计算机或者说 CPU 本身,并没有能力理解这些高级语言。即使在 2019 年的今天,我们使用的现代个人计算机,仍然只能处理所谓的“机器码”,也就是一连串的“0”和“1”这样的数字。 14 | 15 | 那么,我们每天用高级语言的程序,最终是怎么变成一串串“0”和“1”的?这一串串“0”和“1”又是怎么在 CPU 中处理的?今天,我们就来仔细介绍一下,“机器码”和“计算机指令”到底是怎么回事。 16 | 17 | ## 在软硬件接口中,CPU 帮我们做了什么事? 18 | 19 | 我们常说,CPU 就是计算机的大脑。CPU 的全称是 Central Processing Unit,中文是中央处理器。 20 | 21 | 我们上一节说了,从**硬件**的角度来看,CPU 就是一个超大规模集成电路,通过电路实现了加法、乘法乃至各种各样的处理逻辑。 22 | 23 | 如果我们从**软件**工程师的角度来讲,CPU 就是一个执行各种**计算机指令**(Instruction Code)的逻辑机器。这里的计算机指令,就好比一门 CPU 能够听得懂的语言,我们也可以把它叫作**机器语言**(Machine Language)。 24 | 25 | 不同的 CPU 能够听懂的语言不太一样。比如,我们的个人电脑用的是 Intel 的 CPU,苹果手机用的是 ARM 的 CPU。这两者能听懂的语言就不太一样。类似这样两种 CPU 各自支持的语言,就是两组不同的**计算机指令集**,英文叫 Instruction Set。这里面的“Set”,其实就是数学上的集合,代表不同的单词、语法。 26 | 27 | 所以,如果我们在自己电脑上写一个程序,然后把这个程序复制一下,装到自己的手机上,肯定是没办法正常运行的,因为这两者语言不通。而一台电脑上的程序,简单复制一下到另外一台电脑上,通常就能正常运行,因为这两台 CPU 有着相同的指令集,也就是说,它们的语言相通的。 28 | 29 | 一个计算机程序,不可能只有一条指令,而是由成千上万条指令组成的。但是 CPU 里不能一直放着所有指令,所以计算机程序平时是存储在存储器中的。这种程序指令存储在存储器里面的计算机,我们就叫作**存储程序型计算机**(Stored-program Computer)。 30 | 31 | 说到这里,你可能要问了,难道还有不是存储程序型的计算机么?其实,在没有现代计算机之前,有着聪明才智的工程师们,早就发明了一种叫 Plugboard Computer 的计算设备。我把它直译成“插线板计算机”。在一个布满了各种插口和插座的板子上,工程师们用不同的电线来连接不同的插口和插座,从而来完成各种计算任务。下面这个图就是一台 IBM 的 Plugboard,看起来是不是有一股满满的蒸汽朋克范儿? 32 | 33 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/99eb1ab1cdbdfa2d35fce456940ca651.jpg) 34 | 35 | 一台 IBM 的 Plugboard,[图片来源](https://commons.wikimedia.org/w/index.php?curid=522789) 36 | 37 | ## 从编译到汇编,代码怎么变成机器码? 38 | 39 | 了解了计算机指令和计算机指令集,接下来我们来看看,平时编写的代码,到底是怎么变成一条条计算机指令,最后被 CPU 执行的呢?我们拿一小段真实的 C 语言程序来看看。 40 | 41 | ```csharp 42 | // test.c 43 | int main() 44 | { 45 | int a = 1; 46 | int b = 2; 47 | a = a + b; 48 | } 49 | ``` 50 | 51 | 这是一段再简单不过的 C 语言程序,即便你不了解 C 语言,应该也可以看懂。我们给两个变量 a、b 分别赋值 1、2,然后再将 a、b 两个变量中的值加在一起,重新赋值给了 a 整个变量。 52 | 53 | 要让这段程序在一个 Linux 操作系统上跑起来,我们需要把整个程序翻译成一个**汇编语言**(ASM,Assembly Language)的程序,这个过程我们一般叫编译(Compile)成汇编代码。 54 | 55 | 针对汇编代码,我们可以再用汇编器(Assembler)翻译成机器码(Machine Code)。这些机器码由“0”和“1”组成的机器语言表示。这一条条机器码,就是一条条的**计算机指令**。这样一串串的 16 进制数字,就是我们 CPU 能够真正认识的计算机指令。 56 | 57 | 在一个 Linux 操作系统上,我们可以简单地使用 gcc 和 objdump 这样两条命令,把对应的汇编代码和机器码都打印出来。 58 | 59 | ```ruby 60 | $ gcc -g -c test.c 61 | $ objdump -d -M intel -S test.o 62 | ``` 63 | 64 | 可以看到,左侧有一堆数字,这些就是一条条机器码;右边有一系列的 push、mov、add、pop 等,这些就是对应的汇编代码。一行 C 语言代码,有时候只对应一条机器码和汇编代码,有时候则是对应两条机器码和汇编代码。汇编代码和机器码之间是一一对应的。 65 | 66 | ```yaml 67 | test.o: file format elf64-x86-64 68 | Disassembly of section .text: 69 | 0000000000000000
: 70 | int main() 71 | { 72 | 0: 55 push rbp 73 | 1: 48 89 e5 mov rbp,rsp 74 | int a = 1; 75 | 4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1 76 | int b = 2; 77 | b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2 78 | a = a + b; 79 | 12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 80 | 15: 01 45 fc add DWORD PTR [rbp-0x4],eax 81 | } 82 | 18: 5d pop rbp 83 | 19: c3 ret 84 | ``` 85 | 86 | 这个时候你可能又要问了,我们实际在用 GCC(GUC 编译器套装,GUI Compiler Collectipon)编译器的时候,可以直接把代码编译成机器码呀,为什么还需要汇编代码呢?原因很简单,你看着那一串数字表示的机器码,是不是摸不着头脑?但是即使你没有学过汇编代码,看的时候多少也能“猜”出一些这些代码的含义。 87 | 88 | 因为汇编代码其实就是“给程序员看的机器码”,也正因为这样,机器码和汇编代码是一一对应的。我们人类很容易记住 add、mov 这些用英文表示的指令,而 8b 45 f8 这样的指令,由于很难一下子看明白是在干什么,所以会非常难以记忆。尽管早年互联网上到处流传,大神程序员着拿小刀在光盘上刻出操作系统的梗,但是要让你用打孔卡来写个程序,估计浪费的卡片比用上的卡片要多得多。 89 | 90 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/67cf3c90ac9bde229352e1be0db24b5b.png) 91 | 92 | 从高级语言到汇编代码,再到机器码,就是一个日常开发程序,最终变成了 CPU 可以执行的计算机指令的过程。 93 | 94 | ## 解析指令和机器码 95 | 96 | 了解了这个过程,下面我们放大局部,来看看这一行行的汇编代码和机器指令,到底是什么意思。 97 | 98 | 我们就从平时用的电脑、手机这些设备来说起。这些设备的 CPU 到底有哪些指令呢?这个还真有不少,我们日常用的 Intel CPU,有 2000 条左右的 CPU 指令,实在是太多了,所以我没法一一来给你讲解。不过一般来说,常见的指令可以分成五大类。 99 | 100 | 第一类是**算术类指令**。我们的加减乘除,在 CPU 层面,都会变成一条条算术类指令。 101 | 102 | 第二类是**数据传输类指令**。给变量赋值、在内存里读写数据,用的都是数据传输类指令。 103 | 104 | 第三类是**逻辑类指令**。逻辑上的与或非,都是这一类指令。 105 | 106 | 第四类是**条件分支类指令**。日常我们写的“if/else”,其实都是条件分支类指令。 107 | 108 | 最后一类是**无条件跳转指令**。写一些大一点的程序,我们常常需要写一些函数或者方法。在调用函数的时候,其实就是发起了一个无条件跳转指令。 109 | 110 | 你可能一下子记不住,或者对这些指令的含义还不能一下子掌握,这里我画了一个表格,给你举例子说明一下,帮你理解、记忆。 111 | 112 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/ebfd3bfe5dba764cdcf871e23b29f197.jpeg) 113 | 114 | 下面我们来看看,汇编器是怎么把对应的汇编代码,翻译成为机器码的。 115 | 116 | 我们说过,不同的 CPU 有不同的指令集,也就对应着不同的汇编语言和不同的机器码。为了方便你快速理解这个机器码的计算方式,我们选用最简单的 MIPS 指令集,来看看机器码是如何生成的。 117 | 118 | MIPS 是一组由 MIPS 技术公司在 80 年代中期设计出来的 CPU 指令集。就在最近,MIPS 公司把整个指令集和芯片架构都完全开源了。想要深入研究 CPU 和指令集的同学,我这里推荐[一些](https://www.mips.com/mipsopen/)[资料](https://www.mips.com/mipsopen/),你可以自己了解下。 119 | 120 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/b1ade5f8de67b172bf7b4ec9f63589bf.jpeg) 121 | 122 | MIPS 的指令是一个 32 位的整数,高 6 位叫**操作码**(Opcode),也就是代表这条指令具体是一条什么样的指令,剩下的 26 位有三种格式,分别是 R、I 和 J。 123 | 124 | **R 指令**是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。 125 | 126 | **I 指令**,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。 127 | 128 | **J 指令**就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。 129 | 130 | ```bash 131 | add $t0,$s2,$s1 132 | ``` 133 | 134 | 我以一个简单的加法算术指令 add t0,t0,s1, $s2, 为例,给你解释。为了方便,我们下面都用十进制来表示对应的代码。 135 | 136 | 对应的 MIPS 指令里 opcode 是 0,rs 代表第一个寄存器 s1 的地址是 17,rt 代表第二个寄存器 s2 的地址是 18,rd 代表目标的临时寄存器 t0 的地址,是 8。因为不是位移操作,所以位移量是 0。把这些数字拼在一起,就变成了一个 MIPS 的加法指令。 137 | 138 | 为了读起来方便,我们一般把对应的二进制数,用 16 进制表示出来。在这里,也就是 0X02324020。这个数字也就是这条指令对应的机器码。 139 | 140 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/8fced6ff11d3405cdf941f6742b5081d.jpeg) 141 | 142 | 回到开头我们说的打孔带。如果我们用打孔代表 1,没有打孔代表 0,用 4 行 8 列代表一条指令来打一个穿孔纸带,那么这条命令大概就长这样: 143 | 144 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/31b430f9e4135f24a998b577cae8249c.png) 145 | 146 | 好了,恭喜你,读到这里,你应该学会了怎么作为人肉编译和汇编器,给纸带打孔编程了,不用再对那些用过打孔卡的前辈们顶礼膜拜了。 147 | 148 | ## 总结延伸 149 | 150 | 到这里,想必你也应该明白了,我们在这一讲的开头介绍的打孔卡,其实就是一种存储程序型计算机。 151 | 152 | 只是这整个程序的机器码,不是通过计算机编译出来的,而是由程序员,用人脑“编译”成一张张卡片的。对应的程序,也不是存储在设备里,而是存储成一张打好孔的卡片。但是整个程序运行的逻辑和其他 CPU 的机器语言没有什么分别,也是处理一串“0”和“1”组成的机器码而已。 153 | 154 | 这一讲里,我们看到了一个 C 语言程序,是怎么被编译成为汇编语言,乃至通过汇编器再翻译成机器码的。 155 | 156 | 除了 C 这样的编译型的语言之外,不管是 Python 这样的解释型语言,还是 Java 这样使用虚拟机的语言,其实最终都是由不同形式的程序,把我们写好的代码,转换成 CPU 能够理解的机器码来执行的。 157 | 158 | 只是解释型语言,是通过解释器在程序运行的时候逐句翻译,而 Java 这样使用虚拟机的语言,则是由虚拟机对编译出来的中间代码进行解释,或者即时编译成为机器码来最终执行。 159 | 160 | 然而,单单理解一条指令是怎么变成机器码的肯定是不够的。接下来的几节,我会深入讲解,包含条件、循环、函数、递归这些语句的完整程序,是怎么在 CPU 里面执行的。 161 | 162 | ## 推荐阅读 163 | 164 | 这一讲里,我们用的是相对最简单的 MIPS 指令集作示例。想要对我们日常使用的 Intel CPU 的指令集有所了解,可以参看《计算机组成与设计:软 / 硬件接口》第 5 版的 2.17 小节。 -------------------------------------------------------------------------------- /书籍/计算机原理专栏/冯.若依曼体系结构:计算机组成原理的金字塔.md: -------------------------------------------------------------------------------- 1 | # 01 冯·诺依曼体系结构:计算机组成的金字塔 2 | 3 | 学习计算机组成原理,到底是在学些什么呢?这个事儿,一两句话还真说不清楚。不过没关系,我们先从“装电脑”这个看起来没有什么技术含量的事情说起,来弄清楚计算机到底是由什么组成的。 4 | 5 | 不知道你有没有自己搞过“装机”这回事儿。在 2019 年的今天,大部分人用的计算机,应该都已经是组装好的“品牌机”。如果我们把时钟拨回到上世纪八九十年代,不少早期的电脑爱好者,都是自己采购各种电脑配件,来装一台自己的计算机的。 6 | 7 | ## 计算机的基本硬件组成 8 | 9 | 早年,要自己组装一台计算机,要先有三大件,CPU、内存和主板。 10 | 11 | 在这三大件中,我们首先要说的是**CPU**,它是计算机最重要的核心配件,全名你肯定知道,叫中央处理器(Central Processing Unit)。为什么说 CPU 是“最重要”的呢?因为计算机的所有“计算”都是由 CPU 来进行的。自然,CPU 也是整台计算机中造价最昂贵的部分之一。 12 | 13 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/a9af6307db5b3dde094c964e8940d83c.jpg) 14 | 15 | CPU 是一个超级精细的印刷电路版,[图片来源](https://www.flickr.com/photos/130561288@N04/39836037882/in/photolist-23Gb7cm-25V6DAn-q421FW-qMvhAJ-7yVugk-qMvgHb-o3NoQV-qMwDkj-qMvgT1-7yVu7T-qMvgMj-7yVu5c-py3Fpg-8pZhf1-7yZhR5-7yVuax-ewr4C-7TQAKk-7SbTox-8pZh3b-fkLugb-HCGERb-231L6Mo-5SSUsD-28WhLvN-K2Tvk-98Cc4e-6ag8YH-7Sf6KS-aDGEYV-7yY2XT-b66LSc-r2oZqk-rPcasz-7TQ1dB-754sSu-qMwEzy-npvMDK-4BDkou-zrid4-a8X3jn-5uTaCd-7SbRFV-7TTeJh-6ag8zX-6akhEm-7ihCSj-8Whgmi-6j5iUJ-6ag8m8) 16 | 17 | 第二个重要的配件,就是**内存**(Memory)。你撰写的程序、打开的浏览器、运行的游戏,都要加载到内存里才能运行。程序读取的数据、计算得到的结果,也都要放在内存里。内存越大,能加载的东西自然也就越多。 18 | 19 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/aa20e3813fd7cb438bb0c13f43e09cad.jpg) 20 | 21 | 内存通常直接可以插在主板上,[图片来源](https://www.flickr.com/photos/dennissylvesterhurd/7633424314/in/photolist-cCxi73-4DT7ov-5SFN7f-22ptD6Q-5SEAjJ-5SMkhQ-qvfnJh-7TQ7bM-5SAgnX-jwzhXx-5SFTJY-7TQe2k-atvnG7-YGowK7-4w9tXh-5SEDih-dPcqJ1-5SAgFV-8EboSi-5SGJ9r-62Yv2h-5Tft1r-5Xz9Na-89gSAF-5SFFVy-5SMcvH-5KtAAz-eaehyJ-8kYkea-rEdcLj-b39Kug-EST98f-8tR3Vk-7ihCSj-dTYG6-YL543f-4dEEe-BJ8QZ-88ZMZg-6ZzkhW-8Z6NkM-5SBoXn-6JKJfA-7Zx3Su-5SFT2q-7TQkLk-75VyrS-5SGnr4-5SJnWV-5SBpq8) 22 | 23 | 存放在内存里的程序和数据,需要被 CPU 读取,CPU 计算完之后,还要把数据写回到内存。然而 CPU 不能直接插到内存上,反之亦然。于是,就带来了最后一个大件——**主板**(Motherboard)。 24 | 25 | 主板是一个有着各种各样,有时候多达数十乃至上百个插槽的配件。我们的 CPU 要插在主板上,内存也要插在主板上。主板的**芯片组**(Chipset)和**总线**(Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此,**总线速度**(Bus Speed)决定了数据能传输得多快。 26 | 27 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/16bed40e3f1b1484e842cac3d6e596b0.jpg) 28 | 29 | 计算机主板上通常有着各种各样的插槽,[图片来源](https://www.flickr.com/photos/117150261@N02/12448712795/in/photolist-jY3UBe-7JggqE-DUWwNz-9GWzCa-bvGsRS-8m9cYn-e1BaEo-5SEAjJ-5SMkhQ-2eXVzdk-5SEDoU-dmvKB-5SAgnX-5SFTJY-e1vtir-5Pnxus-5SFFVy-63duyC-5SMcvH-jrTkcC-25V6DAn-imfxix-7VRFgR-inZF2N-io1oLM-zHB1BQ-C7aA66-dmE49-K6oVVQ-7VUTom-4pd9Jb-5SEDih-6LK87S-5SAgFV-5SGJ9r-22u9CTJ-7ihCSj-75VyrS-5PigdF-5SGnr4-5SJnWV-5SBpq8-5SNggT-jrTfcY-5SAjgT-5SSUsD-5SAgMi-4eqcQq-22cvYDk-5SAgSn) 30 | 31 | 有了三大件,只要配上**电源**供电,计算机差不多就可以跑起来了。但是现在还缺少各类输入(Input)/ 输出(Output)设备,也就是我们常说的**I/O 设备**。如果你用的是自己的个人电脑,那显示器肯定必不可少,只有有了显示器我们才能看到计算机输出的各种图像、文字,这也就是所谓的**输出设备**。 32 | 33 | 同样的,鼠标和键盘也都是必不可少的配件。这样我才能输入文本,写下这篇文章。它们也就是所谓的**输入设备**。 34 | 35 | 最后,你自己配的个人计算机,还要配上一个硬盘。这样各种数据才能持久地保存下来。绝大部分人都会给自己的机器装上一个机箱,配上风扇,解决灰尘和散热的问题。不过机箱和风扇,算不上是计算机的必备硬件,我们拿个纸板或者外面放个电风扇,也一样能用。 36 | 37 | 说了这么多,其实你应该有感觉了,显示器、鼠标、键盘和硬盘这些东西并不是一台计算机必须的部分。你想一想,我们其实只需要有 I/O 设备,能让我们从计算机里输入和输出信息,是不是就可以了?答案当然是肯定的。 38 | 39 | 你肯定去过网吧吧?不知道你注意到没有,很多网吧的计算机就没有硬盘,而是直接通过局域网,读写远程网络硬盘里面的数据。我们日常用的各类云服务器,只要让计算机能通过网络,SSH 远程登陆访问就好了,因此也没必要配显示器、鼠标、键盘这些东西。这样不仅能够节约成本,还更方便维护。 40 | 41 | 还有一个很特殊的设备,就是**显卡**(Graphics Card)。现在,使用图形界面操作系统的计算机,无论是 Windows、Mac OS 还是 Linux,显卡都是必不可少的。有人可能要说了,我装机的时候没有买显卡,计算机一样可以正常跑起来啊!那是因为,现在的主板都带了内置的显卡。如果你用计算机玩游戏,做图形渲染或者跑深度学习应用,你多半就需要买一张单独的显卡,插在主板上。显卡之所以特殊,是因为显卡里有除了 CPU 之外的另一个“处理器”,也就是**GPU**(Graphics Processing Unit,图形处理器),GPU 一样可以做各种“计算”的工作。 42 | 43 | 鼠标、键盘以及硬盘,这些都是插在主板上的。作为外部 I/O 设备,它们是通过主板上的**南桥**(SouthBridge)芯片组,来控制和 CPU 之间的通信的。“南桥”芯片的名字很直观,一方面,它在主板上的位置,通常在主板的“南面”。另一方面,它的作用就是作为“桥”,来连接鼠标、键盘以及硬盘这些外部设备和 CPU 之间的通信。 44 | 45 | 有了南桥,自然对应着也有“北桥”。是的,以前的主板上通常也有“北桥”芯片,用来作为“桥”,连接 CPU 和内存、显卡之间的通信。不过,随着时间的变迁,现在的主板上的“北桥”芯片的工作,已经被移到了 CPU 的内部,所以你在主板上,已经看不到北桥芯片了。 46 | 47 | ## 冯·诺依曼体系结构 48 | 49 | 刚才我们讲了一台计算机的硬件组成,这说的是我们平时用的个人电脑或者服务器。那我们平时最常用的智能手机的组成,也是这样吗? 50 | 51 | 我们手机里只有 SD 卡(Secure Digital Memory Card)这样类似硬盘功能的存储卡插槽,并没有内存插槽、CPU 插槽这些东西。没错,因为手机尺寸的原因,手机制造商们选择把 CPU、内存、网络通信,乃至摄像头芯片,都封装到一个芯片,然后再嵌入到手机主板上。这种方式叫**SoC**,也就是 System on a Chip(系统芯片)。 52 | 53 | 这样看起来,个人电脑和智能手机的硬件组成方式不太一样。可是,我们写智能手机上的 App,和写个人电脑的客户端应用似乎没有什么差别,都是通过“高级语言”这样的编程语言撰写、编译之后,一样是把代码和数据加载到内存里来执行。这是为什么呢?因为,无论是个人电脑、服务器、智能手机,还是 Raspberry Pi 这样的微型卡片机,都遵循着同一个“计算机”的抽象概念。这是怎么样一个“计算机”呢?这其实就是,计算机祖师爷之一冯·诺依曼(John von Neumann)提出的**冯·诺依曼体系结构**(Von Neumann architecture),也叫**存储程序计算机**。 54 | 55 | 什么是存储程序计算机呢?这里面其实暗含了两个概念,一个是“**可编程**”计算机,一个是“**存储**”计算机。 56 | 57 | 说到“可编程”,估计你会有点懵,你可以先想想,什么是“不可编程”。计算机是由各种门电路组合而成的,然后通过组装出一个固定的电路版,来完成一个特定的计算程序。一旦需要修改功能,就要重新组装电路。这样的话,计算机就是“不可编程”的,因为程序在计算机硬件层面是“写死”的。最常见的就是老式计算器,电路板设好了加减乘除,做不了任何计算逻辑固定之外的事情。 58 | 59 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/9bc9634431f627d3e684ce2f83cd946a.jpg) 60 | 61 | 计算器的本质是一个不可编程的计算机,[图片来源](https://www.flickr.com/photos/horiavarlan/4273218725/in/photolist-7vBn3V-3j7qrv-8iUqcs-biaK7a-qdmGPv-3jbGUN-6pFNS-3jbBa1-4MZAxs-292yK5p-2akim1j-26Bw8bE-qgskU-4EeDGe-NhdPhL-28gSRkC-292yLd6-4wVKuz-29iaje9-81BJ2h-27DSFgw-292yQkV-2akis1L-292yWRa-292yTqn-9sATYG-2akirG9-29ian6G-27DSDV5-9sAUCq-8EGHW5-29iaj49-2akigzf-29iarj1-MexNtE-292yUkt-LDNqXB-29jdR8d-4pyKYY-29nivE4-29iavZy-29iamfy-292yUMa-2akig6u-2akifN5-29jdQs5-29jdQhW-2akifUN-29jdRah-29jdQtN) 62 | 63 | 我们再来看“存储”计算机。这其实是说,程序本身是存储在计算机的内存里,可以通过加载不同的程序来解决不同的问题。有“存储程序计算机”,自然也有不能存储程序的计算机。典型的就是早年的“Plugboard”这样的插线板式的计算机。整个计算机就是一个巨大的插线板,通过在板子上不同的插头或者接口的位置插入线路,来实现不同的功能。这样的计算机自然是“可编程”的,但是编写好的程序不能存储下来供下一次加载使用,不得不每次要用到和当前不同的“程序”的时候,重新插板子,重新“编程”。 64 | 65 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/cbf639bab23f61d464aa80b4fd10019e.jpg) 66 | 67 | 著名的[Engima Machine](https://en.wikipedia.org/wiki/Enigma_machine)就用到了 Plugboard 来进行“编程”,[图片来源](https://commons.wikimedia.org/wiki/File:Enigma-plugboard.jpg) 68 | 69 | 可以看到,无论是“不可编程”还是“不可存储”,都会让使用计算机的效率大大下降。而这个对于效率的追求,也就是“存储程序计算机”的由来。 70 | 71 | 于是我们的冯祖师爷,基于当时在秘密开发的 EDVAC 写了一篇报告[*First Draft of a Report on the EDVAC*](https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC),描述了他心目中的一台计算机应该长什么样。这篇报告在历史上有个很特殊的简称,叫**First Draft**,翻译成中文,其实就是《第一份草案》。这样,现代计算机的发展就从祖师爷写的一份草案开始了。 72 | 73 | **First Draft**里面说了一台计算机应该有哪些部分组成,我们一起来看看。 74 | 75 | 首先是一个包含算术逻辑单元(Arithmetic Logic Unit,ALU)和处理器寄存器(Processor Register)的**处理器单元**(Processing Unit),用来完成各种算术和逻辑运算。因为它能够完成各种数据的处理或者计算工作,因此也有人把这个叫作数据通路(Datapath)或者运算器。 76 | 77 | 然后是一个包含指令寄存器(Instruction Reigster)和程序计数器(Program Counter)的**控制器单元**(Control Unit/CU),用来控制程序的流程,通常就是不同条件下的分支和跳转。在现在的计算机里,上面的算术逻辑单元和这里的控制器单元,共同组成了我们说的 CPU。 78 | 79 | 接着是用来存储数据(Data)和指令(Instruction)的**内存**。以及更大容量的**外部存储**,在过去,可能是磁带、磁鼓这样的设备,现在通常就是硬盘。 80 | 81 | 最后就是各种**输入和输出设备**,以及对应的输入和输出机制。我们现在无论是使用什么样的计算机,其实都是和输入输出设备在打交道。个人电脑的鼠标键盘是输入设备,显示器是输出设备。我们用的智能手机,触摸屏既是输入设备,又是输出设备。而跑在各种云上的服务器,则是通过网络来进行输入和输出。这个时候,网卡既是输入设备又是输出设备。 82 | 83 | 任何一台计算机的任何一个部件都可以归到运算器、控制器、存储器、输入设备和输出设备中,而所有的现代计算机也都是基于这个基础架构来设计开发的。 84 | 85 | 而所有的计算机程序,也都可以抽象为从**输入设备**读取输入信息,通过**运算器**和**控制器**来执行存储在**存储器**里的程序,最终把结果输出到**输出设备**中。而我们所有撰写的无论高级还是低级语言的程序,也都是基于这样一个抽象框架来进行运作的。 86 | 87 | ![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%e8%ae%a1%e7%ae%97%e6%9c%ba%e7%bb%84%e6%88%90%e5%8e%9f%e7%90%86/assets/fa8e0e3c96a70cc07b4f0490bfe66f2b.jpeg) 88 | 89 | 冯·诺依曼体系结构示意图,[图片来源](https://en.wikipedia.org/wiki/Von_Neumann_architecture#/media/File:Von_Neumann_Architecture.svg) 90 | 91 | ## 总结延伸 92 | 93 | 可以说,冯·诺依曼体系结构确立了我们现在每天使用的计算机硬件的基础架构。因此,学习计算机组成原理,其实就是学习和拆解冯·诺依曼体系结构。 94 | 95 | 具体来说,学习组成原理,其实就是学习控制器、运算器的工作原理,也就是 CPU 是怎么工作的,以及为何这样设计;学习内存的工作原理,从最基本的电路,到上层抽象给到 CPU 乃至应用程序的接口是怎样的;学习 CPU 是怎么和输入设备、输出设备打交道的。 96 | 97 | 学习组成原理,就是在理解从控制器、运算器、存储器、输入设备以及输出设备,从电路这样的硬件,到最终开放给软件的接口,是怎么运作的,为什么要设计成这样,以及在软件开发层面怎么尽可能用好它。 98 | 99 | 好了,这一讲说到这儿就结束了。你应该已经理解了计算机的硬件是由哪些设备组成的,以及冯·诺依曼体系结构是什么样的了。下一讲,我会带你看一张地图,也是计算机组成原理的知识地图。我们一起来看一看怎么样才是学习组成原理的好方法。 100 | 101 | ## 推荐阅读 102 | 103 | 我一直认为,读读经典的论文,是从一个普通工程师迈向优秀工程师必经的一步。如果你有时间,不妨去读一读[*First Draft of a Report on the EDVAC*](https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC)。对于工程师来说,直接读取英文论文的原文,既可以搞清楚、弄明白对应的设计及其背后的思路来源,还可以帮你破除对于论文或者核心技术的恐惧心理。 --------------------------------------------------------------------------------