Linux 网络设备驱动程序体系结构分为四层: 网络协议接口层, 网络设备接口层, 提供实际功能的设备驱动层以及网络设备与媒介层.
(1)网络协议接口层向网络层协议提供统一的数据包收发接口, 不论上层协议是 ARP 还是 IP, 都通过 dev_queue_xmit() 函数发送数据, 并通过 netif_rx() 函数接收数据. 这一层的存在使得上层协议独立于具体的设备.
(2)网络设备接口层向协议接口层提供的用于描述具体网络设备属性和操作的结构体 net_device, 该结构体是设备驱动功能层各函数的容器.
(3)设备驱动功能层的各函数是网络设备接口层 net_device 数据结构的具体成员, 是驱使网络设备硬件完成相应动作的程序, 它通过 nto_start_xmit() 函数启动发送操作, 并通过网络设备上的中断触发接收操作.
(4)网络设备与媒介层是完成数据包发送和接收的物理实体, 包括网络适配器和具体的传输媒介, 网络适配器被设备驱动功能层中的函数在物理上驱动.
驱动工程师的工作: 在设计具体的网络设备驱动程序时, 需要完成的主要工作是编写设备驱动功能层的相关函数以填充 net_device 数据结构的内容并将 net_device 注册入内核.
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 数据结构并传递给上层, 各层剥去相应的协议头直至交给用户.
struct sk_buff {
struct sk_buff * next; // sk_buff 是双向链表, 所以有前去后继, 这是指向后面的 sk_buff 结构体指针
struct sk_buff * prev; // 这是指向前一个 sk_buff 结构体指针
...unsigned int len,
// 表示数据区的长度 (tail-data) 与分片结构体数据区的长度之和.
data_len; // 只表示分片结构体数据区的长度, 所以 len=(tail - data) + data_len;
__u16 mac_len,
// mac 报头的长度
hdr_len; // 用于 clone 时, 表示 clone 的 skb 的头长度
...__u32 priority; // 优先级, 主要用于 QOS
...__be16 protocol; // 包的协议类型, 标识是 IP 包还是 ARP 包还是其他数据包
...__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; //
...
};
View Code
[温馨提示] 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 len); |
函数参数 | len: 为数据缓冲区的空间大小, 通常以 L1_CACHE_BYTES 字节 (对于 ARM 为 32) 对齐 |
priority: 为内存分配的优先级 | |
返回值 | 成功: 返回分配好的 sk_buff 指针; 失败: 返回 NULL |
[温馨提示] 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); |
函数参数 | sk_buff: 套接字缓冲区 |
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 内核中可以用如下函数在缓冲区尾部增加数据:
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).
对于一个空的缓冲区而言, 调用如下函数可以调整缓冲区的头部:
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 传给协议栈.
2 网络设备接口层
网络设备接口层的主要功能是为千变万化的网络设备定义统一, 抽象的数据结构 net_device 结构体, 以不变应万变, 实现多种硬件在软件层次上的统一.
net_device 结构体在内核中指代一个网络设备, 它定义在 include/linux/netdevice.h 文件中, 网络设备驱动程序只需通过填充 net_device 的具体成员并注册 net_device 即可实现硬件操作函数与内核的挂接.
(1)全局信息
char name[IFNAMESIZ]; // name 是网络设备名
(2)硬件信息
unsigned long mem_end; // 设备使用的共享内存的结束地址
unsigned long mem_start; // 设备使用的共享内存的起始地址
unsigned long base_addr; // base_addr 为网络设备 I/O 基地址
unsigned char irq; // irq 为设备使用的中断号
unsigned char if_port; // 指定多端口设备使用哪一个端口, 该字段仅针对多端口设备. 例如, 如果设备同时支持 IF_PORT_10BASE2(同轴电缆)和 IF_PORT_10BASET(双绞线), 则可使用该字段
unsigned char dma; // dma 指定分配给设备的 DMA 通道
(3)接口信息
unsigned short hard_header_len; // 网络设备的硬件头长度, 在以太网设备的初始化函数中, 该成员被赋值为 ETH_HLEN, 即 14
unsigned short type; // 接口的硬件类型
unsigned mtu; // 最大传输单元(MTU)
unsigned char *dev_addr; // 用于存放设备的硬件地址, 驱动可能提供了设置 MAC 地址的接口, 这会导致用户设置的 MAC 地址等存入该成员
dev_addr 范例使用代码:
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;
}
View Code
接口信息继续:
unsigned short flags; // 网络接口标志
网络接口标志以 IFF_开头, 部分标志由内核来管理, 其他的在接口初始化时被设置以说明设备接口的能力和特性. 接口标志包括:
IFF_UP(当设备被激活并可以开始发送数据包时, 内核设置该标志)
IFF_AUTOMEDIA(设备可在多种媒介间切换)
IFF_BROADCAST(允许广播)
IFF_DEBUG(调试模式, 可用于控制 prink 调用的详细程度)
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);
...
};
View Code
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_status* (*ndo_get_stats)(struct net_device *dev);
ndo_get_status() 函数用于获得网络设备的状态信息, 它返回一个 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 * adddr);
ndo_do_ioctl() 函数用于进行设备特定的 I/O 控制.
ndo_set_config() 函数用于配置接口, 也可用于改变设备的 I/O 地址和中断号.
ndo_set_mac_address() 函数用于设置设备的 MAC 地址.
除了 netdev_ops 以外, 在 net_device 中还存在类似于 ethool_ops,header_ops 这样的操作集:
const struct ethtool_ops * ethool_ops;
const struct header_ops * header_ops;
ethool_ops 成员函数与用户空间 ethool 工具的各个命令选项对应, ethool 提供了网卡及网卡驱动管理能力, 能够为 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:
void netif_napi_add(struct net_device * dev, struct napi_struct * napi, int( * poll)(struct napi_struct * , int), int weight);
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 调度.
该函数用于检查 NAPI 是否可以调度, 而 napi_schedule() 函数用于调度轮询实例的运行.
其原形为:
static inline void napi_schedule(struct napi_struct *n);
在 NAPI 处理完成的时候应该调用:
void napi_complete(struct napi_struct *n);
3 设备驱动功能层
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 的私有成员. 在 xxx_private 结构体中可包含设备的特殊属性和操作, 自旋锁与信号量, 定时器以及统计信息等, 这都由工程师自定义. 在驱动中, 要用到私有数据的时候, 则使用在 netdevice.h 中定义的接口:
static inline void *netdev_priv(const struct net_device *dev);
比如在驱动 drivers/net/ethernet/davicom/dm9000.c 的 dm9000_probe() 函数中, 使用 alloc_etherdev(sizeof(struct board_info)) 分配网络设备, board_info 结构体就成了这个网络设备的私有数据, 在其他函数中可以简单地提取这个私有数据. 例如:
static int dm9000_start_xmit(struct sk_buff *skb, struct net_device *dev) {
unsigned long flags;
board_info_t *db = netdev_priv(dev);
...
}
View Code
来源: https://www.cnblogs.com/laoyaodada/p/8397590.html