使用打印机端口进行网络连接

作者:Alessandro Rubini

PLIP 的意思是“并行线路 इंटरनेट协议”。它是一种用于通过并行电缆传输 IP 流量的协议。它可以与任何并行接口一起工作,并且能够以大约 40KB/秒的速度传输数据。使用 PLIP,您可以以几乎零成本连接任意两台计算机。尽管如今 ISA 网卡很容易找到和安装,但只要您获得笔记本电脑,您仍然会喜欢 PLIP,除非您能负担得起 PCMCIA 网卡。

只要您在两台系统上都具有 root 权限(只有 root 用户才能加载模块或配置接口),PLIP 允许您在任何有联网 Linux 机器且并行端口可用的地方将您的计算机连接到互联网。

我发现 PLIP 的内部设计在三个层面上都非常有趣

  1. 它展示了如何使用简单的 I/O 指令。

  2. 它可以让您了解网络接口如何适应整个内核。

  3. 它在实践中展示了内核软件如何使用任务队列。

硬件处理

在展示任何 PLIP 代码之前,我想描述一下标准并行端口的工作原理,以便您能够理解实际的数据传输是如何发生的。

并行端口是一个简单的设备;其外部连接器暴露了 12 个输出位和 5 个输入位。软件可以通过三个 8 位端口直接访问这些位:两个端口用于写入,一个端口用于读取。此外,其中一个输入信号可以触发中断;通过设置其中一个输出端口中的一个位可以启用此功能。图 1 显示了这三个端口如何映射到 25 针连接器。并行端口的 base_addr(其数据端口的地址)通常是 0x378、0x278 或 0x3bc。绝大多数并行端口使用 0x378。

Networking with the Printer Port

图 1. 三个端口到连接器的映射

通过调用内核头文件中定义的两个 C 语言函数来实现对端口的物理访问

#include <linux/io.h>
unsigned char inb(unsigned short port);
void outb(unsigned char value, unsigned short port);

函数名中的 “b” 表示 “字节”。Linux 还提供 inw(字,16 位)、inl(长字,32 位)及其输出对应项,但它们不是使用并行端口所必需的。事实上,刚刚显示的函数只是宏,它们在大多数 Linux 平台上扩展为单条机器指令。它们的定义依赖于 extern inline 函数;这意味着在编译任何使用它们的代码时,您必须打开优化。这背后的原因有些技术性,并且在 gcc 手册页中得到了很好的解释。

您无需处于内核空间即可调用 inboutb。如果您想从 shell 脚本访问 I/O 端口,您可以编译 inp.c 和 outp.c 并使用您的设备玩游戏(甚至破坏计算机)。因此,只有 root 用户才能访问端口。inp.c 和 outp.c 的源代码可通过匿名下载获得,文件为 ftp://ftp.linuxjournal.com/lj/listings/issue47/2662.tgz。

通信

基于我在上一节中提供的并行端口描述,应该清楚的是,使用 PLIP 通信的双方一次最多必须交换五个位。PLIP 电缆必须经过特殊布线,以便将一侧的五个输出连接到另一侧的五个输入,反之亦然。PLIP 电缆的确切引脚排列在源文件 drivers/net/plip.c 和其他几个地方进行了描述,因此我不会在此重复这些信息。

并行端口的缺点之一是硬件中没有任何定时资源可用。将其与包含自身时钟的串行端口进行比较。通信协议无法利用任何高级硬件功能,并且任何握手都必须在软件中执行。所选择的协议使用其中一个位作为选通信号,以指示四个数据位的可用性;接收器必须通过切换其自身的选通线来确认数据的接收。这种数据传输方法非常占用 CPU 资源。处理器必须轮询选通信号才能发送其数据,并且在 PLIP 数据传输期间,系统性能会严重下降。

图 2 描绘了 PLIP 传输的时间线,其中详细说明了单个信息字节传输所涉及的步骤。

Networking with the Printer Port

图 2. PLIP 传输时间线

互联网数据报

就内核本身而言,PLIP 设备就像任何其他网络设备一样。更具体地说,它就像任何其他以太网接口一样,即使它的名称是 plipx 而不是 ethx

当数据报必须通过网络接口传输时,它会被传递给设备驱动程序的传输函数。驱动程序接收一个套接字缓冲区参数(struct sk_buff)和一个指向自身的指针(struct device)。

对于 PLIP,传输是通过将 IP 数据报封装到 “硬件头部” 中进行交付的,这与任何其他传输介质的情况类似。PLIP 的不同之处在于,尽管它接收到的数据包已经包含以太网头部,但驱动程序会添加自己的头部。PLIP 封装的数据包在并行电缆上传输时,由以下字段组成

  • 数据包长度:传输以数据包长度(以字节为单位)为首,最低有效字节优先。计数以 16 位数字传输,不包括计数本身或最终校验和字节。

  • 以太网头部:PLIP 使用以太网封装,以便简化第一个实现(在 PC 世界中,当 Linux 不存在时)。这十四个字节几乎没有用处,但为了向后兼容,它们仍然存在。

  • IP 数据:IP 数据直接跟随头部,就像以太网接口一样。

  • 校验和字节:PLIP 传输的尾字节是一个校验和字节,它必须等于数据包中每个数据字节的总和模 256,不包括前两个字节(长度)和校验和本身。

每当传输数据包时,所有字节都通过电缆使用前面描述的 5 位协议发送。这非常简单,并且可以完美地工作,除非在传输过程中出现问题。

异步操作

PLIP 通信通道的有趣之处在于如何处理异步操作。数据包的传输和接收必须与其他系统操作相适应,并且必须尽可能地容错。这涉及多个内核资源,并且对于任何对内核内部机制感兴趣的人来说都非常有趣。

为了实现可靠的 PLIP 传输,需要克服三个问题。发出数据包必须相对于系统的其余部分异步传输;即使传输是 CPU 密集型的,它也应该发生在正常计算流程之外。接收数据包也必须异步进行,并且它们必须能够将它们的到达通知 PLIP 设备驱动程序。最后一个问题是容错;如果其中一方由于任何原因锁定传输,我们不希望对等主机在等待选通信号时冻结。

异步操作在 PLIP 中是通过使用内核任务队列(在 1996 年 6 月的 Linux Journal 期刊中介绍过)来实现的。容错和超时是使用状态机实现来完成的,该状态机将 PLIP 传输/接收与其他计算活动交错进行,而不会丢失发射器的内部状态跟踪。

让我们看一下连接名为 Tanino 和 Romeo 的机器的 PLIP 电缆(Tanino = Tx,Romeo = Rx)。以下段落解释了当 Tanino 向 Romeo 发送数据包时会发生什么。

Tanino 发送信号中断 Romeo,禁用自身的中断报告,并通过在立即任务队列中注册 plip_bh 并返回来启动传输循环。当 plip_bh 运行时,它知道接口正在发送数据并调用 plip_send_packet

Romeo 在被中断时 (plip_interrupt),在立即任务队列中注册 plip_bh。plip_bh 函数将计算分派给 plip_receive_packet,后者禁用接口中的中断报告并开始接收字节。

Tanino 的循环建立在 plip_send(它传输单个字节)之上,而 Romeo 的循环建立在 plip_receive(它接收单个字节)之上。这两个函数都准备好检测超时情况,在这种情况下,它们将 TIMEOUT 宏返回给调用函数,该函数将 TIMEOUT 返回给 plip_bh。

当被调用者通过返回 TIMEOUT 中止循环时,plip_bh 函数在定时器任务队列中注册一个函数,以便在下一个时钟滴答时再次进入循环。如果超时在几个时钟滴答后仍然持续,则中止此数据包的传输或接收,并在 enet_statistics 结构中注册错误;这些错误由 ifconfig 命令显示。

如果在下一个时钟滴答时超时情况不再持续,则数据交换顺利进行。接口中实现的状态机负责在超时发生的确切位置重新启动通信。

正如您所看到的,PLIP 接口相当对称。

在 Linux 中加载驱动程序

就网络驱动程序而言,能够传输和接收数据并不是它的全部工作。驱动程序需要与内核的其余部分交互,以便与系统的其余部分相适应。PLIP 设备驱动程序将其源代码的大约四分之一用于交互问题,我觉得这里值得介绍一下。

基本上,网络接口需要能够发送和接收数据包。网络驱动程序被组织成一组 “方法函数”,就像字符设备驱动程序一样(参见 动态内核:发现LJ 1996 年 4 月)。发送数据包很容易;其中一种方法函数专用于数据包传输,驱动程序只需实现正确的方法函数即可将数据传输到网络。

