LDD-Network Drivers

Linux设备分为三大类:字符设备,块设备,网络接口。
网络接口和挂载的块设备类似,需要注册到内核中,然后和外界进行数据传输。但是也有不同:磁盘设备在/dev目录下有文件项,但是网络接口没有,常规的文件操作如读、写对于网络接口而言没有意义,无法应用Unix的“一切都是文件”。因此,网络接口存在于自己的命名空间中,导出不同的操作。
二者最重要的区别在于块驱动只会响应来自内核的请求,但是网络驱动可以从外界异步的接收数据包;块驱动被要求向内核发送缓冲数据,而网络设备要求向内核发送到来的数据包。
网络驱动还需要支持管理任务,例如设置地址、修改传输参数、维护传输或者错误的统计数据,网络驱动的API反映了这种需求。
Linux内核的网络子系统的设计完全独立于协议,包括网络协议(IP,IPX)和硬件协议(以太网,ring)。网络驱动和内核每次只会传输一个数据包,从而将协议问题从驱动中隐藏,将物理传输从协议中隐藏。
网络设备用术语octet来代表8位数据,通常为网络设备和协议的最小单元,几乎不使用字节这个单位。首部是添加到数据包前的一些octet。
本章会以snull为例说明网络接口的工作方式。

How snull Is Designed

snull独立于硬件,模拟和真实的远程主机通信来更好地说明写网络驱动的任务。Linux的回环驱动很简单,代码位于drivers/net/loopback.c。
snull只支持IP协议,和内部的工作机制有关——snull需要解析数据包来模拟硬件接口。

Assigning IP Numbers

snull实现了两个接口,和回环不一样,任意一个接口发送的数据都会到达另一个接口,而不是自己。这个不能仅仅通过分配IP地址来实现——内核不会通过接口A将数据发送到自己的接口B,内核不会利用snull,而是直接通过回环网络发送数据包。为了在两个接口间建立连接,需要在传输数据时修改源地址和目的地址,保证接收的接口不会被判定为和发送的接口相同的主机。
为了实现这种隐藏的回环,snull将源地址和目的地址的第三个octet的最低位进行翻转,即将C类IP地址的网络号和设备号同时改变,使得发送到第一个接口的数据包出现在第二个接口。
设计到的IP地址如下:

  • snullnet0是连接到sn0接口的网络,snullnet1是连接到接口sn1的网络,两个网络的地址只有第三个octet的最低位不同,24位掩码
  • local0是sn0的IP地址,属于snullnet0;local1是sn1的IP地址。两个地址的第三和第四个octet的最低位必须不同
  • remote0是snullnet0连接的远程主机,地址中的第四个octet和local1相同;每个发送到remote0的数据包在接口代码修改其网络地址后都会到达local1。remote1是snullnet1连接的主机,地址中的第四个octet和local0相同

可能的网络号为:

snullnet0 192.168.0.0
snullnet1 192.168.1.0

将其添加到/etc/networks中,可以通过名称使用;
可能的主机号为:

192.168.0.1    local0
192.168.0.2    remote0
192.168.1.2    local1
192.168.1.1    remote1

添加到/etc/hosts中。只要保证local0和remote1的主机号相同,local1和remote0的主机号相同。
之后通过ifconfig sn0 local0ifconfig sn1 local1配置网口。

The Physical Transport of Packets

就数据传输的方式而言,snull是以太网(plip接口,使用打印端口也将自己声明为以太网设备),可以使用tcpdump工具查看传输的包。
snull只适用于IP数据包,因为实现代码会修改每个包的源地址、目的地址、首部校验值,而不会判断数据包的类型。

Connecting to the Kernel

我们通过snull的代码来研究网络驱动的结构,查看一些驱动的代码实现也可以帮助了解真实的Linux网络驱动是如何工作的,比如loopback.c,plip.c,e100.c。

Device Registration

网络驱动的注册不会分配主次设备号,而是将包探测到的接口信息的结构体插入到一个网络设备的全局列表中。每一个接口通过struct net_device描述,snull包含两个结构体struct net_device *snull_dev[2];
struct net_device包含一个kobject,引用计数,通过sysfs导出,必须通过函数struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *));动态分配。sizeof_priv是驱动私有数据的长度;在网络驱动中,私有数据和net_device结构体一起分配,位于同一块内存中。name是接口的名称,可以包含printf-风格的%d,内核会用下一个可用的接口号来替换。setup是指向初始化函数的指针,用来填充剩余的net_device结构体。
内核针对不同类型的网络设备提供了相应的初始化函数,可以方便的分配网络设备,而且不需要提供setup初始化函数。
net_device初始化完成后,通过函数register_netdev完成注册。一旦调用register_netdev函数,驱动就可能被调用。

Initializing Each Device

struct net_device可以通过以太网的初始化函数ether_setup进行初始化,然后需要手动设置一些其他成员的值。其中私有数据可以包含一些实现操作函数所需的数据。

Module Unloading

模块卸载函数会注销接口unregister_netdev,做一些内部清除工作,并将net_device返还给系统free_netdev
需要注意的是,只有注销设备以后、释放net_device之前进行内部的清除工作(私有数据)。

The net_device Structure in Detail

net_device是网络驱动的核心,下面列出结构体的所有成员。

Global Information

  • char name[IFNAMSIZ];
    设备的名称
  • unsigned long state;
    设备的状态,驱动通过一些函数而不是直接设置状态位
  • struct net_device *next;
    指向全局网络设备链表的下一项
  • int (*init)(struct net_device *dev);
    初始化函数,如果设置,register_netdev函数会调用来完成初始化

Hardware Information

硬件相关的信息主要用于早期的Linux网络环境中,现在很多驱动程序不会使用,这里列出只是为了完整性。

  • unsigned long rmem_end;
  • unsigned long rmem_start;
  • unsigned long mem_end;
  • unsigned long mem_start;
    设备的内存信息,保存设备共享的内存的始末地址,如果收发的内存不一样,rmem为接收的内存地址。
  • unsigned long base_addr;
    网络接口的I/O基地址,在设备探测阶段由驱动设置
  • unsigned char irq;
    分配的中断号,在使用ifconfig命令时会显示出dev->irq
  • unsigned char if_port;
    多端口设备中正在使用的端口号
  • unsigned char dma;
    设备分配的DMA通道

Interface Information

很多接口相关的信息都由ether_setup函数正确设置,但是flagsdev_addr的值和设备相关,必须在初始化时显式设置。系统还有一些非以太网设备可以使用的接口函数,包括:

  • void ltalk_setup(struct net_device *dev);
  • void fc_setup(struct net_device *dev);
  • void fddi_setup(struct net_device *dev);
  • void hippi_setup(struct net_device *dev);
  • void tr_setup(struct net_device *dev);
    分别用于LocalTalk、fiber-channel、Fiber Distributed Data Interface、High-Performance Parallel Interface、token ring网络接口。

