当前位置: 首页 > news >正文

Linux网络设备驱动结构

Linux系统对网络设备驱动定义了4个层次,这4个层次为网络协议接口层网络设备接口层、提供实际功能的设备驱动功能层网络设备与媒介层

一、Linux网络设备驱动的结构

1)网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP,还是IP,都通 过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立 于具体的设备。

2)网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device, 该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设 备驱动功能层的结构。

3)设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件 完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收 操作。

4)网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网 络适配器被设备驱动功能层中的函数在物理上驱动。对于Linux系统而言,网络设备和媒介都可以是虚拟 的。

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

二、网络协议层

网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口。当上层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 - socket buffer* @next: Next buffer in list* @prev: Previous buffer in list* @len: Length of actual data* @data_len: Data length* @mac_len: Length of link layer header* @hdr_len: writable header length of cloned skb* @csum: Checksum (must include start/offset pair)* @csum_start: Offset from skb->head where checksumming should start* @csum_offset: Offset from csum_start where checksum should be stored* @priority: Packet queueing priority* @protocol: Packet protocol from driver* @inner_protocol: Protocol (encapsulation)* @inner_transport_header: Inner transport layer header (encapsulation)16 * @inner_network_header: Network layer header (encapsulation)* @inner_mac_header: Link layer header (encapsulation)* @transport_header: Transport layer header* @network_header: Network layer header* @mac_header: Link layer header* @tail: Tail pointer* @end: End pointer* @head: Head of buffer* @data: Data head pointer*/struct sk_buff {/* These two members must be first. */struct sk_buff *next;struct sk_buff *prev;...unsigned int len,data_len;35 __u16 mac_len,hdr_len;...__u32 priority;...__be16 protocol;__be16 inner_protocol;__u16 inner_transport_header;__u16 inner_network_header;__u16 inner_mac_header;__u16 transport_header;__u16 network_header;__u16 mac_header;/* 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之间添加新的协议数据。

Linux套接字缓冲区支持分配、释放、变更等功能函数。

1、分配

Linux内核中用于分配套接字缓冲区的函数有:

struct sk_buff *alloc_skb(unsigned int len, gfp_t priority);
struct sk_buff *dev_alloc_skb(unsigned int len);

alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数len为数据缓冲区的空间大小,通常 以L1_CACHE_BYTES字节(对于ARM为32)对齐,参数priority为内存分配的优先级。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);

上述函数用于释放被alloc_skb()函数分配的套接字缓冲区和数据缓冲区。

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()函数在中断 和非中断上下文中皆可采用,它其实是做一个非常简单的上下文判断,然后再调用 __dev_kfree_skb_irq()或者dev_kfree_skb(),这从其代码的实现中也可以看出:

void __dev_kfree_skb_any(struct sk_buff *skb, enum skb_free_reason reason){if (in_irq() || irqs_disabled())__dev_kfree_skb_irq(skb, reason);elsedev_kfree_skb(skb);
}
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)。与该函 数的功能完成相反的函数是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传给协议栈。

==============================================

二、网络设备接口层

网络设备接口层的主要功能是为千变万化的网络设备定义统一、抽象的数据结构net_device结构体, 以不变应万变,实现多种硬件在软件层次上的统一。

net_device结构体在内核中指代一个网络设备,它定义于include/linux/netdevice.h文件中,网络设备驱动程序只需通过填充net_device的具体成员并注册net_device即可实现硬件操作函数与内核的挂接。 net_device是一个巨大的结构体,定义于include/linux/netdevice.h中,包含网络设备的属性描述和操作 接口,下面介绍其中的一些关键成员。

1、全局信息
char name[IFNAMESIZ];

name是网络设备的名称。

2、硬件信息
unsigned long mem_end;
unsigned long mem_start;

mem_start和mem_end分别定义了设备所使用的共享内存的起始和结束地址。

unsigned long base_addr;
unsigned char irq;
unsigned char if_port;
unsigned char dma;

base_addr为网络设备I/O基地址。

irq为设备使用的中断号。

if_port指定多端口设备使用哪一个端口,该字段仅针对多端口设备。

例如,如果设备同时支持 IF_PORT_10BASE2(同轴电缆)和IF_PORT_10BASET(双绞线),则可使用该字段。

dma指定分配给设备的DMA通道。

3、接口信息
unsigned short hard_header_len;

hard_header_len是网络设备的硬件头长度,在以太网设备的初始化函数中,该成员被赋为ETH_HLEN

unsigned short type;

type是接口的硬件类型。

unsigned mtu;

mtu指最大传输单元(MTU)。

unsigned char *dev_addr;

用于存放设备的硬件地址,驱动可能提供了设置MAC地址的接口,这会导致用户设置的MAC地址等存入该成员,如drivers/net/ethernet/moxa/moxart_ether.c中的moxart_set_mac_address()函数所示。

set_mac_address()函数

static int moxart_set_mac_address(struct net_device *ndev, void *addr)
{struct sockaddr *address = addr;if (!is_valid_ether_addr(address->sa_data))return -EADDRNOTAVAIL;memcpy(ndev->dev_addr, address->sa_data, ndev->addr_len);moxart_update_mac_address(ndev);return 0;
}

完成了memcpy()以及最终硬件上的MAC地址变更。

unsigned short flags;

flags指网络接口标志,以IFF_(Interface 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);int (*ndo_stop)(struct net_device *dev);netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb,struct net_device *dev);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);int (*ndo_validate_addr)(struct net_device *dev);int (*ndo_do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);...
};

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_stats* (*ndo_get_stats)(struct net_device *dev);

ndo_get_stats()函数用于获得网络设备的状态信息,它返回一个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 *addr);

ndo_do_ioctl()函数用于进行设备特定的I/O控制。

ndo_set_config()函数用于配置接口,也可用于改变设备的I/O地址和中断号。 ndo_set_mac_address()函数用于设置设备的MAC地址。

除了netdev_ops以外,在net_device中还存在类似于ethtool_ops、header_ops这样的操作集:

const struct ethtool_ops *ethtool_ops;const struct header_ops *header_ops;

ethtool_ops成员函数与用户空间ethtool工具的各个命令选项对应,ethtool提供了网卡及网卡驱动管理能力,能够为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:

static inline void netif_napi_add(struct net_device *dev,struct napi_struct *napi,int (*poll)(struct napi_struct *, int), int weight);
static inline 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调度。

static inline int napi_schedule_prep(struct napi_struct *n);

该函数用于检查NAPI是否可以调度,而napi_schedule()函数用于调度轮询实例的运行,其原型为:

static inline void napi_schedule(struct napi_struct *n);

在NAPI处理完成的时候应该调用:

static inline void napi_complete(struct napi_struct *n);

==============================================

四、设备驱动功能层

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的私有成员。

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

==============================================

三、网络设备驱动的注册与注销

网络设备驱动的注册与注销由register_netdev()和unregister_netdev()函数完成,这两个函数的原型为:

int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);

这两个函数都接收一个net_device结构体指针为参数。

net_device的生成和成员的赋值并不一定要由工程师亲自动手逐个完成,可以利用下面的宏帮助我们 填充:

#define alloc_netdev(sizeof_priv, name, setup) \alloc_netdev_mqs(sizeof_priv, name, setup, 1, 1)
#define alloc_etherdev(sizeof_priv) alloc_etherdev_mq(sizeof_priv, 1)
#define alloc_etherdev_mq(sizeof_priv, count) alloc_etherdev_mqs(sizeof_priv, count, count)

alloc_netdev以及alloc_etherdev宏引用的alloc_netdev_mqs()函数的原型为:

struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name, void (*setup)(struct net_device *),unsigned int txqs, unsigned int rxqs);

alloc_netdev_mqs()函数生成一个net_device结构体,对其成员赋值并返回该结构体的指针。第一个 参数为设备私有成员的大小,第二个参数为设备名,第三个参数为net_device的setup()函数指针,第 四、五个参数为要分配的发送和接收子队列的数量。setup()函数接收的参数也为struct net_device指针, 用于预置net_device成员的值。

free_netdev()完成与alloc_enetdev()和alloc_etherdev()函数相反的功能,即释放net_device结构体 的函数:

void free_netdev(struct net_device *dev);

net_device结构体的分配和网络设备驱动的注册需在网络设备驱动程序初始化时进行,而net_device结 构体的释放和网络设备驱动的注销在设备或驱动被移除的时候执行。

网络设备驱动程序的注册和注销

static int xxx_register(void){.../* 分配net_device结构体并对其成员赋值 */xxx_dev = alloc_netdev(sizeof(struct xxx_priv), "sn%d", xxx_init); 6 if (xxx_dev == NULL)... /* 分配net_device失败 *//* 注册net_device结构体 */if ((result = register_netdev(xxx_dev)))...
}static void xxx_unregister(void)
{.../* 注销net_device结构体 */unregister_netdev(xxx_dev);/* 释放net_device结构体 */free_netdev(xxx_dev);
}

五、网络设备的初始化

网络设备的初始化主要需要完成如下几个方面的工作。 

  • 进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备所使用的硬件资源。 
  • 进行软件接口上的准备工作,分配net_device结构体并对其数据和函数指针成员赋值。
  • 获得设备的私有信息指针并初始化各成员的值。如果私有信息中包括自旋锁或信号量等并发或同步 机制,则需对其进行初始化。

对net_device结构体成员及私有数据的赋值都可能需要与硬件初始化工作协同进行,即硬件检测出了 相应的资源,需要根据检测结果填充net_device结构体成员和私有数据。 网络设备驱动的初始化函数模板如下,具体的设备驱动初始化函数并不一定完全和本 模板一样,但其本质过程是一致的。

void xxx_init(struct net_device *dev)
{/* 设备的私有信息结构体 */struct xxx_priv *priv;/* 检查设备是否存在和设备所使用的硬件资源 */xxx_hw_init();/* 初始化以太网设备的公用成员 */ether_setup(dev);/* 设置设备的成员函数指针 */ndev->netdev_ops = &xxx_netdev_ops;ndev->ethtool_ops = &xxx_ethtool_ops;15 dev->watchdog_timeo = timeout;/* 取得私有信息,并初始化它 */priv = netdev_priv(dev);... /* 初始化设备私有数据区 */
}

