Linux网络设备驱动程序体系结构分为四层:网络协议接口层、网络设备接口层、提供实际功能的设备驱动层以及网络设备与媒介层。
(1)网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。
(2)网络设备接口层向协议接口层提供的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层各函数的容器。
(3)设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过nto_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。
(4)网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数在物理上驱动。
驱动工程师的工作:在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核。
1 网络协议接口层
网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口。当上层ARP或IP需要发送数据包时,它将调用网络协议接口层的dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个指向struct sk_buff数据结构的指针。dev_queue_xmit()函数的原形为:
int dev_queue_xmit(struct sk_buff *skb);
上层通过对数据包的接收也通过向netif_rx()函数传递一个struct sk_buff数据结构的指针来完成。netif_rx()函数的原形为:
int netif_rx(struct sk_buff *skb);
sk_buff定义于include/linux/skbuff.h文件中,含义为“套接字缓冲区”用于在Linux网络子系统各层之间传递数据,是Linux网络子系统数据传递的“中枢神经”。
当发送数据包时,Linux内核的网络处理模块必须建立一个包含要传输的数据包的sk_buff,然后将sk_buff递交给上层,各层在sk_buff中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网络媒体上接收数据包后,它必须将接收到的数据转换为sk_buff数据结构并传递给上层,各层剥去相应的协议头直至交给用户。
1 struct sk_buff { 2 struct sk_buff *next; // sk_buff是双向链表,所以有前去后继,这是指向后面的sk_buff结构体指针 3 struct sk_buff *prev; // 这是指向前一个sk_buff结构体指针 4 ... 5 unsigned int len, // 表示数据区的长度(tail-data)与分片结构体数据区的长度之和。 6 data_len; // 只表示分片结构体数据区的长度,所以len=(tail - data) + data_len; 7 __u16 mac_len, // mac报头的长度 8 hdr_len; // 用于clone时,表示clone的skb的头长度 9 ...10 __u32 priority; // 优先级,主要用于QOS11 ...12 __be16 protocol; // 包的协议类型,标识是IP包还是ARP包还是其他数据包13 14 ...15 16 __be16 inner_protocol;17 __u16 inner_transport_header; 18 __u16 inner_network_header; 19 __u16 inner_mac_header; 20 __u16 transport_header; // 指向传输包头21 __u16 network_header; // 指向传输层包头22 __u16 mac_header; // 指向链路层包头23 /* These elements must be at the end, see alloc_skb() for details. */24 sk_buff_data_t tail;25 sk_buff_data_t end; // 数据缓冲区的结束地址26 unsigned char *head, // 数据缓冲区的开始地址27 *data; // 28 ...29 };
【温馨提示】head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部。每一层会在head和data之间填充协议头,或者在tail和end之间添加协议数据。
下面分析套接字缓冲区涉及的操作函数,Linux套接字缓冲区支持分配、释放、变更等功能函数。
(1)分配:
Linux内核中用于分配套接字缓冲区的函数有:
函数原形 |
struct sk_buff *alloc_skb(unsigned int len, gfp_t priority); struct sk_buff *dev_alloc_skb(unsigned len); |
函数参数 |
len:为数据缓冲区的空间大小,通常以L1_CACHE_BYTES字节(对于ARM为32)对齐 |
priority:为内存分配的优先级 |
|
返回值 |
成功:返回分配好的sk_buff指针;失败:返回NULL |
【温馨提示】dev_alloc_skb()函数以GFP_ATOMIC优先级进行skb的分配。
(2)释放:
Linux内核内部用于释放套接字缓冲区的函数有:
函数原形 |
void kfree_skb(struct sk_buff *skb); void dev_kfree_skb(struct sk_buff *skb); void dev_kfree_skb_irq(struct sk_buff *skb); void dev_kfree_skb_any(struct sk_buff, *skb); |
函数参数 |
sk_buff:套接字缓冲区 |
Linux内核内部使用kree_skb()函数,而在网络设备驱动程序中则最好用dev_kfree_skb()、dev_kfree_skb_irq()或dev_kfree_skb_any()函数进行套接字缓冲区的释放。其中,dev_kfree_skb()函数用于非中断上下文,dev_kfree_skb_irq()函数用于中断上下文,而dev_kfree_skb_any()函数在中断和非中断上下文都可采用。
(3)变更
在Linux内核中可以用如下函数在缓冲区尾部增加数据:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
它会导致skb->tail后移len(skb->tail += len),而skb->len会增加len的大小(skb->len += len)。通常,在设备驱动的接收数据处理中会调用此函数。
在Linux内核中可以用以下函数在缓冲区开头增加数据:
unsigned char *skb_push(struct sk_buff *skb, unsigned int len);
它会导致skb->data前移len(skb->data -= len),而skb->len会增加len的大小(skb->len += len)。
对于一个空的缓冲区而言,调用如下函数可以调整缓冲区的头部:
static inline void skb_reserve(struct sk_buff *skb, int len);
它会将skb->data和skb->tail同时后移len,执行skb->data += len、skb->tail += len。内核里存在许多这样的代码:
skb = alloc_skb(len + headspace, GFP_KERNEL);skb_reserve(skb, headspace);skb_put(skb, len);memcpy_fromfs(skb->data, data, len);pass_to_m_protocol(skb);
上述代码先分配一个全新的sk_buff,接着调用skb_reserve()腾出头部空间,之后调用skb_put()腾出数据空间,然后把数据复制进来,最后把sk_buff传给协议栈。
2 网络设备接口层
网络设备接口层的主要功能是为千变万化的网络设备定义统一、抽象的数据结构net_device结构体,以不变应万变,实现多种硬件在软件层次上的统一。
net_device结构体在内核中指代一个网络设备,它定义在include/linux/netdevice.h文件中,网络设备驱动程序只需通过填充net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。
(1)全局信息
char name[IFNAMESIZ]; // name是网络设备名
(2)硬件信息
unsigned long mem_end; // 设备使用的共享内存的结束地址unsigned long mem_start; // 设备使用的共享内存的起始地址unsigned long base_addr; // base_addr为网络设备I/O基地址unsigned char irq; // irq为设备使用的中断号unsigned char if_port; // 指定多端口设备使用哪一个端口,该字段仅针对多端口设备。例如,如果设备同时支持IF_PORT_10BASE2(同轴电缆)和IF_PORT_10BASET(双绞线),则可使用该字段unsigned char dma; // dma指定分配给设备的DMA通道
(3)接口信息
unsigned short hard_header_len; // 网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋值为ETH_HLEN,即14unsigned short type; // 接口的硬件类型unsigned mtu; // 最大传输单元(MTU)unsigned char *dev_addr; // 用于存放设备的硬件地址,驱动可能提供了设置MAC地址的接口,这会导致用户设置的MAC地址等存入该成员
dev_addr范例使用代码:
1 static int moxart_set_mac_address(struct net_device *ndev, void *addr) { 2 struct sockaddr *address = addr; 3 4 if (!is_valid_ether_addr(address->sa_data)) { 5 return -EADDRNOTAVAIL; 6 } 7 memcpy(ndev->dev_addr, address->sa_data, ndev->addr_len); 8 moxart_update_mac_address(ndev); 9 10 return 0;11 }
接口信息继续:
unsigned short flags; // 网络接口标志
网络接口标志以IFF_开头,部分标志由内核来管理,其他的在接口初始化时被设置以说明设备接口的能力和特性。接口标志包括:
IFF_UP(当设备被激活并可以开始发送数据包时,内核设置该标志)IFF_AUTOMEDIA(设备可在多种媒介间切换)IFF_BROADCAST(允许广播)IFF_DEBUG(调试模式,可用于控制prink调用的详细程度)IFF_LOOPBACK(回环)IFF_MULTICAST(允许组播)IFF_NOARP(接口不能执行ARP)IFF_POINTOPOINT(接口连接到点到点链路)
(4)设备操作函数
const struct net_device_ops *netdev_ops;
具体内容为:
1 struct net_device_ops { 2 int (*ndo_init)(struct net_device *dev); 3 void (*ndo_uninit)(struct net_device *dev); 4 int (*ndo_open)(struct net_device *dev); 5 int (*ndo_stop)(struct net_device *dev); 6 netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb, 7 struct net_device *dev); 8 u16 (*ndo_select_queue)(struct net_device *dev, 9 struct sk_buff *skb,10 void *accel_priv,11 select_queue_fallback_t fallback);12 void (*ndo_change_rx_flags)(struct net_device *dev,13 int flags);14 void (*ndo_set_rx_mode)(struct net_device *dev);15 int (*ndo_set_mac_address)(struct net_device *dev,16 void *addr);17 int (*ndo_validate_addr)(struct net_device *dev);18 int (*ndo_do_ioctl)(struct net_device *dev,19 struct ifreq *ifr, int cmd);20 ...21 };
ndo_open()函数的作用是打开网络接口设备,获得设备需要的I/O地址、IRQ、DMA通道等。stop()函数的作用是停止网络接口设备,与open()函数的作用相反。
int (*ndo_start_xmit) (struct sk_buff *skb, struct net_device *dev);
ndo_start_xmit()函数会启动数据包的发送,当系统调用驱动程序的xmit函数时,需要向其传入一个sk_buff结构体指针,以使得驱动程序能获取从上层传递下来的数据包。
void (*ndo_tx_timeout) (struct net_device *dev);
当数据包的发送超时时,ndo_tx_timeout()函数会被调用,该函数需采取重新启动数据包发送过程或重新启动硬件等措施来恢复网络设备到正常状态。
struct net_device_status* (*ndo_get_stats)(struct net_device *dev);
ndo_get_status()函数用于获得网络设备的状态信息,它返回一个net_device_stats结构体指针。net_device_stats结构体保存了详细的网络设备流量统计信息,如发送和接收的数据包数、字节数等。
int (*ndo_do_ioctl) (struct net_device *dev, struct ifreq *ifr, int cmd);int (*ndo_set_config) (struct net_device *dev, struct ifmap *map);int (*ndo_set_mac_address) (struct net_device *dev, void *adddr);
ndo_do_ioctl()函数用于进行设备特定的I/O控制。
ndo_set_config()函数用于配置接口,也可用于改变设备的I/O地址和中断号。
ndo_set_mac_address()函数用于设置设备的MAC地址。
除了netdev_ops以外,在net_device中还存在类似于ethool_ops、header_ops这样的操作集:
const struct ethtool_ops *ethool_ops;const struct header_ops *header_ops;
ethool_ops成员函数与用户空间ethool工具的各个命令选项对应,ethool提供了网卡及网卡驱动管理能力,能够为Linux网络开发人员和管理人员提供对网卡硬件、驱动程序和网络协议栈的设置、查看以及调试等功能。
header_ops对应于硬件头部操作,主要是完成创建硬件头部和从给定的sk_buff分析出硬件头部等操作。
(5)辅助成员
unsigned long trans_start;unsigned long last_rx;
trans_start记录最后的数据包开始发送时的时间戳,last_rx记录最后一次接收到数据包时的时间戳,这两个时间戳记录的都是jiffies,驱动程序应维护这两个成员。
通常情况下,网络设备驱动以中断方式接收数据包,而poll_controller()则采用纯轮询方式,另外一种数据接收方式是NAPI(New API),其数据接收流程为“接收中断来临->关闭接收中断->以轮询方式接收所有数据包直到收空->开启接收中断->接收中断来临……”,内核提供了如下与NAPI相关的API:
void netif_napi_add(struct net_device *dev, struct napi_struct *napi, int (*poll)(struct napi_struct *, int), int weight);void netif_napi_del(struct napi_struct *napi);
以上两个函数分别用于初始化和移除一个NAPI,netif_napi_add()的poll参数是NAPI要调度执行的轮询函数。
static inline void napi_enable(struct napi_struct *n);static inline void napi_disable(struct napi_struct *n);
以上两个函数分别用于使能和禁止NAPI调度。
该函数用于检查NAPI是否可以调度,而napi_schedule()函数用于调度轮询实例的运行。
其原形为:
static inline void napi_schedule(struct napi_struct *n);
在NAPI处理完成的时候应该调用:
void napi_complete(struct napi_struct *n);
3 设备驱动功能层
net_device结构体的成员(属性和net_device_ops结构体中的函数指针)需要被设备驱动功能层赋予具体的数值和函数。对于具体的设备xxx,工程师应该编写相应的设备驱动功能层的函数,这些函数形如xxx_open()、xxx_stop()、xxx_tx()、xxx_hard_header()、xxx_get_stats()和xxx_tx_timeout()等。
由于网络数据包的接收可由中断引发,设备驱动功能层的另一个主体部分将是中断处理函数,它负责读取硬件上接收到的数据包并传送给上层协议,因此可能包含xxx_interrupt()和xxx_rx()函数,前者完成中断类型判断等基本工作,后者则需完成数据包的生成及将其递交给上层等复杂工作。
对于特定的设备,我们还可以定义相关的私有数据和操作,并封装为一个私有信息结构体xxx_private,让其指针赋值给net_device的私有成员。在xxx_private结构体中可包含设备的特殊属性和操作、自旋锁与信号量、定时器以及统计信息等,这都由工程师自定义。在驱动中,要用到私有数据的时候,则使用在netdevice.h中定义的接口:
static inline void *netdev_priv(const struct net_device *dev);
比如在驱动drivers/net/ethernet/davicom/dm9000.c的dm9000_probe()函数中,使用alloc_etherdev(sizeof(struct board_info))分配网络设备,board_info结构体就成了这个网络设备的私有数据,在其他函数中可以简单地提取这个私有数据。例如:
1 static int dm9000_start_xmit(struct sk_buff *skb, struct net_device *dev) {2 unsigned long flags;3 board_info_t *db = netdev_priv(dev);4 ...5 }