免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12345下一页
最近访问板块 发新帖
查看: 28252 | 回复: 44
打印 上一主题 下一主题

Receive packet steering patch详解 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2010-07-29 09:53 |只看该作者 |倒序浏览
Receive packet steering简称rps,是google贡献给linux kernel的一个patch,主要的功能是解决多核情况下,网络协议栈的软中断的负载均衡。这里的负载均衡也就是指能够将软中断均衡的放在不同的cpu核心上运行。

简介在这里:
http://lwn.net/Articles/362339/

linux现在网卡的驱动支持两种模式,一种是NAPI,一种是非NAPI模式,这两种模式的区别,我前面的blog都有介绍,这里就再次简要的介绍下。

在NAPI中,中断收到数据包后调用__napi_schedule调度软中断,然后软中断处理函数中会调用注册的poll回掉函数中调用netif_receive_skb将数据包发送到3层,没有进行任何的软中断负载均衡。

在非NAPI中,中断收到数据包后调用netif_rx,这个函数会将数据包保存到input_pkt_queue,然后调度软中断,这里为了兼容NAPI的驱动,他的poll方法默认是process_backlog,最终这个函数会从input_pkt_queue中取得数据包然后发送到3层。

通过比较我们可以看到,不管是NAPI还是非NAPI的话都无法做到软中断的负载均衡,因为软中断此时都是运行在在硬件中断相应的cpu上。也就是说如果始终是cpu0相应网卡的硬件中断,那么始终都是cpu0在处理软中断,而此时cpu1就被浪费了,因为无法并行的执行多个软中断。

google的这个patch的基本原理是这样的,根据数据包的源地址,目的地址以及目的和源端口(这里它是将两个端口组合成一个4字节的无符数进行计算的,后面会看到)计算出一个hash值,然后根据这个hash值来选择软中断运行的cpu,从上层来看,也就是说将每个连接和cpu绑定,并通过这个hash值,来均衡软中断在多个cpu上。

这个介绍比较简单,我们来看代码是如何实现的。

它这里主要是hook了两个内核的函数,一个是netif_rx主要是针对非NAPI的驱动,一个是netif_receive_skb这个主要是针对NAPI的驱动,这两个函数我前面blog都有介绍过,想了解可以看我前面的blog,现在这里我只介绍打过patch的实现。

在看netif_rx和netif_receive_skb之前,我们先来看这个patch中两个重要的函数get_rps_cpu和enqueue_to_backlog,我们一个个看。

先来看相关的两个数据结构,首先是netdev_rx_queue,它表示对应的接收队列,因为有的网卡可能硬件上就支持多队列的模式,此时对应就会有多个rx队列,这个结构是挂载在net_device中的,也就是每个网络设备最终都会有一个或者多个rx队列。这个结构在sys文件系统中的表示类似这样的/sys/class/net/<device>/queues/rx-<n> 几个队列就是rx-n.
  1. struct netdev_rx_queue {
  2. //保存了当前队列的rps map
  3.         struct rps_map *rps_map;
  4. //对应的kobject
  5.         struct kobject kobj;
  6. //指向第一个rx队列
  7.         struct netdev_rx_queue *first;
  8. //引用计数
  9.         atomic_t count;
  10. } ____cacheline_aligned_in_smp;
复制代码
然后就是rps_map,其实这个也就是保存了能够执行数据包的cpu。
  1. struct rps_map {
  2. //cpu的个数,也就是cpus数组的个数
  3.         unsigned int len;
  4. //RCU锁
  5.         struct rcu_head rcu;
  6. //保存了cpu的id.
  7.         u16 cpus[0];
  8. };
复制代码
看完上面的结构,我们来看函数的实现。
get_rps_cpu主要是通过传递进来的skb然后来选择这个skb所应该被处理的cpu。它的逻辑很简单,就是通过skb计算hash,然后通过hash从对应的队列的rps_mapping中取得对应的cpu id。

这里有个要注意的就是这个hash值是可以交给硬件网卡去计算的,作者自己说是最好交由硬件去计算这个hash值,因为如果是软件计算的话会导致CPU 缓存不命中,带来一定的性能开销。