如果网络类型不属于上述类型,需要手中设置以下成员的的值:

  • unsigned short hard_header_len;
    硬件首部的长度,即传输的数据包最开始的octet的数量,位于IP等其他协议的首部之前,以太网为14
  • unsigned mtu;
    最大传输单元(maximum transfer unit),供网络层用来驱动数据包的发送,以太网为1500
  • unsigned long tx_queue_len;
    设备发送队列的长度,ether_setup会将其设置为1000,可以改变
  • unsigned short type;
    接口的硬件类型,由ARP用来判断接口支持的硬件地址,以太网为ARPHRD_EHTER
  • unsinged char addr_len;
  • unsinged char broadcast[MAX_ADDR_LEN];
  • unsigned char dev_addr[MAX_ADDR_LEN];
    硬件(MAC)地址的长度和设备的硬件地址,以太网的地址长度为6 octet,广播地址由6个0xff构成;设备的硬件地址从设备读取,再由驱动拷贝到dev_addr中。硬件地址在数据包发送到驱动前用来生成正确的以太网首部
  • unsigned short flags;
    接口的标志位,flags可以包含以下位:
    • IFF_UP
      对于驱动只读,内核在设备处于活跃状态并且准备好传输数据包时设置
    • IFF_BROADCAST
      表明接口支持广播,以太网支持
    • IFF_DEBUG
      代表调试模式,可以用来控制printk或者其他的调试目的,可以由用户程序通过ioctl设置
    • IFF_LOOPBACK
      回环接口需要设置
    • IFF_POINTOPOINT
      表明接口连接到点对点连接,由驱动或者ifconfig设置
    • IFF_NOARP
      表明接口不能进行ARP,比如点对点接口
    • IFF_PROMISC
      激活任意传输,默认情况下以太网接口会使用硬件的过滤器,只接收广播和发给自己的数据包,tcpdump会接收所有的数据包,就会设置此位
    • IFF_MULTICAST
      表明接口可以用于多播传输,ehter_setup默认设置此位
    • IFF_ALLMULTI
      告诉接口收取所有的多播数据包,当主机进行多播路由,而且IFF_MULCAST设置时内核才会设置此位;对于驱动只读
    • IFF_MASTER
    • IFF_SLAVE
      由负载均衡代码使用,驱动不可用
    • IFF_PORTSEL
    • IFF_AUTOMEDIA
      表明设备可以在多种介质类型间切换,如果设置此位,设备会自动选择合适的介质
    • IFF_DYNAMIC
      驱动设置,表明接口的地址可能发生变化
    • IFF_RUNNING
      表明接口开启并且正在运行
    • IFF_NOTRAILERS
      用于BSD,Linux没有使用
    一个程序改变IFF_UP标志时,设备的open或者stop就会被调用;IFF_UP或者任何其他的标志被修改时,set_multicast_list方法会被调用。如果驱动程序需要对标志位的修改作出返回,必须在set_multicast_list函数内进行。
  • int features;
    由驱动设置,告知内核接口的硬件特性,包括以下标志位:
    • NETIF_F_SG
    • NETIF_F_FRAGLIST
      表明接口支持scatter/gather I/O,可以传输分布在不同内存段中的数据包。如果设备没有提供任何形式的校验,内核也不会进行scatter/gather I/O——如果内核需要跨过内存边界来计算校验值,就会将分离的数据拷贝并组合起来。
    • NETIF_F_IP_CSUM
    • NETIF_F_NO_CSUM
    • NETIF_F_HW_CSUM
      NETIF_F_IP_CSUM表明接口支持IP校验,不支持别的;NETIF_F_NO_CSUM表明接口不支持校验;NETIF_F_HW_CSUM表明接口可以进行硬件校验。
    • NETIF_F_HIGHDMA
      表明设备可以在高内存进行DMA操作,不设置此位所有的DMA缓冲区都在低内存分配
    • NETIF_F_HW_VLAN_TX
    • NETIF_F_HW_VLAN_RX
    • NETIF_F_HW_VLAN_FILTER
    • NETIF_F_VLAN_CHALLENGED
      表明硬件对802.1q VLAN的支持情况
    • NETIF_F_TSO
      表明设备支持TCP分段卸载(TCP segmentation offloading)

The Device Methods

网络接口的设备方法可以分为两类:基本的和可选的。基本的为使用接口所需的方法,可选的实现了一些高级的非必需的功能。首先是基本方法:

  • int (*open)(struct net_device *dev);
    打开接口,只要ifconfig激活接口,即被打开。open函数需要注册所需的系统资源,包括I/O端口,中断号,DMA等。
  • int (*stop)(struct net_device *dev);
    关闭接口,和打开时做的操作相反
  • int (*hard_start_xmit)(struct sk_buff *skb, struct net_device *dev);
    开始传输一个数据包,数据包放在skb(socket buffer)中
  • int (*hard_header)(struct sk_buff *skb, struct net_device *dev, unsinged short type, void *daddr, void *saddr, unsigned len);
    hard_start_xmit前调用,根据之前得到的源物理地址和目的物理地址构建硬件首部信息,ether_setup或设置该函数为eth_header
  • int (*rebuild_header)(struct sk_buff *skb);
    在ARP完成后,发送包之前用来改造硬件首部
  • void (*tx_timeout)(struct net_device *dev);
    数据包的传送没有计时完成时调用
  • struct net_device_stats *(*get_stats)(struct net_device *dev);
    应用需要该接口的统计信息时调用,比如ifconfig,netstat
  • int (*set_config)(struct net_device *dev, struct ifmap *map);
    改变接口的配置,是配置驱动程序的入口

接下来是可选方法:

  • int weight;
  • int (*poll)(struct net_device *dev, int *quota);
    NAPI驱动在关闭中断时采用轮询模式操作接口的函数
  • void (*poll_controller)(struct net_device *dev);
    中断关闭时该函数要求驱动检查接口发生的事件,用于特定的内核内网络任务,比如远程控制台,在内核调试网络
  • int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
    执行接口特定的ioctl命令
  • void (*set_multicast_list)(struct net_device *dev);
    设备的多播列表或者标志位发生改变时调用
  • int (*set_mac_address)(struct net_device *dev, void *addr);
    接口能够改变自己的硬件地址的话可以实现该函数,很多设备使用默认的eth_mac_addr函数
  • int (*change_mtu)(struct net_device *dev, int new_mtu);
    接口的MTU发生改变后调用
  • int (*header_cache)(struct neighbour *neigh, struct hh_cache *hh);
    用ARP返回的结果填充hh_cache结构体,以太网驱动使用默认的eth_header_cache函数
  • int (*header_cache_update)(struct hh_cache *hh, struct net_device *dev, unsigned char *haddr);
    目的地址发生改变时用来更新hh_cache结构体
  • int (*hard_header_parse)(struct sk_buff *skb, unsigned char *haddr);
    从skb的包中取出源地址,放在haddr中,返回地址的长度

Utility Fields

保存一些有用的状态信息和统计数据,有些用来供ifconfig和netstat等使用。

  • unsigned long trans_start;
  • unsigned long last_rx;
    保存一个jiffies值,开始发送和收到数据包时更新
  • int watchdog_timeo;
    网络层判断传输超时需要等待的事件,jiffies计
  • void *priv;
    等同于filp->private_data
  • struct dev_mc_list *mc_list;
  • int mc_count;
    处理多播传输,mc_count是mc_list中的表项数
  • spinlock_t xmit_lock;
  • int xmit_lock_owner;
    xmit_lock用来防止多重调用hard_start_xmit函数,xmit_lock_owner是获得xmit_lock的CPU号

Opening and Closing

内核响应ifconfig命令对接口进行打开或者关闭操作。通过ifconfig设置接口的地址时,内核会首先设置接口的地址,然后通过设置IFF_UP位打开网络接口,调用open函数。设备关闭时,ifconfig通过清除IFF_UP位关闭接口,并调用stop函数。
准备好发送数据后,open函数还需要开启接口的发送队列,内核提供了函数void netif_start_queue(struct net_device *dev);,在stop函数中关闭队列void netif_stop_queue(struct net_device *dev);

Packet Transmission

网口最重要的功能就是数据发送和接收,发送过程更容易理解些。
发送指将数据包发送到网络连接的过程,内核需要发送数据时,就像要发送的数据包放在队列中,调用驱动的hard_start_xmit函数发送数据。内核处理的每个数据包都放在套接字缓冲区结构体(socket buffer structure)中。Unix将网络连接抽象为套接字,在网络层的更高一级,每个网络数据包都属于一个套接字。
传送给hard_start_xmit的skb包含需要发送给物理介质的数据包,以及传输层的首部信息。skb->data指向要发送的数据包,skb->len指向数据包的长度,以octet计。
如果发送的数据包的长度小于低层介质支持的最小长度,可能会发生数据泄漏,此时可以对数据包进行填充后再发送。

Controlling Transmission Concurrency

hard_start_xmit通过自旋锁xmit_lock防止函数被并发调用,函数在软件完成对硬件的传输指导后就会返回,而此时硬件很可能并没有完成数据包的发送。
真实的硬件结构通常会异步的传输数据包,用来保存要发送的数据包的存储有限,当存储耗尽,驱动需要告知网络系统不能再进行数据发送。
告知通过netif_stop_queue函数完成,一旦驱动程序停止了发送队列,就需要在未来的某个时间点重新打开,调用函数void netif_wake_queue(struct net_device *dev);
如果需要在hard_start_xmit以外的地方关闭数据包的发送,调用函数void netif_tx_disable(struct net_device *dev);。和netif_stop_queue类似,这个函数还会保证在返回时,其他的CPU也不会在运行hard_start_xmit函数。

Transmission Timeouts

许多驱动需要准备好应对硬件未能做出回应的情况,网口可能忘记自己正在做什么,或者系统可能搞丢一个中断,这些情况在PC上很常见。
许多驱动通过设置计时器来应对这种情况,如果操作在设定的时间内还没有完成,可能有错误发生。由大量计时器控制的状态机组成了网络系统,因此,网络的代码很容易探测发送超时的情况。
也就是说,网络驱动不需要自己探测超时问题,只需要设置超时的时间,即net_device结构体的watchdog_timeo
如果当前的系统时间和trans_start的差值超过了设定的超时时间,网络层就会调用驱动的tx_timeout函数。tx_timeout函数需要解决问题并且保证正在进行的传输正确完成。最重要的是,驱动程序不能丢失对委托给它的任何套接字缓冲区的跟踪。

Scatter/Gather I/O

创建发送的数据包可能需要将多块内存的数据组合到一起,需要大量的数据拷贝工作。如果发送数据包的网口能够进行scatter/gather I/O,就能避免大量的拷贝工作,能够实现从用户空间的缓冲区直接获取发送数据的“零拷贝”传输。
只有设置features域的NETIF_F_SG位,内核才会将散列的数据包传递给驱动的hard_start_xmit方法。设置NETIF_F_SG位后,需要查看skb的“shared info”来判断数据包是否由多个段构成。skb的data域指向一个内存段,其余的段保存在名为frags的共享信息结构体中,其中的每一项都是一个skb_frag_structure结构体:

struct skb_frag_struct {
    struct page *page;
    __u16 page_offset;
    __u16 size;
};

驱动程序需要遍历这些内存段,为每个内存段建立DMA映射,硬件必须组合这些内存段,并将其作为一个数据包发出。

Packet Reception

从网络中接收一个数据包比发送数据包更棘手,因为必须在原子上下文中分配一个sk_buff并递交给更上一层。网络驱动可以通过中断或者轮询两种方式实现数据包的接收。许多驱动通过中断实现,而更高带宽的适配器采用轮询方式。

网络驱动的接收函数首先需要分配(dev_alloc_skb)一个缓冲区来保存收到的数据包,分配函数需要缓冲区的大小作为参数,原子的调用kmalloc函数。分配skb后,将收到的数据包拷贝到缓冲区中。
有些可以进行完全的总线控制I/O的驱动程序可能在接收到数据包前提前分配好套接字缓冲区,然后命令接口将收到的数据包直接放到skb中。为了配合,网络层会将所有的套接字缓冲区都分配在可进行DMA的区域中。但是这种方式需要提前确定收到的数据包的大小。

网络在解析数据包时需要提前了解一些信息,必须在向上递交缓冲区前设置好devprotocol域,以太网的辅助函数eth_type_trans可以获取到protocol的正确值,之后还需要指明校验值的生成方式,即skb->ip_summed的值:

  • CHECKSUM_HW
    硬件已经计算了校验值,比如SPARC HME接口
  • CHECKSUM_NONE
    校验值还没有计算,必须由系统软件完成
  • CHECKSUM_UNNECESSARY
    不要计算校验值

net_devicefeatures域指明的是设备发送数据包时采用的校验方式,此处设置的是收到的数据包的校验方式。

然后,驱动需要更新统计计数器来记录已经收到一个数据包,统计数据的数据结构中最重要的域为rx_packetsrx_bytestx_packetstx_bytes,见名知义。

数据包接收的最后一步通过netif_rx完成,函数会将套接字缓冲区递交给更上一层,并返回数据包接收的结果。

The Interrupt Handler

许多硬件结构通过中断来控制,硬件通过中断告知处理器新数据包的到来或者数据包的发送已经完成,也可以通过中断通知错误,连接状态的改变等等。
中断例程可以通过物理设备上的状态寄存器来分别新数据包到来的中断和发送完成的中断。
数据传输完成后,将不再使用的skb反还给系统,有三个函数可以实现:

  • dev_kfree_skb(struct sk_buff *skb);
    如果确定代码不会运行在中断上下文中使用该函数释放skb
  • dev_kfree_skb_irq(struct sk_buff *skb);
    如果确定会在中断处理函数中释放缓冲区,使用该函数
  • dev_kfree_skb_any(struct sk_buff *skb);
    可以运行在中断或者非中断上下文中

Receive Interrupt Mitigation

通过上述方式完成的网络设备驱动,每收到一个数据包,就会向处理器产生一个中断,在大多数情况下,这样可以。但是对于高带宽的设备而言,每秒可能有几千个包到达,如果每个包都产生一个中断,会严重影响系统的性能。
为了解决这个问题,网络子系统的开发人员创造了基于轮询的替代接口(alternative interface based on polling,NAPI)。对于多说情况下没有任务可做的外设而言,轮询不是一个好的解决方案;而对于高速的接口,轮询效率更高。

支持NAPI的接口需要在初始化时设置net_device中的poll为轮询函数,weight为结构的相对重要性:资源紧张时接口能够接收的流量,不能大于接口所能保存的最大包数。

然后还要更改NAPI驱动的中断处理函数,当数据包到来时,中断处理函数不能处理数据包,而是关闭接收中断,告知内核调用轮询函数。
轮询函数int (*poll)(struct net_device *dev, int *budget);中的budget函数限定能够传输给内核的最大包数,在net_device结构体中,quota指定了另外一个最大值,最终的结果应该是二者间的最小值。轮询函数通过netif_receive_skb将包发送给内核,而不是netif_rx
如果轮询函数能够在限定的时间内完成所有包的处理,需要打开接收中断,调用netif_rx_complete关闭轮询。
网络子系统会保证设备的poll函数不会被不同的处理器同时调用,但是可能和驱动的其他函数并发执行。

Changes in Link State

许多涉及到物理连接的网络技术都会提供一个载波状态(carrier state),载波信号因为着硬件的存在,可以进行工作。以太网适配器能够探测到线路上的载波信号,如果有用户正在使用线路,载波信号会消失,连接断开。驱动程序可以显式改变载波信号的状态,进而控制连接的状态。

void netif_carrier_off(struct net_device *dev);
void netif_carrier_on(struct net_device *dev);

还可以通过函数int netif_carrier_ok(struct net_device *dev);判断载波的状态。

The Socket Buffers

套接字缓冲区是Linux内核的网络子系统的核心,定义在linux/skbuff.h中。

The Important Fields

这些结构体成员驱动可能需要访问:

  • struct net_device *dev;
    接收或者发送此缓冲区的设备
  • union {} h;
  • union {} nh;
  • union {} mac;
    指向不同层网络的首部的指针,比如h可能包含传输层的首部指针struct tcphdr *th,nh包含网络层的首部指针,mac包含连接层的首部指针。
  • unsigned char *head;
  • unsigned char *data;
  • unsigned char *tail;
  • unsigned cahr *end;
    指向包中数据的内存地址。head为分配的空间的起始地址,data是有效octet的起始地址,tail是有效octet的结束地址,endtail能达到的最大地址。
  • unsigned int len;
  • unsigned int data_len;
    len是包中所有数据的长度,data_len是独立的段中保存的包的长度,没有使用scatter/gather I/O时为0
  • unsigned char ip_summed;
    包的校验类型
  • unsigned char pkt_type;
    包的类型,驱动应该设置为PACKET_HOSTPACKET_OTHERHOSTPACKET_BROADCASTPACKET_MULTICAST其中之一。
  • shinfo(struct sk_buff *skb);
  • unsigned int shinfo(skb)->nr_frags;
  • skb_frag_t shinfo(skb)->frags;
    处于性能考虑,一些skb信息存于内存中紧邻着skb的独立结构体中,可以由多个skb共享,因此唤作共享信息,只能通过shinfo宏访问

Functions Acting on Socket Buffers

