对于协议栈的发展,目前有三种处理趋势,一种是类似于使用dpdk的方式,然后将协议栈放到用户态来做,做得比较好的一般都是以bsd的协议栈为底子,可以参考的是腾讯开源的的方案,另外一种是,继续放在内核,但进行一些旁路,比如netpoll的架构,或者pass某一段路径。最后一种是像google一下推出新的协议。本文主要描述协议栈旁路问题。

为什么需要旁路协议栈,当我们有把握直接操作网卡队列的时候,没必要操作协议栈,可以自己填三层和二层的报头,然后发送出去,减少协议栈的消耗。当一个流的第一个包发送的时候,可以经历完整的协议栈,触发路由学习和二层的arp,之后后面的报文就可以旁路掉协议栈。这个一般对于无状态的udp适用,tcp的话,由于需要考虑重传,则不考虑旁路,而选择在ip层旁路。

当我们需要发包的时候,通过sendmsg,send,sendto,sendfile之类的接口来调用协议栈发包,协议栈帮我们做了什么事情呢?

1.以udp为例,当需要缓存报文的时候,也就是开启了cork之类的时候,sk_write_queue 中取一个最后一个skb,将数据挂在这个skb中缓存,并不立刻发送。

2.否则构造skb,把我们用户态拷贝进去的报文(或者sendfile调用的sendpage,是内核态的报文)管理起来,其主要管理的结构就是sock的sk_write_queue 成员中,新的skb挂在这个sk_write_queue 这个queue的尾,这个地方,对于udp来说,就可以作为旁路的点了,因为skb无非是用来管理发送数据的,我可以直接根据sock查找到的flow,来判断走哪个网卡,然后直接构造udp和ip头,然后用自己的skb池子中的skb结构来发包,发包的话,也不经历ip层,也不经历qdisc层,也不经历虚拟设备层(bond,vlan等),直接给网卡的tx加锁发包。

3.假设cork已经解除,则会开始尝试发包,如果报文cork超过了我们能够发送的最大报文,则会调用ip_flush_pending_frames丢弃,不仅仅是丢弃当前skb,而是丢弃该sock下的所有pending data。(Throw away all pending data on the socket)

4.根据sk->dst是否为NULL,如果不为NULL,则check这个路由,否则,调用ip_route_output_flow查找路由,这个一般由__ip_route_output_key_hash 来实现,是主要的路由解析函数

5.这个就进入了ip层,udp的话进入ip_send_skb-->ip_local_out,其实就是调用 skb_dst(skb)->output(sk, skb); 这个一般就是ip_output-->ip_finish_output,不考虑分片和gso的话,则调用ip_finish_output2,tcp的话,主要是回调ip_queue_xmit,对于tcp来说,可以通过修改queue_xmit这个指针来旁路掉ip层,为什么不在tcp之前就进行旁路,因为tcp还有状态机,旁路的话,

比较麻烦。

const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,

6.ip_finish_output2会在确定邻居ok的情况下,进入dst_neigh_output,邻居协议解析一般是单独的,ipv4的话,通过arp和rap协议来维护邻居,如果邻居状态ok,则·neigh_hh_output-->dev_queue_xmit,

7.根据 skb->dev,以及skb的其他信息,调用netdev_pick_tx 来获取txq,txq中的qdisc就获取到了qdisc的队列,一个网卡的tx队列,就有一个qdisc与之相对应,进入qdisc队列,除了一些不需要qdisc队列的,如lo设备,其他会经过qdisc队列

8.调用sch_direct_xmit 发包,qdisc会对网卡的tx队列上锁,注意这个是一把spinlock,上不了锁会忙等待,也会设置

txq->xmit_lock_owner = cpu;这个可以保证,如果当前cpu号和这个xmit_lock_owner 一致,说明cpu遇到了死锁。一般来说,在一个锁的debug版本中,才会设置owner,但这边是强制的。

