彭东 操作系统实战45讲


彭东,网名LMOS,Intel傲腾项目关键开发者。

操作系统实战45讲

极客时间 专栏

  • 互联网时代的一大秘诀:获取用户反馈,快速改进产品。
  • 牛人之所以为牛人就是敢于对现有的规则说不,并勇于改变。
  • 没有硬件平台知识,写操作系统就如同空中建楼。

pedro:

  • 辨识力最重要,知道你自己需要什么,才最重要!
  • 学习思路——二八定律,80% 的功利主义,学对工作最有帮助的,20% 的情怀主义,学自己最感兴趣的。
  • 构建自己的知识库。

《巨人》:什么都舍弃不了的人,什么也改变不了!

机器运行

从按下 PC 机电源开关开始,PC 机的引导过程:
它从 CPU 上电,到加载 BIOS 固件,再由 BIOS 固件对计算机进行自检和默认的初始化,并加载 GRUB 引导程序,最后由 GRUB 加载具体的操作系统。

  1. 程序运行过程:
    gcc HelloWorld.c -E -o HelloWorld.i // 预处理:加入头文件,替换宏。
    gcc HelloWorld.c -S -c HelloWorld.s // 编译:包含预处理,将 C 程序转换成汇编程序。
    gcc HelloWorld.c -c HelloWorld.o // 汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
    gcc HelloWorld.c -o HelloWorld // 链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。

  2. 显卡
    计算机屏幕显示往往是显卡的输出,显卡有很多形式:
    性能依次上升,价格也是

  • 集成在主板的叫集显/集成显卡
  • 做在 CPU 芯片内的叫核显/核芯显卡
  • 独立存在通过 PCIE 接口连接的叫独显/独立显卡

无论 PC 上是什么显卡,它们都支持一种叫 VESA 的标准,有两种工作模式:字符模式和图形模式。
显卡们为了兼容这种标准,不得不自己提供一种叫 VGABIOS 的固件程序。

  1. make
    make 是一个工具程序,它读取一个叫“makefile”的文件,也是一种文本文件,这个文件中写好了构建软件的规则,它能根据这些规则自动化构建软件。

makefile 文件中规则是这样的:
首先有一个或者多个构建目标称为“target”;
目标后面紧跟着用于构建该目标所需要的文件,目标下面是构建该目标所需要的命令及参数。

makefile符号含义:

  • $@ 表示目标要生成的文件
  • $^ 表示所有的依赖文件
  • $< 表示第一个依赖文件
  • $? 表示比目标还要新的依赖文件列表

示例:

# 定义一个宏 CC,等于 gcc
CC = gcc 
# 定义一个宏 CFLAGS,等于 -c
CFLAGS = -c 
# 定义一个宏
OBJS_FILE = file.o file1.o file2.o file3.o file4.o 
# 定义两个伪目标 all, everything
.PHONY : all everything 
# 伪目标 all 依赖于伪目标 everything
all:everything 
# 伪目标 everything 依赖于 OBJS_FILE,
# 而 OBJS_FILE 是宏,会被替换成 file.o file1.o file2.o file3.o file4.o
everything : $(OBJS_FILE) 
%.o : %.c
   $(CC) $(CFLAGS) -o $@ $<

示例说明:

  • make 规定“#”后面为注释,make 处理 makefile 时会自动丢弃。
  • makefile 中可以定义宏,方法是在一个字符串后跟一个“=”或者“:=”符号,引用宏时要用“$(宏名)”,宏最终会在宏出现的地方替换成相应的字符串。
    例如:$(CC) 会被替换成 gcc,$( OBJS_FILE) 会被替换成 file.c file1.c file2.c file3.c file4.c。
  • .PHONY 在 makefile 中表示定义伪目标。
    所谓伪目标,就是它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令。但是伪目标可以依赖于另一个伪目标或者文件。
    例如:all 依赖于 everything,everything 最终依赖于 file.c file1.c file2.c file3.c file4.c。
  • 虽然 everything 下面并没有相关的执行命令,但是下面有个通用规则:“%.o : %.c”。其中的“%”表示通配符,表示所有以“.o”结尾的文件依赖于所有以“.c”结尾的文件。
    例如:file.c、file1.c、file2.c、file3.c、file4.c,通过这个通用规则会自动转换为依赖关系:file.o: file.c、file1.o: file1.c、file2.o: file2.c、file3.o: file3.c、file4.o: file4.c。
  • 针对这些依赖关系,分别会执行:$(CC) $(CFLAGS) -o $@ $< 命令,当然最终会转换为:gcc –c –o xxxx.o xxxx.c,这里的“xxxx”表示一个具体的文件名。

