Linux内核网络层分析

        网络访问层仍受到传输介质的性质以及相关适配器的设备驱动程序的影响很大。网络层与网络适配器的硬件性质几乎完全分离。为什么说几乎?因为该层不仅负责发送和接收数据,还负责在彼此不直接连接的系统之间转发和路由分组。查找最佳路由并选择适当的网络设备来发送分组,也涉及对底层地址族的处理(如特定硬件MAC地址)。

网络访问层(数据链路 + 物理层)直接受限于传输介质(如光纤、双绞线)和硬件驱动程序(如网卡驱动)。

网络层理论上独立于硬件,但实际需要处理路由、地址解析等功能,这些功能间接依赖硬件地址(如 MAC 地址)。

  1. 路由决策需考虑硬件限制

    • 例如:路由器选择路径时需考虑链路带宽、MTU(最大传输单元)等物理层属性。
  2. 地址解析依赖硬件地址

    • 网络层使用 IP 地址,但实际传输需通过 ARP(地址解析协议)将 IP 映射到 MAC 地址(硬件地址)。
    • 例子:当主机 A(IP: 192.168.1.1,MAC: 00:01)要向主机 B(IP: 192.168.1.2)发送数据时,需先通过 ARP 获取主机 B 的 MAC 地址。
  3. 特定硬件功能的利用

    • 某些网络层协议(如 IPv6 邻居发现)依赖 ICMPv6 消息,而 ICMPv6 的传输仍需通过数据链路层。

 

1.ipv4的首部

其中各字段如下:

1. 版本(Version)

  • 定义:4 位字段(之所以不是3位,是为了未来版本以及字段对齐),标识 IP 协议版本,目前常用值为 4(IPv4)或 6(IPv6)。
  • 例子:家庭宽带路由器分配的设备 IP 地址若为 192.168.1.100,属于 IPv4 协议,该字段值为 4;若设备使用 IPv6 地址 2001:db8::1,则版本字段值为 6

2. IP 首部长度(IHL,Internet Header Length)

  • 定义:4 位字段,以 32 位(4 字节)为单位表示首部长度。无选项时,标准 IP 首部为 20 字节,对应 IHL 值为 5(5×4 字节 = 20 字节);若有选项,需重新计算。ip首部最小20字节,最大60字节。
  • 例子:某 IP 分组首部含 20 字节固定部分 + 4 字节选项,总长度 24 字节,IHL 值为 6(6×4 字节 = 24 字节)。

3. 代码点 / 服务类型(DS,Differentiated Services)

  • 定义:8 位字段,前 6 位为区分服务代码点(DSCP),用于标记流量优先级;后 2 位保留。例如视频会议、语音通话等实时业务可设置高优先级。
  • 例子:视频直播流量为确保流畅,网络设备可将其 DS 字段设置为 46(二进制 101110),标记为 “快速转发” 类服务。

4. 总长度(Total Length)

  • 定义:16 位字段,标识 IP 分组整体长度(首部 + 数据部分),单位为字节,最大值 65535 字节。
  • 例子:IP 首部 20 字节,携带 1000 字节网页数据,总长度字段值为 1020(20 + 1000 = 1020)。

5. 分片标识(Identification)

  • 定义:16 位字段,发送方为每个 IP 分组分配唯一标识,用于分片重组。同一原始分组的所有分片共享相同标识。
  • 例子:发送一个 3000 字节的文件,因链路 MTU 限制需分片。所有分片的 “分片标识” 字段均为发送方生成的同一值(如 12345),接收方通过该标识识别属于同一分组的分片。

6. 标志(Flags)

  • 定义:3 位字段,常用标志位:
    • 第 1 位(保留);
    • 第 2 位(DF,Don’t Fragment):置 1 表示禁止分片;
    • 第 3 位(MF,More Fragments):置 1 表示后续还有分片。
  • 例子:某视频流数据要求不被分片,发送方将标志字段设为 010(二进制),其中 DF 位为 1,网络设备若无法传输则丢弃该分组并反馈错误。

7. 分片偏移量(Fragment Offset)

  • 定义:13 位字段,标识分片在原始分组中的位置,单位为 8 字节块。接收方通过该值重组分片。
  • 例子:某分片携带的数据从原始分组第 1000 字节开始,分片偏移量为 125(1000 ÷ 8 = 125),表示该分片在重组时应放在 125×8 = 1000 字节的位置。

8. TTL(生存时间,Time to Live)

  • 定义:8 位字段,限制 IP 分组转发跳数。每经一个路由器,TTL 减 1;减至 0 时,分组被丢弃。
  • 例子:主机发送 TTL 为 64 的分组,经过 10 个路由器后,TTL 变为 54;若第 64 跳仍未到达目标,分组会被第 64 个路由器丢弃。

9. 协议(Protocol)

  • 定义:8 位字段,标识 IP 分组承载的上层协议。常见值:6(TCP)、17(UDP)、1(ICMP)。
  • 例子:访问网页时,浏览器通过 TCP 协议传输数据,IP 首部协议字段值为 6;使用 ping 命令时,基于 ICMP 协议,协议字段值为 1

10. 首部检验和(Header Checksum)

  • 定义:16 位字段,通过计算 IP 首部二进制反码和校验首部传输错误,不校验数据部分。
  • 例子:发送方计算首部字节的反码和,存入该字段;接收方重新计算,若结果与字段值不符,说明首部在传输中出错,丢弃该分组。

11. 源地址、目标地址

  • 定义:各 32 位字段,分别标识发送方和接收方的 IP 地址。
  • 例子:用户电脑(IP:192.168.1.100)向服务器(IP:114.114.114.114)发送请求,IP 首部源地址填 192.168.1.100,目标地址填 114.114.114.114

12. 选项(Options)

  • 定义:可变长字段(最长 40 字节),用于扩展功能,如记录路由、时间戳、源路由等。
  • 例子:网络调试时,使用 “记录路由” 选项,让经过的路由器记录自身 IP,帮助分析路径。

13. 填充(Padding)

  • 定义:若干 0 比特,确保 IP 首部总长度是 32 位(4 字节)的整数倍,满足首部长度对齐要求。
  • 例子:若选项长度使首部总长度为 22 字节,需添加 2 字节填充(补 0),使首部长度变为 24 字节(6×4 字节)。

2.在Linux源码中ip首部由iphdr数据结构进行实现

  • version:对应 IP 首部的 “版本” 字段,标识 IPv4 协议(值固定为 4)。
  • ihl:对应 “IP 首部长度(IHL)”,以 4 字节为单位表示首部长度。
  • tos:对应 “代码点 / 服务类型”,用于标记流量优先级等服务类型。
  • tot_len:对应 “总长度”,表示 IP 分组(首部 + 数据)的总字节数。
  • id:对应 “分片标识”,用于分片后重组数据。
  • frag_off:融合 “标志” 和 “分片偏移量” 功能,既包含分片标志(如是否允许分片),也记录分片偏移位置。
  • ttl:对应 “生存时间”,限制 IP 分组的转发跳数。
  • protocol:对应 “协议” 字段,标识上层协议(如 TCP、UDP)。
  • check:对应 “首部检验和”,用于校验 IP 首部传输是否出错。
  • saddr/daddr:分别对应 “源地址” 和 “目标地址”,存储 32 位 IPv4 地址。

3.分组穿过网络层的路线

        ip_rcv是网络层的入口点,分组向上穿过内核的过程如下:

一、分组接收路径

  1. 底层接收
    分组通过物理网络接口(如以太网)进入主机,借助 轮询机制(检测是否有数据到达),首先进入网络层的分组接收函数 ip_rcv,这是网络层处理接收分组的入口。

  2. 预处理钩子(NF_IP_PRE_ROUTING)
    分组进入 ip_rcv 后,触发 Netfilter 的 NF_IP_PRE_ROUTING 钩子。此处可对分组进行过滤、修改(如防火墙规则检查),是网络层接收分组后的第一处自定义处理点。

  3. 路由决策
    经过预处理的分组进入 路由模块,核心逻辑是判断分组的目标地址:

    • 本地交付:若目标地址是本机 IP,分组进入 ip_local_deliver,将分组提交给 传输层(TCP/UDP)处理。
    • 转发:若目标地址非本机 IP,分组需转发。此时触发 Netfilter 的 NF_IP_FORWARD 钩子,再次进行过滤(如检查是否允许转发),然后进入转发流程。

二、分组发送路径

  1. 传输层到网络层的发送
    传输层(TCP/UDP)生成的分组进入网络层发送函数 ip_queue_xmit,这里负责管理发送队列,为分组添加 IP 首部等操作。

  2. 输出预处理钩子(NF_IP_LOCAL_OUT)
    ip_queue_xmit 触发 Netfilter 的 NF_IP_LOCAL_OUT 钩子,用于本地生成的分组(非转发分组)的过滤与修改(如标记特殊流量)。

  3. 路由与输出处理

    • 分组经过 路由模块,确定从哪个网络接口发送(路由表查询)。
    • 进入 ip_output,完成分组的最终封装(如分片处理),准备发送。
  4. 底层发送
    最终,分组通过 dev_queue_xmit 交给物理网络接口(如以太网),完成从网络层到数据链路层的传递,实现物理发送。

完整流程示例:本地主机发送HTTP请求

  1. 应用层:浏览器调用write()发送HTTP请求。

  2. 传输层:内核TCP模块封装数据,生成TCP段。

  3. LOCAL_OUT钩子:iptables检查OUTPUT链规则。

  4. 路由决策:查询路由表确定出口网卡(如eth0)。

  5. POST_ROUTING钩子:应用SNAT规则,修改源IP为公网地址。

  6. 网络接口层:添加以太网头部,通过dev_queue_xmit发送到eth0。

  7. 物理链路:网卡将数据帧转换为电信号发送到路由器。

一、接收分组以及分组转发

1.接收分组

        在分组转发到 ip_rcv 之后,必须检查接收到的信息确保它是正确的。主要检查计算的校验和与首部中存储的校验和是否一致。其他的检查包括分组是否达到了 IP 首部的最小长度,分组的协议是否确实是 IPv4。

        在进行检查之后,内核并不立即继续对分组的处理,而是调用一个 netfilter 挂钩,使得用户空间可以对分组数据进行操作。netfilter 挂钩插入到内核源代码中定义好的各个位置,使得分组能够被外部动态操作。

         ip_rcv()函数入口---->调用一个netfilter挂钩---->ip_route_input()负责选择路由(判断路由的结果是选择一个函数进行下一步分组处理)---->选择可用的函数ip_local_deliver()和ip_forward()。具体选择哪个函数,取决于分组时交付到本地计算机下一个更高协议例程还是转发到网络中的另一个主机或网络设备

数据报在Linux内核中netfilter处理过程,有不同HOOK点(挂钩):

1. 本地产生数据且目标为本地的流程

  • 来源与目标:数据由本地应用层产生(如本地运行的网络调试工具发送测试数据),目标地址为本机 IP。
  • 处理逻辑
    • 应用层 → 传输层:应用层数据交给传输层(TCP 或 UDP),添加传输层首部(如端口号)。
    • 传输层 → 网络层:传输层数据传递到网络层,网络层添加 IP 首部(源 IP 为本机,目标 IP 也为本机),生成 IP 数据报。
    • 网络层处理:
      • 触发 NF_IP_LOCAL_OUT 钩子(如标记本地流量)。
      • 路由选择判定为本地交付,直接将数据报传递给上层协议(传输层),无需经过 NF_IP_POST_ROUTING 钩子。

2. 本地产生数据但需转发出去的流程

  • 来源与目标:数据由本地应用层产生(如本地代理工具转发外部请求),但目标地址非本机 IP,需转发到其他网络设备。
  • 处理逻辑
    • 应用层 → 传输层:应用层数据交给传输层(TCP 或 UDP),添加传输层首部。
    • 传输层 → 网络层:传输层数据传递到网络层,网络层添加 IP 首部(源 IP 为本机,目标 IP 为外部地址),生成 IP 数据报。
    • 网络层处理:
      • 触发 NF_IP_LOCAL_OUT 钩子(自定义处理,如流量标记)。
      • 经路由选择确定为转发,触发 NF_IP_POST_ROUTING 钩子,将数据报发送到数据链路层,经物理网络转发到下一跳设备。

