鴻蒙子系統解讀-分布式任務排程篇
本文作者:江蘇潤和軟體股份有限公司 郎建中
1.總體描述
1.1.總體介紹
分布式任務排程基于分布式軟總線、分布式資料管理、分布式Profile等技術特性,建構統一的分布式服務管理(發現、同步、注冊、調用)機制,支援對跨裝置的應用進行遠端啟動、遠端調用、遠端連接配接以及遷移等操作,能夠根據不同裝置的能力、位置、業務運作狀态、資源使用情況,以及使用者的習慣和意圖,選擇合适的裝置運作分布式任務。
下圖是分布式排程子系統在整個鴻蒙系統中的位置:
下圖表示分布式排程的示意圖:
從A裝置的某個FA(Feature Ability代表有界面的元能力)應用調用裝置B上的FA應用。這裡的調用的含義包含了:
a、啟動和關閉:啟動和關閉遠端裝置上的ability(包括:基于Page的ability、基于Service的ability、基于Data模闆的ability)
b、連接配接和斷開:向開發者提供跨裝置控制服務的能力。這裡的服務表示:基于Server和Data模闆的ability。
c、遷移能力:向開發者提供跨裝置的業務無縫遷移能力。開發者可以通過基于Page的ability的遷移接口,将本地的業務遷移到指定的裝置中。
1.2.分布式排程中的兩種裝置
在分布式排程中,存在兩個角色。按照上圖有裝置A和裝置B。一般來說裝置A是指智慧屏裝置,裝置B隻一般的輕量裝置。智慧屏裝置一般指智能TV、手機等。輕量裝置一般隻Camera、手表等
下面圖示表示這兩種裝置的系統架構圖:
(限制:如果要實作分布式排程,目前智慧屏裝置和輕量裝置必須處于同一個區域網路段内)
從鴻蒙系統的整體系統架構圖可以看出,分布式排程子系統及周邊的依賴子產品如下圖:
1.3.分布式排程代碼示例–啟動遠端FA
1.3.1.智慧屏上的代碼示例
1.擷取目标線上從裝置的裝置ID
// 引入裝置選擇頭檔案import ohos.distributedschedule.interwork.DeviceInfo;import ohos.distributedschedule.interwork.DeviceManager;// 擷取線上裝置清單List<DeviceInfo> deviceInfoListOnline = DeviceManager.getDmsDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);String remote_device_id;if (deviceInfoListOnline.size() > 0){
remote_device_id = deviceInfoListOnline[0].GetDeviceId(); // 擷取線上清單中第一台裝置的裝置ID}12345678910111213
2.構造want,首先使用ElementName類表明需要啟動的遠端裝置ID,包名,元能力類名,傳入want中,然後設定want中的分布式标志位Want.FLAG_ABILITYSLICE_MULTI_DEVICE表示需要遠端啟動
// 引入相關頭檔案import ohos.aafwk.ability.Ability;import ohos.aafwk.content.Want;import ohos.bundle.ElementName;// 啟動遠端裝置FAWant want = new Want(); // 封裝啟動遠端FA的Want// 使用步驟2中擷取的裝置ID,并指定FA資訊ElementName name = new ElementName(remote_device_id, "com.huawei.remote_package_name", "remote_class_name"); want.setElement(name); // 将待啟動的FA資訊添加到Want中want.setFlags(Want.FLAG_ABILITYSLICE_MULTI_DEVICE); // 設定分布式标記,若不設定将無法使用分布式能力startAbility(want); // 按照Want啟動指定FA,Want參數命名以實際開發平台API為準123456789101112131415
1.3.2.輕量裝置上的代碼示例
輕量裝置上代碼可以參考鴻蒙Java的API參考手冊中,如何建立基于Page的Ability。
https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ability-page-concepts-0000000000033573
2.代碼目錄結構
分布式排程的代碼在foundation/distributedschedule目錄中,目錄結構如下:
其中interfaces中包含了所有的頭檔案,如下:
Services目錄下包含了如下目錄:
其中,dtbschedmgr_lite目錄是輕量級分布式排程子產品代碼。
safwk_lite目錄中是foundation 這個bin檔案的main函數,用于samgr啟動,初始化所有注冊的Service。
samgr_lite目錄是系統服務架構子系統代碼,這個目錄是系統的基礎系統服務架構代碼,分布式排程子系統也是一個系統服務,将會注冊在samgr裡面。并且依賴系統服務架構進行服務的釋出,注冊等功能。
3.代碼分析
3.1.輕量裝置子系統代碼分析
輕量裝置分布式排程子系統的源碼主要是以下的7個檔案:
1.distributed_schedule_service.c 分布式排程對外接口
2.dmslite.c 分布式排程服務實作
3.dmslite_check_remote_permission.c 分布式排程權限管理子產品
4.dmslite_famgr.c 分布式排程FA管理子產品
5.dmslite_msg_parser.c 分布式消息解析子產品
6.dmslite_session.c 跨裝置通信收發子產品
7.dmslite_tlv_common.c TLV格式資料解析子產品
下面我們将分三個過程來分析源碼:1、分布式排程服務的初始化。2、協定封包的接收和解析。3、輕量裝置端拉起FA。
3.1.1.分布式排程服務初始化
分布式排程的服務和特性定義和初始化在distributed_schedule_service.c和dmslite.c檔案中。
1.服務和特性的的定義和注冊
服務的定義和初始化
服務的定義和初始化在distributed_schedule_service.c中。下面的代碼定義了全局唯一的服務對象(用C語言實作了C++類的概念)
g_distributedService 就是全局唯一的服務對象結構體,而DistributedService定義繼承了INHERIT_SERVICE,這是所有服務都必須繼承的。
從上面的代碼可以看出,所有的服務都必須要有4個成員。
GetName()成員就是讓samgr可以得到這個服務的名稱。
Initialize()成員就是服務的初始化過程,在samgr的SAMGR_Bootstrap()函數中會調用所有注冊服務的初始化過程。
MessageHandle()成員是服務對外的消息處理函數。
GetTaskConfig()成員是向samgr上報服務的基本配置,包括:level,priority, stackSize,queueSize,taskFlag。
最後我們在distributed_schedule_service.c檔案中看到如下的初始化定義:
上面的這段代碼首先用SYS_SERVICE_INIT宏定義了分布式排程服務的初始化函數。這個函數被寫入zinitcall這個資料段中(或者通過__attribute__((constructor)) 定義在bin檔案的初始化過程中)。是以由SYS_SERVICE_INIT宏定義的函數都會在main函數之前被執行。
是以,在main()函數執行前,Init()(注意是distributed_schedule_service.c中的)會先被執行。我們看到在Init函數中,調用了SAMGR_GetInstance()->RegisterService((Service *)&g_distributedService); 注冊了分布式排程子系統服務。下面我們在服務的初始化中介紹這個函數調用。
特性的定義和初始化
特性的定義在dmslite.c 中。如下代碼:
g_dmslite是全局唯一的特性對象。DmsLite的定義如下:
INHERIT_FEATURE宏定義了所有的特性都要有的成員(也就是C++中繼承的概念)
GetName:傳回特性的字元串名稱。
OnInitialize:特性的初始化函數。下面的初始化流程中有介紹。
OnStop:特性終止時調用。
OnMessage:特性的消息處理函數。使用者可以通過IUnknown接口發送消息。
在dmslite.c中有如下的特性初始化定義:
SYS_FEATURE_INIT宏與SYS_SERVICE_INIT宏類似,會在main函數調用前被調用。這個宏用來注冊特性的初始化函數入口Init(注意:是dmslite.c中的Init()函數)。
Init()函數調用SAMGR_GetInstance()取得系統服務架構子系統的全局唯一對象,然後調用RegisterFeature來注冊Feature和FeatureApi。
2.服務的初始化
這個分布式排程子系統的初始化過程是在系統服務架構子系統中完成。上面說過在foundation/distributedschedule/services目錄下有三個子目錄,分别是samgr_lite、safwk_lite、dtbschedmgr_lite。其中safwk_lite目錄中隻包含了一個main.c檔案,這個檔案就是系統服務架構子系統的主入口。而samgr_lit和dtbschedmgr_lite目錄下的源碼将分别編譯出庫檔案,然後跟safwk_lite下的main.c一起編譯成foundation 這個bin檔案。
我們先來看一下main.c這個檔案。
從上面的代碼可以看到,main()函數調用了 SAMGR_Bootstrap()後,進入了一個死循環。這個函數定義在samgr_lite/samgr/source/samgr_lite.c中,我們來看看這個函數。
這個函數的實作大體上看分為3個部分:
擷取全局唯一的samgr系統服務對象
g_samgrImpl 是samgr的全局唯一對象,我們來看看這個對象這麼定義和初始化的。
從上面的初始化函數Init可以看到,g_samgrImpl被指派了許多函數指針,其中RegisterService就是給其他服務調用注冊服務用的,而RegisterFeature、RegisterFeatureApi就是給服務注冊特性用的。
那這個Init()什麼時候被調用呢?經過搜尋,我們發現是在SAMGR_GetInstance()函數中被調用,而前面我們說的分布式排程初始化過程的Init()函數中就會調用這個函數。我們看看這個函數的實作。
這個函數首先通過判斷 g_samgrImpl.mutex 這個變量是否初始化過。如果沒有,那麼調用Init來初始化g_samgrImpl這個全局對象,然後傳回g_samgrImpl這個全局對象中的函數指針表,這個指針表中就包含了注冊服務、注冊特性等函數。(這裡有個疑問:這個函數一會直接使用g_samgrImpl全局變量,一會使用GetImplement()函數間接的使用這個全局變量,不知道為什麼?)
我們的分布式排程子系統的服務注冊初始化函數Init(注意是distributed_schedule_service.c中的)在調用了SAMGR_GetInstance()後,得到的函數指針表,然後調用了RegisterService注冊服務對象,我們來看看RegisterService()幹什麼了。
我們看到RegisterService()函數大緻也分為三個部分:
A、 檢視需要注冊的service是否已經注冊過了。
B、 根據要注冊的sevice對象構造一個serviceImpl對象。serviceImpl對象的結構定義如下:
上面我們可以看出:
service:就是我們自己的服務對象,也就是分布式排程的全局唯一的服務對象g_distributedService(前面有過介紹)
defaultApi: 是預設的IUnknown對象指針
taskPool: 是這個服務的任務池對象
features: 是這個服務的特性清單,使用SYS_FEATURE_INIT宏可以初始化一個服務的特性
serviceId: 這個應該是服務的編号
ops: 這個應該是服務的操作消息結構
C、 将serviceImpl對象加入到系統g_samgrImpl的services清單中。
将注冊的服務加入到臨時變量 initServices 這個Vector類型的變量中
這個部分就是把前面說的g_samrImpl中的services清單中的服務都加入到InitServices中。
根據initServices中的服務,初始化所有的注冊服務,這裡也包含了我們的分布式排程服務
這一步就是調用InitializeAllServices(&initServices)函數,我們來看下這個函數的實作。
這個函數大緻分為兩個部分:
A、輪詢每個注冊的服務,初始化taskPool,并且調用InitializeSingleService()函數。
我們看下InitializeSingleService()函數的調用過程:
最終調用到了DEFAULT_Initialize()函數。這個函數先執行impl->service->Initialize(impl->service, id);這裡的impl->service->Initialize就是我們的分布式排程子系統服務對象裡面定義的Initialize函數,前面有過介紹。
DEFAULT_Initialize()函數中下面的紅框中的代碼就是為注冊的Features進行初始化的地方。這裡會調用Feature注冊的OnInitialize()函數(dmslite.c中),這個函數的實作如下:
這裡PublicService是在分布式軟總線(./foundation/communication/services/softbus_lite/discovery/discovery_service/source/discovery_service.c)中實作的函數,功能是在軟總線中釋出。
g_publishInfo全局變量是釋出的資訊:
g_publishCallback全局變量是釋出的回調函數結構體:
我們看一下OnPublishSuccess這個函數,是在釋出成功後的回調。
釋出成功後調用RegisterTcpCallback來建立一個TCP的Session會話伺服器,用于解釋TCP封包。這裡的g_sessionCallback定義如下:
從結構體的定義可以看出:
onBytesReceived:就是當tcp消息到達後用于分析消息的函數。
OnSessionOpened:就是在遠端Session打開後(也就是智慧屏端連接配接啟動一個Session會話後的初始化函數)。
OnSessionClosed:就是會話關閉後的處理函數。
B、輪詢每個注冊的服務,啟動taskPool。
總結:
1、分布式排程初始化過程代碼在:distributed_schedule_service.c、dmslite.c中
2、涉及子產品:
a) softbus_lite:./foundation/communication/services/softbus_lite
3、分布式排程的初始化過程時序圖:
3.1.2.協定封包的接受和解析
前面在初始化過程中,我們介紹過在輕量裝置分布式排程的Feature初始化後,會在軟總線釋出。釋出成功後,調用軟總線的CreateSessionServer()注冊協定處理的回調函數。回調函數總共有3個,分别如下:
我們先看一下OnSessionOpened()和OnSessionClosed(),代碼如下:
上面代碼顯示在會話打開和關閉的時候,基本什麼都沒做,隻是列印了log。這裡的會話我了解是智慧屏端裝置通過軟總線與輕量裝置建立的會話。
下面我們分析下最核心的函數OnBytesReceived()。這個函數應該是在會話有資料傳輸後被軟總線回調的。
在OnBytesReceived()函數中,先用參數data和 dataLen構造了臨時對象interInfo。這個對象中持有了軟總線接收到的會話資料和長度,然後調用了DmsLiteProcessCommuMsg() 函數處理會話資料,并且設定了回調函數。回調函數如下定義:
這裡的onStartAbilityDone()應該就是完成拉起上層應用的FA後的回調。在下一節中我們詳細介紹。
我們繼續看DmsLiteProcessCommuMsg()函數的實作,如下:
我們把DmsLiteProcessCommuMsg()函數的實作分為三部分:
A、進行參數校驗和程序校驗。CanCall是使用者校驗,通過getuid()函數取得目前使用者ID,并判斷是否是合法的使用者。這裡應該是一個初步的安全校驗。
B、對會話封包資料進行解碼
這部分代碼主要是先通過調用DecodeDmsTlv()函數解析會話封包資料,然後調用Feature的回調函數onTlvParseDone()(前面我們看到這個回調并沒有設定,是以這裡不會調用)。
DecodeDmsTlv()函數總共3個參數,前兩個參數是會話封包的資料和長度,最後一個是輸出結構體TlsDmsMsgInfo,定義如下:
commandId:智慧屏發送給輕量裝置的指令字,例如:DMS_MSG_CMD_START_FA(目前的代碼好像隻定義了這一個指令字,後續應該會擴充)
calleeBundleName:輕量裝置端接收指令的包名,也就是要拉起的FA的包名
calleeAbilityName:輕量裝置端接收指令的Ability名,也就是要拉起FA後顯示的Ability的名字
callerSignature:智慧屏裝置端發起者的簽名,用于安全檢查
我們再來分析下DecodeDmsTlv()函數的實作,如下:
①調用TlvBytesToNode()将封包轉化為Node資料格式,并且傳回Node的頭指針。這裡不做詳細代碼分析了,有興趣的同學可以自己研究下協定解析的代碼。
②調用ReadTlvNode()讀取Node的資料到輸出結構dmsMsg。這裡不做詳細代碼分析了,有興趣的同學可以自己研究下協定解析的代碼。
C、根據解碼得到的指令字,執行相應的動作。這裡從代碼上看隻支援一個動作:START_FA。下一節詳細介紹拉起FA的過程。
1、會話資料的處理和協定的解析處理在dmslite_session.c,dmslite_msg_parser.c,dmslite_tlv_common.c中。
a)Softbus_lite:./foundation/communication/services/softbus_lite
3、會話資料處理和協定解析的時序圖如下: