【Linux高级驱动】网卡驱动分析
两个重要的结构体简单介绍
*sk_buff
如果把网络传输看成是运送货物的话,那么sk_buff就是这个“货物”了,所有经手这个货物的人都要干点什么事儿,要么加个包装,要么印个戳儿等等。收货的时候就要拆掉这些包装,得到我们需要的货物(payload data)。没有货物你还运输什么呢?由此可见sk_buff的重要性了。
*net_device
又是一个庞大的结构体。它在内核中就是指代了一个网络设备。驱动程序需要在探测的时候分配并初始化这个结构体,然后使用register_netdev来注册它,这样就可以把操作硬件的函数与内核挂接在一起。
两个重要结构体的说明
========================================================================================
1.net_device
========================================================================================
{
================
(1)全局信息
================
/* 网络设备的名称 */
char name[IFNAMSIZ];
/* init为设备初始化函数指针,如果这个指针被设置了,则网络设备被注册时将调用
* 该函数完成对net_device结构体的初始化。注意:设备驱动程序可以不实现这个函数
* 并将其赋值为NULL
*/
int (*init)(struct net_device *dev);
================
(2)硬件信息
================
/* 下面两个成员分别定义了设备所使用的共享内存的起始和结束地址 */
unsigned long mem_end; /* shared mem end */
unsigned long mem_start; /* shared mem start */
/* 网络设备的I/O基地址 */
unsigned long base_addr;
/* 网络设备使用的中断号 */
unsigned int irq;
/* 指定多端口设备使用哪一个端口,该字段仅针对多端口设备,如:如果设备同时支持
* IF_PORT_10BASE2(同轴电缆)和IF_PORT_10BASE(双绞线),则可以使用该字段
*/
unsigned char if_port;
/* 网络设备还可以通过DMA来传输数据,此字段指定分配给设备的DMA通道 */
unsigned char dma;
================
(3)接口信息
================
/* 网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋值为ETH_HLEN,即14 */
unsigned short hard_header_len; /* hardware hdr length */
/* 指定了网络设备最大传输单元(MTU) */
unsigned mtu;
/* dev_addr[],broadcase[]分别用于存放设备的硬件地址和广播地址。对于以太网而言,这两个地址的长度
* 都是6个字节。以太网设备的广播地址为6个0xFF,而MAC地址需要驱动程序从硬件上读出并填充到dev_addr[]中。
*/
unsigned char dev_addr[MAX_ADDR_LEN];
unsigned char broadcast[MAX_ADDR_LEN]; /* hw bcast add */
================
(4)设备操作函数
================
/* open()函数的作用是打开网络接口设备,获得设备需要的I/O地址、IRQ、DMA通道等。
* stop()函数的作用是停止网络接口设备,与open()函数的作用相反
*/
int (*open)(struct net_device *dev);
int (*stop)(struct net_device *dev);
/* hard_start_xmit()函数用于启动数据包的发送,当系统调用驱动程序的hard_start_xmit()函数时,
* 需要向其传入一个sk_buff结构体指针,以使得驱动程序能获得上层传递下来的数据包
*/
int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);
/* 当数据包发送超时时,tx_timeout()函数会被调用,该函数需要采取重新启动数据包发送过程或者
* 重新启动硬件等措施来恢复网络设备到正常状态。
*/
void (*tx_timeout) (struct net_device *dev);
/* hard_header()函数用来完成硬件帧头的填充,返回填充的字节数。传入该函数的参数包括sk_buff指针
* 设备指针、协议类型、目的地址、源地址及数据长度。注意对于以太网来说,将内核提供的eth_header()
* 函数赋值给hard_header指针即可。
*/
int (*hard_header) (struct sk_buff *skb,struct net_device *dev,unsigned short type,
void *daddr,void *saddr,unsigned len);
/* 用于获取网络设备的状态信息,它返回一个net_device_stats结构体。net_device_stats结构体保存了
* 网络设备详细的统计信息,如发送和接收到得数据包数、字节数等。
*/
struct net_device_stats* (*get_stats)(struct net_device *dev);
/* 用户进行设备特定的I/O操作 */
int (*do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);
/* 用于配置接口,可用于改变设备的I/O地址和中断号 */
int (*set_config)(struct net_device *dev, struct ifmap *map);
/* 用户设置设备的MAC地址 */
int (*set_mac_address)(struct net_device *dev,void *addr);
================
(5)辅助成员
================
/* 此成员记录了最后的数据包开始发送时的时间戳 */
unsigned long trans_start; /* Time (in jiffies) of last Tx */
/* 此成员记录了最后一次接收到数据包时的时间戳,注意:这两个时间戳记录的都是jiffies */
unsigned long last_rx; /* Time of last Rx */
}
========================================================================================
2.网络设备的初始
========================================================================================
(1)进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备所使用的硬件资源
(2)进行软件接口上的准备工作,分配net_device结构体并对其数据和函数指针成员赋值。
(3)获得设备的私有信息指针并初始化其各成员的值,如果私有信息中包括自选锁或信号量等并
发或同步机制,则需对其进行初始化。
一个网络设备驱动初始化函数的模版如下所示:
{
/* 设备的私有信息结构体 */
struct xxx_priv *priv
/* 检测设备是否存在和设备所使用的硬件资源 */
xxx_hw_init();
/* 初始化以太网的共用成员 */
ether_setup (dev);
/* 设置设备的成员函数指针 */
dev->open = cs8900_start;
dev->stop = cs8900_stop;
dev->hard_start_xmit = cs8900_send_start;
dev->get_stats = cs8900_get_stats;
dev->set_multicast_list = cs8900_set_receive_mode;
dev->tx_timeout = cs8900_transmit_timeout;
dev->watchdog_timeo = HZ;
/* 取得私有信息,并初始化它 */
priv =netdev_priv(dev);
... /* 初始化设备私有数据区 */
}
xxx_hw_init()函数完成的基本操作如下所示:
(1)探测xxx网络设备是否存在。探测的方法类似与数学上“反证法”,即先假设存在设备xxx,
访问设备,如果设备的表现与预期的一致,就确定设备存在;否则,假设错误,设备xxx
不存在
(2)探测设备的具体硬件配置。一些设备驱动编写得非常通用,对于同类设备使用统一的驱动,
我们需要在初始化时探测设别的具体型号。另外,即便是同一设备,在硬件上的配置也可
能不一样,我们也可以探测设备所使用的硬件资源
(3)申请设备所需要的硬件资源,如用request_region()函数进行I/O端口的申请等,但是这个
过程可以放在设备的打开函数xxx_open()中完成。
========================================================================================
3. 数据发送流程
========================================================================================
(1)网络设备驱动程序从上层协议传递过来的sk_buff参数获得数据包的有效数据和长度,将有效数据
放入一个临时缓冲区
(2)对于以太网,如果有效数据的长度小于以太网冲突检测所要求数据帧的最小长度ETH_ZLEN,则给
临时缓冲区的末尾填充0
(3)设备硬件的寄存器,驱动网络设备进行数据发送操作
{
int len;
char *data, shortpkt[ETH_ZLEN];
if(xxx_send_available(...)) /* 发送队列未满,可以发送 */
{
/* 获得有效数据指针和长度 */
data = skb->data;
len = skb->len;
if (len < ETH_ZLEN)
{
/* 如果帧长小于以太网帧最小长度,补0 */
memset(shortpkt, 0, ETH_ZLEN);
memcpy(shortpkt, skb->data, skb->len);
len = ETH_ZLEN;
data = shortpkt;
}
dev->trans_start = jiffies; /* 记录发送时间戳 */
/* 设置硬件寄存器让硬件把数据包发送出去 */
xxx_hw_tx(data, len, dev);
} ...
else
{
netif_stop_queue(dev);
...
}
}
注意:1)当发送队列满或因其他原因来不及发送当前上层传下来的包,则调用此函数阻止上层继续向网络设备
驱动传递数据包,当忙于发送的数据包发送完成后,TX结束的中断处理中,应该调用netif_wake_queue
来唤醒被阻塞的上层以启动它继续向网络设备驱动传递数据包。
2)当数据传输超时时,意味着当前的发送操作失败或硬件已陷入未知状态,此时,数据包发送超时
处理函数xxx_tx_timeout()将被调用,在此函数中也应该调用netif_wake_queue()函数重新启动
设备发送队列。
========================================================================================
4. 数据接收流程
========================================================================================
网络设备接收数据的主要方法是由中断引发设备的中断处理函数,中断处理函数判断中断类型,如果为接收中断,则读取接收到的数据,分配sk_buffer数据结构和数据缓冲区,将接收到的数据复制到数据缓冲区,并调用netif_rx()函数将sk_buffer传递给上层协议
接收数据流程的典型模版如下所示:
{
...
switch (status &ISQ_EVENT_MASK)
{
case ISQ_RECEIVER_EVENT:
/* 获取数据包 */
xxx_rx(dev);
break;
/* 其他类型的中断 */
}
}
static void xxx_rx(struct xxx_device *dev)
{
...
length = get_rev_len (...);
/* 分配新的套接字缓冲区
* 一般在上层分配一个sk_buffer用alloc_skb()函数
* 而在设备驱动中最好用dev_alloc_skb
* dev_alloc_skb()函数一GFP_ATOMIC优先级进行skb的分配
* 使用GFP_ATOMIC来申请内存时,若没有空闲页,则不等待
* 直接返回
*/
skb = dev_alloc_skb(length + 2);
/* skb_reserve可以在缓冲区的头部预留一定的空间,它通常
* 被用来在缓冲区中给协议头预留空间或者在某个边界上对齐。
* 这个函数改变data和tail指针,而data和tail指针分别指向
* 负载的开头和结尾。这个函数通常在分配缓冲区之后就调用,
* 此时的data和tail指针还是指向同一个地方。
*/
skb_reserve(skb, 2); /* 对齐 */
skb->dev = dev;
/* 读取硬件上接收到的数据 */
insw(ioaddr + RX_FRAME_PORT, skb_put(skb, length), length >> 1);
if (length &1)
kb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT);
/* 获取上层协议类型 */
skb->protocol = eth_type_trans(skb, dev);
/* 把数据包交给上层 */
netif_rx(skb);
/* 记录接收时间戳 */
dev->last_rx = jiffies;
...
}
========================================================================================
5. 网络设备的连接状态分析
========================================================================================
网络适配器硬件电路可以检测出链路上是否有载波,载波反映了网络的连接是否正常。
网络设备驱动可以通过netif_carrier_on()和netif_carrier_off()函数改变设备的连接状态,
如果驱动检测到连接状态发生变化,也应该以netif_carrier_on()和netif_carrier_off()函数显式地通知内核。
void netif_carrier_off(struct net_device *dev);
int netif_carrier_ok(struct net_device *dev);
========================================================================================
6. 参数设置和统计数据
在网络设备的驱动程序中还提供一些方法供系统对设备的参数进行设置或读取设备相关的信息。
========================================================================================
当用户调用ioctl()函数,并制定SIOCSIFHWADDR命令时,意味着要设备这个设备的MAC地址。设置网络设备的
MAC地址可用如下代码清单模版:
{
struct sockaddr *addr = p;
/* 判断设备是否处于忙的状态 */
if (netif_running(dev))
return -EBUSY;
/* 如果不忙可以开始设置设备的MAC地址 */
xxx_set_mac(dev,adrr),
return 0;
}
注意:调用xxx_set_mac函数在网络适配器硬件内写入新的MAC地址。这要求设备在硬件上支持MAC地址的修改,而实际上,
许多设备并不提供修改MAC地址的接口。
如果用户调用ioctl()时,命令类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统会调用驱动程序的do_ioctl()函数
进行设备专用数据的设备,这个设置大多数情况下也并不需要。
驱动程序还应提供get_stats()函数用以向用户反馈设备状态和统计信息,该函数返回的是一个net_device_stats结构体,
如下代码清单模版所示:
{
board_info_t *db = (board_info_t *) dev->priv;
return &db->stats;
}
net_device_stats结构体定义在内核的include/linux/netdevice.h文件中,它包含了比较完整的统计信息,如代码清单所示:
struct net_device_stats
{
unsigned long rx_packets; /* 收到的数据包数 */
unsigned long tx_packets; /* 发送的数据包数 */
unsigned long rx_bytes; /* 收到的字节数 */
unsigned long tx_bytes; /* 发送的字节数 */
unsigned long rx_errors; /* 收到的错误数据包数 */
unsigned long tx_errors; /* 发送的错误数据包数 */
...
...
}
@成鹏致远
(blogs:http://lcw.cnblogs.com)
(email:wwwlllll@126.com)
(qq:552158509)