使用skb的网络设备需要通过官方接口对skb进行操作:

  • struct sk_buff *alloc_skb(unsigned int len, int priority);
  • struct sk_buff *dev_alloc_skb(unsigned int len);
    分配一个缓冲区,alloc_skb会将缓冲区的datatail都指向headdev_alloc_skb以GFP_ATOMIC权限调用alloc_skb函数,在datahead间保留一段空间供网络层加速使用,驱动程序不能使用。
  • 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);
    释放一个缓冲区,kfree_skb由内核内部使用,驱动程序在非中断上下文中使用dev_kfree_skb,中断上下文中使用dev_kfree_skb_irq,二者都可使用dev_kfree_skb_any
  • unsigned char *skb_put(struct sk_buff *skb, int len);
  • unsigned char *__skb_put(struct sk_buff *skb, int len);
    更新skb的taillen,向缓冲区的尾部添加数据,返回之前的tail值,即新创建的数据空间。skb_put会保证数据长度适合缓冲区,而__skb_put不会。
  • unsigned char *skb_push(struct sk_buff *skb, int len);
  • unsigned char *__skb_push(struct sk_buff *skb, int len);
    减少data增加len,向包的开始添加数据,返回创建的数据区域
  • int skb_tailroom(struct sk_buff *skb);
    返回缓冲区中可用来保存数据的空间,如果数据放入过量的数据,系统会panic
  • int skb_headroom(struct sk_buff *skb);
    返回data前可用的空间
  • void skb_reserve(struct sk_buff *skb, int len);
    增加datatail,可以用来预留headroom
  • unsigned char *skb_pull(struct sk_buff *skb, int len);
    从包的头部移除数据,减少len增加data
  • int skb_is_nonlinear(strcut sk_buff *skb);
    判断skb是否位于多个段中
  • int skb_headlen(struct sk_buff *skb);
    返回skb中第一个段的长度
  • void *kmap_skb_frag(skb_frag_t *frag);
  • void kunmap_skb_frag(void *vaddr);
    如果必须在内核内访问非线性skb中的段,可以通过这些函数映射到内核空间

MAC Address Resolution

以太网的通信需要解决将MAC地址和IP地址相关联的问题,涉及到ARP,没有ARP的以太网首部(例如plip)和非以太网首部。

Using ARP with Ethernet

ARP(Address Resolution Protocol)是地址解析协议,由内核进行管理,以太网接口不需进行操作。只要在打开时正确设置net_deviceaddraddr_len,驱动就无需担心将IP地址转化为MAC地址的过程,ether_setup函数会正确设置hard_headerrebuild_header函数。
尽管内核会处理地址解析的细节,但是会调用接口的驱动来构建数据包;毕竟,驱动程序了解物理层首部的细节,而网络代码的作者尽力将内核的其他部分隔离在外。为此,内核调用驱动的hard_header函数来使用ARP查询的结果。

Overriding ARP

有些点对点网口(例如plip)只需要以太网的首部信息,如果不想使用ARP,驱动程序需要替换掉默认的dev->hard_header函数。

Non-Ethernet Headers

硬件首部包含除了目的地址之外的其他信息,最重要的是通信的协议。当网络连接的另一端收到数据包时,驱动的接收函数需要正确设置skb->protocol, skb->pkt_type和skb->mac.raw。

Custom ioctl Commands

针对一个套接字调用ioctl系统调用时,命令号是linux/sockio.h中定义的一个符号,sock_ioctl函数直接调用特定协议的函数。
协议层不能识别的ioctl命令会传递到设备层,这些设备相关的命令会从用户空间接收第三个参数,类型为struct ifreq *,定义在linux/if.h中。
识别收到的命令后,接口驱动的dev->do_ioctl函数会被调用,和通用的ioctl函数接收的struct ifreq *相同:int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);ifr指向用户传递的参数在内核中的拷贝;do_ioctl返回后,结构体会重新拷贝回用户空间。因此,驱动可以用私有的命令接收数据和发送数据。

Statistical Information

驱动需要的最后一个函数是get_stats,函数返回指向设备的统计数据的指针。由于统计数据保存在设备的数据结构体内,函数的实现非常简单。结构体struct net_device_stats包含以下成员:

  • unsigned long rx_packets;
  • unsigned long tx_packets;
    接口成功收到和发送的数据包的总数
  • unsigned long rx_bytes;
  • unsinged long tx_bytes;
    接口收到和发送的字节数
  • unsigned long rx_errors;
  • unsigned long tx_errors;
    接收和发送过程中产生的错误,传输出错的诱因无穷无尽,net_device_stats包含6个错误计数器
  • unsigned long rx_dropped;
  • unsigned long tx_dropped;
    接收和发送过程中丢弃的数据包数,没有足够的内存时会丢弃数据包,tx_dropped很少使用
  • unsigned long collisions;
    由于传输介质阻塞导致的碰撞数
  • unsigned long multicast;
    收到的多播包数

get_stats函数任何时刻都可以调用,即使接口已经关闭,因此只要net_device结构体还存在,驱动程序就要保持统计信息。

Multicast

