技术

Python实践 下一个平台Agent 激发LLM涌现——提示工程 LLM微调理论及实践 大佬沉思 LLM外挂知识库 LLMOps 多模态LLM Python一些比较有意思的库 LLM部分技术源码学习 LangChain源码学习 通用分布式计算引擎Ray Python并发 go依赖注入 go collection gc的基本原理 golang性能分析及优化 数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

大模型推理服务框架 模型服务化(未完成) 大模型RHLF 大模型训练 大模型推理 从Attention到Transformer k8s设备管理 LLM工具栈 ddd从理念到代码 如何应用LLM 小鼠如何驾驭大象(LLM)? 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练和推理 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 从RNN到Attention pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

k8s设备管理 多类型负载协调员Koordinator controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群管理 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度/Job调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 深入controller openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go informer源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 什么是云原生 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

计算机组成原理

2021年02月05日

简介

本文主要来自极客时间《深入浅出计算机组成原理》,相关书籍《编码:隐匿在计算机软硬件背后的语言》

从电路开始讲计算机底层实现原理 有几个gif非常贴切。

  1. 门电路——信号的关联
  2. 加法器——信号的计算
  3. 时钟——信号的震荡
  4. RAM——保存信号
  5. 程序——自动化。控制单元该怎么实现呢?只要给定输入信号,约定输出信号,任何组件都可以造出来。PS:信号的流动

入门

一些比较有意思的表述:

  1. 存放在内存里的程序和数据,需要被 CPU 读取,CPU 计算完之后,还要把数据写回到内存。然而 CPU 不能直接插到内存上,反之亦然。于是,就带来了最后一个大件——主板。主板的芯片组(Chipset)和总线(Bus)解决了 CPU 和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此,总线速度(Bus Speed)决定了数据能传输得多快。
  2. 手机里只有 SD 卡(Secure Digital Memory Card)这样类似硬盘功能的存储卡插槽,并没有内存插槽、CPU 插槽这些东西。没错,因为手机尺寸的原因,手机制造商们选择把 CPU、内存、网络通信,乃至摄像头芯片,都封装到一个芯片,然后再嵌入到手机主板上。这种方式叫 SoC,也就是 System on a Chip(系统芯片)。
  3. 冯·诺依曼体系结构(Von Neumann architecture),也叫存储程序计算机。
    1. 可编程。计算机是由各种门电路组合而成的,然后通过组装出一个固定的电路板,来完成一个特定的计算程序。一旦需要修改功能,就要重新组装电路。这样的话,计算机就是“不可编程”的。计算器的本质是一个不可编程的计算机
    2. 存储。一个计算机程序,不可能只有一条指令,而是由成千上万条指令组成的。但是 CPU 里不能一直放着所有指令,所以计算机程序平时是存储在存储器中的。程序本身是存储在计算机的内存里,可以通过加载不同的程序来解决不同的问题。

任何一台计算机的任何一个部件都可以归到运算器、控制器、存储器、输入设备和输出设备中,而所有的现代计算机也都是基于这个基础架构来设计开发的。具体来说,学习组成原理,其实就是学习控制器、运算器、内存的工作原理。PS:这就叫框定了模型

电报是现代计算机的一个最简单的原型

制造一台电报机非常容易。电报机本质上就是一个“蜂鸣器 + 长长的电线 + 按钮开关”。蜂鸣器装在接收方手里,开关留在发送方手里。双方用长长的电线连在一起。当按钮开关按下的时候,电线的电路接通了,蜂鸣器就会响。短促地按下,就是一个短促的点信号;按的时间稍微长一些,就是一个稍长的划信号。有了电报机,只要铺设好电报线路,就可以传输我们需要的讯息了。

虽然一个按钮开关的电报机很“容易”操作,但是却不“方便”操作。因为电报员要熟记每一个字母对应的摩尔斯电码,并且需要快速按键来进行输入,一旦输错很难纠正。但是,因为电路之间可以通过与、或、非组合完成更复杂的功能,我们完全可以设计一个和打字机一样的电报机,每按下一个字母按钮,就会接通一部分电路,然后把这个字母的摩尔斯电码输出出去。

