PPP 通用驱动和通道接口¶
Paul Mackerras paulus@samba.org
2002 年 2 月 7 日
linux-2.4 中的通用 PPP 驱动程序提供了在任何 PPP 实现中有用的功能的实现,包括
网络接口单元(ppp0 等)
与网络代码的接口
PPP 多链路:在多个链路之间分割数据报,并对接收到的片段进行排序和组合
通过 /dev/ppp 字符设备与 pppd 的接口
数据包压缩和解压缩
TCP/IP 报头压缩和解压缩
检测按需拨号和空闲超时的网络流量
简单的数据包过滤
为了发送和接收 PPP 帧,通用 PPP 驱动程序调用 PPP 通道
的服务。PPP 通道封装了一种将 PPP 帧从一台机器传输到另一台机器的机制。PPP 通道实现内部可以任意复杂,但与通用 PPP 代码的接口非常简单:它只需要能够发送 PPP 帧,接收 PPP 帧,以及可选地处理 ioctl 请求。目前,有针对异步串口、同步串口和以太网上 PPP 的 PPP 通道实现。
这种架构使得通过允许将多个通道链接到每个 ppp 网络接口单元,从而以自然且直接的方式实现 PPP 多链路成为可能。通用层负责在传输时分割数据报,并在接收时重新组合它们。
PPP 通道 API¶
有关用于在通用 PPP 层和 PPP 通道之间通信的类型和函数的声明,请参见 include/linux/ppp_channel.h。
每个通道都必须通过 ppp_channel.ops 指针向通用 PPP 层提供两个函数
当通用层有帧要发送时,会调用 start_xmit()。通道可以选择出于流量控制的原因拒绝该帧。在这种情况下,start_xmit() 应返回 0,并且通道应在稍后再次可以接受帧时调用 ppp_output_wakeup() 函数,然后通用层将尝试重新传输被拒绝的帧。如果接受了该帧,则 start_xmit() 函数应返回 1。
ioctl() 提供一个接口,用户空间程序可以使用该接口来控制通道行为的各个方面。当用户空间程序在绑定到该通道的 /dev/ppp 实例上执行 ioctl 系统调用时,将调用此过程。(通常只有 pppd 会这样做。)
通用 PPP 层向通道提供七个函数
当创建通道时,会调用 ppp_register_channel() 以通知 PPP 通用层其存在。例如,将串行端口设置为 PPPDISC 行规会使 ppp_async 通道代码调用此函数。
当要销毁通道时,会调用 ppp_unregister_channel()。例如,当在串行端口上检测到挂断时,ppp_async 通道代码会调用此函数。
当通道先前拒绝了对其 start_xmit 函数的调用并且现在可以接受更多数据包时,该通道会调用 ppp_output_wakeup()。
当通道收到完整的 PPP 帧时,它会调用 ppp_input()。
当通道检测到帧已丢失或丢弃时(例如,由于 FCS(帧校验序列)错误),它会调用 ppp_input_error()。
ppp_channel_index() 返回由 PPP 通用层分配给此通道的通道索引。通道应提供某种方式(例如,ioctl)将其传输回用户空间,因为用户空间需要它来将 /dev/ppp 的实例附加到此通道。
ppp_unit_number() 返回此通道连接到的 ppp 网络接口的单元号,如果通道未连接,则返回 -1。
将通道连接到 ppp 通用层是从通道代码而不是通用层启动的。通道应该具有某种方式供用户级进程独立于 ppp 通用层对其进行控制。例如,对于 ppp_async 通道,这是通过串行端口的文件描述符提供的。
通常,用户级进程将初始化底层通信介质并准备好进行 PPP。例如,对于异步 tty,这可能涉及设置 tty 速度和模式,发出调制解调器命令,然后与远程系统进行某种对话以在那里调用 PPP 服务。我们将此过程称为 发现
。然后,用户级进程告诉介质成为 PPP 通道并向通用 PPP 层注册自己。然后,通道必须将分配给它的通道号报告回用户级进程。从那时起,PPP 守护进程(pppd)中的 PPP 协商代码可以接管并执行 PPP 协商,通过 /dev/ppp 接口访问通道。
在与 PPP 通用层的接口处,PPP 帧存储在 skbuff 结构中,并以双字节 PPP 协议号开头。该帧不包含 0xff 地址
字节或 0x03 控制
字节,它们在异步 PPP 中是可选的。也没有任何控制字符的转义,也没有包含任何 FCS 或成帧字符。如果特定介质需要,则这完全是通道代码的责任。也就是说,呈现给 start_xmit() 函数的 skbuff 仅包含 2 字节协议号和数据,并且呈现给 ppp_input() 的 skbuff 必须采用相同的格式。
通道必须提供 ppp_channel 结构的实例来表示通道。通道可以随意使用 private
字段。通道应在调用 ppp_register_channel() 之前初始化 mtu
和 hdrlen
字段,并且在 ppp_unregister_channel() 返回之前不要更改它们。mtu
字段表示 PPP 帧的数据部分的最大大小,即不包括 2 字节协议号。
如果通道在其呈现给它的用于传输的 skbuff 中需要一些空间(即,在 PPP 帧开始之前,skbuff 数据区域中的一些空闲空间),则应将 ppp_channel 结构的 hdrlen
字段设置为所需的空间量。通用 PPP 层将尝试提供那么多空间,但通道仍应检查是否有足够的空间,如果没有,则复制 skbuff。
在输入端,通道理想情况下应在呈现给 ppp_input() 的 skbuff 中提供至少 2 个字节的空间。通用 PPP 代码不需要这样做,但如果这样做会更有效率。
缓冲和流量控制¶
通用 PPP 层旨在最大限度地减少其在传输方向上缓冲的数据量。它维护 PPP 单元(网络接口设备)的传输数据包队列,以及每个附加通道的传输数据包队列。通常,该单元的传输队列最多包含一个数据包;例外情况是当 pppd 通过写入 /dev/ppp 发送数据包,以及当核心网络代码在队列停止的情况下调用通用层的 start_xmit() 函数时,即当通用层调用 netif_stop_queue()
时,这仅在传输超时时发生。start_xmit 函数始终接受并排队它被要求传输的数据包。
传输数据包将从 PPP 单元传输队列中出队,然后根据需要进行 TCP/IP 报头压缩和数据包压缩(Deflate 或 BSD-Compress 压缩)。在此之后,数据包将无法重新排序,因为解压缩算法依赖于以生成它们的相同顺序接收压缩数据包。
如果未使用多链路,则此数据包将传递到附加通道的 start_xmit() 函数。如果通道拒绝接收数据包,则通用层会保存它以供稍后传输。当通道调用 ppp_output_wakeup() 或核心网络代码再次调用通用层的 start_xmit() 函数时,通用层将再次调用通道的 start_xmit() 函数。通用层不包含超时和重传逻辑;它依赖于核心网络代码来实现。
如果正在使用多链路,则通用层将数据包分成一个或多个片段,并在每个片段上放置一个多链路报头。它根据数据包的长度以及目前可能能够接受片段的通道数量来决定使用多少个片段。如果通道当前没有排队任何要传输的片段,则该通道可能能够接受片段。通道可能仍然会拒绝片段;在这种情况下,该片段将排队以供通道稍后传输。此方案的效果是,将更多的片段给予更高带宽的通道。这也意味着在轻负载下,通用层将倾向于在所有通道上分散大型数据包的片段,从而减少延迟,而在重负载下,数据包将倾向于作为单个片段传输,从而减少片段化的开销。
SMP 安全性¶
PPP 通用层被设计为 SMP 安全的。在必要时,会使用锁来保护对内部数据结构的访问,以确保其完整性。作为此过程的一部分,通用层要求通道遵守某些要求,并反过来向通道提供某些保证。本质上,通道需要在构成通道和通用层之间通信基础的 ppp_channel 结构上提供适当的锁定。这是因为通道为 ppp_channel 结构提供存储,因此通道需要保证此存储在适当的时候存在且有效。
通用层要求通道提供以下保证:
ppp_channel 对象必须从调用 ppp_register_channel() 时存在,直到调用 ppp_unregister_channel() 返回之后。
在为该通道调用 ppp_unregister_channel() 时,任何线程都不能正在调用该通道的 ppp_input()、ppp_input_error()、ppp_output_wakeup()、ppp_channel_index() 或 ppp_unit_number()。
必须从进程上下文调用 ppp_register_channel() 和 ppp_unregister_channel(),而不是从中断或软中断/BH 上下文调用。
其余通用层函数可以在软中断/BH 级别调用,但不得从硬件中断处理程序调用。
通用层可以在软中断/BH 级别调用通道的 start_xmit() 函数,但不会在中断级别调用它。因此,start_xmit() 函数可能不会阻塞。
通用层只会以进程上下文调用通道的 ioctl() 函数。
通用层向通道提供以下保证:
当任何线程已在该通道的 start_xmit() 函数中执行时,通用层不会为该通道调用 start_xmit() 函数。
当任何线程已在该通道的 ioctl() 函数中执行时,通用层不会为该通道调用 ioctl() 函数。
在调用 ppp_unregister_channel() 返回之前,不会有任何线程正在执行从通用层到该通道的 start_xmit() 或 ioctl() 函数的调用,并且通用层随后也不会调用这两个函数。
与 pppd 的接口¶
PPP 通用层导出一个名为 /dev/ppp 的字符设备接口。pppd 使用此接口来控制 PPP 接口单元和通道。尽管只有一个 /dev/ppp,但每个打开的 /dev/ppp 实例都独立工作,并且可以附加到 PPP 单元或 PPP 通道。这是通过使用 file->private_data 字段来指向每个打开的 /dev/ppp 实例的单独对象来实现的。通过这种方式,可以获得类似于 Solaris 的克隆打开的效果,使我们能够控制任意数量的 PPP 接口和通道,而无需用数百个设备名称填充 /dev。
当打开 /dev/ppp 时,将创建一个新的实例,该实例最初是未附加的。使用 ioctl 调用,它可以附加到现有单元,附加到新创建的单元,或附加到现有通道。附加到单元的实例可以使用 read() 和 write() 系统调用以及必要的 poll() 来发送和接收 PPP 控制帧。类似地,附加到通道的实例可以用于在该通道上发送和接收 PPP 帧。
在多链路方面,单元表示捆绑包,而通道表示各个物理链路。因此,通过写入单元(即写入附加到单元的 /dev/ppp 实例)发送的 PPP 帧将受到捆绑级压缩,并在各个链路之间进行分片(如果使用多链路)。相反,通过写入通道发送的 PPP 帧将按原样在该通道上发送,没有任何多链路标头。
通道最初未附加到任何单元。在此状态下,它可以用于 PPP 协商,但不能用于传输数据包。然后可以使用 ioctl 调用将其连接到 PPP 单元,这使其可用于发送和接收该单元的数据包。
在 /dev/ppp 的实例上可用的 ioctl 调用取决于它是未附加、附加到 PPP 接口还是附加到 PPP 通道。在未附加实例上可用的 ioctl 调用是:
PPPIOCNEWUNIT 创建一个新的 PPP 接口,并使此 /dev/ppp 实例成为该接口的“所有者”。如果参数 >= 0,则该参数应指向一个 int,它是所需的单元号;如果参数为 -1,则分配最低的未使用单元号。成为接口的所有者意味着,如果关闭此 /dev/ppp 实例,则该接口将被关闭。
PPPIOCATTACH 将此实例附加到现有的 PPP 接口。该参数应指向包含单元号的 int。这不会使此实例成为 PPP 接口的所有者。
PPPIOCATTCHAN 将此实例附加到现有的 PPP 通道。该参数应指向包含通道号的 int。
附加到通道的 /dev/ppp 实例上可用的 ioctl 调用是:
PPPIOCCONNECT 将此通道连接到 PPP 接口。该参数应指向包含接口单元号的 int。如果该通道已连接到接口,则它将返回 EINVAL 错误;如果请求的接口不存在,则返回 ENXIO 错误。
PPPIOCDISCONN 断开此通道与它所连接的 PPP 接口的连接。如果该通道未连接到接口,则它将返回 EINVAL 错误。
PPPIOCBRIDGECHAN 将一个通道与另一个通道桥接。该参数应指向一个 int,其中包含要桥接到的通道的通道号。一旦两个通道被桥接,由 ppp_input() 呈现给一个通道的帧将被传递到桥接实例以进行进一步传输。这允许将帧从一个通道切换到另一个通道:例如,将 PPPoE 帧传递到 PPPoL2TP 会话中。由于通道桥接会中断正常的 ppp_input() 路径,因此一个给定的通道可能不会同时是桥接的一部分和单元的一部分。如果该通道已经是桥接或单元的一部分,则此 ioctl 将返回 EALREADY 错误;如果请求的通道不存在,则返回 ENXIO 错误。
PPPIOCUNBRIDGECHAN 执行 PPPIOCBRIDGECHAN 的反向操作,取消桥接通道对。如果该通道不是桥接的一部分,则此 ioctl 将返回 EINVAL 错误。
所有其他 ioctl 命令都将传递到通道的 ioctl() 函数。
附加到接口单元的实例上可用的 ioctl 调用是:
PPPIOCSMRU 设置接口的 MRU(最大接收单元)。该参数应指向包含新 MRU 值的 int。
PPPIOCSFLAGS 设置控制接口操作的标志。该参数应是指向包含新标志值的 int 的指针。可以在标志值中设置的位是:
SC_COMP_TCP
启用发送 TCP 标头压缩
SC_NO_TCP_CCID
禁用 TCP 标头压缩的连接 ID 压缩
SC_REJ_COMP_TCP
禁用接收 TCP 标头解压缩
SC_CCP_OPEN
压缩控制协议 (CCP) 已打开,因此检查 CCP 数据包
SC_CCP_UP
CCP 已启动,可以(解)压缩数据包
SC_LOOP_TRAFFIC
将 IP 流量发送到 pppd
SC_MULTILINK
启用已发送数据包上的 PPP 多链路分片
SC_MP_SHORTSEQ
期望接收到的多链路分片上出现短多链路序列号
SC_MP_XSHORTSEQ
发送短多链路序列号。
这些标志的值在 <linux/ppp-ioctl.h> 中定义。请注意,如果未选择 CONFIG_PPP_MULTILINK 选项,则 SC_MULTILINK、SC_MP_SHORTSEQ 和 SC_MP_XSHORTSEQ 位的值将被忽略。
PPPIOCGFLAGS 返回接口单元的状态/控制标志的值。该参数应指向一个 int,ioctl 将在该 int 中存储标志值。除了上面列出的 PPPIOCSFLAGS 的值外,返回的值中还可以设置以下位:
SC_COMP_RUN
CCP 压缩器正在运行
SC_DECOMP_RUN
CCP 解压缩器正在运行
SC_DC_ERROR
CCP 解压缩器检测到非致命错误
SC_DC_FERROR
CCP 解压缩器检测到致命错误
PPPIOCSCOMPRESS 设置数据包压缩或解压缩的参数。该参数应指向 ppp_option_data 结构(在 <linux/ppp-ioctl.h> 中定义),该结构包含一个指针/长度对,该对应该描述一个内存块,其中包含一个指定压缩方法及其参数的 CCP 选项。ppp_option_data 结构还包含一个
transmit
字段。如果此字段为 0,则 ioctl 将影响接收路径,否则将影响发送路径。PPPIOCGUNIT 返回参数指向的 int 中此接口单元的单元号。
PPPIOCSDEBUG 将接口的调试标志设置为参数指向的 int 中的值。仅使用最低有效位;如果此位为 1,则通用层将在其操作期间打印一些调试消息。这仅用于调试通用 PPP 层代码;通常对于找出 PPP 连接失败的原因没有帮助。
PPPIOCGDEBUG 在参数指向的 int 中返回接口的调试标志。
PPPIOCGIDLE 返回自上次发送和接收数据包以来的时间(以秒为单位)。该参数应指向 ppp_idle 结构(在 <linux/ppp_defs.h> 中定义)。如果启用了 CONFIG_PPP_FILTER 选项,则将重置发送和接收空闲计时器的这组数据包限制为通过
active
数据包筛选器的那些数据包。存在此命令的两个版本,以处理用户空间期望时间为 32 位或 64 位 time_t 秒。PPPIOCSMAXCID 设置 TCP 标头压缩器和解压缩器的最大连接 ID 参数(以及连接槽的数量)。参数指向的 int 的低 16 位指定压缩器的最大连接 ID。如果该 int 的高 16 位非零,则它们指定解压缩器的最大连接 ID,否则解压缩器的最大连接 ID 设置为 15。
PPPIOCSNPMODE 设置给定网络协议的网络协议模式。该参数应指向 npioctl 结构(在 <linux/ppp-ioctl.h> 中定义)。
protocol
字段给出了要影响的协议的 PPP 协议号,mode
字段指定如何处理该协议的数据包:NPMODE_PASS
正常操作,发送和接收数据包
NPMODE_DROP
静默丢弃该协议的数据包
NPMODE_ERROR
丢弃数据包,并在传输时返回错误
NPMODE_QUEUE
将数据包排队等待传输,丢弃接收到的数据包
目前,NPMODE_ERROR 和 NPMODE_QUEUE 的效果与 NPMODE_DROP 相同。
PPPIOCGNPMODE 返回给定协议的网络协议模式。参数应该指向一个 npioctl 结构体,其中
protocol
字段设置为感兴趣的协议的 PPP 协议号。返回时,mode
字段将被设置为该协议的网络协议模式。PPPIOCSPASS 和 PPPIOCSACTIVE 设置
pass
和active
数据包过滤器。只有在选择了 CONFIG_PPP_FILTER 选项时,这些 ioctl 才能使用。参数应该指向一个 sock_fprog 结构体(定义在 <linux/filter.h> 中),其中包含过滤器的已编译 BPF 指令。如果数据包未能通过pass
过滤器,则会被丢弃;否则,如果它们未能通过active
过滤器,则会被传递,但它们不会重置发送或接收空闲计时器。PPPIOCSMRRU 启用或禁用接收数据包的多链路处理,并设置多链路 MRRU(最大重构接收单元)。参数应该指向一个 int,其中包含新的 MRRU 值。如果 MRRU 值为 0,则禁用接收到的多链路片段的处理。只有选择了 CONFIG_PPP_MULTILINK 选项时,此 ioctl 才能使用。
最后修改时间:2002 年 2 月 7 日