抑郁症健康,内容丰富有趣,生活中的好帮手!
抑郁症健康 > Linux网络设备驱动结构概述

Linux网络设备驱动结构概述

时间:2022-08-27 01:05:06

相关推荐

独角兽企业重金招聘Python工程师标准>>>

网络设备驱动相比字符型设备的驱动要复杂一些,除了总体上驱动的框架有一些相似外,有很多地方都是不同,但网络设备驱动有一个很大的特点就是有固定的框架可以遵循,具体的框架会在后边详细的叙述。

1.网络协议接口层

在网络协议接口层,只提供了两个抽象函数dev_queue_xmit()与 netif_rx(),之所以称之为抽象函数,是因为这两个函数抽象了很多底层的操作,不管是那个芯片它在网络协议结构的操作函数都是这两个函数,采用这样的抽象后,给上层带来了很多的方便,给上层协议提供统一的数据包收发接口,无论上层是ARP协议还是IP协议,都通过dev_queue_xmit() 函数发送数据,通过netif_rx()函数接收数据。此层使上层协议独立于具体的设备。

相关数据结构sk_buff:

sk_buff 称为“套接字缓冲区”,用于在Linux网络子系统中各层之间传递数据。是Linux网络子系统数据传递的“中枢神经”。sk_buff定义位置为:include/linux/skbuff.h,这个数据结构定义了很多用于网络操作的函数,更多的设计整个协议的实现,包括各层报文的头信息,以及报文的帧格式,下边这个这个结构体是有关报文header信息的,下边的代码摘自kernel2.6.29/include/linux /skbuff.h

struct sk_buff {

/* These two members must be first. */

struct sk_buff *next;

struct sk_buff *prev;

struct sock *sk;

struct skb_timeval tstamp;

struct net_device *dev;

struct net_device *input_dev;

union {

struct tcphdr *th;

struct udphdr *uh;

struct icmphdr *icmph;

struct igmphdr *igmph;

struct iphdr *ipiph;

struct ipv6hdr *ipv6h;

unsigned char *raw;

} h;

union {

struct iphdr *iph;

struct ipv6hdr *ipv6h;

struct arphdr *arph;

unsigned char *raw;

} nh;

union {

unsigned char *raw;

} mac;

struct dst_entry *dst;

struct sec_path *sp;

/*

* This is the control buffer. It is free to use for every

* layer. Please put your private variables there. If you

* want to keep them across layers you have to do a skb_clone()

* first. This is owned by whoever has the skb queued ATM.

*/

char cb[48];

unsigned int len,

data_len,

mac_len;

union {

__wsum csum;

__u32 csum_offset;

};

__u32 priority;

__u8 local_df:1,

cloned:1,

ip_summed:2,

nohdr:1,

nfctinfo:3;

__u8 pkt_type:3,

fclone:2,

ipvs_property:1;

__be16 protocol;

void (*destructor)(struct sk_buff *skb);

#ifdef CONFIG_NETFILTER

struct nf_conntrack *nfct;

#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)

struct sk_buff *nfct_reasm;

#endif

#ifdef CONFIG_BRIDGE_NETFILTER

struct nf_bridge_info *nf_bridge;

#endif

#endif /* CONFIG_NETFILTER */

#ifdef CONFIG_NET_SCHED

__u16 tc_index; /* traffic control index */

#ifdef CONFIG_NET_CLS_ACT

__u16 tc_verd; /* traffic control verdict */

#endif

#endif

#ifdef CONFIG_NET_DMA

dma_cookie_t dma_cookie;

#endif

#ifdef CONFIG_NETWORK_SECMARK

__u32 secmark;

#endif

__u32 mark;

/* These elements must be at the end, see alloc_skb() for details. */

unsigned int truesize;

atomic_t users;

unsigned char *head,

*data,

*tail,

*end;

};

sk_buff中的数据缓冲区的指针

Linux必须分配用于容纳数据包的缓冲区,sk_buff中定义了4个指向这片缓冲区的不同位置的指针head、data、tail、end。

head:指针指向内存中已分配的用于存储网路数据的缓冲区起始地址,sk_buff和相关数据块在分配后,该指针的值就固定了。

data:指针指向对应当前协议层有效数据的起始地址。每个协议的有效数据含义不同。

tail:指向对应当前协议层有效数据负载的结尾地址,与data对应。

end:指向内存分配的数据缓冲区的结尾地址,与head指针对应。和head一样,sk_buff和相关数据块被分配后,end指针也就固定了。

套接字缓冲区sk_buff相关操作:

分配空间:

struct sk_buff *dev_alloc_skb(unsigned len)

释放空间:

dev_kfree_skb(struct sk_buff *skb)

dev_kfree_skb_irq(struct sk_buff *skb)

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长度,并增加sk_buff中len的值,返回改变后的tail值。主要用于在尾部追加数据。

push操作:

unsigned char *skb_push(struct sk_buff *skb, unsigned int len);

unsigned char *__skb_push(struct sk_buff *skb, unsigned int len);

作用:将data指针上移,同时增加sk_buff中的len。主要用于在数据包发送时添加头部。函数带__和不带__的区别在于:带__的会检测放入缓冲区的数据,后则不会。

pull操作:

unsigned char *skb_pull(struct sk_buff *skb, unsigned int len);

作用:将data指针下移,并减少sk_buff中的len值。这个操作一般用于下层协议向上层协议移交数据包,使data指针指向上一层协议的协议头

reserve操作:

void skb_reserve(struct sk_buff *skb, unsigned int len);

作用:将data和tail指针同时下移,这个操作主要用于在存储空间的头部预留len长度的空隙。

2.网络设备接口层

我们知道,通常的驱动编写就是填充一个设备相关的结构体,网络设备也是这样,在2.6.29的内核中,需要填写结构体如下:

kernel2.6.29/include/linux/netdevice.h

struct net_device_ops {

int (*ndo_init)(struct net_device *dev);

void (*ndo_uninit)(struct net_device *dev);

int (*ndo_open)(struct net_device *dev);

int (*ndo_stop)(struct net_device *dev);

int (*ndo_start_xmit) (struct sk_buff *skb,

struct net_device *dev);

u16 (*ndo_select_queue)(struct net_device *dev,

struct sk_buff *skb);

#define HAVE_CHANGE_RX_FLAGS

void (*ndo_change_rx_flags)(struct net_device *dev,

int flags);

#define HAVE_SET_RX_MODE

void (*ndo_set_rx_mode)(struct net_device *dev);

#define HAVE_MULTICAST

void (*ndo_set_multicast_list)(struct net_device *dev);

#define HAVE_SET_MAC_ADDR

int (*ndo_set_mac_address)(struct net_device *dev,

void *addr);

#define HAVE_VALIDATE_ADDR

int (*ndo_validate_addr)(struct net_device *dev);

#define HAVE_PRIVATE_IOCTL

int (*ndo_do_ioctl)(struct net_device *dev,

struct ifreq *ifr, int cmd);

#define HAVE_SET_CONFIG

int (*ndo_set_config)(struct net_device *dev,

struct ifmap *map);

#define HAVE_CHANGE_MTU

int (*ndo_change_mtu)(struct net_device *dev,

int new_mtu);

int (*ndo_neigh_setup)(struct net_device *dev,

struct neigh_parms *);

#define HAVE_TX_TIMEOUT

void (*ndo_tx_timeout) (struct net_device *dev);

struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);

void (*ndo_vlan_rx_register)(struct net_device *dev, struct vlan_group *grp);

void (*ndo_vlan_rx_add_vid)(struct net_device *dev,

unsigned short vid);

void (*ndo_vlan_rx_kill_vid)(struct net_device *dev,

unsigned short vid);

#ifdef CONFIG_NET_POLL_CONTROLLER

#define HAVE_NETDEV_POLL

void (*ndo_poll_controller)(struct net_device *dev);

#endif

};

在我们实现这些函数中的一部分后,就实现了驱动的功能了,为千变万化的网络设备定义统一的、抽象的数据结构net_device结构体,以不变应万变,实现多种硬件在软件层次上的统一。net_device结构体在内核中指代一个网络设备,网络设备驱动只需填充其结构体就可以实现内核与具体硬件操作函数的挂接。实际驱动的编写过程中,我们并不需要实现全部的函数,实际上,我们只要根据具体的需要实现上边的部分就可以了。

net_device结构体的相关成员

网络设备的名称:

char name[IFNAMESIZ]

设备初始化指针:

int (*init)(struct net_device *dev);

硬件信息:

unsigned long mem_end;

unsigned long mem_start;

二者分别定义了设备所用的共享内存的起始和结束地址

unsigned long base_addr;网络设备的I/O基地址

unsigned char irq;设备使用的中断号

unsigned char if_port;多端口设备中端口选择

unsigned char dma;分配给设备的DMA通道

接口信息

unsigned short hard_header_len;网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋值为ETH_HLEN,即14

unsigned short type;接口的硬件类型

unsigned mtu;最大传输单元

unsigned char dev_addr[MAX_ADDR_LEN];

unsigned char broadcase[MAX_ADDR_LEN];

二者分别用于存放设备的硬件地址和广播地址,以太网设备的广播地址为6个0xFF

unsigned short flags;网络接口标志

设备操作函数

打开、关闭

int (*open)(struct net_device *dev);

int (*stop)(struct net_device *dev);

启动数据包发送

int (*hard_start_xmit)(struct sk_buff *skb, struct net_device *dev);

获得网络设备状态

struct net_device_status * (*get_status)(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);

辅助成员

unsigned long trans_start;

unsigned long last_rx;

二者分别为:最后一次开始发送的时间戳、最后一次接收到数据包的时间戳,这两个时间戳记录都是jiffies,驱动程序应维护这两个成员。

void *priv;私有信息指针

spinlock_t xmit_lock;

int xmit_lock_owner;

xmit_lock是避免hard_start_xmit函数同时多次调用的自旋锁。xmit_lock_owner指向拥有此自旋锁的CPU的编号。

本篇文章来源于 原文链接:/Net//0611/20777.html

Linux网络设备驱动指南(1)

作者:王成国 时间:-10-08 14:31 出处:51cto 责编:月夜寒箫

摘要:Linux网络设备驱动指南(1)

前言介绍,本文所涉及概念:

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等函数。

Linux网络设备驱动指南(1)

作者:王成国 时间:-10-08 14:31 出处:51cto 责编:月夜寒箫

摘要:Linux网络设备驱动指南(1)

前言介绍,本文所涉及概念:

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等函数。

Linux网络设备驱动编程

来源: ChinaUnix博客 日期: .02.03 17:36(共有1条评论) 我要评论

在此仅仅讨论网络设备驱动的一般写法,有关硬件部分的相关代码由于硬件规格不同,予以省略。有什么地方错误,或补充,欢迎大家提出。

1, 驱动模块的加载和卸载

如果网络设备(包括wireless)是PCI规范的,则先是向内核注册该PCI设备(pci_register_driver),然后由pci_driver数据结构中的probe函数指针所指向的侦测函数来初始化该PCI设备,并且同时注册和初始化该网络设备。

如果网络设备(包括wireless)是PCMCIA规范的,则先是向内核注册该PCMCIA设备(register_pccard_driver),然后driver_info_t数据结构中的attach函数指针所指向的侦测函数来初始化该PCMCIA设备,并且同时注册和初始化该网络设备。

static int __init tg3_init(void)

{

//先注册成PCI设备,并初始化,如果是其他的ESIA,PCMCIA,用其他函数

return pci_module_init(&tg3_driver);

}

static void __exit tg3_cleanup(void)

{

pci_unregister_driver(&tg3_driver);//注销PCI设备

}

module_init(tg3_init); //驱动模块的加载

module_exit(tg3_cleanup); //驱动模块的卸载

申明为PCI设备:

static struct pci_driver tg3_driver = {

.name = DRV_MODULE_NAME,

.id_table = tg3_pci_tbl, //此驱动所支持的网卡系列,vendor_id, device_id

.probe = tg3_init_one, //初始化网络设备的回调函数

.remove = __devexit_p(tg3_remove_one), //注销网络设备的回调函数

.suspend = tg3_suspend, //设备挂起函数

.resume = tg3_resume //设备恢复函数

};

2,PCI设备探测函数probe,初始化网络设备

static int __devinit tg3_init_one(struct pci_dev *pdev, const struct pci_device_id *ent)

{

//初始化设备,使I/O,memory可用,唤醒设备

pci_enable_device(pdev);

//申请内存空间,配置网卡的I/O,memory资源

pci_request_regions(pdev, DRV_MODULE_NAME);

pci_set_master(pdev);

//设置DMA属性

pci_set_dma_mask(pdev, (u64) 0xffffffffffffffff);

//网卡 I/O,memory资源的启始地址

tg3reg_base = pci_resource_start(pdev, 0);

//网卡I/O,memory资源的大小

tg3reg_len = pci_resource_len(pdev, 0);

//分配并设置网络设备

dev = alloc_etherdev(sizeof(*tp));

//申明为内核设备模块

SET_MODULE_OWNER(dev);

//初始化私有结构中的各成员值

tp = dev->priv;

tp->pdev = pdev;

tp->dev = dev;

……

//锁的初始化

spin_lock_init(&tp->lock);

//映射I/O,memory地址到私有域中的寄存器结构

tp->regs = (unsigned long) ioremap(tg3reg_base, tg3reg_len);

dev->irq = pdev->irq;

//网络设备回调函数赋值

dev->open = tg3_open;

dev->stop = tg3_close;

dev->get_stats = tg3_get_stats;

dev->set_multicast_list = tg3_set_rx_mode;

dev->set_mac_address = tg3_set_mac_addr;

dev->do_ioctl = tg3_ioctl;

dev->tx_timeout = tg3_tx_timeout;

dev->hard_start_xmit= tg3_start_xmit;

//网卡的MAC地址赋值dev->addr

tg3_get_device_address(tp);

//注册网络设备

register_netdev(dev);

//把网络设备指针地址放入PCI设备中的设备指针中

pci_set_drvdata(pdev, dev);

}

3,注销网络设备

static void __devexit tg3_remove_one(struct pci_dev *pdev)

{

struct net_device *dev = pci_get_drvdata(pdev);

//注销网络设备

unregister_netdev(dev);

//取消地址映射

iounmap((void *) ((struct tg3 *)(dev->priv))->regs);

//释放网络设备

kfree(dev);

//释放PCI资源

pci_release_regions(pdev);

//停用PCI设备

pci_disable_device(pdev);

//PCI设备中的设备指针赋空

pci_set_drvdata(pdev, NULL);

}

4,打开网络设备

static int tg3_open(struct net_device *dev)

