什么是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: 小弟本科,电信专业应届生,c较好,有uboot、内核移植经验,做过一些ARM9上的监控小项目,对Linux、嵌入式开发很感兴趣,校招签了一个某通信集团(以前属于大唐的)的深圳的子公司,说是做LTE方向的L2/L3协议栈开发,或者业务平台开发,工资5.5K,加每天40餐补,现在在校继续想找更好点的公司,不过现在校招淡季了,准备去实习(1500+餐补),我的困惑是: 1、如果决定去这家公司了,是做协议开发好,还是做业务软件的平台开发?对这个不了解,找了LTE的书看了下,各种图啊,英文简写,反正各种不懂,他的要求是要懂Linux内核,可是完全看不到跟Linux有交集啊,希望了解这个的给指点下 2、有没有必要再继续找一下其他嵌入式的开发?明年三月份还有一波校招,主要是 现在感觉物联网行业不再是以前的空中楼阁,应该5年内会火起来吧,嵌入式生命力应该会很顽强吧,而且更符合自己的专业和兴趣, 第一份工作很重要,请大牛 指点下小弟,谢谢谢谢!