CPU架构对开发环境的影响

最近更换了新的笔记本 M4 Pro,之前是2019款的 MacBook,处理器是 Intel i7。

在使用新电脑进行开发的过程中,遇到一个问题就是构建Docker镜像的时候,发现有个警告。意思是基础镜像是 linux/amd64 平台的,而当前预期使用 linux/arm64:

docker-build-warning

在 MacBook 上运行时,可以运行,但有类似的警告:

docker-run-warning

在 Linux 拉取该镜像会直接失败:

docker-run-error

我们先了解一下CPU的架构、程序的组成,最终我们会理解,为什么有上边的警告和失败。

一、CPU架构与程序

ARM与Intel

apple-chip

MacBook的 Apple 芯片版本(如M4、M4 Pro、M3等)基于ARM架构设计,与之前基于 Intel 架构的 i7、i9 芯片采用不同的 CPU 架构。

ARM架构之前主要用于移动设备和嵌入式系统,因为其功耗较低 ;而Intel架构的芯片由于性能较强,多用于笔记本、台式机和服务器。

近些年,苹果出于战略考虑,使用自主设计的M系列芯片。在性能与能效的提升、软硬件深度优化、高度集成的SoC设计等方面,都有比较明显的收益。

arm-vs-intel

ARM 架构和 Intel 架构(x86/x64架构)最核心的区别是他们的指令集体系结构(Instruction-Set Archietecture, ISA)不同:ARM(Advanced RISC Machine)是基于RISC(精简指令集),强调简单和高效;而x86/x64架构是基于CISC(复杂指令集),强调功能丰富和灵活性,包括上千条指令

前边提到的「ARM 功耗低而Intel性能高」以及应用场景的不同,只是这个核心区别的一个的结果。

上边提到的AMD64,x64于1999年由AMD设计,AMD首次公开64位集以扩展给x86,Intel 的 64 位处理器也兼容这个架构。Intel 称之为 "Intel 64",RPM包管理以称之为 "x86-64" 或 "x86_64",Microsoft称之为 "x64",Linux发行版则使用 "amd64"。比如在Ubuntu的官方文档中,我们也能看到这些称呼方式:

arch-names

为什么程序需要适配不同架构

我们知道一个程序的编译过程如下图,会经过预处理器、编译器、汇编器、链接器最终得到可执行的二进制目标文件。

compilation-system

其中汇编语言 ,使用人类可读的符号和助记符来表示二进制的机器指令和操作数,可以被认为是机器码的符号化表示。

汇编器(Assembler)负责将汇编代码转换成相应的机器码。你肯定也听过反汇编,是将二进制目标文件反向转换成可读的汇编代码。

这里提到的机器指令 也就是上边说的CPU指令集中的指令,因此汇编语言是架构依赖的,不同架构的汇编语言是不同的。

反过来说,ARM架构下的可执行二进制文件拿到 AMD64 架构上不能用,因为其机器指令就不一样。再往前推一步,ARM架构上的编译器构建出的.s文件和AMD64架构上的也不一样。

平台(platform) = 操作系统 x 处理器架构

再说回上图,我们经常说的 C 语言可以跨平台移植

比如一个 Hello World 的C代码,可以在 Linux 上编译出来运行,Windows上也可以;可以在 ARM 架构的机器上编译出来,AMD64 架构上也可以。

但在 Linux 上构建出来的二进制文件,放到 Windows 上是不能运行的。

这里常说的平台,通常是指 操作系统 x 处理器架构。除了上边解释了处理器架构影响二进制结果,操作系统也会有影响,因为不同的操作系统有不同的ABI(应用二进制接口)、系统调用机制、目标文件格式(如ELF、PE等)。

后边我们也会看到通过 platform 参数指定特定平台,构建适用于不同平台的镜像:

build-platform-demo

操作系统是程序

前边介绍了说「程序是可执行的二进制文件」,其实操作系统也可以认为是一种可执行的二进制文件。

我们可以从构建使用来理解一下,操作系统这一特殊的程序,也是需要适配不同的处理器架构的。

先说构建 ,我们可以在 AMD64 的环境中,来构建 ARM 架构的 Linux 内核 ,这里需要使用 ARCH 指定架构,使用 CROSS_COMPILE 来指定交叉编译工具的前缀:

cross-compile-arm-linux

再说使用,我们这里以发行版 Ubuntu 为例,其官方文档支持 amd64、i386、arm64 等很多主流架构:

ubuntu-supported-architectures

你在页面可以直接下载安装镜像,而没有让用户选择架构,是因为页面可以通过浏览器获得一些平台信息,包括架构、操作系统版本等信息:

chrome-request-with-arch

二、Docker容器与镜像

前面有提到交叉编译(Cross Compilation) ,意思是指在一种平台上编译程序,以便在另一种平台上运行

