26.Linux-网卡驱动(详解)
1.描述
网卡的驱动其实很简单,它还是与硬件相关,主要是负责收发网络的数据包,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送, 并将接收到的数据包传递给上层协议。
网卡设备与字符设备和块设备不同, 网络设备并不对应于/dev目录下的文件,不过会存放在/sys/class/net目录下
如下图所示,我们通过ls /sys/class/net/ 命令,可以看到有两个网卡:
2.Linux系统对网络设备驱动定义了4个层次, 这4个层次有到下分为:
1)网络协议接口层:
实现统一的数据包收发的协议,该层主要负责调用dev_queue_xmit()函数发送数据, netif_rx()函数接收数据
2)网络设备接口层:
通过net_device结构体来描述一个具体的网络设备的信息,实现不同的硬件的统一
3)设备驱动功能层:
用来负责驱动网络设备硬件来完成各个功能, 它通过hard_start_xmit() 函数启动发送操作, 并通过网络设备上的中断触发接收操作,
4)网络设备与媒介层:
用来负责完成数据包发送和接收的物理实体, 设备驱动功能层的函数都在这物理上驱动的
层次结构如下图所示:
3.网卡驱动初始化
而我们的网卡驱动程序,只需要编写网络设备接口层,填充net_device数据结构的内容并将net_device注册入内核,设置硬件相关操作,使能中断处理等
3.1其中net_device结构体的重要成员,整理后如下所示:
struct net_device
{
char name[IFNAMSIZ]; //网卡设备名称
unsigned long mem_end; //该设备的内存结束地址
unsigned long mem_start; //该设备的内存起始地址
unsigned long base_addr; //该设备的内存I/O基地址
unsigned int irq; //该设备的中断号 unsigned char if_port; //多端口设备使用的端口类型
unsigned char dma; //该设备的DMA通道
unsigned long state; //网络设备和网络适配器的状态信息 struct net_device_stats* (*get_stats)(struct net_device *dev); //获取流量的统计信息
//运行ifconfig便会调用该成员函数,并返回一个net_device_stats结构体获取信息 struct net_device_stats stats; //用来保存统计信息的net_device_stats结构体 unsigned long features; //接口特征,
unsigned int flags; //flags指网络接口标志,以IFF_(Interface Flags)开头
//当flags =IFF_UP( 当设备被激活并可以开始发送数据包时, 内核设置该标志)、 IFF_AUTOMEDIA(设置设备可在多种媒介间切换)、
IFF_BROADCAST( 允许广播)、IFF_DEBUG( 调试模式, 可用于控制printk调用的详细程度) 、 IFF_LOOPBACK( 回环)、
IFF_MULTICAST( 允许组播) 、 IFF_NOARP( 接口不能执行ARP,点对点接口就不需要运行 ARP) 和IFF_POINTOPOINT( 接口连接到点到点链路) 等。 unsigned mtu; //最大传输单元,也叫最大数据包 unsigned short type; //接口的硬件类型 unsigned short hard_header_len; //硬件帧头长度,一般被赋为ETH_HLEN,即14 unsigned char dev_addr[MAX_ADDR_LEN]; //存放设备的MAC地址 unsigned long last_rx; //接收数据包的时间戳,调用netif_rx()后赋上jiffies即可 unsigned long trans_start; //发送数据包的时间戳,当要发送的时候赋上jiffies即可 unsigned char dev_addr[MAX_ADDR_LEN]; //MAC地址 int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);
//数据包发送函数, sk_buff就是用来收发数据包的结构体 void (*tx_timeout) (struct net_device *dev); //发包超时处理函数
... ...
}
上面讲到的统计信息net_device_stats结构体,其中重要成员如下所示:
struct net_device_stats
{
unsigned long rx_packets; /*收到的数据包数*/
unsigned long tx_packets; /*发送的数据包数 */
unsigned long rx_bytes; /*收到的字节数,可以通过sk_buff结构体的成员len来获取*/
unsigned long tx_bytes; /*发送的字节数,可以通过sk_buff结构体的成员len来获取*/
unsigned long rx_errors; /*收到的错误数据包数*/
unsigned long tx_errors; /*发送的错误数据包数*/
... ...
}
3.2 所以init函数,初始化网卡步骤如下所示:
- 1)使用alloc_netdev()来分配一个net_device结构体
- 2)设置网卡硬件相关的寄存器
- 3)设置net_device结构体的成员
- 4)使用register_netdev()来注册net_device结构体
4.网卡驱动发包过程
在内核中,当上层要发送一个数据包时, 就会调用网络设备层里net_device数据结构的成员hard_start_xmit()将数据包发送出去。
hard_start_xmit()发包函数需要我们自己构建,该函数原型如下所示:
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);
在这个函数中需要涉及到sk_buff结构体,含义为(socket buffer)套接字缓冲区,用来网络各个层次之间传递数据.
4.1 sk_buff结构体是一个双向链表,其中重要成员如下所示:
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; //指向下一个sk_buff结构体
struct sk_buff *prev; //指向前一个sk_buff结构体
... ...
unsigned int len, //数据包的总长度,包括线性数据和非线性数据
data_len, //非线性的数据长度
mac_len; //mac包头长度 __u32 priority; //该sk_buff结构体的优先级 __be16 protocol; //存放上层的协议类型,可以通过eth_type_trans()来获取
... ... sk_buff_data_t transport_header; //传输层头部的偏移值
sk_buff_data_t network_header; //网络层头部的偏移值
sk_buff_data_t mac_header; //MAC数据链路层头部的偏移值 sk_buff_data_t tail; //指向缓冲区的数据包末尾
sk_buff_data_t end; //指向缓冲区的末尾
unsigned char *head, //指向缓冲区的协议头开始位置
*data; //指向缓冲区的数据包开始位置
... ...
}
其中sk_buff结构体的空间,如下图所示:
其中sk_buff-> data数据包格式如下图所示:
4.2 所以,hard_start_xmit()发包函数处理步骤如下所示:
- 1)把数据包发出去之前,需要使用netif_stop_queue()来停止上层传下来的数据包,
- 2)设置寄存器,通过网络设备硬件,来发送数据
- 2)当数据包发出去后, 再调用dev_kfree_skb()函数来释放sk_buff,该函数原型如下:
void dev_kfree_skb(struct sk_buff *skb);
- 3)当数据包发出成功,就会进入TX中断函数,然后更新统计信息,调用netif_wake_queue()来唤醒,启动上层继续发包下来.
- 4)若数据包发出去超时,一直进不到TX中断函数,就会调用net_device结构体的(*tx_timeout)超时成员函数,在该函数中更新统计信息, 调用netif_wake_queue()来唤醒
其中netif_wake_queue()和netif_stop_queue()函数原型如下所示:
void netif_wake_queue(struct net_device *dev); //唤醒被阻塞的上层,启动继续向网络设备驱动层发送数据包 void netif_stop_queue(struct net_device *dev); //阻止上层向网络设备驱动层发送数据包
5.网卡驱动收包过程
而接收数据包主要是通过中断函数处理,来判断中断类型,如果等于ISQ_RECEIVER_EVENT,表示为接收中断,然后进入接收数据函数,通过netif_rx()将数据上交给上层
例如下图所示,参考的内核中自带的网卡驱动:/drivers/net/cs89x0.c
如上图所示,通过获取的status标志来判断是什么中断,如果是接收中断,就进入net_rx()
4.1 其中net_rx()收包函数处理步骤如下所示:
- 1)使用dev_alloc_skb()来构造一个新的sk_buff
- 2)使用skb_reserve(rx_skb, 2); 将sk_buff缓冲区里的数据包先后位移2字节,来腾出sk_buff缓冲区里的头部空间
- 3)读取网络设备硬件上接收到的数据
- 4)使用memcpy()将数据复制到新的sk_buff里的data成员指向的地址处,可以使用skb_put()来动态扩大sk_buff结构体里中的数据区
- 5)使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里
- 6)然后更新统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中
其中skb_put()函数原型如下所示:
static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
//len:将数据区向下扩大len字节
使用skb_put()函数后,其中sk_buff缓冲区变化如下图:
6.写虚拟网卡驱动
本节便开始来写一个简单的虚拟网卡驱动,也就是说不需要硬件相关操作,所以就没有中断函数,我们通过linux的ping命令来实现发包,然后在发包函数中伪造一个收的ping包函数,实现能ping通任何ip地址
在init初始函数中:
- 1)使用alloc_netdev()来分配一个net_device结构体
- 2)设置net_device结构体的成员
- 3)使用register_netdev()来注册net_device结构体
在发包函数中:
- 1)使用netif_stop_queue()来阻止上层向网络设备驱动层发送数据包
- 2)调用收包函数,并代入发送的sk_buff缓冲区, 里面来伪造一个收的ping包函数
- 3)使用dev_kfree_skb()函数来释放发送的sk_buff缓存区
- 4)更新发送的统计信息
- 5)使用netif_wake_queue()来唤醒被阻塞的上层,
在收包函数中:
首先修改发送的sk_buff里数据包的数据,使它变为一个接收的sk_buff,其中数据包结构如下图所示:
- 1)需要对调上图的ethhdr结构体 ”源/目的”MAC地址
- 2)需要对调上图的iphdr结构体”源/目的” IP地址
- 3)使用ip_fast_csum()来重新获取iphdr结构体的校验码
- 4)设置上图数据包的数据类型,之前是发送ping包0x08,需要改为0x00,表示接收ping包
- 5)使用dev_alloc_skb()来构造一个新的sk_buff
- 6)使用skb_reserve(rx_skb, 2);将sk_buff缓冲区里的数据包先后位移2字节,来腾出sk_buff缓冲区里的头部空间
- 7)使用memcpy()将之前修改好的sk_buff->data复制到新的sk_buff里的data成员指向的地址处:
memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);
// skb_put():来动态扩大sk_buff结构体里中的数据区,避免溢出
- 8)设置新的sk_buff 其它成员
- 9)使用eth_type_trans()来获取上层协议,将返回值赋给sk_buff的protocol成员里
- 10)然后更新接收统计信息,最后使用netif_rx( )来将sk_fuffer传递给上层协议中
7.驱动具体代码如下:
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/init.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/ip.h> #include <asm/system.h>
#include <asm/io.h>
#include <asm/irq.h> static struct net_device *virt_net; static void virt_rs_packet(struct sk_buff *skb, struct net_device *dev)
{
unsigned char *type;
struct iphdr *ih;
__be32 *saddr, *daddr, tmp;
unsigned char tmp_dev_addr[ETH_ALEN];
struct ethhdr *ethhdr;
struct sk_buff *rx_skb; /*1) 对调ethhdr结构体 "源/目的"MAC地址*/
ethhdr = (struct ethhdr *)skb->data;
memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);
/*2)对调 iphdr结构体"源/目的" IP地址*/
ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
saddr = &ih->saddr;
daddr = &ih->daddr;
tmp = *saddr;
*saddr = *daddr;
*daddr = tmp; /*3)使用ip_fast_csum()来重新获取iphdr结构体的校验码*/
ih->check = ;
ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl); /*4)设置数据类型*/
type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
*type = ; //之前是发送ping包0x08,需要改为0x00,表示接收ping包 /*5)使用dev_alloc_skb()来构造一个新的sk_buff */
rx_skb = dev_alloc_skb(skb->len + ); /*6)使用skb_reserve()来腾出2字节头部空间 */
skb_reserve(rx_skb, ); /*7)使用memcpy()将之前修改好的sk_buff->data复制到新的sk_buff里*/
memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len); // skb_put():来动态扩大sk_buff结构体里中的数据区,避免溢出 /*8)设置新的sk_buff 其它成员*/
rx_skb->dev = dev;
rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */ /*9)使用eth_type_trans()来获取上层协议 */
rx_skb->protocol = eth_type_trans(rx_skb, dev); /*10) 更新接收统计信息,并使用netif_rx( )来 传递sk_fuffer收包 */
dev->stats.rx_packets++;
dev->stats.rx_bytes += skb->len;
dev->last_rx= jiffies; //收包时间戳 netif_rx(rx_skb); }
static int virt_send_packet(struct sk_buff *skb, struct net_device *dev)
{
/*1)使用netif_stop_queue()来阻止上层向网络设备驱动层发送数据包*/
netif_stop_queue(dev); //期间设置硬件发送数据包 /*2)调用收包函数,里面来伪造一个收的ping包函数*/
virt_rs_packet(skb,dev); /*3)使用dev_kfree_skb()函数来释放发送的sk_buff缓存区*/
dev_kfree_skb(skb); /*4)更新发送的统计信息*/
dev->stats.tx_packets++; //成功发送一个包
dev->stats.tx_bytes+=skb->len; //成功发送len长字节
dev->trans_start = jiffies; //发送时间戳 /*5)使用netif_wake_queue()来唤醒被阻塞的上层*/
netif_wake_queue(dev); return ;
} static int virt_net_init(void)
{
/*1)使用alloc_netdev()来分配一个net_device结构体*/
virt_net= alloc_netdev(sizeof(struct net_device), "virt_eth0", ether_setup); /*2)设置net_device结构体的成员 */
virt_net->hard_start_xmit = virt_send_packet; virt_net->dev_addr[] = 0x08;
virt_net->dev_addr[] = 0x89;
virt_net->dev_addr[] = 0x89;
virt_net->dev_addr[] = 0x89;
virt_net->dev_addr[] = 0x89;
virt_net->dev_addr[] = 0x89; virt_net->flags |= IFF_NOARP;
virt_net->features |= NETIF_F_NO_CSUM; /*3)使用register_netdev()来注册net_device结构体 */
register_netdev(virt_net);
return ;
} static void virt_net_exit(void)
{
unregister_netdev(virt_net);
free_netdev(virt_net);
} module_init(virt_net_init);
module_exit(virt_net_exit); MODULE_LICENSE("GPL");
MODULE_AUTHOR("by:zhang");
8.测试运行
挂载驱动,如下图所示,可以看到net类下就有了这个网卡设备
开始试验,首先设置这个网卡设备的ip,然后去ping一下其它的ip,如下图所示:
上图的ping,之所以成功,是因为我们在发包函数中,伪造了一个来收包,通过netif_rx()来将收包上传给上层
使用ifconfig,可以看到这个网卡设备的统计信息共收发了6个包,以及收发的总数据
下节便开始学习网卡芯片DM9000C
如何编写移植DM9000C网卡驱动程序: http://www.cnblogs.com/lifexy/p/7777961.html
26.Linux-网卡驱动(详解)的更多相关文章
- linux usb 驱动详解
linux usb 驱动详解 USB 设备驱动代码通过urb和所有的 USB 设备通讯.urb用 struct urb 结构描述(include/linux/usb.h ). urb 以一种异步的方式 ...
- 26.Linux-网卡驱动介绍以及制作虚拟网卡驱动(详解)
1.描述 网卡的驱动其实很简单,它还是与硬件相关,主要是负责收发网络的数据包,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送, 并将接收到的数据包传递给上层协议. 网卡设备与字符设备和块 ...
- 13.Linux键盘驱动 (详解)
版权声明:本文为博主原创文章,未经博主允许不得转载. 在上一节分析输入子系统内的intput_handler软件处理部分后,接下来我们开始写input_dev驱动 本节目标: 实现键盘驱动,让开发板的 ...
- Linux网卡bounding详解
多块网卡绑在一起,作为一个网卡用,实现负载均衡和提高带宽 linux双网卡绑定一个IP地址,实质工作就是使用两块网卡虚拟为一块,使用同一个IP地址,是我们能够得到更好的更快的服务.其实这项技术在 ...
- linux网卡设置详解
centos7安装之后是需要在网卡配置文件中开始网络连接 onboot =yes 刚开始时网卡获取IP模式是dhcp 你会发现ifconfig不能用,猜测是废弃了,你要yum install net- ...
- linux 网卡配置文件详解2018-03-07
转自:https://www.cnblogs.com/ienino/p/7717092.html 配置文件位置:/etc/sysconfig/network-scripts/ifcfg-eth0 1. ...
- linux 网卡配置文件详解
配置文件位置:/etc/sysconfig/network-scripts/ifcfg-eth0 1. DEVICE=eth0 网卡的名字 2. HWADDR=00:0c:29:90:89:d9 HW ...
- usb驱动---linux ACM驱动详解ACA【转】
转自:http://blog.chinaunix.net/uid-9185047-id-3404684.html DTE提供或接收数据,连接到网络中的用户端机器,主要是计算机和终端设备.与此相对地,在 ...
- Linux设备驱动详解 宋宝华 硬件基础
处理器 存储器 接口与总线 I2C时序 SPI总线时序 以太网
随机推荐
- 使用Pano2VR 切割图片
图片转换好之后得到一组立方体面片.
- oracle pl/sql 控制结构(分支,循环,控制)
一.pl/sql的进阶--控制结构在任何计算机语言(c,java,pascal)都有各种控制语句(条件语句,循环结构,顺序控制结构...),在pl/sql中也存在这样的控制结构.在本部分学习完成后,希 ...
- Redis学习——Redis持久化之RDB备份方式保存数据
从这一个介绍里面知道,redis比memcache作为缓存数据库强大的地方,一个是支持的数据类型比较多,另一个就是redis持久化功能. 下面就介绍Redis的持久化之RDB! 一:什么是redis的 ...
- 分布式系统中生成全局ID的总结与思考
世间万物,都有自己唯一的标识,比如人,每个人都有自己的指纹(白夜追凶给我科普的,同卵双胞胎DNA一样,但指纹不一样).又如中国人,每个中国人有自己的身份证.对于计算机,很多时候,也需要为每一份数据生成 ...
- P1045
问题 A: P1045 时间限制: 1 Sec 内存限制: 128 MB提交: 145 解决: 127[提交][状态][讨论版] 题目描述 题目很简单,给出N个数字,不改变它们的相对位置,在中间加 ...
- ios实现无限后台任务
需求 我们的app是使用心跳机制来保持用户的登陆状态,这样才能收到服务器发来的消息和命令,但是当app进入后台以后大约3分钟或者10分钟之后app就会被系统挂起,用户就会超时下线,这样就必须保持app ...
- 想到一个赚钱的APP
通过APP上发布调查问卷的需求,鼓励人们注册,并给与一定的报酬.需求主要面向一些调查问卷,一类的需求发布
- JavaScript 版数据结构与算法(三)链表
今天,我们要讲的是数据结构与算法中的链表. 链表简介 链表是什么?链表是一种动态的数据结构,这意味着我们可以任意增删元素,它会按需扩容.为何要使用链表?下面列举一些链表的用途: 因为数组的存储有缺陷: ...
- java web Servlet学习笔记-2 请求重定向和请求转发的区别
请求转发与请求重定向的区别 请求重定向和转发 1.请求重定向:浏览器的行为(通过响应对象HttpServletResponse来执行) 特点:可以重新定向访问其他Web应用下的资源 浏览器发出了2次请 ...
- springboot scheduled并发配置
本文介绍如何使用springboot的sheduled实现任务的定时调度,并将调度的任务实现为并发的方式. 1.定时调度配置scheduled 1)注册定时任务 package com.xiaoju. ...