HOME BLOG

写个 C 库 - part4

本文翻译自 Writing a C library part 4

作者:davidz

作者主页:https://www.blogger.com/profile/18166813552495508964

文章源地址:http://davidz25.blogspot.com/2011/07/writing-c-library-part-4.html

前篇:part one part two part three

1. 辅助设施(Helper)和守护进程

有时,程序或库调用外部进程完成工作是非常有用的。这样做的原因有很多 —— 例如,你想要使用的代码:

  • 可能在 C 语言中用起来不是很容易 —— 它可以用 python),或 gosh,或 bash) 写
  • 可能和信号处理程序或其他全局进程状态搞混
  • 不是线程安全的,或者会泄露或仅仅是膨胀(bloated)
  • 它的错误处理与你的库所使用的方式不兼容
  • 需要提权
  • 你感觉很糟糕,但是又不值得自己重新实现一遍

这里有三种方法来做这件事。

第一种方式是直接调用 fork(2) 并在子进程中使用这个库 —— 这通常是行不通的,因为存在着一种可能,那就是你使用的库在 fork() 之后无法可靠使用(就像之前讨论的那样)。(另外,如果父进程有太多的可写页面映射的话,可能会发生许多不必要的写入时复制 (COW))如果还要考虑到对 Windows 的移植性,这也不是一个可行的方法(non-starter),因为 Windows 上没有 fork() 或其他任何有意义的等价物。

第二种方法是编写一个小的辅助程序并随你的库一起发布。这种方法也使用了 fork() ,但是不同之处在于:子进程创建后会立即调用 exec(3),这样在替换进程映像时所有之前的进程状态会被清除(除了文件描述符,因为它们可以通过 exec() 继承,所以要注意不必要的泄露)。如果使用了 GLib,那就有一系列的实用函数(utility functions)来做这件事。(包括对自动关闭文件描述符的支持(automatically closing file descriptors))

第三种方式是让你的进程和长期存在(long-lived)的辅助进程进行交流 (所谓的守护进程(daemon))或后台进程)。辅助的守护进程既可以通过 dbus-daemon(1) 启动(如果你使用 D-Bus 作为 IPC 机制的话),或通过 systemd (如果你使用初始化脚本 Unix domain sockets)(uuidd(8) 被用来做这件事 —— 如果你的库不准备用的话那就是浪费了),或通过你自己的库。

辅助守护进程通常为库用户的多个实例提供服务,然而有时候最好每个库用户实例都有一个辅助守护进程实例。请注意,让一个库生成一个长期存在的进程通常是个坏主意,因为环境和其他的继承进程状态可能是错误的(甚至是不安全的)—— 要了解为什么需要一个好的、可知的、最小的和安全的工作环境,可以参见 Rethinking PID 1 。另一件很难做对(或者说是很容易做错)的事情是 单例模式(uniqueness)—— 例如,你最多只想要一个辅助守护进程的实例 —— 可以看看 Colin's notes for details and how D-Bus can be used 并注意像是 GApplication 的东西已经对单例内建了支持。同样的,在系统级的守护进程中,你可能需要对像是 loginuidexample of how to do this)之类的东西进行设置,来使得 auditing 之类的东西在为客户端提供服务时工作。(这与 Windows 中的 impersonation 有相似之处)

作为一个例子,GLib 的基于 libproxy 实现的 GProxy 使用了一个辅助守护进程,因为与 proxy servers 打交道涉及到解释 JavaScript ,为每一个想要进行连接的进程初始化一个 JS 解释器的开销太大,更不用说这样做所造成的污染了。(sourceD-Bus activation file - also note how the helper daemon is activated by simply creating a D-Bus proxy

如果辅助程序需要提权才能运行,使用像是 PolicyKit 的框架是很方便的(对于检查使用你的库的进程是否被授权),因为它和 desktop shell (以及 console/ssh logins )整合的很好。如果你的库只是使用了短时存在(short-lived)辅助程序,那就更简单了:直接使用 pkexec(1) 命令来启动你的辅助程序(examplepolicy file

顺便说一句(因为本文是关于 C 库的,而不是软件体系结构的),现在的 Linux 桌面中的许多子系统是以系统级守护进程实现的(通常以提权状态运行),它们主要使用的 API 是 D-Bus API(example)和 C 库,来访问根本不存在的(应用程序随后使用一般的 D-Bus 库或像是 gdbus(1)dbus-send(1) 之类的工具),或从类似 IDLD-Bus XML 定义文件(example)生成(generated)的功能。将这种方法与使用辅助程序的库进行比较是很有用的,因为一种方法与另一种相比总是或多或少的颠倒的。

1.1. 清单

  • 确认是否需要一个辅助程序
  • 可能的话,为辅助守护进程的单例模式使用 D-Bus (或相似的东西)
  • 通过 D-Bus 协议与辅助程序通信(而不是自定义的二进制协议)会添加一个安全层,因为信息的内容会被检查
  • 通过 message bus 路由(router)使用 D-Bus(而不是对等(peer-to-peer)连接)又会添加一个安全层,因为两个进程通过中间路由进程连接(一个dbus-daemon(1) 实例),该进程也会验证消息,并且会断开发送垃圾消息的进程
  • 因此,如果辅助程序是提权的(这就意味着它必须 a) 将使用它的应用程序/库视作不受信任的并且是可能受损的;和 b) 验证所有数据 (see Wheeler's Secure Programming notes for details)),在 D-Bus 系统中激活一个辅助守护进程通常比你自己使用 setuid root 生成的辅助进程要好
  • 可能的话,尤其是如果你在编写在 Linux 桌面使用的代码,使用 PolicyKit(或类似的东西)来检查未提权的代码是否被授权来执行请求的操作

2. 测试

当库和应用带着测试套件(test suite)发布时,这通常是一个成熟的标志。一个好的测试套件对于确保发行内容大部分无 bug 是很有用的,更重要的,确保维护人员可以放心地发布发行版,而不会丧失睡眠或理智。对测试细节的讨论超出了这个 C 库系列的讨论范围,不过指出 GLib test framework 的用法 (example,example and example) 和它是如何被 GNOME buildbots 使用的是很有价值的。

评价测试套件好坏(或至少有多广泛)的一个指标是,它覆盖了多少代码 —— 对于这一点,可以使用 gcov 工具(notes on how this is used in D-Bus)。特别地,如果测试套件没有覆盖一些边沿情况,用于处理边沿情况的代码路径将显示为从未执行。或是如果代码处理了 OOM,但是测试套件没有被设置为处理它(例如,使每一个分配失败(by failing each allocation))处理 OOM 的代码路径应该表现为未测试。

创新的测试方法通常会有所帮助,例如,Mozilla 使用了叫做 reftests 的技术(notes on GTK+ reftests),Dracut test suiteclient and server 使用了 VMs 来测试从 iSCSI 启动是否有效。

2.1. 清单

  • 尽早开始编写测试套件
  • 使用像是 gcov 的工具来估计测试套件的好坏
  • 经常运行测试套件 —— 理想条件下把它集成到构建系统、发布产品和版本控制中,等等