上锁成功之后,最终调用我们最为熟悉的dev_hard_start_xmit->xmit_one->netdev_start_xmit-->__netdev_start_xmit,这个函数,也是一个函数指针的回调,也是一个绝佳的旁路的点,比如我们可以通过cork发送大包,然后到此进行拆分成小包,直接调用网卡的发送函数来发包,就避免的skb的大量申请,我们甚至可以维护一个skb的池子,避免skb的缓存缩放,我们可以做一个page的池子,使用sendpage方式的时候,从池子里面取page,然后再释放之前skb中对应的page,反正有很多可以作为的地方。

9.__netdev_start_xmit 里面就完全是使用driver 的ops去发包了,ops->ndo_start_xmit.其实到此为止,一个skb已经从netdevice层送到driver层了.当然,但凡是回调的地方,如果不想直接改内核的话,使用内核模块,可以替换相应的ndo_start_xmit 指针,然后做自己的发包,比如bond设备对应的 ndo_start_xmit  赋值为了bond_start_xmit,你可以替换为自己的xmit。

10.如果步骤9对应的就是实际网卡,那么就调用的是网卡的发包函数,如果是类似于bond之类的虚拟网卡,则在替换skb相应的数据之后,比如skb->dev,skb->queue_mapping等之后,最终还是会重新走一遍9,直到真正的物理设备。下面附一个经历bond处理,再到实际网卡处理发包的堆栈,两次dev_queue_xmit一目了然:

#24 [ffff884dab0f3b48] dev_hard_start_xmit at ffffffff81592bc0
#25 [ffff884dab0f3ba0] sch_direct_xmit at ffffffff815bbd5a
#26 [ffff884dab0f3bf0] __dev_queue_xmit at ffffffff815958e6
#27 [ffff884dab0f3c48] dev_queue_xmit at ffffffff81595c20-----------实际网卡
#28 [ffff884dab0f3c58] bond_dev_queue_xmit at ffffffffc026fbe2 [bonding]
#29 [ffff884dab0f3c78] bond_start_xmit at ffffffffc02717be [bonding]
#30 [ffff884dab0f3cc0] dev_hard_start_xmit at ffffffff81592bc0
#31 [ffff884dab0f3d18] __dev_queue_xmit at ffffffff81595b08
#32 [ffff884dab0f3d70] dev_queue_xmit at ffffffff81595c20-------------bond开始
#33 [ffff884dab0f3d80] ip_finish_output at ffffffff815dc8f6
#34 [ffff884dab0f3dd0] ip_output at ffffffff815dce53
#35 [ffff884dab0f3e30] ip_local_out_sk at ffffffff815daa87
#36 [ffff884dab0f3e50] ip_send_skb at ffffffff815dd8a6
#37 [ffff884dab0f3e68] udp_send_skb at ffffffff81605a9c
#38 [ffff884dab0f3ea8] udp_push_pending_frames at ffffffff81605cde
#39 [ffff884dab0f3ec8] udp_lib_setsockopt at ffffffff81606344
#40 [ffff884dab0f3ee8] udp_setsockopt at ffffffff816073b4
#41 [ffff884dab0f3ef8] sock_common_setsockopt at ffffffff8157831a
#42 [ffff884dab0f3f08] sys_setsockopt at ffffffff81577476
#43 [ffff884dab0f3f50] system_call_fastpath at ffffffff816c4715

穿越了这么多层,其实如果直接对驱动层熟悉的话,可以直接调用ndo_start_xmit 发包,但是一般来说,前文所述的都是sys调用,如果qdisc中的配额用完了,那么会触发一个软中断,也就是著名的NET_TX_SOFTIRQ,也就是软中断中会调用 net_tx_action,这个函数会获取percpu变量softnet_data,然后先处理其completion_queue释放内存,再处理output_queue,其实也就是调用

qdisc_run->qdisc_restart-->sch_direct_xmit 来发包,又回到了步骤8.

总结一下:前面描述的是发包的过程,可以看到这个过程其实蛮重的,旁路掉协议栈,意味着需要和协议栈抢网卡tx的锁,我们拿到锁之后,自己根据第一个报文学习到的flow的二层和三层信息,填好的二层和三层数据的skb报文就能发送出去了,udp协议,建议可以在udp协议层就旁路,tcp协议,建议在进入ip层的指针时旁路,再往下,可以再经历过qdisc之后,替换ndo_start_xmit  ,虽然替换ndo_start_xmit  已经经历了完整的协议栈,不算是旁路,但就是从减少一点skb的申请和释放来说,也是有收益的。旁路协议栈心中要谨记tx的锁,不然很容易导致死锁。比如,在软中断中,可能会获取tx的锁而发包,在sys中,也会获取tx的锁来发包,那么你旁路的时候,要保证别人没有拿到对应tx的锁。

最后,所有的旁路都要保证安全,即本身有些流程可能会拦截一些异常,而这些异常都被跳过,直接到终点了,从实现的角度看,都是在A--》B--》C--》D这种流程中,在D处进行学习,保证那些可以旁路的数据建立特征数据库,这样在A处,检查是否满足特征数据库,如果满足,则直接A--》D,旁路掉中间的流程,当然如果B和C除了检查之外可能也会改造A的数据流,可以将这部分流程抽出来,放在D处来完成,这个就是旁路的常见设计模式了。

对于协议栈的上行旁路,一般什么策略呢?我们以gro来作为例子:

5.0的内核中,gro的dev层的函数为:

static enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)

可以看到,当前的gro,操作的对象依然现定于napi_strcut,对于一个多队列的网卡,每个队列其实有对应的一个napi_strct,但是

有个问题需要看到,我们一个napi_struct过来的报文合并的概率是多大呢?从驱动的流表hash的结果来看,大概率能够保证一个流进入一个网卡的队列,

当使用类似bond之类的虚拟设备的时候,基本也能够通过l4的均衡来保证一个flow的包进入同一个网卡队列。所以看起来好像问题不大,

但是从目前gro的实现来看,它存在的问题有:

1.从时间的角度说,在软中断的收包函数中net_rx_action,如果当前的收包中断使用掉了配额,那么就会调用

napi_gro_flush(n, HZ >= 1000);

这种情况下,gro聚合的时间显然很有限制,

2.从空间的角度说,

/* Instead of increasing this, you should create a hash table. */

#define MAX_GRO_SKBS 8
这个明显太小了,而且作者说了,这个值不适合增大,你如果想增大,干脆创建一个hashtable。

所以,对于flow比较少,发包比较urst的系统来说,linux自带的gro是够用的,但现在,大家发包都强调pacing,自然收包也会出现pacing现象,那么gro的修改就显得很重要了。

从offload的设计来看,gro和gso是放在一起,而通过dev_gro_receive到inet_gro_receive到udp_gro_receive或者tcp_gro_receive是常见的路径了,要想定制gro的话,可以

只判断tcp的流程来做优化,因为udp的gro在普通的服务器使用场景很小,将两个udp的报文合成一个,对于用户态收包程序来说,显然需要一个边界的定义来协助,效果一般。

linux 旁路掉协议栈的处理点的更多相关文章

  1. [转帖]Linux TCP/IP协议栈,数据发送接收流程,TCP协议特点

    Linux TCP/IP协议栈,数据发送接收流程,TCP协议特点 http://network.51cto.com/art/201909/603780.htm 可以毫不夸张的说现如今的互联网是基于TC ...

  2. Linux中USB协议栈的框架简介

    文本旨在简单介绍一下Linux中USB协议栈的代码框架: 下图是USB协议栈相关数据结构的关系图: 下面结合上图看一下系统初始化的流程: 1.USB子系统初始化:\drivers\usb\core\u ...

  3. linux, kill掉占用60%多cpu的进程,几秒后换个pid 和 command 又出现

    linux, kill掉占用60%多cpu的进程,几秒后换个pid 和 command 又出现?快速清理木马流程.假设木马的名字是xysbqaxjqy,如果top看不到,可以在/etc/init.d目 ...

  4. Linux TCP/IP 协议栈之 Socket 的实现分析(一)

    内核版本:2.6.37参考[作者:kendo的文章(基于内涵版本2.6.12)] 第一部份 Socket套接字的创建 socket 并不是 TCP/IP协议的一部份. 从广义上来讲,socket 是U ...

  5. Linux内核网络协议栈优化总纲

    本文原创为freas_1990  转载请标明出处:http://blog.csdn.net/freas_1990/article/details/9474121 Jack:淫龙,Linux内核协议栈如 ...

  6. 【Linux 内核网络协议栈源码剖析】网络栈主要结构介绍(socket、sock、sk_buff,etc)

    原文:http://blog.csdn.net/wenqian1991/article/details/46700177 通过前面的分析,可以发现,网络协议栈中的数据处理,都是基于各类结构体,所有有关 ...

  7. linux kill 掉所有匹配到名字的进程

    如,要 kill 掉 swoole 相关的进程 ps aux | grep swoole |  awk '{print $2}' | xargs kill -9 ps 列出所有进程, 参数: a -  ...

  8. 5.2【Linux 内核网络协议栈源码剖析】socket 函数剖析 ☆☆☆

    深度剖析网络协议栈中的 socket 函数,可以说是把前面介绍的串联起来,将网络协议栈各层关联起来. 应用层 FTP SMTP HTTP ... 传输层 TCP UDP 网络层 IP ICMP ARP ...

  9. Linux 内核网络协议栈 ------sk_buff 结构体 以及 完全解释 (2.6.16)

    转自:http://blog.csdn.net/shanshanpt/article/details/21024465 在2.6.24之后这个结构体有了较大的变化,此处先说一说2.6.16版本的sk_ ...

随机推荐

  1. Docker-Compose实现Mysql主从

    1. 简介 通过使用docker-compose 搭建一个主从数据库,本示例为了解耦 将两个server拆分到了两个compose文件中,当然也可以放到一个compose文件中 演示mysql版本:5 ...

  2. 这篇SpringCloud GateWay 详解,你用的到

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 sentinel,即可免费获取源码 背景 在微服务架构中,通常一个系统 ...

  3. 6000字Locust入门详解

    目录 一.Locust 性能测试 (一). 性能测试工具 主流性能测试工具对比 认识Locust (二) locust 基本用法 1.安装locust 2.编写用例 3. 启动测试 GUI 模式启动 ...

  4. UiPath图片操作截图的介绍和使用

    一.截图(Take Screenshot)的介绍 截取指定的UI元素屏幕截图的一种活动,输出量仅支持图像变量(image) 二.Take Screenshot在UiPath中的使用 1. 打开设计器, ...

  5. Linux yum搭建私有仓库

    搭建yum仓库需要两种资源: rpm包 rpm包的元数据(repodata) 搭建好仓库后需要使用三种网络协议共享出来 http或https ftp 范例: 使用http协议搭建私有仓库 (本示例使用 ...

  6. 在docker中打开redis 客户端 cli

    首先交互方式进入redis容器 docker exec -it redis /bin/bash 随后运行客户端 redis-cli

  7. linux配置svn

    1.安装 yum install subversion 2.测试安装是否成功: svnserve --version 3.创建目录并配置 建立版本库目录 mkdir -pv /data/svn/svn ...

  8. namespace_std 杂题选讲

    CF1458C Latin Square 2021 EC Final C. Random Shuffle [THUPC2021] 混乱邪恶 [JOISC2022] 制作团子 3 2022 集训队互测 ...

  9. [APIO2008]DNA 题解

    题目链接 首先呢,看到 A C G T 对应不同的权值,第一步就是把字母转换成数字. 我们分别对 A->1 C->2 G->3 T->4 进行标号,之后方便 \(\text{d ...

  10. HashSet 添加/遍历元素源码分析

    HashSet 类图 HashSet 简单说明 HashSet 实现了 Set 接口 HashSet 底层实际上是由 HashMap 实现的 public HashSet() { map = new ...