IPC 是 Linux 编程中一个重要的概念,IPC 有多种方式,本 IPC 系列文章的前十篇介绍了几乎所有的常用的 IPC 方法,每种方法都给出了具体实例,前面的文章里介绍了 D-Bus 的基本概念以及调用远程方法的实例,本文介绍 D-Bus 的异步处理机制,以及信号处理的基本方法,本文给出了异步处理 D-Bus 的实例,附有完整的源代码;本文实例在 Ubuntu 20.04 上编译测试通过,gcc版本号为:9.4.0;本文不适合 Linux 编程的初学者阅读。
在阅读本文之前,建议阅读关于 D-Bus 的另一篇文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》
在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中,介绍了服务端如何在 D-Bus 上提供方法调用服务以及客户端如何向服务端请求一个方法调用服务;
通过 D-Bus 向服务端请求方法调用服务,仅仅是 D-Bus 一半的功能,D-Bus 还支持异步的广播通信方法,这种机制称为信号(Signal),当服务端需要向大量接收者发送通知时,该机制非常有用;
举例来说,如果系统正在关闭、网络连接中断以及类似的系统范围内的情况,相关系统服务进程应该广播一个通知,使对这些服务有需求的进程能够及时做出反应,这样一种方式,使得接收信号的进程无需轮询服务状态;
D-Bus 的信号(Signal)与调用方法(Method Call)有许多类似的地方,这里简要回顾一下在上一篇文章中讨论的调用方法的概念:
信号(Signal)也是建立在一个接口下,一个接口下不仅可以有一个或者多个方法,还可以有一个或者多个信号,
所以其实一个接口下可以有若干个方法和信号,除此之外,接口下还可以有若干个属性(Properties),方法、信号、属性组合在一起构成一个接口;
本文仅讨论接口,有关属性的事情,以后的文章中再讨论;
发送信号通常是服务端的事情,信号通常是以广播的方式发出,而只有订阅了这个消息的客户端才能收到消息,实际上,信号也是可以点对点发送的(仅发给指定客户端),这个以后讨论,先讨论通常的广播信号;
服务端发送信号的步骤
dbus_bus_get()
连接到 D-Bus,获得一个连接 DBusConnection;dbus_message_new_signal()
为构建信号初始化一个 DBusMessge;dbus_message_append_args()
将信号参数添加到信号的 DBusMessage 中;dbus_connection_send()
将信号放入发送队列;dbus_connection_flush()
将发送队列的消息全部发送出去;dbus_message_unref()
释放信号的 DBusMessage;整个过程与在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中描述的客户端向服务端请求一个服务的过程高度相似,但要简单一些:
客户端要接收到信号,需要订阅指定的信号,D-Bus 只会把你订阅的信号推送过来;
使用 dbus_bus_add_match()
订阅信号:
void dbus_bus_add_match(DBusConnection *connection,
const char *rule,
DBusError *error);
dbus_bus_get()
获得的连接;key/value
的形式描述,可以有多个 key/value
对用于描述多个条件,每个 key/value
对用 “,” 分隔;"type='signal',sender='cn.whowin.dbus', path='/cn/whowin/dbus', interface='cn.whowin.dbus_iface',member='notify'"
type='signal'
表示消息类型为信号,sender 是发送方的总线名称,path 是发送方的对象路径,interface 是发送方的接口名称,member 是信号名称,D-Bus 会把符合这些条件的信号推送到订阅的进程中;"type='signal',sender='cn.whowin.dbus',path='/cn/whowin/dbus'"
,则从 cn.whowin.dbus
的对象 /cn/whowin/dbus
发出的消息都可以收到;dbus_bus_add_match()
后会立即返回,不会产生阻塞,但是订阅不会生效,需要执行 dbus_connection_flush(conn)
后订阅才会生效,而且如果发生了错误程序也是无法知晓的,所以,不建议这样做;key/value
对中的 key 可以为:type, sender, interface, member, path, destination;:275.6
,在广播信号中通常用不上;DBusError dbus_error;
DBusConnection *conn;
dbus_error_init(&dbus_error);
conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);
dbus_bus_add_match(conn, "type='signal',path='/cn/whowin/dbus/signal',interface='cn.whowin.dbus_iface'", &dbus_error);
......
如果有必要,你可以使用 dbus_bus_add_match()
订阅多个信号。
客户端并不知道什么时候会有信号发出来,所以为了能及时收到信号必须不断轮询,像这样:
DBusConnection *conn;
DBusMessage *message;
......
while (dbus_connection_read_write_dispatch(conn, -1)) {
// loop
message = dbus_connection_pop_message(conn);
if (message == NULL) {
usleep(10000);
continue;
}
if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_SIGNAL) {
usleep(10000);
continue;
}
......
}
函数 dbus_connection_read_write_dispatch()
在文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中做过介绍;
显然,这样的编程模式并不高效,尤其是当程序不仅仅是要接收信号,还有其他工作要做时,这种程序架构就显得更加不可接受;
实际上,libdbus 还提供了另外一种异步接收信息的方式,像下面这样的代码:
DBusHandlerResult signal_filter(DBusConnection *connection, DBusMessage *message, void *usr_data) {
DBusError dbus_error;
dbus_error_init(&dbus_error);
if (dbus_message_get_type(message) != DBUS_MESSAGE_TYPE_SIGNAL) {
printf("Client: This is not a signal.\n");
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
......
return DBUS_HANDLER_RESULT_HANDLED;
}
int main() {
DBusError dbus_error;
DBusConnection *conn;
dbus_error_init(&dbus_error);
conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error);
dbus_connection_add_filter(conn, signal_filter, NULL, NULL);
dbus_bus_add_match(conn, "type='signal',path='/cn/whowin/dbus/signal',interface='cn.whowin.dbus_iface'", &dbus_error);
while (dbus_connection_read_write_dispatch(conn, -1)) {
/* loop */
......
}
......
return;
}
dbus_connection_add_filter()
添加了一个过滤器(Filter),然后再用 dbus_bus_add_match()
订阅感兴趣的信号;signal_filter()
中处理信号;函数 dbus_connection_add_filter()
原型
dbus_bool_t dbus_connection_add_filter(DBusConnection *connection,
DBusHandleMessageFunction function,
void *user_data,
DBusFreeFunction free_data_function
)
dbus_bus_get()
获得的连接;DBusHandleMessageFunction
的定义
typedef DBusHandlerResult(* DBusHandleMessageFunction)(DBusConnection *connection,
DBusMessage *message,
void *user_data);
dbus_connection_add_filter()
中,function 是一个函数指针,该函数将接收三个参数,第一个是从 dbus_bus_get()
获得的连接,第二个参数是一个消息结构 DBusMessage
,表示收到的消息,第三个参数是用户数据,在使用 dbus_connection_add_filter()
添加过滤器时设置;DBUS_HANDLER_RESULT_HANDLED
、DBUS_HANDLER_RESULT_NOT_YET_HANDLED
和 DBUS_HANDLER_RESULT_NEED_MEMORY
;DBUS_HANDLER_RESULT_HANDLED
表示该过滤器函数已经获得了一个有效消息并进行了处理,该消息无需再交给其他过滤器处理;DBUS_HANDLER_RESULT_NOT_YET_HANDLED
表示该过滤器没有处理该消息,如果有其他过滤器,可以把该消息交给其他过滤器处理;DBUS_HANDLER_RESULT_NEED_MEMORY
通常用不上;调用过滤器函数是由 libdbus 实现的,应该是在调用 dbus_connection_read_write_dispatch()
时,当有可读消息时,调用过滤器函数;
调用过滤器函数后的返回值并不会返回到应用程序中,但是对其它过滤器可能会产生影响,当系统内有多个过滤器时,当前过滤器返回
DBUS_HANDLER_RESULT_HANDLED
意味着已经处理好了这个消息,不必再使用其它过滤器处理该消息;DBUS_HANDLER_RESULT_NOT_YET_HANDLED
意味着这个消息没有在当前过滤器中被处理,如果有其它过滤器,应该尝试使用其它过滤器处理;所以,过滤器函数的返回一定要正确,否则会有消息丢失;
如果有必要,你可以添加多个过滤器,去处理不同的消息;
过滤器的概念,其实也不仅仅可以用在接收信号上,也可以用在调用方法上;
尽管我们向 D-Bus 订阅了我们感兴趣的信号,但其实有时也会一些不符合订阅条件的信号到来,所以,在程序中还是要做一些判断,以确保收到的是我们期望的信号,如果不是,返回 DBUS_HANDLER_RESULT_NOT_YET_HANDLED
,让其它过滤器去处理。
源程序:dbus-signals.c(点击文件名下载源程序,建议使用UTF-8字符集)演示了使用 libdbus 对信号进行发送和接收,以及如何异步接收信号;
该程序是一个多进程程序,建立了一个服务端进程和三个客户端进程;
服务端进程在启动后发送出一个内容为 “start” 的信号,暂停 5 秒后,再发出一个内容为 “quit” 的信号,然后退出进程;
服务端在发送信号时,其对象路径、接口名称和信号名称均相同;
客户端进程订阅了服务端的信号,并添加了两个过滤器,一个用于处理内容为 “start” 的信号,另一个用于处理内容为 “quit” 的信号,这里仅是为了演示多个过滤器的工作方式;
客户端检查收到的信号,如果其内容为 “quit”,则退出进程;
编译:gcc -Wall -g dbus-signals.c -o dbus-signals `pkg-config --libs --cflags dbus-1`
有关 pkg-config --libs --cflags dbus-1
可以参阅文章 《IPC之十一:使用D-Bus实现客户端向服务端请求服务的实例》 中的简要说明;
运行:./dbus-signals
运行截图:
程序运行后,客户端进程的两个过滤器都显示了 “Wrong object path” 的信息,这条信息是 D-Bus 为客户端连接分配了名称后发送过来的通知信号,虽然我们没有订阅,但 D-Bus 会强行推送过来;
这条通知信号在经过过滤器时,过滤器返回了 “DBUS_HANDLER_RESULT_NOT_YET_HANDLED”,因为这个返回值导致这个消息在经过第一个过滤器后还会再进入第二个过滤器进行处理,如果过滤器在遇到对象路径不对时返回 “DBUS_HANDLER_RESULT_HANDLED”,则这条消息不会再去第二个过滤器,读者可以尝试修改程序看看是不是这样;
如果你多次运行这个程序你会发现,信号总是首先到达 signal_quit()
过滤器,然后才到达 signal_start()
过滤器,这是因为我们先添加的 signal_quit()
过滤器,如果你改动一下程序,先添加 signal_start()
过滤器,再添加 signal_quit()
过滤器,你会看到信号到达的顺序也会发生变化。