Unix/Linux编程:接口层---以太网

引言

这里我们讨论了所有接口要用到的数据结构以及对这个数据结构的初始化。本文中我们将说明以太网设备驱动程序在初始化后是如何接收和传输帧的

如果读者对一个驱动程序的源代码感兴趣,Net/3版本中包括很多不同接口的源代码。要想研究接口的技术规范,就要求理解设备专用的命令。下图所示的是Net/3提供的各种驱动程序
在这里插入图片描述

网络设备驱动程序通过ifnet结构中的7个函数指针来访问。下图列出了指向我们的三个例子驱动程序的入口点
在这里插入图片描述
只有函数if_output和if_ioctl被经常的调用。而if_init、if_done和if_reset从来不被调用或者仅从设备专用代码调用。函数if_start仅被函数ether_output调用

代码介绍

以太网设备驱动程序和通用接口ioctl的代码包含在两个头文件和三个C文件中。如下所示

在这里插入图片描述

全局变量

下图显示的全局变量包括协议输入队列LANCE接口结构和以太网广播地址
在这里插入图片描述

le_softc是一个数组,因为这里可以有多个以太网接口

统计量

结构ifnet中为每个接口收集的统计量如下图所示:
在这里插入图片描述

SNMP变量

下图所示的是SNMP接口表(ifTable)中的一个接口项对象(ifEntry),它包含在每个接口的ifnet结构中

SNMP变量ifnet成员说明
ifIndexif_index唯一地标识接口
ifDescrif_name接口的文本名称
ifTypeif_type接口的类型(例如以太网、S L I P等等)
ifMtuif_mtu接口的MTU(字节)
ifSpeed(看正文)接口的正常速率(每秒比特)
ifPhyAddressac_enaddr媒体地址(来自结构arpcom)
ifAdminStatus(看正文)接口的期望状态(IFF_UP标志)
ifOperStatusif_flags接口的操作状态(IFF_UP标志)
ifLastChange(看正文) 上一次统计改变时间
ifInOctetsif_ibytes输入的字节总数
ifInUcastPktsif_ipackets -if_imcasts输入的单播分组数
ifInNUcastPktsif_imcasts
ifInDiscardsif_iqdrops因为实现的限制而丢弃的分组数
ifInErrorsif_ierrors差错的分组数
ifInUnknownProtosif_noproto指定为未知协议的分组数
ifOutOctetsif_obytes输出字节数
ifOutUcastPktsif_opackets-if_omcasts输出的单播分组数
ifOutNUcastPktsif_omcasts输出的广播或多播分组数
ifOutDiscardsif_snd.ifq_drops因为实现的限制而丢失的输出分组数
ifOutErrorsif_oerrors因为差错而丢失的输出分组数
ifOutQLenif_snd.ifq_len输出队列长度
ifSpecificn/a媒体专用信息的SNMP对象ID(未实现)
  • ISOMD SNMP代理从if_type获得ifSpeed,并为ifAdminStatus维护一个内部变量。
  • 代理的ifLastChange基于结构ifnet中的if_lastchange,但与代理的启动时间相关,而不是与系统的启动时间相关。
  • 代理为ifSpecific返回一个空变量

以太网接口

Net/3以太网设备驱动程序都遵循同样的设计。对于大多数Unix设备驱动程序来说,都是这样,因为写一个新接口卡的驱动程序总是在一个已有的驱动程序的基础上修改而来的。在本节,我们简要的概述一下以太网的标准和一个以太网驱动程序的设计。

下图说明了一个IP分组的以太网封装
在这里插入图片描述

以太网帧包括48bit的目标地址和源地址,接下来是一个16bit的类型字段,它标识这个帧所携带的数据的格式。对于IP分组,类型是0x0800(2048)。帧的最后是一个32bit的CRC,用来检测帧中的差错

我们用48bit的以太网地址作为硬件地址。IP地址到硬件地址之间的转换用ARP协议,硬件地址到IP地址的转换用RARP协议。

