第十五章 提升网络性能技术--基于Linux3.10

本文介绍了几种网络性能优化技术,包括TSO/GSO、LRO/GRO、RSS、RPS/RFS和XPS等特性。这些特性通过硬件卸载减轻CPU负担,提高网络效率。TSO/GSO用于数据包分段,LRO/GRO用于数据包聚合,RSS和RPS/RFS用于接收队列负载均衡,XPS用于发送队列负载均衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本章的这些特性在于提高网络性能,这些技术中的一些需要NICs的支持,一些特性是软件实现的。这些特性在服务器上使用较为流行,但是对于嵌入式领域,尤其是视频等网络等带宽需求较大的应用场景,这些特性也可能被使用。

以太网数据包长是1500个字节,如果要传输大量的数据,TCP层就需要将这些数据分成若干小片MSS(Maximum Segment Size),然后再将这些小片发送出去。

         如果将一个大数据包(通常是MSS整数倍)送给网卡,那么网卡就需要完成分片工作,将数据分片成1460字节+40字节的包头信息,传送给PHY芯片。这就意味着网卡需要支持这类的特性才能这么使用。网卡特性的跟踪相对而言比较简单,在网卡注册的时候,就将网卡支持的一些特性放在了一个flag中了。

这些特性被称为offload特性,其寓意就是将CPU从繁重的工作中offload了,就意味着CPU可以有更多的时间让给操作系统其它功能部分了。

rx-checksumming TCP接收的校验和由网卡验证。

tx-checksumming: 网卡完成发送TCP头校验和的计算

scatter-gather: 分散/聚集IO,网卡支持该特性,可以减少内存的拷贝。

(TSO)tcp-segmentation-offload: TCP、DCCP数据包分片(依赖tx-checksumming和scatter-gather)

(UFO)udp-fragmentation-offload: 支持UDP分片,ip层不需要对最大64K的数据进行分片。

(GSO)generic-segmentation-offload: 支持tcp、UDP和ipv6分片,由软件模拟来完成,于硬件无关。

(GRO)generic-receive-offload: 支持tcp、UDP和ipv6分片,由软件模拟来完成,于硬件无关。

(LRO)large-receive-offload: 小包聚合成大包传递给协议栈,减小上层开销。




15.1 TSO/GSO

在发送数据包时,提到tcp_write_xmit函数,该函数会调用tcp_init_tso_segs,这个函数初始化一个skb的tso状态。
1429 static int tcp_init_tso_segs(const struct sock *sk, struct sk_buff *skb,
1430                  unsigned int mss_now)
1431 {
1432     int tso_segs = tcp_skb_pcount(skb);//获得skb被分成了多少个tso段,准确来说是tso特性支持了多个段。
//当1、当前分段成员值为0
//2、skb的mss和当前的mss值不相等,则需要初始化skb的TSO相关成员
1434     if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) {
1435         tcp_set_skb_tso_segs(sk, skb, mss_now);//设置skb的TSO成员。见下
1436         tso_segs = tcp_skb_pcount(skb);//获得准确的tso段个数。
1437     }
1438     return tso_segs;
1439 }
//
 977 static void tcp_set_skb_tso_segs(const struct sock *sk, struct sk_buff *skb,
 978                  unsigned int mss_now)
 979 {

//1、数据长度小于当前mss;2、网卡不支持gso;3、ip_summed原本存放的是校验和,但如果等于CHECKSUM_NONE,//则表示网卡没能完成校验和的计算,网卡不支持硬件计算校验和就会这样。以上三种情况没有必要分片。
 980     if (skb->len <= mss_now || !sk_can_gso(sk) ||
 981         skb->ip_summed == CHECKSUM_NONE) {// CHECKSUM_NONE,--device failed to checksum this packet.
 982         /* Avoid the costly divide in the normal
 983          * non-TSO case.
 984          */
 985         skb_shinfo(skb)->gso_segs = 1;
 986         skb_shinfo(skb)->gso_size = 0;
 987         skb_shinfo(skb)->gso_type = 0;
 988     } else {
 989         skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now); //倍数关系
 990         skb_shinfo(skb)->gso_size = mss_now;
//gso类型,比如SKB_GSO_TCPV4,SKB_GSO_UDP…
 991         skb_shinfo(skb)->gso_type = sk->sk_gso_type;
 992     }
 993 }

