Linux网络设备驱动架構學習(一)

Linux网络设备驱动架構學習(一)

Linux 网络设备驱动的结构 

Linux 网络设备驱动程序的体系结构如下图所示,从上到下可以划分为 4 层,依次为网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层以及网络设备与媒介层,这 4 层的作用如下所示。 

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()函数的原型为: 

dev_queue_xmit (struct sk_buff * skb ); 

同样地,上层对数据包的接收也通过向 netif_rx()函数传递一个 struct sk_buff 数据结构的指针来完成。

netif_rx()函数的原型为: 

int netif_rx(struct sk_buff *skb); 

這兩個方法所在的路徑:kernel/net/core/dev.c

sk_buff 结构体非常重要,它的含义为“套接字缓冲区”,用于在 Linux 网络子系统中的各层之间传递数据,是 Linux 网络子系统数据传递的“中枢神经”。 当发送数据包时,Linux 内核的网络处理模块必须建立一个包含要传输的数据包的 sk_buff,然后将 sk_buff 递交给下层,各层在 sk_buff 中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网络媒介上接收到数据包后,它必须将接收到的数据转换为 sk_buff 数据结构并传递给上层,各层剥去相应的协议头直至交给用户。

1.套接字缓冲区成员 

参看 linux/skbuff.h 中的源代码,sk_buff 结构体的簡單說明:

(1)数据缓冲区指针 head、data、tail 和 end

Linux 内核必须分配用于容纳数据包的缓冲区,sk_buff 结构体定义了 4 个指向这片缓冲区不同位置的指针 head、data、tail 和 end。 

head 指针指向内存中已分配的用于承载网络数据的缓冲区的起始地址,sk_buff和相关数据块在分配之后,该指针的值就被固定了。 

data 指针则指向对应当前协议层有效数据的起始地址。每个协议层的有效数据含义并不相同,各层的有效数据信息包含的内容如下。 

1、对于传输层而言,用户数据和传输层协议头属于有效数据。 

2、对于网络层而言,用户数据、传输层协议头和网络层协议头是其有效数据。 

3、对于数据链路层而言,用户数据、传输层协议头、网络层协议头和链路层头部都属于有效数据。 

因此,data 指针的值需随着当前拥有 sk_buff 的协议层的变化进行相应的移动。 

tail 指针则指向对应当前协议层有效数据负载的结尾地址,与 data 指针对应。 

end 指针指向内存中分配的数据缓冲区的结尾,与 head 指针对应。和 head 指针一样,sk_buff 被分配之后,end 指针的值也就固定不变了。 

很显然,有效数据必须位于分配的数据缓冲区内,即(data,tail)区间位于(head,end)区间内,如图 16.2 所示。因此,head、data、tail 和 end 这 4 个指针间存在如下关系:

head <- data <- tail <- end。


从图 16.2 中可以看出, end 指针所指地址数据缓冲区的末尾还包括一个skb_shared_info 结构体的空间,这个结构体存放分隔存储的数据片段,意味着可以将数据包的有效数据分成几片存储在不同的内存空间中。图中的 frags 为分片数组,每一个分片的长度上限是一页。

(2)长度信息 len、data_len、truesize。 

sk_buff 结构体中定义的 len 是指数据包有效数据的长度,包括协议头和负载(Payload)。 为了支持数据包的分片存放,sk_buff 中增加了 data_len 这个成员,它记录分片的数据长度。 truesize 表示缓存区的整体长度,置为 sizeof(struct sk_buff)加上传入 alloc_skb()函数或 dev_alloc_skb()函数(下文将要介绍)的长度,但不包括结构体skb_shared_info的长度。 

2.套接字缓冲区操作 

下面我们来分析套接字缓冲区涉及到的操作函数,Linux 套接字缓冲区支持分配、释放、指针移动等功能函数。 

(1)分配。 

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

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

struct sk_buff *dev_alloc_skb(unsigned int len); 

alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数 len 为数据缓冲区的空间大小,以 16 字节对齐,参数 priority 为内存分配的优先级。 

dev_alloc_skb()函数只是以 GFP_ATOMIC 优先级(代表分配过程不能被中断)调用上面的 alloc_skb()函数,并保存 skb->head 和 skb->data 之间的 16 个字节。 分配成功之后,因为还没有存放具体的网络数据包,所以 sk_buff 的 data、tail 指针都指向存储空间的起始地址 head,而 len 的大小则为 0。 

