Dbus-glib使用心得
一、背景介紹
從安全的角度考慮,廣泛的使用DBUS進行進程間通訊。
1.優點:
DBUS總線分爲系統總線與會話總線兩類,兩者之前不能互相通信,所以任何應用程序不能欺騙系統事件,安全性很好。
2.缺點
l 直接使用Dbus標準接口調用很繁瑣,各服務之間各寫一套,不易維護也容易出錯。
l 接受方法調用端、消息接收端等程序需要非阻塞式(阻塞式的無法多線程DBUS通訊)判斷是否接收到DBUS信息,形如:
While(1)
{
dbus_connection_read_write();
msg = dbus_connection_borrow_message(conn);
if (NULL == msg) {
usleep(xxx);
continue;
}
…
}
如上所示,多個服務同時運行的情況下,會佔用大量CPU時間片,之前就有測試報告應用程序壓力運行單一操作的情況下,應用程序會由快跑慢。
因此需要一個穩定可靠的DBUS調用封裝,上層統一該封裝接口進行DBUS通訊。
二、Dbus-glib介紹
Dbus-glib是GNU標準庫,在Dbus接口上封裝,方便上層服務與應用更好的使用。其形如一個DBUS代理服務器,由它進行所有DBUS消息的遍歷與轉發,服務端與消息發送端只需要向DBUS deamon申請註冊唯一的DBUS name 、綁定GOBJECT後,DBUS deamon就會將申請連到到該DBUS name的DBUS信息轉發給指定應用。
直接調用DBUS接口的構圖如下:
使用Dbus-glib結構圖如下:
l 函數調用流程:
服務端申請一個GObject,綁定以下信息:
Dbus name:A,
Dbus object:B
Dbus interface:C
Method :D
註冊到dbus daemon中,其中D設置爲回調函數
客戶端向dbus daemon申請調用註冊信息爲
Dbus name:A,
Dbus object:B
Dbus interface:C
的D函數
dbus daemon收到客戶端的消息後,查詢是否存在該註冊信息的回調函數,如果找不到daemon會產生錯誤消息,作爲應答消息給客戶端。找到則且執行該回調函數,將結果返回給客戶端。
l 消息發送流程:
1.消息發送端申請一個GObject,綁定以下信息:
Dbus name:A,
Dbus object:B
Dbus interface:C
signal :D
註冊到dbus daemon中
2.消息接收端向dbus daemon申請綁定註冊信息爲
Dbus name:A,
Dbus object:B
Dbus interface:C
Signal 爲D的消息回調函數
dbus daemon收到消息發送端發出的DBUS消息後,查詢是否存在該消息的綁定回調函數,且執行該回調函數。
所以消息發送端是不知道誰是接收端的,這個也與DBUS底層接口實現方式不同
注意:bus daemon不對消息重新排序,如果發送了兩條消息到同一個進程,他們將按照發送順序接受到。接受進程並需要按照順序發出應答消息,例如在多線程中處理這些消息,應答消息的發出是沒有順序的。消息都有一個序列號可以與應答消息進行配對。
三、通過Dbus-glib寫一個服務端
dbus-glib定義向dbus daemon申請一個註冊信息的形式爲GObject(C語言)的對象。
1.寫一個XML
首先,先學習怎麼使用內置的xml文件自動創建出易於使用的dbus代理對象。如下的一個xml文件描述了了一個名爲“HelloWorld”,輸入參數爲char *,輸出參數爲char*[]的被調用的函數。
dbus的接口描述文件統一採用utf-8編碼。type域數據類型定義如下:
a |
ARRAY 數組 |
b |
BOOLEAN 布爾值 |
d |
DOUBLE IEEE 754雙精度浮點數 |
g |
SIGNATURE 類型簽名 |
i |
INT32 32位有符號整數 |
n |
INT16 16位有符號整數 |
o |
OBJECT_PATH 對象路徑 |
q |
UINT16 16位無符號整數 |
s |
STRING 零結尾的UTF-8字符串 |
t |
UINT64 64位無符號整數 |
u |
UINT32 32位無符號整數 |
v |
VARIANT 可以放任意數據類型的容器,數據中包含類型信息。例如glib中的GValue。 |
x |
INT64 64位有符號整數 |
y |
BYTE 8位無符號整數 |
() |
定義結構時使用。例如"(i(ii))" |
{} |
定義鍵-值對時使用。例如"a{us}" |
a表示數組,數組元素的類型由a後面的標記決定。例如:
"as"是字符串數組。
數組"a(i(ii))"的元素是一個結構。用括號將成員的類型括起來就表示結構了,結構可以嵌套。
數組"a{sv}"的元素是一個鍵-值對。"{sv}"表示鍵類型是字符串,值類型是VARIANT。
一個node可以有多個interface ,一個interface可以有多個method或signal,上例只以簡單的單函數來說明,如果多個函數可以寫成:
<node name="/">
<interface name="xxxx">
<method name="xx">
<arg type="s"/>
<arg type="as" direction="out"/>
</method>
<method name="xxxx">
</method>
</interface>
</node>
2.通過dbus-binding-tool生成頭文件
dbus-binding-tool目前在92服務器上就有,可以直接運行,執行指令:
dbus-binding-tool --mode=glib-server --prefix=your_module_name your_server.xml > xxx_stub.h
"--prefix"參數定義了對象前綴。設對象前綴是$(prefix),則生成的DBusGObjectInfo結構變量名就是 dbus_glib_$(prefix)_object_info。綁定文件會爲接口方法定義回調函數。回調函數的名稱是這樣的:首先將xml中的方法名稱轉換到全部小寫,下劃線分隔的格式,然後增加前綴"$(prefix)_"。例如:如果xml中有方法SendMessage,綁定文件就會引用一個名稱爲$(prefix)_send_message的函數。
如上例中,--prefix = some_object 生成的general_stub.h中,DBusGObjectInfo結構變量名dbus_glib_some_object_object_info,回調函數名爲some_object_hello_world。
生成的general_stub.h不需要手動修改,直接使用。
3.創建對象
dbus-glib用GObject實現dbus對象,所以我們首先要實現一個對象,繼承於GObject,以下說明請參考提供的示例demo代碼。
3.1 定義對象
typedef struct SomeObject
{
GObject parent;
}SomeObject;
typedef struct SomeObjectClass
{
GObjectClass parent;
}SomeObjectClass;
在 GObject 中,類是兩個結構體的組合,一個是實例結構體,另一個是類結構體,上例中SomeObject是實例結構體,SomeObjectClass是類結構體
命名爲XXX、XXXClass形式.
3.2實現類類型的定義
G_DEFINE_TYPE(SomeObject, some_object, G_TYPE_OBJECT)
G_DEFINE_TYPE 可以讓 GObject 庫的數據類型系統能夠識別我們所定義的 SomeObject 類類型,它接受三個參數,第一個參數是類名,即 SomeObject;第二個參數則是類的成員函數(面向對象術語稱之爲“方法”或“行爲”)名稱的前綴,例如 some_object _get_type 函數即爲 SomeObject 類的一個成員函數,“ some_object” 是它的前綴;第三個參數則指明 SomeObject類類型的父類型爲 G_TYPE_OBJECT
3.3聲明類的函數
GType some_object_get_type (void);
#define SOME_TYPE_OBJECT (some_object_get_type ())
static void some_object_init (SomeObject *obj)
{
}
static void some_object_class_init (SomeObjectClass *klass)
{
}
some_object_get_type函數的作用是向 GObject 庫所提供的類型管理系統提供要註冊的SomeObject類類型的相關信息,可以不實現,但必須要聲明。
some_object_init 是類成員的構造函數
some_object_class_init 是類結構的構造函數,與類成員構造函數區別在於,該構造函數只在該類定義時運行一次,常用來進行消息信號的初始化等。而some_object_init則在創建成員時都會調用一次(如obj = g_object_new)
上例中通過G_DEFINE_TYPE(SomeObject, some_object, G_TYPE_OBJECT)第二個參數把類成員定爲some_object,所以其成員函數名爲:
some_object_get_type
some_object_init
some_object_class_init
所以如果G_DEFINE_TYPE第二個參數爲“XXX”,則相應的成員函數就是XXX_get_type、XXX_init…
至此對象創建完成。
4.向dbus deamon申請註冊
g_type_init ();
dbus_g_object_type_install_info (SOME_TYPE_OBJECT, &dbus_glib_some_object_object_info);
mainloop = g_main_loop_new (NULL, FALSE);
dbus_g_object_type_install_info的作用是向dbus-glib登記對象信息,dbus_glib_some_object_object_info是哪來的?是由前面XML生成的頭文件中指定:
const DBusGObjectInfo dbus_glib_some_object_object_info = {
…
}
g_main_loop_new用來申請創建一個主循環,接收DBUS消息,用於服務端、消息接收端
bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
if (!bus)
lose_gerror ("Couldn't connect to session bus", error);
申請一個會話總線
bus_proxy = dbus_g_proxy_new_for_name (bus, "org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus");
創建連接到dbus daemon
org.freedesktop.DBus ---- dbus daemonDBUS名
/org/freedesktop/DBus --- dbus daemon對象名
org.freedesktop.DBus --- dbus daemon interface
if (!dbus_g_proxy_call (bus_proxy, "RequestName", &error,
G_TYPE_STRING, "org.designfu.SampleService",
G_TYPE_UINT, 0,
G_TYPE_INVALID,
G_TYPE_UINT, &request_name_result,
G_TYPE_INVALID))
lose_gerror ("Failed to acquire org.designfu.SampleService", error);
調用dbus daemon的函數“RequestName”,申請一個DBUS名爲“org.designfu.SampleService”的註冊信息
obj = g_object_new (SOME_TYPE_OBJECT, NULL);
dbus_g_connection_register_g_object (bus, "/SomeObject", G_OBJECT (obj));
申請之前定義的一個對象SomeObject,將該對象與bus綁定,“/SomeObject”是method的頂層對象路徑
g_main_loop_run (mainloop);
進入loop循環,DBUS服務器完成,信息如下:
Dbus name: org.designfu.SampleService
Dbus object: /SomeObject
Dbus interface:test.method.Type
Method :HelloWorld
5.實現method
在general_stub.h中查詢method直實名稱,如
static const DBusGMethodInfo dbus_glib_some_object_methods[] = {
{ (GCallback)
some_object_hello_world, dbus_glib_marshal_some_object_BOOLEAN__STRING_POINTER_POINTER, 0 },
};
即函數名爲some_object_hello_world,函數的第一個輸入參數固定爲對象實例的指針,最後一個參數必定是GError **,中間爲用戶自定的參數,如本例中聲明如下
gboolean
some_object_hello_world (SomeObject *obj, const char *hello_message, char ***ret, GError **error)
最後在代碼中完成該函數實現。
注意:函數聲明要寫在#include "general_stub.h"之前,否則編繹不識別some_object_hello_world
以上這些跟對象相關的部分比較繁瑣,不理解的情況下也可以把它當作公式一樣記下來,只需要修改自定義的部分,其它照搬就可以了。
四、通過Dbus-glib寫一個客戶端
客戶端實現較服務端簡單,並不需要像DBUS底層庫調用那樣申請自身的DBUS名等。
g_type_init ();
申請一個會話總線
bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
if (!bus)
lose_gerror ("Couldn't connect to session bus", error);
向dbus deamon申請連接到以下信息的DBUS總線上
remote_object = dbus_g_proxy_new_for_name (bus,
"org.designfu.SampleService",
"/SomeObject",
" test.method.Type ");
調用HelloWorld程序
if (!dbus_g_proxy_call (remote_object, "HelloWorld", &error,
G_TYPE_STRING, "Hello from example-client.c!", G_TYPE_INVALID,
G_TYPE_STRV, &reply_list, G_TYPE_INVALID))
lose_gerror ("Failed to complete HelloWorld", error);
使用完成後釋放返回參數,此例中返回值是一個字符串數組
g_strfreev (reply_list);
釋放與目標DBUS的連接,結束
g_object_unref (G_OBJECT (remote_object));
五、消息發送與接收
消息發送端
消息發送與服務端實現基本相同,只需要在類結構構造函數中增加對信號的初始化。
XML文件中增加<signal name =“xxx” />,以下是一個同時具備接受函數調用與發送信號功能XML文件示例
代碼中在類結構構造函數中初始化該信號:
static void
some_object_class_init (SomeObjectClass *klass)
{
signals[0] =
g_signal_new ("say_hi",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
0,
NULL, NULL,
g_cclosure_marshal_VOID__STRING,
G_TYPE_NONE, 1, G_TYPE_UINT);
}
g_signal_new中的其它參數在簡單功能不會用到,我們只需要使用第一個和最後兩個參數。
第一個參數是信號的名字,與XML文件匹配,此處注意,XML中是“SayHi”,在此處信號名會被轉換爲“say_hi”,規則基本就是大寫轉小寫、單詞間加下劃線,且第一個字符必須是字母。所以如果之前信號名叫PMlevel的話,建議改名爲PmLevel,或者將g_signal_new中寫成“p_mlevel”,否則運行會報關連錯誤。XML中如果信號名爲全小寫,則不需要轉換。
最後兩個參數表示消息包含的數據個數與數據類型,如上例中表示該消息包含一個UINT型的數據。
發送消息
Level = 1;
g_signal_emit (obj, signals[0], 0, level);
所以消息發送端只將消息發送給dbus daemon,而由dbus daemon查詢誰對該消息有響應,則將該消息發送給指定的進程。
消息接收端
消息接收端與客戶端實現也基本相同,區別在於消息接收端處於loop循環接收狀態,且需要綁定接收消息後的回調函數。
g_type_init ();
mainloop = g_main_loop_new (NULL, FALSE);
bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
if (!bus)
lose_gerror ("Couldn't connect to session bus", error);
向dbus deamon申請連接到以下信息的DBUS總線上,消息由該總線發出
remote_object = dbus_g_proxy_new_for_name (bus,
"org.designfu.SampleService",
"/SomeObject",
"test.server");
設置接收到消息後的回調函數
dbus_g_proxy_add_signal (remote_object, "SayHi", G_TYPE_UINT, G_TYPE_INVALID);
dbus_g_proxy_connect_signal (remote_object, "SayHi", G_CALLBACK (receive_signal_handler),NULL, NULL);
進入LOOP
g_main_loop_run (mainloop);
當收到SayHi的DBUS消息時,客戶端自動執行回調函數receive_signal_handler。
demo已經將函數調用與消息收發寫在了一起,流程爲:
l 客戶端定時2秒調用一次服務器端的函數HelloWord
l 服務端HelloWord發送消息SayHi,附帶整型數據1
l 客戶端收到消息後,消息回調函數打印出收到的數據
六、效率改進
以上幾點大致說明了dbus-glib使用方法,但其在使用上還是有不方便的地方:
1 一個API要定義一個xml接口描述
2 數據封裝非常複雜,非常不利於以後接口的擴展
爲了克服上面的缺點,提高可擴展性和效率,可以這樣做:
如果一個應用分爲client,server兩端的話,要高效率的實現client/server之間
的通信,可以採用如下方式:
改進一:定義一個通用的API xml 接口描述
<?xml version="1.0" encoding="UTF-8" ?>
<node name=" ="/org/freedesktop/DBus/General_api">
<interface name=" org.freedesktop.DBus.general_api">
<method name=" client_request">
<annotation name="org.pmonitor.DBus.GLib.request" value="_ client_request_cb"/>
<arg type="i" name="action_id" direction="in" />
<arg type="ay" name="input_garray" direction="in" />
<arg type="ay" name="output_garray" direction="out" />
<arg type="i" name="result" direction="out" />
</method>
</interface>
</node>
Ay表示字節的數組,其數據類型爲GArray 這個通用的模板關鍵之處就是這個Garray, Garray本身是個容器,這個容器裏面可以裝任何東西。我們就是利用這個GArray來實現client與server之間數據的傳遞,無論想傳遞什麼要的數據。
額外的參數類型支持如下:
D-Bus type signature |
Description |
GType |
C typedef |
Free function |
Notes |
as |
Array of strings |
G_TYPE_STRV |
char ** |
g_strfreev |
|
v |
Generic value container |
G_TYPE_VALUE |
GValue * |
g_value_unset |
The calling conventions for values expect that method callers have |
數組類型集合。
D-Bus type signature |
Description |
GType |
C typedef |
Free function |
Notes |
ay |
Array of bytes |
DBUS_TYPE_G_BYTE_ARRAY |
GArray * |
g_array_free |
|
au |
Array of uint |
DBUS_TYPE_G_UINT_ARRAY |
GArray * |
g_array_free |
|
ai |
Array of int |
DBUS_TYPE_G_INT_ARRAY |
GArray * |
g_array_free |
|
ax |
Array of int64 |
DBUS_TYPE_G_INT64_ARRAY |
GArray * |
g_array_free |
|
at |
Array of uint64 |
DBUS_TYPE_G_UINT64_ARRAY |
GArray * |
g_array_free |
|
ad |
Array of double |
DBUS_TYPE_G_DOUBLE_ARRAY |
GArray * |
g_array_free |
|
ab |
Array of boolean |
DBUS_TYPE_G_BOOLEAN_ARRAY |
GArray * |
g_array_free |
|
Hash表數組:
D-Bus type signature |
Description |
GType |
C typedef |
Free function |
Notes |
a{ss} |
Dictionary mapping strings to strings |
DBUS_TYPE_G_STRING_STRING_HASHTABLE |
GHashTable * |
g_hash_table_destroy |
|
改進二:用dbus的工具函數生成proxy頭文件
前面有說到使用dbus-binding-tool將XML腳本轉換爲stub.h供服務器使用
dbus-binding-tool --mode=glib-server --prefix=your_module_name dbus_general.xml > general_stub.h
其實將--mode值修改爲glib-client,生成proxy.h文件可供客戶端使用,如:
dbus-binding-tool --mode=glib-client --prefix=your_module_name dbus_general.xml > general_proxy.h
打開general_proxy.h看到服務端提供的HelloWorld已經轉爲
test_server_hello_world (DBusGProxy *proxy, const char * IN_arg0, char *** OUT_arg1, GError **error);
客戶端可以直接調用test_server_hello_world,而不再需要使用dbus_g_proxy_call來調用該函數。
general_proxy.h中的另兩個函數聲明涉及異步調用,暫未使用到,沒有了解
phoenix中的pms與libpms就是使用統用API實現多函數調用的例子。
七、注意事項
1.向dbus deamon註冊DBUS的名稱是可以重複的:
dbus_g_proxy_call (bus_proxy, "RequestName", &error,
G_TYPE_STRING, "org.designfu.SampleService",
G_TYPE_UINT, 0,
G_TYPE_INVALID,
G_TYPE_UINT, &request_name_result,
G_TYPE_INVALID)
比如demo中的dbus_server程序可以連續運行多次,通過dbus-monitor –session可以看到其分配的實際Bus Names是不同的,比如一個是“1.5”,一個是“1.6”,因於“1.5”先創建,所以其處於dbus deamon同名隊列的頂部,所以當客戶端發起DBUS通訊時,就只有“1.5”有效,當“1.5”退出後,“1.6”纔會進入DBUS通訊狀態。
2.函數調用中的out參數,如果是服務端申請的內存空間,客戶端在使用完後,要記得釋放內存。
如g_new -----g_free等,具體數據類型不同,其對應的申請內存與釋放內存的接口不同,詳細請查看http://developer.gnome.org/glib/stable/
3.客戶端等完成函數調用等,要關閉DBUS連接:
remote_object = dbus_g_proxy_new_for_name ();
…
g_object_unref (G_OBJECT (remote_object));
dbus_g_bus_get()
…
dbus_g_connection_unref(bus);
4.服務器端被調函數(method)對應的函數返回值是gboolean類型,實際程序運行成功或失敗要通過輸出參數返回,不能通過函數返回。函數實現中一定要返回TRUE,否則dbus daemon 不會向客戶端回複函數調用結果。
八、多線程防衝突
以上demo中實現方法只是單線程實現dbus調用。如果多線程的情況下,以及庫函數情況下,爲確保不同線程使用不同的DBusConnection,在創建dbus總線時要注意使用關鍵字創建各自私有的總線:
Method客戶端實現:
GMainContext* main_context = NULL;
main_context = g_main_context_new();//申請獨立的context
/*不使用dbus_g_bus_get*/
bus = dbus_g_bus_get_private(DBUS_BUS_SESSION,main_context,&error);
…
dbus_g_connection_close(bus);//私有的總線連接要先close才能unref
dbus_g_connection_unref(bus);
g_main_context_unref(main_context);
注意 :
1. g_main_context_unref()要與g_main_context_new() 配合使用,如果申請的資源未釋放,會導致文件句柄泄露。
2. dbus_g_bus_get_private申請的私有總線連接在使用完成後,要使用dbus_g_connection_close先關閉連接後再釋放資源dbus_g_connection_unref, 否則只調用dbus_g_connection_unref會報“私有連接無法關閉”,導致內存泄露。
dbus_g_connection_close目前glib庫並未封裝,需要自已封裝一個,方法如下:
#define _DBUS_POINTER_UNSHIFT(p) ((void*) (((char*)p) - sizeof (void*)))
#define DBUS_CONNECTION_FROM_G_CONNECTION(x) ((DBusConnection*) _DBUS_POINTER_UNSHIFT(x))
void dbus_g_connection_close( DBusGConnection * connection )
{
return dbus_connection_close(DBUS_CONNECTION_FROM_G_CONNECTION(connection));
}