目录
一、基本概念
1.1 什么是网络设备?
Linux系统中的设备可以分为字符设备、块设备和网络设备三大类。网络设备又叫网络接口,与字符设备一样,网络设备也是内核的特定数据结构中注册自己(字符设备为cdev结构体, 网络设备net_device结构体)。
字符设备在 /dev 目录下会有对应设备文件节点并且在注册时会有设备号。网络设备没有对应设备节点和设备号,网络设备使用套接字来实现网络数据的接收和发送。
二、网络设备驱动架构
Linux网络设备驱动程序的体系结构如图中黄色部分所示所示,从上到下可以划分为4层,依次为网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层以及网络设备与媒介层。
2.1 网络协议接口层
最主要的功能就是给上层协议提供了透明的数据包发送和接收接口。当上层的协议需要发送数据包时,就会调用 dev_queue_xmit() 函数。当上层需要接收数据包时,就会调用netif_rx()函数。函数定义如下:
/* : dev_queue_xmit
* @description : 上层协议发送数据包时的调用接口
* @param – skb : 套接字缓冲区指针
* @return : 0 成功;其他 失败
*/
int dev_queue_xmit(struct sk_buff *skb)
/* : netif_rx
* @description : 上层协议接收数据包时的调用接口
* @param – skb : 套接字缓冲区指针
* @return : 0 成功;其他 失败
*/
int netif_rx(struct sk_buff *skb)
其中,skb表示要发送的数据,是一个sk_buff的结构体指针。sk_buff是Linux网络驱动中重要的结构体。在发送数据时,网络数据都是以sk_buff保存的,各个协议层都会在sk_buff中添加自己的协议头,最终由底层驱动将sk_buff中的数据发送出去。在接收数据时,网络底层驱动将接收到的原始数据打包成sk_buff,然后发送给上层协议,上层协议会逐步去掉对应头部,然后将最终数据发送给用户。
2.2 网络设备接口层
网络设备接口层的主要功能是为千变万化的网络设备定义了统一、 抽象的数据结构 net_device 结构体,实现多种硬件在软件层次上的统一。
Linux内核使用 net_device结构体表示一个具体的网络设备,网络驱动的核心就是初始化net_device 结构体中的各个成员变量,然后将初始化完成以后的net_device 注册到 Linux内核中。
2.3 设备驱动功能层
对应net_device结构体中的设备驱动功能函数,例如xxx_open()、xxx_stop()、xxx_tx()等。
另一部分是中断处理函数,它负责接收硬件上的数据包并传给上层协议,主要函数有xxx_interrupt()和xxx_rx(),前者完成中断类型判断及处理,后者则将从硬件获得的数据进行封包并交给上层。
2.4 网络设备与媒介层
对应实际的硬件设备,用来负责完成数据包发送和接收的物理实体, 设备驱动功能层的函数都在这物理上驱动的。
对于初学者,在设计网络设备驱动程序时,我们主要的工作就是编写设备驱动功能层的相关函数以填充net_device数据结构并将net_device注册入内核。
三、重要结构体说明
3.1 sk_buff结构体
3.1.1 sk_buff结构体定义
sk_buff结构体定义在 include/linux/skbuff.h 中,可以将其理解为指针的集合,其重要成员大致如下:
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; //指向下一个sk_buff结构体
struct sk_buff *prev; //指向前一个sk_buff结构体
... ...
unsigned int len, //数据包的总长度,包括线性数据和非线性数据
data_len, //非线性的数据长度
mac_len; //mac包头长度
__u32 priority; //该sk_buff结构体的优先级
__be16 protocol; //存放上层的协议类型,可以通过eth_type_trans()来获取
... ...
sk_buff_data_t transport_header; //传输层头部的偏移值
sk_buff_data_t network_header; //网络层头部的偏移值
sk_buff_data_t mac_header; //MAC数据链路层头部的偏移值
sk_buff_data_t tail; //指向缓冲区的数据包末尾
sk_buff_data_t end; //指向缓冲区的末尾
unsigned char *head, //指向缓冲区的协议头开始位置
*data; //指向缓冲区的数据包开始位置
... ...
}
sk_buff结构体中tail、end、head、data等字段指向的是真正的数据区。head和end指向sk_buff缓冲区的头部和尾部,data和tail指向实际数据的头部和尾部。
网络协议栈(TCP/IP)每一层(传输层、网络层、网络接口层)都会在head和data之间填充协议头,在tail和end之间添加新的数据协议。在内核中,sk_buff结构体在各层协议之间传输不是通过拷贝sk_buff结构体,而是增加/移除协议头和移动指针来操作。
3.1.2 分配sk_buff
我们想要使用sk_buff 必须先分配,通过alloc_skb 函数可进行分配,alloc_skb() 原型定义在include/linux/skbuff.h 中,如下所示:
/* : alloc_skb
* @description : 分配sk_buff时的调用接口
* @param – size : 要分配的大小,即skb数据段的大小
* @param - priority : 为GFP MASK宏,如GFP_KERNEL、GFP_ATOMIC等
* @return : sk_buff首地址 成功;NULL 失败
*/
static inline struct sk_buff *alloc_skb(unsigned int size,
gfp_t priority)
{
return __alloc_skb(size, priority, 0, NUMA_NO_NODE);
}
此外,还常常使用netdev_alloc_skb来为设备申请一个用于接收的skb_buff,也定义在include/linux/skbuff.h中,如下所示:
/* : netdev_alloc_skb
* @description : 为设备分配用于接收数据的sk_buff时的调用接口
* @param – dev : 要给哪个设备分配sk_buff
* @param - length : 要分配的大小
* @return : sk_buff首地址 成功;NULL 失败
*/
static inline struct sk_buff *netdev_alloc_skb(struct net_device *dev,
unsigned int length)
{
return __netdev_alloc_skb(dev, length, GFP_ATOMIC);
}
alloc_skb()是通用的动态分配sk_buff的函数,可以在任何上下文中调用。它可以为sk_buff分配一个新的、空的缓冲区,并返回一个指向该缓冲区的指针。
netdev_alloc_skb()是专门为网络设备驱动程序设计的sk_buff 分配函数,只能在接收数据包时的中断或软中断上下文中调用。它不仅可以为sk_buff 分配缓冲区,还会为该缓冲区预留额外的头部空间,以便设备驱动程序能够在发送数据包时填写必要的信息,例如 MAC 地址等。
如果需要在任何上下文中动态分配sk_buff,那么应该使用 alloc_skb();如果需要在网络设备驱动程序中接收数据包时动态分配sk_buff,那么应该使用netdev_alloc_skb()。
3.1.3 释放sk_buff
当使用完成以后就要释放掉 sk_buff,释放函数可以使用 kfree_skb(),函数定义在 include/linux/skbuff.c 中,如下所示:
/* : kfree_skb
* @description : 释放sk_buff时的调用接口
* @param – skb : 要释放的sk_buff
* @return : 无返回值
*/
void kfree_skb(struct sk_buff *skb)
此外,我们还可以使用dev_kfree_skb()来释放sk_buff,函数定义如下:
/* : dev_kfree_skb
* @description : 释放sk_buff时的调用接口
* @param – skb : 要释放的sk_buff
* @return : 无返回值
*/
void dev_kfree_skb(struct sk_buff *skb)
kfree_skb() 是通用的 sk_buff 释放函数,可以用于释放任何类型的sk_buff,包括网络协议栈中的、用户空间传输到内核的以及设备驱动程序中的。它会调用sk_buff的析构函数skb->destructor 来清理sk_buff相关的资源。
dev_kfree_skb()是专门为设备驱动程序设计的sk_buff 释放函数,仅可以用于释放由设备驱动程序通过 netif_receive_skb() 或者 netif_rx() 接收到的 sk_buff。它不会调用 skb->destructor 函数,因为设备驱动程序通常不需要对sk_buff 中的数据进行特殊处理。
如果需要释放网络协议栈中的 sk_buff
,或者从用户空间传输到内核的 sk_buff
,那么应该使用 kfree_skb()
;如果需要释放设备驱动程序接收到的 sk_buff
,那么应该使用 dev_kfree_skb()
。
3.1.4 变更sk_buff
(1)skb_reserve()
改函数用于调整缓冲区的头部大小,将data和tail同时向后移动n个字节,原型如下:
/* : skb_reserve
* @description : 同时后移tail和data指针的调用接口
* @param – skb : 要操作的sk_buff
* @param – len : 要增加的缓冲区头部大小
* @return : 无
*/
static inline void skb_reserve(struct sk_buff *skb, int len);
(2)skb_put()
该函数用于在尾部扩展skb_buff的数据区,将skb_buff的tail后移n个字节,使得skb_buff的len增加n个字节,原型如下:
/* : skb_push
* @description : 在尾部扩展sk_buff时的调用接口
* @param – skb : 要操作的sk_buff
* @param – len : 要增加的字节长度
* @return : 扩展出来的那一段数据区首地址 */
unsigned char *skb_push(struct sk_buff *skb, unsigned int len);
(3)skb_push()
该函数用于在头部扩展skb_buff的数据区,原型如下:
/* : skb_push
* @description : 在头部扩展skb_buff数据区
* @param – skb : 要操作的sk_buff
* @param – len : 要增加的字节数
* @return : 扩展后新数据区的首地址
*/
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
(4) skb_pull()
该函数用于从skb_buff的数据区起始位置删除数据,原型如下:
/* : skb_pull
* @description : 从skb_buff数据区起始位置删除数据
* @param – skb : 要操作的sk_buff
* @param – len : 要删除的字节数
* @return : 删除以后新的数据区首地址
*/
unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
3.1.5 sk_buff包的形成
step1: sk_buff数据区刚申请。此时head指针、data指针以及tail指针指向同一个地方。下一步就是预留协议头空间,使head、tail、data指针分离。
step2 :调用skb_reserve()来使data和tail指针向下移动,空出部分空间来为后期添加协议信息。skb_reserve()的作用是预留空间(尽可能大的预留)。因为很多头部都有可选项,我们并不清楚头部可选项有多大,因此只能按照最大分配,预留的头部空间也不一定必须用完。当我们添加协议头信息时,data指针向上移动,增加数据时tail指针向下移动。此时还没有数据存储,所以data指针和tail指针相同。
step3:存储数据。调用skb_put()来使tail指针下移空出空间添加数据,data指针和tail指针之间存放数据信息,即数据区向下扩大len个字节,并更新数据区长度len。
step4:增加头部空间的协议头。调用skb_push()使data指针向上移动,添加协议头信息(TCP层添加TCP首部, IP层添加IP首部,链路层添加链路层首部)。
通过上述分析可知:
(1)head指向缓冲区的首地址,作为上边界;
(2)end指向缓冲区尾地址,作为下边界;
(3)data指针在数据包头封装和解封装的过程中移动指向各层的协议头;
(4)skb_push函数将data指针向低地址方向移动(向上),完成协议头空间的占据;
(5)skb_pull函数将data指针向高地址方向移动(向下),完成协议头空间的解封装;
(6)tail指针在增加应用层用户缓冲数据时移动,skb_put函数将tail指针向高地址方向移动(向下),完成用户数据空间的占据。
3.2 net_device结构体
3.2.1 net_device结构体定义
net_device结构体定义在include/linux/netdevice.h 中。net_device重要成员如下所示:
struct net_device
{
char name[IFNAMSIZ]; //网卡设备名称
unsigned long mem_end; //该设备的内存结束地址
unsigned long mem_start; //该设备的内存起始地址
unsigned long base_addr; //该设备的内存I/O基地址
unsigned int irq; //该设备的中断号
unsigned char if_port; //多端口设备使用的端口类型
unsigned char dma; //该设备的DMA通道
unsigned long state; //网络设备和网络适配器的状态信息
struct net_device_stats* (*get_stats)(struct net_device *dev); //获取流量的统计信息 //运行ifconfig便会调用该成员函数,并返回一个net_device_stats结构体获取信息
struct net_device_stats stats; //用来保存统计信息的net_device_stats结构体
unsigned long features; //接口特征,
unsigned int flags; //flags指网络接口标志,以IFF_(Interface Flags)开头
//当flags =IFF_UP( 当设备被激活并可以开始发送数据包时, 内核设置该标志)、 IFF_AUTOMEDIA(设置设备可在多种媒介间切换)、IFF_BROADCAST( 允许广播)、IFF_DEBUG( 调试模式, 可用于控制printk调用的详细程度) 、 IFF_LOOPBACK( 回环)、IFF_MULTICAST( 允许组播) 、 IFF_NOARP( 接口不能执行ARP,点对点接口就不需要运行 ARP) 和IFF_POINTOPOINT( 接口连接到点到点链路) 等。
unsigned mtu; //最大传输单元,也叫最大数据包
unsigned short type; //接口的硬件类型
unsigned short hard_header_len; //硬件帧头长度,一般被赋为ETH_HLEN,即14
unsigned char dev_addr[MAX_ADDR_LEN]; //存放设备的MAC地址
unsigned long last_rx; //接收数据包的时间戳,调用netif_rx()后赋上jiffies即可
unsigned long trans_start; //发送数据包的时间戳,当要发送的时候赋上jiffies即可
unsigned char dev_addr[MAX_ADDR_LEN]; //MAC地址
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);
//数据包发送函数, sk_buff就是用来收发数据包的结构体
void (*tx_timeout) (struct net_device *dev); //发包超时处理函数
... ...
}
3.2.2 申请net_device
编写网络驱动的时候首先要申请 net_device,使用 alloc_netdev 函数来申请 net_device,这
是一个宏,宏定义如下:
#define alloc_netdev(sizeof_priv, name, name_assign_type, setup) \
alloc_netdev_mqs(sizeof_priv,name,name_assign_type,setup,1,1)
alloc_netdev本质是 alloc_netdev_mqs()函数,该函数定义如下:
/* : alloc_netdev_mqs
* @description : 申请net_deivce
* @param – sizeof_priv : 私有数据块大小
* @param – name : 设备名
* @param – setup : 回调函数,初始化设备后调用此函数
* @param – txqs : 分配的发送队列数量
* @param – rxqs : 分配的接收队列数量
* @return : 成功返回申请到的net_device指针,失败返回NULL
*/
struct net_device *alloc_netdev_mqs(int sizeof_priv,
const char *name,
void (*setup)(struct net_device *),
unsigned int txqs,
unsigned int rxqs)
3.2.3 删除net_device
当我们注销网络驱动时需要释放掉之前已经申请过的net_deivce,释放函数为free_netdev()。原型如下:
/* : free_netdev
* @description : 删除net_device
* @param – dev : 要释放的net_device指针
* @return : 无
*/
void free_netdev(struct net_device *dev)
3.2.4 注册net_device
net_device申请并初始化完成以后就要向内核注册net_device,要用函数register_netdev(),原型如下:
/* : register_netdev
* @description : 向内核注册net_device
* @param – dev : 要注册的net_device指针
* @return : 0 注册成功,负值 注册失败
*/
int register_netdev(struct net_device *dev)
3.2.5 注销net_device
若要对注册的设备进行注销,则使用unregister_netdev(),原型如下:
/* : unregister_netdev
* @description : 向内核注销net_device
* @param – dev : 要注销的net_device指针
* @return : 无
*/
int unregister_netdev(struct net_device *dev)
3.3 net_device_ops结构体
netdev_ops是net_device中重要的成员变量,是net_device_ops结构体指针类型,是网络设备的操作集,net_device_ops结构体定义在 include/linux/netdevice.h 中,net_device_ops结构体里面都是一些以"ndo_"开头的函数,需要我们自己去实现,其核心内容如下:
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);
netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb,
struct net_device *dev); /*当需要发送数据的时候此函数就会执行,此函数有一个参数为 sk_buff结构体指针,sk_buff结构体在Linux的网络驱动中非常重要,sk_buff保存了上层传递给网络驱动层的数据。也就是说,要发送出去的数据都存在了sk_buff中*/
u16 (*ndo_select_queue)(struct net_device *dev, struct sk_buff *skb, void *accel_priv,select_queue_fallback_t fallback); /*设备支持多传输队列的时候选择使用哪个队列*/
void (*ndo_change_rx_flags)(struct net_device *dev,
int flags);
void (*ndo_set_rx_mode)(struct net_device *dev); /*此函数用于改变地址过滤列表*/
int (*ndo_set_mac_address)(struct net_device *dev, void *addr); /*此函数用于修改网卡的 MAC 地址*/
int (*ndo_validate_addr)(struct net_device *dev); /*验证 MAC 地址是否合法,*/
int (*ndo_do_ioctl)(struct net_device *dev,
struct ifreq *ifr, int cmd); /*用户程序调用 ioctl 的时候此函数就会执行,*/
int (*ndo_set_config)(struct net_device *dev,
struct ifmap *map);
int (*ndo_change_mtu)(struct net_device *dev,
int new_mtu); /*更改MTU大小。*/
int (*ndo_neigh_setup)(struct net_device *dev,
struct neigh_parms *);
void (*ndo_tx_timeout) (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
...
int (*ndo_set_features)(struct net_device *dev, etdev_features_t features); /*修改net_device的 features属性,设置相应的硬件属性*/
...
};
四、总结
本文简单介绍了网络设备驱动的基本概念,随后对网络设备开发过程中的一些重要结构体(net_device、sk_buff、net_device_ops)的定义以及相关用法作出了大致说明,为网络设备驱动的开发奠定了一定的基础。