原文链接:https://mp.weixin.qq.com/s/a80xy1__zqtHg5a9jmiljQ
本文要点
引言
以太网接口
leintr函数
leread函数
ether_input函数
ether_output函数
lestart函数
ioctl系统调用
ifioctl函数
ifconf函数
通用接口ioctl命令
if_down和if_up函数
以太网、SLIP和环回
引言
上篇文章概说《TCP/IP详解 卷2》第3章 接口层讨论了所有接口要用到的数据结构以及对其初始化。本文将说明以太网设备驱动在初始化后是如何接收和传输数据帧的;同时配置网络设备的通用ioctl命令。
我们不准备查看整个以太网驱程程序的源代码,因为它大约有1000行代码,其中有一半是一个特定接口卡的硬件细节,但要研究与硬件无关以太网代码部分,及驱动程序是如何与内核其它部分交互的。
网络设备驱动程序通过ifnet结构的7个指针来访问。图1列出了指向我们三个例子驱动程序的入口点。
图1中不包括输入函数,因为它们是网络设备中断驱动的。中断服务例程的配置与硬件相关,不在本文讨论范围。我们要了解的是处理设备中断的函数,而不是这些函数被调用的机制。
图1 例子驱动程序的接口函数
以太网接口
Net/3以太网设备驱动程序都遵循同样的设计。对于大多数Unix设备驱动程序来说,都是如此,写一个新接口卡的驱动程序总是在一个已有的驱动程序的基础上修改而来的。下面将简要概述一下以太网的标准和一个以太网驱动程序设计,以LANCE驱动程序来说明这个设计。
图2说明了一个IP分组的以太网封装。
图2 一个IP分组的以太网封装
以太网帧包括48bit的目标地址和源地址,接下来是一个16bit的类型字段,它标识这个帧所携带的数据的格式。对于IP分组,类型是0x0800。帧的最后是一个32bit的CRC循环冗余检验,用于检验帧的差错。
我们用48bit的以太网地址作为硬件地址。IP地址到硬件地址的转换用ARP协议,而硬件到IP地址的转换用RARP协议。以太网地址有两种类型:单播和多播,一个单播地址描述一个单一的以太网接口,而一个多播地址描述一组以太网接口。以太网广播是所有接口都接收的多播。以太网单播地址由设备厂商分配 ,也有一些设备的地址允许用软件改变。
图3列举了以太网接口的数据结构和函数。
图3 以太网设备驱动程序
图3中,椭圆表示一个函数(leintr),方框表示一个数据结构(le_softc[0])以及用圆角方框标识一组函数(ARP协议)。
图3左上角显示的是OSI无连接网络层协议、IP和ARP的输入队列。ether_input将以太网帧分用到多个协议队列中。
1. leintr函数
我们从以太网帧接收开始,在正常操作中,一个以太网接口接收到目的地址为它的单播地址或者以太网广播地址的帧,当一个完整的帧可用时,接口就产生一个中断,并且内核调用leintr。
leintr检验硬件,并且如果有一个帧到达,就调用leread把这个帧从接口转移到一个mbuf链中(用m_devget)。如果硬件报告一个帧已传输完或者发现一个差错(如有一个错误的检验和),则leintr更新相应的接口统计,复位这个硬件,并调用lestart来传输另外一个帧。
所有以太网设备驱动程序将它们收到的帧传给ether_input做进一步处理。设备驱动程序构造的mbuf链不包括以太网首部,以太网首部作为一个独立参数传递给ether_input。结构ether_header如图4所示。
图4 结构ether_header
38~42 以太网CRC并不总是正确的。它由接口硬件来计算与检验,接口硬件丢弃到达的CRC差错帧。以太网设备驱动程序负责ether_type的网络和主机字节序列的转换。
2. leread函数
函数leread(图5)的开始是由leintr传给它的一个连续的内存缓冲区,并且构造了一个ether_header结构和一个mbuf链。这个链表用于存储来自以太网帧的数据。leread还将输入帧传给BPF。
图5 函数leread
528~539 函数leintr给leread传了三个参数:unit,它表示接收到此帧的特定接口卡;buf,它指向收到的帧;len,它是帧的字节数(包括首部和CRC)。
et指向这个缓存buf的开始,并且将以太网字节序列转换成主机字节序列,来构造结构ether_header。
540~551 将len减去以太网首部和CRC长度得到数据的字节数。短分组是一个长度太短的非法以太网帧,它被记录、统计并丢弃。
552~557 目标地址被检测,并判断是不是以太网广播或者多播地址。以太网广播地址是一个以太网多播地址的特例;它的每一比特都被设置为1。etherbroadcastaddr是一个数组,定义如下:
u_char etherbroadcastaddr[6]={0xff, 0xff, 0xff,0xff,0xff,0xff}
bcmp比较etherbroadcastaddr和ether_dhost,如果相同,则设置标志M_BCAST。一个以太网多播地址由这个地址的首字节的低位比特来标识,如图6所示。
图6 检验一个以太网多播地址
并不是所有以太网多播帧都是IP多播数据报,并且IP必须进一步检测这个分组。如果这个地址的多播比特被置位,在mbuf首部中设置M_MCAST。检测的顺序是重要的:首先和以太网广播地址比较,若不同,则检测标识以太网多播地址的首字节的低位比特。
558~573 如果接口带有BPF,调用bpf_tap把这个帧直接传给BPF。对于SLIP和环回接口要构造一个特定的BPF帧,因为这些网络没有一个链路层首部(不像以太网)。
如果分组发送一个不与此接口地址匹配的单播地址,则被leread丢弃。
574~585 m_devget将传给leread的缓存复制到一个它分配的mbuf链中,并返回mbuf的指针。传给m_devget的第一个参数指向以太网首部后的第一个字节,它是此帧中的第一个数据字节。如果m_devget获取内存失败,leread立即返回。另外广播和多播标志被设置在链表中的第一个mbuf中,ether_input处理这个分组。
3. ether_input函数
函数ether_input显示如图7所示,它检查结构ether_header来判断接收的数据类型,并将接收到的分组加入到队列中等待处理。
图7 函数ether_input
a. 广播和多播的识别
196~209 传给ether_input的参数有:ifp,一个指向接收此分组的接口的ifnet结构的指针;eh,一个指向接收分组的以太网首部的指针;m,一个指向接收分组的指针(不包括以太网首部)。
任何到达不工作接口的分组将被丢弃。可能没有为接口配置一个协议地址,或者接口可能被程序ifconfig显示禁用了。
210~218 变量time是一个全局的timeval结构,内核用它维护当前时间和日期,它是从Unix新纪元(1970年1月1日00:00:00)开始的秒和微秒数。在Net/3源代码中经常会遇到结构timeval:
struct timeval {
long tv_sec;
long tv_usec;
};
用当时时间更新if_lastchange,并把if_ibytes加上输入分组的长度(分组长度加上14字节的以太网首部)。
然后,ether_input再次判断是否为一个广播或者多播分组。
b. 链路层分用
219~227 ether_input根据以太网类型字段来跳转。对于一个IP分组,schednetisr调度一个IP软件中断,并选择IP输入列队,ipintrq。对于一个ARP分组,调度ARP软件中断,并选择arpintrq。一个isr是一个中断服务例程。
228~307 默认情况处理不识别以太网类型或者按802.3标准(例如OSI无连接传输)封装的分组。
注意:以太网的type字段和802.3的length字段在一个以太网帧中占用同一位置。两种封装能够分辨出来,因为一个以太网封装的类型范围和802.3封装的长度范围是不同的,如图8所示。
图8 以太网type字段和802.3length字段
以太网和802.3标准的帧格式如9所示。
图9 以太网和802.3标准帧格式
c. 分组排队
308~315 ether_input把分组放到选择的队列中,若队列已满,则丢弃此分组。IP和ARP队列的默认长度限制分别为50个分组。
当ether_input返回时,设备驱动程序通知硬件它已经准备接收下一分组,这时下一分组可能已存在于设备中。当schednetisr调度的软件中断发生时,处理分组输入队列;准确地说,调用ipintr来处理IP输入队列中的分组。
4. ether_output函数
我们现在查看以太网帧的输出,当一个网络层协议,如IP协议,调用此接口的ifnet结构中指定的函数if_output时,开始处理输出。所有的以太网设备的if_output都是ether_output。ether_output用14字节以太网首部封装一个以太网帧的数据部分,并将它放置到接口的发送队列中。这个函数比较长,我们分四个部分来说明:
验证
特定协议处理
构造帧
接口排队
图10包括这个函数的第一个部分。
图10 函数ehter_output:验证
49~64 ether_output的参数有:ifp,它指向输出接口的ifnet结构;m0,要发送的分组;dst,分组的目标地址;rt0,路由信息。
65~67 在ether_output中多次调用宏senderr。
# define senderr(e) { error = (e); goto bad;}
senderr保存差错码,并跳到函数的尾部bad,在那里分组被丢弃,并且返回err。
如果接口启动并在运行,则更新接口的上次更新时间,否则返回ENETDOWN。
a. 主机路由
68~74 rt0指向ip_output找到的路由项,并传递给ether_output。如果从BPF调用ether_output,rt0可以为空。在这种情况下,控制转给图11中的代码。否则,验证路由。如果路由无效,参考路由表查找新的路由,如若没找到,返回EHOSTUNREACH。这时,rt0和rt指向到下一跳目的地的有效路由。
图11 函数ether_output:网络协议处理
b. 网关路由
75~85 如果分组的下一跳是一个网关,而不是最终目的,那么找到一个到此网关的路由,并且rt指向它。如果不能发现一个网关路由,则返回EHOSTUNERACH。这时,rt指向下一跳目的地的路由。下一跳可能是一个网关或者最终目标地址。
c. 避免ARP泛洪
86~90 当目标方不准备响应ARP请求时,ARP代码设置标志RTF_REJECT来丢弃到达目标方的分组。
ether_output根据此分组的目标地址继续处理。因为以太网设备仅响应以太网地址,要发送一个分组,ether_output必须发现下一跳目的地的IP地址所对应的以太网地址。ARP协议用来实现这个转换,图11显示了驱动程序是如何访问ARP协议的。
d. IP输出
91~101 ether_output根据目标地址中的sa_family进行跳转。图11仅显示了case为AF_INET、AF_ISO和AF_UNSPEC的代码,但略过了case为AF_ISO的代码。
case为AF_INET时调用arpresolve来决定与目标IP地址相应的以太网地址。如果以太网地址已存在于ARP高速缓存中,则arpresolve返回1,并且ether_output继续执行。否则,这个IP分组由ARP控制,并且ARP判断地址,从函数in_arpinput调用ether_output。
假设ARP高速缓存包含了当前设备硬件地址,ether_output检查是否分组要广播,并且接口是否单向(例如,它不能接收自己发送的分组)。如果成立,则m_copy复制这个分组。在执行switch后,这个复制的分组同到达以太网接口的分组一样进行排队。这是广播定义的要求,发送主机必须接收这个分组的一个备份。
e. 显示以太网输出
142~146 有些协议,如ARP,需要显示地指定以太网目的地和类型。地址族类常量AF_UNSPEC指出:dst指向一个以太网首部。bcopy复制edst中的目标地址,并把以太网类型设为type,它不必调用arpresolve,因为以太网目标地址已由调用者显示提供了。
f. 未识别的地址族类
147~151 未识别的地址族类产生一个控制台消息,并且返回EAFNOSUPPORT。图12所示的是ether_output的下一部分:构造以太网帧。
图12 函数ether_output:构造以太网
g. 以太网首部
152~167 如果在switch中的代码复制了这个分组,这个分组副本同在输出接口上接收到的分组一样,也是通过调用looutput来处理。
M_PREPEND确保在分组的前面保留一些空间,因此,M_PREPEND仅需要调整一些指针。
ether_output用type、edst和ac_enaddr构成以太网首部。ac_enaddr是与此输出接口关联的以太网单播地址,并且是所有从此接口传输帧的源地址。ether_header用ac_enaddr重写调用者可能在ether_header结构中指定的源地址,这使得伪造一个以太网帧变得更难。
这时,mbuf包含一个除32bitCRC以外的完整以太网帧,CRC由以太网硬件在传输时计算。图13所示的代码是对设备要传送的帧进行排队。
图13 函数ether_output:输出排队
168~185 如果输出队列已满,ether_output丢弃此帧,并返回ENOBUFS。否则将这个帧放置到接口的发送队列中,并且若接口未激活,接口的if_start函数开始传输帧。
186~190 宏senderr跳到bad,在这里帧被丢弃,并返回一个差错码。
5. lestart函数
函数lestart从接口输出队列中取出排队的帧,并交给LANCE以太网发送。如果设备空闲,调用此函数开始发送帧。例如,图13中ether_output的最后,直接通过接口的if_start函数调用lestart。
如果设备忙,当它完成了当前帧的传输后产生一个中断。设备调用lestart来退队并传输下一帧。一旦开始,协议层不再调用lestart来排队帧,因为驱动程序不断退队,直到队列为空为止。
图14所示的是函数lestart。lestart假设已调用splimp来阻塞所有设备中断。
图14 函数lestart
a. 接口必须初始化
325~333 如果接口没有初始化,lestart立即返回。
b. 将帧从输出队列退队
335~342 如果接口已初始化,下一帧从队列中移去。如果接口输出队列为空,则lestart返回。
c. 传输帧并传递给BPF
343~350 leput将m中的帧复制到leput第一个参数所指向的硬件缓存中。如果接口带有BPF,将帧传给bpf_tap。关于硬件缓存中帧传输的设备专用初始化代码不予讨论。
d. 如果设备准备好,重复发送多帧
359 当le->sc_txcnt等于LETBUF时,lestart停止给设备传送帧。有些以太网接口能排队多个以太网输出帧。对于LANCE驱动器,LETBUF是此驱动器硬件传输缓存的可用个数,并且le->sc_txcnt保持跟踪有多少个缓存被使用。
e. 将设备标记为忙
360~362 最后,lestart在ifnet结构中设置IFF_OACTIVE来标识这个设备忙于传输帧。
ioctl系统调用
ioctl系统调用提供一个通用命令接口,一个进程用它来访问一个设备的标准系统调用所不支持的特性。ioctl的原型为:
int ioctl( int fd, unsigned long com, ...)
fd是一个描述符,通常是一个设备或者网络连接。每种类型的描述符都支持它自己的一套ioctl命令,这套命令由第二个参数com来指定。第三个参数在原型中显示为“...”,因为它是依赖于被调用的ioctl命令的类型的指针。如果命令要取回信息,第三个参数必须是指向一个足够保存数据的缓存的指针。在本文中,我们仅讨论用于插口描述符的ioctl命令,图15列出常用的ioctl命令。
图15 接口ioctl的命令
第一列显示的符号常量标识ioctl命令(第二个参数,com)。第二列显示了传递第一列所显示命令的系统调用时第三个参数的类型。第三列是实现这个命令的函数名称。
图16显示了处理ioctl命令的各种函数的组织。带阴影的函数我们在本章中说明。
图16 ioctl命令的函数组织
1. ifioctl函数
系统调用ioctl将图15中所列的5种命令传递给图17所示的ifioctl函数。
图17 函数ifioctl:综述与SIOCGIFCONF
394~405 对于命令SIOCGIFCONF,ifioctl调用ifconf来构造一个可变长ifreq结构的表。
406~410 对于其它ioctl命令,数据参数是指向一个ifreq结构的指针。ifunit在ifnet列表中查找名称为进程在ifr->ifr_name中提供的文本名称(如"sl0","le1")的接口。如果没有匹配的接口,ifioctl返回ENXIO。剩下的代码依赖于cmd,它们在图24中说明。
447~454 如果接口ioctl命令不能被识别,ifioctl把命令发送给与所请求插口关联的协议的用户请求函数。如果控制到达switch语句外,返回0。
2. ifconf函数
ifconf为进程提供一个标准的方法来发现一个系统中的接口和配置的地址。由结构ifreq和ifconf表示的接口如图18、19所示。
图18 结构ifreq
图19 结构ifconf
262~279 在ifreq结构中,ifr_name表示接口名称。在union中的其它成员被各种ioctl命令访问。通常,用宏来简化对联合体的成员的访问语法。
292~300 在结构ifconf中,ifc_len是ifc_buf指向的缓存的字节数。这个缓存由进程分配,但由ifconf用一个具有可变长ifreq结构的数组来填充。对于函数ifconf,ifr_addr是结构ifreq中联合的相关成员。ifreq结构都是可变长度,因为ifr_addr(一个sockaddr结构)的长度根据地址的类型而变。必须用结构sockaddr的成员sa_len来定位每项的结束,图20说明了ifconf所维护的数据结构。
图20 ifconf数据结构
在图20中,左边的数据在内核中,而右边的数据在一个进程中。我们用这个图来讨论图21所示的ifconf函数。
图21 函数ifconf
462~474 ifconf的两个参数是:cmd,它被忽略;data,它指向此进程指定的ifconf结构的一个副本。
ifc为一个ifconf结构指针,指向data;ifp从ifnet(列表头)开始遍历接口列表,而ifa遍历每个接口的地址列表。cp和ep控制构造在ifr中的接口文本名称,ifr是一个ifreq结构,它在接口名称和地址复制到进程的缓存前保存接口名称和地址。ifrq指向这个缓存,并且在每个地址被复制后指向下一个。space是进程缓存中剩余字节的个数,cp用来搜索名称的结尾,而ep标志接口名称数字部分最后的可能位置。
475~488 for循环遍历接口列表。对于每个接口,文本名称被复制到ifr_name,在ifr_name的后面跟着if_unit数的文本表示。如果没有给接口分配地址,一个全0的地址被构造,所得到的ifreq结构被复制到进程中,并减少space,增加ifrp。
489~515 如果接口有一个或者多个地址,用for循环来处理每个地址。地址加到ifr中的接口名称中,然后ifr被复制到进程中。长度超过标准sockaddr结构的地址不放到ifr中,并且直接复制到进程。在复制完每个地址后,调整space和ifrp的值。所有接口处理完后,更新缓存长度(ifc->ifc_len),并且ifconf返回。系统调用ioctl负责将结构ifconf中新的内容复制回进程中的结构ifconf。
图22显示了以太网、SLIP和环回接口初始化的接口结构的配置。
图22 接口和地址数据结构
执行下列代码后的ifc和buffer的内容如图23所示。
struct ifconf ifc;
char buffer[144];
int s
ifc.ifc_len = 144
ifc.ifc_buf = buffer;
if ( ioctl(s, SIOCGIFCONF, &ifc)<0 ){
perror("ioctl failed");
exit(1);
}
这里对命令SIOCGIFCONF操作的插口类型没有限制,如我们所看到的,这个命令返回所有协议族类的地址。
在图22中,因为缓存中返回的三个地址仅占用108(36*3)字节,ioctl将ifc_len由144改为108。返回三个sockaddr_dl地址,并且这个缓存后面的36个字节未用。每项的前16个字节包含接口的文本名称,在这16个字节中只有前3个字节被使用。
图23 SIOCGIFCONF 命令返回后的数据
ifr_addr为一个sockaddr结构的形式,因此第一个值为长度(20字节),且第二个值为地址的类型(18,AF_LINK)。接下来一个值为sdl_index,与sdl_type一样,对于每个接口,它是不同的(IFT_ETHER、IFT_SLIP和IFT_LOOP相对应的值为6、28和24)。
下面三个值为sa_nlen(文本名称长度)、sa_alen(硬件地址长度)及sa_slen(未用,总为0)。
最后,是接口的文本名称,其后面是硬件地址(仅对于以太网)。SLIP和环回接口在sockaddr_dl结构中不存放一个硬件级地址。
此例中,仅返回sodkaddr_dl地址,因为图22中未配置其它地址类型,因此缓存中每项大小一样。如果为每个接口配置其它地址(如IP地址),它们会同sockaddr_dl地址一起返回,并且每项的大小根据返回的地址类型不同而不同。
3. 通用接口ioctl命令
图15中剩下的四个接口命令(SIOCGIFFLAGS、SIOCGIFMETRIC、SIOCSIFFLAGS和SIOCSIFMETRIC)由函数ifioctl处理。图24所示的是处理这些命令的case语句。
图24 函数ifioctl:标志和度量
a. SIOCGIFLAGS和SIOCGIFMETRIC
410~416 对于两个SIOCGxxx命令,ifioctl将每个接口的if_flags或者if_metric值复制到ifreq结构中。标志使用联合成员ifr_flags;而度量使用成员ifr_metric。
b. SIOCSIFFLAGS
417~429 为改变接口的标志,调用进程必须有超级用户权限。如果进程正在关闭一个运行的接口或者启动一个未运行的接口,分别调用if_down和if_up。
c. 忽略标志IFF_CANTCHAGE
430~434 有些接口标志不能被进程改变。表达式(ifp->if_flags&IFF_CANTCHANGE)清除能被进程改变的接口标志,而表达式(ifr->ifr_flags&~IFF_CANTCHANGE)清除在请求中不被进程改变的标志。这两个表达式进行或运算作为新值保存在ifp->if_flags中。返回前,请求被传递给设备相关联的if_ioctl函数(例如:LANCE驱动器的leioctl图26)。
d. SIOCSIFMETRIC
435~439 改变接口的度量要容易些;进程同样要有超级用户权限,ifioctl将接口新的度量复制到if_metric中。
4. if_down和if_up函数
利用程序ifconfig,一个管理员可以通过命令SIOCSIFFLAGS设置或者清除标志IFF_UP来启动或者禁用一个接口。图25显示了函数if_down和if_up的代码。
图25 函数if_down和if_up
292~302 当一个接口被关闭时,IFF_UP标志被清除并且对与接口关联的每个地址用pfctlinput发送命令PRC_IFDOWN。这给每个协议一个机会来响应被关闭的接口。有些协议,如OSI,要使用接口来终止连接。对于IP,如果可能,要通过其它接口为连接进行重新路由。TCP和UDP忽略失效接口,并依赖路由协议去发现分组的可选路径。
if_qflush忽略接口的任何排队分组。rt_ifmsg通知路由系统发生的变化。TCP自动重传丢失的分组;UDP应用必须显示的检测这种情况并对此作出响应。
308~315 当一个接口被启用时,IFF_UP标志被设置,并且rt_ifmsg通知路由系统接口状态发生变化。
5. 以太网、SLIP和环回
图24中处理SIOCSIFFLAGS命令的代码,ifioctl调用接口的if_ioctl函数。在我们的三个例子接口中,函数slioctl和loioctl忽略SIOCSFFLAGS命令而返回EINVAL。图26显示了函数leioctl及LANCE以太网驱动程序的SIOCSIFFLAGS命令的处理。
图26 函数leioctl
614~623 leioctl把第三个参数data转换为一个ifaddr结构的指针,并保存在ifa中。le指针引用下标为ifp->unit的le_softc结构。基于cmd的switch语句构成了这个函数的主体。
638~656 图26中仅显示了case为SIOCSIFFLAGS的情况。这次ifioctl调用leioctl,接口标志被改变,强制物理接口进入标志所配置的状态。如果要关闭接口,但接口正在工作,则关闭接口。若要启动未操作的接口,接口被初始化并重启。
672~677 处理未识别命令的default情况发送EINVAL,并在函数结尾返回。
更多最新文章尽在公众号:大白爱爬山,欢迎关注!