拥塞窗口
//返回true,则需要增加拥塞窗口,否则不需要增加拥塞窗口。
284 bool tcp_is_cwnd_limited(const struct sock *sk, u32 in_flight)
285 {
286     const struct tcp_sock *tp = tcp_sk(sk);
287     u32 left;
//已发送还未ack数据包等于发送拥塞窗口长度, 
289     if (in_flight >= tp->snd_cwnd)
290         return true;
//还可以发送的数据量。 
292     left = tp->snd_cwnd - in_flight;
//
293     if (sk_can_gso(sk) &&
294         left * sysctl_tcp_tso_win_divisor < tp->snd_cwnd &&
295         left * tp->mss_cache < sk->sk_gso_max_size &&
296         left < sk->sk_gso_max_segs)
297         return true;
298     return left <= tcp_max_tso_deferred_mss(tp);
299 }

为了最小化TSO分片,延迟发送数据包。TSONagle算法。返回值是1,表示可以延迟发送,返回0,则立即发送。

1596 static bool tcp_tso_should_defer(struct sock *sk, struct sk_buff *skb)
1597 {
1598     struct tcp_sock *tp = tcp_sk(sk);
1599     const struct inet_connection_sock *icsk = inet_csk(sk);
1600     u32 send_win, cong_win, limit, in_flight;
1601     int win_divisor;
// TCPHDR_FIN意味着是close()调用触发的关闭套接字连接,则立即发送。
1603     if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
1604         goto send_now;
//正常状态,则立即发送。
1606     if (icsk->icsk_ca_state != TCP_CA_Open)
1607         goto send_now;
1608 
//上一个skb被延迟了,且超过一个时钟,则立即发送。
1610     if (tp->tso_deferred &&
1611         (((u32)jiffies << 1) >> 1) - (tp->tso_deferred >> 1) > 1)
1612         goto send_now;
//发送但没收到ack包,包括重传的数据包。
1614     in_flight = tcp_packets_in_flight(tp);
//通告窗口剩余量
1618     send_win = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
//拥塞窗口剩余量。
1621     cong_win = (tp->snd_cwnd - in_flight) * tp->mss_cache;
//而这较小值,作为发送门限。
1623     limit = min(send_win, cong_win);
1624 
1625     /* If a full-sized TSO skb can be sent, do it. */
1626     if (limit >= min_t(unsigned int, sk->sk_gso_max_size,
1627                sk->sk_gso_max_segs * tp->mss_cache))
1628         goto send_now;
1629 
1630     /* Middle in queue won't get any more data, full sendable already? */
1631     if ((skb != tcp_write_queue_tail(sk)) && (limit >= skb->len))
1632         goto send_now;
1633 
1634     win_divisor = ACCESS_ONCE(sysctl_tcp_tso_win_divisor);
1635     if (win_divisor) {
//一个RTT内能够发送的最大字节数。
1636         u32 chunk = min(tp->snd_wnd, tp->snd_cwnd * tp->mss_cache);
1637 
1638         /* If at least some fraction of a window is available,
1639          * just use it.
1640          */
1641         chunk /= win_divisor; //单个TSO占用的发送量。
1642         if (limit >= chunk)
1643             goto send_now;
1644     } else {
1645         /* Different approach, try not to defer past a single
1646          * ACK.  Receiver should ACK every other full sized
1647          * frame, so if we have space for more than 3 frames
1648          * then send now.
1649          */
1650         if (limit > tcp_max_tso_deferred_mss(tp) * tp->mss_cache)
1651             goto send_now;
1652     }
1653 
1654     /* Ok, it looks like it is advisable to defer.
1655      * Do not rearm the timer if already set to not break TCP ACK clocking.
1656      */
1657     if (!tp->tso_deferred)
1658         tp->tso_deferred = 1 | (jiffies << 1); //记录延迟时间戳
1659 
1660     return true;
1661 
1662 send_now:
1663     tp->tso_deferred = 0;
1664     return false;
1665 }

15.2 LRO/GRO

15.1节所述特性用在skb的发送流程上,而这一节的特性用在了skb的接收流程上,LRO(Large Receive Offload)针对TCP数据包计数,将多个数据包聚合到一个大skb发送给上层协议栈,这样提高网络效率,GRO是更通用的版本。

    LRO管理结构体Include/linux/inet_lro.h

