第12章 网络 (4)

目录

12.7 网络设备层

12.7.2 接收分组

12.7.3 发送分组


本专栏文章将有70篇左右,欢迎+关注,查看后续文章。

12.7 网络设备层

12.7.2 接收分组

接收时,使用中断通知分组到达。几乎所有网卡都支持DMA。

一个 skb如何找到对应网络设备?

        skb->dev

一个socket 如何关联对应 sk_buff ?

        socket fd

                -> struct socket

                        -> struct sock

                                -> struct sock.sk_receive_queue //socket的接收缓存队列。

                                        -> struct sk_buff

收到一个sk_buff,如何知道查找哪个 struct sock?

tcp为例:

int    tcp_v4_rcv(struct sk_buff    *skb)

{

        const struct tcphdr    *th;

        th    =     tcp_hdr(skb);

        sk     =     __inet_lookup_skb(&tcp_hashinfo,     skb,    th->source,     th->dest);

                //通过源目端口和IP的hash值,找到struct sock。

}

网卡接收环形缓冲区:

        网卡内的一块内存,存储收到的数据帧。

        缓冲区大小及其数量由网卡硬件决定。

传统收包:

        每收到一个数据包产生一个中断。

        缺点:

                数据包过多时,中断上下文切换频繁,性能降低。

因此引进 NAPI,即 New API。结合了IRQ + 轮询。

先介绍一个重要结构体:

struct    softnet_data {         //代表一个CPU收包队列。

        struct   list_head     poll_list:

                //NAPI 轮询的设备列表。

                //list_add_tail(&napi->poll_list,     &sd->poll_list);

        struct   sk_buff_head     process_queue;

        struct   sk_buff_head     input_pkt_queue; // 收包队列

        struct   napi_struct     backlog;

                // 为兼容NAPI模式,传统方法收包也用一个napi,用于处理积压队列中报文。

                // sd->backlog.poll     =    process_backlog;

}

下面介绍传统和 NAPI 两种方法的接收分组。

        传统网卡使用传统方式。

        高吞吐、高性能网卡使用NAPI方式。

在Linux-3.10中,为兼容NAPI方式,将传统方式当做一种特殊NAPI。

即 struct napi_struct   backlog。

所以先讲解 NAPI 方式。

1. NAPI

NAPI原理:

        1. 收到第一个分组导致网卡发出中断,然后关闭中断。将设备放到轮询表中。

        2. 只要设备接收缓存区有分组需处理,就一直对设备轮询。

        3. 设备缓存中分组处理完毕后,开启RX中断。

网卡驱动中probe函数中执行netif_napi_add,就是使用 NAPI 模式。

void    netif_napi_add(struct net_device   *dev,    struct napi_struct   *napi,    int (*poll) (*, ),    int weight)

        poll 函数:驱动自定义。用于 IRQ 禁用后被 NAPI 调度执行。以接收分组。

        weight:一次轮询中处理多少分组。(不超过RX缓冲区容量)。

int     (*poll) (struct   napi_struct   *,     int   budget)

        budget:预算/权重,一次轮询最多允许处理的分组数。

网卡速率越高,weight可设更高。

struct    napi_struct   {         //一个网卡的轮询结构体。

        struct list_head         poll_list;

        unsigned long           state;

        int                                 weight; //一次轮询可处理的包数量。

        unsigned int              gro_count;

        int                             (*poll)(struct   napi_struct   *,     int);

        ...

};

成员介绍:

struct list_head    poll_list;

        用于将 NAPI 实例链接到 CPU 的软中断列表中。

        使用举例:

                struct   softnet_data    *sd;

                list_add_tail(&napi->poll_list,    &sd->poll_list);

state:

        跟踪 NAPI 实例的状态。值有:

                1. NAPI_STATE_SCHED:

                        该 NAPI 实例已加入 softnet_data->poll_list 轮询队列中。

                        等待CPU调度执行该 NAPI 的poll函数。

                2. NAPI_STATE_DISABLE:

                        禁用该 NAPI 实例。

                        该 NAPI 实例不会被加入softnet_data->poll_list轮询队列中。

实现 poll 函数

网卡驱动自定义底层收包。

intel e100网卡为例:

int   e100_poll(struct    napi_struct    *napi,     int   budget)

{

        struct   nic    *nic    =    container_of(napi,    struct   nic,    napi);

        unsigned int     work_done    =     0;

        e100_rx_clean(nic,     &work_done,     budget);

        if (work_done    <    budget) {

                //没有接收足够数量的报文,表示接收完毕,可退出poll模式,并开启中断。

                napi_complete(napi);

                e100_enable_irq(nic);

        }

        return work_done;

}

实现 IRQ 处理程序

e100网卡为例:

request_irq(irq,     e100_intr,    IRQF_SHARED,    netdev_name,    nic->netdev);

irqreturn_t      e100_intr(int   irq,     void   *dev_id)

{

        struct   net_device     *netdev     =    dev_id;

        struct nic     *nic    =    netdev_priv(netdev);

        if (napi_schedule_prep( &nic->napi )) {

                e100_disable_irq(nic);

                __napi_schedule(&nic->napi);

        }

        return    IRQ_HANDLED;

}

