基于GDBus框架的进程间通信详解


基于GDBus框架的进程间通信详解

  • 1. GDBus简介
  • 2. 生成D-Bus interface
  • 3.GDBus interface源文件详解
  • 4. Client端编程
  • 5. Server端编程

1. GDBus简介

首先简单介绍一下什么是GDBus。GDBus是一种基于d-bus技术的进程间通信框架,其中最核心的部分就是“Bus”,个人理解,它就是一个进程间通信的“桥梁”,不同的进程之间进行接收或者传递消息,都需要通过这个"Bus"总线。进程间通信的消息都会先发送到"Bus"总线上,然后再分发到目标进程上,"Bus"总线会根据收到消息的类型不同,采取不同的处理,主要处理可以分为两类:函数调用、信号广播。为了方便起见,我们假设有两个进程需要进行通信,一个Client进程,一个Server进程。

  1. 函数调用
    Client进程需要调用Server进程的某个方法(Method Call),请求信息会先发送给"Bus"总线,经过"Bus"总线处理后,再将请求信息发送到Server进程,调用相应的Method方法去执行某些操作,然后将操作结果返回给Client进程。
  2. 信号广播
    Server进程可以主动发送一些信号(Signal),Client进程可以选择注册感兴趣的信号。当Server发送信号时,消息同样会先发送到"Bus"总线上,如果Client进程正好注册接受该类型的信号,信号就会发送到Client进程上。

2. 生成D-Bus interface

实现Client和Server 进程通信的第一步,就是要定义好通信的接口。GDBus通信接口是在xml文件下描述的,然后通过gdbus-codegen工具就可以自动生成对应D-Bus interface源文件和头文件了。

Interface.xml:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<node>
  <interface name="com.gdbus.demo">
    <method name="SetData">
      <arg name="data" type="s" direction="in"/>
      <arg name="response" type="s" direction="out"/>
    </method>
    <signal name="SendSignal">
      <arg name="sig" type="i"/>
    </signal>
  </interface>
</node>

这里我们将interface name定义为"com.gdbus.demo",然后分别定义了一个method方法和一个signal方法。
arg name表示参数名字,type表示参数类型,direction in和out分别表示入参和出参。
这里xml中定义的参数类型都是GVariant类型的,对于每个字符代表的含义,可以参照下表。

GVariant数据类型 代表含义
b:G_VARIANT_TYPE_BOOLEAN的类型字符串 布尔值。
y:G_VARIANT_TYPE_BYTE的类型字符串 一个字节。
n:G_VARIANT_TYPE_INT16的类型字符串; 有符号的16位整数。
q:G_VARIANT_TYPE_UINT16的类型字符串 一个无符号的16位整数。
i:G_VARIANT_TYPE_INT32的类型字符串 有符号的32位整数。
u:G_VARIANT_TYPE_UINT32的类型字符串 一个无符号的32位整数。
x:G_VARIANT_TYPE_INT64的类型字符串 有符号的64位整数。
t:G_VARIANT_TYPE_UINT64的类型字符串 一个无符号的64位整数。
h:G_VARIANT_TYPE_HANDLE的类型字符串 一个有符号的32位值,按照惯例,该值用作与D-Bus消息一起发送的文件描述符数组的索引。
d:G_VARIANT_TYPE_DOUBLE的类型字符串 双精度浮点值。
s:G_VARIANT_NEW_STRING的类型字符串 一个字符串。
o:G_VARIANT_TYPE_OBJECT_PATH的类型字符串 D-Bus对象路径形式的字符串。
g:G_VARIANT_TYPE_SIGNATURE的类型字符串 D-Bus类型签名形式的字符串。
?:G_VARIANT_TYPE_BASIC的类型字符串 一个不确定类型,它是任何基本类型的超类型。
v:G_VARIANT_TYPE_VARIANT的类型字符串 包含任何其他类型的值的容器类型。
a:用作另一个类型字符串的前缀,表示该类型的数组 例如,类型字符串“ ai”是带符号的32位整数数组的类型。
m:用作另一个类型字符串的前缀,表示该类型的“也许”或“可空”版本 例如,类型字符串“ ms”是可能包含字符串或不包含任何值的值的类型。
():用于封装零个或多个其他串联类型的字符串以创建元组类型 例如,类型字符串“(is)”,是整数和字符串对的类型。
r:G_VARIANT_TYPE_TUPLE的类型字符串 一个不定类型,它是任何元组类型的超类型,而与元素数无关。

