主要参考了《深入Linux内核架构》和《精通Linux内核网络》相关章节
文章目录
网络层
网络访问层仍然受到传输介质的性质以及相关适配器的设备驱动程序的很大影响。网络层(具体地说是IP协议)与网络适配器的硬件性质几乎是完全分离的。
为什么说是几乎?读者稍后会看到,该层不仅负责发送和接收数据,还负责在彼此不直接连接的系统之间转发和路由分组。查找最佳路由并选择适当的网络设备来发送分组,也涉及对底层地址族的处理(如特定于硬件的MAC地址),这是该层至少要与网卡松散关联的原因。在网络层地址和网络访问层之间的指派是由这一层完成的,这也是互联网络层无法与硬件完全脱离的原因。
**如果不考虑底层硬件,是无法将较大的分组分割为较小单位的(事实上,硬件的性质是需要分割分组的首要原因)。**因为每一种传输技术所支持的分组长度都有一个最大值,IP协议必须方法将较大的分组划分为较小的单位,由接收方重新组合,更高层协议不会注意到这一点。划分后分组的长度取决于特定传输协议的能力。
一个标准的以太网数据帧最大为1518,其中源MAC 6字节,目的MAC 6个字节,Type 2个字节,FCS 4个字节(前导码不算在内,在物理层就已经去掉了),6+6+2+4=18个字节,1518-18=1500,这1500正好是是留给上层协议传输的大小,也就是我们说的数据帧的大小是1500个字节,包括IP头部以及上层协议与数据整体在内,也就是说在二层以太网中,实际能传输的数据是1500个字节。
Maximum Transmission Unit,MTU,最大传输单元。网络层最大管控值。
Maximum Segment Size,MSS, 最大报文段长度。传输层最大管控值。
为了照顾网络延迟,引入一个差不多大小的MTU来限制单个IP包的大小,而为了让IP层少分包或是不分包(因为IP分包中间丢了一个就得整个重传,而TCP分包只需要重传丢的那一个),传输层引入小于MTU的MSS来限制单个TCP包的大小。
IP数据报首部的格式。
首部最小20个字节,最大60个字节。最小时就是只有固定部分(每个单位32bit,也就是4个字节,共5行,就是20个字节),一个单位指的是一行。
- version(版本)指定了所用IP协议的版本。当前,该字段的有效值为4或6。 在支持两种协议版本的主机上,所使用的版本由前一章讨论的传输协议标识符确定。对协议的两个版本来说,该标识符中保存的值是不同的。
- IHL(IP首部长度)定义了首部的长度,由于选项数量可变,这个值并不总是相同的。
- Codepoint(代码点)或Type of Service(服务类型)用于更复杂的协议选项,我们在这里无须关注。
- Length(长度)指定了分组的总长度,即首部加数据的长度。
- **fragment ID(分片标识)标识了一个分片的IP分组的各个部分。**分片方法将同一分片ID指定到同一原始分组的各个数据片,使之可标识为同一分组的成员。各个部分的相对位置由fragment offset(分片偏移量)字段定义。偏移量的单位是64 bit。
- 有3个状态标志位用于启用或禁用特定的特性,目前只使用其中两个。
- DF意为“don’t fragment”,即指定分组不可拆分为更小的单位。
- MF表示当前分组是一个更大分组的分片,后面还有其他分片(除了最后一个分片之外,所有分片都会设置该标志位)。第三个标志位“保留供未来使用”,但考虑到IPv6的存在,这是不太可能的。
- TTL意为“Time to Live”,指定了从发送者到接收者的传输路径上中间站点的最大数目(或跳数)。
- Protocol标识了IP分组承载的高层协议(传输层)。例如,TCP和UDP协议都有对应的唯一值。
- Checksum包含了一个校验和,根据首部和数据的内容计算。如果指定的校验和与接收方计算的值不一致,那么可能发生了传输错误,应该丢弃该分组。
- src和dest指定了源和目标的32位IP地址。
- options用于扩展IP选项,在这里不讨论了。
- data保存了分组数据(净荷)。
IP首部中所有的数值都以网络字节序存储(大端序)。在内核源代码中,该首部由iphdr数据结构实现:
TTL 定义的本意是数据包的存活时间,以秒为单位,但实际应用中大多被作为数据包可通过的网络数(或者叫跳数,或者叫路由数)比如说我们用ping或tracert的时候,ttl就是跳数,但在ip层对数据包进行分段和重组时,TTL 就表示实际时间了,在收到第一个分段时以其TTL开始计时,在定时器过期之前分段没有全部到达,则接收方会发送含有代码为1(分段重组超时)的ICMP消息类型11(超时)要求重传。
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4, // 版本
ihl:4; // 首部长度
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos; // 服务类型
__be16 tot_len; // 总长度
__be16 id;
__be16 frag_off; // 分片偏移量
__u8 ttl; // 存活时间(这是一个路数计数器,每个转发结点都会将ttl减1,当ttl变成o时,将丢弃数据包,并发回一条ICMP消息到源主机,以避免数据包因某种原因而被休止地转发
__u8 protocol; // 传输层协议,如IPPROTO_TCP表示TCP流量,而IPPROTO_UDP表示UDP流量。
__sum16 check;
__be32 saddr;
__be32 daddr;
/*The options start here. */
};
ip_rcv函数是网络层的入口点。分组向上穿过内核的路线如下图所示。
![image-20220714182617084](https://i-blog.csdnimg.cn/blog_migrate/e24a361361489f9a11b2f3746419a572.png)
发送和接收操作的程序流程并不总是分离的,如果分组只通过当前计算机转发,那么发送和接收操作是交织的。这种分组不会传递到更高的协议层(或应用程序),而是立即离开计算机,发往新的目的地。
接收分组及分组转发(ipv4)
ip层收包流程概述:
(1) 在inet_init中注册了类型为ETH_P_IP协议的数据包的回调ip_rcv
(2) 当二层数据包接收完毕,会调用netif_receive_skb根据协议进行向上层分发
(3) 类型为ETH_P_IP类型的数据包,被传递到三层,调用ip_rcv函数
(4) ip_rcv完成基本的校验和处理工作后,经过PRE_ROUTING钩子点
(5) 经过PRE_ROUTING钩子点之后,调用ip_rcv_finish完成数据包接收,包括选项处理,路由查询,并且根据路由决定数据包是发往本机还是转发
数据包在Linux内核中netfilter处理过程,其中有5个HOOK点执行:
- a:数据报从进入系统进行IP检验以后,首先经过第一个HOOK函数NF_IP_PRE_ROUTING进行处理;
- b:然后再进入路由代码,具体决定该数据报是需要转发还是发给本机;
- b1:若数据报是被发到本机,由该数据经过HOOK函数NF_IP_LOCAL_IN处理以的然后传递给上层协议;
- b2:若该数据报应该被转发则它被NF_IP_FORWARD处理;
- c:经过转发的数据报经过最后个HOOK函数NF_IP_POST_ROUTING处理以后,再传输到网络上面;
- d:本地产生的数据经过HOOK函数NF_IP_LOCAL_OUT处理后,进行路由选择处理,然后经过NF_IP_POST_ROUTING处理后发出去。
SNMP 是专门设计用于在 IP 网络管理网络节点(服务器、工作站、路由器、交换机及HUBS等)的一种标准协议,它是一种应用层协议。 SNMP 使网络管理员能够管理网络效能,发现并解决网络问题以及规划网络增长。通过 SNMP 接收随机消息(及事件报告)网络管理系统获知网络出现问题。
关于网络层SNMP统计的信息,也可以通过netstat指令看到统计值信息
接收分组 ip_rcv
这里不打算讨论第三种选项,即通过多播将分组发送到一组计算机
接收分组处理流程
- ip_rcv()函数入口
- 完整性检测
- 校验和(checksum)
- 调用一个netflter挂钩(使得用户空间可以对分组数据进行操作)
- ip_rcv_finish()负责选择路由(判断路由的结果是选择一个函数进行一步分组处理)
- ip _local_deliver() —— 数据包发送目地的为本机
- ip_forward() —— 数据包需要转发
当一个IP包被接收到网卡中,函数ip_rcv()被执行,具体源码如下:
交付到本地传输层
分片合并
由于IP分组可能是分片的,因此会带来一些困难。不见得一定有一个完整的分组可用。该函数的第一项任务,就是通过ip_defrag重新组合分片分组的各个部分。
内核在一个独立的缓存中管理原本属于一个分组的各个分片,该缓存称为分片缓存(fragment cache)。在缓存中,属于同一分组的各个分片保存在一个独立的等待队列中,直至该分组的所有分片都到达。
接下来调用ip_find函数。它使用一个基于分片ID、源地址、目标地址、分组的协议标识的散列过程,检查是否已经为对应的分组创建了等待队列。如果没有,则建立一个新的队列,并将当前处理的分组置于其上。 否则返回现存队列的地址,以便ip_frag_queue将分组置于队列上。②
在分组的所有分片都进入缓存(即第一个和最后一个分片都已经到达,且所有分片中数据的长度之和等于分组预期的总长度)后,ip_frag_reasm将各个分片重新组合起来。接下来释放套接字缓冲区,供其他用途使用。
如果分组的分片尚未全部到达,则ip_defrag返回一个NULL指针,终止互联网络层的分组处理。在所有分片都到达后,将恢复处理。
交付到传输层
如果分组的目的地是本地计算机,ip_local_deliver必须设法找到一个适当的传输层函数,将分组转送过去。IP分组通常对应的传输层协议是TCP或UDP。
ip_local_deliver
/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb->dev);
// 是否分片,若分片需要重组
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
// 调用netfilter挂钩NF_IP_LOCAL_IN(查询是否可接收),恢复在ip_local_deliver_finish函数中的处理。
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
ip_local_deliver_finish
static int ip_local_deliver_finish(struct sock *sk, struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
__skb_pull(skb, skb_network_header_len(skb)); //报文移动到传输层头
rcu_read_lock();
{
int protocol = ip_hdr(skb)->protocol; //得到IP头中的协议类型,即4层协议
const struct net_protocol *ipprot;
int raw;
resubmit:
raw = raw_local_deliver(skb, protocol); //AF_INET的raw sock处理入口
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
int ret;
if (!ipprot->no_policy) { //4.1.12内核中,所有协议的no_policy都为1,条件不成立
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) { //ipsec策略检测
kfree_skb(skb);
goto out;
}
nf_reset(skb);
}
ret = ipprot->handler(skb); //交给上层处理报文,UDP/TCP/ICMP等等
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
} else { //协议未定义
if (!raw) { //如果不是raw,则检测ipse策略,如果检测通过则发送ICMP消息
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb(skb);
} else {
IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}
out:
rcu_read_unlock();
return 0;
}
分组转发
IP分组可能如上所述交付给本地计算机处理,它们也可能离开互联网络层,转发到另一台计算机,而不牵涉本地计算机的高层协议实例。分组的目标地址可分为以下两类。
- (1) 目标计算机在某个本地网络中,发送计算机与该网络有连接。
- (2) 目标计算机在地理上属于远程计算机,不连接到本地网络,只能通过网关访问。
第二种场景要复杂得多。首先必须找到剩余路由中的第一个站点,将分组转发到该站点,这是向最终目标地址的第一步传输。因此,不仅需要计算机所属本地网络结构的相关信息,还需要相邻网络结构和相关的外出路径的信息。
该信息由路由表(routing table)提供,路由表由内核通过多种数据结构实现并管理。在接收分组时调用的ip_route_input函数充当路由实现的接口,这一方面是因为该函数能够识别出分组是交付到本地还是转发出去,另一方面该函数能够找到通向目标地址的路由。目标地址存储在套接字缓冲区的dst字段中。
这使得ip_forward的工作非常容易
![image-20220715102834779](https://i-blog.csdnimg.cn/blog_migrate/b161308de3f5a261f140de8ccd02382d.png)
ip_forward流程中主要功能包括:根据报文信息得到路由、ipset安全检测、转发的基本逻辑(ttl减少、mtu判断等。
-
首先,该函数根据TTL字段来检查当前分组是否允许传输到下一跳。如果TTL值小于或等于1,则丢弃分组,否则,将TTL计数器值减1。
-
ip_decrease_ttl负责该工作,修改TTL字段的同时,分组的校验和也会发生变化,同样需要修改。
在调用netfilter挂钩NF_IP_FORWARD后,内核在ip_forward_finish中恢复处理。该函数将其工作委托给如下两个函数。
- 如果分组包含额外的选项(通常情况下没有),则在ip_forward_options中处理。
- dst_output将分组传递到在路由子系统选择。保存在skb->dst->output中的发送函数通常使用ip_output,该函数将分组传递到与目标地址匹配的网络适配器。下一节描述的IP分组发送操作中,ip_output是其中一部分。
ip_forward
报文转发的入口函数为ip_forward
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
/* that should never happen */
if (skb->pkt_type != PACKET_HOST) //不允许处理非本host的报文,即报文目的mac不是本机
goto drop;
if (unlikely(skb->sk))
goto drop;
if (skb_warn_if_lro(skb)) //报文为非线性,gso_size不为零,但是gso_type为零,丢弃此类报文
goto drop;
if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb)) //ipset安全规则检测
goto drop;
if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
return NET_RX_SUCCESS;
skb_forward_csum(skb);
/*
* According to the RFC, we must first decrease the TTL field. If
* that reaches zero, we must reply an ICMP control message telling
* that the packet's lifetime expired.
*/
if (ip_hdr(skb)->ttl <= 1) //ttl减到0 ,丢弃报文
goto too_many_hops;
if (!xfrm4_route_forward(skb)) //ipset路由安全规则检测,得到路由信息
goto drop;
rt = skb_rtable(skb); //得到路由表项
if (opt->is_strictroute && rt->rt_uses_gateway)
goto sr_failed;
IPCB(skb)->flags |= IPSKB_FORWARDED; //flag中田间forward标记,
mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (!ip_may_fragment(skb) && ip_exceeds_mtu(skb, mtu)) {
IP_INC_STATS(dev_net(rt->dst.dev), IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, //报文长度超过mtu且不允许ip分片,发送icmp消息给发送者
htonl(mtu));
goto drop;
}
/* We are about to mangle packet. Copy it! */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev)+rt->dst.header_len)) //扩展报文,以填充mac头
goto drop;
iph = ip_hdr(skb);
/* Decrease ttl after skb cow done */
ip_decrease_ttl(iph); //ip头的ttl减一
/*
* We now generate an ICMP HOST REDIRECT giving the route
* we calculated.
*/
if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr &&
!skb_sec_path(skb))
ip_rt_send_redirect(skb); //通知发送端,路由重定向
skb->priority = rt_tos2priority(iph->tos); //根据tos值计算priority值
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, NULL, skb,
skb->dev, rt->dst.dev, ip_forward_finish); //调用netfilter,实现iptables功能,通过后调用ip_forward_finish
sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;
too_many_hops:
/* Tell the sender its packet died... */
IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
ip_forward_finish
static int ip_forward_finish(struct sock *sk, struct sk_buff *skb)
{
struct ip_options *opt = &(IPCB(skb)->opt);
IP_INC_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTFORWDATAGRAMS); //报文统计
IP_ADD_STATS_BH(dev_net(skb_dst(skb)->dev), IPSTATS_MIB_OUTOCTETS, skb->len);
if (unlikely(opt->optlen))
ip_forward_options(skb);
skb_sender_cpu_clear(skb);
return dst_output_sk(sk, skb); //此时会进入xfrm4_outpot处理,最终会调用ip_output
}
发送分组ipv4数据包
ip_queue_xmit
-
第一个任务是查找可用于该分组的路由。内核利用了下述事实:起源于同一套接字的所有分组的目标地址都是相同的,这样不必每次都重新确定路由。下文将讨论指向相应数据结构的一个指针,它与套接字数据结构相关联。在发送第一个分组时,内核需要查找一个新的路由(在下文讨论)。
-
在ip_send_check为分组生成校验和之后,②内核调用netfilter挂钩NF_IP_LOCAL_OUT。
-
接下来调用dst_output函数。该函数基于确定路由期间找到的skb->dst->output函数,后者位于套接字缓冲区中,与目标地址相关。通常,该函数指针指向ip_output,本地产生和转发的分组将在该函数中汇集。
ip_queue_xmit是ip层提供给tcp层发送回调,大多数tcp发送都会使用这个回调,tcp层使用tcp_transmit_skb封装了tcp头之后,调用该函数,该函数提供了路由查找校验、封装ip头和ip选项的功能,封装完成之后调用ip_local_out发送数据包;
ip_build_and_send_pkt函数是服务器端在给客户端回复syn+ack时调用的,该函数在构造ip头之后,调用ip_local_out发送数据包;
ip_send_unicast_reply函数目前只用于发送ACK和RST,该函数根据对端发过来的skb构造ip头,然后调用ip_append_data向发送队列中附加/新增数据,最后调用ip_push_pending_frames发送数据包;
/* Note: skb->sk can be different from sk, in case of tunnels */
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
struct ip_options_rcu *inet_opt;
struct flowi4 *fl4;
struct rtable *rt;
struct iphdr *iph;
int res;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
rcu_read_lock();
inet_opt = rcu_dereference(inet->inet_opt);
fl4 = &fl->u.ip4;
/* 获取skb中的路由缓存 */
rt = skb_rtable(skb);
/* skb中有缓存则跳转处理 */
if (rt)
goto packet_routed;
/* Make sure we can route this packet. */
/* 检查控制块中的路由缓存 */
rt = (struct rtable *)__sk_dst_check(sk, 0);
/* 缓存过期 */
if (!rt) {
__be32 daddr;
/* Use correct destination address if we have options. */
/* 目的地址 */
daddr = inet->inet_daddr;
/* 严格路由选项 */
if (inet_opt && inet_opt->opt.srr)
daddr = inet_opt->opt.faddr;
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
/* 查找路由缓存 */
rt = ip_route_output_ports(net, fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS(sk),
sk->sk_bound_dev_if);
/* 失败 */
if (IS_ERR(rt))
goto no_route;
/* 设置控制块的路由缓存 */
sk_setup_caps(sk, &rt->dst);
}
/* 将路由设置到skb中 */
skb_dst_set_noref(skb, &rt->dst);
packet_routed:
/* 严格路由选项 &&使用网关,无路由 */
if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
/* 加入ip头 */
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
skb_reset_network_header(skb);
/* 构造ip头 */
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
/* Transport layer set skb->h.foo itself. */
/* 构造ip选项 */
if (inet_opt && inet_opt->opt.optlen) {
iph->ihl += inet_opt->opt.optlen >> 2;
ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
}
/* 设置id */
ip_select_ident_segs(net, skb, sk,
skb_shinfo(skb)->gso_segs ?: 1);
/* TODO : should we use skb->sk here instead of sk ? */
/* QOS等级 */
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
/* 输出 */
res = ip_local_out(net, sk, skb);
rcu_read_unlock();
return res;
no_route:
/* 无路由处理 */
rcu_read_unlock();
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
ip_output
int ip_output(struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
IP_UPD_PO_STATS(dev_net(dev), IPSTATS_MIB_OUT, skb->len);
skb->dev = dev;
skb->protocol = htons(ETH_P_IP); //设置报文协议为IPV4
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, sk, skb,
NULL, dev,
ip_finish_output, //报文发送netfilter处理,如果允许则调用ip_finish_output
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
ip_finish_output
Generic Segmentation Offload
通用分段延后处理。指网卡在支持GSO功能时,对于超大数据包(大于MTU值),内核会将分段的工作延迟到交给驱动的前一刻。如果网卡不支持此功能,则内核用软件的方式对数据包进行分片。
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
unsigned int mtu;
int ret;
ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb);
if (ret) {
kfree_skb(skb);
return ret;
}
#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
/* Policy lookup after SNAT yielded a new policy */
if (skb_dst(skb)->xfrm) {
IPCB(skb)->flags |= IPSKB_REROUTED;
return dst_output(net, sk, skb);
}
#endif
mtu = ip_skb_dst_mtu(sk, skb);
if (skb_is_gso(skb)) // 如果是gso报文
return ip_finish_output_gso(net, sk, skb, mtu);
// 如不不是gso报文,报文大小超过MTU需要分片
if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
return ip_finish_output2(net, sk, skb); // 直接发送报文
}
分组分片 ip_fragment
ip_fragment将IP分组划分为更小的单位
如果忽略RFC 791中记载的各种微妙情形,那么IP分片是非常简单的。在循环的每一轮中,都抽取出一个数据分片,其长度与对应的MTU兼容。创建一个新套接字缓冲区来保存抽取的数据分片,旧的IP首部可以稍作修改后重用。所有的分片都会分配一个共同的分片ID,以便在目标系统上重新组装分组。分片的顺序基于分片偏移量建立,此时也需要适当地设置。MF(more fragments)标志位也需要设置。只有序列中的最后一个分片可以将该标志位置0。每个分片都在使ip_send_check产生校验和之后,用ip_output发送。
路由选择子系统(概述)
在任何IP实现中,路由都是一个重要的部分,不仅在转发外部分组时需要,而且也用于发送本地计算机产生的分组。查找数据从计算机“外出”的正确路径的问题,不仅在处理非本地地址时会遇到,在本地计算机有几个网络接口时,也会有此类问题。即使只有一个物理上的网络适配器,也可能有环回设备这样的虚拟接口,同样会导致该问题。
每个接收到的分组都属于下列3个类别之一。
-
(1) 其目标是本地主机。
-
(2) 其目标是当前主机直接连接的计算机。
-
(3) 其目标是远程计算机,只能经由中间系统到达。
前一节讨论了第一类分组。这些分组将传递到更高层的协议,进行进一步处理(之所以在下文讨论这一类型,是因为所有到达的分组都会传递到路由子系统)。如果分组的目标系统与本地主机直接连接,路由通常特化为查找对应的网卡。否则,必须根据路由选择信息来查找网关系统(以及与网关相关联的网卡),分组需要通过网关来发送。
随着内核版本的演变,路由的实现逐渐牵涉越来越广泛的内容,现在占网络子系统源代码的很大一部分。**由于许多路由工作都对时间要求很高,因而使用了缓存和冗长的散列表来加速工作。**这反映到路由相关的大量数据结构上。为节省篇幅,这里不去关注诸如在内核数据结构中查找正确路由之类的机制,只考察内核用于传递结果的数据结构。
路由的起始点是ip_route_input函数(static函数),它首先试图在路由缓存中查找路由(这里不讨论该主题,也不涉及多播路由选择的问题)。
ip_route_input_slow用于根据内核的数据结构来建立一个新的路由。基本上,该例程依赖于fib_lookup,后者的隐式返回值(通过一个用作参数的指针)是一个fib_result结构的实例,包含了我们需要的信息。fib代表转发信息库,是一个表,用于管理内核保存的路由选择信息。
路由结果关联到一个套接字缓冲区,套接字缓冲区的dst成员指向一个dest_entry结构的实例,该实例的内容是在路由查找期间填充的。该数据结构的定义如下(简化了很多):
include/net/dst.h
struct dst_entry
{
struct net_device *dev;
int (*input)(struct sk_buff*);
int (*output)(struct sk_buff*);
struct neighbour *neighbour;
};
- input和output分别用于处理进入和外出的分组,如上文所述。
- dev指定了用于处理该分组的网络设备。根据分组的类型,会对input和output指定不同的函数。
- 对需要交付到本地的分组,input设置为ip_local_deliver,而output设置为ip_rt_bug(该函数只向内核日志输出一个错误信息,因为在内核代码中对本地分组调用output是一种错误,不应该发生)。
- 对于需要转发的分组,input设置为ip_forward,而output设置为ip_output函数。
- neighbour成员存储了计算机在本地网络中的IP和硬件地址,这可以通过网络访问层直接到达。对我们来说,只考察该结构的几个成员就足够了:
struct neighbour
{
struct net_device *dev;
unsigned char ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
int (*output)(struct sk_buff *skb);
};
dev保存了网络设备的数据结构,而ha是设备的硬件地址,output是指向适当的内核函数的指针,在通过网络适配器传输分组时必须调用。neighbour实例由内核中实现ARP(address resolution protocol,地址转换协议)的ARP层创建,ARP协议负责将IP地址转换为硬件地址。因为dst_entry结构有一个成员指针指向neighbour实例,网络访问层的代码在分组通过网络适配器离开当前系统时可调用output函数。
tcp/ip 发包流程
tcp/ip 发包,函数调用:从 tcp_connect() 到 __dev_queue_xmit()
tcp_connect()
--> tcp_transmit_skb() # tcp_output.c // actually transmits TCP packets queued in by tcp_do_sendmsg().
--> icsk->icsk_af_ops->queue_xmit # kernel/net/ipv4/tcp_output.c ( .queue_xmit = ip_queue_xmit )
--> ip_queue_xmit() # kernel/net/ipv4/ip_output.c
* --> ip_local_out() # kernel/net/ipv4/ip_output.c
--> __ip_local_out() # kernel/net/ipv4/ip_output.c //调用netfilter,返回值为1说明允许报文通过
--> nf_hook() # kernel/include/linux/netfilter.h
--> nf_hook_thresh() # kernel/include/linux/netfilter.h \
Returns 1 if the hook has allowed the packet to pass.
ip_forward --> dst_output() # kernel/include/net/dst.h
--> skb_dst(skb)->output(net, sk, skb) # kernel/include/net/dst.h // //实际调用 ip_output 函数
* --> ip_output() # kernel/net/ipv4/ip_output.c
--> ip_finish_output() # kernel/net/ipv4/ip_output.c
* --> ip_finish_output2() # kernel/net/ipv4/ip_output.c
* --> dst_neigh_output() # kernel/include/net/dst.h
--> neigh_hh_output() # kernel/include/net/neighbour.h //支持硬件缓存头方法的发送
--> dev_queue_xmit() # kernel/net/core/dev.c
--> n->output() # //不支持硬件缓存头的方法的发送
* --> neigh_resolve_output() # kernel/net/core/neighbour.c
--> neigh_event_send() # kernel/include/net/neighbour.h
--> __neigh_event_send() # kernel/net/core/neighbour.c
* --> neigh_probe() # kernel/net/core/neighbour.c
--> neigh->ops->solicit # 实际调用 arp_solicit 函数,该函数会发送 arp 请求
* --> arp_solicit() # kernel/net/ipv4/arp.c
* --> arp_send_dst() # kernel/net/ipv4/arp.c // Create and send an arp packet.
* --> arp_create() # kernel/net/ipv4/arp.c
* --> arp_xmit() # kernel/net/ipv4/arp.c
* --> arp_xmit_finish() # kernel/net/ipv4/arp.c
* --> dev_queue_xmit() # kernel/net/core/dev.c
--> __dev_queue_xmit() # kernel/net/core/dev.c
--> dev_queue_xmit() # kernel/net/core/dev.c
* --> __dev_queue_xmit() # kernel/net/core/dev.c
* --> __dev_xmit_skb() # kernel/net/core/dev.c
--> neigh_update() # kernel/net/core/neighbour.c
--> neigh_connect(neigh); # 修改output函数为neigh_connected_output
--> n1->output(n1, skb); # 调用neigh的output函数,此时已经改成connect函数
--> neigh_connected_output() # kernel/net/core/neighbour.c
--> dev_queue_xmit() # kernel/net/core/dev.c