3. 外部进来需转发出去的流程

  • 来源与目标:数据报来自外部网络,经网络接口进入本机,目标地址非本机 IP。
  • 处理逻辑
    • 数据报进入内核后,先经过 NF_IP_PRE_ROUTING 钩子(预处理,如过滤)。
    • 路由模块判定为转发,触发 NF_IP_FORWARD 钩子(检查转发规则)。
    • 最后通过 NF_IP_POST_ROUTING 钩子,发送到下一跳网络设备。

4. 外部进来且目标为本机的流程

  • 来源与目标:数据报来自外部网络,经网络接口进入本机,目标地址为本机 IP。
  • 处理逻辑
    • 数据报进入内核后,先进行 IP 检验(验证首部有效性)。
    • 触发 NF_IP_PRE_ROUTING 钩子(如防火墙规则检查)。
    • 路由模块判定为本地交付,触发 NF_IP_LOCAL_IN 钩子(用户空间自定义操作,如安全检查)。
    • 最终将数据报传递给上层协议(传输层),由对应协议(如 TCP、UDP)处理后续逻辑。

2.ip_rcv函数整体调用栈      

         当收到ip数据报的时候,调用ip_rcv函数进行处理,检查ip数据报并通过Netfilter钩子处理之后交给ip_rcv_finish进行后续的处理。ip_rcv_finish 函数主要完成 IP 数据包接收后的一些收尾处理,包括调用 l3mdev_ip_rcv 处理可能存在的 L3 主设备情况,然后调用 ip_rcv_finish_core 进行核心的路由和统计更新等操作,最后根据处理结果调用 dst_input。

        在 ip_rcv_finish 函数中,如果 ip_rcv_finish_core 的返回值不是 NET_RX_DROP,就会调用 dst_input(skb)dst_input 是一个通用的数据包输入处理函数,它会根据 skb 中的目的信息(存储在 skb_dst(skb) 中)调用相应的输入函数。以下是这个处理函数被设置的时机:当 arp_rcv 函数调用 arp_process 处理 ARP 数据包时,arp_process 会通过 ip_route_input_noref () 触发路由选择子系统执行查找。该过程会判断数据包目的地址属性:若为本地地址,最终通过 skb_dst_set (skb, &rt->dst) 完成 skb 与路由结果 dst 的关联,此时 dst 的 input 函数指针会被设置为 ip_local_deliver,用于本地协议栈交付;若目的地址非本地(需转发),该 input 函数指针则会被设置为 ip_forward,为后续数据包转发处理做准备。这一机制不仅适用于 ARP 场景,更是 IP 层处理所有数据包路由走向(本地接收或转发)的通用逻辑。

        当 skb_dst(skb)->input 为 ip_local_deliver 时,就会调用 ip_local_deliver 函数,将数据包递交给本地协议栈进行进一步处理 。ip_local_deliver 函数主要处理 IP 分片重组,并再次调用 Netfilter 的 NF_INET_LOCAL_IN 钩子函数。如果数据包通过了这些钩子函数的检查,最终会调用 ip_local_deliver_finish。

调用流程总结

下面是从 ip_rcv_finish 到 ip_local_deliver 再到 ip_local_deliver_finish 的完整调用流程:

  1. ip_rcv_finish 调用 ip_rcv_finish_core 完成核心的路由和统计更新等操作。
  2. 如果 ip_rcv_finish_core 处理成功,ip_rcv_finish 调用 dst_input(skb)
  3. dst_input(skb) 根据 skb_dst(skb)->input 调用 ip_local_deliver
  4. ip_local_deliver 处理 IP 分片重组并调用 Netfilter 的 NF_INET_LOCAL_IN 钩子函数。
  5. 如果数据包通过 Netfilter 检查,NF_HOOK 调用 ip_local_deliver_finish 完成最终的本地交付处理。

        更加详细的流程请参考以下文章:
内核arp_rcv函数到ip_local_deliver_finish的具体调用流程-CSDN博客

3.具体ip_rcv(...)函数分析

/** IP receive entry point*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
{//从网络设备 dev 中提取其所属的 网络命名空间(Network Namespace)。//Linux内核通过命名空间实现容器化网络隔离,每个命名空间有独立的网络栈(IP地址、路由表等)struct net *net = dev_net(dev);/*对IP数据包进行初步处理,包括:长度检查:确保数据包长度不小于IP头部长度。版本检查:验证 iph->version 是否为IPv4(值为 4)。校验和验证:计算IP头部校验和,若错误则丢弃。处理IP选项:若IP头包含选项(如记录路由、时间戳),解析并处理。数据包规范化:可能对数据包进行重新封装或调整(如移除填充字节)。*/skb = ip_rcv_core(skb, net);if (skb == NULL)return NET_RX_DROP;/*NFPROTO_IPV4:指定协议族为 IPv4。NF_INET_PRE_ROUTING:指定钩子点为 PRE_ROUTING,即在路由决策之前处理数据包。net:网络命名空间。skb:指向数据包的 sk_buff 指针。dev:接收数据包的网络设备。ip_rcv_finish:如果 Netfilter 钩子处理通过,数据包将被传递给 ip_rcv_finish 函数进行后续的        处理,如路由决策等。*/return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
}

参数说明:

  • struct sk_buff *skbsk_buff 是 Linux 内核中用于存储网络数据包的核心数据结构,也被称为套接字缓冲区。它包含了数据包的实际内容以及相关的控制信息,如源地址、目的地址、协议类型等。skb 指针指向接收到的数据包。
  • struct net_device *devnet_device 结构体表示网络设备,如以太网网卡、无线网卡等。dev 指向接收该数据包的网络设备。
  • struct packet_type *ptpacket_type 结构体用于描述网络数据包的类型信息,包括协议类型、处理函数等。pt 指向与该数据包类型相关的结构体。
  • struct net_device *orig_dev:表示原始的网络设备,通常在数据包经过桥接等处理时使用,这里用于标识数据包最初进入系统的网络设备。

函数功能:

ip_rcv() 是IPv4协议的数据包接收入口函数,主要完成以下任务:

  1. 协议分发入口
    从网络接口层(如以太网)接收数据包后,根据 packet_type 确认是IPv4协议数据包,进入IP层处理。

  2. 初步处理
    调用 ip_rcv_core() 进行IP头部合法性检查、校验和验证、选项解析等。

  3. Netfilter拦截
    通过 NF_HOOK 触发 NF_INET_PRE_ROUTING 钩子(如iptables的PREROUTING规则),决定是否继续处理或丢弃数据包。

  4. 路由决策
    最终调用 ip_rcv_finish() 完成路由选择(转发或本地处理)。

4.具体ip_rcv_core(...)代码讲解

函数参数

  • skb:指向 struct sk_buff 类型的指针,sk_buff 是 Linux 内核中用于表示网络数据包的结构体,它包含了数据包的各种信息,如数据、头部信息、网络设备信息等。
  • net:指向 struct net 类型的指针,net 结构体代表一个网络命名空间,用于隔离不同的网络环境。

函数功能

ip_rcv_core 是 Linux 内核中处理 IP 数据包接收的核心函数。它负责对接收到的 IP 数据包进行基本的合法性检查,如数据包类型、IP 头部长度、版本号、校验和等,若检查通过则对数据包进行一些必要的处理,如调整数据包长度、设置传输层头部指针等,最后返回处理后的 sk_buff 结构体指针;若检查不通过,则丢弃该数据包并返回 NULL

内部重要逻辑部分

1. 检查数据包类型
if (skb->pkt_type == PACKET_OTHERHOST)goto drop;

当网络接口处于混杂模式时,会接收到不属于本机的数据包。若数据包类型为 PACKET_OTHERHOST,表示该数据包是发给其他主机的,直接跳转到 drop 标签处丢弃该数据包。

2. 增加接收统计信
__IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len);

更新网络命名空间 net 中的接收统计信息,记录接收到的数据包的总长度。

3. 检查数据包共享情况
skb = skb_share_check(skb, GFP_ATOMIC);
if (!skb) {__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);goto out;
}

skb_share_check 函数用于检查 skb 是否被共享,若共享则复制一份。若复制失败,增加接收丢弃统计信息并跳转到 out 标签处返回 NULL

4. 检查 IP 头部长度
if (!pskb_may_pull(skb, sizeof(struct iphdr)))goto inhdr_error;
iph = ip_hdr(skb);

pskb_may_pull 函数用于检查 skb 中是否有足够的数据来容纳一个 IP 头部。若不足,跳转到 inhdr_error 标签处增加头部错误统计信息并丢弃数据包。若足够,则获取 IP 头部指针 iph

5. 检查 IP 头部基本信息
if (iph->ihl < 5 || iph->version != 4)goto inhdr_error;

检查 IP 头部长度字段 ihl 是否至少为 5(表示 IP 头部至少有 20 字节),以及 IP 版本号是否为 4(IPv4)。若不满足条件,跳转到 inhdr_error 标签处处理。

6. 增加 ECN 统计信
BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_1);
BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_0);
BUILD_BUG_ON(IPSTATS_MIB_CEPKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_CE);
__IP_ADD_STATS(net,IPSTATS_MIB_NOECTPKTS + (iph->tos & INET_ECN_MASK),max_t(unsigned short, 1, skb_shinfo(skb)->gso_segs));

根据 IP 头部的服务类型字段 tos 中的显式拥塞通知(ECN)信息,更新网络命名空间 net 中的相应统计信息。

7. 再次检查 IP 头部长度
if (!pskb_may_pull(skb, iph->ihl*4))goto inhdr_error;
iph = ip_hdr(skb);

再次检查 skb 中是否有足够的数据来容纳完整的 IP 头部(ihl * 4 字节),若不足则跳转到 inhdr_error 标签处处理。

8. 检查 IP 头部校验和
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))goto csum_error;

ip_fast_csum 函数用于快速计算 IP 头部的校验和。若校验和不通过,跳转到 csum_error 标签处增加校验和错误统计信息并丢弃数据包。

9. 检查数据包总长度
len = ntohs(iph->tot_len);
if (skb->len < len) {__IP_INC_STATS(net, IPSTATS_MIB_INTRUNCATEDPKTS);goto drop;
} else if (len < (iph->ihl*4))goto inhdr_error;

将 IP 头部中的总长度字段 tot_len 从网络字节序转换为主机字节序。若 skb 的实际长度小于总长度,说明数据包被截断,增加截断数据包统计信息并丢弃该数据包;若总长度小于 IP 头部长度,跳转到 inhdr_error 标签处处理。

10. 调整数据包长度
if (pskb_trim_rcsum(skb, len)) {__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);goto drop;
}

pskb_trim_rcsum 函数用于将 skb 的长度调整为 IP 头部指定的总长度,并更新校验和。若调整失败,增加接收丢弃统计信息并丢弃该数据包。

11. 设置传输层头部指针
iph = ip_hdr(skb);
skb->transport_header = skb->network_header + iph->ihl*4;

更新 IP 头部指针,并设置传输层头部指针为 IP 头部之后的位置。

12. 清空套接字控制块信息
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
IPCB(skb)->iif = skb->skb_iif;

清空 skb 中的套接字控制块信息,并设置输入接口信息。

13. 孤儿化 skb
skb_orphan(skb);

由于 TProxy 的原因,将 skb 从当前套接字中分离出来。

孤儿化的含义

skb_orphan 函数的作用是将 skb 从当前关联的套接字中分离出来,使其成为一个 “孤儿” 数据包。在 Linux 内核网络子系统中,一个 skb 通常会与某个套接字(struct sock)相关联,这种关联用于管理数据包的发送和接收。

与 TProxy 的关系

TProxy(Transparent Proxy)是一种透明代理技术,它允许在不修改客户端和服务器之间的通信的情况下对数据包进行拦截和转发。在 TProxy 场景下,数据包可能需要被重新路由或者转发到其他地方,此时就需要将数据包从原来的套接字关联中分离出来,以避免干扰后续的处理流程。通过调用 skb_orphan 函数,可以确保 skb 不再与原来的套接字有任何关联,从而可以独立地进行后续的处理,例如重新路由到代理服务器或者进行其他形式的转发。

14. 返回处理后的 skb

return skb;

若所有检查和处理都通过,返回处理后的 skb 结构体指针。

错误处理
csum_error:__IP_INC_STATS(net, IPSTATS_MIB_CSUMERRORS);
inhdr_error:__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
drop:kfree_skb(skb);
out:return NULL;

在不同的错误情况下,增加相应的错误统计信息,释放 skb 占用的内存,并返回 NULL

        IP 分组可能如上述交付给本地计算机处理,也可能离开互联网络层,转发到另一台计算机,而不牵涉本地计算机的高层协议实例。分组的目标地址分为以下两类:
(1)目标计算机在某个本地网络中,发送计算机与该网络有连接。
(2)目标计算机在地理上属于远程计算机,不连接到本地网络,只能通过网关访问。
第二种场景更复杂,首先需找到剩余路由中的第一个站点,将分组转发到该站点(这是向最终目标地址的第一步传输)。因此,不仅需要计算机所属本地网络结构的相关信息,还需要相邻网络结构和相关外出路径的信息。

1. ip_rcv(IP 层接收入口)
  • 作用:接收网络设备驱动层传来的 IP 分组,进行校验和、版本等基础检查。
  • 关键逻辑:判断分组是否属于本地(通过查找本地 IP 地址),若不属于本地,则进入转发流程,调用 ip_forward
2. ip_forward(IP 转发主函数)
  • 作用:处理非本地接收的 IP 分组,执行转发策略检查(如转发是否允许)、TTL 递减等操作。
  • 关键逻辑:通过路由查找函数 fib_lookup 确定转发路径,若找到合适路由,调用 ip_forward_finish 继续处理。
3. fib_lookup(路由查找)
  • 作用:根据目标 IP 地址,在路由表(FIB,转发信息库)中查找转发路径,确定下一跳网关和出接口。
  • 关键逻辑:匹配路由表项,获取下一跳地址和输出网络设备。
4. ip_forward_finish(转发收尾处理)
  • 作用:完成分组转发前的最后处理,如更新 IP 头部校验和、设置链路层头部等。
  • 关键逻辑:调用 __ip_forward_skbuff 处理分组,最终通过 dev_queue_xmit 将分组发送到输出网络设备。
5. dev_queue_xmit(设备层发送)
  • 作用:将处理好的分组交给网络设备驱动,完成物理层发送。
  • 关键逻辑:触发设备驱动的发送函数,将分组通过物理链路发送到下一跳。

 5.ip_forward(...)

        通过网关访问:相邻网络结构和外出路径信息,该信息由路由表提供,路由表由内核通过多种数据结构实现并管理,在接收分组时调用ip_route_input函数充当路由实现的接口,一方面因为该函数能够识别出分组时交付到本地还是转发出去,另一方面因为该函数能够找到通向目标地址的路由。我们所说的目标地址存储在套接字缓冲区的dst字段地址。

ip_forward代码流程如下:

        其主要功能:根据报文信息得到路由、ipset安全监测、转发的基本逻辑,ip层提交本地处理流程等,根据ip地址决定是提交给本地处理(ip_local_deliver),还是报文转发(ip_forward)。

二、发送分组

1.ip_queue_xmit(...)

        内核提供几个通过网络层发送数据的函数,可以由较高协议层使用。其中ip_queue_xmit是最常使用的,代码流程如下:

ip_queue_xmit() 是 IP 层的通用数据包发送函数,负责所有 IP 数据包的封装、分片和发送,与上层协议是否面向连接无关。其核心逻辑如下:

  1. 路由缓存优化

    • 无论套接字是否面向连接(如 TCP/UDP),内核会缓存最近使用的路由信息(dst_cache)。
    • 对于已连接的 TCP 套接字,目的地址固定,可直接复用缓存路由,减少路由查询开销。
    • 对于未连接的 UDP 套接字(未调用 connect),每次发送需实时查询路由(除非缓存命中)。
  2. IP 层处理流程

    • 构建 IP 头部(源 / 目的 IP、协议号、TTL 等)。
    • 调用 Netfilter 钩子(如 local_outpost_routing)进行包过滤。
    • 若数据包超过 MTU,调用 ip_fragment 分片。
    • 将数据包传递给链路层发送(dev_queue_xmit)。
  3. 协议无关性

    • TCP:通过 tcp_transmit_skb() 调用 ip_queue_xmit(),依赖路由缓存提升效率。
    • UDP:通过 udp_sendmsg() → ip_send_skb() 调用 ip_queue_xmit(),分片逻辑与 TCP 一致,但无连接状态管理。

调用完ip_queue_xmit函数后,通常会按以下流程继续处理:

  1. netfilter 钩子处理ip_queue_xmit完成其工作(比如构建 IP 报头)后,会调用netfilterlocal_out钩子进行本地输出的数据包过滤。之后还会调用netfilterpost_routing钩子 ,进一步对即将离开主机的数据包进行过滤和处理,只有通过这些钩子检查的数据包才会继续后续流程。
  2. 路由操作:进行输出路由操作,确定数据包的下一跳地址和出接口等信息。
  3. 数据包分段(分片):调用ip_output函数,在此函数中会判断数据包是否需要分片(相对的重组的函数是ip_rcv中的ip_defrag)。如果数据包大小超过了下一跳链路的最大传输单元(MTU),就会调用ip_fragment函数进行分片处理 ,将大数据包拆分成多个小的分片以便传输。
  4. 链路层处理:调用链路层发送函数(如dev_queue_xmit),将处理好的数据包(可能是原包,也可能是分片后的包)传递给数据链路层。数据链路层会进行帧封装等操作,然后通过网络设备驱动程序将数据发送到物理链路。
  5. 队列处理与发送:数据进入设备的发送队列(qdisc,队列调度器),如default_qdisc,然后根据队列算法进行处理。设备驱动程序会将数据包排入硬件的发送缓冲区(如ringbuffer tx) 。
  6. 硬件传输:网络接口卡(NIC)从内存中获取数据包(通过直接内存访问,DMA),并将其发送到物理网络上。传输完成后,NIC 会发出硬件中断信号表示传输结束,驱动程序处理该中断,并进行后续的资源管理和处理操作 。

用户层的tcp的api函数send 函数与 ip_queue_xmit 的关系:

用户层的 send 函数是应用层 API,其底层实现会通过系统调用(如 write 或 sendto)进入内核空间。以 TCP 为例:

  1. 传输层处理send 会调用 TCP 层的发送函数(如 tcp_sendmsg),处理 TCP 协议相关逻辑(如分段、序号管理、滑动窗口等)。
  2. IP 层调用:TCP 层处理完数据后,会调用 IP 层函数(如 ip_queue_xmit),由 IP 层负责构建 IP 包头、路由查找、分片等操作。

2.ip_output(...) 

1. Netfilter 钩子处理

  • 调用 netfilter 框架的 NF_IP_POST_ROUTING 钩子,允许防火墙、流量监控等模块对即将离开主机的 IP 数据包进行过滤、修改等操作,实现网络安全策略或流量控制。

2. 分片决策与处理

  • 判断数据包是否需要分片:
    • 若数据包大小超过输出接口的最大传输单元(MTU),调用 ip_fragment 对数据包进行分片,确保分片后的数据包符合链路层传输要求。
    • 若无需分片,直接进入后续流程。

3. 数据包发送准备

  • 调用 ip_finish_output 和 ip_finish_output2 函数,完成 IP 层对数据包的最终处理,包括:
    • 检查数据包是否有足够空间容纳链路层硬件首部(如以太网首部),若不足,通过 skp_realloc_headroom 调整缓冲区空间。
    • 最终通过 dst->neighbour->output 调用邻居子系统(Neighbor Subsystem)的发送函数,将数据包交付给数据链路层,由链路层完成帧封装并通过物理网络发送。

4. 衔接网络层与链路层

作为 IP 层向网络访问层(链路层)的过渡函数,ip_output 整合了 IP 层的路由、分片逻辑,最终将处理好的数据包移交链路层,实现从网络层到物理传输的关键跳转。

 

 

 3.路由

        在任何 IP 实现中,不仅在转发外部分组时需要,而且也用于发送本地计算机产生的分组。
每个接收到的分组属于 3 个类别之一:其目标是本地主机;其目标是当前主机直接连接的计算机;其目标是远程计算机,只能经由中间系统到达。
        路由结果关联到一个套接字缓冲区,套接字缓冲区的 dst 成员指向一个 dest_entry 结构的实例,此实例的内容是在路由查找期间填充的,具体内核代码如下:

         struct neighbour成员存储计算机在本地网络中的IP和硬件地址,这样就可以通过网络访问层直接到达。neighbour实例由内核中实现ARP的ARP层创建,ARP协议负责将IP地址转为硬件地址(MAC地址).

一、dst_entry 结构体使用案例

场景:本地主机通过以太网发送 UDP 数据包到远程服务器(192.168.1.100),路由查找后 dst_entry 结构体的典型应用流程如下:

  1. 应用层触发发送
    用户空间调用 sendto 发送 UDP 数据,内核通过系统调用进入 udp_sendmsg,开始构建数据包。

  2. 路由查找填充 dst_entry

  • 调用路由查找函数(如 ip_route_output_ports),根据目标 IP(192.168.1.100)查找路由表(1)
  • 找到匹配路由规则后,创建或获取 dst_entry 实例:
    struct dst_entry *dst;  
    // 路由查找示例(简化逻辑)  
    dst = ip_route_output_ports(net, sk, saddr, daddr, tos, dev, NULL, NULL, OIF);  
    
  • 填充 dst_entry 成员:
    • dev:指定输出网络设备(如 eth0)。
    • ops:关联操作函数集(如 inet_ops,包含 output 函数指针,指向 ip_output)。
  1. 基于 dst_entry 发送数据包
  • 调用 dst->ops->output(即 ip_output),将数据包交给 IP 层处理:
    dst->ops->output(net, sk, skb);  
    
  • 后续流程:IP 层分片、调用 Netfilter 钩子,最终通过 dst->deveth0)发送数据。

二、dst_entry 与其他结构体的交互逻辑

  1. 与 net_device 的交互
  • dst_entry->dev 指向输出网络设备(如 eth0),决定数据包从哪个物理接口发送。
  • 示例:发送数据包前,通过 dst->dev 获取接口 MTU,判断是否需要分片。
  1. 与 dst_ops 的交互
  • dst_entry->ops 是操作函数集,定义关键行为(如 output 函数)。
  • 交互示例:
    struct dst_ops inet_ops = {  .output = ip_output,  // 关联 IP 层发送函数  // 其他函数(如释放资源)  
    };  
    dst->ops = &inet_ops;  
    
    通过 dst->ops->output 触发 IP 层处理流程。
  1. 与 neighbour 结构体的交互
  • 目标:解析链路层地址(如 MAC 地址)。
  • 流程
    1. dst_entry 关联的路由确定下一跳 IP 后,通过邻居子系统(neighbour)解析下一跳的 MAC 地址。
    2. 例如,ARP 模块创建 neighbour 实例,查询 ARP 表获取 MAC 地址,填充到链路层首部,最终通过 dst->dev 发送数据。

 上述内容补充:

(1)这个路由表查询的是什么?

1. 典型路由表条目数据

假设系统路由表中有如下条目:

字段示例数据说明
目的网络192.168.1.0目标网络地址(子网)
子网掩码255.255.255.0用于判断目标 IP 是否属于该子网
下一跳 IP192.168.1.1数据包转发的下一跳地址(若目标在子网外,通常为网关)
出接口eth0数据包从该网络接口发送
路由优先级(管理距离)100多路由匹配时,优先级高的规则优先使用