以太网地址有两种类型,单播和多播。一个单播地址描述一个单一的以太网接口,而一个多播地址描述一组以太网接口。一个以太网广播地址是一个所有接口都接收的多播。以太网单播地址由设备的厂商分配,也有一些设备的地址允许用软件改变。

下图列举了以太网接口的数据结构和函数:

  • 一个椭圆标识一个函数
  • 一个方框标识一个数据结构(比如le_softc[0])
  • 一个圆角方框标识一组函数(比如ARP协议)
    在这里插入图片描述

上图左上角显示的是OSI无连接网络层(clnl)协议、IP和ARP输入队列===>也就是说,ether_input函数要将以太网帧分用到多个协议队列中。

leintr函数

我们从以太网帧的接收开始。现在,假设硬件已初始化并且系统已完成配置,当接口产生一个中断时,leintr被调用。在正常操作中,一个以太网接口接收发送到它的单播地址和以太网广播地址的帧。当一个完整的帧可用时,接口就产生一个中断,并且内核调用leintr。

leintr检测硬件,并且如果有一个帧到达,就调用leread把这个帧从接口转移到另一个mbuf中(用m_devget)。如果硬件报告一个帧已完成传输或者发现一个差错,则leintr更新相应的接口统计,复位这个硬件,并调用restart来传输另一个帧

所有以太网设备驱动程序将它们接收到的帧传递给ether_input做进一步的处理。设备驱动程序构造的mbuf链不包括以太网首部,以太网首部ether_header作为一个独立的参数传递给ether_input。ether_header如下:

//4.4BSD-Lite\usr\src\sys\netinet\if_ether.h
/*
 * Structure of a 10Mb/s Ethernet header.
 */
struct	ether_header {
	u_char	ether_dhost[6];
	u_char	ether_shost[6];
	u_short	ether_type;
};
  • 以太网CRC并不总是可用。它由接口硬件计算与检验,接口硬件丢弃到达的CRC差错帧。以太网设备驱动程序负责ether_type的网络和主机字节序之间的转换。在驱动程序外,它总是主机字节序

leread函数

函数leread的开始是由leintr传给它的一个连续的内存缓冲区,并且构造了一个ether_header结构和一个mbuf链。这个链表存储来自以太网帧的数据。leread还将输入帧传给BPF。

// 4.4BSD-Lite\usr\src\sys\hp300\dev\if_le.c


leread(int unit, char *buf, int len)
{
	register struct le_softc *le = &le_softc[unit];
	register struct ether_header *et;
    	struct mbuf *m;
	int off, resid, flags;

	le->sc_if.if_ipackets++;
	et = (struct ether_header *)buf;
	et->ether_type = ntohs((u_short)et->ether_type);
	/* adjust input length to account for header and CRC */
	len = len - sizeof(struct ether_header) - 4;

	if (len <= 0) {
		if (ledebug)
			log(LOG_WARNING,
			    "le%d: ierror(runt packet): from %s: len=%d\n",
			    unit, ether_sprintf(et->ether_shost), len);
		le->sc_runt++;
		le->sc_if.if_ierrors++;
		return;
	}
	flags = 0;
	if (bcmp((caddr_t)etherbroadcastaddr,
	    (caddr_t)et->ether_dhost, sizeof(etherbroadcastaddr)) == 0)
		flags |= M_BCAST;
	if (et->ether_dhost[0] & 1)
		flags |= M_MCAST;

#if NBPFILTER > 0
	/*
	 * Check if there's a bpf filter listening on this interface.
	 * If so, hand off the raw packet to enet.
	 */
	if (le->sc_if.if_bpf) {
		bpf_tap(le->sc_if.if_bpf, buf, len + sizeof(struct ether_header));

		/*
		 * Keep the packet if it's a broadcast or has our
		 * physical ethernet address (or if we support
		 * multicast and it's one).
		 */
		if (
#ifdef MULTICAST
		    (flags & (M_BCAST | M_MCAST)) == 0 &&
#else
		    (flags & M_BCAST) == 0 &&
#endif
		    bcmp(et->ether_dhost, le->sc_addr,
			sizeof(et->ether_dhost)) != 0)
			return;
	}
#endif
	/*
	 * Pull packet off interface.  Off is nonzero if packet
	 * has trailing header; m_devget will then force this header
	 * information to be at the front, but we still have to drop
	 * the type and length which are at the front of any trailer data.
	 */
	m = m_devget((char *)(et + 1), len, off, &le->sc_if, 0);
	if (m == 0)
		return;
	m->m_flags |= flags;
	ether_input(&le->sc_if, et, m);
}
  • 如下,函数leintr给leread传了三个参数:
    • unit:它标识接收到此帧的特定接口卡
    • buf:它指向接收到的帧
    • len:它是帧的字节数(包括首部和CRC)
  • 函数et指向这个缓存的起始,并且将以太网字节序转换成主机字节序,来构造结构ether_header