设计

内核结构

宏内核有极致的性能,微内核有极致的可移植性、可扩展性。

  1. 宏内核
    把诸如管理进程的代码、管理内存的代码、管理各种 I/O 设备的代码、文件系统的代码、图形系统代码以及其它功能模块的代码,经过编译,最后链接在一起,形成一个大的可执行程序。
    这个大程序里有实现支持这些功能的所有代码,向用户应用软件提供一些接口,这些接口就是常说的系统 API 函数。
    而这个大程序会在处理器的特权模式下运行,这个模式通常被称为宏内核模式。

宏内核结构:

  • 应用层 - 各种应用
  • 内核层 - 各种硬件驱动,IO管理器,内存管理器,文件系统,网络组件,图形系统,进程管理,安全组件,接口

明显的缺点:

  • 没有模块化,没有扩展性、没有移植性,高度耦合在一起,一旦其中一个组件有漏洞,内核中所有的组件可能都会出问题。
  • 开发一个新的功能也得重新编译、链接、安装内核。

唯一的优点:

  • 性能很好。因为在内核中,这些组件可以互相调用,性能极高。
  1. 微内核
    与宏内核架构相反,它提倡内核功能尽可能少:仅仅只有进程调度、处理中断、内存空间映射、进程间通信等功能。
    这样的内核是不能完成什么实际功能的,开发者们把实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进程。
    和用户应用进程一样,只是它们很特殊,宏内核提供的功能,在微内核架构里由这些服务进程专门负责完成。

微内核定义了一种良好的进程间通信的机制——消息。
应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。
服务进程的编程模型就是循环处理来自其它进程的消息,完成相关的服务功能。

微内核结构:

  • 应用层 - 进程管理,内存管理,文件管理,IO管理,图形系统,网络服务,安全组件,各种设备驱动,各种应用程序
  • 内核层

优点:

  • 系统结构相当清晰利于协作开发。
  • 系统有良好的移植性,微内核代码量非常少,就算重写整个内核也不是难事。
  • 微内核有相当好的伸缩性、扩展性,因为那些系统功能只是一个进程,可以随时拿掉一个服务进程以减少系统功能,或者增加几个服务进程以增强系统功能。

缺点:

  • 系统性能打折扣。分配内存时,在微内核下消息传递开销大,各个服务的进程切换开销也不小。

微内核的代表作有 MACH、MINIX、L4 系统,但是它们不是商业级的系统。
商业级的系统不采用微内核主要还是因为性能差。

内核架构

  1. Linux 内核
    Linux,全称 GNU/Linux,是一套免费使用和自由传播的操作系统,支持类 UNIX、POSIX 标准接口,也支持多用户、多进程、多线程,可以在多 CPU 的机器上运行。
    Linux 的基本思想是:一切皆文件(每个文件都有确定的用途)。

https://makelinux.github.io/kernel/map/
操作系统实战-内核架构-Linux

操作系统实战-内核架构-Linux五大重要组件

  1. Darwin-XNU 内核
    Darwin 是由苹果公司在 2000 年开发的一个开放源代码的操作系统。

苹果公司有台式计算机、笔记本、平板、手机。
台式计算机、笔记本使用了 macOS 操作系统,平板和手机则使用了 iOS 操作系统。
Darwin 作为 macOS 与 iOS 操作系统的核心,从技术实现角度说,它必然要支持 PowerPC、x86、ARM 架构的处理器。
Darwin 使用了一种微内核 Mach 和相应的固件来支持不同的处理器平台,并提供操作系统原始的基础服务,上层的功能性系统服务和工具则是整合了 BSD 系统所提供的。
苹果公司还为其开发了大量的库、框架和服务,不过它们都工作在用户态且闭源。

