网络设备是完成用户数据包在网络媒介上发送和接收的设备,它将上层协议传递下来的数据包以特定的媒介访问控制方式进行发送,并将接收到的数据包传递给上层协议。应用程序使用套接字(socket)完成与网络设备的接口。
Linux网络设备驱动的结构
Linux 系统对网络设备驱动的驱动体系从上到下划分为4个层次,依次为为网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层和网络设备与媒介层,4层的作用如下:
网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议为 ARP 还是 IP,都通过dev_queue_init()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。
网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层中各函数的容器。实际上,网络设备接口层从宏观上规划了具体操作硬件的设备驱动功能层的结构。
设备驱动功能层各函数是网络设备接口层 net_device 数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过hard_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作
网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数物理上驱动。
网络协议接口层
网络协议接口层最主要的功能是给上层协议提供透明的数据包发送和接收接口,当上层 ARP 或 IP协议需要发送数据包时,它将调用网络协议接口层的dev_queue_xmit()函数发送该数据包,同时需传递给该函数一个指向 struct sk_buff 数据结构的指针。 dev_queue_xmit()函数的原型为:
dev_queue_xmit (struct sk_buff * skb );
同样地,上层对数据包的接收也通过向 netif_rx()函数传递一个 struct sk_buff 数据结构的指针来完成。netif_rx()函数的原型为:
int netif_rx(struct sk_buff *skb);
sk_buff 结构体非常重要,它的含义为“套接字缓冲区”,用于在 Linux 网络子系统中的各层之间传递数据,是 Linux 网络子系统数据传递的“中枢神经”。当发送数据包时, Linux 内核的网络处理模块必须建立一个包含要传输的数据包的 sk_buff,然后将sk_buff 递交给下层,各层在 sk_buff 中添加不同的协议头直至交给网络设备发送。同样地,当网络设备从网络媒介上接收到数据包后,它必须将接收到的数据转换为 sk_buff 数据结构并传递给上层,各层剥去相应的协议头直至交给用户。
1、套接字缓冲区成员
sk_buff结构体包含的主要成员如下:
(1)各层协议头 h、 nh 和 mac。sk_buff 结构体中定义了 3 个协议头以对应于网络协议的不同层次,这 3 个协议头为传输层 TCP/UDP及 ICMP 和 IGMP协议头 h、网络层协议头 nh 和链路层协议头 mac。这 3 个协议头数据结构都被定义为联合体,如代码清单所示:
union{
struct tcphdr *th; /* TCP 头部 */
struct udphdr *uh; /* UDP 头部*/
struct icmphdr *icmph; /* ICMP 头部*/
struct igmphdr *igmph; /* IGMP 头部,Internet组管理协议*/
struct iphdr *ipiph; /* IP 头部*/
struct ipv6hdr *ipv6h; /* IPv6 头部*/
unsigned char *raw; /* 数据链路层头部*/
}h;
union{
struct iphdr *iph; /* IP 头部*/
struct ipv6hdr *ipv6h; /* IPv6 头部*/
struct arphdr *arph; /*ARP 头部*/
unsigned char *raw; /* 数据链路层头部*/
}nh;
union{
unsigned char *raw; /* 数据链路层头部*/
} mac;
(2)数据缓冲区指针 head、 data、 tail 和 end。
Linux 内核必须分配用于容纳数据包的缓冲区, sk_buff 结构体定义了 4 个指向这片缓冲区不同位置的指针 head、 data、 tail 和 end。
head 指针指向内存中已分配的用于承载网络数据的缓冲区的起始地址, sk_buff 和相关数据块在分配之后,该指针的值就被固定了。
data 指针则指向对应当前协议层有效数据的起始地址。每个协议层的有效数据含义并不相同,各层的有效数据信息包含的内容如下。
对于传输层而言,用户数据和传输层协议头属于有效数据。
对于网络层而言,用户数据、传输层协议头和网络层协议头是其有效数据。
对于数据链路层而言,用户数据、传输层协议头、网络层协议头和链路层头部都属于有效数据。
因此, data 指针的值需随着当前拥有 sk_buff 的协议层的变化进行相应的移动。
tail 指针则指向对应当前协议层有效数据负载的结尾地址,与 data 指针对应。
end 指针指向内存中分配的数据缓冲区的结尾,与 head 指针对应。和 head 指针一样, sk_buff 被分配之后, end 指针的值也就固定不变了。
(3)长度信息 len、 data_len、 truesize。
sk_buff 结构体中定义的 len 是指数据包有效数据的长度,包括协议头和负载(Payload)。为了支持数据包的分片存放, sk_buff 中增加了 data_len 这个成员,它记录分片的数据长度。
truesize 表示缓存区的整体长度,置为 sizeof(struct sk_buff)加上传入 alloc_skb()函数或 dev_alloc_skb()函数的长度。
2、套接字缓冲区操作
Linux 套接字缓冲区支持分配、释放、指针移动等功能函数。
(1)分配。
Linux 内核用于分配套接字缓冲区的函数有:
struct sk_buff *alloc_skb(unsigned int len,int priority);
struct sk_buff *dev_alloc_skb(unsigned int len);
alloc_skb()函数分配一个套接字缓冲区和一个数据缓冲区,参数 len 为数据缓冲区的空间大小,以 16字节对齐,参数 priority 为内存分配的优先级。
dev_alloc_skb()函数只是以 GFP_ATOMIC 优先级(代表分配过程不能被中断)调用上面的 alloc_skb()函数,并保存 skb->head 和 skb->data 之间的 16 个字节。
分配成功之后,因为还没有存放具体的网络数据包,所以 sk_buff 的 data、 tail 指针都指向存储空间的起始地址 head,而 len 的大小则为 0。
(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);
上述函数用于释放被 alloc_skb()函数分配的套接字缓冲区和数据缓冲区。
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 套接字缓冲区中的数据缓冲区指针移动操作包括 put(放置)、 push(推)、 pull(拉)、 reserve(保留)等。
① put 操作
数据缓冲区指针 put 操作以下列函数完成:
unsigned char *skb_put(struct sk_buff *skb, unsigned int len);
unsigned char *_ _skb_put(struct sk_buff *skb, unsigned int len);
上述函数将 tail 指针下移,增加 sk_buff 的 len 值,并返回 skb->tail 的当前值。 skb_put()和__ skb_put()的区别在于前者会检测放入缓冲区的数据, 而后者不会检查。 这两个函数主要用于在缓冲区尾部添加数据。
② push 操作
数据缓冲区指针 push 操作以下列函数完成:
unsigned char *skb_push(struct sk_buff *skb, unsigned int len);
unsigned char *_ _skb_push(struct sk_buff *skb, unsigned int len);
与 skb_put()和_ _skb_put()不同, skb_push()和_ _skb_push()会将 data 指针上移,因此也要增加 sk_buff的 len 值。 push 操作在存储空间的头部增加一段可以存储网络数据包的空间,而 put 操作则在存储空间的尾部增加一段可以存储网络数据包的空间,因此主要用于在数据包发送时添加头部。 skb_push()与_skb_push()的区别和 skb_put()和_ _skb_put()的区别类似。
③ pull 操作
数据缓冲区指针 pull 操作以下列函数完成:
unsigned char * skb_pull(struct sk_buff *skb, unsigned int len);
skb_pull()函数将 data 指针下移,并减小 skb 的 len 值。这个操作一般用于下层协议向上层协议移交数据包,使 data 指针指向上一层协议的协议头。
④ reserve 操作
数据缓冲区指针 reserve 操作以下列函数完成:
void skb_reserve(struct sk_buff *skb, unsigned int len);
skb_reserve()函数将 data 指针和 tail 指针同时下移,这个操作主要用于在存储空间的头部预留 len 长度的空隙
网络设备接口层
网络设备接口层的主要功能是为千变万化的网络设备定义了统一、 抽象的数据结构 net_device 结构体,以不变应万变,实现多种硬件在软件层次上的统一。
net_device 结构体在内核中指代一个网络设备, 网络设备驱动程序只需通过填充 net_device 的具体成员并注册 net_device 即可实现硬件操作函数与内核的挂接。net_device 本身是一个巨型结构体,包含网络设备的属性描述和操作接口。
(1)全局信息。
char name[IFNAMESIZ];
name 是网络设备的名称。
int (*init)(struct net_device *dev);
init 为设备初始化函数指针,如果这个指针被设置了,则网络设备被注册时将调用该函数完成对net_device 结构体的初始化。
(2)硬件信息。
unsigned long mem_end;
unsigned long mem_start;
mem_start 和 mem_end 分别定义了设备所使用的共享内存的起始和结束地址。
unsigned long base_addr;
unsigned char irq;
unsigned char if_port;
unsigned char dma;
base_addr 为网络设备 I/O 基地址。
irq 为设备使用的中断号。
if_port 指定多端口设备使用哪一个端口,该字段仅针对多端口设备。例如,如果设备同时支持
IF_PORT_10BASE2(同轴电缆)和 IF_PORT_10BASET(双绞线),则可使用该字段。
dma 指定分配给设备的 DMA 通道。
(3)接口信息。
unsigned short hard_header_len;
hard_header_len 是网络设备的硬件头长度, 在以太网设备的初始化函数中, 该成员被赋为 ETH_HLEN,即 14。
unsigned short type;
type 是接口的硬件类型。
unsigned mtu;
mtu 指最大传输单元(MTU)。
unsigned char dev_addr[MAX_ADDR_LEN];
unsigned char broadcast[MAX_ADDR_LEN];
dev_addr[ ]、 broadcast[ ]无符号字符数组, 分别用于存放设备的硬件地址和广播地址。 对于以太网而言,这两个地址的长度都为 6 个字节。以太网设备的广播地址为 6 个 0xFF,而 MAC 地址需由驱动程序从硬件上读出并填充到 dev_addr[ ]中。
unsigned short flags;
flags 指网络接口标志,以 IFF_(interface flags)开头,部分标志由内核来管理,其他的在接口初始化时被设置以说明设备接口的能力和特性。接口标志包括 IFF_UP(当设备被激活并可以开始发送数据包时,内核设置该标志)、 IFF_AUTOMEDIA(设备可在多种媒介间切换)、 IFF_BROADCAST(允许广播)、IFF_DEBUG(调试模式, 可用于控制 printk 调用的详细程度)、 IFF_LOOPBACK(回环)、 IFF_MULTICAST(允许组播)、 IFF_NOARP(接口不能执行 ARP)、IFF_POINTOPOINT(接口连接到点到点链路)等。
(4)设备操作函数。
int (*open)(struct net_device *dev);
int (*stop)(struct net_device *dev);
open()函数的作用是打开网络接口设备,获得设备需要的 I/O 地址、 IRQ、 DMA 通道等。
stop()函数的作用是停止网络接口设备,与 open()函数的作用相反。
int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);
hard_start_xmit()函数会启动数据包的发送,当系统调用驱动程序的 hard_start_xmit()函数时,需要向其传入一个 sk_buff 结构体指针,以使得驱动程序能获取从上层传递下来的数据包。
void (*tx_timeout)(struct net_device *dev);
当数据包的发送超时时, tx_timeout ()函数会被调用,该函数需采取重新启动数据包发送过程或重新启动硬件等策略来恢复网络设备到正常状态。
int (*hard_header) (struct sk_buff *skb,struct net_device *dev,unsigned short type,void *daddr,void *saddr,unsigned len);
hard_header()函数完成硬件帧头填充,返回填充的字节数。传入该函数的参数包括 sk_buff 指针、设备指针、协议类型、目的地址、源地址以及数据长度。对于以太网设备而言,将内核提供的 eth_header()函数赋值给 hard_header 指针即可。
struct net_device_stats* (*get_stats)(struct net_device *dev);
get_stats()函数用于获得网络设备的状态信息,它返回一个 net_device_stats 结构体。 net_device_stats 结构体保存了网络设备详细的流量统计信息,如发送和接收到的数据包数、字节数等。
int (*do_ioctl)(struct net_device *dev, struct ifreq *ifr, int cmd);
int (*set_config)(struct net_device *dev, struct ifmap *map);
int (*set_mac_address)(struct net_device *dev, void *addr);
do_ioctl()函数用于进行设备特定的 I/O 控制。
set_config()函数用于配置接口,可用于改变设备的 I/O 地址和中断号。
set_mac_address()函数用于设置设备的 MAC 地址。
int (*poll)( struct net_device *dev,int quota);
对于 NAPI(网络中断缓和)兼容的设备驱动,将以轮询方式操作接口,接收数据包。 NAPI 是 Linux系统上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据包,而是采用首先借助中断唤醒数据包接收的服务程序,然后以轮询方式获取数据包。
net_device 结构体的上述成员需要在设备初始化时被填充。
(5)辅助成员。
unsigned long trans_start;
unsigned long last_rx;
trans_start 记录最后的数据包开始发送时的时间戳, last_rx 记录最后一次接收到数据包时的时间戳,这两个时间戳记录的都是 jiffies,驱动程序应维护这两个成员。
void *priv;
priv 为设备的私有信息指针,与 filp->private_data 的地位相当。设备驱动程序中应该以 netdev_priv()函数获得该指针。
spinlock_t xmit_lock;
int xmit_lock_owner;
xmit_lock 是避免 hard_start_xmit()函数被同时多次调用的自旋锁。 xmit_lock_owner 则指当前拥有xmit_lock 自旋锁的 CPU 的编号。
设备驱动功能层
net_device 结构体的成员(属性和函数指针)需要被设备驱动功能层的具体数值和函数赋予。对于具体的设备 xxx,工程师应该编写设备驱动功能层的函数,这些函数形如 xxx_open()、 xxx_stop()、 xxx_tx()、xxx_ hard_header()、 xxx_get_stats()、 xxx_tx_timeout()、 xxx_poll()等。
由于网络数据包的接收可由中断引发,设备驱动功能层中另一个主体部分将是中断处理函数,它负责读取硬件上接收的数据包并传送给上层协议,可能包含 xxx_interrupt()和 xxx_rx()函数,前者完成中断类型判断等基本的工作,后者则需完成数据包的生成和递交上层等复杂工作。
对于特定的设备, 我们还可以定义其相关私有数据和操作, 并封装为一个私有信息结构体 xxx_private,让其指针被赋值给 net_device 的 priv 成员。 xxx_private 结构体中可包含设备特殊的属性和操作、自旋锁与信号量、定时器以及统计信息等
网络设备与媒介层
网络设备与媒介层直接对应于实际的硬件设备。为了给设备的物理配置和寄存器操作一个更一般的描述,我们可以定义一组宏和一组访问设备内部寄存器的函数,具体的宏和函数与特定的硬件紧密相关。
网络驱动设备的注册与注销
网络设备驱动的注册与注销使用成对出现的 register_netdev()和 unregister_netdev()函数完成,这两个函数的原型为:
int register_netdev(struct net_device *dev);
void unregister_netdev(struct net_device *dev);
这两个函数都接收一个 net_device 结构体指针为参数,可以利用以下函数完成net_device的生成和成员的赋值。
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void(*setup)(struct net_device*));
struct net_device *alloc_etherdev(int sizeof_priv);
alloc_netdev()函数生成一个 net_device 结构体,对其成员赋值并返回该结构体的指针。第一个参数为设备私有成员的大小,第二个参数为设备名,第三个参数为 net_device 的 setup()函数指针。 setup()函数接收的参数也为 struct net_device 指针,用于预置 net_device 成员的值。
alloc_etherdev()是 alloc_netdev()针对以太网的“快捷”函数,这从 alloc_etherdev()的源代码可以看出:
struct net_device *alloc_etherdev(int sizeof_priv)
{
/* 以 ether_setup 为 alloc_netdev 的 setup 参数 */
return alloc_netdev(sizeof_priv, "eth%d", ether_setup);
}
完成与 alloc_enetdev()和 alloc_etherdev()函数相反功能,即释放 net_device 结构体的函数为:
void free_netdev(struct net_device *dev);
net_device 结构体的分配和网络设备驱动注册需在网络设备驱动程序的模块加载函数中进行,而net_device 结构体的释放和网络设备驱动的注销则需在模块卸载函数中完成。网络设备驱动的模块加载/卸载函数模板如下:
int xxx_init_module(void)
{
...
/* 分配 net_device 结构体并对其成员赋值 */
xxx_dev = alloc_netdev(sizeof(struct xxx_priv), "sn%d", xxx_init);
if (xxx_dev == NULL)
... /* 分配 net_device 失败 */
/* 注册 net_device 结构体 */
if ((result = register_netdev(xxx_dev)))
...
}
void xxx_cleanup(void)
{
...
/* 注销 net_device 结构体 */
unregister_netdev(xxx_dev);
/* 释放 net_device 结构体 */
free_netdev(xxx_dev);
}
网络设备的初始化
网络设备的初始化主要需要完成如下几个方面的工作。
进行硬件上的准备工作,检查网络设备是否存在,如果存在,则检测设备所使用的硬件资源。
进行软件接口上的准备工作,分配 net_device 结构体并对其数据和函数指针成员赋值。
获得设备的私有信息指针并初始化其各成员的值。如果私有信息中包括自旋锁或信号量等并发或同步机制,则需对其进行初始化。对 net_device 结构体成员及私有数据的赋值都可能需要与硬件初始化工作协同进行,即硬件检测出了相应的资源,需要根据检测结果填充 net_device 结构体成员和私有数据。网络设备驱动的初始化函数模板如下:
void xxx_init(struct net_device *dev)
{
/*设备的私有信息结构体*/
struct xxx_priv *priv;
/* 检查设备是否存在和设备所使用的硬件资源 */
xxx_hw_init();
/* 初始化以太网设备的公用成员 */
ether_setup(dev);
/*设置设备的成员函数指针*/
dev->open = xxx_open;
dev->stop = xxx_release;
dev->set_config = xxx_config;
dev->hard_start_xmit = xxx_tx;
dev->do_ioctl = xxx_ioctl;
dev->get_stats = xxx_stats;
dev->change_mtu = xxx_change_mtu;
dev->rebuild_header = xxx_rebuild_header;
dev->hard_header = xxx_header;
dev->tx_timeout = xxx_tx_timeout;
dev->watchdog_timeo = timeout;
/*如果使用 NAPI,设置 pool 函数*/
if (use_napi)
{
dev->poll = xxx_poll;
}
/* 取得私有信息,并初始化它*/
priv = netdev_priv(dev);
... /* 初始化设备私有数据区 */
}
其中,xxx_hw_init()函数完成硬件相关的初始化操作,如下所示。
探测 xxx 网络设备是否存在。探测的方法类似于数学上的“反证法”,即先假设存在设备 xxx,访问该设备,如果设备的表现与预期的一致,就确定设备存在;否则,假设错误,设备 xxx 不存在。
探测设备的具体硬件配置。一些设备驱动编写得非常通用,对于同类的设备使用统一的驱动,我们需要在初始化时探测设备的具体型号。另外,即便是同一设备,在硬件上的配置也可能不一样,我们也可以探测设备所使用的硬件资源。
申请设备所需要的硬件资源,如用 request_region()函数进行 I/O 端口的申请等,但是这个过程可以放在设备的打开函数 xxx_open()中完成。
网络设备的打开与释放
网络设备的打开函数需要完成如下工作。
使能设备使用的硬件资源,申请 I/O 区域、中断和 DMA 通道等。
调用 Linux 内核提供的 netif_start_queue()函数,激活设备发送队列。
网络设备的关闭函数需要完成如下工作。
调用 Linux 内核提供的 netif_stop_queue()函数,停止设备传输包。
释放设备所使用的I/O区域、中断和 DMA 资源。
Linux 内核提供的 netif_start_queue()和 netif_stop_queue()两个函数的原型为:
void netif_start_queue(struct net_device *dev);
void netif_stop_queue (struct net_device *dev);
网络设备的打开和释放函数模板如下:
int xxx_open(struct net_device *dev)
{
/* 申请端口、 IRQ 等,类似于 fops->open */
ret = request_irq(dev->irq, &xxx_interrupt, 0, dev->name, dev);
...
netif_start_queue(dev);
...
}
int xxx_release(struct net_device *dev)
{
/* 释放端口、 IRQ 等,类似于 fops->close */
free_irq(dev->irq, dev);
...
netif_stop_queue(dev); /* can't transmit any more */
...
}
数据发送流程
Linux 网络子系统在发送数据包时,会调用驱动程序提供的 hard_start_transmit()函数,该函数用于启动数据包的发送。在设备初始化的时候,这个函数指针需被初始化指向设备的 xxx_tx()函数。
网络设备驱动完成数据包发送的流程如下。
(1)网络设备驱动程序从上层协议传递过来的 sk_buff 参数获得数据包的有效数据和长度,将有效数据放入临时缓冲区。
(2)对于以太网,如果有效数据的长度小于以太网冲突检测所要求数据帧的最小长度 ETH_ZLEN则给临时缓冲区的末尾填充 0。
(3)设置硬件的寄存器,驱使网络设备进行数据发送操作。
网络设备驱动程序的数据包发送函数模板如下:
int xxx_tx(struct sk_buff *skb, struct net_device *dev)
{
int len;
char *data, shortpkt[ETH_ZLEN];
/* 获得有效数据指针和长度 */
data = skb->data;
len = skb->len;
if (len < ETH_ZLEN)
{
/* 如果帧长小于以太网帧最小长度,补 0 */
memset(shortpkt, 0, ETH_ZLEN);
memcpy(shortpkt, skb->data, skb->len);
len = ETH_ZLEN;
data = shortpkt;
}
dev->trans_start = jiffies; /* 记录发送时间戳 */
/* 设置硬件寄存器让硬件把数据包发送出去 */
xxx_hw_tx(data, len, dev);
...
}
当数据传输超时时,意味着当前的发送操作失败,此时,数据包发送超时处理函数 xxx_tx_ timeout()将被调用。这个函数需要调用 Linux 内核提供的 netif_wake_queue()函数重新启动设备发送队列,如代码所示:
void xxx_tx_timeout(struct net_device *dev)
{
...
netif_wake_queue(dev); /* 重新启动设备发送队列 */
}
数据接收流程
网络设备接收数据的主要方法是由中断引发设备的中断处理函数,中断处理函数判断中断类型,如果为接收中断,则读取接收到的数据,分配 sk_buffer 数据结构和数据缓冲区,将接收到的数据复制到数据缓冲区, 并调用 netif_rx()函数将 sk_buffer 传递给上层协议。
网络设备驱动的中断处理函数模板如下:
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
...
switch (status &ISQ_EVENT_MASK)
{
case ISQ_RECEIVER_EVENT:
/* 获取数据包 */
xxx_rx(dev);
break;
/* 其他类型的中断 */
}
}
static void xxx_rx(struct xxx_device *dev)
{
...
length = get_rev_len (...);
/* 分配新的套接字缓冲区 */
skb = dev_alloc_skb(length + 2);
skb_reserve(skb, 2); /* 对齐 */
skb->dev = dev;
/* 读取硬件上接收到的数据 */
insw(ioaddr + RX_FRAME_PORT, skb_put(skb, length), length >> 1);
if (length &1)
skb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT);
/* 获取上层协议类型 */
skb->protocol = eth_type_trans(skb, dev);
/* 把数据包交给上层 */
netif_rx(skb);
/* 记录接收时间戳 */
dev->last_rx = jiffies;
...
}
如果是 NAPI 兼容的设备驱动,则可以通过 poll 方式接收数据包。这种情况下,我们需要为该设备驱动提供 xxx_poll()函数,如代码如下:
static int xxx_poll(struct net_device *dev, int *budget)
{
int npackets = 0, quota = min(dev->quota, *budget);
struct sk_buff *skb;
struct xxx_priv *priv = netdev_priv(dev);
struct xxx_packet *pkt;
while (npackets < quota && priv->rx_queue)
{
/*从队列中取出数据包*/
pkt = xxx_dequeue_buf(dev);
/*接下来的处理,和中断触发的数据包接收一致*/
skb = dev_alloc_skb(pkt->datalen + 2);
if (!skb)
{
...
continue;
}
skb_reserve(skb, 2);
memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
/*调用 netif_receive_skb 而不是 net_rx 将数据包交给上层协议*/
netif_receive_skb(skb);
/*更改统计数据 */
priv->stats.rx_packets++;
priv->stats.rx_bytes += pkt->datalen;
xxx_release_buffer(pkt);
}
/* 处理完所有数据包*/
*budget -= npackets;
dev->quota -= npackets;
if (!priv->rx_queue)
{
netif_rx_complete(dev);
xxx_enable_rx_int (…); /* 再次使能网络设备的接收中断 */
return 0;
}
return 1;
}
虽然 NAPI 兼容的设备驱动以 poll 方式接收数据包,但是仍然需要首次数据包接收中断来触发 poll 过程。与数据包的中断接收方式不同的是,以轮询方式接收数据包时,当第一次中断发生后,中断处理程序要禁止设备的数据包接收中断,如代码清单所示:
static void xxx_poll_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
switch (status &ISQ_EVENT_MASK)
{
case ISQ_RECEIVER_EVENT:
… /* 获取数据包 */
xxx_disable_rx_int(...); /* 禁到添加接收中断 */
netif_rx_schedule(dev); //将设备的Poll方法添加到网络层的poll处理队列中,排队接收数据包,最终触发NET_RX_SOFTRQ软中断,通知网络层接收数据包
break;
… /* 其他类型的中断 */
}
}
NAPI 驱动程序各部分的调用关系如下:
网络连接状态
网络适配器硬件电路可以检测出链路上是否有载波,载波反映了网络的连接是否正常。网络设备驱动可以通过 netif_carrier_on()和 netif_carrier_off()函数改变设备的连接状态,如果驱动检测到连接状态发生变化,也应该以 netif_carrier_on()和 netif_carrier_off()函数显式地通知内核。
除了 netif_carrier_on()和 netif_carrier_off()函数以外,另一个函数 netif_carrier_ok()可用于向调用者返回链路上的载波信号是否存在。
这几个函数都接收一个 net_device 设备结构体指针为参数,原型分别为:
void netif_carrier_on(struct net_device *dev);
void netif_carrier_off(struct net_device *dev);
int netif_carrier_ok(struct net_device *dev);
网络设备驱动程序中往往设置一个定时器来对链路状态进行周期性地检查。当定时器到期之后,在定时器处理函数中读取物理设备的相关寄存器获得载波状态,从而更新设备的连接状态,如代码清单所示:
static void xxx_timer(unsigned long data)
{
struct net_device *dev = (struct net_device*)data;
u16 link;
…
if (!(dev->flags &IFF_UP))
{
goto set_timer;
}
/* 获得物理上的连接状态 */
if (link = xxx_chk_link(dev)) //用于读取网络适配器硬件的相关寄存器以获得链路连接状态,具体实现由硬件决定
{
if (!(dev->flags &IFF_RUNNING))
{
netif_carrier_on(dev);
dev->flags |= IFF_RUNNING;
printk(KERN_DEBUG "%s: link up\n", dev->name);
}
}
else
{
if (dev->flags &IFF_RUNNING)
{
netif_carrier_off(dev);
dev->flags &= ~IFF_RUNNING;
printk(KERN_DEBUG "%s: link down\n", dev->name);
}
}
set_timer:
priv->timer.expires = jiffies + 1 * HZ;
priv->timer.data = (unsigned long)dev;
priv->timer.function = &xxx_timer; /* timer handler */
add_timer(&priv->timer);
}
参数设置和统计数据
在网络设备的驱动程序中还提供一些方法供系统对设备的参数进行设置或读取设备相关的信息。
当用户调用 ioctl()函数,并指定 SIOCSIFHWADDR 命令时,意味着要设置这个设备的 MAC 地址。设置网络设备的 MAC 地址可用如代码清单所示的模板。
static int set_mac_address(struct net_device *dev, void *addr)
{
if (netif_running(dev))
return -EBUSY; /* 设备忙 */
/* 设置以太网的 MAC 地址 */
xxx_set_ mac(dev, addr);
return 0;
}
当用户调用 ioctl()函数时,若命令为 SIOCSIFMAP(如在控制台中运行网络配置命令 ifconfig 就会引发这一调用),系统会调用驱动程序的 set_config()函数。系统会向 set_config()函数传递一个 ifmap 结构体,该结构体中主要包含用户欲设置的设备要使用的 I/O地址、中断等信息。set_config()函数的例子如代码清单所示
int xxx_config(struct net_device *dev, struct ifmap *map)
{
if (netif_running(dev)) /* 不能设置一个正在运行状态的设备 */
return - EBUSY;
/* 假设不允许改变 I/O 地址 */
if (map->base_addr != dev->base_addr)
{
printk(KERN_WARNING "xxx: Can't change I/O address\n");
return - EOPNOTSUPP;
}
/* 假设允许改变 IRQ */
if (map->irq != dev->irq)
{
dev->irq = map->irq;
}
return 0;
}
如果用户调用 ioctl()时,命令类型在 SIOCDEVPRIVATE 和 SIOCDEVPRIVATE+15 之间,系统会调用驱动程序的 do_ioctl()函数,进行设备专用数据的设置。
驱动程序还应提供 get_stats()函数用以向用户反馈设备状态和统计信息,该函数返回的是一个net_device_stats 结构体,如代码清单所示
struct net_device_stats *xxx_stats(struct net_device *dev)
{
struct xxx_priv *priv = netdev_priv(dev);
return &priv->stats;
}
net_device_stats 结构体定义在内核的include/linux/netdevice.h 文件中,它包含了比较完整的统计信息,如代码清单所示
struct net_device_stats
{
unsigned long rx_packets; /* 收到的数据包数 */
unsigned long tx_packets; /* 发送的数据包数 */
unsigned long rx_bytes; /* 收到的字节数 */
unsigned long tx_bytes; /* 发送的字节数 */
unsigned long rx_errors; /* 收到的错误数据包数 */
unsigned long tx_errors; /* 发生发送错误的数据包数 */
...
};
net_device_stats 结构体适宜包含在设备的私有信息结构体中,而其中统计信息的修改则应该在设备驱动的与发送和接收相关的具体函数中完成,这些函数包括中断处理程序、数据包发送函数、数据包发送超时函数和数据包接收相关函数等。具体操作 如下:
/* 发送超时函数 */
void xxx_tx_timeout(struct net_device *dev)
{
struct xxx_priv *priv = netdev_priv(dev);
...
priv->stats.tx_errors++; /* 发送错误包数加 1 */
...
}
/* 中断处理函数 */
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
switch (status &ISQ_EVENT_MASK)
{
...
case ISQ_TRANSMITTER_EVENT: /
priv->stats.tx_packets++; /* 数据包发送成功, tx_packets 信息加 1 */
netif_wake_queue(dev); /* 通知上层协议 */
if ((status &(TX_OK | TX_LOST_CRS | TX_SQE_ERROR |
TX_LATE_COL | TX_16_COL)) != TX_OK) /*读取硬件上的出错标志*/
{
/* 根据错误的不同情况,对 net_device_stats 的不同成员加 1 */
if ((status &TX_OK) == 0)
priv->stats.tx_errors++;
if (status &TX_LOST_CRS)
priv->stats.tx_carrier_errors++;
if (status &TX_SQE_ERROR)
priv->stats.tx_heartbeat_errors++;
if (status &TX_LATE_COL)
priv->stats.tx_window_errors++;
if (status &TX_16_COL)
priv->stats.tx_aborted_errors++;
}
break;
case ISQ_RX_MISS_EVENT:
priv->stats.rx_missed_errors += (status >> 6);
break;
case ISQ_TX_COL_EVENT:
priv->stats.collisions += (status >> 6);
break;
}
}
来源:CSDN
作者:jansert
链接:https://blog.csdn.net/jansert/article/details/103751524