docker 的 buildx 需要内核支持,不升级内核的情况下,实际上可以利用 qemu 和 binfmt_misc 在 x86_64 上构建其他架构容器,之前这块也是没大概看组合原理,这次龙芯 loongarch64 适配的时候正好理解
由来
借着这次在 x86_64 上构建龙芯的 docker 镜像理解了下 qemu 和 binfmt_misc 的组合大概,相信不少人使用过下面的,运行一个 qemu-user-static 的容器后,就可以在 x86_64 机器上执行其他架构容器
1 | $ uname -m |
原理讲解
这次文章就是介绍运行这个特权容器到底干了啥和用了啥科技
qemu
qemu 是虚拟化技术,可以完全模拟一个虚拟机,如果你安装 RHEL 的 gui 系统或者使用过 proxmox,能看到默认就带 qemu 和 kvm,kvm 是内核模块工作在内核态,它大部分承担硬件翻译执行能力, qemu 和 kvm 是互相弥补不足的,组合起来模拟性能更强和模拟的方面更全面。
qemu 分为两种模式:
- 模拟/系统模式(System Mode):模拟整个计算机系统,包括中央处理器及其他周边设备,它使能为跨平台编写的程序进行测试及排错工作变得容易。其亦能用来在一部主机上虚拟数个不同的虚拟计算机,类似我们平常使用的Vmare、VirtualBox等。运行的二进制是
qemu-system-$arch
- 用户模式(User Mode):在 os 上直接启动运行非本机器架构的 Linux 程序,因为 qemu-user 有内置了系统调用翻译,运行的二进制是
qemu-$arch
、qemu-$arch-static
qemu user 模式
看上面使用到的镜像名字里有 qemu-user
字样,说明利用到的是 qemu 的用户模式,下面是一个示例:
1 | $ cat arch.go |
编译上面两个文件后,可以看到是不同架构的二进制文件,当然你也可以其他语言,例如 c 语言写个类似的,然后交叉编译工具编译出来:
1 | CGO_ENABLED=0 go build -o arch_amd64 arch.go |
直接执行是执行不了其他架构的,报错也可能是 exec format error 之类的,下面就是使用 qemu-user 模式运行
1 | # 你也可以用包管理安装,有些系统的自带的源里没有静态编译的 qemu-$arch |
如果需要运行的程序不是静态链接的,需要宿主机行支持,或者 chroot 进去后,把 qemu-user-static 拷贝进去执行。
Linux 的 binfmt_misc
windows 上可以设置不同的后缀文件使用不同软件打开,在 Linux 上,也有类似的功能。Linux的内核从很早开始就引入了一个叫做 Miscellaneous Binary Format(binfmt_misc)的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开。比 Windows 更加强大的地方是,它不光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(ELF Magic Byte)来判断。
binfmt_misc 开启
使用下面命令启用这个功能:
1 | # 内核编译的选项 Executable file formats / Emulations ---> <M> Kernel support for MISC binaries |
binfmt_misc 的注册格式
然后就可以在 /proc/sys/fs/binfmt_misc
里面看到两个文件
register
:该文件只能写入,不可读取,写入注册格式就能注册status
:读取它可以看到当前 binfmt_misc 是否启用
注册格式很简单:
1 | :name:type:offset:magic:mask:interpreter:flags |
- name:名字,用来标识这条记录的,理论上可以取任何名字,只要不重名就可以了
- type:
- M 表示目标文件的内容 magic 来识别的,
- E 则是认文件后缀
- offset 在 type 为 M 的时候有用,指定识别的时候的偏移位置,默认是 0 。
- magic 即用来识别的具体 magic 内容
- mask 在 type 为 M 的时候用,默认值全是 0xFF 的 bitmask,二进制下某一位为1,则表示文件的 ELF magic 必须和 magic对应的位匹配。magic 长度一般是 40,具体可以去看 qemu-binfmt-conf.sh 脚本,获取二进制文件的 magic 可以使用例如下面的 shell:
xxd hello_arm64 | awk '{for(i=2;i<NF;i++){a=a$i;c++;if(c>9){print a;exit;}}}'
该示例是 40 个
- interpreter 具体用来执行的解释器,必须用绝对路径。不能超过127个字符。
- flags 可选的,用来控制 interpreter 打开文件的行为:
- P 用于保存用户于命令行中输入的原程序名(通过将程序名添加到argv);interpreter 必须知悉到此标记才能正确将此额外函数作为其argv[0]传递至解释程序。
- O 用于打开程序文件并将其文档描述符传递至interpreter以读取用户无法读取的文件(对于无读取权限的用户而言)。
- C 用于根据程序文件而非 interpreter 文件决定新进程凭证(参见setuid);此值默认为O。
- F 最常用的,白话讲就是配置的时候会把 interpreter 文件导入到内存里,后续用户空间和 chroot 里都没这个文件也可以执行。
- 一些注意事项
- offset + size(magic) 一定要少于 128 位
- 后添加的会先被匹配
取消注册
1 | # 全部 |
multiarch/qemu-user-static 做了啥
1 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes |
实际上运行上面的特权容器,等同于:
- 特权容器下,容器内的 /proc 和 mount 是和宿主机一致的
- 入口 register.sh 脚本 挂载 binfmt_misc
- shift 掉
--reset
后,剩下参数传递执行 qemu 的 qemu-binfmt-conf.sh 脚本,注册各种架构二进制的打开方式为对应的qemu-$arch-static
- 因为
multiarch/qemu-user-static
镜像里有 COPY qemu-*-static /usr/bin/,然后把-p yes
传递给最终在脚本里,也就是注册的时候会开 flags 的F
,会把这些/usr/bin/qemu-*-static
导入到内存里。这个需要内核支持,centos 3.10内核就不支持 - 然后在 x86_64 上执行其他架构的镜像,会被 binfmt_misc 识别,调用内存里的
/usr/bin/qemu-*-static
翻译执行
还有个 multiarch/qemu-user-static:register
的镜像,运行是:
1 | docker run --rm --privileged multiarch/qemu-user-static:register --reset |
因为这个镜像里没 /usr/bin/qemu-$arch-static
,所以也不会用 -p yes
,这种使用方式就需要你挂载 qemu 到 /usr/bin/ 里:
1 | $ docker run --rm -t -v $PWD/qemu-aarch64-static:/usr/bin/qemu-aarch64-static arm64v8/ubuntu uname -m |
或者制作 docker 镜像的时候,内部 /usr/bin/qemu-$arch-static
存在。例如我们业务的 Dockerfile_arm64
:
1 | # 提前所有 jenkins 上执行 docker run --rm --privileged multiarch/qemu-user-static:register --reset |
或者 第一阶段是 golang 交叉编译,第二阶段是最终架构:
1 | FROM go:xxx as build |
当然,也不是只有 docker,因为是内核拦截的 execute 的 syscall ,所以此刻宿主机上也可以执行:
1 | $ ./arch_arm64 |
一些注意点
- register.sh 脚本 里默认是把
QEMU_BIN_DIR
设置为/usr/bin/
,直接使用 qemu 的脚本默认则是/usr/local/bin/
- register.sh 脚本 里默认设置了
--qemu-suffix "-static"
,意味着最终的 interpreter 会带有-static
后缀 - multiarch/qemu-user-static 镜像不一定更新及时,可能需要自己替换里面的一些文件,例如我这几天搞的 loongarch64 ,龙芯官方提供的
qemu-loongarch64
+ 最新的 qemu-binfmt-conf.sh 才行
1 | FROM multiarch/qemu-user-static |
构建后测试
1 | $ docker build -t multiarch/qemu-user-static-2 . |
之前使用 multiarch/qemu-user-static 的 qemu-loongarch64-static
会报错 Function not implemented
,龙芯官方给我的才可以正常使用,这块后续得等龙芯他们合并到 qemu 去了
1 | $ docker run --rm -ti cr.loongnix.cn/library/alpine:3.11.11 ls |
env
发现 dockerfile 里这样会影响 qemu 执行
1 | ARG QEMU_VERSION |