操作系统实战-内核架构-Darwin-XNU

  1. Windows NT 内核
    Windows NT 是微软于 1993 年推出的面向工作站、网络服务器和大型计算机的网络操作系统,也可做 PC 操作系统。
    它是一款全新从零开始开发的新操作系统,并应用了现代硬件的所有特性。
    NT = New Technology 新技术

操作系统实战-内核架构-WindowsNT

硬件

CPU工作模式

  1. 实模式
    早期 CPU 是为了支持单道程序运行而实现的,单道程序能掌控计算机所有的资源,早期的软件规模不大,内存资源也很少,所以实模式极其简单,仅支持 16 位地址空间,分段的内存模型,对指令不加限制地运行,对内存没有保护隔离作用。

  2. 保护模式
    随着多道程序的出现,就需要操作系统了。内存需求量不断增加,所以 CPU 实现了保护模式以支持这些需求。
    保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。

  3. 长模式 / AMD64 模式
    最早由 AMD 公司制定。由于软件对 CPU 性能需求永无止境,所以长模式在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器,使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力。
    长模式弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。

地址转换

MMU = Memory Management Unit 内存管理单元
MMU增加了转换的灵活性,它的实现方式是硬件执行转换过程,但又依赖于软件提供的地址转换表。

Cache与内存

Cache 的 MESI 协议
MESI 协议定义了 4 种基本状态:

  • M = Modified 修改
  • E = Exclusive 独占
  • S = Shared 共享
  • I = Invalid 无效

同步原语

数据同步方法:

  • 原子变量
  • 关中断
  • 自旋锁:
    首先读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;如果已经加锁了,就要返回第一步继续执行后续步骤,因而得名自旋锁。
    自旋锁不会引起加锁进程睡眠,如果自旋锁已经被别的进程持有,加锁进程就需要一直循环在那里,查看是否该自旋锁的持有者已经释放了锁,”自旋”一词就是因此而得名。
  • 信号量:
    信号量由一套数据结构和函数组成,它能使获取数据的代码执行流进入睡眠,然后在相关条件满足时被唤醒,这样就能让 CPU 能有时间处理其它任务。
    信号量同时解决了三个问题:等待、互斥、唤醒。

操作系统实战-同步原语-Linux数据同步

启动初始化

工作模式

大多数虚拟机都是用文件来模拟硬盘的,即主机系统 HOST OS(你使用的物理机系统)下特定格式的文件,虚拟机中操作系统的数据只是写入了这个文件中。

板级初始化

HAL = Hardware Abstraction Layer 硬件抽象层

为了分离硬件的特性,设计了 hal 层,把硬件相关的操作集中在这个层,并向上提供接口,目的是让内核上层不用关注硬件相关的细节,也能方便以后移植和扩展。

中断分成两类:(根据原因的类型不同)

  • 异常。同步的,原因是错误和故障。
    不修复错误就不能继续运行。
    这时CPU 会跳到这种错误的处理代码那里开始运行,运行完了会返回。
  • 中断。异步的,因为外部事件而产生的。
    不确定何种设备何时发出这种中断信号。
    通常设备需要 CPU 关注时,会给 CPU 发送一个中断信号,这时 CPU 会跳到处理这种事件的代码那里开始运行,运行完了会返回。

多个设备的中断信号线都会连接到中断控制器上,中断控制器可以决定启用或者屏蔽哪些设备的中断,还可以决定设备中断之间的优先线。

x86 CPU 上,最多支持 256 个中断,需要有 256 个中断门描述符和 256 个中断处理程序的入口。
x86 平台上的中断控制器有多种,最开始是 8259A,然后是 IOAPIC,最新的是 MSI-X。

操作系统实战-启动初始化-Linux初始化

内存

内存分区

内存区结构初始化
物理地址空间在逻辑上分成三个区:硬件区、内核区、用户区。

内存页的释放,最核心的还是要对空闲页面进行合并,合并成更大的连续的内存页面。

内存对象

把一个或者多个内存页面分配出来,作为一个内存对象的容器,在这个容器中容纳相同的内存对象,即同等大小的内存块。

访问内存的两大关键点,一是寻址,二是内存空间的保护。

虚拟内存

虚拟内存:一个应用往往拥有很大的连续地址空间,并且每个应用都是一样的,只有在运行时才能分配到真正的物理内存。

虚拟地址就是逻辑上的一个数值,而虚拟地址空间就是一堆数值的集合。
通常情况下,32 位的处理器有 0~0xFFFFFFFF 的虚拟地址空间,而 64 位的虚拟地址空间有 0~0xFFFFFFFFFFFFFFFF 的虚拟地址空间。

内核占用的称为内核空间,应用占用的就叫应用空间。

现实中往往是等到发生缺页异常了,才分配物理内存页面,建立对应的 MMU 页表。
这种延迟内存分配技术在系统工程中非常有用,因为它能最大限度的节约物理内存。分配的虚拟地址空间,只有实际访问到了才分配对应的物理内存页面。

Linux 内核中,一个 page 结构表示一个物理内存页面。

NUMA = Non-Uniform Memory Access
非均匀存储器访问 体系结构:
在很多服务器和大型计算机上,如果物理内存是分布式的,由多个计算节点组成,那么每个 CPU 核都会有自己的本地内存,CPU 在访问它的本地内存的时候就比较快,访问其他 CPU 核内存的时候就比较慢。

内存压缩不是指压缩内存中的数据,而是指移动内存页面,进行内存碎片整理,腾出更大的连续的内存空间。
如果内存碎片整理了,还是不能成功分配内存,就要杀死进程以便释放更多内存页面了。

进程

进程

从进程的结构看,进程是一个应用程序运行时刻的实例。
从进程的功能看,进程是应用程序运行时所需资源的容器。
从操作系统对进程实现的角度来说,进程是一堆数据结构。

多进程

进程调度器是为了在合适的时间点、合适的代码执行路径上进行进程调度。
就是从当前运行进程切换到另一个进程上运行,让当前进程停止运行,由 CPU 开始执行另一个进程的代码。

进程的时间 = CPU 总时间 * 进程的权重 / 就绪队列所有进程权重之和

操作系统实战-进程-进程状态切换

操作系统实战-进程-Linux进程调度

设备I/O

驱动模型:操作系统内核要为驱动程序开发者提供哪些功能接口函数。
I/O 包:把各种操作所需的各种参数封装在一个数据结构中,用来统一驱动程序功能函数的形式。

总线是组织设备和驱动的容器,也是同类设备的共有功能的抽象层。

Linux设备类型:

设备类型 描述和举例
字符设备 以字节流形式被访问的设备,比如字符终端和串口设备
块设备 以数据块形式被访问的设备,比如硬盘、光盘等
网络设备 主机与主机之间进行数据交换的设备
杂项设备 一些不符合Linux预先确定的字符设备,划分为杂项设备

操作系统实战-设备-计算机结构

文件系统

FS

文件系统存放文件数据的格式,类 UNIX 系统和 Windows 系统都采用了相同的方案,
逻辑上认为一个文件就是一个可以动态增加、减少的线性字节数组,即文件数据的每个字节都一一对应到这个线性数组中的每个元素。

文件系统超级块 / 文件系统描述块:包含文件系统标识、版本、状态,储存介质大小,文件系统逻辑储存块大小,位图所在的储存块,根目录等重要信息的数据结构。

格式化操作并不是把设备上所有的空间都清零,而是在这个设备上重建了文件系统用于管理文件的一整套数据结构。
格式化后的设备,还能通过一些反删除软件找回一些文件。
在储存设备上创建文件系统,其实就是执行格式化操作,即重建文件系统的数据结构。

文件的六大基本操作:新建、删除、打开、读、写、关闭。

Linux VFS

VFS = Virtual File System 虚拟文件系统

VFS 是 Linux 中一个中间层,它抽象了文件系统共有数据结构和操作函数集合。
一个具体的文件系统只要实现这些函数集合就可以插入 VFS 中;也因为 VFS 的存在,使得 Linux 可以同时支持各种不同的文件系统。

