【Linux驱动设备开发详解】14.Linux网络设备架构

news/2024/10/5 15:23:09

1.Linux网络设备驱动的结构

与字符设备和块设备不同,网络设备并不对应于/dev目录下的文件,应用程序最终使用套接字完成与网络设备的接口。
Linux系统对网络设备驱动定义了4个层次,这4个层次为:

  • 网络协议接口层:向网络层协议提供同一的数据包收发接口,无论是IP还是ARP,都是通过dev_queue_xmit()发送数据,通过netif_rx()接收数据
  • 网络设备接口层:向网络协议层提供同一用于描述具体网络设备属性和操作的结构体net_device,此结构体是设备驱动功能层中各函数的容器
  • 设备驱动功能层:这一层的各函数是网络设备接口层net_device数据结构体的具体成员,驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作
  • 网络设备与媒介层:完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动驱动功能层中的函数在物理上驱动

image.png

在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核

1.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数据结构体并传递给上层,各层剥去相应的协议头直至交给用户。

sk_buff原型:

struct sk_buff {union {struct {/* These two members must be first. */struct sk_buff		*next;struct sk_buff		*prev;union {struct net_device	*dev;/* Some protocols might use this space to store information,* while device pointer would be NULL.* UDP receive path is one user.*/unsigned long		dev_scratch;};};struct rb_node		rbnode; /* used in netem, ip4 defrag, and tcp stack */struct list_head	list;};......unsigned int		len,data_len;__u16			mac_len,hdr_len;.....__u32			priority;int			skb_iif;__u32			hash;__be16			vlan_proto;__u16			vlan_tci;....union {__be16		inner_protocol;__u8		inner_ipproto;};__u16			inner_transport_header;__u16			inner_network_header;__u16			inner_mac_header;__be16			protocol;__u16			transport_header;__u16			network_header;__u16			mac_header;/* private: */__u32			headers_end[0];/* public: *//* These elements must be at the end, see alloc_skb() for details.  */sk_buff_data_t		tail;sk_buff_data_t		end;unsigned char		*head,*data;...
};

head和end指向缓冲区的头部和尾部,而data和tail指向实际数据的头部和尾部,每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据

image

套接字缓冲区涉及的操作函数:

(1) 分配

分配套接字缓冲区的函数:

// 分配一个套接字缓冲区和一个数据缓冲区, 参数len为数据缓冲区的空间大小, 通常以L1_CACHE_BYTES字节(对于ARM为32) 对齐, 参数priority为内存分配的优先级
struct sk_buff *alloc_skb(unsigned int len, gfp_t priority);
// dev_alloc_skb()函数以GFP_ATOMIC优先级进行skb的分配, 原因是该函数经常在设备驱动的接收中断里被调用
struct sk_buff *dev_alloc_skb(unsigned int len);

(2) 释放

用于释放alloc_skb套接字缓冲区和数据缓冲区的函数有:

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);	// 在中断或非中断皆可采用(实际是在内部做了判断,分别调用dev_kfree_skb_irq和dev_kfree_skb)

(3) 变更

在缓冲区尾部增加数据

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

它会导致skb->tail后移len(skb->tail+=len) , 而skb->len会增加len的大小(skb->len+=len) 。 通常, 在设备驱动的接收数据处理中会调用此函数。

在缓冲区开头增加数据

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

它会导致skb->data前移len(skb->data-=len) , 而skb->len会增加len的大小(skb->len+=len) 。 与该函数的功能完成相反的函数是skb_pull() , 它可以在缓冲区开头移除数据, 执行的动作是skb->len-=len、skb->data+=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传给协议栈。

1.2 网络设备接口层

net_device结构体在内核中指代一个网络设备,它定义于include/linux/netdevice.h文件中,网络设备程序只需通过net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。

net_device中包含了网络设备的属性描述和操作接口,比如下面这些关键成员:

(1)全局信息

char name[IFNAMESIZE];            // 网络设备的名称

(2)硬件信息

unsigned long mem_end;              // 设备所使用的共享内存的起始地址
unsigned long mem_start;	    // 设备所使用的共享内存的结束地址
unsigned long base_addr;                  // 网络设备I/O基地址
unsigned char irq;			  // 设备使用的中断号
unsigned char if_port;                    // 指定多端口设备使用哪一个端口,比如IF_PORT_10BASE2(同轴电缆)和IF_PORT_10BASET(双绞线)
unsigned char dma;			  // 指定分配给设备的DMA通道

