Xfce漫游(3) - 连接与使用D-Bus

上回讨论到xfce4-session会启动一个Xfce桌面,把壁纸、面板之类的通通显示出了。本节将从源码出发探讨xfce4-session的职责以及工作原理。

Xfce4是一个桌面环境以及各种应用程序集合的项目,大部分项目都采用GPLv2协议授权,也存在着LGPL的。项目的开发维护都位于Xfce gitlab,开发讨论现阶段使用Matrix完成,更多参见Xfce官网,总之我们先取得一份xfce4-session的源码吧:

$ git clone --depth 1 https://gitlab.xfce.org/xfce/xfce4-session.git 

项目README中写道:

Xfce4-session是Xfce的会话管理器,它的任务是保存你的桌面状态(已打开的程序以及它们的位置),在下次启动时恢复它们。可以保存多个会话,在启动时选择要恢复哪一个。

在Xfce设置管理器 -> 会话和保存中可以手动保存会话。保存的一般位于${XDG_CACHE_HOME}/sessions/xfce4-session-$(hostname)${DISPLAY}。为了不影响自己正在使用的桌面,还是像先前一样创建一个虚拟屏幕来玩这些吧。——虽然写着可以保存多个会话但我其实没找到在哪里保存。

获取D-Bus

xfce session作为启动桌面环境时第一个启动的程序,其要做的第一件事是获取Session Bus(D-bus的一种,按D-Bus规格说明文档的描述,这是用于实现桌面环境的,每个会话中只能有一个程序使用Session Bus)。D-Bus(D-总线,D代表什么未知,可能是data或者desktop吧)在系统启动之初就由systemd启动了dbus.service,然而这个是系统级别的;按照Archwiki: Systemd/User中的描述,每当一个用户首次登录时(必须是“登录”,例如通过getty或者DM,所以不包括su,具体要看/etc/pam.d里面哪些调用了pam_systemd.so了),都会启动一个systemd --user,他会将用户的default.target带起来,具体描述见wiki吧,这里只关心一点:它启动了用户级别的dbus。

DBus为进程间通信提供了一个“总线”(联想一下计算机网络中的总线型通信与p2p通信),其并非一个通用型的IPC(进程间通信)方案,而是有两个设计目的:

  • 系统级总线:让系统向用户会话发出消息、或者向用户会话询问输入
  • 用户级总线:用于实现桌面环境。至于怎么实现的,在之后的分析中自然会体现

反正D-Bus实际功能就是进程间通信,比如QQ来了消息,最后xfce4-notifyd这另一个进程收到并向屏幕显示通知内容,但是不同的桌面环境各有各的notify方式,QQ怎么知道连接到哪个进程;为此它其实连接到D-Bus然后D-Bus再将数据发给正确的进程。

D-Bus下的进程间通信

说了这么多,还是打开源码来看xfce4-session/main.c中的main函数部分吧:

int
main (int argc, char **argv)
{
  XfsmManager      *manager = NULL;
  GError           *error = NULL;

  if (!xfsm_dbus_require_session (argc, argv))
    return EXIT_SUCCESS;

第一步检测是否有Dbus在运行没有的话就跑一个;这个肯定是运行着的,systemd --user已经帮忙启动好了dbus。

D-Bus的概念与连接

d-spy是基于gtk4制作的D-Bus查看器。dbus-monitor和bustle工具可以监听D-Bus发生的消息(就像wireshark抓网络包那样)。前者是dbus自带的,后者基于gtk4,可以安装玩玩。安装了这些之后我们就可对D-Bus稍微研究研究了。当然dbus文档也是不可少的:https://www.freedesktop.org/wiki/IntroductionToDBus/

继续看xfce4-session的main函数,初始化gtk和xfconfd的先不管,接下来的是:

static void xfsm_dbus_init (XfsmManager **manager) {
  name_id = g_bus_own_name (G_BUS_TYPE_SESSION,
                            "org.xfce.SessionManager",
                            G_BUS_NAME_OWNER_FLAGS_NONE,
                            bus_acquired, name_acquired, name_lost,
                            manager,
                            NULL);

  if (name_id == 0)
    {
      g_warning ("Another session manager is already running");
      exit (EXIT_FAILURE);
    }
}

连接到D-Bus后,d-spy中的显示

这段代码描述的向DBus中的session bus建立连接的过程。关于函数本身的描述请看https://docs.gtk.org/gio/func.bus_own_name.html。首先,每个总线自身会有个地址,一般是个unix socket;当一个进程建立连接后,D-Bus首先会分配一个特别连接名(unique connection name),保证唯一性(此处的:1-5);然后进程还可以申请一个well-known connection name(org.xfce.SessionManager),一样的不能重复。

一个D-Bus的对象、接口、属性、信号、方法

从这个连接入手,可以了解更多dbus的相关概念,慢慢来看:

对象(Object)

指的是在总线上能够进行通信的一端。一个进程可能创建多个对象。对象的功能有:

  • 向其他对象发送请求。
  • 回复来自其他对象的请求。
  • 单方面发送一条消息,其余感兴趣的对象会收到。

大致来说,基于对象的通信分1-1 request-reply模式与 1-n publish-subscribe模式。

方法(Method)

当客户向对象发送请求时,dbus视为调用了目标对象的某某方法;该对象需要执行这个方法定义的某些行为。

方法可能包含参数列表与返回值,对应着请求携带的参数与回复的消息内容。如此一来进程间通信的运作方式就像是调用其他进程提供的方法一样。

虽然看着像是调用原生方法一样,但是实际进程不需要等待方法的返回值,可以在ipc期间做别的事,也就是所谓的异步等待。

信号(Signal)

用于一对多单向信息传递。客户端可以指定对某某对象的某某信号感兴趣。信号不接受回复。

接口(Interface)

方法与信号统称对对象的成员。一个对象的所有成员都在接口(interfaces)中定义。与 Java 语言中的接口类似,D-Bus 接口也是一组声明。"实现"某个接口意味着对象必须提供该接口中定义的所有方法,并允许监听其声明的信号。这些成员的参数(输入/输出)必须严格符合接口的规范。

和 Java 一样,任何对象都可以实现某个接口(多个不同对象可以实现同一个接口),同时,单个对象也可以实现多个接口(尽管在 D-Bus 中,完全不实现任何接口的对象可能没有意义,而这在 Java 类中是完全正常的)。对象支持的所有接口的组合称为该对象的类型(type)。

当客户端调用方法或监听信号时,必须指明目标对象(object)和成员(member)。此外,客户端还可以指定该成员所属的接口(interface),这在某些情况下是必要的。例如,如果一个对象实现了两个接口,而这两个接口都定义了一个名为 foo 的方法,那么对象可能会为这两个 foo 提供不同的实现。如果客户端在调用 foo 时不指定接口,就无法保证最终调用的是哪个 foo,甚至 D-Bus 实现可能会直接拒绝该请求。类似地,如果不指定接口监听信号,可能会误收到名称相同但实际不同的信号。早期版本的 D-Bus 甚至存在一个 Bug:如果请求或信号消息未指定接口,可能会导致消息丢失。

至于接口内部是否允许成员"重载"(即同一接口内是否允许存在多个同名的成员),这取决于具体的绑定(binding)实现。


一个进程会用到的dbus对象,以及对象们的成员都定义在xml文件中。对于xfce4-session那就是xfce4-session/xfsm-client-dbus.xmlxfce4-session/xfsm-manager-dbus.xml了。编译时它们会被编译成C文件然后随项目进行编译,语在meson.build

回到之前的代码,g_bus_own_name函数中传入了几个callback,主要看点在bus_acquire函数。首先创建一个XfsmManager对象:

static void
bus_acquired (GDBusConnection *connection,
              const gchar *name,
              gpointer user_data)
{
  XfsmManager **manager = user_data;

  xfsm_verbose ("bus_acquired %s\n", name);

  *manager = xfsm_manager_new (connection);

至此出现了C程序面向对象写法,这些命名规则以及整个面向对象机制都由GObject库定义与实现,感兴趣可自行探究,我之后可能会写一些记录一下。回到代码本身,首先类的继承关系:GObject->GDBusInterfaceSkeleton->XfsmDBusManagerSkeleton->XfsmManager。它的父类就是那个xfsm-manager-dbus.xml生成的c文件定义的。XfsmManager有义务实现xml中定义的各种接口(virtual xxx fn() = 0;之类的,当然是GObject的虚函数写法看着头疼),包括所有signal handler。在xml中定义的dbus signal会自动生成Glib.Signal的handler(作用自然是向dbus发送广播),而其他的method则需要子类手动实现每一个handleMethod了。调用其他对象的method大概也类似传出gsignal吧。再说吧。

关于GDBus编程详见https://docs.gtk.org/gio/migrating-gdbus.html。以及manpage故事man gdbus-codegen或https://gnome.pages.gitlab.gnome.org/libsoup/gio/gdbus-codegen.html。总而言之,dbus建立完成后,xfce4-session就进入了主循环。

D-Bus的ipc流程

按照dbus的理论,ipc过程就是dbus传来一个信号,然后触发相应的handler,这些handler的触发方式有:

  • 某Object拥有某Method,生成的代码中包含了触发handle_xxx_method信号的地方
  • 订阅了某个object的signal

额先看第二种,对于信号接收方而言,首先要订阅相关信号:

  manager->name_owner_id = g_dbus_connection_signal_subscribe (manager->connection,
    "org.freedesktop.DBus", "org.freedesktop.DBus", "NameOwnerChanged", "/org/freedesktop/DBus",
    NULL, G_DBUS_SIGNAL_FLAGS_NONE, on_name_owner_notify, manager, NULL);

几个字符串参数分别对应了发送信号的那个connection的名字、接口名字、信号名字、对象路径。然后接入一个callback函数。之后在收到消息后就会调用callback,没啥好说的。可能底层还能深挖一下,比如从poll到收到信号到调用callback的流程?

对于发送方就调用emit函数就行了:

xfsm_dbus_manager_emit_shutdown_cancelled (XFSM_DBUS_MANAGER (manager));

然后是第一种,这个就比较复杂了,考虑xxx有方法foo:

  • 首先有异步调用method的方法:xxx_call_foo, xxx_call_foo_finish
  • 然后有同步调用:xxx_call_foo_sync
  • 以及调用结束:xxx_complete_foo
  • 最后子类需要实现接口中的handle_foo方法

用锁屏举例子,在libxfce4ui/xfce-screensaver.c:

switch (i) {
  case SCREENSAVER_TYPE_XFCE:
    response = g_dbus_proxy_call_sync (saver->proxies[i], "Lock", NULL,
      G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);

在此之前的初始化:

saver->proxies[i] = g_dbus_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION,
  G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION,
  NULL, "org.xfce.ScreenSaver", "/org/xfce/ScreenSaver", "org.xfce.ScreenSaver",
  NULL, &error);

因为是调用其他进程的对象的方法,所以需要通过创建代理的方式进行。而方法的传递与处理自然就是dbus与gdbus的底层机制了,最后调用handle_method实现实际的逻辑,并通过xxx_complete_method发回返回值。

posted @ 2025-04-25 01:34  Notify-ctrl  阅读(67)  评论(0)    收藏  举报