
sk_buff是Linux网络中最核心的结构体,它用来管理和控制接收或发送数据包的信息。各层协议都依赖于sk_buff而存在。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来 *** 作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来 *** 作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头。这样做是为了提高CPU的工作效率。
skb_buff结构如下所示:
这里要声明两个概念的区别,后续直接用这两个概念,注意区分:
(1)线性数据:head - end。
(2)实际线性数据:data - tail,不包含线性数据中的头空间和尾空间。
skb->data_len : skb中的分片数据(非线性数据)的长度。
skb->len : skb中的数据块的总长度,数据块包括实际线性数据和非线性数据,非线性数据为data_len,所以skb->len= (data - tail) + data_len。
skb->truesize : skb的总长度,包括sk_buff结构和数据部分,skb=sk_buff控制信息 + 线性数据(包括头空间和尾空间) + skb_shared_info控制信息 + 非线性数据,所以skb->truesize = sizeof(struct sk_buff) + (head - end) + sizeof(struct skb_shared_info) + data_len。
sk_buff结构体中的都是sk_buff的控制信息,是网络数据包的一些配置,真正储存数据的是sk_buff结构体中几个指针指向的数据区中,线性数据区的大小 = (skb->end - skb->head),对于每个数据包来说这个大小都是固定不变的,在传输过程中skb->end和skb->head所指向的地址都是不变的,这里要注意这个地址不是本机的地址,如果是本机的地址那么数据包传到其他主机上这个地址就是无效的,所以这个地址是这个skb缓冲区的相对地址。
线性数据区是用来存放各层协议头部和应用层发下来的数据。各层协议头部相关信息放在线性数据区中。实际数据指针为data和tail,data指向实际数据开始的地方,tail指向实际数据结束的地方。
用一张图来表示sk_buff和数据区的关系:
这一节介绍先行数据区在sk_buff创建过程中的变化,图中暂时省略了非线性数据区:
2.1中所讲的都是线性数据区中的相关的配置,当线性数据区不够用的时候就会启用非线性数据区作为数据区域的扩展,skb中用skb_shared_info分片结构体来配置非线性数据。
skb_shared_info结构体是和skb中的线性数据区一体的,所以在skb的各种 *** 作时都会把这两个结构看作是一个结构来 *** 作。如:
skb_shared_info结构:
非线性数据区有两种不同的构成数据的方式
(1)用数组存储的分片数据区,采用是是结构体中的frags[MAX_SKB_FRAGS]
对于frags[]一般用在当数据比较多,在线性数据区装不下的时候,skb_frag_t中是一页一页的数据,skb_frag_struct结构体如下:
下图显示了frags是怎么分配分片数据的:
(2)frag_list指针来指向的分片数据:
参考:
从ip_finish_output2到dev_queue_xmit路径:
http://www.bluestep.cc/linux%e5%91%bd%e4%bb%a4arping-%e7%bd%91%e7%bb%9c%e7%ae%a1%e7%90%86-%e9%80%9a%e8%bf%87%e5%8f%91%e9%80%81arp%e5%8d%8f%e8%ae%ae%e6%8a%a5%e6%96%87%e6%b5%8b%e8%af%95%e7%bd%91%e7%bb%9c/
arp协议:
(1).硬件类型:
硬件地址类型,该字段值一般为ARPHRD_ETHER,表示以太网。
(2).协议类型:
表示三层地址使用的协议,该字段值一般为ETH_P_IP,表示IP协议
(3)硬件地址长度,以太网MAC地址就是6;
(4)协议地址长度,IP地址就是4;
(5) *** 作码
常见的有四种,arp请求,arp相应,rarp请求,rarp相应。
(6)发送方硬件地址与IP地址,(7)目标硬件地址与目标IP地址。
arp头数据结构:
arp模块的初始化函数为arp_init(),这个函数在ipv4协议栈的初始化函数inet_init()中被调用。
1.初始化arp表arp_tbl
2.注册arp协议类型;
3.建立arp相关proc文件,/proc/net/arp;
4.注册通知事件
一个neigh_table对应一种邻居协议,IPv4就是arp协议。用来存储于邻居协议相关的参数、功能函数、邻居项散列表等。
一个neighbour对应一个邻居项,就是一个arp条目
邻居项函数指针表,实现三层和二层的dev_queue_xmit()之间的跳转。
用来存储统计信息,一个结构实例对应一个网络设备上的一种邻居协议。
注册arp报文类型 :dev_add_pack(&arp_packet_type)
就是把arp_packet_type添加到ptype_base哈希表中。
注册新通知事件的时候,在已经注册和UP的设备上,会调用一次这个通知事件。
设备事件类型:
创建一个邻居项,并将其添加到散列表上,返回指向该邻居项的指针。
tbl:待创建的邻居项所属的邻居表,即arp_tbl;
pkey:三层协议地址(IP地址)
dev:输出设备
want_ref:??
创建邻居项
1.设置邻居项的类型
2.设置邻居项的ops指针
3.设置邻居项的output函数指针
调用dst_link_failure()函数向三层报告错误,当邻居项缓存中还有未发送的报文,而该邻居却无法访问时被调用。不懂。
用来发送arp请求,在邻居项状态定时器处理函数中被调用。
neigh:arp请求的目的邻居项
skb:缓存在该邻居项中的待发送报文,用来获取该skb的源ip地址。
将得到的硬件源、目的地址,IP源、目的地址等作为参数,调用arp_send()函数创建一个arp报文并将其输出。
创建及发送arp报文
创建arp报文,填充字段。
发送arp报文
用来从二层接收并处理一个arp报文。这个函数中就是做了一些参数检查,然后调用arp_process()函数。
neigh_event_ns
neigh_update
这个函数的作用就是更新邻居项硬件地址和状态。分支比较多。
neigh_update_notify
代理arp(proxy arp),通常像路由器这样的设备才使用,用来代替处于另一个网段的主机回答本网段主机的arp请求。
感觉代码ARP好像没啥用呀。
网络主机发包的一般过程:
1.当目的IP和自己在同一网段时,直接arp请求该目的IP的MAC。
2.当目的IP和自己不再同一网段时,arp请求默认网关的MAC。
https://www.cnblogs.com/taitai139/p/12336554.html
https://www.cnblogs.com/Widesky/p/10489514.html
当主机没有默认网关的时候,arp请求别的网段的报文,到达路由器后,本来路由器是要隔离广播的,把这个arp请求报文给丢弃,这样就没法通信了。当路由器开启arp proxy后,路由器发现请求的目的IP在其他网段,就自己给主机回复一个arp响应报文,这样源主机就把路由器的MAC当成目的IP主机对应的MAC,可以通信了。这样可能会造成主机arp表中,多个IP地址都对应于路由器的同一个MAC地址。
可以使用arping命令发送指定IP的arp请求报文。
写完了发现这个老妹写的arp代理文章蛮好的,不过她好像是转载的。
一个报文的产生和发送,都需要硬件和软件的完美配合。硬件层面接收到报文之后,做一系列的初始化 *** 作,之后驱动才开始把一个封包封装为skb。
当然这是在x86架构下,如果是在cavium架构下,封包是wqe形式存在。
不管是skb还是wqe,都仅仅是一种手段,一种达到完成报文传输所采用的一种解决方案,一种方法而已。
或许处理方案的具体实现细节差别万千,但是基本的原理,都是殊途同归,万变不离其宗。
skb的产生,让Linux协议栈旅程的开启,具备了最基本的条件,接下来的协议栈之旅,才会更加精彩。
写作本文的原因是现在本机网络 IO 应用非常广。
在 php 中 一般 nginx 和 php-fpm 是通过 127.0.0.1 来进行通信的;
在微服务中,由于 side car 模式的应用,本机网络请求更是越来越多。
所以,如果能深度理解这个问题在各种网络通信应用的技术实践中将非常的有意义。
今天咱们就把 127.0.0.1 本机网络通信相关问题搞搞清楚!
为了方便讨论,我把这个问题拆分成3问:
1)127.0.0.1 本机网络 IO 需要经过网卡吗?
2)和外网网络通信相比,在内核收发流程上有啥差别?
3)使用 127.0.0.1 能比 192.168.x.x 更快吗?
在上面这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。
当数据包到达另外一台机器的时候,Linux 数据包的接收过程开始了。
当网卡收到数据以后,CPU发起一个中断,以通知 CPU 有数据到达。
当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。
ksoftirqd 检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。
当协议栈处理完并把数据放到接收队列的之后,唤醒用户进程(假设是阻塞方式)。
关于跨机网络通信的理解,可以通俗地用下面这张图来总结一下:
前面,我们看到了跨机时整个网络数据的发送过程 。
在本机网络 IO 的过程中,流程会有一些差别。
为了突出重点,本节将不再介绍整体流程,而是只介绍和跨机逻辑不同的地方。
有差异的地方总共有两个,分别是路由和驱动程序。
对于本机网络 IO 来说,特殊之处在于在 local 路由表中就能找到路由项,对应的设备都将使用 loopback 网卡,也就是我们常见的 lO。
从上述结果可以看出,对于目的是 127.0.0.1 的路由在 local 路由表中就能够找到了。
对于是本机的网络请求,设备将全部都使用 lo 虚拟网卡,接下来的网络层仍然和跨机网络 IO 一样。
本机网络 IO 需要进行 IP 分片吗?
因为和正常的网络层处理过程一样,如果 skb 大于 MTU 的话,仍然会进行分片。
只不过 lo 的 MTU 比 Ethernet 要大很多。
通过 ifconfig 命令就可以查到,普通网卡一般为 1500,而 lO 虚拟接口能有 65535。
为什么我把“驱动”加个引号呢,因为 loopback 是一个纯软件性质的虚拟接口,并没有真正意义上的驱动。
在邻居子系统函数中经过处理,进入到网络设备子系统,只有触发完软中断,发送过程就算是完成了。
在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。
而在本机的网络 IO 过程中,由于并不真的过网卡,所以网卡实际传输,硬中断就都省去了。直接从软中断开始,送进协议栈。
网络再往后依次是传输层,最后唤醒用户进程,这里就不多展开了。
我们来总结一下本机网络通信的内核执行流程:
回想下跨机网络 IO 的流程:
通过本文的叙述,我们确定地得出结论,不需要经过网卡。即使了把网卡拔了本机网络是否还可以正常使用的。
总的来说,本机网络 IO 和跨机 IO 比较起来,确实是节约了一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。
但是在内核其它组件上可是一点都没少:系统调用、协议栈(传输层、网络层等)、网络设备子系统、邻居子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东东)。所以即使是本机网络 IO,也别误以为没啥开销。
先说结论:我认为这两种使用方法在性能上没有啥差别。
我觉得有相当大一部分人都会认为访问本机server 的话,用 127.0.0.1 更快。原因是直觉上认为访问 IP 就会经过网卡。
其实内核知道本机上所有的 IP,只要发现目的地址是本机 IP 就可以全走 loopback 回环设备了。
本机其它 IP 和 127.0.0.1 一样,也是不用过物理网卡的,所以访问它们性能开销基本一样!
How SKBs work - Linux kernel
http://vger.kernel.org/~davem/skb.html
一篇解读Linux网络协议栈
https://zhuanlan.zhihu.com/p/475319464
你真的了解127.0.0.1和0.0.0.0的区别?
http://www.52im.net/thread-2928-1-1.html
深入 *** 作系统,彻底搞懂127.0.0.1本机网络通信
http://www.52im.net/thread-3590-1-1.html
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)