什么是NAPI
NAPI是linux一套最新的处理网口数据的API,linux 2.5引入的,所以很多驱动并不支持这种操作方式。简单来说,NAPI是综合中断方式与轮询方式的技术。数据量很低与很高时,NAPI可以发挥中断方式与轮询方式的优点,性能较好。如果数据量不稳定,且说高不高说低不低,则NAPI会在两种方式切换上消耗不少时间,效率反而较低一些。
下面会用到netdev_priv()这个函数,这里先讲解下,每个网卡驱动都有自己的私有的数据,来维持网络的正常运行,而这部分私有数据放在网络设备数据后面(内存概念上),这个函数就是通过dev来取得这部分私有数据,注间这部分私有数据不在dev结构体中,而是紧接在dev内存空间后。
static inline void *netdev_priv(const struct net_device *dev)
return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);
弄清这个函数还得先清楚dev这个结构的分配
alloc_netdev() -> alloc_netdev_mq()
struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,
void (*setup)(struct net_device *), unsigned int queue_count)
alloc_size = sizeof(struct net_device);
if (sizeof_priv) {
/* ensure 32-byte alignment of private area */
alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);
alloc_size += sizeof_priv;
/* ensure 32-byte alignment of whole construct */
alloc_size += NETDEV_ALIGN - 1;
p = kzalloc(alloc_size, GFP_KERNEL);
if (!p) {
printk(KERN_ERR "alloc_netdev: Unable to allocate device./n");
return NULL;
可以看到,dev在分配时,即在它的后面分配了private的空间,需要注意的是,这两部分都是32字节对齐的,如下图所示,padding是加入的的补齐字节:
举个例子,假设sizeof(net_device)大小为31B,private大小45B,则实际分配空间如图所示:
b44_interrupt():当有数据包收发或发生错误时,会产生硬件中断,该函数被触发
struct b44 *bp = netdev_priv(dev);
取出网卡驱动的私有数据private,该部分数据位于dev数据后面
istat = br32(bp, B44_ISTAT);
imask = br32(bp, B44_IMASK);
读出当前中断状态和中断屏蔽字
if (istat) {
if (napi_schedule_prep(&bp->napi)) {
bp->istat = istat;
__b44_disable_ints(bp);
__napi_schedule(&bp->napi);
设置NAPI为SCHED状态,记录当前中断状态,关闭中断,执行调度
void __napi_schedule(struct napi_struct *n)
unsigned long flags;
local_irq_save(flags);
list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_restore(flags);
__get_cpu_var():得到当前CPU的偏移量,与多CPU有关
将
napi
的
poll_list
加入到
softnet_data
队列尾部,然后引起软中断
NET_RX_SOFTIRQ
。
似乎还没有真正的收发函数出现,别急;
关于软中断的机制请参考资料
,在
net_dev_init()[dev.c]
中,注册了两个软中断处理函数,所以引起软中断后,最终调用了
net_rx_action()
。
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
下面来看下net_rx_action()函数实现:
static void net_rx_action(struct softirq_action *h)
struct list_head *list = &__get_cpu_var(softnet_data).poll_list; // [1]
n = list_first_entry(list, struct napi_struct, poll_list); // [2]
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight); // [3]
trace_napi_poll(n);
__get_cpu_var是不是很熟悉,在b44_interrupt()中才向它的poll_list中加入了一个napi_struct;代码[2]很简单了,从poll_list的头中取出一个napi_struct,然后执行代码[3],调用poll()函数;注意到这里在interrupt时,会向poll_list尾部加入一个napi_struct,并引起软中断,在软中断处理函数中,会从poll_list头部移除一个napi_struct,进行处理,理论上说,硬件中断加入的数据在其引起的软中断中被处理。
poll函数实际指向的是b44_poll(),这是显而易见的,但具体怎样调用的呢?在网卡驱动初始化函数b44_init_one()有这样一行代码:
netif_napi_add(dev, &bp->napi, b44_poll, 64);
而netif_napi_add()中初始化napi并将其加入dev的队列,
napi->poll = poll;
这行代码就是b44_poll赋给napi_poll,所以在
NET_RX_SOFTIRQ
软中断处理函数
net_rx_action()
中执行的
b44_poll()
。
怎么到这里都还没有收发数据包的函数呢!
b44_poll()
就是轮询中断向量,查找出引起本次操作的中断;
static int b44_poll(struct napi_struct *napi, int budget)
if (bp->istat & (ISTAT_TX | ISTAT_TO))
b44_tx(bp);
if (bp->istat & ISTAT_RX)
work_done += b44_rx(bp, budget);
if (bp->istat & ISTAT_ERRORS)
可以看到,查询了四种中断:
ISTAT_TX
、
ISTAT_TO
、
ISTAT_RX
、
ISTAT_ERRORS
#define
ISTAT_TO 0x00000080 /* General Purpose Timeout */
#define
ISTAT_RX 0x00010000 /* RX Interrupt */
#define
ISTAT_TX 0x01000000 /* TX Interrupt */
#define ISTAT_ERRORS (ISTAT_DSCE|ISTAT_DATAE|ISTAT_DPE|ISTAT_RDU|ISTAT_RFO|ISTAT_TFU)
如果是
TX
中断,则调用
b44_tx
发送数据包;如果是
RX
中断,则调用
b44_rx
接收数据包。至此,从网卡驱动收发数据包的调用就是如此了。
最后,给个总结性的图:
纠结了好多天,终于弄懂了
B440X
的处理。
上篇讲到通过中断,最终网卡调用了
b44_rx()
来接收报文
对这个函数中的一些参数,可以这样理解:
bp->rx_cons –
处理器处理到的缓冲区号
bp->rx_pending –
分配的缓冲区个数
bp->rx_prod –
当前缓冲区的最后一个缓冲号
这里要参数
B440X
的手册了解下寄存器的作用:
#define B44_DMARX_ADDR
0x0214UL /* DMA RX Descriptor Ring Address */
#define B44_DMARX_PTR
0x0218UL /* DMA RX Last Posted Descriptor */
#define B44_DMARX_STAT
0x021CUL /* DMA RX Current Active Desc. + Status */
仅
b44_rx()
来说,
B44_DMARX_ADDR
储存了环形缓冲的基地址,
B44_DMARX_PTR
存储了环形缓冲最后一个缓冲区号,这两个寄存器都由处理来设置;
B44_DMARX_STAT
储存了状态及网卡当前处理到的缓冲区号,这个寄存器只能由网卡来设置。
网卡中
DMA
也很重要:
在网卡初始化阶段,
b44_open() -> b44_alloc_consistent()
bp->rx_buffers = kzalloc(size, gfp);
// size = B44_RX_RING_SIZE * sizeof(struct ring_info)
bp->rx_ring = ssb_dma_alloc_consistent(bp->sdev, size, &bp->rx_ring_dma, gfp);
// size = DMA_TABLE_BYTES
rx_ring
是
DMA
映射的虚拟地址,
rx_rind_dma
是
DMA
映射的总线地址,这个地址将会写入
B44_DMARX_ADDR
寄存器,作为环形缓冲的基地址。
bw32(bp, B44_DMARX_ADDR, bp->rx_ring_dma + bp->dma_offset);
稍后在
rx_init_rings() -> b44_alloc_rx_skb()
mapping = ssb_dma_map_single(bp->sdev, skb->data,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);
将
rx_buffers
进行
DMA
映射,并将映射地址存储在
rx_ring
中
dp->addr = cpu_to_le32((u32) mapping + bp->dma_offset); // dp
是
rx_ring
中一个
DMA
的大致流程:
不准确,但可以参考下大致意思
网卡读取
B44_DMARX_ADDR
与
B44_DMARX_STAT
寄存器,得到下一个处理的
struct dma_desc
,然后根据
dma_desc
中的
addr
找到报文缓冲区,通过
DMA
处理器将网卡收到报文拷贝到
addr
地址处,这个过程
CPU
是不参与的。
prod –
网卡
[
硬件
]
处理到的缓冲区号
prod
= br32(bp, B44_DMARX_STAT) & DMARX_STAT_CDMASK;
prod /= sizeof(struct dma_desc);
cons = bp->rx_cons;
根据上面分析,
prod
读取
B44_DMARX_STAT
寄存器,存储网卡当前处理到的缓冲区号;
cons
存储处理器处理到的缓冲区号。
while (cons != prod && budget > 0) {
处理报文当前时刻网卡接收到的所有报文,每处理一个报文
cons
都会加
1
,由于是环形缓冲,因此这里用相等,而不是大小比较。
struct ring_info *rp = &bp->rx_buffers[cons];
struct sk_buff *skb = rp->skb;
dma_addr_t map = rp->mapping;
skb
和
map
保存了当关地址,下面在交换缓冲区后会用到。
ssb_dma_sync_single_for_cpu(bp->sdev, map,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);
CPU
取得
rx_buffer[cons]
的控制权,此时网卡不能再处理该缓冲区。
rh = (struct rx_header *) skb->data;
len = le16_to_cpu(rh->len);
len -= 4;
CPU
取得控制权后,取得报文头,再从报文头取出报文长度
len
,
len-=4
表示忽略了最后
4
节字的
CRC
,从这里可以看出,
B440X
网卡驱动不会检查
CRC
校验。而每个报文数据最前面添加了网卡的头部信息
struct rx_header
,这里是
28
字节。
struct sk_buff *copy_skb;
b44_recycle_rx(bp, cons, bp->rx_prod);
copy_skb = netdev_alloc_skb(bp->dev, len + 2);
copy_skb
作为传送报文的中间量,在第三句为其分配了
len + 2
的空间
(
为了
IP
头对齐,稍后提到
)
。
b44_recycle_rx()
函数很关键,它作了如下工作:
1.
将缓冲区号
cons
赋值给缓冲区号
rx_prod
;
2.
rx_buffers[cons].skb = NULL
3.
将缓冲区号
rx_prod
控制权给网卡
简单来说,就是将
cons
号缓冲区交由
CPU
处理,而用
rx_prod
号缓冲区代替其给网卡使用。
a.
b44_recycle_rx
前
b. b44_recycle_rx
后
以起始状态为例,缓冲区
rx_ring
分配了
512
个,但
rx_buffers
仅分配了
200
个,此时
cons = 0
,
rx_prod = 200
。执行
b44_recycle_rx()
后,网卡处理缓冲区变为
1~200
,而
0
号缓冲区交由
CPU
处理,将报文拷贝,并向上送至协议栈。注意
rx_ring
和
rx_buffer
是不同的。
这样做的好处在于,不用等待
CPU
处理完
0
号缓冲区,网卡的缓冲区数保持
200
,而不会减少,这也是
rx_pending = 200
的原因所在。
skb_reserve(copy_skb, 2);
skb_put(copy_skb, len);
关于
skb
的操作自己去了解,这里
skb_reserve()
在报文头部保留了两个字节,我们知道链路层报头是
14
字节,正常
IP
报文会从
14
字节开始,这样就不是
4
字节对齐了,所以在头部保留
2
字节,使
IP
报文从
16
字节开始。
skb_copy_from_linear_data_offset(skb, RX_PKT_OFFSET,copy_skb->data, len);
skb = copy_skb;
CPU
将报文从
skb
拷贝到
copy_skb
中,跳过了网卡报头的额外信息,因为这部分信息在上层协议站是没用的,所以去掉。在函数开始时说过
skb
是保存了
cons
号的地址,因为在
b44_recycle_rx()
后
cons
号不再引用
skb
指向的空间,而仅由
skb
引用,这样便可以向上层传送,而不用额外复制。
netif_receive_skb(skb);
received++;
budget--;
next_pkt:
bp->rx_prod = (bp->rx_prod + 1) & (B44_RX_RING_SIZE - 1);
cons = (cons + 1) & (B44_RX_RING_SIZE - 1);
netif_receive_skb()
将报文交由上层协议栈处理,这是下一节的内容,然后
CPU
处理下一个报文,
rx_prod
和
cons
各加
1
,它们代表的含义开头有说明。
如此循环,直到
cons == prod
,此时网卡收到的报文都已被
CPU
处理,更新变量:
bp->rx_cons = cons;
bw32(bp, B44_DMARX_PTR, cons * sizeof(struct dma_desc));
在
netif_receive_skb()
函数中,可以看出处理的是像
ARP
、
IP
这些链路层以上的协议,那么,链路层报头是在哪里去掉的呢?答案是网卡驱动中,在调用
netif_receive_skb()
前,
skb->protocol = eth_type_trans(skb, bp->dev);
该函数对处理后
skb>data
跳过以太网报头,由
mac_header
指示以太网报头:
进入
netif_receive_skb()
函数
list_for_each_entry_rcu(ptype,&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list)
按照协议类型依次由相应的协议模块进行处理,而所以的协议模块处理都会注册在
ptype_base
中,实际是链表结构。
net/core/dev.c
static struct list_head ptype_base __read_mostly;
/* Taps */
而相应的协议模块是通过
dev_add_pack()
函数加入的:
void dev_add_pack(struct packet_type *pt)
int hash;
spin_lock_bh(&ptype_lock);
if (pt->type == htons(ETH_P_ALL))
list_add_rcu(&pt->list, &ptype_all);
else {
hash = ntohs(pt->type) & PTYPE_HASH_MASK;
list_add_rcu(&pt->list, &ptype_base[hash]);
spin_unlock_bh(&ptype_lock);
以
ARP
处理为例
该模块的定义,它会在
arp_init()
中注册进
ptype_base
链表中:
static struct packet_type arp_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_ARP),
.func = arp_rcv,
然后在根据报文的
TYPE
来在
ptype_base
中查找相应协议模块进行处理时,实际调用
arp_rcv()
进行接收
arp_rcv() --> arp_process()
arp = arp_hdr(skb);
arp_ptr= (unsigned char *)(arp+1);
sha= arp_ptr;
arp_ptr += dev->addr_len;
memcpy(&sip, arp_ptr, 4);
arp_ptr += 4;
arp_ptr += dev->addr_len;
memcpy(&tip, arp_ptr, 4);
操作后这指针位置:
然后判断是
ARP
请求报文,这时先查询路由表
ip_route_input()
if (arp->ar_op == htons(ARPOP_REQUEST) &&
ip_route_input(skb, tip, sip, 0, dev) == 0)
在
ip_route_input()
函数中,先在
cache
中查询是否存在相应的路由表项:
hash = rt_hash(daddr, saddr, iif, rt_genid(net));
缓存的路由项在内核中组织成
hash
表的形式,因此在查询时,先算出的
hash
值,再用该项
- rt_hash_table[hash].chain
即可。这里可以看到,缓存路由项包括了源
IP
地址、目的
IP
地址、网卡号。
如果在缓存中没有查到匹配项,或指定不查询
cache
,则查询路由表
ip_route_input_slow()
;
进入
ip_route_input_slow()
函数,最终调用
fib_lookup()
得到查询结果
fib_result
if ((err = fib_lookup(net, &fl, &res)) != 0)
如果结果
fib_result
合法,则需要更新路由缓存,将此次查询结果写入缓存
hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net));
err = rt_intern_hash(hash, rth, NULL, skb, fl.iif);
在查找完路由表后,回到
arp_process()
函数,如果路由项指向本地,则应由本机接收该报文:
if (addr_type == RTN_LOCAL) {
if (!dont_send) {
n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
if (n) {
arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);
neigh_release(n);
goto out;
首先更新邻居表
neigh_event_ns()
,然后发送
ARP
响应
– arp_send
。
至此,大致的
ARP
流程完成。由于
ARP
部分涉及到路由表以及邻居表,这都是很大的概念,在下一篇中介绍,这里直接略过了。
什么是NAPINAPI是linux一套最新的处理网口数据的API,linux 2.5引入的,所以很多驱动并不支持这种操作方式。简单来说,NAPI是综合中断方式与轮询方式的技术。数据量很低与很高时,NAPI可以发挥中断方式与轮询方式的优点,性能较好。如果数据量不稳定,且说高不高说低不低,则NAPI会在两种方式切换上消耗不少时间,效率反而较低一些。 下面会用到netdev_priv()这个
How to Port Open vSwitch to New Software or Hardware
Open vSwitch (OVS) is intended to be easily ported to new software and
hardware platforms. This document describes the types of changes that
are most likely to be necessary in porting OVS to Unix-like p
在Linux系统中,系统调用是操作系统提供给应用程序使用操作系统服务的重要接口,但同时也正是通过系统调用机制,操作系统屏蔽了用户直接访问系统内核的可能性。幸运的是Linux提供了LKM机制可以使我们在内核空间工作,在LKM机制中一个重要的组成部分就是proc伪文件系统,它为用户提供了动态操作Linux内核信息的接口,是除系统调用之外另一个重要的Linux内核空间与用户空间交换数据的途径。
文章目录
NetDev
一、前提二、
NetDev
ice三、NetBuf四、适配器
NetDev
iceImpl
NetDev
iceImplOp五、netif网口发送数据到lwip网口从lwip接收数据
NetDev
WIFI 芯片属于网络设备,自然也要归OpenHarmony的网络框架管理,本文用于了解 网络数据如何在
协议
栈和网络驱动之间传输。
网络设备的使用需要配合网络
协议
栈,OpenHarmony的网络
协议
栈有两种,一种是liteos-a内核使用的lwip
协议
栈,一种是标准系统linux内核网络
协议
栈。
aystys: