一,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()函数的作用:
![](http://blogimg.chinaunix.net/blog/upfile2/090406142125.jpg)
可见,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:其实无论是字符设备驱动程序,块设备驱动程序或是网络驱动程序,其基本框架和编写方法都不难理解,真正难理解的是硬件的工作原理,如何去操作这个硬件才是难点所在。自己暂时只是在理论上对这部分有些了解,希望以后能有机会真正的跟硬件打打交道。