|
1)snull:是一种虚拟的网络模型,通过它可以基本了解真实物理网络接口设备驱动程序的运作。
2)本文代码都是标准C语言格式。
3)*skb:重要的指针量,指向一块内存区域用以缓冲待处理的进出网络数据包。
Linux网络接口设备概述
Linux中网络接口是3大标准类设备之一,其他2种类型分别是字符设备和块设备。下面介绍网络接口设备的驱动程序如何和内核模块进行交互的知识。网络设备驱动程序异步地接收来自外部世界的网络数据包,通过push操作将进来的数据包压向内核,而块设备驱动程序则将一片数据缓冲发送给内核,驱动程序都需要将相应设备的特征信息登记到内核中特定的数据结构中,块设备驱动程序可以使用文件形式来描述,而网络驱动程序则不可以使用一般的文件读写操作调用,网络设备驱动程序有自己的内存名字空间,使用push等操作来完成数据包的转换与递送。
Unix世界里的"一切皆是文件"的论述,对网络设备接口来说是不适用的。块设备在系统文件树的/dev目录下可以找到特定的文件入口标志,而网络设备则没有这种文件操作入口,一个物理网络接口上的数百个用于网络数据交换的插口可以被应用程序重复使用。
 |
| 图1 |
如上图,内核中有专门为网络设备驱动程序设计的数据包操作接口。通过网络接口sn0到达网络snullnet0就是路由的意思.使用route add -net snullnet0 dev sn0指令来完成,2.2以上的Linux内核无需这样做了,内核自动将添加。如果网络非C类地址,请用掩码指定255.255.255.0指定的是C类网络,因为在snull实验网络模型中地址机制被修改,所以发向192.168.0.33的数据包将通过sn0接口送达snullnet1中得192.168.1.33机器,送到其它网络地址的数据包将被sn1接口丢弃.说到物理数据传输,snull网络模型机制隶属于以太网Ethernet范畴.
由于以太网已经被广泛使用的原因,甚至连打印协议plip接口也声称自己隶属于以太网设备范畴.你还可以用tcpdump工具来察看snull实验网模型中的网络数据走向。使用tcpdump工具在2.0内核中加载snull设备驱动时需要显示指定Eth=1项。注意snull网络模型只能用来试验ip网络协议的数据包。利用snull网络模型传送非IP网络数据包必须对snull模块的源代码进行修改,否则会破坏其它非ip网络数据包。
Snull模型核心操作
snull核心操作大概有11多项,它们是:
ether_setup,
open,
stop,
set_config,
hard_start_xmit,
do_ioctl,get_stats,
rebuild_header,
tx_timeout,
watchdog_timeo,
flags,
hard_header_cache,
SET_MODULE_OWNER等. |
注意:snull网络不处理arp数据包,因为核心操作中的IFF_NOARP标志位.ARP是一个底层的以太网协议标准。任务是将IP地址对应到以太网物理媒质访问控制地址,也就是MAC地址。因为snull网络模拟出来的网络机器无需处理MAC地址,故不处理了。同样道理hard_header_cache也无需处理了,snull模型中将它设成了NULL。
如何操作网络设备
以下要介绍如何对网络设备展开具体操作。网络接口可以操作数据包之前必须由内核打开接口并赋予内存地址,使用ifconfig命令就可以做到,首先通过ioctl分配地址,再将IFF_UP标志位打开,这时候驱动程序的功能还没有用到,只是内核在执行而已。插槽输入输出接口标志位打开就调用了open方法打开了设备接口。关闭接口方法同理。如果执行成功将返回0,否则返回一个负数。
驱动程序的实际代码完成很多类似于char或block类型的操作,open调用并分配系统资源,stop停止并释放系统资源。接口和外部世界通讯之前需要将物理设备上的地址复制到dev->dev_addr变量中,snull模型驱动程序中使用ASCII字符串ETH_ALEN来杜撰一个物理网卡地址。
open方法也开启了一个传输队列,内核提供了一个函数来实现一个队列:
void netif_start_queue (struct net_device *dev);
另外snull模型的代码类似以下:
int snull_open(struct net_device *dev)
{
MOD_INC_USE_COUNT;
memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
dev->dev_addr[ETH_ALEN-1] += (dev - snull_devs);
netif_start_queue(dev); return 0;
}
|
因为snull模型中并没有真实的硬件网络设备所以open方法中没有太多代码,stop方法同理:
int snull_release(struct net_device *dev)
{
/*释放端口和irq,类似于fops->close */
netif_stop_queue(dev);
/*无法再传输数据了*/
MOD_DEC_USE_COUNT;
return 0;
} | 开启和关闭接口的方法是一对矛盾。
网络接口的最重要任务:发送和接受网络数据包
接着讲网络接口要完成的最重要的任务:发送和接受网络数据包。发送包容易理解所以先讲,在高级网络层中每个包都属于一个特定编号的网络插槽(socket)进出的包使用sk_buff结构变量来列表表示,插槽缓冲(sk_buff)结构变量贯穿于整个linux网络之系统中。具体定义可以在linux/skbuff.h文件中找到。
指向sk_buff变量的指针通常被称作skb,插槽缓冲是一个复杂的结构,内核负责提供一定数量的动作函数来操作该结构,开始通过调用hard_start_transmit方法函数把需要发送的包弄进出站队列,hard_start_xmit中含有包在物理媒介上的地址,skb->data指向正在传输的包,skb->len是用八进制表示的包长度。snull模型中包的传输代码和实际物理网卡的驱动代码是隔离的,snull的包传输代码如下:
int snull_tx(struct sk_buff *skb, struct net_device *dev)
{
int len;
char *data;
struct snull_priv *priv = (struct snull_priv *) dev->priv;
len = skb->len < ETH_ZLEN ? ETH_ZLEN : skb->len; data = skb->data;
dev->trans_start = jiffies;
/*保存时戳*/
priv->skb = skb;
/*记住缓冲指针以便随时中断释放,
实际传送数据与设备相关,这里省略了*/ snull_hw_tx(data, len, dev);
return 0;
}
|
那么如何并发地控制传输呢?对于缓冲暂时满载时就要通过netif_stop_queue来暂停,并使用void netif_wake_queue(struct net_device *dev)来继续重新启动,设备被分的内存中可同时有多个包缓冲。
一种机制控制设定的动作没有在规定时间内完成则被视为超过时间,有问题发生,相关的监测函数是watchdog_timeo,该函数在net_device结构中,jiffies是时间控制表。然后调用tx_timeout方法来处理。超时一旦发生,驱动程序代码必须做出错误标记,snull模型中错误一旦发生将调用snull_interrupt,netif_wake_queue等函数。
网络数据包的接收代码说明:
首先网络接口设备插槽的接受缓冲区sk_buff必须先获得一定的内存空间,而且处在缓冲区上层的程序代码可以通过中断操作和sk_buff进行数据交换。网包一般用中断机制来处理,也可以使用polling机制,但后者多用在分析内核计时器的时候。网络接口设备多数是用中断来处理的,这样可以提供吞吐量和计算性能。
在snull网络模型中,模拟出的网络数据到达内存时,对应的位置指针将提交给负责接收数据的函数snull_rx来进行处理,snull_rx函数就知道了接收到的数据长度以及在内存中的具体位置了,然后snull_rx再将数据提交到网络代码的上一层应用代码进行处理。snull_rx代码和网包数据指针的获取代码是相互独立的。
中断机制的操作柄
绝大多数硬件是通过产生中断的事件来取得和处理器的交互的,当网络硬件接口发生新的数据收发将会产生中断事件信号给处理器以获得用于处理的计算资源。注意但面对同样的中断事件时,异步的网络包传送和PLIP,PPP三种形式的底层中断代码实现是不一样的,在常见的中断例行程序代码可以找到区别。
snull模型的中断代码如下:
注意要锁住设备,数据收发工作完成后要释放缓冲占用的内存空间,这两项工作可以用
spin_lock(&priv->lock);
和
dev_kfree_skb(priv->skb);来完成。
操作器先获得准确的net_device结构指针,该指针来自于dev_id的参数。包接受工作无需调用中断操作,只要执行snull_rx动作函数就可以了。
改变联结状态的代码部分。真实网络中的传输介质上附着载波信号。拔掉网线载波信号消失,说明网络状态为关。
网络设备默认具备载波信号存在,驱动程序可通过动作函数来显式地改变状态。比如:
void netif_carrier_on (struct net_device *dev);如果驱动程序没有侦测到设备上的载波信号,那么将执行
netif_carrier_off(struct net_device *dev);动作,并告诉内核,一旦侦测到载波信号,那么将执行
void netif_carrier_on动作函数,也可用int netif_carrier_ok(struct net_device *dev);。
介质访问控制(MAC)地址的解析
介质访问控制(MAC)地址的解析简介:网络接口一般具有唯一的硬件标识号,用MAC地址来表示,如何将MAC地址和IP地址建立关联呢?下面将分别对ARP(地址解析协议),没有ARP的以太网包头的PLIP协议以及非以太网包头分别介绍。
在以太网上使用ARP,幸运的是ARP协议由内核来进行管理,网络接口不要做这些特殊的管理工作,只要接口打开时dev->addr和dev->addr_len变量被正确地赋值,驱动程序就无需去为IP地址如何解析到MAC硬件地址而担心了,ether_setup动作为dev->hard_header和dev->rebuild_header变量提供正确的设备方法,内核在缓冲管理详细的MAC地址管理的时候,调用接口驱动程序来帮助建立网络数据包,内核利用ARP查询的结果并调用驱动程序中的hard_header方法释放出网络包,网络代码员一般不必须关心这些细节。
不使用arp而直接操作硬件网包头部分的代码,修改dev->hard_header方法,snull模型就是这样:
int snull_header(struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned int len)
{
struct ethhdr *eth = (struct ethhdr *)skb_push(skb,ETH_HLEN);
eth->h_proto = htons(type);
memcpy(eth->h_source, saddr ? saddr : dev->dev_addr, dev->addr_len);
memcpy(eth->h_dest, daddr ? daddr : dev->dev_addr, dev->addr_len);
eth->h_dest[ETH_ALEN-1] ^= 0x01; return(dev->hard_header_len);
}
以上功能将内核提供的信息格式化成标准的以太网包头,同时还在目标以太网地址中设定一个位,如果包头中的pkt_type被设置成PACKET_OTHERHOST,除非接口被设置成混杂模式(promiscuous),否则这些包将被接收主机的netif_rx动作函数丢弃,plip驱动程序将硬件地址的开始的八进制设置为0xfc,而snull模型驱动设置为0x00,plip和snull模型地址效果跟以太网的点对点(ppp)连接。
接着介绍非以太网包头,具体信息可去内核源代码库中找,绝大多数驱动程序代码员只要直接采用以太网方案实现就可以了,包中的协议如何表示的呢?比如IP协议可以用ETH_P_IP来表示。用16位的二进制序列来表示协议。点对点连接来说,地址信息可以省略掉,因为目的地址和源地址已固定,只要提交协议就可以了。
字符指针变量skb->mac.raw被网络层的地址解析机制使用,在网络层中实现,具体可以参考net/ipv4/arp.c源码文件。网络设备的类型值在Linux/if_arp.h中定义,eth_type_trans函数负责为接受到的包进行以太网包头处理,例如:
skb->mac.raw = skb->data;
skb_pull(skb, dev->hard_header_len);
网络硬件设备的具体类型说明可参考drivers/net/appletalk/cops.c,drivers/net/irda/smc_ircc.c,drivers/net/ppp_generic.c等源文件。
多播网包
多播网包,就是同时发往多个网络设备的网络数据包,网包是否属于多播察看目标地址的前面几位就可以了,很多网卡硬件地址都有清楚的相应标志位,内核负责给网包付给正确的目的地址,驱动程序负责为内核控制的网络地址提供相应的数据网包就可以了,
注意:有些网络接口可能无法处理多播网包。
内核支持多播网包时,需要设备操作方法函数、数据结构、设备标志等的支持,比如:
void (*dev->set_multicast_list) (struct net_device *dev);
struct dev_mc_list *dev->mc_list;(<-与设备相关的多播网包列表)
int dev->mc_count;
IFF_MULTICAST,
IFF_ALLMULTI(多播网包路由开启时有效),
IFF_PROMISC等,
dev_mc_list结构的定义如下:
struct dev_mc_list
{
struct dev_mc_list *next;
__u8 dmi_addr[MAX_ADDR_LEN];
unsigned char dmi_addrlen;
int dmi_users; int dmi_gusers;
};
用一段伪代码来描述,代码中以ff_开头的都是存放硬件操作的,具体代码:
void set_multicast_list(struct net_device *dev)
{
struct dev_mc_list *mcptr;
if (dev->flags & IFF_PROMISC) {ff_get_all_packets();
return;
}
/*如果包很多则用另外软件空间来排序和处理*/
if (dev->flags & IFF_ALLMULTI || dev->mc_count > FF_TABLE_SIZE){ff_get_all_multicast_packets();
return;
}
if (dev->mc_count == 0){ff_get_only_own_packets();
return;
/*所有多播地址存放在硬件过滤器中*/
}
ff_clear_mc_list();
for (mc_ptr = dev->mc_list;
mc_ptr;
mc_ptr = mc_ptr->next)
ff_store_mc_address(mc_ptr->dmi_addr);
ff_get_packets_in_multicast_list();
}
如果接口无法处理多播包,则上述代码可简化,FF_TABLE_SIZE就是0,只需要代码的最后4个分句就可以了,但set_multicast_list方法还是要的,需要它来通知dev->flags标志位的改变。简化后的多播包处理代码叫nonefeatured(nf),如下:
void nf_set_multicast_list(struct net_device *dev)
{
if (dev->flags & IFF_PROMISC) nf_get_all_packets();
else nf_get_only_own_packets();
}
对于点对点的连接来说,无需设置set_multicast_list因为接口会接受到所有来的包,代码中的IFF_PROMISC变量很重要,使用tcpdump等网络包分析工具时必须开启该变量。
其他提醒说明
本文参考Linux内核版本是2.3.43,各Linux版本之间的网络子系统变量和函数的修订区别,这里省略。内核中的网络驱动部分可以看到一些可选其他方式的处理代码:
#if_def HAVE_DEVLIST
struct netdev_entry netcard_drv = {cardname, netcard_probel, NETCARD_IO_EXTENT, netcard_portlist};
#else 正常的探测例行程序代码。
了解更多其它类型的Linux设备驱动,可以参考.
|