74 struct net_lro_mgr {
 75     struct net_device *dev; //关联LRO功能和网络设备
 76     struct net_lro_stats stats; //LRO 统计信息
 77 
 78     /* LRO features */
 79     unsigned long features; //如下#define定义的特性
 80 #define LRO_F_NAPI      //通过NAPI方式传递Packet
 81 #define LRO_F_EXTRACT_VLAN_ID 2  // VLAN 和8021Q
89     u32 ip_summed; //在启用分片模式时当生成的SKB没有被添加到分片管理链表ip_summed会被置位
 90     u32 ip_summed_aggr; /* 聚合成大的SKBs校验CHECKSUM_UNNECESSARY或者CHECKSUM_NONE */
 92 
 93     int max_desc; //LRO描述符最多个数,每一个LRO描述符对应一路TCP流。
 94     int max_aggr; //聚合成一个SKB的最多包数目
 95 
 96     int frag_align_pad; /* 当使用分片时为L3在生成的SKB对其头而预留的填充
 98 
 99     struct net_lro_desc *lro_arr; /* Array of LRO descriptors */
// get_skb_header: returns tcp and ip header for packet in SKB
106     int (*get_skb_header)(struct sk_buff *skb, void **ip_hdr,
107                   void **tcpudp_hdr, u64 *hdr_flags, void *priv);
108 
109     /* hdr_flags: */
110 #define LRO_IPV4 1 /* ip_hdr is IPv4 header */
111 #define LRO_TCP  2 /* tcpudp_hdr is TCP header */
112 
113     /*
114      * get_frag_header: returns mac, tcp and ip header for packet in SKB
115      *
116      * @hdr_flags: Indicate what kind of LRO has to be done
117      *             (IPv4/IPv6/TCP/UDP)
118      */
119     int (*get_frag_header)(struct skb_frag_struct *frag, void **mac_hdr,
120                    void **ip_hdr, void **tcpudp_hdr, u64 *hdr_flags,
121                    void *priv);
122 };

LRO属于NICs特性,在具体NICs驱动程序接收数据包时会调用lro_receive_skb函数以lro方式接收数据包。

496 void lro_receive_skb(struct net_lro_mgr *lro_mgr,
497              struct sk_buff *skb,
498              void *priv)
499 {
500     if (__lro_proc_skb(lro_mgr, skb, priv)) {
501         if (lro_mgr->features & LRO_F_NAPI)
502             netif_receive_skb(skb);
503         else
504             netif_rx(skb);
505     }
506 }

通常数据包会聚合到最大值,但是对延迟要求苛刻可能立即发送,这会调用lro_flush

 

GRO是LRO的加强版,其使用范围比LRO要广。然后__napi_gro_receive 会调用 dev_gro_receive ,dev_gro_receive会先调用ptype->gso_receive,一般而言就是ip协议对应的inet_gso_receive

inet_gro_receive 主要做如下事情:

首先拿到ip包头,然后对包头做check,如果check passed则开始遍历napi_struct->gro_list,根据ip saddr, daddr, tos, protocol等来和那些之前在二层有可能是同一flow的skb进行判断,如果不一致就把same_flow置0,当然光是slow_flow并不能就此开始merge,还要进行flush的判断,任何flush判断不过都会放弃merge而调用直接调用skb_gro_flush函数交到协议栈上层去处理

ip层结束之后,会继续tcp层的gro_receive,调用tcp_gro_receive ,其核心也是遍历napi_struct->gro_list,基于source addr判断是否是same_flow,对是否需要flush做计算,这里提一下关于ack一致的要求,ack一致说明是同一个tcp payload被tso/gso分段之后的结果,所以是必需条件

如果 tcp 也认为不需要flush,那么会进到 skb_gro_receive 中,这个函数就是用来合并的,第一个参数是gro_list里的那个skb,第二个是新来的skb,这里不多说了,我推荐的博客文章里讲的很清楚了。其实就分两种情况,如果是scatter-gather的skb包,则把新skb里的frags的数据放到gro_list的skb对应的frags数据后面;否则skb数据都在skb的线性地址中,这样直接alloc一个新的skb,把新skb挂到frag_list里面,最后放到原来gro_list的位置上;如果gro_list的skb已经有了frag_list,那么就直接挂进去好了

现在返回到dev_gro_receive中了,这时如果需要flush或者same_flow为0,说明需要传给上层协议栈了,此时调用napi_gro_complete

走到最后一种情况即这个skb是个新的flow,那么就加到gro_list的链表中

 

最后提下,所谓flush是指把现有gro_list中的skb flush到上层协议。

1539 struct packet_type {
1540     __be16          type;   /* This is really htons(ether_type). */
1541     struct net_device   *dev;   /* NULL is wildcarded here       */
1542     int         (*func) (struct sk_buff *,
1543                      struct net_device *,
1544                      struct packet_type *,
1545                      struct net_device *);
1546     bool            (*id_match)(struct packet_type *ptype,
1547                         struct sock *sk);
1548     void            *af_packet_priv;
1549     struct list_head    list;
1550 };
1551 
1552 struct offload_callbacks {
1553     struct sk_buff      *(*gso_segment)(struct sk_buff *skb,
1554                         netdev_features_t features);
1555     int         (*gso_send_check)(struct sk_buff *skb);
1556     struct sk_buff      **(*gro_receive)(struct sk_buff **head,
1557                            struct sk_buff *skb);
1558     int         (*gro_complete)(struct sk_buff *skb);
1559 };
1560 
1561 struct packet_offload {
1562     __be16           type;  /* This is really htons(ether_type). */
1563     struct offload_callbacks callbacks;
1564     struct list_head     list;
1565 };
Include/linux/netdevice.h
306 struct napi_struct {
 307     /* The poll_list must only be managed by the entity which
 308      * changes the state of the NAPI_STATE_SCHED bit.  This means
 309      * whoever atomically sets that bit can add this napi_struct
 310      * to the per-cpu poll_list, and whoever clears that bit
 311      * can remove from the list right before clearing the bit.
 312      */
 313     struct list_head    poll_list;
 314 
 315     unsigned long       state;
 316     int         weight;
 317     unsigned int        gro_count;
 318     int         (*poll)(struct napi_struct *, int);
 319 #ifdef CONFIG_NETPOLL
 320     spinlock_t      poll_lock;
 321     int         poll_owner;
 322 #endif
 323     struct net_device   *dev;
 324     struct sk_buff      *gro_list;
 325     struct sk_buff      *skb;
 326     struct list_head    dev_list;
 327 };

Net/core/dev.c
3829 static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
3830 {
3831     switch (ret) {
3832     case GRO_NORMAL:
3833         if (netif_receive_skb(skb))
3834             ret = GRO_DROP;
3835         break;
3836 
3837     case GRO_DROP:
3838         kfree_skb(skb);
3839         break;
3840 
3841     case GRO_MERGED_FREE:
3842         if (NAPI_GRO_CB(skb)->free == NAPI_GRO_FREE_STOLEN_HEAD)
3843             kmem_cache_free(skbuff_head_cache, skb);
3844         else
3845             __kfree_skb(skb);
3846         break;
3847 
3848     case GRO_HELD:
3849     case GRO_MERGED:
3850         break;
3851     }
3852 
3853     return ret;
3854 }
3855 
3856 static void skb_gro_reset_offset(struct sk_buff *skb)
3857 {
3858     const struct skb_shared_info *pinfo = skb_shinfo(skb);
3859     const skb_frag_t *frag0 = &pinfo->frags[0];
3860 
3861     NAPI_GRO_CB(skb)->data_offset = 0;
3862     NAPI_GRO_CB(skb)->frag0 = NULL;
3863     NAPI_GRO_CB(skb)->frag0_len = 0;
3864 
3865     if (skb->mac_header == skb->tail &&
3866         pinfo->nr_frags &&
3867         !PageHighMem(skb_frag_page(frag0))) {
3868         NAPI_GRO_CB(skb)->frag0 = skb_frag_address(frag0);
3869         NAPI_GRO_CB(skb)->frag0_len = skb_frag_size(frag0);
3870     }
3871 }
3872 
3873 gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
3874 {
3875     skb_gro_reset_offset(skb);
3876 
3877     return napi_skb_finish(dev_gro_receive(napi, skb), skb);
3878 }

3736 static enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
3737 {
3738     struct sk_buff **pp = NULL;
3739     struct packet_offload *ptype;
3740     __be16 type = skb->protocol;
3741     struct list_head *head = &offload_base;
3742     int same_flow;
3743     enum gro_result ret;
3744 
3745     if (!(skb->dev->features & NETIF_F_GRO) || netpoll_rx_on(skb))
3746         goto normal;
3747 
3748     if (skb_is_gso(skb) || skb_has_frag_list(skb))
3749         goto normal;
3750 
3751     gro_list_prepare(napi, skb);
3752 
3753     rcu_read_lock();
3754     list_for_each_entry_rcu(ptype, head, list) {
3755         if (ptype->type != type || !ptype->callbacks.gro_receive)
3756             continue;
3757 
3758         skb_set_network_header(skb, skb_gro_offset(skb));
3759         skb_reset_mac_len(skb);
3760         NAPI_GRO_CB(skb)->same_flow = 0;
3761         NAPI_GRO_CB(skb)->flush = 0;
3762         NAPI_GRO_CB(skb)->free = 0;
3763 
3764         pp = ptype->callbacks.gro_receive(&napi->gro_list, skb);
3765         break;
3766     }
3767     rcu_read_unlock();
3768 
3769     if (&ptype->list == head)
3770         goto normal;
3771 
3772     same_flow = NAPI_GRO_CB(skb)->same_flow;
3773     ret = NAPI_GRO_CB(skb)->free ? GRO_MERGED_FREE : GRO_MERGED;
3774 
3775     if (pp) {
3776         struct sk_buff *nskb = *pp;
3777 
3778         *pp = nskb->next;
3779         nskb->next = NULL;
3780         napi_gro_complete(nskb);
3781         napi->gro_count--;
3782     }
3783 
3784     if (same_flow)
3785         goto ok;
3786 
3787     if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS)
3788         goto normal;
3789 
3790     napi->gro_count++;
3791     NAPI_GRO_CB(skb)->count = 1;
3792     NAPI_GRO_CB(skb)->age = jiffies;
3793     skb_shinfo(skb)->gso_size = skb_gro_len(skb);
3794     skb->next = napi->gro_list;
3795     napi->gro_list = skb;
3796     ret = GRO_HELD;
3797 
3798 pull:
3799     if (skb_headlen(skb) < skb_gro_offset(skb)) {
3800         int grow = skb_gro_offset(skb) - skb_headlen(skb);
3801 
3802         BUG_ON(skb->end - skb->tail < grow);
3803 
3804         memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
3805 
3806         skb->tail += grow;
3807         skb->data_len -= grow;
3808 
3809         skb_shinfo(skb)->frags[0].page_offset += grow;
3810         skb_frag_size_sub(&skb_shinfo(skb)->frags[0], grow);
3811 
3812         if (unlikely(!skb_frag_size(&skb_shinfo(skb)->frags[0]))) {
3813             skb_frag_unref(skb, 0);
3814             memmove(skb_shinfo(skb)->frags,
3815                 skb_shinfo(skb)->frags + 1,
3816                 --skb_shinfo(skb)->nr_frags * sizeof(skb_frag_t));
3817         }
3818     }
3819 
3820 ok:
3821     return ret;
3822 
3823 normal:
3824     ret = GRO_NORMAL;
3825     goto pull;
3826 }

15.3 RSS(Receive Side Scaling)队列:

网卡多队列技术,硬件支持这一特性,将受到的数据包发往不同的队列,已在不同的CPU上处理。RSS流程如图15.2,NICs在接收到串行数据包后,根据数据包的L3层和L4层头信息,硬件计算哈希值,不同的哈希值对应于不同的队列,即将数据包按哈希值分配到不同的队列上,不同的队列有不同的CPU处理,这样就实现了负载均衡。这一特性在drivers/net/ ethernet/XXX等各种网卡驱动中设置网卡RSS控制寄存器来实现。目前内核支持的队列个数取8和CPU个数中较小的那个值。每个队列都有一个IRQ号,对应中断的服务CPU号(一个或多个)使用affinity设置,见Documentation/IRQ-affinity.txt。


图15.2 RSS队列处理流程

15.4 RPS(Receive Packet Steering)队列:

RPS(Receive Packet Steering)源于谷歌patch,软件方法实现RSS。将数据包的源IP和目的IP、源端口和目的端口计算得到一个哈希值,根据该哈希值决定了软中断执行的核号,,这种方法来均衡软中断在各个核的分布,以其达到负载均衡的目的。

sysctl -w net.core.rps_sock_flow_entries=

/sys/class/net/eth*/queues/rx-*/rps_flow_cnt

/sys/class/net/eth*/queues/rx-*/rps_cpus

两个重要的函数

该结构体包含一个RX队列。Include/linux/netdevice.h

struct rps_map {
//零长数组的长度
	unsigned int len;
	struct rcu_head rcu;
//能够处理RPS数据包的cpu ID数组。
	u16 cpus[0];
};
struct netdev_rx_queue {
//保存一个RPS映射,映射的对象是能够处理这个队列的CPU核数组。
	struct rps_map __rcu		*rps_map;
//流控映射表
	struct rps_dev_flow_table __rcu	*rps_flow_table;
	struct kobject			kobj;
	struct net_device		*dev;
} ____cacheline_aligned_in_smp;

get_rps_cpu由netif_receive_skb(NAPI)和netif_rx函数调用,为一个给定的skb,从RPS接收队列映射关系中返回处理该数据包的CPU ID。哈希值计算可以网卡计算。

Net/core/dev.c

2939 static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
2940                struct rps_dev_flow **rflowp)
2941 {
2942     struct netdev_rx_queue *rxqueue;
2943     struct rps_map *map;
2944     struct rps_dev_flow_table *flow_table;
2945     struct rps_sock_flow_table *sock_flow_table; //包含skb期望在那个核被处理信息。
2946     int cpu = -1;
2947     u16 tcpu;
//获得网卡对应的接收队列
2949     if (skb_rx_queue_recorded(skb)) { //判断是否支持多队列,一个队列对应于一个netdev_rx_queue 
2950         u16 index = skb_get_rx_queue(skb); //获得skb所在队列的索引
2951         if (unlikely(index >= dev->real_num_rx_queues)) { //索引范围合法性检查
2952             WARN_ONCE(dev->real_num_rx_queues > 1,
2953                   "%s received packet on queue %u, but number "
2954                   "of RX queues is %u\n",
2955                   dev->name, index, dev->real_num_rx_queues);
2956             goto done;
2957         }
2958         rxqueue = dev->_rx + index; //队列首地址+队列索引,获得skb所在的队列。
2959     } else
2960         rxqueue = dev->_rx; //不支持多队列,直接返回即可。
//rcu变量引用方法,获得rps_map员。2962行判断该成员是否为NULL,如果不为空,那么可能存在能够处理该队列的CPU //ID了
2962     map = rcu_dereference(rxqueue->rps_map);
2963     if (map) { 
2964         if (map->len == 1 &&
2965             !rcu_access_pointer(rxqueue->rps_flow_table)) {//
2966             tcpu = map->cpus[0]; //获得RPS对应的CPU ID。
2967             if (cpu_online(tcpu)) //判断该CPU是否被启动(多个CPU情况,不一定每个CPU核都被启用)
2968                 cpu = tcpu;
2969             goto done;
2970         }
2971     } else if (!rcu_access_pointer(rxqueue->rps_flow_table)) {
2972         goto done; 
2973     }
//获得网络头
2975     skb_reset_network_header(skb);
2976     if (!skb_get_rxhash(skb)) //获得计算得到的哈希值。
2977         goto done;
//2979~3016流控处理,见RFS一节
2979     flow_table = rcu_dereference(rxqueue->rps_flow_table);
2980     sock_flow_table = rcu_dereference(rps_sock_flow_table); //包含映射了应用程序处理的流控信息。
2981     if (flow_table && sock_flow_table) {
2982         u16 next_cpu;
2983         struct rps_dev_flow *rflow;
2984 
2985         rflow = &flow_table->flows[skb->rxhash & flow_table->mask];
2986         tcpu = rflow->cpu; //流控处理期望所在的CPU
2987 
2988         next_cpu = sock_flow_table->ents[skb->rxhash &
2989             sock_flow_table->mask]; //流控处理期望在该CPU上被处理
2990 
3002         if (unlikely(tcpu != next_cpu) && //两者不一致,需要进一步判断是否需要进行切换CPU。
3003             (tcpu == RPS_NO_CPU || !cpu_online(tcpu) ||
3004              ((int)(per_cpu(softnet_data, tcpu).input_queue_head -
3005               rflow->last_qtail)) >= 0)) {
3006             tcpu = next_cpu;
3007             rflow = set_rps_cpu(dev, skb, rflow, next_cpu); //见RFS队列。
3008         }
3009 
3010         if (tcpu != RPS_NO_CPU && cpu_online(tcpu)) { //RPS设置的CPU启用,并且这里流控处理核也是该CPU
3011             *rflowp = rflow;
3012             cpu = tcpu;
3013             goto done;
3014         }
3015     }
//不做流控处理会到达这里,根据计算得到的哈希值,获取对应的CPU核号。
3017     if (map) {
3018         tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32];
3019 
3020         if (cpu_online(tcpu)) { //CPU 处于开启状态。
3021             cpu = tcpu;
3022             goto done;
3023         }
3024     }
3025 
3026 done:
3027     return cpu;
3028 }

enqueue_to_backlog用于将受到的skb放到对应CPU的RPS队列上。

net/core/dev.c
3106 static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
3107                   unsigned int *qtail)
3108 {
3109     struct softnet_data *sd;
3110     unsigned long flags;
3111 
3112     sd = &per_cpu(softnet_data, cpu); //获得第二个参数cpu所指ID的cpu的softnet_data成员。
3113 
3114     local_irq_save(flags);
3115 
3116     rps_lock(sd);
//如果存储于接收队列(input_pkt_queue)的skb个数大于设备设定的最大值,则到3137行,更新丢弃计数并丢弃该数据包
3117     if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) { 
3118         if (skb_queue_len(&sd->input_pkt_queue)) {
3119 enqueue:
3120             __skb_queue_tail(&sd->input_pkt_queue, skb); //将数据包放到队列尾部
3121             input_queue_tail_incr_save(sd, qtail);//更新计数
3122             rps_unlock(sd);
3123             local_irq_restore(flags);
3124             return NET_RX_SUCCESS;
3125         }
3126 
//NAPI方式,一个硬件中断会接收多个数据包。
3130         if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
3131             if (!rps_ipi_queued(sd)) //检查该sd是否是其它CPU的,如果是其它CPU,则____napi_schedule被调用。
3132                 ____napi_schedule(sd, &sd->backlog);
3133         }
3134         goto enqueue;
3135     }
3136 
3137     sd->dropped++; //丢弃计数更新
3138     rps_unlock(sd);
3139 
3140     local_irq_restore(flags);
3141 
3142     atomic_long_inc(&skb->dev->rx_dropped);
3143     kfree_skb(skb); //丢弃该skb
3144     return NET_RX_DROP;
3145 }