{

//分配一个中断

request_irq(dev->irq, tg3_interrupt, SA_SHIRQ, dev->name, dev);

/* int request_irq(unsigned int irq,

void (*handler)(int irq, void *dev_id, struct pt_regs *regs),

unsigned long irqflags,

const char * devname,

void *dev_id);

irq是要申请的硬件中断号。在Intel平台,范围0--15。handler是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,device id,寄存器值。dev_id就是下面的request_irq时传递给系统的参数dev_id。irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT,标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用rq2dev_map找到中断对应的设备。*/

//初始化硬件

tg3_init_hw(tp);

//初始化收包和发包的缓冲区

tg3_init_rings(tp);

//初始化定时器

init_timer(&tp->timer);

tp->timer.expires = jiffies + tp->timer_offset;

tp->timer.data = (unsigned long) tp;

tp->timer.function = tg3_timer; //超时回调函数

add_timer(&tp->timer);

//允许网卡开始传输包

netif_start_queue(dev);

}

5,关闭网络设备

static int tg3_close(struct net_device *dev)

{

//停止网卡传输包

netif_stop_queue(dev);

netif_carrier_off(tp->dev);

//去除定时器

del_timer_sync(&tp->timer);

//释放收包和发包的缓冲区

tg3_free_rings(tp);

//释放中断

free_irq(dev->irq, dev);

}

6,硬件处理数据包发送

static int tg3_start_xmit(struct sk_buff *skb, struct net_device *dev)

{

len = (skb->len - skb->data_len);

//以DMA方式向网卡物理设备传输包。如果是wireless的话,需要根据802.11协议及硬件的规范从新填充

//硬件帧头,然后提交给硬件发送。

mapping = pci_map_single(tp->pdev, skb->data, len, PCI_DMA_TODEVICE);

tp->tx_buffers[entry].skb = skb;

pci_unmap_addr_set(&tp->tx_buffers[entry], mapping, mapping);

//硬件发送

tg3_set_txd(tp, entry, mapping, len, base_flags, mss_and_is_end);

//记录发包开始时间

dev->trans_start = jiffies;

}

7,中断处理收包,发包

static void tg3_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

//如果要收包

tg3_rx(tp);

//如果要发包

tg3_tx(tp);

}

8,发包

static void tg3_tx(struct tg3 *tp)

{

struct tx_ring_info *ri = &tp->tx_buffers[sw_idx];

struct sk_buff *skb = ri->skb;

//以DMA方式向网卡传输包完毕

pci_unmap_single(tp->pdev, pci_unmap_addr(ri, mapping),

(skb->len - skb->data_len), PCI_DMA_TODEVICE);

ri->skb = NULL;

dev_kfree_skb_irq(skb);

}

9,收包

static int tg3_rx(struct tg3 *tp, int budget)

{

struct sk_buff *copy_skb;

//分配一个包

copy_skb = dev_alloc_skb(len + 2);

copy_skb->dev = tp->dev;

//修改包头空间

skb_reserve(copy_skb, 2);

//加入数据到包中

skb_put(copy_skb, len);

//以DMA方式从网卡传输回数据

pci_dma_sync_single(tp->pdev, dma_addr, len, PCI_DMA_FROMDEVICE);

memcpy(copy_skb->data, skb->data, len);

skb = copy_skb;

//解析包的协议

skb->protocol = eth_type_trans(skb, tp->dev);

//把包送到协议层

netif_rx(skb);

//记录收包时间

tp->dev->last_rx = jiffies;

}

10, 读取包的网卡收发包的状态,统计数据

static struct net_device_stats *tg3_get_stats(struct net_device *dev)

{

//从硬件相关的寄存器读取数据,累加

//stats->rx_packets, stats->tx_packets, stats->rx_bytes, stats->tx_bytes等

}

11, 用户的ioctl命令系统调用

static int tg3_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)

{

struct mii_ioctl_data *data = (struct mii_ioctl_data *)&ifr->ifr_data;

switch(cmd) {

//ethtool程序命令的调用

case SIOCETHTOOL:

return tg3_ethtool_ioctl(dev, (void *) ifr->ifr_data);

//mii程序命令的调用

case SIOCGMIIREG: {

err = tg3_readphy(tp, data->reg_num & 0x1f, &mii_regval)

data->val_out = mii_regval;

return err;

}

……

}

}

12, PCI设备的挂起和恢复函数

static int tg3_suspend(struct pci_dev *pdev, u32 state)

{

//停用网卡的中断寄存器

tg3_disable_ints(tp);

//停止网卡收发包

netif_device_detach(dev);

//停止网卡某些硬件,fireware的一些功能

tg3_halt(tp);

//设置网卡的电源状态

tg3_set_power_state(tp, state);

}

static int tg3_resume(struct pci_dev *pdev)

{

//恢复网卡电源

tg3_set_power_state(tp, 0);

//允许网卡收发包

netif_device_attach(dev);

//初始化收发包的缓冲区

tg3_init_rings(tp);

//初始化网卡硬件

tg3_init_hw(tp);

//打开网卡中断寄存器

tg3_enable_ints(tp);

}

13,参数设置

在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有:

tg3_set_mac_addr (dev->set_mac_address)

当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。

dev->set_config()

当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法

用户会传递一个ifmap结构包含需要的I/O、中断等参数。

总结:

所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象的方法。一个设备就是一个对象(net_device 结构),它内部有自己的数据和方法。一个网络设备最基本的方法有初始化,发送和接收。

Linux网络驱动程序的体系结构可以划分为四层:

网络协议接口,网络设备接口,设备驱动功能,网络设备和网络媒介层

网络驱动程序,最主要的工作就是完成设备驱动功能层。在Linux中所有网络设备都抽象为一个接口,这个接口提供了对所有网络设备的操作集合。由数据结构struct net_device来表示网络设备在内核中的运行情况,即网络设备接口。它既包括纯软件网络设备接口,如环路(Loopback),也包括硬件网络设备接口,如以太网卡。而由以dev_base为头指针的设备链表来集体管理所有网络设备,该设备链表中的每个元素代表一个网络设备接口。数据结构net_device中有很多供系统访问和协议层调用的设备方法,包括初始化,打开和关闭网络设备的open和stop函数,处理数据包发送的hard_start_xmit函数,以及中断处理函数等。

网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

Linux网络设备驱动编程

最高的质量最低的成本——节省70%PCB返修成本 查看最近90天中添加的最新产品 最新电子元器件资料免费下载 派睿电子TI有奖问答 - 送3D汽车鼠标 IR推出采用焊前金属的汽车级绝缘栅双极晶体管 全球电子连接器生产商—samtec 最新断路器保护套

在此仅仅讨论网络设备驱动的一般写法,有关硬件部分的相关代码由于硬件规格不同,予以省略。有什么地方错误,或补充,欢迎大家提出。

1.驱动模块的加载和卸载

如果网络设备(包括wireless)是PCI规范的,则先是向内核注册该PCI设备(pci_register_driver),然后由pci_driver数据结构中的probe函数指针所指向的侦测函数来初始化该PCI设备,并且同时注册和初始化该网络设备。

如果网络设备(包括wireless)是PCMCIA规范的,则先是向内核注册该PCMCIA设备(register_pccard_driver),然后driver_info_t数据结构中的attach函数指针所指向的侦测函数来初始化该PCMCIA设备,并且同时注册和初始化该网络设备。

static int __init tg3_init(void)

{

//先注册成PCI设备,并初始化,如果是其他的ESIA,PCMCIA,用其他函数

return pci_module_init(&tg3_driver);

}

static void __exit tg3_cleanup(void)

{

pci_unregister_driver(&tg3_driver);//注销PCI设备

}

module_init(tg3_init); //驱动模块的加载

module_exit(tg3_cleanup); //驱动模块的卸载

申明为PCI设备:

static struct pci_driver tg3_driver = {

.name = DRV_MODULE_NAME,

.id_table = tg3_pci_tbl, //此驱动所支持的网卡系列,vendor_id, device_id

.probe = tg3_init_one, //初始化网络设备的回调函数

.remove = __devexit_p(tg3_remove_one), //注销网络设备的回调函数

.suspend = tg3_suspend, //设备挂起函数

.resume = tg3_resume //设备恢复函数

};

2.PCI设备探测函数probe,初始化网络设备

static int __devinit tg3_init_one(struct pci_dev *pdev, const struct pci_device_id *ent)

{

//初始化设备,使I/O,memory可用,唤醒设备

pci_enable_device(pdev);

//申请内存空间,配置网卡的I/O,memory资源

pci_request_regions(pdev, DRV_MODULE_NAME);

pci_set_master(pdev);

//设置DMA属性

pci_set_dma_mask(pdev, (u64) 0xffffffffffffffff);

//网卡 I/O,memory资源的启始地址

tg3reg_base = pci_resource_start(pdev, 0);

//网卡I/O,memory资源的大小

tg3reg_len = pci_resource_len(pdev, 0);

//分配并设置网络设备

dev = alloc_etherdev(sizeof(*tp));

//申明为内核设备模块

SET_MODULE_OWNER(dev);

//初始化私有结构中的各成员值

tp = dev->priv;

tp->pdev = pdev;

tp->dev = dev;

……

//锁的初始化

spin_lock_init(&tp->lock);

//映射I/O,memory地址到私有域中的寄存器结构

tp->regs = (unsigned long) ioremap(tg3reg_base, tg3reg_len);

dev->irq = pdev->irq;

//网络设备回调函数赋值

dev->open = tg3_open;

dev->stop = tg3_close;

dev->get_stats = tg3_get_stats;

dev->set_multicast_list = tg3_set_rx_mode;

dev->set_mac_aDDRess = tg3_set_mac_addr;

dev->do_ioctl = tg3_ioctl;

dev->tx_timeout = tg3_tx_timeout;

dev->hard_start_xmit= tg3_start_xmit;

//网卡的MAC地址赋值dev->addr

tg3_get_device_address(tp);

//注册网络设备

register_netdev(dev);

//把网络设备指针地址放入PCI设备中的设备指针中

pci_set_drvdata(pdev, dev);

}

3.注销网络设备

static void __devexit tg3_remove_one(struct pci_dev *pdev)

{

struct net_device *dev = pci_get_drvdata(pdev);

//注销网络设备

unregister_netdev(dev);

//取消地址映射

iounmap((void *) ((struct tg3 *)(dev->priv))->regs);

//释放网络设备

kfree(dev);

//释放PCI资源

pci_release_regions(pdev);

//停用PCI设备

pci_disable_device(pdev);

//PCI设备中的设备指针赋空

pci_set_drvdata(pdev, NULL);

}

4.打开网络设备

static int tg3_open(struct net_device *dev)

{

//分配一个中断

request_irq(dev->irq, tg3_interrupt, SA_SHIRQ, dev->name, dev);

/* int request_irq(unsigned int irq,

void (*handler)(int irq, void *dev_id, struct pt_regs *regs),

unsigned long irqflags,

const char * devname,

void *dev_id);

irq是要申请的硬件中断号。在Intel平台,范围0--15。handler是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,device id,寄存器值。dev_id就是下面的request_irq时传递给系统的参数dev_id。irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT,标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用rq2dev_map找到中断对应的设备。*/

//初始化硬件

tg3_init_hw(tp);

//初始化收包和发包的缓冲区

tg3_init_rings(tp);

//初始化定时器

init_timer(&tp->timer);

tp->timer.expires = jiffies + tp->timer_offset;

tp->timer.data = (unsigned long) tp;

tp->timer.function = tg3_timer; //超时回调函数

add_timer(&tp->timer);

//允许网卡开始传输包

netif_start_queue(dev);

}

5.关闭网络设备

static int tg3_close(struct net_device *dev)

{

//停止网卡传输包

netif_stop_queue(dev);

netif_carrier_off(tp->dev);

//去除定时器

del_timer_sync(&tp->timer);

//释放收包和发包的缓冲区

tg3_free_rings(tp);

//释放中断

free_irq(dev->irq, dev);

}

[NextPage]

6.硬件处理数据包发送

static int tg3_start_xmit(struct sk_buff *skb, struct net_device *dev)

{

len = (skb->len - skb->data_len);

//以DMA方式向网卡物理设备传输包。如果是wireless的话,需要根据802.11协议及硬件的规范从新填充

//硬件帧头,然后提交给硬件发送。

mapping = pci_map_single(tp->pdev, skb->data, len, PCI_DMA_TODEVICE);

tp->tx_buffers[entry].skb = skb;

pci_unmap_addr_set(&tp->tx_buffers[entry], mapping, mapping);

//硬件发送

tg3_set_txd(tp, entry, mapping, len, base_flags, mss_and_is_end);

//记录发包开始时间

dev->trans_start = jiffies;

}

7.中断处理收包,发包

static void tg3_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

//如果要收包

tg3_rx(tp);

//如果要发包

tg3_tx(tp);

}

8.发包

static void tg3_tx(struct tg3 *tp)

{

struct tx_ring_info *ri = &tp->tx_buffers[sw_idx];

struct sk_buff *skb = ri->skb;

//以DMA方式向网卡传输包完毕

pci_unmap_single(tp->pdev, pci_unmap_addr(ri, mapping),

(skb->len - skb->data_len), PCI_DMA_TODEVICE);

ri->skb = NULL;

dev_kfree_skb_irq(skb);

}

9.收包

static int tg3_rx(struct tg3 *tp, int budget)

{

struct sk_buff *copy_skb;

//分配一个包

copy_skb = dev_alloc_skb(len + 2);

copy_skb->dev = tp->dev;

//修改包头空间

skb_reserve(copy_skb, 2);

//加入数据到包中

skb_put(copy_skb, len);

//以DMA方式从网卡传输回数据

pci_dma_sync_single(tp->pdev, dma_addr, len, PCI_DMA_FROMDEVICE);

memcpy(copy_skb->data, skb->data, len);

skb = copy_skb;

//解析包的协议

skb->protocol = eth_type_trans(skb, tp->dev);

//把包送到协议层

netif_rx(skb);

//记录收包时间

tp->dev->last_rx = jiffies;

}

10.读取包的网卡收发包的状态,统计数据

static struct net_device_stats *tg3_get_stats(struct net_device *dev)

{

//从硬件相关的寄存器读取数据,累加

//stats->rx_packets, stats->tx_packets, stats->rx_bytes, stats->tx_bytes等

}

11.用户的ioctl命令系统调用

static int tg3_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)

