融云技术Native C/C++ 服务适配多指令集 CPU 漫谈
Posted 融云RongCloud
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了融云技术Native C/C++ 服务适配多指令集 CPU 漫谈相关的知识,希望对你有一定的参考价值。
而对于国产 CPU 行业而言,除了北大众志、海光、兆芯等少数几家手上拥有 x86_64 指令集授权外,其他厂家基本都专注于非 x86_64 指令集。如:华为和飞腾在研发 ARM CPU,龙芯常年专注于 MIPS CPU;近些年兴起的 RISC-V 也吸引了众多厂家的目光。
-
如果应用基于浏览器即可满足所有功能,国产化系统发型版本,一般内置了 Firefox 浏览器,应用对 Firefox 浏览器进行适配即可。 -
如果应用是一个轻度的桌面应用,可以考虑使用 Electron 的方案。Electron(原名为 Atom Shell)是 GitHub 开发的一个开源框架。它通过使用 Node.js(作为后端)和 Chromium 的渲染引擎(作为前端)完成跨平台的桌面 GUI 应用程序的开发。这种情况下,首先可以看下国产化系统的软件源是否有对应的 Electron 依赖(一般都有);如果没有,需要进行编译。 -
如果应用是一个重度的 Native 应用,则需要把代码在对应的指令集和系统依赖上进行编译,工作量较大。
-
如果使用的是面向虚拟机的语言,比如 Java 或基于 JVM 的各种语言(Kotlin、Scala 等),则服务不需要进行特殊的适配。一般国产化系统的软件源中一般都会自带已实现好的 OpenJDK;如果没有,参见的指令集一般也都能找到对应的 OpenJDK 开源实现,可以自行安装。 -
近些年出现的一些对 C 库无强依赖的语言,如 Go 等。编译体系在设计之初就考虑了多种目标系统和指令集架构,只需要在编译时指定目标系统和架构即可,如 GOOS=linux GOARCH=arm64 go build ,如果使用了 CGO 还需要指定 C/C++ 的编译器。 -
如果服务使用的是 C/C++ 等 Native 语言,且对系统 C 库有强依赖,则需要把代码在对应的指令集和系统依赖上进行编译,工作量较大。
通过编译和运行的整个流程分析,我们可以在业界找到很多工具,提升适配的效率。
因为追求 CI/CD 快速搭建并且对系统无依赖,我们会采用 docker 的方式进行编译。
通过在 Dockerfile 中从零开始安装所有工具和依赖库,可以严格保证每次编译的环境是一致的。
在编译阶段,如果依赖较为清晰,可以使用交叉编译的方式,在 x86_64 机器上直接编译对应的程序。
如果系统依赖库比较复杂但是代码量比较小的情况下,还可以考虑使用 qemu 模拟对应的指令集进行本地编译,其实就是用 qemu 把 gcc/clang 的指令直接翻译一遍而环境都不需要修改。docker 的 buildx 就是基于这个思路实现的。
但是需要注意的是,qemu 是通过指令集翻译的方式来执行的,效率不高,代码量大点的情况下基本不用考虑这个方案了。docker buildx 也还不太稳定,本人不止一次使用 buildx 编译把 docker service 搞挂。
代码量大且编译工具依赖较深的情况下,gcc/clang 交叉编译可能不好改造,可以直接在对应的指令集上进行本地编译。
具体情况需要看工程实践,代码仓库巨大且改造困难的情况下,甚至可以不同模块一部分使用交叉编译一部分使用模拟或者目标机器本地编译,最后再链接到一起,只要保证工程效率最高即可。
特定 CPU 效率优化
不同的 CPU,即使是同一个体系结构,支持的具体机器指令也有不同,这些都会影响到执行效率,比如是否能使用到一些长指令。正常的优化流程是,各 CPU 厂家把自己的特性推到 gcc/clang/llvm,作为开发者在编译时就可以使用到了。但是这个过程需要时间,并且对编译器的版本还有要求,所以各 CPU 厂家也会在文档中说明,在编译时可能需要注意 gcc 具体版本,甚至在执行 gcc 命令时增加特殊的参数。
我们 RTC 服务使用了 kubernetes 进行服务编排,所以编译产出物其实是 docker images。在面对多指令集架构的时候,选择基础镜像需要更加谨慎。
docker 基础镜像通常大家会从 scratch、alpine、debian、debian-slim、ubuntu、centos 里面进行选择。
除非特殊要求,否则大家都不会选择 scratch 空镜像从头构建。
而 alpine 体积只有 5M,看起来很美好,但是系统 C 库是基于 musl 而不是桌面系统或服务器常见的 glic,重度 C/C++ 应用,尽量不要使用这个版本,否则可能会导致工作量大增。
debian-slim 相比于 debian,主要是删除了一些不常用的文件和文档,一般服务可以选择 slim。
而 ubuntu 和 centos 都缺少 mips 架构的官方支持,如果工作中要考虑龙芯等 mips CPU 的情况,则可以考虑 debian-slim。
另外一点注意的是,很多开源软件的编译验证系统选择的是 ubuntu,而在编译时需要注意的是,ubuntu 是基于 debian unstable 或者 testing 分支的,使用的 C 库版本与 debian 会有差异。
CI 编译完,可以使用 qemu + docker 启动服务,在一个架构上对多指令集进行简单验证,而不需要依赖与特性的机器和环境。
docker 支持将聚合多种架构的 image 聚合到一个 tag,即在不同的机器上,执行docker pull会根据当前系统的指令集和架构,获取对应的镜像。但是这样的设计,在一个系统上,生成和存储多架构,使用和验证时特殊指定一个架构,会较为繁琐。所以我们在工程实践中,直接在 image tag 上标识出了不同的架构,这样生成、获取、验证镜像都非常简单直接。
如果最终程序需要在 Native 而非 Docker 环境运行,面对不同的系统依赖,可以通过修改当前进程的LD_LIBRARY_PATH环境变量指定动态库加载路径。
在编译生成可执行二进制文件的时候,可以通过执行 ldd 命令,将所有的依赖库拷贝出来,通过 LD_LIBRARY_PATH 指定到对应的路径,可以隔绝对系统库的依赖。有些情况下,因为系统基础 C 库版本不一致,可能会导致可执行二进制文件在链接的情况下就会出问题。这时候可以考虑 patchelf 对 ELF 进行修改,只用指令的 C 库和链接器,隔绝各种环境依赖。
-
qemu: https://www.qemu.org/ -
docker buildx: https://docs.docker.com/buildx/working-with-buildx/ -
patchelf: https://github.com/NixOS/patchelf
以上是关于融云技术Native C/C++ 服务适配多指令集 CPU 漫谈的主要内容,如果未能解决你的问题,请参考以下文章
IMRTC技术两生花,看融云如何打造“IM+RTC+Push”一站式通信云服务
带有 C/C++ 和 nw.js/node.js/node-native-module (C++) 的终端服务器上的 IPC?