PPP通用驱动和通道接口¶
Paul Mackerras paulus@samba.org
2002年2月7日
linux-2.4中的通用PPP驱动程序提供了一种功能的实现,该功能可用于任何PPP实现,包括
网络接口单元(ppp0等)
到网络代码的接口
PPP多链路:在多个链接之间拆分数据报,以及对接收到的片段进行排序和组合
通过/dev/ppp字符设备与pppd的接口
数据包压缩和解压缩
TCP/IP报头压缩和解压缩
检测需求拨号和空闲超时的网络流量
简单的数据包过滤
对于发送和接收PPP帧,通用PPP驱动程序调用PPP channels
的服务。 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 服务。我们将此过程称为 discovery
。然后,用户级别进程告诉介质成为 PPP 通道并将其自身注册到通用 PPP 层。然后,该通道必须将分配给它的通道号报告回用户级别进程。从那时起,PPP 守护程序 (pppd) 中的 PPP 协商代码可以接管并执行 PPP 协商,通过 /dev/ppp 接口访问通道。
在与 PPP 通用层的接口处,PPP 帧存储在 skbuff 结构中,并以两字节的 PPP 协议号开头。该帧不包括 0xff address
字节或 0x03 control
字节,它们是可选地在异步 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(),而不是从中断或 softirq/BH 上下文中调用。
剩余的通用层函数可以在 softirq/BH 级别调用,但不得从硬件中断处理程序中调用。
通用层可以在 softirq/BH 级别调用通道 start_xmit() 函数,但不会在中断级别调用它。因此,start_xmit() 函数可能不会阻塞。
通用层仅会在进程上下文中调用通道 ioctl() 函数。
通用层向通道提供以下保证
当任何线程已经在该通道的该函数中执行时,通用层不会为该通道调用 start_xmit() 函数。
当任何线程已经在该通道的该函数中执行时,通用层不会为该通道调用 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,则该参数应指向一个int,该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将在其中存储标志值。除了上面列出的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(最大重构接收单元)。参数应指向一个包含新MRRU值的int。如果MRRU值为0,则禁用接收到的多链路片段的处理。只有选择了CONFIG_PPP_MULTILINK选项,此ioctl才可用。
上次修改时间:2002年2月7日