定义完这个xml文件,我们只需要在终端输入以下命令,就可以生成其对应的源文件了:

gdbus-codegen --generate-c-code=gdbusdemo_gen Interface.xml

3.GDBus interface源文件详解

自己在学习GDBus的时候,看过很多篇文章,对gdbus-codegen生成的D-Bus interface源文件,很多文章都是一带而过,由于后面编写Server和Client部分的代码都需要用到这些接口,如果不理解的话,代码写起来也是一头雾水,只能去网上复制别人的代码,到最后也不知道实现过程和原理。。 为了加深对GDBus的理解,笔者自己去研究了一下,下面带大家看一看生成的这些文件中到底有什么神秘的东西。

3.1
首先,你会看到源文件中定义了如下结构体(Introspection data):
在这里插入图片描述
让我们来看一下这些结构体的具体组成:

_ExtendedGDBusArgInfo:

1
2
3
4
5
6
7
8
9
10
static const _ExtendedGDBusArgInfo _com_gdbus_demo_method_info_set_name_IN_ARG_name =
{
  {
    -1,
    (gchar *) "name",
    (gchar *) "s",
    NULL
  },
  FALSE
};

可以看到,_ExtendedGDBusArgInfo这个结构体中又嵌套了一个GDBusArgInfo结构体:

1
2
3
4
5
6
typedef struct {
  volatile gint         ref_count;
  gchar                *name;
  gchar                *signature;
  GDBusAnnotationInfo **annotations;
} GDBusArgInfo;

其主要用来说明我们定义的Method或者Signal的参数。

_ExtendedGDBusMethodInfo:

1
2
3
4
5
6
7
8
9
10
11
12
static const _ExtendedGDBusMethodInfo _com_gdbus_demo_method_info_set_name =
{
  {
    -1,
    (gchar *) "SetName",
    (GDBusArgInfo **) &_com_gdbus_demo_method_info_set_name_IN_ARG_pointers,
    (GDBusArgInfo **) &_com_gdbus_demo_method_info_set_name_OUT_ARG_pointers,
    NULL
  },
  "handle-set-name",
  FALSE
};

在_ExtendedGDBusMethodInfo中嵌套了一个GDBusMethodInfo结构体:

1
2
3
4
5
6
7
typedef struct {
  volatile gint         ref_count;
  gchar                *name;
  GDBusArgInfo        **in_args;
  GDBusArgInfo        **out_args;
  GDBusAnnotationInfo **annotations;
} GDBusMethodInfo;

其主要用来说明D-Bus interface的Method方法,name表示方法的名字,**in_args 、**out_args分别表示指向入参和出参结构体的指针。

_ExtendedGDBusSignalInfo:

1
2
3
4
5
6
7
8
9
10
static const _ExtendedGDBusSignalInfo _com_gdbus_demo_signal_info_send_signal =
{
  {
    -1,
    (gchar *) "SendSignal",
    (GDBusArgInfo **) &_com_gdbus_demo_signal_info_send_signal_ARG_pointers,
    NULL
  },
  "send-signal"
};

在_ExtendedGDBusSignalInfo中嵌套了一个GDBusSignalInfo结构体:

1
2
3
4
5
6
typedef struct {
  volatile gint         ref_count;
  gchar                *name;
  GDBusArgInfo        **args;
  GDBusAnnotationInfo **annotations;
} GDBusSignalInfo;

其主要用来说明D-Bus interface的Signal方法,name表示信号的名字,**args表示指向参数结构体的指针。
所有的_ExtendedGDBusMethodInfo和_ExtendedGDBusSignalInfo会分别存放到method_info_pointers[]和signal_info_pointers[]
这两个指针数组中。

1
2
3
4
5
static const _ExtendedGDBusSignalInfo * const _com_gdbus_demo_signal_info_pointers[] =
{
  &_com_gdbus_demo_signal_info_send_signal,
  NULL
};