15.5RFS(Receive Flow Steering)AcceleratedReceive Flow Steering

不论是RSS还是RPS考虑都是将收到的数据包进行哈希散列,这没有考虑到应用程序所在的CPU号,加入一个等待数据包的网络进程正在5号CPU上等待数据包,而RPS计算得到的哈希值是1,这就意味着数据包将将由1号CPU接收。这会导致一个问题,那就是缓存命中失败。RFS利用RPS机制将数据包插入指定CPU的backlog队列,并唤醒那个CPU执行。该函数在RPS一节调用到。全局数据流表(rps_sock_flow_table)的总数可以通过下面的参数来设置:

 /proc/sys/net/core/rps_sock_flow_entries

每个队列的数据流表总数可以通过下面的参数来设置:

 /sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt

2889 static struct rps_dev_flow *
2890 set_rps_cpu(struct net_device *dev, struct sk_buff *skb,
2891         struct rps_dev_flow *rflow, u16 next_cpu)
2892 {
2893     if (next_cpu != RPS_NO_CPU) {
2894 #ifdef CONFIG_RFS_ACCEL
2895         struct netdev_rx_queue *rxqueue;
2896         struct rps_dev_flow_table *flow_table;
2897         struct rps_dev_flow *old_rflow;
2898         u32 flow_id;
2899         u16 rxq_index;
2900         int rc;
2901 
2902         /* Should we steer this flow to a different hardware queue? */
//skb_rx_queue_recorded判断skb多队列映射支持是否置位,没置位直接goto out
// rx_cpu_rmap 设备是否支持CPU亲和度
//dev->features & NETIF_F_NTUPLE 判断NIC是否支持n元匹配过滤
//上述任何一个回答是否,将执行goto out;
2903         if (!skb_rx_queue_recorded(skb) || !dev->rx_cpu_rmap ||
2904             !(dev->features & NETIF_F_NTUPLE))
2905             goto out;
2906         rxq_index = cpu_rmap_lookup_index(dev->rx_cpu_rmap, next_cpu);
2907         if (rxq_index == skb_get_rx_queue(skb)) //接收队列在期望的CPU上,直接返回就行了?
2908             goto out;
2909 
2910         rxqueue = dev->_rx + rxq_index; //获得期望CPU的接收队列索引
2911         flow_table = rcu_dereference(rxqueue->rps_flow_table);
2912         if (!flow_table)
2913             goto out;
2914         flow_id = skb->rxhash & flow_table->mask;
//ndo_rx_flow_steer是NIC硬件支持RFS配置接口,CONFIG_RFS_ACCEL配置选项用于表示是否硬件支持
2915         rc = dev->netdev_ops->ndo_rx_flow_steer(dev, skb,
2916                             rxq_index, flow_id); 
2917         if (rc < 0)
2918             goto out;
2919         old_rflow = rflow;
2920         rflow = &flow_table->flows[flow_id]; //将流放在期望CPU的流控结构里
2921         rflow->filter = rc;//接收过滤器标志
2922         if (old_rflow->filter == rflow->filter)
2923             old_rflow->filter = RPS_NO_FILTER;
2924     out:
2925 #endif
2926         rflow->last_qtail =
2927             per_cpu(softnet_data, next_cpu).input_queue_head;
2928     }
2929 
2930     rflow->cpu = next_cpu;
2931     return rflow;
2932 }


15.6 XPS(Transmit Packet Steering)

上面都是对接收数据包的处理,XPS是对发送数据包的多队列处理。XPS通过将发送数据包的CPU映射到一个队列的方法来实现。这类似于RPS不过是在发送端的“RPS”,RPS基于数据包队列选择处理的CPU,而XPS则是基于发送数据包的CPU选择发送队列。

一个发送队列可以由一个或多个CPU支持,配置参数位于:

/sys/class/net/eth<n>/queues/tx-<n>/xps_cpus

XPS支持

