设计D-Bus server时要注意的若干问题
- Server的定义
- 精灵化,后台化
- 事件循环和电量消耗
- 支持并行处理能力
- 调试
Server的定义
就软件而言,server一般理解成:能为client提供服务的软件组件。在Linux中,server一般是daemon的形式运行的。Daemon 是一个术语:daemon是同terminal剥离的这么一个进程,一般运行于后台,处理一些用户不需要看见的动作。
有时候,你可能会听见人们把server称作engine。Engine是个更通用的一个术语,Engine和如何实现一个服务没有太大的关系(Engine可以单独作为一个进程;可以作为一个库,由client直接调用)。更广泛点讲:Engine是应用程序的一部分,主要是实现部分功能,但不是interface。在Model-View-Controller模型中,Engine相当于Model。
目前为止,我们的server程序并不是以deamon的形式运行的,主要是想在terminal/屏幕上看到更多的打印信息。如果我们运行server程序时,加个参数:"--stay-on-foreground" ,这样,我们的server就不会被“精灵化”,而保持在前台运行。对于调试,这是非常重要的。
默认情况下,一个server程序被daemonize后,它的输入/输出文件就会关闭了,这时再从terminal读取用户输入,或者写数据到terminal都会失败(包括printf和g_print)。
Daemonization(精灵化)
把一个进程process转化为一个后台daemon的目的就是想把它同其父进程剥离开,并为它单独创建一个会话。这样剥离是有好处的:server的父进程不会自动关闭server了。把一个process变成daemon的过程如下:
- fork 一个新的子进程,原进程推出,子进程进入init process中;
- 用setsid为子进程创建一个新的会话;
- 从当前工作目录切换至root目录,因此daemon不会阻止文件系统的正常卸载;
- 对daemon创建的目录和文件设置umask值,不如其他进程访问。在Internet Tablet产品中,没有这样做,因为只有一个用户;
- 关闭所有的标准I/O文件描述符,为了在terminal关闭时不致于产生SIGPIPE信号给daemon。并且把所有的stdin, stdout, stderr只想/dev/null,这样所有的打印将会丢失掉。
函数daemon可以让你切换目录并且关闭已经打开的文件描述符:
#ifndef NO_DAEMON
/* This will attempt to daemonize this process. It will switch this
process' working directory to / (chdir) and then reopen stdin,
stdout and stderr to /dev/null. Which means that all printouts
that would occur after this, will be lost. Obviously the
daemonization will also detach the process from the controlling
terminal as well. */
/* daemon函数用于把一个进程daemon化,它主要做2件事:1 切换当前目录到root目录;2 把stdin,stdout,stderr定位到/dev/null; 很明显,daemon处理后,当前进程就从其控制terminal剥离开了。D-Bus的daemon化,不是调用daemon函数的,而是遵循了上面的步骤,一步步搞的。*/
if (daemon(0, 0) != 0) {
g_error(PROGNAME ": Failed to daemonize.\n");
}
#else
g_print(PROGNAME
": Not daemonizing (built with NO_DAEMON-build define)\n");
#endif
Makefile控制是否daemon处理:
# -DNO_DAEMON : do not daemonize the server (on a separate line so can
# be disabled just by commenting the line)
ADD_CFLAGS += -DNO_DAEMON
# Combine flags
CFLAGS := $(PKG_CFLAGS) $(ADD_CFLAGS) $(CFLAGS)
如果不想修改makefile,而编译出不同的版本时,可以采用-U选项:
[sbox-CHINOOK_X86: ~/glib-dbus-sync] > CFLAGS='-UNO_DAEMON' make server
dbus-binding-tool --prefix=value_object --mode=glib-server \
value-dbus-interface.xml > value-server-stub.h
cc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include
-I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include
-g -Wall -DG_DISABLE_DEPRECATED -DNO_DAEMON -UNO_DAEMON
-DPROGNAME=\"server\" -c server.c -o server.o
cc server.o -o server -ldbus-glib-1 -ldbus-1 -lgobject-2.0 -lglib-2.0
gcc是从左到用处理所有的-D,-U选项的,加上CFLAGS='-UNO_DAEMON',会把makefile中定义的宏变量取消掉。当然,你也可以直接修改makefile.
下面我们去掉(&), 直接运行:
[sbox-CHINOOK_X86: ~/glib-dbus-sync] > run-standalone.sh ./server
server:main Connecting to the Session D-Bus.
server:main Registering the well-known name (org.maemo.Platdev_ex)
server:main RequestName returned 1.
server:main Creating one Value object.
server:main Registering it on the D-Bus.
server:main Ready to serve requests (daemonizing).
[sbox-CHINOOK_X86: ~/glib-dbus-sync] >
由于server的信息看不到,我们用别的方法去查看server是否正在运行:
[sbox-CHINOOK_X86: ~/glib-dbus-sync] > ps aux | grep "\./server" | grep -v pts
user 8982 0.0 0.1 2780 664 ? Ss 00:14 0:00 ./server
上面grep的用法稍微有点费解。
事件循环和电量消耗
现在大部分的CPU都有多级省电的方式可选。每一级省电的能力各不相同,主要是考虑到价格的因素在内。对手机来说,能做好省电这块工作,很有意义:增加待机时间,这也是消费者比较关心的问题。
频繁的切换CPU的状态会耗电,要避免这样做。为了省电,我们要尽量地减少CPU的高负荷运转。
如何减少CPU的调用呢?这里我们先比较两种编程方法:基于事件编程和基于轮询(polling)编程。轮询方式:我们持续检查(轮询)我们关系的事件,只有在相关事件发生时,才采取动作。由于持续的检查,CPU也会频繁的工作,我们需要尽力避免这个方案。
相对于轮询方式,基于事件的编程不需要单独的polling循环,至于当关心的事件发生时才调用回调函数。这就有个问题:何时激活回调。使用定时器看起来是个简单的办法。定时去查询我们关心的状态。这种模式也不好,由于频繁的使用定时器,导致CPU不能够进入深睡眠模式。
现在大部分操作系统内核都提供了唤醒机制:当所需数据可用时,唤醒该进程,否则退出当前运行队列。在Linux系统中,最常用的就是围绕select/poll的机制。在Linux中,很多介质都被称作“file”。如果你的程序使用Glib(或者GTK)编程,你可以直接使用GMainLoop, 这个对象内部已经封装了事件机制(select/poll/其它),你所需要做的就是给特定的事件注册回调;启动主循环等。
如果你使用了一些文件描述符(network sockets, open files, etc), 你可以通过GIOChannels把它们集成到GMainLoop中。
对于使用定时器的回调函数,你要注意避免下面的问题:
- 长时间(> 5秒)使用频率过高(> 1 HZ)的定时器;
- 当某个事件发生时再触发其回调函数。不要自己手动去轮询那个事件。
举个例子:前面我们介绍过LibOSSO的一个程序:FlashLight,这个程序使用定时器来保持背光。不过,那个定时器很慢(45秒),因此,对于CPU的影响不是什么大的问题,不至于对电池电量造成什么太大的负面影响。
对于程序可能有很多界面,当由很多程序同时运行时,处于后台的程序不必老是更新,只更新前台(top)的程序界面,这样处理会更省电。LibOSSO支持这种处理方式。
另外一方面,由于server并没有界面,并不适合上面的处理方法。当server没有任何处于活动状态的client时,避免使用定时器。只有client和server建立了connection, server再去使用相关的资源,当完成操作后,把相关资源释放。
如果可能的话,尽量使用system bus的D-Bus信号来关闭一些活动。即使你处理的是一个无界面的server,也要监听系统关闭的信号,然后能安然关闭你的server,不至于导致一些崩溃。
总之,设计一个低耗电的产品(或者server)并不是一件简单的事情,有4个原则可以遵守:
- 尽量避免做额外的工作
- 尽快做完
- 尽可能少地调用它
- 只要必须的资源,其它的不必多揽
对于GUI程序来讲,还要考虑“图形侧”的问题,这也是耗电的大户,尤其是刷新大屏。
支持并行请求操作
前面的例子中,带有延时的server有个主要的缺点:一次只能处理一个请求,其它的请求都要在外面等待。这样就会出现新的问题:一个server有不同的client, 并且同时访问server,这个问题怎么解决呢?
通常情况下,通过在消息分发机制(这里是libdbus)基础上增加一些复用机制来实现对并行操作的支持。大概有下面三种模型:
- 1 为每个request启动一个单独的线程去处理它。这种方法看起来很简单,但是如何处理多个线程访问共享资源?而且多线程的调试更困难点。
- 2 使用一个基于事件驱动的模型,来同时处理多个事件源,并且只有一个事件处于唤醒状态。在这种情况下,“select”和“poll”是最常用的。基于事件的方法比基于线程的方法要好:因为它不涉及到同步的问题,只需要在kernel和用户空间之间切换上下文。Glib在基于事件编程模型基础上做了封装,提供GMainLoop对象来处理事件。你可以使用GIOChannel对象来表示一个事件源,并且给它注册回调函数。
- 3 使用fork去创建一份server进程的拷贝进程,这个新的进程仅处理一个request,并且在处理完后,退出该进程。这个方法所面临的问题:要额外创建进程、进程之间共享数据的同步。因此你必须要做到进程之间的同步。许多静态的web服务器使用这种模型,因为他们不必去共享数据。
引起server比较慢的原因在于:Glib/D-Bus本身不能直接支持并行处理。即使使用fork模型,也会面临一个问题:多个进程访问同一个D-Bus connection。因此,你如果使用Glib/D-Bus,你需要自行把你自己的“复用”代码加入。
这种情况下,拿上面任何一种方法,并结合libdbus。这需要重写server,暂时舍弃GType, 可能也会创建出一个轻量级的实现:把libdbus函数集成到Glib GMainLoop中。不支持GType, 意味这你必须自己实现自省功能。
另外一种方案:伪造client调用的完成。这样RPC方法可以立马完成,但是server侧仍然在继续处理。这个方案的困难之处:很难知道哪个client对应于哪个响应。这样的话,server需要把处理后的结果用信号广播给所有的client, 这样会吧所有的client都唤醒的,即使大部分对该信号并不关系。
一句话,当你使用Glib/D-Bus时,没有简单的方案来处理并行操作。
调试
调试你的server最简单的方法就是使用打印。在Maemo中,如果server不是以deamon的形式运行的,则打印出关系的信息,如果是以daemon的形式运行的,这些打印信息不会打印。你可以把这个idea扩展:支持多级打印。
宏_func__和__file__比较有用:
/* A small macro that will wrap g_print and expand to empty when
server will daemonize. We use this to add debugging info on
the server side, but if server will be daemonized, it doesn't
make sense to even compile the code in.
The macro is quite "hairy", but very convenient. */
#ifdef NO_DAEMON
#define dbg(fmtstr, args...) \
(g_print(PROGNAME ":%s: " fmtstr "\n", __func__, ##args))
#else
#define dbg(dummy...)
#endif
在Maemo中用的打印方法还是比较简单的,我前面的一些文章对这方面做了些总结:
发表于 @ 2008年02月26日 21:34:00|评论(loading...)|编辑