背景介紹前端
因爲近些年,CPU 行業的摩爾定律失效了,不少廠商都紛紛從指令集架構層面尋找替代解決方案。在消費產品領域,蘋果推出了 ARM 指令集的 Apple Silicon M1,大獲好評;在雲服務行業,華爲雲和 Amazon 前些年就已經在自研並上線了 ARM CPU 服務器,在成本和性能方面很有建樹。linux
而對於國產 CPU 行業而言,除了北大衆志、海光、兆芯等少數幾家手上擁有 x86_64 指令集受權外,其餘廠家基本都專一於非 x86_64 指令集。如:華爲和飛騰在研發 ARM CPU,龍芯常年專一於 MIPS CPU;近些年興起的 RISC-V 也吸引了衆多廠家的目光。git
對於各類非 x86_64 CPU,行業軟件的移植和適配主要會涉及嵌入式端、手機端、桌面端和服務器。嵌入式端考慮到功耗,通常邏輯較爲簡單,代碼移植和適配複雜程度不高。手機端通常都是 Android ARM,不涉及太多適配問題。github
桌面端分爲三種狀況:docker
若是應用基於瀏覽器便可知足全部功能,國產化系統髮型版本,通常內置了 Firefox 瀏覽器,應用對 Firefox 瀏覽器進行適配便可。
若是應用是一個輕度的桌面應用,能夠考慮使用 Electron 的方案。Electron(原名爲 Atom Shell)是 GitHub 開發的一個開源框架。它經過使用 Node.js(做爲後端)和 Chromium 的渲染引擎(做爲前端)完成跨平臺的桌面 GUI 應用程序的開發。這種狀況下,首先能夠看下國產化系統的軟件源是否有對應的 Electron 依賴(通常都有);若是沒有,須要進行編譯。
若是應用是一個重度的 Native 應用,則須要把代碼在對應的指令集和系統依賴上進行編譯,工做量較大。數據庫
服務器,也分爲三種狀況:ubuntu
若是使用的是面向虛擬機的語言,好比 Java 或基於 JVM 的各類語言(Kotlin、Scala 等),則服務不須要進行特殊的適配。通常國產化系統的軟件源中通常都會自帶已實現好的 OpenJDK;若是沒有,參見的指令集通常也都能找到對應的 OpenJDK 開源實現,能夠自行安裝。
近些年出現的一些對 C 庫無強依賴的語言,如 Go 等。編譯體系在設計之初就考慮了多種目標系統和指令集架構,只須要在編譯時指定目標系統和架構便可,如 GOOS=linux GOARCH=arm64 go build,若是使用了 CGO 還須要指定 C/C++ 的編譯器。
若是服務使用的是 C/C++ 等 Native 語言,且對系統 C 庫有強依賴,則須要把代碼在對應的指令集和系統依賴上進行編譯,工做量較大。後端
而上面能夠看出,服務器和桌面端在 Native C/C++ 的適配上相似,而服務器對性能的要求會更爲嚴苛。本文分享的內容主要是服務器 Native C/C++ 如何在多種指令集 CPU 上進行適配,特別是龐大代碼量時如何提升工程效率,大部份內容桌面端也一樣能夠參考。centos
編譯運行漫談瀏覽器
既然咱們要處理的是 Native C/C++ 程序在多種指令集 CPU 的適配,咱們須要先了解下程序是如何編譯和運行的,才能在各個環節藉助各類工具,提升適配的效率。
你們在上計算機課時通常都有了解,C/C++ 源代碼通過預處理、編譯和連接,會生成目標文件。而後計算機將程序從磁盤加載到內存中,便可運行。而這中間其實隱藏了很是多的細節,讓咱們一一來看。
首先,源碼在編譯過程當中,先通過編譯器前端,進行詞法分析、語法分析、類型檢查、中間代碼生成,生成與目標平臺無關的中間表示代碼。而後再交給編譯器後端,進行代碼優化、目標代碼生成、目標代碼優化,生成對應指令集的目標 .o 文件。
GCC 在這個過程當中是先後端都一塊兒處理了,而 Clang/LLVM 則分別對應了前端和後端。由此咱們也能看出,常見的交叉編譯是如何實現的,即編譯器後端對接到不一樣的指令集和體系架構上。
理論上,全部的 C/C++ 程序都應該能經過本地和交叉編譯工具鏈編譯到全部的目標平臺。可是實際工程的時候,還須要考慮到實際使用的編譯工具,如 make、cmake、bazel、ninja 是否已經能支持各類狀況。好比,在本文發佈的時候, Chromium 和 WebRTC 就由於 ninjia 和 gn 工具鏈的問題,是沒有辦法在 Mac ARM64 上編譯自身架構的。
而後,連接器將目標 .o 文件和各類依賴庫,連接到一塊兒,生成可執行可執行文件。
連接過程當中,會根據環境變量,查找對應的庫文件。經過 ldd 命令就能夠看到可執行文件,依賴的庫列表。在適配相同指令集不一樣系統環境的時候,能夠考慮將全部的庫依賴和二進制可執行文件一塊兒拷貝出來做爲編譯輸出。
而最終生成的可執行文件,不管是 Windows 仍是 Linux 平臺,都是 COFF(Common File Format) 格式的變種,Windows 下是 PE(Portable Executable),Linux下是 ELF(Executable Linkable Format)。
事實上,除了可執行文件外,動態連接庫(DDL,Dynamic Linking Library)、靜態連接庫(Static Linking Library) 均採用可執行文件格式存儲。它們在 Window 下均按照 PE-COFF 格式存儲;Linux 下均按照 ELF 格式存儲,只是文件名後綴不一樣而已。
最後,二進制可執行程序在被啓動的時候,系統會加載到一個新的地址空間。這也就意味着,系統會從目標文件讀取頭信息並將程序讀入到地址空間段中,用連接器和加載器加載庫和進行地址空間轉換。而後設置進程各類環境信息和程序參數,最終將程序運行起來,執行程序對應的每條機器指令。
而每一個系統環境的庫和依賴都不盡相同,能夠在經過設置 LD_LIBRARY_PATH 環境變量指定讀取的庫目錄,或者經過 docker 等方案,完整指定一個運行環境。
而計算機在讀取每一條機器指令再執行的過程當中,其實還能夠經過虛擬機的方式進行機器指令的轉譯進行模擬,好比 qemu 能支持多種指令集, Mac rosetta 2 能將 x86_64 高效翻譯爲 arm64 並執行。
適配與工程效率
經過編譯和運行的整個流程分析,咱們能夠在業界找到不少工具,提高適配的效率。
由於追求 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 庫和連接器,隔絕各類環境依賴。
結語
融雲一直專一於 IM 和 RTC 領域,不管在公有云或者私有云市場,咱們都感覺到了市場上對多種 CPU 指令集架構的需求。目前咱們針對公有云 AWS/華爲 ARM CPU 和信創市場全部的 ARM/MIPS CPU 都進行了全功能的適配和優化,對於信創市場各類操做系統、數據庫和中間件也進行了針對性的適配。本文對其中編譯適配工程中用到的技術和工具進行了分析,歡迎你們多多交流。
參考連接
qemu: https://www.qemu.org/
docker buildx:
https://docs.docker.com/build...
patchelf: https://github.com/NixOS/patc...