cpu 四大基本电路

CPU 也可以想象成我们熟悉的软件,一样能抽象成几大模块,然后再进行模块化开发。其实 CPU 其实就是一些简单的门电路像搭积木一样搭出来的。从最简单的门电路,搭建成半加器、全加器,然后再搭建成完整功能的 ALU。这些电路里呢,有完成各种实际计算功能的组合逻辑电路,也有用来控制数据访问,创建出寄存器和内存的时序逻辑电路

  1. 组合逻辑电路。ALU 的功能就是在特定的输入下,没有状态的,根据组合电路的逻辑,生成特定的输出。
  2. 锁存器和 D 触发器电路。我们需要有一个电路,能够存储到上一次的计算结果。这个计算结果并不一定要立刻拿到电路的下游去使用,但是可以在需要的时候拿出来用。这也是现代计算机体系结构中的“冯·诺伊曼”机的一个关键,就是程序需要可以“存储”,而不是靠固定的线路连接或者手工拨动开关,来实现计算机的可存储和可编程的功能。
  3. 计数器电路。需要有一个“自动”的电路,按照固定的周期,不停地实现 PC 寄存器自增,自动地去执行“取指令 - 指令译码 - 执行指令“的步骤。我们的程序执行,并不是靠人去拨动开关来执行指令的。我们希望有一个“自动”的电路,不停地去一条条执行指令。
  4. 译码器电路。我们需要有一个“译码”的电路。无论是对于指令进行 decode,还是对于拿到的内存地址去获取对应的数据或者指令,我们都需要通过一个电路找到对应的数据。

把这四类电路,通过各种方式组合在一起,就能最终组成功能强大的 CPU 了。我们通过一个自动计数器的电路,来实现一个PC寄存器,不断生成下一条要执行的计算机指令的内存地址,然后通过地址译码器,从内存里读出对应的指令,写入到D触发器实现的指令寄存器中,再通过另外一个指令译码器,把它解析成我们需要执行的指令和操作数的地址,这些就是计算器中的控制器。我们把opcode和对应的操作数,发送给ALU进行计算,得到计算结果,再写回到寄存器以及内存里,这就是运算器。时钟信号,则提供了协调这样一条条指令的执行时间和先后顺序的机制。PS:每一个电路又都是几个基本电路的组合,比如 基本电路>门电路>全加器>加法器

在最简单的情况下,我们需要让每一条指令,从程序计数,到获取指令、执行指令,都在一个时钟周期内完成。如果 PC 寄存器自增地太快,程序就会出错。因为前一次的运算结果还没有写回到对应的寄存器里面的时候,后面一条指令已经开始读取里面的数据来做下一次计算了。这个时候,如果我们的指令使用同样的寄存器,前一条指令的计算就会没有效果,计算结果就错了。在这种设计下,我们需要在一个时钟周期里,确保执行完一条最复杂的 CPU 指令,也就是耗时最长的一条 CPU 指令。这样的 CPU 设计,我们称之为单指令周期处理器(Single Cycle Processor)。

基本电路的基本电路

开关 A,一开始是断开的,由我们手工控制;另外一个开关 B,一开始是合上的,磁性线圈对准一开始就合上的开关 B。于是,一旦我们合上开关 A,磁性线圈就会通电,产生磁性,开关 B 就会从合上变成断开。一旦这个开关断开了,电路就中断了,磁性线圈就失去了磁性。于是,开关 B 又会弹回到合上的状态。

开关 A 闭合(也就是相当于接通电路之后),开关 B 就会不停地在开和关之间切换,这个不断切换的过程,对于下游电路来说,就是不断地产生新的 0 和 1 这样的信号。如果你在下游的电路上接上一个灯泡,就会发现这个灯泡在亮和暗之间不停切换。

R-S 触发器

  1. 接通开关 R,输出变为 1,即使断开开关,输出还是 1 不变。
  2. 接通开关 S,输出变为 0,即使断开开关,输出也还是 0。 也就是,当两个开关都断开的时候,最终的输出结果,取决于之前动作的输出结果,这个也就是我们说的记忆功能。PS: 所谓记忆电路,就是电路会持续输出电路上一次改动的输出结果(毕竟输出重新作为了输入) CPU 的工作原理是什么?

电路竟然具备存储信息的能力了。现在为保存信息你需要同时设置S端和R端,但你的输入是有一个(存储一个bit位嘛),当D为0时,整个电路保存的就是0,否则就是1。

在R-S 触发器电路里加两个与门和一个小小的时钟信号,我们就可以实现一个利用时钟信号来操作一个电路了。这个电路可以帮我们实现什么时候可以往 Q 里写入数据。当时钟信号 CLK 在低电平的时候,与门的输入里有一个 0,两个实际的 R 和 S 后的与门的输出必然是 0。也就是说,无论我们怎么按 R 和 S 的开关,根据 R-S 触发器的真值表,对应的 Q 的输出都不会发生变化。PS:时钟信号和你真正要写入的信号求与,可以让你的输入信号都是0。 时序电路。

一个 D 型触发器,只能控制 1 个比特的读写,但是如果我们同时拿出多个 D 型触发器并列在一起,并且把用同一个 CLK 信号控制作为所有 D 型触发器的开关,这就变成了一个 N 位的 D 型触发器,也就可以同时控制 N 位的读写。

有了时钟信号,我们可以提供定时的输入;有了 D 型触发器,我们可以在时钟信号控制的时间点写入数据。加法器的两个输入,一个始终设置成 1,另外一个来自于一个 D 型触发器 A。我们把加法器的输出结果,写到这个 D 型触发器 A 里面。于是,D 型触发器里面的数据就会在固定的时钟信号为 1 的时候更新一次。这样,我们就有了一个每过一个时钟周期,就能固定自增 1 的自动计数器了。这个自动计数器,可以拿来当我们的 PC 寄存器。每次自增之后,我们可以去对应的 D 型触发器里面取值,这也是我们下一条需要运行指令的地址。

读写数据所需要的译码器:现在,我们的数据能够存储在 D 型触发器里了。如果我们把很多个 D 型触发器放在一起,就可以形成一块很大的存储空间,甚至可以当成一块内存来用。那我们怎么才能知道,写入和读取的数据,是在这么大的内存的哪几个比特呢?

如果把“寻址”这件事情退化到最简单的情况,就是在两个地址中,去选择一个地址。我们通过一个反相器、两个与门和一个或门,就可以实现一个 2-1 选择器。通过控制反相器的输入是 0 还是 1,能够决定对应的输出信号,是和地址 A,还是地址 B 的输入信号一致。PS:这个2-1选择器打个比方就是,如果AB 两根线都通电/想数据输出,我可以通过“一根线” 决定让AB 哪根线出去。一个byte 8根线,则需要3根线即可决定谁“输出”。3根线是8根线的开关,它的值可以视为一个地址,而不是真的是一个“物理地址”。按地址取数据不是数据的搬运,是电流的组合的结果

总结:

  1. 01输入经过门电路可以转换为输出,输出可以作为另一个门电路的输入
  2. 门电路的组合(与或非异或等) 有以下可能
    1. 输入 单纯的是 逻辑算术运算的 “因子”
    2. 时钟的01输入 与常规01输入求与 可以使得01输入有效或无效,或者说让输入有“节奏”
    3. 上一次输出原样作为电路的一部分输入,可以产生“记忆”的效果
    4. 某个输入可以让其它输入有效或无效,即起到选择作用

在我们的想象中,物理机机器内部似乎要维护一张大而全的指令集表,每次cpu 读入一个机器指令时,去扫描这张指令集表,从而识别机器码,并进一步判断该操作码后是否有操作数。而事实上,cpu 内置的“指令集” 是硬件结构/数字电路。只要向cpu 传递一个指令,cpu 便可以根据预先设定好的电路进行解码(高低电平),然后操作对应的寄存器或者某些电路 去读取该操作码后面的操作数。同时,另一些电路支持读取当前机器指令的下一条指令。 如此一来,cpu 便能完成取指 ==> 译码 ==> 执行 ==> 取指的循环了。PS:从内存中取数 可以认为是 让 所有的 存储电路有选择的输出,内存地址每一根线即开关。那么译码(地址+时钟)也可以认为 让所有电路有选择的 触发,指令代码的每一位即开关。

硬件描述语言

在芯片设计时,根据不同模块的功能特点,通常把它们分为数字电路模块和模拟电路模块。模拟电路还是像早期的半导体电路那样,处理的是连续变化的模拟信号,所以只能用传统的电路设计方法。而数字电路处理的是已经量化的数字信号,往往用来实现特定的逻辑功能,更容易被抽象化,所以就产生了专门用于设计数字电路的硬件描述语言。没错,芯片前端设计工程师写 Verilog 代码的目的,就是把一份电路用代码的形式表示出来,然后由计算机把代码转换为所对应的逻辑电路。

写软件代码和写硬件代码的最大区别是什么?Verilog 代码和 C 语言、Java 等这些计算机编程语言有本质的不同,在可综合(这里的“可综合”和代码“编译”的意思差不多)的 Verilog 代码里,基本所有写出来的东西都对应着实际的电路。所以,我们用 Verilog 的时候,必须理解每条语句实质上对应着什么电路,并且要从电路的角度来思考它为何要这样设计。而高级编程语言通常只要功能实现就行。在 Verilog 里举几个例子来说明:

  1. 声明变量的时候,如果指定是一个 reg,那么这个变量就有寄存数值的功能,可以综合出来一个实际的寄存器;
  2. 如果指定是一段 wire,那么它就只能传递数据,只是表示一条线。
  3. 写一个判断语句,可能就对应了一个 MUX(数据选择器)
  4. 写一个 for 可能就是把一段电路重复好几遍。

在开始一个大的芯片设计时,往往需要先从整个芯片系统做好规划,在写具体的 Verilog 代码之前,把系统划分成几个大的基本的功能模块。之后,每个功能模块再按一定的规则,划分出下一个层次的基本单元。Verilog 都是基于模块进行编写的,一个模块实现一个基本功能,大部分的 Verilog 逻辑语句都放在模块内部。

一个包含加、减、与、或、非等功能的简单 ALU 模块

module alu(a, b, cin, sel, y);
  input [7:0] a, b;
  input cin;
  input [3:0] sel;
  output [7:0] y;

  reg [7:0] y;
  reg [7:0] arithval;
  reg [7:0] logicval;

  // 算术执行单元
  always @(a or b or cin or sel) begin
    case (sel[2:0])
      3'b000  : arithval = a;
      3'b001  : arithval = a + 1;
      3'b010  : arithval = a - 1;
      3'b011  : arithval = b;
      3'b100  : arithval = b + 1;
      3'b101  : arithval = b - 1;
      3'b110  : arithval = a + b;
      default : arithval = a + b + cin;
    endcase
  end

  // 逻辑处理单元
  always @(a or b or sel) begin
    case (sel[2:0])
      3'b000  : logicval =  ~a;
      3'b001  : logicval =  ~b;
      3'b010  : logicval = a & b;
      3'b011  : logicval = a | b;
      3'b100  : logicval =  ~((a & b));
      3'b101  : logicval =  ~((a | b));
      3'b110  : logicval = a ^ b;
      default : logicval =  ~(a ^ b);
    endcase
  end

  // 输出选择单元
  always @(arithval or logicval or sel) begin
    case (sel[3])
      1'b0    : y = arithval;
      default : y = logicval;
    endcase
  end

endmodule

