開發與維運

如何在RTOS上全量支持C++11

1、概述

1.1、 C++語言的特點

C語言自誕生以來已被廣泛應用於系統和應用開發。比如Google的微內核操作系統fuchsia就是用C實現的,ARM的嵌入式操作系統ARMmbed也主要基於C實現。在應用開發方面,C被廣泛用於GUI、遊戲引擎、圖形引擎、瀏覽器引擎、數據庫等的開發。

C++語言的廣泛使用,得益於其如下特點:
(1)支持面向對象編程,封裝、繼承、多態等機制使編程更加高效。
(2)兼容C,支持面向過程編程及驅動開發。
(3)標準庫支持豐富的文件和數據結構操作。
(4)性能優異。

1.2、C++在嵌入式RTOS上的應用

隨著MCU芯片處理能力的增強,嵌入式設備的圖形顯示、模式識別、腳本解析等能力不斷賦能設備拓展應用邊界。同時,隨著應用的不斷拓展,對嵌入式設備提出了更高的要求。通常,嵌入式系統採用C語言開發,但GUI、AI算法等複雜應用採用C開發,為此在RTOS上支持C語言的需求變得越來越強烈。

有些RTOS封裝系統接口為上層應用提供了自定義的C類,但由於這些類不符合C標準,基於這些類開發的應用缺乏可移植性。另一方面,當使用外部開源軟件時,需要進行適配,若軟件比較複雜,適配工作量比較大,更為災難性的是,自定義的類由於不夠全,往往很難滿足上層軟件的需要。所以,最可行的方法還是要支持標準C++庫。

image.png

本文主要闡述了基於GCC工具鏈在RTOS上支持C++的兩個關鍵部分:

RTOS上對C++初始化的支持;
RTOS上適配C++庫;
說明:本篇文章基於物聯網操作系統AliOS Things上C++11實踐總結而成,已在智能音箱等場景中應用。

2、C++初始化支持

對C語言而言,只能用常量或常量表達式初始化全局變量,比如不允許調用函數初始化,也不允許用另一個全局變量初始化。也即是說,全局變量的值在編譯時就確定了。另外對於全局數組,其長度在編譯時也確定了。

編譯器將未賦初值的全局變量放在bss段,有初始值的全局變量放在data段(只讀數據放到rodata段)。當RTOS啟動時,bss段全部清為0,從程序鏡像中讀取data段的內容並寫入到對應data段的內存,這樣就完成了所有全局變量的初始化。

引入C++後,有了對象的概念,這個時候RTOS啟動過程中的初始化就不再是清內存、拷貝內存那麼簡單了。對象內部的空間需要調用new分配,比如虛函數表、一些容器的內部存儲空間。同時,對象初始化過程中需要調用父類的構造函數。這些都無法在編譯時確定。

C處理這個問題的辦法是:把所有C源文件中需要在初始化時調用的函數的地址集中放到一個表中,RTOS在初始化時遍歷該函數表並調

image.png
RTOS啟動時,初始化C++對象的偽代碼如下:

for (f = __ctors_start__; f < __ctors_end__; f++) {

    (*f)();

}

3、在RTOS上適配C++庫

在RTOS上實現對C的初始化支持後,下載一個芯片廠商提供的工具鏈,配置一下編譯選項,C似乎可以跑起來,但其實存在諸多問題。

芯片廠商提供的要麼是基於linux的工具鏈,要麼是基於裸機的工具鏈(bare-metal)。在RTOS上顯然只能選bare-metal工具鏈。所謂bare-metal,其含義是無操作系統平臺,其線程模式為single,即無多線程併發。在這種模式下,不支持C的多線程,比如不支持mutex、thread、condition_variable等類,同時C庫內部實現中不考慮多線程互斥。所以這種模式下的工具鏈用在RTOS上,一方面功能不全,另一方面存在穩定性隱患,尤其在多核平臺上多線程併發問題將變得嚴重。因此,為了真正實現對C++11的全量支持,需針對RTOS進行適配。

3.1、C++庫的依賴關係

GCC工具鏈中集成了一個C++庫,其依賴關係如下:
image.png

上圖中三個依賴部分說明如下:

C把C庫中的符號導入到std命名空間提供給上層應用使用,同時C內部機制的實現也有賴於C庫,比如輸出流依賴於puts、putchar等接口。當然,C庫也要實現多線程支持,這個有機會另外開闢一篇進行闡述,這裡就不展開了。
libgcc庫提供了一些較底層的接口與機制,比如C++的線程變量基於libgcc庫提供的線程變量管理機制。
C適配層實現了C與RTOS的對接,這部分因底層OS的不同而不同。所以,ARM等芯片廠商或編譯器廠商提供的針對嵌入式的工具鏈往往是裸機平臺的,因為RTOS數量眾多,無法一一滿足。針對特定RTOS的多線程版本工具鏈需RTOS廠商自己定製。

3.2、適配

適配主要涉及類型與接口兩部分,具體可參考./gcc/libgcc/gthr.h文件。

3.2.1、適配的接口說明如下:

image.png

3.2.2、適配的接口如下
image.png

3.2.3、適配說明

用typedef把C內部類型定義為RTOS的類型,基於RTOS的接口實現上述適配接口,便完成了C庫的適配。如果RTOS上已經完成了對posix接口的支持,那麼適配就比較方便了。示例如下:

//__gthread_t類型定義

typedef pthread_t __gthread_t;



//__gthread_create接口實現

static inline int

__gthread_create (__gthread_t *__threadid,  void *(*__func) (void*), void *__args)

{
    return pthread_create (__threadid, NULL, __func, __args);
}

3.3、配置與重編C++庫

在編譯C++庫時,需配置為使能多線程模式,主要配置項如下:
--enable-threads=posix

其實使能該選項只是觸發了配置腳本檢查是否支持多線程,若配置腳本執行過程中判斷系統不支持多線程,最終編譯出來的庫還是單線程的。比如,配置腳本中對__GTHREADS_CXX0X宏是否定義進行了判斷,若該宏未定義則使能多線程失敗。

完成適配與配置後,重編工具鏈即可生成多線程版本的C庫。編譯完成後可查看cconfig.h文件,確認使能的C++特性。以_GLIBCXX_USE_SCHED_YIELD宏為例,若沒有生成該宏,那麼thread類yield()函數實現為空函數、

4、後記

得益於C++良好的封裝機制,用C++寫的代碼比用C寫的代碼bug率低很多。但硬幣的另一面是,調試難度增加了。

即便開啟了編譯優化,C語言的一行語句與彙編的對應關係也相對比較清楚。但C++由於其複雜的機制,一行簡單的賦值語句往往會對應十幾條、甚至幾十條彙編。這需要RTOS的維測能力提供高效的調試支持。

Leave a Reply

Your email address will not be published. Required fields are marked *