网络驱动<一>
基础知识
ARP地址解析协议
Linux网络子系统
系统调用接口----向用户空间的应用程序提供一种访问内核网络子系统的方法.如socket
协议无关接口----这提供一种通用的方法来使用传输的协议,实现一种通用的函数访问不同的协议
网络协议--------TCP UDP等
设备无关接口----这提供了协议与设备驱动通信的通用接口,提供一组通用的函数底层驱动使用
设备驱动程序----负责管理物理网络设备
Linux网络驱动体系分四层结构
一,概况
网络协议接口层
网络设备接口层
设备驱动功能层
网络设备与媒介层
二,解释
网络协议接口层
该层向网络层协议提供统一的数据包收发接口,ARP或IP通过dev_queue_xmit()函数发送
netif_rx()函数接收,这一层使得上层协议独立的硬件设备
网络设备接口层
该层提供统一的用于描述具体网络设备属性和操作结构体net_device,该数据结构是驱动功能层中
各个函数的容器,从宏观上规划好了具体操作硬件驱动程序功能层的结构
设备驱动功能层
该层是填充net_device数据结构的具体成员,驱动硬件完成相应的操作,启动
发送函数hard_start_xmit(),通过网络设备上的中断触发接收操作
网络设备与媒介层
该层是完成发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能
层的函数物理上驱动,对Linux而言,对于网络适配器和媒介都是可以虚拟的
设计网络驱动时,主要完成的工作是编写驱动功能层相关的函数以及填充net_device结构,同时将其向
内核注册
三,深入探求
@@@网络协议接口层
网络协议接口层最主要的功能是向上层协议提供透明的数据包发送和接收接口
发送函数:dev_queue_xmit(struct sk_buff *skb)
接收函数:int netif_rx(struct sk_buff *skb)
sk_buff结构体相当的重要,是各层之间传递数据的媒介,用于保存网络包,就是对应一个网络包,是开发协议栈的基础
发送数据包时:Linux内核的网络处理模块须建立一个包含有要传送数据包的sk_buff,然后将
其交给下层各层在sk_buff中添加不同地协议头直至交给网络设备发送
接收数据包时:网络设备从媒介上接收到数据包,须将接收到的数据转换成sk_buff数据结构并传递给上
层,各层剥去相应的协议头最终传递给用户
相关代码:linux/skbuff.h
里面有定义的相关各层协议头,均为共同体
协议头h:传输层TCP/UDP,ICMP/IGMP
网络协议头nh:
链路协议头mac:
重要成员
struct device *dev;//指向哪一个设备正在处理skb包
__u32 saddr;//IP源地址
__u32 daddr;//IP目标地址
__u32 raddr;//IP路由地址
sk_buff指针
head指内存中已分配的用于承载网络数据缓冲区的起始地址,sk_buff和相关的数据块分配之后,这个指针就固定了
data指向当前协议层的有效数据的起始地址.每个层的有效数据含义并不相同
传输层:有效数据是用户数据和传输层协议头
网络层:有效数据是用户数据,传输层协议头和网络层协议头
链路层:有效数据是用户数据,传输层转文头,网络层协议头和链路层协议头
由此可见:data指针的值是随着当前拥有sk_buff的协议层的变化而变化
tail是和data相对应,是有效数据的结尾地址
end与head相对应,是网络数据缓冲区的结尾地址,sk_buff被分配后,该值就固定了
长度信息:len指有效数据包的长度,包括协议头和有效负载
套接定缓冲区的操作
分配:
struct sk_buff * alloc_skb(unsigned int len,int priority);
len长度,priority指优先级------------供协议栈代码用
struct sk_buff * dev_alloc_skb(unsigned int len);
len长度,优先级固定为GFP_ATOMIC,表示分配过程不能被中断,实质上是调用上面的函数---供驱动代码用
分配成功后,data,tail都指向head,len为0
释放:
void kfree_skb(struct sk_buff *skb);//Linux 内核内部使用
void dev_kfree_skb(struct sk_buff *skb);//用于套接字缓冲区释放,用于非中断上下文
void dev_kfree_skb_irp(struct sk_buff *skb);//用于中断上下文
void dev_kfree_skb_any(struct sk_buff *skb);//中断或非中断皆可
指针移动:
put操作:在缓冲区尾部添加数据
unsigned char *skb_put(struct sk_buff *skb,unsigned int len);
unsigned char *__skb_put(struct sk_buff *skb,unsigned int len);
作用是将tail指针下移,增加len的值,同时返回skb->tail的当前值,而二者的区别是skb_put会检查放入缓冲区的数据
push操作:在数据包发送时添加文件头部
unsigned char *skb_push(struct sk_buff *skb,unsigned int len);
unsigned char *__skb_push(struct sk_buff *skb,unsigned int len);
作用是将data指针上移,也会增加len的值,该操作会在存储空间的头部增加一段存储空间,区别类似于put
实际作用可以先填协议头再添加数据
pull操作:下层协议向上层协议移交数据,使data指针指向上一层的协议头
unsigned char *skb_pull(struct sk_buff *skb,unsigned int len);
作用是将data下移,并减小len的值
reserve操作:在存储空间的头部预留一部分的空间
void skb_reserve(struct sk_buff *skb,unsigned int len);
作用是将data和tail指针同时下移
同时需要注意,sk_buff的操作绝大部分工作是由内核完成的,做驱动只需要完成涉及数据链路层的部分工作.
可以通过搜索网卡接收udp包来看一下
这里有函数需要理解
readwords();
@@@网络设备接口层
主要功能就是为千变万化的网络设备提供统一的抽象的数据结构net_device
当然这个一个极其庞大的数据结构,编写网络设备驱动只需要了解其中的一部分:
填充net_device上的具体成员并注册它,就可以实现硬件操作函数和内核的挂接
相关数据定义及结构
全局信息:
char name[IFNAMESIZ] name是设备的名称,通常写成eth%d,表示要求内核自动分配名字
int (*init)(struct net_device *dev);
init 为初始化函数指针,如果这个指针被设置了,那么网络设备注册的时候会调用该函数完成对net_device结构
的初始化,当然可以把该函数设置为NULL不用实现
硬件信息:
unsigned long mem_end;
unsigned long mem_start;
分别对应设备所使用的共享内存对应的起始和结束地址
unsigned long base_addr;//网络设备I/O基地址
unsigned char irp;//设备使用的中断号
unsigned char if_port;//针对多端口设备,指定使用哪一个端口
unsigned char dma;//指定分配给设备的dma通道
接口信息:
unsigned short hard_header_len;//网络设备硬件头的长度,以太网设备中赋值为ETH_HLEN即14.
unsigned short type;//接口的硬件类型
unsigned mtu;//最大传输单元mtu
unsigned char dev_addr[MAX_ADDR_LEN];//硬件地址,从硬件上读出填充到此,无符号6字节
unsigned char broadcast[MAX_ADDR_LEN];//广播地址,6个0xFF
unsigned short flags;//网络接口标志,以IFF_开头interface flags
IFF_UP当设备被激活并可以开始发送数据包
IFF_AUTOMEDIA设备可在多种媒介转换
IFF_BROADCAST允许广播
IFF_DEBUG调试模式,可用于控制printk调用的详细程度
IFF_LOOKBACK回环
IFF_MULTICAST允许组播
IFF_NOARP接口不能执行ARP
IFF_POINTOPOINT接口连接到点到点的链路
设备操作函数:
int (*open)(struct net_device *dev);
int (*stop)(struct net_deive *dev);
open是打开网络接口设备,获得设备所需的I/O地址,IRQ,DMA通道等,stop与此相反
注:关于中断,一般字符设备的中断是放在模块初始化的时候,而网卡驱动是放在open当中,原因是网卡在不用的时候
可能禁用,所在此时没有必要占用中断了,就可释放中断出来
实例
int net_open(struct net_device *dev)
{
request_irq(dev->irq,&net_interrupt,SA_SHIRQ,"dm9000",dev);//申请中断
..............//设置寄存器,启动设备
netif_start_queue(dev);//启动发送队列
}
发送数据时,内核会首先调用hard_start_transmit,而这个函数最终会调用hard_start_xmit()函数
int (*hard_start_xmit)(struct sk_buff *skb,struct net_device *dev);
启动数据包发送,当系统调用该函数时,需要传送一个sk_buff的结构体指针,以使得驱动程序能获取从上层传递下
来的数据包,协议通过网上发送时,调用该函数
void (*tx_timeout);
当数据包发送超时会调用该函数,该函数需采用重启数据包发送过程或重启硬件等策略来发恢复网络设备到正常状态
int (*hard_header) (struct sk_buff *skb,
struct net_device *dev,
unsigned short type,
void *daddr,
void *saddr,
unsigned len);
该函数完成硬件帧头的填充,返回填充的字节数.上述参数为,sk_buff指针,设备指针
协议类型,目的地址,源地址,数据长度.
不过对以太网设备而言,将内核提供的eth_header函数赋值给hard_header指针即可.
struct net_device_status* (*get_status)(struct net_device *dev);
get_status()函数用于获取网络设备的状态,并且返回一个net_device_status的结构体,该结构体保存了网络设备的
详细流量统计信息,如发送和接收到的数据包以及字节数.
int (*do_ioctl)(struct net_device *dev,struct ifreq *ifr,int cmd);//用于设备特定的I/O控制
int (*set_config)(struct net_device *dev,struct ifmap *map);//配置接口,用于改变设备I/O和中断号.
int (*set_mac_address)(struct net_device *dev,void *addr);//设置设备的mac地址,需要硬件支持
int (*poll)(struct net_device *dev,int quota);//轮询操作
说明一下,对于NAPI(NEW API)网络中断缓和,兼容的设备驱动,将采用轮询的方式操作接口,接收数据包.NAPI是Linux
专用的高效处理网络数据技术,这不采用中断读取数据包,而是先借助于中断唤醒数据包接收服务程序,后用轮询获取包.
辅助成员:
unsigned long trans_start;//记录最后数据包发送时的时间戳
unsignde long last_rx;//记录最后一次接收数据包时的时间戳,同上jiffies,驱动维护这两个成员
void *priv;//设备私有信息指针,与flip->private_data的地位相当
spinlock_t xmit_lock;//避免hard_start_xmit()函数被多次调用的自旋锁
int xmit_lock_owner;//指当前xmit_lock自旋锁的cpu编号
@@@设备驱动功能层
net_device结构体成员(属性和函数指针)需要在该层实现,具体的数值和函数的赋予
对于一个具体的设备xxx
要实现如下函数
xxx_open()
xxx_stop()
xxx_tx()
xxx_hard_header()
xxx_get_status()
xxx_tx_timeout()
xxx_poll()
中断处理函数,负责读取硬件上接收到的数据包并传送给上层协议
xxx_interrupt()//完成中断类型判断等基本工作
xxx_rx()//完成数据包的生成和递交给上层的复杂工作
同时对于特定的设备,可进行私有数据定义和操作
xxx_private结构体
包含有特定设备的属性和操作,自旋锁,信号量,定时器及统计信息,可自己定义
@@@网络设备与媒介层
该层直接面对的硬件
需要定义一组和一组访问内部寄存器的函数来描述硬件和对寄存器的操作
具体的宏和函数和硬件密切相关
例如:
/*寄存器定义*/
#define DATA_REG 0x0004
#define CMD_REG 0x0008
/*寄存器读写函数*/
static u16 xxx_readword(u32 base_addr,int portno)
{
...//读取寄存器值并返回
}
static u16 xxx_writeword(u32 base_addr,int portno,u16 value)
{
...//向寄存器写入数据
}
四,框架分块解析
@@@网络驱动程序的注册与注销
int register_netdev(struct net_device *dev);//注册函数
void unregister_netdev(struct net_device *dev);//注销函数
net_device是两个函数的参数,在此可以看出net_device的重要性
同时,net_device的成员生成和赋值可由以下两个函数生成,不必亲自手动完成
struct net_device *alloc_netdev(int sizeof_priv,sonst char *name,void(*setup)(struct net_device*));
该函数生成一个net_device的结构体,并对其成员赋值同时返回该结构体的指针.
sizeof_priv为私有设备成员的大小
name为设备名
第三个参数为net_device的setup函数的指针,用于预置net_device成员值
alloc_etherdev()是alloc_netdev()的特例,原型如下
struct net_device alloc_ethernet(int sizeof_priv)
{
return alloc_netdev(sizeof_priv,"eth%d",ether_setup);
}
该函数中的ether_setup是由Linux内核提供的可以为以太网设备net_device结构体中的公有成员快速赋值的函数
释放net_device结构体的函数为
void free_netdev(struct net_device *dev);
由此可见,net_device结构体的分配和网络设备驱动的注册是在模块加载的函数中进行
而释放是在模块卸载函数中进行
如下是其模板
int xxx_init_module(void)
{
...
/*分配net_device结构体并赋值*/
xxx_dev=alloc_netdev(sizeof(struct xxx_priv),"sn%d",xxx_init);
if(xxx_dev == NULL)
...//分配失败
/*注册net_device结构体*/
if((result = register_netdev(xxx_dev)))
...
}
void xxx_cleanup(void)
{
...
/*注销net_device结构体*/
unregister_netdev(xxx_dev);
/*释放net_devicex结构本*/
free_netdev(xxx_dev);
}
@@@网络设备的初始化
首先会检查硬件是否存在,接下来分配和初始化net_device结构体,再者获取私有数据指针并初始化,包括并发同步机制
设备初始化一般要和net_device结构成员赋值协同进行,即检查设备后,根据结果进行填充和分配
模板如下:
void xxx_init(struct net_device *dev)
{
/*初始化私有数据结构*/
struct xxx_priv *priv;
/*初始化硬件*/
xxx_hw_init();
/*初始化以太网设备公有成员*/
ether_setup(dev);
/*设置设备的成员函数指针*/
dev->open = xxx_open;
dev->stop = xxx_release;
dev->set_config = xxx_config;
dev->hard_start_xmit = xxx_tx;
dev->do_ioctl = xxx_ioctl;
dev->get_status = xxx_status;
dev->change_mtu = xxx_change_mtu;
dev->rebuild_header = xxx_rebuild_header;
dev->hard_header = xxx_header;
dev->tx_timeout = xxx_tx_timeout;
dev->watchdog_timeo = timeout;
/*如果使用NAPI,设置poll函数*/
if(use_napi)
{
dev->poll = xxx_poll;
}
priv = netdev(dev);
.../*初始化私有数据*/
}
@@@关于中断
几种类型:
1,新到报文中断
2,报文发送完成中断
3,出错中断
可以通过查看中断状态寄存器来分辨中断类型
@@@网络设备的打开与释放
打开函数要完成的工作:
1,使能设备的硬件资源,申请中断,申请I/O区域,申请DMA通道
2,调用Linux内核提供的netif_start_queue()激活设备发送队列
void netif_start_queue(struct net_deivce *dev);
释放函数要完成的工作:
1,调用内核提供的netif_stop_queue()停止设备传输包
2,释放设备所使用的I/O区域,中断和DMA通道
void netif_stop_queue(struct net_device *dev);
函数模板
int xxx_open(struct net_device *dev)
{
ret = request_irq(dev->irq,&xxx_interrupt,0,dev->name,dev);
...
netif_start_queue(dev);
}
int xxx_release(struct net_device *dev)
{
free_irq(dev->irq,dev);
...
netif_stop_queue(dev);
...
}
@@@数据在发送流程
当核心要发送数据包时,会调用函数hard_start_transmit(),而该函数在设备初始化的时候由net_device结构中
hard_start_xmit()指向到xx_tx()
流程如下
1,上层协议传递下来的sk_buff,从sk_buff当中获得数据包的有效数据和长度,将有效数据放入临时缓冲区
2,对于以太网,如果有效数据长度小于以太网冲突检测所要求的数据帧最小长度ETH_ZLEN,则给临时缓冲区末尾补0
3,设置硬件寄存器,驱使网络设备发送操作
帮助理解:
网卡中的几个重要的寄存器
LINELCTL:线控制寄存器,主要配置物理接口的连接类型,如10M/100M,全双工,关双工等
RXCTL:接收特定数据报,能过配置可以设置成广播或特定地址
TXCFG:配置接收到数据后是否引发中断
BUSCT:总中断是否使能设置,控制芯片的I/O接口
ISQ:中断状态查询寄存器,可查询是否产生中断及类型
PORT0:数据收发的窗口,要接收/发送的数据都是能过该寄存器转送到硬件
TXCMD:发送控制寄存器,当设置相应的数据后,网卡会在全部数据写入后开始发送
TXLENG:储存发送数据的长度,发送数据时首先写入数据的长度,然后将数据通过PORT0写入到芯片
工作流程:
首先,对网卡芯片进行初始化,主要是填充寄存器,如LINECTL,RXCTL,RXCFG,BUSCT.
当发送时,向TXCMD写入相应的数据,并将发送的长度写入到TXLENG当中,然后依次将要发送的数据写入到PORT0,网卡
芯片将组织成链路层类型并添加填充位和CRC校验送到网络上.
模板:
int xxx_rx(struct sk_buff *skb,struct net_device *dev)
{
int len;
char *data,shortpkt[ETH_ZLEN];
data = skb->data;
len = skb-len;
if(len {
memset(shortpkt,0,ETH_ZLEN);
memcpy(shortpkt,skb->data,skb->len);
len = ETH_ZLEN;
data = shortpkt;
}
dev->trans_start = jiffies;
xxx_hw_rx(data,len,dev);
}
传输超时时,意味着发送失败,超时处理函数会调用xxx_tx_timeout()
int xxx_tx_timeout(struct net_device *dev)
{
...
netif_wake_queue(dev);/*重新启动设备发送队列*/
}
几个函数:
memest原型 (please type "man memset" in your shell)
void *memset(void *s, int c, size_t n);
memset:作用是在一段内存块中填充某个给定的值,它对较大的结构体或数组进行清零操作的一种最快方法。
用法:
一定要记住 如果要把一个char a[20]清零, 一定是 memset(a, 0, 20)
char a[20];
memset(a,0,20);//作用,将a清0
memcpy
原型:extern void *memcpy(void *dest, void *src, unsigned int count);
用法:#include
功能:由src所指内存区域复制count个字节到dest所指内存区域。
说明:src和dest所指内存区域不能重叠,函数返回指向dest的指针
@@@数据接收流程
浓缩后的接收流程
1,分配skb
dev_alloc_skb();
2,从设备读取数据到skb
3,把包丢给协议栈,也就是协议无关接口处理,统一使用如下函数
netif_rx();
网络设备接收的主要方法是由中断引发设备的中断处理函数,依据中断类型判断是否不接收中断
static void xxx_interrupt(int irq,void *dev_id,struct pt_reg *regs)
{
...
switch(status &ISQ_EVENT_MASK)
{
case ISQ_RECEIVER_EVENT:
xxx_rx(dev);
break;
}
}
static void xxx_rx(struct net_device *dev)
{
...
length = get_rev_len(...);
/*分配skb*/
skb = dev_alloc_skb(length+2);
skb_reserve(skb,2);
skb->dev = dev;
/*读取数据到skb*/
insw(ioaddr + RX_FRAME_PORT,skb_put(skb,length),length >> 1);
if(length &1)
skb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT);
/*获取上层协议*/
skb->protocol = eth_type_trans(skb,dev);
/*把数据交给上层*/
netif_rx(skb);
/*记录时间戳*/
dev->last_rx = jiffies;
...
}
inw(ioaddr)从端口ioaddr中读取一个16位字
insw(ioaddr, buf, count)从端口ioaddr中连续读取count个16位字顺序写入buf
另外,如果是NAPI兼容的驱动,需要通过poll方式接收数据包,那么就要填充xxx_poll()函数,另作深入研究.
@@@网络连接状态
以太网卡适配器硬件电路可以检测链中上是否有载波,载波反映了网络的连接状态,设备驱动通过以下程序改变连接状态
void netif_carrier_on(struct net_device *dev);//通知内核链路正常
void netif_carrier_off(struct net_device *dev);//链路失去连接
void netif_carrier_ok(struct net_device *dev);//向调用调用者返回链路上载波信号是否存在
同时网络设备驱动中往往要设置一个定时器来周期性的读取物理设备相关寄存器的数据确定是否有载波,然后更新连接状态
模板:
static void xxx_timer(unsigned long data)
{
struct net_device *dev = (struct net_device*)data;
u16 link;
...
if(!(dev->flags &IFF_UP)
{
set_timer;
}
/*xxx_chk_link就是读取硬件上的信息获取链路的连接状态,硬件固有特性*/
if(link = xxx_chk_link(dev))
{
if(!(dev->flags & IFF_RUNNING)
{
netif_carrier_on(dev);
dev->flags |= IFF_RUNNING;
printk(KERN_DEBUG "%s: link up\n",dev->name);
}
}
else
{
if(dev->flags &= ~IFF_RUNNING)
{
netif_carrier_off(dev);
dev->flags &= ~IFF_RUNNING;
printk(KERN_DEBUG "%s: link down\n",dev->name);
}
}
set_timer:
priv->timer.expires = jiffies + 1 * HZ;
priv->timer.data = (unsigned long)dev;
priv->timer.function = &xxx_timer;
add_timer(&priv->timer);
}
而在驱动程序中,xxx_timer()最初是由open函数调用的
static int xxx_open(struct net_device *dev)
{
struct xxx_priv *priv = (struct xxx_priv*)dev->priv;
...
priv->timer.expires = jiffies + 3;
priv->timer.data = (unsigned long)dev;
priv->timer.function = &xxx_timer;
add_timer(&priv->timer);
...
}
@@@参数设置和统计信息
设置mac地址,很多设备不允许这么做
static int set_mac_address(struct net_device *dev,void *addr)
{
if(netif_running(dev))
return -EBUSY;
xxx_set_mac(dev,addr);
return 0;
}
其中netif_running(dev)由以下宏定义
#define netif_running(dev) (dev->flags & IFF_UP)
另外一个是set_config会调用xxx_config,实际上使用ifconfig时会使用到
int xxx_config(struct net_device *dev,struct imap *map)
{
if(netif_running(dev))
return -EBUSY;
if(map->base_addr != dev->base_addr)
{
printk(KERN_WARNING "xxx: can't change I/O address\n");
return -EOPNOTSUPP;
}
if (map->irq != dev->irp)
{
dev->irq = map->irq;
}
return 0;
}
上面试图修改I/O和irq,要求硬件支持才行.
驱动程序还提供get_stats函数向用户反馈状态和统计信息,该函数返回一个net_device_stats的结构体
struct net_device_stats *xxx_stats(struct net_device *dev)
{
struct xxx_priv * priv = netdev_priv(dev);
return &priv->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;//发生发送的数据包数
...
}
该结构适合在包含在私有数据结构中,在发送的接收相关的具体函数当中应用,如中断函数,数据包发送函数,数据包发送
超时函数,数据包接收函数
至此,网络驱动的主要的部分架构已经基本上分析完成了,后面会继续分析一个实例DM9000网卡