同样地,我们也可以在一种平台上,构建另外一种平台上可以运行的镜像

容器是程序

我这样描述不太准确,但是为了让大家理解为什么不同的平台上需要特定的镜像

更准确的应该是,运行的容器是进程 ,所以要运行的文件也是需要适配平台的------就是镜像。

前面的文章 《揭秘容器(三):容器镜像》镜像索引小节中有介绍,OCI容器镜像由所谓的 manifest(清单) 、配置、层集和可选的镜像索引组成。清单内容如下,是一个对象数组,每个对象中有通过 platform 指定操作系统和架构。

docker-index-json-content

通过这种方式,Docker镜像就可以支持多平台。在不同的平台上要运行,选择合适的平台版本即可。

接下来,我们看看 ARM64 平台和 AMD64 平台上交叉构建和使用尽享的过程。下文为了表达更清晰,我直接用Mac M4 替换 ARM64。

Buildx 和 QEMU 仿真器

Docker Buildx 是 Docker 的一个扩展工具。提供了强大构建功能:

  • 多平台构建:Buildx 允许你在一个构建过程中生成适用于多个平台(如 x86_64、ARM、ARM64 等)的镜像。
  • 高级构建功能:支持更复杂的构建场景,如多阶段构建、缓存优化、外部存储驱动等。
  • 并行构建:可以并行构建多个镜像,提升构建速度。

QEMU 是一个开源的仿真器和虚拟机管理器,能够仿真多种不同的硬件平台:

  • 跨平台仿真:允许在一个平台上运行为另一个平台编译的二进制文件。例如,在 x86_64 主机上运行 ARM64 的二进制文件。
  • 开发和测试:开发者可以在本地开发和测试为不同架构编写的代码,而无需实际的硬件。
  • 容器仿真:与 Docker 结合使用,允许在不同架构的主机上运行不同架构的容器镜像。

Docker Desktop通常已经包含了 Buildx 插件,并且整合了 QEMU。

M4上构建x64镜像

我们直接使用一个最简单的 Dockerfile:

css 复制代码
FROM ubuntu:24.04

M4 MacBook上可以利用 Docker 的Buildx工具,来构建可以在 x86 架构上运行的 Docker 镜像。

build-multiple-arch

我们可以看到构建的过程中,有使用两种不同的镜像。

使用 docker buildx 构建的多平台镜像不会直接显示在 docker image ls 列表中。可以将其 push 到远端,然后再进行使用。构建并 push 的指令:

bash 复制代码
docker buildx build --platform linux/amd64,linux/arm64 -t xxx.com/wlbcoder/ubuntu:base --push .

这样我们就可以再直接使用 docker run 来运行了。为了比较,我这里在 M4 MacBook 和 Intel 的开发机上分别运行镜像,你会看到他们使用的是不同的架构

docker-run-on-diff-arch

M4上运行x64镜像

你可以直接拉取并运行 amd64 架构的镜像,Docker 会自动使用 QEMU 进行仿真。

下边是在 MacBook 上运行两种环境的镜像的比较,第二条指令中的 --platform=linux/amd64 指定了要运行的镜像架构。

docker-run-diff-arch-on-mac

Docker 会自动处理架构仿真,除了platform之外,不需要用户额外的操作。

三、其他

关于架构和程序的话题,你还能想到哪些?可以评论补充

.jar包不算程序

前边说 C 的 Hello World 构建出来之后是可执行二进制文件,放到不同平台通常不能运行。而 Java 的 Hello World 会生成 .jar 包则不同,.jar 包是 Java 应用程序的打包格式,与平台无关的中间表示形式------字节码,可以在任何安装了 Java 虚拟机(JVM)的平台上运行。JVM 负责将字节码翻译成特定平台的机器码并执行。

.jar 包的运行,依赖目标平台上有兼容的 JVM,因此 .jar 包不能算上完整的程序。这点就跟 Python 代码需要解释器运行差不多。

相关推荐
Java中文社群5 分钟前
面试官:你项目是如何保证高可用的?
java·后端·面试
狂炫一碗大米饭7 分钟前
Event Loop事件循环机制,那是什么事件?又是怎么循环呢?
前端·javascript·面试
程序员小续10 分钟前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
拳布离手29 分钟前
修改Conda默认存储路径到AutoDL数据盘
面试
冲鸭ONE1 小时前
for循环优化方式有哪些?
后端·性能优化
兮动人1 小时前
DBeaver连接OceanBase数据库
后端
拳布离手1 小时前
Xinference Huggingface常用命令
面试
Nu111 小时前
weakMap 和 weakSet 原理
前端·面试
刘鹏3781 小时前
深入浅出Java中的CAS:原理、源码与实战应用
后端
Enddme1 小时前
「面试必问!Proxy对比defineProperty的六大核心差异与底层原理」
前端·面试