VFS 用 inode 结构表示一个文件索引结点,它里面包含文件权限、文件所属用户、文件访问和修改时间、文件数据块号等一个文件的全部信息,一个 inode 结构就对应一个文件。
目录也是文件,需要用 inode 索引结构来管理目录文件数据。

描述抽象出一个文件系统的四大对象:超级块、目录结构、文件索引节点,打开文件的实例。

读写是两个操作,只是数据流向不同:

  • 读操作是数据从文件经由内核流向进程。数据 -> 文件 -> 内核 -> 进程
  • 写操作是数据从进程经由内核流向文件。 文件 <- 内核 <- 进程 <- 数据

操作系统实战-文件系统-LinuxVFS结构图

网络

每个网络栏的地址都符合通用的 URI 语法。
URI 语法一般由五个分层序列组成:
URI = scheme:[//authority]path[?query][#fragment]
URI = 方案:[//授权]路径[?查询][#片段ID]

按照由高到低的优先级,DNS 域名解析的过程排列如下:
DNS解析 > 浏览器DNS缓存 > hosts文件 > 本地DNS服务器 > ISP DNS服务器

可靠性传输:建立 TCP 连接。

IP 层协议的函数都要对网络数据包做后面这 5 步操作:

  1. 数据包校验和检验
  2. 防火墙对数据包过滤
  3. IP 选项处理
  4. 数据分片和重组
  5. 接收、发送和前送

为了完成上述操作,IP 层被设计成三个部分:IP 寻址,路由,分包组包。
IP 地址并不是以主机数目进行配置的,而是根据网卡数来进行。

网卡 MAC 地址指的是计算机网卡的物理地址 Physical Address,MAC 地址被固化到网卡中,用来标识一个网络设备。
MAC 地址是唯一且无重复的,由国际标准化组织分配,用来确保网络中的每个网卡是唯一的。

数据包转换电信号的过程:
数据包通过网络协议栈的层层处理,最终得到了 MAC 数据包,交给网卡驱动程序,
网卡驱动程序会将 MAC 数据包写入网卡的缓冲区(网卡上的内存),在 MAC 数据包的起止位置加入起止帧和校验序列,
最后网卡会将加入起止帧和校验序列的 MAC 数据包转化为电信号,发送出去。

套接字:在 Linux 操作系统中,替代传输层以上协议实体的标准接口。它负责实现传输层以上所有的功能。
套接字是 TCP/IP 协议栈对外的窗口,通信的抽象描述。

操作系统实战-网络-网络数据发送过程

操作系统实战-网络-网络数据接受过程

接口

设备向 CPU 发送一个中断信号,CPU 接受到这个电子信号后,在允许响应中断的情况下,就会中断当前正在运行的程序,自动切换到相应的 CPU R0 特权级,并跳转到中断门描述符中相应的地址上运行中断处理代码。
中断处理代码是操作系统内核的代码,这样 CPU 的控制权就转到操作系统内核的手中。

软中断指令:模拟了中断的电子信号的指令。
应用软件也可以给 CPU 发送中断。
在现代 CPU 上,一旦执行该指令,CPU 就要中断当前正在运行的程序,自动跳转到相应的固定地址上运行代码。
这里的代码也就是操作系统内核的代码,这样 CPU 的控制权同样会回到操作系统内核的手中。

在 x86 CPU 上是 int 指令。
int 常数 // 常数表示 CPU 从中断表描述符表中取得第几个中断描述符进入内核
比如,int255

Exception 异常,切入的视角是处理器被动接收到了异常。
Interrupt 中断,对应的视角是处理器主动申请,

glibc 是 Linux 内核上 C 程序运行的基础。

虚拟化

KVM

KVM = Kernel-based Virtual Machine 基于内核的虚拟机

虚拟化的本质是一种资源管理的技术,它可以通过各种技术手段把计算机的实体资源模拟出来,如:CPU、RAM、存储、网络、I/O 等等。

计算机最重要的资源,可以简单抽象成为三大类:

  • CPU - 计算
  • 内存 - 存储,如 RAM, ROM
  • I/O - 连接各种设备的抽象

容器

容器是这样一种工作模式:
轻量、拥有一个模具(镜像),既可以规模生产出多个相同集装箱(运行实例),又可以和外部环境(宿主机)隔离,最终实现对“内容”的打包隔离,方便其运输传送。
容器的目的就是提供一个独立的运行环境。

Docker 是一个基于 Linux 操作系统下的 Namespace 和 Cgroups 和 UnionFS 的虚拟化工具。
以 Docker 为蓝本,容器的基础功能架构,包括客户端 Client、管理进程 Host、镜像仓库 Registry 三大部分。
Docker 的核心是引擎进程 Host,包括引擎进程 Daemon、驱动 Driver、容器管理包 Libcontainer、镜像 Images。

现代 CPU 加速

常见的五种加速套路:

  • 更多的硬件指令
  • 通过缓存来提高数据装载效率
  • 流水线乱序执行与分支预测
  • 多核心 CPU
  • 超线程。
    为了尽可能地“压榨”硬件资源,工程师们又设计了额外的逻辑处理单元来保证多个可执行程序可以共享同一个 CPU 内的资源。

指令系统类型:
CISC = Complex Instruction Set Computer 复杂指令集计算机:让硬件实现更多且复杂的指令。
RISC = Reduced Instruction Set Computer 精简指令集计算机:使用少部分相对简短且长度统一的指令集。高性能、低功耗、低成本。

CISC(复杂) RISC(精简)
指令 数量多,使用频率差别大,可变长格式 数量少,使用频率接近,定长格式,大部分为单周期指令,操作寄存器,只有Load/Store 操作内存
寻址方式 支持多种 支持方式少
实现方式 微程序控制技术 微码 增加通用寄存器,硬布线逻辑控制为主,适合采用流水线
其他 研制周期长 优化编译,有效支持高级语言

“其他”是标准用法,可以指代人和事物,建议日常使用及规范的书面文件使用;
“其它”是日常用法,一般只能指代事物。

ARM = Advanced RISC Machine
在 ARM 中,每条指令都是 4 个字节解码器

苹果的 M1 芯片,它在继承了 ARM 优点的同时,还做了很多优化,
比如,增加解码器提高并行计算能力,利用提高指令缓存空间的机制提升了指令加载与计算的效率,还引入了统一内存的巧妙设计。
严格讲,M1 芯片其实并不是 CPU。M1 芯片其实是包含了 CPU、GPU、IPU、DSP、NPU、IO 控制器、网络模块、视频编解码器、安全模块等很多异构的处理器共同组成的系统级 SOC 芯片。

ARMv8

ARMv8 是首款支持 64 位指令集的 ARM 处理器架构,
它兼容了 ARMv7 与之前处理器的技术基础,
也兼容现有的 A32(ARM 32bit)指令集,
扩充了基于 64bit 的 AArch64 架构。

ARMv8 定义的三种架构:

  • ARMv8-A Application 架构
    支持基于内存管理的虚拟内存系统体系结构 VMSA ,支持 A64、A32、T32 指令集。
    主打高性能,广泛应用于移动智能设备。
  • ARMv8-R Real-time 架构
    支持基于内存保护的受保护内存系统架构 PMSA ,支持 A32 和 T32 指令集。
    一般用于实时计算系统。
  • ARMv8-M Microcontroller 架构
    一个压缩成本的嵌入式架构,需要极低延迟中断处理。支持 T32 指令集的变体。
    主打低功耗,一般用于物联网设备。

ARM CPU工作模式:

工作模式 说明 特权模式 异常模式
用户 - user 模式 用户程序运行模式
系统 - system 模式 运行特权级的操作系统任务
一般中断 - IRQ 模式 普通中断模式
快速中断 - FIQ 模式 快速中断模式
管理 - supervisor 模式 提供操作系统使用的一种保护模式,swi命令状态
中止 - abort 模式 虚拟内存管理和内存数据访问保护
未定义指令终止 - undefined 模式 支持通过软件仿真硬件的协处理

文章作者: SS Tian
文章链接: https://sstian.github.io
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SS Tian !
  目录