网络设备驱动简介
网络设备驱动是linux内核中三大类设备驱动之一,它用来完成高层网络协议的底层数据传输及设备控制。
网络设备与其他两种设备的区别:
网络接口不存在于linux的文件系统中,及/dev下没有设备文件,用户通过套接口socket函数使用网络。
网络除了响应来自内核的请求外,还需要处理外界的异步数据除了数据处理,网络设备还要完成地址设置、配置网络参数及流量统计等管理任务。
提到网卡不得不提一下网络分层模型,在互联网发展过程中出现过两种协议分层模型,及OSI模型和TCP/IP参考模型,但事实上被采用的是TCP/IP模型。
ISO模型和TCP/IP模型:
![](https://i-blog.csdnimg.cn/blog_migrate/0803870b319c7bed655195d5e9ee804e.png)
根据TCP/IP参考模型,网络设备及其驱动程序实际上完成的是最底层的主机到网络层。
在linux中,网络驱动子系统被设计成与协议无关,网络驱动只需负责具体的数据收发过程,而上层协议相关内容,由内核的网络协议栈完成。
网络世界中常用术语“octer”,指代一组8个的数据位,跟字节是一个意思。
网络系统的数据传输使用了数据封装的方式,当网络数据包从上层往下层传输时,下层会加上本层协议头,继续传给下层。而数据包从下层向上层传输时,则过程相反。
linux网络驱动框架
linux网络设备驱动层次结构为
![](https://i-blog.csdnimg.cn/blog_migrate/3c8338500ad2c9c59ad3ffd801b09f4b.png)
网络协议接口层
给上层协议提供统一的数据包收发接口,无论上层是ARP协议还是IP协议,都通过dev_queue_xmit函数发送数据,对数据包的接收通过netif_rx函数实现,函数原型:
dev_queue_xmit(struct sk_buff *skb) Int_netif_rx(struct sk_buff *skb)
sk_buff结构体:套接字缓冲区,用于在linux网络子系统中各层间传输数据,发送数据时内核协议栈将建立好的sk_buff交给网络驱动部分,当网络设备接收到数据,网络驱动要数据转换为sk_buff传给协议栈。
网络设备接口层
为千变万化的网络设备定义统一的、抽象的数据结构net_device结构体,以不变应万变,实现多种硬件在软件层次上的统一,包含网络设备的属性描述和操作接口。
net_device结构体在内核中指代一个网络设备,网络设备驱动只需填充其结构体并注册到内核就可以实现内核与具体硬件操作函数的挂接。
net_device结构体很复杂,它包含网络设备的属性描述和操作接口,后面我们会介绍该结构体。
设备驱动功能层
对应net_device结构体中的设备驱动功能函数,例如xxx_open()、xxx_stop()、xxx_tx()等。
另一个主题部分是中断处理函数,它负责接收硬件上的数据包并传给上层协议,主要函数有xxx_interrupt()和xxx_rx(),前者完成中断类型判断及处理,后者则将从硬件获得的数据进行封包并交给上层。
网络设备与媒介层
对应实际的硬件设备,对设备操作进行更一般的描述主要是一些访问网络设备内部寄存器的操作。具体的接口函数与特定的硬件紧密相关。
物理网卡通常包括PHY和MAC两个控制器,在OSI七层模型中,PHY指物理层,定义数据收发所需要的电气特性;MAC对应数据链路层,提供寻址机构、数据帧的构建、数据差错检查、传送控制。
linux驱动数据结构
在linux系统中所有的网络设备都被抽象为一个接口,这个接口提供了对所有网络设备的操作集合。
数据结构 struct net_device 就是网络设备接口,它既包含了纯软件网络设备接口,如环路(lo),又包含了硬件网络设备接口,如以太网(ethX),我们构建网络设备驱动的核心就是构建此数据结构。
该结构体只有部分成员会被驱动程序用到,其它成员仅提供给内核使用。
net_device部分成员介绍
char name[]:网络设备名称,名称字符串末尾的数字表示统一类型的多个适配器。
以下是一些网络设备的命名:
ethN 以太网接口包括10Mbps/100Mbps/1000Mbps
wifi0 无线网络接口
pppN ppp网络接口
isdnN ISDN网络接口
lo 回送网络接口
struct net_device_ops *netdev_ops:
网络设备方法操作集,该数据结构定义了针对当前设备的一组操作集合,比如ndo_open、ndo_stop和ndo_start_xmit等。
struct netdev_hw_addr_list uc:网络设备的单播MAC地址列表。
struct netdev_hw_addr_list mc:网络设备多播MAC地址列表。
struct netdev_hw_addr_list dev_addrs:网络设备硬件地址链表,net_device通过该成员将当前设备添加到链表中。
struct netdev_queue rx_queue:网络设备接收队列。
struct netdev_queue *_tx:网络设备发送队列。
watchdog_timeo:用于设定网络设备在传输数据包时传输超时的到期时间。
设备方法net_device_ops
网络设备最核心的功能是收发数据包,此外还需要提供配置与统计功能,这些被称之为设备方法
设备方法的实现依赖于具体的硬件环境,该部分正好对应了网卡驱动中的设备功能层。
从不同的硬件设备中提取具有共性的东西进行统一描述和操作,形成了net_device_ops设备方法,以下是linux中该结构体的部分成员:
![](https://i-blog.csdnimg.cn/blog_migrate/b8d63d5116b30d0dd28eea4f8e3efada.png)
针对网络设备操作集,我们需要根据设备的实际功能选择性的实现接口函数。
介绍几个接口函数:
int (*ndo_init)(struct net_device *dev):为具体硬件的初始化提供接口
int (*ndo_open)(struct net_device *dev)、int (*ndo_stop)(struct net_device *dev):使用ifconfig打开或关闭网络接口时,以上函数最终被调用。
netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,struct net_device *dev):
完成数据包的发送任务,skb为待发送的数据包,dev则为本次用来发送网络数据包的设备对象
提到数据的发送不得不提数据包的接收,由于网络设备接收数据是随机的,它采用中断的方式让驱动程序响应,后面我们会介绍数据包的接收流程。
linux驱动注册过程
net_device对象的分配和释放由于net_device结构体比较复杂,申请时需要进行一系列的初始化,所以我们使用内核提供的接口来完成申请和释放任务。
接口宏:
alloc_netdev():申请一个net_device对象并初始化。
alloc_etherdev():申请一个具体针对以太网卡设备的net_device对象同时初始化。
free_netdev():释放alloc_netdev等分配的系统资源。
网络设备驱动的注册与注销
int register_netdev(struct net_device *dev) 该函数完成注册网络设备驱动到linux内核,该函数会为设备分配一个接口名称,然后进行设备的注册。
int unregister_netdev(struct net_device *dev)注销设备驱动,释放占用的系统资源。
linux驱动数据包收发流程
数据收发流程概述:
发送:发送数据时,linux内核的网络处理模块必须建立一个包含要传输的数据包的sk_buff,然后将sk_buff递交给下层,各层在sk_buff中添加不同的协议头直至交给网络设备发送。
接收:当网络设备从媒介收到数据包后,它必须将数据转换为sk_buff数据结构并传递给上层,各层剥去相应的协议头直至交给用户。
相关数据结构sk_buff
sk_buff称为“套接字缓冲区”,用于在linux网络子系统中各层之间传递数据。是Linux网络子系统数据传递的“中枢神经”,是IP层与链路层交流的桥梁。 sk_buff的申请和释放:
struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
struct sk_buff *dev_alloc_skb(unsigned int length)
void kfree_skb(struct sk_buff *skb)
void dev_kfree_skb(struct sk_buff *skb)
sk_buff成员:
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head, *data; head指向已分配空间的头,end指向尾,这俩值是不变的;data和tail由模块根据需要进行修改,如图:
![](https://i-blog.csdnimg.cn/blog_migrate/d025c370cbec06498fd1abd424ababbd.png)
数据包的发送流程
数据包的接收流程
数据包发送的流控机制
软件层的ndo_start_xmit调用过程与网络设备的实际数据发送是异步的,这样会带来一个问题,网络子系统的高层代码快速发送大量数据,但底层网络设备实际发送速度无法与之匹配。
内核通过维护一个发送队列来缓解这个问题,同时需要底层设备及时告知上层当前的工作情况,当网络设备无法继续传输数据包时,告知网络子系统高层暂停数据发送。
内核专门为设备驱动提供了流控接口函数:
void netif_stop_queue(struct net_device *dev):通知上层停止调用ndo_start_xmit函数。
void netif_start_queue(struct net_device *dev):告诉上层可以调用dev_hard_start_xmit进行数据发送。
void netif_wake_queue(struct net_device *dev):告知上层可以发送数据,并重新触发数据包传输流程。
传输超时
对于网络子系统高层传下来的数据包,如果网络设备在指定的时间内没有发送出去,则会产生传输超时的问题。
linux内核的网络子系统提供了相应框架应对该情况,及传输超时控制机制对于驱动开发者传输超时的配置比较简单。
主要分两部分:
对dev_device中成员watchdog_timeo(超时定时器到期时间)的设置。
实现net_device_ops中的ndo_tx_timeout函数,该函数可以实现传输超时问题的具体处理。
对于ndo_tx_timeout函数要完成的任务,没有一个通行的规则,通常的是进行错误记录,调用netif_wake_queue函数重启传输队列,有时也可能需要对网络设备进行复位,根据具体网络设备而定。
虚拟网卡驱动实例
虚拟网卡是模拟出来的一块网络设备,其接口不依赖于任何硬件虚拟网卡有点像回环(loopback)接口,但不是一个回环接口,它模拟了两台机器的远程会话驱动程序在ndo_start_xmit(发送数据的函数)设备方法中没有将数据发送出去,而是把数据包的目的和源地址进行调换,通过netif_rx将数据又返回给上层协议。
注册:
![](https://i-blog.csdnimg.cn/blog_migrate/26d9f9cf9a98f9af00ccff442326e883.png)
注销:
虚拟网卡的设备方法
该虚拟网卡只是实现了ndo_start_xmit操作方法,驱动中的virt_net_send_packet函数将上层传过来的待发送数据进行源和目的地址的调换,又返回给网络子系统的上层,这样就实现了类似于回环接口的功能。