随后又定义了一个_ExtendedGDBusInterfaceInfo结构体,在这个结构体中,存放了我们上面提到的那两个指针数组。
这个结构体就是针对于我们xml文件生成的D-Bus interface的一个整体说明,把所有Methond、Signal及其参数都关联到了这一个结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
static const _ExtendedGDBusInterfaceInfo _com_gdbus_demo_interface_info =
{
  {
    -1,
    (gchar *) "com.gdbus.demo.",
    (GDBusMethodInfo **) &_com_gdbus_demo_method_info_pointers,
    (GDBusSignalInfo **) &_com_gdbus_demo_signal_info_pointers,
    NULL,
    NULL
  },
  "com-gdbus-demo",
};

3.2
接下来,你会看到一堆这样的函数接口:
在这里插入图片描述
看完下面这些内容,相信大家应该就会对D-Bus的通信的实现过程有一定了解了。首先,com_gdbus_demo_default_init () 通过 g_signal_new() 定义了用于传入D-Bus方法调用的GObject信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static void
com_gdbus_demo_default_init (ComGdbusDemoIface *iface)
{
  /* GObject signals for incoming D-Bus method calls: */
  /**
   * ComGdbusDemo::handle-set-name:
   * @object: A #ComGdbusDemo.
   * @invocation: A #GDBusMethodInvocation.
   * @arg_name: Argument passed by remote caller.
   *
   * Signal emitted when a remote caller is invoking the <link linkend="gdbus-method-com-gdbus-demo-.SetName">SetName()</link> D-Bus method.
   *
   * If a signal handler returns %TRUE, it means the signal handler will handle the invocation (e.g. take a reference to @invocation and eventually call com_gdbus_demo_complete_set_name() or e.g. g_dbus_method_invocation_return_error() on it) and no order signal handlers will run. If no signal handler handles the invocation, the %G_DBUS_ERROR_UNKNOWN_METHOD error is returned.
   *
   * Returns: %TRUE if the invocation was handled, %FALSE to let other signal handlers run.
   */
  g_signal_new ("handle-set-name",
    G_TYPE_FROM_INTERFACE (iface),
    G_SIGNAL_RUN_LAST,
    G_STRUCT_OFFSET (ComGdbusDemoIface, handle_set_name),
    g_signal_accumulator_true_handled,
    NULL,
    g_cclosure_marshal_generic,
    G_TYPE_BOOLEAN,
    2,
    G_TYPE_DBUS_METHOD_INVOCATION, G_TYPE_STRING);

  /* GObject signals for received D-Bus signals: */
  /**
   * ComGdbusDemo::send-signal:
   * @object: A #ComGdbusDemo.
   * @arg_sig: Argument.
   *
   * On the client-side, this signal is emitted whenever the D-Bus signal <link linkend="gdbus-signal-com-gdbus-demo-.SendSignal">"SendSignal"</link> is received.
   *
   * On the service-side, this signal can be used with e.g. g_signal_emit_by_name() to make the object emit the D-Bus signal.
   */
  g_signal_new ("send-signal",
    G_TYPE_FROM_INTERFACE (iface),
    G_SIGNAL_RUN_LAST,
    G_STRUCT_OFFSET (ComGdbusDemoIface, send_signal),
    NULL,
    NULL,
    g_cclosure_marshal_generic,
    G_TYPE_NONE,
    1, G_TYPE_INT);

}

由于我们xml中只定义了一个method和一个signal,所以这里只通过g_signal_new() 生成两个信号:"handle-set-name"和 “send-signal”。

  • 当Client端远程调用D-Bus上的SetName方法的时候,就会触发
    "handle-set-name"信号,D-Bus收到这个信号后,就会通知Server去采取相应处理。
  • 每当D-Bus接收到Sever发来的SendSignal信号时,就会给Client发送"send-signal",这样Client就接收到了Server广播的信号。

还剩下一些接口,我把它们分为了以下两类:

  1. 对于xml中定义的signal,其接口会抽象为 xxx_emit_xxx() 的形式(com_gdbus_demo_emit_send_signal),内部实现是通过调用 g_signal_emit_by_name() 来发送一个信号。