多播数据包会被多个但不是所有的主机收到,通过将特殊的硬件地址分配给一组主机来实现,发送到特殊地址的数据包会被组内的所有主机收到。在以太网中,多播地址的第一个octet的最低位为1,而所有设备的物理地址此位为0。
内核负责随时追踪有用的多播地址,多播地址的列表和正在运行的程序及用户有关,会频繁变化,驱动程序需要接受有用的多播地址的列表并将发送到这些地址的所有数据包递交给内核。驱动如何实现多播列表依赖于底层硬件的工作方式,从多播的角度来讲,硬件通常分为三类:

  • 不能处理多播的接口,这些接口要么接收发送到自己的物理地址(包括广播)的数据包,要么接收所有的数据包。这种设备的驱动程序不会设置IFF_MULTICAST位
  • 接口可以从数据包中区分多播数据包,这些接口可以接收每个多播数据包,然后让软件决定地址是否对主机有用,这种方式带来的开销可以接收,因为网络的多播数据包数量很少
  • 接口的硬件能够探测多播地址,这种接口可以设置需要接收的多播地址列表,忽略其他多播数据包,不会带来开销

内核通过支持第三种接口来尽量利用高层接口的功能,因此会在有效多播列表变化时通知并传递给驱动。

Kernel Support for Multicasting

多播数据包通过设备函数,数据结构,和设备标志符实现:

  • void (*dev->set_multicast_list)(struct net_device *dev);
    设备相关的机器地址列表变化时调用,dev->flags变化时也会调用,因为有些标志位需要重新设置物理过滤器
  • struct dev_mc_list *dev->mc_list;
    和设备相关的所有多播地址的列表
  • int dev->mc_count;
    多播列表中的表项数
  • IFF_MULTICAST
    只有设置此位的接口才会被要求处理多播数据包,多播列表可能在设备不活跃的状态下调用
  • IFF_ALLMULTI
    网络软件(networking software)用来高速驱动从网络中获取所有的多播数据包,mc_list不再起作用
  • IFF_PROMISC
    进入混杂模式,网络上的所有数据包都会接收

struct dev_mc_list定义在linux/netdevice.h

struct dev_mc_list {
    struct dev_mc_list *next;   /* 表中的下一个地址 */
    __u8 dmi_addr[MAX_ADDR_LEN];    /* 硬件地址 */
    unsigned char dmi_addrlen;  /* 地址长度 */
    int dmi_users;  /* 用户的数量 */
    int dmi_gusers; /* 组的数量 */
};

A Few Other Details

Media Independent Interface Support

介质无关接口(MII)是IEEE 802.3的一个标准,描述了以太网的收发器如何与网络控制器交互,市场的许多产品都遵守这个标准,内核提供了通用的MII层供驱动开发人员使用。
要使用通用的MII层,需要包含linux/mii.h头文件,根据收发器的物理ID填充mii_if_info结构体,以及两个函数:

int (*mdio_read)(struct net_device *dev, int phy_id, int location);
void (*mdio_write)(struct net_device *dev, int phy_id, int location, int val);

实现和MII接口的交互。
通用MII层还提供了查询和修改收发器运行状态的函数,通常和ethtool一起工作。

Ethtool Support

ethtool在驱动程序支持的情况下供系统管理员控制网口的运行,包括速度、介质类型、双工操作、创建DMA环、硬件校验、局域网唤醒操作。
ethtool定义在linux/ethtool.h中,核心是结构体ethtool_ops,包含了24个操作。设备的驱动需要通过宏SET_ETHTOOL_OPS设置net_deviceethtool_ops成员来支持ethtoolethtool的方法在设备关闭时也能调用。

Netpoll

网络轮询用来帮助内核在完整的网络子系统和I/O子系统不可用的时候发送和接收数据包,用来支持远程网络控制台和远程内核调试。
支持网络轮询的驱动需要实现poll_controller方法,在缺少设备中断的时候和控制器保持一致,几乎所有的poll_controller函数具有以下形式:

void my_poll_controller(struct net_device *dev)
{
    disable_device_interrupts(dev);
    call_interrrupt_handler(dev->irq, dev, NULL);
    reenable_device_interrupts(dev);
}

可以看到,poll_controller只是模拟中断处理函数而已。

猜你喜欢

转载自www.cnblogs.com/adera/p/10150733.html
ldd