NETDEV 协议

2024-06-11 17:48
文章标签 协议 netdev

本文主要是介绍NETDEV 协议,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

什么是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有关

napipoll_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_TXISTAT_TOISTAT_RXISTAT_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_ringDMA映射的虚拟地址,rx_rind_dmaDMA映射的总线地址,这个地址将会写入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); // dprx_ring中一个

 

DMA的大致流程:

       不准确,但可以参考下大致意思

 

网卡读取B44_DMARX_ADDRB44_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;

skbmap保存了当关地址,下面在交换缓冲区后会用到。

 

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取得控制权后,取得报文头,再从报文头取出报文长度lenlen-=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 = 0rx_prod = 200。执行b44_recycle_rx()后,网卡处理缓冲区变为1~200,而0号缓冲区交由CPU处理,将报文拷贝,并向上送至协议栈。注意rx_ringrx_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_prodcons各加1,它们代表的含义开头有说明。

 

如此循环,直到cons == prod,此时网卡收到的报文都已被CPU处理,更新变量:

 

bp->rx_cons = cons;

bw32(bp, B44_DMARX_PTR, cons * sizeof(struct dma_desc));

 

netif_receive_skb()函数中,可以看出处理的是像ARPIP这些链路层以上的协议,那么,链路层报头是在哪里去掉的呢?答案是网卡驱动中,在调用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部分涉及到路由表以及邻居表,这都是很大的概念,在下一篇中介绍,这里直接略过了。

 

 

 

 

 

这篇关于NETDEV 协议的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1051863

相关文章

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

【Go】go连接clickhouse使用TCP协议

离开你是傻是对是错 是看破是软弱 这结果是爱是恨或者是什么 如果是种解脱 怎么会还有眷恋在我心窝 那么爱你为什么                      🎵 黄品源/莫文蔚《那么爱你为什么》 package mainimport ("context""fmt""log""time""github.com/ClickHouse/clickhouse-go/v2")func main(

2024.9.8 TCP/IP协议学习笔记

1.所谓的层就是数据交换的深度,电脑点对点就是单层,物理层,加上集线器还是物理层,加上交换机就变成链路层了,有地址表,路由器就到了第三层网络层,每个端口都有一个mac地址 2.A 给 C 发数据包,怎么知道是否要通过路由器转发呢?答案:子网 3.将源 IP 与目的 IP 分别同这个子网掩码进行与运算****,相等则是在一个子网,不相等就是在不同子网 4.A 如何知道,哪个设备是路由器?答案:在 A

Modbus-RTU协议

一、协议概述 Modbus-RTU(Remote Terminal Unit)是一种基于主从架构的通信协议,采用二进制数据表示,消息中的每个8位字节含有两个4位十六进制字符。它主要通过RS-485、RS-232、RS-422等物理接口实现数据的传输,传输距离远、抗干扰能力强、通信效率高。 二、报文结构 一个标准的Modbus-RTU报文通常包含以下部分: 地址域:单个字节,表示从站设备

网络原理之TCP协议(万字详解!!!)

目录 前言 TCP协议段格式 TCP协议相关特性 1.确认应答 2.超时重传 3.连接管理(三次握手、四次挥手) 三次握手(建立TCP连接) 四次挥手(断开连接)  4.滑动窗口 5.流量控制 6.拥塞控制 7.延迟应答 8.捎带应答  9.基于字节流 10.异常情况的处理 小结  前言 在前面,我们已经讲解了有关UDP协议的相关知识,但是在传输层,还有

DNS协议基础笔记

1.定义 DNS(Domain Name System,域名系统)是互联网的一项核心服务,它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。 2.域名解析过程 当用户在浏览器中输入一个域名,浏览器首先会检查自己的缓存中是否有该域名对应的 IP 地址。本地 DNS 服务器收到查询请求后,首先会检查自己的缓存中是否有该域名对应的 IP 地址。根域名服务器收到查询请

4G模块、WIFI模块、NBIOT模块通过AT指令连接华为云物联网服务器(MQTT协议)

MQTT协议概述 MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,它被设计用来提供一对多的消息分发和应用之间的通讯,尤其适用于远程位置的设备和高延迟或低带宽的网络。MQTT协议基于客户端-服务器架构,客户端可以订阅任意数量的主题,并可以发布消息到这些主题。服务器(通常称为MQTT Broker)则负责接受来自客户端的连接请求,并转发消

HTTP协议 HTTPS协议 MQTT协议介绍

目录 一.HTTP协议 1. HTTP 协议介绍 基本介绍: 协议:  注意: 2. HTTP 协议的工作过程 基础术语: 客户端: 主动发起网络请求的一端 服务器: 被动接收网络请求的一端 请求: 客户端给服务器发送的数据 响应: 服务器给客户端返回的数据 HTTP 协议的重要特点: 一发一收,一问一答 注意: 网络编程中,除了一发一收之外,还有其它的模式 二.HTT

CAMediaTiming协议

今天看下下CALayer这个类,里面的属性是实现CAMediaTiming这个协议的,这里简单介绍一下CAMediaTiming协议里面的属性。官网链接 如下 beginTime:开始时间(和父类相关) timeOffset:动态的本地时间t,tp是父类事件。t = (tp - begin) * speed + offset.用于暂停一个layer。  fillMode:layer完成后的

浏览器工作原理(3)-TCP协议文件如何从服务器到浏览器

浏览器工作原理-TCP协议,文件如何从服务器到浏览器 本周继续学习浏览器工作原理及实践,本次内容来看一下TCP协议确保文件完整的送到至浏览器 First Page 是指页面加载到首次开始绘制的时长,而影响这个性能指标的一个重要原因是网络加载速度,网络传输协议无论使用http还是websocket,都是基于TCP/IP的,所以有必要了解一下TCP/IP,对于web的性能调优和问题定位都有很