1
2
3
4
5
6
7
void
com_gdbus_demo_emit_send_signal (
    ComGdbusDemo *object,
    gint arg_sig)
{
  g_signal_emit_by_name (object, "send-signal", arg_sig);
}
  1. 对于xml中定义的method方法,其接口会抽象为以下三种形式:xxx_call_xxx()、 xxx_call_xxx_finish() 、xxx_call_xxx_sync(),这三者之间有什么联系和区别呢?对于 xxx_call_xxx()xxx_call_xxx_sync(),前者是通过 g_dbus_proxy_call() 接口来异步调用proxy上的method方法,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
com_gdbus_demo_call_set_name (
    ComGdbusDemo *proxy,
    const gchar *arg_name,
    GCancellable *cancellable,
    GAsyncReadyCallback callback,
    gpointer user_data)
{
  g_dbus_proxy_call (G_DBUS_PROXY (proxy),
    "SetName",
    g_variant_new ("(s)",
                   arg_name),
    G_DBUS_CALL_FLAGS_NONE,
    -1,
    cancellable,
    callback,
    user_data);
}

g_dbus_proxy_call 的倒数第二个参数(callback)是请求成功时调用的callback函数,如果你关心调用结果如何,可以给他注册一个GAsyncReadyCallback (),在这个函数里调用xxx_call_xxx_finish() (函数内部调用g_dbus_proxy_call_finish()接口来完成由g_dbus_proxy_call()开始的操作,并获取返回值。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gboolean
com_gdbus_demo_call_set_name_finish (
    ComGdbusDemo *proxy,
    gchar **out_response,
    GAsyncResult *res,
    GError **error)
{
  GVariant *_ret;
  _ret = g_dbus_proxy_call_finish (G_DBUS_PROXY (proxy), res, error);
  if (_ret == NULL)
    goto _out;
  g_variant_get (_ret,
                 "(s)",
                 out_response);
  g_variant_unref (_ret);
_out:
  return _ret != NULL;
}

如果你并不关心调用结果,那么用NULL就可以。注意,GAsyncReadyCallback接口不可以随便定义,必须按照以下形式进行定义:

1
2
3
4
void
(*GAsyncReadyCallback) (GObject *source_object,
                        GAsyncResult *res,
                        gpointer user_data);

xxx_call_xxx_sync() 是通过g_dbus_proxy_call_sync() 来同步调用proxy上的method方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
gboolean
com_gdbus_demo_call_set_name_sync (
    ComGdbusDemo *proxy,
    const gchar *arg_name,
    gchar **out_response,
    GCancellable *cancellable,
    GError **error)
{
  GVariant *_ret;
  _ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (proxy),
    "SetName",
    g_variant_new ("(s)",
                   arg_name),
    G_DBUS_CALL_FLAGS_NONE,
    -1,
    cancellable,
    error);
  if (_ret == NULL)
    goto _out;
  g_variant_get (_ret,
                 "(s)",
                 out_response);
  g_variant_unref (_ret);
_out:
  return _ret != NULL;
}

xxx_complete_xxx()xxx_call_xxx_sync() 对应,在Client调用调用xxx_call_xxx_sync() 后,还需要在Server调用xxx_complete_xxx() 来获得返回值并结束同步方法的调用。

对于g_dbus_proxy_call()这些接口的具体讲解,这里就不展开说明了,内容实在太多了,感兴趣的同学可以自己从这个链接学习一下。

3.3
最后还剩下一些关于proxy 和skeleton的接口,常用的应该就是xxx_proxy_new_sync()xxx_skeleton_new()
xxx_proxy_new_sync() 是为D-Bus interface同步创建一个代理使用的。
xxx_skeleton_new() 是为D-Bus创建框架对象使用的。
对于其他接口,感兴趣的朋友可以自己再研究一下,通过看注释以及参考官方文档应该都能明白,后续有时间的话,我可能也会研究整理一下。

有了前面这些内容的了解,接下来就是该考虑如何利用这些接口去编写Client和Server的代码了。

4. Client端编程

首先来明确一下,我们需要在Client实现的两个最重要的内容:

  1. 同步创建一个proxy,将Server进程发来的signal信号与proxy连接,并在回调函数中根据signal采取相应的处理。
  2. 主动发送请求,向Server进程调用method方法。

下面来看一下Client进程具体代码思路:

  1. 调用g_main_loop_new ()接口创建一个新的GMainLoop(表示GLib或GTK +应用程序的主事件循环)。