(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()函数则在中断和非中断上下文中皆可采用。 

(3)指针移动。 

Linux 套接字缓冲区中的数据缓冲区指针移动操作包括 put(放置)、push(推)、pull(拉)、reserve(保留)等。 

① put 操作 

数据缓冲区指针 put 操作以下列函数完成: 

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

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

上述函数将 tail 指针下移,增加 sk_buff 的 len 值,并返回 skb->tail 的当前值。

skb_put()和__ skb_put()的区别在于前者会检测放入缓冲区的数据,而后者不会检查。这两个函数主要用于在缓冲区尾部添加数据。 

② push 操作 

数据缓冲区指针 push 操作以下列函数完成: 

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

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

与 skb_put()和_ _skb_put()不同,skb_push()和_ _skb_push()会将 data 指针上移,因此也要增加 sk_buff 的 len 值。push 操作在存储空间的头部增加一段可以存储网络数据包的空间,而 put 操作则在存储空间的尾部增加一段可以存储网络数据包的空间,因此主要用于在数据包发送时添加头部。skb_push()与_ _skb_push()的区别和skb_put()和_ _skb_put()的区别类似。 

③ pull 操作 

数据缓冲区指针 pull 操作以下列函数完成: 

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

skb_pull()函数将 data 指针下移,并减小 skb 的 len 值。这个操作一般用于下层协议向上层协议移交数据包,使 data 指针指向上一层协议的协议头。 

④ reserve 操作 

数据缓冲区指针 reserve 操作以下列函数完成: 

void skb_reserve(struct sk_buff *skb, unsigned int len); 

skb_reserve()函数将 data 指针和 tail 指针同时下移,这个操作主要用于在存储空间的头部预留 len 长度的空隙。 

下面我们以一个具体的 UDP 数据包接收的 Linux 处理流程为例来说明 sk_buff 的操作过程,这一过程的绝大部分工作都由 Linux 内核完成,驱动工程师只需完成涉及的数据链路层部分。 

假设以太网适配器(以太网卡)收到了一个 UDP 数据包,Linux 从底层到应用层处理这一数据包的流程如下。 

(1)网卡收到一个 UDP 数据包后,驱动程序需要创建一个 sk_buff 结构体和数据缓冲区,将收到的数据全部复制到 data 指向的空间,并将 skb->mac.raw 指向 data。此时,有效数据的开始位置是一个以太网头,skb->mac.raw 指向链路层的以太网头部。

(2)数据链路层通过调用 skb_pull()函数剥掉以太网协议头,向网络层 IP 传递数据包。在剥离过程中,data 指针会下移一个以太网头部的长度 sizeof(struct ethhdr),而 len则减去 sizeof(struct ethhdr)。此时,有效数据的开始位置是一个 IP 头部,skb->nh.raw指向 data,即 IP 头部,而 skb->mac.raw 仍指向以太网头部。 

(3)网络层通过 skb_pull()函数剥掉 IP 头,向传输层 UDP 传递数据包。在剥离过程中,data 指针会下移一个 IP 头部的长度 sizeof(struct iphdr),而 len 则减去 sizeof(struct iphdr)。此时,有效数据的开始位置是一个 UDP 头部,skb->h.raw 指向 data,即 UDP首部,而 skb->nh.raw 指向 IP 头部,skb->mac.raw 仍指向以太网头部。 

(4)应用程序在调用 recv()接收数据时,从 skb->data+sizeof(struct udphdr)的位置开始复制到应用层缓冲区。由此可见,UDP 头部到最后也没有被剥离。 

如图 16.3 所示为上述过程中各指针的指向和移动过程。


上面這個過程僅供參考,新的內核已經沒有了mac,nh和h這幾個特殊的聯合體了。

网络设备接口层 

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

net_device 结构体在内核中指代一个网络设备,网络设备驱动程序只需通过填充net_device 的具体成员并注册 net_device 即可实现硬件操作函数与内核的挂接。 net_device 本身是一个巨型结构体,包含网络设备的属性描述和操作接口。当我们编写网络设备驱动程序时,只需要了解其中的一部分。 

(1)全局信息。 

char name[IFNAMESIZ]; 

name 是网络设备的名称。 

int (*init)(struct net_device *dev); 

init 为设备初始化函数指针,如果这个指针被设置了,则网络设备被注册时将调用该函数完成对 net_device 结构体的初始化。但是,设备驱动程序可以不实现这个函
数并将其赋值为 NULL。 

(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,即 14。 

unsigned short type;

type 是接口的硬件类型。 

unsigned mtu; 

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

unsigned char dev_addr[MAX_ADDR_LEN]; 

unsigned char broadcast[MAX_ADDR_LEN]; 

dev_addr[ ]、broadcast[ ]无符号字符数组,分别用于存放设备的硬件地址和广播地址。对于以太网而言,这两个地址的长度都为 6 个字节。以太网设备的广播地址为 6
个 0xFF,而 MAC 地址需由驱动程序从硬件上读出并填充到 dev_addr[ ]中。 

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)设备操作函数。 

int (*open)(struct net_device *dev); 

int (*stop)(struct net_device *dev); 

open()函数的作用是打开网络接口设备,获得设备需要的 I/O 地址、IRQ、DMA通道等。stop()函数的作用是停止网络接口设备,与 open()函数的作用相反。 

int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev); 