还有就是rps_mapping这个值是可以通过sys 文件系统设置的,位置在这里:
/sys/class/net/<device>/queues/rx-<n>/rps_cpus 。
  1. static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb)
  2. {
  3.         struct ipv6hdr *ip6;
  4.         struct iphdr *ip;
  5.         struct netdev_rx_queue *rxqueue;
  6.         struct rps_map *map;
  7.         int cpu = -1;
  8.         u8 ip_proto;
  9.         u32 addr1, addr2, ports, ihl;
  10. //rcu锁
  11.         rcu_read_lock();
  12. //取得设备对应的rx 队列
  13.         if (skb_rx_queue_recorded(skb)) {
  14.         ..........................................
  15.                 rxqueue = dev->_rx + index;
  16.         } else
  17.                 rxqueue = dev->_rx;

  18.         if (!rxqueue->rps_map)
  19.                 goto done;
  20. //如果硬件已经计算,则跳过计算过程
  21.         if (skb->rxhash)
  22.                 goto got_hash; /* Skip hash computation on packet header */

  23.         switch (skb->protocol) {
  24.         case __constant_htons(ETH_P_IP):
  25.                 if (!pskb_may_pull(skb, sizeof(*ip)))
  26.                         goto done;
  27. //得到计算hash的几个值
  28.                 ip = (struct iphdr *) skb->data;
  29.                 ip_proto = ip->protocol;
  30. //两个地址
  31.                 addr1 = ip->saddr;
  32.                 addr2 = ip->daddr;
  33. //得到ip头
  34.                 ihl = ip->ihl;
  35.                 break;
  36.         case __constant_htons(ETH_P_IPV6):
  37. ..........................................
  38.                 break;
  39.         default:
  40.                 goto done;
  41.         }
  42.         ports = 0;
  43.         switch (ip_proto) {
  44.         case IPPROTO_TCP:
  45.         case IPPROTO_UDP:
  46.         case IPPROTO_DCCP:
  47.         case IPPROTO_ESP:
  48.         case IPPROTO_AH:
  49.         case IPPROTO_SCTP:
  50.         case IPPROTO_UDPLITE:
  51.                 if (pskb_may_pull(skb, (ihl * 4) + 4))
  52. //我们知道tcp头的前4个字节就是源和目的端口,因此这里跳过ip头得到tcp头的前4个字节
  53.                         ports = *((u32 *) (skb->data + (ihl * 4)));
  54.                 break;

  55.         default:
  56.                 break;
  57.         }
  58. //计算hash
  59.         skb->rxhash = jhash_3words(addr1, addr2, ports, hashrnd);
  60.         if (!skb->rxhash)
  61.                 skb->rxhash = 1;

  62. got_hash:
  63. //通过rcu得到对应rps map
  64.         map = rcu_dereference(rxqueue->rps_map);
  65.         if (map) {
  66. //取得对应的cpu
  67.                 u16 tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32];
  68. //如果cpu是online的,则返回计算出的这个cpu,否则跳出循环。
  69.                 if (cpu_online(tcpu)) {
  70.                         cpu = tcpu;
  71.                         goto done;
  72.                 }
  73.         }

  74. done:
  75.         rcu_read_unlock();
  76. //如果上面失败,则返回-1.
  77.         return cpu;
  78. }
复制代码
然后是enqueue_to_backlog这个方法,首先我们知道在每个cpu都有一个softnet结构,而他有一个input_pkt_queue的队列,以前这个主要是用于非NAPi的驱动的,而这个patch则将这个队列也用与NAPI的处理中了。也就是每个cpu现在都会有一个input_pkt_queue队列,用于保存需要处理的数据包队列。这个队列作用现在是,如果发现不属于当前cpu处理的数据包,则我们可以直接将数据包挂载到他所属的cpu的input_pkt_queue中。

enqueue_to_backlog接受一个skb和cpu为参数,通过cpu来判断skb如何处理。要么加入所属的input_pkt_queue中,要么schecule 软中断。

还有个要注意就是我们知道NAPI为了兼容非NAPI模式,有个backlog的napi_struct结构,也就是非NAPI驱动会schedule backlog这个napi结构,而在enqueue_to_backlog中则是利用了这个结构,也就是它会schedule backlog,因为它会将数据放到input_pkt_queue中,而backlog的pool方法process_backlog就是从input_pkt_queue中取得数据然后交给上层处理。

这里还有一个会用到结构就是 rps_remote_softirq_cpus,它主要是保存了当前cpu上需要去另外的cpu schedule 软中断的cpu 掩码。因为我们可能将要处理的数据包放到了另外的cpu的input queue上,因此我们需要schedule 另外的cpu上的napi(也就是软中断),所以我们需要保存对应的cpu掩码,以便于后面遍历,然后schedule。