napi_schedule_prep:

        设置NAPI state为NAPI_STATE_SCHED。(即将将设备放到轮询表中)

e100_disable_irq:

        禁止中断,因为即将调用poll函数,轮询收包。

__napi_schedule:

        进行NAPI调度。

void     ____napi_schedule(struct   softnet_data     *sd,     struct   napi_struct    *napi)

{

        list_add_tail(&napi->poll_list,     &sd->poll_list);         // 加入本CPU的轮询表中。

        __raise_softirq_irqoff( NET_RX_SOFTIRQ );

                //触发软中断。

}

处理RX软中断

void     net_rx_action(struct   softirq_action    *h)

{

        struct softnet_data     *sd    =    &__get_cpu_var(softnet_data);

        unsigned long     time_limit   =   jiffies + 2;

        int   budget    =    netdev_budget;         // 对应 proc/net/sys/net/core/netdev_budget

        struct   softnet_data     *sd    =    &__get_cpu_var(softnet_data);

        while  ( &sd->poll_list ))   { // 遍历该CPU上所有napi

                if (unlikely(budget   <=   0    ||    time_after_eq(jiffies,   time_limit)))

                        goto    softnet_break;         // 预算用尽,或执行执行太久。

                n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);

                if (test_bit(NAPI_STATE_SCHED,    &n->state)) {

                        work    =    n->poll(n,    weight);

                                // 遍历执行,不同驱动定义的poll函数。

                                //包括传统方法的struct  napi_struct     backlog

                }

                if (work    ==    weight) {

                        napi_complete( n );         // 接收到足够报文,将该napi从CPU中移除。

                }

        }

}

第12章 网络 (4)_第1张图片

总结:

        1. 第一个分组导致网卡发出IRQ,触发中断处理函数。

        2. 中断处理函数中,将网卡的napi 放在CPU的轮询表上,最后触发NET_RX_SOFTIRQ软中断。

        3. 软中断中执行各个napi的轮询函数。

/proc/sys/net/core/netdev_budget:

        该CPU中所有 napi 预算总和,每个NAPI设备轮询后,从总数中减去。

        budget为0后或超时,退出软中断处理函数。

2. 传统方法

request_irq(dev->irq,    net_interrupt,    irqflags,    dev->name,   dev);

第12章 网络 (4)_第2张图片

net_interrupt :

        网卡驱动中自定义。

        通常进一步调用 netif_rx。

net/core/dev.c

        netif_rx:是传统方式的收包,而不是NAPI方式的收包。

int    netif_rx( struct   sk_buff      *skb)

{

        enqueue_to_backlog(skb,    get_cpu(),    &qtail);

}

int    enqueue_to_backlog(struct   sk_buff     *skb,    int   cpu,     unsigned int    *qtail)

{

        struct   softnet_data     *sd;

        sd    =    &per_cpu(softnet_data,    cpu);

        if (skb_queue_len( &sd->input_pkt_queue)    <=    netdev_max_backlog) {

                if (skb_queue_len(&sd->input_pkt_queue)) {

                enqueue:

                        __skb_queue_tail(&sd->input_pkt_queue, skb);

                        //将收到的skb 放到 CPU 的等待队列 input_pkt_queue中。

                        return    NET_RX_SUCCESS;

                }

                if (!__test_and_set_bit(NAPI_STATE_SCHED,     &sd->backlog.state)) {

                        ____napi_schedule(sd,     &sd->backlog);

                }

                goto   enqueue;

        }

        return    NET_RX_DROP;

}

void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)

{

        list_add_tail(&napi->poll_list,     &sd->poll_list);

        __raise_softirq_irqoff(NET_RX_SOFTIRQ);         // 触发NET_RX_SOFTIRQ 软中断。

}

3. 执行软中断处理函数 net_rx_action

NAPI也在软中断中调用 net_rx_action。

net_rx_action 执行所有napi中poll函数,接收报文。

        包括:

                1. 传统方式:sd->backlog.poll = process_backlog;

                2. NAPI方式:网卡驱动自定义 poll 函数。

process_backlog:

第12章 网络 (4)_第3张图片

拓展

GRO技术:

        原理:

                将多个数据合并成一个大包,减少中断次数和内存拷贝,提高性能。

        使用场景:

                高吞吐量场景,如大文件、高并发。

                高速网络环境,如10GbE网络。

                长连接场景,如TCP长连接,GRO合并多个TCP包。

                大量小数据包场景。

        不适合场景:

                HTTP短连接、直播、在线游戏等(GRO会增加延迟)。

                数据包长度频繁变化,如VoIP流(容易合并错误)

                加密或压缩包。

12.7.3 发送分组

发包的注意事项:

        1. 特定协议的首部校验和。

        2. 确定路由。

        3. 确定目的MAC。

net/core/dev.c 中 dev_queue_xmit:

        作用:把分组放置到发送队列中。

然后调用 网卡驱动自定义 net_device -> hard_start_xmit 函数指针:

        作用:完成发送动作。

你可能感兴趣的:(网络,linux,服务器,c语言,架构)