xxx_hw_init()函数完成的与硬件相关的初始化操作如下。

  • 探测xxx网络设备是否存在。探测的方法类似于数学上的“反证法” ,即先假设存在设备xxx,访问该 设备,如果设备的表现与预期一致,就确定设备存在;否则,假设错误,设备xxx不存在。
  • 探测设备的具体硬件配置。一些设备驱动编写得非常通用,对于同类的设备使用统一的驱动,我们 需要在初始化时探测设备的具体型号。

另外,即便是同一设备,在硬件上的配置也可能不一样,我们也可 以探测设备所使用的硬件资源。 ·申请设备所需要的硬件资源,如用request_region()函数进行I/O端口的申请等,但是这个过程可以放在设备的打开函数xxx_open()中完成。

六、网络设备的打开与释放

网络设备的打开函数需要完成如下工作。

  • 使能设备使用的硬件资源,申请I/O区域、中断和DMA通道等。
  • 调用Linux内核提供的netif_start_queue()函数,激活设备发送队列。

网络设备的关闭函数需要完成如下工作。

  • 调用Linux内核提供的netif_stop_queue()函数,停止设备传输包。
  • 释放设备所使用的I/O区域、中断和DMA资源。

Linux内核提供的netif_start_queue()和netif_stop_queue()两个函数的原型为:

void netif_start_queue(struct net_device *dev);
void netif_stop_queue (struct net_device *dev);

网络设备打开和释放函数模板:

static int xxx_open(struct net_device *dev){/* 申请端口、IRQ等,类似于fops->open */ret = request_irq(dev->irq, &xxx_interrupt, 0, dev->name, dev); 5 ...netif_start_queue(dev);...}static int xxx_release(struct net_device *dev)
{/* 释放端口、IRQ等,类似于fops->close */free_irq(dev->irq, dev);...netif_stop_queue(dev); /* can't transmit any more */...
}

文章转载自:

http://HOT1M8Lv.rzmsL.cn
http://zSUsiGR2.rzmsL.cn
http://DmI3aDJk.rzmsL.cn
http://oc9xIRRA.rzmsL.cn
http://jebve9uw.rzmsL.cn
http://Xmox7jdv.rzmsL.cn
http://zrEHDRip.rzmsL.cn
http://LMYqNruA.rzmsL.cn
http://cVYAiHBF.rzmsL.cn
http://ylwj7khH.rzmsL.cn
http://N060pTwE.rzmsL.cn
http://FIhb9by1.rzmsL.cn
http://XJgEHtlf.rzmsL.cn
http://9JRtoJ6w.rzmsL.cn
http://Eu0ce9zW.rzmsL.cn
http://uIe1VeNN.rzmsL.cn
http://p0kNcDa9.rzmsL.cn
http://LhpGyLMk.rzmsL.cn
http://vElNjpCq.rzmsL.cn
http://AJ0JhJsd.rzmsL.cn
http://R20dDgTg.rzmsL.cn
http://6rty5exl.rzmsL.cn
http://c7IYvMee.rzmsL.cn
http://qw4H5Onw.rzmsL.cn
http://GV2rn4Jt.rzmsL.cn
http://Ur4MRuGN.rzmsL.cn
http://xWcagGNv.rzmsL.cn
http://YKkPzVAW.rzmsL.cn
http://r0xdwZaO.rzmsL.cn
http://adH58SSb.rzmsL.cn
http://www.dtcms.com/a/384677.html

相关文章:

  • 第四阶段C#通讯开发-3:串口通讯之Modbus协议
  • 使用生成式 AI 和 Amazon Bedrock Data Automation 处理大规模智能文档
  • 可可图片编辑 HarmonyOS(7)图片绘画
  • django登录注册案例(上)
  • 查看iOS设备文件管理 访问iPhone用户文件、App沙盒目录 系统日志与缓存
  • 基于Echarts+HTML5可视化数据大屏展示-白茶大数据溯源平台V2
  • android 框架—网络访问Okhttp
  • CUDA 中Thrust exclusive_scan使用详解
  • Quat 四元数库使用教程:应用场景概述
  • GitHub 热榜项目 - 日榜(2025-09-15)
  • 让AI数据中心突破性能极限
  • Self-supervised Feature Adaptation for 3D Industrial Anomaly Detection 论文精读
  • 【3D图像算法技术讨论】如何给基于3dgs重建的城市街景增加碰撞的属性,满足仿真的要求?
  • numpy学习笔记
  • Oracle体系结构-归档日志文件(Archive Log Files)
  • 唐源电气:机器视觉与AI Agent引领智能运维
  • 【Python】在pycharm中使用environment.ylm文件配置虚拟环境
  • 2025前端面试题及答案-2(详细)
  • 技术突破:《Light Sci. Appl.》SH-GSL理论,为超表面提供全通道谐波调控能力
  • 2025年ASOC SCI2区TOP,多类别教学优化算法+多修剪机器人与多施肥无人机协同任务分配,深度解析+性能实测
  • 佰力博检测与您探讨高低温介电测试的应用领域
  • 网络编程-day6
  • 【04】AI辅助编程完整的安卓二次商业实战-寻找修改替换新UI首页图标-菜单图标-消息列表图标-优雅草伊凡
  • 《格式工厂FormatFactory》 [5.21.0][便携版] 下载
  • 【ubuntu24.04】安装rust
  • vue-sync修饰符解析以及切换iframe页面进行保存提示功能的思路
  • 005 Rust变量与常量
  • DOM---操作元素样式属性详解
  • Excel简单教程
  • Node.js 项目依赖包管理