hard_start_xmit() 函数会启动数据包的发送,当系统调用驱动程序的hard_start_xmit()函数时,需要向其传入一个 sk_buff 结构体指针,以使得驱动程序能获取从上层传递下来的数据包。 

void (*tx_timeout)(struct net_device *dev); 

当数据包的发送超时时,tx_timeout ()函数会被调用,该函数需采取重新启动数据包发送过程或重新启动硬件等策略来恢复网络设备到正常状态。 

int (*hard_header) (struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned len); 

hard_header()函数完成硬件帧头填充,返回填充的字节数。传入该函数的参数包括 sk_buff 指针、设备指针、协议类型、目的地址、源地址以及数据长度。对于以太网设备而言,将内核提供的 eth_header()函数赋值给 hard_header 指针即可。 

struct net_device_stats* (*get_stats)(struct net_device *dev); 

get_stats()函数用于获得网络设备的状态信息,它返回一个 net_device_stats 结构体。net_device_stats 结构体保存了网络设备详细的流量统计信息,如发送和接收到的数据包数、字节数等

int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd); 

int (*set_config)(struct net_device *dev, struct ifmap *map); 

int (*set_mac_address)(struct net_device *dev, void *addr); 

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

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

set_mac_address()函数用于设置设备的 MAC 地址。 

int (*poll)( struct net_device *dev,int quota); 

对于 NAPI(网络中断缓和)兼容的设备驱动,将以轮询方式操作接口,接收数据包。NAPI 是 Linux 系统上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据包,而是采用首先借助中断唤醒数据包接收的服务程序,然后以轮询方式获取数据包。 

net_device 结构体的上述成员需要在设备初始化时被填充

(5)辅助成员。 

unsigned long trans_start; 

unsigned long last_rx; 

trans_start 记录最后的数据包开始发送时的时间戳,last_rx 记录最后一次接收到数据包时的时间戳,这两个时间戳记录的都是 jiffies,驱动程序应维护这两个成员。 

void *priv; 

priv 为设备的私有信息指针,与 filp->private_data 的地位相当。设备驱动程序中应该以 netdev_priv()函数获得该指针。 

spinlock_t xmit_lock; 

int xmit_lock_owner; 

xmit_lock 是避免 hard_start_xmit()函数被同时多次调用的自旋锁。xmit_lock_owner则指当前拥有 xmit_lock 自旋锁的 CPU 的编号。

最新的內核中這些方法都存在,只是名字稍微會有一些不一樣,使用是注意下。

设备驱动功能层 

net_device 结构体的成员(属性和函数指针)需要被设备驱动功能层的具体数值和函数赋予。对于具体的设备 xxx,工程师应该编写设备驱动功能层的函数,这些函数形如 xxx_open() 、xxx_stop() 、xxx_tx() 、xxx_ hard_header() 、xxx_get_stats() 、xxx_tx_timeout()、xxx_poll()等。 

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

对于特定的设备,我们还可以定义其相关私有数据和操作,并封装为一个私有信息结构体 xxx_private,让其指针被赋值给 net_device 的 priv 成员。xxx_private 结构体中可包含设备特殊的属性和操作、自旋锁与信号量、定时器以及统计信息等,由工程师自定义。

网络设备与媒介层 

网络设备与媒介层直接对应于实际的硬件设备。为了给设备的物理配置和寄存器操作一个更一般的描述,我们可以定义一组宏和一组访问设备内部寄存器的函数,具体的宏和函数与特定的硬件紧密相关。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页