2. 匹配过程

  • 子网匹配
    目标 IP 192.168.1.100 与子网掩码 255.255.255.0 按位与,得到网络地址 192.168.1.0,与路由表中目的网络 192.168.1.0 匹配。
  • 结果输出
    匹配成功后,路由查找函数获取该条目中的 下一跳 IP(192.168.1.1 和 出接口(eth0,填充到 dst_entry 结构体,用于后续数据包转发。

 (2)当通过路由表确定下一条地址的时候,下一步要查询arp表,如果不存在,要发送arp请求,那么这个arp请求是广播吗?然后一定跟本机在同一子网吗?

当通过路由表确定下一跳地址后,若 ARP 表中没有对应的映射条目,通常会发送 ARP 请求,这个 ARP 请求一般是广播形式 ,但也有特殊情况;而下一跳地址不一定与本机在同一子网,具体说明如下:

ARP 请求是否为广播

  • 通常为广播形式:在常见的网络环境中,当设备需要解析某个 IP 地址对应的 MAC 地址,且 ARP 表中没有相关记录时,会以广播的形式发送 ARP 请求。广播的目的 MAC 地址是全 F(例如在以太网中为 FF - FF - FF - FF - FF - FF),这样同一个广播域内的所有设备都会收到该请求。只有目标 IP 地址匹配的设备会回复 ARP 响应,告知自身的 MAC 地址。
  • 特殊情况:也存在一些特殊情况不是以广播形式发送 ARP 请求。例如在支持 IPV6 的网络中,使用邻居发现协议(NDP)替代 ARP,NDP 使用组播地址代替广播;另外在一些启用了代理 ARP 功能的网络场景中,ARP 请求的发送和处理方式会有所变化,代理 ARP 设备会代替目标设备响应 ARP 请求,减少广播流量。

下一跳地址与本机是否在同一子网

  • 可能在同一子网:当目标 IP 地址与本机在同一子网时,通过路由表确定的下一跳地址就是目标主机的 IP 地址,此时下一跳地址与本机在同一子网。例如,在一个小型局域网中,两台主机进行通信,它们的 IP 地址都在 192.168.1.0/24 网段内,数据可以直接在子网内传输,下一跳就是目标主机 ,和本机处于同一子网。
  • 可能不在同一子网:若目标 IP 地址与本机不在同一子网,此时通过路由表确定的下一跳地址通常是网关(路由器)的 IP 地址,用于将数据包转发到其他子网或网络。这种情况下,下一跳地址(网关)和本机不一定在同一子网。比如,主机 A 的 IP 地址是 192.168.1.10,要访问的目标服务器 IP 地址是 192.168.2.50,它们属于不同子网,主机 A 会将数据包发送给网关(如 192.168.1.1) ,由网关进行后续转发,该网关就和主机 A 不在同一个目标子网内。

(3) 当目标与本机不在同一子网,还需要arp协议找到本地网关的MAC地址吗?

ARP 协议的作用

        ARP(Address Resolution Protocol)即地址解析协议,其主要功能是将 IP 地址解析为对应的 MAC 地址。在以太网等局域网环境中,数据链路层的帧传输需要知道目标设备的 MAC 地址,而网络层的通信使用的是 IP 地址,因此需要通过 ARP 协议来完成这两种地址之间的映射。

通信流程及 ARP 协议的使用

        当主机 A(IP 地址为 192.168.1.10)要访问目标服务器(IP 地址为 192.168.2.50)时,由于它们处于不同子网,主机 A 会根据路由表确定下一跳地址为网关(如 192.168.1.1)。接下来的步骤如下:

  1. ARP 表检查:主机 A 首先会检查自己的 ARP 表,看是否已经存在网关(192.168.1.1)对应的 MAC 地址记录。
  2. ARP 请求发送:如果 ARP 表中没有该记录,主机 A 会以广播的形式发送一个 ARP 请求。这个 ARP 请求的目标 IP 地址是网关的 IP 地址(192.168.1.1),请求内容是询问拥有该 IP 地址的设备的 MAC 地址是什么。
  3. ARP 响应接收:网关设备接收到这个 ARP 请求后,会识别出目标 IP 地址是自己的,于是会发送一个 ARP 响应给主机 A,响应中包含了网关自己的 MAC 地址。
  4. ARP 表更新:主机 A 接收到网关的 ARP 响应后,会将网关的 IP 地址和对应的 MAC 地址记录到自己的 ARP 表中,以便后续通信使用。
  5. 数据帧封装与发送:主机 A 在知道了网关的 MAC 地址后,会将数据封装成以太网帧,其中目的 MAC 地址设置为网关的 MAC 地址,源 MAC 地址设置为自己的 MAC 地址,然后将该帧通过物理网络发送给网关。网关接收到帧后,会根据自身的路由表将数据包转发到目标服务器所在的子网。

必要性分析

        网络层的数据包最终要通过数据链路层进行传输,而数据链路层的帧必须明确目标设备的 MAC 地址才能正确发送。虽然主机 A 知道了下一跳的 IP 地址(网关的 IP 地址),但在数据链路层通信时需要对应的 MAC 地址,因此必须使用 ARP 协议来解析网关的 IP 地址对应的 MAC 地址,这样才能保证数据包能够正确地从主机 A 传输到网关,进而由网关转发到目标服务器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/34384.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenHarmony子系统开发 - Rust编译构建指导

OpenHarmony子系统开发 - Rust编译构建指导 一、Rust模块配置规则和指导 概述 Rust是一门静态强类型语言&#xff0c;具有更安全的内存管理、更好的运行性能、原生支持多线程开发等优势。Rust官方也使用Cargo工具来专门为Rust代码创建工程和构建编译。 OpenHarmony为了集成C…

分享一个免费的CKA认证学习资料

关于CKA考试 CKA&#xff08;Certified Kubernetes Administrator&#xff09;是CNCF基金会&#xff08;Cloud Native Computing Foundation&#xff09;官方推出的Kubernetes管理员认证计划&#xff0c;用于证明持有人有履行Kubernetes管理的知识&#xff0c;技能等相关的能力…

MySQL的一些八股文

1.什么是BufferPool&#xff1f; Buffer Pool基本概念 Buffer Pool&#xff1a;缓冲池&#xff0c;简称BP。其作用是用来缓存表数据与索引数据&#xff0c;减少磁盘IO操作&#xff0c;提升效率。 Buffer Pool由缓存数据页(Page) 和 对缓存数据页进行描述的控制块 组成, 控制…

卷积神经网络(笔记02)

一、简述在卷积神经网络中池化层的作用&#xff0c;并解释其为何能帮助提高模型性能 。 池化层的作用 1. 降低数据维度 池化操作通过对输入特征图进行下采样&#xff0c;减少特征图的空间尺寸。常见的池化方式有最大池化&#xff08;Max Pooling&#xff09;和平均池化&…

面试系列|蚂蚁金服技术面【1】

哈喽&#xff0c;大家好&#xff01;今天分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经&#xff0c;复盘面试过程中踩过的坑&#xff0c;整理面试过程中提到的知识点&#xff0c;希望能给正在准备面试的你一些参考和启发&#xff0c;希望对你有帮助&#xff0c;愿你能够获…

带环链表的相关知识点

带环链表的相关知识点 1.判断是否有环2.寻找入环节点补充&#xff1a;相交链表 如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;索引从 0 开…

初探 Threejs 物理引擎CANNON,解锁 3D 动态魅力

简介 Cannon.js 是一个基于 JavaScript 的物理引擎&#xff0c;它可以在浏览器中模拟物理效果。它支持碰撞检测、刚体动力学、约束等物理效果&#xff0c;可以用于创建逼真的物理场景和交互。 参考文档 官方示例 原理 Cannon.js 使用了欧拉角来表示物体的旋转&#xff0c;…

【小沐学Web3D】three.js 加载三维模型(React)

文章目录 1、简介1.1 three.js1.2 react.js 2、three.js React结语 1、简介 1.1 three.js Three.js 是一款 webGL&#xff08;3D绘图标准&#xff09;引擎&#xff0c;可以运行于所有支持 webGL 的浏览器。Three.js 封装了 webGL 底层的 API &#xff0c;为我们提供了高级的…

简述计算机网络中的七层模型和四层模型

在计算机网络中&#xff0c;网络协议栈的设计通常采用分层结构来处理不同的通信任务。常见的分层结构有OSI七层模型和TCP/IP四层模型。虽然它们的层次数量不同&#xff0c;但本质上都在解决如何有效地进行计算机间通信。本文将分别介绍这两种结构的功能和各层的协议。 一、OSI七…

在 CentOS 上安装 Oracle 数据库

文章目录 **1. 系统准备****1.1 检查系统要求****1.2 更新系统****1.3 安装必要的依赖包****1.4 创建 Oracle 用户和组****1.5 配置内核参数****1.6 配置用户限制****1.7 配置 PAM 模块****1.8 创建 Oracle 安装目录** **2. 下载 Oracle 数据库安装包****2.1 访问 Oracle 官方网…

掌握这些 UI 交互设计原则,提升产品易用性

在当今数字化时代&#xff0c;用户对于产品的体验要求越来越高&#xff0c;UI 交互设计成为决定产品成败的关键因素之一。一个易用的产品能够让用户轻松、高效地完成各种操作&#xff0c;而实现这一目标的核心在于遵循一系列科学合理的 UI 交互设计原则。本文将详细阐述简洁性、…

创新实践分享:基于边缘智能+扣子的智能取物机器人解决方案

在 2024 年全国大学生物联网设计竞赛中&#xff0c;火山引擎作为支持企业&#xff0c;不仅参与了赛道的命题设计&#xff0c;还为参赛队伍提供了相关的硬件和软件支持。以边缘智能和扣子的联合应用为核心&#xff0c;参赛者们在这场竞赛中展现出了卓越的创新性和实用性&#xf…

Python----数据可视化(Pyecharts一:介绍安装,全局配置,系列配置)

一、PyEcharts介绍 1.1、概况 Echarts 是一个由百度开源的数据可视化&#xff0c;凭借着良好的交互性&#xff0c;精巧的图表设计&#xff0c;得到了众多开发者的认可。而 Python 是一门富有表达力的语言&#xff0c;很适合用于数据处理。当数据分析遇上数据可视化时&#xff…

Cursor初体验:excel转成CANoe的vsysvar文件

今天公司大佬先锋们给培训了cursor的使用&#xff0c;还给注册了官方账号&#xff01;跃跃欲试&#xff0c;但是测试任务好重&#xff0c;结合第三方工具开发也是没有头绪。 但巧的是&#xff0c;刚好下午有同事有个需求&#xff0c;想要把一个几千行的excel转成canoe的系统变…

【3DGS】SuperSplat本地运行+修改监听端口+导入ply模型+修剪模型+在线渲染3DGS网站推荐

SuperSplat官网代码&#xff1a;https://github.com/playcanvas/supersplat 本地安装和运行 Clone the repository: git clone https://github.com/playcanvas/supersplat.git cd supersplat Install dependencies: npm install Build SuperSplat and start a local web ser…

MySQL中的B+树索引经验总结

一、什么是B树 B树是一种二叉树&#xff0c;由二叉查找树&#xff0c;平衡二叉树&#xff0c;B树演化而来。 请看上图 B树的特点&#xff1a; 1&#xff09;非叶子节点不存放数据&#xff0c;只存放键值&#xff0c;数据都存放在叶子节点中。 2&#xff09;叶子节点都在同一…

C# NX二次开发:在多个体的模型中如何实现拉伸操作布尔减

大家好&#xff0c;今天接着上一篇拉伸文章去讲。 UF_MODL_create_extruded1 (view source) uf_list_p_tobjectsInputList of objects to be extruded.char *taper_angleInputTaper angle (in degrees).char *limit [ 2 ]InputLimit of extrusion. This is declared as: char …

【深度学习】多源物料融合算法(一):量纲对齐常见方法

目录 一、引言 二、量纲对齐常见方法 2.1 Z-score标准化Sigmoid归一化 2.2 Min-Max 归一化 2.3 Rank Transformation 2.4 Log Transformation 2.5 Robust Scaling 3、总结 一、引言 类似抖音、快手、小红书等产品的信息流推荐业务&#xff0c;主要通过信息流广告、信…

前端高级CSS用法

前端高级CSS用法 在前端开发中&#xff0c;CSS&#xff08;层叠样式表&#xff09;不仅是用来控制网页的外观和布局&#xff0c;更是实现复杂交互和动态效果的关键技术之一。随着前端技术的不断发展&#xff0c;CSS的用法也日益丰富和高级。本文将深入探讨前端高级CSS的用法&a…

How to install a package in offline scenario in Ubuntu 24.04

概述 做过信创项目的兄弟们在工作上每天可能面对很多需要解决的问题&#xff0c;不过&#xff0c;有一类问题可能是大家经常遇的&#xff0c;比方说&#xff0c;有时候我们不得不硬着头皮在离线生产环境中安装某些软件包&#xff0c;相信很多兄弟被这种细碎的小事搞得焦头烂额…