一,snull
为了加深对网络驱动的理解,下面以一个基于内存的模块化接口实例来说明如何编写网路驱动程序,称之snull.为了简化讨论,做如下假设:
1,snull接口使用以太网硬件协议.
2,snull接口只传输ip数据包.
3,snull不依赖于任何硬件.
snull的设计:
模拟了与远程主机会话的过程.snull模块创建了两个接口,通过其中一个接口传输的任何数据,都将出现在另外一个接口上.示意图如下:
如图所示,snullnet0是连接到sn0接口的网络,snullnet1是连接到sn1接口的网络.local0是sn0的ip地址,local1是sn1的ip地址.romote0属于snullnet0.romote1属于snullnet1.
为了达到我们的设计目标----从一个接口发送的数据将出现在另一个接口上.在赋予ip地址时需注意以下原则:
local0的主机部分和remote1的主机部分一样,而local1的主机部分和remote0的主机部分一样.
基于以上假设,加入我们ip地址的分配如下:
snullnet0: 192.168.0.0
snullnet1: 192.168.1.0
local0: 192.168.0.1
remote0: 192.168.0.2
local1: 192.168.1.2
remote1: 192.168.1.1
这样,为实现咱们的目的,snull的发送函数只需要把源地址和目的地址的网络地址部分的0==>1,1==>0就可以了.
eg:模拟一个数据包发往remote0,则
源地址:192.168.0.1 目的地址:192.168.0.2 经过snull0后变为:
源地址:192.168.1.1 目的地址:192.168.1.2
这样数据包就会发送到snull1接口,而源地址为192.168.1.1,则模拟了从remote1发送数据到local1的过程.
ok,下面来看看如何编写网络驱动程序,来实现我们的snull.以增进我们对网络驱动程序结构的理解.
二,设备的分配,初始化与注册
显然,想让一个设备可用,必须要将其注册到内核中去.而在把我们的设备注册到内核中之前,有两件事要完成:给设备分配内存空间并初始化.
这里,每个接口用一个net_device结构来描述.其定义在<linux/netdevice.h>中.因为在我们的例子中,有两个接口,snull0和snull1.所以snull在一个数组里保存了两个指向该结构的指针.
struct net_device *snull_devs[2];
分配内存空间:
函数原型:
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *) )
snull的相应代码如下:
snull_devs[0] = alloc_netdev(sizeof(struct snull_priv), "sn%d" , snull_init);
snull_devs[1] = alloc_netdev(sizeof(struct snull_priv), "sn%d" , snull_init);
if(snull_devs[0] == NULL || snull_devs[1] == NULL)
goto out;
|
这里将下一个可用的接口号代替%d.
网络子系统针对alloc_netdev函数,为不同种类的接口封装了许多函数.最常用的是alloc_etherdev,它在<linux/etherdevice.h>中定义.比如说"
以太网设备:alloc_etherdev; 光纤通道设备:alloc_fcdev; FDDI设备:alloc_fddidev; 令牌环设备: alloc_trdev.
现在已经为设备分配了内存空间,下面则应该将其进行初始化.
设备的初始化:
设备初始化最主要的作用是完成了设备操作的对应关系.即把内核所能看见的操作(dev->open)与我们自己所定义的函数(snull_open)进行对应.snull的初始化代码snull_init的核心代码如下:
ether_setup(dev); /*因为我们将snull设计为以太网设备,所以可以用此函数初始化我们的设备.此函数会以'以太网'方式来初始化一些成员.*/
dev->open = snull_open;
dev->stop = snull_release;
dev->set_config = snull_config;
dev->hard_start_xmit = snull_tx;
dev->do_ioctl = snull_ioctl;
dev->get_stats = snull_stats;
dev->rebuild_header = snull_rebuild_header;
dev->hard_header = snull_header;
dev->tx_timeout = snull_tx_timeout;
dev->watchdog_tiomeo = timeout;
|
比如,当网络子系统调用ifconfig打开一个网络接口时,内核会调用dev->open,而由我们的初始化函数可以看到,我们将dev->open映射为snull_open.所以将执行snull_open操作.同理,当网络子系统需要通过一个网路接口发送数据时,会调用dev->hard_start_xmit函数,而该函数则会对应到我们自定义的snull_tx函数,所以真正执行发送数据的函数是我们自己定义的函数,这里的作用跟字符设备驱动程序的file_operations结构体的作用差不多.
上面完成了内存分配和初始化的工作,下面要做的就是把我们的设备注册到内核中去.
网络设备的注册:
网络接口的注册是通过函数register_netdev来完成的.每一个注册过的netdevice都保存在一个由dev_base指向的链表中.下面是snull的注册部分代码:
for(i = 0; i < 2; i++)
{
if( ( result = register_netdev(snull_devs[i]) ) ) /*注册成功时返回0*/
printk("snull register error..\n");
}
|
注意:当调用register_netdev函数后,就可以调用驱动程序操作设备了.所以,必须在初始化一切事情结束后再进行注册.也就是说,应该把一切都准备好了以后再进行设备的注册.
相应的,如果我们不在使用网络接口,则应该将其注销掉并释放相关的内存.相应的注销函数为unregister_netdev.
三,设备方法
在经过上面的操作后,我们的设备已经注册到网络子系统中了,下面来看看一个网络接口的每个设备方法都应该完成什么样的工作.
int (*open)(struct net_device *dev);
打开接口.在ifconfig激活接口时,接口将被打开.open函数应该注册所有的系统资源(I/O端口,IRQ,DMA等等),打开硬件,并对设备执行其他所需的设置与操作.
int (*stop)(struct net_device *dev);
停止接口.在接口终止时应该将其停止.在该函数中执行的操作与打开时执行的操作相反.
int (*hard_start_xmit)(struct sk_buff *skb,struct net_device *dev);
数据包的发送函数.完整的数据包(协议头和数据)包含在一个套节字缓冲区中(sk_buff).
int (*hard_header) (struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned len);
该函数根据先前检索到的源和目的硬件地址建立硬件头.应该在调用hard_start_xmit之前被调用.eth_header是以太网类型接口的默认函数,ether_setup将该成员赋值成eth_header.
int (*rebuild_header)(struct sk_buff *skb);
该函数用来在传输数据包之前,完成arp解析之后,重新构建硬件头.
void (*tx_timeout)(struct net_device *dev);
如果数据包的传输在合理的时间段内失败,则网络子系统调用此函数,负责解决问题并重新开始传输数据包.
struct net_device_stats *(*get_stats)(struct net_device *dev);
当应用程序需要获得接口的统计信息时,将调用该函数.例如,在运行ifconfig或netstat -i时将利用该方法.
int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
执行接口特有的ioctl命令.如果接口不需要实现任何接口特有的命令,则net_device中的对应成员可保持为NULL.
....
四,snull网络接口的设备方法
下面通过snull的几个设备方法的具体实现代码,来增强对其的了解.
打开操作:
int snull_open(struct net_device *dev)
{
/* request_region(), request_irq(), .... (like fops->open) */
/*
* Assign the hardware address of the board: use "\0SNULx", where
* x is 0 or 1. The first byte is '\0' to avoid being a multicast
* address (the first byte of multicast addrs is odd).
*/
memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
if (dev == snull_devs[1])
dev->dev_addr[ETH_ALEN-1]++; /*
\0SNUL1 */
netif_start_queue(dev);
return 0;
}
|
首先,在接口能够和外界通讯之前,要将mac地址从硬件设备复制到dev->dev_addr。硬件地址可以在打开期间拷贝到设备中。snull设计成是与硬件无关的,所以这里赋给了一个虚拟的地址。一旦接口准备好开始传输数据后,open方法应该启动接口的传输队列。
由于与硬件无关,所以snull的open操作做的事情很少。对stop而言,也是这样,它是open操作的逆过程。
关闭操作:
int snull_release(struct net_device *dev)
{
/* release ports, irq and such -- like fops->close */
netif_stop_queue(dev); /*
can't transmit any more */
return 0;
}
|
注意,在接口被关闭时,必须调用netif_stop_queue函数,用来停止接口的队列传输。
数据包的传输:
当一个接口被打开之后,就可以用来传输数据了,相关函数如下:
int snull_tx(struct sk_buff *skb, struct net_device *dev)
{
int len;
char *data, shortpkt[ETH_ZLEN]; /*ETH_ZLEN为snull支持传输的最小数据长度*/
struct snull_priv *priv = netdev_priv(dev);
data = skb->data;
len = skb->len;
if (len < ETH_ZLEN) {
memset(shortpkt, 0, ETH_ZLEN);
memcpy(shortpkt, skb->data, skb->len);
len = ETH_ZLEN;
data = shortpkt;
}
dev->trans_start = jiffies; /*
save the timestamp */
/* Remember the skb, so we can free it at interrupt time */
priv->skb = skb;
/* actual deliver of data is device-specific, and not shown here */
snull_hw_tx(data, len, dev);
return 0; /* Our simple device can not fail */
}
|
通过代码我们可以看到,这个函数只是对数据包的长度做了检查,然后调用硬件相关的函数进行数据包的传输。
/*
* Transmit a packet (low level interface)
*/
static void snull_hw_tx(char *buf, int len, struct net_device *dev)
{
/*
* This function deals with hw details. This interface loops
* back the packet to the other snull interface (if any).
* In other words, this function implements the snull behaviour,
* while all other procedures are rather device-independent
*/
struct iphdr *ih;
struct net_device *dest;
struct snull_priv *priv;
u32 *saddr, *daddr;
struct snull_packet *tx_buffer;
/* I am paranoid. Ain't I? */
if (len < sizeof(struct ethhdr) + sizeof(struct iphdr)) {
printk("snull: Hmm... packet too short (%i octets)\n",
len);
return;
}
/*
* Ethhdr is 14 bytes, but the kernel arranges for iphdr
* to be aligned (i.e., ethhdr is unaligned)
*/
ih = (struct iphdr *)(buf+sizeof(struct ethhdr)); /*获得ip头的位置,注意此方法*/
saddr = &ih->saddr;
daddr = &ih->daddr;
((u8 *)saddr)[2] ^= 1; /*
change the third octet (class C) */
((u8 *)daddr)[2] ^= 1; /*
进行异或运算,相同为0,不同为1。*/
ih->check = 0; /*
and rebuild the checksum (ip needs it) */
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl); /*
重新计算ip校验和,正常情况下还应该重新计算tcp头校验和,icmp头检验和。。 */
if (dev == snull_devs[0])
PDEBUGG("%08x:%05i --> %08x:%05i\n",
ntohl(ih->saddr),ntohs(((struct tcphdr *)(ih+1))->source),
ntohl(ih->daddr),ntohs(((struct tcphdr *)(ih+1))->dest));
else
PDEBUGG("%08x:%05i <-- %08x:%05i\n",
ntohl(ih->daddr),ntohs(((struct tcphdr *)(ih+1))->dest),
ntohl(ih->saddr),ntohs(((struct tcphdr *)(ih+1))->source));
/*
* Ok, now the packet is ready for transmission: first simulate a
* receive interrupt on the twin device, then a
* transmission-done on the transmitting device
*/
/*除了上面修改网络地址的代码外,以下为snull功能实现的核心代码*/
/*在接收端产生一个接收中断*/
dest = snull_devs[dev == snull_devs[0] ? 1 : 0];
priv = netdev_priv(dest);
tx_buffer = snull_get_tx_buffer(dev);
tx_buffer->datalen = len;
memcpy(tx_buffer->data, buf, len);
snull_enqueue_buf(dest, tx_buffer); /*将发送的数据放到接收设备的缓冲队列*/
if (priv->rx_int_enabled) {
priv->status |= SNULL_RX_INTR;
snull_interrupt(0, dest, NULL); /*产生中断----接收*/
}
/*在发送端产生一个发送完成中断*/
priv = netdev_priv(dev);
priv->tx_packetlen = len;
priv->tx_packetdata = buf;
priv->status |= SNULL_TX_INTR;
if (lockup && ((priv->stats.tx_packets + 1) % lockup) == 0) {
/* Simulate a dropped transmit interrupt */
netif_stop_queue(dev);
PDEBUG("Simulate lockup at %ld, txp %ld\n", jiffies,
(unsigned long) priv->stats.tx_packets);
}
else
snull_interrupt(0, dev, NULL); /*产生中断----发送*/
}
|
数据包的接收:
/*
* Receive a packet: retrieve, encapsulate and pass over to upper levels
*/
void snull_rx(struct net_device *dev, struct snull_packet *pkt)
{
struct sk_buff *skb;
struct snull_priv *priv = netdev_priv(dev);
/*
* The packet has been retrieved from the transmission
* medium. Build an skb around it, so upper layers can handle it
* 已经从传输介质中获得数据包,为数据包分配sk_buff缓冲区,以便上层进行操作。
*/
skb = dev_alloc_skb(pkt->datalen + 2); /*以太网头为14个字节,对齐*/
if (!skb) {
if (printk_ratelimit()) /*控制printk被调用的速度,当向控制台发送大量信息时,printk_ratelimit返回0*/
printk(KERN_NOTICE "snull rx: low on mem - packet dropped\n");
priv->stats.rx_dropped++;
goto out;
}
skb_reserve(skb, 2); /*
align IP on 16B boundary */
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen); /*skb_put改变tail指针并更新skb->len的值*/
/* Write metadata, and then pass to the receive level */
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev); /*主要任务是返回skb的协议号*/
skb->ip_summed = CHECKSUM_UNNECESSARY; /*
don't check it */
priv->stats.rx_packets++;
priv->stats.rx_bytes += pkt->datalen;
netif_rx(skb);
out:
return;
}
|
在能够处理数据包之前,网络层必须知道数据包的一些信息。为此必须在将skb缓冲区传递到上层之前,对dev和protocol成员正确赋值。以太网设备支持eth_type_trans函数来查找填入protocol中的正确值。然后指定如何求得校验和。接收数据包的最后一个步骤由netif_rx执行,它将skb缓冲区传递给上层处理。
这里再来看一下skb_put()函数的作用:

可见,skb_put()更新了tail指针并且增加了skb->len。
中断处理程序:
网络接口在两种可能的事件下中断处理器。也就是说有两种情况会引起中断的发生:新数据包的到达和外发数据包的传输已完成。
通常情况下,中断处理程序通过检查物理设备中的状态寄存器,以区分是新数据包的到达产生的中断还是数据传输完毕产生的中断。
snull接口的工作原理也是这样,只是因为其与硬件无关,所以它的状态值是通过软件来实现的,其保存在dev->priv中。snull的中断处理程序如下:
static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int statusword;
struct snull_priv *priv;
struct snull_packet *pkt = NULL;
/*
* As usual, check the "device" pointer to be sure it is
* really interrupting.
* Then assign "struct device *dev"
*/
struct net_device *dev = (struct net_device *)dev_id; /*如果对应的中断号允许共享,则触发中断后可能有多个中断处理程序与其对应,
这时就需要查看中断是否是与自己的接口对应的*/
/* ... and check with hw if it's really ours */
/* paranoid */
if (!dev)
return;
/* Lock the device */
priv = netdev_priv(dev);
spin_lock(&priv->lock);
/* retrieve statusword: real netdevices use I/O instructions */
statusword = priv->status;
priv->status = 0;
if (statusword & SNULL_RX_INTR) { /*如果是新数据包的到达,触发接收中断*/
/* send it to snull_rx for handling */
pkt = priv->rx_queue;
if (pkt) {
priv->rx_queue = pkt->next;
snull_rx(dev, pkt); /*调用接收函数*/
}
}
if (statusword & SNULL_TX_INTR) { /*如果是发送数据包完成,触发发送中断*/
/* a transmission is over: free the skb */
priv->stats.tx_packets++;
priv->stats.tx_bytes += priv->tx_packetlen;
dev_kfree_skb(priv->skb);
}
/* Unlock the device and we are done */
spin_unlock(&priv->lock);
if (pkt) snull_release_buffer(pkt); /*
Do this outside the lock! */
return;
}
|
从代码中可以看到,新数据包的到达,和发送数据包的完成,都有相应的对应代码。由于snull与硬件无关,所以中断处理程序也简单的多。
ok,弄了两天,终于是把这篇笔记写完了。
ps:其实无论是字符设备驱动程序,块设备驱动程序或是网络驱动程序,其基本框架和编写方法都不难理解,真正难理解的是硬件的工作原理,如何去操作这个硬件才是难点所在。自己暂时只是在理论上对这部分有些了解,希望以后能有机会真正的跟硬件打打交道。