ping命令全链路分析(2)
本文使用 Zhihu On VSCode 创作并发布
上篇文章对开源网络协议栈实现 tapip 触发进行了分析,探讨了执行 ping 命令时,核源数据包是码a码如何到达网络协议栈的。本文将继续探讨 ping 命令与网络协议栈的核源联系。目前广泛使用的码a码微良源码网络协议栈是五层协议划分:应用层、传输层、核源网络层、码a码链路层和物理层。核源ping 命令采用的码a码 ICMP 协议位于网络层,但特别之处在于 ICMP 报文是核源封装在 IP 报文之内的。下文将从 ICMP 协议开始分析。码a码
ICMP 协议
ping 命令的核源执行过程实际上包含了源端向目的端发送 ICMP 请求报文和目的端向源端发送 ICMP 回复报文的过程。ICMP 报文头包含了 ICMP type、码a码code、核源id、seq 等字段,报文头部为 字节,payload 部分数据长度为可变长度。
ICMP 报文头部包含 8bit 类型码 type、8bit 代码 code 和 bit 校验和 checksum,其余部分内容和类型码 type 相关。ICMP 报文中定义 type 字段包含以下几种,type 字段与 code 的详细对应关系见附录 1:
其中,ping 命令使用的报文类型为响应请求和响应应答,其报文格式如图:
ICMP 响应请求
在 tapip 中,ICMP 响应请求报文构造是在 ping.c:send_packet() 函数中完成的。ICMP 报文填充构建代码如下:
根据上一篇文章的分析,tapip 采用一个 tap 设备作为虚拟网卡,ICMP 数据报文最终通过 wirte() 接口写入 tap 设备文件中,最终被 Linux 内核中的网络协议栈处理。这里还是先从 tapip 出发,研究下网络协议栈中如何处理 ICMP 响应请求报文。在 tapip 源码中,处理 ICMP 响应请求报文在函数 icmp_echo_request() 中,其函数调用栈如下:
在 Linux 系统中,数据包到达网络设备后会触发中断,网卡驱动程序将对应数据包传递到内核网络协议栈处理,处理结果通过系统调用接口返回给应用程序(ping 应用)。
tapip 作为一种用户态实现,网络设备 net device 是通过 tap 设备模拟的,tap 设备文件描述符中被写入数据包就相当于网卡设备接收到网络数据包;
网卡驱动程序的工作对应 tapip 中 netdev_interrupt() 到 veth_rx() 之间的过程:首先在中断处理函数中调用 veth_poll() 函数采用轮询的方式检查 tap 设备的文件描述符是否有写入事件;当发生写入事件时,veth_rx() 函数被调用,从文件描述符中读取数据包,并传递到网络协议栈中处理,此时,网络协议栈处理的入口 net_in() 被调用。
网络协议栈按照网络分层模型进行处理:
ICMP 响应回复
ICMP 响应回复的处理过程与接收侧处理 ICMP 响应请求的流程基本一致,不同点在于最后 icmp 报文响应的处理,其 type 为 0,对应的处理函数为 icmp_echo_reply(),具体函数调用栈如下:
总结
本文主要分析了用户态网络协议栈 tapip 处理 ping 命令对应的 ICMP 报文的过程,后续将结合 Linux 内核分析这个过程在内核中是如何处理的,另外还会分析下 ARP 协议的实现。
学海无涯,感觉 tapip 的实现逻辑清晰,读起来非常舒服,非常推荐对网络感兴趣的同学学习参考。
(最近特别水逆,希望能早日走出困境,迎来光明吧。)
附录 1: ICMP 报文类型表
markdown
| 类型 Type | 代码 Code | 描述 |
| :------: | :------: | :--------------------------: |
| 0 | 0 | 回显应答(ping 应答) |
| 3 | 0 | 网络不可达 |
| 3 | 1 | 主机不可达 |
| 3 | 2 | 协议不可达 |
| 3 | 3 | 端口不可达 |
| ... | ... | ... |
TODO:
转发:轻松理解 Docker 网络虚拟化基础之 veth 设备!
大家好,我是系统源码学习飞哥! 最近,飞哥对网络虚拟化技术产生了浓厚的兴趣,特别是想深入理解在Docker等虚拟技术下,网络底层是如何运行的。在探索过程中,飞哥意识到网络虚拟化技术是一个挑战,尽管飞哥对原生Linux网络实现过程的理解还算不错,但在研究网络虚拟化相关技术时,仍感到有些难度。 然而,飞哥有解决这类问题的技巧,那就是从基础开始。今天,飞哥将带大家深入理解Docker网络虚拟化中的基础技术之一——veth。 在物理机组成的网络中,最基础、简单的网络连接方式是什么?没错,就是直接用一根交叉网线把两台电脑的网卡连起来,这样一台机器发送数据,另一台就能接收到。 网络虚拟化实现的第一步就是用软件模拟这种简单的网络连接。实现的技术就是我们今天讨论的主角——veth。veth模拟了在物理世界里的两块网卡,以及一条网线,通过它可以将两个虚拟的设备连接起来,实现相互通信。平时我们在Docker镜像中看到的eth0设备,实际上就是veth。 Veth是一种通过软件模拟硬件的方式来实现网络连接的技术。我们本机网络IO中的lo回环设备也是一种通过软件虚拟出来的设备,与veth的主要区别在于veth总是成对出现。 现在,让我们深入了解veth是如何工作的。veth的使用
在Linux中,我们可以使用ip命令创建一对veth。这个命令可以用于管理和查看网络接口,包括物理网络接口和虚拟接口。 通过使用`ip link show`进行查看。 和eth0、lo等网络设备一样,veth也需要配置IP才能正常工作。我们为这对veth配置IP。 接下来,启动这两个设备。 当设备启动后,我们可以通过熟悉的ifconfig查看它们。 现在,我们已经建立了一对虚拟设备。但为了使它们互相通信,我们需要做些准备工作,包括关闭反向过滤rp_filter模块和打开accept_local模块。具体准备工作如下: 现在,在veth0上pingveth1,它们之间可以通信了,真是太棒了! 在另一个控制台启动了tcpdump抓包,结果如下。 由于两个设备之间的首次通信,veth0首先发出一个arp请求,veth1收到后回复一个arp回复。然后就是正常的ping命令下的IP包。veth底层创建过程
在上一小节中,我们亲手创建了一对veth设备,若辰源码并通过简单的配置让它们互相通信。接下来,让我们看看在内核中,veth是如何被创建的。 veth相关的源码位于`drivers/net/veth.c`,初始化入口是`veth_init`。 `veth_init`中注册了`veth_link_ops`(veth设备的操作方法),包含了veth设备的创建、启动和删除等回调函数。 我们先来看看veth设备的创建函数`veth_newlink`,这是理解veth的关键。 `veth_newlink`中,通过`register_netdevice`创建了peer和dev两个网络虚拟设备。接下来的`netdev_priv`函数返回的是网络设备的私有数据,`priv->peer`只是一个指针。 两个新创建的设备dev和peer通过`priv->peer`指针完成配对。dev设备中的`priv->peer`指针指向peer设备,而peer设备中的`priv->peer`指针指向dev。 接着我们再看看veth设备的启动过程。 其中`dev->netdev_ops = &veth_netdev_ops`这行代码也非常重要。`veth_netdev_ops`是veth设备的操作函数。例如在发送过程中调用的函数指针`ndo_start_xmit`,对于veth设备来说就会调用到`veth_xmit`。这部分内容将在下一个小节详细说明。veth网络通信过程
回顾《张图,一万字,拆解Linux网络包发送过程》和《图解Linux网络包接收过程》中的内容,我们系统介绍了Linux网络包的收发过程。在《.0.0.1 之本机网络通信过程知多少 ?》中,我们详细讨论了基于回环设备lo的本机网络IO过程。 基于veth的网络IO过程与上述过程图几乎完全相同,不同之处在于使用的驱动程序。我们将在下一节中具体说明。 网络设备层最后会通过`ops->ndo_start_xmit`调用驱动进行真正的发送。 在《.0.0.1 之本机网络通信过程知多少 ?》一文中,我们提到对于回环设备lo来说,`netdev_ops`是`loopback_ops`。那么`ops->ndo_start_xmit`对应的发送函数就是`loopback_xmit`。这就是在整个发送过程中唯一与lo设备不同的地方。我们简单看看这个发送函数的代码。 `veth_xmit`中的主要操作是获取当前veth设备,然后将数据发送到对端。发送到对端设备的工作由`dev_forward_skb`函数完成。 先调用`eth_type_trans`将`skb`所属设备更改为刚刚取到的veth对端设备rcv。 接着调用`netif_rx`,这部分操作与lo设备的操作相似。在该方法中最终执行到`enqueue_to_backlog`,将要发送的`skb`插入`softnet_data->input_pkt_queue`队列中,并调用`_napi_schedule`触发软中断。 当数据发送完毕唤起软中断后,veth对端设备开始接收。与发送过程不同的是,所有虚拟设备的收包`poll`函数都是一样的,都是在设备层初始化为`process_backlog`。 因此,veth设备的接收过程与lo设备完全相同。想了解更多这部分内容的同学,请参考《.0.0.1 之本机网络通信过程知多少 ?》一文中的第三节。总结
大部分同学在日常工作中通常不会接触到veth,因此在看到Docker相关技术文章中提到veth时,可能会觉得它是一个高深的技术。实际上,从实现角度来看,金大福源码虚拟设备veth与我们日常接触的lo设备非常相似。基于veth的本机网络IO通信图直接从《.0.0.1的那篇文章》中复制而来。只要你看过飞哥的《.0.0.1的那篇文章》,理解veth将变得非常容易。 只是与lo设备相比,veth是为了虚拟化技术而设计的,因此它具有配对的概念。在`veth_newlink`函数中,一次创建了两个网络设备,并将对方设置为各自的peer。在发送数据时,找到发送设备的peer,然后发起软中断让对方收取数据即可。 怎么样,是不是很容易理解! 轻松理解 Docker 网络虚拟化基础之veth设备!Linux实现ARP缓存老化时间原理问题深入解析
一.问题众所周知,ARP是一个链路层的地址解析协议,它以IP地址为键值,查询保有该IP地址主机的MAC地址。协议的详情就不详述了,你可以看RFC,也可以看教科书。这里写这么一篇文章,主要是为了做一点记录,同时也为同学们提供一点思路。具体呢,我遇到过两个问题:
1.使用keepalived进行热备份的系统需要一个虚拟的IP地址,然而该虚拟IP地址到底属于哪台机器是根据热备群的主备来决定的,因此主机器在获得该虚拟IP的时候,必须要广播一个免费的arp,起初人们认为这没有必要,理由是不这么做,热备群也工作的很好,然而事实证明,这是必须的;
2.ARP缓存表项都有一个老化时间,然而在linux系统中却没有给出具体如何来设置这个老化时间。那么到底怎么设置这个老化时间呢?
二.解答问题前的说明
ARP协议的规范只是阐述了地址解析的细节,然而并没有规定协议栈的实现如何去维护ARP缓存。ARP缓存需要有一个到期时间,这是必要的,因为ARP缓存并不维护映射的状态,也不进行认证,因此协议本身不能保证这种映射永远都是正确的,它只能保证该映射在得到arp应答之后的一定时间内是有效的。这也给了ARP欺骗以可乘之机,不过本文不讨论这种欺骗。
像Cisco或者基于VRP的华为设备都有明确的配置来配置arp缓存的到期时间,然而Linux系统中却没有这样的配置,起码可以说没有这样的直接配置。Linux用户都知道如果需要配置什么系统行为,那么使用sysctl工具配置procfs下的sys接口是一个方法,然而当我们google了好久,终于发现关于ARP的配置处在/proc/sys/net/ipv4/neigh/ethX的时候,我们最终又迷茫于该目录下的N多文件,即使去查询Linux内核的Documents也不能清晰的明了这些文件的具体含义。对于Linux这样的成熟系统,一定有办法来配置ARP缓存的到期时间,但是具体到操作上,到底怎么配置呢?这还得从Linux实现的ARP状态机说起。
如果你看过《Understading Linux Networking Internals》并且真的做到深入理解的话,那么本文讲的基本就是废话,但是强势袭击源码很多人是没有看过那本书的,因此本文的内容还是有一定价值的。
Linux协议栈实现为ARP缓存维护了一个状态机,在理解具体的行为之前,先看一下下面的图(该图基于《Understading Linux Networking Internals》里面的图-修改,在第二十六章):
在上图中,我们看到只有arp缓存项的reachable状态对于外发包是可用的,对于stale状态的arp缓存项而言,它实际上是不可用的。如果此时有人要发包,那么需要进行重新解析,对于常规的理解,重新解析意味着要重新发送arp请求,然后事实上却不一定这样,因为Linux为arp增加了一个“事件点”来“不用发送arp请求”而对arp协议生成的缓存维护的优化措施,事实上,这种措施十分有效。这就是arp的“确认”机制,也就是说,如果说从一个邻居主动发来一个数据包到本机,那么就可以确认该包的“上一跳”这个邻居是有效的,然而为何只有到达本机的包才能确认“上一跳”这个邻居的有效性呢?因为Linux并不想为IP层的处理增加负担,也即不想改变IP层的原始语义。
Linux维护一个stale状态其实就是为了保留一个neighbour结构体,在其状态改变时只是个别字段得到修改或者填充。如果按照简单的实现,只保存一个reachable状态即可,其到期则删除arp缓存表项。Linux的做法只是做了很多的优化,但是如果你为这些优化而绞尽脑汁,那就悲剧了...
三.Linux如何来维护这个stale状态
在Linux实现的ARP状态机中,最复杂的就是stale状态了,在此状态中的arp缓存表项面临着生死抉择,抉择者就是本地发出的包,如果本地发出的包使用了这个stale状态的arp缓存表项,那么就将状态机推进到delay状态,如果在“垃圾收集”定时器到期后还没有人使用该邻居,那么就有可能删除这个表项了,到底删除吗?这样看看有木有其它路径使用它,关键是看路由缓存,路由缓存虽然是一个第三层的概念,然而却保留了该路由的下一条的ARP缓存表项,这个意义上,Linux的路由缓存实则一个转发表而不是一个路由表。
如果有外发包使用了这个表项,那么该表项的ARP状态机将进入delay状态,在delay状态中,只要有“本地”确认的到来(本地接收包的上一跳来自该邻居),linux还是不会发送ARP请求的,但是如果一直都没有本地确认,那么Linux就将发送真正的ARP请求了,进入probe状态。因此可以看到,从stale状态开始,所有的状态只是为一种优化措施而存在的,stale状态的ARP缓存表项就是一个缓存的缓存,如果Linux只是将过期的reachable状态的arp缓存表项删除,语义是一样的,但是实现看起来以及理解起来会简单得多!
再次强调,reachable过期进入stale状态而不是直接删除,是为了保留neighbour结构体,优化内存以及CPU利用,实际上进入stale状态的arp缓存表项时不可用的,要想使其可用,要么在delay状态定时器到期前本地给予了确认,比如tcp收到了一个包,要么delay状态到期进入probe状态后arp请求得到了回应。否则还是会被删除。
四.Linux的ARP缓存实现要点
在blog中分析源码是儿时的记忆了,现在不再浪费版面了。只要知道Linux在实现arp时维护的几个定时器的要点即可。
1.Reachable状态定时器
每当有arp回应到达或者其它能证明该ARP表项表示的邻居真的可达时,启动该定时器。到期时根据配置的时间将对应的ARP缓存表项转换到下一个状态。
2.垃圾回收定时器
定时启动该定时器,具体下一次什么到期,是根据配置的base_reachable_time来决定的,具体见下面的代码:
复制代码
代码如下:
static void neigh_periodic_timer(unsigned long arg)
{
...
if (time_after(now, tbl-last_rand + * HZ)) { //内核每5分钟重新进行一次配置
struct neigh_parms *p;
tbl-last_rand = now;
for (p = tbl-parms; p; p = p-next)
p-reachable_time =
neigh_rand_reach_time(p-base_reachable_time);
}
...
/* Cycle through all hash buckets every base_reachable_time/2 ticks.
* ARP entry timeouts range from 1/2 base_reachable_time to 3/2
* base_reachable_time.
*/
expire = tbl-parms.base_reachable_time 1;
expire /= (tbl-hash_mask + 1);
if (!expire)
expire = 1;
//下次何时到期完全基于base_reachable_time);
mod_timer(tbl-gc_timer, now + expire);
...
}
static void neigh_periodic_timer(unsigned long arg)
{
...
if (time_after(now, tbl-last_rand + * HZ)) { //内核每5分钟重新进行一次配置
struct neigh_parms *p;
tbl-last_rand = now;
for (p = tbl-parms; p; p = p-next)
p-reachable_time =
neigh_rand_reach_time(p-base_reachable_time);
}
...
/* Cycle through all hash buckets every base_reachable_time/2 ticks.
* ARP entry timeouts range from 1/2 base_reachable_time to 3/2
* base_reachable_time.
*/
expire = tbl-parms.base_reachable_time 1;
expire /= (tbl-hash_mask + 1);
if (!expire)
expire = 1;
//下次何时到期完全基于base_reachable_time);
mod_timer(tbl-gc_timer, now + expire);
...
}
一旦这个定时器到期,将执行neigh_periodic_timer回调函数,里面有以下的逻辑,也即上面的...省略的部分:
复制代码
代码如下:
if (atomic_read(n-refcnt) == 1 //n-used可能会因为“本地确认”机制而向前推进
(state == NUD_FAILED ||time_after(now, n-used + n-parms-gc_staletime))) {
*np = n-next;
n-dead = 1;
write_unlock(n-lock);
neigh_release(n);
continue;
}
if (atomic_read(n-refcnt) == 1 //n-used可能会因为“本地确认”机制而向前推进
(state == NUD_FAILED ||time_after(now, n-used + n-parms-gc_staletime))) {
*np = n-next;
n-dead = 1;
write_unlock(n-lock);
neigh_release(n);
continue;
}
如果在实验中,你的处于stale状态的表项没有被及时删除,那么试着执行一下下面的命令:
[plain] view plaincopyprint?ip route flush cache
ip route flush cache然后再看看ip neigh ls all的结果,注意,不要指望马上会被删除,因为此时垃圾回收定时器还没有到期呢...但是我敢保证,不长的时间之后,该缓存表项将被删除。
五.第一个问题的解决
在启用keepalived进行基于vrrp热备份的群组上,很多同学认为根本不需要在进入master状态时重新绑定自己的MAC地址和虚拟IP地址,然而这是根本错误的,如果说没有出现什么问题,那也是侥幸,因为各个路由器上默认配置的arp超时时间一般很短,然而我们不能依赖这种配置。请看下面的图示:
如果发生了切换,假设路由器上的arp缓存超时时间为1小时,那么在将近一小时内,单向数据将无法通信(假设群组中的主机不会发送数据通过路由器,排出“本地确认”,毕竟我不知道路由器是不是在运行Linux),路由器上的数据将持续不断的法往原来的master,然而原始的matser已经不再持有虚拟IP地址。
因此,为了使得数据行为不再依赖路由器的配置,必须在vrrp协议下切换到master时手动绑定虚拟IP地址和自己的MAC地址,在Linux上使用方便的arping则是:
[plain] view plaincopyprint?arping -i ethX -S 1.1.1.1 -B -c 1
arping -i ethX -S 1.1.1.1 -B -c 1这样一来,获得1.1.1.1这个IP地址的master主机将IP地址为...的ARP请求广播到全网,假设路由器运行Linux,则路由器接收到该ARP请求后将根据来源IP地址更新其本地的ARP缓存表项(如果有的话),然而问题是,该表项更新的结果状态却是stale,这只是ARP的规定,具体在代码中体现是这样的,在arp_process函数的最后:
复制代码
代码如下:
if (arp-ar_op != htons(ARPOP_REPLY) || skb-pkt_type != PACKET_HOST)
state = NUD_STALE;
neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);
if (arp-ar_op != htons(ARPOP_REPLY) || skb-pkt_type != PACKET_HOST)
state = NUD_STALE;
neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);
由此可见,只有实际的外发包的下一跳是1.1.1.1时,才会通过“本地确认”机制或者实际发送ARP请求的方式将对应的MAC地址映射reachable状态。
更正:在看了keepalived的源码之后,发现这个担心是多余的,毕竟keepalived已经很成熟了,不应该犯“如此低级的错误”,keepalived在某主机切换到master之后,会主动发送免费arp,在keepalived中有代码如是:
复制代码
代码如下:
vrrp_send_update(vrrp_rt * vrrp, ip_address * ipaddress, int idx)
{
char *msg;
char addr_str[];
if (!IP_IS6(ipaddress)) {
msg = "gratuitous ARPs";
inet_ntop(AF_INET, ipaddress-u.sin.sin_addr, addr_str, );
send_gratuitous_arp(ipaddress);
} else {
msg = "Unsolicited Neighbour Adverts";
inet_ntop(AF_INET6, ipaddress-u.sin6_addr, addr_str, );
ndisc_send_unsolicited_na(ipaddress);
}
if (0 == idx debug ) {
log_message(LOG_INFO, "VRRP_Instance(%s) Sending %s on %s for %s",
vrrp-iname, msg, IF_NAME(ipaddress-ifp), addr_str);
}
}
vrrp_send_update(vrrp_rt * vrrp, ip_address * ipaddress, int idx)
{
char *msg;
char addr_str[];
if (!IP_IS6(ipaddress)) {
msg = "gratuitous ARPs";
inet_ntop(AF_INET, ipaddress-u.sin.sin_addr, addr_str, );
send_gratuitous_arp(ipaddress);
} else {
msg = "Unsolicited Neighbour Adverts";
inet_ntop(AF_INET6, ipaddress-u.sin6_addr, addr_str, );
ndisc_send_unsolicited_na(ipaddress);
}
if (0 == idx debug ) {
log_message(LOG_INFO, "VRRP_Instance(%s) Sending %s on %s for %s",
vrrp-iname, msg, IF_NAME(ipaddress-ifp), addr_str);
}
}
六.第二个问题的解决
扯了这么多,在Linux上到底怎么设置ARP缓存的老化时间呢?
我们看到/proc/sys/net/ipv4/neigh/ethX目录下面有多个文件,到底哪个是ARP缓存的老化时间呢?实际上,直接点说,就是base_reachable_time这个文件。其它的都只是优化行为的措施。比如gc_stale_time这个文件记录的是“ARP缓存表项的缓存”的存活时间,该时间只是一个缓存的缓存的存活时间,在该时间内,如果需要用到该邻居,那么直接使用表项记录的数据作为ARP请求的内容即可,或者得到“本地确认”后直接将其置为reachable状态,而不用再通过路由查找,ARP查找,ARP邻居创建,ARP邻居解析这种慢速的方式。
默认情况下,reachable状态的超时时间是秒,超过秒,ARP缓存表项将改为stale状态,此时,你可以认为该表项已经老化到期了,只是Linux的实现中并没有将其删除罢了,再过了gc_stale_time时间,表项才被删除。在ARP缓存表项成为非reachable之后,垃圾回收器负责执行“再过了gc_stale_time时间,表项才被删除”这件事,这个定时器的下次到期时间是根据base_reachable_time计算出来的,具体就是在neigh_periodic_timer中:
复制代码
代码如下:
if (time_after(now, tbl-last_rand + * HZ)) {
struct neigh_parms *p;
tbl-last_rand = now;
for (p = tbl-parms; p; p = p-next)
//随计化很重要,防止“共振行为”引发的ARP解析风暴
p-reachable_time =neigh_rand_reach_time(p-base_reachable_time);
}
...
expire = tbl-parms.base_reachable_time 1;
expire /= (tbl-hash_mask + 1);
if (!expire)
expire = 1;
mod_timer(tbl-gc_timer, now + expire);
if (time_after(now, tbl-last_rand + * HZ)) {
struct neigh_parms *p;
tbl-last_rand = now;
for (p = tbl-parms; p; p = p-next)
//随计化很重要,防止“共振行为”引发的ARP解析风暴
p-reachable_time =neigh_rand_reach_time(p-base_reachable_time);
}
...
expire = tbl-parms.base_reachable_time 1;
expire /= (tbl-hash_mask + 1);
if (!expire)
expire = 1;
mod_timer(tbl-gc_timer, now + expire);
可见一斑啊!适当地,我们可以通过看代码注释来理解这一点,好心人都会写上注释的。为了实验的条理清晰,我们设计以下两个场景:
1.使用iptables禁止一切本地接收,从而屏蔽arp本地确认,使用sysctl将base_reachable_time设置为5秒,将gc_stale_time为5秒。
2.关闭iptables的禁止策略,使用TCP下载外部网络一个超大文件或者进行持续短连接,使用sysctl将base_reachable_time设置为5秒,将gc_stale_time为5秒。
在两个场景下都使用ping命令来ping本地局域网的默认网关,然后迅速Ctrl-C掉这个ping,用ip neigh show all可以看到默认网关的arp表项,然而在场景1下,大约5秒之内,arp表项将变为stale之后不再改变,再ping的话,表项先变为delay再变为probe,然后为reachable,5秒之内再次成为stale,而在场景2下,arp表项持续为reachable以及dealy,这说明了Linux中的ARP状态机。那么为何场景1中,当表项成为stale之后很久都不会被删除呢?其实这是因为还有路由缓存项在使用它,此时你删除路由缓存之后,arp表项很快被删除。
七.总结
1.在Linux上如果你想设置你的ARP缓存老化时间,那么执行sysctl -w net.ipv4.neigh.ethX=Y即可,如果设置别的,只是影响了性能,在Linux中,ARP缓存老化以其变为stale状态为准,而不是以其表项被删除为准,stale状态只是对缓存又进行了缓存;
2.永远记住,在将一个IP地址更换到另一台本网段设备时,尽可能快地广播免费ARP,在Linux上可以使用arping来玩小技巧。
Linux内核网络udp数据包发送(二)UDP协议层分析
在Linux内核中,UDP数据包的发送涉及到udp_sendmsg和udp_send_skb函数的深入处理。首先,UDP插入优化允许内核累积用户数据,通过corking技术。用户通过设置或请求辅助数据(如IP_PKTINFO)来影响发送行为,如指定源地址或自定义IP选项。
在数据发送过程中,UDP套接字的状态影响了数据处理,如获取目的地址、设置源地址和设备索引,以及使用辅助消息设置IP选项。套接字状态为已连接时,会使用TCP状态信息。对于未连接的套接字,会检查自定义IP选项,如SRR和TOS,根据用户设置决定数据包属性。
发送多播或单播数据时,UDP会根据目标地址和用户请求选择正确的设备和源地址。路由过程包括快速和慢速路径,处理路由记录和确认ARP缓存的有效性。错误处理包括确认缓存和UDP套接字状态的更新。
数据被封装到skb中,经过ip_make_skb函数的复杂处理,包括UFO和SG支持,以及对发送缓冲大小的管理。如果有错误,错误计数会相应增加。最后,udp_send_skb将skb发送到IP协议层,更新发送统计信息。
为了监控和调优UDP性能,可以通过/proc/net/snmp和/proc/net/udp查看统计文件。系统参数如net.core.wmem_max可以调整发送缓冲大小,以优化网络性能。通过本文,我们深入了解了UDP数据包发送的底层机制,后续将探讨IP协议层的处理。
拓展资源:欲了解更多内核技术,欢迎加入技术交流群,获取学习资料和内核技术分享。直达链接:Linux内核技术交流群,以及内核源码学习路线、视频教程和代码资料。
深度解析Linux内核协议栈探索网络通信的奥秘linux内核协议栈
近年来,随着互联网的普及和信息技术的不断发展,网络通信的重要性日益凸显。而在网络通信中,协议栈是至关重要的环节。Linux内核协议栈作为Linux操作系统的核心组成部分之一,是实现网络通信的关键组件。本文将详细介绍Linux内核协议栈的结构和工作原理,探索网络通信的奥秘。
一、Linux内核协议栈的结构
Linux内核协议栈主要由五个层次组成,分别是应用层、传输层、网络层、数据链路层和物理层。
(1)应用层
应用层是网络通信的最上层。在Linux内核中,应用层由一系列协议组成,例如HTTP协议、FTP协议、SMTP协议等。这些协议负责处理应用程序与网络的交互过程,将应用程序发送的数据转化为可传输的网络数据包。
(2)传输层
传输层是应用层下的一个子层。在Linux内核中,传输层主要由TCP协议和UDP协议组成。它们负责将应用程序转化的数据传输到网络层。
(3)网络层
网络层是传输层下的一个子层。在Linux内核中,网络层由IP协议、ICMP协议和IGMP协议组成。网络层负责将传输层传输的数据包进行路由和寻址,保证数据包传输到达目的地址。
(4)数据链路层
数据链路层是网络层下的一个子层。在Linux内核中,数据链路层由以太网协议、ARP协议和RARP协议组成。数据链路层负责将网络层传输的数据包进行分段和组装,以及实现局域网内计算机之间的通信。
(5)物理层
物理层是整个协议栈中最底层的一层。物理层负责将数字信号转化为模拟信号,通过物理媒介(例如光缆或者电缆)进行传输。
二、Linux内核协议栈的工作原理
Linux内核协议栈中各个层次之间的数据传输是通过TCP/IP协议进行的。当应用程序需要发送数据时,会将数据打包成数据包,然后通过传输层的TCP或UDP协议进行传输。传输层将数据包进行封装并加入TCP或UDP头部信息后,将数据包传输到网络层。在网络层,数据包的IP地址和端口号信息被加入IP头部,同时添加了用于检验数据完整性的校验和。然后,数据包被传输到数据链路层进行分段和组装,最终通过物理层传输到接收方计算机。
需要注意的是,Linux内核协议栈中的每个层次都需要进行协议处理和数据加工,这个过程需要消耗大量的系统资源。因此,在进行网络通信时,需要科学地配置协议栈,保证数据的快速传输。
三、深入学习Linux内核协议栈的方法
如果想要深入学习Linux内核协议栈,你需要掌握以下知识点:
(1)Linux内核协议栈的结构和工作原理;
(2)TCP/IP协议的基本原理和应用场景;
(3)Linux操作系统的基本知识和网络编程技能。
此外,还可以通过阅读相关的书籍和博客,以及参与开源社区中的Linux内核开发实践来深入学习。
参考文献:
1. Linux内核源代码解析.卷2:进程,内存和文件系统;
2. TCP/IP详解。
文章讲了怎样深入学习Linux内核协议栈,以及Linux内核协议栈的结构和工作原理,探索网络通信的奥秘。
深入理解kubernetes(k8s)网络原理之五-flannel原理
flannel在Kubernetes(k8s)网络架构中扮演着关键角色,其提供多种网络模式,其中最为广泛应用的是VXLAN模式。本文旨在深入探讨VXLAN模式下flannel的运作原理,同时对UDP模式进行简要介绍。
VXLAN模式下的flannel依赖于VXLAN协议,实现跨主机Pod间的通信。这种模式下,flannel的组件工作流程涉及多个关键步骤。首先,flannel-cni文件作为CNI规范下的二进制文件,负责生成配置文件并调用其它CNI插件(如bridge和host-local),从而实现主机到主机的网络互通。flannel-cni文件并非flannel项目源码,而是位于CNI的plugins中。
在flannel-cni工作流程中,kubelet在创建Pod时,会启动一个pause容器,并获取网络命名空间。随后,它调用配置文件指定的CNI插件(即flannel),以加载相关参数。flannel读取从/subnet.env文件获取的节点子网信息,生成符合CNI标准的配置文件。接着,flannel利用此配置文件调用bridge插件,完成Pod到主机、同主机Pod间的数据通信。
kube-flannel作为Kubernetes的daemonset运行,主要负责跨节点Pod通信的编织工作。它完成的主要任务包括为每个节点创建VXLAN设备,并更新主机路由。当节点添加或移除时,kube-flannel会相应地调整网络配置。在VXLAN模式下,每个节点上的kube-flannel会与flanneld守护进程进行通信,以同步路由信息。
在UDP模式下,每个节点运行flanneld守护进程,参与数据包转发。flanneld通过Unix域套接字与本地flanneld通信,而非通过fdb表和邻居表同步路由信息。当节点新增时,kube-flannel会在节点间建立路由条目,并调整网络配置以确保通信的连续性。
flannel在0.9.0版本前,使用不同策略处理VXLAN封包过程中可能缺少的ARP记录和fdb记录。从0.9.0版本开始,flannel不再监听netlink消息,优化了内核态与用户态的交互,从而提升性能。
通过理解flannel的运行机制,可以发现它在VXLAN模式下实现了高效的跨节点Pod通信。flannel挂载情况不影响现有Pod的通信,但新节点或新Pod的加入需flannel参与网络配置。本文最后提示读者,了解flannel原理后,可尝试自行开发CNI插件。
2024-11-30 18:43
2024-11-30 18:09
2024-11-30 17:22
2024-11-30 16:59
2024-11-30 16:36