(3)接口信息

unsigned short hard_header_len;                 // 网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋值为ETH_HLEN,即14
unsigned short type;                            // 接口的硬件类型
unsigned mtu;                                   // 最大传输单元
unsigned char *dev_addr;                        // 存放设备的硬件地址,驱动可能会提供设置MAC地址的接口,这会导致用户设置的MAC地址等存入该成员
unsigned short flags;                           // 网络接口标志

网络接口标志主要包括以下几种:

IFF_UP:当设备被激活并可以开始发送数据包时,内核设置该标志
IFF_AUTOMEDIA:设备可在多种媒介间切换
IFF_BROADCAST:允许广播
IFF_DEBUG:调试模式,可用于控制printk调用的详细程度
IFF_LOOPBACK:回环
IFF_MULTICAST:允许组播
IFF_NOARP:接口不能执行ARP
IFF_POINTOPOINT:接口连接到点对点链路

(4) 设备操作函数

const struct net_device_ops *netdev_ops;           // 此结构式网络设备的一系列硬件操作的集合
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);         // 打开网络接口设备,获取设备需要的I/O地址,IRQ,DMA通道等等int			(*ndo_stop)(struct net_device *dev);         // 停止网络接口设备netdev_tx_t		(*ndo_start_xmit)(struct sk_buff *skb, struct net_device *dev);   // 启动数据包发送netdev_features_t	(*ndo_features_check)(struct sk_buff *skb,struct net_device *dev,netdev_features_t features);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);int			(*ndo_do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);        // 进行设备特定的I/O控制int			(*ndo_set_config)(struct net_device *dev,struct ifmap *map);               // 用于配置接口,也可用于改变设备的I/O地址和中断号int			(*ndo_change_mtu)(struct net_device *dev,int new_mtu);int			(*ndo_neigh_setup)(struct net_device *dev,struct neigh_parms *);void			(*ndo_tx_timeout) (struct net_device *dev);        // 数据包发送超时时调用,需采取重新启动数据包发送过程或重新启动硬件等措施来恢复网络设备到正常状态void			(*ndo_get_stats64)(struct net_device *dev,struct rtnl_link_stats64 *storage);bool			(*ndo_has_offload_stats)(const struct net_device *dev, int attr_id);int			(*ndo_get_offload_stats)(int attr_id,const struct net_device *dev,void *attr_data);struct net_device_stats* (*ndo_get_stats)(struct net_device *dev);          // 获取网络设备的状态信息int			(*ndo_vlan_rx_add_vid)(struct net_device *dev,__be16 proto, u16 vid);int			(*ndo_vlan_rx_kill_vid)(struct net_device *dev,__be16 proto, u16 vid);....
};
const struct ethtool_ops *ethtool_ops;      // 成员函数与ethtool各个命令选项对应
const struct header_ops *header_ops;        // 对应于硬件头部操作,主要完成创建硬件头部和从给sk_buff分析出硬件头部等操作

(5) 辅助成员

unsigned long trans_start;                         // 记录最后的数据包开始发送时的时间戳
unsigned long last_rx;				   // 最后一次接收到数据包时的时间戳,这俩个时间戳记录的都是jiffies

NAPI

通常情况下,网络设备以中断方式接收数据包,而poll_conmtroller()则采用纯轮询方式,另外一种数据接收方式是NAPI(New API),其数据接收流程为"接收中断来临->关闭接收中断->以轮询方式接收所有数据包直到收空->开启接收中断->接收中断来临......"

