Linux 电话内核 API
一年前,互联网电话还是一种新鲜事物,许多人认为它永远无法用于真正的电话通话。现在,随着 Net2Phone、Deltathree.com 和 DialPad 等服务通过互联网提供免费或极低价格的电话通话,IP 语音 (VoIP) 已接近主流地位。虽然这些服务的 Linux 客户端尚未面世,但 Linux 并未落后。凭借 2.2.14 内核,Linux 在计算机电话集成领域取得了大胆的领先地位:我们拥有第一个现代操作系统,它具有定义的内核层应用程序编程接口 (API),用于电话支持。更令人欣喜的是,优秀的开源电话软件已经在使用这个 API。您可以使用 Linux 和互联网在世界各地通话——而且通话是免费的!
本文将解释电话设备驱动程序如何集成到内核中的基本原理,以便为不同供应商创建通用的 API。然后,我们将讨论 API 设计和功能背后的基本思想,以及如何分别处理数据和事件信息。最后,我们将讨论如何在称为“异步事件通知”的过程中处理电话事件(如响铃或拿起听筒)。
许多人问过,为什么我们需要一个新的内核电话支持 API。即使是 Alan Cox 也必须被说服!毕竟,大多数互联网电话软件都可以使用声卡,如果该声卡支持全双工(同时播放和录制),并且用户拥有体面的麦克风扬声器耳机,则通话质量可以令人满意。为什么要增加复杂性和新的 API 呢?
答案很简单:声卡不是电话设备!声卡无法生成拨号音、铃声、Wink、Flash-Hook 或来电显示——所有这些都是正常操作电话设备所必需的。是的,声卡对于使用耳机和麦克风的有限电话用途是有效的,但真正的电话卡允许您插入漂亮的无线电话,并从连接到计算机的短线中解放出来……或者插入复杂的商业电话系统。声卡也无法连接到本地电话公司提供的电话线,因此失去了进行呼入和呼出呼叫控制、跳入和跳出长途旁路应用程序或使用当今正在编写的任何其他新一代电话软件应用程序的能力。
此外,对于任何真正的基于互联网的 VoIP 用途,都必须压缩音频数据。为了与其他 VoIP 应用程序和设备兼容,您必须支持常用的压缩编解码器,如 G.723.1 或 G.729a。不幸的是,如果您想在软件中执行此操作,您必须获得编解码器的许可,这可能会花费六位数的金额(很容易!),这些库肯定不是开源的。电话卡(如下面提到的 Quicknet 卡)已经预先获得了这些编解码器的许可,并将它们内置到硬件中。这避免了为编解码器支付按副本版税的麻烦(它是硬件成本的一部分),并允许您使用自己选择的许可证开发代码,而不是编解码器开发人员强加给您的许可证。换句话说,您可以进行开源 VoIP 软件开发,并且仍然可以使用高级编解码器!这些编解码器绝对不是您在声卡上能找到的东西。
此外,声卡无法与电话或电话系统接口,也无法支持基于硬件的音频压缩编解码器。声卡与电话卡根本不同。
公平地说,电话卡也不是声卡。声卡具有远远超出电话功能的音频功能。例如,声卡是立体的;电话是单声道的。声卡可以以音乐频率 (20Hz-20kHz) 对声音进行采样和回放;电话通常仅限于语音频率 (300Hz-4kHz)。声卡具有高级音乐功能,通常支持 MIDI 和广泛的声音效果。电话卡无法做到任何这些。硬件和功能非常不同。设备驱动程序和 API 也需要不同。
但是等等,您可能会说,“所有出色的软件都可以与声卡配合使用,例如语音识别和文本到语音处理?” 如果能够以最小的努力在电话设备上使用该软件,岂不是很好? 您可以。Linux 电话 API 的设计方式使其不排除将最初为声卡设计的软件与电话设备一起使用。是的,需要进行一些小的代码更改,但这并不难。我将在本文末尾详细解释这一点。
推动这个新 API 的先锋是 Quicknet Technologies, Inc.,该公司生产一系列电话卡。1999 年 11 月,Quicknet 的 Ed Okerson 和我本人(当时我是 Quicknet 的员工)向 Alan Cox 提出了当今 API 的前身。经过几周紧张的电子邮件往来以及 Ed 和 Alan 的大量代码,Linux 电话 API 诞生了。
那么,它是如何工作的呢? 让我们开始吧。
在操作系统级别,所有设备都通过数字来引用。我们查看 /dev 并看到一系列文件名,但实际上 Linux 根据主设备号和次设备号来查看设备。特定类型的设备都共享一个主设备号;该类型的各个设备必须各自具有自己的次设备号。例如,如果您执行 ls -al /dev/ttyS0,您将看到
gherlein@tux:~/lj > ls -al /dev/ttyS0 crwxrwxr-x 1 root uucp 4, 64 Oct 27 06:23 /dev/ttyS0
请注意,文件权限掩码的第一个字符是“c”,表示它是一个字符设备。它归 root 所有,并且在 uucp 组中。接下来的两个数字不是文件大小,就像您在普通文件上看到的那样;它们是主设备号和次设备号。在本例中,ttyS0 的主设备号为 4,次设备号为 64。
Linux 电话 API 将主设备号 100 分配给电话类型的设备。您的 Linux 发行版可能没有像为 /dev/ttyS*、/dev/audio 和其他较旧的、普遍接受的设备映射那样为您创建设备。如果您的系统上不存在 /dev/phone* 设备,您需要在使用 Linux 电话 API 之前创建它们。您可以使用以下命令(以 root 身份)快速自行修复此问题
mknod /dev/phone0 c 100 0
这将创建一个名为 /dev/phone0 的新设备文件。它是一个字符设备,主设备号为 100,次设备号为 0。有关此命令的更多详细信息,请参阅 mknod 手册页。您实际上只需要足够的设备文件来满足您的硬件需求,但大多数人默认创建设备 0-15。
请注意,文件 /usr/src/linux/Documentation/devices.txt 中目前存在错误。该文件提供了所有主设备号的官方分配,它目前声明 Linux 电话应使用主设备号 159,而 100 已被弃用。这是一个公认的错误,将在未来的文档中修复。Linux 电话的正确主设备号(以及内核中使用的号码)是 100。
Alan Cox 基于他对 Video4Linux 项目采用的类似方法开发了 phonedev 模块。将会有许多供应商创建能够作为电话设备的产品。与其让多个电话产品供应商各自需要自己的主设备号,不如让他们都使用主设备号 100 和通用定义的 /dev/phoneN(其中 N 是设备号)。
所有这些都必须向用户空间软件呈现一个通用的基本接口,也就是说,它们都必须遵循相同的通用 API,尽管它们可能会提供特定于其产品的扩展。供应商将创建自己的设备驱动程序模块,该模块将通过处理其特定硬件的内部细节来实现此通用 API 作为外部接口。
phonedev 模块解决了在运行时将次设备号映射到特定供应商类型模块的问题。源代码位于 files/usr/src/linux/drivers/telephony/phonedev.c 中,头文件 phonedev.h 和 telephony.h 位于 /usr/include/linux 中。它的工作原理如下。
每个电话设备都必须使用 phone_device 结构(参见列表 1)。同样,每个电话设备都必须调用两个函数来与 phonedev 模块交互,以便注册和注销自身。这些函数定义为
extern int phone_register_device (struct phone_device *, int unit); extern void phone_unregister_device (struct phone_device *);
在加载时,phonedev 模块会自行设置并等待服务其他电话设备。当加载电话驱动程序时(通过 modprobe 或 insmod,稍后讨论),它会调用 phone_register_device 函数。对此函数的简单解释是,它在 phonedev 模块中保留一个指向 phone_device 结构的指针,搜索第一个打开的次设备号并将其分配给电话设备,然后递增一个计数器来跟踪正在使用 phonedev 模块的事实(以防止在使用时卸载它)。
这在实践中的含义很简单:首先加载的电话模块将被分配第一个可用的(编号最小的)次设备号。这对于需要来自不同供应商的模块在同一系统上共存的情况至关重要,并且希望特定卡的特定分配与特定的次设备号(特定的 /dev/phoneN 设备)相匹配。换句话说,如果您有来自 XYZ 公司的设备和来自 ABC 公司的设备,并且您希望 ABC 公司的卡成为 /dev/phone0,您必须确保 ABC 公司的驱动程序首先加载。
所有设备都必须至少提供与设备交互的基本功能:打开、读取、写入、关闭等。这些都是“文件操作结构”的一部分(有关详细信息,请参见 linux/fs.h)。每个设备都定义了适合自身的功能。
任何时候程序打开 /dev/phoneN 设备时,它实际上都在调用 phonedev 模块的文件操作结构中定义的 fopen 函数。此函数执行以下操作
它从 /dev/phoneN inode 中获取次设备号。
它使用主设备号和次设备号构建一个 char-major-%d-%d 形式的字符串。对于次设备号 0(对应于 /dev/phone0),此字符串将为 char-major-100-0。
它在对 request_module 的调用中使用此字符串,以请求加载模块。这具有与程序 modprobe 被调用相同的效果(实际上,它实际上启动了一个单独的内核线程并在其中执行 modprobe)。这确保了,如果设备是一个模块而不是内核的一部分,kmod 有机会在 phonedev 尝试使用它之前加载模块。
然后,它调用电话设备模块中的 fopen 函数来执行打开单个设备的实际操作。
如您所见,phonedev 模块有两个基本目的:在加载时动态地为电话设备分配次设备号,并提供一种干净的方式来在运行时自动加载所需的电话设备模块。但是,很明显,要完全理解电话模块及其交互,需要对 modprobe 和模块依赖性有很好的理解。
如果您正在使用 kmod 来在需要时自动加载内核模块,那么您将需要确保在 /etc/modules.conf 文件中定义了多个别名。
首先,系统需要知道 char-major-100 是 phonedev 模块。添加此行以定义
alias char-major-100 phonedev
现在系统将需要知道将哪些实际电话设备模块与特定的次设备号关联。正如我们在上面了解到的,这可能无法保证 phonedev 模块实际上在模块加载时将该次设备号分配给了电话设备,但我们将在下面更详细地介绍这一点。在以下示例中,我们将使用 Quicknet Technologies Inc. (www.quicknet.net) 的电话卡。这些卡在内核中具有 Linux 驱动程序,并且与大多数电话卡相比相对便宜。Quicknet 的设备驱动程序是 ixj.o,模块名称是 ixj。此驱动程序用于他们的所有电话卡产品(它足够智能,可以处理 ISA、PCI 或 PCMCIA 类型,并了解哪些卡具有哪些类型的电话接口电路)。要定义 Quicknet 的驱动程序与 /dev/phone0 关联,请将此行添加到 /etc/modules.conf
alias char-major-100-0 ixj正如您从上面对 phonedev 的 fopen 函数的讨论中回忆起的那样,phonedev 将构建一个 char-major-%d-%d 形式的字符串,并用 100(主设备号)和请求的次设备号填充参数。在我们的示例中,尝试打开 /dev/phone0 将导致 phonedev 尝试加载 char-major-100-0。内核模块加载器不知道该设备。上面的别名命令将该字符串 映射 到模块名称 ixj。当我们尝试打开 /dev/phone0 时,phondev 模块将为我们自动加载 ixj 模块,然后调用 ixj 模块中定义的 fopen 函数来打开设备(假设您的内核构建为支持内核模块加载器,CONFIG_KMOD=y)。
具体来说,什么是音频数据,它与事件数据有何不同?音频数据是电话设备麦克风上的音频信号进行模数 (A/D) 转换(并可能进行数据压缩)的结果。摘机信号(由拿起电话听筒引起)是一个 事件。用户在电话听筒上按下数字键产生的音调是一个事件,即使该操作也可能生成音频。传入的振铃信号称为事件。简而言之,所有非麦克风输入都是事件。所有麦克风输入(以及相应的扬声器输出)都是音频数据。此音频数据是使用标准读取和写入系统调用从电话设备读取和写入的。
大多数电话设备将在设备中提供音频数据压缩。事实上,对于成功的互联网电话应用程序,需要某种形式的音频数据压缩。这些压缩技术称为“音频编解码器”(或简称编解码器),并且有一组常用的可互操作的编解码器。Linux 电话 API 包括大多数这些常用编解码器的已定义常量;但是,特定的电话设备可能并非支持所有这些编解码器。正在使用的编解码器的控制由 ioctl 系统调用处理。
Linux 电话 API 与以前使用声卡的工作方式的一个主要区别是,Linux 电话 API 是“面向帧”的,而声卡是“面向字节”的。面向帧的设备读取与时间单位对应的离散数据帧。这样做是因为所有音频编解码器一次都在一段时间的音频数据上运行(通常为 10、20 或 30 毫秒的数据)。由于使用压缩编解码器是网络电话应用程序的常态,因此这是电话设备的正常和预期操作模式。声卡没有此限制,并且可以自由地在任何给定的呼叫中读取和写入可变数量的数据字节。API 为每个编解码器定义了“帧大小”,并且来自设备的原始未压缩音频数据是编解码器选择之一。例如,LINEAR16 编解码器(未压缩的 16 位声音样本)的默认帧大小为 240 字节——对应于默认 8000Hz 采样率下的 30 毫秒数据。每次从设备读取操作将产生 240 字节的数据或什么也不产生。当然,您可以使用 ioctl 调用来更改此行为,以调整未压缩编解码器的帧大小。
让电话设备执行某些操作的命令不会使用写入调用写入到电话设备——只有音频数据会被写入到设备。为了控制电话设备,定义了一组 ioctl 函数来处理基本的电话活动。这组基本的 ioctl 函数在 /usr/include/linux/telephony.h 中定义。希望扩展基本功能集的供应商可以这样做,但这些功能仅限于他们自己的设备驱动程序,并且超出了通用 Linux 电话 API 的范围。
一个示例将最好地说明这种潜力。将电话 API 与 Quicknet Internet PhoneJACK 卡和插入卡中的电话(电话人员称为 FXS 端口或 POTS 端口)一起使用,列表 2 显示了一个简短的程序来使电话响铃。
您会注意到,设备已打开,如果用户未在命令行上提供特定设备名称,则默认为 /dev/phone0。使用 PHONE_MAXRINGS 常量通过 ioctl 调用设置最大响铃次数。然后指示电话使用 PHONE_RING ioctl 响铃。此示例程序是 Quicknet 的软件开发工具包 (SDK) 中找到的 LGPL 模块 ring.c 的简化版本。它太简单了,除了说明技术和演示使用 ioctl 来控制电话设备之外,什么也做不了,但是实际程序不必复杂得多,API 相当简单。所有 Linux 电话 API ioctl 常量都在头文件 /etc/include/linux/telephony.h 中定义。
正是这个基本事实使得为声卡编写的软件可以轻松地适应与电话设备一起使用;与声卡一样,使用读取和写入系统调用写入的唯一数据是音频数据。此外,在定义的电话 ioctl 调用中使用的底层常量旨在避免与现有的声卡 ioctl 冲突。将期望声卡使用电话卡的应用移植,主要涉及处理您的代码进行的声卡 ioctl 调用返回的错误。应该可以(尽管可能不容易)编写一个包装器,该包装器打开电话设备并派生一个子进程(继承到电话设备的打开文件描述符),该子进程运行期望声卡接口的软件。虽然它不是完全透明的,但它是可能的,并且实际上不应该那么困难。
设备电话侧发生的事件需要传达给运行电话的用户空间软件。旧的和粗糙的方法需要软件持续轮询设备以获取状态和更改。Linux 电话 API 当然避免了这种情况,并提供了两种不同的技术,都统称为“异步事件通知”。第一种方法使用信号进行指示,第二种方法使用文件描述符集中用于异常的“异常位”。我将按顺序介绍这两种技术。
使用信号进行事件通知需要三个步骤:首先,为 SIGIO 信号准备和声明信号处理程序函数;其次,设置运行进程的进程 ID (PID) 以接收信号;第三,使用 fcntl 系统调用在打开的文件描述符上启用信号的生成。在 W. Richard Stevens 的 UNIX 环境高级编程 第 12 章中可以找到对这些步骤的出色描述(无论如何,这是一本值得拥有的书)。同样,一个简短的示例可能会澄清这一点。假设您有一个打开的文件描述符 ixj1,它是一个打开的电话设备,您可以使用以下代码片段通过信号启用异步事件通知
signal(SIGIO, &getdata); fcntl(ixj1, F_SETOWN, getpid()); oflags1 = fcntl(ixj1, F_GETFL); fcntl(ixj1, F_SETFL, oflags1 | FASYNC);
以及相关的信号处理函数(上面代码中的 getdata)来处理数据。当您收到信号时,您仍然不知道发生了哪种事件——只知道发生了事件。然后,您的程序将必须对电话设备进行 ioctl 调用,以询问检测到哪种事件(稍后会详细介绍)。此外,如果您的程序有多个打开的电话设备文件描述符,您将不知道哪个生成了信号。而且,信号可能难以处理,并且在多线程程序中可能不可靠,因此,某些开发人员会避免使用信号。这些因素限制了此方法的有效性,从而导致了在文件描述符集中使用“异常位”进行异常的更有效用例。
程序通常会设置读取和写入异常集,然后使用 select() 系统调用来等待文件描述符可读或可写。select() 调用的一个鲜为人知的方面是“异常集”。Linux 电话 API 使用此异常集来向进程发出已发生电话事件的信号。列表 3 提供了一个使用文件描述符 ixj1 的简单示例。
这个极其简单(且并非真正有用)的示例纯粹是为了说明使用异常集和 select() 来检测事件的技术。如果发生了事件,电话设备驱动程序将在异常集中设置相应的位,从而导致 select() 返回。用户可以筛选读取、写入和异常描述符集,以确定其自己的文件描述符是否被设备驱动程序标记为已准备好进行该类型的操作。如果数据已准备好被读取,则语句 FD_ISSET(ixj1,&rfds) 将返回 TRUE;如果设备已准备好被写入,则 FD_ISSET(ixj1,&wfds) 语句返回 TRUE。并且,如果发生了电话事件,则 FD_ISSET(ixj1,&efds) 将返回 TRUE。那么,您如何检测到发生了什么具体事件呢?
API 提供了一个特殊的 PHONE_EXCEPTION ioctl 调用和一个关联的 telephony_exception 结构来解码返回值。此调用将在结构中设置位,以指示发生了哪些电话事件(可能发生了多个)。在上面的示例中,在语句 if(ixje.bits.hookstate) 中检查了“hookstate”位,如果设置了该位,则表示状态已更改。然后进行 ioctl 调用以确定电话是处于摘机还是挂机状态。实际代码将使用大型 select 或扩展的 if-else-if 阶梯来检查 PHONE_EXCEPTION ioctl 调用后的 ixje.bits 的内容。关于如何使用此技术的详细解释超出了本文的范围,但请参阅 /usr/include/linux/telephony.h 文件,以了解可以检测到哪些类型的事件的详细信息。
现在有许多开源程序使用此 API。但是,最著名和最广泛使用的程序是 ohphone,,这是一个使用开源 OpenH323 库的控制台应用程序。Ohphone 是 OpenH323 项目 (www.openh323.org) 的一部分,每天有数千人使用它通过互联网进行免费、高质量的电话通话。Ohphone 不仅完全支持 Linux 电话 API,而且还与其他基于 H.323 的产品兼容,如 Microsoft NetMeeting<+H>tm<+H> 和 Cisco 启用语音的路由器。对这个优秀软件的更详细讨论对于本文来说太多了,但我们鼓励您查看其网站以了解最新消息。开发 OpenH323 库的公司最近被 Quicknet Technologies, Inc. 收购,这是 Quicknet 为确保该开源项目持续进行重大开发工作而做出的努力的一部分。凭借如此充分的商业支持和对开源的承诺,我预计 OpenH323 项目软件在不久的将来会变得更好。
Linux 电话 API 为在 Linux 上开发电话软件提供了一个通用且一致的接口。虽然目前只有一家供应商(Quicknet Technologies, Inc.)拥有完全符合此 API 的驱动程序,但其他几家供应商正在努力开发符合标准的驱动程序。该 API 精简、设计良好,不会与现有的声卡 API 冲突,并提供支持同一接口背后的多个供应商的能力。在未来的一年中,肯定会为 Linux 开发一些令人兴奋的新电话软件。
Greg Herlein 自 1994 年以来一直是狂热的 Linux 开发人员。他的公司 Herlein Engineering 目前提供 Linux/UNIX 咨询服务,尤其是在电话软件开发领域。他居住并在加利福尼亚州旧金山工作。可以通过 greg@herlein.com 与他联系。