通过逻辑综合,我们就能完成从 Verilog 代码到门级电路的转换。而逻辑综合的结果,就是把设计的 Verilog 代码,翻译成门级网表 Netlist。逻辑综合需要基于特定的综合库,不同的库中,门电路基本标准单元(Standard Cell)的面积、时序参数是不一样的。所以,选用的综合库不一样,综合出来的电路在时序、面积上也不同。因此,哪怕采用同样的设计,选用台湾的台积电(TSMC)工艺和上海的中芯国际(SMIC)的工艺,最后生产出来的芯片性能也是有差异的。通常,工业界使用的逻辑综合工具有 Synopsys 的 Design Compiler(DC),Cadence 的 RTL Compiler,Synplicity 的 Synplify 等。然而,这些 EDA 工具都被国外垄断了,且需要收取高昂的授权费用。学习用途 可以使用轻量级开源综合工具Yosys

可以看到,这张图是由基本的 and、or、not、add、sub、cmp、mux 等电路单元组成。如果你还想进一步了解它们底层电路结构,可以自行查阅大学里学过的《数电》《模电》。

整体思路:cpu ==> 模块 == 硬件描述语言 ==> 基本电路单元 ==> 门电路

CPU怎么识别我们写的代码?

CPU怎么识别我们写的代码?啥叫半导体?半导体其实就是介于导体和绝缘体中间的一种东西。

假设你有8位加法器了,也有一个位移1位(乘2)的模块了。串起来你就能算(A+B)×2了。要是想算A×2+B呢?简单,你把加法器模块和位移模块的接线改一下就行了,改成输入A先过位移模块,再进加法器就可以了。改个程序还得重新接线?你以为呢?实际上,编程就是把线来回插啊。早期的计算机就是这样编程的,而且插线是个细致且需要耐心的工作,所以那个时候的程序员都是清一色的漂亮女孩子,穿制服的那种。

插线也是个累死人的工作。所以我们需要改进一下,让CPU可以根据指令来相加或者乘2。这里再引入两个模块

  1. flip-flop,简称FF,中文好像叫触发器,这个模块的作用是存储1bit数据。我们用FF来保存计算的中间数据(也可以是中间状态或者别的什么),1bit肯定是不够的,不过我们可以并联嘛,用4个或者8个来保存4位或者8位数据。这种我们称之为寄存器(Register)。
  2. MUX,中文叫选择器,input: sel,I0,I1。 sel=0 则输出I0,sel=1 则输出I1。当然选择器可以做的很长,比如四进一出。

有这个东西我们就可以给加法器和乘2模块(位移)设计一个激活针脚。这个激活针脚输入1则激活这个模块,输入0则不激活。这样我们就可以控制数据是流入加法器还是位移模块了。于是我们给CPU先设计8个输入针脚,4位指令,4位数据。我们再设计3个指令:

  1. 0100,数据读入寄存器
  2. 0001,数据与寄存器相加,结果保存到寄存器
  3. 0010,寄存器数据向左位移一位(乘2)

分别用指令输入的第二第三第四个针脚连接寄存器,加法器和位移器的激活针脚。这样我们输入0100这个指令的时候,寄存器输入被激活,其他模块都是0没有激活,数据就存入寄存器了。同理,如果我们输入0001这个指令,则加法器开始工作,我们就可以执行相加这个操作了。

这里就可以简单回答:CPU是为什么能看懂这些二级制的数呢?为什么CPU能看懂,因为CPU里面的线就是这么接的呗。你输入一个二进制数,就像开关一样激活CPU里面若干个指定的模块以及改变这些模块的连通方式,最终得出结果。

  1. CPU里面可能有成千上万个小模块,一个32位/64位的指令能控制那么多吗?我们举例子的CPU里面只有3个模块,就直接接了。真正的CPU里会有一个解码器(decoder),把指令翻译成需要的形式。
  2. 举例子的简单CPU,如果我输入指令0011会怎么样?当然是同时激活了加法器和位移器从而产生不可预料的后果,简单的说因为你使用了没有设计的指令,所以后果自负。在真正的CPU上这么干大概率就是崩溃,不过肯定会有各种保护性的设计。
  3. 你设计的指令【0001,数据与寄存器相加,结果保存到寄存器】这个一步做不出来吧?毕竟还有一个回写的过程,实际上确实是这样。我们设计的简易CPU执行一个指令差不多得三步,读取指令,执行指令,写寄存器。经典的RISC设计则是分5步:读取指令(IF),解码指令(ID),执行指令(EX),内存操作(MEM),写寄存器(WB)。我们平常用的x86的CPU有的指令可能要分将近20个步骤。那CPU怎么知道自己走到哪一步了呢?具体的设计涉及到FSM(finite-state machine),也就是有限状态机理论,以及怎么用FF实装。