static inline void netif_napi_add(struct net_device *dev,struct napi_struct *napi,int (*poll)(struct napi_struct *,int),         // NAPI要调度执行的轮询函数int weight);                     // 初始化一个NAPI
static inline void netif_napi_del(struct napi_struct *napi);       // 移除一个NAPI
static inline void napi_enable(struct napi_struct *n);               // 使能NAPI调度
static inline void napi_disable(struct napi_struct *n);		     // 禁止NAPI调度
static inline int napi_schedule_prep(struct napi_struct *n);         // 用于检查NAPI是否可以调度
static inline void napi_schedule(struct napi_struct *n);             // 用于调用轮询实例的运行
static inline void napi_complete(structg napi_struct *n);            // NAPI处理完成的时候应该调用

1.3 设备驱动功能层

设备驱动功能层主要是给net_device结构体中的成员(属性和net_device_ops结构体中的函数指针)赋予具体的数值和函数。也就是说设备驱动功能层中的函数是实际的硬件驱动函数。这些函数形如:xxx_open(),xxx_stop(),xxx_tx(),xxx_hard_header()等。

由于网络数据包的接收可由中断触发,所以设备驱动功能层中的另一个主体部分将是中断处理函数,它负责读取硬件上接收到的数据包并传送给上层协议,它负责读取硬件上接收到的数据包并传送给上层协议,因此可能包含xxx_interrupt()和xxx_rx()函数,前者完成中断类型判断的等基本工作,后者完成数据包生成及将其传递给上层等复杂工作。

对于特定的设备,还可以定义相关的私有数据和操作,并封装为一个私有信息结构体xxx_private,让其指针赋值给net_device的私有成员。在xxx_private结构体中可包含设备的页数属性和统计信息等。

在驱动中要用到私有数据的时候,则使用在netdevice.h中定义的接口:

static inline void *netdev_priv(const net_device *dev);

比如驱动dm9000.c的dm9000_probe函数中,使用alloc_etherdev(sizeof(struct board_info))分配网络设备,board_info结构体就成了这个网络设备的私有数据,在其他函数里可以简单地提取这个私有数据,例如:

static int dm9000_start_xmit(struct sk_buff,struct net_device *dev)
{unsigned long flags;board_info_t *db = netdev_priv(dev);...
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hjln.cn/news/43217.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

振动电阻式传感器测量模块的传感器接口

振动电阻式传感器测量模块的传感器接口 RM502模块采用了高精度模拟信号驱动和采集技术,能够驱动和测量对电阻精度要求较高的传感器。它采用恒流驱动和实时电流测量,有效避免了环境温度变化引起的测量误差。同时,它具有高精度差分AD转换和可编程增益放大功能,能够对小信号具…

微服务全链路追踪

随着现代应用微服务化,客户端的请求往往需要服务器端多个组件的协调工作。 事务的处理是由分布式的服务架构完成,在这个过程中,问题的定位变得较为困难,我们需要梳理组件之间的依赖,并准确定位到问题所在。 这时候我们需要借助一些手段实现问题的定位和跟踪。 通常的做法有…

C语言中的数据类型及其转换

目录计算机中的数据类型整型数据之间的转换相同字长之间的转换小字长转大字长大字长转小字长int、float、double之间的转换float->doubledouble->floatfloat/double->intint->floatint->double 计算机中的数据类型 计算机中的数据以二进制的形式存储在寄存器或存…

机器学习之支持向量机

什么是SVM SVM(全称Support Vector Machine)中文名支持向量机。SVM是一种监督机器学习算法,是一种二分类模型,它的目的是寻找一个超平面来对样本进行分割,分割的原则是间隔最大化,最终转化为一个凸二次规划问题来求解。可用于分类或回归挑战。然而,它主要用于分类问题。…

Redis之发布订阅

发布订阅Redis 发布订阅(pub/sub)是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。消息发送者&消息接收者&频道可以想象这么一个场景。我们抖音、微博关注了哪个博主,当他发布一条文章时,系统就会给我们推送他…

DBAPI 安装部署standalone 数据API接口发布平台

DBAPI 项目地址:https://gitee.com/freakchicken/db-api 项目简介:零代码开发api服务,只需编写sql,就可以生成http api服务。支持api动态创建,兼容多种数据库。 适用于BI报表、数据可视化大屏的后端接口快速开发。 旨在为企业数据服务的发布提供完整解决方案 一、下载安装…

wu

转载自 他是 ISIJ 第四名,也是在线知名题库的洛谷“网红”。 2024年全国青少年信息学奥林匹克竞赛冬令营(WC)上,以优秀成绩斩下第一名年仅六年级的 $liuyi0905$,成为最夺目的选手之一。 而且虽然是六年级的选手,但他取得优异成绩后,不少网友并不感到陌生,纷纷留言: 这…

如何阅读MTF图表

前言 1943年——就在二战方酣的困顿时期——蔡司(Zeiss)发展出一套称为「调制传递函数」(德:Modulationsbertragungsfunktion;英:Modulation Transfer Function, MTF;日:変调伝达关数)的科学程序,用来评量镜头的影像品质。光学仪器业者、相机镜头制造商欣然拥抱这项新…