首页 > 系统相关 >【Linux驱动设备开发详解】14.Linux网络设备架构

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

时间:2024-06-11 15:55:11浏览次数:16  
标签:struct Linux dev len skb device 网络设备 net 14

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);
	...
}

标签:struct,Linux,dev,len,skb,device,网络设备,net,14
From: https://www.cnblogs.com/Wangzx000/p/18232029

相关文章

  • petalinux 交叉编译指定内核驱动
    需要编译u-dma-buf驱动。ThisrepositorycontainsaMakefie.MakefilehasthefollowingParameters:ParameterNameDescriptionDefaultValueARCHArchitectureName$(shelluname-m|sed-es/arm.*/arm/-es/aarch64.*/arm64/)KERNEL_SRCKernelSourc......
  • rockylinux8编译安装zabbix6.0.30-LTS
    zabbix6.和mysql安装系统环境:rockylinux8.10zabbix版本:zabbix-6.0.30LTS版本php版本:php7.2nginx版本:1.26mysql版本:mysql8#下载软件包wgethttps://cdn.zabbix.com/zabbix/sources/stable/6.0/zabbix-6.0.30.tar.gztarxvfzabbix-6.0.30.tar.gzln-s/tools/zabbix-6.0......
  • 【工作必备知识】Linux系统网络诊断与netstat命令
    【工作必备知识】Linux系统网络诊断与netstat命令大家好,我叫秋意零。今天分享一篇Linux系统中与网络相关的干货(包含相关面试题),有可能对你理解网络有一定帮助。同时工作中网络诊断也时常使用,对排查问题有帮助,绝对干货。如果有帮助记得点赞三连呀。netstat命令netstat......
  • linux内存管理(六)- 内核新struct - folio
    folio大概是5.16引入的,看起来像是page的封装,这里有一篇讲解folio很好的博客,论好名字的重要性:Linux内核page到folio的变迁-CSDN博客structfolio{/*private:don'tdocumenttheanonunion*/union{struct{/*public:*/unsignedlon......
  • 每天学一个 Linux 命令(6):shutdown
    Github地址:https://github.com/mingongge/Learn-a-Linux-command-every-day命令介绍shutdown命令可以用执行系统关机或系统重启,shutdown可以关闭系统的所有应用程序,并按用户的指定要求,进行系统关闭或重启的动作执行。此命令需要具备系统管理员权限才能使用。命令格式shutdo......
  • linux内存管理(五)- 缺页处理
    分析一下缺页的处理。缺页的意思是在访问内存的时候该地址还没有建好页表,页面尚未分配,或者页面被swap出去或者没有权限。缺页是同步异常,用户态发生缺页异常会等待内核解决,当然这一切对于用户态都是透明的。缺页处理的核心函数是do_page_fault,这个函数是架构相关的所以这个函数分布......
  • 前端使用 Konva 实现可视化设计器(14)- 折线 - 最优路径应用【代码篇】
    话接上回《前端使用Konva实现可视化设计器(13)-折线-最优路径应用【思路篇】》,这一章继续说说相关的代码如何构思的,如何一步步构建数据模型可供AStar算法进行路径规划,最终画出节点之间的连接折线。请大家动动小手,给我一个免费的Star吧~大家如果发现了Bug,欢迎来提Issue......
  • 在Linux中,当用户反馈网站访问慢,如何处理?
    当用户反馈网站访问慢时,在Linux环境中进行问题排查和解决可以遵循以下步骤:确认问题存在:首先,尝试复现问题。自己或让同事从不同地点和网络环境下访问网站,看是否同样慢。使用浏览器的开发者工具(如Chrome的Network面板)检查页面加载时间,识别哪个资源加载慢。定位问题源头:......
  • 在Linux中,文件权限有哪些?
    在Linux中,文件权限是确保系统安全的重要机制,它们控制着用户能够对文件或目录执行的操作类型。Linux文件权限分为以下几种基本类型:读权限(r):对于文件:允许用户查看文件的内容,例如使用cat、less或more命令阅读文件。对于目录:允许用户查看目录中的文件列表,即可以执行ls命令。......
  • 在Linux中,性能调优都有哪几种方法?
    在Linux中,性能调优是一个综合性的过程,旨在提升系统的运行效率、响应速度和资源利用率。以下是一些关键的性能调优方法:监控与分析使用工具如top,htop,vmstat,iostat,netstat,dstat,iftop,nmon等监控CPU使用率、内存使用、磁盘I/O、网络流量等,以便识别瓶颈。利用sysdig......