现在我们有3个指令了。我们来试试算个(1+4)X2+3吧。

  1. 0100 0001 ;寄存器存入1
  2. 0001 0100 ;寄存器的数字加4
  3. 0010 0000 ;乘2
  4. 0001 0011 ;再加三

题外话:栈是一种数据结构,跟CPU无关。只不过栈这个数据结构实在太常用了,以至于CPU会针对性的进行优化。给CPU再加一个寄存器SP,并定义两个指令:一个PUSH,一个POP。动作分别是把数据写入SP的地址,然后SP=SP+1,POP的话反过来。这样有什么好处呢?好处在于PUSH/POP这样的指令消耗特别少,速度特别快。而栈这种数据结构在各种程序里用的又特别频繁,设计成专用的指令则可以很大程度上提升效率。

其它

CPU 的工作原理是什么?

  1. 晶体管可以实现 AND、OR、NOT
  2. 给定足够的AND、OR以及NOT门,就可以实现任何一个逻辑函数,{AND、OR、NOT}就是逻辑完备的
  3. 一个与门和一个异或门就可以实现二进制加法,有了加法任何算术运算就ok了
  4. 两个NAND门的组合可以实现记忆一个bit的效果(NAND由与或非门组合而成) ==> 电路有了记忆能力 ==> 寄存器和内存就出来了。

cpu空闲的时候在干嘛?内核设计者创建了一个叫做空闲任务的进程,在 Linux 下就是第 0号进程。当其它进程都处于不可运行状态时,调度器就从队列中取出空闲进程运行,显然,空闲进程永远处于就绪状态,且优先级最低,空闲进程是一个不断执行 halt 指令的循环。 这条指令会让部分CPU进入休眠状态,从而极大减少对电力的消耗,通常这条指令也被放到循环中执行。halt 指令是特权指令,也就是说只有在内核态下 CPU 才可以执行这条指令,程序员写的应用都运行在用户态,因此你没有办法在用户态让 CPU 去执行这条指令。

《编程高手必学的内存知识》

  1. 组合逻辑电路是指,输出仅由输入信号的状态决定的电路。而时序逻辑电路是指,电路的输出值同时依赖于电路过去的状态和现在的输入,所以时序逻辑电路中含有用于记忆电路状态的存储单元。
  2. 寄存器的存储单元:RS 锁存器 ==> D 锁存器 ==> D 触发器
  3. RAM 大体上分为两类:静态随机访问存储器(Static RAM, SRAM)和动态随机访问存储器(Dynamic RAM,DRAM)。
    1. SRAM 存储单元的特点是使用 6 个晶体管来实现。其中两个 P 型 MOS 管和两个 N 型 MOS 管组成两个反相器用于存储信息。还有两个用于控制存储单元是否选通。
    2. DRAM 的存储单元使用一个 MOS 管和一个电容实现。DRAM 因为有电容的存在,不再是单纯的逻辑电路。 DRAM电路相比 SRAM 更加简单,也就更容易大规模集成,成本也更低,但是它的读取速度比较慢。
  4. 存储器在存储数据时,一定要区分数据是要存在哪个地方的,也就是说,我们要想办法对存储器里的各个单元进行编码,并且能将地址总线的数据转换成相应存储单元的使能信号,然后进一步区分控制总线的读写信号。如果是读操作,存储控制器就要将存储单元内的值读入数据总线,如果是写操作,控制器就应该把数据总线的数据写入存储单元。现在的核心问题在于,控制器如何对地址信号进行转换,使得目标字节的字线变成高电压?这个工作需要译码器来完成。