struct xps_map {
	unsigned int len; //记录queues[0]成员长度
	unsigned int alloc_len;
	struct rcu_head rcu;
	u16 queues[0]; //队列数组
};
#define XPS_MAP_SIZE(_num) (sizeof(struct xps_map) + ((_num) * sizeof(u16))) //映射num个队列时,xps_map的size
#define XPS_MIN_MAP_ALLOC ((L1_CACHE_BYTES - sizeof(struct xps_map))	\
    / sizeof(u16))
//NICs的XPS映射,映射由CPU索引
struct xps_dev_maps {
	struct rcu_head rcu;
	struct xps_map __rcu *cpu_map[0];
};
#define XPS_DEV_MAPS_SIZE (sizeof(struct xps_dev_maps) +		\
    (nr_cpu_ids * sizeof(struct xps_map *)))

在dev_queue_xmit函数发送数据包时,会调用netdev_pick_tx选定发送队列。

由skb和dev信息获得对应的发送队列索引函数位于net/core/flow_dissector.c文件。

364 struct netdev_queue *netdev_pick_tx(struct net_device *dev,
365                     struct sk_buff *skb)
366 {
367     int queue_index = 0;
// real_num_tx_queues 是NICs当前发送数据包队列活跃的队列数。如果不是1,则表示启用了多队列技术。
369     if (dev->real_num_tx_queues != 1) { 
370         const struct net_device_ops *ops = dev->netdev_ops;//获得NICs操作方法
371         if (ops->ndo_select_queue)//NICs支持队列选择。
372             queue_index = ops->ndo_select_queue(dev, skb);
373         else
374             queue_index = __netdev_pick_tx(dev, skb);//见下面。
375         queue_index = dev_cap_txqueue(dev, queue_index);
376     }
377 
378     skb_set_queue_mapping(skb, queue_index);//存储设备多队列映射索引到skb中。
379     return netdev_get_tx_queue(dev, queue_index);
380 }

__netdev_pick_tx用于获取发送队列映射索引,如果映射索引变化,那么路由项相应的也要跟着变化。

337 u16 __netdev_pick_tx(struct net_device *dev, struct sk_buff *skb)
338 {
339     struct sock *sk = skb->sk;
340     int queue_index = sk_tx_queue_get(sk); //获得skb的发送队列映射的索引值。
// 如果queue_index非法,表明发送队列索引值需要重建,或者ooo_okay允许映射到发送队列的skb被改变标志置位,则重新//映射
342     if (queue_index < 0 || skb->ooo_okay ||
343         queue_index >= dev->real_num_tx_queues) {
344         int new_index = get_xps_queue(dev, skb); //计算新的映射索引值,见下文。
345         if (new_index < 0) //返回值小于0,说明不支持XPS。
346             new_index = skb_tx_hash(dev, skb);//根据设备是否支持发送队列映射方法获取发送队列索引
347 
//新的索引值和原理的索引值,不一致,则需要更换skb的路由项,以实现流控
348         if (queue_index != new_index && sk) {
349             struct dst_entry *dst =
350                     rcu_dereference_check(sk->sk_dst_cache, 1); //获得路由项
351 
352             if (dst && skb_dst(skb) == dst)
353                 sk_tx_queue_set(sk, queue_index);
354 
355         }
356 
357         queue_index = new_index;
358     }
359 
360     return queue_index;
361 }

299 static inline int get_xps_queue(struct net_device *dev, struct sk_buff *skb)
300 {
301 #ifdef CONFIG_XPS
302     struct xps_dev_maps *dev_maps;
303     struct xps_map *map;
304     int queue_index = -1;
305 
306     rcu_read_lock();
307     dev_maps = rcu_dereference(dev->xps_maps);
308     if (dev_maps) {
309         map = rcu_dereference(
310             dev_maps->cpu_map[raw_smp_processor_id()]); //获取当前CPU的XPS流控映射表
311         if (map) {
312             if (map->len == 1)//XPS流控映射表只有一项,则直接返回该项。
313                 queue_index = map->queues[0];
314             else {
315                 u32 hash;
316                 if (skb->sk && skb->sk->sk_hash)// sk_hash是协议查找表的哈希值。
317                     hash = skb->sk->sk_hash;
318                 else//否则,需要根据协议计算这个哈希值。
319                     hash = (__force u16) skb->protocol ^
320                         skb->rxhash;//接收队列的哈希值。
321                 hash = jhash_1word(hash, hashrnd);//上述哈希值再次哈希
322                 queue_index = map->queues[
323                     ((u64)hash * map->len) >> 32];//新的发送队列索引值
324             }
325             if (unlikely(queue_index >= dev->real_num_tx_queues))
326                 queue_index = -1;
327         }
328     }
329     rcu_read_unlock();
330 
331     return queue_index;
332 #else
333     return -1;
334 #endif
335 }








     




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shichaog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值