D-BUS 是一個大有前途的消息總線和活動系統,正開始深入地滲透到 Linux 桌面之中。了解創建它的原因、它的用途以及發展前景。 D-BUS 本質上是 進程間通信(inter-process communication)(IPC)的一個實現。不過,有一些特性使得 D-BUS 遠遠不是“只是另一個 IPC 實現”。有很多不同的 IPC 實現,因為每一個都定位於解決特定的明確定義的問題。CORBA 是用於面向對象編程中復雜的 IPC 的一個強大的解決方案。DCOP 是一個較輕量級的 IPC 框架,功能較少,但是可以很好地集成到 K 桌面環境中。SOAP 和 XML-RPC 設計用於 Web 服務,因而使用 HTTP 作為其傳輸協議。D-BUS 設計用於桌面應用程序和 OS 通信。 桌面應用程序通信 典型的桌面都會有多個應用程序在運行,而且,它們經常需要彼此進行通信。DCOP 是一個用於 KDE 的解決方案,但是它依賴於 Qt,所以不能用於其他桌面環境之中。類似的,Bonobo 是一個用於 GNOME 的解決方案,但是非常笨重,因為它是基於 CORBA 的。它還依賴於 GObject,所以也不能用於 GNOME 之外。 D-BUS 的目標是將 DCOP 和 Bonobo 替換為簡單的 IPC,並集成這兩種桌面環境。由於盡可能地減少了 D-BUS 所需的依賴,所以其他可能會使用 D-BUS 的應用程序不用擔心引入過多依賴。 桌面/操作系統通信 術語“操作系統”在這裡不僅包括內核,還包括系統後台進程。例如,通過使用 D-BUS 的 udev(Linux 2.6 中取代 devfs 的,提供動態 /dev 目錄),當設備(比如一個 USB 照相機)插入時會發放出一個信號。這樣可以更緊密地將硬件集成到桌面中,從而改善用戶體驗。 D-BUS 特性 D-BUS 有一些有趣的特性,使其像是一個非常有前途的選擇。 協議是低延遲而且低開銷的,設計得小而高效,以便最小化傳送的往返時間。另外,協議是二進制的,而不是文本的,這樣就排除了費時的序列化過程。由於只面向本地機器處理的使用情形,所以所有的消息都以其自然字節次序發送。字節次序在每個消息中聲明,所以如果一個 D-BUS 消息通過網絡傳輸到遠程的主機,它仍可以被正確地識別出來。 從開發者的角度來看,D-BUS 是易於使用的。有線協議容易理解,客戶機程序庫以直觀的方式對其進行包裝。 程序庫還設計用於為其他系統所包裝。預期,GNOME 將使用 GObject 創建包裝 D-BUS 的包裝器(實際上這些已經部分存在了,將 D-BUS 集成入它們的事件循環),KDE 將使用 Qt 創建類似的包裝器。由於 Python 具有面向對象特性和靈活的類型,已經有了具備類似接口的 Python 包裝器。 最後,D-BUS 正在 freedesktop.org 的保護下進行開發,在那裡,來自 GNOME、KDE 以及其他組織的對此感興趣的成員參與了設計與實現。 D-BUS 的內部工作方式 典型的 D-BUS 設置將由幾個總線構成。將有一個持久的 系統總線(system bus),它在引導時就會啟動。這個總線由操作系統和後台進程使用,安全性非常好,以使得任意的應用程序不能欺騙系統事件。還將有很多 會話總線(session buses),這些總線當用戶登錄後啟動,屬於那個用戶私有。它是用戶的應用程序用來通信的一個會話總線。當然,如果一個應用程序需要接收來自系統總線的消息,它不如直接連接到系統總線 —— 不過,它可以發送的消息將是受限的。 一旦應用程序連接到了一個總線,它們就必須通過添加 匹配器(matchers) 來聲明它們希望收到哪種消息。匹配器為可以基於接口、對象路徑和方法進行接收的消息指定一組規則(見後)。這樣就使得應用程序可以集中精力去處理它們想處理的內容,以實現消息的高效路由,並保持總線上消息的預期數量,以使得不會因為這些消息導致所有應用程序的性能下降並變得很慢。 對象 本質上,D-BUS 是一個對等(peer-to-peer)的協議 —— 每個消息都有一個源和一個目的。這些地址被指定為 對象路徑。概念上,所有使用 D-BUS 的應用程序都包括一組 對象,消息發送到或者發送自特定對象 —— 不是應用程序 —— 這些對象由對象路徑來標識。 另外,每個對象都可以支持一個或多個 接口(interfaces)。這些接口看起來類似於 Java 中的接口或者 C++ 中的純粹的虛類(pure virtual classes)。不過,沒有選項來檢查對象是否實現了它們所聲明的接口,而且也沒有辦法可以調查對象內部以使列出其支持的接口。接口用於名稱空間和方法名稱,因此一個單獨的對象可以有名稱相同而接口不同的多個方法。 消息 在 D-BUS 中有四種類型的消息:方法調用(method calls)、方法返回(method returns)、信號(signals)和錯誤(errors)。要執行 D-BUS 對象的方法,您需要向對象發送一個方法調用消息。它將完成一些處理並返回一個方法返回消息或者錯誤消息。信號的不同之處在於它們不返回任何內容:既沒有“信號返回”消息,也沒有任何類型的錯誤消息。 消息也可以有任意的參數。參數是強類型的,類型的范圍是從基本的非派生類型(布爾(booleans)、字節(bytes)、整型(integers))到高層次數據結構(字符串(strings)、數組( arrays)和字典(dictionaries))。 服務 服務(Services) 是 D-BUS 的最高層次抽象,它們的實現當前還在不斷發展變化。應用程序可以通過一個總線來注冊一個服務,如果成功,則應用程序就已經 獲得 了那個服務。其他應用程序可以檢查在總線上是否已經存在一個特定的服務,如果沒有可以要求總線啟動它。服務抽象的細節 —— 尤其是服務活化 —— 當前正處於發展之中,應該會有變化。 用例 盡管 D-BUS 相對較新,但是卻迅速地得到了采用。如前所述,可以構建具有 D-BUS 支持的 udev 以使得當熱插拔(hot-plug)設備時它可以發送一個信號。任何應用程序都可以偵聽這些事件並當接收到這些事件時執行動作。例如,gnome-volume-manager 可以檢測到 USB 存儲棒的插入並自動掛載它;或者,當插入一個數碼相機時它可以自動下載照片。 一個更為有趣但很不實用的例子是 Jamboree 和 Ringaling 的結合。Jamboree 是一個簡單的音樂播放器,它具有 D-BUS 接口,以使得它可以被告知播放、到下一首歌、改變音量等等。Ringaling 是一個小程序,它打開 /dev/ttyS0(一個串行端口)並觀察接收到的內容。當 Ringaling 發現文本“RING”時,就通過 D-BUS 告知 Jamboree 減小音量。最終的結果是,如果您的計算機上插入了一個調制解調器,而且電話鈴響,則音樂音量就會為您減小。這 正是計算機所追求的! 代碼示例 現在,讓我們來接觸一些使用 D-BUS 代碼的示例。 dbus-ping-send.c 每秒通過會話總線發送一個參數為字符串“Ping!”的信號。我使用 Glib 來管理總線,以使得我不需要自己來處理總線的連接細節。 清單 1. dbus-ping-send.c #include <glib.h> #include <dbus/dbus-glib.h> static gboolean send_ping (DBusConnection *bus); int main (int argc, char **argv) { GMainLoop *loop; DBusConnection *bus; DBusError error; /* Create a new event loop to run in */ loop = g_main_loop_new (NULL, FALSE); /* Get a connection to the session bus */ dbus_error_init (&error); bus = dbus_bus_get (DBUS_BUS_SESSION, &error); if (!bus) { g_warning ("Failed to connect to the D-BUS daemon: %s", error.message); dbus_error_free (&error); return 1; } /* Set up this connection to work in a GLib event loop */ dbus_connection_setup_with_g_main (bus, NULL); /* Every second call send_ping() with the bus as an argument*/ g_timeout_add (1000, (GSourceFunc)send_ping, bus); /* Start the event loop */ g_main_loop_run (loop); return 0; } static gboolean send_ping (DBusConnection *bus) { DBusMessage *message; /* Create a new signal "Ping" on the "com.burtonini.dbus.Signal" interface, * from the object "/com/burtonini/dbus/ping". */ message = dbus_message_new_signal ("/com/burtonini/dbus/ping", "com.burtonini.dbus.Signal", "Ping"); /* Append the string "Ping!" to the signal */ dbus_message_append_args (message, DBUS_TYPE_STRING, "Ping!", DBUS_TYPE_INVALID); /* Send the signal */ dbus_connection_send (bus, message, NULL); /* Free the signal now we have finished with it */ dbus_message_unref (message); /* Tell the user we send a signal */ g_print("Ping!\n"); /* Return TRUE to tell the event loop we want to be called again */ return TRUE; } main 函數創建一個 GLib 事件循環,獲得會話總線的一個連接,並將 D-BUS 事件處理集成到 Glib 事件循環之中。然後它創建了一個名為 send_ping 間隔為一秒的計時器,並啟動事件循環。 send_ping 構造一個來自於對象路徑 /com/burtonini/dbus/ping 和接口 com.burtonini.dbus.Signal 的新的 Ping 信號。然後,字符串 “Ping!”作為參數添加到信號中並通過總線發送。在標准輸出中會打印一條消息以讓用戶知道發送了一個信號。 當然,不應該向總線發送了信號而沒有任何程序在偵聽它們……於是我們需要: 清單 2. dbus-ping-listen.c #include <glib.h> #include <dbus/dbus-glib.h> static DBusHandlerResult signal_filter (DBusConnection *connection, DBusMessage *message, void *user_data); int main (int argc, char **argv) { GMainLoop *loop; DBusConnection *bus; DBusError error; loop = g_main_loop_new (NULL, FALSE); dbus_error_init (&error); bus = dbus_bus_get (DBUS_BUS_SESSION, &error); if (!bus) { g_warning ("Failed to connect to the D-BUS daemon: %s", error.message); dbus_error_free (&error); return 1; } dbus_connection_setup_with_g_main (bus, NULL); /* listening to messages from all objects as no path is specified */ dbus_bus_add_match (bus, "type='signal',interface='com.burtonini.dbus.Signal'"); dbus_connection_add_filter (bus, signal_filter, loop, NULL); g_main_loop_run (loop); return 0; } static DBusHandlerResult signal_filter (DBusConnection *connection, DBusMessage *message, void *user_data) { /* User data is the event loop we are running in */ GMainLoop *loop = user_data; /* A signal from the bus saying we are about to be disconnected */ if (dbus_message_is_signal (message, DBUS_INTERFACE_ORG_FREEDESKTOP_LOCAL, "Disconnected")) { /* Tell the main loop to quit */ g_main_loop_quit (loop); /* We have handled this message, don't pass it on */ return DBUS_HANDLER_RESULT_HANDLED; } /* A Ping signal on the com.burtonini.dbus.Signal interface */ else if (dbus_message_is_signal (message, "com.burtonini.dbus.Signal", "Ping")) { DBusError error; char *s; dbus_error_init (&error); if (dbus_message_get_args (message, &error, DBUS_TYPE_STRING, &s, DBUS_TYPE_INVALID)) { g_print("Ping received: %s\n", s); dbus_free (s); } else { g_print("Ping received, but error getting message: %s\n", error.message); dbus_error_free (&error); } return DBUS_HANDLER_RESULT_HANDLED; } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } 這個程序偵聽 dbus-ping-send.c 正在發出的信號。main 函數和前面一樣啟動,創建一個到總線的連接。然後它聲明願意當具有 com.burtonini.dbus.Signal 接口的信號被發送時得到通知,將 signal_filter 設置為通知函數,然後進入事件循環。 當滿足匹配的消息被發送時,signal_func 會被調用。不過,它也將會收到來自總線本身的總線管理信號。要確定接收到消息時應該做些什麼,僅僅需要檢驗消息頭。如果消息是總線斷開信號,則事件循環終止,因為偵聽一個不存在的總線是沒有意義的。(告知總線信號已經處理)。然後,將到來的消息與期望的消息相比較,如果成功,則解出參數並輸出。如果到來的消息不是其中的任何一個,則告知總線沒有處理那個消息。 那兩個示例使用了低層的 D-BUS 程序庫,這個程序庫是完全的,但是當您想創建服務和很多對象時,使用起來冗長得令人厭倦。有正在開發中的 C# 和 Python 包裝器,提供了非常接近於 D-BUS 的邏輯模型的編程接口。作為一個示例,這裡是用 Python 重新對 ping/listen 示例進行了更為精致的實現。由於 Python 綁定模擬了邏輯接口,所以不可能不通過一個服務來發送信號。所以這個例子也要創建一個服務: 清單 3. dbus-ping-send.py #! /usr/bin/env python import gtk import dbus # Connect to the bus bus = dbus.Bus() # Create a service on the bus service = dbus.Service("com.burtonini.dbus.SignalService", bus) # Define a D-BUS object class SignalObject(dbus.Object): def __init__(self, service): dbus.Object.__init__(self, "/", [], service) # Create an instance of the object, which is part of the service signal_object = SignalObject(service) def send_ping(): signal_object.broadcast_signal("com.burtonini.dbus.Signal", "Ping") print "Ping!" return gtk.TRUE # Call send_ping every second to send the signal gtk.timeout_add(1000, send_ping) gtk.main() 代碼大部分是不言而明的:獲得一個到總線的連接並注冊 com.burtonini.dbus.SignalService 服務。然後創建一個最小限度的 D-BUS 對象,這個對象每秒廣播一個信號。代碼比相應的 C 代碼更簡單,但是需要做 Python 綁定的工作。(例如,沒有方法向信號添加參數。) 清單 4. dbus-ping-listen.py #! /usr/bin/env python import gtk import dbus bus = dbus.Bus() def signal_callback(interface, signal_name, service, path, message): print "Received signal %s from %s" % (signal_name, interface) # Catch signals from a specific interface and object, and call signal_callback # when they arrive. bus.add_signal_receiver(signal_callback, "com.burtonini.dbus.Signal", # Interface None, # Any service "/" # Path of sending object ) # Enter the event loop, waiting for signals gtk.main() 此代碼比 dbus-ping-listen.c 中相應的 C 代碼更簡明,也更容易讀懂。此外,有些地方需要做綁定的工作(當調用 bus.add_signal_receiver 時,用戶必須傳入一個接口和一個對象路徑;否則會創建不正常的匹配器)。這是一個微不足道的缺陷,一旦這個缺陷被修正,就可以去服務和對象路徑參數除,這將進一步提高代碼的可讀性。 結束語 D-BUS 是一個輕量級但是很強大的遠程過程調用系統,為希望使用它的應用程序帶來最小的開銷代價。 D-BUS 正由一組非常有經驗的程序員進行積極的公開開發。D-BUS 得到了早期采用者的迅速接受,因而它在 Linux 桌面領域似乎有一個樂觀的未來。