1
pLoop = g_main_loop_new(NULL,FALSE);

这里我们先创建,并不开启(将 g_main_loop_new () 第二个入参设为false),后面通过调用g_main_loop_run() 再来开启循环。

  1. 同步创建一个proxy
1
2
3
4
5
6
pProxy = com_gdbus_demo_proxy_new_sync(pConnection,
             G_DBUS_PROXY_FLAGS_NONE,
             COM_GDBUS_DEMO_NAME,
             COM_GDBUS_DEMO_OBJECT_PATH,
             NULL,
             &pProxyError);
  1. 将signal信号与proxy连接,并在回调函数中根据收到的信号采取相应的处理。
1
g_signal_connect(pProxy, "send-signal", G_CALLBACK(sendSignalHandler), NULL);
  1. 向D-Bus发送信号,主动调用Server的method方法
1
com_gdbus_demo_call_set_name_sync (pProxy, in_arg, out_arg, NULL, pError);

  1. 最后调用g_main_loop_run()接口开启GMainLoop。
1
g_main_loop_run( pLoop );

5. Server端编程

同样,首先来明确一下,我们需要在Server实现的两个最重要的内容:

  1. 创建一个skeleton(skeleton可以理解为Server端的D-Bus 接口),将skeleton与method信号连接,并在回调函数中具体完成Method的实现。
  2. 主动向Client端发送Signal信号。

Server进程具体代码思路如下:

  1. 调用g_main_loop_new ()接口创建一个新的GMainLoop。
1
pLoop = g_main_loop_new(NULL,FALSE);
  1. 调用g_bus_own_name ()接口连接上D-Bus,获取总线上的名字。
1
2
3
4
5
6
7
8
9
guint own_id =
    g_bus_own_name (COM_GDBUS_DEMO_BUS,
                COM_GDBUS_DEMO_NAME,
                G_BUS_NAME_OWNER_FLAGS_NONE,
                &GBusAcquired_Callback,
                &GBusNameAcquired_Callback,
                &GBusNameLost_Callback,
                NULL,
                NULL);

这上面提到了三个非常重要的回调函数,GBusAcquiredCallback、GBusNameAcquiredCallback、GBusNameLostCallback。下面我们来讲一下,这三个函数分别在什么时候被调用,以及在这三个回调函数中应该分别实现什么功能。

  • 如果无法建立与总线的连接会调用name_lost_handler()
  • 如果无法获取 bus name,会先调用bus_acquired_handler(),再调用name_lost_handler()
  • 如果已获得 bus name ,会先调用bus_acquired_handler(),再调用name_acquired_handler()

2.1 GBusAcquiredCallback()
当获得到与消息总线的连接时会被调用,在这个函数里,我们要实现以下内容:首先要为D-Bus interface创建一个skeleton对象:

1
pSkeleton = com_gdbus_demo_skeleton_new();

然后将调用g_signal_connect() 接口将信号与对应的callback函数连接:

1
(void) g_signal_connect(pSkeleton, "handle-set-name", G_CALLBACK(setName), NULL);

将pSkeleton连接到D-Bus路径上。

1
2
3
4
(void) g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(pSkeleton),
                                                connection,
                                                COM_GDBUS_DEMO_OBJECT_PATH,
                                                &pError);

还应该有一个函数主动向Client发送信号

1
g_timeout_add(2000, (GSourceFunc)sendSignal, NULL);

2.2 GBusNameAcquiredCallback
这个函数会在成功获取到Bus name的时候被调用。

2.3 GBusNameLostCallback ()
当获取到Bus name失败的时候会被调用,在这里我们应该打印相应的Error log,出问题的时候方便我们排查错误原因。此时还应该调用g_main_loop_quit() 来退出循环。

  1. 最后调用g_main_loop_run() 开启GMainLoop。
1
g_main_loop_run( pLoop );

到这里,关于GDBus的讲解就全部结束了,有理解错误的地方欢迎大家指出。笔者最后自己写了一个Demo,带大家来看一下运行效果:
在这里插入图片描述

部分源码已在文章中提到,完整工程代码已经上传到了Github上,想学习交流的朋友们欢迎下载。

完整代码:https://github.com/Pedal2Metal/GDBus