leread(int unit, char *buf, int len)
{
	struct le_softc *le = &le_softc[unit];
	struct ether_header *et;
    struct mbuf *m;
	int off, resid, flags;

	le->sc_if.if_ipackets++;
	et = (struct ether_header *)buf;
	et->ether_type = ntohs((u_short)et->ether_type);
  • 将len减去以太网首部和CRC的大小得到数据的字节数。短分组是一个长度太短的非法一条短帧,它被记录、统计、丢弃
/* adjust input length to account for header and CRC */
	len = len - sizeof(struct ether_header) - 4;

	if (len <= 0) {
		if (ledebug)
			log(LOG_WARNING,
			    "le%d: ierror(runt packet): from %s: len=%d\n",
			    unit, ether_sprintf(et->ether_shost), len);
		le->sc_runt++;
		le->sc_if.if_ierrors++;
		return;
	}
  • 接下来,目标地址被检测,并判断是不是以太网广播或多播地址。以太网广播地址是一个以太网多播地址的特例;它的每一帧都被设置了。
	flags = 0;
	if (bcmp((caddr_t)etherbroadcastaddr,
	    (caddr_t)et->ether_dhost, sizeof(etherbroadcastaddr)) == 0)
		flags |= M_BCAST;
	if (et->ether_dhost[0] & 1)
		flags |= M_MCAST;

etherbroadcastaddr是一个数组,定义如下:

u_char etherbroadcastaddr[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};

这是C语言中定义一个48bit值的简便方法。这项技术仅在我们假设字符是8bit值时才起作用-----ANSI C并不保证这一点。

bcmp比较etherbroadcastaddr和ether_dhost,如果相同,则设置标志M_BCAST。

一个以太网多播地址由这个地址的首字节的低位比特来标识,如下图所示:
在这里插入图片描述
并不是所有以太网多播帧都是IP多播数据报,并且IP必须进一步检测这个分组。

如果这个地址的多播比特被置位,在mbuf首部中设置M_MCAST。检测的顺序是重要的:首先ether_input将这个48bit地址和以太网广播地址相比较,如果不同,则检测标识以太网多播地址的首字节的低位比特。

  • 如果接口带有BPF,调用bpf_tap把这个帧直接传给BPF。我们会看见对于SLIP和换回接口,要构造一个特定的BPF帧,因为这些网络没有一个链路层首部(不像以太网)

当一个接口带有BPF时,它可以配置为运行在混淆模式,并且接口网络上出现的所有以太网帧,而不是通常由硬件接收的帧的子集。如果分组发送给一个不与此接口地址匹配的单播地址,则被leread丢弃。

	/*
	 * 检查是否有一个bpf滤波器在这个接口上监听。如果是这样,就把原始数据包交给enet。
	 */
	if (le->sc_if.if_bpf) {
		bpf_tap(le->sc_if.if_bpf, buf, len + sizeof(struct ether_header));

		/*
		 * 如果数据包是广播或具有我们的物理以太网地址(或者如果我们支持多播且它是一个),则保留该数据包。
		 */
		if (
#ifdef MULTICAST
		    (flags & (M_BCAST | M_MCAST)) == 0 &&
#else
		    (flags & M_BCAST) == 0 &&
#endif
		    bcmp(et->ether_dhost, le->sc_addr,
			sizeof(et->ether_dhost)) != 0)
			return;
	}
  • m_devget将数据从传给leread的缓存中复制到一个它分配的mbuf链中。传给m_devget的第一个参数指向以太网首部后的第一个字节,它是此帧中的第一个数据字节。如果m_devget内存用完,leread立即返回。另外广播和多播标志被设置在链表中的第一个mbuf中,ether_input处理这个分组
	m = m_devget((char *)(et + 1), len, off, &le->sc_if, 0);
	if (m == 0)
		return;
	m->m_flags |= flags;
	ether_input(&le->sc_if, et, m);

ether_input函数

如下,函数ether_input的参数有:

  • ifp,一个指向接收此分组的接口的ifnet结构的指针
  • eh,一个指向接收分组的以太网首部的指针
  • m,一个指向接收分组的指针(不包括以太网首部)

任何到达不工作接口的分组将被丢弃。可能没有为此接口配置一个协议地址,或者接口可能被ifconfig显示的禁用了

//  F:\source\4.4BSD-Lite\usr\src\sys\net\if_ethersubr.c

void
ether_input(struct ifnet *ifp, struct ether_header *eh, struct mbuf *m)
{
	register struct ifqueue *inq;
	register struct llc *l;
	struct arpcom *ac = (struct arpcom *)ifp;
	int s;

	if ((ifp->if_flags & IFF_UP) == 0) {
		m_freem(m);
		return;
	}
  • 变量time是一个全局的timeval结构,内核用它维护当前时间和日期,它是从1970/1/1/00/00/00开始的秒和微秒数。
  • ether_input用当前时间更新if_lastchange ,并且把if_ibytes加上输入分组的长度(分组长度加上14字节的以太网首部)
  • 然后,ether_input再次用leread去判断分组是否为一个广播或者多播分组。
  • 有些内核编译时可能没有包括BPF代码,因此测试必须在ether_input中进行
    ifp->if_lastchange = time;
	ifp->if_ibytes += m->m_pkthdr.len + sizeof (*eh);
	if (bcmp((caddr_t)etherbroadcastaddr, (caddr_t)eh->ether_dhost,
	    sizeof(etherbroadcastaddr)) == 0)
		m->m_flags |= M_BCAST;
	else if (eh->ether_dhost[0] & 1)
		m->m_flags |= M_MCAST;
	if (m->m_flags & (M_BCAST|M_MCAST))
		ifp->if_imcasts++;

链路层分用

  • ether_input根据以太网类型字符来跳转。对于一个IP分组,schednetisr调度一个IP软件中断,并选择IP输入队列,ipintrq。对于一个ARP分组,调度ARP软件中断,并选择arpintrq
	switch (eh->ether_type) {
#ifdef INET
	case ETHERTYPE_IP:
		schednetisr(NETISR_IP);
		inq = &ipintrq;
		break;

	case ETHERTYPE_ARP:
		schednetisr(NETISR_ARP);
		inq = &arpintrq;
		break;
#endif

分组排队

  • 最后,ether_input把分组放置到选择的队列中,如果队列满了,则丢弃此分组。IP和ARP队列的默认限制为每个50个分组(ipqmaxlen)
  • 当ether_input返回时,设备驱动程序通知硬件它已准备接收下一分组,这时下一分组可能已存在设备中。当schednetisr调度的软件中断发生时,处理分组输入队列。准确的说,调用ipintr来处理IP输入队列中的分组,调用arpintr来处理ARP输入队列中的分组
	s = splimp();
	if (IF_QFULL(inq)) {
		IF_DROP(inq);
		m_freem(m);
	} else
		IF_ENQUEUE(inq, m);
	splx(s);

ether_output函数

我们现在查看以太网帧的输出,当一个网络层协议,比如IP,调用此接口ifnet结构中指定的函数if_output时,开始处理输出。所有以太网设备的if_output是ether_output。ether_output用14字节以太网首部封装一个以太网帧的数据部分,并将它放置到接口的发送队列中。这个函数比较大,我们分四个部分来说明:

验证

  • 如下,ether_output的参数有:
    • ifp,它指向输出接口的ifnet结构
    • m0,要发送的分组
    • dst,分组的目标地址
    • rt0,路由信息
int ether_output(struct ifnet *ifp, struct mbuf *m0, struct sockaddr *dst, struct rtentry *rt0)
{
	short type;
	int s, error = 0;
 	u_char edst[6];
	register struct mbuf *m = m0;
	register struct rtentry *rt;
	struct mbuf *mcopy = (struct mbuf *)0;
	register struct ether_header *eh;
	int off, len = m->m_pkthdr.len;
	struct arpcom *ac = (struct arpcom *)ifp;
  • 在ether_output中多次调用宏senderr:#define senderr(e) { error = (e); goto bad;}
  • senderr保存差错码,并跳到函数的尾部bad,在那里分组被丢弃,并且ether_output返回error
  • 如果接口启动并在运行,ether_output更新接口的上次更改时间。否则,返回ENETDOWN。

	if ((ifp->if_flags & (IFF_UP|IFF_RUNNING)) != (IFF_UP|IFF_RUNNING))
		senderr(ENETDOWN);
	ifp->if_lastchange = time;

主机路由

  • 如下,rt0指向ip_output找到的路由项,并传递给ether_output。如果从BPF调用ether_output,rt0可以为空。此时,控制转到下面【IP输出】部分中。否则,验证路由。如果路由无效,参考路由表,并且当路由不能被找到时,返回EHOSTUNREACH。这时,rt0和rt指向一个到下一跳目的地的有效路由。
if (rt = rt0) {
		if ((rt->rt_flags & RTF_UP) == 0) {
			if (rt0 = rt = rtalloc1(dst, 1))
				rt->rt_refcnt--;
			else 
				senderr(EHOSTUNREACH);
		}

网关路由

  • 如果分组的下一跳是一个网关(而不是最终目的),找到一个到此网关的路由,并且rt指向它。如果不能发现一个网关路由,则返回EHOSTUNREACH。这时,rt指向下一跳目的地的路由。下一跳可能是一个网关或最终目标
if (rt->rt_flags & RTF_GATEWAY) {
			if (rt->rt_gwroute == 0)
				goto lookup;
			if (((rt = rt->rt_gwroute)->rt_flags & RTF_UP) == 0) {
				rtfree(rt); rt = rt0;
			lookup: rt->rt_gwroute = rtalloc1(rt->rt_gateway, 1);
				if ((rt = rt->rt_gwroute) == 0)
					senderr(EHOSTUNREACH);
			}
		}

避免ARP泛洪

  • 当目标不准备响应ARP请求时,ARP代码设置标志RTF_REJECT来丢弃到达目标方的分组。
if (rt->rt_flags & RTF_REJECT)
			if (rt->rt_rmx.rmx_expire == 0 ||
			    time.tv_sec < rt->rt_rmx.rmx_expire)
				senderr(rt == rt0 ? EHOSTDOWN : EHOSTUNREACH);

IP输出

  • ether_output根据目标地址中的sa_family进行跳转。
    • case AF_INET调用arpresolve来决定与目标IP地址相对应的以太网地址。如果以太网地址已经存在于ARP高速缓存中,则arpresolve返回1,并且ether_output继续执行。否则,这个IP分组由ARP控制,并且ARP判断地址,从函数in_arpinput调用ether_output
    • 假设ARP高速缓存包含硬件地址,ether_output检查是否分组要广播,并且接口是否是单向的(例如,它不能接收自己发送的分组)。如果都成立,则m_copy复制这个分组,在执行switch之后,这个复制的分组同到达以太网接口的分组一样进行排队。这时广播定义的要求,发送主机必须接收这个分组的一个备份
switch (dst->sa_family) {

#ifdef INET
	case AF_INET:
		if (!arpresolve(ac, rt, m, dst, edst))
			return (0);	/* if not yet resolved */
		/* If broadcasting on a simplex interface, loopback a copy */
		if ((m->m_flags & M_BCAST) && (ifp->if_flags & IFF_SIMPLEX))
			mcopy = m_copy(m, 0, (int)M_COPYALL);
		off = m->m_pkthdr.len - m->m_len;
		type = ETHERTYPE_IP;
		break;
#endif
#ifdef NS
	case AF_NS:
		type = ETHERTYPE_NS;
 		bcopy((caddr_t)&(((struct sockaddr_ns *)dst)->sns_addr.x_host),
		    (caddr_t)edst, sizeof (edst));
		if (!bcmp((caddr_t)edst, (caddr_t)&ns_thishost, sizeof(edst)))
			return (looutput(ifp, m, dst, rt));
		/* If broadcasting on a simplex interface, loopback a copy */
		if ((m->m_flags & M_BCAST) && (ifp->if_flags & IFF_SIMPLEX))
			mcopy = m_copy(m, 0, (int)M_COPYALL);
		break;

显示以太网输出

  • 有些协议,比如ARP,需要显示的指定以太网目的地和类型。地址族类常量AF_UNSPEC指出:dst指向一个以太网首部。bcopy复制edst中的目标地址,并把以太网类型设置为type。它不必调用arpresolve(如AF_INET),因为以太网目标地址已由调用者显示提供了
	case AF_UNSPEC:
		eh = (struct ether_header *)dst->sa_data;
 		bcopy((caddr_t)eh->ether_dhost, (caddr_t)edst, sizeof (edst));
		type = eh->ether_type;
		break;

未识别的地址族类

  • 为失败的地址族类产生一个控制台信息,并且返回EAFNOSUPPORT
	default:
		printf("%s%d: can't handle af%d\n", ifp->if_name, ifp->if_unit,
			dst->sa_family);
		senderr(EAFNOSUPPORT);

构造以太网帧

以太网首部

  • 如果swit复制了这个分组,这个分组副本同在输出接口上接收到的分组一样通过调用looutput来处理。
  • M_PREPEND确保分组的前面有14字节的空间(大多数协议要在mbuf链表的前面留一些空间,因此,M_PREPEND仅需要调整一些指针)
  • ether_output用type、edst、ac_enaddr构成以太网首部。
    • ac_enaddr是与此输出接口关联的以太网单播地址,并且是所有从此接口传输的帧的源地址。
    • ether_header用ac_enaddr重写调用者可能在ether_header结构中指定的源地址。这使得伪造一个以太网帧的源地址变得更难
	if (mcopy)
		(void) looutput(ifp, mcopy, dst, rt);
	/*
	 * Add local net header.  If no space in first mbuf,
	 * allocate another.
	 */
	M_PREPEND(m, sizeof (struct ether_header), M_DONTWAIT);
	if (m == 0)
		senderr(ENOBUFS);
	eh = mtod(m, struct ether_header *);
	type = htons((u_short)type);
	bcopy((caddr_t)&type,(caddr_t)&eh->ether_type,
		sizeof(eh->ether_type));
 	bcopy((caddr_t)edst, (caddr_t)eh->ether_dhost, sizeof (edst));
 	bcopy((caddr_t)ac->ac_enaddr, (caddr_t)eh->ether_shost,
	    sizeof(eh->ether_shost));

接口排队

这时,mbuf包含一个除32bit CRC以外的完整以太网帧,CRC由以太网硬件在传输时计算。这时,需要先用如下代码对设备要传输的帧进行排队

  • 如果输出队列为空,ether_output丢弃此帧,并返回ENOBUFS。如果输出队列不为空,这个帧放置到接口的发送队列中,并且如果接口未激活,接口的if_start函数传输下一帧
  • 宏senderr调到bad,在这里帧被丢弃,并返回一个差错码
	s = splimp();
	/*
	 * Queue message on interface, and start output if interface
	 * not yet active.
	 */
	if (IF_QFULL(&ifp->if_snd)) {
		IF_DROP(&ifp->if_snd);
		splx(s);
		senderr(ENOBUFS);
	}
	IF_ENQUEUE(&ifp->if_snd, m);
	if ((ifp->if_flags & IFF_OACTIVE) == 0)
		(*ifp->if_start)(ifp);
	splx(s);
	ifp->if_obytes += len + sizeof (struct ether_header);
	if (m->m_flags & M_MCAST)
		ifp->if_omcasts++;
	return (error);

bad:
	if (m)
		m_freem(m);
	return (error);

lestart函数

函数lestart从接口输出队列中取出排队的帧,并交给LANCE以太网卡发送。如果设备空闲,调用此函数开始发送帧。

如果设备忙,当它完成了当前帧的传输时产生一个中断。设备调用lestart来退队并传输下一帧。一旦开始,协议层不再用调用lestart来排队帧,因为驱动程序不断退队并传输帧,直到队列为空为止。

下面分组中假设已经调用splimp来阻塞所有设备中断。

接口必须初始化

  • 如果接口没有初始化,lestart立即返回
lestart(struct ifnet *ifp)
{
	register struct le_softc *le = &le_softc[ifp->if_unit];
	register struct letmd *tmd;
	register struct mbuf *m;
	int len;

	if ((le->sc_if.if_flags & IFF_RUNNING) == 0)
		return (0);

将帧从输出队列中退队

  • 如果接口已经初始化,下一帧从队列中移除。如果接口输出队列为空,则lestart返回
tmd = &le->sc_r2->ler2_tmd[le->sc_tmd];
	do {
		if (tmd->tmd1 & LE_OWN) {
			le->sc_xown2++;
			return (0);
		}
		IF_DEQUEUE(&le->sc_if.if_snd, m);
		if (m == 0)
			return (0);

传输帧并传递给BPF

  • leput将m中的帧复制到leput第一个参数所指向的硬件缓存中。如果接口带有BPF,将帧传给bpf_tap。
len = leput(le->sc_r2->ler2_tbuf[le->sc_tmd], m);
#if NBPFILTER > 0
		/* 
		 * If bpf is listening on this interface, let it 
		 * see the packet before we commit it to the wire.
		 */
		if (ifp->if_bpf)
			bpf_tap(ifp->if_bpf, le->sc_r2->ler2_tbuf[le->sc_tmd],
				len);
#endif

如果设备准备好,重复发送多帧

  • 如果le->sc_txcnt等于LETBUF时,lestart停止给设备传送帧。有些以太网接口能排队多个以太网输出帧。对于LANCE驱动器,LETBUF是此驱动器硬件传输缓存的可用个数,并且le->sc_txcnt保存跟踪由多少个缓存被使用
} while (++le->sc_txcnt < LETBUF);

将设备标记为忙

  • 最后,lestart在ifnet结构中设置IFF_OACTIVE来表示这个设备忙于传输帧
	le->sc_if.if_flags |= IFF_OACTIVE;
	return (0);

ioctl系统调用

ioctl系统调用提供一个通用命令接口,一个进程用它来访问一个设备的标准系统调用所不支持的特性。其原型为:

int ioctl(int fd, unsigned long com, ...);
  • fd是一个描述符,通常是一个设备或网络连接。
  • 每种类型的描述符都支持它自己的一套ioctl命令,这套命令由第二个参数com来指定。
  • 第三个参数在原型中显示为"…",因为它是依赖于被调用的ioctl命令的类型的指针。如果命令要取回信息,第三个参数必须是指向一个足够保存数据的缓冲的指针。

在这里插入图片描述
第一列显示的符号常量标识 ioctl命令(第二个参数, com)。第二列显示传递给第一列所显示的命令的系统调用的第三个参数的类型。第三列是实现这个命令的函数的名称。
在这里插入图片描述

ifioctl函数

系统调用ioctl将上上图所列的5种命令传递给下面所示的ifioctl函数。

  • 对于命令SIOGIFCONF,ifioctl调用ifconf来构造一个可变长ifreq结构的表
int
ifioctl(so, cmd, data, p)
	struct socket *so;
	int cmd;
	caddr_t data;
	struct proc *p;
{
	register struct ifnet *ifp;
	register struct ifreq *ifr;
	int error;

	switch (cmd) {

	case SIOCGIFCONF:
	case OSIOCGIFCONF:
		return (ifconf(cmd, data));
	}
  • 对于其他ioctl命令,数据参数是指向一个ifreq结构的指针。ifunit在ifnet列表中查找名称的进程在ifr->ifr_name中提供的文本名称(比如"s10",“le1”,“lo0”)的接口。如果没有匹配的接口,ifioctl返回ENXIO。剩下的代码依赖cmd
    ifr = (struct ifreq *)data;
	ifp = ifunit(ifr->ifr_name);
	if (ifp == 0)
		return (ENXIO);
	switch (cmd) {
  • 如果接口ioctl命令不能被识别,ifioctl把命令发送给与所请求查看关联的协议的用户要求函数。
	default:
		if (so->so_proto == 0)
			return (EOPNOTSUPP);
#ifndef COMPAT_43
		return ((*so->so_proto->pr_usrreq)(so, PRU_CONTROL,
			cmd, data, ifp));

ifconf函数

ifconf为进程提供一个标准的方法来发现一个系统中的接口和配置的地址。

  • 一个ifreq结构包含在ifr_name中一个接口的名称。在联合中的其他成员被各种ioctl命令访问。通常,用宏来简化对联合的成员的访问语法
  • ifr_addr是结构ifreq中联合的相关成员。每个ifreq结构有一个可变长度,因为ifr_addr(一个sockaddr 结构)的长度根据地址的类型而变。必须用sockaddr 的成员sd_len来定位每项的结束
// F:\source\4.4BSD-Lite\usr\src\sys\net\if.h
struct	ifreq {
#define	IFNAMSIZ	16
	char	ifr_name[IFNAMSIZ];		/* if name, e.g. "en0" */
	union {
		struct	sockaddr ifru_addr;
		struct	sockaddr ifru_dstaddr;
		struct	sockaddr ifru_broadaddr;
		short	ifru_flags;
		int	ifru_metric;
		caddr_t	ifru_data;
	} ifr_ifru;
#define	ifr_addr	ifr_ifru.ifru_addr	/* address */
#define	ifr_dstaddr	ifr_ifru.ifru_dstaddr	/* other end of p-to-p link */
#define	ifr_broadaddr	ifr_ifru.ifru_broadaddr	/* broadcast address */
#define	ifr_flags	ifr_ifru.ifru_flags	/* flags */
#define	ifr_metric	ifr_ifru.ifru_metric	/* metric */
#define	ifr_data	ifr_ifru.ifru_data	/* for use by interface */
};
  • 在结构ifconf 中,ifc_len是ifc_buf指向的缓存的字节数。这个缓存由一个进程分配,但由ifconf用一个具有可变长ifreq结构的数组来填充。
struct	ifconf {
	int	ifc_len;		/* size of associated buffer */
	union {
		caddr_t	ifcu_buf;
		struct	ifreq *ifcu_req;
	} ifc_ifcu;
#define	ifc_buf	ifc_ifcu.ifcu_buf	/* buffer address */
#define	ifc_req	ifc_ifcu.ifcu_req	/* array of structures returned */
};

下图中,左边的数据在内核中,而右边的数据在一个进程中,我们用这个图来讨论ifconf函数:
在这里插入图片描述

  • ifconf的两个参数:cmd,它被忽略;data,它指向此进程指定的ifconf结构的一个副本
  • ifc是强制为一个ifconf结构指针的data。
  • ifp从ifnet(列表头)开始遍历接口列表
  • ifa用于遍历每个接口的地址列表
  • cp和ep控制构造在ifr的接口文本名称
  • ifr是一个ifreq结构,它在接口名称和地址复制到进程的缓存前保存接口名称和地址
  • ifrq指向这个缓存,并且在每个地址被复制后指向下一个
  • space 是进程缓存中剩余字节的个数,cp用来搜索名称的结尾,ep标识接口名称数字部分最后的可能位置
int ifconf(int cmd,  caddr_t  data)
{
	register struct ifconf *ifc = (struct ifconf *)data;
	register struct ifnet *ifp = ifnet;
	register struct ifaddr *ifa;
	register char *cp, *ep;
	struct ifreq ifr, *ifrp;
	int space = ifc->ifc_len, error = 0;

	ifrp = ifc->ifc_req;
	ep = ifr.ifr_name + sizeof (ifr.ifr_name) - 2;
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值