1. 程式人生 > >Qt高級——D-Bus快速入門

Qt高級——D-Bus快速入門

D-Bus 快速入門

Qt高級——D-Bus快速入門

一、D-Bus簡介

1、D-Bus簡介

D-Bus是Desktop Bus的縮寫,是針對桌面環境優化的IPC(InterProcess Communication)機制,用於進程間的通信或進程與內核的通信。
D-Bus是為Linux系統開發的進程間通信(IPC)和遠程過程調用(RPC)機制,使用統一的通信協議來代替現有的各種IPC解決方案。D-Bus允許系統級進程(如:打印機和硬件驅動服務)和普通用戶進程進行通信。
D-Bus使用一個快速的二進制消息傳遞協議,D-Bus協議的低延遲和低消耗特點適用於同一臺機器的通信。D-Bus的規範目前由freedesktop.org項目定義,可供所有團體使用。

D-Bus不和低層的IPC直接競爭,比如sockets,shared memory或message queues。低層IPC有自己的特點,和D-Bus並不沖突。
與其他重量級的進程間通信技術不同,D-Bus是非事務的。D-Bus使用了狀態以及連接的概念,比UDP等底層消息傳輸協議更“聰明”。但另一方面,D-Bus傳送的是離散消息,與TCP協議將數據看做“流”有所不同。D-Bus支持點對點的消息傳遞以及廣播/訂閱式的通信。

2、不同IPC通信方式比較

不同IPC通信機制的特點如下:
A、CORBA是用於面向對象編程中復雜IPC的一個強大的解決方案。
B、Bonobo是一個只用於GNOME的解決方案,基於CORBA並依賴於GObject。

C、DCOP是一個較輕量級的IPC框架,功能較少,但可以很好地集成到KDE桌面環境中。
D、SOAP和XML-RPC設計用於Web服務,因而使用HTTP作為其傳輸協議。
E、D-BUS設計用於桌面應用程序和OS通信。

3、D-Bus特性

A、D-BUS的協議是低延遲而且低開銷的,設計小巧且高效,以便最小化傳送時間。從設計上避免往返交互並允許異步操作。
B、協議是二進制的,而不是文本,排除序列化過程。
C、考慮了字節序問題。
D、易用性:按照消息而不是字節流來工作,並且自動地處理了許多困難的IPC問題,並且D-Bus庫以可以封裝的方式來設計,開發者可以使用框架裏存在的對象/類型系統,而不用學習一種新的專用於IPC的對象/類型系統。

E、請求時啟動服務以及安全策略。
F、支持多語言(C/C++/Java/C#/Python/Ruby),多平臺(Linux/windows/maemo)。
G、采用C語言,而不是C++。
H、由於基本上不用於internet上的IPC,因此對本地IPC進行了特別優化。
I、提供服務註冊,理論上可以進行無限擴展。

二、D-Bus架構

1、D-Bus架構簡介

技術分享圖片
D-Bus是按一定的層次結構實現的,總體上D-Bus分為三層:
A、接口層——接口層由libdbus庫提供,進程通過libdbus庫使用D-Bus的能力。通過底層庫的接口可以實現兩個進程之間進行連接並發送消息。
B、總線層——由消息總線守護進程(message bus daemon )提供,消息總線守護進程是基於libdbus底層庫的,可以路由消息。消息總線守護進程負責進程間的消息路由和傳遞,其中包括Linux內核和Linux桌面環境的消息傳遞。
C、封裝層——封裝層是一系列基於特定應用程序框架的Wrapper庫,將D-Bus底層接口封裝成方便用戶使用的通用API。

2、D-Bus接口層

libdbus只支持點對點的通信,即只支持一進程與另外的一個進程進行通信。通信是基於消息的,消息包含頭部和消息體。
libdbus提供C語言的底層API,API是為了將D-Bus綁定到特定的對象或是語言而設計的,官方文檔中建議不要在應用上直接使用D-Bus的底層接口,推薦使用D-Bus的綁定,如QtDBus、GDBus、dbus-c++等實現。

3、D-Bus總線層

D-Bus總線層由消息總線守護進程(message bus daemon )提供。消息總線守護進程是一個後臺進程,是/usr/bin/dbus-daemon的一個運行實例,? 負責消息的轉發,dbus-daemon運行時會調用libdus的庫。應用程序調用特定的應用程序框架的Wrapper庫與dbus-daemon進行通信。應用程序通過D-Bus與其它進程通信必須先建立到消息總線守護進程實例的連接。
最常見的基於dbus的程序也符合C/S結構。比如開發兩個程序A和B,其中A是客戶端,B是服務端。假設A要調用B的一個函數func,那麽實際的消息流動方向是:A告訴dbus-daemon請求要調用B的func函數,然後dbus-daemon去調用B的func函數,如果func有返回值的話,B會把返回值告訴dbus-daemon,然後dbus- daemon再把返回值告訴A。如果B進程還沒有啟動,則dbus-daemon會自動的先把B進程啟動起來。
通常情況下,Linux會有兩個dbus-daemon進程,一個屬於system,一個屬於session,在用戶登錄的時候由dbus-launch啟動。
大多數普通程序,都是使用session的dbus-daemon,默認情況下,A就是將消息發給屬於session的dbus-daemon。
dbus-daemon是有地址的,環境變量DBUS_SESSION_BUS_ADDRESS用於表示當前登錄用戶的session的dbus-daemon進程的地址,可以使用echo $DBUS_SESSION_BUS_ADDRESS查看。
技術分享圖片
當用戶登錄進桌面環境的時候,系統啟動腳本會調用到dbus-launch來啟動一個dbus-daemon進程,同時會把啟動的dbus-daemon地址賦予環境變量DBUS_SESSION_BUS_ADDRESS。
一般情況下,不需要考慮DBUS_SESSION_BUS_ADDRESS,但某些時候,單獨啟動一個dbus-daemon有助於程序的調試。
利用dbus-daemon自啟動機制運行的服務進程,都是後臺進程,標準輸出設備已經被重定向,如果B進程有一些調試用的打印信息輸出,則很難直接查看。此時,可以單獨啟動一個dbus-daemon,讓A和B都使用自己啟動的dbus-daemon,此時,dbus-daemon能把B的打印信息顯示出來。
先在終端下啟動一個dbus-daemon,命令如下形式如下:
?????DBUS_VERBOSE=1 dbus-daemon --session --print-address
如此啟動的dbus-daemon會前臺執行,並且打印出地址。
技術分享圖片
然後,在執行A程序的時候,設置環境變量DBUS_SESSION_BUS_ADDRESS為剛才得到的地址值。
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-7MlJMxxGnW,guid=437c0e6060516670cfccacc15afc43c6 ./A
此時運行程序A和B,使用自己啟動的dbus-daemon來轉發消息,並且會把B的打印信息顯示出來。
消息總線守護進程是一個特殊的進程,用於管理系統內的總線,可以將一個進程的消息路由給另外一個進程。如果有很多應用程序連接到消息總線守護進程的總線上,總線能把消息路由到對應的一個或多個進程中去。因此在總線層上,實現了點對點通信的支持,也實現了廣播/訂閱通信方式。
在最底層,D-Bus只支持點對點的通信,一般使用本地套接字(AF_UNIX)在應用和消息總線守護進程之間通信。D-Bus的點對點是經過bus daemon抽象過的,由bus daemon來完成尋址和發送消息,因此每個應用不必關心要把消息發給哪個進程。D-Bus發送消息通常包含如下步驟:
A、應用程序創建和發送消息給消息總線守護進程。
B、消息總線守護進程對收到的消息進行分發處理。
C、目標程序接收到消息,然後根據消息的種類,做不同的響應:確認、應答、忽略。
總線是D-Bus的進程間通信機制,一個系統中通常存在多條總線,總線由D-Bus總線守護進程管理。
最重要的總線為系統總線(System Bus),Linux內核引導時,系統總線就已被裝入內存。只有Linux內核、Linux桌面環境和權限較高的程序才能向系統總線寫入消息,以此保障系統安全性,防止有惡意進程假冒Linux發送消息。
會話總線(Session Buses)由普通進程創建,可同時存在多條。會話總線屬於某個進程私有,用於進程間傳遞消息。

4、D-Bus封裝層

D-Bus封裝層是將libdbus底層API綁定到特定的對象系統或是語言中,將不便使用的libdbus底層API封裝成可以在應用層使用的高級API,如libdbus-glib、libdbus-qt等。
D-Bus在很多不同的編程語言上都有其接口實現。不同語言的接口封裝了D-Bus低級API,提供了更符合編程語言的語法結構。
實現D-Bus接口的語言正在逐漸增加。在C語言中,有最底層的API,但其實現及使用上非常復雜。C語言中另一個實用化的實現基於GLib。在Java、Perl、Python等都有D-Bus接口實現。

三、D-Bus術語

1、D-Bus術語簡介

總線是消息總線守護進程(message bus daemon)的運行實例,每個總線都有一個地址,應用進程就是通過總線地址和相應的總線連接的。總線上的每一個連接都有一個連接名,連接名也稱bus name。每個連接上有至少一個對象,通常有多個對象,對象使用對象路徑唯一標識。對象要實現一個或多個接口,每個接口包含一組方法和信號。

2、總線(Bus)

在D-Bus中,總線(bus)是核心的概念:不同的程序可以通過總線進行某些操作,比如方法調用、發送信號和監聽特定的信號。總線通常有兩種,系統總線(system bus)和用戶總線(session bus),系統總線通常只有一條,用戶總線在用戶登錄時創建。
系統總線是一個持久的總線,在系統啟動時就創建,供系統內核和後臺進程使用,具有較高的安全性。系統總線最常用是發送系統消息,比如:插入一個新的存儲設備、有新的網絡連接等。
會話總線是在某個用戶登錄後啟動,屬於某個用戶私有,是某用戶的應用程序用來通話的通道。在很多嵌入式系統中,只有一個用戶ID登錄運行,因此只有一個會話總線。
一條消息總線就是一個消息路由器,是消息總線守護進程(message bus daemon)的一個實例。

3、地址(Address)

使用d-bus的應用程序既可以是server端也可以是client端,server端監聽到來的連接,client端連接到server端,一旦連接建立,消息就可以流轉。如果使用dbus daemon,所有的應用程序都是client端,dbus daemon監聽所有的連接,應用程序初始化連接到dbus daemon。
每一條總線都有一個地址,進程通過總線的地址連接到總線上。一個D-Bus的地址是指server端用於監聽,client端將要連接的地方,例如unix:path=/tmp/abcedf標識server端將在路徑/tmp/abcedf的UNIX domain socket監聽,client端將要連接到這個地址。地址可以是指定的TCP/IP socket或者其他在或者將在D-Bus協議中定義的傳輸方式。
如果使用bus daemon,libdbus將通過讀取環境變量DBUS_SESSION_BUS_ADDRESS自動獲取session bus damon的地址,通過檢查一個指定的UNIX domain socket路徑獲取system bus的地址。
如果使用D-bus,但不是daemon,需要定義哪個應用是server端,哪個是client端,並定義一套機制用於認可server端的地址。

4、連接名(Bus Name)

總線上的每個連接都有一個或多個名字。當連接建立以後,D-Bus 服務會分配一個不可改變的連接名,稱為唯一連接名(unique connection name),唯一連接名即使在進程結束後也不會再被其他進程所使用。唯一連接名以冒號開頭,如“:34-907”。但唯一連接名總是臨時分配,無法確定,也難以記憶,因此應用可以要求有另外一個名字公共名(well-known name)來對應唯一連接名。例如可以使用“com.mycompany”來映射“:34-907”。應用程序可能會要求擁有額外的公共名(well-known name)。例如,可以寫一個規範來定義一個名字叫做 com.mycompany.TextEditor。協議可以指定自己擁有名字為com.mycompany.TextEditor的連接,一個路徑為/com/mycompany/TextFileManager的對象,對象擁有接口org.freedesktop.FileHandler。應用程序就可以發送消息到總線上的連接名字,對象和接口以執行方法調用。
連接名可以用於跟蹤應用程序的生命周期。當應用退出(或者崩潰)時,與總線的連接將被OS內核關掉,總線將會發送通知,告訴剩余的應用程序。

5、對象和對象路徑(Object and Object Path)

D-Bus的對象和面向對象語言中的對象含義是不同的,D-Bus的對象表示的是D-Bus通道中信息流向的端點。對象由客戶進程創建,並在連接進程中保持不變。
所有使用D-BUS的應用程序都包含一些對象, 當經由一個D-BUS連接收到一條消息時,消息是被發往一個對象而不是整個應用程序。應用程序框架中定義了這樣的對象,如GObject,QObject等,在D-Bus中稱為原生對象(native object)。
對於底層的D-Bus協議,即libdbus API,並不理會原生對象,使用對象路徑(object path)的概念。通過對象路徑,高層API接口可以綁定到對象,允許遠程應用指向對象。對象路徑如同文件系統路徑,例如一個對象可能叫做“/org/kde/kspread/sheets/3/cells/4/5”。
對象路徑在全局(session或者system)是唯一的,用於消息的路由。

6、接口(Interface)

每一個對象支持一個或者多個接口,接口是一組方法和信號的集和,接口定義一個對象實體的類型。D-Bus對接口的命名方式,類似org.freedesktop.Introspectable。開發人員通常使用編程語言名字作為接口名字。

7、方法(Methods)

每一個對象有兩類成員:方法和信號。方法是一段函數代碼,帶有輸入和輸出;信號是廣播給所有興趣的其他實體,信號可以帶有數據payload。
客戶向某對象發送一個請求,即對象被請求執行一個明確的、有名稱的動作。如果客戶請求執行一個目標對象未提供的方法,將會產生一個錯誤。方法的定義可以支持輸入參數。對於每個請求,都有一個包含請求結果以及結果數據(輸出參數)的響應返回給請求者。當請求無法完成時,響應中將包含異常信息,其中至少有異常名稱以及錯誤信息。
大多數語言都將這些封裝在自身的語言機制中,比如將參數包裝進消息包,將異常信息轉換成語言自身的異常等等。在這些實現中,向遠程對象傳遞一個字符串參數就好像是在本地執行一個字符串參數的函數一樣簡單。此時不再需要數據類型轉換、數據復制等繁瑣工作,語言本身封裝了一切底層實現。

8、信號(Signals)

信號依然遵從面向對象概念,信號是從對象發出但沒有特定目的地址的單向數據廣播。客戶進程可以預先註冊其感興趣的信號,如特定名稱的信號或從某個對象發出的信號等。當對象發出信號後,所有訂閱了該信號的客戶進程將收到此信號的復本。接收端可能有多種情況出現,或者有一個客戶進程,或者有多個客戶進程,或者根本沒有客戶進程對這個信號感興趣。對於信號來說沒有響應消息,發出信號的對象不會知道是不是有客戶進程在接收,有多少客戶進程接收,以及從客戶進程收到任何反饋。
信號可以有參數。但信號是單向通信,因此不可能像方法一樣具有輸入輸出參數。D-Bus允許客戶進程通過參數比對過濾其需要的信號。
信號一般用來廣播一些客戶可能會感興趣的事件,比如某個其它的客戶進程與總線的連接斷開等。這些信號來自總線對象,因此從信號中客戶進程可以分辨斷線是由於正常退出、被殺掉或者程序崩潰。

9、代理(Proxies)

代理對象用來表示其他的remote object。當觸發了proxy對象的method時,將會在D-Bus上發送一個method_call的消息,並等待答復,根據答復返回。
總線上的對象一般通過代理來訪問。總線上的對象位於客戶進程以外,而客戶可以調用本地接口與對象通信,此時,本地接口充當了代理的角色。當觸發了代理對象的方法時,將會在D-Bus上發送一個method_call的消息,並等待答復返回,就象使用一個本地對象一樣。
一些語言的代理支持“斷線重連”。比如所連接的對象在某段時間裏暫時斷開了與總線的連接,代理會自動重連到相同的連接名並重新找到對象,程序甚至不會知道目標對象有段時間不可用。並不是所有的語言都支持這一特性,在GLib中的兩種代理中的一種支持。
比如不用代理時的代碼如下:

Message message = new Message("/remote/object/path", "MethodName", arg1, arg2); 
Connection connection = getBusConnection();           
connection.send(message);           
Message reply = connection.waitForReply(message);           
if (reply.isError()) {                         
}
 else {              
Object returnValue = reply.getReturnValue();           
} 

采用代理時對應的代碼則是:

Proxy proxy = new Proxy(getBusConnection(), "/remote/object/path");           
Object returnValue = proxy.MethodName(arg1, arg2);

10、服務

服務是 D-BUS 的最高層次抽象,服務的實現當前還在不斷發展變化。應用程序可以通過一個總線來註冊一個服務,如果成功,則應用程序就已經獲得了服務。其他應用程序可以檢查在總線上是否已經存在一個特定的服務,如果沒有可以要求總線啟動它。

四、消息和消息總線

1、消息簡介

D-Bus通信機制是通過進程間發送消息實現的,最基本的D-Bus協議是一對一的通信協議。與socket通信不同,D-Bus是面向消息的協議。但如果使用高層的綁定接口,不會直接接觸到D-Bus的消息。
D-Bus 有四種類型的消息:
A、method_call方法調用
B、method_return方法返回
C、error錯誤
D、signal信號
代理中的遠程對象調用涉及到了消息總線以及method_call和method_return兩類消息。
消息有消息頭(header)和消息體(body)。消息頭包含消息體的路由信息,消息體是凈荷,通常包含的是參數。消息頭通常包含發送進程的連接名(Bus Name)、方法或者信號名等等,其中有一字段是用於描述消息體中的參數的類型的,例如“i”標識32位整數,“ii”表示2個32位整數。

2、調用method的流程

進程A要調用進程B的一個method,進程A發送method_call消息到進程B,進程B回復method_return消息。在發送消息時,發送方會在消息中添加不同的序列號,同樣,回復消息中也會含有序列號,以便對應。
調用method的流程如下:
A、在發送method_call消息時,如果使用了代理,進程A要調用進程B的某方法,不用構造method_call消息,只需調用代理的本地方法,代理會自動生成method_call消息發送到消息總線上。
B、如果使用底層API,進程A需要構造一個method_call消息。method_call消息包含對應進程B的連接名、方法名、方法所需參數、進程B中的對象路徑和進程B中聲明此方法的接口。
C、將method_call消息發送到消息總線上。
D、信息總線檢查消息頭中的目的連接名,當找到一個進程與此連接名對應時發送消息到該進程。當找不到一個進程與此連接名對應時,返回給進程A一個error消息。
E、進程B解析消息,如果是采用底層API方式,則直接調用方法,然後發宋應答消息到消息總線。如果是D-BUs高級API接口,會先檢測對象路徑、接口、方法名稱,然後把消息轉換成對應的本地對象(如GObject,QT中的QObject等)的方法,調用本地對象方法後再將應答結果轉換成應答消息發給消息總線。
F、消息總線接收到method_return消息,將把method_return消息直接發給發出調用消息的進程。
消息總線不會對總線上的消息進行重排序。如果發送了兩條消息到同一個進程,將按照發送順序接收到。接收進程不需要按照順序發出應答消息,例如在多線程中處理這些消息,應答消息的發出是沒有順序的。消息都有一個序列號可以與應答消息進行配對。

3、發送signal的流程

發送信號是單向廣播的,信號的發送者不知道對信號作響應的有哪些進程,所以信號發送是沒有返回值的。信號接收者通過向消息總線註冊匹配規則來表示對某信號感興趣,而匹配規則通常包含信號的發出者和信號名。
信號發送的流程如下:
A、當使用dbus底層接口時,信號需要應用進程自己創建和發送到消息總線;使用dbus高層API接口時,可以使用相關對象進行發送。信號消息包含有聲明信號的接口名、信號名、所在進程對應的連接名和相關參數。
B、連接到消息總線上的進程如果對某個信號感興趣,則註冊相應的匹配規則。消息總線保持有匹配規則表。
C、消息總線根據匹配規則表,將信號發送到對信號感興趣的進程。
D、每個進程收到信號後,如果使用dbus高級API接口,可以選擇觸發代理對象上的信號。如果使用dbus底層接口,需要檢查發送者名稱和信號名稱,然後決定怎麽做。

4、DBus工具

D-Bus提供了兩個小工具:dbus-send 和dbus-monitor。可以用dbus-send發送消息,用dbus-monitor監視通道上流動的消息。
dbus-send
用於發送一個消息到消息通道上,使用格式如下:

dbus-send [--system | --session] --type=TYPE --print-reply --dest=連接名對象路徑接口名.方法名參數類型:參數值參數類型:參數值
dbus-send --session --type=method_call --print-reply --dest=連接名對象路徑接口名.方法名 參數類型:參數值 參數類型:參數值

dbus-send支持的參數類型包括:string, int32, uint32, double, byte, boolean。
dbus-monitor
用於打印消息通道上的消息,使用格式如下:

dbus-monitor [--system | --session | --address ADDRESS] [--profile | --monitor] [watch expressions]
dbus-monitor "type=‘signal‘, sender=‘org.gnome.TypingMonitor‘, interface=‘org.gnome.TypingMonitor‘"

5、消息總線上的方法和信號

消息總線是一個特殊的應用,主要關於消息總線上的方法和信號。
A、Introspection
消息總線上有一個接口org.freedesktop.DBus.Introspectable,接口中聲明了一個方法Introspect,不帶參數,將返回一個XML string,XML字符串描述接口、方法、信號。
B、消息總線上的方法和信號
可以通過向名稱為“org.freedesktop.DBus”的連接上的對象“/”發送消息來調用消息總線提供的方法。消息總線對象支持標準接口"org.freedesktop.DBus.Introspectable",可以調用org.freedesktop.DBus.Introspectable.Introspect方法查看消息總線對象支持的接口。
dbus-send --session --type=method_call --print-reply --dest=org.freedesktop.DBus / org.freedesktop.DBus.Introspectable.Introspect
用戶總線對象支持標準接口“org.freedesktop.DBus.Introspectable”和接口 “org.freedesktop.DBus”。接口“org.freedesktop.DBus”有18個方法和3個信號。

<interface name="org.freedesktop.DBus">
    <method name="Hello">
      <arg direction="out" type="s"/>
    </method>
    <method name="RequestName">
      <arg direction="in" type="s"/>
      <arg direction="in" type="u"/>
      <arg direction="out" type="u"/>
    </method>
    <method name="ReleaseName">
      <arg direction="in" type="s"/>
      <arg direction="out" type="u"/>
    </method>
    <method name="StartServiceByName">
      <arg direction="in" type="s"/>
      <arg direction="in" type="u"/>
      <arg direction="out" type="u"/>
    </method>
    <method name="UpdateActivationEnvironment">
      <arg direction="in" type="a{ss}"/>
    </method>
    <method name="NameHasOwner">
      <arg direction="in" type="s"/>
      <arg direction="out" type="b"/>
    </method>
    <method name="ListNames">
      <arg direction="out" type="as"/>
    </method>
    <method name="ListActivatableNames">
      <arg direction="out" type="as"/>
    </method>
    <method name="AddMatch">
      <arg direction="in" type="s"/>
    </method>
    <method name="RemoveMatch">
      <arg direction="in" type="s"/>
    </method>
    <method name="GetNameOwner">
      <arg direction="in" type="s"/>
      <arg direction="out" type="s"/>
    </method>
    <method name="ListQueuedOwners">
      <arg direction="in" type="s"/>
      <arg direction="out" type="as"/>
    </method>
    <method name="GetConnectionUnixUser">
      <arg direction="in" type="s"/>
      <arg direction="out" type="u"/>
    </method>
    <method name="GetConnectionUnixProcessID">
      <arg direction="in" type="s"/>
      <arg direction="out" type="u"/>
    </method>
    <method name="GetAdtAuditSessionData">
      <arg direction="in" type="s"/>
      <arg direction="out" type="ay"/>
    </method>
    <method name="GetConnectionSELinuxSecurityContext">
      <arg direction="in" type="s"/>
      <arg direction="out" type="ay"/>
    </method>
    <method name="ReloadConfig">
    </method>
    <method name="GetId">
      <arg direction="out" type="s"/>
    </method>
    <signal name="NameOwnerChanged">
      <arg type="s"/>
      <arg type="s"/>
      <arg type="s"/>
    </signal>
    <signal name="NameLost">
      <arg type="s"/>
    </signal>
    <signal name="NameAcquired">
      <arg type="s"/>
    </signal>
  </interface>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg direction="out" type="s"/>
    </method>
  </interface>

Qt高級——D-Bus快速入門