网络驱动开发样例snull详解(基于3.10.0)
本章素材为ldd3书中的网络驱动snull部分。由于现在内核的更新,导致其在最新的内核中无法编译该网络驱动,需要针对修改,顾为此文(内核3.10.0)。另外,网络驱动的开发对理解linux内核网络协议栈有较大帮助,文中涉及设备虽为虚拟设备,但提供了网络驱动必备的知识。
1.1.1 snull编译问题
1、CFLAGS变为EXTRA_CFLAGS
2、config.h头文件替换注释掉
3、struct net_device 的结构在新版本内核中发生了变化,删去了quota成员;将(open,stop,set_config,hard_start_xmit等)封装进了 struct net_device_ops;将(poll,weight等)封装进了 struct napi_struct;将(hard_header,hard_header_cache,rebuild等)封装进了struct header_ops。并对部分函数名和参数进行了变动。
4、netif_rx_complete和netif_rx_schedule属于poll机制中的函数,接口名称已经变为napi_complete和napi_schedule。
具体修改过的文件可以从:
https://download.csdn.net/download/notbaron/10285251中下载
1.1.2 snull使用
回环地址的实现在drivers/net/loopback.c文件中。
在/etc/network文件中增加如下内容:
snullnet0 172.168.0.0
snullnet1 172.168.1.0
在/etc/hosts中:
172.168.0.1 local0
172.168.0.2 remote0
172.168.1.2 local1
172.168.1.1 remote1
配置虚拟网络设备:sn0和sn1.
#ifconfig sn0 local0
#ifconfig sn1 local1
测试虚拟设备:
#ping local0
#ping local1
1.1.3 驱动链接到内核
网络驱动通过模块初始化函数注册的方式与字符和块驱动是不同的,驱动为接口在一个全局的网络设备列表里插入一个数据结构。
每个接口由一个结构net_device项描述,定义在
include/linux/netdevice.h文件中。
通过内核函数alloc_netdev来进行分配,定义如下:
在3.10.0-514中为如下:
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs);
在4.14.14的内核中如下:
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
unsigned char name_assign_type,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs);
其中sizeof_priv表示私有数据区的大小。name是接口的名字。setup是设备初始化后的回调函数指针。如果是在该版本内核下那么snull.c中源码就存在问题了。
也可以直接调用alloc_etherdev(定义在文件net/ethernet/eth.c)函数,该函数也是调用alloc_netdev,只是把相关参数给你设置好了,例如回调函数为ether_setup,名字为eth开头。初始化函数主要是对net_device结构的例行初始化,大部分是存储我们的各种驱动函数指针。主要特点是禁用的ARP,因为是模拟的远程系统并不存在,所以去掉arp.
dev->flags |= IFF_NOARP; (定义在文件include/uapi/linux/if.h 中,在4.14.14代码中可以看到该变量可以被sysfs进行设置,但是不是持久的,重启后失效)
完成net_device结构初始化后,将该结构传递给register_netdev,来注册设备。
for (i = 0; i < 2; i++)
if ((result = register_netdev(snull_devs[i])))
printk("snull: error %i registering device \"%s\"\n",
result, snull_devs[i]->name);
else
ret = 0;
分配net_device之后,需要进行初始化。因为net_device 结构庞大,内核负责以太网范围中的缺省值,通过ether_setup函数(由alloc_etherdev调用)。
当初始化完设备后,调用register_netdev时候,驱动可能马上被调用来操作设备。
其中函数snull_rx_ints用于使能接收中断(设置snull_priv中的使能变量)。
snull_setup_pool设置包(snull_packet)缓冲池,是一个单向链表结构。
netif_napi_add函数依赖于模块参数use_napi,主要是实现实现napi结构的初始化以及与设备的关联。定义如下net/core/dev.c
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *, int), int weight)
1.1.4 卸载驱动
调用unregister_netdev调用,从系统中去除接口,free_netdev归还net_device结构给内核。释放初始化时候申请的缓冲区。
调用函数:snull_cleanup
释放缓冲区的函数为:snull_teardown_pool,该函数在注销了设备后才能进行.而且需要在net_device结构返回给系统之间进行,因为一旦调用free_netdev就不能再使用设备或者自己的私有数据做任何引用了。
1.1.5 设备方法
除了设备的初始化,注册和卸载方法之外,还需要定义设备相关的方法。网络设备能声明操作它的函数。
基本方法包括那些必须能够使用接口的,例如。
int dev_open(struct net_device *dev);
struct sk_buff *dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, int *ret);
1.1.5.1 基本方法
打开函数请求任何它需要的系统资源并且告知接口启动;关闭接口并释放系统资源。
int dev_open(struct net_device *dev);
硬件地址在open是复制到设备.同时启动接口的发送队列。内核提供函数netif_start_queue来启动队列,定义在include/linux/netdevice.h文件中。
int snull_open(struct net_device *dev)
{
memcpy(dev->dev_addr, "\0SNUL0", ETH_ALEN);
if (dev == snull_devs[1])
dev->dev_addr[ETH_ALEN-1]++; /* \0SNUL1 */
netif_start_queue(dev);
return 0;
}
netif_start_queue函数来告诉内核网络子系统,现在可以开始数据包的发送。
在虚拟设备中open操作动作很有限,就是初始化设备的MAC地址,调用netif_start_queue启动队列。
关闭函数就是逆操作:
int snull_release(struct net_device *dev)
{
netif_stop_queue(dev); /* can't transmit any more */
return 0;
}
调用netif_stop_queue后,就不能再继续发送了。在接口关闭时调用。
1.1.5.2 可选方法
例如:NAPI驱动提供的方法,在中断关闭下,用来在查询模式下操作接口。
change_mtu方法支持改变硬件地址的能力。
1.1.5.3 公用方法
例如:由接口使用来持有有用的状态信息。有些是ifconfig和netstat用来提供给用户关于当前配置的信息。
watchdog_timeo是设置的一个超时值,超时后调用驱动tx_timeout函数。
1.1.6 报文传送
驱动可以在模块加载时或者内核启动时探测接口。但是在接口能承载报文前,内核必须打开并分配一个地址给接口。使用的命令为ifconfig命令。需要做两个步骤,一个通过ioctl(SIOSCIFADDR)是安排地址,另一个通过iocatl(SIOCSIFFLAGS)设置IFF_UP来打开接口。
关闭时候调用iocatl(SIOCSIFFLAGS)来清除IFF_UP。
其发送函数为snull_tx,函数中保存发送时间,同时保存该skb结构体用于在中断时候释放。snull_tx最后调用snull_hw_tx函数,该函数本与硬件相关,而本snull是在虚拟设备上的,所以未有硬件相关细节,只是将包转发送其他的虚拟接口上,该函数的功能就是snull驱动的主要功能。
监视检测包的合法性,然后修改包中的源和目的ip地址,然后重新计算ip检验值。
其中函数snull_get_tx_buffer从缓冲池中获取包,过程中会进制软中断。如果缓冲池为空,则调用netif_stop_queue函数。
获取后,往获取的结构体中复制数据包内容,然后调用snull_enqueue_buf。
snull_enqueue_buf函数,将包放入到发送队列中(定义在私有数据结构snull_priv中的*rx_queue),此过程中会禁止软中断。
如果中断使能,则调用:snull_interrupt(0, dest, NULL);在未定义use_napi时使用snull_regular_interrupt函数,否则使用snull_napi_interrupt函数。定义分别如下:
static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
static void snull_napi_interrupt(int irq, void *dev_id, struct pt_regs *regs)
中断中会去检查是接收中断还是发送中断。终端中会更新统计数值,并释放skb,最后调用snull_release_buffer函数。snull_release_buffer函数释放pkt缓存。
snull_napi_interrupt函数,在接收触发中断时,禁用中断(在snull_regular_interrupt中并未禁用中断),然后调用napi_schedule函数使用轮询函数snull_poll。在发送时,统计包的发送个情况,调用dev_kfree_skb函数来释放skb.
snull_poll循环从发送队列中获取需要发送的包。最后使用napi_complete函数来结束查询,并使能中断。
超时时候调用驱动的tx_timeout方法。超时设置在watchdog_timeo即可,驱动不用去检测,由网络层来最终调用这个tx_timeout函数。
1.1.7 接收发送
网络的接收报文比发送要难一点。中断处理会调用函数snull_rx函数。
先分配一个缓存区来保存报文,就是skb,调用的函数是dev_alloc_skb,该函数的返回值必须检查,不然可能会遭殃。然后复制包内容到分配的skb内存结构中。
驱动更新统计计数来记录收到的一个报文。统计结构主要由以下几个成员组成:rx_packet,rx_bytes和tx_bytes,表示收到的报文数目,发送的数目和发送的字节总数。
最后有netif_rx函数将skb给上层。
1.1.8 链接状态改变
大部分涉及实际的物理连接的网络技术提供有一个载波状态。当驱动检测到一个设备载波丢失,它应当调用netif_carrier_off来通知内核这个改变。当载波回来时,应当调用netif_carrier_on。
1.1.9 统计信息
调用net_device_stats函数可以返回统计信息。
1.1.10 查看当前网卡驱动
查看网卡生产厂商和信号
# lspci | grep -i ethernet
使用lspci -vvv可以查看到驱动信息。当然pci设备也是可以查看到的。
1.1.11 查看驱动实例
appletalk驱动如下:
drivers/net/appletalk/cops.c
e1000驱动如下:
drivers/net/ethernet/intel/e1000/
1.1.12 加载驱动参数
module_param(name, type, perm);
其中,name:表示参数的名字;
type:表示参数的类型;
perm:表示参数的访问权限;