接收数据包在某种程度上更困难,因为数据包是通过中断到达的,并且驱动程序必须主动管理接收到的数据。任何网络接口的数据包接收都通过利用所谓的 “底半部” 来管理。

在 Linux 中,中断处理代码分为两部分。顶半部是硬件中断,它由硬件事件触发并立即执行。底半部是一个软件例程,由内核调度运行,而不会干扰正常的系统运行。当进程从系统调用返回以及 “慢速” 中断处理程序返回时,底半部会运行。当慢速处理程序运行时,所有处理器寄存器都会被保存,并且硬件中断不会被禁用;因此,当此类处理程序返回时,运行待处理的底半部是安全的。值得注意的是,2.1 系列的新内核不再区分快速和慢速中断处理程序。

底半部处理程序必须被 “标记”;这包括在位掩码寄存器中设置一个位,以便内核将检查是否是否有任何底半部待处理。PLIP 驱动程序使用的立即任务队列被实现为底半部。当任务排队时,调用者必须调用 mark_bh(IMMEDIATE_BH),并且一旦进程完成系统调用或慢速处理程序返回,队列将立即运行。

回到网络接口,当驱动程序接收到网络数据报时,它必须进行以下调用

netif_rx(struct sk_buff *skb)

其中 skb 是承载接收到的数据包的缓冲区;PLIP 从 plip_receive_packet 调用 netif_rx。netif_rx 函数将数据包排队以供后续处理,并调用

mark_bh(NET_BH)
然后,当底半部运行时,数据包将被处理。

实际上,需要更多内容才能将网络接口融入 Linux 内核;模块必须注册其自身接口并初始化它们。此外,接口必须导出一小部分内核将调用的内务处理函数。所有这些都由以下列出的几个简短函数执行

  • plip_init:此函数负责初始化网络设备;当 init_module 注册其设备时会调用它。该函数检查系统中是否安装了硬件,并在描述接口的 struct device 中分配字段。

  • plip_open:每当接口启动时,内核都会调用其 open 函数。该函数必须准备就绪(类似于字符设备的 open 方法函数)。

  • plip_close:此函数与 plip_open 相反。

  • plip_get_stats:每当需要统计信息时,都会调用此函数。例如,ifconfig 的打印输出显示由此函数返回的值。

  • plip_config:如果程序更改设备的硬件配置,则会调用此函数。PLIP 允许您在运行时指定中断线,因为当模块加载时,探测无法安全地执行。大多数并行端口都配置为使用默认中断线。

  • plip_ioctl:任何需要实现设备特定 ioctl 命令的接口都必须具有 ioctl 方法函数。PLIP 允许更改其超时值,尽管我从未需要使用这些数字。plipconfig 程序允许更改超时。

  • plip_rebuild_header:此函数用于在 IP 数据前面构建以太网头部。使用 ARP 的以太网接口不需要实现此函数,因为以太网接口的默认函数会完成所有工作。

  • init_module:正如您可能已经知道的那样,这是模块化驱动程序的入口点。当网络接口加载到运行系统时,其 init_module 应该调用 register_netdev,传递一个指向 struct device 的指针。此类结构应部分初始化,并应包含一个指向 init 函数的指针,该函数完成结构的初始化。对于 PLIP,这样的函数是 plip_init。

这些函数,以及负责实际数据包传输的 hw_start_xmit 函数,是使网络接口在 Linux 中运行所需的全部内容。尽管我承认要编写真正的驱动程序还需要了解更多内容,但我希望实际的源代码可以证明对填补空白很有趣。

更多信息

我选择讨论 PLIP 的动机是这种网络连接易于获得,以及 DIY 方法可能会说服某人构建自己的红外以太网链路。如果您真的打算查看源代码以学习网络接口的工作原理,我建议从 loopback.c 开始,它实现了 o 接口,以及 skeleton.c,它非常详细地介绍了您在构建网络驱动程序时会遇到的问题。

如果您更热衷于使用 PLIP 而不是编写设备驱动程序,您可以参考任何 LDP 镜像站点中的 PLIP-HOWTO,以及大多数 Linux 安装中的 /usr/doc/HOWTO。

Networking with the Printer Port
Alessandro 总是在想为什么笔记本电脑配备软盘驱动器而不是第二个并行端口。他的电子邮件地址是 rubini@linux.it,您可以在那里就 Linux 以及设备驱动程序等问题找他。他写了一本书,名为 Linux Device Drivers for O'Reilly and Associates。
加载 Disqus 评论