网络驱动开发样例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。

 

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:表示参数的访问权限;