Linux网络设备驱动-以太网驱动分析
1.概述网络上数据包的发送和接收由网络设备完成。网络设备与字符设备、块设备不同,其并不对应于/dev目录下的文件,也不能使用常规的操作方法操作网络设备。现在比较通用的做法是通过套接字访问网络设备。网络设备的驱动程序管理网络设备,如地址设置、修改传输参数及维护流量和错误统计,同时也驱动网络设备进行数据的收发。因此,分析网络设备驱动程序,对理解网路设备的工作机制有很大的帮助。Linux内核将网络设备驱
1.概述
网络上数据包的发送和接收由网络设备完成。网络设备与字符设备、块设备不同,其并不对应于/dev目录下的文件,也不能使用常规的操作方法操作网络设备。现在比较通用的做法是通过套接字访问网络设备。网络设备的驱动程序管理网络设备,如地址设置、修改传输参数及维护流量和错误统计,同时也驱动网络设备进行数据的收发。因此,分析网络设备驱动程序,对理解网路设备的工作机制有很大的帮助。
Linux内核将网络设备驱动程序划分为4个层次,分别为网络协议接口层、网路设备接口层、提供实际功能的设备驱动功能层、网络设备与媒介层。
(1)网络协议接口层
网络协议接口层向网络协议栈提供统一的数据包收发接口,不论上层是ARP协议还是IP协议,发送数据通过dev_queue_xmit等接口,接收数据通过netif_rx等接口。该层的实现独立于具体的网络协议和网络设备,使网络协议和网络设备之间解耦。
(2)网路设备接口层
网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
(3)提供实际功能的设备驱动功能层
设备驱动功能层的各函数是网络设备结构层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序。
(4)网络设备与媒介层
网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的程序在物理上驱动。对于Linux而言,网络设备和媒介都是可以虚拟的。
在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数,并填充net_device数据结构,然后将其注册到内核中。
2.Linux网络设备驱动程序层次结构
2.1.网络协议接口层
网络协议接口层主要的功能是给上层协议提供透明的数据包发送和接收接口。dev_queue_xmit
用于发送数据,netif_rx
和netif_receive_skb
用于接收数据。
int dev_queue_xmit(struct sk_buff *skb)
int netif_rx(struct sk_buff *skb)
int netif_receive_skb(struct sk_buff *skb)
发送和接收都用到了struct sk_buff
结构体,含义为套接字缓冲区,用于在网络子系统中的各层之间传递数据。当发送数据包时,内核的网络处理模块必须创建一个struct sk_buff
结构体,将要发送的数据信息填充到此结构体当中,然后传递到下层,每层都可以添加协议数据,直至被网路设备发送出去。当网络设备接收到数据时,会创建一个struct sk_buff
结构体,将接收到的数据信息填充到此结构体当中,然后传递到上层,每层剥离相应的协议数据直至交给用户。
2.1.1.核心数据结构
[include/linux/skbuff.h]
#if BITS_PER_LONG > 32
#define NET_SKBUFF_DATA_USES_OFFSET 1
#endif
#ifdef NET_SKBUFF_DATA_USES_OFFSET
typedef unsigned int sk_buff_data_t;
#else
typedef unsigned char *sk_buff_data_t;
#endif
struct sk_buff {
union {
struct {
struct sk_buff *next; // 双向链表的前一个sk_buff节点
struct sk_buff *prev; // 双向链表的后一个sk_buff节点
union {
// 数据到达的时间
ktime_t tstamp;
struct skb_mstamp skb_mstamp;
};
};
// 在netem和tcp stack中使用红黑树,而不是双向链表
struct rb_node rbnode; /* used in netem & tcp stack */
};
// 发送给本机的报文时,sk指向拥有sk_buff的套接字,否则为NULL
struct sock *sk; // 属于那个socket
struct net_device *dev; // 从哪个网络设备上发送或接收
// 控制缓存,给每层使用,可以将私有变量放在此处,如果要跨越不同层,
// 则需要调用skb_clone
char cb[48] __aligned(8);
unsigned int len; // 真实的数据长度
unsigned int data_len; // 数据长度
__u16 mac_len; // 链路层报文长度
__u16 hdr_len; // writable header length of cloned skb
__u32 priority; // 数据包队列的优先级
#if defined(CONFIG_NET_RX_BUSY_POLL) || defined(CONFIG_XPS)
union {
unsigned int napi_id; // 此skb来自NAPI结构体的ID
unsigned int sender_cpu;
};
#endif
// 驱动接收的数据包的网络协议,封装在MAC头的报文协议
__be16 protocol;
__u16 transport_header; // 传输层报文头
__u16 network_header; // 网络层报文头
__u16 mac_header; // 链路层报文头
// 在32位系统上sk_buff_data_t为unsigned char*类型,
// 在64位系统上sk_buff_data_t为unsigned char类型
sk_buff_data_t tail; // 指向缓冲区数据的结尾
sk_buff_data_t end; // 指向缓冲区的结尾
unsigned char *head; // 指向缓冲区的头部
unsigned char *data; // 指向缓冲区数据的头部
//该缓冲区分配的所有总的内存,包括:skb_buff + 所有数据大小
unsigned int truesize;
/*
这是一个引用计数,用于计算有多少实体引用了这个sk_buff缓冲区。
它的主要用途是防止释放sk_buff后,还有其他实体引用这个sk_buff。
因此,每个引用这个缓冲区的实体都必须在适当的时候增加或减小这个变量。
这个计数器只保护sk_buff结构本身。使用函数skb_get增大引用计数,
使用kfree_skb减小引用计数。当释放sk_buff时,若users不为零,
则递减引用计数;当users为1时,说明只有当前程序引用sk_buff,
则释放内存空间。*/
atomic_t users;
};
使用skb_shared_info
保存IP分片数据。
struct skb_shared_info {
// 分页段数目,即frags数组元素的个数
unsigned char nr_frags;
__u8 tx_flags;
unsigned short gso_size;
unsigned short gso_segs;
unsigned short gso_type;
//
struct sk_buff *frag_list;
// 硬件时间戳
struct skb_shared_hwtstamps hwtstamps;
u32 tskey;
__be32 ip6_frag_id;
atomic_t dataref; // 对象被引用的次数
void * destructor_arg;
skb_frag_t frags[MAX_SKB_FRAGS];
};
2.1.2.操作函数
(1)分配
struct sk_buff
结构体需要使用alloc_skb
或dev_alloc_skb
进行动态分配。这3个函数的作用都一样,内部都调用了__alloc_skb
函数,只是传递的参数不一样。建议使用netdev_alloc_skb
。struct sk_buff
使用了slab系统分配内存,可提高性能。分配skbuff时,需要分配两块内存,一块是skbuff本身占用的内存,使用kmem_cache_alloc_node
分配。一块是skbuff管理的数据的内存,使用kmalloc_node_track_caller
分配,分配的内存大小由size
或length
决定。在数据内存的末尾,还会分配一个struct skb_shared_info
结构体。skbuff的head指针指向数据内存的开始地址,end指向数据内存的结束地址。即数据内存的总长度为size/length + sizeof(struct skb_shared_info)。
[include/linux/skbuff.h]
// size-分配的内存大小,priority-分配内存的标志,取值为GFP_KERNEL、
// GFP_ATOMIC、__GFP_HIGHMEM或__GFP_HIGH,中断中必须使用GFP_ATOMIC,
// alloc_skb不会预留数据头部
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
// 内部会传入GFP_ATOMIC标记,可直接在中断中使用,网络设备结构体指针为NULL
// dev_alloc_skb内部调用了netdev_alloc_skb,只是第一个参数为NULL
static inline struct sk_buff *dev_alloc_skb(unsigned int length)
// 传入了网络设备结构体,以GFP_ATOMIC调用__netdev_alloc_skb,
// netdev_alloc_skb内部分配数据缓冲区时,会多分配NET_SKB_PAD字节,
// 用于留出数据头部,最后会调用skb_reserve留出数据头部,数据头部的大小
// 为NET_SKB_PAD字节
#define NET_SKB_PAD max(32, L1_CACHE_BYTES)
static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev, unsigned int length)
(2)释放
下面的4个函数用于释放已经分配的struct sk_buff
结构体内存和其管理的数据的缓冲区。kfree_skb
和dev_kfree_skb
不能在硬件中断上下文中或关闭硬件中断的上下文中调用,非中断环境建议使用dev_kfree_skb
。dev_kfree_skb_irq
在中断环境中使用,dev_kfree_skb_any
可在中断环境中使用,也可在非中断环境中使用。
[include/linux/skbuff.h]
void kfree_skb(struct sk_buff *skb);
#define dev_kfree_skb(a) consume_skb(a)
[include/linux/netdevice.h]
static inline void dev_kfree_skb_irq(struct sk_buff *skb)
static inline void dev_kfree_skb_any(struct sk_buff *skb)
static inline void dev_consume_skb_any(struct sk_buff *skb)
(3)变更
sk_buff管理的数据的缓冲区,有时候需要增大或减小。Linux内核提供了管理数据缓冲区的函数。skb_put
通过移动缓冲区的尾指针来增大数据缓冲区,首先移动尾指针tail(skb->tail += len),然后更新数据缓冲区的长度(skb->len += len),通常,在设备驱动的接收数据处理函数中会调用此函数。skb_push
通过移动缓冲区头指针来增大数据缓冲区。首先移动头指针(skb->data -= len),然后更新数据缓冲区的长度(skb->len += len)。skb_pull
与skb_push
的作用相反,通过移动缓冲区的头指针减少缓冲区的大小,首选更新数据缓冲区长度(skb->len -= len),然后移动头指针(skb->data += len)。对于一个空的数据缓冲区,skb_reserve
函数可以调整数据缓冲区预留的头部大小,大小由参数len
指定,将数据头指针和数据尾指针后移(skb->data += len,skb->tail += len)。
unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
unsigned char *skb_push(struct sk_buff *skb, unsigned int len);
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len);
static inline void skb_reserve(struct sk_buff *skb, int len);
2.2.网路设备接口层
2.2.1.核心数据结构
网络设备接口层的主要功能是为千变万化的网络设备定义统一、抽象的数据结构struct net_device
,以实现多种硬件在软件层次上的统一。网络设备驱动程序只需要设置net_device
并注册,即可实现网络通信的功能。
[include/linux/netdevice.h]
struct net_device {
char name[IFNAMSIZ]; // 网络设备的名称
struct hlist_node name_hlist;
char *ifalias;
unsigned long mem_end; // 共享内存的起始地址
unsigned long mem_start; // 共享内存的结束地址
unsigned long base_addr; // I/O基地址
struct list_head dev_list;
// 该网络设备采用NAPI时,将NAPI结构体挂入到此链表
struct list_head napi_list;
// 网络设备一系列硬件操作函数,包含常见的open、stop函数
const struct net_device_ops *netdev_ops;
// 用户空间ethtool工具的底层实现,提供了网络设备及网卡驱动管理能力
const struct ethtool_ops *ethtool_ops;
const struct header_ops *header_ops;
unsigned int flags; // 网络接口标志,以IFF_开头,
int irq; // 中断号
unsigned char dma; // 分配设备的DMA通道
unsigned int mtu; // 最大传输单元
unsigned short type; // 接口的硬件类型
// 网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被设置成ETH_HLEN,即14
unsigned short hard_header_len;
unsigned long last_rx; // 最后收到数据包的时间(jiffies)
unsigned char *dev_addr; // 硬件地址
unsigned long trans_start; // 发送数据包的时间(jiffies)
......
};
struct net_device_ops {
// 打开网络设备,获得设备需要的I/O地址、IRQ、DMA通道等
int (*ndo_open)(struct net_device *dev);
// stop停止设备与open作用相反
int (*ndo_stop)(struct net_device *dev);
// 启动数据数据包发送,需要传入一个sk_buff结构体
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,struct net_device *dev);
// 设置设备的mac地址
int (*ndo_set_mac_address)(struct net_device *dev,void *addr);
// ioctl的底层实现函数
int (*ndo_do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);
// 改变网路设备的MTU值
int (*ndo_change_mtu)(struct net_device *dev,int new_mtu);
// 发送超时时调用,重新发送数据包或者重新启动硬件措施来恢复网络设备
void (*ndo_tx_timeout) (struct net_device *dev);
// 获取网络设备状态,net_device_stats中保存了网络设备流量统计信息
struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);
#ifdef CONFIG_NET_POLL_CONTROLLER // 轮训方法
void (*ndo_poll_controller)(struct net_device *dev);
int (*ndo_netpoll_setup)(struct net_device *dev,struct netpoll_info *info);
void (*ndo_netpoll_cleanup)(struct net_device *dev);
#endif
#ifdef CONFIG_NET_RX_BUSY_POLL
int (*ndo_busy_poll)(struct napi_struct *dev);
#endif
}
struct net_device
结构体使用下面三个接口分配,alloc_netdev_mqs
是最基本的分配函数,alloc_netdev
和alloc_netdev_mq
是两个宏,对alloc_netdev_mqs
进行了封装。net_device
中可以保存私有数据,在分配的时候只需要指定私有数据的大小即可。使用free_netdev
释放分配的net_device
结构体。
[include/linux/netdevice.h]
// sizeof_priv-私有数据大小,name-名称格式化字符串,
// name_assign_type-名称对其类型,setup-初始化设备的回调函数,
// txqs-发送队列大小,rxqs-接收队列大小
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs);
// 宏定义,对alloc_netdev_mqs的封装
#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \
alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, 1, 1)
// 宏定义,对alloc_netdev_mqs的封装
#define alloc_netdev_mq(sizeof_priv, name, name_assign_type, setup, count) \
alloc_netdev_mqs(sizeof_priv, name, name_assign_type, setup, count,\
count)
[net/ethernet/eth.c]
// 宏定义,对alloc_netdev_mqs的封装
struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs,
unsigned int rxqs)
{
// "eth%d"-网络设备名称字符串格式化函数
// NET_NAME_UNKNOWN-网络设备名称对其类型
// ether_setup-网络设备初始化函数
return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_UNKNOWN,
ether_setup, txqs, rxqs);
}
// 释放分配的net_device结构体
void free_netdev(struct net_device *dev)
// 获取私有空间指针
static inline void *netdev_priv(const struct net_device *dev)
2.2.2.NAPI数据结构
通常情况下,网络设备驱动以中断方式接收数据包,但当网络流量较大时,会频繁产生中断,导致系统的的性能降低。使用轮训能改善大流量情况下的系统性能,但在小流量时,大部分轮训都得不到数据包,导致系统的资源利用率降低。从Linux内核2.6版本起,引入了NAPI(New API),大流量时采用轮训的方式接收数据包,小流量时,采用中断的方式接收数据包。NAPI综合了中断和轮训的优势。NAPI数据包的循环流程为:数据接收中断发生->关闭接收中断->以轮训方式接收所有数据包或轮训权重耗尽->开启接收中断->数据接收中断发生…。NAPI的数据结构为struct napi_struct
,内核也提供了一系列NAPI的函数,如初始化、使能、禁止、调度、完成NAPI的函数。
[include/linux/netdevice.h]
enum { // napi_struct的state成员取值
NAPI_STATE_SCHED, /* 已被调度,调用netif_napi_add后设置此标志 */
NAPI_STATE_DISABLE, /* 不能被scheduled */
NAPI_STATE_NPSVC, /* Netpoll - don't dequeue from poll_list */
NAPI_STATE_HASHED, /* In NAPI hash (busy polling possible) */
NAPI_STATE_NO_BUSY_POLL,/* Do not add in napi_hash, no busy polling */
};
struct napi_struct {
// 轮训设备链表,可挂入到softnet_data的poll_list链表中
struct list_head poll_list;
// NAPI的状态位,取值为上面的枚举类型
unsigned long state;
int weight; // NAPI权重
unsigned int gro_count;
// 轮训时调用的函数
int (*poll)(struct napi_struct *, int);
#ifdef CONFIG_NETPOLL
spinlock_t poll_lock;
int poll_owner;
#endif
// 网络设备结构体指针
struct net_device *dev;
struct sk_buff *gro_list;
struct sk_buff *skb;
struct hrtimer timer;
// 挂入到net_device的napi_list链表
struct list_head dev_list;
// 挂入到napi_hash哈希表
struct hlist_node napi_hash_node;
// NAPI的ID,大于NR_CPUS + 1
unsigned int napi_id;
};
// 初始化NAPI,dev-网络设备结构体指针,napi-NAPI结构体指针,poll-轮训函数,
// weight-轮训的权重,在初始化NAPI时设置,一般设置为64
// 内部会设置NAPI_STATE_SCHED标志,初始化完后NAPI还不能被调度,
// 需要使能才能调度
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *, int), int weight);
// 删除NAPI
void netif_napi_del(struct napi_struct *napi);
// 使能NAPI,使能后可以被调度,清除NAPI_STATE_SCHED和NAPI_STATE_NPSVC标志
static inline void napi_enable(struct napi_struct *n)
// 禁止NAPI,禁止后不能被调度,设置NAPI_STATE_SCHED和NAPI_STATE_NPSVC标志
void napi_disable(struct napi_struct *n);
// 检查NAPI是否可以被调度,若设置了NAPI_STATE_DISABLE或
// NAPI_STATE_SCHED标志,则不可调度,反之可以调度并设置
// NAPI_STATE_SCHED标志
static inline bool napi_schedule_prep(struct napi_struct *n)
// 调度NAPi,把n挂入softnet_data的poll_list链表,
// 然后触发NET_RX_SOFTIRQ软中断
static inline void napi_schedule(struct napi_struct *n)
// NAPI轮训完成,将n从softnet_data的poll_list链表中移除,
// 并清除NAPI_STATE_SCHED标志
static inline void napi_complete(struct napi_struct *n)
网络设备采用NAPi的方式接收数据包时,需要使用软中断进行轮训。当NAPI被调度时,napi_struct
会被挂到softnet_data
的poll_list
链表中,网络数据包收发软中断被触发后,会遍历poll_list
链表轮训所有的网络设备。struct softnet_data
是一个percpu变量,在net/core/dev.c文件中定义并初始化,同时注册了网络数据包收发的软中断处理函数。发送数据包的软中断处理函数为net_tx_action
,接收数据包的软中断处理函数为net_rx_action
。
[include/linux/netdevice.h]
struct softnet_data {
// 网络设备轮询队列。使用NAPi时驱动应该将需要轮询的网络设备的napi_struct,
// 挂入该链表中,不使用NAPI时,会将backlog的poll_list挂入到此链表,
// 网络收包软中断会遍历该队列,调用驱动提供的netpoll接收数据
struct list_head poll_list;
struct sk_buff_head process_queue;
unsigned int processed;
unsigned int time_squeeze;
unsigned int cpu_collision;
unsigned int received_rps;
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
struct sk_buff *completion_queue;
unsigned int dropped;
// 对于非NAPI方式的接收,驱动通过轮询或者硬中断的方式将数据包放入该队列,
// 然后激活软中断处理该队列中数据包,最后基于流量控制的排队规则将数据包
// 递交给上层
struct sk_buff_head input_pkt_queue;
// 为了将软中断接收处理程序对非NAPI方式和NAPIF方式的处理统一,对于非NAPI接收,
// 在硬中断处理后,将backlog结构加入到poll_list,然后触发软中断接收程序,
// backlog的轮训函数为process_backlog
struct napi_struct backlog;
};
[net/core/dev.c]
// 注册网络设备发送数据包的软中断,软中断的优先级为NET_TX_SOFTIRQ,
// 处理函数为net_tx_action
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
// 注册网络设备接收数据包的软中断,软中断的优先级为NET_RX_SOFTIRQ,
// 处理函数为net_rx_action
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
2.3.网路设备驱动功能层
设备驱动层的功能主要体现在net_device
结构体中的net_device_ops
操作函数集合。驱动工程师应该实现ndo_open
、ndo_stop
、ndo_do_ioctl
、ndo_start_xmit
、ndo_set_rx_mode
等函数,同时也应该实现网络设备的中断处理功能等。
3.网络设备驱动程序的注册与注销
网络设备驱动的注册与注销由register_netdev
和unregister_netdev
函数完成。这两个函数都接收一个net_device
结构体指针作为参数。在驱动初始化的时候使用register_netdev
注册,在驱动卸载的时候调用unregister_netdev
移除注册的net_device
。
int register_netdev(struct net_device *dev)
void unregister_netdev(struct net_device *dev)
4.网络设备的初始化
网络设备的初始化工作主要完成以下几个工作:
(1)进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备使用的硬件资源。
(2)进行软件接口上的准备工作,分配net_device
结构体并对其初始化。
(3)获取设备的私有信息指针并初始化各成员的值,如果私有信息中包括自旋锁或信号量等并发同步机制,则需要对其进行初始化。
5.网络设备的打开与释放
网络设备的打开函数需要完成如下工作:
(1)使能设备使用的硬件资源,申请I/O区域、中断和DMA通道等。
(2)调用Linux内核提供的netif_start_queue
函数,激活设备发送队列。
网络设备的关闭函数需要完成如下工作:
(1)调用Linux内核提供的netif_stop_queue
函数,关闭设备发送队列。
(2)释放设备所使用的I/O区域、中断和DMA资源。
[include/linux/netdevice.h]
// open时激活发送队列,上层会向下传输数据包
static inline void netif_start_queue(struct net_device *dev)
// close或需要停止发送(发送队列满或驱动来不及发送数据)时
static inline void netif_stop_queue(struct net_device *dev)
// 关闭发送队列后,若需要再次激活发送队列,则调用此函数
static inline void netif_wake_queue(struct net_device *dev)
6.数据发送流程
协议栈要发送数据的时候,调用驱动注册的ndo_start_xmit
函数进行发送。驱动通过sk_buff
获取数据的长度和有效数据,将数据复制到驱动的缓冲区,对于以太网数据包,若长度小于规定的最小长度ETH_ZLEN,则需要给末尾补零,最后驱动硬件啊,将数据包发送出去。当数据传输超时时,意味着当前的发送操作失败或硬件已陷入未知状态,此时,数据包发送超时处理函数ndo_tx_timeout
将被调用。
7.数据接收流程
当网络设备接收到数据包后,触发中断,然后在中断中判断是否是接收中断,若是接收中断,则读取接收到的数据,分配sk_buff
数据结构,将接收到的数据复制到数据缓冲区,并调用netif_rx
函数将sk_buff
传递给上层协议。如果是NAPI兼容的设备驱动,则可以通过轮询的方式接收数据包。在这种情况下,我们需要为该设备驱动提供作为netif_napi_add
参数的xxx_poll函数。xxx_poll函数的模板如下:
static int xxx_poll(struct napi_struct *napi, int budget)
{
......
while (npkt < budget && priv->rx_queue) {
// 从接收队列中取出数据包
pkt = xxx_dequeue_buf(dev);
skb = netdev_alloc_skb(dev, pkt->datalen + 2);
......
skb_reserve(skb, 2);
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
skb->protocol=eth_type_trans(skb, dev);
// NAPI方式接收数据需要使用netif_recvive_skb,而不是netif_rx
netif_recvive_skb(skb);
// 更新统计数据
......
}
if (npkt < budget) {
// napi完成
napi_complete(napi);
// 重新打开接收中断
xxx_enable_rx_int(...);
}
return npkt;
}
上述代码中的budget是在初始化阶段分配的wright,xxx_poll函数每次最多接收budget个数据包。循环读取设备接收缓冲区中的数据,每读取到一包,分配sk_buff
并拷贝数据,然后调用netif_recvive_skb
将数据包上送到协议栈。当循环读取的数据达到budget或网络设备接收缓冲区为空时,停止接收数据。当轮询读取的数据包小于budget,说明网络流量较小,此时应该调用napi_complete
取消NAPI轮询,打开接收中断。下一次接收中断到来时会再次调度NAPI同时关闭接收中断。NAPI兼容驱动,首次接收数据包采用的是中断,后续数据包的接收采用的是轮询。NAPI兼容驱动的中断处理函数模板如下。napi_schedule
在网络设备的中断处理函数中调用,其将napi结构体挂到软中断的poll_list
链表中,然后触发NET_RX_SOFTIRQ
软中断,软中断最终调用net_rx_action
接收数据包。
static irqreturn_t xxx_interrupt(int irq, void* dev_id)
{
switch (status & ISQ_EVENT_MASK) {
case ISQ_RECEIVER_EVENT:
xxx_disable_rx_int(); // 禁止接收中断
napi_schedule(&priv->napi); // 调度NAPI
break;
......
}
}
NAPI用于网络报文接收的软中断处理函数为net_rx_action
。首选设置总轮询的数据包数量为300,总轮询的时间为2jiffies,然后遍历poll_list链表,调用每一个网络设备的poll函数接收数据包,并把数据包上传到协议栈。若总数据包超过了300或者时间超过了2jiffies,则软中断退出。
static void net_rx_action(struct softirq_action *h)
// 接收软中断最多执行2jiffies
unsigned long time_limit = jiffies + 2;
// 获取软中断轮询的权值,netdev_budget为全局变量,表示轮询读取
// 数据包的数量,可通过/proc/sys/net/core/netdev_budget设置
// netdev_budget的默认值为300
int budget = netdev_budget;
local_irq_disable // 禁止中断
// 将poll_list链表从softnet_data中移动到临时链表list中
list_splice_init(&sd->poll_list, &list);
local_irq_enable // 开启中断
for (;;) {
n = list_first_entry // 获取poll_list的第一个元素
// 开始轮询网络设备,每一个网络设备轮询获取的数据包数量都要从总
// 数据包中减去
budget -= napi_poll(n, &repoll);
netpoll_poll_lock // 加锁
spin_lock(&napi->poll_lock);
weight = n->weight; // 获取网络设备对应的轮询权值
// 调用网卡驱动提供的轮询函数,n为napi_struct结构体指针,
// weight为该网络设备对应的权值,如果是NAPI,则调用底层驱动
// 的poll函数,如果是非NAPI,则调用process_backlog
work = n->poll(n, weight);
// 将轮询完的napi_struct挂到repoll链表中
list_add_tail(&n->poll_list, repoll);
netpoll_poll_unlock(have); // 解锁
spin_lock(&napi->poll_lock);
// 如果消耗完了总轮询数据包数量或执行时间超过了2jiffies,则退出
if (unlikely(budget <= 0 ||
time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++;
break;
}
}
NAPI的工作流程如下图所示。
8.网络连接状态
网络适配器硬件电路可以检测网络的链接状态。网路设备驱动可以通过netif_carrier_on
和netif_carrier_off
函数改变设备的连接状态,如果驱动检测到连接状态发生变化,也应该调用netif_carrier_on
和netif_carrier_off
函数显示的通知内核。除了netif_carrier_on
和netif_carrier_off
函数外,netif_carrier_ok
可用于向调用者返回链路上的连接状态是否正常,主要用于查询网络连接状态。
[include/linux/netdevice.h]
void netif_carrier_on(struct net_device *dev);
void netif_carrier_off(struct net_device *dev);
static inline bool netif_carrier_ok(const struct net_device *dev)
9.统计数据
内核提供了net_device_stats
结构体,包含了比较完整的统计信息,网络设备驱动程序负责填充此结构体。net_device_stats
结构体已经包含在了net_device
结构体当中。
[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;
unsigned long rx_dropped;
unsigned long tx_dropped;
unsigned long multicast;
unsigned long collisions;
unsigned long rx_length_errors;
unsigned long rx_over_errors;
unsigned long rx_crc_errors;
unsigned long rx_frame_errors;
unsigned long rx_fifo_errors;
unsigned long rx_missed_errors;
unsigned long tx_aborted_errors;
unsigned long tx_carrier_errors;
unsigned long tx_fifo_errors;
unsigned long tx_heartbeat_errors;
unsigned long tx_window_errors;
unsigned long rx_compressed;
unsigned long tx_compressed;
};
10.总结
Linux网络设备驱动体系结构的层次化设计实现了对上层协议接口的统一和硬件驱动对下层多样化硬件设备的可适应。驱动工程师需要完成的工作集中在设备驱动功能层,网络设备接口层net_device
结构体将千变万化的网络设备进行抽象,使得设备功能层中除数据包接收以外的主体工作都由填充net_device
的属性和函数指针完成。网络数据包的传递依赖套接字缓冲区sk_buff
,它是数据流动的载体。
参考资料
http://cxd2014.github.io/2017/10/15/linux-napi/#%E5%8F%82%E8%80%83
https://blog.csdn.net/rong_toa/article/details/109401935
《Linux设备驱动程序开发详解:基于最新的Linux内核4.0版本》
更多推荐
所有评论(0)