而这里为什么mask有两个元素,注释写的很清楚:
  1. /*
  2. * This structure holds the per-CPU mask of CPUs for which IPIs are scheduled
  3. * to be sent to kick remote softirq processing.  There are two masks since
  4. * the sending of IPIs must be done with interrupts enabled.  The select field
  5. * indicates the current mask that enqueue_backlog uses to schedule IPIs.
  6. * select is flipped before net_rps_action is called while still under lock,
  7. * net_rps_action then uses the non-selected mask to send the IPIs and clears
  8. * it without conflicting with enqueue_backlog operation.
  9. */
  10. struct rps_remote_softirq_cpus {
  11. //对应的cpu掩码
  12.         cpumask_t mask[2];
  13. //表示应该使用的数组索引
  14.         int select;
  15. };
复制代码
然后就是enqueue_backlog这个函数:
  1. static int enqueue_to_backlog(struct sk_buff *skb, int cpu)
  2. {
  3.         struct softnet_data *queue;
  4.         unsigned long flags;
  5. //取出传递进来的cpu的softnet-data结构
  6.         queue = &per_cpu(softnet_data, cpu);

  7.         local_irq_save(flags);
  8.         __get_cpu_var(netdev_rx_stat).total++;
  9. //自旋锁
  10.         spin_lock(&queue->input_pkt_queue.lock);
  11. //如果保存的队列还没到上限
  12.         if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
  13. //如果当前队列的输入队列长度不为空
  14.                 if (queue->input_pkt_queue.qlen) {
  15. enqueue:
  16. //将数据包加入到input_pkt_queue中,这里会有一个小问题,我们后面再说。
  17.                         __skb_queue_tail(&queue->input_pkt_queue, skb);
  18.                         spin_unlock_irqrestore(&queue->input_pkt_queue.lock,
  19.                             flags);
  20.                         return NET_RX_SUCCESS;
  21.                 }

  22.                 /* Schedule NAPI for backlog device */
  23. //如果可以调度软中断
  24.                 if (napi_schedule_prep(&queue->backlog)) {
  25. //首先判断数据包该不该当前的cpu处理
  26.                         if (cpu != smp_processor_id()) {
  27. //如果不该,
  28.                                 struct rps_remote_softirq_cpus *rcpus =
  29.                                     &__get_cpu_var(rps_remote_softirq_cpus);

  30.                                 cpu_set(cpu, rcpus->mask[rcpus->select]);
  31.                                 __raise_softirq_irqoff(NET_RX_SOFTIRQ);
  32.                         } else
  33. //如果就是应该当前cpu处理,则直接schedule 软中断,这里可以看到传递进去的是backlog
  34.                                 __napi_schedule(&queue->backlog);
  35.                 }
  36.                 goto enqueue;
  37.         }

  38.         spin_unlock(&queue->input_pkt_queue.lock);

  39.         __get_cpu_var(netdev_rx_stat).dropped++;
  40.         local_irq_restore(flags);

  41.         kfree_skb(skb);
  42.         return NET_RX_DROP;
  43. }
复制代码
这里会有一个小问题,那就是假设此时一个属于cpu0的包进入处理,此时我们运行在cpu1,此时将数据包加入到input队列,然后cpu0上面刚好又来了一个cpu0需要处理的数据包,此时由于qlen不为0则又将数据包加入到input队列中,我们会发现cpu0上的napi没机会进行调度了。

google的patch对这个是这样处理的,在软中断处理函数中当数据包处理完毕,会调用net_rps_action来调度前面保存到其他cpu上的input队列。

下面就是代码片断(net_rx_action)
  1. //得到对应的rcpus.
  2. rcpus = &__get_cpu_var(rps_remote_softirq_cpus);
  3.         select = rcpus->select;
  4. //翻转select,防止和enqueue_backlog冲突
  5.         rcpus->select ^= 1;

  6. //打开中断,此时下面的调度才会起作用.
  7.         local_irq_enable();
  8. //这个函数里面调度对应的远程cpu的napi.
  9.         net_rps_action(&rcpus->mask[select]);
复制代码
然后就是net_rps_action,这个函数很简单,就是遍历所需要处理的cpu,然后调度napi
  1. static void net_rps_action(cpumask_t *mask)
  2. {
  3.         int cpu;

  4.         /* Send pending IPI's to kick RPS processing on remote cpus. */
  5. //遍历
  6.         for_each_cpu_mask_nr(cpu, *mask) {
  7.                 struct softnet_data *queue = &per_cpu(softnet_data, cpu);
  8.                 if (cpu_online(cpu))
  9. //到对应的cpu调用csd方法。
  10.                         __smp_call_function_single(cpu, &queue->csd, 0);
  11.         }
  12. //清理mask
  13.         cpus_clear(*mask);
  14. }
