Linux网络设备驱动架构
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; // ... };
【温馨提示】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); ; }
接口信息继续:
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); ... };
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); ... }
Linux网络设备驱动架构的更多相关文章
- Linux网络设备驱动架構學習(三)
Linux网络设备驱动架構學習(三) 接下來會從以下幾個方面介紹網絡設備驅動的編寫流程: 1.網絡設備的註冊與註銷 2.網絡設備的初始化 3.網絡設備的打開與釋放 4.網絡數據發送流程 5.網絡數據接 ...
- linux i2c驱动架构-dm368 i2c驱动分析
linux i2c驱动架构-dm368 i2c驱动分析 在阅读本文最好先熟悉一种i2c设备的驱动程序,并且浏览一下i2c-core.c以及芯片提供商的提供的i2c总线驱动(i2c-davinc ...
- linux网络设备驱动
Linux网络设备驱动 Linux网络驱动程序的体系结构可划分为4个层次.Linux内核源代码中提供了网络设备接口及以网络子系统的上层的代码,移植特定网络硬件的驱动程序的主要工作就是完成设备驱动功能层 ...
- Linux网络设备驱动架構學習(二)
Linux网络设备驱动架構學習(二) 接下來會從以下幾個方面介紹網絡設備驅動的編寫流程: 1.網絡設備的註冊與註銷 2.網絡設備的初始化 3.網絡設備的打開與釋放 4.網絡數據發送流程 5.網絡數據接 ...
- Linux网络驱动架构
网络设备介绍 网络设备是计算机体系结构中必不可少的一部分,处理器如果想与外界通信,通常都会选择网络设备作为通信接口.众所周知,在 OSI(Open Systems Interconnection,开放 ...
- 【Linux开发】Linux V4L2驱动架构解析与开发导引
Linux V4L2驱动架构解析与开发导引 Andrew按:众所周知,linux中可以采用灵活的多层次的驱动架构来对接口进行统一与抽象,最低层次的驱动总是直接面向硬件的,而最高层次的驱动在linux中 ...
- 《linux设备驱动开发详解》笔记——14 linux网络设备驱动
14.1 网络设备驱动结构 网络协议接口层:硬件无关,标准收发函数dev_queue_xmit()和netif_rx(); 注意,netif_rx是将接收到的数据给上层,有时也在驱动收到数据以后调用 ...
- Linux网卡驱动架构分析
一.网卡驱动架构 由上到下层次依次为:应用程序→系统调用接口→协议无关接口→网络协议栈→设备无关接口→设备驱动. 二.重要数据结构 1.Linux内核中每一个网卡由一个net_device结构来描述. ...
- Linux网络设备驱动(一) _驱动模型
Linux素来以其强大的网络功能著名,同时, 网络设备也作为三大设备之一, 成为Linux驱动学习中必不可少的设备类型, 此外, 由于历史原因, Linux并没有强制对网络设备贯彻其"一切皆 ...
随机推荐
- [Spark性能调优] 第二章:彻底解密Spark的HashShuffle
本課主題 Shuffle 是分布式系统的天敌 Spark HashShuffle介绍 Spark Consolidated HashShuffle介绍 Shuffle 是如何成为 Spark 性能杀手 ...
- Mac安装Homebrew
Homebrew的安装,打开终端复制.粘贴以下命令,回车 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/i ...
- javascript编码标准
前面的话 编码标准是有争议的.几乎每个人都有自己的标准,但对标准应该是什么样的,则似乎很少能达成共识.但编码标准意味着,通过共同语言和一致的结构,把开发人员从无意义的工作中解放出来.允许开发人员把创新 ...
- Go同步和异步执行多个任务封装
同步执行类RunnerAsync 支持返回超时检测,系统中断检测 错误常量定义 //超时错误 var ErrTimeout = errors.New("received timeout&qu ...
- Tsung脚本中使用动态参数(一)---直接在脚本里编写Erlang代码
杀死一个程序猿,只要改三次需求.同理,杀死一个接口自动化测试人员,只要改三次接口数据处理方式.我目前的状态,改了一次接口数据处理方式,有一种胸闷的感觉. 因为改需求,所以,要改脚本.T_T.所以,才有 ...
- HTML基础知识(常见元素、列表、链接元素、图片元素)
1.HTML有关概念 全称: Hyper Text Markup Language(超文本标记语言) 其文件扩展名为".html"或".htm" * 超文本 - ...
- [51nod1373]哈利与他的机械键盘
作为一名屌丝程序员,机械键盘是哈利梦寐以求的神器.终于,在除夕夜的时候,他爸爸送了他一个机械键盘. 哈利的键盘与我们平常所见到的的键盘不一样,我们可以认为他的键盘是一个500*500的矩形,其中26个 ...
- JAVA爬虫实践(实践一:知乎)
爬虫顺序 1.分析网站网络请求 通过浏览器F12开发者工具查看网站的内容获取方式. 2.模拟HTTP请求,获取网页内容. 可以采用HttpClient,利用JAVA HttpClient工具可以模拟H ...
- 启动redis
方法一:默认是前端启动 默认端口:6379 cd /usr/local/redis/bin/redis-server 方法二:后端启动 方法二:后端启动 然后进入 指定配置文件 [roo ...
- 阻止浏览器冒泡事件,兼容firefox和ie
//得到事件 function getEvent(){ if(window.event) {return window.event;} func=getEvent.caller; while(func ...