{

struct mii_ioctl_data *data = (struct mii_ioctl_data *)&ifr->ifr_data;

switch(cmd) {

//ethtool程序命令的调用

case SIO*HTOOL:

return tg3_ethtool_ioctl(dev, (void *) ifr->ifr_data);

//mii程序命令的调用

case SIOCGMIIREG: {

err = tg3_readphy(tp, data->reg_num & 0x1f, &mii_regval)

data->val_out = mii_regval;

return err;

}

……

}

}

12.PCI设备的挂起和恢复函数

static int tg3_suspend(struct pci_dev *pdev, u32 state)

{

//停用网卡的中断寄存器

tg3_disable_ints(tp);

//停止网卡收发包

netif_device_detach(dev);

//停止网卡某些硬件,fireware的一些功能

tg3_halt(tp);

//设置网卡的电源状态

tg3_set_power_state(tp, state);

}

static int tg3_resume(struct pci_dev *pdev)

{

//恢复网卡电源

tg3_set_power_state(tp, 0);

//允许网卡收发包

netif_device_attach(dev);

//初始化收发包的缓冲区

tg3_init_rings(tp);

//初始化网卡硬件

tg3_init_hw(tp);

//打开网卡中断寄存器

tg3_enable_ints(tp);

}

13.参数设置

在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有:

tg3_set_mac_addr (dev->set_mac_address)

当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。

dev->set_config()

当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法

用户会传递一个ifmap结构包含需要的I/O、中断等参数。

总结:

所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象的方法。一个设备就是一个对象(net_device 结构),它内部有自己的数据和方法。一个网络设备最基本的方法有初始化,发送和接收。

Linux网络驱动程序的体系结构可以划分为四层:

网络协议接口,网络设备接口,设备驱动功能,网络设备和网络媒介层

网络驱动程序,最主要的工作就是完成设备驱动功能层。在Linux中所有网络设备都抽象为一个接口,这个接口提供了对所有网络设备的操作集合。由数据结构struct net_device来表示网络设备在内核中的运行情况,即网络设备接口。它既包括纯软件网络设备接口,如环路(Loopback),也包括硬件网络设备接口,如以太网卡。而由以dev_base为头指针的设备链表来集体管理所有网络设备,该设备链表中的每个元素代表一个网络设备接口。数据结构net_device中有很多供系统访问和协议层调用的设备方法,包括初始化,打开和关闭网络设备的open和stop函数,处理数据包发送的hard_start_xmit函数,以及中断处理函数等。

网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

浅谈 Linux 内核开发之网络设备驱动

赵 昊翔, 软件工程师, Cisco Systems

简介: 本文介绍了网络设备的基本概念,并从 Linux 内核的角度出发,介绍了网络设备驱动的开发方法和开发中的注意事项。

标记本文!

发布日期: 年 7 月 15 日

级别: 中级

访问情况 435 次浏览

建议: 3 (查看或添加评论)

平均分 (共 10 个评分 )

网络设备介绍

网络设备是计算机体系结构中必不可少的一部分,处理器如果想与外界通信,通常都会选择网络设备作为通信接口。众所周知,在 OSI(Open Systems Interconnection,开放网际互连)中,网络被划分为七个层次,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。我们所讲的网络设备也包括两个层次,一层叫做 MAC(Media Access Control)层,对应于 OSI 的数据链路层;另一层叫做 PHY(Physical Layer)层,对应于物理层。

常用的网络设备有很多,比如 PPC85XX 的 TSEC、AMCC 440GX 的 EMAC、INTEL 的 82559 等,它们的工作原理基本相同。

DMA 介绍

网络设备的核心处理模块是一个被称作 DMA(Direct Memory Access)的控制器,DMA 模块能够协助处理器处理数据收发。对于数据发送来说,它能够将组织好的数据自动发出,无需处理器干预;对于数据接收来说,它能够将收到的数据以一定的格式组织起来,通知处理器,并等待处理器来取。

DMA 模块收发数据的单元被称为 BD(Buffer Description,缓存描述符),每个包都会被分成若干个帧,而每个帧则被保存在一个 BD 中。BD 结构通常包含有以下字段:

typedef struct {

void *bufptr; /* 保存当前 BD 对应缓存的起始地址 */

int length; /* 保存缓存中存储的数据包长度 */

int sc; /* 保存当前 BD 的状态信息 */

} BD_STRUCT;

所有的 BD 就组成了一张 BD 表,如图 1 所示,一般来说发送方向和接收方向的 BD 表是各自独立的。

图 1. BD 表结构

数据发送流程

网络设备通过 DMA 进行数据发送的流程如 图 2所示。

图 2. 数据发送流程

图中各步骤的具体含义描述如下:

(1)协议层通知处理器开始发送数据;

(2)处理器从 BD 表中取出一个 BD,将需要发送的数据拷贝至当前 BD 对应的缓存内,并设置好 BD 的状态;

(3)处理器通知网络设备开始发送数据;

(4)MAC 模块通知 DMA 单元开始发送数据;

(5)DMA 模块操作 BD 表,取出当前有效 BD;

(6)DMA 模块将当前 BD 对应缓存内的数据发送至 MAC 模块;

(7)MAC 模块将这些数据发送到网络中;

(8)网络设备通知处理器数据发送完毕;

(9)处理器通知协议层发送下面一帧数据。

其中步骤(4)~(8)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。

数据接收流程

网络设备通过 DMA 进行数据接收的流程如图 3 所示。

图 3. 数据接收流程

图中各步骤的具体含义描述如下:

(1)处理器初始化 BD 表;

(2)处理器初始化网络设备;

(3)MAC 模块从网络中接收数据;

(4)MAC 模块通知 DMA 模块来取数据;

(5)DMA 模块从 BD 表中取出合适的 BD;

(6)MAC 模块将数据发送至当前 BD 对应的缓存内;

(7)网络设备通知处理器开始接收数据(以中断方式或轮询方式);

(8)协议层从当前的 BD 缓存内取走数据。

其中步骤(3)~(6)是硬件自动完成的,不需要软件的干预,如此可以节省处理器的工作量。

回页首

Linux 网络设备驱动模型

数据结构

数据结构

Linux 内核中对网络设备进行描述的核心结构类型叫做 net_device,net_device 结构定义在 include/linux/netdevice.h 文件中。该结构的字段可以分为以下几类。

全局信息

该类中包含了设备名(name 字段)、设备状态(state 字段)、设备初始化函数(init 字段)等。

硬件信息

该类中包含了设备内存使用情况(mem_end 和 mem_start 字段)、中断号(irq 字段)、IO 基地址(base_addr 字段)等。

接口信息

该类中包含了 MAC 地址(dev_addr 字段)、设备属性(flag 字段)、最大传输单元(mtu 字段)等。

设备接口函数

该类中包含了当前设备所提供的所有接口函数,比如设备打开函数(open 字段),该函数负责打开设备接口,当用户使用 ifconfig 命令配置网络时,该函数默认被调用;设备停止函数(stop 字段),该函数负责关闭设备接口;数据发送函数(hard_start_xmit 字段),当用户调用 socket 开始写数据时,该函数被调用,并负责往网络设备中发送数据。

函数接口

设备初始化函数

网络设备驱动在 Linux 内核中是以内核模块的形式存在的,对应于模块的初始化,需要提供一个初始化函数来初始化网络设备的硬件寄存器、配置 DMA 以及初始化相关内核变量等。设备初始化函数在内核模块被加载时调用,它的函数形式如下:

static int __init xx_init (void) {

……

}

module_init(xx_init); // 这句话表明模块加载时自动调用 xx_init 函数

设备初始化函数主要完成以下功能:

1. 硬件初始化

因为网络设备主要分为 PHY、MAC 和 DMA 三个硬件模块,开发者需要分别对这三个模块进行初始化。

1.初始化 PHY 模块,包括设置双工 / 半双工运行模式、设备运行速率和自协商模式等。

2.初始化 MAC 模块,包括设置设备接口模式等。

3.初始化 DMA 模块,包括建立 BD 表、设置 BD 属性以及给 BD 分配缓存等。

2. 内核变量初始化

初始化并注册内核设备。内核设备是属性为 net_device 的一个变量,开发者需要申请该变量对应的空间(通过 alloc_netdev 函数)、设置变量参数、挂接接口函数以及注册设备(通过 register_netdev 函数)。

常用的挂接接口函数如下:

net_device *dev_p;

dev_p->open = xx_open; // 设备打开函数

dev_p->stop = xx_stop; // 设备停止函数

dev_p->hard_start_xmit = xx_tx; // 数据发送函数

dev_p->do_ioctl = xx_ioctl; // 其它的控制函数

……

数据收发函数

数据的接收和发送是网络设备驱动最重要的部分,对于用户来说,他们无需了解当前系统使用了什么网络设备、网络设备收发如何进行等,所有的这些细节对于用户都是屏蔽的。Linux 使用 socket 做为连接用户和网络设备的一个桥梁。用户可以通过 read / write 等函数操作 socket,然后通过 socket 与具体的网络设备进行交互,从而进行实际的数据收发工作。

Linux 提供了一个被称为 sk_buff 的数据接口类型,用户传给 socket 的数据首先会保存在 sk_buff 对应的缓冲区中,sk_buff 的结构定义在 include/linux/skbuff.h 文件中。它保存数据包的结构示意图如下所示。

图 4. sk_buff 数据结构图

1. 数据发送流程

当用户调用 socket 开始发送数据时,数据被储存到了 sk_buff 类型的缓存中,网络设备的发送函数(设备初始化函数中注册的 hard_start_xmit)也随之被调用,流程图如下所示。

图 5. 数据发送流程图

a.用户首先创建一个 socket,然后调用 write 之类的写函数通过 socket 访问网络设备,同时将数据保存在 sk_buff 类型的缓冲区中。

b.socket 接口调用网络设备发送函数(hard_start_xmit),hard_start_xmit 已经在初始化过程中被挂接成类似于 xx_tx 的具体的发送函数,xx_tx 主要实现如下步骤。

1.从发送 BD 表中取出一个空闲的 BD。

2.根据 sk_buff 中保存的数据修改 BD 的属性,一个是数据长度,另一个是数据包缓存指针。值得注意的是,数据包缓存指针对应的必须是物理地址,这是因为 DMA 在获取 BD 中对应的数据时只能识别储存该数据缓存的物理地址。

bd_p->length = skb_p->len;

bd_p->bufptr = virt_to_phys(skb_p->data);

3.修改该 BD 的状态为就绪态,DMA 模块将自动发送处于就绪态 BD 中所对应的数据。

4.移动发送 BD 表的指针指向下一个 BD。

c.DMA 模块开始将处于就绪态 BD 缓存内的数据发送至网络中,当发送完成后自动恢复该 BD 为空闲态。

2. 数据接收流程

当网络设备接收到数据时,DMA 模块会自动将数据保存起来并通知处理器来取,处理器通过中断或者轮询方式发现有数据接收进来后,再将数据保存到 sk_buff 缓冲区中,并通过 socket 接口读出来。流程图如下所示。

图 6. 数据接收流程图

a.网络设备接收到数据后,DMA 模块搜索接收 BD 表,取出空闲的 BD,并将数据自动保存到该 BD 的缓存中,修改 BD 为就绪态,并同时触发中断(该步骤可选)。

b.处理器可以通过中断或者轮询的方式检查接收 BD 表的状态,无论采用哪种方式,它们都需要实现以下步骤。

1.从接收 BD 表中取出一个空闲的 BD。

2.如果当前 BD 为就绪态,检查当前 BD 的数据状态,更新数据接收统计。

3.从 BD 中取出数据保存在 sk_buff 的缓冲区中。

4.更新 BD 的状态为空闲态。

5.移动接收 BD 表的指针指向下一个 BD。

c.用户调用 read 之类的读函数,从 sk_buff 缓冲区中读出数据,同时释放该缓冲区。

中断和轮询

Linux 内核在接收数据时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。

中断方式

如果选择中断方式,首先在使用该驱动之前,需要将该中断对应的中断类型号和中断处理程序注册进去。网络设备驱动在初始化时会将具体的 xx_open 函数挂接在驱动的 open 接口上,xx_open 函数挂接中断的步骤如下。

request_irq(rx_irq, xx_isr_rx, …… );

request_irq(tx_irq, xx_isr_tx, …… );

网络设备的中断一般会分为两种,一种是发送中断,另一种是接收中断。内核需要分别对这两种中断类型号进行注册。

1.发送中断处理程序(xx_isr_tx)的工作主要是监控数据发送状态、更新数据发送统计等。

2.接收中断处理程序(xx_isr_rx)的工作主要是接收数据并传递给协议层、监控数据接收状态、更新数据接收统计等。

对于中断方式来说,由于每收到一个包都会产生一个中断,而处理器会迅速跳到中断服务程序中去处理收包,因此中断接收方式的实时性高,但如果遇到数据包流量很大的情况时,过多的中断会增加系统的负荷。

轮询方式

如果采用轮询方式,就不需要使能网络设备的中断状态,也不需要注册中断处理程序。操作系统会专门开启一个任务去定时检查 BD 表,如果发现当前指针指向的 BD 非空闲,则将该 BD 对应的数据取出来,并恢复 BD 的空闲状态。

由于是采用任务定时检查的原理,从而轮询接收方式的实时性较差,但它没有中断那种系统上下文切换的开销,因此轮询方式在处理大流量数据包时会显得更加高效。

回页首

Linux 网络设备驱动优化

随着科技的不断发展,网络设备所能承载的速率在不断提升,当前流行的网络设备普遍都能支持 10Mbps / 100Mbps / 1Gbps 这三种速率。虽然网络设备的硬件性能在不断的提升,但是实际在 Linux 系统中其运行性能(收发包速率)真能达到多达 1Gbps 的水平吗?这和处理器的性能有关,一般来说我们运行的系统中报文的收发速率是达不到 1Gbps 的(因为我们不可能将所有处理器的资源都贡献给报文的收发),但是我们可以在有限的条件下尽可能的采取一些优化手段提高网络设备的运行性能。

Cache 的应用

Cache 位于存储系统金字塔的顶层(下面一层是内存),Cache 的容量不大(一级 Cache 一般是几十 KB,二级 Cache 一般是几 MB),但是它的访问速率却是内存的几十倍。因此如果处理器通过 Cache 来访问内存,将会极大的提高访问速率。在网络设备的数据收发中,恰当的应用 Cache 可以优化驱动的性能。下面列举几点 Cache 的优化措施。

合理设置内存属性

内存的页表有多种属性,其中有一项就是是否通过 Cache 访问。在给 BD 表配置内存时,这些被分配的内存属性需要支持 Cache 访问。

Cache 的访问还有两种方式:一种是写回操作(Write Back),处理器更新内存数据时,该数据首先保存在 Cache 中,Cache 并不及时将数据更新进内存,而是等到 Cache 需要再次更新时才会将数据写回到内存中。另一种是写穿操作(Write Through),处理器更新内存数据时,该数据首先保存在 Cache 中,Cache 随即将数据立刻更新进内存。显而易见,写回操作的性能比写穿操作更高,通常我们设置内存页表属性为写回方式。

数据收发时的 Cache 操作

在内存支持 Cache 且采用写回方式的情况下,当发送数据时,处理器先将数据写进 Cache,如果 DMA 模块直接从内存中取出数据发送的话,该数据将与 Cache 并不一致,因此在驱动程序中,需要将 Cache 中的数据更新到内存,然后再通知 DMA 进行发送。

当接收数据时,DMA 模块会将数据收到内存中,如果这时候处理器从该内存接收数据的话,处理器会从 Cache 中取数据,但是 Cache 并不知道内存已经被更新,这就会导致接收到的数据与实际不符,因此在驱动程序中,需要在接收数据之前刷新一下该 Cache,以保证 Cache 与内存的一致性。

需要说明的是,并不是所有处理器都需要以上操作,有的处理器所带的 DMA 控制器是能感知 Cache(IO-Cache Coherence)的,它们能够自动进行上述的 Cache 操作,因此对于这类处理器,驱动程序中无需关注 Cache。

中断还是轮询?

前面曾经介绍过,网络设备驱动支持两种接收数据的方式,一种是中断,另一种是轮询,在数据流量比较大的情况下,可以考虑采用轮询的方式以达到更高的效率。

当采用轮询方式时,还有一个不得不考虑的问题,那就是轮询任务优先级的选择,众所周知,当任务优先级高时,该任务不会被其他的低优先级任务所打断,从而可以保证处理器能够专心完成数据接收工作;但如果任务优先级低时,一旦发生了其他高优先级的任务,处理器会将当前的数据接收工作暂停,转而执行别的任务,如此会影响网络设备驱动的效率。因此驱动设计者需要结合实际情况,恰当的选择任务的优先级。

设备接口模式

有时候我们会发现虽然网络设备号称有 100Mbps 的速率,但是实际数据收发却非常慢,遇到这种情况,我们首先需要检查网络设备接口模式是否设置正确。

PHY 模块接口模式

PHY 模块的接口模式有两种,强制模式(强制 10M / 100M / 1G 等)和自协商模式。究竟选择哪种模式需要看当前 PHY 模块所连接的对端 PHY 状态才行,如果对端设置的是自协商模式,本端的 PHY 模块也需要相应设置成自协商,如此就能够保证协商出来的结果是当前链路所能支持的最大速率。反之,如果对端设置成强制模式,本端也需要设置成强制,且强制速率要与对端设置的强制速率相同。

MAC 模块接口模式

MAC 模块对于不同的速率(10M / 100M / 1G 等)也会有不同的接口模式选择,如果设置的模式与 PHY 模块所运行的速率不匹配的话,会极大的影响网络设备数据收发的速度。因此在初始化 MAC 模块时,需要检查 PHY 模块的运行速率,从而选择恰当的接口模式。

每个 PHY / MAC 模块设备的接口模式选择都不尽相同,因此在开发网络设备驱动时,需要明确所使用的设备,并在该设备初始化时正确配置其接口模式。

Linux网络设备驱动指南(1)

-06-22 11:49 王成国 我要评论(0)

摘要:Linux中网络接口是3大标准类设备之一,其他2种类型分别是字符设备和块设备。Unix世界里的"一切皆是文件"的论述,对网络设备接口来说是不适用的,下面介绍网络接口设备的驱动程序如何和内核模块进行交互的知识。

标签:驱动 网络设备 Linux snull 接口

限时报名参加“甲骨文全球大会··北京”及“JavaOne和甲骨文开发者大会”

【独家特稿】前言介绍,本文所涉及概念:

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等函数。

Linux网络设备驱动指南(2)

-06-22 11:49 王成国 我要评论(0)

摘要:Linux中网络接口是3大标准类设备之一,其他2种类型分别是字符设备和块设备。Unix世界里的"一切皆是文件"的论述,对网络设备接口来说是不适用的,下面介绍网络接口设备的驱动程序如何和内核模块进行交互的知识。

标签:驱动 网络设备 Linux snull 接口

限时报名参加“甲骨文全球大会··北京”及“JavaOne和甲骨文开发者大会”

网络数据包的接收代码说明:

首先网络接口设备插槽的接受缓冲区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设备驱动,可以参考.

设计Linux系统网络设备驱动程序 (1)

发布时间:2002.09.18 13:23 来源:开放系统世界——赛迪网 作者:李卫刚

Linux网络设备驱动程序是Linux操作系统网络应用中的一个重要组成部分。分析其运行机理,对于设计Linux网络应用程序是很有帮助的。我们可以在网络驱动程序这一级做一些与应用相关联的特殊事情,例如在设计Linux防火墙和网络入侵检测系统时,可以在网络驱动程序的基础上拦截网络数据包,继而对其进行分析。由于Linux是开放源代码的,所以给我们提供了一个分析和改造网络驱动程序,并使其满足特殊应用的绝好机会。本文对Linux内核中的网络驱动程序部分进行了详细讨论,并给出了实现Linux网络驱动程序的重要过程、一种实现模式和具体实例。

运行机理

1.体系结构

Linux网络驱动程序的体系结构如图1所示。可以划分为四层,从上到下分别为协议接口层、网络设备接口层、提供实际功能的设备驱动功能层,以及网络设备和网络媒介层。在设计网络驱动程序时,最主要的工作就是完成设备驱动功能层,使其满足我们自己所需的功能。在Linux中,把所有网络设备都抽象为一个接口。这个接口提供了对所有网络设备的操作集合。由数据结构 struct device来表示网络设备在内核中的运行情况,即网络设备接口。它既包括纯软件网络设备接口,如环路(Loopback),也可以包括硬件网络设备接口,如以太网卡。它由以dev_base为头指针的设备链表来集中管理所有网络设备。该设备链表中的每个元素代表一个网络设备接口。数据结构device中有很多供系统访问和协议层调用的设备方法,包括供设备初始化和往系统注册用的init函数、打开和关闭网络设备的open和stop函数、处理数据包发送的函数hard_ start_xmit,以及中断处理函数等。有关device数据结构(在内核中也就是net_device)的详细内容,请参看/linux/include/linux/netdevice.h。

2.初始化

网络设备的初始化主要是由device数据结构中的init函数指针所指的初始化函数来完成的。当内核启动或加载网络驱动模块的时候,就会调用初始化过程。这个过程将首先检测网络物理设备是否存在。它通过检测物理设备的硬件特征来完成,然后再对设备进行资源配置。这些完成之后就要构造设备的device数据结构,用检测到的数值来对device中的变量初始化。这一步很重要。最后向Linux内核注册该设备并申请内存空间。

3. 数据包的发送与接收

数据包的发送和接收是实现Linux网络驱动程序中两个最关键的过程。对这两个过程处理的好坏将直接影响到驱动程序的整体运行质量。图1中也很明确地说明了网络数据包的传输过程。首先在网络设备驱动加载时,通过device域中的init函数指针调用网络设备的初始化函数,对设备进行初始化。如果操作成功就可以通过device域中的open函数指针调用网络设备的打开函数打开设备,再通过device域中的建立硬件包头函数指针hard_header来建立硬件包头信息。最后通过协议接口层函数dev_queue_xmit(详见/linux/net/core/dev.c)来调用device域中的hard_start_xmit函数指针,完成数据包的发送。该函数将把存放在套接字缓冲区中的数据发送到物理设备。该缓冲区是由数据结构sk_buff (详见/linux/include/linux/sk_buff.h)来表示的。

数据包的接收是通过中断机制来完成的。当有数据到达时,就产生中断信号,网络设备驱动功能层就调用中断处理程序,即数据包接收程序来处理数据包的接收。然后,网络协议接口层调用netif_rx函数(详见/linux/net/core/dev.c),把接收到的数据包传输到网络协议的上层进行处理。

实现模式

实现Linux网络设备驱动功能主要有两种形式:一是通过内核来进行加载,当内核启动的时候,就开始加载网络设备驱动程序,内核启动完成之后,网络驱动功能也随即实现了;再就是通过模块加载的形式。比较两者,第二种形式更加灵活。在此着重对模块加载形式进行讨论。

模块设计是Linux中特有的技术,它使Linux内核功能更容易扩展。采用模块来设计Linux网络设备驱动程序会很轻松,并且能够形成固定的模式。任何人只要依照这个模式去设计,都能设计出优良的网络驱动程序。先简要介绍一下基于模块加载网络驱动程序的设计步骤,后面还结合具体实例来讲解。首先通过模块加载命令insmod来把网络设备驱动程序插入到内核之中。然后,insmod将调用init_module()函数首先对网络设备的init函数指针初始化,再通过调用register_netdev()函数在Linux系统中注册该网络设备。如果成功,再调用init函数指针所指的网络设备初始化函数来对设备初始化,将设备的device数据结构插入到dev_base链表的末尾。最后可以通过执行模块卸载命令rmmod,来调用网络驱动程序中的cleanup_module()函数,对网络驱动程序模块进行卸载。具体实现过程见图2所示。

通过模块初始化网络接口是在编译内核时标记为编译为模块。系统在启动时并不知道该接口的存在,需要用户在/etc/rc.d/目录中定义的初始启动脚本中写入命令或手动将模块插入内核空间来激活网络接口。这也给我们在何时加载网络设备驱动程序提供了灵活性。

1 2 下一页>>

设计Linux系统网络设备驱动程序 (2)

发布时间:2002.09.18 13:23 来源:开放系统世界——赛迪网 作者:李卫刚

应用实例

我们以NE2000兼容网卡为例,来具体介绍基于模块的网络驱动程序的设计过程。可以参考文件linux/drivers/net/ne.c和linux/drivers/net/8390.c。

1.模块加载和卸载

NE2000网卡的模块加载功能由init_module()函数完成。具体过程及解释如下:

int init_module(void)

{

int this_dev, found = 0;

//循环检测ne2000类型的网络设备接口

for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++)

{

//获得网络接口对应的net-device结构指针

struct net_device *dev = &#38;dev_ne[this_dev];

dev->irq = irq[this_dev]; //初始化该接口的中断请求号

dev->mem_end = bad[this_dev]; //初始化接收缓冲区的终点位置

dev->base_addr = io[this_dev]; //初始化网络接口的I/O基地址

dev->init = ne_probe; //初始化init为ne_probe,后面介绍此函数

//调用registre_netdevice()向系统登记网络接口,在这个函数中将分配给网络接口在系统中惟一

的名称。并且将该网络接口设备添加到系统管理的链表dev-base中进行管理。

if (register_netdev(dev) == 0) {

found++;

continue; }

… //省略

}

return 0;}

模块卸载功能由cleanup_module()函数来实现。如下所示:

void cleanup_module(void)

{

int this_dev;

//遍历整个dev-ne数组

for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++) {

//获得net-device结构指针

struct net_device *dev = &#38;dev_ne[this_dev];

if (dev->priv != NULL) {

void *priv = dev->priv;

struct pci_dev *idev = (struct pci_dev *)ei_status.priv;

//调用函数指针 idev->deactive将已经激活的网卡关闭使用

if (idev) idev->deactivate(idev);

free_irq(dev->irq, dev);

//调用函数release_region()释放该网卡占用的I/O地址空间

release_region(dev->base_addr, NE_IO_EXTENT);

//调用unregister_netdev()注销 这个net_device()结构

unregister_netdev(dev);

kfree(priv); //释放priv空间

}

}

}

2.网络接口初始化

实现此功能是由ne_probe()函数来完成的。前面已经提到过,在init_module()函数中用它来初始化init函数指针。它主要对网卡进行检测,并且初始化系统中网络设备信息,用于后面的网络数据的发送和接收。具体过程及解释如下:

int __init ne_probe(struct net_device *dev)

{

unsigned int base_addr = dev->base_addr;

//初始化dev-owner成员,因为使用模块类型驱动,会将dev-owner指向对象modules结构指针。

SET_MODULE_OWNER(dev);

//检测dev->base_addr是否合法,是则执行ne-probe1()函数检测过程。不是,则需要自动检测。

if (base_addr > 0x1ff)

return ne_probe1(dev, base_addr);

else if (base_addr != 0)

return -ENXIO;

//如果有ISAPnP设备,则调用ne_probe_isapnp()检测这种类型的网卡。

if (isapnp_present() &#38;&#38; (ne_probe_isapnp(dev) == 0))

return 0;

…//省略

return -ENODEV;

}

这其中两个函数ne_probe_isapnp()和ne_probe19()的区别在于检测中断号上。PCI方式只需指定I/O基地址就可以自动获得IRQ,是由BIOS自动分配的;而ISA方式需要获得空闲的中断资源才能分配。

3.网络接口设备打开和关闭

网络接口设备打开就是激活网络接口,使它能接收来自网络的数据并且传递到网络协议栈的上面,也可以将数据发送到网络上。设备关闭就是停止操作。

在NE2000网络驱动程序中,网络设备打开由dev_open()和ne_open()完成,设备关闭有dev_close()和ne_close()完成。它们相应调用底层函数ei_open()和ei_close()来完成。其实现过程相对简单,不再赘述。

4.数据包接收和发送

在驱动程序层次上的发送和接收数据都是通过低层对硬件的读写来完成的。当网络上的数据到来时,将触发硬件中断,根据注册的中断向量表确定处理函数,进入中断向量处理程序,将数据送到上层协议进行处理。

对NE2000网卡的数据接收过程是由ne_probe()函数中的中断处理函数ei_interrupt来完成的。在进入ei_interrupt()之后再通过ei_receive()从8309的接收缓冲区获得数据,并组合成sk_buff结构,再通过netif_rx()函数将接收到的数据存放在系统的饿接收队列之中。

Ei-interrupt()的函数原型:void ei_interrupt(int irq,void *dev_id, struct pt_regs *regs)。其中irq为中断号,dev-id是表示产生中断的网络接口设备对应的结构指针,regs表示当前的寄存器内容。数据发送是由dev_dev_start_xmit函数指针对应的ei_start_xmit函数它来完成数据包的发送。在函数ethdev_init()把net_device结构的hard_start_xmit指针初始化为ei_start_xmit。

设计Linux网络设备驱动程序有一定的模式。遵循这个模式,将会大大减轻我们设计程序的工作量。本文分析了网络驱动程序在内核中的工作机理,提出了设计网络驱动程序的一种模式,并给出了实例。这对设计复杂的网络驱动程序是很有帮助的

如果觉得《Linux网络设备驱动结构概述》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。