复制代码
上面我们看到会调用csd方法,而上面的csd回掉就是被初始化为trigger_softirq函数。
  1. static void trigger_softirq(void *data)
  2. {
  3.         struct softnet_data *queue = data;
  4. //调度napi可以看到依旧是backlog 这个napi结构体。
  5.         __napi_schedule(&queue->backlog);
  6.         __get_cpu_var(netdev_rx_stat).received_rps++;
  7. }
复制代码
上面的函数都分析完毕了,剩下的就很简单了。

首先来看netif_rx如何被修改的,它被修改的很简单,首先是得到当前skb所应该被处理的cpu id,然后再通过比较这个cpu和当前正在处理的cpu id进行比较来做不同的处理。
  1. int netif_rx(struct sk_buff *skb)
  2. {
  3.         int cpu;

  4.         /* if netpoll wants it, pretend we never saw it */
  5.         if (netpoll_rx(skb))
  6.                 return NET_RX_DROP;

  7.         if (!skb->tstamp.tv64)
  8.                 net_timestamp(skb);
  9. //得到cpu id。
  10.         cpu = get_rps_cpu(skb->dev, skb);
  11.         if (cpu < 0)
  12.                 cpu = smp_processor_id();
  13. //通过cpu进行队列不同的处理
  14.         return enqueue_to_backlog(skb, cpu);
  15. }
复制代码
然后是netif_receive_skb,这里patch将内核本身的这个函数改写为__netif_receive_skb。然后当返回值小于0,则说明不需要对队列进行处理,此时直接发送到3层。
  1. int netif_receive_skb(struct sk_buff *skb)
  2. {
  3.         int cpu;

  4.         cpu = get_rps_cpu(skb->dev, skb);

  5.         if (cpu < 0)
  6.                 return __netif_receive_skb(skb);
  7.         else
  8.                 return enqueue_to_backlog(skb, cpu);
  9. }
复制代码
最后来总结一下,可以看到input_pkt_queue是一个FIFO的队列,而且如果当qlen有值的时候,也就是在另外的cpu有数据包放到input_pkt_queue中,则当前cpu不会调度napi,而是将数据包放到input_pkt_queue中,然后等待trigger_softirq来调度napi。

因此这个patch完美的解决了软中断在多核下的均衡问题,并且由于是同一个连接会map到相同的cpu,并且input_pkt_queue的使用,因此乱序的问题也不会出现。

我们部门有人测试了这个patch,据说能差不多提高20%。

我看了下35的内核,这个补丁还有另外一个也是这个作者的补丁RFS(rps的增强)都已经进入内核了,看来35的网络性能还是值得期待的。

评分

参与人数 1可用积分 +6 收起 理由
buzzle + 6 只能加两分,多谢分享.

查看全部评分

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
2 [报告]
发表于 2010-07-29 10:16 |只看该作者
多谢LZ的分享。高亮一把先。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
3 [报告]
发表于 2010-07-29 10:17 |只看该作者
标题可以写的更为容易理解的。。。

论坛徽章:
0
4 [报告]
发表于 2010-07-29 10:24 |只看该作者
看来 35 的网络性能还是值得期待的

论坛徽章:
0
5 [报告]
发表于 2010-07-29 12:59 |只看该作者
spin_lock(&queue->input_pkt_queue.lock);
之后怎么
spin_unlock_irqrestore(&queue->input_pkt_queue.lock, flags);
可以这么用吗?

论坛徽章:
0
6 [报告]
发表于 2010-07-29 13:49 |只看该作者
spin_lock(&queue->input_pkt_queue.lock);
之后怎么
spin_unlock_irqrestore(&queue->input_pkt_queue.l ...
platinum 发表于 2010-07-29 12:59


上面不是还有一个local_irq_save(flags); 呢?

论坛徽章:
0
7 [报告]
发表于 2010-07-29 13:50 |只看该作者
标题可以写的更为容易理解的。。。
Godbach 发表于 2010-07-29 10:17


找不到什么好的题目。。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
8 [报告]
发表于 2010-07-29 13:58 |只看该作者
这个patch是为了实现中断均衡的功能吧。

论坛徽章:
0
9 [报告]
发表于 2010-07-29 14:21 |只看该作者
上面不是还有一个local_irq_save(flags); 呢?
simohayha_cu 发表于 2010-07-29 13:49


哦,是我没说清楚
我的疑问在于,对于通一个 lock 变量,是否可以采用不同的 lock 方式
比如在有些代码中,看到了 spin_lock(lock),又同时看到有 spin_lock_bh(lock)

论坛徽章:
0
10 [报告]
发表于 2010-07-29 15:03 |只看该作者
多谢LZ的分享,学习了
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP